From 57e615aa3697c5bed443ab87a4ca3f2b1d537a31 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 23 Jun 2024 00:36:58 +0300 Subject: [PATCH 0001/2411] Don't log Shelly push update failures if there are no errors (#120189) --- homeassistant/components/shelly/coordinator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index f15eca51413..82d358b33d8 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -404,9 +404,10 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): "ip_address": self.device.ip_address, }, ) - LOGGER.debug( - "Push update failures for %s: %s", self.name, self._push_update_failures - ) + if self._push_update_failures: + LOGGER.debug( + "Push update failures for %s: %s", self.name, self._push_update_failures + ) self.async_set_updated_data(None) def async_setup(self, pending_platforms: list[Platform] | None = None) -> None: From ea0c93e3dbf03fea8a3a32cebce56c8e73bf4a46 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Jun 2024 18:11:48 -0500 Subject: [PATCH 0002/2411] Update uiprotect to 3.1.1 (#120173) --- .../components/unifiprotect/__init__.py | 17 ++- .../components/unifiprotect/binary_sensor.py | 3 +- .../components/unifiprotect/const.py | 7 +- homeassistant/components/unifiprotect/data.py | 113 +++++++++--------- .../components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/conftest.py | 8 ++ tests/components/unifiprotect/test_camera.py | 38 +++++- tests/components/unifiprotect/test_init.py | 16 ++- tests/components/unifiprotect/utils.py | 2 + 11 files changed, 123 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 068c5665e6b..394a7f43329 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -6,6 +6,7 @@ from datetime import timedelta import logging from aiohttp.client_exceptions import ServerDisconnectedError +from uiprotect.api import DEVICE_UPDATE_INTERVAL from uiprotect.data import Bootstrap from uiprotect.data.types import FirmwareReleaseChannel from uiprotect.exceptions import ClientError, NotAuthorized @@ -29,7 +30,6 @@ from homeassistant.helpers.typing import ConfigType from .const import ( AUTH_RETRIES, CONF_ALLOW_EA, - DEFAULT_SCAN_INTERVAL, DEVICES_THAT_ADOPT, DOMAIN, MIN_REQUIRED_PROTECT_V, @@ -49,7 +49,7 @@ from .views import ThumbnailProxyView, VideoProxyView _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=DEFAULT_SCAN_INTERVAL) +SCAN_INTERVAL = timedelta(seconds=DEVICE_UPDATE_INTERVAL) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) @@ -70,11 +70,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool: """Set up the UniFi Protect config entries.""" protect = async_create_api_client(hass, entry) _LOGGER.debug("Connect to UniFi Protect") - data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry) try: - bootstrap = await protect.get_bootstrap() - nvr_info = bootstrap.nvr + await protect.update() except NotAuthorized as err: retry_key = f"{entry.entry_id}_auth" retries = hass.data.setdefault(DOMAIN, {}).get(retry_key, 0) @@ -86,6 +84,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool: except (TimeoutError, ClientError, ServerDisconnectedError) as err: raise ConfigEntryNotReady from err + data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry) + bootstrap = protect.bootstrap + nvr_info = bootstrap.nvr auth_user = bootstrap.users.get(bootstrap.auth_user_id) if auth_user and auth_user.cloud_account: ir.async_create_issue( @@ -169,11 +170,7 @@ async def _async_setup_entry( bootstrap: Bootstrap, ) -> None: await async_migrate_data(hass, entry, data_service.api, bootstrap) - - await data_service.async_setup() - if not data_service.last_update_success: - raise ConfigEntryNotReady - + data_service.async_setup() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) hass.http.register_view(ThumbnailProxyView(hass)) hass.http.register_view(VideoProxyView(hass)) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 5596d3b7a62..fb60158580e 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -691,6 +691,7 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): super()._async_update_device_from_protect(device) slot = self._disk.slot self._attr_available = False + available = self.data.last_update_success # should not be possible since it would require user to # _downgrade_ to make ustorage disppear @@ -698,7 +699,7 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): for disk in self.device.system_info.ustorage.disks: if disk.slot == slot: self._disk = disk - self._attr_available = True + self._attr_available = available break self._attr_is_on = not self._disk.is_healthy diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 9839d823585..b56761263f4 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -5,9 +5,9 @@ from uiprotect.data import ModelType, Version from homeassistant.const import Platform DOMAIN = "unifiprotect" -# some UniFi OS consoles have an unknown rate limit on auth -# if rate limit is triggered a 401 is returned -AUTH_RETRIES = 11 # ~12 hours of retries with the last waiting ~6 hours +# If rate limit for 4.x or later a 429 is returned +# so we can use a lower value +AUTH_RETRIES = 2 ATTR_EVENT_SCORE = "event_score" ATTR_EVENT_ID = "event_id" @@ -35,7 +35,6 @@ CONFIG_OPTIONS = [ DEFAULT_PORT = 443 DEFAULT_ATTRIBUTION = "Powered by UniFi Protect Server" DEFAULT_BRAND = "Ubiquiti" -DEFAULT_SCAN_INTERVAL = 60 DEFAULT_VERIFY_SSL = False DEFAULT_MAX_MEDIA = 1000 diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index e3e4cbc7f50..6b502eaa5f3 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -13,7 +13,6 @@ from typing_extensions import Generator from uiprotect import ProtectApiClient from uiprotect.data import ( NVR, - Bootstrap, Camera, Event, EventType, @@ -23,6 +22,7 @@ from uiprotect.data import ( ) from uiprotect.exceptions import ClientError, NotAuthorized from uiprotect.utils import log_event +from uiprotect.websocket import WebsocketState from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -83,8 +83,7 @@ class ProtectData: str, set[Callable[[ProtectDeviceType], None]] ] = defaultdict(set) self._pending_camera_ids: set[str] = set() - self._unsub_interval: CALLBACK_TYPE | None = None - self._unsub_websocket: CALLBACK_TYPE | None = None + self._unsubs: list[CALLBACK_TYPE] = [] self._auth_failures = 0 self.last_update_success = False self.api = protect @@ -115,11 +114,9 @@ class ProtectData: self, device_types: Iterable[ModelType], ignore_unadopted: bool = True ) -> Generator[ProtectAdoptableDeviceModel]: """Get all devices matching types.""" + bootstrap = self.api.bootstrap for device_type in device_types: - devices = async_get_devices_by_type( - self.api.bootstrap, device_type - ).values() - for device in devices: + for device in async_get_devices_by_type(bootstrap, device_type).values(): if ignore_unadopted and not device.is_adopted_by_us: continue yield device @@ -130,33 +127,61 @@ class ProtectData: Generator[Camera], self.get_by_types({ModelType.CAMERA}, ignore_unadopted) ) - async def async_setup(self) -> None: + @callback + def async_setup(self) -> None: """Subscribe and do the refresh.""" - self._unsub_websocket = self.api.subscribe_websocket( - self._async_process_ws_message - ) - await self.async_refresh() + self.last_update_success = True + self._async_update_change(True, force_update=True) + api = self.api + self._unsubs = [ + api.subscribe_websocket_state(self._async_websocket_state_changed), + api.subscribe_websocket(self._async_process_ws_message), + async_track_time_interval( + self._hass, self._async_poll, self._update_interval + ), + ] + + @callback + def _async_websocket_state_changed(self, state: WebsocketState) -> None: + """Handle a change in the websocket state.""" + self._async_update_change(state is WebsocketState.CONNECTED) + + def _async_update_change( + self, + success: bool, + force_update: bool = False, + exception: Exception | None = None, + ) -> None: + """Process a change in update success.""" + was_success = self.last_update_success + self.last_update_success = success + + if not success: + level = logging.ERROR if was_success else logging.DEBUG + title = self._entry.title + _LOGGER.log(level, "%s: Connection lost", title, exc_info=exception) + self._async_process_updates() + return + + self._auth_failures = 0 + if not was_success: + _LOGGER.info("%s: Connection restored", self._entry.title) + self._async_process_updates() + elif force_update: + self._async_process_updates() async def async_stop(self, *args: Any) -> None: """Stop processing data.""" - if self._unsub_websocket: - self._unsub_websocket() - self._unsub_websocket = None - if self._unsub_interval: - self._unsub_interval() - self._unsub_interval = None + for unsub in self._unsubs: + unsub() + self._unsubs.clear() await self.api.async_disconnect_ws() - async def async_refresh(self, *_: Any, force: bool = False) -> None: + async def async_refresh(self) -> None: """Update the data.""" - - # if last update was failure, force until success - if not self.last_update_success: - force = True - try: - updates = await self.api.update(force=force) - except NotAuthorized: + await self.api.update() + except NotAuthorized as ex: if self._auth_failures < AUTH_RETRIES: _LOGGER.exception("Auth error while updating") self._auth_failures += 1 @@ -164,17 +189,11 @@ class ProtectData: await self.async_stop() _LOGGER.exception("Reauthentication required") self._entry.async_start_reauth(self._hass) - self.last_update_success = False - except ClientError: - if self.last_update_success: - _LOGGER.exception("Error while updating") - self.last_update_success = False - # manually trigger update to mark entities unavailable - self._async_process_updates(self.api.bootstrap) + self._async_update_change(False, exception=ex) + except ClientError as ex: + self._async_update_change(False, exception=ex) else: - self.last_update_success = True - self._auth_failures = 0 - self._async_process_updates(updates) + self._async_update_change(True, force_update=True) @callback def async_add_pending_camera_id(self, camera_id: str) -> None: @@ -184,7 +203,6 @@ class ProtectData: initialized yet. Will cause Websocket code to check for channels to be initialized for the camera and issue a dispatch once they do. """ - self._pending_camera_ids.add(camera_id) @callback @@ -278,25 +296,15 @@ class ProtectData: self._async_update_device(new_obj, message.changed_data) @callback - def _async_process_updates(self, updates: Bootstrap | None) -> None: + def _async_process_updates(self) -> None: """Process update from the protect data.""" - - # Websocket connected, use data from it - if updates is None: - return - self._async_signal_device_update(self.api.bootstrap.nvr) for device in self.get_by_types(DEVICES_THAT_ADOPT): self._async_signal_device_update(device) @callback def _async_poll(self, now: datetime) -> None: - """Poll the Protect API. - - If the websocket is connected, most of the time - this will be a no-op. If the websocket is disconnected, - this will trigger a reconnect and refresh. - """ + """Poll the Protect API.""" self._entry.async_create_background_task( self._hass, self.async_refresh(), @@ -309,10 +317,6 @@ class ProtectData: self, mac: str, update_callback: Callable[[ProtectDeviceType], None] ) -> CALLBACK_TYPE: """Add an callback subscriber.""" - if not self._subscriptions: - self._unsub_interval = async_track_time_interval( - self._hass, self._async_poll, self._update_interval - ) self._subscriptions[mac].add(update_callback) return partial(self._async_unsubscribe, mac, update_callback) @@ -324,9 +328,6 @@ class ProtectData: self._subscriptions[mac].remove(update_callback) if not self._subscriptions[mac]: del self._subscriptions[mac] - if not self._subscriptions and self._unsub_interval: - self._unsub_interval() - self._unsub_interval = None @callback def _async_signal_device_update(self, device: ProtectDeviceType) -> None: diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 987329abbba..15b8b5b4a1b 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==1.20.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==3.1.1", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 0d98c4f773c..2343fa9bd4e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2794,7 +2794,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.20.0 +uiprotect==3.1.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ad51c91f9b6..fdd67cf9e29 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2174,7 +2174,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==1.20.0 +uiprotect==3.1.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 6366a4f9244..0bef1ff0eb9 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -29,6 +29,7 @@ from uiprotect.data import ( Viewer, WSSubscriptionMessage, ) +from uiprotect.websocket import WebsocketState from homeassistant.components.unifiprotect.const import DOMAIN from homeassistant.core import HomeAssistant @@ -148,7 +149,14 @@ def mock_entry( ufp.ws_subscription = ws_callback return Mock() + def subscribe_websocket_state( + ws_state_subscription: Callable[[WebsocketState], None], + ) -> Any: + ufp.ws_state_subscription = ws_state_subscription + return Mock() + ufp_client.subscribe_websocket = subscribe + ufp_client.subscribe_websocket_state = subscribe_websocket_state yield ufp diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 444898fbd85..9fedb67fea4 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -4,10 +4,13 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock +from uiprotect.api import DEVICE_UPDATE_INTERVAL from uiprotect.data import Camera as ProtectCamera, CameraChannel, StateType from uiprotect.exceptions import NvrError +from uiprotect.websocket import WebsocketState from homeassistant.components.camera import ( + STATE_IDLE, CameraEntityFeature, async_get_image, async_get_stream_source, @@ -19,13 +22,13 @@ from homeassistant.components.unifiprotect.const import ( ATTR_HEIGHT, ATTR_WIDTH, DEFAULT_ATTRIBUTION, - DEFAULT_SCAN_INTERVAL, ) from homeassistant.components.unifiprotect.utils import get_camera_base_name from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant @@ -377,7 +380,7 @@ async def test_camera_interval_update( ufp.api.bootstrap.cameras = {new_camera.id: new_camera} ufp.api.update = AsyncMock(return_value=ufp.api.bootstrap) - await time_changed(hass, DEFAULT_SCAN_INTERVAL) + await time_changed(hass, DEVICE_UPDATE_INTERVAL) state = hass.states.get(entity_id) assert state and state.state == "recording" @@ -397,19 +400,46 @@ async def test_camera_bad_interval_update( # update fails ufp.api.update = AsyncMock(side_effect=NvrError) - await time_changed(hass, DEFAULT_SCAN_INTERVAL) + await time_changed(hass, DEVICE_UPDATE_INTERVAL) state = hass.states.get(entity_id) assert state and state.state == "unavailable" # next update succeeds ufp.api.update = AsyncMock(return_value=ufp.api.bootstrap) - await time_changed(hass, DEFAULT_SCAN_INTERVAL) + await time_changed(hass, DEVICE_UPDATE_INTERVAL) state = hass.states.get(entity_id) assert state and state.state == "idle" +async def test_camera_websocket_disconnected( + hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera +) -> None: + """Test the websocket gets disconnected and reconnected.""" + + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.CAMERA, 2, 1) + entity_id = "camera.test_camera_high_resolution_channel" + + state = hass.states.get(entity_id) + assert state and state.state == STATE_IDLE + + # websocket disconnects + ufp.ws_state_subscription(WebsocketState.DISCONNECTED) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state and state.state == STATE_UNAVAILABLE + + # websocket reconnects + ufp.ws_state_subscription(WebsocketState.CONNECTED) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state and state.state == STATE_IDLE + + async def test_camera_ws_update( hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera ) -> None: diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 3b75afaace8..46e57c62101 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -5,12 +5,12 @@ from __future__ import annotations from unittest.mock import AsyncMock, patch from uiprotect import NotAuthorized, NvrError, ProtectApiClient +from uiprotect.api import DEVICE_UPDATE_INTERVAL from uiprotect.data import NVR, Bootstrap, CloudAccount, Light from homeassistant.components.unifiprotect.const import ( AUTH_RETRIES, CONF_DISABLE_RTSP, - DEFAULT_SCAN_INTERVAL, DOMAIN, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -116,12 +116,12 @@ async def test_setup_too_old( old_bootstrap = ufp.api.bootstrap.copy() old_bootstrap.nvr = old_nvr - ufp.api.get_bootstrap.return_value = old_bootstrap + ufp.api.update.return_value = old_bootstrap + ufp.api.bootstrap = old_bootstrap await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() assert ufp.entry.state is ConfigEntryState.SETUP_ERROR - assert not ufp.api.update.called async def test_setup_cloud_account( @@ -179,13 +179,13 @@ async def test_setup_failed_update_reauth( # to verify it is not transient ufp.api.update = AsyncMock(side_effect=NotAuthorized) for _ in range(AUTH_RETRIES): - await time_changed(hass, DEFAULT_SCAN_INTERVAL) + await time_changed(hass, DEVICE_UPDATE_INTERVAL) assert len(hass.config_entries.flow._progress) == 0 assert ufp.api.update.call_count == AUTH_RETRIES assert ufp.entry.state is ConfigEntryState.LOADED - await time_changed(hass, DEFAULT_SCAN_INTERVAL) + await time_changed(hass, DEVICE_UPDATE_INTERVAL) assert ufp.api.update.call_count == AUTH_RETRIES + 1 assert len(hass.config_entries.flow._progress) == 1 @@ -193,18 +193,17 @@ async def test_setup_failed_update_reauth( async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture) -> None: """Test setup of unifiprotect entry with generic error.""" - ufp.api.get_bootstrap = AsyncMock(side_effect=NvrError) + ufp.api.update = AsyncMock(side_effect=NvrError) await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() assert ufp.entry.state is ConfigEntryState.SETUP_RETRY - assert not ufp.api.update.called async def test_setup_failed_auth(hass: HomeAssistant, ufp: MockUFPFixture) -> None: """Test setup of unifiprotect entry with unauthorized error after multiple retries.""" - ufp.api.get_bootstrap = AsyncMock(side_effect=NotAuthorized) + ufp.api.update = AsyncMock(side_effect=NotAuthorized) await hass.config_entries.async_setup(ufp.entry.entry_id) assert ufp.entry.state is ConfigEntryState.SETUP_RETRY @@ -215,7 +214,6 @@ async def test_setup_failed_auth(hass: HomeAssistant, ufp: MockUFPFixture) -> No await hass.config_entries.async_reload(ufp.entry.entry_id) assert ufp.entry.state is ConfigEntryState.SETUP_ERROR - assert not ufp.api.update.called async def test_setup_starts_discovery( diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index ab3aefaa09d..21c01f77c5f 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -20,6 +20,7 @@ from uiprotect.data import ( ) from uiprotect.data.bootstrap import ProtectDeviceRef from uiprotect.test_util.anonymize import random_hex +from uiprotect.websocket import WebsocketState from homeassistant.const import Platform from homeassistant.core import HomeAssistant, split_entity_id @@ -38,6 +39,7 @@ class MockUFPFixture: entry: MockConfigEntry api: ProtectApiClient ws_subscription: Callable[[WSSubscriptionMessage], None] | None = None + ws_state_subscription: Callable[[WebsocketState], None] | None = None def ws_msg(self, msg: WSSubscriptionMessage) -> Any: """Emit WS message for testing.""" From 22467cc575c7d837993e39081817fe296401f649 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 22 Jun 2024 18:33:44 -0700 Subject: [PATCH 0003/2411] Avoid Opower time gaps (#117763) * Avoid time gaps * fix mypy * async_get_time_zone --- .../components/opower/coordinator.py | 94 +++++++++++-------- 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index 94a56bb1922..d0795ae4e15 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -29,6 +29,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import dt as dt_util from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN @@ -113,13 +114,17 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): ) if not last_stat: _LOGGER.debug("Updating statistic for the first time") - cost_reads = await self._async_get_all_cost_reads(account) + cost_reads = await self._async_get_cost_reads( + account, self.api.utility.timezone() + ) cost_sum = 0.0 consumption_sum = 0.0 last_stats_time = None else: - cost_reads = await self._async_get_recent_cost_reads( - account, last_stat[cost_statistic_id][0]["start"] + cost_reads = await self._async_get_cost_reads( + account, + self.api.utility.timezone(), + last_stat[cost_statistic_id][0]["start"], ) if not cost_reads: _LOGGER.debug("No recent usage/cost data. Skipping update") @@ -187,59 +192,68 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): self.hass, consumption_metadata, consumption_statistics ) - async def _async_get_all_cost_reads(self, account: Account) -> list[CostRead]: - """Get all cost reads since account activation but at different resolutions depending on age. + async def _async_get_cost_reads( + self, account: Account, time_zone_str: str, start_time: float | None = None + ) -> list[CostRead]: + """Get cost reads. + If start_time is None, get cost reads since account activation, + otherwise since start_time - 30 days to allow corrections in data from utilities + + We read at different resolutions depending on age: - month resolution for all years (since account activation) - day resolution for past 3 years (if account's read resolution supports it) - hour resolution for past 2 months (if account's read resolution supports it) """ - cost_reads = [] - start = None - end = datetime.now() - if account.read_resolution != ReadResolution.BILLING: - end -= timedelta(days=3 * 365) - cost_reads += await self.api.async_get_cost_reads( + def _update_with_finer_cost_reads( + cost_reads: list[CostRead], finer_cost_reads: list[CostRead] + ) -> None: + for i, cost_read in enumerate(cost_reads): + for j, finer_cost_read in enumerate(finer_cost_reads): + if cost_read.start_time == finer_cost_read.start_time: + cost_reads[i:] = finer_cost_reads[j:] + return + if cost_read.end_time == finer_cost_read.start_time: + cost_reads[i + 1 :] = finer_cost_reads[j:] + return + if cost_read.end_time < finer_cost_read.start_time: + break + cost_reads += finer_cost_reads + + tz = await dt_util.async_get_time_zone(time_zone_str) + if start_time is None: + start = None + else: + start = datetime.fromtimestamp(start_time, tz=tz) - timedelta(days=30) + end = dt_util.now(tz) + cost_reads = await self.api.async_get_cost_reads( account, AggregateType.BILL, start, end ) if account.read_resolution == ReadResolution.BILLING: return cost_reads - start = end if not cost_reads else cost_reads[-1].end_time - end = datetime.now() - if account.read_resolution != ReadResolution.DAY: - end -= timedelta(days=2 * 30) - cost_reads += await self.api.async_get_cost_reads( + if start_time is None: + start = end - timedelta(days=3 * 365) + else: + if cost_reads: + start = cost_reads[0].start_time + assert start + start = max(start, end - timedelta(days=3 * 365)) + daily_cost_reads = await self.api.async_get_cost_reads( account, AggregateType.DAY, start, end ) + _update_with_finer_cost_reads(cost_reads, daily_cost_reads) if account.read_resolution == ReadResolution.DAY: return cost_reads - start = end if not cost_reads else cost_reads[-1].end_time - end = datetime.now() - cost_reads += await self.api.async_get_cost_reads( + if start_time is None: + start = end - timedelta(days=2 * 30) + else: + assert start + start = max(start, end - timedelta(days=2 * 30)) + hourly_cost_reads = await self.api.async_get_cost_reads( account, AggregateType.HOUR, start, end ) + _update_with_finer_cost_reads(cost_reads, hourly_cost_reads) return cost_reads - - async def _async_get_recent_cost_reads( - self, account: Account, last_stat_time: float - ) -> list[CostRead]: - """Get cost reads within the past 30 days to allow corrections in data from utilities.""" - if account.read_resolution in [ - ReadResolution.HOUR, - ReadResolution.HALF_HOUR, - ReadResolution.QUARTER_HOUR, - ]: - aggregate_type = AggregateType.HOUR - elif account.read_resolution == ReadResolution.DAY: - aggregate_type = AggregateType.DAY - else: - aggregate_type = AggregateType.BILL - return await self.api.async_get_cost_reads( - account, - aggregate_type, - datetime.fromtimestamp(last_stat_time) - timedelta(days=30), - datetime.now(), - ) From bc45dcbad3bad93cd2cc417b3f51fd679bd0f9d6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 22 Jun 2024 21:51:09 -0400 Subject: [PATCH 0004/2411] Add template config_entry_attr function (#119899) * Template config_entry_attr function * Complete test coverage * Improve readability --- homeassistant/helpers/template.py | 21 +++++++++++++++ tests/helpers/test_template.py | 43 +++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 714a57336bd..cc619e25aed 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1381,6 +1381,24 @@ def device_attr(hass: HomeAssistant, device_or_entity_id: str, attr_name: str) - return getattr(device, attr_name) +def config_entry_attr( + hass: HomeAssistant, config_entry_id_: str, attr_name: str +) -> Any: + """Get config entry specific attribute.""" + if not isinstance(config_entry_id_, str): + raise TemplateError("Must provide a config entry ID") + + if attr_name not in ("domain", "title", "state", "source", "disabled_by"): + raise TemplateError("Invalid config entry attribute") + + config_entry = hass.config_entries.async_get_entry(config_entry_id_) + + if config_entry is None: + return None + + return getattr(config_entry, attr_name) + + def is_device_attr( hass: HomeAssistant, device_or_entity_id: str, attr_name: str, attr_value: Any ) -> bool: @@ -2868,6 +2886,9 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["device_attr"] = hassfunction(device_attr) self.filters["device_attr"] = self.globals["device_attr"] + self.globals["config_entry_attr"] = hassfunction(config_entry_attr) + self.filters["config_entry_attr"] = self.globals["config_entry_attr"] + self.globals["is_device_attr"] = hassfunction(is_device_attr) self.tests["is_device_attr"] = hassfunction(is_device_attr, pass_eval_context) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 26e4f986592..3123c01f500 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -17,6 +17,7 @@ import orjson import pytest import voluptuous as vol +from homeassistant import config_entries from homeassistant.components import group from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -3990,6 +3991,48 @@ async def test_device_attr( assert info.rate_limit is None +async def test_config_entry_attr(hass: HomeAssistant) -> None: + """Test config entry attr.""" + info = { + "domain": "mock_light", + "title": "mock title", + "source": config_entries.SOURCE_BLUETOOTH, + "disabled_by": config_entries.ConfigEntryDisabler.USER, + } + config_entry = MockConfigEntry(**info) + config_entry.add_to_hass(hass) + + info["state"] = config_entries.ConfigEntryState.NOT_LOADED + + for key, value in info.items(): + tpl = template.Template( + "{{ config_entry_attr('" + config_entry.entry_id + "', '" + key + "') }}", + hass, + ) + assert tpl.async_render(parse_result=False) == str(value) + + for config_entry_id, key in ( + (config_entry.entry_id, "invalid_key"), + (56, "domain"), + ): + with pytest.raises(TemplateError): + template.Template( + "{{ config_entry_attr(" + + json.dumps(config_entry_id) + + ", '" + + key + + "') }}", + hass, + ).async_render() + + assert ( + template.Template( + "{{ config_entry_attr('invalid_id', 'domain') }}", hass + ).async_render(parse_result=False) + == "None" + ) + + async def test_issues(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None: """Test issues function.""" # Test no issues From f0d5640f5ddde7a181938fe4e8944fa540b2b9b7 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Sun, 23 Jun 2024 04:22:13 +0200 Subject: [PATCH 0005/2411] Bump pyloadapi to v1.2.0 (#120218) --- homeassistant/components/pyload/manifest.json | 2 +- homeassistant/components/pyload/sensor.py | 14 +++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/pyload/conftest.py | 6 ++++-- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index 90d750ff9b8..2a6e54fdf54 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -6,5 +6,5 @@ "integration_type": "service", "iot_class": "local_polling", "loggers": ["pyloadapi"], - "requirements": ["PyLoadAPI==1.1.0"] + "requirements": ["PyLoadAPI==1.2.0"] } diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index a005f848c37..a0420db819c 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -6,11 +6,15 @@ from datetime import timedelta from enum import StrEnum import logging from time import monotonic -from typing import Any from aiohttp import CookieJar -from pyloadapi.api import PyLoadAPI -from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError +from pyloadapi import ( + CannotConnect, + InvalidAuth, + ParserError, + PyLoadAPI, + StatusServerResponse, +) import voluptuous as vol from homeassistant.components.sensor import ( @@ -132,7 +136,7 @@ class PyLoadSensor(SensorEntity): self.api = api self.entity_description = entity_description self._attr_available = False - self.data: dict[str, Any] = {} + self.data: StatusServerResponse async def async_update(self) -> None: """Update state of sensor.""" @@ -167,7 +171,7 @@ class PyLoadSensor(SensorEntity): self._attr_available = False return else: - self.data = status.to_dict() + self.data = status _LOGGER.debug( "Finished fetching pyload data in %.3f seconds", monotonic() - start, diff --git a/requirements_all.txt b/requirements_all.txt index 2343fa9bd4e..a1ce60412ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -60,7 +60,7 @@ PyFlume==0.6.5 PyFronius==0.7.3 # homeassistant.components.pyload -PyLoadAPI==1.1.0 +PyLoadAPI==1.2.0 # homeassistant.components.mvglive PyMVGLive==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdd67cf9e29..7e1316c8b74 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -51,7 +51,7 @@ PyFlume==0.6.5 PyFronius==0.7.3 # homeassistant.components.pyload -PyLoadAPI==1.1.0 +PyLoadAPI==1.2.0 # homeassistant.components.met_eireann PyMetEireann==2021.8.0 diff --git a/tests/components/pyload/conftest.py b/tests/components/pyload/conftest.py index 67694bcb4b9..53e86639c4a 100644 --- a/tests/components/pyload/conftest.py +++ b/tests/components/pyload/conftest.py @@ -47,7 +47,7 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: ): client = mock_client.return_value client.username = "username" - client.login.return_value = LoginResponse.from_dict( + client.login.return_value = LoginResponse( { "_permanent": True, "authenticated": True, @@ -59,7 +59,8 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: "_flashes": [["message", "Logged in successfully"]], } ) - client.get_status.return_value = StatusServerResponse.from_dict( + + client.get_status.return_value = StatusServerResponse( { "pause": False, "active": 1, @@ -71,5 +72,6 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: "captcha": False, } ) + client.free_space.return_value = 99999999999 yield client From f257fcb0d1f210b50a1a13ca6c565546790a76cd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sun, 23 Jun 2024 10:58:08 +0200 Subject: [PATCH 0006/2411] Bump plugwise to v0.38.3 (#120152) --- homeassistant/components/plugwise/climate.py | 2 +- homeassistant/components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/number.py | 14 +++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../adam_multiple_devices_per_zone/all_data.json | 1 + .../fixtures/anna_heatpump_heating/all_data.json | 1 + .../plugwise/fixtures/m_adam_cooling/all_data.json | 3 ++- .../plugwise/fixtures/m_adam_heating/all_data.json | 3 ++- .../plugwise/fixtures/m_adam_jip/all_data.json | 11 ++--------- .../fixtures/m_anna_heatpump_cooling/all_data.json | 1 + .../fixtures/m_anna_heatpump_idle/all_data.json | 1 + .../fixtures/p1v4_442_single/all_data.json | 1 + .../fixtures/p1v4_442_triple/all_data.json | 1 + .../plugwise/snapshots/test_diagnostics.ambr | 1 + tests/components/plugwise/test_climate.py | 2 +- tests/components/plugwise/test_number.py | 14 +++++++------- 17 files changed, 32 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 006cfbe87da..29d44fe8159 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -155,7 +155,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): if "regulation_modes" in self.gateway_data: hvac_modes.append(HVACMode.OFF) - if self.device["available_schedules"] != ["None"]: + if "available_schedules" in self.device: hvac_modes.append(HVACMode.AUTO) if self.cdr_gateway["cooling_present"]: diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index b1937ee219d..10faf75d0f1 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["plugwise"], - "requirements": ["plugwise==0.37.4.1"], + "requirements": ["plugwise==0.38.3"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index f00b9e38876..c84ca2cf5c7 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -35,8 +35,8 @@ NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="maximum_boiler_temperature", translation_key="maximum_boiler_temperature", - command=lambda api, number, dev_id, value: api.set_number_setpoint( - number, dev_id, value + command=lambda api, dev_id, number, value: api.set_number( + dev_id, number, value ), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, @@ -45,8 +45,8 @@ NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="max_dhw_temperature", translation_key="max_dhw_temperature", - command=lambda api, number, dev_id, value: api.set_number_setpoint( - number, dev_id, value + command=lambda api, dev_id, number, value: api.set_number( + dev_id, number, value ), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, @@ -55,8 +55,8 @@ NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="temperature_offset", translation_key="temperature_offset", - command=lambda api, number, dev_id, value: api.set_temperature_offset( - number, dev_id, value + command=lambda api, dev_id, number, value: api.set_temperature_offset( + dev_id, value ), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, @@ -124,6 +124,6 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Change to the new setpoint value.""" await self.entity_description.command( - self.coordinator.api, self.entity_description.key, self.device_id, value + self.coordinator.api, self.device_id, self.entity_description.key, value ) await self.coordinator.async_request_refresh() diff --git a/requirements_all.txt b/requirements_all.txt index a1ce60412ba..d65267ea5f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1575,7 +1575,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.37.4.1 +plugwise==0.38.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e1316c8b74..02ec3650970 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1258,7 +1258,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.37.4.1 +plugwise==0.38.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json index 47c8e4dceb0..9c17df5072d 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -479,6 +479,7 @@ "warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device." } }, + "reboot": true, "smile_name": "Adam" } } diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json index d496edb4149..5088281404a 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json @@ -99,6 +99,7 @@ "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", "item_count": 66, "notifications": {}, + "reboot": true, "smile_name": "Smile Anna" } } diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json index 6cd3241a637..759d0094dbb 100644 --- a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json @@ -66,7 +66,7 @@ "model": "ThermoTouch", "name": "Anna", "preset_modes": ["no_frost", "asleep", "vacation", "home", "away"], - "select_schedule": "None", + "select_schedule": "off", "sensors": { "setpoint": 23.5, "temperature": 25.8 @@ -165,6 +165,7 @@ "heater_id": "056ee145a816487eaa69243c3280f8bf", "item_count": 147, "notifications": {}, + "reboot": true, "smile_name": "Adam" } } diff --git a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json index 0e9df1a5079..e2c23df42d6 100644 --- a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json @@ -71,7 +71,7 @@ "model": "ThermoTouch", "name": "Anna", "preset_modes": ["no_frost", "asleep", "vacation", "home", "away"], - "select_schedule": "None", + "select_schedule": "off", "sensors": { "setpoint": 20.0, "temperature": 19.1 @@ -164,6 +164,7 @@ "heater_id": "056ee145a816487eaa69243c3280f8bf", "item_count": 147, "notifications": {}, + "reboot": true, "smile_name": "Adam" } } diff --git a/tests/components/plugwise/fixtures/m_adam_jip/all_data.json b/tests/components/plugwise/fixtures/m_adam_jip/all_data.json index 378a5e0a760..7888d777804 100644 --- a/tests/components/plugwise/fixtures/m_adam_jip/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_jip/all_data.json @@ -3,7 +3,6 @@ "1346fbd8498d4dbcab7e18d51b771f3d": { "active_preset": "no_frost", "available": true, - "available_schedules": ["None"], "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", @@ -13,7 +12,6 @@ "model": "Lisa", "name": "Slaapkamer", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "select_schedule": "None", "sensors": { "battery": 92, "setpoint": 13.0, @@ -99,7 +97,6 @@ "6f3e9d7084214c21b9dfa46f6eeb8700": { "active_preset": "home", "available": true, - "available_schedules": ["None"], "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", @@ -109,7 +106,6 @@ "model": "Lisa", "name": "Kinderkamer", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "select_schedule": "None", "sensors": { "battery": 79, "setpoint": 13.0, @@ -156,7 +152,6 @@ "a6abc6a129ee499c88a4d420cc413b47": { "active_preset": "home", "available": true, - "available_schedules": ["None"], "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", @@ -166,7 +161,6 @@ "model": "Lisa", "name": "Logeerkamer", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "select_schedule": "None", "sensors": { "battery": 80, "setpoint": 13.0, @@ -269,7 +263,6 @@ "f61f1a2535f54f52ad006a3d18e459ca": { "active_preset": "home", "available": true, - "available_schedules": ["None"], "control_state": "off", "dev_class": "zone_thermometer", "firmware": "2020-09-01T02:00:00+02:00", @@ -279,7 +272,6 @@ "model": "Jip", "name": "Woonkamer", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "select_schedule": "None", "sensors": { "battery": 100, "humidity": 56.2, @@ -306,8 +298,9 @@ "cooling_present": false, "gateway_id": "b5c2386c6f6342669e50fe49dd05b188", "heater_id": "e4684553153b44afbef2200885f379dc", - "item_count": 221, + "item_count": 213, "notifications": {}, + "reboot": true, "smile_name": "Adam" } } diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json index ef7af8a362b..cb30b919797 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json @@ -99,6 +99,7 @@ "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", "item_count": 66, "notifications": {}, + "reboot": true, "smile_name": "Smile Anna" } } diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json index 8f2e6a75f3f..660f6b5a76b 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json @@ -99,6 +99,7 @@ "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", "item_count": 66, "notifications": {}, + "reboot": true, "smile_name": "Smile Anna" } } diff --git a/tests/components/plugwise/fixtures/p1v4_442_single/all_data.json b/tests/components/plugwise/fixtures/p1v4_442_single/all_data.json index 318035a5d2c..7f152779252 100644 --- a/tests/components/plugwise/fixtures/p1v4_442_single/all_data.json +++ b/tests/components/plugwise/fixtures/p1v4_442_single/all_data.json @@ -44,6 +44,7 @@ "gateway_id": "a455b61e52394b2db5081ce025a430f3", "item_count": 31, "notifications": {}, + "reboot": true, "smile_name": "Smile P1" } } diff --git a/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json b/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json index ecda8049163..582c883a3a7 100644 --- a/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json +++ b/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json @@ -57,6 +57,7 @@ "warning": "The Smile P1 is not connected to a smart meter." } }, + "reboot": true, "smile_name": "Smile P1" } } diff --git a/tests/components/plugwise/snapshots/test_diagnostics.ambr b/tests/components/plugwise/snapshots/test_diagnostics.ambr index 0fa3df4e660..44f4023d014 100644 --- a/tests/components/plugwise/snapshots/test_diagnostics.ambr +++ b/tests/components/plugwise/snapshots/test_diagnostics.ambr @@ -511,6 +511,7 @@ 'warning': "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device.", }), }), + 'reboot': True, 'smile_name': 'Adam', }), }) diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index 5cdc468a957..b3f42031ed8 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -395,7 +395,7 @@ async def test_anna_climate_entity_climate_changes( "c784ee9fdab44e1395b8dee7d7a497d5", "off" ) data = mock_smile_anna.async_update.return_value - data.devices["3cb70739631c4d17a86b8b12e8a5161b"]["available_schedules"] = ["None"] + data.devices["3cb70739631c4d17a86b8b12e8a5161b"].pop("available_schedules") with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): async_fire_time_changed(hass, utcnow() + timedelta(minutes=1)) await hass.async_block_till_done() diff --git a/tests/components/plugwise/test_number.py b/tests/components/plugwise/test_number.py index 6fa65b3e65a..8d49d07b9fb 100644 --- a/tests/components/plugwise/test_number.py +++ b/tests/components/plugwise/test_number.py @@ -36,9 +36,9 @@ async def test_anna_max_boiler_temp_change( blocking=True, ) - assert mock_smile_anna.set_number_setpoint.call_count == 1 - mock_smile_anna.set_number_setpoint.assert_called_with( - "maximum_boiler_temperature", "1cbf783bb11e4a7c8a6843dee3a86927", 65.0 + assert mock_smile_anna.set_number.call_count == 1 + mock_smile_anna.set_number.assert_called_with( + "1cbf783bb11e4a7c8a6843dee3a86927", "maximum_boiler_temperature", 65.0 ) @@ -65,9 +65,9 @@ async def test_adam_dhw_setpoint_change( blocking=True, ) - assert mock_smile_adam_2.set_number_setpoint.call_count == 1 - mock_smile_adam_2.set_number_setpoint.assert_called_with( - "max_dhw_temperature", "056ee145a816487eaa69243c3280f8bf", 55.0 + assert mock_smile_adam_2.set_number.call_count == 1 + mock_smile_adam_2.set_number.assert_called_with( + "056ee145a816487eaa69243c3280f8bf", "max_dhw_temperature", 55.0 ) @@ -99,5 +99,5 @@ async def test_adam_temperature_offset_change( assert mock_smile_adam.set_temperature_offset.call_count == 1 mock_smile_adam.set_temperature_offset.assert_called_with( - "temperature_offset", "6a3bf693d05e48e0b460c815a4fdd09d", 1.0 + "6a3bf693d05e48e0b460c815a4fdd09d", 1.0 ) From 28fb361c64d8466ddd61ea60b27d3ae14b4104e4 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Sun, 23 Jun 2024 12:34:32 +0200 Subject: [PATCH 0007/2411] Add config flow to pyLoad integration (#120135) * Add config flow to pyLoad integration * address issues * remove suggested values * Fix exception * abort import flow on error * readd repair issues on error * fix ruff * changes * changes * exception hints --- homeassistant/components/pyload/__init__.py | 71 +++- .../components/pyload/config_flow.py | 120 ++++++ homeassistant/components/pyload/const.py | 2 + homeassistant/components/pyload/manifest.json | 1 + homeassistant/components/pyload/sensor.py | 94 +++-- homeassistant/components/pyload/strings.json | 44 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- tests/components/pyload/conftest.py | 61 ++- .../pyload/snapshots/test_sensor.ambr | 378 ++++++++++++++++++ tests/components/pyload/test_config_flow.py | 166 ++++++++ tests/components/pyload/test_init.py | 63 +++ tests/components/pyload/test_sensor.py | 150 ++++--- 13 files changed, 1041 insertions(+), 112 deletions(-) create mode 100644 homeassistant/components/pyload/config_flow.py create mode 100644 homeassistant/components/pyload/strings.json create mode 100644 tests/components/pyload/test_config_flow.py create mode 100644 tests/components/pyload/test_init.py diff --git a/homeassistant/components/pyload/__init__.py b/homeassistant/components/pyload/__init__.py index 19103572e0b..a2e105e6454 100644 --- a/homeassistant/components/pyload/__init__.py +++ b/homeassistant/components/pyload/__init__.py @@ -1 +1,70 @@ -"""The pyload component.""" +"""The pyLoad integration.""" + +from __future__ import annotations + +from aiohttp import CookieJar +from pyloadapi.api import PyLoadAPI +from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_create_clientsession + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +type PyLoadConfigEntry = ConfigEntry[PyLoadAPI] + + +async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bool: + """Set up pyLoad from a config entry.""" + + url = ( + f"{"https" if entry.data[CONF_SSL] else "http"}://" + f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}/" + ) + + session = async_create_clientsession( + hass, + verify_ssl=entry.data[CONF_VERIFY_SSL], + cookie_jar=CookieJar(unsafe=True), + ) + pyloadapi = PyLoadAPI( + session, + api_url=url, + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + ) + + try: + await pyloadapi.login() + except CannotConnect as e: + raise ConfigEntryNotReady( + "Unable to connect and retrieve data from pyLoad API" + ) from e + except ParserError as e: + raise ConfigEntryNotReady("Unable to parse data from pyLoad API") from e + except InvalidAuth as e: + raise ConfigEntryError( + f"Authentication failed for {entry.data[CONF_USERNAME]}, check your login credentials" + ) from e + + entry.runtime_data = pyloadapi + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/pyload/config_flow.py b/homeassistant/components/pyload/config_flow.py new file mode 100644 index 00000000000..7ebc4a501d4 --- /dev/null +++ b/homeassistant/components/pyload/config_flow.py @@ -0,0 +1,120 @@ +"""Config flow for pyLoad integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from aiohttp import CookieJar +from pyloadapi.api import PyLoadAPI +from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_create_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_SSL, default=False): cv.boolean, + vol.Required(CONF_VERIFY_SSL, default=True): bool, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +async def validate_input(hass: HomeAssistant, user_input: dict[str, Any]) -> None: + """Validate the user input and try to connect to PyLoad.""" + + session = async_create_clientsession( + hass, + user_input[CONF_VERIFY_SSL], + cookie_jar=CookieJar(unsafe=True), + ) + + url = ( + f"{"https" if user_input[CONF_SSL] else "http"}://" + f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}/" + ) + pyload = PyLoadAPI( + session, + api_url=url, + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + ) + + await pyload.login() + + +class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for pyLoad.""" + + VERSION = 1 + # store values from yaml import so we can use them as + # suggested values when the configuration step is resumed + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) + try: + await validate_input(self.hass, user_input) + except (CannotConnect, ParserError): + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + title = user_input.pop(CONF_NAME, DEFAULT_NAME) + return self.async_create_entry(title=title, data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema( + STEP_USER_DATA_SCHEMA, user_input + ), + errors=errors, + ) + + async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult: + """Import config from yaml.""" + + config = { + CONF_NAME: import_info.get(CONF_NAME), + CONF_HOST: import_info.get(CONF_HOST, DEFAULT_HOST), + CONF_PASSWORD: import_info.get(CONF_PASSWORD, ""), + CONF_PORT: import_info.get(CONF_PORT, DEFAULT_PORT), + CONF_SSL: import_info.get(CONF_SSL, False), + CONF_USERNAME: import_info.get(CONF_USERNAME, ""), + CONF_VERIFY_SSL: False, + } + + result = await self.async_step_user(config) + + if errors := result.get("errors"): + return self.async_abort(reason=errors["base"]) + return result diff --git a/homeassistant/components/pyload/const.py b/homeassistant/components/pyload/const.py index a7d155d8b33..8ee1c05696f 100644 --- a/homeassistant/components/pyload/const.py +++ b/homeassistant/components/pyload/const.py @@ -5,3 +5,5 @@ DOMAIN = "pyload" DEFAULT_HOST = "localhost" DEFAULT_NAME = "pyLoad" DEFAULT_PORT = 8000 + +ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=pyload"} diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index 2a6e54fdf54..95e73118c42 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -2,6 +2,7 @@ "domain": "pyload", "name": "pyLoad", "codeowners": ["@tr4nt0r"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pyload", "integration_type": "service", "iot_class": "local_polling", diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index a0420db819c..131fec68609 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -7,7 +7,6 @@ from enum import StrEnum import logging from time import monotonic -from aiohttp import CookieJar from pyloadapi import ( CannotConnect, InvalidAuth, @@ -23,6 +22,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_HOST, CONF_MONITORED_VARIABLES, @@ -33,14 +33,15 @@ from homeassistant.const import ( CONF_USERNAME, UnitOfDataRate, ) -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT +from . import PyLoadConfigEntry +from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN, ISSUE_PLACEHOLDER _LOGGER = logging.getLogger(__name__) @@ -82,41 +83,63 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - async_add_entities: AddEntitiesCallback, + add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the pyLoad sensors.""" - host = config[CONF_HOST] - port = config[CONF_PORT] - protocol = "https" if config[CONF_SSL] else "http" - name = config[CONF_NAME] - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - url = f"{protocol}://{host}:{port}/" + """Import config from yaml.""" - session = async_create_clientsession( - hass, - verify_ssl=False, - cookie_jar=CookieJar(unsafe=True), + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) - pyloadapi = PyLoadAPI(session, api_url=url, username=username, password=password) - try: - await pyloadapi.login() - except CannotConnect as conn_err: - raise PlatformNotReady( - "Unable to connect and retrieve data from pyLoad API" - ) from conn_err - except ParserError as e: - raise PlatformNotReady("Unable to parse data from pyLoad API") from e - except InvalidAuth as e: - raise PlatformNotReady( - f"Authentication failed for {config[CONF_USERNAME]}, check your login credentials" - ) from e + _LOGGER.debug(result) + if ( + result.get("type") == FlowResultType.CREATE_ENTRY + or result.get("reason") == "already_configured" + ): + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + is_fixable=False, + issue_domain=DOMAIN, + breaks_in_ha_version="2025.2.0", + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "pyLoad", + }, + ) + elif error := result.get("reason"): + async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_import_issue_{error}", + breaks_in_ha_version="2025.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{error}", + translation_placeholders=ISSUE_PLACEHOLDER, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PyLoadConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the pyLoad sensors.""" + + pyloadapi = entry.runtime_data async_add_entities( ( PyLoadSensor( - api=pyloadapi, entity_description=description, client_name=name + api=pyloadapi, + entity_description=description, + client_name=entry.title, + entry_id=entry.entry_id, ) for description in SENSOR_DESCRIPTIONS ), @@ -128,12 +151,17 @@ class PyLoadSensor(SensorEntity): """Representation of a pyLoad sensor.""" def __init__( - self, api: PyLoadAPI, entity_description: SensorEntityDescription, client_name + self, + api: PyLoadAPI, + entity_description: SensorEntityDescription, + client_name: str, + entry_id: str, ) -> None: """Initialize a new pyLoad sensor.""" self._attr_name = f"{client_name} {entity_description.name}" self.type = entity_description.key self.api = api + self._attr_unique_id = f"{entry_id}_{entity_description.key}" self.entity_description = entity_description self._attr_available = False self.data: StatusServerResponse diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json new file mode 100644 index 00000000000..30e2366eb86 --- /dev/null +++ b/homeassistant/components/pyload/strings.json @@ -0,0 +1,44 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "ssl": "[%key:common::config_flow::data::ssl%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "name": "The name to use for your pyLoad instance in Home Assistant", + "host": "The hostname or IP address of the device running your pyLoad instance.", + "port": "pyLoad uses port 8000 by default." + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "issues": { + "deprecated_yaml_import_issue_cannot_connect": { + "title": "The pyLoad YAML configuration import failed", + "description": "Configuring pyLoad using YAML is being removed but there was a connection error when trying to import the YAML configuration.\n\nPlease verify that you have a stable internet connection and restart Home Assistant to try again or remove the pyLoad YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "deprecated_yaml_import_issue_invalid_auth": { + "title": "The pyLoad YAML configuration import failed", + "description": "Configuring pyLoad using YAML is being removed but there was an authentication error when trying to import the YAML configuration.\n\nCorrect the YAML configuration and restart Home Assistant to try again or remove the pyLoad YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "deprecated_yaml_import_issue_unknown": { + "title": "The pyLoad YAML configuration import failed", + "description": "Configuring pyLoad using YAML is being removed but there was an unknown error when trying to import the YAML configuration.\n\nEnsure the YAML configuration is correct and restart Home Assistant to try again or remove the pyLoad YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index cf6e2bb4fa7..631a5b6abb4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -435,6 +435,7 @@ FLOWS = { "pushover", "pvoutput", "pvpc_hourly_pricing", + "pyload", "qbittorrent", "qingping", "qnap", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index bbf96e4461b..eef78c212c8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4781,7 +4781,7 @@ "pyload": { "name": "pyLoad", "integration_type": "service", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "python_script": { diff --git a/tests/components/pyload/conftest.py b/tests/components/pyload/conftest.py index 53e86639c4a..0dafb9af4df 100644 --- a/tests/components/pyload/conftest.py +++ b/tests/components/pyload/conftest.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, patch from pyloadapi.types import LoginResponse, StatusServerResponse import pytest +from homeassistant.components.pyload.const import DEFAULT_NAME, DOMAIN from homeassistant.const import ( CONF_HOST, CONF_MONITORED_VARIABLES, @@ -15,25 +16,46 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, CONF_USERNAME, + CONF_VERIFY_SSL, ) from homeassistant.helpers.typing import ConfigType +from tests.common import MockConfigEntry + +USER_INPUT = { + CONF_HOST: "pyload.local", + CONF_PASSWORD: "test-password", + CONF_PORT: 8000, + CONF_SSL: True, + CONF_USERNAME: "test-username", + CONF_VERIFY_SSL: False, +} + +YAML_INPUT = { + CONF_HOST: "pyload.local", + CONF_MONITORED_VARIABLES: ["speed"], + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_PLATFORM: "pyload", + CONF_PORT: 8000, + CONF_SSL: True, + CONF_USERNAME: "test-username", +} + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.pyload.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + @pytest.fixture def pyload_config() -> ConfigType: """Mock pyload configuration entry.""" - return { - "sensor": { - CONF_PLATFORM: "pyload", - CONF_HOST: "localhost", - CONF_PORT: 8000, - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_SSL: True, - CONF_MONITORED_VARIABLES: ["speed"], - CONF_NAME: "pyload", - } - } + return {"sensor": YAML_INPUT} @pytest.fixture @@ -41,12 +63,15 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: """Mock PyLoadAPI.""" with ( patch( - "homeassistant.components.pyload.sensor.PyLoadAPI", - autospec=True, + "homeassistant.components.pyload.PyLoadAPI", autospec=True ) as mock_client, + patch("homeassistant.components.pyload.config_flow.PyLoadAPI", new=mock_client), + patch("homeassistant.components.pyload.sensor.PyLoadAPI", new=mock_client), ): client = mock_client.return_value client.username = "username" + client.api_url = "https://pyload.local:8000/" + client.login.return_value = LoginResponse( { "_permanent": True, @@ -75,3 +100,11 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: client.free_space.return_value = 99999999999 yield client + + +@pytest.fixture(name="config_entry") +def mock_config_entry() -> MockConfigEntry: + """Mock pyLoad configuration entry.""" + return MockConfigEntry( + domain=DOMAIN, title=DEFAULT_NAME, data=USER_INPUT, entry_id="XXXXXXXXXXXXXX" + ) diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index 226221240d2..3aaa7f4679f 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -1,4 +1,328 @@ # serializer version: 1 +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_speed', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'pyLoad Speed', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXXXX_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyLoad Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_speed', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'pyLoad Speed', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXXXX_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyLoad Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_speed', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'pyLoad Speed', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXXXX_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyLoad Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors_unavailable[CannotConnect][sensor.pyload_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Speed', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXXXX_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors_unavailable[CannotConnect][sensor.pyload_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyLoad Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors_unavailable[InvalidAuth][sensor.pyload_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Speed', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXXXX_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors_unavailable[InvalidAuth][sensor.pyload_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyLoad Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors_unavailable[ParserError][sensor.pyload_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Speed', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXXXX_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors_unavailable[ParserError][sensor.pyload_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyLoad Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_setup StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -14,3 +338,57 @@ 'state': '5.405963', }) # --- +# name: test_setup[sensor.pyload_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_speed', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'pyLoad Speed', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXXXX_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.pyload_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'pyLoad Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.405963', + }) +# --- diff --git a/tests/components/pyload/test_config_flow.py b/tests/components/pyload/test_config_flow.py new file mode 100644 index 00000000000..70d324fd980 --- /dev/null +++ b/tests/components/pyload/test_config_flow.py @@ -0,0 +1,166 @@ +"""Test the pyLoad config flow.""" + +from unittest.mock import AsyncMock + +from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError +import pytest + +from homeassistant.components.pyload.const import DEFAULT_NAME, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .conftest import USER_INPUT, YAML_INPUT + +from tests.common import MockConfigEntry + + +async def test_form( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_pyloadapi: AsyncMock, +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == USER_INPUT + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "expected_error"), + [ + (InvalidAuth, "invalid_auth"), + (CannotConnect, "cannot_connect"), + (ParserError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_form_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_pyloadapi: AsyncMock, + exception: Exception, + expected_error: str, +) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + mock_pyloadapi.login.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": expected_error} + + mock_pyloadapi.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == USER_INPUT + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_flow_user_already_configured( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_pyloadapi: AsyncMock +) -> None: + """Test we abort user data set when entry is already configured.""" + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=USER_INPUT, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_import( + hass: HomeAssistant, + mock_pyloadapi: AsyncMock, +) -> None: + """Test that we can import a YAML config.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=YAML_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test-name" + assert result["data"] == USER_INPUT + + +async def test_flow_import_already_configured( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_pyloadapi: AsyncMock +) -> None: + """Test we abort import data set when entry is already configured.""" + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=YAML_INPUT, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("exception", "reason"), + [ + (InvalidAuth, "invalid_auth"), + (CannotConnect, "cannot_connect"), + (ParserError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_flow_import_errors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + exception: Exception, + reason: str, +) -> None: + """Test we abort import data set when entry is already configured.""" + + mock_pyloadapi.login.side_effect = exception + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=YAML_INPUT, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == reason diff --git a/tests/components/pyload/test_init.py b/tests/components/pyload/test_init.py new file mode 100644 index 00000000000..a1ecf294523 --- /dev/null +++ b/tests/components/pyload/test_init.py @@ -0,0 +1,63 @@ +"""Test pyLoad init.""" + +from unittest.mock import MagicMock + +from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError +import pytest + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_entry_setup_unload( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: MagicMock, +) -> None: + """Test integration setup and unload.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + 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( + ("side_effect"), + [CannotConnect, ParserError], +) +async def test_config_entry_setup_errors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: MagicMock, + side_effect: Exception, +) -> None: + """Test config entry not ready.""" + mock_pyloadapi.login.side_effect = side_effect + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_config_entry_setup_invalid_auth( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: MagicMock, +) -> None: + """Test config entry authentication.""" + mock_pyloadapi.login.side_effect = InvalidAuth + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/pyload/test_sensor.py b/tests/components/pyload/test_sensor.py index e2b392b06f9..d0e912f82f2 100644 --- a/tests/components/pyload/test_sensor.py +++ b/tests/components/pyload/test_sensor.py @@ -7,108 +7,74 @@ from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.pyload.const import DOMAIN from homeassistant.components.pyload.sensor import SCAN_INTERVAL -from homeassistant.components.sensor import DOMAIN -from homeassistant.const import STATE_UNAVAILABLE -from homeassistant.core import HomeAssistant +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component -from tests.common import async_fire_time_changed - -SENSORS = ["sensor.pyload_speed"] +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform -@pytest.mark.usefixtures("mock_pyloadapi") async def test_setup( hass: HomeAssistant, - pyload_config: ConfigType, + config_entry: MockConfigEntry, snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_pyloadapi: AsyncMock, ) -> None: """Test setup of the pyload sensor platform.""" - - assert await async_setup_component(hass, DOMAIN, pyload_config) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - for sensor in SENSORS: - result = hass.states.get(sensor) - assert result == snapshot + assert config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) @pytest.mark.parametrize( - ("exception", "expected_exception"), - [ - (CannotConnect, "Unable to connect and retrieve data from pyLoad API"), - (ParserError, "Unable to parse data from pyLoad API"), - ( - InvalidAuth, - "Authentication failed for username, check your login credentials", - ), - ], -) -async def test_setup_exceptions( - hass: HomeAssistant, - pyload_config: ConfigType, - mock_pyloadapi: AsyncMock, - exception: Exception, - expected_exception: str, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test exceptions during setup up pyLoad platform.""" - - mock_pyloadapi.login.side_effect = exception - - assert await async_setup_component(hass, DOMAIN, pyload_config) - await hass.async_block_till_done() - - assert len(hass.states.async_all(DOMAIN)) == 0 - assert expected_exception in caplog.text - - -@pytest.mark.parametrize( - ("exception", "expected_exception"), - [ - (CannotConnect, "Unable to connect and retrieve data from pyLoad API"), - (ParserError, "Unable to parse data from pyLoad API"), - (InvalidAuth, "Authentication failed, trying to reauthenticate"), - ], + "exception", + [CannotConnect, InvalidAuth, ParserError], ) async def test_sensor_update_exceptions( hass: HomeAssistant, - pyload_config: ConfigType, + config_entry: MockConfigEntry, mock_pyloadapi: AsyncMock, exception: Exception, - expected_exception: str, - caplog: pytest.LogCaptureFixture, snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, ) -> None: - """Test exceptions during update of pyLoad sensor.""" + """Test if pyLoad sensors go unavailable when exceptions occur (except ParserErrors).""" - mock_pyloadapi.get_status.side_effect = exception - - assert await async_setup_component(hass, DOMAIN, pyload_config) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_all(DOMAIN)) == 1 - assert expected_exception in caplog.text + mock_pyloadapi.get_status.side_effect = exception + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() - for sensor in SENSORS: - assert hass.states.get(sensor).state == STATE_UNAVAILABLE + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) async def test_sensor_invalid_auth( hass: HomeAssistant, - pyload_config: ConfigType, + config_entry: MockConfigEntry, mock_pyloadapi: AsyncMock, caplog: pytest.LogCaptureFixture, freezer: FrozenDateTimeFactory, ) -> None: """Test invalid auth during sensor update.""" - assert await async_setup_component(hass, DOMAIN, pyload_config) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_all(DOMAIN)) == 1 mock_pyloadapi.get_status.side_effect = InvalidAuth mock_pyloadapi.login.side_effect = InvalidAuth @@ -121,3 +87,61 @@ async def test_sensor_invalid_auth( "Authentication failed for username, check your login credentials" in caplog.text ) + + +async def test_platform_setup_triggers_import_flow( + hass: HomeAssistant, + pyload_config: ConfigType, + mock_setup_entry: AsyncMock, + mock_pyloadapi: AsyncMock, +) -> None: + """Test if an issue is created when attempting setup from yaml config.""" + + assert await async_setup_component(hass, SENSOR_DOMAIN, pyload_config) + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "reason"), + [ + (InvalidAuth, "invalid_auth"), + (CannotConnect, "cannot_connect"), + (ParserError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_deprecated_yaml_import_issue( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + pyload_config: ConfigType, + mock_pyloadapi: AsyncMock, + exception: Exception, + reason: str, +) -> None: + """Test an issue is created when attempting setup from yaml config and an error happens.""" + + mock_pyloadapi.login.side_effect = exception + await async_setup_component(hass, SENSOR_DOMAIN, pyload_config) + await hass.async_block_till_done() + + assert issue_registry.async_get_issue( + domain=DOMAIN, issue_id=f"deprecated_yaml_import_issue_{reason}" + ) + + +async def test_deprecated_yaml( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + pyload_config: ConfigType, + mock_pyloadapi: AsyncMock, +) -> None: + """Test an issue is created when we import from yaml config.""" + + await async_setup_component(hass, SENSOR_DOMAIN, pyload_config) + await hass.async_block_till_done() + + assert issue_registry.async_get_issue( + domain=HOMEASSISTANT_DOMAIN, issue_id=f"deprecated_yaml_{DOMAIN}" + ) From 4474e8c7ef0b1af2944a6ae02dff9f44b80ac8e8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 23 Jun 2024 12:51:12 +0200 Subject: [PATCH 0008/2411] Remove YAML import for tado (#120231) --- homeassistant/components/tado/config_flow.py | 44 ------ .../components/tado/device_tracker.py | 57 +------ homeassistant/components/tado/strings.json | 16 -- tests/components/tado/test_config_flow.py | 141 ------------------ 4 files changed, 3 insertions(+), 255 deletions(-) diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index e52b87796f7..d27a8c4b10b 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -23,7 +23,6 @@ from homeassistant.exceptions import HomeAssistantError from .const import ( CONF_FALLBACK, - CONF_HOME_ID, CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_TADO_OPTIONS, DOMAIN, @@ -117,49 +116,6 @@ class TadoConfigFlow(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return await self.async_step_user() - async def async_step_import( - self, import_config: dict[str, Any] - ) -> ConfigFlowResult: - """Import a config entry from configuration.yaml.""" - _LOGGER.debug("Importing Tado from configuration.yaml") - username = import_config[CONF_USERNAME] - password = import_config[CONF_PASSWORD] - imported_home_id = import_config[CONF_HOME_ID] - - self._async_abort_entries_match( - { - CONF_USERNAME: username, - CONF_PASSWORD: password, - CONF_HOME_ID: imported_home_id, - } - ) - - try: - validate_result = await validate_input( - self.hass, - { - CONF_USERNAME: username, - CONF_PASSWORD: password, - }, - ) - except HomeAssistantError: - return self.async_abort(reason="import_failed") - except PyTado.exceptions.TadoWrongCredentialsException: - return self.async_abort(reason="import_failed_invalid_auth") - - home_id = validate_result[UNIQUE_ID] - await self.async_set_unique_id(home_id) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=import_config[CONF_USERNAME], - data={ - CONF_USERNAME: username, - CONF_PASSWORD: password, - CONF_HOME_ID: home_id, - }, - ) - async def async_step_reconfigure( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index d3996db7faf..1caea1b3103 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -4,74 +4,23 @@ from __future__ import annotations import logging -import voluptuous as vol - from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, - DeviceScanner, SourceType, TrackerEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_HOME, STATE_NOT_HOME +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_HOME, STATE_NOT_HOME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType from . import TadoConnector -from .const import CONF_HOME_ID, DATA, DOMAIN, SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED +from .const import DATA, DOMAIN, SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_HOME_ID): cv.string, - } -) - - -async def async_get_scanner( - hass: HomeAssistant, config: ConfigType -) -> DeviceScanner | None: - """Configure the Tado device scanner.""" - device_config = config["device_tracker"] - import_result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_USERNAME: device_config[CONF_USERNAME], - CONF_PASSWORD: device_config[CONF_PASSWORD], - CONF_HOME_ID: device_config.get(CONF_HOME_ID), - }, - ) - - translation_key = "deprecated_yaml_import_device_tracker" - if import_result.get("type") == FlowResultType.ABORT: - translation_key = "import_aborted" - if import_result.get("reason") == "import_failed": - translation_key = "import_failed" - if import_result.get("reason") == "import_failed_invalid_auth": - translation_key = "import_failed_invalid_auth" - - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml_import_device_tracker", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key=translation_key, - ) - return None - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index d992befe112..ab903dafb5b 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -150,22 +150,6 @@ } }, "issues": { - "deprecated_yaml_import_device_tracker": { - "title": "Tado YAML device tracker configuration imported", - "description": "Configuring the Tado Device Tracker using YAML is being removed.\nRemove the YAML device tracker configuration and restart Home Assistant." - }, - "import_aborted": { - "title": "Import aborted", - "description": "Configuring the Tado Device Tracker using YAML is being removed.\n The import was aborted, due to an existing config entry being the same as the data being imported in the YAML. Remove the YAML device tracker configuration and restart Home Assistant. Please use the UI to configure Tado." - }, - "import_failed": { - "title": "Failed to import", - "description": "Failed to import the configuration for the Tado Device Tracker. Please use the UI to configure Tado. Don't forget to delete the YAML configuration." - }, - "import_failed_invalid_auth": { - "title": "Failed to import, invalid credentials", - "description": "Failed to import the configuration for the Tado Device Tracker, due to invalid credentials. Please fix the YAML configuration and restart Home Assistant. Alternatively you can use the UI to configure Tado. Don't forget to delete the YAML configuration, once the import is successful." - }, "water_heater_fallback": { "title": "Tado Water Heater entities now support fallback options", "description": "Due to added support for water heaters entities, these entities may use different overlay. Please configure integration entity and tado app water heater zone overlay options." diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index a8883f47fe2..4f5f4180fb5 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -271,147 +271,6 @@ async def test_form_homekit(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.ABORT -async def test_import_step(hass: HomeAssistant) -> None: - """Test import step.""" - mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) - - with ( - patch( - "homeassistant.components.tado.config_flow.Tado", - return_value=mock_tado_api, - ), - patch( - "homeassistant.components.tado.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={ - "username": "test-username", - "password": "test-password", - "home_id": 1, - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"] == { - "username": "test-username", - "password": "test-password", - "home_id": "1", - } - assert mock_setup_entry.call_count == 1 - - -async def test_import_step_existing_entry(hass: HomeAssistant) -> None: - """Test import step with existing entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "username": "test-username", - "password": "test-password", - "home_id": 1, - }, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.tado.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={ - "username": "test-username", - "password": "test-password", - "home_id": 1, - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - assert mock_setup_entry.call_count == 0 - - -async def test_import_step_validation_failed(hass: HomeAssistant) -> None: - """Test import step with validation failed.""" - with patch( - "homeassistant.components.tado.config_flow.Tado", - side_effect=RuntimeError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "username": "test-username", - "password": "test-password", - "home_id": 1, - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "import_failed" - - -async def test_import_step_device_authentication_failed(hass: HomeAssistant) -> None: - """Test import step with device tracker authentication failed.""" - with patch( - "homeassistant.components.tado.config_flow.Tado", - side_effect=PyTado.exceptions.TadoWrongCredentialsException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "username": "test-username", - "password": "test-password", - "home_id": 1, - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "import_failed_invalid_auth" - - -async def test_import_step_unique_id_configured(hass: HomeAssistant) -> None: - """Test import step with unique ID already configured.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "username": "test-username", - "password": "test-password", - "home_id": 1, - }, - unique_id="unique_id", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.tado.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={ - "username": "test-username", - "password": "test-password", - "home_id": 1, - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - assert mock_setup_entry.call_count == 0 - - @pytest.mark.parametrize( ("exception", "error"), [ From 84d1d111385dd705fa38bbcde34a95d2d171afcb Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 23 Jun 2024 12:56:41 +0200 Subject: [PATCH 0009/2411] Add config flow to generic hygrostat (#119017) * Add config flow to hygrostat Co-authored-by: Franck Nijhof --- .../components/generic_hygrostat/__init__.py | 20 ++++ .../generic_hygrostat/config_flow.py | 100 +++++++++++++++++ .../generic_hygrostat/humidifier.py | 50 +++++++-- .../generic_hygrostat/manifest.json | 2 + .../components/generic_hygrostat/strings.json | 56 +++++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 12 +- .../snapshots/test_config_flow.ambr | 66 +++++++++++ .../generic_hygrostat/test_config_flow.py | 106 ++++++++++++++++++ 9 files changed, 400 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/generic_hygrostat/config_flow.py create mode 100644 homeassistant/components/generic_hygrostat/strings.json create mode 100644 tests/components/generic_hygrostat/snapshots/test_config_flow.ambr create mode 100644 tests/components/generic_hygrostat/test_config_flow.py diff --git a/homeassistant/components/generic_hygrostat/__init__.py b/homeassistant/components/generic_hygrostat/__init__.py index 467a9f0e0c5..ef032da1ee2 100644 --- a/homeassistant/components/generic_hygrostat/__init__.py +++ b/homeassistant/components/generic_hygrostat/__init__.py @@ -3,6 +3,7 @@ import voluptuous as vol from homeassistant.components.humidifier import HumidifierDeviceClass +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, discovery @@ -73,3 +74,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,)) + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms( + entry, (Platform.HUMIDIFIER,) + ) diff --git a/homeassistant/components/generic_hygrostat/config_flow.py b/homeassistant/components/generic_hygrostat/config_flow.py new file mode 100644 index 00000000000..cade566968d --- /dev/null +++ b/homeassistant/components/generic_hygrostat/config_flow.py @@ -0,0 +1,100 @@ +"""Config flow for Generic hygrostat.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.components.humidifier import HumidifierDeviceClass +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import CONF_NAME, PERCENTAGE +from homeassistant.helpers import selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, +) + +from . import ( + CONF_DEVICE_CLASS, + CONF_DRY_TOLERANCE, + CONF_HUMIDIFIER, + CONF_MIN_DUR, + CONF_SENSOR, + CONF_WET_TOLERANCE, + DEFAULT_TOLERANCE, + DOMAIN, +) + +OPTIONS_SCHEMA = { + vol.Required(CONF_DEVICE_CLASS): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[ + HumidifierDeviceClass.HUMIDIFIER, + HumidifierDeviceClass.DEHUMIDIFIER, + ], + translation_key=CONF_DEVICE_CLASS, + mode=selector.SelectSelectorMode.DROPDOWN, + ), + ), + vol.Required(CONF_SENSOR): selector.EntitySelector( + selector.EntitySelectorConfig( + domain=SENSOR_DOMAIN, device_class=SensorDeviceClass.HUMIDITY + ) + ), + vol.Required(CONF_HUMIDIFIER): selector.EntitySelector( + selector.EntitySelectorConfig(domain=SWITCH_DOMAIN) + ), + vol.Required( + CONF_DRY_TOLERANCE, default=DEFAULT_TOLERANCE + ): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, + max=100, + step=0.5, + unit_of_measurement=PERCENTAGE, + mode=selector.NumberSelectorMode.BOX, + ) + ), + vol.Required( + CONF_WET_TOLERANCE, default=DEFAULT_TOLERANCE + ): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, + max=100, + step=0.5, + unit_of_measurement=PERCENTAGE, + mode=selector.NumberSelectorMode.BOX, + ) + ), + vol.Optional(CONF_MIN_DUR): selector.DurationSelector( + selector.DurationSelectorConfig(allow_negative=False) + ), +} + +CONFIG_SCHEMA = { + vol.Required(CONF_NAME): selector.TextSelector(), + **OPTIONS_SCHEMA, +} + + +CONFIG_FLOW = { + "user": SchemaFlowFormStep(vol.Schema(CONFIG_SCHEMA)), +} + +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(vol.Schema(OPTIONS_SCHEMA)), +} + + +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options["name"]) diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index dea614d92f2..c22904a4caa 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -3,10 +3,10 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Mapping from datetime import datetime, timedelta import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from homeassistant.components.humidifier import ( ATTR_HUMIDITY, @@ -18,6 +18,7 @@ from homeassistant.components.humidifier import ( HumidifierEntity, HumidifierEntityFeature, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -39,7 +40,7 @@ from homeassistant.core import ( State, callback, ) -from homeassistant.helpers import condition +from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_track_state_change_event, @@ -83,6 +84,38 @@ async def async_setup_platform( """Set up the generic hygrostat platform.""" if discovery_info: config = discovery_info + await _async_setup_config( + hass, config, config.get(CONF_UNIQUE_ID), async_add_entities + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize config entry.""" + + await _async_setup_config( + hass, + config_entry.options, + config_entry.entry_id, + async_add_entities, + ) + + +def _time_period_or_none(value: Any) -> timedelta | None: + if value is None: + return None + return cast(timedelta, cv.time_period(value)) + + +async def _async_setup_config( + hass: HomeAssistant, + config: Mapping[str, Any], + unique_id: str | None, + async_add_entities: AddEntitiesCallback, +) -> None: name: str = config[CONF_NAME] switch_entity_id: str = config[CONF_HUMIDIFIER] sensor_entity_id: str = config[CONF_SENSOR] @@ -90,15 +123,18 @@ async def async_setup_platform( max_humidity: float | None = config.get(CONF_MAX_HUMIDITY) target_humidity: float | None = config.get(CONF_TARGET_HUMIDITY) device_class: HumidifierDeviceClass | None = config.get(CONF_DEVICE_CLASS) - min_cycle_duration: timedelta | None = config.get(CONF_MIN_DUR) - sensor_stale_duration: timedelta | None = config.get(CONF_STALE_DURATION) + min_cycle_duration: timedelta | None = _time_period_or_none( + config.get(CONF_MIN_DUR) + ) + sensor_stale_duration: timedelta | None = _time_period_or_none( + config.get(CONF_STALE_DURATION) + ) dry_tolerance: float = config[CONF_DRY_TOLERANCE] wet_tolerance: float = config[CONF_WET_TOLERANCE] - keep_alive: timedelta | None = config.get(CONF_KEEP_ALIVE) + keep_alive: timedelta | None = _time_period_or_none(config.get(CONF_KEEP_ALIVE)) initial_state: bool | None = config.get(CONF_INITIAL_STATE) away_humidity: int | None = config.get(CONF_AWAY_HUMIDITY) away_fixed: bool | None = config.get(CONF_AWAY_FIXED) - unique_id: str | None = config.get(CONF_UNIQUE_ID) async_add_entities( [ diff --git a/homeassistant/components/generic_hygrostat/manifest.json b/homeassistant/components/generic_hygrostat/manifest.json index cf0ace5e011..20222fd3617 100644 --- a/homeassistant/components/generic_hygrostat/manifest.json +++ b/homeassistant/components/generic_hygrostat/manifest.json @@ -2,7 +2,9 @@ "domain": "generic_hygrostat", "name": "Generic hygrostat", "codeowners": ["@Shulyaka"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/generic_hygrostat", + "integration_type": "helper", "iot_class": "local_polling", "quality_scale": "internal" } diff --git a/homeassistant/components/generic_hygrostat/strings.json b/homeassistant/components/generic_hygrostat/strings.json new file mode 100644 index 00000000000..a21ab68c628 --- /dev/null +++ b/homeassistant/components/generic_hygrostat/strings.json @@ -0,0 +1,56 @@ +{ + "title": "Generic hygrostat", + "config": { + "step": { + "user": { + "title": "Add generic hygrostat", + "description": "Create a entity that control the humidity via a switch and sensor.", + "data": { + "device_class": "Device class", + "dry_tolerance": "Dry tolerance", + "humidifier": "Switch", + "min_cycle_duration": "Minimum cycle duration", + "name": "[%key:common::config_flow::data::name%]", + "target_sensor": "Humidity sensor", + "wet_tolerance": "Wet tolerance" + }, + "data_description": { + "dry_tolerance": "The minimum amount of difference between the humidity read by the sensor specified in the target sensor option and the target humidity that must change prior to being switched on.", + "humidifier": "Humidifier or dehumidifier switch; must be a toggle device.", + "min_cycle_duration": "Set a minimum amount of time that the switch specified in the humidifier option must be in its current state prior to being switched either off or on.", + "target_sensor": "Sensor with current humidity.", + "wet_tolerance": "The minimum amount of difference between the humidity read by the sensor specified in the target sensor option and the target humidity that must change prior to being switched off." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "device_class": "[%key:component::generic_hygrostat::config::step::user::data::device_class%]", + "dry_tolerance": "[%key:component::generic_hygrostat::config::step::user::data::dry_tolerance%]", + "humidifier": "[%key:component::generic_hygrostat::config::step::user::data::humidifier%]", + "min_cycle_duration": "[%key:component::generic_hygrostat::config::step::user::data::min_cycle_duration%]", + "target_sensor": "[%key:component::generic_hygrostat::config::step::user::data::target_sensor%]", + "wet_tolerance": "[%key:component::generic_hygrostat::config::step::user::data::wet_tolerance%]" + }, + "data_description": { + "dry_tolerance": "[%key:component::generic_hygrostat::config::step::user::data_description::dry_tolerance%]", + "humidifier": "[%key:component::generic_hygrostat::config::step::user::data_description::humidifier%]", + "min_cycle_duration": "[%key:component::generic_hygrostat::config::step::user::data_description::min_cycle_duration%]", + "target_sensor": "[%key:component::generic_hygrostat::config::step::user::data_description::target_sensor%]", + "wet_tolerance": "[%key:component::generic_hygrostat::config::step::user::data_description::wet_tolerance%]" + } + } + } + }, + "selector": { + "device_class": { + "options": { + "humidifier": "Humidifier", + "dehumidifier": "Dehumidifier" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 631a5b6abb4..e5eeeb29403 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -6,6 +6,7 @@ To update, run python3 -m script.hassfest FLOWS = { "helper": [ "derivative", + "generic_hygrostat", "generic_thermostat", "group", "integration", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index eef78c212c8..d3380fdd17f 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2127,12 +2127,6 @@ "config_flow": true, "iot_class": "local_push" }, - "generic_hygrostat": { - "name": "Generic hygrostat", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling" - }, "geniushub": { "name": "Genius Hub", "integration_type": "hub", @@ -7160,6 +7154,11 @@ "config_flow": true, "iot_class": "calculated" }, + "generic_hygrostat": { + "integration_type": "helper", + "config_flow": true, + "iot_class": "local_polling" + }, "generic_thermostat": { "integration_type": "helper", "config_flow": true, @@ -7265,6 +7264,7 @@ "filesize", "garages_amsterdam", "generic", + "generic_hygrostat", "generic_thermostat", "google_travel_time", "group", diff --git a/tests/components/generic_hygrostat/snapshots/test_config_flow.ambr b/tests/components/generic_hygrostat/snapshots/test_config_flow.ambr new file mode 100644 index 00000000000..3527596c9b9 --- /dev/null +++ b/tests/components/generic_hygrostat/snapshots/test_config_flow.ambr @@ -0,0 +1,66 @@ +# serializer version: 1 +# name: test_config_flow[create] + FlowResultSnapshot({ + 'result': ConfigEntrySnapshot({ + 'title': 'My hygrostat', + }), + 'title': 'My hygrostat', + 'type': , + }) +# --- +# name: test_config_flow[init] + FlowResultSnapshot({ + 'type': , + }) +# --- +# name: test_options[create_entry] + FlowResultSnapshot({ + 'result': True, + 'type': , + }) +# --- +# name: test_options[dehumidifier] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'action': , + 'current_humidity': 10.0, + 'device_class': 'dehumidifier', + 'friendly_name': 'My hygrostat', + 'humidity': 100, + 'max_humidity': 100, + 'min_humidity': 0, + 'supported_features': , + }), + 'context': , + 'entity_id': 'humidifier.my_hygrostat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_options[humidifier] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'action': , + 'current_humidity': 10.0, + 'device_class': 'humidifier', + 'friendly_name': 'My hygrostat', + 'humidity': 100, + 'max_humidity': 100, + 'min_humidity': 0, + 'supported_features': , + }), + 'context': , + 'entity_id': 'humidifier.my_hygrostat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_options[init] + FlowResultSnapshot({ + 'type': , + }) +# --- diff --git a/tests/components/generic_hygrostat/test_config_flow.py b/tests/components/generic_hygrostat/test_config_flow.py new file mode 100644 index 00000000000..49572e296e4 --- /dev/null +++ b/tests/components/generic_hygrostat/test_config_flow.py @@ -0,0 +1,106 @@ +"""Test the generic hygrostat config flow.""" + +from unittest.mock import patch + +from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.components.generic_hygrostat import ( + CONF_DEVICE_CLASS, + CONF_DRY_TOLERANCE, + CONF_HUMIDIFIER, + CONF_NAME, + CONF_SENSOR, + CONF_WET_TOLERANCE, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +SNAPSHOT_FLOW_PROPS = props("type", "title", "result", "error") + + +async def test_config_flow(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: + """Test the config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result == snapshot(name="init", include=SNAPSHOT_FLOW_PROPS) + + with patch( + "homeassistant.components.generic_hygrostat.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: "My hygrostat", + CONF_DRY_TOLERANCE: 2, + CONF_WET_TOLERANCE: 4, + CONF_HUMIDIFIER: "switch.run", + CONF_SENSOR: "sensor.humidity", + CONF_DEVICE_CLASS: "dehumidifier", + }, + ) + await hass.async_block_till_done() + + assert result == snapshot(name="create", include=SNAPSHOT_FLOW_PROPS) + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.title == "My hygrostat" + + +async def test_options(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: + """Test reconfiguring.""" + + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_DEVICE_CLASS: "dehumidifier", + CONF_DRY_TOLERANCE: 2.0, + CONF_HUMIDIFIER: "switch.run", + CONF_NAME: "My hygrostat", + CONF_SENSOR: "sensor.humidity", + CONF_WET_TOLERANCE: 4.0, + }, + title="My hygrostat", + ) + config_entry.add_to_hass(hass) + + # set some initial values + hass.states.async_set( + "sensor.humidity", + "10", + {"unit_of_measurement": "%", "device_class": "humidity"}, + ) + hass.states.async_set("switch.run", "on") + + # check that it is setup + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.states.get("humidifier.my_hygrostat") == snapshot(name="dehumidifier") + + # switch to humidifier + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result == snapshot(name="init", include=SNAPSHOT_FLOW_PROPS) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_DRY_TOLERANCE: 2, + CONF_WET_TOLERANCE: 4, + CONF_HUMIDIFIER: "switch.run", + CONF_SENSOR: "sensor.humidity", + CONF_DEVICE_CLASS: "humidifier", + }, + ) + assert result == snapshot(name="create_entry", include=SNAPSHOT_FLOW_PROPS) + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + assert hass.states.get("humidifier.my_hygrostat") == snapshot(name="humidifier") From 826587abb265e4a16142893587130ef0203671d5 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Sun, 23 Jun 2024 13:16:00 +0200 Subject: [PATCH 0010/2411] Add `DeviceInfo` to pyLoad integration (#120232) * Add device info to pyLoad integration * Update homeassistant/components/pyload/sensor.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/pyload/sensor.py Co-authored-by: Joost Lekkerkerker * remove name, add entry_type --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/pyload/sensor.py | 11 ++++++++++- .../components/pyload/snapshots/test_sensor.ambr | 16 ++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 131fec68609..75f3227d542 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -36,6 +36,7 @@ from homeassistant.const import ( from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType @@ -150,6 +151,8 @@ async def async_setup_entry( class PyLoadSensor(SensorEntity): """Representation of a pyLoad sensor.""" + _attr_has_entity_name = True + def __init__( self, api: PyLoadAPI, @@ -158,13 +161,19 @@ class PyLoadSensor(SensorEntity): entry_id: str, ) -> None: """Initialize a new pyLoad sensor.""" - self._attr_name = f"{client_name} {entity_description.name}" self.type = entity_description.key self.api = api self._attr_unique_id = f"{entry_id}_{entity_description.key}" self.entity_description = entity_description self._attr_available = False self.data: StatusServerResponse + self.device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + manufacturer="PyLoad Team", + model="pyLoad", + configuration_url=api.api_url, + identifiers={(DOMAIN, entry_id)}, + ) async def async_update(self) -> None: """Update state of sensor.""" diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index 3aaa7f4679f..b772a2c39b1 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -12,7 +12,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.pyload_speed', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -29,7 +29,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'pyLoad Speed', + 'original_name': 'Speed', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, @@ -66,7 +66,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.pyload_speed', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -83,7 +83,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'pyLoad Speed', + 'original_name': 'Speed', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, @@ -120,7 +120,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.pyload_speed', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -137,7 +137,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'pyLoad Speed', + 'original_name': 'Speed', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, @@ -351,7 +351,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.pyload_speed', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -368,7 +368,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'pyLoad Speed', + 'original_name': 'Speed', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, From 001abdaccf5b21df646c0e942ed2717d8c412658 Mon Sep 17 00:00:00 2001 From: Virenbar Date: Sun, 23 Jun 2024 16:49:43 +0500 Subject: [PATCH 0011/2411] Fix generic thermostat string (#120235) --- homeassistant/components/generic_thermostat/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/generic_thermostat/strings.json b/homeassistant/components/generic_thermostat/strings.json index 27a563a9d8d..1ddd41de734 100644 --- a/homeassistant/components/generic_thermostat/strings.json +++ b/homeassistant/components/generic_thermostat/strings.json @@ -29,7 +29,7 @@ "away_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]", "comfort_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]", "eco_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::eco%]", - "home_temp": "[%common::state::home%]", + "home_temp": "[%key:common::state::home%]", "sleep_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::sleep%]", "activity_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::activity%]" } From 473b3b61ebea3096ec75ebcf54c25615a32f3247 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Sun, 23 Jun 2024 14:25:36 +0200 Subject: [PATCH 0012/2411] Add string and icon translations to pyLoad integration (#120234) add string and icon translations to pyLoad --- homeassistant/components/pyload/icons.json | 9 +++++++++ homeassistant/components/pyload/sensor.py | 2 +- homeassistant/components/pyload/strings.json | 7 +++++++ tests/components/pyload/snapshots/test_sensor.ambr | 8 ++++---- 4 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/pyload/icons.json diff --git a/homeassistant/components/pyload/icons.json b/homeassistant/components/pyload/icons.json new file mode 100644 index 00000000000..b3b7d148b1a --- /dev/null +++ b/homeassistant/components/pyload/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "speed": { + "default": "mdi:speedometer" + } + } + } +} diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 75f3227d542..8c35f8e7431 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -58,7 +58,7 @@ class PyLoadSensorEntity(StrEnum): SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=PyLoadSensorEntity.SPEED, - name="Speed", + translation_key=PyLoadSensorEntity.SPEED, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 30e2366eb86..a8544bf48eb 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -27,6 +27,13 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, + "entity": { + "sensor": { + "speed": { + "name": "Speed" + } + } + }, "issues": { "deprecated_yaml_import_issue_cannot_connect": { "title": "The pyLoad YAML configuration import failed", diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index b772a2c39b1..77a79e3eddd 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -33,7 +33,7 @@ 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', 'unit_of_measurement': , }) @@ -87,7 +87,7 @@ 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', 'unit_of_measurement': , }) @@ -141,7 +141,7 @@ 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', 'unit_of_measurement': , }) @@ -372,7 +372,7 @@ 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', 'unit_of_measurement': , }) From 2cc34fd7e7d00d09df28d3358af1b867a1c99ac1 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Sun, 23 Jun 2024 18:26:45 +0300 Subject: [PATCH 0013/2411] Improve Jewish calendar entities (#120236) * By default don't enable all sensors * Fix tests * Add entity category * Set has_entity_name to true * Revert "Set has_entity_name to true" This reverts commit 5ebfcde78ab0ff54bdca037b3bf3e6ec187cafea. --- .../jewish_calendar/binary_sensor.py | 5 +++- .../components/jewish_calendar/sensor.py | 25 ++++++++++++++++++- .../components/jewish_calendar/test_sensor.py | 4 +++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index c28dee88cf5..54080fcefd8 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -16,7 +16,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LANGUAGE, CONF_LOCATION +from homeassistant.const import CONF_LANGUAGE, CONF_LOCATION, EntityCategory from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import event from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -55,11 +55,13 @@ BINARY_SENSORS: tuple[JewishCalendarBinarySensorEntityDescription, ...] = ( key="erev_shabbat_hag", name="Erev Shabbat/Hag", is_on=lambda state: bool(state.erev_shabbat_chag), + entity_registry_enabled_default=False, ), JewishCalendarBinarySensorEntityDescription( key="motzei_shabbat_hag", name="Motzei Shabbat/Hag", is_on=lambda state: bool(state.motzei_shabbat_chag), + entity_registry_enabled_default=False, ), ) @@ -82,6 +84,7 @@ class JewishCalendarBinarySensor(BinarySensorEntity): """Representation of an Jewish Calendar binary sensor.""" _attr_should_poll = False + _attr_entity_category = EntityCategory.DIAGNOSTIC entity_description: JewishCalendarBinarySensorEntityDescription def __init__( diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index aff9d7ee602..02a5da27119 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -15,7 +15,12 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LANGUAGE, CONF_LOCATION, SUN_EVENT_SUNSET +from homeassistant.const import ( + CONF_LANGUAGE, + CONF_LOCATION, + SUN_EVENT_SUNSET, + EntityCategory, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import get_astral_event_date @@ -54,11 +59,13 @@ INFO_SENSORS: tuple[SensorEntityDescription, ...] = ( key="omer_count", name="Day of the Omer", icon="mdi:counter", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="daf_yomi", name="Daf Yomi", icon="mdi:book-open-variant", + entity_registry_enabled_default=False, ), ) @@ -67,11 +74,13 @@ TIME_SENSORS: tuple[SensorEntityDescription, ...] = ( key="first_light", name="Alot Hashachar", # codespell:ignore alot icon="mdi:weather-sunset-up", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="talit", name="Talit and Tefillin", icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="sunrise", @@ -82,41 +91,49 @@ TIME_SENSORS: tuple[SensorEntityDescription, ...] = ( key="gra_end_shma", name='Latest time for Shma Gr"a', icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="mga_end_shma", name='Latest time for Shma MG"A', icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="gra_end_tfila", name='Latest time for Tefilla Gr"a', icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="mga_end_tfila", name='Latest time for Tefilla MG"A', icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="midday", name="Chatzot Hayom", icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="big_mincha", name="Mincha Gedola", icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="small_mincha", name="Mincha Ketana", icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="plag_mincha", name="Plag Hamincha", icon="mdi:weather-sunset-down", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="sunset", @@ -127,21 +144,25 @@ TIME_SENSORS: tuple[SensorEntityDescription, ...] = ( key="first_stars", name="T'set Hakochavim", icon="mdi:weather-night", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="three_stars", name="T'set Hakochavim, 3 stars", icon="mdi:weather-night", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="upcoming_shabbat_candle_lighting", name="Upcoming Shabbat Candle Lighting", icon="mdi:candle", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="upcoming_shabbat_havdalah", name="Upcoming Shabbat Havdalah", icon="mdi:weather-night", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="upcoming_candle_lighting", @@ -178,6 +199,8 @@ async def async_setup_entry( class JewishCalendarSensor(SensorEntity): """Representation of an Jewish calendar sensor.""" + _attr_entity_category = EntityCategory.DIAGNOSTIC + def __init__( self, entry_id: str, diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 509e17017d5..e2f7cf25244 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -201,6 +201,7 @@ TEST_IDS = [ TEST_PARAMS, ids=TEST_IDS, ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_jewish_calendar_sensor( hass: HomeAssistant, now, @@ -541,6 +542,7 @@ SHABBAT_TEST_IDS = [ SHABBAT_PARAMS, ids=SHABBAT_TEST_IDS, ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_shabbat_times_sensor( hass: HomeAssistant, language, @@ -617,6 +619,7 @@ OMER_TEST_IDS = [ @pytest.mark.parametrize(("test_time", "result"), OMER_PARAMS, ids=OMER_TEST_IDS) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_omer_sensor(hass: HomeAssistant, test_time, result) -> None: """Test Omer Count sensor output.""" test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone)) @@ -651,6 +654,7 @@ DAFYOMI_TEST_IDS = [ @pytest.mark.parametrize(("test_time", "result"), DAFYOMI_PARAMS, ids=DAFYOMI_TEST_IDS) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_dafyomi_sensor(hass: HomeAssistant, test_time, result) -> None: """Test Daf Yomi sensor output.""" test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone)) From f1fd52bc306d07576dc1726cdc2cb581b5497e4c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 23 Jun 2024 17:37:08 +0200 Subject: [PATCH 0014/2411] Fix issue in mqtt fixture calling disconnect handler (#120246) --- tests/components/mqtt/test_init.py | 67 +++++++----------------------- tests/conftest.py | 1 + 2 files changed, 15 insertions(+), 53 deletions(-) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index cd710ba610e..264f80f48f8 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1111,9 +1111,7 @@ async def test_subscribe_and_resubscribe( record_calls: MessageCallbackType, ) -> None: """Test resubscribing within the debounce time.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) # This unsub will be un-done with the following subscribe @@ -1452,10 +1450,7 @@ async def test_subscribe_same_topic( When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again for it to resend any retained messages. """ - mqtt_mock = await mqtt_mock_entry() - - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] @@ -1506,10 +1501,7 @@ async def test_replaying_payload_same_topic( Retained messages must only be replayed for new subscriptions, except when the MQTT client is reconnecting. """ - mqtt_mock = await mqtt_mock_entry() - - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] @@ -1613,10 +1605,7 @@ async def test_replaying_payload_after_resubscribing( Retained messages must only be replayed for new subscriptions, except when the MQTT client is reconnection. """ - mqtt_mock = await mqtt_mock_entry() - - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() calls_a: list[ReceiveMessage] = [] @@ -1677,10 +1666,7 @@ async def test_replaying_payload_wildcard_topic( Retained messages should only be replayed for new subscriptions, except when the MQTT client is reconnection. """ - mqtt_mock = await mqtt_mock_entry() - - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] @@ -1759,10 +1745,7 @@ async def test_not_calling_unsubscribe_with_active_subscribers( record_calls: MessageCallbackType, ) -> None: """Test not calling unsubscribe() when other subscribers are active.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True - + await mqtt_mock_entry() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) await mqtt.async_subscribe(hass, "test/state", record_calls, 1) await hass.async_block_till_done() @@ -1787,10 +1770,8 @@ async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( Make sure subscriptions are cleared if unsubscribed before the subscribe cool down period has ended. """ - mqtt_mock = await mqtt_mock_entry() + await mqtt_mock_entry() mqtt_client_mock.subscribe.reset_mock() - # Fake that the client is connected - mqtt_mock().connected = True unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) unsub() @@ -1808,9 +1789,7 @@ async def test_unsubscribe_race( mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test not calling unsubscribe() when other subscribers are active.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown await hass.async_block_till_done() @@ -1871,9 +1850,7 @@ async def test_restore_subscriptions_on_reconnect( record_calls: MessageCallbackType, ) -> None: """Test subscriptions are restored on reconnect.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown await hass.async_block_till_done() @@ -1909,9 +1886,7 @@ async def test_restore_all_active_subscriptions_on_reconnect( freezer: FrozenDateTimeFactory, ) -> None: """Test active subscriptions are restored correctly on reconnect.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) @@ -1964,9 +1939,7 @@ async def test_subscribed_at_highest_qos( freezer: FrozenDateTimeFactory, ) -> None: """Test the highest qos as assigned when subscribing to the same topic.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) await hass.async_block_till_done() @@ -2064,12 +2037,9 @@ async def test_canceling_debouncer_on_shutdown( ) -> None: """Test canceling the debouncer when HA shuts down.""" - mqtt_mock = await mqtt_mock_entry() + await mqtt_mock_entry() mqtt_client_mock.subscribe.reset_mock() - # Fake that the client is connected - mqtt_mock().connected = True - await mqtt.async_subscribe(hass, "test/state1", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.2)) await hass.async_block_till_done() @@ -2879,9 +2849,7 @@ async def test_mqtt_subscribes_in_single_call( record_calls: MessageCallbackType, ) -> None: """Test bundled client subscription to topic.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() mqtt_client_mock.subscribe.reset_mock() await mqtt.async_subscribe(hass, "topic/test", record_calls) @@ -2919,9 +2887,7 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( record_calls: MessageCallbackType, ) -> None: """Test chunked client subscriptions.""" - mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True + await mqtt_mock_entry() mqtt_client_mock.subscribe.reset_mock() unsub_tasks: list[CALLBACK_TYPE] = [] @@ -4177,9 +4143,6 @@ async def test_reload_config_entry( assert await hass.config_entries.async_reload(entry.entry_id) assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() - # Assert the MQTT client was connected gracefully - with caplog.at_level(logging.INFO): - assert "Disconnected from MQTT server mock-broker:1883" in caplog.text assert (state := hass.states.get("sensor.test_manual1")) is not None assert state.attributes["friendly_name"] == "test_manual1_updated" @@ -4609,8 +4572,6 @@ async def test_client_sock_failure_after_connect( ) -> None: """Test handling the socket connected and disconnected.""" mqtt_mock = await mqtt_mock_entry() - # Fake that the client is connected - mqtt_mock().connected = True await hass.async_block_till_done() assert mqtt_mock.connected is True diff --git a/tests/conftest.py b/tests/conftest.py index 14e6f97d7c4..6aa370ae539 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -960,6 +960,7 @@ def mqtt_client_mock(hass: HomeAssistant) -> Generator[MqttMockPahoClient]: mock_client.subscribe.side_effect = _subscribe mock_client.unsubscribe.side_effect = _unsubscribe mock_client.publish.side_effect = _async_fire_mqtt_message + mock_client.loop_read.return_value = 0 yield mock_client From 9769dec44b304844c73a2024b00b77d435caef7d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 23 Jun 2024 17:45:43 +0200 Subject: [PATCH 0015/2411] Add number platform to AirGradient (#120247) * Add number entity * Add airgradient number entities * Fix --- .../components/airgradient/__init__.py | 2 +- .../components/airgradient/number.py | 130 ++++++++++++++++++ .../components/airgradient/strings.json | 8 ++ .../airgradient/snapshots/test_number.ambr | 113 +++++++++++++++ tests/components/airgradient/test_number.py | 101 ++++++++++++++ 5 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/airgradient/number.py create mode 100644 tests/components/airgradient/snapshots/test_number.ambr create mode 100644 tests/components/airgradient/test_number.py diff --git a/homeassistant/components/airgradient/__init__.py b/homeassistant/components/airgradient/__init__.py index 91ee0a440a6..76e11c05527 100644 --- a/homeassistant/components/airgradient/__init__.py +++ b/homeassistant/components/airgradient/__init__.py @@ -15,7 +15,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator -PLATFORMS: list[Platform] = [Platform.SELECT, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SELECT, Platform.SENSOR] @dataclass diff --git a/homeassistant/components/airgradient/number.py b/homeassistant/components/airgradient/number.py new file mode 100644 index 00000000000..e065b76ed51 --- /dev/null +++ b/homeassistant/components/airgradient/number.py @@ -0,0 +1,130 @@ +"""Support for AirGradient number entities.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from airgradient import AirGradientClient, Config +from airgradient.models import ConfigurationControl + +from homeassistant.components.number import ( + DOMAIN as NUMBER_DOMAIN, + NumberEntity, + NumberEntityDescription, +) +from homeassistant.const import PERCENTAGE, EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AirGradientConfigEntry +from .const import DOMAIN +from .coordinator import AirGradientConfigCoordinator +from .entity import AirGradientEntity + + +@dataclass(frozen=True, kw_only=True) +class AirGradientNumberEntityDescription(NumberEntityDescription): + """Describes AirGradient number entity.""" + + value_fn: Callable[[Config], int] + set_value_fn: Callable[[AirGradientClient, int], Awaitable[None]] + + +DISPLAY_BRIGHTNESS = AirGradientNumberEntityDescription( + key="display_brightness", + translation_key="display_brightness", + entity_category=EntityCategory.CONFIG, + native_min_value=0, + native_max_value=100, + native_step=1, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda config: config.display_brightness, + set_value_fn=lambda client, value: client.set_display_brightness(value), +) + +LED_BAR_BRIGHTNESS = AirGradientNumberEntityDescription( + key="led_bar_brightness", + translation_key="led_bar_brightness", + entity_category=EntityCategory.CONFIG, + native_min_value=0, + native_max_value=100, + native_step=1, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda config: config.led_bar_brightness, + set_value_fn=lambda client, value: client.set_led_bar_brightness(value), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AirGradientConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up AirGradient number entities based on a config entry.""" + + model = entry.runtime_data.measurement.data.model + coordinator = entry.runtime_data.config + + added_entities = False + + @callback + def _async_check_entities() -> None: + nonlocal added_entities + + if ( + coordinator.data.configuration_control is ConfigurationControl.LOCAL + and not added_entities + ): + entities = [] + if "I" in model: + entities.append(AirGradientNumber(coordinator, DISPLAY_BRIGHTNESS)) + if "L" in model: + entities.append(AirGradientNumber(coordinator, LED_BAR_BRIGHTNESS)) + + async_add_entities(entities) + added_entities = True + elif ( + coordinator.data.configuration_control is not ConfigurationControl.LOCAL + and added_entities + ): + entity_registry = er.async_get(hass) + unique_ids = [ + f"{coordinator.serial_number}-{entity_description.key}" + for entity_description in (DISPLAY_BRIGHTNESS, LED_BAR_BRIGHTNESS) + ] + for unique_id in unique_ids: + if entity_id := entity_registry.async_get_entity_id( + NUMBER_DOMAIN, DOMAIN, unique_id + ): + entity_registry.async_remove(entity_id) + added_entities = False + + coordinator.async_add_listener(_async_check_entities) + _async_check_entities() + + +class AirGradientNumber(AirGradientEntity, NumberEntity): + """Defines an AirGradient number entity.""" + + entity_description: AirGradientNumberEntityDescription + coordinator: AirGradientConfigCoordinator + + def __init__( + self, + coordinator: AirGradientConfigCoordinator, + description: AirGradientNumberEntityDescription, + ) -> None: + """Initialize AirGradient number.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" + + @property + def native_value(self) -> int | None: + """Return the state of the number.""" + return self.entity_description.value_fn(self.coordinator.data) + + async def async_set_native_value(self, value: float) -> None: + """Set the selected value.""" + await self.entity_description.set_value_fn(self.coordinator.client, int(value)) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index f4b558cf31a..0ab80286570 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -24,6 +24,14 @@ } }, "entity": { + "number": { + "led_bar_brightness": { + "name": "LED bar brightness" + }, + "display_brightness": { + "name": "Display brightness" + } + }, "select": { "configuration_control": { "name": "Configuration source", diff --git a/tests/components/airgradient/snapshots/test_number.ambr b/tests/components/airgradient/snapshots/test_number.ambr new file mode 100644 index 00000000000..87df8757eeb --- /dev/null +++ b/tests/components/airgradient/snapshots/test_number.ambr @@ -0,0 +1,113 @@ +# serializer version: 1 +# name: test_all_entities[number.airgradient_display_brightness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.airgradient_display_brightness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Display brightness', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'display_brightness', + 'unique_id': '84fce612f5b8-display_brightness', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[number.airgradient_display_brightness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Display brightness', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.airgradient_display_brightness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[number.airgradient_led_bar_brightness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.airgradient_led_bar_brightness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'LED bar brightness', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'led_bar_brightness', + 'unique_id': '84fce612f5b8-led_bar_brightness', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[number.airgradient_led_bar_brightness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient LED bar brightness', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.airgradient_led_bar_brightness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- diff --git a/tests/components/airgradient/test_number.py b/tests/components/airgradient/test_number.py new file mode 100644 index 00000000000..ba659829c50 --- /dev/null +++ b/tests/components/airgradient/test_number.py @@ -0,0 +1,101 @@ +"""Tests for the AirGradient button platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from airgradient import Config +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion + +from homeassistant.components.airgradient import DOMAIN +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_fixture, + snapshot_platform, +) + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.NUMBER]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_setting_value( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setting value.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ATTR_VALUE: 50}, + target={ATTR_ENTITY_ID: "number.airgradient_display_brightness"}, + blocking=True, + ) + mock_airgradient_client.set_display_brightness.assert_called_once() + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ATTR_VALUE: 50}, + target={ATTR_ENTITY_ID: "number.airgradient_led_bar_brightness"}, + blocking=True, + ) + mock_airgradient_client.set_led_bar_brightness.assert_called_once() + + +async def test_cloud_creates_no_number( + hass: HomeAssistant, + mock_cloud_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test cloud configuration control.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.NUMBER]): + await setup_integration(hass, mock_config_entry) + + assert len(hass.states.async_all()) == 0 + + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_local.json", DOMAIN) + ) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_cloud.json", DOMAIN) + ) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 From 080d90b63a6fa805d913cc0fbc8f30e1f4ba30fc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 23 Jun 2024 17:48:54 +0200 Subject: [PATCH 0016/2411] Add airgradient param fixture (#120241) --- tests/components/airgradient/conftest.py | 17 +- ...ures.json => current_measures_indoor.json} | 0 .../airgradient/snapshots/test_init.ambr | 32 +- .../airgradient/snapshots/test_select.ambr | 20 +- .../airgradient/snapshots/test_sensor.ambr | 347 ++++++++++++++---- tests/components/airgradient/test_init.py | 2 +- tests/components/airgradient/test_select.py | 25 +- tests/components/airgradient/test_sensor.py | 4 +- 8 files changed, 334 insertions(+), 113 deletions(-) rename tests/components/airgradient/fixtures/{current_measures.json => current_measures_indoor.json} (100%) diff --git a/tests/components/airgradient/conftest.py b/tests/components/airgradient/conftest.py index c5cc46cc8eb..7ca1198ce5f 100644 --- a/tests/components/airgradient/conftest.py +++ b/tests/components/airgradient/conftest.py @@ -39,7 +39,7 @@ def mock_airgradient_client() -> Generator[AsyncMock]: client = mock_client.return_value client.host = "10.0.0.131" client.get_current_measures.return_value = Measures.from_json( - load_fixture("current_measures.json", DOMAIN) + load_fixture("current_measures_indoor.json", DOMAIN) ) client.get_config.return_value = Config.from_json( load_fixture("get_config_local.json", DOMAIN) @@ -47,10 +47,21 @@ def mock_airgradient_client() -> Generator[AsyncMock]: yield client +@pytest.fixture(params=["indoor", "outdoor"]) +def airgradient_devices( + mock_airgradient_client: AsyncMock, request: pytest.FixtureRequest +) -> Generator[AsyncMock]: + """Return a list of AirGradient devices.""" + mock_airgradient_client.get_current_measures.return_value = Measures.from_json( + load_fixture(f"current_measures_{request.param}.json", DOMAIN) + ) + return mock_airgradient_client + + @pytest.fixture def mock_new_airgradient_client( mock_airgradient_client: AsyncMock, -) -> Generator[AsyncMock]: +) -> AsyncMock: """Mock a new AirGradient client.""" mock_airgradient_client.get_config.return_value = Config.from_json( load_fixture("get_config.json", DOMAIN) @@ -61,7 +72,7 @@ def mock_new_airgradient_client( @pytest.fixture def mock_cloud_airgradient_client( mock_airgradient_client: AsyncMock, -) -> Generator[AsyncMock]: +) -> AsyncMock: """Mock a cloud AirGradient client.""" mock_airgradient_client.get_config.return_value = Config.from_json( load_fixture("get_config_cloud.json", DOMAIN) diff --git a/tests/components/airgradient/fixtures/current_measures.json b/tests/components/airgradient/fixtures/current_measures_indoor.json similarity index 100% rename from tests/components/airgradient/fixtures/current_measures.json rename to tests/components/airgradient/fixtures/current_measures_indoor.json diff --git a/tests/components/airgradient/snapshots/test_init.ambr b/tests/components/airgradient/snapshots/test_init.ambr index 7109f603c9d..7c2e6ce4f78 100644 --- a/tests/components/airgradient/snapshots/test_init.ambr +++ b/tests/components/airgradient/snapshots/test_init.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_device_info +# name: test_device_info[indoor] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -29,3 +29,33 @@ 'via_device_id': None, }) # --- +# name: test_device_info[outdoor] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'airgradient', + '84fce612f5b8', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'AirGradient', + 'model': 'O-1PPT', + 'name': 'Airgradient', + 'name_by_user': None, + 'serial_number': '84fce60bec38', + 'suggested_area': None, + 'sw_version': '3.1.1', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index d29c7d23923..409eae52225 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_all_entities[select.airgradient_configuration_source-entry] +# name: test_all_entities[indoor][select.airgradient_configuration_source-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -37,7 +37,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[select.airgradient_configuration_source-state] +# name: test_all_entities[indoor][select.airgradient_configuration_source-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient Configuration source', @@ -54,7 +54,7 @@ 'state': 'local', }) # --- -# name: test_all_entities[select.airgradient_display_pm_standard-entry] +# name: test_all_entities[indoor][select.airgradient_display_pm_standard-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -92,7 +92,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[select.airgradient_display_pm_standard-state] +# name: test_all_entities[indoor][select.airgradient_display_pm_standard-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient Display PM standard', @@ -109,7 +109,7 @@ 'state': 'ugm3', }) # --- -# name: test_all_entities[select.airgradient_display_temperature_unit-entry] +# name: test_all_entities[indoor][select.airgradient_display_temperature_unit-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -147,7 +147,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[select.airgradient_display_temperature_unit-state] +# name: test_all_entities[indoor][select.airgradient_display_temperature_unit-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient Display temperature unit', @@ -164,7 +164,7 @@ 'state': 'c', }) # --- -# name: test_all_entities[select.airgradient_led_bar_mode-entry] +# name: test_all_entities[indoor][select.airgradient_led_bar_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -203,7 +203,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[select.airgradient_led_bar_mode-state] +# name: test_all_entities[indoor][select.airgradient_led_bar_mode-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient LED bar mode', @@ -221,7 +221,7 @@ 'state': 'co2', }) # --- -# name: test_all_entities_outdoor[select.airgradient_configuration_source-entry] +# name: test_all_entities[outdoor][select.airgradient_configuration_source-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -259,7 +259,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities_outdoor[select.airgradient_configuration_source-state] +# name: test_all_entities[outdoor][select.airgradient_configuration_source-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient Configuration source', diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index b0e22e7a9af..e96d2be1004 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_all_entities[sensor.airgradient_carbon_dioxide-entry] +# name: test_all_entities[indoor][sensor.airgradient_carbon_dioxide-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': 'ppm', }) # --- -# name: test_all_entities[sensor.airgradient_carbon_dioxide-state] +# name: test_all_entities[indoor][sensor.airgradient_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'carbon_dioxide', @@ -50,7 +50,7 @@ 'state': '778', }) # --- -# name: test_all_entities[sensor.airgradient_humidity-entry] +# name: test_all_entities[indoor][sensor.airgradient_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -85,7 +85,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_all_entities[sensor.airgradient_humidity-state] +# name: test_all_entities[indoor][sensor.airgradient_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -101,7 +101,7 @@ 'state': '48.0', }) # --- -# name: test_all_entities[sensor.airgradient_nox_index-entry] +# name: test_all_entities[indoor][sensor.airgradient_nox_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -136,7 +136,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.airgradient_nox_index-state] +# name: test_all_entities[indoor][sensor.airgradient_nox_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient NOx index', @@ -150,7 +150,7 @@ 'state': '1', }) # --- -# name: test_all_entities[sensor.airgradient_pm0_3-entry] +# name: test_all_entities[indoor][sensor.airgradient_pm0_3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -185,7 +185,7 @@ 'unit_of_measurement': 'particles/dL', }) # --- -# name: test_all_entities[sensor.airgradient_pm0_3-state] +# name: test_all_entities[indoor][sensor.airgradient_pm0_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient PM0.3', @@ -200,57 +200,7 @@ 'state': '270', }) # --- -# name: test_all_entities[sensor.airgradient_pm0_3_count-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.airgradient_pm0_3_count', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'PM0.3 count', - 'platform': 'airgradient', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pm003_count', - 'unique_id': '84fce612f5b8-pm003', - 'unit_of_measurement': 'particles/dL', - }) -# --- -# name: test_all_entities[sensor.airgradient_pm0_3_count-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient PM0.3 count', - 'state_class': , - 'unit_of_measurement': 'particles/dL', - }), - 'context': , - 'entity_id': 'sensor.airgradient_pm0_3_count', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '270', - }) -# --- -# name: test_all_entities[sensor.airgradient_pm1-entry] +# name: test_all_entities[indoor][sensor.airgradient_pm1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -285,7 +235,7 @@ 'unit_of_measurement': 'µg/m³', }) # --- -# name: test_all_entities[sensor.airgradient_pm1-state] +# name: test_all_entities[indoor][sensor.airgradient_pm1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'pm1', @@ -301,7 +251,7 @@ 'state': '22', }) # --- -# name: test_all_entities[sensor.airgradient_pm10-entry] +# name: test_all_entities[indoor][sensor.airgradient_pm10-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -336,7 +286,7 @@ 'unit_of_measurement': 'µg/m³', }) # --- -# name: test_all_entities[sensor.airgradient_pm10-state] +# name: test_all_entities[indoor][sensor.airgradient_pm10-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'pm10', @@ -352,7 +302,7 @@ 'state': '41', }) # --- -# name: test_all_entities[sensor.airgradient_pm2_5-entry] +# name: test_all_entities[indoor][sensor.airgradient_pm2_5-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -387,7 +337,7 @@ 'unit_of_measurement': 'µg/m³', }) # --- -# name: test_all_entities[sensor.airgradient_pm2_5-state] +# name: test_all_entities[indoor][sensor.airgradient_pm2_5-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'pm25', @@ -403,7 +353,7 @@ 'state': '34', }) # --- -# name: test_all_entities[sensor.airgradient_raw_nox-entry] +# name: test_all_entities[indoor][sensor.airgradient_raw_nox-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -438,7 +388,7 @@ 'unit_of_measurement': 'ticks', }) # --- -# name: test_all_entities[sensor.airgradient_raw_nox-state] +# name: test_all_entities[indoor][sensor.airgradient_raw_nox-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient Raw NOx', @@ -453,7 +403,7 @@ 'state': '16931', }) # --- -# name: test_all_entities[sensor.airgradient_raw_voc-entry] +# name: test_all_entities[indoor][sensor.airgradient_raw_voc-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -488,7 +438,7 @@ 'unit_of_measurement': 'ticks', }) # --- -# name: test_all_entities[sensor.airgradient_raw_voc-state] +# name: test_all_entities[indoor][sensor.airgradient_raw_voc-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient Raw VOC', @@ -503,7 +453,7 @@ 'state': '31792', }) # --- -# name: test_all_entities[sensor.airgradient_signal_strength-entry] +# name: test_all_entities[indoor][sensor.airgradient_signal_strength-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -538,7 +488,7 @@ 'unit_of_measurement': 'dBm', }) # --- -# name: test_all_entities[sensor.airgradient_signal_strength-state] +# name: test_all_entities[indoor][sensor.airgradient_signal_strength-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'signal_strength', @@ -554,7 +504,7 @@ 'state': '-52', }) # --- -# name: test_all_entities[sensor.airgradient_temperature-entry] +# name: test_all_entities[indoor][sensor.airgradient_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -589,7 +539,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[sensor.airgradient_temperature-state] +# name: test_all_entities[indoor][sensor.airgradient_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -605,7 +555,7 @@ 'state': '27.96', }) # --- -# name: test_all_entities[sensor.airgradient_voc_index-entry] +# name: test_all_entities[indoor][sensor.airgradient_voc_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -640,7 +590,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.airgradient_voc_index-state] +# name: test_all_entities[indoor][sensor.airgradient_voc_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Airgradient VOC index', @@ -654,3 +604,252 @@ 'state': '99', }) # --- +# name: test_all_entities[outdoor][sensor.airgradient_nox_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airgradient_nox_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'NOx index', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nitrogen_index', + 'unique_id': '84fce612f5b8-nitrogen_index', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_nox_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient NOx index', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_nox_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_raw_nox-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airgradient_raw_nox', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Raw NOx', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'raw_nitrogen', + 'unique_id': '84fce612f5b8-nox_raw', + 'unit_of_measurement': 'ticks', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_raw_nox-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Raw NOx', + 'state_class': , + 'unit_of_measurement': 'ticks', + }), + 'context': , + 'entity_id': 'sensor.airgradient_raw_nox', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16359', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_raw_voc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airgradient_raw_voc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Raw VOC', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'raw_total_volatile_organic_component', + 'unique_id': '84fce612f5b8-tvoc_raw', + 'unit_of_measurement': 'ticks', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_raw_voc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Raw VOC', + 'state_class': , + 'unit_of_measurement': 'ticks', + }), + 'context': , + 'entity_id': 'sensor.airgradient_raw_voc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30802', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_signal_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_signal_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Signal strength', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '84fce612f5b8-signal_strength', + 'unit_of_measurement': 'dBm', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_signal_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'Airgradient Signal strength', + 'state_class': , + 'unit_of_measurement': 'dBm', + }), + 'context': , + 'entity_id': 'sensor.airgradient_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-64', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_voc_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airgradient_voc_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'VOC index', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_volatile_organic_component_index', + 'unique_id': '84fce612f5b8-tvoc', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_voc_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient VOC index', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_voc_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '49', + }) +# --- diff --git a/tests/components/airgradient/test_init.py b/tests/components/airgradient/test_init.py index 273f425f4fc..408e6f5f3ba 100644 --- a/tests/components/airgradient/test_init.py +++ b/tests/components/airgradient/test_init.py @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry async def test_device_info( hass: HomeAssistant, snapshot: SnapshotAssertion, - mock_airgradient_client: AsyncMock, + airgradient_devices: AsyncMock, mock_config_entry: MockConfigEntry, device_registry: dr.DeviceRegistry, ) -> None: diff --git a/tests/components/airgradient/test_select.py b/tests/components/airgradient/test_select.py index 986295bd245..84bf081af63 100644 --- a/tests/components/airgradient/test_select.py +++ b/tests/components/airgradient/test_select.py @@ -2,11 +2,10 @@ from unittest.mock import AsyncMock, patch -from airgradient import ConfigurationControl, Measures +from airgradient import ConfigurationControl import pytest from syrupy import SnapshotAssertion -from homeassistant.components.airgradient import DOMAIN from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, @@ -18,14 +17,14 @@ from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry, load_fixture, snapshot_platform +from tests.common import MockConfigEntry, snapshot_platform @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_all_entities( hass: HomeAssistant, snapshot: SnapshotAssertion, - mock_airgradient_client: AsyncMock, + airgradient_devices: AsyncMock, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, ) -> None: @@ -36,24 +35,6 @@ async def test_all_entities( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_all_entities_outdoor( - hass: HomeAssistant, - snapshot: SnapshotAssertion, - mock_airgradient_client: AsyncMock, - mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, -) -> None: - """Test all entities.""" - mock_airgradient_client.get_current_measures.return_value = Measures.from_json( - load_fixture("current_measures_outdoor.json", DOMAIN) - ) - with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.SELECT]): - await setup_integration(hass, mock_config_entry) - - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - async def test_setting_value( hass: HomeAssistant, mock_airgradient_client: AsyncMock, diff --git a/tests/components/airgradient/test_sensor.py b/tests/components/airgradient/test_sensor.py index 65c96a0669f..c2e53ef4de2 100644 --- a/tests/components/airgradient/test_sensor.py +++ b/tests/components/airgradient/test_sensor.py @@ -27,7 +27,7 @@ from tests.common import ( async def test_all_entities( hass: HomeAssistant, snapshot: SnapshotAssertion, - mock_airgradient_client: AsyncMock, + airgradient_devices: AsyncMock, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, ) -> None: @@ -53,7 +53,7 @@ async def test_create_entities( assert len(hass.states.async_all()) == 0 mock_airgradient_client.get_current_measures.return_value = Measures.from_json( - load_fixture("current_measures.json", DOMAIN) + load_fixture("current_measures_indoor.json", DOMAIN) ) freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) From fa4b7f307878c20c6530676b672dab4cffea62e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Jun 2024 11:16:11 -0500 Subject: [PATCH 0017/2411] Bump yalexs to 6.4.1 (#120248) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 13658e7401d..a8f087e3acc 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==6.4.0", "yalexs-ble==2.4.2"] + "requirements": ["yalexs==6.4.1", "yalexs-ble==2.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index d65267ea5f6..c7e395b89ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2942,7 +2942,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==6.4.0 +yalexs==6.4.1 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02ec3650970..c14856fcb5b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2301,7 +2301,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==6.4.0 +yalexs==6.4.1 # homeassistant.components.yeelight yeelight==0.7.14 From 5fbb965624e048497b2ac22309982a8204152129 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Jun 2024 11:35:58 -0500 Subject: [PATCH 0018/2411] Bump uiprotect to 3.1.8 (#120244) --- 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 15b8b5b4a1b..817d7c9c074 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==3.1.1", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==3.1.8", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index c7e395b89ef..ec94fe54fc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2794,7 +2794,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.1.1 +uiprotect==3.1.8 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c14856fcb5b..d0ca8262a19 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2174,7 +2174,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.1.1 +uiprotect==3.1.8 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 7efd1079bd7bb78eadde2b03259e3bc93d5638cd Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Sun, 23 Jun 2024 19:26:55 +0200 Subject: [PATCH 0019/2411] Add Swiss public transport via stations (#115891) * add via stations * bump minor version due to backwards incompatibility * better coverage of many via station options in unit tests * fix migration unit test for new minor version 1.3 * switch version bump to major and improve migration test * fixes * improve error messages * use placeholders for strings --- .../swiss_public_transport/__init__.py | 34 ++++--- .../swiss_public_transport/config_flow.py | 61 ++++++++----- .../swiss_public_transport/const.py | 8 +- .../swiss_public_transport/helper.py | 15 +++ .../swiss_public_transport/strings.json | 14 ++- .../test_config_flow.py | 58 +++++++++--- .../swiss_public_transport/test_init.py | 91 ++++++++++++++++--- 7 files changed, 216 insertions(+), 65 deletions(-) create mode 100644 homeassistant/components/swiss_public_transport/helper.py diff --git a/homeassistant/components/swiss_public_transport/__init__.py b/homeassistant/components/swiss_public_transport/__init__.py index 74a7d90cfb2..1242c95269e 100644 --- a/homeassistant/components/swiss_public_transport/__init__.py +++ b/homeassistant/components/swiss_public_transport/__init__.py @@ -14,8 +14,9 @@ from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_DESTINATION, CONF_START, DOMAIN +from .const import CONF_DESTINATION, CONF_START, CONF_VIA, DOMAIN, PLACEHOLDERS from .coordinator import SwissPublicTransportDataUpdateCoordinator +from .helper import unique_id_from_config _LOGGER = logging.getLogger(__name__) @@ -33,19 +34,28 @@ async def async_setup_entry( destination = config[CONF_DESTINATION] session = async_get_clientsession(hass) - opendata = OpendataTransport(start, destination, session) + opendata = OpendataTransport(start, destination, session, via=config.get(CONF_VIA)) try: await opendata.async_get_data() except OpendataTransportConnectionError as e: raise ConfigEntryNotReady( - f"Timeout while connecting for entry '{start} {destination}'" + translation_domain=DOMAIN, + translation_key="request_timeout", + translation_placeholders={ + "config_title": entry.title, + "error": e, + }, ) from e except OpendataTransportError as e: raise ConfigEntryError( - f"Setup failed for entry '{start} {destination}' with invalid data, check " - "at http://transport.opendata.ch/examples/stationboard.html if your " - "station names are valid" + translation_domain=DOMAIN, + translation_key="invalid_data", + translation_placeholders={ + **PLACEHOLDERS, + "config_title": entry.title, + "error": e, + }, ) from e coordinator = SwissPublicTransportDataUpdateCoordinator(hass, opendata) @@ -72,15 +82,13 @@ async def async_migrate_entry( """Migrate config entry.""" _LOGGER.debug("Migrating from version %s", config_entry.version) - if config_entry.minor_version > 3: + if config_entry.version > 2: # This means the user has downgraded from a future version return False - if config_entry.minor_version == 1: + if config_entry.version == 1 and config_entry.minor_version == 1: # Remove wrongly registered devices and entries - new_unique_id = ( - f"{config_entry.data[CONF_START]} {config_entry.data[CONF_DESTINATION]}" - ) + new_unique_id = unique_id_from_config(config_entry.data) entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry( @@ -109,6 +117,10 @@ async def async_migrate_entry( config_entry, unique_id=new_unique_id, minor_version=2 ) + if config_entry.version < 2: + # Via stations now available, which are not backwards compatible if used, changes unique id + hass.config_entries.async_update_entry(config_entry, version=2, minor_version=1) + _LOGGER.debug( "Migration to version %s.%s successful", config_entry.version, diff --git a/homeassistant/components/swiss_public_transport/config_flow.py b/homeassistant/components/swiss_public_transport/config_flow.py index bb852efd211..74c6223f1d9 100644 --- a/homeassistant/components/swiss_public_transport/config_flow.py +++ b/homeassistant/components/swiss_public_transport/config_flow.py @@ -13,12 +13,24 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) -from .const import CONF_DESTINATION, CONF_START, DOMAIN, PLACEHOLDERS +from .const import CONF_DESTINATION, CONF_START, CONF_VIA, DOMAIN, MAX_VIA, PLACEHOLDERS +from .helper import unique_id_from_config DATA_SCHEMA = vol.Schema( { vol.Required(CONF_START): cv.string, + vol.Optional(CONF_VIA): TextSelector( + TextSelectorConfig( + type=TextSelectorType.TEXT, + multiple=True, + ), + ), vol.Required(CONF_DESTINATION): cv.string, } ) @@ -29,8 +41,8 @@ _LOGGER = logging.getLogger(__name__) class SwissPublicTransportConfigFlow(ConfigFlow, domain=DOMAIN): """Swiss public transport config flow.""" - VERSION = 1 - MINOR_VERSION = 2 + VERSION = 2 + MINOR_VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -38,29 +50,34 @@ class SwissPublicTransportConfigFlow(ConfigFlow, domain=DOMAIN): """Async user step to set up the connection.""" errors: dict[str, str] = {} if user_input is not None: - await self.async_set_unique_id( - f"{user_input[CONF_START]} {user_input[CONF_DESTINATION]}" - ) + unique_id = unique_id_from_config(user_input) + await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() - session = async_get_clientsession(self.hass) - opendata = OpendataTransport( - user_input[CONF_START], user_input[CONF_DESTINATION], session - ) - try: - await opendata.async_get_data() - except OpendataTransportConnectionError: - errors["base"] = "cannot_connect" - except OpendataTransportError: - errors["base"] = "bad_config" - except Exception: - _LOGGER.exception("Unknown error") - errors["base"] = "unknown" + if CONF_VIA in user_input and len(user_input[CONF_VIA]) > MAX_VIA: + errors["base"] = "too_many_via_stations" else: - return self.async_create_entry( - title=f"{user_input[CONF_START]} {user_input[CONF_DESTINATION]}", - data=user_input, + session = async_get_clientsession(self.hass) + opendata = OpendataTransport( + user_input[CONF_START], + user_input[CONF_DESTINATION], + session, + via=user_input.get(CONF_VIA), ) + try: + await opendata.async_get_data() + except OpendataTransportConnectionError: + errors["base"] = "cannot_connect" + except OpendataTransportError: + errors["base"] = "bad_config" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=unique_id, + data=user_input, + ) return self.async_show_form( step_id="user", diff --git a/homeassistant/components/swiss_public_transport/const.py b/homeassistant/components/swiss_public_transport/const.py index 6ae3cc9fd2f..32b6427ced5 100644 --- a/homeassistant/components/swiss_public_transport/const.py +++ b/homeassistant/components/swiss_public_transport/const.py @@ -1,12 +1,16 @@ """Constants for the swiss_public_transport integration.""" +from typing import Final + DOMAIN = "swiss_public_transport" -CONF_DESTINATION = "to" -CONF_START = "from" +CONF_DESTINATION: Final = "to" +CONF_START: Final = "from" +CONF_VIA: Final = "via" DEFAULT_NAME = "Next Destination" +MAX_VIA = 5 SENSOR_CONNECTIONS_COUNT = 3 diff --git a/homeassistant/components/swiss_public_transport/helper.py b/homeassistant/components/swiss_public_transport/helper.py new file mode 100644 index 00000000000..af03f7ad193 --- /dev/null +++ b/homeassistant/components/swiss_public_transport/helper.py @@ -0,0 +1,15 @@ +"""Helper functions for swiss_public_transport.""" + +from types import MappingProxyType +from typing import Any + +from .const import CONF_DESTINATION, CONF_START, CONF_VIA + + +def unique_id_from_config(config: MappingProxyType[str, Any] | dict[str, Any]) -> str: + """Build a unique id from a config entry.""" + return f"{config[CONF_START]} {config[CONF_DESTINATION]}" + ( + " via " + ", ".join(config[CONF_VIA]) + if CONF_VIA in config and len(config[CONF_VIA]) > 0 + else "" + ) diff --git a/homeassistant/components/swiss_public_transport/strings.json b/homeassistant/components/swiss_public_transport/strings.json index 4732bb0f527..4f4bc0522fc 100644 --- a/homeassistant/components/swiss_public_transport/strings.json +++ b/homeassistant/components/swiss_public_transport/strings.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Cannot connect to server", "bad_config": "Request failed due to bad config: Check at [stationboard]({stationboard_url}) if your station names are valid", + "too_many_via_stations": "Too many via stations, only up to 5 via stations are allowed per connection.", "unknown": "An unknown error was raised by python-opendata-transport" }, "abort": { @@ -15,9 +16,10 @@ "user": { "data": { "from": "Start station", - "to": "End station" + "to": "End station", + "via": "List of up to 5 via stations" }, - "description": "Provide start and end station for your connection\n\nCheck the [stationboard]({stationboard_url}) for valid stations.", + "description": "Provide start and end station for your connection,\nand optionally up to 5 via stations.\n\nCheck the [stationboard]({stationboard_url}) for valid stations.", "title": "Swiss Public Transport" } } @@ -46,5 +48,13 @@ "name": "Delay" } } + }, + "exceptions": { + "invalid_data": { + "message": "Setup failed for entry {config_title} with invalid data, check at the [stationboard]({stationboard_url}) if your station names are valid.\n{error}" + }, + "request_timeout": { + "message": "Timeout while connecting for entry {config_title}.\n{error}" + } } } diff --git a/tests/components/swiss_public_transport/test_config_flow.py b/tests/components/swiss_public_transport/test_config_flow.py index b728c87d4b0..027336e28a6 100644 --- a/tests/components/swiss_public_transport/test_config_flow.py +++ b/tests/components/swiss_public_transport/test_config_flow.py @@ -12,7 +12,10 @@ from homeassistant.components.swiss_public_transport import config_flow from homeassistant.components.swiss_public_transport.const import ( CONF_DESTINATION, CONF_START, + CONF_VIA, + MAX_VIA, ) +from homeassistant.components.swiss_public_transport.helper import unique_id_from_config from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -25,8 +28,36 @@ MOCK_DATA_STEP = { CONF_DESTINATION: "test_destination", } +MOCK_DATA_STEP_ONE_VIA = { + **MOCK_DATA_STEP, + CONF_VIA: ["via_station"], +} -async def test_flow_user_init_data_success(hass: HomeAssistant) -> None: +MOCK_DATA_STEP_MANY_VIA = { + **MOCK_DATA_STEP, + CONF_VIA: ["via_station_1", "via_station_2", "via_station_3"], +} + +MOCK_DATA_STEP_TOO_MANY_STATIONS = { + **MOCK_DATA_STEP, + CONF_VIA: MOCK_DATA_STEP_ONE_VIA[CONF_VIA] * (MAX_VIA + 1), +} + + +@pytest.mark.parametrize( + ("user_input", "config_title"), + [ + (MOCK_DATA_STEP, "test_start test_destination"), + (MOCK_DATA_STEP_ONE_VIA, "test_start test_destination via via_station"), + ( + MOCK_DATA_STEP_MANY_VIA, + "test_start test_destination via via_station_1, via_station_2, via_station_3", + ), + ], +) +async def test_flow_user_init_data_success( + hass: HomeAssistant, user_input, config_title +) -> None: """Test success response.""" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} @@ -47,25 +78,26 @@ async def test_flow_user_init_data_success(hass: HomeAssistant) -> None: ) result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=MOCK_DATA_STEP, + user_input=user_input, ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["result"].title == "test_start test_destination" + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["result"].title == config_title - assert result["data"] == MOCK_DATA_STEP + assert result["data"] == user_input @pytest.mark.parametrize( - ("raise_error", "text_error"), + ("raise_error", "text_error", "user_input_error"), [ - (OpendataTransportConnectionError(), "cannot_connect"), - (OpendataTransportError(), "bad_config"), - (IndexError(), "unknown"), + (OpendataTransportConnectionError(), "cannot_connect", MOCK_DATA_STEP), + (OpendataTransportError(), "bad_config", MOCK_DATA_STEP), + (None, "too_many_via_stations", MOCK_DATA_STEP_TOO_MANY_STATIONS), + (IndexError(), "unknown", MOCK_DATA_STEP), ], ) async def test_flow_user_init_data_error_and_recover( - hass: HomeAssistant, raise_error, text_error + hass: HomeAssistant, raise_error, text_error, user_input_error ) -> None: """Test unknown errors.""" with patch( @@ -78,7 +110,7 @@ async def test_flow_user_init_data_error_and_recover( ) result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=MOCK_DATA_STEP, + user_input=user_input_error, ) assert result["type"] is FlowResultType.FORM @@ -92,7 +124,7 @@ async def test_flow_user_init_data_error_and_recover( user_input=MOCK_DATA_STEP, ) - assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["result"].title == "test_start test_destination" assert result["data"] == MOCK_DATA_STEP @@ -104,7 +136,7 @@ async def test_flow_user_init_data_already_configured(hass: HomeAssistant) -> No entry = MockConfigEntry( domain=config_flow.DOMAIN, data=MOCK_DATA_STEP, - unique_id=f"{MOCK_DATA_STEP[CONF_START]} {MOCK_DATA_STEP[CONF_DESTINATION]}", + unique_id=unique_id_from_config(MOCK_DATA_STEP), ) entry.add_to_hass(hass) diff --git a/tests/components/swiss_public_transport/test_init.py b/tests/components/swiss_public_transport/test_init.py index e1b27cf5fe1..47360f93cf2 100644 --- a/tests/components/swiss_public_transport/test_init.py +++ b/tests/components/swiss_public_transport/test_init.py @@ -2,22 +2,32 @@ from unittest.mock import AsyncMock, patch +import pytest + from homeassistant.components.swiss_public_transport.const import ( CONF_DESTINATION, CONF_START, + CONF_VIA, DOMAIN, ) +from homeassistant.components.swiss_public_transport.helper import unique_id_from_config +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -MOCK_DATA_STEP = { +MOCK_DATA_STEP_BASE = { CONF_START: "test_start", CONF_DESTINATION: "test_destination", } +MOCK_DATA_STEP_VIA = { + **MOCK_DATA_STEP_BASE, + CONF_VIA: ["via_station"], +} + CONNECTIONS = [ { "departure": "2024-01-06T18:03:00+0100", @@ -46,19 +56,38 @@ CONNECTIONS = [ ] -async def test_migration_1_1_to_1_2( - hass: HomeAssistant, entity_registry: er.EntityRegistry +@pytest.mark.parametrize( + ( + "from_version", + "from_minor_version", + "config_data", + "overwrite_unique_id", + ), + [ + (1, 1, MOCK_DATA_STEP_BASE, "None_departure"), + (1, 2, MOCK_DATA_STEP_BASE, None), + (2, 1, MOCK_DATA_STEP_VIA, None), + ], +) +async def test_migration_from( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + from_version, + from_minor_version, + config_data, + overwrite_unique_id, ) -> None: """Test successful setup.""" - config_entry_faulty = MockConfigEntry( + config_entry = MockConfigEntry( domain=DOMAIN, - data=MOCK_DATA_STEP, - title="MIGRATION_TEST", - version=1, - minor_version=1, + data=config_data, + title=f"MIGRATION_TEST from {from_version}.{from_minor_version}", + version=from_version, + minor_version=from_minor_version, + unique_id=overwrite_unique_id or unique_id_from_config(config_data), ) - config_entry_faulty.add_to_hass(hass) + config_entry.add_to_hass(hass) with patch( "homeassistant.components.swiss_public_transport.OpendataTransport", @@ -67,21 +96,53 @@ async def test_migration_1_1_to_1_2( mock().connections = CONNECTIONS # Setup the config entry - await hass.config_entries.async_setup(config_entry_faulty.entry_id) + unique_id = unique_id_from_config(config_entry.data) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert entity_registry.async_is_registered( entity_registry.entities.get_entity_id( - (Platform.SENSOR, DOMAIN, "test_start test_destination_departure") + ( + Platform.SENSOR, + DOMAIN, + f"{unique_id}_departure", + ) ) ) - # Check change in config entry - assert config_entry_faulty.minor_version == 2 - assert config_entry_faulty.unique_id == "test_start test_destination" + # Check change in config entry and verify most recent version + assert config_entry.version == 2 + assert config_entry.minor_version == 1 + assert config_entry.unique_id == unique_id - # Check "None" is gone + # Check "None" is gone from version 1.1 to 1.2 assert not entity_registry.async_is_registered( entity_registry.entities.get_entity_id( (Platform.SENSOR, DOMAIN, "None_departure") ) ) + + +async def test_migrate_error_from_future(hass: HomeAssistant) -> None: + """Test a future version isn't migrated.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + version=3, + minor_version=1, + unique_id="some_crazy_future_unique_id", + data=MOCK_DATA_STEP_BASE, + ) + + mock_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.swiss_public_transport.OpendataTransport", + return_value=AsyncMock(), + ) as mock: + mock().connections = CONNECTIONS + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert entry.state is ConfigEntryState.MIGRATION_ERROR From 034b5e88e08dd9e906d0d6b98162be331cb46b94 Mon Sep 17 00:00:00 2001 From: Matthew FitzGerald-Chamberlain Date: Sun, 23 Jun 2024 12:47:09 -0500 Subject: [PATCH 0020/2411] Add Aprilaire air cleaning and fresh air functionality (#120174) * Add custom services for Aprilaire * Add icons.json * Use select/number entities instead of services * Remove unneeded consts * Remove number platform * Code review updates * Update homeassistant/components/aprilaire/strings.json Co-authored-by: Joost Lekkerkerker * Code review updates --------- Co-authored-by: Joost Lekkerkerker --- .coveragerc | 1 + .../components/aprilaire/__init__.py | 6 +- homeassistant/components/aprilaire/select.py | 153 ++++++++++++++++++ .../components/aprilaire/strings.json | 33 ++++ 4 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/aprilaire/select.py diff --git a/.coveragerc b/.coveragerc index 003b4908b17..da3b7b91ece 100644 --- a/.coveragerc +++ b/.coveragerc @@ -87,6 +87,7 @@ omit = homeassistant/components/aprilaire/climate.py homeassistant/components/aprilaire/coordinator.py homeassistant/components/aprilaire/entity.py + homeassistant/components/aprilaire/select.py homeassistant/components/aprilaire/sensor.py homeassistant/components/apsystems/__init__.py homeassistant/components/apsystems/coordinator.py diff --git a/homeassistant/components/aprilaire/__init__.py b/homeassistant/components/aprilaire/__init__.py index ba310615567..9747a4d40a4 100644 --- a/homeassistant/components/aprilaire/__init__.py +++ b/homeassistant/components/aprilaire/__init__.py @@ -15,7 +15,11 @@ from homeassistant.helpers.device_registry import format_mac from .const import DOMAIN from .coordinator import AprilaireCoordinator -PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.CLIMATE, + Platform.SELECT, + Platform.SENSOR, +] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/aprilaire/select.py b/homeassistant/components/aprilaire/select.py new file mode 100644 index 00000000000..504453f7463 --- /dev/null +++ b/homeassistant/components/aprilaire/select.py @@ -0,0 +1,153 @@ +"""The Aprilaire select component.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import cast + +from pyaprilaire.const import Attribute + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import AprilaireCoordinator +from .entity import BaseAprilaireEntity + +AIR_CLEANING_EVENT_MAP = {0: "off", 3: "event_clean", 4: "allergies"} +AIR_CLEANING_MODE_MAP = {0: "off", 1: "constant_clean", 2: "automatic"} +FRESH_AIR_EVENT_MAP = {0: "off", 2: "3hour", 3: "24hour"} +FRESH_AIR_MODE_MAP = {0: "off", 1: "automatic"} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Aprilaire select devices.""" + + coordinator: AprilaireCoordinator = hass.data[DOMAIN][config_entry.unique_id] + + assert config_entry.unique_id is not None + + descriptions: list[AprilaireSelectDescription] = [] + + if coordinator.data.get(Attribute.AIR_CLEANING_AVAILABLE) == 1: + descriptions.extend( + [ + AprilaireSelectDescription( + key="air_cleaning_event", + translation_key="air_cleaning_event", + options_map=AIR_CLEANING_EVENT_MAP, + event_value_key=Attribute.AIR_CLEANING_EVENT, + mode_value_key=Attribute.AIR_CLEANING_MODE, + is_event=True, + select_option_fn=coordinator.client.set_air_cleaning, + ), + AprilaireSelectDescription( + key="air_cleaning_mode", + translation_key="air_cleaning_mode", + options_map=AIR_CLEANING_MODE_MAP, + event_value_key=Attribute.AIR_CLEANING_EVENT, + mode_value_key=Attribute.AIR_CLEANING_MODE, + is_event=False, + select_option_fn=coordinator.client.set_air_cleaning, + ), + ] + ) + + if coordinator.data.get(Attribute.VENTILATION_AVAILABLE) == 1: + descriptions.extend( + [ + AprilaireSelectDescription( + key="fresh_air_event", + translation_key="fresh_air_event", + options_map=FRESH_AIR_EVENT_MAP, + event_value_key=Attribute.FRESH_AIR_EVENT, + mode_value_key=Attribute.FRESH_AIR_MODE, + is_event=True, + select_option_fn=coordinator.client.set_fresh_air, + ), + AprilaireSelectDescription( + key="fresh_air_mode", + translation_key="fresh_air_mode", + options_map=FRESH_AIR_MODE_MAP, + event_value_key=Attribute.FRESH_AIR_EVENT, + mode_value_key=Attribute.FRESH_AIR_MODE, + is_event=False, + select_option_fn=coordinator.client.set_fresh_air, + ), + ] + ) + + async_add_entities( + AprilaireSelectEntity(coordinator, description, config_entry.unique_id) + for description in descriptions + ) + + +@dataclass(frozen=True, kw_only=True) +class AprilaireSelectDescription(SelectEntityDescription): + """Class describing Aprilaire select entities.""" + + options_map: dict[int, str] + event_value_key: str + mode_value_key: str + is_event: bool + select_option_fn: Callable[[int, int], Awaitable] + + +class AprilaireSelectEntity(BaseAprilaireEntity, SelectEntity): + """Base select entity for Aprilaire.""" + + entity_description: AprilaireSelectDescription + + def __init__( + self, + coordinator: AprilaireCoordinator, + description: AprilaireSelectDescription, + unique_id: str, + ) -> None: + """Initialize a select for an Aprilaire device.""" + + self.entity_description = description + self.values_map = {v: k for k, v in description.options_map.items()} + + super().__init__(coordinator, unique_id) + + self._attr_options = list(description.options_map.values()) + + @property + def current_option(self) -> str: + """Get the current option.""" + + if self.entity_description.is_event: + value_key = self.entity_description.event_value_key + else: + value_key = self.entity_description.mode_value_key + + current_value = int(self.coordinator.data.get(value_key, 0)) + + return self.entity_description.options_map.get(current_value, "off") + + async def async_select_option(self, option: str) -> None: + """Set the current option.""" + + if self.entity_description.is_event: + event_value = self.values_map[option] + + mode_value = cast( + int, self.coordinator.data.get(self.entity_description.mode_value_key) + ) + else: + mode_value = self.values_map[option] + + event_value = cast( + int, self.coordinator.data.get(self.entity_description.event_value_key) + ) + + await self.entity_description.select_option_fn(mode_value, event_value) diff --git a/homeassistant/components/aprilaire/strings.json b/homeassistant/components/aprilaire/strings.json index 72005e0215c..0849f2255dd 100644 --- a/homeassistant/components/aprilaire/strings.json +++ b/homeassistant/components/aprilaire/strings.json @@ -24,6 +24,39 @@ "name": "Thermostat" } }, + "select": { + "air_cleaning_event": { + "name": "Air cleaning event", + "state": { + "off": "[%key:common::state::off%]", + "event_clean": "Event clean (3 hour)", + "allergies": "Allergies (24 hour)" + } + }, + "air_cleaning_mode": { + "name": "Air cleaning mode", + "state": { + "off": "[%key:common::state::off%]", + "constant_clean": "Constant clean", + "automatic": "Automatic" + } + }, + "fresh_air_event": { + "name": "Fresh air event", + "state": { + "off": "[%key:common::state::off%]", + "3hour": "3 hour event", + "24hour": "24 hour event" + } + }, + "fresh_air_mode": { + "name": "Fresh air mode", + "state": { + "off": "[%key:common::state::off%]", + "automatic": "[%key:component::aprilaire::entity::select::air_cleaning_mode::state::automatic%]" + } + } + }, "sensor": { "indoor_humidity_controlling_sensor": { "name": "Indoor humidity controlling sensor" From 29da88d8f6ee7d11b548094eb41837a2b16686de Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Sun, 23 Jun 2024 20:55:27 +0300 Subject: [PATCH 0021/2411] Create a Jewish Calendar entity (#120253) * Set has_entity_name to true * Move common code to jewish calendar service entity * Remove already existing assignment * Move data to common entity * Remove description name * Use config entry title instead of name for the device * Address comments --- .../jewish_calendar/binary_sensor.py | 33 ++++----------- .../components/jewish_calendar/entity.py | 41 +++++++++++++++++++ .../components/jewish_calendar/sensor.py | 33 ++++----------- .../jewish_calendar/test_binary_sensor.py | 3 ++ .../components/jewish_calendar/test_sensor.py | 13 ++++-- 5 files changed, 68 insertions(+), 55 deletions(-) create mode 100644 homeassistant/components/jewish_calendar/entity.py diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index 54080fcefd8..060650ee25c 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -6,7 +6,6 @@ from collections.abc import Callable from dataclasses import dataclass import datetime as dt from datetime import datetime -from typing import Any import hdate from hdate.zmanim import Zmanim @@ -16,18 +15,14 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LANGUAGE, CONF_LOCATION, EntityCategory +from homeassistant.const import EntityCategory from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import event from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import ( - CONF_CANDLE_LIGHT_MINUTES, - CONF_HAVDALAH_OFFSET_MINUTES, - DEFAULT_NAME, - DOMAIN, -) +from .const import DOMAIN +from .entity import JewishCalendarEntity @dataclass(frozen=True) @@ -75,33 +70,19 @@ async def async_setup_entry( entry = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - JewishCalendarBinarySensor(config_entry.entry_id, entry, description) + JewishCalendarBinarySensor(config_entry, entry, description) for description in BINARY_SENSORS ) -class JewishCalendarBinarySensor(BinarySensorEntity): +class JewishCalendarBinarySensor(JewishCalendarEntity, BinarySensorEntity): """Representation of an Jewish Calendar binary sensor.""" _attr_should_poll = False _attr_entity_category = EntityCategory.DIAGNOSTIC - entity_description: JewishCalendarBinarySensorEntityDescription + _update_unsub: CALLBACK_TYPE | None = None - def __init__( - self, - entry_id: str, - data: dict[str, Any], - description: JewishCalendarBinarySensorEntityDescription, - ) -> None: - """Initialize the binary sensor.""" - self.entity_description = description - self._attr_name = f"{DEFAULT_NAME} {description.name}" - self._attr_unique_id = f"{entry_id}-{description.key}" - self._location = data[CONF_LOCATION] - self._hebrew = data[CONF_LANGUAGE] == "hebrew" - self._candle_lighting_offset = data[CONF_CANDLE_LIGHT_MINUTES] - self._havdalah_offset = data[CONF_HAVDALAH_OFFSET_MINUTES] - self._update_unsub: CALLBACK_TYPE | None = None + entity_description: JewishCalendarBinarySensorEntityDescription @property def is_on(self) -> bool: diff --git a/homeassistant/components/jewish_calendar/entity.py b/homeassistant/components/jewish_calendar/entity.py new file mode 100644 index 00000000000..aba76599f63 --- /dev/null +++ b/homeassistant/components/jewish_calendar/entity.py @@ -0,0 +1,41 @@ +"""Entity representing a Jewish Calendar sensor.""" + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_LANGUAGE, CONF_LOCATION +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription + +from .const import ( + CONF_CANDLE_LIGHT_MINUTES, + CONF_DIASPORA, + CONF_HAVDALAH_OFFSET_MINUTES, + DOMAIN, +) + + +class JewishCalendarEntity(Entity): + """An HA implementation for Jewish Calendar entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + config_entry: ConfigEntry, + data: dict[str, Any], + description: EntityDescription, + ) -> None: + """Initialize a Jewish Calendar entity.""" + self.entity_description = description + self._attr_unique_id = f"{config_entry.entry_id}_{description.key}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, config_entry.entry_id)}, + name=config_entry.title, + ) + self._location = data[CONF_LOCATION] + self._hebrew = data[CONF_LANGUAGE] == "hebrew" + self._candle_lighting_offset = data[CONF_CANDLE_LIGHT_MINUTES] + self._havdalah_offset = data[CONF_HAVDALAH_OFFSET_MINUTES] + self._diaspora = data[CONF_DIASPORA] diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 02a5da27119..87b4375b8b2 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -15,24 +15,14 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_LANGUAGE, - CONF_LOCATION, - SUN_EVENT_SUNSET, - EntityCategory, -) +from homeassistant.const import SUN_EVENT_SUNSET, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import get_astral_event_date import homeassistant.util.dt as dt_util -from .const import ( - CONF_CANDLE_LIGHT_MINUTES, - CONF_DIASPORA, - CONF_HAVDALAH_OFFSET_MINUTES, - DEFAULT_NAME, - DOMAIN, -) +from .const import DOMAIN +from .entity import JewishCalendarEntity _LOGGER = logging.getLogger(__name__) @@ -185,37 +175,30 @@ async def async_setup_entry( """Set up the Jewish calendar sensors .""" entry = hass.data[DOMAIN][config_entry.entry_id] sensors = [ - JewishCalendarSensor(config_entry.entry_id, entry, description) + JewishCalendarSensor(config_entry, entry, description) for description in INFO_SENSORS ] sensors.extend( - JewishCalendarTimeSensor(config_entry.entry_id, entry, description) + JewishCalendarTimeSensor(config_entry, entry, description) for description in TIME_SENSORS ) async_add_entities(sensors) -class JewishCalendarSensor(SensorEntity): +class JewishCalendarSensor(JewishCalendarEntity, SensorEntity): """Representation of an Jewish calendar sensor.""" _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, - entry_id: str, + config_entry: ConfigEntry, data: dict[str, Any], description: SensorEntityDescription, ) -> None: """Initialize the Jewish calendar sensor.""" - self.entity_description = description - self._attr_name = f"{DEFAULT_NAME} {description.name}" - self._attr_unique_id = f"{entry_id}-{description.key}" - self._location = data[CONF_LOCATION] - self._hebrew = data[CONF_LANGUAGE] == "hebrew" - self._candle_lighting_offset = data[CONF_CANDLE_LIGHT_MINUTES] - self._havdalah_offset = data[CONF_HAVDALAH_OFFSET_MINUTES] - self._diaspora = data[CONF_DIASPORA] + super().__init__(config_entry, data, description) self._attrs: dict[str, str] = {} async def async_update(self) -> None: diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index b60e7698266..8abaaecb77d 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.jewish_calendar.const import ( CONF_CANDLE_LIGHT_MINUTES, CONF_DIASPORA, CONF_HAVDALAH_OFFSET_MINUTES, + DEFAULT_NAME, DOMAIN, ) from homeassistant.const import CONF_LANGUAGE, CONF_PLATFORM, STATE_OFF, STATE_ON @@ -192,6 +193,7 @@ async def test_issur_melacha_sensor( with alter_time(test_time): entry = MockConfigEntry( + title=DEFAULT_NAME, domain=DOMAIN, data={ CONF_LANGUAGE: "english", @@ -264,6 +266,7 @@ async def test_issur_melacha_sensor_update( with alter_time(test_time): entry = MockConfigEntry( + title=DEFAULT_NAME, domain=DOMAIN, data={ CONF_LANGUAGE: "english", diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index e2f7cf25244..cb054751f67 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.jewish_calendar.const import ( CONF_CANDLE_LIGHT_MINUTES, CONF_DIASPORA, CONF_HAVDALAH_OFFSET_MINUTES, + DEFAULT_NAME, DOMAIN, ) from homeassistant.const import CONF_LANGUAGE, CONF_PLATFORM @@ -24,7 +25,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_jewish_calendar_min_config(hass: HomeAssistant) -> None: """Test minimum jewish calendar configuration.""" - entry = MockConfigEntry(domain=DOMAIN, data={}) + entry = MockConfigEntry(title=DEFAULT_NAME, domain=DOMAIN, data={}) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -33,7 +34,9 @@ async def test_jewish_calendar_min_config(hass: HomeAssistant) -> None: async def test_jewish_calendar_hebrew(hass: HomeAssistant) -> None: """Test jewish calendar sensor with language set to hebrew.""" - entry = MockConfigEntry(domain=DOMAIN, data={"language": "hebrew"}) + entry = MockConfigEntry( + title=DEFAULT_NAME, domain=DOMAIN, data={"language": "hebrew"} + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -224,6 +227,7 @@ async def test_jewish_calendar_sensor( with alter_time(test_time): entry = MockConfigEntry( + title=DEFAULT_NAME, domain=DOMAIN, data={ CONF_LANGUAGE: language, @@ -565,6 +569,7 @@ async def test_shabbat_times_sensor( with alter_time(test_time): entry = MockConfigEntry( + title=DEFAULT_NAME, domain=DOMAIN, data={ CONF_LANGUAGE: language, @@ -625,7 +630,7 @@ async def test_omer_sensor(hass: HomeAssistant, test_time, result) -> None: test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone)) with alter_time(test_time): - entry = MockConfigEntry(domain=DOMAIN) + entry = MockConfigEntry(title=DEFAULT_NAME, domain=DOMAIN) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -660,7 +665,7 @@ async def test_dafyomi_sensor(hass: HomeAssistant, test_time, result) -> None: test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone)) with alter_time(test_time): - entry = MockConfigEntry(domain=DOMAIN) + entry = MockConfigEntry(title=DEFAULT_NAME, domain=DOMAIN) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() From 480ffeda2ceeedaadaf99c3e25a5d69246ca6170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Sun, 23 Jun 2024 18:56:10 +0100 Subject: [PATCH 0022/2411] Remove connection state handling from Idasen Desk (#120242) --- .../components/idasen_desk/__init__.py | 2 +- .../components/idasen_desk/coordinator.py | 39 ++---------- .../components/idasen_desk/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/idasen_desk/test_init.py | 63 +++---------------- 6 files changed, 20 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/idasen_desk/__init__.py b/homeassistant/components/idasen_desk/__init__.py index 1ea9b3b2f00..f0d8013cb50 100644 --- a/homeassistant/components/idasen_desk/__init__.py +++ b/homeassistant/components/idasen_desk/__init__.py @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) -> None: """Update from a Bluetooth callback to ensure that a new BLEDevice is fetched.""" _LOGGER.debug("Bluetooth callback triggered") - hass.async_create_task(coordinator.async_ensure_connection_state()) + hass.async_create_task(coordinator.async_connect_if_expected()) entry.async_on_unload( bluetooth.async_register_callback( diff --git a/homeassistant/components/idasen_desk/coordinator.py b/homeassistant/components/idasen_desk/coordinator.py index 5bdf1b37331..0661f2dede1 100644 --- a/homeassistant/components/idasen_desk/coordinator.py +++ b/homeassistant/components/idasen_desk/coordinator.py @@ -2,13 +2,12 @@ from __future__ import annotations -import asyncio import logging from idasen_ha import Desk from homeassistant.components import bluetooth -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -29,55 +28,29 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]): super().__init__(hass, logger, name=name) self._address = address self._expected_connected = False - self._connection_lost = False - self._disconnect_lock = asyncio.Lock() self.desk = Desk(self.async_set_updated_data) async def async_connect(self) -> bool: """Connect to desk.""" _LOGGER.debug("Trying to connect %s", self._address) + self._expected_connected = True ble_device = bluetooth.async_ble_device_from_address( self.hass, self._address, connectable=True ) if ble_device is None: _LOGGER.debug("No BLEDevice for %s", self._address) return False - self._expected_connected = True await self.desk.connect(ble_device) return True async def async_disconnect(self) -> None: """Disconnect from desk.""" - _LOGGER.debug("Disconnecting from %s", self._address) self._expected_connected = False - self._connection_lost = False + _LOGGER.debug("Disconnecting from %s", self._address) await self.desk.disconnect() - async def async_ensure_connection_state(self) -> None: - """Check if the expected connection state matches the current state. - - If the expected and current state don't match, calls connect/disconnect - as needed. - """ + async def async_connect_if_expected(self) -> None: + """Ensure that the desk is connected if that is the expected state.""" if self._expected_connected: - if not self.desk.is_connected: - _LOGGER.debug("Desk disconnected. Reconnecting") - self._connection_lost = True - await self.async_connect() - elif self._connection_lost: - _LOGGER.info("Reconnected to desk") - self._connection_lost = False - elif self.desk.is_connected: - if self._disconnect_lock.locked(): - _LOGGER.debug("Already disconnecting") - return - async with self._disconnect_lock: - _LOGGER.debug("Desk is connected but should not be. Disconnecting") - await self.desk.disconnect() - - @callback - def async_set_updated_data(self, data: int | None) -> None: - """Handle data update.""" - self.hass.async_create_task(self.async_ensure_connection_state()) - return super().async_set_updated_data(data) + await self.async_connect() diff --git a/homeassistant/components/idasen_desk/manifest.json b/homeassistant/components/idasen_desk/manifest.json index a912fabfa54..a09d155b5b0 100644 --- a/homeassistant/components/idasen_desk/manifest.json +++ b/homeassistant/components/idasen_desk/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/idasen_desk", "iot_class": "local_push", "quality_scale": "silver", - "requirements": ["idasen-ha==2.5.3"] + "requirements": ["idasen-ha==2.6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index ec94fe54fc9..1f4ada7f499 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1140,7 +1140,7 @@ ical==8.0.1 icmplib==3.0 # homeassistant.components.idasen_desk -idasen-ha==2.5.3 +idasen-ha==2.6.1 # homeassistant.components.network ifaddr==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0ca8262a19..65de7d41374 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -936,7 +936,7 @@ ical==8.0.1 icmplib==3.0 # homeassistant.components.idasen_desk -idasen-ha==2.5.3 +idasen-ha==2.6.1 # homeassistant.components.network ifaddr==0.2.0 diff --git a/tests/components/idasen_desk/test_init.py b/tests/components/idasen_desk/test_init.py index 60f1fb3e5e3..ae7bd5e3fdf 100644 --- a/tests/components/idasen_desk/test_init.py +++ b/tests/components/idasen_desk/test_init.py @@ -1,6 +1,5 @@ """Test the IKEA Idasen Desk init.""" -import asyncio from unittest import mock from unittest.mock import AsyncMock, MagicMock @@ -66,63 +65,21 @@ async def test_reconnect_on_bluetooth_callback( mock_desk_api.connect.assert_called_once() mock_register_callback.assert_called_once() - mock_desk_api.is_connected = False _, register_callback_args, _ = mock_register_callback.mock_calls[0] bt_callback = register_callback_args[1] + + mock_desk_api.connect.reset_mock() bt_callback(None, None) await hass.async_block_till_done() - assert mock_desk_api.connect.call_count == 2 + mock_desk_api.connect.assert_called_once() - -async def test_duplicated_disconnect_is_no_op( - hass: HomeAssistant, mock_desk_api: MagicMock -) -> None: - """Test that calling disconnect while disconnecting is a no-op.""" - await init_integration(hass) - - await hass.services.async_call( - "button", "press", {"entity_id": "button.test_disconnect"}, blocking=True - ) - await hass.async_block_till_done() - - async def mock_disconnect(): - await asyncio.sleep(0) - - mock_desk_api.disconnect.reset_mock() - mock_desk_api.disconnect.side_effect = mock_disconnect - - # Since the disconnect button was pressed but the desk indicates "connected", - # any update event will call disconnect() - mock_desk_api.is_connected = True - mock_desk_api.trigger_update_callback(None) - mock_desk_api.trigger_update_callback(None) - mock_desk_api.trigger_update_callback(None) - await hass.async_block_till_done() - mock_desk_api.disconnect.assert_called_once() - - -async def test_ensure_connection_state( - hass: HomeAssistant, mock_desk_api: MagicMock -) -> None: - """Test that the connection state is ensured.""" - await init_integration(hass) - - mock_desk_api.connect.reset_mock() - mock_desk_api.is_connected = False - mock_desk_api.trigger_update_callback(None) - await hass.async_block_till_done() - mock_desk_api.connect.assert_called_once() - - await hass.services.async_call( - "button", "press", {"entity_id": "button.test_disconnect"}, blocking=True - ) - await hass.async_block_till_done() - - mock_desk_api.disconnect.reset_mock() - mock_desk_api.is_connected = True - mock_desk_api.trigger_update_callback(None) - await hass.async_block_till_done() - mock_desk_api.disconnect.assert_called_once() + mock_desk_api.connect.reset_mock() + await hass.services.async_call( + "button", "press", {"entity_id": "button.test_disconnect"}, blocking=True + ) + bt_callback(None, None) + await hass.async_block_till_done() + assert mock_desk_api.connect.call_count == 0 async def test_unload_entry(hass: HomeAssistant, mock_desk_api: MagicMock) -> None: From 55a2645e78ae1509c480e64feb2a5c822ef312ba Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 23 Jun 2024 21:21:56 +0200 Subject: [PATCH 0023/2411] Bump async_upnp_client to 0.39.0 (#120250) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/device.py | 15 +++++---------- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dlna_dmr/conftest.py | 1 + tests/components/dlna_dmr/test_config_flow.py | 2 ++ tests/components/upnp/conftest.py | 11 +++++------ tests/components/upnp/test_binary_sensor.py | 11 +++++------ tests/components/upnp/test_sensor.py | 11 +++++------ 15 files changed, 32 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index ebbab957700..963a22850df 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "iot_class": "local_push", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.38.3", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.39.0", "getmac==0.9.4"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index c87e5e87779..e02326376b3 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["async-upnp-client==0.38.3"], + "requirements": ["async-upnp-client==0.39.0"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 460e191828e..7d9a8a9a0a8 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -39,7 +39,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.6.0", "wakeonlan==2.1.0", - "async-upnp-client==0.38.3" + "async-upnp-client==0.39.0" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 5e549c31806..304ee4b6410 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["async_upnp_client"], "quality_scale": "internal", - "requirements": ["async-upnp-client==0.38.3"] + "requirements": ["async-upnp-client==0.39.0"] } diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 0b9eecb1b15..bb0bcfc6a6e 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -154,14 +154,9 @@ class Device: async def async_get_data(self) -> dict[str, str | datetime | int | float | None]: """Get all data from device.""" _LOGGER.debug("Getting data for device: %s", self) - igd_state = await self._igd_device.async_get_traffic_and_status_data() - status_info = igd_state.status_info - if status_info is not None and not isinstance(status_info, BaseException): - wan_status = status_info.connection_status - router_uptime = status_info.uptime - else: - wan_status = None - router_uptime = None + igd_state = await self._igd_device.async_get_traffic_and_status_data( + force_poll=True + ) def get_value(value: Any) -> Any: if value is None or isinstance(value, BaseException): @@ -175,8 +170,8 @@ class Device: BYTES_SENT: get_value(igd_state.bytes_sent), PACKETS_RECEIVED: get_value(igd_state.packets_received), PACKETS_SENT: get_value(igd_state.packets_sent), - WAN_STATUS: wan_status, - ROUTER_UPTIME: router_uptime, + WAN_STATUS: get_value(igd_state.connection_status), + ROUTER_UPTIME: get_value(igd_state.uptime), ROUTER_IP: get_value(igd_state.external_ip_address), KIBIBYTES_PER_SEC_RECEIVED: igd_state.kibibytes_per_sec_received, KIBIBYTES_PER_SEC_SENT: igd_state.kibibytes_per_sec_sent, diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 7d353a475c7..b2972fc7790 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.38.3", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.39.0", "getmac==0.9.4"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index e9f304d38cb..4c63ab79baf 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], "quality_scale": "platinum", - "requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.3"], + "requirements": ["yeelight==0.7.14", "async-upnp-client==0.39.0"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7dfec9e63b3..a21d89705e8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ aiohttp_cors==0.7.0 aiozoneinfo==0.2.0 astral==2.2 async-interrupt==1.1.1 -async-upnp-client==0.38.3 +async-upnp-client==0.39.0 atomicwrites-homeassistant==1.4.1 attrs==23.2.0 awesomeversion==24.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1f4ada7f499..4f80886a568 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -490,7 +490,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.3 +async-upnp-client==0.39.0 # homeassistant.components.arve asyncarve==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65de7d41374..bafff5f2f77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -445,7 +445,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.3 +async-upnp-client==0.39.0 # homeassistant.components.arve asyncarve==0.0.9 diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py index 0d88009f58e..f470fbabc6f 100644 --- a/tests/components/dlna_dmr/conftest.py +++ b/tests/components/dlna_dmr/conftest.py @@ -72,6 +72,7 @@ def domain_data_mock(hass: HomeAssistant) -> Iterable[Mock]: service_id="urn:upnp-org:serviceId:RenderingControl", ), } + upnp_device.all_services = list(upnp_device.services.values()) seal(upnp_device) domain_data.upnp_factory.async_create_device.return_value = upnp_device diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 765d65ff0b9..a91cd4744d9 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -238,7 +238,9 @@ async def test_user_flow_embedded_st( embedded_device.device_type = MOCK_DEVICE_TYPE embedded_device.name = MOCK_DEVICE_NAME embedded_device.services = upnp_device.services + embedded_device.all_services = upnp_device.all_services upnp_device.services = {} + upnp_device.all_services = [] upnp_device.all_devices.append(embedded_device) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index 00e8db124f0..0bfcd062ac0 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, PropertyMock, create_autospec, p from urllib.parse import urlparse from async_upnp_client.client import UpnpDevice -from async_upnp_client.profiles.igd import IgdDevice, IgdState, StatusInfo +from async_upnp_client.profiles.igd import IgdDevice, IgdState import pytest from homeassistant.components import ssdp @@ -87,16 +87,15 @@ def mock_igd_device(mock_async_create_device) -> IgdDevice: bytes_sent=0, packets_received=0, packets_sent=0, - status_info=StatusInfo( - "Connected", - "", - 10, - ), + connection_status="Connected", + last_connection_error="", + uptime=10, external_ip_address="8.9.10.11", kibibytes_per_sec_received=None, kibibytes_per_sec_sent=None, packets_per_sec_received=None, packets_per_sec_sent=None, + port_mapping_number_of_entries=0, ) with patch( diff --git a/tests/components/upnp/test_binary_sensor.py b/tests/components/upnp/test_binary_sensor.py index 3a800ca75b9..087cd9e9fb4 100644 --- a/tests/components/upnp/test_binary_sensor.py +++ b/tests/components/upnp/test_binary_sensor.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta -from async_upnp_client.profiles.igd import IgdDevice, IgdState, StatusInfo +from async_upnp_client.profiles.igd import IgdDevice, IgdState from homeassistant.components.upnp.const import DEFAULT_SCAN_INTERVAL from homeassistant.core import HomeAssistant @@ -27,16 +27,15 @@ async def test_upnp_binary_sensors( bytes_sent=0, packets_received=0, packets_sent=0, - status_info=StatusInfo( - "Disconnected", - "", - 40, - ), + connection_status="Disconnected", + last_connection_error="", + uptime=40, external_ip_address="8.9.10.11", kibibytes_per_sec_received=None, kibibytes_per_sec_sent=None, packets_per_sec_received=None, packets_per_sec_sent=None, + port_mapping_number_of_entries=0, ) async_fire_time_changed( diff --git a/tests/components/upnp/test_sensor.py b/tests/components/upnp/test_sensor.py index 7dfbb144b01..67a64b265d9 100644 --- a/tests/components/upnp/test_sensor.py +++ b/tests/components/upnp/test_sensor.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta -from async_upnp_client.profiles.igd import IgdDevice, IgdState, StatusInfo +from async_upnp_client.profiles.igd import IgdDevice, IgdState from homeassistant.components.upnp.const import DEFAULT_SCAN_INTERVAL from homeassistant.core import HomeAssistant @@ -35,16 +35,15 @@ async def test_upnp_sensors( bytes_sent=20480, packets_received=30, packets_sent=40, - status_info=StatusInfo( - "Disconnected", - "", - 40, - ), + connection_status="Disconnected", + last_connection_error="", + uptime=40, external_ip_address="", kibibytes_per_sec_received=10.0, kibibytes_per_sec_sent=20.0, packets_per_sec_received=30.0, packets_per_sec_sent=40.0, + port_mapping_number_of_entries=0, ) now = dt_util.utcnow() From 436c36e3ddb766b7d6a8d55b0e1aab02fe4d2bb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Jun 2024 15:20:46 -0500 Subject: [PATCH 0024/2411] Bump aioesphomeapi to 24.6.1 (#120261) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index de855e15d4c..ab175028bea 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "mqtt": ["esphome/discover/#"], "requirements": [ - "aioesphomeapi==24.6.0", + "aioesphomeapi==24.6.1", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 4f80886a568..dbbfe7f6790 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -243,7 +243,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==24.6.0 +aioesphomeapi==24.6.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bafff5f2f77..ddf9a87d7ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==24.6.0 +aioesphomeapi==24.6.1 # homeassistant.components.flo aioflo==2021.11.0 From 143e8d09af3aee6fd27c22506e767f5739f76259 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Mon, 24 Jun 2024 00:04:16 +0300 Subject: [PATCH 0025/2411] Fix blocking call in Jewish Calendar while initializing location (#120265) --- .../components/jewish_calendar/__init__.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 81fe6cb5377..fd238e8d615 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from functools import partial + from hdate import Location import voluptuous as vol @@ -129,13 +131,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b CONF_HAVDALAH_OFFSET_MINUTES, DEFAULT_HAVDALAH_OFFSET_MINUTES ) - location = Location( - name=hass.config.location_name, - diaspora=diaspora, - latitude=config_entry.data.get(CONF_LATITUDE, hass.config.latitude), - longitude=config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), - altitude=config_entry.data.get(CONF_ELEVATION, hass.config.elevation), - timezone=config_entry.data.get(CONF_TIME_ZONE, hass.config.time_zone), + location = await hass.async_add_executor_job( + partial( + Location, + name=hass.config.location_name, + diaspora=diaspora, + latitude=config_entry.data.get(CONF_LATITUDE, hass.config.latitude), + longitude=config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), + altitude=config_entry.data.get(CONF_ELEVATION, hass.config.elevation), + timezone=config_entry.data.get(CONF_TIME_ZONE, hass.config.time_zone), + ) ) hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = { From 19f97a3e53ed99e85fb3778521aa0e4358229fbc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 23 Jun 2024 17:09:57 -0400 Subject: [PATCH 0026/2411] LLM to handle decimal attributes (#120257) --- homeassistant/helpers/llm.py | 5 ++++- tests/helpers/test_llm.py | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 53ec092fda2..6673786e2e1 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass +from decimal import Decimal from enum import Enum from functools import cache, partial from typing import Any @@ -461,7 +462,9 @@ def _get_exposed_entities( info["areas"] = ", ".join(area_names) if attributes := { - attr_name: str(attr_value) if isinstance(attr_value, Enum) else attr_value + attr_name: str(attr_value) + if isinstance(attr_value, (Enum, Decimal)) + else attr_value for attr_name, attr_value in state.attributes.items() if attr_name in interesting_attributes }: diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index e62d9ffdbee..5389490b401 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -1,5 +1,6 @@ """Tests for the llm helpers.""" +from decimal import Decimal from unittest.mock import patch import pytest @@ -402,7 +403,11 @@ async def test_assist_api_prompt( suggested_object_id="living_room", device_id=device.id, ) - hass.states.async_set(entry1.entity_id, "on", {"friendly_name": "Kitchen"}) + hass.states.async_set( + entry1.entity_id, + "on", + {"friendly_name": "Kitchen", "temperature": Decimal("0.9")}, + ) hass.states.async_set(entry2.entity_id, "on", {"friendly_name": "Living Room"}) def create_entity(device: dr.DeviceEntry, write_state=True) -> None: @@ -510,6 +515,9 @@ async def test_assist_api_prompt( entry1.entity_id: { "names": "Kitchen", "state": "on", + "attributes": { + "temperature": "0.9", + }, }, entry2.entity_id: { "areas": "Test Area, Alternative name", From 66b91a84f9209e0c58c944fbcc243b1b3cbc5f59 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Mon, 24 Jun 2024 03:39:58 +0200 Subject: [PATCH 0027/2411] mystrom: Add MAC and Config URL to devices (#120271) * Add MAC address to mystrom switch devices * Add configuration URL to mystrom switch devices --- homeassistant/components/mystrom/switch.py | 4 +++- tests/components/mystrom/__init__.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 9958fcf7f01..af135027aac 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -10,7 +10,7 @@ from pymystrom.exceptions import MyStromConnectionError from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo, format_mac from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, MANUFACTURER @@ -43,6 +43,8 @@ class MyStromSwitch(SwitchEntity): name=name, manufacturer=MANUFACTURER, sw_version=self.plug.firmware, + connections={("mac", format_mac(self.plug.mac))}, + configuration_url=self.plug.uri, ) async def async_turn_on(self, **kwargs: Any) -> None: diff --git a/tests/components/mystrom/__init__.py b/tests/components/mystrom/__init__.py index ac6ac1d8c54..8ee62996f92 100644 --- a/tests/components/mystrom/__init__.py +++ b/tests/components/mystrom/__init__.py @@ -173,3 +173,10 @@ class MyStromSwitchMock(MyStromDeviceMock): if not self._requested_state: return None return self._state["temperature"] + + @property + def uri(self) -> str | None: + """Return the URI.""" + if not self._requested_state: + return None + return f"http://{self._state["ip"]}" From d095d4e60d1ba7b0c4de814fc0f452d87ede878f Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Mon, 24 Jun 2024 07:53:15 +0200 Subject: [PATCH 0028/2411] Change suggested data rate unit to Mbit/s in pyLoad (#120275) Change data rate unit to Mbit/s --- homeassistant/components/pyload/sensor.py | 2 +- .../pyload/snapshots/test_sensor.ambr | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 8c35f8e7431..aa86dde9260 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -61,7 +61,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( translation_key=PyLoadSensorEntity.SPEED, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, - suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, suggested_display_precision=1, ), ) diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index 77a79e3eddd..f1e42ea049c 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -24,7 +24,7 @@ 'suggested_display_precision': 1, }), 'sensor.private': dict({ - 'suggested_unit_of_measurement': , + 'suggested_unit_of_measurement': , }), }), 'original_device_class': , @@ -35,7 +35,7 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_speed-state] @@ -43,7 +43,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'data_rate', 'friendly_name': 'pyLoad Speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.pyload_speed', @@ -78,7 +78,7 @@ 'suggested_display_precision': 1, }), 'sensor.private': dict({ - 'suggested_unit_of_measurement': , + 'suggested_unit_of_measurement': , }), }), 'original_device_class': , @@ -89,7 +89,7 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_speed-state] @@ -97,7 +97,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'data_rate', 'friendly_name': 'pyLoad Speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.pyload_speed', @@ -132,7 +132,7 @@ 'suggested_display_precision': 1, }), 'sensor.private': dict({ - 'suggested_unit_of_measurement': , + 'suggested_unit_of_measurement': , }), }), 'original_device_class': , @@ -143,7 +143,7 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensor_update_exceptions[ParserError][sensor.pyload_speed-state] @@ -151,7 +151,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'data_rate', 'friendly_name': 'pyLoad Speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.pyload_speed', @@ -363,7 +363,7 @@ 'suggested_display_precision': 1, }), 'sensor.private': dict({ - 'suggested_unit_of_measurement': , + 'suggested_unit_of_measurement': , }), }), 'original_device_class': , @@ -374,7 +374,7 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_setup[sensor.pyload_speed-state] @@ -382,13 +382,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'data_rate', 'friendly_name': 'pyLoad Speed', - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.pyload_speed', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5.405963', + 'state': '43.247704', }) # --- From fe3027f7de279d1708d4cefb02503f9bf0facb3d Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Mon, 24 Jun 2024 08:16:26 +0200 Subject: [PATCH 0029/2411] Adjust base entities in Husqvarna Automower (#120258) * adjust base entities * Adjust docstrings --- .../components/husqvarna_automower/button.py | 4 +-- .../components/husqvarna_automower/entity.py | 33 +++++++++++++++++-- .../husqvarna_automower/lawn_mower.py | 4 +-- .../components/husqvarna_automower/switch.py | 31 +---------------- 4 files changed, 35 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/button.py b/homeassistant/components/husqvarna_automower/button.py index 60c05b92a31..a9747108393 100644 --- a/homeassistant/components/husqvarna_automower/button.py +++ b/homeassistant/components/husqvarna_automower/button.py @@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AutomowerConfigEntry from .const import DOMAIN from .coordinator import AutomowerDataUpdateCoordinator -from .entity import AutomowerControlEntity +from .entity import AutomowerAvailableEntity _LOGGER = logging.getLogger(__name__) @@ -29,7 +29,7 @@ async def async_setup_entry( ) -class AutomowerButtonEntity(AutomowerControlEntity, ButtonEntity): +class AutomowerButtonEntity(AutomowerAvailableEntity, ButtonEntity): """Defining the AutomowerButtonEntity.""" _attr_translation_key = "confirm_error" diff --git a/homeassistant/components/husqvarna_automower/entity.py b/homeassistant/components/husqvarna_automower/entity.py index 4d20d2d677b..80a936c2caf 100644 --- a/homeassistant/components/husqvarna_automower/entity.py +++ b/homeassistant/components/husqvarna_automower/entity.py @@ -2,7 +2,7 @@ import logging -from aioautomower.model import MowerAttributes +from aioautomower.model import MowerActivities, MowerAttributes, MowerStates from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -12,6 +12,21 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +ERROR_ACTIVITIES = ( + MowerActivities.STOPPED_IN_GARDEN, + MowerActivities.UNKNOWN, + MowerActivities.NOT_APPLICABLE, +) +ERROR_STATES = [ + MowerStates.FATAL_ERROR, + MowerStates.ERROR, + MowerStates.ERROR_AT_POWER_UP, + MowerStates.NOT_APPLICABLE, + MowerStates.UNKNOWN, + MowerStates.STOPPED, + MowerStates.OFF, +] + class AutomowerBaseEntity(CoordinatorEntity[AutomowerDataUpdateCoordinator]): """Defining the Automower base Entity.""" @@ -41,10 +56,22 @@ class AutomowerBaseEntity(CoordinatorEntity[AutomowerDataUpdateCoordinator]): return self.coordinator.data[self.mower_id] -class AutomowerControlEntity(AutomowerBaseEntity): - """AutomowerControlEntity, for dynamic availability.""" +class AutomowerAvailableEntity(AutomowerBaseEntity): + """Replies available when the mower is connected.""" @property def available(self) -> bool: """Return True if the device is available.""" return super().available and self.mower_attributes.metadata.connected + + +class AutomowerControlEntity(AutomowerAvailableEntity): + """Replies available when the mower is connected and not in error state.""" + + @property + def available(self) -> bool: + """Return True if the device is available.""" + return super().available and ( + self.mower_attributes.mower.state not in ERROR_STATES + or self.mower_attributes.mower.activity not in ERROR_ACTIVITIES + ) diff --git a/homeassistant/components/husqvarna_automower/lawn_mower.py b/homeassistant/components/husqvarna_automower/lawn_mower.py index c0b566a7f66..e59d9e635e9 100644 --- a/homeassistant/components/husqvarna_automower/lawn_mower.py +++ b/homeassistant/components/husqvarna_automower/lawn_mower.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AutomowerConfigEntry from .const import DOMAIN from .coordinator import AutomowerDataUpdateCoordinator -from .entity import AutomowerControlEntity +from .entity import AutomowerAvailableEntity DOCKED_ACTIVITIES = (MowerActivities.PARKED_IN_CS, MowerActivities.CHARGING) MOWING_ACTIVITIES = ( @@ -94,7 +94,7 @@ async def async_setup_entry( ) -class AutomowerLawnMowerEntity(AutomowerControlEntity, LawnMowerEntity): +class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity): """Defining each mower Entity.""" _attr_name = None diff --git a/homeassistant/components/husqvarna_automower/switch.py b/homeassistant/components/husqvarna_automower/switch.py index a856e9c9050..8a450b8e81a 100644 --- a/homeassistant/components/husqvarna_automower/switch.py +++ b/homeassistant/components/husqvarna_automower/switch.py @@ -5,13 +5,7 @@ import logging from typing import TYPE_CHECKING, Any from aioautomower.exceptions import ApiException -from aioautomower.model import ( - MowerActivities, - MowerModes, - MowerStates, - StayOutZones, - Zone, -) +from aioautomower.model import MowerModes, StayOutZones, Zone from homeassistant.components.switch import SwitchEntity from homeassistant.const import Platform @@ -27,21 +21,6 @@ from .entity import AutomowerControlEntity _LOGGER = logging.getLogger(__name__) -ERROR_ACTIVITIES = ( - MowerActivities.STOPPED_IN_GARDEN, - MowerActivities.UNKNOWN, - MowerActivities.NOT_APPLICABLE, -) -ERROR_STATES = [ - MowerStates.FATAL_ERROR, - MowerStates.ERROR, - MowerStates.ERROR_AT_POWER_UP, - MowerStates.NOT_APPLICABLE, - MowerStates.UNKNOWN, - MowerStates.STOPPED, - MowerStates.OFF, -] - async def async_setup_entry( hass: HomeAssistant, @@ -88,14 +67,6 @@ class AutomowerScheduleSwitchEntity(AutomowerControlEntity, SwitchEntity): """Return the state of the switch.""" return self.mower_attributes.mower.mode != MowerModes.HOME - @property - def available(self) -> bool: - """Return True if the device is available.""" - return super().available and ( - self.mower_attributes.mower.state not in ERROR_STATES - or self.mower_attributes.mower.activity not in ERROR_ACTIVITIES - ) - async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" try: From fdade672119143191b9ae927b787272005b1ca54 Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Mon, 24 Jun 2024 08:20:34 +0200 Subject: [PATCH 0030/2411] Add device info for Aemet (#120243) * Update sensor.py * Update weather.py * Update sensor.py * ruff * add device info to entity * remove info from sensor * remove info from weather * ruff * amend entity * Update sensor.py * Update weather.py * ruff again * add DOMAIN * type unique_id * Update entity.py * Update entity.py * assert * update tests * change snapshot --- homeassistant/components/aemet/entity.py | 18 ++++++++++++++++++ homeassistant/components/aemet/sensor.py | 6 ++++-- homeassistant/components/aemet/weather.py | 2 +- .../aemet/snapshots/test_diagnostics.ambr | 2 +- tests/components/aemet/util.py | 1 + 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/aemet/entity.py b/homeassistant/components/aemet/entity.py index ba3f7e56193..f48eaa1593d 100644 --- a/homeassistant/components/aemet/entity.py +++ b/homeassistant/components/aemet/entity.py @@ -7,14 +7,32 @@ from typing import Any from aemet_opendata.helpers import dict_nested_value from homeassistant.components.weather import Forecast +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .const import DOMAIN from .coordinator import WeatherUpdateCoordinator class AemetEntity(CoordinatorEntity[WeatherUpdateCoordinator]): """Define an AEMET entity.""" + def __init__( + self, + coordinator: WeatherUpdateCoordinator, + name: str, + unique_id: str, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + name=name, + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, unique_id)}, + manufacturer="AEMET", + model="Forecast", + ) + def get_aemet_forecast(self, forecast_mode: str) -> list[Forecast]: """Return AEMET entity forecast by mode.""" return self.coordinator.data["forecast"][forecast_mode] diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index 268112070e8..cafb9be8a70 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -392,10 +392,12 @@ class AemetSensor(AemetEntity, SensorEntity): config_entry: ConfigEntry, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator) + assert config_entry.unique_id is not None + unique_id = config_entry.unique_id + super().__init__(coordinator, name, unique_id) self.entity_description = description self._attr_name = f"{name} {description.name}" - self._attr_unique_id = f"{config_entry.unique_id}-{description.key}" + self._attr_unique_id = f"{unique_id}-{description.key}" @property def native_value(self): diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index 4df0b1081f5..9c905941f62 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -71,7 +71,7 @@ class AemetWeather( coordinator: WeatherUpdateCoordinator, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator) + super().__init__(coordinator, name, unique_id) self._attr_name = name self._attr_unique_id = unique_id diff --git a/tests/components/aemet/snapshots/test_diagnostics.ambr b/tests/components/aemet/snapshots/test_diagnostics.ambr index 4b678dc1da5..8d4132cad84 100644 --- a/tests/components/aemet/snapshots/test_diagnostics.ambr +++ b/tests/components/aemet/snapshots/test_diagnostics.ambr @@ -20,7 +20,7 @@ 'pref_disable_polling': False, 'source': 'user', 'title': 'Mock Title', - 'unique_id': None, + 'unique_id': '**REDACTED**', 'version': 1, }), 'coord_data': dict({ diff --git a/tests/components/aemet/util.py b/tests/components/aemet/util.py index bb8885f7b4c..162ee657513 100644 --- a/tests/components/aemet/util.py +++ b/tests/components/aemet/util.py @@ -68,6 +68,7 @@ async def async_init_integration(hass: HomeAssistant): CONF_NAME: "AEMET", }, entry_id="7442b231f139e813fc1939281123f220", + unique_id="40.30403754--3.72935236", ) config_entry.add_to_hass(hass) From 6a5c1fc613982912b52f70fbaba7dcdd02e6445f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 24 Jun 2024 02:43:13 -0400 Subject: [PATCH 0031/2411] Replace custom validator from zwave_js with `from_dict` funcs (#120279) --- homeassistant/components/zwave_js/api.py | 63 +----------------------- 1 file changed, 2 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index fee828c9fd8..8f81790708f 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -166,65 +166,6 @@ STRATEGY = "strategy" MINIMUM_QR_STRING_LENGTH = 52 -def convert_planned_provisioning_entry(info: dict) -> ProvisioningEntry: - """Handle provisioning entry dict to ProvisioningEntry.""" - return ProvisioningEntry( - dsk=info[DSK], - security_classes=info[SECURITY_CLASSES], - status=info[STATUS], - requested_security_classes=info.get(REQUESTED_SECURITY_CLASSES), - additional_properties={ - k: v - for k, v in info.items() - if k not in (DSK, SECURITY_CLASSES, STATUS, REQUESTED_SECURITY_CLASSES) - }, - ) - - -def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation: - """Convert QR provisioning information dict to QRProvisioningInformation.""" - ## Remove this when we have fix for QRProvisioningInformation.from_dict() - return QRProvisioningInformation( - version=info[VERSION], - security_classes=info[SECURITY_CLASSES], - dsk=info[DSK], - generic_device_class=info[GENERIC_DEVICE_CLASS], - specific_device_class=info[SPECIFIC_DEVICE_CLASS], - installer_icon_type=info[INSTALLER_ICON_TYPE], - manufacturer_id=info[MANUFACTURER_ID], - product_type=info[PRODUCT_TYPE], - product_id=info[PRODUCT_ID], - application_version=info[APPLICATION_VERSION], - max_inclusion_request_interval=info.get(MAX_INCLUSION_REQUEST_INTERVAL), - uuid=info.get(UUID), - supported_protocols=info.get(SUPPORTED_PROTOCOLS), - status=info[STATUS], - requested_security_classes=info.get(REQUESTED_SECURITY_CLASSES), - additional_properties={ - k: v - for k, v in info.items() - if k - not in ( - VERSION, - SECURITY_CLASSES, - DSK, - GENERIC_DEVICE_CLASS, - SPECIFIC_DEVICE_CLASS, - INSTALLER_ICON_TYPE, - MANUFACTURER_ID, - PRODUCT_TYPE, - PRODUCT_ID, - APPLICATION_VERSION, - MAX_INCLUSION_REQUEST_INTERVAL, - UUID, - SUPPORTED_PROTOCOLS, - STATUS, - REQUESTED_SECURITY_CLASSES, - ) - }, - ) - - # Helper schemas PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All( vol.Schema( @@ -244,7 +185,7 @@ PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All( # Provisioning entries can have extra keys for SmartStart extra=vol.ALLOW_EXTRA, ), - convert_planned_provisioning_entry, + ProvisioningEntry.from_dict, ) QR_PROVISIONING_INFORMATION_SCHEMA = vol.All( @@ -278,7 +219,7 @@ QR_PROVISIONING_INFORMATION_SCHEMA = vol.All( }, extra=vol.ALLOW_EXTRA, ), - convert_qr_provisioning_information, + QRProvisioningInformation.from_dict, ) QR_CODE_STRING_SCHEMA = vol.All(str, vol.Length(min=MINIMUM_QR_STRING_LENGTH)) From fa9bced6b0788bbdd495203f8c88c6f0fab3932f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 23 Jun 2024 23:43:42 -0700 Subject: [PATCH 0032/2411] Load local calendar ics in background thread to avoid timezone I/O in event loop (#120276) --- homeassistant/components/local_calendar/calendar.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/local_calendar/calendar.py b/homeassistant/components/local_calendar/calendar.py index 213ee37ef37..66b3f80c19c 100644 --- a/homeassistant/components/local_calendar/calendar.py +++ b/homeassistant/components/local_calendar/calendar.py @@ -44,7 +44,9 @@ async def async_setup_entry( """Set up the local calendar platform.""" store = hass.data[DOMAIN][config_entry.entry_id] ics = await store.async_load() - calendar = IcsCalendarStream.calendar_from_ics(ics) + calendar: Calendar = await hass.async_add_executor_job( + IcsCalendarStream.calendar_from_ics, ics + ) calendar.prodid = PRODID name = config_entry.data[CONF_CALENDAR_NAME] From 4785810dc3811ad25b1b947cb4af8bd37682d2b8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jun 2024 08:54:46 +0200 Subject: [PATCH 0033/2411] Migrate AEMET to has entity name (#120284) --- homeassistant/components/aemet/entity.py | 5 ++++- homeassistant/components/aemet/sensor.py | 13 +++++-------- homeassistant/components/aemet/weather.py | 17 ++++++++--------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/aemet/entity.py b/homeassistant/components/aemet/entity.py index f48eaa1593d..562d82fd9c7 100644 --- a/homeassistant/components/aemet/entity.py +++ b/homeassistant/components/aemet/entity.py @@ -10,13 +10,16 @@ from homeassistant.components.weather import Forecast from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from .const import ATTRIBUTION, DOMAIN from .coordinator import WeatherUpdateCoordinator class AemetEntity(CoordinatorEntity[WeatherUpdateCoordinator]): """Define an AEMET entity.""" + _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True + def __init__( self, coordinator: WeatherUpdateCoordinator, diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index cafb9be8a70..83d490f7fe2 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -43,7 +43,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEGREE, PERCENTAGE, @@ -86,7 +85,6 @@ from .const import ( ATTR_API_WIND_BEARING, ATTR_API_WIND_MAX_SPEED, ATTR_API_WIND_SPEED, - ATTRIBUTION, CONDITIONS_MAP, ) from .coordinator import WeatherUpdateCoordinator @@ -366,12 +364,15 @@ async def async_setup_entry( name = domain_data.name coordinator = domain_data.coordinator + unique_id = config_entry.unique_id + assert unique_id is not None + async_add_entities( AemetSensor( name, coordinator, description, - config_entry, + unique_id, ) for description in FORECAST_SENSORS + WEATHER_SENSORS if dict_nested_value(coordinator.data["lib"], description.keys) is not None @@ -381,7 +382,6 @@ async def async_setup_entry( class AemetSensor(AemetEntity, SensorEntity): """Implementation of an AEMET OpenData sensor.""" - _attr_attribution = ATTRIBUTION entity_description: AemetSensorEntityDescription def __init__( @@ -389,14 +389,11 @@ class AemetSensor(AemetEntity, SensorEntity): name: str, coordinator: WeatherUpdateCoordinator, description: AemetSensorEntityDescription, - config_entry: ConfigEntry, + unique_id: str, ) -> None: """Initialize the sensor.""" - assert config_entry.unique_id is not None - unique_id = config_entry.unique_id super().__init__(coordinator, name, unique_id) self.entity_description = description - self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{unique_id}-{description.key}" @property diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index 9c905941f62..341b81d71c4 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -28,7 +28,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AemetConfigEntry -from .const import ATTRIBUTION, CONDITIONS_MAP +from .const import CONDITIONS_MAP from .coordinator import WeatherUpdateCoordinator from .entity import AemetEntity @@ -43,10 +43,10 @@ async def async_setup_entry( name = domain_data.name weather_coordinator = domain_data.coordinator - async_add_entities( - [AemetWeather(name, config_entry.unique_id, weather_coordinator)], - False, - ) + unique_id = config_entry.unique_id + assert unique_id is not None + + async_add_entities([AemetWeather(name, unique_id, weather_coordinator)]) class AemetWeather( @@ -55,7 +55,6 @@ class AemetWeather( ): """Implementation of an AEMET OpenData weather.""" - _attr_attribution = ATTRIBUTION _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS _attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_temperature_unit = UnitOfTemperature.CELSIUS @@ -63,16 +62,16 @@ class AemetWeather( _attr_supported_features = ( WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY ) + _attr_name = None def __init__( self, - name, - unique_id, + name: str, + unique_id: str, coordinator: WeatherUpdateCoordinator, ) -> None: """Initialize the sensor.""" super().__init__(coordinator, name, unique_id) - self._attr_name = name self._attr_unique_id = unique_id @property From 5c2db162c4042d9a29e75191de532fa45fbff58a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Jun 2024 02:57:59 -0400 Subject: [PATCH 0034/2411] Remove "no API" prompt (#120280) --- .../conversation.py | 41 ++++++++----------- .../openai_conversation/conversation.py | 39 ++++++++---------- homeassistant/helpers/llm.py | 10 ++--- .../snapshots/test_conversation.ambr | 6 --- .../test_conversation.py | 4 -- 5 files changed, 41 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index b9f0006dbff..2cfbc09ed08 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -349,27 +349,22 @@ class GoogleGenerativeAIConversationEntity( ): user_name = user.name - if llm_api: - api_prompt = llm_api.api_prompt - else: - api_prompt = llm.async_render_no_api_prompt(self.hass) - - return "\n".join( - ( - template.Template( - llm.BASE_PROMPT - + self.entry.options.get( - CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT - ), - self.hass, - ).async_render( - { - "ha_name": self.hass.config.location_name, - "user_name": user_name, - "llm_context": llm_context, - }, - parse_result=False, - ), - api_prompt, + parts = [ + template.Template( + llm.BASE_PROMPT + + self.entry.options.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT), + self.hass, + ).async_render( + { + "ha_name": self.hass.config.location_name, + "user_name": user_name, + "llm_context": llm_context, + }, + parse_result=False, ) - ) + ] + + if llm_api: + parts.append(llm_api.api_prompt) + + return "\n".join(parts) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index d0b3ef8f895..40242f5c6cc 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -172,28 +172,20 @@ class OpenAIConversationEntity( user_name = user.name try: - if llm_api: - api_prompt = llm_api.api_prompt - else: - api_prompt = llm.async_render_no_api_prompt(self.hass) - - prompt = "\n".join( - ( - template.Template( - llm.BASE_PROMPT - + options.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT), - self.hass, - ).async_render( - { - "ha_name": self.hass.config.location_name, - "user_name": user_name, - "llm_context": llm_context, - }, - parse_result=False, - ), - api_prompt, + prompt_parts = [ + template.Template( + llm.BASE_PROMPT + + options.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT), + self.hass, + ).async_render( + { + "ha_name": self.hass.config.location_name, + "user_name": user_name, + "llm_context": llm_context, + }, + parse_result=False, ) - ) + ] except TemplateError as err: LOGGER.error("Error rendering prompt: %s", err) @@ -206,6 +198,11 @@ class OpenAIConversationEntity( response=intent_response, conversation_id=conversation_id ) + if llm_api: + prompt_parts.append(llm_api.api_prompt) + + prompt = "\n".join(prompt_parts) + # Create a copy of the variable because we attach it to the trace messages = [ ChatCompletionSystemMessageParam(role="system", content=prompt), diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 6673786e2e1..a4e18fdb2c0 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -51,11 +51,11 @@ Answer in plain text. Keep it simple and to the point. @callback def async_render_no_api_prompt(hass: HomeAssistant) -> str: - """Return the prompt to be used when no API is configured.""" - return ( - "Only if the user wants to control a device, tell them to edit the AI configuration " - "and allow access to Home Assistant." - ) + """Return the prompt to be used when no API is configured. + + No longer used since Home Assistant 2024.7. + """ + return "" @singleton("llm") diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index aec8d088b20..b0a0ce967de 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -35,7 +35,6 @@ You are a voice assistant for Home Assistant. Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. - Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. ''', 'role': 'user', }), @@ -88,7 +87,6 @@ You are a voice assistant for Home Assistant. Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. - Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. ''', 'role': 'user', }), @@ -142,7 +140,6 @@ You are a voice assistant for Home Assistant. Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. - Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. ''', 'tools': None, }), @@ -187,7 +184,6 @@ You are a voice assistant for Home Assistant. Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. - Only if the user wants to control a device, tell them to edit the AI configuration and allow access to Home Assistant. ''', 'tools': None, }), @@ -244,7 +240,6 @@ You are a voice assistant for Home Assistant. Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. - ''', 'tools': None, }), @@ -293,7 +288,6 @@ You are a voice assistant for Home Assistant. Answer questions about the world truthfully. Answer in plain text. Keep it simple and to the point. - ''', 'tools': None, }), diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 7f4fe886e90..990058aa89d 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -75,10 +75,6 @@ async def test_default_prompt( "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_api_prompt", return_value="", ), - patch( - "homeassistant.components.google_generative_ai_conversation.conversation.llm.async_render_no_api_prompt", - return_value="", - ), ): mock_chat = AsyncMock() mock_model.return_value.start_chat.return_value = mock_chat From ab9cbf64da39fab4583fd1c0becc159342ab0b9a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jun 2024 09:54:22 +0200 Subject: [PATCH 0035/2411] Add sensors to Airgradient (#120286) --- homeassistant/components/airgradient/const.py | 8 + .../components/airgradient/select.py | 15 +- .../components/airgradient/sensor.py | 173 +++++- .../components/airgradient/strings.json | 37 ++ .../airgradient/snapshots/test_sensor.ambr | 552 ++++++++++++++++++ 5 files changed, 750 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/airgradient/const.py b/homeassistant/components/airgradient/const.py index bbb15a3741d..2817a27e37b 100644 --- a/homeassistant/components/airgradient/const.py +++ b/homeassistant/components/airgradient/const.py @@ -2,6 +2,14 @@ import logging +from airgradient import PmStandard + DOMAIN = "airgradient" LOGGER = logging.getLogger(__package__) + +PM_STANDARD = { + PmStandard.UGM3: "ugm3", + PmStandard.USAQI: "us_aqi", +} +PM_STANDARD_REVERSE = {v: k for k, v in PM_STANDARD.items()} diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index 8fac06917fd..e85e1224000 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -4,12 +4,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from airgradient import AirGradientClient, Config -from airgradient.models import ( - ConfigurationControl, - LedBarMode, - PmStandard, - TemperatureUnit, -) +from airgradient.models import ConfigurationControl, LedBarMode, TemperatureUnit from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import EntityCategory @@ -18,16 +13,10 @@ from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AirGradientConfigEntry -from .const import DOMAIN +from .const import DOMAIN, PM_STANDARD, PM_STANDARD_REVERSE from .coordinator import AirGradientConfigCoordinator from .entity import AirGradientEntity -PM_STANDARD = { - PmStandard.UGM3: "ugm3", - PmStandard.USAQI: "us_aqi", -} -PM_STANDARD_REVERSE = {v: k for k, v in PM_STANDARD.items()} - @dataclass(frozen=True, kw_only=True) class AirGradientSelectEntityDescription(SelectEntityDescription): diff --git a/homeassistant/components/airgradient/sensor.py b/homeassistant/components/airgradient/sensor.py index 6123d4289f9..f431c49ed2a 100644 --- a/homeassistant/components/airgradient/sensor.py +++ b/homeassistant/components/airgradient/sensor.py @@ -3,7 +3,13 @@ from collections.abc import Callable from dataclasses import dataclass -from airgradient.models import Measures +from airgradient import Config +from airgradient.models import ( + ConfigurationControl, + LedBarMode, + Measures, + TemperatureUnit, +) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -18,60 +24,69 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS_MILLIWATT, EntityCategory, UnitOfTemperature, + UnitOfTime, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import AirGradientConfigEntry -from .coordinator import AirGradientMeasurementCoordinator +from .const import PM_STANDARD, PM_STANDARD_REVERSE +from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator from .entity import AirGradientEntity @dataclass(frozen=True, kw_only=True) -class AirGradientSensorEntityDescription(SensorEntityDescription): - """Describes AirGradient sensor entity.""" +class AirGradientMeasurementSensorEntityDescription(SensorEntityDescription): + """Describes AirGradient measurement sensor entity.""" value_fn: Callable[[Measures], StateType] -SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = ( - AirGradientSensorEntityDescription( +@dataclass(frozen=True, kw_only=True) +class AirGradientConfigSensorEntityDescription(SensorEntityDescription): + """Describes AirGradient config sensor entity.""" + + value_fn: Callable[[Config], StateType] + + +MEASUREMENT_SENSOR_TYPES: tuple[AirGradientMeasurementSensorEntityDescription, ...] = ( + AirGradientMeasurementSensorEntityDescription( key="pm01", device_class=SensorDeviceClass.PM1, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.pm01, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="pm02", device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.pm02, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="pm10", device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.pm10, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.ambient_temperature, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.relative_humidity, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="signal_strength", device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -80,33 +95,33 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, value_fn=lambda status: status.signal_strength, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="tvoc", translation_key="total_volatile_organic_component_index", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.total_volatile_organic_component_index, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="nitrogen_index", translation_key="nitrogen_index", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.nitrogen_index, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="co2", device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.rco2, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="pm003", translation_key="pm003_count", native_unit_of_measurement="particles/dL", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.pm003_count, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="nox_raw", translation_key="raw_nitrogen", native_unit_of_measurement="ticks", @@ -114,7 +129,7 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, value_fn=lambda status: status.raw_nitrogen, ), - AirGradientSensorEntityDescription( + AirGradientMeasurementSensorEntityDescription( key="tvoc_raw", translation_key="raw_total_volatile_organic_component", native_unit_of_measurement="ticks", @@ -124,6 +139,77 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = ( ), ) +CONFIG_SENSOR_TYPES: tuple[AirGradientConfigSensorEntityDescription, ...] = ( + AirGradientConfigSensorEntityDescription( + key="co2_automatic_baseline_calibration_days", + translation_key="co2_automatic_baseline_calibration_days", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.DAYS, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: config.co2_automatic_baseline_calibration_days, + ), + AirGradientConfigSensorEntityDescription( + key="nox_learning_offset", + translation_key="nox_learning_offset", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.DAYS, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: config.nox_learning_offset, + ), + AirGradientConfigSensorEntityDescription( + key="tvoc_learning_offset", + translation_key="tvoc_learning_offset", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.DAYS, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: config.tvoc_learning_offset, + ), +) + +CONFIG_LED_BAR_SENSOR_TYPES: tuple[AirGradientConfigSensorEntityDescription, ...] = ( + AirGradientConfigSensorEntityDescription( + key="led_bar_mode", + translation_key="led_bar_mode", + device_class=SensorDeviceClass.ENUM, + options=[x.value for x in LedBarMode], + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: config.led_bar_mode, + ), + AirGradientConfigSensorEntityDescription( + key="led_bar_brightness", + translation_key="led_bar_brightness", + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: config.led_bar_brightness, + ), +) + +CONFIG_DISPLAY_SENSOR_TYPES: tuple[AirGradientConfigSensorEntityDescription, ...] = ( + AirGradientConfigSensorEntityDescription( + key="display_temperature_unit", + translation_key="display_temperature_unit", + device_class=SensorDeviceClass.ENUM, + options=[x.value for x in TemperatureUnit], + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: config.temperature_unit, + ), + AirGradientConfigSensorEntityDescription( + key="display_pm_standard", + translation_key="display_pm_standard", + device_class=SensorDeviceClass.ENUM, + options=list(PM_STANDARD_REVERSE), + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: PM_STANDARD.get(config.pm_standard), + ), + AirGradientConfigSensorEntityDescription( + key="display_brightness", + translation_key="display_brightness", + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda config: config.display_brightness, + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -134,7 +220,9 @@ async def async_setup_entry( coordinator = entry.runtime_data.measurement listener: Callable[[], None] | None = None - not_setup: set[AirGradientSensorEntityDescription] = set(SENSOR_TYPES) + not_setup: set[AirGradientMeasurementSensorEntityDescription] = set( + MEASUREMENT_SENSOR_TYPES + ) @callback def add_entities() -> None: @@ -147,7 +235,7 @@ async def async_setup_entry( if description.value_fn(coordinator.data) is None: not_setup.add(description) else: - sensors.append(AirGradientSensor(coordinator, description)) + sensors.append(AirGradientMeasurementSensor(coordinator, description)) if sensors: async_add_entities(sensors) @@ -159,17 +247,33 @@ async def async_setup_entry( add_entities() + entities = [ + AirGradientConfigSensor(entry.runtime_data.config, description) + for description in CONFIG_SENSOR_TYPES + ] + if "L" in coordinator.data.model: + entities.extend( + AirGradientConfigSensor(entry.runtime_data.config, description) + for description in CONFIG_LED_BAR_SENSOR_TYPES + ) + if "I" in coordinator.data.model: + entities.extend( + AirGradientConfigSensor(entry.runtime_data.config, description) + for description in CONFIG_DISPLAY_SENSOR_TYPES + ) + async_add_entities(entities) -class AirGradientSensor(AirGradientEntity, SensorEntity): + +class AirGradientMeasurementSensor(AirGradientEntity, SensorEntity): """Defines an AirGradient sensor.""" - entity_description: AirGradientSensorEntityDescription + entity_description: AirGradientMeasurementSensorEntityDescription coordinator: AirGradientMeasurementCoordinator def __init__( self, coordinator: AirGradientMeasurementCoordinator, - description: AirGradientSensorEntityDescription, + description: AirGradientMeasurementSensorEntityDescription, ) -> None: """Initialize airgradient sensor.""" super().__init__(coordinator) @@ -180,3 +284,28 @@ class AirGradientSensor(AirGradientEntity, SensorEntity): def native_value(self) -> StateType: """Return the state of the sensor.""" return self.entity_description.value_fn(self.coordinator.data) + + +class AirGradientConfigSensor(AirGradientEntity, SensorEntity): + """Defines an AirGradient sensor.""" + + entity_description: AirGradientConfigSensorEntityDescription + coordinator: AirGradientConfigCoordinator + + def __init__( + self, + coordinator: AirGradientConfigCoordinator, + description: AirGradientConfigSensorEntityDescription, + ) -> None: + """Initialize airgradient sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" + self._attr_entity_registry_enabled_default = ( + coordinator.data.configuration_control is not ConfigurationControl.LOCAL + ) + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 0ab80286570..6c079419839 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -78,6 +78,43 @@ }, "raw_nitrogen": { "name": "Raw NOx" + }, + "display_pm_standard": { + "name": "[%key:component::airgradient::entity::select::display_pm_standard::name%]", + "state": { + "ugm3": "[%key:component::airgradient::entity::select::display_pm_standard::state::ugm3%]", + "us_aqi": "[%key:component::airgradient::entity::select::display_pm_standard::state::us_aqi%]" + } + }, + "co2_automatic_baseline_calibration_days": { + "name": "Carbon dioxide automatic baseline calibration" + }, + "nox_learning_offset": { + "name": "NOx learning offset" + }, + "tvoc_learning_offset": { + "name": "VOC learning offset" + }, + "led_bar_mode": { + "name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]", + "state": { + "off": "[%key:component::airgradient::entity::select::led_bar_mode::state::off%]", + "co2": "[%key:component::airgradient::entity::select::led_bar_mode::state::co2%]", + "pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]" + } + }, + "led_bar_brightness": { + "name": "[%key:component::airgradient::entity::number::led_bar_brightness::name%]" + }, + "display_temperature_unit": { + "name": "[%key:component::airgradient::entity::select::display_temperature_unit::name%]", + "state": { + "c": "[%key:component::airgradient::entity::select::display_temperature_unit::state::c%]", + "f": "[%key:component::airgradient::entity::select::display_temperature_unit::state::f%]" + } + }, + "display_brightness": { + "name": "[%key:component::airgradient::entity::number::display_brightness::name%]" } } }, diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index e96d2be1004..c3d14c7d8fc 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -50,6 +50,213 @@ 'state': '778', }) # --- +# name: test_all_entities[indoor][sensor.airgradient_carbon_dioxide_automatic_baseline_calibration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_carbon_dioxide_automatic_baseline_calibration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Carbon dioxide automatic baseline calibration', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co2_automatic_baseline_calibration_days', + 'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration_days', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_carbon_dioxide_automatic_baseline_calibration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Airgradient Carbon dioxide automatic baseline calibration', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_carbon_dioxide_automatic_baseline_calibration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_display_brightness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_display_brightness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Display brightness', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'display_brightness', + 'unique_id': '84fce612f5b8-display_brightness', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_display_brightness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Display brightness', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.airgradient_display_brightness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_display_pm_standard-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'ugm3', + 'us_aqi', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_display_pm_standard', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Display PM standard', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'display_pm_standard', + 'unique_id': '84fce612f5b8-display_pm_standard', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_display_pm_standard-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Airgradient Display PM standard', + 'options': list([ + 'ugm3', + 'us_aqi', + ]), + }), + 'context': , + 'entity_id': 'sensor.airgradient_display_pm_standard', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ugm3', + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_display_temperature_unit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'c', + 'f', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_display_temperature_unit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Display temperature unit', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'display_temperature_unit', + 'unique_id': '84fce612f5b8-display_temperature_unit', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_display_temperature_unit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Airgradient Display temperature unit', + 'options': list([ + 'c', + 'f', + ]), + }), + 'context': , + 'entity_id': 'sensor.airgradient_display_temperature_unit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'c', + }) +# --- # name: test_all_entities[indoor][sensor.airgradient_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -101,6 +308,111 @@ 'state': '48.0', }) # --- +# name: test_all_entities[indoor][sensor.airgradient_led_bar_brightness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_led_bar_brightness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'LED bar brightness', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'led_bar_brightness', + 'unique_id': '84fce612f5b8-led_bar_brightness', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_led_bar_brightness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient LED bar brightness', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.airgradient_led_bar_brightness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_led_bar_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'co2', + 'pm', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_led_bar_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'LED bar mode', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'led_bar_mode', + 'unique_id': '84fce612f5b8-led_bar_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_led_bar_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Airgradient LED bar mode', + 'options': list([ + 'off', + 'co2', + 'pm', + ]), + }), + 'context': , + 'entity_id': 'sensor.airgradient_led_bar_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'co2', + }) +# --- # name: test_all_entities[indoor][sensor.airgradient_nox_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -150,6 +462,54 @@ 'state': '1', }) # --- +# name: test_all_entities[indoor][sensor.airgradient_nox_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'NOx learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nox_learning_offset', + 'unique_id': '84fce612f5b8-nox_learning_offset', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_nox_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Airgradient NOx learning offset', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- # name: test_all_entities[indoor][sensor.airgradient_pm0_3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -604,6 +964,102 @@ 'state': '99', }) # --- +# name: test_all_entities[indoor][sensor.airgradient_voc_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VOC learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tvoc_learning_offset', + 'unique_id': '84fce612f5b8-tvoc_learning_offset', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[indoor][sensor.airgradient_voc_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Airgradient VOC learning offset', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_carbon_dioxide_automatic_baseline_calibration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_carbon_dioxide_automatic_baseline_calibration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Carbon dioxide automatic baseline calibration', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co2_automatic_baseline_calibration_days', + 'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration_days', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_carbon_dioxide_automatic_baseline_calibration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Airgradient Carbon dioxide automatic baseline calibration', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_carbon_dioxide_automatic_baseline_calibration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- # name: test_all_entities[outdoor][sensor.airgradient_nox_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -653,6 +1109,54 @@ 'state': '1', }) # --- +# name: test_all_entities[outdoor][sensor.airgradient_nox_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'NOx learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nox_learning_offset', + 'unique_id': '84fce612f5b8-nox_learning_offset', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_nox_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Airgradient NOx learning offset', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- # name: test_all_entities[outdoor][sensor.airgradient_raw_nox-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -853,3 +1357,51 @@ 'state': '49', }) # --- +# name: test_all_entities[outdoor][sensor.airgradient_voc_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VOC learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tvoc_learning_offset', + 'unique_id': '84fce612f5b8-tvoc_learning_offset', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[outdoor][sensor.airgradient_voc_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Airgradient VOC learning offset', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- From 213cb6f0fd04ec75eb702868a3fe17114b2d1e6f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Mon, 24 Jun 2024 09:57:04 +0200 Subject: [PATCH 0036/2411] Improve Plugwise runtime-updating (#120230) --- .../components/plugwise/binary_sensor.py | 28 ++++++++----------- homeassistant/components/plugwise/climate.py | 7 ++--- .../components/plugwise/coordinator.py | 16 ++++------- homeassistant/components/plugwise/number.py | 7 ++--- homeassistant/components/plugwise/select.py | 7 ++--- homeassistant/components/plugwise/sensor.py | 27 ++++++------------ homeassistant/components/plugwise/switch.py | 22 ++++++--------- 7 files changed, 41 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index ef1051fa7b2..4b251d20a02 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -89,26 +89,20 @@ async def async_setup_entry( if not coordinator.new_devices: return - entities: list[PlugwiseBinarySensorEntity] = [] - for device_id, device in coordinator.data.devices.items(): - if not (binary_sensors := device.get("binary_sensors")): - continue - for description in BINARY_SENSORS: - if description.key not in binary_sensors: - continue - - entities.append( - PlugwiseBinarySensorEntity( - coordinator, - device_id, - description, - ) + async_add_entities( + PlugwiseBinarySensorEntity(coordinator, device_id, description) + for device_id in coordinator.new_devices + if ( + binary_sensors := coordinator.data.devices[device_id].get( + "binary_sensors" ) - async_add_entities(entities) - - entry.async_on_unload(coordinator.async_add_listener(_add_entities)) + ) + for description in BINARY_SENSORS + if description.key in binary_sensors + ) _add_entities() + entry.async_on_unload(coordinator.async_add_listener(_add_entities)) class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity): diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 29d44fe8159..7b0fe35835d 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -41,13 +41,12 @@ async def async_setup_entry( async_add_entities( PlugwiseClimateEntity(coordinator, device_id) - for device_id, device in coordinator.data.devices.items() - if device["dev_class"] in MASTER_THERMOSTATS + for device_id in coordinator.new_devices + if coordinator.data.devices[device_id]["dev_class"] in MASTER_THERMOSTATS ) - entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + entry.async_on_unload(coordinator.async_add_listener(_add_entities)) class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py index 34d983510ed..1dff11d26d8 100644 --- a/homeassistant/components/plugwise/coordinator.py +++ b/homeassistant/components/plugwise/coordinator.py @@ -15,7 +15,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError -from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -55,8 +54,8 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): timeout=30, websession=async_get_clientsession(hass, verify_ssl=False), ) - self.device_list: list[dr.DeviceEntry] = [] - self.new_devices: bool = False + self._current_devices: set[str] = set() + self.new_devices: set[str] = set() async def _connect(self) -> None: """Connect to the Plugwise Smile.""" @@ -81,13 +80,8 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): raise ConfigEntryError("Device with unsupported firmware") from err except ConnectionFailedError as err: raise UpdateFailed("Failed to connect to the Plugwise Smile") from err - - device_reg = dr.async_get(self.hass) - device_list = dr.async_entries_for_config_entry( - device_reg, self.config_entry.entry_id - ) - - self.new_devices = len(data.devices.keys()) - len(self.device_list) > 0 - self.device_list = device_list + else: + self.new_devices = set(data.devices) - self._current_devices + self._current_devices = set(data.devices) return data diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index c84ca2cf5c7..1f12b2374b3 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -81,14 +81,13 @@ async def async_setup_entry( async_add_entities( PlugwiseNumberEntity(coordinator, device_id, description) - for device_id, device in coordinator.data.devices.items() + for device_id in coordinator.new_devices for description in NUMBER_TYPES - if description.key in device + if description.key in coordinator.data.devices[device_id] ) - entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + entry.async_on_unload(coordinator.async_add_listener(_add_entities)) class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index 88c97b9b9f3..c8c9791c0da 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -74,14 +74,13 @@ async def async_setup_entry( async_add_entities( PlugwiseSelectEntity(coordinator, device_id, description) - for device_id, device in coordinator.data.devices.items() + for device_id in coordinator.new_devices for description in SELECT_TYPES - if description.options_key in device + if description.options_key in coordinator.data.devices[device_id] ) - entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + entry.async_on_unload(coordinator.async_add_listener(_add_entities)) class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 147bab828a8..ae5b4e6ed91 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -414,27 +414,16 @@ async def async_setup_entry( if not coordinator.new_devices: return - entities: list[PlugwiseSensorEntity] = [] - for device_id, device in coordinator.data.devices.items(): - if not (sensors := device.get("sensors")): - continue - for description in SENSORS: - if description.key not in sensors: - continue - - entities.append( - PlugwiseSensorEntity( - coordinator, - device_id, - description, - ) - ) - - async_add_entities(entities) - - entry.async_on_unload(coordinator.async_add_listener(_add_entities)) + async_add_entities( + PlugwiseSensorEntity(coordinator, device_id, description) + for device_id in coordinator.new_devices + if (sensors := coordinator.data.devices[device_id].get("sensors")) + for description in SENSORS + if description.key in sensors + ) _add_entities() + entry.async_on_unload(coordinator.async_add_listener(_add_entities)) class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity): diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py index 3ed2d14b8dd..a134ab5b044 100644 --- a/homeassistant/components/plugwise/switch.py +++ b/homeassistant/components/plugwise/switch.py @@ -68,22 +68,16 @@ async def async_setup_entry( if not coordinator.new_devices: return - entities: list[PlugwiseSwitchEntity] = [] - for device_id, device in coordinator.data.devices.items(): - if not (switches := device.get("switches")): - continue - for description in SWITCHES: - if description.key not in switches: - continue - entities.append( - PlugwiseSwitchEntity(coordinator, device_id, description) - ) - - async_add_entities(entities) - - entry.async_on_unload(coordinator.async_add_listener(_add_entities)) + async_add_entities( + PlugwiseSwitchEntity(coordinator, device_id, description) + for device_id in coordinator.new_devices + if (switches := coordinator.data.devices[device_id].get("switches")) + for description in SWITCHES + if description.key in switches + ) _add_entities() + entry.async_on_unload(coordinator.async_add_listener(_add_entities)) class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity): From b798d7670634d4318d6a2cbef52d6a411aa2de05 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jun 2024 09:57:15 +0200 Subject: [PATCH 0037/2411] Update mypy-dev to 1.11.0a9 (#120289) --- .../bluetooth/passive_update_processor.py | 15 +++++++-------- homeassistant/components/recorder/core.py | 2 ++ homeassistant/components/sonos/__init__.py | 9 ++++----- homeassistant/components/tessie/lock.py | 2 +- requirements_test.txt | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 29ebda3488b..3e7e4e96659 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -146,20 +146,19 @@ class PassiveBluetoothDataUpdate[_T]: """ device_change = False changed_entity_keys: set[PassiveBluetoothEntityKey] = set() - for key, device_info in new_data.devices.items(): - if device_change or self.devices.get(key, UNDEFINED) != device_info: + for device_key, device_info in new_data.devices.items(): + if device_change or self.devices.get(device_key, UNDEFINED) != device_info: device_change = True - self.devices[key] = device_info + self.devices[device_key] = device_info for incoming, current in ( (new_data.entity_descriptions, self.entity_descriptions), (new_data.entity_names, self.entity_names), (new_data.entity_data, self.entity_data), ): - # mypy can't seem to work this out - for key, data in incoming.items(): # type: ignore[attr-defined] - if current.get(key, UNDEFINED) != data: # type: ignore[attr-defined] - changed_entity_keys.add(key) # type: ignore[arg-type] - current[key] = data # type: ignore[index] + for key, data in incoming.items(): + if current.get(key, UNDEFINED) != data: + changed_entity_keys.add(key) + current[key] = data # type: ignore[assignment] # If the device changed we don't need to return the changed # entity keys as all entities will be updated return None if device_change else changed_entity_keys diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index a5eecf42f22..4e5ac04c3bf 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -85,6 +85,7 @@ from .db_schema import ( ) from .executor import DBInterruptibleThreadPoolExecutor from .migration import ( + BaseRunTimeMigration, EntityIDMigration, EventsContextIDMigration, EventTypeIDMigration, @@ -805,6 +806,7 @@ class Recorder(threading.Thread): for row in execute_stmt_lambda_element(session, get_migration_changes()) } + migrator: BaseRunTimeMigration for migrator_cls in (StatesContextIDMigration, EventsContextIDMigration): migrator = migrator_cls(session, schema_version, migration_changes) if migrator.needs_migrate(): diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 2049cb4c8c7..912a8d04f4e 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -9,7 +9,7 @@ import datetime from functools import partial import logging import socket -from typing import TYPE_CHECKING, Any, cast +from typing import Any, cast from urllib.parse import urlparse from aiohttp import ClientError @@ -372,12 +372,11 @@ class SonosDiscoveryManager: (SonosAlarms, self.data.alarms), (SonosFavorites, self.data.favorites), ): - if TYPE_CHECKING: - coord_dict = cast(dict[str, Any], coord_dict) - if soco.household_id not in coord_dict: + c_dict: dict[str, Any] = coord_dict + if soco.household_id not in c_dict: new_coordinator = coordinator(self.hass, soco.household_id) new_coordinator.setup(soco) - coord_dict[soco.household_id] = new_coordinator + c_dict[soco.household_id] = new_coordinator speaker.setup(self.entry) except (OSError, SoCoException, Timeout) as ex: _LOGGER.warning("Failed to add SonosSpeaker using %s: %s", soco, ex) diff --git a/homeassistant/components/tessie/lock.py b/homeassistant/components/tessie/lock.py index 9457d476e32..0ea65ce4781 100644 --- a/homeassistant/components/tessie/lock.py +++ b/homeassistant/components/tessie/lock.py @@ -35,7 +35,7 @@ async def async_setup_entry( """Set up the Tessie sensor platform from a config entry.""" data = entry.runtime_data - entities = [ + entities: list[TessieEntity] = [ klass(vehicle) for klass in (TessieLockEntity, TessieCableLockEntity) for vehicle in data.vehicles diff --git a/requirements_test.txt b/requirements_test.txt index fce669c4929..460da410db6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ astroid==3.2.2 coverage==7.5.3 freezegun==1.5.0 mock-open==1.4.0 -mypy-dev==1.11.0a8 +mypy-dev==1.11.0a9 pre-commit==3.7.1 pydantic==1.10.17 pylint==3.2.2 From 0dff7e8a55fc48375abad2733b1a1265be4a790c Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 24 Jun 2024 01:02:30 -0700 Subject: [PATCH 0038/2411] Bump PyFlume to 0.8.7 (#120288) --- homeassistant/components/flume/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index 953d9791f2f..bb6783bafbe 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/flume", "iot_class": "cloud_polling", "loggers": ["pyflume"], - "requirements": ["PyFlume==0.6.5"] + "requirements": ["PyFlume==0.8.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index dbbfe7f6790..52b887723fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,7 +54,7 @@ PyChromecast==14.0.1 PyFlick==0.0.2 # homeassistant.components.flume -PyFlume==0.6.5 +PyFlume==0.8.7 # homeassistant.components.fronius PyFronius==0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddf9a87d7ee..88caa904094 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,7 +45,7 @@ PyChromecast==14.0.1 PyFlick==0.0.2 # homeassistant.components.flume -PyFlume==0.6.5 +PyFlume==0.8.7 # homeassistant.components.fronius PyFronius==0.7.3 From deee10813c48904bc5fabce619716adb8ed3d47b Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 24 Jun 2024 01:08:51 -0700 Subject: [PATCH 0039/2411] Ensure flume sees the most recent notifications (#120290) --- homeassistant/components/flume/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/flume/coordinator.py b/homeassistant/components/flume/coordinator.py index 30e7962304c..c75bffdc615 100644 --- a/homeassistant/components/flume/coordinator.py +++ b/homeassistant/components/flume/coordinator.py @@ -98,7 +98,7 @@ class FlumeNotificationDataUpdateCoordinator(DataUpdateCoordinator[None]): # The related binary sensors (leak detected, high flow, low battery) # will be active until the notification is deleted in the Flume app. self.notifications = pyflume.FlumeNotificationList( - self.auth, read=None + self.auth, read=None, sort_direction="DESC" ).notification_list _LOGGER.debug("Notifications %s", self.notifications) From 158c8b84008b53d3b96559e68b8234e433a2dafb Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Mon, 24 Jun 2024 10:37:32 +0200 Subject: [PATCH 0040/2411] Add optional test fixture collection to enphase_envoy diagnostic report (#116242) * diagnostics_fixtures * fix codespell errors * fix merge order and typo * remove pointless-string-statement --- .../components/enphase_envoy/config_flow.py | 50 +- .../components/enphase_envoy/const.py | 3 + .../components/enphase_envoy/diagnostics.py | 52 +- .../components/enphase_envoy/strings.json | 10 + tests/components/enphase_envoy/conftest.py | 13 +- .../snapshots/test_diagnostics.ambr | 8033 +++++++++++++++++ .../enphase_envoy/test_config_flow.py | 42 +- .../enphase_envoy/test_diagnostics.py | 85 +- 8 files changed, 8282 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index e115f0c6ea8..695709627b7 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -12,12 +12,22 @@ from pyenphase import AUTH_TOKEN_MIN_VERSION, Envoy, EnvoyError import voluptuous as vol from homeassistant.components import zeroconf -from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlowWithConfigEntry, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.httpx_client import get_async_client -from .const import DOMAIN, INVALID_AUTH_ERRORS +from .const import ( + DOMAIN, + INVALID_AUTH_ERRORS, + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE, +) _LOGGER = logging.getLogger(__name__) @@ -50,6 +60,12 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN): self.protovers: str | None = None self._reauth_entry: ConfigEntry | None = None + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> EnvoyOptionsFlowHandler: + """Options flow handler for Enphase_Envoy.""" + return EnvoyOptionsFlowHandler(config_entry) + @callback def _async_generate_schema(self) -> vol.Schema: """Generate schema.""" @@ -282,3 +298,33 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders=description_placeholders, errors=errors, ) + + +class EnvoyOptionsFlowHandler(OptionsFlowWithConfigEntry): + """Envoy config flow options handler.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, + default=self.config_entry.options.get( + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE, + ), + ): bool, + } + ), + description_placeholders={ + CONF_SERIAL: self.config_entry.unique_id, + CONF_HOST: self.config_entry.data.get("host"), + }, + ) diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index fe8e7e9ec1f..80ce8604f24 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -15,3 +15,6 @@ PLATFORMS = [ ] INVALID_AUTH_ERRORS = (EnvoyAuthenticationError, EnvoyAuthenticationRequired) + +OPTION_DIAGNOSTICS_INCLUDE_FIXTURES = "diagnostics_include_fixtures" +OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE = False diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py index 28d9690ae70..0fe7be8aaef 100644 --- a/homeassistant/components/enphase_envoy/diagnostics.py +++ b/homeassistant/components/enphase_envoy/diagnostics.py @@ -6,6 +6,8 @@ import copy from typing import TYPE_CHECKING, Any from attr import asdict +from pyenphase.envoy import Envoy +from pyenphase.exceptions import EnvoyError from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -21,7 +23,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.json import json_dumps from homeassistant.util.json import json_loads -from .const import DOMAIN +from .const import DOMAIN, OPTION_DIAGNOSTICS_INCLUDE_FIXTURES from .coordinator import EnphaseUpdateCoordinator CONF_TITLE = "title" @@ -38,6 +40,46 @@ TO_REDACT = { } +async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]: + """Collect Envoy endpoints to use for test fixture set.""" + fixture_data: dict[str, Any] = {} + end_points = [ + "/info", + "/api/v1/production", + "/api/v1/production/inverters", + "/production.json", + "/production.json?details=1", + "/production", + "/ivp/ensemble/power", + "/ivp/ensemble/inventory", + "/ivp/ensemble/dry_contacts", + "/ivp/ensemble/status", + "/ivp/ensemble/secctrl", + "/ivp/ss/dry_contact_settings", + "/admin/lib/tariff", + "/ivp/ss/gen_config", + "/ivp/ss/gen_schedule", + "/ivp/sc/pvlimit", + "/ivp/ss/pel_settings", + "/ivp/ensemble/generator", + "/ivp/meters", + "/ivp/meters/readings", + ] + + for end_point in end_points: + response = await envoy.request(end_point) + fixture_data[end_point] = response.text.replace("\n", "").replace( + serial, CLEAN_TEXT + ) + fixture_data[f"{end_point}_log"] = json_dumps( + { + "headers": dict(response.headers.items()), + "code": response.status_code, + } + ) + return fixture_data + + async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: @@ -113,12 +155,20 @@ async def async_get_config_entry_diagnostics( "ct_storage_meter": envoy.storage_meter_type, } + fixture_data: dict[str, Any] = {} + if entry.options.get(OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, False): + try: + fixture_data = await _get_fixture_collection(envoy=envoy, serial=old_serial) + except EnvoyError as err: + fixture_data["Error"] = repr(err) + diagnostic_data: dict[str, Any] = { "config_entry": async_redact_data(entry.as_dict(), TO_REDACT), "envoy_properties": envoy_properties, "raw_data": json_loads(coordinator_data_cleaned), "envoy_model_data": envoy_model, "envoy_entities_by_device": json_loads(device_entities_cleaned), + "fixtures": fixture_data, } return diagnostic_data diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index 295aa1948f8..f7964bf2f45 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -36,6 +36,16 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, + "options": { + "step": { + "init": { + "title": "Envoy {serial} {host} options", + "data": { + "diagnostics_include_fixtures": "Include test fixture data in diagnostic report. Use when requested to provide test data for troubleshooting or development activies. With this option enabled the diagnostic report may take more time to download. When report is created best disable this option again." + } + } + } + }, "entity": { "binary_sensor": { "communicating": { diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py index 965af3b40fc..5dd62419b2b 100644 --- a/tests/components/enphase_envoy/conftest.py +++ b/tests/components/enphase_envoy/conftest.py @@ -339,11 +339,22 @@ def mock_envoy_fixture( raw={"varies_by": "firmware_version"}, ) mock_envoy.update = AsyncMock(return_value=mock_envoy.data) + + response = Mock() + response.status_code = 200 + response.text = "Testing request \nreplies." + response.headers = {"Hello": "World"} + mock_envoy.request = AsyncMock(return_value=response) + return mock_envoy @pytest.fixture(name="setup_enphase_envoy") -async def setup_enphase_envoy_fixture(hass: HomeAssistant, config, mock_envoy): +async def setup_enphase_envoy_fixture( + hass: HomeAssistant, + config, + mock_envoy, +): """Define a fixture to set up Enphase Envoy.""" with ( patch( diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index c2ab51a7dbd..008922e8d2b 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -3986,6 +3986,8039 @@ 'CTMETERS', ]), }), + 'fixtures': dict({ + }), + 'raw_data': dict({ + 'varies_by': 'firmware_version', + }), + }) +# --- +# name: test_entry_diagnostics_with_fixtures + dict({ + 'config_entry': dict({ + 'data': dict({ + 'host': '1.1.1.1', + 'name': '**REDACTED**', + 'password': '**REDACTED**', + 'token': '**REDACTED**', + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'enphase_envoy', + 'entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'minor_version': 1, + 'options': dict({ + 'diagnostics_include_fixtures': True, + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': '**REDACTED**', + 'version': 1, + }), + 'envoy_entities_by_device': list([ + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + '45a36e55aaddb2007c5f6602e0c38e72', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '<>56789', + 'identifiers': list([ + list([ + 'enphase_envoy', + '<>', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Enphase', + 'model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', + 'name': 'Envoy <>', + 'name_by_user': None, + 'serial_number': '<>', + 'suggested_area': None, + 'sw_version': '7.1.2', + }), + 'entities': list([ + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production', + 'unique_id': '<>_production', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power production', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_production', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production', + 'unique_id': '<>_daily_production', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production today', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_today', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production', + 'unique_id': '<>_seven_days_production', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production last seven days', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production', + 'unique_id': '<>_lifetime_production', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy production', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production', + 'state': '0.00<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption', + 'unique_id': '<>_consumption', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power consumption', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_consumption', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption', + 'unique_id': '<>_daily_consumption', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption today', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_today', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption', + 'unique_id': '<>_seven_days_consumption', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption last seven days', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption', + 'unique_id': '<>_lifetime_consumption', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy consumption', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption', + 'state': '0.00<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption', + 'unique_id': '<>_lifetime_net_consumption', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime net energy consumption', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption', + 'state': '0.02<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production', + 'unique_id': '<>_lifetime_net_production', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime net energy production', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production', + 'state': '0.022345', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption', + 'unique_id': '<>_net_consumption', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current net power consumption', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption', + 'state': '0.101', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency', + 'unique_id': '<>_frequency', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage', + 'unique_id': '<>_voltage', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status', + 'unique_id': '<>_net_consumption_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags', + 'unique_id': '<>_net_consumption_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l1', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l1', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l2', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l2', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l3', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l3', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status', + 'unique_id': '<>_production_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags', + 'unique_id': '<>_production_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged', + 'unique_id': '<>_lifetime_battery_discharged', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime battery energy discharged', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged', + 'state': '0.03<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged', + 'unique_id': '<>_lifetime_battery_charged', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime battery energy charged', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged', + 'state': '0.032345', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge', + 'unique_id': '<>_battery_discharge', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current battery discharge', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_battery_discharge', + 'state': '0.103', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage', + 'unique_id': '<>_storage_voltage', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status', + 'unique_id': '<>_storage_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags', + 'unique_id': '<>_storage_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l1', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l2', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l3', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + '45a36e55aaddb2007c5f6602e0c38e72', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'identifiers': list([ + list([ + 'enphase_envoy', + '1', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Enphase', + 'model': 'Inverter', + 'name': 'Inverter 1', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + }), + 'entities': list([ + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.inverter_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': None, + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1', + 'unit_of_measurement': 'W', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Inverter 1', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'W', + }), + 'entity_id': 'sensor.inverter_1', + 'state': '1', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.inverter_1_last_reported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'timestamp', + 'original_icon': 'mdi:flash', + 'original_name': 'Last reported', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_reported', + 'unique_id': '1_last_reported', + 'unit_of_measurement': None, + }), + 'state': None, + }), + ]), + }), + ]), + 'envoy_model_data': dict({ + 'ctmeter_consumption': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000020', timestamp=1708006120, energy_delivered=21234, energy_received=22345, active_power=101, power_factor=0.21, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'ctmeter_consumption_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000021', timestamp=1708006121, energy_delivered=212341, energy_received=223451, active_power=21, power_factor=0.22, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000022', timestamp=1708006122, energy_delivered=212342, energy_received=223452, active_power=31, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000023', timestamp=1708006123, energy_delivered=212343, energy_received=223453, active_power=51, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'ctmeter_production': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000010', timestamp=1708006110, energy_delivered=11234, energy_received=12345, active_power=100, power_factor=0.11, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[, ])", + }), + 'ctmeter_production_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000011', timestamp=1708006111, energy_delivered=112341, energy_received=123451, active_power=20, power_factor=0.12, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000012', timestamp=1708006112, energy_delivered=112342, energy_received=123452, active_power=30, power_factor=0.13, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000013', timestamp=1708006113, energy_delivered=112343, energy_received=123453, active_power=50, power_factor=0.14, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'ctmeter_storage': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000030', timestamp=1708006120, energy_delivered=31234, energy_received=32345, active_power=103, power_factor=0.23, voltage=113, current=0.4, frequency=50.3, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'ctmeter_storage_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000031', timestamp=1708006121, energy_delivered=312341, energy_received=323451, active_power=22, power_factor=0.32, voltage=113, current=0.4, frequency=50.3, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000032', timestamp=1708006122, energy_delivered=312342, energy_received=323452, active_power=33, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000033', timestamp=1708006123, energy_delivered=312343, energy_received=323453, active_power=53, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'dry_contact_settings': dict({ + }), + 'dry_contact_status': dict({ + }), + 'encharge_aggregate': None, + 'encharge_inventory': None, + 'encharge_power': None, + 'enpower': None, + 'inverters': dict({ + '1': dict({ + '__type': "", + 'repr': "EnvoyInverter(serial_number='1', last_report_date=1, last_report_watts=1, max_report_watts=1)", + }), + }), + 'system_consumption': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=1234, watt_hours_last_7_days=1234, watt_hours_today=1234, watts_now=1234)', + }), + 'system_consumption_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=1322, watt_hours_last_7_days=1321, watt_hours_today=1323, watts_now=1324)', + }), + 'L2': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=2322, watt_hours_last_7_days=2321, watt_hours_today=2323, watts_now=2324)', + }), + 'L3': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=3322, watt_hours_last_7_days=3321, watt_hours_today=3323, watts_now=3324)', + }), + }), + 'system_production': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=1234, watt_hours_last_7_days=1234, watt_hours_today=1234, watts_now=1234)', + }), + 'system_production_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=1232, watt_hours_last_7_days=1231, watt_hours_today=1233, watts_now=1234)', + }), + 'L2': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=2232, watt_hours_last_7_days=2231, watt_hours_today=2233, watts_now=2234)', + }), + 'L3': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=3232, watt_hours_last_7_days=3231, watt_hours_today=3233, watts_now=3234)', + }), + }), + 'tariff': None, + }), + 'envoy_properties': dict({ + 'active_phasecount': 3, + 'ct_consumption_meter': 'net-consumption', + 'ct_count': 3, + 'ct_production_meter': 'production', + 'ct_storage_meter': 'storage', + 'envoy_firmware': '7.1.2', + 'envoy_model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', + 'part_number': '123456789', + 'phase_count': 3, + 'phase_mode': 'three', + 'supported_features': list([ + 'INVERTERS', + 'METERING', + 'PRODUCTION', + 'THREEPHASE', + 'CTMETERS', + ]), + }), + 'fixtures': dict({ + '/admin/lib/tariff': 'Testing request replies.', + '/admin/lib/tariff_log': '{"headers":{"Hello":"World"},"code":200}', + '/api/v1/production': 'Testing request replies.', + '/api/v1/production/inverters': 'Testing request replies.', + '/api/v1/production/inverters_log': '{"headers":{"Hello":"World"},"code":200}', + '/api/v1/production_log': '{"headers":{"Hello":"World"},"code":200}', + '/info': 'Testing request replies.', + '/info_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ensemble/dry_contacts': 'Testing request replies.', + '/ivp/ensemble/dry_contacts_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ensemble/generator': 'Testing request replies.', + '/ivp/ensemble/generator_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ensemble/inventory': 'Testing request replies.', + '/ivp/ensemble/inventory_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ensemble/power': 'Testing request replies.', + '/ivp/ensemble/power_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ensemble/secctrl': 'Testing request replies.', + '/ivp/ensemble/secctrl_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ensemble/status': 'Testing request replies.', + '/ivp/ensemble/status_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/meters': 'Testing request replies.', + '/ivp/meters/readings': 'Testing request replies.', + '/ivp/meters/readings_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/meters_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/sc/pvlimit': 'Testing request replies.', + '/ivp/sc/pvlimit_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ss/dry_contact_settings': 'Testing request replies.', + '/ivp/ss/dry_contact_settings_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ss/gen_config': 'Testing request replies.', + '/ivp/ss/gen_config_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ss/gen_schedule': 'Testing request replies.', + '/ivp/ss/gen_schedule_log': '{"headers":{"Hello":"World"},"code":200}', + '/ivp/ss/pel_settings': 'Testing request replies.', + '/ivp/ss/pel_settings_log': '{"headers":{"Hello":"World"},"code":200}', + '/production': 'Testing request replies.', + '/production.json': 'Testing request replies.', + '/production.json?details=1': 'Testing request replies.', + '/production.json?details=1_log': '{"headers":{"Hello":"World"},"code":200}', + '/production.json_log': '{"headers":{"Hello":"World"},"code":200}', + '/production_log': '{"headers":{"Hello":"World"},"code":200}', + }), + 'raw_data': dict({ + 'varies_by': 'firmware_version', + }), + }) +# --- +# name: test_entry_diagnostics_with_fixtures_with_error + dict({ + 'config_entry': dict({ + 'data': dict({ + 'host': '1.1.1.1', + 'name': '**REDACTED**', + 'password': '**REDACTED**', + 'token': '**REDACTED**', + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'enphase_envoy', + 'entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'minor_version': 1, + 'options': dict({ + 'diagnostics_include_fixtures': True, + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': '**REDACTED**', + 'version': 1, + }), + 'envoy_entities_by_device': list([ + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + '45a36e55aaddb2007c5f6602e0c38e72', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '<>56789', + 'identifiers': list([ + list([ + 'enphase_envoy', + '<>', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Enphase', + 'model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', + 'name': 'Envoy <>', + 'name_by_user': None, + 'serial_number': '<>', + 'suggested_area': None, + 'sw_version': '7.1.2', + }), + 'entities': list([ + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production', + 'unique_id': '<>_production', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power production', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_production', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production', + 'unique_id': '<>_daily_production', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production today', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_today', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production', + 'unique_id': '<>_seven_days_production', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production last seven days', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production', + 'unique_id': '<>_lifetime_production', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy production', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production', + 'state': '0.00<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption', + 'unique_id': '<>_consumption', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power consumption', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_consumption', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption', + 'unique_id': '<>_daily_consumption', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption today', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_today', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption', + 'unique_id': '<>_seven_days_consumption', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption last seven days', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption', + 'unique_id': '<>_lifetime_consumption', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy consumption', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption', + 'state': '0.00<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption', + 'unique_id': '<>_lifetime_net_consumption', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime net energy consumption', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption', + 'state': '0.02<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production', + 'unique_id': '<>_lifetime_net_production', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime net energy production', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production', + 'state': '0.022345', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption', + 'unique_id': '<>_net_consumption', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current net power consumption', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption', + 'state': '0.101', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency', + 'unique_id': '<>_frequency', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage', + 'unique_id': '<>_voltage', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status', + 'unique_id': '<>_net_consumption_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags', + 'unique_id': '<>_net_consumption_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l1', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l1', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l2', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l2', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l3', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l3', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status', + 'unique_id': '<>_production_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags', + 'unique_id': '<>_production_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged', + 'unique_id': '<>_lifetime_battery_discharged', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime battery energy discharged', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged', + 'state': '0.03<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged', + 'unique_id': '<>_lifetime_battery_charged', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime battery energy charged', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged', + 'state': '0.032345', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge', + 'unique_id': '<>_battery_discharge', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current battery discharge', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_battery_discharge', + 'state': '0.103', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage', + 'unique_id': '<>_storage_voltage', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status', + 'unique_id': '<>_storage_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags', + 'unique_id': '<>_storage_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l1', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l2', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l3', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + '45a36e55aaddb2007c5f6602e0c38e72', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'identifiers': list([ + list([ + 'enphase_envoy', + '1', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Enphase', + 'model': 'Inverter', + 'name': 'Inverter 1', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + }), + 'entities': list([ + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.inverter_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': None, + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1', + 'unit_of_measurement': 'W', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Inverter 1', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'W', + }), + 'entity_id': 'sensor.inverter_1', + 'state': '1', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.inverter_1_last_reported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'timestamp', + 'original_icon': 'mdi:flash', + 'original_name': 'Last reported', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_reported', + 'unique_id': '1_last_reported', + 'unit_of_measurement': None, + }), + 'state': None, + }), + ]), + }), + ]), + 'envoy_model_data': dict({ + 'ctmeter_consumption': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000020', timestamp=1708006120, energy_delivered=21234, energy_received=22345, active_power=101, power_factor=0.21, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'ctmeter_consumption_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000021', timestamp=1708006121, energy_delivered=212341, energy_received=223451, active_power=21, power_factor=0.22, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000022', timestamp=1708006122, energy_delivered=212342, energy_received=223452, active_power=31, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000023', timestamp=1708006123, energy_delivered=212343, energy_received=223453, active_power=51, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'ctmeter_production': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000010', timestamp=1708006110, energy_delivered=11234, energy_received=12345, active_power=100, power_factor=0.11, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[, ])", + }), + 'ctmeter_production_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000011', timestamp=1708006111, energy_delivered=112341, energy_received=123451, active_power=20, power_factor=0.12, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000012', timestamp=1708006112, energy_delivered=112342, energy_received=123452, active_power=30, power_factor=0.13, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000013', timestamp=1708006113, energy_delivered=112343, energy_received=123453, active_power=50, power_factor=0.14, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'ctmeter_storage': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000030', timestamp=1708006120, energy_delivered=31234, energy_received=32345, active_power=103, power_factor=0.23, voltage=113, current=0.4, frequency=50.3, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'ctmeter_storage_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000031', timestamp=1708006121, energy_delivered=312341, energy_received=323451, active_power=22, power_factor=0.32, voltage=113, current=0.4, frequency=50.3, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000032', timestamp=1708006122, energy_delivered=312342, energy_received=323452, active_power=33, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000033', timestamp=1708006123, energy_delivered=312343, energy_received=323453, active_power=53, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'dry_contact_settings': dict({ + }), + 'dry_contact_status': dict({ + }), + 'encharge_aggregate': None, + 'encharge_inventory': None, + 'encharge_power': None, + 'enpower': None, + 'inverters': dict({ + '1': dict({ + '__type': "", + 'repr': "EnvoyInverter(serial_number='1', last_report_date=1, last_report_watts=1, max_report_watts=1)", + }), + }), + 'system_consumption': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=1234, watt_hours_last_7_days=1234, watt_hours_today=1234, watts_now=1234)', + }), + 'system_consumption_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=1322, watt_hours_last_7_days=1321, watt_hours_today=1323, watts_now=1324)', + }), + 'L2': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=2322, watt_hours_last_7_days=2321, watt_hours_today=2323, watts_now=2324)', + }), + 'L3': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=3322, watt_hours_last_7_days=3321, watt_hours_today=3323, watts_now=3324)', + }), + }), + 'system_production': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=1234, watt_hours_last_7_days=1234, watt_hours_today=1234, watts_now=1234)', + }), + 'system_production_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=1232, watt_hours_last_7_days=1231, watt_hours_today=1233, watts_now=1234)', + }), + 'L2': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=2232, watt_hours_last_7_days=2231, watt_hours_today=2233, watts_now=2234)', + }), + 'L3': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=3232, watt_hours_last_7_days=3231, watt_hours_today=3233, watts_now=3234)', + }), + }), + 'tariff': None, + }), + 'envoy_properties': dict({ + 'active_phasecount': 3, + 'ct_consumption_meter': 'net-consumption', + 'ct_count': 3, + 'ct_production_meter': 'production', + 'ct_storage_meter': 'storage', + 'envoy_firmware': '7.1.2', + 'envoy_model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', + 'part_number': '123456789', + 'phase_count': 3, + 'phase_mode': 'three', + 'supported_features': list([ + 'INVERTERS', + 'METERING', + 'PRODUCTION', + 'THREEPHASE', + 'CTMETERS', + ]), + }), + 'fixtures': dict({ + 'Error': "EnvoyError('Test')", + }), 'raw_data': dict({ 'varies_by': 'firmware_version', }), diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 7e1808ffa52..b60b03e5df9 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -10,7 +10,12 @@ from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.components.enphase_envoy.const import DOMAIN, PLATFORMS +from homeassistant.components.enphase_envoy.const import ( + DOMAIN, + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE, + PLATFORMS, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -656,6 +661,41 @@ async def test_reauth(hass: HomeAssistant, config_entry, setup_enphase_envoy) -> assert result2["reason"] == "reauth_successful" +async def test_options_default( + hass: HomeAssistant, config_entry, setup_enphase_envoy +) -> None: + """Test we can configure options.""" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert config_entry.options == { + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: OPTION_DIAGNOSTICS_INCLUDE_FIXTURES_DEFAULT_VALUE + } + + +async def test_options_set( + hass: HomeAssistant, config_entry, setup_enphase_envoy +) -> None: + """Test we can configure options.""" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: True} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert config_entry.options == {OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: True} + + async def test_reconfigure( hass: HomeAssistant, config_entry, setup_enphase_envoy ) -> None: diff --git a/tests/components/enphase_envoy/test_diagnostics.py b/tests/components/enphase_envoy/test_diagnostics.py index a3b4f8e0f3c..9ee6b7905e7 100644 --- a/tests/components/enphase_envoy/test_diagnostics.py +++ b/tests/components/enphase_envoy/test_diagnostics.py @@ -1,10 +1,20 @@ """Test Enphase Envoy diagnostics.""" -from syrupy import SnapshotAssertion +from unittest.mock import AsyncMock, patch +from pyenphase.exceptions import EnvoyError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.enphase_envoy.const import ( + DOMAIN, + OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -35,3 +45,76 @@ async def test_entry_diagnostics( assert await get_diagnostics_for_config_entry( hass, hass_client, config_entry ) == snapshot(exclude=limit_diagnostic_attrs) + + +@pytest.fixture(name="config_entry_options") +def config_entry_options_fixture(hass: HomeAssistant, config, serial_number): + """Define a config entry fixture.""" + entry = MockConfigEntry( + domain=DOMAIN, + entry_id="45a36e55aaddb2007c5f6602e0c38e72", + title=f"Envoy {serial_number}" if serial_number else "Envoy", + unique_id=serial_number, + data=config, + options={OPTION_DIAGNOSTICS_INCLUDE_FIXTURES: True}, + ) + entry.add_to_hass(hass) + return entry + + +async def test_entry_diagnostics_with_fixtures( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + config_entry_options: ConfigEntry, + setup_enphase_envoy, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry_options + ) == snapshot(exclude=limit_diagnostic_attrs) + + +@pytest.fixture(name="setup_enphase_envoy_options_error") +async def setup_enphase_envoy_options_error_fixture( + hass: HomeAssistant, + config, + mock_envoy_options_error, +): + """Define a fixture to set up Enphase Envoy.""" + with ( + patch( + "homeassistant.components.enphase_envoy.config_flow.Envoy", + return_value=mock_envoy_options_error, + ), + patch( + "homeassistant.components.enphase_envoy.Envoy", + return_value=mock_envoy_options_error, + ), + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + yield + + +@pytest.fixture(name="mock_envoy_options_error") +def mock_envoy_options_fixture( + mock_envoy, +): + """Mock envoy with error in request.""" + mock_envoy_options = mock_envoy + mock_envoy_options.request.side_effect = AsyncMock(side_effect=EnvoyError("Test")) + return mock_envoy_options + + +async def test_entry_diagnostics_with_fixtures_with_error( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + config_entry_options: ConfigEntry, + setup_enphase_envoy_options_error, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry_options + ) == snapshot(exclude=limit_diagnostic_attrs) From be6dfc7a709fb04143b4a4413b0e39464ca8f913 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:07:22 +0200 Subject: [PATCH 0041/2411] Typing improvements (#120297) --- homeassistant/components/fritz/switch.py | 4 ++-- homeassistant/components/geniushub/sensor.py | 2 +- homeassistant/components/heos/media_player.py | 2 +- homeassistant/components/keenetic_ndms2/device_tracker.py | 2 +- homeassistant/components/knx/config_flow.py | 1 + homeassistant/components/lovelace/dashboard.py | 2 +- homeassistant/components/modbus/sensor.py | 6 +++--- homeassistant/components/zwave_me/light.py | 5 ++--- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 8af5b8ba529..ce89cfc736d 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -331,7 +331,7 @@ class FritzBoxBaseSwitch(FritzBoxBaseEntity, SwitchEntity): self._name = f"{self._friendly_name} {self._description}" self._unique_id = f"{self._avm_wrapper.unique_id}-{slugify(self._description)}" - self._attributes: dict[str, str] = {} + self._attributes: dict[str, str | None] = {} self._is_available = True @property @@ -355,7 +355,7 @@ class FritzBoxBaseSwitch(FritzBoxBaseEntity, SwitchEntity): return self._is_available @property - def extra_state_attributes(self) -> dict[str, str]: + def extra_state_attributes(self) -> dict[str, str | None]: """Return device attributes.""" return self._attributes diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 998bd6f1edb..f5cd8625e8b 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -85,7 +85,7 @@ class GeniusBattery(GeniusDevice, SensorEntity): return icon @property - def native_value(self) -> str: + def native_value(self) -> int: """Return the state of the sensor.""" level = self._device.data["state"][self._state_attr] return level if level != 255 else 0 diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 820bcb2fb2b..858ebd225b7 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -377,7 +377,7 @@ class HeosMediaPlayer(MediaPlayerEntity): return self._media_position_updated_at @property - def media_image_url(self) -> str: + def media_image_url(self) -> str | None: """Image url of current playing media.""" # May be an empty string, if so, return None image_url = self._player.now_playing_media.image_url diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index e15c96d8353..34c5cb502c6 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -119,7 +119,7 @@ class KeeneticTracker(ScannerEntity): return f"{self._device.mac}_{self._router.config_entry.entry_id}" @property - def ip_address(self) -> str: + def ip_address(self) -> str | None: """Return the primary ip address of the device.""" return self._device.ip if self.is_connected else None diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 22c4a647e80..c526a1e25f6 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -332,6 +332,7 @@ class KNXCommonFlow(ABC, ConfigEntryBaseFlow): self.initial_data.get(CONF_KNX_CONNECTION_TYPE) in CONF_KNX_TUNNELING_TYPE_LABELS ) + ip_address: str | None if ( # initial attempt on ConfigFlow or coming from automatic / routing (isinstance(self, ConfigFlow) or not _reconfiguring_existing_tunnel) and not user_input diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index db6db2fa7ef..411bbae9153 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -56,7 +56,7 @@ class LovelaceConfig(ABC): self.config = None @property - def url_path(self) -> str: + def url_path(self) -> str | None: """Return url path.""" return self.config[CONF_URL_PATH] if self.config else None diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 6c6e1ef1830..dbc464e98a9 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -74,7 +74,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): super().__init__(hass, hub, entry) if slave_count: self._count = self._count * (slave_count + 1) - self._coordinator: DataUpdateCoordinator[list[int] | None] | None = None + self._coordinator: DataUpdateCoordinator[list[float] | None] | None = None self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._attr_state_class = entry.get(CONF_STATE_CLASS) self._attr_device_class = entry.get(CONF_DEVICE_CLASS) @@ -142,7 +142,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): class SlaveSensor( - CoordinatorEntity[DataUpdateCoordinator[list[int] | None]], + CoordinatorEntity[DataUpdateCoordinator[list[float] | None]], RestoreSensor, SensorEntity, ): @@ -150,7 +150,7 @@ class SlaveSensor( def __init__( self, - coordinator: DataUpdateCoordinator[list[int] | None], + coordinator: DataUpdateCoordinator[list[float] | None], idx: int, entry: dict[str, Any], ) -> None: diff --git a/homeassistant/components/zwave_me/light.py b/homeassistant/components/zwave_me/light.py index b1065d45160..2289fe7b115 100644 --- a/homeassistant/components/zwave_me/light.py +++ b/homeassistant/components/zwave_me/light.py @@ -84,9 +84,8 @@ class ZWaveMeRGB(ZWaveMeEntity, LightEntity): self.device.id, f"exact?level={round(brightness / 2.55)}" ) return - cmd = "exact?red={}&green={}&blue={}".format( - *color if any(color) else 255, 255, 255 - ) + cmd = "exact?red={}&green={}&blue={}" + cmd = cmd.format(*color) if any(color) else cmd.format(*(255, 255, 255)) self.controller.zwave_api.send_command(self.device.id, cmd) @property From e32a27a8ff2864f43a9686dbd68e6813ee95cf31 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 Jun 2024 11:14:08 +0200 Subject: [PATCH 0042/2411] Remove hass_recorder test fixture (#120295) --- pylint/plugins/hass_enforce_type_hints.py | 1 - tests/common.py | 26 ----- tests/conftest.py | 116 ---------------------- 3 files changed, 143 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 6dd19d96d01..67eea59bc9a 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -125,7 +125,6 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "hass_owner_user": "MockUser", "hass_read_only_access_token": "str", "hass_read_only_user": "MockUser", - "hass_recorder": "Callable[..., HomeAssistant]", "hass_storage": "dict[str, Any]", "hass_supervisor_access_token": "str", "hass_supervisor_user": "MockUser", diff --git a/tests/common.py b/tests/common.py index 30c7cc2d971..f5531dbf40d 100644 --- a/tests/common.py +++ b/tests/common.py @@ -70,7 +70,6 @@ from homeassistant.helpers import ( intent, issue_registry as ir, label_registry as lr, - recorder as recorder_helper, restore_state as rs, storage, translation, @@ -83,7 +82,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.json import JSONEncoder, _orjson_default_encoder, json_dumps from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.setup import setup_component from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.json import ( @@ -1162,30 +1160,6 @@ def assert_setup_component(count, domain=None): ), f"setup_component failed, expected {count} got {res_len}: {res}" -def init_recorder_component(hass, add_config=None, db_url="sqlite://"): - """Initialize the recorder.""" - # Local import to avoid processing recorder and SQLite modules when running a - # testcase which does not use the recorder. - # pylint: disable-next=import-outside-toplevel - from homeassistant.components import recorder - - config = dict(add_config) if add_config else {} - if recorder.CONF_DB_URL not in config: - config[recorder.CONF_DB_URL] = db_url - if recorder.CONF_COMMIT_INTERVAL not in config: - config[recorder.CONF_COMMIT_INTERVAL] = 0 - - with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True): - 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( - "Test recorder successfully started, database location: %s", - config[recorder.CONF_DB_URL], - ) - - def mock_restore_cache(hass: HomeAssistant, states: Sequence[State]) -> None: """Mock the DATA_RESTORE_CACHE.""" key = rs.DATA_RESTORE_STATE diff --git a/tests/conftest.py b/tests/conftest.py index 6aa370ae539..1d4699647c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -106,8 +106,6 @@ from .common import ( # noqa: E402, isort:skip MockUser, async_fire_mqtt_message, async_test_home_assistant, - get_test_home_assistant, - init_recorder_component, mock_storage, patch_yaml_files, extract_stack_to_frame, @@ -1350,120 +1348,6 @@ def recorder_db_url( sqlalchemy_utils.drop_database(db_url) -@pytest.fixture -def hass_recorder( - recorder_db_url: str, - enable_nightly_purge: bool, - enable_statistics: bool, - enable_schema_validation: bool, - enable_migrate_context_ids: bool, - enable_migrate_event_type_ids: bool, - enable_migrate_entity_ids: bool, - hass_storage: dict[str, Any], -) -> Generator[Callable[..., HomeAssistant]]: - """Home Assistant fixture with in-memory recorder.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.components import recorder - - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.recorder import migration - - with get_test_home_assistant() as hass: - nightly = ( - recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None - ) - stats = ( - recorder.Recorder.async_periodic_statistics if enable_statistics else None - ) - compile_missing = ( - recorder.Recorder._schedule_compile_missing_statistics - if enable_statistics - else None - ) - schema_validate = ( - migration._find_schema_errors - if enable_schema_validation - else itertools.repeat(set()) - ) - migrate_states_context_ids = ( - recorder.Recorder._migrate_states_context_ids - if enable_migrate_context_ids - else None - ) - migrate_events_context_ids = ( - recorder.Recorder._migrate_events_context_ids - if enable_migrate_context_ids - else None - ) - migrate_event_type_ids = ( - recorder.Recorder._migrate_event_type_ids - if enable_migrate_event_type_ids - else None - ) - migrate_entity_ids = ( - recorder.Recorder._migrate_entity_ids if enable_migrate_entity_ids else None - ) - with ( - patch( - "homeassistant.components.recorder.Recorder.async_nightly_tasks", - side_effect=nightly, - autospec=True, - ), - patch( - "homeassistant.components.recorder.Recorder.async_periodic_statistics", - side_effect=stats, - autospec=True, - ), - patch( - "homeassistant.components.recorder.migration._find_schema_errors", - side_effect=schema_validate, - autospec=True, - ), - patch( - "homeassistant.components.recorder.Recorder._migrate_events_context_ids", - side_effect=migrate_events_context_ids, - autospec=True, - ), - patch( - "homeassistant.components.recorder.Recorder._migrate_states_context_ids", - side_effect=migrate_states_context_ids, - autospec=True, - ), - patch( - "homeassistant.components.recorder.Recorder._migrate_event_type_ids", - side_effect=migrate_event_type_ids, - autospec=True, - ), - patch( - "homeassistant.components.recorder.Recorder._migrate_entity_ids", - side_effect=migrate_entity_ids, - autospec=True, - ), - patch( - "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", - side_effect=compile_missing, - autospec=True, - ), - ): - - def setup_recorder( - *, config: dict[str, Any] | None = None, timezone: str | None = None - ) -> HomeAssistant: - """Set up with params.""" - if timezone is not None: - asyncio.run_coroutine_threadsafe( - hass.config.async_set_time_zone(timezone), hass.loop - ).result() - init_recorder_component(hass, config, recorder_db_url) - hass.start() - hass.block_till_done() - hass.data[recorder.DATA_INSTANCE].block_till_done() - return hass - - yield setup_recorder - hass.stop() - - async def _async_init_recorder_component( hass: HomeAssistant, add_config: dict[str, Any] | None = None, From 59dd63ea86a3bdb2173ffe8130dc8e7d03f4539a Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Mon, 24 Jun 2024 11:18:10 +0200 Subject: [PATCH 0043/2411] Remove deprecated attributes from Swiss public transport integration (#120256) --- .../swiss_public_transport/coordinator.py | 4 --- .../swiss_public_transport/sensor.py | 28 ------------------- 2 files changed, 32 deletions(-) diff --git a/homeassistant/components/swiss_public_transport/coordinator.py b/homeassistant/components/swiss_public_transport/coordinator.py index eb6ab9c6017..ae7e1b2366d 100644 --- a/homeassistant/components/swiss_public_transport/coordinator.py +++ b/homeassistant/components/swiss_public_transport/coordinator.py @@ -23,8 +23,6 @@ class DataConnection(TypedDict): """A connection data class.""" departure: datetime | None - next_departure: datetime | None - next_on_departure: datetime | None duration: int | None platform: str remaining_time: str @@ -88,8 +86,6 @@ class SwissPublicTransportDataUpdateCoordinator( return [ DataConnection( departure=self.nth_departure_time(i), - next_departure=self.nth_departure_time(i + 1), - next_on_departure=self.nth_departure_time(i + 2), train_number=connections[i]["number"], platform=connections[i]["platform"], transfers=connections[i]["transfers"], diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index 844797e5dd5..88a6dbecae4 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -15,7 +15,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.const import UnitOfTime -from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -36,7 +35,6 @@ class SwissPublicTransportSensorEntityDescription(SensorEntityDescription): value_fn: Callable[[DataConnection], StateType | datetime] index: int = 0 - has_legacy_attributes: bool = False SENSORS: tuple[SwissPublicTransportSensorEntityDescription, ...] = ( @@ -45,7 +43,6 @@ SENSORS: tuple[SwissPublicTransportSensorEntityDescription, ...] = ( key=f"departure{i or ''}", translation_key=f"departure{i}", device_class=SensorDeviceClass.TIMESTAMP, - has_legacy_attributes=i == 0, value_fn=lambda data_connection: data_connection["departure"], index=i, ) @@ -127,28 +124,3 @@ class SwissPublicTransportSensor( return self.entity_description.value_fn( self.coordinator.data[self.entity_description.index] ) - - async def async_added_to_hass(self) -> None: - """Prepare the extra attributes at start.""" - if self.entity_description.has_legacy_attributes: - self._async_update_attrs() - await super().async_added_to_hass() - - @callback - def _handle_coordinator_update(self) -> None: - """Handle the state update and prepare the extra state attributes.""" - if self.entity_description.has_legacy_attributes: - self._async_update_attrs() - return super()._handle_coordinator_update() - - @callback - def _async_update_attrs(self) -> None: - """Update the extra state attributes based on the coordinator data.""" - if self.entity_description.has_legacy_attributes: - self._attr_extra_state_attributes = { - key: value - for key, value in self.coordinator.data[ - self.entity_description.index - ].items() - if key not in {"departure"} - } From c04a6cc639dba4b9fd4164c67101a165c53729eb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 24 Jun 2024 05:37:12 -0400 Subject: [PATCH 0044/2411] Bump jaraco.abode to 5.1.2 (#117363) Co-authored-by: Joost Lekkerkerker --- .../components/abode/binary_sensor.py | 17 ++++---------- homeassistant/components/abode/camera.py | 3 +-- homeassistant/components/abode/cover.py | 3 +-- homeassistant/components/abode/light.py | 3 +-- homeassistant/components/abode/lock.py | 3 +-- homeassistant/components/abode/manifest.json | 2 +- homeassistant/components/abode/sensor.py | 23 ++++++------------- homeassistant/components/abode/switch.py | 5 ++-- requirements_all.txt | 5 +--- requirements_test_all.txt | 5 +--- .../abode/test_alarm_control_panel.py | 8 +++---- 11 files changed, 24 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 1bccbf61701..0f1372dc8be 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -5,13 +5,6 @@ from __future__ import annotations from typing import cast from jaraco.abode.devices.sensor import BinarySensor -from jaraco.abode.helpers.constants import ( - TYPE_CONNECTIVITY, - TYPE_MOISTURE, - TYPE_MOTION, - TYPE_OCCUPANCY, - TYPE_OPENING, -) from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -34,11 +27,11 @@ async def async_setup_entry( data: AbodeSystem = hass.data[DOMAIN] device_types = [ - TYPE_CONNECTIVITY, - TYPE_MOISTURE, - TYPE_MOTION, - TYPE_OCCUPANCY, - TYPE_OPENING, + "connectivity", + "moisture", + "motion", + "occupancy", + "door", ] async_add_entities( diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 57fcbf1fca4..58107f16462 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -8,7 +8,6 @@ from typing import Any, cast from jaraco.abode.devices.base import Device from jaraco.abode.devices.camera import Camera as AbodeCam from jaraco.abode.helpers import timeline -from jaraco.abode.helpers.constants import TYPE_CAMERA import requests from requests.models import Response @@ -34,7 +33,7 @@ async def async_setup_entry( async_add_entities( AbodeCamera(data, device, timeline.CAPTURE_IMAGE) - for device in data.abode.get_devices(generic_type=TYPE_CAMERA) + for device in data.abode.get_devices(generic_type="camera") ) diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index 96270cfd966..b5b1e878b96 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -3,7 +3,6 @@ from typing import Any from jaraco.abode.devices.cover import Cover -from jaraco.abode.helpers.constants import TYPE_COVER from homeassistant.components.cover import CoverEntity from homeassistant.config_entries import ConfigEntry @@ -23,7 +22,7 @@ async def async_setup_entry( async_add_entities( AbodeCover(data, device) - for device in data.abode.get_devices(generic_type=TYPE_COVER) + for device in data.abode.get_devices(generic_type="cover") ) diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 83f00e417ad..d69aad80875 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -6,7 +6,6 @@ from math import ceil from typing import Any from jaraco.abode.devices.light import Light -from jaraco.abode.helpers.constants import TYPE_LIGHT from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -36,7 +35,7 @@ async def async_setup_entry( async_add_entities( AbodeLight(data, device) - for device in data.abode.get_devices(generic_type=TYPE_LIGHT) + for device in data.abode.get_devices(generic_type="light") ) diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index 3a65fa4d6dc..ceff263e6b5 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -3,7 +3,6 @@ from typing import Any from jaraco.abode.devices.lock import Lock -from jaraco.abode.helpers.constants import TYPE_LOCK from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry @@ -23,7 +22,7 @@ async def async_setup_entry( async_add_entities( AbodeLock(data, device) - for device in data.abode.get_devices(generic_type=TYPE_LOCK) + for device in data.abode.get_devices(generic_type="lock") ) diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index c7d51c7ea1f..de1000319f1 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -9,5 +9,5 @@ }, "iot_class": "cloud_push", "loggers": ["jaraco.abode", "lomond"], - "requirements": ["jaraco.abode==3.3.0", "jaraco.functools==3.9.0"] + "requirements": ["jaraco.abode==5.1.2"] } diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index b57b3e77abc..d6a5389029b 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -7,15 +7,6 @@ from dataclasses import dataclass from typing import cast from jaraco.abode.devices.sensor import Sensor -from jaraco.abode.helpers.constants import ( - HUMI_STATUS_KEY, - LUX_STATUS_KEY, - STATUSES_KEY, - TEMP_STATUS_KEY, - TYPE_SENSOR, - UNIT_CELSIUS, - UNIT_FAHRENHEIT, -) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -32,8 +23,8 @@ from .const import DOMAIN from .entity import AbodeDevice ABODE_TEMPERATURE_UNIT_HA_UNIT = { - UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT, - UNIT_CELSIUS: UnitOfTemperature.CELSIUS, + "°F": UnitOfTemperature.FAHRENHEIT, + "°C": UnitOfTemperature.CELSIUS, } @@ -47,7 +38,7 @@ class AbodeSensorDescription(SensorEntityDescription): SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = ( AbodeSensorDescription( - key=TEMP_STATUS_KEY, + key="temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[ device.temp_unit @@ -55,13 +46,13 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = ( value_fn=lambda device: cast(float, device.temp), ), AbodeSensorDescription( - key=HUMI_STATUS_KEY, + key="humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement_fn=lambda _: PERCENTAGE, value_fn=lambda device: cast(float, device.humidity), ), AbodeSensorDescription( - key=LUX_STATUS_KEY, + key="lux", device_class=SensorDeviceClass.ILLUMINANCE, native_unit_of_measurement_fn=lambda _: LIGHT_LUX, value_fn=lambda device: cast(float, device.lux), @@ -78,8 +69,8 @@ async def async_setup_entry( async_add_entities( AbodeSensor(data, device, description) for description in SENSOR_TYPES - for device in data.abode.get_devices(generic_type=TYPE_SENSOR) - if description.key in device.get_value(STATUSES_KEY) + for device in data.abode.get_devices(generic_type="sensor") + if description.key in device.get_value("statuses") ) diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 64eb3529aab..7dad750c8d5 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -5,7 +5,6 @@ from __future__ import annotations from typing import Any, cast from jaraco.abode.devices.switch import Switch -from jaraco.abode.helpers.constants import TYPE_SWITCH, TYPE_VALVE from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -17,7 +16,7 @@ from . import AbodeSystem from .const import DOMAIN from .entity import AbodeAutomation, AbodeDevice -DEVICE_TYPES = [TYPE_SWITCH, TYPE_VALVE] +DEVICE_TYPES = ["switch", "valve"] async def async_setup_entry( @@ -89,4 +88,4 @@ class AbodeAutomationSwitch(AbodeAutomation, SwitchEntity): @property def is_on(self) -> bool: """Return True if the automation is enabled.""" - return bool(self._automation.is_enabled) + return bool(self._automation.enabled) diff --git a/requirements_all.txt b/requirements_all.txt index 52b887723fe..45b56edecbf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1182,10 +1182,7 @@ isal==1.6.1 ismartgate==5.0.1 # homeassistant.components.abode -jaraco.abode==3.3.0 - -# homeassistant.components.abode -jaraco.functools==3.9.0 +jaraco.abode==5.1.2 # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88caa904094..8feebec2ef1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -969,10 +969,7 @@ isal==1.6.1 ismartgate==5.0.1 # homeassistant.components.abode -jaraco.abode==3.3.0 - -# homeassistant.components.abode -jaraco.functools==3.9.0 +jaraco.abode==5.1.2 # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 diff --git a/tests/components/abode/test_alarm_control_panel.py b/tests/components/abode/test_alarm_control_panel.py index 428e2791ee2..51e0ee46838 100644 --- a/tests/components/abode/test_alarm_control_panel.py +++ b/tests/components/abode/test_alarm_control_panel.py @@ -2,8 +2,6 @@ from unittest.mock import PropertyMock, patch -from jaraco.abode.helpers import constants as CONST - from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN from homeassistant.const import ( @@ -70,7 +68,7 @@ async def test_set_alarm_away(hass: HomeAssistant) -> None: "jaraco.abode.devices.alarm.Alarm.mode", new_callable=PropertyMock, ) as mock_mode: - mock_mode.return_value = CONST.MODE_AWAY + mock_mode.return_value = "away" update_callback = mock_callback.call_args[0][1] await hass.async_add_executor_job(update_callback, "area_1") @@ -100,7 +98,7 @@ async def test_set_alarm_home(hass: HomeAssistant) -> None: with patch( "jaraco.abode.devices.alarm.Alarm.mode", new_callable=PropertyMock ) as mock_mode: - mock_mode.return_value = CONST.MODE_HOME + mock_mode.return_value = "home" update_callback = mock_callback.call_args[0][1] await hass.async_add_executor_job(update_callback, "area_1") @@ -129,7 +127,7 @@ async def test_set_alarm_standby(hass: HomeAssistant) -> None: with patch( "jaraco.abode.devices.alarm.Alarm.mode", new_callable=PropertyMock ) as mock_mode: - mock_mode.return_value = CONST.MODE_STANDBY + mock_mode.return_value = "standby" update_callback = mock_callback.call_args[0][1] await hass.async_add_executor_job(update_callback, "area_1") From f3a1ca6d5403641fa35844ad9d0d36a976ec7249 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jun 2024 11:41:33 +0200 Subject: [PATCH 0045/2411] Add coordinator to Knocki (#120251) --- homeassistant/components/knocki/__init__.py | 30 +++++++--------- .../components/knocki/coordinator.py | 34 +++++++++++++++++++ homeassistant/components/knocki/event.py | 22 ++++++++++-- tests/components/knocki/test_event.py | 25 +++++++++++++- 4 files changed, 89 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/knocki/coordinator.py diff --git a/homeassistant/components/knocki/__init__.py b/homeassistant/components/knocki/__init__.py index ef024d6f4d6..ddf389649f2 100644 --- a/homeassistant/components/knocki/__init__.py +++ b/homeassistant/components/knocki/__init__.py @@ -2,27 +2,18 @@ from __future__ import annotations -from dataclasses import dataclass - -from knocki import KnockiClient, KnockiConnectionError, Trigger +from knocki import EventType, KnockiClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from .coordinator import KnockiCoordinator + PLATFORMS: list[Platform] = [Platform.EVENT] -type KnockiConfigEntry = ConfigEntry[KnockiData] - - -@dataclass -class KnockiData: - """Knocki data.""" - - client: KnockiClient - triggers: list[Trigger] +type KnockiConfigEntry = ConfigEntry[KnockiCoordinator] async def async_setup_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bool: @@ -31,12 +22,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bo session=async_get_clientsession(hass), token=entry.data[CONF_TOKEN] ) - try: - triggers = await client.get_triggers() - except KnockiConnectionError as exc: - raise ConfigEntryNotReady from exc + coordinator = KnockiCoordinator(hass, client) - entry.runtime_data = KnockiData(client=client, triggers=triggers) + await coordinator.async_config_entry_first_refresh() + + entry.async_on_unload( + client.register_listener(EventType.CREATED, coordinator.add_trigger) + ) + + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/knocki/coordinator.py b/homeassistant/components/knocki/coordinator.py new file mode 100644 index 00000000000..020b3921a1e --- /dev/null +++ b/homeassistant/components/knocki/coordinator.py @@ -0,0 +1,34 @@ +"""Update coordinator for Knocki integration.""" + +from knocki import Event, KnockiClient, KnockiConnectionError, Trigger + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER + + +class KnockiCoordinator(DataUpdateCoordinator[dict[int, Trigger]]): + """The Knocki coordinator.""" + + def __init__(self, hass: HomeAssistant, client: KnockiClient) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, + logger=LOGGER, + name=DOMAIN, + ) + self.client = client + + async def _async_update_data(self) -> dict[int, Trigger]: + try: + triggers = await self.client.get_triggers() + except KnockiConnectionError as exc: + raise UpdateFailed from exc + return {trigger.details.trigger_id: trigger for trigger in triggers} + + def add_trigger(self, event: Event) -> None: + """Add a trigger to the coordinator.""" + self.async_set_updated_data( + {**self.data, event.payload.details.trigger_id: event.payload} + ) diff --git a/homeassistant/components/knocki/event.py b/homeassistant/components/knocki/event.py index 8cd5de21958..adaf344e468 100644 --- a/homeassistant/components/knocki/event.py +++ b/homeassistant/components/knocki/event.py @@ -3,7 +3,7 @@ from knocki import Event, EventType, KnockiClient, Trigger from homeassistant.components.event import EventEntity -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -17,10 +17,26 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Knocki from a config entry.""" - entry_data = entry.runtime_data + coordinator = entry.runtime_data + + added_triggers = set(coordinator.data) + + @callback + def _async_add_entities() -> None: + current_triggers = set(coordinator.data) + new_triggers = current_triggers - added_triggers + added_triggers.update(new_triggers) + if new_triggers: + async_add_entities( + KnockiTrigger(coordinator.data[trigger], coordinator.client) + for trigger in new_triggers + ) + + coordinator.async_add_listener(_async_add_entities) async_add_entities( - KnockiTrigger(trigger, entry_data.client) for trigger in entry_data.triggers + KnockiTrigger(trigger, coordinator.client) + for trigger in coordinator.data.values() ) diff --git a/tests/components/knocki/test_event.py b/tests/components/knocki/test_event.py index a53e2811854..4740ddc9167 100644 --- a/tests/components/knocki/test_event.py +++ b/tests/components/knocki/test_event.py @@ -7,13 +7,14 @@ from knocki import Event, EventType, Trigger, TriggerDetails import pytest from syrupy import SnapshotAssertion +from homeassistant.components.knocki.const import DOMAIN from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, load_json_array_fixture, snapshot_platform async def test_entities( @@ -73,3 +74,25 @@ async def test_subscription( await hass.async_block_till_done() assert mock_knocki_client.register_listener.return_value.called + + +async def test_adding_runtime_entities( + hass: HomeAssistant, + mock_knocki_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we can create devices on runtime.""" + mock_knocki_client.get_triggers.return_value = [] + + await setup_integration(hass, mock_config_entry) + + assert not hass.states.get("event.knc1_w_00000214_aaaa") + + add_trigger_function: Callable[[Event], None] = ( + mock_knocki_client.register_listener.call_args[0][1] + ) + trigger = Trigger.from_dict(load_json_array_fixture("triggers.json", DOMAIN)[0]) + + add_trigger_function(Event(EventType.CREATED, trigger)) + + assert hass.states.get("event.knc1_w_00000214_aaaa") is not None From 674dfa6e9cf425c8f323057c82b4ab3ef246457b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jun 2024 11:55:48 +0200 Subject: [PATCH 0046/2411] Add button platform to AirGradient (#119917) --- .../components/airgradient/__init__.py | 7 +- .../components/airgradient/button.py | 104 +++++++++++++ .../components/airgradient/icons.json | 8 + .../components/airgradient/number.py | 7 +- .../components/airgradient/strings.json | 8 + .../airgradient/snapshots/test_button.ambr | 139 ++++++++++++++++++ tests/components/airgradient/test_button.py | 99 +++++++++++++ 7 files changed, 366 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/airgradient/button.py create mode 100644 tests/components/airgradient/snapshots/test_button.ambr create mode 100644 tests/components/airgradient/test_button.py diff --git a/homeassistant/components/airgradient/__init__.py b/homeassistant/components/airgradient/__init__.py index 76e11c05527..b1b5a28ef67 100644 --- a/homeassistant/components/airgradient/__init__.py +++ b/homeassistant/components/airgradient/__init__.py @@ -15,7 +15,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator -PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SELECT, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.BUTTON, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, +] @dataclass diff --git a/homeassistant/components/airgradient/button.py b/homeassistant/components/airgradient/button.py new file mode 100644 index 00000000000..b59188ebdd4 --- /dev/null +++ b/homeassistant/components/airgradient/button.py @@ -0,0 +1,104 @@ +"""Support for AirGradient buttons.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from airgradient import AirGradientClient, ConfigurationControl + +from homeassistant.components.button import ( + DOMAIN as BUTTON_DOMAIN, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN, AirGradientConfigEntry +from .coordinator import AirGradientConfigCoordinator +from .entity import AirGradientEntity + + +@dataclass(frozen=True, kw_only=True) +class AirGradientButtonEntityDescription(ButtonEntityDescription): + """Describes AirGradient button entity.""" + + press_fn: Callable[[AirGradientClient], Awaitable[None]] + + +CO2_CALIBRATION = AirGradientButtonEntityDescription( + key="co2_calibration", + translation_key="co2_calibration", + entity_category=EntityCategory.CONFIG, + press_fn=lambda client: client.request_co2_calibration(), +) +LED_BAR_TEST = AirGradientButtonEntityDescription( + key="led_bar_test", + translation_key="led_bar_test", + entity_category=EntityCategory.CONFIG, + press_fn=lambda client: client.request_led_bar_test(), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AirGradientConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up AirGradient button entities based on a config entry.""" + model = entry.runtime_data.measurement.data.model + coordinator = entry.runtime_data.config + + added_entities = False + + @callback + def _check_entities() -> None: + nonlocal added_entities + + if ( + coordinator.data.configuration_control is ConfigurationControl.LOCAL + and not added_entities + ): + entities = [AirGradientButton(coordinator, CO2_CALIBRATION)] + if "L" in model: + entities.append(AirGradientButton(coordinator, LED_BAR_TEST)) + + async_add_entities(entities) + added_entities = True + elif ( + coordinator.data.configuration_control is not ConfigurationControl.LOCAL + and added_entities + ): + entity_registry = er.async_get(hass) + for entity_description in (CO2_CALIBRATION, LED_BAR_TEST): + unique_id = f"{coordinator.serial_number}-{entity_description.key}" + if entity_id := entity_registry.async_get_entity_id( + BUTTON_DOMAIN, DOMAIN, unique_id + ): + entity_registry.async_remove(entity_id) + added_entities = False + + coordinator.async_add_listener(_check_entities) + _check_entities() + + +class AirGradientButton(AirGradientEntity, ButtonEntity): + """Defines an AirGradient button.""" + + entity_description: AirGradientButtonEntityDescription + coordinator: AirGradientConfigCoordinator + + def __init__( + self, + coordinator: AirGradientConfigCoordinator, + description: AirGradientButtonEntityDescription, + ) -> None: + """Initialize airgradient button.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" + + async def async_press(self) -> None: + """Press the button.""" + await self.entity_description.press_fn(self.coordinator.client) diff --git a/homeassistant/components/airgradient/icons.json b/homeassistant/components/airgradient/icons.json index cf0c80c873e..45d1e12d46e 100644 --- a/homeassistant/components/airgradient/icons.json +++ b/homeassistant/components/airgradient/icons.json @@ -1,5 +1,13 @@ { "entity": { + "button": { + "co2_calibration": { + "default": "mdi:molecule-co2" + }, + "led_bar_test": { + "default": "mdi:lightbulb-on-outline" + } + }, "sensor": { "total_volatile_organic_component_index": { "default": "mdi:molecule" diff --git a/homeassistant/components/airgradient/number.py b/homeassistant/components/airgradient/number.py index e065b76ed51..139357f3753 100644 --- a/homeassistant/components/airgradient/number.py +++ b/homeassistant/components/airgradient/number.py @@ -88,11 +88,8 @@ async def async_setup_entry( and added_entities ): entity_registry = er.async_get(hass) - unique_ids = [ - f"{coordinator.serial_number}-{entity_description.key}" - for entity_description in (DISPLAY_BRIGHTNESS, LED_BAR_BRIGHTNESS) - ] - for unique_id in unique_ids: + for entity_description in (DISPLAY_BRIGHTNESS, LED_BAR_BRIGHTNESS): + unique_id = f"{coordinator.serial_number}-{entity_description.key}" if entity_id := entity_registry.async_get_entity_id( NUMBER_DOMAIN, DOMAIN, unique_id ): diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 6c079419839..0b5c245f04c 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -24,6 +24,14 @@ } }, "entity": { + "button": { + "co2_calibration": { + "name": "Calibrate CO2 sensor" + }, + "led_bar_test": { + "name": "Test LED bar" + } + }, "number": { "led_bar_brightness": { "name": "LED bar brightness" diff --git a/tests/components/airgradient/snapshots/test_button.ambr b/tests/components/airgradient/snapshots/test_button.ambr new file mode 100644 index 00000000000..fa3f8994c3c --- /dev/null +++ b/tests/components/airgradient/snapshots/test_button.ambr @@ -0,0 +1,139 @@ +# serializer version: 1 +# name: test_all_entities[indoor][button.airgradient_calibrate_co2_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.airgradient_calibrate_co2_sensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Calibrate CO2 sensor', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co2_calibration', + 'unique_id': '84fce612f5b8-co2_calibration', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][button.airgradient_calibrate_co2_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Calibrate CO2 sensor', + }), + 'context': , + 'entity_id': 'button.airgradient_calibrate_co2_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[indoor][button.airgradient_test_led_bar-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.airgradient_test_led_bar', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Test LED bar', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'led_bar_test', + 'unique_id': '84fce612f5b8-led_bar_test', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][button.airgradient_test_led_bar-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Test LED bar', + }), + 'context': , + 'entity_id': 'button.airgradient_test_led_bar', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[outdoor][button.airgradient_calibrate_co2_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.airgradient_calibrate_co2_sensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Calibrate CO2 sensor', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co2_calibration', + 'unique_id': '84fce612f5b8-co2_calibration', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][button.airgradient_calibrate_co2_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Calibrate CO2 sensor', + }), + 'context': , + 'entity_id': 'button.airgradient_calibrate_co2_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/airgradient/test_button.py b/tests/components/airgradient/test_button.py new file mode 100644 index 00000000000..7901c3a067b --- /dev/null +++ b/tests/components/airgradient/test_button.py @@ -0,0 +1,99 @@ +"""Tests for the AirGradient button platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from airgradient import Config +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion + +from homeassistant.components.airgradient import DOMAIN +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_fixture, + snapshot_platform, +) + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + airgradient_devices: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.BUTTON]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_pressing_button( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test pressing button.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.airgradient_calibrate_co2_sensor", + }, + blocking=True, + ) + mock_airgradient_client.request_co2_calibration.assert_called_once() + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.airgradient_test_led_bar", + }, + blocking=True, + ) + mock_airgradient_client.request_led_bar_test.assert_called_once() + + +async def test_cloud_creates_no_button( + hass: HomeAssistant, + mock_cloud_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test cloud configuration control.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.BUTTON]): + await setup_integration(hass, mock_config_entry) + + assert len(hass.states.async_all()) == 0 + + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_local.json", DOMAIN) + ) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_cloud.json", DOMAIN) + ) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 From 237f20de6ce462f6013228b70808b0de0c68937b Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Mon, 24 Jun 2024 12:58:37 +0200 Subject: [PATCH 0047/2411] Add DataUpdateCoordinator to pyLoad integration (#120237) * Add DataUpdateCoordinator * Update tests * changes * changes * test coverage * some changes * Update homeassistant/components/pyload/sensor.py * use dataclass * fix ConfigEntry * fix configtype * fix some issues * remove logger * remove unnecessary else * revert fixture changes --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/pyload/__init__.py | 8 +- .../components/pyload/coordinator.py | 78 ++++++++ homeassistant/components/pyload/sensor.py | 87 ++------- tests/components/pyload/conftest.py | 4 +- .../pyload/snapshots/test_sensor.ambr | 177 ------------------ tests/components/pyload/test_sensor.py | 2 +- 6 files changed, 100 insertions(+), 256 deletions(-) create mode 100644 homeassistant/components/pyload/coordinator.py diff --git a/homeassistant/components/pyload/__init__.py b/homeassistant/components/pyload/__init__.py index a2e105e6454..d7c7e9454ea 100644 --- a/homeassistant/components/pyload/__init__.py +++ b/homeassistant/components/pyload/__init__.py @@ -20,9 +20,11 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_create_clientsession +from .coordinator import PyLoadCoordinator + PLATFORMS: list[Platform] = [Platform.SENSOR] -type PyLoadConfigEntry = ConfigEntry[PyLoadAPI] +type PyLoadConfigEntry = ConfigEntry[PyLoadCoordinator] async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bool: @@ -57,9 +59,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bo raise ConfigEntryError( f"Authentication failed for {entry.data[CONF_USERNAME]}, check your login credentials" ) from e + coordinator = PyLoadCoordinator(hass, pyloadapi) - entry.runtime_data = pyloadapi + await coordinator.async_config_entry_first_refresh() + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/pyload/coordinator.py b/homeassistant/components/pyload/coordinator.py new file mode 100644 index 00000000000..008375c3a34 --- /dev/null +++ b/homeassistant/components/pyload/coordinator.py @@ -0,0 +1,78 @@ +"""Update coordinator for pyLoad Integration.""" + +from dataclasses import dataclass +from datetime import timedelta +import logging + +from pyloadapi import CannotConnect, InvalidAuth, ParserError, PyLoadAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=20) + + +@dataclass(kw_only=True) +class pyLoadData: + """Data from pyLoad.""" + + pause: bool + active: int + queue: int + total: int + speed: float + download: bool + reconnect: bool + captcha: bool + free_space: int + + +class PyLoadCoordinator(DataUpdateCoordinator[pyLoadData]): + """pyLoad coordinator.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, pyload: PyLoadAPI) -> None: + """Initialize pyLoad coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + self.pyload = pyload + self.version: str | None = None + + async def _async_update_data(self) -> pyLoadData: + """Fetch data from API endpoint.""" + try: + if not self.version: + self.version = await self.pyload.version() + return pyLoadData( + **await self.pyload.get_status(), + free_space=await self.pyload.free_space(), + ) + + except InvalidAuth as e: + try: + await self.pyload.login() + except InvalidAuth as exc: + raise ConfigEntryError( + f"Authentication failed for {self.pyload.username}, check your login credentials", + ) from exc + + raise UpdateFailed( + "Unable to retrieve data due to cookie expiration but re-authentication was successful." + ) from e + except CannotConnect as e: + raise UpdateFailed( + "Unable to connect and retrieve data from pyLoad API" + ) from e + except ParserError as e: + raise UpdateFailed("Unable to parse data from pyLoad API") from e diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index aa86dde9260..7caef84d2dc 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -2,18 +2,8 @@ from __future__ import annotations -from datetime import timedelta from enum import StrEnum -import logging -from time import monotonic -from pyloadapi import ( - CannotConnect, - InvalidAuth, - ParserError, - PyLoadAPI, - StatusServerResponse, -) import voluptuous as vol from homeassistant.components.sensor import ( @@ -40,13 +30,11 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PyLoadConfigEntry from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN, ISSUE_PLACEHOLDER - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=15) +from .coordinator import PyLoadCoordinator class PyLoadSensorEntity(StrEnum): @@ -92,7 +80,6 @@ async def async_setup_platform( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) - _LOGGER.debug(result) if ( result.get("type") == FlowResultType.CREATE_ENTRY or result.get("reason") == "already_configured" @@ -132,91 +119,45 @@ async def async_setup_entry( ) -> None: """Set up the pyLoad sensors.""" - pyloadapi = entry.runtime_data + coordinator = entry.runtime_data async_add_entities( ( PyLoadSensor( - api=pyloadapi, + coordinator=coordinator, entity_description=description, - client_name=entry.title, - entry_id=entry.entry_id, ) for description in SENSOR_DESCRIPTIONS ), - True, ) -class PyLoadSensor(SensorEntity): +class PyLoadSensor(CoordinatorEntity[PyLoadCoordinator], SensorEntity): """Representation of a pyLoad sensor.""" _attr_has_entity_name = True def __init__( self, - api: PyLoadAPI, + coordinator: PyLoadCoordinator, entity_description: SensorEntityDescription, - client_name: str, - entry_id: str, ) -> None: """Initialize a new pyLoad sensor.""" - self.type = entity_description.key - self.api = api - self._attr_unique_id = f"{entry_id}_{entity_description.key}" + super().__init__(coordinator) + self._attr_unique_id = ( + f"{coordinator.config_entry.entry_id}_{entity_description.key}" + ) self.entity_description = entity_description - self._attr_available = False - self.data: StatusServerResponse self.device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, manufacturer="PyLoad Team", model="pyLoad", - configuration_url=api.api_url, - identifiers={(DOMAIN, entry_id)}, + configuration_url=coordinator.pyload.api_url, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + sw_version=coordinator.version, ) - async def async_update(self) -> None: - """Update state of sensor.""" - start = monotonic() - try: - status = await self.api.get_status() - except InvalidAuth: - _LOGGER.info("Authentication failed, trying to reauthenticate") - try: - await self.api.login() - except InvalidAuth: - _LOGGER.error( - "Authentication failed for %s, check your login credentials", - self.api.username, - ) - return - else: - _LOGGER.info( - "Unable to retrieve data due to cookie expiration " - "but re-authentication was successful" - ) - return - finally: - self._attr_available = False - - except CannotConnect: - _LOGGER.debug("Unable to connect and retrieve data from pyLoad API") - self._attr_available = False - return - except ParserError: - _LOGGER.error("Unable to parse data from pyLoad API") - self._attr_available = False - return - else: - self.data = status - _LOGGER.debug( - "Finished fetching pyload data in %.3f seconds", - monotonic() - start, - ) - - self._attr_available = True - @property def native_value(self) -> StateType: """Return the state of the sensor.""" - return self.data.get(self.entity_description.key) + return getattr(self.coordinator.data, self.entity_description.key) diff --git a/tests/components/pyload/conftest.py b/tests/components/pyload/conftest.py index 0dafb9af4df..3c6f9fdb49a 100644 --- a/tests/components/pyload/conftest.py +++ b/tests/components/pyload/conftest.py @@ -66,12 +66,10 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: "homeassistant.components.pyload.PyLoadAPI", autospec=True ) as mock_client, patch("homeassistant.components.pyload.config_flow.PyLoadAPI", new=mock_client), - patch("homeassistant.components.pyload.sensor.PyLoadAPI", new=mock_client), ): client = mock_client.return_value client.username = "username" client.api_url = "https://pyload.local:8000/" - client.login.return_value = LoginResponse( { "_permanent": True, @@ -97,7 +95,7 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]: "captcha": False, } ) - + client.version.return_value = "0.5.0" client.free_space.return_value = 99999999999 yield client diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index f1e42ea049c..a6049577f47 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -161,183 +161,6 @@ 'state': 'unavailable', }) # --- -# name: test_sensors_unavailable[CannotConnect][sensor.pyload_speed-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_speed', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Speed', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'XXXXXXXXXXXXXX_speed', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors_unavailable[CannotConnect][sensor.pyload_speed-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'data_rate', - 'friendly_name': 'pyLoad Speed', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.pyload_speed', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_sensors_unavailable[InvalidAuth][sensor.pyload_speed-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_speed', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Speed', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'XXXXXXXXXXXXXX_speed', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors_unavailable[InvalidAuth][sensor.pyload_speed-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'data_rate', - 'friendly_name': 'pyLoad Speed', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.pyload_speed', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_sensors_unavailable[ParserError][sensor.pyload_speed-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_speed', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Speed', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'XXXXXXXXXXXXXX_speed', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors_unavailable[ParserError][sensor.pyload_speed-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'data_rate', - 'friendly_name': 'pyLoad Speed', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.pyload_speed', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_setup - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'data_rate', - 'friendly_name': 'pyload Speed', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.pyload_speed', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '5.405963', - }) -# --- # name: test_setup[sensor.pyload_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/pyload/test_sensor.py b/tests/components/pyload/test_sensor.py index d0e912f82f2..49795284fc6 100644 --- a/tests/components/pyload/test_sensor.py +++ b/tests/components/pyload/test_sensor.py @@ -8,7 +8,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.pyload.const import DOMAIN -from homeassistant.components.pyload.sensor import SCAN_INTERVAL +from homeassistant.components.pyload.coordinator import SCAN_INTERVAL from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant From e3806d12f442bb2373f879de3a52bebacbca81d9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:34:23 +0200 Subject: [PATCH 0048/2411] Improve type hints in simplisafe tests (#120303) --- tests/components/simplisafe/conftest.py | 53 +++++++++++++++---------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index cc387ee765b..aaf853863e5 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -1,18 +1,20 @@ """Define test fixtures for SimpliSafe.""" -import json from unittest.mock import AsyncMock, Mock, patch import pytest from simplipy.system.v3 import SystemV3 +from typing_extensions import AsyncGenerator from homeassistant.components.simplisafe.const import DOMAIN from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util.json import JsonObjectType from .common import REFRESH_TOKEN, USER_ID, USERNAME -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, load_json_object_fixture CODE = "12345" PASSWORD = "password" @@ -20,7 +22,9 @@ SYSTEM_ID = 12345 @pytest.fixture(name="api") -def api_fixture(data_subscription, system_v3, websocket): +def api_fixture( + data_subscription: JsonObjectType, system_v3: SystemV3, websocket: Mock +) -> Mock: """Define a simplisafe-python API object.""" return Mock( async_get_systems=AsyncMock(return_value={SYSTEM_ID: system_v3}), @@ -32,7 +36,9 @@ def api_fixture(data_subscription, system_v3, websocket): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, unique_id): +def config_entry_fixture( + hass: HomeAssistant, config: dict[str, str], unique_id: str +) -> MockConfigEntry: """Define a config entry.""" entry = MockConfigEntry( domain=DOMAIN, unique_id=unique_id, data=config, options={CONF_CODE: "1234"} @@ -42,7 +48,7 @@ def config_entry_fixture(hass, config, unique_id): @pytest.fixture(name="config") -def config_fixture(): +def config_fixture() -> dict[str, str]: """Define config entry data config.""" return { CONF_TOKEN: REFRESH_TOKEN, @@ -51,7 +57,7 @@ def config_fixture(): @pytest.fixture(name="credentials_config") -def credentials_config_fixture(): +def credentials_config_fixture() -> dict[str, str]: """Define a username/password config.""" return { CONF_USERNAME: USERNAME, @@ -60,32 +66,32 @@ def credentials_config_fixture(): @pytest.fixture(name="data_latest_event", scope="package") -def data_latest_event_fixture(): +def data_latest_event_fixture() -> JsonObjectType: """Define latest event data.""" - return json.loads(load_fixture("latest_event_data.json", "simplisafe")) + return load_json_object_fixture("latest_event_data.json", "simplisafe") @pytest.fixture(name="data_sensor", scope="package") -def data_sensor_fixture(): +def data_sensor_fixture() -> JsonObjectType: """Define sensor data.""" - return json.loads(load_fixture("sensor_data.json", "simplisafe")) + return load_json_object_fixture("sensor_data.json", "simplisafe") @pytest.fixture(name="data_settings", scope="package") -def data_settings_fixture(): +def data_settings_fixture() -> JsonObjectType: """Define settings data.""" - return json.loads(load_fixture("settings_data.json", "simplisafe")) + return load_json_object_fixture("settings_data.json", "simplisafe") @pytest.fixture(name="data_subscription", scope="package") -def data_subscription_fixture(): +def data_subscription_fixture() -> JsonObjectType: """Define subscription data.""" - data = json.loads(load_fixture("subscription_data.json", "simplisafe")) + data = load_json_object_fixture("subscription_data.json", "simplisafe") return {SYSTEM_ID: data} @pytest.fixture(name="reauth_config") -def reauth_config_fixture(): +def reauth_config_fixture() -> dict[str, str]: """Define a reauth config.""" return { CONF_PASSWORD: PASSWORD, @@ -93,7 +99,9 @@ def reauth_config_fixture(): @pytest.fixture(name="setup_simplisafe") -async def setup_simplisafe_fixture(hass, api, config): +async def setup_simplisafe_fixture( + hass: HomeAssistant, api: Mock, config: dict[str, str] +) -> AsyncGenerator[None]: """Define a fixture to set up SimpliSafe.""" with ( patch( @@ -122,7 +130,7 @@ async def setup_simplisafe_fixture(hass, api, config): @pytest.fixture(name="sms_config") -def sms_config_fixture(): +def sms_config_fixture() -> dict[str, str]: """Define a SMS-based two-factor authentication config.""" return { CONF_CODE: CODE, @@ -130,7 +138,12 @@ def sms_config_fixture(): @pytest.fixture(name="system_v3") -def system_v3_fixture(data_latest_event, data_sensor, data_settings, data_subscription): +def system_v3_fixture( + data_latest_event: JsonObjectType, + data_sensor: JsonObjectType, + data_settings: JsonObjectType, + data_subscription: JsonObjectType, +) -> SystemV3: """Define a simplisafe-python V3 System object.""" system = SystemV3(Mock(subscription_data=data_subscription), SYSTEM_ID) system.async_get_latest_event = AsyncMock(return_value=data_latest_event) @@ -141,13 +154,13 @@ def system_v3_fixture(data_latest_event, data_sensor, data_settings, data_subscr @pytest.fixture(name="unique_id") -def unique_id_fixture(): +def unique_id_fixture() -> str: """Define a unique ID.""" return USER_ID @pytest.fixture(name="websocket") -def websocket_fixture(): +def websocket_fixture() -> Mock: """Define a simplisafe-python websocket object.""" return Mock( async_connect=AsyncMock(), From aef2f7d7078cf317a17bff7f17f949097137b4ae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:34:44 +0200 Subject: [PATCH 0049/2411] Improve type hints in canary tests (#120305) --- tests/components/canary/conftest.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/components/canary/conftest.py b/tests/components/canary/conftest.py index 336e6577ecc..583986fd483 100644 --- a/tests/components/canary/conftest.py +++ b/tests/components/canary/conftest.py @@ -4,16 +4,19 @@ from unittest.mock import MagicMock, patch from canary.api import Api import pytest +from typing_extensions import Generator + +from homeassistant.core import HomeAssistant @pytest.fixture(autouse=True) -def mock_ffmpeg(hass): +def mock_ffmpeg(hass: HomeAssistant) -> None: """Mock ffmpeg is loaded.""" hass.config.components.add("ffmpeg") @pytest.fixture -def canary(hass): +def canary() -> Generator[MagicMock]: """Mock the CanaryApi for easier testing.""" with ( patch.object(Api, "login", return_value=True), @@ -38,7 +41,7 @@ def canary(hass): @pytest.fixture -def canary_config_flow(hass): +def canary_config_flow() -> Generator[MagicMock]: """Mock the CanaryApi for easier config flow testing.""" with ( patch.object(Api, "login", return_value=True), From b4d0de9c0ff37a979a487b246935df210c4c3477 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:41:55 +0200 Subject: [PATCH 0050/2411] Improve type hints in conversation tests (#120306) --- .../conversation/test_default_agent.py | 104 ++++++++++-------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 511967e3a9c..dee7b4ca0ff 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -1,6 +1,7 @@ """Test for the default agent.""" from collections import defaultdict +from typing import Any from unittest.mock import AsyncMock, patch from hassil.recognize import Intent, IntentData, MatchEntity, RecognizeResult @@ -34,7 +35,7 @@ from tests.common import MockConfigEntry, async_mock_service @pytest.fixture -async def init_components(hass): +async def init_components(hass: HomeAssistant) -> None: """Initialize relevant components with empty configs.""" assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "conversation", {}) @@ -50,8 +51,9 @@ async def init_components(hass): {"entity_category": entity.EntityCategory.DIAGNOSTIC}, ], ) +@pytest.mark.usefixtures("init_components") async def test_hidden_entities_skipped( - hass: HomeAssistant, init_components, er_kwargs, entity_registry: er.EntityRegistry + hass: HomeAssistant, er_kwargs: dict[str, Any], entity_registry: er.EntityRegistry ) -> None: """Test we skip hidden entities.""" @@ -69,7 +71,8 @@ async def test_hidden_entities_skipped( assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS -async def test_exposed_domains(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_exposed_domains(hass: HomeAssistant) -> None: """Test that we can't interact with entities that aren't exposed.""" hass.states.async_set( "lock.front_door", "off", attributes={ATTR_FRIENDLY_NAME: "Front Door"} @@ -93,9 +96,9 @@ async def test_exposed_domains(hass: HomeAssistant, init_components) -> None: assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS +@pytest.mark.usefixtures("init_components") async def test_exposed_areas( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -160,10 +163,8 @@ async def test_exposed_areas( assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER -async def test_conversation_agent( - hass: HomeAssistant, - init_components, -) -> None: +@pytest.mark.usefixtures("init_components") +async def test_conversation_agent(hass: HomeAssistant) -> None: """Test DefaultAgent.""" agent = default_agent.async_get_default_agent(hass) with patch( @@ -209,9 +210,9 @@ async def test_expose_flag_automatically_set( } +@pytest.mark.usefixtures("init_components") async def test_unexposed_entities_skipped( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, ) -> None: @@ -262,7 +263,8 @@ async def test_unexposed_entities_skipped( assert result.response.matched_states[0].entity_id == exposed_light.entity_id -async def test_trigger_sentences(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_trigger_sentences(hass: HomeAssistant) -> None: """Test registering/unregistering/matching a few trigger sentences.""" trigger_sentences = ["It's party time", "It is time to party"] trigger_response = "Cowabunga!" @@ -303,9 +305,8 @@ async def test_trigger_sentences(hass: HomeAssistant, init_components) -> None: assert len(callback.mock_calls) == 0 -async def test_shopping_list_add_item( - hass: HomeAssistant, init_components, sl_setup -) -> None: +@pytest.mark.usefixtures("init_components", "sl_setup") +async def test_shopping_list_add_item(hass: HomeAssistant) -> None: """Test adding an item to the shopping list through the default agent.""" result = await conversation.async_converse( hass, "add apples to my shopping list", None, Context() @@ -316,7 +317,8 @@ async def test_shopping_list_add_item( } -async def test_nevermind_item(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_nevermind_item(hass: HomeAssistant) -> None: """Test HassNevermind intent through the default agent.""" result = await conversation.async_converse(hass, "nevermind", None, Context()) assert result.response.intent is not None @@ -326,9 +328,9 @@ async def test_nevermind_item(hass: HomeAssistant, init_components) -> None: assert not result.response.speech +@pytest.mark.usefixtures("init_components") async def test_device_area_context( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -465,7 +467,8 @@ async def test_device_area_context( } -async def test_error_no_device(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_no_device(hass: HomeAssistant) -> None: """Test error message when device/entity is missing.""" result = await conversation.async_converse( hass, "turn on missing entity", None, Context(), None @@ -479,7 +482,8 @@ async def test_error_no_device(hass: HomeAssistant, init_components) -> None: ) -async def test_error_no_area(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_no_area(hass: HomeAssistant) -> None: """Test error message when area is missing.""" result = await conversation.async_converse( hass, "turn on the lights in missing area", None, Context(), None @@ -493,7 +497,8 @@ async def test_error_no_area(hass: HomeAssistant, init_components) -> None: ) -async def test_error_no_floor(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_no_floor(hass: HomeAssistant) -> None: """Test error message when floor is missing.""" result = await conversation.async_converse( hass, "turn on all the lights on missing floor", None, Context(), None @@ -507,8 +512,9 @@ async def test_error_no_floor(hass: HomeAssistant, init_components) -> None: ) +@pytest.mark.usefixtures("init_components") async def test_error_no_device_in_area( - hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry + hass: HomeAssistant, area_registry: ar.AreaRegistry ) -> None: """Test error message when area is missing a device/entity.""" area_kitchen = area_registry.async_get_or_create("kitchen_id") @@ -525,9 +531,8 @@ async def test_error_no_device_in_area( ) -async def test_error_no_domain( - hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry -) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_no_domain(hass: HomeAssistant) -> None: """Test error message when no devices/entities exist for a domain.""" # We don't have a sentence for turning on all fans @@ -558,8 +563,9 @@ async def test_error_no_domain( ) +@pytest.mark.usefixtures("init_components") async def test_error_no_domain_in_area( - hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry + hass: HomeAssistant, area_registry: ar.AreaRegistry ) -> None: """Test error message when no devices/entities for a domain exist in an area.""" area_kitchen = area_registry.async_get_or_create("kitchen_id") @@ -576,9 +582,9 @@ async def test_error_no_domain_in_area( ) +@pytest.mark.usefixtures("init_components") async def test_error_no_domain_in_floor( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, floor_registry: fr.FloorRegistry, ) -> None: @@ -618,7 +624,8 @@ async def test_error_no_domain_in_floor( ) -async def test_error_no_device_class(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_no_device_class(hass: HomeAssistant) -> None: """Test error message when no entities of a device class exist.""" # Create a cover entity that is not a window. # This ensures that the filtering below won't exit early because there are @@ -658,8 +665,9 @@ async def test_error_no_device_class(hass: HomeAssistant, init_components) -> No ) +@pytest.mark.usefixtures("init_components") async def test_error_no_device_class_in_area( - hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry + hass: HomeAssistant, area_registry: ar.AreaRegistry ) -> None: """Test error message when no entities of a device class exist in an area.""" area_bedroom = area_registry.async_get_or_create("bedroom_id") @@ -676,7 +684,8 @@ async def test_error_no_device_class_in_area( ) -async def test_error_no_intent(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_no_intent(hass: HomeAssistant) -> None: """Test response with an intent match failure.""" with patch( "homeassistant.components.conversation.default_agent.recognize_all", @@ -696,8 +705,9 @@ async def test_error_no_intent(hass: HomeAssistant, init_components) -> None: ) +@pytest.mark.usefixtures("init_components") async def test_error_duplicate_names( - hass: HomeAssistant, init_components, entity_registry: er.EntityRegistry + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test error message when multiple devices have the same name (or alias).""" kitchen_light_1 = entity_registry.async_get_or_create("light", "demo", "1234") @@ -747,9 +757,9 @@ async def test_error_duplicate_names( ) +@pytest.mark.usefixtures("init_components") async def test_error_duplicate_names_in_area( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, ) -> None: @@ -805,7 +815,8 @@ async def test_error_duplicate_names_in_area( ) -async def test_error_wrong_state(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_wrong_state(hass: HomeAssistant) -> None: """Test error message when no entities are in the correct state.""" assert await async_setup_component(hass, media_player.DOMAIN, {}) @@ -824,9 +835,8 @@ async def test_error_wrong_state(hass: HomeAssistant, init_components) -> None: assert result.response.speech["plain"]["speech"] == "Sorry, no device is playing" -async def test_error_feature_not_supported( - hass: HomeAssistant, init_components -) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_feature_not_supported(hass: HomeAssistant) -> None: """Test error message when no devices support a required feature.""" assert await async_setup_component(hass, media_player.DOMAIN, {}) @@ -849,7 +859,8 @@ async def test_error_feature_not_supported( ) -async def test_error_no_timer_support(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_no_timer_support(hass: HomeAssistant) -> None: """Test error message when a device does not support timers (no handler is registered).""" device_id = "test_device" @@ -866,7 +877,8 @@ async def test_error_no_timer_support(hass: HomeAssistant, init_components) -> N ) -async def test_error_timer_not_found(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_error_timer_not_found(hass: HomeAssistant) -> None: """Test error message when a timer cannot be matched.""" device_id = "test_device" @@ -888,9 +900,9 @@ async def test_error_timer_not_found(hass: HomeAssistant, init_components) -> No ) +@pytest.mark.usefixtures("init_components") async def test_error_multiple_timers_matched( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, ) -> None: @@ -938,8 +950,9 @@ async def test_error_multiple_timers_matched( ) +@pytest.mark.usefixtures("init_components") async def test_no_states_matched_default_error( - hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry + hass: HomeAssistant, area_registry: ar.AreaRegistry ) -> None: """Test default response when no states match and slots are missing.""" area_kitchen = area_registry.async_get_or_create("kitchen_id") @@ -966,9 +979,9 @@ async def test_no_states_matched_default_error( ) +@pytest.mark.usefixtures("init_components") async def test_empty_aliases( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -1031,7 +1044,8 @@ async def test_empty_aliases( assert floors.values[0].text_in.text == floor_1.name -async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_all_domains_loaded(hass: HomeAssistant) -> None: """Test that sentences for all domains are always loaded.""" # light domain is not loaded @@ -1050,9 +1064,9 @@ async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None: ) +@pytest.mark.usefixtures("init_components") async def test_same_named_entities_in_different_areas( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, ) -> None: @@ -1147,9 +1161,9 @@ async def test_same_named_entities_in_different_areas( assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER +@pytest.mark.usefixtures("init_components") async def test_same_aliased_entities_in_different_areas( hass: HomeAssistant, - init_components, area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, ) -> None: @@ -1238,7 +1252,8 @@ async def test_same_aliased_entities_in_different_areas( assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER -async def test_device_id_in_handler(hass: HomeAssistant, init_components) -> None: +@pytest.mark.usefixtures("init_components") +async def test_device_id_in_handler(hass: HomeAssistant) -> None: """Test that the default agent passes device_id to intent handler.""" device_id = "test_device" @@ -1270,9 +1285,8 @@ async def test_device_id_in_handler(hass: HomeAssistant, init_components) -> Non assert handler.device_id == device_id -async def test_name_wildcard_lower_priority( - hass: HomeAssistant, init_components -) -> None: +@pytest.mark.usefixtures("init_components") +async def test_name_wildcard_lower_priority(hass: HomeAssistant) -> None: """Test that the default agent does not prioritize a {name} slot when it's a wildcard.""" class OrderBeerIntentHandler(intent.IntentHandler): From fbdc06647b2594f709b08f2d46924ec8b086bd9b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 14:05:13 +0200 Subject: [PATCH 0051/2411] Bump aiodhcpwatcher to 1.0.2 (#120311) --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index b8abd0a9919..ff81540b0ea 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -14,7 +14,7 @@ ], "quality_scale": "internal", "requirements": [ - "aiodhcpwatcher==1.0.0", + "aiodhcpwatcher==1.0.2", "aiodiscover==2.1.0", "cached_ipaddress==0.3.0" ] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a21d89705e8..c1924ef5afb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ # Automatically generated by gen_requirements_all.py, do not edit -aiodhcpwatcher==1.0.0 +aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-url-dispatcher==0.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 45b56edecbf..5c98a020f1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -219,7 +219,7 @@ aiobotocore==2.13.0 aiocomelit==0.9.0 # homeassistant.components.dhcp -aiodhcpwatcher==1.0.0 +aiodhcpwatcher==1.0.2 # homeassistant.components.dhcp aiodiscover==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8feebec2ef1..3b16aa85752 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -198,7 +198,7 @@ aiobotocore==2.13.0 aiocomelit==0.9.0 # homeassistant.components.dhcp -aiodhcpwatcher==1.0.0 +aiodhcpwatcher==1.0.2 # homeassistant.components.dhcp aiodiscover==2.1.0 From a5e6728227669af6afb0e0328b88fb6b0dba81bf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 Jun 2024 14:17:52 +0200 Subject: [PATCH 0052/2411] Improve integration sensor tests (#120316) --- tests/components/integration/test_sensor.py | 37 +++++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 243504cb3e0..3c8798600e9 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -314,7 +314,12 @@ async def test_trapezoidal(hass: HomeAssistant) -> None: start_time = dt_util.utcnow() with freeze_time(start_time) as freezer: # Testing a power sensor with non-monotonic intervals and values - for time, value in ((20, 10), (30, 30), (40, 5), (50, 0)): + for time, value, expected in ( + (20, 10, 1.67), + (30, 30, 5.0), + (40, 5, 7.92), + (50, 0, 8.33), + ): freezer.move_to(start_time + timedelta(minutes=time)) hass.states.async_set( entity_id, @@ -323,6 +328,8 @@ async def test_trapezoidal(hass: HomeAssistant) -> None: force_update=True, ) await hass.async_block_till_done() + state = hass.states.get("sensor.integration") + assert round(float(state.state), config["sensor"]["round"]) == expected state = hass.states.get("sensor.integration") assert state is not None @@ -353,9 +360,15 @@ async def test_left(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Testing a power sensor with non-monotonic intervals and values - for time, value in ((20, 10), (30, 30), (40, 5), (50, 0)): - now = dt_util.utcnow() + timedelta(minutes=time) - with freeze_time(now): + start_time = dt_util.utcnow() + with freeze_time(start_time) as freezer: + for time, value, expected in ( + (20, 10, 0.0), + (30, 30, 1.67), + (40, 5, 6.67), + (50, 0, 7.5), + ): + freezer.move_to(start_time + timedelta(minutes=time)) hass.states.async_set( entity_id, value, @@ -363,6 +376,8 @@ async def test_left(hass: HomeAssistant) -> None: force_update=True, ) await hass.async_block_till_done() + state = hass.states.get("sensor.integration") + assert round(float(state.state), config["sensor"]["round"]) == expected state = hass.states.get("sensor.integration") assert state is not None @@ -393,9 +408,15 @@ async def test_right(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Testing a power sensor with non-monotonic intervals and values - for time, value in ((20, 10), (30, 30), (40, 5), (50, 0)): - now = dt_util.utcnow() + timedelta(minutes=time) - with freeze_time(now): + start_time = dt_util.utcnow() + with freeze_time(start_time) as freezer: + for time, value, expected in ( + (20, 10, 3.33), + (30, 30, 8.33), + (40, 5, 9.17), + (50, 0, 9.17), + ): + freezer.move_to(start_time + timedelta(minutes=time)) hass.states.async_set( entity_id, value, @@ -403,6 +424,8 @@ async def test_right(hass: HomeAssistant) -> None: force_update=True, ) await hass.async_block_till_done() + state = hass.states.get("sensor.integration") + assert round(float(state.state), config["sensor"]["round"]) == expected state = hass.states.get("sensor.integration") assert state is not None From 8acb73df2e2aac8f7840ab45955f0eaee799336d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 14:32:28 +0200 Subject: [PATCH 0053/2411] Bump aiooui to 0.1.6 (#120312) --- homeassistant/components/nmap_tracker/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index 5200f778d4c..08d9b94cf2d 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", "iot_class": "local_polling", "loggers": ["nmap"], - "requirements": ["netmap==0.7.0.2", "getmac==0.9.4", "aiooui==0.1.5"] + "requirements": ["netmap==0.7.0.2", "getmac==0.9.4", "aiooui==0.1.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5c98a020f1d..5da81532ac5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -318,7 +318,7 @@ aiooncue==0.3.7 aioopenexchangerates==0.4.0 # homeassistant.components.nmap_tracker -aiooui==0.1.5 +aiooui==0.1.6 # homeassistant.components.pegel_online aiopegelonline==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b16aa85752..3635684f156 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -291,7 +291,7 @@ aiooncue==0.3.7 aioopenexchangerates==0.4.0 # homeassistant.components.nmap_tracker -aiooui==0.1.5 +aiooui==0.1.6 # homeassistant.components.pegel_online aiopegelonline==0.0.10 From 37c60d800e470bd3438daa2be66e81d22808d4ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 14:32:52 +0200 Subject: [PATCH 0054/2411] Bump aionut to 4.3.3 (#120313) --- homeassistant/components/nut/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index 1f649a32d7f..9e968b5a349 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["aionut"], - "requirements": ["aionut==4.3.2"], + "requirements": ["aionut==4.3.3"], "zeroconf": ["_nut._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 5da81532ac5..1fb02e82aec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -309,7 +309,7 @@ aionanoleaf==0.2.1 aionotion==2024.03.0 # homeassistant.components.nut -aionut==4.3.2 +aionut==4.3.3 # homeassistant.components.oncue aiooncue==0.3.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3635684f156..ede794f1cfe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ aionanoleaf==0.2.1 aionotion==2024.03.0 # homeassistant.components.nut -aionut==4.3.2 +aionut==4.3.3 # homeassistant.components.oncue aiooncue==0.3.7 From 604561aac0f420515293d1eb59f5bd60876af2b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 14:33:04 +0200 Subject: [PATCH 0055/2411] Bump uiprotect to 3.3.1 (#120314) --- 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 817d7c9c074..ba8e6f89dd5 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==3.1.8", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==3.3.1", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 1fb02e82aec..dfcdff788c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2791,7 +2791,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.1.8 +uiprotect==3.3.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ede794f1cfe..5543c68f5e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2171,7 +2171,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.1.8 +uiprotect==3.3.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From e6cb68d199f97bd58754795246010d0c075e657f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 14:33:16 +0200 Subject: [PATCH 0056/2411] Bump aiohttp-fast-zlib to 0.1.1 (#120315) --- 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 c1924ef5afb..3dd4fb13f7b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-url-dispatcher==0.3.0 -aiohttp-fast-zlib==0.1.0 +aiohttp-fast-zlib==0.1.1 aiohttp==3.9.5 aiohttp_cors==0.7.0 aiozoneinfo==0.2.0 diff --git a/pyproject.toml b/pyproject.toml index 9f83edd7f3e..f3269ee9765 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "aiohttp==3.9.5", "aiohttp_cors==0.7.0", "aiohttp-fast-url-dispatcher==0.3.0", - "aiohttp-fast-zlib==0.1.0", + "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.0", "astral==2.2", "async-interrupt==1.1.1", diff --git a/requirements.txt b/requirements.txt index 4c5e349d8b6..a470f12e57b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ aiodns==3.2.0 aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp-fast-url-dispatcher==0.3.0 -aiohttp-fast-zlib==0.1.0 +aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.0 astral==2.2 async-interrupt==1.1.1 From 57cdd3353736f7aada47746aa36c1297b1cbec1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 14:56:48 +0200 Subject: [PATCH 0057/2411] Bump aiosteamist to 1.0.0 (#120318) --- homeassistant/components/steamist/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/steamist/manifest.json b/homeassistant/components/steamist/manifest.json index 91ebc7f6a21..dcb0a50a9a9 100644 --- a/homeassistant/components/steamist/manifest.json +++ b/homeassistant/components/steamist/manifest.json @@ -16,5 +16,5 @@ "documentation": "https://www.home-assistant.io/integrations/steamist", "iot_class": "local_polling", "loggers": ["aiosteamist", "discovery30303"], - "requirements": ["aiosteamist==0.3.2", "discovery30303==0.2.1"] + "requirements": ["aiosteamist==1.0.0", "discovery30303==0.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index dfcdff788c2..c443edba8ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -374,7 +374,7 @@ aioslimproto==3.0.0 aiosolaredge==0.2.0 # homeassistant.components.steamist -aiosteamist==0.3.2 +aiosteamist==1.0.0 # homeassistant.components.switcher_kis aioswitcher==3.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5543c68f5e4..1c07b5c2ae2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -347,7 +347,7 @@ aioslimproto==3.0.0 aiosolaredge==0.2.0 # homeassistant.components.steamist -aiosteamist==0.3.2 +aiosteamist==1.0.0 # homeassistant.components.switcher_kis aioswitcher==3.4.3 From 389b9d1ad64f5d7a082266bb3f12c2ccae30b60a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 24 Jun 2024 15:16:09 +0200 Subject: [PATCH 0058/2411] Make sure ACK's are processed before mqtt tests are teared down (#120329) --- tests/components/mqtt/test_binary_sensor.py | 2 ++ tests/components/mqtt/test_device_tracker.py | 2 ++ tests/components/mqtt/test_event.py | 2 ++ tests/components/mqtt/test_mixins.py | 2 +- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 995aadd7dba..afa9ca9970e 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1173,6 +1173,8 @@ async def test_cleanup_triggers_and_restoring_state( state = hass.states.get("binary_sensor.test2") assert state.state == state2 + await hass.async_block_till_done(wait_background_tasks=True) + @pytest.mark.parametrize( "hass_config", diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 254885919b0..76129d4c549 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -240,6 +240,8 @@ async def test_device_tracker_discovery_update( # Entity was not updated as the state was not changed assert state.last_updated == datetime(2023, 8, 22, 19, 16, tzinfo=UTC) + await hass.async_block_till_done(wait_background_tasks=True) + async def test_cleanup_device_tracker( hass: HomeAssistant, diff --git a/tests/components/mqtt/test_event.py b/tests/components/mqtt/test_event.py index 64a2003606c..fd4f8eb3e5d 100644 --- a/tests/components/mqtt/test_event.py +++ b/tests/components/mqtt/test_event.py @@ -222,6 +222,8 @@ async def test_setting_event_value_via_mqtt_json_message_and_default_current_sta assert state.attributes.get("val") == "valcontent" assert state.attributes.get("par") == "parcontent" + await hass.async_block_till_done(wait_background_tasks=True) + @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_availability_when_connection_lost( diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index e46f0b56c15..ae4d232ba54 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -335,7 +335,7 @@ async def test_default_entity_and_device_name( # Assert that no issues ware registered assert len(events) == 0 - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Assert that no issues ware registered assert len(events) == 0 From 1a27cea6f2a69ce5576fb46a7fd6e3942e07b923 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 15:25:08 +0200 Subject: [PATCH 0059/2411] Bump bluetooth-adapters to 0.19.2 (#120324) --- 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 095eeff7f30..7239b5b3d05 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,7 +16,7 @@ "requirements": [ "bleak==0.22.1", "bleak-retry-connector==3.5.0", - "bluetooth-adapters==0.19.2", + "bluetooth-adapters==0.19.3", "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.3", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3dd4fb13f7b..4dc15aaf94b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ awesomeversion==24.2.0 bcrypt==4.1.2 bleak-retry-connector==3.5.0 bleak==0.22.1 -bluetooth-adapters==0.19.2 +bluetooth-adapters==0.19.3 bluetooth-auto-recovery==1.4.2 bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index c443edba8ba..b518a91a788 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -594,7 +594,7 @@ bluemaestro-ble==0.2.3 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.2 +bluetooth-adapters==0.19.3 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c07b5c2ae2..01718dcb96e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -509,7 +509,7 @@ bluecurrent-api==1.2.3 bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.2 +bluetooth-adapters==0.19.3 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 From 0d1b05052035fdc0d303eafe74318ca6b4719f1f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 Jun 2024 15:41:08 +0200 Subject: [PATCH 0060/2411] Remove create_create from StorageCollectionWebsocket.async_setup (#119489) --- .../components/assist_pipeline/pipeline.py | 9 ++--- .../components/image_upload/__init__.py | 18 ++++++++-- .../components/lovelace/resources.py | 9 ++--- homeassistant/helpers/collection.py | 34 ++++++++----------- 4 files changed, 34 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index ff360676cf7..6c1b3ced470 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -1605,14 +1605,9 @@ class PipelineStorageCollectionWebsocket( """Class to expose storage collection management over websocket.""" @callback - def async_setup( - self, - hass: HomeAssistant, - *, - create_create: bool = True, - ) -> None: + def async_setup(self, hass: HomeAssistant) -> None: """Set up the websocket commands.""" - super().async_setup(hass, create_create=create_create) + super().async_setup(hass) websocket_api.async_register_command( hass, diff --git a/homeassistant/components/image_upload/__init__.py b/homeassistant/components/image_upload/__init__.py index 69e2b0f12db..59b594561f0 100644 --- a/homeassistant/components/image_upload/__init__.py +++ b/homeassistant/components/image_upload/__init__.py @@ -14,6 +14,7 @@ from aiohttp.web_request import FileField from PIL import Image, ImageOps, UnidentifiedImageError import voluptuous as vol +from homeassistant.components import websocket_api from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.http.static import CACHE_HEADERS from homeassistant.const import CONF_ID @@ -47,13 +48,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: image_dir = pathlib.Path(hass.config.path("image")) hass.data[DOMAIN] = storage_collection = ImageStorageCollection(hass, image_dir) await storage_collection.async_load() - collection.DictStorageCollectionWebsocket( + ImageUploadStorageCollectionWebsocket( storage_collection, "image", "image", CREATE_FIELDS, UPDATE_FIELDS, - ).async_setup(hass, create_create=False) + ).async_setup(hass) hass.http.register_view(ImageUploadView) hass.http.register_view(ImageServeView(image_dir, storage_collection)) @@ -151,6 +152,19 @@ class ImageStorageCollection(collection.DictStorageCollection): await self.hass.async_add_executor_job(shutil.rmtree, self.image_dir / item_id) +class ImageUploadStorageCollectionWebsocket(collection.DictStorageCollectionWebsocket): + """Class to expose storage collection management over websocket.""" + + async def ws_create_item( + self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict + ) -> None: + """Create an item. + + Not supported, images are uploaded via the ImageUploadView. + """ + raise NotImplementedError + + class ImageUploadView(HomeAssistantView): """View to upload images.""" diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index c25c81e2c6f..316a31e8e9d 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -133,14 +133,9 @@ class ResourceStorageCollectionWebsocket(collection.DictStorageCollectionWebsock """Class to expose storage collection management over websocket.""" @callback - def async_setup( - self, - hass: HomeAssistant, - *, - create_create: bool = True, - ) -> None: + def async_setup(self, hass: HomeAssistant) -> None: """Set up the websocket commands.""" - super().async_setup(hass, create_create=create_create) + super().async_setup(hass) # Register lovelace/resources for backwards compatibility, remove in # Home Assistant Core 2025.1 diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 1dd94d85f9a..b9993098003 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -536,12 +536,7 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: return f"{self.model_name}_id" @callback - def async_setup( - self, - hass: HomeAssistant, - *, - create_create: bool = True, - ) -> None: + def async_setup(self, hass: HomeAssistant) -> None: """Set up the websocket commands.""" websocket_api.async_register_command( hass, @@ -552,20 +547,19 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: ), ) - if create_create: - websocket_api.async_register_command( - hass, - f"{self.api_prefix}/create", - websocket_api.require_admin( - websocket_api.async_response(self.ws_create_item) - ), - websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - **self.create_schema, - vol.Required("type"): f"{self.api_prefix}/create", - } - ), - ) + websocket_api.async_register_command( + hass, + f"{self.api_prefix}/create", + websocket_api.require_admin( + websocket_api.async_response(self.ws_create_item) + ), + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + **self.create_schema, + vol.Required("type"): f"{self.api_prefix}/create", + } + ), + ) websocket_api.async_register_command( hass, From 2776b28bb796356468140a135a2ba52be0a55bae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 16:20:07 +0200 Subject: [PATCH 0061/2411] Bump govee-ble to 0.31.3 (#120335) --- 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 98b802f8233..858e916d2d8 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -90,5 +90,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/govee_ble", "iot_class": "local_push", - "requirements": ["govee-ble==0.31.2"] + "requirements": ["govee-ble==0.31.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index b518a91a788..590fa2fcc29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -995,7 +995,7 @@ goslide-api==0.5.1 gotailwind==0.2.3 # homeassistant.components.govee_ble -govee-ble==0.31.2 +govee-ble==0.31.3 # homeassistant.components.govee_light_local govee-local-api==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 01718dcb96e..5c40fb32cc7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -821,7 +821,7 @@ googlemaps==2.5.1 gotailwind==0.2.3 # homeassistant.components.govee_ble -govee-ble==0.31.2 +govee-ble==0.31.3 # homeassistant.components.govee_light_local govee-local-api==1.5.0 From 85720f9e02e8cd7bfc6779245a352d9daa2c5bba Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 24 Jun 2024 16:20:44 +0200 Subject: [PATCH 0062/2411] Fix setup and tear down issues for mqtt discovery and config flow tests (#120333) * Fix setup and tear down issues for mqtt discovery and config flow tests * Use async callback --- tests/components/mqtt/conftest.py | 12 ++- tests/components/mqtt/test_config_flow.py | 10 +- tests/components/mqtt/test_discovery.py | 112 ++++++++++++++-------- 3 files changed, 85 insertions(+), 49 deletions(-) diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index bc4fa2e6634..9649e0b9ddf 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -6,7 +6,17 @@ from unittest.mock import patch import pytest from typing_extensions import Generator -from tests.components.light.conftest import mock_light_profiles # noqa: F401 +from homeassistant.components import mqtt + +ENTRY_DEFAULT_BIRTH_MESSAGE = { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "homeassistant/status", + mqtt.ATTR_PAYLOAD: "online", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, + }, +} @pytest.fixture(autouse=True) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 8df5de8e2fb..21ddf5ecc11 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1104,7 +1104,6 @@ async def test_skipping_advanced_options( ) async def test_step_reauth( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, mqtt_client_mock: MqttMockPahoClient, mock_try_connection: MagicMock, mock_reload_after_entry_update: MagicMock, @@ -1115,12 +1114,9 @@ async def test_step_reauth( """Test that the reauth step works.""" # Prepare the config entry - config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - hass.config_entries.async_update_entry( - config_entry, - data=test_input, - ) - await mqtt_mock_entry() + config_entry = MockConfigEntry(domain=mqtt.DOMAIN, data=test_input) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) # Start reauth flow config_entry.async_start_reauth(hass) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 911d205269c..e36971e386f 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -22,7 +22,9 @@ from homeassistant.components.mqtt.discovery import ( MQTTDiscoveryPayload, async_start, ) +from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.const import ( + EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED, STATE_ON, STATE_UNAVAILABLE, @@ -40,6 +42,7 @@ from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from homeassistant.setup import async_setup_component from homeassistant.util.signal_type import SignalTypeFormat +from .conftest import ENTRY_DEFAULT_BIRTH_MESSAGE from .test_common import help_all_subscribe_calls, help_test_unload_config_entry from tests.common import ( @@ -1454,32 +1457,15 @@ async def test_complex_discovery_topic_prefix( ].discovery_already_discovered +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_integration_discovery_subscribe_unsubscribe( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Check MQTT integration discovery subscribe and unsubscribe.""" - mqtt_mock = await mqtt_mock_entry() - mock_platform(hass, "comp.config_flow", None) - - entry = hass.config_entries.async_entries("mqtt")[0] - mqtt_mock().connected = True - - with patch( - "homeassistant.components.mqtt.discovery.async_get_mqtt", - return_value={"comp": ["comp/discovery/#"]}, - ): - await async_start(hass, "homeassistant", entry) - await hass.async_block_till_done() - await hass.async_block_till_done() - await hass.async_block_till_done() - - assert ("comp/discovery/#", 0) in help_all_subscribe_calls(mqtt_client_mock) - assert not mqtt_client_mock.unsubscribe.called class TestFlow(config_entries.ConfigFlow): """Test flow.""" @@ -1488,49 +1474,57 @@ async def test_mqtt_integration_discovery_subscribe_unsubscribe( """Test mqtt step.""" return self.async_abort(reason="already_configured") - assert not mqtt_client_mock.unsubscribe.called + mock_platform(hass, "comp.config_flow", None) + + birth = asyncio.Event() + + @callback + def wait_birth(msg: ReceiveMessage) -> None: + """Handle birth message.""" + birth.set() wait_unsub = asyncio.Event() + @callback def _mock_unsubscribe(topics: list[str]) -> tuple[int, int]: wait_unsub.set() return (0, 0) + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=ENTRY_DEFAULT_BIRTH_MESSAGE) + entry.add_to_hass(hass) with ( + patch( + "homeassistant.components.mqtt.discovery.async_get_mqtt", + return_value={"comp": ["comp/discovery/#"]}, + ), mock_config_flow("comp", TestFlow), patch.object(mqtt_client_mock, "unsubscribe", side_effect=_mock_unsubscribe), ): + assert await hass.config_entries.async_setup(entry.entry_id) + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await birth.wait() + + assert ("comp/discovery/#", 0) in help_all_subscribe_calls(mqtt_client_mock) + assert not mqtt_client_mock.unsubscribe.called + mqtt_client_mock.reset_mock() + + await hass.async_block_till_done(wait_background_tasks=True) async_fire_mqtt_message(hass, "comp/discovery/bla/config", "") await wait_unsub.wait() mqtt_client_mock.unsubscribe.assert_called_once_with(["comp/discovery/#"]) + await hass.async_block_till_done(wait_background_tasks=True) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_discovery_unsubscribe_once( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Check MQTT integration discovery unsubscribe once.""" - mqtt_mock = await mqtt_mock_entry() - mock_platform(hass, "comp.config_flow", None) - - entry = hass.config_entries.async_entries("mqtt")[0] - mqtt_mock().connected = True - - with patch( - "homeassistant.components.mqtt.discovery.async_get_mqtt", - return_value={"comp": ["comp/discovery/#"]}, - ): - await async_start(hass, "homeassistant", entry) - await hass.async_block_till_done() - await hass.async_block_till_done() - await hass.async_block_till_done() - - assert ("comp/discovery/#", 0) in help_all_subscribe_calls(mqtt_client_mock) - assert not mqtt_client_mock.unsubscribe.called class TestFlow(config_entries.ConfigFlow): """Test flow.""" @@ -1540,13 +1534,49 @@ async def test_mqtt_discovery_unsubscribe_once( await asyncio.sleep(0.1) return self.async_abort(reason="already_configured") - with mock_config_flow("comp", TestFlow): + mock_platform(hass, "comp.config_flow", None) + + birth = asyncio.Event() + + @callback + def wait_birth(msg: ReceiveMessage) -> None: + """Handle birth message.""" + birth.set() + + wait_unsub = asyncio.Event() + + @callback + def _mock_unsubscribe(topics: list[str]) -> tuple[int, int]: + wait_unsub.set() + return (0, 0) + + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=ENTRY_DEFAULT_BIRTH_MESSAGE) + entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.mqtt.discovery.async_get_mqtt", + return_value={"comp": ["comp/discovery/#"]}, + ), + mock_config_flow("comp", TestFlow), + patch.object(mqtt_client_mock, "unsubscribe", side_effect=_mock_unsubscribe), + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await birth.wait() + + assert ("comp/discovery/#", 0) in help_all_subscribe_calls(mqtt_client_mock) + assert not mqtt_client_mock.unsubscribe.called + + await hass.async_block_till_done(wait_background_tasks=True) async_fire_mqtt_message(hass, "comp/discovery/bla/config", "") async_fire_mqtt_message(hass, "comp/discovery/bla/config", "") - await asyncio.sleep(0.1) - await hass.async_block_till_done() - await hass.async_block_till_done() + await wait_unsub.wait() + await asyncio.sleep(0.2) + await hass.async_block_till_done(wait_background_tasks=True) mqtt_client_mock.unsubscribe.assert_called_once_with(["comp/discovery/#"]) + await hass.async_block_till_done(wait_background_tasks=True) async def test_clear_config_topic_disabled_entity( From 015bc0e172407a5779b7e9edee3e32101d8b15c1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:37:07 +0200 Subject: [PATCH 0063/2411] Use HassKey in homeassistant integration (#120332) --- homeassistant/components/homeassistant/const.py | 10 ++++++++-- .../components/homeassistant/exposed_entities.py | 16 ++++++++-------- tests/components/cloud/test_alexa_config.py | 7 +++---- tests/components/cloud/test_client.py | 3 +-- tests/components/cloud/test_google_config.py | 7 +++---- tests/components/conversation/__init__.py | 7 +++---- .../homeassistant/test_exposed_entities.py | 10 +++++----- 7 files changed, 31 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/homeassistant/const.py b/homeassistant/components/homeassistant/const.py index d56ab4397d9..7a51e218a16 100644 --- a/homeassistant/components/homeassistant/const.py +++ b/homeassistant/components/homeassistant/const.py @@ -1,12 +1,18 @@ """Constants for the Homeassistant integration.""" -from typing import Final +from __future__ import annotations + +from typing import TYPE_CHECKING, Final import homeassistant.core as ha +from homeassistant.util.hass_dict import HassKey + +if TYPE_CHECKING: + from .exposed_entities import ExposedEntities DOMAIN = ha.DOMAIN -DATA_EXPOSED_ENTITIES = f"{DOMAIN}.exposed_entites" +DATA_EXPOSED_ENTITIES: HassKey[ExposedEntities] = HassKey(f"{DOMAIN}.exposed_entites") DATA_STOP_HANDLER = f"{DOMAIN}.stop_handler" SERVICE_HOMEASSISTANT_STOP: Final = "stop" diff --git a/homeassistant/components/homeassistant/exposed_entities.py b/homeassistant/components/homeassistant/exposed_entities.py index 68632223045..7bd9f9ab7bc 100644 --- a/homeassistant/components/homeassistant/exposed_entities.py +++ b/homeassistant/components/homeassistant/exposed_entities.py @@ -440,7 +440,7 @@ def ws_list_exposed_entities( """Expose an entity to an assistant.""" result: dict[str, Any] = {} - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] entity_registry = er.async_get(hass) for entity_id in chain(exposed_entities.entities, entity_registry.entities): result[entity_id] = {} @@ -464,7 +464,7 @@ def ws_expose_new_entities_get( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Check if new entities are exposed to an assistant.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] expose_new = exposed_entities.async_get_expose_new_entities(msg["assistant"]) connection.send_result(msg["id"], {"expose_new": expose_new}) @@ -482,7 +482,7 @@ def ws_expose_new_entities_set( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Expose new entities to an assistant.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities(msg["assistant"], msg["expose_new"]) connection.send_result(msg["id"]) @@ -492,7 +492,7 @@ def async_listen_entity_updates( hass: HomeAssistant, assistant: str, listener: Callable[[], None] ) -> CALLBACK_TYPE: """Listen for updates to entity expose settings.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] return exposed_entities.async_listen_entity_updates(assistant, listener) @@ -501,7 +501,7 @@ def async_get_assistant_settings( hass: HomeAssistant, assistant: str ) -> dict[str, Mapping[str, Any]]: """Get all entity expose settings for an assistant.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] return exposed_entities.async_get_assistant_settings(assistant) @@ -510,7 +510,7 @@ def async_get_entity_settings( hass: HomeAssistant, entity_id: str ) -> dict[str, Mapping[str, Any]]: """Get assistant expose settings for an entity.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] return exposed_entities.async_get_entity_settings(entity_id) @@ -530,7 +530,7 @@ def async_expose_entity( @callback def async_should_expose(hass: HomeAssistant, assistant: str, entity_id: str) -> bool: """Return True if an entity should be exposed to an assistant.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] return exposed_entities.async_should_expose(assistant, entity_id) @@ -542,5 +542,5 @@ def async_set_assistant_option( Notify listeners if expose flag was changed. """ - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_assistant_option(assistant, entity_id, option, value) diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index a6b05198ca4..f37ee114220 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -15,7 +15,6 @@ from homeassistant.components.cloud.const import ( from homeassistant.components.cloud.prefs import CloudPreferences from homeassistant.components.homeassistant.exposed_entities import ( DATA_EXPOSED_ENTITIES, - ExposedEntities, async_expose_entity, async_get_entity_settings, ) @@ -39,13 +38,13 @@ def cloud_stub(): return Mock(is_logged_in=True, subscription_expired=False) -def expose_new(hass, expose_new): +def expose_new(hass: HomeAssistant, expose_new: bool) -> None: """Enable exposing new entities to Alexa.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities("cloud.alexa", expose_new) -def expose_entity(hass, entity_id, should_expose): +def expose_entity(hass: HomeAssistant, entity_id: str, should_expose: bool) -> None: """Expose an entity to Alexa.""" async_expose_entity(hass, "cloud.alexa", entity_id, should_expose) diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 7c04373c261..3126d56e3fb 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -21,7 +21,6 @@ from homeassistant.components.cloud.const import ( ) from homeassistant.components.homeassistant.exposed_entities import ( DATA_EXPOSED_ENTITIES, - ExposedEntities, async_expose_entity, ) from homeassistant.const import CONTENT_TYPE_JSON, __version__ as HA_VERSION @@ -262,7 +261,7 @@ async def test_google_config_expose_entity( """Test Google config exposing entity method uses latest config.""" # Enable exposing new entities to Google - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities("cloud.google_assistant", True) # Register a light entity diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 66530bfa3f8..89882d92037 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -18,7 +18,6 @@ from homeassistant.components.cloud.prefs import CloudPreferences from homeassistant.components.google_assistant import helpers as ga_helpers from homeassistant.components.homeassistant.exposed_entities import ( DATA_EXPOSED_ENTITIES, - ExposedEntities, async_expose_entity, async_get_entity_settings, ) @@ -47,13 +46,13 @@ def mock_conf(hass, cloud_prefs): ) -def expose_new(hass, expose_new): +def expose_new(hass: HomeAssistant, expose_new: bool) -> None: """Enable exposing new entities to Google.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities("cloud.google_assistant", expose_new) -def expose_entity(hass, entity_id, should_expose): +def expose_entity(hass: HomeAssistant, entity_id: str, should_expose: bool) -> None: """Expose an entity to Google.""" async_expose_entity(hass, "cloud.google_assistant", entity_id, should_expose) diff --git a/tests/components/conversation/__init__.py b/tests/components/conversation/__init__.py index fb9bcab7498..1ae3372968e 100644 --- a/tests/components/conversation/__init__.py +++ b/tests/components/conversation/__init__.py @@ -11,7 +11,6 @@ from homeassistant.components.conversation.models import ( ) from homeassistant.components.homeassistant.exposed_entities import ( DATA_EXPOSED_ENTITIES, - ExposedEntities, async_expose_entity, ) from homeassistant.core import HomeAssistant @@ -45,12 +44,12 @@ class MockAgent(conversation.AbstractConversationAgent): ) -def expose_new(hass: HomeAssistant, expose_new: bool): +def expose_new(hass: HomeAssistant, expose_new: bool) -> None: """Enable exposing new entities to the default agent.""" - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities(conversation.DOMAIN, expose_new) -def expose_entity(hass: HomeAssistant, entity_id: str, should_expose: bool): +def expose_entity(hass: HomeAssistant, entity_id: str, should_expose: bool) -> None: """Expose an entity to the default agent.""" async_expose_entity(hass, conversation.DOMAIN, entity_id, should_expose) diff --git a/tests/components/homeassistant/test_exposed_entities.py b/tests/components/homeassistant/test_exposed_entities.py index b3ff6594509..1f1955c2f82 100644 --- a/tests/components/homeassistant/test_exposed_entities.py +++ b/tests/components/homeassistant/test_exposed_entities.py @@ -103,7 +103,7 @@ async def test_load_preferences(hass: HomeAssistant) -> None: """Make sure that we can load/save data correctly.""" assert await async_setup_component(hass, "homeassistant", {}) - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] assert exposed_entities._assistants == {} exposed_entities.async_set_expose_new_entities("test1", True) @@ -139,7 +139,7 @@ async def test_expose_entity( entry1 = entity_registry.async_get_or_create("test", "test", "unique1") entry2 = entity_registry.async_get_or_create("test", "test", "unique2") - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] assert len(exposed_entities.entities) == 0 # Set options @@ -196,7 +196,7 @@ async def test_expose_entity_unknown( assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] assert len(exposed_entities.entities) == 0 # Set options @@ -442,7 +442,7 @@ async def test_should_expose( ) # Check with a different assistant - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities("cloud.no_default_expose", False) assert ( async_should_expose( @@ -545,7 +545,7 @@ async def test_listeners( """Make sure we call entity listeners.""" assert await async_setup_component(hass, "homeassistant", {}) - exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] + exposed_entities = hass.data[DATA_EXPOSED_ENTITIES] callbacks = [] exposed_entities.async_listen_entity_updates("test1", lambda: callbacks.append(1)) From d1de42a299148f921cbed1f99fc6d4d4281ca693 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:44:59 +0200 Subject: [PATCH 0064/2411] Replace deprecated attribute in abode (#120343) --- homeassistant/components/abode/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/abode/entity.py b/homeassistant/components/abode/entity.py index adbb68d86c6..70fe3a7caa4 100644 --- a/homeassistant/components/abode/entity.py +++ b/homeassistant/components/abode/entity.py @@ -105,7 +105,7 @@ class AbodeAutomation(AbodeEntity): super().__init__(data) self._automation = automation self._attr_name = automation.name - self._attr_unique_id = automation.automation_id + self._attr_unique_id = automation.id self._attr_extra_state_attributes = { "type": "CUE automation", } From e2f9ba5455712c3f9ecc8c4b0a5356b06bf2ce01 Mon Sep 17 00:00:00 2001 From: Lennard Beers Date: Mon, 24 Jun 2024 17:00:37 +0200 Subject: [PATCH 0065/2411] Bump eq3btsmart to 1.1.9 (#120339) --- homeassistant/components/eq3btsmart/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index bf5489531bc..d308d02027d 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -23,5 +23,5 @@ "iot_class": "local_polling", "loggers": ["eq3btsmart"], "quality_scale": "silver", - "requirements": ["eq3btsmart==1.1.8", "bleak-esphome==1.0.0"] + "requirements": ["eq3btsmart==1.1.9", "bleak-esphome==1.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 590fa2fcc29..442a73e658b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -831,7 +831,7 @@ epion==0.0.3 epson-projector==0.5.1 # homeassistant.components.eq3btsmart -eq3btsmart==1.1.8 +eq3btsmart==1.1.9 # homeassistant.components.esphome esphome-dashboard-api==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c40fb32cc7..e9f8fb5ca76 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -688,7 +688,7 @@ epion==0.0.3 epson-projector==0.5.1 # homeassistant.components.eq3btsmart -eq3btsmart==1.1.8 +eq3btsmart==1.1.9 # homeassistant.components.esphome esphome-dashboard-api==1.2.3 From bd6164ad4be690ba76fe14428d81a1bb94de7da7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 17:10:57 +0200 Subject: [PATCH 0066/2411] Bump bluetooth-data-tools to 1.19.3 (#120323) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/components/ld2410_ble/manifest.json | 2 +- homeassistant/components/led_ble/manifest.json | 2 +- homeassistant/components/private_ble_device/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 7239b5b3d05..0d6116f436a 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak-retry-connector==3.5.0", "bluetooth-adapters==0.19.3", "bluetooth-auto-recovery==1.4.2", - "bluetooth-data-tools==1.19.0", + "bluetooth-data-tools==1.19.3", "dbus-fast==2.21.3", "habluetooth==3.1.1" ] diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index 8b220f78e53..2389e3199e2 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/ld2410_ble", "integration_type": "device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.19.0", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.19.3", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index ee5d0431fc8..b793c64f67d 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -35,5 +35,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/led_ble", "iot_class": "local_polling", - "requirements": ["bluetooth-data-tools==1.19.0", "led-ble==1.0.1"] + "requirements": ["bluetooth-data-tools==1.19.3", "led-ble==1.0.1"] } diff --git a/homeassistant/components/private_ble_device/manifest.json b/homeassistant/components/private_ble_device/manifest.json index bc4ad0f2912..bb29e2cf105 100644 --- a/homeassistant/components/private_ble_device/manifest.json +++ b/homeassistant/components/private_ble_device/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/private_ble_device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.19.0"] + "requirements": ["bluetooth-data-tools==1.19.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4dc15aaf94b..7aa76295d4b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ bleak-retry-connector==3.5.0 bleak==0.22.1 bluetooth-adapters==0.19.3 bluetooth-auto-recovery==1.4.2 -bluetooth-data-tools==1.19.0 +bluetooth-data-tools==1.19.3 cached_ipaddress==0.3.0 certifi>=2021.5.30 ciso8601==2.3.1 diff --git a/requirements_all.txt b/requirements_all.txt index 442a73e658b..9bc0d50edec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -603,7 +603,7 @@ bluetooth-auto-recovery==1.4.2 # homeassistant.components.ld2410_ble # homeassistant.components.led_ble # homeassistant.components.private_ble_device -bluetooth-data-tools==1.19.0 +bluetooth-data-tools==1.19.3 # homeassistant.components.bond bond-async==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9f8fb5ca76..10e011a0aa5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -518,7 +518,7 @@ bluetooth-auto-recovery==1.4.2 # homeassistant.components.ld2410_ble # homeassistant.components.led_ble # homeassistant.components.private_ble_device -bluetooth-data-tools==1.19.0 +bluetooth-data-tools==1.19.3 # homeassistant.components.bond bond-async==0.2.1 From b7bf61a8c9f63029413ab5c3104aa9e068ec9fe5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 17:35:33 +0200 Subject: [PATCH 0067/2411] Bump habluetooth to 3.1.3 (#120337) --- 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 0d6116f436a..8883e63f286 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,6 +20,6 @@ "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.3", "dbus-fast==2.21.3", - "habluetooth==3.1.1" + "habluetooth==3.1.3" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7aa76295d4b..577889288a1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ dbus-fast==2.21.3 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.2.0 -habluetooth==3.1.1 +habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9bc0d50edec..6a0aaa29ae0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1050,7 +1050,7 @@ ha-philipsjs==3.2.2 habitipy==0.3.1 # homeassistant.components.bluetooth -habluetooth==3.1.1 +habluetooth==3.1.3 # homeassistant.components.cloud hass-nabucasa==0.81.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10e011a0aa5..4645df451cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -867,7 +867,7 @@ ha-philipsjs==3.2.2 habitipy==0.3.1 # homeassistant.components.bluetooth -habluetooth==3.1.1 +habluetooth==3.1.3 # homeassistant.components.cloud hass-nabucasa==0.81.1 From 063a3f3bca6eae8308c6b21968710aee30ee3e1b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 17:36:48 +0200 Subject: [PATCH 0068/2411] Bump discovery30303 to 0.3.2 (#120340) --- homeassistant/components/steamist/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/steamist/manifest.json b/homeassistant/components/steamist/manifest.json index dcb0a50a9a9..b15d7f87312 100644 --- a/homeassistant/components/steamist/manifest.json +++ b/homeassistant/components/steamist/manifest.json @@ -16,5 +16,5 @@ "documentation": "https://www.home-assistant.io/integrations/steamist", "iot_class": "local_polling", "loggers": ["aiosteamist", "discovery30303"], - "requirements": ["aiosteamist==1.0.0", "discovery30303==0.2.1"] + "requirements": ["aiosteamist==1.0.0", "discovery30303==0.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6a0aaa29ae0..19e8655df0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ directv==0.4.0 discogs-client==2.3.0 # homeassistant.components.steamist -discovery30303==0.2.1 +discovery30303==0.3.2 # homeassistant.components.dovado dovado==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4645df451cb..3ee23e7602d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -619,7 +619,7 @@ devolo-plc-api==1.4.1 directv==0.4.0 # homeassistant.components.steamist -discovery30303==0.2.1 +discovery30303==0.3.2 # homeassistant.components.dremel_3d_printer dremel3dpy==2.1.1 From eab1dc5255a2c9477a41781c9fe754a8c4993232 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 17:38:17 +0200 Subject: [PATCH 0069/2411] Bump home-assistant-bluetooth to 1.12.2 (#120338) --- 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 577889288a1..92e8f0a08e3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ ha-ffmpeg==3.2.0 habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 -home-assistant-bluetooth==1.12.1 +home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240610.1 home-assistant-intents==2024.6.21 httpx==0.27.0 diff --git a/pyproject.toml b/pyproject.toml index f3269ee9765..527451eaf61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.27.0", - "home-assistant-bluetooth==1.12.1", + "home-assistant-bluetooth==1.12.2", "ifaddr==0.2.0", "Jinja2==3.1.4", "lru-dict==1.3.0", diff --git a/requirements.txt b/requirements.txt index a470f12e57b..265d6231250 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ ciso8601==2.3.1 fnv-hash-fast==0.5.0 hass-nabucasa==0.81.1 httpx==0.27.0 -home-assistant-bluetooth==1.12.1 +home-assistant-bluetooth==1.12.2 ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 From 8e2665591527793db18134f34b152f1ed6064aab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 17:51:30 +0200 Subject: [PATCH 0070/2411] Bump led-ble to 1.0.2 (#120347) --- homeassistant/components/led_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index b793c64f67d..bf15ab1cc66 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -35,5 +35,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/led_ble", "iot_class": "local_polling", - "requirements": ["bluetooth-data-tools==1.19.3", "led-ble==1.0.1"] + "requirements": ["bluetooth-data-tools==1.19.3", "led-ble==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 19e8655df0a..061f2f2e849 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1233,7 +1233,7 @@ ld2410-ble==0.1.1 leaone-ble==0.1.0 # homeassistant.components.led_ble -led-ble==1.0.1 +led-ble==1.0.2 # homeassistant.components.foscam libpyfoscam==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ee23e7602d..8374190865d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1008,7 +1008,7 @@ ld2410-ble==0.1.1 leaone-ble==0.1.0 # homeassistant.components.led_ble -led-ble==1.0.1 +led-ble==1.0.2 # homeassistant.components.foscam libpyfoscam==1.2.2 From 0247f9185517163bb50f04ef57eed141f227ddf3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 18:32:32 +0200 Subject: [PATCH 0071/2411] Bump bleak to 0.22.2 (#120325) --- 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 8883e63f286..df2278399ab 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -14,7 +14,7 @@ ], "quality_scale": "internal", "requirements": [ - "bleak==0.22.1", + "bleak==0.22.2", "bleak-retry-connector==3.5.0", "bluetooth-adapters==0.19.3", "bluetooth-auto-recovery==1.4.2", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 92e8f0a08e3..438a73586c0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ attrs==23.2.0 awesomeversion==24.2.0 bcrypt==4.1.2 bleak-retry-connector==3.5.0 -bleak==0.22.1 +bleak==0.22.2 bluetooth-adapters==0.19.3 bluetooth-auto-recovery==1.4.2 bluetooth-data-tools==1.19.3 diff --git a/requirements_all.txt b/requirements_all.txt index 061f2f2e849..5b321763d2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -572,7 +572,7 @@ bleak-esphome==1.0.0 bleak-retry-connector==3.5.0 # homeassistant.components.bluetooth -bleak==0.22.1 +bleak==0.22.2 # homeassistant.components.blebox blebox-uniapi==2.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8374190865d..3d12f32d295 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -494,7 +494,7 @@ bleak-esphome==1.0.0 bleak-retry-connector==3.5.0 # homeassistant.components.bluetooth -bleak==0.22.1 +bleak==0.22.2 # homeassistant.components.blebox blebox-uniapi==2.4.2 From d073fd9b37f6cc4a6e7b1971088b8e6a422425da Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 Jun 2024 18:33:08 +0200 Subject: [PATCH 0072/2411] Improve integration sensor tests (#120326) --- tests/components/integration/test_sensor.py | 135 ++++++++++++++------ 1 file changed, 96 insertions(+), 39 deletions(-) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 3c8798600e9..03df38893a2 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -294,7 +294,36 @@ async def test_restore_state_failed(hass: HomeAssistant, extra_attributes) -> No assert state.state == STATE_UNKNOWN -async def test_trapezoidal(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("force_update", "sequence"), + [ + ( + False, + ( + (20, 10, 1.67), + (30, 30, 5.0), + (40, 5, 7.92), + (50, 5, 7.92), + (60, 0, 8.75), + ), + ), + ( + True, + ( + (20, 10, 1.67), + (30, 30, 5.0), + (40, 5, 7.92), + (50, 5, 8.75), + (60, 0, 9.17), + ), + ), + ], +) +async def test_trapezoidal( + hass: HomeAssistant, + sequence: tuple[tuple[float, float, float, ...]], + force_update: bool, +) -> None: """Test integration sensor state.""" config = { "sensor": { @@ -314,32 +343,51 @@ async def test_trapezoidal(hass: HomeAssistant) -> None: start_time = dt_util.utcnow() with freeze_time(start_time) as freezer: # Testing a power sensor with non-monotonic intervals and values - for time, value, expected in ( - (20, 10, 1.67), - (30, 30, 5.0), - (40, 5, 7.92), - (50, 0, 8.33), - ): + for time, value, expected in sequence: freezer.move_to(start_time + timedelta(minutes=time)) hass.states.async_set( entity_id, value, {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT}, - force_update=True, + force_update=force_update, ) await hass.async_block_till_done() state = hass.states.get("sensor.integration") assert round(float(state.state), config["sensor"]["round"]) == expected - state = hass.states.get("sensor.integration") - assert state is not None - - assert round(float(state.state), config["sensor"]["round"]) == 8.33 - assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR -async def test_left(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("force_update", "sequence"), + [ + ( + False, + ( + (20, 10, 0.0), + (30, 30, 1.67), + (40, 5, 6.67), + (50, 5, 6.67), + (60, 0, 8.33), + ), + ), + ( + True, + ( + (20, 10, 0.0), + (30, 30, 1.67), + (40, 5, 6.67), + (50, 5, 7.5), + (60, 0, 8.33), + ), + ), + ], +) +async def test_left( + hass: HomeAssistant, + sequence: tuple[tuple[float, float, float, ...]], + force_update: bool, +) -> None: """Test integration sensor state with left reimann method.""" config = { "sensor": { @@ -362,32 +410,51 @@ async def test_left(hass: HomeAssistant) -> None: # Testing a power sensor with non-monotonic intervals and values start_time = dt_util.utcnow() with freeze_time(start_time) as freezer: - for time, value, expected in ( - (20, 10, 0.0), - (30, 30, 1.67), - (40, 5, 6.67), - (50, 0, 7.5), - ): + for time, value, expected in sequence: freezer.move_to(start_time + timedelta(minutes=time)) hass.states.async_set( entity_id, value, {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT}, - force_update=True, + force_update=force_update, ) await hass.async_block_till_done() state = hass.states.get("sensor.integration") assert round(float(state.state), config["sensor"]["round"]) == expected - state = hass.states.get("sensor.integration") - assert state is not None - - assert round(float(state.state), config["sensor"]["round"]) == 7.5 - assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR -async def test_right(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("force_update", "sequence"), + [ + ( + False, + ( + (20, 10, 3.33), + (30, 30, 8.33), + (40, 5, 9.17), + (50, 5, 9.17), + (60, 0, 9.17), + ), + ), + ( + True, + ( + (20, 10, 3.33), + (30, 30, 8.33), + (40, 5, 9.17), + (50, 5, 10.0), + (60, 0, 10.0), + ), + ), + ], +) +async def test_right( + hass: HomeAssistant, + sequence: tuple[tuple[float, float, float, ...]], + force_update: bool, +) -> None: """Test integration sensor state with left reimann method.""" config = { "sensor": { @@ -410,28 +477,18 @@ async def test_right(hass: HomeAssistant) -> None: # Testing a power sensor with non-monotonic intervals and values start_time = dt_util.utcnow() with freeze_time(start_time) as freezer: - for time, value, expected in ( - (20, 10, 3.33), - (30, 30, 8.33), - (40, 5, 9.17), - (50, 0, 9.17), - ): + for time, value, expected in sequence: freezer.move_to(start_time + timedelta(minutes=time)) hass.states.async_set( entity_id, value, {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT}, - force_update=True, + force_update=force_update, ) await hass.async_block_till_done() state = hass.states.get("sensor.integration") assert round(float(state.state), config["sensor"]["round"]) == expected - state = hass.states.get("sensor.integration") - assert state is not None - - assert round(float(state.state), config["sensor"]["round"]) == 9.17 - assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR From 4089b808c3512e5460b3cde8044e967f0e9a964a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:33:51 +0200 Subject: [PATCH 0073/2411] Improve type hints in comfoconnect tests (#120345) --- tests/components/comfoconnect/test_sensor.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/components/comfoconnect/test_sensor.py b/tests/components/comfoconnect/test_sensor.py index cea5ed0122f..91e7e1f0e25 100644 --- a/tests/components/comfoconnect/test_sensor.py +++ b/tests/components/comfoconnect/test_sensor.py @@ -1,9 +1,9 @@ """Tests for the comfoconnect sensor platform.""" -# import json -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator from homeassistant.components.sensor import DOMAIN from homeassistant.core import HomeAssistant @@ -28,7 +28,7 @@ VALID_CONFIG = { @pytest.fixture -def mock_bridge_discover(): +def mock_bridge_discover() -> Generator[MagicMock]: """Mock the bridge discover method.""" with patch("pycomfoconnect.bridge.Bridge.discover") as mock_bridge_discover: mock_bridge_discover.return_value[0].uuid.hex.return_value = "00" @@ -36,7 +36,7 @@ def mock_bridge_discover(): @pytest.fixture -def mock_comfoconnect_command(): +def mock_comfoconnect_command() -> Generator[MagicMock]: """Mock the ComfoConnect connect method.""" with patch( "pycomfoconnect.comfoconnect.ComfoConnect._command" @@ -45,14 +45,19 @@ def mock_comfoconnect_command(): @pytest.fixture -async def setup_sensor(hass, mock_bridge_discover, mock_comfoconnect_command): +async def setup_sensor( + hass: HomeAssistant, + mock_bridge_discover: MagicMock, + mock_comfoconnect_command: MagicMock, +) -> None: """Set up demo sensor component.""" with assert_setup_component(1, DOMAIN): await async_setup_component(hass, DOMAIN, VALID_CONFIG) await hass.async_block_till_done() -async def test_sensors(hass: HomeAssistant, setup_sensor) -> None: +@pytest.mark.usefixtures("setup_sensor") +async def test_sensors(hass: HomeAssistant) -> None: """Test the sensors.""" state = hass.states.get("sensor.comfoairq_inside_humidity") assert state is not None From 8bad421a04b7568028e2633a20cfa8c7111247d0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:36:57 +0200 Subject: [PATCH 0074/2411] Improve type hints in config tests (#120346) --- tests/components/config/conftest.py | 6 +- .../test_auth_provider_homeassistant.py | 22 ++++-- tests/components/config/test_automation.py | 34 ++++---- .../components/config/test_config_entries.py | 79 +++++++++++-------- tests/components/config/test_scene.py | 23 +++--- tests/components/config/test_script.py | 25 +++--- 6 files changed, 109 insertions(+), 80 deletions(-) diff --git a/tests/components/config/conftest.py b/tests/components/config/conftest.py index ffd2f764922..c401ac19fa9 100644 --- a/tests/components/config/conftest.py +++ b/tests/components/config/conftest.py @@ -5,9 +5,11 @@ from copy import deepcopy import json import logging from os.path import basename +from typing import Any from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.core import HomeAssistant @@ -17,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) @contextmanager -def mock_config_store(data=None): +def mock_config_store(data: dict[str, Any] | None = None) -> Generator[dict[str, Any]]: """Mock config yaml store. Data is a dict {'key': {'version': version, 'data': data}} @@ -72,7 +74,7 @@ def mock_config_store(data=None): @pytest.fixture -def hass_config_store(): +def hass_config_store() -> Generator[dict[str, Any]]: """Fixture to mock config yaml store.""" with mock_config_store() as stored_data: yield stored_data diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index 5c5661376e2..044d6cdb571 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -38,7 +38,9 @@ async def owner_access_token(hass: HomeAssistant, hass_owner_user: MockUser) -> @pytest.fixture -async def hass_admin_credential(hass, auth_provider): +async def hass_admin_credential( + hass: HomeAssistant, auth_provider: prov_ha.HassAuthProvider +): """Overload credentials to admin user.""" await hass.async_add_executor_job( auth_provider.data.add_auth, "test-user", "test-pass" @@ -284,7 +286,9 @@ async def test_delete_unknown_auth( async def test_change_password( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, auth_provider + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + auth_provider: prov_ha.HassAuthProvider, ) -> None: """Test that change password succeeds with valid password.""" client = await hass_ws_client(hass) @@ -306,7 +310,7 @@ async def test_change_password_wrong_pw( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_admin_user: MockUser, - auth_provider, + auth_provider: prov_ha.HassAuthProvider, ) -> None: """Test that change password fails with invalid password.""" @@ -349,7 +353,9 @@ async def test_change_password_no_creds( async def test_admin_change_password_not_owner( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, auth_provider + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + auth_provider: prov_ha.HassAuthProvider, ) -> None: """Test that change password fails when not owner.""" client = await hass_ws_client(hass) @@ -372,7 +378,7 @@ async def test_admin_change_password_not_owner( async def test_admin_change_password_no_user( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, owner_access_token + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, owner_access_token: str ) -> None: """Test that change password fails with unknown user.""" client = await hass_ws_client(hass, owner_access_token) @@ -394,7 +400,7 @@ async def test_admin_change_password_no_user( async def test_admin_change_password_no_cred( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - owner_access_token, + owner_access_token: str, hass_admin_user: MockUser, ) -> None: """Test that change password fails with unknown credential.""" @@ -419,8 +425,8 @@ async def test_admin_change_password_no_cred( async def test_admin_change_password( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - owner_access_token, - auth_provider, + owner_access_token: str, + auth_provider: prov_ha.HassAuthProvider, hass_admin_user: MockUser, ) -> None: """Test that owners can change any password.""" diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 9d9ee5d5649..f907732109d 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -26,7 +26,7 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @pytest.fixture async def setup_automation( hass: HomeAssistant, - automation_config, + automation_config: dict[str, Any], stub_blueprint_populate: None, ) -> None: """Set up automation integration.""" @@ -36,11 +36,11 @@ async def setup_automation( @pytest.mark.parametrize("automation_config", [{}]) +@pytest.mark.usefixtures("setup_automation") async def test_get_automation_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], ) -> None: """Test getting automation config.""" with patch.object(config, "SECTIONS", [automation]): @@ -59,11 +59,11 @@ async def test_get_automation_config( @pytest.mark.parametrize("automation_config", [{}]) +@pytest.mark.usefixtures("setup_automation") async def test_update_automation_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], ) -> None: """Test updating automation config.""" with patch.object(config, "SECTIONS", [automation]): @@ -143,11 +143,11 @@ async def test_update_automation_config( ), ], ) +@pytest.mark.usefixtures("setup_automation") async def test_update_automation_config_with_error( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], caplog: pytest.LogCaptureFixture, updated_config: Any, validation_error: str, @@ -196,11 +196,11 @@ async def test_update_automation_config_with_error( ), ], ) +@pytest.mark.usefixtures("setup_automation") async def test_update_automation_config_with_blueprint_substitution_error( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], caplog: pytest.LogCaptureFixture, updated_config: Any, validation_error: str, @@ -235,11 +235,11 @@ async def test_update_automation_config_with_blueprint_substitution_error( @pytest.mark.parametrize("automation_config", [{}]) +@pytest.mark.usefixtures("setup_automation") async def test_update_remove_key_automation_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], ) -> None: """Test updating automation config while removing a key.""" with patch.object(config, "SECTIONS", [automation]): @@ -272,11 +272,11 @@ async def test_update_remove_key_automation_config( @pytest.mark.parametrize("automation_config", [{}]) +@pytest.mark.usefixtures("setup_automation") async def test_bad_formatted_automations( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], ) -> None: """Test that we handle automations without ID.""" with patch.object(config, "SECTIONS", [automation]): @@ -332,12 +332,12 @@ async def test_bad_formatted_automations( ], ], ) +@pytest.mark.usefixtures("setup_automation") async def test_delete_automation( hass: HomeAssistant, hass_client: ClientSessionGenerator, entity_registry: er.EntityRegistry, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], ) -> None: """Test deleting an automation.""" @@ -373,12 +373,12 @@ async def test_delete_automation( @pytest.mark.parametrize("automation_config", [{}]) +@pytest.mark.usefixtures("setup_automation") async def test_api_calls_require_admin( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_read_only_access_token: str, - hass_config_store, - setup_automation, + hass_config_store: dict[str, Any], ) -> None: """Test cloud APIs endpoints do not work as a normal user.""" with patch.object(config, "SECTIONS", [automation]): diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 95ff87c2beb..e023a60f215 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -6,6 +6,7 @@ from unittest.mock import ANY, AsyncMock, patch from aiohttp.test_utils import TestClient import pytest +from typing_extensions import Generator import voluptuous as vol from homeassistant import config_entries as core_ce, data_entry_flow, loader @@ -30,14 +31,14 @@ from tests.typing import ClientSessionGenerator, WebSocketGenerator @pytest.fixture -def clear_handlers(): +def clear_handlers() -> Generator[None]: """Clear config entry handlers.""" with patch.dict(HANDLERS, clear=True): yield @pytest.fixture(autouse=True) -def mock_test_component(hass): +def mock_test_component(hass: HomeAssistant) -> None: """Ensure a component called 'test' exists.""" mock_integration(hass, MockModule("test")) @@ -53,7 +54,7 @@ async def client( @pytest.fixture -async def mock_flow(): +def mock_flow() -> Generator[None]: """Mock a config flow.""" class Comp1ConfigFlow(ConfigFlow): @@ -68,9 +69,8 @@ async def mock_flow(): yield -async def test_get_entries( - hass: HomeAssistant, client, clear_handlers, mock_flow -) -> None: +@pytest.mark.usefixtures("clear_handlers", "mock_flow") +async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None: """Test get entries.""" mock_integration(hass, MockModule("comp1")) mock_integration( @@ -238,7 +238,7 @@ async def test_get_entries( assert data[0]["domain"] == "comp5" -async def test_remove_entry(hass: HomeAssistant, client) -> None: +async def test_remove_entry(hass: HomeAssistant, client: TestClient) -> None: """Test removing an entry via the API.""" entry = MockConfigEntry( domain="kitchen_sink", state=core_ce.ConfigEntryState.LOADED @@ -251,7 +251,7 @@ async def test_remove_entry(hass: HomeAssistant, client) -> None: assert len(hass.config_entries.async_entries()) == 0 -async def test_reload_entry(hass: HomeAssistant, client) -> None: +async def test_reload_entry(hass: HomeAssistant, client: TestClient) -> None: """Test reloading an entry via the API.""" entry = MockConfigEntry( domain="kitchen_sink", state=core_ce.ConfigEntryState.LOADED @@ -267,14 +267,14 @@ async def test_reload_entry(hass: HomeAssistant, client) -> None: assert len(hass.config_entries.async_entries()) == 1 -async def test_reload_invalid_entry(hass: HomeAssistant, client) -> None: +async def test_reload_invalid_entry(hass: HomeAssistant, client: TestClient) -> None: """Test reloading an invalid entry via the API.""" resp = await client.post("/api/config/config_entries/entry/invalid/reload") assert resp.status == HTTPStatus.NOT_FOUND async def test_remove_entry_unauth( - hass: HomeAssistant, client, hass_admin_user: MockUser + hass: HomeAssistant, client: TestClient, hass_admin_user: MockUser ) -> None: """Test removing an entry via the API.""" hass_admin_user.groups = [] @@ -286,7 +286,7 @@ async def test_remove_entry_unauth( async def test_reload_entry_unauth( - hass: HomeAssistant, client, hass_admin_user: MockUser + hass: HomeAssistant, client: TestClient, hass_admin_user: MockUser ) -> None: """Test reloading an entry via the API.""" hass_admin_user.groups = [] @@ -300,7 +300,7 @@ async def test_reload_entry_unauth( async def test_reload_entry_in_failed_state( - hass: HomeAssistant, client, hass_admin_user: MockUser + hass: HomeAssistant, client: TestClient, hass_admin_user: MockUser ) -> None: """Test reloading an entry via the API that has already failed to unload.""" entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.FAILED_UNLOAD) @@ -314,7 +314,7 @@ async def test_reload_entry_in_failed_state( async def test_reload_entry_in_setup_retry( - hass: HomeAssistant, client, hass_admin_user: MockUser + hass: HomeAssistant, client: TestClient, hass_admin_user: MockUser ) -> None: """Test reloading an entry via the API that is in setup retry.""" mock_setup_entry = AsyncMock(return_value=True) @@ -356,7 +356,7 @@ async def test_reload_entry_in_setup_retry( ], ) async def test_available_flows( - hass: HomeAssistant, client, type_filter, result + hass: HomeAssistant, client: TestClient, type_filter: str | None, result: set[str] ) -> None: """Test querying the available flows.""" with patch.object( @@ -378,7 +378,7 @@ async def test_available_flows( ############################ -async def test_initialize_flow(hass: HomeAssistant, client) -> None: +async def test_initialize_flow(hass: HomeAssistant, client: TestClient) -> None: """Test we can initialize a flow.""" mock_platform(hass, "test.config_flow", None) @@ -427,7 +427,9 @@ async def test_initialize_flow(hass: HomeAssistant, client) -> None: } -async def test_initialize_flow_unmet_dependency(hass: HomeAssistant, client) -> None: +async def test_initialize_flow_unmet_dependency( + hass: HomeAssistant, client: TestClient +) -> None: """Test unmet dependencies are listed.""" mock_platform(hass, "test.config_flow", None) @@ -457,7 +459,7 @@ async def test_initialize_flow_unmet_dependency(hass: HomeAssistant, client) -> async def test_initialize_flow_unauth( - hass: HomeAssistant, client, hass_admin_user: MockUser + hass: HomeAssistant, client: TestClient, hass_admin_user: MockUser ) -> None: """Test we can initialize a flow.""" hass_admin_user.groups = [] @@ -483,7 +485,7 @@ async def test_initialize_flow_unauth( assert resp.status == HTTPStatus.UNAUTHORIZED -async def test_abort(hass: HomeAssistant, client) -> None: +async def test_abort(hass: HomeAssistant, client: TestClient) -> None: """Test a flow that aborts.""" mock_platform(hass, "test.config_flow", None) @@ -508,7 +510,7 @@ async def test_abort(hass: HomeAssistant, client) -> None: @pytest.mark.usefixtures("enable_custom_integrations") -async def test_create_account(hass: HomeAssistant, client) -> None: +async def test_create_account(hass: HomeAssistant, client: TestClient) -> None: """Test a flow that creates an account.""" mock_platform(hass, "test.config_flow", None) @@ -566,7 +568,7 @@ async def test_create_account(hass: HomeAssistant, client) -> None: @pytest.mark.usefixtures("enable_custom_integrations") -async def test_two_step_flow(hass: HomeAssistant, client) -> None: +async def test_two_step_flow(hass: HomeAssistant, client: TestClient) -> None: """Test we can finish a two step flow.""" mock_integration( hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) @@ -646,7 +648,7 @@ async def test_two_step_flow(hass: HomeAssistant, client) -> None: async def test_continue_flow_unauth( - hass: HomeAssistant, client, hass_admin_user: MockUser + hass: HomeAssistant, client: TestClient, hass_admin_user: MockUser ) -> None: """Test we can't finish a two step flow.""" mock_integration( @@ -745,7 +747,7 @@ async def test_get_progress_index_unauth( assert response["error"]["code"] == "unauthorized" -async def test_get_progress_flow(hass: HomeAssistant, client) -> None: +async def test_get_progress_flow(hass: HomeAssistant, client: TestClient) -> None: """Test we can query the API for same result as we get from init a flow.""" mock_platform(hass, "test.config_flow", None) @@ -780,7 +782,7 @@ async def test_get_progress_flow(hass: HomeAssistant, client) -> None: async def test_get_progress_flow_unauth( - hass: HomeAssistant, client, hass_admin_user: MockUser + hass: HomeAssistant, client: TestClient, hass_admin_user: MockUser ) -> None: """Test we can can't query the API for result of flow.""" mock_platform(hass, "test.config_flow", None) @@ -814,7 +816,7 @@ async def test_get_progress_flow_unauth( assert resp2.status == HTTPStatus.UNAUTHORIZED -async def test_options_flow(hass: HomeAssistant, client) -> None: +async def test_options_flow(hass: HomeAssistant, client: TestClient) -> None: """Test we can change options.""" class TestFlow(core_ce.ConfigFlow): @@ -874,7 +876,11 @@ async def test_options_flow(hass: HomeAssistant, client) -> None: ], ) async def test_options_flow_unauth( - hass: HomeAssistant, client, hass_admin_user: MockUser, endpoint: str, method: str + hass: HomeAssistant, + client: TestClient, + hass_admin_user: MockUser, + endpoint: str, + method: str, ) -> None: """Test unauthorized on options flow.""" @@ -911,7 +917,7 @@ async def test_options_flow_unauth( assert resp.status == HTTPStatus.UNAUTHORIZED -async def test_two_step_options_flow(hass: HomeAssistant, client) -> None: +async def test_two_step_options_flow(hass: HomeAssistant, client: TestClient) -> None: """Test we can finish a two step options flow.""" mock_integration( hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) @@ -977,7 +983,9 @@ async def test_two_step_options_flow(hass: HomeAssistant, client) -> None: } -async def test_options_flow_with_invalid_data(hass: HomeAssistant, client) -> None: +async def test_options_flow_with_invalid_data( + hass: HomeAssistant, client: TestClient +) -> None: """Test an options flow with invalid_data.""" mock_integration( hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) @@ -1358,8 +1366,9 @@ async def test_ignore_flow_nonexisting( assert response["error"]["code"] == "not_found" +@pytest.mark.usefixtures("clear_handlers") async def test_get_matching_entries_ws( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, clear_handlers + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test get entries with the websocket api.""" assert await async_setup_component(hass, "config", {}) @@ -1748,8 +1757,9 @@ async def test_get_matching_entries_ws( assert response["success"] is False +@pytest.mark.usefixtures("clear_handlers") async def test_subscribe_entries_ws( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, clear_handlers + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test subscribe entries with the websocket api.""" assert await async_setup_component(hass, "config", {}) @@ -1934,8 +1944,9 @@ async def test_subscribe_entries_ws( ] +@pytest.mark.usefixtures("clear_handlers") async def test_subscribe_entries_ws_filtered( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, clear_handlers + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test subscribe entries with the websocket api with a type filter.""" assert await async_setup_component(hass, "config", {}) @@ -2139,7 +2150,9 @@ async def test_subscribe_entries_ws_filtered( ] -async def test_flow_with_multiple_schema_errors(hass: HomeAssistant, client) -> None: +async def test_flow_with_multiple_schema_errors( + hass: HomeAssistant, client: TestClient +) -> None: """Test an config flow with multiple schema errors.""" mock_integration( hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) @@ -2182,7 +2195,7 @@ async def test_flow_with_multiple_schema_errors(hass: HomeAssistant, client) -> async def test_flow_with_multiple_schema_errors_base( - hass: HomeAssistant, client + hass: HomeAssistant, client: TestClient ) -> None: """Test an config flow with multiple schema errors where fields are not in the schema.""" mock_integration( @@ -2226,7 +2239,7 @@ async def test_flow_with_multiple_schema_errors_base( @pytest.mark.usefixtures("enable_custom_integrations") -async def test_supports_reconfigure(hass: HomeAssistant, client) -> None: +async def test_supports_reconfigure(hass: HomeAssistant, client: TestClient) -> None: """Test a flow that support reconfigure step.""" mock_platform(hass, "test.config_flow", None) diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index 6ca42e7f56d..22bcfa345a2 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -2,6 +2,7 @@ from http import HTTPStatus import json +from typing import Any from unittest.mock import ANY, patch import pytest @@ -16,18 +17,18 @@ from tests.typing import ClientSessionGenerator @pytest.fixture -async def setup_scene(hass, scene_config): +async def setup_scene(hass: HomeAssistant, scene_config: dict[str, Any]) -> None: """Set up scene integration.""" assert await async_setup_component(hass, "scene", {"scene": scene_config}) await hass.async_block_till_done() @pytest.mark.parametrize("scene_config", [{}]) +@pytest.mark.usefixtures("setup_scene") async def test_create_scene( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_scene, + hass_config_store: dict[str, Any], ) -> None: """Test creating a scene.""" with patch.object(config, "SECTIONS", [scene]): @@ -70,11 +71,11 @@ async def test_create_scene( @pytest.mark.parametrize("scene_config", [{}]) +@pytest.mark.usefixtures("setup_scene") async def test_update_scene( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_scene, + hass_config_store: dict[str, Any], ) -> None: """Test updating a scene.""" with patch.object(config, "SECTIONS", [scene]): @@ -118,11 +119,11 @@ async def test_update_scene( @pytest.mark.parametrize("scene_config", [{}]) +@pytest.mark.usefixtures("setup_scene") async def test_bad_formatted_scene( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - setup_scene, + hass_config_store: dict[str, Any], ) -> None: """Test that we handle scene without ID.""" with patch.object(config, "SECTIONS", [scene]): @@ -184,12 +185,12 @@ async def test_bad_formatted_scene( ], ], ) +@pytest.mark.usefixtures("setup_scene") async def test_delete_scene( hass: HomeAssistant, hass_client: ClientSessionGenerator, entity_registry: er.EntityRegistry, - hass_config_store, - setup_scene, + hass_config_store: dict[str, Any], ) -> None: """Test deleting a scene.""" @@ -227,12 +228,12 @@ async def test_delete_scene( @pytest.mark.parametrize("scene_config", [{}]) +@pytest.mark.usefixtures("setup_scene") async def test_api_calls_require_admin( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_read_only_access_token: str, - hass_config_store, - setup_scene, + hass_config_store: dict[str, Any], ) -> None: """Test scene APIs endpoints do not work as a normal user.""" with patch.object(config, "SECTIONS", [scene]): diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 3ee45aec26a..4771576ed6e 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -31,7 +31,9 @@ async def setup_script(hass: HomeAssistant, script_config: dict[str, Any]) -> No @pytest.mark.parametrize("script_config", [{}]) async def test_get_script_config( - hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_config_store: dict[str, Any], ) -> None: """Test getting script config.""" with patch.object(config, "SECTIONS", [script]): @@ -54,7 +56,9 @@ async def test_get_script_config( @pytest.mark.parametrize("script_config", [{}]) async def test_update_script_config( - hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_config_store: dict[str, Any], ) -> None: """Test updating script config.""" with patch.object(config, "SECTIONS", [script]): @@ -90,7 +94,9 @@ async def test_update_script_config( @pytest.mark.parametrize("script_config", [{}]) async def test_invalid_object_id( - hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_config_store: dict[str, Any], ) -> None: """Test creating a script with an invalid object_id.""" with patch.object(config, "SECTIONS", [script]): @@ -152,7 +158,7 @@ async def test_invalid_object_id( async def test_update_script_config_with_error( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, + hass_config_store: dict[str, Any], caplog: pytest.LogCaptureFixture, updated_config: Any, validation_error: str, @@ -202,8 +208,7 @@ async def test_update_script_config_with_error( async def test_update_script_config_with_blueprint_substitution_error( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_config_store, - # setup_automation, + hass_config_store: dict[str, Any], caplog: pytest.LogCaptureFixture, updated_config: Any, validation_error: str, @@ -239,7 +244,9 @@ async def test_update_script_config_with_blueprint_substitution_error( @pytest.mark.parametrize("script_config", [{}]) async def test_update_remove_key_script_config( - hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_config_store: dict[str, Any], ) -> None: """Test updating script config while removing a key.""" with patch.object(config, "SECTIONS", [script]): @@ -286,7 +293,7 @@ async def test_delete_script( hass: HomeAssistant, hass_client: ClientSessionGenerator, entity_registry: er.EntityRegistry, - hass_config_store, + hass_config_store: dict[str, Any], ) -> None: """Test deleting a script.""" with patch.object(config, "SECTIONS", [script]): @@ -325,7 +332,7 @@ async def test_api_calls_require_admin( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_read_only_access_token: str, - hass_config_store, + hass_config_store: dict[str, Any], ) -> None: """Test script APIs endpoints do not work as a normal user.""" with patch.object(config, "SECTIONS", [script]): From dd379a9a0a21d9a850628ed349d12fe2a48d63a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 18:39:38 +0200 Subject: [PATCH 0075/2411] Bump aiozoneinfo to 0.2.1 (#120319) --- 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 438a73586c0..e8e0638beac 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,7 +7,7 @@ aiohttp-fast-url-dispatcher==0.3.0 aiohttp-fast-zlib==0.1.1 aiohttp==3.9.5 aiohttp_cors==0.7.0 -aiozoneinfo==0.2.0 +aiozoneinfo==0.2.1 astral==2.2 async-interrupt==1.1.1 async-upnp-client==0.39.0 diff --git a/pyproject.toml b/pyproject.toml index 527451eaf61..e6847385e0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "aiohttp_cors==0.7.0", "aiohttp-fast-url-dispatcher==0.3.0", "aiohttp-fast-zlib==0.1.1", - "aiozoneinfo==0.2.0", + "aiozoneinfo==0.2.1", "astral==2.2", "async-interrupt==1.1.1", "attrs==23.2.0", diff --git a/requirements.txt b/requirements.txt index 265d6231250..db1137875aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-fast-zlib==0.1.1 -aiozoneinfo==0.2.0 +aiozoneinfo==0.2.1 astral==2.2 async-interrupt==1.1.1 attrs==23.2.0 From 1e3ee8419f390c0d372b201362efb5bace98a690 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jun 2024 18:41:42 +0200 Subject: [PATCH 0076/2411] Bump async-interrupt to 1.1.2 (#120321) --- 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 e8e0638beac..1f1811eca4c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,7 +9,7 @@ aiohttp==3.9.5 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 -async-interrupt==1.1.1 +async-interrupt==1.1.2 async-upnp-client==0.39.0 atomicwrites-homeassistant==1.4.1 attrs==23.2.0 diff --git a/pyproject.toml b/pyproject.toml index e6847385e0a..d7fbe67edba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", "astral==2.2", - "async-interrupt==1.1.1", + "async-interrupt==1.1.2", "attrs==23.2.0", "atomicwrites-homeassistant==1.4.1", "awesomeversion==24.2.0", diff --git a/requirements.txt b/requirements.txt index db1137875aa..cff85c2478f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ aiohttp-fast-url-dispatcher==0.3.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 astral==2.2 -async-interrupt==1.1.1 +async-interrupt==1.1.2 attrs==23.2.0 atomicwrites-homeassistant==1.4.1 awesomeversion==24.2.0 From 641507a45ac77085402127e486ab8f44c672d896 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 24 Jun 2024 18:51:19 +0200 Subject: [PATCH 0077/2411] Add change username endpoint (#109057) --- homeassistant/auth/__init__.py | 7 + homeassistant/auth/auth_store.py | 8 + homeassistant/auth/providers/homeassistant.py | 92 +++++++++- homeassistant/components/auth/strings.json | 8 + .../config/auth_provider_homeassistant.py | 42 +++++ script/hassfest/translations.py | 14 +- tests/auth/providers/test_homeassistant.py | 147 +++++++++++---- .../test_auth_provider_homeassistant.py | 167 ++++++++++++++++++ 8 files changed, 440 insertions(+), 45 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index c39657b6147..8c991d3f227 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -374,6 +374,13 @@ class AuthManager: self.hass.bus.async_fire(EVENT_USER_UPDATED, {"user_id": user.id}) + @callback + def async_update_user_credentials_data( + self, credentials: models.Credentials, data: dict[str, Any] + ) -> None: + """Update credentials data.""" + self._store.async_update_user_credentials_data(credentials, data=data) + async def async_activate_user(self, user: models.User) -> None: """Activate a user.""" await self._store.async_activate_user(user) diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 3bf025c058c..7843cb58df2 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -296,6 +296,14 @@ class AuthStore: refresh_token.expire_at = None self._async_schedule_save() + @callback + def async_update_user_credentials_data( + self, credentials: models.Credentials, data: dict[str, Any] + ) -> None: + """Update credentials data.""" + credentials.data = data + self._async_schedule_save() + async def async_load(self) -> None: # noqa: C901 """Load the users.""" if self._loaded: diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index d277ce96fe2..1ed2f1dd3f7 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -55,6 +55,27 @@ class InvalidUser(HomeAssistantError): """ +class InvalidUsername(InvalidUser): + """Raised when invalid username is specified. + + Will not be raised when validating authentication. + """ + + def __init__( + self, + *args: object, + translation_key: str | None = None, + translation_placeholders: dict[str, str] | None = None, + ) -> None: + """Initialize exception.""" + super().__init__( + *args, + translation_domain="auth", + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + + class Data: """Hold the user data.""" @@ -71,9 +92,11 @@ class Data: self.is_legacy = False @callback - def normalize_username(self, username: str) -> str: + def normalize_username( + self, username: str, *, force_normalize: bool = False + ) -> str: """Normalize a username based on the mode.""" - if self.is_legacy: + if self.is_legacy and not force_normalize: return username return username.strip().casefold() @@ -162,13 +185,11 @@ class Data: return hashed def add_auth(self, username: str, password: str) -> None: - """Add a new authenticated user/pass.""" - username = self.normalize_username(username) + """Add a new authenticated user/pass. - if any( - self.normalize_username(user["username"]) == username for user in self.users - ): - raise InvalidUser + Raises InvalidUsername if the new username is invalid. + """ + self._validate_new_username(username) self.users.append( { @@ -207,6 +228,45 @@ class Data: else: raise InvalidUser + def _validate_new_username(self, new_username: str) -> None: + """Validate that username is normalized and unique. + + Raises InvalidUsername if the new username is invalid. + """ + normalized_username = self.normalize_username( + new_username, force_normalize=True + ) + if normalized_username != new_username: + raise InvalidUsername( + translation_key="username_not_normalized", + translation_placeholders={"new_username": new_username}, + ) + + if any( + self.normalize_username(user["username"]) == normalized_username + for user in self.users + ): + raise InvalidUsername( + translation_key="username_already_exists", + translation_placeholders={"username": new_username}, + ) + + def change_username(self, username: str, new_username: str) -> None: + """Update the username. + + Raises InvalidUser if user cannot be found. + Raises InvalidUsername if the new username is invalid. + """ + username = self.normalize_username(username) + self._validate_new_username(new_username) + + for user in self.users: + if self.normalize_username(user["username"]) == username: + user["username"] = new_username + break + else: + raise InvalidUser + async def async_save(self) -> None: """Save data.""" if self._data is not None: @@ -278,6 +338,22 @@ class HassAuthProvider(AuthProvider): ) await self.data.async_save() + async def async_change_username( + self, credential: Credentials, new_username: str + ) -> None: + """Validate new username and change it including updating credentials object.""" + if self.data is None: + await self.async_initialize() + assert self.data is not None + + await self.hass.async_add_executor_job( + self.data.change_username, credential.data["username"], new_username + ) + self.hass.auth.async_update_user_credentials_data( + credential, {**credential.data, "username": new_username} + ) + await self.data.async_save() + async def async_get_or_create_credentials( self, flow_result: Mapping[str, str] ) -> Credentials: diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index d386bb7a488..2b96b84c1cf 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -31,5 +31,13 @@ "invalid_code": "Invalid code, please try again." } } + }, + "exceptions": { + "username_already_exists": { + "message": "Username \"{username}\" already exists" + }, + "username_not_normalized": { + "message": "Username \"{new_username}\" is not normalized" + } } } diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 94c179e1a5f..1cfcda6d4b2 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -19,6 +19,7 @@ def async_setup(hass: HomeAssistant) -> bool: websocket_api.async_register_command(hass, websocket_delete) websocket_api.async_register_command(hass, websocket_change_password) websocket_api.async_register_command(hass, websocket_admin_change_password) + websocket_api.async_register_command(hass, websocket_admin_change_username) return True @@ -194,3 +195,44 @@ async def websocket_admin_change_password( msg["id"], "credentials_not_found", "Credentials not found" ) return + + +@websocket_api.websocket_command( + { + vol.Required( + "type" + ): "config/auth_provider/homeassistant/admin_change_username", + vol.Required("user_id"): str, + vol.Required("username"): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_admin_change_username( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Change the username for any user.""" + if not connection.user.is_owner: + raise Unauthorized(context=connection.context(msg)) + + if (user := await hass.auth.async_get_user(msg["user_id"])) is None: + connection.send_error(msg["id"], "user_not_found", "User not found") + return + + provider = auth_ha.async_get_provider(hass) + found_credential = None + for credential in user.credentials: + if credential.auth_provider_type == provider.type: + found_credential = credential + break + + if found_credential is None: + connection.send_error( + msg["id"], "credentials_not_found", "Credentials not found" + ) + return + + await provider.async_change_username(found_credential, msg["username"]) + connection.send_result(msg["id"]) diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 04ea85ca5d5..7ffb5861bb4 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -250,6 +250,14 @@ def gen_issues_schema(config: Config, integration: Integration) -> dict[str, Any } +_EXCEPTIONS_SCHEMA = { + vol.Optional("exceptions"): cv.schema_with_slug_keys( + {vol.Optional("message"): translation_value_validator}, + slug_validator=cv.slug, + ), +} + + def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: """Generate a strings schema.""" return vol.Schema( @@ -355,10 +363,7 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: ), slug_validator=cv.slug, ), - vol.Optional("exceptions"): cv.schema_with_slug_keys( - {vol.Optional("message"): translation_value_validator}, - slug_validator=cv.slug, - ), + **_EXCEPTIONS_SCHEMA, vol.Optional("services"): cv.schema_with_slug_keys( { vol.Required("name"): translation_value_validator, @@ -397,6 +402,7 @@ def gen_auth_schema(config: Config, integration: Integration) -> vol.Schema: ) }, vol.Optional("issues"): gen_issues_schema(config, integration), + **_EXCEPTIONS_SCHEMA, } ) diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index dc5c255579c..3224bf6b4f7 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -13,10 +13,11 @@ from homeassistant.auth.providers import ( homeassistant as hass_auth, ) from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component @pytest.fixture -def data(hass): +def data(hass: HomeAssistant) -> hass_auth.Data: """Create a loaded data class.""" data = hass_auth.Data(hass) hass.loop.run_until_complete(data.async_load()) @@ -24,7 +25,7 @@ def data(hass): @pytest.fixture -def legacy_data(hass): +def legacy_data(hass: HomeAssistant) -> hass_auth.Data: """Create a loaded legacy data class.""" data = hass_auth.Data(hass) hass.loop.run_until_complete(data.async_load()) @@ -32,7 +33,13 @@ def legacy_data(hass): return data -async def test_validating_password_invalid_user(data, hass: HomeAssistant) -> None: +@pytest.fixture +async def load_auth_component(hass: HomeAssistant) -> None: + """Load the auth component for translations.""" + await async_setup_component(hass, "auth", {}) + + +async def test_validating_password_invalid_user(data: hass_auth.Data) -> None: """Test validating an invalid user.""" with pytest.raises(hass_auth.InvalidAuth): data.validate_login("non-existing", "pw") @@ -48,7 +55,9 @@ async def test_not_allow_set_id() -> None: ) -async def test_new_users_populate_values(hass: HomeAssistant, data) -> None: +async def test_new_users_populate_values( + hass: HomeAssistant, data: hass_auth.Data +) -> None: """Test that we populate data for new users.""" data.add_auth("hello", "test-pass") await data.async_save() @@ -61,7 +70,7 @@ async def test_new_users_populate_values(hass: HomeAssistant, data) -> None: assert user.is_active -async def test_changing_password_raises_invalid_user(data, hass: HomeAssistant) -> None: +async def test_changing_password_raises_invalid_user(data: hass_auth.Data) -> None: """Test that changing password raises invalid user.""" with pytest.raises(hass_auth.InvalidUser): data.change_password("non-existing", "pw") @@ -70,20 +79,34 @@ async def test_changing_password_raises_invalid_user(data, hass: HomeAssistant) # Modern mode -async def test_adding_user(data, hass: HomeAssistant) -> None: +async def test_adding_user(data: hass_auth.Data) -> None: """Test adding a user.""" data.add_auth("test-user", "test-pass") data.validate_login(" test-user ", "test-pass") -async def test_adding_user_duplicate_username(data, hass: HomeAssistant) -> None: +@pytest.mark.parametrize("username", ["test-user ", "TEST-USER"]) +@pytest.mark.usefixtures("load_auth_component") +def test_adding_user_not_normalized(data: hass_auth.Data, username: str) -> None: + """Test adding a user.""" + with pytest.raises( + hass_auth.InvalidUsername, match=f'Username "{username}" is not normalized' + ): + data.add_auth(username, "test-pass") + + +@pytest.mark.usefixtures("load_auth_component") +def test_adding_user_duplicate_username(data: hass_auth.Data) -> None: """Test adding a user with duplicate username.""" data.add_auth("test-user", "test-pass") - with pytest.raises(hass_auth.InvalidUser): - data.add_auth("TEST-user ", "other-pass") + + with pytest.raises( + hass_auth.InvalidUsername, match='Username "test-user" already exists' + ): + data.add_auth("test-user", "other-pass") -async def test_validating_password_invalid_password(data, hass: HomeAssistant) -> None: +async def test_validating_password_invalid_password(data: hass_auth.Data) -> None: """Test validating an invalid password.""" data.add_auth("test-user", "test-pass") @@ -97,7 +120,7 @@ async def test_validating_password_invalid_password(data, hass: HomeAssistant) - data.validate_login("test-user", "Test-pass") -async def test_changing_password(data, hass: HomeAssistant) -> None: +async def test_changing_password(data: hass_auth.Data) -> None: """Test adding a user.""" data.add_auth("test-user", "test-pass") data.change_password("TEST-USER ", "new-pass") @@ -108,7 +131,7 @@ async def test_changing_password(data, hass: HomeAssistant) -> None: data.validate_login("test-UsEr", "new-pass") -async def test_login_flow_validates(data, hass: HomeAssistant) -> None: +async def test_login_flow_validates(data: hass_auth.Data, hass: HomeAssistant) -> None: """Test login flow.""" data.add_auth("test-user", "test-pass") await data.async_save() @@ -139,7 +162,7 @@ async def test_login_flow_validates(data, hass: HomeAssistant) -> None: assert result["data"]["username"] == "test-USER" -async def test_saving_loading(data, hass: HomeAssistant) -> None: +async def test_saving_loading(data: hass_auth.Data, hass: HomeAssistant) -> None: """Test saving and loading JSON.""" data.add_auth("test-user", "test-pass") data.add_auth("second-user", "second-pass") @@ -151,7 +174,9 @@ async def test_saving_loading(data, hass: HomeAssistant) -> None: data.validate_login("second-user ", "second-pass") -async def test_get_or_create_credentials(hass: HomeAssistant, data) -> None: +async def test_get_or_create_credentials( + hass: HomeAssistant, data: hass_auth.Data +) -> None: """Test that we can get or create credentials.""" manager = await auth_manager_from_config(hass, [{"type": "homeassistant"}], []) provider = manager.auth_providers[0] @@ -167,26 +192,14 @@ async def test_get_or_create_credentials(hass: HomeAssistant, data) -> None: # Legacy mode -async def test_legacy_adding_user(legacy_data, hass: HomeAssistant) -> None: +async def test_legacy_adding_user(legacy_data: hass_auth.Data) -> None: """Test in legacy mode adding a user.""" legacy_data.add_auth("test-user", "test-pass") legacy_data.validate_login("test-user", "test-pass") -async def test_legacy_adding_user_duplicate_username( - legacy_data, hass: HomeAssistant -) -> None: - """Test in legacy mode adding a user with duplicate username.""" - legacy_data.add_auth("test-user", "test-pass") - with pytest.raises(hass_auth.InvalidUser): - legacy_data.add_auth("test-user", "other-pass") - # Not considered duplicate - legacy_data.add_auth("test-user ", "test-pass") - legacy_data.add_auth("Test-user", "test-pass") - - async def test_legacy_validating_password_invalid_password( - legacy_data, hass: HomeAssistant + legacy_data: hass_auth.Data, ) -> None: """Test in legacy mode validating an invalid password.""" legacy_data.add_auth("test-user", "test-pass") @@ -195,7 +208,7 @@ async def test_legacy_validating_password_invalid_password( legacy_data.validate_login("test-user", "invalid-pass") -async def test_legacy_changing_password(legacy_data, hass: HomeAssistant) -> None: +async def test_legacy_changing_password(legacy_data: hass_auth.Data) -> None: """Test in legacy mode adding a user.""" user = "test-user" legacy_data.add_auth(user, "test-pass") @@ -208,14 +221,16 @@ async def test_legacy_changing_password(legacy_data, hass: HomeAssistant) -> Non async def test_legacy_changing_password_raises_invalid_user( - legacy_data, hass: HomeAssistant + legacy_data: hass_auth.Data, ) -> None: """Test in legacy mode that we initialize an empty config.""" with pytest.raises(hass_auth.InvalidUser): legacy_data.change_password("non-existing", "pw") -async def test_legacy_login_flow_validates(legacy_data, hass: HomeAssistant) -> None: +async def test_legacy_login_flow_validates( + legacy_data: hass_auth.Data, hass: HomeAssistant +) -> None: """Test in legacy mode login flow.""" legacy_data.add_auth("test-user", "test-pass") await legacy_data.async_save() @@ -246,7 +261,9 @@ async def test_legacy_login_flow_validates(legacy_data, hass: HomeAssistant) -> assert result["data"]["username"] == "test-user" -async def test_legacy_saving_loading(legacy_data, hass: HomeAssistant) -> None: +async def test_legacy_saving_loading( + legacy_data: hass_auth.Data, hass: HomeAssistant +) -> None: """Test in legacy mode saving and loading JSON.""" legacy_data.add_auth("test-user", "test-pass") legacy_data.add_auth("second-user", "second-pass") @@ -263,7 +280,7 @@ async def test_legacy_saving_loading(legacy_data, hass: HomeAssistant) -> None: async def test_legacy_get_or_create_credentials( - hass: HomeAssistant, legacy_data + hass: HomeAssistant, legacy_data: hass_auth.Data ) -> None: """Test in legacy mode that we can get or create credentials.""" manager = await auth_manager_from_config(hass, [{"type": "homeassistant"}], []) @@ -308,3 +325,67 @@ async def test_race_condition_in_data_loading(hass: HomeAssistant) -> None: assert isinstance(results[0], hass_auth.InvalidAuth) # results[1] will be a TypeError if race condition occurred assert isinstance(results[1], hass_auth.InvalidAuth) + + +def test_change_username(data: hass_auth.Data) -> None: + """Test changing username.""" + data.add_auth("test-user", "test-pass") + users = data.users + assert len(users) == 1 + assert users[0]["username"] == "test-user" + + data.change_username("test-user", "new-user") + + users = data.users + assert len(users) == 1 + assert users[0]["username"] == "new-user" + + +@pytest.mark.parametrize("username", ["test-user ", "TEST-USER"]) +def test_change_username_legacy(legacy_data: hass_auth.Data, username: str) -> None: + """Test changing username.""" + # Cannot use add_auth as it normalizes username + legacy_data.users.append( + { + "username": username, + "password": legacy_data.hash_password("test-pass", True).decode(), + } + ) + + users = legacy_data.users + assert len(users) == 1 + assert users[0]["username"] == username + + legacy_data.change_username(username, "test-user") + + users = legacy_data.users + assert len(users) == 1 + assert users[0]["username"] == "test-user" + + +def test_change_username_invalid_user(data: hass_auth.Data) -> None: + """Test changing username raises on invalid user.""" + data.add_auth("test-user", "test-pass") + users = data.users + assert len(users) == 1 + assert users[0]["username"] == "test-user" + + with pytest.raises(hass_auth.InvalidUser): + data.change_username("non-existing", "new-user") + + users = data.users + assert len(users) == 1 + assert users[0]["username"] == "test-user" + + +@pytest.mark.usefixtures("load_auth_component") +async def test_change_username_not_normalized( + data: hass_auth.Data, hass: HomeAssistant +) -> None: + """Test changing username raises on not normalized username.""" + data.add_auth("test-user", "test-pass") + + with pytest.raises( + hass_auth.InvalidUsername, match='Username "TEST-user " is not normalized' + ): + data.change_username("test-user", "TEST-user ") diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index 044d6cdb571..ffee88f91ec 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -445,3 +445,170 @@ async def test_admin_change_password( assert result["success"], result await auth_provider.async_validate_login("test-user", "new-pass") + + +def _assert_username( + local_auth: prov_ha.HassAuthProvider, username: str, *, should_exist: bool +) -> None: + if any(user["username"] == username for user in local_auth.data.users): + if should_exist: + return # found + + pytest.fail(f"Found user with username {username} when not expected") + + if should_exist: + pytest.fail(f"Did not find user with username {username}") + + +async def _test_admin_change_username( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + local_auth: prov_ha.HassAuthProvider, + hass_admin_user: MockUser, + owner_access_token: str, + new_username: str, +) -> dict[str, Any]: + """Test admin change username ws endpoint.""" + client = await hass_ws_client(hass, owner_access_token) + current_username_user = hass_admin_user.credentials[0].data["username"] + _assert_username(local_auth, current_username_user, should_exist=True) + + await client.send_json_auto_id( + { + "type": "config/auth_provider/homeassistant/admin_change_username", + "user_id": hass_admin_user.id, + "username": new_username, + } + ) + return await client.receive_json() + + +async def test_admin_change_username_success( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + local_auth: prov_ha.HassAuthProvider, + hass_admin_user: MockUser, + owner_access_token: str, +) -> None: + """Test that change username succeeds.""" + current_username = hass_admin_user.credentials[0].data["username"] + new_username = "blabla" + + result = await _test_admin_change_username( + hass, + hass_ws_client, + local_auth, + hass_admin_user, + owner_access_token, + new_username, + ) + + assert result["success"], result + _assert_username(local_auth, current_username, should_exist=False) + _assert_username(local_auth, new_username, should_exist=True) + assert hass_admin_user.credentials[0].data["username"] == new_username + # Validate new login works + await local_auth.async_validate_login(new_username, "test-pass") + with pytest.raises(prov_ha.InvalidAuth): + # Verify old login does not work + await local_auth.async_validate_login(current_username, "test-pass") + + +@pytest.mark.parametrize("new_username", [" bla", "bla ", "BlA"]) +async def test_admin_change_username_error_not_normalized( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + local_auth: prov_ha.HassAuthProvider, + hass_admin_user: MockUser, + owner_access_token: str, + new_username: str, +) -> None: + """Test that change username raises error.""" + current_username = hass_admin_user.credentials[0].data["username"] + + result = await _test_admin_change_username( + hass, + hass_ws_client, + local_auth, + hass_admin_user, + owner_access_token, + new_username, + ) + assert not result["success"], result + assert result["error"] == { + "code": "home_assistant_error", + "message": "username_not_normalized", + "translation_key": "username_not_normalized", + "translation_placeholders": {"new_username": new_username}, + "translation_domain": "auth", + } + _assert_username(local_auth, current_username, should_exist=True) + _assert_username(local_auth, new_username, should_exist=False) + assert hass_admin_user.credentials[0].data["username"] == current_username + # Validate old login still works + await local_auth.async_validate_login(current_username, "test-pass") + + +async def test_admin_change_username_not_owner( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, auth_provider +) -> None: + """Test that change username fails when not owner.""" + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "config/auth_provider/homeassistant/admin_change_username", + "user_id": "test-user", + "username": "new-user", + } + ) + + result = await client.receive_json() + assert not result["success"], result + assert result["error"]["code"] == "unauthorized" + + # Validate old login still works + await auth_provider.async_validate_login("test-user", "test-pass") + + +async def test_admin_change_username_no_user( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, owner_access_token +) -> None: + """Test that change username fails with unknown user.""" + client = await hass_ws_client(hass, owner_access_token) + + await client.send_json_auto_id( + { + "type": "config/auth_provider/homeassistant/admin_change_username", + "user_id": "non-existing", + "username": "new-username", + } + ) + + result = await client.receive_json() + assert not result["success"], result + assert result["error"]["code"] == "user_not_found" + + +async def test_admin_change_username_no_cred( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + owner_access_token, + hass_admin_user: MockUser, +) -> None: + """Test that change username fails with unknown credential.""" + + hass_admin_user.credentials.clear() + client = await hass_ws_client(hass, owner_access_token) + + await client.send_json_auto_id( + { + "type": "config/auth_provider/homeassistant/admin_change_username", + "user_id": hass_admin_user.id, + "username": "new-username", + } + ) + + result = await client.receive_json() + assert not result["success"], result + assert result["error"]["code"] == "credentials_not_found" From a4e22bcba690ae638d224e48806baad9fb34ecc4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:52:49 +0200 Subject: [PATCH 0078/2411] Update tenacity constraint (#120348) --- homeassistant/package_constraints.txt | 2 +- script/gen_requirements_all.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1f1811eca4c..f3be7c5515e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -199,4 +199,4 @@ scapy>=2.5.0 tuf>=4.0.0 # https://github.com/jd/tenacity/issues/471 -tenacity<8.4.0 +tenacity!=8.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 57b4a2e1855..434b4d0071f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -221,7 +221,7 @@ scapy>=2.5.0 tuf>=4.0.0 # https://github.com/jd/tenacity/issues/471 -tenacity<8.4.0 +tenacity!=8.4.0 """ GENERATED_MESSAGE = ( From 31157828e1e58ca47439cd64147abf2af289d924 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:55:36 +0200 Subject: [PATCH 0079/2411] Improve type hints in cloudflare tests (#120344) --- tests/components/cloudflare/__init__.py | 23 ++++++++----------- tests/components/cloudflare/conftest.py | 15 ++++++------ .../components/cloudflare/test_config_flow.py | 22 +++++++++++------- tests/components/cloudflare/test_init.py | 18 ++++++++------- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/tests/components/cloudflare/__init__.py b/tests/components/cloudflare/__init__.py index 5e1529a9da8..9827355c9cc 100644 --- a/tests/components/cloudflare/__init__.py +++ b/tests/components/cloudflare/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch import pycfdns @@ -80,25 +80,20 @@ async def init_integration( return entry -def _get_mock_client( - zone: pycfdns.ZoneModel | UndefinedType = UNDEFINED, - records: list[pycfdns.RecordModel] | UndefinedType = UNDEFINED, -): - client: pycfdns.Client = AsyncMock() +def get_mock_client() -> Mock: + """Return of Mock of pycfdns.Client.""" + client = Mock() - client.list_zones = AsyncMock( - return_value=[MOCK_ZONE if zone is UNDEFINED else zone] - ) - client.list_dns_records = AsyncMock( - return_value=MOCK_ZONE_RECORDS if records is UNDEFINED else records - ) + client.list_zones = AsyncMock(return_value=[MOCK_ZONE]) + client.list_dns_records = AsyncMock(return_value=MOCK_ZONE_RECORDS) client.update_dns_record = AsyncMock(return_value=None) return client -def _patch_async_setup_entry(return_value=True): +def patch_async_setup_entry() -> AsyncMock: + """Patch the async_setup_entry method and return a mock.""" return patch( "homeassistant.components.cloudflare.async_setup_entry", - return_value=return_value, + return_value=True, ) diff --git a/tests/components/cloudflare/conftest.py b/tests/components/cloudflare/conftest.py index 81b52dd291d..6c41e9fd179 100644 --- a/tests/components/cloudflare/conftest.py +++ b/tests/components/cloudflare/conftest.py @@ -1,16 +1,17 @@ """Define fixtures available for all tests.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Generator -from . import _get_mock_client +from . import get_mock_client @pytest.fixture -def cfupdate(hass): +def cfupdate() -> Generator[MagicMock]: """Mock the CloudflareUpdater for easier testing.""" - mock_cfupdate = _get_mock_client() + mock_cfupdate = get_mock_client() with patch( "homeassistant.components.cloudflare.pycfdns.Client", return_value=mock_cfupdate, @@ -19,11 +20,11 @@ def cfupdate(hass): @pytest.fixture -def cfupdate_flow(hass): +def cfupdate_flow() -> Generator[MagicMock]: """Mock the CloudflareUpdater for easier config flow testing.""" - mock_cfupdate = _get_mock_client() + mock_cfupdate = get_mock_client() with patch( - "homeassistant.components.cloudflare.pycfdns.Client", + "homeassistant.components.cloudflare.config_flow.pycfdns.Client", return_value=mock_cfupdate, ) as mock_api: yield mock_api diff --git a/tests/components/cloudflare/test_config_flow.py b/tests/components/cloudflare/test_config_flow.py index 4b0df91bc60..1278113c0c7 100644 --- a/tests/components/cloudflare/test_config_flow.py +++ b/tests/components/cloudflare/test_config_flow.py @@ -1,5 +1,7 @@ """Test the Cloudflare config flow.""" +from unittest.mock import MagicMock + import pycfdns from homeassistant.components.cloudflare.const import CONF_RECORDS, DOMAIN @@ -13,13 +15,13 @@ from . import ( USER_INPUT, USER_INPUT_RECORDS, USER_INPUT_ZONE, - _patch_async_setup_entry, + patch_async_setup_entry, ) from tests.common import MockConfigEntry -async def test_user_form(hass: HomeAssistant, cfupdate_flow) -> None: +async def test_user_form(hass: HomeAssistant, cfupdate_flow: MagicMock) -> None: """Test we get the user initiated form.""" result = await hass.config_entries.flow.async_init( @@ -49,7 +51,7 @@ async def test_user_form(hass: HomeAssistant, cfupdate_flow) -> None: assert result["step_id"] == "records" assert result["errors"] is None - with _patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT_RECORDS, @@ -70,7 +72,9 @@ async def test_user_form(hass: HomeAssistant, cfupdate_flow) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_form_cannot_connect(hass: HomeAssistant, cfupdate_flow) -> None: +async def test_user_form_cannot_connect( + hass: HomeAssistant, cfupdate_flow: MagicMock +) -> None: """Test we handle cannot connect error.""" instance = cfupdate_flow.return_value @@ -88,7 +92,9 @@ async def test_user_form_cannot_connect(hass: HomeAssistant, cfupdate_flow) -> N assert result["errors"] == {"base": "cannot_connect"} -async def test_user_form_invalid_auth(hass: HomeAssistant, cfupdate_flow) -> None: +async def test_user_form_invalid_auth( + hass: HomeAssistant, cfupdate_flow: MagicMock +) -> None: """Test we handle invalid auth error.""" instance = cfupdate_flow.return_value @@ -107,7 +113,7 @@ async def test_user_form_invalid_auth(hass: HomeAssistant, cfupdate_flow) -> Non async def test_user_form_unexpected_exception( - hass: HomeAssistant, cfupdate_flow + hass: HomeAssistant, cfupdate_flow: MagicMock ) -> None: """Test we handle unexpected exception.""" instance = cfupdate_flow.return_value @@ -140,7 +146,7 @@ async def test_user_form_single_instance_allowed(hass: HomeAssistant) -> None: assert result["reason"] == "single_instance_allowed" -async def test_reauth_flow(hass: HomeAssistant, cfupdate_flow) -> None: +async def test_reauth_flow(hass: HomeAssistant, cfupdate_flow: MagicMock) -> None: """Test the reauthentication configuration flow.""" entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG) entry.add_to_hass(hass) @@ -157,7 +163,7 @@ async def test_reauth_flow(hass: HomeAssistant, cfupdate_flow) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with _patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_TOKEN: "other_token"}, diff --git a/tests/components/cloudflare/test_init.py b/tests/components/cloudflare/test_init.py index 3b2a6803566..d629607e503 100644 --- a/tests/components/cloudflare/test_init.py +++ b/tests/components/cloudflare/test_init.py @@ -1,7 +1,7 @@ """Test the Cloudflare integration.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pycfdns import pytest @@ -23,7 +23,7 @@ from . import ENTRY_CONFIG, init_integration from tests.common import MockConfigEntry, async_fire_time_changed -async def test_unload_entry(hass: HomeAssistant, cfupdate) -> None: +async def test_unload_entry(hass: HomeAssistant, cfupdate: MagicMock) -> None: """Test successful unload of entry.""" entry = await init_integration(hass) @@ -42,7 +42,7 @@ async def test_unload_entry(hass: HomeAssistant, cfupdate) -> None: [pycfdns.ComunicationException()], ) async def test_async_setup_raises_entry_not_ready( - hass: HomeAssistant, cfupdate, side_effect + hass: HomeAssistant, cfupdate: MagicMock, side_effect: Exception ) -> None: """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" instance = cfupdate.return_value @@ -57,7 +57,7 @@ async def test_async_setup_raises_entry_not_ready( async def test_async_setup_raises_entry_auth_failed( - hass: HomeAssistant, cfupdate + hass: HomeAssistant, cfupdate: MagicMock ) -> None: """Test that it throws ConfigEntryAuthFailed when exception occurs during setup.""" instance = cfupdate.return_value @@ -84,7 +84,7 @@ async def test_async_setup_raises_entry_auth_failed( async def test_integration_services( - hass: HomeAssistant, cfupdate, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, cfupdate: MagicMock, caplog: pytest.LogCaptureFixture ) -> None: """Test integration services.""" instance = cfupdate.return_value @@ -120,7 +120,9 @@ async def test_integration_services( assert "All target records are up to date" not in caplog.text -async def test_integration_services_with_issue(hass: HomeAssistant, cfupdate) -> None: +async def test_integration_services_with_issue( + hass: HomeAssistant, cfupdate: MagicMock +) -> None: """Test integration services with issue.""" instance = cfupdate.return_value @@ -145,7 +147,7 @@ async def test_integration_services_with_issue(hass: HomeAssistant, cfupdate) -> async def test_integration_services_with_nonexisting_record( - hass: HomeAssistant, cfupdate, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, cfupdate: MagicMock, caplog: pytest.LogCaptureFixture ) -> None: """Test integration services.""" instance = cfupdate.return_value @@ -185,7 +187,7 @@ async def test_integration_services_with_nonexisting_record( async def test_integration_update_interval( hass: HomeAssistant, - cfupdate, + cfupdate: MagicMock, caplog: pytest.LogCaptureFixture, ) -> None: """Test integration update interval.""" From 1e5f4c2d754bee1caaae073bca803bba46bfe73c Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Mon, 24 Jun 2024 18:56:33 +0200 Subject: [PATCH 0080/2411] Add additional sensors to pyLoad integration (#120309) --- homeassistant/components/pyload/icons.json | 9 + homeassistant/components/pyload/sensor.py | 25 + homeassistant/components/pyload/strings.json | 12 + .../pyload/snapshots/test_sensor.ambr | 952 ++++++++++++++++++ 4 files changed, 998 insertions(+) diff --git a/homeassistant/components/pyload/icons.json b/homeassistant/components/pyload/icons.json index b3b7d148b1a..bc068165851 100644 --- a/homeassistant/components/pyload/icons.json +++ b/homeassistant/components/pyload/icons.json @@ -3,6 +3,15 @@ "sensor": { "speed": { "default": "mdi:speedometer" + }, + "active": { + "default": "mdi:cloud-download" + }, + "queue": { + "default": "mdi:cloud-clock" + }, + "total": { + "default": "mdi:cloud-alert" } } } diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 7caef84d2dc..c4fea3e43bb 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, UnitOfDataRate, + UnitOfInformation, ) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -40,7 +41,11 @@ from .coordinator import PyLoadCoordinator class PyLoadSensorEntity(StrEnum): """pyLoad Sensor Entities.""" + ACTIVE = "active" + FREE_SPACE = "free_space" + QUEUE = "queue" SPEED = "speed" + TOTAL = "total" SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( @@ -52,6 +57,26 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, suggested_display_precision=1, ), + SensorEntityDescription( + key=PyLoadSensorEntity.ACTIVE, + translation_key=PyLoadSensorEntity.ACTIVE, + ), + SensorEntityDescription( + key=PyLoadSensorEntity.QUEUE, + translation_key=PyLoadSensorEntity.QUEUE, + ), + SensorEntityDescription( + key=PyLoadSensorEntity.TOTAL, + translation_key=PyLoadSensorEntity.TOTAL, + ), + SensorEntityDescription( + key=PyLoadSensorEntity.FREE_SPACE, + translation_key=PyLoadSensorEntity.FREE_SPACE, + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, + suggested_display_precision=1, + ), ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index a8544bf48eb..cc53ef7465b 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -31,6 +31,18 @@ "sensor": { "speed": { "name": "Speed" + }, + "active": { + "name": "Active downloads" + }, + "queue": { + "name": "Downloads in queue" + }, + "total": { + "name": "Total downlods" + }, + "free_space": { + "name": "Free space" } } }, diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index a6049577f47..8675fb696a5 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -1,4 +1,196 @@ # serializer version: 1 +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_active_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_active_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Active downloads', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_active', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_active_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Active downloads', + }), + 'context': , + 'entity_id': 'sensor.pyload_active_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_in_queue-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads in queue', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_queue', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_in_queue-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads in queue', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads total', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads total', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_free_space-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_free_space', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Free space', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_free_space', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_free_space-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'pyLoad Free space', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_free_space', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -53,6 +245,244 @@ 'state': 'unavailable', }) # --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_total_downlods-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_total_downlods', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total downlods', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_total_downlods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Total downlods', + }), + 'context': , + 'entity_id': 'sensor.pyload_total_downlods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_active_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_active_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Active downloads', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_active', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_active_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Active downloads', + }), + 'context': , + 'entity_id': 'sensor.pyload_active_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_in_queue-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads in queue', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_queue', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_in_queue-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads in queue', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads total', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads total', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_free_space-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_free_space', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Free space', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_free_space', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_free_space-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'pyLoad Free space', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_free_space', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -107,6 +537,244 @@ 'state': 'unavailable', }) # --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_total_downlods-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_total_downlods', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total downlods', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_total_downlods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Total downlods', + }), + 'context': , + 'entity_id': 'sensor.pyload_total_downlods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_active_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_active_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Active downloads', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_active', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_active_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Active downloads', + }), + 'context': , + 'entity_id': 'sensor.pyload_active_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_in_queue-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads in queue', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_queue', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_in_queue-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads in queue', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads total', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads total', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_free_space-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_free_space', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Free space', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_free_space', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_free_space-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'pyLoad Free space', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_free_space', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_sensor_update_exceptions[ParserError][sensor.pyload_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -161,6 +829,244 @@ 'state': 'unavailable', }) # --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_total_downlods-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_total_downlods', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total downlods', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_total_downlods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Total downlods', + }), + 'context': , + 'entity_id': 'sensor.pyload_total_downlods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_setup[sensor.pyload_active_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_active_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Active downloads', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_active', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.pyload_active_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Active downloads', + }), + 'context': , + 'entity_id': 'sensor.pyload_active_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_setup[sensor.pyload_downloads_in_queue-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads in queue', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_queue', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.pyload_downloads_in_queue-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads in queue', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_in_queue', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6', + }) +# --- +# name: test_setup[sensor.pyload_downloads_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_downloads_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Downloads total', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.pyload_downloads_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Downloads total', + }), + 'context': , + 'entity_id': 'sensor.pyload_downloads_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '37', + }) +# --- +# name: test_setup[sensor.pyload_free_space-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_free_space', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Free space', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_free_space', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[sensor.pyload_free_space-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'pyLoad Free space', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pyload_free_space', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '93.1322574606165', + }) +# --- # name: test_setup[sensor.pyload_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -215,3 +1121,49 @@ 'state': '43.247704', }) # --- +# name: test_setup[sensor.pyload_total_downlods-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_total_downlods', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total downlods', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.pyload_total_downlods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Total downlods', + }), + 'context': , + 'entity_id': 'sensor.pyload_total_downlods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '37', + }) +# --- From a7200a70b2968b5938d97c1ab8539683405e36fe Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 24 Jun 2024 19:42:32 +0200 Subject: [PATCH 0081/2411] Set up mqtt tests from client fixture of mqtt entry setup fixture, not both (#120274) * Fix entry setup and cleanup issues in mqtt tests * Reduce changes by using mqtt_client_mock alias * Reduce sleep time where possibe --- tests/components/mqtt/conftest.py | 42 +- tests/components/mqtt/test_discovery.py | 4 +- tests/components/mqtt/test_init.py | 741 ++++++++++-------------- 3 files changed, 339 insertions(+), 448 deletions(-) diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index 9649e0b9ddf..39b9f122f75 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -1,12 +1,20 @@ """Test fixtures for mqtt component.""" +import asyncio from random import getrandbits +from typing import Any from unittest.mock import patch import pytest -from typing_extensions import Generator +from typing_extensions import AsyncGenerator, Generator from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant, callback + +from tests.common import MockConfigEntry +from tests.typing import MqttMockPahoClient ENTRY_DEFAULT_BIRTH_MESSAGE = { mqtt.CONF_BROKER: "mock-broker", @@ -39,3 +47,35 @@ def mock_temp_dir(temp_dir_prefix: str) -> Generator[str]: f"home-assistant-mqtt-{temp_dir_prefix}-{getrandbits(10):03x}", ) as mocked_temp_dir: yield mocked_temp_dir + + +@pytest.fixture +async def setup_with_birth_msg_client_mock( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any] | None, + mqtt_client_mock: MqttMockPahoClient, +) -> AsyncGenerator[MqttMockPahoClient]: + """Test sending birth message.""" + birth = asyncio.Event() + with ( + patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0), + patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0), + patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0), + ): + entry = MockConfigEntry( + domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} + ) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + + @callback + def wait_birth(msg: ReceiveMessage) -> None: + """Handle birth message.""" + birth.set() + + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + await hass.async_block_till_done() + await birth.wait() + yield mqtt_client_mock diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index e36971e386f..b9ef1a3c210 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1531,7 +1531,7 @@ async def test_mqtt_discovery_unsubscribe_once( async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: """Test mqtt step.""" - await asyncio.sleep(0.1) + await asyncio.sleep(0) return self.async_abort(reason="already_configured") mock_platform(hass, "comp.config_flow", None) @@ -1573,7 +1573,7 @@ async def test_mqtt_discovery_unsubscribe_once( async_fire_mqtt_message(hass, "comp/discovery/bla/config", "") async_fire_mqtt_message(hass, "comp/discovery/bla/config", "") await wait_unsub.wait() - await asyncio.sleep(0.2) + await asyncio.sleep(0) await hass.async_block_till_done(wait_background_tasks=True) mqtt_client_mock.unsubscribe.assert_called_once_with(["comp/discovery/#"]) await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 264f80f48f8..8a76c71f1f3 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -56,6 +56,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from homeassistant.util.dt import utcnow +from .conftest import ENTRY_DEFAULT_BIRTH_MESSAGE from .test_common import help_all_subscribe_calls from tests.common import ( @@ -149,21 +150,19 @@ def help_assert_message( async def test_mqtt_connects_on_home_assistant_mqtt_setup( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test if client is connected after mqtt init on bootstrap.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock assert mqtt_client_mock.connect.call_count == 1 async def test_mqtt_does_not_disconnect_on_home_assistant_stop( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test if client is not disconnected on HA stop.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock hass.bus.fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_block_till_done() @@ -226,12 +225,13 @@ async def test_mqtt_await_ack_at_disconnect( await hass.async_block_till_done(wait_background_tasks=True) +@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) async def test_publish( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test the publish function.""" - mqtt_mock = await mqtt_mock_entry() - publish_mock: MagicMock = mqtt_mock._mqttc.publish + publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish await mqtt.async_publish(hass, "test-topic", "test-payload") await hass.async_block_till_done() assert publish_mock.called @@ -292,7 +292,7 @@ async def test_publish( 0, False, ) - mqtt_mock.reset_mock() + publish_mock.reset_mock() # test null payload mqtt.publish( @@ -1100,42 +1100,40 @@ async def test_subscribe_mqtt_config_entry_disabled( await mqtt.async_subscribe(hass, "test-topic", record_calls) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.2) async def test_subscribe_and_resubscribe( hass: HomeAssistant, client_debug_log: None, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, ) -> None: """Test resubscribing within the debounce time.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock + with ( + patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.4), + patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.4), + ): + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + # This unsub will be un-done with the following subscribe + # unsubscribe should not be called at the broker + unsub() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - # This unsub will be un-done with the following subscribe - # unsubscribe should not be called at the broker - unsub() - await asyncio.sleep(0.1) - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - await asyncio.sleep(0.1) - await hass.async_block_till_done() + async_fire_mqtt_message(hass, "test-topic", "test-payload") + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) - async_fire_mqtt_message(hass, "test-topic", "test-payload") - await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + # assert unsubscribe was not called + mqtt_client_mock.unsubscribe.assert_not_called() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - # assert unsubscribe was not called - mqtt_client_mock.unsubscribe.assert_not_called() + unsub() - unsub() - - await asyncio.sleep(0.2) - await hass.async_block_till_done() - mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) async def test_subscribe_topic_non_async( @@ -1442,25 +1440,26 @@ async def test_subscribe_special_characters( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_subscribe_same_topic( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test subscribing to same topic twice and simulate retained messages. When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again for it to resend any retained messages. """ - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + @callback def _callback_b(msg: ReceiveMessage) -> None: calls_b.append(msg) + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/state", _callback_a, qos=0) # Simulate a non retained message after the first subscription async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) @@ -1486,13 +1485,9 @@ async def test_subscribe_same_topic( mqtt_client_mock.subscribe.assert_called() -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_replaying_payload_same_topic( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying retained messages. @@ -1501,22 +1496,26 @@ async def test_replaying_payload_same_topic( Retained messages must only be replayed for new subscriptions, except when the MQTT client is reconnecting. """ - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + @callback def _callback_b(msg: ReceiveMessage) -> None: calls_b.append(msg) + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/state", _callback_a) + await hass.async_block_till_done() async_fire_mqtt_message( hass, "test/state", "online", qos=0, retain=True ) # Simulate a (retained) message played back - await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await asyncio.sleep(0) await hass.async_block_till_done() assert len(calls_a) == 1 @@ -1538,6 +1537,7 @@ async def test_replaying_payload_same_topic( # Make sure the debouncer delay was passed await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await asyncio.sleep(0) await hass.async_block_till_done() # The current subscription only received the message without retain flag @@ -1562,6 +1562,7 @@ async def test_replaying_payload_same_topic( async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await asyncio.sleep(0) await hass.async_block_till_done() assert len(calls_a) == 1 assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) @@ -1576,13 +1577,15 @@ async def test_replaying_payload_same_topic( mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await asyncio.sleep(0) await hass.async_block_till_done() mqtt_client_mock.subscribe.assert_called() # Simulate a (retained) message played back after reconnecting async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) # Both subscriptions now should replay the retained message assert len(calls_a) == 1 assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) @@ -1595,8 +1598,7 @@ async def test_replaying_payload_same_topic( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_replaying_payload_after_resubscribing( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying and filtering retained messages after resubscribing. @@ -1605,13 +1607,14 @@ async def test_replaying_payload_after_resubscribing( Retained messages must only be replayed for new subscriptions, except when the MQTT client is reconnection. """ - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + mqtt_client_mock.reset_mock() unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) @@ -1655,8 +1658,7 @@ async def test_replaying_payload_after_resubscribing( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_replaying_payload_wildcard_topic( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying retained messages. @@ -1666,23 +1668,26 @@ async def test_replaying_payload_wildcard_topic( Retained messages should only be replayed for new subscriptions, except when the MQTT client is reconnection. """ - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + @callback def _callback_b(msg: ReceiveMessage) -> None: calls_b.append(msg) + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/#", _callback_a) # Simulate (retained) messages being played back on new subscriptions async_fire_mqtt_message(hass, "test/state1", "new_value_1", qos=0, retain=True) async_fire_mqtt_message(hass, "test/state2", "new_value_2", qos=0, retain=True) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await asyncio.sleep(0) await hass.async_block_till_done() assert len(calls_a) == 2 mqtt_client_mock.subscribe.assert_called() @@ -1696,6 +1701,7 @@ async def test_replaying_payload_wildcard_topic( async_fire_mqtt_message(hass, "test/state2", "initial_value_2", qos=0, retain=True) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await asyncio.sleep(0) await hass.async_block_till_done() # The retained messages playback should only be processed for the new subscriptions assert len(calls_a) == 0 @@ -1710,6 +1716,7 @@ async def test_replaying_payload_wildcard_topic( async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=False) async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=False) await hass.async_block_till_done() + await asyncio.sleep(0) assert len(calls_a) == 2 assert len(calls_b) == 2 @@ -1721,6 +1728,7 @@ async def test_replaying_payload_wildcard_topic( mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await asyncio.sleep(0) await hass.async_block_till_done() mqtt_client_mock.subscribe.assert_called() # Simulate the (retained) messages are played back after reconnecting @@ -1729,40 +1737,38 @@ async def test_replaying_payload_wildcard_topic( async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=True) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) # Both subscriptions should replay assert len(calls_a) == 2 assert len(calls_b) == 2 -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_not_calling_unsubscribe_with_active_subscribers( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test not calling unsubscribe() when other subscribers are active.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) await mqtt.async_subscribe(hass, "test/state", record_calls, 1) await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() + await asyncio.sleep(0) await hass.async_block_till_done() assert mqtt_client_mock.subscribe.called unsub() await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await hass.async_block_till_done(wait_background_tasks=True) assert not mqtt_client_mock.unsubscribe.called async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test not calling subscribe() when it is unsubscribed. @@ -1770,7 +1776,7 @@ async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( Make sure subscriptions are cleared if unsubscribed before the subscribe cool down period has ended. """ - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.subscribe.reset_mock() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) @@ -1780,26 +1786,20 @@ async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( assert not mqtt_client_mock.subscribe.called -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_unsubscribe_race( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test not calling unsubscribe() when other subscribers are active.""" - await mqtt_mock_entry() - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() - + mqtt_client_mock = setup_with_birth_msg_client_mock calls_a: list[ReceiveMessage] = [] calls_b: list[ReceiveMessage] = [] + @callback def _callback_a(msg: ReceiveMessage) -> None: calls_a.append(msg) + @callback def _callback_b(msg: ReceiveMessage) -> None: calls_b.append(msg) @@ -1807,10 +1807,11 @@ async def test_unsubscribe_race( unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) unsub() await mqtt.async_subscribe(hass, "test/state", _callback_b) - await hass.async_block_till_done() + await asyncio.sleep(0) await hass.async_block_till_done() async_fire_mqtt_message(hass, "test/state", "online") + await asyncio.sleep(0) await hass.async_block_till_done() assert not calls_a assert calls_b @@ -1840,54 +1841,44 @@ async def test_unsubscribe_race( "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) async def test_restore_subscriptions_on_reconnect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test subscriptions are restored on reconnect.""" - await mqtt_mock_entry() - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() - mqtt_client_mock.subscribe.reset_mock() + mqtt_client_mock = setup_with_birth_msg_client_mock + + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/state", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await asyncio.sleep(0) await hass.async_block_till_done() - assert mqtt_client_mock.subscribe.call_count == 1 + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + mqtt_client_mock.reset_mock() mqtt_client_mock.on_disconnect(None, None, 0) mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() - assert mqtt_client_mock.subscribe.call_count == 2 + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) @pytest.mark.parametrize( "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 1.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 1.0) async def test_restore_all_active_subscriptions_on_reconnect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, freezer: FrozenDateTimeFactory, ) -> None: """Test active subscriptions are restored correctly on reconnect.""" - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) @@ -1914,14 +1905,15 @@ async def test_restore_all_active_subscriptions_on_reconnect( await hass.async_block_till_done() expected.append(call([("test/state", 1)])) - assert mqtt_client_mock.subscribe.mock_calls == expected + for expected_call in expected: + assert mqtt_client_mock.subscribe.hass_call(expected_call) freezer.tick(3) async_fire_time_changed(hass) # cooldown await hass.async_block_till_done() freezer.tick(3) async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) @pytest.mark.parametrize( @@ -1933,14 +1925,13 @@ async def test_restore_all_active_subscriptions_on_reconnect( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 1.0) async def test_subscribed_at_highest_qos( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, freezer: FrozenDateTimeFactory, ) -> None: """Test the highest qos as assigned when subscribing to the same topic.""" - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) await hass.async_block_till_done() freezer.tick(5) @@ -1974,7 +1965,6 @@ async def test_reload_entry_with_restored_subscriptions( entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) - mqtt_client_mock.connect.return_value = 0 with patch("homeassistant.config.load_yaml_config_file", return_value={}): await hass.config_entries.async_setup(entry.entry_id) @@ -2026,49 +2016,42 @@ async def test_reload_entry_with_restored_subscriptions( assert recorded_calls[1].payload == "wild-card-payload3" -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 2) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 2) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 2) async def test_canceling_debouncer_on_shutdown( hass: HomeAssistant, record_calls: MessageCallbackType, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test canceling the debouncer when HA shuts down.""" + mqtt_client_mock = setup_with_birth_msg_client_mock - await mqtt_mock_entry() - mqtt_client_mock.subscribe.reset_mock() + with patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 2): + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + await mqtt.async_subscribe(hass, "test/state1", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + # Stop HA so the scheduled debouncer task will be canceled + mqtt_client_mock.subscribe.reset_mock() + hass.bus.fire(EVENT_HOMEASSISTANT_STOP) + await mqtt.async_subscribe(hass, "test/state2", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state3", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state4", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state5", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await hass.async_block_till_done() - await mqtt.async_subscribe(hass, "test/state1", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.2)) - await hass.async_block_till_done() + mqtt_client_mock.subscribe.assert_not_called() - await mqtt.async_subscribe(hass, "test/state2", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.2)) - await hass.async_block_till_done() - - await mqtt.async_subscribe(hass, "test/state3", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.2)) - await hass.async_block_till_done() - - await mqtt.async_subscribe(hass, "test/state4", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.2)) - await hass.async_block_till_done() - - await mqtt.async_subscribe(hass, "test/state5", record_calls) - - mqtt_client_mock.subscribe.assert_not_called() - - # Stop HA so the scheduled task will be canceled - hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - # mock disconnect status - mqtt_client_mock.on_disconnect(None, None, 0) - await hass.async_block_till_done() - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() - mqtt_client_mock.subscribe.assert_not_called() + # Note thet the broker connection will not be disconnected gracefully + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.subscribe.assert_not_called() + mqtt_client_mock.disconnect.assert_not_called() async def test_canceling_debouncer_normal( @@ -2130,13 +2113,13 @@ async def test_initial_setup_logs_error( async def test_logs_error_if_no_connect_broker( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test for setup failure if connection to broker is missing.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock # test with rc = 3 -> broker unavailable - mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 3) + mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_connect(Mock(), None, None, 3) await hass.async_block_till_done() assert ( "Unable to connect to the MQTT broker: Connection Refused: broker unavailable." @@ -2148,14 +2131,14 @@ async def test_logs_error_if_no_connect_broker( async def test_triggers_reauth_flow_if_auth_fails( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, return_code: int, ) -> None: """Test re-auth is triggered if authentication is failing.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock # test with rc = 4 -> CONNACK_REFUSED_NOT_AUTHORIZED and 5 -> CONNACK_REFUSED_BAD_USERNAME_PASSWORD - mqtt_client_mock.on_connect(mqtt_client_mock, None, None, return_code) + mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_connect(Mock(), None, None, return_code) await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2166,11 +2149,10 @@ async def test_triggers_reauth_flow_if_auth_fails( async def test_handle_mqtt_on_callback( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test receiving an ACK callback before waiting for it.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock with patch.object(mqtt_client_mock, "get_mid", return_value=100): # Simulate an ACK for mid == 100, this will call mqtt_mock._async_get_mid_future(mid) mqtt_client_mock.on_publish(mqtt_client_mock, None, 100) @@ -2222,51 +2204,47 @@ async def test_publish_error( assert "Failed to connect to MQTT server: Out of memory." in caplog.text -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) async def test_subscribe_error( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, caplog: pytest.LogCaptureFixture, ) -> None: """Test publish error.""" - await mqtt_mock_entry() - mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0) - await hass.async_block_till_done() - await hass.async_block_till_done() + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.reset_mock() # simulate client is not connected error before subscribing mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None) - await mqtt.async_subscribe(hass, "some-topic", record_calls) - while mqtt_client_mock.subscribe.call_count == 0: + with patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0): + await mqtt.async_subscribe(hass, "some-topic", record_calls) + while mqtt_client_mock.subscribe.call_count == 0: + await hass.async_block_till_done() await hass.async_block_till_done() - await hass.async_block_till_done() - await hass.async_block_till_done() - assert ( - "Error talking to MQTT: The client is not currently connected." in caplog.text - ) + await hass.async_block_till_done() + assert ( + "Error talking to MQTT: The client is not currently connected." + in caplog.text + ) async def test_handle_message_callback( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test for handling an incoming message callback.""" + mqtt_client_mock = setup_with_birth_msg_client_mock callbacks = [] @callback def _callback(args) -> None: callbacks.append(args) - mock_mqtt = await mqtt_mock_entry() msg = ReceiveMessage( "some-topic", b"test-payload", 1, False, "some-topic", datetime.now() ) - mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0) await mqtt.async_subscribe(hass, "some-topic", _callback) - mqtt_client_mock.on_message(mock_mqtt, None, msg) + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_message(None, None, msg) await hass.async_block_till_done() await hass.async_block_till_done() @@ -2395,8 +2373,6 @@ async def test_handle_mqtt_timeout_on_callback( ) 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 hass.config_entries.async_setup(entry.entry_id) @@ -2506,62 +2482,48 @@ async def test_tls_version( } ], ) +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_custom_birth_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test sending birth message.""" - await mqtt_mock_entry() - birth = asyncio.Event() - async def wait_birth(msg: ReceiveMessage) -> None: + birth = asyncio.Event() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + + @callback + def wait_birth(msg: ReceiveMessage) -> None: """Handle birth message.""" birth.set() - with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): - await mqtt.async_subscribe(hass, "birth", wait_birth) - mqtt_client_mock.on_connect(None, None, 0, 0) - await hass.async_block_till_done() - await birth.wait() - mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) + await mqtt.async_subscribe(hass, "birth", wait_birth) + await hass.async_block_till_done() + await birth.wait() + mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) @pytest.mark.parametrize( "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "homeassistant/status", - mqtt.ATTR_PAYLOAD: "online", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], + [ENTRY_DEFAULT_BIRTH_MESSAGE], ) async def test_default_birth_message( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test sending birth message.""" - await mqtt_mock_entry() - birth = asyncio.Event() - - async def wait_birth(msg: ReceiveMessage) -> None: - """Handle birth message.""" - birth.set() - - with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): - await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - mqtt_client_mock.on_connect(None, None, 0, 0) - await hass.async_block_till_done() - await birth.wait() - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) + mqtt_client_mock = setup_with_birth_msg_client_mock + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) @pytest.mark.parametrize( @@ -2573,28 +2535,30 @@ async def test_default_birth_message( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_no_birth_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test disabling birth message.""" - await mqtt_mock_entry() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) await hass.async_block_till_done() mqtt_client_mock.reset_mock() - # Assert no birth message was sent - mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) await hass.async_block_till_done() mqtt_client_mock.publish.assert_not_called() - async def callback(msg: ReceiveMessage) -> None: - """Handle birth message.""" + @callback + def msg_callback(msg: ReceiveMessage) -> None: + """Handle callback.""" mqtt_client_mock.reset_mock() - await mqtt.async_subscribe(hass, "homeassistant/some-topic", callback) + await mqtt.async_subscribe(hass, "homeassistant/some-topic", msg_callback) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) await hass.async_block_till_done() @@ -2603,130 +2567,61 @@ async def test_no_birth_message( @pytest.mark.parametrize( "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "homeassistant/status", - mqtt.ATTR_PAYLOAD: "online", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], + [ENTRY_DEFAULT_BIRTH_MESSAGE], ) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.2) async def test_delayed_birth_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_config_entry_data, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test sending birth message does not happen until Home Assistant starts.""" - mqtt_mock = await mqtt_mock_entry() - hass.set_state(CoreState.starting) - birth = asyncio.Event() - await hass.async_block_till_done() - + birth = asyncio.Event() entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - mqtt_component_mock = MagicMock( - return_value=hass.data["mqtt"].client, - wraps=hass.data["mqtt"].client, - ) - mqtt_component_mock._mqttc = mqtt_client_mock - - hass.data["mqtt"].client = mqtt_component_mock - mqtt_mock = hass.data["mqtt"].client - mqtt_mock.reset_mock() - - async def wait_birth(msg: ReceiveMessage) -> None: - """Handle birth message.""" - birth.set() - - with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): - await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - mqtt_client_mock.on_connect(None, None, 0, 0) - await hass.async_block_till_done() - with pytest.raises(TimeoutError): - await asyncio.wait_for(birth.wait(), 0.2) - assert not mqtt_client_mock.publish.called - assert not birth.is_set() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await birth.wait() - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "homeassistant/status", - mqtt.ATTR_PAYLOAD: "online", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], -) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -async def test_subscription_done_when_birth_message_is_sent( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, - mqtt_config_entry_data, -) -> None: - """Test sending birth message until initial subscription has been completed.""" - hass.set_state(CoreState.starting) - birth = asyncio.Event() - - await hass.async_block_till_done() - - 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) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - mqtt_client_mock.on_disconnect(None, None, 0, 0) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - @callback def wait_birth(msg: ReceiveMessage) -> None: """Handle birth message.""" birth.set() - await mqtt.async_subscribe(hass, "topic/test", record_calls) await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) await hass.async_block_till_done() - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_connect(None, None, 0, 0) - # We wait until we receive a birth message - await asyncio.wait_for(birth.wait(), 1) + with pytest.raises(TimeoutError): + await asyncio.wait_for(birth.wait(), 0.05) + assert not mqtt_client_mock.publish.called + assert not birth.is_set() - # Assert we already have subscribed at the client - # for new config payloads at the time we the birth message is received + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await birth.wait() + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_subscription_done_when_birth_message_is_sent( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test sending birth message until initial subscription has been completed.""" + mqtt_client_mock = setup_with_birth_msg_client_mock subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) assert ("homeassistant/+/+/config", 0) in subscribe_calls assert ("homeassistant/+/+/+/config", 0) in subscribe_calls mqtt_client_mock.publish.assert_called_with( "homeassistant/status", "online", 0, False ) - assert ("topic/test", 0) in subscribe_calls @pytest.mark.parametrize( @@ -2745,11 +2640,15 @@ async def test_subscription_done_when_birth_message_is_sent( ) async def test_custom_will_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test will message.""" - await mqtt_mock_entry() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mqtt_client_mock.will_set.assert_called_with( topic="death", payload="death", qos=0, retain=False @@ -2758,12 +2657,10 @@ async def test_custom_will_message( async def test_default_will_message( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test will message.""" - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.will_set.assert_called_with( topic="homeassistant/status", payload="offline", qos=0, retain=False ) @@ -2775,56 +2672,47 @@ async def test_default_will_message( ) async def test_no_will_message( hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test will message.""" - await mqtt_mock_entry() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mqtt_client_mock.will_set.assert_not_called() @pytest.mark.parametrize( "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: {}, - mqtt.CONF_DISCOVERY: False, - } - ], + [ENTRY_DEFAULT_BIRTH_MESSAGE | {mqtt.CONF_DISCOVERY: False}], ) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_subscribes_topics_on_connect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test subscription to topic on connect.""" - await mqtt_mock_entry() - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - mqtt_client_mock.reset_mock() + mqtt_client_mock = setup_with_birth_msg_client_mock await mqtt.async_subscribe(hass, "topic/test", record_calls) await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2) await mqtt.async_subscribe(hass, "still/pending", record_calls) await mqtt.async_subscribe(hass, "still/pending", record_calls, 1) - mqtt_client_mock.on_connect(None, None, 0, 0) + mqtt_client_mock.on_disconnect(Mock(), None, 0) + + mqtt_client_mock.reset_mock() + + mqtt_client_mock.on_connect(Mock(), None, 0, 0) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - - assert mqtt_client_mock.disconnect.call_count == 0 + await hass.async_block_till_done(wait_background_tasks=True) subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) - assert len(subscribe_calls) == 3 assert ("topic/test", 0) in subscribe_calls assert ("home/sensor", 2) in subscribe_calls assert ("still/pending", 1) in subscribe_calls @@ -2832,31 +2720,21 @@ async def test_mqtt_subscribes_topics_on_connect( @pytest.mark.parametrize( "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: {}, - mqtt.CONF_DISCOVERY: False, - } - ], + [ENTRY_DEFAULT_BIRTH_MESSAGE], ) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_subscribes_in_single_call( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test bundled client subscription to topic.""" - await mqtt_mock_entry() - + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.subscribe.reset_mock() await mqtt.async_subscribe(hass, "topic/test", record_calls) await mqtt.async_subscribe(hass, "home/sensor", record_calls) - await hass.async_block_till_done() # Make sure the debouncer finishes - await asyncio.sleep(0.2) + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) assert mqtt_client_mock.subscribe.call_count == 1 # Assert we have a single subscription call with both subscriptions @@ -2866,28 +2744,16 @@ async def test_mqtt_subscribes_in_single_call( ] -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: {}, - mqtt.CONF_DISCOVERY: False, - } - ], -) +@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) @patch("homeassistant.components.mqtt.client.MAX_SUBSCRIBES_PER_CALL", 2) @patch("homeassistant.components.mqtt.client.MAX_UNSUBSCRIBES_PER_CALL", 2) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_subscribes_and_unsubscribes_in_chunks( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test chunked client subscriptions.""" - await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.subscribe.reset_mock() unsub_tasks: list[CALLBACK_TYPE] = [] @@ -2895,9 +2761,9 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor1", record_calls)) unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test2", record_calls)) unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor2", record_calls)) - await hass.async_block_till_done() # Make sure the debouncer finishes - await asyncio.sleep(0.2) + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) assert mqtt_client_mock.subscribe.call_count == 2 # Assert we have a 2 subscription calls with both 2 subscriptions @@ -2909,7 +2775,8 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( task() await hass.async_block_till_done() # Make sure the debouncer finishes - await asyncio.sleep(0.2) + await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) assert mqtt_client_mock.unsubscribe.call_count == 2 # Assert we have a 2 unsubscribe calls with both 2 topic @@ -2920,7 +2787,6 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( async def test_default_entry_setting_are_applied( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - mqtt_mock_entry: MqttMockHAClientGenerator, mqtt_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: @@ -2932,11 +2798,12 @@ async def test_default_entry_setting_are_applied( ) # Config entry data is incomplete but valid according the schema - entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - hass.config_entries.async_update_entry( - entry, data={"broker": "test-broker", "port": 1234} + entry = MockConfigEntry( + domain=mqtt.DOMAIN, data={"broker": "test-broker", "port": 1234} ) - await mqtt_mock_entry() + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() # Discover a device to verify the entry was setup correctly @@ -2977,10 +2844,9 @@ async def test_message_callback_exception_gets_logged( async def test_message_partial_callback_exception_gets_logged( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test exception raised by message handler.""" - await mqtt_mock_entry() @callback def bad_handler(msg: ReceiveMessage) -> None: @@ -2998,8 +2864,12 @@ async def test_message_partial_callback_exception_gets_logged( await mqtt.async_subscribe( hass, "test-topic", partial(parial_handler, bad_handler, {"some_attr"}) ) + await hass.async_block_till_done(wait_background_tasks=True) async_fire_mqtt_message(hass, "test-topic", "test") await hass.async_block_till_done() + await hass.async_block_till_done() + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) assert ( "Exception in bad_handler when handling msg on 'test-topic':" @@ -3726,11 +3596,10 @@ async def test_publish_json_from_template( async def test_subscribe_connection_status( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test connextion status subscription.""" - mqtt_mock = await mqtt_mock_entry() + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_connected_calls_callback: list[bool] = [] mqtt_connected_calls_async: list[bool] = [] @@ -3743,7 +3612,13 @@ async def test_subscribe_connection_status( """Update state on connection/disconnection to MQTT broker.""" mqtt_connected_calls_async.append(status) - mqtt_mock.connected = True + # Check connection status + assert mqtt.is_connected(hass) is True + + # Mock disconnect status + mqtt_client_mock.on_disconnect(None, None, 0) + await hass.async_block_till_done() + assert mqtt.is_connected(hass) is False unsub_callback = mqtt.async_subscribe_connection_status( hass, async_mqtt_connected_callback @@ -3753,7 +3628,7 @@ async def test_subscribe_connection_status( ) await hass.async_block_till_done() - # Mock connection status + # Mock connect status mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() assert mqtt.is_connected(hass) is True @@ -3761,13 +3636,17 @@ async def test_subscribe_connection_status( # Mock disconnect status mqtt_client_mock.on_disconnect(None, None, 0) await hass.async_block_till_done() + assert mqtt.is_connected(hass) is False # Unsubscribe unsub_callback() unsub_async() + # Mock connect status mqtt_client_mock.on_connect(None, None, 0, 0) + await asyncio.sleep(0) await hass.async_block_till_done() + assert mqtt.is_connected(hass) is True # Check calls assert len(mqtt_connected_calls_callback) == 2 @@ -3781,11 +3660,11 @@ async def test_subscribe_connection_status( async def test_unload_config_entry( hass: HomeAssistant, - mqtt_mock: MqttMockHAClient, - mqtt_client_mock: MqttMockPahoClient, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test unloading the MQTT entry.""" + mqtt_client_mock = setup_with_birth_msg_client_mock assert hass.services.has_service(mqtt.DOMAIN, "dump") assert hass.services.has_service(mqtt.DOMAIN, "publish") @@ -4015,6 +3894,7 @@ async def test_link_config_entry( mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] mqtt_platforms = async_get_platforms(hass, mqtt.DOMAIN) + @callback def _check_entities() -> int: entities: list[Entity] = [] for mqtt_platform in mqtt_platforms: @@ -4096,6 +3976,7 @@ async def test_reload_config_entry( entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + @callback def _check_entities() -> int: entities: list[Entity] = [] mqtt_platforms = async_get_platforms(hass, mqtt.DOMAIN) @@ -4406,19 +4287,14 @@ async def test_multi_platform_discovery( ) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_auto_reconnect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test reconnection is automatically done.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.reconnect.reset_mock() mqtt_client_mock.disconnect() @@ -4454,20 +4330,15 @@ async def test_auto_reconnect( assert len(mqtt_client_mock.reconnect.mock_calls) == 2 -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_server_sock_connect_and_disconnect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, ) -> None: """Test handling the socket connected and disconnected.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4495,19 +4366,14 @@ async def test_server_sock_connect_and_disconnect( assert len(recorded_calls) == 0 -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_server_sock_buffer_size( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test handling the socket buffer size fails.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4523,19 +4389,14 @@ async def test_server_sock_buffer_size( assert "Unable to increase the socket buffer size" in caplog.text -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_server_sock_buffer_size_with_websocket( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test handling the socket buffer size fails.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4560,20 +4421,15 @@ async def test_server_sock_buffer_size_with_websocket( assert "Unable to increase the socket buffer size" in caplog.text -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_client_sock_failure_after_connect( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, ) -> None: """Test handling the socket connected and disconnected.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4589,7 +4445,7 @@ async def test_client_sock_failure_after_connect( mqtt_client_mock.loop_write.side_effect = OSError("foo") client.close() # close the client socket out from under the client - assert mqtt_mock.connected is True + assert mqtt_client_mock.connect.call_count == 1 unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) await hass.async_block_till_done() @@ -4599,19 +4455,14 @@ async def test_client_sock_failure_after_connect( assert len(recorded_calls) == 0 -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_loop_write_failure( hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, + setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test handling the socket connected and disconnected.""" - mqtt_mock = await mqtt_mock_entry() - await hass.async_block_till_done() - assert mqtt_mock.connected is True + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS @@ -4642,7 +4493,7 @@ async def test_loop_write_failure( # Final for the disconnect callback await hass.async_block_till_done() - assert "Disconnected from MQTT server mock-broker:1883" in caplog.text + assert "Disconnected from MQTT server test-broker:1883" in caplog.text @pytest.mark.parametrize( From e2b0c558839aa0105a56daa9c2ab3be12ab92332 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 24 Jun 2024 14:42:31 -0400 Subject: [PATCH 0082/2411] Bump python-fullykiosk to 0.0.14 (#120361) --- homeassistant/components/fully_kiosk/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fully_kiosk/manifest.json b/homeassistant/components/fully_kiosk/manifest.json index 8d9ba85a058..4d7d1a2d7da 100644 --- a/homeassistant/components/fully_kiosk/manifest.json +++ b/homeassistant/components/fully_kiosk/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/fully_kiosk", "iot_class": "local_polling", "mqtt": ["fully/deviceInfo/+"], - "requirements": ["python-fullykiosk==0.0.13"] + "requirements": ["python-fullykiosk==0.0.14"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5b321763d2e..0330e991994 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2248,7 +2248,7 @@ python-etherscan-api==0.0.3 python-family-hub-local==0.0.2 # homeassistant.components.fully_kiosk -python-fullykiosk==0.0.13 +python-fullykiosk==0.0.14 # homeassistant.components.sms # python-gammu==3.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d12f32d295..8f90092740a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1760,7 +1760,7 @@ python-bsblan==0.5.18 python-ecobee-api==0.2.18 # homeassistant.components.fully_kiosk -python-fullykiosk==0.0.13 +python-fullykiosk==0.0.14 # homeassistant.components.sms # python-gammu==3.2.4 From 6b78e913f2ab32d629a3e5843f21d3807d39e2fa Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 24 Jun 2024 12:45:30 -0600 Subject: [PATCH 0083/2411] Bump pybalboa to 1.0.2 (#120360) --- homeassistant/components/balboa/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/balboa/manifest.json b/homeassistant/components/balboa/manifest.json index 152a89bde31..d7c15bab88f 100644 --- a/homeassistant/components/balboa/manifest.json +++ b/homeassistant/components/balboa/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/balboa", "iot_class": "local_push", "loggers": ["pybalboa"], - "requirements": ["pybalboa==1.0.1"] + "requirements": ["pybalboa==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0330e991994..ea455bf47a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1734,7 +1734,7 @@ pyatv==0.14.3 pyaussiebb==0.0.15 # homeassistant.components.balboa -pybalboa==1.0.1 +pybalboa==1.0.2 # homeassistant.components.bbox pybbox==0.0.5-alpha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f90092740a..3d78a857095 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1381,7 +1381,7 @@ pyatv==0.14.3 pyaussiebb==0.0.15 # homeassistant.components.balboa -pybalboa==1.0.1 +pybalboa==1.0.2 # homeassistant.components.blackbird pyblackbird==0.6 From fb3059e6e68684732a6b9fd85b4ce9bfed036ce9 Mon Sep 17 00:00:00 2001 From: Koen van Zuijlen <8818390+kvanzuijlen@users.noreply.github.com> Date: Mon, 24 Jun 2024 20:47:42 +0200 Subject: [PATCH 0084/2411] Bump justnimbus to 0.7.4 (#120355) --- homeassistant/components/justnimbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/justnimbus/manifest.json b/homeassistant/components/justnimbus/manifest.json index 26cbc80e166..48fdad69ac8 100644 --- a/homeassistant/components/justnimbus/manifest.json +++ b/homeassistant/components/justnimbus/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/justnimbus", "iot_class": "cloud_polling", - "requirements": ["justnimbus==0.7.3"] + "requirements": ["justnimbus==0.7.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index ea455bf47a5..e21548fce85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1191,7 +1191,7 @@ jellyfin-apiclient-python==1.9.2 jsonpath==0.82.2 # homeassistant.components.justnimbus -justnimbus==0.7.3 +justnimbus==0.7.4 # homeassistant.components.kaiterra kaiterra-async-client==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d78a857095..2dd55cd9f7c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -978,7 +978,7 @@ jellyfin-apiclient-python==1.9.2 jsonpath==0.82.2 # homeassistant.components.justnimbus -justnimbus==0.7.3 +justnimbus==0.7.4 # homeassistant.components.kegtron kegtron-ble==0.4.0 From 00621ad512e2f377f48cad5c27d8fc2e9529f60b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 24 Jun 2024 20:53:49 +0200 Subject: [PATCH 0085/2411] Use runtime data in version (#120363) --- homeassistant/components/version/__init__.py | 13 ++++++------- homeassistant/components/version/binary_sensor.py | 9 ++++----- homeassistant/components/version/diagnostics.py | 8 +++----- homeassistant/components/version/sensor.py | 9 ++++----- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/version/__init__.py b/homeassistant/components/version/__init__.py index 4112cc51e46..cf13821dc8a 100644 --- a/homeassistant/components/version/__init__.py +++ b/homeassistant/components/version/__init__.py @@ -16,15 +16,16 @@ from .const import ( CONF_CHANNEL, CONF_IMAGE, CONF_SOURCE, - DOMAIN, PLATFORMS, ) from .coordinator import VersionDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +type VersionConfigEntry = ConfigEntry[VersionDataUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: VersionConfigEntry) -> bool: """Set up the version integration from a config entry.""" board = entry.data[CONF_BOARD] @@ -50,14 +51,12 @@ 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 + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: VersionConfigEntry) -> bool: """Unload the config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/version/binary_sensor.py b/homeassistant/components/version/binary_sensor.py index ff4f51e409f..827029e1d8c 100644 --- a/homeassistant/components/version/binary_sensor.py +++ b/homeassistant/components/version/binary_sensor.py @@ -9,13 +9,12 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, EntityCategory, __version__ as HA_VERSION from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import CONF_SOURCE, DEFAULT_NAME, DOMAIN -from .coordinator import VersionDataUpdateCoordinator +from . import VersionConfigEntry +from .const import CONF_SOURCE, DEFAULT_NAME from .entity import VersionEntity HA_VERSION_OBJECT = AwesomeVersion(HA_VERSION) @@ -23,11 +22,11 @@ HA_VERSION_OBJECT = AwesomeVersion(HA_VERSION) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: VersionConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up version binary_sensors.""" - coordinator: VersionDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data if (source := config_entry.data[CONF_SOURCE]) == "local": return diff --git a/homeassistant/components/version/diagnostics.py b/homeassistant/components/version/diagnostics.py index 194027d6ef4..ca7318f468b 100644 --- a/homeassistant/components/version/diagnostics.py +++ b/homeassistant/components/version/diagnostics.py @@ -6,20 +6,18 @@ from typing import Any from attr import asdict -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 DOMAIN -from .coordinator import VersionDataUpdateCoordinator +from . import VersionConfigEntry async def async_get_config_entry_diagnostics( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: VersionConfigEntry, ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: VersionDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 6b0565b8cb3..e1d552bcd36 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -5,24 +5,23 @@ from __future__ import annotations from typing import Any from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import CONF_SOURCE, DEFAULT_NAME, DOMAIN -from .coordinator import VersionDataUpdateCoordinator +from . import VersionConfigEntry +from .const import CONF_SOURCE, DEFAULT_NAME from .entity import VersionEntity async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: VersionConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up version sensors.""" - coordinator: VersionDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data if (entity_name := entry.data[CONF_NAME]) == DEFAULT_NAME: entity_name = entry.title From 6689dbbcc68ff0d527cfc1160928feb60c348f05 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 24 Jun 2024 20:56:35 +0200 Subject: [PATCH 0086/2411] Deprecate DTE Energy Bridge (#120350) Co-authored-by: Franck Nijhof --- .../components/dte_energy_bridge/sensor.py | 14 ++++++++++++++ .../components/dte_energy_bridge/strings.json | 8 ++++++++ 2 files changed, 22 insertions(+) create mode 100644 homeassistant/components/dte_energy_bridge/strings.json diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index c33bb37e468..112ebd55f94 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import CONF_NAME, UnitOfPower from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -27,6 +28,7 @@ CONF_VERSION = "version" DEFAULT_NAME = "Current Energy Usage" DEFAULT_VERSION = 1 +DOMAIN = "dte_energy_bridge" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -46,6 +48,18 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the DTE energy bridge sensor.""" + create_issue( + hass, + DOMAIN, + "deprecated_integration", + breaks_in_ha_version="2025.1.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_integration", + translation_placeholders={"domain": DOMAIN}, + ) + name = config[CONF_NAME] ip_address = config[CONF_IP_ADDRESS] version = config[CONF_VERSION] diff --git a/homeassistant/components/dte_energy_bridge/strings.json b/homeassistant/components/dte_energy_bridge/strings.json new file mode 100644 index 00000000000..f75867b8faa --- /dev/null +++ b/homeassistant/components/dte_energy_bridge/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "deprecated_integration": { + "title": "The DTE Energy Bridge integration will be removed", + "description": "The DTE Energy Bridge integration will be removed as new users can't get any supported devices, and the integration will fail as soon as a current device gets internet access.\n\n Please remove all `{domain}`platform sensors from your configuration and restart Home Assistant." + } + } +} From 46dcf1dc44e1c44336969592b9a4d6a4013864a9 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 24 Jun 2024 13:57:56 -0500 Subject: [PATCH 0087/2411] Prioritize custom intents over builtin (#120358) --- .../components/conversation/default_agent.py | 32 +++++- tests/components/conversation/test_init.py | 103 ++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 7bb2c2182b3..71b14f8d299 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -419,6 +419,7 @@ class DefaultAgent(ConversationEntity): language: str, ) -> RecognizeResult | None: """Search intents for a match to user input.""" + custom_result: RecognizeResult | None = None name_result: RecognizeResult | None = None best_results: list[RecognizeResult] = [] best_text_chunks_matched: int | None = None @@ -429,6 +430,20 @@ class DefaultAgent(ConversationEntity): intent_context=intent_context, language=language, ): + # User intents have highest priority + if (result.intent_metadata is not None) and result.intent_metadata.get( + METADATA_CUSTOM_SENTENCE + ): + if (custom_result is None) or ( + result.text_chunks_matched > custom_result.text_chunks_matched + ): + custom_result = result + + # Clear builtin results + best_results = [] + name_result = None + continue + # Prioritize results with a "name" slot, but still prefer ones with # more literal text matched. if ( @@ -453,6 +468,10 @@ class DefaultAgent(ConversationEntity): # We will resolve the ambiguity below. best_results.append(result) + if custom_result is not None: + # Prioritize user intents + return custom_result + if name_result is not None: # Prioritize matches with entity names above area names return name_result @@ -718,11 +737,22 @@ class DefaultAgent(ConversationEntity): if self._config_intents and ( self.hass.config.language in (language, language_variant) ): + hass_config_path = self.hass.config.path() merge_dict( intents_dict, { "intents": { - intent_name: {"data": [{"sentences": sentences}]} + intent_name: { + "data": [ + { + "sentences": sentences, + "metadata": { + METADATA_CUSTOM_SENTENCE: True, + METADATA_CUSTOM_FILE: hass_config_path, + }, + } + ] + } for intent_name, sentences in self._config_intents.items() } }, diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 48f227e9497..dc940dba81b 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1,12 +1,15 @@ """The tests for the Conversation component.""" from http import HTTPStatus +import os +import tempfile from typing import Any from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion import voluptuous as vol +import yaml from homeassistant.components import conversation from homeassistant.components.conversation import default_agent @@ -1389,3 +1392,103 @@ async def test_ws_hass_agent_debug_sentence_trigger( # Trigger should not have been executed assert len(calls) == 0 + + +async def test_custom_sentences_priority( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_admin_user: MockUser, + snapshot: SnapshotAssertion, +) -> None: + """Test that user intents from custom_sentences have priority over builtin intents/sentences.""" + with tempfile.NamedTemporaryFile( + mode="w+", + encoding="utf-8", + suffix=".yaml", + dir=os.path.join(hass.config.config_dir, "custom_sentences", "en"), + ) as custom_sentences_file: + # Add a custom sentence that would match a builtin sentence. + # Custom sentences have priority. + yaml.dump( + { + "language": "en", + "intents": { + "CustomIntent": {"data": [{"sentences": ["turn on the lamp"]}]} + }, + }, + custom_sentences_file, + ) + custom_sentences_file.flush() + custom_sentences_file.seek(0) + + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "conversation", {}) + assert await async_setup_component(hass, "light", {}) + assert await async_setup_component(hass, "intent", {}) + assert await async_setup_component( + hass, + "intent_script", + { + "intent_script": { + "CustomIntent": {"speech": {"text": "custom response"}} + } + }, + ) + + # Ensure that a "lamp" exists so that we can verify the custom intent + # overrides the builtin sentence. + hass.states.async_set("light.lamp", "off") + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", + json={ + "text": "turn on the lamp", + "language": hass.config.language, + }, + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data["response"]["response_type"] == "action_done" + assert data["response"]["speech"]["plain"]["speech"] == "custom response" + + +async def test_config_sentences_priority( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_admin_user: MockUser, + snapshot: SnapshotAssertion, +) -> None: + """Test that user intents from configuration.yaml have priority over builtin intents/sentences.""" + # Add a custom sentence that would match a builtin sentence. + # Custom sentences have priority. + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "intent", {}) + assert await async_setup_component( + hass, + "conversation", + {"conversation": {"intents": {"CustomIntent": ["turn on the lamp"]}}}, + ) + assert await async_setup_component(hass, "light", {}) + assert await async_setup_component( + hass, + "intent_script", + {"intent_script": {"CustomIntent": {"speech": {"text": "custom response"}}}}, + ) + + # Ensure that a "lamp" exists so that we can verify the custom intent + # overrides the builtin sentence. + hass.states.async_set("light.lamp", "off") + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", + json={ + "text": "turn on the lamp", + "language": hass.config.language, + }, + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data["response"]["response_type"] == "action_done" + assert data["response"]["speech"]["plain"]["speech"] == "custom response" From 3b79ab6e1832aac01e557aa5e72a3098a3a2ea0c Mon Sep 17 00:00:00 2001 From: David Knowles Date: Mon, 24 Jun 2024 14:58:54 -0400 Subject: [PATCH 0088/2411] Reduce the amount of data fetched in individual Hydrawise API calls (#120328) --- homeassistant/components/hydrawise/config_flow.py | 4 ++-- homeassistant/components/hydrawise/coordinator.py | 6 +++++- tests/components/hydrawise/conftest.py | 9 +++------ tests/components/hydrawise/test_config_flow.py | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hydrawise/config_flow.py b/homeassistant/components/hydrawise/config_flow.py index 1c2c1c5cf29..ab9ebbb065d 100644 --- a/homeassistant/components/hydrawise/config_flow.py +++ b/homeassistant/components/hydrawise/config_flow.py @@ -37,8 +37,8 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN): # Verify that the provided credentials work.""" api = client.Hydrawise(auth.Auth(username, password)) try: - # Skip fetching zones to save on metered API calls. - user = await api.get_user() + # Don't fetch zones because we don't need them yet. + user = await api.get_user(fetch_zones=False) except NotAuthorizedError: return on_failure("invalid_auth") except TimeoutError: diff --git a/homeassistant/components/hydrawise/coordinator.py b/homeassistant/components/hydrawise/coordinator.py index d046dfcc92a..50caaa0c0de 100644 --- a/homeassistant/components/hydrawise/coordinator.py +++ b/homeassistant/components/hydrawise/coordinator.py @@ -40,13 +40,17 @@ class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]): async def _async_update_data(self) -> HydrawiseData: """Fetch the latest data from Hydrawise.""" - user = await self.api.get_user() + # Don't fetch zones. We'll fetch them for each controller later. + # This is to prevent 502 errors in some cases. + # See: https://github.com/home-assistant/core/issues/120128 + user = await self.api.get_user(fetch_zones=False) controllers = {} zones = {} sensors = {} daily_water_use: dict[int, ControllerWaterUseSummary] = {} for controller in user.controllers: controllers[controller.id] = controller + controller.zones = await self.api.get_zones(controller) for zone in controller.zones: zones[zone.id] = zone for sensor in controller.sensors: diff --git a/tests/components/hydrawise/conftest.py b/tests/components/hydrawise/conftest.py index eb1518eb7f2..0b5327cd7b2 100644 --- a/tests/components/hydrawise/conftest.py +++ b/tests/components/hydrawise/conftest.py @@ -1,6 +1,6 @@ """Common fixtures for the Hydrawise tests.""" -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator from datetime import datetime, timedelta from unittest.mock import AsyncMock, patch @@ -20,7 +20,6 @@ from pydrawise.schema import ( Zone, ) import pytest -from typing_extensions import Generator from homeassistant.components.hydrawise.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME @@ -67,9 +66,9 @@ def mock_pydrawise( """Mock Hydrawise.""" with patch("pydrawise.client.Hydrawise", autospec=True) as mock_pydrawise: user.controllers = [controller] - controller.zones = zones controller.sensors = sensors mock_pydrawise.return_value.get_user.return_value = user + mock_pydrawise.return_value.get_zones.return_value = zones mock_pydrawise.return_value.get_water_use_summary.return_value = ( controller_water_use_summary ) @@ -142,7 +141,7 @@ def sensors() -> list[Sensor]: ), status=SensorStatus( water_flow=LocalizedValueType(value=577.0044752010709, unit="gal"), - active=None, + active=False, ), ), ] @@ -154,7 +153,6 @@ def zones() -> list[Zone]: return [ Zone( name="Zone One", - number=1, id=5965394, scheduled_runs=ScheduledZoneRuns( summary="", @@ -171,7 +169,6 @@ def zones() -> list[Zone]: ), Zone( name="Zone Two", - number=2, id=5965395, scheduled_runs=ScheduledZoneRuns( current_run=ScheduledZoneRun( diff --git a/tests/components/hydrawise/test_config_flow.py b/tests/components/hydrawise/test_config_flow.py index a7fbc008aab..e85b1b9b249 100644 --- a/tests/components/hydrawise/test_config_flow.py +++ b/tests/components/hydrawise/test_config_flow.py @@ -46,7 +46,7 @@ async def test_form( CONF_PASSWORD: "__password__", } assert len(mock_setup_entry.mock_calls) == 1 - mock_pydrawise.get_user.assert_called_once_with() + mock_pydrawise.get_user.assert_called_once_with(fetch_zones=False) async def test_form_api_error( From b223cb7bb9c052516dd8267c1a3e2d9e22b9d079 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:00:12 +0200 Subject: [PATCH 0089/2411] Ensure config_entry is added to hass in tests (#120327) --- tests/components/honeywell/test_init.py | 3 +-- tests/components/switch_as_x/test_init.py | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index cdd767f019d..ac24876413d 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -173,14 +173,13 @@ async def test_remove_stale_device( identifiers={("OtherDomain", 7654321)}, ) + config_entry.add_to_hass(hass) device_registry.async_update_device( device_entry_other.id, add_config_entry_id=config_entry.entry_id, merge_identifiers={(DOMAIN, 7654321)}, ) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index 3889a43f741..e250cacb7ac 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -171,8 +171,10 @@ async def test_device_registry_config_entry_1( original_name="ABC", ) # Add another config entry to the same device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) device_registry.async_update_device( - device_entry.id, add_config_entry_id=MockConfigEntry().entry_id + device_entry.id, add_config_entry_id=other_config_entry.entry_id ) switch_as_x_config_entry = MockConfigEntry( From ea09d0cbed706bcf720d3340c04da8e4289169fb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:02:08 +0200 Subject: [PATCH 0090/2411] Use HassKey in cloud integration (#120322) --- homeassistant/components/cloud/__init__.py | 25 ++++++----- .../components/cloud/account_link.py | 10 +++-- .../components/cloud/assist_pipeline.py | 4 +- .../components/cloud/binary_sensor.py | 4 +- homeassistant/components/cloud/const.py | 14 ++++++- homeassistant/components/cloud/http_api.py | 42 +++++++++---------- homeassistant/components/cloud/repairs.py | 6 +-- homeassistant/components/cloud/stt.py | 7 ++-- .../components/cloud/system_health.py | 7 +--- homeassistant/components/cloud/tts.py | 9 ++-- tests/components/cloud/__init__.py | 9 ++-- tests/components/cloud/conftest.py | 9 ++-- tests/components/cloud/test_account_link.py | 7 ++-- tests/components/cloud/test_alexa_config.py | 9 ++-- tests/components/cloud/test_client.py | 19 +++++---- tests/components/cloud/test_google_config.py | 19 +++++---- tests/components/cloud/test_init.py | 11 +++-- 17 files changed, 109 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index cd8e5101e73..80c02571d24 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -55,6 +55,7 @@ from .const import ( CONF_SERVICEHANDLERS_SERVER, CONF_THINGTALK_SERVER, CONF_USER_POOL_ID, + DATA_CLOUD, DATA_PLATFORMS_SETUP, DOMAIN, MODE_DEV, @@ -155,14 +156,14 @@ def async_is_logged_in(hass: HomeAssistant) -> bool: Note: This returns True even if not currently connected to the cloud. """ - return DOMAIN in hass.data and hass.data[DOMAIN].is_logged_in + return DATA_CLOUD in hass.data and hass.data[DATA_CLOUD].is_logged_in @bind_hass @callback def async_is_connected(hass: HomeAssistant) -> bool: """Test if connected to the cloud.""" - return DOMAIN in hass.data and hass.data[DOMAIN].iot.connected + return DATA_CLOUD in hass.data and hass.data[DATA_CLOUD].iot.connected @callback @@ -178,7 +179,7 @@ def async_listen_connection_change( @callback def async_active_subscription(hass: HomeAssistant) -> bool: """Test if user has an active subscription.""" - return async_is_logged_in(hass) and not hass.data[DOMAIN].subscription_expired + return async_is_logged_in(hass) and not hass.data[DATA_CLOUD].subscription_expired async def async_get_or_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str: @@ -189,7 +190,7 @@ async def async_get_or_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> if not async_is_logged_in(hass): raise CloudNotAvailable - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] cloudhooks = cloud.client.cloudhooks if hook := cloudhooks.get(webhook_id): return cast(str, hook["cloudhook_url"]) @@ -206,7 +207,7 @@ async def async_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str: if not async_is_logged_in(hass): raise CloudNotAvailable - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] hook = await cloud.cloudhooks.async_create(webhook_id, True) cloudhook_url: str = hook["cloudhook_url"] return cloudhook_url @@ -215,10 +216,10 @@ async def async_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str: @bind_hass async def async_delete_cloudhook(hass: HomeAssistant, webhook_id: str) -> None: """Delete a cloudhook.""" - if DOMAIN not in hass.data: + if DATA_CLOUD not in hass.data: raise CloudNotAvailable - await hass.data[DOMAIN].cloudhooks.async_delete(webhook_id) + await hass.data[DATA_CLOUD].cloudhooks.async_delete(webhook_id) @bind_hass @@ -228,10 +229,10 @@ def async_remote_ui_url(hass: HomeAssistant) -> str: if not async_is_logged_in(hass): raise CloudNotAvailable - if not hass.data[DOMAIN].client.prefs.remote_enabled: + if not hass.data[DATA_CLOUD].client.prefs.remote_enabled: raise CloudNotAvailable - if not (remote_domain := hass.data[DOMAIN].client.prefs.remote_domain): + if not (remote_domain := hass.data[DATA_CLOUD].client.prefs.remote_domain): raise CloudNotAvailable return f"https://{remote_domain}" @@ -256,7 +257,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Initialize Cloud websession = async_get_clientsession(hass) client = CloudClient(hass, prefs, websession, alexa_conf, google_conf) - cloud = hass.data[DOMAIN] = Cloud(client, **kwargs) + cloud = hass.data[DATA_CLOUD] = Cloud(client, **kwargs) async def _shutdown(event: Event) -> None: """Shutdown event.""" @@ -373,9 +374,7 @@ def _remote_handle_prefs_updated(cloud: Cloud[CloudClient]) -> None: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - stt_tts_entities_added: asyncio.Event = hass.data[DATA_PLATFORMS_SETUP][ - "stt_tts_entities_added" - ] + stt_tts_entities_added = hass.data[DATA_PLATFORMS_SETUP]["stt_tts_entities_added"] stt_tts_entities_added.set() return True diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 784de14e6ad..b67c1afad71 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -14,7 +14,7 @@ from homeassistant.const import __version__ as HA_VERSION from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_entry_oauth2_flow, event -from .const import DOMAIN +from .const import DATA_CLOUD, DOMAIN DATA_SERVICES = "cloud_account_link_services" CACHE_TIMEOUT = 3600 @@ -68,7 +68,9 @@ async def _get_services(hass: HomeAssistant) -> list[dict[str, Any]]: return services # noqa: RET504 try: - services = await account_link.async_fetch_available_services(hass.data[DOMAIN]) + services = await account_link.async_fetch_available_services( + hass.data[DATA_CLOUD] + ) except (aiohttp.ClientError, TimeoutError): return [] @@ -105,7 +107,7 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement async def async_generate_authorize_url(self, flow_id: str) -> str: """Generate a url for the user to authorize.""" helper = account_link.AuthorizeAccountHelper( - self.hass.data[DOMAIN], self.service + self.hass.data[DATA_CLOUD], self.service ) authorize_url = await helper.async_get_authorize_url() @@ -138,6 +140,6 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement async def _async_refresh_token(self, token: dict) -> dict: """Refresh a token.""" new_token = await account_link.async_fetch_access_token( - self.hass.data[DOMAIN], self.service, token["refresh_token"] + self.hass.data[DATA_CLOUD], self.service, token["refresh_token"] ) return {**token, **new_token} diff --git a/homeassistant/components/cloud/assist_pipeline.py b/homeassistant/components/cloud/assist_pipeline.py index e9d66bdcc1f..f3a591d6eda 100644 --- a/homeassistant/components/cloud/assist_pipeline.py +++ b/homeassistant/components/cloud/assist_pipeline.py @@ -27,7 +27,7 @@ async def async_create_cloud_pipeline(hass: HomeAssistant) -> str | None: """Create a cloud assist pipeline.""" # Wait for stt and tts platforms to set up and entities to be added # before creating the pipeline. - platforms_setup: dict[str, asyncio.Event] = hass.data[DATA_PLATFORMS_SETUP] + platforms_setup = hass.data[DATA_PLATFORMS_SETUP] await asyncio.gather(*(event.wait() for event in platforms_setup.values())) # Make sure the pipeline store is loaded, needed because assist_pipeline # is an after dependency of cloud @@ -91,7 +91,7 @@ async def async_migrate_cloud_pipeline_engine( else: raise ValueError(f"Invalid platform {platform}") - platforms_setup: dict[str, asyncio.Event] = hass.data[DATA_PLATFORMS_SETUP] + platforms_setup = hass.data[DATA_PLATFORMS_SETUP] await platforms_setup[wait_for_platform].wait() # Make sure the pipeline store is loaded, needed because assist_pipeline diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index 0693a8285ce..75cbd3c9f3d 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -18,7 +18,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .client import CloudClient -from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN +from .const import DATA_CLOUD, DISPATCHER_REMOTE_UPDATE WAIT_UNTIL_CHANGE = 3 @@ -29,7 +29,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Home Assistant Cloud binary sensors.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] async_add_entities([CloudRemoteBinary(cloud)]) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 2c58dd57340..5e9fb2e9dc7 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -2,12 +2,22 @@ from __future__ import annotations -from typing import Any +import asyncio +from typing import TYPE_CHECKING, Any +from homeassistant.util.hass_dict import HassKey from homeassistant.util.signal_type import SignalType +if TYPE_CHECKING: + from hass_nabucasa import Cloud + + from .client import CloudClient + DOMAIN = "cloud" -DATA_PLATFORMS_SETUP = "cloud_platforms_setup" +DATA_CLOUD: HassKey[Cloud[CloudClient]] = HassKey(DOMAIN) +DATA_PLATFORMS_SETUP: HassKey[dict[str, asyncio.Event]] = HassKey( + "cloud_platforms_setup" +) REQUEST_TIMEOUT = 10 PREF_ENABLE_ALEXA = "alexa_enabled" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index bd2860b19df..b1931515745 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -38,7 +38,7 @@ from .alexa_config import entity_supported as entity_supported_by_alexa from .assist_pipeline import async_create_cloud_pipeline from .client import CloudClient from .const import ( - DOMAIN, + DATA_CLOUD, PREF_ALEXA_REPORT_STATE, PREF_DISABLE_2FA, PREF_ENABLE_ALEXA, @@ -196,7 +196,7 @@ class GoogleActionsSyncView(HomeAssistantView): async def post(self, request: web.Request) -> web.Response: """Trigger a Google Actions sync.""" hass = request.app[KEY_HASS] - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] gconf = await cloud.client.get_google_config() status = await gconf.async_sync_entities(gconf.agent_user_id) return self.json({}, status_code=status) @@ -216,7 +216,7 @@ class CloudLoginView(HomeAssistantView): async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle login request.""" hass = request.app[KEY_HASS] - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] await cloud.login(data["email"], data["password"]) if "assist_pipeline" in hass.config.components: @@ -237,7 +237,7 @@ class CloudLogoutView(HomeAssistantView): async def post(self, request: web.Request) -> web.Response: """Handle logout request.""" hass = request.app[KEY_HASS] - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] async with asyncio.timeout(REQUEST_TIMEOUT): await cloud.logout() @@ -264,7 +264,7 @@ class CloudRegisterView(HomeAssistantView): async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle registration request.""" hass = request.app[KEY_HASS] - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] client_metadata = None @@ -301,7 +301,7 @@ class CloudResendConfirmView(HomeAssistantView): async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle resending confirm email code request.""" hass = request.app[KEY_HASS] - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] async with asyncio.timeout(REQUEST_TIMEOUT): await cloud.auth.async_resend_email_confirm(data["email"]) @@ -321,7 +321,7 @@ class CloudForgotPasswordView(HomeAssistantView): async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle forgot password request.""" hass = request.app[KEY_HASS] - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] async with asyncio.timeout(REQUEST_TIMEOUT): await cloud.auth.async_forgot_password(data["email"]) @@ -341,7 +341,7 @@ async def websocket_cloud_remove_data( Async friendly. """ - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] if cloud.is_logged_in: connection.send_message( websocket_api.error_message( @@ -367,7 +367,7 @@ async def websocket_cloud_status( Async friendly. """ - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] connection.send_message( websocket_api.result_message(msg["id"], await _account_data(hass, cloud)) ) @@ -391,7 +391,7 @@ def _require_cloud_login( msg: dict[str, Any], ) -> None: """Require to be logged into the cloud.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] if not cloud.is_logged_in: connection.send_message( websocket_api.error_message( @@ -414,7 +414,7 @@ async def websocket_subscription( msg: dict[str, Any], ) -> None: """Handle request for account info.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] if (data := await async_subscription_info(cloud)) is None: connection.send_error( msg["id"], "request_failed", "Failed to request subscription" @@ -457,7 +457,7 @@ async def websocket_update_prefs( msg: dict[str, Any], ) -> None: """Handle request for account info.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] changes = dict(msg) changes.pop("id") @@ -508,7 +508,7 @@ async def websocket_hook_create( msg: dict[str, Any], ) -> None: """Handle request for account info.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] hook = await cloud.cloudhooks.async_create(msg["webhook_id"], False) connection.send_message(websocket_api.result_message(msg["id"], hook)) @@ -528,7 +528,7 @@ async def websocket_hook_delete( msg: dict[str, Any], ) -> None: """Handle request for account info.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] await cloud.cloudhooks.async_delete(msg["webhook_id"]) connection.send_message(websocket_api.result_message(msg["id"])) @@ -597,7 +597,7 @@ async def websocket_remote_connect( msg: dict[str, Any], ) -> None: """Handle request for connect remote.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] await cloud.client.prefs.async_update(remote_enabled=True) connection.send_result(msg["id"], await _account_data(hass, cloud)) @@ -613,7 +613,7 @@ async def websocket_remote_disconnect( msg: dict[str, Any], ) -> None: """Handle request for disconnect remote.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] await cloud.client.prefs.async_update(remote_enabled=False) connection.send_result(msg["id"], await _account_data(hass, cloud)) @@ -634,7 +634,7 @@ async def google_assistant_get( msg: dict[str, Any], ) -> None: """Get data for a single google assistant entity.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] gconf = await cloud.client.get_google_config() entity_id: str = msg["entity_id"] state = hass.states.get(entity_id) @@ -682,7 +682,7 @@ async def google_assistant_list( msg: dict[str, Any], ) -> None: """List all google assistant entities.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] gconf = await cloud.client.get_google_config() entities = google_helpers.async_get_entities(hass, gconf) @@ -774,7 +774,7 @@ async def alexa_list( msg: dict[str, Any], ) -> None: """List all alexa entities.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] alexa_config = await cloud.client.get_alexa_config() entities = alexa_entities.async_get_entities(hass, alexa_config) @@ -800,7 +800,7 @@ async def alexa_sync( msg: dict[str, Any], ) -> None: """Sync with Alexa.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] alexa_config = await cloud.client.get_alexa_config() async with asyncio.timeout(10): @@ -830,7 +830,7 @@ async def thingtalk_convert( msg: dict[str, Any], ) -> None: """Convert a query.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] async with asyncio.timeout(10): try: diff --git a/homeassistant/components/cloud/repairs.py b/homeassistant/components/cloud/repairs.py index 9042a010589..fe418fb5340 100644 --- a/homeassistant/components/cloud/repairs.py +++ b/homeassistant/components/cloud/repairs.py @@ -5,7 +5,6 @@ from __future__ import annotations import asyncio from typing import Any -from hass_nabucasa import Cloud import voluptuous as vol from homeassistant.components.repairs import ( @@ -17,8 +16,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import issue_registry as ir -from .client import CloudClient -from .const import DOMAIN +from .const import DATA_CLOUD, DOMAIN from .subscription import async_migrate_paypal_agreement, async_subscription_info BACKOFF_TIME = 5 @@ -73,7 +71,7 @@ class LegacySubscriptionRepairFlow(RepairsFlow): async def async_step_change_plan(self, _: None = None) -> FlowResult: """Wait for the user to authorize the app installation.""" - cloud: Cloud[CloudClient] = self.hass.data[DOMAIN] + cloud = self.hass.data[DATA_CLOUD] async def _async_wait_for_plan_change() -> None: flow_manager = repairs_flow_manager(self.hass) diff --git a/homeassistant/components/cloud/stt.py b/homeassistant/components/cloud/stt.py index c68e9f245ee..b2154448d3a 100644 --- a/homeassistant/components/cloud/stt.py +++ b/homeassistant/components/cloud/stt.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from collections.abc import AsyncIterable import logging @@ -28,7 +27,7 @@ from homeassistant.setup import async_when_setup from .assist_pipeline import async_migrate_cloud_pipeline_engine from .client import CloudClient -from .const import DATA_PLATFORMS_SETUP, DOMAIN, STT_ENTITY_UNIQUE_ID +from .const import DATA_CLOUD, DATA_PLATFORMS_SETUP, STT_ENTITY_UNIQUE_ID _LOGGER = logging.getLogger(__name__) @@ -39,9 +38,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Home Assistant Cloud speech platform via config entry.""" - stt_platform_loaded: asyncio.Event = hass.data[DATA_PLATFORMS_SETUP][Platform.STT] + stt_platform_loaded = hass.data[DATA_PLATFORMS_SETUP][Platform.STT] stt_platform_loaded.set() - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] async_add_entities([CloudProviderEntity(cloud)]) diff --git a/homeassistant/components/cloud/system_health.py b/homeassistant/components/cloud/system_health.py index 866626f4c79..0e65aa93eaf 100644 --- a/homeassistant/components/cloud/system_health.py +++ b/homeassistant/components/cloud/system_health.py @@ -2,13 +2,10 @@ from typing import Any -from hass_nabucasa import Cloud - from homeassistant.components import system_health from homeassistant.core import HomeAssistant, callback -from .client import CloudClient -from .const import DOMAIN +from .const import DATA_CLOUD @callback @@ -21,7 +18,7 @@ def async_register( async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: """Get info for the info page.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] client = cloud.client data: dict[str, Any] = { diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 53cec74d133..8cf18c08314 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio import logging from typing import Any @@ -31,7 +30,7 @@ from homeassistant.setup import async_when_setup from .assist_pipeline import async_migrate_cloud_pipeline_engine from .client import CloudClient -from .const import DATA_PLATFORMS_SETUP, DOMAIN, TTS_ENTITY_UNIQUE_ID +from .const import DATA_CLOUD, DATA_PLATFORMS_SETUP, DOMAIN, TTS_ENTITY_UNIQUE_ID from .prefs import CloudPreferences ATTR_GENDER = "gender" @@ -97,7 +96,7 @@ async def async_get_engine( discovery_info: DiscoveryInfoType | None = None, ) -> CloudProvider: """Set up Cloud speech component.""" - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] cloud_provider = CloudProvider(cloud) if discovery_info is not None: discovery_info["platform_loaded"].set() @@ -110,9 +109,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Home Assistant Cloud text-to-speech platform.""" - tts_platform_loaded: asyncio.Event = hass.data[DATA_PLATFORMS_SETUP][Platform.TTS] + tts_platform_loaded = hass.data[DATA_PLATFORMS_SETUP][Platform.TTS] tts_platform_loaded.set() - cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] async_add_entities([CloudTTSEntity(cloud)]) diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index d527cbbeec2..82280336a8c 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -2,10 +2,9 @@ from unittest.mock import AsyncMock, patch -from hass_nabucasa import Cloud - from homeassistant.components import cloud from homeassistant.components.cloud import const, prefs as cloud_prefs +from homeassistant.components.cloud.const import DATA_CLOUD from homeassistant.setup import async_setup_component PIPELINE_DATA = { @@ -64,7 +63,7 @@ async def mock_cloud(hass, config=None): assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, cloud.DOMAIN, {"cloud": config or {}}) - cloud_inst: Cloud = hass.data["cloud"] + cloud_inst = hass.data[DATA_CLOUD] with patch("hass_nabucasa.Cloud.run_executor", AsyncMock(return_value=None)): await cloud_inst.initialize() @@ -79,5 +78,5 @@ def mock_cloud_prefs(hass, prefs): const.PREF_GOOGLE_SETTINGS_VERSION: cloud_prefs.GOOGLE_SETTINGS_VERSION, } prefs_to_set.update(prefs) - hass.data[cloud.DOMAIN].client._prefs._prefs = prefs_to_set - return hass.data[cloud.DOMAIN].client._prefs + hass.data[DATA_CLOUD].client._prefs._prefs = prefs_to_set + return hass.data[DATA_CLOUD].client._prefs diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index ebd9ea6663e..3058718551e 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -17,7 +17,8 @@ import jwt import pytest from typing_extensions import AsyncGenerator -from homeassistant.components.cloud import CloudClient, const, prefs +from homeassistant.components.cloud import CloudClient, prefs +from homeassistant.components.cloud.const import DATA_CLOUD from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -223,7 +224,7 @@ async def mock_cloud_setup(hass): @pytest.fixture def mock_cloud_login(hass, mock_cloud_setup): """Mock cloud is logged in.""" - hass.data[const.DOMAIN].id_token = jwt.encode( + hass.data[DATA_CLOUD].id_token = jwt.encode( { "email": "hello@home-assistant.io", "custom:sub-exp": "2300-01-03", @@ -231,7 +232,7 @@ def mock_cloud_login(hass, mock_cloud_setup): }, "test", ) - with patch.object(hass.data[const.DOMAIN].auth, "async_check_token"): + with patch.object(hass.data[DATA_CLOUD].auth, "async_check_token"): yield @@ -248,7 +249,7 @@ def mock_auth_fixture(): @pytest.fixture def mock_expired_cloud_login(hass, mock_cloud_setup): """Mock cloud is logged in.""" - hass.data[const.DOMAIN].id_token = jwt.encode( + hass.data[DATA_CLOUD].id_token = jwt.encode( { "email": "hello@home-assistant.io", "custom:sub-exp": "2018-01-01", diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 3f108961bc5..7a85531904a 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -9,6 +9,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.cloud import account_link +from homeassistant.components.cloud.const import DATA_CLOUD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow @@ -133,7 +134,7 @@ async def test_setup_provide_implementation(hass: HomeAssistant) -> None: async def test_get_services_cached(hass: HomeAssistant) -> None: """Test that we cache services.""" - hass.data["cloud"] = None + hass.data[DATA_CLOUD] = None services = 1 @@ -165,7 +166,7 @@ async def test_get_services_cached(hass: HomeAssistant) -> None: async def test_get_services_error(hass: HomeAssistant) -> None: """Test that we cache services.""" - hass.data["cloud"] = None + hass.data[DATA_CLOUD] = None with ( patch.object(account_link, "CACHE_TIMEOUT", 0), @@ -181,7 +182,7 @@ async def test_get_services_error(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("current_request_with_host") async def test_implementation(hass: HomeAssistant, flow_handler) -> None: """Test Cloud OAuth2 implementation.""" - hass.data["cloud"] = None + hass.data[DATA_CLOUD] = None impl = account_link.CloudOAuth2Implementation(hass, "test") assert impl.name == "Home Assistant Cloud" diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index f37ee114220..e4ad425d4d4 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components.alexa import errors from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config from homeassistant.components.cloud.const import ( + DATA_CLOUD, PREF_ALEXA_DEFAULT_EXPOSE, PREF_ALEXA_ENTITY_CONFIGS, PREF_SHOULD_EXPOSE, @@ -425,7 +426,7 @@ async def test_alexa_entity_registry_sync( expose_new(hass, True) await alexa_config.CloudAlexaConfig( - hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ).async_initialize() with patch_sync_helper() as (to_update, to_remove): @@ -506,11 +507,11 @@ def test_enabled_requires_valid_sub( ) -> None: """Test that alexa config enabled requires a valid Cloud sub.""" assert cloud_prefs.alexa_enabled - assert hass.data["cloud"].is_logged_in - assert hass.data["cloud"].subscription_expired + assert hass.data[DATA_CLOUD].is_logged_in + assert hass.data[DATA_CLOUD].subscription_expired config = alexa_config.CloudAlexaConfig( - hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ) assert not config.enabled diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 3126d56e3fb..62af4e88857 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -15,6 +15,7 @@ from homeassistant.components.cloud.client import ( CloudClient, ) from homeassistant.components.cloud.const import ( + DATA_CLOUD, PREF_ALEXA_REPORT_STATE, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, @@ -63,7 +64,7 @@ async def test_handler_alexa(hass: HomeAssistant) -> None: ) mock_cloud_prefs(hass, {PREF_ALEXA_REPORT_STATE: False}) - cloud = hass.data["cloud"] + cloud = hass.data[DATA_CLOUD] resp = await cloud.client.async_alexa_message( test_alexa.get_new_request("Alexa.Discovery", "Discover") @@ -83,7 +84,7 @@ async def test_handler_alexa(hass: HomeAssistant) -> None: async def test_handler_alexa_disabled(hass: HomeAssistant, mock_cloud_fixture) -> None: """Test handler Alexa when user has disabled it.""" mock_cloud_fixture._prefs[PREF_ENABLE_ALEXA] = False - cloud = hass.data["cloud"] + cloud = hass.data[DATA_CLOUD] resp = await cloud.client.async_alexa_message( test_alexa.get_new_request("Alexa.Discovery", "Discover") @@ -117,7 +118,7 @@ async def test_handler_google_actions(hass: HomeAssistant) -> None: ) mock_cloud_prefs(hass, {}) - cloud = hass.data["cloud"] + cloud = hass.data[DATA_CLOUD] reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} @@ -164,7 +165,7 @@ async def test_handler_google_actions_disabled( reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": intent}]} - cloud = hass.data["cloud"] + cloud = hass.data[DATA_CLOUD] with patch( "hass_nabucasa.Cloud._decode_claims", return_value={"cognito:username": "myUserName"}, @@ -182,7 +183,7 @@ async def test_webhook_msg( with patch("hass_nabucasa.Cloud.initialize"): setup = await async_setup_component(hass, "cloud", {"cloud": {}}) assert setup - cloud = hass.data["cloud"] + cloud = hass.data[DATA_CLOUD] await cloud.client.prefs.async_initialize() await cloud.client.prefs.async_update( @@ -269,7 +270,7 @@ async def test_google_config_expose_entity( "light", "test", "unique", suggested_object_id="kitchen" ) - cloud_client = hass.data[DOMAIN].client + cloud_client = hass.data[DATA_CLOUD].client state = State(entity_entry.entity_id, "on") gconf = await cloud_client.get_google_config() @@ -293,7 +294,7 @@ async def test_google_config_should_2fa( "light", "test", "unique", suggested_object_id="kitchen" ) - cloud_client = hass.data[DOMAIN].client + cloud_client = hass.data[DATA_CLOUD].client gconf = await cloud_client.get_google_config() state = State(entity_entry.entity_id, "on") @@ -350,7 +351,7 @@ async def test_system_msg(hass: HomeAssistant) -> None: with patch("hass_nabucasa.Cloud.initialize"): setup = await async_setup_component(hass, "cloud", {"cloud": {}}) assert setup - cloud = hass.data["cloud"] + cloud = hass.data[DATA_CLOUD] assert cloud.client.relayer_region is None @@ -373,7 +374,7 @@ async def test_cloud_connection_info(hass: HomeAssistant) -> None: hexmock.return_value = "12345678901234567890" setup = await async_setup_component(hass, "cloud", {"cloud": {}}) assert setup - cloud = hass.data["cloud"] + cloud = hass.data[DATA_CLOUD] response = await cloud.client.async_cloud_connection_info({}) diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 89882d92037..40d3f6ef2c5 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components.cloud import GACTIONS_SCHEMA from homeassistant.components.cloud.const import ( + DATA_CLOUD, PREF_DISABLE_2FA, PREF_GOOGLE_DEFAULT_EXPOSE, PREF_GOOGLE_ENTITY_CONFIGS, @@ -196,7 +197,7 @@ async def test_google_entity_registry_sync( expose_new(hass, True) config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ) await config.async_initialize() await config.async_connect_agent_user("mock-user-id") @@ -264,7 +265,7 @@ async def test_google_device_registry_sync( ) -> None: """Test Google config responds to device registry.""" config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ) # Enable exposing new entities to Google @@ -333,7 +334,7 @@ async def test_sync_google_when_started( ) -> None: """Test Google config syncs on init.""" config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ) with patch.object(config, "async_sync_entities_all") as mock_sync: await config.async_initialize() @@ -346,7 +347,7 @@ async def test_sync_google_on_home_assistant_start( ) -> None: """Test Google config syncs when home assistant started.""" config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ) hass.set_state(CoreState.not_running) with patch.object(config, "async_sync_entities_all") as mock_sync: @@ -441,11 +442,11 @@ def test_enabled_requires_valid_sub( ) -> None: """Test that google config enabled requires a valid Cloud sub.""" assert cloud_prefs.google_enabled - assert hass.data["cloud"].is_logged_in - assert hass.data["cloud"].subscription_expired + assert hass.data[DATA_CLOUD].is_logged_in + assert hass.data[DATA_CLOUD].subscription_expired config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ) assert not config.enabled @@ -494,7 +495,7 @@ async def test_google_handle_logout( await cloud_prefs.get_cloud_user() with patch.object( - hass.data["cloud"].auth, + hass.data[DATA_CLOUD].auth, "async_check_token", side_effect=AssertionError("Should not be called"), ): @@ -857,7 +858,7 @@ async def test_google_config_get_agent_user_id( ) -> None: """Test overridden get_agent_user_id_from_webhook method.""" config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data[DATA_CLOUD] ) assert ( config.get_agent_user_id_from_webhook(cloud_prefs.google_local_webhook_id) diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 9cc1324ebc1..db8253b0329 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -4,7 +4,6 @@ from collections.abc import Callable, Coroutine from typing import Any from unittest.mock import MagicMock, patch -from hass_nabucasa import Cloud import pytest from homeassistant.components import cloud @@ -13,7 +12,7 @@ from homeassistant.components.cloud import ( CloudNotConnected, async_get_or_create_cloudhook, ) -from homeassistant.components.cloud.const import DOMAIN, PREF_CLOUDHOOKS +from homeassistant.components.cloud.const import DATA_CLOUD, DOMAIN, PREF_CLOUDHOOKS from homeassistant.components.cloud.prefs import STORAGE_KEY from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Context, HomeAssistant @@ -47,7 +46,7 @@ async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None: ) assert result - cl = hass.data["cloud"] + cl = hass.data[DATA_CLOUD] assert cl.mode == cloud.MODE_DEV assert cl.cognito_client_id == "test-cognito_client_id" assert cl.user_pool_id == "test-user_pool_id" @@ -65,7 +64,7 @@ async def test_remote_services( hass: HomeAssistant, mock_cloud_fixture, hass_read_only_user: MockUser ) -> None: """Setup cloud component and test services.""" - cloud = hass.data[DOMAIN] + cloud = hass.data[DATA_CLOUD] assert hass.services.has_service(DOMAIN, "remote_connect") assert hass.services.has_service(DOMAIN, "remote_disconnect") @@ -145,7 +144,7 @@ async def test_setup_existing_cloud_user( async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None: """Test cloud on connect triggers.""" - cl: Cloud[cloud.client.CloudClient] = hass.data["cloud"] + cl = hass.data[DATA_CLOUD] assert len(cl.iot._on_connect) == 3 @@ -202,7 +201,7 @@ async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None: async def test_remote_ui_url(hass: HomeAssistant, mock_cloud_fixture) -> None: """Test getting remote ui url.""" - cl = hass.data["cloud"] + cl = hass.data[DATA_CLOUD] # Not logged in with pytest.raises(cloud.CloudNotAvailable): From bbb8bb31f9cb4bdc2220635391cb4fdd1e91e9cd Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 24 Jun 2024 21:03:41 +0200 Subject: [PATCH 0091/2411] Only raise Reolink re-auth flow when login fails 3 consecutive times (#120291) --- homeassistant/components/reolink/__init__.py | 13 +++++-- homeassistant/components/reolink/host.py | 2 ++ tests/components/reolink/test_init.py | 38 ++++++++++++++++++-- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 27bd504e9bb..a3e49f1f526 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -38,6 +38,7 @@ PLATFORMS = [ ] DEVICE_UPDATE_INTERVAL = timedelta(seconds=60) FIRMWARE_UPDATE_INTERVAL = timedelta(hours=12) +NUM_CRED_ERRORS = 3 @dataclass @@ -82,10 +83,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: await host.update_states() except CredentialsInvalidError as err: - await host.stop() - raise ConfigEntryAuthFailed(err) from err - except ReolinkError as err: + host.credential_errors += 1 + if host.credential_errors >= NUM_CRED_ERRORS: + await host.stop() + raise ConfigEntryAuthFailed(err) from err raise UpdateFailed(str(err)) from err + except ReolinkError as err: + host.credential_errors = 0 + raise UpdateFailed(str(err)) from err + + host.credential_errors = 0 async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): await host.renew() diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index c69a80ce972..bccb5c5b684 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -79,6 +79,8 @@ class ReolinkHost: ) self.firmware_ch_list: list[int | None] = [] + self.credential_errors: int = 0 + self.webhook_id: str | None = None self._onvif_push_supported: bool = True self._onvif_long_poll_supported: bool = True diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 466836e52ef..922fe0829f6 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -7,11 +7,16 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError -from homeassistant.components.reolink import FIRMWARE_UPDATE_INTERVAL, const +from homeassistant.components.reolink import ( + DEVICE_UPDATE_INTERVAL, + FIRMWARE_UPDATE_INTERVAL, + NUM_CRED_ERRORS, + const, +) from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.helpers import ( device_registry as dr, entity_registry as er, @@ -58,7 +63,7 @@ pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") ConfigEntryState.SETUP_RETRY, ), ( - "get_states", + "get_host_data", AsyncMock(side_effect=CredentialsInvalidError("Test error")), ConfigEntryState.SETUP_ERROR, ), @@ -113,6 +118,33 @@ async def test_firmware_error_twice( assert hass.states.is_state(entity_id, STATE_UNAVAILABLE) +async def test_credential_error_three( + hass: HomeAssistant, + reolink_connect: MagicMock, + config_entry: MockConfigEntry, + issue_registry: ir.IssueRegistry, +) -> None: + """Test when the update gives credential error 3 times.""" + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + reolink_connect.get_states = AsyncMock( + side_effect=CredentialsInvalidError("Test error") + ) + + issue_id = f"config_entry_reauth_{const.DOMAIN}_{config_entry.entry_id}" + for _ in range(NUM_CRED_ERRORS): + assert (HA_DOMAIN, issue_id) not in issue_registry.issues + async_fire_time_changed( + hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30) + ) + await hass.async_block_till_done() + + assert (HA_DOMAIN, issue_id) in issue_registry.issues + + async def test_entry_reloading( hass: HomeAssistant, config_entry: MockConfigEntry, From 72d1b3e36093e2e93a97f41392d5891a5255475c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jun 2024 21:05:23 +0200 Subject: [PATCH 0092/2411] Deprecate Nanoleaf gesture device trigger (#120078) --- homeassistant/components/nanoleaf/device_trigger.py | 10 ++++++++++ homeassistant/components/nanoleaf/strings.json | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/homeassistant/components/nanoleaf/device_trigger.py b/homeassistant/components/nanoleaf/device_trigger.py index 15b14e9719e..b4049f2199d 100644 --- a/homeassistant/components/nanoleaf/device_trigger.py +++ b/homeassistant/components/nanoleaf/device_trigger.py @@ -16,6 +16,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType @@ -60,6 +61,15 @@ async def async_attach_trigger( trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_device_trigger_nanoleaf", + is_fixable=False, + breaks_in_ha_version="2025.1.0", + severity=IssueSeverity.WARNING, + translation_key="deprecated_device_trigger", + ) event_config = event_trigger.TRIGGER_SCHEMA( { event_trigger.CONF_PLATFORM: CONF_EVENT, diff --git a/homeassistant/components/nanoleaf/strings.json b/homeassistant/components/nanoleaf/strings.json index 40cd7294ec3..ef7df8c0ab5 100644 --- a/homeassistant/components/nanoleaf/strings.json +++ b/homeassistant/components/nanoleaf/strings.json @@ -52,5 +52,11 @@ } } } + }, + "issues": { + "deprecated_device_trigger": { + "title": "Nanoleaf device trigger is deprecated", + "description": "The Nanoleaf device trigger is deprecated and will be removed in a future release. You can now use the gesture event entity to build automations." + } } } From d0961ca473a099b88e353ba6dcd6b71c7bb1fc0f Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Mon, 24 Jun 2024 21:06:57 +0200 Subject: [PATCH 0093/2411] Make Bang & Olufsen products ignore .m3u media source files (#120317) --- homeassistant/components/bang_olufsen/media_player.py | 7 +++++-- homeassistant/components/bang_olufsen/strings.json | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 5c214a3fb17..d23c75046ff 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -45,6 +45,7 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODEL from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -572,9 +573,11 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): media_id = async_process_play_media_url(self.hass, sourced_media.url) - # Remove playlist extension as it is unsupported. + # Exit if the source uses unsupported file. if media_id.endswith(".m3u"): - media_id = media_id.replace(".m3u", "") + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="m3u_invalid_format" + ) if announce: extra = kwargs.get(ATTR_MEDIA_EXTRA, {}) diff --git a/homeassistant/components/bang_olufsen/strings.json b/homeassistant/components/bang_olufsen/strings.json index 3cebfb891bc..93b55cf0db2 100644 --- a/homeassistant/components/bang_olufsen/strings.json +++ b/homeassistant/components/bang_olufsen/strings.json @@ -24,5 +24,10 @@ "description": "Confirm the configuration of the {model}-{serial_number} @ {host}." } } + }, + "exceptions": { + "m3u_invalid_format": { + "message": "Media sources with the .m3u extension are not supported." + } } } From 6d917f0242f35e487358fda4d2090501056bdc9e Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 24 Jun 2024 15:21:51 -0500 Subject: [PATCH 0094/2411] Don't run timer callbacks for delayed commands (#120367) * Don't send timer events for delayed commands * Don't run timer callbacks for delayed commands --- homeassistant/components/intent/timers.py | 28 +++++++++++------------ tests/components/intent/test_timers.py | 18 +++++---------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/intent/timers.py b/homeassistant/components/intent/timers.py index cddfce55b9f..40b55134e92 100644 --- a/homeassistant/components/intent/timers.py +++ b/homeassistant/components/intent/timers.py @@ -278,7 +278,7 @@ class TimerManager: name=f"Timer {timer_id}", ) - if timer.device_id in self.handlers: + if (not timer.conversation_command) and (timer.device_id in self.handlers): self.handlers[timer.device_id](TimerEventType.STARTED, timer) _LOGGER.debug( "Timer started: id=%s, name=%s, hours=%s, minutes=%s, seconds=%s, device_id=%s", @@ -317,7 +317,7 @@ class TimerManager: timer.cancel() - if timer.device_id in self.handlers: + if (not timer.conversation_command) and (timer.device_id in self.handlers): self.handlers[timer.device_id](TimerEventType.CANCELLED, timer) _LOGGER.debug( "Timer cancelled: id=%s, name=%s, seconds_left=%s, device_id=%s", @@ -346,7 +346,7 @@ class TimerManager: name=f"Timer {timer_id}", ) - if timer.device_id in self.handlers: + if (not timer.conversation_command) and (timer.device_id in self.handlers): self.handlers[timer.device_id](TimerEventType.UPDATED, timer) if seconds > 0: @@ -384,7 +384,7 @@ class TimerManager: task = self.timer_tasks.pop(timer_id) task.cancel() - if timer.device_id in self.handlers: + if (not timer.conversation_command) and (timer.device_id in self.handlers): self.handlers[timer.device_id](TimerEventType.UPDATED, timer) _LOGGER.debug( "Timer paused: id=%s, name=%s, seconds_left=%s, device_id=%s", @@ -410,7 +410,7 @@ class TimerManager: name=f"Timer {timer.id}", ) - if timer.device_id in self.handlers: + if (not timer.conversation_command) and (timer.device_id in self.handlers): self.handlers[timer.device_id](TimerEventType.UPDATED, timer) _LOGGER.debug( "Timer unpaused: id=%s, name=%s, seconds_left=%s, device_id=%s", @@ -426,15 +426,6 @@ class TimerManager: timer.finish() - if timer.device_id in self.handlers: - self.handlers[timer.device_id](TimerEventType.FINISHED, timer) - _LOGGER.debug( - "Timer finished: id=%s, name=%s, device_id=%s", - timer_id, - timer.name, - timer.device_id, - ) - if timer.conversation_command: # pylint: disable-next=import-outside-toplevel from homeassistant.components.conversation import async_converse @@ -451,6 +442,15 @@ class TimerManager: ), "timer assist command", ) + elif timer.device_id in self.handlers: + self.handlers[timer.device_id](TimerEventType.FINISHED, timer) + + _LOGGER.debug( + "Timer finished: id=%s, name=%s, device_id=%s", + timer_id, + timer.name, + timer.device_id, + ) def is_timer_device(self, device_id: str) -> bool: """Return True if device has been registered to handle timer events.""" diff --git a/tests/components/intent/test_timers.py b/tests/components/intent/test_timers.py index a884fd13de5..329db6e8b2b 100644 --- a/tests/components/intent/test_timers.py +++ b/tests/components/intent/test_timers.py @@ -1430,18 +1430,10 @@ async def test_start_timer_with_conversation_command( timer_name = "test timer" test_command = "turn on the lights" agent_id = "test_agent" - finished_event = asyncio.Event() - @callback - def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None: - if event_type == TimerEventType.FINISHED: - assert timer.conversation_command == test_command - assert timer.conversation_agent_id == agent_id - finished_event.set() + mock_handle_timer = MagicMock() + async_register_timer_handler(hass, device_id, mock_handle_timer) - async_register_timer_handler(hass, device_id, handle_timer) - - # Device id is required if no conversation command timer_manager = TimerManager(hass) with pytest.raises(ValueError): timer_manager.start_timer( @@ -1468,9 +1460,11 @@ async def test_start_timer_with_conversation_command( assert result.response_type == intent.IntentResponseType.ACTION_DONE - async with asyncio.timeout(1): - await finished_event.wait() + # No timer events for delayed commands + mock_handle_timer.assert_not_called() + # Wait for process service call to finish + await hass.async_block_till_done() mock_converse.assert_called_once() assert mock_converse.call_args.args[1] == test_command From 1e16afb43b3a38cd82375054b95e62835a5bf818 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 24 Jun 2024 16:03:34 -0500 Subject: [PATCH 0095/2411] Fix pylint error in Google generative AI tests (#120371) * Fix pylint error * Add second fix --- .../google_generative_ai_conversation/test_config_flow.py | 2 +- tests/components/google_generative_ai_conversation/test_init.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/google_generative_ai_conversation/test_config_flow.py b/tests/components/google_generative_ai_conversation/test_config_flow.py index 24ed06a408f..c835a4d3b13 100644 --- a/tests/components/google_generative_ai_conversation/test_config_flow.py +++ b/tests/components/google_generative_ai_conversation/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, Mock, patch from google.api_core.exceptions import ClientError, DeadlineExceeded -from google.rpc.error_details_pb2 import ErrorInfo +from google.rpc.error_details_pb2 import ErrorInfo # pylint: disable=no-name-in-module import pytest from homeassistant import config_entries diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index 7afa9b4a31e..eeaa777f614 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from google.api_core.exceptions import ClientError, DeadlineExceeded -from google.rpc.error_details_pb2 import ErrorInfo +from google.rpc.error_details_pb2 import ErrorInfo # pylint: disable=no-name-in-module import pytest from syrupy.assertion import SnapshotAssertion From f1ddf80dff8644b4aab33cf966896faf0673bb47 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 03:34:23 +0200 Subject: [PATCH 0096/2411] Fix dlna_dms test RuntimeWarning (#120341) --- tests/components/dlna_dms/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/dlna_dms/conftest.py b/tests/components/dlna_dms/conftest.py index 1fa56f4bc24..ed05dfa4c76 100644 --- a/tests/components/dlna_dms/conftest.py +++ b/tests/components/dlna_dms/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import AsyncIterable, Iterable from typing import Final, cast -from unittest.mock import Mock, create_autospec, patch, seal +from unittest.mock import AsyncMock, MagicMock, Mock, create_autospec, patch, seal from async_upnp_client.client import UpnpDevice, UpnpService from async_upnp_client.utils import absolute_url @@ -87,6 +87,8 @@ def aiohttp_session_requester_mock() -> Iterable[Mock]: with patch( "homeassistant.components.dlna_dms.dms.AiohttpSessionRequester", autospec=True ) as requester_mock: + requester_mock.return_value = mock = AsyncMock() + mock.async_http_request.return_value.body = MagicMock() yield requester_mock From 59080a3a6fa3befd25dc6c8f7502f8da90631afb Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Tue, 25 Jun 2024 08:00:19 +0200 Subject: [PATCH 0097/2411] Strip whitespace characters from token in One-Time-Passwort (OTP) integration (#120380) --- homeassistant/components/otp/config_flow.py | 2 ++ tests/components/otp/test_config_flow.py | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/otp/config_flow.py b/homeassistant/components/otp/config_flow.py index 15d04c910ad..6aa4532683a 100644 --- a/homeassistant/components/otp/config_flow.py +++ b/homeassistant/components/otp/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations import binascii import logging +from re import sub from typing import Any import pyotp @@ -47,6 +48,7 @@ class TOTPConfigFlow(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: if user_input.get(CONF_TOKEN) and not user_input.get(CONF_NEW_TOKEN): + user_input[CONF_TOKEN] = sub(r"\s+", "", user_input[CONF_TOKEN]) try: await self.hass.async_add_executor_job( pyotp.TOTP(user_input[CONF_TOKEN]).now diff --git a/tests/components/otp/test_config_flow.py b/tests/components/otp/test_config_flow.py index eefb1a6f4e0..f9fac433ff9 100644 --- a/tests/components/otp/test_config_flow.py +++ b/tests/components/otp/test_config_flow.py @@ -12,6 +12,10 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType TEST_DATA = { + CONF_NAME: "OTP Sensor", + CONF_TOKEN: "2FX5 FBSY RE6V EC2F SHBQ CRKO 2GND VZ52", +} +TEST_DATA_RESULT = { CONF_NAME: "OTP Sensor", CONF_TOKEN: "2FX5FBSYRE6VEC2FSHBQCRKO2GNDVZ52", } @@ -41,7 +45,11 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result["flow_id"], TEST_DATA, ) - await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "OTP Sensor" + assert result["data"] == TEST_DATA_RESULT + assert len(mock_setup_entry.mock_calls) == 1 @pytest.mark.parametrize( @@ -85,7 +93,7 @@ async def test_errors_and_recover( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "OTP Sensor" - assert result["data"] == TEST_DATA + assert result["data"] == TEST_DATA_RESULT assert len(mock_setup_entry.mock_calls) == 1 @@ -96,13 +104,13 @@ async def test_flow_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, - data=TEST_DATA, + data=TEST_DATA_RESULT, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "OTP Sensor" - assert result["data"] == TEST_DATA + assert result["data"] == TEST_DATA_RESULT @pytest.mark.usefixtures("mock_pyotp") @@ -134,7 +142,7 @@ async def test_generate_new_token( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "OTP Sensor" - assert result["data"] == TEST_DATA + assert result["data"] == TEST_DATA_RESULT assert len(mock_setup_entry.mock_calls) == 1 @@ -181,5 +189,5 @@ async def test_generate_new_token_errors( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "OTP Sensor" - assert result["data"] == TEST_DATA + assert result["data"] == TEST_DATA_RESULT assert len(mock_setup_entry.mock_calls) == 1 From aa8427abe507687e1395d1e9f2c2404027ba4dd4 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Tue, 25 Jun 2024 08:02:02 +0200 Subject: [PATCH 0098/2411] Bump Bang & Olufsen mozart-open-api to 3.4.1.8.6 fixing blocking IO call (#120369) Co-authored-by: J. Nick Koston --- homeassistant/components/bang_olufsen/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bang_olufsen/manifest.json b/homeassistant/components/bang_olufsen/manifest.json index f2b31293227..3cc9fdb5cd1 100644 --- a/homeassistant/components/bang_olufsen/manifest.json +++ b/homeassistant/components/bang_olufsen/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/bang_olufsen", "integration_type": "device", "iot_class": "local_push", - "requirements": ["mozart-api==3.4.1.8.5"], + "requirements": ["mozart-api==3.4.1.8.6"], "zeroconf": ["_bangolufsen._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index e21548fce85..87d0bfd84c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1359,7 +1359,7 @@ motionblindsble==0.1.0 motioneye-client==0.3.14 # homeassistant.components.bang_olufsen -mozart-api==3.4.1.8.5 +mozart-api==3.4.1.8.6 # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2dd55cd9f7c..4890b8bdf3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1107,7 +1107,7 @@ motionblindsble==0.1.0 motioneye-client==0.3.14 # homeassistant.components.bang_olufsen -mozart-api==3.4.1.8.5 +mozart-api==3.4.1.8.6 # homeassistant.components.mullvad mullvad-api==1.0.0 From 59998bc48a9d805adc2bc041ab1b3e51715d279e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 25 Jun 2024 08:02:57 +0200 Subject: [PATCH 0099/2411] Use runtime_data in github (#120362) --- homeassistant/components/github/__init__.py | 18 +++++++++--------- homeassistant/components/github/diagnostics.py | 5 +---- homeassistant/components/github/sensor.py | 6 +++--- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 20df559b819..74575e38e09 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -19,10 +19,11 @@ from .coordinator import GitHubDataUpdateCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up GitHub from a config entry.""" - hass.data.setdefault(DOMAIN, {}) +type GithubConfigEntry = ConfigEntry[dict[str, GitHubDataUpdateCoordinator]] + +async def async_setup_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> bool: + """Set up GitHub from a config entry.""" client = GitHubAPI( token=entry.data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass), @@ -31,6 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: repositories: list[str] = entry.options[CONF_REPOSITORIES] + entry.runtime_data = {} for repository in repositories: coordinator = GitHubDataUpdateCoordinator( hass=hass, @@ -43,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not entry.pref_disable_polling: await coordinator.subscribe() - hass.data[DOMAIN][repository] = coordinator + entry.runtime_data[repository] = coordinator async_cleanup_device_registry(hass=hass, entry=entry) @@ -81,15 +83,13 @@ def async_cleanup_device_registry( break -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> bool: """Unload a config entry.""" - repositories: dict[str, GitHubDataUpdateCoordinator] = hass.data[DOMAIN] + repositories = entry.runtime_data for coordinator in repositories.values(): coordinator.unsubscribe() - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data.pop(DOMAIN) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/github/diagnostics.py b/homeassistant/components/github/diagnostics.py index df1e4b4a4cf..8d2d496a813 100644 --- a/homeassistant/components/github/diagnostics.py +++ b/homeassistant/components/github/diagnostics.py @@ -14,9 +14,6 @@ from homeassistant.helpers.aiohttp_client import ( async_get_clientsession, ) -from .const import DOMAIN -from .coordinator import GitHubDataUpdateCoordinator - async def async_get_config_entry_diagnostics( hass: HomeAssistant, @@ -37,7 +34,7 @@ async def async_get_config_entry_diagnostics( else: data["rate_limit"] = rate_limit_response.data.as_dict - repositories: dict[str, GitHubDataUpdateCoordinator] = hass.data[DOMAIN] + repositories = config_entry.runtime_data data["repositories"] = {} for repository, coordinator in repositories.items(): diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index a082f888767..9a2b5ef5ac4 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -11,7 +11,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo @@ -19,6 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import GithubConfigEntry from .const import DOMAIN from .coordinator import GitHubDataUpdateCoordinator @@ -145,11 +145,11 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GithubConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up GitHub sensor based on a config entry.""" - repositories: dict[str, GitHubDataUpdateCoordinator] = hass.data[DOMAIN] + repositories = entry.runtime_data async_add_entities( ( GitHubSensorEntity(coordinator, description) From adc074f60adc55926faab970b7913e2e76bb7106 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Tue, 25 Jun 2024 02:03:20 -0400 Subject: [PATCH 0100/2411] Remove humbertogontijo as Codeowner for Roborock (#120336) --- CODEOWNERS | 4 ++-- homeassistant/components/roborock/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9b23b5cc83a..2e954ed1315 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1185,8 +1185,8 @@ build.json @home-assistant/supervisor /tests/components/rituals_perfume_genie/ @milanmeu @frenck /homeassistant/components/rmvtransport/ @cgtobi /tests/components/rmvtransport/ @cgtobi -/homeassistant/components/roborock/ @humbertogontijo @Lash-L -/tests/components/roborock/ @humbertogontijo @Lash-L +/homeassistant/components/roborock/ @Lash-L +/tests/components/roborock/ @Lash-L /homeassistant/components/roku/ @ctalkington /tests/components/roku/ @ctalkington /homeassistant/components/romy/ @xeniter diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 42c0f9ba347..51b1835247f 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -1,7 +1,7 @@ { "domain": "roborock", "name": "Roborock", - "codeowners": ["@humbertogontijo", "@Lash-L"], + "codeowners": ["@Lash-L"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", From fd0fee1900bb50b9ad2198694eef98aa55d5f46c Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Tue, 25 Jun 2024 08:09:54 +0200 Subject: [PATCH 0101/2411] Add button platform to pyLoad integration (#120359) --- homeassistant/components/pyload/__init__.py | 2 +- homeassistant/components/pyload/button.py | 107 ++++++++++ homeassistant/components/pyload/const.py | 3 + homeassistant/components/pyload/icons.json | 14 ++ homeassistant/components/pyload/strings.json | 14 ++ .../pyload/snapshots/test_button.ambr | 185 ++++++++++++++++++ tests/components/pyload/test_button.py | 83 ++++++++ tests/components/pyload/test_sensor.py | 14 +- 8 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/pyload/button.py create mode 100644 tests/components/pyload/snapshots/test_button.ambr create mode 100644 tests/components/pyload/test_button.py diff --git a/homeassistant/components/pyload/__init__.py b/homeassistant/components/pyload/__init__.py index d7c7e9454ea..b30b044e238 100644 --- a/homeassistant/components/pyload/__init__.py +++ b/homeassistant/components/pyload/__init__.py @@ -22,7 +22,7 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession from .coordinator import PyLoadCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR] type PyLoadConfigEntry = ConfigEntry[PyLoadCoordinator] diff --git a/homeassistant/components/pyload/button.py b/homeassistant/components/pyload/button.py new file mode 100644 index 00000000000..1f6bf3c3d10 --- /dev/null +++ b/homeassistant/components/pyload/button.py @@ -0,0 +1,107 @@ +"""Support for monitoring pyLoad.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from enum import StrEnum +from typing import Any + +from pyloadapi.api import PyLoadAPI + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import PyLoadConfigEntry +from .const import DOMAIN, MANUFACTURER, SERVICE_NAME +from .coordinator import PyLoadCoordinator + + +@dataclass(kw_only=True, frozen=True) +class PyLoadButtonEntityDescription(ButtonEntityDescription): + """Describes pyLoad button entity.""" + + press_fn: Callable[[PyLoadAPI], Awaitable[Any]] + + +class PyLoadButtonEntity(StrEnum): + """PyLoad button Entities.""" + + ABORT_DOWNLOADS = "abort_downloads" + RESTART_FAILED = "restart_failed" + DELETE_FINISHED = "delete_finished" + RESTART = "restart" + + +SENSOR_DESCRIPTIONS: tuple[PyLoadButtonEntityDescription, ...] = ( + PyLoadButtonEntityDescription( + key=PyLoadButtonEntity.ABORT_DOWNLOADS, + translation_key=PyLoadButtonEntity.ABORT_DOWNLOADS, + press_fn=lambda api: api.stop_all_downloads(), + ), + PyLoadButtonEntityDescription( + key=PyLoadButtonEntity.RESTART_FAILED, + translation_key=PyLoadButtonEntity.RESTART_FAILED, + press_fn=lambda api: api.restart_failed(), + ), + PyLoadButtonEntityDescription( + key=PyLoadButtonEntity.DELETE_FINISHED, + translation_key=PyLoadButtonEntity.DELETE_FINISHED, + press_fn=lambda api: api.delete_finished(), + ), + PyLoadButtonEntityDescription( + key=PyLoadButtonEntity.RESTART, + translation_key=PyLoadButtonEntity.RESTART, + press_fn=lambda api: api.restart(), + entity_registry_enabled_default=False, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PyLoadConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up buttons from a config entry.""" + + coordinator = entry.runtime_data + + async_add_entities( + PyLoadBinarySensor(coordinator, description) + for description in SENSOR_DESCRIPTIONS + ) + + +class PyLoadBinarySensor(CoordinatorEntity[PyLoadCoordinator], ButtonEntity): + """Representation of a pyLoad button.""" + + _attr_has_entity_name = True + entity_description: PyLoadButtonEntityDescription + + def __init__( + self, + coordinator: PyLoadCoordinator, + entity_description: PyLoadButtonEntityDescription, + ) -> None: + """Initialize the button.""" + super().__init__(coordinator) + self._attr_unique_id = ( + f"{coordinator.config_entry.entry_id}_{entity_description.key}" + ) + self.entity_description = entity_description + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + manufacturer=MANUFACTURER, + model=SERVICE_NAME, + configuration_url=coordinator.pyload.api_url, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + translation_key=DOMAIN, + ) + + async def async_press(self) -> None: + """Handle the button press.""" + await self.entity_description.press_fn(self.coordinator.pyload) diff --git a/homeassistant/components/pyload/const.py b/homeassistant/components/pyload/const.py index 8ee1c05696f..9419786fd88 100644 --- a/homeassistant/components/pyload/const.py +++ b/homeassistant/components/pyload/const.py @@ -7,3 +7,6 @@ DEFAULT_NAME = "pyLoad" DEFAULT_PORT = 8000 ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=pyload"} + +MANUFACTURER = "pyLoad Team" +SERVICE_NAME = "pyLoad" diff --git a/homeassistant/components/pyload/icons.json b/homeassistant/components/pyload/icons.json index bc068165851..8f6f016641f 100644 --- a/homeassistant/components/pyload/icons.json +++ b/homeassistant/components/pyload/icons.json @@ -1,5 +1,19 @@ { "entity": { + "button": { + "abort_downloads": { + "default": "mdi:stop" + }, + "restart_failed": { + "default": "mdi:cached" + }, + "delete_finished": { + "default": "mdi:trash-can" + }, + "restart": { + "default": "mdi:restart" + } + }, "sensor": { "speed": { "default": "mdi:speedometer" diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index cc53ef7465b..94c0c29d286 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -28,6 +28,20 @@ } }, "entity": { + "button": { + "abort_downloads": { + "name": "Abort all running downloads" + }, + "restart_failed": { + "name": "Restart all failed files" + }, + "delete_finished": { + "name": "Delete finished files/packages" + }, + "restart": { + "name": "Restart pyload core" + } + }, "sensor": { "speed": { "name": "Speed" diff --git a/tests/components/pyload/snapshots/test_button.ambr b/tests/components/pyload/snapshots/test_button.ambr new file mode 100644 index 00000000000..c9a901aba15 --- /dev/null +++ b/tests/components/pyload/snapshots/test_button.ambr @@ -0,0 +1,185 @@ +# serializer version: 1 +# name: test_state[button.pyload_abort_all_running_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.pyload_abort_all_running_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Abort all running downloads', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_abort_downloads', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[button.pyload_abort_all_running_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyload Abort all running downloads', + }), + 'context': , + 'entity_id': 'button.pyload_abort_all_running_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_state[button.pyload_delete_finished_files_packages-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.pyload_delete_finished_files_packages', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Delete finished files/packages', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_delete_finished', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[button.pyload_delete_finished_files_packages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyload Delete finished files/packages', + }), + 'context': , + 'entity_id': 'button.pyload_delete_finished_files_packages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_state[button.pyload_restart_all_failed_files-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.pyload_restart_all_failed_files', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Restart all failed files', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_restart_failed', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[button.pyload_restart_all_failed_files-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyload Restart all failed files', + }), + 'context': , + 'entity_id': 'button.pyload_restart_all_failed_files', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_state[button.pyload_restart_pyload_core-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.pyload_restart_pyload_core', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Restart pyload core', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_restart', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[button.pyload_restart_pyload_core-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyload Restart pyload core', + }), + 'context': , + 'entity_id': 'button.pyload_restart_pyload_core', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/pyload/test_button.py b/tests/components/pyload/test_button.py new file mode 100644 index 00000000000..b30a4cefd42 --- /dev/null +++ b/tests/components/pyload/test_button.py @@ -0,0 +1,83 @@ +"""The tests for the button component.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, call, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.pyload.button import PyLoadButtonEntity +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + +API_CALL = { + PyLoadButtonEntity.ABORT_DOWNLOADS: call.stop_all_downloads, + PyLoadButtonEntity.RESTART_FAILED: call.restart_failed, + PyLoadButtonEntity.DELETE_FINISHED: call.delete_finished, + PyLoadButtonEntity.RESTART: call.restart, +} + + +@pytest.fixture(autouse=True) +async def button_only() -> AsyncGenerator[None, None]: + """Enable only the button platform.""" + with patch( + "homeassistant.components.pyload.PLATFORMS", + [Platform.BUTTON], + ): + yield + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_state( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_pyloadapi: AsyncMock, +) -> None: + """Test button state.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_button_press( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test switch turn on method.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + entity_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + + for entity_entry in entity_entries: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_entry.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert API_CALL[entity_entry.translation_key] in mock_pyloadapi.method_calls + mock_pyloadapi.reset_mock() diff --git a/tests/components/pyload/test_sensor.py b/tests/components/pyload/test_sensor.py index 49795284fc6..61a9a872f33 100644 --- a/tests/components/pyload/test_sensor.py +++ b/tests/components/pyload/test_sensor.py @@ -1,6 +1,7 @@ """Tests for the pyLoad Sensors.""" -from unittest.mock import AsyncMock +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError @@ -11,6 +12,7 @@ from homeassistant.components.pyload.const import DOMAIN from homeassistant.components.pyload.coordinator import SCAN_INTERVAL from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers.typing import ConfigType @@ -19,6 +21,16 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform +@pytest.fixture(autouse=True) +async def sensor_only() -> AsyncGenerator[None, None]: + """Enable only the sensor platform.""" + with patch( + "homeassistant.components.pyload.PLATFORMS", + [Platform.SENSOR], + ): + yield + + async def test_setup( hass: HomeAssistant, config_entry: MockConfigEntry, From ced6c0dd8c3e26beae4cfc140b6d50802e2e7b3c Mon Sep 17 00:00:00 2001 From: Jan Schneider Date: Tue, 25 Jun 2024 08:11:26 +0200 Subject: [PATCH 0102/2411] Update moehlenhoff-alpha2 to 1.3.1 (#120351) Co-authored-by: J. Nick Koston --- homeassistant/components/moehlenhoff_alpha2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/moehlenhoff_alpha2/manifest.json b/homeassistant/components/moehlenhoff_alpha2/manifest.json index f4cc11453e0..14f40991a84 100644 --- a/homeassistant/components/moehlenhoff_alpha2/manifest.json +++ b/homeassistant/components/moehlenhoff_alpha2/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/moehlenhoff_alpha2", "iot_class": "local_push", - "requirements": ["moehlenhoff-alpha2==1.3.0"] + "requirements": ["moehlenhoff-alpha2==1.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 87d0bfd84c6..8c5d9662e20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1341,7 +1341,7 @@ minio==7.1.12 moat-ble==0.1.1 # homeassistant.components.moehlenhoff_alpha2 -moehlenhoff-alpha2==1.3.0 +moehlenhoff-alpha2==1.3.1 # homeassistant.components.monzo monzopy==1.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4890b8bdf3e..9755e583d9d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1089,7 +1089,7 @@ minio==7.1.12 moat-ble==0.1.1 # homeassistant.components.moehlenhoff_alpha2 -moehlenhoff-alpha2==1.3.0 +moehlenhoff-alpha2==1.3.1 # homeassistant.components.monzo monzopy==1.3.0 From 744161928e450978f73b48a1d26849c372cf2bf6 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 25 Jun 2024 07:25:04 +0100 Subject: [PATCH 0103/2411] Bump evohome-async to 0.4.20 (#120353) --- homeassistant/components/evohome/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 6b893dc8f48..e81e71c5b07 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/evohome", "iot_class": "cloud_polling", "loggers": ["evohomeasync", "evohomeasync2"], - "requirements": ["evohome-async==0.4.19"] + "requirements": ["evohome-async==0.4.20"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8c5d9662e20..da4afc918f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -846,7 +846,7 @@ eufylife-ble-client==0.1.8 # evdev==1.6.1 # homeassistant.components.evohome -evohome-async==0.4.19 +evohome-async==0.4.20 # homeassistant.components.faa_delays faadelays==2023.9.1 From 6fb400f76bcb722bc07c27349065d3d9ed9583c8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 Jun 2024 09:47:43 +0200 Subject: [PATCH 0104/2411] Add test of get_all_descriptions resolving features (#120384) --- tests/helpers/test_service.py | 77 +++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 60fe87db9d2..3e7d8e6ef03 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -971,6 +971,83 @@ async def test_async_get_all_descriptions_dot_keys(hass: HomeAssistant) -> None: } +async def test_async_get_all_descriptions_filter(hass: HomeAssistant) -> None: + """Test async_get_all_descriptions with filters.""" + service_descriptions = """ + test_service: + target: + entity: + domain: alarm_control_panel + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME + fields: + temperature: + filter: + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME + attribute: + supported_color_modes: + - light.ColorMode.COLOR_TEMP + selector: + number: + """ + + domain = "test_domain" + + hass.services.async_register(domain, "test_service", lambda call: None) + mock_integration(hass, MockModule(domain), top_level_files={"services.yaml"}) + assert await async_setup_component(hass, domain, {}) + + def load_yaml(fname, secrets=None): + with io.StringIO(service_descriptions) as file: + return parse_yaml(file) + + with ( + patch( + "homeassistant.helpers.service._load_services_files", + side_effect=service._load_services_files, + ) as proxy_load_services_files, + patch( + "homeassistant.util.yaml.loader.load_yaml", + side_effect=load_yaml, + ) as mock_load_yaml, + ): + descriptions = await service.async_get_all_descriptions(hass) + + mock_load_yaml.assert_called_once_with("services.yaml", None) + assert proxy_load_services_files.mock_calls[0][1][1] == unordered( + [ + await async_get_integration(hass, domain), + ] + ) + + test_service_schema = { + "description": "", + "fields": { + "temperature": { + "filter": { + "attribute": {"supported_color_modes": ["color_temp"]}, + "supported_features": [1], + }, + "selector": {"number": None}, + }, + }, + "name": "", + "target": { + "entity": [ + { + "domain": ["alarm_control_panel"], + "supported_features": [1], + }, + ], + }, + } + + assert descriptions == { + "test_domain": {"test_service": test_service_schema}, + } + + async def test_async_get_all_descriptions_failing_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: From 7f20c1a4891844610c002f35b44230facf16b810 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 09:55:59 +0200 Subject: [PATCH 0105/2411] Improve type hints in demo tests (#120387) --- tests/components/demo/test_camera.py | 5 +++-- tests/components/demo/test_climate.py | 5 +++-- tests/components/demo/test_cover.py | 29 ++++++++++++++------------- tests/components/demo/test_init.py | 7 ++++--- tests/components/demo/test_light.py | 5 +++-- tests/components/demo/test_number.py | 5 +++-- tests/components/demo/test_switch.py | 11 +++++----- tests/components/demo/test_text.py | 5 +++-- 8 files changed, 40 insertions(+), 32 deletions(-) diff --git a/tests/components/demo/test_camera.py b/tests/components/demo/test_camera.py index ecbd3fecee3..756609ed094 100644 --- a/tests/components/demo/test_camera.py +++ b/tests/components/demo/test_camera.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.camera import ( DOMAIN as CAMERA_DOMAIN, @@ -24,7 +25,7 @@ ENTITY_CAMERA = "camera.demo_camera" @pytest.fixture -async def camera_only() -> None: +def camera_only() -> Generator[None]: """Enable only the button platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -34,7 +35,7 @@ async def camera_only() -> None: @pytest.fixture(autouse=True) -async def demo_camera(hass, camera_only): +async def demo_camera(hass: HomeAssistant, camera_only: None) -> None: """Initialize a demo camera platform.""" assert await async_setup_component( hass, CAMERA_DOMAIN, {CAMERA_DOMAIN: {"platform": DOMAIN}} diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py index ff18f9e6a4e..682b85f0845 100644 --- a/tests/components/demo/test_climate.py +++ b/tests/components/demo/test_climate.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from typing_extensions import Generator import voluptuous as vol from homeassistant.components.climate import ( @@ -50,7 +51,7 @@ ENTITY_HEATPUMP = "climate.heatpump" @pytest.fixture -async def climate_only() -> None: +def climate_only() -> Generator[None]: """Enable only the climate platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -60,7 +61,7 @@ async def climate_only() -> None: @pytest.fixture(autouse=True) -async def setup_demo_climate(hass, climate_only): +async def setup_demo_climate(hass: HomeAssistant, climate_only: None) -> None: """Initialize setup demo climate.""" hass.config.units = METRIC_SYSTEM assert await async_setup_component(hass, DOMAIN, {"climate": {"platform": "demo"}}) diff --git a/tests/components/demo/test_cover.py b/tests/components/demo/test_cover.py index 9ea743a0a01..7ee408d3bfc 100644 --- a/tests/components/demo/test_cover.py +++ b/tests/components/demo/test_cover.py @@ -4,6 +4,7 @@ from datetime import timedelta from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, @@ -42,7 +43,7 @@ ENTITY_COVER = "cover.living_room_window" @pytest.fixture -async def cover_only() -> None: +def cover_only() -> Generator[None]: """Enable only the climate platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -51,15 +52,15 @@ async def cover_only() -> None: yield -@pytest.fixture -async def setup_comp(hass, cover_only): +@pytest.fixture(autouse=True) +async def setup_comp(hass: HomeAssistant, cover_only: None) -> None: """Set up demo cover component.""" with assert_setup_component(1, DOMAIN): await async_setup_component(hass, DOMAIN, CONFIG) await hass.async_block_till_done() -async def test_supported_features(hass: HomeAssistant, setup_comp) -> None: +async def test_supported_features(hass: HomeAssistant) -> None: """Test cover supported features.""" state = hass.states.get("cover.garage_door") assert state.attributes[ATTR_SUPPORTED_FEATURES] == 3 @@ -71,7 +72,7 @@ async def test_supported_features(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_SUPPORTED_FEATURES] == 255 -async def test_close_cover(hass: HomeAssistant, setup_comp) -> None: +async def test_close_cover(hass: HomeAssistant) -> None: """Test closing the cover.""" state = hass.states.get(ENTITY_COVER) assert state.state == STATE_OPEN @@ -92,7 +93,7 @@ async def test_close_cover(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_POSITION] == 0 -async def test_open_cover(hass: HomeAssistant, setup_comp) -> None: +async def test_open_cover(hass: HomeAssistant) -> None: """Test opening the cover.""" state = hass.states.get(ENTITY_COVER) assert state.state == STATE_OPEN @@ -112,7 +113,7 @@ async def test_open_cover(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_POSITION] == 100 -async def test_toggle_cover(hass: HomeAssistant, setup_comp) -> None: +async def test_toggle_cover(hass: HomeAssistant) -> None: """Test toggling the cover.""" # Start open await hass.services.async_call( @@ -152,7 +153,7 @@ async def test_toggle_cover(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_POSITION] == 100 -async def test_set_cover_position(hass: HomeAssistant, setup_comp) -> None: +async def test_set_cover_position(hass: HomeAssistant) -> None: """Test moving the cover to a specific position.""" state = hass.states.get(ENTITY_COVER) assert state.attributes[ATTR_CURRENT_POSITION] == 70 @@ -171,7 +172,7 @@ async def test_set_cover_position(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_POSITION] == 10 -async def test_stop_cover(hass: HomeAssistant, setup_comp) -> None: +async def test_stop_cover(hass: HomeAssistant) -> None: """Test stopping the cover.""" state = hass.states.get(ENTITY_COVER) assert state.attributes[ATTR_CURRENT_POSITION] == 70 @@ -190,7 +191,7 @@ async def test_stop_cover(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_POSITION] == 80 -async def test_close_cover_tilt(hass: HomeAssistant, setup_comp) -> None: +async def test_close_cover_tilt(hass: HomeAssistant) -> None: """Test closing the cover tilt.""" state = hass.states.get(ENTITY_COVER) assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 @@ -206,7 +207,7 @@ async def test_close_cover_tilt(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 -async def test_open_cover_tilt(hass: HomeAssistant, setup_comp) -> None: +async def test_open_cover_tilt(hass: HomeAssistant) -> None: """Test opening the cover tilt.""" state = hass.states.get(ENTITY_COVER) assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 @@ -222,7 +223,7 @@ async def test_open_cover_tilt(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 -async def test_toggle_cover_tilt(hass: HomeAssistant, setup_comp) -> None: +async def test_toggle_cover_tilt(hass: HomeAssistant) -> None: """Test toggling the cover tilt.""" # Start open await hass.services.async_call( @@ -259,7 +260,7 @@ async def test_toggle_cover_tilt(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 -async def test_set_cover_tilt_position(hass: HomeAssistant, setup_comp) -> None: +async def test_set_cover_tilt_position(hass: HomeAssistant) -> None: """Test moving the cover til to a specific position.""" state = hass.states.get(ENTITY_COVER) assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 @@ -278,7 +279,7 @@ async def test_set_cover_tilt_position(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 90 -async def test_stop_cover_tilt(hass: HomeAssistant, setup_comp) -> None: +async def test_stop_cover_tilt(hass: HomeAssistant) -> None: """Test stopping the cover tilt.""" state = hass.states.get(ENTITY_COVER) assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 2d60f7caf94..498a03600cb 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -4,6 +4,7 @@ import json from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.demo import DOMAIN from homeassistant.core import HomeAssistant @@ -12,19 +13,19 @@ from homeassistant.setup import async_setup_component @pytest.fixture -def mock_history(hass): +def mock_history(hass: HomeAssistant) -> None: """Mock history component loaded.""" hass.config.components.add("history") @pytest.fixture(autouse=True) -def mock_device_tracker_update_config(): +def mock_device_tracker_update_config() -> Generator[None]: """Prevent device tracker from creating known devices file.""" with patch("homeassistant.components.device_tracker.legacy.update_config"): yield -async def test_setting_up_demo(mock_history, hass: HomeAssistant) -> None: +async def test_setting_up_demo(mock_history: None, hass: HomeAssistant) -> None: """Test if we can set up the demo and dump it to JSON.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/demo/test_light.py b/tests/components/demo/test_light.py index b67acf3f60f..5c2c478b0bf 100644 --- a/tests/components/demo/test_light.py +++ b/tests/components/demo/test_light.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.demo import DOMAIN from homeassistant.components.light import ( @@ -27,7 +28,7 @@ ENTITY_LIGHT = "light.bed_light" @pytest.fixture -async def light_only() -> None: +def light_only() -> Generator[None]: """Enable only the light platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -37,7 +38,7 @@ async def light_only() -> None: @pytest.fixture(autouse=True) -async def setup_comp(hass, light_only): +async def setup_comp(hass: HomeAssistant, light_only: None) -> None: """Set up demo component.""" assert await async_setup_component( hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": DOMAIN}} diff --git a/tests/components/demo/test_number.py b/tests/components/demo/test_number.py index 20e3ce8fc11..37763b6e289 100644 --- a/tests/components/demo/test_number.py +++ b/tests/components/demo/test_number.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from typing_extensions import Generator import voluptuous as vol from homeassistant.components.number import ( @@ -26,7 +27,7 @@ ENTITY_SMALL_RANGE = "number.small_range" @pytest.fixture -async def number_only() -> None: +def number_only() -> Generator[None]: """Enable only the number platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -36,7 +37,7 @@ async def number_only() -> None: @pytest.fixture(autouse=True) -async def setup_demo_number(hass, number_only): +async def setup_demo_number(hass: HomeAssistant, number_only: None) -> None: """Initialize setup demo Number entity.""" assert await async_setup_component(hass, DOMAIN, {"number": {"platform": "demo"}}) await hass.async_block_till_done() diff --git a/tests/components/demo/test_switch.py b/tests/components/demo/test_switch.py index d8c3284875e..8b78171fd17 100644 --- a/tests/components/demo/test_switch.py +++ b/tests/components/demo/test_switch.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.demo import DOMAIN from homeassistant.components.switch import ( @@ -18,7 +19,7 @@ SWITCH_ENTITY_IDS = ["switch.decorative_lights", "switch.ac"] @pytest.fixture -async def switch_only() -> None: +def switch_only() -> Generator[None]: """Enable only the switch platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -28,7 +29,7 @@ async def switch_only() -> None: @pytest.fixture(autouse=True) -async def setup_comp(hass, switch_only): +async def setup_comp(hass: HomeAssistant, switch_only: None) -> None: """Set up demo component.""" assert await async_setup_component( hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": DOMAIN}} @@ -37,7 +38,7 @@ async def setup_comp(hass, switch_only): @pytest.mark.parametrize("switch_entity_id", SWITCH_ENTITY_IDS) -async def test_turn_on(hass: HomeAssistant, switch_entity_id) -> None: +async def test_turn_on(hass: HomeAssistant, switch_entity_id: str) -> None: """Test switch turn on method.""" await hass.services.async_call( SWITCH_DOMAIN, @@ -61,7 +62,7 @@ async def test_turn_on(hass: HomeAssistant, switch_entity_id) -> None: @pytest.mark.parametrize("switch_entity_id", SWITCH_ENTITY_IDS) -async def test_turn_off(hass: HomeAssistant, switch_entity_id) -> None: +async def test_turn_off(hass: HomeAssistant, switch_entity_id: str) -> None: """Test switch turn off method.""" await hass.services.async_call( SWITCH_DOMAIN, @@ -86,7 +87,7 @@ async def test_turn_off(hass: HomeAssistant, switch_entity_id) -> None: @pytest.mark.parametrize("switch_entity_id", SWITCH_ENTITY_IDS) async def test_turn_off_without_entity_id( - hass: HomeAssistant, switch_entity_id + hass: HomeAssistant, switch_entity_id: str ) -> None: """Test switch turn off all switches.""" await hass.services.async_call( diff --git a/tests/components/demo/test_text.py b/tests/components/demo/test_text.py index faf611d9875..3588330c75c 100644 --- a/tests/components/demo/test_text.py +++ b/tests/components/demo/test_text.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from typing_extensions import Generator from homeassistant.components.text import ( ATTR_MAX, @@ -25,7 +26,7 @@ ENTITY_TEXT = "text.text" @pytest.fixture -async def text_only() -> None: +def text_only() -> Generator[None]: """Enable only the text platform.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -35,7 +36,7 @@ async def text_only() -> None: @pytest.fixture(autouse=True) -async def setup_demo_text(hass, text_only): +async def setup_demo_text(hass: HomeAssistant, text_only: None) -> None: """Initialize setup demo text.""" assert await async_setup_component(hass, DOMAIN, {"text": {"platform": "demo"}}) await hass.async_block_till_done() From f8d5c9144a45b3a3fa5ab3489616fb07120705ec Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:06:38 +0200 Subject: [PATCH 0106/2411] Improve type hints in device_tracker tests (#120390) --- tests/components/device_tracker/test_device_trigger.py | 2 +- tests/components/device_tracker/test_init.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/components/device_tracker/test_device_trigger.py b/tests/components/device_tracker/test_device_trigger.py index 8932eb15997..4236e316424 100644 --- a/tests/components/device_tracker/test_device_trigger.py +++ b/tests/components/device_tracker/test_device_trigger.py @@ -43,7 +43,7 @@ def calls(hass: HomeAssistant) -> list[ServiceCall]: @pytest.fixture(autouse=True) -def setup_zone(hass): +def setup_zone(hass: HomeAssistant) -> None: """Create test zone.""" hass.loop.run_until_complete( async_setup_component( diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 6999a99f7ba..cedf2a2f0bc 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -8,6 +8,7 @@ from types import ModuleType from unittest.mock import call, patch import pytest +from typing_extensions import Generator from homeassistant.components import device_tracker, zone from homeassistant.components.device_tracker import SourceType, const, legacy @@ -49,7 +50,7 @@ _LOGGER = logging.getLogger(__name__) @pytest.fixture(name="yaml_devices") -def mock_yaml_devices(hass): +def mock_yaml_devices(hass: HomeAssistant) -> Generator[str]: """Get a path for storing yaml devices.""" yaml_devices = hass.config.path(legacy.YAML_DEVICES) if os.path.isfile(yaml_devices): @@ -108,7 +109,7 @@ async def test_reading_broken_yaml_config(hass: HomeAssistant) -> None: assert res[0].dev_id == "my_device" -async def test_reading_yaml_config(hass: HomeAssistant, yaml_devices) -> None: +async def test_reading_yaml_config(hass: HomeAssistant, yaml_devices: str) -> None: """Test the rendering of the YAML configuration.""" dev_id = "test" device = legacy.Device( @@ -186,7 +187,7 @@ async def test_duplicate_mac_dev_id(mock_warning, hass: HomeAssistant) -> None: assert "Duplicate device IDs" in args[0], "Duplicate device IDs warning expected" -async def test_setup_without_yaml_file(hass: HomeAssistant, yaml_devices) -> None: +async def test_setup_without_yaml_file(hass: HomeAssistant, yaml_devices: str) -> None: """Test with no YAML file.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) @@ -487,7 +488,7 @@ async def test_invalid_dev_id( assert not devices -async def test_see_state(hass: HomeAssistant, yaml_devices) -> None: +async def test_see_state(hass: HomeAssistant, yaml_devices: str) -> None: """Test device tracker see records state correctly.""" assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) await hass.async_block_till_done() From 46ed76df314454874b2a25bad2698dc62aa57a0e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:07:05 +0200 Subject: [PATCH 0107/2411] Improve type hints in diagnostics tests (#120391) --- tests/components/diagnostics/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/diagnostics/test_init.py b/tests/components/diagnostics/test_init.py index eeb4f420225..7f583395387 100644 --- a/tests/components/diagnostics/test_init.py +++ b/tests/components/diagnostics/test_init.py @@ -19,7 +19,7 @@ from tests.typing import ClientSessionGenerator, WebSocketGenerator @pytest.fixture(autouse=True) -async def mock_diagnostics_integration(hass): +async def mock_diagnostics_integration(hass: HomeAssistant) -> None: """Mock a diagnostics integration.""" hass.config.components.add("fake_integration") mock_platform( From 1d16cbec96801dd6fda9fda187ce1be43cc00003 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 25 Jun 2024 10:33:58 +0200 Subject: [PATCH 0108/2411] Move mqtt debouncer to mqtt utils (#120392) --- homeassistant/components/mqtt/client.py | 100 +--------------------- homeassistant/components/mqtt/util.py | 106 +++++++++++++++++++++++- tests/components/mqtt/conftest.py | 20 ++++- tests/components/mqtt/test_init.py | 100 +--------------------- tests/components/mqtt/test_util.py | 88 +++++++++++++++++++- 5 files changed, 213 insertions(+), 201 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 18ce89beb9b..7788c1db641 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -45,7 +45,6 @@ from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.setup import SetupPhases, async_pause_setup -from homeassistant.util.async_ import create_eager_task from homeassistant.util.collection import chunked_or_all from homeassistant.util.logging import catch_log_exception, log_exception @@ -85,7 +84,7 @@ from .models import ( PublishPayloadType, ReceiveMessage, ) -from .util import get_file_path, mqtt_config_entry_enabled +from .util import EnsureJobAfterCooldown, get_file_path, mqtt_config_entry_enabled if TYPE_CHECKING: # Only import for paho-mqtt type checking here, imports are done locally @@ -358,103 +357,6 @@ class MqttClientSetup: return self._client -class EnsureJobAfterCooldown: - """Ensure a cool down period before executing a job. - - When a new execute request arrives we cancel the current request - and start a new one. - """ - - def __init__( - self, timeout: float, callback_job: Callable[[], Coroutine[Any, None, None]] - ) -> None: - """Initialize the timer.""" - self._loop = asyncio.get_running_loop() - self._timeout = timeout - self._callback = callback_job - self._task: asyncio.Task | None = None - self._timer: asyncio.TimerHandle | None = None - self._next_execute_time = 0.0 - - def set_timeout(self, timeout: float) -> None: - """Set a new timeout period.""" - self._timeout = timeout - - async def _async_job(self) -> None: - """Execute after a cooldown period.""" - try: - await self._callback() - except HomeAssistantError as ha_error: - _LOGGER.error("%s", ha_error) - - @callback - def _async_task_done(self, task: asyncio.Task) -> None: - """Handle task done.""" - self._task = None - - @callback - def async_execute(self) -> asyncio.Task: - """Execute the job.""" - if self._task: - # Task already running, - # so we schedule another run - self.async_schedule() - return self._task - - self._async_cancel_timer() - self._task = create_eager_task(self._async_job()) - self._task.add_done_callback(self._async_task_done) - return self._task - - @callback - def _async_cancel_timer(self) -> None: - """Cancel any pending task.""" - if self._timer: - self._timer.cancel() - self._timer = None - - @callback - def async_schedule(self) -> None: - """Ensure we execute after a cooldown period.""" - # We want to reschedule the timer in the future - # every time this is called. - next_when = self._loop.time() + self._timeout - if not self._timer: - self._timer = self._loop.call_at(next_when, self._async_timer_reached) - return - - if self._timer.when() < next_when: - # Timer already running, set the next execute time - # if it fires too early, it will get rescheduled - self._next_execute_time = next_when - - @callback - def _async_timer_reached(self) -> None: - """Handle timer fire.""" - self._timer = None - if self._loop.time() >= self._next_execute_time: - self.async_execute() - return - # Timer fired too early because there were multiple - # calls async_schedule. Reschedule the timer. - self._timer = self._loop.call_at( - self._next_execute_time, self._async_timer_reached - ) - - async def async_cleanup(self) -> None: - """Cleanup any pending task.""" - self._async_cancel_timer() - if not self._task: - return - self._task.cancel() - try: - await self._task - except asyncio.CancelledError: - pass - except Exception: - _LOGGER.exception("Error cleaning up task") - - class MQTT: """Home Assistant MQTT client.""" diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 256bad71ba6..97fa616fdd1 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable, Coroutine from functools import lru_cache import logging import os @@ -14,7 +15,8 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import MAX_LENGTH_STATE_STATE, STATE_UNKNOWN, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.typing import ConfigType from homeassistant.util.async_ import create_eager_task @@ -40,6 +42,108 @@ TEMP_DIR_NAME = f"home-assistant-{DOMAIN}" _VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) +_LOGGER = logging.getLogger(__name__) + + +class EnsureJobAfterCooldown: + """Ensure a cool down period before executing a job. + + When a new execute request arrives we cancel the current request + and start a new one. + + We allow patching this util, as we generally have exceptions + for sleeps/waits/debouncers/timers causing long run times in tests. + """ + + def __init__( + self, timeout: float, callback_job: Callable[[], Coroutine[Any, None, None]] + ) -> None: + """Initialize the timer.""" + self._loop = asyncio.get_running_loop() + self._timeout = timeout + self._callback = callback_job + self._task: asyncio.Task | None = None + self._timer: asyncio.TimerHandle | None = None + self._next_execute_time = 0.0 + + def set_timeout(self, timeout: float) -> None: + """Set a new timeout period.""" + self._timeout = timeout + + async def _async_job(self) -> None: + """Execute after a cooldown period.""" + try: + await self._callback() + except HomeAssistantError as ha_error: + _LOGGER.error("%s", ha_error) + + @callback + def _async_task_done(self, task: asyncio.Task) -> None: + """Handle task done.""" + self._task = None + + @callback + def async_execute(self) -> asyncio.Task: + """Execute the job.""" + if self._task: + # Task already running, + # so we schedule another run + self.async_schedule() + return self._task + + self._async_cancel_timer() + self._task = create_eager_task(self._async_job()) + self._task.add_done_callback(self._async_task_done) + return self._task + + @callback + def _async_cancel_timer(self) -> None: + """Cancel any pending task.""" + if self._timer: + self._timer.cancel() + self._timer = None + + @callback + def async_schedule(self) -> None: + """Ensure we execute after a cooldown period.""" + # We want to reschedule the timer in the future + # every time this is called. + next_when = self._loop.time() + self._timeout + if not self._timer: + self._timer = self._loop.call_at(next_when, self._async_timer_reached) + return + + if self._timer.when() < next_when: + # Timer already running, set the next execute time + # if it fires too early, it will get rescheduled + self._next_execute_time = next_when + + @callback + def _async_timer_reached(self) -> None: + """Handle timer fire.""" + self._timer = None + if self._loop.time() >= self._next_execute_time: + self.async_execute() + return + # Timer fired too early because there were multiple + # calls async_schedule. Reschedule the timer. + self._timer = self._loop.call_at( + self._next_execute_time, self._async_timer_reached + ) + + async def async_cleanup(self) -> None: + """Cleanup any pending task.""" + self._async_cancel_timer() + if not self._task: + return + self._task.cancel() + try: + await self._task + except asyncio.CancelledError: + pass + except Exception: + _LOGGER.exception("Error cleaning up task") + def platforms_from_config(config: list[ConfigType]) -> set[Platform | str]: """Return the platforms to be set up.""" diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index 39b9f122f75..5a1f65667cf 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -9,7 +9,7 @@ import pytest from typing_extensions import AsyncGenerator, Generator from homeassistant.components import mqtt -from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.components.mqtt.models import MessageCallbackType, ReceiveMessage from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant, callback @@ -79,3 +79,21 @@ async def setup_with_birth_msg_client_mock( await hass.async_block_till_done() await birth.wait() yield mqtt_client_mock + + +@pytest.fixture +def recorded_calls() -> list[ReceiveMessage]: + """Fixture to hold recorded calls.""" + return [] + + +@pytest.fixture +def record_calls(recorded_calls: list[ReceiveMessage]) -> MessageCallbackType: + """Fixture to record calls.""" + + @callback + def record_calls(msg: ReceiveMessage) -> None: + """Record calls.""" + recorded_calls.append(msg) + + return record_calls diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 8a76c71f1f3..2c3ca31bff9 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -24,7 +24,6 @@ from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.client import ( _LOGGER as CLIENT_LOGGER, RECONNECT_INTERVAL_SECONDS, - EnsureJobAfterCooldown, ) from homeassistant.components.mqtt.models import ( MessageCallbackType, @@ -101,24 +100,6 @@ def mock_storage(hass_storage: dict[str, Any]) -> None: """Autouse hass_storage for the TestCase tests.""" -@pytest.fixture -def recorded_calls() -> list[ReceiveMessage]: - """Fixture to hold recorded calls.""" - return [] - - -@pytest.fixture -def record_calls(recorded_calls: list[ReceiveMessage]) -> MessageCallbackType: - """Fixture to record calls.""" - - @callback - def record_calls(msg: ReceiveMessage) -> None: - """Record calls.""" - recorded_calls.append(msg) - - return record_calls - - @pytest.fixture def client_debug_log() -> Generator[None]: """Set the mqtt client log level to DEBUG.""" @@ -1070,6 +1051,7 @@ async def test_subscribe_topic( async def test_subscribe_topic_not_initialize( hass: HomeAssistant, + record_calls: MessageCallbackType, mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test the subscription of a topic when MQTT was not initialized.""" @@ -1080,7 +1062,7 @@ async def test_subscribe_topic_not_initialize( async def test_subscribe_mqtt_config_entry_disabled( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, record_calls: MessageCallbackType ) -> None: """Test the subscription of a topic when MQTT config entry is disabled.""" mqtt_mock.connected = True @@ -2016,84 +1998,6 @@ async def test_reload_entry_with_restored_subscriptions( assert recorded_calls[1].payload == "wild-card-payload3" -async def test_canceling_debouncer_on_shutdown( - hass: HomeAssistant, - record_calls: MessageCallbackType, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test canceling the debouncer when HA shuts down.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - - with patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 2): - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() - await mqtt.async_subscribe(hass, "test/state1", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) - # Stop HA so the scheduled debouncer task will be canceled - mqtt_client_mock.subscribe.reset_mock() - hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - await mqtt.async_subscribe(hass, "test/state2", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) - await mqtt.async_subscribe(hass, "test/state3", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) - await mqtt.async_subscribe(hass, "test/state4", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) - await mqtt.async_subscribe(hass, "test/state5", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) - await hass.async_block_till_done() - - mqtt_client_mock.subscribe.assert_not_called() - - # Note thet the broker connection will not be disconnected gracefully - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await asyncio.sleep(0) - await hass.async_block_till_done(wait_background_tasks=True) - mqtt_client_mock.subscribe.assert_not_called() - mqtt_client_mock.disconnect.assert_not_called() - - -async def test_canceling_debouncer_normal( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test canceling the debouncer before completion.""" - - async def _async_myjob() -> None: - await asyncio.sleep(1.0) - - debouncer = EnsureJobAfterCooldown(0.0, _async_myjob) - debouncer.async_schedule() - await asyncio.sleep(0.01) - assert debouncer._task is not None - await debouncer.async_cleanup() - assert debouncer._task is None - - -async def test_canceling_debouncer_throws( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test canceling the debouncer when HA shuts down.""" - - async def _async_myjob() -> None: - await asyncio.sleep(1.0) - - debouncer = EnsureJobAfterCooldown(0.0, _async_myjob) - debouncer.async_schedule() - await asyncio.sleep(0.01) - assert debouncer._task is not None - # let debouncer._task fail by mocking it - with patch.object(debouncer, "_task") as task: - task.cancel = MagicMock(return_value=True) - await debouncer.async_cleanup() - assert "Error cleaning up task" in caplog.text - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() - - async def test_initial_setup_logs_error( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, diff --git a/tests/components/mqtt/test_util.py b/tests/components/mqtt/test_util.py index 290f561e1ad..955fc88448c 100644 --- a/tests/components/mqtt/test_util.py +++ b/tests/components/mqtt/test_util.py @@ -1,22 +1,106 @@ """Test MQTT utils.""" +import asyncio from collections.abc import Callable +from datetime import timedelta from pathlib import Path from random import getrandbits import shutil import tempfile -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from homeassistant.components import mqtt +from homeassistant.components.mqtt.models import MessageCallbackType +from homeassistant.components.mqtt.util import EnsureJobAfterCooldown from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, HomeAssistant +from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed from tests.typing import MqttMockHAClient, MqttMockPahoClient +async def test_canceling_debouncer_on_shutdown( + hass: HomeAssistant, + record_calls: MessageCallbackType, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test canceling the debouncer when HA shuts down.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + + with patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 2): + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + await mqtt.async_subscribe(hass, "test/state1", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + # Stop HA so the scheduled debouncer task will be canceled + mqtt_client_mock.subscribe.reset_mock() + hass.bus.fire(EVENT_HOMEASSISTANT_STOP) + await mqtt.async_subscribe(hass, "test/state2", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state3", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state4", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await mqtt.async_subscribe(hass, "test/state5", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) + await hass.async_block_till_done() + + mqtt_client_mock.subscribe.assert_not_called() + + # Note thet the broker connection will not be disconnected gracefully + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await asyncio.sleep(0) + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.subscribe.assert_not_called() + mqtt_client_mock.disconnect.assert_not_called() + + +async def test_canceling_debouncer_normal( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test canceling the debouncer before completion.""" + + async def _async_myjob() -> None: + await asyncio.sleep(1.0) + + debouncer = EnsureJobAfterCooldown(0.0, _async_myjob) + debouncer.async_schedule() + await asyncio.sleep(0.01) + assert debouncer._task is not None + await debouncer.async_cleanup() + assert debouncer._task is None + + +async def test_canceling_debouncer_throws( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test canceling the debouncer when HA shuts down.""" + + async def _async_myjob() -> None: + await asyncio.sleep(1.0) + + debouncer = EnsureJobAfterCooldown(0.0, _async_myjob) + debouncer.async_schedule() + await asyncio.sleep(0.01) + assert debouncer._task is not None + # let debouncer._task fail by mocking it + with patch.object(debouncer, "_task") as task: + task.cancel = MagicMock(return_value=True) + await debouncer.async_cleanup() + assert "Error cleaning up task" in caplog.text + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + + async def help_create_test_certificate_file( hass: HomeAssistant, mock_temp_dir: str, From ddd8083302e2ccf6154e62b8538137215078c732 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 25 Jun 2024 10:37:42 +0200 Subject: [PATCH 0109/2411] Fix translation error in Reolink reauth flow (#120385) --- homeassistant/components/reolink/config_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index d8caff9f120..be897a69d7d 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -123,7 +123,10 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN): """Dialog that informs the user that reauth is required.""" if user_input is not None: return await self.async_step_user() - return self.async_show_form(step_id="reauth_confirm") + placeholders = {"name": self.context["title_placeholders"]["name"]} + return self.async_show_form( + step_id="reauth_confirm", description_placeholders=placeholders + ) async def async_step_dhcp( self, discovery_info: dhcp.DhcpServiceInfo From 3d1ff72a8834befbe546cf8ce4937df51b85199c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:58:10 +0200 Subject: [PATCH 0110/2411] Improve type hints in device_automation tests (#120389) --- tests/components/device_automation/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 7d68a944de1..b270d2ddd7a 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -46,7 +46,7 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @pytest.fixture -def fake_integration(hass): +def fake_integration(hass: HomeAssistant) -> None: """Set up a mock integration with device automation support.""" DOMAIN = "fake_integration" From 0545ed8082c91ca5cd5bea88e6dd07169b365c7b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 Jun 2024 11:02:00 +0200 Subject: [PATCH 0111/2411] Section support for data entry flows (#118369) * Add expandable support for data entry form flows * Update config_validation.py * optional options * Adjust * Correct translations of data within sections * Update homeassistant/components/kitchen_sink/config_flow.py Co-authored-by: Robert Resch * Add missing import * Update tests/components/kitchen_sink/test_config_flow.py Co-authored-by: Robert Resch * Format code * Match frontend when serializing * Move section class to data_entry_flow * Correct serializing * Fix import in kitchen_sink * Move and update test --------- Co-authored-by: Bram Kragten Co-authored-by: Robert Resch --- .../components/kitchen_sink/config_flow.py | 69 ++++++++++++++++++- .../components/kitchen_sink/icons.json | 11 +++ .../components/kitchen_sink/strings.json | 20 ++++++ homeassistant/data_entry_flow.py | 27 ++++++++ homeassistant/helpers/config_validation.py | 10 +++ script/hassfest/icons.py | 18 +++++ script/hassfest/translations.py | 7 ++ .../kitchen_sink/test_config_flow.py | 38 ++++++++++ tests/test_data_entry_flow.py | 23 +++++++ 9 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/kitchen_sink/icons.json diff --git a/homeassistant/components/kitchen_sink/config_flow.py b/homeassistant/components/kitchen_sink/config_flow.py index 93c8a292ba9..c561ca29b8a 100644 --- a/homeassistant/components/kitchen_sink/config_flow.py +++ b/homeassistant/components/kitchen_sink/config_flow.py @@ -4,16 +4,36 @@ from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlowWithConfigEntry, +) +from homeassistant.core import callback from . import DOMAIN +CONF_BOOLEAN = "bool" +CONF_INT = "int" + class KitchenSinkConfigFlow(ConfigFlow, domain=DOMAIN): """Kitchen Sink configuration flow.""" VERSION = 1 + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlowHandler: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult: """Set the config entry up from yaml.""" if self._async_current_entries(): @@ -30,3 +50,50 @@ class KitchenSinkConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is None: return self.async_show_form(step_id="reauth_confirm") return self.async_abort(reason="reauth_successful") + + +class OptionsFlowHandler(OptionsFlowWithConfigEntry): + """Handle options.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options.""" + return await self.async_step_options_1() + + async def async_step_options_1( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options.""" + if user_input is not None: + self.options.update(user_input) + return await self._update_options() + + return self.async_show_form( + step_id="options_1", + data_schema=vol.Schema( + { + vol.Required("section_1"): data_entry_flow.section( + vol.Schema( + { + vol.Optional( + CONF_BOOLEAN, + default=self.config_entry.options.get( + CONF_BOOLEAN, False + ), + ): bool, + vol.Optional( + CONF_INT, + default=self.config_entry.options.get(CONF_INT, 10), + ): int, + } + ), + {"collapsed": False}, + ), + } + ), + ) + + async def _update_options(self) -> ConfigFlowResult: + """Update config entry options.""" + return self.async_create_entry(title="", data=self.options) diff --git a/homeassistant/components/kitchen_sink/icons.json b/homeassistant/components/kitchen_sink/icons.json new file mode 100644 index 00000000000..85472996819 --- /dev/null +++ b/homeassistant/components/kitchen_sink/icons.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "options_1": { + "section": { + "section_1": "mdi:robot" + } + } + } + } +} diff --git a/homeassistant/components/kitchen_sink/strings.json b/homeassistant/components/kitchen_sink/strings.json index ecfbe406aab..e67527d8468 100644 --- a/homeassistant/components/kitchen_sink/strings.json +++ b/homeassistant/components/kitchen_sink/strings.json @@ -6,6 +6,26 @@ } } }, + "options": { + "step": { + "init": { + "data": {} + }, + "options_1": { + "section": { + "section_1": { + "data": { + "bool": "Optional boolean", + "int": "Numeric input" + }, + "description": "This section allows input of some extra data", + "name": "Collapsible section" + } + }, + "submit": "Save!" + } + } + }, "device": { "n_ch_power_strip": { "name": "Power strip with {number_of_sockets} sockets" diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index de45702ad95..155e64d259e 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -906,6 +906,33 @@ class FlowHandler(Generic[_FlowResultT, _HandlerT]): self.__progress_task = progress_task +class SectionConfig(TypedDict, total=False): + """Class to represent a section config.""" + + collapsed: bool + + +class section: + """Data entry flow section.""" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("collapsed", default=False): bool, + }, + ) + + def __init__( + self, schema: vol.Schema, options: SectionConfig | None = None + ) -> None: + """Initialize.""" + self.schema = schema + self.options: SectionConfig = self.CONFIG_SCHEMA(options or {}) + + def __call__(self, value: Any) -> Any: + """Validate input.""" + return self.schema(value) + + # These can be removed if no deprecated constant are in this module anymore __getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) __dir__ = partial( diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 295cd13fed4..0463bb07e11 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1037,6 +1037,7 @@ def key_dependency( def custom_serializer(schema: Any) -> Any: """Serialize additional types for voluptuous_serialize.""" + from .. import data_entry_flow # pylint: disable=import-outside-toplevel from . import selector # pylint: disable=import-outside-toplevel if schema is positive_time_period_dict: @@ -1048,6 +1049,15 @@ def custom_serializer(schema: Any) -> Any: if schema is boolean: return {"type": "boolean"} + if isinstance(schema, data_entry_flow.section): + return { + "type": "expandable", + "schema": voluptuous_serialize.convert( + schema.schema, custom_serializer=custom_serializer + ), + "expanded": not schema.options["collapsed"], + } + if isinstance(schema, multi_select): return {"type": "multi_select", "options": schema.options} diff --git a/script/hassfest/icons.py b/script/hassfest/icons.py index e7451dfd498..087d395afeb 100644 --- a/script/hassfest/icons.py +++ b/script/hassfest/icons.py @@ -47,6 +47,19 @@ def ensure_not_same_as_default(value: dict) -> dict: return value +DATA_ENTRY_ICONS_SCHEMA = vol.Schema( + { + "step": { + str: { + "section": { + str: icon_value_validator, + } + } + } + } +) + + def icon_schema(integration_type: str, no_entity_platform: bool) -> vol.Schema: """Create an icon schema.""" @@ -73,6 +86,11 @@ def icon_schema(integration_type: str, no_entity_platform: bool) -> vol.Schema: schema = vol.Schema( { + vol.Optional("config"): DATA_ENTRY_ICONS_SCHEMA, + vol.Optional("issues"): vol.Schema( + {str: {"fix_flow": DATA_ENTRY_ICONS_SCHEMA}} + ), + vol.Optional("options"): DATA_ENTRY_ICONS_SCHEMA, vol.Optional("services"): state_validator, } ) diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 7ffb5861bb4..965d1dc62b8 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -166,6 +166,13 @@ def gen_data_entry_schema( vol.Optional("data_description"): {str: translation_value_validator}, vol.Optional("menu_options"): {str: translation_value_validator}, vol.Optional("submit"): translation_value_validator, + vol.Optional("section"): { + str: { + vol.Optional("data"): {str: translation_value_validator}, + vol.Optional("description"): translation_value_validator, + vol.Optional("name"): translation_value_validator, + }, + }, } }, vol.Optional("error"): {str: translation_value_validator}, diff --git a/tests/components/kitchen_sink/test_config_flow.py b/tests/components/kitchen_sink/test_config_flow.py index e530ed0e6f3..290167196cd 100644 --- a/tests/components/kitchen_sink/test_config_flow.py +++ b/tests/components/kitchen_sink/test_config_flow.py @@ -1,13 +1,28 @@ """Test the Everything but the Kitchen Sink config flow.""" +from collections.abc import AsyncGenerator from unittest.mock import patch +import pytest + from homeassistant import config_entries, setup from homeassistant.components.kitchen_sink import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + + +@pytest.fixture +async def no_platforms() -> AsyncGenerator[None, None]: + """Don't enable any platforms.""" + with patch( + "homeassistant.components.kitchen_sink.COMPONENTS_WITH_DEMO_PLATFORM", + [], + ): + yield + async def test_import(hass: HomeAssistant) -> None: """Test that we can import a config entry.""" @@ -66,3 +81,26 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" + + +@pytest.mark.usefixtures("no_platforms") +async def test_options_flow(hass: HomeAssistant) -> None: + """Test config flow options.""" + config_entry = MockConfigEntry(domain=DOMAIN) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "options_1" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"section_1": {"bool": True, "int": 15}}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert config_entry.options == {"section_1": {"bool": True, "int": 15}} + + await hass.async_block_till_done() diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 782f349f9f2..967b2565206 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv from homeassistant.util.decorator import Registry from .common import ( @@ -1075,3 +1076,25 @@ def test_deprecated_constants( import_and_test_deprecated_constant_enum( caplog, data_entry_flow, enum, "RESULT_TYPE_", "2025.1" ) + + +def test_section_in_serializer() -> None: + """Test section with custom_serializer.""" + assert cv.custom_serializer( + data_entry_flow.section( + vol.Schema( + { + vol.Optional("option_1", default=False): bool, + vol.Required("option_2"): int, + } + ), + {"collapsed": False}, + ) + ) == { + "expanded": True, + "schema": [ + {"default": False, "name": "option_1", "optional": True, "type": "boolean"}, + {"name": "option_2", "required": True, "type": "integer"}, + ], + "type": "expandable", + } From d3e76b1f39ba5756bdf362853fecc036a34c9e40 Mon Sep 17 00:00:00 2001 From: treetip Date: Tue, 25 Jun 2024 12:24:57 +0300 Subject: [PATCH 0112/2411] Update vallox_websocket_api to 5.3.0 (#120395) --- 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 9a57358cd14..bbc806d8f38 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/vallox", "iot_class": "local_polling", "loggers": ["vallox_websocket_api"], - "requirements": ["vallox-websocket-api==5.1.1"] + "requirements": ["vallox-websocket-api==5.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index da4afc918f3..d1481147699 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2826,7 +2826,7 @@ uvcclient==0.11.0 vacuum-map-parser-roborock==0.1.2 # homeassistant.components.vallox -vallox-websocket-api==5.1.1 +vallox-websocket-api==5.3.0 # homeassistant.components.rdw vehicle==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9755e583d9d..3af1fa4184a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2200,7 +2200,7 @@ uvcclient==0.11.0 vacuum-map-parser-roborock==0.1.2 # homeassistant.components.vallox -vallox-websocket-api==5.1.1 +vallox-websocket-api==5.3.0 # homeassistant.components.rdw vehicle==2.2.1 From 53f5dec1b4533d4cd1677298d3fe3ab79c2755ba Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 11:26:45 +0200 Subject: [PATCH 0113/2411] Install libturbojpeg [ci] (#120397) --- .github/workflows/ci.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index af29c00af9e..8a030d7d45c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -488,6 +488,7 @@ jobs: sudo apt-get -y install \ bluez \ ffmpeg \ + libturbojpeg \ libavcodec-dev \ libavdevice-dev \ libavfilter-dev \ @@ -747,6 +748,7 @@ jobs: sudo apt-get -y install \ bluez \ ffmpeg \ + libturbojpeg \ libgammu-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.7 @@ -809,6 +811,7 @@ jobs: sudo apt-get -y install \ bluez \ ffmpeg \ + libturbojpeg \ libgammu-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.7 @@ -926,6 +929,7 @@ jobs: sudo apt-get -y install \ bluez \ ffmpeg \ + libturbojpeg \ libmariadb-dev-compat - name: Check out code from GitHub uses: actions/checkout@v4.1.7 @@ -1050,6 +1054,7 @@ jobs: sudo apt-get -y install \ bluez \ ffmpeg \ + libturbojpeg \ postgresql-server-dev-14 - name: Check out code from GitHub uses: actions/checkout@v4.1.7 @@ -1194,6 +1199,7 @@ jobs: sudo apt-get -y install \ bluez \ ffmpeg \ + libturbojpeg \ libgammu-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.7 From b4eee166aa820626119ebff590a90a0f034ca766 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 11:58:27 +0200 Subject: [PATCH 0114/2411] Add voluptuous type aliases (#120399) --- homeassistant/core.py | 8 ++++---- homeassistant/helpers/entity_component.py | 7 +++---- homeassistant/helpers/entity_platform.py | 6 ++---- homeassistant/helpers/typing.py | 4 ++++ 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index ac287fb2d5f..f114049b2b2 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -96,7 +96,7 @@ from .helpers.deprecation import ( dir_with_deprecated_constants, ) from .helpers.json import json_bytes, json_fragment -from .helpers.typing import UNDEFINED, UndefinedType +from .helpers.typing import UNDEFINED, UndefinedType, VolSchemaType from .util import dt as dt_util, location from .util.async_ import ( cancelling, @@ -2355,7 +2355,7 @@ class Service: | EntityServiceResponse | None, ], - schema: vol.Schema | None, + schema: VolSchemaType | None, domain: str, service: str, context: Context | None = None, @@ -2503,7 +2503,7 @@ class ServiceRegistry: | EntityServiceResponse | None, ], - schema: vol.Schema | None = None, + schema: VolSchemaType | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, job_type: HassJobType | None = None, ) -> None: @@ -2530,7 +2530,7 @@ class ServiceRegistry: | EntityServiceResponse | None, ], - schema: vol.Schema | None = None, + schema: VolSchemaType | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, job_type: HassJobType | None = None, ) -> None: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index aae0e2058e4..0034eb1c6fc 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -11,7 +11,6 @@ from types import ModuleType from typing import Any, Generic from typing_extensions import TypeVar -import voluptuous as vol from homeassistant import config as conf_util from homeassistant.config_entries import ConfigEntry @@ -36,7 +35,7 @@ from homeassistant.setup import async_prepare_setup_platform from . import config_validation as cv, discovery, entity, service from .entity_platform import EntityPlatform -from .typing import ConfigType, DiscoveryInfoType +from .typing import ConfigType, DiscoveryInfoType, VolDictType, VolSchemaType DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) DATA_INSTANCES = "entity_components" @@ -222,7 +221,7 @@ class EntityComponent(Generic[_EntityT]): def async_register_legacy_entity_service( self, name: str, - schema: dict[str | vol.Marker, Any] | vol.Schema, + schema: VolDictType | VolSchemaType, func: str | Callable[..., Any], required_features: list[int] | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, @@ -259,7 +258,7 @@ class EntityComponent(Generic[_EntityT]): def async_register_entity_service( self, name: str, - schema: dict[str | vol.Marker, Any] | vol.Schema, + schema: VolDictType | VolSchemaType, func: str | Callable[..., Any], required_features: list[int] | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 4dbe3ac68d8..6774780f00f 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -10,8 +10,6 @@ from functools import partial from logging import Logger, getLogger from typing import TYPE_CHECKING, Any, Protocol -import voluptuous as vol - from homeassistant import config_entries from homeassistant.const import ( ATTR_RESTORED, @@ -52,7 +50,7 @@ from . import ( from .entity_registry import EntityRegistry, RegistryEntryDisabler, RegistryEntryHider from .event import async_call_later from .issue_registry import IssueSeverity, async_create_issue -from .typing import UNDEFINED, ConfigType, DiscoveryInfoType +from .typing import UNDEFINED, ConfigType, DiscoveryInfoType, VolDictType, VolSchemaType if TYPE_CHECKING: from .entity import Entity @@ -987,7 +985,7 @@ class EntityPlatform: def async_register_entity_service( self, name: str, - schema: dict[str | vol.Marker, Any] | vol.Schema, + schema: VolDictType | VolSchemaType, func: str | Callable[..., Any], required_features: Iterable[int] | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 3cdd9ec9250..65774a0b168 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -5,6 +5,8 @@ from enum import Enum from functools import partial from typing import Any, Never +import voluptuous as vol + from .deprecation import ( DeferredDeprecatedAlias, all_with_deprecated_constants, @@ -19,6 +21,8 @@ type ServiceDataType = dict[str, Any] type StateType = str | int | float | None type TemplateVarsType = Mapping[str, Any] | None type NoEventData = Mapping[str, Never] +type VolSchemaType = vol.Schema | vol.All | vol.Any +type VolDictType = dict[str | vol.Marker, Any] # Custom type for recorder Queries type QueryType = Any From 3a5acd6a57db11e7fc8ee31c7f22eea9157e501f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:33:41 +0200 Subject: [PATCH 0115/2411] Use VolDictType for service schemas (#120403) --- homeassistant/components/camera/__init__.py | 8 ++++---- .../components/elkm1/alarm_control_panel.py | 3 ++- homeassistant/components/elkm1/const.py | 3 ++- homeassistant/components/elkm1/sensor.py | 3 ++- .../components/environment_canada/camera.py | 3 ++- homeassistant/components/flux_led/light.py | 7 ++++--- homeassistant/components/geniushub/switch.py | 4 ++-- homeassistant/components/harmony/remote.py | 3 ++- homeassistant/components/isy994/services.py | 7 +++++-- homeassistant/components/izone/climate.py | 4 ++-- homeassistant/components/keymitt_ble/switch.py | 3 ++- homeassistant/components/kodi/media_player.py | 4 ++-- homeassistant/components/lifx/light.py | 5 +++-- homeassistant/components/light/__init__.py | 9 ++++++--- homeassistant/components/lyric/climate.py | 3 ++- homeassistant/components/motion_blinds/cover.py | 3 ++- homeassistant/components/nexia/climate.py | 5 +++-- homeassistant/components/rainbird/switch.py | 3 ++- homeassistant/components/rainmachine/switch.py | 6 ++++-- homeassistant/components/renson/fan.py | 7 ++++--- homeassistant/components/roku/media_player.py | 3 ++- homeassistant/components/siren/__init__.py | 4 ++-- homeassistant/components/smarttub/binary_sensor.py | 5 +++-- homeassistant/components/smarttub/sensor.py | 3 ++- homeassistant/components/switcher_kis/switch.py | 5 +++-- homeassistant/components/tado/climate.py | 5 +++-- homeassistant/components/tado/water_heater.py | 3 ++- homeassistant/components/upb/const.py | 3 ++- homeassistant/components/vizio/const.py | 3 ++- homeassistant/components/wemo/fan.py | 3 ++- homeassistant/components/yardian/switch.py | 3 ++- homeassistant/components/yeelight/__init__.py | 4 ++-- homeassistant/components/yeelight/light.py | 13 +++++++------ 33 files changed, 91 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 4d2ba00900f..428e8d856fb 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -64,7 +64,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.network import get_url from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.loader import bind_hass from .const import ( # noqa: F401 @@ -130,14 +130,14 @@ _RND: Final = SystemRandom() MIN_STREAM_INTERVAL: Final = 0.5 # seconds -CAMERA_SERVICE_SNAPSHOT: Final = {vol.Required(ATTR_FILENAME): cv.template} +CAMERA_SERVICE_SNAPSHOT: VolDictType = {vol.Required(ATTR_FILENAME): cv.template} -CAMERA_SERVICE_PLAY_STREAM: Final = { +CAMERA_SERVICE_PLAY_STREAM: VolDictType = { vol.Required(ATTR_MEDIA_PLAYER): cv.entities_domain(DOMAIN_MP), vol.Optional(ATTR_FORMAT, default="hls"): vol.In(OUTPUT_FORMATS), } -CAMERA_SERVICE_RECORD: Final = { +CAMERA_SERVICE_RECORD: VolDictType = { vol.Required(CONF_FILENAME): cv.template, vol.Optional(CONF_DURATION, default=30): vol.Coerce(int), vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int), diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index eb8d7360ce2..b24d0f869c6 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -31,6 +31,7 @@ from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import VolDictType from . import ElkAttachedEntity, ElkEntity, ElkM1ConfigEntry, create_elk_entities from .const import ( @@ -41,7 +42,7 @@ from .const import ( ) from .models import ELKM1Data -DISPLAY_MESSAGE_SERVICE_SCHEMA = { +DISPLAY_MESSAGE_SERVICE_SCHEMA: VolDictType = { vol.Optional("clear", default=2): vol.All(vol.Coerce(int), vol.In([0, 1, 2])), vol.Optional("beep", default=False): cv.boolean, vol.Optional("timeout", default=0): vol.All( diff --git a/homeassistant/components/elkm1/const.py b/homeassistant/components/elkm1/const.py index 9e952c7ee0b..61d1994b797 100644 --- a/homeassistant/components/elkm1/const.py +++ b/homeassistant/components/elkm1/const.py @@ -6,6 +6,7 @@ from elkm1_lib.const import Max import voluptuous as vol from homeassistant.const import ATTR_CODE, CONF_ZONE +from homeassistant.helpers.typing import VolDictType DOMAIN = "elkm1" @@ -48,6 +49,6 @@ ATTR_CHANGED_BY_ID = "changed_by_id" ATTR_CHANGED_BY_TIME = "changed_by_time" ATTR_VALUE = "value" -ELK_USER_CODE_SERVICE_SCHEMA = { +ELK_USER_CODE_SERVICE_SCHEMA: VolDictType = { vol.Required(ATTR_CODE): vol.All(vol.Coerce(int), vol.Range(0, 999999)) } diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 801a09b76eb..7d3601f0bd0 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -20,6 +20,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from . import ElkAttachedEntity, ElkEntity, ElkM1ConfigEntry, create_elk_entities from .const import ATTR_VALUE, ELK_USER_CODE_SERVICE_SCHEMA @@ -30,7 +31,7 @@ SERVICE_SENSOR_ZONE_BYPASS = "sensor_zone_bypass" SERVICE_SENSOR_ZONE_TRIGGER = "sensor_zone_trigger" UNDEFINED_TEMPERATURE = -40 -ELK_SET_COUNTER_SERVICE_SCHEMA = { +ELK_SET_COUNTER_SERVICE_SCHEMA: VolDictType = { vol.Required(ATTR_VALUE): vol.All(vol.Coerce(int), vol.Range(0, 65535)) } diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index 73032f59ac2..1625cd253da 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -11,13 +11,14 @@ from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, ) +from homeassistant.helpers.typing import VolDictType 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" -SET_RADAR_TYPE_SCHEMA = { +SET_RADAR_TYPE_SCHEMA: VolDictType = { vol.Required("radar_type"): vol.In(["Auto", "Rain", "Snow"]), } diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 6456eb36dbb..f4982a13c3a 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -28,6 +28,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.color import ( color_temperature_kelvin_to_mired, @@ -88,7 +89,7 @@ SERVICE_CUSTOM_EFFECT: Final = "set_custom_effect" SERVICE_SET_ZONES: Final = "set_zones" SERVICE_SET_MUSIC_MODE: Final = "set_music_mode" -CUSTOM_EFFECT_DICT: Final = { +CUSTOM_EFFECT_DICT: VolDictType = { vol.Required(CONF_COLORS): vol.All( cv.ensure_list, vol.Length(min=1, max=16), @@ -102,7 +103,7 @@ CUSTOM_EFFECT_DICT: Final = { ), } -SET_MUSIC_MODE_DICT: Final = { +SET_MUSIC_MODE_DICT: VolDictType = { vol.Optional(ATTR_SENSITIVITY, default=100): vol.All( vol.Coerce(int), vol.Range(min=0, max=100) ), @@ -121,7 +122,7 @@ SET_MUSIC_MODE_DICT: Final = { ), } -SET_ZONES_DICT: Final = { +SET_ZONES_DICT: VolDictType = { vol.Required(CONF_COLORS): vol.All( cv.ensure_list, vol.Length(min=1, max=2048), diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index b703df57f90..85f7f1bb03a 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -11,7 +11,7 @@ from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolDictType from . import ATTR_DURATION, DOMAIN, GeniusZone @@ -19,7 +19,7 @@ GH_ON_OFF_ZONE = "on / off" SVC_SET_SWITCH_OVERRIDE = "set_switch_override" -SET_SWITCH_OVERRIDE_SCHEMA = { +SET_SWITCH_OVERRIDE_SCHEMA: VolDictType = { vol.Optional(ATTR_DURATION): vol.All( cv.time_period, vol.Range(min=timedelta(minutes=5), max=timedelta(days=1)), diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 0c9bdcb9c6e..a52f298dc41 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -26,6 +26,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import VolDictType from .const import ( ACTIVITY_POWER_OFF, @@ -50,7 +51,7 @@ PARALLEL_UPDATES = 0 ATTR_CHANNEL = "channel" -HARMONY_CHANGE_CHANNEL_SCHEMA = { +HARMONY_CHANGE_CHANNEL_SCHEMA: VolDictType = { vol.Required(ATTR_CHANNEL): cv.positive_int, } diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index fedf7f8e902..ffcea5cc8f8 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -18,6 +18,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.helpers.service import entity_service_call +from homeassistant.helpers.typing import VolDictType from .const import _LOGGER, DOMAIN @@ -102,12 +103,14 @@ SERVICE_SET_ZWAVE_PARAMETER_SCHEMA = { vol.Required(CONF_SIZE): vol.All(vol.Coerce(int), vol.In(VALID_PARAMETER_SIZES)), } -SERVICE_SET_USER_CODE_SCHEMA = { +SERVICE_SET_USER_CODE_SCHEMA: VolDictType = { vol.Required(CONF_USER_NUM): vol.Coerce(int), vol.Required(CONF_CODE): vol.Coerce(int), } -SERVICE_DELETE_USER_CODE_SCHEMA = {vol.Required(CONF_USER_NUM): vol.Coerce(int)} +SERVICE_DELETE_USER_CODE_SCHEMA: VolDictType = { + vol.Required(CONF_USER_NUM): vol.Coerce(int) +} SERVICE_SEND_PROGRAM_COMMAND_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_ADDRESS, CONF_NAME), diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 14267a626fc..3a1279a9bd4 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -35,7 +35,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.temperature import display_temp as show_temp -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from .const import ( DATA_CONFIG, @@ -65,7 +65,7 @@ ATTR_AIRFLOW = "airflow" IZONE_SERVICE_AIRFLOW_MIN = "airflow_min" IZONE_SERVICE_AIRFLOW_MAX = "airflow_max" -IZONE_SERVICE_AIRFLOW_SCHEMA = { +IZONE_SERVICE_AIRFLOW_SCHEMA: VolDictType = { vol.Required(ATTR_AIRFLOW): vol.All( vol.Coerce(int), vol.Range(min=0, max=100), msg="invalid airflow" ), diff --git a/homeassistant/components/keymitt_ble/switch.py b/homeassistant/components/keymitt_ble/switch.py index 2c255ecdf28..ca458c5020f 100644 --- a/homeassistant/components/keymitt_ble/switch.py +++ b/homeassistant/components/keymitt_ble/switch.py @@ -14,13 +14,14 @@ from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, ) +from homeassistant.helpers.typing import VolDictType from .const import DOMAIN from .coordinator import MicroBotDataUpdateCoordinator from .entity import MicroBotEntity CALIBRATE = "calibrate" -CALIBRATE_SCHEMA = { +CALIBRATE_SCHEMA: VolDictType = { vol.Required("depth"): cv.positive_int, vol.Required("duration"): cv.positive_int, vol.Required("mode"): vol.In(["normal", "invert", "toggle"]), diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 2bfe21b6eaa..3ba5804f8b3 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -49,7 +49,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.network import is_internal_request -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolDictType import homeassistant.util.dt as dt_util from .browse_media import ( @@ -147,7 +147,7 @@ ATTR_MEDIA_ID = "media_id" ATTR_METHOD = "method" -KODI_ADD_MEDIA_SCHEMA = { +KODI_ADD_MEDIA_SCHEMA: VolDictType = { vol.Required(ATTR_MEDIA_TYPE): cv.string, vol.Optional(ATTR_MEDIA_ID): cv.string, vol.Optional(ATTR_MEDIA_NAME): cv.string, diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 90632f82d9e..caa1140b099 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -25,6 +25,7 @@ from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.typing import VolDictType from .const import ( _LOGGER, @@ -53,7 +54,7 @@ LIFX_STATE_SETTLE_DELAY = 0.3 SERVICE_LIFX_SET_STATE = "set_state" -LIFX_SET_STATE_SCHEMA = { +LIFX_SET_STATE_SCHEMA: VolDictType = { **LIGHT_TURN_ON_SCHEMA, ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]), @@ -63,7 +64,7 @@ LIFX_SET_STATE_SCHEMA = { SERVICE_LIFX_SET_HEV_CYCLE_STATE = "set_hev_cycle_state" -LIFX_SET_HEV_CYCLE_STATE_SCHEMA = { +LIFX_SET_HEV_CYCLE_STATE_SCHEMA: VolDictType = { ATTR_POWER: vol.Required(cv.boolean), ATTR_DURATION: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=86400)), } diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 6d3065c48c9..16367c35ec5 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -31,7 +31,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.loader import bind_hass import homeassistant.util.color as color_util @@ -247,7 +247,7 @@ VALID_BRIGHTNESS_STEP = vol.All(vol.Coerce(int), vol.Clamp(min=-255, max=255)) VALID_BRIGHTNESS_STEP_PCT = vol.All(vol.Coerce(float), vol.Clamp(min=-100, max=100)) VALID_FLASH = vol.In([FLASH_SHORT, FLASH_LONG]) -LIGHT_TURN_ON_SCHEMA = { +LIGHT_TURN_ON_SCHEMA: VolDictType = { vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string, ATTR_TRANSITION: VALID_TRANSITION, vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS, @@ -286,7 +286,10 @@ LIGHT_TURN_ON_SCHEMA = { ATTR_EFFECT: cv.string, } -LIGHT_TURN_OFF_SCHEMA = {ATTR_TRANSITION: VALID_TRANSITION, ATTR_FLASH: VALID_FLASH} +LIGHT_TURN_OFF_SCHEMA: VolDictType = { + ATTR_TRANSITION: VALID_TRANSITION, + ATTR_FLASH: VALID_FLASH, +} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index f8ae978c2fd..50add155915 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -37,6 +37,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import LyricDeviceEntity @@ -111,7 +112,7 @@ HVAC_ACTIONS = { SERVICE_HOLD_TIME = "set_hold_time" ATTR_TIME_PERIOD = "time_period" -SCHEMA_HOLD_TIME = { +SCHEMA_HOLD_TIME: VolDictType = { vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All( cv.time_period, cv.positive_timedelta, diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index eb40a1d66ca..2cbee96adb7 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -20,6 +20,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.typing import VolDictType from .const import ( ATTR_ABSOLUTE_POSITION, @@ -75,7 +76,7 @@ TDBU_DEVICE_MAP = { } -SET_ABSOLUTE_POSITION_SCHEMA = { +SET_ABSOLUTE_POSITION_SCHEMA: VolDictType = { vol.Required(ATTR_ABSOLUTE_POSITION): vol.All(cv.positive_int, vol.Range(max=100)), vol.Optional(ATTR_TILT_POSITION): vol.All(cv.positive_int, vol.Range(max=100)), vol.Optional(ATTR_WIDTH): vol.All(cv.positive_int, vol.Range(max=100)), diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 7d09f710828..7c28062f4b8 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -37,6 +37,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from .const import ( ATTR_AIRCLEANER_MODE, @@ -55,11 +56,11 @@ SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode" SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint" SERVICE_SET_HVAC_RUN_MODE = "set_hvac_run_mode" -SET_AIRCLEANER_SCHEMA = { +SET_AIRCLEANER_SCHEMA: VolDictType = { vol.Required(ATTR_AIRCLEANER_MODE): cv.string, } -SET_HUMIDITY_SCHEMA = { +SET_HUMIDITY_SCHEMA: VolDictType = { vol.Required(ATTR_HUMIDITY): vol.All(vol.Coerce(int), vol.Range(min=35, max=65)), } diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 7f43553aa41..62a2a7c4a32 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -14,6 +14,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTR_DURATION, CONF_IMPORTED_NAMES, DOMAIN, MANUFACTURER @@ -23,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) SERVICE_START_IRRIGATION = "start_irrigation" -SERVICE_SCHEMA_IRRIGATION = { +SERVICE_SCHEMA_IRRIGATION: VolDictType = { vol.Required(ATTR_DURATION): cv.positive_float, } diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 328d5193e1e..667e609e11c 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -6,7 +6,7 @@ import asyncio from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass from datetime import datetime -from typing import Any, Concatenate +from typing import Any, Concatenate, cast from regenmaschine.errors import RainMachineError import voluptuous as vol @@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from . import RainMachineData, RainMachineEntity, async_update_programs_and_zones from .const import ( @@ -191,7 +192,8 @@ async def async_setup_entry( ("stop_program", {}, "async_stop_program"), ("stop_zone", {}, "async_stop_zone"), ): - platform.async_register_entity_service(service_name, schema, method) + schema_dict = cast(VolDictType, schema) + platform.async_register_entity_service(service_name, schema_dict, method) data: RainMachineData = hass.data[DOMAIN][entry.entry_id] entities: list[RainMachineBaseSwitch] = [] diff --git a/homeassistant/components/renson/fan.py b/homeassistant/components/renson/fan.py index 226d623af2b..bff84017e29 100644 --- a/homeassistant/components/renson/fan.py +++ b/homeassistant/components/renson/fan.py @@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from homeassistant.util.percentage import ( percentage_to_ranged_value, ranged_value_to_percentage, @@ -51,20 +52,20 @@ SPEED_MAPPING = { Level.LEVEL4.value: 4, } -SET_TIMER_LEVEL_SCHEMA = { +SET_TIMER_LEVEL_SCHEMA: VolDictType = { vol.Required("timer_level"): vol.In( ["level1", "level2", "level3", "level4", "holiday", "breeze"] ), vol.Required("minutes"): cv.positive_int, } -SET_BREEZE_SCHEMA = { +SET_BREEZE_SCHEMA: VolDictType = { vol.Required("breeze_level"): vol.In(["level1", "level2", "level3", "level4"]), vol.Required("temperature"): cv.positive_int, vol.Required("activate"): bool, } -SET_POLLUTION_SETTINGS_SCHEMA = { +SET_POLLUTION_SETTINGS_SCHEMA: VolDictType = { vol.Required("day_pollution_level"): vol.In( ["level1", "level2", "level3", "level4"] ), diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 92361909219..881dda38f15 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -28,6 +28,7 @@ from homeassistant.const import ATTR_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from .browse_media import async_browse_media from .const import ( @@ -78,7 +79,7 @@ ATTRS_TO_PLAY_ON_ROKU_AUDIO_PARAMS = { ATTR_THUMBNAIL: "albumArtUrl", } -SEARCH_SCHEMA = {vol.Required(ATTR_KEYWORD): str} +SEARCH_SCHEMA: VolDictType = {vol.Required(ATTR_KEYWORD): str} async def async_setup_entry( diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index a0a599dd2df..e5837fdd1bf 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -24,7 +24,7 @@ from homeassistant.helpers.deprecation import ( ) from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from .const import ( # noqa: F401 _DEPRECATED_SUPPORT_DURATION, @@ -44,7 +44,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=60) -TURN_ON_SCHEMA = { +TURN_ON_SCHEMA: VolDictType = { vol.Optional(ATTR_TONE): vol.Any(vol.Coerce(int), cv.string), vol.Optional(ATTR_DURATION): cv.positive_int, vol.Optional(ATTR_VOLUME_LEVEL): cv.small_float, diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index cca0c6bc2ce..f665f5e61b3 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from .const import ATTR_ERRORS, ATTR_REMINDERS, DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubEntity, SmartTubSensorBase @@ -29,12 +30,12 @@ ATTR_UPDATED_AT = "updated_at" # how many days to snooze the reminder for ATTR_REMINDER_DAYS = "days" -RESET_REMINDER_SCHEMA = { +RESET_REMINDER_SCHEMA: VolDictType = { vol.Required(ATTR_REMINDER_DAYS): vol.All( vol.Coerce(int), vol.Range(min=30, max=365) ) } -SNOOZE_REMINDER_SCHEMA = { +SNOOZE_REMINDER_SCHEMA: VolDictType = { vol.Required(ATTR_REMINDER_DAYS): vol.All( vol.Coerce(int), vol.Range(min=10, max=120) ) diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index 3694ca81a6b..585e8859432 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from .const import DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubSensorBase @@ -31,7 +32,7 @@ SET_PRIMARY_FILTRATION_SCHEMA = vol.All( ), ) -SET_SECONDARY_FILTRATION_SCHEMA = { +SET_SECONDARY_FILTRATION_SCHEMA: VolDictType = { vol.Required(ATTR_MODE): vol.In( { mode.name.lower() diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 2280d6bc845..aac5da10ae1 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -21,6 +21,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -37,11 +38,11 @@ _LOGGER = logging.getLogger(__name__) API_CONTROL_DEVICE = "control_device" API_SET_AUTO_SHUTDOWN = "set_auto_shutdown" -SERVICE_SET_AUTO_OFF_SCHEMA = { +SERVICE_SET_AUTO_OFF_SCHEMA: VolDictType = { vol.Required(CONF_AUTO_OFF): cv.time_period_str, } -SERVICE_TURN_ON_WITH_TIMER_SCHEMA = { +SERVICE_TURN_ON_WITH_TIMER_SCHEMA: VolDictType = { vol.Required(CONF_TIMER_MINUTES): vol.All( cv.positive_int, vol.Range(min=1, max=150) ), diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 2698b6e1446..40bdb19b31b 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -28,6 +28,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from . import TadoConnector from .const import ( @@ -80,7 +81,7 @@ SERVICE_CLIMATE_TIMER = "set_climate_timer" ATTR_TIME_PERIOD = "time_period" ATTR_REQUESTED_OVERLAY = "requested_overlay" -CLIMATE_TIMER_SCHEMA = { +CLIMATE_TIMER_SCHEMA: VolDictType = { vol.Required(ATTR_TEMPERATURE): vol.Coerce(float), vol.Exclusive(ATTR_TIME_PERIOD, CONST_EXCLUSIVE_OVERLAY_GROUP): vol.All( cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() @@ -93,7 +94,7 @@ CLIMATE_TIMER_SCHEMA = { SERVICE_TEMP_OFFSET = "set_climate_temperature_offset" ATTR_OFFSET = "offset" -CLIMATE_TEMP_OFFSET_SCHEMA = { +CLIMATE_TEMP_OFFSET_SCHEMA: VolDictType = { vol.Required(ATTR_OFFSET, default=0): vol.Coerce(float), } diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index 1b3b811d231..0954db71460 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from . import TadoConnector from .const import ( @@ -55,7 +56,7 @@ WATER_HEATER_MAP_TADO = { SERVICE_WATER_HEATER_TIMER = "set_water_heater_timer" ATTR_TIME_PERIOD = "time_period" -WATER_HEATER_TIMER_SCHEMA = { +WATER_HEATER_TIMER_SCHEMA: VolDictType = { vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All( cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() ), diff --git a/homeassistant/components/upb/const.py b/homeassistant/components/upb/const.py index 8a2c435a70f..16f2f1b7923 100644 --- a/homeassistant/components/upb/const.py +++ b/homeassistant/components/upb/const.py @@ -3,6 +3,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import VolDictType DOMAIN = "upb" @@ -29,7 +30,7 @@ UPB_BRIGHTNESS_RATE_SCHEMA = vol.All( ), ) -UPB_BLINK_RATE_SCHEMA = { +UPB_BLINK_RATE_SCHEMA: VolDictType = { vol.Required(ATTR_BLINK_RATE, default=0.5): vol.All( vol.Coerce(float), vol.Range(min=0, max=4.25) ) diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index 03caa723771..4eb96256d2e 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_NAME, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import VolDictType SERVICE_UPDATE_SETTING = "update_setting" @@ -26,7 +27,7 @@ ATTR_SETTING_TYPE = "setting_type" ATTR_SETTING_NAME = "setting_name" ATTR_NEW_VALUE = "new_value" -UPDATE_SETTING_SCHEMA = { +UPDATE_SETTING_SCHEMA: VolDictType = { vol.Required(ATTR_SETTING_TYPE): vol.All(cv.string, vol.Lower, cv.slugify), vol.Required(ATTR_SETTING_NAME): vol.All(cv.string, vol.Lower, cv.slugify), vol.Required(ATTR_NEW_VALUE): vol.Any(vol.Coerce(int), cv.string), diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 3ef8aa67a3d..e1b9aaf2388 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from homeassistant.util.percentage import ( percentage_to_ranged_value, ranged_value_to_percentage, @@ -37,7 +38,7 @@ ATTR_WATER_LEVEL = "water_level" SPEED_RANGE = (FanMode.Minimum, FanMode.Maximum) # off is not included -SET_HUMIDITY_SCHEMA = { +SET_HUMIDITY_SCHEMA: VolDictType = { vol.Required(ATTR_TARGET_HUMIDITY): vol.All( vol.Coerce(float), vol.Range(min=0, max=100) ), diff --git a/homeassistant/components/yardian/switch.py b/homeassistant/components/yardian/switch.py index 549331b6b5f..910bacc1c2e 100644 --- a/homeassistant/components/yardian/switch.py +++ b/homeassistant/components/yardian/switch.py @@ -11,13 +11,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_WATERING_DURATION, DOMAIN from .coordinator import YardianUpdateCoordinator SERVICE_START_IRRIGATION = "start_irrigation" -SERVICE_SCHEMA_START_IRRIGATION = { +SERVICE_SCHEMA_START_IRRIGATION: VolDictType = { vol.Required("duration"): cv.positive_int, } diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 0ed75318ac7..9b71bbc3b16 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from .const import ( ACTION_OFF, @@ -59,7 +59,7 @@ from .scanner import YeelightScanner _LOGGER = logging.getLogger(__name__) -YEELIGHT_FLOW_TRANSITION_SCHEMA = { +YEELIGHT_FLOW_TRANSITION_SCHEMA: VolDictType = { vol.Optional(ATTR_COUNT, default=0): cv.positive_int, vol.Optional(ATTR_ACTION, default=ACTION_RECOVER): vol.Any( ACTION_RECOVER, ACTION_OFF, ACTION_STAY diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 1d514c131d2..d0d53510859 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -38,6 +38,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.typing import VolDictType import homeassistant.util.color as color_util from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, @@ -170,22 +171,22 @@ EFFECTS_MAP = { VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Range(min=1, max=100)) -SERVICE_SCHEMA_SET_MODE = { +SERVICE_SCHEMA_SET_MODE: VolDictType = { vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode]) } -SERVICE_SCHEMA_SET_MUSIC_MODE = {vol.Required(ATTR_MODE_MUSIC): cv.boolean} +SERVICE_SCHEMA_SET_MUSIC_MODE: VolDictType = {vol.Required(ATTR_MODE_MUSIC): cv.boolean} SERVICE_SCHEMA_START_FLOW = YEELIGHT_FLOW_TRANSITION_SCHEMA -SERVICE_SCHEMA_SET_COLOR_SCENE = { +SERVICE_SCHEMA_SET_COLOR_SCENE: VolDictType = { vol.Required(ATTR_RGB_COLOR): vol.All( vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)) ), vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, } -SERVICE_SCHEMA_SET_HSV_SCENE = { +SERVICE_SCHEMA_SET_HSV_SCENE: VolDictType = { vol.Required(ATTR_HS_COLOR): vol.All( vol.Coerce(tuple), vol.ExactSequence( @@ -198,14 +199,14 @@ SERVICE_SCHEMA_SET_HSV_SCENE = { vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, } -SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE = { +SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE: VolDictType = { vol.Required(ATTR_KELVIN): vol.All(vol.Coerce(int), vol.Range(min=1700, max=6500)), vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, } SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE = YEELIGHT_FLOW_TRANSITION_SCHEMA -SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE = { +SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE: VolDictType = { vol.Required(ATTR_MINUTES): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)), vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, } From de8bccb650a1515d7a27f2a7404f34b6d291b0c7 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 25 Jun 2024 20:44:06 +1000 Subject: [PATCH 0116/2411] Add services to Teslemetry (#119119) * Add custom services * Fixes * wip * Test coverage * Update homeassistant/components/teslemetry/__init__.py Co-authored-by: G Johansson * Add error translations * Translate command error * Fix test * Expand on comment as requested * Remove impossible cases --------- Co-authored-by: G Johansson --- .../components/teslemetry/__init__.py | 20 ++ .../components/teslemetry/icons.json | 8 + .../components/teslemetry/services.py | 321 ++++++++++++++++++ .../components/teslemetry/services.yaml | 132 +++++++ .../components/teslemetry/strings.json | 150 ++++++++ tests/components/teslemetry/test_services.py | 238 +++++++++++++ 6 files changed, 869 insertions(+) create mode 100644 homeassistant/components/teslemetry/services.py create mode 100644 homeassistant/components/teslemetry/services.yaml create mode 100644 tests/components/teslemetry/test_services.py diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 21ea2915884..b65f5fb64ce 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -15,8 +15,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, LOGGER, MODELS from .coordinator import ( @@ -25,6 +28,7 @@ from .coordinator import ( TeslemetryVehicleDataCoordinator, ) from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData +from .services import async_register_services PLATFORMS: Final = [ Platform.BINARY_SENSOR, @@ -43,6 +47,14 @@ PLATFORMS: Final = [ type TeslemetryConfigEntry = ConfigEntry[TeslemetryData] +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Telemetry integration.""" + async_register_services(hass) + return True + async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -> bool: """Set up Teslemetry config.""" @@ -65,6 +77,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - except TeslaFleetError as e: raise ConfigEntryNotReady from e + device_registry = dr.async_get(hass) + # Create array of classes vehicles: list[TeslemetryVehicleData] = [] energysites: list[TeslemetryEnergyData] = [] @@ -143,6 +157,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - if models: energysite.device["model"] = ", ".join(sorted(models)) + # Create the energy site device regardless of it having entities + # This is so users with a Wall Connector but without a Powerwall can still make service calls + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, **energysite.device + ) + # Setup Platforms entry.runtime_data = TeslemetryData(vehicles, energysites, scopes) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/teslemetry/icons.json b/homeassistant/components/teslemetry/icons.json index 089a3bea548..aea98e95e0b 100644 --- a/homeassistant/components/teslemetry/icons.json +++ b/homeassistant/components/teslemetry/icons.json @@ -257,5 +257,13 @@ "default": "mdi:speedometer-slow" } } + }, + "services": { + "navigation_gps_request": "mdi:crosshairs-gps", + "set_scheduled_charging": "mdi:timeline-clock-outline", + "set_scheduled_departure": "mdi:home-clock", + "speed_limit": "mdi:car-speed-limiter", + "valet_mode": "mdi:speedometer-slow", + "time_of_use": "mdi:clock-time-eight-outline" } } diff --git a/homeassistant/components/teslemetry/services.py b/homeassistant/components/teslemetry/services.py new file mode 100644 index 00000000000..97cfffa1699 --- /dev/null +++ b/homeassistant/components/teslemetry/services.py @@ -0,0 +1,321 @@ +"""Service calls for the Teslemetry integration.""" + +import logging + +import voluptuous as vol +from voluptuous import All, Range + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE_ID, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import config_validation as cv, device_registry as dr + +from .const import DOMAIN +from .helpers import handle_command, handle_vehicle_command, wake_up_vehicle +from .models import TeslemetryEnergyData, TeslemetryVehicleData + +_LOGGER = logging.getLogger(__name__) + +# Attributes +ATTR_ID = "id" +ATTR_GPS = "gps" +ATTR_TYPE = "type" +ATTR_VALUE = "value" +ATTR_LOCALE = "locale" +ATTR_ORDER = "order" +ATTR_TIMESTAMP = "timestamp" +ATTR_FIELDS = "fields" +ATTR_ENABLE = "enable" +ATTR_TIME = "time" +ATTR_PIN = "pin" +ATTR_TOU_SETTINGS = "tou_settings" +ATTR_PRECONDITIONING_ENABLED = "preconditioning_enabled" +ATTR_PRECONDITIONING_WEEKDAYS = "preconditioning_weekdays_only" +ATTR_DEPARTURE_TIME = "departure_time" +ATTR_OFF_PEAK_CHARGING_ENABLED = "off_peak_charging_enabled" +ATTR_OFF_PEAK_CHARGING_WEEKDAYS = "off_peak_charging_weekdays_only" +ATTR_END_OFF_PEAK_TIME = "end_off_peak_time" + +# Services +SERVICE_NAVIGATE_ATTR_GPS_REQUEST = "navigation_gps_request" +SERVICE_SET_SCHEDULED_CHARGING = "set_scheduled_charging" +SERVICE_SET_SCHEDULED_DEPARTURE = "set_scheduled_departure" +SERVICE_VALET_MODE = "valet_mode" +SERVICE_SPEED_LIMIT = "speed_limit" +SERVICE_TIME_OF_USE = "time_of_use" + + +def async_get_device_for_service_call( + hass: HomeAssistant, call: ServiceCall +) -> dr.DeviceEntry: + """Get the device entry related to a service call.""" + device_id = call.data[CONF_DEVICE_ID] + device_registry = dr.async_get(hass) + if (device_entry := device_registry.async_get(device_id)) is None: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_device", + translation_placeholders={"device_id": device_id}, + ) + + return device_entry + + +def async_get_config_for_device( + hass: HomeAssistant, device_entry: dr.DeviceEntry +) -> ConfigEntry: + """Get the config entry related to a device entry.""" + config_entry: ConfigEntry + for entry_id in device_entry.config_entries: + if entry := hass.config_entries.async_get_entry(entry_id): + if entry.domain == DOMAIN: + config_entry = entry + return config_entry + + +def async_get_vehicle_for_entry( + hass: HomeAssistant, device: dr.DeviceEntry, config: ConfigEntry +) -> TeslemetryVehicleData: + """Get the vehicle data for a config entry.""" + vehicle_data: TeslemetryVehicleData + assert device.serial_number is not None + for vehicle in config.runtime_data.vehicles: + if vehicle.vin == device.serial_number: + vehicle_data = vehicle + return vehicle_data + + +def async_get_energy_site_for_entry( + hass: HomeAssistant, device: dr.DeviceEntry, config: ConfigEntry +) -> TeslemetryEnergyData: + """Get the energy site data for a config entry.""" + energy_data: TeslemetryEnergyData + assert device.serial_number is not None + for energysite in config.runtime_data.energysites: + if str(energysite.id) == device.serial_number: + energy_data = energysite + return energy_data + + +def async_register_services(hass: HomeAssistant) -> None: # noqa: C901 + """Set up the Teslemetry services.""" + + async def navigate_gps_request(call: ServiceCall) -> None: + """Send lat,lon,order with a vehicle.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + vehicle = async_get_vehicle_for_entry(hass, device, config) + + await wake_up_vehicle(vehicle) + await handle_vehicle_command( + vehicle.api.navigation_gps_request( + lat=call.data[ATTR_GPS][CONF_LATITUDE], + lon=call.data[ATTR_GPS][CONF_LONGITUDE], + order=call.data.get(ATTR_ORDER), + ) + ) + + hass.services.async_register( + DOMAIN, + SERVICE_NAVIGATE_ATTR_GPS_REQUEST, + navigate_gps_request, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(ATTR_GPS): { + vol.Required(CONF_LATITUDE): cv.latitude, + vol.Required(CONF_LONGITUDE): cv.longitude, + }, + vol.Optional(ATTR_ORDER): cv.positive_int, + } + ), + ) + + async def set_scheduled_charging(call: ServiceCall) -> None: + """Configure fleet telemetry.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + vehicle = async_get_vehicle_for_entry(hass, device, config) + + time: int | None = None + # Convert time to minutes since minute + if "time" in call.data: + (hours, minutes, *seconds) = call.data["time"].split(":") + time = int(hours) * 60 + int(minutes) + elif call.data["enable"]: + raise ServiceValidationError( + translation_domain=DOMAIN, translation_key="set_scheduled_charging_time" + ) + + await wake_up_vehicle(vehicle) + await handle_vehicle_command( + vehicle.api.set_scheduled_charging(enable=call.data["enable"], time=time) + ) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_SCHEDULED_CHARGING, + set_scheduled_charging, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(ATTR_ENABLE): bool, + vol.Optional(ATTR_TIME): str, + } + ), + ) + + async def set_scheduled_departure(call: ServiceCall) -> None: + """Configure fleet telemetry.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + vehicle = async_get_vehicle_for_entry(hass, device, config) + + enable = call.data.get("enable", True) + + # Preconditioning + preconditioning_enabled = call.data.get(ATTR_PRECONDITIONING_ENABLED, False) + preconditioning_weekdays_only = call.data.get( + ATTR_PRECONDITIONING_WEEKDAYS, False + ) + departure_time: int | None = None + if ATTR_DEPARTURE_TIME in call.data: + (hours, minutes, *seconds) = call.data[ATTR_DEPARTURE_TIME].split(":") + departure_time = int(hours) * 60 + int(minutes) + elif preconditioning_enabled: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="set_scheduled_departure_preconditioning", + ) + + # Off peak charging + off_peak_charging_enabled = call.data.get(ATTR_OFF_PEAK_CHARGING_ENABLED, False) + off_peak_charging_weekdays_only = call.data.get( + ATTR_OFF_PEAK_CHARGING_WEEKDAYS, False + ) + end_off_peak_time: int | None = None + + if ATTR_END_OFF_PEAK_TIME in call.data: + (hours, minutes, *seconds) = call.data[ATTR_END_OFF_PEAK_TIME].split(":") + end_off_peak_time = int(hours) * 60 + int(minutes) + elif off_peak_charging_enabled: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="set_scheduled_departure_off_peak", + ) + + await wake_up_vehicle(vehicle) + await handle_vehicle_command( + vehicle.api.set_scheduled_departure( + enable, + preconditioning_enabled, + preconditioning_weekdays_only, + departure_time, + off_peak_charging_enabled, + off_peak_charging_weekdays_only, + end_off_peak_time, + ) + ) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_SCHEDULED_DEPARTURE, + set_scheduled_departure, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Optional(ATTR_ENABLE): bool, + vol.Optional(ATTR_PRECONDITIONING_ENABLED): bool, + vol.Optional(ATTR_PRECONDITIONING_WEEKDAYS): bool, + vol.Optional(ATTR_DEPARTURE_TIME): str, + vol.Optional(ATTR_OFF_PEAK_CHARGING_ENABLED): bool, + vol.Optional(ATTR_OFF_PEAK_CHARGING_WEEKDAYS): bool, + vol.Optional(ATTR_END_OFF_PEAK_TIME): str, + } + ), + ) + + async def valet_mode(call: ServiceCall) -> None: + """Configure fleet telemetry.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + vehicle = async_get_vehicle_for_entry(hass, device, config) + + await wake_up_vehicle(vehicle) + await handle_vehicle_command( + vehicle.api.set_valet_mode( + call.data.get("enable"), call.data.get("pin", "") + ) + ) + + hass.services.async_register( + DOMAIN, + SERVICE_VALET_MODE, + valet_mode, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(ATTR_ENABLE): cv.boolean, + vol.Required(ATTR_PIN): All(cv.positive_int, Range(min=1000, max=9999)), + } + ), + ) + + async def speed_limit(call: ServiceCall) -> None: + """Configure fleet telemetry.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + vehicle = async_get_vehicle_for_entry(hass, device, config) + + await wake_up_vehicle(vehicle) + enable = call.data.get("enable") + if enable is True: + await handle_vehicle_command( + vehicle.api.speed_limit_activate(call.data.get("pin")) + ) + elif enable is False: + await handle_vehicle_command( + vehicle.api.speed_limit_deactivate(call.data.get("pin")) + ) + + hass.services.async_register( + DOMAIN, + SERVICE_SPEED_LIMIT, + speed_limit, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(ATTR_ENABLE): cv.boolean, + vol.Required(ATTR_PIN): All(cv.positive_int, Range(min=1000, max=9999)), + } + ), + ) + + async def time_of_use(call: ServiceCall) -> None: + """Configure time of use settings.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + site = async_get_energy_site_for_entry(hass, device, config) + + resp = await handle_command( + site.api.time_of_use_settings(call.data.get(ATTR_TOU_SETTINGS)) + ) + if "error" in resp: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="command_error", + translation_placeholders={"error": resp["error"]}, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_TIME_OF_USE, + time_of_use, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(ATTR_TOU_SETTINGS): dict, + } + ), + ) diff --git a/homeassistant/components/teslemetry/services.yaml b/homeassistant/components/teslemetry/services.yaml new file mode 100644 index 00000000000..e98f124dd19 --- /dev/null +++ b/homeassistant/components/teslemetry/services.yaml @@ -0,0 +1,132 @@ +navigation_gps_request: + fields: + device_id: + required: true + selector: + device: + filter: + - integration: teslemetry + gps: + required: true + example: '{"latitude": -27.9699373, "longitude": 153.4081865}' + selector: + location: + radius: false + order: + required: false + default: 1 + selector: + number: + +time_of_use: + fields: + device_id: + required: true + selector: + device: + filter: + - integration: teslemetry + tou_settings: + required: true + selector: + object: + +set_scheduled_charging: + fields: + device_id: + required: true + selector: + device: + filter: + integration: teslemetry + enable: + required: true + default: true + selector: + boolean: + time: + required: false + selector: + time: + +set_scheduled_departure: + fields: + device_id: + required: true + selector: + device: + filter: + integration: teslemetry + enable: + required: false + default: true + selector: + boolean: + preconditioning_enabled: + required: false + default: false + selector: + boolean: + preconditioning_weekdays_only: + required: false + default: false + selector: + boolean: + departure_time: + required: false + selector: + time: + off_peak_charging_enabled: + required: false + default: false + selector: + boolean: + off_peak_charging_weekdays_only: + required: false + default: false + selector: + boolean: + end_off_peak_time: + required: false + selector: + time: + +valet_mode: + fields: + device_id: + required: true + selector: + device: + filter: + integration: teslemetry + enable: + required: true + selector: + boolean: + pin: + required: true + selector: + number: + min: 1000 + max: 9999 + mode: box + +speed_limit: + fields: + device_id: + required: true + selector: + device: + filter: + integration: teslemetry + enable: + required: true + selector: + boolean: + pin: + required: true + selector: + number: + min: 1000 + max: 9999 + mode: box diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index fe45b4ee9e3..9ff14f2dc8c 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -473,6 +473,156 @@ }, "invalid_cop_temp": { "message": "Cabin overheat protection does not support that temperature" + }, + "set_scheduled_charging_time": { + "message": "Time required to complete the operation" + }, + "set_scheduled_departure_preconditioning": { + "message": "Departure time required to enable preconditioning" + }, + "set_scheduled_departure_off_peak": { + "message": "To enable scheduled departure, end off peak time is required." + }, + "invalid_device": { + "message": "Invalid device ID: {device_id}" + }, + "no_config_entry_for_device": { + "message": "No config entry for device ID: {device_id}" + }, + "no_vehicle_data_for_device": { + "message": "No vehicle data for device ID: {device_id}" + }, + "no_energy_site_data_for_device": { + "message": "No energy site data for device ID: {device_id}" + }, + "command_error": { + "message": "Command returned error: {error}" + } + }, + "services": { + "navigation_gps_request": { + "description": "Set vehicle navigation to the provided latitude/longitude coordinates.", + "fields": { + "device_id": { + "description": "Vehicle to share to.", + "name": "Vehicle" + }, + "gps": { + "description": "Location to navigate to.", + "name": "Location" + }, + "order": { + "description": "Order for this destination if specifying multiple destinations.", + "name": "Order" + } + }, + "name": "Navigate to coordinates" + }, + "set_scheduled_charging": { + "description": "Sets a time at which charging should be completed.", + "fields": { + "device_id": { + "description": "Vehicle to schedule.", + "name": "Vehicle" + }, + "enable": { + "description": "Enable or disable scheduled charging.", + "name": "Enable" + }, + "time": { + "description": "Time to start charging.", + "name": "Time" + } + }, + "name": "Set scheduled charging" + }, + "set_scheduled_departure": { + "description": "Sets a time at which departure should be completed.", + "fields": { + "departure_time": { + "description": "Time to be preconditioned by.", + "name": "Departure time" + }, + "device_id": { + "description": "Vehicle to schedule.", + "name": "Vehicle" + }, + "enable": { + "description": "Enable or disable scheduled departure.", + "name": "Enable" + }, + "end_off_peak_time": { + "description": "Time to complete charging by.", + "name": "End off peak time" + }, + "off_peak_charging_enabled": { + "description": "Enable off peak charging.", + "name": "Off peak charging enabled" + }, + "off_peak_charging_weekdays_only": { + "description": "Enable off peak charging on weekdays only.", + "name": "Off peak charging weekdays only" + }, + "preconditioning_enabled": { + "description": "Enable preconditioning.", + "name": "Preconditioning enabled" + }, + "preconditioning_weekdays_only": { + "description": "Enable preconditioning on weekdays only.", + "name": "Preconditioning weekdays only" + } + }, + "name": "Set scheduled departure" + }, + "speed_limit": { + "description": "Activate the speed limit of the vehicle.", + "fields": { + "device_id": { + "description": "Vehicle to limit.", + "name": "Vehicle" + }, + "enable": { + "description": "Enable or disable speed limit.", + "name": "Enable" + }, + "pin": { + "description": "4 digit PIN.", + "name": "PIN" + } + }, + "name": "Set speed limit" + }, + "time_of_use": { + "description": "Update the time of use settings for the energy site.", + "fields": { + "device_id": { + "description": "Energy Site to configure.", + "name": "Energy Site" + }, + "tou_settings": { + "description": "See https://developer.tesla.com/docs/fleet-api#time_of_use_settings for details.", + "name": "Settings" + } + }, + "name": "Time of use settings" + }, + "valet_mode": { + "description": "Activate the valet mode of the vehicle.", + "fields": { + "device_id": { + "description": "Vehicle to limit.", + "name": "Vehicle" + }, + "enable": { + "description": "Enable or disable valet mode.", + "name": "Enable" + }, + "pin": { + "description": "4 digit PIN.", + "name": "PIN" + } + }, + "name": "Set valet mode" } } } diff --git a/tests/components/teslemetry/test_services.py b/tests/components/teslemetry/test_services.py new file mode 100644 index 00000000000..a5b55f5dcc5 --- /dev/null +++ b/tests/components/teslemetry/test_services.py @@ -0,0 +1,238 @@ +"""Test the Teslemetry services.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.teslemetry.const import DOMAIN +from homeassistant.components.teslemetry.services import ( + ATTR_DEPARTURE_TIME, + ATTR_ENABLE, + ATTR_END_OFF_PEAK_TIME, + ATTR_GPS, + ATTR_OFF_PEAK_CHARGING_ENABLED, + ATTR_OFF_PEAK_CHARGING_WEEKDAYS, + ATTR_PIN, + ATTR_PRECONDITIONING_ENABLED, + ATTR_PRECONDITIONING_WEEKDAYS, + ATTR_TIME, + ATTR_TOU_SETTINGS, + SERVICE_NAVIGATE_ATTR_GPS_REQUEST, + SERVICE_SET_SCHEDULED_CHARGING, + SERVICE_SET_SCHEDULED_DEPARTURE, + SERVICE_SPEED_LIMIT, + SERVICE_TIME_OF_USE, + SERVICE_VALET_MODE, +) +from homeassistant.const import CONF_DEVICE_ID, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import entity_registry as er + +from . import setup_platform +from .const import COMMAND_ERROR, COMMAND_OK + +lat = -27.9699373 +lon = 153.3726526 + + +async def test_services( + hass: HomeAssistant, +) -> None: + """Tests that the custom services are correct.""" + + await setup_platform(hass) + entity_registry = er.async_get(hass) + + # Get a vehicle device ID + vehicle_device = entity_registry.async_get("sensor.test_charging").device_id + energy_device = entity_registry.async_get( + "sensor.energy_site_battery_power" + ).device_id + + with patch( + "homeassistant.components.teslemetry.VehicleSpecific.navigation_gps_request", + return_value=COMMAND_OK, + ) as navigation_gps_request: + await hass.services.async_call( + DOMAIN, + SERVICE_NAVIGATE_ATTR_GPS_REQUEST, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_GPS: {CONF_LATITUDE: lat, CONF_LONGITUDE: lon}, + }, + blocking=True, + ) + navigation_gps_request.assert_called_once() + + with patch( + "homeassistant.components.teslemetry.VehicleSpecific.set_scheduled_charging", + return_value=COMMAND_OK, + ) as set_scheduled_charging: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SCHEDULED_CHARGING, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: True, + ATTR_TIME: "6:00", + }, + blocking=True, + ) + set_scheduled_charging.assert_called_once() + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SCHEDULED_CHARGING, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: True, + }, + blocking=True, + ) + + with patch( + "homeassistant.components.teslemetry.VehicleSpecific.set_scheduled_departure", + return_value=COMMAND_OK, + ) as set_scheduled_departure: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SCHEDULED_DEPARTURE, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: True, + ATTR_PRECONDITIONING_ENABLED: True, + ATTR_PRECONDITIONING_WEEKDAYS: False, + ATTR_DEPARTURE_TIME: "6:00", + ATTR_OFF_PEAK_CHARGING_ENABLED: True, + ATTR_OFF_PEAK_CHARGING_WEEKDAYS: False, + ATTR_END_OFF_PEAK_TIME: "5:00", + }, + blocking=True, + ) + set_scheduled_departure.assert_called_once() + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SCHEDULED_DEPARTURE, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: True, + ATTR_PRECONDITIONING_ENABLED: True, + }, + blocking=True, + ) + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SCHEDULED_DEPARTURE, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: True, + ATTR_OFF_PEAK_CHARGING_ENABLED: True, + }, + blocking=True, + ) + + with patch( + "homeassistant.components.teslemetry.VehicleSpecific.set_valet_mode", + return_value=COMMAND_OK, + ) as set_valet_mode: + await hass.services.async_call( + DOMAIN, + SERVICE_VALET_MODE, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: True, + ATTR_PIN: 1234, + }, + blocking=True, + ) + set_valet_mode.assert_called_once() + + with patch( + "homeassistant.components.teslemetry.VehicleSpecific.speed_limit_activate", + return_value=COMMAND_OK, + ) as speed_limit_activate: + await hass.services.async_call( + DOMAIN, + SERVICE_SPEED_LIMIT, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: True, + ATTR_PIN: 1234, + }, + blocking=True, + ) + speed_limit_activate.assert_called_once() + + with patch( + "homeassistant.components.teslemetry.VehicleSpecific.speed_limit_deactivate", + return_value=COMMAND_OK, + ) as speed_limit_deactivate: + await hass.services.async_call( + DOMAIN, + SERVICE_SPEED_LIMIT, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ENABLE: False, + ATTR_PIN: 1234, + }, + blocking=True, + ) + speed_limit_deactivate.assert_called_once() + + with patch( + "homeassistant.components.teslemetry.EnergySpecific.time_of_use_settings", + return_value=COMMAND_OK, + ) as set_time_of_use: + await hass.services.async_call( + DOMAIN, + SERVICE_TIME_OF_USE, + { + CONF_DEVICE_ID: energy_device, + ATTR_TOU_SETTINGS: {}, + }, + blocking=True, + ) + set_time_of_use.assert_called_once() + + with ( + patch( + "homeassistant.components.teslemetry.EnergySpecific.time_of_use_settings", + return_value=COMMAND_ERROR, + ) as set_time_of_use, + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + DOMAIN, + SERVICE_TIME_OF_USE, + { + CONF_DEVICE_ID: energy_device, + ATTR_TOU_SETTINGS: {}, + }, + blocking=True, + ) + + +async def test_service_validation_errors( + hass: HomeAssistant, +) -> None: + """Tests that the custom services handle bad data.""" + + await setup_platform(hass) + + # Bad device ID + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + DOMAIN, + SERVICE_NAVIGATE_ATTR_GPS_REQUEST, + { + CONF_DEVICE_ID: "nope", + ATTR_GPS: {CONF_LATITUDE: lat, CONF_LONGITUDE: lon}, + }, + blocking=True, + ) From 62fd692d2704715adf0514b1e626576a9cc54720 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:48:00 +0200 Subject: [PATCH 0117/2411] Improve async_register_admin_service schema typing (#120405) --- homeassistant/components/zha/websocket_api.py | 5 +++-- homeassistant/helpers/service.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/websocket_api.py b/homeassistant/components/zha/websocket_api.py index 1a51a06243e..cb95e930b1a 100644 --- a/homeassistant/components/zha/websocket_api.py +++ b/homeassistant/components/zha/websocket_api.py @@ -21,6 +21,7 @@ from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.typing import VolDictType, VolSchemaType from .api import ( async_change_channel, @@ -126,7 +127,7 @@ def _ensure_list_if_present[_T](value: _T | None) -> list[_T] | list[Any] | None return cast("list[_T]", value) if isinstance(value, list) else [value] -SERVICE_PERMIT_PARAMS = { +SERVICE_PERMIT_PARAMS: VolDictType = { vol.Optional(ATTR_IEEE): IEEE_SCHEMA, vol.Optional(ATTR_DURATION, default=60): vol.All( vol.Coerce(int), vol.Range(0, 254) @@ -138,7 +139,7 @@ SERVICE_PERMIT_PARAMS = { vol.Exclusive(ATTR_QR_CODE, "install_code"): vol.All(cv.string, qr_to_install_code), } -SERVICE_SCHEMAS = { +SERVICE_SCHEMAS: dict[str, VolSchemaType] = { SERVICE_PERMIT: vol.Schema( vol.All( cv.deprecated(ATTR_IEEE_ADDRESS, replacement_key=ATTR_IEEE), diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index a9959902084..22f5e7f8710 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -63,7 +63,7 @@ from . import ( ) from .group import expand_entity_ids from .selector import TargetSelector -from .typing import ConfigType, TemplateVarsType +from .typing import ConfigType, TemplateVarsType, VolSchemaType if TYPE_CHECKING: from .entity import Entity @@ -1100,7 +1100,7 @@ def async_register_admin_service( domain: str, service: str, service_func: Callable[[ServiceCall], Awaitable[None] | None], - schema: vol.Schema = vol.Schema({}, extra=vol.PREVENT_EXTRA), + schema: VolSchemaType = vol.Schema({}, extra=vol.PREVENT_EXTRA), ) -> None: """Register a service that requires admin access.""" hass.services.async_register( From 7453b7df63d7fe6a330d1612fa0d0ff5a69886f5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 13:03:18 +0200 Subject: [PATCH 0118/2411] Improve mqtt schema typing (#120407) --- homeassistant/components/mqtt/climate.py | 4 ++-- homeassistant/components/mqtt/cover.py | 4 ++-- homeassistant/components/mqtt/device_tracker.py | 4 ++-- homeassistant/components/mqtt/event.py | 4 ++-- homeassistant/components/mqtt/fan.py | 4 ++-- homeassistant/components/mqtt/humidifier.py | 4 ++-- homeassistant/components/mqtt/image.py | 4 ++-- homeassistant/components/mqtt/lawn_mower.py | 4 ++-- homeassistant/components/mqtt/light/__init__.py | 6 +++--- homeassistant/components/mqtt/light/schema_basic.py | 4 ++-- homeassistant/components/mqtt/light/schema_json.py | 4 ++-- .../components/mqtt/light/schema_template.py | 4 ++-- homeassistant/components/mqtt/mixins.py | 7 ++++--- homeassistant/components/mqtt/models.py | 11 +++++++---- homeassistant/components/mqtt/number.py | 4 ++-- homeassistant/components/mqtt/select.py | 4 ++-- homeassistant/components/mqtt/sensor.py | 4 ++-- homeassistant/components/mqtt/siren.py | 4 ++-- homeassistant/components/mqtt/text.py | 4 ++-- homeassistant/components/mqtt/update.py | 4 ++-- homeassistant/components/mqtt/vacuum.py | 4 ++-- homeassistant/components/mqtt/valve.py | 4 ++-- homeassistant/components/mqtt/water_heater.py | 4 ++-- 23 files changed, 54 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index f63c9ecc7ae..7873b056889 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -47,7 +47,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util.unit_conversion import TemperatureConverter from . import subscription @@ -550,7 +550,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): _enable_turn_on_off_backwards_compatibility = False @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index bd79c0f9470..2d1b64d002a 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -31,7 +31,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.service_info.mqtt import ReceivePayloadType -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads from homeassistant.util.percentage import ( percentage_to_ranged_value, @@ -246,7 +246,7 @@ class MqttCover(MqttEntity, CoverEntity): _tilt_range: tuple[int, int] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index 082483a64a3..b2aeb4c0fc1 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -27,7 +27,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from . import subscription from .config import MQTT_BASE_SCHEMA @@ -103,7 +103,7 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/event.py b/homeassistant/components/mqtt/event.py index 15b70b1b98d..5e801fda54b 100644 --- a/homeassistant/components/mqtt/event.py +++ b/homeassistant/components/mqtt/event.py @@ -19,7 +19,7 @@ from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLAT 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 homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads_object from . import subscription @@ -94,7 +94,7 @@ class MqttEvent(MqttEntity, EventEntity): _template: Callable[[ReceivePayloadType, PayloadSentinel], ReceivePayloadType] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 1933b5e17b5..dd777bd178e 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -30,7 +30,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util.percentage import ( percentage_to_ranged_value, ranged_value_to_percentage, @@ -226,7 +226,7 @@ class MqttFan(MqttEntity, FanEntity): _speed_range: tuple[int, int] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 8f7eda21240..a4510ee5951 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -33,7 +33,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from . import subscription from .config import MQTT_RW_SCHEMA @@ -212,7 +212,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): _topic: dict[str, Any] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/image.py b/homeassistant/components/mqtt/image.py index d5930a1668a..30fd102764d 100644 --- a/homeassistant/components/mqtt/image.py +++ b/homeassistant/components/mqtt/image.py @@ -20,7 +20,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.service_info.mqtt import ReceivePayloadType -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolSchemaType from homeassistant.util import dt as dt_util from . import subscription @@ -117,7 +117,7 @@ class MqttImage(MqttEntity, ImageEntity): MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/lawn_mower.py b/homeassistant/components/mqtt/lawn_mower.py index 853ce743f12..a74d278401c 100644 --- a/homeassistant/components/mqtt/lawn_mower.py +++ b/homeassistant/components/mqtt/lawn_mower.py @@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from . import subscription from .config import MQTT_BASE_SCHEMA @@ -103,7 +103,7 @@ class MqttLawnMower(MqttEntity, LawnMowerEntity, RestoreEntity): _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index ac2d1ff14ee..04619b08e11 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -10,7 +10,7 @@ from homeassistant.components import light from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from ..mixins import async_setup_entity_entry_helper from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA @@ -33,7 +33,7 @@ from .schema_template import ( def validate_mqtt_light_discovery(config_value: dict[str, Any]) -> ConfigType: """Validate MQTT light schema for discovery.""" - schemas = { + schemas: dict[str, VolSchemaType] = { "basic": DISCOVERY_SCHEMA_BASIC, "json": DISCOVERY_SCHEMA_JSON, "template": DISCOVERY_SCHEMA_TEMPLATE, @@ -44,7 +44,7 @@ def validate_mqtt_light_discovery(config_value: dict[str, Any]) -> ConfigType: def validate_mqtt_light_modern(config_value: dict[str, Any]) -> ConfigType: """Validate MQTT light schema for setup from configuration.yaml.""" - schemas = { + schemas: dict[str, VolSchemaType] = { "basic": PLATFORM_SCHEMA_MODERN_BASIC, "json": PLATFORM_SCHEMA_MODERN_JSON, "template": PLATFORM_SCHEMA_MODERN_TEMPLATE, diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 565cf4d7132..b0ffae4e328 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -39,7 +39,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType import homeassistant.util.color as color_util from .. import subscription @@ -249,7 +249,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): _optimistic_xy_color: bool @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA_BASIC diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 1d3ad3a6ef0..58fde4a3800 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -51,7 +51,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.json import json_dumps from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType import homeassistant.util.color as color_util from homeassistant.util.json import json_loads_object from homeassistant.util.yaml import dump as yaml_dump @@ -267,7 +267,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): _deprecated_color_handling: bool = False @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA_JSON diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index d414f219241..c35b0e6ced9 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -31,7 +31,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType, VolSchemaType import homeassistant.util.color as color_util from .. import subscription @@ -120,7 +120,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): _topics: dict[str, str | None] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA_TEMPLATE diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 55b76337db0..0800aeb8ee4 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -53,6 +53,7 @@ from homeassistant.helpers.typing import ( ConfigType, DiscoveryInfoType, UndefinedType, + VolSchemaType, ) from homeassistant.util.json import json_loads from homeassistant.util.yaml import dump as yaml_dump @@ -247,8 +248,8 @@ def async_setup_entity_entry_helper( entity_class: type[MqttEntity] | None, domain: str, async_add_entities: AddEntitiesCallback, - discovery_schema: vol.Schema, - platform_schema_modern: vol.Schema, + discovery_schema: VolSchemaType, + platform_schema_modern: VolSchemaType, schema_class_mapping: dict[str, type[MqttEntity]] | None = None, ) -> None: """Set up entity creation dynamically through MQTT discovery.""" @@ -1187,7 +1188,7 @@ class MqttEntity( @staticmethod @abstractmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" def _set_entity_name(self, config: ConfigType) -> None: diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index f26ed196663..e5a9a9c44da 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -11,15 +11,18 @@ from enum import StrEnum import logging from typing import TYPE_CHECKING, Any, TypedDict -import voluptuous as vol - from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError, TemplateError from homeassistant.helpers import template from homeassistant.helpers.entity import Entity from homeassistant.helpers.service_info.mqtt import ReceivePayloadType -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, TemplateVarsType +from homeassistant.helpers.typing import ( + ConfigType, + DiscoveryInfoType, + TemplateVarsType, + VolSchemaType, +) from homeassistant.util.hass_dict import HassKey if TYPE_CHECKING: @@ -418,7 +421,7 @@ class MqttData: platforms_loaded: set[Platform | str] = field(default_factory=set) reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list) reload_handlers: dict[str, CALLBACK_TYPE] = field(default_factory=dict) - reload_schema: dict[str, vol.Schema] = field(default_factory=dict) + reload_schema: dict[str, VolSchemaType] = field(default_factory=dict) state_write_requests: EntityTopicState = field(default_factory=EntityTopicState) subscriptions_to_restore: list[Subscription] = field(default_factory=list) tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 50a4f398c7d..e8f2cf0cfe4 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -28,7 +28,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from . import subscription from .config import MQTT_RW_SCHEMA @@ -133,7 +133,7 @@ class MqttNumber(MqttEntity, RestoreNumber): _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index ea0a0886082..5cc7a586c71 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from . import subscription from .config import MQTT_RW_SCHEMA @@ -84,7 +84,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): _optimistic: bool = False @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 043bc9a5c0e..4a41f486831 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -33,7 +33,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util import dt as dt_util from . import subscription @@ -185,7 +185,7 @@ class MqttSensor(MqttEntity, RestoreSensor): await MqttEntity.async_will_remove_from_hass(self) @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 49645f7b1b4..9f1466dd95d 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -32,7 +32,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.json import json_dumps from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType, VolSchemaType from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads_object from . import subscription @@ -142,7 +142,7 @@ class MqttSiren(MqttEntity, SirenEntity): _optimistic: bool @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/text.py b/homeassistant/components/mqtt/text.py index 73adaa2cb0c..0b122dec7b5 100644 --- a/homeassistant/components/mqtt/text.py +++ b/homeassistant/components/mqtt/text.py @@ -22,7 +22,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from . import subscription from .config import MQTT_RW_SCHEMA @@ -121,7 +121,7 @@ class MqttTextEntity(MqttEntity, TextEntity): _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index eecd7b967de..4b87e0ef7da 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads from . import subscription @@ -107,7 +107,7 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity): return super().entity_picture @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index eac3556a28b..8aa42270016 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -35,7 +35,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.json import json_dumps -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolSchemaType from homeassistant.util.json import json_loads_object from . import subscription @@ -281,7 +281,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/valve.py b/homeassistant/components/mqtt/valve.py index f3c76462269..02127dfc19c 100644 --- a/homeassistant/components/mqtt/valve.py +++ b/homeassistant/components/mqtt/valve.py @@ -28,7 +28,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads from homeassistant.util.percentage import ( percentage_to_ranged_value, @@ -163,7 +163,7 @@ class MqttValve(MqttEntity, ValveEntity): _tilt_optimistic: bool @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA diff --git a/homeassistant/components/mqtt/water_heater.py b/homeassistant/components/mqtt/water_heater.py index ac3c8aacc92..13b0478210f 100644 --- a/homeassistant/components/mqtt/water_heater.py +++ b/homeassistant/components/mqtt/water_heater.py @@ -38,7 +38,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.util.unit_conversion import TemperatureConverter from .climate import MqttTemperatureControlEntity @@ -188,7 +188,7 @@ class MqttWaterHeater(MqttTemperatureControlEntity, WaterHeaterEntity): _attr_target_temperature_high: float | None = None @staticmethod - def config_schema() -> vol.Schema: + def config_schema() -> VolSchemaType: """Return the config schema.""" return DISCOVERY_SCHEMA From fccb7ea1f9596733f9869b0f69c7378d463fc5bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 13:10:09 +0200 Subject: [PATCH 0119/2411] Migrate ESPHome to use entry.runtime_data (#120402) --- homeassistant/components/esphome/__init__.py | 11 ++++---- .../components/esphome/alarm_control_panel.py | 6 ++-- .../components/esphome/binary_sensor.py | 9 +++--- homeassistant/components/esphome/button.py | 6 ++-- homeassistant/components/esphome/camera.py | 6 ++-- homeassistant/components/esphome/climate.py | 6 ++-- homeassistant/components/esphome/cover.py | 6 ++-- homeassistant/components/esphome/dashboard.py | 2 +- homeassistant/components/esphome/date.py | 4 +-- homeassistant/components/esphome/datetime.py | 4 +-- .../components/esphome/diagnostics.py | 8 +++--- .../components/esphome/domain_data.py | 28 ++++++++----------- homeassistant/components/esphome/entity.py | 9 ++---- .../components/esphome/entry_data.py | 9 ++++-- homeassistant/components/esphome/event.py | 6 ++-- homeassistant/components/esphome/fan.py | 6 ++-- homeassistant/components/esphome/light.py | 6 ++-- homeassistant/components/esphome/lock.py | 6 ++-- homeassistant/components/esphome/manager.py | 14 +++++----- homeassistant/components/esphome/select.py | 8 ++---- homeassistant/components/esphome/switch.py | 6 ++-- homeassistant/components/esphome/valve.py | 6 ++-- 22 files changed, 93 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 3de5d48391f..3af95576c18 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -5,7 +5,6 @@ from __future__ import annotations from aioesphomeapi import APIClient from homeassistant.components import zeroconf -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -21,7 +20,7 @@ from .dashboard import async_setup as async_setup_dashboard from .domain_data import DomainData # Import config flow so that it's added to the registry -from .entry_data import RuntimeEntryData +from .entry_data import ESPHomeConfigEntry, RuntimeEntryData from .manager import ESPHomeManager, cleanup_instance CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) @@ -33,7 +32,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> bool: """Set up the esphome component.""" host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] @@ -59,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: store=domain_data.get_or_create_store(hass, entry), original_options=dict(entry.options), ) - domain_data.set_entry_data(entry, entry_data) + entry.runtime_data = entry_data manager = ESPHomeManager( hass, entry, host, password, cli, zeroconf_instance, domain_data, entry_data @@ -69,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> bool: """Unload an esphome config entry.""" entry_data = await cleanup_instance(hass, entry) return await hass.config_entries.async_unload_platforms( @@ -77,6 +76,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) -async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_remove_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> None: """Remove an esphome config entry.""" await DomainData.get(hass).get_or_create_store(hass, entry).async_remove() diff --git a/homeassistant/components/esphome/alarm_control_panel.py b/homeassistant/components/esphome/alarm_control_panel.py index 54bce4e6015..17079fe8c6a 100644 --- a/homeassistant/components/esphome/alarm_control_panel.py +++ b/homeassistant/components/esphome/alarm_control_panel.py @@ -16,7 +16,6 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, CodeFormat, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, @@ -38,6 +37,7 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry from .enum_mapper import EsphomeEnumMapper _ESPHOME_ACP_STATE_TO_HASS_STATE: EsphomeEnumMapper[AlarmControlPanelState, str] = ( @@ -70,7 +70,9 @@ class EspHomeACPFeatures(APIIntEnum): async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome switches based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 05ddfc2c43f..32d96785601 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -9,17 +9,18 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum -from .domain_data import DomainData from .entity import EsphomeAssistEntity, EsphomeEntity, platform_async_setup_entry +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome binary sensors based on a config entry.""" await platform_async_setup_entry( @@ -31,7 +32,7 @@ async def async_setup_entry( state_type=BinarySensorState, ) - entry_data = DomainData.get(hass).get_entry_data(entry) + entry_data = entry.runtime_data assert entry_data.device_info is not None if entry_data.device_info.voice_assistant_feature_flags_compat( entry_data.api_version diff --git a/homeassistant/components/esphome/button.py b/homeassistant/components/esphome/button.py index a825bb9b9b4..8883c4b6bea 100644 --- a/homeassistant/components/esphome/button.py +++ b/homeassistant/components/esphome/button.py @@ -5,7 +5,6 @@ from __future__ import annotations from aioesphomeapi import ButtonInfo, EntityInfo, EntityState from homeassistant.components.button import ButtonDeviceClass, ButtonEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum @@ -15,10 +14,13 @@ from .entity import ( convert_api_error_ha_error, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome buttons based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 83cf8d03e78..abe7f6809e6 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -12,15 +12,17 @@ from aiohttp import web from homeassistant.components import camera from homeassistant.components.camera import Camera -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import EsphomeEntity, platform_async_setup_entry +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up esphome cameras based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 4225f60af0c..6c82207ddc9 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -45,7 +45,6 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, PRECISION_HALVES, @@ -62,13 +61,16 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry from .enum_mapper import EsphomeEnumMapper FAN_QUIET = "quiet" async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome climate devices based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 0b845c255a3..4597b4f3566 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -13,7 +13,6 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum @@ -24,10 +23,13 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome covers based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index b2d0487df9c..b0a37aefd0d 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -106,7 +106,7 @@ class ESPHomeDashboardManager: reloads = [ hass.config_entries.async_reload(entry.entry_id) for entry in hass.config_entries.async_entries(DOMAIN) - if entry.state == ConfigEntryState.LOADED + if entry.state is ConfigEntryState.LOADED ] # Re-auth flows will check the dashboard for encryption key when the form is requested # but we only trigger reauth if the dashboard is available. diff --git a/homeassistant/components/esphome/date.py b/homeassistant/components/esphome/date.py index 9998eea1a5d..eb26ec918d0 100644 --- a/homeassistant/components/esphome/date.py +++ b/homeassistant/components/esphome/date.py @@ -7,16 +7,16 @@ from datetime import date from aioesphomeapi import DateInfo, DateState from homeassistant.components.date import DateEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ESPHomeConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up esphome dates based on a config entry.""" diff --git a/homeassistant/components/esphome/datetime.py b/homeassistant/components/esphome/datetime.py index 15509a46158..5d578ae4928 100644 --- a/homeassistant/components/esphome/datetime.py +++ b/homeassistant/components/esphome/datetime.py @@ -7,17 +7,17 @@ from datetime import datetime from aioesphomeapi import DateTimeInfo, DateTimeState from homeassistant.components.datetime import DateTimeEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ESPHomeConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up esphome datetimes based on a config entry.""" diff --git a/homeassistant/components/esphome/diagnostics.py b/homeassistant/components/esphome/diagnostics.py index 44241f5950c..58c9a8fe666 100644 --- a/homeassistant/components/esphome/diagnostics.py +++ b/homeassistant/components/esphome/diagnostics.py @@ -6,12 +6,12 @@ from typing import Any from homeassistant.components.bluetooth import async_scanner_by_source from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant -from . import CONF_NOISE_PSK, DomainData +from . import CONF_NOISE_PSK from .dashboard import async_get_dashboard +from .entry_data import ESPHomeConfigEntry CONF_MAC_ADDRESS = "mac_address" @@ -19,14 +19,14 @@ REDACT_KEYS = {CONF_NOISE_PSK, CONF_PASSWORD, CONF_MAC_ADDRESS} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: ESPHomeConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" diag: dict[str, Any] = {} diag["config"] = config_entry.as_dict() - entry_data = DomainData.get(hass).get_entry_data(config_entry) + entry_data = config_entry.runtime_data if (storage_data := await entry_data.store.async_load()) is not None: diag["storage_data"] = storage_data diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index 9ac8fe97614..e9057ddfeaa 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -3,16 +3,16 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Self, cast +from functools import cache +from typing import Self from bleak_esphome.backend.cache import ESPHomeBluetoothCache -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.json import JSONEncoder from .const import DOMAIN -from .entry_data import ESPHomeStorage, RuntimeEntryData +from .entry_data import ESPHomeConfigEntry, ESPHomeStorage, RuntimeEntryData STORAGE_VERSION = 1 @@ -21,30 +21,26 @@ STORAGE_VERSION = 1 class DomainData: """Define a class that stores global esphome data in hass.data[DOMAIN].""" - _entry_datas: dict[str, RuntimeEntryData] = field(default_factory=dict) _stores: dict[str, ESPHomeStorage] = field(default_factory=dict) bluetooth_cache: ESPHomeBluetoothCache = field( default_factory=ESPHomeBluetoothCache ) - def get_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: + def get_entry_data(self, entry: ESPHomeConfigEntry) -> RuntimeEntryData: """Return the runtime entry data associated with this config entry. Raises KeyError if the entry isn't loaded yet. """ - return self._entry_datas[entry.entry_id] + return entry.runtime_data - def set_entry_data(self, entry: ConfigEntry, entry_data: RuntimeEntryData) -> None: + def set_entry_data( + self, entry: ESPHomeConfigEntry, entry_data: RuntimeEntryData + ) -> None: """Set the runtime entry data associated with this config entry.""" - assert entry.entry_id not in self._entry_datas, "Entry data already set!" - self._entry_datas[entry.entry_id] = entry_data - - def pop_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: - """Pop the runtime entry data instance associated with this config entry.""" - return self._entry_datas.pop(entry.entry_id) + entry.runtime_data = entry_data def get_or_create_store( - self, hass: HomeAssistant, entry: ConfigEntry + self, hass: HomeAssistant, entry: ESPHomeConfigEntry ) -> ESPHomeStorage: """Get or create a Store instance for the given config entry.""" return self._stores.setdefault( @@ -55,10 +51,8 @@ class DomainData: ) @classmethod + @cache def get(cls, hass: HomeAssistant) -> Self: """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(Self, hass.data[DOMAIN]) ret = hass.data[DOMAIN] = cls() return ret diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 374c22eef72..8241d0f4563 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -16,7 +16,6 @@ from aioesphomeapi import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -27,10 +26,8 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .domain_data import DomainData - # Import config flow so that it's added to the registry -from .entry_data import RuntimeEntryData +from .entry_data import ESPHomeConfigEntry, RuntimeEntryData from .enum_mapper import EsphomeEnumMapper _R = TypeVar("_R") @@ -85,7 +82,7 @@ def async_static_info_updated( async def platform_async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ESPHomeConfigEntry, async_add_entities: AddEntitiesCallback, *, info_type: type[_InfoT], @@ -97,7 +94,7 @@ async def platform_async_setup_entry( This method is in charge of receiving, distributing and storing info and state updates. """ - entry_data: RuntimeEntryData = DomainData.get(hass).get_entry_data(entry) + entry_data = entry.runtime_data entry_data.info[info_type] = {} entry_data.state.setdefault(state_type, {}) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 7a491d1863b..ff6f048eba1 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -55,6 +55,9 @@ from homeassistant.helpers.storage import Store from .const import DOMAIN from .dashboard import async_get_dashboard +type ESPHomeConfigEntry = ConfigEntry[RuntimeEntryData] + + INFO_TO_COMPONENT_TYPE: Final = {v: k for k, v in COMPONENT_TYPE_TO_INFO.items()} _SENTINEL = object() @@ -248,7 +251,7 @@ class RuntimeEntryData: async def _ensure_platforms_loaded( self, hass: HomeAssistant, - entry: ConfigEntry, + entry: ESPHomeConfigEntry, platforms: set[Platform], ) -> None: async with self.platform_load_lock: @@ -259,7 +262,7 @@ class RuntimeEntryData: async def async_update_static_infos( self, hass: HomeAssistant, - entry: ConfigEntry, + entry: ESPHomeConfigEntry, infos: list[EntityInfo], mac: str, ) -> None: @@ -452,7 +455,7 @@ class RuntimeEntryData: await self.store.async_save(self._pending_storage()) async def async_update_listener( - self, hass: HomeAssistant, entry: ConfigEntry + self, hass: HomeAssistant, entry: ESPHomeConfigEntry ) -> None: """Handle options update.""" if self.original_options == entry.options: diff --git a/homeassistant/components/esphome/event.py b/homeassistant/components/esphome/event.py index 3c7331beba0..9435597e25b 100644 --- a/homeassistant/components/esphome/event.py +++ b/homeassistant/components/esphome/event.py @@ -5,16 +5,18 @@ from __future__ import annotations from aioesphomeapi import EntityInfo, Event, EventInfo from homeassistant.components.event import EventDeviceClass, EventEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum from .entity import EsphomeEntity, platform_async_setup_entry +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome event based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 082de3f7b7d..35a19348281 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -13,7 +13,6 @@ from homeassistant.components.fan import ( FanEntity, FanEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( @@ -29,13 +28,16 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry from .enum_mapper import EsphomeEnumMapper ORDERED_NAMED_FAN_SPEEDS = [FanSpeed.LOW, FanSpeed.MEDIUM, FanSpeed.HIGH] async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome fans based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index bbb4021d58f..c5f83805cce 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -29,7 +29,6 @@ from homeassistant.components.light import ( LightEntity, LightEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -39,12 +38,15 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry FLASH_LENGTHS = {FLASH_SHORT: 2, FLASH_LONG: 10} async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome lights based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/lock.py b/homeassistant/components/esphome/lock.py index 98efdece92e..c00f81839cb 100644 --- a/homeassistant/components/esphome/lock.py +++ b/homeassistant/components/esphome/lock.py @@ -7,7 +7,6 @@ from typing import Any from aioesphomeapi import EntityInfo, LockCommand, LockEntityState, LockInfo, LockState from homeassistant.components.lock import LockEntity, LockEntityFeature -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CODE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -18,10 +17,13 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome switches based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index f191c36c574..870bd704ee4 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -28,7 +28,6 @@ import voluptuous as vol from homeassistant.components import tag, zeroconf from homeassistant.components.intent import async_register_timer_handler -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_ID, CONF_MODE, @@ -73,7 +72,7 @@ from .dashboard import async_get_dashboard from .domain_data import DomainData # Import config flow so that it's added to the registry -from .entry_data import RuntimeEntryData +from .entry_data import ESPHomeConfigEntry, RuntimeEntryData from .voice_assistant import ( VoiceAssistantAPIPipeline, VoiceAssistantPipeline, @@ -159,7 +158,7 @@ class ESPHomeManager: def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, + entry: ESPHomeConfigEntry, host: str, password: str | None, cli: APIClient, @@ -639,7 +638,7 @@ class ESPHomeManager: @callback def _async_setup_device_registry( - hass: HomeAssistant, entry: ConfigEntry, entry_data: RuntimeEntryData + hass: HomeAssistant, entry: ESPHomeConfigEntry, entry_data: RuntimeEntryData ) -> str: """Set up device registry feature for a particular config entry.""" device_info = entry_data.device_info @@ -839,10 +838,11 @@ def _setup_services( _async_register_service(hass, entry_data, device_info, service) -async def cleanup_instance(hass: HomeAssistant, entry: ConfigEntry) -> RuntimeEntryData: +async def cleanup_instance( + hass: HomeAssistant, entry: ESPHomeConfigEntry +) -> RuntimeEntryData: """Cleanup the esphome client if it exists.""" - domain_data = DomainData.get(hass) - data = domain_data.pop_entry_data(entry) + data = entry.runtime_data data.async_on_disconnect() for cleanup_callback in data.cleanup_callbacks: cleanup_callback() diff --git a/homeassistant/components/esphome/select.py b/homeassistant/components/esphome/select.py index 612ffc4bcc6..ed37a9a6ab8 100644 --- a/homeassistant/components/esphome/select.py +++ b/homeassistant/components/esphome/select.py @@ -9,12 +9,10 @@ from homeassistant.components.assist_pipeline.select import ( VadSensitivitySelect, ) from homeassistant.components.select import SelectEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .domain_data import DomainData from .entity import ( EsphomeAssistEntity, EsphomeEntity, @@ -22,12 +20,12 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import RuntimeEntryData +from .entry_data import ESPHomeConfigEntry, RuntimeEntryData async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ESPHomeConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up esphome selects based on a config entry.""" @@ -40,7 +38,7 @@ async def async_setup_entry( state_type=SelectState, ) - entry_data = DomainData.get(hass).get_entry_data(entry) + entry_data = entry.runtime_data assert entry_data.device_info is not None if entry_data.device_info.voice_assistant_feature_flags_compat( entry_data.api_version diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index 6fa73058bd2..b2245c78f52 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -7,7 +7,6 @@ from typing import Any from aioesphomeapi import EntityInfo, SwitchInfo, SwitchState from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum @@ -18,10 +17,13 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome switches based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/valve.py b/homeassistant/components/esphome/valve.py index 5798d38803f..a82d65366c6 100644 --- a/homeassistant/components/esphome/valve.py +++ b/homeassistant/components/esphome/valve.py @@ -11,7 +11,6 @@ from homeassistant.components.valve import ( ValveEntity, ValveEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum @@ -22,10 +21,13 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) +from .entry_data import ESPHomeConfigEntry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ESPHomeConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up ESPHome valves based on a config entry.""" await platform_async_setup_entry( From cbb3d48bd9417fe855820b33ca61ea3d9350a4dc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 13:11:27 +0200 Subject: [PATCH 0120/2411] Improve type hints in dsmr tests (#120393) --- tests/components/dsmr/conftest.py | 15 +++-- tests/components/dsmr/test_config_flow.py | 37 ++++++++---- tests/components/dsmr/test_mbus_migration.py | 5 +- tests/components/dsmr/test_sensor.py | 62 +++++++++++++++----- 4 files changed, 85 insertions(+), 34 deletions(-) diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py index 05881d9c877..2257b8414a6 100644 --- a/tests/components/dsmr/conftest.py +++ b/tests/components/dsmr/conftest.py @@ -15,10 +15,11 @@ from dsmr_parser.obis_references import ( ) from dsmr_parser.objects import CosemObject import pytest +from typing_extensions import Generator @pytest.fixture -async def dsmr_connection_fixture(hass): +def dsmr_connection_fixture() -> Generator[tuple[MagicMock, MagicMock, MagicMock]]: """Fixture that mocks serial connection.""" transport = MagicMock(spec=asyncio.Transport) @@ -44,7 +45,9 @@ async def dsmr_connection_fixture(hass): @pytest.fixture -async def rfxtrx_dsmr_connection_fixture(hass): +def rfxtrx_dsmr_connection_fixture() -> ( + Generator[tuple[MagicMock, MagicMock, MagicMock]] +): """Fixture that mocks RFXtrx connection.""" transport = MagicMock(spec=asyncio.Transport) @@ -70,7 +73,9 @@ async def rfxtrx_dsmr_connection_fixture(hass): @pytest.fixture -async def dsmr_connection_send_validate_fixture(hass): +def dsmr_connection_send_validate_fixture() -> ( + Generator[tuple[MagicMock, MagicMock, MagicMock]] +): """Fixture that mocks serial connection.""" transport = MagicMock(spec=asyncio.Transport) @@ -151,7 +156,9 @@ async def dsmr_connection_send_validate_fixture(hass): @pytest.fixture -async def rfxtrx_dsmr_connection_send_validate_fixture(hass): +def rfxtrx_dsmr_connection_send_validate_fixture() -> ( + Generator[tuple[MagicMock, MagicMock, MagicMock]] +): """Fixture that mocks serial connection.""" transport = MagicMock(spec=asyncio.Transport) diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 711b29f4ae0..3b4dc533993 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -32,7 +32,8 @@ def com_port(): async def test_setup_network( - hass: HomeAssistant, dsmr_connection_send_validate_fixture + hass: HomeAssistant, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], ) -> None: """Test we can setup network.""" result = await hass.config_entries.flow.async_init( @@ -77,8 +78,10 @@ async def test_setup_network( async def test_setup_network_rfxtrx( hass: HomeAssistant, - dsmr_connection_send_validate_fixture, - rfxtrx_dsmr_connection_send_validate_fixture, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], + rfxtrx_dsmr_connection_send_validate_fixture: tuple[ + MagicMock, MagicMock, MagicMock + ], ) -> None: """Test we can setup network.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture @@ -185,7 +188,7 @@ async def test_setup_network_rfxtrx( async def test_setup_serial( com_mock, hass: HomeAssistant, - dsmr_connection_send_validate_fixture, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], version: str, entry_data: dict[str, Any], ) -> None: @@ -225,8 +228,10 @@ async def test_setup_serial( async def test_setup_serial_rfxtrx( com_mock, hass: HomeAssistant, - dsmr_connection_send_validate_fixture, - rfxtrx_dsmr_connection_send_validate_fixture, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], + rfxtrx_dsmr_connection_send_validate_fixture: tuple[ + MagicMock, MagicMock, MagicMock + ], ) -> None: """Test we can setup serial.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture @@ -273,7 +278,9 @@ async def test_setup_serial_rfxtrx( @patch("serial.tools.list_ports.comports", return_value=[com_port()]) async def test_setup_serial_manual( - com_mock, hass: HomeAssistant, dsmr_connection_send_validate_fixture + com_mock, + hass: HomeAssistant, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], ) -> None: """Test we can setup serial with manual entry.""" result = await hass.config_entries.flow.async_init( @@ -321,7 +328,9 @@ async def test_setup_serial_manual( @patch("serial.tools.list_ports.comports", return_value=[com_port()]) async def test_setup_serial_fail( - com_mock, hass: HomeAssistant, dsmr_connection_send_validate_fixture + com_mock, + hass: HomeAssistant, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], ) -> None: """Test failed serial connection.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture @@ -369,8 +378,10 @@ async def test_setup_serial_fail( async def test_setup_serial_timeout( com_mock, hass: HomeAssistant, - dsmr_connection_send_validate_fixture, - rfxtrx_dsmr_connection_send_validate_fixture, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], + rfxtrx_dsmr_connection_send_validate_fixture: tuple[ + MagicMock, MagicMock, MagicMock + ], ) -> None: """Test failed serial connection.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture @@ -425,8 +436,10 @@ async def test_setup_serial_timeout( async def test_setup_serial_wrong_telegram( com_mock, hass: HomeAssistant, - dsmr_connection_send_validate_fixture, - rfxtrx_dsmr_connection_send_validate_fixture, + dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock], + rfxtrx_dsmr_connection_send_validate_fixture: tuple[ + MagicMock, MagicMock, MagicMock + ], ) -> None: """Test failed telegram data.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture diff --git a/tests/components/dsmr/test_mbus_migration.py b/tests/components/dsmr/test_mbus_migration.py index 284a0001b89..18f5e850ecd 100644 --- a/tests/components/dsmr/test_mbus_migration.py +++ b/tests/components/dsmr/test_mbus_migration.py @@ -2,6 +2,7 @@ import datetime from decimal import Decimal +from unittest.mock import MagicMock from dsmr_parser.obis_references import ( BELGIUM_MBUS1_DEVICE_TYPE, @@ -22,7 +23,7 @@ async def test_migrate_gas_to_mbus( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, - dsmr_connection_fixture, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], ) -> None: """Test migration of unique_id.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -113,7 +114,7 @@ async def test_migrate_gas_to_mbus_exists( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, - dsmr_connection_fixture, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], ) -> None: """Test migration of unique_id.""" (connection_factory, transport, protocol) = dsmr_connection_fixture diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index e014fdb68f2..435594d4eef 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -63,7 +63,9 @@ from tests.common import MockConfigEntry, patch async def test_default_setup( - hass: HomeAssistant, entity_registry: er.EntityRegistry, dsmr_connection_fixture + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], ) -> None: """Test the default setup.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -191,7 +193,9 @@ async def test_default_setup( async def test_setup_only_energy( - hass: HomeAssistant, entity_registry: er.EntityRegistry, dsmr_connection_fixture + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], ) -> None: """Test the default setup.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -240,7 +244,9 @@ async def test_setup_only_energy( assert not entry -async def test_v4_meter(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_v4_meter( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if v4 meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -319,7 +325,10 @@ async def test_v4_meter(hass: HomeAssistant, dsmr_connection_fixture) -> None: ], ) async def test_v5_meter( - hass: HomeAssistant, dsmr_connection_fixture, value: Decimal, state: str + hass: HomeAssistant, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], + value: Decimal, + state: str, ) -> None: """Test if v5 meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -386,7 +395,9 @@ async def test_v5_meter( ) -async def test_luxembourg_meter(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_luxembourg_meter( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if v5 meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -468,7 +479,9 @@ async def test_luxembourg_meter(hass: HomeAssistant, dsmr_connection_fixture) -> ) -async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_belgian_meter( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -651,7 +664,9 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No ) -async def test_belgian_meter_alt(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_belgian_meter_alt( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -798,7 +813,9 @@ async def test_belgian_meter_alt(hass: HomeAssistant, dsmr_connection_fixture) - ) -async def test_belgian_meter_mbus(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_belgian_meter_mbus( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -905,7 +922,9 @@ async def test_belgian_meter_mbus(hass: HomeAssistant, dsmr_connection_fixture) ) -async def test_belgian_meter_low(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_belgian_meter_low( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -951,7 +970,9 @@ async def test_belgian_meter_low(hass: HomeAssistant, dsmr_connection_fixture) - assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None -async def test_swedish_meter(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_swedish_meter( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if v5 meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -1017,7 +1038,9 @@ async def test_swedish_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No ) -async def test_easymeter(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_easymeter( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """Test if Q3D meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -1086,7 +1109,9 @@ async def test_easymeter(hass: HomeAssistant, dsmr_connection_fixture) -> None: ) -async def test_tcp(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_tcp( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """If proper config provided TCP connection should be made.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -1112,7 +1137,10 @@ async def test_tcp(hass: HomeAssistant, dsmr_connection_fixture) -> None: assert connection_factory.call_args_list[0][0][1] == "1234" -async def test_rfxtrx_tcp(hass: HomeAssistant, rfxtrx_dsmr_connection_fixture) -> None: +async def test_rfxtrx_tcp( + hass: HomeAssistant, + rfxtrx_dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], +) -> None: """If proper config provided RFXtrx TCP connection should be made.""" (connection_factory, transport, protocol) = rfxtrx_dsmr_connection_fixture @@ -1140,7 +1168,7 @@ async def test_rfxtrx_tcp(hass: HomeAssistant, rfxtrx_dsmr_connection_fixture) - @patch("homeassistant.components.dsmr.sensor.DEFAULT_RECONNECT_INTERVAL", 0) async def test_connection_errors_retry( - hass: HomeAssistant, dsmr_connection_fixture + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] ) -> None: """Connection should be retried on error during setup.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -1177,7 +1205,9 @@ async def test_connection_errors_retry( @patch("homeassistant.components.dsmr.sensor.DEFAULT_RECONNECT_INTERVAL", 0) -async def test_reconnect(hass: HomeAssistant, dsmr_connection_fixture) -> None: +async def test_reconnect( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: """If transport disconnects, the connection should be retried.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -1255,7 +1285,7 @@ async def test_reconnect(hass: HomeAssistant, dsmr_connection_fixture) -> None: async def test_gas_meter_providing_energy_reading( - hass: HomeAssistant, dsmr_connection_fixture + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] ) -> None: """Test that gas providing energy readings use the correct device class.""" (connection_factory, transport, protocol) = dsmr_connection_fixture From 76e890865e4c832f3e4f9efb0aee0f3192f5af25 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 13:13:14 +0200 Subject: [PATCH 0121/2411] Adjust imports in cloud tests (#120386) --- tests/components/cloud/__init__.py | 28 ++++++++---- tests/components/cloud/conftest.py | 13 ++++-- tests/components/cloud/test_init.py | 45 +++++++++++--------- tests/components/cloud/test_repairs.py | 14 +++--- tests/components/cloud/test_stt.py | 2 +- tests/components/cloud/test_system_health.py | 2 +- tests/components/cloud/test_tts.py | 24 +++++------ 7 files changed, 75 insertions(+), 53 deletions(-) diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index 82280336a8c..f1ce24e576f 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -2,9 +2,19 @@ from unittest.mock import AsyncMock, patch -from homeassistant.components import cloud -from homeassistant.components.cloud import const, prefs as cloud_prefs -from homeassistant.components.cloud.const import DATA_CLOUD +from homeassistant.components.cloud.const import ( + DATA_CLOUD, + DOMAIN, + PREF_ALEXA_SETTINGS_VERSION, + PREF_ENABLE_ALEXA, + PREF_ENABLE_GOOGLE, + PREF_GOOGLE_SECURE_DEVICES_PIN, + PREF_GOOGLE_SETTINGS_VERSION, +) +from homeassistant.components.cloud.prefs import ( + ALEXA_SETTINGS_VERSION, + GOOGLE_SETTINGS_VERSION, +) from homeassistant.setup import async_setup_component PIPELINE_DATA = { @@ -62,7 +72,7 @@ async def mock_cloud(hass, config=None): # because it's always setup by bootstrap. Set it up manually in tests. assert await async_setup_component(hass, "homeassistant", {}) - assert await async_setup_component(hass, cloud.DOMAIN, {"cloud": config or {}}) + assert await async_setup_component(hass, DOMAIN, {"cloud": config or {}}) cloud_inst = hass.data[DATA_CLOUD] with patch("hass_nabucasa.Cloud.run_executor", AsyncMock(return_value=None)): await cloud_inst.initialize() @@ -71,11 +81,11 @@ async def mock_cloud(hass, config=None): def mock_cloud_prefs(hass, prefs): """Fixture for cloud component.""" prefs_to_set = { - const.PREF_ALEXA_SETTINGS_VERSION: cloud_prefs.ALEXA_SETTINGS_VERSION, - const.PREF_ENABLE_ALEXA: True, - const.PREF_ENABLE_GOOGLE: True, - const.PREF_GOOGLE_SECURE_DEVICES_PIN: None, - const.PREF_GOOGLE_SETTINGS_VERSION: cloud_prefs.GOOGLE_SETTINGS_VERSION, + PREF_ALEXA_SETTINGS_VERSION: ALEXA_SETTINGS_VERSION, + PREF_ENABLE_ALEXA: True, + PREF_ENABLE_GOOGLE: True, + PREF_GOOGLE_SECURE_DEVICES_PIN: None, + PREF_GOOGLE_SETTINGS_VERSION: GOOGLE_SETTINGS_VERSION, } prefs_to_set.update(prefs) hass.data[DATA_CLOUD].client._prefs._prefs = prefs_to_set diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 3058718551e..a7abb932124 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -17,8 +17,13 @@ import jwt import pytest from typing_extensions import AsyncGenerator -from homeassistant.components.cloud import CloudClient, prefs +from homeassistant.components.cloud.client import CloudClient from homeassistant.components.cloud.const import DATA_CLOUD +from homeassistant.components.cloud.prefs import ( + PREF_ALEXA_DEFAULT_EXPOSE, + PREF_GOOGLE_DEFAULT_EXPOSE, + CloudPreferences, +) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -174,8 +179,8 @@ def set_cloud_prefs_fixture( async def set_cloud_prefs(prefs_settings: dict[str, Any]) -> None: """Set cloud prefs.""" prefs_to_set = cloud.client.prefs.as_dict() - prefs_to_set.pop(prefs.PREF_ALEXA_DEFAULT_EXPOSE) - prefs_to_set.pop(prefs.PREF_GOOGLE_DEFAULT_EXPOSE) + prefs_to_set.pop(PREF_ALEXA_DEFAULT_EXPOSE) + prefs_to_set.pop(PREF_GOOGLE_DEFAULT_EXPOSE) prefs_to_set.update(prefs_settings) await cloud.client.prefs.async_update(**prefs_to_set) @@ -210,7 +215,7 @@ def mock_cloud_fixture(hass): @pytest.fixture async def cloud_prefs(hass): """Fixture for cloud preferences.""" - cloud_prefs = prefs.CloudPreferences(hass) + cloud_prefs = CloudPreferences(hass) await cloud_prefs.async_initialize() return cloud_prefs diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index db8253b0329..d201b45b670 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -6,15 +6,22 @@ from unittest.mock import MagicMock, patch import pytest -from homeassistant.components import cloud from homeassistant.components.cloud import ( + CloudConnectionState, CloudNotAvailable, CloudNotConnected, async_get_or_create_cloudhook, + async_listen_connection_change, + async_remote_ui_url, +) +from homeassistant.components.cloud.const import ( + DATA_CLOUD, + DOMAIN, + MODE_DEV, + PREF_CLOUDHOOKS, ) -from homeassistant.components.cloud.const import DATA_CLOUD, DOMAIN, PREF_CLOUDHOOKS from homeassistant.components.cloud.prefs import STORAGE_KEY -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_MODE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component @@ -31,7 +38,7 @@ async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None: { "http": {}, "cloud": { - cloud.CONF_MODE: cloud.MODE_DEV, + CONF_MODE: MODE_DEV, "cognito_client_id": "test-cognito_client_id", "user_pool_id": "test-user_pool_id", "region": "test-region", @@ -47,7 +54,7 @@ async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None: assert result cl = hass.data[DATA_CLOUD] - assert cl.mode == cloud.MODE_DEV + assert cl.mode == MODE_DEV assert cl.cognito_client_id == "test-cognito_client_id" assert cl.user_pool_id == "test-user_pool_id" assert cl.region == "test-region" @@ -129,7 +136,7 @@ async def test_setup_existing_cloud_user( { "http": {}, "cloud": { - cloud.CONF_MODE: cloud.MODE_DEV, + CONF_MODE: MODE_DEV, "cognito_client_id": "test-cognito_client_id", "user_pool_id": "test-user_pool_id", "region": "test-region", @@ -156,7 +163,7 @@ async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None: nonlocal cloud_states cloud_states.append(cloud_state) - cloud.async_listen_connection_change(hass, handle_state) + async_listen_connection_change(hass, handle_state) assert "async_setup" in str(cl.iot._on_connect[-1]) await cl.iot._on_connect[-1]() @@ -178,12 +185,12 @@ async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None: assert len(mock_load.mock_calls) == 0 assert len(cloud_states) == 1 - assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_CONNECTED + assert cloud_states[-1] == CloudConnectionState.CLOUD_CONNECTED await cl.iot._on_connect[-1]() await hass.async_block_till_done() assert len(cloud_states) == 2 - assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_CONNECTED + assert cloud_states[-1] == CloudConnectionState.CLOUD_CONNECTED assert len(cl.iot._on_disconnect) == 2 assert "async_setup" in str(cl.iot._on_disconnect[-1]) @@ -191,12 +198,12 @@ async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None: await hass.async_block_till_done() assert len(cloud_states) == 3 - assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_DISCONNECTED + assert cloud_states[-1] == CloudConnectionState.CLOUD_DISCONNECTED await cl.iot._on_disconnect[-1]() await hass.async_block_till_done() assert len(cloud_states) == 4 - assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_DISCONNECTED + assert cloud_states[-1] == CloudConnectionState.CLOUD_DISCONNECTED async def test_remote_ui_url(hass: HomeAssistant, mock_cloud_fixture) -> None: @@ -204,26 +211,26 @@ async def test_remote_ui_url(hass: HomeAssistant, mock_cloud_fixture) -> None: cl = hass.data[DATA_CLOUD] # Not logged in - with pytest.raises(cloud.CloudNotAvailable): - cloud.async_remote_ui_url(hass) + with pytest.raises(CloudNotAvailable): + async_remote_ui_url(hass) - with patch.object(cloud, "async_is_logged_in", return_value=True): + with patch("homeassistant.components.cloud.async_is_logged_in", return_value=True): # Remote not enabled - with pytest.raises(cloud.CloudNotAvailable): - cloud.async_remote_ui_url(hass) + with pytest.raises(CloudNotAvailable): + async_remote_ui_url(hass) with patch.object(cl.remote, "connect"): await cl.client.prefs.async_update(remote_enabled=True) await hass.async_block_till_done() # No instance domain - with pytest.raises(cloud.CloudNotAvailable): - cloud.async_remote_ui_url(hass) + with pytest.raises(CloudNotAvailable): + async_remote_ui_url(hass) # Remote finished initializing cl.client.prefs._prefs["remote_domain"] = "example.com" - assert cloud.async_remote_ui_url(hass) == "https://example.com" + assert async_remote_ui_url(hass) == "https://example.com" async def test_async_get_or_create_cloudhook( diff --git a/tests/components/cloud/test_repairs.py b/tests/components/cloud/test_repairs.py index 7ca20d84bce..d165a129dbe 100644 --- a/tests/components/cloud/test_repairs.py +++ b/tests/components/cloud/test_repairs.py @@ -6,8 +6,10 @@ from unittest.mock import patch import pytest -from homeassistant.components.cloud import DOMAIN -import homeassistant.components.cloud.repairs as cloud_repairs +from homeassistant.components.cloud.const import DOMAIN +from homeassistant.components.cloud.repairs import ( + async_manage_legacy_subscription_issue, +) from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN from homeassistant.core import HomeAssistant import homeassistant.helpers.issue_registry as ir @@ -65,12 +67,12 @@ async def test_legacy_subscription_delete_issue_if_no_longer_legacy( issue_registry: ir.IssueRegistry, ) -> None: """Test that we delete the legacy subscription issue if no longer legacy.""" - cloud_repairs.async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) + async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) assert issue_registry.async_get_issue( domain="cloud", issue_id="legacy_subscription" ) - cloud_repairs.async_manage_legacy_subscription_issue(hass, {}) + async_manage_legacy_subscription_issue(hass, {}) assert not issue_registry.async_get_issue( domain="cloud", issue_id="legacy_subscription" ) @@ -93,7 +95,7 @@ async def test_legacy_subscription_repair_flow( json={"url": "https://paypal.com"}, ) - cloud_repairs.async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) + async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) repair_issue = issue_registry.async_get_issue( domain="cloud", issue_id="legacy_subscription" ) @@ -174,7 +176,7 @@ async def test_legacy_subscription_repair_flow_timeout( status=403, ) - cloud_repairs.async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) + async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) repair_issue = issue_registry.async_get_issue( domain="cloud", issue_id="legacy_subscription" ) diff --git a/tests/components/cloud/test_stt.py b/tests/components/cloud/test_stt.py index a20325d6dc3..df9e62380f8 100644 --- a/tests/components/cloud/test_stt.py +++ b/tests/components/cloud/test_stt.py @@ -10,7 +10,7 @@ import pytest from typing_extensions import AsyncGenerator from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY -from homeassistant.components.cloud import DOMAIN +from homeassistant.components.cloud.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/cloud/test_system_health.py b/tests/components/cloud/test_system_health.py index c6e738011d6..60b23e47fec 100644 --- a/tests/components/cloud/test_system_health.py +++ b/tests/components/cloud/test_system_health.py @@ -8,7 +8,7 @@ from unittest.mock import MagicMock from aiohttp import ClientError from hass_nabucasa.remote import CertificateStatus -from homeassistant.components.cloud import DOMAIN +from homeassistant.components.cloud.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index 00466d0d177..bf45b6b2895 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -12,7 +12,8 @@ from typing_extensions import AsyncGenerator import voluptuous as vol from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY -from homeassistant.components.cloud import DOMAIN, const, tts +from homeassistant.components.cloud.const import DEFAULT_TTS_DEFAULT_VOICE, DOMAIN +from homeassistant.components.cloud.tts import PLATFORM_SCHEMA, SUPPORT_LANGUAGES, Voice from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, @@ -57,33 +58,30 @@ async def internal_url_mock(hass: HomeAssistant) -> None: def test_default_exists() -> None: """Test our default language exists.""" - assert const.DEFAULT_TTS_DEFAULT_VOICE[0] in TTS_VOICES - assert ( - const.DEFAULT_TTS_DEFAULT_VOICE[1] - in TTS_VOICES[const.DEFAULT_TTS_DEFAULT_VOICE[0]] - ) + assert DEFAULT_TTS_DEFAULT_VOICE[0] in TTS_VOICES + assert DEFAULT_TTS_DEFAULT_VOICE[1] in TTS_VOICES[DEFAULT_TTS_DEFAULT_VOICE[0]] def test_schema() -> None: """Test schema.""" - assert "nl-NL" in tts.SUPPORT_LANGUAGES + assert "nl-NL" in SUPPORT_LANGUAGES - processed = tts.PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL"}) + processed = PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL"}) assert processed["gender"] == "female" with pytest.raises(vol.Invalid): - tts.PLATFORM_SCHEMA( + PLATFORM_SCHEMA( {"platform": "cloud", "language": "non-existing", "gender": "female"} ) with pytest.raises(vol.Invalid): - tts.PLATFORM_SCHEMA( + PLATFORM_SCHEMA( {"platform": "cloud", "language": "nl-NL", "gender": "not-supported"} ) # Should not raise - tts.PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL", "gender": "female"}) - tts.PLATFORM_SCHEMA({"platform": "cloud"}) + PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL", "gender": "female"}) + PLATFORM_SCHEMA({"platform": "cloud"}) @pytest.mark.parametrize( @@ -188,7 +186,7 @@ async def test_provider_properties( assert "nl-NL" in engine.supported_languages supported_voices = engine.async_get_supported_voices("nl-NL") assert supported_voices is not None - assert tts.Voice("ColetteNeural", "ColetteNeural") in supported_voices + assert Voice("ColetteNeural", "ColetteNeural") in supported_voices supported_voices = engine.async_get_supported_voices("missing_language") assert supported_voices is None From c15718519bd8c867b8cdc339cebc363e32dbbdf1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 13:14:11 +0200 Subject: [PATCH 0122/2411] Improve test coverage for ESPHome manager (#120400) --- tests/components/esphome/conftest.py | 35 ++++++++++++++++++++---- tests/components/esphome/test_manager.py | 25 +++++++++++++++++ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 43edca54158..f55ab9cbe4a 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -263,6 +263,7 @@ async def _mock_generic_device_entry( mock_list_entities_services: tuple[list[EntityInfo], list[UserService]], states: list[EntityState], entry: MockConfigEntry | None = None, + hass_storage: dict[str, Any] | None = None, ) -> MockESPHomeDevice: if not entry: entry = MockConfigEntry( @@ -286,6 +287,17 @@ async def _mock_generic_device_entry( } device_info = DeviceInfo(**(default_device_info | mock_device_info)) + if hass_storage: + storage_key = f"{DOMAIN}.{entry.entry_id}" + hass_storage[storage_key] = { + "version": 1, + "minor_version": 1, + "key": storage_key, + "data": { + "device_info": device_info.to_dict(), + }, + } + mock_device = MockESPHomeDevice(entry, mock_client, device_info) def _subscribe_states(callback: Callable[[EntityState], None]) -> None: @@ -453,6 +465,7 @@ async def mock_bluetooth_entry_with_legacy_adv( @pytest.fixture async def mock_generic_device_entry( hass: HomeAssistant, + hass_storage: dict[str, Any], ) -> Callable[ [APIClient, list[EntityInfo], list[UserService], list[EntityState]], Awaitable[MockConfigEntry], @@ -464,10 +477,17 @@ async def mock_generic_device_entry( entity_info: list[EntityInfo], user_service: list[UserService], states: list[EntityState], + mock_storage: bool = False, ) -> MockConfigEntry: return ( await _mock_generic_device_entry( - hass, mock_client, {}, (entity_info, user_service), states + hass, + mock_client, + {}, + (entity_info, user_service), + states, + None, + hass_storage if mock_storage else None, ) ).entry @@ -477,6 +497,7 @@ async def mock_generic_device_entry( @pytest.fixture async def mock_esphome_device( hass: HomeAssistant, + hass_storage: dict[str, Any], ) -> Callable[ [APIClient, list[EntityInfo], list[UserService], list[EntityState]], Awaitable[MockESPHomeDevice], @@ -485,19 +506,21 @@ async def mock_esphome_device( async def _mock_device( mock_client: APIClient, - entity_info: list[EntityInfo], - user_service: list[UserService], - states: list[EntityState], + entity_info: list[EntityInfo] | None = None, + user_service: list[UserService] | None = None, + states: list[EntityState] | None = None, entry: MockConfigEntry | None = None, device_info: dict[str, Any] | None = None, + mock_storage: bool = False, ) -> MockESPHomeDevice: return await _mock_generic_device_entry( hass, mock_client, device_info or {}, - (entity_info, user_service), - states, + (entity_info or [], user_service or []), + states or [], entry, + hass_storage if mock_storage else None, ) return _mock_device diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index c17ff9a7d8c..d937b63b1db 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -1156,3 +1156,28 @@ async def test_start_reauth( assert len(flows) == 1 flow = flows[0] assert flow["context"]["source"] == "reauth" + + +async def test_entry_missing_unique_id( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test the unique id is added from storage if available.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=None, + data={ + CONF_HOST: "test.local", + CONF_PORT: 6053, + CONF_PASSWORD: "", + }, + options={CONF_ALLOW_SERVICE_CALLS: True}, + ) + entry.add_to_hass(hass) + await mock_esphome_device(mock_client=mock_client, mock_storage=True) + await hass.async_block_till_done() + assert entry.unique_id == "11:22:33:44:55:aa" From b5afc5a7f02bd34ebdf4e3aae32ab3fc91dafe94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 13:31:50 +0200 Subject: [PATCH 0123/2411] Fix incorrect mocking in ESPHome tests (#120410) --- tests/components/esphome/test_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index d937b63b1db..92c21842e78 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -575,7 +575,7 @@ async def test_connection_aborted_wrong_device( entry.add_to_hass(hass) disconnect_done = hass.loop.create_future() - def async_disconnect(*args, **kwargs) -> None: + async def async_disconnect(*args, **kwargs) -> None: disconnect_done.set_result(None) mock_client.disconnect = async_disconnect From b816fce976de453f04f796bf13f7c7e5b77a3b77 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 14:08:30 +0200 Subject: [PATCH 0124/2411] Improve websocket_api schema typing (#120411) --- homeassistant/components/websocket_api/__init__.py | 6 ++---- homeassistant/components/websocket_api/decorators.py | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index d8427bff10e..f9bc4396e01 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -4,11 +4,9 @@ from __future__ import annotations from typing import Final, cast -import voluptuous as vol - from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.loader import bind_hass from . import commands, connection, const, decorators, http, messages # noqa: F401 @@ -55,7 +53,7 @@ def async_register_command( hass: HomeAssistant, command_or_handler: str | const.WebSocketCommandHandler, handler: const.WebSocketCommandHandler | None = None, - schema: vol.Schema | None = None, + schema: VolSchemaType | None = None, ) -> None: """Register a websocket command.""" if handler is None: diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index 5131d02b4d3..b9924bc91d1 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.const import HASSIO_USER_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized +from homeassistant.helpers.typing import VolDictType from . import const, messages from .connection import ActiveConnection @@ -130,7 +131,7 @@ def ws_require_user( def websocket_command( - schema: dict[vol.Marker, Any] | vol.All, + schema: VolDictType | vol.All, ) -> Callable[[const.WebSocketCommandHandler], const.WebSocketCommandHandler]: """Tag a function as a websocket command. From aa05f7321066250e82df498fbea1464f5a4536d5 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 25 Jun 2024 14:26:20 +0200 Subject: [PATCH 0125/2411] Add fixture to synchronize with debouncer in MQTT tests (#120373) * Add fixture to synchronze with debouncer in MQTT tests * Migrate more tests to use the debouncer * Migrate more tests * Migrate util tests * Improve mqtt on_callback test using new fixture * Improve test_subscribe_error * Migrate other tests * Import EnsureJobAfterCooldown from `util.py` but patch `client.py` --- tests/components/mqtt/conftest.py | 24 ++ tests/components/mqtt/test_init.py | 354 +++++++++++++---------------- tests/components/mqtt/test_util.py | 15 +- 3 files changed, 192 insertions(+), 201 deletions(-) diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index 5a1f65667cf..774785bb42a 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -10,6 +10,7 @@ from typing_extensions import AsyncGenerator, Generator from homeassistant.components import mqtt from homeassistant.components.mqtt.models import MessageCallbackType, ReceiveMessage +from homeassistant.components.mqtt.util import EnsureJobAfterCooldown from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant, callback @@ -49,6 +50,29 @@ def mock_temp_dir(temp_dir_prefix: str) -> Generator[str]: yield mocked_temp_dir +@pytest.fixture +def mock_debouncer(hass: HomeAssistant) -> Generator[asyncio.Event]: + """Mock EnsureJobAfterCooldown. + + Returns an asyncio.Event that allows to await the debouncer task to be finished. + """ + task_done = asyncio.Event() + + class MockDeboncer(EnsureJobAfterCooldown): + """Mock the MQTT client (un)subscribe debouncer.""" + + async def _async_job(self) -> None: + """Execute after a cooldown period.""" + await super()._async_job() + task_done.set() + + # We mock the import of EnsureJobAfterCooldown in client.py + with patch( + "homeassistant.components.mqtt.client.EnsureJobAfterCooldown", MockDeboncer + ): + yield task_done + + @pytest.fixture async def setup_with_birth_msg_client_mock( hass: HomeAssistant, diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 2c3ca31bff9..231379601c6 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -140,13 +140,13 @@ async def test_mqtt_connects_on_home_assistant_mqtt_setup( async def test_mqtt_does_not_disconnect_on_home_assistant_stop( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test if client is not disconnected on HA stop.""" mqtt_client_mock = setup_with_birth_msg_client_mock hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - await hass.async_block_till_done() + await mock_debouncer.wait() assert mqtt_client_mock.disconnect.call_count == 0 @@ -1085,6 +1085,7 @@ async def test_subscribe_mqtt_config_entry_disabled( async def test_subscribe_and_resubscribe( hass: HomeAssistant, client_debug_log: None, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, @@ -1095,15 +1096,16 @@ async def test_subscribe_and_resubscribe( patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.4), patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.4), ): + mock_debouncer.clear() unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) # This unsub will be un-done with the following subscribe # unsubscribe should not be called at the broker unsub() unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + await mock_debouncer.wait() + mock_debouncer.clear() async_fire_mqtt_message(hass, "test-topic", "test-payload") - await asyncio.sleep(0.1) - await hass.async_block_till_done(wait_background_tasks=True) assert len(recorded_calls) == 1 assert recorded_calls[0].topic == "test-topic" @@ -1111,38 +1113,41 @@ async def test_subscribe_and_resubscribe( # assert unsubscribe was not called mqtt_client_mock.unsubscribe.assert_not_called() + mock_debouncer.clear() unsub() - await asyncio.sleep(0.1) - await hass.async_block_till_done(wait_background_tasks=True) + await mock_debouncer.wait() mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) async def test_subscribe_topic_non_async( hass: HomeAssistant, + mock_debouncer: asyncio.Event, mqtt_mock_entry: MqttMockHAClientGenerator, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, ) -> None: """Test the subscription of a topic using the non-async function.""" await mqtt_mock_entry() + await mock_debouncer.wait() + mock_debouncer.clear() unsub = await hass.async_add_executor_job( mqtt.subscribe, hass, "test-topic", record_calls ) - await hass.async_block_till_done() + await mock_debouncer.wait() async_fire_mqtt_message(hass, "test-topic", "test-payload") - await hass.async_block_till_done() assert len(recorded_calls) == 1 assert recorded_calls[0].topic == "test-topic" assert recorded_calls[0].payload == "test-payload" + mock_debouncer.clear() await hass.async_add_executor_job(unsub) + await mock_debouncer.wait() async_fire_mqtt_message(hass, "test-topic", "test-payload") - await hass.async_block_till_done() assert len(recorded_calls) == 1 @@ -1417,11 +1422,9 @@ async def test_subscribe_special_characters( assert recorded_calls[0].payload == payload -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_subscribe_same_topic( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test subscribing to same topic twice and simulate retained messages. @@ -1442,25 +1445,22 @@ async def test_subscribe_same_topic( calls_b.append(msg) mqtt_client_mock.reset_mock() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state", _callback_a, qos=0) # Simulate a non retained message after the first subscription async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=1)) - await hass.async_block_till_done() + await mock_debouncer.wait() assert len(calls_a) == 1 mqtt_client_mock.subscribe.assert_called() calls_a = [] mqtt_client_mock.reset_mock() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) await hass.async_block_till_done() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state", _callback_b, qos=1) # Simulate an other non retained message after the second subscription async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=1)) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=1)) - await hass.async_block_till_done() + await mock_debouncer.wait() # Both subscriptions should receive updates assert len(calls_a) == 1 assert len(calls_b) == 1 @@ -1469,6 +1469,7 @@ async def test_subscribe_same_topic( async def test_replaying_payload_same_topic( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying retained messages. @@ -1491,21 +1492,20 @@ async def test_replaying_payload_same_topic( calls_b.append(msg) mqtt_client_mock.reset_mock() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state", _callback_a) - await hass.async_block_till_done() + await mock_debouncer.wait() async_fire_mqtt_message( hass, "test/state", "online", qos=0, retain=True ) # Simulate a (retained) message played back - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await asyncio.sleep(0) - await hass.async_block_till_done() - assert len(calls_a) == 1 mqtt_client_mock.subscribe.assert_called() calls_a = [] mqtt_client_mock.reset_mock() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state", _callback_b) + await mock_debouncer.wait() # Simulate edge case where non retained message was received # after subscription at HA but before the debouncer delay was passed. @@ -1516,12 +1516,6 @@ async def test_replaying_payload_same_topic( # Simulate a (retained) message played back on new subscriptions async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - # Make sure the debouncer delay was passed - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await asyncio.sleep(0) - await hass.async_block_till_done() - # The current subscription only received the message without retain flag assert len(calls_a) == 1 assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) @@ -1542,10 +1536,6 @@ async def test_replaying_payload_same_topic( # After connecting the retain flag will not be set, even if the # payload published was retained, we cannot see that async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await asyncio.sleep(0) - await hass.async_block_till_done() assert len(calls_a) == 1 assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) assert len(calls_b) == 1 @@ -1556,18 +1546,13 @@ async def test_replaying_payload_same_topic( calls_b = [] mqtt_client_mock.reset_mock() mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() mqtt_client_mock.on_connect(None, None, None, 0) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await asyncio.sleep(0) - await hass.async_block_till_done() + await mock_debouncer.wait() mqtt_client_mock.subscribe.assert_called() # Simulate a (retained) message played back after reconnecting async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await asyncio.sleep(0) - await hass.async_block_till_done(wait_background_tasks=True) # Both subscriptions now should replay the retained message assert len(calls_a) == 1 assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) @@ -1575,11 +1560,9 @@ async def test_replaying_payload_same_topic( assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=True) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_replaying_payload_after_resubscribing( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying and filtering retained messages after resubscribing. @@ -1597,22 +1580,18 @@ async def test_replaying_payload_after_resubscribing( calls_a.append(msg) mqtt_client_mock.reset_mock() + mock_debouncer.clear() unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - await hass.async_block_till_done() + await mock_debouncer.wait() mqtt_client_mock.subscribe.assert_called() # Simulate a (retained) message played back async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - await hass.async_block_till_done() assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) calls_a.clear() # Test we get updates async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=False) - await hass.async_block_till_done() assert help_assert_message(calls_a[0], "test/state", "offline", qos=0, retain=False) calls_a.clear() @@ -1622,24 +1601,20 @@ async def test_replaying_payload_after_resubscribing( assert len(calls_a) == 0 # Unsubscribe an resubscribe again + mock_debouncer.clear() unsub() unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() + await mock_debouncer.wait() mqtt_client_mock.subscribe.assert_called() # Simulate we can receive a (retained) played back message again async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - await hass.async_block_till_done() assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_replaying_payload_wildcard_topic( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test replaying retained messages. @@ -1663,28 +1638,24 @@ async def test_replaying_payload_wildcard_topic( calls_b.append(msg) mqtt_client_mock.reset_mock() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/#", _callback_a) + await mock_debouncer.wait() # Simulate (retained) messages being played back on new subscriptions async_fire_mqtt_message(hass, "test/state1", "new_value_1", qos=0, retain=True) async_fire_mqtt_message(hass, "test/state2", "new_value_2", qos=0, retain=True) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await asyncio.sleep(0) - await hass.async_block_till_done() assert len(calls_a) == 2 mqtt_client_mock.subscribe.assert_called() calls_a = [] mqtt_client_mock.reset_mock() # resubscribe to the wild card topic again + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/#", _callback_b) + await mock_debouncer.wait() # Simulate (retained) messages being played back on new subscriptions async_fire_mqtt_message(hass, "test/state1", "initial_value_1", qos=0, retain=True) async_fire_mqtt_message(hass, "test/state2", "initial_value_2", qos=0, retain=True) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await asyncio.sleep(0) - await hass.async_block_till_done() # The retained messages playback should only be processed for the new subscriptions assert len(calls_a) == 0 assert len(calls_b) == 2 @@ -1697,8 +1668,6 @@ async def test_replaying_payload_wildcard_topic( # Simulate new messages being received async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=False) async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=False) - await hass.async_block_till_done() - await asyncio.sleep(0) assert len(calls_a) == 2 assert len(calls_b) == 2 @@ -1707,20 +1676,16 @@ async def test_replaying_payload_wildcard_topic( calls_b = [] mqtt_client_mock.reset_mock() mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() mqtt_client_mock.on_connect(None, None, None, 0) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await asyncio.sleep(0) - await hass.async_block_till_done() + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() # Simulate the (retained) messages are played back after reconnecting # for all subscriptions async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=True) async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=True) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await asyncio.sleep(0) - await hass.async_block_till_done(wait_background_tasks=True) # Both subscriptions should replay assert len(calls_a) == 2 assert len(calls_b) == 2 @@ -1728,29 +1693,32 @@ async def test_replaying_payload_wildcard_topic( async def test_not_calling_unsubscribe_with_active_subscribers( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test not calling unsubscribe() when other subscribers are active.""" mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.reset_mock() + mock_debouncer.clear() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) await mqtt.async_subscribe(hass, "test/state", record_calls, 1) - await hass.async_block_till_done() - await asyncio.sleep(0) - await hass.async_block_till_done() + await mock_debouncer.wait() assert mqtt_client_mock.subscribe.called + mock_debouncer.clear() unsub() await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown await hass.async_block_till_done(wait_background_tasks=True) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown assert not mqtt_client_mock.unsubscribe.called + assert not mock_debouncer.is_set() async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + mock_debouncer: asyncio.Event, + mqtt_mock_entry: MqttMockHAClientGenerator, record_calls: MessageCallbackType, ) -> None: """Test not calling subscribe() when it is unsubscribed. @@ -1758,18 +1726,22 @@ async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( Make sure subscriptions are cleared if unsubscribed before the subscribe cool down period has ended. """ - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.subscribe.reset_mock() + mqtt_mock = await mqtt_mock_entry() + mqtt_client_mock = mqtt_mock._mqttc + await mock_debouncer.wait() + mock_debouncer.clear() + mqtt_client_mock.subscribe.reset_mock() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) unsub() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await hass.async_block_till_done() + await mock_debouncer.wait() + # The debouncer executes without an pending subscribes assert not mqtt_client_mock.subscribe.called async def test_unsubscribe_race( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test not calling unsubscribe() when other subscribers are active.""" @@ -1786,15 +1758,14 @@ async def test_unsubscribe_race( calls_b.append(msg) mqtt_client_mock.reset_mock() + + mock_debouncer.clear() unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) unsub() await mqtt.async_subscribe(hass, "test/state", _callback_b) - await asyncio.sleep(0) - await hass.async_block_till_done() + await mock_debouncer.wait() async_fire_mqtt_message(hass, "test/state", "online") - await asyncio.sleep(0) - await hass.async_block_till_done() assert not calls_a assert calls_b @@ -1825,6 +1796,7 @@ async def test_unsubscribe_race( ) async def test_restore_subscriptions_on_reconnect( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: @@ -1833,18 +1805,18 @@ async def test_restore_subscriptions_on_reconnect( mqtt_client_mock.reset_mock() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await asyncio.sleep(0) - await hass.async_block_till_done() + await mock_debouncer.wait() assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) mqtt_client_mock.reset_mock() mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() mqtt_client_mock.on_connect(None, None, None, 0) - await hass.async_block_till_done() - await asyncio.sleep(0.1) - await hass.async_block_till_done(wait_background_tasks=True) + await mock_debouncer.wait() assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) @@ -1854,20 +1826,19 @@ async def test_restore_subscriptions_on_reconnect( ) async def test_restore_all_active_subscriptions_on_reconnect( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, - freezer: FrozenDateTimeFactory, ) -> None: """Test active subscriptions are restored correctly on reconnect.""" mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.reset_mock() + mock_debouncer.clear() unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) - await hass.async_block_till_done() - freezer.tick(3) - async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() + # cooldown + await mock_debouncer.wait() # the subscription with the highest QoS should survive expected = [ @@ -1876,68 +1847,54 @@ async def test_restore_all_active_subscriptions_on_reconnect( assert mqtt_client_mock.subscribe.mock_calls == expected unsub() - await hass.async_block_till_done() assert mqtt_client_mock.unsubscribe.call_count == 0 mqtt_client_mock.on_disconnect(None, None, 0) - await hass.async_block_till_done() + + mock_debouncer.clear() mqtt_client_mock.on_connect(None, None, None, 0) - freezer.tick(3) - async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() + # wait for cooldown + await mock_debouncer.wait() expected.append(call([("test/state", 1)])) for expected_call in expected: assert mqtt_client_mock.subscribe.hass_call(expected_call) - freezer.tick(3) - async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() - freezer.tick(3) - async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done(wait_background_tasks=True) - @pytest.mark.parametrize( "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 1.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 1.0) async def test_subscribed_at_highest_qos( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, - freezer: FrozenDateTimeFactory, ) -> None: """Test the highest qos as assigned when subscribing to the same topic.""" mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.reset_mock() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) await hass.async_block_till_done() - freezer.tick(5) - async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() + # cooldown + await mock_debouncer.wait() assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) mqtt_client_mock.reset_mock() - freezer.tick(5) - async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() - await hass.async_block_till_done() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) - await hass.async_block_till_done() - freezer.tick(5) - async_fire_time_changed(hass) # cooldown - await hass.async_block_till_done() + # cooldown + await mock_debouncer.wait() + # the subscription with the highest QoS should survive assert help_all_subscribe_calls(mqtt_client_mock) == [("test/state", 2)] async def test_reload_entry_with_restored_subscriptions( hass: HomeAssistant, + mock_debouncer: asyncio.Event, mqtt_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, recorded_calls: list[ReceiveMessage], @@ -1950,13 +1907,15 @@ async def test_reload_entry_with_restored_subscriptions( with patch("homeassistant.config.load_yaml_config_file", return_value={}): await hass.config_entries.async_setup(entry.entry_id) + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test-topic", record_calls) await mqtt.async_subscribe(hass, "wild/+/card", record_calls) + # cooldown + await mock_debouncer.wait() async_fire_mqtt_message(hass, "test-topic", "test-payload") async_fire_mqtt_message(hass, "wild/any/card", "wild-card-payload") - await hass.async_block_till_done() assert len(recorded_calls) == 2 assert recorded_calls[0].topic == "test-topic" assert recorded_calls[0].payload == "test-payload" @@ -1967,13 +1926,14 @@ async def test_reload_entry_with_restored_subscriptions( # Reload the entry with patch("homeassistant.config.load_yaml_config_file", return_value={}): assert await hass.config_entries.async_reload(entry.entry_id) + mock_debouncer.clear() assert entry.state is ConfigEntryState.LOADED - await hass.async_block_till_done() + # cooldown + await mock_debouncer.wait() async_fire_mqtt_message(hass, "test-topic", "test-payload2") async_fire_mqtt_message(hass, "wild/any/card", "wild-card-payload2") - await hass.async_block_till_done() assert len(recorded_calls) == 2 assert recorded_calls[0].topic == "test-topic" assert recorded_calls[0].payload == "test-payload2" @@ -1984,13 +1944,14 @@ async def test_reload_entry_with_restored_subscriptions( # Reload the entry again with patch("homeassistant.config.load_yaml_config_file", return_value={}): assert await hass.config_entries.async_reload(entry.entry_id) + mock_debouncer.clear() assert entry.state is ConfigEntryState.LOADED - await hass.async_block_till_done() + # cooldown + await mock_debouncer.wait() async_fire_mqtt_message(hass, "test-topic", "test-payload3") async_fire_mqtt_message(hass, "wild/any/card", "wild-card-payload3") - await hass.async_block_till_done() assert len(recorded_calls) == 2 assert recorded_calls[0].topic == "test-topic" assert recorded_calls[0].payload == "test-payload3" @@ -2079,9 +2040,9 @@ async def test_handle_mqtt_on_callback_after_timeout( """Test receiving an ACK after a timeout.""" mqtt_mock = await mqtt_mock_entry() # Simulate the mid future getting a timeout - mqtt_mock()._async_get_mid_future(100).set_exception(asyncio.TimeoutError) - # Simulate an ACK for mid == 100, being received after the timeout - mqtt_client_mock.on_publish(mqtt_client_mock, None, 100) + mqtt_mock()._async_get_mid_future(101).set_exception(asyncio.TimeoutError) + # Simulate an ACK for mid == 101, being received after the timeout + mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) await hass.async_block_till_done() assert "No ACK from MQTT server" not in caplog.text assert "InvalidStateError" not in caplog.text @@ -2119,20 +2080,18 @@ async def test_subscribe_error( mqtt_client_mock.reset_mock() # simulate client is not connected error before subscribing mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None) - with patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0): - await mqtt.async_subscribe(hass, "some-topic", record_calls) - while mqtt_client_mock.subscribe.call_count == 0: - await hass.async_block_till_done() + await mqtt.async_subscribe(hass, "some-topic", record_calls) + while mqtt_client_mock.subscribe.call_count == 0: await hass.async_block_till_done() - await hass.async_block_till_done() - assert ( - "Error talking to MQTT: The client is not currently connected." - in caplog.text - ) + await hass.async_block_till_done() + assert ( + "Error talking to MQTT: The client is not currently connected." in caplog.text + ) async def test_handle_message_callback( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test for handling an incoming message callback.""" @@ -2146,12 +2105,12 @@ async def test_handle_message_callback( msg = ReceiveMessage( "some-topic", b"test-payload", 1, False, "some-topic", datetime.now() ) + mock_debouncer.clear() await mqtt.async_subscribe(hass, "some-topic", _callback) + await mock_debouncer.wait() mqtt_client_mock.reset_mock() mqtt_client_mock.on_message(None, None, msg) - await hass.async_block_till_done() - await hass.async_block_till_done() assert len(callbacks) == 1 assert callbacks[0].topic == "some-topic" assert callbacks[0].qos == 1 @@ -2239,7 +2198,7 @@ async def test_setup_mqtt_client_protocol( @patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) async def test_handle_mqtt_timeout_on_callback( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_debouncer: asyncio.Event ) -> None: """Test publish without receiving an ACK callback.""" mid = 0 @@ -2247,7 +2206,7 @@ async def test_handle_mqtt_timeout_on_callback( class FakeInfo: """Returns a simulated client publish response.""" - mid = 100 + mid = 102 rc = 0 with patch( @@ -2264,7 +2223,9 @@ async def test_handle_mqtt_timeout_on_callback( # We want to simulate the publish behaviour MQTT client mock_client = mock_client.return_value mock_client.publish.return_value = FakeInfo() + # Mock we get a mid and rc=0 mock_client.subscribe.side_effect = _mock_ack + mock_client.unsubscribe.side_effect = _mock_ack mock_client.connect = MagicMock( return_value=0, side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( @@ -2278,6 +2239,7 @@ async def test_handle_mqtt_timeout_on_callback( entry.add_to_hass(hass) # Set up the integration + mock_debouncer.clear() assert await hass.config_entries.async_setup(entry.entry_id) # Now call we publish without simulating and ACK callback @@ -2286,6 +2248,12 @@ async def test_handle_mqtt_timeout_on_callback( # 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 + # Ensure we stop lingering background tasks + await hass.config_entries.async_unload(entry.entry_id) + # Assert we did not have any completed subscribes, + # because the debouncer subscribe job failed to receive an ACK, + # and the time auto caused the debouncer job to fail. + assert not mock_debouncer.is_set() async def test_setup_raises_config_entry_not_ready_if_no_connect_broker( @@ -2391,26 +2359,22 @@ async def test_tls_version( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_custom_birth_message( hass: HomeAssistant, + mock_debouncer: asyncio.Event, mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, ) -> None: """Test sending birth message.""" - birth = asyncio.Event() entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) assert await hass.config_entries.async_setup(entry.entry_id) + mock_debouncer.clear() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - - @callback - def wait_birth(msg: ReceiveMessage) -> None: - """Handle birth message.""" - birth.set() - - await mqtt.async_subscribe(hass, "birth", wait_birth) - await hass.async_block_till_done() - await birth.wait() + # discovery cooldown + await mock_debouncer.wait() + # Wait for publish call to finish + await hass.async_block_till_done(wait_background_tasks=True) mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) @@ -2439,6 +2403,8 @@ async def test_default_birth_message( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) async def test_no_birth_message( hass: HomeAssistant, + record_calls: MessageCallbackType, + mock_debouncer: asyncio.Event, mqtt_config_entry_data: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, ) -> None: @@ -2446,26 +2412,19 @@ async def test_no_birth_message( entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) + mock_debouncer.clear() assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() - mqtt_client_mock.reset_mock() - - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done() + # Wait for discovery cooldown + await mock_debouncer.wait() + # Ensure any publishing could have been processed + await hass.async_block_till_done(wait_background_tasks=True) mqtt_client_mock.publish.assert_not_called() - @callback - def msg_callback(msg: ReceiveMessage) -> None: - """Handle callback.""" - mqtt_client_mock.reset_mock() - await mqtt.async_subscribe(hass, "homeassistant/some-topic", msg_callback) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "homeassistant/some-topic", record_calls) + # Wait for discovery cooldown + await mock_debouncer.wait() mqtt_client_mock.subscribe.assert_called() @@ -2487,7 +2446,6 @@ async def test_delayed_birth_message( entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() @callback def wait_birth(msg: ReceiveMessage) -> None: @@ -2495,7 +2453,6 @@ async def test_delayed_birth_message( birth.set() await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - await hass.async_block_till_done() with pytest.raises(TimeoutError): await asyncio.wait_for(birth.wait(), 0.05) assert not mqtt_client_mock.publish.called @@ -2595,26 +2552,27 @@ async def test_no_will_message( ) async def test_mqtt_subscribes_topics_on_connect( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test subscription to topic on connect.""" mqtt_client_mock = setup_with_birth_msg_client_mock + mock_debouncer.clear() await mqtt.async_subscribe(hass, "topic/test", record_calls) await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2) await mqtt.async_subscribe(hass, "still/pending", record_calls) await mqtt.async_subscribe(hass, "still/pending", record_calls, 1) + await mock_debouncer.wait() mqtt_client_mock.on_disconnect(Mock(), None, 0) mqtt_client_mock.reset_mock() + mock_debouncer.clear() mqtt_client_mock.on_connect(Mock(), None, 0, 0) - - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) - await hass.async_block_till_done(wait_background_tasks=True) + await mock_debouncer.wait() subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) assert ("topic/test", 0) in subscribe_calls @@ -2628,17 +2586,18 @@ async def test_mqtt_subscribes_topics_on_connect( ) async def test_mqtt_subscribes_in_single_call( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: """Test bundled client subscription to topic.""" mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_client_mock.subscribe.reset_mock() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "topic/test", record_calls) await mqtt.async_subscribe(hass, "home/sensor", record_calls) # Make sure the debouncer finishes - await asyncio.sleep(0) - await hass.async_block_till_done(wait_background_tasks=True) + await mock_debouncer.wait() assert mqtt_client_mock.subscribe.call_count == 1 # Assert we have a single subscription call with both subscriptions @@ -2653,6 +2612,7 @@ async def test_mqtt_subscribes_in_single_call( @patch("homeassistant.components.mqtt.client.MAX_UNSUBSCRIBES_PER_CALL", 2) async def test_mqtt_subscribes_and_unsubscribes_in_chunks( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, ) -> None: @@ -2661,13 +2621,13 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( mqtt_client_mock.subscribe.reset_mock() unsub_tasks: list[CALLBACK_TYPE] = [] + mock_debouncer.clear() unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test1", record_calls)) unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor1", record_calls)) unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test2", record_calls)) unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor2", record_calls)) # Make sure the debouncer finishes - await asyncio.sleep(0.1) - await hass.async_block_till_done(wait_background_tasks=True) + await mock_debouncer.wait() assert mqtt_client_mock.subscribe.call_count == 2 # Assert we have a 2 subscription calls with both 2 subscriptions @@ -2675,12 +2635,11 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( assert len(mqtt_client_mock.subscribe.mock_calls[1][1][0]) == 2 # Unsubscribe all topics + mock_debouncer.clear() for task in unsub_tasks: task() - await hass.async_block_till_done() # Make sure the debouncer finishes - await asyncio.sleep(0.1) - await hass.async_block_till_done(wait_background_tasks=True) + await mock_debouncer.wait() assert mqtt_client_mock.unsubscribe.call_count == 2 # Assert we have a 2 unsubscribe calls with both 2 topic @@ -2748,6 +2707,7 @@ async def test_message_callback_exception_gets_logged( async def test_message_partial_callback_exception_gets_logged( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test exception raised by message handler.""" @@ -2765,15 +2725,13 @@ async def test_message_partial_callback_exception_gets_logged( """Partial callback handler.""" msg_callback(msg) + mock_debouncer.clear() await mqtt.async_subscribe( hass, "test-topic", partial(parial_handler, bad_handler, {"some_attr"}) ) - await hass.async_block_till_done(wait_background_tasks=True) + await mock_debouncer.wait() async_fire_mqtt_message(hass, "test-topic", "test") await hass.async_block_till_done() - await hass.async_block_till_done() - await asyncio.sleep(0) - await hass.async_block_till_done(wait_background_tasks=True) assert ( "Exception in bad_handler when handling msg on 'test-topic':" @@ -3500,6 +3458,7 @@ async def test_publish_json_from_template( async def test_subscribe_connection_status( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test connextion status subscription.""" @@ -3533,8 +3492,9 @@ async def test_subscribe_connection_status( await hass.async_block_till_done() # Mock connect status + mock_debouncer.clear() mqtt_client_mock.on_connect(None, None, 0, 0) - await hass.async_block_till_done() + await mock_debouncer.wait() assert mqtt.is_connected(hass) is True # Mock disconnect status @@ -3547,9 +3507,9 @@ async def test_subscribe_connection_status( unsub_async() # Mock connect status + mock_debouncer.clear() mqtt_client_mock.on_connect(None, None, 0, 0) - await asyncio.sleep(0) - await hass.async_block_till_done() + await mock_debouncer.wait() assert mqtt.is_connected(hass) is True # Check calls @@ -3584,7 +3544,7 @@ async def test_unload_config_entry( new_mqtt_config_entry = mqtt_config_entry mqtt_client_mock.publish.assert_any_call("just_in_time", "published", 0, False) assert new_mqtt_config_entry.state is ConfigEntryState.NOT_LOADED - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) 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 @@ -4236,6 +4196,7 @@ async def test_auto_reconnect( async def test_server_sock_connect_and_disconnect( hass: HomeAssistant, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], record_calls: MessageCallbackType, @@ -4257,14 +4218,19 @@ async def test_server_sock_connect_and_disconnect( server.close() # mock the server closing the connection on us + mock_debouncer.clear() unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + await mock_debouncer.wait() mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_CONN_LOST mqtt_client_mock.on_socket_unregister_write(mqtt_client_mock, None, client) mqtt_client_mock.on_socket_close(mqtt_client_mock, None, client) mqtt_client_mock.on_disconnect(mqtt_client_mock, None, client) await hass.async_block_till_done() + mock_debouncer.clear() unsub() + await hass.async_block_till_done() + assert not mock_debouncer.is_set() # Should have failed assert len(recorded_calls) == 0 diff --git a/tests/components/mqtt/test_util.py b/tests/components/mqtt/test_util.py index 955fc88448c..a3802de69da 100644 --- a/tests/components/mqtt/test_util.py +++ b/tests/components/mqtt/test_util.py @@ -26,15 +26,15 @@ from tests.typing import MqttMockHAClient, MqttMockPahoClient async def test_canceling_debouncer_on_shutdown( hass: HomeAssistant, record_calls: MessageCallbackType, + mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test canceling the debouncer when HA shuts down.""" mqtt_client_mock = setup_with_birth_msg_client_mock - + # Mock we are past initial setup + await mock_debouncer.wait() with patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 2): - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() + mock_debouncer.clear() await mqtt.async_subscribe(hass, "test/state1", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) # Stop HA so the scheduled debouncer task will be canceled @@ -47,9 +47,10 @@ async def test_canceling_debouncer_on_shutdown( await mqtt.async_subscribe(hass, "test/state4", record_calls) async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) await mqtt.async_subscribe(hass, "test/state5", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=0.1)) - await hass.async_block_till_done() - + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done(wait_background_tasks=True) + # Assert the debouncer subscribe job was not executed + assert not mock_debouncer.is_set() mqtt_client_mock.subscribe.assert_not_called() # Note thet the broker connection will not be disconnected gracefully From bcd1243686e565d8c49e90d971fbfc442f9262ca Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 15:15:59 +0200 Subject: [PATCH 0126/2411] Use VolDictType to improve schema typing (#120417) --- .../components/asuswrt/config_flow.py | 2 ++ homeassistant/components/axis/config_flow.py | 3 ++- .../components/dlna_dmr/config_flow.py | 3 ++- .../components/ecovacs/config_flow.py | 3 ++- .../components/enphase_envoy/config_flow.py | 3 ++- .../homekit_controller/config_flow.py | 3 ++- .../components/http/data_validator.py | 6 +++++- .../components/humidifier/device_action.py | 4 ++-- .../components/ibeacon/config_flow.py | 3 ++- homeassistant/components/knx/config_flow.py | 6 +++--- .../components/light/device_action.py | 4 ++-- .../components/mysensors/config_flow.py | 18 +++++++++--------- .../components/nmap_tracker/config_flow.py | 3 ++- homeassistant/components/opower/config_flow.py | 3 ++- .../components/proximity/config_flow.py | 3 ++- homeassistant/components/rfxtrx/config_flow.py | 4 +++- .../components/synology_dsm/config_flow.py | 8 +++----- homeassistant/components/tplink/light.py | 9 +++++---- .../components/zwave_js/config_flow.py | 3 ++- 19 files changed, 54 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index f5db3dfa3d8..d58a216aaee 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -32,6 +32,7 @@ from homeassistant.helpers.schema_config_entry_flow import ( SchemaOptionsFlowHandler, ) from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig +from homeassistant.helpers.typing import VolDictType from .bridge import AsusWrtBridge from .const import ( @@ -143,6 +144,7 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): user_input = self._config_data + add_schema: VolDictType if self.show_advanced_options: add_schema = { vol.Exclusive(CONF_PASSWORD, PASS_KEY, PASS_KEY_MSG): str, diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 1754e37853f..63cac941423 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -30,6 +30,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.typing import VolDictType from homeassistant.util.network import is_link_local from . import AxisConfigEntry @@ -63,7 +64,7 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN): def __init__(self) -> None: """Initialize the Axis config flow.""" self.config: dict[str, Any] = {} - self.discovery_schema: dict[vol.Required, type[str | int]] | None = None + self.discovery_schema: VolDictType | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 6b551f0e999..265c78fd9a9 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -27,6 +27,7 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_MAC, CONF_TYPE, from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import IntegrationError from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.typing import VolDictType from .const import ( CONF_BROWSE_UNFILTERED, @@ -382,7 +383,7 @@ class DlnaDmrOptionsFlowHandler(OptionsFlow): if not errors: return self.async_create_entry(title="", data=options) - fields = {} + fields: VolDictType = {} def _add_with_suggestion(key: str, validator: Callable | type[bool]) -> None: """Add a field to with a suggested value. diff --git a/homeassistant/components/ecovacs/config_flow.py b/homeassistant/components/ecovacs/config_flow.py index 7e4bfbe5597..a254731a946 100644 --- a/homeassistant/components/ecovacs/config_flow.py +++ b/homeassistant/components/ecovacs/config_flow.py @@ -22,6 +22,7 @@ from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import aiohttp_client, selector from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import VolDictType from homeassistant.loader import async_get_issue_tracker from homeassistant.util.ssl import get_default_no_verify_context @@ -181,7 +182,7 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN): title=user_input[CONF_USERNAME], data=user_input ) - schema = { + schema: VolDictType = { vol.Required(CONF_USERNAME): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT) ), diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 695709627b7..c18401859de 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -21,6 +21,7 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.typing import VolDictType from .const import ( DOMAIN, @@ -69,7 +70,7 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN): @callback def _async_generate_schema(self) -> vol.Schema: """Generate schema.""" - schema = {} + schema: VolDictType = {} if self.ip_address: schema[vol.Required(CONF_HOST, default=self.ip_address)] = vol.In( diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 48aa3fc2bc7..2ca32ccb911 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -24,6 +24,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import VolDictType from .const import DOMAIN, KNOWN_DEVICES from .storage import async_get_entity_storage @@ -555,7 +556,7 @@ class HomekitControllerFlowHandler(ConfigFlow, domain=DOMAIN): "category": formatted_category(self.category), } - schema = {vol.Required("pairing_code"): vol.All(str, vol.Strip)} + schema: VolDictType = {vol.Required("pairing_code"): vol.All(str, vol.Strip)} if errors and errors.get("pairing_code") == "insecure_setup_code": schema[vol.Optional("allow_insecure_setup_codes")] = bool diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index b2f6496a77b..abfeadc7189 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -11,6 +11,8 @@ from typing import Any, Concatenate from aiohttp import web import voluptuous as vol +from homeassistant.helpers.typing import VolDictType + from .view import HomeAssistantView _LOGGER = logging.getLogger(__name__) @@ -25,7 +27,9 @@ class RequestDataValidator: Will return a 400 if no JSON provided or doesn't match schema. """ - def __init__(self, schema: vol.Schema, allow_empty: bool = False) -> None: + def __init__( + self, schema: VolDictType | vol.Schema, allow_empty: bool = False + ) -> None: """Initialize the decorator.""" if isinstance(schema, dict): schema = vol.Schema(schema) diff --git a/homeassistant/components/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py index 74ef73443d6..de1d4c871e3 100644 --- a/homeassistant/components/humidifier/device_action.py +++ b/homeassistant/components/humidifier/device_action.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import get_capability, get_supported_features -from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType, VolDictType from . import DOMAIN, const @@ -114,7 +114,7 @@ async def async_get_action_capabilities( """List action capabilities.""" action_type = config[CONF_TYPE] - fields = {} + fields: VolDictType = {} if action_type == "set_humidity": fields[vol.Required(const.ATTR_HUMIDITY)] = vol.Coerce(int) diff --git a/homeassistant/components/ibeacon/config_flow.py b/homeassistant/components/ibeacon/config_flow.py index ccedaa675b6..424befa81ec 100644 --- a/homeassistant/components/ibeacon/config_flow.py +++ b/homeassistant/components/ibeacon/config_flow.py @@ -16,6 +16,7 @@ from homeassistant.config_entries import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import VolDictType from .const import CONF_ALLOW_NAMELESS_UUIDS, DOMAIN @@ -81,7 +82,7 @@ class IBeaconOptionsFlow(OptionsFlow): data = {CONF_ALLOW_NAMELESS_UUIDS: list(updated_uuids)} return self.async_create_entry(title="", data=data) - schema = { + schema: VolDictType = { vol.Optional( "new_uuid", description={"suggested_value": new_uuid}, diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index c526a1e25f6..226abc1b868 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -29,7 +29,7 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import selector -from homeassistant.helpers.typing import UNDEFINED +from homeassistant.helpers.typing import UNDEFINED, VolDictType from .const import ( CONF_KNX_AUTOMATIC, @@ -368,7 +368,7 @@ class KNXCommonFlow(ABC, ConfigEntryBaseFlow): CONF_KNX_ROUTE_BACK, not bool(self._selected_tunnel) ) - fields = { + fields: VolDictType = { vol.Required(CONF_KNX_TUNNELING_TYPE, default=default_type): vol.In( CONF_KNX_TUNNELING_TYPE_LABELS ), @@ -694,7 +694,7 @@ class KNXCommonFlow(ABC, ConfigEntryBaseFlow): router for router in routers if router.routing_requires_secure ) - fields = { + fields: VolDictType = { vol.Required( CONF_KNX_INDIVIDUAL_ADDRESS, default=_individual_address ): _IA_SELECTOR, diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index dbdf7200a7b..45e9731c5b8 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -21,7 +21,7 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity import get_supported_features -from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType, VolDictType from . import ( ATTR_BRIGHTNESS_PCT, @@ -150,7 +150,7 @@ async def async_get_action_capabilities( supported_color_modes = None supported_features = 0 - extra_fields = {} + extra_fields: VolDictType = {} if brightness_supported(supported_color_modes): extra_fields[vol.Optional(ATTR_BRIGHTNESS_PCT)] = VALID_BRIGHTNESS_PCT diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 9a8d79ca3a7..f3fb03ffac8 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -22,6 +22,7 @@ from homeassistant.const import CONF_DEVICE from homeassistant.core import callback from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import VolDictType from .const import ( CONF_BAUD_RATE, @@ -153,7 +154,7 @@ class MySensorsConfigFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry(user_input) user_input = user_input or {} - schema = { + schema: VolDictType = { vol.Required( CONF_DEVICE, default=user_input.get(CONF_DEVICE, "/dev/ttyACM0") ): str, @@ -164,9 +165,8 @@ class MySensorsConfigFlowHandler(ConfigFlow, domain=DOMAIN): } schema.update(_get_schema_common(user_input)) - schema = vol.Schema(schema) return self.async_show_form( - step_id="gw_serial", data_schema=schema, errors=errors + step_id="gw_serial", data_schema=vol.Schema(schema), errors=errors ) async def async_step_gw_tcp( @@ -182,7 +182,7 @@ class MySensorsConfigFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry(user_input) user_input = user_input or {} - schema = { + schema: VolDictType = { vol.Required( CONF_DEVICE, default=user_input.get(CONF_DEVICE, "127.0.0.1") ): str, @@ -192,8 +192,9 @@ class MySensorsConfigFlowHandler(ConfigFlow, domain=DOMAIN): } schema.update(_get_schema_common(user_input)) - schema = vol.Schema(schema) - return self.async_show_form(step_id="gw_tcp", data_schema=schema, errors=errors) + return self.async_show_form( + step_id="gw_tcp", data_schema=vol.Schema(schema), errors=errors + ) def _check_topic_exists(self, topic: str) -> bool: for other_config in self._async_current_entries(): @@ -243,7 +244,7 @@ class MySensorsConfigFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry(user_input) user_input = user_input or {} - schema = { + schema: VolDictType = { vol.Required( CONF_TOPIC_IN_PREFIX, default=user_input.get(CONF_TOPIC_IN_PREFIX, "") ): str, @@ -254,9 +255,8 @@ class MySensorsConfigFlowHandler(ConfigFlow, domain=DOMAIN): } schema.update(_get_schema_common(user_input)) - schema = vol.Schema(schema) return self.async_show_form( - step_id="gw_mqtt", data_schema=schema, errors=errors + step_id="gw_mqtt", data_schema=vol.Schema(schema), errors=errors ) @callback diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py index a89c50a2210..b724dca1a81 100644 --- a/homeassistant/components/nmap_tracker/config_flow.py +++ b/homeassistant/components/nmap_tracker/config_flow.py @@ -23,6 +23,7 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import VolDictType from .const import ( CONF_HOME_INTERVAL, @@ -110,7 +111,7 @@ async def _async_build_schema_with_user_input( exclude = user_input.get( CONF_EXCLUDE, await network.async_get_source_ip(hass, MDNS_TARGET_IP) ) - schema = { + schema: VolDictType = { vol.Required(CONF_HOSTS, default=hosts): str, vol.Required( CONF_HOME_INTERVAL, default=user_input.get(CONF_HOME_INTERVAL, 0) diff --git a/homeassistant/components/opower/config_flow.py b/homeassistant/components/opower/config_flow.py index bbd9315eaa3..574062aca52 100644 --- a/homeassistant/components/opower/config_flow.py +++ b/homeassistant/components/opower/config_flow.py @@ -20,6 +20,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResu from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.typing import VolDictType from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN @@ -151,7 +152,7 @@ class OpowerConfigFlow(ConfigFlow, domain=DOMAIN): ) await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") - schema = { + schema: VolDictType = { vol.Required(CONF_USERNAME): self.reauth_entry.data[CONF_USERNAME], vol.Required(CONF_PASSWORD): str, } diff --git a/homeassistant/components/proximity/config_flow.py b/homeassistant/components/proximity/config_flow.py index 14f26f5d45d..d133b14cb6a 100644 --- a/homeassistant/components/proximity/config_flow.py +++ b/homeassistant/components/proximity/config_flow.py @@ -23,6 +23,7 @@ from homeassistant.helpers.selector import ( NumberSelector, NumberSelectorConfig, ) +from homeassistant.helpers.typing import VolDictType from homeassistant.util import slugify from .const import ( @@ -37,7 +38,7 @@ from .const import ( RESULT_SUCCESS = "success" -def _base_schema(user_input: dict[str, Any]) -> vol.Schema: +def _base_schema(user_input: dict[str, Any]) -> VolDictType: return { vol.Required( CONF_TRACKED_ENTITIES, default=user_input.get(CONF_TRACKED_ENTITIES, []) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 1fbb2e8fc29..ceb9bea4661 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -38,6 +38,7 @@ from homeassistant.helpers import ( entity_registry as er, ) from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.typing import VolDictType from . import ( DOMAIN, @@ -245,9 +246,10 @@ class RfxtrxOptionsFlow(OptionsFlow): device_data = self._selected_device - data_schema = {} + data_schema: VolDictType = {} if binary_supported(self._selected_device_object): + off_delay_schema: VolDictType if device_data.get(CONF_OFF_DELAY): off_delay_schema = { vol.Optional( diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 63ff804951c..6e2b090fc98 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -41,7 +41,7 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import DiscoveryInfoType +from homeassistant.helpers.typing import DiscoveryInfoType, VolDictType from homeassistant.util.network import is_ip_address as is_ip from .const import ( @@ -79,7 +79,7 @@ def _reauth_schema() -> vol.Schema: def _user_schema_with_defaults(user_input: dict[str, Any]) -> vol.Schema: - user_schema = { + user_schema: VolDictType = { vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, } user_schema.update(_ordered_shared_schema(user_input)) @@ -87,9 +87,7 @@ def _user_schema_with_defaults(user_input: dict[str, Any]) -> vol.Schema: return vol.Schema(user_schema) -def _ordered_shared_schema( - schema_input: dict[str, Any], -) -> dict[vol.Required | vol.Optional, Any]: +def _ordered_shared_schema(schema_input: dict[str, Any]) -> VolDictType: return { vol.Required(CONF_USERNAME, default=schema_input.get(CONF_USERNAME, "")): str, vol.Required(CONF_PASSWORD, default=schema_input.get(CONF_PASSWORD, "")): str, diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index aa50c3f2ed2..977e75215aa 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Sequence import logging -from typing import Any, Final, cast +from typing import Any, cast from kasa import SmartBulb, SmartLightStrip import voluptuous as vol @@ -25,6 +25,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType from . import legacy_device_id from .const import DOMAIN @@ -43,7 +44,7 @@ VAL = vol.Range(min=0, max=100) TRANSITION = vol.Range(min=0, max=6000) HSV_SEQUENCE = vol.ExactSequence((HUE, SAT, VAL)) -BASE_EFFECT_DICT: Final = { +BASE_EFFECT_DICT: VolDictType = { vol.Optional("brightness", default=100): vol.All( vol.Coerce(int), vol.Range(min=0, max=100) ), @@ -58,7 +59,7 @@ BASE_EFFECT_DICT: Final = { ), } -SEQUENCE_EFFECT_DICT: Final = { +SEQUENCE_EFFECT_DICT: VolDictType = { **BASE_EFFECT_DICT, vol.Required("sequence"): vol.All( cv.ensure_list, @@ -76,7 +77,7 @@ SEQUENCE_EFFECT_DICT: Final = { ), } -RANDOM_EFFECT_DICT: Final = { +RANDOM_EFFECT_DICT: VolDictType = { **BASE_EFFECT_DICT, vol.Optional("fadeoff", default=0): vol.All( vol.Coerce(int), vol.Range(min=0, max=3000) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index dff582558b1..e73fa9fc3a7 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -38,6 +38,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow, FlowManager from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import VolDictType from . import disconnect_client from .addon import get_addon_manager @@ -639,7 +640,7 @@ class ZWaveJSConfigFlow(BaseZwaveJSFlow, ConfigFlow, domain=DOMAIN): CONF_ADDON_LR_S2_AUTHENTICATED_KEY, self.lr_s2_authenticated_key or "" ) - schema = { + schema: VolDictType = { vol.Optional(CONF_S0_LEGACY_KEY, default=s0_legacy_key): str, vol.Optional( CONF_S2_ACCESS_CONTROL_KEY, default=s2_access_control_key From f934fea754a8c27fe456648b27d80d8dad5d305f Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Tue, 25 Jun 2024 06:34:54 -0700 Subject: [PATCH 0127/2411] Apply all todoist custom project filters for calendar events (#117454) Co-authored-by: Robert Resch --- homeassistant/components/todoist/calendar.py | 14 ++-- tests/components/todoist/test_calendar.py | 67 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index e3f87043e02..baa7103f7eb 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -436,7 +436,7 @@ class TodoistProjectData: self._coordinator = coordinator self._name = project_data[CONF_NAME] - # If no ID is defined, fetch all tasks. + # If no ID is defined, this is a custom project. self._id = project_data.get(CONF_ID) # All labels the user has defined, for easy lookup. @@ -497,6 +497,13 @@ class TodoistProjectData: SUMMARY: data.content, } + if ( + self._project_id_whitelist + and data.project_id not in self._project_id_whitelist + ): + # Project isn't in `include_projects` filter. + return None + # All task Labels (optional parameter). task[LABELS] = [ label.name for label in self._labels if label.name in data.labels @@ -625,10 +632,7 @@ class TodoistProjectData: tasks = self._coordinator.data if self._id is None: project_task_data = [ - task - for task in tasks - if not self._project_id_whitelist - or task.project_id in self._project_id_whitelist + task for task in tasks if self.create_todoist_task(task) is not None ] else: project_task_data = [task for task in tasks if task.project_id == self._id] diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py index 8ba4da9b2e8..d8123af3231 100644 --- a/tests/components/todoist/test_calendar.py +++ b/tests/components/todoist/test_calendar.py @@ -366,6 +366,73 @@ async def test_task_due_datetime( assert await response.json() == [] +@pytest.mark.parametrize( + ("todoist_config", "due", "start", "end", "expected_response"), + [ + ( + {"custom_projects": [{"name": "Test", "labels": ["Label1"]}]}, + Due(date="2023-03-30", is_recurring=False, string="Mar 30"), + "2023-03-28T00:00:00.000Z", + "2023-04-01T00:00:00.000Z", + [get_events_response({"date": "2023-03-30"}, {"date": "2023-03-31"})], + ), + ( + {"custom_projects": [{"name": "Test", "labels": ["custom"]}]}, + Due(date="2023-03-30", is_recurring=False, string="Mar 30"), + "2023-03-28T00:00:00.000Z", + "2023-04-01T00:00:00.000Z", + [], + ), + ( + {"custom_projects": [{"name": "Test", "include_projects": ["Name"]}]}, + Due(date="2023-03-30", is_recurring=False, string="Mar 30"), + "2023-03-28T00:00:00.000Z", + "2023-04-01T00:00:00.000Z", + [get_events_response({"date": "2023-03-30"}, {"date": "2023-03-31"})], + ), + ( + {"custom_projects": [{"name": "Test", "due_date_days": 1}]}, + Due(date="2023-03-30", is_recurring=False, string="Mar 30"), + "2023-03-28T00:00:00.000Z", + "2023-04-01T00:00:00.000Z", + [get_events_response({"date": "2023-03-30"}, {"date": "2023-03-31"})], + ), + ( + {"custom_projects": [{"name": "Test", "due_date_days": 1}]}, + Due( + date=(dt_util.now() + timedelta(days=2)).strftime("%Y-%m-%d"), + is_recurring=False, + string="Mar 30", + ), + dt_util.now().isoformat(), + (dt_util.now() + timedelta(days=5)).isoformat(), + [], + ), + ], + ids=[ + "in_labels_whitelist", + "not_in_labels_whitelist", + "in_include_projects", + "in_due_date_days", + "not_in_due_date_days", + ], +) +async def test_events_filtered_for_custom_projects( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + start: str, + end: str, + expected_response: dict[str, Any], +) -> None: + """Test we filter out tasks from custom projects based on their config.""" + client = await hass_client() + response = await client.get( + get_events_url("calendar.test", start, end), + ) + assert response.status == HTTPStatus.OK + assert await response.json() == expected_response + + @pytest.mark.parametrize( ("due", "setup_platform"), [ From edaa5c60a7108eca8956861539f4c3c6ab4be8aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 17:00:03 +0200 Subject: [PATCH 0128/2411] Small cleanups to ESPHome (#120414) --- homeassistant/components/esphome/__init__.py | 14 ++++++++------ homeassistant/components/esphome/domain_data.py | 6 ------ homeassistant/components/esphome/manager.py | 3 +-- tests/components/esphome/test_config_flow.py | 9 +++------ 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 3af95576c18..b06fcd4bab0 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -25,6 +25,8 @@ from .manager import ESPHomeManager, cleanup_instance CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) +CLIENT_INFO = f"Home Assistant {ha_version}" + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the esphome component.""" @@ -34,10 +36,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> bool: """Set up the esphome component.""" - host = entry.data[CONF_HOST] - port = entry.data[CONF_PORT] - password = entry.data[CONF_PASSWORD] - noise_psk = entry.data.get(CONF_NOISE_PSK) + host: str = entry.data[CONF_HOST] + port: int = entry.data[CONF_PORT] + password: str | None = entry.data[CONF_PASSWORD] + noise_psk: str | None = entry.data.get(CONF_NOISE_PSK) zeroconf_instance = await zeroconf.async_get_instance(hass) @@ -45,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> b host, port, password, - client_info=f"Home Assistant {ha_version}", + client_info=CLIENT_INFO, zeroconf_instance=zeroconf_instance, noise_psk=noise_psk, ) @@ -61,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> b entry.runtime_data = entry_data manager = ESPHomeManager( - hass, entry, host, password, cli, zeroconf_instance, domain_data, entry_data + hass, entry, host, password, cli, zeroconf_instance, domain_data ) await manager.async_start() diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index e9057ddfeaa..aa46469c40e 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -33,12 +33,6 @@ class DomainData: """ return entry.runtime_data - def set_entry_data( - self, entry: ESPHomeConfigEntry, entry_data: RuntimeEntryData - ) -> None: - """Set the runtime entry data associated with this config entry.""" - entry.runtime_data = entry_data - def get_or_create_store( self, hass: HomeAssistant, entry: ESPHomeConfigEntry ) -> ESPHomeStorage: diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 870bd704ee4..5ab0265c1d4 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -164,7 +164,6 @@ class ESPHomeManager: cli: APIClient, zeroconf_instance: zeroconf.HaZeroconf, domain_data: DomainData, - entry_data: RuntimeEntryData, ) -> None: """Initialize the esphome manager.""" self.hass = hass @@ -177,7 +176,7 @@ class ESPHomeManager: self.voice_assistant_pipeline: VoiceAssistantPipeline | None = None self.reconnect_logic: ReconnectLogic | None = None self.zeroconf_instance = zeroconf_instance - self.entry_data = entry_data + self.entry_data = entry.runtime_data async def on_stop(self, event: Event) -> None: """Cleanup the socket client on HA close.""" diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 9c61a5d0615..9a2b1f1a80e 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -2,7 +2,7 @@ from ipaddress import ip_address import json -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, patch from aioesphomeapi import ( APIClient, @@ -18,7 +18,7 @@ import pytest from homeassistant import config_entries from homeassistant.components import dhcp, zeroconf -from homeassistant.components.esphome import DomainData, dashboard +from homeassistant.components.esphome import dashboard from homeassistant.components.esphome.const import ( CONF_ALLOW_SERVICE_CALLS, CONF_DEVICE_NAME, @@ -1136,10 +1136,7 @@ async def test_discovery_dhcp_no_changes( ) 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) + mock_client.device_info = AsyncMock(return_value=DeviceInfo(name="test8266")) service_info = dhcp.DhcpServiceInfo( ip="192.168.43.183", From 49e6316c42c7dc6a5586e5e836346c9c3f6b4184 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 17:00:26 +0200 Subject: [PATCH 0129/2411] Bump yalexs-ble to 2.4.3 (#120428) --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index a8f087e3acc..f898ce64ce6 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==6.4.1", "yalexs-ble==2.4.2"] + "requirements": ["yalexs==6.4.1", "yalexs-ble==2.4.3"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 0cf142b63b5..293ba87df86 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "iot_class": "local_push", - "requirements": ["yalexs-ble==2.4.2"] + "requirements": ["yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index d1481147699..2b1b393d1f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2936,7 +2936,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.4.2 +yalexs-ble==2.4.3 # homeassistant.components.august yalexs==6.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3af1fa4184a..135581ff6ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2295,7 +2295,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.4.2 +yalexs-ble==2.4.3 # homeassistant.components.august yalexs==6.4.1 From 4feca36ca60cff05211fe430743fa13e192cff7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 17:03:04 +0200 Subject: [PATCH 0130/2411] Refactor esphome platform setup to reduce boilerplate (#120415) --- .../components/esphome/alarm_control_panel.py | 30 +++++++----------- homeassistant/components/esphome/button.py | 30 +++++++----------- homeassistant/components/esphome/camera.py | 28 ++++++----------- homeassistant/components/esphome/climate.py | 29 ++++++----------- homeassistant/components/esphome/cover.py | 29 ++++++----------- homeassistant/components/esphome/date.py | 28 ++++++----------- homeassistant/components/esphome/datetime.py | 28 ++++++----------- homeassistant/components/esphome/event.py | 30 +++++++----------- homeassistant/components/esphome/fan.py | 29 ++++++----------- homeassistant/components/esphome/light.py | 30 ++++++------------ homeassistant/components/esphome/lock.py | 29 ++++++----------- .../components/esphome/media_player.py | 30 ++++++------------ homeassistant/components/esphome/number.py | 30 ++++++------------ homeassistant/components/esphome/switch.py | 29 ++++++----------- homeassistant/components/esphome/text.py | 31 +++++++------------ homeassistant/components/esphome/time.py | 28 ++++++----------- homeassistant/components/esphome/valve.py | 29 ++++++----------- 17 files changed, 170 insertions(+), 327 deletions(-) diff --git a/homeassistant/components/esphome/alarm_control_panel.py b/homeassistant/components/esphome/alarm_control_panel.py index 17079fe8c6a..64a0210f0f7 100644 --- a/homeassistant/components/esphome/alarm_control_panel.py +++ b/homeassistant/components/esphome/alarm_control_panel.py @@ -2,6 +2,8 @@ from __future__ import annotations +from functools import partial + from aioesphomeapi import ( AlarmControlPanelCommand, AlarmControlPanelEntityState, @@ -28,8 +30,7 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from .entity import ( EsphomeEntity, @@ -37,7 +38,6 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry from .enum_mapper import EsphomeEnumMapper _ESPHOME_ACP_STATE_TO_HASS_STATE: EsphomeEnumMapper[AlarmControlPanelState, str] = ( @@ -69,22 +69,6 @@ class EspHomeACPFeatures(APIIntEnum): ARM_VACATION = 32 -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome switches based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=AlarmControlPanelInfo, - entity_type=EsphomeAlarmControlPanel, - state_type=AlarmControlPanelEntityState, - ) - - class EsphomeAlarmControlPanel( EsphomeEntity[AlarmControlPanelInfo, AlarmControlPanelEntityState], AlarmControlPanelEntity, @@ -169,3 +153,11 @@ class EsphomeAlarmControlPanel( self._client.alarm_control_panel_command( self._key, AlarmControlPanelCommand.TRIGGER, code ) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=AlarmControlPanelInfo, + entity_type=EsphomeAlarmControlPanel, + state_type=AlarmControlPanelEntityState, +) diff --git a/homeassistant/components/esphome/button.py b/homeassistant/components/esphome/button.py index 8883c4b6bea..f13fa65ede1 100644 --- a/homeassistant/components/esphome/button.py +++ b/homeassistant/components/esphome/button.py @@ -2,11 +2,12 @@ from __future__ import annotations +from functools import partial + from aioesphomeapi import ButtonInfo, EntityInfo, EntityState from homeassistant.components.button import ButtonDeviceClass, ButtonEntity -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from homeassistant.util.enum import try_parse_enum from .entity import ( @@ -14,23 +15,6 @@ from .entity import ( convert_api_error_ha_error, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome buttons based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=ButtonInfo, - entity_type=EsphomeButton, - state_type=EntityState, - ) class EsphomeButton(EsphomeEntity[ButtonInfo, EntityState], ButtonEntity): @@ -63,3 +47,11 @@ class EsphomeButton(EsphomeEntity[ButtonInfo, EntityState], ButtonEntity): async def async_press(self) -> None: """Press the button.""" self._client.button_command(self._key) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=ButtonInfo, + entity_type=EsphomeButton, + state_type=EntityState, +) diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index abe7f6809e6..6038bf52e06 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -12,27 +12,9 @@ from aiohttp import web from homeassistant.components import camera from homeassistant.components.camera import Camera -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from .entity import EsphomeEntity, platform_async_setup_entry -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up esphome cameras based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=CameraInfo, - entity_type=EsphomeCamera, - state_type=CameraState, - ) class EsphomeCamera(Camera, EsphomeEntity[CameraInfo, CameraState]): @@ -95,3 +77,11 @@ class EsphomeCamera(Camera, EsphomeEntity[CameraInfo, CameraState]): return await camera.async_get_still_stream( request, stream_request, camera.DEFAULT_CONTENT_TYPE, 0.0 ) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=CameraInfo, + entity_type=EsphomeCamera, + state_type=CameraState, +) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 6c82207ddc9..da1cdfb0eab 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial from typing import Any, cast from aioesphomeapi import ( @@ -52,8 +53,7 @@ from homeassistant.const import ( PRECISION_WHOLE, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from .entity import ( EsphomeEntity, @@ -61,28 +61,11 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry from .enum_mapper import EsphomeEnumMapper FAN_QUIET = "quiet" -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome climate devices based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=ClimateInfo, - entity_type=EsphomeClimateEntity, - state_type=ClimateState, - ) - - _CLIMATE_MODES: EsphomeEnumMapper[ClimateMode, HVACMode] = EsphomeEnumMapper( { ClimateMode.OFF: HVACMode.OFF, @@ -335,3 +318,11 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti self._client.climate_command( key=self._key, swing_mode=_SWING_MODES.from_hass(swing_mode) ) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=ClimateInfo, + entity_type=EsphomeClimateEntity, + state_type=ClimateState, +) diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 4597b4f3566..19ce4cbf55a 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial from typing import Any from aioesphomeapi import APIVersion, CoverInfo, CoverOperation, CoverState, EntityInfo @@ -13,8 +14,7 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from homeassistant.util.enum import try_parse_enum from .entity import ( @@ -23,23 +23,6 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome covers based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=CoverInfo, - entity_type=EsphomeCover, - state_type=CoverState, - ) class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): @@ -137,3 +120,11 @@ class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): """Move the cover tilt to a specific position.""" tilt_position: int = kwargs[ATTR_TILT_POSITION] self._client.cover_command(key=self._key, tilt=tilt_position / 100) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=CoverInfo, + entity_type=EsphomeCover, + state_type=CoverState, +) diff --git a/homeassistant/components/esphome/date.py b/homeassistant/components/esphome/date.py index eb26ec918d0..28bc532918a 100644 --- a/homeassistant/components/esphome/date.py +++ b/homeassistant/components/esphome/date.py @@ -3,31 +3,13 @@ from __future__ import annotations from datetime import date +from functools import partial from aioesphomeapi import DateInfo, DateState from homeassistant.components.date import DateEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up esphome dates based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=DateInfo, - entity_type=EsphomeDate, - state_type=DateState, - ) class EsphomeDate(EsphomeEntity[DateInfo, DateState], DateEntity): @@ -45,3 +27,11 @@ class EsphomeDate(EsphomeEntity[DateInfo, DateState], DateEntity): async def async_set_value(self, value: date) -> None: """Update the current date.""" self._client.date_command(self._key, value.year, value.month, value.day) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=DateInfo, + entity_type=EsphomeDate, + state_type=DateState, +) diff --git a/homeassistant/components/esphome/datetime.py b/homeassistant/components/esphome/datetime.py index 5d578ae4928..20d0d651bba 100644 --- a/homeassistant/components/esphome/datetime.py +++ b/homeassistant/components/esphome/datetime.py @@ -3,32 +3,14 @@ from __future__ import annotations from datetime import datetime +from functools import partial from aioesphomeapi import DateTimeInfo, DateTimeState from homeassistant.components.datetime import DateTimeEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up esphome datetimes based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=DateTimeInfo, - entity_type=EsphomeDateTime, - state_type=DateTimeState, - ) class EsphomeDateTime(EsphomeEntity[DateTimeInfo, DateTimeState], DateTimeEntity): @@ -46,3 +28,11 @@ class EsphomeDateTime(EsphomeEntity[DateTimeInfo, DateTimeState], DateTimeEntity async def async_set_value(self, value: datetime) -> None: """Update the current datetime.""" self._client.datetime_command(self._key, int(value.timestamp())) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=DateTimeInfo, + entity_type=EsphomeDateTime, + state_type=DateTimeState, +) diff --git a/homeassistant/components/esphome/event.py b/homeassistant/components/esphome/event.py index 9435597e25b..11a5d0cfb33 100644 --- a/homeassistant/components/esphome/event.py +++ b/homeassistant/components/esphome/event.py @@ -2,31 +2,15 @@ from __future__ import annotations +from functools import partial + from aioesphomeapi import EntityInfo, Event, EventInfo from homeassistant.components.event import EventDeviceClass, EventEntity -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from homeassistant.util.enum import try_parse_enum from .entity import EsphomeEntity, platform_async_setup_entry -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome event based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=EventInfo, - entity_type=EsphomeEvent, - state_type=Event, - ) class EsphomeEvent(EsphomeEntity[EventInfo, Event], EventEntity): @@ -48,3 +32,11 @@ class EsphomeEvent(EsphomeEntity[EventInfo, Event], EventEntity): self._update_state_from_entry_data() self._trigger_event(self._state.event_type) self.async_write_ha_state() + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=EventInfo, + entity_type=EsphomeEvent, + state_type=Event, +) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 35a19348281..43ffd96c475 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial import math from typing import Any @@ -13,8 +14,7 @@ from homeassistant.components.fan import ( FanEntity, FanEntityFeature, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from homeassistant.util.percentage import ( ordered_list_item_to_percentage, percentage_to_ordered_list_item, @@ -28,28 +28,11 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry from .enum_mapper import EsphomeEnumMapper ORDERED_NAMED_FAN_SPEEDS = [FanSpeed.LOW, FanSpeed.MEDIUM, FanSpeed.HIGH] -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome fans based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=FanInfo, - entity_type=EsphomeFan, - state_type=FanState, - ) - - _FAN_DIRECTIONS: EsphomeEnumMapper[FanDirection, str] = EsphomeEnumMapper( { FanDirection.FORWARD: DIRECTION_FORWARD, @@ -180,3 +163,11 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): self._attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS) else: self._attr_speed_count = static_info.supported_speed_levels + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=FanInfo, + entity_type=EsphomeFan, + state_type=FanState, +) diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index c5f83805cce..295f9365cd0 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -2,7 +2,7 @@ from __future__ import annotations -from functools import lru_cache +from functools import lru_cache, partial from typing import TYPE_CHECKING, Any, cast from aioesphomeapi import ( @@ -29,8 +29,7 @@ from homeassistant.components.light import ( LightEntity, LightEntityFeature, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from .entity import ( EsphomeEntity, @@ -38,27 +37,10 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry FLASH_LENGTHS = {FLASH_SHORT: 2, FLASH_LONG: 10} -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome lights based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=LightInfo, - entity_type=EsphomeLight, - state_type=LightState, - ) - - _COLOR_MODE_MAPPING = { ColorMode.ONOFF: [ LightColorCapability.ON_OFF, @@ -437,3 +419,11 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): if ColorMode.COLOR_TEMP in supported: self._attr_min_color_temp_kelvin = _mired_to_kelvin(static_info.max_mireds) self._attr_max_color_temp_kelvin = _mired_to_kelvin(static_info.min_mireds) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=LightInfo, + entity_type=EsphomeLight, + state_type=LightState, +) diff --git a/homeassistant/components/esphome/lock.py b/homeassistant/components/esphome/lock.py index c00f81839cb..4caa1f68612 100644 --- a/homeassistant/components/esphome/lock.py +++ b/homeassistant/components/esphome/lock.py @@ -2,14 +2,14 @@ from __future__ import annotations +from functools import partial from typing import Any from aioesphomeapi import EntityInfo, LockCommand, LockEntityState, LockInfo, LockState from homeassistant.components.lock import LockEntity, LockEntityFeature from homeassistant.const import ATTR_CODE -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from .entity import ( EsphomeEntity, @@ -17,23 +17,6 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome switches based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=LockInfo, - entity_type=EsphomeLock, - state_type=LockEntityState, - ) class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity): @@ -92,3 +75,11 @@ class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity): async def async_open(self, **kwargs: Any) -> None: """Open the door latch.""" self._client.lock_command(self._key, LockCommand.OPEN) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=LockInfo, + entity_type=EsphomeLock, + state_type=LockEntityState, +) diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 8caad0f939d..ec9d61fb9e7 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial from typing import Any from aioesphomeapi import ( @@ -23,9 +24,7 @@ from homeassistant.components.media_player import ( MediaType, async_process_play_media_url, ) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from .entity import ( EsphomeEntity, @@ -35,23 +34,6 @@ from .entity import ( ) from .enum_mapper import EsphomeEnumMapper - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up esphome media players based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=MediaPlayerInfo, - entity_type=EsphomeMediaPlayer, - state_type=MediaPlayerEntityState, - ) - - _STATES: EsphomeEnumMapper[EspMediaPlayerState, MediaPlayerState] = EsphomeEnumMapper( { EspMediaPlayerState.IDLE: MediaPlayerState.IDLE, @@ -159,3 +141,11 @@ class EsphomeMediaPlayer( self._key, command=MediaPlayerCommand.MUTE if mute else MediaPlayerCommand.UNMUTE, ) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=MediaPlayerInfo, + entity_type=EsphomeMediaPlayer, + state_type=MediaPlayerEntityState, +) diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index 01744dd9998..1e588c8d35e 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial import math from aioesphomeapi import ( @@ -12,9 +13,7 @@ from aioesphomeapi import ( ) from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from homeassistant.util.enum import try_parse_enum from .entity import ( @@ -25,23 +24,6 @@ from .entity import ( ) from .enum_mapper import EsphomeEnumMapper - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up esphome numbers based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=NumberInfo, - entity_type=EsphomeNumber, - state_type=NumberState, - ) - - NUMBER_MODES: EsphomeEnumMapper[EsphomeNumberMode, NumberMode] = EsphomeEnumMapper( { EsphomeNumberMode.AUTO: NumberMode.AUTO, @@ -87,3 +69,11 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): async def async_set_native_value(self, value: float) -> None: """Update the current value.""" self._client.number_command(self._key, value) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=NumberInfo, + entity_type=EsphomeNumber, + state_type=NumberState, +) diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index b2245c78f52..c210ae1440b 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -2,13 +2,13 @@ from __future__ import annotations +from functools import partial from typing import Any from aioesphomeapi import EntityInfo, SwitchInfo, SwitchState from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from homeassistant.util.enum import try_parse_enum from .entity import ( @@ -17,23 +17,6 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome switches based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=SwitchInfo, - entity_type=EsphomeSwitch, - state_type=SwitchState, - ) class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): @@ -64,3 +47,11 @@ class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self._client.switch_command(self._key, False) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=SwitchInfo, + entity_type=EsphomeSwitch, + state_type=SwitchState, +) diff --git a/homeassistant/components/esphome/text.py b/homeassistant/components/esphome/text.py index 7d455e9ec21..f9dbbbcd853 100644 --- a/homeassistant/components/esphome/text.py +++ b/homeassistant/components/esphome/text.py @@ -2,12 +2,12 @@ from __future__ import annotations +from functools import partial + from aioesphomeapi import EntityInfo, TextInfo, TextMode as EsphomeTextMode, TextState from homeassistant.components.text import TextEntity, TextMode -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from .entity import ( EsphomeEntity, @@ -17,23 +17,6 @@ from .entity import ( ) from .enum_mapper import EsphomeEnumMapper - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up esphome texts based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=TextInfo, - entity_type=EsphomeText, - state_type=TextState, - ) - - TEXT_MODES: EsphomeEnumMapper[EsphomeTextMode, TextMode] = EsphomeEnumMapper( { EsphomeTextMode.TEXT: TextMode.TEXT, @@ -68,3 +51,11 @@ class EsphomeText(EsphomeEntity[TextInfo, TextState], TextEntity): async def async_set_value(self, value: str) -> None: """Update the current value.""" self._client.text_command(self._key, value) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=TextInfo, + entity_type=EsphomeText, + state_type=TextState, +) diff --git a/homeassistant/components/esphome/time.py b/homeassistant/components/esphome/time.py index de985a1e1d6..477c47cf636 100644 --- a/homeassistant/components/esphome/time.py +++ b/homeassistant/components/esphome/time.py @@ -3,33 +3,15 @@ from __future__ import annotations from datetime import time +from functools import partial from aioesphomeapi import TimeInfo, TimeState from homeassistant.components.time import TimeEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up esphome times based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=TimeInfo, - entity_type=EsphomeTime, - state_type=TimeState, - ) - - class EsphomeTime(EsphomeEntity[TimeInfo, TimeState], TimeEntity): """A time implementation for esphome.""" @@ -45,3 +27,11 @@ class EsphomeTime(EsphomeEntity[TimeInfo, TimeState], TimeEntity): async def async_set_value(self, value: time) -> None: """Update the current time.""" self._client.time_command(self._key, value.hour, value.minute, value.second) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=TimeInfo, + entity_type=EsphomeTime, + state_type=TimeState, +) diff --git a/homeassistant/components/esphome/valve.py b/homeassistant/components/esphome/valve.py index a82d65366c6..d779a6abb9f 100644 --- a/homeassistant/components/esphome/valve.py +++ b/homeassistant/components/esphome/valve.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial from typing import Any from aioesphomeapi import EntityInfo, ValveInfo, ValveOperation, ValveState @@ -11,8 +12,7 @@ from homeassistant.components.valve import ( ValveEntity, ValveEntityFeature, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import callback from homeassistant.util.enum import try_parse_enum from .entity import ( @@ -21,23 +21,6 @@ from .entity import ( esphome_state_property, platform_async_setup_entry, ) -from .entry_data import ESPHomeConfigEntry - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ESPHomeConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up ESPHome valves based on a config entry.""" - await platform_async_setup_entry( - hass, - entry, - async_add_entities, - info_type=ValveInfo, - entity_type=EsphomeValve, - state_type=ValveState, - ) class EsphomeValve(EsphomeEntity[ValveInfo, ValveState], ValveEntity): @@ -103,3 +86,11 @@ class EsphomeValve(EsphomeEntity[ValveInfo, ValveState], ValveEntity): async def async_set_valve_position(self, position: float) -> None: """Move the valve to a specific position.""" self._client.valve_command(key=self._key, position=position / 100) + + +async_setup_entry = partial( + platform_async_setup_entry, + info_type=ValveInfo, + entity_type=EsphomeValve, + state_type=ValveState, +) From 6e5bc0da94b56e18a0e16267fd70354ff1e600fa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:07:50 +0200 Subject: [PATCH 0131/2411] Improve type hints in cloud tests (#120420) --- tests/components/cloud/__init__.py | 7 ++- tests/components/cloud/conftest.py | 16 +++---- tests/components/cloud/test_account_link.py | 10 ++++- tests/components/cloud/test_alexa_config.py | 41 ++++++++++------- tests/components/cloud/test_client.py | 19 +++++--- tests/components/cloud/test_google_config.py | 47 +++++++++++++------- tests/components/cloud/test_init.py | 12 +++-- 7 files changed, 96 insertions(+), 56 deletions(-) diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index f1ce24e576f..18f8cd4d311 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -1,5 +1,6 @@ """Tests for the cloud component.""" +from typing import Any from unittest.mock import AsyncMock, patch from homeassistant.components.cloud.const import ( @@ -14,7 +15,9 @@ from homeassistant.components.cloud.const import ( from homeassistant.components.cloud.prefs import ( ALEXA_SETTINGS_VERSION, GOOGLE_SETTINGS_VERSION, + CloudPreferences, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component PIPELINE_DATA = { @@ -66,7 +69,7 @@ PIPELINE_DATA = { } -async def mock_cloud(hass, config=None): +async def mock_cloud(hass: HomeAssistant, config: dict[str, Any] | None = None) -> None: """Mock cloud.""" # The homeassistant integration is needed by cloud. It's not in it's requirements # because it's always setup by bootstrap. Set it up manually in tests. @@ -78,7 +81,7 @@ async def mock_cloud(hass, config=None): await cloud_inst.initialize() -def mock_cloud_prefs(hass, prefs): +def mock_cloud_prefs(hass: HomeAssistant, prefs: dict[str, Any]) -> CloudPreferences: """Fixture for cloud component.""" prefs_to_set = { PREF_ALEXA_SETTINGS_VERSION: ALEXA_SETTINGS_VERSION, diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index a7abb932124..c7d0702ea88 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -15,7 +15,7 @@ from hass_nabucasa.remote import RemoteUI from hass_nabucasa.voice import Voice import jwt import pytest -from typing_extensions import AsyncGenerator +from typing_extensions import AsyncGenerator, Generator from homeassistant.components.cloud.client import CloudClient from homeassistant.components.cloud.const import DATA_CLOUD @@ -199,21 +199,21 @@ def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock: MagicMock) -> None: @pytest.fixture(autouse=True) -def mock_user_data(): +def mock_user_data() -> Generator[MagicMock]: """Mock os module.""" with patch("hass_nabucasa.Cloud._write_user_info") as writer: yield writer @pytest.fixture -def mock_cloud_fixture(hass): +def mock_cloud_fixture(hass: HomeAssistant) -> CloudPreferences: """Fixture for cloud component.""" hass.loop.run_until_complete(mock_cloud(hass)) return mock_cloud_prefs(hass, {}) @pytest.fixture -async def cloud_prefs(hass): +async def cloud_prefs(hass: HomeAssistant) -> CloudPreferences: """Fixture for cloud preferences.""" cloud_prefs = CloudPreferences(hass) await cloud_prefs.async_initialize() @@ -221,13 +221,13 @@ async def cloud_prefs(hass): @pytest.fixture -async def mock_cloud_setup(hass): +async def mock_cloud_setup(hass: HomeAssistant) -> None: """Set up the cloud.""" await mock_cloud(hass) @pytest.fixture -def mock_cloud_login(hass, mock_cloud_setup): +def mock_cloud_login(hass: HomeAssistant, mock_cloud_setup: None) -> Generator[None]: """Mock cloud is logged in.""" hass.data[DATA_CLOUD].id_token = jwt.encode( { @@ -242,7 +242,7 @@ def mock_cloud_login(hass, mock_cloud_setup): @pytest.fixture(name="mock_auth") -def mock_auth_fixture(): +def mock_auth_fixture() -> Generator[None]: """Mock check token.""" with ( patch("hass_nabucasa.auth.CognitoAuth.async_check_token"), @@ -252,7 +252,7 @@ def mock_auth_fixture(): @pytest.fixture -def mock_expired_cloud_login(hass, mock_cloud_setup): +def mock_expired_cloud_login(hass: HomeAssistant, mock_cloud_setup: None) -> None: """Mock cloud is logged in.""" hass.data[DATA_CLOUD].id_token = jwt.encode( { diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 7a85531904a..acaff7db76c 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -6,6 +6,7 @@ from time import time from unittest.mock import AsyncMock, Mock, patch import pytest +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.cloud import account_link @@ -21,7 +22,9 @@ TEST_DOMAIN = "oauth2_test" @pytest.fixture -def flow_handler(hass): +def flow_handler( + hass: HomeAssistant, +) -> Generator[type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler]]: """Return a registered config flow.""" mock_platform(hass, f"{TEST_DOMAIN}.config_flow") @@ -180,7 +183,10 @@ async def test_get_services_error(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("current_request_with_host") -async def test_implementation(hass: HomeAssistant, flow_handler) -> None: +async def test_implementation( + hass: HomeAssistant, + flow_handler: type[config_entry_oauth2_flow.AbstractOAuth2FlowHandler], +) -> None: """Test Cloud OAuth2 implementation.""" hass.data[DATA_CLOUD] = None diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index e4ad425d4d4..3b4868b56ac 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -34,7 +34,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker @pytest.fixture -def cloud_stub(): +def cloud_stub() -> Mock: """Stub the cloud.""" return Mock(is_logged_in=True, subscription_expired=False) @@ -51,7 +51,10 @@ def expose_entity(hass: HomeAssistant, entity_id: str, should_expose: bool) -> N async def test_alexa_config_expose_entity_prefs( - hass: HomeAssistant, cloud_prefs, cloud_stub, entity_registry: er.EntityRegistry + hass: HomeAssistant, + cloud_prefs: CloudPreferences, + cloud_stub: Mock, + entity_registry: er.EntityRegistry, ) -> None: """Test Alexa config should expose using prefs.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -133,7 +136,7 @@ async def test_alexa_config_expose_entity_prefs( async def test_alexa_config_report_state( - hass: HomeAssistant, cloud_prefs, cloud_stub + hass: HomeAssistant, cloud_prefs: CloudPreferences, cloud_stub: Mock ) -> None: """Test Alexa config should expose using prefs.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -168,7 +171,9 @@ async def test_alexa_config_report_state( async def test_alexa_config_invalidate_token( - hass: HomeAssistant, cloud_prefs, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + cloud_prefs: CloudPreferences, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test Alexa config should expose using prefs.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -218,11 +223,11 @@ async def test_alexa_config_invalidate_token( ) async def test_alexa_config_fail_refresh_token( hass: HomeAssistant, - cloud_prefs, + cloud_prefs: CloudPreferences, aioclient_mock: AiohttpClientMocker, entity_registry: er.EntityRegistry, - reject_reason, - expected_exception, + reject_reason: str, + expected_exception: type[Exception], ) -> None: """Test Alexa config failing to refresh token.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -342,7 +347,10 @@ def patch_sync_helper(): async def test_alexa_update_expose_trigger_sync( - hass: HomeAssistant, entity_registry: er.EntityRegistry, cloud_prefs, cloud_stub + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + cloud_prefs: CloudPreferences, + cloud_stub: Mock, ) -> None: """Test Alexa config responds to updating exposed entities.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -415,11 +423,11 @@ async def test_alexa_update_expose_trigger_sync( ] +@pytest.mark.usefixtures("mock_cloud_login") async def test_alexa_entity_registry_sync( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_cloud_login, - cloud_prefs, + cloud_prefs: CloudPreferences, ) -> None: """Test Alexa config responds to entity registry.""" # Enable exposing new entities to Alexa @@ -475,7 +483,7 @@ async def test_alexa_entity_registry_sync( async def test_alexa_update_report_state( - hass: HomeAssistant, cloud_prefs, cloud_stub + hass: HomeAssistant, cloud_prefs: CloudPreferences, cloud_stub: Mock ) -> None: """Test Alexa config responds to reporting state.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -502,8 +510,9 @@ async def test_alexa_update_report_state( assert len(mock_sync.mock_calls) == 1 +@pytest.mark.usefixtures("mock_expired_cloud_login") def test_enabled_requires_valid_sub( - hass: HomeAssistant, mock_expired_cloud_login, cloud_prefs + hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test that alexa config enabled requires a valid Cloud sub.""" assert cloud_prefs.alexa_enabled @@ -518,7 +527,7 @@ def test_enabled_requires_valid_sub( async def test_alexa_handle_logout( - hass: HomeAssistant, cloud_prefs, cloud_stub + hass: HomeAssistant, cloud_prefs: CloudPreferences, cloud_stub: Mock ) -> None: """Test Alexa config responds to logging out.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -561,7 +570,7 @@ async def test_alexa_handle_logout( async def test_alexa_config_migrate_expose_entity_prefs( hass: HomeAssistant, cloud_prefs: CloudPreferences, - cloud_stub, + cloud_stub: Mock, entity_registry: er.EntityRegistry, alexa_settings_version: int, ) -> None: @@ -755,7 +764,7 @@ async def test_alexa_config_migrate_expose_entity_prefs_v2_exposed( async def test_alexa_config_migrate_expose_entity_prefs_default_none( hass: HomeAssistant, cloud_prefs: CloudPreferences, - cloud_stub, + cloud_stub: Mock, entity_registry: er.EntityRegistry, ) -> None: """Test migrating Alexa entity config.""" @@ -793,7 +802,7 @@ async def test_alexa_config_migrate_expose_entity_prefs_default_none( async def test_alexa_config_migrate_expose_entity_prefs_default( hass: HomeAssistant, cloud_prefs: CloudPreferences, - cloud_stub, + cloud_stub: Mock, entity_registry: er.EntityRegistry, ) -> None: """Test migrating Alexa entity config.""" diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 62af4e88857..005efd990fb 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -1,6 +1,7 @@ """Test the cloud.iot module.""" from datetime import timedelta +from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch import aiohttp @@ -20,6 +21,7 @@ from homeassistant.components.cloud.const import ( PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, ) +from homeassistant.components.cloud.prefs import CloudPreferences from homeassistant.components.homeassistant.exposed_entities import ( DATA_EXPOSED_ENTITIES, async_expose_entity, @@ -37,7 +39,7 @@ from tests.components.alexa import test_smart_home as test_alexa @pytest.fixture -def mock_cloud_inst(): +def mock_cloud_inst() -> MagicMock: """Mock cloud class.""" return MagicMock(subscription_expired=False) @@ -81,7 +83,9 @@ async def test_handler_alexa(hass: HomeAssistant) -> None: assert device["manufacturerName"] == "Home Assistant" -async def test_handler_alexa_disabled(hass: HomeAssistant, mock_cloud_fixture) -> None: +async def test_handler_alexa_disabled( + hass: HomeAssistant, mock_cloud_fixture: CloudPreferences +) -> None: """Test handler Alexa when user has disabled it.""" mock_cloud_fixture._prefs[PREF_ENABLE_ALEXA] = False cloud = hass.data[DATA_CLOUD] @@ -154,7 +158,10 @@ async def test_handler_google_actions(hass: HomeAssistant) -> None: ], ) async def test_handler_google_actions_disabled( - hass: HomeAssistant, mock_cloud_fixture, intent, response_payload + hass: HomeAssistant, + mock_cloud_fixture: CloudPreferences, + intent: str, + response_payload: dict[str, Any], ) -> None: """Test handler Google Actions when user has disabled it.""" mock_cloud_fixture._prefs[PREF_ENABLE_GOOGLE] = False @@ -253,11 +260,10 @@ async def test_webhook_msg( assert '{"nonexisting": "payload"}' in caplog.text +@pytest.mark.usefixtures("mock_cloud_setup", "mock_cloud_login") async def test_google_config_expose_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_cloud_setup, - mock_cloud_login, ) -> None: """Test Google config exposing entity method uses latest config.""" @@ -281,11 +287,10 @@ async def test_google_config_expose_entity( assert not gconf.should_expose(state) +@pytest.mark.usefixtures("mock_cloud_setup", "mock_cloud_login") async def test_google_config_should_2fa( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_cloud_setup, - mock_cloud_login, ) -> None: """Test Google config disabling 2FA method uses latest config.""" diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 40d3f6ef2c5..b152309b24a 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -36,7 +36,7 @@ from tests.common import async_fire_time_changed @pytest.fixture -def mock_conf(hass, cloud_prefs): +def mock_conf(hass: HomeAssistant, cloud_prefs: CloudPreferences) -> CloudGoogleConfig: """Mock Google conf.""" return CloudGoogleConfig( hass, @@ -59,7 +59,7 @@ def expose_entity(hass: HomeAssistant, entity_id: str, should_expose: bool) -> N async def test_google_update_report_state( - mock_conf, hass: HomeAssistant, cloud_prefs + mock_conf: CloudGoogleConfig, hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test Google config responds to updating preference.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -83,7 +83,7 @@ async def test_google_update_report_state( async def test_google_update_report_state_subscription_expired( - mock_conf, hass: HomeAssistant, cloud_prefs + mock_conf: CloudGoogleConfig, hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test Google config not reporting state when subscription has expired.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -106,7 +106,9 @@ async def test_google_update_report_state_subscription_expired( assert len(mock_report_state.mock_calls) == 0 -async def test_sync_entities(mock_conf, hass: HomeAssistant, cloud_prefs) -> None: +async def test_sync_entities( + mock_conf: CloudGoogleConfig, hass: HomeAssistant, cloud_prefs: CloudPreferences +) -> None: """Test sync devices.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -129,7 +131,9 @@ async def test_sync_entities(mock_conf, hass: HomeAssistant, cloud_prefs) -> Non async def test_google_update_expose_trigger_sync( - hass: HomeAssistant, entity_registry: er.EntityRegistry, cloud_prefs + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + cloud_prefs: CloudPreferences, ) -> None: """Test Google config responds to updating exposed entities.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -185,11 +189,11 @@ async def test_google_update_expose_trigger_sync( assert len(mock_sync.mock_calls) == 1 +@pytest.mark.usefixtures("mock_cloud_login") async def test_google_entity_registry_sync( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_cloud_login, - cloud_prefs, + cloud_prefs: CloudPreferences, ) -> None: """Test Google config responds to entity registry.""" @@ -257,11 +261,11 @@ async def test_google_entity_registry_sync( assert len(mock_sync.mock_calls) == 3 +@pytest.mark.usefixtures("mock_cloud_login") async def test_google_device_registry_sync( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_cloud_login, - cloud_prefs, + cloud_prefs: CloudPreferences, ) -> None: """Test Google config responds to device registry.""" config = CloudGoogleConfig( @@ -329,8 +333,9 @@ async def test_google_device_registry_sync( assert len(mock_sync.mock_calls) == 1 +@pytest.mark.usefixtures("mock_cloud_login") async def test_sync_google_when_started( - hass: HomeAssistant, mock_cloud_login, cloud_prefs + hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test Google config syncs on init.""" config = CloudGoogleConfig( @@ -342,8 +347,9 @@ async def test_sync_google_when_started( assert len(mock_sync.mock_calls) == 1 +@pytest.mark.usefixtures("mock_cloud_login") async def test_sync_google_on_home_assistant_start( - hass: HomeAssistant, mock_cloud_login, cloud_prefs + hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test Google config syncs when home assistant started.""" config = CloudGoogleConfig( @@ -361,7 +367,10 @@ async def test_sync_google_on_home_assistant_start( async def test_google_config_expose_entity_prefs( - hass: HomeAssistant, mock_conf, cloud_prefs, entity_registry: er.EntityRegistry + hass: HomeAssistant, + mock_conf: CloudGoogleConfig, + cloud_prefs: CloudPreferences, + entity_registry: er.EntityRegistry, ) -> None: """Test Google config should expose using prefs.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -437,8 +446,9 @@ async def test_google_config_expose_entity_prefs( assert not mock_conf.should_expose(state_not_exposed) +@pytest.mark.usefixtures("mock_expired_cloud_login") def test_enabled_requires_valid_sub( - hass: HomeAssistant, mock_expired_cloud_login, cloud_prefs + hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test that google config enabled requires a valid Cloud sub.""" assert cloud_prefs.google_enabled @@ -453,7 +463,7 @@ def test_enabled_requires_valid_sub( async def test_setup_google_assistant( - hass: HomeAssistant, mock_conf, cloud_prefs + hass: HomeAssistant, mock_conf: CloudGoogleConfig, cloud_prefs: CloudPreferences ) -> None: """Test that we set up the google_assistant integration if enabled in cloud.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -472,8 +482,9 @@ async def test_setup_google_assistant( assert "google_assistant" in hass.config.components +@pytest.mark.usefixtures("mock_cloud_login") async def test_google_handle_logout( - hass: HomeAssistant, cloud_prefs, mock_cloud_login + hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test Google config responds to logging out.""" gconf = CloudGoogleConfig( @@ -853,8 +864,9 @@ async def test_google_config_migrate_expose_entity_prefs_default( } +@pytest.mark.usefixtures("mock_cloud_login") async def test_google_config_get_agent_user_id( - hass: HomeAssistant, mock_cloud_login, cloud_prefs + hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test overridden get_agent_user_id_from_webhook method.""" config = CloudGoogleConfig( @@ -867,8 +879,9 @@ async def test_google_config_get_agent_user_id( assert config.get_agent_user_id_from_webhook("other_id") != config.agent_user_id +@pytest.mark.usefixtures("mock_cloud_login") async def test_google_config_get_agent_users( - hass: HomeAssistant, mock_cloud_login, cloud_prefs + hass: HomeAssistant, cloud_prefs: CloudPreferences ) -> None: """Test overridden async_get_agent_users method.""" username_mock = PropertyMock(return_value="blah") diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index d201b45b670..ad123cded84 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -67,8 +67,9 @@ async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None: assert cl.remotestate_server == "test-remotestate-server" +@pytest.mark.usefixtures("mock_cloud_fixture") async def test_remote_services( - hass: HomeAssistant, mock_cloud_fixture, hass_read_only_user: MockUser + hass: HomeAssistant, hass_read_only_user: MockUser ) -> None: """Setup cloud component and test services.""" cloud = hass.data[DATA_CLOUD] @@ -114,7 +115,8 @@ async def test_remote_services( assert mock_disconnect.called is False -async def test_shutdown_event(hass: HomeAssistant, mock_cloud_fixture) -> None: +@pytest.mark.usefixtures("mock_cloud_fixture") +async def test_shutdown_event(hass: HomeAssistant) -> None: """Test if the cloud will stop on shutdown event.""" with patch("hass_nabucasa.Cloud.stop") as mock_stop: hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) @@ -149,7 +151,8 @@ async def test_setup_existing_cloud_user( assert hass_storage[STORAGE_KEY]["data"]["cloud_user"] == user.id -async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None: +@pytest.mark.usefixtures("mock_cloud_fixture") +async def test_on_connect(hass: HomeAssistant) -> None: """Test cloud on connect triggers.""" cl = hass.data[DATA_CLOUD] @@ -206,7 +209,8 @@ async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None: assert cloud_states[-1] == CloudConnectionState.CLOUD_DISCONNECTED -async def test_remote_ui_url(hass: HomeAssistant, mock_cloud_fixture) -> None: +@pytest.mark.usefixtures("mock_cloud_fixture") +async def test_remote_ui_url(hass: HomeAssistant) -> None: """Test getting remote ui url.""" cl = hass.data[DATA_CLOUD] From 6a370bde34c96d06dbc9129124ad3439814733b7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:08:10 +0200 Subject: [PATCH 0132/2411] Adjust imports in samsungtv tests (#120409) --- tests/components/samsungtv/test_init.py | 21 ++-- .../components/samsungtv/test_media_player.py | 108 +++++++++--------- tests/components/samsungtv/test_remote.py | 4 +- 3 files changed, 68 insertions(+), 65 deletions(-) diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 479664d4ec0..5715bd4b0aa 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -6,13 +6,16 @@ import pytest from samsungtvws.async_remote import SamsungTVWSAsyncRemote from syrupy.assertion import SnapshotAssertion -from homeassistant.components.media_player import DOMAIN, MediaPlayerEntityFeature +from homeassistant.components.media_player import ( + DOMAIN as MP_DOMAIN, + MediaPlayerEntityFeature, +) from homeassistant.components.samsungtv.const import ( CONF_MANUFACTURER, CONF_SESSION_ID, CONF_SSDP_MAIN_TV_AGENT_LOCATION, CONF_SSDP_RENDERING_CONTROL_LOCATION, - DOMAIN as SAMSUNGTV_DOMAIN, + DOMAIN, LEGACY_PORT, METHOD_LEGACY, METHOD_WEBSOCKET, @@ -47,7 +50,7 @@ from .const import ( from tests.common import MockConfigEntry -ENTITY_ID = f"{DOMAIN}.fake_name" +ENTITY_ID = f"{MP_DOMAIN}.fake_name" MOCK_CONFIG = { CONF_HOST: "fake_host", CONF_NAME: "fake_name", @@ -71,7 +74,7 @@ async def test_setup(hass: HomeAssistant) -> None: # test host and port await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -94,7 +97,7 @@ async def test_setup_without_port_device_offline(hass: HomeAssistant) -> None: ): await setup_samsungtv_entry(hass, MOCK_CONFIG) - config_entries_domain = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + config_entries_domain = hass.config_entries.async_entries(DOMAIN) assert len(config_entries_domain) == 1 assert config_entries_domain[0].state is ConfigEntryState.SETUP_RETRY @@ -104,7 +107,7 @@ async def test_setup_without_port_device_online(hass: HomeAssistant) -> None: """Test import from yaml when the device is online.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) - config_entries_domain = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + config_entries_domain = hass.config_entries.async_entries(DOMAIN) assert len(config_entries_domain) == 1 assert config_entries_domain[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" @@ -183,7 +186,7 @@ async def test_update_imported_legacy_without_method(hass: HomeAssistant) -> Non hass, {CONF_HOST: "fake_host", CONF_MANUFACTURER: "Samsung"} ) - entries = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 assert entries[0].data[CONF_METHOD] == METHOD_LEGACY assert entries[0].data[CONF_PORT] == LEGACY_PORT @@ -214,7 +217,7 @@ async def test_incorrectly_formatted_mac_fixed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - config_entries = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + config_entries = hass.config_entries.async_entries(DOMAIN) assert len(config_entries) == 1 assert config_entries[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" @@ -229,7 +232,7 @@ async def test_cleanup_mac( Reverted due to device registry collisions in #119249 / #119082 """ entry = MockConfigEntry( - domain=SAMSUNGTV_DOMAIN, + domain=DOMAIN, data=MOCK_ENTRY_WS_WITH_MAC, entry_id="123456", unique_id="any", diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 4c7ee0e116d..ef7e58251e8 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -31,7 +31,7 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, - DOMAIN, + DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, MediaPlayerDeviceClass, @@ -39,7 +39,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.samsungtv.const import ( CONF_SSDP_RENDERING_CONTROL_LOCATION, - DOMAIN as SAMSUNGTV_DOMAIN, + DOMAIN, ENCRYPTED_WEBSOCKET_PORT, METHOD_ENCRYPTED_WEBSOCKET, METHOD_WEBSOCKET, @@ -91,7 +91,7 @@ from .const import ( from tests.common import MockConfigEntry, async_fire_time_changed -ENTITY_ID = f"{DOMAIN}.fake" +ENTITY_ID = f"{MP_DOMAIN}.fake" MOCK_CONFIGWS = { CONF_HOST: "fake_host", CONF_NAME: "fake", @@ -145,7 +145,7 @@ async def test_setup_websocket(hass: HomeAssistant) -> None: await hass.async_block_till_done() - config_entries = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + config_entries = hass.config_entries.async_entries(DOMAIN) assert len(config_entries) == 1 assert config_entries[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" @@ -155,16 +155,16 @@ async def test_setup_websocket_2( hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_now: datetime ) -> None: """Test setup of platform from config entry.""" - entity_id = f"{DOMAIN}.fake" + entity_id = f"{MP_DOMAIN}.fake" entry = MockConfigEntry( - domain=SAMSUNGTV_DOMAIN, + domain=DOMAIN, data=MOCK_ENTRY_WS, unique_id=entity_id, ) entry.add_to_hass(hass) - config_entries = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + config_entries = hass.config_entries.async_entries(DOMAIN) assert len(config_entries) == 1 assert entry is config_entries[0] @@ -549,7 +549,7 @@ async def test_send_key(hass: HomeAssistant, remote: Mock) -> None: """Test for send key.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) # key called @@ -563,7 +563,7 @@ async def test_send_key_broken_pipe(hass: HomeAssistant, remote: Mock) -> None: await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.control = Mock(side_effect=BrokenPipeError("Boom")) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -578,7 +578,7 @@ async def test_send_key_connection_closed_retry_succeed( side_effect=[exceptions.ConnectionClosed("Boom"), DEFAULT_MOCK, DEFAULT_MOCK] ) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) # key because of retry two times @@ -595,7 +595,7 @@ async def test_send_key_unhandled_response(hass: HomeAssistant, remote: Mock) -> await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.control = Mock(side_effect=exceptions.UnhandledResponse("Boom")) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -607,7 +607,7 @@ async def test_send_key_websocketexception(hass: HomeAssistant, remotews: Mock) await setup_samsungtv_entry(hass, MOCK_CONFIGWS) remotews.send_commands = Mock(side_effect=WebSocketException("Boom")) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -621,7 +621,7 @@ async def test_send_key_websocketexception_encrypted( await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) remoteencws.send_commands = Mock(side_effect=WebSocketException("Boom")) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -633,7 +633,7 @@ async def test_send_key_os_error_ws(hass: HomeAssistant, remotews: Mock) -> None await setup_samsungtv_entry(hass, MOCK_CONFIGWS) remotews.send_commands = Mock(side_effect=OSError("Boom")) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -647,7 +647,7 @@ async def test_send_key_os_error_ws_encrypted( await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) remoteencws.send_commands = Mock(side_effect=OSError("Boom")) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -658,7 +658,7 @@ async def test_send_key_os_error(hass: HomeAssistant, remote: Mock) -> None: await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.control = Mock(side_effect=OSError("Boom")) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -677,12 +677,12 @@ async def test_state(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> Non """Test for state property.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) # Should be STATE_UNAVAILABLE after the timer expires @@ -733,7 +733,7 @@ async def test_turn_off_websocket( remotews.send_commands.reset_mock() await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remotews.send_commands.call_count == 1 @@ -745,11 +745,11 @@ async def test_turn_off_websocket( # commands not sent : power off in progress remotews.send_commands.reset_mock() await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) assert "TV is powering off, not sending keys: ['KEY_VOLUP']" in caplog.text await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_SELECT_SOURCE, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "Deezer"}, True, @@ -772,7 +772,7 @@ async def test_turn_off_websocket_frame( remotews.send_commands.reset_mock() await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remotews.send_commands.call_count == 1 @@ -800,7 +800,7 @@ async def test_turn_off_encrypted_websocket( caplog.clear() await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remoteencws.send_commands.call_count == 1 @@ -815,7 +815,7 @@ async def test_turn_off_encrypted_websocket( # commands not sent : power off in progress remoteencws.send_commands.reset_mock() await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) assert "TV is powering off, not sending keys: ['KEY_VOLUP']" in caplog.text remoteencws.send_commands.assert_not_called() @@ -841,7 +841,7 @@ async def test_turn_off_encrypted_websocket_key_type( caplog.clear() await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remoteencws.send_commands.call_count == 1 @@ -856,7 +856,7 @@ async def test_turn_off_legacy(hass: HomeAssistant, remote: Mock) -> None: """Test for turn_off.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 1 @@ -871,7 +871,7 @@ async def test_turn_off_os_error( await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.close = Mock(side_effect=OSError("BOOM")) await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) assert "Could not establish connection" in caplog.text @@ -885,7 +885,7 @@ async def test_turn_off_ws_os_error( await setup_samsungtv_entry(hass, MOCK_CONFIGWS) remotews.close = Mock(side_effect=OSError("BOOM")) await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) assert "Error closing connection" in caplog.text @@ -899,7 +899,7 @@ async def test_turn_off_encryptedws_os_error( await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) remoteencws.close = Mock(side_effect=OSError("BOOM")) await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) assert "Error closing connection" in caplog.text @@ -908,7 +908,7 @@ async def test_volume_up(hass: HomeAssistant, remote: Mock) -> None: """Test for volume_up.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 1 @@ -919,7 +919,7 @@ async def test_volume_down(hass: HomeAssistant, remote: Mock) -> None: """Test for volume_down.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_DOWN, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_VOLUME_DOWN, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 1 @@ -930,7 +930,7 @@ async def test_mute_volume(hass: HomeAssistant, remote: Mock) -> None: """Test for mute_volume.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_VOLUME_MUTE, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_MUTED: True}, True, @@ -944,14 +944,14 @@ async def test_media_play(hass: HomeAssistant, remote: Mock) -> None: """Test for media_play.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_MEDIA_PLAY, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_MEDIA_PLAY, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 1 assert remote.control.call_args_list == [call("KEY_PLAY")] await hass.services.async_call( - DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 2 @@ -962,14 +962,14 @@ async def test_media_pause(hass: HomeAssistant, remote: Mock) -> None: """Test for media_pause.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_MEDIA_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_MEDIA_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 1 assert remote.control.call_args_list == [call("KEY_PAUSE")] await hass.services.async_call( - DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 2 @@ -980,7 +980,7 @@ async def test_media_next_track(hass: HomeAssistant, remote: Mock) -> None: """Test for media_next_track.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_MEDIA_NEXT_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 1 @@ -991,7 +991,7 @@ async def test_media_previous_track(hass: HomeAssistant, remote: Mock) -> None: """Test for media_previous_track.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called assert remote.control.call_count == 1 @@ -1002,7 +1002,7 @@ async def test_media_previous_track(hass: HomeAssistant, remote: Mock) -> None: async def test_turn_on_wol(hass: HomeAssistant) -> None: """Test turn on.""" entry = MockConfigEntry( - domain=SAMSUNGTV_DOMAIN, + domain=DOMAIN, data=MOCK_ENTRY_WS_WITH_MAC, unique_id="any", ) @@ -1013,7 +1013,7 @@ async def test_turn_on_wol(hass: HomeAssistant) -> None: "homeassistant.components.samsungtv.entity.send_magic_packet" ) as mock_send_magic_packet: await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True ) await hass.async_block_till_done() assert mock_send_magic_packet.called @@ -1024,7 +1024,7 @@ async def test_turn_on_without_turnon(hass: HomeAssistant, remote: Mock) -> None await setup_samsungtv_entry(hass, MOCK_CONFIG) with pytest.raises(HomeAssistantError, match="does not support this service"): await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # nothing called as not supported feature assert remote.control.call_count == 0 @@ -1035,7 +1035,7 @@ async def test_play_media(hass: HomeAssistant, remote: Mock) -> None: await setup_samsungtv_entry(hass, MOCK_CONFIG) with patch("homeassistant.components.samsungtv.bridge.asyncio.sleep") as sleep: await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, @@ -1062,7 +1062,7 @@ async def test_play_media_invalid_type(hass: HomeAssistant) -> None: await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.reset_mock() await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, @@ -1082,7 +1082,7 @@ async def test_play_media_channel_as_string(hass: HomeAssistant) -> None: await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.reset_mock() await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, @@ -1101,7 +1101,7 @@ async def test_play_media_channel_as_non_positive(hass: HomeAssistant) -> None: await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.reset_mock() await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, @@ -1118,7 +1118,7 @@ async def test_select_source(hass: HomeAssistant, remote: Mock) -> None: """Test for select_source.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_SELECT_SOURCE, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "HDMI"}, True, @@ -1134,7 +1134,7 @@ async def test_select_source_invalid_source(hass: HomeAssistant) -> None: await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.reset_mock() await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_SELECT_SOURCE, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "INVALID"}, True, @@ -1150,7 +1150,7 @@ async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: remotews.send_commands.reset_mock() await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: ENTITY_ID, @@ -1174,7 +1174,7 @@ async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: remotews.send_commands.reset_mock() await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_SELECT_SOURCE, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "Deezer"}, True, @@ -1199,7 +1199,7 @@ async def test_websocket_unsupported_remote_control( remotews.send_commands.reset_mock() await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) remotews.raise_mock_ws_event_callback( "ms.error", @@ -1248,7 +1248,7 @@ async def test_volume_control_upnp( # Upnp action succeeds await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_VOLUME_SET, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.5}, True, @@ -1262,7 +1262,7 @@ async def test_volume_control_upnp( status=500, error_code=501, error_desc="Action Failed" ) await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_VOLUME_SET, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6}, True, @@ -1281,7 +1281,7 @@ async def test_upnp_not_available( # Upnp action fails await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_VOLUME_SET, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6}, True, @@ -1299,7 +1299,7 @@ async def test_upnp_missing_service( # Upnp action fails await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_VOLUME_SET, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6}, True, diff --git a/tests/components/samsungtv/test_remote.py b/tests/components/samsungtv/test_remote.py index 98cf712e0d2..854c92207bf 100644 --- a/tests/components/samsungtv/test_remote.py +++ b/tests/components/samsungtv/test_remote.py @@ -10,7 +10,7 @@ from homeassistant.components.remote import ( DOMAIN as REMOTE_DOMAIN, SERVICE_SEND_COMMAND, ) -from homeassistant.components.samsungtv.const import DOMAIN as SAMSUNGTV_DOMAIN +from homeassistant.components.samsungtv.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -102,7 +102,7 @@ async def test_send_command_service(hass: HomeAssistant, remoteencws: Mock) -> N async def test_turn_on_wol(hass: HomeAssistant) -> None: """Test turn on.""" entry = MockConfigEntry( - domain=SAMSUNGTV_DOMAIN, + domain=DOMAIN, data=MOCK_ENTRY_WS_WITH_MAC, unique_id="any", ) From 253514a1242bf1ecc1236e6a7b1d1628ea405df6 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 25 Jun 2024 17:08:36 +0200 Subject: [PATCH 0133/2411] Bump pywaze to 1.0.2 (#120412) --- homeassistant/components/waze_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index ce7c9105781..9d615431c7d 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "iot_class": "cloud_polling", "loggers": ["pywaze", "homeassistant.helpers.location"], - "requirements": ["pywaze==1.0.1"] + "requirements": ["pywaze==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b1b393d1f4..48eae313cf4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2391,7 +2391,7 @@ pyvlx==0.2.21 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==1.0.1 +pywaze==1.0.2 # homeassistant.components.weatherflow pyweatherflowudp==1.4.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 135581ff6ea..3b3adcf409a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1870,7 +1870,7 @@ pyvlx==0.2.21 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==1.0.1 +pywaze==1.0.2 # homeassistant.components.weatherflow pyweatherflowudp==1.4.5 From 77fea8a73eb271d35c5e359f9a62567c1d760a32 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Tue, 25 Jun 2024 17:15:12 +0200 Subject: [PATCH 0134/2411] Add reauth flow to pyLoad integration (#120376) Add reauth flow --- homeassistant/components/pyload/__init__.py | 4 +- .../components/pyload/config_flow.py | 77 +++++++++++++++- .../components/pyload/coordinator.py | 6 +- homeassistant/components/pyload/strings.json | 10 ++- tests/components/pyload/conftest.py | 13 +++ tests/components/pyload/test_config_flow.py | 90 ++++++++++++++++++- tests/components/pyload/test_init.py | 4 +- 7 files changed, 192 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/pyload/__init__.py b/homeassistant/components/pyload/__init__.py index b30b044e238..8bf065797e5 100644 --- a/homeassistant/components/pyload/__init__.py +++ b/homeassistant/components/pyload/__init__.py @@ -17,7 +17,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_create_clientsession from .coordinator import PyLoadCoordinator @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bo except ParserError as e: raise ConfigEntryNotReady("Unable to parse data from pyLoad API") from e except InvalidAuth as e: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( f"Authentication failed for {entry.data[CONF_USERNAME]}, check your login credentials" ) from e coordinator = PyLoadCoordinator(hass, pyloadapi) diff --git a/homeassistant/components/pyload/config_flow.py b/homeassistant/components/pyload/config_flow.py index 7ebc4a501d4..7a2dfddeb5b 100644 --- a/homeassistant/components/pyload/config_flow.py +++ b/homeassistant/components/pyload/config_flow.py @@ -2,8 +2,9 @@ from __future__ import annotations +from collections.abc import Mapping import logging -from typing import Any +from typing import TYPE_CHECKING, Any from aiohttp import CookieJar from pyloadapi.api import PyLoadAPI @@ -23,7 +24,13 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) +from . import PyLoadConfigEntry from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -39,6 +46,23 @@ STEP_USER_DATA_SCHEMA = vol.Schema( } ) +REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): TextSelector( + TextSelectorConfig( + type=TextSelectorType.TEXT, + autocomplete="username", + ), + ), + vol.Required(CONF_PASSWORD): TextSelector( + TextSelectorConfig( + type=TextSelectorType.PASSWORD, + autocomplete="current-password", + ), + ), + } +) + async def validate_input(hass: HomeAssistant, user_input: dict[str, Any]) -> None: """Validate the user input and try to connect to PyLoad.""" @@ -67,8 +91,7 @@ class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for pyLoad.""" VERSION = 1 - # store values from yaml import so we can use them as - # suggested values when the configuration step is resumed + config_entry: PyLoadConfigEntry | None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -118,3 +141,51 @@ class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN): if errors := result.get("errors"): return self.async_abort(reason=errors["base"]) return result + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an API authentication error.""" + self.config_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Dialog that informs the user that reauth is required.""" + errors = {} + + if TYPE_CHECKING: + assert self.config_entry + + if user_input is not None: + new_input = self.config_entry.data | user_input + try: + await validate_input(self.hass, new_input) + except (CannotConnect, ParserError): + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_update_reload_and_abort( + self.config_entry, data=new_input + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=self.add_suggested_values_to_schema( + REAUTH_SCHEMA, + { + CONF_USERNAME: user_input[CONF_USERNAME] + if user_input is not None + else self.config_entry.data[CONF_USERNAME] + }, + ), + description_placeholders={CONF_NAME: self.config_entry.data[CONF_USERNAME]}, + errors=errors, + ) diff --git a/homeassistant/components/pyload/coordinator.py b/homeassistant/components/pyload/coordinator.py index 008375c3a34..b96a8d2ccbf 100644 --- a/homeassistant/components/pyload/coordinator.py +++ b/homeassistant/components/pyload/coordinator.py @@ -8,7 +8,7 @@ from pyloadapi import CannotConnect, InvalidAuth, ParserError, PyLoadAPI from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -63,12 +63,12 @@ class PyLoadCoordinator(DataUpdateCoordinator[pyLoadData]): try: await self.pyload.login() except InvalidAuth as exc: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( f"Authentication failed for {self.pyload.username}, check your login credentials", ) from exc raise UpdateFailed( - "Unable to retrieve data due to cookie expiration but re-authentication was successful." + "Unable to retrieve data due to cookie expiration" ) from e except CannotConnect as e: raise UpdateFailed( diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 94c0c29d286..6efdb23eaf4 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -16,6 +16,13 @@ "host": "The hostname or IP address of the device running your pyLoad instance.", "port": "pyLoad uses port 8000 by default." } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -24,7 +31,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "entity": { diff --git a/tests/components/pyload/conftest.py b/tests/components/pyload/conftest.py index 3c6f9fdb49a..1d7b11567c7 100644 --- a/tests/components/pyload/conftest.py +++ b/tests/components/pyload/conftest.py @@ -41,6 +41,19 @@ YAML_INPUT = { CONF_SSL: True, CONF_USERNAME: "test-username", } +REAUTH_INPUT = { + CONF_PASSWORD: "new-password", + CONF_USERNAME: "new-username", +} + +NEW_INPUT = { + CONF_HOST: "pyload.local", + CONF_PASSWORD: "new-password", + CONF_PORT: 8000, + CONF_SSL: True, + CONF_USERNAME: "new-username", + CONF_VERIFY_SSL: False, +} @pytest.fixture diff --git a/tests/components/pyload/test_config_flow.py b/tests/components/pyload/test_config_flow.py index 70d324fd980..63297de7127 100644 --- a/tests/components/pyload/test_config_flow.py +++ b/tests/components/pyload/test_config_flow.py @@ -6,11 +6,11 @@ from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError import pytest from homeassistant.components.pyload.const import DEFAULT_NAME, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .conftest import USER_INPUT, YAML_INPUT +from .conftest import NEW_INPUT, REAUTH_INPUT, USER_INPUT, YAML_INPUT from tests.common import MockConfigEntry @@ -164,3 +164,89 @@ async def test_flow_import_errors( assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason + + +async def test_reauth( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, +) -> None: + """Test reauth flow.""" + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + REAUTH_INPUT, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert config_entry.data == NEW_INPUT + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("side_effect", "error_text"), + [ + (InvalidAuth, "invalid_auth"), + (CannotConnect, "cannot_connect"), + (IndexError, "unknown"), + ], +) +async def test_reauth_errors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + side_effect: Exception, + error_text: str, +) -> None: + """Test reauth flow.""" + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_pyloadapi.login.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + REAUTH_INPUT, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error_text} + + mock_pyloadapi.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + REAUTH_INPUT, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert config_entry.data == NEW_INPUT + assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/pyload/test_init.py b/tests/components/pyload/test_init.py index a1ecf294523..12713ef2e54 100644 --- a/tests/components/pyload/test_init.py +++ b/tests/components/pyload/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError import pytest -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -61,3 +61,5 @@ async def test_config_entry_setup_invalid_auth( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.SETUP_ERROR + + assert any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) From 2386ed383002facb46424e6dc88ee20a9f83ab9e Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Tue, 25 Jun 2024 18:43:26 +0300 Subject: [PATCH 0135/2411] Add script llm tool (#118936) * Add script llm tool * Add tests * More tests * more test * more test * Add area and floor resolving * coverage * coverage * fix ColorTempSelector * fix mypy * fix mypy * add script reload test * Cache script tool parameters * Make custom_serializer a part of api --------- Co-authored-by: Michael Hansen --- .../conversation.py | 13 +- .../manifest.json | 2 +- .../openai_conversation/conversation.py | 16 +- .../openai_conversation/manifest.json | 2 +- homeassistant/helpers/intent.py | 10 +- homeassistant/helpers/llm.py | 284 ++++++++++++++- homeassistant/helpers/selector.py | 27 +- homeassistant/package_constraints.txt | 1 + pyproject.toml | 1 + requirements.txt | 1 + requirements_all.txt | 4 - requirements_test_all.txt | 4 - tests/helpers/test_llm.py | 327 +++++++++++++++++- tests/helpers/test_selector.py | 2 + 14 files changed, 639 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 2cfbc09ed08..fb7f5c3b21c 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -3,6 +3,7 @@ from __future__ import annotations import codecs +from collections.abc import Callable from typing import Any, Literal from google.api_core.exceptions import GoogleAPICallError @@ -89,10 +90,14 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: return result -def _format_tool(tool: llm.Tool) -> dict[str, Any]: +def _format_tool( + tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None +) -> dict[str, Any]: """Format tool specification.""" - parameters = _format_schema(convert(tool.parameters)) + parameters = _format_schema( + convert(tool.parameters, custom_serializer=custom_serializer) + ) return protos.Tool( { @@ -193,7 +198,9 @@ class GoogleGenerativeAIConversationEntity( f"Error preparing LLM API: {err}", ) return result - tools = [_format_tool(tool) for tool in llm_api.tools] + tools = [ + _format_tool(tool, llm_api.custom_serializer) for tool in llm_api.tools + ] try: prompt = await self._async_render_prompt(user_input, llm_api, llm_context) diff --git a/homeassistant/components/google_generative_ai_conversation/manifest.json b/homeassistant/components/google_generative_ai_conversation/manifest.json index 168fee105a0..9e0dc1ddeab 100644 --- a/homeassistant/components/google_generative_ai_conversation/manifest.json +++ b/homeassistant/components/google_generative_ai_conversation/manifest.json @@ -9,5 +9,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["google-generativeai==0.6.0", "voluptuous-openapi==0.0.4"] + "requirements": ["google-generativeai==0.6.0"] } diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 40242f5c6cc..46be803bcad 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -1,7 +1,8 @@ """Conversation support for OpenAI.""" +from collections.abc import Callable import json -from typing import Literal +from typing import Any, Literal import openai from openai._types import NOT_GIVEN @@ -58,9 +59,14 @@ async def async_setup_entry( async_add_entities([agent]) -def _format_tool(tool: llm.Tool) -> ChatCompletionToolParam: +def _format_tool( + tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None +) -> ChatCompletionToolParam: """Format tool specification.""" - tool_spec = FunctionDefinition(name=tool.name, parameters=convert(tool.parameters)) + tool_spec = FunctionDefinition( + name=tool.name, + parameters=convert(tool.parameters, custom_serializer=custom_serializer), + ) if tool.description: tool_spec["description"] = tool.description return ChatCompletionToolParam(type="function", function=tool_spec) @@ -139,7 +145,9 @@ class OpenAIConversationEntity( return conversation.ConversationResult( response=intent_response, conversation_id=user_input.conversation_id ) - tools = [_format_tool(tool) for tool in llm_api.tools] + tools = [ + _format_tool(tool, llm_api.custom_serializer) for tool in llm_api.tools + ] if user_input.conversation_id is None: conversation_id = ulid.ulid_now() diff --git a/homeassistant/components/openai_conversation/manifest.json b/homeassistant/components/openai_conversation/manifest.json index 480712574c4..0c06a3d4cd8 100644 --- a/homeassistant/components/openai_conversation/manifest.json +++ b/homeassistant/components/openai_conversation/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/openai_conversation", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["openai==1.3.8", "voluptuous-openapi==0.0.4"] + "requirements": ["openai==1.3.8"] } diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index b1ddf5eacc7..502b20eaf8f 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -352,7 +352,7 @@ class MatchTargetsCandidate: matched_name: str | None = None -def _find_areas( +def find_areas( name: str, areas: area_registry.AreaRegistry ) -> Iterable[area_registry.AreaEntry]: """Find all areas matching a name (including aliases).""" @@ -372,7 +372,7 @@ def _find_areas( break -def _find_floors( +def find_floors( name: str, floors: floor_registry.FloorRegistry ) -> Iterable[floor_registry.FloorEntry]: """Find all floors matching a name (including aliases).""" @@ -530,7 +530,7 @@ def async_match_targets( # noqa: C901 if not states: return MatchTargetsResult(False, MatchFailedReason.STATE) - # Exit early so we can to avoid registry lookups + # Exit early so we can avoid registry lookups if not ( constraints.name or constraints.features @@ -580,7 +580,7 @@ def async_match_targets( # noqa: C901 if constraints.floor_name: # Filter by areas associated with floor fr = floor_registry.async_get(hass) - targeted_floors = list(_find_floors(constraints.floor_name, fr)) + targeted_floors = list(find_floors(constraints.floor_name, fr)) if not targeted_floors: return MatchTargetsResult( False, @@ -609,7 +609,7 @@ def async_match_targets( # noqa: C901 possible_area_ids = {area.id for area in ar.async_list_areas()} if constraints.area_name: - targeted_areas = list(_find_areas(constraints.area_name, ar)) + targeted_areas = list(find_areas(constraints.area_name, ar)) if not targeted_areas: return MatchTargetsResult( False, diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index a4e18fdb2c0..480b9cb5237 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from collections.abc import Callable from dataclasses import dataclass from decimal import Decimal from enum import Enum @@ -11,6 +12,7 @@ from typing import Any import slugify as unicode_slug import voluptuous as vol +from voluptuous_openapi import UNSUPPORTED, convert from homeassistant.components.climate.intent import INTENT_GET_TEMPERATURE from homeassistant.components.conversation.trace import ( @@ -20,22 +22,39 @@ from homeassistant.components.conversation.trace import ( from homeassistant.components.cover.intent import INTENT_CLOSE_COVER, INTENT_OPEN_COVER from homeassistant.components.homeassistant.exposed_entities import async_should_expose from homeassistant.components.intent import async_device_supports_timers +from homeassistant.components.script import ATTR_VARIABLES, DOMAIN as SCRIPT_DOMAIN from homeassistant.components.weather.intent import INTENT_GET_WEATHER -from homeassistant.core import Context, HomeAssistant, callback +from homeassistant.const import ( + ATTR_DOMAIN, + ATTR_ENTITY_ID, + ATTR_SERVICE, + EVENT_HOMEASSISTANT_CLOSE, + EVENT_SERVICE_REMOVED, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, Event, HomeAssistant, callback, split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.util import yaml +from homeassistant.util.hass_dict import HassKey from homeassistant.util.json import JsonObjectType from . import ( area_registry as ar, + config_validation as cv, device_registry as dr, entity_registry as er, floor_registry as fr, intent, + selector, service, ) from .singleton import singleton +SCRIPT_PARAMETERS_CACHE: HassKey[dict[str, tuple[str | None, vol.Schema]]] = HassKey( + "llm_script_parameters_cache" +) + + LLM_API_ASSIST = "assist" BASE_PROMPT = ( @@ -143,6 +162,7 @@ class APIInstance: api_prompt: str llm_context: LLMContext tools: list[Tool] + custom_serializer: Callable[[Any], Any] | None = None async def async_call_tool(self, tool_input: ToolInput) -> JsonObjectType: """Call a LLM tool, validate args and return the response.""" @@ -284,6 +304,7 @@ class AssistAPI(API): api_prompt=self._async_get_api_prompt(llm_context, exposed_entities), llm_context=llm_context, tools=self._async_get_tools(llm_context, exposed_entities), + custom_serializer=_selector_serializer, ) @callback @@ -372,7 +393,7 @@ class AssistAPI(API): exposed_domains: set[str] | None = None if exposed_entities is not None: exposed_domains = { - entity_id.split(".")[0] for entity_id in exposed_entities + split_entity_id(entity_id)[0] for entity_id in exposed_entities } intent_handlers = [ intent_handler @@ -381,11 +402,22 @@ class AssistAPI(API): or intent_handler.platforms & exposed_domains ] - return [ + tools: list[Tool] = [ IntentTool(self.cached_slugify(intent_handler.intent_type), intent_handler) for intent_handler in intent_handlers ] + if llm_context.assistant is not None: + for state in self.hass.states.async_all(SCRIPT_DOMAIN): + if not async_should_expose( + self.hass, llm_context.assistant, state.entity_id + ): + continue + + tools.append(ScriptTool(self.hass, state.entity_id)) + + return tools + def _get_exposed_entities( hass: HomeAssistant, assistant: str @@ -413,13 +445,15 @@ def _get_exposed_entities( entities = {} for state in hass.states.async_all(): + if state.domain == SCRIPT_DOMAIN: + continue + if not async_should_expose(hass, assistant, state.entity_id): continue entity_entry = entity_registry.async_get(state.entity_id) names = [state.name] area_names = [] - description: str | None = None if entity_entry is not None: names.extend(entity_entry.aliases) @@ -439,25 +473,11 @@ def _get_exposed_entities( area_names.append(area.name) area_names.extend(area.aliases) - if ( - state.domain == "script" - and entity_entry.unique_id - and ( - service_desc := service.async_get_cached_service_description( - hass, "script", entity_entry.unique_id - ) - ) - ): - description = service_desc.get("description") - info: dict[str, Any] = { "names": ", ".join(names), "state": state.state, } - if description: - info["description"] = description - if area_names: info["areas"] = ", ".join(area_names) @@ -473,3 +493,231 @@ def _get_exposed_entities( entities[state.entity_id] = info return entities + + +def _selector_serializer(schema: Any) -> Any: # noqa: C901 + """Convert selectors into OpenAPI schema.""" + if not isinstance(schema, selector.Selector): + return UNSUPPORTED + + if isinstance(schema, selector.BackupLocationSelector): + return {"type": "string", "pattern": "^(?:\\/backup|\\w+)$"} + + if isinstance(schema, selector.BooleanSelector): + return {"type": "boolean"} + + if isinstance(schema, selector.ColorRGBSelector): + return { + "type": "array", + "items": {"type": "number"}, + "minItems": 3, + "maxItems": 3, + "format": "RGB", + } + + if isinstance(schema, selector.ConditionSelector): + return convert(cv.CONDITIONS_SCHEMA) + + if isinstance(schema, selector.ConstantSelector): + return {"enum": [schema.config["value"]]} + + result: dict[str, Any] + if isinstance(schema, selector.ColorTempSelector): + result = {"type": "number"} + if "min" in schema.config: + result["minimum"] = schema.config["min"] + elif "min_mireds" in schema.config: + result["minimum"] = schema.config["min_mireds"] + if "max" in schema.config: + result["maximum"] = schema.config["max"] + elif "max_mireds" in schema.config: + result["maximum"] = schema.config["max_mireds"] + return result + + if isinstance(schema, selector.CountrySelector): + if schema.config.get("countries"): + return {"type": "string", "enum": schema.config["countries"]} + return {"type": "string", "format": "ISO 3166-1 alpha-2"} + + if isinstance(schema, selector.DateSelector): + return {"type": "string", "format": "date"} + + if isinstance(schema, selector.DateTimeSelector): + return {"type": "string", "format": "date-time"} + + if isinstance(schema, selector.DurationSelector): + return convert(cv.time_period_dict) + + if isinstance(schema, selector.EntitySelector): + if schema.config.get("multiple"): + return {"type": "array", "items": {"type": "string", "format": "entity_id"}} + + return {"type": "string", "format": "entity_id"} + + if isinstance(schema, selector.LanguageSelector): + if schema.config.get("languages"): + return {"type": "string", "enum": schema.config["languages"]} + return {"type": "string", "format": "RFC 5646"} + + if isinstance(schema, (selector.LocationSelector, selector.MediaSelector)): + return convert(schema.DATA_SCHEMA) + + if isinstance(schema, selector.NumberSelector): + result = {"type": "number"} + if "min" in schema.config: + result["minimum"] = schema.config["min"] + if "max" in schema.config: + result["maximum"] = schema.config["max"] + return result + + if isinstance(schema, selector.ObjectSelector): + return {"type": "object"} + + if isinstance(schema, selector.SelectSelector): + options = [ + x["value"] if isinstance(x, dict) else x for x in schema.config["options"] + ] + if schema.config.get("multiple"): + return { + "type": "array", + "items": {"type": "string", "enum": options}, + "uniqueItems": True, + } + return {"type": "string", "enum": options} + + if isinstance(schema, selector.TargetSelector): + return convert(cv.TARGET_SERVICE_FIELDS) + + if isinstance(schema, selector.TemplateSelector): + return {"type": "string", "format": "jinja2"} + + if isinstance(schema, selector.TimeSelector): + return {"type": "string", "format": "time"} + + if isinstance(schema, selector.TriggerSelector): + return convert(cv.TRIGGER_SCHEMA) + + if schema.config.get("multiple"): + return {"type": "array", "items": {"type": "string"}} + + return {"type": "string"} + + +class ScriptTool(Tool): + """LLM Tool representing a Script.""" + + def __init__( + self, + hass: HomeAssistant, + script_entity_id: str, + ) -> None: + """Init the class.""" + entity_registry = er.async_get(hass) + + self.name = split_entity_id(script_entity_id)[1] + self.parameters = vol.Schema({}) + entity_entry = entity_registry.async_get(script_entity_id) + if entity_entry and entity_entry.unique_id: + parameters_cache = hass.data.get(SCRIPT_PARAMETERS_CACHE) + + if parameters_cache is None: + parameters_cache = hass.data[SCRIPT_PARAMETERS_CACHE] = {} + + @callback + def clear_cache(event: Event) -> None: + """Clear script parameter cache on script reload or delete.""" + if ( + event.data[ATTR_DOMAIN] == SCRIPT_DOMAIN + and event.data[ATTR_SERVICE] in parameters_cache + ): + parameters_cache.pop(event.data[ATTR_SERVICE]) + + cancel = hass.bus.async_listen(EVENT_SERVICE_REMOVED, clear_cache) + + @callback + def on_homeassistant_close(event: Event) -> None: + """Cleanup.""" + cancel() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_CLOSE, on_homeassistant_close + ) + + if entity_entry.unique_id in parameters_cache: + self.description, self.parameters = parameters_cache[ + entity_entry.unique_id + ] + return + + if service_desc := service.async_get_cached_service_description( + hass, SCRIPT_DOMAIN, entity_entry.unique_id + ): + self.description = service_desc.get("description") + schema: dict[vol.Marker, Any] = {} + fields = service_desc.get("fields", {}) + + for field, config in fields.items(): + description = config.get("description") + if not description: + description = config.get("name") + if config.get("required"): + key = vol.Required(field, description=description) + else: + key = vol.Optional(field, description=description) + if "selector" in config: + schema[key] = selector.selector(config["selector"]) + else: + schema[key] = cv.string + + self.parameters = vol.Schema(schema) + + parameters_cache[entity_entry.unique_id] = ( + self.description, + self.parameters, + ) + + async def async_call( + self, hass: HomeAssistant, tool_input: ToolInput, llm_context: LLMContext + ) -> JsonObjectType: + """Run the script.""" + + for field, validator in self.parameters.schema.items(): + if field not in tool_input.tool_args: + continue + if isinstance(validator, selector.AreaSelector): + area_reg = ar.async_get(hass) + if validator.config.get("multiple"): + areas: list[ar.AreaEntry] = [] + for area in tool_input.tool_args[field]: + areas.extend(intent.find_areas(area, area_reg)) + tool_input.tool_args[field] = list({area.id for area in areas}) + else: + area = tool_input.tool_args[field] + area = list(intent.find_areas(area, area_reg))[0].id + tool_input.tool_args[field] = area + + elif isinstance(validator, selector.FloorSelector): + floor_reg = fr.async_get(hass) + if validator.config.get("multiple"): + floors: list[fr.FloorEntry] = [] + for floor in tool_input.tool_args[field]: + floors.extend(intent.find_floors(floor, floor_reg)) + tool_input.tool_args[field] = list( + {floor.floor_id for floor in floors} + ) + else: + floor = tool_input.tool_args[field] + floor = list(intent.find_floors(floor, floor_reg))[0].floor_id + tool_input.tool_args[field] = floor + + await hass.services.async_call( + SCRIPT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: SCRIPT_DOMAIN + "." + self.name, + ATTR_VARIABLES: tool_input.tool_args, + }, + context=llm_context.context, + ) + + return {"success": True} diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 1db4dd9f80b..16aaa40db86 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -75,6 +75,13 @@ class Selector[_T: Mapping[str, Any]]: self.config = self.CONFIG_SCHEMA(config) + def __eq__(self, other: object) -> bool: + """Check equality.""" + if not isinstance(other, Selector): + return NotImplemented + + return self.selector_type == other.selector_type and self.config == other.config + def serialize(self) -> dict[str, dict[str, _T]]: """Serialize Selector for voluptuous_serialize.""" return {"selector": {self.selector_type: self.config}} @@ -278,7 +285,7 @@ class AssistPipelineSelector(Selector[AssistPipelineSelectorConfig]): CONFIG_SCHEMA = vol.Schema({}) - def __init__(self, config: AssistPipelineSelectorConfig) -> None: + def __init__(self, config: AssistPipelineSelectorConfig | None = None) -> None: """Instantiate a selector.""" super().__init__(config) @@ -430,10 +437,10 @@ class ColorTempSelector(Selector[ColorTempSelectorConfig]): range_min = self.config.get("min") range_max = self.config.get("max") - if not range_min: + if range_min is None: range_min = self.config.get("min_mireds") - if not range_max: + if range_max is None: range_max = self.config.get("max_mireds") value: int = vol.All( @@ -517,7 +524,7 @@ class ConstantSelector(Selector[ConstantSelectorConfig]): } ) - def __init__(self, config: ConstantSelectorConfig | None = None) -> None: + def __init__(self, config: ConstantSelectorConfig) -> None: """Instantiate a selector.""" super().__init__(config) @@ -560,7 +567,7 @@ class QrCodeSelector(Selector[QrCodeSelectorConfig]): } ) - def __init__(self, config: QrCodeSelectorConfig | None = None) -> None: + def __init__(self, config: QrCodeSelectorConfig) -> None: """Instantiate a selector.""" super().__init__(config) @@ -588,7 +595,7 @@ class ConversationAgentSelector(Selector[ConversationAgentSelectorConfig]): } ) - def __init__(self, config: ConversationAgentSelectorConfig) -> None: + def __init__(self, config: ConversationAgentSelectorConfig | None = None) -> None: """Instantiate a selector.""" super().__init__(config) @@ -820,7 +827,7 @@ class FloorSelectorConfig(TypedDict, total=False): @SELECTORS.register("floor") -class FloorSelector(Selector[AreaSelectorConfig]): +class FloorSelector(Selector[FloorSelectorConfig]): """Selector of a single or list of floors.""" selector_type = "floor" @@ -934,7 +941,7 @@ class LanguageSelector(Selector[LanguageSelectorConfig]): } ) - def __init__(self, config: LanguageSelectorConfig) -> None: + def __init__(self, config: LanguageSelectorConfig | None = None) -> None: """Instantiate a selector.""" super().__init__(config) @@ -1159,7 +1166,7 @@ class SelectSelector(Selector[SelectSelectorConfig]): } ) - def __init__(self, config: SelectSelectorConfig | None = None) -> None: + def __init__(self, config: SelectSelectorConfig) -> None: """Instantiate a selector.""" super().__init__(config) @@ -1434,7 +1441,7 @@ class FileSelector(Selector[FileSelectorConfig]): } ) - def __init__(self, config: FileSelectorConfig | None = None) -> None: + def __init__(self, config: FileSelectorConfig) -> None: """Instantiate a selector.""" super().__init__(config) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f3be7c5515e..25d10874239 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -58,6 +58,7 @@ SQLAlchemy==2.0.31 typing-extensions>=4.12.2,<5.0 ulid-transform==0.9.0 urllib3>=1.26.5,<2 +voluptuous-openapi==0.0.4 voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 diff --git a/pyproject.toml b/pyproject.toml index d7fbe67edba..6ecbb8b51d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,7 @@ dependencies = [ "urllib3>=1.26.5,<2", "voluptuous==0.13.1", "voluptuous-serialize==2.6.0", + "voluptuous-openapi==0.0.4", "yarl==1.9.4", ] diff --git a/requirements.txt b/requirements.txt index cff85c2478f..5b1c57c7e1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,4 +41,5 @@ ulid-transform==0.9.0 urllib3>=1.26.5,<2 voluptuous==0.13.1 voluptuous-serialize==2.6.0 +voluptuous-openapi==0.0.4 yarl==1.9.4 diff --git a/requirements_all.txt b/requirements_all.txt index 48eae313cf4..14c4ed00a0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2846,10 +2846,6 @@ voip-utils==0.1.0 # homeassistant.components.volkszaehler volkszaehler==0.4.0 -# homeassistant.components.google_generative_ai_conversation -# homeassistant.components.openai_conversation -voluptuous-openapi==0.0.4 - # homeassistant.components.volvooncall volvooncall==0.10.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b3adcf409a..0e70472b67b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2217,10 +2217,6 @@ vilfo-api-client==0.5.0 # homeassistant.components.voip voip-utils==0.1.0 -# homeassistant.components.google_generative_ai_conversation -# homeassistant.components.openai_conversation -voluptuous-openapi==0.0.4 - # homeassistant.components.volvooncall volvooncall==0.10.3 diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 5389490b401..872297b09ec 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.components.intent import async_register_timer_handler +from homeassistant.components.script.config import ScriptConfig from homeassistant.core import Context, HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( @@ -18,6 +19,7 @@ from homeassistant.helpers import ( floor_registry as fr, intent, llm, + selector, ) from homeassistant.setup import async_setup_component from homeassistant.util import yaml @@ -564,11 +566,6 @@ async def test_assist_api_prompt( "names": "Unnamed Device", "state": "unavailable", }, - "script.test_script": { - "description": "This is a test script", - "names": "test_script", - "state": "off", - }, } exposed_entities_prompt = ( "An overview of the areas and the devices in this smart home:\n" @@ -634,3 +631,323 @@ async def test_assist_api_prompt( {area_prompt} {exposed_entities_prompt}""" ) + + +async def test_script_tool( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + floor_registry: fr.FloorRegistry, +) -> None: + """Test ScriptTool for the assist API.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "intent", {}) + context = Context() + llm_context = llm.LLMContext( + platform="test_platform", + context=context, + user_prompt="test_text", + language="*", + assistant="conversation", + device_id=None, + ) + + # Create a script with a unique ID + assert await async_setup_component( + hass, + "script", + { + "script": { + "test_script": { + "description": "This is a test script", + "sequence": [], + "fields": { + "beer": {"description": "Number of beers", "required": True}, + "wine": {"selector": {"number": {"min": 0, "max": 3}}}, + "where": {"selector": {"area": {}}}, + "area_list": {"selector": {"area": {"multiple": True}}}, + "floor": {"selector": {"floor": {}}}, + "floor_list": {"selector": {"floor": {"multiple": True}}}, + "extra_field": {"selector": {"area": {}}}, + }, + }, + "unexposed_script": { + "sequence": [], + }, + } + }, + ) + async_expose_entity(hass, "conversation", "script.test_script", True) + + area = area_registry.async_create("Living room") + floor = floor_registry.async_create("2") + + assert llm.SCRIPT_PARAMETERS_CACHE not in hass.data + + api = await llm.async_get_api(hass, "assist", llm_context) + + tools = [tool for tool in api.tools if isinstance(tool, llm.ScriptTool)] + assert len(tools) == 1 + + tool = tools[0] + assert tool.name == "test_script" + assert tool.description == "This is a test script" + schema = { + vol.Required("beer", description="Number of beers"): cv.string, + vol.Optional("wine"): selector.NumberSelector({"min": 0, "max": 3}), + vol.Optional("where"): selector.AreaSelector(), + vol.Optional("area_list"): selector.AreaSelector({"multiple": True}), + vol.Optional("floor"): selector.FloorSelector(), + vol.Optional("floor_list"): selector.FloorSelector({"multiple": True}), + vol.Optional("extra_field"): selector.AreaSelector(), + } + assert tool.parameters.schema == schema + + assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == { + "test_script": ("This is a test script", vol.Schema(schema)) + } + + tool_input = llm.ToolInput( + tool_name="test_script", + tool_args={ + "beer": "3", + "wine": 0, + "where": "Living room", + "area_list": ["Living room"], + "floor": "2", + "floor_list": ["2"], + }, + ) + + with patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: + response = await api.async_call_tool(tool_input) + + mock_service_call.assert_awaited_once_with( + "script", + "turn_on", + { + "entity_id": "script.test_script", + "variables": { + "beer": "3", + "wine": 0, + "where": area.id, + "area_list": [area.id], + "floor": floor.floor_id, + "floor_list": [floor.floor_id], + }, + }, + context=context, + ) + assert response == {"success": True} + + # Test reload script with new parameters + config = { + "script": { + "test_script": ScriptConfig( + { + "description": "This is a new test script", + "sequence": [], + "mode": "single", + "max": 2, + "max_exceeded": "WARNING", + "trace": {}, + "fields": { + "beer": {"description": "Number of beers", "required": True}, + }, + } + ) + } + } + + with patch( + "homeassistant.helpers.entity_component.EntityComponent.async_prepare_reload", + return_value=config, + ): + await hass.services.async_call("script", "reload", blocking=True) + + assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == {} + + api = await llm.async_get_api(hass, "assist", llm_context) + + tools = [tool for tool in api.tools if isinstance(tool, llm.ScriptTool)] + assert len(tools) == 1 + + tool = tools[0] + assert tool.name == "test_script" + assert tool.description == "This is a new test script" + schema = {vol.Required("beer", description="Number of beers"): cv.string} + assert tool.parameters.schema == schema + + assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == { + "test_script": ("This is a new test script", vol.Schema(schema)) + } + + +async def test_selector_serializer( + hass: HomeAssistant, llm_context: llm.LLMContext +) -> None: + """Test serialization of Selectors in Open API format.""" + api = await llm.async_get_api(hass, "assist", llm_context) + selector_serializer = api.custom_serializer + + assert selector_serializer(selector.ActionSelector()) == {"type": "string"} + assert selector_serializer(selector.AddonSelector()) == {"type": "string"} + assert selector_serializer(selector.AreaSelector()) == {"type": "string"} + assert selector_serializer(selector.AreaSelector({"multiple": True})) == { + "type": "array", + "items": {"type": "string"}, + } + assert selector_serializer(selector.AssistPipelineSelector()) == {"type": "string"} + assert selector_serializer( + selector.AttributeSelector({"entity_id": "sensor.test"}) + ) == {"type": "string"} + assert selector_serializer(selector.BackupLocationSelector()) == { + "type": "string", + "pattern": "^(?:\\/backup|\\w+)$", + } + assert selector_serializer(selector.BooleanSelector()) == {"type": "boolean"} + assert selector_serializer(selector.ColorRGBSelector()) == { + "type": "array", + "items": {"type": "number"}, + "maxItems": 3, + "minItems": 3, + "format": "RGB", + } + assert selector_serializer(selector.ColorTempSelector()) == {"type": "number"} + assert selector_serializer(selector.ColorTempSelector({"min": 0, "max": 1000})) == { + "type": "number", + "minimum": 0, + "maximum": 1000, + } + assert selector_serializer( + selector.ColorTempSelector({"min_mireds": 100, "max_mireds": 1000}) + ) == {"type": "number", "minimum": 100, "maximum": 1000} + assert selector_serializer(selector.ConfigEntrySelector()) == {"type": "string"} + assert selector_serializer(selector.ConstantSelector({"value": "test"})) == { + "enum": ["test"] + } + assert selector_serializer(selector.ConstantSelector({"value": 1})) == {"enum": [1]} + assert selector_serializer(selector.ConstantSelector({"value": True})) == { + "enum": [True] + } + assert selector_serializer(selector.QrCodeSelector({"data": "test"})) == { + "type": "string" + } + assert selector_serializer(selector.ConversationAgentSelector()) == { + "type": "string" + } + assert selector_serializer(selector.CountrySelector()) == { + "type": "string", + "format": "ISO 3166-1 alpha-2", + } + assert selector_serializer( + selector.CountrySelector({"countries": ["GB", "FR"]}) + ) == {"type": "string", "enum": ["GB", "FR"]} + assert selector_serializer(selector.DateSelector()) == { + "type": "string", + "format": "date", + } + assert selector_serializer(selector.DateTimeSelector()) == { + "type": "string", + "format": "date-time", + } + assert selector_serializer(selector.DeviceSelector()) == {"type": "string"} + assert selector_serializer(selector.DeviceSelector({"multiple": True})) == { + "type": "array", + "items": {"type": "string"}, + } + assert selector_serializer(selector.EntitySelector()) == { + "type": "string", + "format": "entity_id", + } + assert selector_serializer(selector.EntitySelector({"multiple": True})) == { + "type": "array", + "items": {"type": "string", "format": "entity_id"}, + } + assert selector_serializer(selector.FloorSelector()) == {"type": "string"} + assert selector_serializer(selector.FloorSelector({"multiple": True})) == { + "type": "array", + "items": {"type": "string"}, + } + assert selector_serializer(selector.IconSelector()) == {"type": "string"} + assert selector_serializer(selector.LabelSelector()) == {"type": "string"} + assert selector_serializer(selector.LabelSelector({"multiple": True})) == { + "type": "array", + "items": {"type": "string"}, + } + assert selector_serializer(selector.LanguageSelector()) == { + "type": "string", + "format": "RFC 5646", + } + assert selector_serializer( + selector.LanguageSelector({"languages": ["en", "fr"]}) + ) == {"type": "string", "enum": ["en", "fr"]} + assert selector_serializer(selector.LocationSelector()) == { + "type": "object", + "properties": { + "latitude": {"type": "number"}, + "longitude": {"type": "number"}, + "radius": {"type": "number"}, + }, + "required": ["latitude", "longitude"], + } + assert selector_serializer(selector.MediaSelector()) == { + "type": "object", + "properties": { + "entity_id": {"type": "string"}, + "media_content_id": {"type": "string"}, + "media_content_type": {"type": "string"}, + "metadata": {"type": "object", "additionalProperties": True}, + }, + "required": ["entity_id", "media_content_id", "media_content_type"], + } + assert selector_serializer(selector.NumberSelector({"mode": "box"})) == { + "type": "number" + } + assert selector_serializer(selector.NumberSelector({"min": 30, "max": 100})) == { + "type": "number", + "minimum": 30, + "maximum": 100, + } + assert selector_serializer(selector.ObjectSelector()) == {"type": "object"} + assert selector_serializer( + selector.SelectSelector( + { + "options": [ + {"value": "A", "label": "Letter A"}, + {"value": "B", "label": "Letter B"}, + {"value": "C", "label": "Letter C"}, + ] + } + ) + ) == {"type": "string", "enum": ["A", "B", "C"]} + assert selector_serializer( + selector.SelectSelector({"options": ["A", "B", "C"], "multiple": True}) + ) == { + "type": "array", + "items": {"type": "string", "enum": ["A", "B", "C"]}, + "uniqueItems": True, + } + assert selector_serializer( + selector.StateSelector({"entity_id": "sensor.test"}) + ) == {"type": "string"} + assert selector_serializer(selector.TemplateSelector()) == { + "type": "string", + "format": "jinja2", + } + assert selector_serializer(selector.TextSelector()) == {"type": "string"} + assert selector_serializer(selector.TextSelector({"multiple": True})) == { + "type": "array", + "items": {"type": "string"}, + } + assert selector_serializer(selector.ThemeSelector()) == {"type": "string"} + assert selector_serializer(selector.TimeSelector()) == { + "type": "string", + "format": "time", + } + assert selector_serializer(selector.TriggerSelector()) == { + "type": "array", + "items": {"type": "string"}, + } + assert selector_serializer(selector.FileSelector({"accept": ".txt"})) == { + "type": "string" + } diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 6db313baa24..e93ec3b8c22 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -55,6 +55,8 @@ def _test_selector( config = {selector_type: schema} selector.validate_selector(config) selector_instance = selector.selector(config) + assert selector_instance == selector.selector(config) + assert selector_instance != 5 # We do not allow enums in the config, as they cannot serialize assert not any(isinstance(val, Enum) for val in selector_instance.config.values()) From 09e8f7e9bba68614a4bc74974867da7fdd63bb6d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:44:03 +0200 Subject: [PATCH 0136/2411] Improve type hints in deconz tests (#120388) --- tests/components/deconz/test_device_trigger.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 329cf0405db..54b735ba021 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -27,7 +27,7 @@ from homeassistant.const import ( CONF_TYPE, STATE_UNAVAILABLE, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.setup import async_setup_component @@ -44,7 +44,7 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @pytest.fixture -def automation_calls(hass): +def automation_calls(hass: HomeAssistant) -> list[ServiceCall]: """Track automation calls to a mock service.""" return async_mock_service(hass, "test", "automation") @@ -300,7 +300,7 @@ async def test_functional_device_trigger( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket, - automation_calls, + automation_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, ) -> None: """Test proper matching and attachment of device trigger automation.""" From e0b98551605ae3e1073068cd5cdebe4f3943b18d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 18:22:56 +0200 Subject: [PATCH 0137/2411] Bump uiprotect to 3.4.0 (#120433) --- 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 ba8e6f89dd5..7a1556387a8 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==3.3.1", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==3.4.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 14c4ed00a0a..7c6f39ed88b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2791,7 +2791,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.3.1 +uiprotect==3.4.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e70472b67b..9bc9a740278 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2171,7 +2171,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.3.1 +uiprotect==3.4.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From f017134199fbfa13ceb380cacfb67bbf37ed2952 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:53:06 +0200 Subject: [PATCH 0138/2411] Fix missing vol.Optional keyword (#120444) --- homeassistant/components/proxy/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 3a93c7a2d36..5cd72b05871 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -49,8 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean, - vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean, + vol.Optional(CONF_CACHE_IMAGES, default=False): cv.boolean, + vol.Optional(CONF_FORCE_RESIZE, default=False): cv.boolean, vol.Optional(CONF_MODE, default=MODE_RESIZE): vol.In([MODE_RESIZE, MODE_CROP]), vol.Optional(CONF_IMAGE_QUALITY): int, vol.Optional(CONF_IMAGE_REFRESH_RATE): float, From 197062139e867030a6a251116f96e99014f5fc63 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:54:06 +0200 Subject: [PATCH 0139/2411] Fix schema typing (1) (#120443) --- homeassistant/components/forked_daapd/config_flow.py | 2 +- homeassistant/components/linear_garage_door/config_flow.py | 4 +--- homeassistant/components/motion_blinds/config_flow.py | 2 +- homeassistant/components/synology_dsm/config_flow.py | 2 +- homeassistant/components/vizio/config_flow.py | 2 +- homeassistant/components/zha/config_flow.py | 4 ++-- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/forked_daapd/config_flow.py b/homeassistant/components/forked_daapd/config_flow.py index 2440fc82943..7edf25a2595 100644 --- a/homeassistant/components/forked_daapd/config_flow.py +++ b/homeassistant/components/forked_daapd/config_flow.py @@ -111,7 +111,7 @@ class ForkedDaapdFlowHandler(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize.""" - self.discovery_schema = None + self.discovery_schema: vol.Schema | None = None @staticmethod @callback diff --git a/homeassistant/components/linear_garage_door/config_flow.py b/homeassistant/components/linear_garage_door/config_flow.py index dca2780cfea..d1dda97c513 100644 --- a/homeassistant/components/linear_garage_door/config_flow.py +++ b/homeassistant/components/linear_garage_door/config_flow.py @@ -75,9 +75,7 @@ class LinearGarageDoorConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" - data_schema = STEP_USER_DATA_SCHEMA - - data_schema = vol.Schema(data_schema) + data_schema = vol.Schema(STEP_USER_DATA_SCHEMA) if user_input is None: return self.async_show_form(step_id="user", data_schema=data_schema) diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index c838825a4bd..131299314a2 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -75,7 +75,7 @@ class MotionBlindsFlowHandler(ConfigFlow, domain=DOMAIN): """Initialize the Motionblinds flow.""" self._host: str | None = None self._ips: list[str] = [] - self._config_settings = None + self._config_settings: vol.Schema | None = None @staticmethod @callback diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 6e2b090fc98..d019361edad 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -138,7 +138,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): user_input = {} description_placeholders = {} - data_schema = {} + data_schema = None if step_id == "link": user_input.update(self.discovered_conf) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index fb5f74f4e09..d8b99595f54 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -188,7 +188,7 @@ class VizioConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize config flow.""" - self._user_schema = None + self._user_schema: vol.Schema | None = None self._must_show_form: bool | None = None self._ch_type: str | None = None self._pairing_token: str | None = None diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 037ad4192bd..9be27f7b37c 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -221,7 +221,7 @@ class BaseZhaFlow(ConfigEntryBaseFlow): return await self.async_step_verify_radio() # Pre-select the currently configured port - default_port = vol.UNDEFINED + default_port: vol.Undefined | str = vol.UNDEFINED if self._radio_mgr.device_path is not None: for description, port in zip(list_of_ports, ports, strict=False): @@ -251,7 +251,7 @@ class BaseZhaFlow(ConfigEntryBaseFlow): return await self.async_step_manual_port_config() # Pre-select the current radio type - default = vol.UNDEFINED + default: vol.Undefined | str = vol.UNDEFINED if self._radio_mgr.radio_type is not None: default = self._radio_mgr.radio_type.description From b393024acd4d046b0acf5111433115cf15398890 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:57:15 +0200 Subject: [PATCH 0140/2411] Improve collection schema typing (#120441) --- .../components/application_credentials/__init__.py | 6 +++--- homeassistant/components/assist_pipeline/pipeline.py | 4 ++-- homeassistant/components/counter/__init__.py | 4 ++-- homeassistant/components/image_upload/__init__.py | 6 +++--- homeassistant/components/input_boolean/__init__.py | 4 ++-- homeassistant/components/input_button/__init__.py | 4 ++-- homeassistant/components/input_datetime/__init__.py | 4 ++-- homeassistant/components/input_number/__init__.py | 4 ++-- homeassistant/components/input_select/__init__.py | 4 ++-- homeassistant/components/input_text/__init__.py | 4 ++-- homeassistant/components/lovelace/const.py | 11 ++++++----- homeassistant/components/person/__init__.py | 6 +++--- homeassistant/components/schedule/__init__.py | 10 +++++----- homeassistant/components/tag/__init__.py | 10 +++++----- homeassistant/components/timer/__init__.py | 4 ++-- homeassistant/components/zone/__init__.py | 6 +++--- homeassistant/helpers/collection.py | 6 +++--- 17 files changed, 49 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/application_credentials/__init__.py b/homeassistant/components/application_credentials/__init__.py index aacd18fc795..22deb124859 100644 --- a/homeassistant/components/application_credentials/__init__.py +++ b/homeassistant/components/application_credentials/__init__.py @@ -29,7 +29,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, config_entry_oauth2_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.loader import ( IntegrationNotFound, async_get_application_credentials, @@ -49,14 +49,14 @@ DATA_STORAGE = "storage" CONF_AUTH_DOMAIN = "auth_domain" DEFAULT_IMPORT_NAME = "Import from configuration.yaml" -CREATE_FIELDS = { +CREATE_FIELDS: VolDictType = { vol.Required(CONF_DOMAIN): cv.string, vol.Required(CONF_CLIENT_ID): vol.All(cv.string, vol.Strip), vol.Required(CONF_CLIENT_SECRET): vol.All(cv.string, vol.Strip), vol.Optional(CONF_AUTH_DOMAIN): cv.string, vol.Optional(CONF_NAME): cv.string, } -UPDATE_FIELDS: dict = {} # Not supported +UPDATE_FIELDS: VolDictType = {} # Not supported CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 6c1b3ced470..56f88f60104 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -45,7 +45,7 @@ from homeassistant.helpers.collection import ( ) from homeassistant.helpers.singleton import singleton from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import UNDEFINED, UndefinedType +from homeassistant.helpers.typing import UNDEFINED, UndefinedType, VolDictType from homeassistant.util import ( dt as dt_util, language as language_util, @@ -94,7 +94,7 @@ def validate_language(data: dict[str, Any]) -> Any: return data -PIPELINE_FIELDS = { +PIPELINE_FIELDS: VolDictType = { vol.Required("conversation_engine"): str, vol.Required("conversation_language"): str, vol.Required("language"): str, diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 3d68d70e575..324668a63e2 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -21,7 +21,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType _LOGGER = logging.getLogger(__name__) @@ -49,7 +49,7 @@ SERVICE_SET_VALUE = "set_value" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.positive_int, vol.Required(CONF_NAME): vol.All(cv.string, vol.Length(min=1)), diff --git a/homeassistant/components/image_upload/__init__.py b/homeassistant/components/image_upload/__init__.py index 59b594561f0..8bb3aca3708 100644 --- a/homeassistant/components/image_upload/__init__.py +++ b/homeassistant/components/image_upload/__init__.py @@ -21,7 +21,7 @@ from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import collection, config_validation as cv from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType import homeassistant.util.dt as dt_util from .const import DOMAIN @@ -32,11 +32,11 @@ STORAGE_VERSION = 1 VALID_SIZES = {256, 512} MAX_SIZE = 1024 * 1024 * 10 -CREATE_FIELDS = { +CREATE_FIELDS: VolDictType = { vol.Required("file"): FileField, } -UPDATE_FIELDS = { +UPDATE_FIELDS: VolDictType = { vol.Optional("name"): vol.All(str, vol.Length(min=1)), } diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 91c7de96fe0..57165c5508a 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.loader import bind_hass DOMAIN = "input_boolean" @@ -35,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) CONF_INITIAL = "initial" -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_INITIAL): cv.boolean, vol.Optional(CONF_ICON): cv.icon, diff --git a/homeassistant/components/input_button/__init__.py b/homeassistant/components/input_button/__init__.py index d6c3644487b..e70bbacd933 100644 --- a/homeassistant/components/input_button/__init__.py +++ b/homeassistant/components/input_button/__init__.py @@ -22,13 +22,13 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType DOMAIN = "input_button" _LOGGER = logging.getLogger(__name__) -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_ICON): cv.icon, } diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 11aab52e6a4..5d2c1e7ff8d 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -24,7 +24,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -59,7 +59,7 @@ def validate_set_datetime_attrs(config): STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_HAS_DATE, default=False): cv.boolean, vol.Optional(CONF_HAS_TIME, default=False): cv.boolean, diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index e37f530b8af..f55ceabc6f0 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -25,7 +25,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType _LOGGER = logging.getLogger(__name__) @@ -64,7 +64,7 @@ def _cv_input_number(cfg): return cfg -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Required(CONF_MIN): vol.Coerce(float), vol.Required(CONF_MAX): vol.Coerce(float), diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 2741c9e21bc..44d2df02a92 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -33,7 +33,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType _LOGGER = logging.getLogger(__name__) @@ -55,7 +55,7 @@ def _unique(options: Any) -> Any: raise HomeAssistantError("Duplicate options are not allowed") from exc -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Required(CONF_OPTIONS): vol.All( cv.ensure_list, vol.Length(min=1), _unique, [cv.string] diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 55b43ee8a1e..3d75ff9f5c2 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -24,7 +24,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType _LOGGER = logging.getLogger(__name__) @@ -50,7 +50,7 @@ SERVICE_SET_VALUE = "set_value" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int), vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int), diff --git a/homeassistant/components/lovelace/const.py b/homeassistant/components/lovelace/const.py index 538bd49d72c..86f47fe2b5c 100644 --- a/homeassistant/components/lovelace/const.py +++ b/homeassistant/components/lovelace/const.py @@ -13,6 +13,7 @@ from homeassistant.const import ( ) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import VolDictType from homeassistant.util import slugify DOMAIN = "lovelace" @@ -37,12 +38,12 @@ RESOURCE_FIELDS = { RESOURCE_SCHEMA = vol.Schema(RESOURCE_FIELDS) -RESOURCE_CREATE_FIELDS = { +RESOURCE_CREATE_FIELDS: VolDictType = { vol.Required(CONF_RESOURCE_TYPE_WS): vol.In(RESOURCE_TYPES), vol.Required(CONF_URL): cv.string, } -RESOURCE_UPDATE_FIELDS = { +RESOURCE_UPDATE_FIELDS: VolDictType = { vol.Optional(CONF_RESOURCE_TYPE_WS): vol.In(RESOURCE_TYPES), vol.Optional(CONF_URL): cv.string, } @@ -54,7 +55,7 @@ CONF_TITLE = "title" CONF_REQUIRE_ADMIN = "require_admin" CONF_SHOW_IN_SIDEBAR = "show_in_sidebar" -DASHBOARD_BASE_CREATE_FIELDS = { +DASHBOARD_BASE_CREATE_FIELDS: VolDictType = { vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean, vol.Optional(CONF_ICON): cv.icon, vol.Required(CONF_TITLE): cv.string, @@ -62,7 +63,7 @@ DASHBOARD_BASE_CREATE_FIELDS = { } -DASHBOARD_BASE_UPDATE_FIELDS = { +DASHBOARD_BASE_UPDATE_FIELDS: VolDictType = { vol.Optional(CONF_REQUIRE_ADMIN): cv.boolean, vol.Optional(CONF_ICON): vol.Any(cv.icon, None), vol.Optional(CONF_TITLE): cv.string, @@ -70,7 +71,7 @@ DASHBOARD_BASE_UPDATE_FIELDS = { } -STORAGE_DASHBOARD_CREATE_FIELDS = { +STORAGE_DASHBOARD_CREATE_FIELDS: VolDictType = { **DASHBOARD_BASE_CREATE_FIELDS, vol.Required(CONF_URL_PATH): cv.string, # For now we write "storage" as all modes. diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 0779140a091..b793f4b33ae 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -50,7 +50,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.loader import bind_hass from .const import DOMAIN @@ -165,7 +165,7 @@ def entities_in_person(hass: HomeAssistant, entity_id: str) -> list[str]: return person_entity.device_trackers -CREATE_FIELDS = { +CREATE_FIELDS: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_USER_ID): vol.Any(str, None), vol.Optional(CONF_DEVICE_TRACKERS, default=list): vol.All( @@ -175,7 +175,7 @@ CREATE_FIELDS = { } -UPDATE_FIELDS = { +UPDATE_FIELDS: VolDictType = { vol.Optional(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_USER_ID): vol.Any(str, None), vol.Optional(CONF_DEVICE_TRACKERS, default=list): vol.All( diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py index e69a6761bc7..08d0b083f7c 100644 --- a/homeassistant/components/schedule/__init__.py +++ b/homeassistant/components/schedule/__init__.py @@ -33,7 +33,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.util import dt as dt_util from .const import ( @@ -104,12 +104,12 @@ def serialize_to_time(value: Any) -> Any: return vol.Coerce(str)(value) -BASE_SCHEMA = { +BASE_SCHEMA: VolDictType = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_ICON): cv.icon, } -TIME_RANGE_SCHEMA = { +TIME_RANGE_SCHEMA: VolDictType = { vol.Required(CONF_FROM): cv.time, vol.Required(CONF_TO): deserialize_to_time, } @@ -122,13 +122,13 @@ STORAGE_TIME_RANGE_SCHEMA = vol.Schema( } ) -SCHEDULE_SCHEMA = { +SCHEDULE_SCHEMA: VolDictType = { vol.Optional(day, default=[]): vol.All( cv.ensure_list, [TIME_RANGE_SCHEMA], valid_schedule ) for day in CONF_ALL_DAYS } -STORAGE_SCHEDULE_SCHEMA = { +STORAGE_SCHEDULE_SCHEMA: VolDictType = { vol.Optional(day, default=[]): vol.All( cv.ensure_list, [TIME_RANGE_SCHEMA], valid_schedule, [STORAGE_TIME_RANGE_SCHEMA] ) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index af3d06cf2d4..97307112f22 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.util import slugify import homeassistant.util.dt as dt_util from homeassistant.util.hass_dict import HassKey @@ -35,7 +35,7 @@ STORAGE_VERSION_MINOR = 3 TAG_DATA: HassKey[TagStorageCollection] = HassKey(DOMAIN) -CREATE_FIELDS = { +CREATE_FIELDS: VolDictType = { vol.Optional(TAG_ID): cv.string, vol.Optional(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional("description"): cv.string, @@ -43,7 +43,7 @@ CREATE_FIELDS = { vol.Optional(DEVICE_ID): cv.string, } -UPDATE_FIELDS = { +UPDATE_FIELDS: VolDictType = { vol.Optional(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional("description"): cv.string, vol.Optional(LAST_SCANNED): cv.datetime, @@ -192,8 +192,8 @@ class TagDictStorageCollectionWebsocket( storage_collection: TagStorageCollection, api_prefix: str, model_name: str, - create_schema: ConfigType, - update_schema: ConfigType, + create_schema: VolDictType, + update_schema: VolDictType, ) -> None: """Initialize a websocket for tag.""" super().__init__( diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 8927439a6cc..3f2b4bd7f43 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -66,7 +66,7 @@ SERVICE_FINISH = "finish" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_FIELDS = { +STORAGE_FIELDS: VolDictType = { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): cv.time_period, diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 0fef9961679..1c43a79e10e 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -45,7 +45,7 @@ from homeassistant.helpers import ( service, storage, ) -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.loader import bind_hass from homeassistant.util.location import distance @@ -62,7 +62,7 @@ ENTITY_ID_HOME = ENTITY_ID_FORMAT.format(HOME_ZONE) ICON_HOME = "mdi:home" ICON_IMPORT = "mdi:import" -CREATE_FIELDS = { +CREATE_FIELDS: VolDictType = { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_LATITUDE): cv.latitude, vol.Required(CONF_LONGITUDE): cv.longitude, @@ -72,7 +72,7 @@ CREATE_FIELDS = { } -UPDATE_FIELDS = { +UPDATE_FIELDS: VolDictType = { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index b9993098003..036aaacf0e9 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -26,7 +26,7 @@ from . import entity_registry from .entity import Entity from .entity_component import EntityComponent from .storage import Store -from .typing import ConfigType +from .typing import ConfigType, VolDictType STORAGE_VERSION = 1 SAVE_DELAY = 10 @@ -515,8 +515,8 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: storage_collection: _StorageCollectionT, api_prefix: str, model_name: str, - create_schema: dict, - update_schema: dict, + create_schema: VolDictType, + update_schema: VolDictType, ) -> None: """Initialize a websocket CRUD.""" self.storage_collection = storage_collection From 185e79fa1ba9b2023c2dd584820f51731d34c5bc Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 19:02:04 +0200 Subject: [PATCH 0141/2411] Improve intent schema typing (#120442) --- homeassistant/helpers/intent.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 502b20eaf8f..e191bddf102 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -33,6 +33,7 @@ from . import ( entity_registry, floor_registry, ) +from .typing import VolSchemaType _LOGGER = logging.getLogger(__name__) type _SlotsType = dict[str, Any] @@ -807,8 +808,8 @@ class DynamicServiceIntentHandler(IntentHandler): self, intent_type: str, speech: str | None = None, - required_slots: dict[str | tuple[str, str], vol.Schema] | None = None, - optional_slots: dict[str | tuple[str, str], vol.Schema] | None = None, + required_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, + optional_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, required_domains: set[str] | None = None, required_features: int | None = None, required_states: set[str] | None = None, @@ -824,7 +825,7 @@ class DynamicServiceIntentHandler(IntentHandler): self.description = description self.platforms = platforms - self.required_slots: dict[tuple[str, str], vol.Schema] = {} + self.required_slots: dict[tuple[str, str], VolSchemaType] = {} if required_slots: for key, value_schema in required_slots.items(): if isinstance(key, str): @@ -833,7 +834,7 @@ class DynamicServiceIntentHandler(IntentHandler): self.required_slots[key] = value_schema - self.optional_slots: dict[tuple[str, str], vol.Schema] = {} + self.optional_slots: dict[tuple[str, str], VolSchemaType] = {} if optional_slots: for key, value_schema in optional_slots.items(): if isinstance(key, str): @@ -1107,8 +1108,8 @@ class ServiceIntentHandler(DynamicServiceIntentHandler): domain: str, service: str, speech: str | None = None, - required_slots: dict[str | tuple[str, str], vol.Schema] | None = None, - optional_slots: dict[str | tuple[str, str], vol.Schema] | None = None, + required_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, + optional_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, required_domains: set[str] | None = None, required_features: int | None = None, required_states: set[str] | None = None, From cbcf29720dd42f49c0b9e8975e44380b89c3ccb3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 25 Jun 2024 19:15:11 +0200 Subject: [PATCH 0142/2411] Cleanup common mqtt tests (#120446) --- .../mqtt/test_alarm_control_panel.py | 48 ++++----------- tests/components/mqtt/test_binary_sensor.py | 51 ++++------------ tests/components/mqtt/test_button.py | 48 ++++----------- tests/components/mqtt/test_camera.py | 40 +++---------- tests/components/mqtt/test_climate.py | 41 ++++--------- tests/components/mqtt/test_common.py | 19 +----- tests/components/mqtt/test_cover.py | 41 ++++--------- tests/components/mqtt/test_event.py | 24 ++------ tests/components/mqtt/test_fan.py | 37 ++++-------- tests/components/mqtt/test_humidifier.py | 46 ++++----------- tests/components/mqtt/test_image.py | 41 ++++--------- tests/components/mqtt/test_lawn_mower.py | 38 +++--------- tests/components/mqtt/test_light.py | 45 ++++---------- tests/components/mqtt/test_light_json.py | 57 ++++-------------- tests/components/mqtt/test_light_template.py | 41 ++++--------- tests/components/mqtt/test_lock.py | 37 ++++-------- tests/components/mqtt/test_notify.py | 48 ++++----------- tests/components/mqtt/test_number.py | 43 ++++---------- tests/components/mqtt/test_scene.py | 46 ++++----------- tests/components/mqtt/test_select.py | 43 ++++---------- tests/components/mqtt/test_sensor.py | 42 +++----------- tests/components/mqtt/test_siren.py | 39 +++---------- tests/components/mqtt/test_switch.py | 38 +++--------- tests/components/mqtt/test_text.py | 58 +++++-------------- tests/components/mqtt/test_update.py | 40 +++---------- tests/components/mqtt/test_vacuum.py | 40 +++---------- tests/components/mqtt/test_valve.py | 38 +++--------- tests/components/mqtt/test_water_heater.py | 41 ++++--------- 28 files changed, 266 insertions(+), 904 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index a90e71cebe5..cd7e8ab7339 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -192,7 +192,7 @@ def does_not_raise(): ], ) async def test_fail_setup_without_state_or_command_topic( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, valid + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, valid: bool ) -> None: """Test for failing setup with no state or command topic.""" assert await mqtt_mock_entry() @@ -351,8 +351,8 @@ async def test_supported_features( async def test_publish_mqtt_no_code( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - service, - payload, + service: str, + payload: str, ) -> None: """Test publishing of MQTT messages when no code is configured.""" mqtt_mock = await mqtt_mock_entry() @@ -952,17 +952,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) @@ -997,21 +991,17 @@ async def test_unique_id( async def test_discovery_removal_alarm( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered alarm_control_panel.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, alarm_control_panel.DOMAIN, data + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, data ) async def test_discovery_update_alarm_topic_and_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) @@ -1036,7 +1026,6 @@ async def test_discovery_update_alarm_topic_and_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, alarm_control_panel.DOMAIN, config1, config2, @@ -1046,9 +1035,7 @@ async def test_discovery_update_alarm_topic_and_template( async def test_discovery_update_alarm_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) @@ -1071,7 +1058,6 @@ async def test_discovery_update_alarm_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, alarm_control_panel.DOMAIN, config1, config2, @@ -1081,9 +1067,7 @@ async def test_discovery_update_alarm_template( async def test_discovery_update_unchanged_alarm( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) @@ -1096,7 +1080,6 @@ async def test_discovery_update_unchanged_alarm( await help_test_discovery_update_unchanged( hass, mqtt_mock_entry, - caplog, alarm_control_panel.DOMAIN, data1, discovery_update, @@ -1105,9 +1088,7 @@ async def test_discovery_update_unchanged_alarm( @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -1117,12 +1098,7 @@ async def test_discovery_broken( ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, - mqtt_mock_entry, - caplog, - alarm_control_panel.DOMAIN, - data1, - data2, + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, data1, data2 ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index afa9ca9970e..7ad394243df 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -90,7 +90,6 @@ DEFAULT_CONFIG = { async def test_setting_sensor_value_expires_availability_topic( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test the expiration of the value.""" await mqtt_mock_entry() @@ -797,17 +796,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -840,21 +833,15 @@ async def test_unique_id( async def test_discovery_removal_binary_sensor( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered binary_sensor.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, binary_sensor.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, binary_sensor.DOMAIN, data) async def test_discovery_update_binary_sensor_topic_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) @@ -881,7 +868,6 @@ async def test_discovery_update_binary_sensor_topic_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, binary_sensor.DOMAIN, config1, config2, @@ -891,9 +877,7 @@ async def test_discovery_update_binary_sensor_topic_template( async def test_discovery_update_binary_sensor_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) @@ -918,7 +902,6 @@ async def test_discovery_update_binary_sensor_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, binary_sensor.DOMAIN, config1, config2, @@ -962,9 +945,7 @@ async def test_encoding_subscribable_topics( async def test_discovery_update_unchanged_binary_sensor( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) @@ -975,31 +956,19 @@ async def test_discovery_update_unchanged_binary_sensor( "homeassistant.components.mqtt.binary_sensor.MqttBinarySensor.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - binary_sensor.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "off_delay": -1 }' data2 = '{ "name": "Milk", "state_topic": "test_topic" }' await help_test_discovery_broken( - hass, - mqtt_mock_entry, - caplog, - binary_sensor.DOMAIN, - data1, - data2, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, data1, data2 ) diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 3d5d295d4d4..2d21128237e 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -252,17 +252,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - button.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, button.DOMAIN, DEFAULT_CONFIG ) @@ -295,21 +289,15 @@ async def test_unique_id( async def test_discovery_removal_button( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered button.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, button.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, button.DOMAIN, data) async def test_discovery_update_button( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered button.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][button.DOMAIN]) @@ -318,19 +306,12 @@ async def test_discovery_update_button( config2["name"] = "Milk" await help_test_discovery_update( - hass, - mqtt_mock_entry, - caplog, - button.DOMAIN, - config1, - config2, + hass, mqtt_mock_entry, button.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_button( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered button.""" data1 = ( @@ -342,27 +323,18 @@ async def test_discovery_update_unchanged_button( "homeassistant.components.mqtt.button.MqttButton.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - button.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, button.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, button.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, button.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index fb0107d6780..9dbf5035fc9 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -246,17 +246,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - camera.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, camera.DOMAIN, DEFAULT_CONFIG ) @@ -289,35 +283,28 @@ async def test_unique_id( async def test_discovery_removal_camera( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered camera.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][camera.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, camera.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, camera.DOMAIN, data) async def test_discovery_update_camera( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test update of discovered camera.""" config1 = {"name": "Beer", "topic": "test_topic"} config2 = {"name": "Milk", "topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, camera.DOMAIN, config1, config2 + hass, mqtt_mock_entry, camera.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_camera( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered camera.""" data1 = '{ "name": "Beer", "topic": "test_topic"}' @@ -325,28 +312,19 @@ async def test_discovery_update_unchanged_camera( "homeassistant.components.mqtt.camera.MqttCamera.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - camera.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, camera.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "topic": "test_topic"}' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, camera.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, camera.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 2bf78e59e42..5428dc9b3e1 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -1903,17 +1903,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - climate.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, climate.DOMAIN, DEFAULT_CONFIG ) @@ -1987,34 +1981,26 @@ async def test_encoding_subscribable_topics( async def test_discovery_removal_climate( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered climate.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][climate.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, climate.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, climate.DOMAIN, data) async def test_discovery_update_climate( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered climate.""" config1 = {"name": "Beer"} config2 = {"name": "Milk"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, climate.DOMAIN, config1, config2 + hass, mqtt_mock_entry, climate.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_climate( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered climate.""" data1 = '{ "name": "Beer" }' @@ -2022,26 +2008,19 @@ async def test_discovery_update_unchanged_climate( "homeassistant.components.mqtt.climate.MqttClimate.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - climate.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, climate.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "power_command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "power_command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, climate.DOMAIN, data1, data2 + hass, mqtt_mock_entry, climate.DOMAIN, data1, data2 ) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index d196e1998fb..8d457d9da85 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -103,9 +103,7 @@ def help_custom_config( async def help_test_availability_when_connection_lost( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - domain: str, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, domain: str ) -> None: """Test availability after MQTT disconnection.""" mqtt_mock = await mqtt_mock_entry() @@ -251,8 +249,6 @@ async def help_test_default_availability_list_payload_all( domain: str, config: ConfigType, no_assumed_state: bool = False, - state_topic: str | None = None, - state_message: str | None = None, ) -> None: """Test availability by default payload with defined topic. @@ -314,8 +310,6 @@ async def help_test_default_availability_list_payload_any( domain: str, config: ConfigType, no_assumed_state: bool = False, - state_topic: str | None = None, - state_message: str | None = None, ) -> None: """Test availability by default payload with defined topic. @@ -657,7 +651,6 @@ async def help_test_update_with_json_attrs_bad_json( async def help_test_discovery_update_attr( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, domain: str, config: ConfigType, ) -> None: @@ -696,9 +689,7 @@ async def help_test_discovery_update_attr( async def help_test_unique_id( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - domain: str, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, domain: str ) -> None: """Test unique id option only creates one entity per unique_id.""" await mqtt_mock_entry() @@ -709,7 +700,6 @@ async def help_test_unique_id( async def help_test_discovery_removal( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, domain: str, data: str, ) -> None: @@ -735,8 +725,7 @@ async def help_test_discovery_removal( async def help_test_discovery_update( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog, - domain, + domain: str, discovery_config1: DiscoveryInfoType, discovery_config2: DiscoveryInfoType, state_data1: _StateDataType | None = None, @@ -800,7 +789,6 @@ async def help_test_discovery_update( async def help_test_discovery_update_unchanged( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, domain: str, data1: str, discovery_update: MagicMock, @@ -826,7 +814,6 @@ async def help_test_discovery_update_unchanged( async def help_test_discovery_broken( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, domain: str, data1: str, data2: str, diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 4b46f49c629..988119d09c1 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -2571,17 +2571,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - cover.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, cover.DOMAIN, DEFAULT_CONFIG ) @@ -2614,32 +2608,26 @@ async def test_unique_id( async def test_discovery_removal_cover( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered cover.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, cover.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, cover.DOMAIN, data) async def test_discovery_update_cover( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered cover.""" config1 = {"name": "Beer", "command_topic": "test_topic"} config2 = {"name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, cover.DOMAIN, config1, config2 + hass, mqtt_mock_entry, cover.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_cover( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered cover.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' @@ -2647,27 +2635,18 @@ async def test_discovery_update_unchanged_cover( "homeassistant.components.mqtt.cover.MqttCover.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - cover.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, cover.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, cover.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, cover.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_event.py b/tests/components/mqtt/test_event.py index fd4f8eb3e5d..48f80bf41d7 100644 --- a/tests/components/mqtt/test_event.py +++ b/tests/components/mqtt/test_event.py @@ -397,17 +397,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - event.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG ) @@ -442,13 +436,11 @@ async def test_unique_id( async def test_discovery_removal_event( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered event.""" data = '{ "name": "test", "state_topic": "test_topic", "event_types": ["press"] }' - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, event.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, event.DOMAIN, data) async def test_discovery_update_event_template( @@ -491,16 +483,12 @@ async def test_discovery_update_event_template( @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "state_topic": "test_topic#", "event_types": ["press"] }' data2 = '{ "name": "Milk", "state_topic": "test_topic", "event_types": ["press"] }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, event.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, event.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 0dbfa3037b2..80e45c87789 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -1989,13 +1989,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry, caplog, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry, fan.DOMAIN, DEFAULT_CONFIG ) @@ -2030,32 +2028,26 @@ async def test_unique_id( async def test_discovery_removal_fan( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered fan.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, fan.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, fan.DOMAIN, data) async def test_discovery_update_fan( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered fan.""" config1 = {"name": "Beer", "command_topic": "test_topic"} config2 = {"name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, fan.DOMAIN, config1, config2 + hass, mqtt_mock_entry, fan.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_fan( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered fan.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' @@ -2063,28 +2055,19 @@ async def test_discovery_update_unchanged_fan( "homeassistant.components.mqtt.fan.MqttFan.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - fan.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, fan.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, fan.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, fan.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 4e8918d330e..b583412b4ff 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -1276,17 +1276,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - humidifier.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, humidifier.DOMAIN, DEFAULT_CONFIG ) @@ -1323,21 +1317,15 @@ async def test_unique_id( async def test_discovery_removal_humidifier( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered humidifier.""" data = '{ "name": "test", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, humidifier.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, humidifier.DOMAIN, data) async def test_discovery_update_humidifier( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered humidifier.""" config1 = { @@ -1351,19 +1339,12 @@ async def test_discovery_update_humidifier( "target_humidity_command_topic": "test-topic2", } await help_test_discovery_update( - hass, - mqtt_mock_entry, - caplog, - humidifier.DOMAIN, - config1, - config2, + hass, mqtt_mock_entry, humidifier.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_humidifier( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered humidifier.""" data1 = '{ "name": "Beer", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' @@ -1371,26 +1352,19 @@ async def test_discovery_update_unchanged_humidifier( "homeassistant.components.mqtt.fan.MqttFan.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - humidifier.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, humidifier.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, humidifier.DOMAIN, data1, data2 + hass, mqtt_mock_entry, humidifier.DOMAIN, data1, data2 ) diff --git a/tests/components/mqtt/test_image.py b/tests/components/mqtt/test_image.py index 79e6cf1d281..a299474c0ac 100644 --- a/tests/components/mqtt/test_image.py +++ b/tests/components/mqtt/test_image.py @@ -600,17 +600,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - image.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, image.DOMAIN, DEFAULT_CONFIG ) @@ -643,33 +637,27 @@ async def test_unique_id( async def test_discovery_removal_image( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered image.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][image.DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, image.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, image.DOMAIN, data) async def test_discovery_update_image( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered image.""" config1 = {"name": "Beer", "image_topic": "test_topic"} config2 = {"name": "Milk", "image_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, image.DOMAIN, config1, config2 + hass, mqtt_mock_entry, image.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_image( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered image.""" data1 = '{ "name": "Beer", "image_topic": "test_topic"}' @@ -677,28 +665,19 @@ async def test_discovery_update_unchanged_image( "homeassistant.components.mqtt.image.MqttImage.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - image.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, image.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "image_topic": "test_topic"}' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, image.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, image.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_lawn_mower.py b/tests/components/mqtt/test_lawn_mower.py index a258339e9cc..120a09deb88 100644 --- a/tests/components/mqtt/test_lawn_mower.py +++ b/tests/components/mqtt/test_lawn_mower.py @@ -466,17 +466,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - lawn_mower.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, lawn_mower.DOMAIN, DEFAULT_CONFIG ) @@ -509,21 +503,16 @@ async def test_unique_id( async def test_discovery_removal_lawn_mower( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered lawn_mower.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][lawn_mower.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, lawn_mower.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, lawn_mower.DOMAIN, data) async def test_discovery_update_lawn_mower( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test update of discovered lawn_mower.""" config1 = { @@ -540,14 +529,12 @@ async def test_discovery_update_lawn_mower( } await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, lawn_mower.DOMAIN, config1, config2 + hass, mqtt_mock_entry, lawn_mower.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_lawn_mower( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered lawn_mower.""" data1 = '{ "name": "Beer", "activity_state_topic": "test-topic", "command_topic": "test-topic", "actions": ["milk", "beer"]}' @@ -555,27 +542,20 @@ async def test_discovery_update_unchanged_lawn_mower( "homeassistant.components.mqtt.lawn_mower.MqttLawnMower.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - lawn_mower.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, lawn_mower.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "invalid" }' data2 = '{ "name": "Milk", "activity_state_topic": "test-topic", "pause_command_topic": "test-topic"}' await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, lawn_mower.DOMAIN, data1, data2 + hass, mqtt_mock_entry, lawn_mower.DOMAIN, data1, data2 ) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 492bc6806da..bfce49b9ecb 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -2516,17 +2516,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, light.DOMAIN, DEFAULT_CONFIG ) @@ -2561,9 +2555,7 @@ async def test_unique_id( async def test_discovery_removal_light( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered light.""" data = ( @@ -2571,7 +2563,7 @@ async def test_discovery_removal_light( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, light.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, light.DOMAIN, data) async def test_discovery_ignores_extra_keys( @@ -2591,9 +2583,7 @@ async def test_discovery_ignores_extra_keys( async def test_discovery_update_light_topic_and_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered light.""" config1 = { @@ -2838,7 +2828,6 @@ async def test_discovery_update_light_topic_and_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, light.DOMAIN, config1, config2, @@ -2848,9 +2837,7 @@ async def test_discovery_update_light_topic_and_template( async def test_discovery_update_light_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered light.""" config1 = { @@ -3053,7 +3040,6 @@ async def test_discovery_update_light_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, light.DOMAIN, config1, config2, @@ -3063,9 +3049,7 @@ async def test_discovery_update_light_template( async def test_discovery_update_unchanged_light( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered light.""" data1 = ( @@ -3077,20 +3061,13 @@ async def test_discovery_update_unchanged_light( "homeassistant.components.mqtt.light.schema_basic.MqttLight.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, light.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -3099,9 +3076,7 @@ async def test_discovery_broken( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, light.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, light.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 739240a352c..5ab2a32dc83 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -2398,17 +2398,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, light.DOMAIN, DEFAULT_CONFIG ) @@ -2445,25 +2439,15 @@ async def test_unique_id( async def test_discovery_removal( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered mqtt_json lights.""" data = '{ "name": "test", "schema": "json", "command_topic": "test_topic" }' - await help_test_discovery_removal( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - data, - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, light.DOMAIN, data) async def test_discovery_update_light( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered light.""" config1 = { @@ -2479,19 +2463,12 @@ async def test_discovery_update_light( "command_topic": "test_topic", } await help_test_discovery_update( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - config1, - config2, + hass, mqtt_mock_entry, light.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_light( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered light.""" data1 = ( @@ -2504,20 +2481,13 @@ async def test_discovery_update_unchanged_light( "homeassistant.components.mqtt.light.schema_json.MqttLightJson.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, light.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -2527,14 +2497,7 @@ async def test_discovery_broken( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_broken( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - data1, - data2, - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, light.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index da6195fa32e..aace09f402a 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -1002,17 +1002,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, light.DOMAIN, DEFAULT_CONFIG ) @@ -1053,9 +1047,7 @@ async def test_unique_id( async def test_discovery_removal( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered mqtt_json lights.""" data = ( @@ -1065,13 +1057,11 @@ async def test_discovery_removal( ' "command_on_template": "on",' ' "command_off_template": "off"}' ) - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, light.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, light.DOMAIN, data) async def test_discovery_update_light( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered light.""" config1 = { @@ -1091,14 +1081,12 @@ async def test_discovery_update_light( "command_off_template": "off", } await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, light.DOMAIN, config1, config2 + hass, mqtt_mock_entry, light.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_light( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered light.""" data1 = ( @@ -1113,20 +1101,13 @@ async def test_discovery_update_unchanged_light( "homeassistant.components.mqtt.light.schema_template.MqttLightTemplate.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, light.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -1138,9 +1119,7 @@ async def test_discovery_broken( ' "command_on_template": "on",' ' "command_off_template": "off"}' ) - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, light.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, light.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index c9c2928f991..c9546bdfdb3 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -805,13 +805,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock_entry, caplog, lock.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry, lock.DOMAIN, DEFAULT_CONFIG ) @@ -846,19 +844,15 @@ async def test_unique_id( async def test_discovery_removal_lock( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered lock.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, lock.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, lock.DOMAIN, data) async def test_discovery_update_lock( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered lock.""" config1 = { @@ -874,14 +868,12 @@ async def test_discovery_update_lock( "availability_topic": "availability_topic2", } await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, lock.DOMAIN, config1, config2 + hass, mqtt_mock_entry, lock.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_lock( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered lock.""" data1 = ( @@ -893,27 +885,18 @@ async def test_discovery_update_unchanged_lock( "homeassistant.components.mqtt.lock.MqttLock.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - lock.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, lock.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, lock.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, lock.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_notify.py b/tests/components/mqtt/test_notify.py index bc833b79eb0..540dbbafd99 100644 --- a/tests/components/mqtt/test_notify.py +++ b/tests/components/mqtt/test_notify.py @@ -223,17 +223,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - notify.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG ) @@ -266,21 +260,15 @@ async def test_unique_id( async def test_discovery_removal_notify( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered notify.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, notify.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, notify.DOMAIN, data) async def test_discovery_update_notify( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered notify.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][notify.DOMAIN]) @@ -289,19 +277,12 @@ async def test_discovery_update_notify( config2["name"] = "Milk" await help_test_discovery_update( - hass, - mqtt_mock_entry, - caplog, - notify.DOMAIN, - config1, - config2, + hass, mqtt_mock_entry, notify.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_notify( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered notify.""" data1 = ( @@ -313,27 +294,18 @@ async def test_discovery_update_unchanged_notify( "homeassistant.components.mqtt.notify.MqttNotify.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - notify.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, notify.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, notify.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, notify.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index b0f9e79cb3e..2cd5c5390f5 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -581,17 +581,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - number.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, number.DOMAIN, DEFAULT_CONFIG ) @@ -626,21 +620,15 @@ async def test_unique_id( async def test_discovery_removal_number( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered number.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][number.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, number.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, number.DOMAIN, data) async def test_discovery_update_number( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered number.""" config1 = { @@ -655,14 +643,12 @@ async def test_discovery_update_number( } await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, number.DOMAIN, config1, config2 + hass, mqtt_mock_entry, number.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_number( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered number.""" data1 = ( @@ -672,20 +658,13 @@ async def test_discovery_update_unchanged_number( "homeassistant.components.mqtt.number.MqttNumber.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - number.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, number.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -693,9 +672,7 @@ async def test_discovery_broken( '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic"}' ) - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, number.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, number.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 3e9eacd3be2..9badd6aeee0 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -183,19 +183,15 @@ async def test_unique_id( async def test_discovery_removal_scene( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered scene.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, scene.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, scene.DOMAIN, data) async def test_discovery_update_payload( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered scene.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][scene.DOMAIN]) @@ -206,19 +202,12 @@ async def test_discovery_update_payload( config2["payload_on"] = "ACTIVATE" await help_test_discovery_update( - hass, - mqtt_mock_entry, - caplog, - scene.DOMAIN, - config1, - config2, + hass, mqtt_mock_entry, scene.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_scene( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered scene.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' @@ -226,27 +215,18 @@ async def test_discovery_update_unchanged_scene( "homeassistant.components.mqtt.scene.MqttScene.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - scene.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, scene.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, scene.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, scene.DOMAIN, data1, data2) async def test_setting_attribute_via_mqtt_json_message( @@ -307,17 +287,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - scene.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, scene.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index b8c55dd2ffb..26a64d70fee 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -431,17 +431,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - select.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, select.DOMAIN, DEFAULT_CONFIG ) @@ -478,21 +472,15 @@ async def test_unique_id( async def test_discovery_removal_select( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered select.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][select.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, select.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, select.DOMAIN, data) async def test_discovery_update_select( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered select.""" config1 = { @@ -509,14 +497,12 @@ async def test_discovery_update_select( } await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, select.DOMAIN, config1, config2 + hass, mqtt_mock_entry, select.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_select( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered select.""" data1 = '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' @@ -524,28 +510,19 @@ async def test_discovery_update_unchanged_select( "homeassistant.components.mqtt.select.MqttSelect.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - select.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, select.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, select.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, select.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index bde85abf3fb..94eb049dda7 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -978,15 +978,12 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( hass, mqtt_mock_entry, - caplog, sensor.DOMAIN, DEFAULT_CONFIG, ) @@ -1021,21 +1018,15 @@ async def test_unique_id( async def test_discovery_removal_sensor( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered sensor.""" data = '{ "name": "test", "state_topic": "test_topic" }' - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, sensor.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, sensor.DOMAIN, data) async def test_discovery_update_sensor_topic_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered sensor.""" config = {"name": "test", "state_topic": "test_topic"} @@ -1060,7 +1051,6 @@ async def test_discovery_update_sensor_topic_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, sensor.DOMAIN, config1, config2, @@ -1070,9 +1060,7 @@ async def test_discovery_update_sensor_topic_template( async def test_discovery_update_sensor_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered sensor.""" config = {"name": "test", "state_topic": "test_topic"} @@ -1095,7 +1083,6 @@ async def test_discovery_update_sensor_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, sensor.DOMAIN, config1, config2, @@ -1105,9 +1092,7 @@ async def test_discovery_update_sensor_template( async def test_discovery_update_unchanged_sensor( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered sensor.""" data1 = '{ "name": "Beer", "state_topic": "test_topic" }' @@ -1115,27 +1100,18 @@ async def test_discovery_update_unchanged_sensor( "homeassistant.components.mqtt.sensor.MqttSensor.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - sensor.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, sensor.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "state_topic": "test_topic#" }' data2 = '{ "name": "Milk", "state_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, sensor.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, sensor.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 28b88e2793d..c32c57d4f02 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -642,17 +642,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - siren.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, siren.DOMAIN, DEFAULT_CONFIG ) @@ -687,9 +681,7 @@ async def test_unique_id( async def test_discovery_removal_siren( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered siren.""" data = ( @@ -697,13 +689,11 @@ async def test_discovery_removal_siren( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, siren.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, siren.DOMAIN, data) async def test_discovery_update_siren_topic_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered siren.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN]) @@ -730,7 +720,6 @@ async def test_discovery_update_siren_topic_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, siren.DOMAIN, config1, config2, @@ -740,9 +729,7 @@ async def test_discovery_update_siren_topic_template( async def test_discovery_update_siren_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered siren.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN]) @@ -767,7 +754,6 @@ async def test_discovery_update_siren_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, siren.DOMAIN, config1, config2, @@ -867,9 +853,7 @@ async def test_command_templates( async def test_discovery_update_unchanged_siren( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered siren.""" data1 = ( @@ -884,7 +868,6 @@ async def test_discovery_update_unchanged_siren( await help_test_discovery_update_unchanged( hass, mqtt_mock_entry, - caplog, siren.DOMAIN, data1, discovery_update, @@ -893,9 +876,7 @@ async def test_discovery_update_unchanged_siren( @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -904,9 +885,7 @@ async def test_discovery_broken( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, siren.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, siren.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index b497d4a2f52..42d2e092d83 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -403,17 +403,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - switch.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, switch.DOMAIN, DEFAULT_CONFIG ) @@ -448,9 +442,7 @@ async def test_unique_id( async def test_discovery_removal_switch( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered switch.""" data = ( @@ -458,15 +450,11 @@ async def test_discovery_removal_switch( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, switch.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, switch.DOMAIN, data) async def test_discovery_update_switch_topic_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered switch.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][switch.DOMAIN]) @@ -493,7 +481,6 @@ async def test_discovery_update_switch_topic_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, switch.DOMAIN, config1, config2, @@ -503,9 +490,7 @@ async def test_discovery_update_switch_topic_template( async def test_discovery_update_switch_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered switch.""" config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][switch.DOMAIN]) @@ -530,7 +515,6 @@ async def test_discovery_update_switch_template( await help_test_discovery_update( hass, mqtt_mock_entry, - caplog, switch.DOMAIN, config1, config2, @@ -542,7 +526,6 @@ async def test_discovery_update_switch_template( async def test_discovery_update_unchanged_switch( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test update of discovered switch.""" data1 = ( @@ -557,7 +540,6 @@ async def test_discovery_update_unchanged_switch( await help_test_discovery_update_unchanged( hass, mqtt_mock_entry, - caplog, switch.DOMAIN, data1, discovery_update, @@ -566,9 +548,7 @@ async def test_discovery_update_unchanged_switch( @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -577,9 +557,7 @@ async def test_discovery_broken( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, switch.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, switch.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_text.py b/tests/components/mqtt/test_text.py index 2c58cae690d..fc714efa513 100644 --- a/tests/components/mqtt/test_text.py +++ b/tests/components/mqtt/test_text.py @@ -529,17 +529,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - text.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, text.DOMAIN, DEFAULT_CONFIG ) @@ -574,9 +568,7 @@ async def test_unique_id( async def test_discovery_removal_text( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered text entity.""" data = ( @@ -584,13 +576,11 @@ async def test_discovery_removal_text( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, text.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, text.DOMAIN, data) async def test_discovery_text_update( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered text entity.""" config1 = { @@ -605,14 +595,12 @@ async def test_discovery_text_update( } await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, text.DOMAIN, config1, config2 + hass, mqtt_mock_entry, text.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_update( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered update.""" data1 = '{ "name": "Beer", "state_topic": "text-topic", "command_topic": "command-topic"}' @@ -620,32 +608,23 @@ async def test_discovery_update_unchanged_update( "homeassistant.components.mqtt.text.MqttTextEntity.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - text.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, text.DOMAIN, data1, discovery_update ) async def test_discovery_update_text( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered text entity.""" config1 = {"name": "Beer", "command_topic": "cmd-topic1"} config2 = {"name": "Milk", "command_topic": "cmd-topic2"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, text.DOMAIN, config1, config2 + hass, mqtt_mock_entry, text.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_climate( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered text entity.""" data1 = '{ "name": "Beer", "command_topic": "cmd-topic" }' @@ -653,20 +632,13 @@ async def test_discovery_update_unchanged_climate( "homeassistant.components.mqtt.text.MqttTextEntity.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - text.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, text.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -675,9 +647,7 @@ async def test_discovery_broken( ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, text.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, text.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py index bb80a0c274f..bb9ae12c66b 100644 --- a/tests/components/mqtt/test_update.py +++ b/tests/components/mqtt/test_update.py @@ -530,15 +530,10 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - update.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, update.DOMAIN, DEFAULT_CONFIG ) @@ -573,21 +568,15 @@ async def test_unique_id( async def test_discovery_removal_update( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered update.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][update.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, update.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, update.DOMAIN, data) async def test_discovery_update_update( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered update.""" config1 = { @@ -602,14 +591,12 @@ async def test_discovery_update_update( } await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, update.DOMAIN, config1, config2 + hass, mqtt_mock_entry, update.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_update( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered update.""" data1 = '{ "name": "Beer", "state_topic": "installed-topic", "latest_version_topic": "latest-topic"}' @@ -617,28 +604,19 @@ async def test_discovery_update_unchanged_update( "homeassistant.components.mqtt.update.MqttUpdate.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - update.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, update.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "state_topic": "installed-topic", "latest_version_topic": "latest-topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, update.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, update.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index 0a06759c7e6..8c01138ccb9 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -531,17 +531,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - vacuum.DOMAIN, - DEFAULT_CONFIG_2, + hass, mqtt_mock_entry, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -574,34 +568,27 @@ async def test_unique_id( async def test_discovery_removal_vacuum( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered vacuum.""" data = '{"name": "test", "command_topic": "test_topic"}' - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, vacuum.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, vacuum.DOMAIN, data) async def test_discovery_update_vacuum( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered vacuum.""" config1 = {"name": "Beer", "command_topic": "test_topic"} config2 = {"name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, vacuum.DOMAIN, config1, config2 + hass, mqtt_mock_entry, vacuum.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_vacuum( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test update of discovered vacuum.""" data1 = '{"name": "Beer", "command_topic": "test_topic"}' @@ -609,27 +596,18 @@ async def test_discovery_update_unchanged_vacuum( "homeassistant.components.mqtt.vacuum.MqttStateVacuum.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - vacuum.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, vacuum.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{"name": "Beer", "command_topic": "test_topic#"}' data2 = '{"name": "Milk", "command_topic": "test_topic"}' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, vacuum.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, vacuum.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_valve.py b/tests/components/mqtt/test_valve.py index 2efa30d096a..6f88e160b73 100644 --- a/tests/components/mqtt/test_valve.py +++ b/tests/components/mqtt/test_valve.py @@ -1200,15 +1200,10 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - valve.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, valve.DOMAIN, DEFAULT_CONFIG ) @@ -1241,32 +1236,26 @@ async def test_unique_id( async def test_discovery_removal_valve( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered valve.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, valve.DOMAIN, data) + await help_test_discovery_removal(hass, mqtt_mock_entry, valve.DOMAIN, data) async def test_discovery_update_valve( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered valve.""" config1 = {"name": "Beer", "command_topic": "test_topic"} config2 = {"name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, valve.DOMAIN, config1, config2 + hass, mqtt_mock_entry, valve.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_valve( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered valve.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' @@ -1274,27 +1263,18 @@ async def test_discovery_update_unchanged_valve( "homeassistant.components.mqtt.valve.MqttValve.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - valve.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, valve.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, valve.DOMAIN, data1, data2 - ) + await help_test_discovery_broken(hass, mqtt_mock_entry, valve.DOMAIN, data1, data2) async def test_entity_device_info_with_connection( diff --git a/tests/components/mqtt/test_water_heater.py b/tests/components/mqtt/test_water_heater.py index a80ab59657f..849a1ac8785 100644 --- a/tests/components/mqtt/test_water_heater.py +++ b/tests/components/mqtt/test_water_heater.py @@ -858,17 +858,11 @@ async def test_update_with_json_attrs_bad_json( async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, - mqtt_mock_entry, - caplog, - water_heater.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, water_heater.DOMAIN, DEFAULT_CONFIG ) @@ -933,34 +927,26 @@ async def test_encoding_subscribable_topics( async def test_discovery_removal_water_heater( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of discovered water heater.""" data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][water_heater.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock_entry, caplog, water_heater.DOMAIN, data - ) + await help_test_discovery_removal(hass, mqtt_mock_entry, water_heater.DOMAIN, data) async def test_discovery_update_water_heater( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered water heater.""" config1 = {"name": "Beer"} config2 = {"name": "Milk"} await help_test_discovery_update( - hass, mqtt_mock_entry, caplog, water_heater.DOMAIN, config1, config2 + hass, mqtt_mock_entry, water_heater.DOMAIN, config1, config2 ) async def test_discovery_update_unchanged_water_heater( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered water heater.""" data1 = '{ "name": "Beer" }' @@ -968,26 +954,19 @@ async def test_discovery_update_unchanged_water_heater( "homeassistant.components.mqtt.water_heater.MqttWaterHeater.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, - mqtt_mock_entry, - caplog, - water_heater.DOMAIN, - data1, - discovery_update, + hass, mqtt_mock_entry, water_heater.DOMAIN, data1, discovery_update ) @pytest.mark.no_fail_on_log_exception async def test_discovery_broken( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "mode_command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "mode_command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock_entry, caplog, water_heater.DOMAIN, data1, data2 + hass, mqtt_mock_entry, water_heater.DOMAIN, data1, data2 ) From 3559755aeda368ea17133b388ac366ab058e2a77 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 19:16:19 +0200 Subject: [PATCH 0143/2411] Add import aliases for PLATFORM_SCHEMA (#120445) --- homeassistant/components/azure_service_bus/notify.py | 4 ++-- homeassistant/components/blackbird/media_player.py | 4 ++-- homeassistant/components/broadlink/switch.py | 4 ++-- homeassistant/components/citybikes/sensor.py | 4 ++-- homeassistant/components/history_stats/sensor.py | 4 ++-- homeassistant/components/integration/sensor.py | 4 ++-- homeassistant/components/rest/binary_sensor.py | 7 +++---- homeassistant/components/rest/sensor.py | 7 +++---- homeassistant/components/statistics/sensor.py | 4 ++-- homeassistant/components/template/light.py | 6 ++++-- homeassistant/components/template/sensor.py | 4 ++-- homeassistant/components/template/weather.py | 6 ++++-- homeassistant/components/tts/notify.py | 7 +++++-- 13 files changed, 35 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py index 38c57b3db19..a0aa36804c3 100644 --- a/homeassistant/components/azure_service_bus/notify.py +++ b/homeassistant/components/azure_service_bus/notify.py @@ -18,7 +18,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, ATTR_TITLE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONTENT_TYPE_JSON @@ -36,7 +36,7 @@ ATTR_ASB_TARGET = "target" PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_QUEUE_NAME, CONF_TOPIC_NAME), - PLATFORM_SCHEMA.extend( + NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_CONNECTION_STRING): cv.string, vol.Exclusive( diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index 4006b12738f..46cabaf4099 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -9,7 +9,7 @@ from serial import SerialException import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -56,7 +56,7 @@ SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8)) PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_PORT, CONF_HOST), - PLATFORM_SCHEMA.extend( + MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Exclusive(CONF_PORT, CONF_TYPE): cv.string, vol.Exclusive(CONF_HOST, CONF_TYPE): cv.string, diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 9cf7e3391fa..cc3b9dad464 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -10,7 +10,7 @@ from broadlink.exceptions import BroadlinkException import voluptuous as vol from homeassistant.components.switch import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchDeviceClass, SwitchEntity, ) @@ -56,7 +56,7 @@ PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_SLOTS), cv.deprecated(CONF_TIMEOUT), cv.deprecated(CONF_TYPE), - PLATFORM_SCHEMA.extend( + SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): mac_address, vol.Optional(CONF_HOST): cv.string, diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index 4049a656caf..5e4da231eef 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, ) from homeassistant.const import ( @@ -73,7 +73,7 @@ CITYBIKES_NETWORKS = "citybikes_networks" PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_RADIUS, CONF_STATIONS_LIST), - PLATFORM_SCHEMA.extend( + SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=""): cv.string, vol.Optional(CONF_NETWORK): cv.string, diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 0b02ddb2a8e..16279560d30 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -9,7 +9,7 @@ from typing import Any import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -66,7 +66,7 @@ def exactly_two_period_keys[_T: dict[str, Any]](conf: _T) -> _T: PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend( + SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_STATE): vol.All(cv.ensure_list, [cv.string]), diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index ffb7a3d8e6a..106eb9cc79c 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( DEVICE_CLASS_UNITS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, RestoreSensor, SensorDeviceClass, SensorExtraStoredData, @@ -81,7 +81,7 @@ DEFAULT_ROUND = 3 PLATFORM_SCHEMA = vol.All( cv.removed(CONF_UNIT_OF_MEASUREMENT), - PLATFORM_SCHEMA.extend( + SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 5aafd727178..e8119a40f8c 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.const import ( @@ -44,10 +44,9 @@ from .schema import BINARY_SENSOR_SCHEMA, RESOURCE_SCHEMA _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **BINARY_SENSOR_SCHEMA}) - PLATFORM_SCHEMA = vol.All( - cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA + BINARY_SENSOR_PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **BINARY_SENSOR_SCHEMA}), + cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), ) TRIGGER_ENTITY_OPTIONS = ( diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 810d286d147..d7bb0ea33fb 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( CONF_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, ) from homeassistant.components.sensor.helpers import async_parse_date_datetime @@ -49,10 +49,9 @@ from .util import parse_json_attributes _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **SENSOR_SCHEMA}) - PLATFORM_SCHEMA = vol.All( - cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA + SENSOR_PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **SENSOR_SCHEMA}), + cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), ) TRIGGER_ENTITY_OPTIONS = ( diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index fef10f7296f..eb4df4d98b2 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -17,7 +17,7 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI from homeassistant.components.recorder import get_instance, history from homeassistant.components.sensor import ( DEVICE_CLASS_STATE_CLASSES, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -229,7 +229,7 @@ def valid_keep_last_sample(config: dict[str, Any]) -> dict[str, Any]: return config -_PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 71443789703..de8a2998d34 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -34,7 +34,9 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.config_validation import ( + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, +) from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script @@ -115,7 +117,7 @@ PLATFORM_SCHEMA = vol.All( # CONF_WHITE_VALUE_* is deprecated, support will be removed in release 2022.9 cv.removed(CONF_WHITE_VALUE_ACTION), cv.removed(CONF_WHITE_VALUE_TEMPLATE), - PLATFORM_SCHEMA.extend( + BASE_PLATFORM_SCHEMA.extend( {vol.Required(CONF_LIGHTS): cv.schema_with_slug_keys(LIGHT_SCHEMA)} ), ) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 6cb73a15632..51669f11afe 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASSES_SCHEMA, DOMAIN as SENSOR_DOMAIN, ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, RestoreSensor, SensorDeviceClass, SensorEntity, @@ -152,7 +152,7 @@ def rewrite_legacy_to_modern_conf(cfg: dict[str, dict]) -> list[dict]: PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend( + SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TRIGGER): cv.match_all, # to raise custom warning vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(LEGACY_SENSOR_SCHEMA), diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index e8981fb33f9..0f80f65f501 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -41,7 +41,9 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.config_validation import ( + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, +) from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity @@ -136,7 +138,7 @@ WEATHER_SCHEMA = vol.Schema( PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_FORECAST_TEMPLATE), - PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema), + BASE_PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema), ) diff --git a/homeassistant/components/tts/notify.py b/homeassistant/components/tts/notify.py index e6963619043..429d46660e7 100644 --- a/homeassistant/components/tts/notify.py +++ b/homeassistant/components/tts/notify.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.components.notify import ( + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ATTR_ENTITY_ID, CONF_ENTITY_ID, CONF_NAME from homeassistant.core import HomeAssistant, split_entity_id import homeassistant.helpers.config_validation as cv @@ -23,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_TTS_SERVICE, CONF_ENTITY_ID), - PLATFORM_SCHEMA.extend( + NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME): cv.string, vol.Exclusive(CONF_TTS_SERVICE, ENTITY_LEGACY_PROVIDER_GROUP): cv.entity_id, From d4e93dd01dc076fcb3a54cb7228537ade02b532a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 Jun 2024 19:17:54 +0200 Subject: [PATCH 0144/2411] Validate new device identifiers and connections (#120413) --- homeassistant/helpers/device_registry.py | 110 ++++++++++++++- tests/helpers/test_device_registry.py | 164 +++++++++++++++++++++++ 2 files changed, 271 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 2a90d885d70..36249733f71 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -185,6 +185,35 @@ class DeviceInfoError(HomeAssistantError): self.domain = domain +class DeviceCollisionError(HomeAssistantError): + """Raised when a device collision is detected.""" + + +class DeviceIdentifierCollisionError(DeviceCollisionError): + """Raised when a device identifier collision is detected.""" + + def __init__( + self, identifiers: set[tuple[str, str]], existing_device: DeviceEntry + ) -> None: + """Initialize error.""" + super().__init__( + f"Identifiers {identifiers} already registered with {existing_device}" + ) + + +class DeviceConnectionCollisionError(DeviceCollisionError): + """Raised when a device connection collision is detected.""" + + def __init__( + self, normalized_connections: set[tuple[str, str]], existing_device: DeviceEntry + ) -> None: + """Initialize error.""" + super().__init__( + f"Connections {normalized_connections} " + f"already registered with {existing_device}" + ) + + def _validate_device_info( config_entry: ConfigEntry, device_info: DeviceInfo, @@ -759,6 +788,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): device = self.async_update_device( device.id, + allow_collisions=True, add_config_entry_id=config_entry_id, configuration_url=configuration_url, device_info_type=device_info_type, @@ -782,11 +812,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): return device @callback - def async_update_device( + def async_update_device( # noqa: C901 self, device_id: str, *, add_config_entry_id: str | UndefinedType = UNDEFINED, + # Temporary flag so we don't blow up when collisions are implicitly introduced + # by calls to async_get_or_create. Must not be set by integrations. + allow_collisions: bool = False, area_id: str | None | UndefinedType = UNDEFINED, configuration_url: str | URL | None | UndefinedType = UNDEFINED, device_info_type: str | UndefinedType = UNDEFINED, @@ -894,12 +927,36 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): new_values[attr_name] = old_value | setvalue old_values[attr_name] = old_value + if merge_connections is not UNDEFINED: + normalized_connections = self._validate_connections( + device_id, + merge_connections, + allow_collisions, + ) + old_connections = old.connections + if not normalized_connections.issubset(old_connections): + new_values["connections"] = old_connections | normalized_connections + old_values["connections"] = old_connections + + if merge_identifiers is not UNDEFINED: + merge_identifiers = self._validate_identifiers( + device_id, merge_identifiers, allow_collisions + ) + old_identifiers = old.identifiers + if not merge_identifiers.issubset(old_identifiers): + new_values["identifiers"] = old_identifiers | merge_identifiers + old_values["identifiers"] = old_identifiers + if new_connections is not UNDEFINED: - new_values["connections"] = _normalize_connections(new_connections) + new_values["connections"] = self._validate_connections( + device_id, new_connections, False + ) old_values["connections"] = old.connections if new_identifiers is not UNDEFINED: - new_values["identifiers"] = new_identifiers + new_values["identifiers"] = self._validate_identifiers( + device_id, new_identifiers, False + ) old_values["identifiers"] = old.identifiers if configuration_url is not UNDEFINED: @@ -955,6 +1012,53 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): return new + @callback + def _validate_connections( + self, + device_id: str, + connections: set[tuple[str, str]], + allow_collisions: bool, + ) -> set[tuple[str, str]]: + """Normalize and validate connections, raise on collision with other devices.""" + normalized_connections = _normalize_connections(connections) + if allow_collisions: + return normalized_connections + + for connection in normalized_connections: + # We need to iterate over each connection because if there is a + # conflict, the index will only see the last one and we will not + # be able to tell which one caused the conflict + if ( + existing_device := self.async_get_device(connections={connection}) + ) and existing_device.id != device_id: + raise DeviceConnectionCollisionError( + normalized_connections, existing_device + ) + + return normalized_connections + + @callback + def _validate_identifiers( + self, + device_id: str, + identifiers: set[tuple[str, str]], + allow_collisions: bool, + ) -> set[tuple[str, str]]: + """Validate identifiers, raise on collision with other devices.""" + if allow_collisions: + return identifiers + + for identifier in identifiers: + # We need to iterate over each identifier because if there is a + # conflict, the index will only see the last one and we will not + # be able to tell which one caused the conflict + if ( + existing_device := self.async_get_device(identifiers={identifier}) + ) and existing_device.id != device_id: + raise DeviceIdentifierCollisionError(identifiers, existing_device) + + return identifiers + @callback def async_remove_device(self, device_id: str) -> None: """Remove a device from the device registry.""" diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index b141e29f678..f8f10baad08 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -2630,3 +2630,167 @@ async def test_async_remove_device_thread_safety( await hass.async_add_executor_job( device_registry.async_remove_device, device.id ) + + +async def test_device_registry_connections_collision( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Test connection collisions in the device registry.""" + config_entry = MockConfigEntry() + config_entry.add_to_hass(hass) + + device1 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "none")}, + manufacturer="manufacturer", + model="model", + ) + device2 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "none")}, + manufacturer="manufacturer", + model="model", + ) + + assert device1.id == device2.id + + device3 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + + # Attempt to merge connection for device3 with the same + # connection that already exists in device1 + with pytest.raises( + HomeAssistantError, match=f"Connections.*already registered.*{device1.id}" + ): + device_registry.async_update_device( + device3.id, + merge_connections={ + (dr.CONNECTION_NETWORK_MAC, "EE:EE:EE:EE:EE:EE"), + (dr.CONNECTION_NETWORK_MAC, "none"), + }, + ) + + # Attempt to add new connections for device3 with the same + # connection that already exists in device1 + with pytest.raises( + HomeAssistantError, match=f"Connections.*already registered.*{device1.id}" + ): + device_registry.async_update_device( + device3.id, + new_connections={ + (dr.CONNECTION_NETWORK_MAC, "EE:EE:EE:EE:EE:EE"), + (dr.CONNECTION_NETWORK_MAC, "none"), + }, + ) + + device3_refetched = device_registry.async_get(device3.id) + assert device3_refetched.connections == set() + assert device3_refetched.identifiers == {("bridgeid", "0123")} + + device1_refetched = device_registry.async_get(device1.id) + assert device1_refetched.connections == {(dr.CONNECTION_NETWORK_MAC, "none")} + assert device1_refetched.identifiers == set() + + device2_refetched = device_registry.async_get(device2.id) + assert device2_refetched.connections == {(dr.CONNECTION_NETWORK_MAC, "none")} + assert device2_refetched.identifiers == set() + + assert device2_refetched.id == device1_refetched.id + assert len(device_registry.devices) == 2 + + # Attempt to implicitly merge connection for device3 with the same + # connection that already exists in device1 + device4 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "0123")}, + connections={ + (dr.CONNECTION_NETWORK_MAC, "EE:EE:EE:EE:EE:EE"), + (dr.CONNECTION_NETWORK_MAC, "none"), + }, + ) + assert len(device_registry.devices) == 2 + assert device4.id in (device1.id, device3.id) + + device3_refetched = device_registry.async_get(device3.id) + device1_refetched = device_registry.async_get(device1.id) + assert not device1_refetched.connections.isdisjoint(device3_refetched.connections) + + +async def test_device_registry_identifiers_collision( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Test identifiers collisions in the device registry.""" + config_entry = MockConfigEntry() + config_entry.add_to_hass(hass) + + device1 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + device2 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + + assert device1.id == device2.id + + device3 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "4567")}, + manufacturer="manufacturer", + model="model", + ) + + # Attempt to merge identifiers for device3 with the same + # connection that already exists in device1 + with pytest.raises( + HomeAssistantError, match=f"Identifiers.*already registered.*{device1.id}" + ): + device_registry.async_update_device( + device3.id, merge_identifiers={("bridgeid", "0123"), ("bridgeid", "8888")} + ) + + # Attempt to add new identifiers for device3 with the same + # connection that already exists in device1 + with pytest.raises( + HomeAssistantError, match=f"Identifiers.*already registered.*{device1.id}" + ): + device_registry.async_update_device( + device3.id, new_identifiers={("bridgeid", "0123"), ("bridgeid", "8888")} + ) + + device3_refetched = device_registry.async_get(device3.id) + assert device3_refetched.connections == set() + assert device3_refetched.identifiers == {("bridgeid", "4567")} + + device1_refetched = device_registry.async_get(device1.id) + assert device1_refetched.connections == set() + assert device1_refetched.identifiers == {("bridgeid", "0123")} + + device2_refetched = device_registry.async_get(device2.id) + assert device2_refetched.connections == set() + assert device2_refetched.identifiers == {("bridgeid", "0123")} + + assert device2_refetched.id == device1_refetched.id + assert len(device_registry.devices) == 2 + + # Attempt to implicitly merge identifiers for device3 with the same + # connection that already exists in device1 + device4 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "4567"), ("bridgeid", "0123")}, + ) + assert len(device_registry.devices) == 2 + assert device4.id in (device1.id, device3.id) + + device3_refetched = device_registry.async_get(device3.id) + device1_refetched = device_registry.async_get(device1.id) + assert not device1_refetched.identifiers.isdisjoint(device3_refetched.identifiers) From c4b277b6ab6a3b9ca0bed68316ff18a1dc00a9a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jun 2024 19:40:04 +0200 Subject: [PATCH 0145/2411] Small cleanups to ESPHome manager reconnect shutdown (#120401) --- homeassistant/components/esphome/manager.py | 33 ++++++++++----------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 5ab0265c1d4..e8d002fba9d 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -586,23 +586,6 @@ class ESPHomeManager: if entry.options.get(CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS): async_delete_issue(hass, DOMAIN, self.services_issue) - # Use async_listen instead of async_listen_once so that we don't deregister - # the callback twice when shutting down Home Assistant. - # "Unable to remove unknown listener - # .onetime_listener>" - # We only close the connection at the last possible moment - # when the CLOSE event is fired so anything using a Bluetooth - # proxy has a chance to shut down properly. - entry_data.cleanup_callbacks.append( - hass.bus.async_listen(EVENT_HOMEASSISTANT_CLOSE, self.on_stop) - ) - entry_data.cleanup_callbacks.append( - hass.bus.async_listen( - EVENT_LOGGING_CHANGED, - self._async_handle_logging_changed, - ) - ) - reconnect_logic = ReconnectLogic( client=self.cli, on_connect=self.on_connect, @@ -613,6 +596,21 @@ class ESPHomeManager: ) self.reconnect_logic = reconnect_logic + # Use async_listen instead of async_listen_once so that we don't deregister + # the callback twice when shutting down Home Assistant. + # "Unable to remove unknown listener + # .onetime_listener>" + # We only close the connection at the last possible moment + # when the CLOSE event is fired so anything using a Bluetooth + # proxy has a chance to shut down properly. + bus = hass.bus + cleanups = ( + bus.async_listen(EVENT_HOMEASSISTANT_CLOSE, self.on_stop), + bus.async_listen(EVENT_LOGGING_CHANGED, self._async_handle_logging_changed), + reconnect_logic.stop_callback, + ) + entry_data.cleanup_callbacks.extend(cleanups) + infos, services = await entry_data.async_load_from_store() if entry.unique_id: await entry_data.async_update_static_infos( @@ -628,7 +626,6 @@ class ESPHomeManager: ) await reconnect_logic.start() - entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) entry.async_on_unload( entry.add_update_listener(entry_data.async_update_listener) From 75c7ae7c699ab1c53d6da4d496005d0580ffedcf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 Jun 2024 20:00:48 +0200 Subject: [PATCH 0146/2411] Support in service descriptions for input sections (#116100) --- homeassistant/components/light/services.yaml | 180 +++++++++---------- homeassistant/components/light/strings.json | 18 +- homeassistant/helpers/service.py | 11 +- script/hassfest/services.py | 55 +++++- script/hassfest/translations.py | 7 + tests/helpers/test_service.py | 22 +++ 6 files changed, 198 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 6183d2a49df..2a1fbd11afd 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -199,45 +199,6 @@ turn_on: example: "[255, 100, 100]" selector: color_rgb: - rgbw_color: &rgbw_color - filter: *color_support - advanced: true - example: "[255, 100, 100, 50]" - selector: - object: - rgbww_color: &rgbww_color - filter: *color_support - advanced: true - example: "[255, 100, 100, 50, 70]" - selector: - object: - color_name: &color_name - filter: *color_support - advanced: true - selector: - select: - translation_key: color_name - options: *named_colors - hs_color: &hs_color - filter: *color_support - advanced: true - example: "[300, 70]" - selector: - object: - xy_color: &xy_color - filter: *color_support - advanced: true - example: "[0.52, 0.43]" - selector: - object: - color_temp: &color_temp - filter: *color_temp_support - advanced: true - selector: - color_temp: - unit: "mired" - min: 153 - max: 500 kelvin: &kelvin filter: *color_temp_support selector: @@ -245,13 +206,6 @@ turn_on: unit: "kelvin" min: 2000 max: 6500 - brightness: &brightness - filter: *brightness_support - advanced: true - selector: - number: - min: 0 - max: 255 brightness_pct: &brightness_pct filter: *brightness_support selector: @@ -259,13 +213,6 @@ turn_on: min: 0 max: 100 unit_of_measurement: "%" - brightness_step: - filter: *brightness_support - advanced: true - selector: - number: - min: -225 - max: 255 brightness_step_pct: filter: *brightness_support selector: @@ -273,39 +220,84 @@ turn_on: min: -100 max: 100 unit_of_measurement: "%" - white: &white - filter: - attribute: - supported_color_modes: - - light.ColorMode.WHITE - advanced: true - selector: - constant: - value: true - label: Enabled - profile: &profile - advanced: true - example: relax - selector: - text: - flash: &flash - filter: - supported_features: - - light.LightEntityFeature.FLASH - advanced: true - selector: - select: - options: - - label: "Long" - value: "long" - - label: "Short" - value: "short" effect: &effect filter: supported_features: - light.LightEntityFeature.EFFECT selector: text: + advanced_fields: + collapsed: true + fields: + rgbw_color: &rgbw_color + filter: *color_support + example: "[255, 100, 100, 50]" + selector: + object: + rgbww_color: &rgbww_color + filter: *color_support + example: "[255, 100, 100, 50, 70]" + selector: + object: + color_name: &color_name + filter: *color_support + selector: + select: + translation_key: color_name + options: *named_colors + hs_color: &hs_color + filter: *color_support + example: "[300, 70]" + selector: + object: + xy_color: &xy_color + filter: *color_support + example: "[0.52, 0.43]" + selector: + object: + color_temp: &color_temp + filter: *color_temp_support + selector: + color_temp: + unit: "mired" + min: 153 + max: 500 + brightness: &brightness + filter: *brightness_support + selector: + number: + min: 0 + max: 255 + brightness_step: + filter: *brightness_support + selector: + number: + min: -225 + max: 255 + white: &white + filter: + attribute: + supported_color_modes: + - light.ColorMode.WHITE + selector: + constant: + value: true + label: Enabled + profile: &profile + example: relax + selector: + text: + flash: &flash + filter: + supported_features: + - light.LightEntityFeature.FLASH + selector: + select: + options: + - label: "Long" + value: "long" + - label: "Short" + value: "short" turn_off: target: @@ -313,7 +305,10 @@ turn_off: domain: light fields: transition: *transition - flash: *flash + advanced_fields: + collapsed: true + fields: + flash: *flash toggle: target: @@ -322,16 +317,19 @@ toggle: fields: transition: *transition rgb_color: *rgb_color - rgbw_color: *rgbw_color - rgbww_color: *rgbww_color - color_name: *color_name - hs_color: *hs_color - xy_color: *xy_color - color_temp: *color_temp kelvin: *kelvin - brightness: *brightness brightness_pct: *brightness_pct - white: *white - profile: *profile - flash: *flash effect: *effect + advanced_fields: + collapsed: true + fields: + rgbw_color: *rgbw_color + rgbww_color: *rgbww_color + color_name: *color_name + hs_color: *hs_color + xy_color: *xy_color + color_temp: *color_temp + brightness: *brightness + white: *white + profile: *profile + flash: *flash diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 76156404991..b874e48406e 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -34,7 +34,8 @@ "field_white_description": "Set the light to white mode.", "field_white_name": "White", "field_xy_color_description": "Color in XY-format. A list of two decimal numbers between 0 and 1.", - "field_xy_color_name": "XY-color" + "field_xy_color_name": "XY-color", + "section_advanced_fields_name": "Advanced options" }, "device_automation": { "action_type": { @@ -354,6 +355,11 @@ "name": "[%key:component::light::common::field_effect_name%]", "description": "[%key:component::light::common::field_effect_description%]" } + }, + "sections": { + "advanced_fields": { + "name": "[%key:component::light::common::section_advanced_fields_name%]" + } } }, "turn_off": { @@ -368,6 +374,11 @@ "name": "[%key:component::light::common::field_flash_name%]", "description": "[%key:component::light::common::field_flash_description%]" } + }, + "sections": { + "advanced_fields": { + "name": "[%key:component::light::common::section_advanced_fields_name%]" + } } }, "toggle": { @@ -434,6 +445,11 @@ "name": "[%key:component::light::common::field_effect_name%]", "description": "[%key:component::light::common::field_effect_description%]" } + }, + "sections": { + "advanced_fields": { + "name": "[%key:component::light::common::section_advanced_fields_name%]" + } } } } diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 22f5e7f8710..35c682437cb 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -179,10 +179,19 @@ _FIELD_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +_SECTION_SCHEMA = vol.Schema( + { + vol.Required("fields"): vol.Schema({str: _FIELD_SCHEMA}), + }, + extra=vol.ALLOW_EXTRA, +) + _SERVICE_SCHEMA = vol.Schema( { vol.Optional("target"): vol.Any(TargetSelector.CONFIG_SCHEMA, None), - vol.Optional("fields"): vol.Schema({str: _FIELD_SCHEMA}), + vol.Optional("fields"): vol.Schema( + {str: vol.Any(_SECTION_SCHEMA, _FIELD_SCHEMA)} + ), }, extra=vol.ALLOW_EXTRA, ) diff --git a/script/hassfest/services.py b/script/hassfest/services.py index ea4503d5410..92fca14d373 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -26,6 +26,23 @@ def exists(value: Any) -> Any: return value +def unique_field_validator(fields: Any) -> Any: + """Validate the inputs don't have duplicate keys under different sections.""" + all_fields = set() + for key, value in fields.items(): + if value and "fields" in value: + for key in value["fields"]: + if key in all_fields: + raise vol.Invalid(f"Duplicate use of field {key} in service.") + all_fields.add(key) + else: + if key in all_fields: + raise vol.Invalid(f"Duplicate use of field {key} in service.") + all_fields.add(key) + + return fields + + CORE_INTEGRATION_FIELD_SCHEMA = vol.Schema( { vol.Optional("example"): exists, @@ -44,6 +61,13 @@ CORE_INTEGRATION_FIELD_SCHEMA = vol.Schema( } ) +CORE_INTEGRATION_SECTION_SCHEMA = vol.Schema( + { + vol.Optional("collapsed"): bool, + vol.Required("fields"): vol.Schema({str: CORE_INTEGRATION_FIELD_SCHEMA}), + } +) + CUSTOM_INTEGRATION_FIELD_SCHEMA = CORE_INTEGRATION_FIELD_SCHEMA.extend( { vol.Optional("description"): str, @@ -57,7 +81,17 @@ CORE_INTEGRATION_SERVICE_SCHEMA = vol.Any( vol.Optional("target"): vol.Any( selector.TargetSelector.CONFIG_SCHEMA, None ), - vol.Optional("fields"): vol.Schema({str: CORE_INTEGRATION_FIELD_SCHEMA}), + vol.Optional("fields"): vol.All( + vol.Schema( + { + str: vol.Any( + CORE_INTEGRATION_FIELD_SCHEMA, + CORE_INTEGRATION_SECTION_SCHEMA, + ) + } + ), + unique_field_validator, + ), } ), None, @@ -107,7 +141,7 @@ def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> bool return False -def validate_services(config: Config, integration: Integration) -> None: +def validate_services(config: Config, integration: Integration) -> None: # noqa: C901 """Validate services.""" try: data = load_yaml_dict(str(integration.path / "services.yaml")) @@ -200,6 +234,9 @@ def validate_services(config: Config, integration: Integration) -> None: # The same check is done for the description in each of the fields of the # service schema. for field_name, field_schema in service_schema.get("fields", {}).items(): + if "fields" in field_schema: + # This is a section + continue if "name" not in field_schema: try: strings["services"][service_name]["fields"][field_name]["name"] @@ -233,6 +270,20 @@ def validate_services(config: Config, integration: Integration) -> None: f"Service {service_name} has a field {field_name} with a selector with a translation key {translation_key} that is not in the translations file", ) + # The same check is done for the description in each of the sections of the + # service schema. + for section_name, section_schema in service_schema.get("fields", {}).items(): + if "fields" not in section_schema: + # This is not a section + continue + try: + strings["services"][service_name]["sections"][section_name]["name"] + except KeyError: + integration.add_error( + "services", + f"Service {service_name} has a section {section_name} with no name {error_msg_suffix}", + ) + def validate(integrations: dict[str, Integration], config: Config) -> None: """Handle dependencies for integrations.""" diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 965d1dc62b8..c39c070eba2 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -383,6 +383,13 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: }, slug_validator=translation_key_validator, ), + vol.Optional("sections"): cv.schema_with_slug_keys( + { + vol.Required("name"): str, + vol.Optional("description"): translation_value_validator, + }, + slug_validator=translation_key_validator, + ), }, slug_validator=translation_key_validator, ), diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 3e7d8e6ef03..9c5cda67725 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -990,6 +990,17 @@ async def test_async_get_all_descriptions_filter(hass: HomeAssistant) -> None: - light.ColorMode.COLOR_TEMP selector: number: + advanced_stuff: + fields: + temperature: + filter: + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME + attribute: + supported_color_modes: + - light.ColorMode.COLOR_TEMP + selector: + number: """ domain = "test_domain" @@ -1024,6 +1035,17 @@ async def test_async_get_all_descriptions_filter(hass: HomeAssistant) -> None: test_service_schema = { "description": "", "fields": { + "advanced_stuff": { + "fields": { + "temperature": { + "filter": { + "attribute": {"supported_color_modes": ["color_temp"]}, + "supported_features": [1], + }, + "selector": {"number": None}, + }, + }, + }, "temperature": { "filter": { "attribute": {"supported_color_modes": ["color_temp"]}, From 9dc26652ee3b2d5b26d609bd152568be29e5bb03 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 20:08:04 +0200 Subject: [PATCH 0147/2411] Fix gtfs typing (#120451) --- homeassistant/components/gtfs/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index a0a0f0ebc0e..fbc65050704 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -270,7 +270,7 @@ def get_next_departure( schedule: Any, start_station_id: Any, end_station_id: Any, - offset: cv.time_period, + offset: datetime.timedelta, include_tomorrow: bool = False, ) -> dict: """Get the next departure for the given schedule.""" @@ -405,7 +405,7 @@ def get_next_departure( item = {} for key in sorted(timetable.keys()): - if dt_util.parse_datetime(key) > now: + if (value := dt_util.parse_datetime(key)) is not None and value > now: item = timetable[key] _LOGGER.debug( "Departure found for station %s @ %s -> %s", start_station_id, key, item From 7d2ae5b3a5a0be2076aac39d543fb7667e52d123 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 Jun 2024 20:15:11 +0200 Subject: [PATCH 0148/2411] Add WS command blueprint/substitute (#119890) --- .../components/blueprint/websocket_api.py | 107 ++++++++++++---- .../blueprint/test_websocket_api.py | 119 +++++++++++++++++- 2 files changed, 200 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py index 98cc8131166..9d3329d8195 100644 --- a/homeassistant/components/blueprint/websocket_api.py +++ b/homeassistant/components/blueprint/websocket_api.py @@ -3,6 +3,8 @@ from __future__ import annotations import asyncio +from collections.abc import Callable, Coroutine +import functools from typing import Any, cast import voluptuous as vol @@ -15,16 +17,50 @@ from homeassistant.util import yaml from . import importer, models from .const import DOMAIN -from .errors import FailedToLoad, FileAlreadyExists +from .errors import BlueprintException, FailedToLoad, FileAlreadyExists @callback def async_setup(hass: HomeAssistant) -> None: """Set up the websocket API.""" - websocket_api.async_register_command(hass, ws_list_blueprints) - websocket_api.async_register_command(hass, ws_import_blueprint) - websocket_api.async_register_command(hass, ws_save_blueprint) websocket_api.async_register_command(hass, ws_delete_blueprint) + websocket_api.async_register_command(hass, ws_import_blueprint) + websocket_api.async_register_command(hass, ws_list_blueprints) + websocket_api.async_register_command(hass, ws_save_blueprint) + websocket_api.async_register_command(hass, ws_substitute_blueprint) + + +def _ws_with_blueprint_domain( + func: Callable[ + [ + HomeAssistant, + websocket_api.ActiveConnection, + dict[str, Any], + models.DomainBlueprints, + ], + Coroutine[Any, Any, None], + ], +) -> websocket_api.AsyncWebSocketCommandHandler: + """Decorate a function to pass in the domain blueprints.""" + + @functools.wraps(func) + async def with_domain_blueprints( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], + ) -> None: + domain_blueprints: models.DomainBlueprints | None = hass.data.get( + DOMAIN, {} + ).get(msg["domain"]) + if domain_blueprints is None: + connection.send_error( + msg["id"], websocket_api.ERR_INVALID_FORMAT, "Unsupported domain" + ) + return + + await func(hass, connection, msg, domain_blueprints) + + return with_domain_blueprints @websocket_api.websocket_command( @@ -124,23 +160,18 @@ async def ws_import_blueprint( } ) @websocket_api.async_response +@_ws_with_blueprint_domain async def ws_save_blueprint( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any], + domain_blueprints: models.DomainBlueprints, ) -> None: """Save a blueprint.""" path = msg["path"] domain = msg["domain"] - domain_blueprints: dict[str, models.DomainBlueprints] = hass.data.get(DOMAIN, {}) - - if domain not in domain_blueprints: - connection.send_error( - msg["id"], websocket_api.ERR_INVALID_FORMAT, "Unsupported domain" - ) - try: yaml_data = cast(dict[str, Any], yaml.parse_yaml(msg["yaml"])) blueprint = models.Blueprint(yaml_data, expected_domain=domain) @@ -154,7 +185,7 @@ async def ws_save_blueprint( path = f"{path}.yaml" try: - overrides_existing = await domain_blueprints[domain].async_add_blueprint( + overrides_existing = await domain_blueprints.async_add_blueprint( blueprint, path, allow_override=msg.get("allow_override", False) ) except FileAlreadyExists: @@ -180,25 +211,16 @@ async def ws_save_blueprint( } ) @websocket_api.async_response +@_ws_with_blueprint_domain async def ws_delete_blueprint( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any], + domain_blueprints: models.DomainBlueprints, ) -> None: """Delete a blueprint.""" - - path = msg["path"] - domain = msg["domain"] - - domain_blueprints: dict[str, models.DomainBlueprints] = hass.data.get(DOMAIN, {}) - - if domain not in domain_blueprints: - connection.send_error( - msg["id"], websocket_api.ERR_INVALID_FORMAT, "Unsupported domain" - ) - try: - await domain_blueprints[domain].async_remove_blueprint(path) + await domain_blueprints.async_remove_blueprint(msg["path"]) except OSError as err: connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err)) return @@ -206,3 +228,40 @@ async def ws_delete_blueprint( connection.send_result( msg["id"], ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "blueprint/substitute", + vol.Required("domain"): cv.string, + vol.Required("path"): cv.path, + vol.Required("input"): dict, + } +) +@websocket_api.async_response +@_ws_with_blueprint_domain +async def ws_substitute_blueprint( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], + domain_blueprints: models.DomainBlueprints, +) -> None: + """Process a blueprinted config to allow editing.""" + + blueprint_config = {"use_blueprint": {"path": msg["path"], "input": msg["input"]}} + + try: + blueprint_inputs = await domain_blueprints.async_inputs_from_config( + blueprint_config + ) + except BlueprintException as err: + connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err)) + return + + try: + config = blueprint_inputs.async_substitute() + except yaml.UndefinedSubstitution as err: + connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err)) + return + + connection.send_result(msg["id"], {"substituted_config": config}) diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 1f684b451ed..13615803569 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -9,7 +9,7 @@ import yaml from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from homeassistant.util.yaml import parse_yaml +from homeassistant.util.yaml import UndefinedSubstitution, parse_yaml from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import WebSocketGenerator @@ -454,9 +454,124 @@ async def test_delete_blueprint_in_use_by_script( msg = await client.receive_json() assert not unlink_mock.mock_calls - assert msg["id"] == 9 assert not msg["success"] assert msg["error"] == { "code": "home_assistant_error", "message": "Blueprint in use", } + + +async def test_substituting_blueprint_inputs( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test substituting blueprint inputs.""" + client = await hass_ws_client(hass) + await client.send_json_auto_id( + { + "type": "blueprint/substitute", + "domain": "automation", + "path": "test_event_service.yaml", + "input": { + "trigger_event": "test_event", + "service_to_call": "test.automation", + "a_number": 5, + }, + } + ) + + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"]["substituted_config"] == { + "action": { + "entity_id": "light.kitchen", + "service": "test.automation", + }, + "trigger": { + "event_type": "test_event", + "platform": "event", + }, + } + + +async def test_substituting_blueprint_inputs_unknown_domain( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test substituting blueprint inputs.""" + client = await hass_ws_client(hass) + await client.send_json_auto_id( + { + "type": "blueprint/substitute", + "domain": "donald_duck", + "path": "test_event_service.yaml", + "input": { + "trigger_event": "test_event", + "service_to_call": "test.automation", + "a_number": 5, + }, + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"] == { + "code": "invalid_format", + "message": "Unsupported domain", + } + + +async def test_substituting_blueprint_inputs_incomplete_input( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test substituting blueprint inputs.""" + client = await hass_ws_client(hass) + await client.send_json_auto_id( + { + "type": "blueprint/substitute", + "domain": "automation", + "path": "test_event_service.yaml", + "input": { + "service_to_call": "test.automation", + "a_number": 5, + }, + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"] == { + "code": "unknown_error", + "message": "Missing input trigger_event", + } + + +async def test_substituting_blueprint_inputs_incomplete_input_2( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test substituting blueprint inputs.""" + client = await hass_ws_client(hass) + with patch( + "homeassistant.components.blueprint.models.BlueprintInputs.async_substitute", + side_effect=UndefinedSubstitution("blah"), + ): + await client.send_json_auto_id( + { + "type": "blueprint/substitute", + "domain": "automation", + "path": "test_event_service.yaml", + "input": { + "trigger_event": "test_event", + "service_to_call": "test.automation", + "a_number": 5, + }, + } + ) + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"] == { + "code": "unknown_error", + "message": "No substitution found for input blah", + } From f4b124f5f15f3df7474ed76027db2ddf0f318193 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 20:17:38 +0200 Subject: [PATCH 0149/2411] Fix invalid schemas (#120450) --- homeassistant/components/elkm1/__init__.py | 2 +- .../components/mobile_app/webhook.py | 24 ++++++++++--------- homeassistant/components/ombi/__init__.py | 22 +++++++++-------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index fff40b6ad73..b66a4ce2ed8 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -171,8 +171,8 @@ DEVICE_SCHEMA = vol.All( vol.Optional(CONF_THERMOSTAT, default={}): DEVICE_SCHEMA_SUBDOMAIN, vol.Optional(CONF_ZONE, default={}): DEVICE_SCHEMA_SUBDOMAIN, }, - _host_validator, ), + _host_validator, ) CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index e7cccd0f151..e93b4c5ea99 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -406,18 +406,20 @@ async def webhook_render_template( @WEBHOOK_COMMANDS.register("update_location") @validate_schema( - vol.Schema( + vol.All( cv.key_dependency(ATTR_GPS, ATTR_GPS_ACCURACY), - { - vol.Optional(ATTR_LOCATION_NAME): cv.string, - vol.Optional(ATTR_GPS): cv.gps, - vol.Optional(ATTR_GPS_ACCURACY): cv.positive_int, - vol.Optional(ATTR_BATTERY): cv.positive_int, - vol.Optional(ATTR_SPEED): cv.positive_int, - vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), - vol.Optional(ATTR_COURSE): cv.positive_int, - vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int, - }, + vol.Schema( + { + vol.Optional(ATTR_LOCATION_NAME): cv.string, + vol.Optional(ATTR_GPS): cv.gps, + vol.Optional(ATTR_GPS_ACCURACY): cv.positive_int, + vol.Optional(ATTR_BATTERY): cv.positive_int, + vol.Optional(ATTR_SPEED): cv.positive_int, + vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), + vol.Optional(ATTR_COURSE): cv.positive_int, + vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int, + }, + ), ) ) async def webhook_update_location( diff --git a/homeassistant/components/ombi/__init__.py b/homeassistant/components/ombi/__init__.py index 719efdc8ae3..a4cbe39f3e0 100644 --- a/homeassistant/components/ombi/__init__.py +++ b/homeassistant/components/ombi/__init__.py @@ -61,16 +61,18 @@ SUBMIT_TV_REQUEST_SERVICE_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Exclusive(CONF_API_KEY, "auth"): cv.string, - vol.Exclusive(CONF_PASSWORD, "auth"): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_URLBASE, default=DEFAULT_URLBASE): urlbase, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - }, + DOMAIN: vol.All( + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Exclusive(CONF_API_KEY, "auth"): cv.string, + vol.Exclusive(CONF_PASSWORD, "auth"): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_URLBASE, default=DEFAULT_URLBASE): urlbase, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + } + ), cv.has_at_least_one_key("auth"), ) }, From 9bc436185595bb7be3ea57d91179bec7e38fc4d4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 25 Jun 2024 20:17:51 +0200 Subject: [PATCH 0150/2411] Bump Knocki to 0.2.0 (#120447) --- homeassistant/components/knocki/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knocki/manifest.json b/homeassistant/components/knocki/manifest.json index bf4dcea4b67..e78e9856d62 100644 --- a/homeassistant/components/knocki/manifest.json +++ b/homeassistant/components/knocki/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "cloud_push", "loggers": ["knocki"], - "requirements": ["knocki==0.1.5"] + "requirements": ["knocki==0.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7c6f39ed88b..b75555283a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1206,7 +1206,7 @@ kegtron-ble==0.4.0 kiwiki-client==0.1.1 # homeassistant.components.knocki -knocki==0.1.5 +knocki==0.2.0 # homeassistant.components.knx knx-frontend==2024.1.20.105944 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bc9a740278..37b3700372b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -984,7 +984,7 @@ justnimbus==0.7.4 kegtron-ble==0.4.0 # homeassistant.components.knocki -knocki==0.1.5 +knocki==0.2.0 # homeassistant.components.knx knx-frontend==2024.1.20.105944 From 4290a1fcb5675c618a71093222b232a39239f95f Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 25 Jun 2024 22:01:21 +0200 Subject: [PATCH 0151/2411] Upgrade tplink with new platforms, features and device support (#120060) Co-authored-by: Teemu Rytilahti Co-authored-by: sdb9696 Co-authored-by: Steven B <51370195+sdb9696@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: Teemu R. --- homeassistant/components/tplink/README.md | 34 + homeassistant/components/tplink/__init__.py | 125 ++- .../components/tplink/binary_sensor.py | 96 +++ homeassistant/components/tplink/button.py | 69 ++ homeassistant/components/tplink/climate.py | 140 ++++ .../components/tplink/config_flow.py | 56 +- homeassistant/components/tplink/const.py | 22 +- .../components/tplink/coordinator.py | 8 +- .../components/tplink/diagnostics.py | 12 +- homeassistant/components/tplink/entity.py | 375 ++++++++- homeassistant/components/tplink/fan.py | 111 +++ homeassistant/components/tplink/icons.json | 99 +++ homeassistant/components/tplink/light.py | 190 +++-- homeassistant/components/tplink/manifest.json | 30 +- homeassistant/components/tplink/number.py | 108 +++ homeassistant/components/tplink/select.py | 95 +++ homeassistant/components/tplink/sensor.py | 225 +++-- homeassistant/components/tplink/strings.json | 140 +++- homeassistant/components/tplink/switch.py | 182 ++-- homeassistant/generated/dhcp.py | 35 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tplink/__init__.py | 492 +++++++---- tests/components/tplink/conftest.py | 12 +- .../components/tplink/fixtures/features.json | 287 +++++++ .../tplink/snapshots/test_binary_sensor.ambr | 369 ++++++++ .../tplink/snapshots/test_button.ambr | 127 +++ .../tplink/snapshots/test_climate.ambr | 94 +++ .../components/tplink/snapshots/test_fan.ambr | 194 +++++ .../tplink/snapshots/test_number.ambr | 255 ++++++ .../tplink/snapshots/test_select.ambr | 238 ++++++ .../tplink/snapshots/test_sensor.ambr | 790 ++++++++++++++++++ .../tplink/snapshots/test_switch.ambr | 311 +++++++ tests/components/tplink/test_binary_sensor.py | 124 +++ tests/components/tplink/test_button.py | 153 ++++ tests/components/tplink/test_climate.py | 226 +++++ tests/components/tplink/test_config_flow.py | 54 +- tests/components/tplink/test_diagnostics.py | 10 +- tests/components/tplink/test_fan.py | 154 ++++ tests/components/tplink/test_init.py | 190 ++++- tests/components/tplink/test_light.py | 427 ++++++---- tests/components/tplink/test_number.py | 163 ++++ tests/components/tplink/test_select.py | 158 ++++ tests/components/tplink/test_sensor.py | 233 +++++- tests/components/tplink/test_switch.py | 160 +++- 45 files changed, 6528 insertions(+), 849 deletions(-) create mode 100644 homeassistant/components/tplink/README.md create mode 100644 homeassistant/components/tplink/binary_sensor.py create mode 100644 homeassistant/components/tplink/button.py create mode 100644 homeassistant/components/tplink/climate.py create mode 100644 homeassistant/components/tplink/fan.py create mode 100644 homeassistant/components/tplink/number.py create mode 100644 homeassistant/components/tplink/select.py create mode 100644 tests/components/tplink/fixtures/features.json create mode 100644 tests/components/tplink/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/tplink/snapshots/test_button.ambr create mode 100644 tests/components/tplink/snapshots/test_climate.ambr create mode 100644 tests/components/tplink/snapshots/test_fan.ambr create mode 100644 tests/components/tplink/snapshots/test_number.ambr create mode 100644 tests/components/tplink/snapshots/test_select.ambr create mode 100644 tests/components/tplink/snapshots/test_sensor.ambr create mode 100644 tests/components/tplink/snapshots/test_switch.ambr create mode 100644 tests/components/tplink/test_binary_sensor.py create mode 100644 tests/components/tplink/test_button.py create mode 100644 tests/components/tplink/test_climate.py create mode 100644 tests/components/tplink/test_fan.py create mode 100644 tests/components/tplink/test_number.py create mode 100644 tests/components/tplink/test_select.py diff --git a/homeassistant/components/tplink/README.md b/homeassistant/components/tplink/README.md new file mode 100644 index 00000000000..129d9e7fcce --- /dev/null +++ b/homeassistant/components/tplink/README.md @@ -0,0 +1,34 @@ +# TPLink Integration + +This document covers details that new contributors may find helpful when getting started. + +## Modules vs Features + +The python-kasa library which this integration depends on exposes functionality via modules and features. +The `Module` APIs encapsulate groups of functionality provided by a device, +e.g. Light which has multiple attributes and methods such as `set_hsv`, `brightness` etc. +The `features` encapsulate unitary functions and allow for introspection. +e.g. `on_since`, `voltage` etc. + +If the integration implements a platform that presents single functions or data points, such as `sensor`, +`button`, `switch` it uses features. +If it's implementing a platform with more complex functionality like `light`, `fan` or `climate` it will +use modules. + +## Adding new entities + +All feature-based entities are created based on the information from the upstream library. +If you want to add new feature, it needs to be implemented at first in there. +After the feature is exposed by the upstream library, +it needs to be added to the `_DESCRIPTIONS` list of the corresponding platform. +The integration logs missing descriptions on features supported by the device to help spotting them. + +In many cases it is enough to define the `key` (corresponding to upstream `feature.id`), +but you can pass more information for nicer user experience: +* `device_class` and `state_class` should be set accordingly for binary_sensor and sensor +* If no matching classes are available, you need to update `strings.json` and `icons.json` +When doing so, do not forget to run `script/setup` to generate the translations. + +Other information like the category and whether to enable per default are read from the feature, +as are information about units and display precision hints. + diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index fbb176b2d5f..764867f0bee 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -9,14 +9,15 @@ from typing import Any from aiohttp import ClientSession from kasa import ( - AuthenticationException, + AuthenticationError, Credentials, + Device, DeviceConfig, Discover, - SmartDevice, - SmartDeviceException, + KasaException, ) from kasa.httpclient import get_cookie_jar +from kasa.iot import IotStrip from homeassistant import config_entries from homeassistant.components import network @@ -51,6 +52,8 @@ from .const import ( from .coordinator import TPLinkDataUpdateCoordinator from .models import TPLinkData +type TPLinkConfigEntry = ConfigEntry[TPLinkData] + DISCOVERY_INTERVAL = timedelta(minutes=15) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) @@ -67,7 +70,7 @@ def create_async_tplink_clientsession(hass: HomeAssistant) -> ClientSession: @callback def async_trigger_discovery( hass: HomeAssistant, - discovered_devices: dict[str, SmartDevice], + discovered_devices: dict[str, Device], ) -> None: """Trigger config flows for discovered devices.""" for formatted_mac, device in discovered_devices.items(): @@ -87,7 +90,7 @@ def async_trigger_discovery( ) -async def async_discover_devices(hass: HomeAssistant) -> dict[str, SmartDevice]: +async def async_discover_devices(hass: HomeAssistant) -> dict[str, Device]: """Discover TPLink devices on configured network interfaces.""" credentials = await get_credentials(hass) @@ -101,7 +104,7 @@ async def async_discover_devices(hass: HomeAssistant) -> dict[str, SmartDevice]: ) for address in broadcast_addresses ] - discovered_devices: dict[str, SmartDevice] = {} + discovered_devices: dict[str, Device] = {} for device_list in await asyncio.gather(*tasks): for device in device_list.values(): discovered_devices[dr.format_mac(device.mac)] = device @@ -126,7 +129,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bool: """Set up TPLink from a config entry.""" host: str = entry.data[CONF_HOST] credentials = await get_credentials(hass) @@ -135,7 +138,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if config_dict := entry.data.get(CONF_DEVICE_CONFIG): try: config = DeviceConfig.from_dict(config_dict) - except SmartDeviceException: + except KasaException: _LOGGER.warning( "Invalid connection type dict for %s: %s", host, config_dict ) @@ -151,10 +154,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if credentials: config.credentials = credentials try: - device: SmartDevice = await SmartDevice.connect(config=config) - except AuthenticationException as ex: + device: Device = await Device.connect(config=config) + except AuthenticationError as ex: raise ConfigEntryAuthFailed from ex - except SmartDeviceException as ex: + except KasaException as ex: raise ConfigEntryNotReady from ex device_config_dict = device.config.to_dict( @@ -189,7 +192,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: parent_coordinator = TPLinkDataUpdateCoordinator(hass, device, timedelta(seconds=5)) child_coordinators: list[TPLinkDataUpdateCoordinator] = [] - if device.is_strip: + # The iot HS300 allows a limited number of concurrent requests and fetching the + # emeter information requires separate ones so create child coordinators here. + if isinstance(device, IotStrip): child_coordinators = [ # The child coordinators only update energy data so we can # set a longer update interval to avoid flooding the device @@ -197,27 +202,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for child in device.children ] - hass.data[DOMAIN][entry.entry_id] = TPLinkData( - parent_coordinator, child_coordinators - ) + entry.runtime_data = TPLinkData(parent_coordinator, child_coordinators) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bool: """Unload a config entry.""" - hass_data: dict[str, Any] = hass.data[DOMAIN] - data: TPLinkData = hass_data[entry.entry_id] + data = entry.runtime_data device = data.parent_coordinator.device - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass_data.pop(entry.entry_id) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) await device.protocol.close() return unload_ok -def legacy_device_id(device: SmartDevice) -> str: +def legacy_device_id(device: Device) -> str: """Convert the device id so it matches what was used in the original version.""" device_id: str = device.device_id # Plugs are prefixed with the mac in python-kasa but not @@ -227,6 +228,24 @@ def legacy_device_id(device: SmartDevice) -> str: return device_id.split("_")[1] +def get_device_name(device: Device, parent: Device | None = None) -> str: + """Get a name for the device. alias can be none on some devices.""" + if device.alias: + return device.alias + # Return the child device type with an index if there's more than one child device + # of the same type. i.e. Devices like the ks240 with one child of each type + # skip the suffix + if parent: + devices = [ + child.device_id + for child in parent.children + if child.device_type is device.device_type + ] + suffix = f" {devices.index(device.device_id) + 1}" if len(devices) > 1 else "" + return f"{device.device_type.value.capitalize()}{suffix}" + return f"Unnamed {device.model}" + + async def get_credentials(hass: HomeAssistant) -> Credentials | None: """Retrieve the credentials from hass data.""" if DOMAIN in hass.data and CONF_AUTHENTICATION in hass.data[DOMAIN]: @@ -247,3 +266,67 @@ async def set_credentials(hass: HomeAssistant, username: str, password: str) -> def mac_alias(mac: str) -> str: """Convert a MAC address to a short address for the UI.""" return mac.replace(":", "")[-4:].upper() + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + version = config_entry.version + minor_version = config_entry.minor_version + + _LOGGER.debug("Migrating from version %s.%s", version, minor_version) + + if version == 1 and minor_version < 3: + # Previously entities on child devices added themselves to the parent + # device and set their device id as identifiers along with mac + # as a connection which creates a single device entry linked by all + # identifiers. Now we create separate devices connected with via_device + # so the identifier linkage must be removed otherwise the devices will + # always be linked into one device. + dev_reg = dr.async_get(hass) + for device in dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id): + new_identifiers: set[tuple[str, str]] | None = None + if len(device.identifiers) > 1 and ( + mac := next( + iter( + [ + conn[1] + for conn in device.connections + if conn[0] == dr.CONNECTION_NETWORK_MAC + ] + ), + None, + ) + ): + for identifier in device.identifiers: + # Previously only iot devices that use the MAC address as + # device_id had child devices so check for mac as the + # parent device. + if identifier[0] == DOMAIN and identifier[1].upper() == mac.upper(): + new_identifiers = {identifier} + break + if new_identifiers: + dev_reg.async_update_device( + device.id, new_identifiers=new_identifiers + ) + _LOGGER.debug( + "Replaced identifiers for device %s (%s): %s with: %s", + device.name, + device.model, + device.identifiers, + new_identifiers, + ) + else: + # No match on mac so raise an error. + _LOGGER.error( + "Unable to replace identifiers for device %s (%s): %s", + device.name, + device.model, + device.identifiers, + ) + + minor_version = 3 + hass.config_entries.async_update_entry(config_entry, minor_version=3) + + _LOGGER.debug("Migration to version %s.%s successful", version, minor_version) + + return True diff --git a/homeassistant/components/tplink/binary_sensor.py b/homeassistant/components/tplink/binary_sensor.py new file mode 100644 index 00000000000..97bb794a8f9 --- /dev/null +++ b/homeassistant/components/tplink/binary_sensor.py @@ -0,0 +1,96 @@ +"""Support for TPLink binary sensors.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Final + +from kasa import Feature + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import TPLinkConfigEntry +from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription + + +@dataclass(frozen=True, kw_only=True) +class TPLinkBinarySensorEntityDescription( + BinarySensorEntityDescription, TPLinkFeatureEntityDescription +): + """Base class for a TPLink feature based sensor entity description.""" + + +BINARY_SENSOR_DESCRIPTIONS: Final = ( + TPLinkBinarySensorEntityDescription( + key="overheated", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + TPLinkBinarySensorEntityDescription( + key="battery_low", + device_class=BinarySensorDeviceClass.BATTERY, + ), + TPLinkBinarySensorEntityDescription( + key="cloud_connection", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), + # To be replaced & disabled per default by the upcoming update platform. + TPLinkBinarySensorEntityDescription( + key="update_available", + device_class=BinarySensorDeviceClass.UPDATE, + ), + TPLinkBinarySensorEntityDescription( + key="temperature_warning", + ), + TPLinkBinarySensorEntityDescription( + key="humidity_warning", + ), + TPLinkBinarySensorEntityDescription( + key="is_open", + device_class=BinarySensorDeviceClass.DOOR, + ), + TPLinkBinarySensorEntityDescription( + key="water_alert", + device_class=BinarySensorDeviceClass.MOISTURE, + ), +) + +BINARYSENSOR_DESCRIPTIONS_MAP = {desc.key: desc for desc in BINARY_SENSOR_DESCRIPTIONS} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: TPLinkConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors.""" + data = config_entry.runtime_data + parent_coordinator = data.parent_coordinator + children_coordinators = data.children_coordinators + device = parent_coordinator.device + + entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children( + device=device, + coordinator=parent_coordinator, + feature_type=Feature.Type.BinarySensor, + entity_class=TPLinkBinarySensorEntity, + descriptions=BINARYSENSOR_DESCRIPTIONS_MAP, + child_coordinators=children_coordinators, + ) + async_add_entities(entities) + + +class TPLinkBinarySensorEntity(CoordinatedTPLinkFeatureEntity, BinarySensorEntity): + """Representation of a TPLink binary sensor.""" + + entity_description: TPLinkBinarySensorEntityDescription + + @callback + def _async_update_attrs(self) -> None: + """Update the entity's attributes.""" + self._attr_is_on = self._feature.value diff --git a/homeassistant/components/tplink/button.py b/homeassistant/components/tplink/button.py new file mode 100644 index 00000000000..4dcc27858a8 --- /dev/null +++ b/homeassistant/components/tplink/button.py @@ -0,0 +1,69 @@ +"""Support for TPLink button entities.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Final + +from kasa import Feature + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import TPLinkConfigEntry +from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription + + +@dataclass(frozen=True, kw_only=True) +class TPLinkButtonEntityDescription( + ButtonEntityDescription, TPLinkFeatureEntityDescription +): + """Base class for a TPLink feature based button entity description.""" + + +BUTTON_DESCRIPTIONS: Final = [ + TPLinkButtonEntityDescription( + key="test_alarm", + ), + TPLinkButtonEntityDescription( + key="stop_alarm", + ), +] + +BUTTON_DESCRIPTIONS_MAP = {desc.key: desc for desc in BUTTON_DESCRIPTIONS} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: TPLinkConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up buttons.""" + data = config_entry.runtime_data + parent_coordinator = data.parent_coordinator + children_coordinators = data.children_coordinators + device = parent_coordinator.device + + entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children( + device=device, + coordinator=parent_coordinator, + feature_type=Feature.Type.Action, + entity_class=TPLinkButtonEntity, + descriptions=BUTTON_DESCRIPTIONS_MAP, + child_coordinators=children_coordinators, + ) + async_add_entities(entities) + + +class TPLinkButtonEntity(CoordinatedTPLinkFeatureEntity, ButtonEntity): + """Representation of a TPLink button entity.""" + + entity_description: TPLinkButtonEntityDescription + + async def async_press(self) -> None: + """Execute action.""" + await self._feature.set_value(True) + + def _async_update_attrs(self) -> None: + """No need to update anything.""" diff --git a/homeassistant/components/tplink/climate.py b/homeassistant/components/tplink/climate.py new file mode 100644 index 00000000000..99a8c43fac3 --- /dev/null +++ b/homeassistant/components/tplink/climate.py @@ -0,0 +1,140 @@ +"""Support for TP-Link thermostats.""" + +from __future__ import annotations + +import logging +from typing import Any, cast + +from kasa import Device, DeviceType +from kasa.smart.modules.temperaturecontrol import ThermostatState + +from homeassistant.components.climate import ( + ATTR_TEMPERATURE, + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.const import PRECISION_WHOLE +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import TPLinkConfigEntry +from .const import UNIT_MAPPING +from .coordinator import TPLinkDataUpdateCoordinator +from .entity import CoordinatedTPLinkEntity, async_refresh_after + +# Upstream state to HVACAction +STATE_TO_ACTION = { + ThermostatState.Idle: HVACAction.IDLE, + ThermostatState.Heating: HVACAction.HEATING, + ThermostatState.Off: HVACAction.OFF, +} + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: TPLinkConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up climate entities.""" + data = config_entry.runtime_data + parent_coordinator = data.parent_coordinator + device = parent_coordinator.device + + # As there are no standalone thermostats, we just iterate over the children. + async_add_entities( + TPLinkClimateEntity(child, parent_coordinator, parent=device) + for child in device.children + if child.device_type is DeviceType.Thermostat + ) + + +class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity): + """Representation of a TPLink thermostat.""" + + _attr_name = None + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) + _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] + _attr_precision = PRECISION_WHOLE + + # This disables the warning for async_turn_{on,off}, can be removed later. + _enable_turn_on_off_backwards_compatibility = False + + def __init__( + self, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + parent: Device, + ) -> None: + """Initialize the climate entity.""" + super().__init__(device, coordinator, parent=parent) + self._state_feature = self._device.features["state"] + self._mode_feature = self._device.features["thermostat_mode"] + self._temp_feature = self._device.features["temperature"] + self._target_feature = self._device.features["target_temperature"] + + self._attr_min_temp = self._target_feature.minimum_value + self._attr_max_temp = self._target_feature.maximum_value + self._attr_temperature_unit = UNIT_MAPPING[cast(str, self._temp_feature.unit)] + + @async_refresh_after + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set target temperature.""" + await self._target_feature.set_value(int(kwargs[ATTR_TEMPERATURE])) + + @async_refresh_after + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set hvac mode (heat/off).""" + if hvac_mode is HVACMode.HEAT: + await self._state_feature.set_value(True) + elif hvac_mode is HVACMode.OFF: + await self._state_feature.set_value(False) + else: + raise ServiceValidationError(f"Tried to set unsupported mode: {hvac_mode}") + + @async_refresh_after + async def async_turn_on(self) -> None: + """Turn heating on.""" + await self._state_feature.set_value(True) + + @async_refresh_after + async def async_turn_off(self) -> None: + """Turn heating off.""" + await self._state_feature.set_value(False) + + @callback + def _async_update_attrs(self) -> None: + """Update the entity's attributes.""" + self._attr_current_temperature = self._temp_feature.value + self._attr_target_temperature = self._target_feature.value + + self._attr_hvac_mode = ( + HVACMode.HEAT if self._state_feature.value else HVACMode.OFF + ) + + if ( + self._mode_feature.value not in STATE_TO_ACTION + and self._attr_hvac_action is not HVACAction.OFF + ): + _LOGGER.warning( + "Unknown thermostat state, defaulting to OFF: %s", + self._mode_feature.value, + ) + self._attr_hvac_action = HVACAction.OFF + return + + self._attr_hvac_action = STATE_TO_ACTION[self._mode_feature.value] + + def _get_unique_id(self) -> str: + """Return unique id.""" + return f"{self._device.device_id}_climate" diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index df3291561fa..7bead2207a3 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -6,13 +6,13 @@ from collections.abc import Mapping from typing import Any from kasa import ( - AuthenticationException, + AuthenticationError, Credentials, + Device, DeviceConfig, Discover, - SmartDevice, - SmartDeviceException, - TimeoutException, + KasaException, + TimeoutError, ) import voluptuous as vol @@ -55,13 +55,13 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for tplink.""" VERSION = 1 - MINOR_VERSION = 2 + MINOR_VERSION = 3 reauth_entry: ConfigEntry | None = None def __init__(self) -> None: """Initialize the config flow.""" - self._discovered_devices: dict[str, SmartDevice] = {} - self._discovered_device: SmartDevice | None = None + self._discovered_devices: dict[str, Device] = {} + self._discovered_device: Device | None = None async def async_step_dhcp( self, discovery_info: dhcp.DhcpServiceInfo @@ -129,9 +129,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): await self._async_try_discover_and_update( host, credentials, raise_on_progress=True ) - except AuthenticationException: + except AuthenticationError: return await self.async_step_discovery_auth_confirm() - except SmartDeviceException: + except KasaException: return self.async_abort(reason="cannot_connect") return await self.async_step_discovery_confirm() @@ -149,7 +149,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): device = await self._async_try_connect( self._discovered_device, credentials ) - except AuthenticationException: + except AuthenticationError: pass # Authentication exceptions should continue to the rest of the step else: self._discovered_device = device @@ -165,10 +165,10 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): device = await self._async_try_connect( self._discovered_device, credentials ) - except AuthenticationException as ex: + except AuthenticationError as ex: errors[CONF_PASSWORD] = "invalid_auth" placeholders["error"] = str(ex) - except SmartDeviceException as ex: + except KasaException as ex: errors["base"] = "cannot_connect" placeholders["error"] = str(ex) else: @@ -229,9 +229,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): device = await self._async_try_discover_and_update( host, credentials, raise_on_progress=False ) - except AuthenticationException: + except AuthenticationError: return await self.async_step_user_auth_confirm() - except SmartDeviceException as ex: + except KasaException as ex: errors["base"] = "cannot_connect" placeholders["error"] = str(ex) else: @@ -261,10 +261,10 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): device = await self._async_try_connect( self._discovered_device, credentials ) - except AuthenticationException as ex: + except AuthenticationError as ex: errors[CONF_PASSWORD] = "invalid_auth" placeholders["error"] = str(ex) - except SmartDeviceException as ex: + except KasaException as ex: errors["base"] = "cannot_connect" placeholders["error"] = str(ex) else: @@ -298,9 +298,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): device = await self._async_try_connect( self._discovered_device, credentials ) - except AuthenticationException: + except AuthenticationError: return await self.async_step_user_auth_confirm() - except SmartDeviceException: + except KasaException: return self.async_abort(reason="cannot_connect") return self._async_create_entry_from_device(device) @@ -343,7 +343,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): _config_entries.flow.async_abort(flow["flow_id"]) @callback - def _async_create_entry_from_device(self, device: SmartDevice) -> ConfigFlowResult: + def _async_create_entry_from_device(self, device: Device) -> ConfigFlowResult: """Create a config entry from a smart device.""" self._abort_if_unique_id_configured(updates={CONF_HOST: device.host}) return self.async_create_entry( @@ -364,7 +364,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): host: str, credentials: Credentials | None, raise_on_progress: bool, - ) -> SmartDevice: + ) -> Device: """Try to discover the device and call update. Will try to connect to legacy devices if discovery fails. @@ -373,11 +373,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_device = await Discover.discover_single( host, credentials=credentials ) - except TimeoutException: + except TimeoutError: # Try connect() to legacy devices if discovery fails - self._discovered_device = await SmartDevice.connect( - config=DeviceConfig(host) - ) + self._discovered_device = await Device.connect(config=DeviceConfig(host)) else: if self._discovered_device.config.uses_http: self._discovered_device.config.http_client = ( @@ -392,9 +390,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): async def _async_try_connect( self, - discovered_device: SmartDevice, + discovered_device: Device, credentials: Credentials | None, - ) -> SmartDevice: + ) -> Device: """Try to connect.""" self._async_abort_entries_match({CONF_HOST: discovered_device.host}) @@ -405,7 +403,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): if config.uses_http: config.http_client = create_async_tplink_clientsession(self.hass) - self._discovered_device = await SmartDevice.connect(config=config) + self._discovered_device = await Device.connect(config=config) await self.async_set_unique_id( dr.format_mac(self._discovered_device.mac), raise_on_progress=False, @@ -442,10 +440,10 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): credentials=credentials, raise_on_progress=True, ) - except AuthenticationException as ex: + except AuthenticationError as ex: errors[CONF_PASSWORD] = "invalid_auth" placeholders["error"] = str(ex) - except SmartDeviceException as ex: + except KasaException as ex: errors["base"] = "cannot_connect" placeholders["error"] = str(ex) else: diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index 96892bacee7..d77d415aa9c 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -4,13 +4,16 @@ from __future__ import annotations from typing import Final -from homeassistant.const import Platform +from homeassistant.const import Platform, UnitOfTemperature DOMAIN = "tplink" DISCOVERY_TIMEOUT = 5 # Home Assistant will complain if startup takes > 10s CONNECT_TIMEOUT = 5 +# Identifier used for primary control state. +PRIMARY_STATE_ID = "state" + ATTR_CURRENT_A: Final = "current_a" ATTR_CURRENT_POWER_W: Final = "current_power_w" ATTR_TODAY_ENERGY_KWH: Final = "today_energy_kwh" @@ -18,4 +21,19 @@ ATTR_TOTAL_ENERGY_KWH: Final = "total_energy_kwh" CONF_DEVICE_CONFIG: Final = "device_config" -PLATFORMS: Final = [Platform.LIGHT, Platform.SENSOR, Platform.SWITCH] +PLATFORMS: Final = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CLIMATE, + Platform.FAN, + Platform.LIGHT, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] + +UNIT_MAPPING = { + "celsius": UnitOfTemperature.CELSIUS, + "fahrenheit": UnitOfTemperature.FAHRENHEIT, +} diff --git a/homeassistant/components/tplink/coordinator.py b/homeassistant/components/tplink/coordinator.py index 7595cdd8f90..1c362d33746 100644 --- a/homeassistant/components/tplink/coordinator.py +++ b/homeassistant/components/tplink/coordinator.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import timedelta import logging -from kasa import AuthenticationException, SmartDevice, SmartDeviceException +from kasa import AuthenticationError, Device, KasaException from homeassistant import config_entries from homeassistant.core import HomeAssistant @@ -26,7 +26,7 @@ class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]): def __init__( self, hass: HomeAssistant, - device: SmartDevice, + device: Device, update_interval: timedelta, ) -> None: """Initialize DataUpdateCoordinator to gather data for specific SmartPlug.""" @@ -47,7 +47,7 @@ class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]): """Fetch all device and sensor data from api.""" try: await self.device.update(update_children=False) - except AuthenticationException as ex: + except AuthenticationError as ex: raise ConfigEntryAuthFailed from ex - except SmartDeviceException as ex: + except KasaException as ex: raise UpdateFailed(ex) from ex diff --git a/homeassistant/components/tplink/diagnostics.py b/homeassistant/components/tplink/diagnostics.py index e5e84b48162..46a5f0cb1bd 100644 --- a/homeassistant/components/tplink/diagnostics.py +++ b/homeassistant/components/tplink/diagnostics.py @@ -5,12 +5,10 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac -from .const import DOMAIN -from .models import TPLinkData +from . import TPLinkConfigEntry TO_REDACT = { # Entry fields @@ -23,6 +21,7 @@ TO_REDACT = { "hwId", "oemId", "deviceId", + "id", # child id for HS300 # Device location "latitude", "latitude_i", @@ -38,14 +37,17 @@ TO_REDACT = { "ssid", "nickname", "ip", + # Child device information + "original_device_id", + "parent_device_id", } async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: TPLinkConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data: TPLinkData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data coordinator = data.parent_coordinator oui = format_mac(coordinator.device.mac)[:8].upper() return async_redact_data( diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 52b226a1c57..4e8ec0e0779 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -2,24 +2,81 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable, Coroutine +from abc import ABC, abstractmethod +from collections.abc import Awaitable, Callable, Coroutine, Mapping +from dataclasses import dataclass, replace +import logging from typing import Any, Concatenate from kasa import ( - AuthenticationException, - SmartDevice, - SmartDeviceException, - TimeoutException, + AuthenticationError, + Device, + DeviceType, + Feature, + KasaException, + TimeoutError, ) +from homeassistant.const import EntityCategory +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from . import get_device_name, legacy_device_id +from .const import ( + ATTR_CURRENT_A, + ATTR_CURRENT_POWER_W, + ATTR_TODAY_ENERGY_KWH, + ATTR_TOTAL_ENERGY_KWH, + DOMAIN, + PRIMARY_STATE_ID, +) from .coordinator import TPLinkDataUpdateCoordinator +_LOGGER = logging.getLogger(__name__) + +# Mapping from upstream category to homeassistant category +FEATURE_CATEGORY_TO_ENTITY_CATEGORY = { + Feature.Category.Config: EntityCategory.CONFIG, + Feature.Category.Info: EntityCategory.DIAGNOSTIC, + Feature.Category.Debug: EntityCategory.DIAGNOSTIC, +} + +# Skips creating entities for primary features supported by a specialized platform. +# For example, we do not need a separate "state" switch for light bulbs. +DEVICETYPES_WITH_SPECIALIZED_PLATFORMS = { + DeviceType.Bulb, + DeviceType.LightStrip, + DeviceType.Dimmer, + DeviceType.Fan, + DeviceType.Thermostat, +} + +# Features excluded due to future platform additions +EXCLUDED_FEATURES = { + # update + "current_firmware_version", + "available_firmware_version", + # fan + "fan_speed_level", +} + +LEGACY_KEY_MAPPING = { + "current": ATTR_CURRENT_A, + "current_consumption": ATTR_CURRENT_POWER_W, + "consumption_today": ATTR_TODAY_ENERGY_KWH, + "consumption_total": ATTR_TOTAL_ENERGY_KWH, +} + + +@dataclass(frozen=True, kw_only=True) +class TPLinkFeatureEntityDescription(EntityDescription): + """Base class for a TPLink feature based entity description.""" + def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P]( func: Callable[Concatenate[_T, _P], Awaitable[None]], @@ -29,7 +86,7 @@ def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P]( async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: try: await func(self, *args, **kwargs) - except AuthenticationException as ex: + except AuthenticationError as ex: self.coordinator.config_entry.async_start_reauth(self.hass) raise HomeAssistantError( translation_domain=DOMAIN, @@ -39,7 +96,7 @@ def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P]( "exc": str(ex), }, ) from ex - except TimeoutException as ex: + except TimeoutError as ex: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="device_timeout", @@ -48,7 +105,7 @@ def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P]( "exc": str(ex), }, ) from ex - except SmartDeviceException as ex: + except KasaException as ex: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="device_error", @@ -62,24 +119,302 @@ def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P]( return _async_wrap -class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator]): +class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], ABC): """Common base class for all coordinated tplink entities.""" _attr_has_entity_name = True + _device: Device def __init__( - self, device: SmartDevice, coordinator: TPLinkDataUpdateCoordinator + self, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + feature: Feature | None = None, + parent: Device | None = None, ) -> None: - """Initialize the switch.""" + """Initialize the entity.""" super().__init__(coordinator) - self.device: SmartDevice = device - self._attr_unique_id = device.device_id + self._device: Device = device + self._feature = feature + + registry_device = device + device_name = get_device_name(device, parent=parent) + if parent and parent.device_type is not Device.Type.Hub: + if not feature or feature.id == PRIMARY_STATE_ID: + # Entity will be added to parent if not a hub and no feature + # parameter (i.e. core platform like Light, Fan) or the feature + # is the primary state + registry_device = parent + device_name = get_device_name(registry_device) + else: + # Prefix the device name with the parent name unless it is a + # hub attached device. Sensible default for child devices like + # strip plugs or the ks240 where the child alias makes more + # sense in the context of the parent. i.e. Hall Ceiling Fan & + # Bedroom Ceiling Fan; Child device aliases will be Ceiling Fan + # and Dimmer Switch for both so should be distinguished by the + # parent name. + device_name = f"{get_device_name(parent)} {get_device_name(device, parent=parent)}" + self._attr_device_info = DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, device.mac)}, - identifiers={(DOMAIN, str(device.device_id))}, + identifiers={(DOMAIN, str(registry_device.device_id))}, manufacturer="TP-Link", - model=device.model, - name=device.alias, - sw_version=device.hw_info["sw_ver"], - hw_version=device.hw_info["hw_ver"], + model=registry_device.model, + name=device_name, + sw_version=registry_device.hw_info["sw_ver"], + hw_version=registry_device.hw_info["hw_ver"], ) + + if ( + parent is not None + and parent != registry_device + and parent.device_type is not Device.Type.WallSwitch + ): + self._attr_device_info["via_device"] = (DOMAIN, parent.device_id) + else: + self._attr_device_info["connections"] = { + (dr.CONNECTION_NETWORK_MAC, device.mac) + } + + self._attr_unique_id = self._get_unique_id() + + def _get_unique_id(self) -> str: + """Return unique ID for the entity.""" + return legacy_device_id(self._device) + + async def async_added_to_hass(self) -> None: + """Handle being added to hass.""" + self._async_call_update_attrs() + return await super().async_added_to_hass() + + @abstractmethod + @callback + def _async_update_attrs(self) -> None: + """Platforms implement this to update the entity internals.""" + raise NotImplementedError + + @callback + def _async_call_update_attrs(self) -> None: + """Call update_attrs and make entity unavailable on error. + + update_attrs can sometimes fail if a device firmware update breaks the + downstream library. + """ + try: + self._async_update_attrs() + except Exception as ex: # noqa: BLE001 + if self._attr_available: + _LOGGER.warning( + "Unable to read data for %s %s: %s", + self._device, + self.entity_id, + ex, + ) + self._attr_available = False + else: + self._attr_available = True + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._async_call_update_attrs() + super()._handle_coordinator_update() + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.last_update_success and self._attr_available + + +class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC): + """Common base class for all coordinated tplink feature entities.""" + + entity_description: TPLinkFeatureEntityDescription + _feature: Feature + + def __init__( + self, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + feature: Feature, + description: TPLinkFeatureEntityDescription, + parent: Device | None = None, + ) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(device, coordinator, parent=parent, feature=feature) + + def _get_unique_id(self) -> str: + """Return unique ID for the entity.""" + key = self.entity_description.key + # The unique id for the state feature in the switch platform is the + # device_id + if key == PRIMARY_STATE_ID: + return legacy_device_id(self._device) + + # Historically the legacy device emeter attributes which are now + # replaced with features used slightly different keys. This ensures + # that those entities are not orphaned. Returns the mapped key or the + # provided key if not mapped. + key = LEGACY_KEY_MAPPING.get(key, key) + return f"{legacy_device_id(self._device)}_{key}" + + @classmethod + def _category_for_feature(cls, feature: Feature | None) -> EntityCategory | None: + """Return entity category for a feature.""" + # Main controls have no category + if feature is None or feature.category is Feature.Category.Primary: + return None + + if ( + entity_category := FEATURE_CATEGORY_TO_ENTITY_CATEGORY.get(feature.category) + ) is None: + _LOGGER.error( + "Unhandled category %s, fallback to DIAGNOSTIC", feature.category + ) + entity_category = EntityCategory.DIAGNOSTIC + + return entity_category + + @classmethod + def _description_for_feature[_D: EntityDescription]( + cls, + feature: Feature, + descriptions: Mapping[str, _D], + *, + device: Device, + parent: Device | None = None, + ) -> _D | None: + """Return description object for the given feature. + + This is responsible for setting the common parameters & deciding + based on feature id which additional parameters are passed. + """ + + if descriptions and (desc := descriptions.get(feature.id)): + translation_key: str | None = feature.id + # HA logic is to name entities based on the following logic: + # _attr_name > translation.name > description.name + # > device_class (if base platform supports). + name: str | None | UndefinedType = UNDEFINED + + # The state feature gets the device name or the child device + # name if it's a child device + if feature.id == PRIMARY_STATE_ID: + translation_key = None + # if None will use device name + name = get_device_name(device, parent=parent) if parent else None + + return replace( + desc, + translation_key=translation_key, + name=name, # if undefined will use translation key + entity_category=cls._category_for_feature(feature), + # enabled_default can be overridden to False in the description + entity_registry_enabled_default=feature.category + is not Feature.Category.Debug + and desc.entity_registry_enabled_default, + ) + + _LOGGER.info( + "Device feature: %s (%s) needs an entity description defined in HA", + feature.name, + feature.id, + ) + return None + + @classmethod + def _entities_for_device[ + _E: CoordinatedTPLinkFeatureEntity, + _D: TPLinkFeatureEntityDescription, + ]( + cls, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + feature_type: Feature.Type, + entity_class: type[_E], + descriptions: Mapping[str, _D], + parent: Device | None = None, + ) -> list[_E]: + """Return a list of entities to add. + + This filters out unwanted features to avoid creating unnecessary entities + for device features that are implemented by specialized platforms like light. + """ + entities: list[_E] = [ + entity_class( + device, + coordinator, + feature=feat, + description=desc, + parent=parent, + ) + for feat in device.features.values() + if feat.type == feature_type + and feat.id not in EXCLUDED_FEATURES + and ( + feat.category is not Feature.Category.Primary + or device.device_type not in DEVICETYPES_WITH_SPECIALIZED_PLATFORMS + ) + and ( + desc := cls._description_for_feature( + feat, descriptions, device=device, parent=parent + ) + ) + ] + return entities + + @classmethod + def entities_for_device_and_its_children[ + _E: CoordinatedTPLinkFeatureEntity, + _D: TPLinkFeatureEntityDescription, + ]( + cls, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + feature_type: Feature.Type, + entity_class: type[_E], + descriptions: Mapping[str, _D], + child_coordinators: list[TPLinkDataUpdateCoordinator] | None = None, + ) -> list[_E]: + """Create entities for device and its children. + + This is a helper that calls *_entities_for_device* for the device and its children. + """ + entities: list[_E] = [] + # Add parent entities before children so via_device id works. + entities.extend( + cls._entities_for_device( + device, + coordinator=coordinator, + feature_type=feature_type, + entity_class=entity_class, + descriptions=descriptions, + ) + ) + if device.children: + _LOGGER.debug("Initializing device with %s children", len(device.children)) + for idx, child in enumerate(device.children): + # HS300 does not like too many concurrent requests and its + # emeter data requires a request for each socket, so we receive + # separate coordinators. + if child_coordinators: + child_coordinator = child_coordinators[idx] + else: + child_coordinator = coordinator + entities.extend( + cls._entities_for_device( + child, + coordinator=child_coordinator, + feature_type=feature_type, + entity_class=entity_class, + descriptions=descriptions, + parent=device, + ) + ) + + return entities diff --git a/homeassistant/components/tplink/fan.py b/homeassistant/components/tplink/fan.py new file mode 100644 index 00000000000..947a9072329 --- /dev/null +++ b/homeassistant/components/tplink/fan.py @@ -0,0 +1,111 @@ +"""Support for TPLink Fan devices.""" + +import logging +import math +from typing import Any + +from kasa import Device, Module +from kasa.interfaces import Fan as FanInterface + +from homeassistant.components.fan import FanEntity, FanEntityFeature +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) +from homeassistant.util.scaling import int_states_in_range + +from . import TPLinkConfigEntry +from .coordinator import TPLinkDataUpdateCoordinator +from .entity import CoordinatedTPLinkEntity, async_refresh_after + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: TPLinkConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up fans.""" + data = config_entry.runtime_data + parent_coordinator = data.parent_coordinator + device = parent_coordinator.device + entities: list[CoordinatedTPLinkEntity] = [] + if Module.Fan in device.modules: + entities.append( + TPLinkFanEntity( + device, parent_coordinator, fan_module=device.modules[Module.Fan] + ) + ) + entities.extend( + TPLinkFanEntity( + child, + parent_coordinator, + fan_module=child.modules[Module.Fan], + parent=device, + ) + for child in device.children + if Module.Fan in child.modules + ) + async_add_entities(entities) + + +SPEED_RANGE = (1, 4) # off is not included + + +class TPLinkFanEntity(CoordinatedTPLinkEntity, FanEntity): + """Representation of a fan for a TPLink Fan device.""" + + _attr_speed_count = int_states_in_range(SPEED_RANGE) + _attr_supported_features = FanEntityFeature.SET_SPEED + + def __init__( + self, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + fan_module: FanInterface, + parent: Device | None = None, + ) -> None: + """Initialize the fan.""" + super().__init__(device, coordinator, parent=parent) + self.fan_module = fan_module + # If _attr_name is None the entity name will be the device name + self._attr_name = None if parent is None else device.alias + + @async_refresh_after + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn on the fan.""" + if percentage is not None: + value_in_range = math.ceil( + percentage_to_ranged_value(SPEED_RANGE, percentage) + ) + else: + value_in_range = SPEED_RANGE[1] + await self.fan_module.set_fan_speed_level(value_in_range) + + @async_refresh_after + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the fan off.""" + await self.fan_module.set_fan_speed_level(0) + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + value_in_range = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + await self.fan_module.set_fan_speed_level(value_in_range) + + @callback + def _async_update_attrs(self) -> None: + """Update the entity's attributes.""" + fan_speed = self.fan_module.fan_speed_level + self._attr_is_on = fan_speed != 0 + if self._attr_is_on: + self._attr_percentage = ranged_value_to_percentage(SPEED_RANGE, fan_speed) + else: + self._attr_percentage = None diff --git a/homeassistant/components/tplink/icons.json b/homeassistant/components/tplink/icons.json index 9b83b3abc85..3da3b4806d3 100644 --- a/homeassistant/components/tplink/icons.json +++ b/homeassistant/components/tplink/icons.json @@ -1,11 +1,110 @@ { "entity": { + "binary_sensor": { + "humidity_warning": { + "default": "mdi:water-percent", + "state": { + "on": "mdi:water-percent-alert" + } + }, + "temperature_warning": { + "default": "mdi:thermometer-check", + "state": { + "on": "mdi:thermometer-alert" + } + } + }, + "button": { + "test_alarm": { + "default": "mdi:bell-alert" + }, + "stop_alarm": { + "default": "mdi:bell-cancel" + } + }, + "select": { + "light_preset": { + "default": "mdi:sign-direction" + }, + "alarm_sound": { + "default": "mdi:music-note" + }, + "alarm_volume": { + "default": "mdi:volume-medium", + "state": { + "low": "mdi:volume-low", + "medium": "mdi:volume-medium", + "high": "mdi:volume-high" + } + } + }, "switch": { "led": { "default": "mdi:led-off", "state": { "on": "mdi:led-on" } + }, + "auto_update_enabled": { + "default": "mdi:autorenew-off", + "state": { + "on": "mdi:autorenew" + } + }, + "auto_off_enabled": { + "default": "mdi:sleep-off", + "state": { + "on": "mdi:sleep" + } + }, + "smooth_transitions": { + "default": "mdi:transition-masked", + "state": { + "on": "mdi:transition" + } + }, + "fan_sleep_mode": { + "default": "mdi:sleep-off", + "state": { + "on": "mdi:sleep" + } + } + }, + "sensor": { + "on_since": { + "default": "mdi:clock" + }, + "ssid": { + "default": "mdi:wifi" + }, + "signal_level": { + "default": "mdi:signal" + }, + "current_firmware_version": { + "default": "mdi:information" + }, + "available_firmware_version": { + "default": "mdi:information-outline" + }, + "alarm_source": { + "default": "mdi:bell" + } + }, + "number": { + "smooth_transition_off": { + "default": "mdi:weather-sunset-down" + }, + "smooth_transition_on": { + "default": "mdi:weather-sunset-up" + }, + "auto_off_minutes": { + "default": "mdi:sleep" + }, + "temperature_offset": { + "default": "mdi:contrast" + }, + "target_temperature": { + "default": "mdi:thermometer" } } }, diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 977e75215aa..633648bbf23 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -4,9 +4,11 @@ from __future__ import annotations from collections.abc import Sequence import logging -from typing import Any, cast +from typing import Any -from kasa import SmartBulb, SmartLightStrip +from kasa import Device, DeviceType, LightState, Module +from kasa.interfaces import Light, LightEffect +from kasa.iot import IotDevice import voluptuous as vol from homeassistant.components.light import ( @@ -15,23 +17,21 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, + EFFECT_OFF, ColorMode, LightEntity, LightEntityFeature, filter_supported_color_modes, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import VolDictType -from . import legacy_device_id -from .const import DOMAIN +from . import TPLinkConfigEntry, legacy_device_id from .coordinator import TPLinkDataUpdateCoordinator from .entity import CoordinatedTPLinkEntity, async_refresh_after -from .models import TPLinkData _LOGGER = logging.getLogger(__name__) @@ -132,16 +132,24 @@ def _async_build_base_effect( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: TPLinkConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up switches.""" - data: TPLinkData = hass.data[DOMAIN][config_entry.entry_id] + data = config_entry.runtime_data parent_coordinator = data.parent_coordinator device = parent_coordinator.device - if device.is_light_strip: - async_add_entities( - [TPLinkSmartLightStrip(cast(SmartLightStrip, device), parent_coordinator)] + entities: list[TPLinkLightEntity | TPLinkLightEffectEntity] = [] + if ( + effect_module := device.modules.get(Module.LightEffect) + ) and effect_module.has_custom_effects: + entities.append( + TPLinkLightEffectEntity( + device, + parent_coordinator, + light_module=device.modules[Module.Light], + effect_module=effect_module, + ) ) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( @@ -154,52 +162,83 @@ async def async_setup_entry( SEQUENCE_EFFECT_DICT, "async_set_sequence_effect", ) - elif device.is_bulb or device.is_dimmer: - async_add_entities( - [TPLinkSmartBulb(cast(SmartBulb, device), parent_coordinator)] + elif Module.Light in device.modules: + entities.append( + TPLinkLightEntity( + device, parent_coordinator, light_module=device.modules[Module.Light] + ) ) + entities.extend( + TPLinkLightEntity( + child, + parent_coordinator, + light_module=child.modules[Module.Light], + parent=device, + ) + for child in device.children + if Module.Light in child.modules + ) + async_add_entities(entities) -class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): +class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity): """Representation of a TPLink Smart Bulb.""" _attr_supported_features = LightEntityFeature.TRANSITION - _attr_name = None _fixed_color_mode: ColorMode | None = None - device: SmartBulb - def __init__( self, - device: SmartBulb, + device: Device, coordinator: TPLinkDataUpdateCoordinator, + *, + light_module: Light, + parent: Device | None = None, ) -> None: - """Initialize the switch.""" - super().__init__(device, coordinator) - # For backwards compat with pyHS100 - if device.is_dimmer: - # Dimmers used to use the switch format since - # pyHS100 treated them as SmartPlug but the old code - # created them as lights - # https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86 - self._attr_unique_id = legacy_device_id(device) - else: - self._attr_unique_id = device.mac.replace(":", "").upper() + """Initialize the light.""" + self._parent = parent + super().__init__(device, coordinator, parent=parent) + self._light_module = light_module + # If _attr_name is None the entity name will be the device name + self._attr_name = None if parent is None else device.alias modes: set[ColorMode] = {ColorMode.ONOFF} - if device.is_variable_color_temp: + if light_module.is_variable_color_temp: modes.add(ColorMode.COLOR_TEMP) - temp_range = device.valid_temperature_range + temp_range = light_module.valid_temperature_range self._attr_min_color_temp_kelvin = temp_range.min self._attr_max_color_temp_kelvin = temp_range.max - if device.is_color: + if light_module.is_color: modes.add(ColorMode.HS) - if device.is_dimmable: + if light_module.is_dimmable: modes.add(ColorMode.BRIGHTNESS) self._attr_supported_color_modes = filter_supported_color_modes(modes) if len(self._attr_supported_color_modes) == 1: # If the light supports only a single color mode, set it now self._fixed_color_mode = next(iter(self._attr_supported_color_modes)) - self._async_update_attrs() + self._async_call_update_attrs() + + def _get_unique_id(self) -> str: + """Return unique ID for the entity.""" + # For historical reasons the light platform uses the mac address as + # the unique id whereas all other platforms use device_id. + device = self._device + + # For backwards compat with pyHS100 + if device.device_type is DeviceType.Dimmer and isinstance(device, IotDevice): + # Dimmers used to use the switch format since + # pyHS100 treated them as SmartPlug but the old code + # created them as lights + # https://github.com/home-assistant/core/blob/2021.9.7/ \ + # homeassistant/components/tplink/common.py#L86 + return legacy_device_id(device) + + # Newer devices can have child lights. While there isn't currently + # an example of a device with more than one light we use the device_id + # for consistency and future proofing + if self._parent or device.children: + return legacy_device_id(device) + + return device.mac.replace(":", "").upper() @callback def _async_extract_brightness_transition( @@ -211,12 +250,12 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: brightness = round((brightness * 100.0) / 255.0) - if self.device.is_dimmer and transition is None: - # This is a stopgap solution for inconsistent set_brightness handling - # in the upstream library, see #57265. + if self._device.device_type is DeviceType.Dimmer and transition is None: + # This is a stopgap solution for inconsistent set_brightness + # handling in the upstream library, see #57265. # This should be removed when the upstream has fixed the issue. # The device logic is to change the settings without turning it on - # except when transition is defined, so we leverage that here for now. + # except when transition is defined so we leverage that for now. transition = 1 return brightness, transition @@ -226,13 +265,13 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): ) -> None: # TP-Link requires integers. hue, sat = tuple(int(val) for val in hs_color) - await self.device.set_hsv(hue, sat, brightness, transition=transition) + await self._light_module.set_hsv(hue, sat, brightness, transition=transition) async def _async_set_color_temp( self, color_temp: float, brightness: int | None, transition: int | None ) -> None: - device = self.device - valid_temperature_range = device.valid_temperature_range + light_module = self._light_module + valid_temperature_range = light_module.valid_temperature_range requested_color_temp = round(color_temp) # Clamp color temp to valid range # since if the light in a group we will @@ -242,7 +281,7 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): valid_temperature_range.max, max(valid_temperature_range.min, requested_color_temp), ) - await device.set_color_temp( + await light_module.set_color_temp( clamped_color_temp, brightness=brightness, transition=transition, @@ -253,9 +292,11 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): ) -> None: # Fallback to adjusting brightness or turning the bulb on if brightness is not None: - await self.device.set_brightness(brightness, transition=transition) + await self._light_module.set_brightness(brightness, transition=transition) return - await self.device.turn_on(transition=transition) + await self._light_module.set_state( + LightState(light_on=True, transition=transition) + ) @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: @@ -275,7 +316,9 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): """Turn the light off.""" if (transition := kwargs.get(ATTR_TRANSITION)) is not None: transition = int(transition * 1_000) - await self.device.turn_off(transition=transition) + await self._light_module.set_state( + LightState(light_on=False, transition=transition) + ) def _determine_color_mode(self) -> ColorMode: """Return the active color mode.""" @@ -284,48 +327,53 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): return self._fixed_color_mode # The light supports both color temp and color, determine which on is active - if self.device.is_variable_color_temp and self.device.color_temp: + if self._light_module.is_variable_color_temp and self._light_module.color_temp: return ColorMode.COLOR_TEMP return ColorMode.HS @callback def _async_update_attrs(self) -> None: """Update the entity's attributes.""" - device = self.device - self._attr_is_on = device.is_on - if device.is_dimmable: - self._attr_brightness = round((device.brightness * 255.0) / 100.0) + light_module = self._light_module + self._attr_is_on = light_module.state.light_on is True + if light_module.is_dimmable: + self._attr_brightness = round((light_module.brightness * 255.0) / 100.0) color_mode = self._determine_color_mode() self._attr_color_mode = color_mode if color_mode is ColorMode.COLOR_TEMP: - self._attr_color_temp_kelvin = device.color_temp + self._attr_color_temp_kelvin = light_module.color_temp elif color_mode is ColorMode.HS: - hue, saturation, _ = device.hsv + hue, saturation, _ = light_module.hsv self._attr_hs_color = hue, saturation - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._async_update_attrs() - super()._handle_coordinator_update() - -class TPLinkSmartLightStrip(TPLinkSmartBulb): +class TPLinkLightEffectEntity(TPLinkLightEntity): """Representation of a TPLink Smart Light Strip.""" - device: SmartLightStrip + def __init__( + self, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + light_module: Light, + effect_module: LightEffect, + ) -> None: + """Initialize the light strip.""" + self._effect_module = effect_module + super().__init__(device, coordinator, light_module=light_module) + _attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT @callback def _async_update_attrs(self) -> None: """Update the entity's attributes.""" super()._async_update_attrs() - device = self.device - if (effect := device.effect) and effect["enable"]: - self._attr_effect = effect["name"] + effect_module = self._effect_module + if effect_module.effect != LightEffect.LIGHT_EFFECTS_OFF: + self._attr_effect = effect_module.effect else: - self._attr_effect = None - if effect_list := device.effect_list: + self._attr_effect = EFFECT_OFF + if effect_list := effect_module.effect_list: self._attr_effect_list = effect_list else: self._attr_effect_list = None @@ -335,15 +383,15 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb): """Turn the light on.""" brightness, transition = self._async_extract_brightness_transition(**kwargs) if ATTR_EFFECT in kwargs: - await self.device.set_effect( + await self._effect_module.set_effect( kwargs[ATTR_EFFECT], brightness=brightness, transition=transition ) elif ATTR_COLOR_TEMP_KELVIN in kwargs: if self.effect: # If there is an effect in progress - # we have to set an HSV value to clear the effect + # we have to clear the effect # before we can set a color temp - await self.device.set_hsv(0, 0, brightness) + await self._light_module.set_hsv(0, 0, brightness) await self._async_set_color_temp( kwargs[ATTR_COLOR_TEMP_KELVIN], brightness, transition ) @@ -390,7 +438,7 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb): if transition_range: effect["transition_range"] = transition_range effect["transition"] = 0 - await self.device.set_custom_effect(effect) + await self._effect_module.set_custom_effect(effect) async def async_set_sequence_effect( self, @@ -412,4 +460,4 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb): "spread": spread, "direction": direction, } - await self.device.set_custom_effect(effect) + await self._effect_module.set_custom_effect(effect) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index a91e7e5a46f..5b8e6f8fc1b 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -40,6 +40,10 @@ "hostname": "k[lps]*", "macaddress": "5091E3*" }, + { + "hostname": "p1*", + "macaddress": "5091E3*" + }, { "hostname": "k[lps]*", "macaddress": "9C5322*" @@ -216,14 +220,26 @@ "hostname": "s5*", "macaddress": "3C52A1*" }, + { + "hostname": "h1*", + "macaddress": "3C52A1*" + }, { "hostname": "l9*", "macaddress": "A842A1*" }, + { + "hostname": "p1*", + "macaddress": "A842A1*" + }, { "hostname": "l9*", "macaddress": "3460F9*" }, + { + "hostname": "p1*", + "macaddress": "3460F9*" + }, { "hostname": "hs*", "macaddress": "704F57*" @@ -232,6 +248,10 @@ "hostname": "k[lps]*", "macaddress": "74DA88*" }, + { + "hostname": "p1*", + "macaddress": "74DA88*" + }, { "hostname": "p3*", "macaddress": "788CB5*" @@ -263,11 +283,19 @@ { "hostname": "l9*", "macaddress": "F0A731*" + }, + { + "hostname": "ks2*", + "macaddress": "F0A731*" + }, + { + "hostname": "kh1*", + "macaddress": "F0A731*" } ], "documentation": "https://www.home-assistant.io/integrations/tplink", "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.6.2.1"] + "requirements": ["python-kasa[speedups]==0.7.0.1"] } diff --git a/homeassistant/components/tplink/number.py b/homeassistant/components/tplink/number.py new file mode 100644 index 00000000000..4b273800e6a --- /dev/null +++ b/homeassistant/components/tplink/number.py @@ -0,0 +1,108 @@ +"""Support for TPLink number entities.""" + +from __future__ import annotations + +import logging +from typing import Final + +from kasa import Device, Feature + +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import TPLinkConfigEntry +from .entity import ( + CoordinatedTPLinkFeatureEntity, + TPLinkDataUpdateCoordinator, + TPLinkFeatureEntityDescription, + async_refresh_after, +) + +_LOGGER = logging.getLogger(__name__) + + +class TPLinkNumberEntityDescription( + NumberEntityDescription, TPLinkFeatureEntityDescription +): + """Base class for a TPLink feature based sensor entity description.""" + + +NUMBER_DESCRIPTIONS: Final = ( + TPLinkNumberEntityDescription( + key="smooth_transition_on", + mode=NumberMode.BOX, + ), + TPLinkNumberEntityDescription( + key="smooth_transition_off", + mode=NumberMode.BOX, + ), + TPLinkNumberEntityDescription( + key="auto_off_minutes", + mode=NumberMode.BOX, + ), + TPLinkNumberEntityDescription( + key="temperature_offset", + mode=NumberMode.BOX, + ), +) + +NUMBER_DESCRIPTIONS_MAP = {desc.key: desc for desc in NUMBER_DESCRIPTIONS} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: TPLinkConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors.""" + data = config_entry.runtime_data + parent_coordinator = data.parent_coordinator + children_coordinators = data.children_coordinators + device = parent_coordinator.device + entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children( + device=device, + coordinator=parent_coordinator, + feature_type=Feature.Type.Number, + entity_class=TPLinkNumberEntity, + descriptions=NUMBER_DESCRIPTIONS_MAP, + child_coordinators=children_coordinators, + ) + + async_add_entities(entities) + + +class TPLinkNumberEntity(CoordinatedTPLinkFeatureEntity, NumberEntity): + """Representation of a feature-based TPLink sensor.""" + + entity_description: TPLinkNumberEntityDescription + + def __init__( + self, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + feature: Feature, + description: TPLinkFeatureEntityDescription, + parent: Device | None = None, + ) -> None: + """Initialize the a switch.""" + super().__init__( + device, coordinator, feature=feature, description=description, parent=parent + ) + self._attr_native_min_value = self._feature.minimum_value + self._attr_native_max_value = self._feature.maximum_value + + @async_refresh_after + async def async_set_native_value(self, value: float) -> None: + """Set feature value.""" + await self._feature.set_value(int(value)) + + @callback + def _async_update_attrs(self) -> None: + """Update the entity's attributes.""" + self._attr_native_value = self._feature.value diff --git a/homeassistant/components/tplink/select.py b/homeassistant/components/tplink/select.py new file mode 100644 index 00000000000..41703b27e5a --- /dev/null +++ b/homeassistant/components/tplink/select.py @@ -0,0 +1,95 @@ +"""Support for TPLink select entities.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Final, cast + +from kasa import Device, Feature + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import TPLinkConfigEntry +from .entity import ( + CoordinatedTPLinkFeatureEntity, + TPLinkDataUpdateCoordinator, + TPLinkFeatureEntityDescription, + async_refresh_after, +) + + +@dataclass(frozen=True, kw_only=True) +class TPLinkSelectEntityDescription( + SelectEntityDescription, TPLinkFeatureEntityDescription +): + """Base class for a TPLink feature based sensor entity description.""" + + +SELECT_DESCRIPTIONS: Final = [ + TPLinkSelectEntityDescription( + key="light_preset", + ), + TPLinkSelectEntityDescription( + key="alarm_sound", + ), + TPLinkSelectEntityDescription( + key="alarm_volume", + ), +] + +SELECT_DESCRIPTIONS_MAP = {desc.key: desc for desc in SELECT_DESCRIPTIONS} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: TPLinkConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors.""" + data = config_entry.runtime_data + parent_coordinator = data.parent_coordinator + children_coordinators = data.children_coordinators + device = parent_coordinator.device + + entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children( + device=device, + coordinator=parent_coordinator, + feature_type=Feature.Type.Choice, + entity_class=TPLinkSelectEntity, + descriptions=SELECT_DESCRIPTIONS_MAP, + child_coordinators=children_coordinators, + ) + async_add_entities(entities) + + +class TPLinkSelectEntity(CoordinatedTPLinkFeatureEntity, SelectEntity): + """Representation of a tplink select entity.""" + + entity_description: TPLinkSelectEntityDescription + + def __init__( + self, + device: Device, + coordinator: TPLinkDataUpdateCoordinator, + *, + feature: Feature, + description: TPLinkFeatureEntityDescription, + parent: Device | None = None, + ) -> None: + """Initialize a select.""" + super().__init__( + device, coordinator, feature=feature, description=description, parent=parent + ) + self._attr_options = cast(list, self._feature.choices) + + @async_refresh_after + async def async_select_option(self, option: str) -> None: + """Update the current selected option.""" + await self._feature.set_value(option) + + @callback + def _async_update_attrs(self) -> None: + """Update the entity's attributes.""" + self._attr_current_option = self._feature.value diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index d7563dd0401..474ee6bfacf 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -1,11 +1,11 @@ -"""Support for TPLink HS100/HS110/HS200 smart switch energy sensors.""" +"""Support for TPLink sensor entities.""" from __future__ import annotations from dataclasses import dataclass from typing import cast -from kasa import SmartDevice +from kasa import Device, Feature from homeassistant.components.sensor import ( SensorDeviceClass, @@ -13,175 +13,164 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_VOLTAGE, - UnitOfElectricCurrent, - UnitOfElectricPotential, - UnitOfEnergy, - UnitOfPower, -) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import legacy_device_id -from .const import ( - ATTR_CURRENT_A, - ATTR_CURRENT_POWER_W, - ATTR_TODAY_ENERGY_KWH, - ATTR_TOTAL_ENERGY_KWH, - DOMAIN, -) +from . import TPLinkConfigEntry +from .const import UNIT_MAPPING from .coordinator import TPLinkDataUpdateCoordinator -from .entity import CoordinatedTPLinkEntity -from .models import TPLinkData +from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription -@dataclass(frozen=True) -class TPLinkSensorEntityDescription(SensorEntityDescription): - """Describes TPLink sensor entity.""" - - emeter_attr: str | None = None - precision: int | None = None +@dataclass(frozen=True, kw_only=True) +class TPLinkSensorEntityDescription( + SensorEntityDescription, TPLinkFeatureEntityDescription +): + """Base class for a TPLink feature based sensor entity description.""" -ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( +SENSOR_DESCRIPTIONS: tuple[TPLinkSensorEntityDescription, ...] = ( TPLinkSensorEntityDescription( - key=ATTR_CURRENT_POWER_W, - translation_key="current_consumption", - native_unit_of_measurement=UnitOfPower.WATT, + key="current_consumption", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, - emeter_attr="power", - precision=1, ), TPLinkSensorEntityDescription( - key=ATTR_TOTAL_ENERGY_KWH, - translation_key="total_consumption", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + key="consumption_total", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, - emeter_attr="total", - precision=3, ), TPLinkSensorEntityDescription( - key=ATTR_TODAY_ENERGY_KWH, - translation_key="today_consumption", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + key="consumption_today", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, - precision=3, ), TPLinkSensorEntityDescription( - key=ATTR_VOLTAGE, - native_unit_of_measurement=UnitOfElectricPotential.VOLT, + key="consumption_this_month", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + TPLinkSensorEntityDescription( + key="voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, - emeter_attr="voltage", - precision=1, ), TPLinkSensorEntityDescription( - key=ATTR_CURRENT_A, - native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + key="current", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, - emeter_attr="current", - precision=2, + ), + TPLinkSensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + TPLinkSensorEntityDescription( + # Disable as the value reported by the device changes seconds frequently + entity_registry_enabled_default=False, + key="on_since", + device_class=SensorDeviceClass.TIMESTAMP, + ), + TPLinkSensorEntityDescription( + key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, + ), + TPLinkSensorEntityDescription( + key="signal_level", + state_class=SensorStateClass.MEASUREMENT, + ), + TPLinkSensorEntityDescription( + key="ssid", + ), + TPLinkSensorEntityDescription( + key="battery_level", + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + ), + TPLinkSensorEntityDescription( + key="auto_off_at", + device_class=SensorDeviceClass.TIMESTAMP, + ), + TPLinkSensorEntityDescription( + key="device_time", + device_class=SensorDeviceClass.TIMESTAMP, + ), + TPLinkSensorEntityDescription( + key="humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + TPLinkSensorEntityDescription( + key="report_interval", + device_class=SensorDeviceClass.DURATION, + ), + TPLinkSensorEntityDescription( + key="alarm_source", + ), + TPLinkSensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, ), ) - -def async_emeter_from_device( - device: SmartDevice, description: TPLinkSensorEntityDescription -) -> float | None: - """Map a sensor key to the device attribute.""" - if attr := description.emeter_attr: - if (val := getattr(device.emeter_realtime, attr)) is None: - return None - return round(cast(float, val), description.precision) - - # ATTR_TODAY_ENERGY_KWH - if (emeter_today := device.emeter_today) is not None: - return round(cast(float, emeter_today), description.precision) - # today's consumption not available, when device was off all the day - # bulb's do not report this information, so filter it out - return None if device.is_bulb else 0.0 - - -def _async_sensors_for_device( - device: SmartDevice, - coordinator: TPLinkDataUpdateCoordinator, - has_parent: bool = False, -) -> list[SmartPlugSensor]: - """Generate the sensors for the device.""" - return [ - SmartPlugSensor(device, coordinator, description, has_parent) - for description in ENERGY_SENSORS - if async_emeter_from_device(device, description) is not None - ] +SENSOR_DESCRIPTIONS_MAP = {desc.key: desc for desc in SENSOR_DESCRIPTIONS} async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: TPLinkConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors.""" - data: TPLinkData = hass.data[DOMAIN][config_entry.entry_id] + data = config_entry.runtime_data parent_coordinator = data.parent_coordinator children_coordinators = data.children_coordinators - entities: list[SmartPlugSensor] = [] - parent = parent_coordinator.device - if not parent.has_emeter: - return - - if parent.is_strip: - # Historically we only add the children if the device is a strip - for idx, child in enumerate(parent.children): - entities.extend( - _async_sensors_for_device(child, children_coordinators[idx], True) - ) - else: - entities.extend(_async_sensors_for_device(parent, parent_coordinator)) + device = parent_coordinator.device + entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children( + device=device, + coordinator=parent_coordinator, + feature_type=Feature.Type.Sensor, + entity_class=TPLinkSensorEntity, + descriptions=SENSOR_DESCRIPTIONS_MAP, + child_coordinators=children_coordinators, + ) async_add_entities(entities) -class SmartPlugSensor(CoordinatedTPLinkEntity, SensorEntity): - """Representation of a TPLink Smart Plug energy sensor.""" +class TPLinkSensorEntity(CoordinatedTPLinkFeatureEntity, SensorEntity): + """Representation of a feature-based TPLink sensor.""" entity_description: TPLinkSensorEntityDescription def __init__( self, - device: SmartDevice, + device: Device, coordinator: TPLinkDataUpdateCoordinator, + *, + feature: Feature, description: TPLinkSensorEntityDescription, - has_parent: bool = False, + parent: Device | None = None, ) -> None: - """Initialize the switch.""" - super().__init__(device, coordinator) - self.entity_description = description - self._attr_unique_id = f"{legacy_device_id(device)}_{description.key}" - if has_parent: - assert device.alias - self._attr_translation_placeholders = {"device_name": device.alias} - if description.translation_key: - self._attr_translation_key = f"{description.translation_key}_child" - else: - assert description.device_class - self._attr_translation_key = f"{description.device_class.value}_child" - self._async_update_attrs() + """Initialize the sensor.""" + super().__init__( + device, coordinator, description=description, feature=feature, parent=parent + ) + self._async_call_update_attrs() @callback def _async_update_attrs(self) -> None: """Update the entity's attributes.""" - self._attr_native_value = async_emeter_from_device( - self.device, self.entity_description - ) + value = self._feature.value + if value is not None and self._feature.precision_hint is not None: + value = round(cast(float, value), self._feature.precision_hint) + # We probably do not need this, when we are rounding already? + self._attr_suggested_display_precision = self._feature.precision_hint - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._async_update_attrs() - super()._handle_coordinator_update() + self._attr_native_value = value + # Map to homeassistant units and fallback to upstream one if none found + if self._feature.unit is not None: + self._attr_native_unit_of_measurement = UNIT_MAPPING.get( + self._feature.unit, self._feature.unit + ) diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json index c863df7c81c..34ce96612f5 100644 --- a/homeassistant/components/tplink/strings.json +++ b/homeassistant/components/tplink/strings.json @@ -59,35 +59,151 @@ } }, "entity": { + "binary_sensor": { + "humidity_warning": { + "name": "Humidity warning" + }, + "temperature_warning": { + "name": "Temperature warning" + }, + "overheated": { + "name": "Overheated" + }, + "battery_low": { + "name": "Battery low" + }, + "cloud_connection": { + "name": "Cloud connection" + }, + "update_available": { + "name": "[%key:component::binary_sensor::entity_component::update::name%]", + "state": { + "off": "[%key:component::binary_sensor::entity_component::update::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::update::state::on%]" + } + }, + "is_open": { + "name": "[%key:component::binary_sensor::entity_component::door::name%]", + "state": { + "off": "[%key:common::state::closed%]", + "on": "[%key:common::state::open%]" + } + }, + "water_alert": { + "name": "[%key:component::binary_sensor::entity_component::moisture::name%]", + "state": { + "off": "[%key:component::binary_sensor::entity_component::moisture::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::moisture::state::on%]" + } + } + }, + "button": { + "test_alarm": { + "name": "Test alarm" + }, + "stop_alarm": { + "name": "Stop alarm" + } + }, + "select": { + "light_preset": { + "name": "Light preset" + }, + "alarm_sound": { + "name": "Alarm sound" + }, + "alarm_volume": { + "name": "Alarm volume" + } + }, "sensor": { "current_consumption": { "name": "Current consumption" }, - "total_consumption": { + "consumption_total": { "name": "Total consumption" }, - "today_consumption": { + "consumption_today": { "name": "Today's consumption" }, - "current_consumption_child": { - "name": "{device_name} current consumption" + "consumption_this_month": { + "name": "This month's consumption" }, - "total_consumption_child": { - "name": "{device_name} total consumption" + "on_since": { + "name": "On since" }, - "today_consumption_child": { - "name": "{device_name} today's consumption" + "ssid": { + "name": "SSID" }, - "current_child": { - "name": "{device_name} current" + "signal_level": { + "name": "Signal level" }, - "voltage_child": { - "name": "{device_name} voltage" + "current_firmware_version": { + "name": "Current firmware version" + }, + "available_firmware_version": { + "name": "Available firmware version" + }, + "battery_level": { + "name": "Battery level" + }, + "temperature": { + "name": "[%key:component::sensor::entity_component::temperature::name%]" + }, + "voltage": { + "name": "[%key:component::sensor::entity_component::voltage::name%]" + }, + "current": { + "name": "[%key:component::sensor::entity_component::current::name%]" + }, + "humidity": { + "name": "[%key:component::sensor::entity_component::humidity::name%]" + }, + "device_time": { + "name": "Device time" + }, + "auto_off_at": { + "name": "Auto off at" + }, + "report_interval": { + "name": "Report interval" + }, + "alarm_source": { + "name": "Alarm source" + }, + "rssi": { + "name": "[%key:component::sensor::entity_component::signal_strength::name%]" } }, "switch": { "led": { "name": "LED" + }, + "auto_update_enabled": { + "name": "Auto update enabled" + }, + "auto_off_enabled": { + "name": "Auto off enabled" + }, + "smooth_transitions": { + "name": "Smooth transitions" + }, + "fan_sleep_mode": { + "name": "Fan sleep mode" + } + }, + "number": { + "smooth_transition_on": { + "name": "Smooth on" + }, + "smooth_transition_off": { + "name": "Smooth off" + }, + "auto_off_minutes": { + "name": "Turn off in" + }, + "temperature_offset": { + "name": "Temperature offset" } } }, diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index da3dda9c041..2520de9dd3e 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -1,158 +1,112 @@ -"""Support for TPLink HS100/HS110/HS200 smart switch.""" +"""Support for TPLink switch entities.""" from __future__ import annotations +from dataclasses import dataclass import logging -from typing import Any, cast +from typing import Any -from kasa import SmartDevice, SmartPlug +from kasa import Device, Feature -from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import legacy_device_id -from .const import DOMAIN +from . import TPLinkConfigEntry from .coordinator import TPLinkDataUpdateCoordinator -from .entity import CoordinatedTPLinkEntity, async_refresh_after -from .models import TPLinkData +from .entity import ( + CoordinatedTPLinkFeatureEntity, + TPLinkFeatureEntityDescription, + async_refresh_after, +) _LOGGER = logging.getLogger(__name__) +@dataclass(frozen=True, kw_only=True) +class TPLinkSwitchEntityDescription( + SwitchEntityDescription, TPLinkFeatureEntityDescription +): + """Base class for a TPLink feature based sensor entity description.""" + + +SWITCH_DESCRIPTIONS: tuple[TPLinkSwitchEntityDescription, ...] = ( + TPLinkSwitchEntityDescription( + key="state", + ), + TPLinkSwitchEntityDescription( + key="led", + ), + TPLinkSwitchEntityDescription( + key="auto_update_enabled", + ), + TPLinkSwitchEntityDescription( + key="auto_off_enabled", + ), + TPLinkSwitchEntityDescription( + key="smooth_transitions", + ), + TPLinkSwitchEntityDescription( + key="fan_sleep_mode", + ), +) + +SWITCH_DESCRIPTIONS_MAP = {desc.key: desc for desc in SWITCH_DESCRIPTIONS} + + async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: TPLinkConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up switches.""" - data: TPLinkData = hass.data[DOMAIN][config_entry.entry_id] + data = config_entry.runtime_data parent_coordinator = data.parent_coordinator - device = cast(SmartPlug, parent_coordinator.device) - if not device.is_plug and not device.is_strip and not device.is_dimmer: - return - entities: list = [] - if device.is_strip: - # Historically we only add the children if the device is a strip - _LOGGER.debug("Initializing strip with %s sockets", len(device.children)) - entities.extend( - SmartPlugSwitchChild(device, parent_coordinator, child) - for child in device.children - ) - elif device.is_plug: - entities.append(SmartPlugSwitch(device, parent_coordinator)) + device = parent_coordinator.device - # this will be removed on the led is implemented - if hasattr(device, "led"): - entities.append(SmartPlugLedSwitch(device, parent_coordinator)) + entities = CoordinatedTPLinkFeatureEntity.entities_for_device_and_its_children( + device, + coordinator=parent_coordinator, + feature_type=Feature.Switch, + entity_class=TPLinkSwitch, + descriptions=SWITCH_DESCRIPTIONS_MAP, + ) async_add_entities(entities) -class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): - """Representation of switch for the LED of a TPLink Smart Plug.""" +class TPLinkSwitch(CoordinatedTPLinkFeatureEntity, SwitchEntity): + """Representation of a feature-based TPLink switch.""" - device: SmartPlug - - _attr_translation_key = "led" - _attr_entity_category = EntityCategory.CONFIG - - def __init__( - self, device: SmartPlug, coordinator: TPLinkDataUpdateCoordinator - ) -> None: - """Initialize the LED switch.""" - super().__init__(device, coordinator) - self._attr_unique_id = f"{device.mac}_led" - self._async_update_attrs() - - @async_refresh_after - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the LED switch on.""" - await self.device.set_led(True) - - @async_refresh_after - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the LED switch off.""" - await self.device.set_led(False) - - @callback - def _async_update_attrs(self) -> None: - """Update the entity's attributes.""" - self._attr_is_on = self.device.led - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._async_update_attrs() - super()._handle_coordinator_update() - - -class SmartPlugSwitch(CoordinatedTPLinkEntity, SwitchEntity): - """Representation of a TPLink Smart Plug switch.""" - - _attr_name: str | None = None + entity_description: TPLinkSwitchEntityDescription def __init__( self, - device: SmartDevice, + device: Device, coordinator: TPLinkDataUpdateCoordinator, + *, + feature: Feature, + description: TPLinkSwitchEntityDescription, + parent: Device | None = None, ) -> None: """Initialize the switch.""" - super().__init__(device, coordinator) - # For backwards compat with pyHS100 - self._attr_unique_id = legacy_device_id(device) - self._async_update_attrs() + super().__init__( + device, coordinator, description=description, feature=feature, parent=parent + ) + + self._async_call_update_attrs() @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - await self.device.turn_on() + await self._feature.set_value(True) @async_refresh_after async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - await self.device.turn_off() + await self._feature.set_value(False) @callback def _async_update_attrs(self) -> None: """Update the entity's attributes.""" - self._attr_is_on = self.device.is_on - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._async_update_attrs() - super()._handle_coordinator_update() - - -class SmartPlugSwitchChild(SmartPlugSwitch): - """Representation of an individual plug of a TPLink Smart Plug strip.""" - - def __init__( - self, - device: SmartDevice, - coordinator: TPLinkDataUpdateCoordinator, - plug: SmartDevice, - ) -> None: - """Initialize the child switch.""" - self._plug = plug - super().__init__(device, coordinator) - self._attr_unique_id = legacy_device_id(plug) - self._attr_name = plug.alias - - @async_refresh_after - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the child switch on.""" - await self._plug.turn_on() - - @async_refresh_after - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the child switch off.""" - await self._plug.turn_off() - - @callback - def _async_update_attrs(self) -> None: - """Update the entity's attributes.""" - self._attr_is_on = self._plug.is_on + self._attr_is_on = self._feature.value diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 3b5fe9843f2..e898f64d128 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -650,6 +650,11 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "k[lps]*", "macaddress": "5091E3*", }, + { + "domain": "tplink", + "hostname": "p1*", + "macaddress": "5091E3*", + }, { "domain": "tplink", "hostname": "k[lps]*", @@ -870,16 +875,31 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "s5*", "macaddress": "3C52A1*", }, + { + "domain": "tplink", + "hostname": "h1*", + "macaddress": "3C52A1*", + }, { "domain": "tplink", "hostname": "l9*", "macaddress": "A842A1*", }, + { + "domain": "tplink", + "hostname": "p1*", + "macaddress": "A842A1*", + }, { "domain": "tplink", "hostname": "l9*", "macaddress": "3460F9*", }, + { + "domain": "tplink", + "hostname": "p1*", + "macaddress": "3460F9*", + }, { "domain": "tplink", "hostname": "hs*", @@ -890,6 +910,11 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "k[lps]*", "macaddress": "74DA88*", }, + { + "domain": "tplink", + "hostname": "p1*", + "macaddress": "74DA88*", + }, { "domain": "tplink", "hostname": "p3*", @@ -930,6 +955,16 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "l9*", "macaddress": "F0A731*", }, + { + "domain": "tplink", + "hostname": "ks2*", + "macaddress": "F0A731*", + }, + { + "domain": "tplink", + "hostname": "kh1*", + "macaddress": "F0A731*", + }, { "domain": "tuya", "macaddress": "105A17*", diff --git a/requirements_all.txt b/requirements_all.txt index b75555283a2..de167c2f7e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2278,7 +2278,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.6.2.1 +python-kasa[speedups]==0.7.0.1 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37b3700372b..8eb468b0947 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1778,7 +1778,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.6.2.1 +python-kasa[speedups]==0.7.0.1 # homeassistant.components.matter python-matter-server==6.1.0 diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index d1454d12e68..9c8aeb99be1 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -1,21 +1,24 @@ """Tests for the TP-Link component.""" +from collections import namedtuple +from datetime import datetime +from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from kasa import ( - ConnectionType, + Device, DeviceConfig, - DeviceFamilyType, - EncryptType, - SmartBulb, - SmartDevice, - SmartDimmer, - SmartLightStrip, - SmartPlug, - SmartStrip, + DeviceConnectionParameters, + DeviceEncryptionType, + DeviceFamily, + DeviceType, + Feature, + KasaException, + Module, ) -from kasa.exceptions import SmartDeviceException +from kasa.interfaces import Fan, Light, LightEffect, LightState from kasa.protocol import BaseProtocol +from syrupy import SnapshotAssertion from homeassistant.components.tplink import ( CONF_ALIAS, @@ -25,9 +28,17 @@ from homeassistant.components.tplink import ( Credentials, ) from homeassistant.components.tplink.const import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.translation import async_get_translations +from homeassistant.helpers.typing import UNDEFINED +from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_json_value_fixture + +ColorTempRange = namedtuple("ColorTempRange", ["min", "max"]) MODULE = "homeassistant.components.tplink" MODULE_CONFIG_FLOW = "homeassistant.components.tplink.config_flow" @@ -36,6 +47,7 @@ IP_ADDRESS2 = "127.0.0.2" ALIAS = "My Bulb" MODEL = "HS100" MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" +DEVICE_ID = "123456789ABCDEFGH" DHCP_FORMATTED_MAC_ADDRESS = MAC_ADDRESS.replace(":", "") MAC_ADDRESS2 = "11:22:33:44:55:66" DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" @@ -49,16 +61,16 @@ CREDENTIALS_HASH_AUTH = "abcdefghijklmnopqrstuv==" DEVICE_CONFIG_AUTH = DeviceConfig( IP_ADDRESS, credentials=CREDENTIALS, - connection_type=ConnectionType( - DeviceFamilyType.IotSmartPlugSwitch, EncryptType.Klap + connection_type=DeviceConnectionParameters( + DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Klap ), uses_http=True, ) DEVICE_CONFIG_AUTH2 = DeviceConfig( IP_ADDRESS2, credentials=CREDENTIALS, - connection_type=ConnectionType( - DeviceFamilyType.IotSmartPlugSwitch, EncryptType.Klap + connection_type=DeviceConnectionParameters( + DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Klap ), uses_http=True, ) @@ -90,190 +102,316 @@ CREATE_ENTRY_DATA_AUTH2 = { } +def _load_feature_fixtures(): + fixtures = load_json_value_fixture("features.json", DOMAIN) + for fixture in fixtures.values(): + if isinstance(fixture["value"], str): + try: + time = datetime.strptime(fixture["value"], "%Y-%m-%d %H:%M:%S.%f%z") + fixture["value"] = time + except ValueError: + pass + return fixtures + + +FEATURES_FIXTURE = _load_feature_fixtures() + + +async def setup_platform_for_device( + hass: HomeAssistant, config_entry: ConfigEntry, platform: Platform, device: Device +): + """Set up a single tplink platform with a device.""" + config_entry.add_to_hass(hass) + + with ( + patch("homeassistant.components.tplink.PLATFORMS", [platform]), + _patch_discovery(device=device), + _patch_connect(device=device), + ): + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + # Good practice to wait background tasks in tests see PR #112726 + await hass.async_block_till_done(wait_background_tasks=True) + + +async def snapshot_platform( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, + config_entry_id: str, +) -> None: + """Snapshot a platform.""" + device_entries = dr.async_entries_for_config_entry(device_registry, config_entry_id) + assert device_entries + for device_entry in device_entries: + assert device_entry == snapshot( + name=f"{device_entry.name}-entry" + ), f"device entry snapshot failed for {device_entry.name}" + + entity_entries = er.async_entries_for_config_entry(entity_registry, config_entry_id) + assert entity_entries + assert ( + len({entity_entry.domain for entity_entry in entity_entries}) == 1 + ), "Please limit the loaded platforms to 1 platform." + + translations = await async_get_translations(hass, "en", "entity", [DOMAIN]) + for entity_entry in entity_entries: + if entity_entry.translation_key: + key = f"component.{DOMAIN}.entity.{entity_entry.domain}.{entity_entry.translation_key}.name" + assert ( + key in translations + ), f"No translation for entity {entity_entry.unique_id}, expected {key}" + assert entity_entry == snapshot( + name=f"{entity_entry.entity_id}-entry" + ), f"entity entry snapshot failed for {entity_entry.entity_id}" + if entity_entry.disabled_by is None: + state = hass.states.get(entity_entry.entity_id) + assert state, f"State not found for {entity_entry.entity_id}" + assert state == snapshot( + name=f"{entity_entry.entity_id}-state" + ), f"state snapshot failed for {entity_entry.entity_id}" + + def _mock_protocol() -> BaseProtocol: - protocol = MagicMock(auto_spec=BaseProtocol) + protocol = MagicMock(spec=BaseProtocol) protocol.close = AsyncMock() return protocol -def _mocked_bulb( +def _mocked_device( device_config=DEVICE_CONFIG_LEGACY, credentials_hash=CREDENTIALS_HASH_LEGACY, mac=MAC_ADDRESS, + device_id=DEVICE_ID, alias=ALIAS, -) -> SmartBulb: - bulb = MagicMock(auto_spec=SmartBulb, name="Mocked bulb") - bulb.update = AsyncMock() - bulb.mac = mac - bulb.alias = alias - bulb.model = MODEL - bulb.host = IP_ADDRESS - bulb.brightness = 50 - bulb.color_temp = 4000 - bulb.is_color = True - bulb.is_strip = False - bulb.is_plug = False - bulb.is_dimmer = False - bulb.is_light_strip = False - bulb.has_effects = False - bulb.effect = None - bulb.effect_list = None - bulb.hsv = (10, 30, 5) - bulb.device_id = mac - bulb.valid_temperature_range.min = 4000 - bulb.valid_temperature_range.max = 9000 - bulb.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} - bulb.turn_off = AsyncMock() - bulb.turn_on = AsyncMock() - bulb.set_brightness = AsyncMock() - bulb.set_hsv = AsyncMock() - bulb.set_color_temp = AsyncMock() - bulb.protocol = _mock_protocol() - bulb.config = device_config - bulb.credentials_hash = credentials_hash - return bulb + model=MODEL, + ip_address=IP_ADDRESS, + modules: list[str] | None = None, + children: list[Device] | None = None, + features: list[str | Feature] | None = None, + device_type=None, + spec: type = Device, +) -> Device: + device = MagicMock(spec=spec, name="Mocked device") + device.update = AsyncMock() + device.turn_off = AsyncMock() + device.turn_on = AsyncMock() + + device.mac = mac + device.alias = alias + device.model = model + device.host = ip_address + device.device_id = device_id + device.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} + device.modules = {} + device.features = {} + + if modules: + device.modules = { + module_name: MODULE_TO_MOCK_GEN[module_name]() for module_name in modules + } + + if features: + device.features = { + feature_id: _mocked_feature(feature_id, require_fixture=True) + for feature_id in features + if isinstance(feature_id, str) + } + + device.features.update( + { + feature.id: feature + for feature in features + if isinstance(feature, Feature) + } + ) + device.children = [] + if children: + for child in children: + child.mac = mac + device.children = children + device.device_type = device_type if device_type else DeviceType.Unknown + if ( + not device_type + and device.children + and all( + child.device_type is DeviceType.StripSocket for child in device.children + ) + ): + device.device_type = DeviceType.Strip + + device.protocol = _mock_protocol() + device.config = device_config + device.credentials_hash = credentials_hash + return device -class MockedSmartLightStrip(SmartLightStrip): - """Mock a SmartLightStrip.""" +def _mocked_feature( + id: str, + *, + require_fixture=False, + value: Any = UNDEFINED, + name=None, + type_=None, + category=None, + precision_hint=None, + choices=None, + unit=None, + minimum_value=0, + maximum_value=2**16, # Arbitrary max +) -> Feature: + """Get a mocked feature. - def __new__(cls, *args, **kwargs): - """Mock a SmartLightStrip that will pass an isinstance check.""" - return MagicMock(spec=cls) + If kwargs are provided they will override the attributes for any features defined in fixtures.json + """ + feature = MagicMock(spec=Feature, name=f"Mocked {id} feature") + feature.id = id + feature.name = name or id.upper() + feature.set_value = AsyncMock() + if not (fixture := FEATURES_FIXTURE.get(id)): + assert ( + require_fixture is False + ), f"No fixture defined for feature {id} and require_fixture is True" + assert ( + value is not UNDEFINED + ), f"Value must be provided if feature {id} not defined in features.json" + fixture = {"value": value, "category": "Primary", "type": "Sensor"} + elif value is not UNDEFINED: + fixture["value"] = value + feature.value = fixture["value"] + + feature.type = type_ or Feature.Type[fixture["type"]] + feature.category = category or Feature.Category[fixture["category"]] + + # sensor + feature.precision_hint = precision_hint or fixture.get("precision_hint") + feature.unit = unit or fixture.get("unit") + + # number + feature.minimum_value = minimum_value or fixture.get("minimum_value") + feature.maximum_value = maximum_value or fixture.get("maximum_value") + + # select + feature.choices = choices or fixture.get("choices") + return feature -def _mocked_smart_light_strip() -> SmartLightStrip: - strip = MockedSmartLightStrip() - strip.update = AsyncMock() - strip.mac = MAC_ADDRESS - strip.alias = ALIAS - strip.model = MODEL - strip.host = IP_ADDRESS - strip.brightness = 50 - strip.color_temp = 4000 - strip.is_color = True - strip.is_strip = False - strip.is_plug = False - strip.is_dimmer = False - strip.is_light_strip = True - strip.has_effects = True - strip.effect = {"name": "Effect1", "enable": 1} - strip.effect_list = ["Effect1", "Effect2"] - strip.hsv = (10, 30, 5) - strip.device_id = MAC_ADDRESS - strip.valid_temperature_range.min = 4000 - strip.valid_temperature_range.max = 9000 - strip.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} - strip.turn_off = AsyncMock() - strip.turn_on = AsyncMock() - strip.set_brightness = AsyncMock() - strip.set_hsv = AsyncMock() - strip.set_color_temp = AsyncMock() - strip.set_effect = AsyncMock() - strip.set_custom_effect = AsyncMock() - strip.protocol = _mock_protocol() - strip.config = DEVICE_CONFIG_LEGACY - strip.credentials_hash = CREDENTIALS_HASH_LEGACY - return strip +def _mocked_light_module() -> Light: + light = MagicMock(spec=Light, name="Mocked light module") + light.update = AsyncMock() + light.brightness = 50 + light.color_temp = 4000 + light.state = LightState( + light_on=True, brightness=light.brightness, color_temp=light.color_temp + ) + light.is_color = True + light.is_variable_color_temp = True + light.is_dimmable = True + light.is_brightness = True + light.has_effects = False + light.hsv = (10, 30, 5) + light.valid_temperature_range = ColorTempRange(min=4000, max=9000) + light.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} + light.set_state = AsyncMock() + light.set_brightness = AsyncMock() + light.set_hsv = AsyncMock() + light.set_color_temp = AsyncMock() + light.protocol = _mock_protocol() + return light -def _mocked_dimmer() -> SmartDimmer: - dimmer = MagicMock(auto_spec=SmartDimmer, name="Mocked dimmer") - dimmer.update = AsyncMock() - dimmer.mac = MAC_ADDRESS - dimmer.alias = "My Dimmer" - dimmer.model = MODEL - dimmer.host = IP_ADDRESS - dimmer.brightness = 50 - dimmer.color_temp = 4000 - dimmer.is_color = True - dimmer.is_strip = False - dimmer.is_plug = False - dimmer.is_dimmer = True - dimmer.is_light_strip = False - dimmer.effect = None - dimmer.effect_list = None - dimmer.hsv = (10, 30, 5) - dimmer.device_id = MAC_ADDRESS - dimmer.valid_temperature_range.min = 4000 - dimmer.valid_temperature_range.max = 9000 - dimmer.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} - dimmer.turn_off = AsyncMock() - dimmer.turn_on = AsyncMock() - dimmer.set_brightness = AsyncMock() - dimmer.set_hsv = AsyncMock() - dimmer.set_color_temp = AsyncMock() - dimmer.set_led = AsyncMock() - dimmer.protocol = _mock_protocol() - dimmer.config = DEVICE_CONFIG_LEGACY - dimmer.credentials_hash = CREDENTIALS_HASH_LEGACY - return dimmer +def _mocked_light_effect_module() -> LightEffect: + effect = MagicMock(spec=LightEffect, name="Mocked light effect") + effect.has_effects = True + effect.has_custom_effects = True + effect.effect = "Effect1" + effect.effect_list = ["Off", "Effect1", "Effect2"] + effect.set_effect = AsyncMock() + effect.set_custom_effect = AsyncMock() + return effect -def _mocked_plug() -> SmartPlug: - plug = MagicMock(auto_spec=SmartPlug, name="Mocked plug") - plug.update = AsyncMock() - plug.mac = MAC_ADDRESS - plug.alias = "My Plug" - plug.model = MODEL - plug.host = IP_ADDRESS - plug.is_light_strip = False - plug.is_bulb = False - plug.is_dimmer = False - plug.is_strip = False - plug.is_plug = True - plug.device_id = MAC_ADDRESS - plug.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} - plug.turn_off = AsyncMock() - plug.turn_on = AsyncMock() - plug.set_led = AsyncMock() - plug.protocol = _mock_protocol() - plug.config = DEVICE_CONFIG_LEGACY - plug.credentials_hash = CREDENTIALS_HASH_LEGACY - return plug +def _mocked_fan_module() -> Fan: + fan = MagicMock(auto_spec=Fan, name="Mocked fan") + fan.fan_speed_level = 0 + fan.set_fan_speed_level = AsyncMock() + return fan -def _mocked_strip() -> SmartStrip: - strip = MagicMock(auto_spec=SmartStrip, name="Mocked strip") - strip.update = AsyncMock() - strip.mac = MAC_ADDRESS - strip.alias = "My Strip" - strip.model = MODEL - strip.host = IP_ADDRESS - strip.is_light_strip = False - strip.is_bulb = False - strip.is_dimmer = False - strip.is_strip = True - strip.is_plug = True - strip.device_id = MAC_ADDRESS - strip.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} - strip.turn_off = AsyncMock() - strip.turn_on = AsyncMock() - strip.set_led = AsyncMock() - strip.protocol = _mock_protocol() - strip.config = DEVICE_CONFIG_LEGACY - strip.credentials_hash = CREDENTIALS_HASH_LEGACY - plug0 = _mocked_plug() - plug0.alias = "Plug0" - plug0.device_id = "bb:bb:cc:dd:ee:ff_PLUG0DEVICEID" - plug0.mac = "bb:bb:cc:dd:ee:ff" +def _mocked_strip_children(features=None, alias=None) -> list[Device]: + plug0 = _mocked_device( + alias="Plug0" if alias is None else alias, + device_id="bb:bb:cc:dd:ee:ff_PLUG0DEVICEID", + mac="bb:bb:cc:dd:ee:ff", + device_type=DeviceType.StripSocket, + features=features, + ) + plug1 = _mocked_device( + alias="Plug1" if alias is None else alias, + device_id="cc:bb:cc:dd:ee:ff_PLUG1DEVICEID", + mac="cc:bb:cc:dd:ee:ff", + device_type=DeviceType.StripSocket, + features=features, + ) plug0.is_on = True - plug0.protocol = _mock_protocol() - plug1 = _mocked_plug() - plug1.device_id = "cc:bb:cc:dd:ee:ff_PLUG1DEVICEID" - plug1.mac = "cc:bb:cc:dd:ee:ff" - plug1.alias = "Plug1" - plug1.protocol = _mock_protocol() plug1.is_on = False - strip.children = [plug0, plug1] - return strip + return [plug0, plug1] + + +def _mocked_energy_features( + power=None, total=None, voltage=None, current=None, today=None +) -> list[Feature]: + feats = [] + if power is not None: + feats.append( + _mocked_feature( + "current_consumption", + value=power, + ) + ) + if total is not None: + feats.append( + _mocked_feature( + "consumption_total", + value=total, + ) + ) + if voltage is not None: + feats.append( + _mocked_feature( + "voltage", + value=voltage, + ) + ) + if current is not None: + feats.append( + _mocked_feature( + "current", + value=current, + ) + ) + # Today is always reported as 0 by the library rather than none + feats.append( + _mocked_feature( + "consumption_today", + value=today if today is not None else 0.0, + ) + ) + return feats + + +MODULE_TO_MOCK_GEN = { + Module.Light: _mocked_light_module, + Module.LightEffect: _mocked_light_effect_module, + Module.Fan: _mocked_fan_module, +} def _patch_discovery(device=None, no_device=False): async def _discovery(*args, **kwargs): if no_device: return {} - return {IP_ADDRESS: _mocked_bulb()} + return {IP_ADDRESS: _mocked_device()} return patch("homeassistant.components.tplink.Discover.discover", new=_discovery) @@ -281,8 +419,8 @@ def _patch_discovery(device=None, no_device=False): def _patch_single_discovery(device=None, no_device=False): async def _discover_single(*args, **kwargs): if no_device: - raise SmartDeviceException - return device if device else _mocked_bulb() + raise KasaException + return device if device else _mocked_device() return patch( "homeassistant.components.tplink.Discover.discover_single", new=_discover_single @@ -292,14 +430,14 @@ def _patch_single_discovery(device=None, no_device=False): def _patch_connect(device=None, no_device=False): async def _connect(*args, **kwargs): if no_device: - raise SmartDeviceException - return device if device else _mocked_bulb() + raise KasaException + return device if device else _mocked_device() - return patch("homeassistant.components.tplink.SmartDevice.connect", new=_connect) + return patch("homeassistant.components.tplink.Device.connect", new=_connect) async def initialize_config_entry_for_device( - hass: HomeAssistant, dev: SmartDevice + hass: HomeAssistant, dev: Device ) -> MockConfigEntry: """Create a mocked configuration entry for the given device. diff --git a/tests/components/tplink/conftest.py b/tests/components/tplink/conftest.py index 88da9b699a7..f8d933de71e 100644 --- a/tests/components/tplink/conftest.py +++ b/tests/components/tplink/conftest.py @@ -17,7 +17,7 @@ from . import ( IP_ADDRESS2, MAC_ADDRESS, MAC_ADDRESS2, - _mocked_bulb, + _mocked_device, ) from tests.common import MockConfigEntry, mock_device_registry, mock_registry @@ -31,13 +31,13 @@ def mock_discovery(): discover=DEFAULT, discover_single=DEFAULT, ) as mock_discovery: - device = _mocked_bulb( + device = _mocked_device( device_config=copy.deepcopy(DEVICE_CONFIG_AUTH), credentials_hash=CREDENTIALS_HASH_AUTH, alias=None, ) devices = { - "127.0.0.1": _mocked_bulb( + "127.0.0.1": _mocked_device( device_config=copy.deepcopy(DEVICE_CONFIG_AUTH), credentials_hash=CREDENTIALS_HASH_AUTH, alias=None, @@ -52,12 +52,12 @@ def mock_discovery(): @pytest.fixture def mock_connect(): """Mock python-kasa connect.""" - with patch("homeassistant.components.tplink.SmartDevice.connect") as mock_connect: + with patch("homeassistant.components.tplink.Device.connect") as mock_connect: devices = { - IP_ADDRESS: _mocked_bulb( + IP_ADDRESS: _mocked_device( device_config=DEVICE_CONFIG_AUTH, credentials_hash=CREDENTIALS_HASH_AUTH ), - IP_ADDRESS2: _mocked_bulb( + IP_ADDRESS2: _mocked_device( device_config=DEVICE_CONFIG_AUTH, credentials_hash=CREDENTIALS_HASH_AUTH, mac=MAC_ADDRESS2, diff --git a/tests/components/tplink/fixtures/features.json b/tests/components/tplink/fixtures/features.json new file mode 100644 index 00000000000..daf86a74643 --- /dev/null +++ b/tests/components/tplink/fixtures/features.json @@ -0,0 +1,287 @@ +{ + "state": { + "value": true, + "type": "Switch", + "category": "Primary" + }, + "led": { + "value": true, + "type": "Switch", + "category": "Config" + }, + "auto_update_enabled": { + "value": true, + "type": "Switch", + "category": "Config" + }, + "auto_off_enabled": { + "value": true, + "type": "Switch", + "category": "Config" + }, + "smooth_transitions": { + "value": true, + "type": "Switch", + "category": "Config" + }, + "frost_protection_enabled": { + "value": true, + "type": "Switch", + "category": "Config" + }, + "fan_sleep_mode": { + "value": false, + "type": "Switch", + "category": "Config" + }, + "current_consumption": { + "value": 5.23, + "type": "Sensor", + "category": "Primary", + "unit": "W", + "precision_hint": 1 + }, + "consumption_today": { + "value": 5.23, + "type": "Sensor", + "category": "Info", + "unit": "kWh", + "precision_hint": 3 + }, + "consumption_this_month": { + "value": 15.345, + "type": "Sensor", + "category": "Info", + "unit": "kWh", + "precision_hint": 3 + }, + "consumption_total": { + "value": 30.0049, + "type": "Sensor", + "category": "Info", + "unit": "kWh", + "precision_hint": 3 + }, + "current": { + "value": 5.035, + "type": "Sensor", + "category": "Primary", + "unit": "A", + "precision_hint": 2 + }, + "voltage": { + "value": 121.1, + "type": "Sensor", + "category": "Primary", + "unit": "v", + "precision_hint": 1 + }, + "device_id": { + "value": "94hd2dn298812je12u0931828", + "type": "Sensor", + "category": "Debug" + }, + "signal_level": { + "value": 2, + "type": "Sensor", + "category": "Info" + }, + "rssi": { + "value": -62, + "type": "Sensor", + "category": "Debug" + }, + "ssid": { + "value": "HOMEWIFI", + "type": "Sensor", + "category": "Debug" + }, + "on_since": { + "value": "2024-06-24 10:03:11.046643+01:00", + "type": "Sensor", + "category": "Debug" + }, + "battery_level": { + "value": 85, + "type": "Sensor", + "category": "Info", + "unit": "%" + }, + "auto_off_at": { + "value": "2024-06-24 10:03:11.046643+01:00", + "type": "Sensor", + "category": "Info" + }, + "humidity": { + "value": 12, + "type": "Sensor", + "category": "Primary", + "unit": "%" + }, + "report_interval": { + "value": 16, + "type": "Sensor", + "category": "Debug", + "unit": "%" + }, + "alarm_source": { + "value": "", + "type": "Sensor", + "category": "Debug" + }, + "device_time": { + "value": "2024-06-24 10:03:11.046643+01:00", + "type": "Sensor", + "category": "Debug" + }, + "temperature": { + "value": 19.2, + "type": "Sensor", + "category": "Debug", + "unit": "celsius" + }, + "current_firmware_version": { + "value": "1.1.2", + "type": "Sensor", + "category": "Debug" + }, + "available_firmware_version": { + "value": "1.1.3", + "type": "Sensor", + "category": "Debug" + }, + "thermostat_mode": { + "value": "off", + "type": "Sensor", + "category": "Primary" + }, + "overheated": { + "value": false, + "type": "BinarySensor", + "category": "Info" + }, + "battery_low": { + "value": false, + "type": "BinarySensor", + "category": "Debug" + }, + "update_available": { + "value": false, + "type": "BinarySensor", + "category": "Info" + }, + "cloud_connection": { + "value": false, + "type": "BinarySensor", + "category": "Info" + }, + "temperature_warning": { + "value": false, + "type": "BinarySensor", + "category": "Debug" + }, + "humidity_warning": { + "value": false, + "type": "BinarySensor", + "category": "Debug" + }, + "water_alert": { + "value": false, + "type": "BinarySensor", + "category": "Primary" + }, + "is_open": { + "value": false, + "type": "BinarySensor", + "category": "Primary" + }, + "test_alarm": { + "value": "", + "type": "Action", + "category": "Config" + }, + "stop_alarm": { + "value": "", + "type": "Action", + "category": "Config" + }, + "smooth_transition_on": { + "value": false, + "type": "Number", + "category": "Config", + "minimum_value": 0, + "maximum_value": 60 + }, + "smooth_transition_off": { + "value": false, + "type": "Number", + "category": "Config", + "minimum_value": 0, + "maximum_value": 60 + }, + "auto_off_minutes": { + "value": false, + "type": "Number", + "category": "Config", + "unit": "min", + "minimum_value": 0, + "maximum_value": 60 + }, + "temperature_offset": { + "value": false, + "type": "Number", + "category": "Config", + "minimum_value": -10, + "maximum_value": 10 + }, + "target_temperature": { + "value": false, + "type": "Number", + "category": "Primary" + }, + "fan_speed_level": { + "value": 2, + "type": "Number", + "category": "Primary", + "minimum_value": 0, + "maximum_value": 4 + }, + "light_preset": { + "value": "Off", + "type": "Choice", + "category": "Config", + "choices": ["Off", "Preset 1", "Preset 2"] + }, + "alarm_sound": { + "value": "Phone Ring", + "type": "Choice", + "category": "Config", + "choices": [ + "Doorbell Ring 1", + "Doorbell Ring 2", + "Doorbell Ring 3", + "Doorbell Ring 4", + "Doorbell Ring 5", + "Doorbell Ring 6", + "Doorbell Ring 7", + "Doorbell Ring 8", + "Doorbell Ring 9", + "Doorbell Ring 10", + "Phone Ring", + "Alarm 1", + "Alarm 2", + "Alarm 3", + "Alarm 4", + "Dripping Tap", + "Alarm 5", + "Connection 1", + "Connection 2" + ] + }, + "alarm_volume": { + "value": "normal", + "type": "Choice", + "category": "Config", + "choices": ["low", "normal", "high"] + } +} diff --git a/tests/components/tplink/snapshots/test_binary_sensor.ambr b/tests/components/tplink/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..27b1372df27 --- /dev/null +++ b/tests/components/tplink/snapshots/test_binary_sensor.ambr @@ -0,0 +1,369 @@ +# serializer version: 1 +# name: test_states[binary_sensor.my_device_battery_low-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.my_device_battery_low', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery low', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_low', + 'unique_id': '123456789ABCDEFGH_battery_low', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_cloud_connection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.my_device_cloud_connection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cloud connection', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_connection', + 'unique_id': '123456789ABCDEFGH_cloud_connection', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_cloud_connection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'my_device Cloud connection', + }), + 'context': , + 'entity_id': 'binary_sensor.my_device_cloud_connection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[binary_sensor.my_device_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.my_device_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'is_open', + 'unique_id': '123456789ABCDEFGH_is_open', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'my_device Door', + }), + 'context': , + 'entity_id': 'binary_sensor.my_device_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[binary_sensor.my_device_humidity_warning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.my_device_humidity_warning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Humidity warning', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_warning', + 'unique_id': '123456789ABCDEFGH_humidity_warning', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_moisture-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.my_device_moisture', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Moisture', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_alert', + 'unique_id': '123456789ABCDEFGH_water_alert', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_moisture-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'moisture', + 'friendly_name': 'my_device Moisture', + }), + 'context': , + 'entity_id': 'binary_sensor.my_device_moisture', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[binary_sensor.my_device_overheated-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.my_device_overheated', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overheated', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'overheated', + 'unique_id': '123456789ABCDEFGH_overheated', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_overheated-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'my_device Overheated', + }), + 'context': , + 'entity_id': 'binary_sensor.my_device_overheated', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[binary_sensor.my_device_temperature_warning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.my_device_temperature_warning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature warning', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_warning', + 'unique_id': '123456789ABCDEFGH_temperature_warning', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_update-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.my_device_update', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Update', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'update_available', + 'unique_id': '123456789ABCDEFGH_update_available', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_update-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'update', + 'friendly_name': 'my_device Update', + }), + 'context': , + 'entity_id': 'binary_sensor.my_device_update', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[my_device-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'my_device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/tplink/snapshots/test_button.ambr b/tests/components/tplink/snapshots/test_button.ambr new file mode 100644 index 00000000000..f26829101f7 --- /dev/null +++ b/tests/components/tplink/snapshots/test_button.ambr @@ -0,0 +1,127 @@ +# serializer version: 1 +# name: test_states[button.my_device_stop_alarm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_stop_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Stop alarm', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'stop_alarm', + 'unique_id': '123456789ABCDEFGH_stop_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[button.my_device_stop_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Stop alarm', + }), + 'context': , + 'entity_id': 'button.my_device_stop_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_states[button.my_device_test_alarm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_test_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Test alarm', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'test_alarm', + 'unique_id': '123456789ABCDEFGH_test_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[button.my_device_test_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Test alarm', + }), + 'context': , + 'entity_id': 'button.my_device_test_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_states[my_device-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'my_device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/tplink/snapshots/test_climate.ambr b/tests/components/tplink/snapshots/test_climate.ambr new file mode 100644 index 00000000000..d30f8cd3532 --- /dev/null +++ b/tests/components/tplink/snapshots/test_climate.ambr @@ -0,0 +1,94 @@ +# serializer version: 1 +# name: test_states[climate.thermostat-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 65536, + 'min_temp': None, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.thermostat', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '123456789ABCDEFGH_climate', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[climate.thermostat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20, + 'friendly_name': 'thermostat', + 'hvac_action': , + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 65536, + 'min_temp': None, + 'supported_features': , + 'temperature': 22, + }), + 'context': , + 'entity_id': 'climate.thermostat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_states[thermostat-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'thermostat', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/tplink/snapshots/test_fan.ambr b/tests/components/tplink/snapshots/test_fan.ambr new file mode 100644 index 00000000000..d692abdce03 --- /dev/null +++ b/tests/components/tplink/snapshots/test_fan.ambr @@ -0,0 +1,194 @@ +# serializer version: 1 +# name: test_states[fan.my_device-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': None, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.my_device', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '123456789ABCDEFGH', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[fan.my_device-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device', + 'percentage': None, + 'percentage_step': 25.0, + 'preset_mode': None, + 'preset_modes': None, + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.my_device', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[fan.my_device_my_fan_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': None, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.my_device_my_fan_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'my_fan_0', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '123456789ABCDEFGH00', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[fan.my_device_my_fan_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device my_fan_0', + 'percentage': None, + 'percentage_step': 25.0, + 'preset_mode': None, + 'preset_modes': None, + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.my_device_my_fan_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[fan.my_device_my_fan_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': None, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.my_device_my_fan_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'my_fan_1', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '123456789ABCDEFGH01', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[fan.my_device_my_fan_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device my_fan_1', + 'percentage': None, + 'percentage_step': 25.0, + 'preset_mode': None, + 'preset_modes': None, + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.my_device_my_fan_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[my_device-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'my_device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/tplink/snapshots/test_number.ambr b/tests/components/tplink/snapshots/test_number.ambr new file mode 100644 index 00000000000..9bfc9c0126a --- /dev/null +++ b/tests/components/tplink/snapshots/test_number.ambr @@ -0,0 +1,255 @@ +# serializer version: 1 +# name: test_states[my_device-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'my_device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- +# name: test_states[number.my_device_smooth_off-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.my_device_smooth_off', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Smooth off', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smooth_transition_off', + 'unique_id': '123456789ABCDEFGH_smooth_transition_off', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[number.my_device_smooth_off-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Smooth off', + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.my_device_smooth_off', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'False', + }) +# --- +# name: test_states[number.my_device_smooth_on-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.my_device_smooth_on', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Smooth on', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smooth_transition_on', + 'unique_id': '123456789ABCDEFGH_smooth_transition_on', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[number.my_device_smooth_on-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Smooth on', + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.my_device_smooth_on', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'False', + }) +# --- +# name: test_states[number.my_device_temperature_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65536, + 'min': -10, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.my_device_temperature_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature offset', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_offset', + 'unique_id': '123456789ABCDEFGH_temperature_offset', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[number.my_device_temperature_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Temperature offset', + 'max': 65536, + 'min': -10, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.my_device_temperature_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'False', + }) +# --- +# name: test_states[number.my_device_turn_off_in-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.my_device_turn_off_in', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Turn off in', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_off_minutes', + 'unique_id': '123456789ABCDEFGH_auto_off_minutes', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[number.my_device_turn_off_in-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Turn off in', + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.my_device_turn_off_in', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'False', + }) +# --- diff --git a/tests/components/tplink/snapshots/test_select.ambr b/tests/components/tplink/snapshots/test_select.ambr new file mode 100644 index 00000000000..2cf02415238 --- /dev/null +++ b/tests/components/tplink/snapshots/test_select.ambr @@ -0,0 +1,238 @@ +# serializer version: 1 +# name: test_states[my_device-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'my_device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- +# name: test_states[select.my_device_alarm_sound-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Doorbell Ring 1', + 'Doorbell Ring 2', + 'Doorbell Ring 3', + 'Doorbell Ring 4', + 'Doorbell Ring 5', + 'Doorbell Ring 6', + 'Doorbell Ring 7', + 'Doorbell Ring 8', + 'Doorbell Ring 9', + 'Doorbell Ring 10', + 'Phone Ring', + 'Alarm 1', + 'Alarm 2', + 'Alarm 3', + 'Alarm 4', + 'Dripping Tap', + 'Alarm 5', + 'Connection 1', + 'Connection 2', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.my_device_alarm_sound', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm sound', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_sound', + 'unique_id': '123456789ABCDEFGH_alarm_sound', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[select.my_device_alarm_sound-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Alarm sound', + 'options': list([ + 'Doorbell Ring 1', + 'Doorbell Ring 2', + 'Doorbell Ring 3', + 'Doorbell Ring 4', + 'Doorbell Ring 5', + 'Doorbell Ring 6', + 'Doorbell Ring 7', + 'Doorbell Ring 8', + 'Doorbell Ring 9', + 'Doorbell Ring 10', + 'Phone Ring', + 'Alarm 1', + 'Alarm 2', + 'Alarm 3', + 'Alarm 4', + 'Dripping Tap', + 'Alarm 5', + 'Connection 1', + 'Connection 2', + ]), + }), + 'context': , + 'entity_id': 'select.my_device_alarm_sound', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Phone Ring', + }) +# --- +# name: test_states[select.my_device_alarm_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'low', + 'normal', + 'high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.my_device_alarm_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm volume', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_volume', + 'unique_id': '123456789ABCDEFGH_alarm_volume', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[select.my_device_alarm_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Alarm volume', + 'options': list([ + 'low', + 'normal', + 'high', + ]), + }), + 'context': , + 'entity_id': 'select.my_device_alarm_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'normal', + }) +# --- +# name: test_states[select.my_device_light_preset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Off', + 'Preset 1', + 'Preset 2', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.my_device_light_preset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light preset', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light_preset', + 'unique_id': '123456789ABCDEFGH_light_preset', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[select.my_device_light_preset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Light preset', + 'options': list([ + 'Off', + 'Preset 1', + 'Preset 2', + ]), + }), + 'context': , + 'entity_id': 'select.my_device_light_preset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Off', + }) +# --- diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..cd8980bf57f --- /dev/null +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -0,0 +1,790 @@ +# serializer version: 1 +# name: test_states[my_device-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'my_device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- +# name: test_states[sensor.my_device_alarm_source-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_alarm_source', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm source', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_source', + 'unique_id': '123456789ABCDEFGH_alarm_source', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_auto_off_at-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_auto_off_at', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Auto off at', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_off_at', + 'unique_id': '123456789ABCDEFGH_auto_off_at', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_auto_off_at-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'my_device Auto off at', + }), + 'context': , + 'entity_id': 'sensor.my_device_auto_off_at', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-06-24T09:03:11+00:00', + }) +# --- +# name: test_states[sensor.my_device_battery_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_battery_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery level', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_level', + 'unique_id': '123456789ABCDEFGH_battery_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_states[sensor.my_device_battery_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'my_device Battery level', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.my_device_battery_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '85', + }) +# --- +# name: test_states[sensor.my_device_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_device_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': '123456789ABCDEFGH_current_a', + 'unit_of_measurement': 'A', + }) +# --- +# name: test_states[sensor.my_device_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'my_device Current', + 'state_class': , + 'unit_of_measurement': 'A', + }), + 'context': , + 'entity_id': 'sensor.my_device_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.04', + }) +# --- +# name: test_states[sensor.my_device_current_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_device_current_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current consumption', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_consumption', + 'unique_id': '123456789ABCDEFGH_current_power_w', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_states[sensor.my_device_current_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'my_device Current consumption', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.my_device_current_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.2', + }) +# --- +# name: test_states[sensor.my_device_device_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_device_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Device time', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_time', + 'unique_id': '123456789ABCDEFGH_device_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_device_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': '123456789ABCDEFGH_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_states[sensor.my_device_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'my_device Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.my_device_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_states[sensor.my_device_on_since-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_on_since', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'On since', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'on_since', + 'unique_id': '123456789ABCDEFGH_on_since', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_report_interval-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_report_interval', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Report interval', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'report_interval', + 'unique_id': '123456789ABCDEFGH_report_interval', + 'unit_of_measurement': '%', + }) +# --- +# name: test_states[sensor.my_device_signal_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_signal_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Signal level', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'signal_level', + 'unique_id': '123456789ABCDEFGH_signal_level', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_signal_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Signal level', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.my_device_signal_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_states[sensor.my_device_signal_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_signal_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Signal strength', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'rssi', + 'unique_id': '123456789ABCDEFGH_rssi', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_ssid-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_ssid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SSID', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ssid', + 'unique_id': '123456789ABCDEFGH_ssid', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': '123456789ABCDEFGH_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_states[sensor.my_device_this_month_s_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_this_month_s_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': "This month's consumption", + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_this_month', + 'unique_id': '123456789ABCDEFGH_consumption_this_month', + 'unit_of_measurement': 'kWh', + }) +# --- +# name: test_states[sensor.my_device_this_month_s_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': "my_device This month's consumption", + 'state_class': , + 'unit_of_measurement': 'kWh', + }), + 'context': , + 'entity_id': 'sensor.my_device_this_month_s_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.345', + }) +# --- +# name: test_states[sensor.my_device_today_s_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_today_s_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': "Today's consumption", + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_today', + 'unique_id': '123456789ABCDEFGH_today_energy_kwh', + 'unit_of_measurement': 'kWh', + }) +# --- +# name: test_states[sensor.my_device_today_s_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': "my_device Today's consumption", + 'state_class': , + 'unit_of_measurement': 'kWh', + }), + 'context': , + 'entity_id': 'sensor.my_device_today_s_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.23', + }) +# --- +# name: test_states[sensor.my_device_total_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_total_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total consumption', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_total', + 'unique_id': '123456789ABCDEFGH_total_energy_kwh', + 'unit_of_measurement': 'kWh', + }) +# --- +# name: test_states[sensor.my_device_total_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'my_device Total consumption', + 'state_class': , + 'unit_of_measurement': 'kWh', + }), + 'context': , + 'entity_id': 'sensor.my_device_total_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.005', + }) +# --- +# name: test_states[sensor.my_device_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_device_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': '123456789ABCDEFGH_voltage', + 'unit_of_measurement': 'v', + }) +# --- +# name: test_states[sensor.my_device_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'my_device Voltage', + 'state_class': , + 'unit_of_measurement': 'v', + }), + 'context': , + 'entity_id': 'sensor.my_device_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '121.1', + }) +# --- diff --git a/tests/components/tplink/snapshots/test_switch.ambr b/tests/components/tplink/snapshots/test_switch.ambr new file mode 100644 index 00000000000..2fe1f6e6b08 --- /dev/null +++ b/tests/components/tplink/snapshots/test_switch.ambr @@ -0,0 +1,311 @@ +# serializer version: 1 +# name: test_states[my_device-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'name': 'my_device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- +# name: test_states[switch.my_device-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.my_device', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789ABCDEFGH', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device', + }), + 'context': , + 'entity_id': 'switch.my_device', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_states[switch.my_device_auto_off_enabled-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.my_device_auto_off_enabled', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto off enabled', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_off_enabled', + 'unique_id': '123456789ABCDEFGH_auto_off_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_auto_off_enabled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Auto off enabled', + }), + 'context': , + 'entity_id': 'switch.my_device_auto_off_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_states[switch.my_device_auto_update_enabled-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.my_device_auto_update_enabled', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto update enabled', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_update_enabled', + 'unique_id': '123456789ABCDEFGH_auto_update_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_auto_update_enabled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Auto update enabled', + }), + 'context': , + 'entity_id': 'switch.my_device_auto_update_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_states[switch.my_device_fan_sleep_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.my_device_fan_sleep_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fan sleep mode', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fan_sleep_mode', + 'unique_id': '123456789ABCDEFGH_fan_sleep_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_fan_sleep_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Fan sleep mode', + }), + 'context': , + 'entity_id': 'switch.my_device_fan_sleep_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[switch.my_device_led-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.my_device_led', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'LED', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'led', + 'unique_id': '123456789ABCDEFGH_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_led-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device LED', + }), + 'context': , + 'entity_id': 'switch.my_device_led', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_states[switch.my_device_smooth_transitions-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.my_device_smooth_transitions', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Smooth transitions', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smooth_transitions', + 'unique_id': '123456789ABCDEFGH_smooth_transitions', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_smooth_transitions-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Smooth transitions', + }), + 'context': , + 'entity_id': 'switch.my_device_smooth_transitions', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/tplink/test_binary_sensor.py b/tests/components/tplink/test_binary_sensor.py new file mode 100644 index 00000000000..e2b9cd08d13 --- /dev/null +++ b/tests/components/tplink/test_binary_sensor.py @@ -0,0 +1,124 @@ +"""Tests for tplink binary_sensor platform.""" + +from kasa import Feature +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components import tplink +from homeassistant.components.tplink.binary_sensor import BINARY_SENSOR_DESCRIPTIONS +from homeassistant.components.tplink.const import DOMAIN +from homeassistant.components.tplink.entity import EXCLUDED_FEATURES +from homeassistant.const import CONF_HOST, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from . import ( + DEVICE_ID, + MAC_ADDRESS, + _mocked_device, + _mocked_feature, + _mocked_strip_children, + _patch_connect, + _patch_discovery, + setup_platform_for_device, + snapshot_platform, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mocked_feature_binary_sensor() -> Feature: + """Return mocked tplink binary sensor feature.""" + return _mocked_feature( + "overheated", + value=False, + name="Overheated", + type_=Feature.Type.BinarySensor, + category=Feature.Category.Primary, + ) + + +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test a sensor unique ids.""" + features = {description.key for description in BINARY_SENSOR_DESCRIPTIONS} + features.update(EXCLUDED_FEATURES) + device = _mocked_device(alias="my_device", features=features) + + await setup_platform_for_device( + hass, mock_config_entry, Platform.BINARY_SENSOR, device + ) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + for excluded in EXCLUDED_FEATURES: + assert hass.states.get(f"sensor.my_device_{excluded}") is None + + +async def test_binary_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mocked_feature_binary_sensor: Feature, +) -> None: + """Test a sensor unique ids.""" + mocked_feature = mocked_feature_binary_sensor + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + + plug = _mocked_device(alias="my_plug", features=[mocked_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + # The entity_id is based on standard name from core. + entity_id = "binary_sensor.my_plug_overheated" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_{mocked_feature.id}" + + +async def test_binary_sensor_children( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mocked_feature_binary_sensor: Feature, +) -> None: + """Test a sensor unique ids.""" + mocked_feature = mocked_feature_binary_sensor + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + plug = _mocked_device( + alias="my_plug", + features=[mocked_feature], + children=_mocked_strip_children(features=[mocked_feature]), + ) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "binary_sensor.my_plug_overheated" + entity = entity_registry.async_get(entity_id) + assert entity + device = device_registry.async_get(entity.device_id) + + for plug_id in range(2): + child_entity_id = f"binary_sensor.my_plug_plug{plug_id}_overheated" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_{mocked_feature.id}" + assert child_entity.device_id != entity.device_id + child_device = device_registry.async_get(child_entity.device_id) + assert child_device + assert child_device.via_device_id == device.id diff --git a/tests/components/tplink/test_button.py b/tests/components/tplink/test_button.py new file mode 100644 index 00000000000..143a882a6cb --- /dev/null +++ b/tests/components/tplink/test_button.py @@ -0,0 +1,153 @@ +"""Tests for tplink button platform.""" + +from kasa import Feature +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components import tplink +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.tplink.button import BUTTON_DESCRIPTIONS +from homeassistant.components.tplink.const import DOMAIN +from homeassistant.components.tplink.entity import EXCLUDED_FEATURES +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from . import ( + DEVICE_ID, + MAC_ADDRESS, + _mocked_device, + _mocked_feature, + _mocked_strip_children, + _patch_connect, + _patch_discovery, + setup_platform_for_device, + snapshot_platform, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mocked_feature_button() -> Feature: + """Return mocked tplink binary sensor feature.""" + return _mocked_feature( + "test_alarm", + value="", + name="Test alarm", + type_=Feature.Type.Action, + category=Feature.Category.Primary, + ) + + +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test a sensor unique ids.""" + features = {description.key for description in BUTTON_DESCRIPTIONS} + features.update(EXCLUDED_FEATURES) + device = _mocked_device(alias="my_device", features=features) + + await setup_platform_for_device(hass, mock_config_entry, Platform.BUTTON, device) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + for excluded in EXCLUDED_FEATURES: + assert hass.states.get(f"sensor.my_device_{excluded}") is None + + +async def test_button( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mocked_feature_button: Feature, +) -> None: + """Test a sensor unique ids.""" + mocked_feature = mocked_feature_button + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + + plug = _mocked_device(alias="my_plug", features=[mocked_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + # The entity_id is based on standard name from core. + entity_id = "button.my_plug_test_alarm" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_{mocked_feature.id}" + + +async def test_button_children( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mocked_feature_button: Feature, +) -> None: + """Test a sensor unique ids.""" + mocked_feature = mocked_feature_button + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + plug = _mocked_device( + alias="my_plug", + features=[mocked_feature], + children=_mocked_strip_children(features=[mocked_feature]), + ) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "button.my_plug_test_alarm" + entity = entity_registry.async_get(entity_id) + assert entity + device = device_registry.async_get(entity.device_id) + + for plug_id in range(2): + child_entity_id = f"button.my_plug_plug{plug_id}_test_alarm" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_{mocked_feature.id}" + assert child_entity.device_id != entity.device_id + child_device = device_registry.async_get(child_entity.device_id) + assert child_device + assert child_device.via_device_id == device.id + + +async def test_button_press( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mocked_feature_button: Feature, +) -> None: + """Test a number entity limits and setting values.""" + mocked_feature = mocked_feature_button + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + plug = _mocked_device(alias="my_plug", features=[mocked_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "button.my_plug_test_alarm" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_test_alarm" + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mocked_feature.set_value.assert_called_with(True) diff --git a/tests/components/tplink/test_climate.py b/tests/components/tplink/test_climate.py new file mode 100644 index 00000000000..a80a74a5697 --- /dev/null +++ b/tests/components/tplink/test_climate.py @@ -0,0 +1,226 @@ +"""Tests for tplink climate platform.""" + +from datetime import timedelta + +from kasa import Device, Feature +from kasa.smart.modules.temperaturecontrol import ThermostatState +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.climate import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_ACTION, + ATTR_HVAC_MODE, + ATTR_TEMPERATURE, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + HVACAction, + HVACMode, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util import dt as dt_util + +from . import ( + DEVICE_ID, + _mocked_device, + _mocked_feature, + setup_platform_for_device, + snapshot_platform, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + +ENTITY_ID = "climate.thermostat" + + +@pytest.fixture +async def mocked_hub(hass: HomeAssistant) -> Device: + """Return mocked tplink binary sensor feature.""" + + features = [ + _mocked_feature( + "temperature", value=20, category=Feature.Category.Primary, unit="celsius" + ), + _mocked_feature( + "target_temperature", + value=22, + type_=Feature.Type.Number, + category=Feature.Category.Primary, + unit="celsius", + ), + _mocked_feature( + "state", + value=True, + type_=Feature.Type.Switch, + category=Feature.Category.Primary, + ), + _mocked_feature( + "thermostat_mode", + value=ThermostatState.Heating, + type_=Feature.Type.Choice, + category=Feature.Category.Primary, + ), + ] + + thermostat = _mocked_device( + alias="thermostat", features=features, device_type=Device.Type.Thermostat + ) + + return _mocked_device( + alias="hub", children=[thermostat], device_type=Device.Type.Hub + ) + + +async def test_climate( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + mocked_hub: Device, +) -> None: + """Test initialization.""" + await setup_platform_for_device( + hass, mock_config_entry, Platform.CLIMATE, mocked_hub + ) + + entity = entity_registry.async_get(ENTITY_ID) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_climate" + + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_HVAC_ACTION] is HVACAction.HEATING + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 20 + assert state.attributes[ATTR_TEMPERATURE] == 22 + + +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, + mocked_hub: Device, +) -> None: + """Snapshot test.""" + await setup_platform_for_device( + hass, mock_config_entry, Platform.CLIMATE, mocked_hub + ) + + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + +async def test_set_temperature( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mocked_hub: Device +) -> None: + """Test that set_temperature service calls the setter.""" + await setup_platform_for_device( + hass, mock_config_entry, Platform.CLIMATE, mocked_hub + ) + + mocked_thermostat = mocked_hub.children[0] + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 10}, + blocking=True, + ) + target_temp_feature = mocked_thermostat.features["target_temperature"] + target_temp_feature.set_value.assert_called_with(10) + + +async def test_set_hvac_mode( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mocked_hub: Device +) -> None: + """Test that set_hvac_mode service works.""" + await setup_platform_for_device( + hass, mock_config_entry, Platform.CLIMATE, mocked_hub + ) + + mocked_thermostat = mocked_hub.children[0] + mocked_state = mocked_thermostat.features["state"] + assert mocked_state is not None + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.OFF}, + blocking=True, + ) + + mocked_state.set_value.assert_called_with(False) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: [ENTITY_ID], ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + mocked_state.set_value.assert_called_with(True) + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: [ENTITY_ID], ATTR_HVAC_MODE: HVACMode.DRY}, + blocking=True, + ) + + +async def test_turn_on_and_off( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mocked_hub: Device +) -> None: + """Test that turn_on and turn_off services work as expected.""" + await setup_platform_for_device( + hass, mock_config_entry, Platform.CLIMATE, mocked_hub + ) + + mocked_thermostat = mocked_hub.children[0] + mocked_state = mocked_thermostat.features["state"] + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [ENTITY_ID]}, + blocking=True, + ) + + mocked_state.set_value.assert_called_with(False) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [ENTITY_ID]}, + blocking=True, + ) + + mocked_state.set_value.assert_called_with(True) + + +async def test_unknown_mode( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mocked_hub: Device, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that unknown device modes log a warning and default to off.""" + await setup_platform_for_device( + hass, mock_config_entry, Platform.CLIMATE, mocked_hub + ) + + mocked_thermostat = mocked_hub.children[0] + mocked_state = mocked_thermostat.features["thermostat_mode"] + mocked_state.value = ThermostatState.Unknown + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF + assert "Unknown thermostat state, defaulting to OFF" in caplog.text diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index 7bf3b8cce5e..7560ff4a72d 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -2,17 +2,17 @@ from unittest.mock import AsyncMock, patch -from kasa import TimeoutException +from kasa import TimeoutError import pytest from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.tplink import ( DOMAIN, - AuthenticationException, + AuthenticationError, Credentials, DeviceConfig, - SmartDeviceException, + KasaException, ) from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG from homeassistant.config_entries import ConfigEntryState @@ -40,7 +40,7 @@ from . import ( MAC_ADDRESS, MAC_ADDRESS2, MODULE, - _mocked_bulb, + _mocked_device, _patch_connect, _patch_discovery, _patch_single_discovery, @@ -120,7 +120,7 @@ async def test_discovery_auth( ) -> None: """Test authenticated discovery.""" - mock_discovery["mock_device"].update.side_effect = AuthenticationException + mock_discovery["mock_device"].update.side_effect = AuthenticationError result = await hass.config_entries.flow.async_init( DOMAIN, @@ -155,8 +155,8 @@ async def test_discovery_auth( @pytest.mark.parametrize( ("error_type", "errors_msg", "error_placement"), [ - (AuthenticationException("auth_error_details"), "invalid_auth", CONF_PASSWORD), - (SmartDeviceException("smart_device_error_details"), "cannot_connect", "base"), + (AuthenticationError("auth_error_details"), "invalid_auth", CONF_PASSWORD), + (KasaException("smart_device_error_details"), "cannot_connect", "base"), ], ids=["invalid-auth", "unknown-error"], ) @@ -170,7 +170,7 @@ async def test_discovery_auth_errors( error_placement, ) -> None: """Test handling of discovery authentication errors.""" - mock_discovery["mock_device"].update.side_effect = AuthenticationException + mock_discovery["mock_device"].update.side_effect = AuthenticationError default_connect_side_effect = mock_connect["connect"].side_effect mock_connect["connect"].side_effect = error_type @@ -223,7 +223,7 @@ async def test_discovery_new_credentials( mock_init, ) -> None: """Test setting up discovery with new credentials.""" - mock_discovery["mock_device"].update.side_effect = AuthenticationException + mock_discovery["mock_device"].update.side_effect = AuthenticationError result = await hass.config_entries.flow.async_init( DOMAIN, @@ -272,10 +272,10 @@ async def test_discovery_new_credentials_invalid( mock_init, ) -> None: """Test setting up discovery with new invalid credentials.""" - mock_discovery["mock_device"].update.side_effect = AuthenticationException + mock_discovery["mock_device"].update.side_effect = AuthenticationError default_connect_side_effect = mock_connect["connect"].side_effect - mock_connect["connect"].side_effect = AuthenticationException + mock_connect["connect"].side_effect = AuthenticationError result = await hass.config_entries.flow.async_init( DOMAIN, @@ -514,7 +514,7 @@ async def test_manual_auth( assert result["step_id"] == "user" assert not result["errors"] - mock_discovery["mock_device"].update.side_effect = AuthenticationException + mock_discovery["mock_device"].update.side_effect = AuthenticationError result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: IP_ADDRESS} @@ -544,8 +544,8 @@ async def test_manual_auth( @pytest.mark.parametrize( ("error_type", "errors_msg", "error_placement"), [ - (AuthenticationException("auth_error_details"), "invalid_auth", CONF_PASSWORD), - (SmartDeviceException("smart_device_error_details"), "cannot_connect", "base"), + (AuthenticationError("auth_error_details"), "invalid_auth", CONF_PASSWORD), + (KasaException("smart_device_error_details"), "cannot_connect", "base"), ], ids=["invalid-auth", "unknown-error"], ) @@ -566,7 +566,7 @@ async def test_manual_auth_errors( assert result["step_id"] == "user" assert not result["errors"] - mock_discovery["mock_device"].update.side_effect = AuthenticationException + mock_discovery["mock_device"].update.side_effect = AuthenticationError default_connect_side_effect = mock_connect["connect"].side_effect mock_connect["connect"].side_effect = error_type @@ -765,7 +765,7 @@ async def test_integration_discovery_with_ip_change( mock_connect: AsyncMock, ) -> None: """Test reauth flow.""" - mock_connect["connect"].side_effect = SmartDeviceException() + mock_connect["connect"].side_effect = KasaException() mock_config_entry.add_to_hass(hass) with patch("homeassistant.components.tplink.Discover.discover", return_value={}): await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -797,7 +797,7 @@ async def test_integration_discovery_with_ip_change( config = DeviceConfig.from_dict(DEVICE_CONFIG_DICT_AUTH) mock_connect["connect"].reset_mock(side_effect=True) - bulb = _mocked_bulb( + bulb = _mocked_device( device_config=config, mac=mock_config_entry.unique_id, ) @@ -818,7 +818,7 @@ async def test_dhcp_discovery_with_ip_change( mock_connect: AsyncMock, ) -> None: """Test dhcp discovery with an IP change.""" - mock_connect["connect"].side_effect = SmartDeviceException() + mock_connect["connect"].side_effect = KasaException() mock_config_entry.add_to_hass(hass) with patch("homeassistant.components.tplink.Discover.discover", return_value={}): await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -883,7 +883,7 @@ async def test_reauth_update_from_discovery( mock_connect: AsyncMock, ) -> None: """Test reauth flow.""" - mock_connect["connect"].side_effect = AuthenticationException + mock_connect["connect"].side_effect = AuthenticationError mock_config_entry.add_to_hass(hass) with patch("homeassistant.components.tplink.Discover.discover", return_value={}): await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -920,7 +920,7 @@ async def test_reauth_update_from_discovery_with_ip_change( mock_connect: AsyncMock, ) -> None: """Test reauth flow.""" - mock_connect["connect"].side_effect = AuthenticationException() + mock_connect["connect"].side_effect = AuthenticationError() mock_config_entry.add_to_hass(hass) with patch("homeassistant.components.tplink.Discover.discover", return_value={}): await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -957,7 +957,7 @@ async def test_reauth_no_update_if_config_and_ip_the_same( mock_connect: AsyncMock, ) -> None: """Test reauth discovery does not update when the host and config are the same.""" - mock_connect["connect"].side_effect = AuthenticationException() + mock_connect["connect"].side_effect = AuthenticationError() mock_config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( mock_config_entry, @@ -996,8 +996,8 @@ async def test_reauth_no_update_if_config_and_ip_the_same( @pytest.mark.parametrize( ("error_type", "errors_msg", "error_placement"), [ - (AuthenticationException("auth_error_details"), "invalid_auth", CONF_PASSWORD), - (SmartDeviceException("smart_device_error_details"), "cannot_connect", "base"), + (AuthenticationError("auth_error_details"), "invalid_auth", CONF_PASSWORD), + (KasaException("smart_device_error_details"), "cannot_connect", "base"), ], ids=["invalid-auth", "unknown-error"], ) @@ -1060,8 +1060,8 @@ async def test_reauth_errors( @pytest.mark.parametrize( ("error_type", "expected_flow"), [ - (AuthenticationException, FlowResultType.FORM), - (SmartDeviceException, FlowResultType.ABORT), + (AuthenticationError, FlowResultType.FORM), + (KasaException, FlowResultType.ABORT), ], ids=["invalid-auth", "unknown-error"], ) @@ -1119,7 +1119,7 @@ async def test_discovery_timeout_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_discovery["discover_single"].side_effect = TimeoutException + mock_discovery["discover_single"].side_effect = TimeoutError await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -1149,7 +1149,7 @@ async def test_reauth_update_other_flows( unique_id=MAC_ADDRESS2, ) default_side_effect = mock_connect["connect"].side_effect - mock_connect["connect"].side_effect = AuthenticationException() + mock_connect["connect"].side_effect = AuthenticationError() mock_config_entry.add_to_hass(hass) mock_config_entry2.add_to_hass(hass) with patch("homeassistant.components.tplink.Discover.discover", return_value={}): diff --git a/tests/components/tplink/test_diagnostics.py b/tests/components/tplink/test_diagnostics.py index 3543cf95572..7288d631f4a 100644 --- a/tests/components/tplink/test_diagnostics.py +++ b/tests/components/tplink/test_diagnostics.py @@ -2,12 +2,12 @@ import json -from kasa import SmartDevice +from kasa import Device import pytest from homeassistant.core import HomeAssistant -from . import _mocked_bulb, _mocked_plug, initialize_config_entry_for_device +from . import _mocked_device, initialize_config_entry_for_device from tests.common import load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -18,13 +18,13 @@ from tests.typing import ClientSessionGenerator ("mocked_dev", "fixture_file", "sysinfo_vars", "expected_oui"), [ ( - _mocked_bulb(), + _mocked_device(), "tplink-diagnostics-data-bulb-kl130.json", ["mic_mac", "deviceId", "oemId", "hwId", "alias"], "AA:BB:CC", ), ( - _mocked_plug(), + _mocked_device(), "tplink-diagnostics-data-plug-hs110.json", ["mac", "deviceId", "oemId", "hwId", "alias", "longitude_i", "latitude_i"], "AA:BB:CC", @@ -34,7 +34,7 @@ from tests.typing import ClientSessionGenerator async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - mocked_dev: SmartDevice, + mocked_dev: Device, fixture_file: str, sysinfo_vars: list[str], expected_oui: str | None, diff --git a/tests/components/tplink/test_fan.py b/tests/components/tplink/test_fan.py new file mode 100644 index 00000000000..deba33abfa5 --- /dev/null +++ b/tests/components/tplink/test_fan.py @@ -0,0 +1,154 @@ +"""Tests for fan platform.""" + +from __future__ import annotations + +from datetime import timedelta + +from kasa import Device, Module +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.fan import ( + ATTR_PERCENTAGE, + DOMAIN as FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +import homeassistant.util.dt as dt_util + +from . import DEVICE_ID, _mocked_device, setup_platform_for_device, snapshot_platform + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test a fan state.""" + child_fan_1 = _mocked_device( + modules=[Module.Fan], alias="my_fan_0", device_id=f"{DEVICE_ID}00" + ) + child_fan_2 = _mocked_device( + modules=[Module.Fan], alias="my_fan_1", device_id=f"{DEVICE_ID}01" + ) + parent_device = _mocked_device( + device_id=DEVICE_ID, + alias="my_device", + children=[child_fan_1, child_fan_2], + modules=[Module.Fan], + device_type=Device.Type.WallSwitch, + ) + + await setup_platform_for_device( + hass, mock_config_entry, Platform.FAN, parent_device + ) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + +async def test_fan_unique_id( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test a fan unique id.""" + fan = _mocked_device(modules=[Module.Fan], alias="my_fan") + await setup_platform_for_device(hass, mock_config_entry, Platform.FAN, fan) + + device_entries = dr.async_entries_for_config_entry( + device_registry, mock_config_entry.entry_id + ) + assert device_entries + entity_id = "fan.my_fan" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == DEVICE_ID + + +async def test_fan(hass: HomeAssistant, mock_config_entry: MockConfigEntry) -> None: + """Test a color fan and that all transitions are correctly passed.""" + device = _mocked_device(modules=[Module.Fan], alias="my_fan") + fan = device.modules[Module.Fan] + fan.fan_speed_level = 0 + await setup_platform_for_device(hass, mock_config_entry, Platform.FAN, device) + + entity_id = "fan.my_fan" + + state = hass.states.get(entity_id) + assert state.state == "off" + + await hass.services.async_call( + FAN_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + fan.set_fan_speed_level.assert_called_once_with(4) + fan.set_fan_speed_level.reset_mock() + + fan.fan_speed_level = 4 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get(entity_id) + assert state.state == "on" + + await hass.services.async_call( + FAN_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + fan.set_fan_speed_level.assert_called_once_with(0) + fan.set_fan_speed_level.reset_mock() + + await hass.services.async_call( + FAN_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 50}, + blocking=True, + ) + fan.set_fan_speed_level.assert_called_once_with(2) + fan.set_fan_speed_level.reset_mock() + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 25}, + blocking=True, + ) + fan.set_fan_speed_level.assert_called_once_with(1) + fan.set_fan_speed_level.reset_mock() + + +async def test_fan_child( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test child fans are added to parent device with the right ids.""" + child_fan_1 = _mocked_device( + modules=[Module.Fan], alias="my_fan_0", device_id=f"{DEVICE_ID}00" + ) + child_fan_2 = _mocked_device( + modules=[Module.Fan], alias="my_fan_1", device_id=f"{DEVICE_ID}01" + ) + parent_device = _mocked_device( + device_id=DEVICE_ID, + alias="my_device", + children=[child_fan_1, child_fan_2], + modules=[Module.Fan], + device_type=Device.Type.WallSwitch, + ) + await setup_platform_for_device( + hass, mock_config_entry, Platform.FAN, parent_device + ) + + entity_id = "fan.my_device" + entity = entity_registry.async_get(entity_id) + assert entity + + for fan_id in range(2): + child_entity_id = f"fan.my_device_my_fan_{fan_id}" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"{DEVICE_ID}0{fan_id}" + assert child_entity.device_id == entity.device_id diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 481a9e0e2b3..61ec9decc10 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -4,10 +4,10 @@ from __future__ import annotations import copy from datetime import timedelta -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from freezegun.api import FrozenDateTimeFactory -from kasa.exceptions import AuthenticationException +from kasa import AuthenticationError, Feature, KasaException, Module import pytest from homeassistant import setup @@ -21,19 +21,20 @@ from homeassistant.const import ( CONF_USERNAME, STATE_ON, STATE_UNAVAILABLE, + EntityCategory, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import EntityRegistry +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 ( CREATE_ENTRY_DATA_AUTH, + CREATE_ENTRY_DATA_LEGACY, DEVICE_CONFIG_AUTH, IP_ADDRESS, MAC_ADDRESS, - _mocked_dimmer, - _mocked_plug, + _mocked_device, _patch_connect, _patch_discovery, _patch_single_discovery, @@ -100,12 +101,12 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( - hass: HomeAssistant, entity_reg: EntityRegistry + hass: HomeAssistant, entity_reg: er.EntityRegistry ) -> None: """Test no migration happens if the original entity id still exists.""" config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) config_entry.add_to_hass(hass) - dimmer = _mocked_dimmer() + dimmer = _mocked_device(alias="My dimmer", modules=[Module.Light]) rollout_unique_id = MAC_ADDRESS.replace(":", "").upper() original_unique_id = tplink.legacy_device_id(dimmer) original_dimmer_entity_reg = entity_reg.async_get_or_create( @@ -129,7 +130,7 @@ async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( _patch_connect(device=dimmer), ): await setup.async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) migrated_dimmer_entity_reg = entity_reg.async_get_or_create( config_entry=config_entry, @@ -238,8 +239,8 @@ async def test_config_entry_device_config_invalid( @pytest.mark.parametrize( ("error_type", "entry_state", "reauth_flows"), [ - (tplink.AuthenticationException, ConfigEntryState.SETUP_ERROR, True), - (tplink.SmartDeviceException, ConfigEntryState.SETUP_RETRY, False), + (tplink.AuthenticationError, ConfigEntryState.SETUP_ERROR, True), + (tplink.KasaException, ConfigEntryState.SETUP_RETRY, False), ], ids=["invalid-auth", "unknown-error"], ) @@ -275,15 +276,15 @@ async def test_plug_auth_fails(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) config_entry.add_to_hass(hass) - plug = _mocked_plug() - with _patch_discovery(device=plug), _patch_connect(device=plug): + device = _mocked_device(alias="my_plug", features=["state"]) + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() entity_id = "switch.my_plug" state = hass.states.get(entity_id) assert state.state == STATE_ON - plug.update = AsyncMock(side_effect=AuthenticationException) + device.update = AsyncMock(side_effect=AuthenticationError) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() @@ -298,3 +299,166 @@ async def test_plug_auth_fails(hass: HomeAssistant) -> None: ) == 1 ) + + +async def test_update_attrs_fails_in_init( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test a smart plug auth failure.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + config_entry.add_to_hass(hass) + light = _mocked_device(modules=[Module.Light], alias="my_light") + light_module = light.modules[Module.Light] + p = PropertyMock(side_effect=KasaException) + type(light_module).color_temp = p + light.__str__ = lambda _: "MockLight" + with _patch_discovery(device=light), _patch_connect(device=light): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_light" + entity = entity_registry.async_get(entity_id) + assert entity + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + assert "Unable to read data for MockLight None:" in caplog.text + + +async def test_update_attrs_fails_on_update( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test a smart plug auth failure.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + config_entry.add_to_hass(hass) + light = _mocked_device(modules=[Module.Light], alias="my_light") + light_module = light.modules[Module.Light] + + with _patch_discovery(device=light), _patch_connect(device=light): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_light" + entity = entity_registry.async_get(entity_id) + assert entity + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + p = PropertyMock(side_effect=KasaException) + type(light_module).color_temp = p + light.__str__ = lambda _: "MockLight" + freezer.tick(5) + async_fire_time_changed(hass) + entity = entity_registry.async_get(entity_id) + assert entity + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + assert f"Unable to read data for MockLight {entity_id}:" in caplog.text + # Check only logs once + caplog.clear() + freezer.tick(5) + async_fire_time_changed(hass) + entity = entity_registry.async_get(entity_id) + assert entity + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + assert f"Unable to read data for MockLight {entity_id}:" not in caplog.text + + +async def test_feature_no_category( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test a strip unique id.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + dev = _mocked_device( + alias="my_plug", + features=["led"], + ) + dev.features["led"].category = Feature.Category.Unset + with _patch_discovery(device=dev), _patch_connect(device=dev): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "switch.my_plug_led" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.entity_category == EntityCategory.DIAGNOSTIC + assert "Unhandled category Category.Unset, fallback to DIAGNOSTIC" in caplog.text + + +@pytest.mark.parametrize( + ("identifier_base", "expected_message", "expected_count"), + [ + pytest.param("C0:06:C3:42:54:2B", "Replaced", 1, id="success"), + pytest.param("123456789", "Unable to replace", 3, id="failure"), + ], +) +async def test_unlink_devices( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + caplog: pytest.LogCaptureFixture, + identifier_base, + expected_message, + expected_count, +) -> None: + """Test for unlinking child device ids.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={**CREATE_ENTRY_DATA_LEGACY}, + entry_id="123456", + unique_id="any", + version=1, + minor_version=2, + ) + entry.add_to_hass(hass) + + # Setup initial device registry, with linkages + mac = "C0:06:C3:42:54:2B" + identifiers = [ + (DOMAIN, identifier_base), + (DOMAIN, f"{identifier_base}_0001"), + (DOMAIN, f"{identifier_base}_0002"), + ] + device_registry.async_get_or_create( + config_entry_id="123456", + connections={ + (dr.CONNECTION_NETWORK_MAC, mac.lower()), + }, + identifiers=set(identifiers), + model="hs300", + name="dummy", + ) + device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + + assert device_entries[0].connections == { + (dr.CONNECTION_NETWORK_MAC, mac.lower()), + } + assert device_entries[0].identifiers == set(identifiers) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + + assert device_entries[0].connections == {(dr.CONNECTION_NETWORK_MAC, mac.lower())} + # If expected count is 1 will be the first identifier only + expected_identifiers = identifiers[:expected_count] + assert device_entries[0].identifiers == set(expected_identifiers) + assert entry.version == 1 + assert entry.minor_version == 3 + + msg = f"{expected_message} identifiers for device dummy (hs300): {set(identifiers)}" + assert msg in caplog.text diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 9f352e7ffc4..c2f40f47e3d 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -5,7 +5,16 @@ from __future__ import annotations from datetime import timedelta from unittest.mock import MagicMock, PropertyMock -from kasa import AuthenticationException, SmartDeviceException, TimeoutException +from kasa import ( + AuthenticationError, + DeviceType, + KasaException, + LightState, + Module, + TimeoutError, +) +from kasa.interfaces import LightEffect +from kasa.iot import IotDevice import pytest from homeassistant.components import tplink @@ -23,6 +32,7 @@ from homeassistant.components.light import ( ATTR_TRANSITION, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, + EFFECT_OFF, ) from homeassistant.components.tplink.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH @@ -34,9 +44,9 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from . import ( + DEVICE_ID, MAC_ADDRESS, - _mocked_bulb, - _mocked_smart_light_strip, + _mocked_device, _patch_connect, _patch_discovery, _patch_single_discovery, @@ -45,37 +55,77 @@ from . import ( from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.parametrize( + ("device_type"), + [ + pytest.param(DeviceType.Dimmer, id="Dimmer"), + pytest.param(DeviceType.Bulb, id="Bulb"), + pytest.param(DeviceType.LightStrip, id="LightStrip"), + pytest.param(DeviceType.WallSwitch, id="WallSwitch"), + ], +) async def test_light_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, entity_registry: er.EntityRegistry, device_type ) -> None: """Test a light unique id.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.color_temp = None - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + light = _mocked_device(modules=[Module.Light], alias="my_light") + light.device_type = device_type + with _patch_discovery(device=light), _patch_connect(device=light): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" - assert entity_registry.async_get(entity_id).unique_id == "AABBCCDDEEFF" + entity_id = "light.my_light" + assert ( + entity_registry.async_get(entity_id).unique_id + == MAC_ADDRESS.replace(":", "").upper() + ) + + +async def test_legacy_dimmer_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + light = _mocked_device( + modules=[Module.Light], + alias="my_light", + spec=IotDevice, + device_id="aa:bb:cc:dd:ee:ff", + ) + light.device_type = DeviceType.Dimmer + + with _patch_discovery(device=light), _patch_connect(device=light): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_light" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == "aa:bb:cc:dd:ee:ff" @pytest.mark.parametrize( - ("bulb", "transition"), [(_mocked_bulb(), 2.0), (_mocked_smart_light_strip(), None)] + ("device", "transition"), + [ + (_mocked_device(modules=[Module.Light]), 2.0), + (_mocked_device(modules=[Module.Light, Module.LightEffect]), None), + ], ) async def test_color_light( - hass: HomeAssistant, bulb: MagicMock, transition: float | None + hass: HomeAssistant, device: MagicMock, transition: float | None ) -> None: """Test a color light and that all transitions are correctly passed.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb.color_temp = None - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + light = device.modules[Module.Light] + light.color_temp = None + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -101,11 +151,16 @@ async def test_color_light( await hass.services.async_call( LIGHT_DOMAIN, "turn_off", BASE_PAYLOAD, blocking=True ) - bulb.turn_off.assert_called_once_with(transition=KASA_TRANSITION_VALUE) + light.set_state.assert_called_once_with( + LightState(light_on=False, transition=KASA_TRANSITION_VALUE) + ) + light.set_state.reset_mock() await hass.services.async_call(LIGHT_DOMAIN, "turn_on", BASE_PAYLOAD, blocking=True) - bulb.turn_on.assert_called_once_with(transition=KASA_TRANSITION_VALUE) - bulb.turn_on.reset_mock() + light.set_state.assert_called_once_with( + LightState(light_on=True, transition=KASA_TRANSITION_VALUE) + ) + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -113,8 +168,8 @@ async def test_color_light( {**BASE_PAYLOAD, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_brightness.assert_called_with(39, transition=KASA_TRANSITION_VALUE) - bulb.set_brightness.reset_mock() + light.set_brightness.assert_called_with(39, transition=KASA_TRANSITION_VALUE) + light.set_brightness.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -122,10 +177,10 @@ async def test_color_light( {**BASE_PAYLOAD, ATTR_COLOR_TEMP_KELVIN: 6666}, blocking=True, ) - bulb.set_color_temp.assert_called_with( + light.set_color_temp.assert_called_with( 6666, brightness=None, transition=KASA_TRANSITION_VALUE ) - bulb.set_color_temp.reset_mock() + light.set_color_temp.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -133,10 +188,10 @@ async def test_color_light( {**BASE_PAYLOAD, ATTR_COLOR_TEMP_KELVIN: 6666}, blocking=True, ) - bulb.set_color_temp.assert_called_with( + light.set_color_temp.assert_called_with( 6666, brightness=None, transition=KASA_TRANSITION_VALUE ) - bulb.set_color_temp.reset_mock() + light.set_color_temp.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -144,8 +199,8 @@ async def test_color_light( {**BASE_PAYLOAD, ATTR_HS_COLOR: (10, 30)}, blocking=True, ) - bulb.set_hsv.assert_called_with(10, 30, None, transition=KASA_TRANSITION_VALUE) - bulb.set_hsv.reset_mock() + light.set_hsv.assert_called_with(10, 30, None, transition=KASA_TRANSITION_VALUE) + light.set_hsv.reset_mock() async def test_color_light_no_temp(hass: HomeAssistant) -> None: @@ -154,14 +209,15 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.is_variable_color_temp = False - type(bulb).color_temp = PropertyMock(side_effect=Exception) - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] + light.is_variable_color_temp = False + type(light).color_temp = PropertyMock(side_effect=Exception) + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == "on" @@ -176,13 +232,14 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_off.assert_called_once() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_on.assert_called_once() - bulb.turn_on.reset_mock() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -190,8 +247,8 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_brightness.assert_called_with(39, transition=None) - bulb.set_brightness.reset_mock() + light.set_brightness.assert_called_with(39, transition=None) + light.set_brightness.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -199,12 +256,16 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, blocking=True, ) - bulb.set_hsv.assert_called_with(10, 30, None, transition=None) - bulb.set_hsv.reset_mock() + light.set_hsv.assert_called_with(10, 30, None, transition=None) + light.set_hsv.reset_mock() @pytest.mark.parametrize( - ("bulb", "is_color"), [(_mocked_bulb(), True), (_mocked_smart_light_strip(), False)] + ("bulb", "is_color"), + [ + (_mocked_device(modules=[Module.Light], alias="my_light"), True), + (_mocked_device(modules=[Module.Light], alias="my_light"), False), + ], ) async def test_color_temp_light( hass: HomeAssistant, bulb: MagicMock, is_color: bool @@ -214,22 +275,24 @@ async def test_color_temp_light( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb.is_color = is_color - bulb.color_temp = 4000 - bulb.is_variable_color_temp = True + device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] + light.is_color = is_color + light.color_temp = 4000 + light.is_variable_color_temp = True - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == "on" attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "color_temp" - if bulb.is_color: + if light.is_color: assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"] else: assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] @@ -240,13 +303,14 @@ async def test_color_temp_light( await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_off.assert_called_once() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_on.assert_called_once() - bulb.turn_on.reset_mock() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -254,8 +318,8 @@ async def test_color_temp_light( {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_brightness.assert_called_with(39, transition=None) - bulb.set_brightness.reset_mock() + light.set_brightness.assert_called_with(39, transition=None) + light.set_brightness.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -263,8 +327,8 @@ async def test_color_temp_light( {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 6666}, blocking=True, ) - bulb.set_color_temp.assert_called_with(6666, brightness=None, transition=None) - bulb.set_color_temp.reset_mock() + light.set_color_temp.assert_called_with(6666, brightness=None, transition=None) + light.set_color_temp.reset_mock() # Verify color temp is clamped to the valid range await hass.services.async_call( @@ -273,8 +337,8 @@ async def test_color_temp_light( {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 20000}, blocking=True, ) - bulb.set_color_temp.assert_called_with(9000, brightness=None, transition=None) - bulb.set_color_temp.reset_mock() + light.set_color_temp.assert_called_with(9000, brightness=None, transition=None) + light.set_color_temp.reset_mock() # Verify color temp is clamped to the valid range await hass.services.async_call( @@ -283,8 +347,8 @@ async def test_color_temp_light( {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 1}, blocking=True, ) - bulb.set_color_temp.assert_called_with(4000, brightness=None, transition=None) - bulb.set_color_temp.reset_mock() + light.set_color_temp.assert_called_with(4000, brightness=None, transition=None) + light.set_color_temp.reset_mock() async def test_brightness_only_light(hass: HomeAssistant) -> None: @@ -293,15 +357,16 @@ async def test_brightness_only_light(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.is_color = False - bulb.is_variable_color_temp = False + device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] + light.is_color = False + light.is_variable_color_temp = False - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == "on" @@ -313,13 +378,14 @@ async def test_brightness_only_light(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_off.assert_called_once() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_on.assert_called_once() - bulb.turn_on.reset_mock() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -327,8 +393,8 @@ async def test_brightness_only_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_brightness.assert_called_with(39, transition=None) - bulb.set_brightness.reset_mock() + light.set_brightness.assert_called_with(39, transition=None) + light.set_brightness.reset_mock() async def test_on_off_light(hass: HomeAssistant) -> None: @@ -337,16 +403,17 @@ async def test_on_off_light(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.is_color = False - bulb.is_variable_color_temp = False - bulb.is_dimmable = False + device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] + light.is_color = False + light.is_variable_color_temp = False + light.is_dimmable = False - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == "on" @@ -356,13 +423,14 @@ async def test_on_off_light(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_off.assert_called_once() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_on.assert_called_once() - bulb.turn_on.reset_mock() + light.set_state.assert_called_once() + light.set_state.reset_mock() async def test_off_at_start_light(hass: HomeAssistant) -> None: @@ -371,17 +439,18 @@ async def test_off_at_start_light(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.is_color = False - bulb.is_variable_color_temp = False - bulb.is_dimmable = False - bulb.is_on = False + device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] + light.is_color = False + light.is_variable_color_temp = False + light.is_dimmable = False + light.state = LightState(light_on=False) - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == "off" @@ -395,15 +464,16 @@ async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.is_dimmer = True - bulb.is_on = False + device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] + device.device_type = DeviceType.Dimmer + light.state = LightState(light_on=False) - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == "off" @@ -411,8 +481,17 @@ async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turn_on.assert_called_once_with(transition=1) - bulb.turn_on.reset_mock() + light.set_state.assert_called_once_with( + LightState( + light_on=True, + brightness=None, + hue=None, + saturation=None, + color_temp=None, + transition=1, + ) + ) + light.set_state.reset_mock() async def test_smart_strip_effects(hass: HomeAssistant) -> None: @@ -421,22 +500,26 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - strip = _mocked_smart_light_strip() + device = _mocked_device( + modules=[Module.Light, Module.LightEffect], alias="my_light" + ) + light = device.modules[Module.Light] + light_effect = device.modules[Module.LightEffect] with ( - _patch_discovery(device=strip), - _patch_single_discovery(device=strip), - _patch_connect(device=strip), + _patch_discovery(device=device), + _patch_single_discovery(device=device), + _patch_connect(device=device), ): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == STATE_ON assert state.attributes[ATTR_EFFECT] == "Effect1" - assert state.attributes[ATTR_EFFECT_LIST] == ["Effect1", "Effect2"] + assert state.attributes[ATTR_EFFECT_LIST] == ["Off", "Effect1", "Effect2"] # Ensure setting color temp when an effect # is in progress calls set_hsv to clear the effect @@ -446,10 +529,10 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 4000}, blocking=True, ) - strip.set_hsv.assert_called_once_with(0, 0, None) - strip.set_color_temp.assert_called_once_with(4000, brightness=None, transition=None) - strip.set_hsv.reset_mock() - strip.set_color_temp.reset_mock() + light.set_hsv.assert_called_once_with(0, 0, None) + light.set_color_temp.assert_called_once_with(4000, brightness=None, transition=None) + light.set_hsv.reset_mock() + light.set_color_temp.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -457,21 +540,20 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "Effect2"}, blocking=True, ) - strip.set_effect.assert_called_once_with( + light_effect.set_effect.assert_called_once_with( "Effect2", brightness=None, transition=None ) - strip.set_effect.reset_mock() + light_effect.set_effect.reset_mock() - strip.effect = {"name": "Effect1", "enable": 0, "custom": 0} + light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == STATE_ON - assert state.attributes[ATTR_EFFECT] is None + assert state.attributes[ATTR_EFFECT] == EFFECT_OFF - strip.is_off = True - strip.is_on = False + light.state = LightState(light_on=False) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) await hass.async_block_till_done() @@ -485,12 +567,11 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - strip.turn_on.assert_called_once() - strip.turn_on.reset_mock() + light.set_state.assert_called_once() + light.set_state.reset_mock() - strip.is_off = False - strip.is_on = True - strip.effect_list = None + light.state = LightState(light_on=True) + light_effect.effect_list = None async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() @@ -505,13 +586,17 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - strip = _mocked_smart_light_strip() + device = _mocked_device( + modules=[Module.Light, Module.LightEffect], alias="my_light" + ) + light = device.modules[Module.Light] + light_effect = device.modules[Module.LightEffect] - with _patch_discovery(device=strip), _patch_connect(device=strip): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -526,7 +611,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: }, blocking=True, ) - strip.set_custom_effect.assert_called_once_with( + light_effect.set_custom_effect.assert_called_once_with( { "custom": 1, "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", @@ -543,7 +628,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: "backgrounds": [(340, 20, 50), (20, 50, 50), (0, 100, 50)], } ) - strip.set_custom_effect.reset_mock() + light_effect.set_custom_effect.reset_mock() await hass.services.async_call( DOMAIN, @@ -555,7 +640,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: }, blocking=True, ) - strip.set_custom_effect.assert_called_once_with( + light_effect.set_custom_effect.assert_called_once_with( { "custom": 1, "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", @@ -571,9 +656,9 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: "random_seed": 600, } ) - strip.set_custom_effect.reset_mock() + light_effect.set_custom_effect.reset_mock() - strip.effect = { + light_effect.effect = { "custom": 1, "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", "brightness": 100, @@ -586,15 +671,8 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: state = hass.states.get(entity_id) assert state.state == STATE_ON - strip.is_off = True - strip.is_on = False - strip.effect = { - "custom": 1, - "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", - "brightness": 100, - "name": "Custom", - "enable": 0, - } + light.state = LightState(light_on=False) + light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) await hass.async_block_till_done() @@ -608,8 +686,8 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - strip.turn_on.assert_called_once() - strip.turn_on.reset_mock() + light.set_state.assert_called_once() + light.set_state.reset_mock() await hass.services.async_call( DOMAIN, @@ -631,7 +709,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - strip.set_custom_effect.assert_called_once_with( + light_effect.set_custom_effect.assert_called_once_with( { "custom": 1, "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", @@ -653,7 +731,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: "transition_range": [2000, 3000], } ) - strip.set_custom_effect.reset_mock() + light_effect.set_custom_effect.reset_mock() async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) -> None: @@ -662,19 +740,17 @@ async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) -> domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - strip = _mocked_smart_light_strip() - strip.effect = { - "custom": 1, - "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", - "brightness": 100, - "name": "Custom", - "enable": 0, - } - with _patch_discovery(device=strip), _patch_connect(device=strip): + device = _mocked_device( + modules=[Module.Light, Module.LightEffect], alias="my_light" + ) + light = device.modules[Module.Light] + light_effect = device.modules[Module.LightEffect] + light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -685,8 +761,8 @@ async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) -> {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - strip.turn_on.assert_called_once() - strip.turn_on.reset_mock() + light.set_state.assert_called_once() + light.set_state.reset_mock() async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None: @@ -695,13 +771,16 @@ async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - strip = _mocked_smart_light_strip() + device = _mocked_device( + modules=[Module.Light, Module.LightEffect], alias="my_light" + ) + light_effect = device.modules[Module.LightEffect] - with _patch_discovery(device=strip), _patch_connect(device=strip): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -715,7 +794,7 @@ async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None: }, blocking=True, ) - strip.set_custom_effect.assert_called_once_with( + light_effect.set_custom_effect.assert_called_once_with( { "custom": 1, "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", @@ -733,24 +812,24 @@ async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None: "direction": 4, } ) - strip.set_custom_effect.reset_mock() + light_effect.set_custom_effect.reset_mock() @pytest.mark.parametrize( ("exception_type", "msg", "reauth_expected"), [ ( - AuthenticationException, + AuthenticationError, "Device authentication error async_turn_on: test error", True, ), ( - TimeoutException, + TimeoutError, "Timeout communicating with the device async_turn_on: test error", False, ), ( - SmartDeviceException, + KasaException, "Unable to communicate with the device async_turn_on: test error", False, ), @@ -768,14 +847,15 @@ async def test_light_errors_when_turned_on( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.turn_on.side_effect = exception_type(msg) + device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] + light.set_state.side_effect = exception_type(msg) - with _patch_discovery(device=bulb), _patch_connect(device=bulb): + with _patch_discovery(device=device), _patch_connect(device=device): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "light.my_bulb" + entity_id = "light.my_light" assert not any( already_migrated_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}) @@ -786,7 +866,7 @@ async def test_light_errors_when_turned_on( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) await hass.async_block_till_done() - assert bulb.turn_on.call_count == 1 + assert light.set_state.call_count == 1 assert ( any( flow @@ -797,3 +877,42 @@ async def test_light_errors_when_turned_on( ) == reauth_expected ) + + +async def test_light_child( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Test child lights are added to parent device with the right ids.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + + child_light_1 = _mocked_device( + modules=[Module.Light], alias="my_light_0", device_id=f"{DEVICE_ID}00" + ) + child_light_2 = _mocked_device( + modules=[Module.Light], alias="my_light_1", device_id=f"{DEVICE_ID}01" + ) + parent_device = _mocked_device( + device_id=DEVICE_ID, + alias="my_device", + children=[child_light_1, child_light_2], + modules=[Module.Light], + ) + + with _patch_discovery(device=parent_device), _patch_connect(device=parent_device): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_device" + entity = entity_registry.async_get(entity_id) + assert entity + + for light_id in range(2): + child_entity_id = f"light.my_device_my_light_{light_id}" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"{DEVICE_ID}0{light_id}" + assert child_entity.device_id == entity.device_id diff --git a/tests/components/tplink/test_number.py b/tests/components/tplink/test_number.py new file mode 100644 index 00000000000..865ce27ffc0 --- /dev/null +++ b/tests/components/tplink/test_number.py @@ -0,0 +1,163 @@ +"""Tests for tplink number platform.""" + +from kasa import Feature +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components import tplink +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.components.tplink.const import DOMAIN +from homeassistant.components.tplink.entity import EXCLUDED_FEATURES +from homeassistant.components.tplink.number import NUMBER_DESCRIPTIONS +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from . import ( + DEVICE_ID, + MAC_ADDRESS, + _mocked_device, + _mocked_feature, + _mocked_strip_children, + _patch_connect, + _patch_discovery, + setup_platform_for_device, + snapshot_platform, +) + +from tests.common import MockConfigEntry + + +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test a sensor unique ids.""" + features = {description.key for description in NUMBER_DESCRIPTIONS} + features.update(EXCLUDED_FEATURES) + device = _mocked_device(alias="my_device", features=features) + + await setup_platform_for_device(hass, mock_config_entry, Platform.NUMBER, device) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + for excluded in EXCLUDED_FEATURES: + assert hass.states.get(f"sensor.my_device_{excluded}") is None + + +async def test_number(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: + """Test a sensor unique ids.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + new_feature = _mocked_feature( + "temperature_offset", + value=10, + name="Temperature offset", + type_=Feature.Type.Number, + category=Feature.Category.Config, + minimum_value=1, + maximum_value=100, + ) + plug = _mocked_device(alias="my_plug", features=[new_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "number.my_plug_temperature_offset" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_temperature_offset" + + +async def test_number_children( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test a sensor unique ids.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + new_feature = _mocked_feature( + "temperature_offset", + value=10, + name="Some number", + type_=Feature.Type.Number, + category=Feature.Category.Config, + minimum_value=1, + maximum_value=100, + ) + plug = _mocked_device( + alias="my_plug", + features=[new_feature], + children=_mocked_strip_children(features=[new_feature]), + ) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "number.my_plug_temperature_offset" + entity = entity_registry.async_get(entity_id) + assert entity + device = device_registry.async_get(entity.device_id) + + for plug_id in range(2): + child_entity_id = f"number.my_plug_plug{plug_id}_temperature_offset" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_temperature_offset" + assert child_entity.device_id != entity.device_id + child_device = device_registry.async_get(child_entity.device_id) + assert child_device + assert child_device.via_device_id == device.id + + +async def test_number_set( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test a number entity limits and setting values.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + new_feature = _mocked_feature( + "temperature_offset", + value=10, + name="Some number", + type_=Feature.Type.Number, + category=Feature.Category.Config, + minimum_value=1, + maximum_value=200, + ) + plug = _mocked_device(alias="my_plug", features=[new_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "number.my_plug_temperature_offset" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_temperature_offset" + + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "10" + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 50}, + blocking=True, + ) + new_feature.set_value.assert_called_with(50) diff --git a/tests/components/tplink/test_select.py b/tests/components/tplink/test_select.py new file mode 100644 index 00000000000..6c49185d91c --- /dev/null +++ b/tests/components/tplink/test_select.py @@ -0,0 +1,158 @@ +"""Tests for tplink select platform.""" + +from kasa import Feature +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components import tplink +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.components.tplink.const import DOMAIN +from homeassistant.components.tplink.entity import EXCLUDED_FEATURES +from homeassistant.components.tplink.select import SELECT_DESCRIPTIONS +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from . import ( + DEVICE_ID, + MAC_ADDRESS, + _mocked_device, + _mocked_feature, + _mocked_strip_children, + _patch_connect, + _patch_discovery, + setup_platform_for_device, + snapshot_platform, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mocked_feature_select() -> Feature: + """Return mocked tplink binary sensor feature.""" + return _mocked_feature( + "light_preset", + value="First choice", + name="light_preset", + choices=["First choice", "Second choice"], + type_=Feature.Type.Choice, + category=Feature.Category.Config, + ) + + +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test a sensor unique ids.""" + features = {description.key for description in SELECT_DESCRIPTIONS} + features.update(EXCLUDED_FEATURES) + device = _mocked_device(alias="my_device", features=features) + + await setup_platform_for_device(hass, mock_config_entry, Platform.SELECT, device) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + for excluded in EXCLUDED_FEATURES: + assert hass.states.get(f"sensor.my_device_{excluded}") is None + + +async def test_select( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mocked_feature_select: Feature, +) -> None: + """Test a sensor unique ids.""" + mocked_feature = mocked_feature_select + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + + plug = _mocked_device(alias="my_plug", features=[mocked_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + # The entity_id is based on standard name from core. + entity_id = "select.my_plug_light_preset" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_{mocked_feature.id}" + + +async def test_select_children( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mocked_feature_select: Feature, +) -> None: + """Test a sensor unique ids.""" + mocked_feature = mocked_feature_select + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + plug = _mocked_device( + alias="my_plug", + features=[mocked_feature], + children=_mocked_strip_children(features=[mocked_feature]), + ) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "select.my_plug_light_preset" + entity = entity_registry.async_get(entity_id) + assert entity + device = device_registry.async_get(entity.device_id) + + for plug_id in range(2): + child_entity_id = f"select.my_plug_plug{plug_id}_light_preset" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_{mocked_feature.id}" + assert child_entity.device_id != entity.device_id + child_device = device_registry.async_get(child_entity.device_id) + assert child_device + assert child_device.via_device_id == device.id + + +async def test_select_select( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mocked_feature_select: Feature, +) -> None: + """Test a select setting values.""" + mocked_feature = mocked_feature_select + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + plug = _mocked_device(alias="my_plug", features=[mocked_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "select.my_plug_light_preset" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_light_preset" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Second choice"}, + blocking=True, + ) + mocked_feature.set_value.assert_called_with("Second choice") diff --git a/tests/components/tplink/test_sensor.py b/tests/components/tplink/test_sensor.py index 43884083483..dda43c52430 100644 --- a/tests/components/tplink/test_sensor.py +++ b/tests/components/tplink/test_sensor.py @@ -1,35 +1,71 @@ """Tests for light platform.""" -from unittest.mock import Mock +from kasa import Device, Feature, Module +import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components import tplink from homeassistant.components.tplink.const import DOMAIN -from homeassistant.const import CONF_HOST +from homeassistant.components.tplink.entity import EXCLUDED_FEATURES +from homeassistant.components.tplink.sensor import SENSOR_DESCRIPTIONS +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from . import MAC_ADDRESS, _mocked_bulb, _mocked_plug, _patch_connect, _patch_discovery +from . import ( + DEVICE_ID, + MAC_ADDRESS, + _mocked_device, + _mocked_energy_features, + _mocked_feature, + _mocked_strip_children, + _patch_connect, + _patch_discovery, + setup_platform_for_device, + snapshot_platform, +) from tests.common import MockConfigEntry +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test a sensor unique ids.""" + features = {description.key for description in SENSOR_DESCRIPTIONS} + features.update(EXCLUDED_FEATURES) + device = _mocked_device(alias="my_device", features=features) + + await setup_platform_for_device(hass, mock_config_entry, Platform.SENSOR, device) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + for excluded in EXCLUDED_FEATURES: + assert hass.states.get(f"sensor.my_device_{excluded}") is None + + async def test_color_light_with_an_emeter(hass: HomeAssistant) -> None: """Test a light with an emeter.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.color_temp = None - bulb.has_emeter = True - bulb.emeter_realtime = Mock( + emeter_features = _mocked_energy_features( power=None, total=None, voltage=None, current=5, + today=5000.0036, + ) + bulb = _mocked_device( + alias="my_bulb", modules=[Module.Light], features=["state", *emeter_features] ) - bulb.emeter_today = 5000.0036 with _patch_discovery(device=bulb), _patch_connect(device=bulb): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -60,16 +96,13 @@ async def test_plug_with_an_emeter(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - plug = _mocked_plug() - plug.color_temp = None - plug.has_emeter = True - plug.emeter_realtime = Mock( + emeter_features = _mocked_energy_features( power=100.06, total=30.0049, voltage=121.19, current=5.035, ) - plug.emeter_today = None + plug = _mocked_device(alias="my_plug", features=["state", *emeter_features]) with _patch_discovery(device=plug), _patch_connect(device=plug): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -95,8 +128,7 @@ async def test_color_light_no_emeter(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.color_temp = None + bulb = _mocked_device(alias="my_bulb", modules=[Module.Light]) bulb.has_emeter = False with _patch_discovery(device=bulb), _patch_connect(device=bulb): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) @@ -126,26 +158,175 @@ async def test_sensor_unique_id( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - plug = _mocked_plug() - plug.color_temp = None - plug.has_emeter = True - plug.emeter_realtime = Mock( + emeter_features = _mocked_energy_features( power=100, total=30, voltage=121, current=5, + today=None, ) - plug.emeter_today = None + plug = _mocked_device(alias="my_plug", features=emeter_features) with _patch_discovery(device=plug), _patch_connect(device=plug): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() expected = { - "sensor.my_plug_current_consumption": "aa:bb:cc:dd:ee:ff_current_power_w", - "sensor.my_plug_total_consumption": "aa:bb:cc:dd:ee:ff_total_energy_kwh", - "sensor.my_plug_today_s_consumption": "aa:bb:cc:dd:ee:ff_today_energy_kwh", - "sensor.my_plug_voltage": "aa:bb:cc:dd:ee:ff_voltage", - "sensor.my_plug_current": "aa:bb:cc:dd:ee:ff_current_a", + "sensor.my_plug_current_consumption": f"{DEVICE_ID}_current_power_w", + "sensor.my_plug_total_consumption": f"{DEVICE_ID}_total_energy_kwh", + "sensor.my_plug_today_s_consumption": f"{DEVICE_ID}_today_energy_kwh", + "sensor.my_plug_voltage": f"{DEVICE_ID}_voltage", + "sensor.my_plug_current": f"{DEVICE_ID}_current_a", } for sensor_entity_id, value in expected.items(): assert entity_registry.async_get(sensor_entity_id).unique_id == value + + +async def test_undefined_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test a message is logged when discovering a feature without a description.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + new_feature = _mocked_feature( + "consumption_this_fortnight", + value=5.2, + name="Consumption for fortnight", + type_=Feature.Type.Sensor, + category=Feature.Category.Primary, + unit="A", + precision_hint=2, + ) + plug = _mocked_device(alias="my_plug", features=[new_feature]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + msg = ( + "Device feature: Consumption for fortnight (consumption_this_fortnight) " + "needs an entity description defined in HA" + ) + assert msg in caplog.text + + +async def test_sensor_children_on_parent( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test a WallSwitch sensor entities are added to parent.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + feature = _mocked_feature( + "consumption_this_month", + value=5.2, + # integration should ignore name and use the value from strings.json: + # This month's consumption + name="Consumption for month", + type_=Feature.Type.Sensor, + category=Feature.Category.Primary, + unit="A", + precision_hint=2, + ) + plug = _mocked_device( + alias="my_plug", + features=[feature], + children=_mocked_strip_children(features=[feature]), + device_type=Device.Type.WallSwitch, + ) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "sensor.my_plug_this_month_s_consumption" + entity = entity_registry.async_get(entity_id) + assert entity + device = device_registry.async_get(entity.device_id) + + for plug_id in range(2): + child_entity_id = f"sensor.my_plug_plug{plug_id}_this_month_s_consumption" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_consumption_this_month" + child_device = device_registry.async_get(child_entity.device_id) + assert child_device + + assert child_entity.device_id == entity.device_id + assert child_device.connections == device.connections + + +async def test_sensor_children_on_child( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test strip sensors are on child device.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + feature = _mocked_feature( + "consumption_this_month", + value=5.2, + # integration should ignore name and use the value from strings.json: + # This month's consumption + name="Consumption for month", + type_=Feature.Type.Sensor, + category=Feature.Category.Primary, + unit="A", + precision_hint=2, + ) + plug = _mocked_device( + alias="my_plug", + features=[feature], + children=_mocked_strip_children(features=[feature]), + device_type=Device.Type.Strip, + ) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "sensor.my_plug_this_month_s_consumption" + entity = entity_registry.async_get(entity_id) + assert entity + device = device_registry.async_get(entity.device_id) + + for plug_id in range(2): + child_entity_id = f"sensor.my_plug_plug{plug_id}_this_month_s_consumption" + child_entity = entity_registry.async_get(child_entity_id) + assert child_entity + assert child_entity.unique_id == f"PLUG{plug_id}DEVICEID_consumption_this_month" + child_device = device_registry.async_get(child_entity.device_id) + assert child_device + + assert child_entity.device_id != entity.device_id + assert child_device.via_device_id == device.id + + +@pytest.mark.skip +async def test_new_datetime_sensor( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test a sensor unique ids.""" + # Skipped temporarily while datetime handling on hold. + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + plug = _mocked_device(alias="my_plug", features=["on_since"]) + with _patch_discovery(device=plug), _patch_connect(device=plug): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "sensor.my_plug_on_since" + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == f"{DEVICE_ID}_on_since" + state = hass.states.get(entity_id) + assert state + assert state.attributes["device_class"] == "timestamp" diff --git a/tests/components/tplink/test_switch.py b/tests/components/tplink/test_switch.py index 02913e0c37e..e9c8cc07b67 100644 --- a/tests/components/tplink/test_switch.py +++ b/tests/components/tplink/test_switch.py @@ -3,12 +3,16 @@ from datetime import timedelta from unittest.mock import AsyncMock -from kasa import AuthenticationException, SmartDeviceException, TimeoutException +from kasa import AuthenticationError, Device, KasaException, Module, TimeoutError +from kasa.iot import IotStrip import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components import tplink from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.tplink.const import DOMAIN +from homeassistant.components.tplink.entity import EXCLUDED_FEATURES +from homeassistant.components.tplink.switch import SWITCH_DESCRIPTIONS from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( ATTR_ENTITY_ID, @@ -16,32 +20,57 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, slugify from . import ( + DEVICE_ID, MAC_ADDRESS, - _mocked_dimmer, - _mocked_plug, - _mocked_strip, + _mocked_device, + _mocked_strip_children, _patch_connect, _patch_discovery, + setup_platform_for_device, + snapshot_platform, ) from tests.common import MockConfigEntry, async_fire_time_changed +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test a sensor unique ids.""" + features = {description.key for description in SWITCH_DESCRIPTIONS} + features.update(EXCLUDED_FEATURES) + device = _mocked_device(alias="my_device", features=features) + + await setup_platform_for_device(hass, mock_config_entry, Platform.SWITCH, device) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + for excluded in EXCLUDED_FEATURES: + assert hass.states.get(f"sensor.my_device_{excluded}") is None + + async def test_plug(hass: HomeAssistant) -> None: """Test a smart plug.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - plug = _mocked_plug() + plug = _mocked_device(alias="my_plug", features=["state"]) + feat = plug.features["state"] with _patch_discovery(device=plug), _patch_connect(device=plug): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -53,29 +82,42 @@ async def test_plug(hass: HomeAssistant) -> None: await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - plug.turn_off.assert_called_once() - plug.turn_off.reset_mock() + feat.set_value.assert_called_once() + feat.set_value.reset_mock() await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - plug.turn_on.assert_called_once() - plug.turn_on.reset_mock() + feat.set_value.assert_called_once() + feat.set_value.reset_mock() @pytest.mark.parametrize( ("dev", "domain"), [ - (_mocked_plug(), "switch"), - (_mocked_strip(), "switch"), - (_mocked_dimmer(), "light"), + (_mocked_device(alias="my_plug", features=["state", "led"]), "switch"), + ( + _mocked_device( + alias="my_strip", + features=["state", "led"], + children=_mocked_strip_children(), + ), + "switch", + ), + ( + _mocked_device( + alias="my_light", modules=[Module.Light], features=["state", "led"] + ), + "light", + ), ], ) -async def test_led_switch(hass: HomeAssistant, dev, domain: str) -> None: +async def test_led_switch(hass: HomeAssistant, dev: Device, domain: str) -> None: """Test LED setting for plugs, strips and dimmers.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) + feat = dev.features["led"] already_migrated_config_entry.add_to_hass(hass) with _patch_discovery(device=dev), _patch_connect(device=dev): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) @@ -91,14 +133,14 @@ async def test_led_switch(hass: HomeAssistant, dev, domain: str) -> None: await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: led_entity_id}, blocking=True ) - dev.set_led.assert_called_once_with(False) - dev.set_led.reset_mock() + feat.set_value.assert_called_once_with(False) + feat.set_value.reset_mock() await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: led_entity_id}, blocking=True ) - dev.set_led.assert_called_once_with(True) - dev.set_led.reset_mock() + feat.set_value.assert_called_once_with(True) + feat.set_value.reset_mock() async def test_plug_unique_id( @@ -109,13 +151,13 @@ async def test_plug_unique_id( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - plug = _mocked_plug() + plug = _mocked_device(alias="my_plug", features=["state", "led"]) with _patch_discovery(device=plug), _patch_connect(device=plug): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() entity_id = "switch.my_plug" - assert entity_registry.async_get(entity_id).unique_id == "aa:bb:cc:dd:ee:ff" + assert entity_registry.async_get(entity_id).unique_id == DEVICE_ID async def test_plug_update_fails(hass: HomeAssistant) -> None: @@ -124,7 +166,7 @@ async def test_plug_update_fails(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - plug = _mocked_plug() + plug = _mocked_device(alias="my_plug", features=["state", "led"]) with _patch_discovery(device=plug), _patch_connect(device=plug): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -132,7 +174,7 @@ async def test_plug_update_fails(hass: HomeAssistant) -> None: entity_id = "switch.my_plug" state = hass.states.get(entity_id) assert state.state == STATE_ON - plug.update = AsyncMock(side_effect=SmartDeviceException) + plug.update = AsyncMock(side_effect=KasaException) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() @@ -146,15 +188,18 @@ async def test_strip(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - strip = _mocked_strip() + strip = _mocked_device( + alias="my_strip", + children=_mocked_strip_children(features=["state"]), + features=["state", "led"], + spec=IotStrip, + ) + strip.children[0].features["state"].value = True + strip.children[1].features["state"].value = False with _patch_discovery(device=strip), _patch_connect(device=strip): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - # Verify we only create entities for the children - # since this is what the previous version did - assert hass.states.get("switch.my_strip") is None - entity_id = "switch.my_strip_plug0" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -162,14 +207,15 @@ async def test_strip(hass: HomeAssistant) -> None: await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - strip.children[0].turn_off.assert_called_once() - strip.children[0].turn_off.reset_mock() + feat = strip.children[0].features["state"] + feat.set_value.assert_called_once() + feat.set_value.reset_mock() await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - strip.children[0].turn_on.assert_called_once() - strip.children[0].turn_on.reset_mock() + feat.set_value.assert_called_once() + feat.set_value.reset_mock() entity_id = "switch.my_strip_plug1" state = hass.states.get(entity_id) @@ -178,14 +224,15 @@ async def test_strip(hass: HomeAssistant) -> None: await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - strip.children[1].turn_off.assert_called_once() - strip.children[1].turn_off.reset_mock() + feat = strip.children[1].features["state"] + feat.set_value.assert_called_once() + feat.set_value.reset_mock() await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - strip.children[1].turn_on.assert_called_once() - strip.children[1].turn_on.reset_mock() + feat.set_value.assert_called_once() + feat.set_value.reset_mock() async def test_strip_unique_ids( @@ -196,7 +243,11 @@ async def test_strip_unique_ids( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - strip = _mocked_strip() + strip = _mocked_device( + alias="my_strip", + children=_mocked_strip_children(features=["state"]), + features=["state", "led"], + ) with _patch_discovery(device=strip), _patch_connect(device=strip): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -208,21 +259,45 @@ async def test_strip_unique_ids( ) +async def test_strip_blank_alias( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test a strip unique id.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + strip = _mocked_device( + alias="", + model="KS123", + children=_mocked_strip_children(features=["state", "led"], alias=""), + features=["state", "led"], + ) + with _patch_discovery(device=strip), _patch_connect(device=strip): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + for plug_id in range(2): + entity_id = f"switch.unnamed_ks123_stripsocket_{plug_id + 1}" + state = hass.states.get(entity_id) + assert state.name == f"Unnamed KS123 Stripsocket {plug_id + 1}" + + @pytest.mark.parametrize( ("exception_type", "msg", "reauth_expected"), [ ( - AuthenticationException, + AuthenticationError, "Device authentication error async_turn_on: test error", True, ), ( - TimeoutException, + TimeoutError, "Timeout communicating with the device async_turn_on: test error", False, ), ( - SmartDeviceException, + KasaException, "Unable to communicate with the device async_turn_on: test error", False, ), @@ -240,8 +315,9 @@ async def test_plug_errors_when_turned_on( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - plug = _mocked_plug() - plug.turn_on.side_effect = exception_type("test error") + plug = _mocked_device(alias="my_plug", features=["state", "led"]) + feat = plug.features["state"] + feat.set_value.side_effect = exception_type("test error") with _patch_discovery(device=plug), _patch_connect(device=plug): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) @@ -258,7 +334,7 @@ async def test_plug_errors_when_turned_on( SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) await hass.async_block_till_done() - assert plug.turn_on.call_count == 1 + assert feat.set_value.call_count == 1 assert ( any( flow From 1f0e47b25118537d41289791ca8a52800189e44e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 25 Jun 2024 22:27:52 +0200 Subject: [PATCH 0152/2411] Migrate Airgradient select entities to be config source dependent (#120462) Co-authored-by: Robert Resch --- .../components/airgradient/select.py | 88 +++++++++++-------- .../components/airgradient/strings.json | 5 -- tests/components/airgradient/test_select.py | 60 +++++++------ 3 files changed, 84 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index e85e1224000..1cb902a2d3c 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -6,10 +6,14 @@ from dataclasses import dataclass from airgradient import AirGradientClient, Config from airgradient.models import ConfigurationControl, LedBarMode, TemperatureUnit -from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, + SelectEntity, + SelectEntityDescription, +) from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ServiceValidationError +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AirGradientConfigEntry @@ -24,8 +28,6 @@ class AirGradientSelectEntityDescription(SelectEntityDescription): value_fn: Callable[[Config], str | None] set_value_fn: Callable[[AirGradientClient, str], Awaitable[None]] - requires_display: bool = False - requires_led_bar: bool = False CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription( @@ -43,7 +45,7 @@ CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription( ), ) -PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = ( +DISPLAY_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = ( AirGradientSelectEntityDescription( key="display_temperature_unit", translation_key="display_temperature_unit", @@ -53,7 +55,6 @@ PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = ( set_value_fn=lambda client, value: client.set_temperature_unit( TemperatureUnit(value) ), - requires_display=True, ), AirGradientSelectEntityDescription( key="display_pm_standard", @@ -64,8 +65,10 @@ PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = ( set_value_fn=lambda client, value: client.set_pm_standard( PM_STANDARD_REVERSE[value] ), - requires_display=True, ), +) + +LED_BAR_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = ( AirGradientSelectEntityDescription( key="led_bar_mode", translation_key="led_bar_mode", @@ -73,7 +76,6 @@ PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, value_fn=lambda config: config.led_bar_mode, set_value_fn=lambda client, value: client.set_led_bar_mode(LedBarMode(value)), - requires_led_bar=True, ), ) @@ -85,22 +87,52 @@ async def async_setup_entry( ) -> None: """Set up AirGradient select entities based on a config entry.""" - config_coordinator = entry.runtime_data.config + coordinator = entry.runtime_data.config measurement_coordinator = entry.runtime_data.measurement - entities = [AirGradientSelect(config_coordinator, CONFIG_CONTROL_ENTITY)] + async_add_entities([AirGradientSelect(coordinator, CONFIG_CONTROL_ENTITY)]) + + model = measurement_coordinator.data.model + + added_entities = False + + @callback + def _async_check_entities() -> None: + nonlocal added_entities - entities.extend( - AirGradientProtectedSelect(config_coordinator, description) - for description in PROTECTED_SELECT_TYPES if ( - description.requires_display - and measurement_coordinator.data.model.startswith("I") - ) - or (description.requires_led_bar and "L" in measurement_coordinator.data.model) - ) + coordinator.data.configuration_control is ConfigurationControl.LOCAL + and not added_entities + ): + entities: list[AirGradientSelect] = [] + if "I" in model: + entities.extend( + AirGradientSelect(coordinator, description) + for description in DISPLAY_SELECT_TYPES + ) + if "L" in model: + entities.extend( + AirGradientSelect(coordinator, description) + for description in LED_BAR_ENTITIES + ) - async_add_entities(entities) + async_add_entities(entities) + added_entities = True + elif ( + coordinator.data.configuration_control is not ConfigurationControl.LOCAL + and added_entities + ): + entity_registry = er.async_get(hass) + for entity_description in DISPLAY_SELECT_TYPES + LED_BAR_ENTITIES: + unique_id = f"{coordinator.serial_number}-{entity_description.key}" + if entity_id := entity_registry.async_get_entity_id( + SELECT_DOMAIN, DOMAIN, unique_id + ): + entity_registry.async_remove(entity_id) + added_entities = False + + coordinator.async_add_listener(_async_check_entities) + _async_check_entities() class AirGradientSelect(AirGradientEntity, SelectEntity): @@ -128,19 +160,3 @@ class AirGradientSelect(AirGradientEntity, SelectEntity): """Change the selected option.""" await self.entity_description.set_value_fn(self.coordinator.client, option) await self.coordinator.async_request_refresh() - - -class AirGradientProtectedSelect(AirGradientSelect): - """Defines a protected AirGradient select entity.""" - - async def async_select_option(self, option: str) -> None: - """Change the selected option.""" - if ( - self.coordinator.data.configuration_control - is not ConfigurationControl.LOCAL - ): - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="no_local_configuration", - ) - await super().async_select_option(option) diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 0b5c245f04c..4e8973bdde2 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -125,10 +125,5 @@ "name": "[%key:component::airgradient::entity::number::display_brightness::name%]" } } - }, - "exceptions": { - "no_local_configuration": { - "message": "Device should be configured with local configuration to be able to change settings." - } } } diff --git a/tests/components/airgradient/test_select.py b/tests/components/airgradient/test_select.py index 84bf081af63..b4294112062 100644 --- a/tests/components/airgradient/test_select.py +++ b/tests/components/airgradient/test_select.py @@ -1,23 +1,30 @@ """Tests for the AirGradient select platform.""" +from datetime import timedelta from unittest.mock import AsyncMock, patch -from airgradient import ConfigurationControl +from airgradient import Config +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion +from homeassistant.components.airgradient import DOMAIN from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_fixture, + snapshot_platform, +) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -56,37 +63,34 @@ async def test_setting_value( assert mock_airgradient_client.get_config.call_count == 2 -async def test_setting_protected_value( +async def test_cloud_creates_no_number( hass: HomeAssistant, mock_cloud_airgradient_client: AsyncMock, mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, ) -> None: - """Test setting protected value.""" - await setup_integration(hass, mock_config_entry) + """Test cloud configuration control.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.SELECT]): + await setup_integration(hass, mock_config_entry) - with pytest.raises(ServiceValidationError): - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: "select.airgradient_display_temperature_unit", - ATTR_OPTION: "c", - }, - blocking=True, - ) - mock_cloud_airgradient_client.set_temperature_unit.assert_not_called() + assert len(hass.states.async_all()) == 1 - mock_cloud_airgradient_client.get_config.return_value.configuration_control = ( - ConfigurationControl.LOCAL + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_local.json", DOMAIN) ) - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: "select.airgradient_display_temperature_unit", - ATTR_OPTION: "c", - }, - blocking=True, + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 4 + + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_cloud.json", DOMAIN) ) - mock_cloud_airgradient_client.set_temperature_unit.assert_called_once_with("c") + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 From 5983344746b68babf98eecf09d48609754114cc5 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 25 Jun 2024 22:34:56 +0200 Subject: [PATCH 0153/2411] Handle http connection errors to Prusa printers (#120456) --- homeassistant/components/prusalink/coordinator.py | 3 +++ tests/components/prusalink/test_init.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/prusalink/coordinator.py b/homeassistant/components/prusalink/coordinator.py index 7d4526a8b45..1d1989119fa 100644 --- a/homeassistant/components/prusalink/coordinator.py +++ b/homeassistant/components/prusalink/coordinator.py @@ -9,6 +9,7 @@ import logging from time import monotonic from typing import TypeVar +from httpx import ConnectError from pyprusalink import JobInfo, LegacyPrinterStatus, PrinterStatus, PrusaLink from pyprusalink.types import InvalidAuth, PrusaLinkError @@ -47,6 +48,8 @@ class PrusaLinkUpdateCoordinator(DataUpdateCoordinator[T], ABC): raise UpdateFailed("Invalid authentication") from None except PrusaLinkError as err: raise UpdateFailed(str(err)) from err + except (TimeoutError, ConnectError) as err: + raise UpdateFailed("Cannot connect") from err self.update_interval = self._get_update_interval(data) return data diff --git a/tests/components/prusalink/test_init.py b/tests/components/prusalink/test_init.py index 2cdc6894eeb..bd0fb84cafd 100644 --- a/tests/components/prusalink/test_init.py +++ b/tests/components/prusalink/test_init.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import patch +from httpx import ConnectError from pyprusalink.types import InvalidAuth, PrusaLinkError import pytest @@ -36,7 +37,10 @@ async def test_unloading( assert state.state == "unavailable" -@pytest.mark.parametrize("exception", [InvalidAuth, PrusaLinkError]) +@pytest.mark.parametrize( + "exception", + [InvalidAuth, PrusaLinkError, ConnectError("All connection attempts failed")], +) async def test_failed_update( hass: HomeAssistant, mock_config_entry: ConfigEntry, exception ) -> None: From 038f2ce79f803049dd2648ab837fff04bfafdde1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 00:01:57 +0200 Subject: [PATCH 0154/2411] Cleanup mqtt platform tests part 1 (#120470) --- .../mqtt/test_alarm_control_panel.py | 10 +--- tests/components/mqtt/test_binary_sensor.py | 42 ++++---------- tests/components/mqtt/test_button.py | 16 +----- tests/components/mqtt/test_camera.py | 18 +----- tests/components/mqtt/test_climate.py | 56 ++++--------------- tests/components/mqtt/test_config_flow.py | 27 ++++----- 6 files changed, 41 insertions(+), 128 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index cd7e8ab7339..aba2d5f6da2 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -1167,10 +1167,7 @@ async def test_entity_device_info_remove( ) -> None: """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, - mqtt_mock_entry, - alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) @@ -1188,10 +1185,7 @@ async def test_entity_id_update_discovery_update( ) -> None: """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, - mqtt_mock_entry, - alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 7ad394243df..6ba479fca74 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -977,10 +977,7 @@ async def test_entity_device_info_with_connection( ) -> None: """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_connection( - hass, - mqtt_mock_entry, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -989,10 +986,7 @@ async def test_entity_device_info_with_identifier( ) -> None: """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, - mqtt_mock_entry, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1001,10 +995,7 @@ async def test_entity_device_info_update( ) -> None: """Test device registry update.""" await help_test_entity_device_info_update( - hass, - mqtt_mock_entry, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1013,10 +1004,7 @@ async def test_entity_device_info_remove( ) -> None: """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, - mqtt_mock_entry, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1034,10 +1022,7 @@ async def test_entity_id_update_discovery_update( ) -> None: """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, - mqtt_mock_entry, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1046,11 +1031,7 @@ async def test_entity_debug_info_message( ) -> None: """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, - mqtt_mock_entry, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, - None, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG, None ) @@ -1104,10 +1085,10 @@ async def test_cleanup_triggers_and_restoring_state( tmp_path: Path, freezer: FrozenDateTimeFactory, hass_config: ConfigType, - payload1, - state1, - payload2, - state2, + payload1: str, + state1: str, + payload2: str, + state2: str, ) -> None: """Test cleanup old triggers at reloading and restoring the state.""" freezer.move_to("2022-02-02 12:01:00+01:00") @@ -1196,8 +1177,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = binary_sensor.DOMAIN diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 2d21128237e..7e5d748e2ab 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -159,13 +159,7 @@ async def test_default_availability_payload( } } await help_test_default_availability_payload( - hass, - mqtt_mock_entry, - button.DOMAIN, - config, - True, - "state-topic", - "1", + hass, mqtt_mock_entry, button.DOMAIN, config, True, "state-topic", "1" ) @@ -184,13 +178,7 @@ async def test_custom_availability_payload( } await help_test_custom_availability_payload( - hass, - mqtt_mock_entry, - button.DOMAIN, - config, - True, - "state-topic", - "1", + hass, mqtt_mock_entry, button.DOMAIN, config, True, "state-topic", "1" ) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 9dbf5035fc9..d02e19e6063 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -222,11 +222,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - camera.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, camera.DOMAIN, DEFAULT_CONFIG ) @@ -237,11 +233,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - camera.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, camera.DOMAIN, DEFAULT_CONFIG ) @@ -368,11 +360,7 @@ async def test_entity_id_update_subscriptions( ) -> None: """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, - mqtt_mock_entry, - camera.DOMAIN, - DEFAULT_CONFIG, - ["test_topic"], + hass, mqtt_mock_entry, camera.DOMAIN, DEFAULT_CONFIG, ["test_topic"] ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 5428dc9b3e1..c41a6366dfe 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -191,9 +191,7 @@ async def test_get_hvac_modes( @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_set_operation_bad_attr_and_state( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting operation mode without required attribute. @@ -454,9 +452,7 @@ async def test_turn_on_and_off_without_power_command( @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_set_fan_mode_bad_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting fan mode without required attribute.""" await mqtt_mock_entry() @@ -551,9 +547,7 @@ async def test_set_fan_mode( @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_set_swing_mode_bad_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting swing mode without required attribute.""" await mqtt_mock_entry() @@ -1046,9 +1040,7 @@ async def test_handle_action_received( @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_set_preset_mode_optimistic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting of the preset mode.""" mqtt_mock = await mqtt_mock_entry() @@ -1104,9 +1096,7 @@ async def test_set_preset_mode_optimistic( ], ) async def test_set_preset_mode_explicit_optimistic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting of the preset mode.""" mqtt_mock = await mqtt_mock_entry() @@ -1523,9 +1513,7 @@ async def test_get_with_templates( ], ) async def test_set_and_templates( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting various attributes with templates.""" mqtt_mock = await mqtt_mock_entry() @@ -2074,11 +2062,7 @@ async def test_entity_id_update_subscriptions( } } await help_test_entity_id_update_subscriptions( - hass, - mqtt_mock_entry, - climate.DOMAIN, - config, - ["test-topic", "avty-topic"], + hass, mqtt_mock_entry, climate.DOMAIN, config, ["test-topic", "avty-topic"] ) @@ -2170,20 +2154,8 @@ async def test_precision_whole( @pytest.mark.parametrize( ("service", "topic", "parameters", "payload", "template"), [ - ( - climate.SERVICE_TURN_ON, - "power_command_topic", - {}, - "ON", - None, - ), - ( - climate.SERVICE_TURN_OFF, - "power_command_topic", - {}, - "OFF", - None, - ), + (climate.SERVICE_TURN_ON, "power_command_topic", {}, "ON", None), + (climate.SERVICE_TURN_OFF, "power_command_topic", {}, "OFF", None), ( climate.SERVICE_SET_HVAC_MODE, "mode_command_topic", @@ -2346,9 +2318,7 @@ async def test_publishing_with_custom_encoding( ], ) async def test_humidity_configuration_validity( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - valid: bool, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, valid: bool ) -> None: """Test the validity of humidity configurations.""" assert await mqtt_mock_entry() @@ -2357,8 +2327,7 @@ async def test_humidity_configuration_validity( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = climate.DOMAIN @@ -2381,8 +2350,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = climate.DOMAIN diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 21ddf5ecc11..57975fdc309 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -187,11 +187,11 @@ def mock_process_uploaded_file( yield mock_upload +@pytest.mark.usefixtures("mqtt_client_mock") async def test_user_connection_works( hass: HomeAssistant, mock_try_connection: MagicMock, mock_finish_setup: MagicMock, - mqtt_client_mock: MqttMockPahoClient, ) -> None: """Test we can finish a config flow.""" mock_try_connection.return_value = True @@ -217,11 +217,11 @@ async def test_user_connection_works( assert len(mock_finish_setup.mock_calls) == 1 +@pytest.mark.usefixtures("mqtt_client_mock") async def test_user_v5_connection_works( hass: HomeAssistant, mock_try_connection: MagicMock, mock_finish_setup: MagicMock, - mqtt_client_mock: MqttMockPahoClient, ) -> None: """Test we can finish a config flow.""" mock_try_connection.return_value = True @@ -664,11 +664,11 @@ async def test_bad_certificate( ("100", False), ], ) +@pytest.mark.usefixtures("mock_reload_after_entry_update") async def test_keepalive_validation( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, mock_try_connection: MagicMock, - mock_reload_after_entry_update: MagicMock, input_value: str, error: bool, ) -> None: @@ -872,11 +872,11 @@ def get_suggested(schema: vol.Schema, key: str) -> Any: return schema_key.description["suggested_value"] +@pytest.mark.usefixtures("mock_reload_after_entry_update") async def test_option_flow_default_suggested_values( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, mock_try_connection_success: MqttMockPahoClient, - mock_reload_after_entry_update: MagicMock, ) -> None: """Test config flow options has default/suggested values.""" await mqtt_mock_entry() @@ -1030,11 +1030,11 @@ async def test_option_flow_default_suggested_values( @pytest.mark.parametrize( ("advanced_options", "step_id"), [(False, "options"), (True, "broker")] ) +@pytest.mark.usefixtures("mock_reload_after_entry_update") async def test_skipping_advanced_options( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, mock_try_connection: MagicMock, - mock_reload_after_entry_update: MagicMock, advanced_options: bool, step_id: str, ) -> None: @@ -1102,11 +1102,11 @@ async def test_skipping_advanced_options( ), ], ) +@pytest.mark.usefixtures("mock_reload_after_entry_update") async def test_step_reauth( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, mock_try_connection: MagicMock, - mock_reload_after_entry_update: MagicMock, test_input: dict[str, Any], user_input: dict[str, Any], new_password: str, @@ -1284,12 +1284,9 @@ async def test_options_bad_will_message_fails( @pytest.mark.parametrize( "hass_config", [{"mqtt": {"sensor": [{"state_topic": "some-topic"}]}}] ) +@pytest.mark.usefixtures("mock_ssl_context", "mock_process_uploaded_file") async def test_try_connection_with_advanced_parameters( - hass: HomeAssistant, - mock_try_connection_success: MqttMockPahoClient, - tmp_path: Path, - mock_ssl_context: dict[str, MagicMock], - mock_process_uploaded_file: MagicMock, + hass: HomeAssistant, mock_try_connection_success: MqttMockPahoClient ) -> None: """Test config flow with advanced parameters from config.""" config_entry = MockConfigEntry(domain=mqtt.DOMAIN) @@ -1402,10 +1399,10 @@ async def test_try_connection_with_advanced_parameters( await hass.async_block_till_done() +@pytest.mark.usefixtures("mock_ssl_context") async def test_setup_with_advanced_settings( hass: HomeAssistant, mock_try_connection: MagicMock, - mock_ssl_context: dict[str, MagicMock], mock_process_uploaded_file: MagicMock, ) -> None: """Test config flow setup with advanced parameters.""" @@ -1564,11 +1561,9 @@ async def test_setup_with_advanced_settings( } +@pytest.mark.usesfixtures("mock_ssl_context", "mock_process_uploaded_file") async def test_change_websockets_transport_to_tcp( - hass: HomeAssistant, - mock_try_connection, - mock_ssl_context: dict[str, MagicMock], - mock_process_uploaded_file: MagicMock, + hass: HomeAssistant, mock_try_connection: MagicMock ) -> None: """Test option flow setup with websockets transport settings.""" config_entry = MockConfigEntry(domain=mqtt.DOMAIN) From 60519702b4c2bd374ccb43732f4d2865baf2ae71 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Tue, 25 Jun 2024 18:03:01 -0400 Subject: [PATCH 0155/2411] Bump python-roborock to 2.5.0 (#120466) --- homeassistant/components/roborock/manifest.json | 2 +- homeassistant/components/roborock/number.py | 2 +- homeassistant/components/roborock/switch.py | 2 +- homeassistant/components/roborock/time.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 51b1835247f..7a80a9083e9 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["roborock"], "requirements": [ - "python-roborock==2.3.0", + "python-roborock==2.5.0", "vacuum-map-parser-roborock==0.1.2" ] } diff --git a/homeassistant/components/roborock/number.py b/homeassistant/components/roborock/number.py index f761d0b2274..8aa20fad838 100644 --- a/homeassistant/components/roborock/number.py +++ b/homeassistant/components/roborock/number.py @@ -31,7 +31,7 @@ class RoborockNumberDescription(NumberEntityDescription): # Gets the status of the switch cache_key: CacheableAttribute # Sets the status of the switch - update_value: Callable[[AttributeCache, float], Coroutine[Any, Any, dict]] + update_value: Callable[[AttributeCache, float], Coroutine[Any, Any, None]] NUMBER_DESCRIPTIONS: list[RoborockNumberDescription] = [ diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index 694bf864809..7e17844666e 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -32,7 +32,7 @@ class RoborockSwitchDescription(SwitchEntityDescription): # Gets the status of the switch cache_key: CacheableAttribute # Sets the status of the switch - update_value: Callable[[AttributeCache, bool], Coroutine[Any, Any, dict]] + update_value: Callable[[AttributeCache, bool], Coroutine[Any, Any, None]] # Attribute from cache attribute: str diff --git a/homeassistant/components/roborock/time.py b/homeassistant/components/roborock/time.py index 7c9c08bce4d..6ccc2ef0b27 100644 --- a/homeassistant/components/roborock/time.py +++ b/homeassistant/components/roborock/time.py @@ -33,7 +33,7 @@ class RoborockTimeDescription(TimeEntityDescription): # Gets the status of the switch cache_key: CacheableAttribute # Sets the status of the switch - update_value: Callable[[AttributeCache, datetime.time], Coroutine[Any, Any, dict]] + update_value: Callable[[AttributeCache, datetime.time], Coroutine[Any, Any, None]] # Attribute from cache get_value: Callable[[AttributeCache], datetime.time] diff --git a/requirements_all.txt b/requirements_all.txt index de167c2f7e1..61924dc2af9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2315,7 +2315,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==2.3.0 +python-roborock==2.5.0 # homeassistant.components.smarttub python-smarttub==0.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8eb468b0947..62d26ee172d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1809,7 +1809,7 @@ python-picnic-api==1.1.0 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==2.3.0 +python-roborock==2.5.0 # homeassistant.components.smarttub python-smarttub==0.0.36 From 3cc3eb21b4baf3bfa1e5984c612cea46f30911f3 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Tue, 25 Jun 2024 18:04:09 -0400 Subject: [PATCH 0156/2411] Bump pyinsteon to 1.6.3 to fix Insteon device status (#120464) --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 456bc124b66..c5791573195 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["pyinsteon", "pypubsub"], "requirements": [ - "pyinsteon==1.6.1", + "pyinsteon==1.6.3", "insteon-frontend-home-assistant==0.5.0" ], "usb": [ diff --git a/requirements_all.txt b/requirements_all.txt index 61924dc2af9..f42794d8086 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1905,7 +1905,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.6.1 +pyinsteon==1.6.3 # homeassistant.components.intesishome pyintesishome==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62d26ee172d..e70fd97c0c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1498,7 +1498,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.6.1 +pyinsteon==1.6.3 # homeassistant.components.ipma pyipma==3.0.7 From 9718a9571edd7797c4fecadb02a514a7c74e6d1b Mon Sep 17 00:00:00 2001 From: Thomas Kistler Date: Tue, 25 Jun 2024 17:11:52 -0700 Subject: [PATCH 0157/2411] Add @thomaskistler as an owner of hydrawise (#120477) --- CODEOWNERS | 4 ++-- homeassistant/components/hydrawise/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 2e954ed1315..b4ff315872d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -631,8 +631,8 @@ build.json @home-assistant/supervisor /tests/components/huum/ @frwickst /homeassistant/components/hvv_departures/ @vigonotion /tests/components/hvv_departures/ @vigonotion -/homeassistant/components/hydrawise/ @dknowles2 @ptcryan -/tests/components/hydrawise/ @dknowles2 @ptcryan +/homeassistant/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan +/tests/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan /homeassistant/components/hyperion/ @dermotduffy /tests/components/hyperion/ @dermotduffy /homeassistant/components/ialarm/ @RyuzakiKK diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index b85ddca042e..c6f4d7d8dcd 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -1,7 +1,7 @@ { "domain": "hydrawise", "name": "Hunter Hydrawise", - "codeowners": ["@dknowles2", "@ptcryan"], + "codeowners": ["@dknowles2", "@thomaskistler", "@ptcryan"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", From ec2f98d0754e6fc86e7ba85571550938315d2e68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 02:12:04 +0200 Subject: [PATCH 0158/2411] Bump uiprotect to 3.7.0 (#120471) --- 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 7a1556387a8..8e29f5ffb9f 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==3.4.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==3.7.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index f42794d8086..cc8f8898430 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2791,7 +2791,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.4.0 +uiprotect==3.7.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e70fd97c0c8..f5d36af76f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2171,7 +2171,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.4.0 +uiprotect==3.7.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 0bc597f8c74565896ad4fc9f8c8e45003e16fcf0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 02:13:09 +0200 Subject: [PATCH 0159/2411] Improve vol.Invalid handling (#120480) --- homeassistant/components/blueprint/errors.py | 2 +- homeassistant/components/logbook/rest_api.py | 4 ++-- homeassistant/components/mqtt/mixins.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index 221279a39ac..e9a9defe05a 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -47,7 +47,7 @@ class InvalidBlueprint(BlueprintWithNameException): domain: str | None, blueprint_name: str | None, blueprint_data: Any, - msg_or_exc: vol.Invalid, + msg_or_exc: str | vol.Invalid, ) -> None: """Initialize an invalid blueprint error.""" if isinstance(msg_or_exc, vol.Invalid): diff --git a/homeassistant/components/logbook/rest_api.py b/homeassistant/components/logbook/rest_api.py index 5f1918ebccf..bd9efe7aba3 100644 --- a/homeassistant/components/logbook/rest_api.py +++ b/homeassistant/components/logbook/rest_api.py @@ -70,11 +70,11 @@ class LogbookView(HomeAssistantView): if entity_ids_str := request.query.get("entity"): try: entity_ids = cv.entity_ids(entity_ids_str) - except vol.Invalid: + except vol.Invalid as ex: raise InvalidEntityFormatError( f"Invalid entity id(s) encountered: {entity_ids_str}. " "Format should be ." - ) from vol.Invalid + ) from ex else: entity_ids = None diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 0800aeb8ee4..aca88f2cb97 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -157,7 +157,7 @@ class SetupEntity(Protocol): @callback def async_handle_schema_error( - discovery_payload: MQTTDiscoveryPayload, err: vol.MultipleInvalid + discovery_payload: MQTTDiscoveryPayload, err: vol.Invalid ) -> None: """Help handling schema errors on MQTT discovery messages.""" discovery_topic: str = discovery_payload.discovery_data[ATTR_DISCOVERY_TOPIC] From 3937cc2963f7fc3338a06e0e0694cf807385732d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 02:20:48 +0200 Subject: [PATCH 0160/2411] Improve SERVICE_TO_METHOD typing (#120474) --- .coveragerc | 1 + .../components/bluesound/media_player.py | 27 +++++++++---- homeassistant/components/webostv/__init__.py | 28 ++++++++----- homeassistant/components/xiaomi_miio/fan.py | 15 +++---- homeassistant/components/xiaomi_miio/light.py | 39 +++++++++++-------- .../components/xiaomi_miio/switch.py | 27 ++++++------- .../components/xiaomi_miio/typing.py | 12 ++++++ 7 files changed, 95 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/xiaomi_miio/typing.py diff --git a/.coveragerc b/.coveragerc index da3b7b91ece..1952297eb5f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1651,6 +1651,7 @@ omit = homeassistant/components/xiaomi_miio/remote.py homeassistant/components/xiaomi_miio/sensor.py homeassistant/components/xiaomi_miio/switch.py + homeassistant/components/xiaomi_miio/typing.py homeassistant/components/xiaomi_tv/media_player.py homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 7be5a823bf8..73ce963d481 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -7,7 +7,7 @@ from asyncio import CancelledError, timeout from datetime import timedelta from http import HTTPStatus import logging -from typing import Any +from typing import Any, NamedTuple from urllib import parse import aiohttp @@ -85,15 +85,27 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) + +class ServiceMethodDetails(NamedTuple): + """Details for SERVICE_TO_METHOD mapping.""" + + method: str + schema: vol.Schema + + BS_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) BS_JOIN_SCHEMA = BS_SCHEMA.extend({vol.Required(ATTR_MASTER): cv.entity_id}) SERVICE_TO_METHOD = { - SERVICE_JOIN: {"method": "async_join", "schema": BS_JOIN_SCHEMA}, - SERVICE_UNJOIN: {"method": "async_unjoin", "schema": BS_SCHEMA}, - SERVICE_SET_TIMER: {"method": "async_increase_timer", "schema": BS_SCHEMA}, - SERVICE_CLEAR_TIMER: {"method": "async_clear_timer", "schema": BS_SCHEMA}, + SERVICE_JOIN: ServiceMethodDetails(method="async_join", schema=BS_JOIN_SCHEMA), + SERVICE_UNJOIN: ServiceMethodDetails(method="async_unjoin", schema=BS_SCHEMA), + SERVICE_SET_TIMER: ServiceMethodDetails( + method="async_increase_timer", schema=BS_SCHEMA + ), + SERVICE_CLEAR_TIMER: ServiceMethodDetails( + method="async_clear_timer", schema=BS_SCHEMA + ), } @@ -188,12 +200,11 @@ async def async_setup_platform( target_players = hass.data[DATA_BLUESOUND] for player in target_players: - await getattr(player, method["method"])(**params) + await getattr(player, method.method)(**params) for service, method in SERVICE_TO_METHOD.items(): - schema = method["schema"] hass.services.async_register( - DOMAIN, service, async_service_handler, schema=schema + DOMAIN, service, async_service_handler, schema=method.schema ) diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index 479407c3199..36950b0e02a 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from contextlib import suppress import logging +from typing import NamedTuple from aiowebostv import WebOsClient, WebOsTvPairError import voluptuous as vol @@ -43,6 +44,14 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids}) + +class ServiceMethodDetails(NamedTuple): + """Details for SERVICE_TO_METHOD mapping.""" + + method: str + schema: vol.Schema + + BUTTON_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_BUTTON): cv.string}) COMMAND_SCHEMA = CALL_SCHEMA.extend( @@ -52,12 +61,14 @@ COMMAND_SCHEMA = CALL_SCHEMA.extend( SOUND_OUTPUT_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_SOUND_OUTPUT): cv.string}) SERVICE_TO_METHOD = { - SERVICE_BUTTON: {"method": "async_button", "schema": BUTTON_SCHEMA}, - SERVICE_COMMAND: {"method": "async_command", "schema": COMMAND_SCHEMA}, - SERVICE_SELECT_SOUND_OUTPUT: { - "method": "async_select_sound_output", - "schema": SOUND_OUTPUT_SCHEMA, - }, + SERVICE_BUTTON: ServiceMethodDetails(method="async_button", schema=BUTTON_SCHEMA), + SERVICE_COMMAND: ServiceMethodDetails( + method="async_command", schema=COMMAND_SCHEMA + ), + SERVICE_SELECT_SOUND_OUTPUT: ServiceMethodDetails( + method="async_select_sound_output", + schema=SOUND_OUTPUT_SCHEMA, + ), } _LOGGER = logging.getLogger(__name__) @@ -92,13 +103,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_service_handler(service: ServiceCall) -> None: method = SERVICE_TO_METHOD[service.service] data = service.data.copy() - data["method"] = method["method"] + data["method"] = method.method async_dispatcher_send(hass, DOMAIN, data) for service, method in SERVICE_TO_METHOD.items(): - schema = method["schema"] hass.services.async_register( - DOMAIN, service, async_service_handler, schema=schema + DOMAIN, service, async_service_handler, schema=method.schema ) hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = client diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 75533513b5e..4e0e271b071 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -92,6 +92,7 @@ from .const import ( SERVICE_SET_EXTRA_FEATURES, ) from .device import XiaomiCoordinatedMiioEntity +from .typing import ServiceMethodDetails _LOGGER = logging.getLogger(__name__) @@ -182,11 +183,11 @@ SERVICE_SCHEMA_EXTRA_FEATURES = AIRPURIFIER_SERVICE_SCHEMA.extend( ) SERVICE_TO_METHOD = { - SERVICE_RESET_FILTER: {"method": "async_reset_filter"}, - SERVICE_SET_EXTRA_FEATURES: { - "method": "async_set_extra_features", - "schema": SERVICE_SCHEMA_EXTRA_FEATURES, - }, + SERVICE_RESET_FILTER: ServiceMethodDetails(method="async_reset_filter"), + SERVICE_SET_EXTRA_FEATURES: ServiceMethodDetails( + method="async_set_extra_features", + schema=SERVICE_SCHEMA_EXTRA_FEATURES, + ), } FAN_DIRECTIONS_MAP = { @@ -271,7 +272,7 @@ async def async_setup_entry( update_tasks = [] for entity in filtered_entities: - entity_method = getattr(entity, method["method"], None) + entity_method = getattr(entity, method.method, None) if not entity_method: continue await entity_method(**params) @@ -281,7 +282,7 @@ async def async_setup_entry( await asyncio.wait(update_tasks) for air_purifier_service, method in SERVICE_TO_METHOD.items(): - schema = method.get("schema", AIRPURIFIER_SERVICE_SCHEMA) + schema = method.schema or AIRPURIFIER_SERVICE_SCHEMA hass.services.async_register( DOMAIN, air_purifier_service, async_service_handler, schema=schema ) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 96f9595e0e8..35537e82b2e 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -68,6 +68,7 @@ from .const import ( ) from .device import XiaomiMiioEntity from .gateway import XiaomiGatewayDevice +from .typing import ServiceMethodDetails _LOGGER = logging.getLogger(__name__) @@ -108,20 +109,24 @@ SERVICE_SCHEMA_SET_DELAYED_TURN_OFF = XIAOMI_MIIO_SERVICE_SCHEMA.extend( ) SERVICE_TO_METHOD = { - SERVICE_SET_DELAYED_TURN_OFF: { - "method": "async_set_delayed_turn_off", - "schema": SERVICE_SCHEMA_SET_DELAYED_TURN_OFF, - }, - SERVICE_SET_SCENE: { - "method": "async_set_scene", - "schema": SERVICE_SCHEMA_SET_SCENE, - }, - SERVICE_REMINDER_ON: {"method": "async_reminder_on"}, - SERVICE_REMINDER_OFF: {"method": "async_reminder_off"}, - SERVICE_NIGHT_LIGHT_MODE_ON: {"method": "async_night_light_mode_on"}, - SERVICE_NIGHT_LIGHT_MODE_OFF: {"method": "async_night_light_mode_off"}, - SERVICE_EYECARE_MODE_ON: {"method": "async_eyecare_mode_on"}, - SERVICE_EYECARE_MODE_OFF: {"method": "async_eyecare_mode_off"}, + SERVICE_SET_DELAYED_TURN_OFF: ServiceMethodDetails( + method="async_set_delayed_turn_off", + schema=SERVICE_SCHEMA_SET_DELAYED_TURN_OFF, + ), + SERVICE_SET_SCENE: ServiceMethodDetails( + method="async_set_scene", + schema=SERVICE_SCHEMA_SET_SCENE, + ), + SERVICE_REMINDER_ON: ServiceMethodDetails(method="async_reminder_on"), + SERVICE_REMINDER_OFF: ServiceMethodDetails(method="async_reminder_off"), + SERVICE_NIGHT_LIGHT_MODE_ON: ServiceMethodDetails( + method="async_night_light_mode_on" + ), + SERVICE_NIGHT_LIGHT_MODE_OFF: ServiceMethodDetails( + method="async_night_light_mode_off" + ), + SERVICE_EYECARE_MODE_ON: ServiceMethodDetails(method="async_eyecare_mode_on"), + SERVICE_EYECARE_MODE_OFF: ServiceMethodDetails(method="async_eyecare_mode_off"), } @@ -232,9 +237,9 @@ async def async_setup_entry( update_tasks = [] for target_device in target_devices: - if not hasattr(target_device, method["method"]): + if not hasattr(target_device, method.method): continue - await getattr(target_device, method["method"])(**params) + await getattr(target_device, method.method)(**params) update_tasks.append( asyncio.create_task(target_device.async_update_ha_state(True)) ) @@ -243,7 +248,7 @@ async def async_setup_entry( await asyncio.wait(update_tasks) for xiaomi_miio_service, method in SERVICE_TO_METHOD.items(): - schema = method.get("schema", XIAOMI_MIIO_SERVICE_SCHEMA) + schema = method.schema or XIAOMI_MIIO_SERVICE_SCHEMA hass.services.async_register( DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema ) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 34ebb9addf5..797a98d9fa1 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -115,6 +115,7 @@ from .const import ( ) from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity from .gateway import XiaomiGatewayDevice +from .typing import ServiceMethodDetails _LOGGER = logging.getLogger(__name__) @@ -176,16 +177,16 @@ SERVICE_SCHEMA_POWER_PRICE = SERVICE_SCHEMA.extend( ) SERVICE_TO_METHOD = { - SERVICE_SET_WIFI_LED_ON: {"method": "async_set_wifi_led_on"}, - SERVICE_SET_WIFI_LED_OFF: {"method": "async_set_wifi_led_off"}, - SERVICE_SET_POWER_MODE: { - "method": "async_set_power_mode", - "schema": SERVICE_SCHEMA_POWER_MODE, - }, - SERVICE_SET_POWER_PRICE: { - "method": "async_set_power_price", - "schema": SERVICE_SCHEMA_POWER_PRICE, - }, + SERVICE_SET_WIFI_LED_ON: ServiceMethodDetails(method="async_set_wifi_led_on"), + SERVICE_SET_WIFI_LED_OFF: ServiceMethodDetails(method="async_set_wifi_led_off"), + SERVICE_SET_POWER_MODE: ServiceMethodDetails( + method="async_set_power_mode", + schema=SERVICE_SCHEMA_POWER_MODE, + ), + SERVICE_SET_POWER_PRICE: ServiceMethodDetails( + method="async_set_power_price", + schema=SERVICE_SCHEMA_POWER_PRICE, + ), } MODEL_TO_FEATURES_MAP = { @@ -488,9 +489,9 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): update_tasks = [] for device in devices: - if not hasattr(device, method["method"]): + if not hasattr(device, method.method): continue - await getattr(device, method["method"])(**params) + await getattr(device, method.method)(**params) update_tasks.append( asyncio.create_task(device.async_update_ha_state(True)) ) @@ -499,7 +500,7 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): await asyncio.wait(update_tasks) for plug_service, method in SERVICE_TO_METHOD.items(): - schema = method.get("schema", SERVICE_SCHEMA) + schema = method.schema or SERVICE_SCHEMA hass.services.async_register( DOMAIN, plug_service, async_service_handler, schema=schema ) diff --git a/homeassistant/components/xiaomi_miio/typing.py b/homeassistant/components/xiaomi_miio/typing.py new file mode 100644 index 00000000000..8fbb8e3d83f --- /dev/null +++ b/homeassistant/components/xiaomi_miio/typing.py @@ -0,0 +1,12 @@ +"""Typings for the xiaomi_miio integration.""" + +from typing import NamedTuple + +import voluptuous as vol + + +class ServiceMethodDetails(NamedTuple): + """Details for SERVICE_TO_METHOD mapping.""" + + method: str + schema: vol.Schema | None = None From 2380696fcdac1ff6c351a5531ece11677fa34f5b Mon Sep 17 00:00:00 2001 From: EnjoyingM <6302356+mtielen@users.noreply.github.com> Date: Wed, 26 Jun 2024 02:22:09 +0200 Subject: [PATCH 0161/2411] Bump wolf-comm to 0.0.9 (#120473) --- homeassistant/components/wolflink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wolflink/manifest.json b/homeassistant/components/wolflink/manifest.json index e406217a0c8..6a98dcd6ca4 100644 --- a/homeassistant/components/wolflink/manifest.json +++ b/homeassistant/components/wolflink/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/wolflink", "iot_class": "cloud_polling", "loggers": ["wolf_comm"], - "requirements": ["wolf-comm==0.0.8"] + "requirements": ["wolf-comm==0.0.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index cc8f8898430..82ca566a925 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2899,7 +2899,7 @@ wirelesstagpy==0.8.1 wled==0.18.0 # homeassistant.components.wolflink -wolf-comm==0.0.8 +wolf-comm==0.0.9 # homeassistant.components.wyoming wyoming==1.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5d36af76f4..b0d39fdd0bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2261,7 +2261,7 @@ wiffi==1.1.2 wled==0.18.0 # homeassistant.components.wolflink -wolf-comm==0.0.8 +wolf-comm==0.0.9 # homeassistant.components.wyoming wyoming==1.5.4 From 49df0c43668b921d7cbc53b5f0058fb1ce7e68b0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 02:25:30 +0200 Subject: [PATCH 0162/2411] Improve schema typing (2) (#120475) --- .../components/aurora_abb_powerone/config_flow.py | 6 ++++-- homeassistant/components/device_automation/__init__.py | 4 ++-- homeassistant/components/group/config_flow.py | 1 + homeassistant/components/knx/trigger.py | 4 ++-- homeassistant/components/mysensors/helpers.py | 3 ++- homeassistant/components/sma/config_flow.py | 2 +- homeassistant/components/zwave_js/helpers.py | 6 ++++-- homeassistant/helpers/config_validation.py | 7 ++++--- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/llm.py | 1 + homeassistant/helpers/selector.py | 2 +- 11 files changed, 23 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/aurora_abb_powerone/config_flow.py b/homeassistant/components/aurora_abb_powerone/config_flow.py index a1e046f302f..f0093c62631 100644 --- a/homeassistant/components/aurora_abb_powerone/config_flow.py +++ b/homeassistant/components/aurora_abb_powerone/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping import logging -from typing import Any +from typing import TYPE_CHECKING, Any from aurorapy.client import AuroraError, AuroraSerialClient import serial.tools.list_ports @@ -78,7 +78,7 @@ class AuroraABBConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self): """Initialise the config flow.""" self.config = None - self._com_ports_list = None + self._com_ports_list: list[str] | None = None self._default_com_port = None async def async_step_user( @@ -92,6 +92,8 @@ class AuroraABBConfigFlow(ConfigFlow, domain=DOMAIN): self._com_ports_list, self._default_com_port = result if self._default_com_port is None: return self.async_abort(reason="no_serial_ports") + if TYPE_CHECKING: + assert isinstance(self._com_ports_list, list) # Handle the initial step. if user_input is not None: diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 567b8fcc2d2..5e196f40aa1 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -29,7 +29,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from homeassistant.loader import IntegrationNotFound from homeassistant.requirements import ( RequirementsNotFound, @@ -340,7 +340,7 @@ def async_get_entity_registry_entry_or_raise( @callback def async_validate_entity_schema( - hass: HomeAssistant, config: ConfigType, schema: vol.Schema + hass: HomeAssistant, config: ConfigType, schema: VolSchemaType ) -> ConfigType: """Validate schema and resolve entity registry entry id to entity_id.""" config = schema(config) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index b7341aff59a..4eb0f1cdd52 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -51,6 +51,7 @@ async def basic_group_options_schema( domain: str | list[str], handler: SchemaCommonFlowHandler | None ) -> vol.Schema: """Generate options schema.""" + entity_selector: selector.Selector[Any] | vol.Schema if handler is None: entity_selector = selector.selector( {"entity": {"domain": domain, "multiple": True}} diff --git a/homeassistant/components/knx/trigger.py b/homeassistant/components/knx/trigger.py index 1df1ffd6c3b..82149b21561 100644 --- a/homeassistant/components/knx/trigger.py +++ b/homeassistant/components/knx/trigger.py @@ -13,7 +13,7 @@ from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from .const import DOMAIN from .schema import ga_validator @@ -32,7 +32,7 @@ CONF_KNX_INCOMING: Final = "incoming" CONF_KNX_OUTGOING: Final = "outgoing" -TELEGRAM_TRIGGER_SCHEMA: Final = { +TELEGRAM_TRIGGER_SCHEMA: VolDictType = { vol.Optional(CONF_KNX_DESTINATION): vol.All(cv.ensure_list, [ga_validator]), vol.Optional(CONF_KNX_GROUP_VALUE_WRITE, default=True): cv.boolean, vol.Optional(CONF_KNX_GROUP_VALUE_RESPONSE, default=True): cv.boolean, diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index c456cfd1f11..f060f3313dc 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -6,6 +6,7 @@ from collections import defaultdict from collections.abc import Callable from enum import IntEnum import logging +from typing import cast from mysensors import BaseAsyncGateway, Message from mysensors.sensor import ChildSensor @@ -151,7 +152,7 @@ def get_child_schema( ) -> vol.Schema: """Return a child schema.""" set_req = gateway.const.SetReq - child_schema = child.get_schema(gateway.protocol_version) + child_schema = cast(vol.Schema, child.get_schema(gateway.protocol_version)) return child_schema.extend( { vol.Required( diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index 3bfb66c4849..fe26cbee2c8 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -43,7 +43,7 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize.""" - self._data = { + self._data: dict[str, Any] = { CONF_HOST: vol.UNDEFINED, CONF_SSL: False, CONF_VERIFY_SSL: True, diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 598cf2f78f6..737b8deff34 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -40,7 +40,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.group import expand_entity_ids -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolSchemaType from .const import ( ATTR_COMMAND_CLASS, @@ -479,7 +479,9 @@ def copy_available_params( ) -def get_value_state_schema(value: ZwaveValue) -> vol.Schema | None: +def get_value_state_schema( + value: ZwaveValue, +) -> VolSchemaType | vol.Coerce | vol.In | None: """Return device automation schema for a config entry.""" if isinstance(value, ConfigurationValue): min_ = value.metadata.min diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 0463bb07e11..558baaeb779 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -108,6 +108,7 @@ from homeassistant.util.yaml.objects import NodeStrClass from . import script_variables as script_variables_helper, template as template_helper from .frame import get_integration_logger +from .typing import VolDictType, VolSchemaType TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM', 'HH:MM:SS' or 'HH:MM:SS.F'" @@ -980,8 +981,8 @@ def removed( def key_value_schemas( key: str, - value_schemas: dict[Hashable, vol.Schema], - default_schema: vol.Schema | None = None, + value_schemas: dict[Hashable, VolSchemaType], + default_schema: VolSchemaType | None = None, default_description: str | None = None, ) -> Callable[[Any], dict[Hashable, Any]]: """Create a validator that validates based on a value for specific key. @@ -1355,7 +1356,7 @@ NUMERIC_STATE_THRESHOLD_SCHEMA = vol.Any( vol.All(str, entity_domain(["input_number", "number", "sensor", "zone"])), ) -CONDITION_BASE_SCHEMA = { +CONDITION_BASE_SCHEMA: VolDictType = { vol.Optional(CONF_ALIAS): string, vol.Optional(CONF_ENABLED): vol.Any(boolean, template), } diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 6774780f00f..d868e582f8f 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -985,7 +985,7 @@ class EntityPlatform: def async_register_entity_service( self, name: str, - schema: VolDictType | VolSchemaType, + schema: VolDictType | VolSchemaType | None, func: str | Callable[..., Any], required_features: Iterable[int] | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 480b9cb5237..4410de67ef2 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -660,6 +660,7 @@ class ScriptTool(Tool): description = config.get("description") if not description: description = config.get("name") + key: vol.Marker if config.get("required"): key = vol.Required(field, description=description) else: diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 16aaa40db86..5a542657d10 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -1182,7 +1182,7 @@ class SelectSelector(Selector[SelectSelectorConfig]): for option in cast(Sequence[SelectOptionDict], config_options) ] - parent_schema = vol.In(options) + parent_schema: vol.In | vol.Any = vol.In(options) if self.config["custom_value"]: parent_schema = vol.Any(parent_schema, str) From 6fb32db151ad5cadea8b138b9605fd96a4c3c370 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 03:03:54 +0200 Subject: [PATCH 0163/2411] Improve config vol.Invalid typing (#120482) --- homeassistant/config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 8e22f2051f0..ff679d4df51 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -614,7 +614,7 @@ def _get_annotation(item: Any) -> tuple[str, int | str] | None: return (getattr(item, "__config_file__"), getattr(item, "__line__", "?")) -def _get_by_path(data: dict | list, items: list[str | int]) -> Any: +def _get_by_path(data: dict | list, items: list[Hashable]) -> Any: """Access a nested object in root by item sequence. Returns None in case of error. @@ -626,7 +626,7 @@ def _get_by_path(data: dict | list, items: list[str | int]) -> Any: def find_annotation( - config: dict | list, path: list[str | int] + config: dict | list, path: list[Hashable] ) -> tuple[str, int | str] | None: """Find file/line annotation for a node in config pointed to by path. @@ -636,7 +636,7 @@ def find_annotation( """ def find_annotation_for_key( - item: dict, path: list[str | int], tail: str | int + item: dict, path: list[Hashable], tail: Hashable ) -> tuple[str, int | str] | None: for key in item: if key == tail: @@ -646,7 +646,7 @@ def find_annotation( return None def find_annotation_rec( - config: dict | list, path: list[str | int], tail: str | int | None + config: dict | list, path: list[Hashable], tail: Hashable | None ) -> tuple[str, int | str] | None: item = _get_by_path(config, path) if isinstance(item, dict) and tail is not None: From 07b70cba1076e021595a72d522b10fe7baee0208 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 08:32:43 +0200 Subject: [PATCH 0164/2411] Fix dropped unifiprotect motion events (#120489) --- .../components/unifiprotect/binary_sensor.py | 33 +++---------------- .../unifiprotect/test_binary_sensor.py | 3 +- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index fb60158580e..e35eb6f48f3 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -437,9 +437,6 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ufp_enabled="is_motion_detection_on", ufp_event_obj="last_motion_event", ), -) - -SMART_EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ProtectBinaryEventEntityDescription( key="smart_obj_any", name="Object detected", @@ -711,26 +708,6 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity): entity_description: ProtectBinaryEventEntityDescription _state_attrs = ("_attr_available", "_attr_is_on", "_attr_extra_state_attributes") - @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - super()._async_update_device_from_protect(device) - description = self.entity_description - event = self.entity_description.get_event_obj(device) - if is_on := bool(description.get_ufp_value(device)): - if event: - self._set_event_attrs(event) - else: - self._attr_extra_state_attributes = {} - self._attr_is_on = is_on - - -class ProtectSmartEventBinarySensor(EventEntityMixin, BinarySensorEntity): - """A UniFi Protect Device Binary Sensor for smart events.""" - - device: Camera - entity_description: ProtectBinaryEventEntityDescription - _state_attrs = ("_attr_available", "_attr_is_on", "_attr_extra_state_attributes") - @callback def _set_event_done(self) -> None: self._attr_is_on = False @@ -749,7 +726,10 @@ class ProtectSmartEventBinarySensor(EventEntityMixin, BinarySensorEntity): if not ( event - and description.has_matching_smart(event) + and ( + description.ufp_obj_type is None + or description.has_matching_smart(event) + ) and not self._event_already_ended(prev_event, prev_event_end) ): self._set_event_done() @@ -774,11 +754,6 @@ def _async_event_entities( ) -> list[ProtectDeviceEntity]: entities: list[ProtectDeviceEntity] = [] for device in data.get_cameras() if ufp_device is None else [ufp_device]: - entities.extend( - ProtectSmartEventBinarySensor(data, device, description) - for description in SMART_EVENT_SENSORS - if description.has_required(device) - ) entities.extend( ProtectEventBinarySensor(data, device, description) for description in EVENT_SENSORS diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 42782d10429..af8ce015955 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -25,7 +25,6 @@ from homeassistant.components.unifiprotect.binary_sensor import ( LIGHT_SENSORS, MOUNTABLE_SENSE_SENSORS, SENSE_SENSORS, - SMART_EVENT_SENSORS, ) from homeassistant.components.unifiprotect.const import ( ATTR_EVENT_SCORE, @@ -453,7 +452,7 @@ async def test_binary_sensor_package_detected( doorbell.smart_detect_settings.object_types.append(SmartDetectObjectType.PACKAGE) _, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, doorbell, SMART_EVENT_SENSORS[4] + Platform.BINARY_SENSOR, doorbell, EVENT_SENSORS[6] ) event = Event( From ba40340f82388b54b4c3c5adcb260d2fb6c8296a Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 08:45:22 +0200 Subject: [PATCH 0165/2411] Align deviceinfo entries in pyLoad integration (#120478) --- homeassistant/components/pyload/button.py | 2 +- homeassistant/components/pyload/sensor.py | 14 +++++++++++--- tests/components/pyload/snapshots/test_button.ambr | 8 ++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/pyload/button.py b/homeassistant/components/pyload/button.py index 1f6bf3c3d10..0d8a232142a 100644 --- a/homeassistant/components/pyload/button.py +++ b/homeassistant/components/pyload/button.py @@ -99,7 +99,7 @@ class PyLoadBinarySensor(CoordinatorEntity[PyLoadCoordinator], ButtonEntity): model=SERVICE_NAME, configuration_url=coordinator.pyload.api_url, identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, - translation_key=DOMAIN, + sw_version=coordinator.version, ) async def async_press(self) -> None: diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index c4fea3e43bb..3d681c4b65d 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -34,7 +34,15 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateTyp from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PyLoadConfigEntry -from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN, ISSUE_PLACEHOLDER +from .const import ( + DEFAULT_HOST, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, + ISSUE_PLACEHOLDER, + MANUFACTURER, + SERVICE_NAME, +) from .coordinator import PyLoadCoordinator @@ -175,8 +183,8 @@ class PyLoadSensor(CoordinatorEntity[PyLoadCoordinator], SensorEntity): self.entity_description = entity_description self.device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, - manufacturer="PyLoad Team", - model="pyLoad", + manufacturer=MANUFACTURER, + model=SERVICE_NAME, configuration_url=coordinator.pyload.api_url, identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, sw_version=coordinator.version, diff --git a/tests/components/pyload/snapshots/test_button.ambr b/tests/components/pyload/snapshots/test_button.ambr index c9a901aba15..bf1e1f59c98 100644 --- a/tests/components/pyload/snapshots/test_button.ambr +++ b/tests/components/pyload/snapshots/test_button.ambr @@ -35,7 +35,7 @@ # name: test_state[button.pyload_abort_all_running_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyload Abort all running downloads', + 'friendly_name': 'pyLoad Abort all running downloads', }), 'context': , 'entity_id': 'button.pyload_abort_all_running_downloads', @@ -81,7 +81,7 @@ # name: test_state[button.pyload_delete_finished_files_packages-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyload Delete finished files/packages', + 'friendly_name': 'pyLoad Delete finished files/packages', }), 'context': , 'entity_id': 'button.pyload_delete_finished_files_packages', @@ -127,7 +127,7 @@ # name: test_state[button.pyload_restart_all_failed_files-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyload Restart all failed files', + 'friendly_name': 'pyLoad Restart all failed files', }), 'context': , 'entity_id': 'button.pyload_restart_all_failed_files', @@ -173,7 +173,7 @@ # name: test_state[button.pyload_restart_pyload_core-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyload Restart pyload core', + 'friendly_name': 'pyLoad Restart pyload core', }), 'context': , 'entity_id': 'button.pyload_restart_pyload_core', From 0f7229f55fd7152b962f1799f2328960c3cac7b7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 26 Jun 2024 08:45:53 +0200 Subject: [PATCH 0166/2411] Fix holiday using utc instead of local time (#120432) --- homeassistant/components/holiday/calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/holiday/calendar.py b/homeassistant/components/holiday/calendar.py index f56f4f90831..6a336870857 100644 --- a/homeassistant/components/holiday/calendar.py +++ b/homeassistant/components/holiday/calendar.py @@ -122,7 +122,7 @@ class HolidayCalendarEntity(CalendarEntity): def _update_state_and_setup_listener(self) -> None: """Update state and setup listener for next interval.""" - now = dt_util.utcnow() + now = dt_util.now() self._attr_event = self.update_event(now) self.unsub = async_track_point_in_utc_time( self.hass, self.point_in_time_listener, self.get_next_interval(now) From cc6aac5e75c58820dc6c7269a66f3c7c588bbb6a Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 08:50:45 +0200 Subject: [PATCH 0167/2411] Add missing textselectors in `USER_DATA_SCHEMA` in pyLoad integration (#120479) --- homeassistant/components/pyload/config_flow.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/pyload/config_flow.py b/homeassistant/components/pyload/config_flow.py index 7a2dfddeb5b..4825e6fa7cf 100644 --- a/homeassistant/components/pyload/config_flow.py +++ b/homeassistant/components/pyload/config_flow.py @@ -41,8 +41,18 @@ STEP_USER_DATA_SCHEMA = vol.Schema( vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Required(CONF_SSL, default=False): cv.boolean, vol.Required(CONF_VERIFY_SSL, default=True): bool, - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_USERNAME): TextSelector( + TextSelectorConfig( + type=TextSelectorType.TEXT, + autocomplete="username", + ), + ), + vol.Required(CONF_PASSWORD): TextSelector( + TextSelectorConfig( + type=TextSelectorType.PASSWORD, + autocomplete="current-password", + ), + ), } ) From f23020919f5e0566fcee6b40f1e5569a524542cc Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 08:51:12 +0200 Subject: [PATCH 0168/2411] Remove unused translation strings in pyLoad integration (#120476) --- homeassistant/components/pyload/strings.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 6efdb23eaf4..248cec99cc9 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -3,7 +3,6 @@ "step": { "user": { "data": { - "name": "[%key:common::config_flow::data::name%]", "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", @@ -12,7 +11,6 @@ "port": "[%key:common::config_flow::data::port%]" }, "data_description": { - "name": "The name to use for your pyLoad instance in Home Assistant", "host": "The hostname or IP address of the device running your pyLoad instance.", "port": "pyLoad uses port 8000 by default." } From b7e7905b54c61a6d80e4b6d29f1291463f85bcac Mon Sep 17 00:00:00 2001 From: Grubalex <74829763+Grubalex@users.noreply.github.com> Date: Wed, 26 Jun 2024 08:51:55 +0200 Subject: [PATCH 0169/2411] Add Philips WiZ Lightbulbs to Matter TRANSITION_BLOCKLIST (#120461) --- homeassistant/components/matter/light.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 777e4a69010..749d82fd661 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -57,6 +57,11 @@ TRANSITION_BLOCKLIST = ( (4999, 25057, "1.0", "27.0"), (4448, 36866, "V1", "V1.0.0.5"), (5009, 514, "1.0", "1.0.0"), + (4107, 8475, "v1.0", "v1.0"), + (4107, 8550, "v1.0", "v1.0"), + (4107, 8551, "v1.0", "v1.0"), + (4107, 8656, "v1.0", "v1.0"), + (4107, 8571, "v1.0", "v1.0"), ) From cef1d35e31b1a2269c10cc651810d1b67aad0cac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 08:51:57 +0200 Subject: [PATCH 0170/2411] Make fetching integrations with requirements safer (#120481) --- homeassistant/requirements.py | 50 ++++++++++++++--------------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index c0e92610b6e..4de5fed5a73 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -4,16 +4,16 @@ from __future__ import annotations import asyncio from collections.abc import Iterable +import contextlib import logging import os -from typing import Any, cast +from typing import Any from packaging.requirements import Requirement from .core import HomeAssistant, callback from .exceptions import HomeAssistantError from .helpers import singleton -from .helpers.typing import UNDEFINED, UndefinedType from .loader import Integration, IntegrationNotFound, async_get_integration from .util import package as pkg_util @@ -119,11 +119,6 @@ def _install_requirements_if_missing( return installed, failures -def _set_result_unless_done(future: asyncio.Future[None]) -> None: - if not future.done(): - future.set_result(None) - - class RequirementsManager: """Manage requirements.""" @@ -132,7 +127,7 @@ class RequirementsManager: self.hass = hass self.pip_lock = asyncio.Lock() self.integrations_with_reqs: dict[ - str, Integration | asyncio.Future[None] | None | UndefinedType + str, Integration | asyncio.Future[Integration] ] = {} self.install_failure_history: set[str] = set() self.is_installed_cache: set[str] = set() @@ -151,37 +146,32 @@ class RequirementsManager: else: done.add(domain) - if self.hass.config.skip_pip: - return await async_get_integration(self.hass, domain) - cache = self.integrations_with_reqs - int_or_fut = cache.get(domain, UNDEFINED) - - if isinstance(int_or_fut, asyncio.Future): - await int_or_fut - - # When we have waited and it's UNDEFINED, it doesn't exist - # We don't cache that it doesn't exist, or else people can't fix it - # and then restart, because their config will never be valid. - if (int_or_fut := cache.get(domain, UNDEFINED)) is UNDEFINED: - raise IntegrationNotFound(domain) - - if int_or_fut is not UNDEFINED: - return cast(Integration, int_or_fut) + if int_or_fut := cache.get(domain): + if isinstance(int_or_fut, Integration): + return int_or_fut + return await int_or_fut future = cache[domain] = self.hass.loop.create_future() - try: integration = await async_get_integration(self.hass, domain) - await self._async_process_integration(integration, done) - except Exception: + if not self.hass.config.skip_pip: + await self._async_process_integration(integration, done) + except BaseException as ex: + # We do not cache failures as we want to retry, or + # else people can't fix it and then restart, because + # their config will never be valid. del cache[domain] + future.set_exception(ex) + with contextlib.suppress(BaseException): + # Clear the flag as its normal that nothing + # will wait for this future to be resolved + # if there are no concurrent requirements fetches. + await future raise - finally: - _set_result_unless_done(future) cache[domain] = integration - _set_result_unless_done(future) + future.set_result(integration) return integration async def _async_process_integration( From fab901f9b690e856416f3c80ce3132dcb1d11f8f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 08:53:28 +0200 Subject: [PATCH 0171/2411] Cleanup mqtt platform tests part 2 (#120490) --- tests/components/mqtt/test_cover.py | 49 +++++----------- tests/components/mqtt/test_device_tracker.py | 27 +++------ tests/components/mqtt/test_device_trigger.py | 40 ++++++------- tests/components/mqtt/test_diagnostics.py | 1 - tests/components/mqtt/test_discovery.py | 61 ++++++-------------- tests/components/mqtt/test_event.py | 15 ++--- 6 files changed, 67 insertions(+), 126 deletions(-) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 988119d09c1..f37de8b6a2e 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -697,9 +697,7 @@ async def test_position_via_template_and_entity_id( ], ) async def test_optimistic_flag( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - assumed_state: bool, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, assumed_state: bool ) -> None: """Test assumed_state is set correctly.""" await mqtt_mock_entry() @@ -1073,10 +1071,9 @@ async def test_current_cover_position_inverted( } ], ) +@pytest.mark.usefixtures("hass") async def test_optimistic_position( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test optimistic position is not supported.""" assert await mqtt_mock_entry() @@ -1627,7 +1624,6 @@ async def test_tilt_via_invocation_defaults( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test tilt defaults on close/open.""" - await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry() await hass.services.async_call( @@ -2547,11 +2543,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - cover.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, cover.DOMAIN, DEFAULT_CONFIG ) @@ -2562,11 +2554,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - cover.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, cover.DOMAIN, DEFAULT_CONFIG ) @@ -3221,10 +3209,9 @@ async def test_position_via_position_topic_template_return_invalid_json( } ], ) +@pytest.mark.usefixtures("hass") async def test_set_position_topic_without_get_position_topic_error( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test error when set_position_topic is used without position_topic.""" assert await mqtt_mock_entry() @@ -3247,8 +3234,8 @@ async def test_set_position_topic_without_get_position_topic_error( } ], ) +@pytest.mark.usefixtures("hass") async def test_value_template_without_state_topic_error( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: @@ -3273,8 +3260,8 @@ async def test_value_template_without_state_topic_error( } ], ) +@pytest.mark.usefixtures("hass") async def test_position_template_without_position_topic_error( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: @@ -3300,10 +3287,9 @@ async def test_position_template_without_position_topic_error( } ], ) +@pytest.mark.usefixtures("hass") async def test_set_position_template_without_set_position_topic( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test error when set_position_template is used and set_position_topic is missing.""" assert await mqtt_mock_entry() @@ -3327,10 +3313,9 @@ async def test_set_position_template_without_set_position_topic( } ], ) +@pytest.mark.usefixtures("hass") async def test_tilt_command_template_without_tilt_command_topic( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test error when tilt_command_template is used and tilt_command_topic is missing.""" assert await mqtt_mock_entry() @@ -3354,10 +3339,9 @@ async def test_tilt_command_template_without_tilt_command_topic( } ], ) +@pytest.mark.usefixtures("hass") async def test_tilt_status_template_without_tilt_status_topic_topic( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test error when tilt_status_template is used and tilt_status_topic is missing.""" assert await mqtt_mock_entry() @@ -3423,8 +3407,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = cover.DOMAIN diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 76129d4c549..9759dfcadd7 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -334,9 +334,7 @@ async def test_setting_device_tracker_value_via_mqtt_message( async def test_setting_device_tracker_value_via_mqtt_message_and_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the setting of the value via MQTT.""" await mqtt_mock_entry() @@ -361,9 +359,7 @@ async def test_setting_device_tracker_value_via_mqtt_message_and_template( async def test_setting_device_tracker_value_via_mqtt_message_and_template2( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the setting of the value via MQTT.""" await mqtt_mock_entry() @@ -391,9 +387,7 @@ async def test_setting_device_tracker_value_via_mqtt_message_and_template2( async def test_setting_device_tracker_location_via_mqtt_message( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the setting of the location via MQTT.""" await mqtt_mock_entry() @@ -415,9 +409,7 @@ async def test_setting_device_tracker_location_via_mqtt_message( async def test_setting_device_tracker_location_via_lat_lon_message( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the setting of the latitude and longitude via MQTT without state topic.""" await mqtt_mock_entry() @@ -472,9 +464,7 @@ async def test_setting_device_tracker_location_via_lat_lon_message( async def test_setting_device_tracker_location_via_reset_message( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the automatic inference of zones via MQTT via reset.""" await mqtt_mock_entry() @@ -548,9 +538,7 @@ async def test_setting_device_tracker_location_via_reset_message( async def test_setting_device_tracker_location_via_abbr_reset_message( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the setting of reset via abbreviated names and custom payloads via MQTT.""" await mqtt_mock_entry() @@ -625,8 +613,7 @@ async def test_setup_with_modern_schema( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = device_tracker.DOMAIN diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 9e75ea5168b..ce75bd01a03 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -1,6 +1,7 @@ """The tests for MQTT device triggers.""" import json +from typing import Any import pytest from pytest_unordered import unordered @@ -194,7 +195,6 @@ async def test_update_remove_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, ) -> None: """Test triggers can be updated and removed.""" await mqtt_mock_entry() @@ -1016,10 +1016,10 @@ async def test_attach_remove( await hass.async_block_till_done() device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) - calls = [] + callback_calls: list[dict[str, Any]] = [] - def callback(trigger): - calls.append(trigger["trigger"]["payload"]) + def trigger_callback(trigger): + callback_calls.append(trigger["trigger"]["payload"]) remove = await async_initialize_triggers( hass, @@ -1033,7 +1033,7 @@ async def test_attach_remove( "subtype": "button_1", }, ], - callback, + trigger_callback, DOMAIN, "mock-name", _LOGGER.log, @@ -1042,8 +1042,8 @@ async def test_attach_remove( # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0] == "short_press" + assert len(callback_calls) == 1 + assert callback_calls[0] == "short_press" # Remove the trigger remove() @@ -1052,7 +1052,7 @@ async def test_attach_remove( # Verify the triggers are no longer active async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(callback_calls) == 1 async def test_attach_remove_late( @@ -1079,10 +1079,10 @@ async def test_attach_remove_late( await hass.async_block_till_done() device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) - calls = [] + callback_calls: list[dict[str, Any]] = [] - def callback(trigger): - calls.append(trigger["trigger"]["payload"]) + def trigger_callback(trigger): + callback_calls.append(trigger["trigger"]["payload"]) remove = await async_initialize_triggers( hass, @@ -1096,7 +1096,7 @@ async def test_attach_remove_late( "subtype": "button_1", }, ], - callback, + trigger_callback, DOMAIN, "mock-name", _LOGGER.log, @@ -1108,8 +1108,8 @@ async def test_attach_remove_late( # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0] == "short_press" + assert len(callback_calls) == 1 + assert callback_calls[0] == "short_press" # Remove the trigger remove() @@ -1118,7 +1118,7 @@ async def test_attach_remove_late( # Verify the triggers are no longer active async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(callback_calls) == 1 async def test_attach_remove_late2( @@ -1145,10 +1145,10 @@ async def test_attach_remove_late2( await hass.async_block_till_done() device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) - calls = [] + callback_calls: list[dict[str, Any]] = [] - def callback(trigger): - calls.append(trigger["trigger"]["payload"]) + def trigger_callback(trigger): + callback_calls.append(trigger["trigger"]["payload"]) remove = await async_initialize_triggers( hass, @@ -1162,7 +1162,7 @@ async def test_attach_remove_late2( "subtype": "button_1", }, ], - callback, + trigger_callback, DOMAIN, "mock-name", _LOGGER.log, @@ -1178,7 +1178,7 @@ async def test_attach_remove_late2( # Verify the triggers are no longer active async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(callback_calls) == 0 # Try to remove the trigger twice with pytest.raises(HomeAssistantError): diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index f8b547ae1eb..b8499ba5812 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -26,7 +26,6 @@ default_config = { async def test_entry_diagnostics( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, hass_client: ClientSessionGenerator, mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index b9ef1a3c210..fbf878a040a 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -64,8 +64,7 @@ from tests.typing import ( [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) async def test_subscribing_config_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting up discovery.""" mqtt_mock = await mqtt_mock_entry() @@ -205,8 +204,7 @@ async def test_only_valid_components( async def test_correct_config_discovery( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test sending in correct JSON.""" await mqtt_mock_entry() @@ -285,9 +283,7 @@ async def test_discovery_with_invalid_integration_info( """Test sending in correct JSON.""" await mqtt_mock_entry() async_fire_mqtt_message( - hass, - "homeassistant/binary_sensor/bla/config", - config_message, + hass, "homeassistant/binary_sensor/bla/config", config_message ) await hass.async_block_till_done() @@ -298,8 +294,7 @@ async def test_discovery_with_invalid_integration_info( async def test_discover_fan( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test discovering an MQTT fan.""" await mqtt_mock_entry() @@ -318,9 +313,7 @@ async def test_discover_fan( async def test_discover_climate( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test discovering an MQTT climate component.""" await mqtt_mock_entry() @@ -341,8 +334,7 @@ async def test_discover_climate( async def test_discover_alarm_control_panel( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test discovering an MQTT alarm control panel component.""" await mqtt_mock_entry() @@ -531,8 +523,7 @@ async def test_discovery_with_object_id( async def test_discovery_incl_nodeid( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test sending in correct JSON with optional node_id included.""" await mqtt_mock_entry() @@ -581,8 +572,7 @@ async def test_non_duplicate_discovery( async def test_removal( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test removal of component through empty discovery message.""" await mqtt_mock_entry() @@ -602,8 +592,7 @@ async def test_removal( async def test_rediscover( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test rediscover of removed component.""" await mqtt_mock_entry() @@ -632,8 +621,7 @@ async def test_rediscover( async def test_rapid_rediscover( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test immediate rediscover of removed component.""" await mqtt_mock_entry() @@ -684,8 +672,7 @@ async def test_rapid_rediscover( async def test_rapid_rediscover_unique( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test immediate rediscover of removed component.""" await mqtt_mock_entry() @@ -746,8 +733,7 @@ async def test_rapid_rediscover_unique( async def test_rapid_reconfigure( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test immediate reconfigure of added component.""" await mqtt_mock_entry() @@ -1110,8 +1096,7 @@ async def test_cleanup_device_multiple_config_entries_mqtt( async def test_discovery_expansion( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test expansion of abbreviated discovery payload.""" await mqtt_mock_entry() @@ -1172,8 +1157,7 @@ async def test_discovery_expansion( async def test_discovery_expansion_2( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test expansion of abbreviated discovery payload.""" await mqtt_mock_entry() @@ -1249,8 +1233,7 @@ async def test_discovery_expansion_3( async def test_discovery_expansion_without_encoding_and_value_template_1( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test expansion of raw availability payload with a template as list.""" await mqtt_mock_entry() @@ -1300,8 +1283,7 @@ async def test_discovery_expansion_without_encoding_and_value_template_1( async def test_discovery_expansion_without_encoding_and_value_template_2( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test expansion of raw availability payload with a template directly.""" await mqtt_mock_entry() @@ -1379,7 +1361,6 @@ EXCLUDED_MODULES = { async def test_missing_discover_abbreviations( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Check MQTT platforms for missing abbreviations.""" @@ -1403,8 +1384,7 @@ async def test_missing_discover_abbreviations( async def test_no_implicit_state_topic_switch( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test no implicit state topic for switch.""" await mqtt_mock_entry() @@ -1462,8 +1442,7 @@ async def test_complex_discovery_topic_prefix( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_integration_discovery_subscribe_unsubscribe( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Check MQTT integration discovery subscribe and unsubscribe.""" @@ -1521,8 +1500,7 @@ async def test_mqtt_integration_discovery_subscribe_unsubscribe( @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.0) async def test_mqtt_discovery_unsubscribe_once( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Check MQTT integration discovery unsubscribe once.""" @@ -1657,7 +1635,6 @@ async def test_clean_up_registry_monitoring( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, device_registry: dr.DeviceRegistry, - tmp_path: Path, ) -> None: """Test registry monitoring hook is removed after a reload.""" await mqtt_mock_entry() diff --git a/tests/components/mqtt/test_event.py b/tests/components/mqtt/test_event.py index 48f80bf41d7..662a279f639 100644 --- a/tests/components/mqtt/test_event.py +++ b/tests/components/mqtt/test_event.py @@ -325,10 +325,9 @@ async def test_discovery_update_availability( } ], ) +@pytest.mark.usefixtures("hass") async def test_invalid_device_class( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test device_class option with invalid value.""" assert await mqtt_mock_entry() @@ -444,9 +443,7 @@ async def test_discovery_removal_event( async def test_discovery_update_event_template( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered mqtt event template.""" await mqtt_mock_entry() @@ -665,8 +662,7 @@ async def test_value_template_with_entity_id( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = event.DOMAIN @@ -689,8 +685,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = event.DOMAIN From 005c71a4a581805366e5e46e6ea13b99758d4e6f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 08:55:28 +0200 Subject: [PATCH 0172/2411] Deduplicate alarm_control_panel services.yaml (#118796) --- .../alarm_control_panel/services.yaml | 48 +++++-------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index f7a3854b6b3..cabc43a8b80 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -1,14 +1,15 @@ # Describes the format for available alarm control panel services +.common_service_fields: &common_service_fields + code: + example: "1234" + selector: + text: alarm_disarm: target: entity: domain: alarm_control_panel - fields: - code: - example: "1234" - selector: - text: + fields: *common_service_fields alarm_arm_custom_bypass: target: @@ -16,11 +17,7 @@ alarm_arm_custom_bypass: domain: alarm_control_panel supported_features: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS - fields: - code: - example: "1234" - selector: - text: + fields: *common_service_fields alarm_arm_home: target: @@ -28,11 +25,7 @@ alarm_arm_home: domain: alarm_control_panel supported_features: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME - fields: - code: - example: "1234" - selector: - text: + fields: *common_service_fields alarm_arm_away: target: @@ -40,23 +33,14 @@ alarm_arm_away: domain: alarm_control_panel supported_features: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY - fields: - code: - example: "1234" - selector: - text: - + fields: *common_service_fields alarm_arm_night: target: entity: domain: alarm_control_panel supported_features: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT - fields: - code: - example: "1234" - selector: - text: + fields: *common_service_fields alarm_arm_vacation: target: @@ -64,11 +48,7 @@ alarm_arm_vacation: domain: alarm_control_panel supported_features: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION - fields: - code: - example: "1234" - selector: - text: + fields: *common_service_fields alarm_trigger: target: @@ -76,8 +56,4 @@ alarm_trigger: domain: alarm_control_panel supported_features: - alarm_control_panel.AlarmControlPanelEntityFeature.TRIGGER - fields: - code: - example: "1234" - selector: - text: + fields: *common_service_fields From 9f4bf6f11a97c7b21fe0f2b0ad21b76165fec9f5 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 26 Jun 2024 09:00:33 +0200 Subject: [PATCH 0173/2411] Create repair when HA auth provider is running in legacy mode (#119975) --- homeassistant/auth/__init__.py | 8 ++ homeassistant/auth/providers/homeassistant.py | 70 ++++++++------- homeassistant/components/auth/strings.json | 6 ++ tests/auth/providers/test_homeassistant.py | 90 +++++++++++++++++++ 4 files changed, 143 insertions(+), 31 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 8c991d3f227..665bc308d49 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -28,6 +28,7 @@ from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRA from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config from .models import AuthFlowResult from .providers import AuthProvider, LoginFlow, auth_provider_from_config +from .providers.homeassistant import HassAuthProvider EVENT_USER_ADDED = "user_added" EVENT_USER_UPDATED = "user_updated" @@ -73,6 +74,13 @@ async def auth_manager_from_config( key = (provider.type, provider.id) provider_hash[key] = provider + if isinstance(provider, HassAuthProvider): + # Can be removed in 2026.7 with the legacy mode of homeassistant auth provider + # We need to initialize the provider to create the repair if needed as otherwise + # the provider will be initialized on first use, which could be rare as users + # don't frequently change auth settings + await provider.async_initialize() + if module_configs: modules = await asyncio.gather( *(auth_mfa_module_from_config(hass, config) for config in module_configs) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 1ed2f1dd3f7..4e38260dd2f 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -14,6 +14,7 @@ import voluptuous as vol from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.storage import Store from ..models import AuthFlowResult, Credentials, UserMeta @@ -88,7 +89,7 @@ class Data: 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. + # Deprecated in June 2019 and will be removed in 2026.7 self.is_legacy = False @callback @@ -106,44 +107,49 @@ class Data: if (data := await self._store.async_load()) is None: data = cast(dict[str, list[dict[str, str]]], {"users": []}) - seen: set[str] = set() + self._async_check_for_not_normalized_usernames(data) + self._data = data + + @callback + def _async_check_for_not_normalized_usernames( + self, data: dict[str, list[dict[str, str]]] + ) -> None: + not_normalized_usernames: set[str] = set() for user in data["users"]: username = user["username"] - # check if we have duplicates - if (folded := username.casefold()) in seen: - self.is_legacy = True - + if self.normalize_username(username, force_normalize=True) != username: logging.getLogger(__name__).warning( ( "Home Assistant auth provider is running in legacy mode " - "because we detected usernames that are case-insensitive" - "equivalent. Please change the username: '%s'." + "because we detected usernames that are normalized (lowercase and without spaces)." + " Please change the username: '%s'." ), username, ) + not_normalized_usernames.add(username) - break - - seen.add(folded) - - # check if we have unstripped usernames - if username != username.strip(): - self.is_legacy = True - - logging.getLogger(__name__).warning( - ( - "Home Assistant auth provider is running in legacy mode " - "because we detected usernames that start or end in a " - "space. Please change the username: '%s'." - ), - username, - ) - - break - - self._data = data + if not_normalized_usernames: + self.is_legacy = True + ir.async_create_issue( + self.hass, + "auth", + "homeassistant_provider_not_normalized_usernames", + breaks_in_ha_version="2026.7.0", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="homeassistant_provider_not_normalized_usernames", + translation_placeholders={ + "usernames": f'- "{'"\n- "'.join(sorted(not_normalized_usernames))}"' + }, + learn_more_url="homeassistant://config/users", + ) + else: + self.is_legacy = False + ir.async_delete_issue( + self.hass, "auth", "homeassistant_provider_not_normalized_usernames" + ) @property def users(self) -> list[dict[str, str]]: @@ -228,6 +234,7 @@ class Data: else: raise InvalidUser + @callback def _validate_new_username(self, new_username: str) -> None: """Validate that username is normalized and unique. @@ -251,6 +258,7 @@ class Data: translation_placeholders={"username": new_username}, ) + @callback def change_username(self, username: str, new_username: str) -> None: """Update the username. @@ -263,6 +271,8 @@ class Data: for user in self.users: if self.normalize_username(user["username"]) == username: user["username"] = new_username + assert self._data is not None + self._async_check_for_not_normalized_usernames(self._data) break else: raise InvalidUser @@ -346,9 +356,7 @@ class HassAuthProvider(AuthProvider): await self.async_initialize() assert self.data is not None - await self.hass.async_add_executor_job( - self.data.change_username, credential.data["username"], new_username - ) + self.data.change_username(credential.data["username"], new_username) self.hass.auth.async_update_user_credentials_data( credential, {**credential.data, "username": new_username} ) diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index 2b96b84c1cf..0e4cede78a3 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -39,5 +39,11 @@ "username_not_normalized": { "message": "Username \"{new_username}\" is not normalized" } + }, + "issues": { + "homeassistant_provider_not_normalized_usernames": { + "title": "Not normalized usernames detected", + "description": "The Home Assistant auth provider is running in legacy mode because we detected not normalized usernames. The legacy mode is deprecated and will be removed. Please change the following usernames:\n\n{usernames}\n\nNormalized usernames are case folded (lower case) and stripped of whitespaces." + } } } diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index 3224bf6b4f7..dd2ce65b480 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -1,6 +1,7 @@ """Test the Home Assistant local auth provider.""" import asyncio +from typing import Any from unittest.mock import Mock, patch import pytest @@ -13,6 +14,7 @@ from homeassistant.auth.providers import ( homeassistant as hass_auth, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component @@ -389,3 +391,91 @@ async def test_change_username_not_normalized( hass_auth.InvalidUsername, match='Username "TEST-user " is not normalized' ): data.change_username("test-user", "TEST-user ") + + +@pytest.mark.parametrize( + ("usernames_in_storage", "usernames_in_repair"), + [ + (["Uppercase"], '- "Uppercase"'), + ([" leading"], '- " leading"'), + (["trailing "], '- "trailing "'), + (["Test", "test", "Fritz "], '- "Fritz "\n- "Test"'), + ], +) +async def test_create_repair_on_legacy_usernames( + hass: HomeAssistant, + hass_storage: dict[str, Any], + issue_registry: ir.IssueRegistry, + usernames_in_storage: list[str], + usernames_in_repair: str, +) -> None: + """Test that we create a repair issue for legacy usernames.""" + assert not issue_registry.issues.get( + ("auth", "homeassistant_provider_not_normalized_usernames") + ), "Repair issue already exists" + + hass_storage[hass_auth.STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": "auth_provider.homeassistant", + "data": { + "users": [ + { + "username": username, + "password": "onlyherebecauseweneedapasswordstring", + } + for username in usernames_in_storage + ] + }, + } + data = hass_auth.Data(hass) + await data.async_load() + issue = issue_registry.issues.get( + ("auth", "homeassistant_provider_not_normalized_usernames") + ) + assert issue, "Repair issue not created" + assert issue.translation_placeholders == {"usernames": usernames_in_repair} + + +async def test_delete_repair_after_fixing_usernames( + hass: HomeAssistant, + hass_storage: dict[str, Any], + issue_registry: ir.IssueRegistry, +) -> None: + """Test that the repair is deleted after fixing the usernames.""" + hass_storage[hass_auth.STORAGE_KEY] = { + "version": 1, + "minor_version": 1, + "key": "auth_provider.homeassistant", + "data": { + "users": [ + { + "username": "Test", + "password": "onlyherebecauseweneedapasswordstring", + }, + { + "username": "bla ", + "password": "onlyherebecauseweneedapasswordstring", + }, + ] + }, + } + data = hass_auth.Data(hass) + await data.async_load() + issue = issue_registry.issues.get( + ("auth", "homeassistant_provider_not_normalized_usernames") + ) + assert issue, "Repair issue not created" + assert issue.translation_placeholders == {"usernames": '- "Test"\n- "bla "'} + + data.change_username("Test", "test") + issue = issue_registry.issues.get( + ("auth", "homeassistant_provider_not_normalized_usernames") + ) + assert issue + assert issue.translation_placeholders == {"usernames": '- "bla "'} + + data.change_username("bla ", "bla") + assert not issue_registry.issues.get( + ("auth", "homeassistant_provider_not_normalized_usernames") + ), "Repair issue should be deleted" From 8ce53d28e7fc333c9b4f16e0bf43d43b9f9416f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Wed, 26 Jun 2024 08:02:49 +0100 Subject: [PATCH 0174/2411] Handle availability in Idasen Desk height sensor (#120277) --- homeassistant/components/idasen_desk/cover.py | 2 +- .../components/idasen_desk/sensor.py | 6 +++++ tests/components/idasen_desk/test_sensors.py | 26 ++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/idasen_desk/cover.py b/homeassistant/components/idasen_desk/cover.py index f5591eff0d8..eb6bf5523de 100644 --- a/homeassistant/components/idasen_desk/cover.py +++ b/homeassistant/components/idasen_desk/cover.py @@ -66,7 +66,7 @@ class IdasenDeskCover(CoordinatorEntity[IdasenDeskCoordinator], CoverEntity): @property def available(self) -> bool: """Return True if entity is available.""" - return self._desk.is_connected is True + return super().available and self._desk.is_connected is True @property def is_closed(self) -> bool: diff --git a/homeassistant/components/idasen_desk/sensor.py b/homeassistant/components/idasen_desk/sensor.py index 12a3b2ed4d9..8ed85d21a34 100644 --- a/homeassistant/components/idasen_desk/sensor.py +++ b/homeassistant/components/idasen_desk/sensor.py @@ -79,12 +79,18 @@ class IdasenDeskSensor(CoordinatorEntity[IdasenDeskCoordinator], SensorEntity): self._attr_unique_id = f"{description.key}-{address}" self._attr_device_info = device_info self._address = address + self._desk = coordinator.desk async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() self._update_native_value() + @property + def available(self) -> bool: + """Return True if entity is available.""" + return super().available and self._desk.is_connected is True + @callback def _handle_coordinator_update(self, *args: Any) -> None: """Handle data update.""" diff --git a/tests/components/idasen_desk/test_sensors.py b/tests/components/idasen_desk/test_sensors.py index a236555a506..614bce523e6 100644 --- a/tests/components/idasen_desk/test_sensors.py +++ b/tests/components/idasen_desk/test_sensors.py @@ -4,10 +4,13 @@ from unittest.mock import MagicMock import pytest +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from . import init_integration +EXPECTED_INITIAL_HEIGHT = "1" + @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_height_sensor(hass: HomeAssistant, mock_desk_api: MagicMock) -> None: @@ -17,7 +20,7 @@ async def test_height_sensor(hass: HomeAssistant, mock_desk_api: MagicMock) -> N entity_id = "sensor.test_height" state = hass.states.get(entity_id) assert state - assert state.state == "1" + assert state.state == EXPECTED_INITIAL_HEIGHT mock_desk_api.height = 1.2 mock_desk_api.trigger_update_callback(None) @@ -25,3 +28,24 @@ async def test_height_sensor(hass: HomeAssistant, mock_desk_api: MagicMock) -> N state = hass.states.get(entity_id) assert state assert state.state == "1.2" + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor_available( + hass: HomeAssistant, + mock_desk_api: MagicMock, +) -> None: + """Test sensor available property.""" + await init_integration(hass) + + entity_id = "sensor.test_height" + state = hass.states.get(entity_id) + assert state + assert state.state == EXPECTED_INITIAL_HEIGHT + + mock_desk_api.is_connected = False + mock_desk_api.trigger_update_callback(None) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNAVAILABLE From d3ceaef09893d54405345eaa350a3d2139ef758c Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 26 Jun 2024 02:06:56 -0500 Subject: [PATCH 0175/2411] Allow timer management from any device (#120440) --- homeassistant/components/intent/timers.py | 47 ++----- homeassistant/helpers/llm.py | 2 +- .../conversation/test_default_agent.py | 20 ++- tests/components/intent/test_timers.py | 115 ++++++++---------- tests/helpers/test_llm.py | 2 +- 5 files changed, 77 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/intent/timers.py b/homeassistant/components/intent/timers.py index 40b55134e92..82f6121da53 100644 --- a/homeassistant/components/intent/timers.py +++ b/homeassistant/components/intent/timers.py @@ -490,7 +490,7 @@ class FindTimerFilter(StrEnum): def _find_timer( hass: HomeAssistant, - device_id: str, + device_id: str | None, slots: dict[str, Any], find_filter: FindTimerFilter | None = None, ) -> TimerInfo: @@ -577,7 +577,7 @@ def _find_timer( return matching_timers[0] # Use device id - if matching_timers: + if matching_timers and device_id: matching_device_timers = [ t for t in matching_timers if (t.device_id == device_id) ] @@ -626,7 +626,7 @@ def _find_timer( def _find_timers( - hass: HomeAssistant, device_id: str, slots: dict[str, Any] + hass: HomeAssistant, device_id: str | None, slots: dict[str, Any] ) -> list[TimerInfo]: """Match multiple timers with constraints or raise an error.""" timer_manager: TimerManager = hass.data[TIMER_DATA] @@ -689,6 +689,10 @@ def _find_timers( # No matches return matching_timers + if not device_id: + # Can't order using area/floor + return matching_timers + # Use device id to order remaining timers device_registry = dr.async_get(hass) device = device_registry.async_get(device_id) @@ -861,12 +865,6 @@ class CancelTimerIntentHandler(intent.IntentHandler): timer_manager: TimerManager = hass.data[TIMER_DATA] slots = self.async_validate_slots(intent_obj.slots) - if not ( - intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id) - ): - # Fail early - raise TimersNotSupportedError(intent_obj.device_id) - timer = _find_timer(hass, intent_obj.device_id, slots) timer_manager.cancel_timer(timer.id) return intent_obj.create_response() @@ -890,12 +888,6 @@ class IncreaseTimerIntentHandler(intent.IntentHandler): timer_manager: TimerManager = hass.data[TIMER_DATA] slots = self.async_validate_slots(intent_obj.slots) - if not ( - intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id) - ): - # Fail early - raise TimersNotSupportedError(intent_obj.device_id) - total_seconds = _get_total_seconds(slots) timer = _find_timer(hass, intent_obj.device_id, slots) timer_manager.add_time(timer.id, total_seconds) @@ -920,12 +912,6 @@ class DecreaseTimerIntentHandler(intent.IntentHandler): timer_manager: TimerManager = hass.data[TIMER_DATA] slots = self.async_validate_slots(intent_obj.slots) - if not ( - intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id) - ): - # Fail early - raise TimersNotSupportedError(intent_obj.device_id) - total_seconds = _get_total_seconds(slots) timer = _find_timer(hass, intent_obj.device_id, slots) timer_manager.remove_time(timer.id, total_seconds) @@ -949,12 +935,6 @@ class PauseTimerIntentHandler(intent.IntentHandler): timer_manager: TimerManager = hass.data[TIMER_DATA] slots = self.async_validate_slots(intent_obj.slots) - if not ( - intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id) - ): - # Fail early - raise TimersNotSupportedError(intent_obj.device_id) - timer = _find_timer( hass, intent_obj.device_id, slots, find_filter=FindTimerFilter.ONLY_ACTIVE ) @@ -979,12 +959,6 @@ class UnpauseTimerIntentHandler(intent.IntentHandler): timer_manager: TimerManager = hass.data[TIMER_DATA] slots = self.async_validate_slots(intent_obj.slots) - if not ( - intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id) - ): - # Fail early - raise TimersNotSupportedError(intent_obj.device_id) - timer = _find_timer( hass, intent_obj.device_id, slots, find_filter=FindTimerFilter.ONLY_INACTIVE ) @@ -1006,15 +980,8 @@ class TimerStatusIntentHandler(intent.IntentHandler): async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the intent.""" hass = intent_obj.hass - timer_manager: TimerManager = hass.data[TIMER_DATA] slots = self.async_validate_slots(intent_obj.slots) - if not ( - intent_obj.device_id and timer_manager.is_timer_device(intent_obj.device_id) - ): - # Fail early - raise TimersNotSupportedError(intent_obj.device_id) - statuses: list[dict[str, Any]] = [] for timer in _find_timers(hass, intent_obj.device_id, slots): total_seconds = timer.seconds_left diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 4410de67ef2..ba307a785ac 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -355,7 +355,7 @@ class AssistAPI(API): if not llm_context.device_id or not async_device_supports_timers( self.hass, llm_context.device_id ): - prompt.append("This device does not support timers.") + prompt.append("This device is not able to start timers.") if exposed_entities: prompt.append( diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index dee7b4ca0ff..f8a021475d5 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -860,13 +860,27 @@ async def test_error_feature_not_supported(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("init_components") -async def test_error_no_timer_support(hass: HomeAssistant) -> None: +async def test_error_no_timer_support( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, +) -> None: """Test error message when a device does not support timers (no handler is registered).""" - device_id = "test_device" + area_kitchen = area_registry.async_create("kitchen") + + entry = MockConfigEntry() + entry.add_to_hass(hass) + device_kitchen = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections=set(), + identifiers={("demo", "device-kitchen")}, + ) + device_registry.async_update_device(device_kitchen.id, area_id=area_kitchen.id) + device_id = device_kitchen.id # No timer handler is registered for the device result = await conversation.async_converse( - hass, "pause timer", None, Context(), None, device_id=device_id + hass, "set a 5 minute timer", None, Context(), None, device_id=device_id ) assert result.response.response_type == intent.IntentResponseType.ERROR diff --git a/tests/components/intent/test_timers.py b/tests/components/intent/test_timers.py index 329db6e8b2b..c2efe5d39e2 100644 --- a/tests/components/intent/test_timers.py +++ b/tests/components/intent/test_timers.py @@ -64,6 +64,7 @@ async def test_start_finish_timer(hass: HomeAssistant, init_components) -> None: async_register_timer_handler(hass, device_id, handle_timer) + # A device that has been registered to handle timers is required result = await intent.async_handle( hass, "test", @@ -185,6 +186,27 @@ async def test_cancel_timer(hass: HomeAssistant, init_components) -> None: async with asyncio.timeout(1): await cancelled_event.wait() + # Cancel without a device + timer_name = None + started_event.clear() + result = await intent.async_handle( + hass, + "test", + intent.INTENT_START_TIMER, + { + "hours": {"value": 1}, + "minutes": {"value": 2}, + "seconds": {"value": 3}, + }, + device_id=device_id, + ) + + async with asyncio.timeout(1): + await started_event.wait() + + result = await intent.async_handle(hass, "test", intent.INTENT_CANCEL_TIMER, {}) + assert result.response_type == intent.IntentResponseType.ACTION_DONE + async def test_increase_timer(hass: HomeAssistant, init_components) -> None: """Test increasing the time of a running timer.""" @@ -260,7 +282,6 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None: "minutes": {"value": 0}, "seconds": {"value": 0}, }, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -279,7 +300,6 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None: "minutes": {"value": 5}, "seconds": {"value": 30}, }, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -293,7 +313,6 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None: "test", intent.INTENT_CANCEL_TIMER, {"name": {"value": timer_name}}, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -375,7 +394,6 @@ async def test_decrease_timer(hass: HomeAssistant, init_components) -> None: "start_seconds": {"value": 3}, "seconds": {"value": 30}, }, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -389,7 +407,6 @@ async def test_decrease_timer(hass: HomeAssistant, init_components) -> None: "test", intent.INTENT_CANCEL_TIMER, {"name": {"value": timer_name}}, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -467,7 +484,6 @@ async def test_decrease_timer_below_zero(hass: HomeAssistant, init_components) - "start_seconds": {"value": 3}, "seconds": {"value": original_total_seconds + 1}, }, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -482,43 +498,25 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None: """Test finding a timer with the wrong info.""" device_id = "test_device" - for intent_name in ( - intent.INTENT_START_TIMER, - intent.INTENT_CANCEL_TIMER, - intent.INTENT_PAUSE_TIMER, - intent.INTENT_UNPAUSE_TIMER, - intent.INTENT_INCREASE_TIMER, - intent.INTENT_DECREASE_TIMER, - intent.INTENT_TIMER_STATUS, - ): - if intent_name in ( + # No device id + with pytest.raises(TimersNotSupportedError): + await intent.async_handle( + hass, + "test", intent.INTENT_START_TIMER, - intent.INTENT_INCREASE_TIMER, - intent.INTENT_DECREASE_TIMER, - ): - slots = {"minutes": {"value": 5}} - else: - slots = {} + {"minutes": {"value": 5}}, + device_id=None, + ) - # No device id - with pytest.raises(TimersNotSupportedError): - await intent.async_handle( - hass, - "test", - intent_name, - slots, - device_id=None, - ) - - # Unregistered device - with pytest.raises(TimersNotSupportedError): - await intent.async_handle( - hass, - "test", - intent_name, - slots, - device_id=device_id, - ) + # Unregistered device + with pytest.raises(TimersNotSupportedError): + await intent.async_handle( + hass, + "test", + intent.INTENT_START_TIMER, + {"minutes": {"value": 5}}, + device_id=device_id, + ) # Must register a handler before we can do anything with timers @callback @@ -543,7 +541,6 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None: "test", intent.INTENT_INCREASE_TIMER, {"name": {"value": "PIZZA "}, "minutes": {"value": 1}}, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -554,7 +551,6 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None: "test", intent.INTENT_CANCEL_TIMER, {"name": {"value": "does-not-exist"}}, - device_id=device_id, ) # Right start time @@ -563,7 +559,6 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None: "test", intent.INTENT_INCREASE_TIMER, {"start_minutes": {"value": 5}, "minutes": {"value": 1}}, - device_id=device_id, ) assert result.response_type == intent.IntentResponseType.ACTION_DONE @@ -574,7 +569,6 @@ async def test_find_timer_failed(hass: HomeAssistant, init_components) -> None: "test", intent.INTENT_CANCEL_TIMER, {"start_minutes": {"value": 1}}, - device_id=device_id, ) @@ -903,9 +897,7 @@ async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None # Pause the timer expected_active = False - result = await intent.async_handle( - hass, "test", intent.INTENT_PAUSE_TIMER, {}, device_id=device_id - ) + result = await intent.async_handle(hass, "test", intent.INTENT_PAUSE_TIMER, {}) assert result.response_type == intent.IntentResponseType.ACTION_DONE async with asyncio.timeout(1): @@ -913,16 +905,12 @@ async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None # Pausing again will fail because there are no running timers with pytest.raises(TimerNotFoundError): - await intent.async_handle( - hass, "test", intent.INTENT_PAUSE_TIMER, {}, device_id=device_id - ) + await intent.async_handle(hass, "test", intent.INTENT_PAUSE_TIMER, {}) # Unpause the timer updated_event.clear() expected_active = True - result = await intent.async_handle( - hass, "test", intent.INTENT_UNPAUSE_TIMER, {}, device_id=device_id - ) + result = await intent.async_handle(hass, "test", intent.INTENT_UNPAUSE_TIMER, {}) assert result.response_type == intent.IntentResponseType.ACTION_DONE async with asyncio.timeout(1): @@ -930,9 +918,7 @@ async def test_pause_unpause_timer(hass: HomeAssistant, init_components) -> None # Unpausing again will fail because there are no paused timers with pytest.raises(TimerNotFoundError): - await intent.async_handle( - hass, "test", intent.INTENT_UNPAUSE_TIMER, {}, device_id=device_id - ) + await intent.async_handle(hass, "test", intent.INTENT_UNPAUSE_TIMER, {}) async def test_timer_not_found(hass: HomeAssistant) -> None: @@ -1101,13 +1087,14 @@ async def test_timer_status_with_names(hass: HomeAssistant, init_components) -> await started_event.wait() # No constraints returns all timers - result = await intent.async_handle( - hass, "test", intent.INTENT_TIMER_STATUS, {}, device_id=device_id - ) - assert result.response_type == intent.IntentResponseType.ACTION_DONE - timers = result.speech_slots.get("timers", []) - assert len(timers) == 4 - assert {t.get(ATTR_NAME) for t in timers} == {"pizza", "cookies", "chicken"} + for handle_device_id in (device_id, None): + result = await intent.async_handle( + hass, "test", intent.INTENT_TIMER_STATUS, {}, device_id=handle_device_id + ) + assert result.response_type == intent.IntentResponseType.ACTION_DONE + timers = result.speech_slots.get("timers", []) + assert len(timers) == 4 + assert {t.get(ATTR_NAME) for t in timers} == {"pizza", "cookies", "chicken"} # Get status of cookie timer result = await intent.async_handle( diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 872297b09ec..ad18aa53071 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -578,7 +578,7 @@ async def test_assist_api_prompt( "(what comes before the dot in its entity id). " "When controlling an area, prefer passing just area name and domain." ) - no_timer_prompt = "This device does not support timers." + no_timer_prompt = "This device is not able to start timers." area_prompt = ( "When a user asks to turn on all devices of a specific type, " From e567f8f3d5d9cdfb485c06794473c0608a76d656 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 09:14:33 +0200 Subject: [PATCH 0176/2411] Fix issue where an MQTT device is removed linked to two config entries (#120430) * Fix issue where an MQTT device is removed linked to two config entries * Update homeassistant/components/mqtt/discovery.py Co-authored-by: J. Nick Koston * Update homeassistant/components/mqtt/debug_info.py Co-authored-by: J. Nick Koston --------- Co-authored-by: J. Nick Koston --- homeassistant/components/mqtt/debug_info.py | 2 +- homeassistant/components/mqtt/discovery.py | 2 +- homeassistant/components/mqtt/tag.py | 5 +++-- tests/components/mqtt/test_discovery.py | 4 +++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index a8fd318b1e9..2985e6d7707 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -138,7 +138,7 @@ def remove_trigger_discovery_data( hass: HomeAssistant, discovery_hash: tuple[str, str] ) -> None: """Remove discovery data.""" - del hass.data[DATA_MQTT].debug_info_triggers[discovery_hash] + hass.data[DATA_MQTT].debug_info_triggers.pop(discovery_hash, None) def _info_for_entity(hass: HomeAssistant, entity_id: str) -> dict[str, Any]: diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 0d93af26a57..cf2941a3665 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -73,7 +73,7 @@ class MQTTDiscoveryPayload(dict[str, Any]): def clear_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]) -> None: """Clear entry from already discovered list.""" - hass.data[DATA_MQTT].discovery_already_discovered.remove(discovery_hash) + hass.data[DATA_MQTT].discovery_already_discovered.discard(discovery_hash) def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]) -> None: diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 22263a07499..fbb0ea813c2 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -180,5 +180,6 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdateMixin): self._sub_state = subscription.async_unsubscribe_topics( self.hass, self._sub_state ) - if self.device_id: - del self.hass.data[DATA_MQTT].tags[self.device_id][discovery_id] + tags = self.hass.data[DATA_MQTT].tags + if self.device_id in tags and discovery_id in tags[self.device_id]: + del tags[self.device_id][discovery_id] diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index fbf878a040a..23dea310199 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -809,7 +809,7 @@ async def test_duplicate_removal( assert "Component has already been discovered: binary_sensor bla" not in caplog.text -async def test_cleanup_device( +async def test_cleanup_device_manual( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, device_registry: dr.DeviceRegistry, @@ -1012,6 +1012,7 @@ async def test_cleanup_device_multiple_config_entries( async def test_cleanup_device_multiple_config_entries_mqtt( hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -1093,6 +1094,7 @@ async def test_cleanup_device_multiple_config_entries_mqtt( # Verify retained discovery topics have not been cleared again mqtt_mock.async_publish.assert_not_called() + assert "KeyError:" not in caplog.text async def test_discovery_expansion( From 30e0bcb3247fe46eb4973d7008d4ea9dbed12950 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 09:20:43 +0200 Subject: [PATCH 0177/2411] Bump dbus-fast to 2.22.1 (#120491) --- 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 df2278399ab..12bb37ac570 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -19,7 +19,7 @@ "bluetooth-adapters==0.19.3", "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.3", - "dbus-fast==2.21.3", + "dbus-fast==2.22.1", "habluetooth==3.1.3" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 25d10874239..d3320e64fe3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -24,7 +24,7 @@ cached_ipaddress==0.3.0 certifi>=2021.5.30 ciso8601==2.3.1 cryptography==42.0.8 -dbus-fast==2.21.3 +dbus-fast==2.22.1 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 82ca566a925..3548be4ad60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -697,7 +697,7 @@ datadog==0.15.0 datapoint==0.9.9 # homeassistant.components.bluetooth -dbus-fast==2.21.3 +dbus-fast==2.22.1 # homeassistant.components.debugpy debugpy==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0d39fdd0bc..e1fb561ecee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -584,7 +584,7 @@ datadog==0.15.0 datapoint==0.9.9 # homeassistant.components.bluetooth -dbus-fast==2.21.3 +dbus-fast==2.22.1 # homeassistant.components.debugpy debugpy==1.8.1 From bff9d12cc087bcadc82b892719d0d61a953013c7 Mon Sep 17 00:00:00 2001 From: Thomas Kistler Date: Wed, 26 Jun 2024 00:24:48 -0700 Subject: [PATCH 0178/2411] Add active watering time sensor to Hydrawise (#120177) --- .../components/hydrawise/coordinator.py | 22 +-- homeassistant/components/hydrawise/icons.json | 3 + homeassistant/components/hydrawise/sensor.py | 92 +++++++--- .../components/hydrawise/strings.json | 5 +- tests/components/hydrawise/conftest.py | 2 + .../hydrawise/snapshots/test_sensor.ambr | 171 ++++++++++++++++-- tests/components/hydrawise/test_sensor.py | 19 +- 7 files changed, 263 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/hydrawise/coordinator.py b/homeassistant/components/hydrawise/coordinator.py index 50caaa0c0de..6cd233eb1df 100644 --- a/homeassistant/components/hydrawise/coordinator.py +++ b/homeassistant/components/hydrawise/coordinator.py @@ -23,7 +23,7 @@ class HydrawiseData: controllers: dict[int, Controller] zones: dict[int, Zone] sensors: dict[int, Sensor] - daily_water_use: dict[int, ControllerWaterUseSummary] + daily_water_summary: dict[int, ControllerWaterUseSummary] class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]): @@ -47,7 +47,7 @@ class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]): controllers = {} zones = {} sensors = {} - daily_water_use: dict[int, ControllerWaterUseSummary] = {} + daily_water_summary: dict[int, ControllerWaterUseSummary] = {} for controller in user.controllers: controllers[controller.id] = controller controller.zones = await self.api.get_zones(controller) @@ -55,22 +55,16 @@ class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]): zones[zone.id] = zone for sensor in controller.sensors: sensors[sensor.id] = sensor - if any( - "flow meter" in sensor.model.name.lower() - for sensor in controller.sensors - ): - daily_water_use[controller.id] = await self.api.get_water_use_summary( - controller, - now().replace(hour=0, minute=0, second=0, microsecond=0), - now(), - ) - else: - daily_water_use[controller.id] = ControllerWaterUseSummary() + daily_water_summary[controller.id] = await self.api.get_water_use_summary( + controller, + now().replace(hour=0, minute=0, second=0, microsecond=0), + now(), + ) return HydrawiseData( user=user, controllers=controllers, zones=zones, sensors=sensors, - daily_water_use=daily_water_use, + daily_water_summary=daily_water_summary, ) diff --git a/homeassistant/components/hydrawise/icons.json b/homeassistant/components/hydrawise/icons.json index 64deab590da..4af4fe75fcc 100644 --- a/homeassistant/components/hydrawise/icons.json +++ b/homeassistant/components/hydrawise/icons.json @@ -4,6 +4,9 @@ "daily_active_water_use": { "default": "mdi:water" }, + "daily_active_water_time": { + "default": "mdi:timelapse" + }, "daily_inactive_water_use": { "default": "mdi:water" }, diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index fe4b33d5851..563af893700 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timedelta from typing import Any from homeassistant.components.sensor import ( @@ -44,28 +44,65 @@ def _get_zone_next_cycle(sensor: HydrawiseSensor) -> datetime | None: def _get_zone_daily_active_water_use(sensor: HydrawiseSensor) -> float: """Get active water use for the zone.""" - daily_water_summary = sensor.coordinator.data.daily_water_use[sensor.controller.id] + daily_water_summary = sensor.coordinator.data.daily_water_summary[ + sensor.controller.id + ] return float(daily_water_summary.active_use_by_zone_id.get(sensor.zone.id, 0.0)) +def _get_zone_daily_active_water_time(sensor: HydrawiseSensor) -> float | None: + """Get active water time for the zone.""" + daily_water_summary = sensor.coordinator.data.daily_water_summary[ + sensor.controller.id + ] + return daily_water_summary.active_time_by_zone_id.get( + sensor.zone.id, timedelta() + ).total_seconds() + + def _get_controller_daily_active_water_use(sensor: HydrawiseSensor) -> float | None: """Get active water use for the controller.""" - daily_water_summary = sensor.coordinator.data.daily_water_use[sensor.controller.id] + daily_water_summary = sensor.coordinator.data.daily_water_summary[ + sensor.controller.id + ] return daily_water_summary.total_active_use def _get_controller_daily_inactive_water_use(sensor: HydrawiseSensor) -> float | None: """Get inactive water use for the controller.""" - daily_water_summary = sensor.coordinator.data.daily_water_use[sensor.controller.id] + daily_water_summary = sensor.coordinator.data.daily_water_summary[ + sensor.controller.id + ] return daily_water_summary.total_inactive_use +def _get_controller_daily_active_water_time(sensor: HydrawiseSensor) -> float: + """Get active water time for the controller.""" + daily_water_summary = sensor.coordinator.data.daily_water_summary[ + sensor.controller.id + ] + return daily_water_summary.total_active_time.total_seconds() + + def _get_controller_daily_total_water_use(sensor: HydrawiseSensor) -> float | None: """Get inactive water use for the controller.""" - daily_water_summary = sensor.coordinator.data.daily_water_use[sensor.controller.id] + daily_water_summary = sensor.coordinator.data.daily_water_summary[ + sensor.controller.id + ] return daily_water_summary.total_use +CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( + HydrawiseSensorEntityDescription( + key="daily_active_water_time", + translation_key="daily_active_water_time", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + value_fn=_get_controller_daily_active_water_time, + ), +) + + FLOW_CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( HydrawiseSensorEntityDescription( key="daily_total_water_use", @@ -113,6 +150,13 @@ ZONE_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.MINUTES, value_fn=_get_zone_watering_time, ), + HydrawiseSensorEntityDescription( + key="daily_active_water_time", + translation_key="daily_active_water_time", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + value_fn=_get_zone_daily_active_water_time, + ), ) FLOW_MEASUREMENT_KEYS = [x.key for x in FLOW_CONTROLLER_SENSORS] @@ -129,30 +173,31 @@ async def async_setup_entry( ] entities: list[HydrawiseSensor] = [] for controller in coordinator.data.controllers.values(): + entities.extend( + HydrawiseSensor(coordinator, description, controller) + for description in CONTROLLER_SENSORS + ) entities.extend( HydrawiseSensor(coordinator, description, controller, zone_id=zone.id) for zone in controller.zones for description in ZONE_SENSORS ) - entities.extend( - HydrawiseSensor(coordinator, description, controller, sensor_id=sensor.id) - for sensor in controller.sensors - for description in FLOW_CONTROLLER_SENSORS - if "flow meter" in sensor.model.name.lower() - ) - entities.extend( - HydrawiseSensor( - coordinator, - description, - controller, - zone_id=zone.id, - sensor_id=sensor.id, + if coordinator.data.daily_water_summary[controller.id].total_use is not None: + # we have a flow sensor for this controller + entities.extend( + HydrawiseSensor(coordinator, description, controller) + for description in FLOW_CONTROLLER_SENSORS + ) + entities.extend( + HydrawiseSensor( + coordinator, + description, + controller, + zone_id=zone.id, + ) + for zone in controller.zones + for description in FLOW_ZONE_SENSORS ) - for zone in controller.zones - for sensor in controller.sensors - for description in FLOW_ZONE_SENSORS - if "flow meter" in sensor.model.name.lower() - ) async_add_entities(entities) @@ -177,6 +222,7 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity): """Icon of the entity based on the value.""" if ( self.entity_description.key in FLOW_MEASUREMENT_KEYS + and self.entity_description.device_class == SensorDeviceClass.VOLUME and round(self.state, 2) == 0.0 ): return "mdi:water-outline" diff --git a/homeassistant/components/hydrawise/strings.json b/homeassistant/components/hydrawise/strings.json index 1bc5525c9d9..c455412d1a4 100644 --- a/homeassistant/components/hydrawise/strings.json +++ b/homeassistant/components/hydrawise/strings.json @@ -33,6 +33,9 @@ "daily_total_water_use": { "name": "Daily total water use" }, + "daily_active_water_time": { + "name": "Daily active watering time" + }, "daily_active_water_use": { "name": "Daily active water use" }, @@ -43,7 +46,7 @@ "name": "Next cycle" }, "watering_time": { - "name": "Watering time" + "name": "Remaining watering time" } }, "switch": { diff --git a/tests/components/hydrawise/conftest.py b/tests/components/hydrawise/conftest.py index 0b5327cd7b2..a938322414b 100644 --- a/tests/components/hydrawise/conftest.py +++ b/tests/components/hydrawise/conftest.py @@ -187,6 +187,8 @@ def controller_water_use_summary() -> ControllerWaterUseSummary: total_active_use=332.6, total_inactive_use=13.0, active_use_by_zone_id={5965394: 120.1, 5965395: 0.0}, + total_active_time=timedelta(seconds=123), + active_time_by_zone_id={5965394: timedelta(seconds=123), 5965395: timedelta()}, unit="gal", ) diff --git a/tests/components/hydrawise/snapshots/test_sensor.ambr b/tests/components/hydrawise/snapshots/test_sensor.ambr index 3472de98460..dadf3c44789 100644 --- a/tests/components/hydrawise/snapshots/test_sensor.ambr +++ b/tests/components/hydrawise/snapshots/test_sensor.ambr @@ -54,6 +54,55 @@ 'state': '1259.0279593584', }) # --- +# name: test_all_sensors[sensor.home_controller_daily_active_watering_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_controller_daily_active_watering_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Daily active watering time', + 'platform': 'hydrawise', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_active_water_time', + 'unique_id': '52496_daily_active_water_time', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.home_controller_daily_active_watering_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by hydrawise.com', + 'device_class': 'duration', + 'friendly_name': 'Home Controller Daily active watering time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_controller_daily_active_watering_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '123.0', + }) +# --- # name: test_all_sensors[sensor.home_controller_daily_inactive_water_use-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -219,6 +268,55 @@ 'state': '454.6279552584', }) # --- +# name: test_all_sensors[sensor.zone_one_daily_active_watering_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.zone_one_daily_active_watering_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Daily active watering time', + 'platform': 'hydrawise', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_active_water_time', + 'unique_id': '5965394_daily_active_water_time', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.zone_one_daily_active_watering_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by hydrawise.com', + 'device_class': 'duration', + 'friendly_name': 'Zone One Daily active watering time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.zone_one_daily_active_watering_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '123.0', + }) +# --- # name: test_all_sensors[sensor.zone_one_next_cycle-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -267,7 +365,7 @@ 'state': '2023-10-04T19:49:57+00:00', }) # --- -# name: test_all_sensors[sensor.zone_one_watering_time-entry] +# name: test_all_sensors[sensor.zone_one_remaining_watering_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -279,7 +377,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.zone_one_watering_time', + 'entity_id': 'sensor.zone_one_remaining_watering_time', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -291,7 +389,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Watering time', + 'original_name': 'Remaining watering time', 'platform': 'hydrawise', 'previous_unique_id': None, 'supported_features': 0, @@ -300,15 +398,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_sensors[sensor.zone_one_watering_time-state] +# name: test_all_sensors[sensor.zone_one_remaining_watering_time-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by hydrawise.com', - 'friendly_name': 'Zone One Watering time', + 'friendly_name': 'Zone One Remaining watering time', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.zone_one_watering_time', + 'entity_id': 'sensor.zone_one_remaining_watering_time', 'last_changed': , 'last_reported': , 'last_updated': , @@ -371,6 +469,55 @@ 'state': '0.0', }) # --- +# name: test_all_sensors[sensor.zone_two_daily_active_watering_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.zone_two_daily_active_watering_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Daily active watering time', + 'platform': 'hydrawise', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_active_water_time', + 'unique_id': '5965395_daily_active_water_time', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.zone_two_daily_active_watering_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by hydrawise.com', + 'device_class': 'duration', + 'friendly_name': 'Zone Two Daily active watering time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.zone_two_daily_active_watering_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_all_sensors[sensor.zone_two_next_cycle-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -419,7 +566,7 @@ 'state': 'unknown', }) # --- -# name: test_all_sensors[sensor.zone_two_watering_time-entry] +# name: test_all_sensors[sensor.zone_two_remaining_watering_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -431,7 +578,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.zone_two_watering_time', + 'entity_id': 'sensor.zone_two_remaining_watering_time', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -443,7 +590,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Watering time', + 'original_name': 'Remaining watering time', 'platform': 'hydrawise', 'previous_unique_id': None, 'supported_features': 0, @@ -452,15 +599,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_sensors[sensor.zone_two_watering_time-state] +# name: test_all_sensors[sensor.zone_two_remaining_watering_time-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by hydrawise.com', - 'friendly_name': 'Zone Two Watering time', + 'friendly_name': 'Zone Two Remaining watering time', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.zone_two_watering_time', + 'entity_id': 'sensor.zone_two_remaining_watering_time', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/hydrawise/test_sensor.py b/tests/components/hydrawise/test_sensor.py index af75ad69ade..b9ff99f0013 100644 --- a/tests/components/hydrawise/test_sensor.py +++ b/tests/components/hydrawise/test_sensor.py @@ -3,7 +3,7 @@ from collections.abc import Awaitable, Callable from unittest.mock import patch -from pydrawise.schema import Controller, User, Zone +from pydrawise.schema import Controller, ControllerWaterUseSummary, User, Zone import pytest from syrupy.assertion import SnapshotAssertion @@ -53,10 +53,15 @@ async def test_suspended_state( async def test_no_sensor_and_water_state( hass: HomeAssistant, controller: Controller, + controller_water_use_summary: ControllerWaterUseSummary, mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]], ) -> None: """Test rain sensor, flow sensor, and water use in the absence of flow and rain sensors.""" controller.sensors = [] + controller_water_use_summary.total_use = None + controller_water_use_summary.total_active_use = None + controller_water_use_summary.total_inactive_use = None + controller_water_use_summary.active_use_by_zone_id = {} await mock_add_config_entry() assert hass.states.get("sensor.zone_one_daily_active_water_use") is None @@ -65,6 +70,18 @@ async def test_no_sensor_and_water_state( assert hass.states.get("sensor.home_controller_daily_inactive_water_use") is None assert hass.states.get("binary_sensor.home_controller_rain_sensor") is None + sensor = hass.states.get("sensor.home_controller_daily_active_watering_time") + assert sensor is not None + assert sensor.state == "123.0" + + sensor = hass.states.get("sensor.zone_one_daily_active_watering_time") + assert sensor is not None + assert sensor.state == "123.0" + + sensor = hass.states.get("sensor.zone_two_daily_active_watering_time") + assert sensor is not None + assert sensor.state == "0.0" + sensor = hass.states.get("binary_sensor.home_controller_connectivity") assert sensor is not None assert sensor.state == "on" From 5a0841155ef6ce076f4f30d93c58cb4a2e3dcf38 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 09:28:11 +0200 Subject: [PATCH 0179/2411] Add unique_id to MPD (#120495) --- homeassistant/components/mpd/media_player.py | 85 +++++++------------- 1 file changed, 30 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index f0df2cdbbe2..204bbc7f499 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -31,6 +31,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -129,7 +130,7 @@ async def async_setup_entry( entry.data[CONF_HOST], entry.data[CONF_PORT], entry.data.get(CONF_PASSWORD), - entry.title, + entry.entry_id, ) ], True, @@ -140,23 +141,26 @@ class MpdDevice(MediaPlayerEntity): """Representation of a MPD server.""" _attr_media_content_type = MediaType.MUSIC + _attr_has_entity_name = True + _attr_name = None - def __init__(self, server, port, password, name): + def __init__( + self, server: str, port: int, password: str | None, unique_id: str + ) -> None: """Initialize the MPD device.""" self.server = server self.port = port - self._name = name + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + entry_type=DeviceEntryType.SERVICE, + ) self.password = password - self._status = {} + self._status: dict[str, Any] = {} self._currentsong = None - self._playlists = None - self._currentplaylist = None - self._is_available = None - self._muted = False + self._current_playlist: str | None = None self._muted_volume = None - self._media_position_updated_at = None - self._media_position = None self._media_image_hash = None # Track if the song changed so image doesn't have to be loaded every update. self._media_image_file = None @@ -188,7 +192,7 @@ class MpdDevice(MediaPlayerEntity): raise TimeoutError("Connection attempt timed out") from error if self.password is not None: await self._client.password(self.password) - self._is_available = True + self._attr_available = True yield except ( TimeoutError, @@ -199,12 +203,12 @@ class MpdDevice(MediaPlayerEntity): # Log a warning during startup or when previously connected; for # subsequent errors a debug message is sufficient. log_level = logging.DEBUG - if self._is_available is not False: + if self._attr_available is not False: log_level = logging.WARNING LOGGER.log( log_level, "Error connecting to '%s': %s", self.server, error ) - self._is_available = False + self._attr_available = False self._status = {} # Also yield on failure. Handling mpd.ConnectionErrors caused by # attempting to control a disconnected client is the @@ -228,24 +232,14 @@ class MpdDevice(MediaPlayerEntity): if isinstance(position, str) and ":" in position: position = position.split(":")[0] - if position is not None and self._media_position != position: - self._media_position_updated_at = dt_util.utcnow() - self._media_position = int(float(position)) + if position is not None and self._attr_media_position != position: + self._attr_media_position_updated_at = dt_util.utcnow() + self._attr_media_position = int(float(position)) await self._update_playlists() except (mpd.ConnectionError, ValueError) as error: LOGGER.debug("Error updating status: %s", error) - @property - def available(self) -> bool: - """Return true if MPD is available and connected.""" - return self._is_available is True - - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def state(self) -> MediaPlayerState: """Return the media state.""" @@ -260,11 +254,6 @@ class MpdDevice(MediaPlayerEntity): return MediaPlayerState.OFF - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._muted - @property def media_content_id(self): """Return the content ID of current playing media.""" @@ -282,20 +271,6 @@ class MpdDevice(MediaPlayerEntity): return None - @property - def media_position(self): - """Position of current playing media in seconds. - - This is returned as part of the mpd status rather than in the details - of the current song. - """ - return self._media_position - - @property - def media_position_updated_at(self): - """Last valid time of media position.""" - return self._media_position_updated_at - @property def media_title(self): """Return the title of current playing media.""" @@ -436,7 +411,7 @@ class MpdDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_MUTE ) - if self._playlists is not None: + if self._attr_source_list is not None: supported |= MediaPlayerEntityFeature.SELECT_SOURCE return supported @@ -444,7 +419,7 @@ class MpdDevice(MediaPlayerEntity): @property def source(self): """Name of the current input source.""" - return self._currentplaylist + return self._current_playlist @property def source_list(self): @@ -459,12 +434,12 @@ class MpdDevice(MediaPlayerEntity): async def _update_playlists(self, **kwargs: Any) -> None: """Update available MPD playlists.""" try: - self._playlists = [] + self._attr_source_list = [] with suppress(mpd.ConnectionError): for playlist_data in await self._client.listplaylists(): - self._playlists.append(playlist_data["playlist"]) + self._attr_source_list.append(playlist_data["playlist"]) except mpd.CommandError as error: - self._playlists = None + self._attr_source_list = None LOGGER.warning("Playlists could not be updated: %s:", error) async def async_set_volume_level(self, volume: float) -> None: @@ -527,7 +502,7 @@ class MpdDevice(MediaPlayerEntity): await self.async_set_volume_level(0) elif self._muted_volume is not None: await self.async_set_volume_level(self._muted_volume) - self._muted = mute + self._attr_is_volume_muted = mute async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any @@ -543,17 +518,17 @@ class MpdDevice(MediaPlayerEntity): if media_type == MediaType.PLAYLIST: LOGGER.debug("Playing playlist: %s", media_id) - if media_id in self._playlists: - self._currentplaylist = media_id + if self._attr_source_list and media_id in self._attr_source_list: + self._current_playlist = media_id else: - self._currentplaylist = None + self._current_playlist = None LOGGER.warning("Unknown playlist name %s", media_id) await self._client.clear() await self._client.load(media_id) await self._client.play() else: await self._client.clear() - self._currentplaylist = None + self._current_playlist = None await self._client.add(media_id) await self._client.play() From c5b7d2d86855a6b5e0f49323f2ccb839d4a2657a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 09:29:40 +0200 Subject: [PATCH 0180/2411] Cleanup mqtt platform tests part 3 (#120493) --- tests/components/mqtt/test_fan.py | 31 ++++++++--------------- tests/components/mqtt/test_humidifier.py | 32 +++++++++--------------- tests/components/mqtt/test_image.py | 17 +++---------- 3 files changed, 26 insertions(+), 54 deletions(-) diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 80e45c87789..2d1d717c58f 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -83,10 +83,9 @@ DEFAULT_CONFIG = { @pytest.mark.parametrize("hass_config", [{mqtt.DOMAIN: {fan.DOMAIN: {"name": "test"}}}]) +@pytest.mark.usefixtures("hass") async def test_fail_setup_if_no_command_topic( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test if command fails with command topic.""" assert await mqtt_mock_entry() @@ -611,8 +610,7 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( ], ) async def test_sending_mqtt_commands_and_optimistic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test optimistic mode without state topic.""" mqtt_mock = await mqtt_mock_entry() @@ -861,9 +859,7 @@ async def test_sending_mqtt_commands_with_alternate_speed_range( ], ) async def test_sending_mqtt_commands_and_optimistic_no_legacy( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test optimistic mode without state topic without legacy speed command topic.""" mqtt_mock = await mqtt_mock_entry() @@ -1005,8 +1001,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy( ], ) async def test_sending_mqtt_command_templates_( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test optimistic mode without state topic without legacy speed command topic.""" mqtt_mock = await mqtt_mock_entry() @@ -1166,8 +1161,7 @@ async def test_sending_mqtt_command_templates_( ], ) async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test optimistic mode without state topic without percentage command topic.""" mqtt_mock = await mqtt_mock_entry() @@ -1237,8 +1231,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( ], ) async def test_sending_mqtt_commands_and_explicit_optimistic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test optimistic mode with state topic and turn on attributes.""" mqtt_mock = await mqtt_mock_entry() @@ -1533,9 +1526,7 @@ async def test_encoding_subscribable_topics( ], ) async def test_attributes( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test attributes.""" await mqtt_mock_entry() @@ -2215,8 +2206,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = fan.DOMAIN @@ -2239,8 +2229,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = fan.DOMAIN diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index b583412b4ff..05180c17b2f 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -83,17 +83,16 @@ DEFAULT_CONFIG = { } -async def async_turn_on( - hass: HomeAssistant, - entity_id=ENTITY_MATCH_ALL, -) -> None: +async def async_turn_on(hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL) -> None: """Turn all or specified humidifier on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) -async def async_turn_off(hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL) -> None: +async def async_turn_off( + hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL +) -> None: """Turn all or specified humidier off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} @@ -101,7 +100,7 @@ async def async_turn_off(hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL) -> Non async def async_set_mode( - hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, mode: str | None = None + hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL, mode: str | None = None ) -> None: """Set mode for all or specified humidifier.""" data = { @@ -114,7 +113,7 @@ async def async_set_mode( async def async_set_humidity( - hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, humidity: int | None = None + hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL, humidity: int | None = None ) -> None: """Set target humidity for all or specified humidifier.""" data = { @@ -129,10 +128,9 @@ async def async_set_humidity( @pytest.mark.parametrize( "hass_config", [{mqtt.DOMAIN: {humidifier.DOMAIN: {"name": "test"}}}] ) +@pytest.mark.usefixtures("hass") async def test_fail_setup_if_no_command_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test if command fails with command topic.""" assert await mqtt_mock_entry() @@ -892,9 +890,7 @@ async def test_encoding_subscribable_topics( ], ) async def test_attributes( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test attributes.""" await mqtt_mock_entry() @@ -1048,9 +1044,7 @@ async def test_attributes( ], ) async def test_validity_configurations( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - valid: bool, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, valid: bool ) -> None: """Test validity of configurations.""" await mqtt_mock_entry() @@ -1499,8 +1493,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = humidifier.DOMAIN @@ -1523,8 +1516,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_config_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = humidifier.DOMAIN diff --git a/tests/components/mqtt/test_image.py b/tests/components/mqtt/test_image.py index a299474c0ac..29109ee12f4 100644 --- a/tests/components/mqtt/test_image.py +++ b/tests/components/mqtt/test_image.py @@ -356,7 +356,6 @@ async def test_image_from_url_content_type( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, content_type: str, setup_ok: bool, ) -> None: @@ -425,7 +424,6 @@ async def test_image_from_url_fails( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, side_effect: Exception, ) -> None: """Test setup with minimum configuration.""" @@ -501,9 +499,8 @@ async def test_image_from_url_fails( ), ], ) +@pytest.mark.usesfixtures("hass", "hass_client_no_auth") async def test_image_config_fails( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, error_msg: str, @@ -721,11 +718,7 @@ async def test_entity_id_update_subscriptions( ) -> None: """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, - mqtt_mock_entry, - image.DOMAIN, - DEFAULT_CONFIG, - ["test_topic"], + hass, mqtt_mock_entry, image.DOMAIN, DEFAULT_CONFIG, ["test_topic"] ) @@ -754,8 +747,7 @@ async def test_entity_debug_info_message( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = image.DOMAIN @@ -774,8 +766,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = image.DOMAIN From 1b884489144769079a95647dcb659f0052251cd0 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 26 Jun 2024 09:34:45 +0200 Subject: [PATCH 0181/2411] Do not wait for Reolink firmware check (#120377) --- homeassistant/components/reolink/__init__.py | 25 +++++++++---------- homeassistant/components/reolink/host.py | 1 + tests/components/reolink/conftest.py | 1 + tests/components/reolink/test_config_flow.py | 12 ++++----- tests/components/reolink/test_init.py | 26 +++++++++++++++++--- 5 files changed, 41 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index a3e49f1f526..150a23dc64e 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b raise ConfigEntryNotReady( f"Error while trying to setup {host.api.host}:{host.api.port}: {err!s}" ) from err - except Exception: + except BaseException: await host.stop() raise @@ -75,8 +75,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, host.stop) ) - starting = True - async def async_device_config_update() -> None: """Update the host state cache and renew the ONVIF-subscription.""" async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): @@ -103,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: await host.api.check_new_firmware(host.firmware_ch_list) except ReolinkError as err: - if starting: + if host.starting: _LOGGER.debug( "Error checking Reolink firmware update at startup " "from %s, possibly internet access is blocked", @@ -116,6 +114,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b "if the camera is blocked from accessing the internet, " "disable the update entity" ) from err + finally: + host.starting = False device_coordinator = DataUpdateCoordinator( hass, @@ -131,17 +131,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b update_method=async_check_firmware_update, update_interval=FIRMWARE_UPDATE_INTERVAL, ) + + # If camera WAN blocked, firmware check fails and takes long, do not prevent setup + config_entry.async_create_task(hass, firmware_coordinator.async_refresh()) # Fetch initial data so we have data when entities subscribe - results = await asyncio.gather( - device_coordinator.async_config_entry_first_refresh(), - firmware_coordinator.async_config_entry_first_refresh(), - return_exceptions=True, - ) - # If camera WAN blocked, firmware check fails, do not prevent setup - # so don't check firmware_coordinator exceptions - if isinstance(results[0], BaseException): + try: + await device_coordinator.async_config_entry_first_refresh() + except BaseException: await host.stop() - raise results[0] + raise hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData( host=host, @@ -159,7 +157,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.add_update_listener(entry_update_listener) ) - starting = False return True diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index bccb5c5b684..c9989f2c02b 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -79,6 +79,7 @@ class ReolinkHost: ) self.firmware_ch_list: list[int | None] = [] + self.starting: bool = True self.credential_errors: int = 0 self.webhook_id: str | None = None diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 3541aa1f856..105815bae1d 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -87,6 +87,7 @@ def reolink_connect_class() -> Generator[MagicMock]: host_mock.camera_name.return_value = TEST_NVR_NAME host_mock.camera_hardware_version.return_value = "IPC_00001" host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000" + host_mock.camera_sw_version_update_required.return_value = False host_mock.camera_uid.return_value = TEST_UID_CAM host_mock.channel_for_uid.return_value = 0 host_mock.get_encoding.return_value = "h264" diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index de1e7a0bc83..ba845dc1697 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -397,7 +397,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No None, None, TEST_HOST2, - [TEST_HOST, TEST_HOST2, TEST_HOST2], + [TEST_HOST, TEST_HOST2], ), ( True, @@ -475,8 +475,8 @@ async def test_dhcp_ip_update( const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data ) - expected_calls = [ - call( + for host in host_call_list: + expected_call = call( host, TEST_USERNAME, TEST_PASSWORD, @@ -485,10 +485,10 @@ async def test_dhcp_ip_update( protocol=DEFAULT_PROTOCOL, timeout=DEFAULT_TIMEOUT, ) - for host in host_call_list - ] + assert expected_call in reolink_connect_class.call_args_list - assert reolink_connect_class.call_args_list == expected_calls + for exc_call in reolink_connect_class.call_args_list: + assert exc_call[0][0] in host_call_list assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 922fe0829f6..a6c798f9415 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -1,5 +1,6 @@ """Test the Reolink init.""" +import asyncio from datetime import timedelta from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -39,6 +40,11 @@ from tests.common import MockConfigEntry, async_fire_time_changed pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") +async def test_wait(*args, **key_args): + """Ensure a mocked function takes a bit of time to be able to timeout in test.""" + await asyncio.sleep(0) + + @pytest.mark.parametrize( ("attr", "value", "expected"), [ @@ -377,9 +383,13 @@ async def test_no_repair_issue( async def test_https_repair_issue( - hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + issue_registry: ir.IssueRegistry, ) -> None: """Test repairs issue is raised when https local url is used.""" + reolink_connect.get_states = test_wait await async_process_ha_core_config( hass, {"country": "GB", "internal_url": "https://test_homeassistant_address"} ) @@ -400,9 +410,13 @@ async def test_https_repair_issue( async def test_ssl_repair_issue( - hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + issue_registry: ir.IssueRegistry, ) -> None: """Test repairs issue is raised when global ssl certificate is used.""" + reolink_connect.get_states = test_wait assert await async_setup_component(hass, "webhook", {}) hass.config.api.use_ssl = True @@ -446,9 +460,13 @@ async def test_port_repair_issue( async def test_webhook_repair_issue( - hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + issue_registry: ir.IssueRegistry, ) -> None: """Test repairs issue is raised when the webhook url is unreachable.""" + reolink_connect.get_states = test_wait with ( patch("homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0), patch( @@ -471,7 +489,7 @@ async def test_firmware_repair_issue( issue_registry: ir.IssueRegistry, ) -> None: """Test firmware issue is raised when too old firmware is used.""" - reolink_connect.sw_version_update_required = True + reolink_connect.camera_sw_version_update_required.return_value = True assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From c085057847f2c4b426ced9b6477aede99c64b5e7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 26 Jun 2024 09:40:29 +0200 Subject: [PATCH 0182/2411] Add timezone testing in holiday (#120497) --- tests/components/holiday/test_calendar.py | 56 +++++++++++++++++++---- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/tests/components/holiday/test_calendar.py b/tests/components/holiday/test_calendar.py index b5067a467ed..db58b7b1f73 100644 --- a/tests/components/holiday/test_calendar.py +++ b/tests/components/holiday/test_calendar.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from freezegun.api import FrozenDateTimeFactory +import pytest from homeassistant.components.calendar import ( DOMAIN as CALENDAR_DOMAIN, @@ -17,12 +18,18 @@ from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) async def test_holiday_calendar_entity( hass: HomeAssistant, freezer: FrozenDateTimeFactory, + time_zone: str, ) -> None: """Test HolidayCalendarEntity functionality.""" - freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) + freezer.move_to(datetime(2023, 1, 1, 0, 1, 1, tzinfo=zone)) # New Years Day config_entry = MockConfigEntry( domain=DOMAIN, @@ -64,8 +71,16 @@ async def test_holiday_calendar_entity( assert state is not None assert state.state == "on" + freezer.move_to( + datetime(2023, 1, 2, 0, 1, 1, tzinfo=zone) + ) # Day after New Years Day + + state = hass.states.get("calendar.united_states_ak") + assert state is not None + assert state.state == "on" + # Test holidays for the next year - freezer.move_to(datetime(2023, 12, 31, 12, tzinfo=dt_util.UTC)) + freezer.move_to(datetime(2023, 12, 31, 12, tzinfo=zone)) response = await hass.services.async_call( CALENDAR_DOMAIN, @@ -91,12 +106,18 @@ async def test_holiday_calendar_entity( } +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) async def test_default_language( hass: HomeAssistant, freezer: FrozenDateTimeFactory, + time_zone: str, ) -> None: """Test default language.""" - freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) + freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=zone)) config_entry = MockConfigEntry( domain=DOMAIN, @@ -162,12 +183,18 @@ async def test_default_language( } +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) async def test_no_language( hass: HomeAssistant, freezer: FrozenDateTimeFactory, + time_zone: str, ) -> None: """Test language defaults to English if language not exist.""" - freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) + freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=zone)) config_entry = MockConfigEntry( domain=DOMAIN, @@ -203,12 +230,18 @@ async def test_no_language( } +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) async def test_no_next_event( hass: HomeAssistant, freezer: FrozenDateTimeFactory, + time_zone: str, ) -> None: """Test if there is no next event.""" - freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) + freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=zone)) config_entry = MockConfigEntry( domain=DOMAIN, @@ -221,7 +254,7 @@ async def test_no_next_event( await hass.async_block_till_done() # Move time to out of reach - freezer.move_to(datetime(dt_util.now().year + 5, 1, 1, 12, tzinfo=dt_util.UTC)) + freezer.move_to(datetime(dt_util.now().year + 5, 1, 1, 12, tzinfo=zone)) async_fire_time_changed(hass) state = hass.states.get("calendar.germany") @@ -230,15 +263,22 @@ async def test_no_next_event( assert state.attributes == {"friendly_name": "Germany"} +@pytest.mark.parametrize( + "time_zone", ["Asia/Tokyo", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) async def test_language_not_exist( - hass: HomeAssistant, freezer: FrozenDateTimeFactory + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + time_zone: str, ) -> None: """Test when language doesn't exist it will fallback to country default language.""" + await hass.config.async_set_time_zone(time_zone) + zone = await dt_util.async_get_time_zone(time_zone) hass.config.language = "nb" # Norweigan language "Norks bokmål" hass.config.country = "NO" - freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=zone)) config_entry = MockConfigEntry( domain=DOMAIN, From 4a8669325453bb7402fba76e1f14a410a7be48c0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 09:41:16 +0200 Subject: [PATCH 0183/2411] Verify default timezone is restored when test ends (#116216) --- tests/conftest.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1d4699647c9..161ff458ac0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine from contextlib import asynccontextmanager, contextmanager +import datetime import functools import gc import itertools @@ -76,7 +77,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.translation import _TranslationsCacheData from homeassistant.helpers.typing import ConfigType from homeassistant.setup import BASE_PLATFORMS, async_setup_component -from homeassistant.util import location +from homeassistant.util import dt as dt_util, location from homeassistant.util.async_ import create_eager_task from homeassistant.util.json import json_loads @@ -385,6 +386,13 @@ def verify_cleanup( "waitpid-" ) + try: + # Verify the default time zone has been restored + assert dt_util.DEFAULT_TIME_ZONE is datetime.UTC + finally: + # Restore the default time zone to not break subsequent tests + dt_util.DEFAULT_TIME_ZONE = datetime.UTC + @pytest.fixture(autouse=True) def reset_hass_threading_local_object() -> Generator[None]: From 82b8b73b8acb7621123a5fc2b214e72577ad5244 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 09:46:50 +0200 Subject: [PATCH 0184/2411] Add reconfiguration flow to pyLoad integration (#120485) --- .../components/pyload/config_flow.py | 46 +++++++++ homeassistant/components/pyload/strings.json | 17 +++- tests/components/pyload/test_config_flow.py | 93 ++++++++++++++++++- 3 files changed, 154 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/pyload/config_flow.py b/homeassistant/components/pyload/config_flow.py index 4825e6fa7cf..2f4f9519d30 100644 --- a/homeassistant/components/pyload/config_flow.py +++ b/homeassistant/components/pyload/config_flow.py @@ -199,3 +199,49 @@ class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders={CONF_NAME: self.config_entry.data[CONF_USERNAME]}, errors=errors, ) + + async def async_step_reconfigure( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform a reconfiguration.""" + self.config_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reconfigure_confirm() + + async def async_step_reconfigure_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the reconfiguration flow.""" + errors = {} + + if TYPE_CHECKING: + assert self.config_entry + + if user_input is not None: + try: + await validate_input(self.hass, user_input) + except (CannotConnect, ParserError): + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_update_reload_and_abort( + self.config_entry, + data=user_input, + reload_even_if_entry_is_unchanged=False, + reason="reconfigure_successful", + ) + + return self.async_show_form( + step_id="reconfigure_confirm", + data_schema=self.add_suggested_values_to_schema( + STEP_USER_DATA_SCHEMA, + user_input or self.config_entry.data, + ), + description_placeholders={CONF_NAME: self.config_entry.data[CONF_USERNAME]}, + errors=errors, + ) diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 248cec99cc9..31e1443b321 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -15,6 +15,20 @@ "port": "pyLoad uses port 8000 by default." } }, + "reconfigure_confirm": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "ssl": "[%key:common::config_flow::data::ssl%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "host": "The hostname or IP address of the device running your pyLoad instance.", + "port": "pyLoad uses port 8000 by default." + } + }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "data": { @@ -30,7 +44,8 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, "entity": { diff --git a/tests/components/pyload/test_config_flow.py b/tests/components/pyload/test_config_flow.py index 63297de7127..8e9083a49c8 100644 --- a/tests/components/pyload/test_config_flow.py +++ b/tests/components/pyload/test_config_flow.py @@ -6,7 +6,12 @@ from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError import pytest from homeassistant.components.pyload.const import DEFAULT_NAME, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import ( + SOURCE_IMPORT, + SOURCE_REAUTH, + SOURCE_RECONFIGURE, + SOURCE_USER, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -250,3 +255,89 @@ async def test_reauth_errors( assert result["reason"] == "reauth_successful" assert config_entry.data == NEW_INPUT assert len(hass.config_entries.async_entries()) == 1 + + +async def test_reconfiguration( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, +) -> None: + """Test reauth flow.""" + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data == USER_INPUT + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("side_effect", "error_text"), + [ + (InvalidAuth, "invalid_auth"), + (CannotConnect, "cannot_connect"), + (IndexError, "unknown"), + ], +) +async def test_reconfigure_errors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + side_effect: Exception, + error_text: str, +) -> None: + """Test reauth flow.""" + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + mock_pyloadapi.login.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error_text} + + mock_pyloadapi.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data == USER_INPUT + assert len(hass.config_entries.async_entries()) == 1 From 59959141af32055367aaaee99c23396a21a225c0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 09:52:05 +0200 Subject: [PATCH 0185/2411] Remove Knocki triggers on runtime (#120452) * Bump Knocki to 0.2.0 * Remove triggers on runtime in Knocki * Fix --- homeassistant/components/knocki/__init__.py | 9 ++++- .../components/knocki/coordinator.py | 23 +++++++++++++ .../knocki/fixtures/more_triggers.json | 30 +++++++++++++++++ tests/components/knocki/test_event.py | 33 +++++++++++++++++-- 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 tests/components/knocki/fixtures/more_triggers.json diff --git a/homeassistant/components/knocki/__init__.py b/homeassistant/components/knocki/__init__.py index ddf389649f2..42c3956bd68 100644 --- a/homeassistant/components/knocki/__init__.py +++ b/homeassistant/components/knocki/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from knocki import EventType, KnockiClient +from knocki import Event, EventType, KnockiClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN, Platform @@ -30,6 +30,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bo client.register_listener(EventType.CREATED, coordinator.add_trigger) ) + async def _refresh_coordinator(_: Event) -> None: + await coordinator.async_refresh() + + entry.async_on_unload( + client.register_listener(EventType.DELETED, _refresh_coordinator) + ) + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/knocki/coordinator.py b/homeassistant/components/knocki/coordinator.py index 020b3921a1e..f70fbdf79a7 100644 --- a/homeassistant/components/knocki/coordinator.py +++ b/homeassistant/components/knocki/coordinator.py @@ -2,7 +2,9 @@ from knocki import Event, KnockiClient, KnockiConnectionError, Trigger +from homeassistant.components.event import DOMAIN as EVENT_DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, LOGGER @@ -19,12 +21,20 @@ class KnockiCoordinator(DataUpdateCoordinator[dict[int, Trigger]]): name=DOMAIN, ) self.client = client + self._known_triggers: set[tuple[str, int]] = set() async def _async_update_data(self) -> dict[int, Trigger]: try: triggers = await self.client.get_triggers() except KnockiConnectionError as exc: raise UpdateFailed from exc + current_triggers = { + (trigger.device_id, trigger.details.trigger_id) for trigger in triggers + } + removed_triggers = self._known_triggers - current_triggers + for trigger in removed_triggers: + await self._delete_device(trigger) + self._known_triggers = current_triggers return {trigger.details.trigger_id: trigger for trigger in triggers} def add_trigger(self, event: Event) -> None: @@ -32,3 +42,16 @@ class KnockiCoordinator(DataUpdateCoordinator[dict[int, Trigger]]): self.async_set_updated_data( {**self.data, event.payload.details.trigger_id: event.payload} ) + self._known_triggers.add( + (event.payload.device_id, event.payload.details.trigger_id) + ) + + async def _delete_device(self, trigger: tuple[str, int]) -> None: + """Delete a device from the coordinator.""" + device_id, trigger_id = trigger + entity_registry = er.async_get(self.hass) + entity_entry = entity_registry.async_get_entity_id( + EVENT_DOMAIN, DOMAIN, f"{device_id}_{trigger_id}" + ) + if entity_entry: + entity_registry.async_remove(entity_entry) diff --git a/tests/components/knocki/fixtures/more_triggers.json b/tests/components/knocki/fixtures/more_triggers.json new file mode 100644 index 00000000000..dbe4823e3d5 --- /dev/null +++ b/tests/components/knocki/fixtures/more_triggers.json @@ -0,0 +1,30 @@ +[ + { + "device": "KNC1-W-00000214", + "gesture": "d060b870-15ba-42c9-a932-2d2951087152", + "details": { + "description": "Eeee", + "name": "Aaaa", + "id": 31 + }, + "type": "homeassistant", + "user": "7a4d5bf9-01b1-413b-bb4d-77728e931dcc", + "updatedAt": 1716378013721, + "createdAt": 1716378013721, + "id": "1a050b25-7fed-4e0e-b5af-792b8b4650de" + }, + { + "device": "KNC1-W-00000214", + "gesture": "d060b870-15ba-42c9-a932-2d2951087152", + "details": { + "description": "Eeee", + "name": "Bbbb", + "id": 32 + }, + "type": "homeassistant", + "user": "7a4d5bf9-01b1-413b-bb4d-77728e931dcc", + "updatedAt": 1716378013721, + "createdAt": 1716378013721, + "id": "1a050b25-7fed-4e0e-b5af-792b8b4650de" + } +] diff --git a/tests/components/knocki/test_event.py b/tests/components/knocki/test_event.py index 4740ddc9167..4f639e08773 100644 --- a/tests/components/knocki/test_event.py +++ b/tests/components/knocki/test_event.py @@ -1,6 +1,6 @@ """Tests for the Knocki event platform.""" -from collections.abc import Callable +from collections.abc import Awaitable, Callable from unittest.mock import AsyncMock from knocki import Event, EventType, Trigger, TriggerDetails @@ -89,10 +89,39 @@ async def test_adding_runtime_entities( assert not hass.states.get("event.knc1_w_00000214_aaaa") add_trigger_function: Callable[[Event], None] = ( - mock_knocki_client.register_listener.call_args[0][1] + mock_knocki_client.register_listener.call_args_list[0][0][1] ) trigger = Trigger.from_dict(load_json_array_fixture("triggers.json", DOMAIN)[0]) add_trigger_function(Event(EventType.CREATED, trigger)) assert hass.states.get("event.knc1_w_00000214_aaaa") is not None + + +async def test_removing_runtime_entities( + hass: HomeAssistant, + mock_knocki_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we can create devices on runtime.""" + mock_knocki_client.get_triggers.return_value = [ + Trigger.from_dict(trigger) + for trigger in load_json_array_fixture("more_triggers.json", DOMAIN) + ] + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("event.knc1_w_00000214_aaaa") is not None + assert hass.states.get("event.knc1_w_00000214_bbbb") is not None + + remove_trigger_function: Callable[[Event], Awaitable[None]] = ( + mock_knocki_client.register_listener.call_args_list[1][0][1] + ) + trigger = Trigger.from_dict(load_json_array_fixture("triggers.json", DOMAIN)[0]) + + mock_knocki_client.get_triggers.return_value = [trigger] + + await remove_trigger_function(Event(EventType.DELETED, trigger)) + + assert hass.states.get("event.knc1_w_00000214_aaaa") is not None + assert hass.states.get("event.knc1_w_00000214_bbbb") is None From 4bfecea2f42bc9fc96e7b4f4e6980e992f3580db Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:02:24 +0200 Subject: [PATCH 0186/2411] Force alias when importing notify PLATFORM_SCHEMA (#120494) --- homeassistant/components/apprise/notify.py | 4 +-- .../components/cisco_webex_teams/notify.py | 4 +-- homeassistant/components/clickatell/notify.py | 7 ++++-- homeassistant/components/clicksend/notify.py | 25 ++++++++----------- .../components/clicksend_tts/notify.py | 7 ++++-- homeassistant/components/facebook/notify.py | 4 +-- homeassistant/components/file/notify.py | 4 +-- homeassistant/components/flock/notify.py | 9 +++++-- .../components/free_mobile/notify.py | 7 ++++-- homeassistant/components/group/notify.py | 4 +-- homeassistant/components/homematic/notify.py | 4 +-- homeassistant/components/html5/notify.py | 4 +-- .../components/joaoapps_join/notify.py | 4 +-- homeassistant/components/kodi/notify.py | 4 +-- homeassistant/components/lannouncer/notify.py | 4 +-- .../components/llamalab_automate/notify.py | 4 +-- homeassistant/components/mailgun/notify.py | 4 +-- homeassistant/components/mastodon/notify.py | 4 +-- homeassistant/components/matrix/notify.py | 6 +++-- .../components/message_bird/notify.py | 4 +-- homeassistant/components/msteams/notify.py | 4 +-- homeassistant/components/prowl/notify.py | 4 +-- homeassistant/components/pushsafer/notify.py | 6 +++-- homeassistant/components/rest/notify.py | 4 +-- homeassistant/components/rocketchat/notify.py | 4 +-- homeassistant/components/sendgrid/notify.py | 4 +-- .../components/signal_messenger/notify.py | 4 +-- homeassistant/components/sinch/notify.py | 4 +-- homeassistant/components/smtp/notify.py | 4 +-- .../components/synology_chat/notify.py | 4 +-- homeassistant/components/syslog/notify.py | 4 +-- homeassistant/components/telegram/notify.py | 6 +++-- .../components/twilio_call/notify.py | 4 +-- homeassistant/components/twilio_sms/notify.py | 4 +-- homeassistant/components/twitter/notify.py | 4 +-- homeassistant/components/xmpp/notify.py | 4 +-- pyproject.toml | 1 + 37 files changed, 102 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py index 57a7feb6e5c..eb4e21c127f 100644 --- a/homeassistant/components/apprise/notify.py +++ b/homeassistant/components/apprise/notify.py @@ -12,7 +12,7 @@ from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_URL @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) CONF_FILE = "config" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_URL): vol.All(cv.ensure_list, [str]), vol.Optional(CONF_FILE): cv.string, diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py index 30f56ac4712..b93ebb273dd 100644 --- a/homeassistant/components/cisco_webex_teams/notify.py +++ b/homeassistant/components/cisco_webex_teams/notify.py @@ -9,7 +9,7 @@ from webexteamssdk import ApiError, WebexTeamsAPI, exceptions from homeassistant.components.notify import ( ATTR_TITLE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_TOKEN @@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__) CONF_ROOM_ID = "room_id" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( {vol.Required(CONF_TOKEN): cv.string, vol.Required(CONF_ROOM_ID): cv.string} ) diff --git a/homeassistant/components/clickatell/notify.py b/homeassistant/components/clickatell/notify.py index 70170217af2..c8d96d48faf 100644 --- a/homeassistant/components/clickatell/notify.py +++ b/homeassistant/components/clickatell/notify.py @@ -9,7 +9,10 @@ from typing import Any import requests import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.components.notify import ( + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -21,7 +24,7 @@ DEFAULT_NAME = "clickatell" BASE_API_URL = "https://platform.clickatell.com/messages/http/send" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( {vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_RECIPIENT): cv.string} ) diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 44954211748..d00d7b413cc 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -10,7 +10,10 @@ from typing import Any import requests import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.components.notify import ( + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( CONF_API_KEY, CONF_RECIPIENT, @@ -31,19 +34,13 @@ TIMEOUT = 5 HEADERS = {"Content-Type": CONTENT_TYPE_JSON} -PLATFORM_SCHEMA = vol.Schema( - vol.All( - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_RECIPIENT, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_SENDER, default=DEFAULT_SENDER): cv.string, - } - ) - ) +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_RECIPIENT, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_SENDER, default=DEFAULT_SENDER): cv.string, + } ) diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index aeda1b26162..6b5f2040448 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -9,7 +9,10 @@ import logging import requests import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.components.notify import ( + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( CONF_API_KEY, CONF_NAME, @@ -38,7 +41,7 @@ DEFAULT_LANGUAGE = "en-us" DEFAULT_VOICE = FEMALE_VOICE TIMEOUT = 5 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/facebook/notify.py b/homeassistant/components/facebook/notify.py index 38ed78d125b..3319f6bdebd 100644 --- a/homeassistant/components/facebook/notify.py +++ b/homeassistant/components/facebook/notify.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONTENT_TYPE_JSON @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) CONF_PAGE_ACCESS_TOKEN = "page_access_token" BASE_URL = "https://graph.facebook.com/v2.6/me/messages" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( {vol.Required(CONF_PAGE_ACCESS_TOKEN): cv.string} ) diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index 244bd69aa32..1516efd6d96 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, NotifyEntity, NotifyEntityFeature, @@ -34,7 +34,7 @@ _LOGGER = logging.getLogger(__name__) # The legacy platform schema uses a filename, after import # The full file path is stored in the config entry -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FILENAME): cv.string, vol.Optional(CONF_TIMESTAMP, default=False): cv.boolean, diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index 61c9a29bd6c..811ee51749c 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -8,7 +8,10 @@ import logging import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.components.notify import ( + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -18,7 +21,9 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://api.flock.com/hooks/sendMessage/" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_ACCESS_TOKEN): cv.string}) +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_ACCESS_TOKEN): cv.string} +) async def async_get_service( diff --git a/homeassistant/components/free_mobile/notify.py b/homeassistant/components/free_mobile/notify.py index d888ceadb18..90c8ef3246e 100644 --- a/homeassistant/components/free_mobile/notify.py +++ b/homeassistant/components/free_mobile/notify.py @@ -8,7 +8,10 @@ import logging from freesms import FreeClient import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.components.notify import ( + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -16,7 +19,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_ACCESS_TOKEN): cv.string} ) diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index 425dcf5a914..444658a6112 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -13,7 +13,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ATTR_SERVICE @@ -23,7 +23,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType CONF_SERVICES = "services" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SERVICES): vol.All( cv.ensure_list, diff --git a/homeassistant/components/homematic/notify.py b/homeassistant/components/homematic/notify.py index 6b7e71bb7a9..ced8ea6a951 100644 --- a/homeassistant/components/homematic/notify.py +++ b/homeassistant/components/homematic/notify.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.core import HomeAssistant @@ -24,7 +24,7 @@ from .const import ( SERVICE_SET_DEVICE_VALUE, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), vol.Required(ATTR_CHANNEL): vol.Coerce(int), diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 6049f8e2434..cc03202ae88 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -26,7 +26,7 @@ from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ATTR_NAME, URL_ROOT @@ -61,7 +61,7 @@ def gcm_api_deprecated(value): return value -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Optional("gcm_sender_id"): vol.All(cv.string, gcm_api_deprecated), vol.Optional("gcm_api_key"): cv.string, diff --git a/homeassistant/components/joaoapps_join/notify.py b/homeassistant/components/joaoapps_join/notify.py index 6e9efc4da21..7fab894b0e4 100644 --- a/homeassistant/components/joaoapps_join/notify.py +++ b/homeassistant/components/joaoapps_join/notify.py @@ -11,7 +11,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY, CONF_DEVICE_ID @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) CONF_DEVICE_IDS = "device_ids" CONF_DEVICE_NAMES = "device_names" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_DEVICE_ID): cv.string, diff --git a/homeassistant/components/kodi/notify.py b/homeassistant/components/kodi/notify.py index 05b5ff56be4..c811a073cbb 100644 --- a/homeassistant/components/kodi/notify.py +++ b/homeassistant/components/kodi/notify.py @@ -12,7 +12,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ( @@ -34,7 +34,7 @@ DEFAULT_PORT = 8080 DEFAULT_PROXY_SSL = False DEFAULT_TIMEOUT = 5 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/lannouncer/notify.py b/homeassistant/components/lannouncer/notify.py index 525372710af..6c3cd1922cf 100644 --- a/homeassistant/components/lannouncer/notify.py +++ b/homeassistant/components/lannouncer/notify.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_HOST, CONF_PORT @@ -24,7 +24,7 @@ ATTR_METHOD_ALLOWED = ["speak", "alarm"] DEFAULT_PORT = 1035 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/llamalab_automate/notify.py b/homeassistant/components/llamalab_automate/notify.py index 6ce00db71c3..da13267aec3 100644 --- a/homeassistant/components/llamalab_automate/notify.py +++ b/homeassistant/components/llamalab_automate/notify.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY, CONF_DEVICE @@ -25,7 +25,7 @@ ATTR_PRIORITY = "priority" CONF_TO = "to" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_TO): cv.string, diff --git a/homeassistant/components/mailgun/notify.py b/homeassistant/components/mailgun/notify.py index 39aea79d15e..26ff13f2a6f 100644 --- a/homeassistant/components/mailgun/notify.py +++ b/homeassistant/components/mailgun/notify.py @@ -16,7 +16,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY, CONF_DOMAIN, CONF_RECIPIENT, CONF_SENDER @@ -32,7 +32,7 @@ ATTR_IMAGES = "images" DEFAULT_SANDBOX = False -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( {vol.Required(CONF_RECIPIENT): vol.Email(), vol.Optional(CONF_SENDER): vol.Email()} ) diff --git a/homeassistant/components/mastodon/notify.py b/homeassistant/components/mastodon/notify.py index 1ab47896b0d..f15b8c6f0ab 100644 --- a/homeassistant/components/mastodon/notify.py +++ b/homeassistant/components/mastodon/notify.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET @@ -26,7 +26,7 @@ ATTR_TARGET = "target" ATTR_MEDIA_WARNING = "media_warning" ATTR_CONTENT_WARNING = "content_warning" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Required(CONF_CLIENT_ID): cv.string, diff --git a/homeassistant/components/matrix/notify.py b/homeassistant/components/matrix/notify.py index 0c8430afacd..b05c7952d1f 100644 --- a/homeassistant/components/matrix/notify.py +++ b/homeassistant/components/matrix/notify.py @@ -10,7 +10,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.core import HomeAssistant @@ -22,7 +22,9 @@ from .const import DOMAIN, SERVICE_SEND_MESSAGE CONF_DEFAULT_ROOM = "default_room" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_DEFAULT_ROOM): cv.string}) +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_DEFAULT_ROOM): cv.string} +) def get_service( diff --git a/homeassistant/components/message_bird/notify.py b/homeassistant/components/message_bird/notify.py index b1b7b373e6a..6da0e8176ef 100644 --- a/homeassistant/components/message_bird/notify.py +++ b/homeassistant/components/message_bird/notify.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TARGET, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY, CONF_SENDER @@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_SENDER, default="HA"): vol.All( diff --git a/homeassistant/components/msteams/notify.py b/homeassistant/components/msteams/notify.py index d1118ed7ab5..a4de5d126d5 100644 --- a/homeassistant/components/msteams/notify.py +++ b/homeassistant/components/msteams/notify.py @@ -11,7 +11,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_URL @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_FILE_URL = "image_url" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_URL): cv.url}) +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend({vol.Required(CONF_URL): cv.url}) def get_service( diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index b5556c15c6c..1118e747275 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -12,7 +12,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY @@ -24,7 +24,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://api.prowlapp.com/publicapi/" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) async def async_get_service( diff --git a/homeassistant/components/pushsafer/notify.py b/homeassistant/components/pushsafer/notify.py index f4f5bf88a22..b5c517c8662 100644 --- a/homeassistant/components/pushsafer/notify.py +++ b/homeassistant/components/pushsafer/notify.py @@ -16,7 +16,7 @@ from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ATTR_ICON @@ -54,7 +54,9 @@ ATTR_PICTURE1_USERNAME = "username" ATTR_PICTURE1_PASSWORD = "password" ATTR_PICTURE1_AUTH = "auth" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_DEVICE_KEY): cv.string}) +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_DEVICE_KEY): cv.string} +) def get_service( diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index 7744154c1c5..c8314d18707 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -14,7 +14,7 @@ from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ( @@ -45,7 +45,7 @@ DEFAULT_MESSAGE_PARAM_NAME = "message" DEFAULT_METHOD = "GET" DEFAULT_VERIFY_SSL = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.url, vol.Optional( diff --git a/homeassistant/components/rocketchat/notify.py b/homeassistant/components/rocketchat/notify.py index 9b7b40873ce..e39fb2dc0a1 100644 --- a/homeassistant/components/rocketchat/notify.py +++ b/homeassistant/components/rocketchat/notify.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_PASSWORD, CONF_ROOM, CONF_URL, CONF_USERNAME @@ -24,7 +24,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_URL): vol.Url(), vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/sendgrid/notify.py b/homeassistant/components/sendgrid/notify.py index 01ceccf781a..86f01804574 100644 --- a/homeassistant/components/sendgrid/notify.py +++ b/homeassistant/components/sendgrid/notify.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ( @@ -30,7 +30,7 @@ CONF_SENDER_NAME = "sender_name" DEFAULT_SENDER_NAME = "Home Assistant" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_SENDER): vol.Email(), diff --git a/homeassistant/components/signal_messenger/notify.py b/homeassistant/components/signal_messenger/notify.py index b93e5bb43e2..21d42f8912f 100644 --- a/homeassistant/components/signal_messenger/notify.py +++ b/homeassistant/components/signal_messenger/notify.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.core import HomeAssistant @@ -57,7 +57,7 @@ DATA_SCHEMA = vol.Any( DATA_URLS_SCHEMA, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SENDER_NR): cv.string, vol.Required(CONF_SIGNAL_CLI_REST_API): cv.string, diff --git a/homeassistant/components/sinch/notify.py b/homeassistant/components/sinch/notify.py index 77443dd1a84..16780a05704 100644 --- a/homeassistant/components/sinch/notify.py +++ b/homeassistant/components/sinch/notify.py @@ -18,7 +18,7 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY, CONF_SENDER @@ -37,7 +37,7 @@ DEFAULT_SENDER = "Home Assistant" _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_SERVICE_PLAN_ID): cv.string, diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index bac18576f06..5d19a705d87 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -19,7 +19,7 @@ from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ( @@ -60,7 +60,7 @@ PLATFORMS = [Platform.NOTIFY] _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [vol.Email()]), vol.Required(CONF_SENDER): vol.Email(), diff --git a/homeassistant/components/synology_chat/notify.py b/homeassistant/components/synology_chat/notify.py index a36f073b8bb..38c302b7968 100644 --- a/homeassistant/components/synology_chat/notify.py +++ b/homeassistant/components/synology_chat/notify.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL @@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType ATTR_FILE_URL = "file_url" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, diff --git a/homeassistant/components/syslog/notify.py b/homeassistant/components/syslog/notify.py index b16d44fb504..dbbada65fb2 100644 --- a/homeassistant/components/syslog/notify.py +++ b/homeassistant/components/syslog/notify.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.core import HomeAssistant @@ -59,7 +59,7 @@ SYSLOG_PRIORITY = { -2: "LOG_DEBUG", } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_FACILITY, default="syslog"): vol.In(SYSLOG_FACILITY.keys()), vol.Optional(CONF_OPTION, default="pid"): vol.In(SYSLOG_OPTION.keys()), diff --git a/homeassistant/components/telegram/notify.py b/homeassistant/components/telegram/notify.py index 16952868525..adb947bcf6b 100644 --- a/homeassistant/components/telegram/notify.py +++ b/homeassistant/components/telegram/notify.py @@ -11,7 +11,7 @@ from homeassistant.components.notify import ( ATTR_MESSAGE, ATTR_TARGET, ATTR_TITLE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.components.telegram_bot import ( @@ -40,7 +40,9 @@ ATTR_DOCUMENT = "document" CONF_CHAT_ID = "chat_id" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_CHAT_ID): vol.Coerce(int)}) +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_CHAT_ID): vol.Coerce(int)} +) def get_service( diff --git a/homeassistant/components/twilio_call/notify.py b/homeassistant/components/twilio_call/notify.py index d3d128ccd25..5338bb59a79 100644 --- a/homeassistant/components/twilio_call/notify.py +++ b/homeassistant/components/twilio_call/notify.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TARGET, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.components.twilio import DATA_TWILIO @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) CONF_FROM_NUMBER = "from_number" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FROM_NUMBER): vol.All( cv.string, vol.Match(r"^\+?[1-9]\d{1,14}$") diff --git a/homeassistant/components/twilio_sms/notify.py b/homeassistant/components/twilio_sms/notify.py index 2c04594f314..d1e2ca2888f 100644 --- a/homeassistant/components/twilio_sms/notify.py +++ b/homeassistant/components/twilio_sms/notify.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.components.twilio import DATA_TWILIO @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) CONF_FROM_NUMBER = "from_number" ATTR_MEDIAURL = "media_url" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FROM_NUMBER): vol.All( cv.string, diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index 718f4f7dbcf..66b076126b5 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -16,7 +16,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME @@ -33,7 +33,7 @@ CONF_ACCESS_TOKEN_SECRET = "access_token_secret" ATTR_MEDIA = "media" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Required(CONF_ACCESS_TOKEN_SECRET): cv.string, diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 4da1bf35d1a..824f996c675 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -24,7 +24,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ( @@ -56,7 +56,7 @@ DEFAULT_CONTENT_TYPE = "application/octet-stream" DEFAULT_RESOURCE = "home-assistant" XEP_0363_TIMEOUT = 10 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SENDER): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/pyproject.toml b/pyproject.toml index 6ecbb8b51d1..db6c5f0c989 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -811,6 +811,7 @@ ignore = [ [tool.ruff.lint.flake8-import-conventions.extend-aliases] voluptuous = "vol" +"homeassistant.components.notify.PLATFORM_SCHEMA" = "NOTIFY_PLATFORM_SCHEMA" "homeassistant.helpers.area_registry" = "ar" "homeassistant.helpers.category_registry" = "cr" "homeassistant.helpers.config_validation" = "cv" From caa57c56f64bcda54b4c2d101c5a2b3cb57bcf7a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:03:29 +0200 Subject: [PATCH 0187/2411] Force alias when importing air_quality PLATFORM_SCHEMA (#120502) --- homeassistant/components/ampio/air_quality.py | 4 ++-- homeassistant/components/nilu/air_quality.py | 7 +++++-- homeassistant/components/norway_air/air_quality.py | 7 +++++-- homeassistant/components/opensensemap/air_quality.py | 7 +++++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ampio/air_quality.py b/homeassistant/components/ampio/air_quality.py index ce7bff10aa8..05581df6371 100644 --- a/homeassistant/components/ampio/air_quality.py +++ b/homeassistant/components/ampio/air_quality.py @@ -9,7 +9,7 @@ from asmog import AmpioSmog import voluptuous as vol from homeassistant.components.air_quality import ( - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as AIR_QUALITY_PLATFORM_SCHEMA, AirQualityEntity, ) from homeassistant.const import CONF_NAME @@ -24,7 +24,7 @@ from .const import CONF_STATION_ID, SCAN_INTERVAL _LOGGER: Final = logging.getLogger(__name__) -PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = AIR_QUALITY_PLATFORM_SCHEMA.extend( {vol.Required(CONF_STATION_ID): cv.string, vol.Optional(CONF_NAME): cv.string} ) diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index 7b1068771d2..7600a878548 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -23,7 +23,10 @@ from niluclient import ( ) import voluptuous as vol -from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity +from homeassistant.components.air_quality import ( + PLATFORM_SCHEMA as AIR_QUALITY_PLATFORM_SCHEMA, + AirQualityEntity, +) from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -86,7 +89,7 @@ CONF_ALLOWED_AREAS = [ "Ålesund", ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = AIR_QUALITY_PLATFORM_SCHEMA.extend( { vol.Inclusive( CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index c16df860751..bba4737550b 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -8,7 +8,10 @@ import logging import metno import voluptuous as vol -from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity +from homeassistant.components.air_quality import ( + PLATFORM_SCHEMA as AIR_QUALITY_PLATFORM_SCHEMA, + AirQualityEntity, +) from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -26,7 +29,7 @@ DEFAULT_NAME = "Air quality Norway" OVERRIDE_URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/airqualityforecast/0.1/" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = AIR_QUALITY_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_FORECAST, default=DEFAULT_FORECAST): vol.Coerce(int), vol.Optional(CONF_LATITUDE): cv.latitude, diff --git a/homeassistant/components/opensensemap/air_quality.py b/homeassistant/components/opensensemap/air_quality.py index c9b4c726a59..eb8435751c0 100644 --- a/homeassistant/components/opensensemap/air_quality.py +++ b/homeassistant/components/opensensemap/air_quality.py @@ -9,7 +9,10 @@ from opensensemap_api import OpenSenseMap from opensensemap_api.exceptions import OpenSenseMapError import voluptuous as vol -from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity +from homeassistant.components.air_quality import ( + PLATFORM_SCHEMA as AIR_QUALITY_PLATFORM_SCHEMA, + AirQualityEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -26,7 +29,7 @@ CONF_STATION_ID = "station_id" SCAN_INTERVAL = timedelta(minutes=10) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = AIR_QUALITY_PLATFORM_SCHEMA.extend( {vol.Required(CONF_STATION_ID): cv.string, vol.Optional(CONF_NAME): cv.string} ) From d76a82e34085b23a4583749accb2f1fa08759714 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 10:21:54 +0200 Subject: [PATCH 0188/2411] Add switch platform to pyload integration (#120352) --- homeassistant/components/pyload/__init__.py | 2 +- homeassistant/components/pyload/icons.json | 16 ++ homeassistant/components/pyload/strings.json | 8 + homeassistant/components/pyload/switch.py | 122 +++++++++++++++ .../pyload/snapshots/test_switch.ambr | 142 ++++++++++++++++++ tests/components/pyload/test_switch.py | 105 +++++++++++++ 6 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/pyload/switch.py create mode 100644 tests/components/pyload/snapshots/test_switch.ambr create mode 100644 tests/components/pyload/test_switch.py diff --git a/homeassistant/components/pyload/__init__.py b/homeassistant/components/pyload/__init__.py index 8bf065797e5..0a89fbb6140 100644 --- a/homeassistant/components/pyload/__init__.py +++ b/homeassistant/components/pyload/__init__.py @@ -22,7 +22,7 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession from .coordinator import PyLoadCoordinator -PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] type PyLoadConfigEntry = ConfigEntry[PyLoadCoordinator] diff --git a/homeassistant/components/pyload/icons.json b/homeassistant/components/pyload/icons.json index 8f6f016641f..0e307a43e51 100644 --- a/homeassistant/components/pyload/icons.json +++ b/homeassistant/components/pyload/icons.json @@ -27,6 +27,22 @@ "total": { "default": "mdi:cloud-alert" } + }, + "switch": { + "download": { + "default": "mdi:play", + "state": { + "on": "mdi:play", + "off": "mdi:pause" + } + }, + "reconnect": { + "default": "mdi:restart", + "state": { + "on": "mdi:restart", + "off": "mdi:restart-off" + } + } } } } diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 31e1443b321..0ed016aafb8 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -79,6 +79,14 @@ "free_space": { "name": "Free space" } + }, + "switch": { + "download": { + "name": "Pause/Resume queue" + }, + "reconnect": { + "name": "Auto-Reconnect" + } } }, "issues": { diff --git a/homeassistant/components/pyload/switch.py b/homeassistant/components/pyload/switch.py new file mode 100644 index 00000000000..b9391ef818f --- /dev/null +++ b/homeassistant/components/pyload/switch.py @@ -0,0 +1,122 @@ +"""Support for monitoring pyLoad.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from enum import StrEnum +from typing import Any + +from pyloadapi.api import PyLoadAPI + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import PyLoadConfigEntry +from .const import DOMAIN, MANUFACTURER, SERVICE_NAME +from .coordinator import PyLoadCoordinator + + +class PyLoadSwitchEntity(StrEnum): + """PyLoad Switch Entities.""" + + PAUSE_RESUME_QUEUE = "download" + RECONNECT = "reconnect" + + +@dataclass(kw_only=True, frozen=True) +class PyLoadSwitchEntityDescription(SwitchEntityDescription): + """Describes pyLoad switch entity.""" + + turn_on_fn: Callable[[PyLoadAPI], Awaitable[Any]] + turn_off_fn: Callable[[PyLoadAPI], Awaitable[Any]] + toggle_fn: Callable[[PyLoadAPI], Awaitable[Any]] + + +SENSOR_DESCRIPTIONS: tuple[PyLoadSwitchEntityDescription, ...] = ( + PyLoadSwitchEntityDescription( + key=PyLoadSwitchEntity.PAUSE_RESUME_QUEUE, + translation_key=PyLoadSwitchEntity.PAUSE_RESUME_QUEUE, + device_class=SwitchDeviceClass.SWITCH, + turn_on_fn=lambda api: api.unpause(), + turn_off_fn=lambda api: api.pause(), + toggle_fn=lambda api: api.toggle_pause(), + ), + PyLoadSwitchEntityDescription( + key=PyLoadSwitchEntity.RECONNECT, + translation_key=PyLoadSwitchEntity.RECONNECT, + device_class=SwitchDeviceClass.SWITCH, + turn_on_fn=lambda api: api.toggle_reconnect(), + turn_off_fn=lambda api: api.toggle_reconnect(), + toggle_fn=lambda api: api.toggle_reconnect(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PyLoadConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the pyLoad sensors.""" + + coordinator = entry.runtime_data + + async_add_entities( + PyLoadBinarySensor(coordinator, description) + for description in SENSOR_DESCRIPTIONS + ) + + +class PyLoadBinarySensor(CoordinatorEntity[PyLoadCoordinator], SwitchEntity): + """Representation of a pyLoad sensor.""" + + _attr_has_entity_name = True + entity_description: PyLoadSwitchEntityDescription + + def __init__( + self, + coordinator: PyLoadCoordinator, + entity_description: PyLoadSwitchEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._attr_unique_id = ( + f"{coordinator.config_entry.entry_id}_{entity_description.key}" + ) + self.entity_description = entity_description + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + manufacturer=MANUFACTURER, + model=SERVICE_NAME, + configuration_url=coordinator.pyload.api_url, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + sw_version=coordinator.version, + ) + + @property + def is_on(self) -> bool | None: + """Return the state of the device.""" + return getattr(self.coordinator.data, self.entity_description.key) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.entity_description.turn_on_fn(self.coordinator.pyload) + await self.coordinator.async_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.entity_description.turn_off_fn(self.coordinator.pyload) + await self.coordinator.async_refresh() + + async def async_toggle(self, **kwargs: Any) -> None: + """Toggle the entity.""" + await self.entity_description.toggle_fn(self.coordinator.pyload) + await self.coordinator.async_refresh() diff --git a/tests/components/pyload/snapshots/test_switch.ambr b/tests/components/pyload/snapshots/test_switch.ambr new file mode 100644 index 00000000000..94f2910cad8 --- /dev/null +++ b/tests/components/pyload/snapshots/test_switch.ambr @@ -0,0 +1,142 @@ +# serializer version: 1 +# name: test_state[switch.pyload_auto_reconnect-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.pyload_auto_reconnect', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Auto-Reconnect', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_reconnect', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[switch.pyload_auto_reconnect-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'pyLoad Auto-Reconnect', + }), + 'context': , + 'entity_id': 'switch.pyload_auto_reconnect', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_state[switch.pyload_pause_resume_queue-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.pyload_pause_resume_queue', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pause/Resume queue', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_download', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[switch.pyload_pause_resume_queue-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'pyLoad Pause/Resume queue', + }), + 'context': , + 'entity_id': 'switch.pyload_pause_resume_queue', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_state[switch.pyload_reconnect-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.pyload_reconnect', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Reconnect', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_reconnect', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[switch.pyload_reconnect-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'pyLoad Reconnect', + }), + 'context': , + 'entity_id': 'switch.pyload_reconnect', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/pyload/test_switch.py b/tests/components/pyload/test_switch.py new file mode 100644 index 00000000000..e7bd5a24a87 --- /dev/null +++ b/tests/components/pyload/test_switch.py @@ -0,0 +1,105 @@ +"""Tests for the pyLoad Switches.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, call, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.pyload.switch import PyLoadSwitchEntity +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + +# Maps entity to the mock calls to assert +API_CALL = { + PyLoadSwitchEntity.PAUSE_RESUME_QUEUE: { + SERVICE_TURN_ON: call.unpause, + SERVICE_TURN_OFF: call.pause, + SERVICE_TOGGLE: call.toggle_pause, + }, + PyLoadSwitchEntity.RECONNECT: { + SERVICE_TURN_ON: call.toggle_reconnect, + SERVICE_TURN_OFF: call.toggle_reconnect, + SERVICE_TOGGLE: call.toggle_reconnect, + }, +} + + +@pytest.fixture(autouse=True) +async def switch_only() -> AsyncGenerator[None, None]: + """Enable only the switch platform.""" + with patch( + "homeassistant.components.pyload.PLATFORMS", + [Platform.SWITCH], + ): + yield + + +async def test_state( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_pyloadapi: AsyncMock, +) -> None: + """Test switch state.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize( + ("service_call"), + [ + SERVICE_TURN_ON, + SERVICE_TURN_OFF, + SERVICE_TOGGLE, + ], +) +async def test_turn_on_off( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + service_call: str, + entity_registry: er.EntityRegistry, +) -> None: + """Test switch turn on/off, toggle method.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + entity_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + + for entity_entry in entity_entries: + await hass.services.async_call( + SWITCH_DOMAIN, + service_call, + {ATTR_ENTITY_ID: entity_entry.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + API_CALL[entity_entry.translation_key][service_call] + in mock_pyloadapi.method_calls + ) + mock_pyloadapi.reset_mock() From 7b298f177c5728339417520f7695b1e73aec8bab Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:23:37 +0200 Subject: [PATCH 0189/2411] Force alias when importing tts PLATFORM_SCHEMA (#120500) --- homeassistant/components/amazon_polly/tts.py | 4 ++-- homeassistant/components/baidu/tts.py | 8 ++++++-- homeassistant/components/demo/tts.py | 4 ++-- homeassistant/components/google_cloud/tts.py | 8 ++++++-- homeassistant/components/google_translate/tts.py | 4 ++-- homeassistant/components/marytts/tts.py | 8 ++++++-- homeassistant/components/microsoft/tts.py | 8 ++++++-- homeassistant/components/picotts/tts.py | 8 ++++++-- homeassistant/components/voicerss/tts.py | 8 ++++++-- homeassistant/components/watson_tts/tts.py | 7 +++++-- homeassistant/components/yandextts/tts.py | 8 ++++++-- 11 files changed, 53 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index bde690a3163..d5cb7092fe3 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -10,7 +10,7 @@ import botocore import voluptuous as vol from homeassistant.components.tts import ( - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, Provider, TtsAudioType, ) @@ -49,7 +49,7 @@ from .const import ( _LOGGER: Final = logging.getLogger(__name__) -PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = TTS_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(SUPPORTED_REGIONS), vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string, diff --git a/homeassistant/components/baidu/tts.py b/homeassistant/components/baidu/tts.py index 859e79adec9..cdb6697d143 100644 --- a/homeassistant/components/baidu/tts.py +++ b/homeassistant/components/baidu/tts.py @@ -5,7 +5,11 @@ import logging from aip import AipSpeech import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv @@ -22,7 +26,7 @@ CONF_PITCH = "pitch" CONF_VOLUME = "volume" CONF_PERSON = "person" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORTED_LANGUAGES), vol.Required(CONF_APP_ID): cv.string, diff --git a/homeassistant/components/demo/tts.py b/homeassistant/components/demo/tts.py index c2fa367da29..1d28d1358e1 100644 --- a/homeassistant/components/demo/tts.py +++ b/homeassistant/components/demo/tts.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.tts import ( CONF_LANG, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, Provider, TtsAudioType, ) @@ -20,7 +20,7 @@ SUPPORT_LANGUAGES = ["en", "de"] DEFAULT_LANG = "en" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES)} ) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index c5eeaa7d924..1002e594a87 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -7,7 +7,11 @@ import os from google.cloud import texttospeech import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -136,7 +140,7 @@ GAIN_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_GAIN, max=MAX_GAIN)) PROFILES_SCHEMA = vol.All(cv.ensure_list, [vol.In(SUPPORTED_PROFILES)]) TEXT_TYPE_SCHEMA = vol.All(vol.Lower, vol.In(SUPPORTED_TEXT_TYPES)) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_KEY_FILE): cv.string, vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORTED_LANGUAGES), diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index c34713caef7..221c99e7c20 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.tts import ( CONF_LANG, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, Provider, TextToSpeechEntity, TtsAudioType, @@ -35,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_OPTIONS = ["tld"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), vol.Optional(CONF_TLD, default=DEFAULT_TLD): vol.In(SUPPORT_TLD), diff --git a/homeassistant/components/marytts/tts.py b/homeassistant/components/marytts/tts.py index 168d735a987..89832c01937 100644 --- a/homeassistant/components/marytts/tts.py +++ b/homeassistant/components/marytts/tts.py @@ -5,7 +5,11 @@ from __future__ import annotations from speak2mary import MaryTTS import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) from homeassistant.const import CONF_EFFECT, CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -26,7 +30,7 @@ DEFAULT_EFFECTS: dict[str, str] = {} MAP_MARYTTS_CODEC = {"WAVE_FILE": "wav", "AIFF_FILE": "aiff", "AU_FILE": "au"} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index ea95771429f..aa33072089f 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -6,7 +6,11 @@ from pycsspeechtts import pycsspeechtts from requests.exceptions import HTTPError import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) from homeassistant.const import CONF_API_KEY, CONF_REGION, CONF_TYPE, PERCENTAGE from homeassistant.generated.microsoft_tts import SUPPORTED_LANGUAGES import homeassistant.helpers.config_validation as cv @@ -31,7 +35,7 @@ DEFAULT_PITCH = "default" DEFAULT_CONTOUR = "" DEFAULT_REGION = "eastus" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORTED_LANGUAGES), diff --git a/homeassistant/components/picotts/tts.py b/homeassistant/components/picotts/tts.py index 8ba17fdac17..44d33145b3d 100644 --- a/homeassistant/components/picotts/tts.py +++ b/homeassistant/components/picotts/tts.py @@ -8,7 +8,11 @@ import tempfile import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) _LOGGER = logging.getLogger(__name__) @@ -16,7 +20,7 @@ SUPPORT_LANGUAGES = ["en-US", "en-GB", "de-DE", "es-ES", "fr-FR", "it-IT"] DEFAULT_LANG = "en-US" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES)} ) diff --git a/homeassistant/components/voicerss/tts.py b/homeassistant/components/voicerss/tts.py index 84bbcc19409..9f1615ffa01 100644 --- a/homeassistant/components/voicerss/tts.py +++ b/homeassistant/components/voicerss/tts.py @@ -7,7 +7,11 @@ import logging import aiohttp import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) from homeassistant.const import CONF_API_KEY from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -145,7 +149,7 @@ DEFAULT_CODEC = "mp3" DEFAULT_FORMAT = "8khz_8bit_mono" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 3cf1582e008..373d17438c9 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -6,7 +6,10 @@ from ibm_cloud_sdk_core.authenticators import IAMAuthenticator from ibm_watson import TextToSpeechV1 import voluptuous as vol -from homeassistant.components.tts import PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -114,7 +117,7 @@ CONTENT_TYPE_EXTENSIONS = { DEFAULT_VOICE = "en-US_AllisonV3Voice" DEFAULT_OUTPUT_FORMAT = "audio/mp3" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_URL, default=DEFAULT_URL): cv.string, vol.Required(CONF_APIKEY): cv.string, diff --git a/homeassistant/components/yandextts/tts.py b/homeassistant/components/yandextts/tts.py index 1a5fc4a7903..850afd05150 100644 --- a/homeassistant/components/yandextts/tts.py +++ b/homeassistant/components/yandextts/tts.py @@ -7,7 +7,11 @@ import logging import aiohttp import voluptuous as vol -from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider +from homeassistant.components.tts import ( + CONF_LANG, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, + Provider, +) from homeassistant.const import CONF_API_KEY from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -64,7 +68,7 @@ DEFAULT_VOICE = "zahar" DEFAULT_EMOTION = "neutral" DEFAULT_SPEED = 1 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), From 0a48cc29b6dda74122958880e896a035f5de7d0d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:24:29 +0200 Subject: [PATCH 0190/2411] Implement @plugwise_command for Plugwise Number platform (#120503) --- homeassistant/components/plugwise/number.py | 20 ++++---------------- tests/components/plugwise/test_number.py | 6 +++--- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 1f12b2374b3..06db5faa55b 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -2,11 +2,8 @@ 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, @@ -21,13 +18,13 @@ from . import PlugwiseConfigEntry from .const import NumberType from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity +from .util import plugwise_command @dataclass(frozen=True, kw_only=True) class PlugwiseNumberEntityDescription(NumberEntityDescription): """Class describing Plugwise Number entities.""" - command: Callable[[Smile, str, str, float], Awaitable[None]] key: NumberType @@ -35,9 +32,6 @@ NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="maximum_boiler_temperature", translation_key="maximum_boiler_temperature", - command=lambda api, dev_id, number, value: api.set_number( - dev_id, number, value - ), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -45,9 +39,6 @@ NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="max_dhw_temperature", translation_key="max_dhw_temperature", - command=lambda api, dev_id, number, value: api.set_number( - dev_id, number, value - ), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -55,9 +46,6 @@ NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="temperature_offset", translation_key="temperature_offset", - command=lambda api, dev_id, number, value: api.set_temperature_offset( - dev_id, value - ), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -120,9 +108,9 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): """Return the present setpoint value.""" return self.device[self.entity_description.key]["setpoint"] + @plugwise_command async def async_set_native_value(self, value: float) -> None: """Change to the new setpoint value.""" - await self.entity_description.command( - self.coordinator.api, self.device_id, self.entity_description.key, value + await self.coordinator.api.set_number( + self.device_id, self.entity_description.key, value ) - await self.coordinator.async_request_refresh() diff --git a/tests/components/plugwise/test_number.py b/tests/components/plugwise/test_number.py index 8d49d07b9fb..e10a7caa9e9 100644 --- a/tests/components/plugwise/test_number.py +++ b/tests/components/plugwise/test_number.py @@ -97,7 +97,7 @@ async def test_adam_temperature_offset_change( blocking=True, ) - assert mock_smile_adam.set_temperature_offset.call_count == 1 - mock_smile_adam.set_temperature_offset.assert_called_with( - "6a3bf693d05e48e0b460c815a4fdd09d", 1.0 + assert mock_smile_adam.set_number.call_count == 1 + mock_smile_adam.set_number.assert_called_with( + "6a3bf693d05e48e0b460c815a4fdd09d", "temperature_offset", 1.0 ) From a4ba346dfc1308f8ab961ed151a7865958e945f4 Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:28:06 +0200 Subject: [PATCH 0191/2411] Switch onkyo to pyeiscp, making it local_push (#120026) * Switch onkyo to pyeiscp, making it local_push Major rewrite of the integration to use pyeiscp. This facilitates use of the async push updates. Streamline the code dealing with zones. Handle sound mode. Add myself to codeowners. * Add types * Add more types * Address feedback * Remove sound mode support for now * Fix zone detection * Keep legacy unique_id --- CODEOWNERS | 1 + homeassistant/components/onkyo/manifest.json | 8 +- .../components/onkyo/media_player.py | 760 ++++++++---------- homeassistant/generated/integrations.json | 2 +- requirements_all.txt | 6 +- 5 files changed, 364 insertions(+), 413 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b4ff315872d..973780b811c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -997,6 +997,7 @@ build.json @home-assistant/supervisor /tests/components/ondilo_ico/ @JeromeHXP /homeassistant/components/onewire/ @garbled1 @epenet /tests/components/onewire/ @garbled1 @epenet +/homeassistant/components/onkyo/ @arturpragacz /homeassistant/components/onvif/ @hunterjm /tests/components/onvif/ @hunterjm /homeassistant/components/open_meteo/ @frenck diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json index 72ef2f14711..072dc9f9e3b 100644 --- a/homeassistant/components/onkyo/manifest.json +++ b/homeassistant/components/onkyo/manifest.json @@ -1,9 +1,9 @@ { "domain": "onkyo", "name": "Onkyo", - "codeowners": [], + "codeowners": ["@arturpragacz"], "documentation": "https://www.home-assistant.io/integrations/onkyo", - "iot_class": "local_polling", - "loggers": ["eiscp"], - "requirements": ["onkyo-eiscp==1.2.7"] + "iot_class": "local_push", + "loggers": ["pyeiscp"], + "requirements": ["pyeiscp==0.0.7"] } diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 97e0b3e3631..181a8117443 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -2,11 +2,11 @@ from __future__ import annotations +import asyncio import logging from typing import Any -import eiscp -from eiscp import eISCP +import pyeiscp import voluptuous as vol from homeassistant.components.media_player import ( @@ -17,9 +17,14 @@ from homeassistant.components.media_player import ( MediaPlayerState, MediaType, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME -from homeassistant.core import HomeAssistant, ServiceCall -import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_NAME, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -32,13 +37,12 @@ CONF_RECEIVER_MAX_VOLUME = "receiver_max_volume" DEFAULT_NAME = "Onkyo Receiver" SUPPORTED_MAX_VOLUME = 100 DEFAULT_RECEIVER_MAX_VOLUME = 80 - +ZONES = {"zone2": "Zone 2", "zone3": "Zone 3", "zone4": "Zone 4"} SUPPORT_ONKYO_WO_VOLUME = ( MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.SELECT_SOURCE - | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PLAY_MEDIA ) SUPPORT_ONKYO = ( @@ -49,6 +53,7 @@ SUPPORT_ONKYO = ( ) KNOWN_HOSTS: list[str] = [] + DEFAULT_SOURCES = { "tv": "TV", "bd": "Bluray", @@ -63,7 +68,6 @@ DEFAULT_SOURCES = { "video7": "Video 7", "fm": "Radio", } - DEFAULT_PLAYABLE_SOURCES = ("fm", "am", "tuner") PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -80,15 +84,39 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -TIMEOUT_MESSAGE = "Timeout waiting for response." - - ATTR_HDMI_OUTPUT = "hdmi_output" ATTR_PRESET = "preset" ATTR_AUDIO_INFORMATION = "audio_information" ATTR_VIDEO_INFORMATION = "video_information" ATTR_VIDEO_OUT = "video_out" +AUDIO_VIDEO_INFORMATION_UPDATE_WAIT_TIME = 8 + +AUDIO_INFORMATION_MAPPING = [ + "audio_input_port", + "input_signal_format", + "input_frequency", + "input_channels", + "listening_mode", + "output_channels", + "output_frequency", + "precision_quartz_lock_system", + "auto_phase_control_delay", + "auto_phase_control_phase", +] + +VIDEO_INFORMATION_MAPPING = [ + "video_input_port", + "input_resolution", + "input_color_schema", + "input_color_depth", + "video_output_port", + "output_resolution", + "output_color_schema", + "output_color_depth", + "picture_mode", +] + ACCEPTED_VALUES = [ "no", "analog", @@ -106,415 +134,187 @@ ONKYO_SELECT_OUTPUT_SCHEMA = vol.Schema( vol.Required(ATTR_HDMI_OUTPUT): vol.In(ACCEPTED_VALUES), } ) - SERVICE_SELECT_HDMI_OUTPUT = "onkyo_select_hdmi_output" -def _parse_onkyo_payload(payload): - """Parse a payload returned from the eiscp library.""" - if isinstance(payload, bool): - # command not supported by the device - return False - - if len(payload) < 2: - # no value - return None - - if isinstance(payload[1], str): - return payload[1].split(",") - - return payload[1] - - -def _tuple_get(tup, index, default=None): - """Return a tuple item at index or a default value if it doesn't exist.""" - return (tup[index : index + 1] or [default])[0] - - -def determine_zones(receiver): - """Determine what zones are available for the receiver.""" - out = {"zone2": False, "zone3": False} - try: - _LOGGER.debug("Checking for zone 2 capability") - response = receiver.raw("ZPWQSTN") - if response != "ZPWN/A": # Zone 2 Available - out["zone2"] = True - else: - _LOGGER.debug("Zone 2 not available") - except ValueError as error: - if str(error) != TIMEOUT_MESSAGE: - raise - _LOGGER.debug("Zone 2 timed out, assuming no functionality") - try: - _LOGGER.debug("Checking for zone 3 capability") - response = receiver.raw("PW3QSTN") - if response != "PW3N/A": - out["zone3"] = True - else: - _LOGGER.debug("Zone 3 not available") - except ValueError as error: - if str(error) != TIMEOUT_MESSAGE: - raise - _LOGGER.debug("Zone 3 timed out, assuming no functionality") - except AssertionError: - _LOGGER.error("Zone 3 detection failed") - - return out - - -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 Onkyo platform.""" - hosts: list[OnkyoDevice] = [] + receivers: dict[str, pyeiscp.Connection] = {} # indexed by host + entities: dict[str, dict[str, OnkyoMediaPlayer]] = {} # indexed by host and zone - def service_handle(service: ServiceCall) -> None: + async def async_service_handle(service: ServiceCall) -> None: """Handle for services.""" entity_ids = service.data[ATTR_ENTITY_ID] - devices = [d for d in hosts if d.entity_id in entity_ids] + targets = [ + entity + for h in entities.values() + for entity in h.values() + if entity.entity_id in entity_ids + ] - for device in devices: + for target in targets: if service.service == SERVICE_SELECT_HDMI_OUTPUT: - device.select_output(service.data[ATTR_HDMI_OUTPUT]) + await target.async_select_output(service.data[ATTR_HDMI_OUTPUT]) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_SELECT_HDMI_OUTPUT, - service_handle, + async_service_handle, schema=ONKYO_SELECT_OUTPUT_SCHEMA, ) - if CONF_HOST in config and (host := config[CONF_HOST]) not in KNOWN_HOSTS: - try: - receiver = eiscp.eISCP(host) - hosts.append( - OnkyoDevice( - receiver, - config.get(CONF_SOURCES), - name=config.get(CONF_NAME), - max_volume=config.get(CONF_MAX_VOLUME), - receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), - ) + host = config.get(CONF_HOST) + name = config[CONF_NAME] + max_volume = config[CONF_MAX_VOLUME] + receiver_max_volume = config[CONF_RECEIVER_MAX_VOLUME] + sources = config[CONF_SOURCES] + + @callback + def async_onkyo_update_callback(message: tuple[str, str, Any], origin: str) -> None: + """Process new message from receiver.""" + receiver = receivers[origin] + _LOGGER.debug("Received update callback from %s: %s", receiver.name, message) + + zone, _, value = message + entity = entities[origin].get(zone) + if entity is not None: + if entity.enabled: + entity.process_update(message) + elif zone in ZONES and value != "N/A": + # When we receive the status for a zone, and the value is not "N/A", + # then zone is available on the receiver, so we create the entity for it. + _LOGGER.debug("Discovered %s on %s", ZONES[zone], receiver.name) + zone_entity = OnkyoMediaPlayer( + receiver, sources, zone, max_volume, receiver_max_volume ) - KNOWN_HOSTS.append(host) + entities[origin][zone] = zone_entity + async_add_entities([zone_entity]) - zones = determine_zones(receiver) + @callback + def async_onkyo_connect_callback(origin: str) -> None: + """Receiver (re)connected.""" + receiver = receivers[origin] + _LOGGER.debug("Receiver (re)connected: %s (%s)", receiver.name, receiver.host) - # Add Zone2 if available - if zones["zone2"]: - _LOGGER.debug("Setting up zone 2") - hosts.append( - OnkyoDeviceZone( - "2", - receiver, - config.get(CONF_SOURCES), - name=f"{config[CONF_NAME]} Zone 2", - max_volume=config.get(CONF_MAX_VOLUME), - receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), - ) - ) - # Add Zone3 if available - if zones["zone3"]: - _LOGGER.debug("Setting up zone 3") - hosts.append( - OnkyoDeviceZone( - "3", - receiver, - config.get(CONF_SOURCES), - name=f"{config[CONF_NAME]} Zone 3", - max_volume=config.get(CONF_MAX_VOLUME), - receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), - ) - ) - except OSError: - _LOGGER.error("Unable to connect to receiver at %s", host) + for entity in entities[origin].values(): + entity.backfill_state() + + def setup_receiver(receiver: pyeiscp.Connection) -> None: + KNOWN_HOSTS.append(receiver.host) + + # Store the receiver object and create a dictionary to store its entities. + receivers[receiver.host] = receiver + entities[receiver.host] = {} + + # Discover what zones are available for the receiver by querying the power. + # If we get a response for the specific zone, it means it is available. + for zone in ZONES: + receiver.query_property(zone, "power") + + # Add the main zone to entities, since it is always active. + _LOGGER.debug("Adding Main Zone on %s", receiver.name) + main_entity = OnkyoMediaPlayer( + receiver, sources, "main", max_volume, receiver_max_volume + ) + entities[receiver.host]["main"] = main_entity + async_add_entities([main_entity]) + + if host is not None and host not in KNOWN_HOSTS: + _LOGGER.debug("Manually creating receiver: %s (%s)", name, host) + receiver = await pyeiscp.Connection.create( + host=host, + update_callback=async_onkyo_update_callback, + connect_callback=async_onkyo_connect_callback, + ) + + # The library automatically adds a name and identifier only on discovered hosts, + # so manually add them here instead. + receiver.name = name + receiver.identifier = None + + setup_receiver(receiver) else: - for receiver in eISCP.discover(): + + @callback + async def async_onkyo_discovery_callback(receiver: pyeiscp.Connection): + """Receiver discovered, connection not yet active.""" + _LOGGER.debug("Receiver discovered: %s (%s)", receiver.name, receiver.host) if receiver.host not in KNOWN_HOSTS: - hosts.append(OnkyoDevice(receiver, config.get(CONF_SOURCES))) - KNOWN_HOSTS.append(receiver.host) - add_entities(hosts, True) + await receiver.connect() + setup_receiver(receiver) + + _LOGGER.debug("Discovering receivers") + await pyeiscp.Connection.discover( + update_callback=async_onkyo_update_callback, + connect_callback=async_onkyo_connect_callback, + discovery_callback=async_onkyo_discovery_callback, + ) + + @callback + def close_receiver(_event): + for receiver in receivers.values(): + receiver.close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_receiver) -class OnkyoDevice(MediaPlayerEntity): - """Representation of an Onkyo device.""" +class OnkyoMediaPlayer(MediaPlayerEntity): + """Representation of an Onkyo Receiver Media Player (one per each zone).""" - _attr_supported_features = SUPPORT_ONKYO + _attr_should_poll = False + + _supports_volume: bool = False + _supports_audio_info: bool = False + _supports_video_info: bool = False + _query_timer: asyncio.TimerHandle | None = None def __init__( self, - receiver, - sources, - name=None, - max_volume=SUPPORTED_MAX_VOLUME, - receiver_max_volume=DEFAULT_RECEIVER_MAX_VOLUME, - ): + receiver: pyeiscp.Connection, + sources: dict[str, str], + zone: str, + max_volume: int, + receiver_max_volume: int, + ) -> None: """Initialize the Onkyo Receiver.""" self._receiver = receiver - self._attr_is_volume_muted = False - self._attr_volume_level = 0 - self._attr_state = MediaPlayerState.OFF - if name: - # not discovered - self._attr_name = name - else: + name = receiver.name + self._attr_name = f"{name}{' ' + ZONES[zone] if zone != 'main' else ''}" + identifier = receiver.identifier + if identifier is not None: # discovered - self._attr_unique_id = ( - f"{receiver.info['model_name']}_{receiver.info['identifier']}" - ) - self._attr_name = self._attr_unique_id + if zone == "main": + # keep legacy unique_id + self._attr_unique_id = f"{name}_{identifier}" + else: + self._attr_unique_id = f"{identifier}_{zone}" + else: + # not discovered + self._attr_unique_id = None - self._max_volume = max_volume - self._receiver_max_volume = receiver_max_volume - self._attr_source_list = list(sources.values()) + self._zone = zone self._source_mapping = sources self._reverse_mapping = {value: key for key, value in sources.items()} + self._max_volume = max_volume + self._receiver_max_volume = receiver_max_volume + + self._attr_source_list = list(sources.values()) self._attr_extra_state_attributes = {} - self._hdmi_out_supported = True - self._audio_info_supported = True - self._video_info_supported = True - def command(self, command): - """Run an eiscp command and catch connection errors.""" - try: - result = self._receiver.command(command) - except (ValueError, OSError, AttributeError, AssertionError): - if self._receiver.command_socket: - self._receiver.command_socket = None - _LOGGER.debug("Resetting connection to %s", self.name) - else: - _LOGGER.info("%s is disconnected. Attempting to reconnect", self.name) - return False - _LOGGER.debug("Result for %s: %s", command, result) - return result + async def async_added_to_hass(self) -> None: + """Entity has been added to hass.""" + self.backfill_state() - def update(self) -> None: - """Get the latest state from the device.""" - status = self.command("system-power query") - - if not status: - return - if status[1] == "on": - self._attr_state = MediaPlayerState.ON - else: - self._attr_state = MediaPlayerState.OFF - self._attr_extra_state_attributes.pop(ATTR_AUDIO_INFORMATION, None) - self._attr_extra_state_attributes.pop(ATTR_VIDEO_INFORMATION, None) - self._attr_extra_state_attributes.pop(ATTR_PRESET, None) - self._attr_extra_state_attributes.pop(ATTR_VIDEO_OUT, None) - return - - volume_raw = self.command("volume query") - mute_raw = self.command("audio-muting query") - current_source_raw = self.command("input-selector query") - # If the following command is sent to a device with only one HDMI out, - # the display shows 'Not Available'. - # We avoid this by checking if HDMI out is supported - if self._hdmi_out_supported: - hdmi_out_raw = self.command("hdmi-output-selector query") - else: - hdmi_out_raw = [] - preset_raw = self.command("preset query") - if self._audio_info_supported: - audio_information_raw = self.command("audio-information query") - self._parse_audio_information(audio_information_raw) - if self._video_info_supported: - video_information_raw = self.command("video-information query") - self._parse_video_information(video_information_raw) - if not (volume_raw and mute_raw and current_source_raw): - return - - sources = _parse_onkyo_payload(current_source_raw) - - for source in sources: - if source in self._source_mapping: - self._attr_source = self._source_mapping[source] - break - self._attr_source = "_".join(sources) - - if preset_raw and self.source and self.source.lower() == "radio": - self._attr_extra_state_attributes[ATTR_PRESET] = preset_raw[1] - elif ATTR_PRESET in self._attr_extra_state_attributes: - del self._attr_extra_state_attributes[ATTR_PRESET] - - self._attr_is_volume_muted = bool(mute_raw[1] == "on") - # AMP_VOL / (MAX_RECEIVER_VOL * (MAX_VOL / 100)) - self._attr_volume_level = volume_raw[1] / ( - self._receiver_max_volume * self._max_volume / 100 - ) - - if not hdmi_out_raw: - return - self._attr_extra_state_attributes[ATTR_VIDEO_OUT] = ",".join(hdmi_out_raw[1]) - if hdmi_out_raw[1] == "N/A": - self._hdmi_out_supported = False - - def turn_off(self) -> None: - """Turn the media player off.""" - self.command("system-power standby") - - def set_volume_level(self, volume: float) -> None: - """Set volume level, input is range 0..1. - - However full volume on the amp is usually far too loud so allow the user to - specify the upper range with CONF_MAX_VOLUME. We change as per max_volume - set by user. This means that if max volume is 80 then full volume in HA will - give 80% volume on the receiver. Then we convert that to the correct scale - for the receiver. - """ - # HA_VOL * (MAX VOL / 100) * MAX_RECEIVER_VOL - self.command( - "volume" - f" {int(volume * (self._max_volume / 100) * self._receiver_max_volume)}" - ) - - def volume_up(self) -> None: - """Increase volume by 1 step.""" - self.command("volume level-up") - - def volume_down(self) -> None: - """Decrease volume by 1 step.""" - self.command("volume level-down") - - def mute_volume(self, mute: bool) -> None: - """Mute (true) or unmute (false) media player.""" - if mute: - self.command("audio-muting on") - else: - self.command("audio-muting off") - - def turn_on(self) -> None: - """Turn the media player on.""" - self.command("system-power on") - - def select_source(self, source: str) -> None: - """Set the input source.""" - if self.source_list and source in self.source_list: - source = self._reverse_mapping[source] - self.command(f"input-selector {source}") - - def play_media( - self, media_type: MediaType | str, media_id: str, **kwargs: Any - ) -> None: - """Play radio station by preset number.""" - source = self._reverse_mapping[self._attr_source] - if media_type.lower() == "radio" and source in DEFAULT_PLAYABLE_SOURCES: - self.command(f"preset {media_id}") - - def select_output(self, output): - """Set hdmi-out.""" - self.command(f"hdmi-output-selector={output}") - - def _parse_audio_information(self, audio_information_raw): - values = _parse_onkyo_payload(audio_information_raw) - if values is False: - self._audio_info_supported = False - return - - if values: - info = { - "format": _tuple_get(values, 1), - "input_frequency": _tuple_get(values, 2), - "input_channels": _tuple_get(values, 3), - "listening_mode": _tuple_get(values, 4), - "output_channels": _tuple_get(values, 5), - "output_frequency": _tuple_get(values, 6), - } - self._attr_extra_state_attributes[ATTR_AUDIO_INFORMATION] = info - else: - self._attr_extra_state_attributes.pop(ATTR_AUDIO_INFORMATION, None) - - def _parse_video_information(self, video_information_raw): - values = _parse_onkyo_payload(video_information_raw) - if values is False: - self._video_info_supported = False - return - - if values: - info = { - "input_resolution": _tuple_get(values, 1), - "input_color_schema": _tuple_get(values, 2), - "input_color_depth": _tuple_get(values, 3), - "output_resolution": _tuple_get(values, 5), - "output_color_schema": _tuple_get(values, 6), - "output_color_depth": _tuple_get(values, 7), - "picture_mode": _tuple_get(values, 8), - "dynamic_range": _tuple_get(values, 9), - } - self._attr_extra_state_attributes[ATTR_VIDEO_INFORMATION] = info - else: - self._attr_extra_state_attributes.pop(ATTR_VIDEO_INFORMATION, None) - - -class OnkyoDeviceZone(OnkyoDevice): - """Representation of an Onkyo device's extra zone.""" - - def __init__( - self, - zone, - receiver, - sources, - name=None, - max_volume=SUPPORTED_MAX_VOLUME, - receiver_max_volume=DEFAULT_RECEIVER_MAX_VOLUME, - ): - """Initialize the Zone with the zone identifier.""" - self._zone = zone - self._supports_volume = True - super().__init__(receiver, sources, name, max_volume, receiver_max_volume) - - def update(self) -> None: - """Get the latest state from the device.""" - status = self.command(f"zone{self._zone}.power=query") - - if not status: - return - if status[1] == "on": - self._attr_state = MediaPlayerState.ON - else: - self._attr_state = MediaPlayerState.OFF - return - - volume_raw = self.command(f"zone{self._zone}.volume=query") - mute_raw = self.command(f"zone{self._zone}.muting=query") - current_source_raw = self.command(f"zone{self._zone}.selector=query") - preset_raw = self.command(f"zone{self._zone}.preset=query") - # If we received a source value, but not a volume value - # it's likely this zone permanently does not support volume. - if current_source_raw and not volume_raw: - self._supports_volume = False - - if not (volume_raw and mute_raw and current_source_raw): - return - - # It's possible for some players to have zones set to HDMI with - # no sound control. In this case, the string `N/A` is returned. - self._supports_volume = isinstance(volume_raw[1], (float, int)) - - # eiscp can return string or tuple. Make everything tuples. - if isinstance(current_source_raw[1], str): - current_source_tuples = (current_source_raw[0], (current_source_raw[1],)) - else: - current_source_tuples = current_source_raw - - for source in current_source_tuples[1]: - if source in self._source_mapping: - self._attr_source = self._source_mapping[source] - break - self._attr_source = "_".join(current_source_tuples[1]) - self._attr_is_volume_muted = bool(mute_raw[1] == "on") - if preset_raw and self.source and self.source.lower() == "radio": - self._attr_extra_state_attributes[ATTR_PRESET] = preset_raw[1] - elif ATTR_PRESET in self._attr_extra_state_attributes: - del self._attr_extra_state_attributes[ATTR_PRESET] - if self._supports_volume: - # AMP_VOL / (MAX_RECEIVER_VOL * (MAX_VOL / 100)) - self._attr_volume_level = volume_raw[1] / ( - self._receiver_max_volume * self._max_volume / 100 - ) + async def async_will_remove_from_hass(self) -> None: + """Cancel the query timer when the entity is removed.""" + if self._query_timer: + self._query_timer.cancel() + self._query_timer = None @property def supported_features(self) -> MediaPlayerEntityFeature: @@ -523,12 +323,26 @@ class OnkyoDeviceZone(OnkyoDevice): return SUPPORT_ONKYO return SUPPORT_ONKYO_WO_VOLUME - def turn_off(self) -> None: - """Turn the media player off.""" - self.command(f"zone{self._zone}.power=standby") + @callback + def _update_receiver(self, propname: str, value: Any) -> None: + """Update a property in the receiver.""" + self._receiver.update_property(self._zone, propname, value) - def set_volume_level(self, volume: float) -> None: - """Set volume level, input is range 0..1. + @callback + def _query_receiver(self, propname: str) -> None: + """Cause the receiver to send an update about a property.""" + self._receiver.query_property(self._zone, propname) + + async def async_turn_on(self) -> None: + """Turn the media player on.""" + self._update_receiver("power", "on") + + async def async_turn_off(self) -> None: + """Turn the media player off.""" + self._update_receiver("power", "standby") + + async def async_set_volume_level(self, volume: float) -> None: + """Set volume level, range 0..1. However full volume on the amp is usually far too loud so allow the user to specify the upper range with CONF_MAX_VOLUME. We change as per max_volume @@ -537,31 +351,167 @@ class OnkyoDeviceZone(OnkyoDevice): scale for the receiver. """ # HA_VOL * (MAX VOL / 100) * MAX_RECEIVER_VOL - self.command( - f"zone{self._zone}.volume={int(volume * (self._max_volume / 100) * self._receiver_max_volume)}" + self._update_receiver( + "volume", int(volume * (self._max_volume / 100) * self._receiver_max_volume) ) - def volume_up(self) -> None: + async def async_volume_up(self) -> None: """Increase volume by 1 step.""" - self.command(f"zone{self._zone}.volume=level-up") + self._update_receiver("volume", "level-up") - def volume_down(self) -> None: + async def async_volume_down(self) -> None: """Decrease volume by 1 step.""" - self.command(f"zone{self._zone}.volume=level-down") + self._update_receiver("volume", "level-down") - def mute_volume(self, mute: bool) -> None: - """Mute (true) or unmute (false) media player.""" - if mute: - self.command(f"zone{self._zone}.muting=on") - else: - self.command(f"zone{self._zone}.muting=off") + async def async_mute_volume(self, mute: bool) -> None: + """Mute the volume.""" + self._update_receiver( + "audio-muting" if self._zone == "main" else "muting", + "on" if mute else "off", + ) - def turn_on(self) -> None: - """Turn the media player on.""" - self.command(f"zone{self._zone}.power=on") - - def select_source(self, source: str) -> None: - """Set the input source.""" + async def async_select_source(self, source: str) -> None: + """Select input source.""" if self.source_list and source in self.source_list: source = self._reverse_mapping[source] - self.command(f"zone{self._zone}.selector={source}") + self._update_receiver( + "input-selector" if self._zone == "main" else "selector", source + ) + + async def async_select_output(self, hdmi_output: str) -> None: + """Set hdmi-out.""" + self._update_receiver("hdmi-output-selector", hdmi_output) + + async def async_play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: + """Play radio station by preset number.""" + if self.source is not None: + source = self._reverse_mapping[self.source] + if media_type.lower() == "radio" and source in DEFAULT_PLAYABLE_SOURCES: + self._update_receiver("preset", media_id) + + @callback + def backfill_state(self) -> None: + """Get the receiver to send all the info we care about. + + Usually run only on connect, as we can otherwise rely on the + receiver to keep us informed of changes. + """ + self._query_receiver("power") + self._query_receiver("volume") + self._query_receiver("preset") + if self._zone == "main": + self._query_receiver("hdmi-output-selector") + self._query_receiver("audio-muting") + self._query_receiver("input-selector") + self._query_receiver("listening-mode") + self._query_receiver("audio-information") + self._query_receiver("video-information") + else: + self._query_receiver("muting") + self._query_receiver("selector") + + @callback + def process_update(self, update: tuple[str, str, Any]) -> None: + """Store relevant updates so they can be queried later.""" + zone, command, value = update + if zone != self._zone: + return + + if command in ["system-power", "power"]: + if value == "on": + self._attr_state = MediaPlayerState.ON + else: + self._attr_state = MediaPlayerState.OFF + self._attr_extra_state_attributes.pop(ATTR_AUDIO_INFORMATION, None) + self._attr_extra_state_attributes.pop(ATTR_VIDEO_INFORMATION, None) + self._attr_extra_state_attributes.pop(ATTR_PRESET, None) + self._attr_extra_state_attributes.pop(ATTR_VIDEO_OUT, None) + elif command in ["volume", "master-volume"] and value != "N/A": + self._supports_volume = True + # AMP_VOL / (MAX_RECEIVER_VOL * (MAX_VOL / 100)) + self._attr_volume_level = value / ( + self._receiver_max_volume * self._max_volume / 100 + ) + elif command in ["muting", "audio-muting"]: + self._attr_is_volume_muted = bool(value == "on") + elif command in ["selector", "input-selector"]: + self._parse_source(value) + self._query_av_info_delayed() + elif command == "hdmi-output-selector": + self._attr_extra_state_attributes[ATTR_VIDEO_OUT] = ",".join(value) + elif command == "preset": + if self.source is not None and self.source.lower() == "radio": + self._attr_extra_state_attributes[ATTR_PRESET] = value + elif ATTR_PRESET in self._attr_extra_state_attributes: + del self._attr_extra_state_attributes[ATTR_PRESET] + elif command == "audio-information": + self._supports_audio_info = True + self._parse_audio_information(value) + elif command == "video-information": + self._supports_video_info = True + self._parse_video_information(value) + elif command == "fl-display-information": + self._query_av_info_delayed() + + self.async_write_ha_state() + + @callback + def _parse_source(self, source): + # source is either a tuple of values or a single value, + # so we convert to a tuple, when it is a single value. + if not isinstance(source, tuple): + source = (source,) + for value in source: + if value in self._source_mapping: + self._attr_source = self._source_mapping[value] + break + self._attr_source = "_".join(source) + + @callback + def _parse_audio_information(self, audio_information): + # If audio information is not available, N/A is returned, + # so only update the audio information, when it is not N/A. + if audio_information == "N/A": + self._attr_extra_state_attributes.pop(ATTR_AUDIO_INFORMATION, None) + return + + self._attr_extra_state_attributes[ATTR_AUDIO_INFORMATION] = { + name: value + for name, value in zip( + AUDIO_INFORMATION_MAPPING, audio_information, strict=False + ) + if len(value) > 0 + } + + @callback + def _parse_video_information(self, video_information): + # If video information is not available, N/A is returned, + # so only update the video information, when it is not N/A. + if video_information == "N/A": + self._attr_extra_state_attributes.pop(ATTR_VIDEO_INFORMATION, None) + return + + self._attr_extra_state_attributes[ATTR_VIDEO_INFORMATION] = { + name: value + for name, value in zip( + VIDEO_INFORMATION_MAPPING, video_information, strict=False + ) + if len(value) > 0 + } + + def _query_av_info_delayed(self): + if self._zone == "main" and not self._query_timer: + + @callback + def _query_av_info(): + if self._supports_audio_info: + self._query_receiver("audio-information") + if self._supports_video_info: + self._query_receiver("video-information") + self._query_timer = None + + self._query_timer = self.hass.loop.call_later( + AUDIO_VIDEO_INFORMATION_UPDATE_WAIT_TIME, _query_av_info + ) diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index d3380fdd17f..e98df79d096 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4243,7 +4243,7 @@ "name": "Onkyo", "integration_type": "hub", "config_flow": false, - "iot_class": "local_polling" + "iot_class": "local_push" }, "onvif": { "name": "ONVIF", diff --git a/requirements_all.txt b/requirements_all.txt index 3548be4ad60..5967b3f2a94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1470,9 +1470,6 @@ omnilogic==0.4.5 # homeassistant.components.ondilo_ico ondilo==0.5.0 -# homeassistant.components.onkyo -onkyo-eiscp==1.2.7 - # homeassistant.components.onvif onvif-zeep-async==3.1.12 @@ -1826,6 +1823,9 @@ pyefergy==22.5.0 # homeassistant.components.energenie_power_sockets pyegps==0.2.5 +# homeassistant.components.onkyo +pyeiscp==0.0.7 + # homeassistant.components.emoncms pyemoncms==0.0.7 From 9b8922a6787c68dff094b67f55bd806e75731794 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:36:54 +0200 Subject: [PATCH 0192/2411] Force alias when importing switch PLATFORM_SCHEMA (#120504) --- homeassistant/components/acer_projector/switch.py | 7 +++++-- homeassistant/components/ads/switch.py | 7 +++++-- homeassistant/components/anel_pwrctrl/switch.py | 7 +++++-- homeassistant/components/aqualogic/switch.py | 7 +++++-- homeassistant/components/arest/switch.py | 7 +++++-- homeassistant/components/aten_pe/switch.py | 4 ++-- homeassistant/components/digital_ocean/switch.py | 7 +++++-- homeassistant/components/edimax/switch.py | 7 +++++-- homeassistant/components/enocean/switch.py | 7 +++++-- homeassistant/components/gc100/switch.py | 7 +++++-- homeassistant/components/group/switch.py | 8 ++++++-- homeassistant/components/hikvisioncam/switch.py | 7 +++++-- homeassistant/components/kankun/switch.py | 7 +++++-- homeassistant/components/linode/switch.py | 7 +++++-- homeassistant/components/mfi/switch.py | 7 +++++-- homeassistant/components/netio/switch.py | 7 +++++-- homeassistant/components/orvibo/switch.py | 7 +++++-- homeassistant/components/pencom/switch.py | 7 +++++-- homeassistant/components/pilight/switch.py | 7 +++++-- homeassistant/components/pulseaudio_loopback/switch.py | 7 +++++-- homeassistant/components/raincloud/switch.py | 7 +++++-- homeassistant/components/raspyrfm/switch.py | 7 +++++-- homeassistant/components/recswitch/switch.py | 7 +++++-- homeassistant/components/remote_rpi_gpio/switch.py | 7 +++++-- homeassistant/components/rest/switch.py | 4 ++-- homeassistant/components/rflink/switch.py | 7 +++++-- homeassistant/components/scsgate/switch.py | 7 +++++-- homeassistant/components/snmp/switch.py | 7 +++++-- homeassistant/components/sony_projector/switch.py | 7 +++++-- homeassistant/components/switchmate/switch.py | 7 +++++-- homeassistant/components/telnet/switch.py | 4 ++-- homeassistant/components/template/switch.py | 4 ++-- homeassistant/components/thinkingcleaner/switch.py | 4 ++-- homeassistant/components/vultr/switch.py | 7 +++++-- homeassistant/components/wake_on_lan/switch.py | 4 ++-- homeassistant/components/wirelesstag/switch.py | 4 ++-- homeassistant/components/zoneminder/switch.py | 7 +++++-- 37 files changed, 165 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index b29bbf9fa3f..5c1c37df5d8 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -9,7 +9,10 @@ from typing import Any import serial import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_FILENAME, CONF_NAME, @@ -38,7 +41,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FILENAME): cv.isdevice, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/ads/switch.py b/homeassistant/components/ads/switch.py index a793a5996cf..803b95a7d8a 100644 --- a/homeassistant/components/ads/switch.py +++ b/homeassistant/components/ads/switch.py @@ -7,7 +7,10 @@ from typing import Any import pyads import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -18,7 +21,7 @@ from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity DEFAULT_NAME = "ADS Switch" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ADS_VAR): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/anel_pwrctrl/switch.py b/homeassistant/components/anel_pwrctrl/switch.py index 94cd0a59398..6b27a61e065 100644 --- a/homeassistant/components/anel_pwrctrl/switch.py +++ b/homeassistant/components/anel_pwrctrl/switch.py @@ -9,7 +9,10 @@ from typing import Any from anel_pwrctrl import Device, DeviceMaster, Switch import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -24,7 +27,7 @@ CONF_PORT_SEND = "port_send" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PORT_RECV): cv.port, vol.Required(CONF_PORT_SEND): cv.port, diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index 0f1a7e34b3c..ed0cc463263 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -7,7 +7,10 @@ from typing import Any from aqualogic.core import States import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -30,7 +33,7 @@ SWITCH_TYPES = { "aux_7": "Aux 7", } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SWITCH_TYPES)): vol.All( cv.ensure_list, [vol.In(SWITCH_TYPES)] diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index 4b15e6726fe..bcdba36cb58 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -9,7 +9,10 @@ from typing import Any import requests import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_NAME, CONF_RESOURCE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -31,7 +34,7 @@ PIN_FUNCTION_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py index 1349014d8fb..39b18089284 100644 --- a/homeassistant/components/aten_pe/switch.py +++ b/homeassistant/components/aten_pe/switch.py @@ -9,7 +9,7 @@ from atenpdu import AtenPE, AtenPEError import voluptuous as vol from homeassistant.components.switch import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchDeviceClass, SwitchEntity, ) @@ -30,7 +30,7 @@ DEFAULT_COMMUNITY = "private" DEFAULT_PORT = "161" DEFAULT_USERNAME = "administrator" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index a01965e3667..856c9301cfd 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -32,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Droplet" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string])} ) diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py index 61f3e6f4538..e0d063eb9fd 100644 --- a/homeassistant/components/edimax/switch.py +++ b/homeassistant/components/edimax/switch.py @@ -7,7 +7,10 @@ from typing import Any from pyedimax.smartplug import SmartPlug import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -20,7 +23,7 @@ DEFAULT_NAME = "Edimax Smart Plug" DEFAULT_PASSWORD = "1234" DEFAULT_USERNAME = "admin" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index 4fa75ff9712..9bf8b8e775c 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -7,7 +7,10 @@ from typing import Any from enocean.utils import combine_hex import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_ID, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry as er @@ -20,7 +23,7 @@ from .device import EnOceanEntity CONF_CHANNEL = "channel" DEFAULT_NAME = "EnOcean Switch" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/gc100/switch.py b/homeassistant/components/gc100/switch.py index ea90dde6abf..1bcdc7365cf 100644 --- a/homeassistant/components/gc100/switch.py +++ b/homeassistant/components/gc100/switch.py @@ -6,7 +6,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -17,7 +20,7 @@ from . import CONF_PORTS, DATA_GC100 _SWITCH_SCHEMA = vol.Schema({cv.string: cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_PORTS): vol.All(cv.ensure_list, [_SWITCH_SCHEMA])} ) diff --git a/homeassistant/components/group/switch.py b/homeassistant/components/group/switch.py index 7be6b188e72..9db264c8041 100644 --- a/homeassistant/components/group/switch.py +++ b/homeassistant/components/group/switch.py @@ -7,7 +7,11 @@ from typing import Any import voluptuous as vol -from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + DOMAIN, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -33,7 +37,7 @@ CONF_ALL = "all" # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py index c455fcb5bbc..653d5a07174 100644 --- a/homeassistant/components/hikvisioncam/switch.py +++ b/homeassistant/components/hikvisioncam/switch.py @@ -9,7 +9,10 @@ import hikvision.api from hikvision.error import HikvisionError, MissingParamError import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -33,7 +36,7 @@ DEFAULT_PASSWORD = "12345" DEFAULT_PORT = 80 DEFAULT_USERNAME = "admin" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/kankun/switch.py b/homeassistant/components/kankun/switch.py index f650494b3b1..a86bed5eb9a 100644 --- a/homeassistant/components/kankun/switch.py +++ b/homeassistant/components/kankun/switch.py @@ -8,7 +8,10 @@ from typing import Any import requests import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -39,7 +42,7 @@ SWITCH_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA)} ) diff --git a/homeassistant/components/linode/switch.py b/homeassistant/components/linode/switch.py index f2665671c0b..abaf77648ef 100644 --- a/homeassistant/components/linode/switch.py +++ b/homeassistant/components/linode/switch.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -30,7 +33,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Node" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_NODES): vol.All(cv.ensure_list, [cv.string])} ) diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index fe0aeb902ee..833a2c21301 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -9,7 +9,10 @@ from mficlient.client import FailedToLogin, MFiClient import requests import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -30,7 +33,7 @@ DEFAULT_VERIFY_SSL = True SWITCH_MODELS = ["Outlet", "Output 5v", "Output 12v", "Output 24v", "Dimmer Switch"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index 4cc77e44ec4..f5627f5e56b 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -12,7 +12,10 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.http import HomeAssistantView -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -44,7 +47,7 @@ REQ_CONF = [CONF_HOST, CONF_OUTLETS] URL_API_NETIO_EP = "/api/netio/{host}" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/orvibo/switch.py b/homeassistant/components/orvibo/switch.py index ece833b7036..34bf63aaaab 100644 --- a/homeassistant/components/orvibo/switch.py +++ b/homeassistant/components/orvibo/switch.py @@ -8,7 +8,10 @@ from typing import Any from orvibo.s20 import S20, S20Exception, discover import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_DISCOVERY, CONF_HOST, @@ -26,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Orvibo S20 Switch" DEFAULT_DISCOVERY = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SWITCHES, default=[]): vol.All( cv.ensure_list, diff --git a/homeassistant/components/pencom/switch.py b/homeassistant/components/pencom/switch.py index a1ec25a58e9..d16c7e1600c 100644 --- a/homeassistant/components/pencom/switch.py +++ b/homeassistant/components/pencom/switch.py @@ -8,7 +8,10 @@ from typing import Any from pencompy.pencompy import Pencompy import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -31,7 +34,7 @@ RELAY_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port, diff --git a/homeassistant/components/pilight/switch.py b/homeassistant/components/pilight/switch.py index 0d0023d9cd6..5be63064b4a 100644 --- a/homeassistant/components/pilight/switch.py +++ b/homeassistant/components/pilight/switch.py @@ -4,7 +4,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_SWITCHES from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -13,7 +16,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .base_class import SWITCHES_SCHEMA, PilightBaseDevice -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_SWITCHES): vol.Schema({cv.string: SWITCHES_SCHEMA})} ) diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py index 553a1b4a283..4ab1f905068 100644 --- a/homeassistant/components/pulseaudio_loopback/switch.py +++ b/homeassistant/components/pulseaudio_loopback/switch.py @@ -8,7 +8,10 @@ from typing import Any from pulsectl import Pulse, PulseError import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -27,7 +30,7 @@ DEFAULT_PORT = 4713 IGNORED_SWITCH_WARN = "Switch is already in the desired state. Ignoring." -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SINK_NAME): cv.string, vol.Required(CONF_SOURCE_NAME): cv.string, diff --git a/homeassistant/components/raincloud/switch.py b/homeassistant/components/raincloud/switch.py index d901f862133..45d0b4f0fc5 100644 --- a/homeassistant/components/raincloud/switch.py +++ b/homeassistant/components/raincloud/switch.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -25,7 +28,7 @@ from . import ( _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SWITCHES)): vol.All( cv.ensure_list, [vol.In(SWITCHES)] diff --git a/homeassistant/components/raspyrfm/switch.py b/homeassistant/components/raspyrfm/switch.py index ce69818beec..37835ecb40a 100644 --- a/homeassistant/components/raspyrfm/switch.py +++ b/homeassistant/components/raspyrfm/switch.py @@ -13,7 +13,10 @@ from raspyrfm_client.device_implementations.gateway.manufacturer.gateway_constan from raspyrfm_client.device_implementations.manufacturer_constants import Manufacturer import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -34,7 +37,7 @@ CONF_CHANNEL_CONFIG = "channel_config" DEFAULT_HOST = "127.0.0.1" # define configuration parameters -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT): cv.port, diff --git a/homeassistant/components/recswitch/switch.py b/homeassistant/components/recswitch/switch.py index a0035d50582..78fc0a805f6 100644 --- a/homeassistant/components/recswitch/switch.py +++ b/homeassistant/components/recswitch/switch.py @@ -8,7 +8,10 @@ from typing import Any from pyrecswitch import RSNetwork, RSNetworkError import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -21,7 +24,7 @@ DEFAULT_NAME = "RecSwitch {0}" DATA_RSN = "RSN" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_MAC): vol.All(cv.string, vol.Upper), diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py index 756e9dcfce9..ff9ecbcd97b 100644 --- a/homeassistant/components/remote_rpi_gpio/switch.py +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -6,7 +6,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -20,7 +23,7 @@ CONF_PORTS = "ports" _SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORTS): _SENSORS_SCHEMA, diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 99aadce6620..d01aab2cf9f 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.switch import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchEntity, ) from homeassistant.const import ( @@ -64,7 +64,7 @@ DEFAULT_VERIFY_SSL = True SUPPORT_REST_METHODS = ["post", "put", "patch"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { **TEMPLATE_ENTITY_BASE_SCHEMA.schema, vol.Required(CONF_RESOURCE): cv.url, diff --git a/homeassistant/components/rflink/switch.py b/homeassistant/components/rflink/switch.py index fdf8f63ab7d..af4bbc43700 100644 --- a/homeassistant/components/rflink/switch.py +++ b/homeassistant/components/rflink/switch.py @@ -4,7 +4,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_DEVICES, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -25,7 +28,7 @@ from . import ( PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({}) diff --git a/homeassistant/components/scsgate/switch.py b/homeassistant/components/scsgate/switch.py index 8ad31106cf7..abc906a5533 100644 --- a/homeassistant/components/scsgate/switch.py +++ b/homeassistant/components/scsgate/switch.py @@ -9,7 +9,10 @@ from scsgate.messages import ScenarioTriggeredMessage, StateMessage from scsgate.tasks import ToggleStatusTask import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICES, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -23,7 +26,7 @@ ATTR_SCENARIO_ID = "scenario_id" CONF_TRADITIONAL = "traditional" CONF_SCENARIO = "scenario" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_DEVICES): cv.schema_with_slug_keys(SCSGATE_SCHEMA)} ) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 02a94aeb8c1..e3ce09cbf48 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -31,7 +31,10 @@ from pysnmp.proto.rfc1902 import ( ) import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -98,7 +101,7 @@ MAP_SNMP_VARTYPES = { "Unsigned32": Unsigned32, } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_BASEOID): cv.string, vol.Optional(CONF_COMMAND_OID): cv.string, diff --git a/homeassistant/components/sony_projector/switch.py b/homeassistant/components/sony_projector/switch.py index 7ecff46d3bd..e018c06e050 100644 --- a/homeassistant/components/sony_projector/switch.py +++ b/homeassistant/components/sony_projector/switch.py @@ -8,7 +8,10 @@ from typing import Any import pysdcp import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -19,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Sony Projector" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index ee8b65b47e2..8484eb5a2d1 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -8,7 +8,10 @@ from typing import Any from switchmate import Switchmate import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -20,7 +23,7 @@ DEFAULT_NAME = "Switchmate" SCAN_INTERVAL = timedelta(minutes=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index 6a6f758ff79..805f037dbae 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.switch import ( ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchEntity, ) from homeassistant.const import ( @@ -49,7 +49,7 @@ SWITCH_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA)} ) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index f585cd929c0..3a7cfcde0f7 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.switch import ( ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchEntity, ) from homeassistant.const import ( @@ -55,7 +55,7 @@ SWITCH_SCHEMA = vol.All( ).extend(TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY.schema), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( {vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA)} ) diff --git a/homeassistant/components/thinkingcleaner/switch.py b/homeassistant/components/thinkingcleaner/switch.py index f99cda4347a..76c7cdb0db2 100644 --- a/homeassistant/components/thinkingcleaner/switch.py +++ b/homeassistant/components/thinkingcleaner/switch.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.switch import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchEntity, SwitchEntityDescription, ) @@ -42,7 +42,7 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.string}) def setup_platform( diff --git a/homeassistant/components/vultr/switch.py b/homeassistant/components/vultr/switch.py index 6758748b9f3..b03d613895a 100644 --- a/homeassistant/components/vultr/switch.py +++ b/homeassistant/components/vultr/switch.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -35,7 +38,7 @@ from . import ( _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Vultr {}" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SUBSCRIPTION): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index e5c3a055310..cf38d05de38 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -10,7 +10,7 @@ import voluptuous as vol import wakeonlan from homeassistant.components.switch import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchEntity, ) from homeassistant.const import ( @@ -36,7 +36,7 @@ CONF_OFF_ACTION = "turn_off" DEFAULT_NAME = "Wake on LAN" DEFAULT_PING_TIMEOUT = 1 -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_BROADCAST_ADDRESS): cv.string, diff --git a/homeassistant/components/wirelesstag/switch.py b/homeassistant/components/wirelesstag/switch.py index 0eafea0699b..239461df4ea 100644 --- a/homeassistant/components/wirelesstag/switch.py +++ b/homeassistant/components/wirelesstag/switch.py @@ -7,7 +7,7 @@ from typing import Any import voluptuous as vol from homeassistant.components.switch import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, SwitchEntity, SwitchEntityDescription, ) @@ -48,7 +48,7 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SWITCH_KEYS: list[str] = [desc.key for desc in SWITCH_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( cv.ensure_list, [vol.In(SWITCH_KEYS)] diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index 48cbe58a876..23adf2f4c88 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -9,7 +9,10 @@ import voluptuous as vol from zoneminder.monitor import Monitor, MonitorState from zoneminder.zm import ZoneMinder -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -21,7 +24,7 @@ from . import DOMAIN as ZONEMINDER_DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_COMMAND_ON): cv.string, vol.Required(CONF_COMMAND_OFF): cv.string, From 6c81885caeeffded1e6fc699826d9c9fe569d1dd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:40:17 +0200 Subject: [PATCH 0193/2411] Force alias when importing calendar PLATFORM_SCHEMA (#120512) --- homeassistant/components/caldav/calendar.py | 4 ++-- homeassistant/components/todoist/calendar.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index b9f967d1a08..7591722b1ab 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.calendar import ( ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CALENDAR_PLATFORM_SCHEMA, CalendarEntity, CalendarEvent, is_offset_reached, @@ -48,7 +48,7 @@ CONFIG_ENTRY_DEFAULT_DAYS = 7 # Only allow VCALENDARs that support this component type SUPPORTED_COMPONENT = "VEVENT" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CALENDAR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_URL): vol.Url(), vol.Optional(CONF_CALENDARS, default=[]): vol.All(cv.ensure_list, [cv.string]), diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index baa7103f7eb..1c6f40005c1 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -14,7 +14,7 @@ from todoist_api_python.models import Due, Label, Task import voluptuous as vol from homeassistant.components.calendar import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CALENDAR_PLATFORM_SCHEMA, CalendarEntity, CalendarEvent, ) @@ -82,7 +82,7 @@ NEW_TASK_SERVICE_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CALENDAR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_TOKEN): cv.string, vol.Optional(CONF_EXTRA_PROJECTS, default=[]): vol.All( From c96dc00a3a3d6c9e1a342c06b6cbb98e06336354 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:41:46 +0200 Subject: [PATCH 0194/2411] Force alias when importing alarm control panel PLATFORM_SCHEMA (#120505) --- homeassistant/components/concord232/alarm_control_panel.py | 4 ++-- homeassistant/components/ifttt/alarm_control_panel.py | 4 ++-- homeassistant/components/nx584/alarm_control_panel.py | 4 ++-- homeassistant/components/template/alarm_control_panel.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index 0256f5aab37..661a2beacc0 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -10,7 +10,7 @@ import requests import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA, AlarmControlPanelEntity, AlarmControlPanelEntityFeature, CodeFormat, @@ -39,7 +39,7 @@ DEFAULT_MODE = "audible" SCAN_INTERVAL = datetime.timedelta(seconds=10) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index 81ed9320bcb..1af23d716c8 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -7,7 +7,7 @@ import logging import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA, AlarmControlPanelEntity, AlarmControlPanelEntityFeature, CodeFormat, @@ -54,7 +54,7 @@ DEFAULT_EVENT_DISARM = "alarm_disarm" CONF_CODE_ARM_REQUIRED = "code_arm_required" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_CODE): cv.string, diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index 2e306de5908..61de4f611b8 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -10,7 +10,7 @@ import requests import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA, AlarmControlPanelEntity, AlarmControlPanelEntityFeature, CodeFormat, @@ -41,7 +41,7 @@ SERVICE_BYPASS_ZONE = "bypass_zone" SERVICE_UNBYPASS_ZONE = "unbypass_zone" ATTR_ZONE = "zone" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 4a1af80e25c..2ac91d39858 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( ENTITY_ID_FORMAT, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA, AlarmControlPanelEntity, AlarmControlPanelEntityFeature, CodeFormat, @@ -94,7 +94,7 @@ ALARM_CONTROL_PANEL_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ALARM_CONTROL_PANELS): cv.schema_with_slug_keys( ALARM_CONTROL_PANEL_SCHEMA From 8e598ec3ff9b6c96dfe633d5510762c4053279c3 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 10:50:32 +0200 Subject: [PATCH 0195/2411] Rename sensor to finished downloads in pyLoad integration (#120483) --- homeassistant/components/pyload/strings.json | 2 +- .../pyload/snapshots/test_sensor.ambr | 232 ++---------------- 2 files changed, 25 insertions(+), 209 deletions(-) diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 0ed016aafb8..9f043ba224a 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -74,7 +74,7 @@ "name": "Downloads in queue" }, "total": { - "name": "Total downlods" + "name": "Finished downloads" }, "free_space": { "name": "Free space" diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index 8675fb696a5..159309041e0 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -91,7 +91,7 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_total-entry] +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_finished_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -103,7 +103,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -115,7 +115,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Downloads total', + 'original_name': 'Finished downloads', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, @@ -124,13 +124,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_total-state] +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Downloads total', + 'friendly_name': 'pyLoad Finished downloads', }), 'context': , - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'last_changed': , 'last_reported': , 'last_updated': , @@ -245,52 +245,6 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_total_downlods-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_total_downlods', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Total downlods', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_total_downlods-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Total downlods', - }), - 'context': , - 'entity_id': 'sensor.pyload_total_downlods', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- # name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_active_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -383,7 +337,7 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_total-entry] +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_finished_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -395,7 +349,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -407,7 +361,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Downloads total', + 'original_name': 'Finished downloads', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, @@ -416,13 +370,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_total-state] +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Downloads total', + 'friendly_name': 'pyLoad Finished downloads', }), 'context': , - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'last_changed': , 'last_reported': , 'last_updated': , @@ -537,52 +491,6 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_total_downlods-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_total_downlods', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Total downlods', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_total_downlods-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Total downlods', - }), - 'context': , - 'entity_id': 'sensor.pyload_total_downlods', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- # name: test_sensor_update_exceptions[ParserError][sensor.pyload_active_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -675,7 +583,7 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_total-entry] +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_finished_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -687,7 +595,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -699,7 +607,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Downloads total', + 'original_name': 'Finished downloads', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, @@ -708,13 +616,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_total-state] +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Downloads total', + 'friendly_name': 'pyLoad Finished downloads', }), 'context': , - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'last_changed': , 'last_reported': , 'last_updated': , @@ -829,52 +737,6 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[ParserError][sensor.pyload_total_downlods-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_total_downlods', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Total downlods', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor_update_exceptions[ParserError][sensor.pyload_total_downlods-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Total downlods', - }), - 'context': , - 'entity_id': 'sensor.pyload_total_downlods', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- # name: test_setup[sensor.pyload_active_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -967,7 +829,7 @@ 'state': '6', }) # --- -# name: test_setup[sensor.pyload_downloads_total-entry] +# name: test_setup[sensor.pyload_finished_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -979,7 +841,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -991,7 +853,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Downloads total', + 'original_name': 'Finished downloads', 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, @@ -1000,13 +862,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup[sensor.pyload_downloads_total-state] +# name: test_setup[sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Downloads total', + 'friendly_name': 'pyLoad Finished downloads', }), 'context': , - 'entity_id': 'sensor.pyload_downloads_total', + 'entity_id': 'sensor.pyload_finished_downloads', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1121,49 +983,3 @@ 'state': '43.247704', }) # --- -# name: test_setup[sensor.pyload_total_downlods-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_total_downlods', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Total downlods', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, - }) -# --- -# name: test_setup[sensor.pyload_total_downlods-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Total downlods', - }), - 'context': , - 'entity_id': 'sensor.pyload_total_downlods', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '37', - }) -# --- From 399130bc95d2b374bed8454a3e138cf68c4897a1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:54:19 +0200 Subject: [PATCH 0196/2411] Force alias when importing binary sensor PLATFORM_SCHEMA (#120510) --- homeassistant/components/ads/binary_sensor.py | 4 ++-- homeassistant/components/arest/binary_sensor.py | 4 ++-- homeassistant/components/bayesian/binary_sensor.py | 4 ++-- homeassistant/components/bloomsky/binary_sensor.py | 4 ++-- homeassistant/components/concord232/binary_sensor.py | 4 ++-- homeassistant/components/digital_ocean/binary_sensor.py | 4 ++-- homeassistant/components/enocean/binary_sensor.py | 4 ++-- homeassistant/components/ffmpeg_motion/binary_sensor.py | 4 ++-- homeassistant/components/ffmpeg_noise/binary_sensor.py | 4 ++-- homeassistant/components/flic/binary_sensor.py | 7 +++++-- homeassistant/components/gc100/binary_sensor.py | 7 +++++-- homeassistant/components/group/binary_sensor.py | 4 ++-- homeassistant/components/hikvision/binary_sensor.py | 4 ++-- homeassistant/components/linode/binary_sensor.py | 4 ++-- homeassistant/components/meteoalarm/binary_sensor.py | 4 ++-- homeassistant/components/nx584/binary_sensor.py | 4 ++-- homeassistant/components/pilight/binary_sensor.py | 7 +++++-- homeassistant/components/raincloud/binary_sensor.py | 7 +++++-- homeassistant/components/random/binary_sensor.py | 4 ++-- homeassistant/components/remote_rpi_gpio/binary_sensor.py | 7 +++++-- homeassistant/components/rflink/binary_sensor.py | 4 ++-- homeassistant/components/tapsaff/binary_sensor.py | 7 +++++-- homeassistant/components/tcp/binary_sensor.py | 4 ++-- homeassistant/components/template/binary_sensor.py | 4 ++-- homeassistant/components/threshold/binary_sensor.py | 4 ++-- homeassistant/components/tod/binary_sensor.py | 4 ++-- homeassistant/components/trend/binary_sensor.py | 4 ++-- homeassistant/components/vultr/binary_sensor.py | 4 ++-- homeassistant/components/w800rf32/binary_sensor.py | 4 ++-- homeassistant/components/wirelesstag/binary_sensor.py | 7 +++++-- 30 files changed, 81 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py index 2da76382c51..6ee17e07f0f 100644 --- a/homeassistant/components/ads/binary_sensor.py +++ b/homeassistant/components/ads/binary_sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity DEFAULT_NAME = "ADS binary sensor" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ADS_VAR): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index 71f1c081f2d..00d4d6bbf9b 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_PIN, CONF_RESOURCE @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_NAME): cv.string, diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 470732f36d2..192d7987311 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -11,7 +11,7 @@ from uuid import UUID import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -99,7 +99,7 @@ TEMPLATE_SCHEMA = vol.Schema( required=True, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index 3582b186013..12d55f971e1 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -19,7 +19,7 @@ from . import DOMAIN SENSOR_TYPES = {"Rain": BinarySensorDeviceClass.MOISTURE, "Night": None} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( cv.ensure_list, [vol.In(SENSOR_TYPES)] diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 79cf0c758e1..a1dcbc222f7 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -36,7 +36,7 @@ SCAN_INTERVAL = datetime.timedelta(seconds=10) ZONE_TYPES_SCHEMA = vol.Schema({cv.positive_int: BINARY_SENSOR_DEVICE_CLASSES_SCHEMA}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_EXCLUDE_ZONES, default=[]): vol.All( cv.ensure_list, [cv.positive_int] diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index 9218d9bde0e..0d4b31faa2c 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -7,7 +7,7 @@ import logging import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -34,7 +34,7 @@ from . import ( _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Droplet" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string])} ) diff --git a/homeassistant/components/enocean/binary_sensor.py b/homeassistant/components/enocean/binary_sensor.py index 9ebedc52c00..3ecf1ba4ba2 100644 --- a/homeassistant/components/enocean/binary_sensor.py +++ b/homeassistant/components/enocean/binary_sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -23,7 +23,7 @@ DEFAULT_NAME = "EnOcean binary sensor" DEPENDENCIES = ["enocean"] EVENT_BUTTON_PRESSED = "button_pressed" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index a9e1de2ea05..7dc32fd96a3 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -9,7 +9,7 @@ import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -34,7 +34,7 @@ CONF_REPEAT_TIME = "repeat_time" DEFAULT_NAME = "FFmpeg Motion" DEFAULT_INIT_STATE = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_INPUT): cv.string, vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean, diff --git a/homeassistant/components/ffmpeg_noise/binary_sensor.py b/homeassistant/components/ffmpeg_noise/binary_sensor.py index a434b4a9924..abbf77eba6b 100644 --- a/homeassistant/components/ffmpeg_noise/binary_sensor.py +++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py @@ -8,7 +8,7 @@ import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, ) from homeassistant.components.ffmpeg import ( @@ -33,7 +33,7 @@ CONF_RESET = "reset" DEFAULT_NAME = "FFmpeg Noise" DEFAULT_INIT_STATE = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_INPUT): cv.string, vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean, diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py index b7f8bb0c854..fcfe4b6604f 100644 --- a/homeassistant/components/flic/binary_sensor.py +++ b/homeassistant/components/flic/binary_sensor.py @@ -8,7 +8,10 @@ import threading import pyflic import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import ( CONF_DISCOVERY, CONF_HOST, @@ -42,7 +45,7 @@ EVENT_DATA_ADDRESS = "button_address" EVENT_DATA_TYPE = "click_type" EVENT_DATA_QUEUED_TIME = "queued_time" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/gc100/binary_sensor.py b/homeassistant/components/gc100/binary_sensor.py index a03eae509d9..55df72cc3b9 100644 --- a/homeassistant/components/gc100/binary_sensor.py +++ b/homeassistant/components/gc100/binary_sensor.py @@ -4,7 +4,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -15,7 +18,7 @@ from . import CONF_PORTS, DATA_GC100 _SENSORS_SCHEMA = vol.Schema({cv.string: cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_PORTS): vol.All(cv.ensure_list, [_SENSORS_SCHEMA])} ) diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 3fbadfb156c..06c810c2643 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, DOMAIN as BINARY_SENSOR_DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -36,7 +36,7 @@ DEFAULT_NAME = "Binary Sensor Group" CONF_ALL = "all" REG_KEY = f"{BINARY_SENSOR_DOMAIN}_registry" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(BINARY_SENSOR_DOMAIN), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index 2e4af361b38..0656733db6b 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -9,7 +9,7 @@ from pyhik.hikvision import HikCamera import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -74,7 +74,7 @@ CUSTOMIZE_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py index 02c7a1ef383..d0c49c7171b 100644 --- a/homeassistant/components/linode/binary_sensor.py +++ b/homeassistant/components/linode/binary_sensor.py @@ -7,7 +7,7 @@ import logging import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -32,7 +32,7 @@ from . import ( _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Node" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_NODES): vol.All(cv.ensure_list, [cv.string])} ) diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py index 8fb0ae5cdc8..3400ca52f50 100644 --- a/homeassistant/components/meteoalarm/binary_sensor.py +++ b/homeassistant/components/meteoalarm/binary_sensor.py @@ -9,7 +9,7 @@ from meteoalertapi import Meteoalert import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -32,7 +32,7 @@ DEFAULT_NAME = "meteoalarm" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_COUNTRY): cv.string, vol.Required(CONF_PROVINCE): cv.string, diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index 429b517fce4..04e79716423 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -33,7 +33,7 @@ DEFAULT_SSL = False ZONE_TYPES_SCHEMA = vol.Schema({cv.positive_int: BINARY_SENSOR_DEVICE_CLASSES_SCHEMA}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_EXCLUDE_ZONES, default=[]): vol.All( cv.ensure_list, [cv.positive_int] diff --git a/homeassistant/components/pilight/binary_sensor.py b/homeassistant/components/pilight/binary_sensor.py index 0ddb2de4603..4d68748e0f7 100644 --- a/homeassistant/components/pilight/binary_sensor.py +++ b/homeassistant/components/pilight/binary_sensor.py @@ -6,7 +6,10 @@ import datetime import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import ( CONF_DISARM_AFTER_TRIGGER, CONF_NAME, @@ -27,7 +30,7 @@ CONF_VARIABLE = "variable" CONF_RESET_DELAY_SEC = "reset_delay_sec" DEFAULT_NAME = "Pilight Binary Sensor" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_VARIABLE): cv.string, vol.Required(CONF_PAYLOAD): vol.Schema(dict), diff --git a/homeassistant/components/raincloud/binary_sensor.py b/homeassistant/components/raincloud/binary_sensor.py index 8530323dad1..90ad36985ef 100644 --- a/homeassistant/components/raincloud/binary_sensor.py +++ b/homeassistant/components/raincloud/binary_sensor.py @@ -6,7 +6,10 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -17,7 +20,7 @@ from . import BINARY_SENSORS, DATA_RAINCLOUD, ICON_MAP, RainCloudEntity _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): vol.All( cv.ensure_list, [vol.In(BINARY_SENSORS)] diff --git a/homeassistant/components/random/binary_sensor.py b/homeassistant/components/random/binary_sensor.py index bbcf87630c5..9d33ad52692 100644 --- a/homeassistant/components/random/binary_sensor.py +++ b/homeassistant/components/random/binary_sensor.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -22,7 +22,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType DEFAULT_NAME = "Random binary sensor" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py index ad995614ed4..98ae7328bc5 100644 --- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -5,7 +5,10 @@ from __future__ import annotations import requests import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -26,7 +29,7 @@ CONF_PORTS = "ports" _SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORTS): _SENSORS_SCHEMA, diff --git a/homeassistant/components/rflink/binary_sensor.py b/homeassistant/components/rflink/binary_sensor.py index 789e25c62b1..b731037fbfc 100644 --- a/homeassistant/components/rflink/binary_sensor.py +++ b/homeassistant/components/rflink/binary_sensor.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -31,7 +31,7 @@ from . import CONF_ALIASES, RflinkDevice CONF_OFF_DELAY = "off_delay" DEFAULT_FORCE_UPDATE = False -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_DEVICES, default={}): { cv.string: vol.Schema( diff --git a/homeassistant/components/tapsaff/binary_sensor.py b/homeassistant/components/tapsaff/binary_sensor.py index a8b3c138db5..0eb612bdc8e 100644 --- a/homeassistant/components/tapsaff/binary_sensor.py +++ b/homeassistant/components/tapsaff/binary_sensor.py @@ -8,7 +8,10 @@ import logging from tapsaff import TapsAff import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import CONF_LOCATION, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -21,7 +24,7 @@ DEFAULT_NAME = "Taps Aff" SCAN_INTERVAL = timedelta(minutes=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_LOCATION): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/tcp/binary_sensor.py b/homeassistant/components/tcp/binary_sensor.py index 3e432778910..638dfd53de5 100644 --- a/homeassistant/components/tcp/binary_sensor.py +++ b/homeassistant/components/tcp/binary_sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from typing import Final from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.core import HomeAssistant @@ -15,7 +15,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .common import TCP_PLATFORM_SCHEMA, TcpEntity from .const import CONF_VALUE_ON -PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend(TCP_PLATFORM_SCHEMA) +PLATFORM_SCHEMA: Final = BINARY_SENSOR_PLATFORM_SCHEMA.extend(TCP_PLATFORM_SCHEMA) def setup_platform( diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 0fa588a78f1..4618e30b1f3 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, DOMAIN as BINARY_SENSOR_DOMAIN, ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -131,7 +131,7 @@ def rewrite_legacy_to_modern_conf(cfg: dict[str, dict]) -> list[dict]: return sensors -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SENSORS): cv.schema_with_slug_keys( LEGACY_BINARY_SENSOR_SCHEMA diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 8c3882ff360..a791658f049 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -60,7 +60,7 @@ TYPE_LOWER = "lower" TYPE_RANGE = "range" TYPE_UPPER = "upper" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 5b6c7077a97..907df849ea1 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypeGuard import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -44,7 +44,7 @@ ATTR_AFTER = "after" ATTR_BEFORE = "before" ATTR_NEXT_UPDATE = "next_update" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_AFTER): vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), vol.Required(CONF_BEFORE): vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 6788d22219b..693c080e86e 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -92,7 +92,7 @@ SENSOR_SCHEMA = vol.All( _validate_min_max, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA)} ) diff --git a/homeassistant/components/vultr/binary_sensor.py b/homeassistant/components/vultr/binary_sensor.py index 5c0db81e843..6a697eebe11 100644 --- a/homeassistant/components/vultr/binary_sensor.py +++ b/homeassistant/components/vultr/binary_sensor.py @@ -7,7 +7,7 @@ import logging import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -38,7 +38,7 @@ from . import ( _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Vultr {}" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SUBSCRIPTION): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index 49eec35cb1e..06e9e0dfdac 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -9,7 +9,7 @@ import W800rf32 as w800 from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, BinarySensorEntity, ) from homeassistant.const import CONF_DEVICE_CLASS, CONF_DEVICES, CONF_NAME @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) CONF_OFF_DELAY = "off_delay" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_DEVICES): { cv.string: vol.Schema( diff --git a/homeassistant/components/wirelesstag/binary_sensor.py b/homeassistant/components/wirelesstag/binary_sensor.py index 85efab16e70..052f6547dd2 100644 --- a/homeassistant/components/wirelesstag/binary_sensor.py +++ b/homeassistant/components/wirelesstag/binary_sensor.py @@ -4,7 +4,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import CONF_MONITORED_CONDITIONS, STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -65,7 +68,7 @@ SENSOR_TYPES = { } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( cv.ensure_list, [vol.In(SENSOR_TYPES)] From e06d7050f2ddeccd75d0807569efc3492d1e4aa1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:55:09 +0200 Subject: [PATCH 0197/2411] Force alias when importing climate PLATFORM_SCHEMA (#120518) --- homeassistant/components/daikin/climate.py | 4 ++-- homeassistant/components/ephember/climate.py | 4 ++-- homeassistant/components/flexit/climate.py | 4 ++-- homeassistant/components/generic_thermostat/climate.py | 4 ++-- homeassistant/components/heatmiser/climate.py | 4 ++-- homeassistant/components/intesishome/climate.py | 4 ++-- homeassistant/components/oem/climate.py | 4 ++-- homeassistant/components/proliphix/climate.py | 4 ++-- homeassistant/components/schluter/climate.py | 4 ++-- homeassistant/components/tfiac/climate.py | 4 ++-- homeassistant/components/touchline/climate.py | 4 ++-- homeassistant/components/venstar/climate.py | 4 ++-- homeassistant/components/zhong_hong/climate.py | 4 ++-- 13 files changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 34ae8701d5d..fc54d4b0427 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -12,7 +12,7 @@ from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_PRESET_MODE, ATTR_SWING_MODE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, PRESET_AWAY, PRESET_BOOST, PRESET_ECO, @@ -45,7 +45,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( {vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string} ) diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index 89d84a2c6fd..44e5986970d 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -20,7 +20,7 @@ from pyephember.pyephember import ( import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -44,7 +44,7 @@ SCAN_INTERVAL = timedelta(seconds=120) OPERATION_LIST = [HVACMode.HEAT_COOL, HVACMode.HEAT, HVACMode.OFF] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} ) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index c15c74b4aac..d456fbef6fc 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -8,7 +8,7 @@ from typing import Any import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -36,7 +36,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType CALL_TYPE_WRITE_REGISTER = "write_register" CONF_HUB = "hub" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Required(CONF_SLAVE): vol.All(int, vol.Range(min=0, max=32)), diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 91ff1af122d..c080e8b82d7 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.climate import ( ATTR_PRESET_MODE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, PRESET_ACTIVITY, PRESET_AWAY, PRESET_COMFORT, @@ -125,7 +125,7 @@ PLATFORM_SCHEMA_COMMON = vol.Schema( ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_COMMON.schema) +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_COMMON.schema) async def async_setup_entry( diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index 8639d1f953e..f9f0cfacf60 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -9,7 +9,7 @@ from heatmiserV3 import connection, heatmiser import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACMode, @@ -38,7 +38,7 @@ TSTATS_SCHEMA = vol.Schema( ) ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.string, diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 7a504d7aced..82b653a34c7 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.climate import ( ATTR_HVAC_MODE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, @@ -44,7 +44,7 @@ IH_DEVICE_INTESISHOME = "IntesisHome" IH_DEVICE_AIRCONWITHME = "airconwithme" IH_DEVICE_ANYWAIR = "anywair" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/oem/climate.py b/homeassistant/components/oem/climate.py index 6c4b97ca450..cf16f1ba87e 100644 --- a/homeassistant/components/oem/climate.py +++ b/homeassistant/components/oem/climate.py @@ -9,7 +9,7 @@ import requests import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -29,7 +29,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default="Thermostat"): cv.string, diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 23ccc03a038..18b974800a3 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -8,7 +8,7 @@ import proliphix import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -29,7 +29,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType ATTR_FAN = "fan" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py index 74e2d9a0194..6f0a49e6eb9 100644 --- a/homeassistant/components/schluter/climate.py +++ b/homeassistant/components/schluter/climate.py @@ -9,7 +9,7 @@ from requests import RequestException import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, SCAN_INTERVAL, ClimateEntity, ClimateEntityFeature, @@ -29,7 +29,7 @@ from homeassistant.helpers.update_coordinator import ( from . import DATA_SCHLUTER_API, DATA_SCHLUTER_SESSION, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_SCAN_INTERVAL): vol.All(vol.Coerce(int), vol.Range(min=1))} ) diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py index 74d2c3fbe7e..81517a6f1f5 100644 --- a/homeassistant/components/tfiac/climate.py +++ b/homeassistant/components/tfiac/climate.py @@ -15,7 +15,7 @@ from homeassistant.components.climate import ( FAN_HIGH, FAN_LOW, FAN_MEDIUM, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, SWING_BOTH, SWING_HORIZONTAL, SWING_OFF, @@ -32,7 +32,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType SCAN_INTERVAL = timedelta(seconds=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py index 5b1c52534c5..7b14404ee34 100644 --- a/homeassistant/components/touchline/climate.py +++ b/homeassistant/components/touchline/climate.py @@ -8,7 +8,7 @@ from pytouchline import PyTouchline import voluptuous as vol from homeassistant.components.climate import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACMode, @@ -41,7 +41,7 @@ TOUCHLINE_HA_PRESETS = { for preset, settings in PRESET_MODES.items() } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) def setup_platform( diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index f47cf59be9c..ea833dc3183 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -10,7 +10,7 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_ON, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, PRESET_AWAY, PRESET_NONE, ClimateEntity, @@ -48,7 +48,7 @@ from .const import ( ) from .coordinator import VenstarDataUpdateCoordinator -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index b0a8f02a2f3..eaf00b5432f 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -11,7 +11,7 @@ from zhong_hong_hvac.hvac import HVAC as ZhongHongHVAC from homeassistant.components.climate import ( ATTR_HVAC_MODE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, ClimateEntityFeature, HVACMode, @@ -42,7 +42,7 @@ DEFAULT_GATEWAY_ADDRRESS = 1 SIGNAL_DEVICE_ADDED = "zhong_hong_device_added" SIGNAL_ZHONG_HONG_HUB_START = "zhong_hong_hub_start" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, From 45dedf73c8859d955fab6d6ad76686bfe97f24ab Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 11:00:31 +0200 Subject: [PATCH 0198/2411] Add exception translations for pyLoad integration (#120520) --- homeassistant/components/pyload/__init__.py | 13 ++++++++++--- homeassistant/components/pyload/coordinator.py | 5 ++++- homeassistant/components/pyload/strings.json | 11 +++++++++++ tests/components/pyload/test_sensor.py | 2 +- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/pyload/__init__.py b/homeassistant/components/pyload/__init__.py index 0a89fbb6140..8b85dfa29a4 100644 --- a/homeassistant/components/pyload/__init__.py +++ b/homeassistant/components/pyload/__init__.py @@ -20,6 +20,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_create_clientsession +from .const import DOMAIN from .coordinator import PyLoadCoordinator PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] @@ -51,13 +52,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bo await pyloadapi.login() except CannotConnect as e: raise ConfigEntryNotReady( - "Unable to connect and retrieve data from pyLoad API" + translation_domain=DOMAIN, + translation_key="setup_request_exception", ) from e except ParserError as e: - raise ConfigEntryNotReady("Unable to parse data from pyLoad API") from e + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="setup_parse_exception", + ) from e except InvalidAuth as e: raise ConfigEntryAuthFailed( - f"Authentication failed for {entry.data[CONF_USERNAME]}, check your login credentials" + translation_domain=DOMAIN, + translation_key="setup_authentication_exception", + translation_placeholders={CONF_USERNAME: entry.data[CONF_USERNAME]}, ) from e coordinator = PyLoadCoordinator(hass, pyloadapi) diff --git a/homeassistant/components/pyload/coordinator.py b/homeassistant/components/pyload/coordinator.py index b96a8d2ccbf..fd0e95192b3 100644 --- a/homeassistant/components/pyload/coordinator.py +++ b/homeassistant/components/pyload/coordinator.py @@ -7,6 +7,7 @@ import logging from pyloadapi import CannotConnect, InvalidAuth, ParserError, PyLoadAPI from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -64,7 +65,9 @@ class PyLoadCoordinator(DataUpdateCoordinator[pyLoadData]): await self.pyload.login() except InvalidAuth as exc: raise ConfigEntryAuthFailed( - f"Authentication failed for {self.pyload.username}, check your login credentials", + translation_domain=DOMAIN, + translation_key="setup_authentication_exception", + translation_placeholders={CONF_USERNAME: self.pyload.username}, ) from exc raise UpdateFailed( diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 9f043ba224a..9fe311574fb 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -89,6 +89,17 @@ } } }, + "exceptions": { + "setup_request_exception": { + "message": "Unable to connect and retrieve data from pyLoad API, try again later" + }, + "setup_parse_exception": { + "message": "Unable to parse data from pyLoad API, try again later" + }, + "setup_authentication_exception": { + "message": "Authentication failed for {username}, verify your login credentials" + } + }, "issues": { "deprecated_yaml_import_issue_cannot_connect": { "title": "The pyLoad YAML configuration import failed", diff --git a/tests/components/pyload/test_sensor.py b/tests/components/pyload/test_sensor.py index 61a9a872f33..a44c9c8bf91 100644 --- a/tests/components/pyload/test_sensor.py +++ b/tests/components/pyload/test_sensor.py @@ -96,7 +96,7 @@ async def test_sensor_invalid_auth( await hass.async_block_till_done() assert ( - "Authentication failed for username, check your login credentials" + "Authentication failed for username, verify your login credentials" in caplog.text ) From 0c0f666a283a093538f77ffe629f73d0b1e18e57 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:03:13 +0200 Subject: [PATCH 0199/2411] Force alias when importing camera PLATFORM_SCHEMA (#120514) --- homeassistant/components/canary/camera.py | 4 ++-- homeassistant/components/familyhub/camera.py | 7 +++++-- homeassistant/components/ffmpeg/camera.py | 8 ++++++-- homeassistant/components/local_file/camera.py | 7 +++++-- homeassistant/components/proxy/camera.py | 4 ++-- homeassistant/components/push/camera.py | 9 +++++++-- homeassistant/components/uvc/camera.py | 8 ++++++-- homeassistant/components/vivotek/camera.py | 8 ++++++-- homeassistant/components/xeoma/camera.py | 7 +++++-- homeassistant/components/xiaomi/camera.py | 7 +++++-- homeassistant/components/yi/camera.py | 7 +++++-- 11 files changed, 54 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index e081d24e06a..a56d1ebc3de 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components import ffmpeg from homeassistant.components.camera import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, Camera, ) from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager @@ -40,7 +40,7 @@ FORCE_CAMERA_REFRESH_INTERVAL: Final = timedelta(minutes=15) PLATFORM_SCHEMA: Final = vol.All( cv.deprecated(CONF_FFMPEG_ARGUMENTS), - PARENT_PLATFORM_SCHEMA.extend( + CAMERA_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_FFMPEG_ARGUMENTS, default=DEFAULT_FFMPEG_ARGUMENTS diff --git a/homeassistant/components/familyhub/camera.py b/homeassistant/components/familyhub/camera.py index da6f82cf56b..462983278b0 100644 --- a/homeassistant/components/familyhub/camera.py +++ b/homeassistant/components/familyhub/camera.py @@ -5,7 +5,10 @@ from __future__ import annotations from pyfamilyhublocal import FamilyHubCam import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, +) from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -15,7 +18,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType DEFAULT_NAME = "FamilyHub Camera" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_IP_ADDRESS): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index c0ce4ad9746..2c46c4c29d1 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -9,7 +9,11 @@ from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, CameraEntityFeature +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, + CameraEntityFeature, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream @@ -28,7 +32,7 @@ from . import ( DEFAULT_NAME = "FFmpeg" DEFAULT_ARGUMENTS = "-pred 1" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_INPUT): cv.string, vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index 72fe1a88b86..1306751f1a9 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -8,7 +8,10 @@ import os import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, +) from homeassistant.const import ATTR_ENTITY_ID, CONF_FILE_PATH, CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv @@ -19,7 +22,7 @@ from .const import DATA_LOCAL_FILE, DEFAULT_NAME, DOMAIN, SERVICE_UPDATE_FILE_PA _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FILE_PATH): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 5cd72b05871..e5e3d01591a 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -11,7 +11,7 @@ from PIL import Image import voluptuous as vol from homeassistant.components.camera import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, Camera, async_get_image, async_get_mjpeg_stream, @@ -45,7 +45,7 @@ MODE_CROP = "crop" DEFAULT_BASENAME = "Camera Proxy" DEFAULT_QUALITY = 75 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_NAME): cv.string, diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index 8744ce8c2a1..1a37a10bf4f 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -11,7 +11,12 @@ import aiohttp import voluptuous as vol from homeassistant.components import webhook -from homeassistant.components.camera import DOMAIN, PLATFORM_SCHEMA, STATE_IDLE, Camera +from homeassistant.components.camera import ( + DOMAIN, + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + STATE_IDLE, + Camera, +) from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -32,7 +37,7 @@ ATTR_LAST_TRIP = "last_trip" PUSH_CAMERA_DATA = "push_camera" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_BUFFER_SIZE, default=1): cv.positive_int, diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 3162fc67566..cd9594c7d31 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -10,7 +10,11 @@ import requests from uvcclient import camera as uvc_camera, nvr import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, CameraEntityFeature +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, + CameraEntityFeature, +) from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_SSL from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -28,7 +32,7 @@ DEFAULT_PASSWORD = "ubnt" DEFAULT_PORT = 7080 DEFAULT_SSL = False -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NVR): cv.string, vol.Required(CONF_KEY): cv.string, diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index 8719d55ec29..a8bf652e963 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -5,7 +5,11 @@ from __future__ import annotations from libpyvivotek import VivotekCamera import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, CameraEntityFeature +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, + CameraEntityFeature, +) from homeassistant.const import ( CONF_AUTHENTICATION, CONF_IP_ADDRESS, @@ -32,7 +36,7 @@ DEFAULT_EVENT_0_KEY = "event_i0_enable" DEFAULT_SECURITY_LEVEL = "admin" DEFAULT_STREAM_SOURCE = "live.sdp" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_IP_ADDRESS): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/xeoma/camera.py b/homeassistant/components/xeoma/camera.py index 7d6abde8535..0c19e126fa7 100644 --- a/homeassistant/components/xeoma/camera.py +++ b/homeassistant/components/xeoma/camera.py @@ -7,7 +7,10 @@ import logging from pyxeoma.xeoma import Xeoma, XeomaError import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv @@ -32,7 +35,7 @@ CAMERAS_SCHEMA = vol.Schema( required=False, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_CAMERAS): vol.Schema( diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index f3e850a7839..323a0f8a157 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -9,7 +9,10 @@ from haffmpeg.camera import CameraMjpeg import voluptuous as vol from homeassistant.components import ffmpeg -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, +) from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import ( CONF_HOST, @@ -40,7 +43,7 @@ CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" MODEL_YI = "yi" MODEL_XIAOFANG = "xiaofang" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_HOST): cv.template, diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index f512d31cb6b..b2fac03954d 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -9,7 +9,10 @@ from haffmpeg.camera import CameraMjpeg import voluptuous as vol from homeassistant.components import ffmpeg -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, + Camera, +) from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import ( CONF_HOST, @@ -37,7 +40,7 @@ DEFAULT_ARGUMENTS = "-pred 1" CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_HOST): cv.string, From 41026b9227f624091406d30a6490dd21edff9c39 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:04:00 +0200 Subject: [PATCH 0200/2411] Implement @plugwise_command for Plugwise Select platform (#120509) --- homeassistant/components/plugwise/const.py | 1 + homeassistant/components/plugwise/select.py | 23 ++++++++------------- tests/components/plugwise/test_select.py | 14 +++++++++---- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index ed8cb2d2002..14599ce61fb 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -17,6 +17,7 @@ FLOW_SMILE: Final = "smile (Adam/Anna/P1)" FLOW_STRETCH: Final = "stretch (Stretch)" FLOW_TYPE: Final = "flow_type" GATEWAY: Final = "gateway" +LOCATION: Final = "location" PW_TYPE: Final = "plugwise_type" SMILE: Final = "smile" STRETCH: Final = "stretch" diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index c8c9791c0da..99aecacb96b 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -2,27 +2,24 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable from dataclasses import dataclass -from plugwise import Smile - from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import STATE_ON, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import PlugwiseConfigEntry -from .const import SelectOptionsType, SelectType +from .const import LOCATION, SelectOptionsType, SelectType from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity +from .util import plugwise_command @dataclass(frozen=True, kw_only=True) class PlugwiseSelectEntityDescription(SelectEntityDescription): """Class describing Plugwise Select entities.""" - command: Callable[[Smile, str, str], Awaitable[None]] key: SelectType options_key: SelectOptionsType @@ -31,28 +28,24 @@ SELECT_TYPES = ( PlugwiseSelectEntityDescription( key="select_schedule", translation_key="select_schedule", - command=lambda api, loc, opt: api.set_schedule_state(loc, STATE_ON, opt), options_key="available_schedules", ), PlugwiseSelectEntityDescription( key="select_regulation_mode", translation_key="regulation_mode", entity_category=EntityCategory.CONFIG, - command=lambda api, loc, opt: api.set_regulation_mode(opt), options_key="regulation_modes", ), PlugwiseSelectEntityDescription( key="select_dhw_mode", translation_key="dhw_mode", entity_category=EntityCategory.CONFIG, - command=lambda api, loc, opt: api.set_dhw_mode(opt), options_key="dhw_modes", ), PlugwiseSelectEntityDescription( key="select_gateway_mode", translation_key="gateway_mode", entity_category=EntityCategory.CONFIG, - command=lambda api, loc, opt: api.set_gateway_mode(opt), options_key="gateway_modes", ), ) @@ -109,10 +102,12 @@ class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): """Return the available select-options.""" return self.device[self.entity_description.options_key] + @plugwise_command async def async_select_option(self, option: str) -> None: - """Change to the selected entity option.""" - await self.entity_description.command( - self.coordinator.api, self.device["location"], option - ) + """Change to the selected entity option. - await self.coordinator.async_request_refresh() + self.device[LOCATION] and STATE_ON are required for the thermostat-schedule select. + """ + await self.coordinator.api.set_select( + self.entity_description.key, self.device[LOCATION], STATE_ON, option + ) diff --git a/tests/components/plugwise/test_select.py b/tests/components/plugwise/test_select.py index 86b21af9e8b..a6245ff11e7 100644 --- a/tests/components/plugwise/test_select.py +++ b/tests/components/plugwise/test_select.py @@ -38,8 +38,9 @@ async def test_adam_change_select_entity( blocking=True, ) - assert mock_smile_adam.set_schedule_state.call_count == 1 - mock_smile_adam.set_schedule_state.assert_called_with( + assert mock_smile_adam.set_select.call_count == 1 + mock_smile_adam.set_select.assert_called_with( + "select_schedule", "c50f167537524366a5af7aa3942feb1e", "on", "Badkamer Schema", @@ -69,5 +70,10 @@ async def test_adam_select_regulation_mode( }, blocking=True, ) - assert mock_smile_adam_3.set_regulation_mode.call_count == 1 - mock_smile_adam_3.set_regulation_mode.assert_called_with("heating") + assert mock_smile_adam_3.set_select.call_count == 1 + mock_smile_adam_3.set_select.assert_called_with( + "select_regulation_mode", + "bc93488efab249e5bc54fd7e175a6f91", + "on", + "heating", + ) From 2c48843739f57122d2b450e8cdb87537fab19b44 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:13:27 +0200 Subject: [PATCH 0201/2411] Force alias when importing device tracker PLATFORM_SCHEMA (#120523) --- homeassistant/components/actiontec/device_tracker.py | 4 ++-- homeassistant/components/aprs/device_tracker.py | 4 ++-- homeassistant/components/arris_tg2492lg/device_tracker.py | 4 ++-- homeassistant/components/aruba/device_tracker.py | 4 ++-- homeassistant/components/bbox/device_tracker.py | 4 ++-- .../components/bluetooth_le_tracker/device_tracker.py | 4 ++-- .../components/bluetooth_tracker/device_tracker.py | 4 ++-- homeassistant/components/bt_home_hub_5/device_tracker.py | 4 ++-- homeassistant/components/bt_smarthub/device_tracker.py | 4 ++-- homeassistant/components/cisco_ios/device_tracker.py | 4 ++-- .../components/cisco_mobility_express/device_tracker.py | 4 ++-- homeassistant/components/cppm_tracker/device_tracker.py | 4 ++-- homeassistant/components/ddwrt/device_tracker.py | 4 ++-- homeassistant/components/fleetgo/device_tracker.py | 4 ++-- homeassistant/components/fortios/device_tracker.py | 4 ++-- homeassistant/components/google_maps/device_tracker.py | 4 ++-- homeassistant/components/hitron_coda/device_tracker.py | 4 ++-- homeassistant/components/linksys_smart/device_tracker.py | 6 ++++-- homeassistant/components/luci/device_tracker.py | 4 ++-- homeassistant/components/meraki/device_tracker.py | 4 ++-- homeassistant/components/mqtt_json/device_tracker.py | 4 ++-- homeassistant/components/quantum_gateway/device_tracker.py | 4 ++-- homeassistant/components/sky_hub/device_tracker.py | 6 ++++-- homeassistant/components/snmp/device_tracker.py | 4 ++-- homeassistant/components/swisscom/device_tracker.py | 4 ++-- homeassistant/components/thomson/device_tracker.py | 4 ++-- homeassistant/components/tomato/device_tracker.py | 4 ++-- homeassistant/components/traccar/device_tracker.py | 4 ++-- homeassistant/components/ubus/device_tracker.py | 4 ++-- homeassistant/components/unifi_direct/device_tracker.py | 4 ++-- homeassistant/components/upc_connect/device_tracker.py | 4 ++-- homeassistant/components/xiaomi/device_tracker.py | 4 ++-- homeassistant/components/xiaomi_miio/device_tracker.py | 4 ++-- 33 files changed, 70 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index 2afa772421c..8cab6552857 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -23,7 +23,7 @@ from .model import Device _LOGGER: Final = logging.getLogger(__name__) -PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index e96494db930..67d0736e526 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -12,7 +12,7 @@ import geopy.distance import voluptuous as vol from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, SeeCallback, ) from homeassistant.const import ( @@ -53,7 +53,7 @@ FILTER_PORT = 14580 MSG_FORMATS = ["compressed", "uncompressed", "mic-e", "object"] -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_CALLSIGNS): cv.ensure_list, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/arris_tg2492lg/device_tracker.py b/homeassistant/components/arris_tg2492lg/device_tracker.py index 3975109e07a..58daead34f2 100644 --- a/homeassistant/components/arris_tg2492lg/device_tracker.py +++ b/homeassistant/components/arris_tg2492lg/device_tracker.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD @@ -19,7 +19,7 @@ from homeassistant.helpers.typing import ConfigType DEFAULT_HOST = "192.168.178.1" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index dd94a5975f0..4959ff7ef03 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -27,7 +27,7 @@ _DEVICES_REGEX = re.compile( r"(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))\s+" ) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index 5413c75d8e7..6ced2c73c9a 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -27,7 +27,7 @@ DEFAULT_HOST = "192.168.1.254" MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string} ) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 1a88e1c5fa3..24b03b2f566 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -13,7 +13,7 @@ from homeassistant.components import bluetooth from homeassistant.components.bluetooth.match import BluetoothCallbackMatcher from homeassistant.components.device_tracker import ( CONF_TRACK_NEW, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, SCAN_INTERVAL, SourceType, ) @@ -42,7 +42,7 @@ DATA_BLE_ADAPTER = "ADAPTER" BLE_PREFIX = "BLE_" MIN_SEEN_NEW = 5 -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TRACK_BATTERY, default=False): cv.boolean, vol.Optional( diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 7cde6f848d5..1d64d31a248 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import ( CONF_SCAN_INTERVAL, CONF_TRACK_NEW, DEFAULT_TRACK_NEW, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, SCAN_INTERVAL, SourceType, ) @@ -41,7 +41,7 @@ from .const import ( _LOGGER: Final = logging.getLogger(__name__) -PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TRACK_NEW): cv.boolean, vol.Optional(CONF_REQUEST_RSSI): cv.boolean, diff --git a/homeassistant/components/bt_home_hub_5/device_tracker.py b/homeassistant/components/bt_home_hub_5/device_tracker.py index 8706a04e7ad..60ded009d5f 100644 --- a/homeassistant/components/bt_home_hub_5/device_tracker.py +++ b/homeassistant/components/bt_home_hub_5/device_tracker.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__) CONF_DEFAULT_IP = "192.168.1.254" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string} ) diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index 8b5411e2014..10c8000fb93 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) CONF_DEFAULT_IP = "192.168.1.254" CONF_SMARTHUB_MODEL = "smarthub_model" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, vol.Optional(CONF_SMARTHUB_MODEL): vol.In([1, 2]), diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py index 8a21b64cb9f..0b76a85424b 100644 --- a/homeassistant/components/cisco_ios/device_tracker.py +++ b/homeassistant/components/cisco_ios/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = vol.All( - PARENT_PLATFORM_SCHEMA.extend( + DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index d96ab54a68f..38d2c78c66a 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py index 9b1ebbb1ed8..a7a1a1b99e8 100644 --- a/homeassistant/components/cppm_tracker/device_tracker.py +++ b/homeassistant/components/cppm_tracker/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_API_KEY, CONF_CLIENT_ID, CONF_HOST @@ -22,7 +22,7 @@ SCAN_INTERVAL = timedelta(seconds=120) GRANT_TYPE = "client_credentials" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_CLIENT_ID): cv.string, diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index 555b6f8ff00..30ab3af53fb 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -35,7 +35,7 @@ DEFAULT_VERIFY_SSL = True CONF_WIRELESS_ONLY = "wireless_only" DEFAULT_WIRELESS_ONLY = True -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/fleetgo/device_tracker.py b/homeassistant/components/fleetgo/device_tracker.py index 3249e8035b4..008c0765c07 100644 --- a/homeassistant/components/fleetgo/device_tracker.py +++ b/homeassistant/components/fleetgo/device_tracker.py @@ -9,7 +9,7 @@ from ritassist import API import voluptuous as vol from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, SeeCallback, ) from homeassistant.const import ( @@ -26,7 +26,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py index 7cc5bab7d16..192c1e4bc69 100644 --- a/homeassistant/components/fortios/device_tracker.py +++ b/homeassistant/components/fortios/device_tracker.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_VERIFY_SSL @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_VERIFY_SSL = False -PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TOKEN): cv.string, diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index b3b0430271a..d703078d198 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -10,7 +10,7 @@ from locationsharinglib.locationsharinglibexceptions import InvalidCookies import voluptuous as vol from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PLATFORM_SCHEMA_BASE, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, SeeCallback, SourceType, ) @@ -40,7 +40,7 @@ CREDENTIALS_FILE = ".google_maps_location_sharing.cookies" # the parent "device_tracker" have marked the schemas as legacy, so this # need to be refactored as part of a bigger rewrite. -PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_MAX_GPS_ACCURACY, default=100000): vol.Coerce(float), diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index dec15e25b0b..68d93e9719d 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TYPE, CONF_USERNAME @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_TYPE = "rogers" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/linksys_smart/device_tracker.py b/homeassistant/components/linksys_smart/device_tracker.py index a33f0070c70..45ae1d328dd 100644 --- a/homeassistant/components/linksys_smart/device_tracker.py +++ b/homeassistant/components/linksys_smart/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -22,7 +22,9 @@ DEFAULT_TIMEOUT = 10 _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_HOST): cv.string} +) def get_scanner( diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index 183f383e7e4..59d4d12ddf6 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py index a6eefe7345f..95ed2ba9089 100644 --- a/homeassistant/components/meraki/device_tracker.py +++ b/homeassistant/components/meraki/device_tracker.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, AsyncSeeCallback, SourceType, ) @@ -27,7 +27,7 @@ ACCEPTED_VERSIONS = ["2.0", "2.1"] _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( {vol.Required(CONF_VALIDATOR): cv.string, vol.Required(CONF_SECRET): cv.string} ) diff --git a/homeassistant/components/mqtt_json/device_tracker.py b/homeassistant/components/mqtt_json/device_tracker.py index 68f42479930..3200da56cf6 100644 --- a/homeassistant/components/mqtt_json/device_tracker.py +++ b/homeassistant/components/mqtt_json/device_tracker.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, AsyncSeeCallback, ) from homeassistant.components.mqtt import CONF_QOS @@ -36,7 +36,7 @@ GPS_JSON_PAYLOAD_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(mqtt.config.SCHEMA_BASE).extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend(mqtt.config.SCHEMA_BASE).extend( {vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}} ) diff --git a/homeassistant/components/quantum_gateway/device_tracker.py b/homeassistant/components/quantum_gateway/device_tracker.py index 1c43cbb14a8..88cb5d60028 100644 --- a/homeassistant/components/quantum_gateway/device_tracker.py +++ b/homeassistant/components/quantum_gateway/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SSL @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_HOST = "myfiosgateway.com" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_SSL, default=True): cv.boolean, diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index bc4a0fdc743..140a174cc97 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -20,7 +20,9 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_HOST): cv.string} +) async def async_get_scanner( diff --git a/homeassistant/components/snmp/device_tracker.py b/homeassistant/components/snmp/device_tracker.py index d336838117f..91baa8f6b4c 100644 --- a/homeassistant/components/snmp/device_tracker.py +++ b/homeassistant/components/snmp/device_tracker.py @@ -19,7 +19,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -44,7 +44,7 @@ from .util import RequestArgsType, async_create_request_cmd_args _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_BASEOID): cv.string, vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index cd393c79e09..c13e5a322aa 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_IP = "192.168.1.1" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string} ) diff --git a/homeassistant/components/thomson/device_tracker.py b/homeassistant/components/thomson/device_tracker.py index 544260a1e34..339b12f0dc9 100644 --- a/homeassistant/components/thomson/device_tracker.py +++ b/homeassistant/components/thomson/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -30,7 +30,7 @@ _DEVICES_REGEX = re.compile( r"(?P([^\s]+))" ) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py index d28fa505c61..aaa1d10d08d 100644 --- a/homeassistant/components/tomato/device_tracker.py +++ b/homeassistant/components/tomato/device_tracker.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -31,7 +31,7 @@ CONF_HTTP_ID = "http_id" _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT): cv.port, diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 5695e434eff..468d2fd4d05 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -10,7 +10,7 @@ from pytraccar import ApiClient, TraccarException import voluptuous as vol from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, AsyncSeeCallback, SourceType, TrackerEntity, @@ -104,7 +104,7 @@ EVENTS = [ EVENT_ALL_EVENTS, ] -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/ubus/device_tracker.py b/homeassistant/components/ubus/device_tracker.py index b728059d0be..6170ad213a3 100644 --- a/homeassistant/components/ubus/device_tracker.py +++ b/homeassistant/components/ubus/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -24,7 +24,7 @@ CONF_DHCP_SOFTWARE = "dhcp_software" DEFAULT_DHCP_SOFTWARE = "dnsmasq" DHCP_SOFTWARES = ["dnsmasq", "odhcpd", "none"] -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/unifi_direct/device_tracker.py b/homeassistant/components/unifi_direct/device_tracker.py index 51c9c412dad..c2cb9eba632 100644 --- a/homeassistant/components/unifi_direct/device_tracker.py +++ b/homeassistant/components/unifi_direct/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SSH_PORT = 22 -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/upc_connect/device_tracker.py b/homeassistant/components/upc_connect/device_tracker.py index 9e570c9d26b..1ec6dcd3107 100644 --- a/homeassistant/components/upc_connect/device_tracker.py +++ b/homeassistant/components/upc_connect/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_IP = "192.168.0.1" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string, diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index 869a7a1cf1f..b3983e76aaa 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME, default="admin"): cv.string, diff --git a/homeassistant/components/xiaomi_miio/device_tracker.py b/homeassistant/components/xiaomi_miio/device_tracker.py index ba73ccc57f0..9acdb1cc53e 100644 --- a/homeassistant/components/xiaomi_miio/device_tracker.py +++ b/homeassistant/components/xiaomi_miio/device_tracker.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_TOKEN @@ -19,7 +19,7 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), From d589eaf440f614024db2f05110145c0e30a6ffbc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 11:23:26 +0200 Subject: [PATCH 0202/2411] Simplify EVENT_STATE_REPORTED (#120508) --- homeassistant/const.py | 4 ++-- homeassistant/core.py | 38 +++++++++++++++++--------------------- tests/test_core.py | 8 ++++---- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 577e8df6f39..3a970aefd38 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.hass_dict import HassKey from .util.signal_type import SignalType if TYPE_CHECKING: - from .core import EventStateChangedData + from .core import EventStateChangedData, EventStateReportedData from .helpers.typing import NoEventData APPLICATION_NAME: Final = "HomeAssistant" @@ -321,7 +321,7 @@ EVENT_LOGGING_CHANGED: Final = "logging_changed" EVENT_SERVICE_REGISTERED: Final = "service_registered" EVENT_SERVICE_REMOVED: Final = "service_removed" EVENT_STATE_CHANGED: EventType[EventStateChangedData] = EventType("state_changed") -EVENT_STATE_REPORTED: Final = "state_reported" +EVENT_STATE_REPORTED: EventType[EventStateReportedData] = EventType("state_reported") EVENT_THEMES_UPDATED: Final = "themes_updated" EVENT_PANELS_UPDATED: Final = "panels_updated" EVENT_LOVELACE_UPDATED: Final = "lovelace_updated" diff --git a/homeassistant/core.py b/homeassistant/core.py index f114049b2b2..2b43b2d40ff 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -159,13 +159,27 @@ class ConfigSource(enum.StrEnum): class EventStateChangedData(TypedDict): - """EventStateChanged data.""" + """EVENT_STATE_CHANGED data. + + A state changed event is fired when on state write when the state is changed. + """ entity_id: str old_state: State | None new_state: State | None +class EventStateReportedData(TypedDict): + """EVENT_STATE_REPORTED data. + + A state reported event is fired when on state write when the state is unchanged. + """ + + entity_id: str + old_last_reported: datetime.datetime + new_state: State | None + + # SOURCE_* are deprecated as of Home Assistant 2022.2, use ConfigSource instead _DEPRECATED_SOURCE_DISCOVERED = DeprecatedConstantEnum( ConfigSource.DISCOVERED, "2025.1" @@ -1604,27 +1618,8 @@ class EventBus: raise HomeAssistantError( f"Event filter is required for event {event_type}" ) - # Special case for EVENT_STATE_REPORTED, we also want to listen to - # EVENT_STATE_CHANGED - self._listeners[EVENT_STATE_REPORTED].append(filterable_job) - self._listeners[EVENT_STATE_CHANGED].append(filterable_job) - return functools.partial( - self._async_remove_multiple_listeners, - (EVENT_STATE_REPORTED, EVENT_STATE_CHANGED), - filterable_job, - ) return self._async_listen_filterable_job(event_type, filterable_job) - @callback - def _async_remove_multiple_listeners( - self, - keys: Iterable[EventType[_DataT] | str], - filterable_job: _FilterableJobType[Any], - ) -> None: - """Remove multiple listeners for specific event_types.""" - for key in keys: - self._async_remove_listener(key, filterable_job) - @callback def _async_listen_filterable_job( self, @@ -2278,7 +2273,8 @@ class StateMachine: old_last_reported = old_state.last_reported # type: ignore[union-attr] old_state.last_reported = now # type: ignore[union-attr] old_state.last_reported_timestamp = timestamp # type: ignore[union-attr] - self._bus.async_fire_internal( + # Avoid creating an EventStateReportedData + self._bus.async_fire_internal( # type: ignore[misc] EVENT_STATE_REPORTED, { "entity_id": entity_id, diff --git a/tests/test_core.py b/tests/test_core.py index a1748638342..5e6b51cc39e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3385,24 +3385,24 @@ async def test_statemachine_report_state(hass: HomeAssistant) -> None: hass.states.async_set("light.bowl", "on", None, True) await hass.async_block_till_done() assert len(state_changed_events) == 1 - assert len(state_reported_events) == 2 + assert len(state_reported_events) == 1 hass.states.async_set("light.bowl", "off") await hass.async_block_till_done() assert len(state_changed_events) == 2 - assert len(state_reported_events) == 3 + assert len(state_reported_events) == 1 hass.states.async_remove("light.bowl") await hass.async_block_till_done() assert len(state_changed_events) == 3 - assert len(state_reported_events) == 4 + assert len(state_reported_events) == 1 unsub() hass.states.async_set("light.bowl", "on") await hass.async_block_till_done() assert len(state_changed_events) == 4 - assert len(state_reported_events) == 4 + assert len(state_reported_events) == 1 async def test_report_state_listener_restrictions(hass: HomeAssistant) -> None: From 59ae297ccd1ce888fa7dc54a29328c160c5588ad Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:24:21 +0200 Subject: [PATCH 0203/2411] Force alias when importing humidifier PLATFORM_SCHEMA (#120526) --- homeassistant/components/generic_hygrostat/humidifier.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index c22904a4caa..a1f9936fa33 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -12,7 +12,7 @@ from homeassistant.components.humidifier import ( ATTR_HUMIDITY, MODE_AWAY, MODE_NORMAL, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as HUMIDIFIER_PLATFORM_SCHEMA, HumidifierAction, HumidifierDeviceClass, HumidifierEntity, @@ -72,7 +72,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_SAVED_HUMIDITY = "saved_humidity" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(HYGROSTAT_SCHEMA.schema) +PLATFORM_SCHEMA = HUMIDIFIER_PLATFORM_SCHEMA.extend(HYGROSTAT_SCHEMA.schema) async def async_setup_platform( From 17946c4b45e3a4ceb72cc5e14bb54708ae029ee4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:24:45 +0200 Subject: [PATCH 0204/2411] Force alias when importing geo location PLATFORM_SCHEMA (#120525) --- homeassistant/components/ign_sismologia/geo_location.py | 7 +++++-- .../components/nsw_rural_fire_service_feed/geo_location.py | 7 +++++-- homeassistant/components/qld_bushfire/geo_location.py | 7 +++++-- .../components/usgs_earthquakes_feed/geo_location.py | 7 +++++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index af7fab5b79b..779891f4bc2 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -13,7 +13,10 @@ from georss_ign_sismologia_client import ( ) import voluptuous as vol -from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent +from homeassistant.components.geo_location import ( + PLATFORM_SCHEMA as GEO_LOCATION_PLATFORM_SCHEMA, + GeolocationEvent, +) from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -47,7 +50,7 @@ SCAN_INTERVAL = timedelta(minutes=5) SOURCE = "ign_sismologia" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = GEO_LOCATION_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, 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 24bae7f7b12..230141379e5 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -13,7 +13,10 @@ from aio_geojson_nsw_rfs_incidents.feed_entry import ( ) import voluptuous as vol -from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent +from homeassistant.components.geo_location import ( + PLATFORM_SCHEMA as GEO_LOCATION_PLATFORM_SCHEMA, + GeolocationEvent, +) from homeassistant.const import ( ATTR_LOCATION, CONF_LATITUDE, @@ -59,7 +62,7 @@ SOURCE = "nsw_rural_fire_service_feed" VALID_CATEGORIES = ["Advice", "Emergency Warning", "Not Applicable", "Watch and Act"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = GEO_LOCATION_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_CATEGORIES, default=[]): vol.All( cv.ensure_list, [vol.In(VALID_CATEGORIES)] diff --git a/homeassistant/components/qld_bushfire/geo_location.py b/homeassistant/components/qld_bushfire/geo_location.py index 5d0173f8c54..c8cfc30b2b5 100644 --- a/homeassistant/components/qld_bushfire/geo_location.py +++ b/homeassistant/components/qld_bushfire/geo_location.py @@ -13,7 +13,10 @@ from georss_qld_bushfire_alert_client import ( ) import voluptuous as vol -from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent +from homeassistant.components.geo_location import ( + PLATFORM_SCHEMA as GEO_LOCATION_PLATFORM_SCHEMA, + GeolocationEvent, +) from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -56,7 +59,7 @@ VALID_CATEGORIES = [ "Information", ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = GEO_LOCATION_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index c8ee88a84ed..33455dc11a9 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -13,7 +13,10 @@ from aio_geojson_usgs_earthquakes.feed_entry import ( ) import voluptuous as vol -from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent +from homeassistant.components.geo_location import ( + PLATFORM_SCHEMA as GEO_LOCATION_PLATFORM_SCHEMA, + GeolocationEvent, +) from homeassistant.const import ( ATTR_TIME, CONF_LATITUDE, @@ -81,7 +84,7 @@ VALID_FEED_TYPES = [ "past_month_all_earthquakes", ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = GEO_LOCATION_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FEED_TYPE): vol.In(VALID_FEED_TYPES), vol.Optional(CONF_LATITUDE): cv.latitude, From 2c17d84fab61eed6f00cc79c2cec89ce0c2ac024 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:25:14 +0200 Subject: [PATCH 0205/2411] Force alias when importing cover PLATFORM_SCHEMA (#120522) --- homeassistant/components/ads/cover.py | 4 ++-- homeassistant/components/garadget/cover.py | 4 ++-- homeassistant/components/group/cover.py | 4 ++-- homeassistant/components/rflink/cover.py | 7 +++++-- homeassistant/components/scsgate/cover.py | 7 +++++-- homeassistant/components/template/cover.py | 4 ++-- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ads/cover.py b/homeassistant/components/ads/cover.py index c54b3e14267..b0dded8d4d5 100644 --- a/homeassistant/components/ads/cover.py +++ b/homeassistant/components/ads/cover.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.cover import ( ATTR_POSITION, DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, CoverEntity, CoverEntityFeature, ) @@ -36,7 +36,7 @@ CONF_ADS_VAR_OPEN = "adsvar_open" CONF_ADS_VAR_CLOSE = "adsvar_close" CONF_ADS_VAR_STOP = "adsvar_stop" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_ADS_VAR): cv.string, vol.Optional(CONF_ADS_VAR_POSITION): cv.string, diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index f168652b3cf..cb4f402d7bb 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -9,7 +9,7 @@ import requests import voluptuous as vol from homeassistant.components.cover import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, CoverDeviceClass, CoverEntity, ) @@ -61,7 +61,7 @@ COVER_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( {vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA)} ) diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 02e5ebbc7cd..5d7f99012fd 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -12,7 +12,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, CoverEntity, CoverEntityFeature, ) @@ -55,7 +55,7 @@ DEFAULT_NAME = "Cover Group" # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py index d440b324532..54a84a68a2e 100644 --- a/homeassistant/components/rflink/cover.py +++ b/homeassistant/components/rflink/cover.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity +from homeassistant.components.cover import ( + PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, + CoverEntity, +) from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_TYPE, STATE_OPEN from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -34,7 +37,7 @@ PARALLEL_UPDATES = 0 TYPE_STANDARD = "standard" TYPE_INVERTED = "inverted" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({}) diff --git a/homeassistant/components/scsgate/cover.py b/homeassistant/components/scsgate/cover.py index 8f17ca170a0..b6d3317555c 100644 --- a/homeassistant/components/scsgate/cover.py +++ b/homeassistant/components/scsgate/cover.py @@ -12,7 +12,10 @@ from scsgate.tasks import ( ) import voluptuous as vol -from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity +from homeassistant.components.cover import ( + PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, + CoverEntity, +) from homeassistant.const import CONF_DEVICES, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -21,7 +24,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import CONF_SCS_ID, DOMAIN, SCSGATE_SCHEMA -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( {vol.Required(CONF_DEVICES): cv.schema_with_slug_keys(SCSGATE_SCHEMA)} ) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 36ea9f93830..d50067f6278 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -12,7 +12,7 @@ from homeassistant.components.cover import ( ATTR_TILT_POSITION, DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, CoverEntity, CoverEntityFeature, ) @@ -96,7 +96,7 @@ COVER_SCHEMA = vol.All( cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( {vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA)} ) From afbd24adfe05c46a071701b21d0dc08fdaec1854 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:29:52 +0200 Subject: [PATCH 0206/2411] Force alias when importing image processing PLATFORM_SCHEMA (#120527) --- .../components/dlib_face_identify/image_processing.py | 4 ++-- homeassistant/components/doods/image_processing.py | 4 ++-- .../components/microsoft_face_detect/image_processing.py | 4 ++-- .../components/microsoft_face_identify/image_processing.py | 6 ++++-- homeassistant/components/openalpr_cloud/image_processing.py | 4 ++-- homeassistant/components/seven_segments/image_processing.py | 4 ++-- homeassistant/components/sighthound/image_processing.py | 4 ++-- homeassistant/components/tensorflow/image_processing.py | 4 ++-- 8 files changed, 18 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index ac9e69ec9e1..e17f892a7fe 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.image_processing import ( CONF_CONFIDENCE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingFaceEntity, ) from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) CONF_FACES = "faces" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FACES): {cv.string: cv.isfile}, vol.Optional(CONF_CONFIDENCE, default=0.6): vol.Coerce(float), diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 11985ef4889..7ffb6655bb6 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.image_processing import ( CONF_CONFIDENCE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingEntity, ) from homeassistant.const import ( @@ -66,7 +66,7 @@ LABEL_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { vol.Required(CONF_URL): cv.string, vol.Required(CONF_DETECTOR): cv.string, diff --git a/homeassistant/components/microsoft_face_detect/image_processing.py b/homeassistant/components/microsoft_face_detect/image_processing.py index ef8a4f5df4b..80037a29fa8 100644 --- a/homeassistant/components/microsoft_face_detect/image_processing.py +++ b/homeassistant/components/microsoft_face_detect/image_processing.py @@ -10,7 +10,7 @@ from homeassistant.components.image_processing import ( ATTR_AGE, ATTR_GENDER, ATTR_GLASSES, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingFaceEntity, ) from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE @@ -37,7 +37,7 @@ def validate_attributes(list_attributes): return list_attributes -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_ATTRIBUTES, default=DEFAULT_ATTRIBUTES): vol.All( cv.ensure_list, validate_attributes diff --git a/homeassistant/components/microsoft_face_identify/image_processing.py b/homeassistant/components/microsoft_face_identify/image_processing.py index d1af1d4a827..03a6ad22fcd 100644 --- a/homeassistant/components/microsoft_face_identify/image_processing.py +++ b/homeassistant/components/microsoft_face_identify/image_processing.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.image_processing import ( ATTR_CONFIDENCE, CONF_CONFIDENCE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingFaceEntity, ) from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE @@ -24,7 +24,9 @@ _LOGGER = logging.getLogger(__name__) CONF_GROUP = "group" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_GROUP): cv.slugify}) +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_GROUP): cv.slugify} +) async def async_setup_platform( diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index 2a8fe328c7d..e8a8d6859c1 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.image_processing import ( ATTR_CONFIDENCE, CONF_CONFIDENCE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingDeviceClass, ImageProcessingEntity, ) @@ -57,7 +57,7 @@ OPENALPR_REGIONS = [ "vn2", ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_REGION): vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)), diff --git a/homeassistant/components/seven_segments/image_processing.py b/homeassistant/components/seven_segments/image_processing.py index 622ceb761a0..7b41a1702c0 100644 --- a/homeassistant/components/seven_segments/image_processing.py +++ b/homeassistant/components/seven_segments/image_processing.py @@ -11,7 +11,7 @@ from PIL import Image import voluptuous as vol from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingDeviceClass, ImageProcessingEntity, ) @@ -35,7 +35,7 @@ CONF_Y_POS = "y_position" DEFAULT_BINARY = "ssocr" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_EXTRA_ARGUMENTS, default=""): cv.string, vol.Optional(CONF_DIGITS): cv.positive_int, diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py index bcfa4bca3c2..706a8dd037a 100644 --- a/homeassistant/components/sighthound/image_processing.py +++ b/homeassistant/components/sighthound/image_processing.py @@ -11,7 +11,7 @@ import simplehound.core as hound import voluptuous as vol from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingEntity, ) from homeassistant.const import ( @@ -41,7 +41,7 @@ DATETIME_FORMAT = "%Y-%m-%d_%H:%M:%S" DEV = "dev" PROD = "prod" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_ACCOUNT_TYPE, default=DEV): vol.In([DEV, PROD]), diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index c78c2bc2312..85fe6439f1c 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -15,7 +15,7 @@ import voluptuous as vol from homeassistant.components.image_processing import ( CONF_CONFIDENCE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, ImageProcessingEntity, ) from homeassistant.const import ( @@ -68,7 +68,7 @@ CATEGORY_SCHEMA = vol.Schema( {vol.Required(CONF_CATEGORY): cv.string, vol.Optional(CONF_AREA): AREA_SCHEMA} ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_FILE_OUT, default=[]): vol.All(cv.ensure_list, [cv.template]), vol.Required(CONF_MODEL): vol.Schema( From d527113d5992ab95c75fc8bd81b3d9a6ff067be7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:30:07 +0200 Subject: [PATCH 0207/2411] Improve schema typing (3) (#120521) --- .../components/input_button/__init__.py | 4 ++-- homeassistant/components/input_text/__init__.py | 4 ++-- homeassistant/components/light/__init__.py | 6 ++++-- .../components/motioneye/config_flow.py | 8 +++++--- homeassistant/components/zha/device_action.py | 5 +++-- .../components/zwave_js/triggers/event.py | 6 ++---- homeassistant/data_entry_flow.py | 9 +++++---- homeassistant/helpers/config_validation.py | 12 ++++++------ homeassistant/helpers/intent.py | 17 ++++++++++------- .../helpers/schema_config_entry_flow.py | 8 +++++--- 10 files changed, 44 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/input_button/__init__.py b/homeassistant/components/input_button/__init__.py index e70bbacd933..47ec36969c6 100644 --- a/homeassistant/components/input_button/__init__.py +++ b/homeassistant/components/input_button/__init__.py @@ -58,9 +58,9 @@ class InputButtonStorageCollection(collection.DictStorageCollection): CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS) - async def _process_create_data(self, data: dict) -> vol.Schema: + async def _process_create_data(self, data: dict) -> dict[str, str]: """Validate the config is valid.""" - return self.CREATE_UPDATE_SCHEMA(data) + return self.CREATE_UPDATE_SCHEMA(data) # type: ignore[no-any-return] @callback def _get_suggested_id(self, info: dict) -> str: diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 3d75ff9f5c2..7d8f6663673 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -163,9 +163,9 @@ class InputTextStorageCollection(collection.DictStorageCollection): CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_text)) - async def _process_create_data(self, data: dict[str, Any]) -> vol.Schema: + async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]: """Validate the config is valid.""" - return self.CREATE_UPDATE_SCHEMA(data) + return self.CREATE_UPDATE_SCHEMA(data) # type: ignore[no-any-return] @callback def _get_suggested_id(self, info: dict[str, Any]) -> str: diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 16367c35ec5..b61625edaf2 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -302,7 +302,7 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: def preprocess_turn_on_alternatives( - hass: HomeAssistant, params: dict[str, Any] + hass: HomeAssistant, params: dict[str, Any] | dict[str | vol.Optional, Any] ) -> None: """Process extra data for turn light on request. @@ -406,7 +406,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # of the light base platform. hass.async_create_task(profiles.async_initialize(), eager_start=True) - def preprocess_data(data: dict[str, Any]) -> dict[str | vol.Optional, Any]: + def preprocess_data( + data: dict[str | vol.Optional, Any], + ) -> dict[str | vol.Optional, Any]: """Preprocess the service data.""" base: dict[str | vol.Optional, Any] = { entity_field: data.pop(entity_field) diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index bbbd2bc7fba..49059b528db 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -226,14 +226,16 @@ class MotionEyeOptionsFlow(OptionsFlow): if self.show_advanced_options: # The input URL is not validated as being a URL, to allow for the possibility # the template input won't be a valid URL until after it's rendered - stream_kwargs = {} + description: dict[str, str] | None = None if CONF_STREAM_URL_TEMPLATE in self._config_entry.options: - stream_kwargs["description"] = { + description = { "suggested_value": self._config_entry.options[ CONF_STREAM_URL_TEMPLATE ] } - schema[vol.Optional(CONF_STREAM_URL_TEMPLATE, **stream_kwargs)] = str + schema[vol.Optional(CONF_STREAM_URL_TEMPLATE, description=description)] = ( + str + ) return self.async_show_form(step_id="init", data_schema=vol.Schema(schema)) diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 8f5a03a7fe5..a0f16d61f41 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -167,8 +167,9 @@ async def async_get_action_capabilities( hass: HomeAssistant, config: ConfigType ) -> dict[str, vol.Schema]: """List action capabilities.""" - - return {"extra_fields": DEVICE_ACTION_SCHEMAS.get(config[CONF_TYPE], {})} + if (fields := DEVICE_ACTION_SCHEMAS.get(config[CONF_TYPE])) is None: + return {} + return {"extra_fields": fields} async def _execute_service_based_action( diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index 921cae19b3a..9938d08408c 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -80,10 +80,8 @@ def validate_event_data(obj: dict) -> dict: except ValidationError as exc: # Filter out required field errors if keys can be missing, and if there are # still errors, raise an exception - if errors := [ - error for error in exc.errors() if error["type"] != "value_error.missing" - ]: - raise vol.MultipleInvalid(errors) from exc + if [error for error in exc.errors() if error["type"] != "value_error.missing"]: + raise vol.MultipleInvalid from exc return obj diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 155e64d259e..f632e3e4dde 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -5,7 +5,7 @@ from __future__ import annotations import abc import asyncio from collections import defaultdict -from collections.abc import Callable, Container, Iterable, Mapping +from collections.abc import Callable, Container, Hashable, Iterable, Mapping from contextlib import suppress import copy from dataclasses import dataclass @@ -13,7 +13,7 @@ from enum import StrEnum from functools import partial import logging from types import MappingProxyType -from typing import Any, Generic, Required, TypedDict +from typing import Any, Generic, Required, TypedDict, cast from typing_extensions import TypeVar import voluptuous as vol @@ -120,7 +120,7 @@ class InvalidData(vol.Invalid): # type: ignore[misc] def __init__( self, message: str, - path: list[str | vol.Marker] | None, + path: list[Hashable] | None, error_message: str | None, schema_errors: dict[str, Any], **kwargs: Any, @@ -384,6 +384,7 @@ class FlowManager(abc.ABC, Generic[_FlowResultT, _HandlerT]): if ( data_schema := cur_step.get("data_schema") ) is not None and user_input is not None: + data_schema = cast(vol.Schema, data_schema) try: user_input = data_schema(user_input) # type: ignore[operator] except vol.Invalid as ex: @@ -694,7 +695,7 @@ class FlowHandler(Generic[_FlowResultT, _HandlerT]): ): # Copy the marker to not modify the flow schema new_key = copy.copy(key) - new_key.description = {"suggested_value": suggested_values[key]} + new_key.description = {"suggested_value": suggested_values[key.schema]} schema[new_key] = val return vol.Schema(schema) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 558baaeb779..58c76a40c8e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -981,7 +981,7 @@ def removed( def key_value_schemas( key: str, - value_schemas: dict[Hashable, VolSchemaType], + value_schemas: dict[Hashable, VolSchemaType | Callable[[Any], dict[str, Any]]], default_schema: VolSchemaType | None = None, default_description: str | None = None, ) -> Callable[[Any], dict[Hashable, Any]]: @@ -1016,12 +1016,12 @@ def key_value_schemas( # Validator helpers -def key_dependency( +def key_dependency[_KT: Hashable, _VT]( key: Hashable, dependency: Hashable -) -> Callable[[dict[Hashable, Any]], dict[Hashable, Any]]: +) -> Callable[[dict[_KT, _VT]], dict[_KT, _VT]]: """Validate that all dependencies exist for key.""" - def validator(value: dict[Hashable, Any]) -> dict[Hashable, Any]: + def validator(value: dict[_KT, _VT]) -> dict[_KT, _VT]: """Test dependencies.""" if not isinstance(value, dict): raise vol.Invalid("key dependencies require a dict") @@ -1405,13 +1405,13 @@ STATE_CONDITION_ATTRIBUTE_SCHEMA = vol.Schema( ) -def STATE_CONDITION_SCHEMA(value: Any) -> dict: +def STATE_CONDITION_SCHEMA(value: Any) -> dict[str, Any]: """Validate a state condition.""" if not isinstance(value, dict): raise vol.Invalid("Expected a dictionary") if CONF_ATTRIBUTE in value: - validated: dict = STATE_CONDITION_ATTRIBUTE_SCHEMA(value) + validated: dict[str, Any] = STATE_CONDITION_ATTRIBUTE_SCHEMA(value) else: validated = STATE_CONDITION_STATE_SCHEMA(value) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index e191bddf102..1bf78ae3a29 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import abstractmethod import asyncio -from collections.abc import Collection, Coroutine, Iterable +from collections.abc import Callable, Collection, Coroutine, Iterable import dataclasses from dataclasses import dataclass, field from enum import Enum, auto @@ -37,6 +37,9 @@ from .typing import VolSchemaType _LOGGER = logging.getLogger(__name__) type _SlotsType = dict[str, Any] +type _IntentSlotsType = dict[ + str | tuple[str, str], VolSchemaType | Callable[[Any], Any] +] INTENT_TURN_OFF = "HassTurnOff" INTENT_TURN_ON = "HassTurnOn" @@ -808,8 +811,8 @@ class DynamicServiceIntentHandler(IntentHandler): self, intent_type: str, speech: str | None = None, - required_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, - optional_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, + required_slots: _IntentSlotsType | None = None, + optional_slots: _IntentSlotsType | None = None, required_domains: set[str] | None = None, required_features: int | None = None, required_states: set[str] | None = None, @@ -825,7 +828,7 @@ class DynamicServiceIntentHandler(IntentHandler): self.description = description self.platforms = platforms - self.required_slots: dict[tuple[str, str], VolSchemaType] = {} + self.required_slots: _IntentSlotsType = {} if required_slots: for key, value_schema in required_slots.items(): if isinstance(key, str): @@ -834,7 +837,7 @@ class DynamicServiceIntentHandler(IntentHandler): self.required_slots[key] = value_schema - self.optional_slots: dict[tuple[str, str], VolSchemaType] = {} + self.optional_slots: _IntentSlotsType = {} if optional_slots: for key, value_schema in optional_slots.items(): if isinstance(key, str): @@ -1108,8 +1111,8 @@ class ServiceIntentHandler(DynamicServiceIntentHandler): domain: str, service: str, speech: str | None = None, - required_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, - optional_slots: dict[str | tuple[str, str], VolSchemaType] | None = None, + required_slots: _IntentSlotsType | None = None, + optional_slots: _IntentSlotsType | None = None, required_domains: set[str] | None = None, required_features: int | None = None, required_states: set[str] | None = None, diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 05e4a852ad9..7463c9945b2 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -175,7 +175,9 @@ class SchemaCommonFlowHandler: and key.default is not vol.UNDEFINED and key not in self._options ): - user_input[str(key.schema)] = key.default() + user_input[str(key.schema)] = cast( + Callable[[], Any], key.default + )() if user_input is not None and form_step.validate_user_input is not None: # Do extra validation of user input @@ -215,7 +217,7 @@ class SchemaCommonFlowHandler: ) ): # Key not present, delete keys old value (if present) too - values.pop(key, None) + values.pop(key.schema, None) async def _show_next_step_or_create_entry( self, form_step: SchemaFlowFormStep @@ -491,7 +493,7 @@ def wrapped_entity_config_entry_title( def entity_selector_without_own_entities( handler: SchemaOptionsFlowHandler, entity_selector_config: selector.EntitySelectorConfig, -) -> vol.Schema: +) -> selector.EntitySelector: """Return an entity selector which excludes own entities.""" entity_registry = er.async_get(handler.hass) entities = er.async_entries_for_config_entry( From ae73500bebe759d22652486cb23960deb4ca8ab1 Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:33:44 +0200 Subject: [PATCH 0208/2411] Add HmIP-ESI (#116863) --- .../homematicip_cloud/generic_entity.py | 26 +- .../components/homematicip_cloud/helpers.py | 12 + .../components/homematicip_cloud/sensor.py | 168 ++++++- .../fixtures/homematicip_cloud.json | 410 ++++++++++++++++++ .../homematicip_cloud/test_device.py | 2 +- .../homematicip_cloud/test_sensor.py | 123 ++++++ 6 files changed, 733 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index a2e6f8a145f..5cd48515ad7 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -7,6 +7,7 @@ from typing import Any from homematicip.aio.device import AsyncDevice from homematicip.aio.group import AsyncGroup +from homematicip.base.functionalChannels import FunctionalChannel from homeassistant.const import ATTR_ID from homeassistant.core import callback @@ -91,6 +92,7 @@ class HomematicipGenericEntity(Entity): self._post = post self._channel = channel self._is_multi_channel = is_multi_channel + self.functional_channel = self.get_current_channel() # Marker showing that the HmIP device hase been removed. self.hmip_device_removed = False _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) @@ -214,13 +216,14 @@ class HomematicipGenericEntity(Entity): @property def unique_id(self) -> str: """Return a unique ID.""" - unique_id = f"{self.__class__.__name__}_{self._device.id}" - if self._is_multi_channel: - unique_id = ( - f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}" - ) + suffix = "" + if self._post is not None: + suffix = f"_{self._post}" - return unique_id + if self._is_multi_channel: + return f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}{suffix}" + + return f"{self.__class__.__name__}_{self._device.id}{suffix}" @property def icon(self) -> str | None: @@ -251,3 +254,14 @@ class HomematicipGenericEntity(Entity): state_attr[ATTR_IS_GROUP] = True return state_attr + + def get_current_channel(self) -> FunctionalChannel: + """Return the FunctionalChannel for device.""" + if hasattr(self._device, "functionalChannels"): + if self._is_multi_channel: + return self._device.functionalChannels[self._channel] + + if len(self._device.functionalChannels) > 1: + return self._device.functionalChannels[1] + + return None diff --git a/homeassistant/components/homematicip_cloud/helpers.py b/homeassistant/components/homematicip_cloud/helpers.py index 4ac9af48ee1..5b7f98ad884 100644 --- a/homeassistant/components/homematicip_cloud/helpers.py +++ b/homeassistant/components/homematicip_cloud/helpers.py @@ -8,6 +8,9 @@ import json import logging from typing import Any, Concatenate, TypeGuard +from homematicip.base.enums import FunctionalChannelType +from homematicip.device import Device + from homeassistant.exceptions import HomeAssistantError from . import HomematicipGenericEntity @@ -47,3 +50,12 @@ def handle_errors[_HomematicipGenericEntityT: HomematicipGenericEntity, **_P]( ) return inner + + +def get_channels_from_device(device: Device, channel_type: FunctionalChannelType): + """Get all channels matching with channel_type from device.""" + return [ + ch + for ch in device.functionalChannels + if ch.functionalChannelType == channel_type + ] diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index d344639bbc9..6cdff6caef3 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -2,10 +2,13 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from typing import Any from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, + AsyncEnergySensorsInterface, AsyncFullFlushSwitchMeasuring, AsyncHeatingThermostat, AsyncHeatingThermostatCompact, @@ -27,11 +30,13 @@ from homematicip.aio.device import ( AsyncWeatherSensorPlus, AsyncWeatherSensorPro, ) -from homematicip.base.enums import ValveState +from homematicip.base.enums import FunctionalChannelType, ValveState +from homematicip.base.functionalChannels import FunctionalChannel from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry @@ -43,12 +48,16 @@ from homeassistant.const import ( UnitOfPrecipitationDepth, UnitOfSpeed, UnitOfTemperature, + UnitOfVolume, + UnitOfVolumeFlowRate, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP +from .helpers import get_channels_from_device ATTR_CURRENT_ILLUMINATION = "current_illumination" ATTR_LOWEST_ILLUMINATION = "lowest_illumination" @@ -58,6 +67,18 @@ ATTR_RIGHT_COUNTER = "right_counter" ATTR_TEMPERATURE_OFFSET = "temperature_offset" ATTR_WIND_DIRECTION = "wind_direction" ATTR_WIND_DIRECTION_VARIATION = "wind_direction_variation_in_degree" +ATTR_ESI_TYPE = "type" +ESI_TYPE_UNKNOWN = "UNKNOWN" +ESI_CONNECTED_SENSOR_TYPE_IEC = "ES_IEC" +ESI_CONNECTED_SENSOR_TYPE_GAS = "ES_GAS" +ESI_CONNECTED_SENSOR_TYPE_LED = "ES_LED" + +ESI_TYPE_CURRENT_POWER_CONSUMPTION = "CurrentPowerConsumption" +ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF = "ENERGY_COUNTER_USAGE_HIGH_TARIFF" +ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF = "ENERGY_COUNTER_USAGE_LOW_TARIFF" +ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF = "ENERGY_COUNTER_INPUT_SINGLE_TARIFF" +ESI_TYPE_CURRENT_GAS_FLOW = "CurrentGasFlow" +ESI_TYPE_CURRENT_GAS_VOLUME = "GasVolume" ILLUMINATION_DEVICE_ATTRIBUTES = { "currentIllumination": ATTR_CURRENT_ILLUMINATION, @@ -138,6 +159,23 @@ async def async_setup_entry( entities.append(HomematicpTemperatureExternalSensorCh1(hap, device)) entities.append(HomematicpTemperatureExternalSensorCh2(hap, device)) entities.append(HomematicpTemperatureExternalSensorDelta(hap, device)) + if isinstance(device, AsyncEnergySensorsInterface): + for ch in get_channels_from_device( + device, FunctionalChannelType.ENERGY_SENSORS_INTERFACE_CHANNEL + ): + if ch.connectedEnergySensorType not in SENSORS_ESI: + continue + + new_entities = [ + HmipEsiSensorEntity(hap, device, ch.index, description) + for description in SENSORS_ESI[ch.connectedEnergySensorType] + ] + + entities.extend( + entity + for entity in new_entities + if entity.entity_description.exists_fn(ch) + ) async_add_entities(entities) @@ -396,6 +434,134 @@ class HomematicpTemperatureExternalSensorDelta(HomematicipGenericEntity, SensorE return self._device.temperatureExternalDelta +@dataclass(kw_only=True, frozen=True) +class HmipEsiSensorEntityDescription(SensorEntityDescription): + """SensorEntityDescription for HmIP Sensors.""" + + value_fn: Callable[[AsyncEnergySensorsInterface], StateType] + exists_fn: Callable[[FunctionalChannel], bool] + type_fn: Callable[[AsyncEnergySensorsInterface], str] + + +SENSORS_ESI = { + ESI_CONNECTED_SENSOR_TYPE_IEC: [ + HmipEsiSensorEntityDescription( + key=ESI_TYPE_CURRENT_POWER_CONSUMPTION, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: device.functional_channel.currentPowerConsumption, + exists_fn=lambda channel: channel.currentPowerConsumption is not None, + type_fn=lambda device: "CurrentPowerConsumption", + ), + HmipEsiSensorEntityDescription( + key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda device: device.functional_channel.energyCounterOne, + exists_fn=lambda channel: channel.energyCounterOneType != ESI_TYPE_UNKNOWN, + type_fn=lambda device: device.functional_channel.energyCounterOneType, + ), + HmipEsiSensorEntityDescription( + key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda device: device.functional_channel.energyCounterTwo, + exists_fn=lambda channel: channel.energyCounterTwoType != ESI_TYPE_UNKNOWN, + type_fn=lambda device: device.functional_channel.energyCounterTwoType, + ), + HmipEsiSensorEntityDescription( + key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda device: device.functional_channel.energyCounterThree, + exists_fn=lambda channel: channel.energyCounterThreeType + != ESI_TYPE_UNKNOWN, + type_fn=lambda device: device.functional_channel.energyCounterThreeType, + ), + ], + ESI_CONNECTED_SENSOR_TYPE_LED: [ + HmipEsiSensorEntityDescription( + key=ESI_TYPE_CURRENT_POWER_CONSUMPTION, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: device.functional_channel.currentPowerConsumption, + exists_fn=lambda channel: channel.currentPowerConsumption is not None, + type_fn=lambda device: "CurrentPowerConsumption", + ), + HmipEsiSensorEntityDescription( + key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda device: device.functional_channel.energyCounterOne, + exists_fn=lambda channel: channel.energyCounterOne is not None, + type_fn=lambda device: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, + ), + ], + ESI_CONNECTED_SENSOR_TYPE_GAS: [ + HmipEsiSensorEntityDescription( + key=ESI_TYPE_CURRENT_GAS_FLOW, + native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + device_class=SensorDeviceClass.VOLUME_FLOW_RATE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: device.functional_channel.currentGasFlow, + exists_fn=lambda channel: channel.currentGasFlow is not None, + type_fn=lambda device: "CurrentGasFlow", + ), + HmipEsiSensorEntityDescription( + key=ESI_TYPE_CURRENT_GAS_VOLUME, + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + device_class=SensorDeviceClass.VOLUME, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda device: device.functional_channel.gasVolume, + exists_fn=lambda channel: channel.gasVolume is not None, + type_fn=lambda device: "GasVolume", + ), + ], +} + + +class HmipEsiSensorEntity(HomematicipGenericEntity, SensorEntity): + """EntityDescription for HmIP-ESI Sensors.""" + + entity_description: HmipEsiSensorEntityDescription + + def __init__( + self, + hap: HomematicipHAP, + device: HomematicipGenericEntity, + channel_index: int, + entity_description: HmipEsiSensorEntityDescription, + ) -> None: + """Initialize Sensor Entity.""" + super().__init__( + hap=hap, + device=device, + channel=channel_index, + post=entity_description.key, + is_multi_channel=False, + ) + self.entity_description = entity_description + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes of the esi sensor.""" + state_attr = super().extra_state_attributes + state_attr[ATTR_ESI_TYPE] = self.entity_description.type_fn(self) + + return state_attr + + @property + def native_value(self) -> str | None: + """Return the state of the sensor.""" + return str(self.entity_description.value_fn(self)) + + class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP passage detector delta counter.""" diff --git a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json index 922601ca733..eba2c803b1f 100644 --- a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json +++ b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json @@ -7347,6 +7347,416 @@ "serializedGlobalTradeItemNumber": "3014F7110000000000000DLD", "type": "DOOR_LOCK_DRIVE", "updateState": "UP_TO_DATE" + }, + "3014F7110000000000ESIGAS": { + "availableFirmwareVersion": "1.2.2", + "connectionType": "HMIP_RF", + "deviceArchetype": "HMIP", + "firmwareVersion": "1.2.2", + "firmwareVersionInteger": 66050, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "controlsMountingOrientation": null, + "daliBusState": null, + "defaultLinkedGroup": [], + "deviceCommunicationError": null, + "deviceDriveError": null, + "deviceDriveModeError": null, + "deviceId": "3014F7110000000000ESIGAS", + "deviceOperationMode": null, + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": null, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000047"], + "index": 0, + "label": "", + "lockJammed": null, + "lowBat": false, + "mountingOrientation": null, + "multicastRoutingEnabled": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -73, + "rssiPeerValue": null, + "sensorCommunicationError": false, + "sensorError": false, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceCommunicationError": false, + "IFeatureDeviceDaliBusError": false, + "IFeatureDeviceDriveError": false, + "IFeatureDeviceDriveModeError": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceSensorCommunicationError": true, + "IFeatureDeviceSensorError": true, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDefaultLinkedGroup": false, + "IOptionalFeatureDeviceErrorLockJammed": false, + "IOptionalFeatureDeviceOperationMode": false, + "IOptionalFeatureDisplayContrast": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true, + "IOptionalFeatureMountingOrientation": false + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "channelRole": "ENERGY_SENSOR", + "connectedEnergySensorType": "ES_GAS", + "currentGasFlow": 1.03, + "currentPowerConsumption": null, + "deviceId": "3014F7110000000000ESIGAS", + "energyCounterOne": null, + "energyCounterOneType": "UNKNOWN", + "energyCounterThree": null, + "energyCounterThreeType": "UNKNOWN", + "energyCounterTwo": null, + "energyCounterTwoType": "UNKNOWN", + "functionalChannelType": "ENERGY_SENSORS_INTERFACE_CHANNEL", + "gasVolume": 1019.26, + "gasVolumePerImpulse": 0.01, + "groupIndex": 1, + "groups": ["00000000-0000-0000-0000-000000000077"], + "impulsesPerKWH": 10000, + "index": 1, + "label": "", + "supportedOptionalFeatures": { + "IOptionalFeatureCounterOffset": true, + "IOptionalFeatureCurrentGasFlow": true, + "IOptionalFeatureCurrentPowerConsumption": false, + "IOptionalFeatureEnergyCounterOne": false, + "IOptionalFeatureEnergyCounterThree": false, + "IOptionalFeatureEnergyCounterTwo": false, + "IOptionalFeatureGasVolume": true, + "IOptionalFeatureGasVolumePerImpulse": true, + "IOptionalFeatureImpulsesPerKWH": false + } + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000ESIGAS", + "label": "esi_gas", + "lastStatusUpdate": 1708880308351, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manuallyUpdateForced": false, + "manufacturerCode": 1, + "measuredAttributes": { + "1": { + "currentGasFlow": true, + "gasVolume": true + } + }, + "modelId": 509, + "modelType": "HmIP-ESI", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000ESIGAS", + "type": "ENERGY_SENSORS_INTERFACE", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000ESIIEC": { + "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", + "deviceArchetype": "HMIP", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "controlsMountingOrientation": null, + "daliBusState": null, + "defaultLinkedGroup": [], + "deviceCommunicationError": null, + "deviceDriveError": null, + "deviceDriveModeError": null, + "deviceId": "3014F7110000000000ESIIEC", + "deviceOperationMode": null, + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": null, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000031"], + "index": 0, + "label": "", + "lockJammed": null, + "lowBat": false, + "mountingOrientation": null, + "multicastRoutingEnabled": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -94, + "rssiPeerValue": null, + "sensorCommunicationError": false, + "sensorError": true, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceCommunicationError": false, + "IFeatureDeviceDaliBusError": false, + "IFeatureDeviceDriveError": false, + "IFeatureDeviceDriveModeError": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceSensorCommunicationError": true, + "IFeatureDeviceSensorError": true, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDefaultLinkedGroup": false, + "IOptionalFeatureDeviceErrorLockJammed": false, + "IOptionalFeatureDeviceOperationMode": false, + "IOptionalFeatureDisplayContrast": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true, + "IOptionalFeatureMountingOrientation": false + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "channelRole": null, + "connectedEnergySensorType": "ES_IEC", + "currentGasFlow": null, + "currentPowerConsumption": 432, + "deviceId": "3014F7110000000000ESIIEC", + "energyCounterOne": 194.0, + "energyCounterOneType": "ENERGY_COUNTER_USAGE_HIGH_TARIFF", + "energyCounterThree": 3.0, + "energyCounterThreeType": "ENERGY_COUNTER_INPUT_SINGLE_TARIFF", + "energyCounterTwo": 0.0, + "energyCounterTwoType": "ENERGY_COUNTER_USAGE_LOW_TARIFF", + "functionalChannelType": "ENERGY_SENSORS_INTERFACE_CHANNEL", + "gasVolume": null, + "gasVolumePerImpulse": 0.01, + "groupIndex": 1, + "groups": ["00000000-0000-0000-0000-000000000051"], + "impulsesPerKWH": 10000, + "index": 1, + "label": "", + "supportedOptionalFeatures": { + "IOptionalFeatureCounterOffset": false, + "IOptionalFeatureCurrentGasFlow": false, + "IOptionalFeatureCurrentPowerConsumption": true, + "IOptionalFeatureEnergyCounterOne": true, + "IOptionalFeatureEnergyCounterThree": true, + "IOptionalFeatureEnergyCounterTwo": true, + "IOptionalFeatureGasVolume": false, + "IOptionalFeatureGasVolumePerImpulse": false, + "IOptionalFeatureImpulsesPerKWH": false + } + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000ESIIEC", + "label": "esi_iec", + "lastStatusUpdate": 1702420986697, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manuallyUpdateForced": false, + "manufacturerCode": 1, + "measuredAttributes": {}, + "modelId": 509, + "modelType": "HmIP-ESI", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000ESIIEC", + "type": "ENERGY_SENSORS_INTERFACE", + "updateState": "UP_TO_DATE" + }, + "3014F711000000000ESIIEC2": { + "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", + "deviceArchetype": "HMIP", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "controlsMountingOrientation": null, + "daliBusState": null, + "defaultLinkedGroup": [], + "deviceCommunicationError": null, + "deviceDriveError": null, + "deviceDriveModeError": null, + "deviceId": "3014F711000000000ESIIEC2", + "deviceOperationMode": null, + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": null, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000031"], + "index": 0, + "label": "", + "lockJammed": null, + "lowBat": false, + "mountingOrientation": null, + "multicastRoutingEnabled": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -94, + "rssiPeerValue": null, + "sensorCommunicationError": false, + "sensorError": true, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceCommunicationError": false, + "IFeatureDeviceDaliBusError": false, + "IFeatureDeviceDriveError": false, + "IFeatureDeviceDriveModeError": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceSensorCommunicationError": true, + "IFeatureDeviceSensorError": true, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDefaultLinkedGroup": false, + "IOptionalFeatureDeviceErrorLockJammed": false, + "IOptionalFeatureDeviceOperationMode": false, + "IOptionalFeatureDisplayContrast": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true, + "IOptionalFeatureMountingOrientation": false + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "channelRole": null, + "connectedEnergySensorType": "ES_IEC", + "currentGasFlow": null, + "currentPowerConsumption": 432, + "deviceId": "3014F711000000000ESIIEC2", + "energyCounterOne": 194.0, + "energyCounterOneType": "ENERGY_COUNTER_USAGE_HIGH_TARIFF", + "energyCounterThree": 3.0, + "energyCounterThreeType": "UNKNOWN", + "energyCounterTwo": 0.0, + "energyCounterTwoType": "ENERGY_COUNTER_USAGE_LOW_TARIFF", + "functionalChannelType": "ENERGY_SENSORS_INTERFACE_CHANNEL", + "gasVolume": null, + "gasVolumePerImpulse": 0.01, + "groupIndex": 1, + "groups": ["00000000-0000-0000-0000-000000000051"], + "impulsesPerKWH": 10000, + "index": 1, + "label": "", + "supportedOptionalFeatures": { + "IOptionalFeatureCounterOffset": false, + "IOptionalFeatureCurrentGasFlow": false, + "IOptionalFeatureCurrentPowerConsumption": true, + "IOptionalFeatureEnergyCounterOne": true, + "IOptionalFeatureEnergyCounterThree": true, + "IOptionalFeatureEnergyCounterTwo": true, + "IOptionalFeatureGasVolume": false, + "IOptionalFeatureGasVolumePerImpulse": false, + "IOptionalFeatureImpulsesPerKWH": false + } + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711000000000ESIIEC2", + "label": "esi_iec2", + "lastStatusUpdate": 1702420986697, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manuallyUpdateForced": false, + "manufacturerCode": 1, + "measuredAttributes": {}, + "modelId": 509, + "modelType": "HmIP-ESI", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711000000000ESIIEC2", + "type": "ENERGY_SENSORS_INTERFACE", + "updateState": "UP_TO_DATE" } }, "groups": { diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index fb7fe7d7deb..348171b3187 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -26,7 +26,7 @@ async def test_hmip_load_all_supported_devices( test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 278 + assert len(mock_hap.hmip_device_by_entity_id) == 290 async def test_hmip_remove_device( diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 3089bb062e5..6951b750b2f 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -511,3 +511,126 @@ async def test_hmip_passage_detector_delta_counter( await async_manipulate_test_data(hass, hmip_device, "leftRightCounterDelta", 190) ha_state = hass.states.get(entity_id) assert ha_state.state == "190" + + +async def test_hmip_esi_iec_current_power_consumption( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC currentPowerConsumption Sensor.""" + entity_id = "sensor.esi_iec_currentPowerConsumption" + entity_name = "esi_iec CurrentPowerConsumption" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_iec"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "432" + + +async def test_hmip_esi_iec_energy_counter_usage_high_tariff( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC ENERGY_COUNTER_USAGE_HIGH_TARIFF.""" + entity_id = "sensor.esi_iec_energy_counter_usage_high_tariff" + entity_name = "esi_iec ENERGY_COUNTER_USAGE_HIGH_TARIFF" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_iec"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "194.0" + + +async def test_hmip_esi_iec_energy_counter_usage_low_tariff( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC ENERGY_COUNTER_USAGE_LOW_TARIFF.""" + entity_id = "sensor.esi_iec_energy_counter_usage_low_tariff" + entity_name = "esi_iec ENERGY_COUNTER_USAGE_LOW_TARIFF" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_iec"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "0.0" + + +async def test_hmip_esi_iec_energy_counter_input_single_tariff( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC ENERGY_COUNTER_INPUT_SINGLE_TARIFF.""" + entity_id = "sensor.esi_iec_energy_counter_input_single_tariff" + entity_name = "esi_iec ENERGY_COUNTER_INPUT_SINGLE_TARIFF" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_iec"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "3.0" + + +async def test_hmip_esi_iec_unknown_channel( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test devices are loaded partially.""" + not_existing_entity_id = "sensor.esi_iec2_energy_counter_input_single_tariff" + existing_entity_id = "sensor.esi_iec2_energy_counter_usage_high_tariff" + await default_mock_hap_factory.async_get_mock_hap(test_devices=["esi_iec2"]) + + not_existing_ha_state = hass.states.get(not_existing_entity_id) + existing_ha_state = hass.states.get(existing_entity_id) + + assert not_existing_ha_state is None + assert existing_ha_state.state == "194.0" + + +async def test_hmip_esi_gas_current_gas_flow( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC CurrentGasFlow.""" + entity_id = "sensor.esi_gas_currentgasflow" + entity_name = "esi_gas CurrentGasFlow" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_gas"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "1.03" + + +async def test_hmip_esi_gas_gas_volume( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC GasVolume.""" + entity_id = "sensor.esi_gas_gasvolume" + entity_name = "esi_gas GasVolume" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_gas"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "1019.26" From 7b7b97a7a47d13233e766e7f7c5aad91cdb301df Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:35:39 +0200 Subject: [PATCH 0209/2411] Force alias when importing event and fan PLATFORM_SCHEMA (#120524) --- homeassistant/components/group/event.py | 4 ++-- homeassistant/components/group/fan.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/group/event.py b/homeassistant/components/group/event.py index e5752a7835f..67220b878a1 100644 --- a/homeassistant/components/group/event.py +++ b/homeassistant/components/group/event.py @@ -11,7 +11,7 @@ from homeassistant.components.event import ( ATTR_EVENT_TYPE, ATTR_EVENT_TYPES, DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as EVENT_PLATFORM_SCHEMA, EventEntity, ) from homeassistant.config_entries import ConfigEntry @@ -38,7 +38,7 @@ DEFAULT_NAME = "Event group" # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = EVENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index b70a4ff1531..e09477430ef 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -15,7 +15,7 @@ from homeassistant.components.fan import ( ATTR_PERCENTAGE, ATTR_PERCENTAGE_STEP, DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as FAN_PLATFORM_SCHEMA, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_PERCENTAGE, @@ -54,7 +54,7 @@ DEFAULT_NAME = "Fan Group" # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = FAN_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, From 44aad2b8211e93ae40a69bb8df993562821e144f Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 26 Jun 2024 11:43:51 +0200 Subject: [PATCH 0210/2411] Improve Matter Server version incompatibility handling (#120416) * Improve Matter Server version incompatibility handling Improve the handling of Matter Server version. Noteably fix the issues raised (add strings for the issue) and split the version check into two cases: One if the server is too old and one if the server is too new. * Bump Python Matter Server library to 6.2.0b1 * Address review feedback --- homeassistant/components/matter/__init__.py | 32 +++++++--- homeassistant/components/matter/manifest.json | 2 +- homeassistant/components/matter/strings.json | 10 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/matter/test_init.py | 63 ++++++++++++++++--- 6 files changed, 90 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/matter/__init__.py b/homeassistant/components/matter/__init__.py index 86b642f7389..75ae3df6b1a 100644 --- a/homeassistant/components/matter/__init__.py +++ b/homeassistant/components/matter/__init__.py @@ -7,7 +7,12 @@ from contextlib import suppress from functools import cache from matter_server.client import MatterClient -from matter_server.client.exceptions import CannotConnect, InvalidServerVersion +from matter_server.client.exceptions import ( + CannotConnect, + InvalidServerVersion, + ServerVersionTooNew, + ServerVersionTooOld, +) from matter_server.common.errors import MatterError, NodeNotExists from homeassistant.components.hassio import AddonError, AddonManager, AddonState @@ -71,17 +76,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (CannotConnect, TimeoutError) as err: raise ConfigEntryNotReady("Failed to connect to matter server") from err except InvalidServerVersion as err: - if use_addon: - addon_manager = _get_addon_manager(hass) - addon_manager.async_schedule_update_addon(catch_error=True) - else: + if isinstance(err, ServerVersionTooOld): + if use_addon: + addon_manager = _get_addon_manager(hass) + addon_manager.async_schedule_update_addon(catch_error=True) + else: + async_create_issue( + hass, + DOMAIN, + "server_version_version_too_old", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="server_version_version_too_old", + ) + elif isinstance(err, ServerVersionTooNew): async_create_issue( hass, DOMAIN, - "invalid_server_version", + "server_version_version_too_new", is_fixable=False, severity=IssueSeverity.ERROR, - translation_key="invalid_server_version", + translation_key="server_version_version_too_new", ) raise ConfigEntryNotReady(f"Invalid server version: {err}") from err @@ -91,7 +106,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "Unknown error connecting to the Matter server" ) from err - async_delete_issue(hass, DOMAIN, "invalid_server_version") + async_delete_issue(hass, DOMAIN, "server_version_version_too_old") + async_delete_issue(hass, DOMAIN, "server_version_version_too_new") async def on_hass_stop(event: Event) -> None: """Handle incoming stop event from Home Assistant.""" diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 369657df90c..8c88fcc8be2 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,6 +6,6 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==6.1.0"], + "requirements": ["python-matter-server==6.2.0b1"], "zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."] } diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index e94ab2e1780..3389a4bfe81 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -157,6 +157,16 @@ } } }, + "issues": { + "server_version_version_too_old": { + "description": "The version of the Matter Server you are currently running is too old for this version of Home Assistant. Please update the Matter Server to the latest version to fix this issue.", + "title": "Newer version of Matter Server needed" + }, + "server_version_version_too_new": { + "description": "The version of the Matter Server you are currently running is too new for this version of Home Assistant. Please update Home Assistant or downgrade the Matter Server to an older version to fix this issue.", + "title": "Older version of Matter Server needed" + } + }, "services": { "open_commissioning_window": { "name": "Open commissioning window", diff --git a/requirements_all.txt b/requirements_all.txt index 5967b3f2a94..1a297ef2b5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2284,7 +2284,7 @@ python-kasa[speedups]==0.7.0.1 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==6.1.0 +python-matter-server==6.2.0b1 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1fb561ecee..88623000c5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1781,7 +1781,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.0.1 # homeassistant.components.matter -python-matter-server==6.1.0 +python-matter-server==6.2.0b1 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index d3712f24d12..c28385efca3 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -5,7 +5,11 @@ from __future__ import annotations import asyncio from unittest.mock import AsyncMock, MagicMock, call, patch -from matter_server.client.exceptions import CannotConnect, InvalidServerVersion +from matter_server.client.exceptions import ( + CannotConnect, + ServerVersionTooNew, + ServerVersionTooOld, +) from matter_server.client.models.node import MatterNode from matter_server.common.errors import MatterError from matter_server.common.helpers.util import dataclass_from_dict @@ -362,12 +366,30 @@ async def test_addon_info_failure( "backup_calls", "update_addon_side_effect", "create_backup_side_effect", + "connect_side_effect", ), [ - ("1.0.0", True, 1, 1, None, None), - ("1.0.0", False, 0, 0, None, None), - ("1.0.0", True, 1, 1, HassioAPIError("Boom"), None), - ("1.0.0", True, 0, 1, None, HassioAPIError("Boom")), + ("1.0.0", True, 1, 1, None, None, ServerVersionTooOld("Invalid version")), + ("1.0.0", True, 0, 0, None, None, ServerVersionTooNew("Invalid version")), + ("1.0.0", False, 0, 0, None, None, ServerVersionTooOld("Invalid version")), + ( + "1.0.0", + True, + 1, + 1, + HassioAPIError("Boom"), + None, + ServerVersionTooOld("Invalid version"), + ), + ( + "1.0.0", + True, + 0, + 1, + None, + HassioAPIError("Boom"), + ServerVersionTooOld("Invalid version"), + ), ], ) async def test_update_addon( @@ -386,13 +408,14 @@ async def test_update_addon( backup_calls: int, update_addon_side_effect: Exception | None, create_backup_side_effect: Exception | None, + connect_side_effect: Exception, ) -> None: """Test update the Matter add-on during entry setup.""" addon_info.return_value["version"] = addon_version addon_info.return_value["update_available"] = update_available create_backup.side_effect = create_backup_side_effect update_addon.side_effect = update_addon_side_effect - matter_client.connect.side_effect = InvalidServerVersion("Invalid version") + matter_client.connect.side_effect = connect_side_effect entry = MockConfigEntry( domain=DOMAIN, title="Matter", @@ -413,12 +436,32 @@ async def test_update_addon( # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize( + ( + "connect_side_effect", + "issue_raised", + ), + [ + ( + ServerVersionTooOld("Invalid version"), + "server_version_version_too_old", + ), + ( + ServerVersionTooNew("Invalid version"), + "server_version_version_too_new", + ), + ], +) async def test_issue_registry_invalid_version( - hass: HomeAssistant, matter_client: MagicMock, issue_registry: ir.IssueRegistry + hass: HomeAssistant, + matter_client: MagicMock, + issue_registry: ir.IssueRegistry, + connect_side_effect: Exception, + issue_raised: str, ) -> None: """Test issue registry for invalid version.""" original_connect_side_effect = matter_client.connect.side_effect - matter_client.connect.side_effect = InvalidServerVersion("Invalid version") + matter_client.connect.side_effect = connect_side_effect entry = MockConfigEntry( domain=DOMAIN, title="Matter", @@ -434,7 +477,7 @@ async def test_issue_registry_invalid_version( entry_state = entry.state assert entry_state is ConfigEntryState.SETUP_RETRY - assert issue_registry.async_get_issue(DOMAIN, "invalid_server_version") + assert issue_registry.async_get_issue(DOMAIN, issue_raised) matter_client.connect.side_effect = original_connect_side_effect @@ -442,7 +485,7 @@ async def test_issue_registry_invalid_version( await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED - assert not issue_registry.async_get_issue(DOMAIN, "invalid_server_version") + assert not issue_registry.async_get_issue(DOMAIN, issue_raised) @pytest.mark.parametrize( From 42d235ce4d4368c0160163118441ffa2cb2b2635 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 11:44:58 +0200 Subject: [PATCH 0211/2411] Add diagnostics platform to pyLoad integration (#120535) --- .../components/pyload/diagnostics.py | 26 +++++++++++++++++ .../pyload/snapshots/test_diagnostics.ambr | 17 +++++++++++ tests/components/pyload/test_diagnostics.py | 28 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 homeassistant/components/pyload/diagnostics.py create mode 100644 tests/components/pyload/snapshots/test_diagnostics.ambr create mode 100644 tests/components/pyload/test_diagnostics.py diff --git a/homeassistant/components/pyload/diagnostics.py b/homeassistant/components/pyload/diagnostics.py new file mode 100644 index 00000000000..d18e5a5fe0d --- /dev/null +++ b/homeassistant/components/pyload/diagnostics.py @@ -0,0 +1,26 @@ +"""Diagnostics support for pyLoad.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from . import PyLoadConfigEntry +from .coordinator import pyLoadData + +TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: PyLoadConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + pyload_data: pyLoadData = config_entry.runtime_data.data + + return { + "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), + "pyload_data": pyload_data, + } diff --git a/tests/components/pyload/snapshots/test_diagnostics.ambr b/tests/components/pyload/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..8c3e110f2ec --- /dev/null +++ b/tests/components/pyload/snapshots/test_diagnostics.ambr @@ -0,0 +1,17 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'config_entry_data': dict({ + 'host': 'pyload.local', + 'password': '**REDACTED**', + 'port': 8000, + 'ssl': True, + 'username': '**REDACTED**', + 'verify_ssl': False, + }), + 'pyload_data': dict({ + '__type': "", + 'repr': 'pyLoadData(pause=False, active=1, queue=6, total=37, speed=5405963.0, download=True, reconnect=False, captcha=False, free_space=99999999999)', + }), + }) +# --- diff --git a/tests/components/pyload/test_diagnostics.py b/tests/components/pyload/test_diagnostics.py new file mode 100644 index 00000000000..9c5e73f853f --- /dev/null +++ b/tests/components/pyload/test_diagnostics.py @@ -0,0 +1,28 @@ +"""Tests for pyLoad diagnostics.""" + +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) From a8bf671663a00aad825d122ff3d2f19fe196828f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:45:57 +0200 Subject: [PATCH 0212/2411] Force alias when importing remote PLATFORM_SCHEMA (#120533) --- homeassistant/components/itach/remote.py | 4 ++-- homeassistant/components/xiaomi_miio/remote.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/itach/remote.py b/homeassistant/components/itach/remote.py index 606ca4fd021..986dbfb8b95 100644 --- a/homeassistant/components/itach/remote.py +++ b/homeassistant/components/itach/remote.py @@ -13,7 +13,7 @@ from homeassistant.components import remote from homeassistant.components.remote import ( ATTR_NUM_REPEATS, DEFAULT_NUM_REPEATS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as REMOTE_PLATFORM_SCHEMA, ) from homeassistant.const import ( CONF_DEVICES, @@ -42,7 +42,7 @@ CONF_COMMANDS = "commands" CONF_DATA = "data" CONF_IR_COUNT = "ir_count" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = REMOTE_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MAC): cv.string, vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 5baaf614b01..959bf0a7bee 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -16,7 +16,7 @@ from homeassistant.components.remote import ( ATTR_DELAY_SECS, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as REMOTE_PLATFORM_SCHEMA, RemoteEntity, ) from homeassistant.const import ( @@ -49,7 +49,7 @@ COMMAND_SCHEMA = vol.Schema( {vol.Required(CONF_COMMAND): vol.All(cv.ensure_list, [cv.string])} ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = REMOTE_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_HOST): cv.string, From d4dc7d76d95b71295121307f72d6b501907d81a7 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 26 Jun 2024 19:46:30 +1000 Subject: [PATCH 0213/2411] Refactor Tessie for future PR (#120406) * Bump tessie-api * Refactor * revert bump * Fix cover * Apply suggestions from code review Co-authored-by: G Johansson --------- Co-authored-by: G Johansson --- homeassistant/components/tessie/__init__.py | 34 +++++++++++++++---- .../components/tessie/binary_sensor.py | 6 ++-- homeassistant/components/tessie/button.py | 6 ++-- homeassistant/components/tessie/climate.py | 6 ++-- homeassistant/components/tessie/cover.py | 18 +++++----- .../components/tessie/device_tracker.py | 6 ++-- homeassistant/components/tessie/entity.py | 25 ++++---------- homeassistant/components/tessie/lock.py | 14 ++++---- .../components/tessie/media_player.py | 6 ++-- homeassistant/components/tessie/models.py | 13 ++++++- homeassistant/components/tessie/number.py | 6 ++-- homeassistant/components/tessie/select.py | 3 +- homeassistant/components/tessie/sensor.py | 6 ++-- homeassistant/components/tessie/switch.py | 8 ++--- homeassistant/components/tessie/update.py | 6 ++-- 15 files changed, 92 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index 9e7bc42fa27..37fb669e54b 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -11,9 +11,11 @@ from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceInfo +from .const import DOMAIN, MODELS from .coordinator import TessieStateUpdateCoordinator -from .models import TessieData +from .models import TessieData, TessieVehicleData PLATFORMS = [ Platform.BINARY_SENSOR, @@ -40,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo api_key = entry.data[CONF_ACCESS_TOKEN] try: - vehicles = await get_state_of_all_vehicles( + state_of_all_vehicles = await get_state_of_all_vehicles( session=async_get_clientsession(hass), api_key=api_key, only_active=True, @@ -54,13 +56,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo raise ConfigEntryNotReady from e vehicles = [ - TessieStateUpdateCoordinator( - hass, - api_key=api_key, + TessieVehicleData( vin=vehicle["vin"], - data=vehicle["last_state"], + data_coordinator=TessieStateUpdateCoordinator( + hass, + api_key=api_key, + vin=vehicle["vin"], + data=vehicle["last_state"], + ), + device=DeviceInfo( + identifiers={(DOMAIN, vehicle["vin"])}, + manufacturer="Tesla", + configuration_url="https://my.tessie.com/", + name=vehicle["last_state"]["display_name"], + model=MODELS.get( + vehicle["last_state"]["vehicle_config"]["car_type"], + vehicle["last_state"]["vehicle_config"]["car_type"], + ), + sw_version=vehicle["last_state"]["vehicle_state"]["car_version"].split( + " " + )[0], + hw_version=vehicle["last_state"]["vehicle_config"]["driver_assist"], + serial_number=vehicle["vin"], + ), ) - for vehicle in vehicles["results"] + for vehicle in state_of_all_vehicles["results"] if vehicle["last_state"] is not None ] diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py index 2d3f1134444..eee85ce8466 100644 --- a/homeassistant/components/tessie/binary_sensor.py +++ b/homeassistant/components/tessie/binary_sensor.py @@ -16,8 +16,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry from .const import TessieState -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData @dataclass(frozen=True, kw_only=True) @@ -180,11 +180,11 @@ class TessieBinarySensorEntity(TessieEntity, BinarySensorEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, description: TessieBinarySensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, description.key) + super().__init__(vehicle, description.key) self.entity_description = description @property diff --git a/homeassistant/components/tessie/button.py b/homeassistant/components/tessie/button.py index 43dadec60e6..8f80f27616b 100644 --- a/homeassistant/components/tessie/button.py +++ b/homeassistant/components/tessie/button.py @@ -19,8 +19,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData @dataclass(frozen=True, kw_only=True) @@ -67,11 +67,11 @@ class TessieButtonEntity(TessieEntity, ButtonEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, description: TessieButtonEntityDescription, ) -> None: """Initialize the Button.""" - super().__init__(coordinator, description.key) + super().__init__(vehicle, description.key) self.entity_description = description async def async_press(self) -> None: diff --git a/homeassistant/components/tessie/climate.py b/homeassistant/components/tessie/climate.py index 2a3b77ab8ce..7676d2f071b 100644 --- a/homeassistant/components/tessie/climate.py +++ b/homeassistant/components/tessie/climate.py @@ -23,8 +23,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry from .const import TessieClimateKeeper -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData async def async_setup_entry( @@ -62,10 +62,10 @@ class TessieClimateEntity(TessieEntity, ClimateEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, ) -> None: """Initialize the Climate entity.""" - super().__init__(coordinator, "primary") + super().__init__(vehicle, "primary") @property def hvac_mode(self) -> HVACMode | None: diff --git a/homeassistant/components/tessie/cover.py b/homeassistant/components/tessie/cover.py index 5be08107a29..6fdd950b809 100644 --- a/homeassistant/components/tessie/cover.py +++ b/homeassistant/components/tessie/cover.py @@ -23,8 +23,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry from .const import TessieCoverStates -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData async def async_setup_entry( @@ -53,9 +53,9 @@ class TessieWindowEntity(TessieEntity, CoverEntity): _attr_device_class = CoverDeviceClass.WINDOW _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - def __init__(self, coordinator: TessieStateUpdateCoordinator) -> None: + def __init__(self, vehicle: TessieVehicleData) -> None: """Initialize the sensor.""" - super().__init__(coordinator, "windows") + super().__init__(vehicle, "windows") @property def is_closed(self) -> bool | None: @@ -94,9 +94,9 @@ class TessieChargePortEntity(TessieEntity, CoverEntity): _attr_device_class = CoverDeviceClass.DOOR _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - def __init__(self, coordinator: TessieStateUpdateCoordinator) -> None: + def __init__(self, vehicle: TessieVehicleData) -> None: """Initialize the sensor.""" - super().__init__(coordinator, "charge_state_charge_port_door_open") + super().__init__(vehicle, "charge_state_charge_port_door_open") @property def is_closed(self) -> bool | None: @@ -120,9 +120,9 @@ class TessieFrontTrunkEntity(TessieEntity, CoverEntity): _attr_device_class = CoverDeviceClass.DOOR _attr_supported_features = CoverEntityFeature.OPEN - def __init__(self, coordinator: TessieStateUpdateCoordinator) -> None: + def __init__(self, vehicle: TessieVehicleData) -> None: """Initialize the sensor.""" - super().__init__(coordinator, "vehicle_state_ft") + super().__init__(vehicle, "vehicle_state_ft") @property def is_closed(self) -> bool | None: @@ -141,9 +141,9 @@ class TessieRearTrunkEntity(TessieEntity, CoverEntity): _attr_device_class = CoverDeviceClass.DOOR _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - def __init__(self, coordinator: TessieStateUpdateCoordinator) -> None: + def __init__(self, vehicle: TessieVehicleData) -> None: """Initialize the sensor.""" - super().__init__(coordinator, "vehicle_state_rt") + super().__init__(vehicle, "vehicle_state_rt") @property def is_closed(self) -> bool | None: diff --git a/homeassistant/components/tessie/device_tracker.py b/homeassistant/components/tessie/device_tracker.py index 382c775c200..300aae7d858 100644 --- a/homeassistant/components/tessie/device_tracker.py +++ b/homeassistant/components/tessie/device_tracker.py @@ -9,8 +9,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import TessieConfigEntry -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData async def async_setup_entry( @@ -36,10 +36,10 @@ class TessieDeviceTrackerEntity(TessieEntity, TrackerEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, ) -> None: """Initialize the device tracker.""" - super().__init__(coordinator, self.key) + super().__init__(vehicle, self.key) @property def source_type(self) -> SourceType | str: diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index 35d41af32f2..1b7ddcbe84c 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -6,11 +6,11 @@ from typing import Any from aiohttp import ClientResponseError from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, MODELS +from .const import DOMAIN from .coordinator import TessieStateUpdateCoordinator +from .models import TessieVehicleData class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): @@ -20,28 +20,17 @@ class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, key: str, ) -> None: """Initialize common aspects of a Tessie entity.""" - super().__init__(coordinator) - self.vin = coordinator.vin + super().__init__(vehicle.data_coordinator) + self.vin = vehicle.vin self.key = key - car_type = coordinator.data["vehicle_config_car_type"] - self._attr_translation_key = key - self._attr_unique_id = f"{self.vin}-{key}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self.vin)}, - manufacturer="Tesla", - configuration_url="https://my.tessie.com/", - name=coordinator.data["display_name"], - model=MODELS.get(car_type, car_type), - sw_version=coordinator.data["vehicle_state_car_version"].split(" ")[0], - hw_version=coordinator.data["vehicle_config_driver_assist"], - serial_number=self.vin, - ) + self._attr_unique_id = f"{vehicle.vin}-{key}" + self._attr_device_info = vehicle.device @property def _value(self) -> Any: diff --git a/homeassistant/components/tessie/lock.py b/homeassistant/components/tessie/lock.py index 0ea65ce4781..d73d83e399d 100644 --- a/homeassistant/components/tessie/lock.py +++ b/homeassistant/components/tessie/lock.py @@ -23,8 +23,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry from .const import DOMAIN, TessieChargeCableLockStates -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData async def async_setup_entry( @@ -82,10 +82,10 @@ class TessieLockEntity(TessieEntity, LockEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, "vehicle_state_locked") + super().__init__(vehicle, "vehicle_state_locked") @property def is_locked(self) -> bool | None: @@ -110,10 +110,10 @@ class TessieSpeedLimitEntity(TessieEntity, LockEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, "vehicle_state_speed_limit_mode_active") + super().__init__(vehicle, "vehicle_state_speed_limit_mode_active") @property def is_locked(self) -> bool | None: @@ -160,10 +160,10 @@ class TessieCableLockEntity(TessieEntity, LockEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, "charge_state_charge_port_latch") + super().__init__(vehicle, "charge_state_charge_port_latch") @property def is_locked(self) -> bool | None: diff --git a/homeassistant/components/tessie/media_player.py b/homeassistant/components/tessie/media_player.py index f99c8ad1e1f..f3b5e266604 100644 --- a/homeassistant/components/tessie/media_player.py +++ b/homeassistant/components/tessie/media_player.py @@ -11,8 +11,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData STATES = { "Playing": MediaPlayerState.PLAYING, @@ -39,10 +39,10 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, ) -> None: """Initialize the media player entity.""" - super().__init__(coordinator, "media") + super().__init__(vehicle, "media") @property def state(self) -> MediaPlayerState: diff --git a/homeassistant/components/tessie/models.py b/homeassistant/components/tessie/models.py index 3919db3f6d3..e96562ff8e1 100644 --- a/homeassistant/components/tessie/models.py +++ b/homeassistant/components/tessie/models.py @@ -4,6 +4,8 @@ from __future__ import annotations from dataclasses import dataclass +from homeassistant.helpers.device_registry import DeviceInfo + from .coordinator import TessieStateUpdateCoordinator @@ -11,4 +13,13 @@ from .coordinator import TessieStateUpdateCoordinator class TessieData: """Data for the Tessie integration.""" - vehicles: list[TessieStateUpdateCoordinator] + vehicles: list[TessieVehicleData] + + +@dataclass +class TessieVehicleData: + """Data for a Tessie vehicle.""" + + data_coordinator: TessieStateUpdateCoordinator + device: DeviceInfo + vin: str diff --git a/homeassistant/components/tessie/number.py b/homeassistant/components/tessie/number.py index 222922eba3e..56739193d7f 100644 --- a/homeassistant/components/tessie/number.py +++ b/homeassistant/components/tessie/number.py @@ -23,8 +23,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData @dataclass(frozen=True, kw_only=True) @@ -101,11 +101,11 @@ class TessieNumberEntity(TessieEntity, NumberEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, description: TessieNumberEntityDescription, ) -> None: """Initialize the Number entity.""" - super().__init__(coordinator, description.key) + super().__init__(vehicle, description.key) self.entity_description = description @property diff --git a/homeassistant/components/tessie/select.py b/homeassistant/components/tessie/select.py index 801d465ea2a..43af8161697 100644 --- a/homeassistant/components/tessie/select.py +++ b/homeassistant/components/tessie/select.py @@ -35,7 +35,8 @@ async def async_setup_entry( TessieSeatHeaterSelectEntity(vehicle, key) for vehicle in data.vehicles for key in SEAT_HEATERS - if key in vehicle.data # not all vehicles have rear center or third row + if key + in vehicle.data_coordinator.data # not all vehicles have rear center or third row ) diff --git a/homeassistant/components/tessie/sensor.py b/homeassistant/components/tessie/sensor.py index c3023948f4c..dc910c7a03a 100644 --- a/homeassistant/components/tessie/sensor.py +++ b/homeassistant/components/tessie/sensor.py @@ -34,8 +34,8 @@ from homeassistant.util.variance import ignore_variance from . import TessieConfigEntry from .const import TessieChargeStates -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData @callback @@ -280,11 +280,11 @@ class TessieSensorEntity(TessieEntity, SensorEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, description: TessieSensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, description.key) + super().__init__(vehicle, description.key) self.entity_description = description @property diff --git a/homeassistant/components/tessie/switch.py b/homeassistant/components/tessie/switch.py index 2f3902b3bd3..03bd018cd83 100644 --- a/homeassistant/components/tessie/switch.py +++ b/homeassistant/components/tessie/switch.py @@ -29,8 +29,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData @dataclass(frozen=True, kw_only=True) @@ -84,7 +84,7 @@ async def async_setup_entry( TessieSwitchEntity(vehicle, description) for vehicle in entry.runtime_data.vehicles for description in DESCRIPTIONS - if description.key in vehicle.data + if description.key in vehicle.data_coordinator.data ), ( TessieChargeSwitchEntity(vehicle, CHARGE_DESCRIPTION) @@ -102,11 +102,11 @@ class TessieSwitchEntity(TessieEntity, SwitchEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, description: TessieSwitchEntityDescription, ) -> None: """Initialize the Switch.""" - super().__init__(coordinator, description.key) + super().__init__(vehicle, description.key) self.entity_description = description @property diff --git a/homeassistant/components/tessie/update.py b/homeassistant/components/tessie/update.py index 5f51a38d77d..73a01873e37 100644 --- a/homeassistant/components/tessie/update.py +++ b/homeassistant/components/tessie/update.py @@ -12,8 +12,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TessieConfigEntry from .const import TessieUpdateStatus -from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity +from .models import TessieVehicleData async def async_setup_entry( @@ -34,10 +34,10 @@ class TessieUpdateEntity(TessieEntity, UpdateEntity): def __init__( self, - coordinator: TessieStateUpdateCoordinator, + vehicle: TessieVehicleData, ) -> None: """Initialize the Update.""" - super().__init__(coordinator, "update") + super().__init__(vehicle, "update") @property def supported_features(self) -> UpdateEntityFeature: From 912136be258b973b8e07a996a039e3cc618729ec Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:52:57 +0200 Subject: [PATCH 0214/2411] Force alias when importing lock PLATFORM_SCHEMA (#120531) --- homeassistant/components/group/lock.py | 4 ++-- homeassistant/components/kiwi/lock.py | 7 +++++-- homeassistant/components/sesame/lock.py | 7 +++++-- homeassistant/components/template/lock.py | 4 ++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/group/lock.py b/homeassistant/components/group/lock.py index 4da5829634b..8bb7b18ce29 100644 --- a/homeassistant/components/group/lock.py +++ b/homeassistant/components/group/lock.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.lock import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LOCK_PLATFORM_SCHEMA, LockEntity, LockEntityFeature, ) @@ -43,7 +43,7 @@ DEFAULT_NAME = "Lock Group" # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py index 770b842091c..bde9a77f748 100644 --- a/homeassistant/components/kiwi/lock.py +++ b/homeassistant/components/kiwi/lock.py @@ -8,7 +8,10 @@ from typing import Any from kiwiki import KiwiClient, KiwiException import voluptuous as vol -from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity +from homeassistant.components.lock import ( + PLATFORM_SCHEMA as LOCK_PLATFORM_SCHEMA, + LockEntity, +) from homeassistant.const import ( ATTR_ID, ATTR_LATITUDE, @@ -32,7 +35,7 @@ ATTR_CAN_INVITE = "can_invite_others" UNLOCK_MAINTAIN_TIME = 5 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} ) diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py index 050a5978acc..ad8b26f7034 100644 --- a/homeassistant/components/sesame/lock.py +++ b/homeassistant/components/sesame/lock.py @@ -7,7 +7,10 @@ from typing import Any import pysesame2 import voluptuous as vol -from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity +from homeassistant.components.lock import ( + PLATFORM_SCHEMA as LOCK_PLATFORM_SCHEMA, + LockEntity, +) from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_DEVICE_ID, CONF_API_KEY from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -16,7 +19,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType ATTR_SERIAL_NO = "serial" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) +PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) def setup_platform( diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 8259a6c12f0..0fa219fcd9b 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -7,7 +7,7 @@ from typing import Any import voluptuous as vol from homeassistant.components.lock import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LOCK_PLATFORM_SCHEMA, STATE_JAMMED, STATE_LOCKING, STATE_UNLOCKING, @@ -44,7 +44,7 @@ CONF_UNLOCK = "unlock" DEFAULT_NAME = "Template Lock" DEFAULT_OPTIMISTIC = False -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA, From 32bab97f006edb6d81cb4c33417dac9bc10670de Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:53:15 +0200 Subject: [PATCH 0215/2411] Force alias when importing light PLATFORM_SCHEMA (#120529) --- homeassistant/components/ads/light.py | 4 ++-- homeassistant/components/avion/light.py | 4 ++-- homeassistant/components/blinksticklight/light.py | 4 ++-- homeassistant/components/decora/light.py | 4 ++-- homeassistant/components/decora_wifi/light.py | 4 ++-- homeassistant/components/enocean/light.py | 4 ++-- homeassistant/components/everlights/light.py | 4 ++-- homeassistant/components/futurenow/light.py | 4 ++-- homeassistant/components/greenwave/light.py | 4 ++-- homeassistant/components/group/light.py | 4 ++-- homeassistant/components/iglo/light.py | 4 ++-- homeassistant/components/limitlessled/light.py | 4 ++-- homeassistant/components/lw12wifi/light.py | 4 ++-- homeassistant/components/mochad/light.py | 4 ++-- homeassistant/components/niko_home_control/light.py | 4 ++-- homeassistant/components/opple/light.py | 4 ++-- homeassistant/components/osramlightify/light.py | 4 ++-- homeassistant/components/pilight/light.py | 4 ++-- homeassistant/components/rflink/light.py | 4 ++-- homeassistant/components/scsgate/light.py | 8 ++++++-- homeassistant/components/switch/light.py | 8 ++++++-- homeassistant/components/tikteck/light.py | 4 ++-- homeassistant/components/unifiled/light.py | 4 ++-- homeassistant/components/x10/light.py | 4 ++-- homeassistant/components/yeelightsunflower/light.py | 4 ++-- homeassistant/components/zengge/light.py | 4 ++-- 26 files changed, 60 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/ads/light.py b/homeassistant/components/ads/light.py index 13ce9ec261c..0df69a60093 100644 --- a/homeassistant/components/ads/light.py +++ b/homeassistant/components/ads/light.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -29,7 +29,7 @@ from . import ( ) DEFAULT_NAME = "ADS Light" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ADS_VAR): cv.string, vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string, diff --git a/homeassistant/components/avion/light.py b/homeassistant/components/avion/light.py index e26676a0169..687405e3064 100644 --- a/homeassistant/components/avion/light.py +++ b/homeassistant/components/avion/light.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -35,7 +35,7 @@ DEVICE_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, vol.Optional(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/blinksticklight/light.py b/homeassistant/components/blinksticklight/light.py index 3e1f60e0f50..a789a7e0503 100644 --- a/homeassistant/components/blinksticklight/light.py +++ b/homeassistant/components/blinksticklight/light.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -25,7 +25,7 @@ CONF_SERIAL = "serial" DEFAULT_NAME = "Blinkstick" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SERIAL): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 3f8118a6e5d..cef7b98a2c1 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -16,7 +16,7 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -48,7 +48,7 @@ DEVICE_SCHEMA = vol.Schema( PLATFORM_SCHEMA = vol.Schema( vol.All( - PLATFORM_SCHEMA.extend( + LIGHT_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} ), _name_validator, diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 798243b5d4b..63ab5c2bf02 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -15,7 +15,7 @@ from homeassistant.components import persistent_notification from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -29,7 +29,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) # Validation of the user's configuration -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} ) diff --git a/homeassistant/components/enocean/light.py b/homeassistant/components/enocean/light.py index 937930c4a31..1e81e3cd089 100644 --- a/homeassistant/components/enocean/light.py +++ b/homeassistant/components/enocean/light.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -26,7 +26,7 @@ CONF_SENDER_ID = "sender_id" DEFAULT_NAME = "EnOcean Light" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_ID, default=[]): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Required(CONF_SENDER_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index 334e464d67e..2ba47978353 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -13,7 +13,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_HS_COLOR, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string])} ) diff --git a/homeassistant/components/futurenow/light.py b/homeassistant/components/futurenow/light.py index 8474c1073e9..d1ad6f42083 100644 --- a/homeassistant/components/futurenow/light.py +++ b/homeassistant/components/futurenow/light.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -31,7 +31,7 @@ DEVICE_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_DRIVER): vol.In(CONF_DRIVER_TYPES), vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/greenwave/light.py b/homeassistant/components/greenwave/light.py index aa592727220..89d3ca3a535 100644 --- a/homeassistant/components/greenwave/light.py +++ b/homeassistant/components/greenwave/light.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) CONF_VERSION = "version" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_VERSION): cv.positive_int} ) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 9adced828c7..228645df974 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -27,7 +27,7 @@ from homeassistant.components.light import ( ATTR_TRANSITION, ATTR_WHITE, ATTR_XY_COLOR, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -60,7 +60,7 @@ CONF_ALL = "all" # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, diff --git a/homeassistant/components/iglo/light.py b/homeassistant/components/iglo/light.py index 1cd303b8856..a31183f4489 100644 --- a/homeassistant/components/iglo/light.py +++ b/homeassistant/components/iglo/light.py @@ -14,7 +14,7 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -29,7 +29,7 @@ import homeassistant.util.color as color_util DEFAULT_NAME = "iGlo Light" DEFAULT_PORT = 8080 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index 182c12eb395..4456d112d0f 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -27,7 +27,7 @@ from homeassistant.components.light import ( EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -75,7 +75,7 @@ SUPPORT_LIMITLESSLED_RGBWW = ( LightEntityFeature.EFFECT | LightEntityFeature.FLASH | LightEntityFeature.TRANSITION ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_BRIDGES): vol.All( cv.ensure_list, diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index 272fcd4a8a1..60741c861dd 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -13,7 +13,7 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "LW-12 FC" DEFAULT_PORT = 5000 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index 4740823d85a..fe5a8ccd07d 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -26,7 +26,7 @@ from . import CONF_COMM_TYPE, DOMAIN, REQ_LOCK, MochadCtrl _LOGGER = logging.getLogger(__name__) CONF_BRIGHTNESS_LEVELS = "brightness_levels" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PLATFORM): DOMAIN, CONF_DEVICES: [ diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index 27a9cc22549..360b45cceed 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, brightness_supported, @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) SCAN_INTERVAL = timedelta(seconds=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) async def async_setup_platform( diff --git a/homeassistant/components/opple/light.py b/homeassistant/components/opple/light.py index 2fbbf6ae02a..a4aa98bbf69 100644 --- a/homeassistant/components/opple/light.py +++ b/homeassistant/components/opple/light.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "opple light" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index 50696530e8a..0254c478b42 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_TRANSITION, EFFECT_RANDOM, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -45,7 +45,7 @@ DEFAULT_ALLOW_LIGHTIFY_SWITCHES = True DEFAULT_INTERVAL_LIGHTIFY_STATUS = 5 DEFAULT_INTERVAL_LIGHTIFY_CONF = 3600 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional( diff --git a/homeassistant/components/pilight/light.py b/homeassistant/components/pilight/light.py index 60713b59475..5665e96b9c9 100644 --- a/homeassistant/components/pilight/light.py +++ b/homeassistant/components/pilight/light.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -28,7 +28,7 @@ LIGHTS_SCHEMA = SWITCHES_SCHEMA.extend( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_LIGHTS): vol.Schema({cv.string: LIGHTS_SCHEMA})} ) diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index d354e317ccb..b29bb4f1d48 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -46,7 +46,7 @@ TYPE_SWITCHABLE = "switchable" TYPE_HYBRID = "hybrid" TYPE_TOGGLE = "toggle" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({}) diff --git a/homeassistant/components/scsgate/light.py b/homeassistant/components/scsgate/light.py index a4bb78fcd1c..23b73a0fd6b 100644 --- a/homeassistant/components/scsgate/light.py +++ b/homeassistant/components/scsgate/light.py @@ -8,7 +8,11 @@ from typing import Any from scsgate.tasks import ToggleStatusTask import voluptuous as vol -from homeassistant.components.light import PLATFORM_SCHEMA, ColorMode, LightEntity +from homeassistant.components.light import ( + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, + ColorMode, + LightEntity, +) from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICES, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -17,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import CONF_SCS_ID, DOMAIN, SCSGATE_SCHEMA -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_DEVICES): cv.schema_with_slug_keys(SCSGATE_SCHEMA)} ) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index f226ed57e2a..48d555e6616 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -6,7 +6,11 @@ from typing import Any import voluptuous as vol -from homeassistant.components.light import PLATFORM_SCHEMA, ColorMode, LightEntity +from homeassistant.components.light import ( + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, + ColorMode, + LightEntity, +) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, @@ -27,7 +31,7 @@ from .const import DOMAIN as SWITCH_DOMAIN DEFAULT_NAME = "Light Switch" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_ENTITY_ID): cv.entity_domain(SWITCH_DOMAIN), diff --git a/homeassistant/components/tikteck/light.py b/homeassistant/components/tikteck/light.py index 93549b26f48..26ffc0e7b6d 100644 --- a/homeassistant/components/tikteck/light.py +++ b/homeassistant/components/tikteck/light.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -28,7 +28,7 @@ DEVICE_SCHEMA = vol.Schema( {vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} ) diff --git a/homeassistant/components/unifiled/light.py b/homeassistant/components/unifiled/light.py index f69ea5712de..4e1981875f4 100644 --- a/homeassistant/components/unifiled/light.py +++ b/homeassistant/components/unifiled/light.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -23,7 +23,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) # Validation of the user's configuration -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/x10/light.py b/homeassistant/components/x10/light.py index 8f105d9c695..29c15f66993 100644 --- a/homeassistant/components/x10/light.py +++ b/homeassistant/components/x10/light.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -22,7 +22,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_DEVICES): vol.All( cv.ensure_list, diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index 45b662846d5..0d8247fc865 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -11,7 +11,7 @@ import yeelightsunflower from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -24,7 +24,7 @@ import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) def setup_platform( diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index 6657bfb9edd..69b7c63476a 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -12,7 +12,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, ) @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) DEVICE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME): cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} ) From 348ceca19f1fe5b45dadbbd7ec96093c64409a3f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:11:22 +0200 Subject: [PATCH 0216/2411] Force alias when importing scene PLATFORM_SCHEMA (#120534) --- homeassistant/components/config/scene.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index a2e2693036a..fa23d02bcc8 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -5,7 +5,10 @@ from __future__ import annotations from typing import Any import uuid -from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.scene import ( + DOMAIN, + PLATFORM_SCHEMA as SCENE_PLATFORM_SCHEMA, +) from homeassistant.config import SCENE_CONFIG_PATH from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback @@ -40,7 +43,7 @@ def async_setup(hass: HomeAssistant) -> bool: "config", SCENE_CONFIG_PATH, cv.string, - PLATFORM_SCHEMA, + SCENE_PLATFORM_SCHEMA, post_write_hook=hook, ) ) From c49fce5541d72d4142217c33193b4996cc53f93e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:14:13 +0200 Subject: [PATCH 0217/2411] Force alias when importing sensor PLATFORM_SCHEMA (#120536) --- homeassistant/components/ads/sensor.py | 7 +++++-- homeassistant/components/alpha_vantage/sensor.py | 7 +++++-- homeassistant/components/aqualogic/sensor.py | 4 ++-- homeassistant/components/arest/sensor.py | 7 +++++-- homeassistant/components/atome/sensor.py | 4 ++-- homeassistant/components/bbox/sensor.py | 4 ++-- homeassistant/components/beewi_smartclim/sensor.py | 4 ++-- homeassistant/components/bitcoin/sensor.py | 4 ++-- homeassistant/components/bizkaibus/sensor.py | 7 +++++-- homeassistant/components/blockchain/sensor.py | 7 +++++-- homeassistant/components/bloomsky/sensor.py | 4 ++-- homeassistant/components/cert_expiry/sensor.py | 4 ++-- homeassistant/components/comed_hourly_pricing/sensor.py | 4 ++-- homeassistant/components/comfoconnect/sensor.py | 4 ++-- homeassistant/components/cups/sensor.py | 7 +++++-- homeassistant/components/currencylayer/sensor.py | 7 +++++-- homeassistant/components/delijn/sensor.py | 4 ++-- homeassistant/components/derivative/sensor.py | 8 ++++++-- homeassistant/components/discogs/sensor.py | 4 ++-- homeassistant/components/dovado/sensor.py | 4 ++-- homeassistant/components/dte_energy_bridge/sensor.py | 4 ++-- homeassistant/components/dublin_bus_transport/sensor.py | 7 +++++-- homeassistant/components/dweet/sensor.py | 7 +++++-- homeassistant/components/ebox/sensor.py | 4 ++-- homeassistant/components/eddystone_temperature/sensor.py | 4 ++-- homeassistant/components/eliqonline/sensor.py | 4 ++-- homeassistant/components/emoncms/sensor.py | 4 ++-- homeassistant/components/enocean/sensor.py | 4 ++-- .../components/entur_public_transport/sensor.py | 7 +++++-- homeassistant/components/etherscan/sensor.py | 7 +++++-- homeassistant/components/fail2ban/sensor.py | 7 +++++-- homeassistant/components/fido/sensor.py | 4 ++-- homeassistant/components/file/sensor.py | 7 +++++-- homeassistant/components/filter/sensor.py | 4 ++-- homeassistant/components/fints/sensor.py | 7 +++++-- homeassistant/components/fitbit/sensor.py | 4 ++-- homeassistant/components/fixer/sensor.py | 7 +++++-- homeassistant/components/folder/sensor.py | 4 ++-- homeassistant/components/geo_rss_events/sensor.py | 7 +++++-- homeassistant/components/gitlab_ci/sensor.py | 7 +++++-- homeassistant/components/gitter/sensor.py | 7 +++++-- homeassistant/components/google_wifi/sensor.py | 4 ++-- homeassistant/components/gpsd/sensor.py | 4 ++-- homeassistant/components/group/sensor.py | 4 ++-- homeassistant/components/haveibeenpwned/sensor.py | 7 +++++-- homeassistant/components/hddtemp/sensor.py | 4 ++-- homeassistant/components/hp_ilo/sensor.py | 7 +++++-- homeassistant/components/iammeter/sensor.py | 4 ++-- homeassistant/components/irish_rail_transport/sensor.py | 7 +++++-- homeassistant/components/kwb/sensor.py | 9 ++++++--- homeassistant/components/lacrosse/sensor.py | 4 ++-- homeassistant/components/linux_battery/sensor.py | 4 ++-- homeassistant/components/london_air/sensor.py | 7 +++++-- homeassistant/components/london_underground/sensor.py | 7 +++++-- homeassistant/components/mfi/sensor.py | 4 ++-- homeassistant/components/min_max/sensor.py | 4 ++-- homeassistant/components/mold_indicator/sensor.py | 7 +++++-- homeassistant/components/mqtt_room/sensor.py | 7 +++++-- homeassistant/components/mvglive/sensor.py | 7 +++++-- .../components/nederlandse_spoorwegen/sensor.py | 7 +++++-- homeassistant/components/netdata/sensor.py | 7 +++++-- homeassistant/components/neurio_energy/sensor.py | 4 ++-- homeassistant/components/nmbs/sensor.py | 7 +++++-- homeassistant/components/noaa_tides/sensor.py | 7 +++++-- homeassistant/components/nsw_fuel_station/sensor.py | 7 +++++-- homeassistant/components/oasa_telematics/sensor.py | 4 ++-- homeassistant/components/ohmconnect/sensor.py | 7 +++++-- homeassistant/components/openevse/sensor.py | 4 ++-- homeassistant/components/openhardwaremonitor/sensor.py | 7 +++++-- homeassistant/components/oru/sensor.py | 6 ++++-- homeassistant/components/otp/sensor.py | 7 +++++-- homeassistant/components/pilight/sensor.py | 7 +++++-- homeassistant/components/pocketcasts/sensor.py | 7 +++++-- homeassistant/components/pyload/sensor.py | 4 ++-- homeassistant/components/raincloud/sensor.py | 7 +++++-- homeassistant/components/random/sensor.py | 7 +++++-- homeassistant/components/reddit/sensor.py | 7 +++++-- homeassistant/components/rejseplanen/sensor.py | 7 +++++-- homeassistant/components/rflink/sensor.py | 4 ++-- homeassistant/components/ripple/sensor.py | 7 +++++-- homeassistant/components/rmvtransport/sensor.py | 7 +++++-- homeassistant/components/rova/sensor.py | 4 ++-- homeassistant/components/rtorrent/sensor.py | 4 ++-- homeassistant/components/saj/sensor.py | 4 ++-- homeassistant/components/serial/sensor.py | 7 +++++-- homeassistant/components/serial_pm/sensor.py | 7 +++++-- homeassistant/components/seventeentrack/sensor.py | 7 +++++-- homeassistant/components/shodan/sensor.py | 7 +++++-- homeassistant/components/sigfox/sensor.py | 7 +++++-- homeassistant/components/simulated/sensor.py | 7 +++++-- homeassistant/components/skybeacon/sensor.py | 4 ++-- homeassistant/components/snmp/sensor.py | 7 +++++-- homeassistant/components/solaredge_local/sensor.py | 4 ++-- homeassistant/components/starlingbank/sensor.py | 7 +++++-- homeassistant/components/startca/sensor.py | 4 ++-- homeassistant/components/supervisord/sensor.py | 7 +++++-- .../components/swiss_hydrological_data/sensor.py | 7 +++++-- homeassistant/components/tank_utility/sensor.py | 7 +++++-- homeassistant/components/tcp/sensor.py | 4 ++-- homeassistant/components/ted5000/sensor.py | 4 ++-- homeassistant/components/tellstick/sensor.py | 4 ++-- homeassistant/components/temper/sensor.py | 4 ++-- homeassistant/components/thermoworks_smoke/sensor.py | 4 ++-- homeassistant/components/thinkingcleaner/sensor.py | 4 ++-- homeassistant/components/time_date/sensor.py | 4 ++-- homeassistant/components/tmb/sensor.py | 7 +++++-- homeassistant/components/torque/sensor.py | 7 +++++-- homeassistant/components/transport_nsw/sensor.py | 4 ++-- homeassistant/components/travisci/sensor.py | 4 ++-- homeassistant/components/uk_transport/sensor.py | 7 +++++-- homeassistant/components/vasttrafik/sensor.py | 7 +++++-- homeassistant/components/viaggiatreno/sensor.py | 7 +++++-- homeassistant/components/volkszaehler/sensor.py | 4 ++-- homeassistant/components/vultr/sensor.py | 4 ++-- homeassistant/components/wirelesstag/sensor.py | 4 ++-- homeassistant/components/worldclock/sensor.py | 7 +++++-- homeassistant/components/worldtidesinfo/sensor.py | 7 +++++-- homeassistant/components/worxlandroid/sensor.py | 7 +++++-- homeassistant/components/wsdot/sensor.py | 7 +++++-- homeassistant/components/yandex_transport/sensor.py | 4 ++-- homeassistant/components/zabbix/sensor.py | 7 +++++-- homeassistant/components/zestimate/sensor.py | 7 +++++-- homeassistant/components/zoneminder/sensor.py | 4 ++-- 123 files changed, 448 insertions(+), 247 deletions(-) diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py index 4bcc8f776df..483fe2cd725 100644 --- a/homeassistant/components/ads/sensor.py +++ b/homeassistant/components/ads/sensor.py @@ -4,7 +4,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -22,7 +25,7 @@ from . import ( ) DEFAULT_NAME = "ADS sensor" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ADS_VAR): cv.string, vol.Optional(CONF_ADS_FACTOR): cv.positive_int, diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index dc62a734d42..506cb41659a 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -10,7 +10,10 @@ from alpha_vantage.timeseries import TimeSeries import voluptuous as vol from homeassistant.components import persistent_notification -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_CURRENCY, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -59,7 +62,7 @@ CURRENCY_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_FOREIGN_EXCHANGE): vol.All(cv.ensure_list, [CURRENCY_SCHEMA]), diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index bdb582826dc..9c2ee9957af 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -7,7 +7,7 @@ from dataclasses import dataclass import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -104,7 +104,7 @@ SENSOR_TYPES: tuple[AquaLogicSensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( cv.ensure_list, [vol.In(SENSOR_KEYS)] diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index 917b255ef14..ab502fa275a 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -9,7 +9,10 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_MONITORED_VARIABLES, CONF_NAME, @@ -41,7 +44,7 @@ PIN_VARIABLE_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 84751b84855..fd8250e899f 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -9,7 +9,7 @@ from pyatome.client import AtomeClient, PyAtomeError import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -49,7 +49,7 @@ WEEKLY_TYPE = "week" MONTHLY_TYPE = "month" YEARLY_TYPE = "year" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 858ad6c6e47..72fa870efbf 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -10,7 +10,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -80,7 +80,7 @@ SENSOR_TYPES_UPTIME: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in (*SENSOR_TYPES, *SENSOR_TYPES_UPTIME)] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_VARIABLES): vol.All( cv.ensure_list, [vol.In(SENSOR_KEYS)] diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index 3aaf4daaa80..1c80f62e64f 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -6,7 +6,7 @@ from beewi_smartclim import BeewiSmartClimPoller import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -26,7 +26,7 @@ SENSOR_TYPES = [ [SensorDeviceClass.BATTERY, "Battery", PERCENTAGE], ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index e003362ac7e..e4da2ddc2f4 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -9,7 +9,7 @@ from blockchain import exchangerates, statistics import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) @@ -127,7 +127,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( OPTION_KEYS = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_DISPLAY_OPTIONS, default=[]): vol.All( cv.ensure_list, [vol.In(OPTION_KEYS)] diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py index ff7d28b96c7..3efddf0b0d7 100644 --- a/homeassistant/components/bizkaibus/sensor.py +++ b/homeassistant/components/bizkaibus/sensor.py @@ -7,7 +7,10 @@ from contextlib import suppress from bizkaibus.bizkaibus import BizkaibusData import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -21,7 +24,7 @@ CONF_ROUTE = "route" DEFAULT_NAME = "Next bus" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STOP_ID): cv.string, vol.Required(CONF_ROUTE): cv.string, diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index dafd47bcb20..8ae091fa95e 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -8,7 +8,10 @@ import logging from pyblockchain import get_balance, validate_address import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -23,7 +26,7 @@ DEFAULT_NAME = "Bitcoin Balance" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ADDRESSES): [cv.string], vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 1f63b4a7256..6d99506bd44 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -63,7 +63,7 @@ SENSOR_DEVICE_CLASS = { # Which sensors to format numerically FORMAT_NUMBERS = ["Temperature", "Pressure", "Voltage"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES): vol.All( cv.ensure_list, [vol.In(SENSOR_TYPES)] diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 674f7bb6341..f52ff8a40d8 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -8,7 +8,7 @@ from typing import Any import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -27,7 +27,7 @@ from .const import DEFAULT_PORT, DOMAIN SCAN_INTERVAL = timedelta(hours=12) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index 770866aa319..b47255828e8 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -11,7 +11,7 @@ import aiohttp import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) @@ -57,7 +57,7 @@ SENSORS_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_MONITORED_FEEDS): [SENSORS_SCHEMA]} ) diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 97cb7fc61eb..25726b3789b 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -31,7 +31,7 @@ from pycomfoconnect import ( import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -263,7 +263,7 @@ SENSOR_TYPES = ( ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_RESOURCES, default=[]): vol.All( cv.ensure_list, [vol.In([desc.key for desc in SENSOR_TYPES])] diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 647deee79a6..7f45e99f93d 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -9,7 +9,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_HOST, CONF_PORT, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -47,7 +50,7 @@ SCAN_INTERVAL = timedelta(minutes=1) PRINTER_STATES = {3: "idle", 4: "printing", 5: "stopped"} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_PRINTERS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_IS_CUPS_SERVER, default=DEFAULT_IS_CUPS_SERVER): cv.boolean, diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index 2fdf521ad9f..2ad0f88a2ab 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -8,7 +8,10 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_BASE, CONF_NAME, CONF_QUOTE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -24,7 +27,7 @@ DEFAULT_NAME = "CurrencyLayer Sensor" SCAN_INTERVAL = timedelta(hours=4) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_QUOTE): vol.All(cv.ensure_list, [cv.string]), diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index 5693a00e857..017a4c5b2fa 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -10,7 +10,7 @@ from pydelijn.common import HttpException import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -31,7 +31,7 @@ CONF_NUMBER_OF_DEPARTURES = "number_of_departures" DEFAULT_NAME = "De Lijn" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_NEXT_DEPARTURE): [ diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index fd430c6ef4d..36719b43ccb 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -9,7 +9,11 @@ from typing import TYPE_CHECKING import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, RestoreSensor, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + RestoreSensor, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -62,7 +66,7 @@ UNIT_TIME = { DEFAULT_ROUND = 3 DEFAULT_TIME_WINDOW = 0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_SOURCE): cv.entity_id, diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index 4a732130485..3cea6ec4dac 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -10,7 +10,7 @@ import discogs_client import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) @@ -58,7 +58,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ) SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_TOKEN): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index bd53fb22ad2..013b51bfc8f 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -9,7 +9,7 @@ import re import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -78,7 +78,7 @@ SENSOR_TYPES: tuple[DovadoSensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSOR_KEYS)])} ) diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index 112ebd55f94..a0b9253034e 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -9,7 +9,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -30,7 +30,7 @@ DEFAULT_NAME = "Current Energy Usage" DEFAULT_VERSION = 1 DOMAIN = "dte_energy_bridge" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_IP_ADDRESS): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index 3f9c57456f8..91773d08142 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -13,7 +13,10 @@ from http import HTTPStatus import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -38,7 +41,7 @@ DEFAULT_NAME = "Next Bus" SCAN_INTERVAL = timedelta(minutes=1) TIME_STR_FORMAT = "%H:%M" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STOP_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py index 79e25bec0c1..01e0567ac8d 100644 --- a/homeassistant/components/dweet/sensor.py +++ b/homeassistant/components/dweet/sensor.py @@ -9,7 +9,10 @@ import logging import dweepy import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_DEVICE, CONF_NAME, @@ -27,7 +30,7 @@ DEFAULT_NAME = "Dweet.io Sensor" SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_DEVICE): cv.string, vol.Required(CONF_VALUE_TEMPLATE): cv.template, diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index aff154cca02..691e9dd8275 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -13,7 +13,7 @@ from pyebox.client import PyEboxError import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -139,7 +139,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_TYPE_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_VARIABLES): vol.All( cv.ensure_list, [vol.In(SENSOR_TYPE_KEYS)] diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index b136b193686..637beffcf94 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -12,7 +12,7 @@ from beacontools import BeaconScanner, EddystoneFilter, EddystoneTLMFrame import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -43,7 +43,7 @@ BEACON_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_BT_DEVICE_ID, default=0): cv.positive_int, vol.Required(CONF_BEACONS): vol.Schema({cv.string: BEACON_SCHEMA}), diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index 2aa0ab15746..7c9f76824e8 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -9,7 +9,7 @@ import eliqonline import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -29,7 +29,7 @@ DEFAULT_NAME = "ELIQ Online" SCAN_INTERVAL = timedelta(seconds=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Required(CONF_CHANNEL_ID): cv.positive_int, diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 97c69619fa9..e239ffd6c21 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -10,7 +10,7 @@ from pyemoncms import EmoncmsClient import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -54,7 +54,7 @@ DEFAULT_UNIT = UnitOfPower.WATT ONLY_INCL_EXCL_NONE = "only_include_exclude_or_none" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_URL): cv.string, diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index c22a7d95760..177c95c2832 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -9,7 +9,7 @@ from enocean.utils import combine_hex import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, RestoreSensor, SensorDeviceClass, SensorEntityDescription, @@ -87,7 +87,7 @@ SENSOR_DESC_WINDOWHANDLE = EnOceanSensorEntityDescription( ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py index 70b86d0271f..f88bb99cea0 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -8,7 +8,10 @@ from random import randint from enturclient import EnturPublicTransportData import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -46,7 +49,7 @@ ICONS = { SCAN_INTERVAL = timedelta(seconds=45) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STOP_IDS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EXPAND_PLATFORMS, default=True): cv.boolean, diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py index 38219bf659b..e64b596a119 100644 --- a/homeassistant/components/etherscan/sensor.py +++ b/homeassistant/components/etherscan/sensor.py @@ -7,7 +7,10 @@ from datetime import timedelta from pyetherscan import get_balance import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TOKEN from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -18,7 +21,7 @@ CONF_TOKEN_ADDRESS = "token_address" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ADDRESS): cv.string, vol.Optional(CONF_NAME): cv.string, diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index 53490e60c54..9e6d23556d2 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -9,7 +9,10 @@ import re import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_FILE_PATH, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -27,7 +30,7 @@ STATE_CURRENT_BANS = "current_bans" STATE_ALL_BANS = "total_bans" SCAN_INTERVAL = timedelta(seconds=120) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_JAILS): vol.All(cv.ensure_list, vol.Length(min=1)), vol.Optional(CONF_FILE_PATH): cv.isfile, diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index d2169ae32e8..bc6e6340111 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -14,7 +14,7 @@ from pyfido.client import PyFidoError import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -172,7 +172,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_VARIABLES): vol.All( cv.ensure_list, [vol.In(SENSOR_KEYS)] diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index fa04ae7c62a..fda0d14a6aa 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -8,7 +8,10 @@ import os from file_read_backwards import FileReadBackwards import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_FILE_PATH, @@ -26,7 +29,7 @@ from .const import DEFAULT_NAME, FILE_ICON _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FILE_PATH): cv.isfile, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index decb1f0a33f..549d74ffd09 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -20,7 +20,7 @@ from homeassistant.components.recorder import get_instance, history from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -150,7 +150,7 @@ FILTER_TIME_THROTTLE_SCHEMA = FILTER_SCHEMA.extend( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): vol.Any( cv.entity_domain(SENSOR_DOMAIN), diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 4a4c2d05181..2f47fdc09eb 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -12,7 +12,10 @@ from fints.client import FinTS3PinTanClient from fints.models import SEPAAccount import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_PIN, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -43,7 +46,7 @@ SCHEMA_ACCOUNTS = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_BIN): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 6df4968739f..ab9a593e195 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -18,7 +18,7 @@ from homeassistant.components.application_credentials import ( async_import_client_credential, ) from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -538,7 +538,7 @@ FITBIT_RESOURCES_KEYS: Final[list[str]] = [ for desc in (*FITBIT_RESOURCES_LIST, FITBIT_RESOURCE_BATTERY, SLEEP_START_TIME) ] -PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_MONITORED_RESOURCES, default=FITBIT_DEFAULT_RESOURCES diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index 10f05ca29f8..4a03de5d6de 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -9,7 +9,10 @@ from fixerio import Fixerio from fixerio.exceptions import FixerioException import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_TARGET from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -27,7 +30,7 @@ DEFAULT_NAME = "Exchange rate" SCAN_INTERVAL = timedelta(days=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_TARGET): cv.string, diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index 6c8e4fc63a9..3a8a4fdc380 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -10,7 +10,7 @@ import os import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -28,7 +28,7 @@ DEFAULT_FILTER = "*" SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_FOLDER_PATHS): cv.isdir, vol.Optional(CONF_FILTER, default=DEFAULT_FILTER): cv.string, diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index 8c704bcf16a..0dc8918b7dd 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -14,7 +14,10 @@ from georss_client import UPDATE_OK, UPDATE_OK_NO_DATA from georss_generic_client import GenericFeed import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -46,7 +49,7 @@ DOMAIN = "geo_rss_events" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_URL): cv.string, vol.Optional(CONF_LATITUDE): cv.latitude, diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py index d247ef5af60..6ed3112b2af 100644 --- a/homeassistant/components/gitlab_ci/sensor.py +++ b/homeassistant/components/gitlab_ci/sensor.py @@ -8,7 +8,10 @@ import logging from gitlab import Gitlab, GitlabAuthenticationError, GitlabGetError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_URL from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -39,7 +42,7 @@ ICON_SAD = "mdi:emoticon-sad" SCAN_INTERVAL = timedelta(seconds=300) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_GITLAB_ID): cv.string, vol.Required(CONF_TOKEN): cv.string, diff --git a/homeassistant/components/gitter/sensor.py b/homeassistant/components/gitter/sensor.py index 056c275c785..bc444655908 100644 --- a/homeassistant/components/gitter/sensor.py +++ b/homeassistant/components/gitter/sensor.py @@ -8,7 +8,10 @@ from gitterpy.client import GitterClient from gitterpy.errors import GitterRoomError, GitterTokenError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_ROOM from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -25,7 +28,7 @@ DEFAULT_NAME = "Gitter messages" DEFAULT_ROOM = "home-assistant/home-assistant" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index 776fb44a51b..3dd421d99da 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -10,7 +10,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) @@ -93,7 +93,7 @@ SENSOR_TYPES: tuple[GoogleWifiSensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index bc08b7b6203..5a978f9f66e 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -15,7 +15,7 @@ from gps3.agps3threaded import ( import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -70,7 +70,7 @@ SENSOR_TYPES: tuple[GpsdSensorDescription, ...] = ( ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 2e6c321be1e..eaaedcf0e46 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -17,7 +17,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_UNITS, DEVICE_CLASSES_SCHEMA, DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, STATE_CLASSES_SCHEMA, UNIT_CONVERTERS, SensorDeviceClass, @@ -93,7 +93,7 @@ SENSOR_TYPE_TO_ATTR = {v: k for k, v in SENSOR_TYPES.items()} # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain( [DOMAIN, NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN] diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index 9933ba11945..1aebe696e82 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -9,7 +9,10 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_EMAIL from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -30,7 +33,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) URL = "https://haveibeenpwned.com/api/v3/breachedaccount/" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_EMAIL): vol.All(cv.ensure_list, [cv.string]), vol.Required(CONF_API_KEY): cv.string, diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index 3dda9f44004..836e68abe9f 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -10,7 +10,7 @@ from telnetlib import Telnet # pylint: disable=deprecated-module import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -38,7 +38,7 @@ DEFAULT_TIMEOUT = 5 SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_DISKS, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index 8d29b20381d..85908a45af4 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -8,7 +8,10 @@ import logging import hpilo import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_HOST, CONF_MONITORED_VARIABLES, @@ -47,7 +50,7 @@ SENSOR_TYPES = { "network_settings": ["Network Settings", "get_network_settings"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/iammeter/sensor.py b/homeassistant/components/iammeter/sensor.py index a3922b06980..1069c6696fc 100644 --- a/homeassistant/components/iammeter/sensor.py +++ b/homeassistant/components/iammeter/sensor.py @@ -12,7 +12,7 @@ from iammeter.client import IamMeter import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -46,7 +46,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 80 DEFAULT_DEVICE_NAME = "IamMeter" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_DEVICE_NAME): cv.string, diff --git a/homeassistant/components/irish_rail_transport/sensor.py b/homeassistant/components/irish_rail_transport/sensor.py index b0ad9372f86..a96846558fa 100644 --- a/homeassistant/components/irish_rail_transport/sensor.py +++ b/homeassistant/components/irish_rail_transport/sensor.py @@ -7,7 +7,10 @@ from datetime import timedelta from pyirishrail.pyirishrail import IrishRailRTPI import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -36,7 +39,7 @@ DEFAULT_NAME = "Next Train" SCAN_INTERVAL = timedelta(minutes=2) TIME_STR_FORMAT = "%H:%M" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STATION): cv.string, vol.Optional(CONF_DIRECTION): cv.string, diff --git a/homeassistant/components/kwb/sensor.py b/homeassistant/components/kwb/sensor.py index e55b90cf89f..dbe57f9a517 100644 --- a/homeassistant/components/kwb/sensor.py +++ b/homeassistant/components/kwb/sensor.py @@ -5,7 +5,10 @@ from __future__ import annotations from pykwb import kwb import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_DEVICE, CONF_HOST, @@ -27,7 +30,7 @@ MODE_TCP = 1 CONF_RAW = "raw" -SERIAL_SCHEMA = PLATFORM_SCHEMA.extend( +SERIAL_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_RAW, default=DEFAULT_RAW): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -36,7 +39,7 @@ SERIAL_SCHEMA = PLATFORM_SCHEMA.extend( } ) -ETHERNET_SCHEMA = PLATFORM_SCHEMA.extend( +ETHERNET_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_RAW, default=DEFAULT_RAW): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index c059248b422..d7df7a08e76 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -60,7 +60,7 @@ SENSOR_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), vol.Optional(CONF_BAUD, default=DEFAULT_BAUD): cv.positive_int, diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index 9dc0e8c675d..789195e1169 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -9,7 +9,7 @@ from batinfo import Batteries import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -49,7 +49,7 @@ DEFAULT_SYSTEM = "linux" SYSTEMS = ["android", "linux"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_BATTERY, default=DEFAULT_BATTERY): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py index 0895e507a85..c7b7abd51ed 100644 --- a/homeassistant/components/london_air/sensor.py +++ b/homeassistant/components/london_air/sensor.py @@ -9,7 +9,10 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -54,7 +57,7 @@ AUTHORITIES = [ URL = "http://api.erg.kcl.ac.uk/AirQuality/Hourly/MonitoringIndex/GroupName=London/Json" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_LOCATIONS, default=AUTHORITIES): vol.All( cv.ensure_list, [vol.In(AUTHORITIES)] diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index e5735aa7fba..015f7e8ecdc 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -8,7 +8,10 @@ from typing import Any from london_tube_status import TubeData import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -22,7 +25,7 @@ from .coordinator import LondonTubeCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_LINE): vol.All(cv.ensure_list, [vol.In(list(TUBE_LINES))])} ) diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index afa5e00bf02..b93cc669e62 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -9,7 +9,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -45,7 +45,7 @@ SENSOR_MODELS = [ "Input Digital", ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index f34067fea2e..89252a58864 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -10,7 +10,7 @@ from typing import Any import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorStateClass, ) @@ -59,7 +59,7 @@ SENSOR_TYPES = { } SENSOR_TYPE_TO_ATTR = {v: k for k, v in SENSOR_TYPES.items()} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TYPE, default=SENSOR_TYPES[ATTR_MAX_VALUE]): vol.All( cv.string, vol.In(SENSOR_TYPES.values()) diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index cbb531d9672..9064e0387e5 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -8,7 +8,10 @@ import math import voluptuous as vol from homeassistant import util -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, @@ -46,7 +49,7 @@ DEFAULT_NAME = "Mold Indicator" MAGNUS_K2 = 17.62 MAGNUS_K3 = 243.12 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_INDOOR_TEMP): cv.entity_id, vol.Required(CONF_OUTDOOR_TEMP): cv.entity_id, diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index df0be7b4968..849d4562423 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -11,7 +11,10 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import CONF_STATE_TOPIC -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( ATTR_DEVICE_ID, ATTR_ID, @@ -40,7 +43,7 @@ DEFAULT_NAME = "Room Sensor" DEFAULT_TIMEOUT = 5 DEFAULT_TOPIC = "room_presence" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_DEVICE_ID): cv.string, vol.Required(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index 6aefa83d4bb..966bfebb577 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -9,7 +9,10 @@ import logging import MVGLive import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -44,7 +47,7 @@ ATTRIBUTION = "Data provided by MVG-live.de" SCAN_INTERVAL = timedelta(seconds=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NEXT_DEPARTURE): [ { diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 33828e65019..ce3e7d3a002 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -10,7 +10,10 @@ from ns_api import RequestParametersError import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -42,7 +45,7 @@ ROUTE_SCHEMA = vol.Schema( ROUTES_SCHEMA = vol.All(cv.ensure_list, [ROUTE_SCHEMA]) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_ROUTES): ROUTES_SCHEMA} ) diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index abbb3bbb6c9..b77a4392ef4 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -8,7 +8,10 @@ from netdata import Netdata from netdata.exceptions import NetdataError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_HOST, CONF_ICON, @@ -44,7 +47,7 @@ RESOURCE_SCHEMA = vol.Any( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py index 4a7ce43a0d7..a02a37b740d 100644 --- a/homeassistant/components/neurio_energy/sensor.py +++ b/homeassistant/components/neurio_energy/sensor.py @@ -10,7 +10,7 @@ import requests.exceptions import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -38,7 +38,7 @@ DAILY_TYPE = "daily" MIN_TIME_BETWEEN_DAILY_UPDATES = timedelta(seconds=150) MIN_TIME_BETWEEN_ACTIVE_UPDATES = timedelta(seconds=10) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_SECRET): cv.string, diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index a684b47e245..82fc6143b2d 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -7,7 +7,10 @@ import logging from pyrail import iRail import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, @@ -35,7 +38,7 @@ CONF_STATION_TO = "station_to" CONF_STATION_LIVE = "station_live" CONF_EXCLUDE_VIAS = "exclude_vias" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STATION_FROM): cv.string, vol.Required(CONF_STATION_TO): cv.string, diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index 5e213e847ba..b165478927e 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -10,7 +10,10 @@ import noaa_coops as coops import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_TIME_ZONE, CONF_UNIT_SYSTEM from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -34,7 +37,7 @@ SCAN_INTERVAL = timedelta(minutes=60) TIMEZONES = ["gmt", "lst", "lst_ldt"] UNIT_SYSTEMS = ["english", "metric"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STATION_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 7f28a9d28f2..f99790664da 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -6,7 +6,10 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CURRENCY_CENT, UnitOfVolume from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -41,7 +44,7 @@ CONF_ALLOWED_FUEL_TYPES = [ ] CONF_DEFAULT_FUEL_TYPES = ["E10", "U91"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STATION_ID): cv.positive_int, vol.Optional(CONF_FUEL_TYPES, default=CONF_DEFAULT_FUEL_TYPES): vol.All( diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 2a68c7ce15d..fef4cef48af 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -10,7 +10,7 @@ import oasatelematics import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -39,7 +39,7 @@ DEFAULT_NAME = "OASA Telematics" SCAN_INTERVAL = timedelta(seconds=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STOP_ID): cv.string, vol.Required(CONF_ROUTE_ID): cv.string, diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index 2598e5fe514..b32db33cc2d 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -9,7 +9,10 @@ import defusedxml.ElementTree as ET import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_ID, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -23,7 +26,7 @@ DEFAULT_NAME = "OhmConnect Status" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index b2360b13a6f..c228b6c1a14 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -9,7 +9,7 @@ from requests import RequestException import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -80,7 +80,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_MONITORED_VARIABLES, default=["status"]): vol.All( diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index 4e15ca3dd57..4ef71a6c75f 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -8,7 +8,10 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -36,7 +39,7 @@ OHM_MAX = "Max" OHM_CHILDREN = "Children" OHM_NAME = "Text" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=8085): cv.port} ) diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py index b1d814dd98a..213350db6a4 100644 --- a/homeassistant/components/oru/sensor.py +++ b/homeassistant/components/oru/sensor.py @@ -9,7 +9,7 @@ from oru import Meter, MeterError import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -25,7 +25,9 @@ CONF_METER_NUMBER = "meter_number" SCAN_INTERVAL = timedelta(minutes=15) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_METER_NUMBER): cv.string}) +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_METER_NUMBER): cv.string} +) def setup_platform( diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index 466fc994cdb..2e166859729 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -7,7 +7,10 @@ import time import pyotp import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_TOKEN from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback @@ -21,7 +24,7 @@ from .const import DEFAULT_NAME, DOMAIN TIME_STEP = 30 # Default time step assumed by Google Authenticator -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_TOKEN): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/pilight/sensor.py b/homeassistant/components/pilight/sensor.py index 003e3428bdd..8e5f3b7d78a 100644 --- a/homeassistant/components/pilight/sensor.py +++ b/homeassistant/components/pilight/sensor.py @@ -6,7 +6,10 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_PAYLOAD, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -20,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) CONF_VARIABLE = "variable" DEFAULT_NAME = "Pilight Sensor" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_VARIABLE): cv.string, vol.Required(CONF_PAYLOAD): vol.Schema(dict), diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py index b2ad050fc14..1f6af298688 100644 --- a/homeassistant/components/pocketcasts/sensor.py +++ b/homeassistant/components/pocketcasts/sensor.py @@ -8,7 +8,10 @@ import logging from pycketcasts import pocketcasts import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -22,7 +25,7 @@ SENSOR_NAME = "Pocketcasts unlistened episodes" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string} ) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 3d681c4b65d..6cb432e12fd 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -7,7 +7,7 @@ from enum import StrEnum import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -87,7 +87,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_MONITORED_VARIABLES, default=["speed"]): vol.All( diff --git a/homeassistant/components/raincloud/sensor.py b/homeassistant/components/raincloud/sensor.py index 34cd3f213ed..34a7cf73490 100644 --- a/homeassistant/components/raincloud/sensor.py +++ b/homeassistant/components/raincloud/sensor.py @@ -6,7 +6,10 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -24,7 +27,7 @@ from . import ( _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( cv.ensure_list, [vol.In(SENSORS)] diff --git a/homeassistant/components/random/sensor.py b/homeassistant/components/random/sensor.py index 716350b2bb0..3c6e67c9918 100644 --- a/homeassistant/components/random/sensor.py +++ b/homeassistant/components/random/sensor.py @@ -8,7 +8,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -30,7 +33,7 @@ ATTR_MINIMUM = "minimum" DEFAULT_NAME = "Random sensor" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MAXIMUM, default=DEFAULT_MAX): cv.positive_int, vol.Optional(CONF_MINIMUM, default=DEFAULT_MIN): cv.positive_int, diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 47aa2ab86f6..35962ac091b 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -8,7 +8,10 @@ import logging import praw import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( ATTR_ID, CONF_CLIENT_ID, @@ -44,7 +47,7 @@ LIST_TYPES = ["top", "controversial", "hot", "new"] SCAN_INTERVAL = timedelta(seconds=300) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string, diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index d95b9e1b271..40b27014211 100644 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -14,7 +14,10 @@ from operator import itemgetter import rjpl import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -51,7 +54,7 @@ BUS_TYPES = ["BUS", "EXB", "TB"] TRAIN_TYPES = ["LET", "S", "REG", "IC", "LYN", "TOG"] METRO_TYPES = ["M"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STOP_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index b01d1f709fe..f3c3df7f46b 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -8,7 +8,7 @@ from rflink.parser import PACKET_FIELDS, UNITS import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -262,7 +262,7 @@ SENSOR_TYPES = ( SENSOR_TYPES_DICT = {desc.key: desc for desc in SENSOR_TYPES} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean, vol.Optional(CONF_DEVICES, default={}): { diff --git a/homeassistant/components/ripple/sensor.py b/homeassistant/components/ripple/sensor.py index 1b65ec7ae09..72510ea251d 100644 --- a/homeassistant/components/ripple/sensor.py +++ b/homeassistant/components/ripple/sensor.py @@ -7,7 +7,10 @@ from datetime import timedelta from pyripple import get_balance import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -18,7 +21,7 @@ DEFAULT_NAME = "Ripple Balance" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ADDRESS): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index e53423d3b14..e8b976129c5 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -13,7 +13,10 @@ from RMVtransport.rmvtransport import ( ) import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_TIMEOUT, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -55,7 +58,7 @@ ATTRIBUTION = "Data provided by opendata.rmv.de" SCAN_INTERVAL = timedelta(seconds=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NEXT_DEPARTURE): [ { diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index f63b9893c02..e44e84f52fa 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -7,7 +7,7 @@ from datetime import datetime import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -47,7 +47,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ZIP_CODE): cv.string, vol.Required(CONF_HOUSE_NUMBER): cv.string, diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index 099927f1893..654288927d3 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -8,7 +8,7 @@ import xmlrpc.client import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -84,7 +84,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_URL): cv.url, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 75b56c98ac3..c8b40fd5476 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -11,7 +11,7 @@ import pysaj import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -53,7 +53,7 @@ SAJ_UNIT_MAPPINGS = { "°C": UnitOfTemperature.CELSIUS, } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string, diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 9d60877bd1b..e3fee36c09e 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -10,7 +10,10 @@ from serial import SerialException import serial_asyncio_fast as serial_asyncio import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -37,7 +40,7 @@ DEFAULT_XONXOFF = False DEFAULT_RTSCTS = False DEFAULT_DSRDTR = False -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SERIAL_PORT): cv.string, vol.Optional(CONF_BAUDRATE, default=DEFAULT_BAUDRATE): cv.positive_int, diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py index 00ac4fe8731..b454424591d 100644 --- a/homeassistant/components/serial_pm/sensor.py +++ b/homeassistant/components/serial_pm/sensor.py @@ -7,7 +7,10 @@ import logging from pmsensor import serial_pm as pm import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -19,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) CONF_BRAND = "brand" CONF_SERIAL_DEVICE = "serial_device" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_BRAND): cv.string, vol.Required(CONF_SERIAL_DEVICE): cv.string, diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index acc8471c030..fa6f283427d 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol from homeassistant.components import persistent_notification -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_FRIENDLY_NAME, @@ -46,7 +49,7 @@ from .const import ( VALUE_DELIVERED, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/shodan/sensor.py b/homeassistant/components/shodan/sensor.py index fd608cbcb45..867b58ad1ba 100644 --- a/homeassistant/components/shodan/sensor.py +++ b/homeassistant/components/shodan/sensor.py @@ -8,7 +8,10 @@ import logging import shodan import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -23,7 +26,7 @@ DEFAULT_NAME = "Shodan Sensor" SCAN_INTERVAL = timedelta(minutes=15) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_QUERY): cv.string, diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index fbda6fece21..8f9190e4436 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -11,7 +11,10 @@ from urllib.parse import urljoin import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -26,7 +29,7 @@ CONF_API_LOGIN = "api_login" CONF_API_PASSWORD = "api_password" DEFAULT_NAME = "sigfox" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_LOGIN): cv.string, vol.Required(CONF_API_PASSWORD): cv.string, diff --git a/homeassistant/components/simulated/sensor.py b/homeassistant/components/simulated/sensor.py index 51ec19ac80b..b4180ba300d 100644 --- a/homeassistant/components/simulated/sensor.py +++ b/homeassistant/components/simulated/sensor.py @@ -8,7 +8,10 @@ from random import Random import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -35,7 +38,7 @@ DEFAULT_SEED = 999 DEFAULT_UNIT = "value" DEFAULT_RELATIVE_TO_EPOCH = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_AMP, default=DEFAULT_AMP): vol.Coerce(float), vol.Optional(CONF_FWHM, default=DEFAULT_FWHM): vol.Coerce(float), diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index 257ea2e92fa..a3a5eb48098 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -12,7 +12,7 @@ from pygatt.exceptions import BLEError, NotConnectedError, NotificationTimeout import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -44,7 +44,7 @@ DEFAULT_NAME = "Skybeacon" SKIP_HANDLE_LOOKUP = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 0e5b215dcd4..fb7b87403cb 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -20,7 +20,10 @@ from pysnmp.proto.rfc1902 import Opaque from pysnmp.proto.rfc1905 import NoSuchObject import voluptuous as vol -from homeassistant.components.sensor import CONF_STATE_CLASS, PLATFORM_SCHEMA +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, +) from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_HOST, @@ -83,7 +86,7 @@ TRIGGER_ENTITY_OPTIONS = ( CONF_UNIT_OF_MEASUREMENT, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_BASEOID): cv.string, vol.Optional(CONF_ACCEPT_ERRORS, default=False): cv.boolean, diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index ae009410692..a7940aa34b5 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -13,7 +13,7 @@ from solaredge_local import SolarEdge import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -193,7 +193,7 @@ SENSOR_TYPES_ENERGY_EXPORT: tuple[SolarEdgeLocalSensorEntityDescription, ...] = ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_IP_ADDRESS): cv.string, vol.Optional(CONF_NAME, default="SolarEdge"): cv.string, diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py index f6b11a41102..fd351416c28 100644 --- a/homeassistant/components/starlingbank/sensor.py +++ b/homeassistant/components/starlingbank/sensor.py @@ -9,7 +9,10 @@ import requests from starlingbank import StarlingAccount import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -41,7 +44,7 @@ ACCOUNT_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_ACCOUNTS): vol.Schema([ACCOUNT_SCHEMA])} ) diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index fad001d6d29..5fc4872a754 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -12,7 +12,7 @@ import voluptuous as vol import xmltodict from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -127,7 +127,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_VARIABLES): vol.All( cv.ensure_list, [vol.In(SENSOR_KEYS)] diff --git a/homeassistant/components/supervisord/sensor.py b/homeassistant/components/supervisord/sensor.py index 7939232cd6f..24189fb7de0 100644 --- a/homeassistant/components/supervisord/sensor.py +++ b/homeassistant/components/supervisord/sensor.py @@ -7,7 +7,10 @@ import xmlrpc.client import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -21,7 +24,7 @@ ATTR_GROUP = "group" DEFAULT_URL = "http://localhost:9001/RPC2" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url} ) diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index e74d1f66046..c67045521b5 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -8,7 +8,10 @@ import logging from swisshydrodata import SwissHydroData import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -46,7 +49,7 @@ CONDITION_DETAILS = [ ATTR_MIN_24H, ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STATION): vol.Coerce(int), vol.Optional(CONF_MONITORED_CONDITIONS, default=[SENSOR_TEMPERATURE]): vol.All( diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index b4d972f7c06..9bdcc1b6f4f 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -9,7 +9,10 @@ import requests from tank_utility import auth, device as tank_monitor import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_DEVICES, CONF_EMAIL, CONF_PASSWORD, PERCENTAGE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -20,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = datetime.timedelta(hours=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_EMAIL): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index 6c1e6563c50..a3bd4b2c619 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from typing import Final from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, ) from homeassistant.const import CONF_UNIT_OF_MEASUREMENT @@ -15,7 +15,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateTyp from .common import TCP_PLATFORM_SCHEMA, TcpEntity -PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend(TCP_PLATFORM_SCHEMA) +PLATFORM_SCHEMA: Final = SENSOR_PLATFORM_SCHEMA.extend(TCP_PLATFORM_SCHEMA) def setup_platform( diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index 99d8991a02e..68f4520a7e3 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -11,7 +11,7 @@ import voluptuous as vol import xmltodict from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -37,7 +37,7 @@ DEFAULT_NAME = "ted" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=80): cv.port, diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index a2cba41b028..2c304f259da 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -10,7 +10,7 @@ import tellcore.constants as tellcore_constants import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -40,7 +40,7 @@ CONF_TEMPERATURE_SCALE = "temperature_scale" DEFAULT_DATATYPE_MASK = 127 DEFAULT_TEMPERATURE_SCALE = UnitOfTemperature.CELSIUS -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_TEMPERATURE_SCALE, default=DEFAULT_TEMPERATURE_SCALE diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index 7138f40a653..92b7fe3de43 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -8,7 +8,7 @@ from temperusb.temper import TemperHandler import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) CONF_SCALE = "scale" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): vol.Coerce(str), vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 57621ba1055..7dc845ecf60 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -14,7 +14,7 @@ import thermoworks_smoke import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -57,7 +57,7 @@ SENSOR_TYPES = { # exclude these keys from thermoworks data EXCLUDE_KEYS = [FIRMWARE] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_EMAIL): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/thinkingcleaner/sensor.py b/homeassistant/components/thinkingcleaner/sensor.py index 86c5a8813d8..4d28912e20d 100644 --- a/homeassistant/components/thinkingcleaner/sensor.py +++ b/homeassistant/components/thinkingcleaner/sensor.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) @@ -66,7 +66,7 @@ STATES = { "st_unknown": "Unknown state", } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.string}) def setup_platform( diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index ed999e5a0b2..442442f0e1d 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) TIME_STR_FORMAT = "%H:%M" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_DISPLAY_OPTIONS, default=["time"]): vol.All( cv.ensure_list, [vol.In(OPTION_TYPES)] diff --git a/homeassistant/components/tmb/sensor.py b/homeassistant/components/tmb/sensor.py index 4ec86434ea0..126c3128f91 100644 --- a/homeassistant/components/tmb/sensor.py +++ b/homeassistant/components/tmb/sensor.py @@ -9,7 +9,10 @@ from requests import HTTPError from tmb import IBus import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -37,7 +40,7 @@ LINE_STOP_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_APP_ID): cv.string, vol.Required(CONF_APP_KEY): cv.string, diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index 8572a5a0bba..543046fac1c 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -8,7 +8,10 @@ from aiohttp import web import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_EMAIL, CONF_NAME, DEGREE from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -31,7 +34,7 @@ NAME_KEY = re.compile(SENSOR_NAME_KEY) UNIT_KEY = re.compile(SENSOR_UNIT_KEY) VALUE_KEY = re.compile(SENSOR_VALUE_KEY) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_EMAIL): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 4ec4301dc7b..787f3298e59 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -8,7 +8,7 @@ from TransportNSW import TransportNSW import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -44,7 +44,7 @@ ICONS = { SCAN_INTERVAL = timedelta(seconds=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STOP_ID): cv.string, vol.Required(CONF_API_KEY): cv.string, diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index 0a3118b3cca..fe4a6541d9e 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components import persistent_notification from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) @@ -74,7 +74,7 @@ SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] NOTIFICATION_ID = "travisci" NOTIFICATION_TITLE = "Travis CI Sensor Setup" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index 134dd675163..8e874be0bca 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -10,7 +10,10 @@ import re import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_MODE, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -44,7 +47,7 @@ _QUERY_SCHEME = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_APP_ID): cv.string, vol.Required(CONF_API_APP_KEY): cv.string, diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index 611f571336c..48f659103e1 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -8,7 +8,10 @@ import logging import vasttrafik import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_DELAY, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -38,7 +41,7 @@ DEFAULT_DELAY = 0 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_KEY): cv.string, vol.Required(CONF_SECRET): cv.string, diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index 9c6c6bca422..1ea12ed6a41 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -10,7 +10,10 @@ import time import aiohttp import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -52,7 +55,7 @@ CANCELLED_STRING = "Cancelled" NOT_DEPARTED_STRING = "Not departed yet" NO_INFORMATION_STRING = "No information for this train now" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_TRAIN_ID): cv.string, vol.Required(CONF_STATION_ID): cv.string, diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index ce5691b1193..c4fa7b1088b 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -10,7 +10,7 @@ from volkszaehler.exceptions import VolkszaehlerApiConnectionError import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -73,7 +73,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_UUID): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, diff --git a/homeassistant/components/vultr/sensor.py b/homeassistant/components/vultr/sensor.py index 816a55736be..843aa416297 100644 --- a/homeassistant/components/vultr/sensor.py +++ b/homeassistant/components/vultr/sensor.py @@ -7,7 +7,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -45,7 +45,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ) SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SUBSCRIPTION): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index 0e88272a41c..87906bdc2ae 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -7,7 +7,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -65,7 +65,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { SENSOR_KEYS: list[str] = list(SENSOR_TYPES) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( cv.ensure_list, [vol.In(SENSOR_KEYS)] diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py index 9b2cb600ac1..d9b4aa90f07 100644 --- a/homeassistant/components/worldclock/sensor.py +++ b/homeassistant/components/worldclock/sensor.py @@ -6,7 +6,10 @@ from datetime import tzinfo import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_TIME_ZONE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -19,7 +22,7 @@ CONF_TIME_FORMAT = "time_format" DEFAULT_NAME = "Worldclock Sensor" DEFAULT_TIME_STR_FORMAT = "%H:%M" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_TIME_ZONE): cv.time_zone, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py index a4d663cc184..45f39894abb 100644 --- a/homeassistant/components/worldtidesinfo/sensor.py +++ b/homeassistant/components/worldtidesinfo/sensor.py @@ -9,7 +9,10 @@ import time import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -24,7 +27,7 @@ DEFAULT_NAME = "WorldTidesInfo" SCAN_INTERVAL = timedelta(seconds=3600) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_LATITUDE): cv.latitude, diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index 10f40bea685..50700b78f35 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -8,7 +8,10 @@ import logging import aiohttp import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_HOST, CONF_PIN, CONF_TIMEOUT, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -22,7 +25,7 @@ CONF_ALLOW_UNREACHABLE = "allow_unreachable" DEFAULT_TIMEOUT = 5 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PIN): vol.All(vol.Coerce(str), vol.Match(r"\d{4}")), diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 14e21f79282..3aae6746ea9 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -10,7 +10,10 @@ import re import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_ID, CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -39,7 +42,7 @@ RESOURCE = ( SCAN_INTERVAL = timedelta(minutes=3) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_TRAVEL_TIMES): [ diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index bcef8248aa3..30227e3261e 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -9,7 +9,7 @@ from aioymaps import CaptchaError, YandexMapsRequester import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, ) @@ -34,7 +34,7 @@ DEFAULT_NAME = "Yandex Transport" SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_STOP_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py index 4c6af57f780..2187deb22e8 100644 --- a/homeassistant/components/zabbix/sensor.py +++ b/homeassistant/components/zabbix/sensor.py @@ -9,7 +9,10 @@ from typing import Any from pyzabbix import ZabbixAPI import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -35,7 +38,7 @@ _ZABBIX_TRIGGER_SCHEMA = vol.Schema( # SCAN_INTERVAL = 30 # -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(_CONF_TRIGGERS): vol.Any(_ZABBIX_TRIGGER_SCHEMA, None)} ) diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 8bbda7de73a..12831c96932 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -9,7 +9,10 @@ import requests import voluptuous as vol import xmltodict -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -33,7 +36,7 @@ ATTR_LAST_UPDATED = "amount_last_updated" ATTR_VAL_HI = "valuation_range_high" ATTR_VAL_LOW = "valuation_range_low" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_ZPID): vol.All(cv.ensure_list, [cv.string]), diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index 700344f44da..75769d9fd98 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -9,7 +9,7 @@ from zoneminder.monitor import Monitor, TimePeriod from zoneminder.zm import ZoneMinder from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) @@ -53,7 +53,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_INCLUDE_ARCHIVED, default=DEFAULT_INCLUDE_ARCHIVED From d00fe1ce7f5542e40b598cb628ba5bbd2ad141e7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:17:17 +0200 Subject: [PATCH 0218/2411] Import DOMAIN constants for Plugwise and implement (#120530) --- tests/components/plugwise/test_climate.py | 64 +++++++++++++---------- tests/components/plugwise/test_switch.py | 46 +++++++++------- 2 files changed, 61 insertions(+), 49 deletions(-) diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index b3f42031ed8..c91e4d37ba6 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -6,7 +6,13 @@ from unittest.mock import MagicMock, patch from plugwise.exceptions import PlugwiseError import pytest -from homeassistant.components.climate import HVACMode +from homeassistant.components.climate import ( + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_TEMPERATURE, + HVACMode, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.util.dt import utcnow @@ -153,8 +159,8 @@ async def test_adam_climate_adjust_negative_testing( with pytest.raises(HomeAssistantError): await hass.services.async_call( - "climate", - "set_temperature", + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, {"entity_id": "climate.zone_lisa_wk", "temperature": 25}, blocking=True, ) @@ -165,8 +171,8 @@ async def test_adam_climate_entity_climate_changes( ) -> None: """Test handling of user requests in adam climate device environment.""" await hass.services.async_call( - "climate", - "set_temperature", + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, {"entity_id": "climate.zone_lisa_wk", "temperature": 25}, blocking=True, ) @@ -176,8 +182,8 @@ async def test_adam_climate_entity_climate_changes( ) await hass.services.async_call( - "climate", - "set_temperature", + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, { "entity_id": "climate.zone_lisa_wk", "hvac_mode": "heat", @@ -192,15 +198,15 @@ async def test_adam_climate_entity_climate_changes( with pytest.raises(ValueError): await hass.services.async_call( - "climate", - "set_temperature", + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, {"entity_id": "climate.zone_lisa_wk", "temperature": 150}, blocking=True, ) await hass.services.async_call( - "climate", - "set_preset_mode", + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, {"entity_id": "climate.zone_lisa_wk", "preset_mode": "away"}, blocking=True, ) @@ -210,8 +216,8 @@ async def test_adam_climate_entity_climate_changes( ) await hass.services.async_call( - "climate", - "set_hvac_mode", + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, {"entity_id": "climate.zone_lisa_wk", "hvac_mode": "heat"}, blocking=True, ) @@ -222,8 +228,8 @@ async def test_adam_climate_entity_climate_changes( with pytest.raises(HomeAssistantError): await hass.services.async_call( - "climate", - "set_hvac_mode", + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, { "entity_id": "climate.zone_thermostat_jessie", "hvac_mode": "dry", @@ -242,8 +248,8 @@ async def test_adam_climate_off_mode_change( assert state assert state.state == HVACMode.OFF await hass.services.async_call( - "climate", - "set_hvac_mode", + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, { "entity_id": "climate.slaapkamer", "hvac_mode": "heat", @@ -258,8 +264,8 @@ async def test_adam_climate_off_mode_change( assert state assert state.state == HVACMode.HEAT await hass.services.async_call( - "climate", - "set_hvac_mode", + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, { "entity_id": "climate.kinderkamer", "hvac_mode": "off", @@ -274,8 +280,8 @@ async def test_adam_climate_off_mode_change( assert state assert state.state == HVACMode.HEAT await hass.services.async_call( - "climate", - "set_hvac_mode", + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, { "entity_id": "climate.logeerkamer", "hvac_mode": "heat", @@ -353,8 +359,8 @@ async def test_anna_climate_entity_climate_changes( ) -> None: """Test handling of user requests in anna climate device environment.""" await hass.services.async_call( - "climate", - "set_temperature", + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, {"entity_id": "climate.anna", "target_temp_high": 30, "target_temp_low": 20}, blocking=True, ) @@ -365,8 +371,8 @@ async def test_anna_climate_entity_climate_changes( ) await hass.services.async_call( - "climate", - "set_preset_mode", + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, {"entity_id": "climate.anna", "preset_mode": "away"}, blocking=True, ) @@ -376,8 +382,8 @@ async def test_anna_climate_entity_climate_changes( ) await hass.services.async_call( - "climate", - "set_hvac_mode", + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, {"entity_id": "climate.anna", "hvac_mode": "auto"}, blocking=True, ) @@ -385,8 +391,8 @@ async def test_anna_climate_entity_climate_changes( assert mock_smile_anna.set_schedule_state.call_count == 0 await hass.services.async_call( - "climate", - "set_hvac_mode", + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, {"entity_id": "climate.anna", "hvac_mode": "heat_cool"}, blocking=True, ) diff --git a/tests/components/plugwise/test_switch.py b/tests/components/plugwise/test_switch.py index 6b2393476ae..5da76bb0ebd 100644 --- a/tests/components/plugwise/test_switch.py +++ b/tests/components/plugwise/test_switch.py @@ -7,6 +7,12 @@ import pytest from homeassistant.components.plugwise.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -20,11 +26,11 @@ async def test_adam_climate_switch_entities( """Test creation of climate related switch entities.""" state = hass.states.get("switch.cv_pomp_relay") assert state - assert state.state == "on" + assert state.state == STATE_ON state = hass.states.get("switch.fibaro_hc2_relay") assert state - assert state.state == "on" + assert state.state == STATE_ON async def test_adam_climate_switch_negative_testing( @@ -35,8 +41,8 @@ async def test_adam_climate_switch_negative_testing( with pytest.raises(HomeAssistantError): await hass.services.async_call( - "switch", - "turn_off", + SWITCH_DOMAIN, + SERVICE_TURN_OFF, {"entity_id": "switch.cv_pomp_relay"}, blocking=True, ) @@ -48,8 +54,8 @@ async def test_adam_climate_switch_negative_testing( with pytest.raises(HomeAssistantError): await hass.services.async_call( - "switch", - "turn_on", + SWITCH_DOMAIN, + SERVICE_TURN_ON, {"entity_id": "switch.fibaro_hc2_relay"}, blocking=True, ) @@ -65,8 +71,8 @@ async def test_adam_climate_switch_changes( ) -> None: """Test changing of climate related switch entities.""" await hass.services.async_call( - "switch", - "turn_off", + SWITCH_DOMAIN, + SERVICE_TURN_OFF, {"entity_id": "switch.cv_pomp_relay"}, blocking=True, ) @@ -77,8 +83,8 @@ async def test_adam_climate_switch_changes( ) await hass.services.async_call( - "switch", - "toggle", + SWITCH_DOMAIN, + SERVICE_TOGGLE, {"entity_id": "switch.fibaro_hc2_relay"}, blocking=True, ) @@ -89,8 +95,8 @@ async def test_adam_climate_switch_changes( ) await hass.services.async_call( - "switch", - "turn_on", + SWITCH_DOMAIN, + SERVICE_TURN_ON, {"entity_id": "switch.fibaro_hc2_relay"}, blocking=True, ) @@ -107,11 +113,11 @@ async def test_stretch_switch_entities( """Test creation of climate related switch entities.""" state = hass.states.get("switch.koelkast_92c4a_relay") assert state - assert state.state == "on" + assert state.state == STATE_ON state = hass.states.get("switch.droger_52559_relay") assert state - assert state.state == "on" + assert state.state == STATE_ON async def test_stretch_switch_changes( @@ -119,8 +125,8 @@ async def test_stretch_switch_changes( ) -> None: """Test changing of power related switch entities.""" await hass.services.async_call( - "switch", - "turn_off", + SWITCH_DOMAIN, + SERVICE_TURN_OFF, {"entity_id": "switch.koelkast_92c4a_relay"}, blocking=True, ) @@ -130,8 +136,8 @@ async def test_stretch_switch_changes( ) await hass.services.async_call( - "switch", - "toggle", + SWITCH_DOMAIN, + SERVICE_TOGGLE, {"entity_id": "switch.droger_52559_relay"}, blocking=True, ) @@ -141,8 +147,8 @@ async def test_stretch_switch_changes( ) await hass.services.async_call( - "switch", - "turn_on", + SWITCH_DOMAIN, + SERVICE_TURN_ON, {"entity_id": "switch.droger_52559_relay"}, blocking=True, ) From fac8349c37a44f9888c00d3adb8c6d3c1712456a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 12:18:33 +0200 Subject: [PATCH 0219/2411] Add learning offset select to Airgradient (#120532) --- .../components/airgradient/select.py | 68 +++- .../components/airgradient/strings.json | 34 +- .../airgradient/snapshots/test_select.ambr | 366 ++++++++++++++++++ .../airgradient/snapshots/test_sensor.ambr | 48 +-- tests/components/airgradient/test_select.py | 2 +- 5 files changed, 489 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index 1cb902a2d3c..c37df0483d1 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -79,6 +79,65 @@ LED_BAR_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = ( ), ) +LEARNING_TIME_OFFSET_OPTIONS = { + 12: "12", + 60: "60", + 120: "120", + 360: "360", + 720: "720", +} +LEARNING_TIME_OFFSET_OPTIONS_INVERSE = { + v: k for k, v in LEARNING_TIME_OFFSET_OPTIONS.items() +} +ABC_DAYS = { + 8: "8", + 30: "30", + 90: "90", + 180: "180", + 0: "off", +} +ABC_DAYS_INVERSE = {v: k for k, v in ABC_DAYS.items()} + +CONTROL_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = ( + AirGradientSelectEntityDescription( + key="nox_index_learning_time_offset", + translation_key="nox_index_learning_time_offset", + options=list(LEARNING_TIME_OFFSET_OPTIONS_INVERSE), + entity_category=EntityCategory.CONFIG, + value_fn=lambda config: LEARNING_TIME_OFFSET_OPTIONS.get( + config.nox_learning_offset + ), + set_value_fn=lambda client, value: client.set_nox_learning_offset( + LEARNING_TIME_OFFSET_OPTIONS_INVERSE.get(value, 12) + ), + ), + AirGradientSelectEntityDescription( + key="voc_index_learning_time_offset", + translation_key="voc_index_learning_time_offset", + options=list(LEARNING_TIME_OFFSET_OPTIONS_INVERSE), + entity_category=EntityCategory.CONFIG, + value_fn=lambda config: LEARNING_TIME_OFFSET_OPTIONS.get( + config.nox_learning_offset + ), + set_value_fn=lambda client, value: client.set_tvoc_learning_offset( + LEARNING_TIME_OFFSET_OPTIONS_INVERSE.get(value, 12) + ), + ), + AirGradientSelectEntityDescription( + key="co2_automatic_baseline_calibration", + translation_key="co2_automatic_baseline_calibration", + options=list(ABC_DAYS_INVERSE), + entity_category=EntityCategory.CONFIG, + value_fn=lambda config: ABC_DAYS.get( + config.co2_automatic_baseline_calibration_days + ), + set_value_fn=lambda client, + value: client.set_co2_automatic_baseline_calibration( + ABC_DAYS_INVERSE.get(value, 0) + ), + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -104,7 +163,10 @@ async def async_setup_entry( coordinator.data.configuration_control is ConfigurationControl.LOCAL and not added_entities ): - entities: list[AirGradientSelect] = [] + entities: list[AirGradientSelect] = [ + AirGradientSelect(coordinator, description) + for description in CONTROL_ENTITIES + ] if "I" in model: entities.extend( AirGradientSelect(coordinator, description) @@ -123,7 +185,9 @@ async def async_setup_entry( and added_entities ): entity_registry = er.async_get(hass) - for entity_description in DISPLAY_SELECT_TYPES + LED_BAR_ENTITIES: + for entity_description in ( + DISPLAY_SELECT_TYPES + LED_BAR_ENTITIES + CONTROL_ENTITIES + ): unique_id = f"{coordinator.serial_number}-{entity_description.key}" if entity_id := entity_registry.async_get_entity_id( SELECT_DOMAIN, DOMAIN, unique_id diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 4e8973bdde2..eb529a99ae3 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -69,6 +69,36 @@ "co2": "Carbon dioxide", "pm": "Particulate matter" } + }, + "nox_index_learning_time_offset": { + "name": "NOx index learning offset", + "state": { + "12": "12 hours", + "60": "60 hours", + "120": "120 hours", + "360": "360 hours", + "720": "720 hours" + } + }, + "voc_index_learning_time_offset": { + "name": "VOC index learning offset", + "state": { + "12": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::12%]", + "60": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::60%]", + "120": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::120%]", + "360": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::360%]", + "720": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::720%]" + } + }, + "co2_automatic_baseline_calibration": { + "name": "CO2 automatic baseline calibration", + "state": { + "8": "8 days", + "30": "30 days", + "90": "90 days", + "180": "180 days", + "0": "[%key:common::state::off%]" + } } }, "sensor": { @@ -98,10 +128,10 @@ "name": "Carbon dioxide automatic baseline calibration" }, "nox_learning_offset": { - "name": "NOx learning offset" + "name": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::name%]" }, "tvoc_learning_offset": { - "name": "VOC learning offset" + "name": "[%key:component::airgradient::entity::select::voc_index_learning_time_offset::name%]" }, "led_bar_mode": { "name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]", diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index 409eae52225..19cdc2134fc 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -1,4 +1,65 @@ # serializer version: 1 +# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_calibration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '8', + '30', + '90', + '180', + 'off', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CO2 automatic baseline calibration', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co2_automatic_baseline_calibration', + 'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_calibration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient CO2 automatic baseline calibration', + 'options': list([ + '8', + '30', + '90', + '180', + 'off', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- # name: test_all_entities[indoor][select.airgradient_configuration_source-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -221,6 +282,189 @@ 'state': 'co2', }) # --- +# name: test_all_entities[indoor][select.airgradient_nox_index_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_nox_index_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'NOx index learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nox_index_learning_time_offset', + 'unique_id': '84fce612f5b8-nox_index_learning_time_offset', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][select.airgradient_nox_index_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient NOx index learning offset', + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_nox_index_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_all_entities[indoor][select.airgradient_voc_index_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_voc_index_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'VOC index learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voc_index_learning_time_offset', + 'unique_id': '84fce612f5b8-voc_index_learning_time_offset', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[indoor][select.airgradient_voc_index_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient VOC index learning offset', + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_voc_index_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_calibration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '8', + '30', + '90', + '180', + 'off', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CO2 automatic baseline calibration', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co2_automatic_baseline_calibration', + 'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_calibration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient CO2 automatic baseline calibration', + 'options': list([ + '8', + '30', + '90', + '180', + 'off', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- # name: test_all_entities[outdoor][select.airgradient_configuration_source-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -276,3 +520,125 @@ 'state': 'local', }) # --- +# name: test_all_entities[outdoor][select.airgradient_nox_index_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_nox_index_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'NOx index learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nox_index_learning_time_offset', + 'unique_id': '84fce612f5b8-nox_index_learning_time_offset', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_nox_index_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient NOx index learning offset', + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_nox_index_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_voc_index_learning_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.airgradient_voc_index_learning_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'VOC index learning offset', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voc_index_learning_time_offset', + 'unique_id': '84fce612f5b8-voc_index_learning_time_offset', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[outdoor][select.airgradient_voc_index_learning_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient VOC index learning offset', + 'options': list([ + '12', + '60', + '120', + '360', + '720', + ]), + }), + 'context': , + 'entity_id': 'select.airgradient_voc_index_learning_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index c3d14c7d8fc..ff83fdcc111 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -462,7 +462,7 @@ 'state': '1', }) # --- -# name: test_all_entities[indoor][sensor.airgradient_nox_learning_offset-entry] +# name: test_all_entities[indoor][sensor.airgradient_nox_index_learning_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -474,7 +474,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'entity_id': 'sensor.airgradient_nox_index_learning_offset', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -486,7 +486,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'NOx learning offset', + 'original_name': 'NOx index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -495,15 +495,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[indoor][sensor.airgradient_nox_learning_offset-state] +# name: test_all_entities[indoor][sensor.airgradient_nox_index_learning_offset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', - 'friendly_name': 'Airgradient NOx learning offset', + 'friendly_name': 'Airgradient NOx index learning offset', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'entity_id': 'sensor.airgradient_nox_index_learning_offset', 'last_changed': , 'last_reported': , 'last_updated': , @@ -964,7 +964,7 @@ 'state': '99', }) # --- -# name: test_all_entities[indoor][sensor.airgradient_voc_learning_offset-entry] +# name: test_all_entities[indoor][sensor.airgradient_voc_index_learning_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -976,7 +976,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'entity_id': 'sensor.airgradient_voc_index_learning_offset', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -988,7 +988,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'VOC learning offset', + 'original_name': 'VOC index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -997,15 +997,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[indoor][sensor.airgradient_voc_learning_offset-state] +# name: test_all_entities[indoor][sensor.airgradient_voc_index_learning_offset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', - 'friendly_name': 'Airgradient VOC learning offset', + 'friendly_name': 'Airgradient VOC index learning offset', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'entity_id': 'sensor.airgradient_voc_index_learning_offset', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1109,7 +1109,7 @@ 'state': '1', }) # --- -# name: test_all_entities[outdoor][sensor.airgradient_nox_learning_offset-entry] +# name: test_all_entities[outdoor][sensor.airgradient_nox_index_learning_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1121,7 +1121,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'entity_id': 'sensor.airgradient_nox_index_learning_offset', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1133,7 +1133,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'NOx learning offset', + 'original_name': 'NOx index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -1142,15 +1142,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[outdoor][sensor.airgradient_nox_learning_offset-state] +# name: test_all_entities[outdoor][sensor.airgradient_nox_index_learning_offset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', - 'friendly_name': 'Airgradient NOx learning offset', + 'friendly_name': 'Airgradient NOx index learning offset', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.airgradient_nox_learning_offset', + 'entity_id': 'sensor.airgradient_nox_index_learning_offset', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1357,7 +1357,7 @@ 'state': '49', }) # --- -# name: test_all_entities[outdoor][sensor.airgradient_voc_learning_offset-entry] +# name: test_all_entities[outdoor][sensor.airgradient_voc_index_learning_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1369,7 +1369,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'entity_id': 'sensor.airgradient_voc_index_learning_offset', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1381,7 +1381,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'VOC learning offset', + 'original_name': 'VOC index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -1390,15 +1390,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[outdoor][sensor.airgradient_voc_learning_offset-state] +# name: test_all_entities[outdoor][sensor.airgradient_voc_index_learning_offset-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', - 'friendly_name': 'Airgradient VOC learning offset', + 'friendly_name': 'Airgradient VOC index learning offset', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.airgradient_voc_learning_offset', + 'entity_id': 'sensor.airgradient_voc_index_learning_offset', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/airgradient/test_select.py b/tests/components/airgradient/test_select.py index b4294112062..61679a15c07 100644 --- a/tests/components/airgradient/test_select.py +++ b/tests/components/airgradient/test_select.py @@ -83,7 +83,7 @@ async def test_cloud_creates_no_number( async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 7 mock_cloud_airgradient_client.get_config.return_value = Config.from_json( load_fixture("get_config_cloud.json", DOMAIN) From 36d8ffa79ab602c937f8f68621a6e96cfbe0ef3c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:19:04 +0200 Subject: [PATCH 0220/2411] Force alias when importing media player PLATFORM_SCHEMA (#120537) --- homeassistant/components/aquostv/media_player.py | 4 ++-- homeassistant/components/bluesound/media_player.py | 4 ++-- homeassistant/components/channels/media_player.py | 4 ++-- homeassistant/components/clementine/media_player.py | 4 ++-- homeassistant/components/cmus/media_player.py | 4 ++-- homeassistant/components/denon/media_player.py | 4 ++-- homeassistant/components/emby/media_player.py | 4 ++-- homeassistant/components/group/media_player.py | 4 ++-- homeassistant/components/gstreamer/media_player.py | 4 ++-- homeassistant/components/harman_kardon_avr/media_player.py | 4 ++-- homeassistant/components/horizon/media_player.py | 4 ++-- homeassistant/components/itunes/media_player.py | 4 ++-- homeassistant/components/kef/media_player.py | 4 ++-- homeassistant/components/kodi/media_player.py | 4 ++-- homeassistant/components/lg_netcast/media_player.py | 4 ++-- homeassistant/components/mediaroom/media_player.py | 4 ++-- homeassistant/components/mpd/media_player.py | 4 ++-- homeassistant/components/nad/media_player.py | 4 ++-- homeassistant/components/onkyo/media_player.py | 4 ++-- homeassistant/components/panasonic_bluray/media_player.py | 4 ++-- homeassistant/components/pioneer/media_player.py | 4 ++-- homeassistant/components/pjlink/media_player.py | 4 ++-- homeassistant/components/russound_rio/media_player.py | 4 ++-- homeassistant/components/russound_rnet/media_player.py | 4 ++-- homeassistant/components/ue_smart_radio/media_player.py | 4 ++-- homeassistant/components/universal/media_player.py | 4 ++-- homeassistant/components/vlc/media_player.py | 4 ++-- homeassistant/components/xiaomi_tv/media_player.py | 4 ++-- homeassistant/components/yamaha/media_player.py | 4 ++-- homeassistant/components/ziggo_mediabox_xl/media_player.py | 4 ++-- 30 files changed, 60 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index 64631ed1948..343cb6492da 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -10,7 +10,7 @@ import sharp_aquos_rc import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -37,7 +37,7 @@ DEFAULT_PASSWORD = "password" DEFAULT_TIMEOUT = 0.5 DEFAULT_RETRIES = 2 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 73ce963d481..0e752ac1f72 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -18,7 +18,7 @@ import xmltodict from homeassistant.components import media_source from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -70,7 +70,7 @@ UPDATE_CAPTURE_INTERVAL = timedelta(minutes=30) UPDATE_PRESETS_INTERVAL = timedelta(minutes=30) UPDATE_SERVICES_INTERVAL = timedelta(minutes=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOSTS): vol.All( cv.ensure_list, diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 2b8fc4a2b3e..07ed8ce7d66 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -8,7 +8,7 @@ from pychannels import Channels import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -26,7 +26,7 @@ DATA_CHANNELS = "channels" DEFAULT_NAME = "Channels" DEFAULT_PORT = 57000 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/clementine/media_player.py b/homeassistant/components/clementine/media_player.py index 84052aa64b9..233ffc840c0 100644 --- a/homeassistant/components/clementine/media_player.py +++ b/homeassistant/components/clementine/media_player.py @@ -9,7 +9,7 @@ from clementineremote import ClementineRemote import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -26,7 +26,7 @@ DEFAULT_PORT = 5500 SCAN_INTERVAL = timedelta(seconds=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_ACCESS_TOKEN): cv.positive_int, diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index ca9ad8f8489..d55e9ca8f0b 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -9,7 +9,7 @@ from pycmus import exceptions, remote import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "cmus" DEFAULT_PORT = 3000 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Inclusive(CONF_HOST, "remote"): cv.string, vol.Inclusive(CONF_PASSWORD, "remote"): cv.string, diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index 1d49323f0cc..b3b3ba97baa 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -8,7 +8,7 @@ import telnetlib # pylint: disable=deprecated-module import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -38,7 +38,7 @@ SUPPORT_MEDIA_MODES = ( | MediaPlayerEntityFeature.PLAY ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 22d7939a14e..21ee6449c11 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -8,7 +8,7 @@ from pyemby import EmbyServer import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -47,7 +47,7 @@ SUPPORT_EMBY = ( | MediaPlayerEntityFeature.PLAY ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index 6c49f88a12f..4b71cf7f81d 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -16,7 +16,7 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, MediaPlayerEntity, @@ -71,7 +71,7 @@ KEY_VOLUME = "volume" DEFAULT_NAME = "Media Group" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index 054b31c2fbe..fd9de62c016 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -30,7 +30,7 @@ CONF_PIPELINE = "pipeline" DOMAIN = "gstreamer" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string} ) diff --git a/homeassistant/components/harman_kardon_avr/media_player.py b/homeassistant/components/harman_kardon_avr/media_player.py index 815a8f52b42..b8d9f27bcf1 100644 --- a/homeassistant/components/harman_kardon_avr/media_player.py +++ b/homeassistant/components/harman_kardon_avr/media_player.py @@ -6,7 +6,7 @@ import hkavr import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType DEFAULT_NAME = "Harman Kardon AVR" DEFAULT_PORT = 10025 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index c03bcc73f41..9531f9c0ed7 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -34,7 +34,7 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index 13ad66f1417..c32ca287793 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -8,7 +8,7 @@ import requests import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -27,7 +27,7 @@ DEFAULT_TIMEOUT = 10 DOMAIN = "itunes" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 04ecd633d70..ad335499ba4 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -13,7 +13,7 @@ from getmac import get_mac_address import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -59,7 +59,7 @@ SERVICE_UPDATE_DSP = "update_dsp" DSP_SCAN_INTERVAL = timedelta(seconds=3600) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TYPE): vol.In(["LS50", "LSX"]), diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 3ba5804f8b3..290b3b1e566 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -15,7 +15,7 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, BrowseError, BrowseMedia, MediaPlayerEntity, @@ -118,7 +118,7 @@ MAP_KODI_MEDIA_TYPES: dict[MediaType | str, str] = { } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string, diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 3fc07cab12b..4dc694cd085 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -10,7 +10,7 @@ from requests import RequestException import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -49,7 +49,7 @@ SUPPORT_LGTV = ( | MediaPlayerEntityFeature.STOP ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index 22417adcf51..8e60609fbac 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -15,7 +15,7 @@ from pymediaroom import ( import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -46,7 +46,7 @@ MEDIA_TYPE_MEDIAROOM = "mediaroom" SIGNAL_STB_NOTIFY = "mediaroom_stb_discovered" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 204bbc7f499..0c4a2224e63 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -17,7 +17,7 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -61,7 +61,7 @@ SUPPORT_MPD = ( | MediaPlayerEntityFeature.BROWSE_MEDIA ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index fa9ce4dd08e..e3c22b42d28 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -6,7 +6,7 @@ from nad_receiver import NADReceiver, NADReceiverTCP, NADReceiverTelnet import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -43,7 +43,7 @@ CONF_SOURCE_DICT = "sources" # for NADReceiver # Max value based on a C658 with an MDC HDM-2 card installed SOURCE_DICT_SCHEMA = vol.Schema({vol.Range(min=1, max=12): cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): vol.In( ["RS232", "Telnet", "TCP"] diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 181a8117443..63e76e28dbb 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.media_player import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -70,7 +70,7 @@ DEFAULT_SOURCES = { } DEFAULT_PLAYABLE_SOURCES = ("fm", "am", "tuner") -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/panasonic_bluray/media_player.py b/homeassistant/components/panasonic_bluray/media_player.py index a121da93486..a7cb0780ca9 100644 --- a/homeassistant/components/panasonic_bluray/media_player.py +++ b/homeassistant/components/panasonic_bluray/media_player.py @@ -8,7 +8,7 @@ from panacotta import PanasonicBD import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -25,7 +25,7 @@ DEFAULT_NAME = "Panasonic Blu-Ray" SCAN_INTERVAL = timedelta(seconds=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 15cd3cbf303..670ccffaea7 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -9,7 +9,7 @@ from typing import Final import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -33,7 +33,7 @@ DEFAULT_SOURCES: dict[str, str] = {} MAX_VOLUME = 185 MAX_SOURCE_NUMBERS = 60 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index ff3be3266a0..93f8ea5ad9b 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -7,7 +7,7 @@ from pypjlink.projector import ProjectorError import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -22,7 +22,7 @@ from .const import CONF_ENCODING, DEFAULT_ENCODING, DEFAULT_PORT, DOMAIN ERR_PROJECTOR_UNAVAILABLE = "projector unavailable" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 74339153f69..faea8b7193e 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -6,7 +6,7 @@ from russound_rio import Russound import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -23,7 +23,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_NAME): cv.string, diff --git a/homeassistant/components/russound_rnet/media_player.py b/homeassistant/components/russound_rnet/media_player.py index 3b061d5a503..a08cfbe7747 100644 --- a/homeassistant/components/russound_rnet/media_player.py +++ b/homeassistant/components/russound_rnet/media_player.py @@ -9,7 +9,7 @@ from russound import russound import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -30,7 +30,7 @@ ZONE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) SOURCE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_NAME): cv.string, diff --git a/homeassistant/components/ue_smart_radio/media_player.py b/homeassistant/components/ue_smart_radio/media_player.py index 90afca69816..62675c62c0e 100644 --- a/homeassistant/components/ue_smart_radio/media_player.py +++ b/homeassistant/components/ue_smart_radio/media_player.py @@ -8,7 +8,7 @@ import requests import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -31,7 +31,7 @@ PLAYBACK_DICT = { "stop": MediaPlayerState.IDLE, } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} ) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index e4acc6b8657..c5bd9fb50c4 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -36,7 +36,7 @@ from homeassistant.components.media_player import ( ATTR_SOUND_MODE_LIST, DEVICE_CLASSES_SCHEMA, DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOUND_MODE, @@ -119,7 +119,7 @@ STATES_ORDER_IDLE = STATES_ORDER_LOOKUP[MediaPlayerState.IDLE] ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string) CMD_SCHEMA = cv.schema_with_slug_keys(cv.SERVICE_SCHEMA) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_CHILDREN, default=[]): cv.entity_ids, diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index 53831fb8db0..cd05c919d58 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) CONF_ARGUMENTS = "arguments" DEFAULT_NAME = "Vlc" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_ARGUMENTS, default=""): cv.string, vol.Optional(CONF_NAME): cv.string, diff --git a/homeassistant/components/xiaomi_tv/media_player.py b/homeassistant/components/xiaomi_tv/media_player.py index da692d21bfc..675c802f79c 100644 --- a/homeassistant/components/xiaomi_tv/media_player.py +++ b/homeassistant/components/xiaomi_tv/media_player.py @@ -8,7 +8,7 @@ import pymitv import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -24,7 +24,7 @@ DEFAULT_NAME = "Xiaomi TV" _LOGGER = logging.getLogger(__name__) # No host is needed for configuration, however it can be set. -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index c648994c38d..1be7cb03e17 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -10,7 +10,7 @@ import rxv import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -68,7 +68,7 @@ SUPPORT_YAMAHA = ( | MediaPlayerEntityFeature.SELECT_SOUND_MODE ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_HOST): cv.string, diff --git a/homeassistant/components/ziggo_mediabox_xl/media_player.py b/homeassistant/components/ziggo_mediabox_xl/media_player.py index 7c97d38cff3..a81a206b5b2 100644 --- a/homeassistant/components/ziggo_mediabox_xl/media_player.py +++ b/homeassistant/components/ziggo_mediabox_xl/media_player.py @@ -9,7 +9,7 @@ import voluptuous as vol from ziggo_mediabox_xl import ZiggoMediaboxXL from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) DATA_KNOWN_DEVICES = "ziggo_mediabox_xl_known_devices" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( {vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string} ) From f55ddfecf463e1452d6dd6f27231bcba504061a6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 12:25:02 +0200 Subject: [PATCH 0221/2411] Correct type annotations in integration sensor tests (#120541) --- tests/components/integration/test_sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 03df38893a2..10f921ce603 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -321,7 +321,7 @@ async def test_restore_state_failed(hass: HomeAssistant, extra_attributes) -> No ) async def test_trapezoidal( hass: HomeAssistant, - sequence: tuple[tuple[float, float, float, ...]], + sequence: tuple[tuple[float, float, float], ...], force_update: bool, ) -> None: """Test integration sensor state.""" @@ -385,7 +385,7 @@ async def test_trapezoidal( ) async def test_left( hass: HomeAssistant, - sequence: tuple[tuple[float, float, float, ...]], + sequence: tuple[tuple[float, float, float], ...], force_update: bool, ) -> None: """Test integration sensor state with left reimann method.""" @@ -452,7 +452,7 @@ async def test_left( ) async def test_right( hass: HomeAssistant, - sequence: tuple[tuple[float, float, float, ...]], + sequence: tuple[tuple[float, float, float], ...], force_update: bool, ) -> None: """Test integration sensor state with left reimann method.""" From 9bbeb5d608ebbdb23e26e3f604ecec231dc80dd6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 12:26:24 +0200 Subject: [PATCH 0222/2411] Add primary_config_entry attribute to device registry entries (#119959) Co-authored-by: Franck Nijhof Co-authored-by: Robert Resch --- homeassistant/components/logbook/helpers.py | 2 +- homeassistant/components/unifiprotect/data.py | 2 +- homeassistant/helpers/device_registry.py | 93 +++-- .../airgradient/snapshots/test_init.ambr | 2 + .../aosmith/snapshots/test_device.ambr | 1 + .../components/config/test_device_registry.py | 13 +- .../snapshots/test_init.ambr | 1 + .../ecovacs/snapshots/test_init.ambr | 1 + .../elgato/snapshots/test_button.ambr | 2 + .../elgato/snapshots/test_light.ambr | 3 + .../elgato/snapshots/test_sensor.ambr | 5 + .../elgato/snapshots/test_switch.ambr | 2 + .../energyzero/snapshots/test_sensor.ambr | 6 + .../snapshots/test_diagnostics.ambr | 6 + .../snapshots/test_init.ambr | 1 + .../snapshots/test_init.ambr | 98 ++++++ .../homekit_controller/test_connection.py | 2 +- .../homewizard/snapshots/test_button.ambr | 1 + .../homewizard/snapshots/test_number.ambr | 2 + .../homewizard/snapshots/test_sensor.ambr | 218 ++++++++++++ .../homewizard/snapshots/test_switch.ambr | 11 + .../snapshots/test_init.ambr | 1 + tests/components/hyperion/test_camera.py | 2 +- tests/components/hyperion/test_light.py | 2 +- tests/components/hyperion/test_sensor.py | 2 +- tests/components/hyperion/test_switch.py | 2 +- .../ista_ecotrend/snapshots/test_init.ambr | 2 + .../kitchen_sink/snapshots/test_switch.ambr | 4 + .../lamarzocco/snapshots/test_switch.ambr | 1 + tests/components/lifx/test_migration.py | 6 +- .../components/lutron_caseta/test_logbook.py | 2 +- .../mealie/snapshots/test_init.ambr | 1 + tests/components/motioneye/test_camera.py | 2 +- tests/components/mqtt/test_discovery.py | 16 +- tests/components/mqtt/test_tag.py | 4 +- .../netatmo/snapshots/test_init.ambr | 38 ++ .../netgear_lte/snapshots/test_init.ambr | 1 + .../ondilo_ico/snapshots/test_init.ambr | 2 + .../onewire/snapshots/test_binary_sensor.ambr | 22 ++ .../onewire/snapshots/test_sensor.ambr | 22 ++ .../onewire/snapshots/test_switch.ambr | 22 ++ .../renault/snapshots/test_binary_sensor.ambr | 8 + .../renault/snapshots/test_button.ambr | 8 + .../snapshots/test_device_tracker.ambr | 8 + .../renault/snapshots/test_select.ambr | 8 + .../renault/snapshots/test_sensor.ambr | 8 + .../components/rova/snapshots/test_init.ambr | 1 + .../sfr_box/snapshots/test_binary_sensor.ambr | 2 + .../sfr_box/snapshots/test_button.ambr | 1 + .../sfr_box/snapshots/test_sensor.ambr | 1 + .../snapshots/test_binary_sensor.ambr | 2 + .../tailwind/snapshots/test_button.ambr | 1 + .../tailwind/snapshots/test_cover.ambr | 2 + .../tailwind/snapshots/test_number.ambr | 1 + tests/components/tasmota/test_discovery.py | 8 +- .../components/tedee/snapshots/test_init.ambr | 1 + .../components/tedee/snapshots/test_lock.ambr | 2 + .../teslemetry/snapshots/test_init.ambr | 4 + .../tplink/snapshots/test_binary_sensor.ambr | 1 + .../tplink/snapshots/test_button.ambr | 1 + .../tplink/snapshots/test_climate.ambr | 1 + .../components/tplink/snapshots/test_fan.ambr | 1 + .../tplink/snapshots/test_number.ambr | 1 + .../tplink/snapshots/test_select.ambr | 1 + .../tplink/snapshots/test_sensor.ambr | 1 + .../tplink/snapshots/test_switch.ambr | 1 + .../twentemilieu/snapshots/test_calendar.ambr | 1 + .../twentemilieu/snapshots/test_sensor.ambr | 5 + .../uptime/snapshots/test_sensor.ambr | 1 + .../components/vesync/snapshots/test_fan.ambr | 9 + .../vesync/snapshots/test_light.ambr | 9 + .../vesync/snapshots/test_sensor.ambr | 9 + .../vesync/snapshots/test_switch.ambr | 9 + .../whois/snapshots/test_sensor.ambr | 9 + .../wled/snapshots/test_button.ambr | 1 + .../wled/snapshots/test_number.ambr | 2 + .../wled/snapshots/test_select.ambr | 4 + .../wled/snapshots/test_switch.ambr | 4 + tests/helpers/test_device_registry.py | 332 ++++++++++++++++-- tests/helpers/test_entity_platform.py | 1 + tests/helpers/test_entity_registry.py | 8 +- tests/syrupy.py | 2 + 82 files changed, 1001 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index 674f1643793..4fa0da9033a 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -58,7 +58,7 @@ def _async_config_entries_for_ids( dev_reg = dr.async_get(hass) for device_id in device_ids: if (device := dev_reg.async_get(device_id)) and device.config_entries: - config_entry_ids.update(device.config_entries) + config_entry_ids |= device.config_entries return config_entry_ids diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 6b502eaa5f3..a2ef72c7008 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -342,7 +342,7 @@ class ProtectData: @callback def async_ufp_instance_for_config_entry_ids( - hass: HomeAssistant, config_entry_ids: list[str] + hass: HomeAssistant, config_entry_ids: set[str] ) -> ProtectApiClient | None: """Find the UFP instance for the config entry ids.""" return next( diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 36249733f71..cfafa63ec3a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -55,7 +55,7 @@ EVENT_DEVICE_REGISTRY_UPDATED: EventType[EventDeviceRegistryUpdatedData] = Event ) STORAGE_KEY = "core.device_registry" STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 5 +STORAGE_VERSION_MINOR = 6 CLEANUP_DELAY = 10 @@ -145,6 +145,9 @@ DEVICE_INFO_TYPES = { DEVICE_INFO_KEYS = set.union(*(itm for itm in DEVICE_INFO_TYPES.values())) +# Integrations which may share a device with a native integration +LOW_PRIO_CONFIG_ENTRY_DOMAINS = {"homekit_controller", "matter", "mqtt", "upnp"} + class _EventDeviceRegistryUpdatedData_CreateRemove(TypedDict): """EventDeviceRegistryUpdated data for action type 'create' and 'remove'.""" @@ -273,7 +276,7 @@ class DeviceEntry: """Device Registry Entry.""" area_id: str | None = attr.ib(default=None) - config_entries: list[str] = attr.ib(factory=list) + config_entries: set[str] = attr.ib(converter=set, factory=set) configuration_url: str | None = attr.ib(default=None) connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) disabled_by: DeviceEntryDisabler | None = attr.ib(default=None) @@ -286,6 +289,7 @@ class DeviceEntry: model: str | None = attr.ib(default=None) name_by_user: str | None = attr.ib(default=None) name: str | None = attr.ib(default=None) + primary_config_entry: str | None = attr.ib(default=None) serial_number: str | None = attr.ib(default=None) suggested_area: str | None = attr.ib(default=None) sw_version: str | None = attr.ib(default=None) @@ -307,7 +311,7 @@ class DeviceEntry: return { "area_id": self.area_id, "configuration_url": self.configuration_url, - "config_entries": self.config_entries, + "config_entries": list(self.config_entries), "connections": list(self.connections), "disabled_by": self.disabled_by, "entry_type": self.entry_type, @@ -319,6 +323,7 @@ class DeviceEntry: "model": self.model, "name_by_user": self.name_by_user, "name": self.name, + "primary_config_entry": self.primary_config_entry, "serial_number": self.serial_number, "sw_version": self.sw_version, "via_device_id": self.via_device_id, @@ -347,7 +352,7 @@ class DeviceEntry: json_bytes( { "area_id": self.area_id, - "config_entries": self.config_entries, + "config_entries": list(self.config_entries), "configuration_url": self.configuration_url, "connections": list(self.connections), "disabled_by": self.disabled_by, @@ -360,6 +365,7 @@ class DeviceEntry: "model": self.model, "name_by_user": self.name_by_user, "name": self.name, + "primary_config_entry": self.primary_config_entry, "serial_number": self.serial_number, "sw_version": self.sw_version, "via_device_id": self.via_device_id, @@ -372,7 +378,7 @@ class DeviceEntry: class DeletedDeviceEntry: """Deleted Device Registry Entry.""" - config_entries: list[str] = attr.ib() + config_entries: set[str] = attr.ib() connections: set[tuple[str, str]] = attr.ib() identifiers: set[tuple[str, str]] = attr.ib() id: str = attr.ib() @@ -387,7 +393,7 @@ class DeletedDeviceEntry: """Create DeviceEntry from DeletedDeviceEntry.""" return DeviceEntry( # type ignores: likely https://github.com/python/mypy/issues/8625 - config_entries=[config_entry_id], + config_entries={config_entry_id}, # type: ignore[arg-type] connections=self.connections & connections, # type: ignore[arg-type] identifiers=self.identifiers & identifiers, # type: ignore[arg-type] id=self.id, @@ -400,7 +406,7 @@ class DeletedDeviceEntry: return json_fragment( json_bytes( { - "config_entries": self.config_entries, + "config_entries": list(self.config_entries), "connections": list(self.connections), "identifiers": list(self.identifiers), "id": self.id, @@ -473,6 +479,10 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): # Introduced in 2024.3 for device in old_data["devices"]: device["labels"] = device.get("labels", []) + if old_minor_version < 6: + # Introduced in 2024.7 + for device in old_data["devices"]: + device.setdefault("primary_config_entry", None) if old_major_version > 1: raise NotImplementedError @@ -790,6 +800,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): device.id, allow_collisions=True, add_config_entry_id=config_entry_id, + add_config_entry=config_entry, configuration_url=configuration_url, device_info_type=device_info_type, disabled_by=disabled_by, @@ -816,6 +827,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): self, device_id: str, *, + add_config_entry: ConfigEntry | UndefinedType = UNDEFINED, add_config_entry_id: str | UndefinedType = UNDEFINED, # Temporary flag so we don't blow up when collisions are implicitly introduced # by calls to async_get_or_create. Must not be set by integrations. @@ -849,6 +861,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): config_entries = old.config_entries + if add_config_entry_id is not UNDEFINED and add_config_entry is UNDEFINED: + config_entry = self.hass.config_entries.async_get_entry(add_config_entry_id) + if config_entry is None: + raise HomeAssistantError( + f"Can't link device to unknown config entry {add_config_entry_id}" + ) + add_config_entry = config_entry + if merge_connections is not UNDEFINED and new_connections is not UNDEFINED: raise HomeAssistantError( "Cannot define both merge_connections and new_connections" @@ -886,32 +906,40 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): area = ar.async_get(self.hass).async_get_or_create(suggested_area) area_id = area.id - if add_config_entry_id is not UNDEFINED: - # primary ones have to be at the start. - if device_info_type == "primary": - # Move entry to first spot - if not config_entries or config_entries[0] != add_config_entry_id: - config_entries = [add_config_entry_id] + [ - entry - for entry in config_entries - if entry != add_config_entry_id - ] + if add_config_entry is not UNDEFINED: + primary_entry_id = old.primary_config_entry + if ( + device_info_type == "primary" + and add_config_entry.entry_id != primary_entry_id + ): + if ( + primary_entry_id is None + or not ( + primary_entry := self.hass.config_entries.async_get_entry( + primary_entry_id + ) + ) + or primary_entry.domain in LOW_PRIO_CONFIG_ENTRY_DOMAINS + ): + new_values["primary_config_entry"] = add_config_entry.entry_id + old_values["primary_config_entry"] = old.primary_config_entry - # Not primary, append - elif add_config_entry_id not in config_entries: - config_entries = [*config_entries, add_config_entry_id] + if add_config_entry.entry_id not in old.config_entries: + config_entries = old.config_entries | {add_config_entry.entry_id} if ( remove_config_entry_id is not UNDEFINED and remove_config_entry_id in config_entries ): - if config_entries == [remove_config_entry_id]: + if config_entries == {remove_config_entry_id}: self.async_remove_device(device_id) return None - config_entries = [ - entry for entry in config_entries if entry != remove_config_entry_id - ] + if remove_config_entry_id == old.primary_config_entry: + new_values["primary_config_entry"] = None + old_values["primary_config_entry"] = old.primary_config_entry + + config_entries = config_entries - {remove_config_entry_id} if config_entries != old.config_entries: new_values["config_entries"] = config_entries @@ -1095,7 +1123,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): for device in data["devices"]: devices[device["id"]] = DeviceEntry( area_id=device["area_id"], - config_entries=device["config_entries"], + config_entries=set(device["config_entries"]), configuration_url=device["configuration_url"], # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 connections={ @@ -1123,6 +1151,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): model=device["model"], name_by_user=device["name_by_user"], name=device["name"], + primary_config_entry=device["primary_config_entry"], serial_number=device["serial_number"], sw_version=device["sw_version"], via_device_id=device["via_device_id"], @@ -1130,7 +1159,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): # Introduced in 0.111 for device in data["deleted_devices"]: deleted_devices[device["id"]] = DeletedDeviceEntry( - config_entries=device["config_entries"], + config_entries=set(device["config_entries"]), connections={tuple(conn) for conn in device["connections"]}, identifiers={tuple(iden) for iden in device["identifiers"]}, id=device["id"], @@ -1161,15 +1190,13 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): config_entries = deleted_device.config_entries if config_entry_id not in config_entries: continue - if config_entries == [config_entry_id]: + if config_entries == {config_entry_id}: # Add a time stamp when the deleted device became orphaned self.deleted_devices[deleted_device.id] = attr.evolve( - deleted_device, orphaned_timestamp=now_time, config_entries=[] + deleted_device, orphaned_timestamp=now_time, config_entries=set() ) else: - config_entries = [ - entry for entry in config_entries if entry != config_entry_id - ] + config_entries = config_entries - {config_entry_id} # No need to reindex here since we currently # do not have a lookup by config entry self.deleted_devices[deleted_device.id] = attr.evolve( @@ -1275,8 +1302,8 @@ def async_config_entry_disabled_by_changed( if device.disabled: # Device already disabled, do not overwrite continue - if len(device.config_entries) > 1 and any( - entry_id in enabled_config_entries for entry_id in device.config_entries + if len(device.config_entries) > 1 and device.config_entries.intersection( + enabled_config_entries ): continue registry.async_update_device( diff --git a/tests/components/airgradient/snapshots/test_init.ambr b/tests/components/airgradient/snapshots/test_init.ambr index 7c2e6ce4f78..4462a996a49 100644 --- a/tests/components/airgradient/snapshots/test_init.ambr +++ b/tests/components/airgradient/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'I-9PSL', 'name': 'Airgradient', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': '84fce612f5b8', 'suggested_area': None, 'sw_version': '3.1.1', @@ -53,6 +54,7 @@ 'model': 'O-1PPT', 'name': 'Airgradient', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': '84fce60bec38', 'suggested_area': None, 'sw_version': '3.1.1', diff --git a/tests/components/aosmith/snapshots/test_device.ambr b/tests/components/aosmith/snapshots/test_device.ambr index f6e2625afdb..d563090ce9d 100644 --- a/tests/components/aosmith/snapshots/test_device.ambr +++ b/tests/components/aosmith/snapshots/test_device.ambr @@ -23,6 +23,7 @@ 'model': 'HPTS-50 200 202172000', 'name': 'My water heater', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'serial', 'suggested_area': 'Basement', 'sw_version': '2.14', diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 804cf29979e..0717bb6046d 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -70,6 +70,7 @@ async def test_list_devices( "model": "model", "name_by_user": None, "name": None, + "primary_config_entry": entry.entry_id, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -88,6 +89,7 @@ async def test_list_devices( "model": "model", "name_by_user": None, "name": None, + "primary_config_entry": entry.entry_id, "serial_number": None, "sw_version": None, "via_device_id": dev1, @@ -119,6 +121,7 @@ async def test_list_devices( "model": "model", "name_by_user": None, "name": None, + "primary_config_entry": entry.entry_id, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -274,7 +277,7 @@ async def test_remove_config_entry_from_device( config_entry_id=entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - assert device_entry.config_entries == [entry_1.entry_id, entry_2.entry_id] + assert device_entry.config_entries == {entry_1.entry_id, entry_2.entry_id} # Try removing a config entry from the device, it should fail because # async_remove_config_entry_device returns False @@ -293,9 +296,9 @@ async def test_remove_config_entry_from_device( assert response["result"]["config_entries"] == [entry_2.entry_id] # Check that the config entry was removed from the device - assert device_registry.async_get(device_entry.id).config_entries == [ + assert device_registry.async_get(device_entry.id).config_entries == { entry_2.entry_id - ] + } # Remove the 2nd config entry response = await ws_client.remove_device(device_entry.id, entry_2.entry_id) @@ -365,11 +368,11 @@ async def test_remove_config_entry_from_device_fails( config_entry_id=entry_3.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - assert device_entry.config_entries == [ + assert device_entry.config_entries == { entry_1.entry_id, entry_2.entry_id, entry_3.entry_id, - ] + } fake_entry_id = "abc123" assert entry_1.entry_id != fake_entry_id diff --git a/tests/components/devolo_home_network/snapshots/test_init.ambr b/tests/components/devolo_home_network/snapshots/test_init.ambr index b042dfec2f1..8c265400643 100644 --- a/tests/components/devolo_home_network/snapshots/test_init.ambr +++ b/tests/components/devolo_home_network/snapshots/test_init.ambr @@ -27,6 +27,7 @@ 'model': 'dLAN pro 1200+ WiFi ac', 'name': 'Mock Title', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': '1234567890', 'suggested_area': None, 'sw_version': '5.6.1', diff --git a/tests/components/ecovacs/snapshots/test_init.ambr b/tests/components/ecovacs/snapshots/test_init.ambr index f47e747b1cf..3ce872e7898 100644 --- a/tests/components/ecovacs/snapshots/test_init.ambr +++ b/tests/components/ecovacs/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'DEEBOT OZMO 950 Series', 'name': 'Ozmo 950', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'E1234567890000000001', 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/elgato/snapshots/test_button.ambr b/tests/components/elgato/snapshots/test_button.ambr index e7477540f46..77555c85a06 100644 --- a/tests/components/elgato/snapshots/test_button.ambr +++ b/tests/components/elgato/snapshots/test_button.ambr @@ -74,6 +74,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -155,6 +156,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', diff --git a/tests/components/elgato/snapshots/test_light.ambr b/tests/components/elgato/snapshots/test_light.ambr index e2f663d294b..8e2962fc698 100644 --- a/tests/components/elgato/snapshots/test_light.ambr +++ b/tests/components/elgato/snapshots/test_light.ambr @@ -106,6 +106,7 @@ 'model': 'Elgato Key Light', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', @@ -221,6 +222,7 @@ 'model': 'Elgato Light Strip', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', @@ -336,6 +338,7 @@ 'model': 'Elgato Light Strip', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'CN11A1A00001', 'suggested_area': None, 'sw_version': '1.0.3 (192)', diff --git a/tests/components/elgato/snapshots/test_sensor.ambr b/tests/components/elgato/snapshots/test_sensor.ambr index 2b52d6b9f23..c2bcde7a66b 100644 --- a/tests/components/elgato/snapshots/test_sensor.ambr +++ b/tests/components/elgato/snapshots/test_sensor.ambr @@ -81,6 +81,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -172,6 +173,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -263,6 +265,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -351,6 +354,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -442,6 +446,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', diff --git a/tests/components/elgato/snapshots/test_switch.ambr b/tests/components/elgato/snapshots/test_switch.ambr index 41f3a8f3aaf..12857a71cb3 100644 --- a/tests/components/elgato/snapshots/test_switch.ambr +++ b/tests/components/elgato/snapshots/test_switch.ambr @@ -73,6 +73,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', @@ -153,6 +154,7 @@ 'model': 'Elgato Key Light Mini', 'name': 'Frenck', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GW24L1A02987', 'suggested_area': None, 'sw_version': '1.0.4 (229)', diff --git a/tests/components/energyzero/snapshots/test_sensor.ambr b/tests/components/energyzero/snapshots/test_sensor.ambr index 23b232379df..da52526192e 100644 --- a/tests/components/energyzero/snapshots/test_sensor.ambr +++ b/tests/components/energyzero/snapshots/test_sensor.ambr @@ -64,6 +64,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -138,6 +139,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -209,6 +211,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -280,6 +283,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -351,6 +355,7 @@ 'model': None, 'name': 'Energy market price', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -425,6 +430,7 @@ 'model': None, 'name': 'Gas market price', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index 008922e8d2b..acaee292237 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -48,6 +48,7 @@ 'model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', 'name': 'Envoy <>', 'name_by_user': None, + 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', 'suggested_area': None, 'sw_version': '7.1.2', @@ -3772,6 +3773,7 @@ 'model': 'Inverter', 'name': 'Inverter 1', 'name_by_user': None, + 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -4043,6 +4045,7 @@ 'model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', 'name': 'Envoy <>', 'name_by_user': None, + 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', 'suggested_area': None, 'sw_version': '7.1.2', @@ -7767,6 +7770,7 @@ 'model': 'Inverter', 'name': 'Inverter 1', 'name_by_user': None, + 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -8078,6 +8082,7 @@ 'model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', 'name': 'Envoy <>', 'name_by_user': None, + 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': '<>', 'suggested_area': None, 'sw_version': '7.1.2', @@ -11802,6 +11807,7 @@ 'model': 'Inverter', 'name': 'Inverter 1', 'name_by_user': None, + 'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72', 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/gardena_bluetooth/snapshots/test_init.ambr b/tests/components/gardena_bluetooth/snapshots/test_init.ambr index 82e17896d60..8cd77136f8f 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_init.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'Mock Model', 'name': 'Mock Title', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.2.3', diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index c52bf2c3b27..394a442787d 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -26,6 +26,7 @@ 'model': 'AP2', 'name': 'Airversa AP2 1808', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1234', 'suggested_area': None, 'sw_version': '0.8.16', @@ -622,6 +623,7 @@ 'model': 'T8010', 'name': 'eufy HomeBase2-0AAA', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000A', 'suggested_area': None, 'sw_version': '2.1.6', @@ -695,6 +697,7 @@ 'model': 'T8113', 'name': 'eufyCam2-0000', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000D', 'suggested_area': None, 'sw_version': '1.6.7', @@ -936,6 +939,7 @@ 'model': 'T8113', 'name': 'eufyCam2-000A', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000B', 'suggested_area': None, 'sw_version': '1.6.7', @@ -1177,6 +1181,7 @@ 'model': 'T8113', 'name': 'eufyCam2-000A', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'A0000A000000000C', 'suggested_area': None, 'sw_version': '1.6.7', @@ -1422,6 +1427,7 @@ 'model': 'HE1-G01', 'name': 'Aqara-Hub-E1-00A0', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '00aa00000a0', 'suggested_area': None, 'sw_version': '3.3.0', @@ -1628,6 +1634,7 @@ 'model': 'AS006', 'name': 'Contact Sensor', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '158d0007c59c6a', 'suggested_area': None, 'sw_version': '0', @@ -1792,6 +1799,7 @@ 'model': 'ZHWA11LM', 'name': 'Aqara Hub-1563', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '0000000123456789', 'suggested_area': None, 'sw_version': '1.4.7', @@ -2067,6 +2075,7 @@ 'model': 'AR004', 'name': 'Programmable Switch', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '111a1111a1a111', 'suggested_area': None, 'sw_version': '9', @@ -2190,6 +2199,7 @@ 'model': 'ABC1000', 'name': 'ArloBabyA0', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '00A0000000000', 'suggested_area': None, 'sw_version': '1.10.931', @@ -2674,6 +2684,7 @@ 'model': 'CS-IWO', 'name': 'InWall Outlet-0394DE', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1020301376', 'suggested_area': None, 'sw_version': '1.0.0', @@ -3103,6 +3114,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Basement', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AB3C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -3262,6 +3274,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', @@ -3716,6 +3729,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Kitchen', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AB1C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -3875,6 +3889,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Porch', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AB2C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -4038,6 +4053,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', @@ -4496,6 +4512,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Basement', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AB3C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -4610,6 +4627,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', @@ -4891,6 +4909,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Kitchen', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AB1C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -5050,6 +5069,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Porch', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AB2C', 'suggested_area': None, 'sw_version': '1.0.0', @@ -5213,6 +5233,7 @@ 'model': 'ECB501', 'name': 'My ecobee', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '123456789016', 'suggested_area': None, 'sw_version': '4.7.340214', @@ -5680,6 +5701,7 @@ 'model': 'ecobee Switch+', 'name': 'Master Fan', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '111111111111', 'suggested_area': None, 'sw_version': '4.5.130201', @@ -5969,6 +5991,7 @@ 'model': 'Eve Degree 00AAA0000', 'name': 'Eve Degree AA11', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AA00A0A00000', 'suggested_area': None, 'sw_version': '1.2.8', @@ -6325,6 +6348,7 @@ 'model': 'Eve Energy 20EAO8601', 'name': 'Eve Energy 50FF', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AA00A0A00000', 'suggested_area': None, 'sw_version': '1.2.9', @@ -6663,6 +6687,7 @@ 'model': 'RavenSystem HAA', 'name': 'HAA-C718B3', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'C718B3-1', 'suggested_area': None, 'sw_version': '5.0.18', @@ -6868,6 +6893,7 @@ 'model': 'RavenSystem HAA', 'name': 'HAA-C718B3', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'C718B3-2', 'suggested_area': None, 'sw_version': '5.0.18', @@ -7303,6 +7329,7 @@ 'model': 'RYSE Shade', 'name': 'Family Room North', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'cover.family_door_north', 'suggested_area': None, 'sw_version': '3.6.2', @@ -7464,6 +7491,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -7537,6 +7565,7 @@ 'model': 'RYSE Shade', 'name': 'Kitchen Window', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'cover.kitchen_window', 'suggested_area': None, 'sw_version': '3.6.2', @@ -7702,6 +7731,7 @@ 'model': 'Fan', 'name': 'Ceiling Fan', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'fan.ceiling_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -7822,6 +7852,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -7895,6 +7926,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -8020,6 +8052,7 @@ 'model': 'Climate Control', 'name': '89 Living Room', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'climate.89_living_room', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8342,6 +8375,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8419,6 +8453,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8492,6 +8527,7 @@ 'model': '1039102', 'name': 'Laundry Smoke ED78', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'light.laundry_smoke_ed78', 'suggested_area': None, 'sw_version': '1.4.84', @@ -8665,6 +8701,7 @@ 'model': 'RYSE Shade', 'name': 'Family Room North', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'cover.family_door_north', 'suggested_area': None, 'sw_version': '3.6.2', @@ -8826,6 +8863,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -8899,6 +8937,7 @@ 'model': 'RYSE Shade', 'name': 'Kitchen Window', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'cover.kitchen_window', 'suggested_area': None, 'sw_version': '3.6.2', @@ -9064,6 +9103,7 @@ 'model': 'Fan', 'name': 'Ceiling Fan', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'fan.ceiling_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9184,6 +9224,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9257,6 +9298,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9383,6 +9425,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9456,6 +9499,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', @@ -9582,6 +9626,7 @@ 'model': 'Climate Control', 'name': '89 Living Room', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'climate.89_living_room', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -9913,6 +9958,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -9990,6 +10036,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10063,6 +10110,7 @@ 'model': 'WoHumi', 'name': 'Humidifier 182A', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'humidifier.humidifier_182a', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10243,6 +10291,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10316,6 +10365,7 @@ 'model': 'WoHumi', 'name': 'Humidifier 182A', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'humidifier.humidifier_182a', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10496,6 +10546,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', @@ -10569,6 +10620,7 @@ 'model': '1039102', 'name': 'Laundry Smoke ED78', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'light.laundry_smoke_ed78', 'suggested_area': None, 'sw_version': '1.4.84', @@ -10757,6 +10809,7 @@ 'model': 'Daikin-fwec3a-esp32-homekit-bridge', 'name': 'Air Conditioner', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '00000001', 'suggested_area': None, 'sw_version': '1.0.0', @@ -10955,6 +11008,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462395276914', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11091,6 +11145,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462395276939', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11227,6 +11282,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462403113447', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11363,6 +11419,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462403233419', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11499,6 +11556,7 @@ 'model': 'LTW013', 'name': 'Hue ambiance spot', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462412411853', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11645,6 +11703,7 @@ 'model': 'LTW013', 'name': 'Hue ambiance spot', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462412413293', 'suggested_area': None, 'sw_version': '1.46.13', @@ -11791,6 +11850,7 @@ 'model': 'RWL021', 'name': 'Hue dimmer switch', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462389072572', 'suggested_area': None, 'sw_version': '45.1.17846', @@ -12106,6 +12166,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462378982941', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12229,6 +12290,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462378983942', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12352,6 +12414,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462379122122', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12475,6 +12538,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462379123707', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12598,6 +12662,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462383114163', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12721,6 +12786,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462383114193', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12844,6 +12910,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '6623462385996792', 'suggested_area': None, 'sw_version': '1.46.13', @@ -12967,6 +13034,7 @@ 'model': 'BSB002', 'name': 'Philips hue - 482544', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '123456', 'suggested_area': None, 'sw_version': '1.32.1932126170', @@ -13044,6 +13112,7 @@ 'model': 'LS1', 'name': 'Koogeek-LS1-20833F', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AAAA011111111111', 'suggested_area': None, 'sw_version': '2.2.15', @@ -13186,6 +13255,7 @@ 'model': 'P1EU', 'name': 'Koogeek-P1-A00AA0', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'EUCP03190xxxxx48', 'suggested_area': None, 'sw_version': '2.3.7', @@ -13349,6 +13419,7 @@ 'model': 'KH02CN', 'name': 'Koogeek-SW2-187A91', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'CNNT061751001372', 'suggested_area': None, 'sw_version': '1.0.3', @@ -13551,6 +13622,7 @@ 'model': 'E30 2B', 'name': 'Lennox', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'XXXXXXXX', 'suggested_area': None, 'sw_version': '3.40.XX', @@ -13831,6 +13903,7 @@ 'model': 'OLED55B9PUA', 'name': 'LG webOS TV AF80', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '999AAAAAA999', 'suggested_area': None, 'sw_version': '04.71.04', @@ -14010,6 +14083,7 @@ 'model': 'PD-FSQN-XX', 'name': 'Caséta® Wireless Fan Speed Control', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '39024290', 'suggested_area': None, 'sw_version': '001.005', @@ -14130,6 +14204,7 @@ 'model': 'L-BDG2-WH', 'name': 'Smart Bridge 2', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '12344331', 'suggested_area': None, 'sw_version': '08.08', @@ -14207,6 +14282,7 @@ 'model': 'MSS425F', 'name': 'MSS425F-15cc', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'HH41234', 'suggested_area': None, 'sw_version': '4.2.3', @@ -14484,6 +14560,7 @@ 'model': 'MSS565', 'name': 'MSS565-28da', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'BB1121', 'suggested_area': None, 'sw_version': '4.1.9', @@ -14611,6 +14688,7 @@ 'model': 'v1', 'name': 'Mysa-85dda9', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAA000', 'suggested_area': None, 'sw_version': '2.8.1', @@ -14939,6 +15017,7 @@ 'model': 'NL55', 'name': 'Nanoleaf Strip 3B32', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AAAA011111111111', 'suggested_area': None, 'sw_version': '1.4.40', @@ -15209,6 +15288,7 @@ 'model': 'Netatmo Doorbell', 'name': 'Netatmo-Doorbell-g738658', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'g738658', 'suggested_area': None, 'sw_version': '80.0.0', @@ -15501,6 +15581,7 @@ 'model': 'Smart CO Alarm', 'name': 'Smart CO Alarm', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1234', 'suggested_area': None, 'sw_version': '1.0.3', @@ -15660,6 +15741,7 @@ 'model': 'Healthy Home Coach', 'name': 'Healthy Home Coach', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAAAAAAAA', 'suggested_area': None, 'sw_version': '59', @@ -15961,6 +16043,7 @@ 'model': 'SPK5 Pro', 'name': 'RainMachine-00ce4a', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '00aa0000aa0a', 'suggested_area': None, 'sw_version': '1.0.4', @@ -16382,6 +16465,7 @@ 'model': 'RYSE Shade', 'name': 'Master Bath South', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -16543,6 +16627,7 @@ 'model': 'RYSE SmartBridge', 'name': 'RYSE SmartBridge', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '0101.3521.0436', 'suggested_area': None, 'sw_version': '1.3.0', @@ -16616,6 +16701,7 @@ 'model': 'RYSE Shade', 'name': 'RYSE SmartShade', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '', 'suggested_area': None, 'sw_version': '', @@ -16781,6 +16867,7 @@ 'model': 'RYSE Shade', 'name': 'BR Left', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -16942,6 +17029,7 @@ 'model': 'RYSE Shade', 'name': 'LR Left', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -17103,6 +17191,7 @@ 'model': 'RYSE Shade', 'name': 'LR Right', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -17264,6 +17353,7 @@ 'model': 'RYSE SmartBridge', 'name': 'RYSE SmartBridge', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '0401.3521.0679', 'suggested_area': None, 'sw_version': '1.3.0', @@ -17337,6 +17427,7 @@ 'model': 'RYSE Shade', 'name': 'RZSS', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', @@ -17502,6 +17593,7 @@ 'model': 'BE479CAM619', 'name': 'SENSE ', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AAAAAAA000', 'suggested_area': None, 'sw_version': '004.027.000', @@ -17620,6 +17712,7 @@ 'model': 'SIMPLEconnect', 'name': 'SIMPLEconnect Fan-06F674', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1234567890abcd', 'suggested_area': None, 'sw_version': '', @@ -17795,6 +17888,7 @@ 'model': 'VELUX Gateway', 'name': 'VELUX Gateway', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'a1a11a1', 'suggested_area': None, 'sw_version': '70', @@ -17868,6 +17962,7 @@ 'model': 'VELUX Sensor', 'name': 'VELUX Sensor', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'a11b111', 'suggested_area': None, 'sw_version': '16', @@ -18076,6 +18171,7 @@ 'model': 'VELUX Window', 'name': 'VELUX Window', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': '1111111a114a111a', 'suggested_area': None, 'sw_version': '48', @@ -18196,6 +18292,7 @@ 'model': 'Flowerbud', 'name': 'VOCOlinc-Flowerbud-0d324b', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'AM01121849000327', 'suggested_area': None, 'sw_version': '3.121.2', @@ -18500,6 +18597,7 @@ 'model': 'VP3', 'name': 'VOCOlinc-VP3-123456', 'name_by_user': None, + 'primary_config_entry': 'TestData', 'serial_number': 'EU0121203xxxxx07', 'suggested_area': None, 'sw_version': '1.101.2', diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index 0f2cdb7c9db..0a77509d675 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -118,7 +118,7 @@ async def test_migrate_device_id_no_serial_skip_if_other_owner( bridge = device_registry.async_get(bridge.id) assert bridge.identifiers == variant.before - assert bridge.config_entries == [entry.entry_id] + assert bridge.config_entries == {entry.entry_id} @pytest.mark.parametrize("variant", DEVICE_MIGRATION_TESTS) diff --git a/tests/components/homewizard/snapshots/test_button.ambr b/tests/components/homewizard/snapshots/test_button.ambr index 5ab108d344c..eabaeb648aa 100644 --- a/tests/components/homewizard/snapshots/test_button.ambr +++ b/tests/components/homewizard/snapshots/test_button.ambr @@ -74,6 +74,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', diff --git a/tests/components/homewizard/snapshots/test_number.ambr b/tests/components/homewizard/snapshots/test_number.ambr index a9c9e45098d..f292847f2a2 100644 --- a/tests/components/homewizard/snapshots/test_number.ambr +++ b/tests/components/homewizard/snapshots/test_number.ambr @@ -83,6 +83,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -173,6 +174,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index 5e8ddc0d6be..27dfd6399c7 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -60,6 +60,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -145,6 +146,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -230,6 +232,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -315,6 +318,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -400,6 +404,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -485,6 +490,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -573,6 +579,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -658,6 +665,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -743,6 +751,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -828,6 +837,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -908,6 +918,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -992,6 +1003,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1077,6 +1089,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1162,6 +1175,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1247,6 +1261,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1332,6 +1347,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1417,6 +1433,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1502,6 +1519,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1587,6 +1605,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1672,6 +1691,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1757,6 +1777,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1842,6 +1863,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -1927,6 +1949,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2015,6 +2038,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2100,6 +2124,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2185,6 +2210,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2270,6 +2296,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2358,6 +2385,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2446,6 +2474,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2534,6 +2563,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2619,6 +2649,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2704,6 +2735,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2789,6 +2821,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2874,6 +2907,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -2959,6 +2993,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3044,6 +3079,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3129,6 +3165,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3209,6 +3246,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -3293,6 +3331,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3375,6 +3414,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3460,6 +3500,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3545,6 +3586,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3630,6 +3672,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3710,6 +3753,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3795,6 +3839,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3880,6 +3925,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -3965,6 +4011,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4050,6 +4097,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4135,6 +4183,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4220,6 +4269,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4305,6 +4355,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4390,6 +4441,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4475,6 +4527,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4560,6 +4613,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4645,6 +4699,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4725,6 +4780,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4807,6 +4863,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4895,6 +4952,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -4975,6 +5033,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5063,6 +5122,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5151,6 +5211,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5239,6 +5300,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5319,6 +5381,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5399,6 +5462,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5493,6 +5557,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5578,6 +5643,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5663,6 +5729,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5748,6 +5815,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5833,6 +5901,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5913,6 +5982,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -5993,6 +6063,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6073,6 +6144,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6153,6 +6225,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6233,6 +6306,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6313,6 +6387,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6397,6 +6472,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6477,6 +6553,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -6557,6 +6634,7 @@ 'model': 'HWE-P1', 'name': 'Gas meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'gas_meter_G001', 'suggested_area': None, 'sw_version': None, @@ -6638,6 +6716,7 @@ 'model': 'HWE-P1', 'name': 'Heat meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'heat_meter_H001', 'suggested_area': None, 'sw_version': None, @@ -6719,6 +6798,7 @@ 'model': 'HWE-P1', 'name': 'Inlet heat meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'inlet_heat_meter_IH001', 'suggested_area': None, 'sw_version': None, @@ -6799,6 +6879,7 @@ 'model': 'HWE-P1', 'name': 'Warm water meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'warm_water_meter_WW001', 'suggested_area': None, 'sw_version': None, @@ -6880,6 +6961,7 @@ 'model': 'HWE-P1', 'name': 'Water meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'water_meter_W001', 'suggested_area': None, 'sw_version': None, @@ -6965,6 +7047,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7047,6 +7130,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7132,6 +7216,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7217,6 +7302,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7302,6 +7388,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7382,6 +7469,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7467,6 +7555,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7552,6 +7641,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7637,6 +7727,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7722,6 +7813,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7807,6 +7899,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7892,6 +7985,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -7977,6 +8071,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8062,6 +8157,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8147,6 +8243,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8232,6 +8329,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8317,6 +8415,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8397,6 +8496,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8479,6 +8579,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8567,6 +8668,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8647,6 +8749,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8735,6 +8838,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8823,6 +8927,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8911,6 +9016,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -8991,6 +9097,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9071,6 +9178,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9165,6 +9273,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9250,6 +9359,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9335,6 +9445,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9420,6 +9531,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9505,6 +9617,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9585,6 +9698,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9665,6 +9779,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9745,6 +9860,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9825,6 +9941,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9905,6 +10022,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -9985,6 +10103,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10069,6 +10188,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10149,6 +10269,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10229,6 +10350,7 @@ 'model': 'HWE-P1', 'name': 'Gas meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'gas_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10310,6 +10432,7 @@ 'model': 'HWE-P1', 'name': 'Heat meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10391,6 +10514,7 @@ 'model': 'HWE-P1', 'name': 'Inlet heat meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'inlet_heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10471,6 +10595,7 @@ 'model': 'HWE-P1', 'name': 'Warm water meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'warm_water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10552,6 +10677,7 @@ 'model': 'HWE-P1', 'name': 'Water meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'suggested_area': None, 'sw_version': None, @@ -10637,6 +10763,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10719,6 +10846,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10804,6 +10932,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10889,6 +11018,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -10974,6 +11104,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11059,6 +11190,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11144,6 +11276,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11229,6 +11362,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11314,6 +11448,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11399,6 +11534,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11484,6 +11620,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11569,6 +11706,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11654,6 +11792,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11739,6 +11878,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11824,6 +11964,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11909,6 +12050,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -11989,6 +12131,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12077,6 +12220,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12157,6 +12301,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12245,6 +12390,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12333,6 +12479,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12421,6 +12568,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12506,6 +12654,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12591,6 +12740,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12676,6 +12826,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12761,6 +12912,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12841,6 +12993,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -12921,6 +13074,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13001,6 +13155,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13081,6 +13236,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13161,6 +13317,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13241,6 +13398,7 @@ 'model': 'HWE-P1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.19', @@ -13325,6 +13483,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13410,6 +13569,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13495,6 +13655,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13583,6 +13744,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13671,6 +13833,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13751,6 +13914,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -13835,6 +13999,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -13920,6 +14085,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14005,6 +14171,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14090,6 +14257,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14175,6 +14343,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14260,6 +14429,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14348,6 +14518,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14433,6 +14604,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14521,6 +14693,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14606,6 +14779,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14691,6 +14865,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14771,6 +14946,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -14855,6 +15031,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -14940,6 +15117,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -15024,6 +15202,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -15104,6 +15283,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -15188,6 +15368,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15273,6 +15454,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15358,6 +15540,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15443,6 +15626,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15528,6 +15712,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15613,6 +15798,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15701,6 +15887,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15786,6 +15973,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15871,6 +16059,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -15956,6 +16145,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16036,6 +16226,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16120,6 +16311,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16205,6 +16397,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16290,6 +16483,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16375,6 +16569,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16460,6 +16655,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16545,6 +16741,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16630,6 +16827,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16715,6 +16913,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16800,6 +16999,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16885,6 +17085,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -16970,6 +17171,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17055,6 +17257,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17143,6 +17346,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17228,6 +17432,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17313,6 +17518,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17398,6 +17604,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17486,6 +17693,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17574,6 +17782,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17662,6 +17871,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17747,6 +17957,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17832,6 +18043,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -17917,6 +18129,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18002,6 +18215,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18087,6 +18301,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18172,6 +18387,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18257,6 +18473,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -18337,6 +18554,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', diff --git a/tests/components/homewizard/snapshots/test_switch.ambr b/tests/components/homewizard/snapshots/test_switch.ambr index 99a5bcab6cb..ba630e2f0b4 100644 --- a/tests/components/homewizard/snapshots/test_switch.ambr +++ b/tests/components/homewizard/snapshots/test_switch.ambr @@ -73,6 +73,7 @@ 'model': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -153,6 +154,7 @@ 'model': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -234,6 +236,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -314,6 +317,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -394,6 +398,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.03', @@ -475,6 +480,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -555,6 +561,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -635,6 +642,7 @@ 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '4.07', @@ -715,6 +723,7 @@ 'model': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '2.03', @@ -795,6 +804,7 @@ 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', @@ -875,6 +885,7 @@ 'model': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '3.06', diff --git a/tests/components/husqvarna_automower/snapshots/test_init.ambr b/tests/components/husqvarna_automower/snapshots/test_init.ambr index c3a7191b4b9..efe1eb8bd51 100644 --- a/tests/components/husqvarna_automower/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': '450XH-TEST', 'name': 'Test Mower 1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 123, 'suggested_area': 'Garden', 'sw_version': None, diff --git a/tests/components/hyperion/test_camera.py b/tests/components/hyperion/test_camera.py index 41b66f4ad4a..0169759f328 100644 --- a/tests/components/hyperion/test_camera.py +++ b/tests/components/hyperion/test_camera.py @@ -198,7 +198,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) assert device - assert device.config_entries == [TEST_CONFIG_ENTRY_ID] + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} assert device.identifiers == {(DOMAIN, device_id)} assert device.manufacturer == HYPERION_MANUFACTURER_NAME assert device.model == HYPERION_MODEL_NAME diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index b7aef3ac2ac..e1e7711e702 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -803,7 +803,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) assert device - assert device.config_entries == [TEST_CONFIG_ENTRY_ID] + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} assert device.identifiers == {(DOMAIN, device_id)} assert device.manufacturer == HYPERION_MANUFACTURER_NAME assert device.model == HYPERION_MODEL_NAME diff --git a/tests/components/hyperion/test_sensor.py b/tests/components/hyperion/test_sensor.py index bc58c07ac7b..5ace34eaac0 100644 --- a/tests/components/hyperion/test_sensor.py +++ b/tests/components/hyperion/test_sensor.py @@ -66,7 +66,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={(DOMAIN, device_identifer)}) assert device - assert device.config_entries == [TEST_CONFIG_ENTRY_ID] + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} assert device.identifiers == {(DOMAIN, device_identifer)} assert device.manufacturer == HYPERION_MANUFACTURER_NAME assert device.model == HYPERION_MODEL_NAME diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py index 17a1872f832..da458820c81 100644 --- a/tests/components/hyperion/test_switch.py +++ b/tests/components/hyperion/test_switch.py @@ -170,7 +170,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={(DOMAIN, device_identifer)}) assert device - assert device.config_entries == [TEST_CONFIG_ENTRY_ID] + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} assert device.identifiers == {(DOMAIN, device_identifer)} assert device.manufacturer == HYPERION_MANUFACTURER_NAME assert device.model == HYPERION_MODEL_NAME diff --git a/tests/components/ista_ecotrend/snapshots/test_init.ambr b/tests/components/ista_ecotrend/snapshots/test_init.ambr index a9d13510b54..c5dec7d9d56 100644 --- a/tests/components/ista_ecotrend/snapshots/test_init.ambr +++ b/tests/components/ista_ecotrend/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'ista EcoTrend', 'name': 'Luxemburger Str. 1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -53,6 +54,7 @@ 'model': 'ista EcoTrend', 'name': 'Bahnhofsstr. 1A', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/kitchen_sink/snapshots/test_switch.ambr b/tests/components/kitchen_sink/snapshots/test_switch.ambr index 1cd903a59d6..277b4888e05 100644 --- a/tests/components/kitchen_sink/snapshots/test_switch.ambr +++ b/tests/components/kitchen_sink/snapshots/test_switch.ambr @@ -69,6 +69,7 @@ 'model': None, 'name': 'Outlet 1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -99,6 +100,7 @@ 'model': None, 'name': 'Power strip with 2 sockets', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -175,6 +177,7 @@ 'model': None, 'name': 'Outlet 2', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -205,6 +208,7 @@ 'model': None, 'name': 'Power strip with 2 sockets', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 09864be1d5c..0f462955a33 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -115,6 +115,7 @@ 'model': , 'name': 'GS01234', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'GS01234', 'suggested_area': None, 'sw_version': '1.40', diff --git a/tests/components/lifx/test_migration.py b/tests/components/lifx/test_migration.py index 62018790906..0604ee1c8a7 100644 --- a/tests/components/lifx/test_migration.py +++ b/tests/components/lifx/test_migration.py @@ -65,7 +65,7 @@ async def test_migration_device_online_end_to_end( assert migrated_entry is not None - assert device.config_entries == [migrated_entry.entry_id] + 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) == [] @@ -195,7 +195,7 @@ async def test_migration_device_online_end_to_end_after_downgrade( 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 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) == [] @@ -276,7 +276,7 @@ async def test_migration_device_online_end_to_end_ignores_other_devices( assert new_entry is not None assert legacy_entry is None - assert device.config_entries == [legacy_config_entry.entry_id] + 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 diff --git a/tests/components/lutron_caseta/test_logbook.py b/tests/components/lutron_caseta/test_logbook.py index 51c96b9d9a9..b6e8840c85c 100644 --- a/tests/components/lutron_caseta/test_logbook.py +++ b/tests/components/lutron_caseta/test_logbook.py @@ -111,7 +111,7 @@ async def test_humanify_lutron_caseta_button_event_integration_not_loaded( await hass.async_block_till_done() for device in device_registry.devices.values(): - if device.config_entries == [config_entry.entry_id]: + if device.config_entries == {config_entry.entry_id}: dr_device_id = device.id break diff --git a/tests/components/mealie/snapshots/test_init.ambr b/tests/components/mealie/snapshots/test_init.ambr index c2752d938e4..1333b292dac 100644 --- a/tests/components/mealie/snapshots/test_init.ambr +++ b/tests/components/mealie/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': None, 'name': 'Mealie', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index ccbdc022495..0f3a7d6f904 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -339,7 +339,7 @@ async def test_device_info( device = device_registry.async_get_device(identifiers={device_identifier}) assert device - assert device.config_entries == [TEST_CONFIG_ENTRY_ID] + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} assert device.identifiers == {device_identifier} assert device.manufacturer == MOTIONEYE_MANUFACTURER assert device.model == MOTIONEYE_MANUFACTURER diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 23dea310199..8c51e295998 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -965,10 +965,10 @@ async def test_cleanup_device_multiple_config_entries( connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None - assert device_entry.config_entries == [ - config_entry.entry_id, + assert device_entry.config_entries == { mqtt_config_entry.entry_id, - ] + config_entry.entry_id, + } entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is not None @@ -991,7 +991,7 @@ async def test_cleanup_device_multiple_config_entries( ) assert device_entry is not None entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") - assert device_entry.config_entries == [config_entry.entry_id] + assert device_entry.config_entries == {config_entry.entry_id} assert entity_entry is None # Verify state is removed @@ -1060,10 +1060,10 @@ async def test_cleanup_device_multiple_config_entries_mqtt( connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None - assert device_entry.config_entries == [ - config_entry.entry_id, + assert device_entry.config_entries == { mqtt_config_entry.entry_id, - ] + config_entry.entry_id, + } entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is not None @@ -1084,7 +1084,7 @@ async def test_cleanup_device_multiple_config_entries_mqtt( ) assert device_entry is not None entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") - assert device_entry.config_entries == [config_entry.entry_id] + assert device_entry.config_entries == {config_entry.entry_id} assert entity_entry is None # Verify state is removed diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index e70c06c2c4a..0d0765258f2 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -587,7 +587,7 @@ async def test_cleanup_tag( identifiers={("mqtt", "helloworld")} ) assert device_entry1 is not None - assert device_entry1.config_entries == [config_entry.entry_id, mqtt_entry.entry_id] + assert device_entry1.config_entries == {config_entry.entry_id, mqtt_entry.entry_id} device_entry2 = device_registry.async_get_device(identifiers={("mqtt", "hejhopp")}) assert device_entry2 is not None @@ -599,7 +599,7 @@ async def test_cleanup_tag( identifiers={("mqtt", "helloworld")} ) assert device_entry1 is not None - assert device_entry1.config_entries == [mqtt_entry.entry_id] + assert device_entry1.config_entries == {mqtt_entry.entry_id} device_entry2 = device_registry.async_get_device(identifiers={("mqtt", "hejhopp")}) assert device_entry2 is not None mqtt_mock.async_publish.assert_not_called() diff --git a/tests/components/netatmo/snapshots/test_init.ambr b/tests/components/netatmo/snapshots/test_init.ambr index 8f4b357fc5f..38a54f507a0 100644 --- a/tests/components/netatmo/snapshots/test_init.ambr +++ b/tests/components/netatmo/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'Roller Shutter', 'name': 'Entrance Blinds', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -53,6 +54,7 @@ 'model': 'Orientable Shutter', 'name': 'Bubendorff blind', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -83,6 +85,7 @@ 'model': '2 wire light switch/dimmer', 'name': 'Unknown 00:11:22:33:00:11:45:fe', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -113,6 +116,7 @@ 'model': 'Smarther with Netatmo', 'name': 'Corridor', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': 'Corridor', 'sw_version': None, @@ -143,6 +147,7 @@ 'model': 'Connected Energy Meter', 'name': 'Consumption meter', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -173,6 +178,7 @@ 'model': 'Light switch/dimmer with neutral', 'name': 'Bathroom light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -203,6 +209,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -233,6 +240,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 2', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -263,6 +271,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 3', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -293,6 +302,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 4', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -323,6 +333,7 @@ 'model': 'Connected Ecometer', 'name': 'Line 5', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -353,6 +364,7 @@ 'model': 'Connected Ecometer', 'name': 'Total', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -383,6 +395,7 @@ 'model': 'Connected Ecometer', 'name': 'Gas', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -413,6 +426,7 @@ 'model': 'Connected Ecometer', 'name': 'Hot water', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -443,6 +457,7 @@ 'model': 'Connected Ecometer', 'name': 'Cold water', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -473,6 +488,7 @@ 'model': 'Connected Ecometer', 'name': 'Écocompteur', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -503,6 +519,7 @@ 'model': 'Smart Indoor Camera', 'name': 'Hall', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -533,6 +550,7 @@ 'model': 'Smart Anemometer', 'name': 'Villa Garden', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -563,6 +581,7 @@ 'model': 'Smart Outdoor Camera', 'name': 'Front', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -593,6 +612,7 @@ 'model': 'Smart Video Doorbell', 'name': 'Netatmo-Doorbell', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -623,6 +643,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Kitchen', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -653,6 +674,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Livingroom', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -683,6 +705,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Baby Bedroom', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -713,6 +736,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Bedroom', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -743,6 +767,7 @@ 'model': 'Smart Indoor Air Quality Monitor', 'name': 'Parents Bedroom', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -773,6 +798,7 @@ 'model': 'Plug', 'name': 'Prise', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -803,6 +829,7 @@ 'model': 'Smart Outdoor Module', 'name': 'Villa Outdoor', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -833,6 +860,7 @@ 'model': 'Smart Indoor Module', 'name': 'Villa Bedroom', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -863,6 +891,7 @@ 'model': 'Smart Indoor Module', 'name': 'Villa Bathroom', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -893,6 +922,7 @@ 'model': 'Smart Home Weather station', 'name': 'Villa', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -923,6 +953,7 @@ 'model': 'Smart Rain Gauge', 'name': 'Villa Rain', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -953,6 +984,7 @@ 'model': 'OpenTherm Modulating Thermostat', 'name': 'Bureau Modulate', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': 'Bureau', 'sw_version': None, @@ -983,6 +1015,7 @@ 'model': 'Smart Thermostat', 'name': 'Livingroom', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': 'Livingroom', 'sw_version': None, @@ -1013,6 +1046,7 @@ 'model': 'Smart Valve', 'name': 'Valve1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': 'Entrada', 'sw_version': None, @@ -1043,6 +1077,7 @@ 'model': 'Smart Valve', 'name': 'Valve2', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': 'Cocina', 'sw_version': None, @@ -1073,6 +1108,7 @@ 'model': 'Climate', 'name': 'MYHOME', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1103,6 +1139,7 @@ 'model': 'Public Weather station', 'name': 'Home avg', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1133,6 +1170,7 @@ 'model': 'Public Weather station', 'name': 'Home max', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/netgear_lte/snapshots/test_init.ambr b/tests/components/netgear_lte/snapshots/test_init.ambr index 8af22f98e02..e893d36a06e 100644 --- a/tests/components/netgear_lte/snapshots/test_init.ambr +++ b/tests/components/netgear_lte/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'LM1200', 'name': 'Netgear LM1200', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'FFFFFFFFFFFFF', 'suggested_area': None, 'sw_version': 'EC25AFFDR07A09M4G', diff --git a/tests/components/ondilo_ico/snapshots/test_init.ambr b/tests/components/ondilo_ico/snapshots/test_init.ambr index c488b1e3c15..355c5902722 100644 --- a/tests/components/ondilo_ico/snapshots/test_init.ambr +++ b/tests/components/ondilo_ico/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'ICO', 'name': 'Pool 1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.7.1-stable', @@ -53,6 +54,7 @@ 'model': 'ICO', 'name': 'Pool 2', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.7.1-stable', diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index 999794ec20d..b3d330291ab 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -36,6 +36,7 @@ 'model': 'DS2405', 'name': '05.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -76,6 +77,7 @@ 'model': 'DS18S20', 'name': '10.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -116,6 +118,7 @@ 'model': 'DS2406', 'name': '12.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -256,6 +259,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -296,6 +300,7 @@ 'model': 'DS2409', 'name': '1F.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -324,6 +329,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -364,6 +370,7 @@ 'model': 'DS1822', 'name': '22.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -404,6 +411,7 @@ 'model': 'DS2438', 'name': '26.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -444,6 +452,7 @@ 'model': 'DS18B20', 'name': '28.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -484,6 +493,7 @@ 'model': 'DS18B20', 'name': '28.222222222222', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -524,6 +534,7 @@ 'model': 'DS18B20', 'name': '28.222222222223', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -564,6 +575,7 @@ 'model': 'DS2408', 'name': '29.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -956,6 +968,7 @@ 'model': 'DS2760', 'name': '30.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -996,6 +1009,7 @@ 'model': 'DS2413', 'name': '3A.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1124,6 +1138,7 @@ 'model': 'DS1825', 'name': '3B.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1164,6 +1179,7 @@ 'model': 'DS28EA00', 'name': '42.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1204,6 +1220,7 @@ 'model': 'EDS0068', 'name': '7E.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1244,6 +1261,7 @@ 'model': 'EDS0066', 'name': '7E.222222222222', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1284,6 +1302,7 @@ 'model': 'DS2438', 'name': 'A6.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1324,6 +1343,7 @@ 'model': 'HobbyBoards_EF', 'name': 'EF.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1364,6 +1384,7 @@ 'model': 'HB_MOISTURE_METER', 'name': 'EF.111111111112', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1404,6 +1425,7 @@ 'model': 'HB_HUB', 'name': 'EF.111111111113', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr index 59ed167197d..acf9ea6a8c8 100644 --- a/tests/components/onewire/snapshots/test_sensor.ambr +++ b/tests/components/onewire/snapshots/test_sensor.ambr @@ -36,6 +36,7 @@ 'model': 'DS2405', 'name': '05.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -76,6 +77,7 @@ 'model': 'DS18S20', 'name': '10.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -165,6 +167,7 @@ 'model': 'DS2406', 'name': '12.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -315,6 +318,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -451,6 +455,7 @@ 'model': 'DS2409', 'name': '1F.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -479,6 +484,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -615,6 +621,7 @@ 'model': 'DS1822', 'name': '22.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -704,6 +711,7 @@ 'model': 'DS2438', 'name': '26.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1283,6 +1291,7 @@ 'model': 'DS18B20', 'name': '28.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1372,6 +1381,7 @@ 'model': 'DS18B20', 'name': '28.222222222222', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1461,6 +1471,7 @@ 'model': 'DS18B20', 'name': '28.222222222223', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1550,6 +1561,7 @@ 'model': 'DS2408', 'name': '29.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1590,6 +1602,7 @@ 'model': 'DS2760', 'name': '30.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1826,6 +1839,7 @@ 'model': 'DS2413', 'name': '3A.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1866,6 +1880,7 @@ 'model': 'DS1825', 'name': '3B.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1955,6 +1970,7 @@ 'model': 'DS28EA00', 'name': '42.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2044,6 +2060,7 @@ 'model': 'EDS0068', 'name': '7E.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2280,6 +2297,7 @@ 'model': 'EDS0066', 'name': '7E.222222222222', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2418,6 +2436,7 @@ 'model': 'DS2438', 'name': 'A6.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2997,6 +3016,7 @@ 'model': 'HobbyBoards_EF', 'name': 'EF.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -3184,6 +3204,7 @@ 'model': 'HB_MOISTURE_METER', 'name': 'EF.111111111112', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -3420,6 +3441,7 @@ 'model': 'HB_HUB', 'name': 'EF.111111111113', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index 8fd1e2aeef6..d6cbb6f3fef 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -36,6 +36,7 @@ 'model': 'DS2405', 'name': '05.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -120,6 +121,7 @@ 'model': 'DS18S20', 'name': '10.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -160,6 +162,7 @@ 'model': 'DS2406', 'name': '12.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -388,6 +391,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -428,6 +432,7 @@ 'model': 'DS2409', 'name': '1F.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -456,6 +461,7 @@ 'model': 'DS2423', 'name': '1D.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -496,6 +502,7 @@ 'model': 'DS1822', 'name': '22.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -536,6 +543,7 @@ 'model': 'DS2438', 'name': '26.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -620,6 +628,7 @@ 'model': 'DS18B20', 'name': '28.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -660,6 +669,7 @@ 'model': 'DS18B20', 'name': '28.222222222222', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -700,6 +710,7 @@ 'model': 'DS18B20', 'name': '28.222222222223', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -740,6 +751,7 @@ 'model': 'DS2408', 'name': '29.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1484,6 +1496,7 @@ 'model': 'DS2760', 'name': '30.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1524,6 +1537,7 @@ 'model': 'DS2413', 'name': '3A.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1652,6 +1666,7 @@ 'model': 'DS1825', 'name': '3B.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1692,6 +1707,7 @@ 'model': 'DS28EA00', 'name': '42.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1732,6 +1748,7 @@ 'model': 'EDS0068', 'name': '7E.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1772,6 +1789,7 @@ 'model': 'EDS0066', 'name': '7E.222222222222', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1812,6 +1830,7 @@ 'model': 'DS2438', 'name': 'A6.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1896,6 +1915,7 @@ 'model': 'HobbyBoards_EF', 'name': 'EF.111111111111', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1936,6 +1956,7 @@ 'model': 'HB_MOISTURE_METER', 'name': 'EF.111111111112', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -2328,6 +2349,7 @@ 'model': 'HB_HUB', 'name': 'EF.111111111113', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/renault/snapshots/test_binary_sensor.ambr b/tests/components/renault/snapshots/test_binary_sensor.ambr index 7f30faac38e..8f49d7ef761 100644 --- a/tests/components/renault/snapshots/test_binary_sensor.ambr +++ b/tests/components/renault/snapshots/test_binary_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -322,6 +323,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -706,6 +708,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -874,6 +877,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -1300,6 +1304,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -1598,6 +1603,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -1982,6 +1988,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -2150,6 +2157,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_button.ambr b/tests/components/renault/snapshots/test_button.ambr index daef84b5c0a..7fa37319b2e 100644 --- a/tests/components/renault/snapshots/test_button.ambr +++ b/tests/components/renault/snapshots/test_button.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -106,6 +107,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -272,6 +274,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -438,6 +441,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -604,6 +608,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -686,6 +691,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -852,6 +858,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -1018,6 +1025,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_device_tracker.ambr b/tests/components/renault/snapshots/test_device_tracker.ambr index 8fe1713dc0b..61232d0268d 100644 --- a/tests/components/renault/snapshots/test_device_tracker.ambr +++ b/tests/components/renault/snapshots/test_device_tracker.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -107,6 +108,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -190,6 +192,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -230,6 +233,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -313,6 +317,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -399,6 +404,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -485,6 +491,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -525,6 +532,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_select.ambr b/tests/components/renault/snapshots/test_select.ambr index 0722cb5cab3..30181fd3b9c 100644 --- a/tests/components/renault/snapshots/test_select.ambr +++ b/tests/components/renault/snapshots/test_select.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -64,6 +65,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -159,6 +161,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -254,6 +257,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -349,6 +353,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -389,6 +394,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -484,6 +490,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -579,6 +586,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/renault/snapshots/test_sensor.ambr b/tests/components/renault/snapshots/test_sensor.ambr index 5909c66bc5c..1ae033101d4 100644 --- a/tests/components/renault/snapshots/test_sensor.ambr +++ b/tests/components/renault/snapshots/test_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -332,6 +333,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -1085,6 +1087,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -1834,6 +1837,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', @@ -2626,6 +2630,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -2934,6 +2939,7 @@ 'model': 'Captur ii', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'XJB1SU', @@ -3687,6 +3693,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X101VE', @@ -4436,6 +4443,7 @@ 'model': 'Zoe', 'name': 'REG-NUMBER', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'X102VE', diff --git a/tests/components/rova/snapshots/test_init.ambr b/tests/components/rova/snapshots/test_init.ambr index 340b0e6d472..ffb08ee082e 100644 --- a/tests/components/rova/snapshots/test_init.ambr +++ b/tests/components/rova/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': None, 'name': '8381BE 13', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr index 7422c1395c3..f14ec98a418 100644 --- a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', @@ -150,6 +151,7 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', diff --git a/tests/components/sfr_box/snapshots/test_button.ambr b/tests/components/sfr_box/snapshots/test_button.ambr index 0dfbf187f6d..eee419bf373 100644 --- a/tests/components/sfr_box/snapshots/test_button.ambr +++ b/tests/components/sfr_box/snapshots/test_button.ambr @@ -24,6 +24,7 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', diff --git a/tests/components/sfr_box/snapshots/test_sensor.ambr b/tests/components/sfr_box/snapshots/test_sensor.ambr index 0f39eed9e60..649c94c89dc 100644 --- a/tests/components/sfr_box/snapshots/test_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'NB6VAC-FXC-r0', 'name': 'SFR Box', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': 'NB6VAC-MAIN-R4.0.44k', diff --git a/tests/components/tailwind/snapshots/test_binary_sensor.ambr b/tests/components/tailwind/snapshots/test_binary_sensor.ambr index ea2a539363d..20a3282db55 100644 --- a/tests/components/tailwind/snapshots/test_binary_sensor.ambr +++ b/tests/components/tailwind/snapshots/test_binary_sensor.ambr @@ -70,6 +70,7 @@ 'model': 'iQ3', 'name': 'Door 1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', @@ -147,6 +148,7 @@ 'model': 'iQ3', 'name': 'Door 2', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tailwind/snapshots/test_button.ambr b/tests/components/tailwind/snapshots/test_button.ambr index 560d3fe692c..3ddbbb3f81d 100644 --- a/tests/components/tailwind/snapshots/test_button.ambr +++ b/tests/components/tailwind/snapshots/test_button.ambr @@ -74,6 +74,7 @@ 'model': 'iQ3', 'name': 'Tailwind iQ3', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tailwind/snapshots/test_cover.ambr b/tests/components/tailwind/snapshots/test_cover.ambr index 0ecd172b2ca..4ac6d6adc7d 100644 --- a/tests/components/tailwind/snapshots/test_cover.ambr +++ b/tests/components/tailwind/snapshots/test_cover.ambr @@ -71,6 +71,7 @@ 'model': 'iQ3', 'name': 'Door 1', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', @@ -149,6 +150,7 @@ 'model': 'iQ3', 'name': 'Door 2', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tailwind/snapshots/test_number.ambr b/tests/components/tailwind/snapshots/test_number.ambr index cbd61d31a6c..b4e73f4b2aa 100644 --- a/tests/components/tailwind/snapshots/test_number.ambr +++ b/tests/components/tailwind/snapshots/test_number.ambr @@ -83,6 +83,7 @@ 'model': 'iQ3', 'name': 'Tailwind iQ3', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '10.10', diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 5405e6c417d..91832f1f2f0 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -340,7 +340,7 @@ async def test_device_remove_multiple_config_entries_1( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - assert device_entry.config_entries == [tasmota_entry.entry_id, mock_entry.entry_id] + assert device_entry.config_entries == {tasmota_entry.entry_id, mock_entry.entry_id} async_fire_mqtt_message( hass, @@ -354,7 +354,7 @@ async def test_device_remove_multiple_config_entries_1( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - assert device_entry.config_entries == [mock_entry.entry_id] + assert device_entry.config_entries == {mock_entry.entry_id} async def test_device_remove_multiple_config_entries_2( @@ -396,7 +396,7 @@ async def test_device_remove_multiple_config_entries_2( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - assert device_entry.config_entries == [tasmota_entry.entry_id, mock_entry.entry_id] + assert device_entry.config_entries == {tasmota_entry.entry_id, mock_entry.entry_id} assert other_device_entry.id != device_entry.id # Remove other config entry from the device @@ -410,7 +410,7 @@ async def test_device_remove_multiple_config_entries_2( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - assert device_entry.config_entries == [tasmota_entry.entry_id] + assert device_entry.config_entries == {tasmota_entry.entry_id} mqtt_mock.async_publish.assert_not_called() # Remove other config entry from the other device - Tasmota should not do any cleanup diff --git a/tests/components/tedee/snapshots/test_init.ambr b/tests/components/tedee/snapshots/test_init.ambr index 83ab032dfb4..c91fb3ca484 100644 --- a/tests/components/tedee/snapshots/test_init.ambr +++ b/tests/components/tedee/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'Bridge', 'name': 'Bridge-AB1C', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': '0000-0000', 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/tedee/snapshots/test_lock.ambr b/tests/components/tedee/snapshots/test_lock.ambr index 8e4fc464479..8fa8ab7668d 100644 --- a/tests/components/tedee/snapshots/test_lock.ambr +++ b/tests/components/tedee/snapshots/test_lock.ambr @@ -70,6 +70,7 @@ 'model': 'Tedee PRO', 'name': 'Lock-1A2B', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -147,6 +148,7 @@ 'model': 'Tedee GO', 'name': 'Lock-2C3D', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/teslemetry/snapshots/test_init.ambr b/tests/components/teslemetry/snapshots/test_init.ambr index 951e4557bdd..e5dd23ada6e 100644 --- a/tests/components/teslemetry/snapshots/test_init.ambr +++ b/tests/components/teslemetry/snapshots/test_init.ambr @@ -23,6 +23,7 @@ 'model': 'Powerwall 2, Tesla Backup Gateway 2', 'name': 'Energy Site', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': '123456', 'suggested_area': None, 'sw_version': None, @@ -53,6 +54,7 @@ 'model': 'Model X', 'name': 'Test', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': 'LRWXF7EK4KC700000', 'suggested_area': None, 'sw_version': None, @@ -83,6 +85,7 @@ 'model': 'Gen 3 Wall Connector', 'name': 'Wall Connector', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': '123', 'suggested_area': None, 'sw_version': None, @@ -113,6 +116,7 @@ 'model': 'Gen 3 Wall Connector', 'name': 'Wall Connector', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': '234', 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/tplink/snapshots/test_binary_sensor.ambr b/tests/components/tplink/snapshots/test_binary_sensor.ambr index 27b1372df27..b45494d1001 100644 --- a/tests/components/tplink/snapshots/test_binary_sensor.ambr +++ b/tests/components/tplink/snapshots/test_binary_sensor.ambr @@ -361,6 +361,7 @@ 'model': 'HS100', 'name': 'my_device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/tplink/snapshots/test_button.ambr b/tests/components/tplink/snapshots/test_button.ambr index f26829101f7..0167256877d 100644 --- a/tests/components/tplink/snapshots/test_button.ambr +++ b/tests/components/tplink/snapshots/test_button.ambr @@ -119,6 +119,7 @@ 'model': 'HS100', 'name': 'my_device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/tplink/snapshots/test_climate.ambr b/tests/components/tplink/snapshots/test_climate.ambr index d30f8cd3532..4bdfe52b9b1 100644 --- a/tests/components/tplink/snapshots/test_climate.ambr +++ b/tests/components/tplink/snapshots/test_climate.ambr @@ -86,6 +86,7 @@ 'model': 'HS100', 'name': 'thermostat', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/tplink/snapshots/test_fan.ambr b/tests/components/tplink/snapshots/test_fan.ambr index d692abdce03..0a51909affe 100644 --- a/tests/components/tplink/snapshots/test_fan.ambr +++ b/tests/components/tplink/snapshots/test_fan.ambr @@ -186,6 +186,7 @@ 'model': 'HS100', 'name': 'my_device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/tplink/snapshots/test_number.ambr b/tests/components/tplink/snapshots/test_number.ambr index 9bfc9c0126a..8cda0a728b3 100644 --- a/tests/components/tplink/snapshots/test_number.ambr +++ b/tests/components/tplink/snapshots/test_number.ambr @@ -27,6 +27,7 @@ 'model': 'HS100', 'name': 'my_device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/tplink/snapshots/test_select.ambr b/tests/components/tplink/snapshots/test_select.ambr index 2cf02415238..555b0eb74d1 100644 --- a/tests/components/tplink/snapshots/test_select.ambr +++ b/tests/components/tplink/snapshots/test_select.ambr @@ -27,6 +27,7 @@ 'model': 'HS100', 'name': 'my_device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index cd8980bf57f..46fe897500f 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'model': 'HS100', 'name': 'my_device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/tplink/snapshots/test_switch.ambr b/tests/components/tplink/snapshots/test_switch.ambr index 2fe1f6e6b08..65eead6ddf4 100644 --- a/tests/components/tplink/snapshots/test_switch.ambr +++ b/tests/components/tplink/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'model': 'HS100', 'name': 'my_device', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', diff --git a/tests/components/twentemilieu/snapshots/test_calendar.ambr b/tests/components/twentemilieu/snapshots/test_calendar.ambr index 78b2d56afca..e6de21fdca1 100644 --- a/tests/components/twentemilieu/snapshots/test_calendar.ambr +++ b/tests/components/twentemilieu/snapshots/test_calendar.ambr @@ -101,6 +101,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/twentemilieu/snapshots/test_sensor.ambr b/tests/components/twentemilieu/snapshots/test_sensor.ambr index a0f3b75da57..22dcb0331cd 100644 --- a/tests/components/twentemilieu/snapshots/test_sensor.ambr +++ b/tests/components/twentemilieu/snapshots/test_sensor.ambr @@ -70,6 +70,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -147,6 +148,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -224,6 +226,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -301,6 +304,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -378,6 +382,7 @@ 'model': None, 'name': 'Twente Milieu', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/uptime/snapshots/test_sensor.ambr b/tests/components/uptime/snapshots/test_sensor.ambr index 0e7ae6dceaa..92baf939eb3 100644 --- a/tests/components/uptime/snapshots/test_sensor.ambr +++ b/tests/components/uptime/snapshots/test_sensor.ambr @@ -63,6 +63,7 @@ 'model': None, 'name': 'Uptime', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 59304e92d9d..a9210447f1e 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -24,6 +24,7 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -114,6 +115,7 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -209,6 +211,7 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -306,6 +309,7 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -403,6 +407,7 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -439,6 +444,7 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -491,6 +497,7 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -527,6 +534,7 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -563,6 +571,7 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_light.ambr b/tests/components/vesync/snapshots/test_light.ambr index 9990395a36c..c2c9854fa9f 100644 --- a/tests/components/vesync/snapshots/test_light.ambr +++ b/tests/components/vesync/snapshots/test_light.ambr @@ -24,6 +24,7 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -60,6 +61,7 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -96,6 +98,7 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -132,6 +135,7 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -168,6 +172,7 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -256,6 +261,7 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -362,6 +368,7 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -398,6 +405,7 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -501,6 +509,7 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index 268718fb2fe..97013b4e9ce 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -24,6 +24,7 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -152,6 +153,7 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -236,6 +238,7 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -413,6 +416,7 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -590,6 +594,7 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -626,6 +631,7 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -678,6 +684,7 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1008,6 +1015,7 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -1044,6 +1052,7 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/vesync/snapshots/test_switch.ambr b/tests/components/vesync/snapshots/test_switch.ambr index 3df26f74bcf..86b3b0ff5cd 100644 --- a/tests/components/vesync/snapshots/test_switch.ambr +++ b/tests/components/vesync/snapshots/test_switch.ambr @@ -24,6 +24,7 @@ 'model': 'LV-PUR131S', 'name': 'Air Purifier 131s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -60,6 +61,7 @@ 'model': 'Core200S', 'name': 'Air Purifier 200s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -96,6 +98,7 @@ 'model': 'LAP-C401S-WJP', 'name': 'Air Purifier 400s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -132,6 +135,7 @@ 'model': 'LAP-C601S-WUS', 'name': 'Air Purifier 600s', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -168,6 +172,7 @@ 'model': 'ESL100', 'name': 'Dimmable Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -204,6 +209,7 @@ 'model': 'ESWD16', 'name': 'Dimmer Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -256,6 +262,7 @@ 'model': 'wifi-switch-1.3', 'name': 'Outlet', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -336,6 +343,7 @@ 'model': 'ESL100CW', 'name': 'Temperature Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -372,6 +380,7 @@ 'model': 'ESWL01', 'name': 'Wall Switch', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/whois/snapshots/test_sensor.ambr b/tests/components/whois/snapshots/test_sensor.ambr index 61762c36e59..9bc125f204b 100644 --- a/tests/components/whois/snapshots/test_sensor.ambr +++ b/tests/components/whois/snapshots/test_sensor.ambr @@ -69,6 +69,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -146,6 +147,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -227,6 +229,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -304,6 +307,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -381,6 +385,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -457,6 +462,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -533,6 +539,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -609,6 +616,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -685,6 +693,7 @@ 'model': None, 'name': 'home-assistant.io', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/wled/snapshots/test_button.ambr b/tests/components/wled/snapshots/test_button.ambr index b489bcc0a71..9c91c0e0050 100644 --- a/tests/components/wled/snapshots/test_button.ambr +++ b/tests/components/wled/snapshots/test_button.ambr @@ -74,6 +74,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', diff --git a/tests/components/wled/snapshots/test_number.ambr b/tests/components/wled/snapshots/test_number.ambr index c3440108148..bee3e180090 100644 --- a/tests/components/wled/snapshots/test_number.ambr +++ b/tests/components/wled/snapshots/test_number.ambr @@ -82,6 +82,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -171,6 +172,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', diff --git a/tests/components/wled/snapshots/test_select.ambr b/tests/components/wled/snapshots/test_select.ambr index 6d64ec43658..f6447f699c9 100644 --- a/tests/components/wled/snapshots/test_select.ambr +++ b/tests/components/wled/snapshots/test_select.ambr @@ -84,6 +84,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -269,6 +270,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -358,6 +360,7 @@ 'model': 'DIY light', 'name': 'WLED RGBW Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.6b4', @@ -447,6 +450,7 @@ 'model': 'DIY light', 'name': 'WLED RGBW Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.6b4', diff --git a/tests/components/wled/snapshots/test_switch.ambr b/tests/components/wled/snapshots/test_switch.ambr index da69e686f07..6bca0a2ed3b 100644 --- a/tests/components/wled/snapshots/test_switch.ambr +++ b/tests/components/wled/snapshots/test_switch.ambr @@ -76,6 +76,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -156,6 +157,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -237,6 +239,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', @@ -318,6 +321,7 @@ 'model': 'DIY light', 'name': 'WLED RGB Light', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': '0.8.5', diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index f8f10baad08..fa57cc7557e 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -90,7 +90,7 @@ async def test_get_or_create_returns_same_entry( await hass.async_block_till_done() # Only 2 update events. The third entry did not generate any changes. - assert len(update_events) == 2, update_events + assert len(update_events) == 2 assert update_events[0].data == { "action": "create", "device_id": entry.id, @@ -170,9 +170,10 @@ async def test_multiple_config_entries( assert len(device_registry.devices) == 1 assert entry.id == entry2.id assert entry.id == entry3.id - assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id] - # the 3rd get_or_create was a primary update, so that's now first config entry - assert entry3.config_entries == [config_entry_1.entry_id, config_entry_2.entry_id] + assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry2.primary_config_entry == config_entry_1.entry_id + assert entry3.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry3.primary_config_entry == config_entry_1.entry_id @pytest.mark.parametrize("load_registries", [False]) @@ -202,6 +203,7 @@ async def test_loading_from_storage( "model": "model", "name_by_user": "Test Friendly Name", "name": "name", + "primary_config_entry": mock_config_entry.entry_id, "serial_number": "serial_no", "sw_version": "version", "via_device_id": None, @@ -233,7 +235,7 @@ async def test_loading_from_storage( ) assert entry == dr.DeviceEntry( area_id="12345A", - config_entries=[mock_config_entry.entry_id], + config_entries={mock_config_entry.entry_id}, configuration_url="https://example.com/config", connections={("Zigbee", "01.23.45.67.89")}, disabled_by=dr.DeviceEntryDisabler.USER, @@ -246,11 +248,12 @@ async def test_loading_from_storage( model="model", name_by_user="Test Friendly Name", name="name", + primary_config_entry=mock_config_entry.entry_id, serial_number="serial_no", suggested_area=None, # Not stored sw_version="version", ) - assert isinstance(entry.config_entries, list) + assert isinstance(entry.config_entries, set) assert isinstance(entry.connections, set) assert isinstance(entry.identifiers, set) @@ -263,26 +266,27 @@ async def test_loading_from_storage( model="model", ) assert entry == dr.DeviceEntry( - config_entries=[mock_config_entry.entry_id], + config_entries={mock_config_entry.entry_id}, connections={("Zigbee", "23.45.67.89.01")}, id="bcdefghijklmn", identifiers={("serial", "3456ABCDEF12")}, manufacturer="manufacturer", model="model", + primary_config_entry=mock_config_entry.entry_id, ) assert entry.id == "bcdefghijklmn" - assert isinstance(entry.config_entries, list) + assert isinstance(entry.config_entries, set) assert isinstance(entry.connections, set) assert isinstance(entry.identifiers, set) @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_1_to_1_5( +async def test_migration_1_1_to_1_6( hass: HomeAssistant, hass_storage: dict[str, Any], mock_config_entry: MockConfigEntry, ) -> None: - """Test migration from version 1.1 to 1.5.""" + """Test migration from version 1.1 to 1.6.""" hass_storage[dr.STORAGE_KEY] = { "version": 1, "minor_version": 1, @@ -371,6 +375,7 @@ async def test_migration_1_1_to_1_5( "model": "model", "name": "name", "name_by_user": None, + "primary_config_entry": mock_config_entry.entry_id, "serial_number": None, "sw_version": "new_version", "via_device_id": None, @@ -390,6 +395,7 @@ async def test_migration_1_1_to_1_5( "model": None, "name_by_user": None, "name": None, + "primary_config_entry": None, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -409,12 +415,12 @@ async def test_migration_1_1_to_1_5( @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_2_to_1_5( +async def test_migration_1_2_to_1_6( hass: HomeAssistant, hass_storage: dict[str, Any], mock_config_entry: MockConfigEntry, ) -> None: - """Test migration from version 1.2 to 1.5.""" + """Test migration from version 1.2 to 1.6.""" hass_storage[dr.STORAGE_KEY] = { "version": 1, "minor_version": 2, @@ -502,6 +508,7 @@ async def test_migration_1_2_to_1_5( "model": "model", "name": "name", "name_by_user": None, + "primary_config_entry": mock_config_entry.entry_id, "serial_number": None, "sw_version": "new_version", "via_device_id": None, @@ -521,6 +528,7 @@ async def test_migration_1_2_to_1_5( "model": None, "name_by_user": None, "name": None, + "primary_config_entry": None, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -532,12 +540,12 @@ async def test_migration_1_2_to_1_5( @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_3_to_1_5( +async def test_migration_1_3_to_1_6( hass: HomeAssistant, hass_storage: dict[str, Any], mock_config_entry: MockConfigEntry, ) -> None: - """Test migration from version 1.3 to 1.5.""" + """Test migration from version 1.3 to 1.6.""" hass_storage[dr.STORAGE_KEY] = { "version": 1, "minor_version": 3, @@ -627,6 +635,7 @@ async def test_migration_1_3_to_1_5( "model": "model", "name": "name", "name_by_user": None, + "primary_config_entry": mock_config_entry.entry_id, "serial_number": None, "sw_version": "new_version", "via_device_id": None, @@ -644,8 +653,9 @@ async def test_migration_1_3_to_1_5( "labels": [], "manufacturer": None, "model": None, - "name_by_user": None, "name": None, + "name_by_user": None, + "primary_config_entry": None, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -657,12 +667,12 @@ async def test_migration_1_3_to_1_5( @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_4_to_1_5( +async def test_migration_1_4_to_1_6( hass: HomeAssistant, hass_storage: dict[str, Any], mock_config_entry: MockConfigEntry, ) -> None: - """Test migration from version 1.4 to 1.5.""" + """Test migration from version 1.4 to 1.6.""" hass_storage[dr.STORAGE_KEY] = { "version": 1, "minor_version": 4, @@ -754,6 +764,7 @@ async def test_migration_1_4_to_1_5( "model": "model", "name": "name", "name_by_user": None, + "primary_config_entry": mock_config_entry.entry_id, "serial_number": None, "sw_version": "new_version", "via_device_id": None, @@ -773,6 +784,138 @@ async def test_migration_1_4_to_1_5( "model": None, "name_by_user": None, "name": None, + "primary_config_entry": None, + "serial_number": None, + "sw_version": None, + "via_device_id": None, + }, + ], + "deleted_devices": [], + }, + } + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_migration_1_5_to_1_6( + hass: HomeAssistant, + hass_storage: dict[str, Any], + mock_config_entry: MockConfigEntry, +) -> None: + """Test migration from version 1.5 to 1.6.""" + hass_storage[dr.STORAGE_KEY] = { + "version": 1, + "minor_version": 5, + "key": dr.STORAGE_KEY, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": [mock_config_entry.entry_id], + "configuration_url": None, + "connections": [["Zigbee", "01.23.45.67.89"]], + "disabled_by": None, + "entry_type": "service", + "hw_version": "hw_version", + "id": "abcdefghijklm", + "identifiers": [["serial", "123456ABCDEF"]], + "labels": ["blah"], + "manufacturer": "manufacturer", + "model": "model", + "name": "name", + "name_by_user": None, + "serial_number": None, + "sw_version": "new_version", + "via_device_id": None, + }, + { + "area_id": None, + "config_entries": [None], + "configuration_url": None, + "connections": [], + "disabled_by": None, + "entry_type": None, + "hw_version": None, + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "labels": ["blah"], + "manufacturer": None, + "model": None, + "name_by_user": None, + "name": None, + "serial_number": None, + "sw_version": None, + "via_device_id": None, + }, + ], + "deleted_devices": [], + }, + } + + await dr.async_load(hass) + registry = dr.async_get(hass) + + # Test data was loaded + entry = registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={("Zigbee", "01.23.45.67.89")}, + identifiers={("serial", "123456ABCDEF")}, + ) + assert entry.id == "abcdefghijklm" + + # Update to trigger a store + entry = registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={("Zigbee", "01.23.45.67.89")}, + identifiers={("serial", "123456ABCDEF")}, + sw_version="new_version", + ) + assert entry.id == "abcdefghijklm" + + # Check we store migrated data + await flush_store(registry._store) + + assert hass_storage[dr.STORAGE_KEY] == { + "version": dr.STORAGE_VERSION_MAJOR, + "minor_version": dr.STORAGE_VERSION_MINOR, + "key": dr.STORAGE_KEY, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": [mock_config_entry.entry_id], + "configuration_url": None, + "connections": [["Zigbee", "01.23.45.67.89"]], + "disabled_by": None, + "entry_type": "service", + "hw_version": "hw_version", + "id": "abcdefghijklm", + "identifiers": [["serial", "123456ABCDEF"]], + "labels": ["blah"], + "manufacturer": "manufacturer", + "model": "model", + "name": "name", + "name_by_user": None, + "primary_config_entry": mock_config_entry.entry_id, + "serial_number": None, + "sw_version": "new_version", + "via_device_id": None, + }, + { + "area_id": None, + "config_entries": [None], + "configuration_url": None, + "connections": [], + "disabled_by": None, + "entry_type": None, + "hw_version": None, + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "labels": ["blah"], + "manufacturer": None, + "model": None, + "name_by_user": None, + "name": None, + "primary_config_entry": None, "serial_number": None, "sw_version": None, "via_device_id": None, @@ -818,7 +961,7 @@ async def test_removing_config_entries( assert len(device_registry.devices) == 2 assert entry.id == entry2.id assert entry.id != entry3.id - assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id] + assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} device_registry.async_clear_config_entry(config_entry_1.entry_id) entry = device_registry.async_get_device(identifiers={("bridgeid", "0123")}) @@ -826,7 +969,7 @@ async def test_removing_config_entries( identifiers={("bridgeid", "4567")} ) - assert entry.config_entries == [config_entry_2.entry_id] + assert entry.config_entries == {config_entry_2.entry_id} assert entry3_removed is None await hass.async_block_till_done() @@ -839,7 +982,9 @@ async def test_removing_config_entries( assert update_events[1].data == { "action": "update", "device_id": entry.id, - "changes": {"config_entries": [config_entry_1.entry_id]}, + "changes": { + "config_entries": {config_entry_1.entry_id}, + }, } assert update_events[2].data == { "action": "create", @@ -849,7 +994,8 @@ async def test_removing_config_entries( "action": "update", "device_id": entry.id, "changes": { - "config_entries": [config_entry_2.entry_id, config_entry_1.entry_id] + "config_entries": {config_entry_1.entry_id, config_entry_2.entry_id}, + "primary_config_entry": config_entry_1.entry_id, }, } assert update_events[4].data == { @@ -894,7 +1040,7 @@ async def test_deleted_device_removing_config_entries( assert len(device_registry.deleted_devices) == 0 assert entry.id == entry2.id assert entry.id != entry3.id - assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id] + assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} device_registry.async_remove_device(entry.id) device_registry.async_remove_device(entry3.id) @@ -911,7 +1057,9 @@ async def test_deleted_device_removing_config_entries( assert update_events[1].data == { "action": "update", "device_id": entry2.id, - "changes": {"config_entries": [config_entry_1.entry_id]}, + "changes": { + "config_entries": {config_entry_1.entry_id}, + }, } assert update_events[2].data == { "action": "create", @@ -1290,7 +1438,7 @@ async def test_update( assert updated_entry != entry assert updated_entry == dr.DeviceEntry( area_id="12345A", - config_entries=[mock_config_entry.entry_id], + config_entries={mock_config_entry.entry_id}, configuration_url="https://example.com/config", connections={("mac", "65:43:21:fe:dc:ba")}, disabled_by=dr.DeviceEntryDisabler.USER, @@ -1473,6 +1621,8 @@ async def test_update_remove_config_entries( config_entry_1.add_to_hass(hass) config_entry_2 = MockConfigEntry() config_entry_2.add_to_hass(hass) + config_entry_3 = MockConfigEntry() + config_entry_3.add_to_hass(hass) entry = device_registry.async_get_or_create( config_entry_id=config_entry_1.entry_id, @@ -1495,20 +1645,34 @@ async def test_update_remove_config_entries( manufacturer="manufacturer", model="model", ) + entry4 = device_registry.async_update_device( + entry2.id, add_config_entry_id=config_entry_3.entry_id + ) + # Try to add an unknown config entry + with pytest.raises(HomeAssistantError): + device_registry.async_update_device(entry2.id, add_config_entry_id="blabla") assert len(device_registry.devices) == 2 - assert entry.id == entry2.id + assert entry.id == entry2.id == entry4.id assert entry.id != entry3.id - assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id] + assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry4.config_entries == { + config_entry_1.entry_id, + config_entry_2.entry_id, + config_entry_3.entry_id, + } - updated_entry = device_registry.async_update_device( + device_registry.async_update_device( entry2.id, remove_config_entry_id=config_entry_1.entry_id ) + updated_entry = device_registry.async_update_device( + entry2.id, remove_config_entry_id=config_entry_3.entry_id + ) removed_entry = device_registry.async_update_device( entry3.id, remove_config_entry_id=config_entry_1.entry_id ) - assert updated_entry.config_entries == [config_entry_2.entry_id] + assert updated_entry.config_entries == {config_entry_2.entry_id} assert removed_entry is None removed_entry = device_registry.async_get_device(identifiers={("bridgeid", "4567")}) @@ -1517,7 +1681,7 @@ async def test_update_remove_config_entries( await hass.async_block_till_done() - assert len(update_events) == 5 + assert len(update_events) == 7 assert update_events[0].data == { "action": "create", "device_id": entry.id, @@ -1525,7 +1689,9 @@ async def test_update_remove_config_entries( assert update_events[1].data == { "action": "update", "device_id": entry2.id, - "changes": {"config_entries": [config_entry_1.entry_id]}, + "changes": { + "config_entries": {config_entry_1.entry_id}, + }, } assert update_events[2].data == { "action": "create", @@ -1535,10 +1701,29 @@ async def test_update_remove_config_entries( "action": "update", "device_id": entry.id, "changes": { - "config_entries": [config_entry_2.entry_id, config_entry_1.entry_id] + "config_entries": {config_entry_1.entry_id, config_entry_2.entry_id} }, } assert update_events[4].data == { + "action": "update", + "device_id": entry2.id, + "changes": { + "config_entries": { + config_entry_1.entry_id, + config_entry_2.entry_id, + config_entry_3.entry_id, + }, + "primary_config_entry": config_entry_1.entry_id, + }, + } + assert update_events[5].data == { + "action": "update", + "device_id": entry2.id, + "changes": { + "config_entries": {config_entry_2.entry_id, config_entry_3.entry_id} + }, + } + assert update_events[6].data == { "action": "remove", "device_id": entry3.id, } @@ -1768,7 +1953,7 @@ async def test_restore_device( assert len(device_registry.devices) == 2 assert len(device_registry.deleted_devices) == 0 - assert isinstance(entry3.config_entries, list) + assert isinstance(entry3.config_entries, set) assert isinstance(entry3.connections, set) assert isinstance(entry3.identifiers, set) @@ -1900,7 +2085,7 @@ async def test_restore_shared_device( assert len(device_registry.devices) == 1 assert len(device_registry.deleted_devices) == 0 - assert isinstance(entry2.config_entries, list) + assert isinstance(entry2.config_entries, set) assert isinstance(entry2.connections, set) assert isinstance(entry2.identifiers, set) @@ -1918,7 +2103,7 @@ async def test_restore_shared_device( assert len(device_registry.devices) == 1 assert len(device_registry.deleted_devices) == 0 - assert isinstance(entry3.config_entries, list) + assert isinstance(entry3.config_entries, set) assert isinstance(entry3.connections, set) assert isinstance(entry3.identifiers, set) @@ -1934,7 +2119,7 @@ async def test_restore_shared_device( assert len(device_registry.devices) == 1 assert len(device_registry.deleted_devices) == 0 - assert isinstance(entry4.config_entries, list) + assert isinstance(entry4.config_entries, set) assert isinstance(entry4.connections, set) assert isinstance(entry4.identifiers, set) @@ -1949,7 +2134,7 @@ async def test_restore_shared_device( "action": "update", "device_id": entry.id, "changes": { - "config_entries": [config_entry_1.entry_id], + "config_entries": {config_entry_1.entry_id}, "identifiers": {("entry_123", "0123")}, }, } @@ -1973,7 +2158,7 @@ async def test_restore_shared_device( "action": "update", "device_id": entry.id, "changes": { - "config_entries": [config_entry_2.entry_id], + "config_entries": {config_entry_2.entry_id}, "identifiers": {("entry_234", "2345")}, }, } @@ -2291,6 +2476,7 @@ async def test_loading_invalid_configuration_url_from_storage( "model": None, "name_by_user": None, "name": None, + "primary_config_entry": "1234", "serial_number": None, "sw_version": None, "via_device_id": None, @@ -2794,3 +2980,75 @@ async def test_device_registry_identifiers_collision( device3_refetched = device_registry.async_get(device3.id) device1_refetched = device_registry.async_get(device1.id) assert not device1_refetched.identifiers.isdisjoint(device3_refetched.identifiers) + + +async def test_primary_config_entry( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, +) -> None: + """Test the primary integration field.""" + mock_config_entry_1 = MockConfigEntry(domain="mqtt", title=None) + mock_config_entry_1.add_to_hass(hass) + mock_config_entry_2 = MockConfigEntry(title=None) + mock_config_entry_2.add_to_hass(hass) + mock_config_entry_3 = MockConfigEntry(title=None) + mock_config_entry_3.add_to_hass(hass) + mock_config_entry_4 = MockConfigEntry(domain="matter", title=None) + mock_config_entry_4.add_to_hass(hass) + + # Create device without model name etc, config entry will not be marked primary + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_1.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers=set(), + ) + assert device.primary_config_entry is None + + # Set model, mqtt config entry will be promoted to primary + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_1.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + model="model", + ) + assert device.primary_config_entry == mock_config_entry_1.entry_id + + # New config entry with model will be promoted to primary + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_2.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + model="model 2", + ) + assert device.primary_config_entry == mock_config_entry_2.entry_id + + # New config entry with model will not be promoted to primary + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_3.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + model="model 3", + ) + assert device.primary_config_entry == mock_config_entry_2.entry_id + + # New matter config entry with model will not be promoted to primary + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_4.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + model="model 3", + ) + assert device.primary_config_entry == mock_config_entry_2.entry_id + + # Remove the primary config entry + device = device_registry.async_update_device( + device.id, + remove_config_entry_id=mock_config_entry_2.entry_id, + ) + assert device.primary_config_entry is None + + # Create new + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry_1.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers=set(), + manufacturer="manufacturer", + model="model", + ) + assert device.primary_config_entry == mock_config_entry_1.entry_id diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 68024bc936f..4e761a21e8c 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1187,6 +1187,7 @@ async def test_device_info_called( assert device.manufacturer == "test-manuf" assert device.model == "test-model" assert device.name == "test-name" + assert device.primary_config_entry == config_entry.entry_id assert device.suggested_area == "Heliport" assert device.sw_version == "test-sw" assert device.hw_version == "test-hw" diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 1390ef3889d..4dc8d79be3f 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1106,10 +1106,10 @@ async def test_remove_config_entry_from_device_removes_entities( config_entry_id=config_entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - assert device_entry.config_entries == [ + assert device_entry.config_entries == { config_entry_1.entry_id, config_entry_2.entry_id, - ] + } # Create one entity for each config entry entry_1 = entity_registry.async_get_or_create( @@ -1174,10 +1174,10 @@ async def test_remove_config_entry_from_device_removes_entities_2( config_entry_id=config_entry_2.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - assert device_entry.config_entries == [ + assert device_entry.config_entries == { config_entry_1.entry_id, config_entry_2.entry_id, - ] + } # Create one entity for each config entry entry_1 = entity_registry.async_get_or_create( diff --git a/tests/syrupy.py b/tests/syrupy.py index e5bbf017bb3..52bd5756798 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -159,6 +159,8 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): ) if serialized["via_device_id"] is not None: serialized["via_device_id"] = ANY + if serialized["primary_config_entry"] is not None: + serialized["primary_config_entry"] = ANY return serialized @classmethod From d5bcfe98221db55fdf1a20f7d1bb72f87a77fc1a Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:27:55 +0200 Subject: [PATCH 0223/2411] Improve BMW tests (#119171) Co-authored-by: Richard --- .../bmw_connected_drive/__init__.py | 5 +- .../bmw_connected_drive/test_init.py | 56 ++++++++++++++++++- .../bmw_connected_drive/test_sensor.py | 44 ++++++++++++++- 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 663003a5e4b..bd4e1cf7360 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -72,7 +72,10 @@ def _async_migrate_options_from_data_if_missing( options = dict(entry.options) if CONF_READ_ONLY in data or list(options) != list(DEFAULT_OPTIONS): - options = dict(DEFAULT_OPTIONS, **options) + options = dict( + DEFAULT_OPTIONS, + **{k: v for k, v in options.items() if k in DEFAULT_OPTIONS}, + ) options[CONF_READ_ONLY] = data.pop(CONF_READ_ONLY, False) hass.config_entries.async_update_entry(entry, data=data, options=options) diff --git a/tests/components/bmw_connected_drive/test_init.py b/tests/components/bmw_connected_drive/test_init.py index d648ad65f5d..52bc8a7ce05 100644 --- a/tests/components/bmw_connected_drive/test_init.py +++ b/tests/components/bmw_connected_drive/test_init.py @@ -4,7 +4,11 @@ from unittest.mock import patch import pytest -from homeassistant.components.bmw_connected_drive.const import DOMAIN as BMW_DOMAIN +from homeassistant.components.bmw_connected_drive import DEFAULT_OPTIONS +from homeassistant.components.bmw_connected_drive.const import ( + CONF_READ_ONLY, + DOMAIN as BMW_DOMAIN, +) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -18,6 +22,56 @@ VEHICLE_NAME = "i3 (+ REX)" VEHICLE_NAME_SLUG = "i3_rex" +@pytest.mark.usefixtures("bmw_fixture") +@pytest.mark.parametrize( + "options", + [ + DEFAULT_OPTIONS, + {"other_value": 1, **DEFAULT_OPTIONS}, + {}, + ], +) +async def test_migrate_options( + hass: HomeAssistant, + options: dict, +) -> None: + """Test successful migration of options.""" + + config_entry = FIXTURE_CONFIG_ENTRY.copy() + config_entry["options"] = options + + mock_config_entry = MockConfigEntry(**config_entry) + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert len( + hass.config_entries.async_get_entry(mock_config_entry.entry_id).options + ) == len(DEFAULT_OPTIONS) + + +@pytest.mark.usefixtures("bmw_fixture") +async def test_migrate_options_from_data(hass: HomeAssistant) -> None: + """Test successful migration of options.""" + + config_entry = FIXTURE_CONFIG_ENTRY.copy() + config_entry["options"] = {} + config_entry["data"].update({CONF_READ_ONLY: False}) + + mock_config_entry = MockConfigEntry(**config_entry) + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + updated_config_entry = hass.config_entries.async_get_entry( + mock_config_entry.entry_id + ) + assert len(updated_config_entry.options) == len(DEFAULT_OPTIONS) + assert CONF_READ_ONLY not in updated_config_entry.data + + @pytest.mark.parametrize( ("entitydata", "old_unique_id", "new_unique_id"), [ diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py index 6607bed280d..c02f6d425cd 100644 --- a/tests/components/bmw_connected_drive/test_sensor.py +++ b/tests/components/bmw_connected_drive/test_sensor.py @@ -2,13 +2,17 @@ from unittest.mock import patch +from bimmer_connected.models import StrEnum +from bimmer_connected.vehicle import fuel_and_battery +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.components.bmw_connected_drive.const import SCAN_INTERVALS from homeassistant.components.bmw_connected_drive.sensor import SENSOR_TYPES from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.const import Platform +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.translation import async_get_translations @@ -20,7 +24,7 @@ from homeassistant.util.unit_system import ( from . import setup_mocked_integration -from tests.common import snapshot_platform +from tests.common import async_fire_time_changed, snapshot_platform @pytest.mark.freeze_time("2023-06-22 10:30:00+00:00") @@ -107,3 +111,39 @@ async def test_entity_option_translations( } assert sensor_options == translation_states + + +@pytest.mark.usefixtures("bmw_fixture") +async def test_enum_sensor_unknown( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch, freezer: FrozenDateTimeFactory +) -> None: + """Test conversion handling of enum sensors.""" + + # Setup component + assert await setup_mocked_integration(hass) + + entity_id = "sensor.i4_edrive40_charging_status" + + # Check normal state + entity = hass.states.get(entity_id) + assert entity.state == "not_charging" + + class ChargingStateUnkown(StrEnum): + """Charging state of electric vehicle.""" + + UNKNOWN = "UNKNOWN" + + # Setup enum returning only UNKNOWN + monkeypatch.setattr( + fuel_and_battery, + "ChargingState", + ChargingStateUnkown, + ) + + freezer.tick(SCAN_INTERVALS["rest_of_world"]) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Check normal state + entity = hass.states.get("sensor.i4_edrive40_charging_status") + assert entity.state == STATE_UNAVAILABLE From be7a2c2cc29c88bbd4a889ff6d3e48b0223eaf26 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:40:41 +0200 Subject: [PATCH 0224/2411] Revert "Force alias when importing scene PLATFORM_SCHEMA" (#120540) Revert "Force alias when importing scene PLATFORM_SCHEMA (#120534)" This reverts commit 348ceca19f1fe5b45dadbbd7ec96093c64409a3f. --- homeassistant/components/config/scene.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index fa23d02bcc8..a2e2693036a 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -5,10 +5,7 @@ from __future__ import annotations from typing import Any import uuid -from homeassistant.components.scene import ( - DOMAIN, - PLATFORM_SCHEMA as SCENE_PLATFORM_SCHEMA, -) +from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA from homeassistant.config import SCENE_CONFIG_PATH from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback @@ -43,7 +40,7 @@ def async_setup(hass: HomeAssistant) -> bool: "config", SCENE_CONFIG_PATH, cv.string, - SCENE_PLATFORM_SCHEMA, + PLATFORM_SCHEMA, post_write_hook=hook, ) ) From 7ef1db054968e8765dbff61f41fefeab6caabaea Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 12:52:31 +0200 Subject: [PATCH 0225/2411] Fix release in MPD issue (#120545) --- homeassistant/components/mpd/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 0c4a2224e63..eb34fb6289f 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -92,7 +92,7 @@ async def async_setup_platform( hass, HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.12.0", + breaks_in_ha_version="2025.1.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, @@ -107,7 +107,7 @@ async def async_setup_platform( hass, DOMAIN, f"deprecated_yaml_import_issue_{result['reason']}", - breaks_in_ha_version="2024.12.0", + breaks_in_ha_version="2025.1.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, From 34e266762e56ba9ee6f0eb498bed1b7ba439ccfb Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 13:09:50 +0200 Subject: [PATCH 0226/2411] Remove unnecessary icon states in pyLoad integration (#120548) Remove unnecessary icon states --- homeassistant/components/pyload/icons.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/pyload/icons.json b/homeassistant/components/pyload/icons.json index 0e307a43e51..8bcc95c72d7 100644 --- a/homeassistant/components/pyload/icons.json +++ b/homeassistant/components/pyload/icons.json @@ -32,14 +32,12 @@ "download": { "default": "mdi:play", "state": { - "on": "mdi:play", "off": "mdi:pause" } }, "reconnect": { "default": "mdi:restart", "state": { - "on": "mdi:restart", "off": "mdi:restart-off" } } From e8a3e3c8dbef00c894e934cf5fa8ba0988085efd Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 26 Jun 2024 13:19:34 +0200 Subject: [PATCH 0227/2411] Fix airgradient select entities (#120549) --- .../components/airgradient/select.py | 69 +++++++++---------- .../airgradient/snapshots/test_select.ambr | 8 +-- 2 files changed, 37 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index c37df0483d1..a64ce596806 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -79,62 +79,59 @@ LED_BAR_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = ( ), ) -LEARNING_TIME_OFFSET_OPTIONS = { - 12: "12", - 60: "60", - 120: "120", - 360: "360", - 720: "720", -} -LEARNING_TIME_OFFSET_OPTIONS_INVERSE = { - v: k for k, v in LEARNING_TIME_OFFSET_OPTIONS.items() -} -ABC_DAYS = { - 8: "8", - 30: "30", - 90: "90", - 180: "180", - 0: "off", -} -ABC_DAYS_INVERSE = {v: k for k, v in ABC_DAYS.items()} +LEARNING_TIME_OFFSET_OPTIONS = [ + "12", + "60", + "120", + "360", + "720", +] + +ABC_DAYS = [ + "8", + "30", + "90", + "180", + "0", +] + + +def _get_value(value: int, values: list[str]) -> str | None: + str_value = str(value) + return str_value if str_value in values else None + CONTROL_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = ( AirGradientSelectEntityDescription( key="nox_index_learning_time_offset", translation_key="nox_index_learning_time_offset", - options=list(LEARNING_TIME_OFFSET_OPTIONS_INVERSE), + options=LEARNING_TIME_OFFSET_OPTIONS, entity_category=EntityCategory.CONFIG, - value_fn=lambda config: LEARNING_TIME_OFFSET_OPTIONS.get( - config.nox_learning_offset - ), - set_value_fn=lambda client, value: client.set_nox_learning_offset( - LEARNING_TIME_OFFSET_OPTIONS_INVERSE.get(value, 12) + value_fn=lambda config: _get_value( + config.nox_learning_offset, LEARNING_TIME_OFFSET_OPTIONS ), + set_value_fn=lambda client, value: client.set_nox_learning_offset(int(value)), ), AirGradientSelectEntityDescription( key="voc_index_learning_time_offset", translation_key="voc_index_learning_time_offset", - options=list(LEARNING_TIME_OFFSET_OPTIONS_INVERSE), + options=LEARNING_TIME_OFFSET_OPTIONS, entity_category=EntityCategory.CONFIG, - value_fn=lambda config: LEARNING_TIME_OFFSET_OPTIONS.get( - config.nox_learning_offset - ), - set_value_fn=lambda client, value: client.set_tvoc_learning_offset( - LEARNING_TIME_OFFSET_OPTIONS_INVERSE.get(value, 12) + value_fn=lambda config: _get_value( + config.tvoc_learning_offset, LEARNING_TIME_OFFSET_OPTIONS ), + set_value_fn=lambda client, value: client.set_tvoc_learning_offset(int(value)), ), AirGradientSelectEntityDescription( key="co2_automatic_baseline_calibration", translation_key="co2_automatic_baseline_calibration", - options=list(ABC_DAYS_INVERSE), + options=ABC_DAYS, entity_category=EntityCategory.CONFIG, - value_fn=lambda config: ABC_DAYS.get( - config.co2_automatic_baseline_calibration_days + value_fn=lambda config: _get_value( + config.co2_automatic_baseline_calibration_days, ABC_DAYS ), set_value_fn=lambda client, - value: client.set_co2_automatic_baseline_calibration( - ABC_DAYS_INVERSE.get(value, 0) - ), + value: client.set_co2_automatic_baseline_calibration(int(value)), ), ) diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index 19cdc2134fc..ece563b40c6 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -10,7 +10,7 @@ '30', '90', '180', - 'off', + '0', ]), }), 'config_entry_id': , @@ -49,7 +49,7 @@ '30', '90', '180', - 'off', + '0', ]), }), 'context': , @@ -415,7 +415,7 @@ '30', '90', '180', - 'off', + '0', ]), }), 'config_entry_id': , @@ -454,7 +454,7 @@ '30', '90', '180', - 'off', + '0', ]), }), 'context': , From f0590f08b131b4e6037506e170f02d27acbb230a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 26 Jun 2024 13:26:53 +0200 Subject: [PATCH 0228/2411] Update frontend to 20240626.0 (#120546) --- 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 1b17601a2f6..063f7db34a0 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240610.1"] + "requirements": ["home-assistant-frontend==20240626.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d3320e64fe3..18461d6398b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240610.1 +home-assistant-frontend==20240626.0 home-assistant-intents==2024.6.21 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1a297ef2b5c..2ec7f38e5e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240610.1 +home-assistant-frontend==20240626.0 # homeassistant.components.conversation home-assistant-intents==2024.6.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88623000c5e..f0e856c2f6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -898,7 +898,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240610.1 +home-assistant-frontend==20240626.0 # homeassistant.components.conversation home-assistant-intents==2024.6.21 From a36c40a4346b4e0ffd22b615659cd57dc3fad140 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 13:35:01 +0200 Subject: [PATCH 0229/2411] Use state_reported events in Riemann sum sensor (#113869) --- .../components/integration/sensor.py | 131 ++++++++++++------ tests/components/integration/test_sensor.py | 78 +++-------- 2 files changed, 113 insertions(+), 96 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 106eb9cc79c..60cbee5549f 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -8,7 +8,7 @@ from datetime import UTC, datetime, timedelta from decimal import Decimal, InvalidOperation from enum import Enum import logging -from typing import Any, Final, Self +from typing import TYPE_CHECKING, Any, Final, Self import voluptuous as vol @@ -27,6 +27,8 @@ from homeassistant.const import ( CONF_METHOD, CONF_NAME, CONF_UNIQUE_ID, + EVENT_STATE_CHANGED, + EVENT_STATE_REPORTED, STATE_UNAVAILABLE, UnitOfTime, ) @@ -34,6 +36,7 @@ from homeassistant.core import ( CALLBACK_TYPE, Event, EventStateChangedData, + EventStateReportedData, HomeAssistant, State, callback, @@ -42,7 +45,7 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later, async_track_state_change_event +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( @@ -107,9 +110,7 @@ class _IntegrationMethod(ABC): return _NAME_TO_INTEGRATION_METHOD[method_name]() @abstractmethod - def validate_states( - self, left: State, right: State - ) -> tuple[Decimal, Decimal] | None: + def validate_states(self, left: str, right: str) -> tuple[Decimal, Decimal] | None: """Check state requirements for integration.""" @abstractmethod @@ -130,11 +131,9 @@ class _Trapezoidal(_IntegrationMethod): ) -> Decimal: return elapsed_time * (left + right) / 2 - def validate_states( - self, left: State, right: State - ) -> tuple[Decimal, Decimal] | None: - if (left_dec := _decimal_state(left.state)) is None or ( - right_dec := _decimal_state(right.state) + def validate_states(self, left: str, right: str) -> tuple[Decimal, Decimal] | None: + if (left_dec := _decimal_state(left)) is None or ( + right_dec := _decimal_state(right) ) is None: return None return (left_dec, right_dec) @@ -146,10 +145,8 @@ class _Left(_IntegrationMethod): ) -> Decimal: return self.calculate_area_with_one_state(elapsed_time, left) - def validate_states( - self, left: State, right: State - ) -> tuple[Decimal, Decimal] | None: - if (left_dec := _decimal_state(left.state)) is None: + def validate_states(self, left: str, right: str) -> tuple[Decimal, Decimal] | None: + if (left_dec := _decimal_state(left)) is None: return None return (left_dec, left_dec) @@ -160,10 +157,8 @@ class _Right(_IntegrationMethod): ) -> Decimal: return self.calculate_area_with_one_state(elapsed_time, right) - def validate_states( - self, left: State, right: State - ) -> tuple[Decimal, Decimal] | None: - if (right_dec := _decimal_state(right.state)) is None: + def validate_states(self, left: str, right: str) -> tuple[Decimal, Decimal] | None: + if (right_dec := _decimal_state(right)) is None: return None return (right_dec, right_dec) @@ -183,7 +178,7 @@ _NAME_TO_INTEGRATION_METHOD: dict[str, type[_IntegrationMethod]] = { class _IntegrationTrigger(Enum): - StateChange = "state_change" + StateEvent = "state_event" TimeElapsed = "time_elapsed" @@ -343,7 +338,7 @@ class IntegrationSensor(RestoreSensor): ) self._max_sub_interval_exceeded_callback: CALLBACK_TYPE = lambda *args: None self._last_integration_time: datetime = datetime.now(tz=UTC) - self._last_integration_trigger = _IntegrationTrigger.StateChange + self._last_integration_trigger = _IntegrationTrigger.StateEvent self._attr_suggested_display_precision = round_digits or 2 def _calculate_unit(self, source_unit: str) -> str: @@ -433,9 +428,11 @@ class IntegrationSensor(RestoreSensor): source_state = self.hass.states.get(self._sensor_source_id) self._schedule_max_sub_interval_exceeded_if_state_is_numeric(source_state) self.async_on_remove(self._cancel_max_sub_interval_exceeded_callback) - handle_state_change = self._integrate_on_state_change_and_max_sub_interval + handle_state_change = self._integrate_on_state_change_with_max_sub_interval + handle_state_report = self._integrate_on_state_report_with_max_sub_interval else: handle_state_change = self._integrate_on_state_change_callback + handle_state_report = self._integrate_on_state_report_callback if ( state := self.hass.states.get(self._source_entity) @@ -443,16 +440,50 @@ class IntegrationSensor(RestoreSensor): self._derive_and_set_attributes_from_state(state) self.async_on_remove( - async_track_state_change_event( - self.hass, - [self._sensor_source_id], + self.hass.bus.async_listen( + EVENT_STATE_CHANGED, handle_state_change, + event_filter=callback( + lambda event_data: event_data["entity_id"] == self._sensor_source_id + ), + run_immediately=True, + ) + ) + self.async_on_remove( + self.hass.bus.async_listen( + EVENT_STATE_REPORTED, + handle_state_report, + event_filter=callback( + lambda event_data: event_data["entity_id"] == self._sensor_source_id + ), + run_immediately=True, ) ) @callback - def _integrate_on_state_change_and_max_sub_interval( + def _integrate_on_state_change_with_max_sub_interval( self, event: Event[EventStateChangedData] + ) -> None: + """Handle sensor state update when sub interval is configured.""" + self._integrate_on_state_update_with_max_sub_interval( + None, event.data["old_state"], event.data["new_state"] + ) + + @callback + def _integrate_on_state_report_with_max_sub_interval( + self, event: Event[EventStateReportedData] + ) -> None: + """Handle sensor state report when sub interval is configured.""" + self._integrate_on_state_update_with_max_sub_interval( + event.data["old_last_reported"], None, event.data["new_state"] + ) + + @callback + def _integrate_on_state_update_with_max_sub_interval( + self, + old_last_reported: datetime | None, + old_state: State | None, + new_state: State | None, ) -> None: """Integrate based on state change and time. @@ -460,11 +491,9 @@ class IntegrationSensor(RestoreSensor): reschedules time based integration. """ self._cancel_max_sub_interval_exceeded_callback() - old_state = event.data["old_state"] - new_state = event.data["new_state"] try: - self._integrate_on_state_change(old_state, new_state) - self._last_integration_trigger = _IntegrationTrigger.StateChange + self._integrate_on_state_change(old_last_reported, old_state, new_state) + self._last_integration_trigger = _IntegrationTrigger.StateEvent self._last_integration_time = datetime.now(tz=UTC) finally: # When max_sub_interval exceeds without state change the source is assumed @@ -475,13 +504,25 @@ class IntegrationSensor(RestoreSensor): def _integrate_on_state_change_callback( self, event: Event[EventStateChangedData] ) -> None: - """Handle the sensor state changes.""" - old_state = event.data["old_state"] - new_state = event.data["new_state"] - return self._integrate_on_state_change(old_state, new_state) + """Handle sensor state change.""" + return self._integrate_on_state_change( + None, event.data["old_state"], event.data["new_state"] + ) + + @callback + def _integrate_on_state_report_callback( + self, event: Event[EventStateReportedData] + ) -> None: + """Handle sensor state report.""" + return self._integrate_on_state_change( + event.data["old_last_reported"], None, event.data["new_state"] + ) def _integrate_on_state_change( - self, old_state: State | None, new_state: State | None + self, + old_last_reported: datetime | None, + old_state: State | None, + new_state: State | None, ) -> None: if new_state is None: return @@ -491,21 +532,33 @@ class IntegrationSensor(RestoreSensor): self.async_write_ha_state() return + if old_state: + # state has changed, we recover old_state from the event + old_state_state = old_state.state + old_last_reported = old_state.last_reported + else: + # event state reported without any state change + old_state_state = new_state.state + self._attr_available = True self._derive_and_set_attributes_from_state(new_state) - if old_state is None: + if old_last_reported is None and old_state is None: self.async_write_ha_state() return - if not (states := self._method.validate_states(old_state, new_state)): + if not ( + states := self._method.validate_states(old_state_state, new_state.state) + ): self.async_write_ha_state() return + if TYPE_CHECKING: + assert old_last_reported is not None elapsed_seconds = Decimal( - (new_state.last_updated - old_state.last_updated).total_seconds() - if self._last_integration_trigger == _IntegrationTrigger.StateChange - else (new_state.last_updated - self._last_integration_time).total_seconds() + (new_state.last_reported - old_last_reported).total_seconds() + if self._last_integration_trigger == _IntegrationTrigger.StateEvent + else (new_state.last_reported - self._last_integration_time).total_seconds() ) area = self._method.calculate_area_with_two_states(elapsed_seconds, *states) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 10f921ce603..974c8bb8691 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -294,28 +294,16 @@ async def test_restore_state_failed(hass: HomeAssistant, extra_attributes) -> No assert state.state == STATE_UNKNOWN +@pytest.mark.parametrize("force_update", [False, True]) @pytest.mark.parametrize( - ("force_update", "sequence"), + "sequence", [ ( - False, - ( - (20, 10, 1.67), - (30, 30, 5.0), - (40, 5, 7.92), - (50, 5, 7.92), - (60, 0, 8.75), - ), - ), - ( - True, - ( - (20, 10, 1.67), - (30, 30, 5.0), - (40, 5, 7.92), - (50, 5, 8.75), - (60, 0, 9.17), - ), + (20, 10, 1.67), + (30, 30, 5.0), + (40, 5, 7.92), + (50, 5, 8.75), + (60, 0, 9.17), ), ], ) @@ -358,28 +346,16 @@ async def test_trapezoidal( assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR +@pytest.mark.parametrize("force_update", [False, True]) @pytest.mark.parametrize( - ("force_update", "sequence"), + "sequence", [ ( - False, - ( - (20, 10, 0.0), - (30, 30, 1.67), - (40, 5, 6.67), - (50, 5, 6.67), - (60, 0, 8.33), - ), - ), - ( - True, - ( - (20, 10, 0.0), - (30, 30, 1.67), - (40, 5, 6.67), - (50, 5, 7.5), - (60, 0, 8.33), - ), + (20, 10, 0.0), + (30, 30, 1.67), + (40, 5, 6.67), + (50, 5, 7.5), + (60, 0, 8.33), ), ], ) @@ -425,28 +401,16 @@ async def test_left( assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR +@pytest.mark.parametrize("force_update", [False, True]) @pytest.mark.parametrize( - ("force_update", "sequence"), + "sequence", [ ( - False, - ( - (20, 10, 3.33), - (30, 30, 8.33), - (40, 5, 9.17), - (50, 5, 9.17), - (60, 0, 9.17), - ), - ), - ( - True, - ( - (20, 10, 3.33), - (30, 30, 8.33), - (40, 5, 9.17), - (50, 5, 10.0), - (60, 0, 10.0), - ), + (20, 10, 3.33), + (30, 30, 8.33), + (40, 5, 9.17), + (50, 5, 10.0), + (60, 0, 10.0), ), ], ) From 13a9efb6a6045bbe9afc3f01be12cbd66ba3a332 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 13:36:01 +0200 Subject: [PATCH 0230/2411] Convert dataclass to dict in pyLoad diagnostics (#120552) --- homeassistant/components/pyload/diagnostics.py | 3 ++- .../components/pyload/snapshots/test_diagnostics.ambr | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pyload/diagnostics.py b/homeassistant/components/pyload/diagnostics.py index d18e5a5fe0d..95ff37bf9f8 100644 --- a/homeassistant/components/pyload/diagnostics.py +++ b/homeassistant/components/pyload/diagnostics.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import asdict from typing import Any from homeassistant.components.diagnostics import async_redact_data @@ -22,5 +23,5 @@ async def async_get_config_entry_diagnostics( return { "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), - "pyload_data": pyload_data, + "pyload_data": asdict(pyload_data), } diff --git a/tests/components/pyload/snapshots/test_diagnostics.ambr b/tests/components/pyload/snapshots/test_diagnostics.ambr index 8c3e110f2ec..0e078e000c9 100644 --- a/tests/components/pyload/snapshots/test_diagnostics.ambr +++ b/tests/components/pyload/snapshots/test_diagnostics.ambr @@ -10,8 +10,15 @@ 'verify_ssl': False, }), 'pyload_data': dict({ - '__type': "", - 'repr': 'pyLoadData(pause=False, active=1, queue=6, total=37, speed=5405963.0, download=True, reconnect=False, captcha=False, free_space=99999999999)', + 'active': 1, + 'captcha': False, + 'download': True, + 'free_space': 99999999999, + 'pause': False, + 'queue': 6, + 'reconnect': False, + 'speed': 5405963.0, + 'total': 37, }), }) # --- From 972b85a75b644ca24dc3ebf5b60575ae656321fd Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 13:36:25 +0200 Subject: [PATCH 0231/2411] Fix class and variable naming errors in pyLoad integration (#120547) --- homeassistant/components/pyload/sensor.py | 2 +- homeassistant/components/pyload/switch.py | 14 +++++++------- tests/components/pyload/snapshots/test_switch.ambr | 4 ++-- tests/components/pyload/test_switch.py | 7 +++---- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 6cb432e12fd..83585a60c6d 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -181,7 +181,7 @@ class PyLoadSensor(CoordinatorEntity[PyLoadCoordinator], SensorEntity): f"{coordinator.config_entry.entry_id}_{entity_description.key}" ) self.entity_description = entity_description - self.device_info = DeviceInfo( + self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, manufacturer=MANUFACTURER, model=SERVICE_NAME, diff --git a/homeassistant/components/pyload/switch.py b/homeassistant/components/pyload/switch.py index b9391ef818f..b0628005008 100644 --- a/homeassistant/components/pyload/switch.py +++ b/homeassistant/components/pyload/switch.py @@ -24,7 +24,7 @@ from .const import DOMAIN, MANUFACTURER, SERVICE_NAME from .coordinator import PyLoadCoordinator -class PyLoadSwitchEntity(StrEnum): +class PyLoadSwitch(StrEnum): """PyLoad Switch Entities.""" PAUSE_RESUME_QUEUE = "download" @@ -42,16 +42,16 @@ class PyLoadSwitchEntityDescription(SwitchEntityDescription): SENSOR_DESCRIPTIONS: tuple[PyLoadSwitchEntityDescription, ...] = ( PyLoadSwitchEntityDescription( - key=PyLoadSwitchEntity.PAUSE_RESUME_QUEUE, - translation_key=PyLoadSwitchEntity.PAUSE_RESUME_QUEUE, + key=PyLoadSwitch.PAUSE_RESUME_QUEUE, + translation_key=PyLoadSwitch.PAUSE_RESUME_QUEUE, device_class=SwitchDeviceClass.SWITCH, turn_on_fn=lambda api: api.unpause(), turn_off_fn=lambda api: api.pause(), toggle_fn=lambda api: api.toggle_pause(), ), PyLoadSwitchEntityDescription( - key=PyLoadSwitchEntity.RECONNECT, - translation_key=PyLoadSwitchEntity.RECONNECT, + key=PyLoadSwitch.RECONNECT, + translation_key=PyLoadSwitch.RECONNECT, device_class=SwitchDeviceClass.SWITCH, turn_on_fn=lambda api: api.toggle_reconnect(), turn_off_fn=lambda api: api.toggle_reconnect(), @@ -70,12 +70,12 @@ async def async_setup_entry( coordinator = entry.runtime_data async_add_entities( - PyLoadBinarySensor(coordinator, description) + PyLoadSwitchEntity(coordinator, description) for description in SENSOR_DESCRIPTIONS ) -class PyLoadBinarySensor(CoordinatorEntity[PyLoadCoordinator], SwitchEntity): +class PyLoadSwitchEntity(CoordinatorEntity[PyLoadCoordinator], SwitchEntity): """Representation of a pyLoad sensor.""" _attr_has_entity_name = True diff --git a/tests/components/pyload/snapshots/test_switch.ambr b/tests/components/pyload/snapshots/test_switch.ambr index 94f2910cad8..b6465341b0a 100644 --- a/tests/components/pyload/snapshots/test_switch.ambr +++ b/tests/components/pyload/snapshots/test_switch.ambr @@ -27,7 +27,7 @@ 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': , + 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_reconnect', 'unit_of_measurement': None, }) @@ -74,7 +74,7 @@ 'platform': 'pyload', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': , + 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_download', 'unit_of_measurement': None, }) diff --git a/tests/components/pyload/test_switch.py b/tests/components/pyload/test_switch.py index e7bd5a24a87..42a6bfa6f14 100644 --- a/tests/components/pyload/test_switch.py +++ b/tests/components/pyload/test_switch.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, call, patch import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.pyload.switch import PyLoadSwitchEntity +from homeassistant.components.pyload.switch import PyLoadSwitch from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TOGGLE, @@ -22,12 +22,12 @@ from tests.common import MockConfigEntry, snapshot_platform # Maps entity to the mock calls to assert API_CALL = { - PyLoadSwitchEntity.PAUSE_RESUME_QUEUE: { + PyLoadSwitch.PAUSE_RESUME_QUEUE: { SERVICE_TURN_ON: call.unpause, SERVICE_TURN_OFF: call.pause, SERVICE_TOGGLE: call.toggle_pause, }, - PyLoadSwitchEntity.RECONNECT: { + PyLoadSwitch.RECONNECT: { SERVICE_TURN_ON: call.toggle_reconnect, SERVICE_TURN_OFF: call.toggle_reconnect, SERVICE_TOGGLE: call.toggle_reconnect, @@ -97,7 +97,6 @@ async def test_turn_on_off( {ATTR_ENTITY_ID: entity_entry.entity_id}, blocking=True, ) - await hass.async_block_till_done() assert ( API_CALL[entity_entry.translation_key][service_call] in mock_pyloadapi.method_calls From b07453dca404c42637733fa134259c7c531239c7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:37:08 +0200 Subject: [PATCH 0232/2411] Implement remaining select-adaptions for Plugwise (#120544) --- homeassistant/components/plugwise/select.py | 2 +- tests/components/plugwise/test_select.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index 99aecacb96b..b7d4a0a1ded 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -109,5 +109,5 @@ class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): self.device[LOCATION] and STATE_ON are required for the thermostat-schedule select. """ await self.coordinator.api.set_select( - self.entity_description.key, self.device[LOCATION], STATE_ON, option + self.entity_description.key, self.device[LOCATION], option, STATE_ON ) diff --git a/tests/components/plugwise/test_select.py b/tests/components/plugwise/test_select.py index a6245ff11e7..b9dec283bc4 100644 --- a/tests/components/plugwise/test_select.py +++ b/tests/components/plugwise/test_select.py @@ -42,8 +42,8 @@ async def test_adam_change_select_entity( mock_smile_adam.set_select.assert_called_with( "select_schedule", "c50f167537524366a5af7aa3942feb1e", - "on", "Badkamer Schema", + "on", ) @@ -74,6 +74,6 @@ async def test_adam_select_regulation_mode( mock_smile_adam_3.set_select.assert_called_with( "select_regulation_mode", "bc93488efab249e5bc54fd7e175a6f91", - "on", "heating", + "on", ) From 1d0aa6bff0b69e7388fdfd0d761c71cfd239017d Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 13:40:20 +0200 Subject: [PATCH 0233/2411] Update docstrings in pyLoad tests (#120556) --- tests/components/pyload/test_button.py | 2 +- tests/components/pyload/test_config_flow.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/pyload/test_button.py b/tests/components/pyload/test_button.py index b30a4cefd42..b5aa18ad3d9 100644 --- a/tests/components/pyload/test_button.py +++ b/tests/components/pyload/test_button.py @@ -59,7 +59,7 @@ async def test_button_press( mock_pyloadapi: AsyncMock, entity_registry: er.EntityRegistry, ) -> None: - """Test switch turn on method.""" + """Test button press method.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/pyload/test_config_flow.py b/tests/components/pyload/test_config_flow.py index 8e9083a49c8..8c775412371 100644 --- a/tests/components/pyload/test_config_flow.py +++ b/tests/components/pyload/test_config_flow.py @@ -262,7 +262,7 @@ async def test_reconfiguration( config_entry: MockConfigEntry, mock_pyloadapi: AsyncMock, ) -> None: - """Test reauth flow.""" + """Test reconfiguration flow.""" config_entry.add_to_hass(hass) @@ -304,7 +304,7 @@ async def test_reconfigure_errors( side_effect: Exception, error_text: str, ) -> None: - """Test reauth flow.""" + """Test reconfiguration flow.""" config_entry.add_to_hass(hass) From 0d2aeb846f3cc9f9f799cfd0db83567c4f338e14 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 14:05:24 +0200 Subject: [PATCH 0234/2411] Increase max temperature to 40 for Tado (#120560) --- homeassistant/components/tado/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index a41003da95f..5c6a80c5beb 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -226,7 +226,7 @@ HA_TERMINATION_TYPE = "default_overlay_type" HA_TERMINATION_DURATION = "default_overlay_seconds" TADO_DEFAULT_MIN_TEMP = 5 -TADO_DEFAULT_MAX_TEMP = 25 +TADO_DEFAULT_MAX_TEMP = 40 # Constants for service calls SERVICE_ADD_METER_READING = "add_meter_reading" CONF_CONFIG_ENTRY = "config_entry" From 69e0227682eec31d365e2efab9aec58f66972f49 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 26 Jun 2024 08:13:49 -0400 Subject: [PATCH 0235/2411] Add Roborock to strict typing (#120379) --- .strict-typing | 1 + homeassistant/components/roborock/image.py | 6 ++++-- homeassistant/components/roborock/number.py | 3 ++- homeassistant/components/roborock/switch.py | 10 +++++----- mypy.ini | 10 ++++++++++ 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.strict-typing b/.strict-typing index 2a6edfedd32..a6deb6eca3a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -370,6 +370,7 @@ homeassistant.components.rhasspy.* homeassistant.components.ridwell.* homeassistant.components.ring.* homeassistant.components.rituals_perfume_genie.* +homeassistant.components.roborock.* homeassistant.components.roku.* homeassistant.components.romy.* homeassistant.components.rpi_power.* diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index afe1e781a88..33b8b0a2c90 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -1,6 +1,7 @@ """Support for Roborock image.""" import asyncio +from datetime import datetime import io from itertools import chain @@ -48,6 +49,7 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): """A class to let you visualize the map.""" _attr_has_entity_name = True + image_last_updated: datetime def __init__( self, @@ -76,7 +78,7 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): self._attr_entity_category = EntityCategory.DIAGNOSTIC @property - def available(self): + def available(self) -> bool: """Determines if the entity is available.""" return self.cached_map != b"" @@ -98,7 +100,7 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): and bool(self.coordinator.roborock_device_info.props.status.in_cleaning) ) - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: # Bump last updated every third time the coordinator runs, so that async_image # will be called and we will evaluate on the new coordinator data if we should # update the cache. diff --git a/homeassistant/components/roborock/number.py b/homeassistant/components/roborock/number.py index 8aa20fad838..a432c527b0e 100644 --- a/homeassistant/components/roborock/number.py +++ b/homeassistant/components/roborock/number.py @@ -107,7 +107,8 @@ class RoborockNumberEntity(RoborockEntity, NumberEntity): @property def native_value(self) -> float | None: """Get native value.""" - return self.get_cache(self.entity_description.cache_key).value + val: float = self.get_cache(self.entity_description.cache_key).value + return val async def async_set_native_value(self, value: float) -> None: """Set number value.""" diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index 7e17844666e..9a34060fe96 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -167,9 +167,9 @@ class RoborockSwitch(RoborockEntity, SwitchEntity): @property def is_on(self) -> bool | None: """Return True if entity is on.""" - return ( - self.get_cache(self.entity_description.cache_key).value.get( - self.entity_description.attribute - ) - == 1 + status = self.get_cache(self.entity_description.cache_key).value.get( + self.entity_description.attribute ) + if status is None: + return status + return bool(status) diff --git a/mypy.ini b/mypy.ini index 740eb4f2b5b..d94e5a37194 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3463,6 +3463,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.roborock.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.roku.*] check_untyped_defs = true disallow_incomplete_defs = true From f5ff19d60274a662c3d186d312ba95c5829a23d4 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 14:14:48 +0200 Subject: [PATCH 0236/2411] Add measurement unit and state_class to sensors in pyLoad (#120551) --- homeassistant/components/pyload/const.py | 2 + homeassistant/components/pyload/sensor.py | 8 ++ .../pyload/snapshots/test_sensor.ambr | 96 ++++++++++++++----- 3 files changed, 82 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/pyload/const.py b/homeassistant/components/pyload/const.py index 9419786fd88..a0b66687bd0 100644 --- a/homeassistant/components/pyload/const.py +++ b/homeassistant/components/pyload/const.py @@ -10,3 +10,5 @@ ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=pyload"} MANUFACTURER = "pyLoad Team" SERVICE_NAME = "pyLoad" + +UNIT_DOWNLOADS = "downloads" diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 83585a60c6d..bc90fdb7ccb 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( @@ -42,6 +43,7 @@ from .const import ( ISSUE_PLACEHOLDER, MANUFACTURER, SERVICE_NAME, + UNIT_DOWNLOADS, ) from .coordinator import PyLoadCoordinator @@ -68,14 +70,20 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=PyLoadSensorEntity.ACTIVE, translation_key=PyLoadSensorEntity.ACTIVE, + native_unit_of_measurement=UNIT_DOWNLOADS, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=PyLoadSensorEntity.QUEUE, translation_key=PyLoadSensorEntity.QUEUE, + native_unit_of_measurement=UNIT_DOWNLOADS, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=PyLoadSensorEntity.TOTAL, translation_key=PyLoadSensorEntity.TOTAL, + native_unit_of_measurement=UNIT_DOWNLOADS, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=PyLoadSensorEntity.FREE_SPACE, diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index 159309041e0..c1e5a9d6c3a 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -4,7 +4,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -29,13 +31,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_active', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_active_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Active downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_active_downloads', @@ -50,7 +54,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -75,13 +81,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_queue', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_downloads_in_queue-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Downloads in queue', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_downloads_in_queue', @@ -96,7 +104,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -121,13 +131,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Finished downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_finished_downloads', @@ -250,7 +262,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -275,13 +289,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_active', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_active_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Active downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_active_downloads', @@ -296,7 +312,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -321,13 +339,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_queue', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_downloads_in_queue-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Downloads in queue', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_downloads_in_queue', @@ -342,7 +362,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -367,13 +389,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Finished downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_finished_downloads', @@ -496,7 +520,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -521,13 +547,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_active', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[ParserError][sensor.pyload_active_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Active downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_active_downloads', @@ -542,7 +570,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -567,13 +597,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_queue', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[ParserError][sensor.pyload_downloads_in_queue-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Downloads in queue', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_downloads_in_queue', @@ -588,7 +620,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -613,13 +647,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_sensor_update_exceptions[ParserError][sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Finished downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_finished_downloads', @@ -742,7 +778,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -767,13 +805,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_active', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_setup[sensor.pyload_active_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Active downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_active_downloads', @@ -788,7 +828,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -813,13 +855,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_queue', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_setup[sensor.pyload_downloads_in_queue-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Downloads in queue', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_downloads_in_queue', @@ -834,7 +878,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -859,13 +905,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': None, + 'unit_of_measurement': 'downloads', }) # --- # name: test_setup[sensor.pyload_finished_downloads-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'pyLoad Finished downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', }), 'context': , 'entity_id': 'sensor.pyload_finished_downloads', From d515a7f0634641a6aa4990d86621542f249cf175 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 26 Jun 2024 07:20:11 -0500 Subject: [PATCH 0237/2411] Add created_seconds to timer info and pass to ESPHome devices (#120364) --- .../components/esphome/voice_assistant.py | 2 +- homeassistant/components/intent/timers.py | 17 +++++++++++ .../esphome/test_voice_assistant.py | 29 +++++++++++++++++-- tests/components/intent/test_timers.py | 7 +++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index 10358d871ca..a6cedee30ab 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -467,7 +467,7 @@ def handle_timer_event( native_event_type, timer_info.id, timer_info.name, - timer_info.seconds, + timer_info.created_seconds, timer_info.seconds_left, timer_info.is_active, ) diff --git a/homeassistant/components/intent/timers.py b/homeassistant/components/intent/timers.py index 82f6121da53..a8576509a4b 100644 --- a/homeassistant/components/intent/timers.py +++ b/homeassistant/components/intent/timers.py @@ -93,6 +93,13 @@ class TimerInfo: This agent will be used to execute the conversation command. """ + _created_seconds: int = 0 + """Number of seconds on the timer when it was created.""" + + def __post_init__(self) -> None: + """Post initialization.""" + self._created_seconds = self.seconds + @property def seconds_left(self) -> int: """Return number of seconds left on the timer.""" @@ -103,6 +110,15 @@ class TimerInfo: seconds_running = int((now - self.updated_at) / 1e9) return max(0, self.seconds - seconds_running) + @property + def created_seconds(self) -> int: + """Return number of seconds on the timer when it was created. + + This value is increased if time is added to the timer, exceeding its + original created_seconds. + """ + return self._created_seconds + @cached_property def name_normalized(self) -> str: """Return normalized timer name.""" @@ -131,6 +147,7 @@ class TimerInfo: Seconds may be negative to remove time instead. """ self.seconds = max(0, self.seconds_left + seconds) + self._created_seconds = max(self._created_seconds, self.seconds) self.updated_at = time.monotonic_ns() def finish(self) -> None: diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index bcd49f91c03..c347c3dc7d3 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -836,6 +836,7 @@ async def test_timer_events( connections={(dr.CONNECTION_NETWORK_MAC, mock_device.entry.unique_id)} ) + total_seconds = (1 * 60 * 60) + (2 * 60) + 3 await intent_helper.async_handle( hass, "test", @@ -853,8 +854,32 @@ async def test_timer_events( VoiceAssistantTimerEventType.VOICE_ASSISTANT_TIMER_STARTED, ANY, "test timer", - 3723, - 3723, + total_seconds, + total_seconds, + True, + ) + + # Increase timer beyond original time and check total_seconds has increased + mock_client.send_voice_assistant_timer_event.reset_mock() + + total_seconds += 5 * 60 + await intent_helper.async_handle( + hass, + "test", + intent_helper.INTENT_INCREASE_TIMER, + { + "name": {"value": "test timer"}, + "minutes": {"value": 5}, + }, + device_id=dev.id, + ) + + mock_client.send_voice_assistant_timer_event.assert_called_with( + VoiceAssistantTimerEventType.VOICE_ASSISTANT_TIMER_UPDATED, + ANY, + "test timer", + total_seconds, + ANY, True, ) diff --git a/tests/components/intent/test_timers.py b/tests/components/intent/test_timers.py index c2efe5d39e2..d194d532513 100644 --- a/tests/components/intent/test_timers.py +++ b/tests/components/intent/test_timers.py @@ -54,6 +54,7 @@ async def test_start_finish_timer(hass: HomeAssistant, init_components) -> None: assert timer.start_minutes is None assert timer.start_seconds == 0 assert timer.seconds_left == 0 + assert timer.created_seconds == 0 if event_type == TimerEventType.STARTED: timer_id = timer.id @@ -218,6 +219,7 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None: timer_name = "test timer" timer_id: str | None = None original_total_seconds = -1 + seconds_added = 0 @callback def handle_timer(event_type: TimerEventType, timer: TimerInfo) -> None: @@ -238,12 +240,14 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None: + (60 * timer.start_minutes) + timer.start_seconds ) + assert timer.created_seconds == original_total_seconds started_event.set() elif event_type == TimerEventType.UPDATED: assert timer.id == timer_id # Timer was increased assert timer.seconds_left > original_total_seconds + assert timer.created_seconds == original_total_seconds + seconds_added updated_event.set() elif event_type == TimerEventType.CANCELLED: assert timer.id == timer_id @@ -270,6 +274,7 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None: await started_event.wait() # Adding 0 seconds has no effect + seconds_added = 0 result = await intent.async_handle( hass, "test", @@ -288,6 +293,7 @@ async def test_increase_timer(hass: HomeAssistant, init_components) -> None: assert not updated_event.is_set() # Add 30 seconds to the timer + seconds_added = (1 * 60 * 60) + (5 * 60) + 30 result = await intent.async_handle( hass, "test", @@ -357,6 +363,7 @@ async def test_decrease_timer(hass: HomeAssistant, init_components) -> None: # Timer was decreased assert timer.seconds_left <= (original_total_seconds - 30) + assert timer.created_seconds == original_total_seconds updated_event.set() elif event_type == TimerEventType.CANCELLED: From e39d26bdc0d99392cf013436e85bb3e91dc1eed0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 14:21:30 +0200 Subject: [PATCH 0238/2411] Add switch platform to Airgradient (#120559) --- .../components/airgradient/__init__.py | 1 + .../components/airgradient/strings.json | 5 + .../components/airgradient/switch.py | 110 ++++++++++++++++++ .../airgradient/snapshots/test_switch.ambr | 47 ++++++++ tests/components/airgradient/test_number.py | 2 +- tests/components/airgradient/test_switch.py | 101 ++++++++++++++++ 6 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/airgradient/switch.py create mode 100644 tests/components/airgradient/snapshots/test_switch.ambr create mode 100644 tests/components/airgradient/test_switch.py diff --git a/homeassistant/components/airgradient/__init__.py b/homeassistant/components/airgradient/__init__.py index b1b5a28ef67..fe01d239f3c 100644 --- a/homeassistant/components/airgradient/__init__.py +++ b/homeassistant/components/airgradient/__init__.py @@ -20,6 +20,7 @@ PLATFORMS: list[Platform] = [ Platform.NUMBER, Platform.SELECT, Platform.SENSOR, + Platform.SWITCH, ] diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index eb529a99ae3..1dd5fc61a16 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -154,6 +154,11 @@ "display_brightness": { "name": "[%key:component::airgradient::entity::number::display_brightness::name%]" } + }, + "switch": { + "post_data_to_airgradient": { + "name": "Post data to Airgradient" + } } } } diff --git a/homeassistant/components/airgradient/switch.py b/homeassistant/components/airgradient/switch.py new file mode 100644 index 00000000000..60c3f83ae5e --- /dev/null +++ b/homeassistant/components/airgradient/switch.py @@ -0,0 +1,110 @@ +"""Support for AirGradient switch entities.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from airgradient import AirGradientClient, Config +from airgradient.models import ConfigurationControl + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AirGradientConfigEntry +from .const import DOMAIN +from .coordinator import AirGradientConfigCoordinator +from .entity import AirGradientEntity + + +@dataclass(frozen=True, kw_only=True) +class AirGradientSwitchEntityDescription(SwitchEntityDescription): + """Describes AirGradient switch entity.""" + + value_fn: Callable[[Config], bool] + set_value_fn: Callable[[AirGradientClient, bool], Awaitable[None]] + + +POST_DATA_TO_AIRGRADIENT = AirGradientSwitchEntityDescription( + key="post_data_to_airgradient", + translation_key="post_data_to_airgradient", + entity_category=EntityCategory.CONFIG, + value_fn=lambda config: config.post_data_to_airgradient, + set_value_fn=lambda client, value: client.enable_sharing_data(enable=value), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AirGradientConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up AirGradient switch entities based on a config entry.""" + coordinator = entry.runtime_data.config + + added_entities = False + + @callback + def _async_check_entities() -> None: + nonlocal added_entities + + if ( + coordinator.data.configuration_control is ConfigurationControl.LOCAL + and not added_entities + ): + async_add_entities( + [AirGradientSwitch(coordinator, POST_DATA_TO_AIRGRADIENT)] + ) + added_entities = True + elif ( + coordinator.data.configuration_control is not ConfigurationControl.LOCAL + and added_entities + ): + entity_registry = er.async_get(hass) + unique_id = f"{coordinator.serial_number}-{POST_DATA_TO_AIRGRADIENT.key}" + if entity_id := entity_registry.async_get_entity_id( + SWITCH_DOMAIN, DOMAIN, unique_id + ): + entity_registry.async_remove(entity_id) + added_entities = False + + coordinator.async_add_listener(_async_check_entities) + _async_check_entities() + + +class AirGradientSwitch(AirGradientEntity, SwitchEntity): + """Defines an AirGradient switch entity.""" + + entity_description: AirGradientSwitchEntityDescription + coordinator: AirGradientConfigCoordinator + + def __init__( + self, + coordinator: AirGradientConfigCoordinator, + description: AirGradientSwitchEntityDescription, + ) -> None: + """Initialize AirGradient switch.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return self.entity_description.value_fn(self.coordinator.data) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self.entity_description.set_value_fn(self.coordinator.client, True) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self.entity_description.set_value_fn(self.coordinator.client, False) + await self.coordinator.async_request_refresh() diff --git a/tests/components/airgradient/snapshots/test_switch.ambr b/tests/components/airgradient/snapshots/test_switch.ambr new file mode 100644 index 00000000000..752355dbe97 --- /dev/null +++ b/tests/components/airgradient/snapshots/test_switch.ambr @@ -0,0 +1,47 @@ +# serializer version: 1 +# name: test_all_entities[switch.airgradient_post_data_to_airgradient-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.airgradient_post_data_to_airgradient', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Post data to Airgradient', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'post_data_to_airgradient', + 'unique_id': '84fce612f5b8-post_data_to_airgradient', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.airgradient_post_data_to_airgradient-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Airgradient Post data to Airgradient', + }), + 'context': , + 'entity_id': 'switch.airgradient_post_data_to_airgradient', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/airgradient/test_number.py b/tests/components/airgradient/test_number.py index ba659829c50..0803c0d437f 100644 --- a/tests/components/airgradient/test_number.py +++ b/tests/components/airgradient/test_number.py @@ -1,4 +1,4 @@ -"""Tests for the AirGradient button platform.""" +"""Tests for the AirGradient number platform.""" from datetime import timedelta from unittest.mock import AsyncMock, patch diff --git a/tests/components/airgradient/test_switch.py b/tests/components/airgradient/test_switch.py new file mode 100644 index 00000000000..20a1cb7470b --- /dev/null +++ b/tests/components/airgradient/test_switch.py @@ -0,0 +1,101 @@ +"""Tests for the AirGradient switch platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from airgradient import Config +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion + +from homeassistant.components.airgradient import DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_fixture, + snapshot_platform, +) + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.SWITCH]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_setting_value( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setting value.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + target={ATTR_ENTITY_ID: "switch.airgradient_post_data_to_airgradient"}, + blocking=True, + ) + mock_airgradient_client.enable_sharing_data.assert_called_once() + mock_airgradient_client.enable_sharing_data.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + target={ATTR_ENTITY_ID: "switch.airgradient_post_data_to_airgradient"}, + blocking=True, + ) + mock_airgradient_client.enable_sharing_data.assert_called_once() + + +async def test_cloud_creates_no_switch( + hass: HomeAssistant, + mock_cloud_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test cloud configuration control.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.SWITCH]): + await setup_integration(hass, mock_config_entry) + + assert len(hass.states.async_all()) == 0 + + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_local.json", DOMAIN) + ) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + mock_cloud_airgradient_client.get_config.return_value = Config.from_json( + load_fixture("get_config_cloud.json", DOMAIN) + ) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 From fd67fe417e19c2440355d4c65ad6bee894657d7c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:22:52 +0200 Subject: [PATCH 0239/2411] Use ruff to force alias when importing PLATFORM_SCHEMA (#120539) --- homeassistant/components/config/scene.py | 7 +++- .../dlib_face_detect/image_processing.py | 9 ++-- pyproject.toml | 41 +++++++++++++++++++ tests/components/tts/common.py | 4 +- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index a2e2693036a..8192c0051b0 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -5,7 +5,10 @@ from __future__ import annotations from typing import Any import uuid -from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.scene import ( + DOMAIN, + PLATFORM_SCHEMA as SCENE_PLATFORM_SCHEMA, +) from homeassistant.config import SCENE_CONFIG_PATH from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback @@ -14,6 +17,8 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from .const import ACTION_DELETE from .view import EditIdBasedConfigView +PLATFORM_SCHEMA = SCENE_PLATFORM_SCHEMA + @callback def async_setup(hass: HomeAssistant) -> bool: diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index 9f6b30dee61..80becdf9992 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -6,15 +6,16 @@ import io import face_recognition -from homeassistant.components.image_processing import ImageProcessingFaceEntity +from homeassistant.components.image_processing import ( + PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, + ImageProcessingFaceEntity, +) from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.components.image_processing import ( # noqa: F401, isort:skip - PLATFORM_SCHEMA, -) +PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA def setup_platform( diff --git a/pyproject.toml b/pyproject.toml index db6c5f0c989..4edb1535411 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -811,7 +811,48 @@ ignore = [ [tool.ruff.lint.flake8-import-conventions.extend-aliases] voluptuous = "vol" +"homeassistant.components.air_quality.PLATFORM_SCHEMA" = "AIR_QUALITY_PLATFORM_SCHEMA" +"homeassistant.components.alarm_control_panel.PLATFORM_SCHEMA" = "ALARM_CONTROL_PANEL_PLATFORM_SCHEMA" +"homeassistant.components.binary_sensor.PLATFORM_SCHEMA" = "BINARY_SENSOR_PLATFORM_SCHEMA" +"homeassistant.components.button.PLATFORM_SCHEMA" = "BUTTON_PLATFORM_SCHEMA" +"homeassistant.components.calendar.PLATFORM_SCHEMA" = "CALENDAR_PLATFORM_SCHEMA" +"homeassistant.components.camera.PLATFORM_SCHEMA" = "CAMERA_PLATFORM_SCHEMA" +"homeassistant.components.climate.PLATFORM_SCHEMA" = "CLIMATE_PLATFORM_SCHEMA" +"homeassistant.components.conversation.PLATFORM_SCHEMA" = "CONVERSATION_PLATFORM_SCHEMA" +"homeassistant.components.cover.PLATFORM_SCHEMA" = "COVER_PLATFORM_SCHEMA" +"homeassistant.components.date.PLATFORM_SCHEMA" = "DATE_PLATFORM_SCHEMA" +"homeassistant.components.datetime.PLATFORM_SCHEMA" = "DATETIME_PLATFORM_SCHEMA" +"homeassistant.components.device_tracker.PLATFORM_SCHEMA" = "DEVICE_TRACKER_PLATFORM_SCHEMA" +"homeassistant.components.event.PLATFORM_SCHEMA" = "EVENT_PLATFORM_SCHEMA" +"homeassistant.components.fan.PLATFORM_SCHEMA" = "FAN_PLATFORM_SCHEMA" +"homeassistant.components.geo_location.PLATFORM_SCHEMA" = "GEO_LOCATION_PLATFORM_SCHEMA" +"homeassistant.components.humidifier.PLATFORM_SCHEMA" = "HUMIDIFIER_PLATFORM_SCHEMA" +"homeassistant.components.image.PLATFORM_SCHEMA" = "IMAGE_PLATFORM_SCHEMA" +"homeassistant.components.image_processing.PLATFORM_SCHEMA" = "IMAGE_PROCESSING_PLATFORM_SCHEMA" +"homeassistant.components.lawn_mower.PLATFORM_SCHEMA" = "LAWN_MOWER_PLATFORM_SCHEMA" +"homeassistant.components.light.PLATFORM_SCHEMA" = "LIGHT_PLATFORM_SCHEMA" +"homeassistant.components.lock.PLATFORM_SCHEMA" = "LOCK_PLATFORM_SCHEMA" +"homeassistant.components.mailbox.PLATFORM_SCHEMA" = "MAILBOX_PLATFORM_SCHEMA" +"homeassistant.components.media_player.PLATFORM_SCHEMA" = "MEDIA_PLAYER_PLATFORM_SCHEMA" "homeassistant.components.notify.PLATFORM_SCHEMA" = "NOTIFY_PLATFORM_SCHEMA" +"homeassistant.components.number.PLATFORM_SCHEMA" = "NUMBER_PLATFORM_SCHEMA" +"homeassistant.components.remote.PLATFORM_SCHEMA" = "REMOTE_PLATFORM_SCHEMA" +"homeassistant.components.scene.PLATFORM_SCHEMA" = "SCENE_PLATFORM_SCHEMA" +"homeassistant.components.select.PLATFORM_SCHEMA" = "SELECT_PLATFORM_SCHEMA" +"homeassistant.components.sensor.PLATFORM_SCHEMA" = "SENSOR_PLATFORM_SCHEMA" +"homeassistant.components.siren.PLATFORM_SCHEMA" = "SIREN_PLATFORM_SCHEMA" +"homeassistant.components.stt.PLATFORM_SCHEMA" = "STT_PLATFORM_SCHEMA" +"homeassistant.components.switch.PLATFORM_SCHEMA" = "SWITCH_PLATFORM_SCHEMA" +"homeassistant.components.text.PLATFORM_SCHEMA" = "TEXT_PLATFORM_SCHEMA" +"homeassistant.components.time.PLATFORM_SCHEMA" = "TIME_PLATFORM_SCHEMA" +"homeassistant.components.todo.PLATFORM_SCHEMA" = "TODO_PLATFORM_SCHEMA" +"homeassistant.components.tts.PLATFORM_SCHEMA" = "TTS_PLATFORM_SCHEMA" +"homeassistant.components.vacuum.PLATFORM_SCHEMA" = "VACUUM_PLATFORM_SCHEMA" +"homeassistant.components.valve.PLATFORM_SCHEMA" = "VALVE_PLATFORM_SCHEMA" +"homeassistant.components.update.PLATFORM_SCHEMA" = "UPDATE_PLATFORM_SCHEMA" +"homeassistant.components.wake_word.PLATFORM_SCHEMA" = "WAKE_WORD_PLATFORM_SCHEMA" +"homeassistant.components.water_heater.PLATFORM_SCHEMA" = "WATER_HEATER_PLATFORM_SCHEMA" +"homeassistant.components.weather.PLATFORM_SCHEMA" = "WEATHER_PLATFORM_SCHEMA" "homeassistant.helpers.area_registry" = "ar" "homeassistant.helpers.category_registry" = "cr" "homeassistant.helpers.config_validation" = "cv" diff --git a/tests/components/tts/common.py b/tests/components/tts/common.py index e1d9d973f25..b99e6400273 100644 --- a/tests/components/tts/common.py +++ b/tests/components/tts/common.py @@ -15,7 +15,7 @@ from homeassistant.components import media_source from homeassistant.components.tts import ( CONF_LANG, DOMAIN as TTS_DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, Provider, TextToSpeechEntity, TtsAudioType, @@ -184,7 +184,7 @@ class MockTTSEntity(BaseProvider, TextToSpeechEntity): class MockTTS(MockPlatform): """A mock TTS platform.""" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES)} ) From ec16fc235bc1049abf9fb3fe4549a1ccc0f7bf41 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 26 Jun 2024 22:23:06 +1000 Subject: [PATCH 0240/2411] Add new coordinators to Tessie (#118452) * WIP * wip * Add energy classes * Add basis for Testing * Bump Library * fix case * bump library * bump library again * bump library for teslemetry * reorder * Fix super * Update strings.json * Tests * Small tweaks * Bump * Bump teslemetry * Remove version * Add WC states * Bump to match dev * Review feedback Co-authored-by: Joost Lekkerkerker * Review feedback * Review feedback 1 * Review feedback 2 * TessieWallConnectorStates Enum * fixes * Fix translations and value * Update homeassistant/components/tessie/strings.json * Update homeassistant/components/tessie/strings.json --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/tessie/__init__.py | 54 +- homeassistant/components/tessie/const.py | 15 + .../components/tessie/coordinator.py | 91 +- homeassistant/components/tessie/entity.py | 130 ++- homeassistant/components/tessie/icons.json | 36 + homeassistant/components/tessie/manifest.json | 2 +- homeassistant/components/tessie/models.py | 20 +- homeassistant/components/tessie/sensor.py | 216 +++- homeassistant/components/tessie/strings.json | 54 + requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/tessie/common.py | 8 + tests/components/tessie/conftest.py | 47 + .../tessie/fixtures/live_status.json | 33 + .../components/tessie/fixtures/products.json | 121 +++ .../components/tessie/fixtures/site_info.json | 125 +++ .../tessie/snapshots/test_sensor.ambr | 944 +++++++++++++++++- tests/components/tessie/test_coordinator.py | 96 +- tests/components/tessie/test_init.py | 14 + 19 files changed, 1910 insertions(+), 98 deletions(-) create mode 100644 tests/components/tessie/fixtures/live_status.json create mode 100644 tests/components/tessie/fixtures/products.json create mode 100644 tests/components/tessie/fixtures/site_info.json diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index 37fb669e54b..e8891d6665f 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -1,9 +1,12 @@ """Tessie integration.""" +import asyncio from http import HTTPStatus import logging from aiohttp import ClientError, ClientResponseError +from tesla_fleet_api import EnergySpecific, Tessie +from tesla_fleet_api.exceptions import TeslaFleetError from tessie_api import get_state_of_all_vehicles from homeassistant.config_entries import ConfigEntry @@ -14,8 +17,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceInfo from .const import DOMAIN, MODELS -from .coordinator import TessieStateUpdateCoordinator -from .models import TessieData, TessieVehicleData +from .coordinator import ( + TessieEnergySiteInfoCoordinator, + TessieEnergySiteLiveCoordinator, + TessieStateUpdateCoordinator, +) +from .models import TessieData, TessieEnergyData, TessieVehicleData PLATFORMS = [ Platform.BINARY_SENSOR, @@ -40,10 +47,11 @@ type TessieConfigEntry = ConfigEntry[TessieData] async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bool: """Set up Tessie config.""" api_key = entry.data[CONF_ACCESS_TOKEN] + session = async_get_clientsession(hass) try: state_of_all_vehicles = await get_state_of_all_vehicles( - session=async_get_clientsession(hass), + session=session, api_key=api_key, only_active=True, ) @@ -84,7 +92,45 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo if vehicle["last_state"] is not None ] - entry.runtime_data = TessieData(vehicles=vehicles) + # Energy Sites + tessie = Tessie(session, api_key) + try: + products = (await tessie.products())["response"] + except TeslaFleetError as e: + raise ConfigEntryNotReady from e + + energysites: list[TessieEnergyData] = [] + for product in products: + if "energy_site_id" in product: + site_id = product["energy_site_id"] + api = EnergySpecific(tessie.energy, site_id) + energysites.append( + TessieEnergyData( + api=api, + id=site_id, + live_coordinator=TessieEnergySiteLiveCoordinator(hass, api), + info_coordinator=TessieEnergySiteInfoCoordinator(hass, api), + device=DeviceInfo( + identifiers={(DOMAIN, str(site_id))}, + manufacturer="Tesla", + name=product.get("site_name", "Energy Site"), + ), + ) + ) + + # Populate coordinator data before forwarding to platforms + await asyncio.gather( + *( + energysite.live_coordinator.async_config_entry_first_refresh() + for energysite in energysites + ), + *( + energysite.info_coordinator.async_config_entry_first_refresh() + for energysite in energysites + ), + ) + + entry.runtime_data = TessieData(vehicles, energysites) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tessie/const.py b/homeassistant/components/tessie/const.py index f717d758f5a..bdb20193613 100644 --- a/homeassistant/components/tessie/const.py +++ b/homeassistant/components/tessie/const.py @@ -79,3 +79,18 @@ TessieChargeStates = { "Disconnected": "disconnected", "NoPower": "no_power", } + + +class TessieWallConnectorStates(IntEnum): + """Tessie Wall Connector states.""" + + BOOTING = 0 + CHARGING = 1 + DISCONNECTED = 2 + CONNECTED = 4 + SCHEDULED = 5 + NEGOTIATING = 6 + ERROR = 7 + CHARGING_FINISHED = 8 + WAITING_CAR = 9 + CHARGING_REDUCED = 10 diff --git a/homeassistant/components/tessie/coordinator.py b/homeassistant/components/tessie/coordinator.py index bea1bf72a8d..4582260bfb2 100644 --- a/homeassistant/components/tessie/coordinator.py +++ b/homeassistant/components/tessie/coordinator.py @@ -6,21 +6,37 @@ import logging from typing import Any from aiohttp import ClientResponseError +from tesla_fleet_api import EnergySpecific +from tesla_fleet_api.exceptions import InvalidToken, MissingToken, TeslaFleetError from tessie_api import get_state, get_status from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import TessieStatus # This matches the update interval Tessie performs server side TESSIE_SYNC_INTERVAL = 10 +TESSIE_FLEET_API_SYNC_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) +def flatten(data: dict[str, Any], parent: str | None = None) -> dict[str, Any]: + """Flatten the data structure.""" + result = {} + for key, value in data.items(): + if parent: + key = f"{parent}_{key}" + if isinstance(value, dict): + result.update(flatten(value, key)) + else: + result[key] = value + return result + + class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Class to manage fetching data from the Tessie API.""" @@ -41,7 +57,7 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.api_key = api_key self.vin = vin self.session = async_get_clientsession(hass) - self.data = self._flatten(data) + self.data = flatten(data) async def _async_update_data(self) -> dict[str, Any]: """Update vehicle data using Tessie API.""" @@ -68,18 +84,61 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise ConfigEntryAuthFailed from e raise - return self._flatten(vehicle) + return flatten(vehicle) - def _flatten( - self, data: dict[str, Any], parent: str | None = None - ) -> dict[str, Any]: - """Flatten the data structure.""" - result = {} - for key, value in data.items(): - if parent: - key = f"{parent}_{key}" - if isinstance(value, dict): - result.update(self._flatten(value, key)) - else: - result[key] = value - return result + +class TessieEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Class to manage fetching energy site live status from the Tessie API.""" + + def __init__(self, hass: HomeAssistant, api: EnergySpecific) -> None: + """Initialize Tessie Energy Site Live coordinator.""" + super().__init__( + hass, + _LOGGER, + name="Tessie Energy Site Live", + update_interval=TESSIE_FLEET_API_SYNC_INTERVAL, + ) + self.api = api + + async def _async_update_data(self) -> dict[str, Any]: + """Update energy site data using Tessie API.""" + + try: + data = (await self.api.live_status())["response"] + except (InvalidToken, MissingToken) as e: + raise ConfigEntryAuthFailed from e + except TeslaFleetError as e: + raise UpdateFailed(e.message) from e + + # Convert Wall Connectors from array to dict + data["wall_connectors"] = { + wc["din"]: wc for wc in (data.get("wall_connectors") or []) + } + + return data + + +class TessieEnergySiteInfoCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Class to manage fetching energy site info from the Tessie API.""" + + def __init__(self, hass: HomeAssistant, api: EnergySpecific) -> None: + """Initialize Tessie Energy Info coordinator.""" + super().__init__( + hass, + _LOGGER, + name="Tessie Energy Site Info", + update_interval=TESSIE_FLEET_API_SYNC_INTERVAL, + ) + self.api = api + + async def _async_update_data(self) -> dict[str, Any]: + """Update energy site data using Tessie API.""" + + try: + data = (await self.api.site_info())["response"] + except (InvalidToken, MissingToken) as e: + raise ConfigEntryAuthFailed from e + except TeslaFleetError as e: + raise UpdateFailed(e.message) from e + + return flatten(data) diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index 1b7ddcbe84c..93b9f10ae67 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -1,36 +1,47 @@ """Tessie parent entity class.""" +from abc import abstractmethod from collections.abc import Awaitable, Callable from typing import Any from aiohttp import ClientResponseError from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import TessieStateUpdateCoordinator -from .models import TessieVehicleData +from .coordinator import ( + TessieEnergySiteInfoCoordinator, + TessieEnergySiteLiveCoordinator, + TessieStateUpdateCoordinator, +) +from .models import TessieEnergyData, TessieVehicleData -class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): - """Parent class for Tessie Entities.""" +class TessieBaseEntity( + CoordinatorEntity[ + TessieStateUpdateCoordinator + | TessieEnergySiteInfoCoordinator + | TessieEnergySiteLiveCoordinator + ] +): + """Parent class for Tessie entities.""" _attr_has_entity_name = True def __init__( self, - vehicle: TessieVehicleData, + coordinator: TessieStateUpdateCoordinator + | TessieEnergySiteInfoCoordinator + | TessieEnergySiteLiveCoordinator, key: str, ) -> None: """Initialize common aspects of a Tessie entity.""" - super().__init__(vehicle.data_coordinator) - self.vin = vehicle.vin - self.key = key + self.key = key self._attr_translation_key = key - self._attr_unique_id = f"{vehicle.vin}-{key}" - self._attr_device_info = vehicle.device + super().__init__(coordinator) @property def _value(self) -> Any: @@ -41,15 +52,53 @@ class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): """Return a specific value from coordinator data.""" return self.coordinator.data.get(key or self.key, default) + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @abstractmethod + def _async_update_attrs(self) -> None: + """Update the attributes of the entity.""" + + +class TessieEntity(TessieBaseEntity): + """Parent class for Tessie vehicle entities.""" + + def __init__( + self, + vehicle: TessieVehicleData, + key: str, + ) -> None: + """Initialize common aspects of a Tessie vehicle entity.""" + self.vin = vehicle.vin + self._session = vehicle.data_coordinator.session + self._api_key = vehicle.data_coordinator.api_key + self._attr_unique_id = f"{vehicle.vin}-{key}" + self._attr_device_info = vehicle.device + + super().__init__(vehicle.data_coordinator, key) + + @property + def _value(self) -> Any: + """Return value from coordinator data.""" + return self.coordinator.data.get(self.key) + + def set(self, *args: Any) -> None: + """Set a value in coordinator data.""" + for key, value in args: + self.coordinator.data[key] = value + self.async_write_ha_state() + async def run( self, func: Callable[..., Awaitable[dict[str, Any]]], **kargs: Any ) -> None: """Run a tessie_api function and handle exceptions.""" try: response = await func( - session=self.coordinator.session, + session=self._session, vin=self.vin, - api_key=self.coordinator.api_key, + api_key=self._api_key, **kargs, ) except ClientResponseError as e: @@ -63,8 +112,55 @@ class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): translation_placeholders={"name": name}, ) - def set(self, *args: Any) -> None: - """Set a value in coordinator data.""" - for key, value in args: - self.coordinator.data[key] = value - self.async_write_ha_state() + def _async_update_attrs(self) -> None: + """Update the attributes of the entity.""" + # Not used in this class yet + + +class TessieEnergyEntity(TessieBaseEntity): + """Parent class for Tessie energy site entities.""" + + def __init__( + self, + data: TessieEnergyData, + coordinator: TessieEnergySiteInfoCoordinator | TessieEnergySiteLiveCoordinator, + key: str, + ) -> None: + """Initialize common aspects of a Tessie energy site entity.""" + + self._attr_unique_id = f"{data.id}-{key}" + self._attr_device_info = data.device + + super().__init__(coordinator, key) + + +class TessieWallConnectorEntity(TessieBaseEntity): + """Parent class for Tessie wall connector entities.""" + + def __init__( + self, + data: TessieEnergyData, + din: str, + key: str, + ) -> None: + """Initialize common aspects of a Teslemetry entity.""" + self.din = din + self._attr_unique_id = f"{data.id}-{din}-{key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, din)}, + manufacturer="Tesla", + name="Wall Connector", + via_device=(DOMAIN, str(data.id)), + serial_number=din.split("-")[-1], + ) + + super().__init__(data.live_coordinator, key) + + @property + def _value(self) -> int: + """Return a specific wall connector value from coordinator data.""" + return ( + self.coordinator.data.get("wall_connectors", {}) + .get(self.din, {}) + .get(self.key) + ) diff --git a/homeassistant/components/tessie/icons.json b/homeassistant/components/tessie/icons.json index 0b1051e662f..2543b3ab9e1 100644 --- a/homeassistant/components/tessie/icons.json +++ b/homeassistant/components/tessie/icons.json @@ -189,6 +189,42 @@ }, "drive_state_active_route_destination": { "default": "mdi:map-marker" + }, + "battery_power": { + "default": "mdi:home-battery" + }, + "energy_left": { + "default": "mdi:battery" + }, + "generator_power": { + "default": "mdi:generator-stationary" + }, + "grid_power": { + "default": "mdi:transmission-tower" + }, + "grid_services_power": { + "default": "mdi:transmission-tower" + }, + "load_power": { + "default": "mdi:power-plug" + }, + "solar_power": { + "default": "mdi:solar-power" + }, + "total_pack_energy": { + "default": "mdi:battery-high" + }, + "vin": { + "default": "mdi:car-electric" + }, + "wall_connector_fault_state": { + "default": "mdi:ev-station" + }, + "wall_connector_power": { + "default": "mdi:ev-station" + }, + "wall_connector_state": { + "default": "mdi:ev-station" } }, "switch": { diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json index 52fc8dd5be1..bf1ab5f61e4 100644 --- a/homeassistant/components/tessie/manifest.json +++ b/homeassistant/components/tessie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tessie", "iot_class": "cloud_polling", "loggers": ["tessie"], - "requirements": ["tessie-api==0.0.9"] + "requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.1"] } diff --git a/homeassistant/components/tessie/models.py b/homeassistant/components/tessie/models.py index e96562ff8e1..ca670b9650b 100644 --- a/homeassistant/components/tessie/models.py +++ b/homeassistant/components/tessie/models.py @@ -4,9 +4,15 @@ from __future__ import annotations from dataclasses import dataclass +from tesla_fleet_api import EnergySpecific + from homeassistant.helpers.device_registry import DeviceInfo -from .coordinator import TessieStateUpdateCoordinator +from .coordinator import ( + TessieEnergySiteInfoCoordinator, + TessieEnergySiteLiveCoordinator, + TessieStateUpdateCoordinator, +) @dataclass @@ -14,6 +20,18 @@ class TessieData: """Data for the Tessie integration.""" vehicles: list[TessieVehicleData] + energysites: list[TessieEnergyData] + + +@dataclass +class TessieEnergyData: + """Data for a Energy Site in the Tessie integration.""" + + api: EnergySpecific + live_coordinator: TessieEnergySiteLiveCoordinator + info_coordinator: TessieEnergySiteInfoCoordinator + id: int + device: DeviceInfo @dataclass diff --git a/homeassistant/components/tessie/sensor.py b/homeassistant/components/tessie/sensor.py index dc910c7a03a..586162fe779 100644 --- a/homeassistant/components/tessie/sensor.py +++ b/homeassistant/components/tessie/sensor.py @@ -5,6 +5,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta +from itertools import chain from typing import cast from homeassistant.components.sensor import ( @@ -33,9 +34,9 @@ from homeassistant.util import dt as dt_util from homeassistant.util.variance import ignore_variance from . import TessieConfigEntry -from .const import TessieChargeStates -from .entity import TessieEntity -from .models import TessieVehicleData +from .const import TessieChargeStates, TessieWallConnectorStates +from .entity import TessieEnergyEntity, TessieEntity, TessieWallConnectorEntity +from .models import TessieEnergyData, TessieVehicleData @callback @@ -257,6 +258,115 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( ), ) +ENERGY_LIVE_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( + TessieSensorEntityDescription( + key="solar_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + TessieSensorEntityDescription( + key="energy_left", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY_STORAGE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TessieSensorEntityDescription( + key="total_pack_energy", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY_STORAGE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TessieSensorEntityDescription( + key="percentage_charged", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + suggested_display_precision=2, + ), + TessieSensorEntityDescription( + key="battery_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + TessieSensorEntityDescription( + key="load_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + TessieSensorEntityDescription( + key="grid_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + TessieSensorEntityDescription( + key="grid_services_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + TessieSensorEntityDescription( + key="generator_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + ), +) + +WALL_CONNECTOR_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( + TessieSensorEntityDescription( + key="wall_connector_state", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.ENUM, + value_fn=lambda x: TessieWallConnectorStates(cast(int, x)).name.lower(), + options=[state.name.lower() for state in TessieWallConnectorStates], + ), + TessieSensorEntityDescription( + key="wall_connector_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + TessieSensorEntityDescription( + key="vin", + ), +) + +ENERGY_INFO_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( + TessieSensorEntityDescription( + key="vpp_backup_reserve_percent", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -264,17 +374,38 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Tessie sensor platform from a config entry.""" - data = entry.runtime_data async_add_entities( - TessieSensorEntity(vehicle, description) - for vehicle in data.vehicles - for description in DESCRIPTIONS + chain( + ( # Add vehicles + TessieVehicleSensorEntity(vehicle, description) + for vehicle in entry.runtime_data.vehicles + for description in DESCRIPTIONS + ), + ( # Add energy site info + TessieEnergyInfoSensorEntity(energysite, description) + for energysite in entry.runtime_data.energysites + for description in ENERGY_INFO_DESCRIPTIONS + if description.key in energysite.info_coordinator.data + ), + ( # Add energy site live + TessieEnergyLiveSensorEntity(energysite, description) + for energysite in entry.runtime_data.energysites + for description in ENERGY_LIVE_DESCRIPTIONS + if description.key in energysite.live_coordinator.data + ), + ( # Add wall connectors + TessieWallConnectorSensorEntity(energysite, din, description) + for energysite in entry.runtime_data.energysites + for din in energysite.live_coordinator.data.get("wall_connectors", {}) + for description in WALL_CONNECTOR_DESCRIPTIONS + ), + ) ) -class TessieSensorEntity(TessieEntity, SensorEntity): - """Base class for Tessie metric sensors.""" +class TessieVehicleSensorEntity(TessieEntity, SensorEntity): + """Base class for Tessie sensor entities.""" entity_description: TessieSensorEntityDescription @@ -284,8 +415,8 @@ class TessieSensorEntity(TessieEntity, SensorEntity): description: TessieSensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(vehicle, description.key) self.entity_description = description + super().__init__(vehicle, description.key) @property def native_value(self) -> StateType | datetime: @@ -296,3 +427,68 @@ class TessieSensorEntity(TessieEntity, SensorEntity): def available(self) -> bool: """Return if sensor is available.""" return super().available and self.entity_description.available_fn(self.get()) + + +class TessieEnergyLiveSensorEntity(TessieEnergyEntity, SensorEntity): + """Base class for Tessie energy site sensor entity.""" + + entity_description: TessieSensorEntityDescription + + def __init__( + self, + data: TessieEnergyData, + description: TessieSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + self.entity_description = description + super().__init__(data, data.live_coordinator, description.key) + + def _async_update_attrs(self) -> None: + """Update the attributes of the sensor.""" + self._attr_available = self._value is not None + self._attr_native_value = self.entity_description.value_fn(self._value) + + +class TessieEnergyInfoSensorEntity(TessieEnergyEntity, SensorEntity): + """Base class for Tessie energy site sensor entity.""" + + entity_description: TessieSensorEntityDescription + + def __init__( + self, + data: TessieEnergyData, + description: TessieSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + self.entity_description = description + super().__init__(data, data.info_coordinator, description.key) + + def _async_update_attrs(self) -> None: + """Update the attributes of the sensor.""" + self._attr_available = self._value is not None + self._attr_native_value = self._value + + +class TessieWallConnectorSensorEntity(TessieWallConnectorEntity, SensorEntity): + """Base class for Tessie wall connector sensor entity.""" + + entity_description: TessieSensorEntityDescription + + def __init__( + self, + data: TessieEnergyData, + din: str, + description: TessieSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + self.entity_description = description + super().__init__( + data, + din, + description.key, + ) + + def _async_update_attrs(self) -> None: + """Update the attributes of the sensor.""" + self._attr_available = self._value is not None + self._attr_native_value = self.entity_description.value_fn(self._value) diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index ea75660ddb7..8e617f137dc 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -167,6 +167,60 @@ }, "drive_state_active_route_destination": { "name": "Destination" + }, + "battery_power": { + "name": "Battery power" + }, + "energy_left": { + "name": "Energy left" + }, + "generator_power": { + "name": "Generator power" + }, + "grid_power": { + "name": "Grid power" + }, + "grid_services_power": { + "name": "Grid services power" + }, + "load_power": { + "name": "Load power" + }, + "percentage_charged": { + "name": "Percentage charged" + }, + "solar_power": { + "name": "Solar power" + }, + "total_pack_energy": { + "name": "Total pack energy" + }, + "vin": { + "name": "Vehicle" + }, + "vpp_backup_reserve_percent": { + "name": "VPP backup reserve" + }, + "wall_connector_fault_state": { + "name": "Fault state code" + }, + "wall_connector_power": { + "name": "Power" + }, + "wall_connector_state": { + "name": "State", + "state": { + "booting": "Booting", + "charging": "[%key:component::tessie::entity::sensor::charge_state_charging_state::state::charging%]", + "disconnected": "[%key:common::state::disconnected%]", + "connected": "[%key:common::state::connected%]", + "scheduled": "Scheduled", + "negotiating": "Negotiating", + "error": "Error", + "charging_finished": "Charging finished", + "waiting_car": "Waiting car", + "charging_reduced": "Charging reduced" + } } }, "cover": { diff --git a/requirements_all.txt b/requirements_all.txt index 2ec7f38e5e3..f4de7dafa87 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2713,6 +2713,7 @@ temperusb==1.6.1 # tensorflow==2.5.0 # homeassistant.components.teslemetry +# homeassistant.components.tessie tesla-fleet-api==0.6.1 # homeassistant.components.powerwall diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0e856c2f6f..12ab15a7d76 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2111,6 +2111,7 @@ temescal==0.5 temperusb==1.6.1 # homeassistant.components.teslemetry +# homeassistant.components.tessie tesla-fleet-api==0.6.1 # homeassistant.components.powerwall diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index c19f6f65201..3d24c6b233a 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -16,6 +16,7 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType from tests.common import MockConfigEntry, load_json_object_fixture +# Tessie library TEST_STATE_OF_ALL_VEHICLES = load_json_object_fixture("vehicles.json", DOMAIN) TEST_VEHICLE_STATE_ONLINE = load_json_object_fixture("online.json", DOMAIN) TEST_VEHICLE_STATUS_AWAKE = {"status": TessieStatus.AWAKE} @@ -47,6 +48,13 @@ ERROR_VIRTUAL_KEY = ClientResponseError( ) ERROR_CONNECTION = ClientConnectionError() +# Fleet API library +PRODUCTS = load_json_object_fixture("products.json", DOMAIN) +LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN) +SITE_INFO = load_json_object_fixture("site_info.json", DOMAIN) +RESPONSE_OK = {"response": {}, "error": None} +COMMAND_OK = {"response": {"result": True, "reason": ""}} + async def setup_platform( hass: HomeAssistant, platforms: list[Platform] | UndefinedType = UNDEFINED diff --git a/tests/components/tessie/conftest.py b/tests/components/tessie/conftest.py index 77d1e3fd3e2..79cc9aa44c6 100644 --- a/tests/components/tessie/conftest.py +++ b/tests/components/tessie/conftest.py @@ -2,16 +2,23 @@ from __future__ import annotations +from copy import deepcopy from unittest.mock import patch import pytest from .common import ( + COMMAND_OK, + LIVE_STATUS, + PRODUCTS, + SITE_INFO, TEST_STATE_OF_ALL_VEHICLES, TEST_VEHICLE_STATE_ONLINE, TEST_VEHICLE_STATUS_AWAKE, ) +# Tessie + @pytest.fixture(autouse=True) def mock_get_state(): @@ -41,3 +48,43 @@ def mock_get_state_of_all_vehicles(): return_value=TEST_STATE_OF_ALL_VEHICLES, ) as mock_get_state_of_all_vehicles: yield mock_get_state_of_all_vehicles + + +# Fleet API +@pytest.fixture(autouse=True) +def mock_products(): + """Mock Tesla Fleet Api products method.""" + with patch( + "homeassistant.components.tessie.Tessie.products", return_value=PRODUCTS + ) as mock_products: + yield mock_products + + +@pytest.fixture(autouse=True) +def mock_request(): + """Mock Tesla Fleet API request method.""" + with patch( + "homeassistant.components.tessie.Tessie._request", + return_value=COMMAND_OK, + ) as mock_request: + yield mock_request + + +@pytest.fixture(autouse=True) +def mock_live_status(): + """Mock Tesla Fleet API EnergySpecific live_status method.""" + with patch( + "homeassistant.components.tessie.EnergySpecific.live_status", + side_effect=lambda: deepcopy(LIVE_STATUS), + ) as mock_live_status: + yield mock_live_status + + +@pytest.fixture(autouse=True) +def mock_site_info(): + """Mock Tesla Fleet API EnergySpecific site_info method.""" + with patch( + "homeassistant.components.tessie.EnergySpecific.site_info", + side_effect=lambda: deepcopy(SITE_INFO), + ) as mock_live_status: + yield mock_live_status diff --git a/tests/components/tessie/fixtures/live_status.json b/tests/components/tessie/fixtures/live_status.json new file mode 100644 index 00000000000..486f9f4fadd --- /dev/null +++ b/tests/components/tessie/fixtures/live_status.json @@ -0,0 +1,33 @@ +{ + "response": { + "solar_power": 1185, + "energy_left": 38896.47368421053, + "total_pack_energy": 40727, + "percentage_charged": 95.50537403739663, + "backup_capable": true, + "battery_power": 5060, + "load_power": 6245, + "grid_status": "Active", + "grid_services_active": false, + "grid_power": 0, + "grid_services_power": 0, + "generator_power": 0, + "island_status": "on_grid", + "storm_mode_active": false, + "timestamp": "2024-01-01T00:00:00+00:00", + "wall_connectors": [ + { + "din": "abd-123", + "wall_connector_state": 2, + "wall_connector_fault_state": 2, + "wall_connector_power": 0 + }, + { + "din": "bcd-234", + "wall_connector_state": 2, + "wall_connector_fault_state": 2, + "wall_connector_power": 0 + } + ] + } +} diff --git a/tests/components/tessie/fixtures/products.json b/tests/components/tessie/fixtures/products.json new file mode 100644 index 00000000000..e1b76e4cefb --- /dev/null +++ b/tests/components/tessie/fixtures/products.json @@ -0,0 +1,121 @@ +{ + "response": [ + { + "id": 1234, + "user_id": 1234, + "vehicle_id": 1234, + "vin": "LRWXF7EK4KC700000", + "color": null, + "access_type": "OWNER", + "display_name": "Test", + "option_codes": null, + "cached_data": null, + "granular_access": { "hide_private": false }, + "tokens": ["abc", "def"], + "state": "asleep", + "in_service": false, + "id_s": "1234", + "calendar_enabled": true, + "api_version": 71, + "backseat_token": null, + "backseat_token_updated_at": null, + "ble_autopair_enrolled": false, + "vehicle_config": { + "aux_park_lamps": "Eu", + "badge_version": 1, + "can_accept_navigation_requests": true, + "can_actuate_trunks": true, + "car_special_type": "base", + "car_type": "model3", + "charge_port_type": "CCS", + "cop_user_set_temp_supported": false, + "dashcam_clip_save_supported": true, + "default_charge_to_max": false, + "driver_assist": "TeslaAP3", + "ece_restrictions": false, + "efficiency_package": "M32021", + "eu_vehicle": true, + "exterior_color": "DeepBlue", + "exterior_trim": "Black", + "exterior_trim_override": "", + "has_air_suspension": false, + "has_ludicrous_mode": false, + "has_seat_cooling": false, + "headlamp_type": "Global", + "interior_trim_type": "White2", + "key_version": 2, + "motorized_charge_port": true, + "paint_color_override": "0,9,25,0.7,0.04", + "performance_package": "Base", + "plg": true, + "pws": true, + "rear_drive_unit": "PM216MOSFET", + "rear_seat_heaters": 1, + "rear_seat_type": 0, + "rhd": true, + "roof_color": "RoofColorGlass", + "seat_type": null, + "spoiler_type": "None", + "sun_roof_installed": null, + "supports_qr_pairing": false, + "third_row_seats": "None", + "timestamp": 1705701487912, + "trim_badging": "74d", + "use_range_badging": true, + "utc_offset": 36000, + "webcam_selfie_supported": true, + "webcam_supported": true, + "wheel_type": "Pinwheel18CapKit" + }, + "command_signing": "allowed", + "release_notes_supported": true + }, + { + "energy_site_id": 123456, + "resource_type": "battery", + "site_name": "Energy Site", + "id": "ABC123", + "gateway_id": "ABC123", + "asset_site_id": "c0ffee", + "warp_site_number": "GA123456", + "energy_left": 23286.105263157893, + "total_pack_energy": 40804, + "percentage_charged": 57.068192488868476, + "battery_type": "ac_powerwall", + "backup_capable": true, + "battery_power": 14990, + "go_off_grid_test_banner_enabled": null, + "storm_mode_enabled": true, + "powerwall_onboarding_settings_set": true, + "powerwall_tesla_electric_interested_in": null, + "vpp_tour_enabled": null, + "sync_grid_alert_enabled": true, + "breaker_alert_enabled": true, + "components": { + "battery": true, + "battery_type": "ac_powerwall", + "solar": true, + "solar_type": "pv_panel", + "grid": true, + "load_meter": true, + "market_type": "residential", + "wall_connectors": [ + { + "device_id": "abc-123", + "din": "123-abc", + "is_active": true + }, + { + "device_id": "bcd-234", + "din": "234-bcd", + "is_active": true + } + ] + }, + "features": { + "rate_plan_manager_no_pricing_constraint": true + } + } + ], + "count": 2 +} diff --git a/tests/components/tessie/fixtures/site_info.json b/tests/components/tessie/fixtures/site_info.json new file mode 100644 index 00000000000..f581707ff14 --- /dev/null +++ b/tests/components/tessie/fixtures/site_info.json @@ -0,0 +1,125 @@ +{ + "response": { + "id": "1233-abcd", + "site_name": "Site", + "backup_reserve_percent": 0, + "default_real_mode": "self_consumption", + "installation_date": "2022-01-01T00:00:00+00:00", + "user_settings": { + "go_off_grid_test_banner_enabled": false, + "storm_mode_enabled": true, + "powerwall_onboarding_settings_set": true, + "powerwall_tesla_electric_interested_in": false, + "vpp_tour_enabled": true, + "sync_grid_alert_enabled": true, + "breaker_alert_enabled": false + }, + "components": { + "solar": true, + "solar_type": "pv_panel", + "battery": true, + "grid": true, + "backup": true, + "gateway": "teg", + "load_meter": true, + "tou_capable": true, + "storm_mode_capable": true, + "flex_energy_request_capable": false, + "car_charging_data_supported": false, + "off_grid_vehicle_charging_reserve_supported": true, + "vehicle_charging_performance_view_enabled": false, + "vehicle_charging_solar_offset_view_enabled": false, + "battery_solar_offset_view_enabled": true, + "solar_value_enabled": true, + "energy_value_header": "Energy Value", + "energy_value_subheader": "Estimated Value", + "energy_service_self_scheduling_enabled": true, + "show_grid_import_battery_source_cards": true, + "set_islanding_mode_enabled": true, + "wifi_commissioning_enabled": true, + "backup_time_remaining_enabled": true, + "battery_type": "ac_powerwall", + "configurable": true, + "grid_services_enabled": false, + "gateways": [ + { + "device_id": "gateway-id", + "din": "gateway-din", + "serial_number": "CN00000000J50D", + "part_number": "1152100-14-J", + "part_type": 10, + "part_name": "Tesla Backup Gateway 2", + "is_active": true, + "site_id": "1234-abcd", + "firmware_version": "24.4.0 0fe780c9", + "updated_datetime": "2024-05-14T00:00:00.000Z" + } + ], + "batteries": [ + { + "device_id": "battery-1-id", + "din": "battery-1-din", + "serial_number": "TG000000001DA5", + "part_number": "3012170-10-B", + "part_type": 2, + "part_name": "Powerwall 2", + "nameplate_max_charge_power": 5000, + "nameplate_max_discharge_power": 5000, + "nameplate_energy": 13500 + }, + { + "device_id": "battery-2-id", + "din": "battery-2-din", + "serial_number": "TG000000002DA5", + "part_number": "3012170-05-C", + "part_type": 2, + "part_name": "Powerwall 2", + "nameplate_max_charge_power": 5000, + "nameplate_max_discharge_power": 5000, + "nameplate_energy": 13500 + } + ], + "wall_connectors": [ + { + "device_id": "123abc", + "din": "abc123", + "is_active": true + }, + { + "device_id": "234bcd", + "din": "bcd234", + "is_active": true + } + ], + "disallow_charge_from_grid_with_solar_installed": true, + "customer_preferred_export_rule": "pv_only", + "net_meter_mode": "battery_ok", + "system_alerts_enabled": true + }, + "version": "23.44.0 eb113390", + "battery_count": 2, + "tou_settings": { + "optimization_strategy": "economics", + "schedule": [ + { + "target": "off_peak", + "week_days": [1, 0], + "start_seconds": 0, + "end_seconds": 3600 + }, + { + "target": "peak", + "week_days": [1, 0], + "start_seconds": 3600, + "end_seconds": 0 + } + ] + }, + "nameplate_power": 15000, + "nameplate_energy": 40500, + "installation_time_zone": "", + "max_site_meter_power_ac": 1000000000, + "min_site_meter_power_ac": -1000000000, + "vpp_backup_reserve_percent": 0 + } +} diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index 48beab6133c..ba7b4eae0a5 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -1,4 +1,562 @@ # serializer version: 1 +# name: test_sensors[sensor.energy_site_battery_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_battery_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_power', + 'unique_id': '123456-battery_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_battery_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Battery power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_energy_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.energy_site_energy_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy left', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_left', + 'unique_id': '123456-energy_left', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_energy_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Energy Site Energy left', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_energy_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_generator_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_generator_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Generator power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'generator_power', + 'unique_id': '123456-generator_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_generator_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Generator power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_generator_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_power', + 'unique_id': '123456-grid_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Grid power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_services_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid services power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_services_power', + 'unique_id': '123456-grid_services_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Grid services power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_services_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_load_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_load_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Load power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'load_power', + 'unique_id': '123456-load_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_load_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Load power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_load_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_percentage_charged-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_percentage_charged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Percentage charged', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'percentage_charged', + 'unique_id': '123456-percentage_charged', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.energy_site_percentage_charged-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Energy Site Percentage charged', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.energy_site_percentage_charged', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_solar_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_solar_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Solar power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_power', + 'unique_id': '123456-solar_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_solar_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Solar power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_solar_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_total_pack_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.energy_site_total_pack_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total pack energy', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_pack_energy', + 'unique_id': '123456-total_pack_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_total_pack_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Energy Site Total pack energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_total_pack_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.energy_site_vpp_backup_reserve-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.energy_site_vpp_backup_reserve', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VPP backup reserve', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vpp_backup_reserve_percent', + 'unique_id': '123456-vpp_backup_reserve_percent', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.energy_site_vpp_backup_reserve-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Energy Site VPP backup reserve', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.energy_site_vpp_backup_reserve', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_sensors[sensor.test_battery_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -592,42 +1150,6 @@ 'state': 'Giga Texas', }) # --- -# name: test_sensors[sensor.test_distance-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_distance', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Distance', - 'platform': 'tessie', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'charge_state_est_battery_range', - 'unique_id': 'VINVINVIN-charge_state_est_battery_range', - 'unit_of_measurement': , - }) -# --- # name: test_sensors[sensor.test_distance_to_arrival-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1544,3 +2066,353 @@ 'state': '0', }) # --- +# name: test_sensors[sensor.wall_connector_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_power', + 'unique_id': '123456-abd-123-wall_connector_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.wall_connector_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wall Connector Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wall_connector_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_power_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_power_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_power', + 'unique_id': '123456-bcd-234-wall_connector_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.wall_connector_power_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wall Connector Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wall_connector_power_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'booting', + 'charging', + 'disconnected', + 'connected', + 'scheduled', + 'negotiating', + 'error', + 'charging_finished', + 'waiting_car', + 'charging_reduced', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wall_connector_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'State', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_state', + 'unique_id': '123456-abd-123-wall_connector_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Wall Connector State', + 'options': list([ + 'booting', + 'charging', + 'disconnected', + 'connected', + 'scheduled', + 'negotiating', + 'error', + 'charging_finished', + 'waiting_car', + 'charging_reduced', + ]), + }), + 'context': , + 'entity_id': 'sensor.wall_connector_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_state_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'booting', + 'charging', + 'disconnected', + 'connected', + 'scheduled', + 'negotiating', + 'error', + 'charging_finished', + 'waiting_car', + 'charging_reduced', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wall_connector_state_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'State', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_state', + 'unique_id': '123456-bcd-234-wall_connector_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_state_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Wall Connector State', + 'options': list([ + 'booting', + 'charging', + 'disconnected', + 'connected', + 'scheduled', + 'negotiating', + 'error', + 'charging_finished', + 'waiting_car', + 'charging_reduced', + ]), + }), + 'context': , + 'entity_id': 'sensor.wall_connector_state_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_vehicle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Vehicle', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vin', + 'unique_id': '123456-abd-123-vin', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Vehicle', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_vehicle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_vehicle_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Vehicle', + 'platform': 'tessie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vin', + 'unique_id': '123456-bcd-234-vin', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Vehicle', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_vehicle_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/tessie/test_coordinator.py b/tests/components/tessie/test_coordinator.py index c4c1b6d1e72..77b2829b53a 100644 --- a/tests/components/tessie/test_coordinator.py +++ b/tests/components/tessie/test_coordinator.py @@ -2,11 +2,17 @@ from datetime import timedelta +from freezegun.api import FrozenDateTimeFactory +from tesla_fleet_api.exceptions import Forbidden, InvalidToken + from homeassistant.components.tessie import PLATFORMS -from homeassistant.components.tessie.coordinator import TESSIE_SYNC_INTERVAL +from homeassistant.components.tessie.coordinator import ( + TESSIE_FLEET_API_SYNC_INTERVAL, + TESSIE_SYNC_INTERVAL, +) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant -from homeassistant.util.dt import utcnow from .common import ( ERROR_AUTH, @@ -22,60 +28,124 @@ WAIT = timedelta(seconds=TESSIE_SYNC_INTERVAL) async def test_coordinator_online( - hass: HomeAssistant, mock_get_state, mock_get_status + hass: HomeAssistant, mock_get_state, mock_get_status, freezer: FrozenDateTimeFactory ) -> None: """Tests that the coordinator handles online vehicles.""" await setup_platform(hass, PLATFORMS) - async_fire_time_changed(hass, utcnow() + WAIT) + freezer.tick(WAIT) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_get_status.assert_called_once() mock_get_state.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_ON -async def test_coordinator_asleep(hass: HomeAssistant, mock_get_status) -> None: +async def test_coordinator_asleep( + hass: HomeAssistant, mock_get_status, freezer: FrozenDateTimeFactory +) -> None: """Tests that the coordinator handles asleep vehicles.""" await setup_platform(hass, [Platform.BINARY_SENSOR]) mock_get_status.return_value = TEST_VEHICLE_STATUS_ASLEEP - async_fire_time_changed(hass, utcnow() + WAIT) + freezer.tick(WAIT) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_OFF -async def test_coordinator_clienterror(hass: HomeAssistant, mock_get_status) -> None: +async def test_coordinator_clienterror( + hass: HomeAssistant, mock_get_status, freezer: FrozenDateTimeFactory +) -> None: """Tests that the coordinator handles client errors.""" mock_get_status.side_effect = ERROR_UNKNOWN await setup_platform(hass, [Platform.BINARY_SENSOR]) - async_fire_time_changed(hass, utcnow() + WAIT) + freezer.tick(WAIT) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE -async def test_coordinator_auth(hass: HomeAssistant, mock_get_status) -> None: - """Tests that the coordinator handles timeout errors.""" +async def test_coordinator_auth( + hass: HomeAssistant, mock_get_status, freezer: FrozenDateTimeFactory +) -> None: + """Tests that the coordinator handles auth errors.""" mock_get_status.side_effect = ERROR_AUTH await setup_platform(hass, [Platform.BINARY_SENSOR]) - async_fire_time_changed(hass, utcnow() + WAIT) + freezer.tick(WAIT) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_get_status.assert_called_once() -async def test_coordinator_connection(hass: HomeAssistant, mock_get_status) -> None: +async def test_coordinator_connection( + hass: HomeAssistant, mock_get_status, freezer: FrozenDateTimeFactory +) -> None: """Tests that the coordinator handles connection errors.""" mock_get_status.side_effect = ERROR_CONNECTION await setup_platform(hass, [Platform.BINARY_SENSOR]) - async_fire_time_changed(hass, utcnow() + WAIT) + freezer.tick(WAIT) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE + + +async def test_coordinator_live_error( + hass: HomeAssistant, mock_live_status, freezer: FrozenDateTimeFactory +) -> None: + """Tests that the energy live coordinator handles fleet errors.""" + + await setup_platform(hass, [Platform.SENSOR]) + + mock_live_status.reset_mock() + mock_live_status.side_effect = Forbidden + freezer.tick(TESSIE_FLEET_API_SYNC_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + mock_live_status.assert_called_once() + assert hass.states.get("sensor.energy_site_solar_power").state == STATE_UNAVAILABLE + + +async def test_coordinator_info_error( + hass: HomeAssistant, mock_site_info, freezer: FrozenDateTimeFactory +) -> None: + """Tests that the energy info coordinator handles fleet errors.""" + + await setup_platform(hass, [Platform.SENSOR]) + + mock_site_info.reset_mock() + mock_site_info.side_effect = Forbidden + freezer.tick(TESSIE_FLEET_API_SYNC_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + mock_site_info.assert_called_once() + assert ( + hass.states.get("sensor.energy_site_vpp_backup_reserve").state + == STATE_UNAVAILABLE + ) + + +async def test_coordinator_live_reauth(hass: HomeAssistant, mock_live_status) -> None: + """Tests that the energy live coordinator handles auth errors.""" + + mock_live_status.side_effect = InvalidToken + entry = await setup_platform(hass, [Platform.SENSOR]) + assert entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_coordinator_info_reauth(hass: HomeAssistant, mock_site_info) -> None: + """Tests that the energy info coordinator handles auth errors.""" + + mock_site_info.side_effect = InvalidToken + entry = await setup_platform(hass, [Platform.SENSOR]) + assert entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/tessie/test_init.py b/tests/components/tessie/test_init.py index 81d1d758edf..e37512ea8c4 100644 --- a/tests/components/tessie/test_init.py +++ b/tests/components/tessie/test_init.py @@ -1,5 +1,9 @@ """Test the Tessie init.""" +from unittest.mock import patch + +from tesla_fleet_api.exceptions import TeslaFleetError + from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -44,3 +48,13 @@ async def test_connection_failure( mock_get_state_of_all_vehicles.side_effect = ERROR_CONNECTION entry = await setup_platform(hass) assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_fleet_error(hass: HomeAssistant) -> None: + """Test init with a fleet error.""" + + with patch( + "homeassistant.components.tessie.Tessie.products", side_effect=TeslaFleetError + ): + entry = await setup_platform(hass) + assert entry.state is ConfigEntryState.SETUP_RETRY From b9be491016286840d436eed0bd9213417f463edc Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 26 Jun 2024 08:32:26 -0400 Subject: [PATCH 0241/2411] Add options flow to Roborock (#104345) Co-authored-by: Robert Resch --- homeassistant/components/roborock/__init__.py | 10 ++- .../components/roborock/config_flow.py | 61 +++++++++++++++++-- homeassistant/components/roborock/const.py | 27 ++++++-- homeassistant/components/roborock/image.py | 20 ++++-- .../components/roborock/strings.json | 26 ++++++++ tests/components/roborock/test_config_flow.py | 25 +++++++- 6 files changed, 152 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index d7ce0e0f5ec..cdbddbda95b 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -31,6 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up roborock from a config entry.""" _LOGGER.debug("Integration async setup entry: %s", entry.as_dict()) + entry.async_on_unload(entry.add_update_listener(update_listener)) user_data = UserData.from_dict(entry.data[CONF_USER_DATA]) api_client = RoborockApiClient(entry.data[CONF_USERNAME], entry.data[CONF_BASE_URL]) @@ -50,8 +51,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: translation_key="home_data_fail", ) from err _LOGGER.debug("Got home data %s", home_data) + all_devices: list[HomeDataDevice] = home_data.devices + home_data.received_devices device_map: dict[str, HomeDataDevice] = { - device.duid: device for device in home_data.devices + home_data.received_devices + device.duid: device for device in all_devices } product_info: dict[str, HomeDataProduct] = { product.id: product for product in home_data.products @@ -177,3 +179,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) await asyncio.gather(*release_tasks) return unload_ok + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + # Reload entry to update data + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/roborock/config_flow.py b/homeassistant/components/roborock/config_flow.py index c7347178612..2b409bdf8c4 100644 --- a/homeassistant/components/roborock/config_flow.py +++ b/homeassistant/components/roborock/config_flow.py @@ -17,10 +17,24 @@ from roborock.exceptions import ( from roborock.web_api import RoborockApiClient import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, + OptionsFlowWithConfigEntry, +) from homeassistant.const import CONF_USERNAME +from homeassistant.core import callback -from .const import CONF_BASE_URL, CONF_ENTRY_CODE, CONF_USER_DATA, DOMAIN +from .const import ( + CONF_BASE_URL, + CONF_ENTRY_CODE, + CONF_USER_DATA, + DEFAULT_DRAWABLES, + DOMAIN, + DRAWABLES, +) _LOGGER = logging.getLogger(__name__) @@ -107,9 +121,6 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN): CONF_USER_DATA: login_data.as_dict(), }, ) - await self.hass.config_entries.async_reload( - self.reauth_entry.entry_id - ) return self.async_abort(reason="reauth_successful") return self._create_entry(self._client, self._username, login_data) @@ -154,3 +165,43 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN): CONF_BASE_URL: client.base_url, }, ) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlow: + """Create the options flow.""" + return RoborockOptionsFlowHandler(config_entry) + + +class RoborockOptionsFlowHandler(OptionsFlowWithConfigEntry): + """Handle an option flow for Roborock.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options.""" + return await self.async_step_drawables() + + async def async_step_drawables( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the map object drawable options.""" + if user_input is not None: + self.options.setdefault(DRAWABLES, {}).update(user_input) + return self.async_create_entry(title="", data=self.options) + data_schema = {} + for drawable, default_value in DEFAULT_DRAWABLES.items(): + data_schema[ + vol.Required( + drawable.value, + default=self.config_entry.options.get(DRAWABLES, {}).get( + drawable, default_value + ), + ) + ] = bool + return self.async_show_form( + step_id=DRAWABLES, + data_schema=vol.Schema(data_schema), + ) diff --git a/homeassistant/components/roborock/const.py b/homeassistant/components/roborock/const.py index 6b1ed975fca..834b25965c3 100644 --- a/homeassistant/components/roborock/const.py +++ b/homeassistant/components/roborock/const.py @@ -9,6 +9,28 @@ CONF_ENTRY_CODE = "code" CONF_BASE_URL = "base_url" CONF_USER_DATA = "user_data" +# Option Flow steps +DRAWABLES = "drawables" + +DEFAULT_DRAWABLES = { + Drawable.CHARGER: True, + Drawable.CLEANED_AREA: False, + Drawable.GOTO_PATH: False, + Drawable.IGNORED_OBSTACLES: False, + Drawable.IGNORED_OBSTACLES_WITH_PHOTO: False, + Drawable.MOP_PATH: False, + Drawable.NO_CARPET_AREAS: False, + Drawable.NO_GO_AREAS: False, + Drawable.NO_MOPPING_AREAS: False, + Drawable.OBSTACLES: False, + Drawable.OBSTACLES_WITH_PHOTO: False, + Drawable.PATH: True, + Drawable.PREDICTED_PATH: False, + Drawable.VACUUM_POSITION: True, + Drawable.VIRTUAL_WALLS: False, + Drawable.ZONES: False, +} + PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, @@ -21,11 +43,6 @@ PLATFORMS = [ Platform.VACUUM, ] -IMAGE_DRAWABLES: list[Drawable] = [ - Drawable.PATH, - Drawable.CHARGER, - Drawable.VACUUM_POSITION, -] IMAGE_CACHE_INTERVAL = 90 diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index 33b8b0a2c90..9dfe8d53cd3 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -7,6 +7,7 @@ from itertools import chain from roborock import RoborockCommand from vacuum_map_parser_base.config.color import ColorsPalette +from vacuum_map_parser_base.config.drawable import Drawable from vacuum_map_parser_base.config.image_config import ImageConfig from vacuum_map_parser_base.config.size import Sizes from vacuum_map_parser_roborock.map_data_parser import RoborockMapDataParser @@ -20,7 +21,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify import homeassistant.util.dt as dt_util -from .const import DOMAIN, IMAGE_CACHE_INTERVAL, IMAGE_DRAWABLES, MAP_SLEEP +from .const import DEFAULT_DRAWABLES, DOMAIN, DRAWABLES, IMAGE_CACHE_INTERVAL, MAP_SLEEP from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockCoordinatedEntity @@ -35,10 +36,18 @@ async def async_setup_entry( coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ config_entry.entry_id ] + drawables = [ + drawable + for drawable, default_value in DEFAULT_DRAWABLES.items() + if config_entry.options.get(DRAWABLES, {}).get(drawable, default_value) + ] entities = list( chain.from_iterable( await asyncio.gather( - *(create_coordinator_maps(coord) for coord in coordinators.values()) + *( + create_coordinator_maps(coord, drawables) + for coord in coordinators.values() + ) ) ) ) @@ -58,13 +67,14 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): map_flag: int, starting_map: bytes, map_name: str, + drawables: list[Drawable], ) -> None: """Initialize a Roborock map.""" RoborockCoordinatedEntity.__init__(self, unique_id, coordinator) ImageEntity.__init__(self, coordinator.hass) self._attr_name = map_name self.parser = RoborockMapDataParser( - ColorsPalette(), Sizes(), IMAGE_DRAWABLES, ImageConfig(), [] + ColorsPalette(), Sizes(), drawables, ImageConfig(), [] ) self._attr_image_last_updated = dt_util.utcnow() self.map_flag = map_flag @@ -140,7 +150,7 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): async def create_coordinator_maps( - coord: RoborockDataUpdateCoordinator, + coord: RoborockDataUpdateCoordinator, drawables: list[Drawable] ) -> list[RoborockMap]: """Get the starting map information for all maps for this device. @@ -148,7 +158,6 @@ async def create_coordinator_maps( Only one map can be loaded at a time per device. """ entities = [] - cur_map = coord.current_map # This won't be None at this point as the coordinator will have run first. assert cur_map is not None @@ -180,6 +189,7 @@ async def create_coordinator_maps( map_flag, api_data, map_info.name, + drawables, ) ) if len(coord.maps) != 1: diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 30aa64f626a..aaf476d7fc6 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -31,6 +31,32 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, + "options": { + "step": { + "drawables": { + "description": "Specify which features to draw on the map.", + "data": { + "charger": "Charger", + "cleaned_area": "Cleaned area", + "goto_path": "Go-to path", + "ignored_obstacles": "Ignored obstacles", + "ignored_obstacles_with_photo": "Ignored obstacles with photo", + "mop_path": "Mop path", + "no_carpet_zones": "No carpet zones", + "no_go_zones": "No-go zones", + "no_mopping_zones": "No mopping zones", + "obstacles": "Obstacles", + "obstacles_with_photo": "Obstacles with photo", + "path": "Path", + "predicted_path": "Predicted path", + "room_names": "Room names", + "vacuum_position": "Vacuum position", + "virtual_walls": "Virtual walls", + "zones": "Zones" + } + } + } + }, "entity": { "binary_sensor": { "in_cleaning": { diff --git a/tests/components/roborock/test_config_flow.py b/tests/components/roborock/test_config_flow.py index 5134ef7eea2..a5a86e44372 100644 --- a/tests/components/roborock/test_config_flow.py +++ b/tests/components/roborock/test_config_flow.py @@ -11,9 +11,10 @@ from roborock.exceptions import ( RoborockInvalidEmail, RoborockUrlException, ) +from vacuum_map_parser_base.config.drawable import Drawable from homeassistant import config_entries -from homeassistant.components.roborock.const import CONF_ENTRY_CODE, DOMAIN +from homeassistant.components.roborock.const import CONF_ENTRY_CODE, DOMAIN, DRAWABLES from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -185,6 +186,28 @@ async def test_config_flow_failures_code_login( assert len(mock_setup.mock_calls) == 1 +async def test_options_flow_drawables( + hass: HomeAssistant, setup_entry: MockConfigEntry +) -> None: + """Test that the options flow works.""" + result = await hass.config_entries.options.async_init(setup_entry.entry_id) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == DRAWABLES + with patch( + "homeassistant.components.roborock.async_setup_entry", return_value=True + ) as mock_setup: + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={Drawable.PREDICTED_PATH: True}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert setup_entry.options[DRAWABLES][Drawable.PREDICTED_PATH] is True + assert len(mock_setup.mock_calls) == 1 + + async def test_reauth_flow( hass: HomeAssistant, bypass_api_fixture, mock_roborock_entry: MockConfigEntry ) -> None: From fc2968bc1be34168445b311e19d1e3a9e8a0cba9 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 26 Jun 2024 14:35:22 +0200 Subject: [PATCH 0242/2411] Adjust tplink codeowners (#120561) --- CODEOWNERS | 4 ++-- homeassistant/components/tplink/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 973780b811c..7834add43f6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1460,8 +1460,8 @@ build.json @home-assistant/supervisor /tests/components/tomorrowio/ @raman325 @lymanepp /homeassistant/components/totalconnect/ @austinmroczek /tests/components/totalconnect/ @austinmroczek -/homeassistant/components/tplink/ @rytilahti @thegardenmonkey @bdraco @sdb9696 -/tests/components/tplink/ @rytilahti @thegardenmonkey @bdraco @sdb9696 +/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696 +/tests/components/tplink/ @rytilahti @bdraco @sdb9696 /homeassistant/components/tplink_omada/ @MarkGodwin /tests/components/tplink_omada/ @MarkGodwin /homeassistant/components/traccar/ @ludeeus diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 5b8e6f8fc1b..74b80771c65 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -1,7 +1,7 @@ { "domain": "tplink", "name": "TP-Link Smart Home", - "codeowners": ["@rytilahti", "@thegardenmonkey", "@bdraco", "@sdb9696"], + "codeowners": ["@rytilahti", "@bdraco", "@sdb9696"], "config_flow": true, "dependencies": ["network"], "dhcp": [ From 7eb9875a9e11dd45f0fe20f0a54c268340154f77 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 14:45:04 +0200 Subject: [PATCH 0243/2411] Add Base class for entities in PyLoad integration (#120563) * Add Base class for entities * Remove constructors --- homeassistant/components/pyload/button.py | 28 +---------- homeassistant/components/pyload/entity.py | 37 ++++++++++++++ homeassistant/components/pyload/sensor.py | 59 ++++++++++------------- homeassistant/components/pyload/switch.py | 28 +---------- 4 files changed, 66 insertions(+), 86 deletions(-) create mode 100644 homeassistant/components/pyload/entity.py diff --git a/homeassistant/components/pyload/button.py b/homeassistant/components/pyload/button.py index 0d8a232142a..950177f8751 100644 --- a/homeassistant/components/pyload/button.py +++ b/homeassistant/components/pyload/button.py @@ -11,13 +11,10 @@ from pyloadapi.api import PyLoadAPI from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PyLoadConfigEntry -from .const import DOMAIN, MANUFACTURER, SERVICE_NAME -from .coordinator import PyLoadCoordinator +from .entity import BasePyLoadEntity @dataclass(kw_only=True, frozen=True) @@ -76,32 +73,11 @@ async def async_setup_entry( ) -class PyLoadBinarySensor(CoordinatorEntity[PyLoadCoordinator], ButtonEntity): +class PyLoadBinarySensor(BasePyLoadEntity, ButtonEntity): """Representation of a pyLoad button.""" - _attr_has_entity_name = True entity_description: PyLoadButtonEntityDescription - def __init__( - self, - coordinator: PyLoadCoordinator, - entity_description: PyLoadButtonEntityDescription, - ) -> None: - """Initialize the button.""" - super().__init__(coordinator) - self._attr_unique_id = ( - f"{coordinator.config_entry.entry_id}_{entity_description.key}" - ) - self.entity_description = entity_description - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - manufacturer=MANUFACTURER, - model=SERVICE_NAME, - configuration_url=coordinator.pyload.api_url, - identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, - sw_version=coordinator.version, - ) - async def async_press(self) -> None: """Handle the button press.""" await self.entity_description.press_fn(self.coordinator.pyload) diff --git a/homeassistant/components/pyload/entity.py b/homeassistant/components/pyload/entity.py new file mode 100644 index 00000000000..58e93431ca1 --- /dev/null +++ b/homeassistant/components/pyload/entity.py @@ -0,0 +1,37 @@ +"""Base entity for pyLoad.""" + +from __future__ import annotations + +from homeassistant.components.button import EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER, SERVICE_NAME +from .coordinator import PyLoadCoordinator + + +class BasePyLoadEntity(CoordinatorEntity[PyLoadCoordinator]): + """BaseEntity for pyLoad.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: PyLoadCoordinator, + entity_description: EntityDescription, + ) -> None: + """Initialize the Entity.""" + super().__init__(coordinator) + + self._attr_unique_id = ( + f"{coordinator.config_entry.entry_id}_{entity_description.key}" + ) + self.entity_description = entity_description + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + manufacturer=MANUFACTURER, + model=SERVICE_NAME, + configuration_url=coordinator.pyload.api_url, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + sw_version=coordinator.version, + ) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index bc90fdb7ccb..4a0502707b6 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from enum import StrEnum import voluptuous as vol @@ -28,11 +30,9 @@ from homeassistant.const import ( from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PyLoadConfigEntry from .const import ( @@ -41,11 +41,10 @@ from .const import ( DEFAULT_PORT, DOMAIN, ISSUE_PLACEHOLDER, - MANUFACTURER, - SERVICE_NAME, UNIT_DOWNLOADS, ) -from .coordinator import PyLoadCoordinator +from .coordinator import pyLoadData +from .entity import BasePyLoadEntity class PyLoadSensorEntity(StrEnum): @@ -58,40 +57,52 @@ class PyLoadSensorEntity(StrEnum): TOTAL = "total" -SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( +@dataclass(kw_only=True, frozen=True) +class PyLoadSensorEntityDescription(SensorEntityDescription): + """Describes pyLoad switch entity.""" + + value_fn: Callable[[pyLoadData], StateType] + + +SENSOR_DESCRIPTIONS: tuple[PyLoadSensorEntityDescription, ...] = ( + PyLoadSensorEntityDescription( key=PyLoadSensorEntity.SPEED, translation_key=PyLoadSensorEntity.SPEED, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, suggested_display_precision=1, + value_fn=lambda data: data.speed, ), - SensorEntityDescription( + PyLoadSensorEntityDescription( key=PyLoadSensorEntity.ACTIVE, translation_key=PyLoadSensorEntity.ACTIVE, native_unit_of_measurement=UNIT_DOWNLOADS, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.active, ), - SensorEntityDescription( + PyLoadSensorEntityDescription( key=PyLoadSensorEntity.QUEUE, translation_key=PyLoadSensorEntity.QUEUE, native_unit_of_measurement=UNIT_DOWNLOADS, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.queue, ), - SensorEntityDescription( + PyLoadSensorEntityDescription( key=PyLoadSensorEntity.TOTAL, translation_key=PyLoadSensorEntity.TOTAL, native_unit_of_measurement=UNIT_DOWNLOADS, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.total, ), - SensorEntityDescription( + PyLoadSensorEntityDescription( key=PyLoadSensorEntity.FREE_SPACE, translation_key=PyLoadSensorEntity.FREE_SPACE, device_class=SensorDeviceClass.DATA_SIZE, native_unit_of_measurement=UnitOfInformation.BYTES, suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, suggested_display_precision=1, + value_fn=lambda data: data.free_space, ), ) @@ -173,32 +184,12 @@ async def async_setup_entry( ) -class PyLoadSensor(CoordinatorEntity[PyLoadCoordinator], SensorEntity): +class PyLoadSensor(BasePyLoadEntity, SensorEntity): """Representation of a pyLoad sensor.""" - _attr_has_entity_name = True - - def __init__( - self, - coordinator: PyLoadCoordinator, - entity_description: SensorEntityDescription, - ) -> None: - """Initialize a new pyLoad sensor.""" - super().__init__(coordinator) - self._attr_unique_id = ( - f"{coordinator.config_entry.entry_id}_{entity_description.key}" - ) - self.entity_description = entity_description - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - manufacturer=MANUFACTURER, - model=SERVICE_NAME, - configuration_url=coordinator.pyload.api_url, - identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, - sw_version=coordinator.version, - ) + entity_description: PyLoadSensorEntityDescription @property def native_value(self) -> StateType: """Return the state of the sensor.""" - return getattr(self.coordinator.data, self.entity_description.key) + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/pyload/switch.py b/homeassistant/components/pyload/switch.py index b0628005008..4ed3e925488 100644 --- a/homeassistant/components/pyload/switch.py +++ b/homeassistant/components/pyload/switch.py @@ -15,13 +15,10 @@ from homeassistant.components.switch import ( SwitchEntityDescription, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PyLoadConfigEntry -from .const import DOMAIN, MANUFACTURER, SERVICE_NAME -from .coordinator import PyLoadCoordinator +from .entity import BasePyLoadEntity class PyLoadSwitch(StrEnum): @@ -75,32 +72,11 @@ async def async_setup_entry( ) -class PyLoadSwitchEntity(CoordinatorEntity[PyLoadCoordinator], SwitchEntity): +class PyLoadSwitchEntity(BasePyLoadEntity, SwitchEntity): """Representation of a pyLoad sensor.""" - _attr_has_entity_name = True entity_description: PyLoadSwitchEntityDescription - def __init__( - self, - coordinator: PyLoadCoordinator, - entity_description: PyLoadSwitchEntityDescription, - ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator) - self._attr_unique_id = ( - f"{coordinator.config_entry.entry_id}_{entity_description.key}" - ) - self.entity_description = entity_description - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - manufacturer=MANUFACTURER, - model=SERVICE_NAME, - configuration_url=coordinator.pyload.api_url, - identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, - sw_version=coordinator.version, - ) - @property def is_on(self) -> bool | None: """Return the state of the device.""" From 43d686e0f122781023402ae4148648396a2ee542 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 14:59:14 +0200 Subject: [PATCH 0244/2411] Redact the hostname in pyLoad diagnostics (#120567) --- homeassistant/components/pyload/diagnostics.py | 4 ++-- tests/components/pyload/snapshots/test_diagnostics.ambr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pyload/diagnostics.py b/homeassistant/components/pyload/diagnostics.py index 95ff37bf9f8..1b719ffc7b9 100644 --- a/homeassistant/components/pyload/diagnostics.py +++ b/homeassistant/components/pyload/diagnostics.py @@ -6,13 +6,13 @@ from dataclasses import asdict from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from . import PyLoadConfigEntry from .coordinator import pyLoadData -TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} +TO_REDACT = {CONF_USERNAME, CONF_PASSWORD, CONF_HOST} async def async_get_config_entry_diagnostics( diff --git a/tests/components/pyload/snapshots/test_diagnostics.ambr b/tests/components/pyload/snapshots/test_diagnostics.ambr index 0e078e000c9..e2b51ad184a 100644 --- a/tests/components/pyload/snapshots/test_diagnostics.ambr +++ b/tests/components/pyload/snapshots/test_diagnostics.ambr @@ -2,7 +2,7 @@ # name: test_diagnostics dict({ 'config_entry_data': dict({ - 'host': 'pyload.local', + 'host': '**REDACTED**', 'password': '**REDACTED**', 'port': 8000, 'ssl': True, From af9b4b98ca8a0615caf12962f33548bbec5163f5 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 15:09:42 +0200 Subject: [PATCH 0245/2411] Add value_fn to switch entity description in pyLoad (#120569) --- homeassistant/components/pyload/switch.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/pyload/switch.py b/homeassistant/components/pyload/switch.py index 4ed3e925488..21c8d75aaa0 100644 --- a/homeassistant/components/pyload/switch.py +++ b/homeassistant/components/pyload/switch.py @@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import PyLoadConfigEntry +from .coordinator import pyLoadData from .entity import BasePyLoadEntity @@ -35,6 +36,7 @@ class PyLoadSwitchEntityDescription(SwitchEntityDescription): turn_on_fn: Callable[[PyLoadAPI], Awaitable[Any]] turn_off_fn: Callable[[PyLoadAPI], Awaitable[Any]] toggle_fn: Callable[[PyLoadAPI], Awaitable[Any]] + value_fn: Callable[[pyLoadData], bool] SENSOR_DESCRIPTIONS: tuple[PyLoadSwitchEntityDescription, ...] = ( @@ -45,6 +47,7 @@ SENSOR_DESCRIPTIONS: tuple[PyLoadSwitchEntityDescription, ...] = ( turn_on_fn=lambda api: api.unpause(), turn_off_fn=lambda api: api.pause(), toggle_fn=lambda api: api.toggle_pause(), + value_fn=lambda data: data.download, ), PyLoadSwitchEntityDescription( key=PyLoadSwitch.RECONNECT, @@ -53,6 +56,7 @@ SENSOR_DESCRIPTIONS: tuple[PyLoadSwitchEntityDescription, ...] = ( turn_on_fn=lambda api: api.toggle_reconnect(), turn_off_fn=lambda api: api.toggle_reconnect(), toggle_fn=lambda api: api.toggle_reconnect(), + value_fn=lambda data: data.reconnect, ), ) @@ -80,7 +84,9 @@ class PyLoadSwitchEntity(BasePyLoadEntity, SwitchEntity): @property def is_on(self) -> bool | None: """Return the state of the device.""" - return getattr(self.coordinator.data, self.entity_description.key) + return self.entity_description.value_fn( + self.coordinator.data, + ) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" From 4defc4a58f605dd680bddc88c2552f54a89d0d78 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:28:50 +0200 Subject: [PATCH 0246/2411] Implement a reboot-button for Plugwise (#120554) Co-authored-by: Franck Nijhof --- homeassistant/components/plugwise/button.py | 52 +++++++++++++++++++ homeassistant/components/plugwise/const.py | 3 ++ .../components/plugwise/coordinator.py | 14 ++--- .../components/plugwise/strings.json | 5 ++ tests/components/plugwise/test_button.py | 39 ++++++++++++++ tests/components/plugwise/test_init.py | 6 ++- 6 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/plugwise/button.py create mode 100644 tests/components/plugwise/test_button.py diff --git a/homeassistant/components/plugwise/button.py b/homeassistant/components/plugwise/button.py new file mode 100644 index 00000000000..078d31bea12 --- /dev/null +++ b/homeassistant/components/plugwise/button.py @@ -0,0 +1,52 @@ +"""Plugwise Button component for Home Assistant.""" + +from __future__ import annotations + +from homeassistant.components.button import ButtonDeviceClass, ButtonEntity +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import PlugwiseConfigEntry +from .const import GATEWAY_ID, REBOOT +from .coordinator import PlugwiseDataUpdateCoordinator +from .entity import PlugwiseEntity +from .util import plugwise_command + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PlugwiseConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Plugwise buttons from a ConfigEntry.""" + coordinator = entry.runtime_data + + gateway = coordinator.data.gateway + async_add_entities( + PlugwiseButtonEntity(coordinator, device_id) + for device_id in coordinator.data.devices + if device_id == gateway[GATEWAY_ID] and REBOOT in gateway + ) + + +class PlugwiseButtonEntity(PlugwiseEntity, ButtonEntity): + """Defines a Plugwise button.""" + + _attr_device_class = ButtonDeviceClass.RESTART + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, + coordinator: PlugwiseDataUpdateCoordinator, + device_id: str, + ) -> None: + """Initialize the button.""" + super().__init__(coordinator, device_id) + self._attr_translation_key = REBOOT + self._attr_unique_id = f"{device_id}-reboot" + + @plugwise_command + async def async_press(self) -> None: + """Triggers the Plugwise button press service.""" + await self.coordinator.api.reboot_gateway() diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index 14599ce61fb..5e4dea5586b 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -17,14 +17,17 @@ FLOW_SMILE: Final = "smile (Adam/Anna/P1)" FLOW_STRETCH: Final = "stretch (Stretch)" FLOW_TYPE: Final = "flow_type" GATEWAY: Final = "gateway" +GATEWAY_ID: Final = "gateway_id" LOCATION: Final = "location" PW_TYPE: Final = "plugwise_type" +REBOOT: Final = "reboot" SMILE: Final = "smile" STRETCH: Final = "stretch" STRETCH_USERNAME: Final = "stretch" PLATFORMS: Final[list[str]] = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.CLIMATE, Platform.NUMBER, Platform.SELECT, diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py index 1dff11d26d8..bc12ef4443b 100644 --- a/homeassistant/components/plugwise/coordinator.py +++ b/homeassistant/components/plugwise/coordinator.py @@ -7,6 +7,7 @@ from plugwise.exceptions import ( ConnectionFailedError, InvalidAuthentication, InvalidXMLError, + PlugwiseError, ResponseError, UnsupportedDeviceError, ) @@ -64,22 +65,23 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): async def _async_update_data(self) -> PlugwiseData: """Fetch data from Plugwise.""" - + data = PlugwiseData({}, {}) try: if not self._connected: await self._connect() data = await self.api.async_update() + except ConnectionFailedError as err: + raise UpdateFailed("Failed to connect") from err except InvalidAuthentication as err: - raise ConfigEntryError("Invalid username or Smile ID") from err + raise ConfigEntryError("Authentication failed") from err except (InvalidXMLError, ResponseError) as err: raise UpdateFailed( - "Invalid XML data, or error indication received for the Plugwise" - " Adam/Smile/Stretch" + "Invalid XML data, or error indication received from the Plugwise Adam/Smile/Stretch" ) from err + except PlugwiseError as err: + raise UpdateFailed("Data incomplete or missing") from err except UnsupportedDeviceError as err: raise ConfigEntryError("Device with unsupported firmware") from err - except ConnectionFailedError as err: - raise UpdateFailed("Failed to connect to the Plugwise Smile") from err else: self.new_devices = set(data.devices) - self._current_devices self._current_devices = set(data.devices) diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index ef2d6458441..f74fc036e2a 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -55,6 +55,11 @@ "name": "Plugwise notification" } }, + "button": { + "reboot": { + "name": "Reboot" + } + }, "climate": { "plugwise": { "state_attributes": { diff --git a/tests/components/plugwise/test_button.py b/tests/components/plugwise/test_button.py new file mode 100644 index 00000000000..23003b3ffe6 --- /dev/null +++ b/tests/components/plugwise/test_button.py @@ -0,0 +1,39 @@ +"""Tests for Plugwise button entities.""" + +from unittest.mock import MagicMock + +from homeassistant.components.button import ( + DOMAIN as BUTTON_DOMAIN, + SERVICE_PRESS, + ButtonDeviceClass, +) +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_adam_reboot_button( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test creation of button entities.""" + state = hass.states.get("button.adam_reboot") + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_DEVICE_CLASS) == ButtonDeviceClass.RESTART + + registry = er.async_get(hass) + entry = registry.async_get("button.adam_reboot") + assert entry + assert entry.unique_id == "fe799307f1624099878210aa0b9f1475-reboot" + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.adam_reboot"}, + blocking=True, + ) + + assert mock_smile_adam.reboot_gateway.call_count == 1 + mock_smile_adam.reboot_gateway.assert_called_with() diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index 9c709f1c4f6..d3f23a18285 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -7,6 +7,7 @@ from plugwise.exceptions import ( ConnectionFailedError, InvalidAuthentication, InvalidXMLError, + PlugwiseError, ResponseError, UnsupportedDeviceError, ) @@ -83,6 +84,7 @@ async def test_load_unload_config_entry( (ConnectionFailedError, ConfigEntryState.SETUP_RETRY), (InvalidAuthentication, ConfigEntryState.SETUP_ERROR), (InvalidXMLError, ConfigEntryState.SETUP_RETRY), + (PlugwiseError, ConfigEntryState.SETUP_RETRY), (ResponseError, ConfigEntryState.SETUP_RETRY), (UnsupportedDeviceError, ConfigEntryState.SETUP_ERROR), ], @@ -219,7 +221,7 @@ async def test_update_device( entity_registry, mock_config_entry.entry_id ) ) - == 28 + == 29 ) assert ( len( @@ -242,7 +244,7 @@ async def test_update_device( entity_registry, mock_config_entry.entry_id ) ) - == 33 + == 34 ) assert ( len( From d0f82d6f020627152db6f619f7702d158c0e578c Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 26 Jun 2024 09:40:19 -0400 Subject: [PATCH 0247/2411] Add support for Dyad vacuums to Roborock (#115331) --- homeassistant/components/roborock/__init__.py | 87 ++++- .../components/roborock/binary_sensor.py | 18 +- homeassistant/components/roborock/button.py | 20 +- .../components/roborock/coordinator.py | 60 ++- homeassistant/components/roborock/device.py | 67 +++- .../components/roborock/diagnostics.py | 6 +- homeassistant/components/roborock/image.py | 15 +- homeassistant/components/roborock/models.py | 15 + homeassistant/components/roborock/number.py | 13 +- homeassistant/components/roborock/select.py | 22 +- homeassistant/components/roborock/sensor.py | 111 +++++- .../components/roborock/strings.json | 48 +++ homeassistant/components/roborock/switch.py | 13 +- homeassistant/components/roborock/time.py | 13 +- homeassistant/components/roborock/vacuum.py | 19 +- tests/components/roborock/conftest.py | 45 ++- .../roborock/snapshots/test_diagnostics.ambr | 363 ++++++++++++++++++ tests/components/roborock/test_init.py | 48 ++- tests/components/roborock/test_sensor.py | 8 +- 19 files changed, 874 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index cdbddbda95b..310c5fee92b 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Coroutine +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any @@ -12,6 +13,7 @@ from roborock import HomeDataRoom, RoborockException, RoborockInvalidCredentials from roborock.code_mappings import RoborockCategory from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, UserData from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 +from roborock.version_a01_apis import RoborockMqttClientA01 from roborock.web_api import RoborockApiClient from homeassistant.config_entries import ConfigEntry @@ -20,13 +22,27 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import CONF_BASE_URL, CONF_USER_DATA, DOMAIN, PLATFORMS -from .coordinator import RoborockDataUpdateCoordinator +from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01 SCAN_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) +@dataclass +class RoborockCoordinators: + """Roborock coordinators type.""" + + v1: list[RoborockDataUpdateCoordinator] + a01: list[RoborockDataUpdateCoordinatorA01] + + def values( + self, + ) -> list[RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01]: + """Return all coordinators.""" + return self.v1 + self.a01 + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up roborock from a config entry.""" @@ -37,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_client = RoborockApiClient(entry.data[CONF_USERNAME], entry.data[CONF_BASE_URL]) _LOGGER.debug("Getting home data") try: - home_data = await api_client.get_home_data(user_data) + home_data = await api_client.get_home_data_v2(user_data) except RoborockInvalidCredentials as err: raise ConfigEntryAuthFailed( "Invalid credentials", @@ -66,21 +82,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return_exceptions=True, ) # Valid coordinators are those where we had networking cached or we could get networking - valid_coordinators: list[RoborockDataUpdateCoordinator] = [ + v1_coords = [ coord for coord in coordinators if isinstance(coord, RoborockDataUpdateCoordinator) ] - if len(valid_coordinators) == 0: + a01_coords = [ + coord + for coord in coordinators + if isinstance(coord, RoborockDataUpdateCoordinatorA01) + ] + if len(v1_coords) + len(a01_coords) == 0: raise ConfigEntryNotReady( "No devices were able to successfully setup", translation_domain=DOMAIN, translation_key="no_coordinators", ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - coordinator.api.device_info.device.duid: coordinator - for coordinator in valid_coordinators - } + valid_coordinators = RoborockCoordinators(v1_coords, a01_coords) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = valid_coordinators await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -92,14 +111,19 @@ def build_setup_functions( user_data: UserData, product_info: dict[str, HomeDataProduct], home_data_rooms: list[HomeDataRoom], -) -> list[Coroutine[Any, Any, RoborockDataUpdateCoordinator | None]]: +) -> list[ + Coroutine[ + Any, + Any, + RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01 | None, + ] +]: """Create a list of setup functions that can later be called asynchronously.""" return [ setup_device( hass, user_data, device, product_info[device.product_id], home_data_rooms ) for device in device_map.values() - if product_info[device.product_id].category == RoborockCategory.VACUUM ] @@ -109,11 +133,33 @@ async def setup_device( device: HomeDataDevice, product_info: HomeDataProduct, home_data_rooms: list[HomeDataRoom], +) -> RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01 | None: + """Set up a coordinator for a given device.""" + if device.pv == "1.0": + return await setup_device_v1( + hass, user_data, device, product_info, home_data_rooms + ) + if device.pv == "A01": + if product_info.category == RoborockCategory.WET_DRY_VAC: + return await setup_device_a01(hass, user_data, device, product_info) + _LOGGER.info( + "Not adding device %s because its protocol version %s or category %s is not supported", + device.duid, + device.pv, + product_info.category.name, + ) + return None + + +async def setup_device_v1( + hass: HomeAssistant, + user_data: UserData, + device: HomeDataDevice, + product_info: HomeDataProduct, + home_data_rooms: list[HomeDataRoom], ) -> RoborockDataUpdateCoordinator | None: """Set up a device Coordinator.""" - mqtt_client = RoborockMqttClientV1( - user_data, DeviceData(device, product_info.model) - ) + mqtt_client = RoborockMqttClientV1(user_data, DeviceData(device, product_info.name)) try: networking = await mqtt_client.get_networking() if networking is None: @@ -170,6 +216,21 @@ async def setup_device( return coordinator +async def setup_device_a01( + hass: HomeAssistant, + user_data: UserData, + device: HomeDataDevice, + product_info: HomeDataProduct, +) -> RoborockDataUpdateCoordinatorA01 | None: + """Set up a A01 protocol device.""" + mqtt_client = RoborockMqttClientA01( + user_data, DeviceData(device, product_info.name), product_info.category + ) + coord = RoborockDataUpdateCoordinatorA01(hass, device, product_info, mqtt_client) + await coord.async_config_entry_first_refresh() + return coord + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle removal of an entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): diff --git a/homeassistant/components/roborock/binary_sensor.py b/homeassistant/components/roborock/binary_sensor.py index 00716207f7a..2fd6dd8d7d5 100644 --- a/homeassistant/components/roborock/binary_sensor.py +++ b/homeassistant/components/roborock/binary_sensor.py @@ -18,9 +18,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockCoordinatedEntity +from .device import RoborockCoordinatedEntityV1 @dataclass(frozen=True, kw_only=True) @@ -75,34 +76,33 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Roborock vacuum binary sensors.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( RoborockBinarySensorEntity( - f"{description.key}_{slugify(device_id)}", coordinator, description, ) - for device_id, coordinator in coordinators.items() + for coordinator in coordinators.v1 for description in BINARY_SENSOR_DESCRIPTIONS if description.value_fn(coordinator.roborock_device_info.props) is not None ) -class RoborockBinarySensorEntity(RoborockCoordinatedEntity, BinarySensorEntity): +class RoborockBinarySensorEntity(RoborockCoordinatedEntityV1, BinarySensorEntity): """Representation of a Roborock binary sensor.""" entity_description: RoborockBinarySensorDescription def __init__( self, - unique_id: str, coordinator: RoborockDataUpdateCoordinator, description: RoborockBinarySensorDescription, ) -> None: """Initialize the entity.""" - super().__init__(unique_id, coordinator) + super().__init__( + f"{description.key}_{slugify(coordinator.duid)}", + coordinator, + ) self.entity_description = description @property diff --git a/homeassistant/components/roborock/button.py b/homeassistant/components/roborock/button.py index fe6dfabb56c..445033a0f6d 100644 --- a/homeassistant/components/roborock/button.py +++ b/homeassistant/components/roborock/button.py @@ -13,9 +13,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockEntity +from .device import RoborockEntityV1 @dataclass(frozen=True, kw_only=True) @@ -68,33 +69,34 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock button platform.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( RoborockButtonEntity( - f"{description.key}_{slugify(device_id)}", coordinator, description, ) - for device_id, coordinator in coordinators.items() + for coordinator in coordinators.v1 for description in CONSUMABLE_BUTTON_DESCRIPTIONS + if isinstance(coordinator, RoborockDataUpdateCoordinator) ) -class RoborockButtonEntity(RoborockEntity, ButtonEntity): +class RoborockButtonEntity(RoborockEntityV1, ButtonEntity): """A class to define Roborock button entities.""" entity_description: RoborockButtonDescription def __init__( self, - unique_id: str, coordinator: RoborockDataUpdateCoordinator, entity_description: RoborockButtonDescription, ) -> None: """Create a button entity.""" - super().__init__(unique_id, coordinator.device_info, coordinator.api) + super().__init__( + f"{entity_description.key}_{slugify(coordinator.duid)}", + coordinator.device_info, + coordinator.api, + ) self.entity_description = entity_description async def async_press(self) -> None: diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 32b7a487ac8..430e2815a7b 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -9,18 +9,21 @@ import logging from roborock import HomeDataRoom from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo from roborock.exceptions import RoborockException +from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol from roborock.roborock_typing import DeviceProp from roborock.version_1_apis.roborock_local_client_v1 import RoborockLocalClientV1 from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 +from roborock.version_a01_apis import RoborockClientA01 from homeassistant.const import ATTR_CONNECTIONS from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN -from .models import RoborockHassDeviceInfo, RoborockMapInfo +from .models import RoborockA01HassDeviceInfo, RoborockHassDeviceInfo, RoborockMapInfo SCAN_INTERVAL = timedelta(seconds=30) @@ -77,6 +80,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): "Using the cloud API for device %s. This is not recommended as it can lead to rate limiting. We recommend making your vacuum accessible by your Home Assistant instance", self.roborock_device_info.device.duid, ) + await self.api.async_disconnect() # We use the cloud api if the local api fails to connect. self.api = self.cloud_api # Right now this should never be called if the cloud api is the primary api, @@ -137,3 +141,57 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): self.maps[self.current_map].rooms[room.segment_id] = ( self._home_data_rooms.get(room.iot_id, "Unknown") ) + + @property + def duid(self) -> str: + """Get the unique id of the device as specified by Roborock.""" + return self.roborock_device_info.device.duid + + +class RoborockDataUpdateCoordinatorA01( + DataUpdateCoordinator[ + dict[RoborockDyadDataProtocol | RoborockZeoProtocol, StateType] + ] +): + """Class to manage fetching data from the API for A01 devices.""" + + def __init__( + self, + hass: HomeAssistant, + device: HomeDataDevice, + product_info: HomeDataProduct, + api: RoborockClientA01, + ) -> None: + """Initialize.""" + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + self.api = api + self.device_info = DeviceInfo( + name=device.name, + identifiers={(DOMAIN, device.duid)}, + manufacturer="Roborock", + model=product_info.model, + sw_version=device.fv, + ) + self.request_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol] = [ + RoborockDyadDataProtocol.STATUS, + RoborockDyadDataProtocol.POWER, + RoborockDyadDataProtocol.MESH_LEFT, + RoborockDyadDataProtocol.BRUSH_LEFT, + RoborockDyadDataProtocol.ERROR, + RoborockDyadDataProtocol.TOTAL_RUN_TIME, + ] + self.roborock_device_info = RoborockA01HassDeviceInfo(device, product_info) + + async def _async_update_data( + self, + ) -> dict[RoborockDyadDataProtocol | RoborockZeoProtocol, StateType]: + return await self.api.update_values(self.request_protocols) + + async def release(self) -> None: + """Disconnect from API.""" + await self.api.async_release() + + @property + def duid(self) -> str: + """Get the unique id of the device as specified by Roborock.""" + return self.roborock_device_info.device.duid diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 6450d849859..4a16ada5967 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -2,6 +2,7 @@ from typing import Any +from roborock.api import RoborockClient from roborock.command_cache import CacheableAttribute from roborock.containers import Consumable, Status from roborock.exceptions import RoborockException @@ -9,6 +10,7 @@ from roborock.roborock_message import RoborockDataProtocol from roborock.roborock_typing import RoborockCommand from roborock.version_1_apis.roborock_client_v1 import AttributeCache, RoborockClientV1 from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 +from roborock.version_a01_apis import RoborockClientA01 from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo @@ -16,7 +18,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import RoborockDataUpdateCoordinator +from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01 class RoborockEntity(Entity): @@ -28,17 +30,24 @@ class RoborockEntity(Entity): self, unique_id: str, device_info: DeviceInfo, - api: RoborockClientV1, + api: RoborockClient, ) -> None: - """Initialize the coordinated Roborock Device.""" + """Initialize the Roborock Device.""" self._attr_unique_id = unique_id self._attr_device_info = device_info self._api = api - @property - def api(self) -> RoborockClientV1: - """Returns the api.""" - return self._api + +class RoborockEntityV1(RoborockEntity): + """Representation of a base Roborock V1 Entity.""" + + _api: RoborockClientV1 + + def __init__( + self, unique_id: str, device_info: DeviceInfo, api: RoborockClientV1 + ) -> None: + """Initialize the Roborock Device.""" + super().__init__(unique_id, device_info, api) def get_cache(self, attribute: CacheableAttribute) -> AttributeCache: """Get an item from the api cache.""" @@ -66,9 +75,26 @@ class RoborockEntity(Entity): ) from err return response + @property + def api(self) -> RoborockClientV1: + """Returns the api.""" + return self._api -class RoborockCoordinatedEntity( - RoborockEntity, CoordinatorEntity[RoborockDataUpdateCoordinator] + +class RoborockEntityA01(RoborockEntity): + """Representation of a base Roborock Entity for A01 devices.""" + + _api: RoborockClientA01 + + def __init__( + self, unique_id: str, device_info: DeviceInfo, api: RoborockClientA01 + ) -> None: + """Initialize the Roborock Device.""" + super().__init__(unique_id, device_info, api) + + +class RoborockCoordinatedEntityV1( + RoborockEntityV1, CoordinatorEntity[RoborockDataUpdateCoordinator] ): """Representation of a base a coordinated Roborock Entity.""" @@ -83,7 +109,7 @@ class RoborockCoordinatedEntity( | None = None, ) -> None: """Initialize the coordinated Roborock Device.""" - RoborockEntity.__init__( + RoborockEntityV1.__init__( self, unique_id=unique_id, device_info=coordinator.device_info, @@ -138,3 +164,24 @@ class RoborockCoordinatedEntity( self.coordinator.roborock_device_info.props.consumable = value self.coordinator.data = self.coordinator.roborock_device_info.props self.schedule_update_ha_state() + + +class RoborockCoordinatedEntityA01( + RoborockEntityA01, CoordinatorEntity[RoborockDataUpdateCoordinatorA01] +): + """Representation of a base a coordinated Roborock Entity.""" + + def __init__( + self, + unique_id: str, + coordinator: RoborockDataUpdateCoordinatorA01, + ) -> None: + """Initialize the coordinated Roborock Device.""" + RoborockEntityA01.__init__( + self, + unique_id=unique_id, + device_info=coordinator.device_info, + api=coordinator.api, + ) + CoordinatorEntity.__init__(self, coordinator=coordinator) + self._attr_unique_id = unique_id diff --git a/homeassistant/components/roborock/diagnostics.py b/homeassistant/components/roborock/diagnostics.py index 79a9f0bafed..9be8b6f4d63 100644 --- a/homeassistant/components/roborock/diagnostics.py +++ b/homeassistant/components/roborock/diagnostics.py @@ -9,8 +9,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import HomeAssistant +from . import RoborockCoordinators from .const import DOMAIN -from .coordinator import RoborockDataUpdateCoordinator TO_REDACT_CONFIG = ["token", "sn", "rruid", CONF_UNIQUE_ID, "username", "uid"] @@ -21,9 +21,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] return { "config_entry": async_redact_data(config_entry.data, TO_REDACT_CONFIG), diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index 9dfe8d53cd3..d1731d289db 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -21,9 +21,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify import homeassistant.util.dt as dt_util +from . import RoborockCoordinators from .const import DEFAULT_DRAWABLES, DOMAIN, DRAWABLES, IMAGE_CACHE_INTERVAL, MAP_SLEEP from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockCoordinatedEntity +from .device import RoborockCoordinatedEntityV1 async def async_setup_entry( @@ -33,9 +34,7 @@ async def async_setup_entry( ) -> None: """Set up Roborock image platform.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] drawables = [ drawable for drawable, default_value in DEFAULT_DRAWABLES.items() @@ -46,7 +45,7 @@ async def async_setup_entry( await asyncio.gather( *( create_coordinator_maps(coord, drawables) - for coord in coordinators.values() + for coord in coordinators.v1 ) ) ) @@ -54,7 +53,7 @@ async def async_setup_entry( async_add_entities(entities) -class RoborockMap(RoborockCoordinatedEntity, ImageEntity): +class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity): """A class to let you visualize the map.""" _attr_has_entity_name = True @@ -70,7 +69,7 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): drawables: list[Drawable], ) -> None: """Initialize a Roborock map.""" - RoborockCoordinatedEntity.__init__(self, unique_id, coordinator) + RoborockCoordinatedEntityV1.__init__(self, unique_id, coordinator) ImageEntity.__init__(self, coordinator.hass) self._attr_name = map_name self.parser = RoborockMapDataParser( @@ -184,7 +183,7 @@ async def create_coordinator_maps( api_data: bytes = map_update[0] if isinstance(map_update[0], bytes) else b"" entities.append( RoborockMap( - f"{slugify(coord.roborock_device_info.device.duid)}_map_{map_info.name}", + f"{slugify(coord.duid)}_map_{map_info.name}", coord, map_flag, api_data, diff --git a/homeassistant/components/roborock/models.py b/homeassistant/components/roborock/models.py index b516c0ee05c..4b8ab43b4a1 100644 --- a/homeassistant/components/roborock/models.py +++ b/homeassistant/components/roborock/models.py @@ -26,6 +26,21 @@ class RoborockHassDeviceInfo: } +@dataclass +class RoborockA01HassDeviceInfo: + """A model to describe A01 roborock devices.""" + + device: HomeDataDevice + product: HomeDataProduct + + def as_dict(self) -> dict[str, dict[str, Any]]: + """Turn RoborockA01HassDeviceInfo into a dictionary.""" + return { + "device": self.device.as_dict(), + "product": self.product.as_dict(), + } + + @dataclass class RoborockMapInfo: """A model to describe all information about a map we may want.""" diff --git a/homeassistant/components/roborock/number.py b/homeassistant/components/roborock/number.py index a432c527b0e..5e776d40f2d 100644 --- a/homeassistant/components/roborock/number.py +++ b/homeassistant/components/roborock/number.py @@ -17,9 +17,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockEntity +from .device import RoborockEntityV1 _LOGGER = logging.getLogger(__name__) @@ -54,14 +55,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock number platform.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] possible_entities: list[ tuple[RoborockDataUpdateCoordinator, RoborockNumberDescription] ] = [ (coordinator, description) - for coordinator in coordinators.values() + for coordinator in coordinators.v1 for description in NUMBER_DESCRIPTIONS ] # We need to check if this function is supported by the device. @@ -81,7 +80,7 @@ async def async_setup_entry( else: valid_entities.append( RoborockNumberEntity( - f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}", + f"{description.key}_{slugify(coordinator.duid)}", coordinator, description, ) @@ -89,7 +88,7 @@ async def async_setup_entry( async_add_entities(valid_entities) -class RoborockNumberEntity(RoborockEntity, NumberEntity): +class RoborockNumberEntity(RoborockEntityV1, NumberEntity): """A class to let you set options on a Roborock vacuum where the potential options are fixed.""" entity_description: RoborockNumberDescription diff --git a/homeassistant/components/roborock/select.py b/homeassistant/components/roborock/select.py index fa7f4250804..c6073645086 100644 --- a/homeassistant/components/roborock/select.py +++ b/homeassistant/components/roborock/select.py @@ -14,9 +14,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockCoordinatedEntity +from .device import RoborockCoordinatedEntityV1 @dataclass(frozen=True, kw_only=True) @@ -69,14 +70,10 @@ async def async_setup_entry( ) -> None: """Set up Roborock select platform.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - RoborockSelectEntity( - f"{description.key}_{slugify(device_id)}", coordinator, description, options - ) - for device_id, coordinator in coordinators.items() + RoborockSelectEntity(coordinator, description, options) + for coordinator in coordinators.v1 for description in SELECT_DESCRIPTIONS if ( options := description.options_lambda( @@ -87,21 +84,24 @@ async def async_setup_entry( ) -class RoborockSelectEntity(RoborockCoordinatedEntity, SelectEntity): +class RoborockSelectEntity(RoborockCoordinatedEntityV1, SelectEntity): """A class to let you set options on a Roborock vacuum where the potential options are fixed.""" entity_description: RoborockSelectDescription def __init__( self, - unique_id: str, coordinator: RoborockDataUpdateCoordinator, entity_description: RoborockSelectDescription, options: list[str], ) -> None: """Create a select entity.""" self.entity_description = entity_description - super().__init__(unique_id, coordinator, entity_description.protocol_listener) + super().__init__( + f"{entity_description.key}_{slugify(coordinator.duid)}", + coordinator, + entity_description.protocol_listener, + ) self._attr_options = options async def async_select_option(self, option: str) -> None: diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index acee1688cc7..3be7461d149 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -6,13 +6,14 @@ from collections.abc import Callable from dataclasses import dataclass import datetime +from roborock.code_mappings import DyadError, RoborockDyadStateCode from roborock.containers import ( RoborockDockErrorCode, RoborockDockTypeCode, RoborockErrorCode, RoborockStateCode, ) -from roborock.roborock_message import RoborockDataProtocol +from roborock.roborock_message import RoborockDataProtocol, RoborockDyadDataProtocol from roborock.roborock_typing import DeviceProp from homeassistant.components.sensor import ( @@ -32,9 +33,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN -from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockCoordinatedEntity +from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01 +from .device import RoborockCoordinatedEntityA01, RoborockCoordinatedEntityV1 @dataclass(frozen=True, kw_only=True) @@ -46,6 +48,13 @@ class RoborockSensorDescription(SensorEntityDescription): protocol_listener: RoborockDataProtocol | None = None +@dataclass(frozen=True, kw_only=True) +class RoborockSensorDescriptionA01(SensorEntityDescription): + """A class that describes Roborock sensors.""" + + data_protocol: RoborockDyadDataProtocol + + def _dock_error_value_fn(properties: DeviceProp) -> str | None: if ( status := properties.status.dock_error_status @@ -193,41 +202,101 @@ SENSOR_DESCRIPTIONS = [ ] +A01_SENSOR_DESCRIPTIONS: list[RoborockSensorDescriptionA01] = [ + RoborockSensorDescriptionA01( + key="status", + data_protocol=RoborockDyadDataProtocol.STATUS, + translation_key="a01_status", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + options=RoborockDyadStateCode.keys(), + ), + RoborockSensorDescriptionA01( + key="battery", + data_protocol=RoborockDyadDataProtocol.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + ), + RoborockSensorDescriptionA01( + key="filter_time_left", + data_protocol=RoborockDyadDataProtocol.MESH_LEFT, + native_unit_of_measurement=UnitOfTime.SECONDS, + device_class=SensorDeviceClass.DURATION, + translation_key="filter_time_left", + entity_category=EntityCategory.DIAGNOSTIC, + ), + RoborockSensorDescriptionA01( + key="brush_remaining", + data_protocol=RoborockDyadDataProtocol.BRUSH_LEFT, + native_unit_of_measurement=UnitOfTime.SECONDS, + device_class=SensorDeviceClass.DURATION, + translation_key="brush_remaining", + entity_category=EntityCategory.DIAGNOSTIC, + ), + RoborockSensorDescriptionA01( + key="error", + data_protocol=RoborockDyadDataProtocol.ERROR, + device_class=SensorDeviceClass.ENUM, + translation_key="a01_error", + entity_category=EntityCategory.DIAGNOSTIC, + options=DyadError.keys(), + ), + RoborockSensorDescriptionA01( + key="total_cleaning_time", + native_unit_of_measurement=UnitOfTime.MINUTES, + data_protocol=RoborockDyadDataProtocol.TOTAL_RUN_TIME, + device_class=SensorDeviceClass.DURATION, + translation_key="total_cleaning_time", + entity_category=EntityCategory.DIAGNOSTIC, + ), +] + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Roborock vacuum sensors.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( RoborockSensorEntity( - f"{description.key}_{slugify(device_id)}", coordinator, description, ) - for device_id, coordinator in coordinators.items() + for coordinator in coordinators.v1 for description in SENSOR_DESCRIPTIONS if description.value_fn(coordinator.roborock_device_info.props) is not None ) + async_add_entities( + RoborockSensorEntityA01( + coordinator, + description, + ) + for coordinator in coordinators.a01 + for description in A01_SENSOR_DESCRIPTIONS + if description.data_protocol in coordinator.data + ) -class RoborockSensorEntity(RoborockCoordinatedEntity, SensorEntity): +class RoborockSensorEntity(RoborockCoordinatedEntityV1, SensorEntity): """Representation of a Roborock sensor.""" entity_description: RoborockSensorDescription def __init__( self, - unique_id: str, coordinator: RoborockDataUpdateCoordinator, description: RoborockSensorDescription, ) -> None: """Initialize the entity.""" self.entity_description = description - super().__init__(unique_id, coordinator, description.protocol_listener) + super().__init__( + f"{description.key}_{slugify(coordinator.duid)}", + coordinator, + description.protocol_listener, + ) @property def native_value(self) -> StateType | datetime.datetime: @@ -235,3 +304,23 @@ class RoborockSensorEntity(RoborockCoordinatedEntity, SensorEntity): return self.entity_description.value_fn( self.coordinator.roborock_device_info.props ) + + +class RoborockSensorEntityA01(RoborockCoordinatedEntityA01, SensorEntity): + """Representation of a A01 Roborock sensor.""" + + entity_description: RoborockSensorDescriptionA01 + + def __init__( + self, + coordinator: RoborockDataUpdateCoordinatorA01, + description: RoborockSensorDescriptionA01, + ) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(f"{description.key}_{slugify(coordinator.duid)}", coordinator) + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + return self.coordinator.data[self.entity_description.data_protocol] diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index aaf476d7fc6..c7fc34386fd 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -95,6 +95,54 @@ } }, "sensor": { + "a01_error": { + "name": "Error", + "state": { + "none": "[%key:component::roborock::entity::sensor::vacuum_error::state::none%]", + "dirty_tank_full": "Dirty tank full", + "water_level_sensor_stuck": "Water level sensor stuck.", + "clean_tank_empty": "Clean tank empty", + "clean_head_entangled": "Cleaning head entangled", + "clean_head_too_hot": "Cleaning head too hot.", + "fan_protection_e5": "Fan protection", + "cleaning_head_blocked": "Cleaning head blocked", + "temperature_protection": "Temperature protection", + "fan_protection_e4": "[%key:component::roborock::entity::sensor::a01_error::state::fan_protection_e5%]", + "fan_protection_e9": "[%key:component::roborock::entity::sensor::a01_error::state::fan_protection_e5%]", + "battery_temperature_protection_e0": "[%key:component::roborock::entity::sensor::a01_error::state::temperature_protection%]", + "battery_temperature_protection": "Battery temperature protection", + "battery_temperature_protection_2": "[%key:component::roborock::entity::sensor::a01_error::state::battery_temperature_protection%]", + "power_adapter_error": "Power adapter error", + "dirty_charging_contacts": "Clean charging contacts", + "low_battery": "[%key:component::roborock::entity::sensor::vacuum_error::state::low_battery%]", + "battery_under_10": "Battery under 10%" + } + }, + "a01_status": { + "name": "Status", + "state": { + "unknown": "[%key:component::roborock::entity::sensor::status::state::unknown%]", + "fetching": "Fetching", + "fetch_failed": "Fetch failed", + "updating": "[%key:component::roborock::entity::sensor::status::state::updating%]", + "washing": "Washing", + "ready": "Ready", + "charging": "[%key:component::roborock::entity::sensor::status::state::charging%]", + "mop_washing": "Washing mop", + "self_clean_cleaning": "Self clean cleaning", + "self_clean_deep_cleaning": "Self clean deep cleaning", + "self_clean_rinsing": "Self clean rinsing", + "self_clean_dehydrating": "Self clean drying", + "drying": "Drying", + "ventilating": "Ventilating", + "reserving": "Reserving", + "mop_washing_paused": "Mop washing paused", + "dusting_mode": "Dusting mode" + } + }, + "brush_remaining": { + "name": "Roller left" + }, "cleaning_area": { "name": "Cleaning area" }, diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index 9a34060fe96..cdfc0c2dc96 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -18,9 +18,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockEntity +from .device import RoborockEntityV1 _LOGGER = logging.getLogger(__name__) @@ -102,14 +103,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock switch platform.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] possible_entities: list[ tuple[RoborockDataUpdateCoordinator, RoborockSwitchDescription] ] = [ (coordinator, description) - for coordinator in coordinators.values() + for coordinator in coordinators.v1 for description in SWITCH_DESCRIPTIONS ] # We need to check if this function is supported by the device. @@ -129,7 +128,7 @@ async def async_setup_entry( else: valid_entities.append( RoborockSwitch( - f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}", + f"{description.key}_{slugify(coordinator.duid)}", coordinator, description, ) @@ -137,7 +136,7 @@ async def async_setup_entry( async_add_entities(valid_entities) -class RoborockSwitch(RoborockEntity, SwitchEntity): +class RoborockSwitch(RoborockEntityV1, SwitchEntity): """A class to let you turn functionality on Roborock devices on and off that does need a coordinator.""" entity_description: RoborockSwitchDescription diff --git a/homeassistant/components/roborock/time.py b/homeassistant/components/roborock/time.py index 6ccc2ef0b27..21ab26c0013 100644 --- a/homeassistant/components/roborock/time.py +++ b/homeassistant/components/roborock/time.py @@ -19,9 +19,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockEntity +from .device import RoborockEntityV1 _LOGGER = logging.getLogger(__name__) @@ -118,14 +119,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock time platform.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] possible_entities: list[ tuple[RoborockDataUpdateCoordinator, RoborockTimeDescription] ] = [ (coordinator, description) - for coordinator in coordinators.values() + for coordinator in coordinators.v1 for description in TIME_DESCRIPTIONS ] # We need to check if this function is supported by the device. @@ -145,7 +144,7 @@ async def async_setup_entry( else: valid_entities.append( RoborockTimeEntity( - f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}", + f"{description.key}_{slugify(coordinator.duid)}", coordinator, description, ) @@ -153,7 +152,7 @@ async def async_setup_entry( async_add_entities(valid_entities) -class RoborockTimeEntity(RoborockEntity, TimeEntity): +class RoborockTimeEntity(RoborockEntityV1, TimeEntity): """A class to let you set options on a Roborock vacuum where the potential options are fixed.""" entity_description: RoborockTimeDescription diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index 16cf518aa02..cefcc85d7f8 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -23,9 +23,10 @@ from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify +from . import RoborockCoordinators from .const import DOMAIN, GET_MAPS_SERVICE_NAME from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockCoordinatedEntity +from .device import RoborockCoordinatedEntityV1 STATE_CODE_TO_STATE = { RoborockStateCode.starting: STATE_IDLE, # "Starting" @@ -60,12 +61,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Roborock sensor.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - RoborockVacuum(slugify(device_id), coordinator) - for device_id, coordinator in coordinators.items() + RoborockVacuum(coordinator) + for coordinator in coordinators.v1 + if isinstance(coordinator, RoborockDataUpdateCoordinator) ) platform = entity_platform.async_get_current_platform() @@ -78,7 +78,7 @@ async def async_setup_entry( ) -class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): +class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity): """General Representation of a Roborock vacuum.""" _attr_icon = "mdi:robot-vacuum" @@ -99,14 +99,13 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): def __init__( self, - unique_id: str, coordinator: RoborockDataUpdateCoordinator, ) -> None: """Initialize a vacuum.""" StateVacuumEntity.__init__(self) - RoborockCoordinatedEntity.__init__( + RoborockCoordinatedEntityV1.__init__( self, - unique_id, + slugify(coordinator.duid), coordinator, listener_request=[ RoborockDataProtocol.FAN_POWER, diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index d3bb0a221b1..a7ebbf10af3 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -1,9 +1,13 @@ """Global fixtures for Roborock integration.""" +from copy import deepcopy from unittest.mock import patch import pytest from roborock import RoomMapping +from roborock.code_mappings import DyadError, RoborockDyadStateCode +from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol +from roborock.version_a01_apis import RoborockMqttClientA01 from homeassistant.components.roborock.const import ( CONF_BASE_URL, @@ -28,6 +32,28 @@ from .mock_data import ( from tests.common import MockConfigEntry +class A01Mock(RoborockMqttClientA01): + """A class to mock the A01 client.""" + + def __init__(self, user_data, device_info, category) -> None: + """Initialize the A01Mock.""" + super().__init__(user_data, device_info, category) + self.protocol_responses = { + RoborockDyadDataProtocol.STATUS: RoborockDyadStateCode.drying.name, + RoborockDyadDataProtocol.POWER: 100, + RoborockDyadDataProtocol.MESH_LEFT: 111, + RoborockDyadDataProtocol.BRUSH_LEFT: 222, + RoborockDyadDataProtocol.ERROR: DyadError.none.name, + RoborockDyadDataProtocol.TOTAL_RUN_TIME: 213, + } + + async def update_values( + self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol] + ): + """Update values with a predetermined response that can be overridden.""" + return {prot: self.protocol_responses[prot] for prot in dyad_data_protocols} + + @pytest.fixture(name="bypass_api_fixture") def bypass_api_fixture() -> None: """Skip calls to the API.""" @@ -35,7 +61,7 @@ def bypass_api_fixture() -> None: patch("homeassistant.components.roborock.RoborockMqttClientV1.async_connect"), patch("homeassistant.components.roborock.RoborockMqttClientV1._send_command"), patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", return_value=HOME_DATA, ), patch( @@ -95,6 +121,23 @@ def bypass_api_fixture() -> None: "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1", return_value=b"123", ), + patch( + "homeassistant.components.roborock.coordinator.RoborockClientA01", + A01Mock, + ), + patch("homeassistant.components.roborock.RoborockMqttClientA01", A01Mock), + ): + yield + + +@pytest.fixture +def bypass_api_fixture_v1_only(bypass_api_fixture) -> None: + """Bypass api for tests that require only having v1 devices.""" + home_data_copy = deepcopy(HOME_DATA) + home_data_copy.received_devices = [] + with patch( + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + return_value=home_data_copy, ): yield diff --git a/tests/components/roborock/snapshots/test_diagnostics.ambr b/tests/components/roborock/snapshots/test_diagnostics.ambr index 3d78e5fd638..4318b537a2c 100644 --- a/tests/components/roborock/snapshots/test_diagnostics.ambr +++ b/tests/components/roborock/snapshots/test_diagnostics.ambr @@ -588,6 +588,369 @@ }), }), }), + '**REDACTED-2**': dict({ + 'api': dict({ + 'misc_info': dict({ + }), + }), + 'roborock_device_info': dict({ + 'device': dict({ + 'activeTime': 1700754026, + 'deviceStatus': dict({ + '10001': '{"f":"t"}', + '10002': '', + '10004': '{"sid_in_use":25,"sid_version":5,"location":"de","bom":"A.03.0291","language":"en"}', + '10005': '{"sn":"dyad_sn","ssid":"dyad_ssid","timezone":"Europe/Stockholm","posix_timezone":"CET-1CEST,M3.5.0,M10.5.0/3","ip":"1.123.12.1","mac":"b0:4a:33:33:33:33","oba":{"language":"en","name":"A.03.0291_CE","bom":"A.03.0291","location":"de","wifiplan":"EU","timezone":"CET-1CEST,M3.5.0,M10.5.0/3;Europe/Berlin","logserver":"awsde0","featureset":"0"}"}', + '10007': '{"mqttOtaData":{"mqttOtaStatus":{"status":"IDLE"}}}', + '200': 0, + '201': 3, + '202': 0, + '203': 2, + '204': 1, + '205': 1, + '206': 3, + '207': 4, + '208': 1, + '209': 100, + '210': 0, + '212': 1, + '213': 1, + '214': 513, + '215': 513, + '216': 0, + '221': 100, + '222': 0, + '223': 2, + '224': 1, + '225': 360, + '226': 0, + '227': 1320, + '228': 360, + '229': '000,000,003,000,005,000,000,000,003,000,005,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,012,003,000,000', + '230': 352, + '235': 0, + '237': 0, + }), + 'duid': '**REDACTED**', + 'f': False, + 'fv': '01.12.34', + 'iconUrl': '', + 'localKey': '**REDACTED**', + 'name': 'Dyad Pro', + 'online': True, + 'productId': 'dyad_product', + 'pv': 'A01', + 'share': True, + 'shareTime': 1701367095, + 'silentOtaSwitch': False, + 'timeZoneId': 'Europe/Stockholm', + 'tuyaMigrated': False, + }), + 'product': dict({ + 'capability': 2, + 'category': 'roborock.wetdryvac', + 'id': 'dyad_product', + 'model': 'roborock.wetdryvac.a56', + 'name': 'Roborock Dyad Pro', + 'schema': list([ + dict({ + 'code': 'drying_status', + 'id': '134', + 'mode': 'ro', + 'name': '烘干状态', + 'type': 'RAW', + }), + dict({ + 'code': 'start', + 'id': '200', + 'mode': 'rw', + 'name': '启停', + 'type': 'VALUE', + }), + dict({ + 'code': 'status', + 'id': '201', + 'mode': 'ro', + 'name': '状态', + 'type': 'VALUE', + }), + dict({ + 'code': 'self_clean_mode', + 'id': '202', + 'mode': 'rw', + 'name': '自清洁模式', + 'type': 'VALUE', + }), + dict({ + 'code': 'self_clean_level', + 'id': '203', + 'mode': 'rw', + 'name': '自清洁强度', + 'type': 'VALUE', + }), + dict({ + 'code': 'warm_level', + 'id': '204', + 'mode': 'rw', + 'name': '烘干强度', + 'type': 'VALUE', + }), + dict({ + 'code': 'clean_mode', + 'id': '205', + 'mode': 'rw', + 'name': '洗地模式', + 'type': 'VALUE', + }), + dict({ + 'code': 'suction', + 'id': '206', + 'mode': 'rw', + 'name': '吸力', + 'type': 'VALUE', + }), + dict({ + 'code': 'water_level', + 'id': '207', + 'mode': 'rw', + 'name': '水量', + 'type': 'VALUE', + }), + dict({ + 'code': 'brush_speed', + 'id': '208', + 'mode': 'rw', + 'name': '滚刷转速', + 'type': 'VALUE', + }), + dict({ + 'code': 'power', + 'id': '209', + 'mode': 'ro', + 'name': '电量', + 'type': 'VALUE', + }), + dict({ + 'code': 'countdown_time', + 'id': '210', + 'mode': 'rw', + 'name': '预约时间', + 'type': 'VALUE', + }), + dict({ + 'code': 'auto_self_clean_set', + 'id': '212', + 'mode': 'rw', + 'name': '自动自清洁', + 'type': 'VALUE', + }), + dict({ + 'code': 'auto_dry', + 'id': '213', + 'mode': 'rw', + 'name': '自动烘干', + 'type': 'VALUE', + }), + dict({ + 'code': 'mesh_left', + 'id': '214', + 'mode': 'ro', + 'name': '滤网已工作时间', + 'type': 'VALUE', + }), + dict({ + 'code': 'brush_left', + 'id': '215', + 'mode': 'ro', + 'name': '滚刷已工作时间', + 'type': 'VALUE', + }), + dict({ + 'code': 'error', + 'id': '216', + 'mode': 'ro', + 'name': '错误值', + 'type': 'VALUE', + }), + dict({ + 'code': 'mesh_reset', + 'id': '218', + 'mode': 'rw', + 'name': '滤网重置', + 'type': 'VALUE', + }), + dict({ + 'code': 'brush_reset', + 'id': '219', + 'mode': 'rw', + 'name': '滚刷重置', + 'type': 'VALUE', + }), + dict({ + 'code': 'volume_set', + 'id': '221', + 'mode': 'rw', + 'name': '音量', + 'type': 'VALUE', + }), + dict({ + 'code': 'stand_lock_auto_run', + 'id': '222', + 'mode': 'rw', + 'name': '直立解锁自动运行开关', + 'type': 'VALUE', + }), + dict({ + 'code': 'auto_self_clean_set_mode', + 'id': '223', + 'mode': 'rw', + 'name': '自动自清洁 - 模式', + 'type': 'VALUE', + }), + dict({ + 'code': 'auto_dry_mode', + 'id': '224', + 'mode': 'rw', + 'name': '自动烘干 - 模式', + 'type': 'VALUE', + }), + dict({ + 'code': 'silent_dry_duration', + 'id': '225', + 'mode': 'rw', + 'name': '静音烘干时长', + 'type': 'VALUE', + }), + dict({ + 'code': 'silent_mode', + 'id': '226', + 'mode': 'rw', + 'name': '勿扰模式开关', + 'type': 'VALUE', + }), + dict({ + 'code': 'silent_mode_start_time', + 'id': '227', + 'mode': 'rw', + 'name': '勿扰开启时间', + 'type': 'VALUE', + }), + dict({ + 'code': 'silent_mode_end_time', + 'id': '228', + 'mode': 'rw', + 'name': '勿扰结束时间', + 'type': 'VALUE', + }), + dict({ + 'code': 'recent_run_time', + 'id': '229', + 'mode': 'rw', + 'name': '近30天每天洗地时长', + 'type': 'STRING', + }), + dict({ + 'code': 'total_run_time', + 'id': '230', + 'mode': 'rw', + 'name': '洗地总时长', + 'type': 'VALUE', + }), + dict({ + 'code': 'feature_info', + 'id': '235', + 'mode': 'ro', + 'name': 'featureinfo', + 'type': 'VALUE', + }), + dict({ + 'code': 'recover_settings', + 'id': '236', + 'mode': 'rw', + 'name': '恢复初始设置', + 'type': 'VALUE', + }), + dict({ + 'code': 'dry_countdown', + 'id': '237', + 'mode': 'ro', + 'name': '烘干倒计时', + 'type': 'VALUE', + }), + dict({ + 'code': 'id_query', + 'id': '10000', + 'mode': 'rw', + 'name': 'ID点数据查询', + 'type': 'STRING', + }), + dict({ + 'code': 'f_c', + 'id': '10001', + 'mode': 'ro', + 'name': '防串货', + 'type': 'STRING', + }), + dict({ + 'code': 'schedule_task', + 'id': '10002', + 'mode': 'rw', + 'name': '定时任务', + 'type': 'STRING', + }), + dict({ + 'code': 'snd_switch', + 'id': '10003', + 'mode': 'rw', + 'name': '语音包切换', + 'type': 'STRING', + }), + dict({ + 'code': 'snd_state', + 'id': '10004', + 'mode': 'rw', + 'name': '语音包/OBA信息', + 'type': 'STRING', + }), + dict({ + 'code': 'product_info', + 'id': '10005', + 'mode': 'ro', + 'name': '产品信息', + 'type': 'STRING', + }), + dict({ + 'code': 'privacy_info', + 'id': '10006', + 'mode': 'rw', + 'name': '隐私协议', + 'type': 'STRING', + }), + dict({ + 'code': 'ota_nfo', + 'id': '10007', + 'mode': 'ro', + 'name': 'OTA info', + 'type': 'STRING', + }), + dict({ + 'code': 'rpc_req', + 'id': '10101', + 'mode': 'wo', + 'name': 'rpc req', + 'type': 'STRING', + }), + dict({ + 'code': 'rpc_resp', + 'id': '10102', + 'mode': 'ro', + 'name': 'rpc resp', + 'type': 'STRING', + }), + ]), + }), + }), + }), }), }) # --- diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index de858ef7cb2..0437ce781f1 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -1,7 +1,9 @@ """Test for Roborock init.""" +from copy import deepcopy from unittest.mock import patch +import pytest from roborock import RoborockException, RoborockInvalidCredentials from homeassistant.components.roborock.const import DOMAIN @@ -9,6 +11,8 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from .mock_data import HOME_DATA + from tests.common import MockConfigEntry @@ -34,7 +38,7 @@ async def test_config_entry_not_ready( """Test that when coordinator update fails, entry retries.""" with ( patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", ), patch( "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", @@ -51,7 +55,7 @@ async def test_config_entry_not_ready_home_data( """Test that when we fail to get home data, entry retries.""" with ( patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", side_effect=RoborockException(), ), patch( @@ -64,7 +68,9 @@ async def test_config_entry_not_ready_home_data( async def test_get_networking_fails( - hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture + hass: HomeAssistant, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture_v1_only, ) -> None: """Test that when networking fails, we attempt to retry.""" with patch( @@ -76,7 +82,9 @@ async def test_get_networking_fails( async def test_get_networking_fails_none( - hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture + hass: HomeAssistant, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture_v1_only, ) -> None: """Test that when networking returns None, we attempt to retry.""" with patch( @@ -88,7 +96,9 @@ async def test_get_networking_fails_none( async def test_cloud_client_fails_props( - hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture + hass: HomeAssistant, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture_v1_only, ) -> None: """Test that if networking succeeds, but we can't communicate with the vacuum, we can't get props, fail.""" with ( @@ -106,7 +116,9 @@ async def test_cloud_client_fails_props( async def test_local_client_fails_props( - hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture + hass: HomeAssistant, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture_v1_only, ) -> None: """Test that if networking succeeds, but we can't communicate locally with the vacuum, we can't get props, fail.""" with patch( @@ -118,7 +130,9 @@ async def test_local_client_fails_props( async def test_fails_maps_continue( - hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture + hass: HomeAssistant, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture_v1_only, ) -> None: """Test that if we fail to get the maps, we still setup.""" with patch( @@ -136,7 +150,7 @@ async def test_reauth_started( ) -> None: """Test reauth flow started.""" with patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", side_effect=RoborockInvalidCredentials(), ): await async_setup_component(hass, DOMAIN, {}) @@ -145,3 +159,21 @@ async def test_reauth_started( flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 assert flows[0]["step_id"] == "reauth_confirm" + + +async def test_not_supported_protocol( + hass: HomeAssistant, + bypass_api_fixture, + mock_roborock_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that we output a message on incorrect protocol.""" + home_data_copy = deepcopy(HOME_DATA) + home_data_copy.received_devices[0].pv = "random" + with patch( + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + return_value=home_data_copy, + ): + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert "because its protocol version random" in caplog.text diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index 88ed6e1098c..e608895ca43 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -21,7 +21,7 @@ from tests.common import MockConfigEntry async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> None: """Test sensors and check test values are correctly set.""" - assert len(hass.states.async_all("sensor")) == 28 + assert len(hass.states.async_all("sensor")) == 34 assert hass.states.get("sensor.roborock_s7_maxv_main_brush_time_left").state == str( MAIN_BRUSH_REPLACE_TIME - 74382 ) @@ -54,6 +54,12 @@ async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> Non hass.states.get("sensor.roborock_s7_maxv_last_clean_end").state == "2023-01-01T03:43:58+00:00" ) + assert hass.states.get("sensor.dyad_pro_status").state == "drying" + assert hass.states.get("sensor.dyad_pro_battery").state == "100" + assert hass.states.get("sensor.dyad_pro_filter_time_left").state == "111" + assert hass.states.get("sensor.dyad_pro_roller_left").state == "222" + assert hass.states.get("sensor.dyad_pro_error").state == "none" + assert hass.states.get("sensor.dyad_pro_total_cleaning_time").state == "213" async def test_listener_update( From 66a803e56ca019a6e99b84fb1ff9399ccc13654e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 15:41:20 +0200 Subject: [PATCH 0248/2411] Disable Aladdin Connect (#120558) Co-authored-by: Robert Resch --- .../components/aladdin_connect/__init__.py | 4 +- .../components/aladdin_connect/api.py | 3 +- .../components/aladdin_connect/coordinator.py | 6 +- .../components/aladdin_connect/cover.py | 4 +- .../components/aladdin_connect/entity.py | 4 +- .../components/aladdin_connect/manifest.json | 1 + .../components/aladdin_connect/ruff.toml | 5 + .../components/aladdin_connect/sensor.py | 6 +- requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/aladdin_connect/conftest.py | 4 +- .../aladdin_connect/test_config_flow.py | 451 +++++++++--------- 12 files changed, 249 insertions(+), 245 deletions(-) create mode 100644 homeassistant/components/aladdin_connect/ruff.toml diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 436e797271f..ed284c0e6bb 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -1,9 +1,9 @@ """The Aladdin Connect Genie integration.""" +# mypy: ignore-errors from __future__ import annotations -from genie_partner_sdk.client import AladdinConnectClient - +# from genie_partner_sdk.client import AladdinConnectClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/aladdin_connect/api.py b/homeassistant/components/aladdin_connect/api.py index c4a19ef0081..4377fc8fbcb 100644 --- a/homeassistant/components/aladdin_connect/api.py +++ b/homeassistant/components/aladdin_connect/api.py @@ -1,10 +1,11 @@ """API for Aladdin Connect Genie bound to Home Assistant OAuth.""" +# mypy: ignore-errors from typing import cast from aiohttp import ClientSession -from genie_partner_sdk.auth import Auth +# from genie_partner_sdk.auth import Auth from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1" diff --git a/homeassistant/components/aladdin_connect/coordinator.py b/homeassistant/components/aladdin_connect/coordinator.py index d9af0da9450..9af3e330409 100644 --- a/homeassistant/components/aladdin_connect/coordinator.py +++ b/homeassistant/components/aladdin_connect/coordinator.py @@ -1,11 +1,11 @@ """Define an object to coordinate fetching Aladdin Connect data.""" +# mypy: ignore-errors from datetime import timedelta import logging -from genie_partner_sdk.client import AladdinConnectClient -from genie_partner_sdk.model import GarageDoor - +# from genie_partner_sdk.client import AladdinConnectClient +# from genie_partner_sdk.model import GarageDoor from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index b8c48048192..1be41e6b516 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -1,9 +1,9 @@ """Cover Entity for Genie Garage Door.""" +# mypy: ignore-errors from typing import Any -from genie_partner_sdk.model import GarageDoor - +# from genie_partner_sdk.model import GarageDoor from homeassistant.components.cover import ( CoverDeviceClass, CoverEntity, diff --git a/homeassistant/components/aladdin_connect/entity.py b/homeassistant/components/aladdin_connect/entity.py index 8d9eeefcdfb..2615cbc636e 100644 --- a/homeassistant/components/aladdin_connect/entity.py +++ b/homeassistant/components/aladdin_connect/entity.py @@ -1,6 +1,6 @@ """Defines a base Aladdin Connect entity.""" - -from genie_partner_sdk.model import GarageDoor +# mypy: ignore-errors +# from genie_partner_sdk.model import GarageDoor from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 69b38399cce..dce95492272 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -4,6 +4,7 @@ "codeowners": ["@swcloudgenie"], "config_flow": true, "dependencies": ["application_credentials"], + "disabled": "This integration is disabled because it uses non-open source code to operate.", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "iot_class": "cloud_polling", "requirements": ["genie-partner-sdk==1.0.2"] diff --git a/homeassistant/components/aladdin_connect/ruff.toml b/homeassistant/components/aladdin_connect/ruff.toml new file mode 100644 index 00000000000..38f6f586aef --- /dev/null +++ b/homeassistant/components/aladdin_connect/ruff.toml @@ -0,0 +1,5 @@ +extend = "../../../pyproject.toml" + +lint.extend-ignore = [ + "F821" +] diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index 2bd0168a500..cd1fff12c97 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -1,13 +1,13 @@ """Support for Aladdin Connect Garage Door sensors.""" +# mypy: ignore-errors from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from genie_partner_sdk.client import AladdinConnectClient -from genie_partner_sdk.model import GarageDoor - +# from genie_partner_sdk.client import AladdinConnectClient +# from genie_partner_sdk.model import GarageDoor from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, diff --git a/requirements_all.txt b/requirements_all.txt index f4de7dafa87..a3a62b58b4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -926,9 +926,6 @@ gassist-text==0.0.11 # homeassistant.components.google gcal-sync==6.0.4 -# homeassistant.components.aladdin_connect -genie-partner-sdk==1.0.2 - # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12ab15a7d76..3f05bcc3d33 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -764,9 +764,6 @@ gassist-text==0.0.11 # homeassistant.components.google gcal-sync==6.0.4 -# homeassistant.components.aladdin_connect -genie-partner-sdk==1.0.2 - # homeassistant.components.geocaching geocachingapi==0.2.1 diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py index c7e5190d527..2c158998f49 100644 --- a/tests/components/aladdin_connect/conftest.py +++ b/tests/components/aladdin_connect/conftest.py @@ -5,8 +5,6 @@ from unittest.mock import AsyncMock, patch import pytest from typing_extensions import Generator -from homeassistant.components.aladdin_connect import DOMAIN - from tests.common import MockConfigEntry @@ -23,7 +21,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: def mock_config_entry() -> MockConfigEntry: """Return an Aladdin Connect config entry.""" return MockConfigEntry( - domain=DOMAIN, + domain="aladdin_connect", data={}, title="test@test.com", unique_id="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 1537e0f35da..7154c53b9f6 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -1,225 +1,230 @@ """Test the Aladdin Connect Garage Door config flow.""" -from unittest.mock import AsyncMock - -import pytest - -from homeassistant.components.aladdin_connect.const import ( - DOMAIN, - OAUTH2_AUTHORIZE, - OAUTH2_TOKEN, -) -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigFlowResult -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import config_entry_oauth2_flow -from homeassistant.setup import async_setup_component - -from tests.common import MockConfigEntry -from tests.test_util.aiohttp import AiohttpClientMocker -from tests.typing import ClientSessionGenerator - -CLIENT_ID = "1234" -CLIENT_SECRET = "5678" - -EXAMPLE_TOKEN = ( - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYWFhYWFhYS1iYmJiLWNjY2MtZGRk" - "ZC1lZWVlZWVlZWVlZWUiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW" - "1lIjoidGVzdEB0ZXN0LmNvbSJ9.CTU1YItIrUl8nSM3koJxlFJr5CjLghgc9gS6h45D8dE" -) - - -@pytest.fixture -async def setup_credentials(hass: HomeAssistant) -> None: - """Fixture to setup credentials.""" - assert await async_setup_component(hass, "application_credentials", {}) - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential(CLIENT_ID, CLIENT_SECRET), - ) - - -async def _oauth_actions( - hass: HomeAssistant, - result: ConfigFlowResult, - hass_client_no_auth: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, -) -> None: - state = config_entry_oauth2_flow._encode_jwt( - hass, - { - "flow_id": result["flow_id"], - "redirect_uri": "https://example.com/auth/external/callback", - }, - ) - - assert result["url"] == ( - f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" - "&redirect_uri=https://example.com/auth/external/callback" - f"&state={state}" - ) - - client = await hass_client_no_auth() - resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == 200 - assert resp.headers["content-type"] == "text/html; charset=utf-8" - - aioclient_mock.post( - OAUTH2_TOKEN, - json={ - "refresh_token": "mock-refresh-token", - "access_token": EXAMPLE_TOKEN, - "type": "Bearer", - "expires_in": 60, - }, - ) - - -@pytest.mark.usefixtures("current_request_with_host") -async def test_full_flow( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, - setup_credentials: None, - mock_setup_entry: AsyncMock, -) -> None: - """Check full flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "test@test.com" - assert result["data"]["token"]["access_token"] == EXAMPLE_TOKEN - assert result["data"]["token"]["refresh_token"] == "mock-refresh-token" - assert result["result"].unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.usefixtures("current_request_with_host") -async def test_duplicate_entry( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, - setup_credentials: None, - mock_config_entry: MockConfigEntry, -) -> None: - """Test we abort with duplicate entry.""" - mock_config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - -@pytest.mark.usefixtures("current_request_with_host") -async def test_reauth( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, - setup_credentials: None, - mock_config_entry: MockConfigEntry, - mock_setup_entry: AsyncMock, -) -> None: - """Test reauthentication.""" - mock_config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "entry_id": mock_config_entry.entry_id, - }, - data=mock_config_entry.data, - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reauth_successful" - - -@pytest.mark.usefixtures("current_request_with_host") -async def test_reauth_wrong_account( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, - setup_credentials: None, - mock_setup_entry: AsyncMock, -) -> None: - """Test reauthentication with wrong account.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={}, - title="test@test.com", - unique_id="aaaaaaaa-bbbb-ffff-dddd-eeeeeeeeeeee", - version=2, - ) - config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "entry_id": config_entry.entry_id, - }, - data=config_entry.data, - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "wrong_account" - - -@pytest.mark.usefixtures("current_request_with_host") -async def test_reauth_old_account( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, - setup_credentials: None, - mock_setup_entry: AsyncMock, -) -> None: - """Test reauthentication with old account.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={}, - title="test@test.com", - unique_id="test@test.com", - version=2, - ) - config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "entry_id": config_entry.entry_id, - }, - data=config_entry.data, - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reauth_successful" - assert config_entry.unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" +# from unittest.mock import AsyncMock +# +# import pytest +# +# from homeassistant.components.aladdin_connect.const import ( +# DOMAIN, +# OAUTH2_AUTHORIZE, +# OAUTH2_TOKEN, +# ) +# from homeassistant.components.application_credentials import ( +# ClientCredential, +# async_import_client_credential, +# ) +# from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigFlowResult +# from homeassistant.core import HomeAssistant +# from homeassistant.data_entry_flow import FlowResultType +# from homeassistant.helpers import config_entry_oauth2_flow +# from homeassistant.setup import async_setup_component +# +# from tests.common import MockConfigEntry +# from tests.test_util.aiohttp import AiohttpClientMocker +# from tests.typing import ClientSessionGenerator +# +# CLIENT_ID = "1234" +# CLIENT_SECRET = "5678" +# +# EXAMPLE_TOKEN = ( +# "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYWFhYWFhYS1iYmJiLWNjY2MtZGRk" +# "ZC1lZWVlZWVlZWVlZWUiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW" +# "1lIjoidGVzdEB0ZXN0LmNvbSJ9.CTU1YItIrUl8nSM3koJxlFJr5CjLghgc9gS6h45D8dE" +# ) +# +# +# @pytest.fixture +# async def setup_credentials(hass: HomeAssistant) -> None: +# """Fixture to setup credentials.""" +# assert await async_setup_component(hass, "application_credentials", {}) +# await async_import_client_credential( +# hass, +# DOMAIN, +# ClientCredential(CLIENT_ID, CLIENT_SECRET), +# ) +# +# +# async def _oauth_actions( +# hass: HomeAssistant, +# result: ConfigFlowResult, +# hass_client_no_auth: ClientSessionGenerator, +# aioclient_mock: AiohttpClientMocker, +# ) -> None: +# state = config_entry_oauth2_flow._encode_jwt( +# hass, +# { +# "flow_id": result["flow_id"], +# "redirect_uri": "https://example.com/auth/external/callback", +# }, +# ) +# +# assert result["url"] == ( +# f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" +# "&redirect_uri=https://example.com/auth/external/callback" +# f"&state={state}" +# ) +# +# client = await hass_client_no_auth() +# resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") +# assert resp.status == 200 +# assert resp.headers["content-type"] == "text/html; charset=utf-8" +# +# aioclient_mock.post( +# OAUTH2_TOKEN, +# json={ +# "refresh_token": "mock-refresh-token", +# "access_token": EXAMPLE_TOKEN, +# "type": "Bearer", +# "expires_in": 60, +# }, +# ) +# +# +# @pytest.mark.skip(reason="Integration disabled") +# @pytest.mark.usefixtures("current_request_with_host") +# async def test_full_flow( +# hass: HomeAssistant, +# hass_client_no_auth: ClientSessionGenerator, +# aioclient_mock: AiohttpClientMocker, +# setup_credentials: None, +# mock_setup_entry: AsyncMock, +# ) -> None: +# """Check full flow.""" +# result = await hass.config_entries.flow.async_init( +# DOMAIN, context={"source": SOURCE_USER} +# ) +# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) +# +# result = await hass.config_entries.flow.async_configure(result["flow_id"]) +# assert result["type"] is FlowResultType.CREATE_ENTRY +# assert result["title"] == "test@test.com" +# assert result["data"]["token"]["access_token"] == EXAMPLE_TOKEN +# assert result["data"]["token"]["refresh_token"] == "mock-refresh-token" +# assert result["result"].unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" +# +# assert len(hass.config_entries.async_entries(DOMAIN)) == 1 +# assert len(mock_setup_entry.mock_calls) == 1 +# +# +# @pytest.mark.skip(reason="Integration disabled") +# @pytest.mark.usefixtures("current_request_with_host") +# async def test_duplicate_entry( +# hass: HomeAssistant, +# hass_client_no_auth: ClientSessionGenerator, +# aioclient_mock: AiohttpClientMocker, +# setup_credentials: None, +# mock_config_entry: MockConfigEntry, +# ) -> None: +# """Test we abort with duplicate entry.""" +# mock_config_entry.add_to_hass(hass) +# result = await hass.config_entries.flow.async_init( +# DOMAIN, context={"source": SOURCE_USER} +# ) +# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) +# +# result = await hass.config_entries.flow.async_configure(result["flow_id"]) +# assert result["type"] is FlowResultType.ABORT +# assert result["reason"] == "already_configured" +# +# +# @pytest.mark.skip(reason="Integration disabled") +# @pytest.mark.usefixtures("current_request_with_host") +# async def test_reauth( +# hass: HomeAssistant, +# hass_client_no_auth: ClientSessionGenerator, +# aioclient_mock: AiohttpClientMocker, +# setup_credentials: None, +# mock_config_entry: MockConfigEntry, +# mock_setup_entry: AsyncMock, +# ) -> None: +# """Test reauthentication.""" +# mock_config_entry.add_to_hass(hass) +# result = await hass.config_entries.flow.async_init( +# DOMAIN, +# context={ +# "source": SOURCE_REAUTH, +# "entry_id": mock_config_entry.entry_id, +# }, +# data=mock_config_entry.data, +# ) +# assert result["type"] is FlowResultType.FORM +# assert result["step_id"] == "reauth_confirm" +# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) +# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) +# +# result = await hass.config_entries.flow.async_configure(result["flow_id"]) +# assert result["type"] is FlowResultType.ABORT +# assert result["reason"] == "reauth_successful" +# +# +# @pytest.mark.skip(reason="Integration disabled") +# @pytest.mark.usefixtures("current_request_with_host") +# async def test_reauth_wrong_account( +# hass: HomeAssistant, +# hass_client_no_auth: ClientSessionGenerator, +# aioclient_mock: AiohttpClientMocker, +# setup_credentials: None, +# mock_setup_entry: AsyncMock, +# ) -> None: +# """Test reauthentication with wrong account.""" +# config_entry = MockConfigEntry( +# domain=DOMAIN, +# data={}, +# title="test@test.com", +# unique_id="aaaaaaaa-bbbb-ffff-dddd-eeeeeeeeeeee", +# version=2, +# ) +# config_entry.add_to_hass(hass) +# result = await hass.config_entries.flow.async_init( +# DOMAIN, +# context={ +# "source": SOURCE_REAUTH, +# "entry_id": config_entry.entry_id, +# }, +# data=config_entry.data, +# ) +# assert result["type"] is FlowResultType.FORM +# assert result["step_id"] == "reauth_confirm" +# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) +# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) +# +# result = await hass.config_entries.flow.async_configure(result["flow_id"]) +# assert result["type"] is FlowResultType.ABORT +# assert result["reason"] == "wrong_account" +# +# +# @pytest.mark.skip(reason="Integration disabled") +# @pytest.mark.usefixtures("current_request_with_host") +# async def test_reauth_old_account( +# hass: HomeAssistant, +# hass_client_no_auth: ClientSessionGenerator, +# aioclient_mock: AiohttpClientMocker, +# setup_credentials: None, +# mock_setup_entry: AsyncMock, +# ) -> None: +# """Test reauthentication with old account.""" +# config_entry = MockConfigEntry( +# domain=DOMAIN, +# data={}, +# title="test@test.com", +# unique_id="test@test.com", +# version=2, +# ) +# config_entry.add_to_hass(hass) +# result = await hass.config_entries.flow.async_init( +# DOMAIN, +# context={ +# "source": SOURCE_REAUTH, +# "entry_id": config_entry.entry_id, +# }, +# data=config_entry.data, +# ) +# assert result["type"] is FlowResultType.FORM +# assert result["step_id"] == "reauth_confirm" +# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) +# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) +# +# result = await hass.config_entries.flow.async_configure(result["flow_id"]) +# assert result["type"] is FlowResultType.ABORT +# assert result["reason"] == "reauth_successful" +# assert config_entry.unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" From 294e1d4fc487766d60a4f72f8a5f5977723f38f0 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 15:42:03 +0200 Subject: [PATCH 0249/2411] Fix class name and deprecation version (#120570) --- homeassistant/components/pyload/coordinator.py | 8 ++++---- homeassistant/components/pyload/diagnostics.py | 4 ++-- homeassistant/components/pyload/sensor.py | 8 ++++---- homeassistant/components/pyload/switch.py | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/pyload/coordinator.py b/homeassistant/components/pyload/coordinator.py index fd0e95192b3..c55ca4c1630 100644 --- a/homeassistant/components/pyload/coordinator.py +++ b/homeassistant/components/pyload/coordinator.py @@ -20,7 +20,7 @@ SCAN_INTERVAL = timedelta(seconds=20) @dataclass(kw_only=True) -class pyLoadData: +class PyLoadData: """Data from pyLoad.""" pause: bool @@ -34,7 +34,7 @@ class pyLoadData: free_space: int -class PyLoadCoordinator(DataUpdateCoordinator[pyLoadData]): +class PyLoadCoordinator(DataUpdateCoordinator[PyLoadData]): """pyLoad coordinator.""" config_entry: ConfigEntry @@ -50,12 +50,12 @@ class PyLoadCoordinator(DataUpdateCoordinator[pyLoadData]): self.pyload = pyload self.version: str | None = None - async def _async_update_data(self) -> pyLoadData: + async def _async_update_data(self) -> PyLoadData: """Fetch data from API endpoint.""" try: if not self.version: self.version = await self.pyload.version() - return pyLoadData( + return PyLoadData( **await self.pyload.get_status(), free_space=await self.pyload.free_space(), ) diff --git a/homeassistant/components/pyload/diagnostics.py b/homeassistant/components/pyload/diagnostics.py index 1b719ffc7b9..e9688a3369b 100644 --- a/homeassistant/components/pyload/diagnostics.py +++ b/homeassistant/components/pyload/diagnostics.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from . import PyLoadConfigEntry -from .coordinator import pyLoadData +from .coordinator import PyLoadData TO_REDACT = {CONF_USERNAME, CONF_PASSWORD, CONF_HOST} @@ -19,7 +19,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: PyLoadConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - pyload_data: pyLoadData = config_entry.runtime_data.data + pyload_data: PyLoadData = config_entry.runtime_data.data return { "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 4a0502707b6..a1b29b46260 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -43,7 +43,7 @@ from .const import ( ISSUE_PLACEHOLDER, UNIT_DOWNLOADS, ) -from .coordinator import pyLoadData +from .coordinator import PyLoadData from .entity import BasePyLoadEntity @@ -61,7 +61,7 @@ class PyLoadSensorEntity(StrEnum): class PyLoadSensorEntityDescription(SensorEntityDescription): """Describes pyLoad switch entity.""" - value_fn: Callable[[pyLoadData], StateType] + value_fn: Callable[[PyLoadData], StateType] SENSOR_DESCRIPTIONS: tuple[PyLoadSensorEntityDescription, ...] = ( @@ -142,7 +142,7 @@ async def async_setup_platform( f"deprecated_yaml_{DOMAIN}", is_fixable=False, issue_domain=DOMAIN, - breaks_in_ha_version="2025.2.0", + breaks_in_ha_version="2025.1.0", severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", translation_placeholders={ @@ -155,7 +155,7 @@ async def async_setup_platform( hass, DOMAIN, f"deprecated_yaml_import_issue_{error}", - breaks_in_ha_version="2025.2.0", + breaks_in_ha_version="2025.1.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, diff --git a/homeassistant/components/pyload/switch.py b/homeassistant/components/pyload/switch.py index 21c8d75aaa0..5e8c61823dd 100644 --- a/homeassistant/components/pyload/switch.py +++ b/homeassistant/components/pyload/switch.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import PyLoadConfigEntry -from .coordinator import pyLoadData +from .coordinator import PyLoadData from .entity import BasePyLoadEntity @@ -36,7 +36,7 @@ class PyLoadSwitchEntityDescription(SwitchEntityDescription): turn_on_fn: Callable[[PyLoadAPI], Awaitable[Any]] turn_off_fn: Callable[[PyLoadAPI], Awaitable[Any]] toggle_fn: Callable[[PyLoadAPI], Awaitable[Any]] - value_fn: Callable[[pyLoadData], bool] + value_fn: Callable[[PyLoadData], bool] SENSOR_DESCRIPTIONS: tuple[PyLoadSwitchEntityDescription, ...] = ( From f4fa5b581ee5d60fd81061ed672760c2197717fe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:43:48 +0200 Subject: [PATCH 0250/2411] Import PLATFORM_SCHEMA from platform not from helpers (#120565) --- homeassistant/components/enigma2/media_player.py | 6 +++--- homeassistant/components/foobot/sensor.py | 6 +++--- homeassistant/components/openerz/sensor.py | 10 ++++++---- homeassistant/components/template/light.py | 8 +++----- homeassistant/components/template/weather.py | 9 +++------ 5 files changed, 18 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 8e090e7cecb..63acdd8be72 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -11,6 +11,7 @@ from openwebif.enums import PowerState, RemoteControlCodes, SetVolumeOption import voluptuous as vol from homeassistant.components.media_player import ( + PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -26,8 +27,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -57,7 +57,7 @@ ATTR_MEDIA_START_TIME = "media_start_time" _LOGGER = getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index ac8c7e3eec8..f3c6513f051 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -11,6 +11,7 @@ from foobot_async import FoobotClient import voluptuous as vol from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -27,9 +28,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle @@ -86,7 +86,7 @@ PARALLEL_UPDATES = 1 TIMEOUT = 10 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( {vol.Required(CONF_TOKEN): cv.string, vol.Required(CONF_USERNAME): cv.string} ) diff --git a/homeassistant/components/openerz/sensor.py b/homeassistant/components/openerz/sensor.py index 7447f2eafe4..f41b468b224 100644 --- a/homeassistant/components/openerz/sensor.py +++ b/homeassistant/components/openerz/sensor.py @@ -7,10 +7,12 @@ from datetime import timedelta from openerz_api.main import OpenERZConnector import voluptuous as vol -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -20,7 +22,7 @@ CONF_ZIP = "zip" CONF_WASTE_TYPE = "waste_type" CONF_NAME = "name" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ZIP): cv.positive_int, vol.Required(CONF_WASTE_TYPE, default="waste"): cv.string, diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index de8a2998d34..ba6b8ce846b 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -17,6 +17,7 @@ from homeassistant.components.light import ( ATTR_RGBWW_COLOR, ATTR_TRANSITION, ENTITY_ID_FORMAT, + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, ColorMode, LightEntity, LightEntityFeature, @@ -33,10 +34,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script @@ -117,7 +115,7 @@ PLATFORM_SCHEMA = vol.All( # CONF_WHITE_VALUE_* is deprecated, support will be removed in release 2022.9 cv.removed(CONF_WHITE_VALUE_ACTION), cv.removed(CONF_WHITE_VALUE_TEMPLATE), - BASE_PLATFORM_SCHEMA.extend( + LIGHT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_LIGHTS): cv.schema_with_slug_keys(LIGHT_SCHEMA)} ), ) diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index 0f80f65f501..5c3e4107b2c 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -26,6 +26,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY_VARIANT, DOMAIN as WEATHER_DOMAIN, ENTITY_ID_FORMAT, + PLATFORM_SCHEMA as WEATHER_PLATFORM_SCHEMA, Forecast, WeatherEntity, WeatherEntityFeature, @@ -39,11 +40,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, -) +from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity @@ -138,7 +135,7 @@ WEATHER_SCHEMA = vol.Schema( PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_FORECAST_TEMPLATE), - BASE_PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema), + WEATHER_PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema), ) From f65d91f6d221832d852ee55dccc3ae54b9c4cc87 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:44:59 +0200 Subject: [PATCH 0251/2411] Refactor PLATFORM_SCHEMA imports in platforms (#120564) --- .../components/air_quality/__init__.py | 14 +++++------ .../alarm_control_panel/__init__.py | 6 ++--- .../components/binary_sensor/__init__.py | 11 ++++----- homeassistant/components/button/__init__.py | 12 ++++------ homeassistant/components/calendar/__init__.py | 11 ++++----- homeassistant/components/camera/__init__.py | 14 +++++------ homeassistant/components/climate/__init__.py | 23 ++++++++----------- homeassistant/components/cover/__init__.py | 10 ++++---- homeassistant/components/date/__init__.py | 10 ++++---- homeassistant/components/datetime/__init__.py | 10 ++++---- homeassistant/components/event/__init__.py | 13 ++++------- homeassistant/components/fan/__init__.py | 11 ++++----- .../components/geo_location/__init__.py | 16 ++++++------- .../components/humidifier/__init__.py | 12 ++++------ homeassistant/components/image/__init__.py | 9 ++++---- .../components/lawn_mower/__init__.py | 11 ++++----- homeassistant/components/light/__init__.py | 13 ++++------- homeassistant/components/lock/__init__.py | 18 +++++++-------- .../components/media_player/__init__.py | 11 ++++----- homeassistant/components/number/__init__.py | 9 ++++---- homeassistant/components/remote/__init__.py | 20 +++++++--------- homeassistant/components/select/__init__.py | 11 ++++----- homeassistant/components/sensor/__init__.py | 9 +++----- homeassistant/components/siren/__init__.py | 8 +++---- homeassistant/components/switch/__init__.py | 12 ++++------ homeassistant/components/text/__init__.py | 10 ++++---- homeassistant/components/time/__init__.py | 10 ++++---- homeassistant/components/todo/__init__.py | 11 ++++----- homeassistant/components/update/__init__.py | 11 ++++----- homeassistant/components/vacuum/__init__.py | 7 ++---- homeassistant/components/valve/__init__.py | 10 ++++---- .../components/water_heater/__init__.py | 14 +++++------ homeassistant/components/weather/__init__.py | 14 +++++------ 33 files changed, 156 insertions(+), 235 deletions(-) diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 78f2616a74d..9a80ee39e86 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -9,10 +9,7 @@ from typing import Final, final from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, StateType @@ -21,6 +18,11 @@ from .const import DOMAIN _LOGGER: Final = logging.getLogger(__name__) +ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL: Final = timedelta(seconds=30) + ATTR_AQI: Final = "air_quality_index" ATTR_CO2: Final = "carbon_dioxide" ATTR_CO: Final = "carbon_monoxide" @@ -33,10 +35,6 @@ ATTR_PM_10: Final = "particulate_matter_10" ATTR_PM_2_5: Final = "particulate_matter_2_5" ATTR_SO2: Final = "sulphur_dioxide" -ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" - -SCAN_INTERVAL: Final = timedelta(seconds=30) - PROP_TO_ATTR: Final[dict[str, str]] = { "air_quality_index": ATTR_AQI, "carbon_dioxide": ATTR_CO2, diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index f33e168c031..b09d5867d26 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -52,8 +52,10 @@ from .const import ( # noqa: F401 _LOGGER: Final = logging.getLogger(__name__) -SCAN_INTERVAL: Final = timedelta(seconds=30) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" +PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL: Final = timedelta(seconds=30) CONF_DEFAULT_CODE = "default_code" @@ -61,8 +63,6 @@ ALARM_SERVICE_SCHEMA: Final = make_entity_service_schema( {vol.Optional(ATTR_CODE): cv.string} ) -PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA -PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE # mypy: disallow-any-generics diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index dad398e2525..0b3e423e339 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -14,10 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -30,11 +27,11 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) - DOMAIN = "binary_sensor" -SCAN_INTERVAL = timedelta(seconds=30) - ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) class BinarySensorDeviceClass(StrEnum): diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py index cb8ac7745b2..323f9eddd77 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -13,10 +13,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -25,14 +22,15 @@ from homeassistant.util import dt as dt_util from .const import DOMAIN, SERVICE_PRESS -SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -_LOGGER = logging.getLogger(__name__) - class ButtonDeviceClass(StrEnum): """Device class for buttons.""" diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 621356f20e2..b94a6eb935f 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -29,12 +29,7 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, - time_period_str, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_time @@ -74,6 +69,8 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "calendar" ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE SCAN_INTERVAL = datetime.timedelta(seconds=60) # Don't support rrules more often than daily @@ -469,7 +466,7 @@ def extract_offset(summary: str, offset_prefix: str) -> tuple[str, datetime.time else: time = f"0:{time}" - offset_time = time_period_str(time) + offset_time = cv.time_period_str(time) summary = (summary[: search.start()] + summary[search.end() :]).strip() return (summary, offset_time) return (summary, datetime.timedelta()) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 428e8d856fb..d8fa4bfbc7a 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -48,11 +48,7 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -87,14 +83,16 @@ from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401 _LOGGER = logging.getLogger(__name__) +ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL: Final = timedelta(seconds=30) + SERVICE_ENABLE_MOTION: Final = "enable_motion_detection" SERVICE_DISABLE_MOTION: Final = "disable_motion_detection" SERVICE_SNAPSHOT: Final = "snapshot" SERVICE_PLAY_STREAM: Final = "play_stream" -SCAN_INTERVAL: Final = timedelta(seconds=30) -ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" - ATTR_FILENAME: Final = "filename" ATTR_MEDIA_PLAYER: Final = "media_player" ATTR_FORMAT: Final = "format" diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index ac6297dc5b6..bc81ce6e241 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -25,13 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers import issue_registry as ir -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, - make_entity_service_schema, -) +from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.deprecation import ( all_with_deprecated_constants, check_if_deprecated_constant, @@ -117,24 +111,25 @@ from .const import ( # noqa: F401 HVACMode, ) +_LOGGER = logging.getLogger(__name__) + +ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=60) + DEFAULT_MIN_TEMP = 7 DEFAULT_MAX_TEMP = 35 DEFAULT_MIN_HUMIDITY = 30 DEFAULT_MAX_HUMIDITY = 99 -ENTITY_ID_FORMAT = DOMAIN + ".{}" -SCAN_INTERVAL = timedelta(seconds=60) - CONVERTIBLE_ATTRIBUTE = [ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH] -_LOGGER = logging.getLogger(__name__) - - SET_TEMPERATURE_SCHEMA = vol.All( cv.has_at_least_one_key( ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW ), - make_entity_service_schema( + cv.make_entity_service_schema( { vol.Exclusive(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float), vol.Inclusive(ATTR_TARGET_TEMP_HIGH, "temperature"): vol.Coerce(float), diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 852c5fd9cae..645bd88de7a 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -30,10 +30,7 @@ from homeassistant.const import ( STATE_OPENING, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -49,9 +46,10 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=15) - ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=15) class CoverDeviceClass(StrEnum): diff --git a/homeassistant/components/date/__init__.py b/homeassistant/components/date/__init__.py index ddd85ffbf06..7914c6d2984 100644 --- a/homeassistant/components/date/__init__.py +++ b/homeassistant/components/date/__init__.py @@ -13,21 +13,19 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DATE from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, SERVICE_SET_VALUE -SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) -_LOGGER = logging.getLogger(__name__) __all__ = ["DOMAIN", "DateEntity", "DateEntityDescription"] diff --git a/homeassistant/components/datetime/__init__.py b/homeassistant/components/datetime/__init__.py index f2b8526ced6..f418f81da03 100644 --- a/homeassistant/components/datetime/__init__.py +++ b/homeassistant/components/datetime/__init__.py @@ -12,10 +12,6 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -23,11 +19,13 @@ from homeassistant.util import dt as dt_util from .const import ATTR_DATETIME, DOMAIN, SERVICE_SET_VALUE -SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) -_LOGGER = logging.getLogger(__name__) __all__ = ["ATTR_DATETIME", "DOMAIN", "DateTimeEntity", "DateTimeEntityDescription"] diff --git a/homeassistant/components/event/__init__.py b/homeassistant/components/event/__init__.py index 925c0855c71..4ca000f6a40 100644 --- a/homeassistant/components/event/__init__.py +++ b/homeassistant/components/event/__init__.py @@ -11,10 +11,7 @@ from typing import Any, Self, final from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity @@ -23,11 +20,11 @@ from homeassistant.util import dt as dt_util from .const import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES, DOMAIN -SCAN_INTERVAL = timedelta(seconds=30) - -ENTITY_ID_FORMAT = DOMAIN + ".{}" - _LOGGER = logging.getLogger(__name__) +ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) class EventDeviceClass(StrEnum): diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 0bed3eb1ff2..ef6c075a356 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -21,11 +21,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -44,9 +40,10 @@ from homeassistant.util.percentage import ( _LOGGER = logging.getLogger(__name__) DOMAIN = "fan" -SCAN_INTERVAL = timedelta(seconds=30) - ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) class FanEntityFeature(IntFlag): diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 48e2f35ccc1..e0c8d806fe6 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -10,24 +10,22 @@ from typing import Any, final from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) +DOMAIN = "geo_location" +ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=60) + ATTR_DISTANCE = "distance" ATTR_SOURCE = "source" -DOMAIN = "geo_location" - -ENTITY_ID_FORMAT = DOMAIN + ".{}" - -SCAN_INTERVAL = timedelta(seconds=60) # mypy: disallow-any-generics diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index da79df6d52f..ce94eaaf5a0 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -19,11 +19,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( all_with_deprecated_constants, check_if_deprecated_constant, @@ -58,10 +54,10 @@ from .const import ( # noqa: F401 _LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=60) - ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=60) class HumidifierDeviceClass(StrEnum): diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index f40958a28ea..2307a66d5a1 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -20,10 +20,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONTENT_TYPE_MULTIPART, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import ( @@ -37,8 +34,10 @@ from .const import DOMAIN, IMAGE_TIMEOUT _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL: Final = timedelta(seconds=30) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL: Final = timedelta(seconds=30) DEFAULT_CONTENT_TYPE: Final = "image/jpeg" ENTITY_IMAGE_URL: Final = "/api/image_proxy/{0}?token={1}" diff --git a/homeassistant/components/lawn_mower/__init__.py b/homeassistant/components/lawn_mower/__init__.py index 8cb9850bde7..27765d207d8 100644 --- a/homeassistant/components/lawn_mower/__init__.py +++ b/homeassistant/components/lawn_mower/__init__.py @@ -9,10 +9,7 @@ from typing import final from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -26,10 +23,12 @@ from .const import ( LawnMowerEntityFeature, ) -SCAN_INTERVAL = timedelta(seconds=60) - _LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=60) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the lawn_mower component.""" diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index b61625edaf2..67000b6aaaf 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -24,11 +24,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry as er -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, - make_entity_service_schema, -) from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, VolDictType @@ -36,10 +31,12 @@ from homeassistant.loader import bind_hass import homeassistant.util.color as color_util DOMAIN = "light" -SCAN_INTERVAL = timedelta(seconds=30) -DATA_PROFILES = "light_profiles" - ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) + +DATA_PROFILES = "light_profiles" class LightEntityFeature(IntFlag): diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 21533353ac7..fd3f60d3502 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -30,11 +30,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, - make_entity_service_schema, -) from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -49,16 +44,19 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) + ATTR_CHANGED_BY = "changed_by" CONF_DEFAULT_CODE = "default_code" -SCAN_INTERVAL = timedelta(seconds=30) - -ENTITY_ID_FORMAT = DOMAIN + ".{}" - MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -LOCK_SERVICE_SCHEMA = make_entity_service_schema({vol.Optional(ATTR_CODE): cv.string}) +LOCK_SERVICE_SCHEMA = cv.make_entity_service_schema( + {vol.Optional(ATTR_CODE): cv.string} +) class LockEntityFeature(IntFlag): diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 3679b5f89c5..d499ee8d6d3 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -52,12 +52,8 @@ from homeassistant.const import ( # noqa: F401 STATE_STANDBY, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.network import get_url @@ -137,6 +133,9 @@ from .errors import BrowseError _LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = dt.timedelta(seconds=10) CACHE_IMAGES: Final = "images" CACHE_MAXSIZE: Final = "maxsize" @@ -144,8 +143,6 @@ CACHE_LOCK: Final = "lock" CACHE_URL: Final = "url" CACHE_CONTENT: Final = "content" -SCAN_INTERVAL = dt.timedelta(seconds=10) - class MediaPlayerEnqueue(StrEnum): """Enqueue types for playing media.""" diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 77dde242b7e..2c750bd834e 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -22,10 +22,7 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity @@ -53,10 +50,12 @@ from .websocket_api import async_setup as async_setup_ws_api _LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -SCAN_INTERVAL = timedelta(seconds=30) __all__ = [ "ATTR_MAX", diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 88813e4a70c..cb67a7568e2 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -21,12 +21,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, - make_entity_service_schema, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -40,6 +35,12 @@ from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) +DOMAIN = "remote" +ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) + ATTR_ACTIVITY = "activity" ATTR_ACTIVITY_LIST = "activity_list" ATTR_CURRENT_ACTIVITY = "current_activity" @@ -51,11 +52,6 @@ ATTR_HOLD_SECS = "hold_secs" ATTR_ALTERNATIVE = "alternative" ATTR_TIMEOUT = "timeout" -DOMAIN = "remote" -SCAN_INTERVAL = timedelta(seconds=30) - -ENTITY_ID_FORMAT = DOMAIN + ".{}" - MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) SERVICE_SEND_COMMAND = "send_command" @@ -89,7 +85,7 @@ _DEPRECATED_SUPPORT_ACTIVITY = DeprecatedConstantEnum( ) -REMOTE_SERVICE_ACTIVITY_SCHEMA = make_entity_service_schema( +REMOTE_SERVICE_ACTIVITY_SCHEMA = cv.make_entity_service_schema( {vol.Optional(ATTR_ACTIVITY): cv.string} ) diff --git a/homeassistant/components/select/__init__.py b/homeassistant/components/select/__init__.py index 6e134c8958c..27d41dafcd1 100644 --- a/homeassistant/components/select/__init__.py +++ b/homeassistant/components/select/__init__.py @@ -13,10 +13,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -33,14 +29,15 @@ from .const import ( SERVICE_SELECT_PREVIOUS, ) -SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -_LOGGER = logging.getLogger(__name__) - __all__ = [ "ATTR_CYCLE", "ATTR_OPTION", diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 8d81df6431f..63b853f971e 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -50,11 +50,7 @@ from homeassistant.const import ( # noqa: F401 ) from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.deprecation import ( all_with_deprecated_constants, check_if_deprecated_constant, @@ -93,7 +89,8 @@ from .websocket_api import async_setup as async_setup_ws_api _LOGGER: Final = logging.getLogger(__name__) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" - +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE SCAN_INTERVAL: Final = timedelta(seconds=30) __all__ = [ diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index e5837fdd1bf..216e111b7db 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -12,11 +12,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import HomeAssistant, ServiceCall -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( all_with_deprecated_constants, check_if_deprecated_constant, @@ -42,6 +38,8 @@ from .const import ( # noqa: F401 _LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE SCAN_INTERVAL = timedelta(seconds=60) TURN_ON_SCHEMA: VolDictType = { diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 995bcda294f..55e0a7a767e 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -17,10 +17,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -34,14 +31,15 @@ from homeassistant.loader import bind_hass from .const import DOMAIN -SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -_LOGGER = logging.getLogger(__name__) - class SwitchDeviceClass(StrEnum): """Device class for switches.""" diff --git a/homeassistant/components/text/__init__.py b/homeassistant/components/text/__init__.py index f45a9cf3563..33589be8f41 100644 --- a/homeassistant/components/text/__init__.py +++ b/homeassistant/components/text/__init__.py @@ -16,10 +16,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import MAX_LENGTH_STATE_STATE from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity @@ -35,13 +31,15 @@ from .const import ( SERVICE_SET_VALUE, ) -SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -_LOGGER = logging.getLogger(__name__) __all__ = ["DOMAIN", "TextEntity", "TextEntityDescription", "TextMode"] diff --git a/homeassistant/components/time/__init__.py b/homeassistant/components/time/__init__.py index 4e101ddd67d..23c9796ec2e 100644 --- a/homeassistant/components/time/__init__.py +++ b/homeassistant/components/time/__init__.py @@ -13,21 +13,19 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TIME from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, SERVICE_SET_VALUE -SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) -_LOGGER = logging.getLogger(__name__) __all__ = ["DOMAIN", "TimeEntity", "TimeEntityDescription"] diff --git a/homeassistant/components/todo/__init__.py b/homeassistant/components/todo/__init__.py index e574c6372a7..a515f0805e7 100644 --- a/homeassistant/components/todo/__init__.py +++ b/homeassistant/components/todo/__init__.py @@ -21,11 +21,7 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import HomeAssistantError, ServiceValidationError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -44,9 +40,10 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = datetime.timedelta(seconds=60) - ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = datetime.timedelta(seconds=60) @dataclasses.dataclass diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 352237bf201..e7813b354c1 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -17,10 +17,6 @@ from homeassistant.const import ATTR_ENTITY_PICTURE, STATE_OFF, STATE_ON, Entity from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.helpers.entity import ABCCachedProperties, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -43,11 +39,12 @@ from .const import ( UpdateEntityFeature, ) -SCAN_INTERVAL = timedelta(minutes=15) +_LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" - -_LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(minutes=15) class UpdateDeviceClass(StrEnum): diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index f68f9a4f082..90018e2d8cc 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -23,11 +23,6 @@ from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, - make_entity_service_schema, -) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.icon import icon_for_battery_level @@ -39,6 +34,8 @@ from .const import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, STATE_RETU _LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE SCAN_INTERVAL = timedelta(seconds=20) ATTR_BATTERY_ICON = "battery_icon" diff --git a/homeassistant/components/valve/__init__.py b/homeassistant/components/valve/__init__.py index 0363ef55832..e97a68c2e82 100644 --- a/homeassistant/components/valve/__init__.py +++ b/homeassistant/components/valve/__init__.py @@ -23,10 +23,7 @@ from homeassistant.const import ( STATE_OPENING, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -34,9 +31,10 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) DOMAIN = "valve" -SCAN_INTERVAL = timedelta(seconds=15) - ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=15) class ValveDeviceClass(StrEnum): diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 1623b391e53..731a513fb66 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -25,11 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ServiceValidationError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, @@ -44,12 +40,14 @@ from homeassistant.util.unit_conversion import TemperatureConverter from .const import DOMAIN +ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=60) + DEFAULT_MIN_TEMP = 110 DEFAULT_MAX_TEMP = 140 -ENTITY_ID_FORMAT = DOMAIN + ".{}" -SCAN_INTERVAL = timedelta(seconds=60) - SERVICE_SET_AWAY_MODE = "set_away_mode" SERVICE_SET_TEMPERATURE = "set_temperature" SERVICE_SET_OPERATION_MODE = "set_operation_mode" diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index b3ce52510d2..468c023b470 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -31,10 +31,7 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import ABCCachedProperties, Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -74,6 +71,11 @@ from .websocket_api import async_setup as async_setup_ws_api _LOGGER = logging.getLogger(__name__) +ENTITY_ID_FORMAT = DOMAIN + ".{}" +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SCAN_INTERVAL = timedelta(seconds=30) + ATTR_CONDITION_CLASS = "condition_class" ATTR_CONDITION_CLEAR_NIGHT = "clear-night" ATTR_CONDITION_CLOUDY = "cloudy" @@ -115,10 +117,6 @@ ATTR_FORECAST_DEW_POINT: Final = "dew_point" ATTR_FORECAST_CLOUD_COVERAGE: Final = "cloud_coverage" ATTR_FORECAST_UV_INDEX: Final = "uv_index" -ENTITY_ID_FORMAT = DOMAIN + ".{}" - -SCAN_INTERVAL = timedelta(seconds=30) - ROUNDING_PRECISION = 2 SERVICE_GET_FORECASTS: Final = "get_forecasts" From 862cd76f893c5db076f340b38dfe59cd78bc1731 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 15:49:34 +0200 Subject: [PATCH 0252/2411] Add explanatory comment in tests/patch_time.py (#120572) --- tests/patch_time.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/patch_time.py b/tests/patch_time.py index c8052b3b8ac..a93d3c8ec4f 100644 --- a/tests/patch_time.py +++ b/tests/patch_time.py @@ -25,4 +25,5 @@ dt_util.utcnow = _utcnow # type: ignore[assignment] event_helper.time_tracker_utcnow = _utcnow # type: ignore[assignment] util.utcnow = _utcnow # type: ignore[assignment] +# Replace bound methods which are not found by freezegun runner.monotonic = _monotonic # type: ignore[assignment] From 30a3e9af2bfefa1a4c6c9720743e358f52dff5b6 Mon Sep 17 00:00:00 2001 From: treetip Date: Wed, 26 Jun 2024 16:54:13 +0300 Subject: [PATCH 0253/2411] Add profile duration sensor for Vallox integration (#120240) --- homeassistant/components/vallox/sensor.py | 21 +++++++++ homeassistant/components/vallox/strings.json | 3 ++ tests/components/vallox/conftest.py | 6 +-- tests/components/vallox/test_sensor.py | 45 ++++++++++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 281bc002f68..0bb509a9c5a 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( REVOLUTIONS_PER_MINUTE, EntityCategory, UnitOfTemperature, + UnitOfTime, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -127,6 +128,18 @@ class ValloxCellStateSensor(ValloxSensorEntity): return VALLOX_CELL_STATE_TO_STR.get(super_native_value) +class ValloxProfileDurationSensor(ValloxSensorEntity): + """Child class for profile duration reporting.""" + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + + return self.coordinator.data.get_remaining_profile_duration( + self.coordinator.data.profile + ) + + @dataclass(frozen=True) class ValloxSensorEntityDescription(SensorEntityDescription): """Describes Vallox sensor entity.""" @@ -253,6 +266,14 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, entity_registry_enabled_default=False, ), + ValloxSensorEntityDescription( + key="profile_duration", + translation_key="profile_duration", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + entity_type=ValloxProfileDurationSensor, + ), ) diff --git a/homeassistant/components/vallox/strings.json b/homeassistant/components/vallox/strings.json index 072b59b78e0..4df57b81bb5 100644 --- a/homeassistant/components/vallox/strings.json +++ b/homeassistant/components/vallox/strings.json @@ -87,6 +87,9 @@ }, "efficiency": { "name": "Efficiency" + }, + "profile_duration": { + "name": "Profile duration" } }, "switch": { diff --git a/tests/components/vallox/conftest.py b/tests/components/vallox/conftest.py index 9f65734b926..a6ea95944b3 100644 --- a/tests/components/vallox/conftest.py +++ b/tests/components/vallox/conftest.py @@ -112,9 +112,9 @@ def default_metrics(): "A_CYC_UUID5": 10, "A_CYC_UUID6": 11, "A_CYC_UUID7": 12, - "A_CYC_BOOST_TIMER": 30, - "A_CYC_FIREPLACE_TIMER": 30, - "A_CYC_EXTRA_TIMER": 30, + "A_CYC_BOOST_TIMER": 0, + "A_CYC_FIREPLACE_TIMER": 0, + "A_CYC_EXTRA_TIMER": 0, "A_CYC_MODE": 0, "A_CYC_STATE": 0, "A_CYC_FILTER_CHANGED_YEAR": 24, diff --git a/tests/components/vallox/test_sensor.py b/tests/components/vallox/test_sensor.py index d7af7bbb576..dd8d8026d06 100644 --- a/tests/components/vallox/test_sensor.py +++ b/tests/components/vallox/test_sensor.py @@ -135,3 +135,48 @@ async def test_cell_state_sensor( # Assert sensor = hass.states.get("sensor.vallox_cell_state") assert sensor.state == expected_state + + +@pytest.mark.parametrize( + ("metrics", "expected_state"), + [ + ( + {"A_CYC_STATE": 0}, + "unknown", + ), + ( + {"A_CYC_STATE": 1}, + "unknown", + ), + ( + {"A_CYC_EXTRA_TIMER": 10}, + "10", + ), + ( + {"A_CYC_FIREPLACE_TIMER": 9}, + "9", + ), + ( + {"A_CYC_BOOST_TIMER": 8}, + "8", + ), + ], +) +async def test_profile_duration_sensor( + metrics, + expected_state, + mock_entry: MockConfigEntry, + hass: HomeAssistant, + setup_fetch_metric_data_mock, +) -> None: + """Test profile sensor in different states.""" + # Arrange + setup_fetch_metric_data_mock(metrics=metrics) + + # Act + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert + sensor = hass.states.get("sensor.vallox_profile_duration") + assert sensor.state == expected_state From 3d5d4f8ddb2ab0751c21638a6928a598f7510d70 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 26 Jun 2024 16:06:35 +0200 Subject: [PATCH 0254/2411] Add config flow to statistics (#120496) --- .../components/statistics/__init__.py | 21 ++ .../components/statistics/config_flow.py | 150 ++++++++++ .../components/statistics/manifest.json | 2 + homeassistant/components/statistics/sensor.py | 37 +++ .../components/statistics/strings.json | 111 +++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 12 +- tests/components/statistics/conftest.py | 90 ++++++ .../components/statistics/test_config_flow.py | 273 ++++++++++++++++++ tests/components/statistics/test_init.py | 17 ++ tests/components/statistics/test_sensor.py | 43 ++- 11 files changed, 749 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/statistics/config_flow.py create mode 100644 tests/components/statistics/conftest.py create mode 100644 tests/components/statistics/test_config_flow.py create mode 100644 tests/components/statistics/test_init.py diff --git a/homeassistant/components/statistics/__init__.py b/homeassistant/components/statistics/__init__.py index a6419c2fb4d..70739c618f7 100644 --- a/homeassistant/components/statistics/__init__.py +++ b/homeassistant/components/statistics/__init__.py @@ -1,6 +1,27 @@ """The statistics component.""" +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform +from homeassistant.core import HomeAssistant DOMAIN = "statistics" PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Statistics from a config entry.""" + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Statistics config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/statistics/config_flow.py b/homeassistant/components/statistics/config_flow.py new file mode 100644 index 00000000000..773c3d1c364 --- /dev/null +++ b/homeassistant/components/statistics/config_flow.py @@ -0,0 +1,150 @@ +"""Config flow for statistics.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import CONF_ENTITY_ID, CONF_NAME +from homeassistant.core import split_entity_id +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, + SchemaConfigFlowHandler, + SchemaFlowError, + SchemaFlowFormStep, +) +from homeassistant.helpers.selector import ( + BooleanSelector, + DurationSelector, + DurationSelectorConfig, + EntitySelector, + EntitySelectorConfig, + NumberSelector, + NumberSelectorConfig, + NumberSelectorMode, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, + TextSelector, +) + +from . import DOMAIN +from .sensor import ( + CONF_KEEP_LAST_SAMPLE, + CONF_MAX_AGE, + CONF_PERCENTILE, + CONF_PRECISION, + CONF_SAMPLES_MAX_BUFFER_SIZE, + CONF_STATE_CHARACTERISTIC, + DEFAULT_NAME, + DEFAULT_PRECISION, + STATS_BINARY_SUPPORT, + STATS_NUMERIC_SUPPORT, +) + + +async def get_state_characteristics(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return schema with state characteristics.""" + is_binary = ( + split_entity_id(handler.options[CONF_ENTITY_ID])[0] == BINARY_SENSOR_DOMAIN + ) + if is_binary: + options = STATS_BINARY_SUPPORT + else: + options = STATS_NUMERIC_SUPPORT + + return vol.Schema( + { + vol.Required(CONF_STATE_CHARACTERISTIC): SelectSelector( + SelectSelectorConfig( + options=list(options), + translation_key=CONF_STATE_CHARACTERISTIC, + sort=True, + mode=SelectSelectorMode.DROPDOWN, + ) + ), + } + ) + + +async def validate_options( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate options selected.""" + if ( + user_input.get(CONF_SAMPLES_MAX_BUFFER_SIZE) is None + and user_input.get(CONF_MAX_AGE) is None + ): + raise SchemaFlowError("missing_max_age_or_sampling_size") + + if ( + user_input.get(CONF_KEEP_LAST_SAMPLE) is True + and user_input.get(CONF_MAX_AGE) is None + ): + raise SchemaFlowError("missing_keep_last_sample") + + handler.parent_handler._async_abort_entries_match({**handler.options, **user_input}) # noqa: SLF001 + + return user_input + + +DATA_SCHEMA_SETUP = vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): TextSelector(), + vol.Required(CONF_ENTITY_ID): EntitySelector( + EntitySelectorConfig(domain=[BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN]) + ), + } +) +DATA_SCHEMA_OPTIONS = vol.Schema( + { + vol.Optional(CONF_SAMPLES_MAX_BUFFER_SIZE): NumberSelector( + NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX) + ), + vol.Optional(CONF_MAX_AGE): DurationSelector( + DurationSelectorConfig(enable_day=False, allow_negative=False) + ), + vol.Optional(CONF_KEEP_LAST_SAMPLE, default=False): BooleanSelector(), + vol.Optional(CONF_PERCENTILE, default=50): NumberSelector( + NumberSelectorConfig(min=1, max=99, step=1, mode=NumberSelectorMode.BOX) + ), + vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): NumberSelector( + NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX) + ), + } +) + +CONFIG_FLOW = { + "user": SchemaFlowFormStep( + schema=DATA_SCHEMA_SETUP, + next_step="state_characteristic", + ), + "state_characteristic": SchemaFlowFormStep( + schema=get_state_characteristics, next_step="options" + ), + "options": SchemaFlowFormStep( + schema=DATA_SCHEMA_OPTIONS, + validate_user_input=validate_options, + ), +} +OPTIONS_FLOW = { + "init": SchemaFlowFormStep( + DATA_SCHEMA_OPTIONS, + validate_user_input=validate_options, + ), +} + + +class StatisticsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config flow for Statistics.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options[CONF_NAME]) diff --git a/homeassistant/components/statistics/manifest.json b/homeassistant/components/statistics/manifest.json index 04b5277ecf5..24d4b4914cb 100644 --- a/homeassistant/components/statistics/manifest.json +++ b/homeassistant/components/statistics/manifest.json @@ -3,7 +3,9 @@ "name": "Statistics", "after_dependencies": ["recorder"], "codeowners": ["@ThomDietrich"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/statistics", + "integration_type": "helper", "iot_class": "local_polling", "quality_scale": "internal" } diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index eb4df4d98b2..8d28254ad61 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -22,6 +22,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -282,6 +283,42 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Statistics sensor entry.""" + sampling_size = entry.options.get(CONF_SAMPLES_MAX_BUFFER_SIZE) + if sampling_size: + sampling_size = int(sampling_size) + + max_age = None + if max_age_input := entry.options.get(CONF_MAX_AGE): + max_age = timedelta( + hours=max_age_input["hours"], + minutes=max_age_input["minutes"], + seconds=max_age_input["seconds"], + ) + + async_add_entities( + [ + StatisticsSensor( + source_entity_id=entry.options[CONF_ENTITY_ID], + name=entry.options[CONF_NAME], + unique_id=entry.entry_id, + state_characteristic=entry.options[CONF_STATE_CHARACTERISTIC], + samples_max_buffer_size=sampling_size, + samples_max_age=max_age, + samples_keep_last=entry.options[CONF_KEEP_LAST_SAMPLE], + precision=int(entry.options[CONF_PRECISION]), + percentile=int(entry.options[CONF_PERCENTILE]), + ) + ], + True, + ) + + class StatisticsSensor(SensorEntity): """Representation of a Statistics sensor.""" diff --git a/homeassistant/components/statistics/strings.json b/homeassistant/components/statistics/strings.json index 6d7bda36fae..5f32b203bfd 100644 --- a/homeassistant/components/statistics/strings.json +++ b/homeassistant/components/statistics/strings.json @@ -1,4 +1,115 @@ { + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "missing_max_age_or_sampling_size": "The sensor configuration must provide 'max_age' and/or 'sampling_size'", + "missing_keep_last_sample": "The sensor configuration must provide 'max_age' if 'keep_last_sample' is True" + }, + "step": { + "user": { + "description": "Add a statistics sensor", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "entity_id": "Entity" + }, + "data_description": { + "name": "Name for the created entity.", + "entity_id": "Entity to get statistics from." + } + }, + "state_characteristic": { + "description": "Read the documention for further details on available options and how to use them.", + "data": { + "state_characteristic": "State_characteristic" + }, + "data_description": { + "state_characteristic": "The characteristic that should be used as the state of the statistics sensor." + } + }, + "options": { + "description": "Read the documention for further details on how to configure the statistics sensor using these options.", + "data": { + "sampling_size": "Sampling size", + "max_age": "Max age", + "keep_last_sample": "Keep last sample", + "percentile": "Percentile", + "precision": "Precision" + }, + "data_description": { + "sampling_size": "Maximum number of source sensor measurements stored.", + "max_age": "Maximum age of source sensor measurements stored.", + "keep_last_sample": "Defines whether the most recent sampled value should be preserved regardless of the 'max age' setting.", + "percentile": "Only relevant in combination with the 'percentile' characteristic. Must be a value between 1 and 99.", + "precision": "Defines the number of decimal places of the calculated sensor value." + } + } + } + }, + "options": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "missing_max_age_or_sampling_size": "[%key:component::statistics::config::error::missing_max_age_or_sampling_size%]", + "missing_keep_last_sample": "[%key:component::statistics::config::error::missing_keep_last_sample%]" + }, + "step": { + "init": { + "description": "[%key:component::statistics::config::step::options::description%]", + "data": { + "sampling_size": "[%key:component::statistics::config::step::options::data::sampling_size%]", + "max_age": "[%key:component::statistics::config::step::options::data::max_age%]", + "keep_last_sample": "[%key:component::statistics::config::step::options::data::keep_last_sample%]", + "percentile": "[%key:component::statistics::config::step::options::data::percentile%]", + "precision": "[%key:component::statistics::config::step::options::data::precision%]" + }, + "data_description": { + "sampling_size": "[%key:component::statistics::config::step::options::data_description::sampling_size%]", + "max_age": "[%key:component::statistics::config::step::options::data_description::max_age%]", + "keep_last_sample": "[%key:component::statistics::config::step::options::data_description::keep_last_sample%]", + "percentile": "[%key:component::statistics::config::step::options::data_description::percentile%]", + "precision": "[%key:component::statistics::config::step::options::data_description::precision%]" + } + } + } + }, + "selector": { + "state_characteristic": { + "options": { + "average_linear": "Average linear", + "average_step": "Average step", + "average_timeless": "Average timeless", + "change": "Change", + "change_sample": "Change sample", + "change_second": "Change second", + "count": "Count", + "count_on": "Count on", + "count_off": "Count off", + "datetime_newest": "Newest datetime", + "datetime_oldest": "Oldest datetime", + "datetime_value_max": "Max value datetime", + "datetime_value_min": "Min value datetime", + "distance_95_percent_of_values": "Distance 95% of values", + "distance_99_percent_of_values": "Distance 99% of values", + "distance_absolute": "Absolute distance", + "mean": "Mean", + "mean_circular": "Mean circular", + "median": "Median", + "noisiness": "Noisiness", + "percentile": "Percentile", + "standard_deviation": "Standard deviation", + "sum": "Sum", + "sum_differences": "Sum of differences", + "sum_differences_nonnegative": "Sum of differences non-negative", + "total": "Total", + "value_max": "Max value", + "value_min": "Min value", + "variance": "Variance" + } + } + }, "services": { "reload": { "name": "[%key:common::action::reload%]", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e5eeeb29403..23a13bcbfd8 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -12,6 +12,7 @@ FLOWS = { "integration", "min_max", "random", + "statistics", "switch_as_x", "template", "threshold", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index e98df79d096..3371c8de0fa 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5772,12 +5772,6 @@ "config_flow": false, "iot_class": "cloud_polling" }, - "statistics": { - "name": "Statistics", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling" - }, "statsd": { "name": "StatsD", "integration_type": "hub", @@ -7213,6 +7207,12 @@ "integration_type": "helper", "config_flow": false }, + "statistics": { + "name": "Statistics", + "integration_type": "helper", + "config_flow": true, + "iot_class": "local_polling" + }, "switch_as_x": { "integration_type": "helper", "config_flow": true, diff --git a/tests/components/statistics/conftest.py b/tests/components/statistics/conftest.py new file mode 100644 index 00000000000..e62488c4cf6 --- /dev/null +++ b/tests/components/statistics/conftest.py @@ -0,0 +1,90 @@ +"""Fixtures for the Statistics integration.""" + +from __future__ import annotations + +from collections.abc import Generator +from typing import Any +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.statistics import DOMAIN +from homeassistant.components.statistics.sensor import ( + CONF_KEEP_LAST_SAMPLE, + CONF_MAX_AGE, + CONF_PERCENTILE, + CONF_PRECISION, + CONF_SAMPLES_MAX_BUFFER_SIZE, + CONF_STATE_CHARACTERISTIC, + DEFAULT_NAME, + STAT_AVERAGE_LINEAR, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + CONF_ENTITY_ID, + CONF_NAME, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant + +from .test_sensor import VALUES_NUMERIC + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Automatically path uuid generator.""" + with patch( + "homeassistant.components.statistics.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(name="get_config") +async def get_config_to_integration_load() -> dict[str, Any]: + """Return configuration. + + To override the config, tests can be marked with: + @pytest.mark.parametrize("get_config", [{...}]) + """ + return { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR, + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 5, "seconds": 5}, + CONF_KEEP_LAST_SAMPLE: False, + CONF_PERCENTILE: 50.0, + CONF_PRECISION: 2.0, + } + + +@pytest.fixture(name="loaded_entry") +async def load_integration( + hass: HomeAssistant, get_config: dict[str, Any] +) -> MockConfigEntry: + """Set up the Statistics integration in Home Assistant.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + options=get_config, + entry_id="1", + ) + + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + for value in VALUES_NUMERIC: + hass.states.async_set( + "sensor.test_monitored", + str(value), + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, + ) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/statistics/test_config_flow.py b/tests/components/statistics/test_config_flow.py new file mode 100644 index 00000000000..7c9ed5bed47 --- /dev/null +++ b/tests/components/statistics/test_config_flow.py @@ -0,0 +1,273 @@ +"""Test the Scrape config flow.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +from homeassistant import config_entries +from homeassistant.components.statistics import DOMAIN +from homeassistant.components.statistics.sensor import ( + CONF_KEEP_LAST_SAMPLE, + CONF_MAX_AGE, + CONF_PERCENTILE, + CONF_PRECISION, + CONF_SAMPLES_MAX_BUFFER_SIZE, + CONF_STATE_CHARACTERISTIC, + DEFAULT_NAME, + STAT_AVERAGE_LINEAR, + STAT_COUNT, +) +from homeassistant.const import CONF_ENTITY_ID, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_form_sensor(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form for sensor.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR, + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0}, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["version"] == 1 + assert result["options"] == { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR, + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0}, + CONF_KEEP_LAST_SAMPLE: False, + CONF_PERCENTILE: 50.0, + CONF_PRECISION: 2.0, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_binary_sensor( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we get the form for binary sensor.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "binary_sensor.test_monitored", + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_STATE_CHARACTERISTIC: STAT_COUNT, + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0}, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["version"] == 1 + assert result["options"] == { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "binary_sensor.test_monitored", + CONF_STATE_CHARACTERISTIC: STAT_COUNT, + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0}, + CONF_KEEP_LAST_SAMPLE: False, + CONF_PERCENTILE: 50.0, + CONF_PRECISION: 2.0, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_options_flow(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: + """Test options flow.""" + + result = await hass.config_entries.options.async_init(loaded_entry.entry_id) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_SAMPLES_MAX_BUFFER_SIZE: 25.0, + CONF_MAX_AGE: {"hours": 16, "minutes": 0, "seconds": 0}, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR, + CONF_SAMPLES_MAX_BUFFER_SIZE: 25.0, + CONF_MAX_AGE: {"hours": 16, "minutes": 0, "seconds": 0}, + CONF_KEEP_LAST_SAMPLE: False, + CONF_PERCENTILE: 50.0, + CONF_PRECISION: 2.0, + } + + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 2 + + state = hass.states.get("sensor.statistical_characteristic") + assert state is not None + + +async def test_validation_options( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test validation.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR, + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["step_id"] == "options" + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "missing_max_age_or_sampling_size"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_KEEP_LAST_SAMPLE: True, CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0}, + ) + await hass.async_block_till_done() + + assert result["step_id"] == "options" + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "missing_keep_last_sample"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0}, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["version"] == 1 + assert result["options"] == { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR, + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 0, "seconds": 0}, + CONF_KEEP_LAST_SAMPLE: False, + CONF_PERCENTILE: 50.0, + CONF_PRECISION: 2.0, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_entry_already_exist( + hass: HomeAssistant, loaded_entry: MockConfigEntry +) -> None: + """Test abort when entry already exist.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: "sensor.test_monitored", + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_STATE_CHARACTERISTIC: STAT_AVERAGE_LINEAR, + }, + ) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_MAX_AGE: {"hours": 8, "minutes": 5, "seconds": 5}, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/statistics/test_init.py b/tests/components/statistics/test_init.py new file mode 100644 index 00000000000..6cb943c0687 --- /dev/null +++ b/tests/components/statistics/test_init.py @@ -0,0 +1,17 @@ +"""Test Statistics component setup process.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: + """Test unload an entry.""" + + assert loaded_entry.state is ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(loaded_entry.entry_id) + await hass.async_block_till_done() + assert loaded_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 5a716fd8ce8..269c17e34b9 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -19,10 +19,20 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.components.statistics import DOMAIN as STATISTICS_DOMAIN -from homeassistant.components.statistics.sensor import StatisticsSensor +from homeassistant.components.statistics.sensor import ( + CONF_KEEP_LAST_SAMPLE, + CONF_PERCENTILE, + CONF_PRECISION, + CONF_SAMPLES_MAX_BUFFER_SIZE, + CONF_STATE_CHARACTERISTIC, + STAT_MEAN, + StatisticsSensor, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, + CONF_ENTITY_ID, + CONF_NAME, DEGREE, SERVICE_RELOAD, STATE_UNAVAILABLE, @@ -35,7 +45,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.common import async_fire_time_changed, get_fixture_path +from tests.common import MockConfigEntry, async_fire_time_changed, get_fixture_path from tests.components.recorder.common import async_wait_recording_done VALUES_BINARY = ["on", "off", "on", "off", "on", "off", "on", "off", "on"] @@ -171,6 +181,35 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant) -> None: assert new_state.attributes.get("source_value_valid") is False +@pytest.mark.parametrize( + "get_config", + [ + { + CONF_NAME: "test", + CONF_ENTITY_ID: "sensor.test_monitored", + CONF_STATE_CHARACTERISTIC: STAT_MEAN, + CONF_SAMPLES_MAX_BUFFER_SIZE: 20.0, + CONF_KEEP_LAST_SAMPLE: False, + CONF_PERCENTILE: 50.0, + CONF_PRECISION: 2.0, + } + ], +) +async def test_sensor_loaded_from_config_entry( + hass: HomeAssistant, loaded_entry: MockConfigEntry +) -> None: + """Test the sensor loaded from a config entry.""" + + state = hass.states.get("sensor.test") + assert state is not None + assert state.state == str(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2)) + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) + assert state.attributes.get("source_value_valid") is True + assert "age_coverage_ratio" not in state.attributes + + async def test_sensor_defaults_binary(hass: HomeAssistant) -> None: """Test the general behavior of the sensor, with binary source sensor.""" assert await async_setup_component( From 55101fcc452de271d1332103b4e580277b1772de Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 26 Jun 2024 16:06:55 +0200 Subject: [PATCH 0255/2411] Add platinum scale to pyLoad integration (#120542) Add platinum scale --- homeassistant/components/pyload/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index 95e73118c42..fe1888478f8 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -7,5 +7,6 @@ "integration_type": "service", "iot_class": "local_polling", "loggers": ["pyloadapi"], + "quality_scale": "platinum", "requirements": ["PyLoadAPI==1.2.0"] } From 32c07180f6e9b6bd8b93d1de3708e814c8be1508 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:08:32 +0200 Subject: [PATCH 0256/2411] Delete removed device(s) at runtime in Plugwise (#120296) --- .../components/plugwise/coordinator.py | 46 +++++++++++++++++-- tests/components/plugwise/test_init.py | 29 +++++++++++- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py index bc12ef4443b..8958ecae930 100644 --- a/homeassistant/components/plugwise/coordinator.py +++ b/homeassistant/components/plugwise/coordinator.py @@ -16,11 +16,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, LOGGER +from .const import DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, GATEWAY_ID, LOGGER class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): @@ -83,7 +84,46 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): except UnsupportedDeviceError as err: raise ConfigEntryError("Device with unsupported firmware") from err else: - self.new_devices = set(data.devices) - self._current_devices - self._current_devices = set(data.devices) + self._async_add_remove_devices(data, self.config_entry) return data + + def _async_add_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None: + """Add new Plugwise devices, remove non-existing devices.""" + # Check for new or removed devices + self.new_devices = set(data.devices) - self._current_devices + removed_devices = self._current_devices - set(data.devices) + self._current_devices = set(data.devices) + + if removed_devices: + self._async_remove_devices(data, entry) + + def _async_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None: + """Clean registries when removed devices found.""" + device_reg = dr.async_get(self.hass) + device_list = dr.async_entries_for_config_entry( + device_reg, self.config_entry.entry_id + ) + # via_device cannot be None, this will result in the deletion + # of other Plugwise Gateways when present! + via_device: str = "" + for device_entry in device_list: + if device_entry.identifiers: + item = list(list(device_entry.identifiers)[0]) + if item[0] == DOMAIN: + # First find the Plugwise via_device, this is always the first device + if item[1] == data.gateway[GATEWAY_ID]: + via_device = device_entry.id + elif ( # then remove the connected orphaned device(s) + device_entry.via_device_id == via_device + and item[1] not in data.devices + ): + device_reg.async_update_device( + device_entry.id, remove_config_entry_id=entry.entry_id + ) + LOGGER.debug( + "Removed %s device %s %s from device_registry", + DOMAIN, + device_entry.model, + item[1], + ) diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index d3f23a18285..26aedf864dc 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -39,7 +39,7 @@ TOM = { "hardware": "1", "location": "f871b8c4d63549319221e294e4f88074", "model": "Tom/Floor", - "name": "Tom Badkamer", + "name": "Tom Zolder", "sensors": { "battery": 99, "temperature": 18.6, @@ -258,3 +258,30 @@ async def test_update_device( for device_entry in list(device_registry.devices.values()): item_list.extend(x[1] for x in device_entry.identifiers) assert "01234567890abcdefghijklmnopqrstu" in item_list + + # Remove the existing Tom/Floor + data.devices.pop("1772a4ea304041adb83f357b751341ff") + with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): + async_fire_time_changed(hass, utcnow + timedelta(minutes=1)) + await hass.async_block_till_done() + + assert ( + len( + er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + ) + == 29 + ) + assert ( + len( + dr.async_entries_for_config_entry( + device_registry, mock_config_entry.entry_id + ) + ) + == 6 + ) + item_list: list[str] = [] + for device_entry in list(device_registry.devices.values()): + item_list.extend(x[1] for x in device_entry.identifiers) + assert "1772a4ea304041adb83f357b751341ff" not in item_list From 713abf4c6bbed9b1ae33a69277244d053f8e6cdb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:09:20 +0200 Subject: [PATCH 0257/2411] Refactor PLATFORM_SCHEMA imports in tests (#120566) --- tests/test_setup.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/tests/test_setup.py b/tests/test_setup.py index 4ff0f465e21..1e19f1a7b76 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -12,11 +12,7 @@ from homeassistant import config_entries, loader, setup from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import discovery, translation -from homeassistant.helpers.config_validation import ( - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) +from homeassistant.helpers import config_validation as cv, discovery, translation from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -88,8 +84,8 @@ async def test_validate_platform_config( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test validating platform configuration.""" - platform_schema = PLATFORM_SCHEMA.extend({"hello": str}) - platform_schema_base = PLATFORM_SCHEMA_BASE.extend({}) + platform_schema = cv.PLATFORM_SCHEMA.extend({"hello": str}) + platform_schema_base = cv.PLATFORM_SCHEMA_BASE.extend({}) mock_integration( hass, MockModule("platform_conf", platform_schema_base=platform_schema_base), @@ -149,8 +145,8 @@ async def test_validate_platform_config_2( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test component PLATFORM_SCHEMA_BASE prio over PLATFORM_SCHEMA.""" - platform_schema = PLATFORM_SCHEMA.extend({"hello": str}) - platform_schema_base = PLATFORM_SCHEMA_BASE.extend({"hello": "world"}) + platform_schema = cv.PLATFORM_SCHEMA.extend({"hello": str}) + platform_schema_base = cv.PLATFORM_SCHEMA_BASE.extend({"hello": "world"}) mock_integration( hass, MockModule( @@ -183,8 +179,8 @@ async def test_validate_platform_config_3( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test fallback to component PLATFORM_SCHEMA.""" - component_schema = PLATFORM_SCHEMA_BASE.extend({"hello": str}) - platform_schema = PLATFORM_SCHEMA.extend({"cheers": str, "hello": "world"}) + component_schema = cv.PLATFORM_SCHEMA_BASE.extend({"hello": str}) + platform_schema = cv.PLATFORM_SCHEMA.extend({"cheers": str, "hello": "world"}) mock_integration( hass, MockModule("platform_conf", platform_schema=component_schema) ) @@ -210,8 +206,8 @@ async def test_validate_platform_config_3( async def test_validate_platform_config_4(hass: HomeAssistant) -> None: """Test entity_namespace in PLATFORM_SCHEMA.""" - component_schema = PLATFORM_SCHEMA_BASE - platform_schema = PLATFORM_SCHEMA + component_schema = cv.PLATFORM_SCHEMA_BASE + platform_schema = cv.PLATFORM_SCHEMA mock_integration( hass, MockModule("platform_conf", platform_schema_base=component_schema), @@ -386,7 +382,9 @@ async def test_component_setup_with_validation_and_dependency( async def test_platform_specific_config_validation(hass: HomeAssistant) -> None: """Test platform that specifies config.""" - platform_schema = PLATFORM_SCHEMA.extend({"valid": True}, extra=vol.PREVENT_EXTRA) + platform_schema = cv.PLATFORM_SCHEMA.extend( + {"valid": True}, extra=vol.PREVENT_EXTRA + ) mock_setup = Mock(spec_set=True) @@ -533,7 +531,7 @@ async def test_component_warn_slow_setup(hass: HomeAssistant) -> None: async def test_platform_no_warn_slow(hass: HomeAssistant) -> None: """Do not warn for long entity setup time.""" mock_integration( - hass, MockModule("test_component1", platform_schema=PLATFORM_SCHEMA) + hass, MockModule("test_component1", platform_schema=cv.PLATFORM_SCHEMA) ) with patch.object(hass.loop, "call_later") as mock_call: result = await setup.async_setup_component(hass, "test_component1", {}) From f5c640ee5b490a3eb08c4931dd8ba9d1887ebc11 Mon Sep 17 00:00:00 2001 From: Gerben Jongerius Date: Wed, 26 Jun 2024 16:11:21 +0200 Subject: [PATCH 0258/2411] Add additional tests to youless integration (#118869) --- .coveragerc | 2 - tests/components/youless/__init__.py | 38 + tests/components/youless/fixtures/device.json | 5 + .../components/youless/fixtures/enologic.json | 18 + .../youless/snapshots/test_sensor.ambr | 972 ++++++++++++++++++ tests/components/youless/test_init.py | 18 + tests/components/youless/test_sensor.py | 23 + 7 files changed, 1074 insertions(+), 2 deletions(-) create mode 100644 tests/components/youless/fixtures/device.json create mode 100644 tests/components/youless/fixtures/enologic.json create mode 100644 tests/components/youless/snapshots/test_sensor.ambr create mode 100644 tests/components/youless/test_init.py create mode 100644 tests/components/youless/test_sensor.py diff --git a/.coveragerc b/.coveragerc index 1952297eb5f..0784977ff55 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1690,8 +1690,6 @@ omit = homeassistant/components/yolink/siren.py homeassistant/components/yolink/switch.py homeassistant/components/yolink/valve.py - homeassistant/components/youless/__init__.py - homeassistant/components/youless/sensor.py homeassistant/components/zabbix/* homeassistant/components/zamg/coordinator.py homeassistant/components/zengge/light.py diff --git a/tests/components/youless/__init__.py b/tests/components/youless/__init__.py index 8711c6721bc..8770a7e2dc8 100644 --- a/tests/components/youless/__init__.py +++ b/tests/components/youless/__init__.py @@ -1 +1,39 @@ """Tests for the youless component.""" + +import requests_mock + +from homeassistant.components import youless +from homeassistant.const import CONF_DEVICE, CONF_HOST +from homeassistant.core import HomeAssistant + +from tests.common import ( + MockConfigEntry, + load_json_array_fixture, + load_json_object_fixture, +) + + +async def init_component(hass: HomeAssistant) -> MockConfigEntry: + """Check if the setup of the integration succeeds.""" + with requests_mock.Mocker() as mock: + mock.get( + "http://1.1.1.1/d", + json=load_json_object_fixture("device.json", youless.DOMAIN), + ) + mock.get( + "http://1.1.1.1/e", + json=load_json_array_fixture("enologic.json", youless.DOMAIN), + headers={"Content-Type": "application/json"}, + ) + + entry = MockConfigEntry( + domain=youless.DOMAIN, + title="localhost", + data={CONF_HOST: "1.1.1.1", CONF_DEVICE: "localhost"}, + ) + 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/youless/fixtures/device.json b/tests/components/youless/fixtures/device.json new file mode 100644 index 00000000000..7d089851923 --- /dev/null +++ b/tests/components/youless/fixtures/device.json @@ -0,0 +1,5 @@ +{ + "model": "LS120", + "fw": "1.4.2-EL", + "mac": "de2:2d2:3d23" +} diff --git a/tests/components/youless/fixtures/enologic.json b/tests/components/youless/fixtures/enologic.json new file mode 100644 index 00000000000..0189f43af5e --- /dev/null +++ b/tests/components/youless/fixtures/enologic.json @@ -0,0 +1,18 @@ +[ + { + "tm": 1611929119, + "net": 9194.164, + "pwr": 2382, + "ts0": 1608654000, + "cs0": 0.0, + "ps0": 0, + "p1": 4703.562, + "p2": 4490.631, + "n1": 0.029, + "n2": 0.0, + "gas": 1624.264, + "gts": 0, + "wtr": 1234.564, + "wts": 0 + } +] diff --git a/tests/components/youless/snapshots/test_sensor.ambr b/tests/components/youless/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..22e480c390e --- /dev/null +++ b/tests/components/youless/snapshots/test_sensor.ambr @@ -0,0 +1,972 @@ +# serializer version: 1 +# name: test_sensors[sensor.energy_delivery_high-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_delivery_high', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy delivery high', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_delivery_high', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_delivery_high-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy delivery high', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_delivery_high', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.energy_delivery_low-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_delivery_low', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy delivery low', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_delivery_low', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_delivery_low-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy delivery low', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_delivery_low', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.energy_high-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_high', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy high', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_power_high', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_high-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy high', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_high', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4490.631', + }) +# --- +# name: test_sensors[sensor.energy_low-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_low', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy low', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_power_low', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_low-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy low', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_low', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4703.562', + }) +# --- +# name: test_sensors[sensor.energy_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_total', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy total', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_power_total', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9194.164', + }) +# --- +# name: test_sensors[sensor.extra_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.extra_total', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Extra total', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_extra_total', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.extra_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Extra total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.extra_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.extra_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.extra_usage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Extra usage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_extra_usage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.extra_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Extra usage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.extra_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.gas_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gas_usage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:fire', + 'original_name': 'Gas usage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_gas', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.gas_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'gas', + 'friendly_name': 'Gas usage', + 'icon': 'mdi:fire', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gas_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1234.564', + }) +# --- +# name: test_sensors[sensor.phase_1_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_1_current', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 1 current', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_1_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_1_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Phase 1 current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_1_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_1_power', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 1 power', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_1_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_1_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Phase 1 power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_1_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_1_voltage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 1 voltage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_1_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_1_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Phase 1 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_2_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_2_current', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 2 current', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_2_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_2_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Phase 2 current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_2_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_2_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_2_power', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 2 power', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_2_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_2_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Phase 2 power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_2_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_2_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_2_voltage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 2 voltage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_2_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_2_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Phase 2 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_2_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_3_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_3_current', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 3 current', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_3_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_3_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Phase 3 current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_3_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_3_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_3_power', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 3 power', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_3_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_3_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Phase 3 power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_3_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.phase_3_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.phase_3_voltage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase 3 voltage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_phase_3_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.phase_3_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Phase 3 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.phase_3_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.power_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.power_usage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power Usage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_usage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.power_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Power Usage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.power_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2382', + }) +# --- +# name: test_sensors[sensor.water_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.water_usage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:water', + 'original_name': 'Water usage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'youless_localhost_water', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.water_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Water usage', + 'icon': 'mdi:water', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.water_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- diff --git a/tests/components/youless/test_init.py b/tests/components/youless/test_init.py new file mode 100644 index 00000000000..29db8c66af0 --- /dev/null +++ b/tests/components/youless/test_init.py @@ -0,0 +1,18 @@ +"""Test the setup of the Youless integration.""" + +from homeassistant import setup +from homeassistant.components import youless +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import init_component + + +async def test_async_setup_entry(hass: HomeAssistant) -> None: + """Check if the setup of the integration succeeds.""" + + entry = await init_component(hass) + + assert await setup.async_setup_component(hass, youless.DOMAIN, {}) + assert entry.state is ConfigEntryState.LOADED + assert len(hass.states.async_entity_ids()) == 19 diff --git a/tests/components/youless/test_sensor.py b/tests/components/youless/test_sensor.py new file mode 100644 index 00000000000..67dff314df7 --- /dev/null +++ b/tests/components/youless/test_sensor.py @@ -0,0 +1,23 @@ +"""Test the sensor classes for youless.""" + +from unittest.mock import patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_component + +from tests.common import snapshot_platform + + +async def test_sensors( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: + """Test the sensor classes for youless.""" + with patch("homeassistant.components.youless.PLATFORMS", [Platform.SENSOR]): + entry = await init_component(hass) + + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) From 3492171ff8a6291176ddd5bf78c7e91e368e8327 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jun 2024 16:17:57 +0200 Subject: [PATCH 0259/2411] Bump version to 2024.7.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3a970aefd38..54d7f26a5f0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 4edb1535411..0b490d621a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0.dev0" +version = "2024.7.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 33b4f40b2a60b41350a829bb3557d16d19b2270d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jun 2024 16:55:08 +0200 Subject: [PATCH 0260/2411] Bump version to 2024.8.0dev0 (#120577) --- .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 8a030d7d45c..49f58403aab 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -36,7 +36,7 @@ env: CACHE_VERSION: 9 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 8 - HA_SHORT_VERSION: "2024.7" + HA_SHORT_VERSION: "2024.8" DEFAULT_PYTHON: "3.12" ALL_PYTHON_VERSIONS: "['3.12']" # 10.3 is the oldest supported version diff --git a/homeassistant/const.py b/homeassistant/const.py index 3a970aefd38..d0f1d4555d4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 -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 4edb1535411..45c60684ebf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0.dev0" +version = "2024.8.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 02b142fbdeb59234aa41452b1fa6c6fe49ca5f60 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 26 Jun 2024 11:13:01 -0500 Subject: [PATCH 0261/2411] Bump intents to 2024.6.26 (#120584) Bump intents --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/conversation/snapshots/test_init.ambr | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index ee0b29f22fc..2302d03bf4c 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.21"] + "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.26"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 18461d6398b..e42ef84d34c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240626.0 -home-assistant-intents==2024.6.21 +home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index a3a62b58b4b..36c779d2c7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ holidays==0.51 home-assistant-frontend==20240626.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.21 +home-assistant-intents==2024.6.26 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f05bcc3d33..b0fcb6c5eea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -898,7 +898,7 @@ holidays==0.51 home-assistant-frontend==20240626.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.21 +home-assistant-intents==2024.6.26 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 403c72aaa10..6264e61863f 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -563,7 +563,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -703,7 +703,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called late added', + 'speech': 'Sorry, I am not aware of any device called late added light', }), }), }), @@ -783,7 +783,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -803,7 +803,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called my cool', + 'speech': 'Sorry, I am not aware of any device called my cool light', }), }), }), @@ -943,7 +943,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -993,7 +993,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called renamed', + 'speech': 'Sorry, I am not aware of any device called renamed light', }), }), }), From d09a274548ef9abb73617fc92781a18becc75766 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 26 Jun 2024 18:15:53 +0200 Subject: [PATCH 0262/2411] Bump ZHA dependencies (#120581) --- homeassistant/components/zha/manifest.json | 8 ++++---- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index f517742f16f..7087ff0b2f0 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -23,12 +23,12 @@ "requirements": [ "bellows==0.39.1", "pyserial==3.5", - "zha-quirks==0.0.116", - "zigpy-deconz==0.23.1", + "zha-quirks==0.0.117", + "zigpy-deconz==0.23.2", "zigpy==0.64.1", "zigpy-xbee==0.20.1", - "zigpy-zigate==0.12.0", - "zigpy-znp==0.12.1", + "zigpy-zigate==0.12.1", + "zigpy-znp==0.12.2", "universal-silabs-flasher==0.0.20", "pyserial-asyncio-fast==0.11" ], diff --git a/requirements_all.txt b/requirements_all.txt index 36c779d2c7e..eca0100e5df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2966,7 +2966,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.116 +zha-quirks==0.0.117 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 @@ -2975,16 +2975,16 @@ zhong-hong-hvac==1.0.12 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.23.1 +zigpy-deconz==0.23.2 # homeassistant.components.zha zigpy-xbee==0.20.1 # homeassistant.components.zha -zigpy-zigate==0.12.0 +zigpy-zigate==0.12.1 # homeassistant.components.zha -zigpy-znp==0.12.1 +zigpy-znp==0.12.2 # homeassistant.components.zha zigpy==0.64.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0fcb6c5eea..bf8fd1dc081 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2319,19 +2319,19 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.116 +zha-quirks==0.0.117 # homeassistant.components.zha -zigpy-deconz==0.23.1 +zigpy-deconz==0.23.2 # homeassistant.components.zha zigpy-xbee==0.20.1 # homeassistant.components.zha -zigpy-zigate==0.12.0 +zigpy-zigate==0.12.1 # homeassistant.components.zha -zigpy-znp==0.12.1 +zigpy-znp==0.12.2 # homeassistant.components.zha zigpy==0.64.1 From ed1eb8ac9cfc6fab19058cf101f477c1ad4052d9 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 26 Jun 2024 19:19:28 +0300 Subject: [PATCH 0263/2411] Change Shelly connect task log message level to error (#120582) --- homeassistant/components/shelly/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 82d358b33d8..fd5cfaa1542 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -168,7 +168,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( await self.device.initialize() update_device_fw_info(self.hass, self.device, self.entry) except DeviceConnectionError as err: - LOGGER.debug( + LOGGER.error( "Error connecting to Shelly device %s, error: %r", self.name, err ) return False From 31e9de3b95803762083a238a8469a328d768e9f9 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 26 Jun 2024 19:42:15 +0200 Subject: [PATCH 0264/2411] Adapt Roborock to runtime_data (#120578) * Adopt Roborock to runtime_data * Fix name --- homeassistant/components/roborock/__init__.py | 27 +++++++++++-------- .../components/roborock/binary_sensor.py | 9 +++---- homeassistant/components/roborock/button.py | 9 +++---- .../components/roborock/diagnostics.py | 8 +++--- homeassistant/components/roborock/image.py | 8 +++--- homeassistant/components/roborock/number.py | 9 +++---- homeassistant/components/roborock/select.py | 9 +++---- homeassistant/components/roborock/sensor.py | 8 +++--- homeassistant/components/roborock/switch.py | 9 +++---- homeassistant/components/roborock/time.py | 9 +++---- homeassistant/components/roborock/vacuum.py | 8 +++--- tests/components/roborock/test_init.py | 1 - 12 files changed, 46 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index 310c5fee92b..50ffbaaa6e1 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -28,6 +28,8 @@ SCAN_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) +type RoborockConfigEntry = ConfigEntry[RoborockCoordinators] + @dataclass class RoborockCoordinators: @@ -43,7 +45,7 @@ class RoborockCoordinators: return self.v1 + self.a01 -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> bool: """Set up roborock from a config entry.""" _LOGGER.debug("Integration async setup entry: %s", entry.as_dict()) @@ -99,7 +101,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: translation_key="no_coordinators", ) valid_coordinators = RoborockCoordinators(v1_coords, a01_coords) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = valid_coordinators + + async def on_unload() -> None: + release_tasks = set() + for coordinator in valid_coordinators.values(): + release_tasks.add(coordinator.release()) + await asyncio.gather(*release_tasks) + + entry.async_on_unload(on_unload) + entry.runtime_data = valid_coordinators + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -231,18 +242,12 @@ async def setup_device_a01( return coord -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> bool: """Handle removal of an entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - release_tasks = set() - for coordinator in hass.data[DOMAIN][entry.entry_id].values(): - release_tasks.add(coordinator.release()) - hass.data[DOMAIN].pop(entry.entry_id) - await asyncio.gather(*release_tasks) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def update_listener(hass: HomeAssistant, entry: RoborockConfigEntry) -> None: """Handle options update.""" # Reload entry to update data await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/roborock/binary_sensor.py b/homeassistant/components/roborock/binary_sensor.py index 2fd6dd8d7d5..779d3ee234d 100644 --- a/homeassistant/components/roborock/binary_sensor.py +++ b/homeassistant/components/roborock/binary_sensor.py @@ -12,14 +12,12 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify -from . import RoborockCoordinators -from .const import DOMAIN +from . import RoborockConfigEntry from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockCoordinatedEntityV1 @@ -72,17 +70,16 @@ BINARY_SENSOR_DESCRIPTIONS = [ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: RoborockConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Roborock vacuum binary sensors.""" - coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( RoborockBinarySensorEntity( coordinator, description, ) - for coordinator in coordinators.v1 + for coordinator in config_entry.runtime_data.v1 for description in BINARY_SENSOR_DESCRIPTIONS if description.value_fn(coordinator.roborock_device_info.props) is not None ) diff --git a/homeassistant/components/roborock/button.py b/homeassistant/components/roborock/button.py index 445033a0f6d..50d84e37a44 100644 --- a/homeassistant/components/roborock/button.py +++ b/homeassistant/components/roborock/button.py @@ -7,14 +7,12 @@ from dataclasses import dataclass from roborock.roborock_typing import RoborockCommand from homeassistant.components.button import ButtonEntity, ButtonEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify -from . import RoborockCoordinators -from .const import DOMAIN +from . import RoborockConfigEntry from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockEntityV1 @@ -65,17 +63,16 @@ CONSUMABLE_BUTTON_DESCRIPTIONS = [ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: RoborockConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock button platform.""" - coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( RoborockButtonEntity( coordinator, description, ) - for coordinator in coordinators.v1 + for coordinator in config_entry.runtime_data.v1 for description in CONSUMABLE_BUTTON_DESCRIPTIONS if isinstance(coordinator, RoborockDataUpdateCoordinator) ) diff --git a/homeassistant/components/roborock/diagnostics.py b/homeassistant/components/roborock/diagnostics.py index 9be8b6f4d63..63de0da6a7f 100644 --- a/homeassistant/components/roborock/diagnostics.py +++ b/homeassistant/components/roborock/diagnostics.py @@ -5,12 +5,10 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics.util import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import HomeAssistant -from . import RoborockCoordinators -from .const import DOMAIN +from . import RoborockConfigEntry TO_REDACT_CONFIG = ["token", "sn", "rruid", CONF_UNIQUE_ID, "username", "uid"] @@ -18,10 +16,10 @@ TO_REDACT_COORD = ["duid", "localKey", "mac", "bssid"] async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: RoborockConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] + coordinators = config_entry.runtime_data return { "config_entry": async_redact_data(config_entry.data, TO_REDACT_CONFIG), diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index d1731d289db..4ead7e9635d 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -13,7 +13,6 @@ from vacuum_map_parser_base.config.size import Sizes from vacuum_map_parser_roborock.map_data_parser import RoborockMapDataParser from homeassistant.components.image import ImageEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -21,7 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify import homeassistant.util.dt as dt_util -from . import RoborockCoordinators +from . import RoborockConfigEntry from .const import DEFAULT_DRAWABLES, DOMAIN, DRAWABLES, IMAGE_CACHE_INTERVAL, MAP_SLEEP from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockCoordinatedEntityV1 @@ -29,12 +28,11 @@ from .device import RoborockCoordinatedEntityV1 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: RoborockConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock image platform.""" - coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] drawables = [ drawable for drawable, default_value in DEFAULT_DRAWABLES.items() @@ -45,7 +43,7 @@ async def async_setup_entry( await asyncio.gather( *( create_coordinator_maps(coord, drawables) - for coord in coordinators.v1 + for coord in config_entry.runtime_data.v1 ) ) ) diff --git a/homeassistant/components/roborock/number.py b/homeassistant/components/roborock/number.py index 5e776d40f2d..e86f07ad204 100644 --- a/homeassistant/components/roborock/number.py +++ b/homeassistant/components/roborock/number.py @@ -11,14 +11,12 @@ from roborock.exceptions import RoborockException from roborock.version_1_apis.roborock_client_v1 import AttributeCache from homeassistant.components.number import NumberEntity, NumberEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify -from . import RoborockCoordinators -from .const import DOMAIN +from . import RoborockConfigEntry from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockEntityV1 @@ -51,16 +49,15 @@ NUMBER_DESCRIPTIONS: list[RoborockNumberDescription] = [ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: RoborockConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock number platform.""" - coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] possible_entities: list[ tuple[RoborockDataUpdateCoordinator, RoborockNumberDescription] ] = [ (coordinator, description) - for coordinator in coordinators.v1 + for coordinator in config_entry.runtime_data.v1 for description in NUMBER_DESCRIPTIONS ] # We need to check if this function is supported by the device. diff --git a/homeassistant/components/roborock/select.py b/homeassistant/components/roborock/select.py index c6073645086..8966652c24d 100644 --- a/homeassistant/components/roborock/select.py +++ b/homeassistant/components/roborock/select.py @@ -8,14 +8,12 @@ from roborock.roborock_message import RoborockDataProtocol from roborock.roborock_typing import RoborockCommand from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify -from . import RoborockCoordinators -from .const import DOMAIN +from . import RoborockConfigEntry from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockCoordinatedEntityV1 @@ -65,15 +63,14 @@ SELECT_DESCRIPTIONS: list[RoborockSelectDescription] = [ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: RoborockConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock select platform.""" - coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( RoborockSelectEntity(coordinator, description, options) - for coordinator in coordinators.v1 + for coordinator in config_entry.runtime_data.v1 for description in SELECT_DESCRIPTIONS if ( options := description.options_lambda( diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 3be7461d149..71c996f0b53 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -21,7 +21,6 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( AREA_SQUARE_METERS, PERCENTAGE, @@ -33,8 +32,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import slugify -from . import RoborockCoordinators -from .const import DOMAIN +from . import RoborockConfigEntry from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01 from .device import RoborockCoordinatedEntityA01, RoborockCoordinatedEntityV1 @@ -255,11 +253,11 @@ A01_SENSOR_DESCRIPTIONS: list[RoborockSensorDescriptionA01] = [ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: RoborockConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Roborock vacuum sensors.""" - coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] + coordinators = config_entry.runtime_data async_add_entities( RoborockSensorEntity( coordinator, diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index cdfc0c2dc96..6cc562fb533 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -12,14 +12,12 @@ from roborock.command_cache import CacheableAttribute from roborock.version_1_apis.roborock_client_v1 import AttributeCache from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify -from . import RoborockCoordinators -from .const import DOMAIN +from . import RoborockConfigEntry from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockEntityV1 @@ -99,16 +97,15 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: RoborockConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock switch platform.""" - coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] possible_entities: list[ tuple[RoborockDataUpdateCoordinator, RoborockSwitchDescription] ] = [ (coordinator, description) - for coordinator in coordinators.v1 + for coordinator in config_entry.runtime_data.v1 for description in SWITCH_DESCRIPTIONS ] # We need to check if this function is supported by the device. diff --git a/homeassistant/components/roborock/time.py b/homeassistant/components/roborock/time.py index 21ab26c0013..b0fbb18ed56 100644 --- a/homeassistant/components/roborock/time.py +++ b/homeassistant/components/roborock/time.py @@ -13,14 +13,12 @@ from roborock.exceptions import RoborockException from roborock.version_1_apis.roborock_client_v1 import AttributeCache from homeassistant.components.time import TimeEntity, TimeEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify -from . import RoborockCoordinators -from .const import DOMAIN +from . import RoborockConfigEntry from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockEntityV1 @@ -115,16 +113,15 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: RoborockConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock time platform.""" - coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] possible_entities: list[ tuple[RoborockDataUpdateCoordinator, RoborockTimeDescription] ] = [ (coordinator, description) - for coordinator in coordinators.v1 + for coordinator in config_entry.runtime_data.v1 for description in TIME_DESCRIPTIONS ] # We need to check if this function is supported by the device. diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index cefcc85d7f8..90f5002a23e 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -17,13 +17,12 @@ from homeassistant.components.vacuum import ( StateVacuumEntity, VacuumEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify -from . import RoborockCoordinators +from . import RoborockConfigEntry from .const import DOMAIN, GET_MAPS_SERVICE_NAME from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockCoordinatedEntityV1 @@ -57,14 +56,13 @@ STATE_CODE_TO_STATE = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: RoborockConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Roborock sensor.""" - coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( RoborockVacuum(coordinator) - for coordinator in coordinators.v1 + for coordinator in config_entry.runtime_data.v1 if isinstance(coordinator, RoborockDataUpdateCoordinator) ) diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index 0437ce781f1..704f093d3fd 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -29,7 +29,6 @@ async def test_unload_entry( await hass.async_block_till_done() assert mock_disconnect.call_count == 2 assert setup_entry.state is ConfigEntryState.NOT_LOADED - assert not hass.data.get(DOMAIN) async def test_config_entry_not_ready( From d8ab2debfdd2ab7c5ca789447c84ba4c7d9d7f30 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 26 Jun 2024 21:35:23 +0300 Subject: [PATCH 0265/2411] Add last_error reporting to Shelly diagnostics (#120595) --- homeassistant/components/shelly/diagnostics.py | 10 ++++++++++ tests/components/shelly/test_diagnostics.py | 13 ++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/diagnostics.py b/homeassistant/components/shelly/diagnostics.py index db69abc8f55..e70b76a7c00 100644 --- a/homeassistant/components/shelly/diagnostics.py +++ b/homeassistant/components/shelly/diagnostics.py @@ -24,6 +24,8 @@ async def async_get_config_entry_diagnostics( device_settings: str | dict = "not initialized" device_status: str | dict = "not initialized" bluetooth: str | dict = "not initialized" + last_error: str = "not initialized" + if shelly_entry_data.block: block_coordinator = shelly_entry_data.block assert block_coordinator @@ -55,6 +57,10 @@ async def async_get_config_entry_diagnostics( "uptime", ] } + + if block_coordinator.device.last_error: + last_error = repr(block_coordinator.device.last_error) + else: rpc_coordinator = shelly_entry_data.rpc assert rpc_coordinator @@ -79,6 +85,9 @@ async def async_get_config_entry_diagnostics( "scanner": await scanner.async_diagnostics(), } + if rpc_coordinator.device.last_error: + last_error = repr(rpc_coordinator.device.last_error) + if isinstance(device_status, dict): device_status = async_redact_data(device_status, ["ssid"]) @@ -87,5 +96,6 @@ async def async_get_config_entry_diagnostics( "device_info": device_info, "device_settings": device_settings, "device_status": device_status, + "last_error": last_error, "bluetooth": bluetooth, } diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py index f7f238f3327..4fc8ea6ca8f 100644 --- a/tests/components/shelly/test_diagnostics.py +++ b/tests/components/shelly/test_diagnostics.py @@ -1,9 +1,10 @@ """Tests for Shelly diagnostics platform.""" -from unittest.mock import ANY, Mock +from unittest.mock import ANY, Mock, PropertyMock from aioshelly.ble.const import BLE_SCAN_RESULT_EVENT from aioshelly.const import MODEL_25 +from aioshelly.exceptions import DeviceConnectionError import pytest from homeassistant.components.diagnostics import REDACTED @@ -36,6 +37,10 @@ async def test_block_config_entry_diagnostics( {key: REDACTED for key in TO_REDACT if key in entry_dict["data"]} ) + type(mock_block_device).last_error = PropertyMock( + return_value=DeviceConnectionError() + ) + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert result == { @@ -48,6 +53,7 @@ async def test_block_config_entry_diagnostics( }, "device_settings": {"coiot": {"update_period": 15}}, "device_status": MOCK_STATUS_COAP, + "last_error": "DeviceConnectionError()", } @@ -91,6 +97,10 @@ async def test_rpc_config_entry_diagnostics( {key: REDACTED for key in TO_REDACT if key in entry_dict["data"]} ) + type(mock_rpc_device).last_error = PropertyMock( + return_value=DeviceConnectionError() + ) + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert result == { @@ -152,4 +162,5 @@ async def test_rpc_config_entry_diagnostics( }, "wifi": {"rssi": -63}, }, + "last_error": "DeviceConnectionError()", } From 4c6cbadc11bbab816562cf281253474aee0ccbea Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 26 Jun 2024 21:53:02 +0300 Subject: [PATCH 0266/2411] Align Shelly sleeping devices timeout with non-sleeping (#118969) --- homeassistant/components/shelly/const.py | 4 +--- homeassistant/components/shelly/coordinator.py | 9 ++++----- tests/components/shelly/test_binary_sensor.py | 4 ++-- tests/components/shelly/test_coordinator.py | 9 ++++----- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index fcc7cc44af9..c5bdb88bbd1 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -83,11 +83,9 @@ REST_SENSORS_UPDATE_INTERVAL: Final = 60 # Refresh interval for RPC polling sensors RPC_SENSORS_POLLING_INTERVAL: Final = 60 -# Multiplier used to calculate the "update_interval" for sleeping devices. -SLEEP_PERIOD_MULTIPLIER: Final = 1.2 CONF_SLEEP_PERIOD: Final = "sleep_period" -# Multiplier used to calculate the "update_interval" for non-sleeping devices. +# Multiplier used to calculate the "update_interval" for shelly devices. UPDATE_PERIOD_MULTIPLIER: Final = 2.2 # Reconnect interval for GEN2 devices diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index fd5cfaa1542..02feef3633b 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -54,7 +54,6 @@ from .const import ( RPC_RECONNECT_INTERVAL, RPC_SENSORS_POLLING_INTERVAL, SHBTN_MODELS, - SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, BLEScannerMode, ) @@ -229,7 +228,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): """Initialize the Shelly block device coordinator.""" self.entry = entry if self.sleep_period: - update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period + update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period else: update_interval = ( UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] @@ -429,7 +428,7 @@ class ShellyRestCoordinator(ShellyCoordinatorBase[BlockDevice]): in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION ): update_interval = ( - SLEEP_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] + UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] ) super().__init__(hass, entry, device, update_interval) @@ -459,7 +458,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): """Initialize the Shelly RPC device coordinator.""" self.entry = entry if self.sleep_period: - update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period + update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period else: update_interval = RPC_RECONNECT_INTERVAL super().__init__(hass, entry, device, update_interval) @@ -486,7 +485,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): data[CONF_SLEEP_PERIOD] = wakeup_period self.hass.config_entries.async_update_entry(self.entry, data=data) - update_interval = SLEEP_PERIOD_MULTIPLIER * wakeup_period + update_interval = UPDATE_PERIOD_MULTIPLIER * wakeup_period self.update_interval = timedelta(seconds=update_interval) return True diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 026a7041863..3bfbf350f7e 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -7,7 +7,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.shelly.const import SLEEP_PERIOD_MULTIPLIER +from homeassistant.components.shelly.const import UPDATE_PERIOD_MULTIPLIER from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State from homeassistant.helpers.device_registry import DeviceRegistry @@ -122,7 +122,7 @@ async def test_block_rest_binary_sensor_connected_battery_devices( assert hass.states.get(entity_id).state == STATE_OFF # Verify update on slow intervals - await mock_rest_update(hass, freezer, seconds=SLEEP_PERIOD_MULTIPLIER * 3600) + await mock_rest_update(hass, freezer, seconds=UPDATE_PERIOD_MULTIPLIER * 3600) assert hass.states.get(entity_id).state == STATE_ON entry = entity_registry.async_get(entity_id) diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 1e0af115c9e..35123a2db91 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -20,7 +20,6 @@ from homeassistant.components.shelly.const import ( ENTRY_RELOAD_COOLDOWN, MAX_PUSH_UPDATE_FAILURES, RPC_RECONNECT_INTERVAL, - SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, BLEScannerMode, ) @@ -564,7 +563,7 @@ async def test_rpc_update_entry_sleep_period( # Move time to generate sleep period update monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 3600) - freezer.tick(timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER)) + freezer.tick(timedelta(seconds=600 * UPDATE_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -596,7 +595,7 @@ async def test_rpc_sleeping_device_no_periodic_updates( assert get_entity_state(hass, entity_id) == "22.9" # Move time to generate polling - freezer.tick(timedelta(seconds=SLEEP_PERIOD_MULTIPLIER * 1000)) + freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 1000)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -889,7 +888,7 @@ async def test_block_sleeping_device_connection_error( assert get_entity_state(hass, entity_id) == STATE_ON # Move time to generate sleep period update - freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER)) + freezer.tick(timedelta(seconds=sleep_period * UPDATE_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -934,7 +933,7 @@ async def test_rpc_sleeping_device_connection_error( assert get_entity_state(hass, entity_id) == STATE_ON # Move time to generate sleep period update - freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER)) + freezer.tick(timedelta(seconds=sleep_period * UPDATE_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) From 702d53ca30e0f86f07a72d6efaef645d6cfee125 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 26 Jun 2024 20:55:25 +0200 Subject: [PATCH 0267/2411] Correct deprecation warning `async_register_static_paths` (#120592) --- homeassistant/components/http/__init__.py | 2 +- tests/components/http/test_init.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 38f0b628b2c..0d86ab57d3f 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -483,7 +483,7 @@ class HomeAssistantHTTP: frame.report( "calls hass.http.register_static_path which is deprecated because " "it does blocking I/O in the event loop, instead " - "call `await hass.http.async_register_static_path(" + "call `await hass.http.async_register_static_paths(" f'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`; ' "This function will be removed in 2025.7", exclude_integrations={"http"}, diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 7a9fb329fcd..2895209b5f9 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -543,5 +543,5 @@ async def test_register_static_paths( "Detected code that calls hass.http.register_static_path " "which is deprecated because it does blocking I/O in the " "event loop, instead call " - "`await hass.http.async_register_static_path" + "`await hass.http.async_register_static_paths" ) in caplog.text From f1a57d69ccec30920e693c92fb6c3468047c25ae Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 20:55:41 +0200 Subject: [PATCH 0268/2411] Remove deprecated run_immediately flag from integration sensor (#120593) --- homeassistant/components/integration/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 60cbee5549f..4fca92e9b40 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -446,7 +446,6 @@ class IntegrationSensor(RestoreSensor): event_filter=callback( lambda event_data: event_data["entity_id"] == self._sensor_source_id ), - run_immediately=True, ) ) self.async_on_remove( @@ -456,7 +455,6 @@ class IntegrationSensor(RestoreSensor): event_filter=callback( lambda event_data: event_data["entity_id"] == self._sensor_source_id ), - run_immediately=True, ) ) From 8839a71adcb40664a1ae23739096bd88b26185d3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 20:57:27 +0200 Subject: [PATCH 0269/2411] Prevent changes to mutable bmw_connected_drive fixture data (#120600) --- .../bmw_connected_drive/test_config_flow.py | 7 ++++--- tests/components/bmw_connected_drive/test_init.py | 14 +++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index b562e2b898f..3c7f452a011 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -92,7 +92,7 @@ async def test_api_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=FIXTURE_USER_INPUT, + data=deepcopy(FIXTURE_USER_INPUT), ) assert result["type"] is FlowResultType.FORM @@ -116,7 +116,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=FIXTURE_USER_INPUT, + data=deepcopy(FIXTURE_USER_INPUT), ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] @@ -137,7 +137,8 @@ async def test_options_flow_implementation(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry, ): - config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + config_entry_args = deepcopy(FIXTURE_CONFIG_ENTRY) + config_entry = MockConfigEntry(**config_entry_args) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/bmw_connected_drive/test_init.py b/tests/components/bmw_connected_drive/test_init.py index 52bc8a7ce05..5cd6362d6fa 100644 --- a/tests/components/bmw_connected_drive/test_init.py +++ b/tests/components/bmw_connected_drive/test_init.py @@ -1,5 +1,6 @@ """Test Axis component setup process.""" +from copy import deepcopy from unittest.mock import patch import pytest @@ -37,7 +38,7 @@ async def test_migrate_options( ) -> None: """Test successful migration of options.""" - config_entry = FIXTURE_CONFIG_ENTRY.copy() + config_entry = deepcopy(FIXTURE_CONFIG_ENTRY) config_entry["options"] = options mock_config_entry = MockConfigEntry(**config_entry) @@ -55,7 +56,7 @@ async def test_migrate_options( async def test_migrate_options_from_data(hass: HomeAssistant) -> None: """Test successful migration of options.""" - config_entry = FIXTURE_CONFIG_ENTRY.copy() + config_entry = deepcopy(FIXTURE_CONFIG_ENTRY) config_entry["options"] = {} config_entry["data"].update({CONF_READ_ONLY: False}) @@ -107,7 +108,8 @@ async def test_migrate_unique_ids( entity_registry: er.EntityRegistry, ) -> None: """Test successful migration of entity unique_ids.""" - mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + confg_entry = deepcopy(FIXTURE_CONFIG_ENTRY) + mock_config_entry = MockConfigEntry(**confg_entry) mock_config_entry.add_to_hass(hass) entity: er.RegistryEntry = entity_registry.async_get_or_create( @@ -153,7 +155,8 @@ async def test_dont_migrate_unique_ids( entity_registry: er.EntityRegistry, ) -> None: """Test successful migration of entity unique_ids.""" - mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + confg_entry = deepcopy(FIXTURE_CONFIG_ENTRY) + mock_config_entry = MockConfigEntry(**confg_entry) mock_config_entry.add_to_hass(hass) # create existing entry with new_unique_id @@ -196,7 +199,8 @@ async def test_remove_stale_devices( device_registry: dr.DeviceRegistry, ) -> None: """Test remove stale device registry entries.""" - mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + config_entry = deepcopy(FIXTURE_CONFIG_ENTRY) + mock_config_entry = MockConfigEntry(**config_entry) mock_config_entry.add_to_hass(hass) device_registry.async_get_or_create( From dd6cc82f709deee8b092cae5301faca8b603536e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 21:30:30 +0200 Subject: [PATCH 0270/2411] Fix mqtt test fixture usage (#120602) --- tests/components/mqtt/test_config_flow.py | 2 +- tests/components/mqtt/test_image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 57975fdc309..457bd19c16f 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1561,7 +1561,7 @@ async def test_setup_with_advanced_settings( } -@pytest.mark.usesfixtures("mock_ssl_context", "mock_process_uploaded_file") +@pytest.mark.usefixtures("mock_ssl_context", "mock_process_uploaded_file") async def test_change_websockets_transport_to_tcp( hass: HomeAssistant, mock_try_connection: MagicMock ) -> None: diff --git a/tests/components/mqtt/test_image.py b/tests/components/mqtt/test_image.py index 29109ee12f4..bb029fba231 100644 --- a/tests/components/mqtt/test_image.py +++ b/tests/components/mqtt/test_image.py @@ -499,7 +499,7 @@ async def test_image_from_url_fails( ), ], ) -@pytest.mark.usesfixtures("hass", "hass_client_no_auth") +@pytest.mark.usefixtures("hass", "hass_client_no_auth") async def test_image_config_fails( mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, From 9b2915efedbb6d21ba076a17a1a68032fca757cb Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 26 Jun 2024 21:45:17 +0200 Subject: [PATCH 0271/2411] Don't allow switch toggle when device in locked in AVM FRITZ!SmartHome (#120132) * fix: set state of the FritzBox-Switch to disabled if the option for manuel switching in the userinterface is disabled * feat: raise an error instead of disabling switch * feat: rename method signature * fix: tests * fix: wrong import * feat: Update homeassistant/components/fritzbox/strings.json Update error message Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * Update tests/components/fritzbox/test_switch.py feat: update test Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * make ruff happy * fix expected error message check --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- .../components/fritzbox/strings.json | 3 ++ homeassistant/components/fritzbox/switch.py | 12 ++++++++ tests/components/fritzbox/__init__.py | 2 +- tests/components/fritzbox/test_switch.py | 30 +++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index cee0afa26c1..d4f59fd1c08 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -81,6 +81,9 @@ } }, "exceptions": { + "manual_switching_disabled": { + "message": "Can't toggle switch while manual switching is disabled for the device." + }, "change_preset_while_active_mode": { "message": "Can't change preset while holiday or summer mode is active on the device." }, diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 0bdf7a9f944..d13f21e1c14 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -6,9 +6,11 @@ from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FritzBoxDeviceEntity +from .const import DOMAIN from .coordinator import FritzboxConfigEntry @@ -48,10 +50,20 @@ class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" + self.check_lock_state() await self.hass.async_add_executor_job(self.data.set_switch_state_on) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" + self.check_lock_state() await self.hass.async_add_executor_job(self.data.set_switch_state_off) await self.coordinator.async_refresh() + + def check_lock_state(self) -> None: + """Raise an Error if manual switching via FRITZ!Box user interface is disabled.""" + if self.data.lock: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="manual_switching_disabled", + ) diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 2bd8f26d73b..61312805e91 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -151,7 +151,7 @@ class FritzDeviceSwitchMock(FritzEntityBaseMock): has_thermostat = False has_blind = False switch_state = "fake_state" - lock = "fake_locked" + lock = False power = 5678 present = True temperature = 1.23 diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 417b355b396..ba3b1de9b2f 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import Mock +import pytest from requests.exceptions import HTTPError from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN @@ -29,6 +30,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util @@ -130,6 +132,7 @@ async def test_turn_on(hass: HomeAssistant, fritz: Mock) -> None: async def test_turn_off(hass: HomeAssistant, fritz: Mock) -> None: """Test turn device off.""" device = FritzDeviceSwitchMock() + assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -137,9 +140,36 @@ async def test_turn_off(hass: HomeAssistant, fritz: Mock) -> None: await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) + assert device.set_switch_state_off.call_count == 1 +async def test_toggle_while_locked(hass: HomeAssistant, fritz: Mock) -> None: + """Test toggling while device is locked.""" + device = FritzDeviceSwitchMock() + device.lock = True + + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + with pytest.raises( + HomeAssistantError, + match="Can't toggle switch while manual switching is disabled for the device", + ): + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + + with pytest.raises( + HomeAssistantError, + match="Can't toggle switch while manual switching is disabled for the device", + ): + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + + async def test_update(hass: HomeAssistant, fritz: Mock) -> None: """Test update without error.""" device = FritzDeviceSwitchMock() From 2146a4729b570750fe7bc0cad4102e251a3b2371 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Wed, 26 Jun 2024 21:46:59 +0200 Subject: [PATCH 0272/2411] Improve Bang & Olufsen error messages (#120587) * Convert logger messages to raised errors where applicable * Modify exception types * Improve deezer / tidal error message * Update homeassistant/components/bang_olufsen/strings.json Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * Update homeassistant/components/bang_olufsen/media_player.py Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- .../components/bang_olufsen/media_player.py | 41 ++++++++++++------- .../components/bang_olufsen/strings.json | 12 ++++++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index d23c75046ff..0eff9f2bb85 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -45,7 +45,7 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODEL from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -316,7 +316,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): @callback def _async_update_playback_error(self, data: PlaybackError) -> None: """Show playback error.""" - _LOGGER.error(data.error) + raise HomeAssistantError(data.error) @callback def _async_update_playback_progress(self, data: PlaybackProgress) -> None: @@ -516,7 +516,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): self.async_write_ha_state() else: - _LOGGER.error("Seeking is currently only supported when using Deezer") + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="non_deezer_seeking" + ) async def async_media_previous_track(self) -> None: """Send the previous track command.""" @@ -529,12 +531,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): async def async_select_source(self, source: str) -> None: """Select an input source.""" if source not in self._sources.values(): - _LOGGER.error( - "Invalid source: %s. Valid sources are: %s", - source, - list(self._sources.values()), + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_source", + translation_placeholders={ + "invalid_source": source, + "valid_sources": ",".join(list(self._sources.values())), + }, ) - return key = [x for x in self._sources if self._sources[x] == source][0] @@ -559,12 +563,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): media_type = MediaType.MUSIC if media_type not in VALID_MEDIA_TYPES: - _LOGGER.error( - "%s is an invalid type. Valid values are: %s", - media_type, - VALID_MEDIA_TYPES, + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_media_type", + translation_placeholders={ + "invalid_media_type": media_type, + "valid_media_types": ",".join(VALID_MEDIA_TYPES), + }, ) - return if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media( @@ -681,7 +687,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): ) except ApiException as error: - _LOGGER.error(json.loads(error.body)["message"]) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="play_media_error", + translation_placeholders={ + "media_type": media_type, + "error_message": json.loads(error.body)["message"], + }, + ) from error async def async_browse_media( self, diff --git a/homeassistant/components/bang_olufsen/strings.json b/homeassistant/components/bang_olufsen/strings.json index 93b55cf0db2..cf5b212d424 100644 --- a/homeassistant/components/bang_olufsen/strings.json +++ b/homeassistant/components/bang_olufsen/strings.json @@ -28,6 +28,18 @@ "exceptions": { "m3u_invalid_format": { "message": "Media sources with the .m3u extension are not supported." + }, + "non_deezer_seeking": { + "message": "Seeking is currently only supported when using Deezer" + }, + "invalid_source": { + "message": "Invalid source: {invalid_source}. Valid sources are: {valid_sources}" + }, + "invalid_media_type": { + "message": "{invalid_media_type} is an invalid type. Valid values are: {valid_media_types}." + }, + "play_media_error": { + "message": "An error occurred while attempting to play {media_type}: {error_message}." } } } From 516b9126b795759d441bfcb85ace7fd5d191c098 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jun 2024 22:05:30 +0200 Subject: [PATCH 0273/2411] Update adguardhome to 0.7.0 (#120605) --- homeassistant/components/adguard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index 52add51a663..f1b82177d5b 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "local_polling", "loggers": ["adguardhome"], - "requirements": ["adguardhome==0.6.3"] + "requirements": ["adguardhome==0.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index eca0100e5df..4e7d43ccdd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -149,7 +149,7 @@ adb-shell[async]==0.4.4 adext==0.4.3 # homeassistant.components.adguard -adguardhome==0.6.3 +adguardhome==0.7.0 # homeassistant.components.advantage_air advantage-air==0.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf8fd1dc081..2f0a793cc28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -128,7 +128,7 @@ adb-shell[async]==0.4.4 adext==0.4.3 # homeassistant.components.adguard -adguardhome==0.6.3 +adguardhome==0.7.0 # homeassistant.components.advantage_air advantage-air==0.4.4 From fcfb580f0c4f7da1a88da7bd86b07006c1a8590f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 22:29:55 +0200 Subject: [PATCH 0274/2411] Update pylint to 3.2.4 (#120606) --- homeassistant/components/solarlog/config_flow.py | 2 +- requirements_test.txt | 2 +- tests/common.py | 2 +- tests/components/influxdb/test_init.py | 1 - tests/components/owntracks/test_device_tracker.py | 2 -- 5 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py index eb0971e0d92..7c8401be2b8 100644 --- a/homeassistant/components/solarlog/config_flow.py +++ b/homeassistant/components/solarlog/config_flow.py @@ -58,7 +58,7 @@ class SolarLogConfigFlow(ConfigFlow, domain=DOMAIN): except SolarLogConnectionError: self._errors = {CONF_HOST: "cannot_connect"} return False - except SolarLogError: # pylint: disable=broad-except + except SolarLogError: self._errors = {CONF_HOST: "unknown"} return False finally: diff --git a/requirements_test.txt b/requirements_test.txt index 460da410db6..e2818b559ea 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,7 +14,7 @@ mock-open==1.4.0 mypy-dev==1.11.0a9 pre-commit==3.7.1 pydantic==1.10.17 -pylint==3.2.2 +pylint==3.2.4 pylint-per-file-ignores==1.3.2 pipdeptree==2.19.0 pytest-asyncio==0.23.6 diff --git a/tests/common.py b/tests/common.py index f5531dbf40d..52ea4861c81 100644 --- a/tests/common.py +++ b/tests/common.py @@ -379,7 +379,7 @@ def async_mock_service( calls = [] @callback - def mock_service_log(call): # pylint: disable=unnecessary-lambda + def mock_service_log(call): """Mock service call.""" calls.append(call) if raise_exception is not None: diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index aba153cf8a8..2d93322999d 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -79,7 +79,6 @@ def get_mock_call_fixture(request: pytest.FixtureRequest): if request.param == influxdb.API_VERSION_2: return lambda body, precision=None: v2_call(body, precision) - # pylint: disable-next=unnecessary-lambda return lambda body, precision=None: call(body, time_precision=precision) diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 8246a7f51ac..0648a94c70b 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -285,8 +285,6 @@ BAD_MESSAGE = {"_type": "unsupported", "tst": 1} BAD_JSON_PREFIX = "--$this is bad json#--" BAD_JSON_SUFFIX = "** and it ends here ^^" -# pylint: disable=len-as-condition - @pytest.fixture def setup_comp( From 6bceb8ec485ab9c7ec9f0a85e669ea35d5932368 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 22:44:43 +0200 Subject: [PATCH 0275/2411] Add some more VolDictType annotations (#120610) --- homeassistant/components/elkm1/config_flow.py | 4 ++-- homeassistant/components/fritz/config_flow.py | 5 +++-- .../components/generic_thermostat/climate.py | 8 ++++++-- .../components/homeworks/config_flow.py | 5 +++-- .../components/keenetic_ndms2/config_flow.py | 3 ++- homeassistant/components/lcn/schemas.py | 17 +++++++++-------- homeassistant/components/light/__init__.py | 8 +++----- .../components/monoprice/config_flow.py | 3 ++- .../components/motioneye/config_flow.py | 3 ++- homeassistant/components/nina/config_flow.py | 11 +++++++---- homeassistant/components/vera/config_flow.py | 5 ++--- homeassistant/helpers/config_validation.py | 4 ++-- 12 files changed, 43 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 972b38d2ae9..4ab8d1fe181 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -21,7 +21,7 @@ from homeassistant.const import ( ) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.typing import DiscoveryInfoType +from homeassistant.helpers.typing import DiscoveryInfoType, VolDictType from homeassistant.util import slugify from homeassistant.util.network import is_ip_address @@ -52,7 +52,7 @@ PROTOCOL_MAP = { VALIDATE_TIMEOUT = 35 -BASE_SCHEMA = { +BASE_SCHEMA: VolDictType = { vol.Optional(CONF_USERNAME, default=""): str, vol.Optional(CONF_PASSWORD, default=""): str, } diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 4cdd4c19c1b..fbc324fde2b 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -33,6 +33,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.helpers.typing import VolDictType from .const import ( CONF_OLD_DISCOVERY, @@ -210,7 +211,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Show the setup form to the user.""" - advanced_data_schema = {} + advanced_data_schema: VolDictType = {} if self.show_advanced_options: advanced_data_schema = { vol.Optional(CONF_PORT): vol.Coerce(int), @@ -348,7 +349,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any], errors: dict[str, str] | None = None ) -> ConfigFlowResult: """Show the reconfigure form to the user.""" - advanced_data_schema = {} + advanced_data_schema: VolDictType = {} if self.show_advanced_options: advanced_data_schema = { vol.Optional(CONF_PORT, default=user_input[CONF_PORT]): vol.Coerce(int), diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index c080e8b82d7..d284c7d7772 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -61,7 +61,7 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolDictType from . import DOMAIN, PLATFORMS @@ -96,6 +96,10 @@ CONF_PRESETS = { ) } +PRESETS_SCHEMA: VolDictType = { + vol.Optional(v): vol.Coerce(float) for v in CONF_PRESETS.values() +} + PLATFORM_SCHEMA_COMMON = vol.Schema( { vol.Required(CONF_HEATER): cv.entity_id, @@ -120,7 +124,7 @@ PLATFORM_SCHEMA_COMMON = vol.Schema( vol.In([PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]) ), vol.Optional(CONF_UNIQUE_ID): cv.string, - **{vol.Optional(v): vol.Coerce(float) for v in CONF_PRESETS.values()}, + **PRESETS_SCHEMA, } ) diff --git a/homeassistant/components/homeworks/config_flow.py b/homeassistant/components/homeworks/config_flow.py index 4b91018036a..81b31e4644e 100644 --- a/homeassistant/components/homeworks/config_flow.py +++ b/homeassistant/components/homeworks/config_flow.py @@ -29,6 +29,7 @@ from homeassistant.helpers.schema_config_entry_flow import ( SchemaOptionsFlowHandler, ) from homeassistant.helpers.selector import TextSelector +from homeassistant.helpers.typing import VolDictType from homeassistant.util import slugify from . import DEFAULT_FADE_RATE, calculate_unique_id @@ -62,7 +63,7 @@ CONTROLLER_EDIT = { ), } -LIGHT_EDIT = { +LIGHT_EDIT: VolDictType = { vol.Optional(CONF_RATE, default=DEFAULT_FADE_RATE): selector.NumberSelector( selector.NumberSelectorConfig( min=0, @@ -73,7 +74,7 @@ LIGHT_EDIT = { ), } -BUTTON_EDIT = { +BUTTON_EDIT: VolDictType = { vol.Optional(CONF_LED, default=False): selector.BooleanSelector(), vol.Optional(CONF_RELEASE_DELAY, default=0): selector.NumberSelector( selector.NumberSelectorConfig( diff --git a/homeassistant/components/keenetic_ndms2/config_flow.py b/homeassistant/components/keenetic_ndms2/config_flow.py index f00bbe22939..9e3c6728338 100644 --- a/homeassistant/components/keenetic_ndms2/config_flow.py +++ b/homeassistant/components/keenetic_ndms2/config_flow.py @@ -24,6 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import VolDictType from .const import ( CONF_CONSIDER_HOME, @@ -84,7 +85,7 @@ class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN): title=router_info.name, data={CONF_HOST: host, **user_input} ) - host_schema = ( + host_schema: VolDictType = ( {vol.Required(CONF_HOST): str} if CONF_HOST not in self.context else {} ) diff --git a/homeassistant/components/lcn/schemas.py b/homeassistant/components/lcn/schemas.py index b907525747d..9927ea5908d 100644 --- a/homeassistant/components/lcn/schemas.py +++ b/homeassistant/components/lcn/schemas.py @@ -21,6 +21,7 @@ from homeassistant.const import ( UnitOfTemperature, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import VolDictType from .const import ( BINSENSOR_PORTS, @@ -61,14 +62,14 @@ from .helpers import has_unique_host_names, is_address # Domain data # -DOMAIN_DATA_BINARY_SENSOR = { +DOMAIN_DATA_BINARY_SENSOR: VolDictType = { vol.Required(CONF_SOURCE): vol.All( vol.Upper, vol.In(SETPOINTS + KEYS + BINSENSOR_PORTS) ), } -DOMAIN_DATA_CLIMATE = { +DOMAIN_DATA_CLIMATE: VolDictType = { vol.Required(CONF_SOURCE): vol.All(vol.Upper, vol.In(VARIABLES)), vol.Required(CONF_SETPOINT): vol.All(vol.Upper, vol.In(VARIABLES + SETPOINTS)), vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float), @@ -80,7 +81,7 @@ DOMAIN_DATA_CLIMATE = { } -DOMAIN_DATA_COVER = { +DOMAIN_DATA_COVER: VolDictType = { vol.Required(CONF_MOTOR): vol.All(vol.Upper, vol.In(MOTOR_PORTS)), vol.Optional(CONF_REVERSE_TIME, default="rt1200"): vol.All( vol.Upper, vol.In(MOTOR_REVERSE_TIME) @@ -88,7 +89,7 @@ DOMAIN_DATA_COVER = { } -DOMAIN_DATA_LIGHT = { +DOMAIN_DATA_LIGHT: VolDictType = { vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS)), vol.Optional(CONF_DIMMABLE, default=False): vol.Coerce(bool), vol.Optional(CONF_TRANSITION, default=0): vol.All( @@ -97,7 +98,7 @@ DOMAIN_DATA_LIGHT = { } -DOMAIN_DATA_SCENE = { +DOMAIN_DATA_SCENE: VolDictType = { vol.Required(CONF_REGISTER): vol.All(vol.Coerce(int), vol.Range(0, 9)), vol.Required(CONF_SCENE): vol.All(vol.Coerce(int), vol.Range(0, 9)), vol.Optional(CONF_OUTPUTS, default=[]): vol.All( @@ -113,7 +114,7 @@ DOMAIN_DATA_SCENE = { ), } -DOMAIN_DATA_SENSOR = { +DOMAIN_DATA_SENSOR: VolDictType = { vol.Required(CONF_SOURCE): vol.All( vol.Upper, vol.In( @@ -126,7 +127,7 @@ DOMAIN_DATA_SENSOR = { } -DOMAIN_DATA_SWITCH = { +DOMAIN_DATA_SWITCH: VolDictType = { vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS)), } @@ -134,7 +135,7 @@ DOMAIN_DATA_SWITCH = { # Configuration # -DOMAIN_DATA_BASE = { +DOMAIN_DATA_BASE: VolDictType = { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_ADDRESS): is_address, } diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 67000b6aaaf..077071e6735 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -299,7 +299,7 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: def preprocess_turn_on_alternatives( - hass: HomeAssistant, params: dict[str, Any] | dict[str | vol.Optional, Any] + hass: HomeAssistant, params: dict[str, Any] | VolDictType ) -> None: """Process extra data for turn light on request. @@ -403,11 +403,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # of the light base platform. hass.async_create_task(profiles.async_initialize(), eager_start=True) - def preprocess_data( - data: dict[str | vol.Optional, Any], - ) -> dict[str | vol.Optional, Any]: + def preprocess_data(data: VolDictType) -> VolDictType: """Preprocess the service data.""" - base: dict[str | vol.Optional, Any] = { + base: VolDictType = { entity_field: data.pop(entity_field) for entity_field in cv.ENTITY_SERVICE_FIELDS if entity_field in data diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index 542e729dbd2..2c7163123b6 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import VolDictType from .const import ( CONF_SOURCE_1, @@ -35,7 +36,7 @@ SOURCES = [ CONF_SOURCE_6, ] -OPTIONS_FOR_DATA = {vol.Optional(source): str for source in SOURCES} +OPTIONS_FOR_DATA: VolDictType = {vol.Optional(source): str for source in SOURCES} DATA_SCHEMA = vol.Schema({vol.Required(CONF_PORT): str, **OPTIONS_FOR_DATA}) diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index 49059b528db..8107ca760cb 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -24,6 +24,7 @@ from homeassistant.const import CONF_SOURCE, CONF_URL, CONF_WEBHOOK_ID from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import VolDictType from . import create_motioneye_client from .const import ( @@ -55,7 +56,7 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): user_input: dict[str, Any], errors: dict[str, str] | None = None ) -> ConfigFlowResult: """Show the form to the user.""" - url_schema: dict[vol.Required, type[str]] = {} + url_schema: VolDictType = {} if not self._hassio_discovery: # Only ask for URL when not discovered url_schema[ diff --git a/homeassistant/components/nina/config_flow.py b/homeassistant/components/nina/config_flow.py index 3a665bfe987..1fee6430ffc 100644 --- a/homeassistant/components/nina/config_flow.py +++ b/homeassistant/components/nina/config_flow.py @@ -17,6 +17,7 @@ from homeassistant.core import callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import VolDictType from .const import ( _LOGGER, @@ -137,14 +138,16 @@ class NinaConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "no_selection" + regions_schema: VolDictType = { + vol.Optional(region): cv.multi_select(self.regions[region]) + for region in CONST_REGIONS + } + return self.async_show_form( step_id="user", data_schema=vol.Schema( { - **{ - vol.Optional(region): cv.multi_select(self.regions[region]) - for region in CONST_REGIONS - }, + **regions_schema, vol.Required(CONF_MESSAGE_SLOTS, default=5): vol.All( int, vol.Range(min=1, max=20) ), diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index fcb1e5f013e..181849f46a1 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -22,6 +22,7 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import callback from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.typing import VolDictType from .const import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN @@ -49,9 +50,7 @@ def new_options(lights: list[int], exclude: list[int]) -> dict[str, list[int]]: return {CONF_LIGHTS: lights, CONF_EXCLUDE: exclude} -def options_schema( - options: Mapping[str, Any] | None = None, -) -> dict[vol.Optional, type[str]]: +def options_schema(options: Mapping[str, Any] | None = None) -> VolDictType: """Return options schema.""" options = options or {} return { diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 58c76a40c8e..a28c81e6da9 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1204,7 +1204,7 @@ PLATFORM_SCHEMA = vol.Schema( PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) -ENTITY_SERVICE_FIELDS = { +ENTITY_SERVICE_FIELDS: VolDictType = { # Either accept static entity IDs, a single dynamic template or a mixed list # of static and dynamic templates. While this could be solved with a single # complex template, handling it like this, keeps config validation useful. @@ -1310,7 +1310,7 @@ def script_action(value: Any) -> dict: SCRIPT_SCHEMA = vol.All(ensure_list, [script_action]) -SCRIPT_ACTION_BASE_SCHEMA = { +SCRIPT_ACTION_BASE_SCHEMA: VolDictType = { vol.Optional(CONF_ALIAS): string, vol.Optional(CONF_CONTINUE_ON_ERROR): boolean, vol.Optional(CONF_ENABLED): vol.Any(boolean, template), From ba456f256448f6a5d9e9b23f2b64c51c9eb5883a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 26 Jun 2024 21:53:02 +0300 Subject: [PATCH 0276/2411] Align Shelly sleeping devices timeout with non-sleeping (#118969) --- homeassistant/components/shelly/const.py | 4 +--- homeassistant/components/shelly/coordinator.py | 9 ++++----- tests/components/shelly/test_binary_sensor.py | 4 ++-- tests/components/shelly/test_coordinator.py | 9 ++++----- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index fcc7cc44af9..c5bdb88bbd1 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -83,11 +83,9 @@ REST_SENSORS_UPDATE_INTERVAL: Final = 60 # Refresh interval for RPC polling sensors RPC_SENSORS_POLLING_INTERVAL: Final = 60 -# Multiplier used to calculate the "update_interval" for sleeping devices. -SLEEP_PERIOD_MULTIPLIER: Final = 1.2 CONF_SLEEP_PERIOD: Final = "sleep_period" -# Multiplier used to calculate the "update_interval" for non-sleeping devices. +# Multiplier used to calculate the "update_interval" for shelly devices. UPDATE_PERIOD_MULTIPLIER: Final = 2.2 # Reconnect interval for GEN2 devices diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 82d358b33d8..a4ff34f7d9a 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -54,7 +54,6 @@ from .const import ( RPC_RECONNECT_INTERVAL, RPC_SENSORS_POLLING_INTERVAL, SHBTN_MODELS, - SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, BLEScannerMode, ) @@ -229,7 +228,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): """Initialize the Shelly block device coordinator.""" self.entry = entry if self.sleep_period: - update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period + update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period else: update_interval = ( UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] @@ -429,7 +428,7 @@ class ShellyRestCoordinator(ShellyCoordinatorBase[BlockDevice]): in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION ): update_interval = ( - SLEEP_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] + UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] ) super().__init__(hass, entry, device, update_interval) @@ -459,7 +458,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): """Initialize the Shelly RPC device coordinator.""" self.entry = entry if self.sleep_period: - update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period + update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period else: update_interval = RPC_RECONNECT_INTERVAL super().__init__(hass, entry, device, update_interval) @@ -486,7 +485,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): data[CONF_SLEEP_PERIOD] = wakeup_period self.hass.config_entries.async_update_entry(self.entry, data=data) - update_interval = SLEEP_PERIOD_MULTIPLIER * wakeup_period + update_interval = UPDATE_PERIOD_MULTIPLIER * wakeup_period self.update_interval = timedelta(seconds=update_interval) return True diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 026a7041863..3bfbf350f7e 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -7,7 +7,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.shelly.const import SLEEP_PERIOD_MULTIPLIER +from homeassistant.components.shelly.const import UPDATE_PERIOD_MULTIPLIER from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State from homeassistant.helpers.device_registry import DeviceRegistry @@ -122,7 +122,7 @@ async def test_block_rest_binary_sensor_connected_battery_devices( assert hass.states.get(entity_id).state == STATE_OFF # Verify update on slow intervals - await mock_rest_update(hass, freezer, seconds=SLEEP_PERIOD_MULTIPLIER * 3600) + await mock_rest_update(hass, freezer, seconds=UPDATE_PERIOD_MULTIPLIER * 3600) assert hass.states.get(entity_id).state == STATE_ON entry = entity_registry.async_get(entity_id) diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 1e0af115c9e..35123a2db91 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -20,7 +20,6 @@ from homeassistant.components.shelly.const import ( ENTRY_RELOAD_COOLDOWN, MAX_PUSH_UPDATE_FAILURES, RPC_RECONNECT_INTERVAL, - SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, BLEScannerMode, ) @@ -564,7 +563,7 @@ async def test_rpc_update_entry_sleep_period( # Move time to generate sleep period update monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 3600) - freezer.tick(timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER)) + freezer.tick(timedelta(seconds=600 * UPDATE_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -596,7 +595,7 @@ async def test_rpc_sleeping_device_no_periodic_updates( assert get_entity_state(hass, entity_id) == "22.9" # Move time to generate polling - freezer.tick(timedelta(seconds=SLEEP_PERIOD_MULTIPLIER * 1000)) + freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 1000)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -889,7 +888,7 @@ async def test_block_sleeping_device_connection_error( assert get_entity_state(hass, entity_id) == STATE_ON # Move time to generate sleep period update - freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER)) + freezer.tick(timedelta(seconds=sleep_period * UPDATE_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -934,7 +933,7 @@ async def test_rpc_sleeping_device_connection_error( assert get_entity_state(hass, entity_id) == STATE_ON # Move time to generate sleep period update - freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER)) + freezer.tick(timedelta(seconds=sleep_period * UPDATE_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) From 6dd1e09354383cf19eb83946a9722f41b29ee5f5 Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 26 Jun 2024 21:45:17 +0200 Subject: [PATCH 0277/2411] Don't allow switch toggle when device in locked in AVM FRITZ!SmartHome (#120132) * fix: set state of the FritzBox-Switch to disabled if the option for manuel switching in the userinterface is disabled * feat: raise an error instead of disabling switch * feat: rename method signature * fix: tests * fix: wrong import * feat: Update homeassistant/components/fritzbox/strings.json Update error message Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * Update tests/components/fritzbox/test_switch.py feat: update test Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * make ruff happy * fix expected error message check --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- .../components/fritzbox/strings.json | 3 ++ homeassistant/components/fritzbox/switch.py | 12 ++++++++ tests/components/fritzbox/__init__.py | 2 +- tests/components/fritzbox/test_switch.py | 30 +++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index cee0afa26c1..d4f59fd1c08 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -81,6 +81,9 @@ } }, "exceptions": { + "manual_switching_disabled": { + "message": "Can't toggle switch while manual switching is disabled for the device." + }, "change_preset_while_active_mode": { "message": "Can't change preset while holiday or summer mode is active on the device." }, diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 0bdf7a9f944..d13f21e1c14 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -6,9 +6,11 @@ from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FritzBoxDeviceEntity +from .const import DOMAIN from .coordinator import FritzboxConfigEntry @@ -48,10 +50,20 @@ class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" + self.check_lock_state() await self.hass.async_add_executor_job(self.data.set_switch_state_on) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" + self.check_lock_state() await self.hass.async_add_executor_job(self.data.set_switch_state_off) await self.coordinator.async_refresh() + + def check_lock_state(self) -> None: + """Raise an Error if manual switching via FRITZ!Box user interface is disabled.""" + if self.data.lock: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="manual_switching_disabled", + ) diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 2bd8f26d73b..61312805e91 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -151,7 +151,7 @@ class FritzDeviceSwitchMock(FritzEntityBaseMock): has_thermostat = False has_blind = False switch_state = "fake_state" - lock = "fake_locked" + lock = False power = 5678 present = True temperature = 1.23 diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 417b355b396..ba3b1de9b2f 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import Mock +import pytest from requests.exceptions import HTTPError from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN @@ -29,6 +30,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util @@ -130,6 +132,7 @@ async def test_turn_on(hass: HomeAssistant, fritz: Mock) -> None: async def test_turn_off(hass: HomeAssistant, fritz: Mock) -> None: """Test turn device off.""" device = FritzDeviceSwitchMock() + assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -137,9 +140,36 @@ async def test_turn_off(hass: HomeAssistant, fritz: Mock) -> None: await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) + assert device.set_switch_state_off.call_count == 1 +async def test_toggle_while_locked(hass: HomeAssistant, fritz: Mock) -> None: + """Test toggling while device is locked.""" + device = FritzDeviceSwitchMock() + device.lock = True + + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + with pytest.raises( + HomeAssistantError, + match="Can't toggle switch while manual switching is disabled for the device", + ): + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + + with pytest.raises( + HomeAssistantError, + match="Can't toggle switch while manual switching is disabled for the device", + ): + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + + async def test_update(hass: HomeAssistant, fritz: Mock) -> None: """Test update without error.""" device = FritzDeviceSwitchMock() From 3d164c672181a68a7b4e01a300ac6b5db54e452a Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 26 Jun 2024 18:15:53 +0200 Subject: [PATCH 0278/2411] Bump ZHA dependencies (#120581) --- homeassistant/components/zha/manifest.json | 8 ++++---- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index f517742f16f..7087ff0b2f0 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -23,12 +23,12 @@ "requirements": [ "bellows==0.39.1", "pyserial==3.5", - "zha-quirks==0.0.116", - "zigpy-deconz==0.23.1", + "zha-quirks==0.0.117", + "zigpy-deconz==0.23.2", "zigpy==0.64.1", "zigpy-xbee==0.20.1", - "zigpy-zigate==0.12.0", - "zigpy-znp==0.12.1", + "zigpy-zigate==0.12.1", + "zigpy-znp==0.12.2", "universal-silabs-flasher==0.0.20", "pyserial-asyncio-fast==0.11" ], diff --git a/requirements_all.txt b/requirements_all.txt index a3a62b58b4b..3aaec74c36f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2966,7 +2966,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.116 +zha-quirks==0.0.117 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 @@ -2975,16 +2975,16 @@ zhong-hong-hvac==1.0.12 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.23.1 +zigpy-deconz==0.23.2 # homeassistant.components.zha zigpy-xbee==0.20.1 # homeassistant.components.zha -zigpy-zigate==0.12.0 +zigpy-zigate==0.12.1 # homeassistant.components.zha -zigpy-znp==0.12.1 +zigpy-znp==0.12.2 # homeassistant.components.zha zigpy==0.64.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f05bcc3d33..4b1777f4bae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2319,19 +2319,19 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.116 +zha-quirks==0.0.117 # homeassistant.components.zha -zigpy-deconz==0.23.1 +zigpy-deconz==0.23.2 # homeassistant.components.zha zigpy-xbee==0.20.1 # homeassistant.components.zha -zigpy-zigate==0.12.0 +zigpy-zigate==0.12.1 # homeassistant.components.zha -zigpy-znp==0.12.1 +zigpy-znp==0.12.2 # homeassistant.components.zha zigpy==0.64.1 From d3d0e05817938d17c9b7a6095d8043c77d26908c Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 26 Jun 2024 19:19:28 +0300 Subject: [PATCH 0279/2411] Change Shelly connect task log message level to error (#120582) --- homeassistant/components/shelly/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index a4ff34f7d9a..02feef3633b 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -167,7 +167,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( await self.device.initialize() update_device_fw_info(self.hass, self.device, self.entry) except DeviceConnectionError as err: - LOGGER.debug( + LOGGER.error( "Error connecting to Shelly device %s, error: %r", self.name, err ) return False From 1b45069620ed640cf13da880266e3fdff492cf50 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 26 Jun 2024 11:13:01 -0500 Subject: [PATCH 0280/2411] Bump intents to 2024.6.26 (#120584) Bump intents --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/conversation/snapshots/test_init.ambr | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index ee0b29f22fc..2302d03bf4c 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.21"] + "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.26"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 18461d6398b..e42ef84d34c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240626.0 -home-assistant-intents==2024.6.21 +home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index 3aaec74c36f..eca0100e5df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ holidays==0.51 home-assistant-frontend==20240626.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.21 +home-assistant-intents==2024.6.26 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b1777f4bae..bf8fd1dc081 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -898,7 +898,7 @@ holidays==0.51 home-assistant-frontend==20240626.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.21 +home-assistant-intents==2024.6.26 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 403c72aaa10..6264e61863f 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -563,7 +563,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -703,7 +703,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called late added', + 'speech': 'Sorry, I am not aware of any device called late added light', }), }), }), @@ -783,7 +783,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -803,7 +803,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called my cool', + 'speech': 'Sorry, I am not aware of any device called my cool light', }), }), }), @@ -943,7 +943,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen', + 'speech': 'Sorry, I am not aware of any device called kitchen light', }), }), }), @@ -993,7 +993,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called renamed', + 'speech': 'Sorry, I am not aware of any device called renamed light', }), }), }), From b35442ed2de4abfe49aea2a54bb3c151c2fec755 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Wed, 26 Jun 2024 21:46:59 +0200 Subject: [PATCH 0281/2411] Improve Bang & Olufsen error messages (#120587) * Convert logger messages to raised errors where applicable * Modify exception types * Improve deezer / tidal error message * Update homeassistant/components/bang_olufsen/strings.json Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * Update homeassistant/components/bang_olufsen/media_player.py Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- .../components/bang_olufsen/media_player.py | 41 ++++++++++++------- .../components/bang_olufsen/strings.json | 12 ++++++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index d23c75046ff..0eff9f2bb85 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -45,7 +45,7 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODEL from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -316,7 +316,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): @callback def _async_update_playback_error(self, data: PlaybackError) -> None: """Show playback error.""" - _LOGGER.error(data.error) + raise HomeAssistantError(data.error) @callback def _async_update_playback_progress(self, data: PlaybackProgress) -> None: @@ -516,7 +516,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): self.async_write_ha_state() else: - _LOGGER.error("Seeking is currently only supported when using Deezer") + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="non_deezer_seeking" + ) async def async_media_previous_track(self) -> None: """Send the previous track command.""" @@ -529,12 +531,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): async def async_select_source(self, source: str) -> None: """Select an input source.""" if source not in self._sources.values(): - _LOGGER.error( - "Invalid source: %s. Valid sources are: %s", - source, - list(self._sources.values()), + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_source", + translation_placeholders={ + "invalid_source": source, + "valid_sources": ",".join(list(self._sources.values())), + }, ) - return key = [x for x in self._sources if self._sources[x] == source][0] @@ -559,12 +563,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): media_type = MediaType.MUSIC if media_type not in VALID_MEDIA_TYPES: - _LOGGER.error( - "%s is an invalid type. Valid values are: %s", - media_type, - VALID_MEDIA_TYPES, + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_media_type", + translation_placeholders={ + "invalid_media_type": media_type, + "valid_media_types": ",".join(VALID_MEDIA_TYPES), + }, ) - return if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media( @@ -681,7 +687,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): ) except ApiException as error: - _LOGGER.error(json.loads(error.body)["message"]) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="play_media_error", + translation_placeholders={ + "media_type": media_type, + "error_message": json.loads(error.body)["message"], + }, + ) from error async def async_browse_media( self, diff --git a/homeassistant/components/bang_olufsen/strings.json b/homeassistant/components/bang_olufsen/strings.json index 93b55cf0db2..cf5b212d424 100644 --- a/homeassistant/components/bang_olufsen/strings.json +++ b/homeassistant/components/bang_olufsen/strings.json @@ -28,6 +28,18 @@ "exceptions": { "m3u_invalid_format": { "message": "Media sources with the .m3u extension are not supported." + }, + "non_deezer_seeking": { + "message": "Seeking is currently only supported when using Deezer" + }, + "invalid_source": { + "message": "Invalid source: {invalid_source}. Valid sources are: {valid_sources}" + }, + "invalid_media_type": { + "message": "{invalid_media_type} is an invalid type. Valid values are: {valid_media_types}." + }, + "play_media_error": { + "message": "An error occurred while attempting to play {media_type}: {error_message}." } } } From 2e01e169ef96ad5ea9844ae6de1f6f8505d65827 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 26 Jun 2024 20:55:25 +0200 Subject: [PATCH 0282/2411] Correct deprecation warning `async_register_static_paths` (#120592) --- homeassistant/components/http/__init__.py | 2 +- tests/components/http/test_init.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 38f0b628b2c..0d86ab57d3f 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -483,7 +483,7 @@ class HomeAssistantHTTP: frame.report( "calls hass.http.register_static_path which is deprecated because " "it does blocking I/O in the event loop, instead " - "call `await hass.http.async_register_static_path(" + "call `await hass.http.async_register_static_paths(" f'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`; ' "This function will be removed in 2025.7", exclude_integrations={"http"}, diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 7a9fb329fcd..2895209b5f9 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -543,5 +543,5 @@ async def test_register_static_paths( "Detected code that calls hass.http.register_static_path " "which is deprecated because it does blocking I/O in the " "event loop, instead call " - "`await hass.http.async_register_static_path" + "`await hass.http.async_register_static_paths" ) in caplog.text From 80e70993c8a5a4694e38486ab389aae62208103e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 26 Jun 2024 20:55:41 +0200 Subject: [PATCH 0283/2411] Remove deprecated run_immediately flag from integration sensor (#120593) --- homeassistant/components/integration/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 60cbee5549f..4fca92e9b40 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -446,7 +446,6 @@ class IntegrationSensor(RestoreSensor): event_filter=callback( lambda event_data: event_data["entity_id"] == self._sensor_source_id ), - run_immediately=True, ) ) self.async_on_remove( @@ -456,7 +455,6 @@ class IntegrationSensor(RestoreSensor): event_filter=callback( lambda event_data: event_data["entity_id"] == self._sensor_source_id ), - run_immediately=True, ) ) From b5c34808e6893646593f6e9deb94f43257229815 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 26 Jun 2024 21:35:23 +0300 Subject: [PATCH 0284/2411] Add last_error reporting to Shelly diagnostics (#120595) --- homeassistant/components/shelly/diagnostics.py | 10 ++++++++++ tests/components/shelly/test_diagnostics.py | 13 ++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/diagnostics.py b/homeassistant/components/shelly/diagnostics.py index db69abc8f55..e70b76a7c00 100644 --- a/homeassistant/components/shelly/diagnostics.py +++ b/homeassistant/components/shelly/diagnostics.py @@ -24,6 +24,8 @@ async def async_get_config_entry_diagnostics( device_settings: str | dict = "not initialized" device_status: str | dict = "not initialized" bluetooth: str | dict = "not initialized" + last_error: str = "not initialized" + if shelly_entry_data.block: block_coordinator = shelly_entry_data.block assert block_coordinator @@ -55,6 +57,10 @@ async def async_get_config_entry_diagnostics( "uptime", ] } + + if block_coordinator.device.last_error: + last_error = repr(block_coordinator.device.last_error) + else: rpc_coordinator = shelly_entry_data.rpc assert rpc_coordinator @@ -79,6 +85,9 @@ async def async_get_config_entry_diagnostics( "scanner": await scanner.async_diagnostics(), } + if rpc_coordinator.device.last_error: + last_error = repr(rpc_coordinator.device.last_error) + if isinstance(device_status, dict): device_status = async_redact_data(device_status, ["ssid"]) @@ -87,5 +96,6 @@ async def async_get_config_entry_diagnostics( "device_info": device_info, "device_settings": device_settings, "device_status": device_status, + "last_error": last_error, "bluetooth": bluetooth, } diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py index f7f238f3327..4fc8ea6ca8f 100644 --- a/tests/components/shelly/test_diagnostics.py +++ b/tests/components/shelly/test_diagnostics.py @@ -1,9 +1,10 @@ """Tests for Shelly diagnostics platform.""" -from unittest.mock import ANY, Mock +from unittest.mock import ANY, Mock, PropertyMock from aioshelly.ble.const import BLE_SCAN_RESULT_EVENT from aioshelly.const import MODEL_25 +from aioshelly.exceptions import DeviceConnectionError import pytest from homeassistant.components.diagnostics import REDACTED @@ -36,6 +37,10 @@ async def test_block_config_entry_diagnostics( {key: REDACTED for key in TO_REDACT if key in entry_dict["data"]} ) + type(mock_block_device).last_error = PropertyMock( + return_value=DeviceConnectionError() + ) + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert result == { @@ -48,6 +53,7 @@ async def test_block_config_entry_diagnostics( }, "device_settings": {"coiot": {"update_period": 15}}, "device_status": MOCK_STATUS_COAP, + "last_error": "DeviceConnectionError()", } @@ -91,6 +97,10 @@ async def test_rpc_config_entry_diagnostics( {key: REDACTED for key in TO_REDACT if key in entry_dict["data"]} ) + type(mock_rpc_device).last_error = PropertyMock( + return_value=DeviceConnectionError() + ) + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert result == { @@ -152,4 +162,5 @@ async def test_rpc_config_entry_diagnostics( }, "wifi": {"rssi": -63}, }, + "last_error": "DeviceConnectionError()", } From da01635a075a2264f92afc2ba55a9c39b10fdcb9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 20:57:27 +0200 Subject: [PATCH 0285/2411] Prevent changes to mutable bmw_connected_drive fixture data (#120600) --- .../bmw_connected_drive/test_config_flow.py | 7 ++++--- tests/components/bmw_connected_drive/test_init.py | 14 +++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index b562e2b898f..3c7f452a011 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -92,7 +92,7 @@ async def test_api_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=FIXTURE_USER_INPUT, + data=deepcopy(FIXTURE_USER_INPUT), ) assert result["type"] is FlowResultType.FORM @@ -116,7 +116,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=FIXTURE_USER_INPUT, + data=deepcopy(FIXTURE_USER_INPUT), ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] @@ -137,7 +137,8 @@ async def test_options_flow_implementation(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry, ): - config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + config_entry_args = deepcopy(FIXTURE_CONFIG_ENTRY) + config_entry = MockConfigEntry(**config_entry_args) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/bmw_connected_drive/test_init.py b/tests/components/bmw_connected_drive/test_init.py index 52bc8a7ce05..5cd6362d6fa 100644 --- a/tests/components/bmw_connected_drive/test_init.py +++ b/tests/components/bmw_connected_drive/test_init.py @@ -1,5 +1,6 @@ """Test Axis component setup process.""" +from copy import deepcopy from unittest.mock import patch import pytest @@ -37,7 +38,7 @@ async def test_migrate_options( ) -> None: """Test successful migration of options.""" - config_entry = FIXTURE_CONFIG_ENTRY.copy() + config_entry = deepcopy(FIXTURE_CONFIG_ENTRY) config_entry["options"] = options mock_config_entry = MockConfigEntry(**config_entry) @@ -55,7 +56,7 @@ async def test_migrate_options( async def test_migrate_options_from_data(hass: HomeAssistant) -> None: """Test successful migration of options.""" - config_entry = FIXTURE_CONFIG_ENTRY.copy() + config_entry = deepcopy(FIXTURE_CONFIG_ENTRY) config_entry["options"] = {} config_entry["data"].update({CONF_READ_ONLY: False}) @@ -107,7 +108,8 @@ async def test_migrate_unique_ids( entity_registry: er.EntityRegistry, ) -> None: """Test successful migration of entity unique_ids.""" - mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + confg_entry = deepcopy(FIXTURE_CONFIG_ENTRY) + mock_config_entry = MockConfigEntry(**confg_entry) mock_config_entry.add_to_hass(hass) entity: er.RegistryEntry = entity_registry.async_get_or_create( @@ -153,7 +155,8 @@ async def test_dont_migrate_unique_ids( entity_registry: er.EntityRegistry, ) -> None: """Test successful migration of entity unique_ids.""" - mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + confg_entry = deepcopy(FIXTURE_CONFIG_ENTRY) + mock_config_entry = MockConfigEntry(**confg_entry) mock_config_entry.add_to_hass(hass) # create existing entry with new_unique_id @@ -196,7 +199,8 @@ async def test_remove_stale_devices( device_registry: dr.DeviceRegistry, ) -> None: """Test remove stale device registry entries.""" - mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + config_entry = deepcopy(FIXTURE_CONFIG_ENTRY) + mock_config_entry = MockConfigEntry(**config_entry) mock_config_entry.add_to_hass(hass) device_registry.async_get_or_create( From 74204e2ee6be38b6a51f6c3fdd4a03f6411d9228 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 21:30:30 +0200 Subject: [PATCH 0286/2411] Fix mqtt test fixture usage (#120602) --- tests/components/mqtt/test_config_flow.py | 2 +- tests/components/mqtt/test_image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 57975fdc309..457bd19c16f 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1561,7 +1561,7 @@ async def test_setup_with_advanced_settings( } -@pytest.mark.usesfixtures("mock_ssl_context", "mock_process_uploaded_file") +@pytest.mark.usefixtures("mock_ssl_context", "mock_process_uploaded_file") async def test_change_websockets_transport_to_tcp( hass: HomeAssistant, mock_try_connection: MagicMock ) -> None: diff --git a/tests/components/mqtt/test_image.py b/tests/components/mqtt/test_image.py index 29109ee12f4..bb029fba231 100644 --- a/tests/components/mqtt/test_image.py +++ b/tests/components/mqtt/test_image.py @@ -499,7 +499,7 @@ async def test_image_from_url_fails( ), ], ) -@pytest.mark.usesfixtures("hass", "hass_client_no_auth") +@pytest.mark.usefixtures("hass", "hass_client_no_auth") async def test_image_config_fails( mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, From 242b3fa6099a23bbd409c987d95745ee8a9ab286 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jun 2024 22:05:30 +0200 Subject: [PATCH 0287/2411] Update adguardhome to 0.7.0 (#120605) --- homeassistant/components/adguard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index 52add51a663..f1b82177d5b 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "local_polling", "loggers": ["adguardhome"], - "requirements": ["adguardhome==0.6.3"] + "requirements": ["adguardhome==0.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index eca0100e5df..4e7d43ccdd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -149,7 +149,7 @@ adb-shell[async]==0.4.4 adext==0.4.3 # homeassistant.components.adguard -adguardhome==0.6.3 +adguardhome==0.7.0 # homeassistant.components.advantage_air advantage-air==0.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf8fd1dc081..2f0a793cc28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -128,7 +128,7 @@ adb-shell[async]==0.4.4 adext==0.4.3 # homeassistant.components.adguard -adguardhome==0.6.3 +adguardhome==0.7.0 # homeassistant.components.advantage_air advantage-air==0.4.4 From 7d5d81b2298c394f946313e2082a51c60327ec4f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jun 2024 22:51:27 +0200 Subject: [PATCH 0288/2411] Bump version to 2024.7.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 54d7f26a5f0..fe0989a54f8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 0b490d621a3..ea264a29fc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b0" +version = "2024.7.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 1973c604b61ecae14d91d5dbacde4d107b381836 Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Wed, 26 Jun 2024 23:45:47 +0200 Subject: [PATCH 0289/2411] Fix telegram bot thread_id key error (#120613) --- homeassistant/components/telegram_bot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index f37a84a83a6..fed9021a46e 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -702,7 +702,7 @@ class TelegramNotificationService: } if message_tag is not None: event_data[ATTR_MESSAGE_TAG] = message_tag - if kwargs_msg[ATTR_MESSAGE_THREAD_ID] is not None: + if kwargs_msg.get(ATTR_MESSAGE_THREAD_ID) is not None: event_data[ATTR_MESSAGE_THREAD_ID] = kwargs_msg[ ATTR_MESSAGE_THREAD_ID ] From 9d7078e1fa94326b9552ddd5b7e2bd64e003dbc4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 23:52:31 +0200 Subject: [PATCH 0290/2411] Install libturbojpeg in hassfest job [ci] (#120611) --- .github/workflows/ci.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 49f58403aab..64fe949ecc2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -522,6 +522,12 @@ jobs: - info - base steps: + - name: Install additional OS dependencies + run: | + sudo rm /etc/apt/sources.list.d/microsoft-prod.list + sudo apt-get update + sudo apt-get -y install \ + libturbojpeg - name: Check out code from GitHub uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} From b7a65d9a82b4e8fb8235c908a522a17748386dc7 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 26 Jun 2024 23:54:07 +0200 Subject: [PATCH 0291/2411] Update frontend to 20240626.2 (#120614) --- 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 063f7db34a0..89c8fbe30ca 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240626.0"] + "requirements": ["home-assistant-frontend==20240626.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e42ef84d34c..174de784eba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240626.0 +home-assistant-frontend==20240626.2 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4e7d43ccdd2..67ad67799e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.0 +home-assistant-frontend==20240626.2 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f0a793cc28..350b59c0eab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.0 +home-assistant-frontend==20240626.2 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From bea6fe30b86cd526bd12e159839c7e2535b996c3 Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Wed, 26 Jun 2024 23:45:47 +0200 Subject: [PATCH 0292/2411] Fix telegram bot thread_id key error (#120613) --- homeassistant/components/telegram_bot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index f37a84a83a6..fed9021a46e 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -702,7 +702,7 @@ class TelegramNotificationService: } if message_tag is not None: event_data[ATTR_MESSAGE_TAG] = message_tag - if kwargs_msg[ATTR_MESSAGE_THREAD_ID] is not None: + if kwargs_msg.get(ATTR_MESSAGE_THREAD_ID) is not None: event_data[ATTR_MESSAGE_THREAD_ID] = kwargs_msg[ ATTR_MESSAGE_THREAD_ID ] From 0701b0daa93b400a377f4d8c13f513eb25621fa3 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 26 Jun 2024 23:54:07 +0200 Subject: [PATCH 0293/2411] Update frontend to 20240626.2 (#120614) --- 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 063f7db34a0..89c8fbe30ca 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240626.0"] + "requirements": ["home-assistant-frontend==20240626.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e42ef84d34c..174de784eba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240626.0 +home-assistant-frontend==20240626.2 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4e7d43ccdd2..67ad67799e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.0 +home-assistant-frontend==20240626.2 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f0a793cc28..350b59c0eab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.0 +home-assistant-frontend==20240626.2 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From 3da8d0a741d26cb9d38d1602bb9c6427e023a8f7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jun 2024 23:55:20 +0200 Subject: [PATCH 0294/2411] Bump version to 2024.7.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index fe0989a54f8..8291fb93fd7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index ea264a29fc6..709022534b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b1" +version = "2024.7.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 32e64f79110d2f00a58f88838c6a63eec90e6b33 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 23:57:41 +0200 Subject: [PATCH 0295/2411] Cleanup mqtt platform tests part 4 (init) (#120574) --- tests/components/mqtt/test_init.py | 91 ++++++++---------------------- 1 file changed, 23 insertions(+), 68 deletions(-) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 231379601c6..bcadf4a6506 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -5,7 +5,6 @@ from copy import deepcopy from datetime import datetime, timedelta from functools import partial import json -import logging import socket import ssl import time @@ -16,15 +15,11 @@ import certifi from freezegun.api import FrozenDateTimeFactory import paho.mqtt.client as paho_mqtt import pytest -from typing_extensions import Generator import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info -from homeassistant.components.mqtt.client import ( - _LOGGER as CLIENT_LOGGER, - RECONNECT_INTERVAL_SECONDS, -) +from homeassistant.components.mqtt.client import RECONNECT_INTERVAL_SECONDS from homeassistant.components.mqtt.models import ( MessageCallbackType, MqttCommandTemplateException, @@ -100,15 +95,6 @@ def mock_storage(hass_storage: dict[str, Any]) -> None: """Autouse hass_storage for the TestCase tests.""" -@pytest.fixture -def client_debug_log() -> Generator[None]: - """Set the mqtt client log level to DEBUG.""" - logger = logging.getLogger("mqtt_client_tests_debug") - logger.setLevel(logging.DEBUG) - with patch.object(CLIENT_LOGGER, "parent", logger): - yield - - def help_assert_message( msg: ReceiveMessage, topic: str | None = None, @@ -130,8 +116,7 @@ def help_assert_message( async def test_mqtt_connects_on_home_assistant_mqtt_setup( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient ) -> None: """Test if client is connected after mqtt init on bootstrap.""" mqtt_client_mock = setup_with_birth_msg_client_mock @@ -150,9 +135,7 @@ async def test_mqtt_does_not_disconnect_on_home_assistant_stop( assert mqtt_client_mock.disconnect.call_count == 0 -async def test_mqtt_await_ack_at_disconnect( - hass: HomeAssistant, -) -> None: +async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: """Test if ACK is awaited correctly when disconnecting.""" class FakeInfo: @@ -208,8 +191,7 @@ async def test_mqtt_await_ack_at_disconnect( @pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) async def test_publish( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient ) -> None: """Test the publish function.""" publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish @@ -340,9 +322,7 @@ async def test_command_template_value(hass: HomeAssistant) -> None: ], ) async def test_command_template_variables( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - config: ConfigType, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, config: ConfigType ) -> None: """Test the rendering of entity variables.""" topic = "test/select" @@ -888,7 +868,7 @@ def test_entity_device_info_schema() -> None: {"identifiers": [], "connections": [], "name": "Beer"} ) - # not an valid URL + # not a valid URL with pytest.raises(vol.Invalid): MQTT_ENTITY_DEVICE_INFO_SCHEMA( { @@ -1049,10 +1029,9 @@ async def test_subscribe_topic( unsub() +@pytest.mark.usefixtures("mqtt_mock_entry") async def test_subscribe_topic_not_initialize( - hass: HomeAssistant, - record_calls: MessageCallbackType, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, record_calls: MessageCallbackType ) -> None: """Test the subscription of a topic when MQTT was not initialized.""" with pytest.raises( @@ -1084,7 +1063,6 @@ async def test_subscribe_mqtt_config_entry_disabled( async def test_subscribe_and_resubscribe( hass: HomeAssistant, - client_debug_log: None, mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], @@ -1892,10 +1870,10 @@ async def test_subscribed_at_highest_qos( assert help_all_subscribe_calls(mqtt_client_mock) == [("test/state", 2)] +@pytest.mark.usefixtures("mqtt_client_mock") async def test_reload_entry_with_restored_subscriptions( hass: HomeAssistant, mock_debouncer: asyncio.Event, - mqtt_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, recorded_calls: list[ReceiveMessage], ) -> None: @@ -1995,7 +1973,6 @@ async def test_logs_error_if_no_connect_broker( @pytest.mark.parametrize("return_code", [4, 5]) async def test_triggers_reauth_flow_if_auth_fails( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, setup_with_birth_msg_client_mock: MqttMockPahoClient, return_code: int, ) -> None: @@ -2132,9 +2109,7 @@ async def test_handle_message_callback( ], ) async def test_setup_manual_mqtt_with_platform_key( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test set up a manual MQTT item with a platform key.""" assert await mqtt_mock_entry() @@ -2146,9 +2121,7 @@ async def test_setup_manual_mqtt_with_platform_key( @pytest.mark.parametrize("hass_config", [{mqtt.DOMAIN: {"light": {"name": "test"}}}]) async def test_setup_manual_mqtt_with_invalid_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test set up a manual MQTT item with an invalid config.""" assert await mqtt_mock_entry() @@ -2182,9 +2155,7 @@ async def test_setup_manual_mqtt_with_invalid_config( ], ) async def test_setup_mqtt_client_protocol( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - protocol: int, + mqtt_mock_entry: MqttMockHAClientGenerator, protocol: int ) -> None: """Test MQTT client protocol setup.""" with patch( @@ -2383,8 +2354,7 @@ async def test_custom_birth_message( [ENTRY_DEFAULT_BIRTH_MESSAGE], ) async def test_default_birth_message( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient ) -> None: """Test sending birth message.""" mqtt_client_mock = setup_with_birth_msg_client_mock @@ -2470,10 +2440,7 @@ async def test_delayed_birth_message( [ENTRY_DEFAULT_BIRTH_MESSAGE], ) async def test_subscription_done_when_birth_message_is_sent( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, ) -> None: """Test sending birth message until initial subscription has been completed.""" mqtt_client_mock = setup_with_birth_msg_client_mock @@ -2517,7 +2484,6 @@ async def test_custom_will_message( async def test_default_will_message( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test will message.""" @@ -2647,11 +2613,9 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( assert len(mqtt_client_mock.unsubscribe.mock_calls[1][1][0]) == 2 +@pytest.mark.usefixtures("mqtt_client_mock") async def test_default_entry_setting_are_applied( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mqtt_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Test if the MQTT component loads when config entry data not has all default settings.""" data = ( @@ -2704,11 +2668,9 @@ async def test_message_callback_exception_gets_logged( @pytest.mark.no_fail_on_log_exception +@pytest.mark.usefixtures("mock_debouncer", "setup_with_birth_msg_client_mock") async def test_message_partial_callback_exception_gets_logged( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_debouncer: asyncio.Event ) -> None: """Test exception raised by message handler.""" @@ -3730,9 +3692,7 @@ async def test_setup_manual_items_with_unique_ids( ], ) async def test_link_config_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test manual and dynamically setup entities are linked to the config entry.""" # set up manual item @@ -3818,9 +3778,7 @@ async def test_link_config_entry( ], ) async def test_reload_config_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test manual entities reloaded and set up correctly.""" await mqtt_mock_entry() @@ -3966,8 +3924,7 @@ async def test_reload_config_entry( ], ) async def test_reload_with_invalid_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test reloading yaml config fails.""" await mqtt_mock_entry() @@ -4007,8 +3964,7 @@ async def test_reload_with_invalid_config( ], ) async def test_reload_with_empty_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test reloading yaml config fails.""" await mqtt_mock_entry() @@ -4043,8 +3999,7 @@ async def test_reload_with_empty_config( ], ) async def test_reload_with_new_platform_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test reloading yaml with new platform config.""" await mqtt_mock_entry() @@ -4389,6 +4344,6 @@ async def test_loop_write_failure( "valid_subscribe_topic", ], ) -async def test_mqtt_integration_level_imports(hass: HomeAssistant, attr: str) -> None: +async def test_mqtt_integration_level_imports(attr: str) -> None: """Test mqtt integration level public published imports are available.""" assert hasattr(mqtt, attr) From 2449e2029e716cb73436287b21d1d34240542b6c Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 26 Jun 2024 19:14:18 -0400 Subject: [PATCH 0296/2411] Bump anova_wifi to 0.14.0 (#120616) --- homeassistant/components/anova/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/anova/manifest.json b/homeassistant/components/anova/manifest.json index 331a4f61118..d75a791a6f5 100644 --- a/homeassistant/components/anova/manifest.json +++ b/homeassistant/components/anova/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/anova", "iot_class": "cloud_push", "loggers": ["anova_wifi"], - "requirements": ["anova-wifi==0.12.0"] + "requirements": ["anova-wifi==0.14.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 67ad67799e9..a3aa0bbacc3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ androidtvremote2==0.1.1 anel-pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.anova -anova-wifi==0.12.0 +anova-wifi==0.14.0 # homeassistant.components.anthemav anthemav==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 350b59c0eab..9b1d4743c9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -413,7 +413,7 @@ androidtv[async]==0.0.73 androidtvremote2==0.1.1 # homeassistant.components.anova -anova-wifi==0.12.0 +anova-wifi==0.14.0 # homeassistant.components.anthemav anthemav==1.4.1 From 5e1c8b0c54dc1a0c72773a0317830b0d1d0e4289 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 20:37:08 -0500 Subject: [PATCH 0297/2411] Remove unused fields from unifiprotect event sensors (#120568) --- homeassistant/components/unifiprotect/binary_sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index e35eb6f48f3..c4e1aa87df2 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -426,14 +426,12 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.OCCUPANCY, icon="mdi:doorbell-video", ufp_required_field="feature_flags.is_doorbell", - ufp_value="is_ringing", ufp_event_obj="last_ring_event", ), ProtectBinaryEventEntityDescription( key="motion", name="Motion", device_class=BinarySensorDeviceClass.MOTION, - ufp_value="is_motion_currently_detected", ufp_enabled="is_motion_detection_on", ufp_event_obj="last_motion_event", ), From f189d87fe91be7810812c915c07269b0075f14a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 20:37:28 -0500 Subject: [PATCH 0298/2411] Bump uiprotect to 4.0.0 (#120617) --- 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 8e29f5ffb9f..bdbdacae90e 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==3.7.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==4.0.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index a3aa0bbacc3..44bc9f73b1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2789,7 +2789,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.7.0 +uiprotect==4.0.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b1d4743c9b..45cb1087cb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2169,7 +2169,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.7.0 +uiprotect==4.0.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From a5a631148ec81c1ca1fb0ec9acb4d78670da908b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 22:04:27 -0500 Subject: [PATCH 0299/2411] Add async_track_state_reported_event to fix integration performance regression (#120622) split from https://github.com/home-assistant/core/pull/120621 --- homeassistant/helpers/event.py | 37 ++++++++++++++++++++++++++++------ tests/helpers/test_event.py | 32 ++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 4150d871b6b..ebd51948e3b 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any, Concatenate, Generic, TypeVar from homeassistant.const import ( EVENT_CORE_CONFIG_UPDATE, EVENT_STATE_CHANGED, + EVENT_STATE_REPORTED, MATCH_ALL, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, @@ -26,6 +27,7 @@ from homeassistant.core import ( Event, # Explicit reexport of 'EventStateChangedData' for backwards compatibility EventStateChangedData as EventStateChangedData, # noqa: PLC0414 + EventStateReportedData, HassJob, HassJobType, HomeAssistant, @@ -57,6 +59,9 @@ from .typing import TemplateVarsType _TRACK_STATE_CHANGE_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = HassKey( "track_state_change_data" ) +_TRACK_STATE_REPORTED_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey( + "track_state_reported_data" +) _TRACK_STATE_ADDED_DOMAIN_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = ( HassKey("track_state_added_domain_data") ) @@ -324,8 +329,8 @@ def async_track_state_change_event( @callback def _async_dispatch_entity_id_event( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]], - event: Event[EventStateChangedData], + callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], + event: Event[_TypedDictT], ) -> None: """Dispatch to listeners.""" if not (callbacks_list := callbacks.get(event.data["entity_id"])): @@ -342,10 +347,10 @@ def _async_dispatch_entity_id_event( @callback -def _async_state_change_filter( +def _async_state_filter( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]], - event_data: EventStateChangedData, + callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], + event_data: _TypedDictT, ) -> bool: """Filter state changes by entity_id.""" return event_data["entity_id"] in callbacks @@ -355,7 +360,7 @@ _KEYED_TRACK_STATE_CHANGE = _KeyedEventTracker( key=_TRACK_STATE_CHANGE_DATA, event_type=EVENT_STATE_CHANGED, dispatcher_callable=_async_dispatch_entity_id_event, - filter_callable=_async_state_change_filter, + filter_callable=_async_state_filter, ) @@ -372,6 +377,26 @@ def _async_track_state_change_event( ) +_KEYED_TRACK_STATE_REPORTED = _KeyedEventTracker( + key=_TRACK_STATE_REPORTED_DATA, + event_type=EVENT_STATE_REPORTED, + dispatcher_callable=_async_dispatch_entity_id_event, + filter_callable=_async_state_filter, +) + + +def async_track_state_reported_event( + hass: HomeAssistant, + entity_ids: str | Iterable[str], + action: Callable[[Event[EventStateReportedData]], Any], + job_type: HassJobType | None = None, +) -> CALLBACK_TYPE: + """Track EVENT_STATE_REPORTED by entity_id without lowercasing.""" + return _async_track_event( + _KEYED_TRACK_STATE_REPORTED, hass, entity_ids, action, job_type + ) + + @callback def _remove_empty_listener() -> None: """Remove a listener that does nothing.""" diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index edce36218e8..4f983120e36 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -15,7 +15,13 @@ import pytest from homeassistant.const import MATCH_ALL import homeassistant.core as ha -from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + EventStateReportedData, + HomeAssistant, + callback, +) from homeassistant.exceptions import TemplateError from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED @@ -34,6 +40,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_state_change_filtered, async_track_state_removed_domain, + async_track_state_reported_event, async_track_sunrise, async_track_sunset, async_track_template, @@ -4907,3 +4914,26 @@ async def test_track_point_in_time_repr( assert "Exception in callback _TrackPointUTCTime" in caplog.text assert "._raise_exception" in caplog.text await hass.async_block_till_done(wait_background_tasks=True) + + +async def test_async_track_state_reported_event(hass: HomeAssistant) -> None: + """Test async_track_state_reported_event.""" + tracker_called: list[ha.State] = [] + + @ha.callback + def single_run_callback(event: Event[EventStateReportedData]) -> None: + new_state = event.data["new_state"] + tracker_called.append(new_state) + + unsub = async_track_state_reported_event( + hass, ["light.bowl", "light.top"], single_run_callback + ) + hass.states.async_set("light.bowl", "on") + hass.states.async_set("light.top", "on") + await hass.async_block_till_done() + assert len(tracker_called) == 0 + hass.states.async_set("light.bowl", "on") + hass.states.async_set("light.top", "on") + await hass.async_block_till_done() + assert len(tracker_called) == 2 + unsub() From 28e72753ad251ddf8bab549179625a15a26fa8f5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 27 Jun 2024 06:31:22 +0200 Subject: [PATCH 0300/2411] Prevent importing PLATFORM_SCHEMA/_BASE from config validation (#120571) --- pylint/plugins/hass_imports.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index b4d30be483d..3ec8b6c3cd9 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -360,6 +360,12 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { constant=re.compile(r"^RESULT_TYPE_(\w*)$"), ), ], + "homeassistant.helpers.config_validation": [ + ObsoleteImportMatch( + reason="should be imported from homeassistant/components/", + constant=re.compile(r"^PLATFORM_SCHEMA(_BASE)?$"), + ), + ], "homeassistant.helpers.device_registry": [ ObsoleteImportMatch( reason="replaced by DeviceEntryDisabler enum", From 617ab48fa93f204cabee8e80ded28ef4163a18b7 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Thu, 27 Jun 2024 03:50:20 -0300 Subject: [PATCH 0301/2411] Address device helper review comments (#120615) * Address review comments from #119761 * Address review comments from #119761 * Address review comments from #119761 * Remove reference from config entry --- homeassistant/helpers/device.py | 21 ++++++++++++++++----- tests/helpers/test_device.py | 4 ++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/device.py b/homeassistant/helpers/device.py index e1b9ded5723..16212422236 100644 --- a/homeassistant/helpers/device.py +++ b/homeassistant/helpers/device.py @@ -26,7 +26,10 @@ def async_device_info_to_link_from_entity( hass: HomeAssistant, entity_id_or_uuid: str, ) -> dr.DeviceInfo | None: - """DeviceInfo with information to link a device to a configuration entry in the link category from a entity id or entity uuid.""" + """DeviceInfo with information to link a device from an entity. + + DeviceInfo will only return information to categorize as a link. + """ return async_device_info_to_link_from_device_id( hass, @@ -39,7 +42,10 @@ def async_device_info_to_link_from_device_id( hass: HomeAssistant, device_id: str | None, ) -> dr.DeviceInfo | None: - """DeviceInfo with information to link a device to a configuration entry in the link category from a device id.""" + """DeviceInfo with information to link a device from a device id. + + DeviceInfo will only return information to categorize as a link. + """ dev_reg = dr.async_get(hass) @@ -58,7 +64,11 @@ def async_remove_stale_devices_links_keep_entity_device( entry_id: str, source_entity_id_or_uuid: str, ) -> None: - """Remove the link between stales devices and a configuration entry, keeping only the device that the informed entity is linked to.""" + """Remove the link between stale devices and a configuration entry. + + Only the device passed in the source_entity_id_or_uuid parameter + linked to the configuration entry will be maintained. + """ async_remove_stale_devices_links_keep_current_device( hass=hass, @@ -73,9 +83,10 @@ def async_remove_stale_devices_links_keep_current_device( entry_id: str, current_device_id: str | None, ) -> None: - """Remove the link between stales devices and a configuration entry, keeping only the device informed. + """Remove the link between stale devices and a configuration entry. - Device passed in the current_device_id parameter will be kept linked to the configuration entry. + Only the device passed in the current_device_id parameter linked to + the configuration entry will be maintained. """ dev_reg = dr.async_get(hass) diff --git a/tests/helpers/test_device.py b/tests/helpers/test_device.py index 72c602bec48..852d418da23 100644 --- a/tests/helpers/test_device.py +++ b/tests/helpers/test_device.py @@ -169,7 +169,7 @@ async def test_remove_stale_device_links_keep_entity_device( config_entry.entry_id ) - # After cleanup, only one device is expected to be linked to the configuration entry if at least source_entity_id_or_uuid or device_id was given, else zero + # After cleanup, only one device is expected to be linked to the config entry assert len(devices_config_entry) == 1 assert current_device in devices_config_entry @@ -220,7 +220,7 @@ async def test_remove_stale_devices_links_keep_current_device( config_entry.entry_id ) - # After cleanup, only one device is expected to be linked to the configuration entry + # After cleanup, only one device is expected to be linked to the config entry assert len(devices_config_entry) == 1 assert current_device in devices_config_entry From 5503379a3b0597a0523dc83c1ad0a96fe0183263 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 01:50:41 -0500 Subject: [PATCH 0302/2411] Fix performance regression in integration from state_reported (#120621) * Fix performance regression in integration from state_reported Because the callbacks were no longer indexed by entity id, users saw upwards of 1M calls/min https://github.com/home-assistant/core/pull/113869/files#r1655580523 * Update homeassistant/helpers/event.py * coverage --------- Co-authored-by: Paulus Schoutsen --- .../components/integration/sensor.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 4fca92e9b40..8cc5341f081 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -27,8 +27,6 @@ from homeassistant.const import ( CONF_METHOD, CONF_NAME, CONF_UNIQUE_ID, - EVENT_STATE_CHANGED, - EVENT_STATE_REPORTED, STATE_UNAVAILABLE, UnitOfTime, ) @@ -45,7 +43,11 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.event import ( + async_call_later, + async_track_state_change_event, + async_track_state_reported_event, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( @@ -440,21 +442,17 @@ class IntegrationSensor(RestoreSensor): self._derive_and_set_attributes_from_state(state) self.async_on_remove( - self.hass.bus.async_listen( - EVENT_STATE_CHANGED, + async_track_state_change_event( + self.hass, + self._sensor_source_id, handle_state_change, - event_filter=callback( - lambda event_data: event_data["entity_id"] == self._sensor_source_id - ), ) ) self.async_on_remove( - self.hass.bus.async_listen( - EVENT_STATE_REPORTED, + async_track_state_reported_event( + self.hass, + self._sensor_source_id, handle_state_report, - event_filter=callback( - lambda event_data: event_data["entity_id"] == self._sensor_source_id - ), ) ) From c9c573dbcec34e6852ede95820c369685d0781d3 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:21:41 +0200 Subject: [PATCH 0303/2411] Fix the version that raises the issue (#120638) --- homeassistant/components/lamarzocco/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 9c66fdd1b60..dfcaa54047d 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - entry.runtime_data = coordinator gateway_version = coordinator.device.firmware[FirmwareType.GATEWAY].current_version - if version.parse(gateway_version) < version.parse("v3.5-rc5"): + if version.parse(gateway_version) < version.parse("v3.4-rc5"): # incompatible gateway firmware, create an issue ir.async_create_issue( hass, From 3e9b57cc07c81bec24530aab4dfc3792ddc9d289 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 09:26:31 +0200 Subject: [PATCH 0304/2411] Don't allow updating a device to have no connections or identifiers (#120603) * Don't allow updating a device to have no connections or identifiers * Move check to the top of the function --- homeassistant/helpers/device_registry.py | 5 +++++ tests/helpers/test_device_registry.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index cfafa63ec3a..4579739f0e1 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -869,6 +869,11 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): ) add_config_entry = config_entry + if not new_connections and not new_identifiers: + raise HomeAssistantError( + "A device must have at least one of identifiers or connections" + ) + if merge_connections is not UNDEFINED and new_connections is not UNDEFINED: raise HomeAssistantError( "Cannot define both merge_connections and new_connections" diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index fa57cc7557e..3a525f00870 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -3052,3 +3052,22 @@ async def test_primary_config_entry( model="model", ) assert device.primary_config_entry == mock_config_entry_1.entry_id + + +async def test_update_device_no_connections_or_identifiers( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, +) -> None: + """Test updating a device clearing connections and identifiers.""" + mock_config_entry = MockConfigEntry(domain="mqtt", title=None) + mock_config_entry.add_to_hass(hass) + + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + ) + with pytest.raises(HomeAssistantError): + device_registry.async_update_device( + device.id, new_connections=set(), new_identifiers=set() + ) From 5634741ce2d59034df31e7dc82f3852ff1a45d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 27 Jun 2024 10:27:20 +0200 Subject: [PATCH 0305/2411] Bump awesomeversion from 24.2.0 to 24.6.0 (#120642) --- 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 174de784eba..9aed0850478 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ async-interrupt==1.1.2 async-upnp-client==0.39.0 atomicwrites-homeassistant==1.4.1 attrs==23.2.0 -awesomeversion==24.2.0 +awesomeversion==24.6.0 bcrypt==4.1.2 bleak-retry-connector==3.5.0 bleak==0.22.2 diff --git a/pyproject.toml b/pyproject.toml index 45c60684ebf..f81013aa8b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "async-interrupt==1.1.2", "attrs==23.2.0", "atomicwrites-homeassistant==1.4.1", - "awesomeversion==24.2.0", + "awesomeversion==24.6.0", "bcrypt==4.1.2", "certifi>=2021.5.30", "ciso8601==2.3.1", diff --git a/requirements.txt b/requirements.txt index 5b1c57c7e1c..f41fca19ecc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ astral==2.2 async-interrupt==1.1.2 attrs==23.2.0 atomicwrites-homeassistant==1.4.1 -awesomeversion==24.2.0 +awesomeversion==24.6.0 bcrypt==4.1.2 certifi>=2021.5.30 ciso8601==2.3.1 From 85629dc31e17d33d016aead42fedda02aaa3951e Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:34:01 +0200 Subject: [PATCH 0306/2411] Move Auto On/off switches to Config EntityCategory (#120648) --- homeassistant/components/lamarzocco/switch.py | 2 ++ tests/components/lamarzocco/snapshots/test_switch.ambr | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lamarzocco/switch.py b/homeassistant/components/lamarzocco/switch.py index e21cd2f3d94..c57e0662ab2 100644 --- a/homeassistant/components/lamarzocco/switch.py +++ b/homeassistant/components/lamarzocco/switch.py @@ -9,6 +9,7 @@ from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -105,6 +106,7 @@ class LaMarzoccoAutoOnOffSwitchEntity(LaMarzoccoBaseEntity, SwitchEntity): super().__init__(coordinator, f"auto_on_off_{identifier}") self._identifier = identifier self._attr_translation_placeholders = {"id": identifier} + self.entity_category = EntityCategory.CONFIG async def _async_enable(self, state: bool) -> None: """Enable or disable the auto on/off schedule.""" diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 0f462955a33..edda4ffee3b 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -10,7 +10,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'switch', - 'entity_category': None, + 'entity_category': , 'entity_id': 'switch.gs01234_auto_on_off_os2oswx', 'has_entity_name': True, 'hidden_by': None, @@ -43,7 +43,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'switch', - 'entity_category': None, + 'entity_category': , 'entity_id': 'switch.gs01234_auto_on_off_axfz5bj', 'has_entity_name': True, 'hidden_by': None, From 06f495dd45e28baa7580d0f328ec92abce921e91 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 10:43:28 +0200 Subject: [PATCH 0307/2411] Add snapshots to tasmota sensor test (#120647) --- .../tasmota/snapshots/test_sensor.ambr | 1526 +++++++++++++++++ tests/components/tasmota/test_sensor.py | 218 +-- 2 files changed, 1533 insertions(+), 211 deletions(-) create mode 100644 tests/components/tasmota/snapshots/test_sensor.ambr diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..744554c7246 --- /dev/null +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -0,0 +1,1526 @@ +# serializer version: 1 +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DHT11 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHT11 Temperature', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_DHT11_Temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DHT11 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DHT11 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.0', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Speed Act', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'TX23 Speed Act', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_TX23_Speed_Act', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Dir Card', + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'TX23 Dir Card', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_TX23_Dir_Card', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Speed Act', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.3', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Dir Card', + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'WSW', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Speed Act', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Dir Card', + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ESE', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'ENERGY TotalTariff 0', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'ENERGY TotalTariff 1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DS18B20 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DS18B20 Temperature', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_DS18B20_Temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota DS18B20 Id', + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'DS18B20 Id', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_DS18B20_Id', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DS18B20 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.3', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota DS18B20 Id', + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '01191ED79190', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DS18B20 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota DS18B20 Id', + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'meep', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total 0', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_0', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total 1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total Phase1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_Phase1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total Phase2', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_Phase2', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG Temperature1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Temperature1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.0', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Tasmota ANALOG Illuminance3', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG Temperature2', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Temperature2', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Tasmota ANALOG Illuminance3', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG Illuminance3', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Illuminance3', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Tasmota ANALOG Illuminance3', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].9 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Energy', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Energy', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '230', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].12 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].13 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1150', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].14 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '230', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].15 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Power', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Power', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Voltage', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].7 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Current', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Current', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].9 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2300', + }) +# --- diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 2de80de4319..c01485d12a7 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -13,9 +13,9 @@ from hatasmota.utils import ( get_topic_tele_will, ) import pytest +from syrupy import SnapshotAssertion from homeassistant import config_entries -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant @@ -175,7 +175,7 @@ TEMPERATURE_SENSOR_CONFIG = { @pytest.mark.parametrize( - ("sensor_config", "entity_ids", "messages", "states"), + ("sensor_config", "entity_ids", "messages"), [ ( DEFAULT_SENSOR_CONFIG, @@ -184,20 +184,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"DHT11":{"Temperature":20.5}}', '{"StatusSNS":{"DHT11":{"Temperature":20.0}}}', ), - ( - { - "sensor.tasmota_dht11_temperature": { - "state": "20.5", - "attributes": { - "device_class": "temperature", - "unit_of_measurement": "°C", - }, - }, - }, - { - "sensor.tasmota_dht11_temperature": {"state": "20.0"}, - }, - ), ), ( DICT_SENSOR_CONFIG_1, @@ -206,22 +192,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"TX23":{"Speed":{"Act":"12.3"},"Dir": {"Card": "WSW"}}}', '{"StatusSNS":{"TX23":{"Speed":{"Act":"23.4"},"Dir": {"Card": "ESE"}}}}', ), - ( - { - "sensor.tasmota_tx23_speed_act": { - "state": "12.3", - "attributes": { - "device_class": None, - "unit_of_measurement": "km/h", - }, - }, - "sensor.tasmota_tx23_dir_card": {"state": "WSW"}, - }, - { - "sensor.tasmota_tx23_speed_act": {"state": "23.4"}, - "sensor.tasmota_tx23_dir_card": {"state": "ESE"}, - }, - ), ), ( LIST_SENSOR_CONFIG, @@ -233,22 +203,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"TotalTariff":[1.2,3.4]}}', '{"StatusSNS":{"ENERGY":{"TotalTariff":[5.6,7.8]}}}', ), - ( - { - "sensor.tasmota_energy_totaltariff_0": { - "state": "1.2", - "attributes": { - "device_class": None, - "unit_of_measurement": None, - }, - }, - "sensor.tasmota_energy_totaltariff_1": {"state": "3.4"}, - }, - { - "sensor.tasmota_energy_totaltariff_0": {"state": "5.6"}, - "sensor.tasmota_energy_totaltariff_1": {"state": "7.8"}, - }, - ), ), ( TEMPERATURE_SENSOR_CONFIG, @@ -257,22 +211,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"DS18B20":{"Id": "01191ED79190","Temperature": 12.3}}', '{"StatusSNS":{"DS18B20":{"Id": "meep","Temperature": 23.4}}}', ), - ( - { - "sensor.tasmota_ds18b20_temperature": { - "state": "12.3", - "attributes": { - "device_class": "temperature", - "unit_of_measurement": "°C", - }, - }, - "sensor.tasmota_ds18b20_id": {"state": "01191ED79190"}, - }, - { - "sensor.tasmota_ds18b20_temperature": {"state": "23.4"}, - "sensor.tasmota_ds18b20_id": {"state": "meep"}, - }, - ), ), # Test simple Total sensor ( @@ -282,21 +220,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"Total":1.2,"TotalStartTime":"2018-11-23T15:33:47"}}', '{"StatusSNS":{"ENERGY":{"Total":5.6,"TotalStartTime":"2018-11-23T16:33:47"}}}', ), - ( - { - "sensor.tasmota_energy_total": { - "state": "1.2", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - }, - { - "sensor.tasmota_energy_total": {"state": "5.6"}, - }, - ), ), # Test list Total sensors ( @@ -306,30 +229,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"Total":[1.2, 3.4],"TotalStartTime":"2018-11-23T15:33:47"}}', '{"StatusSNS":{"ENERGY":{"Total":[5.6, 7.8],"TotalStartTime":"2018-11-23T16:33:47"}}}', ), - ( - { - "sensor.tasmota_energy_total_0": { - "state": "1.2", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - "sensor.tasmota_energy_total_1": { - "state": "3.4", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - }, - { - "sensor.tasmota_energy_total_0": {"state": "5.6"}, - "sensor.tasmota_energy_total_1": {"state": "7.8"}, - }, - ), ), # Test dict Total sensors ( @@ -342,30 +241,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"Total":{"Phase1":1.2, "Phase2":3.4},"TotalStartTime":"2018-11-23T15:33:47"}}', '{"StatusSNS":{"ENERGY":{"Total":{"Phase1":5.6, "Phase2":7.8},"TotalStartTime":"2018-11-23T15:33:47"}}}', ), - ( - { - "sensor.tasmota_energy_total_phase1": { - "state": "1.2", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - "sensor.tasmota_energy_total_phase2": { - "state": "3.4", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - }, - { - "sensor.tasmota_energy_total_phase1": {"state": "5.6"}, - "sensor.tasmota_energy_total_phase2": {"state": "7.8"}, - }, - ), ), ( NUMBERED_SENSOR_CONFIG, @@ -384,39 +259,6 @@ TEMPERATURE_SENSOR_CONFIG = { '"Illuminance3":1.2}}}' ), ), - ( - { - "sensor.tasmota_analog_temperature1": { - "state": "1.2", - "attributes": { - "device_class": "temperature", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "°C", - }, - }, - "sensor.tasmota_analog_temperature2": { - "state": "3.4", - "attributes": { - "device_class": "temperature", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "°C", - }, - }, - "sensor.tasmota_analog_illuminance3": { - "state": "5.6", - "attributes": { - "device_class": "illuminance", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "lx", - }, - }, - }, - { - "sensor.tasmota_analog_temperature1": {"state": "7.8"}, - "sensor.tasmota_analog_temperature2": {"state": "9.0"}, - "sensor.tasmota_analog_illuminance3": {"state": "1.2"}, - }, - ), ), ( NUMBERED_SENSOR_CONFIG_2, @@ -436,48 +278,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"Energy":1.0,"Power":1150,"Voltage":230,"Current":5}}}}' ), ), - ( - { - "sensor.tasmota_analog_ctenergy1_energy": { - "state": "0.5", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - "sensor.tasmota_analog_ctenergy1_power": { - "state": "2300", - "attributes": { - "device_class": "power", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "W", - }, - }, - "sensor.tasmota_analog_ctenergy1_voltage": { - "state": "230", - "attributes": { - "device_class": "voltage", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "V", - }, - }, - "sensor.tasmota_analog_ctenergy1_current": { - "state": "10", - "attributes": { - "device_class": "current", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "A", - }, - }, - }, - { - "sensor.tasmota_analog_ctenergy1_energy": {"state": "1.0"}, - "sensor.tasmota_analog_ctenergy1_power": {"state": "1150"}, - "sensor.tasmota_analog_ctenergy1_voltage": {"state": "230"}, - "sensor.tasmota_analog_ctenergy1_current": {"state": "5"}, - }, - ), ), ], ) @@ -485,11 +285,11 @@ async def test_controlling_state_via_mqtt( hass: HomeAssistant, entity_registry: er.EntityRegistry, mqtt_mock: MqttMockHAClient, + snapshot: SnapshotAssertion, setup_tasmota, sensor_config, entity_ids, messages, - states, ) -> None: """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -513,11 +313,13 @@ async def test_controlling_state_via_mqtt( state = hass.states.get(entity_id) assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert state == snapshot entry = entity_registry.async_get(entity_id) assert entry.disabled is False assert entry.disabled_by is None assert entry.entity_category is None + assert entry == snapshot async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() @@ -530,19 +332,13 @@ async def test_controlling_state_via_mqtt( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", messages[0]) for entity_id in entity_ids: state = hass.states.get(entity_id) - expected_state = states[0][entity_id] - assert state.state == expected_state["state"] - for attribute, expected in expected_state.get("attributes", {}).items(): - assert state.attributes.get(attribute) == expected + assert state == snapshot # Test polled state update async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/STATUS10", messages[1]) for entity_id in entity_ids: state = hass.states.get(entity_id) - expected_state = states[1][entity_id] - assert state.state == expected_state["state"] - for attribute, expected in expected_state.get("attributes", {}).items(): - assert state.attributes.get(attribute) == expected + assert state == snapshot @pytest.mark.parametrize( From 022f5453427ef24ed79ffa90a5c7c51c55d774e9 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:21:34 +0100 Subject: [PATCH 0308/2411] Remove unnecessary .coveragerc entries (#120620) --- .coveragerc | 3 --- 1 file changed, 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index 0784977ff55..2bc76723445 100644 --- a/.coveragerc +++ b/.coveragerc @@ -370,7 +370,6 @@ omit = homeassistant/components/eq3btsmart/__init__.py homeassistant/components/eq3btsmart/climate.py homeassistant/components/eq3btsmart/entity.py - homeassistant/components/eq3btsmart/models.py homeassistant/components/escea/__init__.py homeassistant/components/escea/climate.py homeassistant/components/escea/discovery.py @@ -1511,7 +1510,6 @@ omit = homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/ukraine_alarm/__init__.py homeassistant/components/ukraine_alarm/binary_sensor.py - homeassistant/components/unifi_direct/__init__.py homeassistant/components/unifi_direct/device_tracker.py homeassistant/components/unifiled/* homeassistant/components/upb/__init__.py @@ -1693,7 +1691,6 @@ omit = homeassistant/components/zabbix/* homeassistant/components/zamg/coordinator.py homeassistant/components/zengge/light.py - homeassistant/components/zeroconf/models.py homeassistant/components/zeroconf/usage.py homeassistant/components/zestimate/sensor.py homeassistant/components/zha/core/cluster_handlers/* From 4f7c6bdce87eb1b501fc83ef1c098f2fa4baa2b1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 12:29:32 +0200 Subject: [PATCH 0309/2411] Disable polling for Knocki (#120656) --- homeassistant/components/knocki/event.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/knocki/event.py b/homeassistant/components/knocki/event.py index adaf344e468..74dc5a0f64c 100644 --- a/homeassistant/components/knocki/event.py +++ b/homeassistant/components/knocki/event.py @@ -48,6 +48,7 @@ class KnockiTrigger(EventEntity): _attr_event_types = [EVENT_TRIGGERED] _attr_has_entity_name = True + _attr_should_poll = False _attr_translation_key = "knocki" def __init__(self, trigger: Trigger, client: KnockiClient) -> None: From 9aa2cc11e9f48afcae5f9477cf6e25a3766de03b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 12:47:58 +0200 Subject: [PATCH 0310/2411] Fix Airgradient ABC days name (#120659) --- .../components/airgradient/select.py | 1 + .../components/airgradient/strings.json | 3 +- .../airgradient/snapshots/test_select.ambr | 28 +++++++++++-------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index a64ce596806..532f7167dff 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -88,6 +88,7 @@ LEARNING_TIME_OFFSET_OPTIONS = [ ] ABC_DAYS = [ + "1", "8", "30", "90", diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 1dd5fc61a16..12049e7b720 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -91,8 +91,9 @@ } }, "co2_automatic_baseline_calibration": { - "name": "CO2 automatic baseline calibration", + "name": "CO2 automatic baseline duration", "state": { + "1": "1 day", "8": "8 days", "30": "30 days", "90": "90 days", diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index ece563b40c6..b8fca4a110b 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -1,11 +1,12 @@ # serializer version: 1 -# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_calibration-entry] +# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ + '1', '8', '30', '90', @@ -19,7 +20,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -31,7 +32,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'CO2 automatic baseline calibration', + 'original_name': 'CO2 automatic baseline duration', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -40,11 +41,12 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_calibration-state] +# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient CO2 automatic baseline calibration', + 'friendly_name': 'Airgradient CO2 automatic baseline duration', 'options': list([ + '1', '8', '30', '90', @@ -53,7 +55,7 @@ ]), }), 'context': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'last_changed': , 'last_reported': , 'last_updated': , @@ -404,13 +406,14 @@ 'state': '12', }) # --- -# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_calibration-entry] +# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ + '1', '8', '30', '90', @@ -424,7 +427,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -436,7 +439,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'CO2 automatic baseline calibration', + 'original_name': 'CO2 automatic baseline duration', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -445,11 +448,12 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_calibration-state] +# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient CO2 automatic baseline calibration', + 'friendly_name': 'Airgradient CO2 automatic baseline duration', 'options': list([ + '1', '8', '30', '90', @@ -458,7 +462,7 @@ ]), }), 'context': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'last_changed': , 'last_reported': , 'last_updated': , From 54a5a3e3fb5b274e5f0c985a71f21ef3da68ce58 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 12:55:49 +0200 Subject: [PATCH 0311/2411] Bump hatasmota to 0.9.1 (#120649) --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 4 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../tasmota/snapshots/test_sensor.ambr | 34 +++++++++++++++---- 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 2ce81772774..69233de07d8 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.8.0"] + "requirements": ["HATasmota==0.9.1"] } diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 546e3eb4539..a7fb415f037 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -190,6 +190,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.ENERGY, STATE_CLASS: SensorStateClass.TOTAL, }, + hc.SENSOR_TOTAL_TARIFF: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, hc.SENSOR_VOLTAGE: { diff --git a/requirements_all.txt b/requirements_all.txt index 44bc9f73b1d..06e5b6ef223 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.8.0 +HATasmota==0.9.1 # homeassistant.components.mastodon Mastodon.py==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45cb1087cb4..58691727bec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.8.0 +HATasmota==0.9.1 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index 744554c7246..c5d70487749 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -232,7 +232,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_0', @@ -247,7 +250,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -264,7 +269,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'ENERGY TotalTariff 0', 'platform': 'tasmota', @@ -272,13 +277,16 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_0', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].2 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_1', @@ -293,7 +301,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -310,7 +320,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'ENERGY TotalTariff 1', 'platform': 'tasmota', @@ -318,13 +328,16 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_1', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].4 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_0', @@ -337,7 +350,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_1', @@ -350,7 +366,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_0', @@ -363,7 +382,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_1', From a165064e9dc60750bb80fc2dd335b7667d59fec9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 13:08:19 +0200 Subject: [PATCH 0312/2411] Improve typing of state event helpers (#120639) --- homeassistant/core.py | 15 +++++++++------ homeassistant/helpers/event.py | 10 ++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 2b43b2d40ff..71ee5f4bd1d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -158,26 +158,29 @@ class ConfigSource(enum.StrEnum): YAML = "yaml" -class EventStateChangedData(TypedDict): +class EventStateEventData(TypedDict): + """Base class for EVENT_STATE_CHANGED and EVENT_STATE_CHANGED data.""" + + entity_id: str + new_state: State | None + + +class EventStateChangedData(EventStateEventData): """EVENT_STATE_CHANGED data. A state changed event is fired when on state write when the state is changed. """ - entity_id: str old_state: State | None - new_state: State | None -class EventStateReportedData(TypedDict): +class EventStateReportedData(EventStateEventData): """EVENT_STATE_REPORTED data. A state reported event is fired when on state write when the state is unchanged. """ - entity_id: str old_last_reported: datetime.datetime - new_state: State | None # SOURCE_* are deprecated as of Home Assistant 2022.2, use ConfigSource instead diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index ebd51948e3b..fa409269ad6 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -27,6 +27,7 @@ from homeassistant.core import ( Event, # Explicit reexport of 'EventStateChangedData' for backwards compatibility EventStateChangedData as EventStateChangedData, # noqa: PLC0414 + EventStateEventData, EventStateReportedData, HassJob, HassJobType, @@ -89,6 +90,7 @@ RANDOM_MICROSECOND_MIN = 50000 RANDOM_MICROSECOND_MAX = 500000 _TypedDictT = TypeVar("_TypedDictT", bound=Mapping[str, Any]) +_StateEventDataT = TypeVar("_StateEventDataT", bound=EventStateEventData) @dataclass(slots=True, frozen=True) @@ -329,8 +331,8 @@ def async_track_state_change_event( @callback def _async_dispatch_entity_id_event( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], - event: Event[_TypedDictT], + callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]], + event: Event[_StateEventDataT], ) -> None: """Dispatch to listeners.""" if not (callbacks_list := callbacks.get(event.data["entity_id"])): @@ -349,8 +351,8 @@ def _async_dispatch_entity_id_event( @callback def _async_state_filter( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], - event_data: _TypedDictT, + callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]], + event_data: _StateEventDataT, ) -> bool: """Filter state changes by entity_id.""" return event_data["entity_id"] in callbacks From a93855ded354f470344e8addb6d66027b93c53b0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 27 Jun 2024 23:08:40 +1200 Subject: [PATCH 0313/2411] [esphome] Add more tests to bring integration to 100% coverage (#120661) --- tests/components/esphome/conftest.py | 147 +++++++++++++++++- tests/components/esphome/test_manager.py | 108 ++++++++++++- .../esphome/test_voice_assistant.py | 14 +- 3 files changed, 258 insertions(+), 11 deletions(-) diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index f55ab9cbe4a..ac1558b8aa0 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from asyncio import Event -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Coroutine from pathlib import Path from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -19,6 +19,8 @@ from aioesphomeapi import ( HomeassistantServiceCall, ReconnectLogic, UserService, + VoiceAssistantAudioSettings, + VoiceAssistantEventType, VoiceAssistantFeature, ) import pytest @@ -32,6 +34,11 @@ from homeassistant.components.esphome.const import ( DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, DOMAIN, ) +from homeassistant.components.esphome.entry_data import RuntimeEntryData +from homeassistant.components.esphome.voice_assistant import ( + VoiceAssistantAPIPipeline, + VoiceAssistantUDPPipeline, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -40,6 +47,8 @@ from . import DASHBOARD_HOST, DASHBOARD_PORT, DASHBOARD_SLUG from tests.common import MockConfigEntry +_ONE_SECOND = 16000 * 2 # 16Khz 16-bit + @pytest.fixture(autouse=True) def mock_bluetooth(enable_bluetooth: None) -> None: @@ -196,6 +205,20 @@ class MockESPHomeDevice: self.home_assistant_state_subscription_callback: Callable[ [str, str | None], None ] + self.voice_assistant_handle_start_callback: Callable[ + [str, int, VoiceAssistantAudioSettings, str | None], + Coroutine[Any, Any, int | None], + ] + self.voice_assistant_handle_stop_callback: Callable[ + [], Coroutine[Any, Any, None] + ] + self.voice_assistant_handle_audio_callback: ( + Callable[ + [bytes], + Coroutine[Any, Any, None], + ] + | None + ) self.device_info = device_info def set_state_callback(self, state_callback: Callable[[EntityState], None]) -> None: @@ -255,6 +278,47 @@ class MockESPHomeDevice: """Mock a state subscription.""" self.home_assistant_state_subscription_callback(entity_id, attribute) + def set_subscribe_voice_assistant_callbacks( + self, + handle_start: Callable[ + [str, int, VoiceAssistantAudioSettings, str | None], + Coroutine[Any, Any, int | None], + ], + handle_stop: Callable[[], Coroutine[Any, Any, None]], + handle_audio: ( + Callable[ + [bytes], + Coroutine[Any, Any, None], + ] + | None + ) = None, + ) -> None: + """Set the voice assistant subscription callbacks.""" + self.voice_assistant_handle_start_callback = handle_start + self.voice_assistant_handle_stop_callback = handle_stop + self.voice_assistant_handle_audio_callback = handle_audio + + async def mock_voice_assistant_handle_start( + self, + conversation_id: str, + flags: int, + settings: VoiceAssistantAudioSettings, + wake_word_phrase: str | None, + ) -> int | None: + """Mock voice assistant handle start.""" + return await self.voice_assistant_handle_start_callback( + conversation_id, flags, settings, wake_word_phrase + ) + + async def mock_voice_assistant_handle_stop(self) -> None: + """Mock voice assistant handle stop.""" + await self.voice_assistant_handle_stop_callback() + + async def mock_voice_assistant_handle_audio(self, audio: bytes) -> None: + """Mock voice assistant handle audio.""" + assert self.voice_assistant_handle_audio_callback is not None + await self.voice_assistant_handle_audio_callback(audio) + async def _mock_generic_device_entry( hass: HomeAssistant, @@ -318,8 +382,33 @@ async def _mock_generic_device_entry( """Subscribe to home assistant states.""" mock_device.set_home_assistant_state_subscription_callback(on_state_sub) + def _subscribe_voice_assistant( + *, + handle_start: Callable[ + [str, int, VoiceAssistantAudioSettings, str | None], + Coroutine[Any, Any, int | None], + ], + handle_stop: Callable[[], Coroutine[Any, Any, None]], + handle_audio: ( + Callable[ + [bytes], + Coroutine[Any, Any, None], + ] + | None + ) = None, + ) -> Callable[[], None]: + """Subscribe to voice assistant.""" + mock_device.set_subscribe_voice_assistant_callbacks( + handle_start, handle_stop, handle_audio + ) + + def unsub(): + pass + + return unsub + mock_client.device_info = AsyncMock(return_value=mock_device.device_info) - mock_client.subscribe_voice_assistant = Mock() + mock_client.subscribe_voice_assistant = _subscribe_voice_assistant mock_client.list_entities_services = AsyncMock( return_value=mock_list_entities_services ) @@ -524,3 +613,57 @@ async def mock_esphome_device( ) return _mock_device + + +@pytest.fixture +def mock_voice_assistant_api_pipeline() -> VoiceAssistantAPIPipeline: + """Return the API Pipeline factory.""" + mock_pipeline = Mock(spec=VoiceAssistantAPIPipeline) + + def mock_constructor( + hass: HomeAssistant, + entry_data: RuntimeEntryData, + handle_event: Callable[[VoiceAssistantEventType, dict[str, str] | None], None], + handle_finished: Callable[[], None], + api_client: APIClient, + ): + """Fake the constructor.""" + mock_pipeline.hass = hass + mock_pipeline.entry_data = entry_data + mock_pipeline.handle_event = handle_event + mock_pipeline.handle_finished = handle_finished + mock_pipeline.api_client = api_client + return mock_pipeline + + mock_pipeline.side_effect = mock_constructor + with patch( + "homeassistant.components.esphome.voice_assistant.VoiceAssistantAPIPipeline", + new=mock_pipeline, + ): + yield mock_pipeline + + +@pytest.fixture +def mock_voice_assistant_udp_pipeline() -> VoiceAssistantUDPPipeline: + """Return the API Pipeline factory.""" + mock_pipeline = Mock(spec=VoiceAssistantUDPPipeline) + + def mock_constructor( + hass: HomeAssistant, + entry_data: RuntimeEntryData, + handle_event: Callable[[VoiceAssistantEventType, dict[str, str] | None], None], + handle_finished: Callable[[], None], + ): + """Fake the constructor.""" + mock_pipeline.hass = hass + mock_pipeline.entry_data = entry_data + mock_pipeline.handle_event = handle_event + mock_pipeline.handle_finished = handle_finished + return mock_pipeline + + mock_pipeline.side_effect = mock_constructor + with patch( + "homeassistant.components.esphome.voice_assistant.VoiceAssistantUDPPipeline", + new=mock_pipeline, + ): + yield mock_pipeline diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 92c21842e78..01f267581f4 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -2,7 +2,7 @@ import asyncio from collections.abc import Awaitable, Callable -from unittest.mock import AsyncMock, call +from unittest.mock import AsyncMock, call, patch from aioesphomeapi import ( APIClient, @@ -17,6 +17,7 @@ from aioesphomeapi import ( UserService, UserServiceArg, UserServiceArgType, + VoiceAssistantFeature, ) import pytest @@ -28,6 +29,10 @@ from homeassistant.components.esphome.const import ( DOMAIN, STABLE_BLE_VERSION_STR, ) +from homeassistant.components.esphome.voice_assistant import ( + VoiceAssistantAPIPipeline, + VoiceAssistantUDPPipeline, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -39,7 +44,7 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.setup import async_setup_component -from .conftest import MockESPHomeDevice +from .conftest import _ONE_SECOND, MockESPHomeDevice from tests.common import MockConfigEntry, async_capture_events, async_mock_service @@ -1181,3 +1186,102 @@ async def test_entry_missing_unique_id( await mock_esphome_device(mock_client=mock_client, mock_storage=True) await hass.async_block_till_done() assert entry.unique_id == "11:22:33:44:55:aa" + + +async def test_manager_voice_assistant_handlers_api( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + caplog: pytest.LogCaptureFixture, + mock_voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, +) -> None: + """Test the handlers are correctly executed in manager.py.""" + + device: MockESPHomeDevice = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={ + "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT + | VoiceAssistantFeature.API_AUDIO + }, + ) + + await hass.async_block_till_done() + + with ( + patch( + "homeassistant.components.esphome.manager.VoiceAssistantAPIPipeline", + new=mock_voice_assistant_api_pipeline, + ), + ): + port: int | None = await device.mock_voice_assistant_handle_start( + "", 0, None, None + ) + + assert port == 0 + + port: int | None = await device.mock_voice_assistant_handle_start( + "", 0, None, None + ) + + assert "Voice assistant UDP server was not stopped" in caplog.text + + await device.mock_voice_assistant_handle_audio(bytes(_ONE_SECOND)) + + mock_voice_assistant_api_pipeline.receive_audio_bytes.assert_called_with( + bytes(_ONE_SECOND) + ) + + mock_voice_assistant_api_pipeline.receive_audio_bytes.reset_mock() + + await device.mock_voice_assistant_handle_stop() + mock_voice_assistant_api_pipeline.handle_finished() + + await device.mock_voice_assistant_handle_audio(bytes(_ONE_SECOND)) + + mock_voice_assistant_api_pipeline.receive_audio_bytes.assert_not_called() + + +async def test_manager_voice_assistant_handlers_udp( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + mock_voice_assistant_udp_pipeline: VoiceAssistantUDPPipeline, +) -> None: + """Test the handlers are correctly executed in manager.py.""" + + device: MockESPHomeDevice = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={ + "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT + }, + ) + + await hass.async_block_till_done() + + with ( + patch( + "homeassistant.components.esphome.manager.VoiceAssistantUDPPipeline", + new=mock_voice_assistant_udp_pipeline, + ), + ): + await device.mock_voice_assistant_handle_start("", 0, None, None) + + mock_voice_assistant_udp_pipeline.run_pipeline.assert_called() + + await device.mock_voice_assistant_handle_stop() + mock_voice_assistant_udp_pipeline.handle_finished() + + mock_voice_assistant_udp_pipeline.stop.assert_called() + mock_voice_assistant_udp_pipeline.close.assert_called() diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index c347c3dc7d3..eafc0243dc6 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -37,15 +37,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import intent as intent_helper import homeassistant.helpers.device_registry as dr -from .conftest import MockESPHomeDevice +from .conftest import _ONE_SECOND, MockESPHomeDevice _TEST_INPUT_TEXT = "This is an input test" _TEST_OUTPUT_TEXT = "This is an output test" _TEST_OUTPUT_URL = "output.mp3" _TEST_MEDIA_ID = "12345" -_ONE_SECOND = 16000 * 2 # 16Khz 16-bit - @pytest.fixture def voice_assistant_udp_pipeline( @@ -813,6 +811,7 @@ async def test_wake_word_abort_exception( async def test_timer_events( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, mock_client: APIClient, mock_esphome_device: Callable[ [APIClient, list[EntityInfo], list[UserService], list[EntityState]], @@ -831,8 +830,8 @@ async def test_timer_events( | VoiceAssistantFeature.TIMERS }, ) - dev_reg = dr.async_get(hass) - dev = dev_reg.async_get_device( + await hass.async_block_till_done() + dev = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mock_device.entry.unique_id)} ) @@ -886,6 +885,7 @@ async def test_timer_events( async def test_unknown_timer_event( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, mock_client: APIClient, mock_esphome_device: Callable[ [APIClient, list[EntityInfo], list[UserService], list[EntityState]], @@ -904,8 +904,8 @@ async def test_unknown_timer_event( | VoiceAssistantFeature.TIMERS }, ) - dev_reg = dr.async_get(hass) - dev = dev_reg.async_get_device( + await hass.async_block_till_done() + dev = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mock_device.entry.unique_id)} ) From cb9251057184b37dccd58fe1cf35c78e1772cd56 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 27 Jun 2024 21:17:15 +1000 Subject: [PATCH 0314/2411] Fix values at startup for Tessie (#120652) --- homeassistant/components/tessie/entity.py | 1 + .../tessie/snapshots/test_lock.ambr | 48 ------------------- .../tessie/snapshots/test_sensor.ambr | 20 ++++---- 3 files changed, 11 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index 93b9f10ae67..d2a59f205fc 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -132,6 +132,7 @@ class TessieEnergyEntity(TessieBaseEntity): self._attr_device_info = data.device super().__init__(coordinator, key) + self._async_update_attrs() class TessieWallConnectorEntity(TessieBaseEntity): diff --git a/tests/components/tessie/snapshots/test_lock.ambr b/tests/components/tessie/snapshots/test_lock.ambr index 1eff418b202..cea2bebbddb 100644 --- a/tests/components/tessie/snapshots/test_lock.ambr +++ b/tests/components/tessie/snapshots/test_lock.ambr @@ -93,51 +93,3 @@ 'state': 'locked', }) # --- -# name: test_locks[lock.test_speed_limit-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'lock', - 'entity_category': None, - 'entity_id': 'lock.test_speed_limit', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Speed limit', - 'platform': 'tessie', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'vehicle_state_speed_limit_mode_active', - 'unique_id': 'VINVINVIN-vehicle_state_speed_limit_mode_active', - 'unit_of_measurement': None, - }) -# --- -# name: test_locks[lock.test_speed_limit-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'code_format': '^\\d\\d\\d\\d$', - 'friendly_name': 'Test Speed limit', - 'supported_features': , - }), - 'context': , - 'entity_id': 'lock.test_speed_limit', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unlocked', - }) -# --- diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index ba7b4eae0a5..afe229feba0 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -53,7 +53,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '5.06', }) # --- # name: test_sensors[sensor.energy_site_energy_left-entry] @@ -110,7 +110,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '38.8964736842105', }) # --- # name: test_sensors[sensor.energy_site_generator_power-entry] @@ -167,7 +167,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.0', }) # --- # name: test_sensors[sensor.energy_site_grid_power-entry] @@ -224,7 +224,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.0', }) # --- # name: test_sensors[sensor.energy_site_grid_services_power-entry] @@ -281,7 +281,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.0', }) # --- # name: test_sensors[sensor.energy_site_load_power-entry] @@ -338,7 +338,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '6.245', }) # --- # name: test_sensors[sensor.energy_site_percentage_charged-entry] @@ -392,7 +392,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '95.5053740373966', }) # --- # name: test_sensors[sensor.energy_site_solar_power-entry] @@ -449,7 +449,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '1.185', }) # --- # name: test_sensors[sensor.energy_site_total_pack_energy-entry] @@ -506,7 +506,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '40.727', }) # --- # name: test_sensors[sensor.energy_site_vpp_backup_reserve-entry] @@ -554,7 +554,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_battery_level-entry] From 9f6783dcf50567684abd4f4ebed34c979ac5ec29 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:21:36 +0200 Subject: [PATCH 0315/2411] Add release url to lamarzocco update (#120645) --- homeassistant/components/lamarzocco/update.py | 5 +++++ tests/components/lamarzocco/snapshots/test_update.ambr | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lamarzocco/update.py b/homeassistant/components/lamarzocco/update.py index 342a3e09071..2769016e43b 100644 --- a/homeassistant/components/lamarzocco/update.py +++ b/homeassistant/components/lamarzocco/update.py @@ -83,6 +83,11 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity): self.entity_description.component ].latest_version + @property + def release_url(self) -> str | None: + """Return the release notes URL.""" + return "https://support-iot.lamarzocco.com/firmware-updates/" + async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: diff --git a/tests/components/lamarzocco/snapshots/test_update.ambr b/tests/components/lamarzocco/snapshots/test_update.ambr index 4ab8e35ffd0..f08b9249f50 100644 --- a/tests/components/lamarzocco/snapshots/test_update.ambr +++ b/tests/components/lamarzocco/snapshots/test_update.ambr @@ -10,7 +10,7 @@ 'installed_version': 'v3.1-rc4', 'latest_version': 'v3.5-rc3', 'release_summary': None, - 'release_url': None, + 'release_url': 'https://support-iot.lamarzocco.com/firmware-updates/', 'skipped_version': None, 'supported_features': , 'title': None, @@ -67,7 +67,7 @@ 'installed_version': '1.40', 'latest_version': '1.55', 'release_summary': None, - 'release_url': None, + 'release_url': 'https://support-iot.lamarzocco.com/firmware-updates/', 'skipped_version': None, 'supported_features': , 'title': None, From 1f541808072ed8f0b965ce2caed51c328b000cab Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 27 Jun 2024 23:26:38 +1200 Subject: [PATCH 0316/2411] Mark esphome integration as platinum (#112565) Co-authored-by: J. Nick Koston --- homeassistant/components/esphome/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index ab175028bea..6e30febd7db 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,6 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "mqtt": ["esphome/discover/#"], + "quality_scale": "platinum", "requirements": [ "aioesphomeapi==24.6.1", "esphome-dashboard-api==1.2.3", From 8de771de96413ff19c921e49d1d9263670b1d131 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 13:45:15 +0200 Subject: [PATCH 0317/2411] Rename async_track_state_reported_event to async_track_state_report_event (#120637) * Rename async_track_state_reported_event to async_track_state_report_event * Update tests --- homeassistant/components/integration/sensor.py | 4 ++-- homeassistant/helpers/event.py | 12 ++++++------ tests/helpers/test_event.py | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 8cc5341f081..a053e5cea5c 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -46,7 +46,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_call_later, async_track_state_change_event, - async_track_state_reported_event, + async_track_state_report_event, ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -449,7 +449,7 @@ class IntegrationSensor(RestoreSensor): ) ) self.async_on_remove( - async_track_state_reported_event( + async_track_state_report_event( self.hass, self._sensor_source_id, handle_state_report, diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index fa409269ad6..0c77809079e 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -60,8 +60,8 @@ from .typing import TemplateVarsType _TRACK_STATE_CHANGE_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = HassKey( "track_state_change_data" ) -_TRACK_STATE_REPORTED_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey( - "track_state_reported_data" +_TRACK_STATE_REPORT_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey( + "track_state_report_data" ) _TRACK_STATE_ADDED_DOMAIN_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = ( HassKey("track_state_added_domain_data") @@ -379,15 +379,15 @@ def _async_track_state_change_event( ) -_KEYED_TRACK_STATE_REPORTED = _KeyedEventTracker( - key=_TRACK_STATE_REPORTED_DATA, +_KEYED_TRACK_STATE_REPORT = _KeyedEventTracker( + key=_TRACK_STATE_REPORT_DATA, event_type=EVENT_STATE_REPORTED, dispatcher_callable=_async_dispatch_entity_id_event, filter_callable=_async_state_filter, ) -def async_track_state_reported_event( +def async_track_state_report_event( hass: HomeAssistant, entity_ids: str | Iterable[str], action: Callable[[Event[EventStateReportedData]], Any], @@ -395,7 +395,7 @@ def async_track_state_reported_event( ) -> CALLBACK_TYPE: """Track EVENT_STATE_REPORTED by entity_id without lowercasing.""" return _async_track_event( - _KEYED_TRACK_STATE_REPORTED, hass, entity_ids, action, job_type + _KEYED_TRACK_STATE_REPORT, hass, entity_ids, action, job_type ) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 4f983120e36..4bb4c1a1967 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -40,7 +40,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_state_change_filtered, async_track_state_removed_domain, - async_track_state_reported_event, + async_track_state_report_event, async_track_sunrise, async_track_sunset, async_track_template, @@ -4916,8 +4916,8 @@ async def test_track_point_in_time_repr( await hass.async_block_till_done(wait_background_tasks=True) -async def test_async_track_state_reported_event(hass: HomeAssistant) -> None: - """Test async_track_state_reported_event.""" +async def test_async_track_state_report_event(hass: HomeAssistant) -> None: + """Test async_track_state_report_event.""" tracker_called: list[ha.State] = [] @ha.callback @@ -4925,7 +4925,7 @@ async def test_async_track_state_reported_event(hass: HomeAssistant) -> None: new_state = event.data["new_state"] tracker_called.append(new_state) - unsub = async_track_state_reported_event( + unsub = async_track_state_report_event( hass, ["light.bowl", "light.top"], single_run_callback ) hass.states.async_set("light.bowl", "on") From 0d53ce4fb887ce17cb21bef92ecef2f2d063b054 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 27 Jun 2024 14:27:35 +0200 Subject: [PATCH 0318/2411] Improve type hints in emulated_hue tests (#120664) --- tests/components/emulated_hue/test_hue_api.py | 171 +++++++++++------- 1 file changed, 110 insertions(+), 61 deletions(-) diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 4edd52b812d..40f9f7bce14 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1,15 +1,18 @@ """The tests for the emulated Hue component.""" +from __future__ import annotations + import asyncio from datetime import timedelta from http import HTTPStatus from ipaddress import ip_address import json -from unittest.mock import patch +from unittest.mock import AsyncMock, _patch, patch from aiohttp.hdrs import CONTENT_TYPE from aiohttp.test_utils import TestClient import pytest +from typing_extensions import Generator from homeassistant import const, setup from homeassistant.components import ( @@ -56,6 +59,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util +from homeassistant.util.json import JsonObjectType from tests.common import ( async_fire_time_changed, @@ -104,14 +108,14 @@ ENTITY_IDS_BY_NUMBER = { ENTITY_NUMBERS_BY_ID = {v: k for k, v in ENTITY_IDS_BY_NUMBER.items()} -def patch_upnp(): +def patch_upnp() -> _patch[AsyncMock]: """Patch async_create_upnp_datagram_endpoint.""" return patch( "homeassistant.components.emulated_hue.async_create_upnp_datagram_endpoint" ) -async def async_get_lights(client): +async def async_get_lights(client: TestClient) -> JsonObjectType: """Get lights with the hue client.""" result = await client.get("/api/username/lights") assert result.status == HTTPStatus.OK @@ -131,7 +135,7 @@ async def _async_setup_emulated_hue(hass: HomeAssistant, conf: ConfigType) -> No @pytest.fixture -async def base_setup(hass): +async def base_setup(hass: HomeAssistant) -> None: """Set up homeassistant and http.""" await asyncio.gather( setup.async_setup_component(hass, "homeassistant", {}), @@ -142,7 +146,7 @@ async def base_setup(hass): @pytest.fixture(autouse=True) -async def wanted_platforms_only() -> None: +def wanted_platforms_only() -> Generator[None]: """Enable only the wanted demo platforms.""" with patch( "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", @@ -159,7 +163,7 @@ async def wanted_platforms_only() -> None: @pytest.fixture -async def demo_setup(hass, wanted_platforms_only): +async def demo_setup(hass: HomeAssistant, wanted_platforms_only: None) -> None: """Fixture to setup demo platforms.""" # We need to do this to get access to homeassistant/turn_(on,off) setups = [ @@ -211,7 +215,9 @@ async def demo_setup(hass, wanted_platforms_only): @pytest.fixture -async def hass_hue(hass, base_setup, demo_setup): +async def hass_hue( + hass: HomeAssistant, base_setup: None, demo_setup: None +) -> HomeAssistant: """Set up a Home Assistant instance for these tests.""" await _async_setup_emulated_hue( hass, @@ -245,7 +251,7 @@ def _mock_hue_endpoints( @pytest.fixture async def hue_client( - hass_hue, hass_client_no_auth: ClientSessionGenerator + hass_hue: HomeAssistant, hass_client_no_auth: ClientSessionGenerator ) -> TestClient: """Create web client for emulated hue api.""" _mock_hue_endpoints( @@ -285,7 +291,7 @@ async def hue_client( return await hass_client_no_auth() -async def test_discover_lights(hass: HomeAssistant, hue_client) -> None: +async def test_discover_lights(hass: HomeAssistant, hue_client: TestClient) -> None: """Test the discovery of lights.""" result = await hue_client.get("/api/username/lights") @@ -343,7 +349,8 @@ async def test_discover_lights(hass: HomeAssistant, hue_client) -> None: assert device["state"][HUE_API_STATE_ON] is False -async def test_light_without_brightness_supported(hass_hue, hue_client) -> None: +@pytest.mark.usefixtures("hass_hue") +async def test_light_without_brightness_supported(hue_client: TestClient) -> None: """Test that light without brightness is supported.""" light_without_brightness_json = await perform_get_light_state( hue_client, "light.no_brightness", HTTPStatus.OK @@ -382,7 +389,9 @@ async def test_lights_all_dimmable( ) -async def test_light_without_brightness_can_be_turned_off(hass_hue, hue_client) -> None: +async def test_light_without_brightness_can_be_turned_off( + hass_hue: HomeAssistant, hue_client: TestClient +) -> None: """Test that light without brightness can be turned off.""" hass_hue.states.async_set("light.no_brightness", "on", {}) turn_off_calls = [] @@ -417,7 +426,9 @@ async def test_light_without_brightness_can_be_turned_off(hass_hue, hue_client) assert "light.no_brightness" in call.data[ATTR_ENTITY_ID] -async def test_light_without_brightness_can_be_turned_on(hass_hue, hue_client) -> None: +async def test_light_without_brightness_can_be_turned_on( + hass_hue: HomeAssistant, hue_client: TestClient +) -> None: """Test that light without brightness can be turned on.""" hass_hue.states.async_set("light.no_brightness", "off", {}) @@ -467,7 +478,9 @@ async def test_light_without_brightness_can_be_turned_on(hass_hue, hue_client) - (const.STATE_UNKNOWN, True), ], ) -async def test_reachable_for_state(hass_hue, hue_client, state, is_reachable) -> None: +async def test_reachable_for_state( + hass_hue: HomeAssistant, hue_client: TestClient, state: str, is_reachable: bool +) -> None: """Test that an entity is reported as unreachable if in unavailable state.""" entity_id = "light.ceiling_lights" @@ -478,7 +491,7 @@ async def test_reachable_for_state(hass_hue, hue_client, state, is_reachable) -> assert state_json["state"]["reachable"] == is_reachable, state_json -async def test_discover_full_state(hue_client) -> None: +async def test_discover_full_state(hue_client: TestClient) -> None: """Test the discovery of full state.""" result = await hue_client.get(f"/api/{HUE_API_USERNAME}") @@ -529,7 +542,7 @@ async def test_discover_full_state(hue_client) -> None: assert config_json["linkbutton"] is True -async def test_discover_config(hue_client) -> None: +async def test_discover_config(hue_client: TestClient) -> None: """Test the discovery of configuration.""" result = await hue_client.get(f"/api/{HUE_API_USERNAME}/config") @@ -587,7 +600,7 @@ async def test_discover_config(hue_client) -> None: assert "error" not in config_json -async def test_get_light_state(hass_hue, hue_client) -> None: +async def test_get_light_state(hass_hue: HomeAssistant, hue_client: TestClient) -> None: """Test the getting of light state.""" # Turn ceiling lights on and set to 127 brightness, and set light color await hass_hue.services.async_call( @@ -648,7 +661,9 @@ async def test_get_light_state(hass_hue, hue_client) -> None: ) -async def test_put_light_state(hass: HomeAssistant, hass_hue, hue_client) -> None: +async def test_put_light_state( + hass: HomeAssistant, hass_hue: HomeAssistant, hue_client: TestClient +) -> None: """Test the setting of light states.""" await perform_put_test_on_ceiling_lights(hass_hue, hue_client) @@ -818,7 +833,7 @@ async def test_put_light_state(hass: HomeAssistant, hass_hue, hue_client) -> Non async def test_put_light_state_script( - hass: HomeAssistant, hass_hue, hue_client + hass: HomeAssistant, hass_hue: HomeAssistant, hue_client: TestClient ) -> None: """Test the setting of script variables.""" # Turn the kitchen light off first @@ -834,7 +849,7 @@ async def test_put_light_state_script( brightness = round(level * 254 / 100) script_result = await perform_put_light_state( - hass_hue, hue_client, "script.set_kitchen_light", True, brightness + hass_hue, hue_client, "script.set_kitchen_light", True, brightness=brightness ) script_result_json = await script_result.json() @@ -851,13 +866,15 @@ async def test_put_light_state_script( ) -async def test_put_light_state_climate_set_temperature(hass_hue, hue_client) -> None: +async def test_put_light_state_climate_set_temperature( + hass_hue: HomeAssistant, hue_client: TestClient +) -> None: """Test setting climate temperature.""" brightness = 19 temperature = round(brightness / 254 * 100) hvac_result = await perform_put_light_state( - hass_hue, hue_client, "climate.hvac", True, brightness + hass_hue, hue_client, "climate.hvac", True, brightness=brightness ) hvac_result_json = await hvac_result.json() @@ -876,7 +893,9 @@ async def test_put_light_state_climate_set_temperature(hass_hue, hue_client) -> assert ecobee_result.status == HTTPStatus.UNAUTHORIZED -async def test_put_light_state_humidifier_set_humidity(hass_hue, hue_client) -> None: +async def test_put_light_state_humidifier_set_humidity( + hass_hue: HomeAssistant, hue_client: TestClient +) -> None: """Test setting humidifier target humidity.""" # Turn the humidifier off first await hass_hue.services.async_call( @@ -890,7 +909,7 @@ async def test_put_light_state_humidifier_set_humidity(hass_hue, hue_client) -> humidity = round(brightness / 254 * 100) humidifier_result = await perform_put_light_state( - hass_hue, hue_client, "humidifier.humidifier", True, brightness + hass_hue, hue_client, "humidifier.humidifier", True, brightness=brightness ) humidifier_result_json = await humidifier_result.json() @@ -909,7 +928,9 @@ async def test_put_light_state_humidifier_set_humidity(hass_hue, hue_client) -> assert hygrostat_result.status == HTTPStatus.UNAUTHORIZED -async def test_put_light_state_media_player(hass_hue, hue_client) -> None: +async def test_put_light_state_media_player( + hass_hue: HomeAssistant, hue_client: TestClient +) -> None: """Test turning on media player and setting volume.""" # Turn the music player off first await hass_hue.services.async_call( @@ -924,7 +945,7 @@ async def test_put_light_state_media_player(hass_hue, hue_client) -> None: brightness = round(level * 254) mp_result = await perform_put_light_state( - hass_hue, hue_client, "media_player.walkman", True, brightness + hass_hue, hue_client, "media_player.walkman", True, brightness=brightness ) mp_result_json = await mp_result.json() @@ -937,7 +958,9 @@ async def test_put_light_state_media_player(hass_hue, hue_client) -> None: assert walkman.attributes[media_player.ATTR_MEDIA_VOLUME_LEVEL] == level -async def test_open_cover_without_position(hass_hue, hue_client) -> None: +async def test_open_cover_without_position( + hass_hue: HomeAssistant, hue_client: TestClient +) -> None: """Test opening cover .""" cover_id = "cover.living_room_window" # Close cover first @@ -1000,7 +1023,9 @@ async def test_open_cover_without_position(hass_hue, hue_client) -> None: assert cover_test_2.attributes.get("current_position") == 0 -async def test_set_position_cover(hass_hue, hue_client) -> None: +async def test_set_position_cover( + hass_hue: HomeAssistant, hue_client: TestClient +) -> None: """Test setting position cover .""" cover_id = "cover.living_room_window" cover_number = ENTITY_NUMBERS_BY_ID[cover_id] @@ -1034,7 +1059,7 @@ async def test_set_position_cover(hass_hue, hue_client) -> None: # Go through the API to open cover_result = await perform_put_light_state( - hass_hue, hue_client, cover_id, False, brightness + hass_hue, hue_client, cover_id, False, brightness=brightness ) assert cover_result.status == HTTPStatus.OK @@ -1057,7 +1082,9 @@ async def test_set_position_cover(hass_hue, hue_client) -> None: assert cover_test_2.attributes.get("current_position") == level -async def test_put_light_state_fan(hass_hue, hue_client) -> None: +async def test_put_light_state_fan( + hass_hue: HomeAssistant, hue_client: TestClient +) -> None: """Test turning on fan and setting speed.""" # Turn the fan off first await hass_hue.services.async_call( @@ -1072,7 +1099,7 @@ async def test_put_light_state_fan(hass_hue, hue_client) -> None: brightness = round(level * 254 / 100) fan_result = await perform_put_light_state( - hass_hue, hue_client, "fan.living_room_fan", True, brightness + hass_hue, hue_client, "fan.living_room_fan", True, brightness=brightness ) fan_result_json = await fan_result.json() @@ -1166,7 +1193,9 @@ async def test_put_light_state_fan(hass_hue, hue_client) -> None: assert fan_json["state"][HUE_API_STATE_BRI] == 1 -async def test_put_with_form_urlencoded_content_type(hass_hue, hue_client) -> None: +async def test_put_with_form_urlencoded_content_type( + hass_hue: HomeAssistant, hue_client: TestClient +) -> None: """Test the form with urlencoded content.""" entity_number = ENTITY_NUMBERS_BY_ID["light.ceiling_lights"] # Needed for Alexa @@ -1185,7 +1214,7 @@ async def test_put_with_form_urlencoded_content_type(hass_hue, hue_client) -> No assert result.status == HTTPStatus.BAD_REQUEST -async def test_entity_not_found(hue_client) -> None: +async def test_entity_not_found(hue_client: TestClient) -> None: """Test for entity which are not found.""" result = await hue_client.get("/api/username/lights/98") @@ -1196,7 +1225,7 @@ async def test_entity_not_found(hue_client) -> None: assert result.status == HTTPStatus.NOT_FOUND -async def test_allowed_methods(hue_client) -> None: +async def test_allowed_methods(hue_client: TestClient) -> None: """Test the allowed methods.""" result = await hue_client.get( "/api/username/lights/ENTITY_NUMBERS_BY_ID[light.ceiling_lights]/state" @@ -1215,7 +1244,7 @@ async def test_allowed_methods(hue_client) -> None: assert result.status == HTTPStatus.METHOD_NOT_ALLOWED -async def test_proper_put_state_request(hue_client) -> None: +async def test_proper_put_state_request(hue_client: TestClient) -> None: """Test the request to set the state.""" # Test proper on value parsing result = await hue_client.put( @@ -1238,7 +1267,7 @@ async def test_proper_put_state_request(hue_client) -> None: assert result.status == HTTPStatus.BAD_REQUEST -async def test_get_empty_groups_state(hue_client) -> None: +async def test_get_empty_groups_state(hue_client: TestClient) -> None: """Test the request to get groups endpoint.""" # Test proper on value parsing result = await hue_client.get("/api/username/groups") @@ -1251,7 +1280,9 @@ async def test_get_empty_groups_state(hue_client) -> None: async def perform_put_test_on_ceiling_lights( - hass_hue, hue_client, content_type=CONTENT_TYPE_JSON + hass_hue: HomeAssistant, + hue_client: TestClient, + content_type: str = CONTENT_TYPE_JSON, ): """Test the setting of a light.""" # Turn the office light off first @@ -1267,7 +1298,12 @@ async def perform_put_test_on_ceiling_lights( # Go through the API to turn it on office_result = await perform_put_light_state( - hass_hue, hue_client, "light.ceiling_lights", True, 56, content_type + hass_hue, + hue_client, + "light.ceiling_lights", + True, + brightness=56, + content_type=content_type, ) assert office_result.status == HTTPStatus.OK @@ -1283,7 +1319,9 @@ async def perform_put_test_on_ceiling_lights( assert ceiling_lights.attributes[light.ATTR_BRIGHTNESS] == 56 -async def perform_get_light_state_by_number(client, entity_number, expected_status): +async def perform_get_light_state_by_number( + client: TestClient, entity_number: int | str, expected_status: HTTPStatus +) -> JsonObjectType | None: """Test the getting of a light state.""" result = await client.get(f"/api/username/lights/{entity_number}") @@ -1297,7 +1335,9 @@ async def perform_get_light_state_by_number(client, entity_number, expected_stat return None -async def perform_get_light_state(client, entity_id, expected_status): +async def perform_get_light_state( + client: TestClient, entity_id: str, expected_status: HTTPStatus +) -> JsonObjectType | None: """Test the getting of a light state.""" entity_number = ENTITY_NUMBERS_BY_ID[entity_id] return await perform_get_light_state_by_number( @@ -1306,18 +1346,19 @@ async def perform_get_light_state(client, entity_id, expected_status): async def perform_put_light_state( - hass_hue, - client, - entity_id, - is_on, - brightness=None, - content_type=CONTENT_TYPE_JSON, - hue=None, - saturation=None, - color_temp=None, - with_state=True, - xy=None, - transitiontime=None, + hass_hue: HomeAssistant, + client: TestClient, + entity_id: str, + is_on: bool, + *, + brightness: int | None = None, + content_type: str = CONTENT_TYPE_JSON, + hue: int | None = None, + saturation: int | None = None, + color_temp: int | None = None, + with_state: bool = True, + xy: tuple[float, float] | None = None, + transitiontime: int | None = None, ): """Test the setting of a light state.""" req_headers = {"Content-Type": content_type} @@ -1353,7 +1394,7 @@ async def perform_put_light_state( return result -async def test_external_ip_blocked(hue_client) -> None: +async def test_external_ip_blocked(hue_client: TestClient) -> None: """Test external IP blocked.""" getUrls = [ "/api/username/groups", @@ -1391,7 +1432,7 @@ async def test_external_ip_blocked(hue_client) -> None: _remote_is_allowed.cache_clear() -async def test_unauthorized_user_blocked(hue_client) -> None: +async def test_unauthorized_user_blocked(hue_client: TestClient) -> None: """Test unauthorized_user blocked.""" getUrls = [ "/api/wronguser", @@ -1405,7 +1446,7 @@ async def test_unauthorized_user_blocked(hue_client) -> None: async def test_put_then_get_cached_properly( - hass: HomeAssistant, hass_hue, hue_client + hass: HomeAssistant, hass_hue: HomeAssistant, hue_client: TestClient ) -> None: """Test the setting of light states and an immediate readback reads the same values.""" @@ -1530,7 +1571,7 @@ async def test_put_then_get_cached_properly( async def test_put_than_get_when_service_call_fails( - hass: HomeAssistant, hass_hue, hue_client + hass: HomeAssistant, hass_hue: HomeAssistant, hue_client: TestClient ) -> None: """Test putting and getting the light state when the service call fails.""" @@ -1581,14 +1622,17 @@ async def test_put_than_get_when_service_call_fails( assert ceiling_json["state"][HUE_API_STATE_ON] is False -async def test_get_invalid_entity(hass: HomeAssistant, hass_hue, hue_client) -> None: +@pytest.mark.usefixtures("hass_hue") +async def test_get_invalid_entity(hue_client: TestClient) -> None: """Test the setting of light states and an immediate readback reads the same values.""" # Check that we get an error with an invalid entity number. await perform_get_light_state_by_number(hue_client, 999, HTTPStatus.NOT_FOUND) -async def test_put_light_state_scene(hass: HomeAssistant, hass_hue, hue_client) -> None: +async def test_put_light_state_scene( + hass_hue: HomeAssistant, hue_client: TestClient +) -> None: """Test the setting of scene variables.""" # Turn the kitchen lights off first await hass_hue.services.async_call( @@ -1630,7 +1674,9 @@ async def test_put_light_state_scene(hass: HomeAssistant, hass_hue, hue_client) assert hass_hue.states.get("light.kitchen_lights").state == STATE_OFF -async def test_only_change_contrast(hass: HomeAssistant, hass_hue, hue_client) -> None: +async def test_only_change_contrast( + hass_hue: HomeAssistant, hue_client: TestClient +) -> None: """Test when only changing the contrast of a light state.""" # Turn the kitchen lights off first @@ -1661,7 +1707,7 @@ async def test_only_change_contrast(hass: HomeAssistant, hass_hue, hue_client) - async def test_only_change_hue_or_saturation( - hass: HomeAssistant, hass_hue, hue_client + hass_hue: HomeAssistant, hue_client: TestClient ) -> None: """Test setting either the hue or the saturation but not both.""" @@ -1700,8 +1746,9 @@ async def test_only_change_hue_or_saturation( ] == (0, 3) +@pytest.mark.usefixtures("base_setup") async def test_specificly_exposed_entities( - hass: HomeAssistant, base_setup, hass_client_no_auth: ClientSessionGenerator + hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator ) -> None: """Test specific entities with expose by default off.""" conf = { @@ -1731,7 +1778,9 @@ async def test_specificly_exposed_entities( assert "1" in result_json -async def test_get_light_state_when_none(hass_hue: HomeAssistant, hue_client) -> None: +async def test_get_light_state_when_none( + hass_hue: HomeAssistant, hue_client: TestClient +) -> None: """Test the getting of light state when brightness is None.""" hass_hue.states.async_set( "light.ceiling_lights", From 970dd99226b342d31ae2e41263533be210720cca Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:34:12 +0100 Subject: [PATCH 0319/2411] Store tplink credentials_hash outside of device_config (#120597) --- homeassistant/components/tplink/__init__.py | 42 +++- .../components/tplink/config_flow.py | 43 +++- homeassistant/components/tplink/const.py | 2 + tests/components/tplink/__init__.py | 19 +- tests/components/tplink/test_config_flow.py | 81 ++++++- tests/components/tplink/test_init.py | 217 +++++++++++++++++- 6 files changed, 373 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 764867f0bee..6d300f68aa0 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -43,6 +43,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from .const import ( + CONF_CREDENTIALS_HASH, CONF_DEVICE_CONFIG, CONNECT_TIMEOUT, DISCOVERY_TIMEOUT, @@ -73,6 +74,7 @@ def async_trigger_discovery( discovered_devices: dict[str, Device], ) -> None: """Trigger config flows for discovered devices.""" + for formatted_mac, device in discovered_devices.items(): discovery_flow.async_create_flow( hass, @@ -83,7 +85,6 @@ def async_trigger_discovery( CONF_HOST: device.host, CONF_MAC: formatted_mac, CONF_DEVICE_CONFIG: device.config.to_dict( - credentials_hash=device.credentials_hash, exclude_credentials=True, ), }, @@ -133,6 +134,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo """Set up TPLink from a config entry.""" host: str = entry.data[CONF_HOST] credentials = await get_credentials(hass) + entry_credentials_hash = entry.data.get(CONF_CREDENTIALS_HASH) config: DeviceConfig | None = None if config_dict := entry.data.get(CONF_DEVICE_CONFIG): @@ -151,19 +153,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo config.timeout = CONNECT_TIMEOUT if config.uses_http is True: config.http_client = create_async_tplink_clientsession(hass) + + # If we have in memory credentials use them otherwise check for credentials_hash if credentials: config.credentials = credentials + elif entry_credentials_hash: + config.credentials_hash = entry_credentials_hash + try: device: Device = await Device.connect(config=config) except AuthenticationError as ex: + # If the stored credentials_hash was used but doesn't work remove it + if not credentials and entry_credentials_hash: + data = {k: v for k, v in entry.data.items() if k != CONF_CREDENTIALS_HASH} + hass.config_entries.async_update_entry(entry, data=data) raise ConfigEntryAuthFailed from ex except KasaException as ex: raise ConfigEntryNotReady from ex - device_config_dict = device.config.to_dict( - credentials_hash=device.credentials_hash, exclude_credentials=True - ) + device_credentials_hash = device.credentials_hash + device_config_dict = device.config.to_dict(exclude_credentials=True) + # Do not store the credentials hash inside the device_config + device_config_dict.pop(CONF_CREDENTIALS_HASH, None) updates: dict[str, Any] = {} + if device_credentials_hash and device_credentials_hash != entry_credentials_hash: + updates[CONF_CREDENTIALS_HASH] = device_credentials_hash if device_config_dict != config_dict: updates[CONF_DEVICE_CONFIG] = device_config_dict if entry.data.get(CONF_ALIAS) != device.alias: @@ -326,7 +340,25 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> minor_version = 3 hass.config_entries.async_update_entry(config_entry, minor_version=3) - _LOGGER.debug("Migration to version %s.%s successful", version, minor_version) + if version == 1 and minor_version == 3: + # credentials_hash stored in the device_config should be moved to data. + updates: dict[str, Any] = {} + if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG): + assert isinstance(config_dict, dict) + if credentials_hash := config_dict.pop(CONF_CREDENTIALS_HASH, None): + updates[CONF_CREDENTIALS_HASH] = credentials_hash + updates[CONF_DEVICE_CONFIG] = config_dict + minor_version = 4 + hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, + **updates, + }, + minor_version=minor_version, + ) + _LOGGER.debug("Migration to version %s.%s complete", version, minor_version) + return True diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index 7bead2207a3..5608ccfa72f 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -44,7 +44,13 @@ from . import ( mac_alias, set_credentials, ) -from .const import CONF_DEVICE_CONFIG, CONNECT_TIMEOUT, DOMAIN +from .const import ( + CONF_CONNECTION_TYPE, + CONF_CREDENTIALS_HASH, + CONF_DEVICE_CONFIG, + CONNECT_TIMEOUT, + DOMAIN, +) STEP_AUTH_DATA_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} @@ -55,7 +61,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for tplink.""" VERSION = 1 - MINOR_VERSION = 3 + MINOR_VERSION = 4 reauth_entry: ConfigEntry | None = None def __init__(self) -> None: @@ -95,9 +101,18 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): entry_config_dict = entry_data.get(CONF_DEVICE_CONFIG) if entry_config_dict == config and entry_data[CONF_HOST] == host: return None + updates = {**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host} + # If the connection parameters have changed the credentials_hash will be invalid. + if ( + entry_config_dict + and isinstance(entry_config_dict, dict) + and entry_config_dict.get(CONF_CONNECTION_TYPE) + != config.get(CONF_CONNECTION_TYPE) + ): + updates.pop(CONF_CREDENTIALS_HASH, None) return self.async_update_reload_and_abort( entry, - data={**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host}, + data=updates, reason="already_configured", ) @@ -345,18 +360,22 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): @callback def _async_create_entry_from_device(self, device: Device) -> ConfigFlowResult: """Create a config entry from a smart device.""" + # This is only ever called after a successful device update so we know that + # the credential_hash is correct and should be saved. self._abort_if_unique_id_configured(updates={CONF_HOST: device.host}) + data = { + CONF_HOST: device.host, + CONF_ALIAS: device.alias, + CONF_MODEL: device.model, + CONF_DEVICE_CONFIG: device.config.to_dict( + exclude_credentials=True, + ), + } + if device.credentials_hash: + data[CONF_CREDENTIALS_HASH] = device.credentials_hash return self.async_create_entry( title=f"{device.alias} {device.model}", - data={ - CONF_HOST: device.host, - CONF_ALIAS: device.alias, - CONF_MODEL: device.model, - CONF_DEVICE_CONFIG: device.config.to_dict( - credentials_hash=device.credentials_hash, - exclude_credentials=True, - ), - }, + data=data, ) async def _async_try_discover_and_update( diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index d77d415aa9c..babd92e2c34 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -20,6 +20,8 @@ ATTR_TODAY_ENERGY_KWH: Final = "today_energy_kwh" ATTR_TOTAL_ENERGY_KWH: Final = "total_energy_kwh" CONF_DEVICE_CONFIG: Final = "device_config" +CONF_CREDENTIALS_HASH: Final = "credentials_hash" +CONF_CONNECTION_TYPE: Final = "connection_type" PLATFORMS: Final = [ Platform.BINARY_SENSOR, diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 9c8aeb99be1..b3092d62904 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -22,6 +22,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.tplink import ( CONF_ALIAS, + CONF_CREDENTIALS_HASH, CONF_DEVICE_CONFIG, CONF_HOST, CONF_MODEL, @@ -53,9 +54,7 @@ MAC_ADDRESS2 = "11:22:33:44:55:66" DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" CREDENTIALS_HASH_LEGACY = "" DEVICE_CONFIG_LEGACY = DeviceConfig(IP_ADDRESS) -DEVICE_CONFIG_DICT_LEGACY = DEVICE_CONFIG_LEGACY.to_dict( - credentials_hash=CREDENTIALS_HASH_LEGACY, exclude_credentials=True -) +DEVICE_CONFIG_DICT_LEGACY = DEVICE_CONFIG_LEGACY.to_dict(exclude_credentials=True) CREDENTIALS = Credentials("foo", "bar") CREDENTIALS_HASH_AUTH = "abcdefghijklmnopqrstuv==" DEVICE_CONFIG_AUTH = DeviceConfig( @@ -74,12 +73,8 @@ DEVICE_CONFIG_AUTH2 = DeviceConfig( ), uses_http=True, ) -DEVICE_CONFIG_DICT_AUTH = DEVICE_CONFIG_AUTH.to_dict( - credentials_hash=CREDENTIALS_HASH_AUTH, exclude_credentials=True -) -DEVICE_CONFIG_DICT_AUTH2 = DEVICE_CONFIG_AUTH2.to_dict( - credentials_hash=CREDENTIALS_HASH_AUTH, exclude_credentials=True -) +DEVICE_CONFIG_DICT_AUTH = DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True) +DEVICE_CONFIG_DICT_AUTH2 = DEVICE_CONFIG_AUTH2.to_dict(exclude_credentials=True) CREATE_ENTRY_DATA_LEGACY = { CONF_HOST: IP_ADDRESS, @@ -92,14 +87,20 @@ CREATE_ENTRY_DATA_AUTH = { CONF_HOST: IP_ADDRESS, CONF_ALIAS: ALIAS, CONF_MODEL: MODEL, + CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AUTH, CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH, } CREATE_ENTRY_DATA_AUTH2 = { CONF_HOST: IP_ADDRESS2, CONF_ALIAS: ALIAS, CONF_MODEL: MODEL, + CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AUTH, CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH2, } +NEW_CONNECTION_TYPE = DeviceConnectionParameters( + DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Aes +) +NEW_CONNECTION_TYPE_DICT = NEW_CONNECTION_TYPE.to_dict() def _load_feature_fixtures(): diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index 7560ff4a72d..e9ae7957520 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -14,8 +14,12 @@ from homeassistant.components.tplink import ( DeviceConfig, KasaException, ) -from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG -from homeassistant.config_entries import ConfigEntryState +from homeassistant.components.tplink.const import ( + CONF_CONNECTION_TYPE, + CONF_CREDENTIALS_HASH, + CONF_DEVICE_CONFIG, +) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( CONF_ALIAS, CONF_DEVICE, @@ -32,6 +36,7 @@ from . import ( CREATE_ENTRY_DATA_AUTH, CREATE_ENTRY_DATA_AUTH2, CREATE_ENTRY_DATA_LEGACY, + CREDENTIALS_HASH_AUTH, DEFAULT_ENTRY_TITLE, DEVICE_CONFIG_DICT_AUTH, DEVICE_CONFIG_DICT_LEGACY, @@ -40,6 +45,7 @@ from . import ( MAC_ADDRESS, MAC_ADDRESS2, MODULE, + NEW_CONNECTION_TYPE_DICT, _mocked_device, _patch_connect, _patch_discovery, @@ -811,6 +817,77 @@ async def test_integration_discovery_with_ip_change( mock_connect["connect"].assert_awaited_once_with(config=config) +async def test_integration_discovery_with_connection_change( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test that config entry is updated with new device config. + + And that connection_hash is removed as it will be invalid. + """ + mock_connect["connect"].side_effect = KasaException() + + mock_config_entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=CREATE_ENTRY_DATA_AUTH, + unique_id=MAC_ADDRESS, + ) + mock_config_entry.add_to_hass(hass) + with patch("homeassistant.components.tplink.Discover.discover", return_value={}): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + assert ( + len( + hass.config_entries.flow.async_progress_by_handler( + DOMAIN, match_context={"source": SOURCE_REAUTH} + ) + ) + == 0 + ) + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AUTH + assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.1" + assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_AUTH + + NEW_DEVICE_CONFIG = { + **DEVICE_CONFIG_DICT_AUTH, + CONF_CONNECTION_TYPE: NEW_CONNECTION_TYPE_DICT, + } + config = DeviceConfig.from_dict(NEW_DEVICE_CONFIG) + # Reset the connect mock so when the config flow reloads the entry it succeeds + mock_connect["connect"].reset_mock(side_effect=True) + bulb = _mocked_device( + device_config=config, + mac=mock_config_entry.unique_id, + ) + mock_connect["connect"].return_value = bulb + + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: "127.0.0.1", + CONF_MAC: MAC_ADDRESS, + CONF_ALIAS: ALIAS, + CONF_DEVICE_CONFIG: NEW_DEVICE_CONFIG, + }, + ) + await hass.async_block_till_done(wait_background_tasks=True) + assert discovery_result["type"] is FlowResultType.ABORT + assert discovery_result["reason"] == "already_configured" + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == NEW_DEVICE_CONFIG + assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" + assert CREDENTIALS_HASH_AUTH not in mock_config_entry.data + + assert mock_config_entry.state is ConfigEntryState.LOADED + + mock_connect["connect"].assert_awaited_once_with(config=config) + + async def test_dhcp_discovery_with_ip_change( hass: HomeAssistant, mock_config_entry: MockConfigEntry, diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 61ec9decc10..bfb7e02b63d 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -7,12 +7,16 @@ from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from freezegun.api import FrozenDateTimeFactory -from kasa import AuthenticationError, Feature, KasaException, Module +from kasa import AuthenticationError, DeviceConfig, Feature, KasaException, Module import pytest from homeassistant import setup from homeassistant.components import tplink -from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG, DOMAIN +from homeassistant.components.tplink.const import ( + CONF_CREDENTIALS_HASH, + CONF_DEVICE_CONFIG, + DOMAIN, +) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( CONF_AUTHENTICATION, @@ -458,7 +462,214 @@ async def test_unlink_devices( expected_identifiers = identifiers[:expected_count] assert device_entries[0].identifiers == set(expected_identifiers) assert entry.version == 1 - assert entry.minor_version == 3 + assert entry.minor_version == 4 msg = f"{expected_message} identifiers for device dummy (hs300): {set(identifiers)}" assert msg in caplog.text + + +async def test_move_credentials_hash( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test credentials hash moved to parent. + + As async_setup_entry will succeed the hash on the parent is updated + from the device. + """ + device_config = { + **DEVICE_CONFIG_AUTH.to_dict( + exclude_credentials=True, credentials_hash="theHash" + ) + } + entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + entry_id="123456", + unique_id=MAC_ADDRESS, + version=1, + minor_version=3, + ) + assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash" + entry.add_to_hass(hass) + + async def _connect(config): + config.credentials_hash = "theNewHash" + return _mocked_device(device_config=config, credentials_hash="theNewHash") + + with ( + patch("homeassistant.components.tplink.Device.connect", new=_connect), + patch("homeassistant.components.tplink.PLATFORMS", []), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.minor_version == 4 + assert entry.state is ConfigEntryState.LOADED + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + assert CONF_CREDENTIALS_HASH in entry.data + # Gets the new hash from the successful connection. + assert entry.data[CONF_CREDENTIALS_HASH] == "theNewHash" + assert "Migration to version 1.4 complete" in caplog.text + + +async def test_move_credentials_hash_auth_error( + hass: HomeAssistant, +) -> None: + """Test credentials hash moved to parent. + + If there is an auth error it should be deleted after migration + in async_setup_entry. + """ + device_config = { + **DEVICE_CONFIG_AUTH.to_dict( + exclude_credentials=True, credentials_hash="theHash" + ) + } + entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + version=1, + minor_version=3, + ) + assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash" + + with ( + patch( + "homeassistant.components.tplink.Device.connect", + side_effect=AuthenticationError, + ), + patch("homeassistant.components.tplink.PLATFORMS", []), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.minor_version == 4 + assert entry.state is ConfigEntryState.SETUP_ERROR + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + # Auth failure deletes the hash + assert CONF_CREDENTIALS_HASH not in entry.data + + +async def test_move_credentials_hash_other_error( + hass: HomeAssistant, +) -> None: + """Test credentials hash moved to parent. + + When there is a KasaException the same hash should still be on the parent + at the end of the test. + """ + device_config = { + **DEVICE_CONFIG_AUTH.to_dict( + exclude_credentials=True, credentials_hash="theHash" + ) + } + entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + version=1, + minor_version=3, + ) + assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash" + + with ( + patch( + "homeassistant.components.tplink.Device.connect", side_effect=KasaException + ), + patch("homeassistant.components.tplink.PLATFORMS", []), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.minor_version == 4 + assert entry.state is ConfigEntryState.SETUP_RETRY + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + assert CONF_CREDENTIALS_HASH in entry.data + assert entry.data[CONF_CREDENTIALS_HASH] == "theHash" + + +async def test_credentials_hash( + hass: HomeAssistant, +) -> None: + """Test credentials_hash used to call connect.""" + device_config = {**DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True)} + entry_data = { + **CREATE_ENTRY_DATA_AUTH, + CONF_DEVICE_CONFIG: device_config, + CONF_CREDENTIALS_HASH: "theHash", + } + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + ) + + async def _connect(config): + config.credentials_hash = "theHash" + return _mocked_device(device_config=config, credentials_hash="theHash") + + with ( + patch("homeassistant.components.tplink.PLATFORMS", []), + patch("homeassistant.components.tplink.Device.connect", new=_connect), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + assert CONF_CREDENTIALS_HASH in entry.data + assert entry.data[CONF_DEVICE_CONFIG] == device_config + assert entry.data[CONF_CREDENTIALS_HASH] == "theHash" + + +async def test_credentials_hash_auth_error( + hass: HomeAssistant, +) -> None: + """Test credentials_hash is deleted after an auth failure.""" + device_config = {**DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True)} + entry_data = { + **CREATE_ENTRY_DATA_AUTH, + CONF_DEVICE_CONFIG: device_config, + CONF_CREDENTIALS_HASH: "theHash", + } + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + ) + + with ( + patch("homeassistant.components.tplink.PLATFORMS", []), + patch( + "homeassistant.components.tplink.Device.connect", + side_effect=AuthenticationError, + ) as connect_mock, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + expected_config = DeviceConfig.from_dict( + DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True, credentials_hash="theHash") + ) + connect_mock.assert_called_with(config=expected_config) + assert entry.state is ConfigEntryState.SETUP_ERROR + assert CONF_CREDENTIALS_HASH not in entry.data From 9758b080366dccb7eff30ddeec4df56605cf1562 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:54:34 +0100 Subject: [PATCH 0320/2411] Update tplink unlink identifiers to deal with ids from other domains (#120596) --- homeassistant/components/tplink/__init__.py | 98 +++++++++++++-------- tests/components/tplink/__init__.py | 1 + tests/components/tplink/test_init.py | 82 ++++++++++++----- 3 files changed, 123 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 6d300f68aa0..83cfc733716 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from datetime import timedelta import logging from typing import Any @@ -282,6 +283,28 @@ def mac_alias(mac: str) -> str: return mac.replace(":", "")[-4:].upper() +def _mac_connection_or_none(device: dr.DeviceEntry) -> str | None: + return next( + ( + conn + for type_, conn in device.connections + if type_ == dr.CONNECTION_NETWORK_MAC + ), + None, + ) + + +def _device_id_is_mac_or_none(mac: str, device_ids: Iterable[str]) -> str | None: + # Previously only iot devices had child devices and iot devices use + # the upper and lcase MAC addresses as device_id so match on case + # insensitive mac address as the parent device. + upper_mac = mac.upper() + return next( + (device_id for device_id in device_ids if device_id.upper() == upper_mac), + None, + ) + + async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old entry.""" version = config_entry.version @@ -298,49 +321,48 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # always be linked into one device. dev_reg = dr.async_get(hass) for device in dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id): - new_identifiers: set[tuple[str, str]] | None = None - if len(device.identifiers) > 1 and ( - mac := next( - iter( - [ - conn[1] - for conn in device.connections - if conn[0] == dr.CONNECTION_NETWORK_MAC - ] - ), - None, + original_identifiers = device.identifiers + # Get only the tplink identifier, could be tapo or other integrations. + tplink_identifiers = [ + ident[1] for ident in original_identifiers if ident[0] == DOMAIN + ] + # Nothing to fix if there's only one identifier. mac connection + # should never be none but if it is there's no problem. + if len(tplink_identifiers) <= 1 or not ( + mac := _mac_connection_or_none(device) + ): + continue + if not ( + tplink_parent_device_id := _device_id_is_mac_or_none( + mac, tplink_identifiers ) ): - for identifier in device.identifiers: - # Previously only iot devices that use the MAC address as - # device_id had child devices so check for mac as the - # parent device. - if identifier[0] == DOMAIN and identifier[1].upper() == mac.upper(): - new_identifiers = {identifier} - break - if new_identifiers: - dev_reg.async_update_device( - device.id, new_identifiers=new_identifiers - ) - _LOGGER.debug( - "Replaced identifiers for device %s (%s): %s with: %s", - device.name, - device.model, - device.identifiers, - new_identifiers, - ) - else: - # No match on mac so raise an error. - _LOGGER.error( - "Unable to replace identifiers for device %s (%s): %s", - device.name, - device.model, - device.identifiers, - ) + # No match on mac so raise an error. + _LOGGER.error( + "Unable to replace identifiers for device %s (%s): %s", + device.name, + device.model, + device.identifiers, + ) + continue + # Retain any identifiers for other domains + new_identifiers = { + ident for ident in device.identifiers if ident[0] != DOMAIN + } + new_identifiers.add((DOMAIN, tplink_parent_device_id)) + dev_reg.async_update_device(device.id, new_identifiers=new_identifiers) + _LOGGER.debug( + "Replaced identifiers for device %s (%s): %s with: %s", + device.name, + device.model, + original_identifiers, + new_identifiers, + ) minor_version = 3 hass.config_entries.async_update_entry(config_entry, minor_version=3) - _LOGGER.debug("Migration to version %s.%s successful", version, minor_version) + + _LOGGER.debug("Migration to version %s.%s complete", version, minor_version) if version == 1 and minor_version == 3: # credentials_hash stored in the device_config should be moved to data. diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index b3092d62904..d12858017cc 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -49,6 +49,7 @@ ALIAS = "My Bulb" MODEL = "HS100" MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" DEVICE_ID = "123456789ABCDEFGH" +DEVICE_ID_MAC = "AA:BB:CC:DD:EE:FF" DHCP_FORMATTED_MAC_ADDRESS = MAC_ADDRESS.replace(":", "") MAC_ADDRESS2 = "11:22:33:44:55:66" DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index bfb7e02b63d..c5c5e2ce6db 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -36,6 +36,8 @@ from . import ( CREATE_ENTRY_DATA_AUTH, CREATE_ENTRY_DATA_LEGACY, DEVICE_CONFIG_AUTH, + DEVICE_ID, + DEVICE_ID_MAC, IP_ADDRESS, MAC_ADDRESS, _mocked_device, @@ -404,19 +406,48 @@ async def test_feature_no_category( @pytest.mark.parametrize( - ("identifier_base", "expected_message", "expected_count"), + ("device_id", "id_count", "domains", "expected_message"), [ - pytest.param("C0:06:C3:42:54:2B", "Replaced", 1, id="success"), - pytest.param("123456789", "Unable to replace", 3, id="failure"), + pytest.param(DEVICE_ID_MAC, 1, [DOMAIN], None, id="mac-id-no-children"), + pytest.param(DEVICE_ID_MAC, 3, [DOMAIN], "Replaced", id="mac-id-children"), + pytest.param( + DEVICE_ID_MAC, + 1, + [DOMAIN, "other"], + None, + id="mac-id-no-children-other-domain", + ), + pytest.param( + DEVICE_ID_MAC, + 3, + [DOMAIN, "other"], + "Replaced", + id="mac-id-children-other-domain", + ), + pytest.param(DEVICE_ID, 1, [DOMAIN], None, id="not-mac-id-no-children"), + pytest.param( + DEVICE_ID, 3, [DOMAIN], "Unable to replace", id="not-mac-children" + ), + pytest.param( + DEVICE_ID, 1, [DOMAIN, "other"], None, id="not-mac-no-children-other-domain" + ), + pytest.param( + DEVICE_ID, + 3, + [DOMAIN, "other"], + "Unable to replace", + id="not-mac-children-other-domain", + ), ], ) async def test_unlink_devices( hass: HomeAssistant, device_registry: dr.DeviceRegistry, caplog: pytest.LogCaptureFixture, - identifier_base, + device_id, + id_count, + domains, expected_message, - expected_count, ) -> None: """Test for unlinking child device ids.""" entry = MockConfigEntry( @@ -429,43 +460,54 @@ async def test_unlink_devices( ) entry.add_to_hass(hass) - # Setup initial device registry, with linkages - mac = "C0:06:C3:42:54:2B" - identifiers = [ - (DOMAIN, identifier_base), - (DOMAIN, f"{identifier_base}_0001"), - (DOMAIN, f"{identifier_base}_0002"), + # Generate list of test identifiers + test_identifiers = [ + (domain, f"{device_id}{"" if i == 0 else f"_000{i}"}") + for i in range(id_count) + for domain in domains ] + update_msg_fragment = "identifiers for device dummy (hs300):" + update_msg = f"{expected_message} {update_msg_fragment}" if expected_message else "" + + # Expected identifiers should include all other domains or all the newer non-mac device ids + # or just the parent mac device id + expected_identifiers = [ + (domain, device_id) + for domain, device_id in test_identifiers + if domain != DOMAIN + or device_id.startswith(DEVICE_ID) + or device_id == DEVICE_ID_MAC + ] + device_registry.async_get_or_create( config_entry_id="123456", connections={ - (dr.CONNECTION_NETWORK_MAC, mac.lower()), + (dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS), }, - identifiers=set(identifiers), + identifiers=set(test_identifiers), model="hs300", name="dummy", ) device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) assert device_entries[0].connections == { - (dr.CONNECTION_NETWORK_MAC, mac.lower()), + (dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS), } - assert device_entries[0].identifiers == set(identifiers) + assert device_entries[0].identifiers == set(test_identifiers) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) - assert device_entries[0].connections == {(dr.CONNECTION_NETWORK_MAC, mac.lower())} - # If expected count is 1 will be the first identifier only - expected_identifiers = identifiers[:expected_count] + assert device_entries[0].connections == {(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)} + assert device_entries[0].identifiers == set(expected_identifiers) assert entry.version == 1 assert entry.minor_version == 4 - msg = f"{expected_message} identifiers for device dummy (hs300): {set(identifiers)}" - assert msg in caplog.text + assert update_msg in caplog.text + assert "Migration to version 1.3 complete" in caplog.text async def test_move_credentials_hash( From 389525296534355238c6b566eaac1b95e26fc144 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 15:00:14 +0200 Subject: [PATCH 0321/2411] Fix docstring for EventStateEventData (#120662) --- homeassistant/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 71ee5f4bd1d..c4392f62c52 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -159,7 +159,7 @@ class ConfigSource(enum.StrEnum): class EventStateEventData(TypedDict): - """Base class for EVENT_STATE_CHANGED and EVENT_STATE_CHANGED data.""" + """Base class for EVENT_STATE_CHANGED and EVENT_STATE_REPORTED data.""" entity_id: str new_state: State | None From e446875c7ea4d8e3f2c0d39a7b9f25eb82923aae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 27 Jun 2024 16:33:14 +0200 Subject: [PATCH 0322/2411] Improve type hints in esphome tests (#120674) --- tests/components/esphome/conftest.py | 3 ++- tests/components/esphome/test_config_flow.py | 27 +++++++++++--------- tests/components/esphome/test_dashboard.py | 9 +++++-- tests/components/esphome/test_diagnostics.py | 3 ++- tests/components/esphome/test_update.py | 13 +++++----- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index ac1558b8aa0..8a069d257d8 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -24,6 +24,7 @@ from aioesphomeapi import ( VoiceAssistantFeature, ) import pytest +from typing_extensions import AsyncGenerator from zeroconf import Zeroconf from homeassistant.components.esphome import dashboard @@ -175,7 +176,7 @@ def mock_client(mock_device_info) -> APIClient: @pytest.fixture -async def mock_dashboard(hass): +async def mock_dashboard(hass: HomeAssistant) -> AsyncGenerator[dict[str, Any]]: """Mock dashboard.""" data = {"configured": [], "importable": []} with patch( diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 9a2b1f1a80e..68af6665380 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -2,6 +2,7 @@ from ipaddress import ip_address import json +from typing import Any from unittest.mock import AsyncMock, patch from aioesphomeapi import ( @@ -329,7 +330,7 @@ async def test_user_invalid_password(hass: HomeAssistant, mock_client) -> None: async def test_user_dashboard_has_wrong_key( hass: HomeAssistant, mock_client, - mock_dashboard, + mock_dashboard: dict[str, Any], mock_setup_entry: None, ) -> None: """Test user step with key from dashboard that is incorrect.""" @@ -376,7 +377,7 @@ async def test_user_dashboard_has_wrong_key( async def test_user_discovers_name_and_gets_key_from_dashboard( hass: HomeAssistant, mock_client, - mock_dashboard, + mock_dashboard: dict[str, Any], mock_setup_entry: None, ) -> None: """Test user step can discover the name and get the key from the dashboard.""" @@ -429,7 +430,7 @@ async def test_user_discovers_name_and_gets_key_from_dashboard_fails( hass: HomeAssistant, dashboard_exception: Exception, mock_client, - mock_dashboard, + mock_dashboard: dict[str, Any], mock_setup_entry: None, ) -> None: """Test user step can discover the name and get the key from the dashboard.""" @@ -484,7 +485,7 @@ async def test_user_discovers_name_and_gets_key_from_dashboard_fails( async def test_user_discovers_name_and_dashboard_is_unavailable( hass: HomeAssistant, mock_client, - mock_dashboard, + mock_dashboard: dict[str, Any], mock_setup_entry: None, ) -> None: """Test user step can discover the name but the dashboard is unavailable.""" @@ -843,7 +844,7 @@ async def test_reauth_confirm_valid( async def test_reauth_fixed_via_dashboard( hass: HomeAssistant, mock_client, - mock_dashboard, + mock_dashboard: dict[str, Any], mock_setup_entry: None, ) -> None: """Test reauth fixed automatically via dashboard.""" @@ -894,7 +895,7 @@ async def test_reauth_fixed_via_dashboard( async def test_reauth_fixed_via_dashboard_add_encryption_remove_password( hass: HomeAssistant, mock_client, - mock_dashboard, + mock_dashboard: dict[str, Any], mock_config_entry, mock_setup_entry: None, ) -> None: @@ -938,7 +939,7 @@ async def test_reauth_fixed_via_remove_password( hass: HomeAssistant, mock_client, mock_config_entry, - mock_dashboard, + mock_dashboard: dict[str, Any], mock_setup_entry: None, ) -> None: """Test reauth fixed automatically by seeing password removed.""" @@ -962,7 +963,7 @@ async def test_reauth_fixed_via_remove_password( async def test_reauth_fixed_via_dashboard_at_confirm( hass: HomeAssistant, mock_client, - mock_dashboard, + mock_dashboard: dict[str, Any], mock_setup_entry: None, ) -> None: """Test reauth fixed automatically via dashboard at confirm step.""" @@ -1153,7 +1154,9 @@ async def test_discovery_dhcp_no_changes( assert entry.data[CONF_HOST] == "192.168.43.183" -async def test_discovery_hassio(hass: HomeAssistant, mock_dashboard) -> None: +async def test_discovery_hassio( + hass: HomeAssistant, mock_dashboard: dict[str, Any] +) -> None: """Test dashboard discovery.""" result = await hass.config_entries.flow.async_init( "esphome", @@ -1181,7 +1184,7 @@ async def test_discovery_hassio(hass: HomeAssistant, mock_dashboard) -> None: async def test_zeroconf_encryption_key_via_dashboard( hass: HomeAssistant, mock_client, - mock_dashboard, + mock_dashboard: dict[str, Any], mock_setup_entry: None, ) -> None: """Test encryption key retrieved from dashboard.""" @@ -1247,7 +1250,7 @@ async def test_zeroconf_encryption_key_via_dashboard( async def test_zeroconf_encryption_key_via_dashboard_with_api_encryption_prop( hass: HomeAssistant, mock_client, - mock_dashboard, + mock_dashboard: dict[str, Any], mock_setup_entry: None, ) -> None: """Test encryption key retrieved from dashboard with api_encryption property set.""" @@ -1313,7 +1316,7 @@ async def test_zeroconf_encryption_key_via_dashboard_with_api_encryption_prop( async def test_zeroconf_no_encryption_key_via_dashboard( hass: HomeAssistant, mock_client, - mock_dashboard, + mock_dashboard: dict[str, Any], mock_setup_entry: None, ) -> None: """Test encryption key not retrieved from dashboard.""" diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index 1b0303a8a48..da805eb2eee 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -16,7 +16,10 @@ from tests.common import MockConfigEntry async def test_dashboard_storage( - hass: HomeAssistant, init_integration, mock_dashboard, hass_storage: dict[str, Any] + hass: HomeAssistant, + init_integration, + mock_dashboard: dict[str, Any], + hass_storage: dict[str, Any], ) -> None: """Test dashboard storage.""" assert hass_storage[dashboard.STORAGE_KEY]["data"] == { @@ -197,7 +200,9 @@ async def test_new_dashboard_fix_reauth( assert mock_config_entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK -async def test_dashboard_supports_update(hass: HomeAssistant, mock_dashboard) -> None: +async def test_dashboard_supports_update( + hass: HomeAssistant, mock_dashboard: dict[str, Any] +) -> None: """Test dashboard supports update.""" dash = dashboard.async_get_dashboard(hass) diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index 4fb8f993aca..03689a5699e 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -1,5 +1,6 @@ """Tests for the diagnostics data provided by the ESPHome integration.""" +from typing import Any from unittest.mock import ANY import pytest @@ -20,7 +21,7 @@ async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, init_integration: MockConfigEntry, - mock_dashboard, + mock_dashboard: dict[str, Any], snapshot: SnapshotAssertion, ) -> None: """Test diagnostics for config entry.""" diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index fc845299142..992a6ad2ba9 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -1,6 +1,7 @@ """Test ESPHome update entities.""" from collections.abc import Awaitable, Callable +from typing import Any from unittest.mock import Mock, patch from aioesphomeapi import ( @@ -84,7 +85,7 @@ async def test_update_entity( stub_reconnect, mock_config_entry, mock_device_info, - mock_dashboard, + mock_dashboard: dict[str, Any], devices_payload, expected_state, expected_attributes, @@ -190,7 +191,7 @@ async def test_update_static_info( [APIClient, list[EntityInfo], list[UserService], list[EntityState]], Awaitable[MockESPHomeDevice], ], - mock_dashboard, + mock_dashboard: dict[str, Any], ) -> None: """Test ESPHome update entity.""" mock_dashboard["configured"] = [ @@ -236,7 +237,7 @@ async def test_update_device_state_for_availability( expected_disconnect: bool, expected_state: str, has_deep_sleep: bool, - mock_dashboard, + mock_dashboard: dict[str, Any], mock_client: APIClient, mock_esphome_device: Callable[ [APIClient, list[EntityInfo], list[UserService], list[EntityState]], @@ -272,7 +273,7 @@ async def test_update_entity_dashboard_not_available_startup( stub_reconnect, mock_config_entry, mock_device_info, - mock_dashboard, + mock_dashboard: dict[str, Any], ) -> None: """Test ESPHome update entity when dashboard is not available at startup.""" with ( @@ -321,7 +322,7 @@ async def test_update_entity_dashboard_discovered_after_startup_but_update_faile [APIClient, list[EntityInfo], list[UserService], list[EntityState]], Awaitable[MockESPHomeDevice], ], - mock_dashboard, + mock_dashboard: dict[str, Any], ) -> None: """Test ESPHome update entity when dashboard is discovered after startup and the first update fails.""" with patch( @@ -386,7 +387,7 @@ async def test_update_becomes_available_at_runtime( [APIClient, list[EntityInfo], list[UserService], list[EntityState]], Awaitable[MockESPHomeDevice], ], - mock_dashboard, + mock_dashboard: dict[str, Any], ) -> None: """Test ESPHome update entity when the dashboard has no device at startup but gets them later.""" await mock_esphome_device( From b9e01b92536a21224dbf0081c7b0cf0af1e8c394 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 27 Jun 2024 13:08:18 -0400 Subject: [PATCH 0323/2411] Bump Environment Canada to 0.7.0 (#120686) --- .../components/environment_canada/manifest.json | 2 +- homeassistant/components/environment_canada/sensor.py | 9 --------- homeassistant/components/environment_canada/strings.json | 3 --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 3 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index a0bdd5d4919..69a6cd7c69b 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.6.3"] + "requirements": ["env-canada==0.7.0"] } diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 8a734f74dd6..1a5d096203d 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -19,7 +19,6 @@ from homeassistant.const import ( PERCENTAGE, UV_INDEX, UnitOfLength, - UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, UnitOfTemperature, @@ -114,14 +113,6 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: data.conditions.get("pop", {}).get("value"), ), - ECSensorEntityDescription( - key="precip_yesterday", - translation_key="precip_yesterday", - device_class=SensorDeviceClass.PRECIPITATION, - native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda data: data.conditions.get("precip_yesterday", {}).get("value"), - ), ECSensorEntityDescription( key="pressure", translation_key="pressure", diff --git a/homeassistant/components/environment_canada/strings.json b/homeassistant/components/environment_canada/strings.json index fc03550b64e..28ca55c6195 100644 --- a/homeassistant/components/environment_canada/strings.json +++ b/homeassistant/components/environment_canada/strings.json @@ -52,9 +52,6 @@ "pop": { "name": "Chance of precipitation" }, - "precip_yesterday": { - "name": "Precipitation yesterday" - }, "pressure": { "name": "Barometric pressure" }, diff --git a/requirements_all.txt b/requirements_all.txt index 06e5b6ef223..faa49266016 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -816,7 +816,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.6.3 +env-canada==0.7.0 # homeassistant.components.season ephem==4.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 58691727bec..089c66173eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ energyzero==2.1.0 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.6.3 +env-canada==0.7.0 # homeassistant.components.season ephem==4.1.5 From 0c910bc000e39422f97d6c1543c99215b4aa417e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 12:20:37 -0500 Subject: [PATCH 0324/2411] Add newer models to unifi integrations discovery (#120688) --- homeassistant/components/unifi/manifest.json | 4 ++++ homeassistant/components/unifiprotect/manifest.json | 4 ++++ homeassistant/generated/ssdp.py | 8 ++++++++ 3 files changed, 16 insertions(+) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index f4bfaec2d42..aa9b553cb67 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -21,6 +21,10 @@ { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE" + }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max" } ] } diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index bdbdacae90e..d0b4947f8fe 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -53,6 +53,10 @@ { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE" + }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max" } ] } diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 8e7319917f0..9ed65bab868 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -297,6 +297,10 @@ SSDP = { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE", }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max", + }, ], "unifiprotect": [ { @@ -311,6 +315,10 @@ SSDP = { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE", }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max", + }, ], "upnp": [ { From f3c76cd6983b8ef5dff5e5c580b2d3b93d0c9eea Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 27 Jun 2024 19:37:43 +0200 Subject: [PATCH 0325/2411] Split mqtt client tests (#120636) --- tests/components/mqtt/test_client.py | 1980 ++++++++++++++++++++++++++ tests/components/mqtt/test_init.py | 1962 +------------------------ 2 files changed, 1983 insertions(+), 1959 deletions(-) create mode 100644 tests/components/mqtt/test_client.py diff --git a/tests/components/mqtt/test_client.py b/tests/components/mqtt/test_client.py new file mode 100644 index 00000000000..49b590383d1 --- /dev/null +++ b/tests/components/mqtt/test_client.py @@ -0,0 +1,1980 @@ +"""The tests for the MQTT client.""" + +import asyncio +from datetime import datetime, timedelta +import socket +import ssl +from typing import Any +from unittest.mock import MagicMock, Mock, call, patch + +import certifi +import paho.mqtt.client as paho_mqtt +import pytest + +from homeassistant.components import mqtt +from homeassistant.components.mqtt.client import RECONNECT_INTERVAL_SECONDS +from homeassistant.components.mqtt.models import MessageCallbackType, ReceiveMessage +from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState +from homeassistant.const import ( + CONF_PROTOCOL, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + UnitOfTemperature, +) +from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.dt import utcnow + +from .conftest import ENTRY_DEFAULT_BIRTH_MESSAGE +from .test_common import help_all_subscribe_calls + +from tests.common import ( + MockConfigEntry, + async_fire_mqtt_message, + async_fire_time_changed, +) +from tests.typing import MqttMockHAClient, MqttMockHAClientGenerator, MqttMockPahoClient + + +@pytest.fixture(autouse=True) +def mock_storage(hass_storage: dict[str, Any]) -> None: + """Autouse hass_storage for the TestCase tests.""" + + +def help_assert_message( + msg: ReceiveMessage, + topic: str | None = None, + payload: str | None = None, + qos: int | None = None, + retain: bool | None = None, +) -> bool: + """Return True if all of the given attributes match with the message.""" + match: bool = True + if topic is not None: + match &= msg.topic == topic + if payload is not None: + match &= msg.payload == payload + if qos is not None: + match &= msg.qos == qos + if retain is not None: + match &= msg.retain == retain + return match + + +async def test_mqtt_connects_on_home_assistant_mqtt_setup( + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient +) -> None: + """Test if client is connected after mqtt init on bootstrap.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + +async def test_mqtt_does_not_disconnect_on_home_assistant_stop( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test if client is not disconnected on HA stop.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + hass.bus.fire(EVENT_HOMEASSISTANT_STOP) + await mock_debouncer.wait() + assert mqtt_client_mock.disconnect.call_count == 0 + + +async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: + """Test if ACK is awaited correctly when disconnecting.""" + + class FakeInfo: + """Returns a simulated client publish response.""" + + mid = 100 + rc = 0 + + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mqtt_client = mock_client.return_value + mqtt_client.connect = MagicMock( + return_value=0, + side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( + mqtt_client.on_connect, mqtt_client, None, 0, 0, 0 + ), + ) + mqtt_client.publish = MagicMock(return_value=FakeInfo()) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={ + "certificate": "auto", + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_DISCOVERY: False, + }, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + + 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, + ) + await hass.async_block_till_done(wait_background_tasks=True) + + +@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) +async def test_publish( + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient +) -> None: + """Test the publish function.""" + publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish + await mqtt.async_publish(hass, "test-topic", "test-payload") + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic", + "test-payload", + 0, + False, + ) + publish_mock.reset_mock() + + await mqtt.async_publish(hass, "test-topic", "test-payload", 2, True) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic", + "test-payload", + 2, + True, + ) + publish_mock.reset_mock() + + mqtt.publish(hass, "test-topic2", "test-payload2") + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic2", + "test-payload2", + 0, + False, + ) + publish_mock.reset_mock() + + mqtt.publish(hass, "test-topic2", "test-payload2", 2, True) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic2", + "test-payload2", + 2, + True, + ) + publish_mock.reset_mock() + + # test binary pass-through + mqtt.publish( + hass, + "test-topic3", + b"\xde\xad\xbe\xef", + 0, + False, + ) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic3", + b"\xde\xad\xbe\xef", + 0, + False, + ) + publish_mock.reset_mock() + + # test null payload + mqtt.publish( + hass, + "test-topic3", + None, + 0, + False, + ) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic3", + None, + 0, + False, + ) + + publish_mock.reset_mock() + + +async def test_convert_outgoing_payload(hass: HomeAssistant) -> None: + """Test the converting of outgoing MQTT payloads without template.""" + command_template = mqtt.MqttCommandTemplate(None, hass=hass) + assert command_template.async_render(b"\xde\xad\xbe\xef") == b"\xde\xad\xbe\xef" + assert ( + command_template.async_render("b'\\xde\\xad\\xbe\\xef'") + == "b'\\xde\\xad\\xbe\\xef'" + ) + assert command_template.async_render(1234) == 1234 + assert command_template.async_render(1234.56) == 1234.56 + assert command_template.async_render(None) is None + + +async def test_all_subscriptions_run_when_decode_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test all other subscriptions still run when decode fails for one.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic", record_calls, encoding="ascii") + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + async_fire_mqtt_message(hass, "test-topic", UnitOfTemperature.CELSIUS) + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + + +async def test_subscribe_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of a topic.""" + await mqtt_mock_entry() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + + unsub() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + + # Cannot unsubscribe twice + with pytest.raises(HomeAssistantError): + unsub() + + +@pytest.mark.usefixtures("mqtt_mock_entry") +async def test_subscribe_topic_not_initialize( + hass: HomeAssistant, record_calls: MessageCallbackType +) -> None: + """Test the subscription of a topic when MQTT was not initialized.""" + with pytest.raises( + HomeAssistantError, match=r".*make sure MQTT is set up correctly" + ): + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + +async def test_subscribe_mqtt_config_entry_disabled( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, record_calls: MessageCallbackType +) -> None: + """Test the subscription of a topic when MQTT config entry is disabled.""" + mqtt_mock.connected = True + + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + assert mqtt_config_entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) + assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED + + await hass.config_entries.async_set_disabled_by( + mqtt_config_entry.entry_id, ConfigEntryDisabler.USER + ) + mqtt_mock.connected = False + + with pytest.raises(HomeAssistantError, match=r".*MQTT is not enabled"): + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + +async def test_subscribe_and_resubscribe( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test resubscribing within the debounce time.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + with ( + patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.4), + patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.4), + ): + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + # This unsub will be un-done with the following subscribe + # unsubscribe should not be called at the broker + unsub() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + await mock_debouncer.wait() + mock_debouncer.clear() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + # assert unsubscribe was not called + mqtt_client_mock.unsubscribe.assert_not_called() + + mock_debouncer.clear() + unsub() + + await mock_debouncer.wait() + mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) + + +async def test_subscribe_topic_non_async( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of a topic using the non-async function.""" + await mqtt_mock_entry() + await mock_debouncer.wait() + mock_debouncer.clear() + unsub = await hass.async_add_executor_job( + mqtt.subscribe, hass, "test-topic", record_calls + ) + await mock_debouncer.wait() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + + mock_debouncer.clear() + await hass.async_add_executor_job(unsub) + await mock_debouncer.wait() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + assert len(recorded_calls) == 1 + + +async def test_subscribe_bad_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of a topic.""" + await mqtt_mock_entry() + with pytest.raises(HomeAssistantError): + await mqtt.async_subscribe(hass, 55, record_calls) # type: ignore[arg-type] + + +async def test_subscribe_topic_not_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test if subscribed topic is not a match.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + async_fire_mqtt_message(hass, "another-test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) + + async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic/bier/on" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_level_wildcard_no_subtree_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) + + async_fire_mqtt_message(hass, "test-topic/bier", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "test-topic-123", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_subtree_wildcard_subtree_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic/bier/on" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_subtree_wildcard_root_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_subtree_wildcard_no_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "another-test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "hi/test-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/test-topic/here-iam", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "hi/test-topic/here-iam" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/here-iam/test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard_and_wildcard_no_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/another-test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_sys_root( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of $ root topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "$test-topic/subtree/on", record_calls) + + async_fire_mqtt_message(hass, "$test-topic/subtree/on", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "$test-topic/subtree/on" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_sys_root_and_wildcard_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of $ root and wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "$test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "$test-topic/some-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "$test-topic/some-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of $ root and wildcard subtree topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "$test-topic/subtree/#", record_calls) + + async_fire_mqtt_message(hass, "$test-topic/subtree/some-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "$test-topic/subtree/some-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_special_characters( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription to topics with special characters.""" + await mqtt_mock_entry() + topic = "/test-topic/$(.)[^]{-}" + payload = "p4y.l[]a|> ?" + + await mqtt.async_subscribe(hass, topic, record_calls) + + async_fire_mqtt_message(hass, topic, payload) + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == topic + assert recorded_calls[0].payload == payload + + +async def test_subscribe_same_topic( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test subscribing to same topic twice and simulate retained messages. + + When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again + for it to resend any retained messages. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_a, qos=0) + # Simulate a non retained message after the first subscription + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + await mock_debouncer.wait() + assert len(calls_a) == 1 + mqtt_client_mock.subscribe.assert_called() + calls_a = [] + mqtt_client_mock.reset_mock() + + await hass.async_block_till_done() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_b, qos=1) + # Simulate an other non retained message after the second subscription + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + await mock_debouncer.wait() + # Both subscriptions should receive updates + assert len(calls_a) == 1 + assert len(calls_b) == 1 + mqtt_client_mock.subscribe.assert_called() + + +async def test_replaying_payload_same_topic( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test replaying retained messages. + + When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again + for it to resend any retained messages for new subscriptions. + Retained messages must only be replayed for new subscriptions, except + when the MQTT client is reconnecting. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_a) + await mock_debouncer.wait() + async_fire_mqtt_message( + hass, "test/state", "online", qos=0, retain=True + ) # Simulate a (retained) message played back + assert len(calls_a) == 1 + mqtt_client_mock.subscribe.assert_called() + calls_a = [] + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_b) + await mock_debouncer.wait() + + # Simulate edge case where non retained message was received + # after subscription at HA but before the debouncer delay was passed. + # The message without retain flag directly after a subscription should + # be processed by both subscriptions. + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + + # Simulate a (retained) message played back on new subscriptions + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + + # The current subscription only received the message without retain flag + assert len(calls_a) == 1 + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) + # The retained message playback should only be processed by the new subscription. + # The existing subscription already got the latest update, hence the existing + # subscription should not receive the replayed (retained) message. + # Messages without retain flag are received on both subscriptions. + assert len(calls_b) == 2 + assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) + assert help_assert_message(calls_b[1], "test/state", "online", qos=0, retain=True) + mqtt_client_mock.subscribe.assert_called() + + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + + # Simulate new message played back on new subscriptions + # After connecting the retain flag will not be set, even if the + # payload published was retained, we cannot see that + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + assert len(calls_a) == 1 + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) + assert len(calls_b) == 1 + assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) + + # Now simulate the broker was disconnected shortly + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + # Simulate a (retained) message played back after reconnecting + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + # Both subscriptions now should replay the retained message + assert len(calls_a) == 1 + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) + assert len(calls_b) == 1 + assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=True) + + +async def test_replaying_payload_after_resubscribing( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test replaying and filtering retained messages after resubscribing. + + When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again + for it to resend any retained messages for new subscriptions. + Retained messages must only be replayed for new subscriptions, except + when the MQTT client is reconnection. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + + # Simulate a (retained) message played back + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) + calls_a.clear() + + # Test we get updates + async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=False) + assert help_assert_message(calls_a[0], "test/state", "offline", qos=0, retain=False) + calls_a.clear() + + # Test we filter new retained updates + async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=True) + await hass.async_block_till_done() + assert len(calls_a) == 0 + + # Unsubscribe an resubscribe again + mock_debouncer.clear() + unsub() + unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + + # Simulate we can receive a (retained) played back message again + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) + + +async def test_replaying_payload_wildcard_topic( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test replaying retained messages. + + When we have multiple subscriptions to the same wildcard topic, + SUBSCRIBE must be sent to the broker again + for it to resend any retained messages for new subscriptions. + Retained messages should only be replayed for new subscriptions, except + when the MQTT client is reconnection. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/#", _callback_a) + await mock_debouncer.wait() + # Simulate (retained) messages being played back on new subscriptions + async_fire_mqtt_message(hass, "test/state1", "new_value_1", qos=0, retain=True) + async_fire_mqtt_message(hass, "test/state2", "new_value_2", qos=0, retain=True) + assert len(calls_a) == 2 + mqtt_client_mock.subscribe.assert_called() + calls_a = [] + mqtt_client_mock.reset_mock() + + # resubscribe to the wild card topic again + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/#", _callback_b) + await mock_debouncer.wait() + # Simulate (retained) messages being played back on new subscriptions + async_fire_mqtt_message(hass, "test/state1", "initial_value_1", qos=0, retain=True) + async_fire_mqtt_message(hass, "test/state2", "initial_value_2", qos=0, retain=True) + # The retained messages playback should only be processed for the new subscriptions + assert len(calls_a) == 0 + assert len(calls_b) == 2 + mqtt_client_mock.subscribe.assert_called() + + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + + # Simulate new messages being received + async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=False) + async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=False) + assert len(calls_a) == 2 + assert len(calls_b) == 2 + + # Now simulate the broker was disconnected shortly + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + await mock_debouncer.wait() + + mqtt_client_mock.subscribe.assert_called() + # Simulate the (retained) messages are played back after reconnecting + # for all subscriptions + async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=True) + async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=True) + # Both subscriptions should replay + assert len(calls_a) == 2 + assert len(calls_b) == 2 + + +async def test_not_calling_unsubscribe_with_active_subscribers( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test not calling unsubscribe() when other subscribers are active.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) + await mqtt.async_subscribe(hass, "test/state", record_calls, 1) + await mock_debouncer.wait() + assert mqtt_client_mock.subscribe.called + + mock_debouncer.clear() + unsub() + await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + assert not mqtt_client_mock.unsubscribe.called + assert not mock_debouncer.is_set() + + +async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + mqtt_mock_entry: MqttMockHAClientGenerator, + record_calls: MessageCallbackType, +) -> None: + """Test not calling subscribe() when it is unsubscribed. + + Make sure subscriptions are cleared if unsubscribed before + the subscribe cool down period has ended. + """ + mqtt_mock = await mqtt_mock_entry() + mqtt_client_mock = mqtt_mock._mqttc + await mock_debouncer.wait() + + mock_debouncer.clear() + mqtt_client_mock.subscribe.reset_mock() + unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) + unsub() + await mock_debouncer.wait() + # The debouncer executes without an pending subscribes + assert not mqtt_client_mock.subscribe.called + + +async def test_unsubscribe_race( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test not calling unsubscribe() when other subscribers are active.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) + unsub() + await mqtt.async_subscribe(hass, "test/state", _callback_b) + await mock_debouncer.wait() + + async_fire_mqtt_message(hass, "test/state", "online") + assert not calls_a + assert calls_b + + # We allow either calls [subscribe, unsubscribe, subscribe], [subscribe, subscribe] or + # when both subscriptions were combined [subscribe] + expected_calls_1 = [ + call.subscribe([("test/state", 0)]), + call.unsubscribe("test/state"), + call.subscribe([("test/state", 0)]), + ] + expected_calls_2 = [ + call.subscribe([("test/state", 0)]), + call.subscribe([("test/state", 0)]), + ] + expected_calls_3 = [ + call.subscribe([("test/state", 0)]), + ] + assert mqtt_client_mock.mock_calls in ( + expected_calls_1, + expected_calls_2, + expected_calls_3, + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], +) +async def test_restore_subscriptions_on_reconnect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test subscriptions are restored on reconnect.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await mock_debouncer.wait() + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + await mock_debouncer.wait() + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], +) +async def test_restore_all_active_subscriptions_on_reconnect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test active subscriptions are restored correctly on reconnect.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) + # cooldown + await mock_debouncer.wait() + + # the subscription with the highest QoS should survive + expected = [ + call([("test/state", 2)]), + ] + assert mqtt_client_mock.subscribe.mock_calls == expected + + unsub() + assert mqtt_client_mock.unsubscribe.call_count == 0 + + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + # wait for cooldown + await mock_debouncer.wait() + + expected.append(call([("test/state", 1)])) + for expected_call in expected: + assert mqtt_client_mock.subscribe.hass_call(expected_call) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], +) +async def test_subscribed_at_highest_qos( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test the highest qos as assigned when subscribing to the same topic.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) + await hass.async_block_till_done() + # cooldown + await mock_debouncer.wait() + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) + # cooldown + await mock_debouncer.wait() + + # the subscription with the highest QoS should survive + assert help_all_subscribe_calls(mqtt_client_mock) == [("test/state", 2)] + + +async def test_initial_setup_logs_error( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """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.side_effect = MagicMock(return_value=1) + try: + assert await hass.config_entries.async_setup(entry.entry_id) + except HomeAssistantError: + assert True + assert "Failed to connect to MQTT server:" in caplog.text + + +async def test_logs_error_if_no_connect_broker( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test for setup failure if connection to broker is missing.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + # test with rc = 3 -> broker unavailable + mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_connect(Mock(), None, None, 3) + await hass.async_block_till_done() + assert ( + "Unable to connect to the MQTT broker: Connection Refused: broker unavailable." + in caplog.text + ) + + +@pytest.mark.parametrize("return_code", [4, 5]) +async def test_triggers_reauth_flow_if_auth_fails( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + return_code: int, +) -> None: + """Test re-auth is triggered if authentication is failing.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + # test with rc = 4 -> CONNACK_REFUSED_NOT_AUTHORIZED and 5 -> CONNACK_REFUSED_BAD_USERNAME_PASSWORD + mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_connect(Mock(), None, None, return_code) + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["source"] == "reauth" + + +@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) +async def test_handle_mqtt_on_callback( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test receiving an ACK callback before waiting for it.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + with patch.object(mqtt_client_mock, "get_mid", return_value=100): + # Simulate an ACK for mid == 100, this will call mqtt_mock._async_get_mid_future(mid) + mqtt_client_mock.on_publish(mqtt_client_mock, None, 100) + await hass.async_block_till_done() + # Make sure the ACK has been received + await hass.async_block_till_done() + # Now call publish without call back, this will call _async_async_wait_for_mid(msg_info.mid) + await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") + # Since the mid event was already set, we should not see any timeout warning in the log + await hass.async_block_till_done() + assert "No ACK from MQTT server" not in caplog.text + + +async def test_handle_mqtt_on_callback_after_timeout( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test receiving an ACK after a timeout.""" + mqtt_mock = await mqtt_mock_entry() + # Simulate the mid future getting a timeout + mqtt_mock()._async_get_mid_future(101).set_exception(asyncio.TimeoutError) + # Simulate an ACK for mid == 101, being received after the timeout + mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) + await hass.async_block_till_done() + assert "No ACK from MQTT server" not in caplog.text + assert "InvalidStateError" not in caplog.text + + +async def test_publish_error( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """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( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mock_client().connect = lambda *args: 1 + mock_client().publish().rc = 1 + assert await hass.config_entries.async_setup(entry.entry_id) + with pytest.raises(HomeAssistantError): + await mqtt.async_publish( + hass, "some-topic", b"test-payload", qos=0, retain=False, encoding=None + ) + assert "Failed to connect to MQTT server: Out of memory." in caplog.text + + +async def test_subscribe_error( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test publish error.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + # simulate client is not connected error before subscribing + mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None) + await mqtt.async_subscribe(hass, "some-topic", record_calls) + while mqtt_client_mock.subscribe.call_count == 0: + await hass.async_block_till_done() + await hass.async_block_till_done() + assert ( + "Error talking to MQTT: The client is not currently connected." in caplog.text + ) + + +async def test_handle_message_callback( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test for handling an incoming message callback.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + callbacks = [] + + @callback + def _callback(args) -> None: + callbacks.append(args) + + msg = ReceiveMessage( + "some-topic", b"test-payload", 1, False, "some-topic", datetime.now() + ) + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "some-topic", _callback) + await mock_debouncer.wait() + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_message(None, None, msg) + + assert len(callbacks) == 1 + assert callbacks[0].topic == "some-topic" + assert callbacks[0].qos == 1 + assert callbacks[0].payload == "test-payload" + + +@pytest.mark.parametrize( + ("mqtt_config_entry_data", "protocol"), + [ + ( + { + mqtt.CONF_BROKER: "mock-broker", + CONF_PROTOCOL: "3.1", + }, + 3, + ), + ( + { + mqtt.CONF_BROKER: "mock-broker", + CONF_PROTOCOL: "3.1.1", + }, + 4, + ), + ( + { + mqtt.CONF_BROKER: "mock-broker", + CONF_PROTOCOL: "5", + }, + 5, + ), + ], +) +async def test_setup_mqtt_client_protocol( + mqtt_mock_entry: MqttMockHAClientGenerator, protocol: int +) -> None: + """Test MQTT client protocol setup.""" + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + await mqtt_mock_entry() + + # check if protocol setup was correctly + assert mock_client.call_args[1]["protocol"] == protocol + + +@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) +async def test_handle_mqtt_timeout_on_callback( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_debouncer: asyncio.Event +) -> None: + """Test publish without receiving an ACK callback.""" + mid = 0 + + class FakeInfo: + """Returns a simulated client publish response.""" + + mid = 102 + rc = 0 + + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + + def _mock_ack(topic: str, qos: int = 0) -> tuple[int, int]: + # Handle ACK for subscribe normally + nonlocal mid + mid += 1 + mock_client.on_subscribe(0, 0, mid) + return (0, mid) + + # We want to simulate the publish behaviour MQTT client + mock_client = mock_client.return_value + mock_client.publish.return_value = FakeInfo() + # Mock we get a mid and rc=0 + mock_client.subscribe.side_effect = _mock_ack + mock_client.unsubscribe.side_effect = _mock_ack + mock_client.connect = MagicMock( + return_value=0, + side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( + mock_client.on_connect, mock_client, None, 0, 0, 0 + ), + ) + + entry = MockConfigEntry( + domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} + ) + entry.add_to_hass(hass) + + # Set up the integration + mock_debouncer.clear() + assert await hass.config_entries.async_setup(entry.entry_id) + + # 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() + # 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 + # Ensure we stop lingering background tasks + await hass.config_entries.async_unload(entry.entry_id) + # Assert we did not have any completed subscribes, + # because the debouncer subscribe job failed to receive an ACK, + # and the time auto caused the debouncer job to fail. + assert not mock_debouncer.is_set() + + +async def test_setup_raises_config_entry_not_ready_if_no_connect_broker( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """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( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mock_client().connect = MagicMock(side_effect=OSError("Connection error")) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert "Failed to connect to MQTT server due to exception:" in caplog.text + + +@pytest.mark.parametrize( + ("mqtt_config_entry_data", "insecure_param"), + [ + ({"broker": "test-broker", "certificate": "auto"}, "not set"), + ( + {"broker": "test-broker", "certificate": "auto", "tls_insecure": False}, + False, + ), + ({"broker": "test-broker", "certificate": "auto", "tls_insecure": True}, True), + ], +) +async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + insecure_param: bool | str, +) -> None: + """Test setup uses bundled certs when certificate is set to auto and insecure.""" + calls = [] + insecure_check = {"insecure": "not set"} + + def mock_tls_set( + certificate, certfile=None, keyfile=None, tls_version=None + ) -> None: + calls.append((certificate, certfile, keyfile, tls_version)) + + def mock_tls_insecure_set(insecure_param) -> None: + insecure_check["insecure"] = insecure_param + + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mock_client().tls_set = mock_tls_set + mock_client().tls_insecure_set = mock_tls_insecure_set + await mqtt_mock_entry() + await hass.async_block_till_done() + + assert calls + + expected_certificate = certifi.where() + assert calls[0][0] == expected_certificate + + # test if insecure is set + assert insecure_check["insecure"] == insecure_param + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_CERTIFICATE: "auto", + } + ], +) +async def test_tls_version( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test setup defaults for tls.""" + await mqtt_mock_entry() + await hass.async_block_till_done() + assert ( + mqtt_client_mock.tls_set.mock_calls[0][2]["tls_version"] + == ssl.PROTOCOL_TLS_CLIENT + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "birth", + mqtt.ATTR_PAYLOAD: "birth", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, + }, + } + ], +) +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) +async def test_custom_birth_message( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test sending birth message.""" + + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + mock_debouncer.clear() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + # discovery cooldown + await mock_debouncer.wait() + # Wait for publish call to finish + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_default_birth_message( + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient +) -> None: + """Test sending birth message.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], +) +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) +async def test_no_birth_message( + hass: HomeAssistant, + record_calls: MessageCallbackType, + mock_debouncer: asyncio.Event, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test disabling birth message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + mock_debouncer.clear() + assert await hass.config_entries.async_setup(entry.entry_id) + # Wait for discovery cooldown + await mock_debouncer.wait() + # Ensure any publishing could have been processed + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_not_called() + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "homeassistant/some-topic", record_calls) + # Wait for discovery cooldown + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.2) +async def test_delayed_birth_message( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test sending birth message does not happen until Home Assistant starts.""" + hass.set_state(CoreState.starting) + await hass.async_block_till_done() + birth = asyncio.Event() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + + @callback + def wait_birth(msg: ReceiveMessage) -> None: + """Handle birth message.""" + birth.set() + + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + with pytest.raises(TimeoutError): + await asyncio.wait_for(birth.wait(), 0.05) + assert not mqtt_client_mock.publish.called + assert not birth.is_set() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await birth.wait() + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_subscription_done_when_birth_message_is_sent( + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test sending birth message until initial subscription has been completed.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) + assert ("homeassistant/+/+/config", 0) in subscribe_calls + assert ("homeassistant/+/+/+/config", 0) in subscribe_calls + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_WILL_MESSAGE: { + mqtt.ATTR_TOPIC: "death", + mqtt.ATTR_PAYLOAD: "death", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, + }, + } + ], +) +async def test_custom_will_message( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test will message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mqtt_client_mock.will_set.assert_called_with( + topic="death", payload="death", qos=0, retain=False + ) + + +async def test_default_will_message( + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test will message.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.will_set.assert_called_with( + topic="homeassistant/status", payload="offline", qos=0, retain=False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], +) +async def test_no_will_message( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test will message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mqtt_client_mock.will_set.assert_not_called() + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE | {mqtt.CONF_DISCOVERY: False}], +) +async def test_mqtt_subscribes_topics_on_connect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test subscription to topic on connect.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "topic/test", record_calls) + await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2) + await mqtt.async_subscribe(hass, "still/pending", record_calls) + await mqtt.async_subscribe(hass, "still/pending", record_calls, 1) + await mock_debouncer.wait() + + mqtt_client_mock.on_disconnect(Mock(), None, 0) + + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + mqtt_client_mock.on_connect(Mock(), None, 0, 0) + await mock_debouncer.wait() + + subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) + assert ("topic/test", 0) in subscribe_calls + assert ("home/sensor", 2) in subscribe_calls + assert ("still/pending", 1) in subscribe_calls + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_mqtt_subscribes_in_single_call( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test bundled client subscription to topic.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.subscribe.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "topic/test", record_calls) + await mqtt.async_subscribe(hass, "home/sensor", record_calls) + # Make sure the debouncer finishes + await mock_debouncer.wait() + + assert mqtt_client_mock.subscribe.call_count == 1 + # Assert we have a single subscription call with both subscriptions + assert mqtt_client_mock.subscribe.mock_calls[0][1][0] in [ + [("topic/test", 0), ("home/sensor", 0)], + [("home/sensor", 0), ("topic/test", 0)], + ] + + +@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) +@patch("homeassistant.components.mqtt.client.MAX_SUBSCRIBES_PER_CALL", 2) +@patch("homeassistant.components.mqtt.client.MAX_UNSUBSCRIBES_PER_CALL", 2) +async def test_mqtt_subscribes_and_unsubscribes_in_chunks( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test chunked client subscriptions.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + + mqtt_client_mock.subscribe.reset_mock() + unsub_tasks: list[CALLBACK_TYPE] = [] + mock_debouncer.clear() + unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test1", record_calls)) + unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor1", record_calls)) + unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test2", record_calls)) + unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor2", record_calls)) + # Make sure the debouncer finishes + await mock_debouncer.wait() + + assert mqtt_client_mock.subscribe.call_count == 2 + # Assert we have a 2 subscription calls with both 2 subscriptions + assert len(mqtt_client_mock.subscribe.mock_calls[0][1][0]) == 2 + assert len(mqtt_client_mock.subscribe.mock_calls[1][1][0]) == 2 + + # Unsubscribe all topics + mock_debouncer.clear() + for task in unsub_tasks: + task() + # Make sure the debouncer finishes + await mock_debouncer.wait() + + assert mqtt_client_mock.unsubscribe.call_count == 2 + # Assert we have a 2 unsubscribe calls with both 2 topic + assert len(mqtt_client_mock.unsubscribe.mock_calls[0][1][0]) == 2 + assert len(mqtt_client_mock.unsubscribe.mock_calls[1][1][0]) == 2 + + +async def test_auto_reconnect( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test reconnection is automatically done.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + mqtt_client_mock.reconnect.reset_mock() + + mqtt_client_mock.disconnect() + mqtt_client_mock.on_disconnect(None, None, 0) + await hass.async_block_till_done() + + mqtt_client_mock.reconnect.side_effect = OSError("foo") + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + assert len(mqtt_client_mock.reconnect.mock_calls) == 1 + assert "Error re-connecting to MQTT server due to exception: foo" in caplog.text + + mqtt_client_mock.reconnect.side_effect = None + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + assert len(mqtt_client_mock.reconnect.mock_calls) == 2 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + + mqtt_client_mock.disconnect() + mqtt_client_mock.on_disconnect(None, None, 0) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + # Should not reconnect after stop + assert len(mqtt_client_mock.reconnect.mock_calls) == 2 + + +async def test_server_sock_connect_and_disconnect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) + await hass.async_block_till_done() + + server.close() # mock the server closing the connection on us + + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + await mock_debouncer.wait() + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + mqtt_client_mock.on_socket_unregister_write(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_close(mqtt_client_mock, None, client) + mqtt_client_mock.on_disconnect(mqtt_client_mock, None, client) + await hass.async_block_till_done() + mock_debouncer.clear() + unsub() + await hass.async_block_till_done() + assert not mock_debouncer.is_set() + + # Should have failed + assert len(recorded_calls) == 0 + + +async def test_server_sock_buffer_size( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling the socket buffer size fails.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + with patch.object(client, "setsockopt", side_effect=OSError("foo")): + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) + await hass.async_block_till_done() + assert "Unable to increase the socket buffer size" in caplog.text + + +async def test_server_sock_buffer_size_with_websocket( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling the socket buffer size fails.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + + class FakeWebsocket(paho_mqtt.WebsocketWrapper): + def _do_handshake(self, *args, **kwargs): + pass + + wrapped_socket = FakeWebsocket(client, "127.0.01", 1, False, "/", None) + + with patch.object(client, "setsockopt", side_effect=OSError("foo")): + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, wrapped_socket) + mqtt_client_mock.on_socket_register_write( + mqtt_client_mock, None, wrapped_socket + ) + await hass.async_block_till_done() + assert "Unable to increase the socket buffer size" in caplog.text + + +async def test_client_sock_failure_after_connect( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_writer(mqtt_client_mock, None, client) + await hass.async_block_till_done() + + mqtt_client_mock.loop_write.side_effect = OSError("foo") + client.close() # close the client socket out from under the client + + assert mqtt_client_mock.connect.call_count == 1 + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + + unsub() + # Should have failed + assert len(recorded_calls) == 0 + + +async def test_loop_write_failure( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) + mqtt_client_mock.loop_write.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + mqtt_client_mock.loop_read.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + + # Fill up the outgoing buffer to ensure that loop_write + # and loop_read are called that next time control is + # returned to the event loop + try: + for _ in range(1000): + server.send(b"long" * 100) + except BlockingIOError: + pass + + server.close() + # Once for the reader callback + await hass.async_block_till_done() + # Another for the writer callback + await hass.async_block_till_done() + # Final for the disconnect callback + await hass.async_block_till_done() + + assert "Disconnected from MQTT server test-broker:1883" in caplog.text diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index bcadf4a6506..403f7974878 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,25 +1,20 @@ -"""The tests for the MQTT component.""" +"""The tests for the MQTT component setup and helpers.""" import asyncio from copy import deepcopy from datetime import datetime, timedelta from functools import partial import json -import socket -import ssl import time from typing import Any, TypedDict -from unittest.mock import ANY, MagicMock, Mock, call, mock_open, patch +from unittest.mock import ANY, MagicMock, Mock, mock_open, patch -import certifi from freezegun.api import FrozenDateTimeFactory -import paho.mqtt.client as paho_mqtt import pytest import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info -from homeassistant.components.mqtt.client import RECONNECT_INTERVAL_SECONDS from homeassistant.components.mqtt.models import ( MessageCallbackType, MqttCommandTemplateException, @@ -31,16 +26,12 @@ from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import ( ATTR_ASSUMED_STATE, - CONF_PROTOCOL, - EVENT_HOMEASSISTANT_STARTED, - EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, - UnitOfTemperature, ) import homeassistant.core as ha -from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr, entity_registry as er, template from homeassistant.helpers.entity import Entity @@ -50,9 +41,6 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from homeassistant.util.dt import utcnow -from .conftest import ENTRY_DEFAULT_BIRTH_MESSAGE -from .test_common import help_all_subscribe_calls - from tests.common import ( MockConfigEntry, MockEntity, @@ -63,7 +51,6 @@ from tests.common import ( ) from tests.components.sensor.common import MockSensor from tests.typing import ( - MqttMockHAClient, MqttMockHAClientGenerator, MqttMockPahoClient, WebSocketGenerator, @@ -95,205 +82,6 @@ def mock_storage(hass_storage: dict[str, Any]) -> None: """Autouse hass_storage for the TestCase tests.""" -def help_assert_message( - msg: ReceiveMessage, - topic: str | None = None, - payload: str | None = None, - qos: int | None = None, - retain: bool | None = None, -) -> bool: - """Return True if all of the given attributes match with the message.""" - match: bool = True - if topic is not None: - match &= msg.topic == topic - if payload is not None: - match &= msg.payload == payload - if qos is not None: - match &= msg.qos == qos - if retain is not None: - match &= msg.retain == retain - return match - - -async def test_mqtt_connects_on_home_assistant_mqtt_setup( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient -) -> None: - """Test if client is connected after mqtt init on bootstrap.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - -async def test_mqtt_does_not_disconnect_on_home_assistant_stop( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test if client is not disconnected on HA stop.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - await mock_debouncer.wait() - assert mqtt_client_mock.disconnect.call_count == 0 - - -async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: - """Test if ACK is awaited correctly when disconnecting.""" - - class FakeInfo: - """Returns a simulated client publish response.""" - - mid = 100 - rc = 0 - - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mqtt_client = mock_client.return_value - mqtt_client.connect = MagicMock( - return_value=0, - side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( - mqtt_client.on_connect, mqtt_client, None, 0, 0, 0 - ), - ) - mqtt_client.publish = MagicMock(return_value=FakeInfo()) - entry = MockConfigEntry( - domain=mqtt.DOMAIN, - data={ - "certificate": "auto", - mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_DISCOVERY: False, - }, - ) - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - - 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, - ) - await hass.async_block_till_done(wait_background_tasks=True) - - -@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) -async def test_publish( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient -) -> None: - """Test the publish function.""" - publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish - await mqtt.async_publish(hass, "test-topic", "test-payload") - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic", - "test-payload", - 0, - False, - ) - publish_mock.reset_mock() - - await mqtt.async_publish(hass, "test-topic", "test-payload", 2, True) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic", - "test-payload", - 2, - True, - ) - publish_mock.reset_mock() - - mqtt.publish(hass, "test-topic2", "test-payload2") - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic2", - "test-payload2", - 0, - False, - ) - publish_mock.reset_mock() - - mqtt.publish(hass, "test-topic2", "test-payload2", 2, True) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic2", - "test-payload2", - 2, - True, - ) - publish_mock.reset_mock() - - # test binary pass-through - mqtt.publish( - hass, - "test-topic3", - b"\xde\xad\xbe\xef", - 0, - False, - ) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic3", - b"\xde\xad\xbe\xef", - 0, - False, - ) - publish_mock.reset_mock() - - # test null payload - mqtt.publish( - hass, - "test-topic3", - None, - 0, - False, - ) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic3", - None, - 0, - False, - ) - - publish_mock.reset_mock() - - -async def test_convert_outgoing_payload(hass: HomeAssistant) -> None: - """Test the converting of outgoing MQTT payloads without template.""" - command_template = mqtt.MqttCommandTemplate(None, hass=hass) - assert command_template.async_render(b"\xde\xad\xbe\xef") == b"\xde\xad\xbe\xef" - - assert ( - command_template.async_render("b'\\xde\\xad\\xbe\\xef'") - == "b'\\xde\\xad\\xbe\\xef'" - ) - - assert command_template.async_render(1234) == 1234 - - assert command_template.async_render(1234.56) == 1234.56 - - assert command_template.async_render(None) is None - - async def test_command_template_value(hass: HomeAssistant) -> None: """Test the rendering of MQTT command template.""" @@ -983,893 +771,6 @@ async def test_receiving_message_with_non_utf8_topic_gets_logged( ) -async def test_all_subscriptions_run_when_decode_fails( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test all other subscriptions still run when decode fails for one.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic", record_calls, encoding="ascii") - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - async_fire_mqtt_message(hass, "test-topic", UnitOfTemperature.CELSIUS) - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - - -async def test_subscribe_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of a topic.""" - await mqtt_mock_entry() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - - unsub() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - - # Cannot unsubscribe twice - with pytest.raises(HomeAssistantError): - unsub() - - -@pytest.mark.usefixtures("mqtt_mock_entry") -async def test_subscribe_topic_not_initialize( - hass: HomeAssistant, record_calls: MessageCallbackType -) -> None: - """Test the subscription of a topic when MQTT was not initialized.""" - with pytest.raises( - HomeAssistantError, match=r".*make sure MQTT is set up correctly" - ): - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - -async def test_subscribe_mqtt_config_entry_disabled( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, record_calls: MessageCallbackType -) -> None: - """Test the subscription of a topic when MQTT config entry is disabled.""" - mqtt_mock.connected = True - - mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - assert mqtt_config_entry.state is ConfigEntryState.LOADED - - assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) - assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED - - await hass.config_entries.async_set_disabled_by( - mqtt_config_entry.entry_id, ConfigEntryDisabler.USER - ) - mqtt_mock.connected = False - - with pytest.raises(HomeAssistantError, match=r".*MQTT is not enabled"): - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - -async def test_subscribe_and_resubscribe( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test resubscribing within the debounce time.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - with ( - patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.4), - patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.4), - ): - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - # This unsub will be un-done with the following subscribe - # unsubscribe should not be called at the broker - unsub() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - await mock_debouncer.wait() - mock_debouncer.clear() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - # assert unsubscribe was not called - mqtt_client_mock.unsubscribe.assert_not_called() - - mock_debouncer.clear() - unsub() - - await mock_debouncer.wait() - mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) - - -async def test_subscribe_topic_non_async( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of a topic using the non-async function.""" - await mqtt_mock_entry() - await mock_debouncer.wait() - mock_debouncer.clear() - unsub = await hass.async_add_executor_job( - mqtt.subscribe, hass, "test-topic", record_calls - ) - await mock_debouncer.wait() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - - mock_debouncer.clear() - await hass.async_add_executor_job(unsub) - await mock_debouncer.wait() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - assert len(recorded_calls) == 1 - - -async def test_subscribe_bad_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of a topic.""" - await mqtt_mock_entry() - with pytest.raises(HomeAssistantError): - await mqtt.async_subscribe(hass, 55, record_calls) # type: ignore[arg-type] - - -async def test_subscribe_topic_not_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test if subscribed topic is not a match.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - async_fire_mqtt_message(hass, "another-test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) - - async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic/bier/on" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_level_wildcard_no_subtree_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) - - async_fire_mqtt_message(hass, "test-topic/bier", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "test-topic-123", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_subtree_wildcard_subtree_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic/bier/on" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_subtree_wildcard_root_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_subtree_wildcard_no_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "another-test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "hi/test-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/test-topic/here-iam", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "hi/test-topic/here-iam" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/here-iam/test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard_and_wildcard_no_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/another-test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_sys_root( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of $ root topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "$test-topic/subtree/on", record_calls) - - async_fire_mqtt_message(hass, "$test-topic/subtree/on", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "$test-topic/subtree/on" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_sys_root_and_wildcard_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of $ root and wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "$test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "$test-topic/some-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "$test-topic/some-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of $ root and wildcard subtree topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "$test-topic/subtree/#", record_calls) - - async_fire_mqtt_message(hass, "$test-topic/subtree/some-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "$test-topic/subtree/some-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_special_characters( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription to topics with special characters.""" - await mqtt_mock_entry() - topic = "/test-topic/$(.)[^]{-}" - payload = "p4y.l[]a|> ?" - - await mqtt.async_subscribe(hass, topic, record_calls) - - async_fire_mqtt_message(hass, topic, payload) - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == topic - assert recorded_calls[0].payload == payload - - -async def test_subscribe_same_topic( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test subscribing to same topic twice and simulate retained messages. - - When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again - for it to resend any retained messages. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_a, qos=0) - # Simulate a non retained message after the first subscription - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - await mock_debouncer.wait() - assert len(calls_a) == 1 - mqtt_client_mock.subscribe.assert_called() - calls_a = [] - mqtt_client_mock.reset_mock() - - await hass.async_block_till_done() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_b, qos=1) - # Simulate an other non retained message after the second subscription - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - await mock_debouncer.wait() - # Both subscriptions should receive updates - assert len(calls_a) == 1 - assert len(calls_b) == 1 - mqtt_client_mock.subscribe.assert_called() - - -async def test_replaying_payload_same_topic( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test replaying retained messages. - - When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again - for it to resend any retained messages for new subscriptions. - Retained messages must only be replayed for new subscriptions, except - when the MQTT client is reconnecting. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_a) - await mock_debouncer.wait() - async_fire_mqtt_message( - hass, "test/state", "online", qos=0, retain=True - ) # Simulate a (retained) message played back - assert len(calls_a) == 1 - mqtt_client_mock.subscribe.assert_called() - calls_a = [] - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_b) - await mock_debouncer.wait() - - # Simulate edge case where non retained message was received - # after subscription at HA but before the debouncer delay was passed. - # The message without retain flag directly after a subscription should - # be processed by both subscriptions. - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - - # Simulate a (retained) message played back on new subscriptions - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - - # The current subscription only received the message without retain flag - assert len(calls_a) == 1 - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) - # The retained message playback should only be processed by the new subscription. - # The existing subscription already got the latest update, hence the existing - # subscription should not receive the replayed (retained) message. - # Messages without retain flag are received on both subscriptions. - assert len(calls_b) == 2 - assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) - assert help_assert_message(calls_b[1], "test/state", "online", qos=0, retain=True) - mqtt_client_mock.subscribe.assert_called() - - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - - # Simulate new message played back on new subscriptions - # After connecting the retain flag will not be set, even if the - # payload published was retained, we cannot see that - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - assert len(calls_a) == 1 - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) - assert len(calls_b) == 1 - assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) - - # Now simulate the broker was disconnected shortly - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - # Simulate a (retained) message played back after reconnecting - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - # Both subscriptions now should replay the retained message - assert len(calls_a) == 1 - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) - assert len(calls_b) == 1 - assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=True) - - -async def test_replaying_payload_after_resubscribing( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test replaying and filtering retained messages after resubscribing. - - When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again - for it to resend any retained messages for new subscriptions. - Retained messages must only be replayed for new subscriptions, except - when the MQTT client is reconnection. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - - # Simulate a (retained) message played back - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) - calls_a.clear() - - # Test we get updates - async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=False) - assert help_assert_message(calls_a[0], "test/state", "offline", qos=0, retain=False) - calls_a.clear() - - # Test we filter new retained updates - async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=True) - await hass.async_block_till_done() - assert len(calls_a) == 0 - - # Unsubscribe an resubscribe again - mock_debouncer.clear() - unsub() - unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - - # Simulate we can receive a (retained) played back message again - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) - - -async def test_replaying_payload_wildcard_topic( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test replaying retained messages. - - When we have multiple subscriptions to the same wildcard topic, - SUBSCRIBE must be sent to the broker again - for it to resend any retained messages for new subscriptions. - Retained messages should only be replayed for new subscriptions, except - when the MQTT client is reconnection. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/#", _callback_a) - await mock_debouncer.wait() - # Simulate (retained) messages being played back on new subscriptions - async_fire_mqtt_message(hass, "test/state1", "new_value_1", qos=0, retain=True) - async_fire_mqtt_message(hass, "test/state2", "new_value_2", qos=0, retain=True) - assert len(calls_a) == 2 - mqtt_client_mock.subscribe.assert_called() - calls_a = [] - mqtt_client_mock.reset_mock() - - # resubscribe to the wild card topic again - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/#", _callback_b) - await mock_debouncer.wait() - # Simulate (retained) messages being played back on new subscriptions - async_fire_mqtt_message(hass, "test/state1", "initial_value_1", qos=0, retain=True) - async_fire_mqtt_message(hass, "test/state2", "initial_value_2", qos=0, retain=True) - # The retained messages playback should only be processed for the new subscriptions - assert len(calls_a) == 0 - assert len(calls_b) == 2 - mqtt_client_mock.subscribe.assert_called() - - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - - # Simulate new messages being received - async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=False) - async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=False) - assert len(calls_a) == 2 - assert len(calls_b) == 2 - - # Now simulate the broker was disconnected shortly - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - await mock_debouncer.wait() - - mqtt_client_mock.subscribe.assert_called() - # Simulate the (retained) messages are played back after reconnecting - # for all subscriptions - async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=True) - async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=True) - # Both subscriptions should replay - assert len(calls_a) == 2 - assert len(calls_b) == 2 - - -async def test_not_calling_unsubscribe_with_active_subscribers( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test not calling unsubscribe() when other subscribers are active.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) - await mqtt.async_subscribe(hass, "test/state", record_calls, 1) - await mock_debouncer.wait() - assert mqtt_client_mock.subscribe.called - - mock_debouncer.clear() - unsub() - await hass.async_block_till_done() - await hass.async_block_till_done(wait_background_tasks=True) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - assert not mqtt_client_mock.unsubscribe.called - assert not mock_debouncer.is_set() - - -async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - mqtt_mock_entry: MqttMockHAClientGenerator, - record_calls: MessageCallbackType, -) -> None: - """Test not calling subscribe() when it is unsubscribed. - - Make sure subscriptions are cleared if unsubscribed before - the subscribe cool down period has ended. - """ - mqtt_mock = await mqtt_mock_entry() - mqtt_client_mock = mqtt_mock._mqttc - await mock_debouncer.wait() - - mock_debouncer.clear() - mqtt_client_mock.subscribe.reset_mock() - unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) - unsub() - await mock_debouncer.wait() - # The debouncer executes without an pending subscribes - assert not mqtt_client_mock.subscribe.called - - -async def test_unsubscribe_race( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test not calling unsubscribe() when other subscribers are active.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - unsub() - await mqtt.async_subscribe(hass, "test/state", _callback_b) - await mock_debouncer.wait() - - async_fire_mqtt_message(hass, "test/state", "online") - assert not calls_a - assert calls_b - - # We allow either calls [subscribe, unsubscribe, subscribe], [subscribe, subscribe] or - # when both subscriptions were combined [subscribe] - expected_calls_1 = [ - call.subscribe([("test/state", 0)]), - call.unsubscribe("test/state"), - call.subscribe([("test/state", 0)]), - ] - expected_calls_2 = [ - call.subscribe([("test/state", 0)]), - call.subscribe([("test/state", 0)]), - ] - expected_calls_3 = [ - call.subscribe([("test/state", 0)]), - ] - assert mqtt_client_mock.mock_calls in ( - expected_calls_1, - expected_calls_2, - expected_calls_3, - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], -) -async def test_restore_subscriptions_on_reconnect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test subscriptions are restored on reconnect.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await mock_debouncer.wait() - assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) - - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - await mock_debouncer.wait() - assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], -) -async def test_restore_all_active_subscriptions_on_reconnect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test active subscriptions are restored correctly on reconnect.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) - # cooldown - await mock_debouncer.wait() - - # the subscription with the highest QoS should survive - expected = [ - call([("test/state", 2)]), - ] - assert mqtt_client_mock.subscribe.mock_calls == expected - - unsub() - assert mqtt_client_mock.unsubscribe.call_count == 0 - - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - # wait for cooldown - await mock_debouncer.wait() - - expected.append(call([("test/state", 1)])) - for expected_call in expected: - assert mqtt_client_mock.subscribe.hass_call(expected_call) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], -) -async def test_subscribed_at_highest_qos( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test the highest qos as assigned when subscribing to the same topic.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) - await hass.async_block_till_done() - # cooldown - await mock_debouncer.wait() - assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) - # cooldown - await mock_debouncer.wait() - - # the subscription with the highest QoS should survive - assert help_all_subscribe_calls(mqtt_client_mock) == [("test/state", 2)] - - @pytest.mark.usefixtures("mqtt_client_mock") async def test_reload_entry_with_restored_subscriptions( hass: HomeAssistant, @@ -1937,163 +838,6 @@ async def test_reload_entry_with_restored_subscriptions( assert recorded_calls[1].payload == "wild-card-payload3" -async def test_initial_setup_logs_error( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """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.side_effect = MagicMock(return_value=1) - try: - assert await hass.config_entries.async_setup(entry.entry_id) - except HomeAssistantError: - assert True - assert "Failed to connect to MQTT server:" in caplog.text - - -async def test_logs_error_if_no_connect_broker( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test for setup failure if connection to broker is missing.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - # test with rc = 3 -> broker unavailable - mqtt_client_mock.on_disconnect(Mock(), None, 0) - mqtt_client_mock.on_connect(Mock(), None, None, 3) - await hass.async_block_till_done() - assert ( - "Unable to connect to the MQTT broker: Connection Refused: broker unavailable." - in caplog.text - ) - - -@pytest.mark.parametrize("return_code", [4, 5]) -async def test_triggers_reauth_flow_if_auth_fails( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - return_code: int, -) -> None: - """Test re-auth is triggered if authentication is failing.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - # test with rc = 4 -> CONNACK_REFUSED_NOT_AUTHORIZED and 5 -> CONNACK_REFUSED_BAD_USERNAME_PASSWORD - mqtt_client_mock.on_disconnect(Mock(), None, 0) - mqtt_client_mock.on_connect(Mock(), None, None, return_code) - await hass.async_block_till_done() - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - assert flows[0]["context"]["source"] == "reauth" - - -@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) -async def test_handle_mqtt_on_callback( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test receiving an ACK callback before waiting for it.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - with patch.object(mqtt_client_mock, "get_mid", return_value=100): - # Simulate an ACK for mid == 100, this will call mqtt_mock._async_get_mid_future(mid) - mqtt_client_mock.on_publish(mqtt_client_mock, None, 100) - await hass.async_block_till_done() - # Make sure the ACK has been received - await hass.async_block_till_done() - # Now call publish without call back, this will call _async_async_wait_for_mid(msg_info.mid) - await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") - # Since the mid event was already set, we should not see any timeout warning in the log - await hass.async_block_till_done() - assert "No ACK from MQTT server" not in caplog.text - - -async def test_handle_mqtt_on_callback_after_timeout( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test receiving an ACK after a timeout.""" - mqtt_mock = await mqtt_mock_entry() - # Simulate the mid future getting a timeout - mqtt_mock()._async_get_mid_future(101).set_exception(asyncio.TimeoutError) - # Simulate an ACK for mid == 101, being received after the timeout - mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) - await hass.async_block_till_done() - assert "No ACK from MQTT server" not in caplog.text - assert "InvalidStateError" not in caplog.text - - -async def test_publish_error( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """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( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mock_client().connect = lambda *args: 1 - mock_client().publish().rc = 1 - assert await hass.config_entries.async_setup(entry.entry_id) - with pytest.raises(HomeAssistantError): - await mqtt.async_publish( - hass, "some-topic", b"test-payload", qos=0, retain=False, encoding=None - ) - assert "Failed to connect to MQTT server: Out of memory." in caplog.text - - -async def test_subscribe_error( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test publish error.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - # simulate client is not connected error before subscribing - mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None) - await mqtt.async_subscribe(hass, "some-topic", record_calls) - while mqtt_client_mock.subscribe.call_count == 0: - await hass.async_block_till_done() - await hass.async_block_till_done() - assert ( - "Error talking to MQTT: The client is not currently connected." in caplog.text - ) - - -async def test_handle_message_callback( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test for handling an incoming message callback.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - callbacks = [] - - @callback - def _callback(args) -> None: - callbacks.append(args) - - msg = ReceiveMessage( - "some-topic", b"test-payload", 1, False, "some-topic", datetime.now() - ) - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "some-topic", _callback) - await mock_debouncer.wait() - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_message(None, None, msg) - - assert len(callbacks) == 1 - assert callbacks[0].topic == "some-topic" - assert callbacks[0].qos == 1 - assert callbacks[0].payload == "test-payload" - - @pytest.mark.parametrize( "hass_config", [ @@ -2128,491 +872,6 @@ async def test_setup_manual_mqtt_with_invalid_config( assert "required key not provided" in caplog.text -@pytest.mark.parametrize( - ("mqtt_config_entry_data", "protocol"), - [ - ( - { - mqtt.CONF_BROKER: "mock-broker", - CONF_PROTOCOL: "3.1", - }, - 3, - ), - ( - { - mqtt.CONF_BROKER: "mock-broker", - CONF_PROTOCOL: "3.1.1", - }, - 4, - ), - ( - { - mqtt.CONF_BROKER: "mock-broker", - CONF_PROTOCOL: "5", - }, - 5, - ), - ], -) -async def test_setup_mqtt_client_protocol( - mqtt_mock_entry: MqttMockHAClientGenerator, protocol: int -) -> None: - """Test MQTT client protocol setup.""" - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - await mqtt_mock_entry() - - # check if protocol setup was correctly - assert mock_client.call_args[1]["protocol"] == protocol - - -@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) -async def test_handle_mqtt_timeout_on_callback( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_debouncer: asyncio.Event -) -> None: - """Test publish without receiving an ACK callback.""" - mid = 0 - - class FakeInfo: - """Returns a simulated client publish response.""" - - mid = 102 - rc = 0 - - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - - def _mock_ack(topic: str, qos: int = 0) -> tuple[int, int]: - # Handle ACK for subscribe normally - nonlocal mid - mid += 1 - mock_client.on_subscribe(0, 0, mid) - return (0, mid) - - # We want to simulate the publish behaviour MQTT client - mock_client = mock_client.return_value - mock_client.publish.return_value = FakeInfo() - # Mock we get a mid and rc=0 - mock_client.subscribe.side_effect = _mock_ack - mock_client.unsubscribe.side_effect = _mock_ack - mock_client.connect = MagicMock( - return_value=0, - side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( - mock_client.on_connect, mock_client, None, 0, 0, 0 - ), - ) - - entry = MockConfigEntry( - domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} - ) - entry.add_to_hass(hass) - - # Set up the integration - mock_debouncer.clear() - assert await hass.config_entries.async_setup(entry.entry_id) - - # 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() - # 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 - # Ensure we stop lingering background tasks - await hass.config_entries.async_unload(entry.entry_id) - # Assert we did not have any completed subscribes, - # because the debouncer subscribe job failed to receive an ACK, - # and the time auto caused the debouncer job to fail. - assert not mock_debouncer.is_set() - - -async def test_setup_raises_config_entry_not_ready_if_no_connect_broker( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """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( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mock_client().connect = MagicMock(side_effect=OSError("Connection error")) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert "Failed to connect to MQTT server due to exception:" in caplog.text - - -@pytest.mark.parametrize( - ("mqtt_config_entry_data", "insecure_param"), - [ - ({"broker": "test-broker", "certificate": "auto"}, "not set"), - ( - {"broker": "test-broker", "certificate": "auto", "tls_insecure": False}, - False, - ), - ({"broker": "test-broker", "certificate": "auto", "tls_insecure": True}, True), - ], -) -async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - insecure_param: bool | str, -) -> None: - """Test setup uses bundled certs when certificate is set to auto and insecure.""" - calls = [] - insecure_check = {"insecure": "not set"} - - def mock_tls_set( - certificate, certfile=None, keyfile=None, tls_version=None - ) -> None: - calls.append((certificate, certfile, keyfile, tls_version)) - - def mock_tls_insecure_set(insecure_param) -> None: - insecure_check["insecure"] = insecure_param - - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mock_client().tls_set = mock_tls_set - mock_client().tls_insecure_set = mock_tls_insecure_set - await mqtt_mock_entry() - await hass.async_block_till_done() - - assert calls - - expected_certificate = certifi.where() - assert calls[0][0] == expected_certificate - - # test if insecure is set - assert insecure_check["insecure"] == insecure_param - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_CERTIFICATE: "auto", - } - ], -) -async def test_tls_version( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, -) -> None: - """Test setup defaults for tls.""" - await mqtt_mock_entry() - await hass.async_block_till_done() - assert ( - mqtt_client_mock.tls_set.mock_calls[0][2]["tls_version"] - == ssl.PROTOCOL_TLS_CLIENT - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "birth", - mqtt.ATTR_PAYLOAD: "birth", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], -) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -async def test_custom_birth_message( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test sending birth message.""" - - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - mock_debouncer.clear() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - # discovery cooldown - await mock_debouncer.wait() - # Wait for publish call to finish - await hass.async_block_till_done(wait_background_tasks=True) - mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -async def test_default_birth_message( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient -) -> None: - """Test sending birth message.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - await hass.async_block_till_done(wait_background_tasks=True) - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], -) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -async def test_no_birth_message( - hass: HomeAssistant, - record_calls: MessageCallbackType, - mock_debouncer: asyncio.Event, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test disabling birth message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - mock_debouncer.clear() - assert await hass.config_entries.async_setup(entry.entry_id) - # Wait for discovery cooldown - await mock_debouncer.wait() - # Ensure any publishing could have been processed - await hass.async_block_till_done(wait_background_tasks=True) - mqtt_client_mock.publish.assert_not_called() - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "homeassistant/some-topic", record_calls) - # Wait for discovery cooldown - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.2) -async def test_delayed_birth_message( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test sending birth message does not happen until Home Assistant starts.""" - hass.set_state(CoreState.starting) - await hass.async_block_till_done() - birth = asyncio.Event() - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - - @callback - def wait_birth(msg: ReceiveMessage) -> None: - """Handle birth message.""" - birth.set() - - await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - with pytest.raises(TimeoutError): - await asyncio.wait_for(birth.wait(), 0.05) - assert not mqtt_client_mock.publish.called - assert not birth.is_set() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await birth.wait() - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -async def test_subscription_done_when_birth_message_is_sent( - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test sending birth message until initial subscription has been completed.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) - assert ("homeassistant/+/+/config", 0) in subscribe_calls - assert ("homeassistant/+/+/+/config", 0) in subscribe_calls - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_WILL_MESSAGE: { - mqtt.ATTR_TOPIC: "death", - mqtt.ATTR_PAYLOAD: "death", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], -) -async def test_custom_will_message( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test will message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - mqtt_client_mock.will_set.assert_called_with( - topic="death", payload="death", qos=0, retain=False - ) - - -async def test_default_will_message( - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test will message.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.will_set.assert_called_with( - topic="homeassistant/status", payload="offline", qos=0, retain=False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], -) -async def test_no_will_message( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test will message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - mqtt_client_mock.will_set.assert_not_called() - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE | {mqtt.CONF_DISCOVERY: False}], -) -async def test_mqtt_subscribes_topics_on_connect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test subscription to topic on connect.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "topic/test", record_calls) - await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2) - await mqtt.async_subscribe(hass, "still/pending", record_calls) - await mqtt.async_subscribe(hass, "still/pending", record_calls, 1) - await mock_debouncer.wait() - - mqtt_client_mock.on_disconnect(Mock(), None, 0) - - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - mqtt_client_mock.on_connect(Mock(), None, 0, 0) - await mock_debouncer.wait() - - subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) - assert ("topic/test", 0) in subscribe_calls - assert ("home/sensor", 2) in subscribe_calls - assert ("still/pending", 1) in subscribe_calls - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -async def test_mqtt_subscribes_in_single_call( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test bundled client subscription to topic.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.subscribe.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "topic/test", record_calls) - await mqtt.async_subscribe(hass, "home/sensor", record_calls) - # Make sure the debouncer finishes - await mock_debouncer.wait() - - assert mqtt_client_mock.subscribe.call_count == 1 - # Assert we have a single subscription call with both subscriptions - assert mqtt_client_mock.subscribe.mock_calls[0][1][0] in [ - [("topic/test", 0), ("home/sensor", 0)], - [("home/sensor", 0), ("topic/test", 0)], - ] - - -@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) -@patch("homeassistant.components.mqtt.client.MAX_SUBSCRIBES_PER_CALL", 2) -@patch("homeassistant.components.mqtt.client.MAX_UNSUBSCRIBES_PER_CALL", 2) -async def test_mqtt_subscribes_and_unsubscribes_in_chunks( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test chunked client subscriptions.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - - mqtt_client_mock.subscribe.reset_mock() - unsub_tasks: list[CALLBACK_TYPE] = [] - mock_debouncer.clear() - unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test1", record_calls)) - unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor1", record_calls)) - unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test2", record_calls)) - unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor2", record_calls)) - # Make sure the debouncer finishes - await mock_debouncer.wait() - - assert mqtt_client_mock.subscribe.call_count == 2 - # Assert we have a 2 subscription calls with both 2 subscriptions - assert len(mqtt_client_mock.subscribe.mock_calls[0][1][0]) == 2 - assert len(mqtt_client_mock.subscribe.mock_calls[1][1][0]) == 2 - - # Unsubscribe all topics - mock_debouncer.clear() - for task in unsub_tasks: - task() - # Make sure the debouncer finishes - await mock_debouncer.wait() - - assert mqtt_client_mock.unsubscribe.call_count == 2 - # Assert we have a 2 unsubscribe calls with both 2 topic - assert len(mqtt_client_mock.unsubscribe.mock_calls[0][1][0]) == 2 - assert len(mqtt_client_mock.unsubscribe.mock_calls[1][1][0]) == 2 - - @pytest.mark.usefixtures("mqtt_client_mock") async def test_default_entry_setting_are_applied( hass: HomeAssistant, device_registry: dr.DeviceRegistry @@ -4106,221 +2365,6 @@ async def test_multi_platform_discovery( ) -async def test_auto_reconnect( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test reconnection is automatically done.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - mqtt_client_mock.reconnect.reset_mock() - - mqtt_client_mock.disconnect() - mqtt_client_mock.on_disconnect(None, None, 0) - await hass.async_block_till_done() - - mqtt_client_mock.reconnect.side_effect = OSError("foo") - async_fire_time_changed( - hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) - ) - await hass.async_block_till_done() - assert len(mqtt_client_mock.reconnect.mock_calls) == 1 - assert "Error re-connecting to MQTT server due to exception: foo" in caplog.text - - mqtt_client_mock.reconnect.side_effect = None - async_fire_time_changed( - hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) - ) - await hass.async_block_till_done() - assert len(mqtt_client_mock.reconnect.mock_calls) == 2 - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - - mqtt_client_mock.disconnect() - mqtt_client_mock.on_disconnect(None, None, 0) - await hass.async_block_till_done() - - async_fire_time_changed( - hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) - ) - await hass.async_block_till_done() - # Should not reconnect after stop - assert len(mqtt_client_mock.reconnect.mock_calls) == 2 - - -async def test_server_sock_connect_and_disconnect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test handling the socket connected and disconnected.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) - await hass.async_block_till_done() - - server.close() # mock the server closing the connection on us - - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - await mock_debouncer.wait() - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_CONN_LOST - mqtt_client_mock.on_socket_unregister_write(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_close(mqtt_client_mock, None, client) - mqtt_client_mock.on_disconnect(mqtt_client_mock, None, client) - await hass.async_block_till_done() - mock_debouncer.clear() - unsub() - await hass.async_block_till_done() - assert not mock_debouncer.is_set() - - # Should have failed - assert len(recorded_calls) == 0 - - -async def test_server_sock_buffer_size( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test handling the socket buffer size fails.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - with patch.object(client, "setsockopt", side_effect=OSError("foo")): - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) - await hass.async_block_till_done() - assert "Unable to increase the socket buffer size" in caplog.text - - -async def test_server_sock_buffer_size_with_websocket( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test handling the socket buffer size fails.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - - class FakeWebsocket(paho_mqtt.WebsocketWrapper): - def _do_handshake(self, *args, **kwargs): - pass - - wrapped_socket = FakeWebsocket(client, "127.0.01", 1, False, "/", None) - - with patch.object(client, "setsockopt", side_effect=OSError("foo")): - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, wrapped_socket) - mqtt_client_mock.on_socket_register_write( - mqtt_client_mock, None, wrapped_socket - ) - await hass.async_block_till_done() - assert "Unable to increase the socket buffer size" in caplog.text - - -async def test_client_sock_failure_after_connect( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test handling the socket connected and disconnected.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_writer(mqtt_client_mock, None, client) - await hass.async_block_till_done() - - mqtt_client_mock.loop_write.side_effect = OSError("foo") - client.close() # close the client socket out from under the client - - assert mqtt_client_mock.connect.call_count == 1 - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() - - unsub() - # Should have failed - assert len(recorded_calls) == 0 - - -async def test_loop_write_failure( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test handling the socket connected and disconnected.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) - mqtt_client_mock.loop_write.return_value = paho_mqtt.MQTT_ERR_CONN_LOST - mqtt_client_mock.loop_read.return_value = paho_mqtt.MQTT_ERR_CONN_LOST - - # Fill up the outgoing buffer to ensure that loop_write - # and loop_read are called that next time control is - # returned to the event loop - try: - for _ in range(1000): - server.send(b"long" * 100) - except BlockingIOError: - pass - - server.close() - # Once for the reader callback - await hass.async_block_till_done() - # Another for the writer callback - await hass.async_block_till_done() - # Final for the disconnect callback - await hass.async_block_till_done() - - assert "Disconnected from MQTT server test-broker:1883" in caplog.text - - @pytest.mark.parametrize( "attr", [ From d423dae8ac28ff6348cc162ac882db2dba63a3c3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 19:41:21 +0200 Subject: [PATCH 0326/2411] Fix unknown attribute in MPD (#120657) --- homeassistant/components/mpd/media_player.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index eb34fb6289f..3538b1c7973 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -421,11 +421,6 @@ class MpdDevice(MediaPlayerEntity): """Name of the current input source.""" return self._current_playlist - @property - def source_list(self): - """Return the list of available input sources.""" - return self._playlists - async def async_select_source(self, source: str) -> None: """Choose a different available playlist and play it.""" await self.async_play_media(MediaType.PLAYLIST, source) From 89dfca962fb3b69b08795a4991d785c2843851bc Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 27 Jun 2024 13:41:36 -0400 Subject: [PATCH 0327/2411] Bump upb-lib to 0.5.7 (#120689) --- homeassistant/components/upb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/upb/manifest.json b/homeassistant/components/upb/manifest.json index a5e32dd298e..b208edbc0e5 100644 --- a/homeassistant/components/upb/manifest.json +++ b/homeassistant/components/upb/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/upb", "iot_class": "local_push", "loggers": ["upb_lib"], - "requirements": ["upb-lib==0.5.6"] + "requirements": ["upb-lib==0.5.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index faa49266016..938ea6df7d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2807,7 +2807,7 @@ unifiled==0.11 universal-silabs-flasher==0.0.20 # homeassistant.components.upb -upb-lib==0.5.6 +upb-lib==0.5.7 # homeassistant.components.upcloud upcloud-api==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 089c66173eb..8ea13e8c28c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2181,7 +2181,7 @@ unifi-discovery==1.1.8 universal-silabs-flasher==0.0.20 # homeassistant.components.upb -upb-lib==0.5.6 +upb-lib==0.5.7 # homeassistant.components.upcloud upcloud-api==2.5.1 From eb2d2ce1b39d2c1b098df4307d64ea8afe2a6b8a Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:47:25 -0400 Subject: [PATCH 0328/2411] Use more observations in NWS (#120687) Use more observations --- homeassistant/components/nws/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index ba3a22e5818..381537775da 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -78,8 +78,8 @@ HOURLY = "hourly" OBSERVATION_VALID_TIME = timedelta(minutes=60) FORECAST_VALID_TIME = timedelta(minutes=45) -# A lot of stations update once hourly plus some wiggle room -UPDATE_TIME_PERIOD = timedelta(minutes=70) +# Ask for observations for last four hours +UPDATE_TIME_PERIOD = timedelta(minutes=240) DEBOUNCE_TIME = 10 * 60 # in seconds DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) From f4b76406f269e2d0a600e6cee3b9376441a12ce5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 27 Jun 2024 19:54:44 +0200 Subject: [PATCH 0329/2411] Add capsys to enforce-type-hints plugin (#120653) --- pylint/plugins/hass_enforce_type_hints.py | 3 +- .../components/srp_energy/test_config_flow.py | 7 ++-- tests/pylint/test_enforce_type_hints.py | 2 ++ tests/scripts/test_auth.py | 32 +++++++++++++++---- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 67eea59bc9a..f5d5b86635a 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -79,7 +79,7 @@ _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"\]$" + rf"^([\w\.]+)\[{_INNER_MATCH}" + f", {_INNER_MATCH}" * (i - 1) + r"\]$" ) for i in _INNER_MATCH_POSSIBILITIES } @@ -102,6 +102,7 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "area_registry": "AreaRegistry", "async_setup_recorder_instance": "RecorderInstanceGenerator", "caplog": "pytest.LogCaptureFixture", + "capsys": "pytest.CaptureFixture[str]", "current_request_with_host": "None", "device_registry": "DeviceRegistry", "enable_bluetooth": "None", diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index 19e21f0e1a0..e3abb3c98df 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -2,6 +2,8 @@ from unittest.mock import MagicMock, patch +import pytest + from homeassistant.components.srp_energy.const import CONF_IS_TOU, DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_SOURCE, CONF_USERNAME @@ -23,8 +25,9 @@ from . import ( from tests.common import MockConfigEntry +@pytest.mark.usefixtures("mock_srp_energy_config_flow") async def test_show_form( - hass: HomeAssistant, mock_srp_energy_config_flow: MagicMock, capsys + hass: HomeAssistant, capsys: pytest.CaptureFixture[str] ) -> None: """Test show configuration form.""" result = await hass.config_entries.flow.async_init( @@ -140,7 +143,7 @@ async def test_flow_entry_already_configured( async def test_flow_multiple_configs( - hass: HomeAssistant, init_integration: MockConfigEntry, capsys + hass: HomeAssistant, init_integration: MockConfigEntry ) -> None: """Test multiple config entries.""" # Verify mock config setup from fixture diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 5b1c494568d..b1692d1d60d 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -55,6 +55,7 @@ def test_regex_get_module_platform( ("list[dict[str, Any]]", 1, ("list", "dict[str, Any]")), ("tuple[bytes | None, str | None]", 2, ("tuple", "bytes | None", "str | None")), ("Callable[[], TestServer]", 2, ("Callable", "[]", "TestServer")), + ("pytest.CaptureFixture[str]", 1, ("pytest.CaptureFixture", "str")), ], ) def test_regex_x_of_y_i( @@ -1264,6 +1265,7 @@ def test_pytest_fixture(linter: UnittestLinter, type_hint_checker: BaseChecker) def sample_fixture( #@ hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + capsys: pytest.CaptureFixture[str], aiohttp_server: Callable[[], TestServer], unused_tcp_port_factory: Callable[[], int], enable_custom_integrations: None, diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index f497751a4d7..19a9277a36a 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -6,6 +6,7 @@ from typing import Any from unittest.mock import Mock, patch import pytest +from typing_extensions import Generator from homeassistant.auth.providers import homeassistant as hass_auth from homeassistant.core import HomeAssistant @@ -15,7 +16,7 @@ from tests.common import register_auth_provider @pytest.fixture(autouse=True) -def reset_log_level(): +def reset_log_level() -> Generator[None]: """Reset log level after each test case.""" logger = logging.getLogger("homeassistant.core") orig_level = logger.level @@ -24,7 +25,7 @@ def reset_log_level(): @pytest.fixture -def provider(hass): +def provider(hass: HomeAssistant) -> hass_auth.HassAuthProvider: """Home Assistant auth provider.""" provider = hass.loop.run_until_complete( register_auth_provider(hass, {"type": "homeassistant"}) @@ -33,7 +34,11 @@ def provider(hass): return provider -async def test_list_user(hass: HomeAssistant, provider, capsys) -> None: +async def test_list_user( + hass: HomeAssistant, + provider: hass_auth.HassAuthProvider, + capsys: pytest.CaptureFixture[str], +) -> None: """Test we can list users.""" data = provider.data data.add_auth("test-user", "test-pass") @@ -47,7 +52,10 @@ async def test_list_user(hass: HomeAssistant, provider, capsys) -> None: async def test_add_user( - hass: HomeAssistant, provider, capsys, hass_storage: dict[str, Any] + hass: HomeAssistant, + provider: hass_auth.HassAuthProvider, + capsys: pytest.CaptureFixture[str], + hass_storage: dict[str, Any], ) -> None: """Test we can add a user.""" data = provider.data @@ -64,7 +72,11 @@ async def test_add_user( data.validate_login("paulus", "test-pass") -async def test_validate_login(hass: HomeAssistant, provider, capsys) -> None: +async def test_validate_login( + hass: HomeAssistant, + provider: hass_auth.HassAuthProvider, + capsys: pytest.CaptureFixture[str], +) -> None: """Test we can validate a user login.""" data = provider.data data.add_auth("test-user", "test-pass") @@ -89,7 +101,10 @@ async def test_validate_login(hass: HomeAssistant, provider, capsys) -> None: async def test_change_password( - hass: HomeAssistant, provider, capsys, hass_storage: dict[str, Any] + hass: HomeAssistant, + provider: hass_auth.HassAuthProvider, + capsys: pytest.CaptureFixture[str], + hass_storage: dict[str, Any], ) -> None: """Test we can change a password.""" data = provider.data @@ -108,7 +123,10 @@ async def test_change_password( async def test_change_password_invalid_user( - hass: HomeAssistant, provider, capsys, hass_storage: dict[str, Any] + hass: HomeAssistant, + provider: hass_auth.HassAuthProvider, + capsys: pytest.CaptureFixture[str], + hass_storage: dict[str, Any], ) -> None: """Test changing password of non-existing user.""" data = provider.data From c419d226d542c9f3943561ef7a8ee298ee1b146e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 12:58:42 -0500 Subject: [PATCH 0330/2411] Bump uiprotect to 4.2.0 (#120669) --- 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 d0b4947f8fe..842db5d2ee1 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==4.0.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==4.2.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 938ea6df7d9..5ed983e4a17 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2789,7 +2789,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==4.0.0 +uiprotect==4.2.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ea13e8c28c..d98590b53e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2169,7 +2169,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==4.0.0 +uiprotect==4.2.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 629dab238fcc942af761eb20033837d540104ed6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 27 Jun 2024 20:24:22 +0200 Subject: [PATCH 0331/2411] Improve type hints in enphase_envoy tests (#120676) --- tests/components/enphase_envoy/conftest.py | 31 ++++++++++--------- tests/components/enphase_envoy/test_sensor.py | 9 ++++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py index 5dd62419b2b..647084c21ff 100644 --- a/tests/components/enphase_envoy/conftest.py +++ b/tests/components/enphase_envoy/conftest.py @@ -21,6 +21,7 @@ from pyenphase.models.meters import ( EnvoyPhaseMode, ) import pytest +from typing_extensions import AsyncGenerator from homeassistant.components.enphase_envoy import DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -31,7 +32,9 @@ from tests.common import MockConfigEntry @pytest.fixture(name="config_entry") -def config_entry_fixture(hass: HomeAssistant, config, serial_number): +def config_entry_fixture( + hass: HomeAssistant, config: dict[str, str], serial_number: str +) -> MockConfigEntry: """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, @@ -45,7 +48,7 @@ def config_entry_fixture(hass: HomeAssistant, config, serial_number): @pytest.fixture(name="config") -def config_fixture(): +def config_fixture() -> dict[str, str]: """Define a config entry data fixture.""" return { CONF_HOST: "1.1.1.1", @@ -57,11 +60,11 @@ def config_fixture(): @pytest.fixture(name="mock_envoy") def mock_envoy_fixture( - serial_number, - mock_authenticate, - mock_setup, - mock_auth, -): + serial_number: str, + mock_authenticate: AsyncMock, + mock_setup: AsyncMock, + mock_auth: EnvoyTokenAuth, +) -> Mock: """Define a mocked Envoy fixture.""" mock_envoy = Mock(spec=Envoy) mock_envoy.serial_number = serial_number @@ -352,9 +355,9 @@ def mock_envoy_fixture( @pytest.fixture(name="setup_enphase_envoy") async def setup_enphase_envoy_fixture( hass: HomeAssistant, - config, - mock_envoy, -): + config: dict[str, str], + mock_envoy: Mock, +) -> AsyncGenerator[None]: """Define a fixture to set up Enphase Envoy.""" with ( patch( @@ -372,13 +375,13 @@ async def setup_enphase_envoy_fixture( @pytest.fixture(name="mock_authenticate") -def mock_authenticate(): +def mock_authenticate() -> AsyncMock: """Define a mocked Envoy.authenticate fixture.""" return AsyncMock() @pytest.fixture(name="mock_auth") -def mock_auth(serial_number): +def mock_auth(serial_number: str) -> EnvoyTokenAuth: """Define a mocked EnvoyAuth fixture.""" token = jwt.encode( payload={"name": "envoy", "exp": 1907837780}, key="secret", algorithm="HS256" @@ -387,12 +390,12 @@ def mock_auth(serial_number): @pytest.fixture(name="mock_setup") -def mock_setup(): +def mock_setup() -> AsyncMock: """Define a mocked Envoy.setup fixture.""" return AsyncMock() @pytest.fixture(name="serial_number") -def serial_number_fixture(): +def serial_number_fixture() -> str: """Define a serial number fixture.""" return "1234" diff --git a/tests/components/enphase_envoy/test_sensor.py b/tests/components/enphase_envoy/test_sensor.py index 13727e29eac..bfb6fdb2826 100644 --- a/tests/components/enphase_envoy/test_sensor.py +++ b/tests/components/enphase_envoy/test_sensor.py @@ -1,9 +1,10 @@ """Test Enphase Envoy sensors.""" -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest from syrupy.assertion import SnapshotAssertion +from typing_extensions import AsyncGenerator from homeassistant.components.enphase_envoy import DOMAIN from homeassistant.components.enphase_envoy.const import Platform @@ -15,7 +16,9 @@ from tests.common import MockConfigEntry @pytest.fixture(name="setup_enphase_envoy_sensor") -async def setup_enphase_envoy_sensor_fixture(hass, config, mock_envoy): +async def setup_enphase_envoy_sensor_fixture( + hass: HomeAssistant, config: dict[str, str], mock_envoy: Mock +) -> AsyncGenerator[None]: """Define a fixture to set up Enphase Envoy with sensor platform only.""" with ( patch( @@ -41,7 +44,7 @@ async def test_sensor( entity_registry: er.EntityRegistry, config_entry: MockConfigEntry, snapshot: SnapshotAssertion, - setup_enphase_envoy_sensor, + setup_enphase_envoy_sensor: None, ) -> None: """Test enphase_envoy sensor entities.""" # compare registered entities against snapshot of prior run From aaef31958be6b7116a10b8158349d856db526b11 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 27 Jun 2024 21:29:17 +0200 Subject: [PATCH 0332/2411] Bump aioautomower to 2024.6.3 (#120697) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 5ca1b500340..7883b057a3f 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.6.1"] + "requirements": ["aioautomower==2024.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5ed983e4a17..5308b0ac511 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -204,7 +204,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.1 +aioautomower==2024.6.3 # homeassistant.components.azure_devops aioazuredevops==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d98590b53e6..f112496a401 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -183,7 +183,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.1 +aioautomower==2024.6.3 # homeassistant.components.azure_devops aioazuredevops==2.1.1 From 195643d916354919b2bd10b84d2c4115fd0ce1db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Thu, 27 Jun 2024 23:05:58 +0300 Subject: [PATCH 0333/2411] Improve AtlanticDomesticHotWaterProductionMBLComponent support in Overkiz (#114178) * add overkiz AtlanticDHW support Adds support of Overkiz water heater entity selection based on device controllable_name Adds support of Atlantic water heater based on Atlantic Steatite Cube WI-FI VM 150 S4CS 2400W Adds more Overkiz water heater binary_sensors, numbers, and sensors * Changed class annotation * min_temp and max_temp as properties * reverted binary_sensors, number, sensor to make separate PRs * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * review fixes, typos, and pylint * review fix * review fix * ruff * temperature properties changed to constructor attributes * logger removed * constants usage consistency * redundant mapping removed * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * boost mode method annotation typo * removed away mode for atlantic dwh * absence and boost mode attributes now support 'prog' state * heating status bugfix * electrical consumption sensor * warm water remaining volume sensor * away mode reintroduced * mypy check * boost plus state support * Update homeassistant/components/overkiz/sensor.py Co-authored-by: Mick Vleeshouwer * sensors reverted to separate them into their own PR * check away and boost modes on before switching them off * atlantic_dhw renamed to atlantic_domestic_hot_water_production * annotation changed * AtlanticDomesticHotWaterProductionMBLComponent file renamed, annotation change reverted --------- Co-authored-by: Mick Vleeshouwer --- .../components/overkiz/binary_sensor.py | 9 +- .../components/overkiz/water_heater.py | 29 ++- .../overkiz/water_heater_entities/__init__.py | 7 + ...stic_hot_water_production_mlb_component.py | 182 ++++++++++++++++++ 4 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py diff --git a/homeassistant/components/overkiz/binary_sensor.py b/homeassistant/components/overkiz/binary_sensor.py index c37afc9cb0c..8ea86e03e8c 100644 --- a/homeassistant/components/overkiz/binary_sensor.py +++ b/homeassistant/components/overkiz/binary_sensor.py @@ -109,17 +109,20 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [ key=OverkizState.CORE_HEATING_STATUS, name="Heating status", device_class=BinarySensorDeviceClass.HEAT, - value_fn=lambda state: state == OverkizCommandParam.ON, + value_fn=lambda state: cast(str, state).lower() + in (OverkizCommandParam.ON, OverkizCommandParam.HEATING), ), OverkizBinarySensorDescription( key=OverkizState.MODBUSLINK_DHW_ABSENCE_MODE, name="Absence mode", - value_fn=lambda state: state == OverkizCommandParam.ON, + value_fn=lambda state: state + in (OverkizCommandParam.ON, OverkizCommandParam.PROG), ), OverkizBinarySensorDescription( key=OverkizState.MODBUSLINK_DHW_BOOST_MODE, name="Boost mode", - value_fn=lambda state: state == OverkizCommandParam.ON, + value_fn=lambda state: state + in (OverkizCommandParam.ON, OverkizCommandParam.PROG), ), ] diff --git a/homeassistant/components/overkiz/water_heater.py b/homeassistant/components/overkiz/water_heater.py index c76f6d5099f..99bfb279e4c 100644 --- a/homeassistant/components/overkiz/water_heater.py +++ b/homeassistant/components/overkiz/water_heater.py @@ -9,7 +9,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantOverkizData from .const import DOMAIN -from .water_heater_entities import WIDGET_TO_WATER_HEATER_ENTITY +from .entity import OverkizEntity +from .water_heater_entities import ( + CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY, + WIDGET_TO_WATER_HEATER_ENTITY, +) async def async_setup_entry( @@ -19,11 +23,20 @@ async def async_setup_entry( ) -> None: """Set up the Overkiz DHW from a config entry.""" data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + entities: list[OverkizEntity] = [] - async_add_entities( - WIDGET_TO_WATER_HEATER_ENTITY[device.widget]( - device.device_url, data.coordinator - ) - for device in data.platforms[Platform.WATER_HEATER] - if device.widget in WIDGET_TO_WATER_HEATER_ENTITY - ) + for device in data.platforms[Platform.WATER_HEATER]: + if device.controllable_name in CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY: + entities.append( + CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY[device.controllable_name]( + device.device_url, data.coordinator + ) + ) + elif device.widget in WIDGET_TO_WATER_HEATER_ENTITY: + entities.append( + WIDGET_TO_WATER_HEATER_ENTITY[device.widget]( + device.device_url, data.coordinator + ) + ) + + async_add_entities(entities) diff --git a/homeassistant/components/overkiz/water_heater_entities/__init__.py b/homeassistant/components/overkiz/water_heater_entities/__init__.py index 6f6539ef659..fdc41f213c6 100644 --- a/homeassistant/components/overkiz/water_heater_entities/__init__.py +++ b/homeassistant/components/overkiz/water_heater_entities/__init__.py @@ -2,6 +2,9 @@ from pyoverkiz.enums.ui import UIWidget +from .atlantic_domestic_hot_water_production_mlb_component import ( + AtlanticDomesticHotWaterProductionMBLComponent, +) from .atlantic_pass_apc_dhw import AtlanticPassAPCDHW from .domestic_hot_water_production import DomesticHotWaterProduction from .hitachi_dhw import HitachiDHW @@ -11,3 +14,7 @@ WIDGET_TO_WATER_HEATER_ENTITY = { UIWidget.DOMESTIC_HOT_WATER_PRODUCTION: DomesticHotWaterProduction, UIWidget.HITACHI_DHW: HitachiDHW, } + +CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY = { + "modbuslink:AtlanticDomesticHotWaterProductionMBLComponent": AtlanticDomesticHotWaterProductionMBLComponent, +} diff --git a/homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py b/homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py new file mode 100644 index 00000000000..de995a2bd1a --- /dev/null +++ b/homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py @@ -0,0 +1,182 @@ +"""Support for AtlanticDomesticHotWaterProductionMBLComponent.""" + +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.water_heater import ( + STATE_ECO, + STATE_OFF, + STATE_PERFORMANCE, + WaterHeaterEntity, + WaterHeaterEntityFeature, +) +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature + +from .. import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + + +class AtlanticDomesticHotWaterProductionMBLComponent(OverkizEntity, WaterHeaterEntity): + """Representation of AtlanticDomesticHotWaterProductionMBLComponent (modbuslink).""" + + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.OPERATION_MODE + | WaterHeaterEntityFeature.AWAY_MODE + | WaterHeaterEntityFeature.ON_OFF + ) + _attr_operation_list = [ + OverkizCommandParam.PERFORMANCE, + OverkizCommandParam.ECO, + OverkizCommandParam.MANUAL, + ] + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Init method.""" + super().__init__(device_url, coordinator) + self._attr_max_temp = cast( + float, + self.executor.select_state( + OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE + ), + ) + self._attr_min_temp = cast( + float, + self.executor.select_state( + OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE + ), + ) + + @property + def current_temperature(self) -> float: + """Return the current temperature.""" + return cast( + float, + self.executor.select_state( + OverkizState.MODBUSLINK_MIDDLE_WATER_TEMPERATURE + ), + ) + + @property + def target_temperature(self) -> float: + """Return the temperature corresponding to the PRESET.""" + return cast( + float, + self.executor.select_state(OverkizState.CORE_WATER_TARGET_TEMPERATURE), + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new temperature.""" + temperature = kwargs[ATTR_TEMPERATURE] + await self.executor.async_execute_command( + OverkizCommand.SET_TARGET_DHW_TEMPERATURE, temperature + ) + + @property + def is_boost_mode_on(self) -> bool: + """Return true if boost mode is on.""" + return self.executor.select_state(OverkizState.MODBUSLINK_DHW_BOOST_MODE) in ( + OverkizCommandParam.ON, + OverkizCommandParam.PROG, + ) + + @property + def is_eco_mode_on(self) -> bool: + """Return true if eco mode is on.""" + return self.executor.select_state(OverkizState.MODBUSLINK_DHW_MODE) in ( + OverkizCommandParam.MANUAL_ECO_ACTIVE, + OverkizCommandParam.AUTO_MODE, + ) + + @property + def is_away_mode_on(self) -> bool: + """Return true if away mode is on.""" + return ( + self.executor.select_state(OverkizState.MODBUSLINK_DHW_ABSENCE_MODE) + == OverkizCommandParam.ON + ) + + @property + def current_operation(self) -> str: + """Return current operation.""" + if self.is_away_mode_on: + return STATE_OFF + + if self.is_boost_mode_on: + return STATE_PERFORMANCE + + if self.is_eco_mode_on: + return STATE_ECO + + if ( + cast(str, self.executor.select_state(OverkizState.MODBUSLINK_DHW_MODE)) + == OverkizCommandParam.MANUAL_ECO_INACTIVE + ): + return OverkizCommandParam.MANUAL + + return STATE_OFF + + async def async_set_operation_mode(self, operation_mode: str) -> None: + """Set new operation mode.""" + if operation_mode in (STATE_PERFORMANCE, OverkizCommandParam.BOOST): + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + await self.async_turn_boost_mode_on() + elif operation_mode in ( + OverkizCommandParam.ECO, + OverkizCommandParam.MANUAL_ECO_ACTIVE, + ): + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + if self.is_boost_mode_on: + await self.async_turn_boost_mode_off() + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_MODE, OverkizCommandParam.AUTO_MODE + ) + elif operation_mode in ( + OverkizCommandParam.MANUAL, + OverkizCommandParam.MANUAL_ECO_INACTIVE, + ): + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + if self.is_boost_mode_on: + await self.async_turn_boost_mode_off() + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_MODE, OverkizCommandParam.MANUAL_ECO_INACTIVE + ) + else: + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + if self.is_boost_mode_on: + await self.async_turn_boost_mode_off() + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_MODE, operation_mode + ) + + async def async_turn_away_mode_on(self) -> None: + """Turn away mode on.""" + await self.executor.async_execute_command( + OverkizCommand.SET_ABSENCE_MODE, OverkizCommandParam.ON + ) + + async def async_turn_away_mode_off(self) -> None: + """Turn away mode off.""" + await self.executor.async_execute_command( + OverkizCommand.SET_ABSENCE_MODE, OverkizCommandParam.OFF + ) + + async def async_turn_boost_mode_on(self) -> None: + """Turn boost mode on.""" + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_MODE, OverkizCommandParam.ON + ) + + async def async_turn_boost_mode_off(self) -> None: + """Turn boost mode off.""" + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_MODE, OverkizCommandParam.OFF + ) From c4ab0dcd74c4d7fbeeddde470e07ce24c1cd3f7d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 27 Jun 2024 22:06:30 +0200 Subject: [PATCH 0334/2411] Update frontend to 20240627.0 (#120693) --- 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 89c8fbe30ca..cd46b358335 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240626.2"] + "requirements": ["home-assistant-frontend==20240627.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9aed0850478..239ee7575a9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240626.2 +home-assistant-frontend==20240627.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5308b0ac511..da38d82baf2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.2 +home-assistant-frontend==20240627.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f112496a401..13b886c48f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.2 +home-assistant-frontend==20240627.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From 642161242902589f9935fe1e1c6a4bacb8e11362 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 22:09:33 +0200 Subject: [PATCH 0335/2411] Bump ttls to 1.8.3 (#120700) --- homeassistant/components/twinkly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json index 6ec89261b3d..a84eebf0f28 100644 --- a/homeassistant/components/twinkly/manifest.json +++ b/homeassistant/components/twinkly/manifest.json @@ -14,5 +14,5 @@ "documentation": "https://www.home-assistant.io/integrations/twinkly", "iot_class": "local_polling", "loggers": ["ttls"], - "requirements": ["ttls==1.5.1"] + "requirements": ["ttls==1.8.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index da38d82baf2..c3b2f7c2f27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2768,7 +2768,7 @@ tplink-omada-client==1.3.12 transmission-rpc==7.0.3 # homeassistant.components.twinkly -ttls==1.5.1 +ttls==1.8.3 # homeassistant.components.thethingsnetwork ttn_client==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13b886c48f8..4ce80f0ea91 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2148,7 +2148,7 @@ tplink-omada-client==1.3.12 transmission-rpc==7.0.3 # homeassistant.components.twinkly -ttls==1.5.1 +ttls==1.8.3 # homeassistant.components.thethingsnetwork ttn_client==1.0.0 From 338687522a6488e2b3dc9b28d3ccfa9c6a2cab0c Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 27 Jun 2024 16:10:11 -0400 Subject: [PATCH 0336/2411] Bump Environment Canada to 0.7.1 (#120699) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 69a6cd7c69b..c77d35b1769 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.7.0"] + "requirements": ["env-canada==0.7.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index c3b2f7c2f27..ce60cad8346 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -816,7 +816,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.7.0 +env-canada==0.7.1 # homeassistant.components.season ephem==4.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ce80f0ea91..1d2a6ad948e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ energyzero==2.1.0 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.7.0 +env-canada==0.7.1 # homeassistant.components.season ephem==4.1.5 From 440928bcbe27e187d10e4801d24e8062128d9648 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 15:10:48 -0500 Subject: [PATCH 0337/2411] Bump unifi-discovery to 1.2.0 (#120684) --- 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 842db5d2ee1..6691d738cd0 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==4.2.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==4.2.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index ce60cad8346..98b5548cd38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2795,7 +2795,7 @@ uiprotect==4.2.0 ultraheat-api==0.5.7 # homeassistant.components.unifiprotect -unifi-discovery==1.1.8 +unifi-discovery==1.2.0 # homeassistant.components.unifi_direct unifi_ap==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d2a6ad948e..2857aee8c9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2175,7 +2175,7 @@ uiprotect==4.2.0 ultraheat-api==0.5.7 # homeassistant.components.unifiprotect -unifi-discovery==1.1.8 +unifi-discovery==1.2.0 # homeassistant.components.zha universal-silabs-flasher==0.0.20 From bccd5c8c355c83ad167726163e5f6f21ff389426 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 27 Jun 2024 22:11:24 +0200 Subject: [PATCH 0338/2411] Improve type hints in evil_genius_labs tests (#120677) --- tests/components/evil_genius_labs/conftest.py | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/tests/components/evil_genius_labs/conftest.py b/tests/components/evil_genius_labs/conftest.py index 3941917e130..081b7a5120a 100644 --- a/tests/components/evil_genius_labs/conftest.py +++ b/tests/components/evil_genius_labs/conftest.py @@ -1,36 +1,44 @@ """Test helpers for Evil Genius Labs.""" -import json +from typing import Any from unittest.mock import patch import pytest +from typing_extensions import AsyncGenerator +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util.json import JsonObjectType -from tests.common import MockConfigEntry, load_fixture +from tests.common import ( + MockConfigEntry, + load_json_array_fixture, + load_json_object_fixture, +) @pytest.fixture(scope="package") -def all_fixture(): +def all_fixture() -> dict[str, Any]: """Fixture data.""" - data = json.loads(load_fixture("data.json", "evil_genius_labs")) + data = load_json_array_fixture("data.json", "evil_genius_labs") return {item["name"]: item for item in data} @pytest.fixture(scope="package") -def info_fixture(): +def info_fixture() -> JsonObjectType: """Fixture info.""" - return json.loads(load_fixture("info.json", "evil_genius_labs")) + return load_json_object_fixture("info.json", "evil_genius_labs") @pytest.fixture(scope="package") -def product_fixture(): +def product_fixture() -> dict[str, str]: """Fixture info.""" return {"productName": "Fibonacci256"} @pytest.fixture -def config_entry(hass): +def config_entry(hass: HomeAssistant) -> MockConfigEntry: """Evil genius labs config entry.""" entry = MockConfigEntry(domain="evil_genius_labs", data={"host": "192.168.1.113"}) entry.add_to_hass(hass) @@ -39,8 +47,13 @@ def config_entry(hass): @pytest.fixture async def setup_evil_genius_labs( - hass, config_entry, all_fixture, info_fixture, product_fixture, platforms -): + hass: HomeAssistant, + config_entry: MockConfigEntry, + all_fixture: dict[str, Any], + info_fixture: JsonObjectType, + product_fixture: dict[str, str], + platforms: list[Platform], +) -> AsyncGenerator[None]: """Test up Evil Genius Labs instance.""" with ( patch( From 53e49861a1c9ee061a9a55cd358c05274f1845db Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 27 Jun 2024 23:26:38 +1200 Subject: [PATCH 0339/2411] Mark esphome integration as platinum (#112565) Co-authored-by: J. Nick Koston --- homeassistant/components/esphome/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index ab175028bea..6e30febd7db 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,6 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "mqtt": ["esphome/discover/#"], + "quality_scale": "platinum", "requirements": [ "aioesphomeapi==24.6.1", "esphome-dashboard-api==1.2.3", From 2c2261254b45e074a62ddc3625428181a9d1ba61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Thu, 27 Jun 2024 23:05:58 +0300 Subject: [PATCH 0340/2411] Improve AtlanticDomesticHotWaterProductionMBLComponent support in Overkiz (#114178) * add overkiz AtlanticDHW support Adds support of Overkiz water heater entity selection based on device controllable_name Adds support of Atlantic water heater based on Atlantic Steatite Cube WI-FI VM 150 S4CS 2400W Adds more Overkiz water heater binary_sensors, numbers, and sensors * Changed class annotation * min_temp and max_temp as properties * reverted binary_sensors, number, sensor to make separate PRs * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * review fixes, typos, and pylint * review fix * review fix * ruff * temperature properties changed to constructor attributes * logger removed * constants usage consistency * redundant mapping removed * Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py Co-authored-by: Mick Vleeshouwer * boost mode method annotation typo * removed away mode for atlantic dwh * absence and boost mode attributes now support 'prog' state * heating status bugfix * electrical consumption sensor * warm water remaining volume sensor * away mode reintroduced * mypy check * boost plus state support * Update homeassistant/components/overkiz/sensor.py Co-authored-by: Mick Vleeshouwer * sensors reverted to separate them into their own PR * check away and boost modes on before switching them off * atlantic_dhw renamed to atlantic_domestic_hot_water_production * annotation changed * AtlanticDomesticHotWaterProductionMBLComponent file renamed, annotation change reverted --------- Co-authored-by: Mick Vleeshouwer --- .../components/overkiz/binary_sensor.py | 9 +- .../components/overkiz/water_heater.py | 29 ++- .../overkiz/water_heater_entities/__init__.py | 7 + ...stic_hot_water_production_mlb_component.py | 182 ++++++++++++++++++ 4 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py diff --git a/homeassistant/components/overkiz/binary_sensor.py b/homeassistant/components/overkiz/binary_sensor.py index c37afc9cb0c..8ea86e03e8c 100644 --- a/homeassistant/components/overkiz/binary_sensor.py +++ b/homeassistant/components/overkiz/binary_sensor.py @@ -109,17 +109,20 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [ key=OverkizState.CORE_HEATING_STATUS, name="Heating status", device_class=BinarySensorDeviceClass.HEAT, - value_fn=lambda state: state == OverkizCommandParam.ON, + value_fn=lambda state: cast(str, state).lower() + in (OverkizCommandParam.ON, OverkizCommandParam.HEATING), ), OverkizBinarySensorDescription( key=OverkizState.MODBUSLINK_DHW_ABSENCE_MODE, name="Absence mode", - value_fn=lambda state: state == OverkizCommandParam.ON, + value_fn=lambda state: state + in (OverkizCommandParam.ON, OverkizCommandParam.PROG), ), OverkizBinarySensorDescription( key=OverkizState.MODBUSLINK_DHW_BOOST_MODE, name="Boost mode", - value_fn=lambda state: state == OverkizCommandParam.ON, + value_fn=lambda state: state + in (OverkizCommandParam.ON, OverkizCommandParam.PROG), ), ] diff --git a/homeassistant/components/overkiz/water_heater.py b/homeassistant/components/overkiz/water_heater.py index c76f6d5099f..99bfb279e4c 100644 --- a/homeassistant/components/overkiz/water_heater.py +++ b/homeassistant/components/overkiz/water_heater.py @@ -9,7 +9,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantOverkizData from .const import DOMAIN -from .water_heater_entities import WIDGET_TO_WATER_HEATER_ENTITY +from .entity import OverkizEntity +from .water_heater_entities import ( + CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY, + WIDGET_TO_WATER_HEATER_ENTITY, +) async def async_setup_entry( @@ -19,11 +23,20 @@ async def async_setup_entry( ) -> None: """Set up the Overkiz DHW from a config entry.""" data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + entities: list[OverkizEntity] = [] - async_add_entities( - WIDGET_TO_WATER_HEATER_ENTITY[device.widget]( - device.device_url, data.coordinator - ) - for device in data.platforms[Platform.WATER_HEATER] - if device.widget in WIDGET_TO_WATER_HEATER_ENTITY - ) + for device in data.platforms[Platform.WATER_HEATER]: + if device.controllable_name in CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY: + entities.append( + CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY[device.controllable_name]( + device.device_url, data.coordinator + ) + ) + elif device.widget in WIDGET_TO_WATER_HEATER_ENTITY: + entities.append( + WIDGET_TO_WATER_HEATER_ENTITY[device.widget]( + device.device_url, data.coordinator + ) + ) + + async_add_entities(entities) diff --git a/homeassistant/components/overkiz/water_heater_entities/__init__.py b/homeassistant/components/overkiz/water_heater_entities/__init__.py index 6f6539ef659..fdc41f213c6 100644 --- a/homeassistant/components/overkiz/water_heater_entities/__init__.py +++ b/homeassistant/components/overkiz/water_heater_entities/__init__.py @@ -2,6 +2,9 @@ from pyoverkiz.enums.ui import UIWidget +from .atlantic_domestic_hot_water_production_mlb_component import ( + AtlanticDomesticHotWaterProductionMBLComponent, +) from .atlantic_pass_apc_dhw import AtlanticPassAPCDHW from .domestic_hot_water_production import DomesticHotWaterProduction from .hitachi_dhw import HitachiDHW @@ -11,3 +14,7 @@ WIDGET_TO_WATER_HEATER_ENTITY = { UIWidget.DOMESTIC_HOT_WATER_PRODUCTION: DomesticHotWaterProduction, UIWidget.HITACHI_DHW: HitachiDHW, } + +CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY = { + "modbuslink:AtlanticDomesticHotWaterProductionMBLComponent": AtlanticDomesticHotWaterProductionMBLComponent, +} diff --git a/homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py b/homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py new file mode 100644 index 00000000000..de995a2bd1a --- /dev/null +++ b/homeassistant/components/overkiz/water_heater_entities/atlantic_domestic_hot_water_production_mlb_component.py @@ -0,0 +1,182 @@ +"""Support for AtlanticDomesticHotWaterProductionMBLComponent.""" + +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.water_heater import ( + STATE_ECO, + STATE_OFF, + STATE_PERFORMANCE, + WaterHeaterEntity, + WaterHeaterEntityFeature, +) +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature + +from .. import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + + +class AtlanticDomesticHotWaterProductionMBLComponent(OverkizEntity, WaterHeaterEntity): + """Representation of AtlanticDomesticHotWaterProductionMBLComponent (modbuslink).""" + + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.OPERATION_MODE + | WaterHeaterEntityFeature.AWAY_MODE + | WaterHeaterEntityFeature.ON_OFF + ) + _attr_operation_list = [ + OverkizCommandParam.PERFORMANCE, + OverkizCommandParam.ECO, + OverkizCommandParam.MANUAL, + ] + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Init method.""" + super().__init__(device_url, coordinator) + self._attr_max_temp = cast( + float, + self.executor.select_state( + OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE + ), + ) + self._attr_min_temp = cast( + float, + self.executor.select_state( + OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE + ), + ) + + @property + def current_temperature(self) -> float: + """Return the current temperature.""" + return cast( + float, + self.executor.select_state( + OverkizState.MODBUSLINK_MIDDLE_WATER_TEMPERATURE + ), + ) + + @property + def target_temperature(self) -> float: + """Return the temperature corresponding to the PRESET.""" + return cast( + float, + self.executor.select_state(OverkizState.CORE_WATER_TARGET_TEMPERATURE), + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new temperature.""" + temperature = kwargs[ATTR_TEMPERATURE] + await self.executor.async_execute_command( + OverkizCommand.SET_TARGET_DHW_TEMPERATURE, temperature + ) + + @property + def is_boost_mode_on(self) -> bool: + """Return true if boost mode is on.""" + return self.executor.select_state(OverkizState.MODBUSLINK_DHW_BOOST_MODE) in ( + OverkizCommandParam.ON, + OverkizCommandParam.PROG, + ) + + @property + def is_eco_mode_on(self) -> bool: + """Return true if eco mode is on.""" + return self.executor.select_state(OverkizState.MODBUSLINK_DHW_MODE) in ( + OverkizCommandParam.MANUAL_ECO_ACTIVE, + OverkizCommandParam.AUTO_MODE, + ) + + @property + def is_away_mode_on(self) -> bool: + """Return true if away mode is on.""" + return ( + self.executor.select_state(OverkizState.MODBUSLINK_DHW_ABSENCE_MODE) + == OverkizCommandParam.ON + ) + + @property + def current_operation(self) -> str: + """Return current operation.""" + if self.is_away_mode_on: + return STATE_OFF + + if self.is_boost_mode_on: + return STATE_PERFORMANCE + + if self.is_eco_mode_on: + return STATE_ECO + + if ( + cast(str, self.executor.select_state(OverkizState.MODBUSLINK_DHW_MODE)) + == OverkizCommandParam.MANUAL_ECO_INACTIVE + ): + return OverkizCommandParam.MANUAL + + return STATE_OFF + + async def async_set_operation_mode(self, operation_mode: str) -> None: + """Set new operation mode.""" + if operation_mode in (STATE_PERFORMANCE, OverkizCommandParam.BOOST): + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + await self.async_turn_boost_mode_on() + elif operation_mode in ( + OverkizCommandParam.ECO, + OverkizCommandParam.MANUAL_ECO_ACTIVE, + ): + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + if self.is_boost_mode_on: + await self.async_turn_boost_mode_off() + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_MODE, OverkizCommandParam.AUTO_MODE + ) + elif operation_mode in ( + OverkizCommandParam.MANUAL, + OverkizCommandParam.MANUAL_ECO_INACTIVE, + ): + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + if self.is_boost_mode_on: + await self.async_turn_boost_mode_off() + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_MODE, OverkizCommandParam.MANUAL_ECO_INACTIVE + ) + else: + if self.is_away_mode_on: + await self.async_turn_away_mode_off() + if self.is_boost_mode_on: + await self.async_turn_boost_mode_off() + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_MODE, operation_mode + ) + + async def async_turn_away_mode_on(self) -> None: + """Turn away mode on.""" + await self.executor.async_execute_command( + OverkizCommand.SET_ABSENCE_MODE, OverkizCommandParam.ON + ) + + async def async_turn_away_mode_off(self) -> None: + """Turn away mode off.""" + await self.executor.async_execute_command( + OverkizCommand.SET_ABSENCE_MODE, OverkizCommandParam.OFF + ) + + async def async_turn_boost_mode_on(self) -> None: + """Turn boost mode on.""" + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_MODE, OverkizCommandParam.ON + ) + + async def async_turn_boost_mode_off(self) -> None: + """Turn boost mode off.""" + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_MODE, OverkizCommandParam.OFF + ) From dcffd6bd7ae0a91a01bc2758462b89603b4cae99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 20:37:08 -0500 Subject: [PATCH 0341/2411] Remove unused fields from unifiprotect event sensors (#120568) --- homeassistant/components/unifiprotect/binary_sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index e35eb6f48f3..c4e1aa87df2 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -426,14 +426,12 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.OCCUPANCY, icon="mdi:doorbell-video", ufp_required_field="feature_flags.is_doorbell", - ufp_value="is_ringing", ufp_event_obj="last_ring_event", ), ProtectBinaryEventEntityDescription( key="motion", name="Motion", device_class=BinarySensorDeviceClass.MOTION, - ufp_value="is_motion_currently_detected", ufp_enabled="is_motion_detection_on", ufp_event_obj="last_motion_event", ), From 210e906a4da8d8f950bdcb2ba2cf7d7d8a980267 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:34:12 +0100 Subject: [PATCH 0342/2411] Store tplink credentials_hash outside of device_config (#120597) --- homeassistant/components/tplink/__init__.py | 42 +++- .../components/tplink/config_flow.py | 43 +++- homeassistant/components/tplink/const.py | 2 + tests/components/tplink/__init__.py | 19 +- tests/components/tplink/test_config_flow.py | 81 ++++++- tests/components/tplink/test_init.py | 217 +++++++++++++++++- 6 files changed, 373 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 764867f0bee..6d300f68aa0 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -43,6 +43,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from .const import ( + CONF_CREDENTIALS_HASH, CONF_DEVICE_CONFIG, CONNECT_TIMEOUT, DISCOVERY_TIMEOUT, @@ -73,6 +74,7 @@ def async_trigger_discovery( discovered_devices: dict[str, Device], ) -> None: """Trigger config flows for discovered devices.""" + for formatted_mac, device in discovered_devices.items(): discovery_flow.async_create_flow( hass, @@ -83,7 +85,6 @@ def async_trigger_discovery( CONF_HOST: device.host, CONF_MAC: formatted_mac, CONF_DEVICE_CONFIG: device.config.to_dict( - credentials_hash=device.credentials_hash, exclude_credentials=True, ), }, @@ -133,6 +134,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo """Set up TPLink from a config entry.""" host: str = entry.data[CONF_HOST] credentials = await get_credentials(hass) + entry_credentials_hash = entry.data.get(CONF_CREDENTIALS_HASH) config: DeviceConfig | None = None if config_dict := entry.data.get(CONF_DEVICE_CONFIG): @@ -151,19 +153,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo config.timeout = CONNECT_TIMEOUT if config.uses_http is True: config.http_client = create_async_tplink_clientsession(hass) + + # If we have in memory credentials use them otherwise check for credentials_hash if credentials: config.credentials = credentials + elif entry_credentials_hash: + config.credentials_hash = entry_credentials_hash + try: device: Device = await Device.connect(config=config) except AuthenticationError as ex: + # If the stored credentials_hash was used but doesn't work remove it + if not credentials and entry_credentials_hash: + data = {k: v for k, v in entry.data.items() if k != CONF_CREDENTIALS_HASH} + hass.config_entries.async_update_entry(entry, data=data) raise ConfigEntryAuthFailed from ex except KasaException as ex: raise ConfigEntryNotReady from ex - device_config_dict = device.config.to_dict( - credentials_hash=device.credentials_hash, exclude_credentials=True - ) + device_credentials_hash = device.credentials_hash + device_config_dict = device.config.to_dict(exclude_credentials=True) + # Do not store the credentials hash inside the device_config + device_config_dict.pop(CONF_CREDENTIALS_HASH, None) updates: dict[str, Any] = {} + if device_credentials_hash and device_credentials_hash != entry_credentials_hash: + updates[CONF_CREDENTIALS_HASH] = device_credentials_hash if device_config_dict != config_dict: updates[CONF_DEVICE_CONFIG] = device_config_dict if entry.data.get(CONF_ALIAS) != device.alias: @@ -326,7 +340,25 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> minor_version = 3 hass.config_entries.async_update_entry(config_entry, minor_version=3) - _LOGGER.debug("Migration to version %s.%s successful", version, minor_version) + if version == 1 and minor_version == 3: + # credentials_hash stored in the device_config should be moved to data. + updates: dict[str, Any] = {} + if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG): + assert isinstance(config_dict, dict) + if credentials_hash := config_dict.pop(CONF_CREDENTIALS_HASH, None): + updates[CONF_CREDENTIALS_HASH] = credentials_hash + updates[CONF_DEVICE_CONFIG] = config_dict + minor_version = 4 + hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, + **updates, + }, + minor_version=minor_version, + ) + _LOGGER.debug("Migration to version %s.%s complete", version, minor_version) + return True diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index 7bead2207a3..5608ccfa72f 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -44,7 +44,13 @@ from . import ( mac_alias, set_credentials, ) -from .const import CONF_DEVICE_CONFIG, CONNECT_TIMEOUT, DOMAIN +from .const import ( + CONF_CONNECTION_TYPE, + CONF_CREDENTIALS_HASH, + CONF_DEVICE_CONFIG, + CONNECT_TIMEOUT, + DOMAIN, +) STEP_AUTH_DATA_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} @@ -55,7 +61,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for tplink.""" VERSION = 1 - MINOR_VERSION = 3 + MINOR_VERSION = 4 reauth_entry: ConfigEntry | None = None def __init__(self) -> None: @@ -95,9 +101,18 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): entry_config_dict = entry_data.get(CONF_DEVICE_CONFIG) if entry_config_dict == config and entry_data[CONF_HOST] == host: return None + updates = {**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host} + # If the connection parameters have changed the credentials_hash will be invalid. + if ( + entry_config_dict + and isinstance(entry_config_dict, dict) + and entry_config_dict.get(CONF_CONNECTION_TYPE) + != config.get(CONF_CONNECTION_TYPE) + ): + updates.pop(CONF_CREDENTIALS_HASH, None) return self.async_update_reload_and_abort( entry, - data={**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host}, + data=updates, reason="already_configured", ) @@ -345,18 +360,22 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): @callback def _async_create_entry_from_device(self, device: Device) -> ConfigFlowResult: """Create a config entry from a smart device.""" + # This is only ever called after a successful device update so we know that + # the credential_hash is correct and should be saved. self._abort_if_unique_id_configured(updates={CONF_HOST: device.host}) + data = { + CONF_HOST: device.host, + CONF_ALIAS: device.alias, + CONF_MODEL: device.model, + CONF_DEVICE_CONFIG: device.config.to_dict( + exclude_credentials=True, + ), + } + if device.credentials_hash: + data[CONF_CREDENTIALS_HASH] = device.credentials_hash return self.async_create_entry( title=f"{device.alias} {device.model}", - data={ - CONF_HOST: device.host, - CONF_ALIAS: device.alias, - CONF_MODEL: device.model, - CONF_DEVICE_CONFIG: device.config.to_dict( - credentials_hash=device.credentials_hash, - exclude_credentials=True, - ), - }, + data=data, ) async def _async_try_discover_and_update( diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index d77d415aa9c..babd92e2c34 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -20,6 +20,8 @@ ATTR_TODAY_ENERGY_KWH: Final = "today_energy_kwh" ATTR_TOTAL_ENERGY_KWH: Final = "total_energy_kwh" CONF_DEVICE_CONFIG: Final = "device_config" +CONF_CREDENTIALS_HASH: Final = "credentials_hash" +CONF_CONNECTION_TYPE: Final = "connection_type" PLATFORMS: Final = [ Platform.BINARY_SENSOR, diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 9c8aeb99be1..b3092d62904 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -22,6 +22,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.tplink import ( CONF_ALIAS, + CONF_CREDENTIALS_HASH, CONF_DEVICE_CONFIG, CONF_HOST, CONF_MODEL, @@ -53,9 +54,7 @@ MAC_ADDRESS2 = "11:22:33:44:55:66" DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" CREDENTIALS_HASH_LEGACY = "" DEVICE_CONFIG_LEGACY = DeviceConfig(IP_ADDRESS) -DEVICE_CONFIG_DICT_LEGACY = DEVICE_CONFIG_LEGACY.to_dict( - credentials_hash=CREDENTIALS_HASH_LEGACY, exclude_credentials=True -) +DEVICE_CONFIG_DICT_LEGACY = DEVICE_CONFIG_LEGACY.to_dict(exclude_credentials=True) CREDENTIALS = Credentials("foo", "bar") CREDENTIALS_HASH_AUTH = "abcdefghijklmnopqrstuv==" DEVICE_CONFIG_AUTH = DeviceConfig( @@ -74,12 +73,8 @@ DEVICE_CONFIG_AUTH2 = DeviceConfig( ), uses_http=True, ) -DEVICE_CONFIG_DICT_AUTH = DEVICE_CONFIG_AUTH.to_dict( - credentials_hash=CREDENTIALS_HASH_AUTH, exclude_credentials=True -) -DEVICE_CONFIG_DICT_AUTH2 = DEVICE_CONFIG_AUTH2.to_dict( - credentials_hash=CREDENTIALS_HASH_AUTH, exclude_credentials=True -) +DEVICE_CONFIG_DICT_AUTH = DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True) +DEVICE_CONFIG_DICT_AUTH2 = DEVICE_CONFIG_AUTH2.to_dict(exclude_credentials=True) CREATE_ENTRY_DATA_LEGACY = { CONF_HOST: IP_ADDRESS, @@ -92,14 +87,20 @@ CREATE_ENTRY_DATA_AUTH = { CONF_HOST: IP_ADDRESS, CONF_ALIAS: ALIAS, CONF_MODEL: MODEL, + CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AUTH, CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH, } CREATE_ENTRY_DATA_AUTH2 = { CONF_HOST: IP_ADDRESS2, CONF_ALIAS: ALIAS, CONF_MODEL: MODEL, + CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AUTH, CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH2, } +NEW_CONNECTION_TYPE = DeviceConnectionParameters( + DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Aes +) +NEW_CONNECTION_TYPE_DICT = NEW_CONNECTION_TYPE.to_dict() def _load_feature_fixtures(): diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index 7560ff4a72d..e9ae7957520 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -14,8 +14,12 @@ from homeassistant.components.tplink import ( DeviceConfig, KasaException, ) -from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG -from homeassistant.config_entries import ConfigEntryState +from homeassistant.components.tplink.const import ( + CONF_CONNECTION_TYPE, + CONF_CREDENTIALS_HASH, + CONF_DEVICE_CONFIG, +) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( CONF_ALIAS, CONF_DEVICE, @@ -32,6 +36,7 @@ from . import ( CREATE_ENTRY_DATA_AUTH, CREATE_ENTRY_DATA_AUTH2, CREATE_ENTRY_DATA_LEGACY, + CREDENTIALS_HASH_AUTH, DEFAULT_ENTRY_TITLE, DEVICE_CONFIG_DICT_AUTH, DEVICE_CONFIG_DICT_LEGACY, @@ -40,6 +45,7 @@ from . import ( MAC_ADDRESS, MAC_ADDRESS2, MODULE, + NEW_CONNECTION_TYPE_DICT, _mocked_device, _patch_connect, _patch_discovery, @@ -811,6 +817,77 @@ async def test_integration_discovery_with_ip_change( mock_connect["connect"].assert_awaited_once_with(config=config) +async def test_integration_discovery_with_connection_change( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test that config entry is updated with new device config. + + And that connection_hash is removed as it will be invalid. + """ + mock_connect["connect"].side_effect = KasaException() + + mock_config_entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=CREATE_ENTRY_DATA_AUTH, + unique_id=MAC_ADDRESS, + ) + mock_config_entry.add_to_hass(hass) + with patch("homeassistant.components.tplink.Discover.discover", return_value={}): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + assert ( + len( + hass.config_entries.flow.async_progress_by_handler( + DOMAIN, match_context={"source": SOURCE_REAUTH} + ) + ) + == 0 + ) + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AUTH + assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.1" + assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_AUTH + + NEW_DEVICE_CONFIG = { + **DEVICE_CONFIG_DICT_AUTH, + CONF_CONNECTION_TYPE: NEW_CONNECTION_TYPE_DICT, + } + config = DeviceConfig.from_dict(NEW_DEVICE_CONFIG) + # Reset the connect mock so when the config flow reloads the entry it succeeds + mock_connect["connect"].reset_mock(side_effect=True) + bulb = _mocked_device( + device_config=config, + mac=mock_config_entry.unique_id, + ) + mock_connect["connect"].return_value = bulb + + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: "127.0.0.1", + CONF_MAC: MAC_ADDRESS, + CONF_ALIAS: ALIAS, + CONF_DEVICE_CONFIG: NEW_DEVICE_CONFIG, + }, + ) + await hass.async_block_till_done(wait_background_tasks=True) + assert discovery_result["type"] is FlowResultType.ABORT + assert discovery_result["reason"] == "already_configured" + assert mock_config_entry.data[CONF_DEVICE_CONFIG] == NEW_DEVICE_CONFIG + assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" + assert CREDENTIALS_HASH_AUTH not in mock_config_entry.data + + assert mock_config_entry.state is ConfigEntryState.LOADED + + mock_connect["connect"].assert_awaited_once_with(config=config) + + async def test_dhcp_discovery_with_ip_change( hass: HomeAssistant, mock_config_entry: MockConfigEntry, diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 61ec9decc10..bfb7e02b63d 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -7,12 +7,16 @@ from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from freezegun.api import FrozenDateTimeFactory -from kasa import AuthenticationError, Feature, KasaException, Module +from kasa import AuthenticationError, DeviceConfig, Feature, KasaException, Module import pytest from homeassistant import setup from homeassistant.components import tplink -from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG, DOMAIN +from homeassistant.components.tplink.const import ( + CONF_CREDENTIALS_HASH, + CONF_DEVICE_CONFIG, + DOMAIN, +) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( CONF_AUTHENTICATION, @@ -458,7 +462,214 @@ async def test_unlink_devices( expected_identifiers = identifiers[:expected_count] assert device_entries[0].identifiers == set(expected_identifiers) assert entry.version == 1 - assert entry.minor_version == 3 + assert entry.minor_version == 4 msg = f"{expected_message} identifiers for device dummy (hs300): {set(identifiers)}" assert msg in caplog.text + + +async def test_move_credentials_hash( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test credentials hash moved to parent. + + As async_setup_entry will succeed the hash on the parent is updated + from the device. + """ + device_config = { + **DEVICE_CONFIG_AUTH.to_dict( + exclude_credentials=True, credentials_hash="theHash" + ) + } + entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + entry_id="123456", + unique_id=MAC_ADDRESS, + version=1, + minor_version=3, + ) + assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash" + entry.add_to_hass(hass) + + async def _connect(config): + config.credentials_hash = "theNewHash" + return _mocked_device(device_config=config, credentials_hash="theNewHash") + + with ( + patch("homeassistant.components.tplink.Device.connect", new=_connect), + patch("homeassistant.components.tplink.PLATFORMS", []), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.minor_version == 4 + assert entry.state is ConfigEntryState.LOADED + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + assert CONF_CREDENTIALS_HASH in entry.data + # Gets the new hash from the successful connection. + assert entry.data[CONF_CREDENTIALS_HASH] == "theNewHash" + assert "Migration to version 1.4 complete" in caplog.text + + +async def test_move_credentials_hash_auth_error( + hass: HomeAssistant, +) -> None: + """Test credentials hash moved to parent. + + If there is an auth error it should be deleted after migration + in async_setup_entry. + """ + device_config = { + **DEVICE_CONFIG_AUTH.to_dict( + exclude_credentials=True, credentials_hash="theHash" + ) + } + entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + version=1, + minor_version=3, + ) + assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash" + + with ( + patch( + "homeassistant.components.tplink.Device.connect", + side_effect=AuthenticationError, + ), + patch("homeassistant.components.tplink.PLATFORMS", []), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.minor_version == 4 + assert entry.state is ConfigEntryState.SETUP_ERROR + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + # Auth failure deletes the hash + assert CONF_CREDENTIALS_HASH not in entry.data + + +async def test_move_credentials_hash_other_error( + hass: HomeAssistant, +) -> None: + """Test credentials hash moved to parent. + + When there is a KasaException the same hash should still be on the parent + at the end of the test. + """ + device_config = { + **DEVICE_CONFIG_AUTH.to_dict( + exclude_credentials=True, credentials_hash="theHash" + ) + } + entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config} + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + version=1, + minor_version=3, + ) + assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash" + + with ( + patch( + "homeassistant.components.tplink.Device.connect", side_effect=KasaException + ), + patch("homeassistant.components.tplink.PLATFORMS", []), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.minor_version == 4 + assert entry.state is ConfigEntryState.SETUP_RETRY + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + assert CONF_CREDENTIALS_HASH in entry.data + assert entry.data[CONF_CREDENTIALS_HASH] == "theHash" + + +async def test_credentials_hash( + hass: HomeAssistant, +) -> None: + """Test credentials_hash used to call connect.""" + device_config = {**DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True)} + entry_data = { + **CREATE_ENTRY_DATA_AUTH, + CONF_DEVICE_CONFIG: device_config, + CONF_CREDENTIALS_HASH: "theHash", + } + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + ) + + async def _connect(config): + config.credentials_hash = "theHash" + return _mocked_device(device_config=config, credentials_hash="theHash") + + with ( + patch("homeassistant.components.tplink.PLATFORMS", []), + patch("homeassistant.components.tplink.Device.connect", new=_connect), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] + assert CONF_CREDENTIALS_HASH in entry.data + assert entry.data[CONF_DEVICE_CONFIG] == device_config + assert entry.data[CONF_CREDENTIALS_HASH] == "theHash" + + +async def test_credentials_hash_auth_error( + hass: HomeAssistant, +) -> None: + """Test credentials_hash is deleted after an auth failure.""" + device_config = {**DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True)} + entry_data = { + **CREATE_ENTRY_DATA_AUTH, + CONF_DEVICE_CONFIG: device_config, + CONF_CREDENTIALS_HASH: "theHash", + } + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=entry_data, + unique_id=MAC_ADDRESS, + ) + + with ( + patch("homeassistant.components.tplink.PLATFORMS", []), + patch( + "homeassistant.components.tplink.Device.connect", + side_effect=AuthenticationError, + ) as connect_mock, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + expected_config = DeviceConfig.from_dict( + DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True, credentials_hash="theHash") + ) + connect_mock.assert_called_with(config=expected_config) + assert entry.state is ConfigEntryState.SETUP_ERROR + assert CONF_CREDENTIALS_HASH not in entry.data From 18d283bed6ecef32dfe1abe92229c832a59ab048 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 09:26:31 +0200 Subject: [PATCH 0343/2411] Don't allow updating a device to have no connections or identifiers (#120603) * Don't allow updating a device to have no connections or identifiers * Move check to the top of the function --- homeassistant/helpers/device_registry.py | 5 +++++ tests/helpers/test_device_registry.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index cfafa63ec3a..4579739f0e1 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -869,6 +869,11 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): ) add_config_entry = config_entry + if not new_connections and not new_identifiers: + raise HomeAssistantError( + "A device must have at least one of identifiers or connections" + ) + if merge_connections is not UNDEFINED and new_connections is not UNDEFINED: raise HomeAssistantError( "Cannot define both merge_connections and new_connections" diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index fa57cc7557e..3a525f00870 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -3052,3 +3052,22 @@ async def test_primary_config_entry( model="model", ) assert device.primary_config_entry == mock_config_entry_1.entry_id + + +async def test_update_device_no_connections_or_identifiers( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, +) -> None: + """Test updating a device clearing connections and identifiers.""" + mock_config_entry = MockConfigEntry(domain="mqtt", title=None) + mock_config_entry.add_to_hass(hass) + + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + ) + with pytest.raises(HomeAssistantError): + device_registry.async_update_device( + device.id, new_connections=set(), new_identifiers=set() + ) From ef47daad9d398de853989f4abdb317b17e442aca Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 26 Jun 2024 19:14:18 -0400 Subject: [PATCH 0344/2411] Bump anova_wifi to 0.14.0 (#120616) --- homeassistant/components/anova/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/anova/manifest.json b/homeassistant/components/anova/manifest.json index 331a4f61118..d75a791a6f5 100644 --- a/homeassistant/components/anova/manifest.json +++ b/homeassistant/components/anova/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/anova", "iot_class": "cloud_push", "loggers": ["anova_wifi"], - "requirements": ["anova-wifi==0.12.0"] + "requirements": ["anova-wifi==0.14.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 67ad67799e9..a3aa0bbacc3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ androidtvremote2==0.1.1 anel-pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.anova -anova-wifi==0.12.0 +anova-wifi==0.14.0 # homeassistant.components.anthemav anthemav==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 350b59c0eab..9b1d4743c9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -413,7 +413,7 @@ androidtv[async]==0.0.73 androidtvremote2==0.1.1 # homeassistant.components.anova -anova-wifi==0.12.0 +anova-wifi==0.14.0 # homeassistant.components.anthemav anthemav==1.4.1 From 7519603bf5ead3a979cc14ed792c2f309fe79f25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 20:37:28 -0500 Subject: [PATCH 0345/2411] Bump uiprotect to 4.0.0 (#120617) --- 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 8e29f5ffb9f..bdbdacae90e 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==3.7.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==4.0.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index a3aa0bbacc3..44bc9f73b1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2789,7 +2789,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.7.0 +uiprotect==4.0.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b1d4743c9b..45cb1087cb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2169,7 +2169,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==3.7.0 +uiprotect==4.0.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 7256f23376be8e28d4e16e210c7020abb107d619 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 01:50:41 -0500 Subject: [PATCH 0346/2411] Fix performance regression in integration from state_reported (#120621) * Fix performance regression in integration from state_reported Because the callbacks were no longer indexed by entity id, users saw upwards of 1M calls/min https://github.com/home-assistant/core/pull/113869/files#r1655580523 * Update homeassistant/helpers/event.py * coverage --------- Co-authored-by: Paulus Schoutsen --- .../components/integration/sensor.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 4fca92e9b40..8cc5341f081 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -27,8 +27,6 @@ from homeassistant.const import ( CONF_METHOD, CONF_NAME, CONF_UNIQUE_ID, - EVENT_STATE_CHANGED, - EVENT_STATE_REPORTED, STATE_UNAVAILABLE, UnitOfTime, ) @@ -45,7 +43,11 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.event import ( + async_call_later, + async_track_state_change_event, + async_track_state_reported_event, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( @@ -440,21 +442,17 @@ class IntegrationSensor(RestoreSensor): self._derive_and_set_attributes_from_state(state) self.async_on_remove( - self.hass.bus.async_listen( - EVENT_STATE_CHANGED, + async_track_state_change_event( + self.hass, + self._sensor_source_id, handle_state_change, - event_filter=callback( - lambda event_data: event_data["entity_id"] == self._sensor_source_id - ), ) ) self.async_on_remove( - self.hass.bus.async_listen( - EVENT_STATE_REPORTED, + async_track_state_reported_event( + self.hass, + self._sensor_source_id, handle_state_report, - event_filter=callback( - lambda event_data: event_data["entity_id"] == self._sensor_source_id - ), ) ) From 38601d48ffc155fb0d09ec08ad5090bd3d4163e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jun 2024 22:04:27 -0500 Subject: [PATCH 0347/2411] Add async_track_state_reported_event to fix integration performance regression (#120622) split from https://github.com/home-assistant/core/pull/120621 --- homeassistant/helpers/event.py | 37 ++++++++++++++++++++++++++++------ tests/helpers/test_event.py | 32 ++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 4150d871b6b..ebd51948e3b 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any, Concatenate, Generic, TypeVar from homeassistant.const import ( EVENT_CORE_CONFIG_UPDATE, EVENT_STATE_CHANGED, + EVENT_STATE_REPORTED, MATCH_ALL, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, @@ -26,6 +27,7 @@ from homeassistant.core import ( Event, # Explicit reexport of 'EventStateChangedData' for backwards compatibility EventStateChangedData as EventStateChangedData, # noqa: PLC0414 + EventStateReportedData, HassJob, HassJobType, HomeAssistant, @@ -57,6 +59,9 @@ from .typing import TemplateVarsType _TRACK_STATE_CHANGE_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = HassKey( "track_state_change_data" ) +_TRACK_STATE_REPORTED_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey( + "track_state_reported_data" +) _TRACK_STATE_ADDED_DOMAIN_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = ( HassKey("track_state_added_domain_data") ) @@ -324,8 +329,8 @@ def async_track_state_change_event( @callback def _async_dispatch_entity_id_event( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]], - event: Event[EventStateChangedData], + callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], + event: Event[_TypedDictT], ) -> None: """Dispatch to listeners.""" if not (callbacks_list := callbacks.get(event.data["entity_id"])): @@ -342,10 +347,10 @@ def _async_dispatch_entity_id_event( @callback -def _async_state_change_filter( +def _async_state_filter( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]], - event_data: EventStateChangedData, + callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], + event_data: _TypedDictT, ) -> bool: """Filter state changes by entity_id.""" return event_data["entity_id"] in callbacks @@ -355,7 +360,7 @@ _KEYED_TRACK_STATE_CHANGE = _KeyedEventTracker( key=_TRACK_STATE_CHANGE_DATA, event_type=EVENT_STATE_CHANGED, dispatcher_callable=_async_dispatch_entity_id_event, - filter_callable=_async_state_change_filter, + filter_callable=_async_state_filter, ) @@ -372,6 +377,26 @@ def _async_track_state_change_event( ) +_KEYED_TRACK_STATE_REPORTED = _KeyedEventTracker( + key=_TRACK_STATE_REPORTED_DATA, + event_type=EVENT_STATE_REPORTED, + dispatcher_callable=_async_dispatch_entity_id_event, + filter_callable=_async_state_filter, +) + + +def async_track_state_reported_event( + hass: HomeAssistant, + entity_ids: str | Iterable[str], + action: Callable[[Event[EventStateReportedData]], Any], + job_type: HassJobType | None = None, +) -> CALLBACK_TYPE: + """Track EVENT_STATE_REPORTED by entity_id without lowercasing.""" + return _async_track_event( + _KEYED_TRACK_STATE_REPORTED, hass, entity_ids, action, job_type + ) + + @callback def _remove_empty_listener() -> None: """Remove a listener that does nothing.""" diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index edce36218e8..4f983120e36 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -15,7 +15,13 @@ import pytest from homeassistant.const import MATCH_ALL import homeassistant.core as ha -from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + EventStateReportedData, + HomeAssistant, + callback, +) from homeassistant.exceptions import TemplateError from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED @@ -34,6 +40,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_state_change_filtered, async_track_state_removed_domain, + async_track_state_reported_event, async_track_sunrise, async_track_sunset, async_track_template, @@ -4907,3 +4914,26 @@ async def test_track_point_in_time_repr( assert "Exception in callback _TrackPointUTCTime" in caplog.text assert "._raise_exception" in caplog.text await hass.async_block_till_done(wait_background_tasks=True) + + +async def test_async_track_state_reported_event(hass: HomeAssistant) -> None: + """Test async_track_state_reported_event.""" + tracker_called: list[ha.State] = [] + + @ha.callback + def single_run_callback(event: Event[EventStateReportedData]) -> None: + new_state = event.data["new_state"] + tracker_called.append(new_state) + + unsub = async_track_state_reported_event( + hass, ["light.bowl", "light.top"], single_run_callback + ) + hass.states.async_set("light.bowl", "on") + hass.states.async_set("light.top", "on") + await hass.async_block_till_done() + assert len(tracker_called) == 0 + hass.states.async_set("light.bowl", "on") + hass.states.async_set("light.top", "on") + await hass.async_block_till_done() + assert len(tracker_called) == 2 + unsub() From 1933454b76249bf6b9ba971e7acc0244f18b5a69 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 13:45:15 +0200 Subject: [PATCH 0348/2411] Rename async_track_state_reported_event to async_track_state_report_event (#120637) * Rename async_track_state_reported_event to async_track_state_report_event * Update tests --- homeassistant/components/integration/sensor.py | 4 ++-- homeassistant/helpers/event.py | 12 ++++++------ tests/helpers/test_event.py | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 8cc5341f081..a053e5cea5c 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -46,7 +46,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_call_later, async_track_state_change_event, - async_track_state_reported_event, + async_track_state_report_event, ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -449,7 +449,7 @@ class IntegrationSensor(RestoreSensor): ) ) self.async_on_remove( - async_track_state_reported_event( + async_track_state_report_event( self.hass, self._sensor_source_id, handle_state_report, diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index ebd51948e3b..51c1a7ba30f 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -59,8 +59,8 @@ from .typing import TemplateVarsType _TRACK_STATE_CHANGE_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = HassKey( "track_state_change_data" ) -_TRACK_STATE_REPORTED_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey( - "track_state_reported_data" +_TRACK_STATE_REPORT_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey( + "track_state_report_data" ) _TRACK_STATE_ADDED_DOMAIN_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = ( HassKey("track_state_added_domain_data") @@ -377,15 +377,15 @@ def _async_track_state_change_event( ) -_KEYED_TRACK_STATE_REPORTED = _KeyedEventTracker( - key=_TRACK_STATE_REPORTED_DATA, +_KEYED_TRACK_STATE_REPORT = _KeyedEventTracker( + key=_TRACK_STATE_REPORT_DATA, event_type=EVENT_STATE_REPORTED, dispatcher_callable=_async_dispatch_entity_id_event, filter_callable=_async_state_filter, ) -def async_track_state_reported_event( +def async_track_state_report_event( hass: HomeAssistant, entity_ids: str | Iterable[str], action: Callable[[Event[EventStateReportedData]], Any], @@ -393,7 +393,7 @@ def async_track_state_reported_event( ) -> CALLBACK_TYPE: """Track EVENT_STATE_REPORTED by entity_id without lowercasing.""" return _async_track_event( - _KEYED_TRACK_STATE_REPORTED, hass, entity_ids, action, job_type + _KEYED_TRACK_STATE_REPORT, hass, entity_ids, action, job_type ) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 4f983120e36..4bb4c1a1967 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -40,7 +40,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_state_change_filtered, async_track_state_removed_domain, - async_track_state_reported_event, + async_track_state_report_event, async_track_sunrise, async_track_sunset, async_track_template, @@ -4916,8 +4916,8 @@ async def test_track_point_in_time_repr( await hass.async_block_till_done(wait_background_tasks=True) -async def test_async_track_state_reported_event(hass: HomeAssistant) -> None: - """Test async_track_state_reported_event.""" +async def test_async_track_state_report_event(hass: HomeAssistant) -> None: + """Test async_track_state_report_event.""" tracker_called: list[ha.State] = [] @ha.callback @@ -4925,7 +4925,7 @@ async def test_async_track_state_reported_event(hass: HomeAssistant) -> None: new_state = event.data["new_state"] tracker_called.append(new_state) - unsub = async_track_state_reported_event( + unsub = async_track_state_report_event( hass, ["light.bowl", "light.top"], single_run_callback ) hass.states.async_set("light.bowl", "on") From 89ac3ce832981fac544befde1fea1a6f3347e0c0 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:21:41 +0200 Subject: [PATCH 0349/2411] Fix the version that raises the issue (#120638) --- homeassistant/components/lamarzocco/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 9c66fdd1b60..dfcaa54047d 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - entry.runtime_data = coordinator gateway_version = coordinator.device.firmware[FirmwareType.GATEWAY].current_version - if version.parse(gateway_version) < version.parse("v3.5-rc5"): + if version.parse(gateway_version) < version.parse("v3.4-rc5"): # incompatible gateway firmware, create an issue ir.async_create_issue( hass, From b290e9535055398e4457aae802da5bd4afc073c2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 13:08:19 +0200 Subject: [PATCH 0350/2411] Improve typing of state event helpers (#120639) --- homeassistant/core.py | 15 +++++++++------ homeassistant/helpers/event.py | 10 ++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 2b43b2d40ff..71ee5f4bd1d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -158,26 +158,29 @@ class ConfigSource(enum.StrEnum): YAML = "yaml" -class EventStateChangedData(TypedDict): +class EventStateEventData(TypedDict): + """Base class for EVENT_STATE_CHANGED and EVENT_STATE_CHANGED data.""" + + entity_id: str + new_state: State | None + + +class EventStateChangedData(EventStateEventData): """EVENT_STATE_CHANGED data. A state changed event is fired when on state write when the state is changed. """ - entity_id: str old_state: State | None - new_state: State | None -class EventStateReportedData(TypedDict): +class EventStateReportedData(EventStateEventData): """EVENT_STATE_REPORTED data. A state reported event is fired when on state write when the state is unchanged. """ - entity_id: str old_last_reported: datetime.datetime - new_state: State | None # SOURCE_* are deprecated as of Home Assistant 2022.2, use ConfigSource instead diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 51c1a7ba30f..0c77809079e 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -27,6 +27,7 @@ from homeassistant.core import ( Event, # Explicit reexport of 'EventStateChangedData' for backwards compatibility EventStateChangedData as EventStateChangedData, # noqa: PLC0414 + EventStateEventData, EventStateReportedData, HassJob, HassJobType, @@ -89,6 +90,7 @@ RANDOM_MICROSECOND_MIN = 50000 RANDOM_MICROSECOND_MAX = 500000 _TypedDictT = TypeVar("_TypedDictT", bound=Mapping[str, Any]) +_StateEventDataT = TypeVar("_StateEventDataT", bound=EventStateEventData) @dataclass(slots=True, frozen=True) @@ -329,8 +331,8 @@ def async_track_state_change_event( @callback def _async_dispatch_entity_id_event( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], - event: Event[_TypedDictT], + callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]], + event: Event[_StateEventDataT], ) -> None: """Dispatch to listeners.""" if not (callbacks_list := callbacks.get(event.data["entity_id"])): @@ -349,8 +351,8 @@ def _async_dispatch_entity_id_event( @callback def _async_state_filter( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], - event_data: _TypedDictT, + callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]], + event_data: _StateEventDataT, ) -> bool: """Filter state changes by entity_id.""" return event_data["entity_id"] in callbacks From 4836d6620b833186d16e120d0825ef1c4b029193 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 10:43:28 +0200 Subject: [PATCH 0351/2411] Add snapshots to tasmota sensor test (#120647) --- .../tasmota/snapshots/test_sensor.ambr | 1526 +++++++++++++++++ tests/components/tasmota/test_sensor.py | 218 +-- 2 files changed, 1533 insertions(+), 211 deletions(-) create mode 100644 tests/components/tasmota/snapshots/test_sensor.ambr diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..744554c7246 --- /dev/null +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -0,0 +1,1526 @@ +# serializer version: 1 +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DHT11 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHT11 Temperature', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_DHT11_Temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DHT11 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config0-entity_ids0-messages0].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DHT11 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_dht11_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.0', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Speed Act', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'TX23 Speed Act', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_TX23_Speed_Act', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Dir Card', + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'TX23 Dir Card', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_TX23_Dir_Card', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Speed Act', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.3', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Dir Card', + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'WSW', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Speed Act', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_speed_act', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config1-entity_ids1-messages1].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota TX23 Dir Card', + }), + 'context': , + 'entity_id': 'sensor.tasmota_tx23_dir_card', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ESE', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'ENERGY TotalTariff 0', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'ENERGY TotalTariff 1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DS18B20 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DS18B20 Temperature', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_DS18B20_Temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota DS18B20 Id', + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'DS18B20 Id', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_DS18B20_Id', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DS18B20 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.3', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota DS18B20 Id', + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '01191ED79190', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota DS18B20 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota DS18B20 Id', + }), + 'context': , + 'entity_id': 'sensor.tasmota_ds18b20_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'meep', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config4-entity_ids4-messages4].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total 0', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_0', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total 1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config5-entity_ids5-messages5].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total Phase1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_Phase1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY Total Phase2', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_Phase2', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config6-entity_ids6-messages6].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY Total Phase2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_total_phase2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG Temperature1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Temperature1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.0', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Tasmota ANALOG Illuminance3', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG Temperature2', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Temperature2', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Tasmota ANALOG Illuminance3', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG Illuminance3', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Illuminance3', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Tasmota ANALOG Illuminance3', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_illuminance3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config7-entity_ids7-messages7].9 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tasmota ANALOG Temperature1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_temperature1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Energy', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Energy', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '230', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].12 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].13 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1150', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].14 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '230', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].15 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Power', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Power', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Voltage', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].7 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_analog_ctenergy1_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ANALOG CTEnergy1 Current', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Current', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config8-entity_ids8-messages8].9 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Tasmota ANALOG CTEnergy1 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_analog_ctenergy1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2300', + }) +# --- diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 2de80de4319..c01485d12a7 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -13,9 +13,9 @@ from hatasmota.utils import ( get_topic_tele_will, ) import pytest +from syrupy import SnapshotAssertion from homeassistant import config_entries -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant @@ -175,7 +175,7 @@ TEMPERATURE_SENSOR_CONFIG = { @pytest.mark.parametrize( - ("sensor_config", "entity_ids", "messages", "states"), + ("sensor_config", "entity_ids", "messages"), [ ( DEFAULT_SENSOR_CONFIG, @@ -184,20 +184,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"DHT11":{"Temperature":20.5}}', '{"StatusSNS":{"DHT11":{"Temperature":20.0}}}', ), - ( - { - "sensor.tasmota_dht11_temperature": { - "state": "20.5", - "attributes": { - "device_class": "temperature", - "unit_of_measurement": "°C", - }, - }, - }, - { - "sensor.tasmota_dht11_temperature": {"state": "20.0"}, - }, - ), ), ( DICT_SENSOR_CONFIG_1, @@ -206,22 +192,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"TX23":{"Speed":{"Act":"12.3"},"Dir": {"Card": "WSW"}}}', '{"StatusSNS":{"TX23":{"Speed":{"Act":"23.4"},"Dir": {"Card": "ESE"}}}}', ), - ( - { - "sensor.tasmota_tx23_speed_act": { - "state": "12.3", - "attributes": { - "device_class": None, - "unit_of_measurement": "km/h", - }, - }, - "sensor.tasmota_tx23_dir_card": {"state": "WSW"}, - }, - { - "sensor.tasmota_tx23_speed_act": {"state": "23.4"}, - "sensor.tasmota_tx23_dir_card": {"state": "ESE"}, - }, - ), ), ( LIST_SENSOR_CONFIG, @@ -233,22 +203,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"TotalTariff":[1.2,3.4]}}', '{"StatusSNS":{"ENERGY":{"TotalTariff":[5.6,7.8]}}}', ), - ( - { - "sensor.tasmota_energy_totaltariff_0": { - "state": "1.2", - "attributes": { - "device_class": None, - "unit_of_measurement": None, - }, - }, - "sensor.tasmota_energy_totaltariff_1": {"state": "3.4"}, - }, - { - "sensor.tasmota_energy_totaltariff_0": {"state": "5.6"}, - "sensor.tasmota_energy_totaltariff_1": {"state": "7.8"}, - }, - ), ), ( TEMPERATURE_SENSOR_CONFIG, @@ -257,22 +211,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"DS18B20":{"Id": "01191ED79190","Temperature": 12.3}}', '{"StatusSNS":{"DS18B20":{"Id": "meep","Temperature": 23.4}}}', ), - ( - { - "sensor.tasmota_ds18b20_temperature": { - "state": "12.3", - "attributes": { - "device_class": "temperature", - "unit_of_measurement": "°C", - }, - }, - "sensor.tasmota_ds18b20_id": {"state": "01191ED79190"}, - }, - { - "sensor.tasmota_ds18b20_temperature": {"state": "23.4"}, - "sensor.tasmota_ds18b20_id": {"state": "meep"}, - }, - ), ), # Test simple Total sensor ( @@ -282,21 +220,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"Total":1.2,"TotalStartTime":"2018-11-23T15:33:47"}}', '{"StatusSNS":{"ENERGY":{"Total":5.6,"TotalStartTime":"2018-11-23T16:33:47"}}}', ), - ( - { - "sensor.tasmota_energy_total": { - "state": "1.2", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - }, - { - "sensor.tasmota_energy_total": {"state": "5.6"}, - }, - ), ), # Test list Total sensors ( @@ -306,30 +229,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"Total":[1.2, 3.4],"TotalStartTime":"2018-11-23T15:33:47"}}', '{"StatusSNS":{"ENERGY":{"Total":[5.6, 7.8],"TotalStartTime":"2018-11-23T16:33:47"}}}', ), - ( - { - "sensor.tasmota_energy_total_0": { - "state": "1.2", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - "sensor.tasmota_energy_total_1": { - "state": "3.4", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - }, - { - "sensor.tasmota_energy_total_0": {"state": "5.6"}, - "sensor.tasmota_energy_total_1": {"state": "7.8"}, - }, - ), ), # Test dict Total sensors ( @@ -342,30 +241,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"ENERGY":{"Total":{"Phase1":1.2, "Phase2":3.4},"TotalStartTime":"2018-11-23T15:33:47"}}', '{"StatusSNS":{"ENERGY":{"Total":{"Phase1":5.6, "Phase2":7.8},"TotalStartTime":"2018-11-23T15:33:47"}}}', ), - ( - { - "sensor.tasmota_energy_total_phase1": { - "state": "1.2", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - "sensor.tasmota_energy_total_phase2": { - "state": "3.4", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - }, - { - "sensor.tasmota_energy_total_phase1": {"state": "5.6"}, - "sensor.tasmota_energy_total_phase2": {"state": "7.8"}, - }, - ), ), ( NUMBERED_SENSOR_CONFIG, @@ -384,39 +259,6 @@ TEMPERATURE_SENSOR_CONFIG = { '"Illuminance3":1.2}}}' ), ), - ( - { - "sensor.tasmota_analog_temperature1": { - "state": "1.2", - "attributes": { - "device_class": "temperature", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "°C", - }, - }, - "sensor.tasmota_analog_temperature2": { - "state": "3.4", - "attributes": { - "device_class": "temperature", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "°C", - }, - }, - "sensor.tasmota_analog_illuminance3": { - "state": "5.6", - "attributes": { - "device_class": "illuminance", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "lx", - }, - }, - }, - { - "sensor.tasmota_analog_temperature1": {"state": "7.8"}, - "sensor.tasmota_analog_temperature2": {"state": "9.0"}, - "sensor.tasmota_analog_illuminance3": {"state": "1.2"}, - }, - ), ), ( NUMBERED_SENSOR_CONFIG_2, @@ -436,48 +278,6 @@ TEMPERATURE_SENSOR_CONFIG = { '{"Energy":1.0,"Power":1150,"Voltage":230,"Current":5}}}}' ), ), - ( - { - "sensor.tasmota_analog_ctenergy1_energy": { - "state": "0.5", - "attributes": { - "device_class": "energy", - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - "unit_of_measurement": "kWh", - }, - }, - "sensor.tasmota_analog_ctenergy1_power": { - "state": "2300", - "attributes": { - "device_class": "power", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "W", - }, - }, - "sensor.tasmota_analog_ctenergy1_voltage": { - "state": "230", - "attributes": { - "device_class": "voltage", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "V", - }, - }, - "sensor.tasmota_analog_ctenergy1_current": { - "state": "10", - "attributes": { - "device_class": "current", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - "unit_of_measurement": "A", - }, - }, - }, - { - "sensor.tasmota_analog_ctenergy1_energy": {"state": "1.0"}, - "sensor.tasmota_analog_ctenergy1_power": {"state": "1150"}, - "sensor.tasmota_analog_ctenergy1_voltage": {"state": "230"}, - "sensor.tasmota_analog_ctenergy1_current": {"state": "5"}, - }, - ), ), ], ) @@ -485,11 +285,11 @@ async def test_controlling_state_via_mqtt( hass: HomeAssistant, entity_registry: er.EntityRegistry, mqtt_mock: MqttMockHAClient, + snapshot: SnapshotAssertion, setup_tasmota, sensor_config, entity_ids, messages, - states, ) -> None: """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -513,11 +313,13 @@ async def test_controlling_state_via_mqtt( state = hass.states.get(entity_id) assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert state == snapshot entry = entity_registry.async_get(entity_id) assert entry.disabled is False assert entry.disabled_by is None assert entry.entity_category is None + assert entry == snapshot async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() @@ -530,19 +332,13 @@ async def test_controlling_state_via_mqtt( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", messages[0]) for entity_id in entity_ids: state = hass.states.get(entity_id) - expected_state = states[0][entity_id] - assert state.state == expected_state["state"] - for attribute, expected in expected_state.get("attributes", {}).items(): - assert state.attributes.get(attribute) == expected + assert state == snapshot # Test polled state update async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/STATUS10", messages[1]) for entity_id in entity_ids: state = hass.states.get(entity_id) - expected_state = states[1][entity_id] - assert state.state == expected_state["state"] - for attribute, expected in expected_state.get("attributes", {}).items(): - assert state.attributes.get(attribute) == expected + assert state == snapshot @pytest.mark.parametrize( From 3022d3bfa04400577fa39de44f38d1864f707ec5 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:34:01 +0200 Subject: [PATCH 0352/2411] Move Auto On/off switches to Config EntityCategory (#120648) --- homeassistant/components/lamarzocco/switch.py | 2 ++ tests/components/lamarzocco/snapshots/test_switch.ambr | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lamarzocco/switch.py b/homeassistant/components/lamarzocco/switch.py index e21cd2f3d94..c57e0662ab2 100644 --- a/homeassistant/components/lamarzocco/switch.py +++ b/homeassistant/components/lamarzocco/switch.py @@ -9,6 +9,7 @@ from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -105,6 +106,7 @@ class LaMarzoccoAutoOnOffSwitchEntity(LaMarzoccoBaseEntity, SwitchEntity): super().__init__(coordinator, f"auto_on_off_{identifier}") self._identifier = identifier self._attr_translation_placeholders = {"id": identifier} + self.entity_category = EntityCategory.CONFIG async def _async_enable(self, state: bool) -> None: """Enable or disable the auto on/off schedule.""" diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 0f462955a33..edda4ffee3b 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -10,7 +10,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'switch', - 'entity_category': None, + 'entity_category': , 'entity_id': 'switch.gs01234_auto_on_off_os2oswx', 'has_entity_name': True, 'hidden_by': None, @@ -43,7 +43,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'switch', - 'entity_category': None, + 'entity_category': , 'entity_id': 'switch.gs01234_auto_on_off_axfz5bj', 'has_entity_name': True, 'hidden_by': None, From 68495977643ebf39ff891c8cc727fd18f6c4eb36 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 12:55:49 +0200 Subject: [PATCH 0353/2411] Bump hatasmota to 0.9.1 (#120649) --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 4 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../tasmota/snapshots/test_sensor.ambr | 34 +++++++++++++++---- 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 2ce81772774..69233de07d8 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.8.0"] + "requirements": ["HATasmota==0.9.1"] } diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 546e3eb4539..a7fb415f037 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -190,6 +190,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.ENERGY, STATE_CLASS: SensorStateClass.TOTAL, }, + hc.SENSOR_TOTAL_TARIFF: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, hc.SENSOR_VOLTAGE: { diff --git a/requirements_all.txt b/requirements_all.txt index 44bc9f73b1d..06e5b6ef223 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.8.0 +HATasmota==0.9.1 # homeassistant.components.mastodon Mastodon.py==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45cb1087cb4..58691727bec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.8.0 +HATasmota==0.9.1 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index 744554c7246..c5d70487749 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -232,7 +232,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_0', @@ -247,7 +250,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -264,7 +269,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'ENERGY TotalTariff 0', 'platform': 'tasmota', @@ -272,13 +277,16 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_0', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].2 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_1', @@ -293,7 +301,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -310,7 +320,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'ENERGY TotalTariff 1', 'platform': 'tasmota', @@ -318,13 +328,16 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_1', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].4 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_0', @@ -337,7 +350,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_1', @@ -350,7 +366,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_0', @@ -363,7 +382,10 @@ # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.tasmota_energy_totaltariff_1', From 0e1dc9878f042f9f6ad79377f11e6ba530b49bc8 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 27 Jun 2024 21:17:15 +1000 Subject: [PATCH 0354/2411] Fix values at startup for Tessie (#120652) --- homeassistant/components/tessie/entity.py | 1 + .../tessie/snapshots/test_lock.ambr | 48 ------------------- .../tessie/snapshots/test_sensor.ambr | 20 ++++---- 3 files changed, 11 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index 93b9f10ae67..d2a59f205fc 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -132,6 +132,7 @@ class TessieEnergyEntity(TessieBaseEntity): self._attr_device_info = data.device super().__init__(coordinator, key) + self._async_update_attrs() class TessieWallConnectorEntity(TessieBaseEntity): diff --git a/tests/components/tessie/snapshots/test_lock.ambr b/tests/components/tessie/snapshots/test_lock.ambr index 1eff418b202..cea2bebbddb 100644 --- a/tests/components/tessie/snapshots/test_lock.ambr +++ b/tests/components/tessie/snapshots/test_lock.ambr @@ -93,51 +93,3 @@ 'state': 'locked', }) # --- -# name: test_locks[lock.test_speed_limit-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'lock', - 'entity_category': None, - 'entity_id': 'lock.test_speed_limit', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Speed limit', - 'platform': 'tessie', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'vehicle_state_speed_limit_mode_active', - 'unique_id': 'VINVINVIN-vehicle_state_speed_limit_mode_active', - 'unit_of_measurement': None, - }) -# --- -# name: test_locks[lock.test_speed_limit-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'code_format': '^\\d\\d\\d\\d$', - 'friendly_name': 'Test Speed limit', - 'supported_features': , - }), - 'context': , - 'entity_id': 'lock.test_speed_limit', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unlocked', - }) -# --- diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index ba7b4eae0a5..afe229feba0 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -53,7 +53,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '5.06', }) # --- # name: test_sensors[sensor.energy_site_energy_left-entry] @@ -110,7 +110,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '38.8964736842105', }) # --- # name: test_sensors[sensor.energy_site_generator_power-entry] @@ -167,7 +167,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.0', }) # --- # name: test_sensors[sensor.energy_site_grid_power-entry] @@ -224,7 +224,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.0', }) # --- # name: test_sensors[sensor.energy_site_grid_services_power-entry] @@ -281,7 +281,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.0', }) # --- # name: test_sensors[sensor.energy_site_load_power-entry] @@ -338,7 +338,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '6.245', }) # --- # name: test_sensors[sensor.energy_site_percentage_charged-entry] @@ -392,7 +392,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '95.5053740373966', }) # --- # name: test_sensors[sensor.energy_site_solar_power-entry] @@ -449,7 +449,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '1.185', }) # --- # name: test_sensors[sensor.energy_site_total_pack_energy-entry] @@ -506,7 +506,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '40.727', }) # --- # name: test_sensors[sensor.energy_site_vpp_backup_reserve-entry] @@ -554,7 +554,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_battery_level-entry] From a8d6866f9f69de331d0eeb4f325ec140b282b218 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 12:29:32 +0200 Subject: [PATCH 0355/2411] Disable polling for Knocki (#120656) --- homeassistant/components/knocki/event.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/knocki/event.py b/homeassistant/components/knocki/event.py index adaf344e468..74dc5a0f64c 100644 --- a/homeassistant/components/knocki/event.py +++ b/homeassistant/components/knocki/event.py @@ -48,6 +48,7 @@ class KnockiTrigger(EventEntity): _attr_event_types = [EVENT_TRIGGERED] _attr_has_entity_name = True + _attr_should_poll = False _attr_translation_key = "knocki" def __init__(self, trigger: Trigger, client: KnockiClient) -> None: From 03d198dd645487338e632b2630e4b14d945803bd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 19:41:21 +0200 Subject: [PATCH 0356/2411] Fix unknown attribute in MPD (#120657) --- homeassistant/components/mpd/media_player.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index eb34fb6289f..3538b1c7973 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -421,11 +421,6 @@ class MpdDevice(MediaPlayerEntity): """Name of the current input source.""" return self._current_playlist - @property - def source_list(self): - """Return the list of available input sources.""" - return self._playlists - async def async_select_source(self, source: str) -> None: """Choose a different available playlist and play it.""" await self.async_play_media(MediaType.PLAYLIST, source) From be086c581c43ebb43dff97b03780f1c3d3d697c6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 12:47:58 +0200 Subject: [PATCH 0357/2411] Fix Airgradient ABC days name (#120659) --- .../components/airgradient/select.py | 1 + .../components/airgradient/strings.json | 3 +- .../airgradient/snapshots/test_select.ambr | 28 +++++++++++-------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index a64ce596806..532f7167dff 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -88,6 +88,7 @@ LEARNING_TIME_OFFSET_OPTIONS = [ ] ABC_DAYS = [ + "1", "8", "30", "90", diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 1dd5fc61a16..12049e7b720 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -91,8 +91,9 @@ } }, "co2_automatic_baseline_calibration": { - "name": "CO2 automatic baseline calibration", + "name": "CO2 automatic baseline duration", "state": { + "1": "1 day", "8": "8 days", "30": "30 days", "90": "90 days", diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index ece563b40c6..b8fca4a110b 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -1,11 +1,12 @@ # serializer version: 1 -# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_calibration-entry] +# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ + '1', '8', '30', '90', @@ -19,7 +20,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -31,7 +32,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'CO2 automatic baseline calibration', + 'original_name': 'CO2 automatic baseline duration', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -40,11 +41,12 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_calibration-state] +# name: test_all_entities[indoor][select.airgradient_co2_automatic_baseline_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient CO2 automatic baseline calibration', + 'friendly_name': 'Airgradient CO2 automatic baseline duration', 'options': list([ + '1', '8', '30', '90', @@ -53,7 +55,7 @@ ]), }), 'context': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'last_changed': , 'last_reported': , 'last_updated': , @@ -404,13 +406,14 @@ 'state': '12', }) # --- -# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_calibration-entry] +# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ + '1', '8', '30', '90', @@ -424,7 +427,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -436,7 +439,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'CO2 automatic baseline calibration', + 'original_name': 'CO2 automatic baseline duration', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, @@ -445,11 +448,12 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_calibration-state] +# name: test_all_entities[outdoor][select.airgradient_co2_automatic_baseline_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient CO2 automatic baseline calibration', + 'friendly_name': 'Airgradient CO2 automatic baseline duration', 'options': list([ + '1', '8', '30', '90', @@ -458,7 +462,7 @@ ]), }), 'context': , - 'entity_id': 'select.airgradient_co2_automatic_baseline_calibration', + 'entity_id': 'select.airgradient_co2_automatic_baseline_duration', 'last_changed': , 'last_reported': , 'last_updated': , From f9ca85735d089bbbe61abb293e23f43282f73d69 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 27 Jun 2024 23:08:40 +1200 Subject: [PATCH 0358/2411] [esphome] Add more tests to bring integration to 100% coverage (#120661) --- tests/components/esphome/conftest.py | 147 +++++++++++++++++- tests/components/esphome/test_manager.py | 108 ++++++++++++- .../esphome/test_voice_assistant.py | 14 +- 3 files changed, 258 insertions(+), 11 deletions(-) diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index f55ab9cbe4a..ac1558b8aa0 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from asyncio import Event -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Coroutine from pathlib import Path from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -19,6 +19,8 @@ from aioesphomeapi import ( HomeassistantServiceCall, ReconnectLogic, UserService, + VoiceAssistantAudioSettings, + VoiceAssistantEventType, VoiceAssistantFeature, ) import pytest @@ -32,6 +34,11 @@ from homeassistant.components.esphome.const import ( DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, DOMAIN, ) +from homeassistant.components.esphome.entry_data import RuntimeEntryData +from homeassistant.components.esphome.voice_assistant import ( + VoiceAssistantAPIPipeline, + VoiceAssistantUDPPipeline, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -40,6 +47,8 @@ from . import DASHBOARD_HOST, DASHBOARD_PORT, DASHBOARD_SLUG from tests.common import MockConfigEntry +_ONE_SECOND = 16000 * 2 # 16Khz 16-bit + @pytest.fixture(autouse=True) def mock_bluetooth(enable_bluetooth: None) -> None: @@ -196,6 +205,20 @@ class MockESPHomeDevice: self.home_assistant_state_subscription_callback: Callable[ [str, str | None], None ] + self.voice_assistant_handle_start_callback: Callable[ + [str, int, VoiceAssistantAudioSettings, str | None], + Coroutine[Any, Any, int | None], + ] + self.voice_assistant_handle_stop_callback: Callable[ + [], Coroutine[Any, Any, None] + ] + self.voice_assistant_handle_audio_callback: ( + Callable[ + [bytes], + Coroutine[Any, Any, None], + ] + | None + ) self.device_info = device_info def set_state_callback(self, state_callback: Callable[[EntityState], None]) -> None: @@ -255,6 +278,47 @@ class MockESPHomeDevice: """Mock a state subscription.""" self.home_assistant_state_subscription_callback(entity_id, attribute) + def set_subscribe_voice_assistant_callbacks( + self, + handle_start: Callable[ + [str, int, VoiceAssistantAudioSettings, str | None], + Coroutine[Any, Any, int | None], + ], + handle_stop: Callable[[], Coroutine[Any, Any, None]], + handle_audio: ( + Callable[ + [bytes], + Coroutine[Any, Any, None], + ] + | None + ) = None, + ) -> None: + """Set the voice assistant subscription callbacks.""" + self.voice_assistant_handle_start_callback = handle_start + self.voice_assistant_handle_stop_callback = handle_stop + self.voice_assistant_handle_audio_callback = handle_audio + + async def mock_voice_assistant_handle_start( + self, + conversation_id: str, + flags: int, + settings: VoiceAssistantAudioSettings, + wake_word_phrase: str | None, + ) -> int | None: + """Mock voice assistant handle start.""" + return await self.voice_assistant_handle_start_callback( + conversation_id, flags, settings, wake_word_phrase + ) + + async def mock_voice_assistant_handle_stop(self) -> None: + """Mock voice assistant handle stop.""" + await self.voice_assistant_handle_stop_callback() + + async def mock_voice_assistant_handle_audio(self, audio: bytes) -> None: + """Mock voice assistant handle audio.""" + assert self.voice_assistant_handle_audio_callback is not None + await self.voice_assistant_handle_audio_callback(audio) + async def _mock_generic_device_entry( hass: HomeAssistant, @@ -318,8 +382,33 @@ async def _mock_generic_device_entry( """Subscribe to home assistant states.""" mock_device.set_home_assistant_state_subscription_callback(on_state_sub) + def _subscribe_voice_assistant( + *, + handle_start: Callable[ + [str, int, VoiceAssistantAudioSettings, str | None], + Coroutine[Any, Any, int | None], + ], + handle_stop: Callable[[], Coroutine[Any, Any, None]], + handle_audio: ( + Callable[ + [bytes], + Coroutine[Any, Any, None], + ] + | None + ) = None, + ) -> Callable[[], None]: + """Subscribe to voice assistant.""" + mock_device.set_subscribe_voice_assistant_callbacks( + handle_start, handle_stop, handle_audio + ) + + def unsub(): + pass + + return unsub + mock_client.device_info = AsyncMock(return_value=mock_device.device_info) - mock_client.subscribe_voice_assistant = Mock() + mock_client.subscribe_voice_assistant = _subscribe_voice_assistant mock_client.list_entities_services = AsyncMock( return_value=mock_list_entities_services ) @@ -524,3 +613,57 @@ async def mock_esphome_device( ) return _mock_device + + +@pytest.fixture +def mock_voice_assistant_api_pipeline() -> VoiceAssistantAPIPipeline: + """Return the API Pipeline factory.""" + mock_pipeline = Mock(spec=VoiceAssistantAPIPipeline) + + def mock_constructor( + hass: HomeAssistant, + entry_data: RuntimeEntryData, + handle_event: Callable[[VoiceAssistantEventType, dict[str, str] | None], None], + handle_finished: Callable[[], None], + api_client: APIClient, + ): + """Fake the constructor.""" + mock_pipeline.hass = hass + mock_pipeline.entry_data = entry_data + mock_pipeline.handle_event = handle_event + mock_pipeline.handle_finished = handle_finished + mock_pipeline.api_client = api_client + return mock_pipeline + + mock_pipeline.side_effect = mock_constructor + with patch( + "homeassistant.components.esphome.voice_assistant.VoiceAssistantAPIPipeline", + new=mock_pipeline, + ): + yield mock_pipeline + + +@pytest.fixture +def mock_voice_assistant_udp_pipeline() -> VoiceAssistantUDPPipeline: + """Return the API Pipeline factory.""" + mock_pipeline = Mock(spec=VoiceAssistantUDPPipeline) + + def mock_constructor( + hass: HomeAssistant, + entry_data: RuntimeEntryData, + handle_event: Callable[[VoiceAssistantEventType, dict[str, str] | None], None], + handle_finished: Callable[[], None], + ): + """Fake the constructor.""" + mock_pipeline.hass = hass + mock_pipeline.entry_data = entry_data + mock_pipeline.handle_event = handle_event + mock_pipeline.handle_finished = handle_finished + return mock_pipeline + + mock_pipeline.side_effect = mock_constructor + with patch( + "homeassistant.components.esphome.voice_assistant.VoiceAssistantUDPPipeline", + new=mock_pipeline, + ): + yield mock_pipeline diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 92c21842e78..01f267581f4 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -2,7 +2,7 @@ import asyncio from collections.abc import Awaitable, Callable -from unittest.mock import AsyncMock, call +from unittest.mock import AsyncMock, call, patch from aioesphomeapi import ( APIClient, @@ -17,6 +17,7 @@ from aioesphomeapi import ( UserService, UserServiceArg, UserServiceArgType, + VoiceAssistantFeature, ) import pytest @@ -28,6 +29,10 @@ from homeassistant.components.esphome.const import ( DOMAIN, STABLE_BLE_VERSION_STR, ) +from homeassistant.components.esphome.voice_assistant import ( + VoiceAssistantAPIPipeline, + VoiceAssistantUDPPipeline, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -39,7 +44,7 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.setup import async_setup_component -from .conftest import MockESPHomeDevice +from .conftest import _ONE_SECOND, MockESPHomeDevice from tests.common import MockConfigEntry, async_capture_events, async_mock_service @@ -1181,3 +1186,102 @@ async def test_entry_missing_unique_id( await mock_esphome_device(mock_client=mock_client, mock_storage=True) await hass.async_block_till_done() assert entry.unique_id == "11:22:33:44:55:aa" + + +async def test_manager_voice_assistant_handlers_api( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + caplog: pytest.LogCaptureFixture, + mock_voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, +) -> None: + """Test the handlers are correctly executed in manager.py.""" + + device: MockESPHomeDevice = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={ + "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT + | VoiceAssistantFeature.API_AUDIO + }, + ) + + await hass.async_block_till_done() + + with ( + patch( + "homeassistant.components.esphome.manager.VoiceAssistantAPIPipeline", + new=mock_voice_assistant_api_pipeline, + ), + ): + port: int | None = await device.mock_voice_assistant_handle_start( + "", 0, None, None + ) + + assert port == 0 + + port: int | None = await device.mock_voice_assistant_handle_start( + "", 0, None, None + ) + + assert "Voice assistant UDP server was not stopped" in caplog.text + + await device.mock_voice_assistant_handle_audio(bytes(_ONE_SECOND)) + + mock_voice_assistant_api_pipeline.receive_audio_bytes.assert_called_with( + bytes(_ONE_SECOND) + ) + + mock_voice_assistant_api_pipeline.receive_audio_bytes.reset_mock() + + await device.mock_voice_assistant_handle_stop() + mock_voice_assistant_api_pipeline.handle_finished() + + await device.mock_voice_assistant_handle_audio(bytes(_ONE_SECOND)) + + mock_voice_assistant_api_pipeline.receive_audio_bytes.assert_not_called() + + +async def test_manager_voice_assistant_handlers_udp( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + mock_voice_assistant_udp_pipeline: VoiceAssistantUDPPipeline, +) -> None: + """Test the handlers are correctly executed in manager.py.""" + + device: MockESPHomeDevice = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={ + "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT + }, + ) + + await hass.async_block_till_done() + + with ( + patch( + "homeassistant.components.esphome.manager.VoiceAssistantUDPPipeline", + new=mock_voice_assistant_udp_pipeline, + ), + ): + await device.mock_voice_assistant_handle_start("", 0, None, None) + + mock_voice_assistant_udp_pipeline.run_pipeline.assert_called() + + await device.mock_voice_assistant_handle_stop() + mock_voice_assistant_udp_pipeline.handle_finished() + + mock_voice_assistant_udp_pipeline.stop.assert_called() + mock_voice_assistant_udp_pipeline.close.assert_called() diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index c347c3dc7d3..eafc0243dc6 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -37,15 +37,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import intent as intent_helper import homeassistant.helpers.device_registry as dr -from .conftest import MockESPHomeDevice +from .conftest import _ONE_SECOND, MockESPHomeDevice _TEST_INPUT_TEXT = "This is an input test" _TEST_OUTPUT_TEXT = "This is an output test" _TEST_OUTPUT_URL = "output.mp3" _TEST_MEDIA_ID = "12345" -_ONE_SECOND = 16000 * 2 # 16Khz 16-bit - @pytest.fixture def voice_assistant_udp_pipeline( @@ -813,6 +811,7 @@ async def test_wake_word_abort_exception( async def test_timer_events( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, mock_client: APIClient, mock_esphome_device: Callable[ [APIClient, list[EntityInfo], list[UserService], list[EntityState]], @@ -831,8 +830,8 @@ async def test_timer_events( | VoiceAssistantFeature.TIMERS }, ) - dev_reg = dr.async_get(hass) - dev = dev_reg.async_get_device( + await hass.async_block_till_done() + dev = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mock_device.entry.unique_id)} ) @@ -886,6 +885,7 @@ async def test_timer_events( async def test_unknown_timer_event( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, mock_client: APIClient, mock_esphome_device: Callable[ [APIClient, list[EntityInfo], list[UserService], list[EntityState]], @@ -904,8 +904,8 @@ async def test_unknown_timer_event( | VoiceAssistantFeature.TIMERS }, ) - dev_reg = dr.async_get(hass) - dev = dev_reg.async_get_device( + await hass.async_block_till_done() + dev = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mock_device.entry.unique_id)} ) From f6aa25c717125e2af4b5f6c7d295b43261049f58 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 15:00:14 +0200 Subject: [PATCH 0359/2411] Fix docstring for EventStateEventData (#120662) --- homeassistant/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 71ee5f4bd1d..c4392f62c52 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -159,7 +159,7 @@ class ConfigSource(enum.StrEnum): class EventStateEventData(TypedDict): - """Base class for EVENT_STATE_CHANGED and EVENT_STATE_CHANGED data.""" + """Base class for EVENT_STATE_CHANGED and EVENT_STATE_REPORTED data.""" entity_id: str new_state: State | None From 94f8f8281f66f150c894af80044115074fac6a44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 12:58:42 -0500 Subject: [PATCH 0360/2411] Bump uiprotect to 4.2.0 (#120669) --- 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 bdbdacae90e..6f61bb97480 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==4.0.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==4.2.0", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 06e5b6ef223..a4f34b61e3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2789,7 +2789,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==4.0.0 +uiprotect==4.2.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 58691727bec..8c9cb2c8a23 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2169,7 +2169,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==4.0.0 +uiprotect==4.2.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From f9c5661c669f357eee0073ac155f8054cc8578bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 15:10:48 -0500 Subject: [PATCH 0361/2411] Bump unifi-discovery to 1.2.0 (#120684) --- 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 6f61bb97480..06716e5342a 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==4.2.0", "unifi-discovery==1.1.8"], + "requirements": ["uiprotect==4.2.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index a4f34b61e3f..397a967788a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2795,7 +2795,7 @@ uiprotect==4.2.0 ultraheat-api==0.5.7 # homeassistant.components.unifiprotect -unifi-discovery==1.1.8 +unifi-discovery==1.2.0 # homeassistant.components.unifi_direct unifi_ap==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c9cb2c8a23..87ec9e11e0f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2175,7 +2175,7 @@ uiprotect==4.2.0 ultraheat-api==0.5.7 # homeassistant.components.unifiprotect -unifi-discovery==1.1.8 +unifi-discovery==1.2.0 # homeassistant.components.zha universal-silabs-flasher==0.0.20 From 07dd832c58d86efa90398b1e17f9f249763131b9 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 27 Jun 2024 13:08:18 -0400 Subject: [PATCH 0362/2411] Bump Environment Canada to 0.7.0 (#120686) --- .../components/environment_canada/manifest.json | 2 +- homeassistant/components/environment_canada/sensor.py | 9 --------- homeassistant/components/environment_canada/strings.json | 3 --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 3 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index a0bdd5d4919..69a6cd7c69b 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.6.3"] + "requirements": ["env-canada==0.7.0"] } diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 8a734f74dd6..1a5d096203d 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -19,7 +19,6 @@ from homeassistant.const import ( PERCENTAGE, UV_INDEX, UnitOfLength, - UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, UnitOfTemperature, @@ -114,14 +113,6 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: data.conditions.get("pop", {}).get("value"), ), - ECSensorEntityDescription( - key="precip_yesterday", - translation_key="precip_yesterday", - device_class=SensorDeviceClass.PRECIPITATION, - native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda data: data.conditions.get("precip_yesterday", {}).get("value"), - ), ECSensorEntityDescription( key="pressure", translation_key="pressure", diff --git a/homeassistant/components/environment_canada/strings.json b/homeassistant/components/environment_canada/strings.json index fc03550b64e..28ca55c6195 100644 --- a/homeassistant/components/environment_canada/strings.json +++ b/homeassistant/components/environment_canada/strings.json @@ -52,9 +52,6 @@ "pop": { "name": "Chance of precipitation" }, - "precip_yesterday": { - "name": "Precipitation yesterday" - }, "pressure": { "name": "Barometric pressure" }, diff --git a/requirements_all.txt b/requirements_all.txt index 397a967788a..2b99124d5b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -816,7 +816,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.6.3 +env-canada==0.7.0 # homeassistant.components.season ephem==4.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 87ec9e11e0f..1a230af870f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ energyzero==2.1.0 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.6.3 +env-canada==0.7.0 # homeassistant.components.season ephem==4.1.5 From 09dbd8e7eb02bf8f4c56202a2c5a9ba05b64b0a2 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:47:25 -0400 Subject: [PATCH 0363/2411] Use more observations in NWS (#120687) Use more observations --- homeassistant/components/nws/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index ba3a22e5818..381537775da 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -78,8 +78,8 @@ HOURLY = "hourly" OBSERVATION_VALID_TIME = timedelta(minutes=60) FORECAST_VALID_TIME = timedelta(minutes=45) -# A lot of stations update once hourly plus some wiggle room -UPDATE_TIME_PERIOD = timedelta(minutes=70) +# Ask for observations for last four hours +UPDATE_TIME_PERIOD = timedelta(minutes=240) DEBOUNCE_TIME = 10 * 60 # in seconds DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) From b9c9921847c6c27e618845c814c18215dfab6186 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jun 2024 12:20:37 -0500 Subject: [PATCH 0364/2411] Add newer models to unifi integrations discovery (#120688) --- homeassistant/components/unifi/manifest.json | 4 ++++ homeassistant/components/unifiprotect/manifest.json | 4 ++++ homeassistant/generated/ssdp.py | 8 ++++++++ 3 files changed, 16 insertions(+) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index f4bfaec2d42..aa9b553cb67 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -21,6 +21,10 @@ { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE" + }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max" } ] } diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 06716e5342a..6691d738cd0 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -53,6 +53,10 @@ { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE" + }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max" } ] } diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 8e7319917f0..9ed65bab868 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -297,6 +297,10 @@ SSDP = { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE", }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max", + }, ], "unifiprotect": [ { @@ -311,6 +315,10 @@ SSDP = { "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE", }, + { + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro Max", + }, ], "upnp": [ { From e756328d523042910a6a147655f267c44ae71620 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 27 Jun 2024 13:41:36 -0400 Subject: [PATCH 0365/2411] Bump upb-lib to 0.5.7 (#120689) --- homeassistant/components/upb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/upb/manifest.json b/homeassistant/components/upb/manifest.json index a5e32dd298e..b208edbc0e5 100644 --- a/homeassistant/components/upb/manifest.json +++ b/homeassistant/components/upb/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/upb", "iot_class": "local_push", "loggers": ["upb_lib"], - "requirements": ["upb-lib==0.5.6"] + "requirements": ["upb-lib==0.5.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b99124d5b3..9cbfe64afd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2807,7 +2807,7 @@ unifiled==0.11 universal-silabs-flasher==0.0.20 # homeassistant.components.upb -upb-lib==0.5.6 +upb-lib==0.5.7 # homeassistant.components.upcloud upcloud-api==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1a230af870f..01b356747b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2181,7 +2181,7 @@ unifi-discovery==1.2.0 universal-silabs-flasher==0.0.20 # homeassistant.components.upb -upb-lib==0.5.6 +upb-lib==0.5.7 # homeassistant.components.upcloud upcloud-api==2.5.1 From 476b9909ac879cf60b6d2d9de877fa8d5099cab6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 27 Jun 2024 22:06:30 +0200 Subject: [PATCH 0366/2411] Update frontend to 20240627.0 (#120693) --- 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 89c8fbe30ca..cd46b358335 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240626.2"] + "requirements": ["home-assistant-frontend==20240627.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 174de784eba..91db2564fa6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240626.2 +home-assistant-frontend==20240627.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9cbfe64afd0..0086de879db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.2 +home-assistant-frontend==20240627.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 01b356747b2..0862fc33ea4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240626.2 +home-assistant-frontend==20240627.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From f3ab3bd5cbe60d3fce5c8e46d40cb69b58ceef0c Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 27 Jun 2024 21:29:17 +0200 Subject: [PATCH 0367/2411] Bump aioautomower to 2024.6.3 (#120697) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 5ca1b500340..7883b057a3f 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.6.1"] + "requirements": ["aioautomower==2024.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0086de879db..a8d7a62d848 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -204,7 +204,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.1 +aioautomower==2024.6.3 # homeassistant.components.azure_devops aioazuredevops==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0862fc33ea4..802231b63d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -183,7 +183,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.1 +aioautomower==2024.6.3 # homeassistant.components.azure_devops aioazuredevops==2.1.1 From 411633d3b3786a9b210c9c1bf925c09e31b5a6b8 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 27 Jun 2024 16:10:11 -0400 Subject: [PATCH 0368/2411] Bump Environment Canada to 0.7.1 (#120699) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 69a6cd7c69b..c77d35b1769 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.7.0"] + "requirements": ["env-canada==0.7.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a8d7a62d848..657e961d803 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -816,7 +816,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.7.0 +env-canada==0.7.1 # homeassistant.components.season ephem==4.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 802231b63d1..3eb005b1dac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ energyzero==2.1.0 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.7.0 +env-canada==0.7.1 # homeassistant.components.season ephem==4.1.5 From 0b8dd738f1a07074029e04845a8e731f1724fbfa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jun 2024 22:09:33 +0200 Subject: [PATCH 0369/2411] Bump ttls to 1.8.3 (#120700) --- homeassistant/components/twinkly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json index 6ec89261b3d..a84eebf0f28 100644 --- a/homeassistant/components/twinkly/manifest.json +++ b/homeassistant/components/twinkly/manifest.json @@ -14,5 +14,5 @@ "documentation": "https://www.home-assistant.io/integrations/twinkly", "iot_class": "local_polling", "loggers": ["ttls"], - "requirements": ["ttls==1.5.1"] + "requirements": ["ttls==1.8.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 657e961d803..98b5548cd38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2768,7 +2768,7 @@ tplink-omada-client==1.3.12 transmission-rpc==7.0.3 # homeassistant.components.twinkly -ttls==1.5.1 +ttls==1.8.3 # homeassistant.components.thethingsnetwork ttn_client==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3eb005b1dac..2857aee8c9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2148,7 +2148,7 @@ tplink-omada-client==1.3.12 transmission-rpc==7.0.3 # homeassistant.components.twinkly -ttls==1.5.1 +ttls==1.8.3 # homeassistant.components.thethingsnetwork ttn_client==1.0.0 From 23056f839b1dba2f2469176a5896a99d46179322 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:54:34 +0100 Subject: [PATCH 0370/2411] Update tplink unlink identifiers to deal with ids from other domains (#120596) --- homeassistant/components/tplink/__init__.py | 98 +++++++++++++-------- tests/components/tplink/__init__.py | 1 + tests/components/tplink/test_init.py | 82 ++++++++++++----- 3 files changed, 123 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 6d300f68aa0..83cfc733716 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from datetime import timedelta import logging from typing import Any @@ -282,6 +283,28 @@ def mac_alias(mac: str) -> str: return mac.replace(":", "")[-4:].upper() +def _mac_connection_or_none(device: dr.DeviceEntry) -> str | None: + return next( + ( + conn + for type_, conn in device.connections + if type_ == dr.CONNECTION_NETWORK_MAC + ), + None, + ) + + +def _device_id_is_mac_or_none(mac: str, device_ids: Iterable[str]) -> str | None: + # Previously only iot devices had child devices and iot devices use + # the upper and lcase MAC addresses as device_id so match on case + # insensitive mac address as the parent device. + upper_mac = mac.upper() + return next( + (device_id for device_id in device_ids if device_id.upper() == upper_mac), + None, + ) + + async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old entry.""" version = config_entry.version @@ -298,49 +321,48 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # always be linked into one device. dev_reg = dr.async_get(hass) for device in dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id): - new_identifiers: set[tuple[str, str]] | None = None - if len(device.identifiers) > 1 and ( - mac := next( - iter( - [ - conn[1] - for conn in device.connections - if conn[0] == dr.CONNECTION_NETWORK_MAC - ] - ), - None, + original_identifiers = device.identifiers + # Get only the tplink identifier, could be tapo or other integrations. + tplink_identifiers = [ + ident[1] for ident in original_identifiers if ident[0] == DOMAIN + ] + # Nothing to fix if there's only one identifier. mac connection + # should never be none but if it is there's no problem. + if len(tplink_identifiers) <= 1 or not ( + mac := _mac_connection_or_none(device) + ): + continue + if not ( + tplink_parent_device_id := _device_id_is_mac_or_none( + mac, tplink_identifiers ) ): - for identifier in device.identifiers: - # Previously only iot devices that use the MAC address as - # device_id had child devices so check for mac as the - # parent device. - if identifier[0] == DOMAIN and identifier[1].upper() == mac.upper(): - new_identifiers = {identifier} - break - if new_identifiers: - dev_reg.async_update_device( - device.id, new_identifiers=new_identifiers - ) - _LOGGER.debug( - "Replaced identifiers for device %s (%s): %s with: %s", - device.name, - device.model, - device.identifiers, - new_identifiers, - ) - else: - # No match on mac so raise an error. - _LOGGER.error( - "Unable to replace identifiers for device %s (%s): %s", - device.name, - device.model, - device.identifiers, - ) + # No match on mac so raise an error. + _LOGGER.error( + "Unable to replace identifiers for device %s (%s): %s", + device.name, + device.model, + device.identifiers, + ) + continue + # Retain any identifiers for other domains + new_identifiers = { + ident for ident in device.identifiers if ident[0] != DOMAIN + } + new_identifiers.add((DOMAIN, tplink_parent_device_id)) + dev_reg.async_update_device(device.id, new_identifiers=new_identifiers) + _LOGGER.debug( + "Replaced identifiers for device %s (%s): %s with: %s", + device.name, + device.model, + original_identifiers, + new_identifiers, + ) minor_version = 3 hass.config_entries.async_update_entry(config_entry, minor_version=3) - _LOGGER.debug("Migration to version %s.%s successful", version, minor_version) + + _LOGGER.debug("Migration to version %s.%s complete", version, minor_version) if version == 1 and minor_version == 3: # credentials_hash stored in the device_config should be moved to data. diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index b3092d62904..d12858017cc 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -49,6 +49,7 @@ ALIAS = "My Bulb" MODEL = "HS100" MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" DEVICE_ID = "123456789ABCDEFGH" +DEVICE_ID_MAC = "AA:BB:CC:DD:EE:FF" DHCP_FORMATTED_MAC_ADDRESS = MAC_ADDRESS.replace(":", "") MAC_ADDRESS2 = "11:22:33:44:55:66" DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index bfb7e02b63d..c5c5e2ce6db 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -36,6 +36,8 @@ from . import ( CREATE_ENTRY_DATA_AUTH, CREATE_ENTRY_DATA_LEGACY, DEVICE_CONFIG_AUTH, + DEVICE_ID, + DEVICE_ID_MAC, IP_ADDRESS, MAC_ADDRESS, _mocked_device, @@ -404,19 +406,48 @@ async def test_feature_no_category( @pytest.mark.parametrize( - ("identifier_base", "expected_message", "expected_count"), + ("device_id", "id_count", "domains", "expected_message"), [ - pytest.param("C0:06:C3:42:54:2B", "Replaced", 1, id="success"), - pytest.param("123456789", "Unable to replace", 3, id="failure"), + pytest.param(DEVICE_ID_MAC, 1, [DOMAIN], None, id="mac-id-no-children"), + pytest.param(DEVICE_ID_MAC, 3, [DOMAIN], "Replaced", id="mac-id-children"), + pytest.param( + DEVICE_ID_MAC, + 1, + [DOMAIN, "other"], + None, + id="mac-id-no-children-other-domain", + ), + pytest.param( + DEVICE_ID_MAC, + 3, + [DOMAIN, "other"], + "Replaced", + id="mac-id-children-other-domain", + ), + pytest.param(DEVICE_ID, 1, [DOMAIN], None, id="not-mac-id-no-children"), + pytest.param( + DEVICE_ID, 3, [DOMAIN], "Unable to replace", id="not-mac-children" + ), + pytest.param( + DEVICE_ID, 1, [DOMAIN, "other"], None, id="not-mac-no-children-other-domain" + ), + pytest.param( + DEVICE_ID, + 3, + [DOMAIN, "other"], + "Unable to replace", + id="not-mac-children-other-domain", + ), ], ) async def test_unlink_devices( hass: HomeAssistant, device_registry: dr.DeviceRegistry, caplog: pytest.LogCaptureFixture, - identifier_base, + device_id, + id_count, + domains, expected_message, - expected_count, ) -> None: """Test for unlinking child device ids.""" entry = MockConfigEntry( @@ -429,43 +460,54 @@ async def test_unlink_devices( ) entry.add_to_hass(hass) - # Setup initial device registry, with linkages - mac = "C0:06:C3:42:54:2B" - identifiers = [ - (DOMAIN, identifier_base), - (DOMAIN, f"{identifier_base}_0001"), - (DOMAIN, f"{identifier_base}_0002"), + # Generate list of test identifiers + test_identifiers = [ + (domain, f"{device_id}{"" if i == 0 else f"_000{i}"}") + for i in range(id_count) + for domain in domains ] + update_msg_fragment = "identifiers for device dummy (hs300):" + update_msg = f"{expected_message} {update_msg_fragment}" if expected_message else "" + + # Expected identifiers should include all other domains or all the newer non-mac device ids + # or just the parent mac device id + expected_identifiers = [ + (domain, device_id) + for domain, device_id in test_identifiers + if domain != DOMAIN + or device_id.startswith(DEVICE_ID) + or device_id == DEVICE_ID_MAC + ] + device_registry.async_get_or_create( config_entry_id="123456", connections={ - (dr.CONNECTION_NETWORK_MAC, mac.lower()), + (dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS), }, - identifiers=set(identifiers), + identifiers=set(test_identifiers), model="hs300", name="dummy", ) device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) assert device_entries[0].connections == { - (dr.CONNECTION_NETWORK_MAC, mac.lower()), + (dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS), } - assert device_entries[0].identifiers == set(identifiers) + assert device_entries[0].identifiers == set(test_identifiers) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) - assert device_entries[0].connections == {(dr.CONNECTION_NETWORK_MAC, mac.lower())} - # If expected count is 1 will be the first identifier only - expected_identifiers = identifiers[:expected_count] + assert device_entries[0].connections == {(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)} + assert device_entries[0].identifiers == set(expected_identifiers) assert entry.version == 1 assert entry.minor_version == 4 - msg = f"{expected_message} identifiers for device dummy (hs300): {set(identifiers)}" - assert msg in caplog.text + assert update_msg in caplog.text + assert "Migration to version 1.3 complete" in caplog.text async def test_move_credentials_hash( From 9b5d0f72dcdee18e07746eb45995fe159006e1b1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Jun 2024 22:20:25 +0200 Subject: [PATCH 0371/2411] Bump version to 2024.7.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8291fb93fd7..33a86f57a5e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 709022534b1..e4ccd9898e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b2" +version = "2024.7.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f3761a8e534ee0c779db47e58a169d17aad77e26 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 22:56:22 +0200 Subject: [PATCH 0372/2411] Bump hatasmota to 0.9.2 (#120670) --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 78 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../tasmota/snapshots/test_sensor.ambr | 232 +++++++++++++++--- tests/components/tasmota/test_sensor.py | 6 +- 6 files changed, 247 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 69233de07d8..783483c6ffd 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.9.1"] + "requirements": ["HATasmota==0.9.2"] } diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index a7fb415f037..631e5f2efb0 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -53,26 +53,10 @@ ICON = "icon" # A Tasmota sensor type may be mapped to either a device class or an icon, # both can only be set if the default device class icon is not appropriate SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { - hc.SENSOR_ACTIVE_ENERGYEXPORT: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_ACTIVE_ENERGYIMPORT: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_ACTIVE_POWERUSAGE: { - DEVICE_CLASS: SensorDeviceClass.POWER, - STATE_CLASS: SensorStateClass.MEASUREMENT, - }, hc.SENSOR_AMBIENT: { DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_APPARENT_POWERUSAGE: { - DEVICE_CLASS: SensorDeviceClass.APPARENT_POWER, - STATE_CLASS: SensorStateClass.MEASUREMENT, - }, hc.SENSOR_BATTERY: { DEVICE_CLASS: SensorDeviceClass.BATTERY, STATE_CLASS: SensorStateClass.MEASUREMENT, @@ -92,7 +76,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.CURRENT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_CURRENTNEUTRAL: { + hc.SENSOR_CURRENT_NEUTRAL: { DEVICE_CLASS: SensorDeviceClass.CURRENT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -110,6 +94,34 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.ENERGY, STATE_CLASS: SensorStateClass.TOTAL, }, + hc.SENSOR_ENERGY_EXPORT_ACTIVE: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_EXPORT_REACTIVE: {STATE_CLASS: SensorStateClass.TOTAL}, + hc.SENSOR_ENERGY_EXPORT_TARIFF: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_ACTIVE: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_REACTIVE: {STATE_CLASS: SensorStateClass.TOTAL}, + hc.SENSOR_ENERGY_IMPORT_TODAY: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + hc.SENSOR_ENERGY_IMPORT_TOTAL: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_TOTAL_TARIFF: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, + hc.SENSOR_ENERGY_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_FREQUENCY: { DEVICE_CLASS: SensorDeviceClass.FREQUENCY, STATE_CLASS: SensorStateClass.MEASUREMENT, @@ -122,6 +134,14 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, + hc.SENSOR_POWER_ACTIVE: { + DEVICE_CLASS: SensorDeviceClass.POWER, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_POWER_APPARENT: { + DEVICE_CLASS: SensorDeviceClass.APPARENT_POWER, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"}, hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"}, hc.SENSOR_MOISTURE: {DEVICE_CLASS: SensorDeviceClass.MOISTURE}, @@ -144,11 +164,11 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.PM25, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_POWERFACTOR: { + hc.SENSOR_POWER_FACTOR: { DEVICE_CLASS: SensorDeviceClass.POWER_FACTOR, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_POWERUSAGE: { + hc.SENSOR_POWER: { DEVICE_CLASS: SensorDeviceClass.POWER, STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -156,14 +176,12 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.PRESSURE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_PRESSUREATSEALEVEL: { + hc.SENSOR_PRESSURE_AT_SEA_LEVEL: { DEVICE_CLASS: SensorDeviceClass.PRESSURE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"}, - hc.SENSOR_REACTIVE_ENERGYEXPORT: {STATE_CLASS: SensorStateClass.TOTAL}, - hc.SENSOR_REACTIVE_ENERGYIMPORT: {STATE_CLASS: SensorStateClass.TOTAL}, - hc.SENSOR_REACTIVE_POWERUSAGE: { + hc.SENSOR_POWER_REACTIVE: { DEVICE_CLASS: SensorDeviceClass.REACTIVE_POWER, STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -182,19 +200,6 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_TODAY: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - hc.SENSOR_TOTAL: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_TOTAL_TARIFF: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, hc.SENSOR_VOLTAGE: { DEVICE_CLASS: SensorDeviceClass.VOLTAGE, @@ -204,7 +209,6 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.WEIGHT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, } SENSOR_UNIT_MAP = { diff --git a/requirements_all.txt b/requirements_all.txt index 98b5548cd38..d2e51a365e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.9.1 +HATasmota==0.9.2 # homeassistant.components.mastodon Mastodon.py==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2857aee8c9b..21e6eedebc7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.9.1 +HATasmota==0.9.2 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index c5d70487749..829df2b881f 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -280,6 +280,102 @@ 'unit_of_measurement': , }) # --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].12 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].13 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].14 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].15 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].2 StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -332,6 +428,108 @@ }) # --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY ExportTariff 0', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_ExportTariff_0', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY ExportTariff 1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_ExportTariff_1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].8 StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -347,7 +545,7 @@ 'state': '1.2', }) # --- -# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].9 StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -363,38 +561,6 @@ 'state': '3.4', }) # --- -# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Tasmota ENERGY TotalTariff 0', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.tasmota_energy_totaltariff_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '5.6', - }) -# --- -# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Tasmota ENERGY TotalTariff 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.tasmota_energy_totaltariff_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '7.8', - }) -# --- # name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index c01485d12a7..e06d55f17a7 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -198,10 +198,12 @@ TEMPERATURE_SENSOR_CONFIG = { [ "sensor.tasmota_energy_totaltariff_0", "sensor.tasmota_energy_totaltariff_1", + "sensor.tasmota_energy_exporttariff_0", + "sensor.tasmota_energy_exporttariff_1", ], ( - '{"ENERGY":{"TotalTariff":[1.2,3.4]}}', - '{"StatusSNS":{"ENERGY":{"TotalTariff":[5.6,7.8]}}}', + '{"ENERGY":{"ExportTariff":[5.6,7.8],"TotalTariff":[1.2,3.4]}}', + '{"StatusSNS":{"ENERGY":{"ExportTariff":[1.2,3.4],"TotalTariff":[5.6,7.8]}}}', ), ), ( From e764afecac101f24154f77394c81607647d45779 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 27 Jun 2024 23:12:20 +0200 Subject: [PATCH 0373/2411] Bump `nextdns` to version 3.1.0 (#120703) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/nextdns/__init__.py | 6 +++--- homeassistant/components/nextdns/config_flow.py | 11 +++++------ homeassistant/components/nextdns/coordinator.py | 12 ++++++++---- homeassistant/components/nextdns/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nextdns/test_config_flow.py | 2 ++ tests/components/nextdns/test_init.py | 9 +++++++-- tests/components/nextdns/test_switch.py | 13 +++++++++++-- 9 files changed, 39 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index f11611007c2..4256126b3c7 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -18,6 +18,7 @@ from nextdns import ( NextDns, Settings, ) +from tenacity import RetryError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform @@ -84,9 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: NextDnsConfigEntry) -> b websession = async_get_clientsession(hass) try: - async with asyncio.timeout(10): - nextdns = await NextDns.create(websession, api_key) - except (ApiError, ClientConnectorError, TimeoutError) as err: + nextdns = await NextDns.create(websession, api_key) + except (ApiError, ClientConnectorError, RetryError, TimeoutError) as err: raise ConfigEntryNotReady from err tasks = [] diff --git a/homeassistant/components/nextdns/config_flow.py b/homeassistant/components/nextdns/config_flow.py index 4955bbb4cad..bd79112b1f9 100644 --- a/homeassistant/components/nextdns/config_flow.py +++ b/homeassistant/components/nextdns/config_flow.py @@ -2,11 +2,11 @@ from __future__ import annotations -import asyncio from typing import Any from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError, InvalidApiKeyError, NextDns +from tenacity import RetryError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -37,13 +37,12 @@ class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN): if user_input is not None: self.api_key = user_input[CONF_API_KEY] try: - async with asyncio.timeout(10): - self.nextdns = await NextDns.create( - websession, user_input[CONF_API_KEY] - ) + self.nextdns = await NextDns.create( + websession, user_input[CONF_API_KEY] + ) except InvalidApiKeyError: errors["base"] = "invalid_api_key" - except (ApiError, ClientConnectorError, TimeoutError): + except (ApiError, ClientConnectorError, RetryError, TimeoutError): errors["base"] = "cannot_connect" except Exception: # noqa: BLE001 errors["base"] = "unknown" diff --git a/homeassistant/components/nextdns/coordinator.py b/homeassistant/components/nextdns/coordinator.py index cad1aeac070..5210807bd3c 100644 --- a/homeassistant/components/nextdns/coordinator.py +++ b/homeassistant/components/nextdns/coordinator.py @@ -1,6 +1,5 @@ """NextDns coordinator.""" -import asyncio from datetime import timedelta import logging from typing import TypeVar @@ -19,6 +18,7 @@ from nextdns import ( Settings, ) from nextdns.model import NextDnsData +from tenacity import RetryError from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo @@ -58,9 +58,13 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]): async def _async_update_data(self) -> CoordinatorDataT: """Update data via internal method.""" try: - async with asyncio.timeout(10): - return await self._async_update_data_internal() - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + return await self._async_update_data_internal() + except ( + ApiError, + ClientConnectorError, + InvalidApiKeyError, + RetryError, + ) as err: raise UpdateFailed(err) from err async def _async_update_data_internal(self) -> CoordinatorDataT: diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index 1e7145ef6d1..b65706ef1ce 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["nextdns"], "quality_scale": "platinum", - "requirements": ["nextdns==3.0.0"] + "requirements": ["nextdns==3.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index d2e51a365e7..c5a167d2b13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1404,7 +1404,7 @@ nextcloudmonitor==1.5.0 nextcord==2.6.0 # homeassistant.components.nextdns -nextdns==3.0.0 +nextdns==3.1.0 # homeassistant.components.nibe_heatpump nibe==2.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21e6eedebc7..7a849b83c86 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1143,7 +1143,7 @@ nextcloudmonitor==1.5.0 nextcord==2.6.0 # homeassistant.components.nextdns -nextdns==3.0.0 +nextdns==3.1.0 # homeassistant.components.nibe_heatpump nibe==2.8.0 diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py index 9247288eebf..7571eef347e 100644 --- a/tests/components/nextdns/test_config_flow.py +++ b/tests/components/nextdns/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from nextdns import ApiError, InvalidApiKeyError import pytest +from tenacity import RetryError from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.config_entries import SOURCE_USER @@ -57,6 +58,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: [ (ApiError("API Error"), "cannot_connect"), (InvalidApiKeyError, "invalid_api_key"), + (RetryError("Retry Error"), "cannot_connect"), (TimeoutError, "cannot_connect"), (ValueError, "unknown"), ], diff --git a/tests/components/nextdns/test_init.py b/tests/components/nextdns/test_init.py index f7b85bb8a54..61a487d917c 100644 --- a/tests/components/nextdns/test_init.py +++ b/tests/components/nextdns/test_init.py @@ -3,6 +3,8 @@ from unittest.mock import patch from nextdns import ApiError +import pytest +from tenacity import RetryError from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -24,7 +26,10 @@ async def test_async_setup_entry(hass: HomeAssistant) -> None: assert state.state == "20.0" -async def test_config_not_ready(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "exc", [ApiError("API Error"), RetryError("Retry Error"), TimeoutError] +) +async def test_config_not_ready(hass: HomeAssistant, exc: Exception) -> None: """Test for setup failure if the connection to the service fails.""" entry = MockConfigEntry( domain=DOMAIN, @@ -35,7 +40,7 @@ async def test_config_not_ready(hass: HomeAssistant) -> None: with patch( "homeassistant.components.nextdns.NextDns.get_profiles", - side_effect=ApiError("API Error"), + side_effect=exc, ): 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 index 059585e9ffe..6e344e34336 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -8,6 +8,7 @@ from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError import pytest from syrupy import SnapshotAssertion +from tenacity import RetryError from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -94,7 +95,15 @@ async def test_switch_off(hass: HomeAssistant) -> None: mock_switch_on.assert_called_once() -async def test_availability(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "exc", + [ + ApiError("API Error"), + RetryError("Retry Error"), + TimeoutError, + ], +) +async def test_availability(hass: HomeAssistant, exc: Exception) -> None: """Ensure that we mark the entities unavailable correctly when service causes an error.""" await init_integration(hass) @@ -106,7 +115,7 @@ async def test_availability(hass: HomeAssistant) -> None: future = utcnow() + timedelta(minutes=10) with patch( "homeassistant.components.nextdns.NextDns.get_settings", - side_effect=ApiError("API Error"), + side_effect=exc, ): async_fire_time_changed(hass, future) await hass.async_block_till_done(wait_background_tasks=True) From 4e34d02d2dd6a8cc892fb0a9cddc6c5e1965e7c4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 08:36:10 +0200 Subject: [PATCH 0374/2411] Bump apsystems-ez1 to 1.3.3 (#120702) --- homeassistant/components/apsystems/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apsystems/manifest.json b/homeassistant/components/apsystems/manifest.json index 8e0ac00796d..cba3e59dba0 100644 --- a/homeassistant/components/apsystems/manifest.json +++ b/homeassistant/components/apsystems/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/apsystems", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["apsystems-ez1==1.3.1"] + "requirements": ["apsystems-ez1==1.3.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index c5a167d2b13..f7c8f979327 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ apprise==1.8.0 aprslib==0.7.2 # homeassistant.components.apsystems -apsystems-ez1==1.3.1 +apsystems-ez1==1.3.3 # homeassistant.components.aqualogic aqualogic==2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a849b83c86..749e3cb3479 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -428,7 +428,7 @@ apprise==1.8.0 aprslib==0.7.2 # homeassistant.components.apsystems -apsystems-ez1==1.3.1 +apsystems-ez1==1.3.3 # homeassistant.components.aranet aranet4==2.3.4 From ec069f9084eceab36817dafa441162b6afade306 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 28 Jun 2024 08:42:47 +0200 Subject: [PATCH 0375/2411] Set stateclass on unknown numeric Tasmota sensors (#120650) --- homeassistant/components/tasmota/sensor.py | 9 + .../tasmota/snapshots/test_sensor.ambr | 298 ++++++++++++++++++ tests/components/tasmota/test_sensor.py | 25 ++ 3 files changed, 332 insertions(+) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 631e5f2efb0..e87ff88092e 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -302,6 +302,15 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): self._attr_native_unit_of_measurement = SENSOR_UNIT_MAP.get( self._tasmota_entity.unit, self._tasmota_entity.unit ) + if ( + self._attr_device_class is None + and self._attr_state_class is None + and self._attr_native_unit_of_measurement is None + ): + # If the sensor has a numeric value, but we couldn't detect what it is, + # set state class to measurement. + if self._tasmota_entity.discovered_as_numeric: + self._attr_state_class = SensorStateClass.MEASUREMENT async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index 829df2b881f..be011e595b9 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -1712,3 +1712,301 @@ 'state': '2300', }) # --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR1 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR1 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR1_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR3 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR4 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].12 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR1 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].13 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR2 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].14 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR3 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].15 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR4 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR2 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR2 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR2_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR3 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR3 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR3_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR4 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].7 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR4 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR4_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR1 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].9 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR2 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index e06d55f17a7..78235f7ebf5 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -50,6 +50,17 @@ BAD_LIST_SENSOR_CONFIG_3 = { } } +# This configuration has sensors which type we can't guess +DEFAULT_SENSOR_CONFIG_UNKNOWN = { + "sn": { + "Time": "2020-09-25T12:47:15", + "SENSOR1": {"Unknown": None}, + "SENSOR2": {"Unknown": "123"}, + "SENSOR3": {"Unknown": 123}, + "SENSOR4": {"Unknown": 123.0}, + } +} + # This configuration has some sensors where values are lists # Home Assistant maps this to one sensor for each list item LIST_SENSOR_CONFIG = { @@ -281,6 +292,20 @@ TEMPERATURE_SENSOR_CONFIG = { ), ), ), + # Test we automatically set state class to measurement on unknown numerical sensors + ( + DEFAULT_SENSOR_CONFIG_UNKNOWN, + [ + "sensor.tasmota_sensor1_unknown", + "sensor.tasmota_sensor2_unknown", + "sensor.tasmota_sensor3_unknown", + "sensor.tasmota_sensor4_unknown", + ], + ( + '{"SENSOR1":{"Unknown":20.5},"SENSOR2":{"Unknown":20.5},"SENSOR3":{"Unknown":20.5},"SENSOR4":{"Unknown":20.5}}', + '{"StatusSNS":{"SENSOR1":{"Unknown":20},"SENSOR2":{"Unknown":20},"SENSOR3":{"Unknown":20},"SENSOR4":{"Unknown":20}}}', + ), + ), ], ) async def test_controlling_state_via_mqtt( From e0e744aed77cff1bc64e7b336cdb57f85f1c7b99 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Fri, 28 Jun 2024 07:52:01 +0100 Subject: [PATCH 0376/2411] Bump ring-doorbell to 0.8.12 (#120671) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 63417f90b42..a3d15bd711d 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -14,5 +14,5 @@ "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], "quality_scale": "silver", - "requirements": ["ring-doorbell[listen]==0.8.11"] + "requirements": ["ring-doorbell[listen]==0.8.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index f7c8f979327..fb5438675b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2469,7 +2469,7 @@ rfk101py==0.0.1 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.11 +ring-doorbell[listen]==0.8.12 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 749e3cb3479..604adf858b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1930,7 +1930,7 @@ reolink-aio==0.9.3 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.11 +ring-doorbell[listen]==0.8.12 # homeassistant.components.roku rokuecp==0.19.3 From 827bfa89b331ae9cdeabb7a8f70153c3e0ea0aa1 Mon Sep 17 00:00:00 2001 From: Dave Leaver Date: Fri, 28 Jun 2024 19:44:54 +1200 Subject: [PATCH 0377/2411] Bump airtouch5py to 1.2.0 (#120715) * Bump airtouch5py to fix console 1.2.0 * Bump airtouch5py again --- homeassistant/components/airtouch5/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airtouch5/manifest.json b/homeassistant/components/airtouch5/manifest.json index 0d4cbc32761..312a627d0e8 100644 --- a/homeassistant/components/airtouch5/manifest.json +++ b/homeassistant/components/airtouch5/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airtouch5", "iot_class": "local_push", "loggers": ["airtouch5py"], - "requirements": ["airtouch5py==0.2.8"] + "requirements": ["airtouch5py==0.2.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index fb5438675b7..3ee5d0d6c62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -428,7 +428,7 @@ airthings-cloud==0.2.0 airtouch4pyapi==1.0.5 # homeassistant.components.airtouch5 -airtouch5py==0.2.8 +airtouch5py==0.2.10 # homeassistant.components.alpha_vantage alpha-vantage==2.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 604adf858b5..b52885dc8d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -401,7 +401,7 @@ airthings-cloud==0.2.0 airtouch4pyapi==1.0.5 # homeassistant.components.airtouch5 -airtouch5py==0.2.8 +airtouch5py==0.2.10 # homeassistant.components.amberelectric amberelectric==1.1.0 From c98e70a6dce0d098ba1607d885794817e3609ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Fri, 28 Jun 2024 11:45:27 +0300 Subject: [PATCH 0378/2411] Add electrical consumption sensor to Overkiz (#120717) electrical consumption sensor --- homeassistant/components/overkiz/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index c62840eea97..d313faf0c1d 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -182,6 +182,13 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), + OverkizSensorDescription( + key=OverkizState.MODBUSLINK_POWER_HEAT_ELECTRICAL, + name="Electric power consumption", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF1, name="Consumption tariff 1", From 03c6e0c55f9686cec5c7345ffda4f6ee5c6c9a03 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 10:46:02 +0200 Subject: [PATCH 0379/2411] Fix SIM300 (#120725) --- .../androidtv_remote/test_media_player.py | 4 +-- tests/components/automation/test_init.py | 6 ++-- tests/components/automation/test_recorder.py | 2 +- .../components/bayesian/test_binary_sensor.py | 20 +++++++------ tests/components/demo/test_notify.py | 4 +-- tests/components/ecobee/test_climate.py | 26 ++++++++--------- tests/components/filter/test_sensor.py | 4 +-- tests/components/group/test_init.py | 29 ++++++++++--------- tests/components/mqtt/test_climate.py | 4 +-- tests/components/mqtt/test_water_heater.py | 4 +-- .../components/universal/test_media_player.py | 8 ++--- tests/helpers/test_config_validation.py | 20 ++++++------- tests/helpers/test_entity_component.py | 25 +++++++++------- tests/helpers/test_service.py | 18 ++++++------ tests/test_config.py | 7 +++-- tests/util/test_color.py | 12 ++++---- tests/util/test_dt.py | 6 ++-- 17 files changed, 103 insertions(+), 96 deletions(-) diff --git a/tests/components/androidtv_remote/test_media_player.py b/tests/components/androidtv_remote/test_media_player.py index ad7c049e32f..46678f18fd3 100644 --- a/tests/components/androidtv_remote/test_media_player.py +++ b/tests/components/androidtv_remote/test_media_player.py @@ -345,7 +345,7 @@ async def test_browse_media( ) response = await client.receive_json() assert response["success"] - assert { + assert response["result"] == { "title": "Applications", "media_class": "directory", "media_content_type": "apps", @@ -377,7 +377,7 @@ async def test_browse_media( "thumbnail": "", }, ], - } == response["result"] + } async def test_media_player_connection_closed( diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 0c300540644..8bac0c15db9 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -178,7 +178,7 @@ async def test_service_specify_entity_id( hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 1 - assert ["hello.world"] == calls[0].data.get(ATTR_ENTITY_ID) + assert calls[0].data.get(ATTR_ENTITY_ID) == ["hello.world"] async def test_service_specify_entity_id_list( @@ -202,7 +202,7 @@ async def test_service_specify_entity_id_list( hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 1 - assert ["hello.world", "hello.world2"] == calls[0].data.get(ATTR_ENTITY_ID) + assert calls[0].data.get(ATTR_ENTITY_ID) == ["hello.world", "hello.world2"] async def test_two_triggers(hass: HomeAssistant, calls: list[ServiceCall]) -> None: @@ -1641,7 +1641,7 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(calls) == 1 - assert ["hello.world"] == calls[0].data.get(ATTR_ENTITY_ID) + assert calls[0].data.get(ATTR_ENTITY_ID) == ["hello.world"] @pytest.mark.parametrize( diff --git a/tests/components/automation/test_recorder.py b/tests/components/automation/test_recorder.py index fc45e6aee5b..af3d0c41151 100644 --- a/tests/components/automation/test_recorder.py +++ b/tests/components/automation/test_recorder.py @@ -48,7 +48,7 @@ async def test_exclude_attributes( hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 1 - assert ["hello.world"] == calls[0].data.get(ATTR_ENTITY_ID) + assert calls[0].data.get(ATTR_ENTITY_ID) == ["hello.world"] await async_wait_recording_done(hass) states = await hass.async_add_executor_job( diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index e4f646572cb..818e9bed909 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -718,17 +718,18 @@ async def test_observed_entities(hass: HomeAssistant) -> None: await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") - assert ["sensor.test_monitored"] == state.attributes.get( - "occurred_observation_entities" - ) + assert state.attributes.get("occurred_observation_entities") == [ + "sensor.test_monitored" + ] hass.states.async_set("sensor.test_monitored1", "on") await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") - assert ["sensor.test_monitored", "sensor.test_monitored1"] == sorted( - state.attributes.get("occurred_observation_entities") - ) + assert sorted(state.attributes.get("occurred_observation_entities")) == [ + "sensor.test_monitored", + "sensor.test_monitored1", + ] async def test_state_attributes_are_serializable(hass: HomeAssistant) -> None: @@ -785,9 +786,10 @@ async def test_state_attributes_are_serializable(hass: HomeAssistant) -> None: await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_binary") - assert ["sensor.test_monitored", "sensor.test_monitored1"] == sorted( - state.attributes.get("occurred_observation_entities") - ) + assert sorted(state.attributes.get("occurred_observation_entities")) == [ + "sensor.test_monitored", + "sensor.test_monitored1", + ] for attrs in state.attributes.values(): json.dumps(attrs) diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index 4ebbfbdac04..e9aa97f3d06 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -81,6 +81,6 @@ async def test_calling_notify_from_script_loaded_from_yaml( await hass.services.async_call("script", "test") await hass.async_block_till_done() assert len(events) == 1 - assert { + assert events[0].data == { "message": "Test 123 4", - } == events[0].data + } diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index ae53132fe46..1c9dcec0ad2 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -195,7 +195,7 @@ async def test_hvac_mode(ecobee_fixture, thermostat) -> None: async def test_hvac_modes(thermostat) -> None: """Test operation list property.""" - assert ["heat_cool", "heat", "cool", "off"] == thermostat.hvac_modes + assert thermostat.hvac_modes == ["heat_cool", "heat", "cool", "off"] async def test_hvac_mode2(ecobee_fixture, thermostat) -> None: @@ -208,51 +208,51 @@ async def test_hvac_mode2(ecobee_fixture, thermostat) -> None: async def test_extra_state_attributes(ecobee_fixture, thermostat) -> None: """Test device state attributes property.""" ecobee_fixture["equipmentStatus"] = "heatPump2" - assert { + assert thermostat.extra_state_attributes == { "fan": "off", "climate_mode": "Climate1", "fan_min_on_time": 10, "equipment_running": "heatPump2", - } == thermostat.extra_state_attributes + } ecobee_fixture["equipmentStatus"] = "auxHeat2" - assert { + assert thermostat.extra_state_attributes == { "fan": "off", "climate_mode": "Climate1", "fan_min_on_time": 10, "equipment_running": "auxHeat2", - } == thermostat.extra_state_attributes + } ecobee_fixture["equipmentStatus"] = "compCool1" - assert { + assert thermostat.extra_state_attributes == { "fan": "off", "climate_mode": "Climate1", "fan_min_on_time": 10, "equipment_running": "compCool1", - } == thermostat.extra_state_attributes + } ecobee_fixture["equipmentStatus"] = "" - assert { + assert thermostat.extra_state_attributes == { "fan": "off", "climate_mode": "Climate1", "fan_min_on_time": 10, "equipment_running": "", - } == thermostat.extra_state_attributes + } ecobee_fixture["equipmentStatus"] = "Unknown" - assert { + assert thermostat.extra_state_attributes == { "fan": "off", "climate_mode": "Climate1", "fan_min_on_time": 10, "equipment_running": "Unknown", - } == thermostat.extra_state_attributes + } ecobee_fixture["program"]["currentClimateRef"] = "c2" - assert { + assert thermostat.extra_state_attributes == { "fan": "off", "climate_mode": "Climate2", "fan_min_on_time": 10, "equipment_running": "Unknown", - } == thermostat.extra_state_attributes + } async def test_is_aux_heat_on(hass: HomeAssistant) -> None: diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 0ece61708f2..a9581b78f4e 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -467,7 +467,7 @@ def test_throttle(values: list[State]) -> None: new_state = filt.filter_state(state) if not filt.skip_processing: filtered.append(new_state) - assert [20, 21] == [f.state for f in filtered] + assert [f.state for f in filtered] == [20, 21] def test_time_throttle(values: list[State]) -> None: @@ -480,7 +480,7 @@ def test_time_throttle(values: list[State]) -> None: new_state = filt.filter_state(state) if not filt.skip_processing: filtered.append(new_state) - assert [20, 18, 22] == [f.state for f in filtered] + assert [f.state for f in filtered] == [20, 18, 22] def test_time_sma(values: list[State]) -> None: diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 7434de74f63..bbbe22cba83 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -405,13 +405,13 @@ async def test_expand_entity_ids_does_not_return_duplicates( order=None, ) - assert ["light.bowl", "light.ceiling"] == sorted( + assert sorted( group.expand_entity_ids(hass, [test_group.entity_id, "light.Ceiling"]) - ) + ) == ["light.bowl", "light.ceiling"] - assert ["light.bowl", "light.ceiling"] == sorted( + assert sorted( group.expand_entity_ids(hass, ["light.bowl", test_group.entity_id]) - ) + ) == ["light.bowl", "light.ceiling"] async def test_expand_entity_ids_recursive(hass: HomeAssistant) -> None: @@ -439,7 +439,7 @@ async def test_expand_entity_ids_recursive(hass: HomeAssistant) -> None: async def test_expand_entity_ids_ignores_non_strings(hass: HomeAssistant) -> None: """Test that non string elements in lists are ignored.""" - assert [] == group.expand_entity_ids(hass, [5, True]) + assert group.expand_entity_ids(hass, [5, True]) == [] async def test_get_entity_ids(hass: HomeAssistant) -> None: @@ -460,9 +460,10 @@ async def test_get_entity_ids(hass: HomeAssistant) -> None: order=None, ) - assert ["light.bowl", "light.ceiling"] == sorted( - group.get_entity_ids(hass, test_group.entity_id) - ) + assert sorted(group.get_entity_ids(hass, test_group.entity_id)) == [ + "light.bowl", + "light.ceiling", + ] async def test_get_entity_ids_with_domain_filter(hass: HomeAssistant) -> None: @@ -482,19 +483,19 @@ async def test_get_entity_ids_with_domain_filter(hass: HomeAssistant) -> None: order=None, ) - assert ["switch.ac"] == group.get_entity_ids( + assert group.get_entity_ids( hass, mixed_group.entity_id, domain_filter="switch" - ) + ) == ["switch.ac"] async def test_get_entity_ids_with_non_existing_group_name(hass: HomeAssistant) -> None: """Test get_entity_ids with a non existing group.""" - assert [] == group.get_entity_ids(hass, "non_existing") + assert group.get_entity_ids(hass, "non_existing") == [] async def test_get_entity_ids_with_non_group_state(hass: HomeAssistant) -> None: """Test get_entity_ids with a non group state.""" - assert [] == group.get_entity_ids(hass, "switch.AC") + assert group.get_entity_ids(hass, "switch.AC") == [] async def test_group_being_init_before_first_tracked_state_is_set_to_on( @@ -620,12 +621,12 @@ async def test_expand_entity_ids_expands_nested_groups(hass: HomeAssistant) -> N order=None, ) - assert [ + assert sorted(group.expand_entity_ids(hass, ["group.group_of_groups"])) == [ "light.test_1", "light.test_2", "switch.test_1", "switch.test_2", - ] == sorted(group.expand_entity_ids(hass, ["group.group_of_groups"])) + ] async def test_set_assumed_state_based_on_tracked(hass: HomeAssistant) -> None: diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index c41a6366dfe..168fed6164e 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -179,14 +179,14 @@ async def test_get_hvac_modes( state = hass.states.get(ENTITY_CLIMATE) modes = state.attributes.get("hvac_modes") - assert [ + assert modes == [ HVACMode.AUTO, HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT, HVACMode.DRY, HVACMode.FAN_ONLY, - ] == modes + ] @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) diff --git a/tests/components/mqtt/test_water_heater.py b/tests/components/mqtt/test_water_heater.py index 849a1ac8785..86f8b227bd5 100644 --- a/tests/components/mqtt/test_water_heater.py +++ b/tests/components/mqtt/test_water_heater.py @@ -141,7 +141,7 @@ async def test_get_operation_modes( await mqtt_mock_entry() state = hass.states.get(ENTITY_WATER_HEATER) - assert [ + assert state.attributes.get("operation_list") == [ STATE_ECO, STATE_ELECTRIC, STATE_GAS, @@ -149,7 +149,7 @@ async def test_get_operation_modes( STATE_HIGH_DEMAND, STATE_PERFORMANCE, STATE_OFF, - ] == state.attributes.get("operation_list") + ] @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 814fa34a125..527675a2208 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -325,10 +325,10 @@ async def test_config_bad_children(hass: HomeAssistant) -> None: config_bad_children = {"name": "test", "children": {}, "platform": "universal"} config_no_children = validate_config(config_no_children) - assert [] == config_no_children["children"] + assert config_no_children["children"] == [] config_bad_children = validate_config(config_bad_children) - assert [] == config_bad_children["children"] + assert config_bad_children["children"] == [] async def test_config_bad_commands(hass: HomeAssistant) -> None: @@ -336,7 +336,7 @@ async def test_config_bad_commands(hass: HomeAssistant) -> None: config = {"name": "test", "platform": "universal"} config = validate_config(config) - assert {} == config["commands"] + assert config["commands"] == {} async def test_config_bad_attributes(hass: HomeAssistant) -> None: @@ -344,7 +344,7 @@ async def test_config_bad_attributes(hass: HomeAssistant) -> None: config = {"name": "test", "platform": "universal"} config = validate_config(config) - assert {} == config["attributes"] + assert config["attributes"] == {} async def test_config_bad_key(hass: HomeAssistant) -> None: diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 6df29eefaff..514eff94250 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -193,12 +193,12 @@ def test_platform_config() -> None: def test_ensure_list() -> None: """Test ensure_list.""" schema = vol.Schema(cv.ensure_list) - assert [] == schema(None) - assert [1] == schema(1) - assert [1] == schema([1]) - assert ["1"] == schema("1") - assert ["1"] == schema(["1"]) - assert [{"1": "2"}] == schema({"1": "2"}) + assert schema(None) == [] + assert schema(1) == [1] + assert schema([1]) == [1] + assert schema("1") == ["1"] + assert schema(["1"]) == ["1"] + assert schema({"1": "2"}) == [{"1": "2"}] def test_entity_id() -> None: @@ -965,7 +965,7 @@ def test_deprecated_with_replacement_key( assert ( "The 'mars' option is deprecated, please replace it with 'jupiter'" ) in caplog.text - assert {"jupiter": True} == output + assert output == {"jupiter": True} caplog.clear() assert len(caplog.records) == 0 @@ -1036,7 +1036,7 @@ def test_deprecated_with_replacement_key_and_default( assert ( "The 'mars' option is deprecated, please replace it with 'jupiter'" ) in caplog.text - assert {"jupiter": True} == output + assert output == {"jupiter": True} caplog.clear() assert len(caplog.records) == 0 @@ -1049,7 +1049,7 @@ def test_deprecated_with_replacement_key_and_default( test_data = {"venus": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 0 - assert {"venus": True, "jupiter": False} == output + assert output == {"venus": True, "jupiter": False} deprecated_schema_with_default = vol.All( vol.Schema( @@ -1068,7 +1068,7 @@ def test_deprecated_with_replacement_key_and_default( assert ( "The 'mars' option is deprecated, please replace it with 'jupiter'" ) in caplog.text - assert {"jupiter": True} == output + assert output == {"jupiter": True} def test_deprecated_cant_find_module() -> None: diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 32ce740edb2..3f34305b39d 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -117,7 +117,7 @@ async def test_setup_does_discovery( await hass.async_block_till_done() assert mock_setup.called - assert ("platform_test", {}, {"msg": "discovery_info"}) == mock_setup.call_args[0] + assert mock_setup.call_args[0] == ("platform_test", {}, {"msg": "discovery_info"}) async def test_set_scan_interval_via_config(hass: HomeAssistant) -> None: @@ -191,9 +191,9 @@ async def test_extract_from_service_available_device(hass: HomeAssistant) -> Non call_1 = ServiceCall("test", "service", data={"entity_id": ENTITY_MATCH_ALL}) - assert ["test_domain.test_1", "test_domain.test_3"] == sorted( + assert sorted( ent.entity_id for ent in (await component.async_extract_from_service(call_1)) - ) + ) == ["test_domain.test_1", "test_domain.test_3"] call_2 = ServiceCall( "test", @@ -201,9 +201,9 @@ async def test_extract_from_service_available_device(hass: HomeAssistant) -> Non data={"entity_id": ["test_domain.test_3", "test_domain.test_4"]}, ) - assert ["test_domain.test_3"] == sorted( + assert sorted( ent.entity_id for ent in (await component.async_extract_from_service(call_2)) - ) + ) == ["test_domain.test_3"] async def test_platform_not_ready(hass: HomeAssistant) -> None: @@ -288,9 +288,9 @@ async def test_extract_from_service_filter_out_non_existing_entities( {"entity_id": ["test_domain.test_2", "test_domain.non_exist"]}, ) - assert ["test_domain.test_2"] == [ + assert [ ent.entity_id for ent in await component.async_extract_from_service(call) - ] + ] == ["test_domain.test_2"] async def test_extract_from_service_no_group_expand(hass: HomeAssistant) -> None: @@ -467,8 +467,11 @@ async def test_extract_all_omit_entity_id( call = ServiceCall("test", "service") - assert [] == sorted( - ent.entity_id for ent in await component.async_extract_from_service(call) + assert ( + sorted( + ent.entity_id for ent in await component.async_extract_from_service(call) + ) + == [] ) @@ -484,9 +487,9 @@ async def test_extract_all_use_match_all( call = ServiceCall("test", "service", {"entity_id": "all"}) - assert ["test_domain.test_1", "test_domain.test_2"] == sorted( + assert sorted( ent.entity_id for ent in await component.async_extract_from_service(call) - ) + ) == ["test_domain.test_1", "test_domain.test_2"] assert ( "Not passing an entity ID to a service to target all entities is deprecated" ) not in caplog.text diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 9c5cda67725..b05cdf9c3ae 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -547,7 +547,7 @@ async def test_split_entity_string(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert ["hello.world", "sensor.beer"] == calls[-1].data.get("entity_id") + assert calls[-1].data.get("entity_id") == ["hello.world", "sensor.beer"] async def test_not_mutate_input(hass: HomeAssistant) -> None: @@ -1792,10 +1792,10 @@ async def test_extract_from_service_available_device(hass: HomeAssistant) -> Non call_1 = ServiceCall("test", "service", data={"entity_id": ENTITY_MATCH_ALL}) - assert ["test_domain.test_1", "test_domain.test_3"] == [ + assert [ ent.entity_id for ent in (await service.async_extract_entities(hass, entities, call_1)) - ] + ] == ["test_domain.test_1", "test_domain.test_3"] call_2 = ServiceCall( "test", @@ -1803,10 +1803,10 @@ async def test_extract_from_service_available_device(hass: HomeAssistant) -> Non data={"entity_id": ["test_domain.test_3", "test_domain.test_4"]}, ) - assert ["test_domain.test_3"] == [ + assert [ ent.entity_id for ent in (await service.async_extract_entities(hass, entities, call_2)) - ] + ] == ["test_domain.test_3"] assert ( await service.async_extract_entities( @@ -1830,10 +1830,10 @@ async def test_extract_from_service_empty_if_no_entity_id(hass: HomeAssistant) - ] call = ServiceCall("test", "service") - assert [] == [ + assert [ ent.entity_id for ent in (await service.async_extract_entities(hass, entities, call)) - ] + ] == [] async def test_extract_from_service_filter_out_non_existing_entities( @@ -1851,10 +1851,10 @@ async def test_extract_from_service_filter_out_non_existing_entities( {"entity_id": ["test_domain.test_2", "test_domain.non_exist"]}, ) - assert ["test_domain.test_2"] == [ + assert [ ent.entity_id for ent in (await service.async_extract_entities(hass, entities, call)) - ] + ] == ["test_domain.test_2"] async def test_extract_from_service_area_id( diff --git a/tests/test_config.py b/tests/test_config.py index 7f94317afea..748255fb205 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -490,9 +490,10 @@ def test_load_yaml_config_preserves_key_order() -> None: fp.write("hello: 2\n") fp.write("world: 1\n") - assert [("hello", 2), ("world", 1)] == list( - config_util.load_yaml_config_file(YAML_PATH).items() - ) + assert list(config_util.load_yaml_config_file(YAML_PATH).items()) == [ + ("hello", 2), + ("world", 1), + ] async def test_create_default_config_returns_none_if_write_error( diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 53c243a1e4f..c8a5e0c8587 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -200,17 +200,17 @@ def test_color_hs_to_xy() -> None: def test_rgb_hex_to_rgb_list() -> None: """Test rgb_hex_to_rgb_list.""" - assert [255, 255, 255] == color_util.rgb_hex_to_rgb_list("ffffff") + assert color_util.rgb_hex_to_rgb_list("ffffff") == [255, 255, 255] - assert [0, 0, 0] == color_util.rgb_hex_to_rgb_list("000000") + assert color_util.rgb_hex_to_rgb_list("000000") == [0, 0, 0] - assert [255, 255, 255, 255] == color_util.rgb_hex_to_rgb_list("ffffffff") + assert color_util.rgb_hex_to_rgb_list("ffffffff") == [255, 255, 255, 255] - assert [0, 0, 0, 0] == color_util.rgb_hex_to_rgb_list("00000000") + assert color_util.rgb_hex_to_rgb_list("00000000") == [0, 0, 0, 0] - assert [51, 153, 255] == color_util.rgb_hex_to_rgb_list("3399ff") + assert color_util.rgb_hex_to_rgb_list("3399ff") == [51, 153, 255] - assert [51, 153, 255, 0] == color_util.rgb_hex_to_rgb_list("3399ff00") + assert color_util.rgb_hex_to_rgb_list("3399ff00") == [51, 153, 255, 0] def test_color_name_to_rgb_valid_name() -> None: diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 6caca092517..0e8432bbb83 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -294,12 +294,12 @@ def test_parse_time_expression() -> None: assert list(range(0, 60, 5)) == dt_util.parse_time_expression("/5", 0, 59) - assert [1, 2, 3] == dt_util.parse_time_expression([2, 1, 3], 0, 59) + assert dt_util.parse_time_expression([2, 1, 3], 0, 59) == [1, 2, 3] assert list(range(24)) == dt_util.parse_time_expression("*", 0, 23) - assert [42] == dt_util.parse_time_expression(42, 0, 59) - assert [42] == dt_util.parse_time_expression("42", 0, 59) + assert dt_util.parse_time_expression(42, 0, 59) == [42] + assert dt_util.parse_time_expression("42", 0, 59) == [42] with pytest.raises(ValueError): dt_util.parse_time_expression(61, 0, 60) From 84de2da19f63390f5f1b037d1d79202e0671bde2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Fri, 28 Jun 2024 11:53:07 +0300 Subject: [PATCH 0380/2411] Add warm water remaining volume sensor to Overkiz (#120718) * warm water remaining volume sensor * Update homeassistant/components/overkiz/sensor.py Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> --------- Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> --- homeassistant/components/overkiz/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index d313faf0c1d..bf9608358eb 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -420,6 +420,13 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), + OverkizSensorDescription( + key=OverkizState.CORE_REMAINING_HOT_WATER, + name="Warm water remaining", + device_class=SensorDeviceClass.VOLUME, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfVolume.LITERS, + ), # Cover OverkizSensorDescription( key=OverkizState.CORE_TARGET_CLOSURE, From 5e39faa9f85d0bcc342a301a6659c2397b5409a4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:01:53 +0200 Subject: [PATCH 0381/2411] Improve type hints in auth tests (#120655) --- tests/auth/providers/test_command_line.py | 41 +++++++---- tests/auth/providers/test_insecure_example.py | 29 +++++--- tests/auth/providers/test_trusted_networks.py | 71 ++++++++++++++----- 3 files changed, 104 insertions(+), 37 deletions(-) diff --git a/tests/auth/providers/test_command_line.py b/tests/auth/providers/test_command_line.py index 016ce767bad..2ce49730e5f 100644 --- a/tests/auth/providers/test_command_line.py +++ b/tests/auth/providers/test_command_line.py @@ -10,10 +10,11 @@ from homeassistant import data_entry_flow from homeassistant.auth import AuthManager, auth_store, models as auth_models from homeassistant.auth.providers import command_line from homeassistant.const import CONF_TYPE +from homeassistant.core import HomeAssistant @pytest.fixture -async def store(hass): +async def store(hass: HomeAssistant) -> auth_store.AuthStore: """Mock store.""" store = auth_store.AuthStore(hass) await store.async_load() @@ -21,7 +22,9 @@ async def store(hass): @pytest.fixture -def provider(hass, store): +def provider( + hass: HomeAssistant, store: auth_store.AuthStore +) -> command_line.CommandLineAuthProvider: """Mock provider.""" return command_line.CommandLineAuthProvider( hass, @@ -38,12 +41,18 @@ def provider(hass, store): @pytest.fixture -def manager(hass, store, provider): +def manager( + hass: HomeAssistant, + store: auth_store.AuthStore, + provider: command_line.CommandLineAuthProvider, +) -> AuthManager: """Mock manager.""" return AuthManager(hass, store, {(provider.type, provider.id): provider}, {}) -async def test_create_new_credential(manager, provider) -> None: +async def test_create_new_credential( + manager: AuthManager, provider: command_line.CommandLineAuthProvider +) -> None: """Test that we create a new credential.""" credentials = await provider.async_get_or_create_credentials( {"username": "good-user", "password": "good-pass"} @@ -57,7 +66,9 @@ async def test_create_new_credential(manager, provider) -> None: assert not user.local_only -async def test_match_existing_credentials(store, provider) -> None: +async def test_match_existing_credentials( + provider: command_line.CommandLineAuthProvider, +) -> None: """See if we match existing users.""" existing = auth_models.Credentials( id=uuid.uuid4(), @@ -73,24 +84,26 @@ async def test_match_existing_credentials(store, provider) -> None: assert credentials is existing -async def test_invalid_username(provider) -> None: +async def test_invalid_username(provider: command_line.CommandLineAuthProvider) -> None: """Test we raise if incorrect user specified.""" with pytest.raises(command_line.InvalidAuthError): await provider.async_validate_login("bad-user", "good-pass") -async def test_invalid_password(provider) -> None: +async def test_invalid_password(provider: command_line.CommandLineAuthProvider) -> None: """Test we raise if incorrect password specified.""" with pytest.raises(command_line.InvalidAuthError): await provider.async_validate_login("good-user", "bad-pass") -async def test_good_auth(provider) -> None: +async def test_good_auth(provider: command_line.CommandLineAuthProvider) -> None: """Test nothing is raised with good credentials.""" await provider.async_validate_login("good-user", "good-pass") -async def test_good_auth_with_meta(manager, provider) -> None: +async def test_good_auth_with_meta( + manager: AuthManager, provider: command_line.CommandLineAuthProvider +) -> None: """Test metadata is added upon successful authentication.""" provider.config[command_line.CONF_ARGS] = ["--with-meta"] provider.config[command_line.CONF_META] = True @@ -110,7 +123,9 @@ async def test_good_auth_with_meta(manager, provider) -> None: assert user.local_only -async def test_utf_8_username_password(provider) -> None: +async def test_utf_8_username_password( + provider: command_line.CommandLineAuthProvider, +) -> None: """Test that we create a new credential.""" credentials = await provider.async_get_or_create_credentials( {"username": "ßßß", "password": "äöü"} @@ -118,7 +133,9 @@ async def test_utf_8_username_password(provider) -> None: assert credentials.is_new is True -async def test_login_flow_validates(provider) -> None: +async def test_login_flow_validates( + provider: command_line.CommandLineAuthProvider, +) -> None: """Test login flow.""" flow = await provider.async_login_flow({}) result = await flow.async_step_init() @@ -137,7 +154,7 @@ async def test_login_flow_validates(provider) -> None: assert result["data"]["username"] == "good-user" -async def test_strip_username(provider) -> None: +async def test_strip_username(provider: command_line.CommandLineAuthProvider) -> None: """Test authentication works with username with whitespace around.""" flow = await provider.async_login_flow({}) result = await flow.async_step_init( diff --git a/tests/auth/providers/test_insecure_example.py b/tests/auth/providers/test_insecure_example.py index f0043231c04..7c28028753c 100644 --- a/tests/auth/providers/test_insecure_example.py +++ b/tests/auth/providers/test_insecure_example.py @@ -7,10 +7,11 @@ import pytest from homeassistant.auth import AuthManager, auth_store, models as auth_models from homeassistant.auth.providers import insecure_example +from homeassistant.core import HomeAssistant @pytest.fixture -async def store(hass): +async def store(hass: HomeAssistant) -> auth_store.AuthStore: """Mock store.""" store = auth_store.AuthStore(hass) await store.async_load() @@ -18,7 +19,9 @@ async def store(hass): @pytest.fixture -def provider(hass, store): +def provider( + hass: HomeAssistant, store: auth_store.AuthStore +) -> insecure_example.ExampleAuthProvider: """Mock provider.""" return insecure_example.ExampleAuthProvider( hass, @@ -38,12 +41,18 @@ def provider(hass, store): @pytest.fixture -def manager(hass, store, provider): +def manager( + hass: HomeAssistant, + store: auth_store.AuthStore, + provider: insecure_example.ExampleAuthProvider, +) -> AuthManager: """Mock manager.""" return AuthManager(hass, store, {(provider.type, provider.id): provider}, {}) -async def test_create_new_credential(manager, provider) -> None: +async def test_create_new_credential( + manager: AuthManager, provider: insecure_example.ExampleAuthProvider +) -> None: """Test that we create a new credential.""" credentials = await provider.async_get_or_create_credentials( {"username": "user-test", "password": "password-test"} @@ -55,7 +64,9 @@ async def test_create_new_credential(manager, provider) -> None: assert user.is_active -async def test_match_existing_credentials(store, provider) -> None: +async def test_match_existing_credentials( + provider: insecure_example.ExampleAuthProvider, +) -> None: """See if we match existing users.""" existing = auth_models.Credentials( id=uuid.uuid4(), @@ -71,19 +82,21 @@ async def test_match_existing_credentials(store, provider) -> None: assert credentials is existing -async def test_verify_username(provider) -> None: +async def test_verify_username(provider: insecure_example.ExampleAuthProvider) -> None: """Test we raise if incorrect user specified.""" with pytest.raises(insecure_example.InvalidAuthError): await provider.async_validate_login("non-existing-user", "password-test") -async def test_verify_password(provider) -> None: +async def test_verify_password(provider: insecure_example.ExampleAuthProvider) -> None: """Test we raise if incorrect user specified.""" with pytest.raises(insecure_example.InvalidAuthError): await provider.async_validate_login("user-test", "incorrect-password") -async def test_utf_8_username_password(provider) -> None: +async def test_utf_8_username_password( + provider: insecure_example.ExampleAuthProvider, +) -> None: """Test that we create a new credential.""" credentials = await provider.async_get_or_create_credentials( {"username": "🎉", "password": "😎"} diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 2f84a256f2d..f16c066a7e8 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -17,7 +17,7 @@ from homeassistant.setup import async_setup_component @pytest.fixture -async def store(hass): +async def store(hass: HomeAssistant) -> auth_store.AuthStore: """Mock store.""" store = auth_store.AuthStore(hass) await store.async_load() @@ -25,7 +25,9 @@ async def store(hass): @pytest.fixture -def provider(hass, store): +def provider( + hass: HomeAssistant, store: auth_store.AuthStore +) -> tn_auth.TrustedNetworksAuthProvider: """Mock provider.""" return tn_auth.TrustedNetworksAuthProvider( hass, @@ -45,7 +47,9 @@ def provider(hass, store): @pytest.fixture -def provider_with_user(hass, store): +def provider_with_user( + hass: HomeAssistant, store: auth_store.AuthStore +) -> tn_auth.TrustedNetworksAuthProvider: """Mock provider with trusted users config.""" return tn_auth.TrustedNetworksAuthProvider( hass, @@ -71,7 +75,9 @@ def provider_with_user(hass, store): @pytest.fixture -def provider_bypass_login(hass, store): +def provider_bypass_login( + hass: HomeAssistant, store: auth_store.AuthStore +) -> tn_auth.TrustedNetworksAuthProvider: """Mock provider with allow_bypass_login config.""" return tn_auth.TrustedNetworksAuthProvider( hass, @@ -92,13 +98,21 @@ def provider_bypass_login(hass, store): @pytest.fixture -def manager(hass, store, provider): +def manager( + hass: HomeAssistant, + store: auth_store.AuthStore, + provider: tn_auth.TrustedNetworksAuthProvider, +) -> auth.AuthManager: """Mock manager.""" return auth.AuthManager(hass, store, {(provider.type, provider.id): provider}, {}) @pytest.fixture -def manager_with_user(hass, store, provider_with_user): +def manager_with_user( + hass: HomeAssistant, + store: auth_store.AuthStore, + provider_with_user: tn_auth.TrustedNetworksAuthProvider, +) -> auth.AuthManager: """Mock manager with trusted user.""" return auth.AuthManager( hass, @@ -109,7 +123,11 @@ def manager_with_user(hass, store, provider_with_user): @pytest.fixture -def manager_bypass_login(hass, store, provider_bypass_login): +def manager_bypass_login( + hass: HomeAssistant, + store: auth_store.AuthStore, + provider_bypass_login: tn_auth.TrustedNetworksAuthProvider, +) -> auth.AuthManager: """Mock manager with allow bypass login.""" return auth.AuthManager( hass, @@ -119,7 +137,7 @@ def manager_bypass_login(hass, store, provider_bypass_login): ) -async def test_config_schema(): +async def test_config_schema() -> None: """Test CONFIG_SCHEMA.""" # Valid configuration tn_auth.CONFIG_SCHEMA( @@ -145,7 +163,9 @@ async def test_config_schema(): ) -async def test_trusted_networks_credentials(manager, provider) -> None: +async def test_trusted_networks_credentials( + manager: auth.AuthManager, provider: tn_auth.TrustedNetworksAuthProvider +) -> None: """Test trusted_networks credentials related functions.""" owner = await manager.async_create_user("test-owner") tn_owner_cred = await provider.async_get_or_create_credentials({"user": owner.id}) @@ -162,7 +182,7 @@ async def test_trusted_networks_credentials(manager, provider) -> None: await provider.async_get_or_create_credentials({"user": "invalid-user"}) -async def test_validate_access(provider) -> None: +async def test_validate_access(provider: tn_auth.TrustedNetworksAuthProvider) -> None: """Test validate access from trusted networks.""" provider.async_validate_access(ip_address("192.168.0.1")) provider.async_validate_access(ip_address("192.168.128.10")) @@ -177,7 +197,9 @@ async def test_validate_access(provider) -> None: provider.async_validate_access(ip_address("2001:db8::ff00:42:8329")) -async def test_validate_access_proxy(hass: HomeAssistant, provider) -> None: +async def test_validate_access_proxy( + hass: HomeAssistant, provider: tn_auth.TrustedNetworksAuthProvider +) -> None: """Test validate access from trusted networks are blocked from proxy.""" await async_setup_component( @@ -200,7 +222,9 @@ async def test_validate_access_proxy(hass: HomeAssistant, provider) -> None: provider.async_validate_access(ip_address("fd00::1")) -async def test_validate_access_cloud(hass: HomeAssistant, provider) -> None: +async def test_validate_access_cloud( + hass: HomeAssistant, provider: tn_auth.TrustedNetworksAuthProvider +) -> None: """Test validate access from trusted networks are blocked from cloud.""" await async_setup_component( hass, @@ -221,7 +245,9 @@ async def test_validate_access_cloud(hass: HomeAssistant, provider) -> None: provider.async_validate_access(ip_address("192.168.128.2")) -async def test_validate_refresh_token(provider) -> None: +async def test_validate_refresh_token( + provider: tn_auth.TrustedNetworksAuthProvider, +) -> None: """Verify re-validation of refresh token.""" with patch.object(provider, "async_validate_access") as mock: with pytest.raises(tn_auth.InvalidAuthError): @@ -231,7 +257,9 @@ async def test_validate_refresh_token(provider) -> None: mock.assert_called_once_with(ip_address("127.0.0.1")) -async def test_login_flow(manager, provider) -> None: +async def test_login_flow( + manager: auth.AuthManager, provider: tn_auth.TrustedNetworksAuthProvider +) -> None: """Test login flow.""" owner = await manager.async_create_user("test-owner") user = await manager.async_create_user("test-user") @@ -258,7 +286,10 @@ async def test_login_flow(manager, provider) -> None: assert step["data"]["user"] == user.id -async def test_trusted_users_login(manager_with_user, provider_with_user) -> None: +async def test_trusted_users_login( + manager_with_user: auth.AuthManager, + provider_with_user: tn_auth.TrustedNetworksAuthProvider, +) -> None: """Test available user list changed per different IP.""" owner = await manager_with_user.async_create_user("test-owner") sys_user = await manager_with_user.async_create_system_user( @@ -338,7 +369,10 @@ async def test_trusted_users_login(manager_with_user, provider_with_user) -> Non assert schema({"user": sys_user.id}) -async def test_trusted_group_login(manager_with_user, provider_with_user) -> None: +async def test_trusted_group_login( + manager_with_user: auth.AuthManager, + provider_with_user: tn_auth.TrustedNetworksAuthProvider, +) -> None: """Test config trusted_user with group_id.""" owner = await manager_with_user.async_create_user("test-owner") # create a user in user group @@ -391,7 +425,10 @@ async def test_trusted_group_login(manager_with_user, provider_with_user) -> Non assert schema({"user": user.id}) -async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login) -> None: +async def test_bypass_login_flow( + manager_bypass_login: auth.AuthManager, + provider_bypass_login: tn_auth.TrustedNetworksAuthProvider, +) -> None: """Test login flow can be bypass if only one user available.""" owner = await manager_bypass_login.async_create_user("test-owner") From 1e72c2f94d5de5aac405059a8a0b90a48191aa42 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:03:01 +0200 Subject: [PATCH 0382/2411] Bump renault-api to 0.2.4 (#120727) --- homeassistant/components/renault/binary_sensor.py | 2 +- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/renault/fixtures/hvac_status.1.json | 2 +- tests/components/renault/fixtures/hvac_status.2.json | 2 +- tests/components/renault/snapshots/test_diagnostics.ambr | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index 7ebc77b8e77..2041499b711 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -81,7 +81,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( key="hvac_status", coordinator="hvac_status", on_key="hvacStatus", - on_value=2, + on_value="on", translation_key="hvac_status", ), RenaultBinarySensorEntityDescription( diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 8407893011c..ffa1cd6acef 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "platinum", - "requirements": ["renault-api==0.2.3"] + "requirements": ["renault-api==0.2.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3ee5d0d6c62..23d95d7c0b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2454,7 +2454,7 @@ refoss-ha==1.2.1 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.3 +renault-api==0.2.4 # homeassistant.components.renson renson-endura-delta==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b52885dc8d8..15a98c97ede 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1918,7 +1918,7 @@ refoss-ha==1.2.1 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.3 +renault-api==0.2.4 # homeassistant.components.renson renson-endura-delta==1.7.1 diff --git a/tests/components/renault/fixtures/hvac_status.1.json b/tests/components/renault/fixtures/hvac_status.1.json index 7cbd7a9fe37..f48cbae68ae 100644 --- a/tests/components/renault/fixtures/hvac_status.1.json +++ b/tests/components/renault/fixtures/hvac_status.1.json @@ -2,6 +2,6 @@ "data": { "type": "Car", "id": "VF1AAAAA555777999", - "attributes": { "externalTemperature": 8.0, "hvacStatus": 1 } + "attributes": { "externalTemperature": 8.0, "hvacStatus": "off" } } } diff --git a/tests/components/renault/fixtures/hvac_status.2.json b/tests/components/renault/fixtures/hvac_status.2.json index 8bb4f941e06..a2ca08a71e9 100644 --- a/tests/components/renault/fixtures/hvac_status.2.json +++ b/tests/components/renault/fixtures/hvac_status.2.json @@ -4,7 +4,7 @@ "id": "VF1AAAAA555777999", "attributes": { "socThreshold": 30.0, - "hvacStatus": 1, + "hvacStatus": "off", "lastUpdateTime": "2020-12-03T00:00:00Z" } } diff --git a/tests/components/renault/snapshots/test_diagnostics.ambr b/tests/components/renault/snapshots/test_diagnostics.ambr index ae90115fcb6..a2921dff35e 100644 --- a/tests/components/renault/snapshots/test_diagnostics.ambr +++ b/tests/components/renault/snapshots/test_diagnostics.ambr @@ -22,7 +22,7 @@ }), 'hvac_status': dict({ 'externalTemperature': 8.0, - 'hvacStatus': 1, + 'hvacStatus': 'off', }), 'res_state': dict({ }), @@ -227,7 +227,7 @@ }), 'hvac_status': dict({ 'externalTemperature': 8.0, - 'hvacStatus': 1, + 'hvacStatus': 'off', }), 'res_state': dict({ }), From 9b980602c9bdeebfec68da53157b3f4b2428ffa8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:09:54 +0200 Subject: [PATCH 0383/2411] Improve type hints in flux_led tests (#120734) --- tests/components/flux_led/conftest.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/components/flux_led/conftest.py b/tests/components/flux_led/conftest.py index 2a67c7b46f7..bc9f68dc3b1 100644 --- a/tests/components/flux_led/conftest.py +++ b/tests/components/flux_led/conftest.py @@ -3,18 +3,11 @@ from unittest.mock import patch import pytest - -from tests.common import mock_device_registry - - -@pytest.fixture(name="device_reg") -def device_reg_fixture(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) +from typing_extensions import Generator @pytest.fixture -def mock_single_broadcast_address(): +def mock_single_broadcast_address() -> Generator[None]: """Mock network's async_async_get_ipv4_broadcast_addresses.""" with patch( "homeassistant.components.network.async_get_ipv4_broadcast_addresses", @@ -24,7 +17,7 @@ def mock_single_broadcast_address(): @pytest.fixture -def mock_multiple_broadcast_addresses(): +def mock_multiple_broadcast_addresses() -> Generator[None]: """Mock network's async_async_get_ipv4_broadcast_addresses to return multiple addresses.""" with patch( "homeassistant.components.network.async_get_ipv4_broadcast_addresses", From 628617704110fdc51517f9ba53115bd61eb5887f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 11:15:44 +0200 Subject: [PATCH 0384/2411] Bump panasonic_viera to 0.4.2 (#120692) * Bump panasonic_viera to 0.4.2 * Bump panasonic_viera to 0.4.2 * Bump panasonic_viera to 0.4.2 * Fix Keys --- .../components/panasonic_viera/__init__.py | 8 ++++---- .../components/panasonic_viera/manifest.json | 2 +- .../components/panasonic_viera/media_player.py | 18 +++++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/panasonic_viera/test_remote.py | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index b2f3bbba91a..2cf91792800 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -196,10 +196,10 @@ class Remote: self.muted = self._control.get_mute() self.volume = self._control.get_volume() / 100 - async def async_send_key(self, key): + async def async_send_key(self, key: Keys | str) -> None: """Send a key to the TV and handle exceptions.""" try: - key = getattr(Keys, key) + key = getattr(Keys, key.upper()) except (AttributeError, TypeError): key = getattr(key, "value", key) @@ -211,13 +211,13 @@ class Remote: await self._on_action.async_run(context=context) await self.async_update() elif self.state != STATE_ON: - await self.async_send_key(Keys.power) + await self.async_send_key(Keys.POWER) await self.async_update() async def async_turn_off(self): """Turn off the TV.""" if self.state != STATE_OFF: - await self.async_send_key(Keys.power) + await self.async_send_key(Keys.POWER) self.state = STATE_OFF await self.async_update() diff --git a/homeassistant/components/panasonic_viera/manifest.json b/homeassistant/components/panasonic_viera/manifest.json index 2afa6599cb2..d9809e5883a 100644 --- a/homeassistant/components/panasonic_viera/manifest.json +++ b/homeassistant/components/panasonic_viera/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/panasonic_viera", "iot_class": "local_polling", "loggers": ["panasonic_viera"], - "requirements": ["panasonic-viera==0.3.6"] + "requirements": ["panasonic-viera==0.4.2"] } diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 44063022129..76ca76c1ca6 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -126,11 +126,11 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): async def async_volume_up(self) -> None: """Volume up the media player.""" - await self._remote.async_send_key(Keys.volume_up) + await self._remote.async_send_key(Keys.VOLUME_UP) async def async_volume_down(self) -> None: """Volume down media player.""" - await self._remote.async_send_key(Keys.volume_down) + await self._remote.async_send_key(Keys.VOLUME_DOWN) async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" @@ -143,33 +143,33 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): async def async_media_play_pause(self) -> None: """Simulate play pause media player.""" if self._remote.playing: - await self._remote.async_send_key(Keys.pause) + await self._remote.async_send_key(Keys.PAUSE) self._remote.playing = False else: - await self._remote.async_send_key(Keys.play) + await self._remote.async_send_key(Keys.PLAY) self._remote.playing = True async def async_media_play(self) -> None: """Send play command.""" - await self._remote.async_send_key(Keys.play) + await self._remote.async_send_key(Keys.PLAY) self._remote.playing = True async def async_media_pause(self) -> None: """Send pause command.""" - await self._remote.async_send_key(Keys.pause) + await self._remote.async_send_key(Keys.PAUSE) self._remote.playing = False async def async_media_stop(self) -> None: """Stop playback.""" - await self._remote.async_send_key(Keys.stop) + await self._remote.async_send_key(Keys.STOP) async def async_media_next_track(self) -> None: """Send the fast forward command.""" - await self._remote.async_send_key(Keys.fast_forward) + await self._remote.async_send_key(Keys.FAST_FORWARD) async def async_media_previous_track(self) -> None: """Send the rewind command.""" - await self._remote.async_send_key(Keys.rewind) + await self._remote.async_send_key(Keys.REWIND) async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any diff --git a/requirements_all.txt b/requirements_all.txt index 23d95d7c0b5..e9be441c93f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1528,7 +1528,7 @@ paho-mqtt==1.6.1 panacotta==0.2 # homeassistant.components.panasonic_viera -panasonic-viera==0.3.6 +panasonic-viera==0.4.2 # homeassistant.components.dunehd pdunehd==1.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15a98c97ede..d9ccf11d754 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1228,7 +1228,7 @@ p1monitor==3.0.0 paho-mqtt==1.6.1 # homeassistant.components.panasonic_viera -panasonic-viera==0.3.6 +panasonic-viera==0.4.2 # homeassistant.components.dunehd pdunehd==1.3.2 diff --git a/tests/components/panasonic_viera/test_remote.py b/tests/components/panasonic_viera/test_remote.py index 05254753d3f..3ae241fc5e9 100644 --- a/tests/components/panasonic_viera/test_remote.py +++ b/tests/components/panasonic_viera/test_remote.py @@ -46,7 +46,7 @@ async def test_onoff(hass: HomeAssistant, mock_remote) -> None: await hass.services.async_call(REMOTE_DOMAIN, SERVICE_TURN_ON, data) await hass.async_block_till_done() - power = getattr(Keys.power, "value", Keys.power) + power = getattr(Keys.POWER, "value", Keys.POWER) assert mock_remote.send_key.call_args_list == [call(power), call(power)] From 540da3cac6fef9d2fe691ead9718142129ed5832 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 28 Jun 2024 11:16:13 +0200 Subject: [PATCH 0385/2411] Add unit and state_class to heating sensor in ista EcoTrend (#120728) * Add unit and state_class to heating sensor * remove constant --- homeassistant/components/ista_ecotrend/sensor.py | 2 ++ .../ista_ecotrend/snapshots/test_sensor.ambr | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ista_ecotrend/sensor.py b/homeassistant/components/ista_ecotrend/sensor.py index c50f322c356..3ae2128e142 100644 --- a/homeassistant/components/ista_ecotrend/sensor.py +++ b/homeassistant/components/ista_ecotrend/sensor.py @@ -56,6 +56,8 @@ SENSOR_DESCRIPTIONS: tuple[IstaSensorEntityDescription, ...] = ( translation_key=IstaSensorEntity.HEATING, suggested_display_precision=0, consumption_type=IstaConsumptionType.HEATING, + native_unit_of_measurement="units", + state_class=SensorStateClass.TOTAL, ), IstaSensorEntityDescription( key=IstaSensorEntity.HEATING_ENERGY, diff --git a/tests/components/ista_ecotrend/snapshots/test_sensor.ambr b/tests/components/ista_ecotrend/snapshots/test_sensor.ambr index c312f9b6350..f9ab7a54b63 100644 --- a/tests/components/ista_ecotrend/snapshots/test_sensor.ambr +++ b/tests/components/ista_ecotrend/snapshots/test_sensor.ambr @@ -64,7 +64,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -92,13 +94,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_heating', - 'unit_of_measurement': None, + 'unit_of_measurement': 'units', }) # --- # name: test_setup[sensor.bahnhofsstr_1a_heating-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bahnhofsstr. 1A Heating', + 'state_class': , + 'unit_of_measurement': 'units', }), 'context': , 'entity_id': 'sensor.bahnhofsstr_1a_heating', @@ -491,7 +495,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -519,13 +525,15 @@ 'supported_features': 0, 'translation_key': , 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_heating', - 'unit_of_measurement': None, + 'unit_of_measurement': 'units', }) # --- # name: test_setup[sensor.luxemburger_str_1_heating-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Luxemburger Str. 1 Heating', + 'state_class': , + 'unit_of_measurement': 'units', }), 'context': , 'entity_id': 'sensor.luxemburger_str_1_heating', From 6d93695e2cf8d3823e29e965d8fadfe9d438e717 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:20:17 +0200 Subject: [PATCH 0386/2411] Improve type hints in flux tests (#120733) --- tests/components/flux/test_switch.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index ab85303584f..f957083dd11 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -29,7 +29,7 @@ from tests.components.light.common import MockLight @pytest.fixture(autouse=True) -async def set_utc(hass): +async def set_utc(hass: HomeAssistant) -> None: """Set timezone to UTC.""" await hass.config.async_set_time_zone("UTC") @@ -723,10 +723,8 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day( assert call.data[light.ATTR_XY_COLOR] == [0.439, 0.37] -@pytest.mark.parametrize("x", [0, 1]) async def test_flux_after_sunset_before_midnight_stop_next_day( hass: HomeAssistant, - x, mock_light_entities: list[MockLight], ) -> None: """Test the flux switch after sunset and before stop. From 3d580259e1936d85316216f8b0ac45036464ccf2 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 28 Jun 2024 19:21:59 +1000 Subject: [PATCH 0387/2411] Check Tessie scopes to fix startup bug (#120710) * Add scope check * Add tests * Bump Teslemetry --- .../components/teslemetry/manifest.json | 2 +- homeassistant/components/tessie/__init__.py | 68 +++++++++++-------- homeassistant/components/tessie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tessie/common.py | 11 +++ tests/components/tessie/conftest.py | 11 +++ tests/components/tessie/test_init.py | 14 +++- 8 files changed, 76 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index 2eb3e221855..49d73909a71 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], "quality_scale": "platinum", - "requirements": ["tesla-fleet-api==0.6.1"] + "requirements": ["tesla-fleet-api==0.6.2"] } diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index e8891d6665f..1d6e2a27786 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -6,6 +6,7 @@ import logging from aiohttp import ClientError, ClientResponseError from tesla_fleet_api import EnergySpecific, Tessie +from tesla_fleet_api.const import Scope from tesla_fleet_api.exceptions import TeslaFleetError from tessie_api import get_state_of_all_vehicles @@ -94,41 +95,48 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo # Energy Sites tessie = Tessie(session, api_key) + energysites: list[TessieEnergyData] = [] + try: - products = (await tessie.products())["response"] + scopes = await tessie.scopes() except TeslaFleetError as e: raise ConfigEntryNotReady from e - energysites: list[TessieEnergyData] = [] - for product in products: - if "energy_site_id" in product: - site_id = product["energy_site_id"] - api = EnergySpecific(tessie.energy, site_id) - energysites.append( - TessieEnergyData( - api=api, - id=site_id, - live_coordinator=TessieEnergySiteLiveCoordinator(hass, api), - info_coordinator=TessieEnergySiteInfoCoordinator(hass, api), - device=DeviceInfo( - identifiers={(DOMAIN, str(site_id))}, - manufacturer="Tesla", - name=product.get("site_name", "Energy Site"), - ), - ) - ) + if Scope.ENERGY_DEVICE_DATA in scopes: + try: + products = (await tessie.products())["response"] + except TeslaFleetError as e: + raise ConfigEntryNotReady from e - # Populate coordinator data before forwarding to platforms - await asyncio.gather( - *( - energysite.live_coordinator.async_config_entry_first_refresh() - for energysite in energysites - ), - *( - energysite.info_coordinator.async_config_entry_first_refresh() - for energysite in energysites - ), - ) + for product in products: + if "energy_site_id" in product: + site_id = product["energy_site_id"] + api = EnergySpecific(tessie.energy, site_id) + energysites.append( + TessieEnergyData( + api=api, + id=site_id, + live_coordinator=TessieEnergySiteLiveCoordinator(hass, api), + info_coordinator=TessieEnergySiteInfoCoordinator(hass, api), + device=DeviceInfo( + identifiers={(DOMAIN, str(site_id))}, + manufacturer="Tesla", + name=product.get("site_name", "Energy Site"), + ), + ) + ) + + # Populate coordinator data before forwarding to platforms + await asyncio.gather( + *( + energysite.live_coordinator.async_config_entry_first_refresh() + for energysite in energysites + ), + *( + energysite.info_coordinator.async_config_entry_first_refresh() + for energysite in energysites + ), + ) entry.runtime_data = TessieData(vehicles, energysites) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json index bf1ab5f61e4..493feeaa77e 100644 --- a/homeassistant/components/tessie/manifest.json +++ b/homeassistant/components/tessie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tessie", "iot_class": "cloud_polling", "loggers": ["tessie"], - "requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.1"] + "requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index e9be441c93f..f88887a732d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2711,7 +2711,7 @@ temperusb==1.6.1 # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==0.6.1 +tesla-fleet-api==0.6.2 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9ccf11d754..9ab639908a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2109,7 +2109,7 @@ temperusb==1.6.1 # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==0.6.1 +tesla-fleet-api==0.6.2 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index 3d24c6b233a..37a38fffaa4 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -54,6 +54,17 @@ LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN) SITE_INFO = load_json_object_fixture("site_info.json", DOMAIN) RESPONSE_OK = {"response": {}, "error": None} COMMAND_OK = {"response": {"result": True, "reason": ""}} +SCOPES = [ + "user_data", + "vehicle_device_data", + "vehicle_cmds", + "vehicle_charging_cmds", + "energy_device_data", + "energy_cmds", + "offline_access", + "openid", +] +NO_SCOPES = ["user_data", "offline_access", "openid"] async def setup_platform( diff --git a/tests/components/tessie/conftest.py b/tests/components/tessie/conftest.py index 79cc9aa44c6..e0aba73af17 100644 --- a/tests/components/tessie/conftest.py +++ b/tests/components/tessie/conftest.py @@ -11,6 +11,7 @@ from .common import ( COMMAND_OK, LIVE_STATUS, PRODUCTS, + SCOPES, SITE_INFO, TEST_STATE_OF_ALL_VEHICLES, TEST_VEHICLE_STATE_ONLINE, @@ -51,6 +52,16 @@ def mock_get_state_of_all_vehicles(): # Fleet API +@pytest.fixture(autouse=True) +def mock_scopes(): + """Mock scopes function.""" + with patch( + "homeassistant.components.tessie.Tessie.scopes", + return_value=SCOPES, + ) as mock_scopes: + yield mock_scopes + + @pytest.fixture(autouse=True) def mock_products(): """Mock Tesla Fleet Api products method.""" diff --git a/tests/components/tessie/test_init.py b/tests/components/tessie/test_init.py index e37512ea8c4..921ef93b1ae 100644 --- a/tests/components/tessie/test_init.py +++ b/tests/components/tessie/test_init.py @@ -50,11 +50,21 @@ async def test_connection_failure( assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_fleet_error(hass: HomeAssistant) -> None: - """Test init with a fleet error.""" +async def test_products_error(hass: HomeAssistant) -> None: + """Test init with a fleet error on products.""" with patch( "homeassistant.components.tessie.Tessie.products", side_effect=TeslaFleetError ): entry = await setup_platform(hass) assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_scopes_error(hass: HomeAssistant) -> None: + """Test init with a fleet error on scopes.""" + + with patch( + "homeassistant.components.tessie.Tessie.scopes", side_effect=TeslaFleetError + ): + entry = await setup_platform(hass) + assert entry.state is ConfigEntryState.SETUP_RETRY From 0fdf037ba03c223677d4c8a3e7b5f163f8810cd2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 11:56:49 +0200 Subject: [PATCH 0388/2411] Fix ruff type comparison E721 (#120731) Fix E721 --- homeassistant/components/insteon/api/properties.py | 6 +++--- tests/auth/mfa_modules/test_insecure_example.py | 2 +- tests/auth/mfa_modules/test_notify.py | 4 ++-- tests/auth/mfa_modules/test_totp.py | 2 +- tests/components/number/test_init.py | 2 +- tests/components/plaato/test_config_flow.py | 6 +++--- tests/components/python_script/test_init.py | 6 +++--- tests/components/rtsp_to_webrtc/test_config_flow.py | 4 ++-- tests/components/sensor/test_init.py | 4 ++-- tests/components/zwave_js/test_helpers.py | 2 +- 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/insteon/api/properties.py b/homeassistant/components/insteon/api/properties.py index 20e798dded0..4d36f1d71e5 100644 --- a/homeassistant/components/insteon/api/properties.py +++ b/homeassistant/components/insteon/api/properties.py @@ -85,11 +85,11 @@ def get_schema(prop, name, groups): if name == LOAD_BUTTON: button_list = {group: groups[group].name for group in groups} return _list_schema(name, button_list) - if prop.value_type == bool: + if prop.value_type is bool: return _bool_schema(name) - if prop.value_type == int: + if prop.value_type is int: return _byte_schema(name) - if prop.value_type == float: + if prop.value_type is float: return _float_schema(name) if prop.value_type == ToggleMode: return _list_schema(name, TOGGLE_MODES) diff --git a/tests/auth/mfa_modules/test_insecure_example.py b/tests/auth/mfa_modules/test_insecure_example.py index f7f8a327059..8caca780ecb 100644 --- a/tests/auth/mfa_modules/test_insecure_example.py +++ b/tests/auth/mfa_modules/test_insecure_example.py @@ -121,7 +121,7 @@ async def test_login(hass: HomeAssistant) -> None: ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" - assert result["data_schema"].schema.get("pin") == str + assert result["data_schema"].schema.get("pin") is str result = await hass.auth.login_flow.async_configure( result["flow_id"], {"pin": "invalid-code"} diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index 23b8811dbf9..d6f4d80f99e 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -155,7 +155,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None: ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" - assert result["data_schema"].schema.get("code") == str + assert result["data_schema"].schema.get("code") is str # wait service call finished await hass.async_block_till_done() @@ -214,7 +214,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None: ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" - assert result["data_schema"].schema.get("code") == str + assert result["data_schema"].schema.get("code") is str # wait service call finished await hass.async_block_till_done() diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index 961db3f44ca..fadc3214712 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -114,7 +114,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None: ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" - assert result["data_schema"].schema.get("code") == str + assert result["data_schema"].schema.get("code") is str with patch("pyotp.TOTP.verify", return_value=False): result = await hass.auth.login_flow.async_configure( diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 6f74a3126c0..aa5df5d737f 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -646,7 +646,7 @@ async def test_restore_number_restore_state( assert entity0.native_min_value == native_min_value assert entity0.native_step == native_step assert entity0.native_value == native_value - assert type(entity0.native_value) == native_value_type + assert type(entity0.native_value) is native_value_type assert entity0.native_unit_of_measurement == uom diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py index efda354f20d..ceadab7f832 100644 --- a/tests/components/plaato/test_config_flow.py +++ b/tests/components/plaato/test_config_flow.py @@ -64,8 +64,8 @@ async def test_show_config_form_device_type_airlock(hass: HomeAssistant) -> None assert result["type"] is 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 + assert result["data_schema"].schema.get(CONF_TOKEN) is str + assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) is bool async def test_show_config_form_device_type_keg(hass: HomeAssistant) -> None: @@ -78,7 +78,7 @@ async def test_show_config_form_device_type_keg(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" - assert result["data_schema"].schema.get(CONF_TOKEN) == str + assert result["data_schema"].schema.get(CONF_TOKEN) is str assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) is None diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index 03fa73f076e..c4dc00c448a 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -155,7 +155,7 @@ raise Exception('boom') task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True) await hass.async_block_till_done(wait_background_tasks=True) - assert type(task.exception()) == HomeAssistantError + assert type(task.exception()) is HomeAssistantError assert "Error executing script (Exception): boom" in str(task.exception()) @@ -183,7 +183,7 @@ hass.async_stop() task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True) await hass.async_block_till_done(wait_background_tasks=True) - assert type(task.exception()) == ServiceValidationError + assert type(task.exception()) is ServiceValidationError assert "Not allowed to access async methods" in str(task.exception()) @@ -233,7 +233,7 @@ async def test_accessing_forbidden_methods_with_response(hass: HomeAssistant) -> task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True) await hass.async_block_till_done(wait_background_tasks=True) - assert type(task.exception()) == ServiceValidationError + assert type(task.exception()) is ServiceValidationError assert f"Not allowed to access {name}" in str(task.exception()) diff --git a/tests/components/rtsp_to_webrtc/test_config_flow.py b/tests/components/rtsp_to_webrtc/test_config_flow.py index 504ede68ac7..5daf9400396 100644 --- a/tests/components/rtsp_to_webrtc/test_config_flow.py +++ b/tests/components/rtsp_to_webrtc/test_config_flow.py @@ -25,7 +25,7 @@ async def test_web_full_flow(hass: HomeAssistant) -> None: ) assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" - assert result.get("data_schema").schema.get("server_url") == str + assert result.get("data_schema").schema.get("server_url") is str assert not result.get("errors") with ( patch("rtsp_to_webrtc.client.Client.heartbeat"), @@ -64,7 +64,7 @@ async def test_invalid_url(hass: HomeAssistant) -> None: ) assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" - assert result.get("data_schema").schema.get("server_url") == str + assert result.get("data_schema").schema.get("server_url") is str assert not result.get("errors") result = await hass.config_entries.flow.async_configure( result["flow_id"], {"server_url": "not-a-url"} diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 126e327f364..689a91f770d 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -418,7 +418,7 @@ async def test_restore_sensor_save_state( assert state["entity_id"] == entity0.entity_id extra_data = hass_storage[RESTORE_STATE_KEY]["data"][0]["extra_data"] assert extra_data == expected_extra_data - assert type(extra_data["native_value"]) == native_value_type + assert type(extra_data["native_value"]) is native_value_type @pytest.mark.parametrize( @@ -479,7 +479,7 @@ async def test_restore_sensor_restore_state( assert hass.states.get(entity0.entity_id) assert entity0.native_value == native_value - assert type(entity0.native_value) == native_value_type + assert type(entity0.native_value) is native_value_type assert entity0.native_unit_of_measurement == uom diff --git a/tests/components/zwave_js/test_helpers.py b/tests/components/zwave_js/test_helpers.py index 016a2d718ac..2df2e134f49 100644 --- a/tests/components/zwave_js/test_helpers.py +++ b/tests/components/zwave_js/test_helpers.py @@ -42,4 +42,4 @@ async def test_get_value_state_schema_boolean_config_value( aeon_smart_switch_6.values["102-112-0-255"] ) assert isinstance(schema_validator, vol.Coerce) - assert schema_validator.type == bool + assert schema_validator.type is bool From 6f8c9c28e7eace8026efd19d4b0c753ca65eb0e3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 12:03:26 +0200 Subject: [PATCH 0389/2411] Improve type hints in fjaraskupan tests (#120732) --- tests/components/fjaraskupan/test_config_flow.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/fjaraskupan/test_config_flow.py b/tests/components/fjaraskupan/test_config_flow.py index fa0df9241dd..886e01c8966 100644 --- a/tests/components/fjaraskupan/test_config_flow.py +++ b/tests/components/fjaraskupan/test_config_flow.py @@ -2,9 +2,10 @@ from __future__ import annotations -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest +from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.fjaraskupan.const import DOMAIN @@ -15,7 +16,7 @@ from . import COOKER_SERVICE_INFO @pytest.fixture(name="mock_setup_entry", autouse=True) -async def fixture_mock_setup_entry(hass): +def fixture_mock_setup_entry() -> Generator[AsyncMock]: """Fixture for config entry.""" with patch( @@ -24,7 +25,7 @@ async def fixture_mock_setup_entry(hass): yield mock_setup_entry -async def test_configure(hass: HomeAssistant, mock_setup_entry) -> None: +async def test_configure(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: """Test we get the form.""" with patch( "homeassistant.components.fjaraskupan.config_flow.async_discovered_service_info", From c13786c95217279bde11f1768bd9b74f66de3a58 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 12:14:24 +0200 Subject: [PATCH 0390/2411] Fix ruff manual-dict-comprehension PERF403 in tests (#120738) Fix PERF403 in tests --- tests/components/recorder/test_init.py | 38 +++++++++++++++----------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 52947ce0c19..071324e4b6a 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -905,16 +905,19 @@ async def test_saving_event_with_oversized_data( hass.bus.async_fire("test_event", event_data) hass.bus.async_fire("test_event_too_big", massive_dict) await async_wait_recording_done(hass) - events = {} with session_scope(hass=hass, read_only=True) as session: - for _, data, event_type in ( - session.query(Events.event_id, EventData.shared_data, EventTypes.event_type) - .outerjoin(EventData, Events.data_id == EventData.data_id) - .outerjoin(EventTypes, Events.event_type_id == EventTypes.event_type_id) - .where(EventTypes.event_type.in_(["test_event", "test_event_too_big"])) - ): - events[event_type] = data + events = { + event_type: data + for _, data, event_type in ( + session.query( + Events.event_id, EventData.shared_data, EventTypes.event_type + ) + .outerjoin(EventData, Events.data_id == EventData.data_id) + .outerjoin(EventTypes, Events.event_type_id == EventTypes.event_type_id) + .where(EventTypes.event_type.in_(["test_event", "test_event_too_big"])) + ) + } assert "test_event_too_big" in caplog.text @@ -932,16 +935,19 @@ async def test_saving_event_invalid_context_ulid( event_data = {"test_attr": 5, "test_attr_10": "nice"} hass.bus.async_fire("test_event", event_data, context=Context(id="invalid")) await async_wait_recording_done(hass) - events = {} with session_scope(hass=hass, read_only=True) as session: - for _, data, event_type in ( - session.query(Events.event_id, EventData.shared_data, EventTypes.event_type) - .outerjoin(EventData, Events.data_id == EventData.data_id) - .outerjoin(EventTypes, Events.event_type_id == EventTypes.event_type_id) - .where(EventTypes.event_type.in_(["test_event"])) - ): - events[event_type] = data + events = { + event_type: data + for _, data, event_type in ( + session.query( + Events.event_id, EventData.shared_data, EventTypes.event_type + ) + .outerjoin(EventData, Events.data_id == EventData.data_id) + .outerjoin(EventTypes, Events.event_type_id == EventTypes.event_type_id) + .where(EventTypes.event_type.in_(["test_event"])) + ) + } assert "invalid" in caplog.text From 23e5e251494210862f546d8bcd9efcbc9e4e797b Mon Sep 17 00:00:00 2001 From: Illia <146177275+ikalnyi@users.noreply.github.com> Date: Fri, 28 Jun 2024 12:14:44 +0200 Subject: [PATCH 0391/2411] Bump asyncarve to 0.1.1 (#120740) --- homeassistant/components/arve/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/arve/manifest.json b/homeassistant/components/arve/manifest.json index fa33b3309ce..4c63d377371 100644 --- a/homeassistant/components/arve/manifest.json +++ b/homeassistant/components/arve/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/arve", "iot_class": "cloud_polling", - "requirements": ["asyncarve==0.0.9"] + "requirements": ["asyncarve==0.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index f88887a732d..9df65cad5fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ asterisk_mbox==0.5.0 async-upnp-client==0.39.0 # homeassistant.components.arve -asyncarve==0.0.9 +asyncarve==0.1.1 # homeassistant.components.keyboard_remote asyncinotify==4.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ab639908a6..f7b3249bcaf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ asterisk_mbox==0.5.0 async-upnp-client==0.39.0 # homeassistant.components.arve -asyncarve==0.0.9 +asyncarve==0.1.1 # homeassistant.components.sleepiq asyncsleepiq==1.5.2 From 4437c4a204f19718fc7cb468cf6de42ddac6c2b0 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Fri, 28 Jun 2024 07:22:24 -0300 Subject: [PATCH 0392/2411] Link the Statistics helper entity to the source entity device (#120705) --- .../components/statistics/__init__.py | 11 ++- homeassistant/components/statistics/sensor.py | 8 ++ tests/components/statistics/test_init.py | 92 +++++++++++++++++++ tests/components/statistics/test_sensor.py | 49 +++++++++- 4 files changed, 158 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/__init__.py b/homeassistant/components/statistics/__init__.py index 70739c618f7..f71274e0ee7 100644 --- a/homeassistant/components/statistics/__init__.py +++ b/homeassistant/components/statistics/__init__.py @@ -1,8 +1,11 @@ """The statistics component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.device import ( + async_remove_stale_devices_links_keep_entity_device, +) DOMAIN = "statistics" PLATFORMS = [Platform.SENSOR] @@ -11,6 +14,12 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Statistics from a config entry.""" + async_remove_stale_devices_links_keep_entity_device( + hass, + entry.entry_id, + entry.options[CONF_ENTITY_ID], + ) + 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/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 8d28254ad61..ca1d75b57ed 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -43,6 +43,7 @@ from homeassistant.core import ( split_entity_id, ) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_track_point_in_utc_time, @@ -268,6 +269,7 @@ async def async_setup_platform( async_add_entities( new_entities=[ StatisticsSensor( + hass=hass, source_entity_id=config[CONF_ENTITY_ID], name=config[CONF_NAME], unique_id=config.get(CONF_UNIQUE_ID), @@ -304,6 +306,7 @@ async def async_setup_entry( async_add_entities( [ StatisticsSensor( + hass=hass, source_entity_id=entry.options[CONF_ENTITY_ID], name=entry.options[CONF_NAME], unique_id=entry.entry_id, @@ -327,6 +330,7 @@ class StatisticsSensor(SensorEntity): def __init__( self, + hass: HomeAssistant, source_entity_id: str, name: str, unique_id: str | None, @@ -341,6 +345,10 @@ class StatisticsSensor(SensorEntity): self._attr_name: str = name self._attr_unique_id: str | None = unique_id self._source_entity_id: str = source_entity_id + self._attr_device_info = async_device_info_to_link_from_entity( + hass, + source_entity_id, + ) self.is_binary: bool = ( split_entity_id(self._source_entity_id)[0] == BINARY_SENSOR_DOMAIN ) diff --git a/tests/components/statistics/test_init.py b/tests/components/statistics/test_init.py index 6cb943c0687..64829ea7d66 100644 --- a/tests/components/statistics/test_init.py +++ b/tests/components/statistics/test_init.py @@ -2,8 +2,10 @@ from __future__ import annotations +from homeassistant.components.statistics import DOMAIN as STATISTICS_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -15,3 +17,93 @@ async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) assert await hass.config_entries.async_unload(loaded_entry.entry_id) await hass.async_block_till_done() assert loaded_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_device_cleaning( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test the cleaning of devices linked to the helper Statistics.""" + + # Source entity device config entry + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + + # Device entry of the source entity + source_device1_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test1")}, + connections={("mac", "30:31:32:33:34:01")}, + ) + + # Source entity registry + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device1_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + # Configure the configuration entry for Statistics + statistics_config_entry = MockConfigEntry( + data={}, + domain=STATISTICS_DOMAIN, + options={ + "name": "Statistics", + "entity_id": "sensor.test_source", + "state_characteristic": "mean", + "keep_last_sample": False, + "percentile": 50.0, + "precision": 2.0, + "sampling_size": 20.0, + }, + title="Statistics", + ) + statistics_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the statistics sensor + statistics_entity = entity_registry.async_get("sensor.statistics") + assert statistics_entity is not None + assert statistics_entity.device_id == source_entity.device_id + + # Device entry incorrectly linked to Statistics config entry + device_registry.async_get_or_create( + config_entry_id=statistics_config_entry.entry_id, + identifiers={("sensor", "identifier_test2")}, + connections={("mac", "30:31:32:33:34:02")}, + ) + device_registry.async_get_or_create( + config_entry_id=statistics_config_entry.entry_id, + identifiers={("sensor", "identifier_test3")}, + connections={("mac", "30:31:32:33:34:03")}, + ) + await hass.async_block_till_done() + + # Before reloading the config entry, two devices are expected to be linked + devices_before_reload = device_registry.devices.get_devices_for_config_entry_id( + statistics_config_entry.entry_id + ) + assert len(devices_before_reload) == 3 + + # Config entry reload + await hass.config_entries.async_reload(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the statistics sensor + statistics_entity = entity_registry.async_get("sensor.statistics") + assert statistics_entity is not None + assert statistics_entity.device_id == source_entity.device_id + + # After reloading the config entry, only one linked device is expected + devices_after_reload = device_registry.devices.get_devices_for_config_entry_id( + statistics_config_entry.entry_id + ) + assert len(devices_after_reload) == 1 + + assert devices_after_reload[0].id == source_device1_entry.id diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 269c17e34b9..c90d685714c 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -41,7 +41,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -1654,3 +1654,50 @@ async def test_reload(recorder_mock: Recorder, hass: HomeAssistant) -> None: assert hass.states.get("sensor.test") is None assert hass.states.get("sensor.cputest") + + +async def test_device_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test for source entity device for Statistics.""" + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + source_device_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + statistics_config_entry = MockConfigEntry( + data={}, + domain=STATISTICS_DOMAIN, + options={ + "name": "Statistics", + "entity_id": "sensor.test_source", + "state_characteristic": "mean", + "keep_last_sample": False, + "percentile": 50.0, + "precision": 2.0, + "sampling_size": 20.0, + }, + title="Statistics", + ) + statistics_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + statistics_entity = entity_registry.async_get("sensor.statistics") + assert statistics_entity is not None + assert statistics_entity.device_id == source_entity.device_id From 6ef8e87f88cf1dc700f802d959207d54c59725a7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 12:31:07 +0200 Subject: [PATCH 0393/2411] Fix ruff redefined-argument-from-local PLR1704 (#120729) * Fix PLR1704 * Fix --- .../components/automation/__init__.py | 19 ++++++++++-------- homeassistant/components/elkm1/discovery.py | 10 ++++++---- .../components/flux_led/discovery.py | 12 +++++++---- homeassistant/components/habitica/__init__.py | 20 ++++++++++--------- .../components/hardware/websocket_api.py | 4 ++-- homeassistant/components/script/__init__.py | 4 ++-- .../components/steamist/discovery.py | 10 ++++++---- .../components/unifi/hub/entity_helper.py | 4 ++-- homeassistant/components/velbus/__init__.py | 6 +++--- homeassistant/helpers/collection.py | 4 ++-- .../test_passive_update_processor.py | 12 +++++------ 11 files changed, 59 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 5a53179cf2c..f2ef404ab34 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1048,29 +1048,32 @@ async def _async_process_config( automation_configs_with_id: dict[str, tuple[int, AutomationEntityConfig]] = {} automation_configs_without_id: list[tuple[int, AutomationEntityConfig]] = [] - for config_idx, config in enumerate(automation_configs): - if automation_id := config.config_block.get(CONF_ID): - automation_configs_with_id[automation_id] = (config_idx, config) + for config_idx, automation_config in enumerate(automation_configs): + if automation_id := automation_config.config_block.get(CONF_ID): + automation_configs_with_id[automation_id] = ( + config_idx, + automation_config, + ) continue - automation_configs_without_id.append((config_idx, config)) + automation_configs_without_id.append((config_idx, automation_config)) for automation_idx, automation in enumerate(automations): if automation.unique_id: if automation.unique_id not in automation_configs_with_id: continue - config_idx, config = automation_configs_with_id.pop( + config_idx, automation_config = automation_configs_with_id.pop( automation.unique_id ) - if automation_matches_config(automation, config): + if automation_matches_config(automation, automation_config): automation_matches.add(automation_idx) config_matches.add(config_idx) continue - for config_idx, config in automation_configs_without_id: + for config_idx, automation_config in automation_configs_without_id: if config_idx in config_matches: # Only allow an automation config to match at most once continue - if automation_matches_config(automation, config): + if automation_matches_config(automation, automation_config): automation_matches.add(automation_idx) config_matches.add(config_idx) # Only allow an automation to match at most once diff --git a/homeassistant/components/elkm1/discovery.py b/homeassistant/components/elkm1/discovery.py index 8a68b6524b7..916e8a8aeac 100644 --- a/homeassistant/components/elkm1/discovery.py +++ b/homeassistant/components/elkm1/discovery.py @@ -46,8 +46,10 @@ async def async_discover_devices( targets = [address] else: targets = [ - str(address) - for address in await network.async_get_ipv4_broadcast_addresses(hass) + str(broadcast_address) + for broadcast_address in await network.async_get_ipv4_broadcast_addresses( + hass + ) ] scanner = AIOELKDiscovery() @@ -55,8 +57,8 @@ async def async_discover_devices( for idx, discovered in enumerate( await asyncio.gather( *[ - scanner.async_scan(timeout=timeout, address=address) - for address in targets + scanner.async_scan(timeout=timeout, address=target_address) + for target_address in targets ], return_exceptions=True, ) diff --git a/homeassistant/components/flux_led/discovery.py b/homeassistant/components/flux_led/discovery.py index 9600f773701..d55f560193f 100644 --- a/homeassistant/components/flux_led/discovery.py +++ b/homeassistant/components/flux_led/discovery.py @@ -178,16 +178,20 @@ async def async_discover_devices( targets = [address] else: targets = [ - str(address) - for address in await network.async_get_ipv4_broadcast_addresses(hass) + str(broadcast_address) + for broadcast_address in await network.async_get_ipv4_broadcast_addresses( + hass + ) ] scanner = AIOBulbScanner() for idx, discovered in enumerate( await asyncio.gather( *[ - create_eager_task(scanner.async_scan(timeout=timeout, address=address)) - for address in targets + create_eager_task( + scanner.async_scan(timeout=timeout, address=target_address) + ) + for target_address in targets ], return_exceptions=True, ) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index e8c0af8f97f..b50e5855f00 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -110,7 +110,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: HabiticaConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, config_entry: HabiticaConfigEntry +) -> bool: """Set up habitica from a config entry.""" class HAHabitipyAsync(HabitipyAsync): @@ -147,9 +149,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: HabiticaConfigEntry) -> websession = async_get_clientsession(hass) - url = entry.data[CONF_URL] - username = entry.data[CONF_API_USER] - password = entry.data[CONF_API_KEY] + url = config_entry.data[CONF_URL] + username = config_entry.data[CONF_API_USER] + password = config_entry.data[CONF_API_KEY] api = await hass.async_add_executor_job( HAHabitipyAsync, @@ -169,18 +171,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: HabiticaConfigEntry) -> ) from e raise ConfigEntryNotReady(e) from e - if not entry.data.get(CONF_NAME): + if not config_entry.data.get(CONF_NAME): name = user["profile"]["name"] hass.config_entries.async_update_entry( - entry, - data={**entry.data, CONF_NAME: name}, + config_entry, + data={**config_entry.data, CONF_NAME: name}, ) coordinator = HabiticaDataUpdateCoordinator(hass, api) await coordinator.async_config_entry_first_refresh() - entry.runtime_data = coordinator - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + config_entry.runtime_data = coordinator + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) if not hass.services.has_service(DOMAIN, SERVICE_API_CALL): hass.services.async_register( diff --git a/homeassistant/components/hardware/websocket_api.py b/homeassistant/components/hardware/websocket_api.py index 694c6e782e1..dfbcfd4c4ac 100644 --- a/homeassistant/components/hardware/websocket_api.py +++ b/homeassistant/components/hardware/websocket_api.py @@ -99,8 +99,8 @@ def ws_subscribe_system_status( "memory_free_mb": round(virtual_memory.available / 1024**2, 1), "timestamp": dt_util.utcnow().isoformat(), } - for connection, msg_id in system_status.subscribers: - connection.send_message(websocket_api.event_message(msg_id, json_msg)) + for conn, msg_id in system_status.subscribers: + conn.send_message(websocket_api.event_message(msg_id, json_msg)) if not system_status.subscribers: system_status.remove_periodic_timer = async_track_time_interval( diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index f19a48fea33..2b9f87f368b 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -369,11 +369,11 @@ async def _async_process_config( config_matches: set[int] = set() for script_idx, script in enumerate(scripts): - for config_idx, config in enumerate(script_configs): + for config_idx, script_config in enumerate(script_configs): if config_idx in config_matches: # Only allow a script config to match at most once continue - if script_matches_config(script, config): + if script_matches_config(script, script_config): script_matches.add(script_idx) config_matches.add(config_idx) # Only allow a script to match at most once diff --git a/homeassistant/components/steamist/discovery.py b/homeassistant/components/steamist/discovery.py index 5c3262ce4eb..2abe2343f99 100644 --- a/homeassistant/components/steamist/discovery.py +++ b/homeassistant/components/steamist/discovery.py @@ -62,16 +62,18 @@ async def async_discover_devices( targets = [address] else: targets = [ - str(address) - for address in await network.async_get_ipv4_broadcast_addresses(hass) + str(broadcast_address) + for broadcast_address in await network.async_get_ipv4_broadcast_addresses( + hass + ) ] scanner = AIODiscovery30303() for idx, discovered in enumerate( await asyncio.gather( *[ - scanner.async_scan(timeout=timeout, address=address) - for address in targets + scanner.async_scan(timeout=timeout, address=target_address) + for target_address in targets ], return_exceptions=True, ) diff --git a/homeassistant/components/unifi/hub/entity_helper.py b/homeassistant/components/unifi/hub/entity_helper.py index c4bcf237386..782b026d6e4 100644 --- a/homeassistant/components/unifi/hub/entity_helper.py +++ b/homeassistant/components/unifi/hub/entity_helper.py @@ -146,8 +146,8 @@ class UnifiDeviceCommand: """Execute previously queued commands.""" queue = self._command_queue.copy() self._command_queue.clear() - for device_id, device_commands in queue.items(): - device = self.api.devices[device_id] + for dev_id, device_commands in queue.items(): + device = self.api.devices[dev_id] commands = list(device_commands.items()) await self.api.request( DeviceSetPoePortModeRequest.create(device, targets=commands) diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 479b7f02024..d47444e3994 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -89,9 +89,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True def check_entry_id(interface: str) -> str: - for entry in hass.config_entries.async_entries(DOMAIN): - if "port" in entry.data and entry.data["port"] == interface: - return entry.entry_id + for config_entry in hass.config_entries.async_entries(DOMAIN): + if "port" in config_entry.data and config_entry.data["port"] == interface: + return config_entry.entry_id raise vol.Invalid( "The interface provided is not defined as a port in a Velbus integration" ) diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 036aaacf0e9..9151a9dfc6b 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -642,8 +642,8 @@ class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]: } for change in change_set ] - for connection, msg_id in self._subscribers: - connection.send_message(websocket_api.event_message(msg_id, json_msg)) + for conn, msg_id in self._subscribers: + conn.send_message(websocket_api.event_message(msg_id, json_msg)) if not self._subscribers: self._remove_subscription = ( diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 8e1163c0bdb..079ac2200fc 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -1653,12 +1653,12 @@ async def test_integration_multiple_entity_platforms_with_reload_and_restart( unregister_binary_sensor_processor() unregister_sensor_processor() - async with async_test_home_assistant() as hass: - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + async with async_test_home_assistant() as test_hass: + await async_setup_component(test_hass, DOMAIN, {DOMAIN: {}}) current_entry.set(entry) coordinator = PassiveBluetoothProcessorCoordinator( - hass, + test_hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE, @@ -1706,7 +1706,7 @@ async def test_integration_multiple_entity_platforms_with_reload_and_restart( ] sensor_entity_one: PassiveBluetoothProcessorEntity = sensor_entities[0] - sensor_entity_one.hass = hass + sensor_entity_one.hass = test_hass assert sensor_entity_one.available is False # service data not injected assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" assert sensor_entity_one.device_info == { @@ -1723,7 +1723,7 @@ async def test_integration_multiple_entity_platforms_with_reload_and_restart( binary_sensor_entity_one: PassiveBluetoothProcessorEntity = ( binary_sensor_entities[0] ) - binary_sensor_entity_one.hass = hass + binary_sensor_entity_one.hass = test_hass assert binary_sensor_entity_one.available is False # service data not injected assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" assert binary_sensor_entity_one.device_info == { @@ -1739,7 +1739,7 @@ async def test_integration_multiple_entity_platforms_with_reload_and_restart( cancel_coordinator() unregister_binary_sensor_processor() unregister_sensor_processor() - await hass.async_stop() + await test_hass.async_stop() NAMING_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( From c385deb6a37d70958c65993adfccb0e3d5cbc913 Mon Sep 17 00:00:00 2001 From: Ivan Belokobylskiy Date: Fri, 28 Jun 2024 15:25:23 +0400 Subject: [PATCH 0394/2411] Bump aiomaps with fixed license classifier (#120654) --- homeassistant/components/yandex_transport/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index 703f81d2823..c29b4d3dc98 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@rishatik92", "@devbis"], "documentation": "https://www.home-assistant.io/integrations/yandex_transport", "iot_class": "cloud_polling", - "requirements": ["aioymaps==1.2.2"] + "requirements": ["aioymaps==1.2.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9df65cad5fa..3ae1b3556fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -410,7 +410,7 @@ aiowebostv==0.4.0 aiowithings==3.0.1 # homeassistant.components.yandex_transport -aioymaps==1.2.2 +aioymaps==1.2.4 # homeassistant.components.airgradient airgradient==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7b3249bcaf..cfa58855981 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -383,7 +383,7 @@ aiowebostv==0.4.0 aiowithings==3.0.1 # homeassistant.components.yandex_transport -aioymaps==1.2.2 +aioymaps==1.2.4 # homeassistant.components.airgradient airgradient==0.6.0 From d2a457c24f24bfcf10bfc1a9c2cdc3fbc91ad90e Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 28 Jun 2024 04:25:55 -0700 Subject: [PATCH 0395/2411] Fix Google Generative AI: 400 Request contains an invalid argument (#120741) --- .../conversation.py | 9 +- .../snapshots/test_conversation.ambr | 166 ++++++++++++++++++ .../test_conversation.py | 83 +++++++++ 3 files changed, 255 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index fb7f5c3b21c..8052ee66f40 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -95,9 +95,12 @@ def _format_tool( ) -> dict[str, Any]: """Format tool specification.""" - parameters = _format_schema( - convert(tool.parameters, custom_serializer=custom_serializer) - ) + if tool.parameters.schema: + parameters = _format_schema( + convert(tool.parameters, custom_serializer=custom_serializer) + ) + else: + parameters = None return protos.Tool( { diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index b0a0ce967de..7f28c172970 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -409,3 +409,169 @@ ), ]) # --- +# name: test_function_call + list([ + tuple( + '', + tuple( + ), + dict({ + 'generation_config': dict({ + 'max_output_tokens': 150, + 'temperature': 1.0, + 'top_k': 64, + 'top_p': 0.95, + }), + 'model_name': 'models/gemini-1.5-flash-latest', + 'safety_settings': dict({ + 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', + 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', + 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', + 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', + }), + 'system_instruction': ''' + Current time is 05:00:00. Today's date is 2024-05-24. + You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. + Answer in plain text. Keep it simple and to the point. + Only if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant. + ''', + 'tools': list([ + function_declarations { + name: "test_tool" + description: "Test function" + parameters { + type_: OBJECT + properties { + key: "param1" + value { + type_: ARRAY + description: "Test parameters" + items { + type_: STRING + format_: "lower" + } + } + } + } + } + , + ]), + }), + ), + tuple( + '().start_chat', + tuple( + ), + dict({ + 'history': list([ + ]), + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + 'Please call the test function', + ), + dict({ + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + parts { + function_response { + name: "test_tool" + response { + fields { + key: "result" + value { + string_value: "Test response" + } + } + } + } + } + , + ), + dict({ + }), + ), + ]) +# --- +# name: test_function_call_without_parameters + list([ + tuple( + '', + tuple( + ), + dict({ + 'generation_config': dict({ + 'max_output_tokens': 150, + 'temperature': 1.0, + 'top_k': 64, + 'top_p': 0.95, + }), + 'model_name': 'models/gemini-1.5-flash-latest', + 'safety_settings': dict({ + 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', + 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', + 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', + 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', + }), + 'system_instruction': ''' + Current time is 05:00:00. Today's date is 2024-05-24. + You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. + Answer in plain text. Keep it simple and to the point. + Only if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant. + ''', + 'tools': list([ + function_declarations { + name: "test_tool" + description: "Test function" + } + , + ]), + }), + ), + tuple( + '().start_chat', + tuple( + ), + dict({ + 'history': list([ + ]), + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + 'Please call the test function', + ), + dict({ + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + parts { + function_response { + name: "test_tool" + response { + fields { + key: "result" + value { + string_value: "Test response" + } + } + } + } + } + , + ), + dict({ + }), + ), + ]) +# --- diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 990058aa89d..30016335f3b 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -172,6 +172,7 @@ async def test_function_call( hass: HomeAssistant, mock_config_entry_with_assist: MockConfigEntry, mock_init_component, + snapshot: SnapshotAssertion, ) -> None: """Test function calling.""" agent_id = mock_config_entry_with_assist.entry_id @@ -256,6 +257,7 @@ async def test_function_call( device_id="test_device", ), ) + assert [tuple(mock_call) for mock_call in mock_model.mock_calls] == snapshot # Test conversating tracing traces = trace.async_get_traces() @@ -272,6 +274,87 @@ async def test_function_call( assert "Answer in plain text" in detail_event["data"]["prompt"] +@patch( + "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" +) +async def test_function_call_without_parameters( + mock_get_tools, + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_init_component, + snapshot: SnapshotAssertion, +) -> None: + """Test function calling without parameters.""" + agent_id = mock_config_entry_with_assist.entry_id + context = Context() + + mock_tool = AsyncMock() + mock_tool.name = "test_tool" + mock_tool.description = "Test function" + mock_tool.parameters = vol.Schema({}) + + mock_get_tools.return_value = [mock_tool] + + with patch("google.generativeai.GenerativeModel") as mock_model: + mock_chat = AsyncMock() + mock_model.return_value.start_chat.return_value = mock_chat + chat_response = MagicMock() + mock_chat.send_message_async.return_value = chat_response + mock_part = MagicMock() + mock_part.function_call = FunctionCall(name="test_tool", args={}) + + def tool_call(hass, tool_input, tool_context): + mock_part.function_call = None + mock_part.text = "Hi there!" + return {"result": "Test response"} + + mock_tool.async_call.side_effect = tool_call + chat_response.parts = [mock_part] + result = await conversation.async_converse( + hass, + "Please call the test function", + None, + context, + agent_id=agent_id, + device_id="test_device", + ) + + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.as_dict()["speech"]["plain"]["speech"] == "Hi there!" + mock_tool_call = mock_chat.send_message_async.mock_calls[1][1][0] + mock_tool_call = type(mock_tool_call).to_dict(mock_tool_call) + assert mock_tool_call == { + "parts": [ + { + "function_response": { + "name": "test_tool", + "response": { + "result": "Test response", + }, + }, + }, + ], + "role": "", + } + + mock_tool.async_call.assert_awaited_once_with( + hass, + llm.ToolInput( + tool_name="test_tool", + tool_args={}, + ), + llm.LLMContext( + platform="google_generative_ai_conversation", + context=context, + user_prompt="Please call the test function", + language="en", + assistant="conversation", + device_id="test_device", + ), + ) + assert [tuple(mock_call) for mock_call in mock_model.mock_calls] == snapshot + + @patch( "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" ) From 2e031d0194b2722115b8f5fd253112eea2e4f05f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:26:31 +0200 Subject: [PATCH 0396/2411] Separate renault strings (#120737) --- homeassistant/components/renault/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/renault/strings.json b/homeassistant/components/renault/strings.json index 322f7a207d7..5217b4ff65a 100644 --- a/homeassistant/components/renault/strings.json +++ b/homeassistant/components/renault/strings.json @@ -73,9 +73,9 @@ "charge_mode": { "name": "Charge mode", "state": { - "always": "Instant", - "always_charging": "[%key:component::renault::entity::select::charge_mode::state::always%]", - "schedule_mode": "Planner", + "always": "Always", + "always_charging": "Always charging", + "schedule_mode": "Schedule mode", "scheduled": "Scheduled" } } From f28cbf1909f89d508d35a80c0bf6d7b58886ec7d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 28 Jun 2024 08:42:47 +0200 Subject: [PATCH 0397/2411] Set stateclass on unknown numeric Tasmota sensors (#120650) --- homeassistant/components/tasmota/sensor.py | 9 + .../tasmota/snapshots/test_sensor.ambr | 298 ++++++++++++++++++ tests/components/tasmota/test_sensor.py | 25 ++ 3 files changed, 332 insertions(+) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index a7fb415f037..db404884e67 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -298,6 +298,15 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): self._attr_native_unit_of_measurement = SENSOR_UNIT_MAP.get( self._tasmota_entity.unit, self._tasmota_entity.unit ) + if ( + self._attr_device_class is None + and self._attr_state_class is None + and self._attr_native_unit_of_measurement is None + ): + # If the sensor has a numeric value, but we couldn't detect what it is, + # set state class to measurement. + if self._tasmota_entity.discovered_as_numeric: + self._attr_state_class = SensorStateClass.MEASUREMENT async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index c5d70487749..b56115f189c 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -1546,3 +1546,301 @@ 'state': '2300', }) # --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR1 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR1 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR1_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR3 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR4 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].12 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR1 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].13 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR2 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].14 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR3 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].15 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR4 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR2 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR2 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR2_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR3 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor3_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR3 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR3_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR4 Unknown', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].7 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_sensor4_unknown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SENSOR4 Unknown', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_SENSOR4_Unknown', + 'unit_of_measurement': None, + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR1 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor1_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config9-entity_ids9-messages9].9 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tasmota SENSOR2 Unknown', + }), + 'context': , + 'entity_id': 'sensor.tasmota_sensor2_unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.5', + }) +# --- diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index c01485d12a7..44a6ce65fd1 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -50,6 +50,17 @@ BAD_LIST_SENSOR_CONFIG_3 = { } } +# This configuration has sensors which type we can't guess +DEFAULT_SENSOR_CONFIG_UNKNOWN = { + "sn": { + "Time": "2020-09-25T12:47:15", + "SENSOR1": {"Unknown": None}, + "SENSOR2": {"Unknown": "123"}, + "SENSOR3": {"Unknown": 123}, + "SENSOR4": {"Unknown": 123.0}, + } +} + # This configuration has some sensors where values are lists # Home Assistant maps this to one sensor for each list item LIST_SENSOR_CONFIG = { @@ -279,6 +290,20 @@ TEMPERATURE_SENSOR_CONFIG = { ), ), ), + # Test we automatically set state class to measurement on unknown numerical sensors + ( + DEFAULT_SENSOR_CONFIG_UNKNOWN, + [ + "sensor.tasmota_sensor1_unknown", + "sensor.tasmota_sensor2_unknown", + "sensor.tasmota_sensor3_unknown", + "sensor.tasmota_sensor4_unknown", + ], + ( + '{"SENSOR1":{"Unknown":20.5},"SENSOR2":{"Unknown":20.5},"SENSOR3":{"Unknown":20.5},"SENSOR4":{"Unknown":20.5}}', + '{"StatusSNS":{"SENSOR1":{"Unknown":20},"SENSOR2":{"Unknown":20},"SENSOR3":{"Unknown":20},"SENSOR4":{"Unknown":20}}}', + ), + ), ], ) async def test_controlling_state_via_mqtt( From 876fb234ceb906e71d672ba3d94856d5a0137b61 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Jun 2024 22:56:22 +0200 Subject: [PATCH 0398/2411] Bump hatasmota to 0.9.2 (#120670) --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 78 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../tasmota/snapshots/test_sensor.ambr | 232 +++++++++++++++--- tests/components/tasmota/test_sensor.py | 6 +- 6 files changed, 247 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 69233de07d8..783483c6ffd 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.9.1"] + "requirements": ["HATasmota==0.9.2"] } diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index db404884e67..e87ff88092e 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -53,26 +53,10 @@ ICON = "icon" # A Tasmota sensor type may be mapped to either a device class or an icon, # both can only be set if the default device class icon is not appropriate SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { - hc.SENSOR_ACTIVE_ENERGYEXPORT: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_ACTIVE_ENERGYIMPORT: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_ACTIVE_POWERUSAGE: { - DEVICE_CLASS: SensorDeviceClass.POWER, - STATE_CLASS: SensorStateClass.MEASUREMENT, - }, hc.SENSOR_AMBIENT: { DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_APPARENT_POWERUSAGE: { - DEVICE_CLASS: SensorDeviceClass.APPARENT_POWER, - STATE_CLASS: SensorStateClass.MEASUREMENT, - }, hc.SENSOR_BATTERY: { DEVICE_CLASS: SensorDeviceClass.BATTERY, STATE_CLASS: SensorStateClass.MEASUREMENT, @@ -92,7 +76,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.CURRENT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_CURRENTNEUTRAL: { + hc.SENSOR_CURRENT_NEUTRAL: { DEVICE_CLASS: SensorDeviceClass.CURRENT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -110,6 +94,34 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.ENERGY, STATE_CLASS: SensorStateClass.TOTAL, }, + hc.SENSOR_ENERGY_EXPORT_ACTIVE: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_EXPORT_REACTIVE: {STATE_CLASS: SensorStateClass.TOTAL}, + hc.SENSOR_ENERGY_EXPORT_TARIFF: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_ACTIVE: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_REACTIVE: {STATE_CLASS: SensorStateClass.TOTAL}, + hc.SENSOR_ENERGY_IMPORT_TODAY: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + }, + hc.SENSOR_ENERGY_IMPORT_TOTAL: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_TOTAL_TARIFF: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ENERGY_IMPORT_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, + hc.SENSOR_ENERGY_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_FREQUENCY: { DEVICE_CLASS: SensorDeviceClass.FREQUENCY, STATE_CLASS: SensorStateClass.MEASUREMENT, @@ -122,6 +134,14 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, + hc.SENSOR_POWER_ACTIVE: { + DEVICE_CLASS: SensorDeviceClass.POWER, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_POWER_APPARENT: { + DEVICE_CLASS: SensorDeviceClass.APPARENT_POWER, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"}, hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"}, hc.SENSOR_MOISTURE: {DEVICE_CLASS: SensorDeviceClass.MOISTURE}, @@ -144,11 +164,11 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.PM25, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_POWERFACTOR: { + hc.SENSOR_POWER_FACTOR: { DEVICE_CLASS: SensorDeviceClass.POWER_FACTOR, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_POWERUSAGE: { + hc.SENSOR_POWER: { DEVICE_CLASS: SensorDeviceClass.POWER, STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -156,14 +176,12 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.PRESSURE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_PRESSUREATSEALEVEL: { + hc.SENSOR_PRESSURE_AT_SEA_LEVEL: { DEVICE_CLASS: SensorDeviceClass.PRESSURE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"}, - hc.SENSOR_REACTIVE_ENERGYEXPORT: {STATE_CLASS: SensorStateClass.TOTAL}, - hc.SENSOR_REACTIVE_ENERGYIMPORT: {STATE_CLASS: SensorStateClass.TOTAL}, - hc.SENSOR_REACTIVE_POWERUSAGE: { + hc.SENSOR_POWER_REACTIVE: { DEVICE_CLASS: SensorDeviceClass.REACTIVE_POWER, STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -182,19 +200,6 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_TODAY: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - hc.SENSOR_TOTAL: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_TOTAL_TARIFF: { - DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL, - }, - hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, hc.SENSOR_VOLTAGE: { DEVICE_CLASS: SensorDeviceClass.VOLTAGE, @@ -204,7 +209,6 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.WEIGHT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, } SENSOR_UNIT_MAP = { diff --git a/requirements_all.txt b/requirements_all.txt index 98b5548cd38..d2e51a365e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.9.1 +HATasmota==0.9.2 # homeassistant.components.mastodon Mastodon.py==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2857aee8c9b..21e6eedebc7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.9.1 +HATasmota==0.9.2 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index b56115f189c..be011e595b9 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -280,6 +280,102 @@ 'unit_of_measurement': , }) # --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].11 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].12 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY TotalTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].13 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY TotalTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_totaltariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.8', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].14 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].15 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.4', + }) +# --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].2 StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -332,6 +428,108 @@ }) # --- # name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 0', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_exporttariff_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY ExportTariff 0', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_ExportTariff_0', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Tasmota ENERGY ExportTariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.tasmota_energy_exporttariff_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENERGY ExportTariff 1', + 'platform': 'tasmota', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_ExportTariff_1', + 'unit_of_measurement': , + }) +# --- +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].8 StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -347,7 +545,7 @@ 'state': '1.2', }) # --- -# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].5 +# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].9 StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -363,38 +561,6 @@ 'state': '3.4', }) # --- -# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].6 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Tasmota ENERGY TotalTariff 0', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.tasmota_energy_totaltariff_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '5.6', - }) -# --- -# name: test_controlling_state_via_mqtt[sensor_config2-entity_ids2-messages2].7 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Tasmota ENERGY TotalTariff 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.tasmota_energy_totaltariff_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '7.8', - }) -# --- # name: test_controlling_state_via_mqtt[sensor_config3-entity_ids3-messages3] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 44a6ce65fd1..78235f7ebf5 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -209,10 +209,12 @@ TEMPERATURE_SENSOR_CONFIG = { [ "sensor.tasmota_energy_totaltariff_0", "sensor.tasmota_energy_totaltariff_1", + "sensor.tasmota_energy_exporttariff_0", + "sensor.tasmota_energy_exporttariff_1", ], ( - '{"ENERGY":{"TotalTariff":[1.2,3.4]}}', - '{"StatusSNS":{"ENERGY":{"TotalTariff":[5.6,7.8]}}}', + '{"ENERGY":{"ExportTariff":[5.6,7.8],"TotalTariff":[1.2,3.4]}}', + '{"StatusSNS":{"ENERGY":{"ExportTariff":[1.2,3.4],"TotalTariff":[5.6,7.8]}}}', ), ), ( From ca515f740e53a83c8b1b5a4fefb2fa2309807a25 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 11:15:44 +0200 Subject: [PATCH 0399/2411] Bump panasonic_viera to 0.4.2 (#120692) * Bump panasonic_viera to 0.4.2 * Bump panasonic_viera to 0.4.2 * Bump panasonic_viera to 0.4.2 * Fix Keys --- .../components/panasonic_viera/__init__.py | 8 ++++---- .../components/panasonic_viera/manifest.json | 2 +- .../components/panasonic_viera/media_player.py | 18 +++++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/panasonic_viera/test_remote.py | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index b2f3bbba91a..2cf91792800 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -196,10 +196,10 @@ class Remote: self.muted = self._control.get_mute() self.volume = self._control.get_volume() / 100 - async def async_send_key(self, key): + async def async_send_key(self, key: Keys | str) -> None: """Send a key to the TV and handle exceptions.""" try: - key = getattr(Keys, key) + key = getattr(Keys, key.upper()) except (AttributeError, TypeError): key = getattr(key, "value", key) @@ -211,13 +211,13 @@ class Remote: await self._on_action.async_run(context=context) await self.async_update() elif self.state != STATE_ON: - await self.async_send_key(Keys.power) + await self.async_send_key(Keys.POWER) await self.async_update() async def async_turn_off(self): """Turn off the TV.""" if self.state != STATE_OFF: - await self.async_send_key(Keys.power) + await self.async_send_key(Keys.POWER) self.state = STATE_OFF await self.async_update() diff --git a/homeassistant/components/panasonic_viera/manifest.json b/homeassistant/components/panasonic_viera/manifest.json index 2afa6599cb2..d9809e5883a 100644 --- a/homeassistant/components/panasonic_viera/manifest.json +++ b/homeassistant/components/panasonic_viera/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/panasonic_viera", "iot_class": "local_polling", "loggers": ["panasonic_viera"], - "requirements": ["panasonic-viera==0.3.6"] + "requirements": ["panasonic-viera==0.4.2"] } diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 44063022129..76ca76c1ca6 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -126,11 +126,11 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): async def async_volume_up(self) -> None: """Volume up the media player.""" - await self._remote.async_send_key(Keys.volume_up) + await self._remote.async_send_key(Keys.VOLUME_UP) async def async_volume_down(self) -> None: """Volume down media player.""" - await self._remote.async_send_key(Keys.volume_down) + await self._remote.async_send_key(Keys.VOLUME_DOWN) async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" @@ -143,33 +143,33 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): async def async_media_play_pause(self) -> None: """Simulate play pause media player.""" if self._remote.playing: - await self._remote.async_send_key(Keys.pause) + await self._remote.async_send_key(Keys.PAUSE) self._remote.playing = False else: - await self._remote.async_send_key(Keys.play) + await self._remote.async_send_key(Keys.PLAY) self._remote.playing = True async def async_media_play(self) -> None: """Send play command.""" - await self._remote.async_send_key(Keys.play) + await self._remote.async_send_key(Keys.PLAY) self._remote.playing = True async def async_media_pause(self) -> None: """Send pause command.""" - await self._remote.async_send_key(Keys.pause) + await self._remote.async_send_key(Keys.PAUSE) self._remote.playing = False async def async_media_stop(self) -> None: """Stop playback.""" - await self._remote.async_send_key(Keys.stop) + await self._remote.async_send_key(Keys.STOP) async def async_media_next_track(self) -> None: """Send the fast forward command.""" - await self._remote.async_send_key(Keys.fast_forward) + await self._remote.async_send_key(Keys.FAST_FORWARD) async def async_media_previous_track(self) -> None: """Send the rewind command.""" - await self._remote.async_send_key(Keys.rewind) + await self._remote.async_send_key(Keys.REWIND) async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any diff --git a/requirements_all.txt b/requirements_all.txt index d2e51a365e7..57cbb7b7710 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1528,7 +1528,7 @@ paho-mqtt==1.6.1 panacotta==0.2 # homeassistant.components.panasonic_viera -panasonic-viera==0.3.6 +panasonic-viera==0.4.2 # homeassistant.components.dunehd pdunehd==1.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21e6eedebc7..306b791488a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1228,7 +1228,7 @@ p1monitor==3.0.0 paho-mqtt==1.6.1 # homeassistant.components.panasonic_viera -panasonic-viera==0.3.6 +panasonic-viera==0.4.2 # homeassistant.components.dunehd pdunehd==1.3.2 diff --git a/tests/components/panasonic_viera/test_remote.py b/tests/components/panasonic_viera/test_remote.py index 05254753d3f..3ae241fc5e9 100644 --- a/tests/components/panasonic_viera/test_remote.py +++ b/tests/components/panasonic_viera/test_remote.py @@ -46,7 +46,7 @@ async def test_onoff(hass: HomeAssistant, mock_remote) -> None: await hass.services.async_call(REMOTE_DOMAIN, SERVICE_TURN_ON, data) await hass.async_block_till_done() - power = getattr(Keys.power, "value", Keys.power) + power = getattr(Keys.POWER, "value", Keys.POWER) assert mock_remote.send_key.call_args_list == [call(power), call(power)] From ef3ecb618350a1f60b7650cd935399b3f74ff9c0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 08:36:10 +0200 Subject: [PATCH 0400/2411] Bump apsystems-ez1 to 1.3.3 (#120702) --- homeassistant/components/apsystems/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apsystems/manifest.json b/homeassistant/components/apsystems/manifest.json index 8e0ac00796d..cba3e59dba0 100644 --- a/homeassistant/components/apsystems/manifest.json +++ b/homeassistant/components/apsystems/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/apsystems", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["apsystems-ez1==1.3.1"] + "requirements": ["apsystems-ez1==1.3.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 57cbb7b7710..cd2a2335987 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ apprise==1.8.0 aprslib==0.7.2 # homeassistant.components.apsystems -apsystems-ez1==1.3.1 +apsystems-ez1==1.3.3 # homeassistant.components.aqualogic aqualogic==2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 306b791488a..6bc406c6982 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -428,7 +428,7 @@ apprise==1.8.0 aprslib==0.7.2 # homeassistant.components.apsystems -apsystems-ez1==1.3.1 +apsystems-ez1==1.3.3 # homeassistant.components.aranet aranet4==2.3.4 From 1227d56aa219391484b2a07c0100109690ae782c Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 27 Jun 2024 23:12:20 +0200 Subject: [PATCH 0401/2411] Bump `nextdns` to version 3.1.0 (#120703) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/nextdns/__init__.py | 6 +++--- homeassistant/components/nextdns/config_flow.py | 11 +++++------ homeassistant/components/nextdns/coordinator.py | 12 ++++++++---- homeassistant/components/nextdns/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nextdns/test_config_flow.py | 2 ++ tests/components/nextdns/test_init.py | 9 +++++++-- tests/components/nextdns/test_switch.py | 13 +++++++++++-- 9 files changed, 39 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index f11611007c2..4256126b3c7 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -18,6 +18,7 @@ from nextdns import ( NextDns, Settings, ) +from tenacity import RetryError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform @@ -84,9 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: NextDnsConfigEntry) -> b websession = async_get_clientsession(hass) try: - async with asyncio.timeout(10): - nextdns = await NextDns.create(websession, api_key) - except (ApiError, ClientConnectorError, TimeoutError) as err: + nextdns = await NextDns.create(websession, api_key) + except (ApiError, ClientConnectorError, RetryError, TimeoutError) as err: raise ConfigEntryNotReady from err tasks = [] diff --git a/homeassistant/components/nextdns/config_flow.py b/homeassistant/components/nextdns/config_flow.py index 4955bbb4cad..bd79112b1f9 100644 --- a/homeassistant/components/nextdns/config_flow.py +++ b/homeassistant/components/nextdns/config_flow.py @@ -2,11 +2,11 @@ from __future__ import annotations -import asyncio from typing import Any from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError, InvalidApiKeyError, NextDns +from tenacity import RetryError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -37,13 +37,12 @@ class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN): if user_input is not None: self.api_key = user_input[CONF_API_KEY] try: - async with asyncio.timeout(10): - self.nextdns = await NextDns.create( - websession, user_input[CONF_API_KEY] - ) + self.nextdns = await NextDns.create( + websession, user_input[CONF_API_KEY] + ) except InvalidApiKeyError: errors["base"] = "invalid_api_key" - except (ApiError, ClientConnectorError, TimeoutError): + except (ApiError, ClientConnectorError, RetryError, TimeoutError): errors["base"] = "cannot_connect" except Exception: # noqa: BLE001 errors["base"] = "unknown" diff --git a/homeassistant/components/nextdns/coordinator.py b/homeassistant/components/nextdns/coordinator.py index cad1aeac070..5210807bd3c 100644 --- a/homeassistant/components/nextdns/coordinator.py +++ b/homeassistant/components/nextdns/coordinator.py @@ -1,6 +1,5 @@ """NextDns coordinator.""" -import asyncio from datetime import timedelta import logging from typing import TypeVar @@ -19,6 +18,7 @@ from nextdns import ( Settings, ) from nextdns.model import NextDnsData +from tenacity import RetryError from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo @@ -58,9 +58,13 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]): async def _async_update_data(self) -> CoordinatorDataT: """Update data via internal method.""" try: - async with asyncio.timeout(10): - return await self._async_update_data_internal() - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + return await self._async_update_data_internal() + except ( + ApiError, + ClientConnectorError, + InvalidApiKeyError, + RetryError, + ) as err: raise UpdateFailed(err) from err async def _async_update_data_internal(self) -> CoordinatorDataT: diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index 1e7145ef6d1..b65706ef1ce 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["nextdns"], "quality_scale": "platinum", - "requirements": ["nextdns==3.0.0"] + "requirements": ["nextdns==3.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index cd2a2335987..cf0e22167a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1404,7 +1404,7 @@ nextcloudmonitor==1.5.0 nextcord==2.6.0 # homeassistant.components.nextdns -nextdns==3.0.0 +nextdns==3.1.0 # homeassistant.components.nibe_heatpump nibe==2.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6bc406c6982..f7254727d3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1143,7 +1143,7 @@ nextcloudmonitor==1.5.0 nextcord==2.6.0 # homeassistant.components.nextdns -nextdns==3.0.0 +nextdns==3.1.0 # homeassistant.components.nibe_heatpump nibe==2.8.0 diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py index 9247288eebf..7571eef347e 100644 --- a/tests/components/nextdns/test_config_flow.py +++ b/tests/components/nextdns/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from nextdns import ApiError, InvalidApiKeyError import pytest +from tenacity import RetryError from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.config_entries import SOURCE_USER @@ -57,6 +58,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: [ (ApiError("API Error"), "cannot_connect"), (InvalidApiKeyError, "invalid_api_key"), + (RetryError("Retry Error"), "cannot_connect"), (TimeoutError, "cannot_connect"), (ValueError, "unknown"), ], diff --git a/tests/components/nextdns/test_init.py b/tests/components/nextdns/test_init.py index f7b85bb8a54..61a487d917c 100644 --- a/tests/components/nextdns/test_init.py +++ b/tests/components/nextdns/test_init.py @@ -3,6 +3,8 @@ from unittest.mock import patch from nextdns import ApiError +import pytest +from tenacity import RetryError from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -24,7 +26,10 @@ async def test_async_setup_entry(hass: HomeAssistant) -> None: assert state.state == "20.0" -async def test_config_not_ready(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "exc", [ApiError("API Error"), RetryError("Retry Error"), TimeoutError] +) +async def test_config_not_ready(hass: HomeAssistant, exc: Exception) -> None: """Test for setup failure if the connection to the service fails.""" entry = MockConfigEntry( domain=DOMAIN, @@ -35,7 +40,7 @@ async def test_config_not_ready(hass: HomeAssistant) -> None: with patch( "homeassistant.components.nextdns.NextDns.get_profiles", - side_effect=ApiError("API Error"), + side_effect=exc, ): 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 index 059585e9ffe..6e344e34336 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -8,6 +8,7 @@ from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError import pytest from syrupy import SnapshotAssertion +from tenacity import RetryError from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -94,7 +95,15 @@ async def test_switch_off(hass: HomeAssistant) -> None: mock_switch_on.assert_called_once() -async def test_availability(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "exc", + [ + ApiError("API Error"), + RetryError("Retry Error"), + TimeoutError, + ], +) +async def test_availability(hass: HomeAssistant, exc: Exception) -> None: """Ensure that we mark the entities unavailable correctly when service causes an error.""" await init_integration(hass) @@ -106,7 +115,7 @@ async def test_availability(hass: HomeAssistant) -> None: future = utcnow() + timedelta(minutes=10) with patch( "homeassistant.components.nextdns.NextDns.get_settings", - side_effect=ApiError("API Error"), + side_effect=exc, ): async_fire_time_changed(hass, future) await hass.async_block_till_done(wait_background_tasks=True) From 35d145d3bc1d34f126864ad71c2d6f82694487b9 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Fri, 28 Jun 2024 07:22:24 -0300 Subject: [PATCH 0402/2411] Link the Statistics helper entity to the source entity device (#120705) --- .../components/statistics/__init__.py | 11 ++- homeassistant/components/statistics/sensor.py | 8 ++ tests/components/statistics/test_init.py | 92 +++++++++++++++++++ tests/components/statistics/test_sensor.py | 49 +++++++++- 4 files changed, 158 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/__init__.py b/homeassistant/components/statistics/__init__.py index 70739c618f7..f71274e0ee7 100644 --- a/homeassistant/components/statistics/__init__.py +++ b/homeassistant/components/statistics/__init__.py @@ -1,8 +1,11 @@ """The statistics component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.device import ( + async_remove_stale_devices_links_keep_entity_device, +) DOMAIN = "statistics" PLATFORMS = [Platform.SENSOR] @@ -11,6 +14,12 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Statistics from a config entry.""" + async_remove_stale_devices_links_keep_entity_device( + hass, + entry.entry_id, + entry.options[CONF_ENTITY_ID], + ) + 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/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 8d28254ad61..ca1d75b57ed 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -43,6 +43,7 @@ from homeassistant.core import ( split_entity_id, ) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_track_point_in_utc_time, @@ -268,6 +269,7 @@ async def async_setup_platform( async_add_entities( new_entities=[ StatisticsSensor( + hass=hass, source_entity_id=config[CONF_ENTITY_ID], name=config[CONF_NAME], unique_id=config.get(CONF_UNIQUE_ID), @@ -304,6 +306,7 @@ async def async_setup_entry( async_add_entities( [ StatisticsSensor( + hass=hass, source_entity_id=entry.options[CONF_ENTITY_ID], name=entry.options[CONF_NAME], unique_id=entry.entry_id, @@ -327,6 +330,7 @@ class StatisticsSensor(SensorEntity): def __init__( self, + hass: HomeAssistant, source_entity_id: str, name: str, unique_id: str | None, @@ -341,6 +345,10 @@ class StatisticsSensor(SensorEntity): self._attr_name: str = name self._attr_unique_id: str | None = unique_id self._source_entity_id: str = source_entity_id + self._attr_device_info = async_device_info_to_link_from_entity( + hass, + source_entity_id, + ) self.is_binary: bool = ( split_entity_id(self._source_entity_id)[0] == BINARY_SENSOR_DOMAIN ) diff --git a/tests/components/statistics/test_init.py b/tests/components/statistics/test_init.py index 6cb943c0687..64829ea7d66 100644 --- a/tests/components/statistics/test_init.py +++ b/tests/components/statistics/test_init.py @@ -2,8 +2,10 @@ from __future__ import annotations +from homeassistant.components.statistics import DOMAIN as STATISTICS_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -15,3 +17,93 @@ async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) assert await hass.config_entries.async_unload(loaded_entry.entry_id) await hass.async_block_till_done() assert loaded_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_device_cleaning( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test the cleaning of devices linked to the helper Statistics.""" + + # Source entity device config entry + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + + # Device entry of the source entity + source_device1_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test1")}, + connections={("mac", "30:31:32:33:34:01")}, + ) + + # Source entity registry + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device1_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + # Configure the configuration entry for Statistics + statistics_config_entry = MockConfigEntry( + data={}, + domain=STATISTICS_DOMAIN, + options={ + "name": "Statistics", + "entity_id": "sensor.test_source", + "state_characteristic": "mean", + "keep_last_sample": False, + "percentile": 50.0, + "precision": 2.0, + "sampling_size": 20.0, + }, + title="Statistics", + ) + statistics_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the statistics sensor + statistics_entity = entity_registry.async_get("sensor.statistics") + assert statistics_entity is not None + assert statistics_entity.device_id == source_entity.device_id + + # Device entry incorrectly linked to Statistics config entry + device_registry.async_get_or_create( + config_entry_id=statistics_config_entry.entry_id, + identifiers={("sensor", "identifier_test2")}, + connections={("mac", "30:31:32:33:34:02")}, + ) + device_registry.async_get_or_create( + config_entry_id=statistics_config_entry.entry_id, + identifiers={("sensor", "identifier_test3")}, + connections={("mac", "30:31:32:33:34:03")}, + ) + await hass.async_block_till_done() + + # Before reloading the config entry, two devices are expected to be linked + devices_before_reload = device_registry.devices.get_devices_for_config_entry_id( + statistics_config_entry.entry_id + ) + assert len(devices_before_reload) == 3 + + # Config entry reload + await hass.config_entries.async_reload(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the statistics sensor + statistics_entity = entity_registry.async_get("sensor.statistics") + assert statistics_entity is not None + assert statistics_entity.device_id == source_entity.device_id + + # After reloading the config entry, only one linked device is expected + devices_after_reload = device_registry.devices.get_devices_for_config_entry_id( + statistics_config_entry.entry_id + ) + assert len(devices_after_reload) == 1 + + assert devices_after_reload[0].id == source_device1_entry.id diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 269c17e34b9..c90d685714c 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -41,7 +41,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -1654,3 +1654,50 @@ async def test_reload(recorder_mock: Recorder, hass: HomeAssistant) -> None: assert hass.states.get("sensor.test") is None assert hass.states.get("sensor.cputest") + + +async def test_device_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test for source entity device for Statistics.""" + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + source_device_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + source_entity = entity_registry.async_get_or_create( + "sensor", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("sensor.test_source") is not None + + statistics_config_entry = MockConfigEntry( + data={}, + domain=STATISTICS_DOMAIN, + options={ + "name": "Statistics", + "entity_id": "sensor.test_source", + "state_characteristic": "mean", + "keep_last_sample": False, + "percentile": 50.0, + "precision": 2.0, + "sampling_size": 20.0, + }, + title="Statistics", + ) + statistics_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + statistics_entity = entity_registry.async_get("sensor.statistics") + assert statistics_entity is not None + assert statistics_entity.device_id == source_entity.device_id From 3932ce57b9da11ffced4b76b7aa5c55186c628d4 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 28 Jun 2024 19:21:59 +1000 Subject: [PATCH 0403/2411] Check Tessie scopes to fix startup bug (#120710) * Add scope check * Add tests * Bump Teslemetry --- .../components/teslemetry/manifest.json | 2 +- homeassistant/components/tessie/__init__.py | 68 +++++++++++-------- homeassistant/components/tessie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tessie/common.py | 11 +++ tests/components/tessie/conftest.py | 11 +++ tests/components/tessie/test_init.py | 14 +++- 8 files changed, 76 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index 2eb3e221855..49d73909a71 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], "quality_scale": "platinum", - "requirements": ["tesla-fleet-api==0.6.1"] + "requirements": ["tesla-fleet-api==0.6.2"] } diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index e8891d6665f..1d6e2a27786 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -6,6 +6,7 @@ import logging from aiohttp import ClientError, ClientResponseError from tesla_fleet_api import EnergySpecific, Tessie +from tesla_fleet_api.const import Scope from tesla_fleet_api.exceptions import TeslaFleetError from tessie_api import get_state_of_all_vehicles @@ -94,41 +95,48 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo # Energy Sites tessie = Tessie(session, api_key) + energysites: list[TessieEnergyData] = [] + try: - products = (await tessie.products())["response"] + scopes = await tessie.scopes() except TeslaFleetError as e: raise ConfigEntryNotReady from e - energysites: list[TessieEnergyData] = [] - for product in products: - if "energy_site_id" in product: - site_id = product["energy_site_id"] - api = EnergySpecific(tessie.energy, site_id) - energysites.append( - TessieEnergyData( - api=api, - id=site_id, - live_coordinator=TessieEnergySiteLiveCoordinator(hass, api), - info_coordinator=TessieEnergySiteInfoCoordinator(hass, api), - device=DeviceInfo( - identifiers={(DOMAIN, str(site_id))}, - manufacturer="Tesla", - name=product.get("site_name", "Energy Site"), - ), - ) - ) + if Scope.ENERGY_DEVICE_DATA in scopes: + try: + products = (await tessie.products())["response"] + except TeslaFleetError as e: + raise ConfigEntryNotReady from e - # Populate coordinator data before forwarding to platforms - await asyncio.gather( - *( - energysite.live_coordinator.async_config_entry_first_refresh() - for energysite in energysites - ), - *( - energysite.info_coordinator.async_config_entry_first_refresh() - for energysite in energysites - ), - ) + for product in products: + if "energy_site_id" in product: + site_id = product["energy_site_id"] + api = EnergySpecific(tessie.energy, site_id) + energysites.append( + TessieEnergyData( + api=api, + id=site_id, + live_coordinator=TessieEnergySiteLiveCoordinator(hass, api), + info_coordinator=TessieEnergySiteInfoCoordinator(hass, api), + device=DeviceInfo( + identifiers={(DOMAIN, str(site_id))}, + manufacturer="Tesla", + name=product.get("site_name", "Energy Site"), + ), + ) + ) + + # Populate coordinator data before forwarding to platforms + await asyncio.gather( + *( + energysite.live_coordinator.async_config_entry_first_refresh() + for energysite in energysites + ), + *( + energysite.info_coordinator.async_config_entry_first_refresh() + for energysite in energysites + ), + ) entry.runtime_data = TessieData(vehicles, energysites) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json index bf1ab5f61e4..493feeaa77e 100644 --- a/homeassistant/components/tessie/manifest.json +++ b/homeassistant/components/tessie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tessie", "iot_class": "cloud_polling", "loggers": ["tessie"], - "requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.1"] + "requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index cf0e22167a0..ba007cb230f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2711,7 +2711,7 @@ temperusb==1.6.1 # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==0.6.1 +tesla-fleet-api==0.6.2 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7254727d3c..05397f24983 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2109,7 +2109,7 @@ temperusb==1.6.1 # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==0.6.1 +tesla-fleet-api==0.6.2 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index 3d24c6b233a..37a38fffaa4 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -54,6 +54,17 @@ LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN) SITE_INFO = load_json_object_fixture("site_info.json", DOMAIN) RESPONSE_OK = {"response": {}, "error": None} COMMAND_OK = {"response": {"result": True, "reason": ""}} +SCOPES = [ + "user_data", + "vehicle_device_data", + "vehicle_cmds", + "vehicle_charging_cmds", + "energy_device_data", + "energy_cmds", + "offline_access", + "openid", +] +NO_SCOPES = ["user_data", "offline_access", "openid"] async def setup_platform( diff --git a/tests/components/tessie/conftest.py b/tests/components/tessie/conftest.py index 79cc9aa44c6..e0aba73af17 100644 --- a/tests/components/tessie/conftest.py +++ b/tests/components/tessie/conftest.py @@ -11,6 +11,7 @@ from .common import ( COMMAND_OK, LIVE_STATUS, PRODUCTS, + SCOPES, SITE_INFO, TEST_STATE_OF_ALL_VEHICLES, TEST_VEHICLE_STATE_ONLINE, @@ -51,6 +52,16 @@ def mock_get_state_of_all_vehicles(): # Fleet API +@pytest.fixture(autouse=True) +def mock_scopes(): + """Mock scopes function.""" + with patch( + "homeassistant.components.tessie.Tessie.scopes", + return_value=SCOPES, + ) as mock_scopes: + yield mock_scopes + + @pytest.fixture(autouse=True) def mock_products(): """Mock Tesla Fleet Api products method.""" diff --git a/tests/components/tessie/test_init.py b/tests/components/tessie/test_init.py index e37512ea8c4..921ef93b1ae 100644 --- a/tests/components/tessie/test_init.py +++ b/tests/components/tessie/test_init.py @@ -50,11 +50,21 @@ async def test_connection_failure( assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_fleet_error(hass: HomeAssistant) -> None: - """Test init with a fleet error.""" +async def test_products_error(hass: HomeAssistant) -> None: + """Test init with a fleet error on products.""" with patch( "homeassistant.components.tessie.Tessie.products", side_effect=TeslaFleetError ): entry = await setup_platform(hass) assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_scopes_error(hass: HomeAssistant) -> None: + """Test init with a fleet error on scopes.""" + + with patch( + "homeassistant.components.tessie.Tessie.scopes", side_effect=TeslaFleetError + ): + entry = await setup_platform(hass) + assert entry.state is ConfigEntryState.SETUP_RETRY From 76780ca04eaf4c23ed1029d9c0acf2b1c9d2e37f Mon Sep 17 00:00:00 2001 From: Dave Leaver Date: Fri, 28 Jun 2024 19:44:54 +1200 Subject: [PATCH 0404/2411] Bump airtouch5py to 1.2.0 (#120715) * Bump airtouch5py to fix console 1.2.0 * Bump airtouch5py again --- homeassistant/components/airtouch5/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airtouch5/manifest.json b/homeassistant/components/airtouch5/manifest.json index 0d4cbc32761..312a627d0e8 100644 --- a/homeassistant/components/airtouch5/manifest.json +++ b/homeassistant/components/airtouch5/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airtouch5", "iot_class": "local_push", "loggers": ["airtouch5py"], - "requirements": ["airtouch5py==0.2.8"] + "requirements": ["airtouch5py==0.2.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index ba007cb230f..788a1ff1be7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -428,7 +428,7 @@ airthings-cloud==0.2.0 airtouch4pyapi==1.0.5 # homeassistant.components.airtouch5 -airtouch5py==0.2.8 +airtouch5py==0.2.10 # homeassistant.components.alpha_vantage alpha-vantage==2.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05397f24983..d41bfa7f997 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -401,7 +401,7 @@ airthings-cloud==0.2.0 airtouch4pyapi==1.0.5 # homeassistant.components.airtouch5 -airtouch5py==0.2.8 +airtouch5py==0.2.10 # homeassistant.components.amberelectric amberelectric==1.1.0 From 0ae11b033576ae41303f9ad68a8e29827a491084 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:03:01 +0200 Subject: [PATCH 0405/2411] Bump renault-api to 0.2.4 (#120727) --- homeassistant/components/renault/binary_sensor.py | 2 +- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/renault/fixtures/hvac_status.1.json | 2 +- tests/components/renault/fixtures/hvac_status.2.json | 2 +- tests/components/renault/snapshots/test_diagnostics.ambr | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index 7ebc77b8e77..2041499b711 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -81,7 +81,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( key="hvac_status", coordinator="hvac_status", on_key="hvacStatus", - on_value=2, + on_value="on", translation_key="hvac_status", ), RenaultBinarySensorEntityDescription( diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 8407893011c..ffa1cd6acef 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "platinum", - "requirements": ["renault-api==0.2.3"] + "requirements": ["renault-api==0.2.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 788a1ff1be7..e6ce960ebfc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2454,7 +2454,7 @@ refoss-ha==1.2.1 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.3 +renault-api==0.2.4 # homeassistant.components.renson renson-endura-delta==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d41bfa7f997..37c5e47c5fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1918,7 +1918,7 @@ refoss-ha==1.2.1 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.3 +renault-api==0.2.4 # homeassistant.components.renson renson-endura-delta==1.7.1 diff --git a/tests/components/renault/fixtures/hvac_status.1.json b/tests/components/renault/fixtures/hvac_status.1.json index 7cbd7a9fe37..f48cbae68ae 100644 --- a/tests/components/renault/fixtures/hvac_status.1.json +++ b/tests/components/renault/fixtures/hvac_status.1.json @@ -2,6 +2,6 @@ "data": { "type": "Car", "id": "VF1AAAAA555777999", - "attributes": { "externalTemperature": 8.0, "hvacStatus": 1 } + "attributes": { "externalTemperature": 8.0, "hvacStatus": "off" } } } diff --git a/tests/components/renault/fixtures/hvac_status.2.json b/tests/components/renault/fixtures/hvac_status.2.json index 8bb4f941e06..a2ca08a71e9 100644 --- a/tests/components/renault/fixtures/hvac_status.2.json +++ b/tests/components/renault/fixtures/hvac_status.2.json @@ -4,7 +4,7 @@ "id": "VF1AAAAA555777999", "attributes": { "socThreshold": 30.0, - "hvacStatus": 1, + "hvacStatus": "off", "lastUpdateTime": "2020-12-03T00:00:00Z" } } diff --git a/tests/components/renault/snapshots/test_diagnostics.ambr b/tests/components/renault/snapshots/test_diagnostics.ambr index ae90115fcb6..a2921dff35e 100644 --- a/tests/components/renault/snapshots/test_diagnostics.ambr +++ b/tests/components/renault/snapshots/test_diagnostics.ambr @@ -22,7 +22,7 @@ }), 'hvac_status': dict({ 'externalTemperature': 8.0, - 'hvacStatus': 1, + 'hvacStatus': 'off', }), 'res_state': dict({ }), @@ -227,7 +227,7 @@ }), 'hvac_status': dict({ 'externalTemperature': 8.0, - 'hvacStatus': 1, + 'hvacStatus': 'off', }), 'res_state': dict({ }), From fe8b5656dd166bf25f4f3504140f6baf6619daef Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:26:31 +0200 Subject: [PATCH 0406/2411] Separate renault strings (#120737) --- homeassistant/components/renault/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/renault/strings.json b/homeassistant/components/renault/strings.json index 322f7a207d7..5217b4ff65a 100644 --- a/homeassistant/components/renault/strings.json +++ b/homeassistant/components/renault/strings.json @@ -73,9 +73,9 @@ "charge_mode": { "name": "Charge mode", "state": { - "always": "Instant", - "always_charging": "[%key:component::renault::entity::select::charge_mode::state::always%]", - "schedule_mode": "Planner", + "always": "Always", + "always_charging": "Always charging", + "schedule_mode": "Schedule mode", "scheduled": "Scheduled" } } From c5fa9ad2729f83eef666e58a21f6a7db9721df88 Mon Sep 17 00:00:00 2001 From: Illia <146177275+ikalnyi@users.noreply.github.com> Date: Fri, 28 Jun 2024 12:14:44 +0200 Subject: [PATCH 0407/2411] Bump asyncarve to 0.1.1 (#120740) --- homeassistant/components/arve/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/arve/manifest.json b/homeassistant/components/arve/manifest.json index fa33b3309ce..4c63d377371 100644 --- a/homeassistant/components/arve/manifest.json +++ b/homeassistant/components/arve/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/arve", "iot_class": "cloud_polling", - "requirements": ["asyncarve==0.0.9"] + "requirements": ["asyncarve==0.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e6ce960ebfc..34eb86f9ddc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ asterisk_mbox==0.5.0 async-upnp-client==0.39.0 # homeassistant.components.arve -asyncarve==0.0.9 +asyncarve==0.1.1 # homeassistant.components.keyboard_remote asyncinotify==4.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37c5e47c5fa..c08fe273b8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ asterisk_mbox==0.5.0 async-upnp-client==0.39.0 # homeassistant.components.arve -asyncarve==0.0.9 +asyncarve==0.1.1 # homeassistant.components.sleepiq asyncsleepiq==1.5.2 From cada78496b048e33f9843ef72f43a76c30772494 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 28 Jun 2024 04:25:55 -0700 Subject: [PATCH 0408/2411] Fix Google Generative AI: 400 Request contains an invalid argument (#120741) --- .../conversation.py | 9 +- .../snapshots/test_conversation.ambr | 166 ++++++++++++++++++ .../test_conversation.py | 83 +++++++++ 3 files changed, 255 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index fb7f5c3b21c..8052ee66f40 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -95,9 +95,12 @@ def _format_tool( ) -> dict[str, Any]: """Format tool specification.""" - parameters = _format_schema( - convert(tool.parameters, custom_serializer=custom_serializer) - ) + if tool.parameters.schema: + parameters = _format_schema( + convert(tool.parameters, custom_serializer=custom_serializer) + ) + else: + parameters = None return protos.Tool( { diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index b0a0ce967de..7f28c172970 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -409,3 +409,169 @@ ), ]) # --- +# name: test_function_call + list([ + tuple( + '', + tuple( + ), + dict({ + 'generation_config': dict({ + 'max_output_tokens': 150, + 'temperature': 1.0, + 'top_k': 64, + 'top_p': 0.95, + }), + 'model_name': 'models/gemini-1.5-flash-latest', + 'safety_settings': dict({ + 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', + 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', + 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', + 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', + }), + 'system_instruction': ''' + Current time is 05:00:00. Today's date is 2024-05-24. + You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. + Answer in plain text. Keep it simple and to the point. + Only if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant. + ''', + 'tools': list([ + function_declarations { + name: "test_tool" + description: "Test function" + parameters { + type_: OBJECT + properties { + key: "param1" + value { + type_: ARRAY + description: "Test parameters" + items { + type_: STRING + format_: "lower" + } + } + } + } + } + , + ]), + }), + ), + tuple( + '().start_chat', + tuple( + ), + dict({ + 'history': list([ + ]), + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + 'Please call the test function', + ), + dict({ + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + parts { + function_response { + name: "test_tool" + response { + fields { + key: "result" + value { + string_value: "Test response" + } + } + } + } + } + , + ), + dict({ + }), + ), + ]) +# --- +# name: test_function_call_without_parameters + list([ + tuple( + '', + tuple( + ), + dict({ + 'generation_config': dict({ + 'max_output_tokens': 150, + 'temperature': 1.0, + 'top_k': 64, + 'top_p': 0.95, + }), + 'model_name': 'models/gemini-1.5-flash-latest', + 'safety_settings': dict({ + 'DANGEROUS': 'BLOCK_MEDIUM_AND_ABOVE', + 'HARASSMENT': 'BLOCK_MEDIUM_AND_ABOVE', + 'HATE': 'BLOCK_MEDIUM_AND_ABOVE', + 'SEXUAL': 'BLOCK_MEDIUM_AND_ABOVE', + }), + 'system_instruction': ''' + Current time is 05:00:00. Today's date is 2024-05-24. + You are a voice assistant for Home Assistant. + Answer questions about the world truthfully. + Answer in plain text. Keep it simple and to the point. + Only if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant. + ''', + 'tools': list([ + function_declarations { + name: "test_tool" + description: "Test function" + } + , + ]), + }), + ), + tuple( + '().start_chat', + tuple( + ), + dict({ + 'history': list([ + ]), + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + 'Please call the test function', + ), + dict({ + }), + ), + tuple( + '().start_chat().send_message_async', + tuple( + parts { + function_response { + name: "test_tool" + response { + fields { + key: "result" + value { + string_value: "Test response" + } + } + } + } + } + , + ), + dict({ + }), + ), + ]) +# --- diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 990058aa89d..30016335f3b 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -172,6 +172,7 @@ async def test_function_call( hass: HomeAssistant, mock_config_entry_with_assist: MockConfigEntry, mock_init_component, + snapshot: SnapshotAssertion, ) -> None: """Test function calling.""" agent_id = mock_config_entry_with_assist.entry_id @@ -256,6 +257,7 @@ async def test_function_call( device_id="test_device", ), ) + assert [tuple(mock_call) for mock_call in mock_model.mock_calls] == snapshot # Test conversating tracing traces = trace.async_get_traces() @@ -272,6 +274,87 @@ async def test_function_call( assert "Answer in plain text" in detail_event["data"]["prompt"] +@patch( + "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" +) +async def test_function_call_without_parameters( + mock_get_tools, + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_init_component, + snapshot: SnapshotAssertion, +) -> None: + """Test function calling without parameters.""" + agent_id = mock_config_entry_with_assist.entry_id + context = Context() + + mock_tool = AsyncMock() + mock_tool.name = "test_tool" + mock_tool.description = "Test function" + mock_tool.parameters = vol.Schema({}) + + mock_get_tools.return_value = [mock_tool] + + with patch("google.generativeai.GenerativeModel") as mock_model: + mock_chat = AsyncMock() + mock_model.return_value.start_chat.return_value = mock_chat + chat_response = MagicMock() + mock_chat.send_message_async.return_value = chat_response + mock_part = MagicMock() + mock_part.function_call = FunctionCall(name="test_tool", args={}) + + def tool_call(hass, tool_input, tool_context): + mock_part.function_call = None + mock_part.text = "Hi there!" + return {"result": "Test response"} + + mock_tool.async_call.side_effect = tool_call + chat_response.parts = [mock_part] + result = await conversation.async_converse( + hass, + "Please call the test function", + None, + context, + agent_id=agent_id, + device_id="test_device", + ) + + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.as_dict()["speech"]["plain"]["speech"] == "Hi there!" + mock_tool_call = mock_chat.send_message_async.mock_calls[1][1][0] + mock_tool_call = type(mock_tool_call).to_dict(mock_tool_call) + assert mock_tool_call == { + "parts": [ + { + "function_response": { + "name": "test_tool", + "response": { + "result": "Test response", + }, + }, + }, + ], + "role": "", + } + + mock_tool.async_call.assert_awaited_once_with( + hass, + llm.ToolInput( + tool_name="test_tool", + tool_args={}, + ), + llm.LLMContext( + platform="google_generative_ai_conversation", + context=context, + user_prompt="Please call the test function", + language="en", + assistant="conversation", + device_id="test_device", + ), + ) + assert [tuple(mock_call) for mock_call in mock_model.mock_calls] == snapshot + + @patch( "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" ) From d0ce0d562e175b0afd599b645bb4668c9f0fe1a8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:34:26 +0200 Subject: [PATCH 0409/2411] Improve type hints in flo tests (#120730) --- tests/components/flo/conftest.py | 2 +- tests/components/flo/test_binary_sensor.py | 7 ++++++- tests/components/flo/test_config_flow.py | 5 ++++- tests/components/flo/test_device.py | 11 ++++++----- tests/components/flo/test_init.py | 9 ++++++--- tests/components/flo/test_sensor.py | 12 +++++++----- tests/components/flo/test_services.py | 5 +++-- tests/components/flo/test_switch.py | 7 ++++++- 8 files changed, 39 insertions(+), 19 deletions(-) diff --git a/tests/components/flo/conftest.py b/tests/components/flo/conftest.py index 33d467a2abf..66b56d1f10b 100644 --- a/tests/components/flo/conftest.py +++ b/tests/components/flo/conftest.py @@ -16,7 +16,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker @pytest.fixture -def config_entry(hass): +def config_entry() -> MockConfigEntry: """Config entry version 1 fixture.""" return MockConfigEntry( domain=FLO_DOMAIN, diff --git a/tests/components/flo/test_binary_sensor.py b/tests/components/flo/test_binary_sensor.py index d3032cde1b5..23a84734b0d 100644 --- a/tests/components/flo/test_binary_sensor.py +++ b/tests/components/flo/test_binary_sensor.py @@ -1,5 +1,7 @@ """Test Flo by Moen binary sensor entities.""" +import pytest + from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN from homeassistant.const import ( ATTR_FRIENDLY_NAME, @@ -13,9 +15,12 @@ from homeassistant.setup import async_setup_component from .common import TEST_PASSWORD, TEST_USER_ID +from tests.common import MockConfigEntry + +@pytest.mark.usefixtures("aioclient_mock_fixture") async def test_binary_sensors( - hass: HomeAssistant, config_entry, aioclient_mock_fixture + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test Flo by Moen sensors.""" config_entry.add_to_hass(hass) diff --git a/tests/components/flo/test_config_flow.py b/tests/components/flo/test_config_flow.py index 99f8f315fb2..f9237e979a6 100644 --- a/tests/components/flo/test_config_flow.py +++ b/tests/components/flo/test_config_flow.py @@ -5,6 +5,8 @@ import json import time from unittest.mock import patch +import pytest + from homeassistant import config_entries from homeassistant.components.flo.const import DOMAIN from homeassistant.const import CONTENT_TYPE_JSON @@ -16,7 +18,8 @@ from .common import TEST_EMAIL_ADDRESS, TEST_PASSWORD, TEST_TOKEN, TEST_USER_ID from tests.test_util.aiohttp import AiohttpClientMocker -async def test_form(hass: HomeAssistant, aioclient_mock_fixture) -> None: +@pytest.mark.usefixtures("aioclient_mock_fixture") +async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/flo/test_device.py b/tests/components/flo/test_device.py index 6248bdcd8f9..c3e26e77370 100644 --- a/tests/components/flo/test_device.py +++ b/tests/components/flo/test_device.py @@ -5,6 +5,7 @@ from unittest.mock import patch from aioflo.errors import RequestError from freezegun.api import FrozenDateTimeFactory +import pytest from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN from homeassistant.components.flo.coordinator import FloDeviceDataUpdateCoordinator @@ -14,14 +15,14 @@ from homeassistant.setup import async_setup_component from .common import TEST_PASSWORD, TEST_USER_ID -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker +@pytest.mark.usefixtures("aioclient_mock_fixture") async def test_device( hass: HomeAssistant, - config_entry, - aioclient_mock_fixture, + config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, freezer: FrozenDateTimeFactory, ) -> None: @@ -90,10 +91,10 @@ async def test_device( assert aioclient_mock.call_count == call_count + 6 +@pytest.mark.usefixtures("aioclient_mock_fixture") async def test_device_failures( hass: HomeAssistant, - config_entry, - aioclient_mock_fixture, + config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, freezer: FrozenDateTimeFactory, ) -> None: diff --git a/tests/components/flo/test_init.py b/tests/components/flo/test_init.py index 599a91b80fb..805a6278395 100644 --- a/tests/components/flo/test_init.py +++ b/tests/components/flo/test_init.py @@ -1,5 +1,7 @@ """Test init.""" +import pytest + from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -7,10 +9,11 @@ from homeassistant.setup import async_setup_component from .common import TEST_PASSWORD, TEST_USER_ID +from tests.common import MockConfigEntry -async def test_setup_entry( - hass: HomeAssistant, config_entry, aioclient_mock_fixture -) -> None: + +@pytest.mark.usefixtures("aioclient_mock_fixture") +async def test_setup_entry(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: """Test migration of config entry from v1.""" config_entry.add_to_hass(hass) assert await async_setup_component( diff --git a/tests/components/flo/test_sensor.py b/tests/components/flo/test_sensor.py index 5fe388c62e1..0c763927296 100644 --- a/tests/components/flo/test_sensor.py +++ b/tests/components/flo/test_sensor.py @@ -1,5 +1,7 @@ """Test Flo by Moen sensor entities.""" +import pytest + from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.const import ATTR_ENTITY_ID, CONF_PASSWORD, CONF_USERNAME @@ -9,12 +11,12 @@ from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from .common import TEST_PASSWORD, TEST_USER_ID +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker -async def test_sensors( - hass: HomeAssistant, config_entry, aioclient_mock_fixture -) -> None: +@pytest.mark.usefixtures("aioclient_mock_fixture") +async def test_sensors(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: """Test Flo by Moen sensors.""" hass.config.units = US_CUSTOMARY_SYSTEM config_entry.add_to_hass(hass) @@ -85,10 +87,10 @@ async def test_sensors( ) +@pytest.mark.usefixtures("aioclient_mock_fixture") async def test_manual_update_entity( hass: HomeAssistant, - config_entry, - aioclient_mock_fixture, + config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, ) -> None: """Test manual update entity via service homeasasistant/update_entity.""" diff --git a/tests/components/flo/test_services.py b/tests/components/flo/test_services.py index d8837d9c6b6..565f39f69fe 100644 --- a/tests/components/flo/test_services.py +++ b/tests/components/flo/test_services.py @@ -19,15 +19,16 @@ from homeassistant.setup import async_setup_component from .common import TEST_PASSWORD, TEST_USER_ID +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker SWITCH_ENTITY_ID = "switch.smart_water_shutoff_shutoff_valve" +@pytest.mark.usefixtures("aioclient_mock_fixture") async def test_services( hass: HomeAssistant, - config_entry, - aioclient_mock_fixture, + config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, ) -> None: """Test Flo services.""" diff --git a/tests/components/flo/test_switch.py b/tests/components/flo/test_switch.py index 85f7ea0f317..02ab93f9e67 100644 --- a/tests/components/flo/test_switch.py +++ b/tests/components/flo/test_switch.py @@ -1,5 +1,7 @@ """Tests for the switch domain for Flo by Moen.""" +import pytest + from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN from homeassistant.components.switch import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_OFF, STATE_ON @@ -8,9 +10,12 @@ from homeassistant.setup import async_setup_component from .common import TEST_PASSWORD, TEST_USER_ID +from tests.common import MockConfigEntry + +@pytest.mark.usefixtures("aioclient_mock_fixture") async def test_valve_switches( - hass: HomeAssistant, config_entry, aioclient_mock_fixture + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test Flo by Moen valve switches.""" config_entry.add_to_hass(hass) From f69b850b1a1599163a8493e0a11ca539ed924fdd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 13:35:34 +0200 Subject: [PATCH 0410/2411] Bump xiaomi-ble to 0.30.1 (#120743) --- homeassistant/components/xiaomi_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/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 1e0a09015ee..f901b9b412e 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.30.0"] + "requirements": ["xiaomi-ble==0.30.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3ae1b3556fc..fffd5b1cdd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2906,7 +2906,7 @@ wyoming==1.5.4 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.30.0 +xiaomi-ble==0.30.1 # homeassistant.components.knx xknx==2.12.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfa58855981..7128c02e43c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2268,7 +2268,7 @@ wyoming==1.5.4 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.30.0 +xiaomi-ble==0.30.1 # homeassistant.components.knx xknx==2.12.2 From d427dff68d22b3a1d29ec9fd24c936020e561b03 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:35:55 +0200 Subject: [PATCH 0411/2411] Improve type hints in forked_daapd tests (#120735) --- tests/components/forked_daapd/conftest.py | 2 +- .../forked_daapd/test_browse_media.py | 26 +++-- .../forked_daapd/test_config_flow.py | 16 ++- .../forked_daapd/test_media_player.py | 98 +++++++++++-------- 4 files changed, 88 insertions(+), 54 deletions(-) diff --git a/tests/components/forked_daapd/conftest.py b/tests/components/forked_daapd/conftest.py index b9dd7087aef..e9f315c030c 100644 --- a/tests/components/forked_daapd/conftest.py +++ b/tests/components/forked_daapd/conftest.py @@ -10,7 +10,7 @@ from tests.common import MockConfigEntry @pytest.fixture(name="config_entry") -def config_entry_fixture(): +def config_entry_fixture() -> MockConfigEntry: """Create hass config_entry fixture.""" data = { CONF_HOST: "192.168.1.1", diff --git a/tests/components/forked_daapd/test_browse_media.py b/tests/components/forked_daapd/test_browse_media.py index 805bcac3976..cbd278128ae 100644 --- a/tests/components/forked_daapd/test_browse_media.py +++ b/tests/components/forked_daapd/test_browse_media.py @@ -3,8 +3,6 @@ from http import HTTPStatus from unittest.mock import patch -import pytest - from homeassistant.components import media_source, spotify from homeassistant.components.forked_daapd.browse_media import ( MediaContent, @@ -19,13 +17,16 @@ from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry from tests.typing import ClientSessionGenerator, WebSocketGenerator TEST_MASTER_ENTITY_NAME = "media_player.owntone_server" async def test_async_browse_media( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, config_entry + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + config_entry: MockConfigEntry, ) -> None: """Test browse media.""" @@ -203,7 +204,9 @@ async def test_async_browse_media( async def test_async_browse_media_not_found( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, config_entry + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + config_entry: MockConfigEntry, ) -> None: """Test browse media not found.""" @@ -261,7 +264,9 @@ async def test_async_browse_media_not_found( async def test_async_browse_spotify( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, config_entry + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + config_entry: MockConfigEntry, ) -> None: """Test browsing spotify.""" @@ -313,7 +318,9 @@ async def test_async_browse_spotify( async def test_async_browse_media_source( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, config_entry + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + config_entry: MockConfigEntry, ) -> None: """Test browsing media_source.""" @@ -361,7 +368,9 @@ async def test_async_browse_media_source( async def test_async_browse_image( - hass: HomeAssistant, hass_client: ClientSessionGenerator, config_entry + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + config_entry: MockConfigEntry, ) -> None: """Test browse media images.""" @@ -416,8 +425,7 @@ async def test_async_browse_image( async def test_async_browse_image_missing( hass: HomeAssistant, hass_client: ClientSessionGenerator, - config_entry, - caplog: pytest.LogCaptureFixture, + config_entry: MockConfigEntry, ) -> None: """Test browse media images with no image available.""" diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index 593b527009b..076fffef59b 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -67,7 +67,7 @@ async def test_show_form(hass: HomeAssistant) -> None: assert result["step_id"] == "user" -async def test_config_flow(hass: HomeAssistant, config_entry) -> None: +async def test_config_flow(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: """Test that the user step works.""" with ( patch( @@ -102,7 +102,9 @@ async def test_config_flow(hass: HomeAssistant, config_entry) -> None: assert result["type"] is FlowResultType.ABORT -async def test_zeroconf_updates_title(hass: HomeAssistant, config_entry) -> None: +async def test_zeroconf_updates_title( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: """Test that zeroconf updates title and aborts with same host.""" MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "different host"}).add_to_hass(hass) config_entry.add_to_hass(hass) @@ -125,7 +127,9 @@ async def test_zeroconf_updates_title(hass: HomeAssistant, config_entry) -> None assert len(hass.config_entries.async_entries(DOMAIN)) == 2 -async def test_config_flow_no_websocket(hass: HomeAssistant, config_entry) -> None: +async def test_config_flow_no_websocket( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: """Test config flow setup without websocket enabled on server.""" with patch( "homeassistant.components.forked_daapd.config_flow.ForkedDaapdAPI.test_connection", @@ -224,7 +228,7 @@ async def test_config_flow_zeroconf_valid(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM -async def test_options_flow(hass: HomeAssistant, config_entry) -> None: +async def test_options_flow(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: """Test config flow options.""" with patch( @@ -251,7 +255,9 @@ async def test_options_flow(hass: HomeAssistant, config_entry) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY -async def test_async_setup_entry_not_ready(hass: HomeAssistant, config_entry) -> None: +async def test_async_setup_entry_not_ready( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: """Test that a PlatformNotReady exception is thrown during platform setup.""" with patch( diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index dd2e03f435f..6d7d267eb63 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -1,6 +1,7 @@ """The media player tests for the forked_daapd media player platform.""" -from unittest.mock import patch +from typing import Any +from unittest.mock import Mock, patch import pytest @@ -63,9 +64,9 @@ from homeassistant.const import ( STATE_PAUSED, STATE_UNAVAILABLE, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceResponse -from tests.common import async_mock_signal +from tests.common import MockConfigEntry, async_mock_signal TEST_MASTER_ENTITY_NAME = "media_player.owntone_server" TEST_ZONE_ENTITY_NAMES = [ @@ -288,7 +289,7 @@ SAMPLE_PLAYLISTS = [{"id": 7, "name": "test_playlist", "uri": "library:playlist: @pytest.fixture(name="get_request_return_values") -async def get_request_return_values_fixture(): +async def get_request_return_values_fixture() -> dict[str, Any]: """Get request return values we can change later.""" return { "config": SAMPLE_CONFIG, @@ -299,7 +300,11 @@ async def get_request_return_values_fixture(): @pytest.fixture(name="mock_api_object") -async def mock_api_object_fixture(hass, config_entry, get_request_return_values): +async def mock_api_object_fixture( + hass: HomeAssistant, + config_entry: MockConfigEntry, + get_request_return_values: dict[str, Any], +) -> Mock: """Create mock api fixture.""" async def get_request_side_effect(update_type): @@ -341,8 +346,9 @@ async def mock_api_object_fixture(hass, config_entry, get_request_return_values) return mock_api.return_value +@pytest.mark.usefixtures("mock_api_object") async def test_unload_config_entry( - hass: HomeAssistant, config_entry, mock_api_object + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test the player is set unavailable when the config entry is unloaded.""" assert hass.states.get(TEST_MASTER_ENTITY_NAME) @@ -352,7 +358,8 @@ async def test_unload_config_entry( assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]).state == STATE_UNAVAILABLE -def test_master_state(hass: HomeAssistant, mock_api_object) -> None: +@pytest.mark.usefixtures("mock_api_object") +def test_master_state(hass: HomeAssistant) -> None: """Test master state attributes.""" state = hass.states.get(TEST_MASTER_ENTITY_NAME) assert state.state == STATE_PAUSED @@ -373,7 +380,7 @@ def test_master_state(hass: HomeAssistant, mock_api_object) -> None: async def test_no_update_when_get_request_returns_none( - hass: HomeAssistant, config_entry, mock_api_object + hass: HomeAssistant, config_entry: MockConfigEntry, mock_api_object: Mock ) -> None: """Test when get request returns None.""" @@ -399,8 +406,12 @@ async def test_no_update_when_get_request_returns_none( async def _service_call( - hass, entity_name, service, additional_service_data=None, blocking=True -): + hass: HomeAssistant, + entity_name: str, + service: str, + additional_service_data: dict[str, Any] | None = None, + blocking: bool = True, +) -> ServiceResponse: if additional_service_data is None: additional_service_data = {} return await hass.services.async_call( @@ -411,7 +422,7 @@ async def _service_call( ) -async def test_zone(hass: HomeAssistant, mock_api_object) -> None: +async def test_zone(hass: HomeAssistant, mock_api_object: Mock) -> None: """Test zone attributes and methods.""" zone_entity_name = TEST_ZONE_ENTITY_NAMES[0] state = hass.states.get(zone_entity_name) @@ -450,7 +461,7 @@ async def test_zone(hass: HomeAssistant, mock_api_object) -> None: mock_api_object.change_output.assert_any_call(output_id, selected=True) -async def test_last_outputs_master(hass: HomeAssistant, mock_api_object) -> None: +async def test_last_outputs_master(hass: HomeAssistant, mock_api_object: Mock) -> None: """Test restoration of _last_outputs.""" # Test turning on sends API call await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_TURN_ON) @@ -467,7 +478,9 @@ async def test_last_outputs_master(hass: HomeAssistant, mock_api_object) -> None async def test_bunch_of_stuff_master( - hass: HomeAssistant, get_request_return_values, mock_api_object + hass: HomeAssistant, + get_request_return_values: dict[str, Any], + mock_api_object: Mock, ) -> None: """Run bunch of stuff.""" await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_TURN_ON) @@ -551,9 +564,8 @@ async def test_bunch_of_stuff_master( mock_api_object.clear_queue.assert_called_once() -async def test_async_play_media_from_paused( - hass: HomeAssistant, mock_api_object -) -> None: +@pytest.mark.usefixtures("mock_api_object") +async def test_async_play_media_from_paused(hass: HomeAssistant) -> None: """Test async play media from paused.""" initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) await _service_call( @@ -571,7 +583,9 @@ async def test_async_play_media_from_paused( async def test_async_play_media_announcement_from_stopped( - hass: HomeAssistant, get_request_return_values, mock_api_object + hass: HomeAssistant, + get_request_return_values: dict[str, Any], + mock_api_object: Mock, ) -> None: """Test async play media announcement (from stopped).""" updater_update = mock_api_object.start_websocket_handler.call_args[0][2] @@ -597,9 +611,8 @@ async def test_async_play_media_announcement_from_stopped( assert state.last_updated > initial_state.last_updated -async def test_async_play_media_unsupported( - hass: HomeAssistant, mock_api_object -) -> None: +@pytest.mark.usefixtures("mock_api_object") +async def test_async_play_media_unsupported(hass: HomeAssistant) -> None: """Test async play media on unsupported media type.""" initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) await _service_call( @@ -616,7 +629,7 @@ async def test_async_play_media_unsupported( async def test_async_play_media_announcement_tts_timeout( - hass: HomeAssistant, mock_api_object + hass: HomeAssistant, mock_api_object: Mock ) -> None: """Test async play media announcement with TTS timeout.""" mock_api_object.add_to_queue.side_effect = None @@ -638,7 +651,7 @@ async def test_async_play_media_announcement_tts_timeout( async def test_use_pipe_control_with_no_api( - hass: HomeAssistant, mock_api_object + hass: HomeAssistant, mock_api_object: Mock ) -> None: """Test using pipe control with no api set.""" await _service_call( @@ -651,7 +664,8 @@ async def test_use_pipe_control_with_no_api( assert mock_api_object.start_playback.call_count == 0 -async def test_clear_source(hass: HomeAssistant, mock_api_object) -> None: +@pytest.mark.usefixtures("mock_api_object") +async def test_clear_source(hass: HomeAssistant) -> None: """Test changing source to clear.""" await _service_call( hass, @@ -665,8 +679,11 @@ async def test_clear_source(hass: HomeAssistant, mock_api_object) -> None: @pytest.fixture(name="pipe_control_api_object") async def pipe_control_api_object_fixture( - hass, config_entry, get_request_return_values, mock_api_object -): + hass: HomeAssistant, + config_entry: MockConfigEntry, + get_request_return_values: dict[str, Any], + mock_api_object: Mock, +) -> Mock: """Fixture for mock librespot_java api.""" with patch( "homeassistant.components.forked_daapd.media_player.LibrespotJavaAPI", @@ -697,9 +714,9 @@ async def pipe_control_api_object_fixture( async def test_librespot_java_stuff( hass: HomeAssistant, - get_request_return_values, - mock_api_object, - pipe_control_api_object, + get_request_return_values: dict[str, Any], + mock_api_object: Mock, + pipe_control_api_object: Mock, ) -> None: """Test options update and librespot-java stuff.""" state = hass.states.get(TEST_MASTER_ENTITY_NAME) @@ -734,9 +751,8 @@ async def test_librespot_java_stuff( assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "some album" -async def test_librespot_java_play_announcement( - hass: HomeAssistant, pipe_control_api_object -) -> None: +@pytest.mark.usefixtures("pipe_control_api_object") +async def test_librespot_java_play_announcement(hass: HomeAssistant) -> None: """Test play announcement with librespot-java pipe.""" initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) await _service_call( @@ -755,7 +771,7 @@ async def test_librespot_java_play_announcement( async def test_librespot_java_play_media_pause_timeout( - hass: HomeAssistant, pipe_control_api_object + hass: HomeAssistant, pipe_control_api_object: Mock ) -> None: """Test play media with librespot-java pipe.""" # test media play with pause timeout @@ -778,7 +794,7 @@ async def test_librespot_java_play_media_pause_timeout( assert state.last_updated > initial_state.last_updated -async def test_unsupported_update(hass: HomeAssistant, mock_api_object) -> None: +async def test_unsupported_update(hass: HomeAssistant, mock_api_object: Mock) -> None: """Test unsupported update type.""" last_updated = hass.states.get(TEST_MASTER_ENTITY_NAME).last_updated updater_update = mock_api_object.start_websocket_handler.call_args[0][2] @@ -787,7 +803,9 @@ async def test_unsupported_update(hass: HomeAssistant, mock_api_object) -> None: assert hass.states.get(TEST_MASTER_ENTITY_NAME).last_updated == last_updated -async def test_invalid_websocket_port(hass: HomeAssistant, config_entry) -> None: +async def test_invalid_websocket_port( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: """Test invalid websocket port on async_init.""" with patch( "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI", @@ -800,7 +818,7 @@ async def test_invalid_websocket_port(hass: HomeAssistant, config_entry) -> None assert hass.states.get(TEST_MASTER_ENTITY_NAME).state == STATE_UNAVAILABLE -async def test_websocket_disconnect(hass: HomeAssistant, mock_api_object) -> None: +async def test_websocket_disconnect(hass: HomeAssistant, mock_api_object: Mock) -> None: """Test websocket disconnection.""" assert hass.states.get(TEST_MASTER_ENTITY_NAME).state != STATE_UNAVAILABLE assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]).state != STATE_UNAVAILABLE @@ -811,7 +829,9 @@ async def test_websocket_disconnect(hass: HomeAssistant, mock_api_object) -> Non assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]).state == STATE_UNAVAILABLE -async def test_async_play_media_enqueue(hass: HomeAssistant, mock_api_object) -> None: +async def test_async_play_media_enqueue( + hass: HomeAssistant, mock_api_object: Mock +) -> None: """Test async play media with different enqueue options.""" initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) await _service_call( @@ -887,7 +907,7 @@ async def test_async_play_media_enqueue(hass: HomeAssistant, mock_api_object) -> ) -async def test_play_owntone_media(hass: HomeAssistant, mock_api_object) -> None: +async def test_play_owntone_media(hass: HomeAssistant, mock_api_object: Mock) -> None: """Test async play media with an owntone source.""" initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) await _service_call( @@ -913,7 +933,7 @@ async def test_play_owntone_media(hass: HomeAssistant, mock_api_object) -> None: ) -async def test_play_spotify_media(hass: HomeAssistant, mock_api_object) -> None: +async def test_play_spotify_media(hass: HomeAssistant, mock_api_object: Mock) -> None: """Test async play media with a spotify source.""" initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) await _service_call( @@ -937,7 +957,7 @@ async def test_play_spotify_media(hass: HomeAssistant, mock_api_object) -> None: ) -async def test_play_media_source(hass: HomeAssistant, mock_api_object) -> None: +async def test_play_media_source(hass: HomeAssistant, mock_api_object: Mock) -> None: """Test async play media with a spotify source.""" initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) with patch( From c7906f90a31dd03819d34b7c84b14299ec80aa2b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:36:26 +0200 Subject: [PATCH 0412/2411] Improve type hints in frontend tests (#120739) --- tests/components/frontend/test_init.py | 89 +++++++++++++---------- tests/components/frontend/test_storage.py | 10 +-- 2 files changed, 53 insertions(+), 46 deletions(-) diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 83c82abea35..501f9c482f2 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -10,6 +10,7 @@ from unittest.mock import patch from aiohttp.test_utils import TestClient from freezegun.api import FrozenDateTimeFactory import pytest +from typing_extensions import Generator from homeassistant.components.frontend import ( CONF_EXTRA_JS_URL_ES5, @@ -64,7 +65,7 @@ CONFIG_THEMES = {DOMAIN: {CONF_THEMES: MOCK_THEMES}} @pytest.fixture -async def ignore_frontend_deps(hass): +async def ignore_frontend_deps(hass: HomeAssistant) -> None: """Frontend dependencies.""" frontend = await async_get_integration(hass, "frontend") for dep in frontend.dependencies: @@ -73,7 +74,7 @@ async def ignore_frontend_deps(hass): @pytest.fixture -async def frontend(hass, ignore_frontend_deps): +async def frontend(hass: HomeAssistant, ignore_frontend_deps: None) -> None: """Frontend setup with themes.""" assert await async_setup_component( hass, @@ -83,7 +84,7 @@ async def frontend(hass, ignore_frontend_deps): @pytest.fixture -async def frontend_themes(hass): +async def frontend_themes(hass: HomeAssistant) -> None: """Frontend setup with themes.""" assert await async_setup_component( hass, @@ -104,7 +105,7 @@ def aiohttp_client( @pytest.fixture async def mock_http_client( - hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, frontend + hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, frontend: None ) -> TestClient: """Start the Home Assistant HTTP component.""" return await aiohttp_client(hass.http.app) @@ -112,7 +113,7 @@ async def mock_http_client( @pytest.fixture async def themes_ws_client( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, frontend_themes + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, frontend_themes: None ) -> MockHAClientWebSocket: """Start the Home Assistant HTTP component.""" return await hass_ws_client(hass) @@ -120,7 +121,7 @@ async def themes_ws_client( @pytest.fixture async def ws_client( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, frontend + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, frontend: None ) -> MockHAClientWebSocket: """Start the Home Assistant HTTP component.""" return await hass_ws_client(hass) @@ -128,7 +129,9 @@ async def ws_client( @pytest.fixture async def mock_http_client_with_extra_js( - hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, ignore_frontend_deps + hass: HomeAssistant, + aiohttp_client: ClientSessionGenerator, + ignore_frontend_deps: None, ) -> TestClient: """Start the Home Assistant HTTP component.""" assert await async_setup_component( @@ -145,7 +148,7 @@ async def mock_http_client_with_extra_js( @pytest.fixture -def mock_onboarded(): +def mock_onboarded() -> Generator[None]: """Mock that we're onboarded.""" with patch( "homeassistant.components.onboarding.async_is_onboarded", return_value=True @@ -153,7 +156,8 @@ def mock_onboarded(): yield -async def test_frontend_and_static(mock_http_client, mock_onboarded) -> None: +@pytest.mark.usefixtures("mock_onboarded") +async def test_frontend_and_static(mock_http_client: TestClient) -> None: """Test if we can get the frontend.""" resp = await mock_http_client.get("") assert resp.status == 200 @@ -170,26 +174,28 @@ async def test_frontend_and_static(mock_http_client, mock_onboarded) -> None: assert "public" in resp.headers.get("cache-control") -async def test_dont_cache_service_worker(mock_http_client) -> None: +async def test_dont_cache_service_worker(mock_http_client: TestClient) -> None: """Test that we don't cache the service worker.""" resp = await mock_http_client.get("/service_worker.js") assert resp.status == 200 assert "cache-control" not in resp.headers -async def test_404(mock_http_client) -> None: +async def test_404(mock_http_client: TestClient) -> None: """Test for HTTP 404 error.""" resp = await mock_http_client.get("/not-existing") assert resp.status == HTTPStatus.NOT_FOUND -async def test_we_cannot_POST_to_root(mock_http_client) -> None: +async def test_we_cannot_POST_to_root(mock_http_client: TestClient) -> None: """Test that POST is not allow to root.""" resp = await mock_http_client.post("/") assert resp.status == 405 -async def test_themes_api(hass: HomeAssistant, themes_ws_client) -> None: +async def test_themes_api( + hass: HomeAssistant, themes_ws_client: MockHAClientWebSocket +) -> None: """Test that /api/themes returns correct data.""" await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"}) msg = await themes_ws_client.receive_json() @@ -216,11 +222,11 @@ async def test_themes_api(hass: HomeAssistant, themes_ws_client) -> None: assert msg["result"]["themes"] == {} +@pytest.mark.usefixtures("ignore_frontend_deps") async def test_themes_persist( hass: HomeAssistant, hass_storage: dict[str, Any], hass_ws_client: WebSocketGenerator, - ignore_frontend_deps, ) -> None: """Test that theme settings are restores after restart.""" hass_storage[THEMES_STORAGE_KEY] = { @@ -242,11 +248,11 @@ async def test_themes_persist( assert msg["result"]["default_dark_theme"] == "dark" +@pytest.mark.usefixtures("frontend_themes") async def test_themes_save_storage( hass: HomeAssistant, hass_storage: dict[str, Any], freezer: FrozenDateTimeFactory, - frontend_themes, ) -> None: """Test that theme settings are restores after restart.""" @@ -270,7 +276,9 @@ async def test_themes_save_storage( } -async def test_themes_set_theme(hass: HomeAssistant, themes_ws_client) -> None: +async def test_themes_set_theme( + hass: HomeAssistant, themes_ws_client: MockHAClientWebSocket +) -> None: """Test frontend.set_theme service.""" await hass.services.async_call( DOMAIN, "set_theme", {"name": "happy"}, blocking=True @@ -303,7 +311,7 @@ async def test_themes_set_theme(hass: HomeAssistant, themes_ws_client) -> None: async def test_themes_set_theme_wrong_name( - hass: HomeAssistant, themes_ws_client + hass: HomeAssistant, themes_ws_client: MockHAClientWebSocket ) -> None: """Test frontend.set_theme service called with wrong name.""" @@ -318,7 +326,9 @@ async def test_themes_set_theme_wrong_name( assert msg["result"]["default_theme"] == "default" -async def test_themes_set_dark_theme(hass: HomeAssistant, themes_ws_client) -> None: +async def test_themes_set_dark_theme( + hass: HomeAssistant, themes_ws_client: MockHAClientWebSocket +) -> None: """Test frontend.set_theme service called with dark mode.""" await hass.services.async_call( @@ -358,8 +368,9 @@ async def test_themes_set_dark_theme(hass: HomeAssistant, themes_ws_client) -> N assert msg["result"]["default_dark_theme"] == "light_and_dark" +@pytest.mark.usefixtures("frontend") async def test_themes_set_dark_theme_wrong_name( - hass: HomeAssistant, frontend, themes_ws_client + hass: HomeAssistant, themes_ws_client: MockHAClientWebSocket ) -> None: """Test frontend.set_theme service called with mode dark and wrong name.""" await hass.services.async_call( @@ -373,8 +384,9 @@ async def test_themes_set_dark_theme_wrong_name( assert msg["result"]["default_dark_theme"] is None +@pytest.mark.usefixtures("frontend") async def test_themes_reload_themes( - hass: HomeAssistant, frontend, themes_ws_client + hass: HomeAssistant, themes_ws_client: MockHAClientWebSocket ) -> None: """Test frontend.reload_themes service.""" @@ -395,7 +407,7 @@ async def test_themes_reload_themes( assert msg["result"]["default_theme"] == "default" -async def test_missing_themes(hass: HomeAssistant, ws_client) -> None: +async def test_missing_themes(ws_client: MockHAClientWebSocket) -> None: """Test that themes API works when themes are not defined.""" await ws_client.send_json({"id": 5, "type": "frontend/get_themes"}) @@ -412,7 +424,7 @@ async def test_missing_themes(hass: HomeAssistant, ws_client) -> None: async def test_extra_js( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - mock_http_client_with_extra_js, + mock_http_client_with_extra_js: TestClient, ) -> None: """Test that extra javascript is loaded.""" @@ -497,7 +509,7 @@ async def test_extra_js( async def test_get_panels( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - mock_http_client, + mock_http_client: TestClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test get_panels command.""" @@ -547,7 +559,7 @@ async def test_get_panels( async def test_get_panels_non_admin( - hass: HomeAssistant, ws_client, hass_admin_user: MockUser + hass: HomeAssistant, ws_client: MockHAClientWebSocket, hass_admin_user: MockUser ) -> None: """Test get_panels command.""" hass_admin_user.groups = [] @@ -568,7 +580,7 @@ async def test_get_panels_non_admin( assert "map" not in msg["result"] -async def test_get_translations(hass: HomeAssistant, ws_client) -> None: +async def test_get_translations(ws_client: MockHAClientWebSocket) -> None: """Test get_translations command.""" with patch( "homeassistant.components.frontend.async_get_translations", @@ -593,7 +605,7 @@ async def test_get_translations(hass: HomeAssistant, ws_client) -> None: async def test_get_translations_for_integrations( - hass: HomeAssistant, ws_client + ws_client: MockHAClientWebSocket, ) -> None: """Test get_translations for integrations command.""" with patch( @@ -621,7 +633,7 @@ async def test_get_translations_for_integrations( async def test_get_translations_for_single_integration( - hass: HomeAssistant, ws_client + ws_client: MockHAClientWebSocket, ) -> None: """Test get_translations for integration command.""" with patch( @@ -660,7 +672,7 @@ async def test_onboarding_load(hass: HomeAssistant) -> None: assert "onboarding" in frontend.dependencies -async def test_auth_authorize(mock_http_client) -> None: +async def test_auth_authorize(mock_http_client: TestClient) -> None: """Test the authorize endpoint works.""" resp = await mock_http_client.get( "/auth/authorize?response_type=code&client_id=https://localhost/&" @@ -683,7 +695,9 @@ async def test_auth_authorize(mock_http_client) -> None: assert "public" in resp.headers.get("cache-control") -async def test_get_version(hass: HomeAssistant, ws_client) -> None: +async def test_get_version( + hass: HomeAssistant, ws_client: MockHAClientWebSocket +) -> None: """Test get_version command.""" frontend = await async_get_integration(hass, "frontend") cur_version = next( @@ -701,7 +715,7 @@ async def test_get_version(hass: HomeAssistant, ws_client) -> None: assert msg["result"] == {"version": cur_version} -async def test_static_paths(hass: HomeAssistant, mock_http_client) -> None: +async def test_static_paths(mock_http_client: TestClient) -> None: """Test static paths.""" resp = await mock_http_client.get( "/.well-known/change-password", allow_redirects=False @@ -710,9 +724,8 @@ async def test_static_paths(hass: HomeAssistant, mock_http_client) -> None: assert resp.headers["location"] == "/profile" -async def test_manifest_json( - hass: HomeAssistant, frontend_themes, mock_http_client -) -> None: +@pytest.mark.usefixtures("frontend_themes") +async def test_manifest_json(hass: HomeAssistant, mock_http_client: TestClient) -> None: """Test for fetching manifest.json.""" resp = await mock_http_client.get("/manifest.json") assert resp.status == HTTPStatus.OK @@ -734,7 +747,7 @@ async def test_manifest_json( assert json["theme_color"] != DEFAULT_THEME_COLOR -async def test_static_path_cache(hass: HomeAssistant, mock_http_client) -> None: +async def test_static_path_cache(mock_http_client: TestClient) -> None: """Test static paths cache.""" resp = await mock_http_client.get("/lovelace/default_view", allow_redirects=False) assert resp.status == 404 @@ -766,7 +779,7 @@ async def test_static_path_cache(hass: HomeAssistant, mock_http_client) -> None: assert resp.status == 404 -async def test_get_icons(hass: HomeAssistant, ws_client: MockHAClientWebSocket) -> None: +async def test_get_icons(ws_client: MockHAClientWebSocket) -> None: """Test get_icons command.""" with patch( "homeassistant.components.frontend.async_get_icons", @@ -787,9 +800,7 @@ async def test_get_icons(hass: HomeAssistant, ws_client: MockHAClientWebSocket) assert msg["result"] == {"resources": {}} -async def test_get_icons_for_integrations( - hass: HomeAssistant, ws_client: MockHAClientWebSocket -) -> None: +async def test_get_icons_for_integrations(ws_client: MockHAClientWebSocket) -> None: """Test get_icons for integrations command.""" with patch( "homeassistant.components.frontend.async_get_icons", @@ -814,7 +825,7 @@ async def test_get_icons_for_integrations( async def test_get_icons_for_single_integration( - hass: HomeAssistant, ws_client: MockHAClientWebSocket + ws_client: MockHAClientWebSocket, ) -> None: """Test get_icons for integration command.""" with patch( diff --git a/tests/components/frontend/test_storage.py b/tests/components/frontend/test_storage.py index 8b97fa9ee04..ce7f7aeb4a1 100644 --- a/tests/components/frontend/test_storage.py +++ b/tests/components/frontend/test_storage.py @@ -13,15 +13,13 @@ from tests.typing import WebSocketGenerator @pytest.fixture(autouse=True) -def setup_frontend(hass): +def setup_frontend(hass: HomeAssistant) -> None: """Fixture to setup the frontend.""" hass.loop.run_until_complete(async_setup_component(hass, "frontend", {})) async def test_get_user_data_empty( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - hass_storage: dict[str, Any], + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test get_user_data command.""" client = await hass_ws_client(hass) @@ -82,9 +80,7 @@ async def test_get_user_data( async def test_set_user_data_empty( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - hass_storage: dict[str, Any], + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test set_user_data command.""" client = await hass_ws_client(hass) From d7a59748cf7d80b0922a7e24578a42b5ade63504 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 Jun 2024 13:38:24 +0200 Subject: [PATCH 0413/2411] Bump version to 2024.7.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 33a86f57a5e..1ab2a3f6893 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index e4ccd9898e0..e96c329fd5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b3" +version = "2024.7.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 1fdd056c0ec25948405660a2175cc4fa4183cfed Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 14:17:47 +0200 Subject: [PATCH 0414/2411] Fix ruff manual-dict-comprehension PERF403 (#120723) * Fix PERF403 * Fix * Fix --- homeassistant/auth/__init__.py | 18 ++++----- homeassistant/auth/auth_store.py | 20 ++++++---- .../components/assist_pipeline/pipeline.py | 34 +++++++++------- homeassistant/components/blueprint/models.py | 3 +- homeassistant/components/cloud/prefs.py | 40 ++++++++++--------- .../components/gdacs/geo_location.py | 34 ++++++++-------- homeassistant/components/gdacs/sensor.py | 26 ++++++------ .../geonetnz_quakes/geo_location.py | 26 ++++++------ .../components/geonetnz_quakes/sensor.py | 26 ++++++------ .../components/geonetnz_volcano/sensor.py | 28 ++++++------- .../components/google_assistant/smart_home.py | 10 +++-- homeassistant/components/html5/notify.py | 5 +-- .../components/ign_sismologia/geo_location.py | 24 +++++------ .../components/kaiterra/air_quality.py | 21 +++++----- .../components/kraken/config_flow.py | 10 +++-- .../components/microsoft_face/__init__.py | 6 +-- .../geo_location.py | 32 +++++++-------- .../components/qld_bushfire/geo_location.py | 22 +++++----- .../usgs_earthquakes_feed/geo_location.py | 28 ++++++------- homeassistant/components/zwave_js/lock.py | 22 +++++----- homeassistant/helpers/area_registry.py | 22 +++++----- 21 files changed, 234 insertions(+), 223 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 665bc308d49..b74fd587fab 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -363,15 +363,15 @@ class AuthManager: local_only: bool | None = None, ) -> None: """Update a user.""" - kwargs: dict[str, Any] = {} - - for attr_name, value in ( - ("name", name), - ("group_ids", group_ids), - ("local_only", local_only), - ): - if value is not None: - kwargs[attr_name] = value + kwargs: dict[str, Any] = { + attr_name: value + for attr_name, value in ( + ("name", name), + ("group_ids", group_ids), + ("local_only", local_only), + ) + if value is not None + } await self._store.async_update_user(user, **kwargs) if is_active is not None: diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 7843cb58df2..fc47a7d71e9 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -105,14 +105,18 @@ class AuthStore: "perm_lookup": self._perm_lookup, } - for attr_name, value in ( - ("is_owner", is_owner), - ("is_active", is_active), - ("local_only", local_only), - ("system_generated", system_generated), - ): - if value is not None: - kwargs[attr_name] = value + kwargs.update( + { + attr_name: value + for attr_name, value in ( + ("is_owner", is_owner), + ("is_active", is_active), + ("local_only", local_only), + ("system_generated", system_generated), + ) + if value is not None + } + ) new_user = models.User(**kwargs) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 56f88f60104..068afe53b49 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -304,21 +304,25 @@ async def async_update_pipeline( updates.pop("id") # Refactor this once we bump to Python 3.12 # and have https://peps.python.org/pep-0692/ - for key, val in ( - ("conversation_engine", conversation_engine), - ("conversation_language", conversation_language), - ("language", language), - ("name", name), - ("stt_engine", stt_engine), - ("stt_language", stt_language), - ("tts_engine", tts_engine), - ("tts_language", tts_language), - ("tts_voice", tts_voice), - ("wake_word_entity", wake_word_entity), - ("wake_word_id", wake_word_id), - ): - if val is not UNDEFINED: - updates[key] = val + updates.update( + { + key: val + for key, val in ( + ("conversation_engine", conversation_engine), + ("conversation_language", conversation_language), + ("language", language), + ("name", name), + ("stt_engine", stt_engine), + ("stt_language", stt_language), + ("tts_engine", tts_engine), + ("tts_language", tts_language), + ("tts_voice", tts_voice), + ("wake_word_entity", wake_word_entity), + ("wake_word_id", wake_word_id), + ) + if val is not UNDEFINED + } + ) await pipeline_data.pipeline_store.async_update_item(pipeline.id, updates) diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 414d4e55a9b..01d26de618d 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -99,8 +99,7 @@ class Blueprint: inputs = {} for key, value in self.data[CONF_BLUEPRINT][CONF_INPUT].items(): if value and CONF_INPUT in value: - for key, value in value[CONF_INPUT].items(): - inputs[key] = value + inputs.update(dict(value[CONF_INPUT])) else: inputs[key] = value return inputs diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index af4e68194d6..9b7f863c368 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -180,24 +180,28 @@ class CloudPreferences: """Update user preferences.""" prefs = {**self._prefs} - for key, value in ( - (PREF_ENABLE_GOOGLE, google_enabled), - (PREF_ENABLE_ALEXA, alexa_enabled), - (PREF_ENABLE_REMOTE, remote_enabled), - (PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin), - (PREF_CLOUDHOOKS, cloudhooks), - (PREF_CLOUD_USER, cloud_user), - (PREF_ALEXA_REPORT_STATE, alexa_report_state), - (PREF_GOOGLE_REPORT_STATE, google_report_state), - (PREF_ALEXA_SETTINGS_VERSION, alexa_settings_version), - (PREF_GOOGLE_SETTINGS_VERSION, google_settings_version), - (PREF_TTS_DEFAULT_VOICE, tts_default_voice), - (PREF_REMOTE_DOMAIN, remote_domain), - (PREF_GOOGLE_CONNECTED, google_connected), - (PREF_REMOTE_ALLOW_REMOTE_ENABLE, remote_allow_remote_enable), - ): - if value is not UNDEFINED: - prefs[key] = value + prefs.update( + { + key: value + for key, value in ( + (PREF_ENABLE_GOOGLE, google_enabled), + (PREF_ENABLE_ALEXA, alexa_enabled), + (PREF_ENABLE_REMOTE, remote_enabled), + (PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin), + (PREF_CLOUDHOOKS, cloudhooks), + (PREF_CLOUD_USER, cloud_user), + (PREF_ALEXA_REPORT_STATE, alexa_report_state), + (PREF_GOOGLE_REPORT_STATE, google_report_state), + (PREF_ALEXA_SETTINGS_VERSION, alexa_settings_version), + (PREF_GOOGLE_SETTINGS_VERSION, google_settings_version), + (PREF_TTS_DEFAULT_VOICE, tts_default_voice), + (PREF_REMOTE_DOMAIN, remote_domain), + (PREF_GOOGLE_CONNECTED, google_connected), + (PREF_REMOTE_ALLOW_REMOTE_ENABLE, remote_allow_remote_enable), + ) + if value is not UNDEFINED + } + ) await self._save_prefs(prefs) diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index 394c9086d71..3f693241b24 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -188,20 +188,20 @@ class GdacsEvent(GeolocationEvent): @property 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_EVENT_TYPE, self._event_type), - (ATTR_ALERT_LEVEL, self._alert_level), - (ATTR_COUNTRY, self._country), - (ATTR_DURATION_IN_WEEK, self._duration_in_week), - (ATTR_FROM_DATE, self._from_date), - (ATTR_TO_DATE, self._to_date), - (ATTR_POPULATION, self._population), - (ATTR_SEVERITY, self._severity), - (ATTR_VULNERABILITY, self._vulnerability), - ): - if value or isinstance(value, bool): - attributes[key] = value - return attributes + return { + key: value + for key, value in ( + (ATTR_EXTERNAL_ID, self._external_id), + (ATTR_DESCRIPTION, self._description), + (ATTR_EVENT_TYPE, self._event_type), + (ATTR_ALERT_LEVEL, self._alert_level), + (ATTR_COUNTRY, self._country), + (ATTR_DURATION_IN_WEEK, self._duration_in_week), + (ATTR_FROM_DATE, self._from_date), + (ATTR_TO_DATE, self._to_date), + (ATTR_POPULATION, self._population), + (ATTR_SEVERITY, self._severity), + (ATTR_VULNERABILITY, self._vulnerability), + ) + if value or isinstance(value, bool) + } diff --git a/homeassistant/components/gdacs/sensor.py b/homeassistant/components/gdacs/sensor.py index 90ea668ea3f..c8205730da4 100644 --- a/homeassistant/components/gdacs/sensor.py +++ b/homeassistant/components/gdacs/sensor.py @@ -133,16 +133,16 @@ class GdacsSensor(SensorEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" - attributes: dict[str, Any] = {} - for key, value in ( - (ATTR_STATUS, self._status), - (ATTR_LAST_UPDATE, self._last_update), - (ATTR_LAST_UPDATE_SUCCESSFUL, self._last_update_successful), - (ATTR_LAST_TIMESTAMP, self._last_timestamp), - (ATTR_CREATED, self._created), - (ATTR_UPDATED, self._updated), - (ATTR_REMOVED, self._removed), - ): - if value or isinstance(value, bool): - attributes[key] = value - return attributes + return { + key: value + for key, value in ( + (ATTR_STATUS, self._status), + (ATTR_LAST_UPDATE, self._last_update), + (ATTR_LAST_UPDATE_SUCCESSFUL, self._last_update_successful), + (ATTR_LAST_TIMESTAMP, self._last_timestamp), + (ATTR_CREATED, self._created), + (ATTR_UPDATED, self._updated), + (ATTR_REMOVED, self._removed), + ) + if value or isinstance(value, bool) + } diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index f3458d96a18..78313e102e0 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -156,16 +156,16 @@ class GeonetnzQuakesEvent(GeolocationEvent): @property 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_DEPTH, self._depth), - (ATTR_LOCALITY, self._locality), - (ATTR_MAGNITUDE, self._magnitude), - (ATTR_MMI, self._mmi), - (ATTR_QUALITY, self._quality), - (ATTR_TIME, self._time), - ): - if value or isinstance(value, bool): - attributes[key] = value - return attributes + return { + key: value + for key, value in ( + (ATTR_EXTERNAL_ID, self._external_id), + (ATTR_DEPTH, self._depth), + (ATTR_LOCALITY, self._locality), + (ATTR_MAGNITUDE, self._magnitude), + (ATTR_MMI, self._mmi), + (ATTR_QUALITY, self._quality), + (ATTR_TIME, self._time), + ) + if value or isinstance(value, bool) + } diff --git a/homeassistant/components/geonetnz_quakes/sensor.py b/homeassistant/components/geonetnz_quakes/sensor.py index 020b76a6c97..2fce3e93d12 100644 --- a/homeassistant/components/geonetnz_quakes/sensor.py +++ b/homeassistant/components/geonetnz_quakes/sensor.py @@ -137,16 +137,16 @@ class GeonetnzQuakesSensor(SensorEntity): @property def extra_state_attributes(self): """Return the device state attributes.""" - attributes = {} - for key, value in ( - (ATTR_STATUS, self._status), - (ATTR_LAST_UPDATE, self._last_update), - (ATTR_LAST_UPDATE_SUCCESSFUL, self._last_update_successful), - (ATTR_LAST_TIMESTAMP, self._last_timestamp), - (ATTR_CREATED, self._created), - (ATTR_UPDATED, self._updated), - (ATTR_REMOVED, self._removed), - ): - if value or isinstance(value, bool): - attributes[key] = value - return attributes + return { + key: value + for key, value in ( + (ATTR_STATUS, self._status), + (ATTR_LAST_UPDATE, self._last_update), + (ATTR_LAST_UPDATE_SUCCESSFUL, self._last_update_successful), + (ATTR_LAST_TIMESTAMP, self._last_timestamp), + (ATTR_CREATED, self._created), + (ATTR_UPDATED, self._updated), + (ATTR_REMOVED, self._removed), + ) + if value or isinstance(value, bool) + } diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py index 6197577b56c..980679cc64f 100644 --- a/homeassistant/components/geonetnz_volcano/sensor.py +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -154,17 +154,17 @@ class GeonetnzVolcanoSensor(SensorEntity): @property def extra_state_attributes(self): """Return the device state attributes.""" - attributes = {} - for key, value in ( - (ATTR_EXTERNAL_ID, self._external_id), - (ATTR_ACTIVITY, self._activity), - (ATTR_HAZARDS, self._hazards), - (ATTR_LONGITUDE, self._longitude), - (ATTR_LATITUDE, self._latitude), - (ATTR_DISTANCE, self._distance), - (ATTR_LAST_UPDATE, self._feed_last_update), - (ATTR_LAST_UPDATE_SUCCESSFUL, self._feed_last_update_successful), - ): - if value or isinstance(value, bool): - attributes[key] = value - return attributes + return { + key: value + for key, value in ( + (ATTR_EXTERNAL_ID, self._external_id), + (ATTR_ACTIVITY, self._activity), + (ATTR_HAZARDS, self._hazards), + (ATTR_LONGITUDE, self._longitude), + (ATTR_LATITUDE, self._latitude), + (ATTR_DISTANCE, self._distance), + (ATTR_LAST_UPDATE, self._feed_last_update), + (ATTR_LAST_UPDATE_SUCCESSFUL, self._feed_last_update_successful), + ) + if value or isinstance(value, bool) + } diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index e362d1121c2..06b1a0251d8 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -262,9 +262,13 @@ async def handle_devices_execute( ), EXECUTE_LIMIT, ) - for entity_id, result in zip(executions, execute_results, strict=False): - if result is not None: - results[entity_id] = result + results.update( + { + entity_id: result + for entity_id, result in zip(executions, execute_results, strict=False) + if result is not None + } + ) except TimeoutError: pass diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index cc03202ae88..798589d2807 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -426,10 +426,7 @@ class HTML5NotificationService(BaseNotificationService): @property def targets(self): """Return a dictionary of registered targets.""" - targets = {} - for registration in self.registrations: - targets[registration] = registration - return targets + return {registration: registration for registration in self.registrations} def dismiss(self, **kwargs): """Dismisses a notification.""" diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index 779891f4bc2..7076d6a77a9 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -224,15 +224,15 @@ class IgnSismologiaLocationEvent(GeolocationEvent): @property 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_TITLE, self._title), - (ATTR_REGION, self._region), - (ATTR_MAGNITUDE, self._magnitude), - (ATTR_PUBLICATION_DATE, self._publication_date), - (ATTR_IMAGE_URL, self._image_url), - ): - if value or isinstance(value, bool): - attributes[key] = value - return attributes + return { + key: value + for key, value in ( + (ATTR_EXTERNAL_ID, self._external_id), + (ATTR_TITLE, self._title), + (ATTR_REGION, self._region), + (ATTR_MAGNITUDE, self._magnitude), + (ATTR_PUBLICATION_DATE, self._publication_date), + (ATTR_IMAGE_URL, self._image_url), + ) + if value or isinstance(value, bool) + } diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py index 4d0d83a38eb..97553d6bda6 100644 --- a/homeassistant/components/kaiterra/air_quality.py +++ b/homeassistant/components/kaiterra/air_quality.py @@ -106,18 +106,15 @@ class KaiterraAirQuality(AirQualityEntity): @property def extra_state_attributes(self): """Return the device state attributes.""" - data = {} - attributes = [ - (ATTR_VOC, self.volatile_organic_compounds), - (ATTR_AQI_LEVEL, self.air_quality_index_level), - (ATTR_AQI_POLLUTANT, self.air_quality_index_pollutant), - ] - - for attr, value in attributes: - if value is not None: - data[attr] = value - - return data + return { + attr: value + for attr, value in ( + (ATTR_VOC, self.volatile_organic_compounds), + (ATTR_AQI_LEVEL, self.air_quality_index_level), + (ATTR_AQI_POLLUTANT, self.air_quality_index_pollutant), + ) + if value is not None + } async def async_added_to_hass(self): """Register callback.""" diff --git a/homeassistant/components/kraken/config_flow.py b/homeassistant/components/kraken/config_flow.py index 93c3c6606a3..67778515273 100644 --- a/homeassistant/components/kraken/config_flow.py +++ b/homeassistant/components/kraken/config_flow.py @@ -75,10 +75,12 @@ class KrakenOptionsFlowHandler(OptionsFlow): tracked_asset_pairs = self.config_entry.options.get( CONF_TRACKED_ASSET_PAIRS, [] ) - for tracked_asset_pair in tracked_asset_pairs: - tradable_asset_pairs_for_multi_select[tracked_asset_pair] = ( - tracked_asset_pair - ) + tradable_asset_pairs_for_multi_select.update( + { + tracked_asset_pair: tracked_asset_pair + for tracked_asset_pair in tracked_asset_pairs + } + ) options = { vol.Optional( diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 23535911e5c..fa4de7f9c99 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -244,11 +244,7 @@ class MicrosoftFaceGroupEntity(Entity): @property def extra_state_attributes(self): """Return device specific state attributes.""" - attr = {} - for name, p_id in self._api.store[self._id].items(): - attr[name] = p_id - - return attr + return dict(self._api.store[self._id]) class MicrosoftFace: 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 230141379e5..98efa90d780 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -269,19 +269,19 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): @property 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_PUBLICATION_DATE, self._publication_date), - (ATTR_COUNCIL_AREA, self._council_area), - (ATTR_STATUS, self._status), - (ATTR_TYPE, self._type), - (ATTR_FIRE, self._fire), - (ATTR_SIZE, self._size), - (ATTR_RESPONSIBLE_AGENCY, self._responsible_agency), - ): - if value or isinstance(value, bool): - attributes[key] = value - return attributes + return { + key: value + for key, value in ( + (ATTR_EXTERNAL_ID, self._external_id), + (ATTR_CATEGORY, self._category), + (ATTR_LOCATION, self._location), + (ATTR_PUBLICATION_DATE, self._publication_date), + (ATTR_COUNCIL_AREA, self._council_area), + (ATTR_STATUS, self._status), + (ATTR_TYPE, self._type), + (ATTR_FIRE, self._fire), + (ATTR_SIZE, self._size), + (ATTR_RESPONSIBLE_AGENCY, self._responsible_agency), + ) + if value or isinstance(value, bool) + } diff --git a/homeassistant/components/qld_bushfire/geo_location.py b/homeassistant/components/qld_bushfire/geo_location.py index c8cfc30b2b5..c1266ab951b 100644 --- a/homeassistant/components/qld_bushfire/geo_location.py +++ b/homeassistant/components/qld_bushfire/geo_location.py @@ -223,14 +223,14 @@ class QldBushfireLocationEvent(GeolocationEvent): @property 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_PUBLICATION_DATE, self._publication_date), - (ATTR_UPDATED_DATE, self._updated_date), - (ATTR_STATUS, self._status), - ): - if value or isinstance(value, bool): - attributes[key] = value - return attributes + return { + key: value + for key, value in ( + (ATTR_EXTERNAL_ID, self._external_id), + (ATTR_CATEGORY, self._category), + (ATTR_PUBLICATION_DATE, self._publication_date), + (ATTR_UPDATED_DATE, self._updated_date), + (ATTR_STATUS, self._status), + ) + if value or isinstance(value, bool) + } diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index 33455dc11a9..aa9817eab7d 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -276,17 +276,17 @@ class UsgsEarthquakesEvent(GeolocationEvent): @property 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_PLACE, self._place), - (ATTR_MAGNITUDE, self._magnitude), - (ATTR_TIME, self._time), - (ATTR_UPDATED, self._updated), - (ATTR_STATUS, self._status), - (ATTR_TYPE, self._type), - (ATTR_ALERT, self._alert), - ): - if value or isinstance(value, bool): - attributes[key] = value - return attributes + return { + key: value + for key, value in ( + (ATTR_EXTERNAL_ID, self._external_id), + (ATTR_PLACE, self._place), + (ATTR_MAGNITUDE, self._magnitude), + (ATTR_TIME, self._time), + (ATTR_UPDATED, self._updated), + (ATTR_STATUS, self._status), + (ATTR_TYPE, self._type), + (ATTR_ALERT, self._alert), + ) + if value or isinstance(value, bool) + } diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index 5eb89e17402..b16c1090ef3 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -196,15 +196,19 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity): ) -> None: """Set the lock configuration.""" params: dict[str, Any] = {"operation_type": operation_type} - for attr, val in ( - ("lock_timeout_configuration", lock_timeout), - ("auto_relock_time", auto_relock_time), - ("hold_and_release_time", hold_and_release_time), - ("twist_assist", twist_assist), - ("block_to_block", block_to_block), - ): - if val is not None: - params[attr] = val + params.update( + { + attr: val + for attr, val in ( + ("lock_timeout_configuration", lock_timeout), + ("auto_relock_time", auto_relock_time), + ("hold_and_release_time", hold_and_release_time), + ("twist_assist", twist_assist), + ("block_to_block", block_to_block), + ) + if val is not None + } + ) configuration = DoorLockCCConfigurationSetOptions(**params) result = await set_configuration( self.info.node.endpoints[self.info.primary_value.endpoint or 0], diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 975750ebbdd..e6862428389 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -315,17 +315,17 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): """Update name of area.""" old = self.areas[area_id] - new_values = {} - - for attr_name, value in ( - ("aliases", aliases), - ("icon", icon), - ("labels", labels), - ("picture", picture), - ("floor_id", floor_id), - ): - if value is not UNDEFINED and value != getattr(old, attr_name): - new_values[attr_name] = value + new_values = { + attr_name: value + for attr_name, value in ( + ("aliases", aliases), + ("icon", icon), + ("labels", labels), + ("picture", picture), + ("floor_id", floor_id), + ) + if value is not UNDEFINED and value != getattr(old, attr_name) + } if name is not UNDEFINED and name != old.name: new_values["name"] = name From 984bbf885198ff4d47a1d083806ef136bd7a7e6a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 14:53:29 +0200 Subject: [PATCH 0415/2411] Bump sense-energy to 0.12.4 (#120744) * Bump sense-energy to 0.12.4 * Fix --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index 843aeddde7b..640a2113d6f 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_push", "loggers": ["sense_energy"], "quality_scale": "internal", - "requirements": ["sense-energy==0.12.2"] + "requirements": ["sense-energy==0.12.4"] } diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 7ef1caefe48..116b714ba82 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/sense", "iot_class": "cloud_polling", "loggers": ["sense_energy"], - "requirements": ["sense-energy==0.12.2"] + "requirements": ["sense-energy==0.12.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index fffd5b1cdd2..234bd302a7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2539,7 +2539,7 @@ sendgrid==6.8.2 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.12.2 +sense-energy==0.12.4 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7128c02e43c..e30ee616601 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1976,7 +1976,7 @@ securetar==2024.2.1 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.12.2 +sense-energy==0.12.4 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 From b56c4a757caf1aafebdf4e85a055487f3df35468 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 15:23:21 +0200 Subject: [PATCH 0416/2411] Bump ruff to 0.5.0 (#120749) --- .pre-commit-config.yaml | 2 +- homeassistant/components/command_line/notify.py | 4 ++-- homeassistant/components/weatherflow_cloud/weather.py | 1 - pyproject.toml | 3 ++- requirements_test_pre_commit.txt | 2 +- tests/components/v2c/test_sensor.py | 4 ++-- tests/components/websocket_api/test_connection.py | 3 +-- tests/conftest.py | 2 +- tests/util/test_process.py | 4 ++-- 9 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 023f917d89c..195500f148e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.9 + rev: v0.5.0 hooks: - id: ruff args: diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 1d025726583..14245b72288 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -42,12 +42,12 @@ class CommandLineNotificationService(BaseNotificationService): def send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message to a command line.""" - with subprocess.Popen( + with subprocess.Popen( # noqa: S602 # shell by design self.command, universal_newlines=True, stdin=subprocess.PIPE, close_fds=False, # required for posix_spawn - shell=True, # noqa: S602 # shell by design + shell=True, ) as proc: try: proc.communicate(input=message, timeout=self._timeout) diff --git a/homeassistant/components/weatherflow_cloud/weather.py b/homeassistant/components/weatherflow_cloud/weather.py index 47e2b6a28df..424a4df4c8e 100644 --- a/homeassistant/components/weatherflow_cloud/weather.py +++ b/homeassistant/components/weatherflow_cloud/weather.py @@ -98,7 +98,6 @@ class WeatherFlowWeather( """Return the Air Pressure @ Station.""" return self.local_data.weather.current_conditions.station_pressure - # @property def humidity(self) -> float | None: """Return the humidity.""" diff --git a/pyproject.toml b/pyproject.toml index f81013aa8b5..d09e4b13d69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -185,6 +185,7 @@ disable = [ "bidirectional-unicode", # PLE2502 "continue-in-finally", # PLE0116 "duplicate-bases", # PLE0241 + "misplaced-bare-raise", # PLE0704 "format-needs-mapping", # F502 "function-redefined", # F811 # Needed because ruff does not understand type of __all__ generated by a function @@ -675,7 +676,7 @@ filterwarnings = [ ] [tool.ruff] -required-version = ">=0.4.8" +required-version = ">=0.5.0" [tool.ruff.lint] select = [ diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index a7e5c20d86c..c6e629cd129 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.3.0 -ruff==0.4.9 +ruff==0.5.0 yamllint==1.35.1 diff --git a/tests/components/v2c/test_sensor.py b/tests/components/v2c/test_sensor.py index 9e7e3800767..430f91647dd 100644 --- a/tests/components/v2c/test_sensor.py +++ b/tests/components/v2c/test_sensor.py @@ -28,7 +28,7 @@ async def test_sensor( await init_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - assert [ + assert _METER_ERROR_OPTIONS == [ "no_error", "communication", "reading", @@ -64,4 +64,4 @@ async def test_sensor( "tcp_head_mismatch", "empty_message", "undefined_error", - ] == _METER_ERROR_OPTIONS + ] diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index d6c2765522e..5740bb48019 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -2,7 +2,7 @@ import logging from typing import Any -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import Mock, patch from aiohttp.test_utils import make_mocked_request import pytest @@ -75,7 +75,6 @@ async def test_exception_handling( send_messages = [] user = MockUser() refresh_token = Mock() - current_request = AsyncMock() hass.data[DOMAIN] = {} def get_extra_info(key: str) -> Any: diff --git a/tests/conftest.py b/tests/conftest.py index 161ff458ac0..6f85a7da06e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -892,7 +892,7 @@ def fail_on_log_exception( return def log_exception(format_err, *args): - raise # pylint: disable=misplaced-bare-raise + raise # noqa: PLE0704 monkeypatch.setattr("homeassistant.util.logging.log_exception", log_exception) diff --git a/tests/util/test_process.py b/tests/util/test_process.py index ae28f5d82fc..c6125b656a5 100644 --- a/tests/util/test_process.py +++ b/tests/util/test_process.py @@ -10,9 +10,9 @@ from homeassistant.util import process async def test_kill_process() -> None: """Test killing a process.""" - sleeper = subprocess.Popen( + sleeper = subprocess.Popen( # noqa: S602 # shell by design "sleep 1000", - shell=True, # noqa: S602 # shell by design + shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) From 3e3ab7a134f0b828097ce4e0623e133318d3a7d6 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 16:14:31 +0200 Subject: [PATCH 0417/2411] Bump easyenergy lib to v2.1.2 (#120753) --- homeassistant/components/easyenergy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/easyenergy/manifest.json b/homeassistant/components/easyenergy/manifest.json index 4dcce0fd705..4d45dc2d399 100644 --- a/homeassistant/components/easyenergy/manifest.json +++ b/homeassistant/components/easyenergy/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/easyenergy", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["easyenergy==2.1.1"] + "requirements": ["easyenergy==2.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 234bd302a7d..084c69d3ba8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -771,7 +771,7 @@ dynalite-panel==0.0.4 eagle100==0.1.1 # homeassistant.components.easyenergy -easyenergy==2.1.1 +easyenergy==2.1.2 # homeassistant.components.ebusd ebusdpy==0.0.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e30ee616601..726c68fa59b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -643,7 +643,7 @@ dynalite-panel==0.0.4 eagle100==0.1.1 # homeassistant.components.easyenergy -easyenergy==2.1.1 +easyenergy==2.1.2 # homeassistant.components.electric_kiwi electrickiwi-api==0.8.5 From 9505fcdd7d652704ff96a7c05582676c0bfc02af Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 16:20:44 +0200 Subject: [PATCH 0418/2411] Bump p1monitor lib to v3.0.1 (#120756) --- homeassistant/components/p1_monitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/p1_monitor/manifest.json b/homeassistant/components/p1_monitor/manifest.json index 0dfe1f3a46c..4702de3546d 100644 --- a/homeassistant/components/p1_monitor/manifest.json +++ b/homeassistant/components/p1_monitor/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["p1monitor"], "quality_scale": "platinum", - "requirements": ["p1monitor==3.0.0"] + "requirements": ["p1monitor==3.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 084c69d3ba8..6962f44a0a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1519,7 +1519,7 @@ ourgroceries==1.5.4 ovoenergy==2.0.0 # homeassistant.components.p1_monitor -p1monitor==3.0.0 +p1monitor==3.0.1 # homeassistant.components.mqtt paho-mqtt==1.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 726c68fa59b..e7939b69977 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1222,7 +1222,7 @@ ourgroceries==1.5.4 ovoenergy==2.0.0 # homeassistant.components.p1_monitor -p1monitor==3.0.0 +p1monitor==3.0.1 # homeassistant.components.mqtt paho-mqtt==1.6.1 From d03a6f84a34e1f83231f8a446ae2396b9969cad5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 16:21:32 +0200 Subject: [PATCH 0419/2411] Bump govee-local-api to 1.5.1 (#120747) --- homeassistant/components/govee_light_local/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_light_local/manifest.json b/homeassistant/components/govee_light_local/manifest.json index 93a19408182..168a13e2477 100644 --- a/homeassistant/components/govee_light_local/manifest.json +++ b/homeassistant/components/govee_light_local/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/govee_light_local", "iot_class": "local_push", - "requirements": ["govee-local-api==1.5.0"] + "requirements": ["govee-local-api==1.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6962f44a0a2..0816d8c06df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -995,7 +995,7 @@ gotailwind==0.2.3 govee-ble==0.31.3 # homeassistant.components.govee_light_local -govee-local-api==1.5.0 +govee-local-api==1.5.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7939b69977..bdd69e261c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -821,7 +821,7 @@ gotailwind==0.2.3 govee-ble==0.31.3 # homeassistant.components.govee_light_local -govee-local-api==1.5.0 +govee-local-api==1.5.1 # homeassistant.components.gpsd gps3==0.33.3 From a8f4684929b0244106d750d07916498de2fb2b96 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 28 Jun 2024 16:22:25 +0200 Subject: [PATCH 0420/2411] Cleanup mqtt platform tests part 6 (last) (#120736) --- .../mqtt/test_alarm_control_panel.py | 37 ++++------------ tests/components/mqtt/test_binary_sensor.py | 17 ++------ tests/components/mqtt/test_button.py | 12 +----- tests/components/mqtt/test_climate.py | 12 +----- tests/components/mqtt/test_device_tracker.py | 6 +-- tests/components/mqtt/test_event.py | 12 +----- tests/components/mqtt/test_fan.py | 18 ++------ tests/components/mqtt/test_humidifier.py | 12 +----- tests/components/mqtt/test_image.py | 12 +----- tests/components/mqtt/test_lawn_mower.py | 12 +----- tests/components/mqtt/test_light.py | 12 +----- tests/components/mqtt/test_light_json.py | 12 +----- tests/components/mqtt/test_light_template.py | 12 +----- tests/components/mqtt/test_lock.py | 18 ++------ tests/components/mqtt/test_notify.py | 12 +----- tests/components/mqtt/test_number.py | 12 +----- tests/components/mqtt/test_scene.py | 12 +----- tests/components/mqtt/test_select.py | 6 +-- tests/components/mqtt/test_sensor.py | 30 ++++--------- tests/components/mqtt/test_siren.py | 33 ++++----------- tests/components/mqtt/test_subscription.py | 22 +++------- tests/components/mqtt/test_switch.py | 21 +++------- tests/components/mqtt/test_tag.py | 31 ++++---------- tests/components/mqtt/test_text.py | 42 ++++--------------- tests/components/mqtt/test_update.py | 18 ++------ tests/components/mqtt/test_vacuum.py | 39 +++-------------- tests/components/mqtt/test_valve.py | 41 ++++-------------- tests/components/mqtt/test_water_heater.py | 28 +++---------- 28 files changed, 109 insertions(+), 442 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index aba2d5f6da2..ff60b782571 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -853,10 +853,7 @@ async def test_availability_without_topic( ) -> None: """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, - mqtt_mock_entry, - alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE, + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE ) @@ -865,10 +862,7 @@ async def test_default_availability_payload( ) -> None: """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, - mqtt_mock_entry, - alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE, + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE ) @@ -877,10 +871,7 @@ async def test_custom_availability_payload( ) -> None: """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, - mqtt_mock_entry, - alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) @@ -889,10 +880,7 @@ async def test_setting_attribute_via_mqtt_json_message( ) -> None: """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, - mqtt_mock_entry, - alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) @@ -914,10 +902,7 @@ async def test_setting_attribute_with_template( ) -> None: """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, - mqtt_mock_entry, - alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) @@ -928,11 +913,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) @@ -943,11 +924,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 6ba479fca74..e1b336000a0 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -758,10 +758,7 @@ async def test_setting_attribute_with_template( ) -> None: """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, - mqtt_mock_entry, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -772,11 +769,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -787,11 +780,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - binary_sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 7e5d748e2ab..7294519c851 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -216,11 +216,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - button.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, button.DOMAIN, DEFAULT_CONFIG ) @@ -231,11 +227,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - button.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, button.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 168fed6164e..8d2d805a899 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -1867,11 +1867,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - climate.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, climate.DOMAIN, DEFAULT_CONFIG ) @@ -1882,11 +1878,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - climate.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, climate.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 9759dfcadd7..00e88860299 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -584,11 +584,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ) -> None: """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, - mqtt_mock_entry, - device_tracker.DOMAIN, - DEFAULT_CONFIG, - None, + hass, mqtt_mock_entry, device_tracker.DOMAIN, DEFAULT_CONFIG, None ) diff --git a/tests/components/mqtt/test_event.py b/tests/components/mqtt/test_event.py index 662a279f639..3d4847a406a 100644 --- a/tests/components/mqtt/test_event.py +++ b/tests/components/mqtt/test_event.py @@ -372,11 +372,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - event.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, event.DOMAIN, DEFAULT_CONFIG ) @@ -387,11 +383,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - event.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, event.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 2d1d717c58f..21c9f1f3e6a 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -1932,11 +1932,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ) -> None: """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, - mqtt_mock_entry, - fan.DOMAIN, - DEFAULT_CONFIG, - MQTT_FAN_ATTRIBUTES_BLOCKED, + hass, mqtt_mock_entry, fan.DOMAIN, DEFAULT_CONFIG, MQTT_FAN_ATTRIBUTES_BLOCKED ) @@ -1956,11 +1952,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - fan.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, fan.DOMAIN, DEFAULT_CONFIG ) @@ -1971,11 +1963,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - fan.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, fan.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 05180c17b2f..62de371af4b 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -1246,11 +1246,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - humidifier.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, humidifier.DOMAIN, DEFAULT_CONFIG ) @@ -1261,11 +1257,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - humidifier.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, humidifier.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_image.py b/tests/components/mqtt/test_image.py index bb029fba231..6f0eb8edf49 100644 --- a/tests/components/mqtt/test_image.py +++ b/tests/components/mqtt/test_image.py @@ -573,11 +573,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - image.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, image.DOMAIN, DEFAULT_CONFIG ) @@ -588,11 +584,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - image.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, image.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_lawn_mower.py b/tests/components/mqtt/test_lawn_mower.py index 120a09deb88..00fd60d59aa 100644 --- a/tests/components/mqtt/test_lawn_mower.py +++ b/tests/components/mqtt/test_lawn_mower.py @@ -442,11 +442,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - lawn_mower.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, lawn_mower.DOMAIN, DEFAULT_CONFIG ) @@ -457,11 +453,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - lawn_mower.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, lawn_mower.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index bfce49b9ecb..46164398d02 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -2492,11 +2492,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, light.DOMAIN, DEFAULT_CONFIG ) @@ -2507,11 +2503,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, light.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 5ab2a32dc83..af43332d625 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -2374,11 +2374,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, light.DOMAIN, DEFAULT_CONFIG ) @@ -2389,11 +2385,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, light.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index aace09f402a..e52b401c135 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -978,11 +978,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, light.DOMAIN, DEFAULT_CONFIG ) @@ -993,11 +989,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - light.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, light.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index c9546bdfdb3..2624be1f1b2 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -757,11 +757,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ) -> None: """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, - mqtt_mock_entry, - lock.DOMAIN, - DEFAULT_CONFIG, - MQTT_LOCK_ATTRIBUTES_BLOCKED, + hass, mqtt_mock_entry, lock.DOMAIN, DEFAULT_CONFIG, MQTT_LOCK_ATTRIBUTES_BLOCKED ) @@ -781,11 +777,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - lock.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, lock.DOMAIN, DEFAULT_CONFIG ) @@ -796,11 +788,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - lock.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, lock.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_notify.py b/tests/components/mqtt/test_notify.py index 540dbbafd99..5e4718e58ea 100644 --- a/tests/components/mqtt/test_notify.py +++ b/tests/components/mqtt/test_notify.py @@ -199,11 +199,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - notify.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, notify.DOMAIN, DEFAULT_CONFIG ) @@ -214,11 +210,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - notify.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, notify.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 2cd5c5390f5..73958def5f7 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -557,11 +557,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - number.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, number.DOMAIN, DEFAULT_CONFIG ) @@ -572,11 +568,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - number.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, number.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 9badd6aeee0..e8beb622e9e 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -263,11 +263,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - scene.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, scene.DOMAIN, DEFAULT_CONFIG ) @@ -278,11 +274,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - scene.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, scene.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 26a64d70fee..afc2b903e93 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -407,11 +407,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - select.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, select.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 94eb049dda7..4b117aaa4d5 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -617,9 +617,7 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message( ], ) async def test_setting_sensor_last_reset_via_mqtt_json_message_2( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the setting of the value via MQTT with JSON payload.""" await hass.async_block_till_done() @@ -810,9 +808,7 @@ async def test_discovery_update_availability( ], ) async def test_invalid_device_class( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test device_class option with invalid value.""" assert await mqtt_mock_entry() @@ -871,9 +867,7 @@ async def test_valid_device_class_and_uom( ], ) async def test_invalid_state_class( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test state_class option with invalid value.""" assert await mqtt_mock_entry() @@ -954,11 +948,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -969,11 +959,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - sensor.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1298,8 +1284,7 @@ async def test_value_template_with_entity_id( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = sensor.DOMAIN @@ -1454,8 +1439,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = sensor.DOMAIN diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index c32c57d4f02..3f720e3ee3c 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -60,9 +60,7 @@ DEFAULT_CONFIG = { async def async_turn_on( - hass: HomeAssistant, - entity_id: str, - parameters: dict[str, Any], + hass: HomeAssistant, entity_id: str, parameters: dict[str, Any] ) -> None: """Turn all or specified siren on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} @@ -180,9 +178,7 @@ async def test_sending_mqtt_commands_and_optimistic( ], ) async def test_controlling_state_via_topic_and_json_message( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the controlling state via topic and JSON message.""" await mqtt_mock_entry() @@ -618,11 +614,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - siren.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, siren.DOMAIN, DEFAULT_CONFIG ) @@ -633,11 +625,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - siren.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, siren.DOMAIN, DEFAULT_CONFIG ) @@ -787,8 +775,7 @@ async def test_discovery_update_siren_template( ], ) async def test_command_templates( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test siren with command templates optimistic.""" mqtt_mock = await mqtt_mock_entry() @@ -1005,8 +992,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = siren.DOMAIN @@ -1016,9 +1002,7 @@ async def test_reloadable( @pytest.mark.parametrize( ("topic", "value", "attribute", "attribute_value"), - [ - ("state_topic", "ON", None, "on"), - ], + [("state_topic", "ON", None, "on")], ) async def test_encoding_subscribable_topics( hass: HomeAssistant, @@ -1056,8 +1040,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = siren.DOMAIN diff --git a/tests/components/mqtt/test_subscription.py b/tests/components/mqtt/test_subscription.py index 7247458a667..86279b2006c 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -2,8 +2,6 @@ from unittest.mock import ANY -import pytest - from homeassistant.components.mqtt.subscription import ( async_prepare_subscribe_topics, async_subscribe_topics, @@ -16,9 +14,7 @@ from tests.typing import MqttMockHAClientGenerator async def test_subscribe_topics( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test subscription to topics.""" await mqtt_mock_entry() @@ -69,9 +65,7 @@ async def test_subscribe_topics( async def test_modify_topics( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test modification of topics.""" await mqtt_mock_entry() @@ -136,9 +130,7 @@ async def test_modify_topics( async def test_qos_encoding_default( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test default qos and encoding.""" mqtt_mock = await mqtt_mock_entry() @@ -158,9 +150,7 @@ async def test_qos_encoding_default( async def test_qos_encoding_custom( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test custom qos and encoding.""" mqtt_mock = await mqtt_mock_entry() @@ -187,9 +177,7 @@ async def test_qos_encoding_custom( async def test_no_change( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test subscription to topics without change.""" mqtt_mock = await mqtt_mock_entry() diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 42d2e092d83..2ccc5dbfcc3 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -379,11 +379,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - switch.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, switch.DOMAIN, DEFAULT_CONFIG ) @@ -394,11 +390,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - switch.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, switch.DOMAIN, DEFAULT_CONFIG ) @@ -524,8 +516,7 @@ async def test_discovery_update_switch_template( async def test_discovery_update_unchanged_switch( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered switch.""" data1 = ( @@ -675,8 +666,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = switch.DOMAIN @@ -726,8 +716,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = switch.DOMAIN diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 0d0765258f2..4cf0606deb8 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -20,7 +20,7 @@ from tests.common import ( async_fire_mqtt_message, async_get_device_automations, ) -from tests.typing import MqttMockHAClient, MqttMockHAClientGenerator, WebSocketGenerator +from tests.typing import MqttMockHAClientGenerator, WebSocketGenerator DEFAULT_CONFIG_DEVICE = { "device": {"identifiers": ["0AFFD2"]}, @@ -102,9 +102,7 @@ async def test_if_fires_on_mqtt_message_with_device( async def test_if_fires_on_mqtt_message_without_device( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock: AsyncMock, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, tag_mock: AsyncMock ) -> None: """Test tag scanning, without device.""" await mqtt_mock_entry() @@ -140,9 +138,7 @@ async def test_if_fires_on_mqtt_message_with_template( async def test_strip_tag_id( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock: AsyncMock, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, tag_mock: AsyncMock ) -> None: """Test strip whitespace from tag_id.""" await mqtt_mock_entry() @@ -208,9 +204,7 @@ async def test_if_fires_on_mqtt_message_after_update_with_device( async def test_if_fires_on_mqtt_message_after_update_without_device( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock: AsyncMock, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, tag_mock: AsyncMock ) -> None: """Test tag scanning after update.""" await mqtt_mock_entry() @@ -359,9 +353,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_with_device( async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_without_device( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock: AsyncMock, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, tag_mock: AsyncMock ) -> None: """Test tag scanning not firing after removal.""" await mqtt_mock_entry() @@ -904,11 +896,9 @@ async def test_update_with_bad_config_not_breaks_discovery( tag_mock.assert_called_once_with(ANY, "12345", ANY) +@pytest.mark.usefixtures("mqtt_mock") async def test_unload_entry( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mqtt_mock: MqttMockHAClient, - tag_mock: AsyncMock, + hass: HomeAssistant, device_registry: dr.DeviceRegistry, tag_mock: AsyncMock ) -> None: """Test unloading the MQTT entry.""" @@ -934,12 +924,9 @@ async def test_unload_entry( tag_mock.assert_not_called() +@pytest.mark.usefixtures("mqtt_mock", "tag_mock") async def test_value_template_fails( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mqtt_mock: MqttMockHAClient, - tag_mock: AsyncMock, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test the rendering of MQTT value template fails.""" config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) diff --git a/tests/components/mqtt/test_text.py b/tests/components/mqtt/test_text.py index fc714efa513..ebcb835844d 100644 --- a/tests/components/mqtt/test_text.py +++ b/tests/components/mqtt/test_text.py @@ -251,9 +251,7 @@ async def test_controlling_validation_state_via_topic( ], ) async def test_attribute_validation_max_greater_then_min( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test the validation of min and max configuration attributes.""" assert await mqtt_mock_entry() @@ -276,9 +274,7 @@ async def test_attribute_validation_max_greater_then_min( ], ) async def test_attribute_validation_max_not_greater_then_max_state_length( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test the max value of of max configuration attribute.""" assert await mqtt_mock_entry() @@ -436,13 +432,7 @@ async def test_default_availability_payload( } } await help_test_default_availability_payload( - hass, - mqtt_mock_entry, - text.DOMAIN, - config, - True, - "state-topic", - "some state", + hass, mqtt_mock_entry, text.DOMAIN, config, True, "state-topic", "some state" ) @@ -461,13 +451,7 @@ async def test_custom_availability_payload( } await help_test_custom_availability_payload( - hass, - mqtt_mock_entry, - text.DOMAIN, - config, - True, - "state-topic", - "1", + hass, mqtt_mock_entry, text.DOMAIN, config, True, "state-topic", "1" ) @@ -505,11 +489,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - text.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, text.DOMAIN, DEFAULT_CONFIG ) @@ -520,11 +500,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - text.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, text.DOMAIN, DEFAULT_CONFIG ) @@ -754,8 +730,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = text.DOMAIN @@ -805,8 +780,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = text.DOMAIN diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py index bb9ae12c66b..937b8cdebd0 100644 --- a/tests/components/mqtt/test_update.py +++ b/tests/components/mqtt/test_update.py @@ -504,11 +504,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - update.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, update.DOMAIN, DEFAULT_CONFIG ) @@ -519,11 +515,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - update.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, update.DOMAIN, DEFAULT_CONFIG ) @@ -679,8 +671,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = update.DOMAIN @@ -691,8 +682,7 @@ async def test_unload_entry( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = update.DOMAIN diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index 8c01138ccb9..a7a5280c3e1 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -507,11 +507,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - vacuum.DOMAIN, - DEFAULT_CONFIG_2, + hass, mqtt_mock_entry, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -522,11 +518,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - vacuum.DOMAIN, - DEFAULT_CONFIG_2, + hass, mqtt_mock_entry, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -682,20 +674,8 @@ async def test_entity_debug_info_message( @pytest.mark.parametrize( ("service", "topic", "parameters", "payload", "template"), [ - ( - vacuum.SERVICE_START, - "command_topic", - None, - "start", - None, - ), - ( - vacuum.SERVICE_CLEAN_SPOT, - "command_topic", - None, - "clean_spot", - None, - ), + (vacuum.SERVICE_START, "command_topic", None, "start", None), + (vacuum.SERVICE_CLEAN_SPOT, "command_topic", None, "clean_spot", None), ( vacuum.SERVICE_SET_FAN_SPEED, "set_fan_speed_topic", @@ -710,13 +690,7 @@ async def test_entity_debug_info_message( "custom command", None, ), - ( - vacuum.SERVICE_STOP, - "command_topic", - None, - "stop", - None, - ), + (vacuum.SERVICE_STOP, "command_topic", None, "stop", None), ], ) async def test_publishing_with_custom_encoding( @@ -760,8 +734,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN diff --git a/tests/components/mqtt/test_valve.py b/tests/components/mqtt/test_valve.py index 6f88e160b73..53a7190eaf3 100644 --- a/tests/components/mqtt/test_valve.py +++ b/tests/components/mqtt/test_valve.py @@ -306,8 +306,7 @@ async def test_state_via_state_topic_through_position( ], ) async def test_opening_closing_state_is_reset( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the controlling state via topic through position. @@ -734,11 +733,7 @@ async def test_controlling_valve_by_position( ) @pytest.mark.parametrize( ("position", "asserted_message"), - [ - (0, "0"), - (30, "30"), - (100, "100"), - ], + [(0, "0"), (30, "30"), (100, "100")], ) async def test_controlling_valve_by_set_valve_position( hass: HomeAssistant, @@ -842,12 +837,7 @@ async def test_controlling_valve_optimistic_by_set_valve_position( ) @pytest.mark.parametrize( ("position", "asserted_message"), - [ - (0, "-128"), - (30, "-52"), - (80, "76"), - (100, "127"), - ], + [(0, "-128"), (30, "-52"), (80, "76"), (100, "127")], ) async def test_controlling_valve_with_alt_range_by_set_valve_position( hass: HomeAssistant, @@ -1127,9 +1117,7 @@ async def test_valid_device_class( ], ) async def test_invalid_device_class( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test the setting of an invalid device class.""" assert await mqtt_mock_entry() @@ -1174,11 +1162,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - valve.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, valve.DOMAIN, DEFAULT_CONFIG ) @@ -1189,17 +1173,12 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - valve.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, valve.DOMAIN, DEFAULT_CONFIG ) async def test_discovery_update_attr( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( @@ -1386,8 +1365,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = valve.DOMAIN @@ -1439,8 +1417,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = valve.DOMAIN diff --git a/tests/components/mqtt/test_water_heater.py b/tests/components/mqtt/test_water_heater.py index 86f8b227bd5..7bab4a5e233 100644 --- a/tests/components/mqtt/test_water_heater.py +++ b/tests/components/mqtt/test_water_heater.py @@ -154,9 +154,7 @@ async def test_get_operation_modes( @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_set_operation_mode_bad_attr_and_state( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting operation mode without required attribute.""" await mqtt_mock_entry() @@ -615,8 +613,7 @@ async def test_get_with_templates( ], ) async def test_set_and_templates( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setting various attributes with templates.""" mqtt_mock = await mqtt_mock_entry() @@ -834,11 +831,7 @@ async def test_update_with_json_attrs_not_dict( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, - mqtt_mock_entry, - caplog, - water_heater.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, water_heater.DOMAIN, DEFAULT_CONFIG ) @@ -849,11 +842,7 @@ async def test_update_with_json_attrs_bad_json( ) -> None: """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_json( - hass, - mqtt_mock_entry, - caplog, - water_heater.DOMAIN, - DEFAULT_CONFIG, + hass, mqtt_mock_entry, caplog, water_heater.DOMAIN, DEFAULT_CONFIG ) @@ -1020,11 +1009,7 @@ async def test_entity_id_update_subscriptions( } } await help_test_entity_id_update_subscriptions( - hass, - mqtt_mock_entry, - water_heater.DOMAIN, - config, - ["test-topic", "avty-topic"], + hass, mqtt_mock_entry, water_heater.DOMAIN, config, ["test-topic", "avty-topic"] ) @@ -1200,8 +1185,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = water_heater.DOMAIN From e907c45981481d937f48a027925aeac2660df34e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 28 Jun 2024 16:22:56 +0200 Subject: [PATCH 0421/2411] Cleanup mqtt platform tests part 5 (#120719) --- .../components/mqtt/test_alarm_control_panel.py | 6 ++---- tests/components/mqtt/test_binary_sensor.py | 3 +-- tests/components/mqtt/test_button.py | 6 ++---- tests/components/mqtt/test_camera.py | 6 ++---- tests/components/mqtt/test_cover.py | 3 +-- tests/components/mqtt/test_lawn_mower.py | 12 ++++-------- tests/components/mqtt/test_light.py | 11 +++-------- tests/components/mqtt/test_light_json.py | 17 ++++------------- tests/components/mqtt/test_light_template.py | 6 ++---- tests/components/mqtt/test_lock.py | 6 ++---- tests/components/mqtt/test_mixins.py | 11 ++++------- tests/components/mqtt/test_notify.py | 3 +-- tests/components/mqtt/test_number.py | 9 +++------ tests/components/mqtt/test_scene.py | 6 ++---- tests/components/mqtt/test_select.py | 10 +++------- 15 files changed, 36 insertions(+), 79 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index ff60b782571..07ebb671e37 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -1236,8 +1236,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = alarm_control_panel.DOMAIN @@ -1260,8 +1259,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = alarm_control_panel.DOMAIN diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index e1b336000a0..e2c168bd46e 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1025,8 +1025,7 @@ async def test_entity_debug_info_message( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = binary_sensor.DOMAIN diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 7294519c851..d85ead6ecee 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -480,8 +480,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = button.DOMAIN @@ -504,8 +503,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = button.DOMAIN diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index d02e19e6063..cda536dc19e 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -389,8 +389,7 @@ async def test_entity_debug_info_message( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = camera.DOMAIN @@ -413,8 +412,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = camera.DOMAIN diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index f37de8b6a2e..451665de96a 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -3461,8 +3461,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = cover.DOMAIN diff --git a/tests/components/mqtt/test_lawn_mower.py b/tests/components/mqtt/test_lawn_mower.py index 00fd60d59aa..4906f6cfda3 100644 --- a/tests/components/mqtt/test_lawn_mower.py +++ b/tests/components/mqtt/test_lawn_mower.py @@ -91,8 +91,7 @@ DEFAULT_CONFIG = { ], ) async def test_run_lawn_mower_setup_and_state_updates( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test that it sets up correctly fetches the given payload.""" await mqtt_mock_entry() @@ -503,8 +502,7 @@ async def test_discovery_removal_lawn_mower( async def test_discovery_update_lawn_mower( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test update of discovered lawn_mower.""" config1 = { @@ -763,8 +761,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = lawn_mower.DOMAIN @@ -818,8 +815,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = lawn_mower.DOMAIN diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 46164398d02..18815281f63 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -2559,9 +2559,7 @@ async def test_discovery_removal_light( async def test_discovery_ignores_extra_keys( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test discovery ignores extra keys that are not blocked.""" await mqtt_mock_entry() @@ -3287,8 +3285,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = light.DOMAIN @@ -3370,7 +3367,6 @@ async def test_encoding_subscribable_topics( async def test_encoding_subscribable_topics_brightness( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, topic: str, value: str, attribute: str, @@ -3582,8 +3578,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = light.DOMAIN diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index af43332d625..829222e0304 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -185,7 +185,6 @@ class JsonValidator: "hass_config", [{mqtt.DOMAIN: {light.DOMAIN: {"schema": "json", "name": "test"}}}] ) async def test_fail_setup_if_no_command_topic( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, ) -> None: @@ -204,7 +203,6 @@ async def test_fail_setup_if_no_command_topic( ], ) async def test_fail_setup_if_color_mode_deprecated( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, ) -> None: @@ -233,7 +231,6 @@ async def test_fail_setup_if_color_mode_deprecated( ids=["color_temp", "hs", "rgb", "xy", "color_temp, rgb"], ) async def test_warning_if_color_mode_flags_are_used( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, color_modes: tuple[str, ...], @@ -316,7 +313,6 @@ async def test_warning_on_discovery_if_color_mode_flags_are_used( ids=["color_temp"], ) async def test_warning_if_color_mode_option_flag_is_used( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, ) -> None: @@ -393,7 +389,6 @@ async def test_warning_on_discovery_if_color_mode_option_flag_is_used( ], ) async def test_fail_setup_if_color_modes_invalid( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, error: str, @@ -421,8 +416,7 @@ async def test_fail_setup_if_color_modes_invalid( ], ) async def test_single_color_mode( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setup with single color_mode.""" await mqtt_mock_entry() @@ -448,8 +442,7 @@ async def test_single_color_mode( @pytest.mark.parametrize("hass_config", [COLOR_MODES_CONFIG]) async def test_turn_on_with_unknown_color_mode_optimistic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setup and turn with unknown color_mode in optimistic mode.""" await mqtt_mock_entry() @@ -486,8 +479,7 @@ async def test_turn_on_with_unknown_color_mode_optimistic( ], ) async def test_controlling_state_with_unknown_color_mode( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test setup and turn with unknown color_mode in optimistic mode.""" await mqtt_mock_entry() @@ -2658,8 +2650,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = light.DOMAIN diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index e52b401c135..d570454a6bf 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -1280,8 +1280,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = light.DOMAIN @@ -1335,8 +1334,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = light.DOMAIN diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 2624be1f1b2..331f21a0a7c 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -996,8 +996,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = lock.DOMAIN @@ -1047,8 +1046,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = lock.DOMAIN diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index ae4d232ba54..5b7984cad62 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -15,7 +15,7 @@ from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, issue_registry as ir from tests.common import MockConfigEntry, async_capture_events, async_fire_mqtt_message -from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient +from tests.typing import MqttMockHAClientGenerator @pytest.mark.parametrize( @@ -37,8 +37,7 @@ from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient ], ) async def test_availability_with_shared_state_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the state is not changed twice. @@ -295,11 +294,10 @@ async def test_availability_with_shared_state_topic( ], ) @patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@pytest.mark.usefixtures("mqtt_client_mock") async def test_default_entity_and_device_name( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - mqtt_client_mock: MqttMockPahoClient, - mqtt_config_entry_data, caplog: pytest.LogCaptureFixture, entity_id: str, friendly_name: str, @@ -341,8 +339,7 @@ async def test_default_entity_and_device_name( async def test_name_attribute_is_set_or_not( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test frendly name with device_class set. diff --git a/tests/components/mqtt/test_notify.py b/tests/components/mqtt/test_notify.py index 5e4718e58ea..4837ee214c4 100644 --- a/tests/components/mqtt/test_notify.py +++ b/tests/components/mqtt/test_notify.py @@ -427,8 +427,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = notify.DOMAIN diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 73958def5f7..44652681fc3 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -783,7 +783,6 @@ async def test_min_max_step_attributes( ], ) async def test_invalid_min_max_attributes( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, ) -> None: @@ -863,7 +862,7 @@ async def test_default_mode( async def test_mode( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - mode, + mode: str, ) -> None: """Test mode.""" await mqtt_mock_entry() @@ -1022,8 +1021,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = number.DOMAIN @@ -1074,8 +1072,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = number.DOMAIN diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index e8beb622e9e..d78dbe5c003 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -382,8 +382,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = scene.DOMAIN @@ -406,8 +405,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = scene.DOMAIN diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index afc2b903e93..b2a4a1f2b49 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -615,9 +615,7 @@ def _test_options_attributes_options_config( _test_options_attributes_options_config((["milk", "beer"], ["milk"], [])), ) async def test_options_attributes( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - options: list[str], + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, options: list[str] ) -> None: """Test options attribute.""" await mqtt_mock_entry() @@ -701,8 +699,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, + hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient ) -> None: """Test reloading the MQTT platform.""" domain = select.DOMAIN @@ -755,8 +752,7 @@ async def test_setup_manual_entity_from_yaml( async def test_unload_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test unloading the config entry.""" domain = select.DOMAIN From 0ea1677f51309298e822f02f231fac2b8303bd03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jun 2024 10:50:55 -0500 Subject: [PATCH 0422/2411] Increase mqtt availablity timeout to 50s (#120760) --- homeassistant/components/mqtt/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 97fa616fdd1..27bdb4f2a35 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -36,7 +36,7 @@ from .const import ( ) from .models import DATA_MQTT, DATA_MQTT_AVAILABLE, ReceiveMessage -AVAILABILITY_TIMEOUT = 30.0 +AVAILABILITY_TIMEOUT = 50.0 TEMP_DIR_NAME = f"home-assistant-{DOMAIN}" From 3e8773c0d546f8b13ea1ee8430a300531d44b7bf Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 17:51:34 +0200 Subject: [PATCH 0423/2411] Bump aiowithings to 3.0.2 (#120765) --- homeassistant/components/withings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index 4c97f43fd80..090f8c4588e 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_push", "loggers": ["aiowithings"], "quality_scale": "platinum", - "requirements": ["aiowithings==3.0.1"] + "requirements": ["aiowithings==3.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0816d8c06df..a72f9676182 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -407,7 +407,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.0 # homeassistant.components.withings -aiowithings==3.0.1 +aiowithings==3.0.2 # homeassistant.components.yandex_transport aioymaps==1.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdd69e261c9..0546fa5d22d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -380,7 +380,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.0 # homeassistant.components.withings -aiowithings==3.0.1 +aiowithings==3.0.2 # homeassistant.components.yandex_transport aioymaps==1.2.4 From f4224a0327028324539244b25bdc9601eaf95081 Mon Sep 17 00:00:00 2001 From: Toni Korhonen Date: Fri, 28 Jun 2024 18:54:20 +0300 Subject: [PATCH 0424/2411] Bump Wallbox to 0.7.0 (#120768) --- homeassistant/components/wallbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json index ce9008ef8bb..63102646508 100644 --- a/homeassistant/components/wallbox/manifest.json +++ b/homeassistant/components/wallbox/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/wallbox", "iot_class": "cloud_polling", "loggers": ["wallbox"], - "requirements": ["wallbox==0.6.0"] + "requirements": ["wallbox==0.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index a72f9676182..f4d6aa0f001 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2864,7 +2864,7 @@ vultr==0.1.2 wakeonlan==2.1.0 # homeassistant.components.wallbox -wallbox==0.6.0 +wallbox==0.7.0 # homeassistant.components.folder_watcher watchdog==2.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0546fa5d22d..317e545e837 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2232,7 +2232,7 @@ vultr==0.1.2 wakeonlan==2.1.0 # homeassistant.components.wallbox -wallbox==0.6.0 +wallbox==0.7.0 # homeassistant.components.folder_watcher watchdog==2.3.1 From c029c534d6dbb209954a54c9f54f79786276ce78 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 28 Jun 2024 18:34:24 +0200 Subject: [PATCH 0425/2411] Do not call async_delete_issue() if there is no issue to delete in Shelly integration (#120762) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/coordinator.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 02feef3633b..33ed07c35de 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -377,12 +377,13 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): eager_start=True, ) elif update_type is BlockUpdateType.COAP_PERIODIC: + if self._push_update_failures >= MAX_PUSH_UPDATE_FAILURES: + ir.async_delete_issue( + self.hass, + DOMAIN, + PUSH_UPDATE_ISSUE_ID.format(unique=self.mac), + ) self._push_update_failures = 0 - ir.async_delete_issue( - self.hass, - DOMAIN, - PUSH_UPDATE_ISSUE_ID.format(unique=self.mac), - ) elif update_type is BlockUpdateType.COAP_REPLY: self._push_update_failures += 1 if self._push_update_failures == MAX_PUSH_UPDATE_FAILURES: From 4fb062102754b27c20dc96f349584ab2910d21e2 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 28 Jun 2024 20:11:03 +0200 Subject: [PATCH 0426/2411] Catch exceptions in service calls by buttons/switches in pyLoad integration (#120701) * Catch exceptions in service calls by buttons/switches * changes * more changes * update tests --- homeassistant/components/pyload/button.py | 17 ++++++- homeassistant/components/pyload/strings.json | 6 +++ homeassistant/components/pyload/switch.py | 46 +++++++++++++++++-- tests/components/pyload/test_button.py | 41 ++++++++++++++++- tests/components/pyload/test_switch.py | 48 ++++++++++++++++++++ 5 files changed, 151 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/pyload/button.py b/homeassistant/components/pyload/button.py index 950177f8751..386fe6968de 100644 --- a/homeassistant/components/pyload/button.py +++ b/homeassistant/components/pyload/button.py @@ -7,13 +7,15 @@ from dataclasses import dataclass from enum import StrEnum from typing import Any -from pyloadapi.api import PyLoadAPI +from pyloadapi.api import CannotConnect, InvalidAuth, PyLoadAPI from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import PyLoadConfigEntry +from .const import DOMAIN from .entity import BasePyLoadEntity @@ -80,4 +82,15 @@ class PyLoadBinarySensor(BasePyLoadEntity, ButtonEntity): async def async_press(self) -> None: """Handle the button press.""" - await self.entity_description.press_fn(self.coordinator.pyload) + try: + await self.entity_description.press_fn(self.coordinator.pyload) + except CannotConnect as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_exception", + ) from e + except InvalidAuth as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_auth_exception", + ) from e diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 9fe311574fb..38e17e5016f 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -98,6 +98,12 @@ }, "setup_authentication_exception": { "message": "Authentication failed for {username}, verify your login credentials" + }, + "service_call_exception": { + "message": "Unable to send command to pyLoad due to a connection error, try again later" + }, + "service_call_auth_exception": { + "message": "Unable to send command to pyLoad due to an authentication error, try again later" } }, "issues": { diff --git a/homeassistant/components/pyload/switch.py b/homeassistant/components/pyload/switch.py index 5e8c61823dd..ea189ed9a8f 100644 --- a/homeassistant/components/pyload/switch.py +++ b/homeassistant/components/pyload/switch.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from enum import StrEnum from typing import Any -from pyloadapi.api import PyLoadAPI +from pyloadapi.api import CannotConnect, InvalidAuth, PyLoadAPI from homeassistant.components.switch import ( SwitchDeviceClass, @@ -15,9 +15,11 @@ from homeassistant.components.switch import ( SwitchEntityDescription, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import PyLoadConfigEntry +from .const import DOMAIN from .coordinator import PyLoadData from .entity import BasePyLoadEntity @@ -90,15 +92,51 @@ class PyLoadSwitchEntity(BasePyLoadEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - await self.entity_description.turn_on_fn(self.coordinator.pyload) + try: + await self.entity_description.turn_on_fn(self.coordinator.pyload) + except CannotConnect as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_exception", + ) from e + except InvalidAuth as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_auth_exception", + ) from e + await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity on.""" - await self.entity_description.turn_off_fn(self.coordinator.pyload) + try: + await self.entity_description.turn_off_fn(self.coordinator.pyload) + except CannotConnect as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_exception", + ) from e + except InvalidAuth as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_auth_exception", + ) from e + await self.coordinator.async_refresh() async def async_toggle(self, **kwargs: Any) -> None: """Toggle the entity.""" - await self.entity_description.toggle_fn(self.coordinator.pyload) + try: + await self.entity_description.toggle_fn(self.coordinator.pyload) + except CannotConnect as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_exception", + ) from e + except InvalidAuth as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_auth_exception", + ) from e + await self.coordinator.async_refresh() diff --git a/tests/components/pyload/test_button.py b/tests/components/pyload/test_button.py index b5aa18ad3d9..53f592374ba 100644 --- a/tests/components/pyload/test_button.py +++ b/tests/components/pyload/test_button.py @@ -3,6 +3,7 @@ from collections.abc import AsyncGenerator from unittest.mock import AsyncMock, call, patch +from pyloadapi import CannotConnect, InvalidAuth import pytest from syrupy.assertion import SnapshotAssertion @@ -11,6 +12,7 @@ from homeassistant.components.pyload.button import PyLoadButtonEntity from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry, snapshot_platform @@ -78,6 +80,43 @@ async def test_button_press( {ATTR_ENTITY_ID: entity_entry.entity_id}, blocking=True, ) - await hass.async_block_till_done() assert API_CALL[entity_entry.translation_key] in mock_pyloadapi.method_calls mock_pyloadapi.reset_mock() + + +@pytest.mark.parametrize( + ("side_effect"), + [CannotConnect, InvalidAuth], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_button_press_errors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + entity_registry: er.EntityRegistry, + side_effect: Exception, +) -> None: + """Test button press method.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + entity_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + mock_pyloadapi.stop_all_downloads.side_effect = side_effect + mock_pyloadapi.restart_failed.side_effect = side_effect + mock_pyloadapi.delete_finished.side_effect = side_effect + mock_pyloadapi.restart.side_effect = side_effect + + for entity_entry in entity_entries: + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_entry.entity_id}, + blocking=True, + ) diff --git a/tests/components/pyload/test_switch.py b/tests/components/pyload/test_switch.py index 42a6bfa6f14..8e99cb00cfe 100644 --- a/tests/components/pyload/test_switch.py +++ b/tests/components/pyload/test_switch.py @@ -3,6 +3,7 @@ from collections.abc import AsyncGenerator from unittest.mock import AsyncMock, call, patch +from pyloadapi import CannotConnect, InvalidAuth import pytest from syrupy.assertion import SnapshotAssertion @@ -16,6 +17,7 @@ from homeassistant.components.switch import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry, snapshot_platform @@ -102,3 +104,49 @@ async def test_turn_on_off( in mock_pyloadapi.method_calls ) mock_pyloadapi.reset_mock() + + +@pytest.mark.parametrize( + ("service_call"), + [ + SERVICE_TURN_ON, + SERVICE_TURN_OFF, + SERVICE_TOGGLE, + ], +) +@pytest.mark.parametrize( + ("side_effect"), + [CannotConnect, InvalidAuth], +) +async def test_turn_on_off_errors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + service_call: str, + entity_registry: er.EntityRegistry, + side_effect: Exception, +) -> None: + """Test switch turn on/off, toggle method.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + entity_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + mock_pyloadapi.unpause.side_effect = side_effect + mock_pyloadapi.pause.side_effect = side_effect + mock_pyloadapi.toggle_pause.side_effect = side_effect + mock_pyloadapi.toggle_reconnect.side_effect = side_effect + + for entity_entry in entity_entries: + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + SWITCH_DOMAIN, + service_call, + {ATTR_ENTITY_ID: entity_entry.entity_id}, + blocking=True, + ) From 97ef56d264e758f6f79bbeaffc93011be0320857 Mon Sep 17 00:00:00 2001 From: Matthew FitzGerald-Chamberlain Date: Fri, 28 Jun 2024 15:15:34 -0500 Subject: [PATCH 0427/2411] Bump pyaprilaire to 0.7.4 (#120782) --- homeassistant/components/aprilaire/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aprilaire/manifest.json b/homeassistant/components/aprilaire/manifest.json index 43ba4417638..3cc44786989 100644 --- a/homeassistant/components/aprilaire/manifest.json +++ b/homeassistant/components/aprilaire/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["pyaprilaire"], - "requirements": ["pyaprilaire==0.7.0"] + "requirements": ["pyaprilaire==0.7.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index f4d6aa0f001..703c5f97e12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1710,7 +1710,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.7.0 +pyaprilaire==0.7.4 # homeassistant.components.asuswrt pyasuswrt==0.1.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 317e545e837..13c1ae45feb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1360,7 +1360,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.7.0 +pyaprilaire==0.7.4 # homeassistant.components.asuswrt pyasuswrt==0.1.21 From a3394675f38d1dcf1ee1ed7ce3db50696390f5b4 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 22:23:44 +0200 Subject: [PATCH 0428/2411] Bump energyzero lib to v2.1.1 (#120783) --- homeassistant/components/energyzero/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/energyzero/manifest.json b/homeassistant/components/energyzero/manifest.json index 025f929a4f6..807a0419967 100644 --- a/homeassistant/components/energyzero/manifest.json +++ b/homeassistant/components/energyzero/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/energyzero", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["energyzero==2.1.0"] + "requirements": ["energyzero==2.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 703c5f97e12..9b38321b750 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -807,7 +807,7 @@ emulated-roku==0.3.0 energyflip-client==0.2.2 # homeassistant.components.energyzero -energyzero==2.1.0 +energyzero==2.1.1 # homeassistant.components.enocean enocean==0.50 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13c1ae45feb..1fb47f21408 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -667,7 +667,7 @@ emulated-roku==0.3.0 energyflip-client==0.2.2 # homeassistant.components.energyzero -energyzero==2.1.0 +energyzero==2.1.1 # homeassistant.components.enocean enocean==0.50 From 3549aaf69c319484df8be6a4054f9780ad1cb7e2 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Fri, 28 Jun 2024 22:47:20 +0200 Subject: [PATCH 0429/2411] Reject small uptime updates for Unifi clients (#120398) Extend logic to reject small uptime updates to Unifi clients + add unit tests --- homeassistant/components/unifi/sensor.py | 5 ++-- tests/components/unifi/test_sensor.py | 36 ++++++++++++++++++------ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 028d70d8880..071230a9652 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -139,7 +139,7 @@ def async_device_uptime_value_fn(hub: UnifiHub, device: Device) -> datetime | No @callback -def async_device_uptime_value_changed_fn( +def async_uptime_value_changed_fn( old: StateType | date | datetime | Decimal, new: datetime | float | str | None ) -> bool: """Reject the new uptime value if it's too similar to the old one. Avoids unwanted fluctuation.""" @@ -310,6 +310,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( supported_fn=lambda hub, _: hub.config.option_allow_uptime_sensors, unique_id_fn=lambda hub, obj_id: f"uptime-{obj_id}", value_fn=async_client_uptime_value_fn, + value_changed_fn=async_uptime_value_changed_fn, ), UnifiSensorEntityDescription[Wlans, Wlan]( key="WLAN clients", @@ -396,7 +397,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( object_fn=lambda api, obj_id: api.devices[obj_id], unique_id_fn=lambda hub, obj_id: f"device_uptime-{obj_id}", value_fn=async_device_uptime_value_fn, - value_changed_fn=async_device_uptime_value_changed_fn, + value_changed_fn=async_uptime_value_changed_fn, ), UnifiSensorEntityDescription[Devices, Device]( key="Device temperature", diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 960a5d3e529..48e524aef76 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -484,12 +484,12 @@ async def test_bandwidth_sensors( ], ) @pytest.mark.parametrize( - ("initial_uptime", "event_uptime", "new_uptime"), + ("initial_uptime", "event_uptime", "small_variation_uptime", "new_uptime"), [ # Uptime listed in epoch time should never change - (1609462800, 1609462800, 1612141200), + (1609462800, 1609462800, 1609462800, 1612141200), # Uptime counted in seconds increases with every event - (60, 64, 60), + (60, 240, 480, 60), ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -503,6 +503,7 @@ async def test_uptime_sensors( client_payload: list[dict[str, Any]], initial_uptime, event_uptime, + small_variation_uptime, new_uptime, ) -> None: """Verify that uptime sensors are working as expected.""" @@ -519,15 +520,24 @@ async def test_uptime_sensors( ) # Verify normal new event doesn't change uptime - # 4 seconds has passed + # 4 minutes have passed uptime_client["uptime"] = event_uptime - now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC) + now = datetime(2021, 1, 1, 1, 4, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): mock_websocket_message(message=MessageKey.CLIENT, data=uptime_client) await hass.async_block_till_done() assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" + # Verify small variation of uptime (<120 seconds) is ignored + # 15 seconds variation after 8 minutes + uptime_client["uptime"] = small_variation_uptime + now = datetime(2021, 1, 1, 1, 8, 15, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_websocket_message(message=MessageKey.CLIENT, data=uptime_client) + + assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" + # Verify new event change uptime # 1 month has passed uptime_client["uptime"] = new_uptime @@ -911,10 +921,20 @@ async def test_device_uptime( ) # Verify normal new event doesn't change uptime - # 4 seconds has passed + # 4 minutes have passed device = device_payload[0] - device["uptime"] = 64 - now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC) + device["uptime"] = 240 + now = datetime(2021, 1, 1, 1, 4, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_websocket_message(message=MessageKey.DEVICE, data=device) + + assert hass.states.get("sensor.device_uptime").state == "2021-01-01T01:00:00+00:00" + + # Verify small variation of uptime (<120 seconds) is ignored + # 15 seconds variation after 8 minutes + device = device_payload[0] + device["uptime"] = 480 + now = datetime(2021, 1, 1, 1, 8, 15, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): mock_websocket_message(message=MessageKey.DEVICE, data=device) From a6454cf3c73fad25a590e8dcefa49d42aa0f6918 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 23:20:16 +0200 Subject: [PATCH 0430/2411] Bump odp-amsterdam lib to v6.0.2 (#120788) --- homeassistant/components/garages_amsterdam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/garages_amsterdam/manifest.json b/homeassistant/components/garages_amsterdam/manifest.json index ebda913abbb..4d4bb9f6fb5 100644 --- a/homeassistant/components/garages_amsterdam/manifest.json +++ b/homeassistant/components/garages_amsterdam/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/garages_amsterdam", "iot_class": "cloud_polling", - "requirements": ["odp-amsterdam==6.0.1"] + "requirements": ["odp-amsterdam==6.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9b38321b750..5b93dba61bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1453,7 +1453,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==6.0.1 +odp-amsterdam==6.0.2 # homeassistant.components.oem oemthermostat==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1fb47f21408..6fbce100c19 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1177,7 +1177,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==6.0.1 +odp-amsterdam==6.0.2 # homeassistant.components.ollama ollama-hass==0.1.7 From b03c10647e7c7e2ac8942c7874b05c60944a89ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jun 2024 16:29:12 -0500 Subject: [PATCH 0431/2411] Fix stale docstring in recorder queries (#120763) --- homeassistant/components/recorder/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py index d982576620d..a5be5dffe10 100644 --- a/homeassistant/components/recorder/queries.py +++ b/homeassistant/components/recorder/queries.py @@ -594,7 +594,7 @@ def delete_statistics_short_term_rows( def delete_event_rows( event_ids: Iterable[int], ) -> StatementLambdaElement: - """Delete statistics_short_term rows.""" + """Delete event rows.""" return lambda_stmt( lambda: delete(Events) .where(Events.event_id.in_(event_ids)) From 8ed11d4b90966de616de44bfe39add1162d9893c Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Fri, 28 Jun 2024 18:37:51 -0300 Subject: [PATCH 0432/2411] Link Generic Thermostat helper entity to actuator entity device (#120767) --- .../components/generic_thermostat/__init__.py | 10 ++ .../components/generic_thermostat/climate.py | 11 ++- .../generic_thermostat/test_climate.py | 50 +++++++++- .../generic_thermostat/test_init.py | 98 +++++++++++++++++++ 4 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 tests/components/generic_thermostat/test_init.py diff --git a/homeassistant/components/generic_thermostat/__init__.py b/homeassistant/components/generic_thermostat/__init__.py index 6a59e24ebd2..fcec36b8d35 100644 --- a/homeassistant/components/generic_thermostat/__init__.py +++ b/homeassistant/components/generic_thermostat/__init__.py @@ -3,13 +3,23 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.device import ( + async_remove_stale_devices_links_keep_entity_device, +) +CONF_HEATER = "heater" DOMAIN = "generic_thermostat" PLATFORMS = [Platform.CLIMATE] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" + + async_remove_stale_devices_links_keep_entity_device( + hass, + entry.entry_id, + entry.options[CONF_HEATER], + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index d284c7d7772..1b19def9cf4 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -54,6 +54,7 @@ from homeassistant.core import ( ) from homeassistant.exceptions import ConditionError from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_track_state_change_event, @@ -63,14 +64,12 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolDictType -from . import DOMAIN, PLATFORMS +from . import CONF_HEATER, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) DEFAULT_TOLERANCE = 0.3 DEFAULT_NAME = "Generic Thermostat" - -CONF_HEATER = "heater" CONF_SENSOR = "target_sensor" CONF_MIN_TEMP = "min_temp" CONF_MAX_TEMP = "max_temp" @@ -190,6 +189,7 @@ async def _async_setup_config( async_add_entities( [ GenericThermostat( + hass, name, heater_entity_id, sensor_entity_id, @@ -220,6 +220,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): def __init__( self, + hass: HomeAssistant, name: str, heater_entity_id: str, sensor_entity_id: str, @@ -242,6 +243,10 @@ class GenericThermostat(ClimateEntity, RestoreEntity): self._attr_name = name self.heater_entity_id = heater_entity_id self.sensor_entity_id = sensor_entity_id + self._attr_device_info = async_device_info_to_link_from_entity( + hass, + heater_entity_id, + ) self.ac_mode = ac_mode self.min_cycle_duration = min_cycle_duration self._cold_tolerance = cold_tolerance diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 1ecde733f48..7fb3e11e189 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -44,12 +44,13 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from tests.common import ( + MockConfigEntry, assert_setup_component, async_fire_time_changed, async_mock_service, @@ -1431,3 +1432,50 @@ async def test_reload(hass: HomeAssistant) -> None: assert len(hass.states.async_all()) == 1 assert hass.states.get("climate.test") is None assert hass.states.get("climate.reload") + + +async def test_device_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test for source entity device.""" + + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + source_device_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("switch", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + source_entity = entity_registry.async_get_or_create( + "switch", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("switch.test_source") is not None + + helper_config_entry = MockConfigEntry( + data={}, + domain=GENERIC_THERMOSTAT_DOMAIN, + options={ + "name": "Test", + "heater": "switch.test_source", + "target_sensor": ENT_SENSOR, + "ac_mode": False, + "cold_tolerance": 0.3, + "hot_tolerance": 0.3, + }, + title="Test", + ) + helper_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(helper_config_entry.entry_id) + await hass.async_block_till_done() + + helper_entity = entity_registry.async_get("climate.test") + assert helper_entity is not None + assert helper_entity.device_id == source_entity.device_id diff --git a/tests/components/generic_thermostat/test_init.py b/tests/components/generic_thermostat/test_init.py new file mode 100644 index 00000000000..0d6e106237c --- /dev/null +++ b/tests/components/generic_thermostat/test_init.py @@ -0,0 +1,98 @@ +"""Test Generic Thermostat component setup process.""" + +from __future__ import annotations + +from homeassistant.components.generic_thermostat import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_device_cleaning( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test cleaning of devices linked to the helper config entry.""" + + # Source entity device config entry + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + + # Device entry of the source entity + source_device1_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("switch", "identifier_test1")}, + connections={("mac", "30:31:32:33:34:01")}, + ) + + # Source entity registry + source_entity = entity_registry.async_get_or_create( + "switch", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device1_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("switch.test_source") is not None + + # Configure the configuration entry for helper + helper_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "Test", + "heater": "switch.test_source", + "target_sensor": "sensor.temperature", + "ac_mode": False, + "cold_tolerance": 0.3, + "hot_tolerance": 0.3, + }, + title="Test", + ) + helper_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(helper_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the helper entity + helper_entity = entity_registry.async_get("climate.test") + assert helper_entity is not None + assert helper_entity.device_id == source_entity.device_id + + # Device entry incorrectly linked to config entry + device_registry.async_get_or_create( + config_entry_id=helper_config_entry.entry_id, + identifiers={("sensor", "identifier_test2")}, + connections={("mac", "30:31:32:33:34:02")}, + ) + device_registry.async_get_or_create( + config_entry_id=helper_config_entry.entry_id, + identifiers={("sensor", "identifier_test3")}, + connections={("mac", "30:31:32:33:34:03")}, + ) + await hass.async_block_till_done() + + # Before reloading the config entry, 3 devices are expected to be linked + devices_before_reload = device_registry.devices.get_devices_for_config_entry_id( + helper_config_entry.entry_id + ) + assert len(devices_before_reload) == 3 + + # Config entry reload + await hass.config_entries.async_reload(helper_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the helper entity + helper_entity = entity_registry.async_get("climate.test") + assert helper_entity is not None + assert helper_entity.device_id == source_entity.device_id + + # After reloading the config entry, only one linked device is expected + devices_after_reload = device_registry.devices.get_devices_for_config_entry_id( + helper_config_entry.entry_id + ) + assert len(devices_after_reload) == 1 + + assert devices_after_reload[0].id == source_device1_entry.id From cef7def024ea8876a36d88b69e9433ce5ceee93f Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Fri, 28 Jun 2024 18:38:45 -0300 Subject: [PATCH 0433/2411] Link Generic Hygrostat helper entity to actuator entity device (#120759) --- .../components/generic_hygrostat/__init__.py | 10 ++ .../generic_hygrostat/humidifier.py | 7 ++ .../generic_hygrostat/test_humidifier.py | 53 ++++++++- .../components/generic_hygrostat/test_init.py | 102 ++++++++++++++++++ 4 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 tests/components/generic_hygrostat/test_init.py diff --git a/homeassistant/components/generic_hygrostat/__init__.py b/homeassistant/components/generic_hygrostat/__init__.py index ef032da1ee2..b4a6014c5a4 100644 --- a/homeassistant/components/generic_hygrostat/__init__.py +++ b/homeassistant/components/generic_hygrostat/__init__.py @@ -7,6 +7,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.device import ( + async_remove_stale_devices_links_keep_entity_device, +) from homeassistant.helpers.typing import ConfigType DOMAIN = "generic_hygrostat" @@ -78,6 +81,13 @@ 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.""" + + async_remove_stale_devices_links_keep_entity_device( + hass, + entry.entry_id, + entry.options[CONF_HUMIDIFIER], + ) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index a1f9936fa33..cc04dbf13c3 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -41,6 +41,7 @@ from homeassistant.core import ( callback, ) from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.device import async_device_info_to_link_from_entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_track_state_change_event, @@ -139,6 +140,7 @@ async def _async_setup_config( async_add_entities( [ GenericHygrostat( + hass, name, switch_entity_id, sensor_entity_id, @@ -167,6 +169,7 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): def __init__( self, + hass: HomeAssistant, name: str, switch_entity_id: str, sensor_entity_id: str, @@ -188,6 +191,10 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): self._name = name self._switch_entity_id = switch_entity_id self._sensor_entity_id = sensor_entity_id + self._attr_device_info = async_device_info_to_link_from_entity( + hass, + switch_entity_id, + ) self._device_class = device_class or HumidifierDeviceClass.HUMIDIFIER self._min_cycle_duration = min_cycle_duration self._dry_tolerance = dry_tolerance diff --git a/tests/components/generic_hygrostat/test_humidifier.py b/tests/components/generic_hygrostat/test_humidifier.py index eadc1b22527..15d80885d27 100644 --- a/tests/components/generic_hygrostat/test_humidifier.py +++ b/tests/components/generic_hygrostat/test_humidifier.py @@ -7,6 +7,9 @@ import pytest import voluptuous as vol from homeassistant.components import input_boolean, switch +from homeassistant.components.generic_hygrostat import ( + DOMAIN as GENERIC_HYDROSTAT_DOMAIN, +) from homeassistant.components.humidifier import ( ATTR_HUMIDITY, DOMAIN, @@ -32,11 +35,12 @@ from homeassistant.core import ( State, callback, ) -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 import homeassistant.util.dt as dt_util from tests.common import ( + MockConfigEntry, assert_setup_component, async_fire_time_changed, mock_restore_cache, @@ -1782,3 +1786,50 @@ async def test_sensor_stale_duration( # Not turning on by itself assert hass.states.get(humidifier_switch).state == STATE_OFF + + +async def test_device_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test for source entity device.""" + + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + source_device_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("switch", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + source_entity = entity_registry.async_get_or_create( + "switch", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("switch.test_source") is not None + + helper_config_entry = MockConfigEntry( + data={}, + domain=GENERIC_HYDROSTAT_DOMAIN, + options={ + "device_class": "humidifier", + "dry_tolerance": 2.0, + "humidifier": "switch.test_source", + "name": "Test", + "target_sensor": ENT_SENSOR, + "wet_tolerance": 4.0, + }, + title="Test", + ) + helper_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(helper_config_entry.entry_id) + await hass.async_block_till_done() + + helper_entity = entity_registry.async_get("humidifier.test") + assert helper_entity is not None + assert helper_entity.device_id == source_entity.device_id diff --git a/tests/components/generic_hygrostat/test_init.py b/tests/components/generic_hygrostat/test_init.py new file mode 100644 index 00000000000..bd4792f939d --- /dev/null +++ b/tests/components/generic_hygrostat/test_init.py @@ -0,0 +1,102 @@ +"""Test Generic Hygrostat component setup process.""" + +from __future__ import annotations + +from homeassistant.components.generic_hygrostat import ( + DOMAIN as GENERIC_HYDROSTAT_DOMAIN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .test_humidifier import ENT_SENSOR + +from tests.common import MockConfigEntry + + +async def test_device_cleaning( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test cleaning of devices linked to the helper config entry.""" + + # Source entity device config entry + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + + # Device entry of the source entity + source_device1_entry = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + identifiers={("switch", "identifier_test1")}, + connections={("mac", "30:31:32:33:34:01")}, + ) + + # Source entity registry + source_entity = entity_registry.async_get_or_create( + "switch", + "test", + "source", + config_entry=source_config_entry, + device_id=source_device1_entry.id, + ) + await hass.async_block_till_done() + assert entity_registry.async_get("switch.test_source") is not None + + # Configure the configuration entry for helper + helper_config_entry = MockConfigEntry( + data={}, + domain=GENERIC_HYDROSTAT_DOMAIN, + options={ + "device_class": "humidifier", + "dry_tolerance": 2.0, + "humidifier": "switch.test_source", + "name": "Test", + "target_sensor": ENT_SENSOR, + "wet_tolerance": 4.0, + }, + title="Test", + ) + helper_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(helper_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the helper entity + helper_entity = entity_registry.async_get("humidifier.test") + assert helper_entity is not None + assert helper_entity.device_id == source_entity.device_id + + # Device entry incorrectly linked to config entry + device_registry.async_get_or_create( + config_entry_id=helper_config_entry.entry_id, + identifiers={("sensor", "identifier_test2")}, + connections={("mac", "30:31:32:33:34:02")}, + ) + device_registry.async_get_or_create( + config_entry_id=helper_config_entry.entry_id, + identifiers={("sensor", "identifier_test3")}, + connections={("mac", "30:31:32:33:34:03")}, + ) + await hass.async_block_till_done() + + # Before reloading the config entry, 3 devices are expected to be linked + devices_before_reload = device_registry.devices.get_devices_for_config_entry_id( + helper_config_entry.entry_id + ) + assert len(devices_before_reload) == 3 + + # Config entry reload + await hass.config_entries.async_reload(helper_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm the link between the source entity device and the helper entity + helper_entity = entity_registry.async_get("humidifier.test") + assert helper_entity is not None + assert helper_entity.device_id == source_entity.device_id + + # After reloading the config entry, only one linked device is expected + devices_after_reload = device_registry.devices.get_devices_for_config_entry_id( + helper_config_entry.entry_id + ) + assert len(devices_after_reload) == 1 + + assert devices_after_reload[0].id == source_device1_entry.id From ba38f2e43bd1be382b6705b060f512b6d84cd79e Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sat, 29 Jun 2024 00:24:43 +0200 Subject: [PATCH 0434/2411] Bump gridnet lib to v5.0.1 (#120793) --- homeassistant/components/pure_energie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pure_energie/manifest.json b/homeassistant/components/pure_energie/manifest.json index 19098c41208..ff52ec0ecf9 100644 --- a/homeassistant/components/pure_energie/manifest.json +++ b/homeassistant/components/pure_energie/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/pure_energie", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["gridnet==5.0.0"], + "requirements": ["gridnet==5.0.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 5b93dba61bc..2920f454ffa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1013,7 +1013,7 @@ greeneye_monitor==3.0.3 greenwavereality==0.5.1 # homeassistant.components.pure_energie -gridnet==5.0.0 +gridnet==5.0.1 # homeassistant.components.growatt_server growattServer==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6fbce100c19..272d6007ddc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -833,7 +833,7 @@ greeclimate==1.4.1 greeneye_monitor==3.0.3 # homeassistant.components.pure_energie -gridnet==5.0.0 +gridnet==5.0.1 # homeassistant.components.growatt_server growattServer==1.5.0 From 5995459de51b1b8bdef9b23a61dcd31a1d2bcf49 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Sat, 29 Jun 2024 06:14:00 +0200 Subject: [PATCH 0435/2411] Update frontend to 20240628.0 (#120785) Co-authored-by: J. Nick Koston --- 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 cd46b358335..70f1f5f4f4f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240627.0"] + "requirements": ["home-assistant-frontend==20240628.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 239ee7575a9..af165f29ae4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240627.0 +home-assistant-frontend==20240628.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2920f454ffa..aa116ddd4d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240627.0 +home-assistant-frontend==20240628.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 272d6007ddc..76f734d662f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240627.0 +home-assistant-frontend==20240628.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From 04ab74589affff4fd5637c09eae1cef9c3e30000 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 00:01:18 -0500 Subject: [PATCH 0436/2411] Fix missing f-string in loop util (#120800) --- homeassistant/util/loop.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/loop.py b/homeassistant/util/loop.py index 8a469569601..866f35e79e2 100644 --- a/homeassistant/util/loop.py +++ b/homeassistant/util/loop.py @@ -106,10 +106,10 @@ def raise_for_blocking_call( if strict: raise RuntimeError( - "Caught blocking call to {func.__name__} with args " - f"{mapped_args.get('args')} inside the event loop by" + f"Caught blocking call to {func.__name__} with args " + f"{mapped_args.get('args')} inside the event loop by " f"{'custom ' if integration_frame.custom_integration else ''}" - "integration '{integration_frame.integration}' at " + f"integration '{integration_frame.integration}' at " f"{integration_frame.relative_filename}, line {integration_frame.line_number}:" f" {integration_frame.line}. (offender: {offender_filename}, line " f"{offender_lineno}: {offender_line}), please {report_issue}\n" From d4ecbc91c3c5de15ea8b79f1561e8856e6a1c9cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 00:40:35 -0500 Subject: [PATCH 0437/2411] Fix blocking I/O in xmpp notify to read uploaded files (#120801) detected by ruff in https://github.com/home-assistant/core/pull/120799 --- homeassistant/components/xmpp/notify.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 824f996c675..c73248f2524 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -305,16 +305,20 @@ async def async_send_message( # noqa: C901 timeout=timeout, ) - async def upload_file_from_path(self, path, timeout=None): + def _read_upload_file(self, path: str) -> bytes: + """Read file from path.""" + with open(path, "rb") as upfile: + _LOGGER.debug("Reading file %s", path) + return upfile.read() + + async def upload_file_from_path(self, path: str, timeout=None): """Upload a file from a local file path via XEP_0363.""" _LOGGER.info("Uploading file from path, %s", path) if not hass.config.is_allowed_path(path): raise PermissionError("Could not access file. Path not allowed") - with open(path, "rb") as upfile: - _LOGGER.debug("Reading file %s", path) - input_file = upfile.read() + input_file = await hass.async_add_executor_job(self._read_upload_file, path) filesize = len(input_file) _LOGGER.debug("Filesize is %s bytes", filesize) From 0dfb5bd7d958668f329dae2c537ff24895e52058 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 00:42:08 -0500 Subject: [PATCH 0438/2411] Fix unneeded dict values for MATCH_ALL recorder attrs exclude (#120804) * Small cleanup to handling MATCH_ALL recorder attrs exclude * Fix unneeded dict values for MATCH_ALL recorder attrs exclude The exclude is a set so the dict values were not needed * Fix unneeded dict values for MATCH_ALL recorder attrs exclude The exclude is a set so the dict values were not needed * Fix unneeded dict values for MATCH_ALL recorder attrs exclude The exclude is a set so the dict values were not needed --- .../components/recorder/db_schema.py | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index ce463067824..ba4a6106bce 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -142,6 +142,13 @@ _DEFAULT_TABLE_ARGS = { "mariadb_engine": MYSQL_ENGINE, } +_MATCH_ALL_KEEP = { + ATTR_DEVICE_CLASS, + ATTR_STATE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + ATTR_FRIENDLY_NAME, +} + class UnusedDateTime(DateTime): """An unused column type that behaves like a datetime.""" @@ -597,19 +604,8 @@ class StateAttributes(Base): if MATCH_ALL in unrecorded_attributes: # Don't exclude device class, state class, unit of measurement # or friendly name when using the MATCH_ALL exclude constant - _exclude_attributes = { - k: v - for k, v in state.attributes.items() - if k - not in ( - ATTR_DEVICE_CLASS, - ATTR_STATE_CLASS, - ATTR_UNIT_OF_MEASUREMENT, - ATTR_FRIENDLY_NAME, - ) - } - exclude_attrs.update(_exclude_attributes) - + exclude_attrs.update(state.attributes) + exclude_attrs -= _MATCH_ALL_KEEP else: exclude_attrs = ALL_DOMAIN_EXCLUDE_ATTRS encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes From 2cfd6d53bd241102c4721a1401eaa9aef06f3f4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 00:49:14 -0500 Subject: [PATCH 0439/2411] Add event platform to unifiprotect (#120681) * Add event platform to unifiprotect * Add event platform to unifiprotect * Add event platform to unifiprotect * Add event platform to unifiprotect * adjust * tweaks * translations * coverage * coverage * Update tests/components/unifiprotect/test_event.py --- .../components/unifiprotect/const.py | 1 + .../components/unifiprotect/event.py | 102 ++++++++++++ .../components/unifiprotect/strings.json | 11 ++ tests/components/unifiprotect/test_event.py | 154 ++++++++++++++++++ 4 files changed, 268 insertions(+) create mode 100644 homeassistant/components/unifiprotect/event.py create mode 100644 tests/components/unifiprotect/test_event.py diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index b56761263f4..ad251ba6153 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -61,6 +61,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CAMERA, + Platform.EVENT, Platform.LIGHT, Platform.LOCK, Platform.MEDIA_PLAYER, diff --git a/homeassistant/components/unifiprotect/event.py b/homeassistant/components/unifiprotect/event.py new file mode 100644 index 00000000000..4e2fd7fce44 --- /dev/null +++ b/homeassistant/components/unifiprotect/event.py @@ -0,0 +1,102 @@ +"""Platform providing event entities for UniFi Protect.""" + +from __future__ import annotations + +import dataclasses + +from uiprotect.data import ( + Camera, + EventType, + ProtectAdoptableDeviceModel, + ProtectModelWithId, +) + +from homeassistant.components.event import ( + EventDeviceClass, + EventEntity, + EventEntityDescription, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_EVENT_ID +from .data import ProtectData, UFPConfigEntry +from .entity import EventEntityMixin, ProtectDeviceEntity +from .models import ProtectEventMixin + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class ProtectEventEntityDescription(ProtectEventMixin, EventEntityDescription): + """Describes UniFi Protect event entity.""" + + +EVENT_DESCRIPTIONS: tuple[ProtectEventEntityDescription, ...] = ( + ProtectEventEntityDescription( + key="doorbell", + translation_key="doorbell", + name="Doorbell", + device_class=EventDeviceClass.DOORBELL, + icon="mdi:doorbell-video", + ufp_required_field="feature_flags.is_doorbell", + ufp_event_obj="last_ring_event", + event_types=[EventType.RING], + ), +) + + +class ProtectDeviceEventEntity(EventEntityMixin, ProtectDeviceEntity, EventEntity): + """A UniFi Protect event entity.""" + + entity_description: ProtectEventEntityDescription + + @callback + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + description = self.entity_description + + prev_event = self._event + prev_event_end = self._event_end + super()._async_update_device_from_protect(device) + if event := description.get_event_obj(device): + self._event = event + self._event_end = event.end if event else None + + if ( + event + and not self._event_already_ended(prev_event, prev_event_end) + and (event_types := description.event_types) + and (event_type := event.type) in event_types + ): + self._trigger_event(event_type, {ATTR_EVENT_ID: event.id}) + self.async_write_ha_state() + + +@callback +def _async_event_entities( + data: ProtectData, + ufp_device: ProtectAdoptableDeviceModel | None = None, +) -> list[ProtectDeviceEntity]: + entities: list[ProtectDeviceEntity] = [] + for device in data.get_cameras() if ufp_device is None else [ufp_device]: + entities.extend( + ProtectDeviceEventEntity(data, device, description) + for description in EVENT_DESCRIPTIONS + if description.has_required(device) + ) + return entities + + +async def async_setup_entry( + hass: HomeAssistant, + entry: UFPConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up event entities for UniFi Protect integration.""" + data = entry.runtime_data + + @callback + def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: + if device.is_adopted and isinstance(device, Camera): + async_add_entities(_async_event_entities(data, ufp_device=device)) + + data.async_subscribe_adopt(_add_new_device) + async_add_entities(_async_event_entities(data)) diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index 1435de5011e..f785498c005 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -137,6 +137,17 @@ "none": "Clear" } } + }, + "event": { + "doorbell": { + "state_attributes": { + "event_type": { + "state": { + "ring": "Ring" + } + } + } + } } }, "services": { diff --git a/tests/components/unifiprotect/test_event.py b/tests/components/unifiprotect/test_event.py new file mode 100644 index 00000000000..9d1a701fe39 --- /dev/null +++ b/tests/components/unifiprotect/test_event.py @@ -0,0 +1,154 @@ +"""Test the UniFi Protect event platform.""" + +from __future__ import annotations + +from datetime import datetime, timedelta +from unittest.mock import Mock + +from uiprotect.data import Camera, Event, EventType, ModelType, SmartDetectObjectType + +from homeassistant.components.unifiprotect.const import ( + ATTR_EVENT_ID, + DEFAULT_ATTRIBUTION, +) +from homeassistant.components.unifiprotect.event import EVENT_DESCRIPTIONS +from homeassistant.const import ATTR_ATTRIBUTION, Platform +from homeassistant.core import Event as HAEvent, HomeAssistant, callback +from homeassistant.helpers.event import async_track_state_change_event + +from .utils import ( + MockUFPFixture, + adopt_devices, + assert_entity_counts, + ids_from_device_description, + init_entry, + remove_entities, +) + + +async def test_camera_remove( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, unadopted_camera: Camera +) -> None: + """Test removing and re-adding a camera device.""" + + ufp.api.bootstrap.nvr.system_info.ustorage = None + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.EVENT, 1, 1) + await remove_entities(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.EVENT, 0, 0) + await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.EVENT, 1, 1) + + +async def test_doorbell_ring( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, + fixed_now: datetime, +) -> None: + """Test a doorbell ring event.""" + + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.EVENT, 1, 1) + events: list[HAEvent] = [] + + @callback + def _capture_event(event: HAEvent) -> None: + events.append(event) + + _, entity_id = ids_from_device_description( + Platform.EVENT, doorbell, EVENT_DESCRIPTIONS[0] + ) + + unsub = async_track_state_change_event(hass, entity_id, _capture_event) + event = Event( + model=ModelType.EVENT, + id="test_event_id", + type=EventType.RING, + start=fixed_now - timedelta(seconds=1), + end=None, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + api=ufp.api, + ) + + new_camera = doorbell.copy() + new_camera.last_ring_event_id = "test_event_id" + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = event + ufp.ws_msg(mock_msg) + + await hass.async_block_till_done() + + assert len(events) == 1 + state = events[0].data["new_state"] + assert state + timestamp = state.state + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + assert state.attributes[ATTR_EVENT_ID] == "test_event_id" + + event = Event( + model=ModelType.EVENT, + id="test_event_id", + type=EventType.RING, + start=fixed_now - timedelta(seconds=1), + end=fixed_now + timedelta(seconds=1), + score=50, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + api=ufp.api, + ) + + new_camera = doorbell.copy() + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = event + ufp.ws_msg(mock_msg) + + await hass.async_block_till_done() + + # Event is already seen and has end, should now be off + state = hass.states.get(entity_id) + assert state + assert state.state == timestamp + + # Now send an event that has an end right away + event = Event( + model=ModelType.EVENT, + id="new_event_id", + type=EventType.RING, + start=fixed_now - timedelta(seconds=1), + end=fixed_now + timedelta(seconds=1), + score=80, + smart_detect_types=[SmartDetectObjectType.PACKAGE], + smart_detect_event_ids=[], + camera_id=doorbell.id, + api=ufp.api, + ) + + new_camera = doorbell.copy() + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = event + + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == timestamp + unsub() From 8f6addcc61b189e5beb599c1a24cd5c9c39a00ab Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Sat, 29 Jun 2024 02:25:22 -0400 Subject: [PATCH 0440/2411] Bump greeclimate to 1.4.6 (#120758) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 58404e90353..a7c884c4042 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/gree", "iot_class": "local_polling", "loggers": ["greeclimate"], - "requirements": ["greeclimate==1.4.1"] + "requirements": ["greeclimate==1.4.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index aa116ddd4d4..209aa1cb122 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1004,7 +1004,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.1 +greeclimate==1.4.6 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 76f734d662f..21d97a6d04d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -827,7 +827,7 @@ govee-local-api==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.1 +greeclimate==1.4.6 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 From 6ed0960648a02e6219c75d33e727447d3cbc7121 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 13:48:43 +0200 Subject: [PATCH 0441/2411] Bump aiomealie to 0.5.0 (#120815) --- homeassistant/components/mealie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index fb81ff850b8..918dd743726 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mealie", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["aiomealie==0.4.0"] + "requirements": ["aiomealie==0.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 209aa1cb122..7a957e99baa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -294,7 +294,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.4.0 +aiomealie==0.5.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21d97a6d04d..9ead78df9fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.4.0 +aiomealie==0.5.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 From 852bb1922348b979fca6195d6fb1f82cb1058fc3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 07:49:16 -0500 Subject: [PATCH 0442/2411] Cleanup db_schema from_event constructors (#120803) --- .../components/recorder/db_schema.py | 73 ++++++++++--------- homeassistant/core.py | 5 ++ tests/test_core.py | 8 ++ 3 files changed, 50 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index ba4a6106bce..915fd4a4bb8 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -238,7 +238,6 @@ class JSONLiteral(JSON): EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] -EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} class Events(Base): @@ -305,18 +304,19 @@ class Events(Base): @staticmethod def from_event(event: Event) -> Events: """Create an event database object from a native event.""" + context = event.context return Events( event_type=None, event_data=None, - origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + origin_idx=event.origin.idx, time_fired=None, time_fired_ts=event.time_fired_timestamp, context_id=None, - context_id_bin=ulid_to_bytes_or_none(event.context.id), + context_id_bin=ulid_to_bytes_or_none(context.id), context_user_id=None, - context_user_id_bin=uuid_hex_to_bytes_or_none(event.context.user_id), + context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_id), context_parent_id=None, - context_parent_id_bin=ulid_to_bytes_or_none(event.context.parent_id), + context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id), ) def to_native(self, validate_entity_id: bool = True) -> Event | None: @@ -492,41 +492,42 @@ class States(Base): @staticmethod def from_event(event: Event[EventStateChangedData]) -> States: """Create object from a state_changed event.""" - entity_id = event.data["entity_id"] state = event.data["new_state"] - dbstate = States( - entity_id=entity_id, - attributes=None, - context_id=None, - context_id_bin=ulid_to_bytes_or_none(event.context.id), - context_user_id=None, - context_user_id_bin=uuid_hex_to_bytes_or_none(event.context.user_id), - context_parent_id=None, - context_parent_id_bin=ulid_to_bytes_or_none(event.context.parent_id), - origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), - last_updated=None, - last_changed=None, - ) # None state means the state was removed from the state machine if state is None: - dbstate.state = "" - dbstate.last_updated_ts = event.time_fired_timestamp - dbstate.last_changed_ts = None - dbstate.last_reported_ts = None - return dbstate - - dbstate.state = state.state - dbstate.last_updated_ts = state.last_updated_timestamp - if state.last_updated == state.last_changed: - dbstate.last_changed_ts = None + state_value = "" + last_updated_ts = event.time_fired_timestamp + last_changed_ts = None + last_reported_ts = None else: - dbstate.last_changed_ts = state.last_changed_timestamp - if state.last_updated == state.last_reported: - dbstate.last_reported_ts = None - else: - dbstate.last_reported_ts = state.last_reported_timestamp - - return dbstate + state_value = state.state + last_updated_ts = state.last_updated_timestamp + if state.last_updated == state.last_changed: + last_changed_ts = None + else: + last_changed_ts = state.last_changed_timestamp + if state.last_updated == state.last_reported: + last_reported_ts = None + else: + last_reported_ts = state.last_reported_timestamp + context = event.context + return States( + state=state_value, + entity_id=event.data["entity_id"], + attributes=None, + context_id=None, + context_id_bin=ulid_to_bytes_or_none(context.id), + context_user_id=None, + context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_id), + context_parent_id=None, + context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id), + origin_idx=event.origin.idx, + last_updated=None, + last_changed=None, + last_updated_ts=last_updated_ts, + last_changed_ts=last_changed_ts, + last_reported_ts=last_reported_ts, + ) def to_native(self, validate_entity_id: bool = True) -> State | None: """Convert to an HA state object.""" diff --git a/homeassistant/core.py b/homeassistant/core.py index c4392f62c52..cf5373ad8c2 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1308,6 +1308,11 @@ class EventOrigin(enum.Enum): """Return the event.""" return self.value + @cached_property + def idx(self) -> int: + """Return the index of the origin.""" + return next((idx for idx, origin in enumerate(EventOrigin) if origin is self)) + class Event(Generic[_DataT]): """Representation of an event within the bus.""" diff --git a/tests/test_core.py b/tests/test_core.py index 5e6b51cc39e..5f824f9e53a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -920,6 +920,14 @@ def test_event_repr() -> None: ) +def test_event_origin_idx() -> None: + """Test the EventOrigin idx.""" + assert ha.EventOrigin.remote is ha.EventOrigin.remote + assert ha.EventOrigin.local is ha.EventOrigin.local + assert ha.EventOrigin.local.idx == 0 + assert ha.EventOrigin.remote.idx == 1 + + def test_event_as_dict() -> None: """Test an Event as dictionary.""" event_type = "some_type" From c5804d362c221695379345dfe764478b15ecfa0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 07:50:53 -0500 Subject: [PATCH 0443/2411] Remove legacy foreign key constraint from sqlite states table (#120779) --- .../components/recorder/migration.py | 76 ++++++++++++++-- tests/components/recorder/test_migrate.py | 89 ++++++++++++++++++- .../components/recorder/test_v32_migration.py | 9 +- 3 files changed, 162 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 561b446f493..cf003f72af4 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -24,7 +24,7 @@ from sqlalchemy.exc import ( SQLAlchemyError, ) from sqlalchemy.orm.session import Session -from sqlalchemy.schema import AddConstraint, DropConstraint +from sqlalchemy.schema import AddConstraint, CreateTable, DropConstraint from sqlalchemy.sql.expression import true from sqlalchemy.sql.lambdas import StatementLambdaElement @@ -1738,14 +1738,15 @@ def cleanup_legacy_states_event_ids(instance: Recorder) -> bool: # Only drop the index if there are no more event_ids in the states table # ex all NULL assert instance.engine is not None, "engine should never be None" - if instance.dialect_name != SupportedDialect.SQLITE: + if instance.dialect_name == SupportedDialect.SQLITE: # SQLite does not support dropping foreign key constraints - # so we can't drop the index at this time but we can avoid - # looking for legacy rows during purge + # so we have to rebuild the table + rebuild_sqlite_table(session_maker, instance.engine, States) + else: _drop_foreign_key_constraints( session_maker, instance.engine, TABLE_STATES, ["event_id"] ) - _drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX) + _drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX) instance.use_legacy_events_index = False return True @@ -1894,3 +1895,68 @@ def _mark_migration_done( migration_id=migration.migration_id, version=migration.migration_version ) ) + + +def rebuild_sqlite_table( + session_maker: Callable[[], Session], engine: Engine, table: type[Base] +) -> None: + """Rebuild an SQLite table. + + This must only be called after all migrations are complete + and the database is in a consistent state. + + If the table is not migrated to the current schema this + will likely fail. + """ + table_table = cast(Table, table.__table__) + orig_name = table_table.name + temp_name = f"{table_table.name}_temp_{int(time())}" + + _LOGGER.warning( + "Rebuilding SQLite table %s; This will take a while; Please be patient!", + orig_name, + ) + + try: + # 12 step SQLite table rebuild + # https://www.sqlite.org/lang_altertable.html + with session_scope(session=session_maker()) as session: + # Step 1 - Disable foreign keys + session.connection().execute(text("PRAGMA foreign_keys=OFF")) + # Step 2 - create a transaction + with session_scope(session=session_maker()) as session: + # Step 3 - we know all the indexes, triggers, and views associated with table X + new_sql = str(CreateTable(table_table).compile(engine)).strip("\n") + ";" + source_sql = f"CREATE TABLE {orig_name}" + replacement_sql = f"CREATE TABLE {temp_name}" + assert source_sql in new_sql, f"{source_sql} should be in new_sql" + new_sql = new_sql.replace(source_sql, replacement_sql) + # Step 4 - Create temp table + session.execute(text(new_sql)) + column_names = ",".join([column.name for column in table_table.columns]) + # Step 5 - Transfer content + sql = f"INSERT INTO {temp_name} SELECT {column_names} FROM {orig_name};" # noqa: S608 + session.execute(text(sql)) + # Step 6 - Drop the original table + session.execute(text(f"DROP TABLE {orig_name}")) + # Step 7 - Rename the temp table + session.execute(text(f"ALTER TABLE {temp_name} RENAME TO {orig_name}")) + # Step 8 - Recreate indexes + for index in table_table.indexes: + index.create(session.connection()) + # Step 9 - Recreate views (there are none) + # Step 10 - Check foreign keys + session.execute(text("PRAGMA foreign_key_check")) + # Step 11 - Commit transaction + session.commit() + except SQLAlchemyError: + _LOGGER.exception("Error recreating SQLite table %s", table_table.name) + # Swallow the exception since we do not want to ever raise + # an integrity error as it would cause the database + # to be discarded and recreated from scratch + else: + _LOGGER.warning("Rebuilding SQLite table %s finished", orig_name) + finally: + with session_scope(session=session_maker()) as session: + # Step 12 - Re-enable foreign keys + session.connection().execute(text("PRAGMA foreign_keys=ON")) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index a21f4771616..cb8e402f65a 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -16,7 +16,7 @@ from sqlalchemy.exc import ( ProgrammingError, SQLAlchemyError, ) -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, scoped_session, sessionmaker from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component @@ -24,6 +24,7 @@ from homeassistant.components import persistent_notification as pn, recorder from homeassistant.components.recorder import db_schema, migration from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, + Events, RecorderRuns, States, ) @@ -633,3 +634,89 @@ def test_raise_if_exception_missing_empty_cause_str() -> None: with pytest.raises(ProgrammingError): migration.raise_if_exception_missing_str(programming_exc, ["not present"]) + + +def test_rebuild_sqlite_states_table(recorder_db_url: str) -> None: + """Test that we can rebuild the states table in SQLite.""" + if not recorder_db_url.startswith("sqlite://"): + # This test is specific for SQLite + return + + engine = create_engine(recorder_db_url) + session_maker = scoped_session(sessionmaker(bind=engine, future=True)) + with session_scope(session=session_maker()) as session: + db_schema.Base.metadata.create_all(engine) + with session_scope(session=session_maker()) as session: + session.add(States(state="on")) + session.commit() + + migration.rebuild_sqlite_table(session_maker, engine, States) + + with session_scope(session=session_maker()) as session: + assert session.query(States).count() == 1 + assert session.query(States).first().state == "on" + + engine.dispose() + + +def test_rebuild_sqlite_states_table_missing_fails( + recorder_db_url: str, caplog: pytest.LogCaptureFixture +) -> None: + """Test handling missing states table when attempting rebuild.""" + if not recorder_db_url.startswith("sqlite://"): + # This test is specific for SQLite + return + + engine = create_engine(recorder_db_url) + session_maker = scoped_session(sessionmaker(bind=engine, future=True)) + with session_scope(session=session_maker()) as session: + db_schema.Base.metadata.create_all(engine) + + with session_scope(session=session_maker()) as session: + session.add(Events(event_type="state_changed", event_data="{}")) + session.connection().execute(text("DROP TABLE states")) + session.commit() + + migration.rebuild_sqlite_table(session_maker, engine, States) + assert "Error recreating SQLite table states" in caplog.text + caplog.clear() + + # Now rebuild the events table to make sure the database did not + # get corrupted + migration.rebuild_sqlite_table(session_maker, engine, Events) + + with session_scope(session=session_maker()) as session: + assert session.query(Events).count() == 1 + assert session.query(Events).first().event_type == "state_changed" + assert session.query(Events).first().event_data == "{}" + + engine.dispose() + + +def test_rebuild_sqlite_states_table_extra_columns( + recorder_db_url: str, caplog: pytest.LogCaptureFixture +) -> None: + """Test handling extra columns when rebuilding the states table.""" + if not recorder_db_url.startswith("sqlite://"): + # This test is specific for SQLite + return + + engine = create_engine(recorder_db_url) + session_maker = scoped_session(sessionmaker(bind=engine, future=True)) + with session_scope(session=session_maker()) as session: + db_schema.Base.metadata.create_all(engine) + with session_scope(session=session_maker()) as session: + session.add(States(state="on")) + session.commit() + session.connection().execute( + text("ALTER TABLE states ADD COLUMN extra_column TEXT") + ) + + migration.rebuild_sqlite_table(session_maker, engine, States) + assert "Error recreating SQLite table states" not in caplog.text + + with session_scope(session=session_maker()) as session: + assert session.query(States).count() == 1 + assert session.query(States).first().state == "on" + + engine.dispose() diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index a07c63b3376..e3398fbf0e3 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -211,10 +211,9 @@ async def test_migrate_times(caplog: pytest.LogCaptureFixture, tmp_path: Path) - ) states_index_names = {index["name"] for index in states_indexes} - # sqlite does not support dropping foreign keys so the - # ix_states_event_id index is not dropped in this case - # but use_legacy_events_index is still False - assert "ix_states_event_id" in states_index_names + # sqlite does not support dropping foreign keys so we had to + # create a new table and copy the data over + assert "ix_states_event_id" not in states_index_names assert recorder.get_instance(hass).use_legacy_events_index is False @@ -342,8 +341,6 @@ async def test_migrate_can_resume_entity_id_post_migration( await hass.async_stop() await hass.async_block_till_done() - assert "ix_states_entity_id_last_updated_ts" in states_index_names - async with async_test_home_assistant() as hass: recorder_helper.async_initialize_recorder(hass) assert await async_setup_component( From 5a6deddd30bb98290a5fcf065f1baf19a63fb650 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 29 Jun 2024 14:53:01 +0200 Subject: [PATCH 0444/2411] Bump pyOverkiz to 1.13.12 (#120819) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index a78eb160a28..12dfe89c7d3 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -19,7 +19,7 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], - "requirements": ["pyoverkiz==1.13.11"], + "requirements": ["pyoverkiz==1.13.12"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 7a957e99baa..653134a9eb0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2069,7 +2069,7 @@ pyotgw==2.2.0 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.11 +pyoverkiz==1.13.12 # homeassistant.components.onewire pyownet==0.10.0.post1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ead78df9fe..fb5e1856372 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1629,7 +1629,7 @@ pyotgw==2.2.0 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.11 +pyoverkiz==1.13.12 # homeassistant.components.onewire pyownet==0.10.0.post1 From f07f9062c1e7de5d0f1ee53f3538105003392cae Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 14:53:42 +0200 Subject: [PATCH 0445/2411] Bump python-opensky to 1.0.1 (#120818) --- homeassistant/components/opensky/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opensky/manifest.json b/homeassistant/components/opensky/manifest.json index 106103cf752..831abbc9cbf 100644 --- a/homeassistant/components/opensky/manifest.json +++ b/homeassistant/components/opensky/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/opensky", "iot_class": "cloud_polling", - "requirements": ["python-opensky==1.0.0"] + "requirements": ["python-opensky==1.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 653134a9eb0..ebe126bcfa3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2296,7 +2296,7 @@ python-mystrom==2.2.0 python-opendata-transport==0.4.0 # homeassistant.components.opensky -python-opensky==1.0.0 +python-opensky==1.0.1 # homeassistant.components.otbr # homeassistant.components.thread diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb5e1856372..c4873a4f917 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1793,7 +1793,7 @@ python-mystrom==2.2.0 python-opendata-transport==0.4.0 # homeassistant.components.opensky -python-opensky==1.0.0 +python-opensky==1.0.1 # homeassistant.components.otbr # homeassistant.components.thread From 7d8cbbaacb0668820cbe498896c021ad1d474152 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 29 Jun 2024 08:45:51 -0700 Subject: [PATCH 0446/2411] Bump gcal_sync to 6.1.3 (#120278) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/google/test_calendar.py | 12 ++++++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 062bf58d2f5..5fc28d2f398 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.0.4", "oauth2client==4.1.3", "ical==8.0.1"] + "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index ebe126bcfa3..fa286e02801 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,7 +924,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.0.4 +gcal-sync==6.1.3 # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4873a4f917..88615ede4ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -762,7 +762,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.0.4 +gcal-sync==6.1.3 # homeassistant.components.geocaching geocachingapi==0.2.1 diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 8e934925f46..5fe26585fe5 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -385,6 +385,9 @@ async def test_update_error( with patch("homeassistant.util.utcnow", return_value=now): async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # Entity is marked uanvailable due to API failure state = hass.states.get(TEST_ENTITY) @@ -414,6 +417,9 @@ async def test_update_error( with patch("homeassistant.util.utcnow", return_value=now): async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # State updated with new API response state = hass.states.get(TEST_ENTITY) @@ -606,6 +612,9 @@ async def test_future_event_update_behavior( freezer.move_to(now) async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # Event has started state = hass.states.get(TEST_ENTITY) @@ -643,6 +652,9 @@ async def test_future_event_offset_update_behavior( freezer.move_to(now) async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # Event has not started, but the offset was reached state = hass.states.get(TEST_ENTITY) From ce34a5e4951ecf23d9725f159ac2baa643e7598d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 17:46:44 +0200 Subject: [PATCH 0447/2411] Add icons to Airgradient (#120820) --- .../components/airgradient/icons.json | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/homeassistant/components/airgradient/icons.json b/homeassistant/components/airgradient/icons.json index 45d1e12d46e..22188d72faa 100644 --- a/homeassistant/components/airgradient/icons.json +++ b/homeassistant/components/airgradient/icons.json @@ -8,6 +8,34 @@ "default": "mdi:lightbulb-on-outline" } }, + "number": { + "led_bar_brightness": { + "default": "mdi:brightness-percent" + }, + "display_brightness": { + "default": "mdi:brightness-percent" + } + }, + "select": { + "configuration_control": { + "default": "mdi:cloud-cog" + }, + "display_temperature_unit": { + "default": "mdi:thermometer-lines" + }, + "led_bar_mode": { + "default": "mdi:led-strip" + }, + "nox_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "voc_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "co2_automatic_baseline_calibration": { + "default": "mdi:molecule-co2" + } + }, "sensor": { "total_volatile_organic_component_index": { "default": "mdi:molecule" @@ -17,6 +45,32 @@ }, "pm003_count": { "default": "mdi:blur" + }, + "led_bar_brightness": { + "default": "mdi:brightness-percent" + }, + "display_brightness": { + "default": "mdi:brightness-percent" + }, + "display_temperature_unit": { + "default": "mdi:thermometer-lines" + }, + "led_bar_mode": { + "default": "mdi:led-strip" + }, + "nox_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "voc_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "co2_automatic_baseline_calibration": { + "default": "mdi:molecule-co2" + } + }, + "switch": { + "post_data_to_airgradient": { + "default": "mdi:cogs" } } } From 0ab7647fea74e76f982ee75511df6c0cdc54191b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 17:47:21 +0200 Subject: [PATCH 0448/2411] Use meal note as fallback in Mealie (#120828) --- homeassistant/components/mealie/calendar.py | 4 ++-- .../components/mealie/fixtures/get_mealplans.json | 11 +++++++++++ .../components/mealie/snapshots/test_calendar.ambr | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mealie/calendar.py b/homeassistant/components/mealie/calendar.py index 08e90ebf5ea..f3c692a1d12 100644 --- a/homeassistant/components/mealie/calendar.py +++ b/homeassistant/components/mealie/calendar.py @@ -30,8 +30,8 @@ async def async_setup_entry( def _get_event_from_mealplan(mealplan: Mealplan) -> CalendarEvent: """Create a CalendarEvent from a Mealplan.""" - description: str | None = None - name = "No recipe" + description: str | None = mealplan.description + name = mealplan.title or "No recipe" if mealplan.recipe: name = mealplan.recipe.name description = mealplan.recipe.description diff --git a/tests/components/mealie/fixtures/get_mealplans.json b/tests/components/mealie/fixtures/get_mealplans.json index 2d63b753d99..9255f9b7396 100644 --- a/tests/components/mealie/fixtures/get_mealplans.json +++ b/tests/components/mealie/fixtures/get_mealplans.json @@ -605,6 +605,17 @@ "updateAt": "2024-01-02T06:35:05.209189", "lastMade": "2024-01-02T22:59:59" } + }, + { + "date": "2024-01-21", + "entryType": "dinner", + "title": "Aquavite", + "text": "Dineren met de boys", + "recipeId": null, + "id": 1, + "groupId": "3931df86-0679-4579-8c63-4bedc9ca9a85", + "userId": "6caa6e4d-521f-4ef4-9ed7-388bdd63f47d", + "recipe": null } ], "next": null, diff --git a/tests/components/mealie/snapshots/test_calendar.ambr b/tests/components/mealie/snapshots/test_calendar.ambr index 6af53c112de..634d4963135 100644 --- a/tests/components/mealie/snapshots/test_calendar.ambr +++ b/tests/components/mealie/snapshots/test_calendar.ambr @@ -147,6 +147,20 @@ 'summary': 'Mousse de saumon', 'uid': None, }), + dict({ + 'description': 'Dineren met de boys', + 'end': dict({ + 'date': '2024-01-22', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-21', + }), + 'summary': 'Aquavite', + 'uid': None, + }), ]) # --- # name: test_entities[calendar.mealie_breakfast-entry] From 25932dff28c16f1901ef6b028e3855851eabc691 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 17:48:28 +0200 Subject: [PATCH 0449/2411] Add unique id to Mealie config entry (#120816) --- homeassistant/components/mealie/calendar.py | 5 +--- .../components/mealie/config_flow.py | 5 ++-- homeassistant/components/mealie/entity.py | 7 ++++-- tests/components/mealie/conftest.py | 6 ++++- .../mealie/fixtures/users_self.json | 24 +++++++++++++++++++ .../mealie/snapshots/test_calendar.ambr | 8 +++---- .../mealie/snapshots/test_init.ambr | 2 +- tests/components/mealie/test_config_flow.py | 5 ++-- tests/components/mealie/test_init.py | 2 +- 9 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 tests/components/mealie/fixtures/users_self.json diff --git a/homeassistant/components/mealie/calendar.py b/homeassistant/components/mealie/calendar.py index f3c692a1d12..fb628754f06 100644 --- a/homeassistant/components/mealie/calendar.py +++ b/homeassistant/components/mealie/calendar.py @@ -50,12 +50,9 @@ class MealieMealplanCalendarEntity(MealieEntity, CalendarEntity): self, coordinator: MealieCoordinator, entry_type: MealplanEntryType ) -> None: """Create the Calendar entity.""" - super().__init__(coordinator) + super().__init__(coordinator, entry_type.name.lower()) self._entry_type = entry_type self._attr_translation_key = entry_type.name.lower() - self._attr_unique_id = ( - f"{self.coordinator.config_entry.entry_id}_{entry_type.name.lower()}" - ) @property def event(self) -> CalendarEvent | None: diff --git a/homeassistant/components/mealie/config_flow.py b/homeassistant/components/mealie/config_flow.py index b25cade148a..550e4679720 100644 --- a/homeassistant/components/mealie/config_flow.py +++ b/homeassistant/components/mealie/config_flow.py @@ -28,14 +28,13 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" errors: dict[str, str] = {} if user_input: - self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) client = MealieClient( user_input[CONF_HOST], token=user_input[CONF_API_TOKEN], session=async_get_clientsession(self.hass), ) try: - await client.get_mealplan_today() + info = await client.get_user_info() except MealieConnectionError: errors["base"] = "cannot_connect" except MealieAuthenticationError: @@ -44,6 +43,8 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN): LOGGER.exception("Unexpected error") errors["base"] = "unknown" else: + await self.async_set_unique_id(info.user_id) + self._abort_if_unique_id_configured() return self.async_create_entry( title="Mealie", data=user_input, diff --git a/homeassistant/components/mealie/entity.py b/homeassistant/components/mealie/entity.py index 5e339c1d4b8..765ae2b99d7 100644 --- a/homeassistant/components/mealie/entity.py +++ b/homeassistant/components/mealie/entity.py @@ -12,10 +12,13 @@ class MealieEntity(CoordinatorEntity[MealieCoordinator]): _attr_has_entity_name = True - def __init__(self, coordinator: MealieCoordinator) -> None: + def __init__(self, coordinator: MealieCoordinator, key: str) -> None: """Initialize Mealie entity.""" super().__init__(coordinator) + unique_id = coordinator.config_entry.unique_id + assert unique_id is not None + self._attr_unique_id = f"{unique_id}_{key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + identifiers={(DOMAIN, unique_id)}, entry_type=DeviceEntryType.SERVICE, ) diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py index dd6309cb524..9bda9e3c46d 100644 --- a/tests/components/mealie/conftest.py +++ b/tests/components/mealie/conftest.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from aiomealie import Mealplan, MealplanResponse +from aiomealie import Mealplan, MealplanResponse, UserInfo from mashumaro.codecs.orjson import ORJSONDecoder import pytest from typing_extensions import Generator @@ -44,6 +44,9 @@ def mock_mealie_client() -> Generator[AsyncMock]: client.get_mealplan_today.return_value = ORJSONDecoder(list[Mealplan]).decode( load_fixture("get_mealplan_today.json", DOMAIN) ) + client.get_user_info.return_value = UserInfo.from_json( + load_fixture("users_self.json", DOMAIN) + ) yield client @@ -55,4 +58,5 @@ def mock_config_entry() -> MockConfigEntry: title="Mealie", data={CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token"}, entry_id="01J0BC4QM2YBRP6H5G933CETT7", + unique_id="bf1c62fe-4941-4332-9886-e54e88dbdba0", ) diff --git a/tests/components/mealie/fixtures/users_self.json b/tests/components/mealie/fixtures/users_self.json new file mode 100644 index 00000000000..6d5901c8cc0 --- /dev/null +++ b/tests/components/mealie/fixtures/users_self.json @@ -0,0 +1,24 @@ +{ + "id": "bf1c62fe-4941-4332-9886-e54e88dbdba0", + "username": "admin", + "fullName": "Change Me", + "email": "changeme@example.com", + "authMethod": "Mealie", + "admin": true, + "group": "home", + "advanced": true, + "canInvite": true, + "canManage": true, + "canOrganize": true, + "groupId": "24477569-f6af-4b53-9e3f-6d04b0ca6916", + "groupSlug": "home", + "tokens": [ + { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb25nX3Rva2VuIjp0cnVlLCJpZCI6ImJmMWM2MmZlLTQ5NDEtNDMzMi05ODg2LWU1NGU4OGRiZGJhMCIsIm5hbWUiOiJ0ZXN0aW5nIiwiaW50ZWdyYXRpb25faWQiOiJnZW5lcmljIiwiZXhwIjoxODczOTA5ODk4fQ.xwXZp4fL2g1RbIqGtBeOaS6RDfsYbQDHj8XtRM3wlX0", + "name": "testing", + "id": 2, + "createdAt": "2024-05-20T10:31:38.179669" + } + ], + "cacheKey": "1234" +} diff --git a/tests/components/mealie/snapshots/test_calendar.ambr b/tests/components/mealie/snapshots/test_calendar.ambr index 634d4963135..c3b26e1e9e2 100644 --- a/tests/components/mealie/snapshots/test_calendar.ambr +++ b/tests/components/mealie/snapshots/test_calendar.ambr @@ -192,7 +192,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'breakfast', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_breakfast', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_breakfast', 'unit_of_measurement': None, }) # --- @@ -244,7 +244,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'dinner', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_dinner', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_dinner', 'unit_of_measurement': None, }) # --- @@ -296,7 +296,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'lunch', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_lunch', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_lunch', 'unit_of_measurement': None, }) # --- @@ -348,7 +348,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'side', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_side', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_side', 'unit_of_measurement': None, }) # --- diff --git a/tests/components/mealie/snapshots/test_init.ambr b/tests/components/mealie/snapshots/test_init.ambr index 1333b292dac..8f800676945 100644 --- a/tests/components/mealie/snapshots/test_init.ambr +++ b/tests/components/mealie/snapshots/test_init.ambr @@ -13,7 +13,7 @@ 'identifiers': set({ tuple( 'mealie', - '01J0BC4QM2YBRP6H5G933CETT7', + 'bf1c62fe-4941-4332-9886-e54e88dbdba0', ), }), 'is_new': False, diff --git a/tests/components/mealie/test_config_flow.py b/tests/components/mealie/test_config_flow.py index ac68ed2fac5..777bb1e4ad1 100644 --- a/tests/components/mealie/test_config_flow.py +++ b/tests/components/mealie/test_config_flow.py @@ -37,6 +37,7 @@ async def test_full_flow( CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token", } + assert result["result"].unique_id == "bf1c62fe-4941-4332-9886-e54e88dbdba0" @pytest.mark.parametrize( @@ -55,7 +56,7 @@ async def test_flow_errors( error: str, ) -> None: """Test flow errors.""" - mock_mealie_client.get_mealplan_today.side_effect = exception + mock_mealie_client.get_user_info.side_effect = exception result = await hass.config_entries.flow.async_init( DOMAIN, @@ -72,7 +73,7 @@ async def test_flow_errors( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} - mock_mealie_client.get_mealplan_today.side_effect = None + mock_mealie_client.get_user_info.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/mealie/test_init.py b/tests/components/mealie/test_init.py index 7d63ad135f9..5a7a5387897 100644 --- a/tests/components/mealie/test_init.py +++ b/tests/components/mealie/test_init.py @@ -26,7 +26,7 @@ async def test_device_info( """Test device registry integration.""" await setup_integration(hass, mock_config_entry) device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, mock_config_entry.entry_id)} + identifiers={(DOMAIN, mock_config_entry.unique_id)} ) assert device_entry is not None assert device_entry == snapshot From 2d5961fa4f606e15422a4d0a8fe1c9fd00d3b105 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 29 Jun 2024 08:45:51 -0700 Subject: [PATCH 0450/2411] Bump gcal_sync to 6.1.3 (#120278) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/google/test_calendar.py | 12 ++++++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 062bf58d2f5..5fc28d2f398 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.0.4", "oauth2client==4.1.3", "ical==8.0.1"] + "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 34eb86f9ddc..f351dc563ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,7 +924,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.0.4 +gcal-sync==6.1.3 # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c08fe273b8e..a29f8c7d5cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -762,7 +762,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.0.4 +gcal-sync==6.1.3 # homeassistant.components.geocaching geocachingapi==0.2.1 diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 8e934925f46..5fe26585fe5 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -385,6 +385,9 @@ async def test_update_error( with patch("homeassistant.util.utcnow", return_value=now): async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # Entity is marked uanvailable due to API failure state = hass.states.get(TEST_ENTITY) @@ -414,6 +417,9 @@ async def test_update_error( with patch("homeassistant.util.utcnow", return_value=now): async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # State updated with new API response state = hass.states.get(TEST_ENTITY) @@ -606,6 +612,9 @@ async def test_future_event_update_behavior( freezer.move_to(now) async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # Event has started state = hass.states.get(TEST_ENTITY) @@ -643,6 +652,9 @@ async def test_future_event_offset_update_behavior( freezer.move_to(now) async_fire_time_changed(hass, now) await hass.async_block_till_done() + # Ensure coordinator update completes + await hass.async_block_till_done() + await hass.async_block_till_done() # Event has not started, but the offset was reached state = hass.states.get(TEST_ENTITY) From 5fd589053aa17b390d138a19cb9c38d95e2397d9 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Fri, 28 Jun 2024 22:47:20 +0200 Subject: [PATCH 0451/2411] Reject small uptime updates for Unifi clients (#120398) Extend logic to reject small uptime updates to Unifi clients + add unit tests --- homeassistant/components/unifi/sensor.py | 5 ++-- tests/components/unifi/test_sensor.py | 36 ++++++++++++++++++------ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 028d70d8880..071230a9652 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -139,7 +139,7 @@ def async_device_uptime_value_fn(hub: UnifiHub, device: Device) -> datetime | No @callback -def async_device_uptime_value_changed_fn( +def async_uptime_value_changed_fn( old: StateType | date | datetime | Decimal, new: datetime | float | str | None ) -> bool: """Reject the new uptime value if it's too similar to the old one. Avoids unwanted fluctuation.""" @@ -310,6 +310,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( supported_fn=lambda hub, _: hub.config.option_allow_uptime_sensors, unique_id_fn=lambda hub, obj_id: f"uptime-{obj_id}", value_fn=async_client_uptime_value_fn, + value_changed_fn=async_uptime_value_changed_fn, ), UnifiSensorEntityDescription[Wlans, Wlan]( key="WLAN clients", @@ -396,7 +397,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( object_fn=lambda api, obj_id: api.devices[obj_id], unique_id_fn=lambda hub, obj_id: f"device_uptime-{obj_id}", value_fn=async_device_uptime_value_fn, - value_changed_fn=async_device_uptime_value_changed_fn, + value_changed_fn=async_uptime_value_changed_fn, ), UnifiSensorEntityDescription[Devices, Device]( key="Device temperature", diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 960a5d3e529..48e524aef76 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -484,12 +484,12 @@ async def test_bandwidth_sensors( ], ) @pytest.mark.parametrize( - ("initial_uptime", "event_uptime", "new_uptime"), + ("initial_uptime", "event_uptime", "small_variation_uptime", "new_uptime"), [ # Uptime listed in epoch time should never change - (1609462800, 1609462800, 1612141200), + (1609462800, 1609462800, 1609462800, 1612141200), # Uptime counted in seconds increases with every event - (60, 64, 60), + (60, 240, 480, 60), ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -503,6 +503,7 @@ async def test_uptime_sensors( client_payload: list[dict[str, Any]], initial_uptime, event_uptime, + small_variation_uptime, new_uptime, ) -> None: """Verify that uptime sensors are working as expected.""" @@ -519,15 +520,24 @@ async def test_uptime_sensors( ) # Verify normal new event doesn't change uptime - # 4 seconds has passed + # 4 minutes have passed uptime_client["uptime"] = event_uptime - now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC) + now = datetime(2021, 1, 1, 1, 4, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): mock_websocket_message(message=MessageKey.CLIENT, data=uptime_client) await hass.async_block_till_done() assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" + # Verify small variation of uptime (<120 seconds) is ignored + # 15 seconds variation after 8 minutes + uptime_client["uptime"] = small_variation_uptime + now = datetime(2021, 1, 1, 1, 8, 15, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_websocket_message(message=MessageKey.CLIENT, data=uptime_client) + + assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" + # Verify new event change uptime # 1 month has passed uptime_client["uptime"] = new_uptime @@ -911,10 +921,20 @@ async def test_device_uptime( ) # Verify normal new event doesn't change uptime - # 4 seconds has passed + # 4 minutes have passed device = device_payload[0] - device["uptime"] = 64 - now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC) + device["uptime"] = 240 + now = datetime(2021, 1, 1, 1, 4, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_websocket_message(message=MessageKey.DEVICE, data=device) + + assert hass.states.get("sensor.device_uptime").state == "2021-01-01T01:00:00+00:00" + + # Verify small variation of uptime (<120 seconds) is ignored + # 15 seconds variation after 8 minutes + device = device_payload[0] + device["uptime"] = 480 + now = datetime(2021, 1, 1, 1, 8, 15, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): mock_websocket_message(message=MessageKey.DEVICE, data=device) From b350ba9657c88f7edf3b27786de8dbcee1ab7371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Fri, 28 Jun 2024 11:45:27 +0300 Subject: [PATCH 0452/2411] Add electrical consumption sensor to Overkiz (#120717) electrical consumption sensor --- homeassistant/components/overkiz/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index c62840eea97..d313faf0c1d 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -182,6 +182,13 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), + OverkizSensorDescription( + key=OverkizState.MODBUSLINK_POWER_HEAT_ELECTRICAL, + name="Electric power consumption", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF1, name="Consumption tariff 1", From 8994ab1686f729e83cc45b44182da5c1564446de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Fri, 28 Jun 2024 11:53:07 +0300 Subject: [PATCH 0453/2411] Add warm water remaining volume sensor to Overkiz (#120718) * warm water remaining volume sensor * Update homeassistant/components/overkiz/sensor.py Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> --------- Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> --- homeassistant/components/overkiz/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index d313faf0c1d..bf9608358eb 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -420,6 +420,13 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), + OverkizSensorDescription( + key=OverkizState.CORE_REMAINING_HOT_WATER, + name="Warm water remaining", + device_class=SensorDeviceClass.VOLUME, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfVolume.LITERS, + ), # Cover OverkizSensorDescription( key=OverkizState.CORE_TARGET_CLOSURE, From f57c9429019bde0b16fa675143deea7f54201642 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 14:53:29 +0200 Subject: [PATCH 0454/2411] Bump sense-energy to 0.12.4 (#120744) * Bump sense-energy to 0.12.4 * Fix --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index 843aeddde7b..640a2113d6f 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_push", "loggers": ["sense_energy"], "quality_scale": "internal", - "requirements": ["sense-energy==0.12.2"] + "requirements": ["sense-energy==0.12.4"] } diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 7ef1caefe48..116b714ba82 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/sense", "iot_class": "cloud_polling", "loggers": ["sense_energy"], - "requirements": ["sense-energy==0.12.2"] + "requirements": ["sense-energy==0.12.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index f351dc563ed..b26425b30bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2539,7 +2539,7 @@ sendgrid==6.8.2 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.12.2 +sense-energy==0.12.4 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a29f8c7d5cf..e95c0b7d600 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1976,7 +1976,7 @@ securetar==2024.2.1 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.12.2 +sense-energy==0.12.4 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 From 20ac0aa7b116683adcaabd8be8c72c1dd097fae3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 16:21:32 +0200 Subject: [PATCH 0455/2411] Bump govee-local-api to 1.5.1 (#120747) --- homeassistant/components/govee_light_local/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_light_local/manifest.json b/homeassistant/components/govee_light_local/manifest.json index 93a19408182..168a13e2477 100644 --- a/homeassistant/components/govee_light_local/manifest.json +++ b/homeassistant/components/govee_light_local/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/govee_light_local", "iot_class": "local_push", - "requirements": ["govee-local-api==1.5.0"] + "requirements": ["govee-local-api==1.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index b26425b30bc..c2bcb3ed65e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -995,7 +995,7 @@ gotailwind==0.2.3 govee-ble==0.31.3 # homeassistant.components.govee_light_local -govee-local-api==1.5.0 +govee-local-api==1.5.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e95c0b7d600..7197bc17a04 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -821,7 +821,7 @@ gotailwind==0.2.3 govee-ble==0.31.3 # homeassistant.components.govee_light_local -govee-local-api==1.5.0 +govee-local-api==1.5.1 # homeassistant.components.gpsd gps3==0.33.3 From 83df47030738164f1454c5ec731444bf9f1ca911 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 16:14:31 +0200 Subject: [PATCH 0456/2411] Bump easyenergy lib to v2.1.2 (#120753) --- homeassistant/components/easyenergy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/easyenergy/manifest.json b/homeassistant/components/easyenergy/manifest.json index 4dcce0fd705..4d45dc2d399 100644 --- a/homeassistant/components/easyenergy/manifest.json +++ b/homeassistant/components/easyenergy/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/easyenergy", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["easyenergy==2.1.1"] + "requirements": ["easyenergy==2.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index c2bcb3ed65e..a98be6b1a9f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -771,7 +771,7 @@ dynalite-panel==0.0.4 eagle100==0.1.1 # homeassistant.components.easyenergy -easyenergy==2.1.1 +easyenergy==2.1.2 # homeassistant.components.ebusd ebusdpy==0.0.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7197bc17a04..834c6c5bf99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -643,7 +643,7 @@ dynalite-panel==0.0.4 eagle100==0.1.1 # homeassistant.components.easyenergy -easyenergy==2.1.1 +easyenergy==2.1.2 # homeassistant.components.electric_kiwi electrickiwi-api==0.8.5 From 6028e5b77add83c795e1be3c61271bd296da5711 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 16:20:44 +0200 Subject: [PATCH 0457/2411] Bump p1monitor lib to v3.0.1 (#120756) --- homeassistant/components/p1_monitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/p1_monitor/manifest.json b/homeassistant/components/p1_monitor/manifest.json index 0dfe1f3a46c..4702de3546d 100644 --- a/homeassistant/components/p1_monitor/manifest.json +++ b/homeassistant/components/p1_monitor/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["p1monitor"], "quality_scale": "platinum", - "requirements": ["p1monitor==3.0.0"] + "requirements": ["p1monitor==3.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a98be6b1a9f..3ce72af693b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1519,7 +1519,7 @@ ourgroceries==1.5.4 ovoenergy==2.0.0 # homeassistant.components.p1_monitor -p1monitor==3.0.0 +p1monitor==3.0.1 # homeassistant.components.mqtt paho-mqtt==1.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 834c6c5bf99..f87edeff701 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1222,7 +1222,7 @@ ourgroceries==1.5.4 ovoenergy==2.0.0 # homeassistant.components.p1_monitor -p1monitor==3.0.0 +p1monitor==3.0.1 # homeassistant.components.mqtt paho-mqtt==1.6.1 From 59bb8b360e636ce115d92e27e9668b39ba7ed725 Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Sat, 29 Jun 2024 02:25:22 -0400 Subject: [PATCH 0458/2411] Bump greeclimate to 1.4.6 (#120758) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 58404e90353..a7c884c4042 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/gree", "iot_class": "local_polling", "loggers": ["greeclimate"], - "requirements": ["greeclimate==1.4.1"] + "requirements": ["greeclimate==1.4.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3ce72af693b..873a2460c1f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1004,7 +1004,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.1 +greeclimate==1.4.6 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f87edeff701..51afc6123ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -827,7 +827,7 @@ govee-local-api==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.1 +greeclimate==1.4.6 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 From 917eeba98422c8a00e678507f54a91e3779a28f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jun 2024 10:50:55 -0500 Subject: [PATCH 0459/2411] Increase mqtt availablity timeout to 50s (#120760) --- homeassistant/components/mqtt/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 97fa616fdd1..27bdb4f2a35 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -36,7 +36,7 @@ from .const import ( ) from .models import DATA_MQTT, DATA_MQTT_AVAILABLE, ReceiveMessage -AVAILABILITY_TIMEOUT = 30.0 +AVAILABILITY_TIMEOUT = 50.0 TEMP_DIR_NAME = f"home-assistant-{DOMAIN}" From d1a96ef3621ff6ef09c7baeb8648bb7f4501c34f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 28 Jun 2024 18:34:24 +0200 Subject: [PATCH 0460/2411] Do not call async_delete_issue() if there is no issue to delete in Shelly integration (#120762) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/coordinator.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 02feef3633b..33ed07c35de 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -377,12 +377,13 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): eager_start=True, ) elif update_type is BlockUpdateType.COAP_PERIODIC: + if self._push_update_failures >= MAX_PUSH_UPDATE_FAILURES: + ir.async_delete_issue( + self.hass, + DOMAIN, + PUSH_UPDATE_ISSUE_ID.format(unique=self.mac), + ) self._push_update_failures = 0 - ir.async_delete_issue( - self.hass, - DOMAIN, - PUSH_UPDATE_ISSUE_ID.format(unique=self.mac), - ) elif update_type is BlockUpdateType.COAP_REPLY: self._push_update_failures += 1 if self._push_update_failures == MAX_PUSH_UPDATE_FAILURES: From 0f3ed3bb674d4df1d99e99bd5a7f5630883064f8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jun 2024 17:51:34 +0200 Subject: [PATCH 0461/2411] Bump aiowithings to 3.0.2 (#120765) --- homeassistant/components/withings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index 4c97f43fd80..090f8c4588e 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_push", "loggers": ["aiowithings"], "quality_scale": "platinum", - "requirements": ["aiowithings==3.0.1"] + "requirements": ["aiowithings==3.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 873a2460c1f..598fef62903 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -407,7 +407,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.0 # homeassistant.components.withings -aiowithings==3.0.1 +aiowithings==3.0.2 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51afc6123ec..5e34afbdcf2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -380,7 +380,7 @@ aiowatttime==0.1.1 aiowebostv==0.4.0 # homeassistant.components.withings -aiowithings==3.0.1 +aiowithings==3.0.2 # homeassistant.components.yandex_transport aioymaps==1.2.2 From 8165acddeb8a35f9556d32ce666f96405b3f153e Mon Sep 17 00:00:00 2001 From: Matthew FitzGerald-Chamberlain Date: Fri, 28 Jun 2024 15:15:34 -0500 Subject: [PATCH 0462/2411] Bump pyaprilaire to 0.7.4 (#120782) --- homeassistant/components/aprilaire/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aprilaire/manifest.json b/homeassistant/components/aprilaire/manifest.json index 43ba4417638..3cc44786989 100644 --- a/homeassistant/components/aprilaire/manifest.json +++ b/homeassistant/components/aprilaire/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["pyaprilaire"], - "requirements": ["pyaprilaire==0.7.0"] + "requirements": ["pyaprilaire==0.7.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 598fef62903..4e407a30b48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1710,7 +1710,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.7.0 +pyaprilaire==0.7.4 # homeassistant.components.asuswrt pyasuswrt==0.1.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e34afbdcf2..b899463e178 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1360,7 +1360,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.7.0 +pyaprilaire==0.7.4 # homeassistant.components.asuswrt pyasuswrt==0.1.21 From b30b4d5a3a63e27460b2e8bbfaf7172a7397727f Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 22:23:44 +0200 Subject: [PATCH 0463/2411] Bump energyzero lib to v2.1.1 (#120783) --- homeassistant/components/energyzero/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/energyzero/manifest.json b/homeassistant/components/energyzero/manifest.json index 025f929a4f6..807a0419967 100644 --- a/homeassistant/components/energyzero/manifest.json +++ b/homeassistant/components/energyzero/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/energyzero", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["energyzero==2.1.0"] + "requirements": ["energyzero==2.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4e407a30b48..d7a5eaa4a62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -807,7 +807,7 @@ emulated-roku==0.3.0 energyflip-client==0.2.2 # homeassistant.components.energyzero -energyzero==2.1.0 +energyzero==2.1.1 # homeassistant.components.enocean enocean==0.50 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b899463e178..a6cf6b7e6d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -667,7 +667,7 @@ emulated-roku==0.3.0 energyflip-client==0.2.2 # homeassistant.components.energyzero -energyzero==2.1.0 +energyzero==2.1.1 # homeassistant.components.enocean enocean==0.50 From 723c4a1eb5276307a11b138f66c26dab9b0df640 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Sat, 29 Jun 2024 06:14:00 +0200 Subject: [PATCH 0464/2411] Update frontend to 20240628.0 (#120785) Co-authored-by: J. Nick Koston --- 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 cd46b358335..70f1f5f4f4f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240627.0"] + "requirements": ["home-assistant-frontend==20240628.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 91db2564fa6..7cccd58d73f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240627.0 +home-assistant-frontend==20240628.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index d7a5eaa4a62..34d6daa8ab2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240627.0 +home-assistant-frontend==20240628.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6cf6b7e6d1..d76492f1c65 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240627.0 +home-assistant-frontend==20240628.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From ec577c7bd333cceff2020ed220c7236c5fa4acde Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 28 Jun 2024 23:20:16 +0200 Subject: [PATCH 0465/2411] Bump odp-amsterdam lib to v6.0.2 (#120788) --- homeassistant/components/garages_amsterdam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/garages_amsterdam/manifest.json b/homeassistant/components/garages_amsterdam/manifest.json index ebda913abbb..4d4bb9f6fb5 100644 --- a/homeassistant/components/garages_amsterdam/manifest.json +++ b/homeassistant/components/garages_amsterdam/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/garages_amsterdam", "iot_class": "cloud_polling", - "requirements": ["odp-amsterdam==6.0.1"] + "requirements": ["odp-amsterdam==6.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 34d6daa8ab2..7f8d32f59d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1453,7 +1453,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==6.0.1 +odp-amsterdam==6.0.2 # homeassistant.components.oem oemthermostat==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d76492f1c65..f1d09d9c406 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1177,7 +1177,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==6.0.1 +odp-amsterdam==6.0.2 # homeassistant.components.ollama ollama-hass==0.1.7 From b45eff9a2b7bcd7c386d45db04f061facf1516bf Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sat, 29 Jun 2024 00:24:43 +0200 Subject: [PATCH 0466/2411] Bump gridnet lib to v5.0.1 (#120793) --- homeassistant/components/pure_energie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pure_energie/manifest.json b/homeassistant/components/pure_energie/manifest.json index 19098c41208..ff52ec0ecf9 100644 --- a/homeassistant/components/pure_energie/manifest.json +++ b/homeassistant/components/pure_energie/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/pure_energie", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["gridnet==5.0.0"], + "requirements": ["gridnet==5.0.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 7f8d32f59d7..60346dd1959 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1013,7 +1013,7 @@ greeneye_monitor==3.0.3 greenwavereality==0.5.1 # homeassistant.components.pure_energie -gridnet==5.0.0 +gridnet==5.0.1 # homeassistant.components.growatt_server growattServer==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1d09d9c406..b3725e8e237 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -833,7 +833,7 @@ greeclimate==1.4.6 greeneye_monitor==3.0.3 # homeassistant.components.pure_energie -gridnet==5.0.0 +gridnet==5.0.1 # homeassistant.components.growatt_server growattServer==1.5.0 From 0dcfd38cdc1d7b924f4356e88fd5b8dbdfb7db70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 00:01:18 -0500 Subject: [PATCH 0467/2411] Fix missing f-string in loop util (#120800) --- homeassistant/util/loop.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/loop.py b/homeassistant/util/loop.py index 8a469569601..866f35e79e2 100644 --- a/homeassistant/util/loop.py +++ b/homeassistant/util/loop.py @@ -106,10 +106,10 @@ def raise_for_blocking_call( if strict: raise RuntimeError( - "Caught blocking call to {func.__name__} with args " - f"{mapped_args.get('args')} inside the event loop by" + f"Caught blocking call to {func.__name__} with args " + f"{mapped_args.get('args')} inside the event loop by " f"{'custom ' if integration_frame.custom_integration else ''}" - "integration '{integration_frame.integration}' at " + f"integration '{integration_frame.integration}' at " f"{integration_frame.relative_filename}, line {integration_frame.line_number}:" f" {integration_frame.line}. (offender: {offender_filename}, line " f"{offender_lineno}: {offender_line}), please {report_issue}\n" From 0ec07001bd275da93709a0b7df4c7d05e1d43e75 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 00:40:35 -0500 Subject: [PATCH 0468/2411] Fix blocking I/O in xmpp notify to read uploaded files (#120801) detected by ruff in https://github.com/home-assistant/core/pull/120799 --- homeassistant/components/xmpp/notify.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 824f996c675..c73248f2524 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -305,16 +305,20 @@ async def async_send_message( # noqa: C901 timeout=timeout, ) - async def upload_file_from_path(self, path, timeout=None): + def _read_upload_file(self, path: str) -> bytes: + """Read file from path.""" + with open(path, "rb") as upfile: + _LOGGER.debug("Reading file %s", path) + return upfile.read() + + async def upload_file_from_path(self, path: str, timeout=None): """Upload a file from a local file path via XEP_0363.""" _LOGGER.info("Uploading file from path, %s", path) if not hass.config.is_allowed_path(path): raise PermissionError("Could not access file. Path not allowed") - with open(path, "rb") as upfile: - _LOGGER.debug("Reading file %s", path) - input_file = upfile.read() + input_file = await hass.async_add_executor_job(self._read_upload_file, path) filesize = len(input_file) _LOGGER.debug("Filesize is %s bytes", filesize) From 66932e3d9a264df08bac40219064e411cbc976a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 00:42:08 -0500 Subject: [PATCH 0469/2411] Fix unneeded dict values for MATCH_ALL recorder attrs exclude (#120804) * Small cleanup to handling MATCH_ALL recorder attrs exclude * Fix unneeded dict values for MATCH_ALL recorder attrs exclude The exclude is a set so the dict values were not needed * Fix unneeded dict values for MATCH_ALL recorder attrs exclude The exclude is a set so the dict values were not needed * Fix unneeded dict values for MATCH_ALL recorder attrs exclude The exclude is a set so the dict values were not needed --- .../components/recorder/db_schema.py | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index ce463067824..ba4a6106bce 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -142,6 +142,13 @@ _DEFAULT_TABLE_ARGS = { "mariadb_engine": MYSQL_ENGINE, } +_MATCH_ALL_KEEP = { + ATTR_DEVICE_CLASS, + ATTR_STATE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + ATTR_FRIENDLY_NAME, +} + class UnusedDateTime(DateTime): """An unused column type that behaves like a datetime.""" @@ -597,19 +604,8 @@ class StateAttributes(Base): if MATCH_ALL in unrecorded_attributes: # Don't exclude device class, state class, unit of measurement # or friendly name when using the MATCH_ALL exclude constant - _exclude_attributes = { - k: v - for k, v in state.attributes.items() - if k - not in ( - ATTR_DEVICE_CLASS, - ATTR_STATE_CLASS, - ATTR_UNIT_OF_MEASUREMENT, - ATTR_FRIENDLY_NAME, - ) - } - exclude_attrs.update(_exclude_attributes) - + exclude_attrs.update(state.attributes) + exclude_attrs -= _MATCH_ALL_KEEP else: exclude_attrs = ALL_DOMAIN_EXCLUDE_ATTRS encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes From 7319492bf3a14c51e59b5be6fb67c01e5015e5d7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 13:48:43 +0200 Subject: [PATCH 0470/2411] Bump aiomealie to 0.5.0 (#120815) --- homeassistant/components/mealie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index fb81ff850b8..918dd743726 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mealie", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["aiomealie==0.4.0"] + "requirements": ["aiomealie==0.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 60346dd1959..e2badab1ccd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -294,7 +294,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.4.0 +aiomealie==0.5.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3725e8e237..7320c66cbdb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.4.0 +aiomealie==0.5.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 From bb52bfd73d048eef06edcf1ebe247a25de66abd7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 17:48:28 +0200 Subject: [PATCH 0471/2411] Add unique id to Mealie config entry (#120816) --- homeassistant/components/mealie/calendar.py | 5 +--- .../components/mealie/config_flow.py | 5 ++-- homeassistant/components/mealie/entity.py | 7 ++++-- tests/components/mealie/conftest.py | 6 ++++- .../mealie/fixtures/users_self.json | 24 +++++++++++++++++++ .../mealie/snapshots/test_calendar.ambr | 8 +++---- .../mealie/snapshots/test_init.ambr | 2 +- tests/components/mealie/test_config_flow.py | 5 ++-- tests/components/mealie/test_init.py | 2 +- 9 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 tests/components/mealie/fixtures/users_self.json diff --git a/homeassistant/components/mealie/calendar.py b/homeassistant/components/mealie/calendar.py index 08e90ebf5ea..62c1473057d 100644 --- a/homeassistant/components/mealie/calendar.py +++ b/homeassistant/components/mealie/calendar.py @@ -50,12 +50,9 @@ class MealieMealplanCalendarEntity(MealieEntity, CalendarEntity): self, coordinator: MealieCoordinator, entry_type: MealplanEntryType ) -> None: """Create the Calendar entity.""" - super().__init__(coordinator) + super().__init__(coordinator, entry_type.name.lower()) self._entry_type = entry_type self._attr_translation_key = entry_type.name.lower() - self._attr_unique_id = ( - f"{self.coordinator.config_entry.entry_id}_{entry_type.name.lower()}" - ) @property def event(self) -> CalendarEvent | None: diff --git a/homeassistant/components/mealie/config_flow.py b/homeassistant/components/mealie/config_flow.py index b25cade148a..550e4679720 100644 --- a/homeassistant/components/mealie/config_flow.py +++ b/homeassistant/components/mealie/config_flow.py @@ -28,14 +28,13 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" errors: dict[str, str] = {} if user_input: - self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) client = MealieClient( user_input[CONF_HOST], token=user_input[CONF_API_TOKEN], session=async_get_clientsession(self.hass), ) try: - await client.get_mealplan_today() + info = await client.get_user_info() except MealieConnectionError: errors["base"] = "cannot_connect" except MealieAuthenticationError: @@ -44,6 +43,8 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN): LOGGER.exception("Unexpected error") errors["base"] = "unknown" else: + await self.async_set_unique_id(info.user_id) + self._abort_if_unique_id_configured() return self.async_create_entry( title="Mealie", data=user_input, diff --git a/homeassistant/components/mealie/entity.py b/homeassistant/components/mealie/entity.py index 5e339c1d4b8..765ae2b99d7 100644 --- a/homeassistant/components/mealie/entity.py +++ b/homeassistant/components/mealie/entity.py @@ -12,10 +12,13 @@ class MealieEntity(CoordinatorEntity[MealieCoordinator]): _attr_has_entity_name = True - def __init__(self, coordinator: MealieCoordinator) -> None: + def __init__(self, coordinator: MealieCoordinator, key: str) -> None: """Initialize Mealie entity.""" super().__init__(coordinator) + unique_id = coordinator.config_entry.unique_id + assert unique_id is not None + self._attr_unique_id = f"{unique_id}_{key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + identifiers={(DOMAIN, unique_id)}, entry_type=DeviceEntryType.SERVICE, ) diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py index dd6309cb524..9bda9e3c46d 100644 --- a/tests/components/mealie/conftest.py +++ b/tests/components/mealie/conftest.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from aiomealie import Mealplan, MealplanResponse +from aiomealie import Mealplan, MealplanResponse, UserInfo from mashumaro.codecs.orjson import ORJSONDecoder import pytest from typing_extensions import Generator @@ -44,6 +44,9 @@ def mock_mealie_client() -> Generator[AsyncMock]: client.get_mealplan_today.return_value = ORJSONDecoder(list[Mealplan]).decode( load_fixture("get_mealplan_today.json", DOMAIN) ) + client.get_user_info.return_value = UserInfo.from_json( + load_fixture("users_self.json", DOMAIN) + ) yield client @@ -55,4 +58,5 @@ def mock_config_entry() -> MockConfigEntry: title="Mealie", data={CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token"}, entry_id="01J0BC4QM2YBRP6H5G933CETT7", + unique_id="bf1c62fe-4941-4332-9886-e54e88dbdba0", ) diff --git a/tests/components/mealie/fixtures/users_self.json b/tests/components/mealie/fixtures/users_self.json new file mode 100644 index 00000000000..6d5901c8cc0 --- /dev/null +++ b/tests/components/mealie/fixtures/users_self.json @@ -0,0 +1,24 @@ +{ + "id": "bf1c62fe-4941-4332-9886-e54e88dbdba0", + "username": "admin", + "fullName": "Change Me", + "email": "changeme@example.com", + "authMethod": "Mealie", + "admin": true, + "group": "home", + "advanced": true, + "canInvite": true, + "canManage": true, + "canOrganize": true, + "groupId": "24477569-f6af-4b53-9e3f-6d04b0ca6916", + "groupSlug": "home", + "tokens": [ + { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb25nX3Rva2VuIjp0cnVlLCJpZCI6ImJmMWM2MmZlLTQ5NDEtNDMzMi05ODg2LWU1NGU4OGRiZGJhMCIsIm5hbWUiOiJ0ZXN0aW5nIiwiaW50ZWdyYXRpb25faWQiOiJnZW5lcmljIiwiZXhwIjoxODczOTA5ODk4fQ.xwXZp4fL2g1RbIqGtBeOaS6RDfsYbQDHj8XtRM3wlX0", + "name": "testing", + "id": 2, + "createdAt": "2024-05-20T10:31:38.179669" + } + ], + "cacheKey": "1234" +} diff --git a/tests/components/mealie/snapshots/test_calendar.ambr b/tests/components/mealie/snapshots/test_calendar.ambr index 6af53c112de..3db0da0d765 100644 --- a/tests/components/mealie/snapshots/test_calendar.ambr +++ b/tests/components/mealie/snapshots/test_calendar.ambr @@ -178,7 +178,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'breakfast', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_breakfast', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_breakfast', 'unit_of_measurement': None, }) # --- @@ -230,7 +230,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'dinner', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_dinner', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_dinner', 'unit_of_measurement': None, }) # --- @@ -282,7 +282,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'lunch', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_lunch', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_lunch', 'unit_of_measurement': None, }) # --- @@ -334,7 +334,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'side', - 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_side', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_side', 'unit_of_measurement': None, }) # --- diff --git a/tests/components/mealie/snapshots/test_init.ambr b/tests/components/mealie/snapshots/test_init.ambr index 1333b292dac..8f800676945 100644 --- a/tests/components/mealie/snapshots/test_init.ambr +++ b/tests/components/mealie/snapshots/test_init.ambr @@ -13,7 +13,7 @@ 'identifiers': set({ tuple( 'mealie', - '01J0BC4QM2YBRP6H5G933CETT7', + 'bf1c62fe-4941-4332-9886-e54e88dbdba0', ), }), 'is_new': False, diff --git a/tests/components/mealie/test_config_flow.py b/tests/components/mealie/test_config_flow.py index ac68ed2fac5..777bb1e4ad1 100644 --- a/tests/components/mealie/test_config_flow.py +++ b/tests/components/mealie/test_config_flow.py @@ -37,6 +37,7 @@ async def test_full_flow( CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token", } + assert result["result"].unique_id == "bf1c62fe-4941-4332-9886-e54e88dbdba0" @pytest.mark.parametrize( @@ -55,7 +56,7 @@ async def test_flow_errors( error: str, ) -> None: """Test flow errors.""" - mock_mealie_client.get_mealplan_today.side_effect = exception + mock_mealie_client.get_user_info.side_effect = exception result = await hass.config_entries.flow.async_init( DOMAIN, @@ -72,7 +73,7 @@ async def test_flow_errors( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} - mock_mealie_client.get_mealplan_today.side_effect = None + mock_mealie_client.get_user_info.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/mealie/test_init.py b/tests/components/mealie/test_init.py index 7d63ad135f9..5a7a5387897 100644 --- a/tests/components/mealie/test_init.py +++ b/tests/components/mealie/test_init.py @@ -26,7 +26,7 @@ async def test_device_info( """Test device registry integration.""" await setup_integration(hass, mock_config_entry) device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, mock_config_entry.entry_id)} + identifiers={(DOMAIN, mock_config_entry.unique_id)} ) assert device_entry is not None assert device_entry == snapshot From 05c63eb88491ca0c1bfe18a2f91ed9aae54cf749 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 14:53:42 +0200 Subject: [PATCH 0472/2411] Bump python-opensky to 1.0.1 (#120818) --- homeassistant/components/opensky/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opensky/manifest.json b/homeassistant/components/opensky/manifest.json index 106103cf752..831abbc9cbf 100644 --- a/homeassistant/components/opensky/manifest.json +++ b/homeassistant/components/opensky/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/opensky", "iot_class": "cloud_polling", - "requirements": ["python-opensky==1.0.0"] + "requirements": ["python-opensky==1.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e2badab1ccd..ba3e2f5fadb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2296,7 +2296,7 @@ python-mystrom==2.2.0 python-opendata-transport==0.4.0 # homeassistant.components.opensky -python-opensky==1.0.0 +python-opensky==1.0.1 # homeassistant.components.otbr # homeassistant.components.thread diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7320c66cbdb..b0a6fb54b3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1793,7 +1793,7 @@ python-mystrom==2.2.0 python-opendata-transport==0.4.0 # homeassistant.components.opensky -python-opensky==1.0.0 +python-opensky==1.0.1 # homeassistant.components.otbr # homeassistant.components.thread From e866417c01c932dcf8ba894e6ca15eff82afb71b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 17:46:44 +0200 Subject: [PATCH 0473/2411] Add icons to Airgradient (#120820) --- .../components/airgradient/icons.json | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/homeassistant/components/airgradient/icons.json b/homeassistant/components/airgradient/icons.json index 45d1e12d46e..22188d72faa 100644 --- a/homeassistant/components/airgradient/icons.json +++ b/homeassistant/components/airgradient/icons.json @@ -8,6 +8,34 @@ "default": "mdi:lightbulb-on-outline" } }, + "number": { + "led_bar_brightness": { + "default": "mdi:brightness-percent" + }, + "display_brightness": { + "default": "mdi:brightness-percent" + } + }, + "select": { + "configuration_control": { + "default": "mdi:cloud-cog" + }, + "display_temperature_unit": { + "default": "mdi:thermometer-lines" + }, + "led_bar_mode": { + "default": "mdi:led-strip" + }, + "nox_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "voc_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "co2_automatic_baseline_calibration": { + "default": "mdi:molecule-co2" + } + }, "sensor": { "total_volatile_organic_component_index": { "default": "mdi:molecule" @@ -17,6 +45,32 @@ }, "pm003_count": { "default": "mdi:blur" + }, + "led_bar_brightness": { + "default": "mdi:brightness-percent" + }, + "display_brightness": { + "default": "mdi:brightness-percent" + }, + "display_temperature_unit": { + "default": "mdi:thermometer-lines" + }, + "led_bar_mode": { + "default": "mdi:led-strip" + }, + "nox_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "voc_index_learning_time_offset": { + "default": "mdi:clock-outline" + }, + "co2_automatic_baseline_calibration": { + "default": "mdi:molecule-co2" + } + }, + "switch": { + "post_data_to_airgradient": { + "default": "mdi:cogs" } } } From 3ee8f6edbac8fc1a6c8ec8e45887f3457505c247 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 29 Jun 2024 17:47:21 +0200 Subject: [PATCH 0474/2411] Use meal note as fallback in Mealie (#120828) --- homeassistant/components/mealie/calendar.py | 4 ++-- .../components/mealie/fixtures/get_mealplans.json | 11 +++++++++++ .../components/mealie/snapshots/test_calendar.ambr | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mealie/calendar.py b/homeassistant/components/mealie/calendar.py index 62c1473057d..fb628754f06 100644 --- a/homeassistant/components/mealie/calendar.py +++ b/homeassistant/components/mealie/calendar.py @@ -30,8 +30,8 @@ async def async_setup_entry( def _get_event_from_mealplan(mealplan: Mealplan) -> CalendarEvent: """Create a CalendarEvent from a Mealplan.""" - description: str | None = None - name = "No recipe" + description: str | None = mealplan.description + name = mealplan.title or "No recipe" if mealplan.recipe: name = mealplan.recipe.name description = mealplan.recipe.description diff --git a/tests/components/mealie/fixtures/get_mealplans.json b/tests/components/mealie/fixtures/get_mealplans.json index 2d63b753d99..9255f9b7396 100644 --- a/tests/components/mealie/fixtures/get_mealplans.json +++ b/tests/components/mealie/fixtures/get_mealplans.json @@ -605,6 +605,17 @@ "updateAt": "2024-01-02T06:35:05.209189", "lastMade": "2024-01-02T22:59:59" } + }, + { + "date": "2024-01-21", + "entryType": "dinner", + "title": "Aquavite", + "text": "Dineren met de boys", + "recipeId": null, + "id": 1, + "groupId": "3931df86-0679-4579-8c63-4bedc9ca9a85", + "userId": "6caa6e4d-521f-4ef4-9ed7-388bdd63f47d", + "recipe": null } ], "next": null, diff --git a/tests/components/mealie/snapshots/test_calendar.ambr b/tests/components/mealie/snapshots/test_calendar.ambr index 3db0da0d765..c3b26e1e9e2 100644 --- a/tests/components/mealie/snapshots/test_calendar.ambr +++ b/tests/components/mealie/snapshots/test_calendar.ambr @@ -147,6 +147,20 @@ 'summary': 'Mousse de saumon', 'uid': None, }), + dict({ + 'description': 'Dineren met de boys', + 'end': dict({ + 'date': '2024-01-22', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-21', + }), + 'summary': 'Aquavite', + 'uid': None, + }), ]) # --- # name: test_entities[calendar.mealie_breakfast-entry] From 08a0eaf1847d995995909d2f7ae53c5821cd594e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 29 Jun 2024 17:51:45 +0200 Subject: [PATCH 0475/2411] Bump version to 2024.7.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1ab2a3f6893..fa19aa7349e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index e96c329fd5a..3b42dfa2d6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b4" +version = "2024.7.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 8f98fb2ec44c586c65d6d6e74708512f46fca5cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 11:06:56 -0500 Subject: [PATCH 0476/2411] Fix publish cancellation handling in MQTT (#120826) --- homeassistant/components/mqtt/client.py | 4 ++-- tests/components/mqtt/test_client.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 7788c1db641..f65769badfa 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -1141,8 +1141,8 @@ class MQTT: # see https://github.com/eclipse/paho.mqtt.python/issues/687 # properties and reason codes are not used in Home Assistant future = self._async_get_mid_future(mid) - if future.done() and future.exception(): - # Timed out + if future.done() and (future.cancelled() or future.exception()): + # Timed out or cancelled return future.set_result(None) diff --git a/tests/components/mqtt/test_client.py b/tests/components/mqtt/test_client.py index 49b590383d1..cd02d805e1c 100644 --- a/tests/components/mqtt/test_client.py +++ b/tests/components/mqtt/test_client.py @@ -1194,6 +1194,23 @@ async def test_handle_mqtt_on_callback( assert "No ACK from MQTT server" not in caplog.text +async def test_handle_mqtt_on_callback_after_cancellation( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test receiving an ACK after a cancellation.""" + mqtt_mock = await mqtt_mock_entry() + # Simulate the mid future getting a cancellation + mqtt_mock()._async_get_mid_future(101).cancel() + # Simulate an ACK for mid == 101, being received after the cancellation + mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) + await hass.async_block_till_done() + assert "No ACK from MQTT server" not in caplog.text + assert "InvalidStateError" not in caplog.text + + async def test_handle_mqtt_on_callback_after_timeout( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, From b5f1076bb232f8a8207d1ae7d008e01d7d6d61d6 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 29 Jun 2024 11:10:35 -0500 Subject: [PATCH 0477/2411] Bump plexapi to 4.15.14 (#120832) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 3393ed1ec81..323bca0477a 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["plexapi", "plexwebsocket"], "requirements": [ - "PlexAPI==4.15.13", + "PlexAPI==4.15.14", "plexauth==0.0.6", "plexwebsocket==0.0.14" ], diff --git a/requirements_all.txt b/requirements_all.txt index fa286e02801..3fd529d0dc7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -39,7 +39,7 @@ Mastodon.py==1.8.1 Pillow==10.3.0 # homeassistant.components.plex -PlexAPI==4.15.13 +PlexAPI==4.15.14 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88615ede4ea..e21d01f4f8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ HATasmota==0.9.2 Pillow==10.3.0 # homeassistant.components.plex -PlexAPI==4.15.13 +PlexAPI==4.15.14 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 From 559caf41793c78dc752b3ae7bbab3d97170d76de Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 29 Jun 2024 19:11:22 +0200 Subject: [PATCH 0478/2411] Use eventing for some of the upnp sensors, instead of polling (#120262) --- homeassistant/components/upnp/__init__.py | 10 +- .../components/upnp/binary_sensor.py | 12 ++- homeassistant/components/upnp/coordinator.py | 28 +++++- homeassistant/components/upnp/device.py | 95 +++++++++++++++++-- homeassistant/components/upnp/sensor.py | 12 ++- tests/components/upnp/conftest.py | 23 ++++- tests/components/upnp/test_init.py | 7 +- 7 files changed, 170 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index ea9930f047f..df8c6326e10 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -82,14 +82,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: UpnpConfigEntry) -> bool assert discovery_info is not None assert discovery_info.ssdp_udn assert discovery_info.ssdp_all_locations + force_poll = False location = get_preferred_location(discovery_info.ssdp_all_locations) try: - device = await async_create_device(hass, location) + device = await async_create_device(hass, location, force_poll) except UpnpConnectionError as err: raise ConfigEntryNotReady( f"Error connecting to device at location: {location}, err: {err}" ) from err + # Try to subscribe, if configured. + if not force_poll: + await device.async_subscribe_services() + + # Unsubscribe services on unload. + entry.async_on_unload(device.async_unsubscribe_services) + # Track the original UDN such that existing sensors do not change their unique_id. if CONFIG_ENTRY_ORIGINAL_UDN not in entry.data: hass.config_entries.async_update_entry( diff --git a/homeassistant/components/upnp/binary_sensor.py b/homeassistant/components/upnp/binary_sensor.py index 9784f9c6e0b..fb32946bf7d 100644 --- a/homeassistant/components/upnp/binary_sensor.py +++ b/homeassistant/components/upnp/binary_sensor.py @@ -51,8 +51,8 @@ async def async_setup_entry( for entity_description in SENSOR_DESCRIPTIONS if coordinator.data.get(entity_description.key) is not None ] - LOGGER.debug("Adding binary_sensor entities: %s", entities) async_add_entities(entities) + LOGGER.debug("Added binary_sensor entities: %s", entities) class UpnpStatusBinarySensor(UpnpEntity, BinarySensorEntity): @@ -72,3 +72,13 @@ class UpnpStatusBinarySensor(UpnpEntity, BinarySensorEntity): def is_on(self) -> bool: """Return true if the binary sensor is on.""" return self.coordinator.data[self.entity_description.key] == "Connected" + + async def async_added_to_hass(self) -> None: + """Subscribe to updates.""" + await super().async_added_to_hass() + + # Register self at coordinator. + key = self.entity_description.key + entity_id = self.entity_id + unregister = self.coordinator.register_entity(key, entity_id) + self.async_on_remove(unregister) diff --git a/homeassistant/components/upnp/coordinator.py b/homeassistant/components/upnp/coordinator.py index 72e14ecc4ff..a4cb608615c 100644 --- a/homeassistant/components/upnp/coordinator.py +++ b/homeassistant/components/upnp/coordinator.py @@ -1,5 +1,7 @@ """UPnP/IGD coordinator.""" +from collections import defaultdict +from collections.abc import Callable from datetime import datetime, timedelta from async_upnp_client.exceptions import UpnpCommunicationError @@ -27,6 +29,7 @@ class UpnpDataUpdateCoordinator( """Initialize.""" self.device = device self.device_entry = device_entry + self._features_by_entity_id: defaultdict[str, set[str]] = defaultdict(set) super().__init__( hass, @@ -35,12 +38,35 @@ class UpnpDataUpdateCoordinator( update_interval=update_interval, ) + def register_entity(self, key: str, entity_id: str) -> Callable[[], None]: + """Register an entity.""" + # self._entities.append(entity) + self._features_by_entity_id[key].add(entity_id) + + def unregister_entity() -> None: + """Unregister entity.""" + self._features_by_entity_id[key].remove(entity_id) + + if not self._features_by_entity_id[key]: + del self._features_by_entity_id[key] + + return unregister_entity + + @property + def _entity_description_keys(self) -> list[str] | None: + """Return a list of entity description keys for which data is required.""" + if not self._features_by_entity_id: + # Must be the first update, no entities attached/enabled yet. + return None + + return list(self._features_by_entity_id.keys()) + async def _async_update_data( self, ) -> dict[str, str | datetime | int | float | None]: """Update data.""" try: - return await self.device.async_get_data() + return await self.device.async_get_data(self._entity_description_keys) except UpnpCommunicationError as exception: LOGGER.debug( "Caught exception when updating device: %s, exception: %s", diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index bb0bcfc6a6e..e819a16f2d2 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -8,9 +8,12 @@ from ipaddress import ip_address from typing import Any from urllib.parse import urlparse -from async_upnp_client.aiohttp import AiohttpSessionRequester +from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester from async_upnp_client.client_factory import UpnpFactory -from async_upnp_client.profiles.igd import IgdDevice +from async_upnp_client.const import AddressTupleVXType +from async_upnp_client.exceptions import UpnpConnectionError +from async_upnp_client.profiles.igd import IgdDevice, IgdStateItem +from async_upnp_client.utils import async_get_local_ip from getmac import get_mac_address from homeassistant.core import HomeAssistant @@ -33,6 +36,20 @@ from .const import ( WAN_STATUS, ) +TYPE_STATE_ITEM_MAPPING = { + BYTES_RECEIVED: IgdStateItem.BYTES_RECEIVED, + BYTES_SENT: IgdStateItem.BYTES_SENT, + KIBIBYTES_PER_SEC_RECEIVED: IgdStateItem.KIBIBYTES_PER_SEC_RECEIVED, + KIBIBYTES_PER_SEC_SENT: IgdStateItem.KIBIBYTES_PER_SEC_SENT, + PACKETS_PER_SEC_RECEIVED: IgdStateItem.PACKETS_PER_SEC_RECEIVED, + PACKETS_PER_SEC_SENT: IgdStateItem.PACKETS_PER_SEC_SENT, + PACKETS_RECEIVED: IgdStateItem.PACKETS_RECEIVED, + PACKETS_SENT: IgdStateItem.PACKETS_SENT, + ROUTER_IP: IgdStateItem.EXTERNAL_IP_ADDRESS, + ROUTER_UPTIME: IgdStateItem.UPTIME, + WAN_STATUS: IgdStateItem.CONNECTION_STATUS, +} + def get_preferred_location(locations: set[str]) -> str: """Get the preferred location (an IPv4 location) from a set of locations.""" @@ -64,26 +81,43 @@ async def async_get_mac_address_from_host(hass: HomeAssistant, host: str) -> str return mac_address -async def async_create_device(hass: HomeAssistant, location: str) -> Device: +async def async_create_device( + hass: HomeAssistant, location: str, force_poll: bool +) -> Device: """Create UPnP/IGD device.""" session = async_get_clientsession(hass, verify_ssl=False) requester = AiohttpSessionRequester(session, with_sleep=True, timeout=20) + # Create UPnP device. factory = UpnpFactory(requester, non_strict=True) upnp_device = await factory.async_create_device(location) + # Create notify server. + _, local_ip = await async_get_local_ip(location) + source: AddressTupleVXType = (local_ip, 0) + notify_server = AiohttpNotifyServer( + requester=requester, + source=source, + ) + await notify_server.async_start_server() + _LOGGER.debug("Started event handler at %s", notify_server.callback_url) + # Create profile wrapper. - igd_device = IgdDevice(upnp_device, None) - return Device(hass, igd_device) + igd_device = IgdDevice(upnp_device, notify_server.event_handler) + return Device(hass, igd_device, force_poll) class Device: """Home Assistant representation of a UPnP/IGD device.""" - def __init__(self, hass: HomeAssistant, igd_device: IgdDevice) -> None: + def __init__( + self, hass: HomeAssistant, igd_device: IgdDevice, force_poll: bool + ) -> None: """Initialize UPnP/IGD device.""" self.hass = hass self._igd_device = igd_device + self._force_poll = force_poll + self.coordinator: ( DataUpdateCoordinator[dict[str, str | datetime | int | float | None]] | None ) = None @@ -151,11 +185,54 @@ class Device: """Get string representation.""" return f"IGD Device: {self.name}/{self.udn}::{self.device_type}" - async def async_get_data(self) -> dict[str, str | datetime | int | float | None]: + @property + def force_poll(self) -> bool: + """Get force_poll.""" + return self._force_poll + + async def async_set_force_poll(self, force_poll: bool) -> None: + """Set force_poll, and (un)subscribe if needed.""" + self._force_poll = force_poll + + if self._force_poll: + # No need for subscriptions, as eventing will never be used. + await self.async_unsubscribe_services() + elif not self._force_poll and not self._igd_device.is_subscribed: + await self.async_subscribe_services() + + async def async_subscribe_services(self) -> None: + """Subscribe to services.""" + try: + await self._igd_device.async_subscribe_services(auto_resubscribe=True) + except UpnpConnectionError as ex: + _LOGGER.debug( + "Error subscribing to services, falling back to forced polling: %s", ex + ) + await self.async_set_force_poll(True) + + async def async_unsubscribe_services(self) -> None: + """Unsubscribe from services.""" + await self._igd_device.async_unsubscribe_services() + + async def async_get_data( + self, entity_description_keys: list[str] | None + ) -> dict[str, str | datetime | int | float | None]: """Get all data from device.""" - _LOGGER.debug("Getting data for device: %s", self) + if not entity_description_keys: + igd_state_items = None + else: + igd_state_items = { + TYPE_STATE_ITEM_MAPPING[key] for key in entity_description_keys + } + + _LOGGER.debug( + "Getting data for device: %s, state_items: %s, force_poll: %s", + self, + igd_state_items, + self._force_poll, + ) igd_state = await self._igd_device.async_get_traffic_and_status_data( - force_poll=True + igd_state_items, force_poll=self._force_poll ) def get_value(value: Any) -> Any: diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index df7128830b3..d72dce55eab 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -159,8 +159,8 @@ async def async_setup_entry( if coordinator.data.get(entity_description.key) is not None ] - LOGGER.debug("Adding sensor entities: %s", entities) async_add_entities(entities) + LOGGER.debug("Added sensor entities: %s", entities) class UpnpSensor(UpnpEntity, SensorEntity): @@ -174,3 +174,13 @@ class UpnpSensor(UpnpEntity, SensorEntity): if (key := self.entity_description.value_key) is None: return None return self.coordinator.data[key] + + async def async_added_to_hass(self) -> None: + """Subscribe to updates.""" + await super().async_added_to_hass() + + # Register self at coordinator. + key = self.entity_description.key + entity_id = self.entity_id + unregister = self.coordinator.register_entity(key, entity_id) + self.async_on_remove(unregister) diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index 0bfcd062ac0..8f5de1fa824 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -4,9 +4,11 @@ from __future__ import annotations import copy from datetime import datetime +import socket from unittest.mock import AsyncMock, MagicMock, PropertyMock, create_autospec, patch from urllib.parse import urlparse +from async_upnp_client.aiohttp import AiohttpNotifyServer from async_upnp_client.client import UpnpDevice from async_upnp_client.profiles.igd import IgdDevice, IgdState import pytest @@ -98,9 +100,24 @@ def mock_igd_device(mock_async_create_device) -> IgdDevice: port_mapping_number_of_entries=0, ) - with patch( - "homeassistant.components.upnp.device.IgdDevice.__new__", - return_value=mock_igd_device, + mock_igd_device.async_subscribe_services = AsyncMock() + + mock_notify_server = create_autospec(AiohttpNotifyServer) + mock_notify_server.event_handler = MagicMock() + + with ( + patch( + "homeassistant.components.upnp.device.async_get_local_ip", + return_value=(socket.AF_INET, "127.0.0.1"), + ), + patch( + "homeassistant.components.upnp.device.IgdDevice.__new__", + return_value=mock_igd_device, + ), + patch( + "homeassistant.components.upnp.device.AiohttpNotifyServer.__new__", + return_value=mock_notify_server, + ), ): yield mock_igd_device diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 4b5e375f8e0..422d8c9e33a 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -5,6 +5,7 @@ from __future__ import annotations import copy from unittest.mock import AsyncMock, MagicMock, patch +from async_upnp_client.profiles.igd import IgdDevice import pytest from homeassistant.components import ssdp @@ -31,7 +32,9 @@ from tests.common import MockConfigEntry @pytest.mark.usefixtures("ssdp_instant_discovery", "mock_mac_address_from_host") -async def test_async_setup_entry_default(hass: HomeAssistant) -> None: +async def test_async_setup_entry_default( + hass: HomeAssistant, mock_igd_device: IgdDevice +) -> None: """Test async_setup_entry.""" entry = MockConfigEntry( domain=DOMAIN, @@ -49,6 +52,8 @@ async def test_async_setup_entry_default(hass: HomeAssistant) -> None: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) is True + mock_igd_device.async_subscribe_services.assert_called() + @pytest.mark.usefixtures("ssdp_instant_discovery", "mock_no_mac_address_from_host") async def test_async_setup_entry_default_no_mac_address(hass: HomeAssistant) -> None: From bcee5f4d9fbaeb1a9c8f6a8d37fdff3d57d3a3c6 Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Sat, 29 Jun 2024 19:32:22 +0200 Subject: [PATCH 0479/2411] Store runtime data inside the config entry in solarlog (#120773) --- homeassistant/components/solarlog/__init__.py | 12 +++++++----- homeassistant/components/solarlog/coordinator.py | 9 +++++++-- homeassistant/components/solarlog/sensor.py | 9 +++++---- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py index 6975a420732..962efa4e190 100644 --- a/homeassistant/components/solarlog/__init__.py +++ b/homeassistant/components/solarlog/__init__.py @@ -7,29 +7,31 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .const import DOMAIN from .coordinator import SolarlogData _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR] +type SolarlogConfigEntry = ConfigEntry[SolarlogData] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: SolarlogConfigEntry) -> bool: """Set up a config entry for solarlog.""" coordinator = SolarlogData(hass, entry) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: SolarlogConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_migrate_entry( + hass: HomeAssistant, config_entry: SolarlogConfigEntry +) -> bool: """Migrate old entry.""" _LOGGER.debug("Migrating from version %s", config_entry.version) diff --git a/homeassistant/components/solarlog/coordinator.py b/homeassistant/components/solarlog/coordinator.py index 794e556add5..d2963e1950e 100644 --- a/homeassistant/components/solarlog/coordinator.py +++ b/homeassistant/components/solarlog/coordinator.py @@ -1,7 +1,10 @@ """DataUpdateCoordinator for solarlog integration.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import TYPE_CHECKING from urllib.parse import ParseResult, urlparse from solarlog_cli.solarlog_connector import SolarLogConnector @@ -10,7 +13,6 @@ from solarlog_cli.solarlog_exceptions import ( SolarLogUpdateError, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -18,11 +20,14 @@ from homeassistant.helpers import update_coordinator _LOGGER = logging.getLogger(__name__) +if TYPE_CHECKING: + from . import SolarlogConfigEntry + class SolarlogData(update_coordinator.DataUpdateCoordinator): """Get and update the latest data.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, entry: SolarlogConfigEntry) -> None: """Initialize the data object.""" super().__init__( hass, _LOGGER, name="SolarLog", update_interval=timedelta(seconds=60) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index a0d6d4bc540..45961133e8a 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, UnitOfElectricPotential, @@ -22,7 +21,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import SolarlogData +from . import SolarlogConfigEntry, SolarlogData from .const import DOMAIN @@ -201,10 +200,12 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: SolarlogConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add solarlog entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( SolarlogSensor(coordinator, description) for description in SENSOR_TYPES ) From e0b0959b3fccd4b8239aee8709926b263b5458a7 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 29 Jun 2024 22:07:37 +0300 Subject: [PATCH 0480/2411] Bump aiowebostv to 0.4.1 (#120838) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index ed8e1a6cc6e..bcafb82a4b0 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_push", "loggers": ["aiowebostv"], "quality_scale": "platinum", - "requirements": ["aiowebostv==0.4.0"], + "requirements": ["aiowebostv==0.4.1"], "ssdp": [ { "st": "urn:lge-com:service:webos-second-screen:1" diff --git a/requirements_all.txt b/requirements_all.txt index 3fd529d0dc7..adb3a15f21d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.4.0 +aiowebostv==0.4.1 # homeassistant.components.withings aiowithings==3.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e21d01f4f8e..cc39b9faabd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -377,7 +377,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.4.0 +aiowebostv==0.4.1 # homeassistant.components.withings aiowithings==3.0.2 From 7172d798f8d77e6725808a01e37a4e6eb5d86290 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Sun, 30 Jun 2024 01:08:24 +0300 Subject: [PATCH 0481/2411] Fix Jewish calendar unique id move to entity (#120842) --- homeassistant/components/jewish_calendar/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/jewish_calendar/entity.py b/homeassistant/components/jewish_calendar/entity.py index aba76599f63..c11925df954 100644 --- a/homeassistant/components/jewish_calendar/entity.py +++ b/homeassistant/components/jewish_calendar/entity.py @@ -28,7 +28,7 @@ class JewishCalendarEntity(Entity): ) -> None: """Initialize a Jewish Calendar entity.""" self.entity_description = description - self._attr_unique_id = f"{config_entry.entry_id}_{description.key}" + self._attr_unique_id = f"{config_entry.entry_id}-{description.key}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, config_entry.entry_id)}, From 5280291f98db41b6edd822a6b2fe6df4dea3df6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 18:43:20 -0500 Subject: [PATCH 0482/2411] Add linked doorbell event support to HomeKit (#120834) --- homeassistant/components/homekit/__init__.py | 59 +++++---- .../components/homekit/type_cameras.py | 73 ++++++----- tests/components/homekit/test_homekit.py | 77 +++++++++++ tests/components/homekit/test_type_cameras.py | 121 +++++++++++++++++- 4 files changed, 273 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 828f8bf94d6..7e1bbad70b4 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections import defaultdict from collections.abc import Iterable from copy import deepcopy import ipaddress @@ -29,6 +30,7 @@ from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.device_automation.trigger import ( async_validate_trigger_config, ) +from homeassistant.components.event import DOMAIN as EVENT_DOMAIN, EventDeviceClass from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass @@ -156,6 +158,17 @@ _HAS_IPV6 = hasattr(socket, "AF_INET6") _DEFAULT_BIND = ["0.0.0.0", "::"] if _HAS_IPV6 else ["0.0.0.0"] +BATTERY_CHARGING_SENSOR = ( + BINARY_SENSOR_DOMAIN, + BinarySensorDeviceClass.BATTERY_CHARGING, +) +BATTERY_SENSOR = (SENSOR_DOMAIN, SensorDeviceClass.BATTERY) +MOTION_SENSOR = (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.MOTION) +DOORBELL_EVENT_SENSOR = (EVENT_DOMAIN, EventDeviceClass.DOORBELL) +DOORBELL_BINARY_SENSOR = (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.OCCUPANCY) +HUMIDITY_SENSOR = (SENSOR_DOMAIN, SensorDeviceClass.HUMIDITY) + + def _has_all_unique_names_and_ports( bridges: list[dict[str, Any]], ) -> list[dict[str, Any]]: @@ -522,7 +535,7 @@ class HomeKit: ip_address: str | None, entity_filter: EntityFilter, exclude_accessory_mode: bool, - entity_config: dict, + entity_config: dict[str, Any], homekit_mode: str, advertise_ips: list[str], entry_id: str, @@ -535,7 +548,9 @@ class HomeKit: self._port = port self._ip_address = ip_address self._filter = entity_filter - self._config = entity_config + self._config: defaultdict[str, dict[str, Any]] = defaultdict( + dict, entity_config + ) self._exclude_accessory_mode = exclude_accessory_mode self._advertise_ips = advertise_ips self._entry_id = entry_id @@ -1074,7 +1089,7 @@ class HomeKit: def _async_configure_linked_sensors( self, ent_reg_ent: er.RegistryEntry, - device_lookup: dict[tuple[str, str | None], str], + lookup: dict[tuple[str, str | None], str], state: State, ) -> None: if (ent_reg_ent.device_class or ent_reg_ent.original_device_class) in ( @@ -1085,46 +1100,44 @@ class HomeKit: domain = state.domain attributes = state.attributes + config = self._config + entity_id = state.entity_id if ATTR_BATTERY_CHARGING not in attributes and ( - battery_charging_binary_sensor_entity_id := device_lookup.get( - (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.BATTERY_CHARGING) + battery_charging_binary_sensor_entity_id := lookup.get( + BATTERY_CHARGING_SENSOR ) ): - self._config.setdefault(state.entity_id, {}).setdefault( + config[entity_id].setdefault( CONF_LINKED_BATTERY_CHARGING_SENSOR, battery_charging_binary_sensor_entity_id, ) if ATTR_BATTERY_LEVEL not in attributes and ( - battery_sensor_entity_id := device_lookup.get( - (SENSOR_DOMAIN, SensorDeviceClass.BATTERY) - ) + battery_sensor_entity_id := lookup.get(BATTERY_SENSOR) ): - self._config.setdefault(state.entity_id, {}).setdefault( + config[entity_id].setdefault( CONF_LINKED_BATTERY_SENSOR, battery_sensor_entity_id ) if domain == CAMERA_DOMAIN: - if motion_binary_sensor_entity_id := device_lookup.get( - (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.MOTION) - ): - self._config.setdefault(state.entity_id, {}).setdefault( + if motion_binary_sensor_entity_id := lookup.get(MOTION_SENSOR): + config[entity_id].setdefault( CONF_LINKED_MOTION_SENSOR, motion_binary_sensor_entity_id ) - if doorbell_binary_sensor_entity_id := device_lookup.get( - (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.OCCUPANCY) - ): - self._config.setdefault(state.entity_id, {}).setdefault( + if doorbell_event_entity_id := lookup.get(DOORBELL_EVENT_SENSOR): + config[entity_id].setdefault( + CONF_LINKED_DOORBELL_SENSOR, doorbell_event_entity_id + ) + elif doorbell_binary_sensor_entity_id := lookup.get(DOORBELL_BINARY_SENSOR): + config[entity_id].setdefault( CONF_LINKED_DOORBELL_SENSOR, doorbell_binary_sensor_entity_id ) if domain == HUMIDIFIER_DOMAIN and ( - current_humidity_sensor_entity_id := device_lookup.get( - (SENSOR_DOMAIN, SensorDeviceClass.HUMIDITY) - ) + current_humidity_sensor_entity_id := lookup.get(HUMIDITY_SENSOR) ): - self._config.setdefault(state.entity_id, {}).setdefault( + config[entity_id].setdefault( CONF_LINKED_HUMIDITY_SENSOR, current_humidity_sensor_entity_id ) @@ -1135,7 +1148,7 @@ class HomeKit: entity_id: str, ) -> None: """Set attributes that will be used for homekit device info.""" - ent_cfg = self._config.setdefault(entity_id, {}) + ent_cfg = self._config[entity_id] if ent_reg_ent.device_id: if dev_reg_ent := dev_reg.async_get(ent_reg_ent.device_id): self._fill_config_from_device_registry_entry(dev_reg_ent, ent_cfg) diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index b5764520b61..ca3a2f0d021 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -16,7 +16,7 @@ from pyhap.util import callback as pyhap_callback from homeassistant.components import camera from homeassistant.components.ffmpeg import get_ffmpeg_manager -from homeassistant.const import STATE_ON +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import ( Event, EventStateChangedData, @@ -234,30 +234,35 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] self._char_doorbell_detected = None self._char_doorbell_detected_switch = None - self.linked_doorbell_sensor = self.config.get(CONF_LINKED_DOORBELL_SENSOR) - if self.linked_doorbell_sensor: - state = self.hass.states.get(self.linked_doorbell_sensor) - if state: - serv_doorbell = self.add_preload_service(SERV_DOORBELL) - self.set_primary_service(serv_doorbell) - self._char_doorbell_detected = serv_doorbell.configure_char( - CHAR_PROGRAMMABLE_SWITCH_EVENT, - value=0, - ) - serv_stateless_switch = self.add_preload_service( - SERV_STATELESS_PROGRAMMABLE_SWITCH - ) - self._char_doorbell_detected_switch = ( - serv_stateless_switch.configure_char( - CHAR_PROGRAMMABLE_SWITCH_EVENT, - value=0, - valid_values={"SinglePress": DOORBELL_SINGLE_PRESS}, - ) - ) - serv_speaker = self.add_preload_service(SERV_SPEAKER) - serv_speaker.configure_char(CHAR_MUTE, value=0) + linked_doorbell_sensor: str | None = self.config.get( + CONF_LINKED_DOORBELL_SENSOR + ) + self.linked_doorbell_sensor = linked_doorbell_sensor + self.doorbell_is_event = False + if not linked_doorbell_sensor: + return + self.doorbell_is_event = linked_doorbell_sensor.startswith("event.") + if not (state := self.hass.states.get(linked_doorbell_sensor)): + return + serv_doorbell = self.add_preload_service(SERV_DOORBELL) + self.set_primary_service(serv_doorbell) + self._char_doorbell_detected = serv_doorbell.configure_char( + CHAR_PROGRAMMABLE_SWITCH_EVENT, + value=0, + ) + serv_stateless_switch = self.add_preload_service( + SERV_STATELESS_PROGRAMMABLE_SWITCH + ) + self._char_doorbell_detected_switch = serv_stateless_switch.configure_char( + CHAR_PROGRAMMABLE_SWITCH_EVENT, + value=0, + valid_values={"SinglePress": DOORBELL_SINGLE_PRESS}, + ) + serv_speaker = self.add_preload_service(SERV_SPEAKER) + serv_speaker.configure_char(CHAR_MUTE, value=0) - self._async_update_doorbell_state(state) + if not self.doorbell_is_event: + self._async_update_doorbell_state(state) @pyhap_callback # type: ignore[misc] @callback @@ -271,7 +276,7 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] self._subscriptions.append( async_track_state_change_event( self.hass, - [self.linked_motion_sensor], + self.linked_motion_sensor, self._async_update_motion_state_event, job_type=HassJobType.Callback, ) @@ -282,7 +287,7 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] self._subscriptions.append( async_track_state_change_event( self.hass, - [self.linked_doorbell_sensor], + self.linked_doorbell_sensor, self._async_update_doorbell_state_event, job_type=HassJobType.Callback, ) @@ -322,18 +327,20 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] self, event: Event[EventStateChangedData] ) -> None: """Handle state change event listener callback.""" - if not state_changed_event_is_same_state(event): - self._async_update_doorbell_state(event.data["new_state"]) + if not state_changed_event_is_same_state(event) and ( + new_state := event.data["new_state"] + ): + self._async_update_doorbell_state(new_state) @callback - def _async_update_doorbell_state(self, new_state: State | None) -> None: + def _async_update_doorbell_state(self, new_state: State) -> None: """Handle link doorbell sensor state change to update HomeKit value.""" - if not new_state: - return - assert self._char_doorbell_detected assert self._char_doorbell_detected_switch - if new_state.state == STATE_ON: + state = new_state.state + if state == STATE_ON or ( + self.doorbell_is_event and state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) + ): self._char_doorbell_detected.set_value(DOORBELL_SINGLE_PRESS) self._char_doorbell_detected_switch.set_value(DOORBELL_SINGLE_PRESS) _LOGGER.debug( diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 33bfc6e66d3..da755dc26f3 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -14,6 +14,7 @@ import pytest from homeassistant import config as hass_config from homeassistant.components import homekit as homekit_base, zeroconf from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.event import EventDeviceClass from homeassistant.components.homekit import ( MAX_DEVICES, STATUS_READY, @@ -2005,6 +2006,82 @@ async def test_homekit_finds_linked_motion_sensors( ) +@pytest.mark.parametrize( + ("domain", "device_class"), + [ + ("binary_sensor", BinarySensorDeviceClass.OCCUPANCY), + ("event", EventDeviceClass.DOORBELL), + ], +) +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_homekit_finds_linked_doorbell_sensors( + hass: HomeAssistant, + hk_driver, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + domain: str, + device_class: EventDeviceClass | BinarySensorDeviceClass, +) -> None: + """Test homekit can find linked doorbell sensors.""" + entry = await async_init_integration(hass) + + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) + + homekit.driver = hk_driver + homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + sw_version="0.16.0", + model="Camera Server", + manufacturer="Ubq", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + entry = entity_registry.async_get_or_create( + domain, + "camera", + "doorbell_sensor", + device_id=device_entry.id, + original_device_class=device_class, + ) + camera = entity_registry.async_get_or_create( + "camera", "camera", "demo", device_id=device_entry.id + ) + + hass.states.async_set( + entry.entity_id, + STATE_ON, + {ATTR_DEVICE_CLASS: device_class}, + ) + hass.states.async_set(camera.entity_id, STATE_ON) + + with ( + patch.object(homekit.bridge, "add_accessory"), + patch(f"{PATH_HOMEKIT}.async_show_setup_message"), + patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + ): + await homekit.async_start() + await hass.async_block_till_done() + + mock_get_acc.assert_called_with( + hass, + ANY, + ANY, + ANY, + { + "manufacturer": "Ubq", + "model": "Camera Server", + "platform": "test", + "sw_version": "0.16.0", + "linked_doorbell_sensor": entry.entity_id, + }, + ) + + @pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_finds_linked_humidity_sensors( hass: HomeAssistant, diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 184ce1b6521..510af680eaa 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -9,6 +9,7 @@ import pytest from homeassistant.components import camera, ffmpeg from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.camera.img_util import TurboJPEGSingleton +from homeassistant.components.event import EventDeviceClass from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( AUDIO_CODEC_COPY, @@ -30,10 +31,11 @@ from homeassistant.components.homekit.const import ( ) from homeassistant.components.homekit.type_cameras import Camera from homeassistant.components.homekit.type_switches import Switch -from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from tests.components.camera.common import mock_turbo_jpeg @@ -941,6 +943,123 @@ async def test_camera_with_linked_doorbell_sensor( assert char2.value is None +async def test_camera_with_linked_doorbell_event( + hass: HomeAssistant, run_driver, events +) -> None: + """Test a camera with a linked doorbell event can update.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + doorbell_entity_id = "event.doorbell" + + hass.states.async_set( + doorbell_entity_id, + dt_util.utcnow().isoformat(), + {ATTR_DEVICE_CLASS: EventDeviceClass.DOORBELL}, + ) + await hass.async_block_till_done() + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera( + hass, + run_driver, + "Camera", + entity_id, + 2, + { + CONF_STREAM_SOURCE: "/dev/null", + CONF_SUPPORT_AUDIO: True, + CONF_VIDEO_CODEC: VIDEO_CODEC_H264_OMX, + CONF_AUDIO_CODEC: AUDIO_CODEC_COPY, + CONF_LINKED_DOORBELL_SENSOR: doorbell_entity_id, + }, + ) + bridge = HomeBridge("hass", run_driver, "Test Bridge") + bridge.add_accessory(acc) + + acc.run() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + service = acc.get_service(SERV_DOORBELL) + assert service + char = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT) + assert char + + assert char.value is None + + service2 = acc.get_service(SERV_STATELESS_PROGRAMMABLE_SWITCH) + assert service2 + char2 = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT) + assert char2 + broker = MagicMock() + char2.broker = broker + assert char2.value is None + + hass.states.async_set( + doorbell_entity_id, + STATE_UNKNOWN, + {ATTR_DEVICE_CLASS: EventDeviceClass.DOORBELL}, + ) + await hass.async_block_till_done() + assert char.value is None + assert char2.value is None + assert len(broker.mock_calls) == 0 + + char.set_value(True) + char2.set_value(True) + broker.reset_mock() + + original_time = dt_util.utcnow().isoformat() + hass.states.async_set( + doorbell_entity_id, + original_time, + {ATTR_DEVICE_CLASS: EventDeviceClass.DOORBELL}, + ) + await hass.async_block_till_done() + assert char.value is None + assert char2.value is None + assert len(broker.mock_calls) == 2 + broker.reset_mock() + + hass.states.async_set( + doorbell_entity_id, + original_time, + {ATTR_DEVICE_CLASS: EventDeviceClass.DOORBELL}, + force_update=True, + ) + await hass.async_block_till_done() + assert char.value is None + assert char2.value is None + assert len(broker.mock_calls) == 0 + broker.reset_mock() + + hass.states.async_set( + doorbell_entity_id, + original_time, + {ATTR_DEVICE_CLASS: EventDeviceClass.DOORBELL, "other": "attr"}, + ) + await hass.async_block_till_done() + assert char.value is None + assert char2.value is None + assert len(broker.mock_calls) == 0 + broker.reset_mock() + + # Ensure we do not throw when the linked + # doorbell sensor is removed + hass.states.async_remove(doorbell_entity_id) + await hass.async_block_till_done() + acc.run() + await hass.async_block_till_done() + assert char.value is None + assert char2.value is None + + async def test_camera_with_a_missing_linked_doorbell_sensor( hass: HomeAssistant, run_driver, events ) -> None: From f65304957a9a43b8d369544d0752cef75f1840c7 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 29 Jun 2024 21:35:48 -0700 Subject: [PATCH 0483/2411] Rollback PyFlume to 0.6.5 (#120846) --- homeassistant/components/flume/coordinator.py | 2 +- homeassistant/components/flume/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flume/coordinator.py b/homeassistant/components/flume/coordinator.py index c75bffdc615..30e7962304c 100644 --- a/homeassistant/components/flume/coordinator.py +++ b/homeassistant/components/flume/coordinator.py @@ -98,7 +98,7 @@ class FlumeNotificationDataUpdateCoordinator(DataUpdateCoordinator[None]): # The related binary sensors (leak detected, high flow, low battery) # will be active until the notification is deleted in the Flume app. self.notifications = pyflume.FlumeNotificationList( - self.auth, read=None, sort_direction="DESC" + self.auth, read=None ).notification_list _LOGGER.debug("Notifications %s", self.notifications) diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index bb6783bafbe..953d9791f2f 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/flume", "iot_class": "cloud_polling", "loggers": ["pyflume"], - "requirements": ["PyFlume==0.8.7"] + "requirements": ["PyFlume==0.6.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index adb3a15f21d..8ef7c1458d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,7 +54,7 @@ PyChromecast==14.0.1 PyFlick==0.0.2 # homeassistant.components.flume -PyFlume==0.8.7 +PyFlume==0.6.5 # homeassistant.components.fronius PyFronius==0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc39b9faabd..97d3f24c0f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,7 +45,7 @@ PyChromecast==14.0.1 PyFlick==0.0.2 # homeassistant.components.flume -PyFlume==0.8.7 +PyFlume==0.6.5 # homeassistant.components.fronius PyFronius==0.7.3 From 72fdebeb885c87f24eec985f0ffc4ebb8249d201 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 29 Jun 2024 22:38:56 -0700 Subject: [PATCH 0484/2411] Bump google-cloud-texttospeech to 2.16.3 (#120845) --- 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 f429cd05257..b4fc3f39b86 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@lufton"], "documentation": "https://www.home-assistant.io/integrations/google_cloud", "iot_class": "cloud_push", - "requirements": ["google-cloud-texttospeech==2.12.3"] + "requirements": ["google-cloud-texttospeech==2.16.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8ef7c1458d7..2ed270e5d44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -974,7 +974,7 @@ google-api-python-client==2.71.0 google-cloud-pubsub==2.13.11 # homeassistant.components.google_cloud -google-cloud-texttospeech==2.12.3 +google-cloud-texttospeech==2.16.3 # homeassistant.components.google_generative_ai_conversation google-generativeai==0.6.0 From 75e3afd3692c8a3849479e95150b215ea46b9387 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 29 Jun 2024 23:28:18 -0700 Subject: [PATCH 0485/2411] Use TextToSpeechAsyncClient in Google Cloud TTS (#120847) --- homeassistant/components/google_cloud/tts.py | 60 ++++++++------------ 1 file changed, 23 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 1002e594a87..0178da30e69 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -1,9 +1,9 @@ """Support for the Google Cloud TTS service.""" -import asyncio import logging import os +from google.api_core.exceptions import GoogleAPIError from google.cloud import texttospeech import voluptuous as vol @@ -210,11 +210,11 @@ class GoogleCloudTTSProvider(Provider): self._text_type = text_type if key_file: - self._client = texttospeech.TextToSpeechClient.from_service_account_json( - key_file + self._client = ( + texttospeech.TextToSpeechAsyncClient.from_service_account_json(key_file) ) else: - self._client = texttospeech.TextToSpeechClient() + self._client = texttospeech.TextToSpeechAsyncClient() @property def supported_languages(self): @@ -261,45 +261,31 @@ class GoogleCloudTTSProvider(Provider): ) options = options_schema(options) - _encoding = options[CONF_ENCODING] - _voice = options[CONF_VOICE] - if _voice and not _voice.startswith(language): - language = _voice[:5] + encoding = options[CONF_ENCODING] + voice = options[CONF_VOICE] + if voice and not voice.startswith(language): + language = voice[:5] - try: - params = {options[CONF_TEXT_TYPE]: message} - synthesis_input = texttospeech.SynthesisInput(**params) - - voice = texttospeech.VoiceSelectionParams( + request = texttospeech.SynthesizeSpeechRequest( + input=texttospeech.SynthesisInput(**{options[CONF_TEXT_TYPE]: message}), + voice=texttospeech.VoiceSelectionParams( language_code=language, ssml_gender=texttospeech.SsmlVoiceGender[options[CONF_GENDER]], - name=_voice, - ) - - audio_config = texttospeech.AudioConfig( - audio_encoding=texttospeech.AudioEncoding[_encoding], + name=voice, + ), + audio_config=texttospeech.AudioConfig( + audio_encoding=texttospeech.AudioEncoding[encoding], speaking_rate=options[CONF_SPEED], pitch=options[CONF_PITCH], volume_gain_db=options[CONF_GAIN], effects_profile_id=options[CONF_PROFILES], - ) + ), + ) - request = { - "voice": voice, - "audio_config": audio_config, - "input": synthesis_input, - } + try: + response = await self._client.synthesize_speech(request, timeout=10) + except GoogleAPIError as err: + _LOGGER.error("Error occurred during Google Cloud TTS call: %s", err) + return None, None - async with asyncio.timeout(10): - assert self.hass - response = await self.hass.async_add_executor_job( - self._client.synthesize_speech, request - ) - return _encoding, response.audio_content - - except TimeoutError as ex: - _LOGGER.error("Timeout for Google Cloud TTS call: %s", ex) - except Exception: - _LOGGER.exception("Error occurred during Google Cloud TTS call") - - return None, None + return encoding, response.audio_content From bf608691d556e604dbdf81613418979f641b36ae Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 30 Jun 2024 02:03:24 -0700 Subject: [PATCH 0486/2411] Do not set gender if voice name is specified in Google Cloud TTS (#120848) * Use TextToSpeechAsyncClient in Google Cloud TTS * Do not set gender if voice name is specified in Google Cloud TTS --- homeassistant/components/google_cloud/tts.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 0178da30e69..975567845ae 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -262,15 +262,18 @@ class GoogleCloudTTSProvider(Provider): options = options_schema(options) encoding = options[CONF_ENCODING] + gender = texttospeech.SsmlVoiceGender[options[CONF_GENDER]] voice = options[CONF_VOICE] - if voice and not voice.startswith(language): - language = voice[:5] + if voice: + gender = None + if not voice.startswith(language): + language = voice[:5] request = texttospeech.SynthesizeSpeechRequest( input=texttospeech.SynthesisInput(**{options[CONF_TEXT_TYPE]: message}), voice=texttospeech.VoiceSelectionParams( language_code=language, - ssml_gender=texttospeech.SsmlVoiceGender[options[CONF_GENDER]], + ssml_gender=gender, name=voice, ), audio_config=texttospeech.AudioConfig( From d55be79e6a048cb0e7d6a5f5b7aa912d9a775811 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 30 Jun 2024 02:03:58 -0700 Subject: [PATCH 0487/2411] Handle error when validating options in Google Cloud TTS (#120850) --- homeassistant/components/google_cloud/tts.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 975567845ae..e5374a2151c 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -259,7 +259,11 @@ class GoogleCloudTTSProvider(Provider): vol.Optional(CONF_TEXT_TYPE, default=self._text_type): TEXT_TYPE_SCHEMA, } ) - options = options_schema(options) + try: + options = options_schema(options) + except vol.Invalid as err: + _LOGGER.error("Error: %s when validating options: %s", err, options) + return None, None encoding = options[CONF_ENCODING] gender = texttospeech.SsmlVoiceGender[options[CONF_GENDER]] From d15d001cfc2a55534e4cbb2fa9a81fb999f35c97 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sun, 30 Jun 2024 14:51:39 +0200 Subject: [PATCH 0488/2411] Use runtime_data for BMW (#120837) --- .../bmw_connected_drive/__init__.py | 32 ++++++++----------- .../bmw_connected_drive/binary_sensor.py | 9 +++--- .../components/bmw_connected_drive/button.py | 8 ++--- .../bmw_connected_drive/device_tracker.py | 9 +++--- .../bmw_connected_drive/diagnostics.py | 13 ++++---- .../components/bmw_connected_drive/lock.py | 8 ++--- .../components/bmw_connected_drive/notify.py | 13 +++++--- .../components/bmw_connected_drive/number.py | 8 ++--- .../components/bmw_connected_drive/select.py | 8 ++--- .../components/bmw_connected_drive/sensor.py | 8 ++--- .../components/bmw_connected_drive/switch.py | 8 ++--- .../bmw_connected_drive/test_config_flow.py | 4 +-- .../bmw_connected_drive/test_coordinator.py | 9 ++---- 13 files changed, 59 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index bd4e1cf7360..5900bd1ecc3 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass import logging from typing import Any @@ -18,10 +19,9 @@ from homeassistant.helpers import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTR_VIN, ATTRIBUTION, CONF_READ_ONLY, DATA_HASS_CONFIG, DOMAIN +from .const import ATTR_VIN, ATTRIBUTION, CONF_READ_ONLY, DOMAIN from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -55,13 +55,14 @@ PLATFORMS = [ SERVICE_UPDATE_STATE = "update_state" -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the BMW Connected Drive component from configuration.yaml.""" - # Store full yaml config in data for platform.NOTIFY - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][DATA_HASS_CONFIG] = config +type BMWConfigEntry = ConfigEntry[BMWData] - return True + +@dataclass +class BMWData: + """Class to store BMW runtime data.""" + + coordinator: BMWDataUpdateCoordinator @callback @@ -82,7 +83,7 @@ def _async_migrate_options_from_data_if_missing( async def _async_migrate_entries( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: BMWConfigEntry ) -> bool: """Migrate old entry.""" entity_registry = er.async_get(hass) @@ -134,8 +135,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator + entry.runtime_data = BMWData(coordinator) # Set up all platforms except notify await hass.config_entries.async_forward_entry_setups( @@ -150,7 +150,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: Platform.NOTIFY, DOMAIN, {CONF_NAME: DOMAIN, CONF_ENTITY_ID: entry.entry_id}, - hass.data[DOMAIN][DATA_HASS_CONFIG], + {}, ) ) @@ -171,15 +171,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( + + return await hass.config_entries.async_unload_platforms( entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] ) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok - class BMWBaseEntity(CoordinatorEntity[BMWDataUpdateCoordinator]): """Common base for BMW entities.""" diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index d40d85e4cd4..e09241d99e7 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -17,13 +17,12 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_system import UnitSystem -from . import BMWBaseEntity -from .const import DOMAIN, UNIT_MAP +from . import BMWBaseEntity, BMWConfigEntry +from .const import UNIT_MAP from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -197,11 +196,11 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: BMWConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the BMW binary sensors from config entry.""" - coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data.coordinator entities = [ BMWBinarySensor(coordinator, vehicle, description, hass.config.units) diff --git a/homeassistant/components/bmw_connected_drive/button.py b/homeassistant/components/bmw_connected_drive/button.py index fe103f0e003..ec0212cc189 100644 --- a/homeassistant/components/bmw_connected_drive/button.py +++ b/homeassistant/components/bmw_connected_drive/button.py @@ -12,13 +12,11 @@ from bimmer_connected.vehicle import MyBMWVehicle from bimmer_connected.vehicle.remote_services import RemoteServiceStatus from homeassistant.components.button import ButtonEntity, ButtonEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BMWBaseEntity -from .const import DOMAIN +from . import BMWBaseEntity, BMWConfigEntry if TYPE_CHECKING: from .coordinator import BMWDataUpdateCoordinator @@ -68,11 +66,11 @@ BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: BMWConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the BMW buttons from config entry.""" - coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data.coordinator entities: list[BMWButton] = [] diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index d6846d0b88e..6dc54e9473f 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -8,12 +8,11 @@ from typing import Any from bimmer_connected.vehicle import MyBMWVehicle from homeassistant.components.device_tracker import SourceType, TrackerEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BMWBaseEntity -from .const import ATTR_DIRECTION, DOMAIN +from . import BMWBaseEntity, BMWConfigEntry +from .const import ATTR_DIRECTION from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -21,11 +20,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: BMWConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the MyBMW tracker from config entry.""" - coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data.coordinator entities: list[BMWDeviceTracker] = [] for vehicle in coordinator.account.vehicles: diff --git a/homeassistant/components/bmw_connected_drive/diagnostics.py b/homeassistant/components/bmw_connected_drive/diagnostics.py index c2bd4b6d24a..a3a8f5f942e 100644 --- a/homeassistant/components/bmw_connected_drive/diagnostics.py +++ b/homeassistant/components/bmw_connected_drive/diagnostics.py @@ -9,17 +9,16 @@ from typing import TYPE_CHECKING, Any from bimmer_connected.utils import MyBMWJSONEncoder from homeassistant.components.diagnostics.util import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntry -from .const import CONF_REFRESH_TOKEN, DOMAIN +from . import BMWConfigEntry +from .const import CONF_REFRESH_TOKEN if TYPE_CHECKING: from bimmer_connected.vehicle import MyBMWVehicle - from .coordinator import BMWDataUpdateCoordinator TO_REDACT_INFO = [CONF_USERNAME, CONF_PASSWORD, CONF_REFRESH_TOKEN] TO_REDACT_DATA = [ @@ -47,10 +46,10 @@ def vehicle_to_dict(vehicle: MyBMWVehicle | None) -> dict: async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: BMWConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data.coordinator coordinator.account.config.log_responses = True await coordinator.account.get_vehicles(force_init=True) @@ -73,10 +72,10 @@ async def async_get_config_entry_diagnostics( async def async_get_device_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry + hass: HomeAssistant, config_entry: BMWConfigEntry, device: DeviceEntry ) -> dict[str, Any]: """Return diagnostics for a device.""" - coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data.coordinator coordinator.account.config.log_responses = True await coordinator.account.get_vehicles(force_init=True) diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index e138f31ba24..4380b736811 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -10,13 +10,11 @@ from bimmer_connected.vehicle import MyBMWVehicle from bimmer_connected.vehicle.doors_windows import LockState from homeassistant.components.lock import LockEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BMWBaseEntity -from .const import DOMAIN +from . import BMWBaseEntity, BMWConfigEntry from .coordinator import BMWDataUpdateCoordinator DOOR_LOCK_STATE = "door_lock_state" @@ -25,11 +23,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: BMWConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the MyBMW lock from config entry.""" - coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data.coordinator if not coordinator.read_only: async_add_entities( diff --git a/homeassistant/components/bmw_connected_drive/notify.py b/homeassistant/components/bmw_connected_drive/notify.py index 84bc2d8459a..8edde1a5cd7 100644 --- a/homeassistant/components/bmw_connected_drive/notify.py +++ b/homeassistant/components/bmw_connected_drive/notify.py @@ -24,8 +24,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN -from .coordinator import BMWDataUpdateCoordinator +from . import BMWConfigEntry ATTR_LAT = "lat" ATTR_LOCATION_ATTRIBUTES = ["street", "city", "postal_code", "country"] @@ -42,12 +41,16 @@ def get_service( discovery_info: DiscoveryInfoType | None = None, ) -> BMWNotificationService: """Get the BMW notification service.""" - coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry: BMWConfigEntry | None = hass.config_entries.async_get_entry( (discovery_info or {})[CONF_ENTITY_ID] - ] + ) targets = {} - if not coordinator.read_only: + if ( + config_entry + and (coordinator := config_entry.runtime_data.coordinator) + and not coordinator.read_only + ): targets.update({v.name: v for v in coordinator.account.vehicles}) return BMWNotificationService(targets) diff --git a/homeassistant/components/bmw_connected_drive/number.py b/homeassistant/components/bmw_connected_drive/number.py index defeb3f0f56..a875e1a6974 100644 --- a/homeassistant/components/bmw_connected_drive/number.py +++ b/homeassistant/components/bmw_connected_drive/number.py @@ -14,13 +14,11 @@ from homeassistant.components.number import ( NumberEntityDescription, NumberMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BMWBaseEntity -from .const import DOMAIN +from . import BMWBaseEntity, BMWConfigEntry from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -56,11 +54,11 @@ NUMBER_TYPES: list[BMWNumberEntityDescription] = [ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: BMWConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the MyBMW number from config entry.""" - coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data.coordinator entities: list[BMWNumber] = [] diff --git a/homeassistant/components/bmw_connected_drive/select.py b/homeassistant/components/bmw_connected_drive/select.py index 2522c6bf2a6..25b816d32b9 100644 --- a/homeassistant/components/bmw_connected_drive/select.py +++ b/homeassistant/components/bmw_connected_drive/select.py @@ -10,14 +10,12 @@ from bimmer_connected.vehicle import MyBMWVehicle from bimmer_connected.vehicle.charging_profile import ChargingMode from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfElectricCurrent from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BMWBaseEntity -from .const import DOMAIN +from . import BMWBaseEntity, BMWConfigEntry from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -63,11 +61,11 @@ SELECT_TYPES: tuple[BMWSelectEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: BMWConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the MyBMW lock from config entry.""" - coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data.coordinator entities: list[BMWSelect] = [] diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 1d9737c7d5f..d4ac1e3decd 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -18,7 +18,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, STATE_UNKNOWN, @@ -30,8 +29,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from . import BMWBaseEntity -from .const import DOMAIN +from . import BMWBaseEntity, BMWConfigEntry from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -171,11 +169,11 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: BMWConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the MyBMW sensors from config entry.""" - coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data.coordinator entities = [ BMWSensor(coordinator, vehicle, description) diff --git a/homeassistant/components/bmw_connected_drive/switch.py b/homeassistant/components/bmw_connected_drive/switch.py index 5ee31d2efd7..2c8622433ba 100644 --- a/homeassistant/components/bmw_connected_drive/switch.py +++ b/homeassistant/components/bmw_connected_drive/switch.py @@ -10,13 +10,11 @@ from bimmer_connected.vehicle import MyBMWVehicle from bimmer_connected.vehicle.fuel_and_battery import ChargingState from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BMWBaseEntity -from .const import DOMAIN +from . import BMWBaseEntity, BMWConfigEntry from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -64,11 +62,11 @@ NUMBER_TYPES: list[BMWSwitchEntityDescription] = [ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: BMWConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the MyBMW switch from config entry.""" - coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data.coordinator entities: list[BMWSwitch] = [] diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 3c7f452a011..f346cd70b26 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -159,7 +159,7 @@ async def test_options_flow_implementation(hass: HomeAssistant) -> None: CONF_READ_ONLY: True, } - assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 2 async def test_reauth(hass: HomeAssistant) -> None: @@ -210,4 +210,4 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result2["reason"] == "reauth_successful" assert config_entry.data == FIXTURE_COMPLETE_ENTRY - assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 2 diff --git a/tests/components/bmw_connected_drive/test_coordinator.py b/tests/components/bmw_connected_drive/test_coordinator.py index 5b3f99a9414..ca629084f6c 100644 --- a/tests/components/bmw_connected_drive/test_coordinator.py +++ b/tests/components/bmw_connected_drive/test_coordinator.py @@ -27,10 +27,7 @@ async def test_update_success(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert ( - hass.data[config_entry.domain][config_entry.entry_id].last_update_success - is True - ) + assert config_entry.runtime_data.coordinator.last_update_success is True @pytest.mark.usefixtures("bmw_fixture") @@ -45,7 +42,7 @@ async def test_update_failed( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - coordinator = hass.data[config_entry.domain][config_entry.entry_id] + coordinator = config_entry.runtime_data.coordinator assert coordinator.last_update_success is True @@ -74,7 +71,7 @@ async def test_update_reauth( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - coordinator = hass.data[config_entry.domain][config_entry.entry_id] + coordinator = config_entry.runtime_data.coordinator assert coordinator.last_update_success is True From ca7fb906cc04869e5a0e6c58eb663264cd2ed5d8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 30 Jun 2024 14:52:20 +0200 Subject: [PATCH 0489/2411] Axis improve fixture naming (#120844) --- tests/components/axis/conftest.py | 65 +++++++++++---------- tests/components/axis/test_binary_sensor.py | 4 +- tests/components/axis/test_camera.py | 8 +-- tests/components/axis/test_config_flow.py | 34 +++++------ tests/components/axis/test_diagnostics.py | 4 +- tests/components/axis/test_hub.py | 28 ++++----- tests/components/axis/test_init.py | 12 ++-- tests/components/axis/test_light.py | 4 +- tests/components/axis/test_switch.py | 4 +- 9 files changed, 82 insertions(+), 81 deletions(-) diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index b306e25c434..2f392960b86 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -49,8 +49,8 @@ from .const import ( from tests.common import MockConfigEntry -@pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock]: +@pytest.fixture(name="mock_setup_entry") +def fixture_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.axis.async_setup_entry", return_value=True @@ -62,7 +62,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture(name="config_entry") -def config_entry_fixture( +def fixture_config_entry( hass: HomeAssistant, config_entry_data: MappingProxyType[str, Any], config_entry_options: MappingProxyType[str, Any], @@ -82,13 +82,13 @@ def config_entry_fixture( @pytest.fixture(name="config_entry_version") -def config_entry_version_fixture() -> int: +def fixture_config_entry_version() -> int: """Define a config entry version fixture.""" return 3 @pytest.fixture(name="config_entry_data") -def config_entry_data_fixture() -> MappingProxyType[str, Any]: +def fixture_config_entry_data() -> MappingProxyType[str, Any]: """Define a config entry data fixture.""" return { CONF_HOST: DEFAULT_HOST, @@ -101,7 +101,7 @@ def config_entry_data_fixture() -> MappingProxyType[str, Any]: @pytest.fixture(name="config_entry_options") -def config_entry_options_fixture() -> MappingProxyType[str, Any]: +def fixture_config_entry_options() -> MappingProxyType[str, Any]: """Define a config entry options fixture.""" return {} @@ -109,8 +109,8 @@ def config_entry_options_fixture() -> MappingProxyType[str, Any]: # Axis API fixtures -@pytest.fixture(name="mock_vapix_requests") -def default_request_fixture( +@pytest.fixture(name="mock_requests") +def fixture_request( respx_mock: respx.MockRouter, port_management_payload: dict[str, Any], param_properties_payload: str, @@ -215,7 +215,7 @@ def api_discovery_items() -> dict[str, Any]: @pytest.fixture(autouse=True) -def api_discovery_fixture(api_discovery_items: dict[str, Any]) -> None: +def fixture_api_discovery(api_discovery_items: dict[str, Any]) -> None: """Apidiscovery mock response.""" data = deepcopy(API_DISCOVERY_RESPONSE) if api_discovery_items: @@ -224,64 +224,65 @@ def api_discovery_fixture(api_discovery_items: dict[str, Any]) -> None: @pytest.fixture(name="port_management_payload") -def io_port_management_data_fixture() -> dict[str, Any]: +def fixture_io_port_management_data() -> dict[str, Any]: """Property parameter data.""" return PORT_MANAGEMENT_RESPONSE @pytest.fixture(name="param_properties_payload") -def param_properties_data_fixture() -> str: +def fixture_param_properties_data() -> str: """Property parameter data.""" return PROPERTIES_RESPONSE @pytest.fixture(name="param_ports_payload") -def param_ports_data_fixture() -> str: +def fixture_param_ports_data() -> str: """Property parameter data.""" return PORTS_RESPONSE @pytest.fixture(name="mqtt_status_code") -def mqtt_status_code_fixture() -> int: +def fixture_mqtt_status_code() -> int: """Property parameter data.""" return 200 -@pytest.fixture(name="setup_default_vapix_requests") -def default_vapix_requests_fixture(mock_vapix_requests: Callable[[str], None]) -> None: +@pytest.fixture(name="mock_default_requests") +def fixture_default_requests(mock_requests: Callable[[str], None]) -> None: """Mock default Vapix requests responses.""" - mock_vapix_requests(DEFAULT_HOST) + mock_requests(DEFAULT_HOST) -@pytest.fixture(name="prepare_config_entry") -async def prep_config_entry_fixture( - hass: HomeAssistant, config_entry: ConfigEntry, setup_default_vapix_requests: None +@pytest.fixture(name="config_entry_factory") +async def fixture_config_entry_factory( + hass: HomeAssistant, + config_entry: ConfigEntry, + mock_requests: Callable[[str], None], ) -> Callable[[], ConfigEntry]: """Fixture factory to set up Axis network device.""" async def __mock_setup_config_entry() -> ConfigEntry: - assert await hass.config_entries.async_setup(config_entry.entry_id) + mock_requests(config_entry.data[CONF_HOST]) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() return config_entry return __mock_setup_config_entry -@pytest.fixture(name="setup_config_entry") -async def setup_config_entry_fixture( - hass: HomeAssistant, config_entry: ConfigEntry, setup_default_vapix_requests: None +@pytest.fixture(name="config_entry_setup") +async def fixture_config_entry_setup( + hass: HomeAssistant, config_entry_factory: Callable[[], ConfigEntry] ) -> ConfigEntry: """Define a fixture to set up Axis network device.""" - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - return config_entry + return await config_entry_factory() # RTSP fixtures -@pytest.fixture(autouse=True) -def mock_axis_rtspclient() -> Generator[Callable[[dict | None, str], None]]: +@pytest.fixture(autouse=True, name="mock_axis_rtspclient") +def fixture_axis_rtspclient() -> Generator[Callable[[dict | None, str], None]]: """No real RTSP communication allowed.""" with patch("axis.stream_manager.RTSPClient") as rtsp_client_mock: rtsp_client_mock.return_value.session.state = State.STOPPED @@ -313,8 +314,8 @@ def mock_axis_rtspclient() -> Generator[Callable[[dict | None, str], None]]: yield make_rtsp_call -@pytest.fixture(autouse=True) -def mock_rtsp_event( +@pytest.fixture(autouse=True, name="mock_rtsp_event") +def fixture_rtsp_event( mock_axis_rtspclient: Callable[[dict | None, str], None], ) -> Callable[[str, str, str, str, str, str], None]: """Fixture to allow mocking received RTSP events.""" @@ -366,8 +367,8 @@ def mock_rtsp_event( return send_event -@pytest.fixture(autouse=True) -def mock_rtsp_signal_state( +@pytest.fixture(autouse=True, name="mock_rtsp_signal_state") +def fixture_rtsp_signal_state( mock_axis_rtspclient: Callable[[dict | None, str], None], ) -> Callable[[bool], None]: """Fixture to allow mocking RTSP state signalling.""" diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 99a530724e3..4fc10bcbb38 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -173,7 +173,7 @@ from .const import NAME ), ], ) -@pytest.mark.usefixtures("setup_config_entry") +@pytest.mark.usefixtures("config_entry_setup") async def test_binary_sensors( hass: HomeAssistant, mock_rtsp_event: Callable[[str, str, str, str, str, str], None], @@ -225,7 +225,7 @@ async def test_binary_sensors( }, ], ) -@pytest.mark.usefixtures("setup_config_entry") +@pytest.mark.usefixtures("config_entry_setup") async def test_unsupported_events( hass: HomeAssistant, mock_rtsp_event: Callable[[str, str, str, str, str, str], None], diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index 7d26cc7a3bc..55692b2dca3 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -30,7 +30,7 @@ async def test_platform_manually_configured(hass: HomeAssistant) -> None: assert AXIS_DOMAIN not in hass.data -@pytest.mark.usefixtures("setup_config_entry") +@pytest.mark.usefixtures("config_entry_setup") async def test_camera(hass: HomeAssistant) -> None: """Test that Axis camera platform is loaded properly.""" assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1 @@ -51,7 +51,7 @@ async def test_camera(hass: HomeAssistant) -> None: @pytest.mark.parametrize("config_entry_options", [{CONF_STREAM_PROFILE: "profile_1"}]) -@pytest.mark.usefixtures("setup_config_entry") +@pytest.mark.usefixtures("config_entry_setup") async def test_camera_with_stream_profile(hass: HomeAssistant) -> None: """Test that Axis camera entity is using the correct path with stream profike.""" assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1 @@ -87,8 +87,8 @@ root.Properties.System.SerialNumber={MAC} @pytest.mark.parametrize("param_properties_payload", [PROPERTY_DATA]) async def test_camera_disabled( - hass: HomeAssistant, prepare_config_entry: Callable[[], ConfigEntry] + hass: HomeAssistant, config_entry_factory: Callable[[], ConfigEntry] ) -> None: """Test that Axis camera platform is loaded properly but does not create camera entity.""" - await prepare_config_entry() + await config_entry_factory() assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 0 diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 055c74cc9a5..8ba17ced01b 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -55,7 +55,7 @@ async def mock_config_entry_fixture( return config_entry -@pytest.mark.usefixtures("setup_default_vapix_requests", "mock_setup_entry") +@pytest.mark.usefixtures("mock_default_requests", "mock_setup_entry") async def test_flow_manual_configuration(hass: HomeAssistant) -> None: """Test that config flow works.""" MockConfigEntry(domain=AXIS_DOMAIN, source=SOURCE_IGNORE).add_to_hass(hass) @@ -94,7 +94,7 @@ async def test_flow_manual_configuration(hass: HomeAssistant) -> None: async def test_manual_configuration_update_configuration( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_vapix_requests: Callable[[str], None], + mock_requests: Callable[[str], None], ) -> None: """Test that config flow fails on already configured device.""" assert mock_config_entry.data[CONF_HOST] == "1.2.3.4" @@ -106,7 +106,7 @@ async def test_manual_configuration_update_configuration( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - mock_vapix_requests("2.3.4.5") + mock_requests("2.3.4.5") result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -178,7 +178,7 @@ async def test_flow_fails_cannot_connect(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "cannot_connect"} -@pytest.mark.usefixtures("setup_default_vapix_requests", "mock_setup_entry") +@pytest.mark.usefixtures("mock_default_requests", "mock_setup_entry") async def test_flow_create_entry_multiple_existing_entries_of_same_model( hass: HomeAssistant, ) -> None: @@ -230,7 +230,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model( async def test_reauth_flow_update_configuration( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_vapix_requests: Callable[[str], None], + mock_requests: Callable[[str], None], ) -> None: """Test that config flow fails on already configured device.""" assert mock_config_entry.data[CONF_HOST] == "1.2.3.4" @@ -246,7 +246,7 @@ async def test_reauth_flow_update_configuration( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - mock_vapix_requests("2.3.4.5") + mock_requests("2.3.4.5") result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -271,7 +271,7 @@ async def test_reauth_flow_update_configuration( async def test_reconfiguration_flow_update_configuration( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_vapix_requests: Callable[[str], None], + mock_requests: Callable[[str], None], ) -> None: """Test that config flow reconfiguration updates configured device.""" assert mock_config_entry.data[CONF_HOST] == "1.2.3.4" @@ -289,7 +289,7 @@ async def test_reconfiguration_flow_update_configuration( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - mock_vapix_requests("2.3.4.5") + mock_requests("2.3.4.5") result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -372,7 +372,7 @@ async def test_reconfiguration_flow_update_configuration( ), ], ) -@pytest.mark.usefixtures("setup_default_vapix_requests", "mock_setup_entry") +@pytest.mark.usefixtures("mock_default_requests", "mock_setup_entry") async def test_discovery_flow( hass: HomeAssistant, source: str, @@ -514,7 +514,7 @@ async def test_discovered_device_already_configured( async def test_discovery_flow_updated_configuration( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_vapix_requests: Callable[[str], None], + mock_requests: Callable[[str], None], source: str, discovery_info: BaseServiceInfo, expected_port: int, @@ -529,7 +529,7 @@ async def test_discovery_flow_updated_configuration( CONF_NAME: NAME, } - mock_vapix_requests("2.3.4.5") + mock_requests("2.3.4.5") result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) @@ -646,13 +646,13 @@ async def test_discovery_flow_ignore_link_local_address( async def test_option_flow( - hass: HomeAssistant, setup_config_entry: ConfigEntry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test config flow options.""" - assert CONF_STREAM_PROFILE not in setup_config_entry.options - assert CONF_VIDEO_SOURCE not in setup_config_entry.options + assert CONF_STREAM_PROFILE not in config_entry_setup.options + assert CONF_VIDEO_SOURCE not in config_entry_setup.options - result = await hass.config_entries.options.async_init(setup_config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry_setup.entry_id) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_stream" @@ -676,5 +676,5 @@ async def test_option_flow( CONF_STREAM_PROFILE: "profile_1", CONF_VIDEO_SOURCE: 1, } - assert setup_config_entry.options[CONF_STREAM_PROFILE] == "profile_1" - assert setup_config_entry.options[CONF_VIDEO_SOURCE] == 1 + assert config_entry_setup.options[CONF_STREAM_PROFILE] == "profile_1" + assert config_entry_setup.options[CONF_VIDEO_SOURCE] == 1 diff --git a/tests/components/axis/test_diagnostics.py b/tests/components/axis/test_diagnostics.py index c3e1faf4277..b949c23236b 100644 --- a/tests/components/axis/test_diagnostics.py +++ b/tests/components/axis/test_diagnostics.py @@ -16,11 +16,11 @@ from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - setup_config_entry: ConfigEntry, + config_entry_setup: ConfigEntry, snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" assert ( - await get_diagnostics_for_config_entry(hass, hass_client, setup_config_entry) + await get_diagnostics_for_config_entry(hass, hass_client, config_entry_setup) == snapshot ) diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index fb0a28bb262..a797b3feee6 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -50,11 +50,11 @@ def hass_mock_forward_entry_setup(hass: HomeAssistant) -> Generator[AsyncMock]: async def test_device_setup( forward_entry_setups: AsyncMock, config_entry_data: MappingProxyType[str, Any], - setup_config_entry: ConfigEntry, + config_entry_setup: ConfigEntry, device_registry: dr.DeviceRegistry, ) -> None: """Successful setup.""" - hub = setup_config_entry.runtime_data + hub = config_entry_setup.runtime_data assert hub.api.vapix.firmware_version == "9.10.1" assert hub.api.vapix.product_number == "M1065-LW" @@ -78,9 +78,9 @@ async def test_device_setup( @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_BASIC_DEVICE_INFO]) -async def test_device_info(setup_config_entry: ConfigEntry) -> None: +async def test_device_info(config_entry_setup: ConfigEntry) -> None: """Verify other path of device information works.""" - hub = setup_config_entry.runtime_data + hub = config_entry_setup.runtime_data assert hub.api.vapix.firmware_version == "9.80.1" assert hub.api.vapix.product_number == "M1065-LW" @@ -89,7 +89,7 @@ async def test_device_info(setup_config_entry: ConfigEntry) -> None: @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_MQTT]) -@pytest.mark.usefixtures("setup_config_entry") +@pytest.mark.usefixtures("config_entry_setup") async def test_device_support_mqtt( hass: HomeAssistant, mqtt_mock: MqttMockHAClient ) -> None: @@ -115,7 +115,7 @@ async def test_device_support_mqtt( @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_MQTT]) @pytest.mark.parametrize("mqtt_status_code", [401]) -@pytest.mark.usefixtures("setup_config_entry") +@pytest.mark.usefixtures("config_entry_setup") async def test_device_support_mqtt_low_privilege(mqtt_mock: MqttMockHAClient) -> None: """Successful setup.""" mqtt_call = call(f"{MAC}/#", mock.ANY, 0, "utf-8") @@ -124,14 +124,14 @@ async def test_device_support_mqtt_low_privilege(mqtt_mock: MqttMockHAClient) -> async def test_update_address( hass: HomeAssistant, - setup_config_entry: ConfigEntry, - mock_vapix_requests: Callable[[str], None], + config_entry_setup: ConfigEntry, + mock_requests: Callable[[str], None], ) -> None: """Test update address works.""" - hub = setup_config_entry.runtime_data + hub = config_entry_setup.runtime_data assert hub.api.config.host == "1.2.3.4" - mock_vapix_requests("2.3.4.5") + mock_requests("2.3.4.5") await hass.config_entries.flow.async_init( AXIS_DOMAIN, data=zeroconf.ZeroconfServiceInfo( @@ -150,7 +150,7 @@ async def test_update_address( assert hub.api.config.host == "2.3.4.5" -@pytest.mark.usefixtures("setup_config_entry") +@pytest.mark.usefixtures("config_entry_setup") async def test_device_unavailable( hass: HomeAssistant, mock_rtsp_event: Callable[[str, str, str, str, str, str], None], @@ -187,7 +187,7 @@ async def test_device_unavailable( assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state == STATE_OFF -@pytest.mark.usefixtures("setup_default_vapix_requests") +@pytest.mark.usefixtures("mock_default_requests") async def test_device_not_accessible( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: @@ -198,7 +198,7 @@ async def test_device_not_accessible( assert hass.data[AXIS_DOMAIN] == {} -@pytest.mark.usefixtures("setup_default_vapix_requests") +@pytest.mark.usefixtures("mock_default_requests") async def test_device_trigger_reauth_flow( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: @@ -215,7 +215,7 @@ async def test_device_trigger_reauth_flow( assert hass.data[AXIS_DOMAIN] == {} -@pytest.mark.usefixtures("setup_default_vapix_requests") +@pytest.mark.usefixtures("mock_default_requests") async def test_device_unknown_error( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index e4dc7cd1eef..2ffd21073af 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -9,9 +9,9 @@ from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -async def test_setup_entry(setup_config_entry: ConfigEntry) -> None: +async def test_setup_entry(config_entry_setup: ConfigEntry) -> None: """Test successful setup of entry.""" - assert setup_config_entry.state is ConfigEntryState.LOADED + assert config_entry_setup.state is ConfigEntryState.LOADED async def test_setup_entry_fails( @@ -30,13 +30,13 @@ async def test_setup_entry_fails( async def test_unload_entry( - hass: HomeAssistant, setup_config_entry: ConfigEntry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test successful unload of entry.""" - assert setup_config_entry.state is ConfigEntryState.LOADED + assert config_entry_setup.state is ConfigEntryState.LOADED - assert await hass.config_entries.async_unload(setup_config_entry.entry_id) - assert setup_config_entry.state is ConfigEntryState.NOT_LOADED + assert await hass.config_entries.async_unload(config_entry_setup.entry_id) + assert config_entry_setup.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize("config_entry_version", [1]) diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index a5ae66afee0..47e00d9c341 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -69,7 +69,7 @@ def light_control_fixture(light_control_items: list[dict[str, Any]]) -> None: @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_LIGHT_CONTROL]) @pytest.mark.parametrize("light_control_items", [[]]) -@pytest.mark.usefixtures("setup_config_entry") +@pytest.mark.usefixtures("config_entry_setup") async def test_no_light_entity_without_light_control_representation( hass: HomeAssistant, mock_rtsp_event: Callable[[str, str, str, str, str, str], None], @@ -88,7 +88,7 @@ async def test_no_light_entity_without_light_control_representation( @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_LIGHT_CONTROL]) -@pytest.mark.usefixtures("setup_config_entry") +@pytest.mark.usefixtures("config_entry_setup") async def test_lights( hass: HomeAssistant, mock_rtsp_event: Callable[[str, str, str, str, str, str], None], diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 479830783b1..a211a42217c 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -30,7 +30,7 @@ root.IOPort.I1.Output.Active=open @pytest.mark.parametrize("param_ports_payload", [PORT_DATA]) -@pytest.mark.usefixtures("setup_config_entry") +@pytest.mark.usefixtures("config_entry_setup") async def test_switches_with_port_cgi( hass: HomeAssistant, mock_rtsp_event: Callable[[str, str, str, str, str, str], None], @@ -115,7 +115,7 @@ PORT_MANAGEMENT_RESPONSE = { @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_PORT_MANAGEMENT]) @pytest.mark.parametrize("port_management_payload", [PORT_MANAGEMENT_RESPONSE]) -@pytest.mark.usefixtures("setup_config_entry") +@pytest.mark.usefixtures("config_entry_setup") async def test_switches_with_port_management( hass: HomeAssistant, mock_rtsp_event: Callable[[str, str, str, str, str, str], None], From 7aca7cf85800983c32f8dad66065a3e1ce6ad2f2 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 30 Jun 2024 14:56:12 +0200 Subject: [PATCH 0490/2411] Bump pyfritzhome to 0.6.12 (#120861) --- homeassistant/components/fritzbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index de2e9e0200a..3735c16571e 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["pyfritzhome"], "quality_scale": "gold", - "requirements": ["pyfritzhome==0.6.11"], + "requirements": ["pyfritzhome==0.6.12"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/requirements_all.txt b/requirements_all.txt index 2ed270e5d44..2d540b5eb52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1866,7 +1866,7 @@ pyforked-daapd==0.1.14 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.11 +pyfritzhome==0.6.12 # homeassistant.components.ifttt pyfttt==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 97d3f24c0f8..8996caf7f35 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1465,7 +1465,7 @@ pyforked-daapd==0.1.14 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.11 +pyfritzhome==0.6.12 # homeassistant.components.ifttt pyfttt==0.3 From 255cc9ed74e08e405846f19b4049b6525bfb7c10 Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Sun, 30 Jun 2024 14:57:48 +0200 Subject: [PATCH 0491/2411] Store runtime data inside the config entry in fyta (#120761) --- homeassistant/components/fyta/__init__.py | 13 +++++-------- homeassistant/components/fyta/config_flow.py | 5 +++-- homeassistant/components/fyta/coordinator.py | 10 +++++++--- homeassistant/components/fyta/diagnostics.py | 7 +++---- homeassistant/components/fyta/sensor.py | 7 +++---- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/fyta/__init__.py b/homeassistant/components/fyta/__init__.py index 2e35b88b18a..b666c5a1f52 100644 --- a/homeassistant/components/fyta/__init__.py +++ b/homeassistant/components/fyta/__init__.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.util.dt import async_get_time_zone -from .const import CONF_EXPIRATION, DOMAIN +from .const import CONF_EXPIRATION from .coordinator import FytaCoordinator _LOGGER = logging.getLogger(__name__) @@ -26,9 +26,10 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [ Platform.SENSOR, ] +type FytaConfigEntry = ConfigEntry[FytaCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: FytaConfigEntry) -> bool: """Set up the Fyta integration.""" tz: str = hass.config.time_zone @@ -45,7 +46,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 + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -55,11 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Fyta entity.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/fyta/config_flow.py b/homeassistant/components/fyta/config_flow.py index c09aac1b966..234ba6fa8c0 100644 --- a/homeassistant/components/fyta/config_flow.py +++ b/homeassistant/components/fyta/config_flow.py @@ -14,9 +14,10 @@ from fyta_cli.fyta_exceptions import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from . import FytaConfigEntry from .const import CONF_EXPIRATION, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -36,7 +37,7 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize FytaConfigFlow.""" self.credentials: dict[str, Any] = {} - self._entry: ConfigEntry | None = None + self._entry: FytaConfigEntry | None = None async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]: """Reusable Auth Helper.""" diff --git a/homeassistant/components/fyta/coordinator.py b/homeassistant/components/fyta/coordinator.py index db79f21eb53..b6fbf73ec25 100644 --- a/homeassistant/components/fyta/coordinator.py +++ b/homeassistant/components/fyta/coordinator.py @@ -1,8 +1,10 @@ """Coordinator for FYTA integration.""" +from __future__ import annotations + from datetime import datetime, timedelta import logging -from typing import Any +from typing import TYPE_CHECKING, Any from fyta_cli.fyta_connector import FytaConnector from fyta_cli.fyta_exceptions import ( @@ -12,7 +14,6 @@ from fyta_cli.fyta_exceptions import ( FytaPlantError, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -20,13 +21,16 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import CONF_EXPIRATION +if TYPE_CHECKING: + from . import FytaConfigEntry + _LOGGER = logging.getLogger(__name__) class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]): """Fyta custom coordinator.""" - config_entry: ConfigEntry + config_entry: FytaConfigEntry def __init__(self, hass: HomeAssistant, fyta: FytaConnector) -> None: """Initialize my coordinator.""" diff --git a/homeassistant/components/fyta/diagnostics.py b/homeassistant/components/fyta/diagnostics.py index 83f2a38dcae..55720b75ee6 100644 --- a/homeassistant/components/fyta/diagnostics.py +++ b/homeassistant/components/fyta/diagnostics.py @@ -5,11 +5,10 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import DOMAIN +from . import FytaConfigEntry TO_REDACT = [ CONF_PASSWORD, @@ -19,10 +18,10 @@ TO_REDACT = [ async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: FytaConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data = hass.data[DOMAIN][config_entry.entry_id].data + data = config_entry.runtime_data.data return { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), diff --git a/homeassistant/components/fyta/sensor.py b/homeassistant/components/fyta/sensor.py index 574b4e7b18e..27576ae5065 100644 --- a/homeassistant/components/fyta/sensor.py +++ b/homeassistant/components/fyta/sensor.py @@ -15,7 +15,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, EntityCategory, @@ -25,7 +24,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import FytaConfigEntry from .coordinator import FytaCoordinator from .entity import FytaPlantEntity @@ -130,10 +129,10 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [ async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: FytaConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the FYTA sensors.""" - coordinator: FytaCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: FytaCoordinator = entry.runtime_data plant_entities = [ FytaPlantSensor(coordinator, entry, sensor, plant_id) From 5deb69d492fe28b0731a579e6b5c8d0b2b33fb96 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 30 Jun 2024 06:02:06 -0700 Subject: [PATCH 0492/2411] Correctly return file extension in Google Cloud TTS (#120849) --- homeassistant/components/google_cloud/tts.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index e5374a2151c..f9b01c9b870 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -265,7 +265,7 @@ class GoogleCloudTTSProvider(Provider): _LOGGER.error("Error: %s when validating options: %s", err, options) return None, None - encoding = options[CONF_ENCODING] + encoding = texttospeech.AudioEncoding[options[CONF_ENCODING]] gender = texttospeech.SsmlVoiceGender[options[CONF_GENDER]] voice = options[CONF_VOICE] if voice: @@ -281,7 +281,7 @@ class GoogleCloudTTSProvider(Provider): name=voice, ), audio_config=texttospeech.AudioConfig( - audio_encoding=texttospeech.AudioEncoding[encoding], + audio_encoding=encoding, speaking_rate=options[CONF_SPEED], pitch=options[CONF_PITCH], volume_gain_db=options[CONF_GAIN], @@ -295,4 +295,11 @@ class GoogleCloudTTSProvider(Provider): _LOGGER.error("Error occurred during Google Cloud TTS call: %s", err) return None, None - return encoding, response.audio_content + if encoding == texttospeech.AudioEncoding.MP3: + extension = "mp3" + elif encoding == texttospeech.AudioEncoding.OGG_OPUS: + extension = "ogg" + else: + extension = "wav" + + return extension, response.audio_content From 57aced50aaf39a3befbc1e8bc07e80a701bda0a2 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sun, 30 Jun 2024 15:04:55 +0200 Subject: [PATCH 0493/2411] Use SelectSelector in BMW config flow (#120831) --- .../components/bmw_connected_drive/config_flow.py | 8 +++++++- .../components/bmw_connected_drive/strings.json | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index fc274fc0f54..636274a01ad 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -20,6 +20,7 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig from . import DOMAIN from .const import CONF_ALLOWED_REGIONS, CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN @@ -28,7 +29,12 @@ DATA_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_REGION): vol.In(CONF_ALLOWED_REGIONS), + vol.Required(CONF_REGION): SelectSelector( + SelectSelectorConfig( + options=CONF_ALLOWED_REGIONS, + translation_key="regions", + ) + ), } ) diff --git a/homeassistant/components/bmw_connected_drive/strings.json b/homeassistant/components/bmw_connected_drive/strings.json index 587b13f084d..e7606232411 100644 --- a/homeassistant/components/bmw_connected_drive/strings.json +++ b/homeassistant/components/bmw_connected_drive/strings.json @@ -159,5 +159,14 @@ "name": "Charging" } } + }, + "selector": { + "regions": { + "options": { + "china": "China", + "north_america": "North America", + "rest_of_world": "Rest of world" + } + } } } From be68255ca4edf3f4822c9944425b0bf4073203a2 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sun, 30 Jun 2024 15:16:41 +0200 Subject: [PATCH 0494/2411] Fix routes with transfer in nmbs integration (#120808) --- homeassistant/components/nmbs/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 82fc6143b2d..6ccdc742430 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -261,7 +261,7 @@ class NMBSSensor(SensorEntity): attrs["via_arrival_platform"] = via["arrival"]["platform"] attrs["via_transfer_platform"] = via["departure"]["platform"] attrs["via_transfer_time"] = get_delay_in_minutes( - via["timeBetween"] + via["timebetween"] ) + get_delay_in_minutes(via["departure"]["delay"]) if delay > 0: From e961ddd5feadefaa44d775c8c6cbdb7f454c8529 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 30 Jun 2024 15:22:15 +0200 Subject: [PATCH 0495/2411] Simplify UniFi uptime sensor test (#120794) --- tests/components/unifi/test_sensor.py | 367 +++++++++++++------------- 1 file changed, 183 insertions(+), 184 deletions(-) diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 48e524aef76..281c4583399 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -459,115 +459,6 @@ async def test_bandwidth_sensors( assert hass.states.get("sensor.wired_client_tx") -@pytest.mark.parametrize( - "config_entry_options", - [ - { - CONF_ALLOW_BANDWIDTH_SENSORS: False, - CONF_ALLOW_UPTIME_SENSORS: True, - CONF_TRACK_CLIENTS: False, - CONF_TRACK_DEVICES: False, - } - ], -) -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "mac": "00:00:00:00:00:01", - "name": "client1", - "oui": "Producer", - "uptime": 0, - } - ] - ], -) -@pytest.mark.parametrize( - ("initial_uptime", "event_uptime", "small_variation_uptime", "new_uptime"), - [ - # Uptime listed in epoch time should never change - (1609462800, 1609462800, 1609462800, 1612141200), - # Uptime counted in seconds increases with every event - (60, 240, 480, 60), - ], -) -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_uptime_sensors( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - freezer: FrozenDateTimeFactory, - mock_websocket_message, - config_entry_options: MappingProxyType[str, Any], - config_entry_factory: Callable[[], ConfigEntry], - client_payload: list[dict[str, Any]], - initial_uptime, - event_uptime, - small_variation_uptime, - new_uptime, -) -> None: - """Verify that uptime sensors are working as expected.""" - uptime_client = client_payload[0] - uptime_client["uptime"] = initial_uptime - freezer.move_to(datetime(2021, 1, 1, 1, 1, 0, tzinfo=dt_util.UTC)) - config_entry = await config_entry_factory() - - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 - assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" - assert ( - entity_registry.async_get("sensor.client1_uptime").entity_category - is EntityCategory.DIAGNOSTIC - ) - - # Verify normal new event doesn't change uptime - # 4 minutes have passed - uptime_client["uptime"] = event_uptime - now = datetime(2021, 1, 1, 1, 4, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.now", return_value=now): - mock_websocket_message(message=MessageKey.CLIENT, data=uptime_client) - await hass.async_block_till_done() - - assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" - - # Verify small variation of uptime (<120 seconds) is ignored - # 15 seconds variation after 8 minutes - uptime_client["uptime"] = small_variation_uptime - now = datetime(2021, 1, 1, 1, 8, 15, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.now", return_value=now): - mock_websocket_message(message=MessageKey.CLIENT, data=uptime_client) - - assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" - - # Verify new event change uptime - # 1 month has passed - uptime_client["uptime"] = new_uptime - now = datetime(2021, 2, 1, 1, 1, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.now", return_value=now): - mock_websocket_message(message=MessageKey.CLIENT, data=uptime_client) - await hass.async_block_till_done() - - assert hass.states.get("sensor.client1_uptime").state == "2021-02-01T01:00:00+00:00" - - # Disable option - options = deepcopy(config_entry_options) - options[CONF_ALLOW_UPTIME_SENSORS] = False - hass.config_entries.async_update_entry(config_entry, options=options) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0 - assert hass.states.get("sensor.client1_uptime") is None - - # Enable option - options = deepcopy(config_entry_options) - options[CONF_ALLOW_UPTIME_SENSORS] = True - with patch("homeassistant.util.dt.now", return_value=now): - hass.config_entries.async_update_entry(config_entry, options=options) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 - assert hass.states.get("sensor.client1_uptime") - - @pytest.mark.parametrize( "config_entry_options", [{CONF_ALLOW_BANDWIDTH_SENSORS: True, CONF_ALLOW_UPTIME_SENSORS: True}], @@ -876,81 +767,6 @@ async def test_outlet_power_readings( assert sensor_data.state == expected_update_value -@pytest.mark.parametrize( - "device_payload", - [ - [ - { - "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, - "uptime": 60, - "version": "4.0.42.10433", - } - ] - ], -) -async def test_device_uptime( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - mock_websocket_message, - config_entry_factory: Callable[[], ConfigEntry], - device_payload: list[dict[str, Any]], -) -> None: - """Verify that uptime sensors are working as expected.""" - now = datetime(2021, 1, 1, 1, 1, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.now", return_value=now): - await config_entry_factory() - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 - assert hass.states.get("sensor.device_uptime").state == "2021-01-01T01:00:00+00:00" - - assert ( - entity_registry.async_get("sensor.device_uptime").entity_category - is EntityCategory.DIAGNOSTIC - ) - - # Verify normal new event doesn't change uptime - # 4 minutes have passed - device = device_payload[0] - device["uptime"] = 240 - now = datetime(2021, 1, 1, 1, 4, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.now", return_value=now): - mock_websocket_message(message=MessageKey.DEVICE, data=device) - - assert hass.states.get("sensor.device_uptime").state == "2021-01-01T01:00:00+00:00" - - # Verify small variation of uptime (<120 seconds) is ignored - # 15 seconds variation after 8 minutes - device = device_payload[0] - device["uptime"] = 480 - now = datetime(2021, 1, 1, 1, 8, 15, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.now", return_value=now): - mock_websocket_message(message=MessageKey.DEVICE, data=device) - - assert hass.states.get("sensor.device_uptime").state == "2021-01-01T01:00:00+00:00" - - # Verify new event change uptime - # 1 month has passed - - device["uptime"] = 60 - now = datetime(2021, 2, 1, 1, 1, 0, tzinfo=dt_util.UTC) - with patch("homeassistant.util.dt.now", return_value=now): - mock_websocket_message(message=MessageKey.DEVICE, data=device) - - assert hass.states.get("sensor.device_uptime").state == "2021-02-01T01:00:00+00:00" - - @pytest.mark.parametrize( "device_payload", [ @@ -1425,3 +1241,186 @@ async def test_sensor_sources( assert state.attributes.get(ATTR_STATE_CLASS) == snapshot assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == snapshot assert state.state == snapshot + + +async def _test_uptime_entity( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_websocket_message, + config_entry_factory: Callable[[], ConfigEntry], + payload: dict[str, Any], + entity_id: str, + message_key: MessageKey, + initial_uptime: int, + event_uptime: int, + small_variation_uptime: int, + new_uptime: int, +) -> None: + """Verify that uptime entities are working as expected.""" + payload["uptime"] = initial_uptime + freezer.move_to(datetime(2021, 1, 1, 1, 1, 0, tzinfo=dt_util.UTC)) + config_entry = await config_entry_factory() + + assert hass.states.get(entity_id).state == "2021-01-01T01:00:00+00:00" + + # Verify normal new event doesn't change uptime + # 4 minutes have passed + + payload["uptime"] = event_uptime + now = datetime(2021, 1, 1, 1, 4, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_websocket_message(message=message_key, data=payload) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "2021-01-01T01:00:00+00:00" + + # Verify small variation of uptime (<120 seconds) is ignored + # 15 seconds variation after 8 minutes + + payload["uptime"] = small_variation_uptime + now = datetime(2021, 1, 1, 1, 8, 15, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_websocket_message(message=message_key, data=payload) + + assert hass.states.get(entity_id).state == "2021-01-01T01:00:00+00:00" + + # Verify new event change uptime + # 1 month has passed + + payload["uptime"] = new_uptime + now = datetime(2021, 2, 1, 1, 1, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.now", return_value=now): + mock_websocket_message(message=message_key, data=payload) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "2021-02-01T01:00:00+00:00" + + return config_entry + + +@pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_UPTIME_SENSORS: True}]) +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "mac": "00:00:00:00:00:01", + "name": "client1", + "oui": "Producer", + "uptime": 0, + } + ] + ], +) +@pytest.mark.parametrize( + ("initial_uptime", "event_uptime", "small_variation_uptime", "new_uptime"), + [ + # Uptime listed in epoch time should never change + (1609462800, 1609462800, 1609462800, 1612141200), + # Uptime counted in seconds increases with every event + (60, 240, 480, 60), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_client_uptime( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, + mock_websocket_message, + config_entry_options: MappingProxyType[str, Any], + config_entry_factory: Callable[[], ConfigEntry], + client_payload: list[dict[str, Any]], + initial_uptime, + event_uptime, + small_variation_uptime, + new_uptime, +) -> None: + """Verify that client uptime sensors are working as expected.""" + config_entry = await _test_uptime_entity( + hass, + freezer, + mock_websocket_message, + config_entry_factory, + payload=client_payload[0], + entity_id="sensor.client1_uptime", + message_key=MessageKey.CLIENT, + initial_uptime=initial_uptime, + event_uptime=event_uptime, + small_variation_uptime=small_variation_uptime, + new_uptime=new_uptime, + ) + + assert ( + entity_registry.async_get("sensor.client1_uptime").entity_category + is EntityCategory.DIAGNOSTIC + ) + + # Disable option + options = deepcopy(config_entry_options) + options[CONF_ALLOW_UPTIME_SENSORS] = False + hass.config_entries.async_update_entry(config_entry, options=options) + await hass.async_block_till_done() + + assert hass.states.get("sensor.client1_uptime") is None + + # Enable option + options = deepcopy(config_entry_options) + options[CONF_ALLOW_UPTIME_SENSORS] = True + hass.config_entries.async_update_entry(config_entry, options=options) + await hass.async_block_till_done() + + assert hass.states.get("sensor.client1_uptime") + + +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "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, + "uptime": 60, + "version": "4.0.42.10433", + } + ] + ], +) +async def test_device_uptime( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, + mock_websocket_message, + config_entry_factory: Callable[[], ConfigEntry], + device_payload: list[dict[str, Any]], +) -> None: + """Verify that device uptime sensors are working as expected.""" + await _test_uptime_entity( + hass, + freezer, + mock_websocket_message, + config_entry_factory, + payload=device_payload[0], + entity_id="sensor.device_uptime", + message_key=MessageKey.DEVICE, + initial_uptime=60, + event_uptime=240, + small_variation_uptime=480, + new_uptime=60, + ) + + assert ( + entity_registry.async_get("sensor.device_uptime").entity_category + is EntityCategory.DIAGNOSTIC + ) From bb62a8a7dcbb6d9651b8fd0c194dfe1d7d74036a Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Sun, 30 Jun 2024 15:22:36 +0200 Subject: [PATCH 0496/2411] Change schema to TextSelector to enable autocomplete (#120771) --- homeassistant/components/fyta/config_flow.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fyta/config_flow.py b/homeassistant/components/fyta/config_flow.py index 234ba6fa8c0..4cb8bddbf10 100644 --- a/homeassistant/components/fyta/config_flow.py +++ b/homeassistant/components/fyta/config_flow.py @@ -16,6 +16,11 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) from . import FytaConfigEntry from .const import CONF_EXPIRATION, DOMAIN @@ -24,7 +29,20 @@ _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + { + vol.Required(CONF_USERNAME): TextSelector( + TextSelectorConfig( + type=TextSelectorType.TEXT, + autocomplete="username", + ), + ), + vol.Required(CONF_PASSWORD): TextSelector( + TextSelectorConfig( + type=TextSelectorType.PASSWORD, + autocomplete="current-password", + ), + ), + } ) From 289a63057842eebc6f2231a532a96ffedc203206 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 30 Jun 2024 15:25:38 +0200 Subject: [PATCH 0497/2411] Improve UniFi device tracker tests Pt2 (#120796) --- tests/components/unifi/test_device_tracker.py | 98 +++++++------------ 1 file changed, 35 insertions(+), 63 deletions(-) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 984fe50753f..7049effb33d 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -500,64 +500,51 @@ async def test_wireless_client_go_wired_issue( UniFi Network has a known issue that when a wireless device goes away it sometimes gets marked as wired. """ client_payload.append( - { - "essid": "ssid", - "hostname": "client", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:01", - } + WIRELESS_CLIENT_1 | {"last_seen": dt_util.as_timestamp(dt_util.utcnow())} ) - config_entry = await config_entry_factory() + await config_entry_factory() assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 # Client is wireless - client_state = hass.states.get("device_tracker.client") - assert client_state.state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME # Trigger wired bug - client = client_payload[0] - client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - client["is_wired"] = True - mock_websocket_message(message=MessageKey.CLIENT, data=client) + ws_client_1 = client_payload[0] | { + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "is_wired": True, + } + mock_websocket_message(message=MessageKey.CLIENT, data=ws_client_1) await hass.async_block_till_done() # Wired bug fix keeps client marked as wireless - client_state = hass.states.get("device_tracker.client") - assert client_state.state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME # Pass time - new_time = dt_util.utcnow() + timedelta( - seconds=(config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME)) - ) + new_time = dt_util.utcnow() + timedelta(seconds=DEFAULT_DETECTION_TIME) with freeze_time(new_time): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() # Marked as home according to the timer - client_state = hass.states.get("device_tracker.client") - assert client_state.state == STATE_NOT_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME # Try to mark client as connected - client["last_seen"] += 1 - mock_websocket_message(message=MessageKey.CLIENT, data=client) + ws_client_1["last_seen"] += 1 + mock_websocket_message(message=MessageKey.CLIENT, data=ws_client_1) await hass.async_block_till_done() # Make sure it don't go online again until wired bug disappears - client_state = hass.states.get("device_tracker.client") - assert client_state.state == STATE_NOT_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME # Make client wireless - client["last_seen"] += 1 - client["is_wired"] = False - mock_websocket_message(message=MessageKey.CLIENT, data=client) + ws_client_1["last_seen"] += 1 + ws_client_1["is_wired"] = False + mock_websocket_message(message=MessageKey.CLIENT, data=ws_client_1) await hass.async_block_till_done() # Client is no longer affected by wired bug and can be marked online - client_state = hass.states.get("device_tracker.client") - assert client_state.state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME @pytest.mark.parametrize("config_entry_options", [{CONF_IGNORE_WIRED_BUG: True}]) @@ -570,64 +557,49 @@ async def test_option_ignore_wired_bug( ) -> None: """Test option to ignore wired bug.""" client_payload.append( - { - "ap_mac": "00:00:00:00:02:01", - "essid": "ssid", - "hostname": "client", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:01", - } + WIRELESS_CLIENT_1 | {"last_seen": dt_util.as_timestamp(dt_util.utcnow())} ) - config_entry = await config_entry_factory() + await config_entry_factory() assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 # Client is wireless - client_state = hass.states.get("device_tracker.client") - assert client_state.state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME # Trigger wired bug - client = client_payload[0] - client["is_wired"] = True - mock_websocket_message(message=MessageKey.CLIENT, data=client) + ws_client_1 = client_payload[0] + ws_client_1["is_wired"] = True + mock_websocket_message(message=MessageKey.CLIENT, data=ws_client_1) await hass.async_block_till_done() # Wired bug in effect - client_state = hass.states.get("device_tracker.client") - assert client_state.state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME - # pass time - new_time = dt_util.utcnow() + timedelta( - seconds=config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) - ) + # Pass time + new_time = dt_util.utcnow() + timedelta(seconds=DEFAULT_DETECTION_TIME) with freeze_time(new_time): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() # Timer marks client as away - client_state = hass.states.get("device_tracker.client") - assert client_state.state == STATE_NOT_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME # Mark client as connected again - client["last_seen"] += 1 - mock_websocket_message(message=MessageKey.CLIENT, data=client) + ws_client_1["last_seen"] += 1 + mock_websocket_message(message=MessageKey.CLIENT, data=ws_client_1) await hass.async_block_till_done() # Ignoring wired bug allows client to go home again even while affected - client_state = hass.states.get("device_tracker.client") - assert client_state.state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME # Make client wireless - client["last_seen"] += 1 - client["is_wired"] = False - mock_websocket_message(message=MessageKey.CLIENT, data=client) + ws_client_1["last_seen"] += 1 + ws_client_1["is_wired"] = False + mock_websocket_message(message=MessageKey.CLIENT, data=ws_client_1) await hass.async_block_till_done() # Client is wireless and still connected - client_state = hass.states.get("device_tracker.client") - assert client_state.state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME @pytest.mark.parametrize( From 8b3319b772d1c844c96f3ad5077db62030e0fbcc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 30 Jun 2024 15:26:45 +0200 Subject: [PATCH 0498/2411] Improve UniFi device tracker tests (#120795) --- tests/components/unifi/test_device_tracker.py | 180 +++++------------- 1 file changed, 51 insertions(+), 129 deletions(-) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 7049effb33d..752b25d942d 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -213,67 +213,40 @@ async def test_client_state_from_event_source( assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME +@pytest.mark.parametrize("device_payload", [[SWITCH_1]]) +@pytest.mark.usefixtures("mock_device_registry") @pytest.mark.parametrize( - "device_payload", + ("state", "interval", "expected"), [ - [ - { - "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", - }, - { - "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", - }, - ] + # Start home, new signal but still home, heartbeat timer triggers away + (1, 20, (STATE_HOME, STATE_HOME, STATE_NOT_HOME)), + # Start away, new signal but still home, heartbeat time do not trigger + (0, 40, (STATE_NOT_HOME, STATE_HOME, STATE_HOME)), ], ) -@pytest.mark.usefixtures("config_entry_setup") -@pytest.mark.usefixtures("mock_device_registry") -async def test_tracked_devices( +async def test_tracked_device_state_change( hass: HomeAssistant, freezer: FrozenDateTimeFactory, + config_entry_factory: Callable[[], ConfigEntry], mock_websocket_message, device_payload: list[dict[str, Any]], + state: int, + interval: int, + expected: list[str], ) -> None: """Test the update_items function with some devices.""" - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 - assert hass.states.get("device_tracker.device_1").state == STATE_HOME - assert hass.states.get("device_tracker.device_2").state == STATE_NOT_HOME + device_payload[0] = device_payload[0] | {"state": state} + await config_entry_factory() + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 + assert hass.states.get("device_tracker.switch_1").state == expected[0] # State change signalling work - device_1 = device_payload[0] - device_1["next_interval"] = 20 - device_2 = device_payload[1] - device_2["state"] = 1 - device_2["next_interval"] = 50 - mock_websocket_message(message=MessageKey.DEVICE, data=[device_1, device_2]) + switch_1 = device_payload[0] | {"state": 1, "next_interval": interval} + mock_websocket_message(message=MessageKey.DEVICE, data=[switch_1]) await hass.async_block_till_done() - assert hass.states.get("device_tracker.device_1").state == STATE_HOME - assert hass.states.get("device_tracker.device_2").state == STATE_HOME + # Too little time has passed + assert hass.states.get("device_tracker.switch_1").state == expected[1] # Change of time can mark device not_home outside of expected reporting interval new_time = dt_util.utcnow() + timedelta(seconds=90) @@ -281,16 +254,15 @@ async def test_tracked_devices( async_fire_time_changed(hass, new_time) await hass.async_block_till_done() - assert hass.states.get("device_tracker.device_1").state == STATE_NOT_HOME - assert hass.states.get("device_tracker.device_2").state == STATE_HOME + # Heartbeat to update state is interval + 60 seconds + assert hass.states.get("device_tracker.switch_1").state == expected[2] # Disabled device is unavailable - device_1["disabled"] = True - mock_websocket_message(message=MessageKey.DEVICE, data=device_1) + switch_1["disabled"] = True + mock_websocket_message(message=MessageKey.DEVICE, data=switch_1) await hass.async_block_till_done() - assert hass.states.get("device_tracker.device_1").state == STATE_UNAVAILABLE - assert hass.states.get("device_tracker.device_2").state == STATE_HOME + assert hass.states.get("device_tracker.switch_1").state == STATE_UNAVAILABLE @pytest.mark.parametrize("client_payload", [[WIRELESS_CLIENT_1, WIRED_CLIENT_1]]) @@ -313,61 +285,25 @@ async def test_remove_clients( assert hass.states.get("device_tracker.wd_client_1") -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "essid": "ssid", - "hostname": "client", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - ] - ], -) -@pytest.mark.parametrize( - "device_payload", - [ - [ - { - "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", - } - ] - ], -) +@pytest.mark.parametrize("client_payload", [[WIRELESS_CLIENT_1]]) +@pytest.mark.parametrize("device_payload", [[SWITCH_1]]) @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("mock_device_registry") async def test_hub_state_change(hass: HomeAssistant, mock_websocket_state) -> None: """Verify entities state reflect on hub connection becoming unavailable.""" assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 - assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME - assert hass.states.get("device_tracker.device").state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME + assert hass.states.get("device_tracker.switch_1").state == STATE_HOME # Controller unavailable await mock_websocket_state.disconnect() - assert hass.states.get("device_tracker.client").state == STATE_UNAVAILABLE - assert hass.states.get("device_tracker.device").state == STATE_UNAVAILABLE + assert hass.states.get("device_tracker.ws_client_1").state == STATE_UNAVAILABLE + assert hass.states.get("device_tracker.switch_1").state == STATE_UNAVAILABLE # Controller available await mock_websocket_state.reconnect() - assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME - assert hass.states.get("device_tracker.device").state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME + assert hass.states.get("device_tracker.switch_1").state == STATE_HOME @pytest.mark.usefixtures("mock_device_registry") @@ -383,13 +319,7 @@ async def test_option_ssid_filter( Client on SSID2 will be removed on change of options. """ client_payload += [ - { - "essid": "ssid", - "hostname": "client", - "is_wired": False, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:01", - }, + WIRELESS_CLIENT_1 | {"last_seen": dt_util.as_timestamp(dt_util.utcnow())}, { "essid": "ssid2", "hostname": "client_on_ssid2", @@ -401,7 +331,7 @@ async def test_option_ssid_filter( config_entry = await config_entry_factory() assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 - assert hass.states.get("device_tracker.client").state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_NOT_HOME # Setting SSID filter will remove clients outside of filter @@ -411,33 +341,29 @@ async def test_option_ssid_filter( await hass.async_block_till_done() # Not affected by SSID filter - assert hass.states.get("device_tracker.client").state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME # Removed due to SSID filter assert not hass.states.get("device_tracker.client_on_ssid2") # Roams to SSID outside of filter - client = client_payload[0] - client["essid"] = "other_ssid" - mock_websocket_message(message=MessageKey.CLIENT, data=client) + ws_client_1 = client_payload[0] | {"essid": "other_ssid"} + mock_websocket_message(message=MessageKey.CLIENT, data=ws_client_1) # Data update while SSID filter is in effect shouldn't create the client - client_on_ssid2 = client_payload[1] - client_on_ssid2["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + client_on_ssid2 = client_payload[1] | { + "last_seen": dt_util.as_timestamp(dt_util.utcnow()) + } mock_websocket_message(message=MessageKey.CLIENT, data=client_on_ssid2) await hass.async_block_till_done() - new_time = dt_util.utcnow() + timedelta( - seconds=( - config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + 1 - ) - ) + new_time = dt_util.utcnow() + timedelta(seconds=(DEFAULT_DETECTION_TIME + 1)) with freeze_time(new_time): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() # SSID filter marks client as away - assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME # SSID still outside of filter assert not hass.states.get("device_tracker.client_on_ssid2") @@ -446,25 +372,23 @@ async def test_option_ssid_filter( hass.config_entries.async_update_entry(config_entry, options={CONF_SSID_FILTER: []}) await hass.async_block_till_done() - client["last_seen"] += 1 + ws_client_1["last_seen"] += 1 client_on_ssid2["last_seen"] += 1 - mock_websocket_message(message=MessageKey.CLIENT, data=[client, client_on_ssid2]) + mock_websocket_message( + message=MessageKey.CLIENT, data=[ws_client_1, client_on_ssid2] + ) await hass.async_block_till_done() - assert hass.states.get("device_tracker.client").state == STATE_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_HOME # Time pass to mark client as away - new_time += timedelta( - seconds=( - config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + 1 - ) - ) + new_time += timedelta(seconds=(DEFAULT_DETECTION_TIME + 1)) with freeze_time(new_time): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() - assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME + assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME client_on_ssid2["last_seen"] += 1 mock_websocket_message(message=MessageKey.CLIENT, data=client_on_ssid2) @@ -478,9 +402,7 @@ async def test_option_ssid_filter( mock_websocket_message(message=MessageKey.CLIENT, data=client_on_ssid2) await hass.async_block_till_done() - new_time += timedelta( - seconds=(config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME)) - ) + new_time += timedelta(seconds=DEFAULT_DETECTION_TIME) with freeze_time(new_time): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() From b375f5227b1d8480bb147d674285eb5521953303 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 30 Jun 2024 15:28:01 +0200 Subject: [PATCH 0499/2411] Bump pizzapi to 0.0.6 (#120691) --- homeassistant/components/dominos/__init__.py | 14 +++++++------- homeassistant/components/dominos/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py index ce7b36f2280..9b11b667e84 100644 --- a/homeassistant/components/dominos/__init__.py +++ b/homeassistant/components/dominos/__init__.py @@ -4,11 +4,11 @@ from datetime import timedelta import logging from pizzapi import Address, Customer, Order -from pizzapi.address import StoreException import voluptuous as vol from homeassistant.components import http from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -118,7 +118,7 @@ class Dominos: self.country = conf.get(ATTR_COUNTRY) try: self.closest_store = self.address.closest_store() - except StoreException: + except Exception: # noqa: BLE001 self.closest_store = None def handle_order(self, call: ServiceCall) -> None: @@ -139,7 +139,7 @@ class Dominos: """Update the shared closest store (if open).""" try: self.closest_store = self.address.closest_store() - except StoreException: + except Exception: # noqa: BLE001 self.closest_store = None return False return True @@ -219,7 +219,7 @@ class DominosOrder(Entity): """Update the order state and refreshes the store.""" try: self.dominos.update_closest_store() - except StoreException: + except Exception: # noqa: BLE001 self._orderable = False return @@ -227,13 +227,13 @@ class DominosOrder(Entity): order = self.order() order.pay_with() self._orderable = True - except StoreException: + except Exception: # noqa: BLE001 self._orderable = False def order(self): """Create the order object.""" if self.dominos.closest_store is None: - raise StoreException + raise HomeAssistantError("No store available") order = Order( self.dominos.closest_store, @@ -252,7 +252,7 @@ class DominosOrder(Entity): try: order = self.order() order.place() - except StoreException: + except Exception: # noqa: BLE001 self._orderable = False _LOGGER.warning( "Attempted to order Dominos - Order invalid or store closed" diff --git a/homeassistant/components/dominos/manifest.json b/homeassistant/components/dominos/manifest.json index dfb8966013f..442f433db7c 100644 --- a/homeassistant/components/dominos/manifest.json +++ b/homeassistant/components/dominos/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/dominos", "iot_class": "cloud_polling", "loggers": ["pizzapi"], - "requirements": ["pizzapi==0.0.3"] + "requirements": ["pizzapi==0.0.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2d540b5eb52..cc610e2929e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1557,7 +1557,7 @@ pigpio==1.78 pilight==0.1.1 # homeassistant.components.dominos -pizzapi==0.0.3 +pizzapi==0.0.6 # homeassistant.components.plex plexauth==0.0.6 From 05ffd637f54de14c41756edb3ea05c5616ff5bb4 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Sun, 30 Jun 2024 15:29:00 +0200 Subject: [PATCH 0500/2411] Migrate Ecowitt to runtime_data (#120675) --- homeassistant/components/ecowitt/__init__.py | 13 ++++++------- homeassistant/components/ecowitt/binary_sensor.py | 11 ++++++----- homeassistant/components/ecowitt/diagnostics.py | 8 +++----- homeassistant/components/ecowitt/sensor.py | 11 ++++++----- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/ecowitt/__init__.py b/homeassistant/components/ecowitt/__init__.py index 0c330bc3f33..3097160f463 100644 --- a/homeassistant/components/ecowitt/__init__.py +++ b/homeassistant/components/ecowitt/__init__.py @@ -14,10 +14,12 @@ from .const import DOMAIN PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] +type EcowittConfigEntry = ConfigEntry[EcoWittListener] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: EcowittConfigEntry) -> bool: """Set up the Ecowitt component from UI.""" - ecowitt = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = EcoWittListener() + ecowitt = entry.runtime_data = EcoWittListener() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -43,11 +45,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: EcowittConfigEntry) -> bool: """Unload a config entry.""" webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID]) - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/ecowitt/binary_sensor.py b/homeassistant/components/ecowitt/binary_sensor.py index 1ef2956d84b..5bc782e3589 100644 --- a/homeassistant/components/ecowitt/binary_sensor.py +++ b/homeassistant/components/ecowitt/binary_sensor.py @@ -3,19 +3,18 @@ import dataclasses from typing import Final -from aioecowitt import EcoWittListener, EcoWittSensor, EcoWittSensorTypes +from aioecowitt import EcoWittSensor, EcoWittSensorTypes from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from . import EcowittConfigEntry from .entity import EcowittEntity ECOWITT_BINARYSENSORS_MAPPING: Final = { @@ -31,10 +30,12 @@ ECOWITT_BINARYSENSORS_MAPPING: Final = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: EcowittConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add sensors if new.""" - ecowitt: EcoWittListener = hass.data[DOMAIN][entry.entry_id] + ecowitt = entry.runtime_data def _new_sensor(sensor: EcoWittSensor) -> None: """Add new sensor.""" diff --git a/homeassistant/components/ecowitt/diagnostics.py b/homeassistant/components/ecowitt/diagnostics.py index a21d11e8126..4c0afa25e0c 100644 --- a/homeassistant/components/ecowitt/diagnostics.py +++ b/homeassistant/components/ecowitt/diagnostics.py @@ -4,20 +4,18 @@ from __future__ import annotations from typing import Any -from aioecowitt import EcoWittListener - -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntry +from . import EcowittConfigEntry from .const import DOMAIN async def async_get_device_diagnostics( - hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry + hass: HomeAssistant, entry: EcowittConfigEntry, device: DeviceEntry ) -> dict[str, Any]: """Return diagnostics for a device entry.""" - ecowitt: EcoWittListener = hass.data[DOMAIN][entry.entry_id] + ecowitt = entry.runtime_data station_id = next(item[1] for item in device.identifiers if item[0] == DOMAIN) station = ecowitt.stations[station_id] diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py index 6845fb64d4c..23af2f2a3af 100644 --- a/homeassistant/components/ecowitt/sensor.py +++ b/homeassistant/components/ecowitt/sensor.py @@ -6,7 +6,7 @@ import dataclasses from datetime import datetime from typing import Final -from aioecowitt import EcoWittListener, EcoWittSensor, EcoWittSensorTypes +from aioecowitt import EcoWittSensor, EcoWittSensorTypes from homeassistant.components.sensor import ( SensorDeviceClass, @@ -14,7 +14,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, @@ -37,7 +36,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM -from .const import DOMAIN +from . import EcowittConfigEntry from .entity import EcowittEntity _METRIC: Final = ( @@ -217,10 +216,12 @@ ECOWITT_SENSORS_MAPPING: Final = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: EcowittConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add sensors if new.""" - ecowitt: EcoWittListener = hass.data[DOMAIN][entry.entry_id] + ecowitt = entry.runtime_data def _new_sensor(sensor: EcoWittSensor) -> None: """Add new sensor.""" From 419d89f86304dcef3ba036b5cc2cba13fe4e0a9a Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 30 Jun 2024 09:30:52 -0400 Subject: [PATCH 0501/2411] Allow EM heat on from any mode in Honeywell (#120750) --- homeassistant/components/honeywell/switch.py | 13 ++++++------ tests/components/honeywell/test_switch.py | 21 +------------------- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/honeywell/switch.py b/homeassistant/components/honeywell/switch.py index 53a9b27ee72..b90dd339593 100644 --- a/homeassistant/components/honeywell/switch.py +++ b/homeassistant/components/honeywell/switch.py @@ -71,13 +71,12 @@ class HoneywellSwitch(SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on if heat mode is enabled.""" - if self._device.system_mode == "heat": - try: - await self._device.set_system_mode("emheat") - except SomeComfortError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, translation_key="switch_failed_on" - ) from err + try: + await self._device.set_system_mode("emheat") + except SomeComfortError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="switch_failed_on" + ) from err async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off if on.""" diff --git a/tests/components/honeywell/test_switch.py b/tests/components/honeywell/test_switch.py index 73052871ef1..482b9837b93 100644 --- a/tests/components/honeywell/test_switch.py +++ b/tests/components/honeywell/test_switch.py @@ -24,26 +24,6 @@ async def test_emheat_switch( await init_integration(hass, config_entry) entity_id = f"switch.{device.name}_emergency_heat" - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - device.set_system_mode.assert_not_called() - - device.set_system_mode.reset_mock() - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - device.set_system_mode.assert_not_called() - - device.system_mode = "heat" - await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -53,6 +33,7 @@ async def test_emheat_switch( device.set_system_mode.assert_called_once_with("emheat") device.set_system_mode.reset_mock() + device.system_mode = "emheat" await hass.services.async_call( SWITCH_DOMAIN, From f672eec5158b7cc23180855f7499460ab1946eb6 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sun, 30 Jun 2024 16:27:03 +0200 Subject: [PATCH 0502/2411] Fix unifi device tracker test imports (#120864) --- tests/components/unifi/test_device_tracker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 752b25d942d..0b20822bea4 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -14,7 +14,6 @@ from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.unifi.const import ( CONF_BLOCK_CLIENT, CONF_CLIENT_SOURCE, - CONF_DETECTION_TIME, CONF_IGNORE_WIRED_BUG, CONF_SSID_FILTER, CONF_TRACK_CLIENTS, From 2f5ec41fa6b01afc5f7d1e27aabb62f62b82043a Mon Sep 17 00:00:00 2001 From: Sander Peterse Date: Sun, 30 Jun 2024 19:04:20 +0200 Subject: [PATCH 0503/2411] Add valve domain to HomeKit (#115901) Co-authored-by: J. Nick Koston --- .../components/homekit/accessories.py | 13 +-- .../components/homekit/config_flow.py | 2 + .../components/homekit/type_switches.py | 86 ++++++++++++++++--- .../homekit/test_get_accessories.py | 23 ++++- .../components/homekit/test_type_switches.py | 70 +++++++++++++-- 5 files changed, 166 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 40e86efe6a9..8d10387e239 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -104,12 +104,12 @@ from .util import ( _LOGGER = logging.getLogger(__name__) SWITCH_TYPES = { - TYPE_FAUCET: "Valve", + TYPE_FAUCET: "ValveSwitch", TYPE_OUTLET: "Outlet", - TYPE_SHOWER: "Valve", - TYPE_SPRINKLER: "Valve", + TYPE_SHOWER: "ValveSwitch", + TYPE_SPRINKLER: "ValveSwitch", TYPE_SWITCH: "Switch", - TYPE_VALVE: "Valve", + TYPE_VALVE: "ValveSwitch", } TYPES: Registry[str, type[HomeAccessory]] = Registry() @@ -244,6 +244,9 @@ def get_accessory( # noqa: C901 else: a_type = "Switch" + elif state.domain == "valve": + a_type = "Valve" + elif state.domain == "vacuum": a_type = "Vacuum" @@ -289,7 +292,7 @@ class HomeAccessory(Accessory): # type: ignore[misc] name: str, entity_id: str, aid: int, - config: dict, + config: dict[str, Any], *args: Any, category: int = CATEGORY_OTHER, device_id: str | None = None, diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 30fb80cbdfc..78979f73490 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -17,6 +17,7 @@ from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN +from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN from homeassistant.config_entries import ( SOURCE_IMPORT, ConfigEntry, @@ -105,6 +106,7 @@ SUPPORTED_DOMAINS = [ "switch", "vacuum", "water_heater", + VALVE_DOMAIN, ] DEFAULT_DOMAINS = [ diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 86861417bdb..45a823882f7 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any, NamedTuple +from typing import Any, Final, NamedTuple from pyhap.characteristic import Characteristic from pyhap.const import ( @@ -28,14 +28,19 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_TYPE, + SERVICE_CLOSE_VALVE, + SERVICE_OPEN_VALVE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_CLOSING, STATE_ON, + STATE_OPEN, + STATE_OPENING, ) -from homeassistant.core import State, callback, split_entity_id +from homeassistant.core import HomeAssistant, State, callback, split_entity_id from homeassistant.helpers.event import async_call_later -from .accessories import TYPES, HomeAccessory +from .accessories import TYPES, HomeAccessory, HomeDriver from .const import ( CHAR_ACTIVE, CHAR_IN_USE, @@ -55,6 +60,8 @@ from .util import cleanup_name_for_homekit _LOGGER = logging.getLogger(__name__) +VALVE_OPEN_STATES: Final = {STATE_OPEN, STATE_OPENING, STATE_CLOSING} + class ValveInfo(NamedTuple): """Category and type information for valve.""" @@ -211,18 +218,28 @@ class Vacuum(Switch): self.char_on.set_value(current_state) -@TYPES.register("Valve") -class Valve(HomeAccessory): - """Generate a Valve accessory.""" +class ValveBase(HomeAccessory): + """Valve base class.""" - def __init__(self, *args: Any) -> None: + def __init__( + self, + valve_type: str, + open_states: set[str], + on_service: str, + off_service: str, + *args: Any, + **kwargs: Any, + ) -> None: """Initialize a Valve accessory object.""" - super().__init__(*args) + super().__init__(*args, **kwargs) + self.domain = split_entity_id(self.entity_id)[0] state = self.hass.states.get(self.entity_id) assert state - valve_type = self.config[CONF_TYPE] self.category = VALVE_TYPE[valve_type].category + self.open_states = open_states + self.on_service = on_service + self.off_service = off_service serv_valve = self.add_preload_service(SERV_VALVE) self.char_active = serv_valve.configure_char( @@ -241,19 +258,64 @@ class Valve(HomeAccessory): _LOGGER.debug("%s: Set switch state to %s", self.entity_id, value) self.char_in_use.set_value(value) params = {ATTR_ENTITY_ID: self.entity_id} - service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF - self.async_call_service(DOMAIN, service, params) + service = self.on_service if value else self.off_service + self.async_call_service(self.domain, service, params) @callback def async_update_state(self, new_state: State) -> None: """Update switch state after state changed.""" - current_state = 1 if new_state.state == STATE_ON else 0 + current_state = 1 if new_state.state in self.open_states else 0 _LOGGER.debug("%s: Set active state to %s", self.entity_id, current_state) self.char_active.set_value(current_state) _LOGGER.debug("%s: Set in_use state to %s", self.entity_id, current_state) self.char_in_use.set_value(current_state) +@TYPES.register("ValveSwitch") +class ValveSwitch(ValveBase): + """Generate a Valve accessory from a HomeAssistant switch.""" + + def __init__( + self, + hass: HomeAssistant, + driver: HomeDriver, + name: str, + entity_id: str, + aid: int, + config: dict[str, Any], + *args: Any, + ) -> None: + """Initialize a Valve accessory object.""" + super().__init__( + config[CONF_TYPE], + {STATE_ON}, + SERVICE_TURN_ON, + SERVICE_TURN_OFF, + hass, + driver, + name, + entity_id, + aid, + config, + *args, + ) + + +@TYPES.register("Valve") +class Valve(ValveBase): + """Generate a Valve accessory from a HomeAssistant valve.""" + + def __init__(self, *args: Any) -> None: + """Initialize a Valve accessory object.""" + super().__init__( + TYPE_VALVE, + VALVE_OPEN_STATES, + SERVICE_OPEN_VALVE, + SERVICE_CLOSE_VALVE, + *args, + ) + + @TYPES.register("SelectSwitch") class SelectSwitch(HomeAccessory): """Generate a Switch accessory that contains multiple switches.""" diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 02a39ed9258..c4b1cbe98d8 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -335,10 +335,10 @@ def test_type_sensors(type_name, entity_id, state, attrs) -> None: ("SelectSwitch", "select.test", "option1", {}, {}), ("Switch", "switch.test", "on", {}, {}), ("Switch", "switch.test", "on", {}, {CONF_TYPE: TYPE_SWITCH}), - ("Valve", "switch.test", "on", {}, {CONF_TYPE: TYPE_FAUCET}), - ("Valve", "switch.test", "on", {}, {CONF_TYPE: TYPE_VALVE}), - ("Valve", "switch.test", "on", {}, {CONF_TYPE: TYPE_SHOWER}), - ("Valve", "switch.test", "on", {}, {CONF_TYPE: TYPE_SPRINKLER}), + ("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_FAUCET}), + ("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_VALVE}), + ("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_SHOWER}), + ("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_SPRINKLER}), ], ) def test_type_switches(type_name, entity_id, state, attrs, config) -> None: @@ -350,6 +350,21 @@ def test_type_switches(type_name, entity_id, state, attrs, config) -> None: assert mock_type.called +@pytest.mark.parametrize( + ("type_name", "entity_id", "state", "attrs"), + [ + ("Valve", "valve.test", "on", {}), + ], +) +def test_type_valve(type_name, entity_id, state, attrs) -> None: + """Test if valve types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, None, entity_state, 2, {}) + assert mock_type.called + + @pytest.mark.parametrize( ("type_name", "entity_id", "state", "attrs"), [ diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 27937babc57..a2c88d7e1ab 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -17,6 +17,7 @@ from homeassistant.components.homekit.type_switches import ( Switch, Vacuum, Valve, + ValveSwitch, ) from homeassistant.components.select import ATTR_OPTIONS from homeassistant.components.vacuum import ( @@ -33,9 +34,13 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_TYPE, + SERVICE_CLOSE_VALVE, + SERVICE_OPEN_VALVE, SERVICE_SELECT_OPTION, + STATE_CLOSED, STATE_OFF, STATE_ON, + STATE_OPEN, ) from homeassistant.core import HomeAssistant, split_entity_id import homeassistant.util.dt as dt_util @@ -140,32 +145,34 @@ async def test_switch_set_state( assert events[-1].data[ATTR_VALUE] is None -async def test_valve_set_state(hass: HomeAssistant, hk_driver, events) -> None: +async def test_valve_switch_set_state(hass: HomeAssistant, hk_driver, events) -> None: """Test if Valve accessory and HA are updated accordingly.""" entity_id = "switch.valve_test" hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Valve(hass, hk_driver, "Valve", entity_id, 2, {CONF_TYPE: TYPE_FAUCET}) + acc = ValveSwitch(hass, hk_driver, "Valve", entity_id, 2, {CONF_TYPE: TYPE_FAUCET}) acc.run() await hass.async_block_till_done() assert acc.category == 29 # Faucet assert acc.char_valve_type.value == 3 # Water faucet - acc = Valve(hass, hk_driver, "Valve", entity_id, 3, {CONF_TYPE: TYPE_SHOWER}) + acc = ValveSwitch(hass, hk_driver, "Valve", entity_id, 3, {CONF_TYPE: TYPE_SHOWER}) acc.run() await hass.async_block_till_done() assert acc.category == 30 # Shower assert acc.char_valve_type.value == 2 # Shower head - acc = Valve(hass, hk_driver, "Valve", entity_id, 4, {CONF_TYPE: TYPE_SPRINKLER}) + acc = ValveSwitch( + hass, hk_driver, "Valve", entity_id, 4, {CONF_TYPE: TYPE_SPRINKLER} + ) acc.run() await hass.async_block_till_done() assert acc.category == 28 # Sprinkler assert acc.char_valve_type.value == 1 # Irrigation - acc = Valve(hass, hk_driver, "Valve", entity_id, 5, {CONF_TYPE: TYPE_VALVE}) + acc = ValveSwitch(hass, hk_driver, "Valve", entity_id, 5, {CONF_TYPE: TYPE_VALVE}) acc.run() await hass.async_block_till_done() @@ -187,8 +194,57 @@ async def test_valve_set_state(hass: HomeAssistant, hk_driver, events) -> None: assert acc.char_in_use.value == 0 # Set from HomeKit - call_turn_on = async_mock_service(hass, "switch", "turn_on") - call_turn_off = async_mock_service(hass, "switch", "turn_off") + call_turn_on = async_mock_service(hass, "switch", SERVICE_TURN_ON) + call_turn_off = async_mock_service(hass, "switch", SERVICE_TURN_OFF) + + acc.char_active.client_update_value(1) + await hass.async_block_till_done() + assert acc.char_in_use.value == 1 + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None + + acc.char_active.client_update_value(0) + await hass.async_block_till_done() + assert acc.char_in_use.value == 0 + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None + + +async def test_valve_set_state(hass: HomeAssistant, hk_driver, events) -> None: + """Test if Valve accessory and HA are updated accordingly.""" + entity_id = "valve.valve_test" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + + acc = Valve(hass, hk_driver, "Valve", entity_id, 5, {CONF_TYPE: TYPE_VALVE}) + acc.run() + await hass.async_block_till_done() + + assert acc.aid == 5 + assert acc.category == 29 # Faucet + + assert acc.char_active.value == 0 + assert acc.char_in_use.value == 0 + assert acc.char_valve_type.value == 0 # Generic Valve + + hass.states.async_set(entity_id, STATE_OPEN) + await hass.async_block_till_done() + assert acc.char_active.value == 1 + assert acc.char_in_use.value == 1 + + hass.states.async_set(entity_id, STATE_CLOSED) + await hass.async_block_till_done() + assert acc.char_active.value == 0 + assert acc.char_in_use.value == 0 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, "valve", SERVICE_OPEN_VALVE) + call_turn_off = async_mock_service(hass, "valve", SERVICE_CLOSE_VALVE) acc.char_active.client_update_value(1) await hass.async_block_till_done() From 1a63bb89cbdc695195570d016389204a0292148b Mon Sep 17 00:00:00 2001 From: Etienne Soufflet Date: Sun, 30 Jun 2024 20:38:35 +0200 Subject: [PATCH 0504/2411] Fix Tado fan mode (#120809) --- homeassistant/components/tado/climate.py | 17 ++- homeassistant/components/tado/helper.py | 12 ++ .../tado/fixtures/smartac4.with_fanlevel.json | 88 ++++++++++++ .../components/tado/fixtures/zone_states.json | 73 ++++++++++ ...th_fanlevel_horizontal_vertical_swing.json | 130 ++++++++++++++++++ tests/components/tado/fixtures/zones.json | 40 ++++++ tests/components/tado/test_climate.py | 32 +++++ tests/components/tado/util.py | 18 +++ 8 files changed, 401 insertions(+), 9 deletions(-) create mode 100644 tests/components/tado/fixtures/smartac4.with_fanlevel.json create mode 100644 tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 40bdb19b31b..116985796d5 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -73,7 +73,7 @@ from .const import ( TYPE_HEATING, ) from .entity import TadoZoneEntity -from .helper import decide_duration, decide_overlay_mode +from .helper import decide_duration, decide_overlay_mode, generate_supported_fanmodes _LOGGER = logging.getLogger(__name__) @@ -200,15 +200,14 @@ def create_climate_entity( continue if capabilities[mode].get("fanSpeeds"): - supported_fan_modes = [ - TADO_TO_HA_FAN_MODE_MAP_LEGACY[speed] - for speed in capabilities[mode]["fanSpeeds"] - ] + supported_fan_modes = generate_supported_fanmodes( + TADO_TO_HA_FAN_MODE_MAP_LEGACY, capabilities[mode]["fanSpeeds"] + ) + else: - supported_fan_modes = [ - TADO_TO_HA_FAN_MODE_MAP[level] - for level in capabilities[mode]["fanLevel"] - ] + supported_fan_modes = generate_supported_fanmodes( + TADO_TO_HA_FAN_MODE_MAP, capabilities[mode]["fanLevel"] + ) cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"] else: diff --git a/homeassistant/components/tado/helper.py b/homeassistant/components/tado/helper.py index efcd3e7c4ea..81bff1e36c3 100644 --- a/homeassistant/components/tado/helper.py +++ b/homeassistant/components/tado/helper.py @@ -49,3 +49,15 @@ def decide_duration( ) return duration + + +def generate_supported_fanmodes(tado_to_ha_mapping: dict[str, str], options: list[str]): + """Return correct list of fan modes or None.""" + supported_fanmodes = [ + tado_to_ha_mapping.get(option) + for option in options + if tado_to_ha_mapping.get(option) is not None + ] + if not supported_fanmodes: + return None + return supported_fanmodes diff --git a/tests/components/tado/fixtures/smartac4.with_fanlevel.json b/tests/components/tado/fixtures/smartac4.with_fanlevel.json new file mode 100644 index 00000000000..ea1f9cbd8e5 --- /dev/null +++ b/tests/components/tado/fixtures/smartac4.with_fanlevel.json @@ -0,0 +1,88 @@ +{ + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 25.0, + "fahrenheit": 77.0 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON", + "horizontalSwing": "ON" + }, + "overlayType": "MANUAL", + "overlay": { + "type": "MANUAL", + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 25.0, + "fahrenheit": 77.0 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON" + }, + "termination": { + "type": "MANUAL", + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null + } + }, + "openWindow": null, + "nextScheduleChange": { + "start": "2024-07-01T05: 45: 00Z", + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 24.0, + "fahrenheit": 75.2 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON", + "horizontalSwing": "ON" + } + }, + "nextTimeBlock": { + "start": "2024-07-01T05: 45: 00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "runningOfflineSchedule": false, + "activityDataPoints": { + "acPower": { + "timestamp": "2022-07-13T18: 06: 58.183Z", + "type": "POWER", + "value": "ON" + } + }, + "sensorDataPoints": { + "insideTemperature": { + "celsius": 24.3, + "fahrenheit": 75.74, + "timestamp": "2024-06-28T22: 23: 15.679Z", + "type": "TEMPERATURE", + "precision": { + "celsius": 0.1, + "fahrenheit": 0.1 + } + }, + "humidity": { + "type": "PERCENTAGE", + "percentage": 70.9, + "timestamp": "2024-06-28T22: 23: 15.679Z" + } + }, + "terminationCondition": { + "type": "MANUAL" + } +} diff --git a/tests/components/tado/fixtures/zone_states.json b/tests/components/tado/fixtures/zone_states.json index 64d457f3b50..df1a99a80f3 100644 --- a/tests/components/tado/fixtures/zone_states.json +++ b/tests/components/tado/fixtures/zone_states.json @@ -287,6 +287,79 @@ "timestamp": "2020-03-28T02:09:27.830Z" } } + }, + "6": { + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "AIR_CONDITIONING", + "power": "OFF" + }, + "overlayType": "MANUAL", + "overlay": { + "type": "MANUAL", + "setting": { + "type": "AIR_CONDITIONING", + "power": "OFF" + }, + "termination": { + "type": "MANUAL", + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null + } + }, + "openWindow": null, + "nextScheduleChange": { + "start": "2024-07-01T05: 45: 00Z", + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 24.0, + "fahrenheit": 75.2 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON", + "horizontalSwing": "ON" + } + }, + "nextTimeBlock": { + "start": "2024-07-01T05: 45: 00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "runningOfflineSchedule": false, + "activityDataPoints": { + "acPower": { + "timestamp": "2022-07-13T18: 06: 58.183Z", + "type": "POWER", + "value": "OFF" + } + }, + "sensorDataPoints": { + "insideTemperature": { + "celsius": 24.21, + "fahrenheit": 75.58, + "timestamp": "2024-06-28T21: 43: 51.067Z", + "type": "TEMPERATURE", + "precision": { + "celsius": 0.1, + "fahrenheit": 0.1 + } + }, + "humidity": { + "type": "PERCENTAGE", + "percentage": 71.4, + "timestamp": "2024-06-28T21: 43: 51.067Z" + } + }, + "terminationCondition": { + "type": "MANUAL" + } } } } diff --git a/tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json b/tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json new file mode 100644 index 00000000000..51ba70b4065 --- /dev/null +++ b/tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json @@ -0,0 +1,130 @@ +{ + "type": "AIR_CONDITIONING", + "COOL": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "FAN": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "AUTO": { + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "HEAT": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "DRY": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "initialStates": { + "mode": "COOL", + "modes": { + "COOL": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "HEAT": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "DRY": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "FAN": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "AUTO": { + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + } + } + } +} diff --git a/tests/components/tado/fixtures/zones.json b/tests/components/tado/fixtures/zones.json index 5ef7374a660..e1d2ec759ba 100644 --- a/tests/components/tado/fixtures/zones.json +++ b/tests/components/tado/fixtures/zones.json @@ -178,5 +178,45 @@ "deviceTypes": ["WR02"], "reportAvailable": false, "type": "AIR_CONDITIONING" + }, + { + "id": 6, + "name": "Air Conditioning with fanlevel", + "type": "AIR_CONDITIONING", + "dateCreated": "2022-07-13T18: 06: 58.183Z", + "deviceTypes": ["WR02"], + "devices": [ + { + "deviceType": "WR02", + "serialNo": "WR5", + "shortSerialNo": "WR5", + "currentFwVersion": "118.7", + "connectionState": { + "value": true, + "timestamp": "2024-06-28T21: 04: 23.463Z" + }, + "characteristics": { + "capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"] + }, + "accessPointWiFi": { + "ssid": "tado8480" + }, + "commandTableUploadState": "FINISHED", + "duties": ["ZONE_UI", "ZONE_DRIVER", "ZONE_LEADER"] + } + ], + "reportAvailable": false, + "showScheduleSetup": false, + "supportsDazzle": true, + "dazzleEnabled": true, + "dazzleMode": { + "supported": true, + "enabled": true + }, + "openWindowDetection": { + "supported": true, + "enabled": true, + "timeoutInSeconds": 900 + } } ] diff --git a/tests/components/tado/test_climate.py b/tests/components/tado/test_climate.py index 98fd2d753a4..5a43c728b6e 100644 --- a/tests/components/tado/test_climate.py +++ b/tests/components/tado/test_climate.py @@ -89,3 +89,35 @@ async def test_smartac_with_swing(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all(item in state.attributes.items() for item in expected_attributes.items()) + + +async def test_smartac_with_fanlevel_vertical_and_horizontal_swing( + hass: HomeAssistant, +) -> None: + """Test creation of smart ac with swing climate.""" + + await async_init_integration(hass) + + state = hass.states.get("climate.air_conditioning_with_fanlevel") + assert state.state == "heat" + + expected_attributes = { + "current_humidity": 70.9, + "current_temperature": 24.3, + "fan_mode": "high", + "fan_modes": ["high", "medium", "auto", "low"], + "friendly_name": "Air Conditioning with fanlevel", + "hvac_action": "heating", + "hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"], + "max_temp": 31.0, + "min_temp": 16.0, + "preset_mode": "auto", + "preset_modes": ["away", "home", "auto"], + "swing_modes": ["vertical", "horizontal", "both", "off"], + "supported_features": 441, + "target_temp_step": 1.0, + "temperature": 25.0, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index dd7c108c984..de4fd515e5a 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -27,6 +27,12 @@ async def async_init_integration( # WR1 Device device_wr1_fixture = "tado/device_wr1.json" + # Smart AC with fanLevel, Vertical and Horizontal swings + zone_6_state_fixture = "tado/smartac4.with_fanlevel.json" + zone_6_capabilities_fixture = ( + "tado/zone_with_fanlevel_horizontal_vertical_swing.json" + ) + # Smart AC with Swing zone_5_state_fixture = "tado/smartac3.with_swing.json" zone_5_capabilities_fixture = "tado/zone_with_swing_capabilities.json" @@ -95,6 +101,10 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/zoneStates", text=load_fixture(zone_states_fixture), ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/6/capabilities", + text=load_fixture(zone_6_capabilities_fixture), + ) m.get( "https://my.tado.com/api/v2/homes/1/zones/5/capabilities", text=load_fixture(zone_5_capabilities_fixture), @@ -135,6 +145,14 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/zones/5/defaultOverlay", text=load_fixture(zone_def_overlay), ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/6/defaultOverlay", + text=load_fixture(zone_def_overlay), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/6/state", + text=load_fixture(zone_6_state_fixture), + ) m.get( "https://my.tado.com/api/v2/homes/1/zones/5/state", text=load_fixture(zone_5_state_fixture), From 38a30b343dee78b3715ad8dab28a982ea367be3a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 30 Jun 2024 15:28:01 +0200 Subject: [PATCH 0505/2411] Bump pizzapi to 0.0.6 (#120691) --- homeassistant/components/dominos/__init__.py | 14 +++++++------- homeassistant/components/dominos/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py index ce7b36f2280..9b11b667e84 100644 --- a/homeassistant/components/dominos/__init__.py +++ b/homeassistant/components/dominos/__init__.py @@ -4,11 +4,11 @@ from datetime import timedelta import logging from pizzapi import Address, Customer, Order -from pizzapi.address import StoreException import voluptuous as vol from homeassistant.components import http from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -118,7 +118,7 @@ class Dominos: self.country = conf.get(ATTR_COUNTRY) try: self.closest_store = self.address.closest_store() - except StoreException: + except Exception: # noqa: BLE001 self.closest_store = None def handle_order(self, call: ServiceCall) -> None: @@ -139,7 +139,7 @@ class Dominos: """Update the shared closest store (if open).""" try: self.closest_store = self.address.closest_store() - except StoreException: + except Exception: # noqa: BLE001 self.closest_store = None return False return True @@ -219,7 +219,7 @@ class DominosOrder(Entity): """Update the order state and refreshes the store.""" try: self.dominos.update_closest_store() - except StoreException: + except Exception: # noqa: BLE001 self._orderable = False return @@ -227,13 +227,13 @@ class DominosOrder(Entity): order = self.order() order.pay_with() self._orderable = True - except StoreException: + except Exception: # noqa: BLE001 self._orderable = False def order(self): """Create the order object.""" if self.dominos.closest_store is None: - raise StoreException + raise HomeAssistantError("No store available") order = Order( self.dominos.closest_store, @@ -252,7 +252,7 @@ class DominosOrder(Entity): try: order = self.order() order.place() - except StoreException: + except Exception: # noqa: BLE001 self._orderable = False _LOGGER.warning( "Attempted to order Dominos - Order invalid or store closed" diff --git a/homeassistant/components/dominos/manifest.json b/homeassistant/components/dominos/manifest.json index dfb8966013f..442f433db7c 100644 --- a/homeassistant/components/dominos/manifest.json +++ b/homeassistant/components/dominos/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/dominos", "iot_class": "cloud_polling", "loggers": ["pizzapi"], - "requirements": ["pizzapi==0.0.3"] + "requirements": ["pizzapi==0.0.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index ba3e2f5fadb..d44ea880636 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1557,7 +1557,7 @@ pigpio==1.78 pilight==0.1.1 # homeassistant.components.dominos -pizzapi==0.0.3 +pizzapi==0.0.6 # homeassistant.components.plex plexauth==0.0.6 From a7246400b3db10a4daa24c6c989368f187f7506c Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 30 Jun 2024 09:30:52 -0400 Subject: [PATCH 0506/2411] Allow EM heat on from any mode in Honeywell (#120750) --- homeassistant/components/honeywell/switch.py | 13 ++++++------ tests/components/honeywell/test_switch.py | 21 +------------------- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/honeywell/switch.py b/homeassistant/components/honeywell/switch.py index 53a9b27ee72..b90dd339593 100644 --- a/homeassistant/components/honeywell/switch.py +++ b/homeassistant/components/honeywell/switch.py @@ -71,13 +71,12 @@ class HoneywellSwitch(SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on if heat mode is enabled.""" - if self._device.system_mode == "heat": - try: - await self._device.set_system_mode("emheat") - except SomeComfortError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, translation_key="switch_failed_on" - ) from err + try: + await self._device.set_system_mode("emheat") + except SomeComfortError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="switch_failed_on" + ) from err async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off if on.""" diff --git a/tests/components/honeywell/test_switch.py b/tests/components/honeywell/test_switch.py index 73052871ef1..482b9837b93 100644 --- a/tests/components/honeywell/test_switch.py +++ b/tests/components/honeywell/test_switch.py @@ -24,26 +24,6 @@ async def test_emheat_switch( await init_integration(hass, config_entry) entity_id = f"switch.{device.name}_emergency_heat" - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - device.set_system_mode.assert_not_called() - - device.set_system_mode.reset_mock() - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - device.set_system_mode.assert_not_called() - - device.system_mode = "heat" - await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -53,6 +33,7 @@ async def test_emheat_switch( device.set_system_mode.assert_called_once_with("emheat") device.set_system_mode.reset_mock() + device.system_mode = "emheat" await hass.services.async_call( SWITCH_DOMAIN, From f58eafe8fca3782cd59989a57a5aabfd8a276f8a Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sun, 30 Jun 2024 15:16:41 +0200 Subject: [PATCH 0507/2411] Fix routes with transfer in nmbs integration (#120808) --- homeassistant/components/nmbs/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 82fc6143b2d..6ccdc742430 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -261,7 +261,7 @@ class NMBSSensor(SensorEntity): attrs["via_arrival_platform"] = via["arrival"]["platform"] attrs["via_transfer_platform"] = via["departure"]["platform"] attrs["via_transfer_time"] = get_delay_in_minutes( - via["timeBetween"] + via["timebetween"] ) + get_delay_in_minutes(via["departure"]["delay"]) if delay > 0: From ad9e0ef8e49a17ec48c19d0ce1c436b9a442d607 Mon Sep 17 00:00:00 2001 From: Etienne Soufflet Date: Sun, 30 Jun 2024 20:38:35 +0200 Subject: [PATCH 0508/2411] Fix Tado fan mode (#120809) --- homeassistant/components/tado/climate.py | 17 ++- homeassistant/components/tado/helper.py | 12 ++ .../tado/fixtures/smartac4.with_fanlevel.json | 88 ++++++++++++ .../components/tado/fixtures/zone_states.json | 73 ++++++++++ ...th_fanlevel_horizontal_vertical_swing.json | 130 ++++++++++++++++++ tests/components/tado/fixtures/zones.json | 40 ++++++ tests/components/tado/test_climate.py | 32 +++++ tests/components/tado/util.py | 18 +++ 8 files changed, 401 insertions(+), 9 deletions(-) create mode 100644 tests/components/tado/fixtures/smartac4.with_fanlevel.json create mode 100644 tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 40bdb19b31b..116985796d5 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -73,7 +73,7 @@ from .const import ( TYPE_HEATING, ) from .entity import TadoZoneEntity -from .helper import decide_duration, decide_overlay_mode +from .helper import decide_duration, decide_overlay_mode, generate_supported_fanmodes _LOGGER = logging.getLogger(__name__) @@ -200,15 +200,14 @@ def create_climate_entity( continue if capabilities[mode].get("fanSpeeds"): - supported_fan_modes = [ - TADO_TO_HA_FAN_MODE_MAP_LEGACY[speed] - for speed in capabilities[mode]["fanSpeeds"] - ] + supported_fan_modes = generate_supported_fanmodes( + TADO_TO_HA_FAN_MODE_MAP_LEGACY, capabilities[mode]["fanSpeeds"] + ) + else: - supported_fan_modes = [ - TADO_TO_HA_FAN_MODE_MAP[level] - for level in capabilities[mode]["fanLevel"] - ] + supported_fan_modes = generate_supported_fanmodes( + TADO_TO_HA_FAN_MODE_MAP, capabilities[mode]["fanLevel"] + ) cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"] else: diff --git a/homeassistant/components/tado/helper.py b/homeassistant/components/tado/helper.py index efcd3e7c4ea..81bff1e36c3 100644 --- a/homeassistant/components/tado/helper.py +++ b/homeassistant/components/tado/helper.py @@ -49,3 +49,15 @@ def decide_duration( ) return duration + + +def generate_supported_fanmodes(tado_to_ha_mapping: dict[str, str], options: list[str]): + """Return correct list of fan modes or None.""" + supported_fanmodes = [ + tado_to_ha_mapping.get(option) + for option in options + if tado_to_ha_mapping.get(option) is not None + ] + if not supported_fanmodes: + return None + return supported_fanmodes diff --git a/tests/components/tado/fixtures/smartac4.with_fanlevel.json b/tests/components/tado/fixtures/smartac4.with_fanlevel.json new file mode 100644 index 00000000000..ea1f9cbd8e5 --- /dev/null +++ b/tests/components/tado/fixtures/smartac4.with_fanlevel.json @@ -0,0 +1,88 @@ +{ + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 25.0, + "fahrenheit": 77.0 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON", + "horizontalSwing": "ON" + }, + "overlayType": "MANUAL", + "overlay": { + "type": "MANUAL", + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 25.0, + "fahrenheit": 77.0 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON" + }, + "termination": { + "type": "MANUAL", + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null + } + }, + "openWindow": null, + "nextScheduleChange": { + "start": "2024-07-01T05: 45: 00Z", + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 24.0, + "fahrenheit": 75.2 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON", + "horizontalSwing": "ON" + } + }, + "nextTimeBlock": { + "start": "2024-07-01T05: 45: 00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "runningOfflineSchedule": false, + "activityDataPoints": { + "acPower": { + "timestamp": "2022-07-13T18: 06: 58.183Z", + "type": "POWER", + "value": "ON" + } + }, + "sensorDataPoints": { + "insideTemperature": { + "celsius": 24.3, + "fahrenheit": 75.74, + "timestamp": "2024-06-28T22: 23: 15.679Z", + "type": "TEMPERATURE", + "precision": { + "celsius": 0.1, + "fahrenheit": 0.1 + } + }, + "humidity": { + "type": "PERCENTAGE", + "percentage": 70.9, + "timestamp": "2024-06-28T22: 23: 15.679Z" + } + }, + "terminationCondition": { + "type": "MANUAL" + } +} diff --git a/tests/components/tado/fixtures/zone_states.json b/tests/components/tado/fixtures/zone_states.json index 64d457f3b50..df1a99a80f3 100644 --- a/tests/components/tado/fixtures/zone_states.json +++ b/tests/components/tado/fixtures/zone_states.json @@ -287,6 +287,79 @@ "timestamp": "2020-03-28T02:09:27.830Z" } } + }, + "6": { + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "AIR_CONDITIONING", + "power": "OFF" + }, + "overlayType": "MANUAL", + "overlay": { + "type": "MANUAL", + "setting": { + "type": "AIR_CONDITIONING", + "power": "OFF" + }, + "termination": { + "type": "MANUAL", + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null + } + }, + "openWindow": null, + "nextScheduleChange": { + "start": "2024-07-01T05: 45: 00Z", + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 24.0, + "fahrenheit": 75.2 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "ON", + "horizontalSwing": "ON" + } + }, + "nextTimeBlock": { + "start": "2024-07-01T05: 45: 00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "runningOfflineSchedule": false, + "activityDataPoints": { + "acPower": { + "timestamp": "2022-07-13T18: 06: 58.183Z", + "type": "POWER", + "value": "OFF" + } + }, + "sensorDataPoints": { + "insideTemperature": { + "celsius": 24.21, + "fahrenheit": 75.58, + "timestamp": "2024-06-28T21: 43: 51.067Z", + "type": "TEMPERATURE", + "precision": { + "celsius": 0.1, + "fahrenheit": 0.1 + } + }, + "humidity": { + "type": "PERCENTAGE", + "percentage": 71.4, + "timestamp": "2024-06-28T21: 43: 51.067Z" + } + }, + "terminationCondition": { + "type": "MANUAL" + } } } } diff --git a/tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json b/tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json new file mode 100644 index 00000000000..51ba70b4065 --- /dev/null +++ b/tests/components/tado/fixtures/zone_with_fanlevel_horizontal_vertical_swing.json @@ -0,0 +1,130 @@ +{ + "type": "AIR_CONDITIONING", + "COOL": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "FAN": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "AUTO": { + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "HEAT": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "fanLevel": ["LEVEL3", "LEVEL2", "AUTO", "LEVEL1", "LEVEL4", "LEVEL5"], + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "DRY": { + "temperatures": { + "celsius": { + "min": 16, + "max": 31, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 88, + "step": 1.0 + } + }, + "verticalSwing": ["MID_UP", "MID_DOWN", "ON", "OFF", "UP", "MID", "DOWN"], + "horizontalSwing": ["OFF", "ON"], + "light": ["ON", "OFF"] + }, + "initialStates": { + "mode": "COOL", + "modes": { + "COOL": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "HEAT": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "DRY": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "FAN": { + "temperature": { + "celsius": 24, + "fahrenheit": 75 + }, + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + }, + "AUTO": { + "fanLevel": "LEVEL3", + "verticalSwing": "OFF", + "horizontalSwing": "OFF", + "light": "ON" + } + } + } +} diff --git a/tests/components/tado/fixtures/zones.json b/tests/components/tado/fixtures/zones.json index 5ef7374a660..e1d2ec759ba 100644 --- a/tests/components/tado/fixtures/zones.json +++ b/tests/components/tado/fixtures/zones.json @@ -178,5 +178,45 @@ "deviceTypes": ["WR02"], "reportAvailable": false, "type": "AIR_CONDITIONING" + }, + { + "id": 6, + "name": "Air Conditioning with fanlevel", + "type": "AIR_CONDITIONING", + "dateCreated": "2022-07-13T18: 06: 58.183Z", + "deviceTypes": ["WR02"], + "devices": [ + { + "deviceType": "WR02", + "serialNo": "WR5", + "shortSerialNo": "WR5", + "currentFwVersion": "118.7", + "connectionState": { + "value": true, + "timestamp": "2024-06-28T21: 04: 23.463Z" + }, + "characteristics": { + "capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"] + }, + "accessPointWiFi": { + "ssid": "tado8480" + }, + "commandTableUploadState": "FINISHED", + "duties": ["ZONE_UI", "ZONE_DRIVER", "ZONE_LEADER"] + } + ], + "reportAvailable": false, + "showScheduleSetup": false, + "supportsDazzle": true, + "dazzleEnabled": true, + "dazzleMode": { + "supported": true, + "enabled": true + }, + "openWindowDetection": { + "supported": true, + "enabled": true, + "timeoutInSeconds": 900 + } } ] diff --git a/tests/components/tado/test_climate.py b/tests/components/tado/test_climate.py index 98fd2d753a4..5a43c728b6e 100644 --- a/tests/components/tado/test_climate.py +++ b/tests/components/tado/test_climate.py @@ -89,3 +89,35 @@ async def test_smartac_with_swing(hass: HomeAssistant) -> None: # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all(item in state.attributes.items() for item in expected_attributes.items()) + + +async def test_smartac_with_fanlevel_vertical_and_horizontal_swing( + hass: HomeAssistant, +) -> None: + """Test creation of smart ac with swing climate.""" + + await async_init_integration(hass) + + state = hass.states.get("climate.air_conditioning_with_fanlevel") + assert state.state == "heat" + + expected_attributes = { + "current_humidity": 70.9, + "current_temperature": 24.3, + "fan_mode": "high", + "fan_modes": ["high", "medium", "auto", "low"], + "friendly_name": "Air Conditioning with fanlevel", + "hvac_action": "heating", + "hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"], + "max_temp": 31.0, + "min_temp": 16.0, + "preset_mode": "auto", + "preset_modes": ["away", "home", "auto"], + "swing_modes": ["vertical", "horizontal", "both", "off"], + "supported_features": 441, + "target_temp_step": 1.0, + "temperature": 25.0, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index dd7c108c984..de4fd515e5a 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -27,6 +27,12 @@ async def async_init_integration( # WR1 Device device_wr1_fixture = "tado/device_wr1.json" + # Smart AC with fanLevel, Vertical and Horizontal swings + zone_6_state_fixture = "tado/smartac4.with_fanlevel.json" + zone_6_capabilities_fixture = ( + "tado/zone_with_fanlevel_horizontal_vertical_swing.json" + ) + # Smart AC with Swing zone_5_state_fixture = "tado/smartac3.with_swing.json" zone_5_capabilities_fixture = "tado/zone_with_swing_capabilities.json" @@ -95,6 +101,10 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/zoneStates", text=load_fixture(zone_states_fixture), ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/6/capabilities", + text=load_fixture(zone_6_capabilities_fixture), + ) m.get( "https://my.tado.com/api/v2/homes/1/zones/5/capabilities", text=load_fixture(zone_5_capabilities_fixture), @@ -135,6 +145,14 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/zones/5/defaultOverlay", text=load_fixture(zone_def_overlay), ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/6/defaultOverlay", + text=load_fixture(zone_def_overlay), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/6/state", + text=load_fixture(zone_6_state_fixture), + ) m.get( "https://my.tado.com/api/v2/homes/1/zones/5/state", text=load_fixture(zone_5_state_fixture), From becf9fcce205f0c0e171c3f276f53d5f1e81d556 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 29 Jun 2024 22:07:37 +0300 Subject: [PATCH 0509/2411] Bump aiowebostv to 0.4.1 (#120838) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index ed8e1a6cc6e..bcafb82a4b0 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_push", "loggers": ["aiowebostv"], "quality_scale": "platinum", - "requirements": ["aiowebostv==0.4.0"], + "requirements": ["aiowebostv==0.4.1"], "ssdp": [ { "st": "urn:lge-com:service:webos-second-screen:1" diff --git a/requirements_all.txt b/requirements_all.txt index d44ea880636..d15c2d13ae0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.4.0 +aiowebostv==0.4.1 # homeassistant.components.withings aiowithings==3.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0a6fb54b3a..20a9b828069 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -377,7 +377,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.4.0 +aiowebostv==0.4.1 # homeassistant.components.withings aiowithings==3.0.2 From bcec268c047e30b7fa634070c9f112596679e39f Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Sun, 30 Jun 2024 01:08:24 +0300 Subject: [PATCH 0510/2411] Fix Jewish calendar unique id move to entity (#120842) --- homeassistant/components/jewish_calendar/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/jewish_calendar/entity.py b/homeassistant/components/jewish_calendar/entity.py index aba76599f63..c11925df954 100644 --- a/homeassistant/components/jewish_calendar/entity.py +++ b/homeassistant/components/jewish_calendar/entity.py @@ -28,7 +28,7 @@ class JewishCalendarEntity(Entity): ) -> None: """Initialize a Jewish Calendar entity.""" self.entity_description = description - self._attr_unique_id = f"{config_entry.entry_id}_{description.key}" + self._attr_unique_id = f"{config_entry.entry_id}-{description.key}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, config_entry.entry_id)}, From 4fc89e886198c599007807d8989acfbf9272a1ed Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 29 Jun 2024 21:35:48 -0700 Subject: [PATCH 0511/2411] Rollback PyFlume to 0.6.5 (#120846) --- homeassistant/components/flume/coordinator.py | 2 +- homeassistant/components/flume/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flume/coordinator.py b/homeassistant/components/flume/coordinator.py index c75bffdc615..30e7962304c 100644 --- a/homeassistant/components/flume/coordinator.py +++ b/homeassistant/components/flume/coordinator.py @@ -98,7 +98,7 @@ class FlumeNotificationDataUpdateCoordinator(DataUpdateCoordinator[None]): # The related binary sensors (leak detected, high flow, low battery) # will be active until the notification is deleted in the Flume app. self.notifications = pyflume.FlumeNotificationList( - self.auth, read=None, sort_direction="DESC" + self.auth, read=None ).notification_list _LOGGER.debug("Notifications %s", self.notifications) diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index bb6783bafbe..953d9791f2f 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/flume", "iot_class": "cloud_polling", "loggers": ["pyflume"], - "requirements": ["PyFlume==0.8.7"] + "requirements": ["PyFlume==0.6.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index d15c2d13ae0..d7a7ecb6eb5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,7 +54,7 @@ PyChromecast==14.0.1 PyFlick==0.0.2 # homeassistant.components.flume -PyFlume==0.8.7 +PyFlume==0.6.5 # homeassistant.components.fronius PyFronius==0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20a9b828069..507b40f8e47 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,7 +45,7 @@ PyChromecast==14.0.1 PyFlick==0.0.2 # homeassistant.components.flume -PyFlume==0.8.7 +PyFlume==0.6.5 # homeassistant.components.fronius PyFronius==0.7.3 From af733425c2d85d3ae559ee3f272cca7bdbddfd1a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 30 Jun 2024 14:56:12 +0200 Subject: [PATCH 0512/2411] Bump pyfritzhome to 0.6.12 (#120861) --- homeassistant/components/fritzbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index de2e9e0200a..3735c16571e 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["pyfritzhome"], "quality_scale": "gold", - "requirements": ["pyfritzhome==0.6.11"], + "requirements": ["pyfritzhome==0.6.12"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/requirements_all.txt b/requirements_all.txt index d7a7ecb6eb5..dd68902baae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1866,7 +1866,7 @@ pyforked-daapd==0.1.14 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.11 +pyfritzhome==0.6.12 # homeassistant.components.ifttt pyfttt==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 507b40f8e47..54e86d60186 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1465,7 +1465,7 @@ pyforked-daapd==0.1.14 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.11 +pyfritzhome==0.6.12 # homeassistant.components.ifttt pyfttt==0.3 From 14af3661f3fe68332bbdee929c1b6586264f384d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 30 Jun 2024 20:42:10 +0200 Subject: [PATCH 0513/2411] Bump version to 2024.7.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 fa19aa7349e..e97f14f830c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 3b42dfa2d6b..0f4b25eb0cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b5" +version = "2024.7.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From eceecbb07b9db5221d02ce520823b98abd227047 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 1 Jul 2024 00:23:42 +0200 Subject: [PATCH 0514/2411] Bump here-routing to 1.0.1 (#120877) --- homeassistant/components/here_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 19c5c4d73d9..2d6621c7c61 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "iot_class": "cloud_polling", "loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"], - "requirements": ["here-routing==0.2.0", "here-transit==1.2.0"] + "requirements": ["here-routing==1.0.1", "here-transit==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index cc610e2929e..a43da9bbb46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1065,7 +1065,7 @@ hdate==0.10.9 heatmiserV3==1.1.18 # homeassistant.components.here_travel_time -here-routing==0.2.0 +here-routing==1.0.1 # homeassistant.components.here_travel_time here-transit==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8996caf7f35..c9c3baf2dbb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hassil==1.7.1 hdate==0.10.9 # homeassistant.components.here_travel_time -here-routing==0.2.0 +here-routing==1.0.1 # homeassistant.components.here_travel_time here-transit==1.2.0 From 6af952731033e5ff1cc132c93b8ec407dec900c9 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Mon, 1 Jul 2024 01:12:33 +0200 Subject: [PATCH 0515/2411] Bump aioautomower to 2024.6.4 (#120875) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 7883b057a3f..f27b04ef0c0 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.6.3"] + "requirements": ["aioautomower==2024.6.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index a43da9bbb46..9183318d1ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -204,7 +204,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.3 +aioautomower==2024.6.4 # homeassistant.components.azure_devops aioazuredevops==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9c3baf2dbb..3babc49e962 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -183,7 +183,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.3 +aioautomower==2024.6.4 # homeassistant.components.azure_devops aioazuredevops==2.1.1 From 269b8b07c46e29153aaef66dc70b2578f1cb550a Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Mon, 1 Jul 2024 01:30:08 -0400 Subject: [PATCH 0516/2411] Add handling for different STATFLAG formats in APCUPSD (#120870) * Add handling for different STATFLAG formats * Just use removesuffix --- .../components/apcupsd/binary_sensor.py | 6 +++++- .../components/apcupsd/test_binary_sensor.py | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 77b2b8591e5..5f86ceb6eec 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -68,4 +68,8 @@ class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity): """Returns true if the UPS is online.""" # Check if ONLINE bit is set in STATFLAG. key = self.entity_description.key.upper() - return int(self.coordinator.data[key], 16) & _VALUE_ONLINE_MASK != 0 + # The daemon could either report just a hex ("0x05000008"), or a hex with a "Status Flag" + # suffix ("0x05000008 Status Flag") in older versions. + # Here we trim the suffix if it exists to support both. + flag = self.coordinator.data[key].removesuffix(" Status Flag") + return int(flag, 16) & _VALUE_ONLINE_MASK != 0 diff --git a/tests/components/apcupsd/test_binary_sensor.py b/tests/components/apcupsd/test_binary_sensor.py index 7616a960b21..02351109603 100644 --- a/tests/components/apcupsd/test_binary_sensor.py +++ b/tests/components/apcupsd/test_binary_sensor.py @@ -1,5 +1,7 @@ """Test binary sensors of APCUPSd integration.""" +import pytest + from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import slugify @@ -31,3 +33,22 @@ async def test_no_binary_sensor(hass: HomeAssistant) -> None: device_slug = slugify(MOCK_STATUS["UPSNAME"]) state = hass.states.get(f"binary_sensor.{device_slug}_online_status") assert state is None + + +@pytest.mark.parametrize( + ("override", "expected"), + [ + ("0x008", "on"), + ("0x02040010 Status Flag", "off"), + ], +) +async def test_statflag(hass: HomeAssistant, override: str, expected: str) -> None: + """Test binary sensor for different STATFLAG values.""" + status = MOCK_STATUS.copy() + status["STATFLAG"] = override + await async_init_integration(hass, status=status) + + device_slug = slugify(MOCK_STATUS["UPSNAME"]) + assert ( + hass.states.get(f"binary_sensor.{device_slug}_online_status").state == expected + ) From 74c2f000d881ab351540975d16ed8eb77f8f4a44 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Mon, 1 Jul 2024 02:44:59 -0300 Subject: [PATCH 0517/2411] Add missing translations for device class in SQL (#120892) --- homeassistant/components/sql/strings.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sql/strings.json b/homeassistant/components/sql/strings.json index 361585b8876..cd36ccf7731 100644 --- a/homeassistant/components/sql/strings.json +++ b/homeassistant/components/sql/strings.json @@ -71,18 +71,19 @@ "selector": { "device_class": { "options": { - "date": "[%key:component::sensor::entity_component::date::name%]", - "duration": "[%key:component::sensor::entity_component::duration::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", "battery": "[%key:component::sensor::entity_component::battery::name%]", - "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", + "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", "current": "[%key:component::sensor::entity_component::current::name%]", "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", "data_size": "[%key:component::sensor::entity_component::data_size::name%]", + "date": "[%key:component::sensor::entity_component::date::name%]", "distance": "[%key:component::sensor::entity_component::distance::name%]", + "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", @@ -100,8 +101,8 @@ "pm1": "[%key:component::sensor::entity_component::pm1::name%]", "pm10": "[%key:component::sensor::entity_component::pm10::name%]", "pm25": "[%key:component::sensor::entity_component::pm25::name%]", - "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "power": "[%key:component::sensor::entity_component::power::name%]", + "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", "pressure": "[%key:component::sensor::entity_component::pressure::name%]", @@ -116,6 +117,7 @@ "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", + "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", From 295cfd26aa809d049947300766787c3107393696 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Mon, 1 Jul 2024 02:55:13 -0300 Subject: [PATCH 0518/2411] Add missing translations for device class in Template (#120893) --- homeassistant/components/template/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 4a1377cbf0b..dc481b76ff8 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -105,6 +105,7 @@ "battery": "[%key:component::sensor::entity_component::battery::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", "current": "[%key:component::sensor::entity_component::current::name%]", "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", "data_size": "[%key:component::sensor::entity_component::data_size::name%]", From d5d77db4f9ef30a94f3175911c8a666261214530 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 08:47:52 +0200 Subject: [PATCH 0519/2411] Bump github/codeql-action from 3.25.10 to 3.25.11 (#120899) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 641f349408a..ef360b2124b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.1.7 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.25.10 + uses: github/codeql-action/init@v3.25.11 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.25.10 + uses: github/codeql-action/analyze@v3.25.11 with: category: "/language:python" From 88583149ea3bf0b8f10d37d56604a0eb84b2e7bd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 10:33:31 +0200 Subject: [PATCH 0520/2411] Use service_calls fixture in deconz tests (#120905) --- tests/components/deconz/test_device_trigger.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 54b735ba021..5f17da89a4b 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -34,7 +34,7 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.common import async_get_device_automations, async_mock_service +from tests.common import async_get_device_automations from tests.test_util.aiohttp import AiohttpClientMocker @@ -43,12 +43,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def automation_calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track automation calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -300,7 +294,7 @@ async def test_functional_device_trigger( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket, - automation_calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, ) -> None: """Test proper matching and attachment of device trigger automation.""" @@ -369,8 +363,8 @@ async def test_functional_device_trigger( await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() - assert len(automation_calls) == 1 - assert automation_calls[0].data["some"] == "test_trigger_button_press" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test_trigger_button_press" @pytest.mark.skip(reason="Temporarily disabled until automation validation is improved") From aa5ebaf613b5fe998dffd0e9a9c6359ef663934e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 1 Jul 2024 01:46:10 -0700 Subject: [PATCH 0521/2411] Bump ical to 8.1.1 (#120888) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 5fc28d2f398..d40daa89b0e 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.0.1"] + "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.1.1"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 73619b6bfe9..95c65089c79 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==8.0.1"] + "requirements": ["ical==8.1.1"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 4fa8e2982f9..313315a34f6 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==8.0.1"] + "requirements": ["ical==8.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9183318d1ec..75f24dbf6aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1131,7 +1131,7 @@ ibmiotf==0.3.4 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==8.0.1 +ical==8.1.1 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3babc49e962..22a130f5082 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -927,7 +927,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==8.0.1 +ical==8.1.1 # homeassistant.components.ping icmplib==3.0 From ca55986057ce0eaa0a5bff5ec6f16b3cd5b453c0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:51:51 +0200 Subject: [PATCH 0522/2411] Import Generator from collections.abc (1) (#120914) --- homeassistant/components/alexa/capabilities.py | 3 +-- homeassistant/components/alexa/entities.py | 4 +--- homeassistant/components/assist_pipeline/pipeline.py | 3 +-- .../components/assist_pipeline/websocket_api.py | 3 +-- homeassistant/components/automation/trace.py | 3 +-- .../bluetooth/passive_update_coordinator.py | 4 +--- homeassistant/components/homekit/aidmanager.py | 2 +- .../components/homekit_controller/device_trigger.py | 3 +-- homeassistant/components/knx/config_flow.py | 2 +- homeassistant/components/logbook/processor.py | 3 +-- homeassistant/components/matter/discovery.py | 3 ++- homeassistant/components/mqtt/client.py | 3 +-- homeassistant/components/profiler/__init__.py | 2 +- homeassistant/components/recorder/util.py | 3 +-- homeassistant/components/stream/fmp4utils.py | 3 +-- homeassistant/components/stream/worker.py | 3 +-- homeassistant/components/unifiprotect/camera.py | 2 +- homeassistant/components/unifiprotect/data.py | 3 +-- homeassistant/components/unifiprotect/utils.py | 3 +-- homeassistant/components/wemo/entity.py | 2 +- homeassistant/components/wyoming/satellite.py | 2 +- homeassistant/components/zwave_js/discovery.py | 2 +- homeassistant/components/zwave_js/services.py | 3 +-- homeassistant/config_entries.py | 12 ++++++++++-- homeassistant/exceptions.py | 4 +--- homeassistant/helpers/condition.py | 3 +-- homeassistant/helpers/script.py | 3 +-- homeassistant/helpers/template.py | 3 +-- homeassistant/helpers/trace.py | 4 +--- homeassistant/helpers/update_coordinator.py | 4 ++-- homeassistant/setup.py | 4 +--- .../scaffold/templates/config_flow/tests/conftest.py | 2 +- .../templates/config_flow_helper/tests/conftest.py | 2 +- tests/common.py | 10 ++++++++-- tests/components/conftest.py | 3 +-- tests/conftest.py | 3 +-- tests/helpers/test_config_entry_flow.py | 2 +- tests/helpers/test_config_entry_oauth2_flow.py | 2 +- tests/helpers/test_discovery_flow.py | 2 +- tests/scripts/test_auth.py | 2 +- tests/test_bootstrap.py | 3 +-- tests/test_config.py | 2 +- tests/test_config_entries.py | 2 +- tests/util/yaml/test_init.py | 2 +- 44 files changed, 62 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 047e981ab0d..44dfae33e18 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -2,11 +2,10 @@ from __future__ import annotations +from collections.abc import Generator import logging from typing import Any -from typing_extensions import Generator - from homeassistant.components import ( button, climate, diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 8d45ac3a11b..d3e9f2a8e7d 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -2,12 +2,10 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Generator, Iterable import logging from typing import TYPE_CHECKING, Any -from typing_extensions import Generator - from homeassistant.components import ( alarm_control_panel, alert, diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 068afe53b49..339417c253a 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -5,7 +5,7 @@ from __future__ import annotations import array import asyncio from collections import defaultdict, deque -from collections.abc import AsyncIterable, Callable, Iterable +from collections.abc import AsyncGenerator, AsyncIterable, Callable, Iterable from dataclasses import asdict, dataclass, field from enum import StrEnum import logging @@ -16,7 +16,6 @@ import time from typing import TYPE_CHECKING, Any, Final, Literal, cast import wave -from typing_extensions import AsyncGenerator import voluptuous as vol if TYPE_CHECKING: diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index 18464810525..7dea960d940 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -5,13 +5,12 @@ import asyncio # Suppressing disable=deprecated-module is needed for Python 3.11 import audioop # pylint: disable=deprecated-module import base64 -from collections.abc import Callable +from collections.abc import AsyncGenerator, Callable import contextlib import logging import math from typing import Any, Final -from typing_extensions import AsyncGenerator import voluptuous as vol from homeassistant.components import conversation, stt, tts, websocket_api diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index 08f42167ceb..ed30b0d348b 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -2,11 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from contextlib import contextmanager from typing import Any -from typing_extensions import Generator - from homeassistant.components.trace import ( CONF_STORED_TRACES, ActionTrace, diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 524faad510b..df06a7c534b 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -15,11 +15,9 @@ from homeassistant.helpers.update_coordinator import ( from .update_coordinator import BasePassiveBluetoothCoordinator if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import Callable, Generator import logging - from typing_extensions import Generator - from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak _PassiveBluetoothDataUpdateCoordinatorT = TypeVar( diff --git a/homeassistant/components/homekit/aidmanager.py b/homeassistant/components/homekit/aidmanager.py index 8049c4fd5e2..f755f6f901f 100644 --- a/homeassistant/components/homekit/aidmanager.py +++ b/homeassistant/components/homekit/aidmanager.py @@ -11,10 +11,10 @@ This module generates and stores them in a HA storage. from __future__ import annotations +from collections.abc import Generator import random from fnv_hash_fast import fnv1a_32 -from typing_extensions import Generator from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index 631ba43116a..6195e61af3f 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -2,14 +2,13 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Generator from typing import TYPE_CHECKING, Any from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.characteristics.const import InputEventValues from aiohomekit.model.services import Service, ServicesTypes from aiohomekit.utils import clamp_enum_to_char -from typing_extensions import Generator import voluptuous as vol from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 226abc1b868..7d6443bd9ef 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -3,9 +3,9 @@ from __future__ import annotations from abc import ABC, abstractmethod +from collections.abc import AsyncGenerator from typing import Any, Final -from typing_extensions import AsyncGenerator import voluptuous as vol from xknx import XKNX from xknx.exceptions.exception import ( diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 4e245189154..ed9888f83d0 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable, Sequence +from collections.abc import Callable, Generator, Sequence from contextlib import suppress from dataclasses import dataclass from datetime import datetime as dt @@ -11,7 +11,6 @@ from typing import Any from sqlalchemy.engine import Result from sqlalchemy.engine.row import Row -from typing_extensions import Generator from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.filters import Filters diff --git a/homeassistant/components/matter/discovery.py b/homeassistant/components/matter/discovery.py index b457be8583c..510b77c24c7 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -2,9 +2,10 @@ from __future__ import annotations +from collections.abc import Generator + from chip.clusters.Objects import ClusterAttributeDescriptor from matter_server.client.models.node import MatterEndpoint -from typing_extensions import Generator from homeassistant.const import Platform from homeassistant.core import callback diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index f65769badfa..2ebd105b432 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Callable, Coroutine, Iterable +from collections.abc import AsyncGenerator, Callable, Coroutine, Iterable import contextlib from dataclasses import dataclass from functools import lru_cache, partial @@ -18,7 +18,6 @@ from typing import TYPE_CHECKING, Any import uuid import certifi -from typing_extensions import AsyncGenerator from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index b9b833647df..9b2b9736574 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -1,6 +1,7 @@ """The profiler integration.""" import asyncio +from collections.abc import Generator import contextlib from contextlib import suppress from datetime import timedelta @@ -14,7 +15,6 @@ import traceback from typing import Any, cast from lru import LRU -from typing_extensions import Generator import voluptuous as vol from homeassistant.components import persistent_notification diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index b4ee90a8323..89621821ff8 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable, Sequence +from collections.abc import Callable, Generator, Sequence import contextlib from contextlib import contextmanager from datetime import date, datetime, timedelta @@ -25,7 +25,6 @@ from sqlalchemy.exc import OperationalError, SQLAlchemyError, StatementError from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.lambdas import StatementLambdaElement -from typing_extensions import Generator import voluptuous as vol from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index e0e3a8ba009..255d75e3b79 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -2,10 +2,9 @@ from __future__ import annotations +from collections.abc import Generator from typing import TYPE_CHECKING -from typing_extensions import Generator - from homeassistant.exceptions import HomeAssistantError from .core import Orientation diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 4fd9b27d02f..f51a3f98b01 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import defaultdict, deque -from collections.abc import Callable, Iterator, Mapping +from collections.abc import Callable, Generator, Iterator, Mapping import contextlib from dataclasses import fields import datetime @@ -13,7 +13,6 @@ from threading import Event from typing import Any, Self, cast import av -from typing_extensions import Generator from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 2a97aa26823..73cdb4a2c31 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Generator import logging -from typing_extensions import Generator from uiprotect.data import ( Camera as UFPCamera, CameraChannel, diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index a2ef72c7008..b8e47e0e0f1 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -3,13 +3,12 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Callable, Iterable +from collections.abc import Callable, Generator, Iterable from datetime import datetime, timedelta from functools import partial import logging from typing import TYPE_CHECKING, Any, cast -from typing_extensions import Generator from uiprotect import ProtectApiClient from uiprotect.data import ( NVR, diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index d98ad72e1d1..61314346d32 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -2,14 +2,13 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Generator, Iterable import contextlib from pathlib import Path import socket from typing import TYPE_CHECKING from aiohttp import CookieJar -from typing_extensions import Generator from uiprotect import ProtectApiClient from uiprotect.data import ( Bootstrap, diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index db64aa3137e..16ab3ae1173 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -2,11 +2,11 @@ from __future__ import annotations +from collections.abc import Generator import contextlib import logging from pywemo.exceptions import ActionException -from typing_extensions import Generator from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/wyoming/satellite.py b/homeassistant/components/wyoming/satellite.py index 5af0c54abad..3ca86a42e5d 100644 --- a/homeassistant/components/wyoming/satellite.py +++ b/homeassistant/components/wyoming/satellite.py @@ -1,6 +1,7 @@ """Support for Wyoming satellite services.""" import asyncio +from collections.abc import AsyncGenerator import io import logging import time @@ -8,7 +9,6 @@ from typing import Final from uuid import uuid4 import wave -from typing_extensions import AsyncGenerator from wyoming.asr import Transcribe, Transcript from wyoming.audio import AudioChunk, AudioChunkConverter, AudioStart, AudioStop from wyoming.client import AsyncTcpClient diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 0b66567c036..c3a2884cb7a 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -2,12 +2,12 @@ from __future__ import annotations +from collections.abc import Generator from dataclasses import asdict, dataclass, field from enum import StrEnum from typing import TYPE_CHECKING, Any, cast from awesomeversion import AwesomeVersion -from typing_extensions import Generator from zwave_js_server.const import ( CURRENT_STATE_PROPERTY, CURRENT_VALUE_PROPERTY, diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 66d09714723..e5c0bd64781 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -3,12 +3,11 @@ from __future__ import annotations import asyncio -from collections.abc import Collection, Sequence +from collections.abc import Collection, Generator, Sequence import logging import math from typing import Any -from typing_extensions import Generator import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import SET_VALUE_SUCCESS, CommandClass, CommandStatus diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c8d671e1fe1..bf3d8fa8f03 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -4,7 +4,15 @@ from __future__ import annotations import asyncio from collections import UserDict -from collections.abc import Callable, Coroutine, Hashable, Iterable, Mapping, ValuesView +from collections.abc import ( + Callable, + Coroutine, + Generator, + Hashable, + Iterable, + Mapping, + ValuesView, +) from contextvars import ContextVar from copy import deepcopy from enum import Enum, StrEnum @@ -16,7 +24,7 @@ from types import MappingProxyType from typing import TYPE_CHECKING, Any, Generic, Self, cast from async_interrupt import interrupt -from typing_extensions import Generator, TypeVar +from typing_extensions import TypeVar from . import data_entry_flow, loader from .components import persistent_notification diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 01e22d16e79..f308cbc5cd8 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -2,12 +2,10 @@ from __future__ import annotations -from collections.abc import Callable, Sequence +from collections.abc import Callable, Generator, Sequence from dataclasses import dataclass from typing import TYPE_CHECKING, Any -from typing_extensions import Generator - from .util.event_type import EventType if TYPE_CHECKING: diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index e15b40a78df..3438336dbfa 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import deque -from collections.abc import Callable, Container +from collections.abc import Callable, Container, Generator from contextlib import contextmanager from datetime import datetime, time as dt_time, timedelta import functools as ft @@ -12,7 +12,6 @@ import re import sys from typing import Any, Protocol, cast -from typing_extensions import Generator import voluptuous as vol from homeassistant.components import zone as zone_cmp diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 84dabb114cd..081641ef6c2 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Mapping, Sequence +from collections.abc import AsyncGenerator, Callable, Mapping, Sequence from contextlib import asynccontextmanager from contextvars import ContextVar from copy import copy @@ -16,7 +16,6 @@ from types import MappingProxyType from typing import Any, Literal, TypedDict, cast import async_interrupt -from typing_extensions import AsyncGenerator import voluptuous as vol from homeassistant import exceptions diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index cc619e25aed..9ab3f353dea 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -6,7 +6,7 @@ from ast import literal_eval import asyncio import base64 import collections.abc -from collections.abc import Callable, Iterable +from collections.abc import Callable, Generator, Iterable from contextlib import AbstractContextManager from contextvars import ContextVar from datetime import date, datetime, time, timedelta @@ -34,7 +34,6 @@ from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.utils import Namespace from lru import LRU import orjson -from typing_extensions import Generator import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index 6f29ff23bec..a36939a0f60 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -3,14 +3,12 @@ from __future__ import annotations from collections import deque -from collections.abc import Callable, Coroutine +from collections.abc import Callable, Coroutine, Generator from contextlib import contextmanager from contextvars import ContextVar from functools import wraps from typing import Any -from typing_extensions import Generator - from homeassistant.core import ServiceResponse import homeassistant.util.dt as dt_util diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 8451c69d2b3..c15dbb2d853 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import abstractmethod import asyncio -from collections.abc import Awaitable, Callable, Coroutine +from collections.abc import Awaitable, Callable, Coroutine, Generator from datetime import datetime, timedelta import logging from random import randint @@ -14,7 +14,7 @@ import urllib.error import aiohttp import requests -from typing_extensions import Generator, TypeVar +from typing_extensions import TypeVar from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 9775a3fee45..3b512e92686 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Awaitable, Callable, Mapping +from collections.abc import Awaitable, Callable, Generator, Mapping import contextlib import contextvars from enum import StrEnum @@ -14,8 +14,6 @@ import time from types import ModuleType from typing import Any, Final, TypedDict -from typing_extensions import Generator - from . import config as conf_util, core, loader, requirements from .const import ( BASE_PLATFORMS, # noqa: F401 diff --git a/script/scaffold/templates/config_flow/tests/conftest.py b/script/scaffold/templates/config_flow/tests/conftest.py index fc217636705..12faacd40df 100644 --- a/script/scaffold/templates/config_flow/tests/conftest.py +++ b/script/scaffold/templates/config_flow/tests/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the NEW_NAME tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/script/scaffold/templates/config_flow_helper/tests/conftest.py b/script/scaffold/templates/config_flow_helper/tests/conftest.py index fc217636705..12faacd40df 100644 --- a/script/scaffold/templates/config_flow_helper/tests/conftest.py +++ b/script/scaffold/templates/config_flow_helper/tests/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the NEW_NAME tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/common.py b/tests/common.py index 52ea4861c81..40745a1df9e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -3,7 +3,14 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine, Mapping, Sequence +from collections.abc import ( + AsyncGenerator, + Callable, + Coroutine, + Generator, + Mapping, + Sequence, +) from contextlib import asynccontextmanager, contextmanager from datetime import UTC, datetime, timedelta from enum import Enum @@ -23,7 +30,6 @@ from unittest.mock import AsyncMock, Mock, patch from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401 import pytest from syrupy import SnapshotAssertion -from typing_extensions import AsyncGenerator, Generator import voluptuous as vol from homeassistant import auth, bootstrap, config_entries, loader diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 42746525a0d..1fe933dbe12 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -2,13 +2,12 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Generator from pathlib import Path from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant diff --git a/tests/conftest.py b/tests/conftest.py index 6f85a7da06e..f9b65c5f138 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine +from collections.abc import AsyncGenerator, Callable, Coroutine, Generator from contextlib import asynccontextmanager, contextmanager import datetime import functools @@ -34,7 +34,6 @@ import pytest import pytest_socket import requests_mock from syrupy.assertion import SnapshotAssertion -from typing_extensions import AsyncGenerator, Generator from homeassistant import block_async_io diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 6a198b7a297..498e57d45a4 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -1,9 +1,9 @@ """Tests for the Config Entry Flow helper.""" +from collections.abc import Generator from unittest.mock import Mock, PropertyMock, patch import pytest -from typing_extensions import Generator from homeassistant import config_entries, data_entry_flow, setup from homeassistant.config import async_process_ha_core_config diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 132a0b41707..23919f3a6a3 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -1,5 +1,6 @@ """Tests for the Somfy config flow.""" +from collections.abc import Generator from http import HTTPStatus import logging import time @@ -8,7 +9,6 @@ from unittest.mock import patch import aiohttp import pytest -from typing_extensions import Generator from homeassistant import config_entries, data_entry_flow, setup from homeassistant.core import HomeAssistant diff --git a/tests/helpers/test_discovery_flow.py b/tests/helpers/test_discovery_flow.py index 9c2249ac17f..c834f60e91e 100644 --- a/tests/helpers/test_discovery_flow.py +++ b/tests/helpers/test_discovery_flow.py @@ -1,9 +1,9 @@ """Test the discovery flow helper.""" +from collections.abc import Generator from unittest.mock import AsyncMock, call, patch import pytest -from typing_extensions import Generator from homeassistant import config_entries from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, CoreState, HomeAssistant diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index 19a9277a36a..002807f08a5 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -1,12 +1,12 @@ """Test the auth script to manage local users.""" from asyncio import AbstractEventLoop +from collections.abc import Generator import logging from typing import Any from unittest.mock import Mock, patch import pytest -from typing_extensions import Generator from homeassistant.auth.providers import homeassistant as hass_auth from homeassistant.core import HomeAssistant diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index ca864006852..7f3793e99e2 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1,7 +1,7 @@ """Test the bootstrapping.""" import asyncio -from collections.abc import Iterable +from collections.abc import Generator, Iterable import contextlib import glob import logging @@ -11,7 +11,6 @@ from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import Generator from homeassistant import bootstrap, loader, runner import homeassistant.config as config_util diff --git a/tests/test_config.py b/tests/test_config.py index 748255fb205..ae6cbb3ba5e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,6 +2,7 @@ import asyncio from collections import OrderedDict +from collections.abc import Generator import contextlib import copy import logging @@ -12,7 +13,6 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator import voluptuous as vol from voluptuous import Invalid, MultipleInvalid import yaml diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index cba7ad8f215..b1c3915f983 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections.abc import Generator from datetime import timedelta from functools import cached_property import logging @@ -12,7 +13,6 @@ from unittest.mock import ANY, AsyncMock, Mock, patch from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant import config_entries, data_entry_flow, loader from homeassistant.components import dhcp diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 6ea3f1437af..d94de23088b 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -1,5 +1,6 @@ """Test Home Assistant yaml loader.""" +from collections.abc import Generator import importlib import io import os @@ -9,7 +10,6 @@ import unittest from unittest.mock import Mock, patch import pytest -from typing_extensions import Generator import voluptuous as vol import yaml as pyyaml From f11b316dac5215d7257538b1f30ce4be7a6b4c0a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:54:42 +0200 Subject: [PATCH 0523/2411] Import Generator from collections.abc (4) (#120917) --- tests/components/qbittorrent/conftest.py | 2 +- tests/components/qnap/conftest.py | 2 +- tests/components/rabbitair/test_config_flow.py | 2 +- tests/components/radio_browser/conftest.py | 2 +- tests/components/rainbird/conftest.py | 2 +- tests/components/rainbird/test_config_flow.py | 2 +- tests/components/rainforest_raven/conftest.py | 2 +- tests/components/rainforest_raven/test_config_flow.py | 2 +- tests/components/rdw/conftest.py | 2 +- tests/components/recorder/conftest.py | 2 +- .../recorder/test_filters_with_entityfilter_schema_37.py | 2 +- tests/components/recorder/test_init.py | 2 +- tests/components/recorder/test_migration_from_schema_32.py | 2 +- tests/components/recorder/test_purge.py | 2 +- tests/components/recorder/test_purge_v32_schema.py | 2 +- tests/components/refoss/conftest.py | 2 +- tests/components/renault/conftest.py | 2 +- tests/components/renault/test_binary_sensor.py | 2 +- tests/components/renault/test_button.py | 2 +- tests/components/renault/test_device_tracker.py | 2 +- tests/components/renault/test_init.py | 2 +- tests/components/renault/test_select.py | 2 +- tests/components/renault/test_sensor.py | 2 +- tests/components/renault/test_services.py | 2 +- tests/components/reolink/conftest.py | 2 +- tests/components/ring/conftest.py | 2 +- tests/components/roku/conftest.py | 2 +- tests/components/rtsp_to_webrtc/conftest.py | 3 +-- tests/components/sabnzbd/conftest.py | 2 +- tests/components/samsungtv/conftest.py | 3 +-- tests/components/sanix/conftest.py | 2 +- tests/components/schlage/conftest.py | 2 +- tests/components/scrape/conftest.py | 2 +- tests/components/screenlogic/test_services.py | 2 +- tests/components/season/conftest.py | 2 +- tests/components/sensor/test_init.py | 2 +- tests/components/seventeentrack/conftest.py | 2 +- tests/components/sfr_box/conftest.py | 2 +- tests/components/sfr_box/test_binary_sensor.py | 2 +- tests/components/sfr_box/test_button.py | 2 +- tests/components/sfr_box/test_diagnostics.py | 2 +- tests/components/sfr_box/test_init.py | 2 +- tests/components/sfr_box/test_sensor.py | 2 +- tests/components/simplisafe/conftest.py | 2 +- tests/components/sleepiq/conftest.py | 2 +- tests/components/slimproto/conftest.py | 2 +- tests/components/snapcast/conftest.py | 2 +- tests/components/sonarr/conftest.py | 2 +- tests/components/srp_energy/conftest.py | 2 +- tests/components/stream/conftest.py | 2 +- tests/components/streamlabswater/conftest.py | 2 +- tests/components/stt/test_init.py | 3 +-- tests/components/suez_water/conftest.py | 2 +- tests/components/swiss_public_transport/conftest.py | 2 +- tests/components/switch_as_x/conftest.py | 2 +- tests/components/switchbot_cloud/conftest.py | 2 +- tests/components/switcher_kis/conftest.py | 2 +- tests/components/synology_dsm/conftest.py | 2 +- tests/components/systemmonitor/conftest.py | 2 +- tests/components/tailscale/conftest.py | 2 +- tests/components/tailwind/conftest.py | 2 +- tests/components/tami4/conftest.py | 2 +- tests/components/tankerkoenig/conftest.py | 2 +- tests/components/technove/conftest.py | 2 +- tests/components/tedee/conftest.py | 2 +- tests/components/time_date/conftest.py | 2 +- tests/components/todo/test_init.py | 2 +- tests/components/todoist/conftest.py | 2 +- tests/components/tplink/conftest.py | 2 +- tests/components/tplink_omada/conftest.py | 3 +-- tests/components/traccar_server/conftest.py | 2 +- tests/components/traccar_server/test_config_flow.py | 2 +- tests/components/traccar_server/test_diagnostics.py | 2 +- tests/components/tractive/conftest.py | 2 +- tests/components/tradfri/conftest.py | 3 +-- tests/components/tts/common.py | 2 +- tests/components/tts/conftest.py | 2 +- tests/components/tuya/conftest.py | 2 +- tests/components/twentemilieu/conftest.py | 2 +- tests/components/twitch/conftest.py | 2 +- tests/components/ukraine_alarm/test_config_flow.py | 2 +- tests/components/unifi/conftest.py | 3 +-- tests/components/update/test_init.py | 2 +- tests/components/uptime/conftest.py | 2 +- tests/components/v2c/conftest.py | 2 +- tests/components/vacuum/conftest.py | 3 ++- tests/components/valve/test_init.py | 3 ++- tests/components/velbus/conftest.py | 2 +- tests/components/velbus/test_config_flow.py | 2 +- tests/components/velux/conftest.py | 2 +- tests/components/verisure/conftest.py | 2 +- tests/components/vicare/conftest.py | 2 +- tests/components/vilfo/conftest.py | 2 +- tests/components/wake_on_lan/conftest.py | 2 +- tests/components/wake_word/test_init.py | 3 +-- tests/components/waqi/conftest.py | 2 +- tests/components/water_heater/conftest.py | 3 ++- tests/components/weather/conftest.py | 3 ++- tests/components/weatherflow/conftest.py | 2 +- tests/components/weatherflow_cloud/conftest.py | 2 +- tests/components/weatherkit/conftest.py | 2 +- tests/components/webmin/conftest.py | 2 +- tests/components/webostv/conftest.py | 2 +- tests/components/whois/conftest.py | 2 +- tests/components/wiffi/conftest.py | 2 +- tests/components/wled/conftest.py | 2 +- tests/components/workday/conftest.py | 2 +- tests/components/wyoming/conftest.py | 2 +- tests/components/xiaomi_ble/conftest.py | 2 +- tests/components/xiaomi_miio/test_vacuum.py | 2 +- tests/components/yardian/conftest.py | 2 +- tests/components/zamg/conftest.py | 2 +- tests/components/zha/conftest.py | 3 +-- tests/components/zha/test_radio_manager.py | 2 +- tests/components/zha/test_registries.py | 2 +- tests/components/zwave_js/test_config_flow.py | 2 +- 116 files changed, 120 insertions(+), 124 deletions(-) diff --git a/tests/components/qbittorrent/conftest.py b/tests/components/qbittorrent/conftest.py index b15e2a6865b..17fb8e15b47 100644 --- a/tests/components/qbittorrent/conftest.py +++ b/tests/components/qbittorrent/conftest.py @@ -1,10 +1,10 @@ """Fixtures for testing qBittorrent component.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest import requests_mock -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/qnap/conftest.py b/tests/components/qnap/conftest.py index c0947318f60..2625f1805b6 100644 --- a/tests/components/qnap/conftest.py +++ b/tests/components/qnap/conftest.py @@ -1,9 +1,9 @@ """Setup the QNAP tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator TEST_HOST = "1.2.3.4" TEST_USERNAME = "admin" diff --git a/tests/components/rabbitair/test_config_flow.py b/tests/components/rabbitair/test_config_flow.py index 2e0cfba38c0..7f9479339a5 100644 --- a/tests/components/rabbitair/test_config_flow.py +++ b/tests/components/rabbitair/test_config_flow.py @@ -2,12 +2,12 @@ from __future__ import annotations +from collections.abc import Generator from ipaddress import ip_address from unittest.mock import MagicMock, Mock, patch import pytest from rabbitair import Mode, Model, Speed -from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components import zeroconf diff --git a/tests/components/radio_browser/conftest.py b/tests/components/radio_browser/conftest.py index 95fda545a6c..fc666b32c53 100644 --- a/tests/components/radio_browser/conftest.py +++ b/tests/components/radio_browser/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.radio_browser.const import DOMAIN diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py index a2c26c71231..b0411d9d313 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator from http import HTTPStatus import json from typing import Any @@ -9,7 +10,6 @@ from unittest.mock import patch from pyrainbird import encryption import pytest -from typing_extensions import Generator from homeassistant.components.rainbird import DOMAIN from homeassistant.components.rainbird.const import ( diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py index cdcef95f458..174bbdb3d48 100644 --- a/tests/components/rainbird/test_config_flow.py +++ b/tests/components/rainbird/test_config_flow.py @@ -1,11 +1,11 @@ """Tests for the Rain Bird config flow.""" +from collections.abc import AsyncGenerator from http import HTTPStatus from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import AsyncGenerator from homeassistant import config_entries from homeassistant.components.rainbird import DOMAIN diff --git a/tests/components/rainforest_raven/conftest.py b/tests/components/rainforest_raven/conftest.py index 0a809c6430a..35ce4443032 100644 --- a/tests/components/rainforest_raven/conftest.py +++ b/tests/components/rainforest_raven/conftest.py @@ -1,9 +1,9 @@ """Fixtures for the Rainforest RAVEn tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.core import HomeAssistant diff --git a/tests/components/rainforest_raven/test_config_flow.py b/tests/components/rainforest_raven/test_config_flow.py index 7f7041cbcd8..da7e65882a4 100644 --- a/tests/components/rainforest_raven/test_config_flow.py +++ b/tests/components/rainforest_raven/test_config_flow.py @@ -1,11 +1,11 @@ """Test Rainforest RAVEn config flow.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch from aioraven.device import RAVEnConnectionError import pytest from serial.tools.list_ports_common import ListPortInfo -from typing_extensions import Generator from homeassistant.components.rainforest_raven.const import DOMAIN from homeassistant.config_entries import SOURCE_USB, SOURCE_USER diff --git a/tests/components/rdw/conftest.py b/tests/components/rdw/conftest.py index 3f45f44e3d8..71c73a55441 100644 --- a/tests/components/rdw/conftest.py +++ b/tests/components/rdw/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from vehicle import Vehicle from homeassistant.components.rdw.const import CONF_LICENSE_PLATE, DOMAIN diff --git a/tests/components/recorder/conftest.py b/tests/components/recorder/conftest.py index 4db573fa65f..a1ff8dc2413 100644 --- a/tests/components/recorder/conftest.py +++ b/tests/components/recorder/conftest.py @@ -1,9 +1,9 @@ """Fixtures for the recorder component tests.""" +from collections.abc import Generator from unittest.mock import patch import pytest -from typing_extensions import Generator from homeassistant.components import recorder from homeassistant.core import HomeAssistant diff --git a/tests/components/recorder/test_filters_with_entityfilter_schema_37.py b/tests/components/recorder/test_filters_with_entityfilter_schema_37.py index 9c66d2ee169..6269d2bf903 100644 --- a/tests/components/recorder/test_filters_with_entityfilter_schema_37.py +++ b/tests/components/recorder/test_filters_with_entityfilter_schema_37.py @@ -1,12 +1,12 @@ """The tests for the recorder filter matching the EntityFilter component.""" +from collections.abc import AsyncGenerator import json from unittest.mock import patch import pytest from sqlalchemy import select from sqlalchemy.engine.row import Row -from typing_extensions import AsyncGenerator from homeassistant.components.recorder import Recorder, get_instance from homeassistant.components.recorder.db_schema import EventData, Events, States diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 071324e4b6a..52a220662ae 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections.abc import Generator from datetime import datetime, timedelta from pathlib import Path import sqlite3 @@ -14,7 +15,6 @@ from freezegun.api import FrozenDateTimeFactory import pytest from sqlalchemy.exc import DatabaseError, OperationalError, SQLAlchemyError from sqlalchemy.pool import QueuePool -from typing_extensions import Generator from homeassistant.components import recorder from homeassistant.components.recorder import ( diff --git a/tests/components/recorder/test_migration_from_schema_32.py b/tests/components/recorder/test_migration_from_schema_32.py index 8fda495cf60..ec307632826 100644 --- a/tests/components/recorder/test_migration_from_schema_32.py +++ b/tests/components/recorder/test_migration_from_schema_32.py @@ -1,5 +1,6 @@ """The tests for the recorder filter matching the EntityFilter component.""" +from collections.abc import AsyncGenerator import datetime import importlib import sys @@ -12,7 +13,6 @@ import pytest from sqlalchemy import create_engine, inspect from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session -from typing_extensions import AsyncGenerator from homeassistant.components import recorder from homeassistant.components.recorder import ( diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 1ccbaada265..df5a6a77cfc 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -1,5 +1,6 @@ """Test data purging.""" +from collections.abc import Generator from datetime import datetime, timedelta import json import sqlite3 @@ -9,7 +10,6 @@ from freezegun import freeze_time import pytest from sqlalchemy.exc import DatabaseError, OperationalError from sqlalchemy.orm.session import Session -from typing_extensions import Generator from voluptuous.error import MultipleInvalid from homeassistant.components import recorder diff --git a/tests/components/recorder/test_purge_v32_schema.py b/tests/components/recorder/test_purge_v32_schema.py index fb636cfa9dc..94ea2d51db3 100644 --- a/tests/components/recorder/test_purge_v32_schema.py +++ b/tests/components/recorder/test_purge_v32_schema.py @@ -1,5 +1,6 @@ """Test data purging.""" +from collections.abc import Generator from datetime import datetime, timedelta import json import sqlite3 @@ -10,7 +11,6 @@ import pytest from sqlalchemy import text, update from sqlalchemy.exc import DatabaseError, OperationalError from sqlalchemy.orm.session import Session -from typing_extensions import Generator from homeassistant.components import recorder from homeassistant.components.recorder import migration diff --git a/tests/components/refoss/conftest.py b/tests/components/refoss/conftest.py index 80b3f4d8b75..5ded3e9489d 100644 --- a/tests/components/refoss/conftest.py +++ b/tests/components/refoss/conftest.py @@ -1,9 +1,9 @@ """Pytest module configuration.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/renault/conftest.py b/tests/components/renault/conftest.py index a5af01b504a..00e35e1fa76 100644 --- a/tests/components/renault/conftest.py +++ b/tests/components/renault/conftest.py @@ -1,5 +1,6 @@ """Provide common Renault fixtures.""" +from collections.abc import Generator import contextlib from types import MappingProxyType from typing import Any @@ -8,7 +9,6 @@ from unittest.mock import AsyncMock, patch import pytest from renault_api.kamereon import exceptions, schemas from renault_api.renault_account import RenaultAccount -from typing_extensions import Generator from homeassistant.components.renault.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntry diff --git a/tests/components/renault/test_binary_sensor.py b/tests/components/renault/test_binary_sensor.py index a0264493544..52b6de33f14 100644 --- a/tests/components/renault/test_binary_sensor.py +++ b/tests/components/renault/test_binary_sensor.py @@ -1,10 +1,10 @@ """Tests for Renault binary sensors.""" +from collections.abc import Generator from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform diff --git a/tests/components/renault/test_button.py b/tests/components/renault/test_button.py index bed188d8881..32c5ce651ae 100644 --- a/tests/components/renault/test_button.py +++ b/tests/components/renault/test_button.py @@ -1,11 +1,11 @@ """Tests for Renault sensors.""" +from collections.abc import Generator from unittest.mock import patch import pytest from renault_api.kamereon import schemas from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.config_entries import ConfigEntry diff --git a/tests/components/renault/test_device_tracker.py b/tests/components/renault/test_device_tracker.py index d8bee097eda..39f37d12a4d 100644 --- a/tests/components/renault/test_device_tracker.py +++ b/tests/components/renault/test_device_tracker.py @@ -1,10 +1,10 @@ """Tests for Renault sensors.""" +from collections.abc import Generator from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index 90963fd3521..0f9d9cbaf5b 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -1,12 +1,12 @@ """Tests for Renault setup process.""" +from collections.abc import Generator from typing import Any from unittest.mock import Mock, patch import aiohttp import pytest from renault_api.gigya.exceptions import GigyaException, InvalidCredentialsException -from typing_extensions import Generator from homeassistant.components.renault.const import DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState diff --git a/tests/components/renault/test_select.py b/tests/components/renault/test_select.py index 0577966d514..7b589d86863 100644 --- a/tests/components/renault/test_select.py +++ b/tests/components/renault/test_select.py @@ -1,11 +1,11 @@ """Tests for Renault selects.""" +from collections.abc import Generator from unittest.mock import patch import pytest from renault_api.kamereon import schemas from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant.components.select import ( ATTR_OPTION, diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index 7e8e4f24c77..d69ab5c0b7f 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -1,10 +1,10 @@ """Tests for Renault sensors.""" +from collections.abc import Generator from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index d30626e4117..4e3460b9afa 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -1,5 +1,6 @@ """Tests for Renault sensors.""" +from collections.abc import Generator from datetime import datetime from unittest.mock import patch @@ -7,7 +8,6 @@ import pytest from renault_api.exceptions import RenaultException from renault_api.kamereon import schemas from renault_api.kamereon.models import ChargeSchedule -from typing_extensions import Generator from homeassistant.components.renault.const import DOMAIN from homeassistant.components.renault.services import ( diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 105815bae1d..ac606017837 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -1,9 +1,9 @@ """Setup the Reolink tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.reolink import const from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index 58e77184f55..cd4447c1a9a 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -1,11 +1,11 @@ """Configuration for Ring tests.""" +from collections.abc import Generator from itertools import chain from unittest.mock import AsyncMock, Mock, create_autospec, patch import pytest import ring_doorbell -from typing_extensions import Generator from homeassistant.components.ring import DOMAIN from homeassistant.const import CONF_USERNAME diff --git a/tests/components/roku/conftest.py b/tests/components/roku/conftest.py index 160a1bf3127..7ac332a1a6c 100644 --- a/tests/components/roku/conftest.py +++ b/tests/components/roku/conftest.py @@ -1,11 +1,11 @@ """Fixtures for Roku integration tests.""" +from collections.abc import Generator import json from unittest.mock import MagicMock, patch import pytest from rokuecp import Device as RokuDevice -from typing_extensions import Generator from homeassistant.components.roku.const import DOMAIN from homeassistant.const import CONF_HOST diff --git a/tests/components/rtsp_to_webrtc/conftest.py b/tests/components/rtsp_to_webrtc/conftest.py index 6e790b4ff00..956825f6372 100644 --- a/tests/components/rtsp_to_webrtc/conftest.py +++ b/tests/components/rtsp_to_webrtc/conftest.py @@ -2,13 +2,12 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import AsyncGenerator, Awaitable, Callable from typing import Any from unittest.mock import patch import pytest import rtsp_to_webrtc -from typing_extensions import AsyncGenerator from homeassistant.components import camera from homeassistant.components.rtsp_to_webrtc import DOMAIN diff --git a/tests/components/sabnzbd/conftest.py b/tests/components/sabnzbd/conftest.py index 7d68d3108f0..b5450e5134f 100644 --- a/tests/components/sabnzbd/conftest.py +++ b/tests/components/sabnzbd/conftest.py @@ -1,9 +1,9 @@ """Configuration for Sabnzbd tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 8d38adad06d..15794440343 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator from datetime import datetime from socket import AddressFamily # pylint: disable=no-name-in-module from typing import Any @@ -19,7 +19,6 @@ from samsungtvws.encrypted.remote import SamsungTVEncryptedWSAsyncRemote from samsungtvws.event import ED_INSTALLED_APP_EVENT from samsungtvws.exceptions import ResponseError from samsungtvws.remote import ChannelEmitCommand -from typing_extensions import Generator from homeassistant.components.samsungtv.const import WEBSOCKET_SSL_PORT from homeassistant.core import HomeAssistant, ServiceCall diff --git a/tests/components/sanix/conftest.py b/tests/components/sanix/conftest.py index 86eaa870770..405cad8b60b 100644 --- a/tests/components/sanix/conftest.py +++ b/tests/components/sanix/conftest.py @@ -1,5 +1,6 @@ """Sanix tests configuration.""" +from collections.abc import Generator from datetime import datetime from unittest.mock import AsyncMock, patch from zoneinfo import ZoneInfo @@ -16,7 +17,6 @@ from sanix import ( ATTR_API_TIME, ) from sanix.models import Measurement -from typing_extensions import Generator from homeassistant.components.sanix.const import CONF_SERIAL_NUMBER, DOMAIN from homeassistant.const import CONF_TOKEN diff --git a/tests/components/schlage/conftest.py b/tests/components/schlage/conftest.py index dcb6bc52a7b..9d61bb877d9 100644 --- a/tests/components/schlage/conftest.py +++ b/tests/components/schlage/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Schlage tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, Mock, create_autospec, patch from pyschlage.lock import Lock import pytest -from typing_extensions import Generator from homeassistant.components.schlage.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME diff --git a/tests/components/scrape/conftest.py b/tests/components/scrape/conftest.py index f6109dbc19a..5b84f4fd44a 100644 --- a/tests/components/scrape/conftest.py +++ b/tests/components/scrape/conftest.py @@ -2,12 +2,12 @@ from __future__ import annotations +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, patch import uuid import pytest -from typing_extensions import Generator from homeassistant.components.rest.data import DEFAULT_TIMEOUT from homeassistant.components.rest.schema import DEFAULT_METHOD, DEFAULT_VERIFY_SSL diff --git a/tests/components/screenlogic/test_services.py b/tests/components/screenlogic/test_services.py index d175ea27c84..0fc79fad0e5 100644 --- a/tests/components/screenlogic/test_services.py +++ b/tests/components/screenlogic/test_services.py @@ -1,12 +1,12 @@ """Tests for ScreenLogic integration service calls.""" +from collections.abc import AsyncGenerator from typing import Any from unittest.mock import DEFAULT, AsyncMock, patch import pytest from screenlogicpy import ScreenLogicGateway from screenlogicpy.device_const.system import COLOR_MODE -from typing_extensions import AsyncGenerator from homeassistant.components.screenlogic import DOMAIN from homeassistant.components.screenlogic.const import ( diff --git a/tests/components/season/conftest.py b/tests/components/season/conftest.py index a45a2078d9b..c7458b0a2e1 100644 --- a/tests/components/season/conftest.py +++ b/tests/components/season/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import patch import pytest -from typing_extensions import Generator from homeassistant.components.season.const import DOMAIN, TYPE_ASTRONOMICAL from homeassistant.const import CONF_TYPE diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 689a91f770d..034360c6cd2 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -2,13 +2,13 @@ from __future__ import annotations +from collections.abc import Generator from datetime import UTC, date, datetime from decimal import Decimal from types import ModuleType from typing import Any import pytest -from typing_extensions import Generator from homeassistant.components import sensor from homeassistant.components.number import NumberDeviceClass diff --git a/tests/components/seventeentrack/conftest.py b/tests/components/seventeentrack/conftest.py index 1ab4eed11ee..180e7b24075 100644 --- a/tests/components/seventeentrack/conftest.py +++ b/tests/components/seventeentrack/conftest.py @@ -1,10 +1,10 @@ """Configuration for 17Track tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch from py17track.package import Package import pytest -from typing_extensions import Generator from homeassistant.components.seventeentrack.const import ( CONF_SHOW_ARCHIVED, diff --git a/tests/components/sfr_box/conftest.py b/tests/components/sfr_box/conftest.py index e86cd06650e..7c1f8bbab5c 100644 --- a/tests/components/sfr_box/conftest.py +++ b/tests/components/sfr_box/conftest.py @@ -1,11 +1,11 @@ """Provide common SFR Box fixtures.""" +from collections.abc import Generator import json from unittest.mock import AsyncMock, patch import pytest from sfrbox_api.models import DslInfo, FtthInfo, SystemInfo, WanInfo -from typing_extensions import Generator from homeassistant.components.sfr_box.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntry diff --git a/tests/components/sfr_box/test_binary_sensor.py b/tests/components/sfr_box/test_binary_sensor.py index 8dba537f6cb..6152f8e2721 100644 --- a/tests/components/sfr_box/test_binary_sensor.py +++ b/tests/components/sfr_box/test_binary_sensor.py @@ -1,11 +1,11 @@ """Test the SFR Box binary sensors.""" +from collections.abc import Generator from unittest.mock import patch import pytest from sfrbox_api.models import SystemInfo from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform diff --git a/tests/components/sfr_box/test_button.py b/tests/components/sfr_box/test_button.py index 4f20a2f34a3..f555ccebbf9 100644 --- a/tests/components/sfr_box/test_button.py +++ b/tests/components/sfr_box/test_button.py @@ -1,11 +1,11 @@ """Test the SFR Box buttons.""" +from collections.abc import Generator from unittest.mock import patch import pytest from sfrbox_api.exceptions import SFRBoxError from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.config_entries import ConfigEntry diff --git a/tests/components/sfr_box/test_diagnostics.py b/tests/components/sfr_box/test_diagnostics.py index 597631d12f1..d31d97cbcf8 100644 --- a/tests/components/sfr_box/test_diagnostics.py +++ b/tests/components/sfr_box/test_diagnostics.py @@ -1,11 +1,11 @@ """Test the SFR Box diagnostics.""" +from collections.abc import Generator from unittest.mock import patch import pytest from sfrbox_api.models import SystemInfo from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/tests/components/sfr_box/test_init.py b/tests/components/sfr_box/test_init.py index 14688009c5c..19e15491be1 100644 --- a/tests/components/sfr_box/test_init.py +++ b/tests/components/sfr_box/test_init.py @@ -1,10 +1,10 @@ """Test the SFR Box setup process.""" +from collections.abc import Generator from unittest.mock import patch import pytest from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError -from typing_extensions import Generator from homeassistant.components.sfr_box.const import DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState diff --git a/tests/components/sfr_box/test_sensor.py b/tests/components/sfr_box/test_sensor.py index 506e1ed8962..dd4a67b42f6 100644 --- a/tests/components/sfr_box/test_sensor.py +++ b/tests/components/sfr_box/test_sensor.py @@ -1,10 +1,10 @@ """Test the SFR Box sensors.""" +from collections.abc import Generator from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index aaf853863e5..12ed845c7d2 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -1,10 +1,10 @@ """Define test fixtures for SimpliSafe.""" +from collections.abc import AsyncGenerator from unittest.mock import AsyncMock, Mock, patch import pytest from simplipy.system.v3 import SystemV3 -from typing_extensions import AsyncGenerator from homeassistant.components.simplisafe.const import DOMAIN from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py index fd07cc414e7..a9456bd3cc6 100644 --- a/tests/components/sleepiq/conftest.py +++ b/tests/components/sleepiq/conftest.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, create_autospec, patch from asyncsleepiq import ( @@ -17,7 +18,6 @@ from asyncsleepiq import ( SleepIQSleeper, ) import pytest -from typing_extensions import Generator from homeassistant.components.sleepiq import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME diff --git a/tests/components/slimproto/conftest.py b/tests/components/slimproto/conftest.py index ece30d3e5cf..1bb2d7f2628 100644 --- a/tests/components/slimproto/conftest.py +++ b/tests/components/slimproto/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.slimproto.const import DOMAIN diff --git a/tests/components/snapcast/conftest.py b/tests/components/snapcast/conftest.py index e5806ac5f40..bcc0ac5bc30 100644 --- a/tests/components/snapcast/conftest.py +++ b/tests/components/snapcast/conftest.py @@ -1,9 +1,9 @@ """Test the snapcast config flow.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/sonarr/conftest.py b/tests/components/sonarr/conftest.py index 739880a99aa..de7a3f781d7 100644 --- a/tests/components/sonarr/conftest.py +++ b/tests/components/sonarr/conftest.py @@ -1,5 +1,6 @@ """Fixtures for Sonarr integration tests.""" +from collections.abc import Generator import json from unittest.mock import MagicMock, patch @@ -13,7 +14,6 @@ from aiopyarr import ( SystemStatus, ) import pytest -from typing_extensions import Generator from homeassistant.components.sonarr.const import ( CONF_BASE_PATH, diff --git a/tests/components/srp_energy/conftest.py b/tests/components/srp_energy/conftest.py index 45eb726443f..b612bc9f3f3 100644 --- a/tests/components/srp_energy/conftest.py +++ b/tests/components/srp_energy/conftest.py @@ -2,12 +2,12 @@ from __future__ import annotations +from collections.abc import Generator import datetime as dt from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory import pytest -from typing_extensions import Generator from homeassistant.components.srp_energy.const import DOMAIN, PHOENIX_TIME_ZONE from homeassistant.const import CONF_ID diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 3cf3de54940..0142d71a805 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -13,13 +13,13 @@ so that it can inspect the output. from __future__ import annotations import asyncio +from collections.abc import Generator import logging import threading from unittest.mock import Mock, patch from aiohttp import web import pytest -from typing_extensions import Generator from homeassistant.components.stream.core import StreamOutput from homeassistant.components.stream.worker import StreamState diff --git a/tests/components/streamlabswater/conftest.py b/tests/components/streamlabswater/conftest.py index 5a53c7204fa..1bbdd3e9a08 100644 --- a/tests/components/streamlabswater/conftest.py +++ b/tests/components/streamlabswater/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the StreamLabs tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest from streamlabswater.streamlabswater import StreamlabsClient -from typing_extensions import Generator from homeassistant.components.streamlabswater import DOMAIN from homeassistant.const import CONF_API_KEY diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py index d28d9c308a7..ca2685ff827 100644 --- a/tests/components/stt/test_init.py +++ b/tests/components/stt/test_init.py @@ -1,12 +1,11 @@ """Test STT component setup.""" -from collections.abc import AsyncIterable +from collections.abc import AsyncIterable, Generator from http import HTTPStatus from pathlib import Path from unittest.mock import AsyncMock import pytest -from typing_extensions import Generator from homeassistant.components.stt import ( DOMAIN, diff --git a/tests/components/suez_water/conftest.py b/tests/components/suez_water/conftest.py index 51ade6009dc..f218fb7d833 100644 --- a/tests/components/suez_water/conftest.py +++ b/tests/components/suez_water/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the Suez Water tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/swiss_public_transport/conftest.py b/tests/components/swiss_public_transport/conftest.py index c139b99e54d..88bd233765b 100644 --- a/tests/components/swiss_public_transport/conftest.py +++ b/tests/components/swiss_public_transport/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the swiss_public_transport tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/switch_as_x/conftest.py b/tests/components/switch_as_x/conftest.py index 88a86892d2d..f8328f38b54 100644 --- a/tests/components/switch_as_x/conftest.py +++ b/tests/components/switch_as_x/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/switchbot_cloud/conftest.py b/tests/components/switchbot_cloud/conftest.py index ed233ff2de9..b559930dedb 100644 --- a/tests/components/switchbot_cloud/conftest.py +++ b/tests/components/switchbot_cloud/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the SwitchBot via API tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index 8ff395fcab3..2cf123af2b0 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -1,9 +1,9 @@ """Common fixtures and objects for the Switcher integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/synology_dsm/conftest.py b/tests/components/synology_dsm/conftest.py index 2f05d0187be..0e8f79ffd40 100644 --- a/tests/components/synology_dsm/conftest.py +++ b/tests/components/synology_dsm/conftest.py @@ -1,9 +1,9 @@ """Configure Synology DSM tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/systemmonitor/conftest.py b/tests/components/systemmonitor/conftest.py index e16debdf263..209f0e80f81 100644 --- a/tests/components/systemmonitor/conftest.py +++ b/tests/components/systemmonitor/conftest.py @@ -2,13 +2,13 @@ from __future__ import annotations +from collections.abc import Generator import socket from unittest.mock import AsyncMock, Mock, NonCallableMock, patch from psutil import NoSuchProcess, Process from psutil._common import sdiskpart, sdiskusage, shwtemp, snetio, snicaddr, sswap import pytest -from typing_extensions import Generator from homeassistant.components.systemmonitor.const import DOMAIN from homeassistant.components.systemmonitor.coordinator import VirtualMemory diff --git a/tests/components/tailscale/conftest.py b/tests/components/tailscale/conftest.py index cb7419daf89..5514678f530 100644 --- a/tests/components/tailscale/conftest.py +++ b/tests/components/tailscale/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest from tailscale.models import Devices -from typing_extensions import Generator from homeassistant.components.tailscale.const import CONF_TAILNET, DOMAIN from homeassistant.const import CONF_API_KEY diff --git a/tests/components/tailwind/conftest.py b/tests/components/tailwind/conftest.py index f23463548bc..ce49926cd2a 100644 --- a/tests/components/tailwind/conftest.py +++ b/tests/components/tailwind/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from gotailwind import TailwindDeviceStatus import pytest -from typing_extensions import Generator from homeassistant.components.tailwind.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TOKEN diff --git a/tests/components/tami4/conftest.py b/tests/components/tami4/conftest.py index 84b96c04735..2f4201d9a9e 100644 --- a/tests/components/tami4/conftest.py +++ b/tests/components/tami4/conftest.py @@ -1,5 +1,6 @@ """Common fixutres with default mocks as well as common test helper methods.""" +from collections.abc import Generator from datetime import datetime from unittest.mock import AsyncMock, MagicMock, patch @@ -7,7 +8,6 @@ import pytest from Tami4EdgeAPI.device import Device from Tami4EdgeAPI.device_metadata import DeviceMetadata from Tami4EdgeAPI.water_quality import UV, Filter, WaterQuality -from typing_extensions import Generator from homeassistant.components.tami4.const import CONF_REFRESH_TOKEN, DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/tankerkoenig/conftest.py b/tests/components/tankerkoenig/conftest.py index 8f2e2c2fb53..1517c3d2060 100644 --- a/tests/components/tankerkoenig/conftest.py +++ b/tests/components/tankerkoenig/conftest.py @@ -1,9 +1,9 @@ """Fixtures for Tankerkoenig integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.tankerkoenig import DOMAIN from homeassistant.const import CONF_SHOW_ON_MAP diff --git a/tests/components/technove/conftest.py b/tests/components/technove/conftest.py index be34ebfefa5..a81575f1edf 100644 --- a/tests/components/technove/conftest.py +++ b/tests/components/technove/conftest.py @@ -1,10 +1,10 @@ """Fixtures for TechnoVE integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest from technove import Station as TechnoVEStation -from typing_extensions import Generator from homeassistant.components.technove.const import DOMAIN from homeassistant.const import CONF_HOST diff --git a/tests/components/tedee/conftest.py b/tests/components/tedee/conftest.py index 295e34fd541..68444de640c 100644 --- a/tests/components/tedee/conftest.py +++ b/tests/components/tedee/conftest.py @@ -2,13 +2,13 @@ from __future__ import annotations +from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch from pytedee_async.bridge import TedeeBridge from pytedee_async.lock import TedeeLock import pytest -from typing_extensions import Generator from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN from homeassistant.const import CONF_HOST, CONF_WEBHOOK_ID diff --git a/tests/components/time_date/conftest.py b/tests/components/time_date/conftest.py index 4bcaa887b6f..7841b6d0b83 100644 --- a/tests/components/time_date/conftest.py +++ b/tests/components/time_date/conftest.py @@ -1,9 +1,9 @@ """Fixtures for Time & Date integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 5999b4b9fbe..cbb61434f1a 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -1,12 +1,12 @@ """Tests for the todo integration.""" +from collections.abc import Generator import datetime from typing import Any from unittest.mock import AsyncMock import zoneinfo import pytest -from typing_extensions import Generator import voluptuous as vol from homeassistant.components import conversation diff --git a/tests/components/todoist/conftest.py b/tests/components/todoist/conftest.py index 386385a0ddb..45fda53ccc1 100644 --- a/tests/components/todoist/conftest.py +++ b/tests/components/todoist/conftest.py @@ -1,5 +1,6 @@ """Common fixtures for the todoist tests.""" +from collections.abc import Generator from http import HTTPStatus from unittest.mock import AsyncMock, patch @@ -7,7 +8,6 @@ import pytest from requests.exceptions import HTTPError from requests.models import Response from todoist_api_python.models import Collaborator, Due, Label, Project, Task -from typing_extensions import Generator from homeassistant.components.todoist import DOMAIN from homeassistant.const import CONF_TOKEN, Platform diff --git a/tests/components/tplink/conftest.py b/tests/components/tplink/conftest.py index f8d933de71e..ad7c85fa728 100644 --- a/tests/components/tplink/conftest.py +++ b/tests/components/tplink/conftest.py @@ -1,10 +1,10 @@ """tplink conftest.""" +from collections.abc import Generator import copy from unittest.mock import DEFAULT, AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.tplink import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/tplink_omada/conftest.py b/tests/components/tplink_omada/conftest.py index c29fcb633e4..aef51bce87c 100644 --- a/tests/components/tplink_omada/conftest.py +++ b/tests/components/tplink_omada/conftest.py @@ -1,6 +1,6 @@ """Test fixtures for TP-Link Omada integration.""" -from collections.abc import AsyncIterable +from collections.abc import AsyncIterable, Generator import json from unittest.mock import AsyncMock, MagicMock, patch @@ -17,7 +17,6 @@ from tplink_omada_client.devices import ( OmadaSwitch, OmadaSwitchPortDetails, ) -from typing_extensions import Generator from homeassistant.components.tplink_omada.config_flow import CONF_SITE from homeassistant.components.tplink_omada.const import DOMAIN diff --git a/tests/components/traccar_server/conftest.py b/tests/components/traccar_server/conftest.py index 6a8e428e7a2..0013b3249bd 100644 --- a/tests/components/traccar_server/conftest.py +++ b/tests/components/traccar_server/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Traccar Server tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest from pytraccar import ApiClient, SubscriptionStatus -from typing_extensions import Generator from homeassistant.components.traccar_server.const import ( CONF_CUSTOM_ATTRIBUTES, diff --git a/tests/components/traccar_server/test_config_flow.py b/tests/components/traccar_server/test_config_flow.py index 5da6f592957..62f39f00dc1 100644 --- a/tests/components/traccar_server/test_config_flow.py +++ b/tests/components/traccar_server/test_config_flow.py @@ -1,11 +1,11 @@ """Test the Traccar Server config flow.""" +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock import pytest from pytraccar import TraccarException -from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.traccar.device_tracker import PLATFORM_SCHEMA diff --git a/tests/components/traccar_server/test_diagnostics.py b/tests/components/traccar_server/test_diagnostics.py index 15d74ef9ef5..738fea1a45d 100644 --- a/tests/components/traccar_server/test_diagnostics.py +++ b/tests/components/traccar_server/test_diagnostics.py @@ -1,9 +1,9 @@ """Test Traccar Server diagnostics.""" +from collections.abc import Generator from unittest.mock import AsyncMock from syrupy import SnapshotAssertion -from typing_extensions import Generator from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/tractive/conftest.py b/tests/components/tractive/conftest.py index 9a17a557c49..7f319a87b5b 100644 --- a/tests/components/tractive/conftest.py +++ b/tests/components/tractive/conftest.py @@ -1,12 +1,12 @@ """Common fixtures for the Tractive tests.""" +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch from aiotractive.trackable_object import TrackableObject from aiotractive.tracker import Tracker import pytest -from typing_extensions import Generator from homeassistant.components.tractive.const import DOMAIN, SERVER_UNAVAILABLE from homeassistant.const import CONF_EMAIL, CONF_PASSWORD diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index 08afe77b4a3..4b0b742850b 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Generator import json from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -12,7 +12,6 @@ from pytradfri.command import Command from pytradfri.const import ATTR_FIRMWARE_VERSION, ATTR_GATEWAY_ID from pytradfri.device import Device from pytradfri.gateway import Gateway -from typing_extensions import Generator from homeassistant.components.tradfri.const import DOMAIN diff --git a/tests/components/tts/common.py b/tests/components/tts/common.py index b99e6400273..1331f441940 100644 --- a/tests/components/tts/common.py +++ b/tests/components/tts/common.py @@ -2,13 +2,13 @@ from __future__ import annotations +from collections.abc import Generator from http import HTTPStatus from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator import voluptuous as vol from homeassistant.components import media_source diff --git a/tests/components/tts/conftest.py b/tests/components/tts/conftest.py index b8abb086260..d9a4499f544 100644 --- a/tests/components/tts/conftest.py +++ b/tests/components/tts/conftest.py @@ -3,11 +3,11 @@ From http://doc.pytest.org/en/latest/example/simple.html#making-test-result-information-available-in-fixtures """ +from collections.abc import Generator from pathlib import Path from unittest.mock import MagicMock import pytest -from typing_extensions import Generator from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ConfigFlow diff --git a/tests/components/tuya/conftest.py b/tests/components/tuya/conftest.py index 981e12ecceb..380209fd5ef 100644 --- a/tests/components/tuya/conftest.py +++ b/tests/components/tuya/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.tuya.const import CONF_APP_TYPE, CONF_USER_CODE, DOMAIN diff --git a/tests/components/twentemilieu/conftest.py b/tests/components/twentemilieu/conftest.py index 7b157572824..7ecf1657ce9 100644 --- a/tests/components/twentemilieu/conftest.py +++ b/tests/components/twentemilieu/conftest.py @@ -2,12 +2,12 @@ from __future__ import annotations +from collections.abc import Generator from datetime import date from unittest.mock import MagicMock, patch import pytest from twentemilieu import WasteType -from typing_extensions import Generator from homeassistant.components.twentemilieu.const import ( CONF_HOUSE_LETTER, diff --git a/tests/components/twitch/conftest.py b/tests/components/twitch/conftest.py index 6c243a8dbbf..25e443c2778 100644 --- a/tests/components/twitch/conftest.py +++ b/tests/components/twitch/conftest.py @@ -1,11 +1,11 @@ """Configure tests for the Twitch integration.""" +from collections.abc import Generator import time from unittest.mock import AsyncMock, patch import pytest from twitchAPI.object.api import FollowedChannel, Stream, TwitchUser, UserSubscription -from typing_extensions import Generator from homeassistant.components.application_credentials import ( ClientCredential, diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index 58b5dde2bac..de9bdd618de 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Ukraine Alarm config flow.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch from aiohttp import ClientConnectionError, ClientError, ClientResponseError, RequestInfo import pytest -from typing_extensions import Generator from yarl import URL from homeassistant import config_entries diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index 4a7d86eea38..f1ebe84c350 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Generator from datetime import timedelta from types import MappingProxyType from typing import Any @@ -12,7 +12,6 @@ from unittest.mock import AsyncMock, patch from aiounifi.models.message import MessageKey import orjson import pytest -from typing_extensions import Generator from homeassistant.components.unifi import STORAGE_KEY, STORAGE_VERSION from homeassistant.components.unifi.const import CONF_SITE_ID, DOMAIN as UNIFI_DOMAIN diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index b37abc2263a..7860c679f37 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -1,9 +1,9 @@ """The tests for the Update component.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.update import ( ATTR_BACKUP, diff --git a/tests/components/uptime/conftest.py b/tests/components/uptime/conftest.py index 2fe96b91b63..008172dc35a 100644 --- a/tests/components/uptime/conftest.py +++ b/tests/components/uptime/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import patch import pytest -from typing_extensions import Generator from homeassistant.components.uptime.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/v2c/conftest.py b/tests/components/v2c/conftest.py index 1803298be28..5c7db8bbab3 100644 --- a/tests/components/v2c/conftest.py +++ b/tests/components/v2c/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the V2C tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest from pytrydan.models.trydan import TrydanData -from typing_extensions import Generator from homeassistant.components.v2c.const import DOMAIN from homeassistant.const import CONF_HOST diff --git a/tests/components/vacuum/conftest.py b/tests/components/vacuum/conftest.py index 5167c868f9f..d298260c575 100644 --- a/tests/components/vacuum/conftest.py +++ b/tests/components/vacuum/conftest.py @@ -1,7 +1,8 @@ """Fixtures for Vacuum platform tests.""" +from collections.abc import Generator + import pytest -from typing_extensions import Generator from homeassistant.config_entries import ConfigFlow from homeassistant.core import HomeAssistant diff --git a/tests/components/valve/test_init.py b/tests/components/valve/test_init.py index 3ef3b1ff4b0..e4519bcef08 100644 --- a/tests/components/valve/test_init.py +++ b/tests/components/valve/test_init.py @@ -1,8 +1,9 @@ """The tests for Valve.""" +from collections.abc import Generator + import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant.components.valve import ( DOMAIN, diff --git a/tests/components/velbus/conftest.py b/tests/components/velbus/conftest.py index 3d59ad615c6..402acb821be 100644 --- a/tests/components/velbus/conftest.py +++ b/tests/components/velbus/conftest.py @@ -1,9 +1,9 @@ """Fixtures for the Velbus tests.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.velbus.const import DOMAIN from homeassistant.config_entries import ConfigEntry diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 59effcae706..432fcea10db 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -1,10 +1,10 @@ """Tests for the Velbus config flow.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest import serial.tools.list_ports -from typing_extensions import Generator from velbusaio.exceptions import VelbusConnectionFailed from homeassistant.components import usb diff --git a/tests/components/velux/conftest.py b/tests/components/velux/conftest.py index 692216827b2..512b2a007ed 100644 --- a/tests/components/velux/conftest.py +++ b/tests/components/velux/conftest.py @@ -1,9 +1,9 @@ """Configuration for Velux tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/verisure/conftest.py b/tests/components/verisure/conftest.py index 03086ac2ead..5aafcda2bb3 100644 --- a/tests/components/verisure/conftest.py +++ b/tests/components/verisure/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.verisure.const import CONF_GIID, DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD diff --git a/tests/components/vicare/conftest.py b/tests/components/vicare/conftest.py index 6899839a0e1..372314d9fe2 100644 --- a/tests/components/vicare/conftest.py +++ b/tests/components/vicare/conftest.py @@ -2,13 +2,13 @@ from __future__ import annotations +from collections.abc import AsyncGenerator, Generator from dataclasses import dataclass from unittest.mock import AsyncMock, Mock, patch import pytest from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig from PyViCare.PyViCareService import ViCareDeviceAccessor, readFeature -from typing_extensions import AsyncGenerator, Generator from homeassistant.components.vicare.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/vilfo/conftest.py b/tests/components/vilfo/conftest.py index 11b620b82e0..fbc48da28b3 100644 --- a/tests/components/vilfo/conftest.py +++ b/tests/components/vilfo/conftest.py @@ -1,9 +1,9 @@ """Vilfo tests conftest.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.vilfo import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST diff --git a/tests/components/wake_on_lan/conftest.py b/tests/components/wake_on_lan/conftest.py index cec3076d83e..c3c58ec4c69 100644 --- a/tests/components/wake_on_lan/conftest.py +++ b/tests/components/wake_on_lan/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/wake_word/test_init.py b/tests/components/wake_word/test_init.py index c19d3e7032f..cdaf7e0e3f0 100644 --- a/tests/components/wake_word/test_init.py +++ b/tests/components/wake_word/test_init.py @@ -1,14 +1,13 @@ """Test wake_word component setup.""" import asyncio -from collections.abc import AsyncIterable +from collections.abc import AsyncIterable, Generator from functools import partial from pathlib import Path from unittest.mock import patch from freezegun import freeze_time import pytest -from typing_extensions import Generator from homeassistant.components import wake_word from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow diff --git a/tests/components/waqi/conftest.py b/tests/components/waqi/conftest.py index b2e1a7d77d4..75709d4f56e 100644 --- a/tests/components/waqi/conftest.py +++ b/tests/components/waqi/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the World Air Quality Index (WAQI) tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.waqi.const import CONF_STATION_NUMBER, DOMAIN from homeassistant.const import CONF_API_KEY diff --git a/tests/components/water_heater/conftest.py b/tests/components/water_heater/conftest.py index 619d5e5c359..df16e5cc6da 100644 --- a/tests/components/water_heater/conftest.py +++ b/tests/components/water_heater/conftest.py @@ -1,7 +1,8 @@ """Fixtures for water heater platform tests.""" +from collections.abc import Generator + import pytest -from typing_extensions import Generator from homeassistant.config_entries import ConfigFlow from homeassistant.core import HomeAssistant diff --git a/tests/components/weather/conftest.py b/tests/components/weather/conftest.py index e3e790300a0..78389381ff3 100644 --- a/tests/components/weather/conftest.py +++ b/tests/components/weather/conftest.py @@ -1,7 +1,8 @@ """Fixtures for Weather platform tests.""" +from collections.abc import Generator + import pytest -from typing_extensions import Generator from homeassistant.config_entries import ConfigFlow from homeassistant.core import HomeAssistant diff --git a/tests/components/weatherflow/conftest.py b/tests/components/weatherflow/conftest.py index c0811597228..21c251d39b5 100644 --- a/tests/components/weatherflow/conftest.py +++ b/tests/components/weatherflow/conftest.py @@ -1,12 +1,12 @@ """Fixtures for Weatherflow integration tests.""" import asyncio +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest from pyweatherflowudp.client import EVENT_DEVICE_DISCOVERED from pyweatherflowudp.device import WeatherFlowDevice -from typing_extensions import Generator from homeassistant.components.weatherflow.const import DOMAIN diff --git a/tests/components/weatherflow_cloud/conftest.py b/tests/components/weatherflow_cloud/conftest.py index d47da3c7d1b..222d487393a 100644 --- a/tests/components/weatherflow_cloud/conftest.py +++ b/tests/components/weatherflow_cloud/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the WeatherflowCloud tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch from aiohttp import ClientResponseError import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/weatherkit/conftest.py b/tests/components/weatherkit/conftest.py index d4b849115f6..14d96d28347 100644 --- a/tests/components/weatherkit/conftest.py +++ b/tests/components/weatherkit/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the Apple WeatherKit tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/webmin/conftest.py b/tests/components/webmin/conftest.py index c3ad43510d5..388297f9665 100644 --- a/tests/components/webmin/conftest.py +++ b/tests/components/webmin/conftest.py @@ -1,9 +1,9 @@ """Fixtures for Webmin integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.webmin.const import DEFAULT_PORT, DOMAIN from homeassistant.const import ( diff --git a/tests/components/webostv/conftest.py b/tests/components/webostv/conftest.py index 2b5d701f899..d25d1c7b031 100644 --- a/tests/components/webostv/conftest.py +++ b/tests/components/webostv/conftest.py @@ -1,9 +1,9 @@ """Common fixtures and objects for the LG webOS integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import Generator from homeassistant.components.webostv.const import LIVE_TV_APP_ID from homeassistant.core import HomeAssistant, ServiceCall diff --git a/tests/components/whois/conftest.py b/tests/components/whois/conftest.py index 5fe420abb92..1c779cce671 100644 --- a/tests/components/whois/conftest.py +++ b/tests/components/whois/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations +from collections.abc import Generator from datetime import datetime from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest -from typing_extensions import Generator from homeassistant.components.whois.const import DOMAIN from homeassistant.const import CONF_DOMAIN diff --git a/tests/components/wiffi/conftest.py b/tests/components/wiffi/conftest.py index 5f16d676e81..2383906291f 100644 --- a/tests/components/wiffi/conftest.py +++ b/tests/components/wiffi/conftest.py @@ -1,9 +1,9 @@ """Configuration for Wiffi tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/wled/conftest.py b/tests/components/wled/conftest.py index 0d839fc8666..2cf909ca664 100644 --- a/tests/components/wled/conftest.py +++ b/tests/components/wled/conftest.py @@ -1,10 +1,10 @@ """Fixtures for WLED integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from freezegun.api import FrozenDateTimeFactory import pytest -from typing_extensions import Generator from wled import Device as WLEDDevice from homeassistant.components.wled.const import DOMAIN diff --git a/tests/components/workday/conftest.py b/tests/components/workday/conftest.py index 33bf98f90c3..081d6ce90db 100644 --- a/tests/components/workday/conftest.py +++ b/tests/components/workday/conftest.py @@ -1,9 +1,9 @@ """Fixtures for Workday integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/wyoming/conftest.py b/tests/components/wyoming/conftest.py index 47ef0566dc6..f6093e34261 100644 --- a/tests/components/wyoming/conftest.py +++ b/tests/components/wyoming/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Wyoming tests.""" +from collections.abc import Generator from pathlib import Path from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components import stt from homeassistant.components.wyoming import DOMAIN diff --git a/tests/components/xiaomi_ble/conftest.py b/tests/components/xiaomi_ble/conftest.py index bb74b3c7af3..8994aec813c 100644 --- a/tests/components/xiaomi_ble/conftest.py +++ b/tests/components/xiaomi_ble/conftest.py @@ -1,9 +1,9 @@ """Session fixtures.""" +from collections.abc import Generator from unittest import mock import pytest -from typing_extensions import Generator class MockServices: diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 462145d16ab..54646d30513 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -1,12 +1,12 @@ """The tests for the Xiaomi vacuum platform.""" +from collections.abc import Generator from datetime import datetime, time, timedelta from unittest import mock from unittest.mock import MagicMock, patch from miio import DeviceException import pytest -from typing_extensions import Generator from homeassistant.components.vacuum import ( ATTR_BATTERY_ICON, diff --git a/tests/components/yardian/conftest.py b/tests/components/yardian/conftest.py index 26a01f889b7..00e76c4c34f 100644 --- a/tests/components/yardian/conftest.py +++ b/tests/components/yardian/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the Yardian tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/zamg/conftest.py b/tests/components/zamg/conftest.py index 1795baa7fad..9fa4f333ef8 100644 --- a/tests/components/zamg/conftest.py +++ b/tests/components/zamg/conftest.py @@ -1,10 +1,10 @@ """Fixtures for Zamg integration tests.""" +from collections.abc import Generator import json from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from zamg import ZamgData as ZamgDevice from homeassistant.components.zamg.const import CONF_STATION_ID, DOMAIN diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 410eaceda76..5658e995a11 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,6 +1,6 @@ """Test configuration for the ZHA component.""" -from collections.abc import Callable +from collections.abc import Callable, Generator import itertools import time from typing import Any @@ -8,7 +8,6 @@ from unittest.mock import AsyncMock, MagicMock, create_autospec, patch import warnings import pytest -from typing_extensions import Generator import zigpy from zigpy.application import ControllerApplication import zigpy.backups diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index 280b3d05daf..5e92991cbb4 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -1,10 +1,10 @@ """Tests for ZHA config flow.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, create_autospec, patch import pytest import serial.tools.list_ports -from typing_extensions import Generator from zigpy.backups import BackupManager import zigpy.config from zigpy.config import CONF_DEVICE_PATH diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py index 2b1c0dcc561..7d1831650f3 100644 --- a/tests/components/zha/test_registries.py +++ b/tests/components/zha/test_registries.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest import mock import pytest -from typing_extensions import Generator import zigpy.quirks as zigpy_quirks from homeassistant.components.zha.binary_sensor import IASZone diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 10fd5edfabb..9b792fcb500 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Z-Wave JS config flow.""" import asyncio +from collections.abc import Generator from copy import copy from ipaddress import ip_address from unittest.mock import DEFAULT, MagicMock, call, patch @@ -8,7 +9,6 @@ from unittest.mock import DEFAULT, MagicMock, call, patch import aiohttp import pytest from serial.tools.list_ports_common import ListPortInfo -from typing_extensions import Generator from zwave_js_server.version import VersionInfo from homeassistant import config_entries From 2f0dd6f704f929e21555f8c4e00d4d0a94f53db3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:58:49 +0200 Subject: [PATCH 0524/2411] Import Generator from collections.abc (2) (#120915) --- tests/components/abode/conftest.py | 2 +- tests/components/accuweather/conftest.py | 2 +- tests/components/aemet/conftest.py | 2 +- tests/components/aftership/conftest.py | 2 +- tests/components/agent_dvr/conftest.py | 2 +- tests/components/airgradient/conftest.py | 2 +- tests/components/airnow/conftest.py | 2 +- tests/components/airq/conftest.py | 2 +- tests/components/airtouch5/conftest.py | 2 +- tests/components/airvisual/conftest.py | 2 +- tests/components/airvisual_pro/conftest.py | 2 +- tests/components/aladdin_connect/conftest.py | 2 +- tests/components/alarm_control_panel/conftest.py | 2 +- tests/components/amberelectric/conftest.py | 2 +- tests/components/ambient_network/conftest.py | 2 +- tests/components/ambient_station/conftest.py | 2 +- tests/components/analytics_insights/conftest.py | 2 +- tests/components/androidtv/conftest.py | 2 +- tests/components/androidtv_remote/conftest.py | 3 +-- tests/components/aosmith/conftest.py | 2 +- tests/components/aosmith/test_sensor.py | 2 +- tests/components/aosmith/test_water_heater.py | 2 +- tests/components/apple_tv/conftest.py | 2 +- tests/components/apple_tv/test_config_flow.py | 2 +- tests/components/application_credentials/test_init.py | 3 +-- tests/components/aprs/test_device_tracker.py | 2 +- tests/components/apsystems/conftest.py | 2 +- tests/components/arcam_fmj/conftest.py | 2 +- tests/components/arcam_fmj/test_config_flow.py | 2 +- tests/components/arve/conftest.py | 2 +- tests/components/assist_pipeline/conftest.py | 3 +-- tests/components/assist_pipeline/test_pipeline.py | 2 +- tests/components/asterisk_mbox/test_init.py | 2 +- tests/components/atag/conftest.py | 2 +- tests/components/aurora/conftest.py | 2 +- tests/components/axis/conftest.py | 3 +-- tests/components/axis/test_hub.py | 3 +-- tests/components/azure_data_explorer/conftest.py | 2 +- tests/components/azure_event_hub/conftest.py | 2 +- tests/components/balboa/conftest.py | 3 +-- tests/components/binary_sensor/test_init.py | 2 +- tests/components/blueprint/common.py | 3 +-- tests/components/bluetooth/conftest.py | 2 +- tests/components/bluetooth/test_manager.py | 2 +- tests/components/bmw_connected_drive/conftest.py | 3 ++- tests/components/braviatv/conftest.py | 2 +- tests/components/bring/conftest.py | 2 +- tests/components/brother/conftest.py | 2 +- tests/components/brottsplatskartan/conftest.py | 2 +- tests/components/brunt/conftest.py | 2 +- tests/components/bsblan/conftest.py | 2 +- tests/components/buienradar/conftest.py | 2 +- tests/components/button/test_init.py | 2 +- tests/components/caldav/test_config_flow.py | 2 +- tests/components/calendar/conftest.py | 2 +- tests/components/calendar/test_init.py | 2 +- tests/components/calendar/test_trigger.py | 3 +-- tests/components/camera/conftest.py | 2 +- tests/components/camera/test_init.py | 2 +- tests/components/canary/conftest.py | 2 +- tests/components/ccm15/conftest.py | 2 +- tests/components/cert_expiry/conftest.py | 2 +- tests/components/climate/conftest.py | 3 ++- tests/components/climate/test_intent.py | 3 ++- tests/components/cloud/conftest.py | 3 +-- tests/components/cloud/test_account_link.py | 2 +- tests/components/cloud/test_binary_sensor.py | 2 +- tests/components/cloud/test_stt.py | 2 +- tests/components/cloud/test_tts.py | 3 +-- tests/components/cloudflare/conftest.py | 2 +- tests/components/co2signal/conftest.py | 2 +- tests/components/comfoconnect/test_sensor.py | 2 +- tests/components/config/conftest.py | 2 +- tests/components/config/test_config_entries.py | 2 +- tests/components/cpuspeed/conftest.py | 2 +- tests/components/crownstone/test_config_flow.py | 2 +- tests/components/demo/test_camera.py | 2 +- tests/components/demo/test_climate.py | 2 +- tests/components/demo/test_cover.py | 2 +- tests/components/demo/test_init.py | 2 +- tests/components/demo/test_light.py | 2 +- tests/components/demo/test_notify.py | 2 +- tests/components/demo/test_number.py | 2 +- tests/components/demo/test_switch.py | 2 +- tests/components/demo/test_text.py | 2 +- tests/components/device_tracker/test_config_entry.py | 2 +- tests/components/device_tracker/test_init.py | 2 +- tests/components/devolo_home_control/conftest.py | 2 +- tests/components/discovergy/conftest.py | 2 +- tests/components/dlink/conftest.py | 3 +-- tests/components/dsmr/conftest.py | 2 +- tests/components/duotecno/conftest.py | 2 +- tests/components/dwd_weather_warnings/conftest.py | 2 +- tests/components/easyenergy/conftest.py | 2 +- tests/components/ecobee/conftest.py | 2 +- tests/components/ecoforest/conftest.py | 2 +- tests/components/ecovacs/conftest.py | 2 +- tests/components/edl21/conftest.py | 2 +- tests/components/electric_kiwi/conftest.py | 3 +-- tests/components/elgato/conftest.py | 2 +- tests/components/elmax/conftest.py | 2 +- tests/components/elvia/conftest.py | 2 +- tests/components/emulated_hue/test_hue_api.py | 2 +- tests/components/emulated_hue/test_upnp.py | 2 +- tests/components/energenie_power_sockets/conftest.py | 2 +- tests/components/energyzero/conftest.py | 2 +- tests/components/enphase_envoy/conftest.py | 2 +- tests/components/enphase_envoy/test_sensor.py | 2 +- tests/components/esphome/conftest.py | 3 +-- tests/components/event/test_init.py | 2 +- tests/components/evil_genius_labs/conftest.py | 2 +- tests/components/fibaro/conftest.py | 2 +- tests/components/file/conftest.py | 2 +- tests/components/filesize/conftest.py | 2 +- tests/components/fitbit/conftest.py | 3 +-- tests/components/fjaraskupan/test_config_flow.py | 2 +- tests/components/flexit_bacnet/conftest.py | 2 +- tests/components/flux_led/conftest.py | 2 +- tests/components/folder_watcher/conftest.py | 2 +- tests/components/forecast_solar/conftest.py | 2 +- tests/components/freedompro/conftest.py | 2 +- tests/components/frontend/test_init.py | 2 +- tests/components/frontier_silicon/conftest.py | 2 +- tests/components/fully_kiosk/conftest.py | 2 +- tests/components/fyta/conftest.py | 2 +- 125 files changed, 128 insertions(+), 139 deletions(-) diff --git a/tests/components/abode/conftest.py b/tests/components/abode/conftest.py index 21b236540d0..097eb568d4a 100644 --- a/tests/components/abode/conftest.py +++ b/tests/components/abode/conftest.py @@ -1,11 +1,11 @@ """Configuration for Abode tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch from jaraco.abode.helpers import urls as URL import pytest from requests_mock import Mocker -from typing_extensions import Generator from tests.common import load_fixture from tests.components.light.conftest import mock_light_profiles # noqa: F401 diff --git a/tests/components/accuweather/conftest.py b/tests/components/accuweather/conftest.py index 3b0006068ea..737fd3f84b6 100644 --- a/tests/components/accuweather/conftest.py +++ b/tests/components/accuweather/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the AccuWeather tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.accuweather.const import DOMAIN diff --git a/tests/components/aemet/conftest.py b/tests/components/aemet/conftest.py index aa4f537c7fb..38f4793541c 100644 --- a/tests/components/aemet/conftest.py +++ b/tests/components/aemet/conftest.py @@ -1,9 +1,9 @@ """Test fixtures for aemet.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/aftership/conftest.py b/tests/components/aftership/conftest.py index 1704b099cc2..d66ae267bfe 100644 --- a/tests/components/aftership/conftest.py +++ b/tests/components/aftership/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the AfterShip tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/agent_dvr/conftest.py b/tests/components/agent_dvr/conftest.py index a62e1738850..0ce1c008a23 100644 --- a/tests/components/agent_dvr/conftest.py +++ b/tests/components/agent_dvr/conftest.py @@ -1,9 +1,9 @@ """Test fixtures for Agent DVR.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/airgradient/conftest.py b/tests/components/airgradient/conftest.py index 7ca1198ce5f..a6ee85ecbdd 100644 --- a/tests/components/airgradient/conftest.py +++ b/tests/components/airgradient/conftest.py @@ -1,10 +1,10 @@ """AirGradient tests configuration.""" +from collections.abc import Generator from unittest.mock import patch from airgradient import Config, Measures import pytest -from typing_extensions import Generator from homeassistant.components.airgradient.const import DOMAIN from homeassistant.const import CONF_HOST diff --git a/tests/components/airnow/conftest.py b/tests/components/airnow/conftest.py index 676595250f1..c5d23fa7289 100644 --- a/tests/components/airnow/conftest.py +++ b/tests/components/airnow/conftest.py @@ -1,10 +1,10 @@ """Define fixtures for AirNow tests.""" +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.airnow import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS diff --git a/tests/components/airq/conftest.py b/tests/components/airq/conftest.py index 5df032c0308..a132153a76f 100644 --- a/tests/components/airq/conftest.py +++ b/tests/components/airq/conftest.py @@ -1,9 +1,9 @@ """Test fixtures for air-Q.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/airtouch5/conftest.py b/tests/components/airtouch5/conftest.py index d6d55689f17..ca678258c77 100644 --- a/tests/components/airtouch5/conftest.py +++ b/tests/components/airtouch5/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the Airtouch 5 tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/airvisual/conftest.py b/tests/components/airvisual/conftest.py index a82dc0ab78c..cc49b60e0d8 100644 --- a/tests/components/airvisual/conftest.py +++ b/tests/components/airvisual/conftest.py @@ -1,10 +1,10 @@ """Define test fixtures for AirVisual.""" +from collections.abc import AsyncGenerator, Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import AsyncGenerator, Generator from homeassistant.components.airvisual import ( CONF_CITY, diff --git a/tests/components/airvisual_pro/conftest.py b/tests/components/airvisual_pro/conftest.py index d25e9821d91..4acf9188889 100644 --- a/tests/components/airvisual_pro/conftest.py +++ b/tests/components/airvisual_pro/conftest.py @@ -1,10 +1,10 @@ """Define test fixtures for AirVisual Pro.""" +from collections.abc import AsyncGenerator, Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import AsyncGenerator, Generator from homeassistant.components.airvisual_pro.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py index 2c158998f49..8399269b30d 100644 --- a/tests/components/aladdin_connect/conftest.py +++ b/tests/components/aladdin_connect/conftest.py @@ -1,9 +1,9 @@ """Test fixtures for the Aladdin Connect Garage Door integration.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from tests.common import MockConfigEntry diff --git a/tests/components/alarm_control_panel/conftest.py b/tests/components/alarm_control_panel/conftest.py index 620b74dd80e..9c59c9e39c3 100644 --- a/tests/components/alarm_control_panel/conftest.py +++ b/tests/components/alarm_control_panel/conftest.py @@ -1,9 +1,9 @@ """Fixturs for Alarm Control Panel tests.""" +from collections.abc import Generator from unittest.mock import MagicMock import pytest -from typing_extensions import Generator from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, diff --git a/tests/components/amberelectric/conftest.py b/tests/components/amberelectric/conftest.py index 9de865fae6c..ce4073db71b 100644 --- a/tests/components/amberelectric/conftest.py +++ b/tests/components/amberelectric/conftest.py @@ -1,9 +1,9 @@ """Provide common Amber fixtures.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/ambient_network/conftest.py b/tests/components/ambient_network/conftest.py index 2900f8ae5fe..9fc001252a0 100644 --- a/tests/components/ambient_network/conftest.py +++ b/tests/components/ambient_network/conftest.py @@ -1,11 +1,11 @@ """Common fixtures for the Ambient Weather Network integration tests.""" +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch from aioambient import OpenAPI import pytest -from typing_extensions import Generator from homeassistant.components import ambient_network from homeassistant.core import HomeAssistant diff --git a/tests/components/ambient_station/conftest.py b/tests/components/ambient_station/conftest.py index e4f067108a5..160c05ad996 100644 --- a/tests/components/ambient_station/conftest.py +++ b/tests/components/ambient_station/conftest.py @@ -1,10 +1,10 @@ """Define test fixtures for Ambient PWS.""" +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import Generator from homeassistant.components.ambient_station.const import CONF_APP_KEY, DOMAIN from homeassistant.const import CONF_API_KEY diff --git a/tests/components/analytics_insights/conftest.py b/tests/components/analytics_insights/conftest.py index 75d47c41f4e..fcdda95e9bd 100644 --- a/tests/components/analytics_insights/conftest.py +++ b/tests/components/analytics_insights/conftest.py @@ -1,11 +1,11 @@ """Common fixtures for the Homeassistant Analytics tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest from python_homeassistant_analytics import CurrentAnalytics from python_homeassistant_analytics.models import CustomIntegration, Integration -from typing_extensions import Generator from homeassistant.components.analytics_insights.const import ( CONF_TRACKED_CUSTOM_INTEGRATIONS, diff --git a/tests/components/androidtv/conftest.py b/tests/components/androidtv/conftest.py index befb9db7a8c..a075ed66079 100644 --- a/tests/components/androidtv/conftest.py +++ b/tests/components/androidtv/conftest.py @@ -1,9 +1,9 @@ """Fixtures for the Android TV integration tests.""" +from collections.abc import Generator from unittest.mock import Mock, patch import pytest -from typing_extensions import Generator from . import patchers diff --git a/tests/components/androidtv_remote/conftest.py b/tests/components/androidtv_remote/conftest.py index aa5583927d1..05e40991ff9 100644 --- a/tests/components/androidtv_remote/conftest.py +++ b/tests/components/androidtv_remote/conftest.py @@ -1,10 +1,9 @@ """Fixtures for the Android TV Remote integration tests.""" -from collections.abc import Callable +from collections.abc import Callable, Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.androidtv_remote.const import DOMAIN from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/aosmith/conftest.py b/tests/components/aosmith/conftest.py index d67ae1ea627..7efbe0c58b2 100644 --- a/tests/components/aosmith/conftest.py +++ b/tests/components/aosmith/conftest.py @@ -1,5 +1,6 @@ """Common fixtures for the A. O. Smith tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from py_aosmith import AOSmithAPIClient @@ -14,7 +15,6 @@ from py_aosmith.models import ( SupportedOperationModeInfo, ) import pytest -from typing_extensions import Generator from homeassistant.components.aosmith.const import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD diff --git a/tests/components/aosmith/test_sensor.py b/tests/components/aosmith/test_sensor.py index a77e4e4576d..8e6f179c088 100644 --- a/tests/components/aosmith/test_sensor.py +++ b/tests/components/aosmith/test_sensor.py @@ -1,10 +1,10 @@ """Tests for the sensor platform of the A. O. Smith integration.""" +from collections.abc import AsyncGenerator from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import AsyncGenerator from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/aosmith/test_water_heater.py b/tests/components/aosmith/test_water_heater.py index ab4a4a33bca..3cd0de1722a 100644 --- a/tests/components/aosmith/test_water_heater.py +++ b/tests/components/aosmith/test_water_heater.py @@ -1,11 +1,11 @@ """Tests for the water heater platform of the A. O. Smith integration.""" +from collections.abc import AsyncGenerator from unittest.mock import MagicMock, patch from py_aosmith.models import OperationMode import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import AsyncGenerator from homeassistant.components.water_heater import ( ATTR_AWAY_MODE, diff --git a/tests/components/apple_tv/conftest.py b/tests/components/apple_tv/conftest.py index 36061924db5..78982a8d51c 100644 --- a/tests/components/apple_tv/conftest.py +++ b/tests/components/apple_tv/conftest.py @@ -1,12 +1,12 @@ """Fixtures for component.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from pyatv import conf from pyatv.const import PairingRequirement, Protocol from pyatv.support import http import pytest -from typing_extensions import Generator from .common import MockPairingHandler, airplay_service, create_conf, mrp_service diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index b8f49e7c8f5..f37042a6f50 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -1,12 +1,12 @@ """Test config flow.""" +from collections.abc import Generator from ipaddress import IPv4Address, ip_address from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from pyatv import exceptions from pyatv.const import PairingRequirement, Protocol import pytest -from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components import zeroconf diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index c427b1d07e0..e6fdf568bcc 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -2,13 +2,12 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Generator import logging from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import Generator from homeassistant import config_entries, data_entry_flow from homeassistant.components.application_credentials import ( diff --git a/tests/components/aprs/test_device_tracker.py b/tests/components/aprs/test_device_tracker.py index 4cdff41598f..4142195b0b9 100644 --- a/tests/components/aprs/test_device_tracker.py +++ b/tests/components/aprs/test_device_tracker.py @@ -1,11 +1,11 @@ """Test APRS device tracker.""" +from collections.abc import Generator from unittest.mock import MagicMock, Mock, patch import aprslib from aprslib import IS import pytest -from typing_extensions import Generator from homeassistant.components.aprs import device_tracker from homeassistant.core import HomeAssistant diff --git a/tests/components/apsystems/conftest.py b/tests/components/apsystems/conftest.py index cd04346c070..0fc1fb183a8 100644 --- a/tests/components/apsystems/conftest.py +++ b/tests/components/apsystems/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the APsystems Local API tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch from APsystemsEZ1 import ReturnDeviceInfo, ReturnOutputData import pytest -from typing_extensions import Generator from homeassistant.components.apsystems.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py index 66850933cc7..6c73b5c763a 100644 --- a/tests/components/arcam_fmj/conftest.py +++ b/tests/components/arcam_fmj/conftest.py @@ -1,11 +1,11 @@ """Tests for the arcam_fmj component.""" +from collections.abc import AsyncGenerator from unittest.mock import Mock, patch from arcam.fmj.client import Client from arcam.fmj.state import State import pytest -from typing_extensions import AsyncGenerator from homeassistant.components.arcam_fmj.const import DEFAULT_NAME from homeassistant.components.arcam_fmj.media_player import ArcamFmj diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index 26e93354900..8f80f5e7e04 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -1,11 +1,11 @@ """Tests for the Arcam FMJ config flow module.""" +from collections.abc import Generator from dataclasses import replace from unittest.mock import AsyncMock, MagicMock, patch from arcam.fmj.client import ConnectionFailed import pytest -from typing_extensions import Generator from homeassistant.components import ssdp from homeassistant.components.arcam_fmj.config_flow import get_entry_client diff --git a/tests/components/arve/conftest.py b/tests/components/arve/conftest.py index 40a5f98291b..8fc35e37000 100644 --- a/tests/components/arve/conftest.py +++ b/tests/components/arve/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Arve tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from asyncarve import ArveCustomer, ArveDevices, ArveSensPro, ArveSensProData import pytest -from typing_extensions import Generator from homeassistant.components.arve.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index f19e70a8ec1..c041a54d8fa 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -2,13 +2,12 @@ from __future__ import annotations -from collections.abc import AsyncIterable +from collections.abc import AsyncIterable, Generator from pathlib import Path from typing import Any from unittest.mock import AsyncMock import pytest -from typing_extensions import Generator from homeassistant.components import stt, tts, wake_word from homeassistant.components.assist_pipeline import DOMAIN, select as assist_select diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index 3e1e99412d8..45a661c0f07 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -1,10 +1,10 @@ """Websocket tests for Voice Assistant integration.""" +from collections.abc import AsyncGenerator from typing import Any from unittest.mock import ANY, patch import pytest -from typing_extensions import AsyncGenerator from homeassistant.components import conversation from homeassistant.components.assist_pipeline.const import DOMAIN diff --git a/tests/components/asterisk_mbox/test_init.py b/tests/components/asterisk_mbox/test_init.py index 4800ada0ec4..d7567ea3286 100644 --- a/tests/components/asterisk_mbox/test_init.py +++ b/tests/components/asterisk_mbox/test_init.py @@ -1,9 +1,9 @@ """Test mailbox.""" +from collections.abc import Generator from unittest.mock import Mock, patch import pytest -from typing_extensions import Generator from homeassistant.components.asterisk_mbox import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/atag/conftest.py b/tests/components/atag/conftest.py index 83ba3e37aad..63476c4846d 100644 --- a/tests/components/atag/conftest.py +++ b/tests/components/atag/conftest.py @@ -1,10 +1,10 @@ """Provide common Atag fixtures.""" import asyncio +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/aurora/conftest.py b/tests/components/aurora/conftest.py index 916f0925c4a..462203193f2 100644 --- a/tests/components/aurora/conftest.py +++ b/tests/components/aurora/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the Aurora tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.aurora.const import CONF_THRESHOLD, DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index 2f392960b86..da58e9576a8 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Generator from copy import deepcopy from types import MappingProxyType from typing import Any @@ -11,7 +11,6 @@ from unittest.mock import AsyncMock, patch from axis.rtsp import Signal, State import pytest import respx -from typing_extensions import Generator from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.config_entries import ConfigEntry diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index a797b3feee6..ff7e6b30cd6 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -1,6 +1,6 @@ """Test Axis device.""" -from collections.abc import Callable +from collections.abc import Callable, Generator from ipaddress import ip_address from types import MappingProxyType from typing import Any @@ -9,7 +9,6 @@ from unittest.mock import ANY, AsyncMock, Mock, call, patch import axis as axislib import pytest -from typing_extensions import Generator from homeassistant.components import axis, zeroconf from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN diff --git a/tests/components/azure_data_explorer/conftest.py b/tests/components/azure_data_explorer/conftest.py index 4168021b333..f8915a12ce1 100644 --- a/tests/components/azure_data_explorer/conftest.py +++ b/tests/components/azure_data_explorer/conftest.py @@ -1,12 +1,12 @@ """Test fixtures for Azure Data Explorer.""" +from collections.abc import Generator from datetime import timedelta import logging from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.azure_data_explorer.const import ( CONF_FILTER, diff --git a/tests/components/azure_event_hub/conftest.py b/tests/components/azure_event_hub/conftest.py index a34f2e646f2..b814a845c86 100644 --- a/tests/components/azure_event_hub/conftest.py +++ b/tests/components/azure_event_hub/conftest.py @@ -1,5 +1,6 @@ """Test fixtures for AEH.""" +from collections.abc import AsyncGenerator, Generator from dataclasses import dataclass from datetime import timedelta import logging @@ -8,7 +9,6 @@ from unittest.mock import AsyncMock, MagicMock, patch from azure.eventhub.aio import EventHubProducerClient import pytest -from typing_extensions import AsyncGenerator, Generator from homeassistant.components.azure_event_hub.const import ( CONF_FILTER, diff --git a/tests/components/balboa/conftest.py b/tests/components/balboa/conftest.py index fbdc2f8a759..0bb8b2cd468 100644 --- a/tests/components/balboa/conftest.py +++ b/tests/components/balboa/conftest.py @@ -2,12 +2,11 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Generator from unittest.mock import AsyncMock, MagicMock, patch from pybalboa.enums import HeatMode, LowHighRange import pytest -from typing_extensions import Generator from homeassistant.core import HomeAssistant diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 8f14063e011..ea0ad05a0db 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -1,9 +1,9 @@ """The tests for the Binary sensor component.""" +from collections.abc import Generator from unittest import mock import pytest -from typing_extensions import Generator from homeassistant.components import binary_sensor from homeassistant.config_entries import ConfigEntry, ConfigFlow diff --git a/tests/components/blueprint/common.py b/tests/components/blueprint/common.py index dd59b6df082..037aa38f6cb 100644 --- a/tests/components/blueprint/common.py +++ b/tests/components/blueprint/common.py @@ -1,9 +1,8 @@ """Blueprints test helpers.""" +from collections.abc import Generator from unittest.mock import patch -from typing_extensions import Generator - def stub_blueprint_populate_fixture_helper() -> Generator[None]: """Stub copying the blueprints to the config folder.""" diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 4373ec3f915..93a1c59cba1 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -1,12 +1,12 @@ """Tests for the bluetooth component.""" +from collections.abc import Generator from unittest.mock import patch from bleak_retry_connector import bleak_manager from dbus_fast.aio import message_bus import habluetooth.util as habluetooth_utils import pytest -from typing_extensions import Generator @pytest.fixture(name="disable_bluez_manager_socket", autouse=True, scope="package") diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index 4bff7cbe94d..0ac49aa72cd 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -1,5 +1,6 @@ """Tests for the Bluetooth integration manager.""" +from collections.abc import Generator from datetime import timedelta import time from typing import Any @@ -11,7 +12,6 @@ from bluetooth_adapters import AdvertisementHistory # pylint: disable-next=no-name-in-module from habluetooth.advertisement_tracker import TRACKER_BUFFERING_WOBBLE_SECONDS import pytest -from typing_extensions import Generator from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( diff --git a/tests/components/bmw_connected_drive/conftest.py b/tests/components/bmw_connected_drive/conftest.py index f69763dae77..7581b8c6f76 100644 --- a/tests/components/bmw_connected_drive/conftest.py +++ b/tests/components/bmw_connected_drive/conftest.py @@ -1,11 +1,12 @@ """Fixtures for BMW tests.""" +from collections.abc import Generator + from bimmer_connected.tests import ALL_CHARGING_SETTINGS, ALL_PROFILES, ALL_STATES from bimmer_connected.tests.common import MyBMWMockRouter from bimmer_connected.vehicle import remote_services import pytest import respx -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/braviatv/conftest.py b/tests/components/braviatv/conftest.py index 186f4e12337..b25e8ddf067 100644 --- a/tests/components/braviatv/conftest.py +++ b/tests/components/braviatv/conftest.py @@ -1,9 +1,9 @@ """Test fixtures for Bravia TV.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/bring/conftest.py b/tests/components/bring/conftest.py index 25330c10ba4..6c39c5020f9 100644 --- a/tests/components/bring/conftest.py +++ b/tests/components/bring/conftest.py @@ -1,11 +1,11 @@ """Common fixtures for the Bring! tests.""" +from collections.abc import Generator from typing import cast from unittest.mock import AsyncMock, patch from bring_api.types import BringAuthResponse import pytest -from typing_extensions import Generator from homeassistant.components.bring import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD diff --git a/tests/components/brother/conftest.py b/tests/components/brother/conftest.py index 5fadca5314d..ec6120db5f5 100644 --- a/tests/components/brother/conftest.py +++ b/tests/components/brother/conftest.py @@ -1,11 +1,11 @@ """Test fixtures for brother.""" +from collections.abc import Generator from datetime import UTC, datetime from unittest.mock import AsyncMock, patch from brother import BrotherSensors import pytest -from typing_extensions import Generator from homeassistant.components.brother.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TYPE diff --git a/tests/components/brottsplatskartan/conftest.py b/tests/components/brottsplatskartan/conftest.py index c10093f18b9..1d0cf236ed9 100644 --- a/tests/components/brottsplatskartan/conftest.py +++ b/tests/components/brottsplatskartan/conftest.py @@ -1,9 +1,9 @@ """Test fixtures for Brottplatskartan.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/brunt/conftest.py b/tests/components/brunt/conftest.py index bfbca238446..1b60db682c3 100644 --- a/tests/components/brunt/conftest.py +++ b/tests/components/brunt/conftest.py @@ -1,9 +1,9 @@ """Configuration for brunt tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py index 224e0e0b157..862f3ae1d0c 100644 --- a/tests/components/bsblan/conftest.py +++ b/tests/components/bsblan/conftest.py @@ -1,10 +1,10 @@ """Fixtures for BSBLAN integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from bsblan import Device, Info, State import pytest -from typing_extensions import Generator from homeassistant.components.bsblan.const import CONF_PASSKEY, DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME diff --git a/tests/components/buienradar/conftest.py b/tests/components/buienradar/conftest.py index 7c9027c7715..7872b50d4a9 100644 --- a/tests/components/buienradar/conftest.py +++ b/tests/components/buienradar/conftest.py @@ -1,9 +1,9 @@ """Test fixtures for buienradar2.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/button/test_init.py b/tests/components/button/test_init.py index 583c625e1b2..7df5308e096 100644 --- a/tests/components/button/test_init.py +++ b/tests/components/button/test_init.py @@ -1,11 +1,11 @@ """The tests for the Button component.""" +from collections.abc import Generator from datetime import timedelta from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory import pytest -from typing_extensions import Generator from homeassistant.components.button import ( DOMAIN, diff --git a/tests/components/caldav/test_config_flow.py b/tests/components/caldav/test_config_flow.py index 7c47ea14607..0079e59a931 100644 --- a/tests/components/caldav/test_config_flow.py +++ b/tests/components/caldav/test_config_flow.py @@ -1,11 +1,11 @@ """Test the CalDAV config flow.""" +from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch from caldav.lib.error import AuthorizationError, DAVError import pytest import requests -from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.caldav.const import DOMAIN diff --git a/tests/components/calendar/conftest.py b/tests/components/calendar/conftest.py index 83ecaca97d3..3e18f595764 100644 --- a/tests/components/calendar/conftest.py +++ b/tests/components/calendar/conftest.py @@ -1,12 +1,12 @@ """Test fixtures for calendar sensor platforms.""" +from collections.abc import Generator import datetime import secrets from typing import Any from unittest.mock import AsyncMock import pytest -from typing_extensions import Generator from homeassistant.components.calendar import DOMAIN, CalendarEntity, CalendarEvent from homeassistant.config_entries import ConfigEntry, ConfigFlow diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 116ca70f15e..cccc960939e 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator from datetime import timedelta from http import HTTPStatus from typing import Any @@ -9,7 +10,6 @@ from typing import Any from freezegun import freeze_time import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator import voluptuous as vol from homeassistant.components.calendar import DOMAIN, SERVICE_GET_EVENTS diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index 3b415d46e63..7e3136237c0 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -9,7 +9,7 @@ forward exercising the triggers. from __future__ import annotations -from collections.abc import AsyncIterator, Callable +from collections.abc import AsyncIterator, Callable, Generator from contextlib import asynccontextmanager import datetime import logging @@ -19,7 +19,6 @@ import zoneinfo from freezegun.api import FrozenDateTimeFactory import pytest -from typing_extensions import Generator from homeassistant.components import automation, calendar from homeassistant.components.calendar.trigger import EVENT_END, EVENT_START diff --git a/tests/components/camera/conftest.py b/tests/components/camera/conftest.py index 524b56c2303..ea3d65f4864 100644 --- a/tests/components/camera/conftest.py +++ b/tests/components/camera/conftest.py @@ -1,9 +1,9 @@ """Test helpers for camera.""" +from collections.abc import AsyncGenerator, Generator from unittest.mock import PropertyMock, patch import pytest -from typing_extensions import AsyncGenerator, Generator from homeassistant.components import camera from homeassistant.components.camera.const import StreamType diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 7da6cd91a7a..098c321e63b 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -1,12 +1,12 @@ """The tests for the camera component.""" +from collections.abc import Generator from http import HTTPStatus import io from types import ModuleType from unittest.mock import AsyncMock, Mock, PropertyMock, mock_open, patch import pytest -from typing_extensions import Generator from homeassistant.components import camera from homeassistant.components.camera.const import ( diff --git a/tests/components/canary/conftest.py b/tests/components/canary/conftest.py index 583986fd483..07a3ce89495 100644 --- a/tests/components/canary/conftest.py +++ b/tests/components/canary/conftest.py @@ -1,10 +1,10 @@ """Define fixtures available for all tests.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch from canary.api import Api import pytest -from typing_extensions import Generator from homeassistant.core import HomeAssistant diff --git a/tests/components/ccm15/conftest.py b/tests/components/ccm15/conftest.py index d6cc66d77dc..3bb67e92c51 100644 --- a/tests/components/ccm15/conftest.py +++ b/tests/components/ccm15/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Midea ccm15 AC Controller tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch from ccm15 import CCM15DeviceState, CCM15SlaveDevice import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/cert_expiry/conftest.py b/tests/components/cert_expiry/conftest.py index 2a86c669970..4932e9e1869 100644 --- a/tests/components/cert_expiry/conftest.py +++ b/tests/components/cert_expiry/conftest.py @@ -1,9 +1,9 @@ """Configuration for cert_expiry tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/climate/conftest.py b/tests/components/climate/conftest.py index a3a6af6e8a3..fd4368c4219 100644 --- a/tests/components/climate/conftest.py +++ b/tests/components/climate/conftest.py @@ -1,7 +1,8 @@ """Fixtures for Climate platform tests.""" +from collections.abc import Generator + import pytest -from typing_extensions import Generator from homeassistant.config_entries import ConfigFlow from homeassistant.core import HomeAssistant diff --git a/tests/components/climate/test_intent.py b/tests/components/climate/test_intent.py index ab1e3629ef8..54e2e4ff1a6 100644 --- a/tests/components/climate/test_intent.py +++ b/tests/components/climate/test_intent.py @@ -1,7 +1,8 @@ """Test climate intents.""" +from collections.abc import Generator + import pytest -from typing_extensions import Generator from homeassistant.components import conversation from homeassistant.components.climate import ( diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index c7d0702ea88..3a5d333f9b8 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -1,6 +1,6 @@ """Fixtures for cloud tests.""" -from collections.abc import Callable, Coroutine +from collections.abc import AsyncGenerator, Callable, Coroutine, Generator from pathlib import Path from typing import Any from unittest.mock import DEFAULT, MagicMock, PropertyMock, patch @@ -15,7 +15,6 @@ from hass_nabucasa.remote import RemoteUI from hass_nabucasa.voice import Voice import jwt import pytest -from typing_extensions import AsyncGenerator, Generator from homeassistant.components.cloud.client import CloudClient from homeassistant.components.cloud.const import DATA_CLOUD diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index acaff7db76c..cd81a7cf691 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -1,12 +1,12 @@ """Test account link services.""" import asyncio +from collections.abc import Generator import logging from time import time from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.cloud import account_link diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py index 789947f3c7d..8a4a1a0e9aa 100644 --- a/tests/components/cloud/test_binary_sensor.py +++ b/tests/components/cloud/test_binary_sensor.py @@ -1,10 +1,10 @@ """Tests for the cloud binary sensor.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch from hass_nabucasa.const import DISPATCH_REMOTE_CONNECT, DISPATCH_REMOTE_DISCONNECT import pytest -from typing_extensions import Generator from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry diff --git a/tests/components/cloud/test_stt.py b/tests/components/cloud/test_stt.py index df9e62380f8..02acda1450e 100644 --- a/tests/components/cloud/test_stt.py +++ b/tests/components/cloud/test_stt.py @@ -1,5 +1,6 @@ """Test the speech-to-text platform for the cloud integration.""" +from collections.abc import AsyncGenerator from copy import deepcopy from http import HTTPStatus from typing import Any @@ -7,7 +8,6 @@ from unittest.mock import AsyncMock, MagicMock, patch from hass_nabucasa.voice import STTResponse, VoiceError import pytest -from typing_extensions import AsyncGenerator from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY from homeassistant.components.cloud.const import DOMAIN diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index bf45b6b2895..52a9bc19ea2 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -1,6 +1,6 @@ """Tests for cloud tts.""" -from collections.abc import Callable, Coroutine +from collections.abc import AsyncGenerator, Callable, Coroutine from copy import deepcopy from http import HTTPStatus from typing import Any @@ -8,7 +8,6 @@ from unittest.mock import AsyncMock, MagicMock, patch from hass_nabucasa.voice import TTS_VOICES, VoiceError, VoiceTokenError import pytest -from typing_extensions import AsyncGenerator import voluptuous as vol from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY diff --git a/tests/components/cloudflare/conftest.py b/tests/components/cloudflare/conftest.py index 6c41e9fd179..977126f39a3 100644 --- a/tests/components/cloudflare/conftest.py +++ b/tests/components/cloudflare/conftest.py @@ -1,9 +1,9 @@ """Define fixtures available for all tests.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from . import get_mock_client diff --git a/tests/components/co2signal/conftest.py b/tests/components/co2signal/conftest.py index 04ab6db7464..d5cca448569 100644 --- a/tests/components/co2signal/conftest.py +++ b/tests/components/co2signal/conftest.py @@ -1,9 +1,9 @@ """Fixtures for Electricity maps integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.co2signal import DOMAIN from homeassistant.const import CONF_API_KEY diff --git a/tests/components/comfoconnect/test_sensor.py b/tests/components/comfoconnect/test_sensor.py index 91e7e1f0e25..fdecfa5b1c7 100644 --- a/tests/components/comfoconnect/test_sensor.py +++ b/tests/components/comfoconnect/test_sensor.py @@ -1,9 +1,9 @@ """Tests for the comfoconnect sensor platform.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.sensor import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/config/conftest.py b/tests/components/config/conftest.py index c401ac19fa9..55393a219b1 100644 --- a/tests/components/config/conftest.py +++ b/tests/components/config/conftest.py @@ -1,5 +1,6 @@ """Test fixtures for the config integration.""" +from collections.abc import Generator from contextlib import contextmanager from copy import deepcopy import json @@ -9,7 +10,6 @@ from typing import Any from unittest.mock import patch import pytest -from typing_extensions import Generator from homeassistant.core import HomeAssistant diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index e023a60f215..b184fedf928 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1,12 +1,12 @@ """Test config entries API.""" from collections import OrderedDict +from collections.abc import Generator from http import HTTPStatus from unittest.mock import ANY, AsyncMock, patch from aiohttp.test_utils import TestClient import pytest -from typing_extensions import Generator import voluptuous as vol from homeassistant import config_entries as core_ce, data_entry_flow, loader diff --git a/tests/components/cpuspeed/conftest.py b/tests/components/cpuspeed/conftest.py index e3ea1432659..d9079079ba2 100644 --- a/tests/components/cpuspeed/conftest.py +++ b/tests/components/cpuspeed/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.cpuspeed.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/crownstone/test_config_flow.py b/tests/components/crownstone/test_config_flow.py index be9086e02da..5dd00e7baff 100644 --- a/tests/components/crownstone/test_config_flow.py +++ b/tests/components/crownstone/test_config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from crownstone_cloud.cloud_models.spheres import Spheres @@ -11,7 +12,6 @@ from crownstone_cloud.exceptions import ( ) import pytest from serial.tools.list_ports_common import ListPortInfo -from typing_extensions import Generator from homeassistant.components import usb from homeassistant.components.crownstone.const import ( diff --git a/tests/components/demo/test_camera.py b/tests/components/demo/test_camera.py index 756609ed094..89dd8e0cdf7 100644 --- a/tests/components/demo/test_camera.py +++ b/tests/components/demo/test_camera.py @@ -1,9 +1,9 @@ """The tests for local file camera component.""" +from collections.abc import Generator from unittest.mock import patch import pytest -from typing_extensions import Generator from homeassistant.components.camera import ( DOMAIN as CAMERA_DOMAIN, diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py index 682b85f0845..383e00834b8 100644 --- a/tests/components/demo/test_climate.py +++ b/tests/components/demo/test_climate.py @@ -1,9 +1,9 @@ """The tests for the demo climate component.""" +from collections.abc import Generator from unittest.mock import patch import pytest -from typing_extensions import Generator import voluptuous as vol from homeassistant.components.climate import ( diff --git a/tests/components/demo/test_cover.py b/tests/components/demo/test_cover.py index 7ee408d3bfc..009d2ca2f49 100644 --- a/tests/components/demo/test_cover.py +++ b/tests/components/demo/test_cover.py @@ -1,10 +1,10 @@ """The tests for the Demo cover platform.""" +from collections.abc import Generator from datetime import timedelta from unittest.mock import patch import pytest -from typing_extensions import Generator from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 498a03600cb..0af15455949 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -1,10 +1,10 @@ """The tests for the Demo component.""" +from collections.abc import Generator import json from unittest.mock import patch import pytest -from typing_extensions import Generator from homeassistant.components.demo import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/demo/test_light.py b/tests/components/demo/test_light.py index 5c2c478b0bf..e3b1efc7eec 100644 --- a/tests/components/demo/test_light.py +++ b/tests/components/demo/test_light.py @@ -1,9 +1,9 @@ """The tests for the demo light component.""" +from collections.abc import Generator from unittest.mock import patch import pytest -from typing_extensions import Generator from homeassistant.components.demo import DOMAIN from homeassistant.components.light import ( diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index e9aa97f3d06..98b3de8448a 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -1,9 +1,9 @@ """The tests for the notify demo platform.""" +from collections.abc import Generator from unittest.mock import patch import pytest -from typing_extensions import Generator from homeassistant.components import notify from homeassistant.components.demo import DOMAIN diff --git a/tests/components/demo/test_number.py b/tests/components/demo/test_number.py index 37763b6e289..79885fa8581 100644 --- a/tests/components/demo/test_number.py +++ b/tests/components/demo/test_number.py @@ -1,9 +1,9 @@ """The tests for the demo number component.""" +from collections.abc import Generator from unittest.mock import patch import pytest -from typing_extensions import Generator import voluptuous as vol from homeassistant.components.number import ( diff --git a/tests/components/demo/test_switch.py b/tests/components/demo/test_switch.py index 8b78171fd17..57384526dc0 100644 --- a/tests/components/demo/test_switch.py +++ b/tests/components/demo/test_switch.py @@ -1,9 +1,9 @@ """The tests for the demo switch component.""" +from collections.abc import Generator from unittest.mock import patch import pytest -from typing_extensions import Generator from homeassistant.components.demo import DOMAIN from homeassistant.components.switch import ( diff --git a/tests/components/demo/test_text.py b/tests/components/demo/test_text.py index 3588330c75c..4ca172e5143 100644 --- a/tests/components/demo/test_text.py +++ b/tests/components/demo/test_text.py @@ -1,9 +1,9 @@ """The tests for the demo text component.""" +from collections.abc import Generator from unittest.mock import patch import pytest -from typing_extensions import Generator from homeassistant.components.text import ( ATTR_MAX, diff --git a/tests/components/device_tracker/test_config_entry.py b/tests/components/device_tracker/test_config_entry.py index 45b94012051..5b9ce78e4f5 100644 --- a/tests/components/device_tracker/test_config_entry.py +++ b/tests/components/device_tracker/test_config_entry.py @@ -1,9 +1,9 @@ """Test Device Tracker config entry things.""" +from collections.abc import Generator from typing import Any import pytest -from typing_extensions import Generator from homeassistant.components.device_tracker import ( ATTR_HOST_NAME, diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index cedf2a2f0bc..362258b035a 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -1,5 +1,6 @@ """The tests for the device tracker component.""" +from collections.abc import Generator from datetime import datetime, timedelta import json import logging @@ -8,7 +9,6 @@ from types import ModuleType from unittest.mock import call, patch import pytest -from typing_extensions import Generator from homeassistant.components import device_tracker, zone from homeassistant.components.device_tracker import SourceType, const, legacy diff --git a/tests/components/devolo_home_control/conftest.py b/tests/components/devolo_home_control/conftest.py index 04752da5925..55e072d075c 100644 --- a/tests/components/devolo_home_control/conftest.py +++ b/tests/components/devolo_home_control/conftest.py @@ -1,9 +1,9 @@ """Fixtures for tests.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/discovergy/conftest.py b/tests/components/discovergy/conftest.py index 056f763c3e2..4f65099c1b4 100644 --- a/tests/components/discovergy/conftest.py +++ b/tests/components/discovergy/conftest.py @@ -1,10 +1,10 @@ """Fixtures for Discovergy integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch from pydiscovergy.models import Reading import pytest -from typing_extensions import Generator from homeassistant.components.discovergy.const import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD diff --git a/tests/components/dlink/conftest.py b/tests/components/dlink/conftest.py index 4bbf99000a9..c56b93c4d3d 100644 --- a/tests/components/dlink/conftest.py +++ b/tests/components/dlink/conftest.py @@ -1,11 +1,10 @@ """Configure pytest for D-Link tests.""" -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator from copy import deepcopy from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components import dhcp from homeassistant.components.dlink.const import CONF_USE_LEGACY_PROTOCOL, DOMAIN diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py index 2257b8414a6..2301b9dfc80 100644 --- a/tests/components/dsmr/conftest.py +++ b/tests/components/dsmr/conftest.py @@ -1,6 +1,7 @@ """Common test tools.""" import asyncio +from collections.abc import Generator from unittest.mock import MagicMock, patch from dsmr_parser.clients.protocol import DSMRProtocol @@ -15,7 +16,6 @@ from dsmr_parser.obis_references import ( ) from dsmr_parser.objects import CosemObject import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/duotecno/conftest.py b/tests/components/duotecno/conftest.py index 1b6ba8f65e5..1bdd26bab9c 100644 --- a/tests/components/duotecno/conftest.py +++ b/tests/components/duotecno/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the duotecno tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/dwd_weather_warnings/conftest.py b/tests/components/dwd_weather_warnings/conftest.py index 40c8bf3cfa0..50c0fe51024 100644 --- a/tests/components/dwd_weather_warnings/conftest.py +++ b/tests/components/dwd_weather_warnings/conftest.py @@ -1,9 +1,9 @@ """Configuration for Deutscher Wetterdienst (DWD) Weather Warnings tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest -from typing_extensions import Generator from homeassistant.components.dwd_weather_warnings.const import ( ADVANCE_WARNING_SENSOR, diff --git a/tests/components/easyenergy/conftest.py b/tests/components/easyenergy/conftest.py index 96d356b8906..ffe0e36f3d2 100644 --- a/tests/components/easyenergy/conftest.py +++ b/tests/components/easyenergy/conftest.py @@ -1,11 +1,11 @@ """Fixtures for easyEnergy integration tests.""" +from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch from easyenergy import Electricity, Gas import pytest -from typing_extensions import Generator from homeassistant.components.easyenergy.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/ecobee/conftest.py b/tests/components/ecobee/conftest.py index d9583e15986..01f249bea15 100644 --- a/tests/components/ecobee/conftest.py +++ b/tests/components/ecobee/conftest.py @@ -1,10 +1,10 @@ """Fixtures for tests.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest from requests_mock import Mocker -from typing_extensions import Generator from homeassistant.components.ecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN diff --git a/tests/components/ecoforest/conftest.py b/tests/components/ecoforest/conftest.py index 3eb13e58aee..85bfff08bdf 100644 --- a/tests/components/ecoforest/conftest.py +++ b/tests/components/ecoforest/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Ecoforest tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch from pyecoforest.models.device import Alarm, Device, OperationMode, State import pytest -from typing_extensions import Generator from homeassistant.components.ecoforest import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME diff --git a/tests/components/ecovacs/conftest.py b/tests/components/ecovacs/conftest.py index 8d0033a6bc9..59721b65563 100644 --- a/tests/components/ecovacs/conftest.py +++ b/tests/components/ecovacs/conftest.py @@ -1,5 +1,6 @@ """Common fixtures for the Ecovacs tests.""" +from collections.abc import AsyncGenerator, Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -9,7 +10,6 @@ from deebot_client.device import Device from deebot_client.exceptions import ApiError from deebot_client.models import Credentials import pytest -from typing_extensions import AsyncGenerator, Generator from homeassistant.components.ecovacs import PLATFORMS from homeassistant.components.ecovacs.const import DOMAIN diff --git a/tests/components/edl21/conftest.py b/tests/components/edl21/conftest.py index b6af4ea9cef..1b14e3366d8 100644 --- a/tests/components/edl21/conftest.py +++ b/tests/components/edl21/conftest.py @@ -1,9 +1,9 @@ """Define test fixtures for EDL21.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/electric_kiwi/conftest.py b/tests/components/electric_kiwi/conftest.py index c9f9c7e04f0..010efcb7b5f 100644 --- a/tests/components/electric_kiwi/conftest.py +++ b/tests/components/electric_kiwi/conftest.py @@ -2,13 +2,12 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator from time import time from unittest.mock import AsyncMock, patch from electrickiwi_api.model import AccountBalance, Hop, HopIntervals import pytest -from typing_extensions import Generator from homeassistant.components.application_credentials import ( ClientCredential, diff --git a/tests/components/elgato/conftest.py b/tests/components/elgato/conftest.py index aaaed0dc8da..73b09421576 100644 --- a/tests/components/elgato/conftest.py +++ b/tests/components/elgato/conftest.py @@ -1,10 +1,10 @@ """Fixtures for Elgato integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from elgato import BatteryInfo, ElgatoNoBatteryError, Info, Settings, State import pytest -from typing_extensions import Generator from homeassistant.components.elgato.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT diff --git a/tests/components/elmax/conftest.py b/tests/components/elmax/conftest.py index 552aa138f1b..f92fc2f1827 100644 --- a/tests/components/elmax/conftest.py +++ b/tests/components/elmax/conftest.py @@ -1,5 +1,6 @@ """Configuration for Elmax tests.""" +from collections.abc import Generator import json from unittest.mock import AsyncMock, patch @@ -12,7 +13,6 @@ from elmax_api.constants import ( from httpx import Response import pytest import respx -from typing_extensions import Generator from . import ( MOCK_DIRECT_HOST, diff --git a/tests/components/elvia/conftest.py b/tests/components/elvia/conftest.py index 0708e5c698a..13955db49d5 100644 --- a/tests/components/elvia/conftest.py +++ b/tests/components/elvia/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the Elvia tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 40f9f7bce14..28e269fdaeb 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections.abc import Generator from datetime import timedelta from http import HTTPStatus from ipaddress import ip_address @@ -12,7 +13,6 @@ from unittest.mock import AsyncMock, _patch, patch from aiohttp.hdrs import CONTENT_TYPE from aiohttp.test_utils import TestClient import pytest -from typing_extensions import Generator from homeassistant import const, setup from homeassistant.components import ( diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 3522f7e8047..b16fda536c6 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -1,6 +1,7 @@ """The tests for the emulated Hue component.""" from asyncio import AbstractEventLoop +from collections.abc import Generator from http import HTTPStatus import json import unittest @@ -10,7 +11,6 @@ from aiohttp import web from aiohttp.test_utils import TestClient import defusedxml.ElementTree as ET import pytest -from typing_extensions import Generator from homeassistant import setup from homeassistant.components import emulated_hue diff --git a/tests/components/energenie_power_sockets/conftest.py b/tests/components/energenie_power_sockets/conftest.py index 64eb8bbd2a8..c142e436fd3 100644 --- a/tests/components/energenie_power_sockets/conftest.py +++ b/tests/components/energenie_power_sockets/conftest.py @@ -1,11 +1,11 @@ """Configure tests for Energenie-Power-Sockets.""" +from collections.abc import Generator from typing import Final from unittest.mock import MagicMock, patch from pyegps.fakes.powerstrip import FakePowerStrip import pytest -from typing_extensions import Generator from homeassistant.components.energenie_power_sockets.const import ( CONF_DEVICE_API_ID, diff --git a/tests/components/energyzero/conftest.py b/tests/components/energyzero/conftest.py index 49f6c18b09e..d42283c0d4b 100644 --- a/tests/components/energyzero/conftest.py +++ b/tests/components/energyzero/conftest.py @@ -1,11 +1,11 @@ """Fixtures for EnergyZero integration tests.""" +from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch from energyzero import Electricity, Gas import pytest -from typing_extensions import Generator from homeassistant.components.energyzero.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py index 647084c21ff..deebada3d45 100644 --- a/tests/components/enphase_envoy/conftest.py +++ b/tests/components/enphase_envoy/conftest.py @@ -1,5 +1,6 @@ """Define test fixtures for Enphase Envoy.""" +from collections.abc import AsyncGenerator from unittest.mock import AsyncMock, Mock, patch import jwt @@ -21,7 +22,6 @@ from pyenphase.models.meters import ( EnvoyPhaseMode, ) import pytest -from typing_extensions import AsyncGenerator from homeassistant.components.enphase_envoy import DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME diff --git a/tests/components/enphase_envoy/test_sensor.py b/tests/components/enphase_envoy/test_sensor.py index bfb6fdb2826..f101031da5f 100644 --- a/tests/components/enphase_envoy/test_sensor.py +++ b/tests/components/enphase_envoy/test_sensor.py @@ -1,10 +1,10 @@ """Test Enphase Envoy sensors.""" +from collections.abc import AsyncGenerator from unittest.mock import Mock, patch import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import AsyncGenerator from homeassistant.components.enphase_envoy import DOMAIN from homeassistant.components.enphase_envoy.const import Platform diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 8a069d257d8..75be231558f 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from asyncio import Event -from collections.abc import Awaitable, Callable, Coroutine +from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine from pathlib import Path from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -24,7 +24,6 @@ from aioesphomeapi import ( VoiceAssistantFeature, ) import pytest -from typing_extensions import AsyncGenerator from zeroconf import Zeroconf from homeassistant.components.esphome import dashboard diff --git a/tests/components/event/test_init.py b/tests/components/event/test_init.py index 981a7744beb..c6828c2c290 100644 --- a/tests/components/event/test_init.py +++ b/tests/components/event/test_init.py @@ -1,10 +1,10 @@ """The tests for the event integration.""" +from collections.abc import Generator from typing import Any from freezegun import freeze_time import pytest -from typing_extensions import Generator from homeassistant.components.event import ( ATTR_EVENT_TYPE, diff --git a/tests/components/evil_genius_labs/conftest.py b/tests/components/evil_genius_labs/conftest.py index 081b7a5120a..fc0725607e2 100644 --- a/tests/components/evil_genius_labs/conftest.py +++ b/tests/components/evil_genius_labs/conftest.py @@ -1,10 +1,10 @@ """Test helpers for Evil Genius Labs.""" +from collections.abc import AsyncGenerator from typing import Any from unittest.mock import patch import pytest -from typing_extensions import AsyncGenerator from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/fibaro/conftest.py b/tests/components/fibaro/conftest.py index d2f004a160c..4d99dea6682 100644 --- a/tests/components/fibaro/conftest.py +++ b/tests/components/fibaro/conftest.py @@ -1,9 +1,9 @@ """Test helpers.""" +from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import Generator from homeassistant.components.fibaro import CONF_IMPORT_PLUGINS, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME diff --git a/tests/components/file/conftest.py b/tests/components/file/conftest.py index 265acde36ca..5345a0d38d0 100644 --- a/tests/components/file/conftest.py +++ b/tests/components/file/conftest.py @@ -1,9 +1,9 @@ """Test fixtures for file platform.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.core import HomeAssistant diff --git a/tests/components/filesize/conftest.py b/tests/components/filesize/conftest.py index 859886a3058..ac66af0d22f 100644 --- a/tests/components/filesize/conftest.py +++ b/tests/components/filesize/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations +from collections.abc import Generator from pathlib import Path from unittest.mock import patch import pytest -from typing_extensions import Generator from homeassistant.components.filesize.const import DOMAIN from homeassistant.const import CONF_FILE_PATH diff --git a/tests/components/fitbit/conftest.py b/tests/components/fitbit/conftest.py index b1ff8a94e12..57511739993 100644 --- a/tests/components/fitbit/conftest.py +++ b/tests/components/fitbit/conftest.py @@ -1,6 +1,6 @@ """Test fixtures for fitbit.""" -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator import datetime from http import HTTPStatus import time @@ -9,7 +9,6 @@ from unittest.mock import patch import pytest from requests_mock.mocker import Mocker -from typing_extensions import Generator from homeassistant.components.application_credentials import ( ClientCredential, diff --git a/tests/components/fjaraskupan/test_config_flow.py b/tests/components/fjaraskupan/test_config_flow.py index 886e01c8966..6d3df614443 100644 --- a/tests/components/fjaraskupan/test_config_flow.py +++ b/tests/components/fjaraskupan/test_config_flow.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.fjaraskupan.const import DOMAIN diff --git a/tests/components/flexit_bacnet/conftest.py b/tests/components/flexit_bacnet/conftest.py index e1b98070d25..cc7c9fa0570 100644 --- a/tests/components/flexit_bacnet/conftest.py +++ b/tests/components/flexit_bacnet/conftest.py @@ -1,10 +1,10 @@ """Configuration for Flexit Nordic (BACnet) tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch from flexit_bacnet import FlexitBACnet import pytest -from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.flexit_bacnet.const import DOMAIN diff --git a/tests/components/flux_led/conftest.py b/tests/components/flux_led/conftest.py index bc9f68dc3b1..d323b321e08 100644 --- a/tests/components/flux_led/conftest.py +++ b/tests/components/flux_led/conftest.py @@ -1,9 +1,9 @@ """Tests for the flux_led integration.""" +from collections.abc import Generator from unittest.mock import patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/folder_watcher/conftest.py b/tests/components/folder_watcher/conftest.py index 6de9c69d574..ed0adea7a7d 100644 --- a/tests/components/folder_watcher/conftest.py +++ b/tests/components/folder_watcher/conftest.py @@ -2,12 +2,12 @@ from __future__ import annotations +from collections.abc import Generator from pathlib import Path from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest -from typing_extensions import Generator from homeassistant.components.folder_watcher.const import DOMAIN from homeassistant.config_entries import SOURCE_USER diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py index d1eacad8dbe..01c1f6d8d32 100644 --- a/tests/components/forecast_solar/conftest.py +++ b/tests/components/forecast_solar/conftest.py @@ -1,11 +1,11 @@ """Fixtures for Forecast.Solar integration tests.""" +from collections.abc import Generator from datetime import datetime, timedelta from unittest.mock import AsyncMock, MagicMock, patch from forecast_solar import models import pytest -from typing_extensions import Generator from homeassistant.components.forecast_solar.const import ( CONF_AZIMUTH, diff --git a/tests/components/freedompro/conftest.py b/tests/components/freedompro/conftest.py index 91eecc24f27..8e581673b92 100644 --- a/tests/components/freedompro/conftest.py +++ b/tests/components/freedompro/conftest.py @@ -2,12 +2,12 @@ from __future__ import annotations +from collections.abc import Generator from copy import deepcopy from typing import Any from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.freedompro.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 501f9c482f2..0856d81e205 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -1,6 +1,7 @@ """The tests for Home Assistant frontend.""" from asyncio import AbstractEventLoop +from collections.abc import Generator from http import HTTPStatus from pathlib import Path import re @@ -10,7 +11,6 @@ from unittest.mock import patch from aiohttp.test_utils import TestClient from freezegun.api import FrozenDateTimeFactory import pytest -from typing_extensions import Generator from homeassistant.components.frontend import ( CONF_EXTRA_JS_URL_ES5, diff --git a/tests/components/frontier_silicon/conftest.py b/tests/components/frontier_silicon/conftest.py index 2322740c69a..709b1842472 100644 --- a/tests/components/frontier_silicon/conftest.py +++ b/tests/components/frontier_silicon/conftest.py @@ -1,9 +1,9 @@ """Configuration for frontier_silicon tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.frontier_silicon.const import CONF_WEBFSAPI_URL, DOMAIN from homeassistant.const import CONF_PIN diff --git a/tests/components/fully_kiosk/conftest.py b/tests/components/fully_kiosk/conftest.py index 3f7c2985daf..028eefcf361 100644 --- a/tests/components/fully_kiosk/conftest.py +++ b/tests/components/fully_kiosk/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations +from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.fully_kiosk.const import DOMAIN from homeassistant.const import ( diff --git a/tests/components/fyta/conftest.py b/tests/components/fyta/conftest.py index de5dece776c..6a67ae75ec2 100644 --- a/tests/components/fyta/conftest.py +++ b/tests/components/fyta/conftest.py @@ -1,10 +1,10 @@ """Test helpers for FYTA.""" +from collections.abc import Generator from datetime import UTC, datetime from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.fyta.const import CONF_EXPIRATION, DOMAIN as FYTA_DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME From 921430d4970d6cfab50ec2621a1b877bd1d5be2a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Jul 2024 12:09:11 +0200 Subject: [PATCH 0525/2411] Import Generator from collections.abc (3) (#120916) --- tests/components/gardena_bluetooth/conftest.py | 3 +-- tests/components/geo_json_events/conftest.py | 2 +- tests/components/geocaching/conftest.py | 2 +- tests/components/github/conftest.py | 2 +- tests/components/google/conftest.py | 3 +-- tests/components/google_sheets/test_config_flow.py | 2 +- tests/components/google_tasks/test_config_flow.py | 2 +- tests/components/google_translate/conftest.py | 2 +- tests/components/google_translate/test_tts.py | 2 +- tests/components/govee_light_local/conftest.py | 2 +- tests/components/gpsd/conftest.py | 2 +- tests/components/gree/conftest.py | 2 +- tests/components/greeneye_monitor/conftest.py | 2 +- tests/components/guardian/conftest.py | 2 +- tests/components/harmony/conftest.py | 2 +- tests/components/hassio/test_addon_manager.py | 2 +- tests/components/holiday/conftest.py | 2 +- tests/components/homeassistant_hardware/conftest.py | 2 +- .../homeassistant_hardware/test_silabs_multiprotocol_addon.py | 2 +- tests/components/homeassistant_sky_connect/conftest.py | 2 +- tests/components/homeassistant_yellow/conftest.py | 2 +- tests/components/homeassistant_yellow/test_config_flow.py | 2 +- tests/components/homekit_controller/conftest.py | 2 +- tests/components/homewizard/conftest.py | 2 +- tests/components/homeworks/conftest.py | 2 +- tests/components/hunterdouglas_powerview/conftest.py | 2 +- tests/components/husqvarna_automower/conftest.py | 2 +- tests/components/idasen_desk/conftest.py | 3 +-- tests/components/image/conftest.py | 3 ++- tests/components/imap/conftest.py | 2 +- tests/components/imgw_pib/conftest.py | 2 +- tests/components/incomfort/conftest.py | 2 +- tests/components/influxdb/test_init.py | 2 +- tests/components/influxdb/test_sensor.py | 2 +- tests/components/intellifire/conftest.py | 2 +- tests/components/ipma/test_config_flow.py | 2 +- tests/components/ipp/conftest.py | 2 +- tests/components/islamic_prayer_times/conftest.py | 2 +- tests/components/ista_ecotrend/conftest.py | 2 +- tests/components/jellyfin/conftest.py | 2 +- tests/components/jewish_calendar/conftest.py | 2 +- tests/components/jvc_projector/conftest.py | 2 +- tests/components/kaleidescape/conftest.py | 2 +- tests/components/kitchen_sink/test_notify.py | 2 +- tests/components/kmtronic/conftest.py | 2 +- tests/components/knocki/conftest.py | 2 +- tests/components/kostal_plenticore/conftest.py | 2 +- tests/components/kostal_plenticore/test_config_flow.py | 2 +- tests/components/kostal_plenticore/test_helper.py | 2 +- tests/components/kostal_plenticore/test_number.py | 2 +- tests/components/lacrosse_view/conftest.py | 2 +- tests/components/lamarzocco/conftest.py | 3 +-- tests/components/lametric/conftest.py | 2 +- tests/components/landisgyr_heat_meter/conftest.py | 2 +- tests/components/lawn_mower/test_init.py | 2 +- tests/components/lidarr/conftest.py | 3 +-- tests/components/linear_garage_door/conftest.py | 2 +- tests/components/local_calendar/conftest.py | 3 +-- tests/components/local_todo/conftest.py | 2 +- tests/components/lock/conftest.py | 2 +- tests/components/loqed/conftest.py | 2 +- tests/components/lovelace/test_cast.py | 2 +- tests/components/lovelace/test_dashboard.py | 2 +- tests/components/lovelace/test_init.py | 2 +- tests/components/lovelace/test_system_health.py | 2 +- tests/components/luftdaten/conftest.py | 2 +- tests/components/lutron/conftest.py | 2 +- tests/components/map/test_init.py | 2 +- tests/components/matrix/conftest.py | 2 +- tests/components/matter/conftest.py | 2 +- tests/components/matter/test_binary_sensor.py | 2 +- tests/components/matter/test_config_flow.py | 2 +- tests/components/matter/test_init.py | 2 +- tests/components/mealie/conftest.py | 2 +- tests/components/media_extractor/conftest.py | 2 +- tests/components/media_source/test_local_source.py | 2 +- tests/components/melnor/conftest.py | 2 +- tests/components/mjpeg/conftest.py | 2 +- tests/components/moon/conftest.py | 2 +- tests/components/motionblinds_ble/conftest.py | 2 +- tests/components/motionmount/conftest.py | 2 +- tests/components/mqtt/conftest.py | 2 +- tests/components/mqtt/test_config_flow.py | 3 +-- tests/components/mqtt/test_tag.py | 2 +- tests/components/mqtt_json/test_device_tracker.py | 2 +- tests/components/mysensors/conftest.py | 3 +-- tests/components/mystrom/conftest.py | 2 +- tests/components/myuplink/conftest.py | 2 +- tests/components/nest/common.py | 3 +-- tests/components/nest/conftest.py | 2 +- tests/components/nest/test_camera.py | 2 +- tests/components/nest/test_init.py | 2 +- tests/components/nest/test_media_source.py | 2 +- tests/components/network/conftest.py | 2 +- tests/components/nextbus/test_config_flow.py | 2 +- tests/components/nextbus/test_sensor.py | 2 +- tests/components/nextcloud/conftest.py | 2 +- tests/components/nibe_heatpump/conftest.py | 2 +- tests/components/notify/conftest.py | 3 ++- tests/components/notion/conftest.py | 2 +- tests/components/number/test_init.py | 2 +- tests/components/obihai/conftest.py | 2 +- tests/components/onboarding/test_views.py | 2 +- tests/components/ondilo_ico/conftest.py | 2 +- tests/components/onewire/conftest.py | 2 +- tests/components/onewire/test_binary_sensor.py | 2 +- tests/components/onewire/test_diagnostics.py | 2 +- tests/components/onewire/test_sensor.py | 2 +- tests/components/onewire/test_switch.py | 2 +- tests/components/open_meteo/conftest.py | 2 +- tests/components/openexchangerates/conftest.py | 2 +- tests/components/openexchangerates/test_config_flow.py | 2 +- tests/components/opengarage/conftest.py | 2 +- tests/components/opensky/conftest.py | 2 +- tests/components/openuv/conftest.py | 2 +- tests/components/opower/test_config_flow.py | 2 +- tests/components/oralb/conftest.py | 2 +- tests/components/ourgroceries/conftest.py | 2 +- tests/components/overkiz/conftest.py | 2 +- tests/components/permobil/conftest.py | 2 +- tests/components/philips_js/conftest.py | 2 +- tests/components/ping/test_device_tracker.py | 2 +- tests/components/plex/conftest.py | 2 +- tests/components/plugwise/conftest.py | 2 +- tests/components/poolsense/conftest.py | 2 +- tests/components/prosegur/test_alarm_control_panel.py | 2 +- tests/components/ps4/conftest.py | 2 +- tests/components/pure_energie/conftest.py | 2 +- tests/components/pvoutput/conftest.py | 2 +- 129 files changed, 131 insertions(+), 138 deletions(-) diff --git a/tests/components/gardena_bluetooth/conftest.py b/tests/components/gardena_bluetooth/conftest.py index 08f698b4b67..882c9b1b090 100644 --- a/tests/components/gardena_bluetooth/conftest.py +++ b/tests/components/gardena_bluetooth/conftest.py @@ -1,6 +1,6 @@ """Common fixtures for the Gardena Bluetooth tests.""" -from collections.abc import Callable, Coroutine +from collections.abc import Callable, Coroutine, Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -10,7 +10,6 @@ from gardena_bluetooth.const import DeviceInformation from gardena_bluetooth.exceptions import CharacteristicNotFound from gardena_bluetooth.parse import Characteristic import pytest -from typing_extensions import Generator from homeassistant.components.gardena_bluetooth.const import DOMAIN from homeassistant.components.gardena_bluetooth.coordinator import SCAN_INTERVAL diff --git a/tests/components/geo_json_events/conftest.py b/tests/components/geo_json_events/conftest.py index beab7bf1403..11928e6f012 100644 --- a/tests/components/geo_json_events/conftest.py +++ b/tests/components/geo_json_events/conftest.py @@ -1,9 +1,9 @@ """Configuration for GeoJSON Events tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.geo_json_events import DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_URL diff --git a/tests/components/geocaching/conftest.py b/tests/components/geocaching/conftest.py index 155cd2c5a7e..28d87176e46 100644 --- a/tests/components/geocaching/conftest.py +++ b/tests/components/geocaching/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from geocachingapi import GeocachingStatus import pytest -from typing_extensions import Generator from homeassistant.components.geocaching.const import DOMAIN diff --git a/tests/components/github/conftest.py b/tests/components/github/conftest.py index df7de604c2c..ab262f3f522 100644 --- a/tests/components/github/conftest.py +++ b/tests/components/github/conftest.py @@ -1,9 +1,9 @@ """conftest for the GitHub integration.""" +from collections.abc import Generator from unittest.mock import patch import pytest -from typing_extensions import Generator from homeassistant.components.github.const import CONF_REPOSITORIES, DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 26a32a64b21..0f9f2a9395d 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import AsyncGenerator, Awaitable, Callable, Generator import datetime import http import time @@ -13,7 +13,6 @@ from aiohttp.client_exceptions import ClientError from gcal_sync.auth import API_BASE_URL from oauth2client.client import OAuth2Credentials import pytest -from typing_extensions import AsyncGenerator, Generator import yaml from homeassistant.components.application_credentials import ( diff --git a/tests/components/google_sheets/test_config_flow.py b/tests/components/google_sheets/test_config_flow.py index 0da046645d2..a504d8c4280 100644 --- a/tests/components/google_sheets/test_config_flow.py +++ b/tests/components/google_sheets/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Google Sheets config flow.""" +from collections.abc import Generator from unittest.mock import Mock, patch from gspread import GSpreadException import pytest -from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.application_credentials import ( diff --git a/tests/components/google_tasks/test_config_flow.py b/tests/components/google_tasks/test_config_flow.py index f2655afd602..f8ccc5e048f 100644 --- a/tests/components/google_tasks/test_config_flow.py +++ b/tests/components/google_tasks/test_config_flow.py @@ -1,11 +1,11 @@ """Test the Google Tasks config flow.""" +from collections.abc import Generator from unittest.mock import Mock, patch from googleapiclient.errors import HttpError from httplib2 import Response import pytest -from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.google_tasks.const import ( diff --git a/tests/components/google_translate/conftest.py b/tests/components/google_translate/conftest.py index 82f8d50b83c..aa84c201f0e 100644 --- a/tests/components/google_translate/conftest.py +++ b/tests/components/google_translate/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the Google Translate text-to-speech tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index d19b1269438..4d14c7e28cb 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator from http import HTTPStatus from pathlib import Path from typing import Any @@ -9,7 +10,6 @@ from unittest.mock import MagicMock, patch from gtts import gTTSError import pytest -from typing_extensions import Generator from homeassistant.components import tts from homeassistant.components.google_translate.const import CONF_TLD, DOMAIN diff --git a/tests/components/govee_light_local/conftest.py b/tests/components/govee_light_local/conftest.py index 90a9f8e6827..6a8ee99b764 100644 --- a/tests/components/govee_light_local/conftest.py +++ b/tests/components/govee_light_local/conftest.py @@ -1,11 +1,11 @@ """Tests configuration for Govee Local API.""" from asyncio import Event +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from govee_local_api import GoveeLightCapability import pytest -from typing_extensions import Generator from homeassistant.components.govee_light_local.coordinator import GoveeController diff --git a/tests/components/gpsd/conftest.py b/tests/components/gpsd/conftest.py index c323365e8fd..c15ef7f0258 100644 --- a/tests/components/gpsd/conftest.py +++ b/tests/components/gpsd/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the GPSD tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/gree/conftest.py b/tests/components/gree/conftest.py index 88bcaea33c2..a9e2fc9e5d4 100644 --- a/tests/components/gree/conftest.py +++ b/tests/components/gree/conftest.py @@ -1,9 +1,9 @@ """Pytest module configuration.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from .common import FakeDiscovery, build_device_mock diff --git a/tests/components/greeneye_monitor/conftest.py b/tests/components/greeneye_monitor/conftest.py index ad8a98ce3fe..343a15346e7 100644 --- a/tests/components/greeneye_monitor/conftest.py +++ b/tests/components/greeneye_monitor/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for testing greeneye_monitor.""" +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.greeneye_monitor import DOMAIN from homeassistant.components.sensor import SensorDeviceClass diff --git a/tests/components/guardian/conftest.py b/tests/components/guardian/conftest.py index 87ff96aff45..0063375f6ff 100644 --- a/tests/components/guardian/conftest.py +++ b/tests/components/guardian/conftest.py @@ -1,10 +1,10 @@ """Define fixtures for Elexa Guardian tests.""" +from collections.abc import Generator import json from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.guardian import CONF_UID, DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT diff --git a/tests/components/harmony/conftest.py b/tests/components/harmony/conftest.py index fb4be73aa72..759770e9746 100644 --- a/tests/components/harmony/conftest.py +++ b/tests/components/harmony/conftest.py @@ -1,10 +1,10 @@ """Fixtures for harmony tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from aioharmony.const import ClientCallbackType import pytest -from typing_extensions import Generator from homeassistant.components.harmony.const import ACTIVITY_POWER_OFF, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME diff --git a/tests/components/hassio/test_addon_manager.py b/tests/components/hassio/test_addon_manager.py index 55c663d66cc..6a20c6eec88 100644 --- a/tests/components/hassio/test_addon_manager.py +++ b/tests/components/hassio/test_addon_manager.py @@ -3,12 +3,12 @@ from __future__ import annotations import asyncio +from collections.abc import Generator import logging from typing import Any from unittest.mock import AsyncMock, call, patch import pytest -from typing_extensions import Generator from homeassistant.components.hassio.addon_manager import ( AddonError, diff --git a/tests/components/holiday/conftest.py b/tests/components/holiday/conftest.py index 1ac595aa1f9..005756695fe 100644 --- a/tests/components/holiday/conftest.py +++ b/tests/components/holiday/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the Holiday tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/homeassistant_hardware/conftest.py b/tests/components/homeassistant_hardware/conftest.py index 72e937396ea..d3dcd443a07 100644 --- a/tests/components/homeassistant_hardware/conftest.py +++ b/tests/components/homeassistant_hardware/conftest.py @@ -1,10 +1,10 @@ """Test fixtures for the Home Assistant Hardware integration.""" +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator @pytest.fixture(autouse=True) diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index 1df8fa86cf9..5718133cd24 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -2,11 +2,11 @@ from __future__ import annotations +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import Generator from homeassistant.components.hassio import AddonError, AddonInfo, AddonState, HassIO from homeassistant.components.hassio.handler import HassioAPIError diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index 099582999d5..69b0901aadf 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -1,9 +1,9 @@ """Test fixtures for the Home Assistant SkyConnect integration.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator @pytest.fixture(name="mock_usb_serial_by_id", autouse=True) diff --git a/tests/components/homeassistant_yellow/conftest.py b/tests/components/homeassistant_yellow/conftest.py index 38398eb719f..2e6340bf54a 100644 --- a/tests/components/homeassistant_yellow/conftest.py +++ b/tests/components/homeassistant_yellow/conftest.py @@ -1,10 +1,10 @@ """Test fixtures for the Home Assistant Yellow integration.""" +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator @pytest.fixture(autouse=True) diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 4ae04180a64..95d7df89c9d 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -1,9 +1,9 @@ """Test the Home Assistant Yellow config flow.""" +from collections.abc import Generator from unittest.mock import Mock, patch import pytest -from typing_extensions import Generator from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN from homeassistant.components.homeassistant_yellow.const import DOMAIN diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index 427c5285436..0c25e68f732 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -1,5 +1,6 @@ """HomeKit controller session fixtures.""" +from collections.abc import Generator import datetime from unittest.mock import MagicMock, patch @@ -7,7 +8,6 @@ from aiohomekit.testing import FakeController from freezegun import freeze_time from freezegun.api import FrozenDateTimeFactory import pytest -from typing_extensions import Generator import homeassistant.util.dt as dt_util diff --git a/tests/components/homewizard/conftest.py b/tests/components/homewizard/conftest.py index eb638492941..fcfe1e5c189 100644 --- a/tests/components/homewizard/conftest.py +++ b/tests/components/homewizard/conftest.py @@ -1,11 +1,11 @@ """Fixtures for HomeWizard integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from homewizard_energy.errors import NotFoundError from homewizard_energy.models import Data, Device, State, System import pytest -from typing_extensions import Generator from homeassistant.components.homewizard.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS diff --git a/tests/components/homeworks/conftest.py b/tests/components/homeworks/conftest.py index ca0e08e9215..86c3381b7a0 100644 --- a/tests/components/homeworks/conftest.py +++ b/tests/components/homeworks/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the Lutron Homeworks Series 4 and 8 tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.homeworks.const import ( CONF_ADDR, diff --git a/tests/components/hunterdouglas_powerview/conftest.py b/tests/components/hunterdouglas_powerview/conftest.py index da339914aac..f7adeb111b8 100644 --- a/tests/components/hunterdouglas_powerview/conftest.py +++ b/tests/components/hunterdouglas_powerview/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for Hunter Douglas Powerview tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from aiopvapi.resources.shade import ShadePosition import pytest -from typing_extensions import Generator from homeassistant.components.hunterdouglas_powerview.const import DOMAIN diff --git a/tests/components/husqvarna_automower/conftest.py b/tests/components/husqvarna_automower/conftest.py index 7ace3b76808..dbb8f3b4c72 100644 --- a/tests/components/husqvarna_automower/conftest.py +++ b/tests/components/husqvarna_automower/conftest.py @@ -1,5 +1,6 @@ """Test helpers for Husqvarna Automower.""" +from collections.abc import Generator import time from unittest.mock import AsyncMock, patch @@ -7,7 +8,6 @@ from aioautomower.session import AutomowerSession, _MowerCommands from aioautomower.utils import mower_list_to_dictionary_dataclass from aiohttp import ClientWebSocketResponse import pytest -from typing_extensions import Generator from homeassistant.components.application_credentials import ( ClientCredential, diff --git a/tests/components/idasen_desk/conftest.py b/tests/components/idasen_desk/conftest.py index 91f3f2de40e..24ef8311445 100644 --- a/tests/components/idasen_desk/conftest.py +++ b/tests/components/idasen_desk/conftest.py @@ -1,11 +1,10 @@ """IKEA Idasen Desk fixtures.""" -from collections.abc import Callable +from collections.abc import Callable, Generator from unittest import mock from unittest.mock import AsyncMock, MagicMock import pytest -from typing_extensions import Generator @pytest.fixture(autouse=True) diff --git a/tests/components/image/conftest.py b/tests/components/image/conftest.py index 65bbf2e0c4f..8bb5d19b6db 100644 --- a/tests/components/image/conftest.py +++ b/tests/components/image/conftest.py @@ -1,7 +1,8 @@ """Test helpers for image.""" +from collections.abc import Generator + import pytest -from typing_extensions import Generator from homeassistant.components import image from homeassistant.config_entries import ConfigEntry, ConfigFlow diff --git a/tests/components/imap/conftest.py b/tests/components/imap/conftest.py index 354c9fbe24e..87663031e7a 100644 --- a/tests/components/imap/conftest.py +++ b/tests/components/imap/conftest.py @@ -1,10 +1,10 @@ """Fixtures for imap tests.""" +from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch from aioimaplib import AUTH, LOGOUT, NONAUTH, SELECTED, STARTED, Response import pytest -from typing_extensions import AsyncGenerator, Generator from .const import EMPTY_SEARCH_RESPONSE, TEST_FETCH_RESPONSE_TEXT_PLAIN diff --git a/tests/components/imgw_pib/conftest.py b/tests/components/imgw_pib/conftest.py index 1d278856b5b..6f23ed3ee80 100644 --- a/tests/components/imgw_pib/conftest.py +++ b/tests/components/imgw_pib/conftest.py @@ -1,11 +1,11 @@ """Common fixtures for the IMGW-PIB tests.""" +from collections.abc import Generator from datetime import UTC, datetime from unittest.mock import AsyncMock, patch from imgw_pib import HydrologicalData, SensorData import pytest -from typing_extensions import Generator from homeassistant.components.imgw_pib.const import DOMAIN diff --git a/tests/components/incomfort/conftest.py b/tests/components/incomfort/conftest.py index 64885e38b65..122868605c8 100644 --- a/tests/components/incomfort/conftest.py +++ b/tests/components/incomfort/conftest.py @@ -1,11 +1,11 @@ """Fixtures for Intergas InComfort integration.""" +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from incomfortclient import DisplayCode import pytest -from typing_extensions import Generator from homeassistant.components.incomfort import DOMAIN from homeassistant.config_entries import ConfigEntry diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 2d93322999d..d7e06b5c101 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -1,5 +1,6 @@ """The tests for the InfluxDB component.""" +from collections.abc import Generator from dataclasses import dataclass import datetime from http import HTTPStatus @@ -7,7 +8,6 @@ import logging from unittest.mock import ANY, MagicMock, Mock, call, patch import pytest -from typing_extensions import Generator from homeassistant.components import influxdb from homeassistant.components.influxdb.const import DEFAULT_BUCKET diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index 48cae2a3ae6..73dd8375a00 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator from dataclasses import dataclass from datetime import timedelta from http import HTTPStatus @@ -10,7 +11,6 @@ from unittest.mock import MagicMock, patch from influxdb.exceptions import InfluxDBClientError, InfluxDBServerError from influxdb_client.rest import ApiException import pytest -from typing_extensions import Generator from voluptuous import Invalid from homeassistant.components import sensor diff --git a/tests/components/intellifire/conftest.py b/tests/components/intellifire/conftest.py index 1aae4fb6dd6..cf1e085c10f 100644 --- a/tests/components/intellifire/conftest.py +++ b/tests/components/intellifire/conftest.py @@ -1,10 +1,10 @@ """Fixtures for IntelliFire integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, Mock, patch from aiohttp.client_reqrep import ConnectionKey import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index 38bb1dbf126..eba11b02ca0 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -1,10 +1,10 @@ """Tests for IPMA config flow.""" +from collections.abc import Generator from unittest.mock import patch from pyipma import IPMAException import pytest -from typing_extensions import Generator from homeassistant.components.ipma.const import DOMAIN from homeassistant.config_entries import SOURCE_USER diff --git a/tests/components/ipp/conftest.py b/tests/components/ipp/conftest.py index 5e39a16f3b1..9a47cc3c355 100644 --- a/tests/components/ipp/conftest.py +++ b/tests/components/ipp/conftest.py @@ -1,11 +1,11 @@ """Fixtures for IPP integration tests.""" +from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch from pyipp import Printer import pytest -from typing_extensions import Generator from homeassistant.components.ipp.const import CONF_BASE_PATH, DOMAIN from homeassistant.const import ( diff --git a/tests/components/islamic_prayer_times/conftest.py b/tests/components/islamic_prayer_times/conftest.py index ae9b1f45eb9..ae0b6741fdf 100644 --- a/tests/components/islamic_prayer_times/conftest.py +++ b/tests/components/islamic_prayer_times/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the islamic_prayer_times tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/ista_ecotrend/conftest.py b/tests/components/ista_ecotrend/conftest.py index 2218ef05ba7..cbbc166031d 100644 --- a/tests/components/ista_ecotrend/conftest.py +++ b/tests/components/ista_ecotrend/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the ista EcoTrend tests.""" +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.ista_ecotrend.const import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD diff --git a/tests/components/jellyfin/conftest.py b/tests/components/jellyfin/conftest.py index 40d03212ceb..c3732714177 100644 --- a/tests/components/jellyfin/conftest.py +++ b/tests/components/jellyfin/conftest.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, create_autospec, patch from jellyfin_apiclient_python import JellyfinClient @@ -9,7 +10,6 @@ from jellyfin_apiclient_python.api import API from jellyfin_apiclient_python.configuration import Config from jellyfin_apiclient_python.connection_manager import ConnectionManager import pytest -from typing_extensions import Generator from homeassistant.components.jellyfin.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME diff --git a/tests/components/jewish_calendar/conftest.py b/tests/components/jewish_calendar/conftest.py index 5e16289f473..97909291f27 100644 --- a/tests/components/jewish_calendar/conftest.py +++ b/tests/components/jewish_calendar/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the jewish_calendar tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.jewish_calendar.const import DEFAULT_NAME, DOMAIN diff --git a/tests/components/jvc_projector/conftest.py b/tests/components/jvc_projector/conftest.py index dd012d3f355..3115cbfe252 100644 --- a/tests/components/jvc_projector/conftest.py +++ b/tests/components/jvc_projector/conftest.py @@ -1,9 +1,9 @@ """Fixtures for JVC Projector integration.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.jvc_projector.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT diff --git a/tests/components/kaleidescape/conftest.py b/tests/components/kaleidescape/conftest.py index 5cd2a8ebb18..e5aeedc3895 100644 --- a/tests/components/kaleidescape/conftest.py +++ b/tests/components/kaleidescape/conftest.py @@ -1,11 +1,11 @@ """Fixtures for Kaleidescape integration.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch from kaleidescape import Dispatcher from kaleidescape.device import Automation, Movie, Power, System import pytest -from typing_extensions import Generator from homeassistant.components.kaleidescape.const import DOMAIN from homeassistant.const import CONF_HOST diff --git a/tests/components/kitchen_sink/test_notify.py b/tests/components/kitchen_sink/test_notify.py index df025087b6b..12e19ffaa49 100644 --- a/tests/components/kitchen_sink/test_notify.py +++ b/tests/components/kitchen_sink/test_notify.py @@ -1,10 +1,10 @@ """The tests for the demo button component.""" +from collections.abc import AsyncGenerator from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest -from typing_extensions import AsyncGenerator from homeassistant.components.kitchen_sink import DOMAIN from homeassistant.components.notify import ( diff --git a/tests/components/kmtronic/conftest.py b/tests/components/kmtronic/conftest.py index 5dc349508e3..11abd2a4d7b 100644 --- a/tests/components/kmtronic/conftest.py +++ b/tests/components/kmtronic/conftest.py @@ -1,9 +1,9 @@ """Define fixtures for kmtronic tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/knocki/conftest.py b/tests/components/knocki/conftest.py index e1bc2e29cde..2fae89c730d 100644 --- a/tests/components/knocki/conftest.py +++ b/tests/components/knocki/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Knocki tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch from knocki import TokenResponse, Trigger import pytest -from typing_extensions import Generator from homeassistant.components.knocki.const import DOMAIN from homeassistant.const import CONF_TOKEN diff --git a/tests/components/kostal_plenticore/conftest.py b/tests/components/kostal_plenticore/conftest.py index af958f19f3a..acce8ebed7a 100644 --- a/tests/components/kostal_plenticore/conftest.py +++ b/tests/components/kostal_plenticore/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from pykoplenti import MeData, VersionData import pytest -from typing_extensions import Generator from homeassistant.components.kostal_plenticore.coordinator import Plenticore from homeassistant.core import HomeAssistant diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index c982e2af818..bd9b9ad278d 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Kostal Plenticore Solar Inverter config flow.""" +from collections.abc import Generator from unittest.mock import ANY, AsyncMock, MagicMock, patch from pykoplenti import ApiClient, AuthenticationException, SettingsData import pytest -from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.kostal_plenticore.const import DOMAIN diff --git a/tests/components/kostal_plenticore/test_helper.py b/tests/components/kostal_plenticore/test_helper.py index a18cf32c5a1..acd33f82a27 100644 --- a/tests/components/kostal_plenticore/test_helper.py +++ b/tests/components/kostal_plenticore/test_helper.py @@ -1,10 +1,10 @@ """Test Kostal Plenticore helper.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from pykoplenti import ApiClient, ExtendedApiClient, SettingsData import pytest -from typing_extensions import Generator from homeassistant.components.kostal_plenticore.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py index 9d94c6f9951..586129c486d 100644 --- a/tests/components/kostal_plenticore/test_number.py +++ b/tests/components/kostal_plenticore/test_number.py @@ -1,11 +1,11 @@ """Test Kostal Plenticore number.""" +from collections.abc import Generator from datetime import timedelta from unittest.mock import patch from pykoplenti import ApiClient, SettingsData import pytest -from typing_extensions import Generator from homeassistant.components.number import ( ATTR_MAX, diff --git a/tests/components/lacrosse_view/conftest.py b/tests/components/lacrosse_view/conftest.py index a6294c64210..4f1bfdc5748 100644 --- a/tests/components/lacrosse_view/conftest.py +++ b/tests/components/lacrosse_view/conftest.py @@ -1,9 +1,9 @@ """Define fixtures for LaCrosse View tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index 6741ac0797c..e7a527a2b14 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -1,6 +1,6 @@ """Lamarzocco session fixtures.""" -from collections.abc import Callable +from collections.abc import Callable, Generator import json from unittest.mock import MagicMock, patch @@ -9,7 +9,6 @@ from lmcloud.const import FirmwareType, MachineModel, SteamLevel from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.models import LaMarzoccoDeviceInfo import pytest -from typing_extensions import Generator from homeassistant.components.lamarzocco.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME, CONF_TOKEN diff --git a/tests/components/lametric/conftest.py b/tests/components/lametric/conftest.py index dd3885b78d9..e8ba727f3db 100644 --- a/tests/components/lametric/conftest.py +++ b/tests/components/lametric/conftest.py @@ -2,12 +2,12 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from demetriek import CloudDevice, Device from pydantic import parse_raw_as # pylint: disable=no-name-in-module import pytest -from typing_extensions import Generator from homeassistant.components.application_credentials import ( ClientCredential, diff --git a/tests/components/landisgyr_heat_meter/conftest.py b/tests/components/landisgyr_heat_meter/conftest.py index 22f29b3a4b1..1dad983c909 100644 --- a/tests/components/landisgyr_heat_meter/conftest.py +++ b/tests/components/landisgyr_heat_meter/conftest.py @@ -1,9 +1,9 @@ """Define fixtures for Landis + Gyr Heat Meter tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/lawn_mower/test_init.py b/tests/components/lawn_mower/test_init.py index e7066ed43c1..16f32da7e04 100644 --- a/tests/components/lawn_mower/test_init.py +++ b/tests/components/lawn_mower/test_init.py @@ -1,9 +1,9 @@ """The tests for the lawn mower integration.""" +from collections.abc import Generator from unittest.mock import MagicMock import pytest -from typing_extensions import Generator from homeassistant.components.lawn_mower import ( DOMAIN as LAWN_MOWER_DOMAIN, diff --git a/tests/components/lidarr/conftest.py b/tests/components/lidarr/conftest.py index 588acb2b87f..1024aadc403 100644 --- a/tests/components/lidarr/conftest.py +++ b/tests/components/lidarr/conftest.py @@ -2,13 +2,12 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator from http import HTTPStatus from aiohttp.client_exceptions import ClientError from aiopyarr.lidarr_client import LidarrClient import pytest -from typing_extensions import Generator from homeassistant.components.lidarr.const import DOMAIN from homeassistant.const import ( diff --git a/tests/components/linear_garage_door/conftest.py b/tests/components/linear_garage_door/conftest.py index 306da23ebf9..4ed7662e5d0 100644 --- a/tests/components/linear_garage_door/conftest.py +++ b/tests/components/linear_garage_door/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the Linear Garage Door tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.linear_garage_door import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD diff --git a/tests/components/local_calendar/conftest.py b/tests/components/local_calendar/conftest.py index 6d2c38544a5..8aef73a9d5a 100644 --- a/tests/components/local_calendar/conftest.py +++ b/tests/components/local_calendar/conftest.py @@ -1,6 +1,6 @@ """Fixtures for local calendar.""" -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator from http import HTTPStatus from pathlib import Path from typing import Any @@ -9,7 +9,6 @@ import urllib from aiohttp import ClientWebSocketResponse import pytest -from typing_extensions import Generator from homeassistant.components.local_calendar import LocalCalendarStore from homeassistant.components.local_calendar.const import CONF_CALENDAR_NAME, DOMAIN diff --git a/tests/components/local_todo/conftest.py b/tests/components/local_todo/conftest.py index 67ef76172b7..ab73dabb474 100644 --- a/tests/components/local_todo/conftest.py +++ b/tests/components/local_todo/conftest.py @@ -1,11 +1,11 @@ """Common fixtures for the local_todo tests.""" +from collections.abc import Generator from pathlib import Path from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import Generator from homeassistant.components.local_todo import LocalTodoListStore from homeassistant.components.local_todo.const import ( diff --git a/tests/components/lock/conftest.py b/tests/components/lock/conftest.py index f1715687339..fd569b162bc 100644 --- a/tests/components/lock/conftest.py +++ b/tests/components/lock/conftest.py @@ -1,10 +1,10 @@ """Fixtures for the lock entity platform tests.""" +from collections.abc import Generator from typing import Any from unittest.mock import MagicMock import pytest -from typing_extensions import Generator from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, diff --git a/tests/components/loqed/conftest.py b/tests/components/loqed/conftest.py index 57ef19d0fcb..ddad8949d7d 100644 --- a/tests/components/loqed/conftest.py +++ b/tests/components/loqed/conftest.py @@ -1,12 +1,12 @@ """Contains fixtures for Loqed tests.""" +from collections.abc import AsyncGenerator import json from typing import Any from unittest.mock import AsyncMock, Mock, patch from loqedAPI import loqed import pytest -from typing_extensions import AsyncGenerator from homeassistant.components.loqed import DOMAIN from homeassistant.components.loqed.const import CONF_CLOUDHOOK_URL diff --git a/tests/components/lovelace/test_cast.py b/tests/components/lovelace/test_cast.py index 632ea731d0c..3d6710d22d1 100644 --- a/tests/components/lovelace/test_cast.py +++ b/tests/components/lovelace/test_cast.py @@ -1,10 +1,10 @@ """Test the Lovelace Cast platform.""" +from collections.abc import Generator from time import time from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.lovelace import cast as lovelace_cast from homeassistant.components.media_player import MediaClass diff --git a/tests/components/lovelace/test_dashboard.py b/tests/components/lovelace/test_dashboard.py index 7577c4dcc0d..3a01e20c1fb 100644 --- a/tests/components/lovelace/test_dashboard.py +++ b/tests/components/lovelace/test_dashboard.py @@ -1,11 +1,11 @@ """Test the Lovelace initialization.""" +from collections.abc import Generator import time from typing import Any from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components import frontend from homeassistant.components.lovelace import const, dashboard diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index dc111ab601e..14d93d8302f 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -1,10 +1,10 @@ """Test the Lovelace initialization.""" +from collections.abc import Generator from typing import Any from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/lovelace/test_system_health.py b/tests/components/lovelace/test_system_health.py index d53ebf2871f..4fe248fa950 100644 --- a/tests/components/lovelace/test_system_health.py +++ b/tests/components/lovelace/test_system_health.py @@ -1,10 +1,10 @@ """Tests for Lovelace system health.""" +from collections.abc import Generator from typing import Any from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.lovelace import dashboard from homeassistant.core import HomeAssistant diff --git a/tests/components/luftdaten/conftest.py b/tests/components/luftdaten/conftest.py index e1aac7caeb0..c3daa390e49 100644 --- a/tests/components/luftdaten/conftest.py +++ b/tests/components/luftdaten/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.luftdaten.const import CONF_SENSOR_ID, DOMAIN from homeassistant.const import CONF_SHOW_ON_MAP diff --git a/tests/components/lutron/conftest.py b/tests/components/lutron/conftest.py index 90f96f1783d..f2106f736dc 100644 --- a/tests/components/lutron/conftest.py +++ b/tests/components/lutron/conftest.py @@ -1,9 +1,9 @@ """Provide common Lutron fixtures and mocks.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/map/test_init.py b/tests/components/map/test_init.py index afafdd1eb16..217550852bd 100644 --- a/tests/components/map/test_init.py +++ b/tests/components/map/test_init.py @@ -1,10 +1,10 @@ """Test the Map initialization.""" +from collections.abc import Generator from typing import Any from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.map import DOMAIN from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant diff --git a/tests/components/matrix/conftest.py b/tests/components/matrix/conftest.py index bb5448a8a09..11347302177 100644 --- a/tests/components/matrix/conftest.py +++ b/tests/components/matrix/conftest.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator from pathlib import Path import re import tempfile @@ -24,7 +25,6 @@ from nio import ( ) from PIL import Image import pytest -from typing_extensions import Generator from homeassistant.components.matrix import ( CONF_COMMANDS, diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 05fd776e57a..d561f6db1f9 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -3,13 +3,13 @@ from __future__ import annotations import asyncio +from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch from matter_server.client.models.node import MatterNode from matter_server.common.const import SCHEMA_VERSION from matter_server.common.models import ServerInfoMessage import pytest -from typing_extensions import AsyncGenerator, Generator from homeassistant.core import HomeAssistant diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py index becedc0af62..f419a12c59f 100644 --- a/tests/components/matter/test_binary_sensor.py +++ b/tests/components/matter/test_binary_sensor.py @@ -1,10 +1,10 @@ """Test Matter binary sensors.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch from matter_server.client.models.node import MatterNode import pytest -from typing_extensions import Generator from homeassistant.components.matter.binary_sensor import ( DISCOVERY_SCHEMAS as BINARY_SENSOR_SCHEMAS, diff --git a/tests/components/matter/test_config_flow.py b/tests/components/matter/test_config_flow.py index 562cf4bb86a..642bfe0f804 100644 --- a/tests/components/matter/test_config_flow.py +++ b/tests/components/matter/test_config_flow.py @@ -2,13 +2,13 @@ from __future__ import annotations +from collections.abc import Generator from ipaddress import ip_address from typing import Any from unittest.mock import DEFAULT, AsyncMock, MagicMock, call, patch from matter_server.client.exceptions import CannotConnect, InvalidServerVersion import pytest -from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.hassio import HassioAPIError, HassioServiceInfo diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index c28385efca3..cd5ef307cd3 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, call, patch from matter_server.client.exceptions import ( @@ -15,7 +16,6 @@ from matter_server.common.errors import MatterError from matter_server.common.helpers.util import dataclass_from_dict from matter_server.common.models import MatterNodeData import pytest -from typing_extensions import Generator from homeassistant.components.hassio import HassioAPIError from homeassistant.components.matter.const import DOMAIN diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py index 9bda9e3c46d..ebcafcce5b5 100644 --- a/tests/components/mealie/conftest.py +++ b/tests/components/mealie/conftest.py @@ -1,11 +1,11 @@ """Mealie tests configuration.""" +from collections.abc import Generator from unittest.mock import patch from aiomealie import Mealplan, MealplanResponse, UserInfo from mashumaro.codecs.orjson import ORJSONDecoder import pytest -from typing_extensions import Generator from homeassistant.components.mealie.const import DOMAIN from homeassistant.const import CONF_API_TOKEN, CONF_HOST diff --git a/tests/components/media_extractor/conftest.py b/tests/components/media_extractor/conftest.py index 1d198681f3f..45b3bb698e0 100644 --- a/tests/components/media_extractor/conftest.py +++ b/tests/components/media_extractor/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Media Extractor tests.""" +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.media_extractor import DOMAIN from homeassistant.core import HomeAssistant, ServiceCall diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index 4c7fbd06edc..de90f229a85 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -1,5 +1,6 @@ """Test Local Media Source.""" +from collections.abc import AsyncGenerator from http import HTTPStatus import io from pathlib import Path @@ -7,7 +8,6 @@ from tempfile import TemporaryDirectory from unittest.mock import patch import pytest -from typing_extensions import AsyncGenerator from homeassistant.components import media_source, websocket_api from homeassistant.components.media_source import const diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py index 38bc1a62d51..f30213c4efd 100644 --- a/tests/components/melnor/conftest.py +++ b/tests/components/melnor/conftest.py @@ -2,12 +2,12 @@ from __future__ import annotations +from collections.abc import Generator from datetime import UTC, datetime, time, timedelta from unittest.mock import AsyncMock, _patch, patch from melnor_bluetooth.device import Device import pytest -from typing_extensions import Generator from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak from homeassistant.components.melnor.const import DOMAIN diff --git a/tests/components/mjpeg/conftest.py b/tests/components/mjpeg/conftest.py index 00eaf946113..12e0b4c0faf 100644 --- a/tests/components/mjpeg/conftest.py +++ b/tests/components/mjpeg/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest from requests_mock import Mocker -from typing_extensions import Generator from homeassistant.components.mjpeg.const import ( CONF_MJPEG_URL, diff --git a/tests/components/moon/conftest.py b/tests/components/moon/conftest.py index 6fa54fcb603..3cf0eb1afc3 100644 --- a/tests/components/moon/conftest.py +++ b/tests/components/moon/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import patch import pytest -from typing_extensions import Generator from homeassistant.components.moon.const import DOMAIN diff --git a/tests/components/motionblinds_ble/conftest.py b/tests/components/motionblinds_ble/conftest.py index 342e958eae4..00db23734dd 100644 --- a/tests/components/motionblinds_ble/conftest.py +++ b/tests/components/motionblinds_ble/conftest.py @@ -1,9 +1,9 @@ """Setup the Motionblinds Bluetooth tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import Generator TEST_MAC = "abcd" TEST_NAME = f"MOTION_{TEST_MAC.upper()}" diff --git a/tests/components/motionmount/conftest.py b/tests/components/motionmount/conftest.py index 9e5b0355387..49f624b5266 100644 --- a/tests/components/motionmount/conftest.py +++ b/tests/components/motionmount/conftest.py @@ -1,9 +1,9 @@ """Fixtures for Vogel's MotionMount integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.motionmount.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index 774785bb42a..7395767aeae 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -1,12 +1,12 @@ """Test fixtures for mqtt component.""" import asyncio +from collections.abc import AsyncGenerator, Generator from random import getrandbits from typing import Any from unittest.mock import patch import pytest -from typing_extensions import AsyncGenerator, Generator from homeassistant.components import mqtt from homeassistant.components.mqtt.models import MessageCallbackType, ReceiveMessage diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 457bd19c16f..38dfdefcf97 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1,6 +1,6 @@ """Test config flow.""" -from collections.abc import Iterator +from collections.abc import Generator, Iterator from contextlib import contextmanager from pathlib import Path from ssl import SSLError @@ -9,7 +9,6 @@ from unittest.mock import AsyncMock, MagicMock, patch from uuid import uuid4 import pytest -from typing_extensions import Generator import voluptuous as vol from homeassistant import config_entries diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 4cf0606deb8..adebd157588 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -1,11 +1,11 @@ """The tests for MQTT tag scanner.""" +from collections.abc import Generator import copy import json from unittest.mock import ANY, AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index a992c985057..36073c11a5d 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -1,12 +1,12 @@ """The tests for the JSON MQTT device tracker platform.""" +from collections.abc import AsyncGenerator import json import logging import os from unittest.mock import patch import pytest -from typing_extensions import AsyncGenerator from homeassistant.components.device_tracker.legacy import ( DOMAIN as DT_DOMAIN, diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index f1b86c9ce5b..b6fce35a4c7 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import AsyncGenerator, Callable, Generator from copy import deepcopy import json from typing import Any @@ -12,7 +12,6 @@ from mysensors import BaseSyncGateway from mysensors.persistence import MySensorsJSONDecoder from mysensors.sensor import Sensor import pytest -from typing_extensions import AsyncGenerator, Generator from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mysensors.config_flow import DEFAULT_BAUD_RATE diff --git a/tests/components/mystrom/conftest.py b/tests/components/mystrom/conftest.py index f5405055805..af8d80ed27e 100644 --- a/tests/components/mystrom/conftest.py +++ b/tests/components/mystrom/conftest.py @@ -1,9 +1,9 @@ """Provide common mystrom fixtures and mocks.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.mystrom.const import DOMAIN from homeassistant.const import CONF_HOST diff --git a/tests/components/myuplink/conftest.py b/tests/components/myuplink/conftest.py index dd05bedcaf4..9ede11146ef 100644 --- a/tests/components/myuplink/conftest.py +++ b/tests/components/myuplink/conftest.py @@ -1,5 +1,6 @@ """Test helpers for myuplink.""" +from collections.abc import AsyncGenerator, Generator import time from typing import Any from unittest.mock import MagicMock, patch @@ -7,7 +8,6 @@ from unittest.mock import MagicMock, patch from myuplink import Device, DevicePoint, System import orjson import pytest -from typing_extensions import AsyncGenerator, Generator from homeassistant.components.application_credentials import ( ClientCredential, diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index bbaa92b7b28..0a553f9c114 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -2,7 +2,7 @@ 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 @@ -14,7 +14,6 @@ from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.event import EventMessage from google_nest_sdm.event_media import CachePolicy from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber -from typing_extensions import Generator from homeassistant.components.application_credentials import ClientCredential from homeassistant.components.nest import DOMAIN diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index de0fc2079fa..4c78bf4c27b 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -3,6 +3,7 @@ from __future__ import annotations from asyncio import AbstractEventLoop +from collections.abc import Generator import copy import shutil import time @@ -15,7 +16,6 @@ from google_nest_sdm import diagnostics from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.device_manager import DeviceManager import pytest -from typing_extensions import Generator from homeassistant.components.application_credentials import ( async_import_client_credential, diff --git a/tests/components/nest/test_camera.py b/tests/components/nest/test_camera.py index 1838c18b6d4..fd2b5ef0388 100644 --- a/tests/components/nest/test_camera.py +++ b/tests/components/nest/test_camera.py @@ -4,6 +4,7 @@ These tests fake out the subscriber/devicemanager, and are not using a real pubsub subscriber. """ +from collections.abc import Generator import datetime from http import HTTPStatus from unittest.mock import AsyncMock, Mock, patch @@ -12,7 +13,6 @@ import aiohttp from freezegun import freeze_time from google_nest_sdm.event import EventMessage import pytest -from typing_extensions import Generator from homeassistant.components import camera from homeassistant.components.camera import STATE_IDLE, STATE_STREAMING, StreamType diff --git a/tests/components/nest/test_init.py b/tests/components/nest/test_init.py index f9813ca63ee..2beed07a979 100644 --- a/tests/components/nest/test_init.py +++ b/tests/components/nest/test_init.py @@ -8,6 +8,7 @@ mode (e.g. yaml, ConfigEntry, etc) however some tests override and just run in relevant modes. """ +from collections.abc import Generator import logging from typing import Any from unittest.mock import patch @@ -19,7 +20,6 @@ from google_nest_sdm.exceptions import ( SubscriberException, ) import pytest -from typing_extensions import Generator from homeassistant.components.nest import DOMAIN from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index f4fb8bdb623..3cfa4ee6687 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -4,6 +4,7 @@ These tests simulate recent camera events received by the subscriber exposed as media in the media source. """ +from collections.abc import Generator import datetime from http import HTTPStatus import io @@ -15,7 +16,6 @@ import av from google_nest_sdm.event import EventMessage import numpy as np import pytest -from typing_extensions import Generator from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source import ( diff --git a/tests/components/network/conftest.py b/tests/components/network/conftest.py index 36d9c449d27..d5fbb95a814 100644 --- a/tests/components/network/conftest.py +++ b/tests/components/network/conftest.py @@ -1,9 +1,9 @@ """Tests for the Network Configuration integration.""" +from collections.abc import Generator from unittest.mock import _patch import pytest -from typing_extensions import Generator @pytest.fixture(autouse=True) diff --git a/tests/components/nextbus/test_config_flow.py b/tests/components/nextbus/test_config_flow.py index 0a64bc97d9a..da8e47ff3e8 100644 --- a/tests/components/nextbus/test_config_flow.py +++ b/tests/components/nextbus/test_config_flow.py @@ -1,9 +1,9 @@ """Test the NextBus config flow.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant import config_entries, setup from homeassistant.components.nextbus.const import CONF_AGENCY, CONF_ROUTE, DOMAIN diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index 3630ff88855..7cdcd58937a 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -1,12 +1,12 @@ """The tests for the nexbus sensor component.""" +from collections.abc import Generator from copy import deepcopy from unittest.mock import MagicMock, patch from urllib.error import HTTPError from py_nextbus.client import NextBusFormatError, NextBusHTTPError import pytest -from typing_extensions import Generator from homeassistant.components import sensor from homeassistant.components.nextbus.const import CONF_AGENCY, CONF_ROUTE, DOMAIN diff --git a/tests/components/nextcloud/conftest.py b/tests/components/nextcloud/conftest.py index d6cd39e7fc8..cf3eda55fe1 100644 --- a/tests/components/nextcloud/conftest.py +++ b/tests/components/nextcloud/conftest.py @@ -1,9 +1,9 @@ """Fixtrues for the Nextcloud integration tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/nibe_heatpump/conftest.py b/tests/components/nibe_heatpump/conftest.py index c44875414e2..47b65772a24 100644 --- a/tests/components/nibe_heatpump/conftest.py +++ b/tests/components/nibe_heatpump/conftest.py @@ -1,12 +1,12 @@ """Test configuration for Nibe Heat Pump.""" +from collections.abc import Generator from contextlib import ExitStack from unittest.mock import AsyncMock, Mock, patch from freezegun.api import FrozenDateTimeFactory from nibe.exceptions import CoilNotFoundException import pytest -from typing_extensions import Generator from homeassistant.core import HomeAssistant diff --git a/tests/components/notify/conftest.py b/tests/components/notify/conftest.py index 0efb3a4689d..91dc92a27fe 100644 --- a/tests/components/notify/conftest.py +++ b/tests/components/notify/conftest.py @@ -1,7 +1,8 @@ """Fixtures for Notify platform tests.""" +from collections.abc import Generator + import pytest -from typing_extensions import Generator from homeassistant.config_entries import ConfigFlow from homeassistant.core import HomeAssistant diff --git a/tests/components/notion/conftest.py b/tests/components/notion/conftest.py index 17bea306ad8..6a6e150c960 100644 --- a/tests/components/notion/conftest.py +++ b/tests/components/notion/conftest.py @@ -1,5 +1,6 @@ """Define fixtures for Notion tests.""" +from collections.abc import Generator import json from unittest.mock import AsyncMock, Mock, patch @@ -8,7 +9,6 @@ from aionotion.listener.models import Listener from aionotion.sensor.models import Sensor from aionotion.user.models import UserPreferences import pytest -from typing_extensions import Generator from homeassistant.components.notion import CONF_REFRESH_TOKEN, CONF_USER_UUID, DOMAIN from homeassistant.const import CONF_USERNAME diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index aa5df5d737f..55dad2506f1 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -1,10 +1,10 @@ """The tests for the Number component.""" +from collections.abc import Generator from typing import Any from unittest.mock import MagicMock import pytest -from typing_extensions import Generator from homeassistant.components.number import ( ATTR_MAX, diff --git a/tests/components/obihai/conftest.py b/tests/components/obihai/conftest.py index c4edfdedf65..ef54c12ba26 100644 --- a/tests/components/obihai/conftest.py +++ b/tests/components/obihai/conftest.py @@ -1,10 +1,10 @@ """Define test fixtures for Obihai.""" +from collections.abc import Generator from socket import gaierror from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index e9ba720adb3..3b6e6f5dbe3 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -1,13 +1,13 @@ """Test the onboarding views.""" import asyncio +from collections.abc import AsyncGenerator from http import HTTPStatus import os from typing import Any from unittest.mock import Mock, patch import pytest -from typing_extensions import AsyncGenerator from homeassistant.components import onboarding from homeassistant.components.onboarding import const, views diff --git a/tests/components/ondilo_ico/conftest.py b/tests/components/ondilo_ico/conftest.py index 6a03d6961c2..a847c1df069 100644 --- a/tests/components/ondilo_ico/conftest.py +++ b/tests/components/ondilo_ico/conftest.py @@ -1,10 +1,10 @@ """Provide basic Ondilo fixture.""" +from collections.abc import Generator from typing import Any from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.ondilo_ico.const import DOMAIN diff --git a/tests/components/onewire/conftest.py b/tests/components/onewire/conftest.py index 47b50ab10e0..65a86b58f2f 100644 --- a/tests/components/onewire/conftest.py +++ b/tests/components/onewire/conftest.py @@ -1,10 +1,10 @@ """Provide common 1-Wire fixtures.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from pyownet.protocol import ConnError import pytest -from typing_extensions import Generator from homeassistant.components.onewire.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntry diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index 8b1129529d5..31895f705ff 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -1,10 +1,10 @@ """Tests for 1-Wire binary sensors.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform diff --git a/tests/components/onewire/test_diagnostics.py b/tests/components/onewire/test_diagnostics.py index 62b045c4516..ecdae859597 100644 --- a/tests/components/onewire/test_diagnostics.py +++ b/tests/components/onewire/test_diagnostics.py @@ -1,10 +1,10 @@ """Test 1-Wire diagnostics.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index df0a81920c9..ba0e21701f8 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -1,5 +1,6 @@ """Tests for 1-Wire sensors.""" +from collections.abc import Generator from copy import deepcopy import logging from unittest.mock import MagicMock, _patch_dict, patch @@ -7,7 +8,6 @@ from unittest.mock import MagicMock, _patch_dict, patch from pyownet.protocol import OwnetError import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index b1b8e5ddbd0..936e83f66ec 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -1,10 +1,10 @@ """Tests for 1-Wire switches.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest from syrupy.assertion import SnapshotAssertion -from typing_extensions import Generator from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry diff --git a/tests/components/open_meteo/conftest.py b/tests/components/open_meteo/conftest.py index 0d3e1274693..22138846915 100644 --- a/tests/components/open_meteo/conftest.py +++ b/tests/components/open_meteo/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import MagicMock, patch from open_meteo import Forecast import pytest -from typing_extensions import Generator from homeassistant.components.open_meteo.const import DOMAIN from homeassistant.const import CONF_ZONE diff --git a/tests/components/openexchangerates/conftest.py b/tests/components/openexchangerates/conftest.py index 6bd7da2c7af..770432ebac3 100644 --- a/tests/components/openexchangerates/conftest.py +++ b/tests/components/openexchangerates/conftest.py @@ -1,9 +1,9 @@ """Provide common fixtures for tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.openexchangerates.const import DOMAIN diff --git a/tests/components/openexchangerates/test_config_flow.py b/tests/components/openexchangerates/test_config_flow.py index 30ea619d646..ec06c662201 100644 --- a/tests/components/openexchangerates/test_config_flow.py +++ b/tests/components/openexchangerates/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Open Exchange Rates config flow.""" import asyncio +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, patch @@ -9,7 +10,6 @@ from aioopenexchangerates import ( OpenExchangeRatesClientError, ) import pytest -from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.openexchangerates.const import DOMAIN diff --git a/tests/components/opengarage/conftest.py b/tests/components/opengarage/conftest.py index c960e723289..2367692096b 100644 --- a/tests/components/opengarage/conftest.py +++ b/tests/components/opengarage/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.opengarage.const import CONF_DEVICE_KEY, DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT, CONF_VERIFY_SSL diff --git a/tests/components/opensky/conftest.py b/tests/components/opensky/conftest.py index c48f3bec8d8..4664c48ef9e 100644 --- a/tests/components/opensky/conftest.py +++ b/tests/components/opensky/conftest.py @@ -1,10 +1,10 @@ """Configure tests for the OpenSky integration.""" +from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, patch import pytest from python_opensky import StatesResponse -from typing_extensions import AsyncGenerator, Generator from homeassistant.components.opensky.const import ( CONF_ALTITUDE, diff --git a/tests/components/openuv/conftest.py b/tests/components/openuv/conftest.py index 69563c94c64..cc344d25ccb 100644 --- a/tests/components/openuv/conftest.py +++ b/tests/components/openuv/conftest.py @@ -1,10 +1,10 @@ """Define test fixtures for OpenUV.""" +from collections.abc import Generator import json from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import Generator from homeassistant.components.openuv import CONF_FROM_WINDOW, CONF_TO_WINDOW, DOMAIN from homeassistant.const import ( diff --git a/tests/components/opower/test_config_flow.py b/tests/components/opower/test_config_flow.py index a236494f2c9..8134539b0a5 100644 --- a/tests/components/opower/test_config_flow.py +++ b/tests/components/opower/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Opower config flow.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch from opower import CannotConnect, InvalidAuth import pytest -from typing_extensions import Generator from homeassistant import config_entries from homeassistant.components.opower.const import DOMAIN diff --git a/tests/components/oralb/conftest.py b/tests/components/oralb/conftest.py index fa4ba463357..c757d79a78e 100644 --- a/tests/components/oralb/conftest.py +++ b/tests/components/oralb/conftest.py @@ -1,9 +1,9 @@ """OralB session fixtures.""" +from collections.abc import Generator from unittest import mock import pytest -from typing_extensions import Generator class MockServices: diff --git a/tests/components/ourgroceries/conftest.py b/tests/components/ourgroceries/conftest.py index bc8c632b511..b3fb4e9bcc6 100644 --- a/tests/components/ourgroceries/conftest.py +++ b/tests/components/ourgroceries/conftest.py @@ -1,9 +1,9 @@ """Common fixtures for the OurGroceries tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.ourgroceries import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME diff --git a/tests/components/overkiz/conftest.py b/tests/components/overkiz/conftest.py index 8ab26e3587b..151d0719ddb 100644 --- a/tests/components/overkiz/conftest.py +++ b/tests/components/overkiz/conftest.py @@ -1,9 +1,9 @@ """Configuration for overkiz tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch import pytest -from typing_extensions import Generator from homeassistant.components.overkiz.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/permobil/conftest.py b/tests/components/permobil/conftest.py index ed6a843b206..d3630d3f366 100644 --- a/tests/components/permobil/conftest.py +++ b/tests/components/permobil/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the MyPermobil tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch from mypermobil import MyPermobil import pytest -from typing_extensions import Generator from .const import MOCK_REGION_NAME, MOCK_TOKEN, MOCK_URL diff --git a/tests/components/philips_js/conftest.py b/tests/components/philips_js/conftest.py index b6c78fe9e5e..2a1325627ee 100644 --- a/tests/components/philips_js/conftest.py +++ b/tests/components/philips_js/conftest.py @@ -1,10 +1,10 @@ """Standard setup for tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, create_autospec, patch from haphilipsjs import PhilipsTV import pytest -from typing_extensions import Generator from homeassistant.components.philips_js.const import DOMAIN diff --git a/tests/components/ping/test_device_tracker.py b/tests/components/ping/test_device_tracker.py index 5aa425226b3..4a5d6ba94ed 100644 --- a/tests/components/ping/test_device_tracker.py +++ b/tests/components/ping/test_device_tracker.py @@ -1,12 +1,12 @@ """Test the binary sensor platform of ping.""" +from collections.abc import Generator from datetime import timedelta from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory from icmplib import Host import pytest -from typing_extensions import Generator from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index a061d9c1105..53c032cb08b 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -1,10 +1,10 @@ """Fixtures for Plex tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest import requests_mock -from typing_extensions import Generator from homeassistant.components.plex.const import DOMAIN, PLEX_SERVER_CONFIG, SERVERS from homeassistant.const import CONF_URL diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index 83826a0a543..ec857a965e5 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -2,13 +2,13 @@ from __future__ import annotations +from collections.abc import Generator import json from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from plugwise import PlugwiseData import pytest -from typing_extensions import Generator from homeassistant.components.plugwise.const import DOMAIN from homeassistant.const import ( diff --git a/tests/components/poolsense/conftest.py b/tests/components/poolsense/conftest.py index ac16ef23ff3..6a842df7cfd 100644 --- a/tests/components/poolsense/conftest.py +++ b/tests/components/poolsense/conftest.py @@ -1,10 +1,10 @@ """Common fixtures for the Poolsense tests.""" +from collections.abc import Generator from datetime import UTC, datetime from unittest.mock import AsyncMock, patch import pytest -from typing_extensions import Generator from homeassistant.components.poolsense.const import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD diff --git a/tests/components/prosegur/test_alarm_control_panel.py b/tests/components/prosegur/test_alarm_control_panel.py index b65b86b3049..f66d070f218 100644 --- a/tests/components/prosegur/test_alarm_control_panel.py +++ b/tests/components/prosegur/test_alarm_control_panel.py @@ -1,10 +1,10 @@ """Tests for the Prosegur alarm control panel device.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch from pyprosegur.installation import Status import pytest -from typing_extensions import Generator from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN from homeassistant.const import ( diff --git a/tests/components/ps4/conftest.py b/tests/components/ps4/conftest.py index bc84ea3b4db..c95cc78f53a 100644 --- a/tests/components/ps4/conftest.py +++ b/tests/components/ps4/conftest.py @@ -1,10 +1,10 @@ """Test configuration for PS4.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch from pyps4_2ndscreen.ddp import DEFAULT_UDP_PORT, DDPProtocol import pytest -from typing_extensions import Generator @pytest.fixture diff --git a/tests/components/pure_energie/conftest.py b/tests/components/pure_energie/conftest.py index 7174befbf5b..9aa3a4cc1b4 100644 --- a/tests/components/pure_energie/conftest.py +++ b/tests/components/pure_energie/conftest.py @@ -1,11 +1,11 @@ """Fixtures for Pure Energie integration tests.""" +from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch from gridnet import Device as GridNetDevice, SmartBridge import pytest -from typing_extensions import Generator from homeassistant.components.pure_energie.const import DOMAIN from homeassistant.const import CONF_HOST diff --git a/tests/components/pvoutput/conftest.py b/tests/components/pvoutput/conftest.py index d19f09d9e6c..a55bb21d2ae 100644 --- a/tests/components/pvoutput/conftest.py +++ b/tests/components/pvoutput/conftest.py @@ -2,11 +2,11 @@ from __future__ import annotations +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from pvo import Status, System import pytest -from typing_extensions import Generator from homeassistant.components.pvoutput.const import CONF_SYSTEM_ID, DOMAIN from homeassistant.const import CONF_API_KEY From f08638eead4826db4c71995a8a0cdaa246f1ba57 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 1 Jul 2024 12:30:20 +0200 Subject: [PATCH 0526/2411] Add typing to Panasonic Viera (#120772) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .../components/panasonic_viera/__init__.py | 87 ++++++++++--------- .../components/panasonic_viera/config_flow.py | 31 ++++--- .../panasonic_viera/media_player.py | 13 ++- .../components/panasonic_viera/remote.py | 11 ++- 4 files changed, 76 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 2cf91792800..69800d2ef1e 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -1,22 +1,18 @@ """The Panasonic Viera integration.""" +from collections.abc import Callable from functools import partial import logging +from typing import Any from urllib.error import HTTPError, URLError from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError import voluptuous as vol +from homeassistant.components.media_player import MediaPlayerState, MediaType from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_OFF, - STATE_ON, - Platform, -) -from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, Platform +from homeassistant.core import Context, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType @@ -132,13 +128,13 @@ class Remote: def __init__( self, - hass, - host, - port, - on_action=None, - app_id=None, - encryption_key=None, - ): + hass: HomeAssistant, + host: str, + port: int, + on_action: Script | None = None, + app_id: str | None = None, + encryption_key: str | None = None, + ) -> None: """Initialize the Remote class.""" self._hass = hass @@ -150,15 +146,14 @@ class Remote: self._app_id = app_id self._encryption_key = encryption_key - self.state = None - self.available = False - self.volume = 0 - self.muted = False - self.playing = True + self._control: RemoteControl | None = None + self.state: MediaPlayerState | None = None + self.available: bool = False + self.volume: float = 0 + self.muted: bool = False + self.playing: bool = True - self._control = None - - async def async_create_remote_control(self, during_setup=False): + async def async_create_remote_control(self, during_setup: bool = False) -> None: """Create remote control.""" try: params = {} @@ -175,15 +170,15 @@ class Remote: except (URLError, SOAPError, OSError) as err: _LOGGER.debug("Could not establish remote connection: %s", err) self._control = None - self.state = STATE_OFF + self.state = MediaPlayerState.OFF self.available = self._on_action is not None except Exception: _LOGGER.exception("An unknown error occurred") self._control = None - self.state = STATE_OFF + self.state = MediaPlayerState.OFF self.available = self._on_action is not None - async def async_update(self): + async def async_update(self) -> None: """Update device data.""" if self._control is None: await self.async_create_remote_control() @@ -191,8 +186,9 @@ class Remote: await self._handle_errors(self._update) - def _update(self): + def _update(self) -> None: """Retrieve the latest data.""" + assert self._control is not None self.muted = self._control.get_mute() self.volume = self._control.get_volume() / 100 @@ -203,39 +199,43 @@ class Remote: except (AttributeError, TypeError): key = getattr(key, "value", key) + assert self._control is not None await self._handle_errors(self._control.send_key, key) - async def async_turn_on(self, context): + async def async_turn_on(self, context: Context | None) -> None: """Turn on the TV.""" if self._on_action is not None: await self._on_action.async_run(context=context) await self.async_update() - elif self.state != STATE_ON: + elif self.state is not MediaPlayerState.ON: await self.async_send_key(Keys.POWER) await self.async_update() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off the TV.""" - if self.state != STATE_OFF: + if self.state is not MediaPlayerState.OFF: await self.async_send_key(Keys.POWER) - self.state = STATE_OFF + self.state = MediaPlayerState.OFF await self.async_update() - async def async_set_mute(self, enable): + async def async_set_mute(self, enable: bool) -> None: """Set mute based on 'enable'.""" + assert self._control is not None await self._handle_errors(self._control.set_mute, enable) - async def async_set_volume(self, volume): + async def async_set_volume(self, volume: float) -> None: """Set volume level, range 0..1.""" + assert self._control is not None volume = int(volume * 100) await self._handle_errors(self._control.set_volume, volume) - async def async_play_media(self, media_type, media_id): + async def async_play_media(self, media_type: MediaType, media_id: str) -> None: """Play media.""" + assert self._control is not None _LOGGER.debug("Play media: %s (%s)", media_id, media_type) await self._handle_errors(self._control.open_webpage, media_id) - async def async_get_device_info(self): + async def async_get_device_info(self) -> dict[str, Any] | None: """Return device info.""" if self._control is None: return None @@ -243,7 +243,9 @@ class Remote: _LOGGER.debug("Fetched device info: %s", str(device_info)) return device_info - async def _handle_errors(self, func, *args): + async def _handle_errors[_R, *_Ts]( + self, func: Callable[[*_Ts], _R], *args: *_Ts + ) -> _R | None: """Handle errors from func, set available and reconnect if needed.""" try: result = await self._hass.async_add_executor_job(func, *args) @@ -252,23 +254,24 @@ class Remote: "The connection couldn't be encrypted. Please reconfigure your TV" ) self.available = False + return None except (SOAPError, HTTPError) as err: _LOGGER.debug("An error occurred: %s", err) - self.state = STATE_OFF + self.state = MediaPlayerState.OFF self.available = True await self.async_create_remote_control() return None except (URLError, OSError) as err: _LOGGER.debug("An error occurred: %s", err) - self.state = STATE_OFF + self.state = MediaPlayerState.OFF self.available = self._on_action is not None await self.async_create_remote_control() return None except Exception: _LOGGER.exception("An unknown error occurred") - self.state = STATE_OFF + self.state = MediaPlayerState.OFF self.available = self._on_action is not None return None - self.state = STATE_ON + self.state = MediaPlayerState.ON self.available = True return result diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index 9cb8fb5da83..0226fb33c9e 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -2,12 +2,13 @@ from functools import partial import logging +from typing import Any from urllib.error import URLError from panasonic_viera import TV_TYPE_ENCRYPTED, RemoteControl, SOAPError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT from .const import ( @@ -33,7 +34,7 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the Panasonic Viera config flow.""" - self._data = { + self._data: dict[str, Any] = { CONF_HOST: None, CONF_NAME: None, CONF_PORT: None, @@ -41,11 +42,13 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN): ATTR_DEVICE_INFO: None, } - self._remote = None + self._remote: RemoteControl | None = None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: await self.async_load_data(user_input) @@ -53,7 +56,7 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN): self._remote = await self.hass.async_add_executor_job( partial(RemoteControl, self._data[CONF_HOST], self._data[CONF_PORT]) ) - + assert self._remote is not None self._data[ATTR_DEVICE_INFO] = await self.hass.async_add_executor_job( self._remote.get_device_info ) @@ -63,8 +66,7 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN): except Exception: _LOGGER.exception("An unknown error occurred") return self.async_abort(reason="unknown") - - if "base" not in errors: + else: await self.async_set_unique_id(self._data[ATTR_DEVICE_INFO][ATTR_UDN]) self._abort_if_unique_id_configured() @@ -102,9 +104,12 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_pairing(self, user_input=None): + async def async_step_pairing( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle the pairing step.""" - errors = {} + errors: dict[str, str] = {} + assert self._remote is not None if user_input is not None: pin = user_input[CONF_PIN] @@ -152,11 +157,13 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, import_config): + async def async_step_import( + self, import_config: dict[str, Any] + ) -> ConfigFlowResult: """Import a config entry from configuration.yaml.""" return await self.async_step_user(user_input=import_config) - async def async_load_data(self, config): + async def async_load_data(self, config: dict[str, Any]) -> None: """Load the data.""" self._data = config diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 76ca76c1ca6..8738b897d29 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -13,6 +13,7 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, MediaType, async_process_play_media_url, ) @@ -72,6 +73,7 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): ) _attr_has_entity_name = True _attr_name = None + _attr_device_class = MediaPlayerDeviceClass.TV def __init__(self, remote, name, device_info): """Initialize the entity.""" @@ -88,12 +90,7 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): self._attr_name = name @property - def device_class(self): - """Return the device class of the device.""" - return MediaPlayerDeviceClass.TV - - @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" return self._remote.state @@ -103,12 +100,12 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): return self._remote.available @property - def volume_level(self): + def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" return self._remote.volume @property - def is_volume_muted(self): + def is_volume_muted(self) -> bool | None: """Boolean if volume is currently muted.""" return self._remote.muted diff --git a/homeassistant/components/panasonic_viera/remote.py b/homeassistant/components/panasonic_viera/remote.py index c47dce36306..ad40a97f700 100644 --- a/homeassistant/components/panasonic_viera/remote.py +++ b/homeassistant/components/panasonic_viera/remote.py @@ -12,6 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import Remote from .const import ( ATTR_DEVICE_INFO, ATTR_MANUFACTURER, @@ -43,7 +44,9 @@ async def async_setup_entry( class PanasonicVieraRemoteEntity(RemoteEntity): """Representation of a Panasonic Viera TV Remote.""" - def __init__(self, remote, name, device_info): + def __init__( + self, remote: Remote, name: str, device_info: dict[str, Any] | None = None + ) -> None: """Initialize the entity.""" # Save a reference to the imported class self._remote = remote @@ -51,7 +54,7 @@ class PanasonicVieraRemoteEntity(RemoteEntity): self._device_info = device_info @property - def unique_id(self): + def unique_id(self) -> str | None: """Return the unique ID of the device.""" if self._device_info is None: return None @@ -70,7 +73,7 @@ class PanasonicVieraRemoteEntity(RemoteEntity): ) @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._name @@ -80,7 +83,7 @@ class PanasonicVieraRemoteEntity(RemoteEntity): return self._remote.available @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._remote.state == STATE_ON From 44640ef9e8c2cae98be54bdfb83ba7e6b767966d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 1 Jul 2024 12:33:51 +0200 Subject: [PATCH 0527/2411] First step towards fixtures in deCONZ tests (#120863) * config entry fixture * Mock web request * Make siren tests use new fixtures * Replace old constants * Add mock put request * Change comment --- tests/components/deconz/conftest.py | 179 ++++++++++++++++++++++++ tests/components/deconz/test_gateway.py | 7 +- tests/components/deconz/test_siren.py | 40 +++--- 3 files changed, 201 insertions(+), 25 deletions(-) diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py index d0f0f11c99b..ec254ea1c1e 100644 --- a/tests/components/deconz/conftest.py +++ b/tests/components/deconz/conftest.py @@ -2,12 +2,191 @@ from __future__ import annotations +from collections.abc import Callable +from types import MappingProxyType +from typing import Any from unittest.mock import patch from pydeconz.websocket import Signal import pytest +from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa: F401 +from tests.test_util.aiohttp import AiohttpClientMocker + +# Config entry fixtures + +API_KEY = "1234567890ABCDEF" +BRIDGEID = "01234E56789A" +HOST = "1.2.3.4" +PORT = 80 + + +@pytest.fixture(name="config_entry") +def fixture_config_entry( + hass: HomeAssistant, + config_entry_data: MappingProxyType[str, Any], + config_entry_options: MappingProxyType[str, Any], +) -> ConfigEntry: + """Define a config entry fixture.""" + config_entry = MockConfigEntry( + domain=DECONZ_DOMAIN, + entry_id="1", + unique_id=BRIDGEID, + data=config_entry_data, + options=config_entry_options, + ) + config_entry.add_to_hass(hass) + return config_entry + + +@pytest.fixture(name="config_entry_data") +def fixture_config_entry_data() -> MappingProxyType[str, Any]: + """Define a config entry data fixture.""" + return { + CONF_API_KEY: API_KEY, + CONF_HOST: HOST, + CONF_PORT: PORT, + } + + +@pytest.fixture(name="config_entry_options") +def fixture_config_entry_options() -> MappingProxyType[str, Any]: + """Define a config entry options fixture.""" + return {} + + +# Request mocks + + +@pytest.fixture(name="mock_put_request") +def fixture_put_request( + aioclient_mock: AiohttpClientMocker, config_entry_data: MappingProxyType[str, Any] +) -> Callable[[str, str], AiohttpClientMocker]: + """Mock a deCONZ put request.""" + _host = config_entry_data[CONF_HOST] + _port = config_entry_data[CONF_PORT] + _api_key = config_entry_data[CONF_API_KEY] + + def __mock_requests(path: str, host: str = "") -> AiohttpClientMocker: + url = f"http://{host or _host}:{_port}/api/{_api_key}{path}" + aioclient_mock.put(url, json={}, headers={"content-type": CONTENT_TYPE_JSON}) + return aioclient_mock + + return __mock_requests + + +@pytest.fixture(name="mock_requests") +def fixture_get_request( + aioclient_mock: AiohttpClientMocker, + config_entry_data: MappingProxyType[str, Any], + deconz_payload: dict[str, Any], +) -> Callable[[str], None]: + """Mock default deCONZ requests responses.""" + _host = config_entry_data[CONF_HOST] + _port = config_entry_data[CONF_PORT] + _api_key = config_entry_data[CONF_API_KEY] + + def __mock_requests(host: str = "") -> None: + url = f"http://{host or _host}:{_port}/api/{_api_key}" + aioclient_mock.get( + url, json=deconz_payload, headers={"content-type": CONTENT_TYPE_JSON} + ) + + return __mock_requests + + +# Request payload fixtures + + +@pytest.fixture(name="deconz_payload") +def fixture_data( + alarm_system_payload: dict[str, Any], + config_payload: dict[str, Any], + group_payload: dict[str, Any], + light_payload: dict[str, Any], + sensor_payload: dict[str, Any], +) -> dict[str, Any]: + """DeCONZ data.""" + return { + "alarmsystems": alarm_system_payload, + "config": config_payload, + "groups": group_payload, + "lights": light_payload, + "sensors": sensor_payload, + } + + +@pytest.fixture(name="alarm_system_payload") +def fixture_alarm_system_data() -> dict[str, Any]: + """Alarm system data.""" + return {} + + +@pytest.fixture(name="config_payload") +def fixture_config_data() -> dict[str, Any]: + """Config data.""" + return { + "bridgeid": BRIDGEID, + "ipaddress": HOST, + "mac": "00:11:22:33:44:55", + "modelid": "deCONZ", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "uuid": "1234", + "websocketport": 1234, + } + + +@pytest.fixture(name="group_payload") +def fixture_group_data() -> dict[str, Any]: + """Group data.""" + return {} + + +@pytest.fixture(name="light_payload") +def fixture_light_data() -> dict[str, Any]: + """Light data.""" + return {} + + +@pytest.fixture(name="sensor_payload") +def fixture_sensor_data() -> dict[str, Any]: + """Sensor data.""" + return {} + + +@pytest.fixture(name="config_entry_factory") +async def fixture_config_entry_factory( + hass: HomeAssistant, + config_entry: ConfigEntry, + mock_requests: Callable[[str, str], None], +) -> Callable[[], ConfigEntry]: + """Fixture factory that can set up UniFi network integration.""" + + async def __mock_setup_config_entry() -> ConfigEntry: + mock_requests(config_entry.data[CONF_HOST]) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return config_entry + + return __mock_setup_config_entry + + +@pytest.fixture(name="config_entry_setup") +async def fixture_config_entry_setup( + hass: HomeAssistant, config_entry_factory: Callable[[], ConfigEntry] +) -> ConfigEntry: + """Fixture providing a set up instance of deCONZ integration.""" + return await config_entry_factory() + + +# Websocket fixtures @pytest.fixture(autouse=True) diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index b00a5cc1f05..71001312dec 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -47,14 +47,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import UNDEFINED, UndefinedType +from .conftest import API_KEY, BRIDGEID, HOST, PORT + from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker -API_KEY = "1234567890ABCDEF" -BRIDGEID = "01234E56789A" -HOST = "1.2.3.4" -PORT = 80 - DEFAULT_URL = f"http://{HOST}:{PORT}/api/{API_KEY}" ENTRY_CONFIG = {CONF_API_KEY: API_KEY, CONF_HOST: HOST, CONF_PORT: PORT} diff --git a/tests/components/deconz/test_siren.py b/tests/components/deconz/test_siren.py index 62ed1b732b8..3db345a6ad2 100644 --- a/tests/components/deconz/test_siren.py +++ b/tests/components/deconz/test_siren.py @@ -1,8 +1,11 @@ """deCONZ switch platform tests.""" -from unittest.mock import patch +from collections.abc import Callable + +import pytest from homeassistant.components.siren import ATTR_DURATION, DOMAIN as SIREN_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -13,21 +16,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from .test_gateway import ( - DECONZ_WEB_REQUEST, - mock_deconz_put_request, - setup_deconz_integration, -) - from tests.test_util.aiohttp import AiohttpClientMocker -async def test_sirens( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test that siren entities are created.""" - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "1": { "name": "Warning device", "type": "Warning device", @@ -41,10 +36,15 @@ async def test_sirens( "uniqueid": "00:00:00:00:00:00:00:01-00", }, } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +async def test_sirens( + hass: HomeAssistant, + config_entry_setup: ConfigEntry, + mock_deconz_websocket, + mock_put_request: Callable[[str, str], AiohttpClientMocker], +) -> None: + """Test that siren entities are created.""" assert len(hass.states.async_all()) == 2 assert hass.states.get("siren.warning_device").state == STATE_ON assert not hass.states.get("siren.unsupported_siren") @@ -63,7 +63,7 @@ async def test_sirens( # Verify service calls - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") + aioclient_mock = mock_put_request("/lights/1/state") # Service turn on siren @@ -95,13 +95,13 @@ async def test_sirens( ) assert aioclient_mock.mock_calls[3][2] == {"alert": "lselect", "ontime": 100} - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) states = hass.states.async_all() assert len(states) == 2 for state in states: assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 From 09ff44e59e1b52bab22fcc2f24255a086ba32a40 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 1 Jul 2024 13:06:14 +0200 Subject: [PATCH 0528/2411] Bump incomfort-client dependency to 0.6.3 (#120913) --- homeassistant/components/incomfort/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index c0b536dabe5..93f350a8e2c 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/incomfort", "iot_class": "local_polling", "loggers": ["incomfortclient"], - "requirements": ["incomfort-client==0.6.2"] + "requirements": ["incomfort-client==0.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 75f24dbf6aa..10087dc62d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1152,7 +1152,7 @@ ihcsdk==2.8.5 imgw_pib==1.0.5 # homeassistant.components.incomfort -incomfort-client==0.6.2 +incomfort-client==0.6.3 # homeassistant.components.influxdb influxdb-client==1.24.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22a130f5082..f0ac1e41b0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -942,7 +942,7 @@ ifaddr==0.2.0 imgw_pib==1.0.5 # homeassistant.components.incomfort -incomfort-client==0.6.2 +incomfort-client==0.6.3 # homeassistant.components.influxdb influxdb-client==1.24.0 From 4cc414fbf87547e70aa67f27b00353abd4e1fd06 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 13:10:39 +0200 Subject: [PATCH 0529/2411] Use service_calls fixture in google_translate tests (#120920) --- tests/components/google_translate/test_tts.py | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 4d14c7e28cb..41cecd8cd98 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -13,17 +13,13 @@ import pytest from homeassistant.components import tts from homeassistant.components.google_translate.const import CONF_TLD, DOMAIN -from homeassistant.components.media_player import ( - ATTR_MEDIA_CONTENT_ID, - DOMAIN as DOMAIN_MP, - SERVICE_PLAY_MEDIA, -) +from homeassistant.components.media_player import ATTR_MEDIA_CONTENT_ID from homeassistant.config import async_process_ha_core_config from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, async_mock_service +from tests.common import MockConfigEntry from tests.components.tts.common import retrieve_media from tests.typing import ClientSessionGenerator @@ -39,12 +35,6 @@ def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: return mock_tts_cache_dir -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Mock media player calls.""" - return async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - - @pytest.fixture(autouse=True) async def setup_internal_url(hass: HomeAssistant) -> None: """Set up internal url.""" @@ -126,7 +116,7 @@ async def test_tts_service( hass: HomeAssistant, mock_gtts: MagicMock, hass_client: ClientSessionGenerator, - calls: list[ServiceCall], + service_calls: list[ServiceCall], setup: str, tts_service: str, service_data: dict[str, Any], @@ -139,9 +129,11 @@ async def test_tts_service( blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 2 assert ( - await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await retrieve_media( + hass, hass_client, service_calls[1].data[ATTR_MEDIA_CONTENT_ID] + ) == HTTPStatus.OK ) assert len(mock_gtts.mock_calls) == 2 @@ -181,7 +173,7 @@ async def test_service_say_german_config( hass: HomeAssistant, mock_gtts: MagicMock, hass_client: ClientSessionGenerator, - calls: list[ServiceCall], + service_calls: list[ServiceCall], setup: str, tts_service: str, service_data: dict[str, Any], @@ -194,9 +186,11 @@ async def test_service_say_german_config( blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 2 assert ( - await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await retrieve_media( + hass, hass_client, service_calls[1].data[ATTR_MEDIA_CONTENT_ID] + ) == HTTPStatus.OK ) assert len(mock_gtts.mock_calls) == 2 @@ -236,7 +230,7 @@ async def test_service_say_german_service( hass: HomeAssistant, mock_gtts: MagicMock, hass_client: ClientSessionGenerator, - calls: list[ServiceCall], + service_calls: list[ServiceCall], setup: str, tts_service: str, service_data: dict[str, Any], @@ -249,9 +243,11 @@ async def test_service_say_german_service( blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 2 assert ( - await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await retrieve_media( + hass, hass_client, service_calls[1].data[ATTR_MEDIA_CONTENT_ID] + ) == HTTPStatus.OK ) assert len(mock_gtts.mock_calls) == 2 @@ -290,7 +286,7 @@ async def test_service_say_en_uk_config( hass: HomeAssistant, mock_gtts: MagicMock, hass_client: ClientSessionGenerator, - calls: list[ServiceCall], + service_calls: list[ServiceCall], setup: str, tts_service: str, service_data: dict[str, Any], @@ -303,9 +299,11 @@ async def test_service_say_en_uk_config( blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 2 assert ( - await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await retrieve_media( + hass, hass_client, service_calls[1].data[ATTR_MEDIA_CONTENT_ID] + ) == HTTPStatus.OK ) assert len(mock_gtts.mock_calls) == 2 @@ -345,7 +343,7 @@ async def test_service_say_en_uk_service( hass: HomeAssistant, mock_gtts: MagicMock, hass_client: ClientSessionGenerator, - calls: list[ServiceCall], + service_calls: list[ServiceCall], setup: str, tts_service: str, service_data: dict[str, Any], @@ -358,9 +356,11 @@ async def test_service_say_en_uk_service( blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 2 assert ( - await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await retrieve_media( + hass, hass_client, service_calls[1].data[ATTR_MEDIA_CONTENT_ID] + ) == HTTPStatus.OK ) assert len(mock_gtts.mock_calls) == 2 @@ -400,7 +400,7 @@ async def test_service_say_en_couk( hass: HomeAssistant, mock_gtts: MagicMock, hass_client: ClientSessionGenerator, - calls: list[ServiceCall], + service_calls: list[ServiceCall], setup: str, tts_service: str, service_data: dict[str, Any], @@ -413,9 +413,11 @@ async def test_service_say_en_couk( blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 2 assert ( - await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await retrieve_media( + hass, hass_client, service_calls[1].data[ATTR_MEDIA_CONTENT_ID] + ) == HTTPStatus.OK ) assert len(mock_gtts.mock_calls) == 2 @@ -454,7 +456,7 @@ async def test_service_say_error( hass: HomeAssistant, mock_gtts: MagicMock, hass_client: ClientSessionGenerator, - calls: list[ServiceCall], + service_calls: list[ServiceCall], setup: str, tts_service: str, service_data: dict[str, Any], @@ -469,9 +471,11 @@ async def test_service_say_error( blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 2 assert ( - await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await retrieve_media( + hass, hass_client, service_calls[1].data[ATTR_MEDIA_CONTENT_ID] + ) == HTTPStatus.NOT_FOUND ) assert len(mock_gtts.mock_calls) == 2 From 414525503c505373a9bb287ab7007e48c15c0476 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:02:54 +0200 Subject: [PATCH 0530/2411] Use TypeVar defaults for Generator (#120921) * Use TypeVar defaults for Generator * Code review --- tests/components/apsystems/conftest.py | 4 ++-- tests/components/aquacell/conftest.py | 6 +++--- tests/components/brother/conftest.py | 6 +++--- tests/components/ecovacs/test_services.py | 4 +--- tests/components/kitchen_sink/test_config_flow.py | 4 ++-- tests/components/mpd/conftest.py | 6 +++--- tests/components/otp/conftest.py | 4 ++-- tests/components/pyload/conftest.py | 4 ++-- tests/components/pyload/test_button.py | 4 ++-- tests/components/pyload/test_sensor.py | 4 ++-- tests/components/pyload/test_switch.py | 4 ++-- tests/components/solarlog/conftest.py | 2 +- 12 files changed, 25 insertions(+), 27 deletions(-) diff --git a/tests/components/apsystems/conftest.py b/tests/components/apsystems/conftest.py index 0fc1fb183a8..682086be380 100644 --- a/tests/components/apsystems/conftest.py +++ b/tests/components/apsystems/conftest.py @@ -1,7 +1,7 @@ """Common fixtures for the APsystems Local API tests.""" from collections.abc import Generator -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from APsystemsEZ1 import ReturnDeviceInfo, ReturnOutputData import pytest @@ -23,7 +23,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_apsystems() -> Generator[AsyncMock, None, None]: +def mock_apsystems() -> Generator[MagicMock]: """Mock APSystems lib.""" with ( patch( diff --git a/tests/components/aquacell/conftest.py b/tests/components/aquacell/conftest.py index db27f51dc03..f5a741ceed8 100644 --- a/tests/components/aquacell/conftest.py +++ b/tests/components/aquacell/conftest.py @@ -2,7 +2,7 @@ from collections.abc import Generator from datetime import datetime -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from aioaquacell import AquacellApi, Softener import pytest @@ -19,7 +19,7 @@ from tests.common import MockConfigEntry, load_json_array_fixture @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.aquacell.async_setup_entry", return_value=True @@ -28,7 +28,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_aquacell_api() -> Generator[AsyncMock, None, None]: +def mock_aquacell_api() -> Generator[MagicMock]: """Build a fixture for the Aquacell API that authenticates successfully and returns a single softener.""" with ( patch( diff --git a/tests/components/brother/conftest.py b/tests/components/brother/conftest.py index ec6120db5f5..de22158da00 100644 --- a/tests/components/brother/conftest.py +++ b/tests/components/brother/conftest.py @@ -2,7 +2,7 @@ from collections.abc import Generator from datetime import UTC, datetime -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from brother import BrotherSensors import pytest @@ -87,7 +87,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_unload_entry() -> Generator[AsyncMock, None, None]: +def mock_unload_entry() -> Generator[AsyncMock]: """Override async_unload_entry.""" with patch( "homeassistant.components.brother.async_unload_entry", return_value=True @@ -96,7 +96,7 @@ def mock_unload_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_brother_client() -> Generator[AsyncMock, None, None]: +def mock_brother_client() -> Generator[MagicMock]: """Mock Brother client.""" with ( patch("homeassistant.components.brother.Brother", autospec=True) as mock_client, diff --git a/tests/components/ecovacs/test_services.py b/tests/components/ecovacs/test_services.py index 973c63782ec..19e8237be03 100644 --- a/tests/components/ecovacs/test_services.py +++ b/tests/components/ecovacs/test_services.py @@ -16,9 +16,7 @@ pytestmark = [pytest.mark.usefixtures("init_integration")] @pytest.fixture -def mock_device_execute_response( - data: dict[str, Any], -) -> Generator[dict[str, Any], None, None]: +def mock_device_execute_response(data: dict[str, Any]) -> Generator[dict[str, Any]]: """Mock the device execute function response.""" response = { diff --git a/tests/components/kitchen_sink/test_config_flow.py b/tests/components/kitchen_sink/test_config_flow.py index 290167196cd..5f163d1342e 100644 --- a/tests/components/kitchen_sink/test_config_flow.py +++ b/tests/components/kitchen_sink/test_config_flow.py @@ -1,6 +1,6 @@ """Test the Everything but the Kitchen Sink config flow.""" -from collections.abc import AsyncGenerator +from collections.abc import Generator from unittest.mock import patch import pytest @@ -15,7 +15,7 @@ from tests.common import MockConfigEntry @pytest.fixture -async def no_platforms() -> AsyncGenerator[None, None]: +def no_platforms() -> Generator[None]: """Don't enable any platforms.""" with patch( "homeassistant.components.kitchen_sink.COMPONENTS_WITH_DEMO_PLATFORM", diff --git a/tests/components/mpd/conftest.py b/tests/components/mpd/conftest.py index 818f085decc..a73a529cd0b 100644 --- a/tests/components/mpd/conftest.py +++ b/tests/components/mpd/conftest.py @@ -1,7 +1,7 @@ """Fixtures for Music Player Daemon integration tests.""" from collections.abc import Generator -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -22,7 +22,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.mpd.async_setup_entry", return_value=True @@ -31,7 +31,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_mpd_client() -> Generator[AsyncMock, None, None]: +def mock_mpd_client() -> Generator[MagicMock]: """Return a mock for Music Player Daemon client.""" with patch( diff --git a/tests/components/otp/conftest.py b/tests/components/otp/conftest.py index 7443d772c69..7926be1e48e 100644 --- a/tests/components/otp/conftest.py +++ b/tests/components/otp/conftest.py @@ -14,7 +14,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.otp.async_setup_entry", return_value=True @@ -23,7 +23,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_pyotp() -> Generator[MagicMock, None, None]: +def mock_pyotp() -> Generator[MagicMock]: """Mock a pyotp.""" with ( patch( diff --git a/tests/components/pyload/conftest.py b/tests/components/pyload/conftest.py index 1d7b11567c7..c0f181396ab 100644 --- a/tests/components/pyload/conftest.py +++ b/tests/components/pyload/conftest.py @@ -1,7 +1,7 @@ """Fixtures for pyLoad integration tests.""" from collections.abc import Generator -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from pyloadapi.types import LoginResponse, StatusServerResponse import pytest @@ -72,7 +72,7 @@ def pyload_config() -> ConfigType: @pytest.fixture -def mock_pyloadapi() -> Generator[AsyncMock, None, None]: +def mock_pyloadapi() -> Generator[MagicMock]: """Mock PyLoadAPI.""" with ( patch( diff --git a/tests/components/pyload/test_button.py b/tests/components/pyload/test_button.py index 53f592374ba..9a2f480bede 100644 --- a/tests/components/pyload/test_button.py +++ b/tests/components/pyload/test_button.py @@ -1,6 +1,6 @@ """The tests for the button component.""" -from collections.abc import AsyncGenerator +from collections.abc import Generator from unittest.mock import AsyncMock, call, patch from pyloadapi import CannotConnect, InvalidAuth @@ -26,7 +26,7 @@ API_CALL = { @pytest.fixture(autouse=True) -async def button_only() -> AsyncGenerator[None, None]: +def button_only() -> Generator[None]: """Enable only the button platform.""" with patch( "homeassistant.components.pyload.PLATFORMS", diff --git a/tests/components/pyload/test_sensor.py b/tests/components/pyload/test_sensor.py index a44c9c8bf91..8eccda07fa7 100644 --- a/tests/components/pyload/test_sensor.py +++ b/tests/components/pyload/test_sensor.py @@ -1,6 +1,6 @@ """Tests for the pyLoad Sensors.""" -from collections.abc import AsyncGenerator +from collections.abc import Generator from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory @@ -22,7 +22,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_plat @pytest.fixture(autouse=True) -async def sensor_only() -> AsyncGenerator[None, None]: +def sensor_only() -> Generator[None]: """Enable only the sensor platform.""" with patch( "homeassistant.components.pyload.PLATFORMS", diff --git a/tests/components/pyload/test_switch.py b/tests/components/pyload/test_switch.py index 8e99cb00cfe..493dbd8c0da 100644 --- a/tests/components/pyload/test_switch.py +++ b/tests/components/pyload/test_switch.py @@ -1,6 +1,6 @@ """Tests for the pyLoad Switches.""" -from collections.abc import AsyncGenerator +from collections.abc import Generator from unittest.mock import AsyncMock, call, patch from pyloadapi import CannotConnect, InvalidAuth @@ -38,7 +38,7 @@ API_CALL = { @pytest.fixture(autouse=True) -async def switch_only() -> AsyncGenerator[None, None]: +def switch_only() -> Generator[None]: """Enable only the switch platform.""" with patch( "homeassistant.components.pyload.PLATFORMS", diff --git a/tests/components/solarlog/conftest.py b/tests/components/solarlog/conftest.py index 08340487d99..86cdc870cde 100644 --- a/tests/components/solarlog/conftest.py +++ b/tests/components/solarlog/conftest.py @@ -60,7 +60,7 @@ def mock_solarlog_connector(): @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.solarlog.async_setup_entry", return_value=True From 546d6b22f1dff55e5e95fb0aef4566515dbbbce9 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 1 Jul 2024 14:03:21 +0200 Subject: [PATCH 0531/2411] Remove OverloadUT as codeowner from Ecovacs (#120517) --- CODEOWNERS | 4 ++-- homeassistant/components/ecovacs/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7834add43f6..355985b6d4c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -360,8 +360,8 @@ build.json @home-assistant/supervisor /tests/components/ecoforest/ @pjanuario /homeassistant/components/econet/ @w1ll1am23 /tests/components/econet/ @w1ll1am23 -/homeassistant/components/ecovacs/ @OverloadUT @mib1185 @edenhaus @Augar -/tests/components/ecovacs/ @OverloadUT @mib1185 @edenhaus @Augar +/homeassistant/components/ecovacs/ @mib1185 @edenhaus @Augar +/tests/components/ecovacs/ @mib1185 @edenhaus @Augar /homeassistant/components/ecowitt/ @pvizeli /tests/components/ecowitt/ @pvizeli /homeassistant/components/efergy/ @tkdrob diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index d14291576ff..c042027baa8 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -1,7 +1,7 @@ { "domain": "ecovacs", "name": "Ecovacs", - "codeowners": ["@OverloadUT", "@mib1185", "@edenhaus", "@Augar"], + "codeowners": ["@mib1185", "@edenhaus", "@Augar"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", From 61b2e4ca323c0f8bf37a0202c407542568e3d8ca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:05:30 +0200 Subject: [PATCH 0532/2411] Add Context to service_calls fixture (#120923) --- tests/conftest.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f9b65c5f138..3cef2dd0279 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -55,6 +55,7 @@ from homeassistant.config import YAML_CONFIG_FILE from homeassistant.config_entries import ConfigEntries, ConfigEntry, ConfigEntryState from homeassistant.const import HASSIO_USER_NAME from homeassistant.core import ( + Context, CoreState, HassJob, HomeAssistant, @@ -1661,7 +1662,7 @@ def label_registry(hass: HomeAssistant) -> lr.LabelRegistry: @pytest.fixture -def service_calls(hass: HomeAssistant) -> Generator[None, None, list[ServiceCall]]: +def service_calls(hass: HomeAssistant) -> Generator[list[ServiceCall]]: """Track all service calls.""" calls = [] @@ -1672,15 +1673,23 @@ def service_calls(hass: HomeAssistant) -> Generator[None, None, list[ServiceCall domain: str, service: str, service_data: dict[str, Any] | None = None, - **kwargs: Any, + blocking: bool = False, + context: Context | None = None, + target: dict[str, Any] | None = None, + return_response: bool = False, ) -> ServiceResponse: - calls.append(ServiceCall(domain, service, service_data)) + calls.append( + ServiceCall(domain, service, service_data, context, return_response) + ) try: return await _original_async_call( domain, service, service_data, - **kwargs, + blocking, + context, + target, + return_response, ) except ha.ServiceNotFound: _LOGGER.debug("Ignoring unknown service call to %s.%s", domain, service) @@ -1697,7 +1706,7 @@ def snapshot(snapshot: SnapshotAssertion) -> SnapshotAssertion: @pytest.fixture -def disable_block_async_io() -> Generator[Any, Any, None]: +def disable_block_async_io() -> Generator[None]: """Fixture to disable the loop protection from block_async_io.""" yield calls = block_async_io._BLOCKED_CALLS.calls From 5513682de42de9d5c8f75da63156573d23d1ce1c Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:21:41 -0300 Subject: [PATCH 0533/2411] Add missing translations for device class in Scrape (#120891) --- homeassistant/components/scrape/strings.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index 9b534aed77b..42cf3001b75 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -139,18 +139,19 @@ "selector": { "device_class": { "options": { - "date": "[%key:component::sensor::entity_component::date::name%]", - "duration": "[%key:component::sensor::entity_component::duration::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", "battery": "[%key:component::sensor::entity_component::battery::name%]", - "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", + "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", "current": "[%key:component::sensor::entity_component::current::name%]", "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", "data_size": "[%key:component::sensor::entity_component::data_size::name%]", + "date": "[%key:component::sensor::entity_component::date::name%]", "distance": "[%key:component::sensor::entity_component::distance::name%]", + "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", @@ -168,8 +169,8 @@ "pm1": "[%key:component::sensor::entity_component::pm1::name%]", "pm10": "[%key:component::sensor::entity_component::pm10::name%]", "pm25": "[%key:component::sensor::entity_component::pm25::name%]", - "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "power": "[%key:component::sensor::entity_component::power::name%]", + "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", "pressure": "[%key:component::sensor::entity_component::pressure::name%]", @@ -184,6 +185,7 @@ "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", + "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", From bc3562a9e84ff1129045a4f0ccddad0cc12a2dce Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:33:01 +0200 Subject: [PATCH 0534/2411] Use service_calls fixture in knx tests (#120930) --- tests/components/knx/test_device_trigger.py | 52 +++++++-------- tests/components/knx/test_trigger.py | 72 ++++++++++----------- 2 files changed, 58 insertions(+), 66 deletions(-) diff --git a/tests/components/knx/test_device_trigger.py b/tests/components/knx/test_device_trigger.py index 136dddefaab..9b49df080f5 100644 --- a/tests/components/knx/test_device_trigger.py +++ b/tests/components/knx/test_device_trigger.py @@ -18,18 +18,12 @@ from homeassistant.setup import async_setup_component from .conftest import KNXTestKit -from tests.common import async_get_device_automations, async_mock_service - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") +from tests.common import async_get_device_automations async def test_if_fires_on_telegram( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, knx: KNXTestKit, ) -> None: @@ -98,31 +92,31 @@ async def test_if_fires_on_telegram( # "specific" shall ignore destination address await knx.receive_write("0/0/1", (0x03, 0x2F)) - assert len(calls) == 1 - test_call = calls.pop() + assert len(service_calls) == 1 + test_call = service_calls.pop() assert test_call.data["catch_all"] == "telegram - 0/0/1" assert test_call.data["id"] == 0 await knx.receive_write("1/2/4", (0x03, 0x2F)) - assert len(calls) == 2 - test_call = calls.pop() + assert len(service_calls) == 2 + test_call = service_calls.pop() assert test_call.data["specific"] == "telegram - 1/2/4" assert test_call.data["id"] == "test-id" - test_call = calls.pop() + test_call = service_calls.pop() assert test_call.data["catch_all"] == "telegram - 1/2/4" assert test_call.data["id"] == 0 # "specific" shall ignore GroupValueRead await knx.receive_read("1/2/4") - assert len(calls) == 1 - test_call = calls.pop() + assert len(service_calls) == 1 + test_call = service_calls.pop() assert test_call.data["catch_all"] == "telegram - 1/2/4" assert test_call.data["id"] == 0 async def test_default_if_fires_on_telegram( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, knx: KNXTestKit, ) -> None: @@ -179,34 +173,34 @@ async def test_default_if_fires_on_telegram( ) await knx.receive_write("0/0/1", (0x03, 0x2F)) - assert len(calls) == 1 - test_call = calls.pop() + assert len(service_calls) == 1 + test_call = service_calls.pop() assert test_call.data["catch_all"] == "telegram - 0/0/1" assert test_call.data["id"] == 0 await knx.receive_write("1/2/4", (0x03, 0x2F)) - assert len(calls) == 2 - test_call = calls.pop() + assert len(service_calls) == 2 + test_call = service_calls.pop() assert test_call.data["specific"] == "telegram - 1/2/4" assert test_call.data["id"] == "test-id" - test_call = calls.pop() + test_call = service_calls.pop() assert test_call.data["catch_all"] == "telegram - 1/2/4" assert test_call.data["id"] == 0 # "specific" shall catch GroupValueRead as it is not set explicitly await knx.receive_read("1/2/4") - assert len(calls) == 2 - test_call = calls.pop() + assert len(service_calls) == 2 + test_call = service_calls.pop() assert test_call.data["specific"] == "telegram - 1/2/4" assert test_call.data["id"] == "test-id" - test_call = calls.pop() + test_call = service_calls.pop() assert test_call.data["catch_all"] == "telegram - 1/2/4" assert test_call.data["id"] == 0 async def test_remove_device_trigger( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, knx: KNXTestKit, ) -> None: @@ -241,8 +235,8 @@ async def test_remove_device_trigger( ) await knx.receive_write("0/0/1", (0x03, 0x2F)) - assert len(calls) == 1 - assert calls.pop().data["catch_all"] == "telegram - 0/0/1" + assert len(service_calls) == 1 + assert service_calls.pop().data["catch_all"] == "telegram - 0/0/1" await hass.services.async_call( automation.DOMAIN, @@ -250,8 +244,10 @@ async def test_remove_device_trigger( {ATTR_ENTITY_ID: f"automation.{automation_name}"}, blocking=True, ) + assert len(service_calls) == 1 + await knx.receive_write("0/0/1", (0x03, 0x2F)) - assert len(calls) == 0 + assert len(service_calls) == 1 async def test_get_triggers( diff --git a/tests/components/knx/test_trigger.py b/tests/components/knx/test_trigger.py index d957082de18..4565122aba6 100644 --- a/tests/components/knx/test_trigger.py +++ b/tests/components/knx/test_trigger.py @@ -11,18 +11,10 @@ from homeassistant.setup import async_setup_component from .conftest import KNXTestKit -from tests.common import async_mock_service - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - async def test_telegram_trigger( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], knx: KNXTestKit, ) -> None: """Test telegram triggers firing.""" @@ -73,24 +65,24 @@ async def test_telegram_trigger( # "specific" shall ignore destination address await knx.receive_write("0/0/1", (0x03, 0x2F)) - assert len(calls) == 1 - test_call = calls.pop() + assert len(service_calls) == 1 + test_call = service_calls.pop() assert test_call.data["catch_all"] == "telegram - 0/0/1" assert test_call.data["id"] == 0 await knx.receive_write("1/2/4", (0x03, 0x2F)) - assert len(calls) == 2 - test_call = calls.pop() + assert len(service_calls) == 2 + test_call = service_calls.pop() assert test_call.data["specific"] == "telegram - 1/2/4" assert test_call.data["id"] == "test-id" - test_call = calls.pop() + test_call = service_calls.pop() assert test_call.data["catch_all"] == "telegram - 1/2/4" assert test_call.data["id"] == 0 # "specific" shall ignore GroupValueRead await knx.receive_read("1/2/4") - assert len(calls) == 1 - test_call = calls.pop() + assert len(service_calls) == 1 + test_call = service_calls.pop() assert test_call.data["catch_all"] == "telegram - 1/2/4" assert test_call.data["id"] == 0 @@ -105,7 +97,7 @@ async def test_telegram_trigger( ) async def test_telegram_trigger_dpt_option( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], knx: KNXTestKit, payload: tuple[int, ...], type_option: dict[str, bool], @@ -138,16 +130,16 @@ async def test_telegram_trigger_dpt_option( ) await knx.receive_write("0/0/1", payload) - assert len(calls) == 1 - test_call = calls.pop() + assert len(service_calls) == 1 + test_call = service_calls.pop() assert test_call.data["catch_all"] == "telegram - 0/0/1" assert test_call.data["trigger"]["value"] == expected_value assert test_call.data["trigger"]["unit"] == expected_unit await knx.receive_read("0/0/1") - assert len(calls) == 1 - test_call = calls.pop() + assert len(service_calls) == 1 + test_call = service_calls.pop() assert test_call.data["catch_all"] == "telegram - 0/0/1" assert test_call.data["trigger"]["value"] is None assert test_call.data["trigger"]["unit"] is None @@ -192,7 +184,7 @@ async def test_telegram_trigger_dpt_option( ) async def test_telegram_trigger_options( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], knx: KNXTestKit, group_value_options: dict[str, bool], direction_options: dict[str, bool], @@ -225,28 +217,28 @@ async def test_telegram_trigger_options( if group_value_options.get("group_value_write", True) and direction_options.get( "incoming", True ): - assert len(calls) == 1 - assert calls.pop().data["catch_all"] == "telegram - 0/0/1" + assert len(service_calls) == 1 + assert service_calls.pop().data["catch_all"] == "telegram - 0/0/1" else: - assert len(calls) == 0 + assert len(service_calls) == 0 await knx.receive_response("0/0/1", 1) if group_value_options["group_value_response"] and direction_options.get( "incoming", True ): - assert len(calls) == 1 - assert calls.pop().data["catch_all"] == "telegram - 0/0/1" + assert len(service_calls) == 1 + assert service_calls.pop().data["catch_all"] == "telegram - 0/0/1" else: - assert len(calls) == 0 + assert len(service_calls) == 0 await knx.receive_read("0/0/1") if group_value_options["group_value_read"] and direction_options.get( "incoming", True ): - assert len(calls) == 1 - assert calls.pop().data["catch_all"] == "telegram - 0/0/1" + assert len(service_calls) == 1 + assert service_calls.pop().data["catch_all"] == "telegram - 0/0/1" else: - assert len(calls) == 0 + assert len(service_calls) == 0 await hass.services.async_call( "knx", @@ -254,20 +246,22 @@ async def test_telegram_trigger_options( {"address": "0/0/1", "payload": True}, blocking=True, ) + assert len(service_calls) == 1 + await knx.assert_write("0/0/1", True) if ( group_value_options.get("group_value_write", True) and direction_options["outgoing"] ): - assert len(calls) == 1 - assert calls.pop().data["catch_all"] == "telegram - 0/0/1" + assert len(service_calls) == 2 + assert service_calls.pop().data["catch_all"] == "telegram - 0/0/1" else: - assert len(calls) == 0 + assert len(service_calls) == 1 async def test_remove_telegram_trigger( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], knx: KNXTestKit, ) -> None: """Test for removed callback when telegram trigger not used.""" @@ -296,8 +290,8 @@ async def test_remove_telegram_trigger( ) await knx.receive_write("0/0/1", (0x03, 0x2F)) - assert len(calls) == 1 - assert calls.pop().data["catch_all"] == "telegram - 0/0/1" + assert len(service_calls) == 1 + assert service_calls.pop().data["catch_all"] == "telegram - 0/0/1" await hass.services.async_call( automation.DOMAIN, @@ -305,8 +299,10 @@ async def test_remove_telegram_trigger( {ATTR_ENTITY_ID: f"automation.{automation_name}"}, blocking=True, ) + assert len(service_calls) == 1 + await knx.receive_write("0/0/1", (0x03, 0x2F)) - assert len(calls) == 0 + assert len(service_calls) == 1 async def test_invalid_trigger( From c9911fa8ce500160d3faa8871d00ad97f03e48a7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:16:19 +0200 Subject: [PATCH 0535/2411] Use service_calls fixture in hue tests (#120928) --- tests/components/hue/conftest.py | 14 +------------- tests/components/hue/test_device_trigger_v1.py | 8 ++++---- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index fca950d6b7a..43a3b1518b7 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -16,17 +16,11 @@ from homeassistant.components import hue from homeassistant.components.hue.v1 import sensor_base as hue_sensor_base from homeassistant.components.hue.v2.device import async_setup_devices from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component from .const import FAKE_BRIDGE, FAKE_BRIDGE_DEVICE -from tests.common import ( - MockConfigEntry, - async_mock_service, - load_fixture, - mock_device_registry, -) +from tests.common import MockConfigEntry, load_fixture, mock_device_registry @pytest.fixture(autouse=True) @@ -288,9 +282,3 @@ async def setup_platform( def get_device_reg(hass): """Return an empty, loaded, registry.""" return mock_device_registry(hass) - - -@pytest.fixture(name="calls") -def track_calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") diff --git a/tests/components/hue/test_device_trigger_v1.py b/tests/components/hue/test_device_trigger_v1.py index 3d8fa64baf4..facd267cad9 100644 --- a/tests/components/hue/test_device_trigger_v1.py +++ b/tests/components/hue/test_device_trigger_v1.py @@ -92,7 +92,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, mock_bridge_v1, device_reg: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for button press trigger firing.""" mock_bridge_v1.mock_sensor_responses.append(REMOTES_RESPONSE) @@ -158,8 +158,8 @@ async def test_if_fires_on_state_change( assert len(mock_bridge_v1.mock_requests) == 2 - assert len(calls) == 1 - assert calls[0].data["some"] == "B4 - 18" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "B4 - 18" # Fake another button press. new_sensor_response["7"] = dict(new_sensor_response["7"]) @@ -173,4 +173,4 @@ async def test_if_fires_on_state_change( await mock_bridge_v1.sensor_manager.coordinator.async_refresh() await hass.async_block_till_done() assert len(mock_bridge_v1.mock_requests) == 3 - assert len(calls) == 1 + assert len(service_calls) == 1 From 2506acc095ce85867a803c8c473e010566b91133 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 1 Jul 2024 07:41:47 -0700 Subject: [PATCH 0536/2411] Improve flume test coverage (#120851) * Add Flume init tests * Increase test coverage * Improve readability * Fix pydoc for tests * Use pytest.mark.usefixtures --- .coveragerc | 1 - tests/components/flume/conftest.py | 167 +++++++++++++++++++ tests/components/flume/test_config_flow.py | 177 +++++++++++---------- tests/components/flume/test_init.py | 135 ++++++++++++++++ 4 files changed, 392 insertions(+), 88 deletions(-) create mode 100644 tests/components/flume/conftest.py create mode 100644 tests/components/flume/test_init.py diff --git a/.coveragerc b/.coveragerc index 2bc76723445..c3ab7f1006f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -441,7 +441,6 @@ omit = homeassistant/components/flick_electric/__init__.py homeassistant/components/flick_electric/sensor.py homeassistant/components/flock/notify.py - homeassistant/components/flume/__init__.py homeassistant/components/flume/binary_sensor.py homeassistant/components/flume/coordinator.py homeassistant/components/flume/entity.py diff --git a/tests/components/flume/conftest.py b/tests/components/flume/conftest.py new file mode 100644 index 00000000000..999bbd70ce8 --- /dev/null +++ b/tests/components/flume/conftest.py @@ -0,0 +1,167 @@ +"""Flume test fixtures.""" + +from collections.abc import Generator +import datetime +from http import HTTPStatus +import json +from unittest.mock import mock_open, patch + +import jwt +import pytest +import requests +from requests_mock.mocker import Mocker + +from homeassistant.components.flume.const import DOMAIN +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_PASSWORD, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +USER_ID = "test-user-id" +REFRESH_TOKEN = "refresh-token" +TOKEN_URL = "https://api.flumetech.com/oauth/token" +DEVICE_LIST_URL = ( + "https://api.flumetech.com/users/test-user-id/devices?user=true&location=true" +) +BRIDGE_DEVICE = { + "id": "1234", + "type": 1, # Bridge + "location": { + "name": "Bridge Location", + }, + "name": "Flume Bridge", + "connected": True, +} +SENSOR_DEVICE = { + "id": "1234", + "type": 2, # Sensor + "location": { + "name": "Sensor Location", + }, + "name": "Flume Sensor", + "connected": True, +} +DEVICE_LIST = [BRIDGE_DEVICE, SENSOR_DEVICE] +NOTIFICATIONS_URL = "https://api.flumetech.com/users/test-user-id/notifications?limit=50&offset=0&sort_direction=ASC" +NOTIFICATION = { + "id": 111111, + "device_id": "6248148189204194987", + "user_id": USER_ID, + "type": 1, + "message": "Low Flow Leak triggered at Home. Water has been running for 2 hours averaging 0.43 gallons every minute.", + "created_datetime": "2020-01-15T16:33:39.000Z", + "title": "Potential Leak Detected!", + "read": True, + "extra": { + "query": { + "request_id": "SYSTEM_TRIGGERED_USAGE_ALERT", + "since_datetime": "2020-01-15 06:33:59", + "until_datetime": "2020-01-15 08:33:59", + "tz": "America/Los_Angeles", + "bucket": "MIN", + "raw": False, + "group_multiplier": 2, + "device_id": ["6248148189204194987"], + } + }, + "event_rule": "Low Flow Leak", +} + +NOTIFICATIONS_LIST = [NOTIFICATION] + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(hass: HomeAssistant) -> MockConfigEntry: + """Fixture to create a config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="test-username", + unique_id="test-username", + data={ + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + }, + ) + config_entry.add_to_hass(hass) + return config_entry + + +def encode_access_token() -> str: + """Encode the payload of the access token.""" + expiration_time = datetime.datetime.now() + datetime.timedelta(hours=12) + payload = { + "user_id": USER_ID, + "exp": int(expiration_time.timestamp()), + } + return jwt.encode(payload, key="secret") + + +@pytest.fixture(name="access_token") +def access_token_fixture(requests_mock: Mocker) -> Generator[None, None, None]: + """Fixture to setup the access token.""" + token_response = { + "refresh_token": REFRESH_TOKEN, + "access_token": encode_access_token(), + } + requests_mock.register_uri( + "POST", + TOKEN_URL, + status_code=HTTPStatus.OK, + json={"data": [token_response]}, + ) + with patch("builtins.open", mock_open(read_data=json.dumps(token_response))): + yield + + +@pytest.fixture(name="device_list") +def device_list_fixture(requests_mock: Mocker) -> None: + """Fixture to setup the device list API response access token.""" + requests_mock.register_uri( + "GET", + DEVICE_LIST_URL, + status_code=HTTPStatus.OK, + json={ + "data": DEVICE_LIST, + }, + ) + + +@pytest.fixture(name="device_list_timeout") +def device_list_timeout_fixture(requests_mock: Mocker) -> None: + """Fixture to test a timeout when connecting to the device list url.""" + requests_mock.register_uri( + "GET", + DEVICE_LIST_URL, + exc=requests.exceptions.ConnectTimeout, + ) + + +@pytest.fixture(name="device_list_unauthorized") +def device_list_unauthorized_fixture(requests_mock: Mocker) -> None: + """Fixture to test an authorized error from the device list url.""" + requests_mock.register_uri( + "GET", + DEVICE_LIST_URL, + status_code=HTTPStatus.UNAUTHORIZED, + json={}, + ) + + +@pytest.fixture(name="notifications_list") +def notifications_list_fixture(requests_mock: Mocker) -> None: + """Fixture to setup the device list API response access token.""" + requests_mock.register_uri( + "GET", + NOTIFICATIONS_URL, + status_code=HTTPStatus.OK, + json={ + "data": NOTIFICATIONS_LIST, + }, + ) diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index 706cee44739..915299223e9 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -1,8 +1,11 @@ """Test the flume config flow.""" -from unittest.mock import MagicMock, patch +from http import HTTPStatus +from unittest.mock import patch +import pytest import requests.exceptions +from requests_mock.mocker import Mocker from homeassistant import config_entries from homeassistant.components.flume.const import DOMAIN @@ -15,15 +18,12 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from .conftest import DEVICE_LIST, DEVICE_LIST_URL + from tests.common import MockConfigEntry -def _get_mocked_flume_device_list(): - flume_device_list_mock = MagicMock() - type(flume_device_list_mock).device_list = ["mock"] - return flume_device_list_mock - - +@pytest.mark.usefixtures("access_token", "device_list") async def test_form(hass: HomeAssistant) -> None: """Test we get the form and can setup from user input.""" @@ -33,17 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - mock_flume_device_list = _get_mocked_flume_device_list() - with ( - patch( - "homeassistant.components.flume.config_flow.FlumeAuth", - return_value=True, - ), - patch( - "homeassistant.components.flume.config_flow.FlumeDeviceList", - return_value=mock_flume_device_list, - ), patch( "homeassistant.components.flume.async_setup_entry", return_value=True, @@ -71,66 +61,57 @@ async def test_form(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_invalid_auth(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("access_token") +async def test_form_invalid_auth(hass: HomeAssistant, requests_mock: Mocker) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with ( - patch( - "homeassistant.components.flume.config_flow.FlumeAuth", - return_value=True, - ), - patch( - "homeassistant.components.flume.config_flow.FlumeDeviceList", - side_effect=Exception, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - CONF_CLIENT_ID: "client_id", - CONF_CLIENT_SECRET: "client_secret", - }, - ) + requests_mock.register_uri( + "GET", + DEVICE_LIST_URL, + status_code=HTTPStatus.UNAUTHORIZED, + json={"message": "Failure"}, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + }, + ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} +@pytest.mark.usefixtures("access_token", "device_list_timeout") async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with ( - patch( - "homeassistant.components.flume.config_flow.FlumeAuth", - return_value=True, - ), - patch( - "homeassistant.components.flume.config_flow.FlumeDeviceList", - side_effect=requests.exceptions.ConnectionError(), - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - CONF_CLIENT_ID: "client_id", - CONF_CLIENT_SECRET: "client_secret", - }, - ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + }, + ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} -async def test_reauth(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("access_token") +async def test_reauth(hass: HomeAssistant, requests_mock: Mocker) -> None: """Test we can reauth.""" entry = MockConfigEntry( domain=DOMAIN, @@ -151,35 +132,28 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with ( - patch( - "homeassistant.components.flume.config_flow.FlumeAuth", - return_value=True, - ), - patch( - "homeassistant.components.flume.config_flow.FlumeDeviceList", - side_effect=Exception, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_PASSWORD: "test-password", - }, - ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} + requests_mock.register_uri( + "GET", + DEVICE_LIST_URL, + exc=requests.exceptions.ConnectTimeout, + ) + with ( patch( - "homeassistant.components.flume.config_flow.FlumeAuth", + "homeassistant.components.flume.config_flow.os.path.exists", return_value=True, ), - patch( - "homeassistant.components.flume.config_flow.FlumeDeviceList", - side_effect=requests.exceptions.ConnectionError(), - ), + patch("homeassistant.components.flume.config_flow.os.unlink") as mock_unlink, ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -187,21 +161,22 @@ async def test_reauth(hass: HomeAssistant) -> None: CONF_PASSWORD: "test-password", }, ) + # The existing token file was removed + assert len(mock_unlink.mock_calls) == 1 assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} - mock_flume_device_list = _get_mocked_flume_device_list() + requests_mock.register_uri( + "GET", + DEVICE_LIST_URL, + status_code=HTTPStatus.OK, + json={ + "data": DEVICE_LIST, + }, + ) with ( - patch( - "homeassistant.components.flume.config_flow.FlumeAuth", - return_value=True, - ), - patch( - "homeassistant.components.flume.config_flow.FlumeDeviceList", - return_value=mock_flume_device_list, - ), patch( "homeassistant.components.flume.async_setup_entry", return_value=True, @@ -217,3 +192,31 @@ async def test_reauth(hass: HomeAssistant) -> None: assert mock_setup_entry.called assert result4["type"] is FlowResultType.ABORT assert result4["reason"] == "reauth_successful" + + +@pytest.mark.usefixtures("access_token") +async def test_form_no_devices(hass: HomeAssistant, requests_mock: Mocker) -> None: + """Test a device list response that contains no values will raise an error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + requests_mock.register_uri( + "GET", + DEVICE_LIST_URL, + status_code=HTTPStatus.OK, + json={"data": []}, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + }, + ) + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/flume/test_init.py b/tests/components/flume/test_init.py new file mode 100644 index 00000000000..44a66425949 --- /dev/null +++ b/tests/components/flume/test_init.py @@ -0,0 +1,135 @@ +"""Test the flume init.""" + +from collections.abc import Generator +from unittest.mock import patch + +import pytest +from requests_mock.mocker import Mocker + +from homeassistant import config_entries +from homeassistant.components.flume.const import DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .conftest import USER_ID + +from tests.common import MockConfigEntry + + +@pytest.fixture(autouse=True) +def platforms_fixture() -> Generator[list[str]]: + """Return the platforms to be loaded for this test.""" + # Arbitrary platform to ensure notifications are loaded + with patch("homeassistant.components.flume.PLATFORMS", [Platform.BINARY_SENSOR]): + yield + + +@pytest.mark.usefixtures("access_token", "device_list") +async def test_setup_config_entry( + hass: HomeAssistant, + requests_mock: Mocker, + config_entry: MockConfigEntry, +) -> None: + """Test load and unload of a ConfigEntry.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is config_entries.ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED + + +@pytest.mark.usefixtures("access_token", "device_list_timeout") +async def test_device_list_timeout( + hass: HomeAssistant, + requests_mock: Mocker, + config_entry: MockConfigEntry, +) -> None: + """Test error handling for a timeout when listing devices.""" + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is config_entries.ConfigEntryState.SETUP_RETRY + + +@pytest.mark.usefixtures("access_token", "device_list_unauthorized") +async def test_reauth_when_unauthorized( + hass: HomeAssistant, + requests_mock: Mocker, + config_entry: MockConfigEntry, +) -> None: + """Test error handling for an authentication error when listing devices.""" + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + + +@pytest.mark.usefixtures("access_token", "device_list", "notifications_list") +async def test_list_notifications_service( + hass: HomeAssistant, + requests_mock: Mocker, + config_entry: MockConfigEntry, +) -> None: + """Test the list notifications service.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is config_entries.ConfigEntryState.LOADED + + response = await hass.services.async_call( + DOMAIN, + "list_notifications", + {}, + target={ + "config_entry": config_entry.entry_id, + }, + blocking=True, + return_response=True, + ) + notifications = response.get("notifications") + assert notifications + assert len(notifications) == 1 + assert notifications[0].get("user_id") == USER_ID + + +@pytest.mark.usefixtures("access_token", "device_list", "notifications_list") +async def test_list_notifications_service_config_entry_errors( + hass: HomeAssistant, + requests_mock: Mocker, + config_entry: MockConfigEntry, +) -> None: + """Test error handling for notification service with invalid config entries.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is config_entries.ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED + + with pytest.raises(ValueError, match="Config entry not loaded"): + await hass.services.async_call( + DOMAIN, + "list_notifications", + {}, + target={ + "config_entry": config_entry.entry_id, + }, + blocking=True, + return_response=True, + ) + + with pytest.raises(ValueError, match="Invalid config entry: does-not-exist"): + await hass.services.async_call( + DOMAIN, + "list_notifications", + {}, + target={ + "config_entry": "does-not-exist", + }, + blocking=True, + return_response=True, + ) From 38aa6bcf195017afd729ea789dc9e928eccf7694 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:50:35 +0100 Subject: [PATCH 0537/2411] Bump python-kasa to 0.7.0.2 (#120940) --- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 74b80771c65..1270bb3469b 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -297,5 +297,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.7.0.1"] + "requirements": ["python-kasa[speedups]==0.7.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 10087dc62d3..1b6315d8953 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2275,7 +2275,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.1 +python-kasa[speedups]==0.7.0.2 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0ac1e41b0d..91dd535f8bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1775,7 +1775,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.1 +python-kasa[speedups]==0.7.0.2 # homeassistant.components.matter python-matter-server==6.2.0b1 From 2815c43f3ee7b60c65cf56e6daa40bba21dc239b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:24:36 +0200 Subject: [PATCH 0538/2411] Use service_calls fixture in lutron_caseta tests (#120934) --- .../lutron_caseta/test_device_trigger.py | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 208dd36cccd..3e97be67da1 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -39,11 +39,7 @@ from homeassistant.setup import async_setup_component from . import MockBridge -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) +from tests.common import MockConfigEntry, async_get_device_automations MOCK_BUTTON_DEVICES = [ { @@ -102,12 +98,6 @@ MOCK_BUTTON_DEVICES = [ ] -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def _async_setup_lutron_with_picos(hass): """Setups a lutron bridge with picos.""" config_entry = MockConfigEntry( @@ -220,7 +210,9 @@ async def test_none_serial_keypad( async def test_if_fires_on_button_event( - hass: HomeAssistant, calls: list[ServiceCall], device_registry: dr.DeviceRegistry + hass: HomeAssistant, + service_calls: list[ServiceCall], + device_registry: dr.DeviceRegistry, ) -> None: """Test for press trigger firing.""" await _async_setup_lutron_with_picos(hass) @@ -266,12 +258,14 @@ async def test_if_fires_on_button_event( 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" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test_trigger_button_press" async def test_if_fires_on_button_event_without_lip( - hass: HomeAssistant, calls: list[ServiceCall], device_registry: dr.DeviceRegistry + hass: HomeAssistant, + service_calls: list[ServiceCall], + device_registry: dr.DeviceRegistry, ) -> None: """Test for press trigger firing on a device that does not support lip.""" await _async_setup_lutron_with_picos(hass) @@ -315,12 +309,12 @@ async def test_if_fires_on_button_event_without_lip( 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" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test_trigger_button_press" async def test_validate_trigger_config_no_device( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for no press with no device.""" @@ -356,11 +350,11 @@ async def test_validate_trigger_config_no_device( hass.bus.async_fire(LUTRON_CASETA_BUTTON_EVENT, message) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_validate_trigger_config_unknown_device( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for no press with an unknown device.""" @@ -404,7 +398,7 @@ async def test_validate_trigger_config_unknown_device( hass.bus.async_fire(LUTRON_CASETA_BUTTON_EVENT, message) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_validate_trigger_invalid_triggers( @@ -444,7 +438,9 @@ async def test_validate_trigger_invalid_triggers( async def test_if_fires_on_button_event_late_setup( - hass: HomeAssistant, calls: list[ServiceCall], device_registry: dr.DeviceRegistry + hass: HomeAssistant, + service_calls: list[ServiceCall], + device_registry: dr.DeviceRegistry, ) -> None: """Test for press trigger firing with integration getting setup late.""" config_entry_id = await _async_setup_lutron_with_picos(hass) @@ -495,5 +491,5 @@ async def test_if_fires_on_button_event_late_setup( 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" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test_trigger_button_press" From c8bb64882e5c2ea839ce99fa2085e5f3cb851191 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:26:41 +0200 Subject: [PATCH 0539/2411] Use service_calls fixture in homeassistant tests (#120922) --- .../homeassistant/triggers/test_event.py | 113 +++--- .../triggers/test_numeric_state.py | 325 ++++++++-------- .../homeassistant/triggers/test_state.py | 363 +++++++++--------- .../homeassistant/triggers/test_time.py | 110 +++--- .../triggers/test_time_pattern.py | 75 ++-- 5 files changed, 515 insertions(+), 471 deletions(-) diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index b7bf8e5e7f3..293a9007175 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -7,28 +7,24 @@ from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_O from homeassistant.core import Context, HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component -from tests.common import async_mock_service, mock_component +from tests.common import mock_component @pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - -@pytest.fixture -def context_with_user(): +def context_with_user() -> Context: """Create a context with default user_id.""" return Context(user_id="test_user_id") @pytest.fixture(autouse=True) -def setup_comp(hass): +def setup_comp(hass: HomeAssistant) -> None: """Initialize components.""" mock_component(hass, "group") -async def test_if_fires_on_event(hass: HomeAssistant, calls: list[ServiceCall]) -> None: +async def test_if_fires_on_event( + hass: HomeAssistant, service_calls: list[ServiceCall] +) -> None: """Test the firing of events.""" context = Context() @@ -48,8 +44,8 @@ async def test_if_fires_on_event(hass: HomeAssistant, calls: list[ServiceCall]) hass.bus.async_fire("test_event", context=context) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context.parent_id == context.id + assert len(service_calls) == 1 + assert service_calls[0].context.parent_id == context.id await hass.services.async_call( automation.DOMAIN, @@ -57,15 +53,16 @@ async def test_if_fires_on_event(hass: HomeAssistant, calls: list[ServiceCall]) {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 2 hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["id"] == 0 + assert len(service_calls) == 2 + assert service_calls[0].data["id"] == 0 async def test_if_fires_on_templated_event( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test the firing of events.""" context = Context() @@ -84,8 +81,8 @@ async def test_if_fires_on_templated_event( hass.bus.async_fire("test_event", context=context) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context.parent_id == context.id + assert len(service_calls) == 1 + assert service_calls[0].context.parent_id == context.id await hass.services.async_call( automation.DOMAIN, @@ -93,14 +90,15 @@ async def test_if_fires_on_templated_event( {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 2 hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 2 async def test_if_fires_on_multiple_events( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test the firing of events.""" context = Context() @@ -123,13 +121,13 @@ async def test_if_fires_on_multiple_events( await hass.async_block_till_done() hass.bus.async_fire("test2_event", context=context) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[0].context.parent_id == context.id - assert calls[1].context.parent_id == context.id + assert len(service_calls) == 2 + assert service_calls[0].context.parent_id == context.id + assert service_calls[1].context.parent_id == context.id async def test_if_fires_on_event_extra_data( - hass: HomeAssistant, calls: list[ServiceCall], context_with_user: Context + hass: HomeAssistant, service_calls: list[ServiceCall], context_with_user: Context ) -> None: """Test the firing of events still matches with event data and context.""" assert await async_setup_component( @@ -146,7 +144,7 @@ async def test_if_fires_on_event_extra_data( "test_event", {"extra_key": "extra_data"}, context=context_with_user ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await hass.services.async_call( automation.DOMAIN, @@ -154,14 +152,15 @@ async def test_if_fires_on_event_extra_data( {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 2 hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 2 async def test_if_fires_on_event_with_data_and_context( - hass: HomeAssistant, calls: list[ServiceCall], context_with_user: Context + hass: HomeAssistant, service_calls: list[ServiceCall], context_with_user: Context ) -> None: """Test the firing of events with data and context.""" assert await async_setup_component( @@ -189,7 +188,7 @@ async def test_if_fires_on_event_with_data_and_context( context=context_with_user, ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.bus.async_fire( "test_event", @@ -197,18 +196,18 @@ async def test_if_fires_on_event_with_data_and_context( context=context_with_user, ) await hass.async_block_till_done() - assert len(calls) == 1 # No new call + assert len(service_calls) == 1 # No new call hass.bus.async_fire( "test_event", {"some_attr": "some_value", "another": "value", "second_attr": "second_value"}, ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_event_with_templated_data_and_context( - hass: HomeAssistant, calls: list[ServiceCall], context_with_user: Context + hass: HomeAssistant, service_calls: list[ServiceCall], context_with_user: Context ) -> None: """Test the firing of events with templated data and context.""" assert await async_setup_component( @@ -241,7 +240,7 @@ async def test_if_fires_on_event_with_templated_data_and_context( context=context_with_user, ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.bus.async_fire( "test_event", @@ -249,18 +248,18 @@ async def test_if_fires_on_event_with_templated_data_and_context( context=context_with_user, ) await hass.async_block_till_done() - assert len(calls) == 1 # No new call + assert len(service_calls) == 1 # No new call hass.bus.async_fire( "test_event", {"attr_1": "milk", "another": "value", "attr_2": "beer"}, ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_event_with_empty_data_and_context_config( - hass: HomeAssistant, calls: list[ServiceCall], context_with_user: Context + hass: HomeAssistant, service_calls: list[ServiceCall], context_with_user: Context ) -> None: """Test the firing of events with empty data and context config. @@ -289,11 +288,11 @@ async def test_if_fires_on_event_with_empty_data_and_context_config( context=context_with_user, ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_event_with_nested_data( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test the firing of events with nested data. @@ -319,11 +318,11 @@ async def test_if_fires_on_event_with_nested_data( "test_event", {"parent_attr": {"some_attr": "some_value", "another": "value"}} ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_event_with_empty_data( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test the firing of events with empty data. @@ -345,11 +344,11 @@ async def test_if_fires_on_event_with_empty_data( ) hass.bus.async_fire("test_event", {"any_attr": {}}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_sample_zha_event( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test the firing of events with a sample zha event. @@ -390,7 +389,7 @@ async def test_if_fires_on_sample_zha_event( }, ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.bus.async_fire( "zha_event", @@ -404,11 +403,11 @@ async def test_if_fires_on_sample_zha_event( }, ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_not_fires_if_event_data_not_matches( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test firing of event if no data match.""" assert await async_setup_component( @@ -428,11 +427,11 @@ async def test_if_not_fires_if_event_data_not_matches( hass.bus.async_fire("test_event", {"some_attr": "some_other_value"}) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_if_not_fires_if_event_context_not_matches( - hass: HomeAssistant, calls: list[ServiceCall], context_with_user: Context + hass: HomeAssistant, service_calls: list[ServiceCall], context_with_user: Context ) -> None: """Test firing of event if no context match.""" assert await async_setup_component( @@ -452,11 +451,11 @@ async def test_if_not_fires_if_event_context_not_matches( hass.bus.async_fire("test_event", {}, context=context_with_user) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_if_fires_on_multiple_user_ids( - hass: HomeAssistant, calls: list[ServiceCall], context_with_user: Context + hass: HomeAssistant, service_calls: list[ServiceCall], context_with_user: Context ) -> None: """Test the firing of event when the trigger has multiple user ids. @@ -481,11 +480,11 @@ async def test_if_fires_on_multiple_user_ids( hass.bus.async_fire("test_event", {}, context=context_with_user) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_event_data_with_list( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test the (non)firing of event when the data schema has lists.""" assert await async_setup_component( @@ -506,17 +505,17 @@ async def test_event_data_with_list( hass.bus.async_fire("test_event", {"some_attr": [1, 2]}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # don't match a single value hass.bus.async_fire("test_event", {"some_attr": 1}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # don't match a containing list hass.bus.async_fire("test_event", {"some_attr": [1, 2, 3]}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize( @@ -524,7 +523,7 @@ async def test_event_data_with_list( ) async def test_state_reported_event( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], caplog: pytest.LogCaptureFixture, event_type: str | list[str], ) -> None: @@ -547,7 +546,7 @@ async def test_state_reported_event( hass.bus.async_fire("test_event", context=context) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 assert ( "Unnamed automation failed to setup triggers and has been disabled: Can't " "listen to state_reported in event trigger for dictionary value @ " @@ -556,7 +555,9 @@ async def test_state_reported_event( async def test_templated_state_reported_event( - hass: HomeAssistant, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + service_calls: list[ServiceCall], + caplog: pytest.LogCaptureFixture, ) -> None: """Test triggering on state reported event.""" context = Context() @@ -578,7 +579,7 @@ async def test_templated_state_reported_event( hass.bus.async_fire("test_event", context=context) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 assert ( "Got error 'Can't listen to state_reported in event trigger' " "when setting up triggers for automation 0" in caplog.text diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 59cd7e2a2a7..85882274fec 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -23,22 +23,11 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import ( - assert_setup_component, - async_fire_time_changed, - async_mock_service, - mock_component, -) - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") +from tests.common import assert_setup_component, async_fire_time_changed, mock_component @pytest.fixture(autouse=True) -async def setup_comp(hass): +async def setup_comp(hass: HomeAssistant) -> None: """Initialize components.""" mock_component(hass, "group") await async_setup_component( @@ -63,7 +52,7 @@ async def setup_comp(hass): "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_not_fires_on_entity_removal( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test the firing with removed entity.""" hass.states.async_set("test.entity", 11) @@ -86,14 +75,14 @@ async def test_if_not_fires_on_entity_removal( # Entity disappears hass.states.async_remove("test.entity") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize( "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_fires_on_entity_change_below( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) @@ -120,8 +109,8 @@ async def test_if_fires_on_entity_change_below( # 9 is below 10 hass.states.async_set("test.entity", 9, context=context) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context.parent_id == context.id + assert len(service_calls) == 1 + assert service_calls[0].context.parent_id == context.id # Set above 12 so the automation will fire again hass.states.async_set("test.entity", 12) @@ -132,10 +121,12 @@ async def test_if_fires_on_entity_change_below( {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 2 + hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["id"] == 0 + assert len(service_calls) == 2 + assert service_calls[0].data["id"] == 0 @pytest.mark.parametrize( @@ -144,7 +135,7 @@ async def test_if_fires_on_entity_change_below( async def test_if_fires_on_entity_change_below_uuid( hass: HomeAssistant, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], below: int | str, ) -> None: """Test the firing with changed entity specified by registry entry id.""" @@ -177,8 +168,8 @@ async def test_if_fires_on_entity_change_below_uuid( # 9 is below 10 hass.states.async_set("test.entity", 9, context=context) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context.parent_id == context.id + assert len(service_calls) == 1 + assert service_calls[0].context.parent_id == context.id # Set above 12 so the automation will fire again hass.states.async_set("test.entity", 12) @@ -189,17 +180,19 @@ async def test_if_fires_on_entity_change_below_uuid( {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 2 + hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["id"] == 0 + assert len(service_calls) == 2 + assert service_calls[0].data["id"] == 0 @pytest.mark.parametrize( "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_fires_on_entity_change_over_to_below( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) @@ -223,14 +216,14 @@ async def test_if_fires_on_entity_change_over_to_below( # 9 is below 10 hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize( "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_fires_on_entities_change_over_to_below( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test the firing with changed entities.""" hass.states.async_set("test.entity_1", 11) @@ -255,17 +248,17 @@ async def test_if_fires_on_entities_change_over_to_below( # 9 is below 10 hass.states.async_set("test.entity_1", 9) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.states.async_set("test.entity_2", 9) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 @pytest.mark.parametrize( "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_not_fires_on_entity_change_below_to_below( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test the firing with changed entity.""" context = Context() @@ -290,25 +283,25 @@ async def test_if_not_fires_on_entity_change_below_to_below( # 9 is below 10 so this should fire hass.states.async_set("test.entity", 9, context=context) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context.parent_id == context.id + assert len(service_calls) == 1 + assert service_calls[0].context.parent_id == context.id # already below so should not fire again hass.states.async_set("test.entity", 5) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # still below so should not fire again hass.states.async_set("test.entity", 3) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize( "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_not_below_fires_on_entity_change_to_equal( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) @@ -332,14 +325,14 @@ async def test_if_not_below_fires_on_entity_change_to_equal( # 10 is not below 10 so this should not fire again hass.states.async_set("test.entity", 10) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize( "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_not_fires_on_initial_entity_below( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test the firing when starting with a match.""" hass.states.async_set("test.entity", 9) @@ -363,14 +356,14 @@ async def test_if_not_fires_on_initial_entity_below( # Do not fire on first update when initial state was already below hass.states.async_set("test.entity", 8) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize( "above", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_not_fires_on_initial_entity_above( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], above: int | str ) -> None: """Test the firing when starting with a match.""" hass.states.async_set("test.entity", 11) @@ -394,14 +387,14 @@ async def test_if_not_fires_on_initial_entity_above( # Do not fire on first update when initial state was already above hass.states.async_set("test.entity", 12) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize( "above", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_fires_on_entity_change_above( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], above: int | str ) -> None: """Test the firing with changed entity.""" hass.states.async_set("test.entity", 9) @@ -424,11 +417,11 @@ async def test_if_fires_on_entity_change_above( # 11 is above 10 hass.states.async_set("test.entity", 11) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_entity_unavailable_at_startup( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test the firing with changed entity at startup.""" assert await async_setup_component( @@ -448,12 +441,12 @@ async def test_if_fires_on_entity_unavailable_at_startup( # 11 is above 10 hass.states.async_set("test.entity", 11) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize("above", [10, "input_number.value_10"]) async def test_if_fires_on_entity_change_below_to_above( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], above: int | str ) -> None: """Test the firing with changed entity.""" # set initial state @@ -478,12 +471,12 @@ async def test_if_fires_on_entity_change_below_to_above( # 11 is above 10 and 9 is below hass.states.async_set("test.entity", 11) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize("above", [10, "input_number.value_10"]) async def test_if_not_fires_on_entity_change_above_to_above( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], above: int | str ) -> None: """Test the firing with changed entity.""" # set initial state @@ -508,17 +501,17 @@ async def test_if_not_fires_on_entity_change_above_to_above( # 12 is above 10 so this should fire hass.states.async_set("test.entity", 12) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # already above, should not fire again hass.states.async_set("test.entity", 15) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize("above", [10, "input_number.value_10"]) async def test_if_not_above_fires_on_entity_change_to_equal( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], above: int | str ) -> None: """Test the firing with changed entity.""" # set initial state @@ -543,7 +536,7 @@ async def test_if_not_above_fires_on_entity_change_to_equal( # 10 is not above 10 so this should not fire again hass.states.async_set("test.entity", 10) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize( @@ -556,7 +549,10 @@ async def test_if_not_above_fires_on_entity_change_to_equal( ], ) async def test_if_fires_on_entity_change_below_range( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str, below: int | str + hass: HomeAssistant, + service_calls: list[ServiceCall], + above: int | str, + below: int | str, ) -> None: """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) @@ -580,7 +576,7 @@ async def test_if_fires_on_entity_change_below_range( # 9 is below 10 hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize( @@ -593,7 +589,10 @@ async def test_if_fires_on_entity_change_below_range( ], ) async def test_if_fires_on_entity_change_below_above_range( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str, below: int | str + hass: HomeAssistant, + service_calls: list[ServiceCall], + above: int | str, + below: int | str, ) -> None: """Test the firing with changed entity.""" assert await async_setup_component( @@ -614,7 +613,7 @@ async def test_if_fires_on_entity_change_below_above_range( # 4 is below 5 hass.states.async_set("test.entity", 4) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize( @@ -627,7 +626,10 @@ async def test_if_fires_on_entity_change_below_above_range( ], ) async def test_if_fires_on_entity_change_over_to_below_range( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str, below: int | str + hass: HomeAssistant, + service_calls: list[ServiceCall], + above: int | str, + below: int | str, ) -> None: """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) @@ -652,7 +654,7 @@ async def test_if_fires_on_entity_change_over_to_below_range( # 9 is below 10 hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize( @@ -665,7 +667,10 @@ async def test_if_fires_on_entity_change_over_to_below_range( ], ) async def test_if_fires_on_entity_change_over_to_below_above_range( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str, below: int | str + hass: HomeAssistant, + service_calls: list[ServiceCall], + above: int | str, + below: int | str, ) -> None: """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) @@ -690,12 +695,12 @@ async def test_if_fires_on_entity_change_over_to_below_above_range( # 4 is below 5 so it should not fire hass.states.async_set("test.entity", 4) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize("below", [100, "input_number.value_100"]) async def test_if_not_fires_if_entity_not_match( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test if not fired with non matching entity.""" assert await async_setup_component( @@ -715,11 +720,13 @@ async def test_if_not_fires_if_entity_not_match( hass.states.async_set("test.entity", 11) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_if_not_fires_and_warns_if_below_entity_unknown( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, calls: list[ServiceCall] + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + service_calls: list[ServiceCall], ) -> None: """Test if warns with unknown below entity.""" assert await async_setup_component( @@ -742,7 +749,7 @@ async def test_if_not_fires_and_warns_if_below_entity_unknown( hass.states.async_set("test.entity", 1) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 assert len(caplog.record_tuples) == 1 assert caplog.record_tuples[0][1] == logging.WARNING @@ -750,7 +757,7 @@ async def test_if_not_fires_and_warns_if_below_entity_unknown( @pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_if_fires_on_entity_change_below_with_attribute( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test attributes change.""" hass.states.async_set("test.entity", 11, {"test_attribute": 11}) @@ -773,12 +780,12 @@ async def test_if_fires_on_entity_change_below_with_attribute( # 9 is below 10 hass.states.async_set("test.entity", 9, {"test_attribute": 11}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_if_not_fires_on_entity_change_not_below_with_attribute( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test attributes.""" assert await async_setup_component( @@ -798,12 +805,12 @@ async def test_if_not_fires_on_entity_change_not_below_with_attribute( # 11 is not below 10 hass.states.async_set("test.entity", 11, {"test_attribute": 9}) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_if_fires_on_attribute_change_with_attribute_below( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test attributes change.""" hass.states.async_set("test.entity", "entity", {"test_attribute": 11}) @@ -827,12 +834,12 @@ async def test_if_fires_on_attribute_change_with_attribute_below( # 9 is below 10 hass.states.async_set("test.entity", "entity", {"test_attribute": 9}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_if_not_fires_on_attribute_change_with_attribute_not_below( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test attributes change.""" assert await async_setup_component( @@ -853,12 +860,12 @@ async def test_if_not_fires_on_attribute_change_with_attribute_not_below( # 11 is not below 10 hass.states.async_set("test.entity", "entity", {"test_attribute": 11}) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_if_not_fires_on_entity_change_with_attribute_below( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test attributes change.""" assert await async_setup_component( @@ -879,12 +886,12 @@ async def test_if_not_fires_on_entity_change_with_attribute_below( # 11 is not below 10, entity state value should not be tested hass.states.async_set("test.entity", "9", {"test_attribute": 11}) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_if_not_fires_on_entity_change_with_not_attribute_below( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test attributes change.""" assert await async_setup_component( @@ -905,12 +912,12 @@ async def test_if_not_fires_on_entity_change_with_not_attribute_below( # 11 is not below 10, entity state value should not be tested hass.states.async_set("test.entity", "entity") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test attributes change.""" hass.states.async_set( @@ -937,12 +944,12 @@ async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr( "test.entity", "entity", {"test_attribute": 9, "not_test_attribute": 11} ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_template_list( - hass: HomeAssistant, calls: list[ServiceCall], below: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: int | str ) -> None: """Test template list.""" hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 11]}) @@ -965,12 +972,12 @@ async def test_template_list( # 3 is below 10 hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 3]}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize("below", [10.0, "input_number.value_10"]) async def test_template_string( - hass: HomeAssistant, calls: list[ServiceCall], below: float | str + hass: HomeAssistant, service_calls: list[ServiceCall], below: float | str ) -> None: """Test template string.""" assert await async_setup_component( @@ -1004,15 +1011,15 @@ async def test_template_string( await hass.async_block_till_done() hass.states.async_set("test.entity", "test state 2", {"test_attribute": "0.9"}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"numeric_state - test.entity - {below} - None - test state 1 - test state 2" ) async def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test if not fired changed attributes.""" assert await async_setup_component( @@ -1035,7 +1042,7 @@ async def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr( "test.entity", "entity", {"test_attribute": 11, "not_test_attribute": 9} ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize( @@ -1048,7 +1055,10 @@ async def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr( ], ) async def test_if_action( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str, below: int | str + hass: HomeAssistant, + service_calls: list[ServiceCall], + above: int | str, + below: int | str, ) -> None: """Test if action.""" entity_id = "domain.test_entity" @@ -1073,19 +1083,19 @@ async def test_if_action( hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.states.async_set(entity_id, 8) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.states.async_set(entity_id, 9) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 @pytest.mark.parametrize( @@ -1098,7 +1108,7 @@ async def test_if_action( ], ) async def test_if_fails_setup_bad_for( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str, below: int | str + hass: HomeAssistant, above: int | str, below: int | str ) -> None: """Test for setup failure for bad for.""" hass.states.async_set("test.entity", 5) @@ -1124,9 +1134,7 @@ async def test_if_fails_setup_bad_for( assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE -async def test_if_fails_setup_for_without_above_below( - hass: HomeAssistant, calls: list[ServiceCall] -) -> None: +async def test_if_fails_setup_for_without_above_below(hass: HomeAssistant) -> None: """Test for setup failures for missing above or below.""" with assert_setup_component(1, automation.DOMAIN): assert await async_setup_component( @@ -1158,7 +1166,7 @@ async def test_if_fails_setup_for_without_above_below( async def test_if_not_fires_on_entity_change_with_for( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - calls: list[ServiceCall], + service_calls: list[ServiceCall], above: int | str, below: int | str, ) -> None: @@ -1187,7 +1195,7 @@ async def test_if_not_fires_on_entity_change_with_for( freezer.tick(timedelta(seconds=10)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize( @@ -1200,7 +1208,10 @@ async def test_if_not_fires_on_entity_change_with_for( ], ) async def test_if_not_fires_on_entities_change_with_for_after_stop( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str, below: int | str + hass: HomeAssistant, + service_calls: list[ServiceCall], + above: int | str, + below: int | str, ) -> None: """Test for not firing on entities change with for after stop.""" hass.states.async_set("test.entity_1", 0) @@ -1232,7 +1243,7 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop( await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.states.async_set("test.entity_1", 15) hass.states.async_set("test.entity_2", 15) @@ -1246,9 +1257,11 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop( {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 2 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 2 @pytest.mark.parametrize( @@ -1263,7 +1276,7 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop( async def test_if_fires_on_entity_change_with_for_attribute_change( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - calls: list[ServiceCall], + service_calls: list[ServiceCall], above: int | str, below: int | str, ) -> None: @@ -1294,11 +1307,11 @@ async def test_if_fires_on_entity_change_with_for_attribute_change( async_fire_time_changed(hass) hass.states.async_set("test.entity", 9, attributes={"mock_attr": "attr_change"}) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 freezer.tick(timedelta(seconds=4)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize( @@ -1311,7 +1324,10 @@ async def test_if_fires_on_entity_change_with_for_attribute_change( ], ) async def test_if_fires_on_entity_change_with_for( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str, below: int | str + hass: HomeAssistant, + service_calls: list[ServiceCall], + above: int | str, + below: int | str, ) -> None: """Test for firing on entity change with for.""" hass.states.async_set("test.entity", 0) @@ -1338,12 +1354,12 @@ async def test_if_fires_on_entity_change_with_for( await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize("above", [10, "input_number.value_10"]) async def test_wait_template_with_trigger( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], above: int | str ) -> None: """Test using wait template with 'trigger.entity_id'.""" hass.states.async_set("test.entity", "0") @@ -1381,8 +1397,8 @@ async def test_wait_template_with_trigger( hass.states.async_set("test.entity", "12") hass.states.async_set("test.entity", "8") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "numeric_state - test.entity - 12" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "numeric_state - test.entity - 12" @pytest.mark.parametrize( @@ -1397,7 +1413,7 @@ async def test_wait_template_with_trigger( async def test_if_fires_on_entities_change_no_overlap( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - calls: list[ServiceCall], + service_calls: list[ServiceCall], above: int | str, below: int | str, ) -> None: @@ -1432,16 +1448,16 @@ async def test_if_fires_on_entities_change_no_overlap( freezer.tick(timedelta(seconds=10)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test.entity_1" hass.states.async_set("test.entity_2", 9) await hass.async_block_till_done() freezer.tick(timedelta(seconds=10)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "test.entity_2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "test.entity_2" @pytest.mark.parametrize( @@ -1456,7 +1472,7 @@ async def test_if_fires_on_entities_change_no_overlap( async def test_if_fires_on_entities_change_overlap( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - calls: list[ServiceCall], + service_calls: list[ServiceCall], above: int | str, below: int | str, ) -> None: @@ -1500,18 +1516,18 @@ async def test_if_fires_on_entities_change_overlap( async_fire_time_changed(hass) hass.states.async_set("test.entity_2", 9) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 freezer.tick(timedelta(seconds=3)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test.entity_1" freezer.tick(timedelta(seconds=3)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "test.entity_2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "test.entity_2" @pytest.mark.parametrize( @@ -1524,7 +1540,10 @@ async def test_if_fires_on_entities_change_overlap( ], ) async def test_if_fires_on_change_with_for_template_1( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str, below: int | str + hass: HomeAssistant, + service_calls: list[ServiceCall], + above: int | str, + below: int | str, ) -> None: """Test for firing on change with for template.""" hass.states.async_set("test.entity", 0) @@ -1549,10 +1568,10 @@ async def test_if_fires_on_change_with_for_template_1( hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize( @@ -1565,7 +1584,10 @@ async def test_if_fires_on_change_with_for_template_1( ], ) async def test_if_fires_on_change_with_for_template_2( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str, below: int | str + hass: HomeAssistant, + service_calls: list[ServiceCall], + above: int | str, + below: int | str, ) -> None: """Test for firing on change with for template.""" hass.states.async_set("test.entity", 0) @@ -1590,10 +1612,10 @@ async def test_if_fires_on_change_with_for_template_2( hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize( @@ -1606,7 +1628,10 @@ async def test_if_fires_on_change_with_for_template_2( ], ) async def test_if_fires_on_change_with_for_template_3( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str, below: int | str + hass: HomeAssistant, + service_calls: list[ServiceCall], + above: int | str, + below: int | str, ) -> None: """Test for firing on change with for template.""" hass.states.async_set("test.entity", 0) @@ -1631,14 +1656,14 @@ async def test_if_fires_on_change_with_for_template_3( hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_not_fires_on_error_with_for_template( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for not firing on error with for template.""" hass.states.async_set("test.entity", 0) @@ -1662,17 +1687,17 @@ async def test_if_not_fires_on_error_with_for_template( hass.states.async_set("test.entity", 101) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3)) hass.states.async_set("test.entity", "unavailable") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3)) hass.states.async_set("test.entity", 101) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 @pytest.mark.parametrize( @@ -1685,7 +1710,7 @@ async def test_if_not_fires_on_error_with_for_template( ], ) async def test_invalid_for_template( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str, below: int | str + hass: HomeAssistant, above: int | str, below: int | str ) -> None: """Test for invalid for template.""" hass.states.async_set("test.entity", 0) @@ -1726,7 +1751,7 @@ async def test_invalid_for_template( async def test_if_fires_on_entities_change_overlap_for_template( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - calls: list[ServiceCall], + service_calls: list[ServiceCall], above: int | str, below: int | str, ) -> None: @@ -1773,22 +1798,22 @@ async def test_if_fires_on_entities_change_overlap_for_template( async_fire_time_changed(hass) hass.states.async_set("test.entity_2", 9) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 freezer.tick(timedelta(seconds=3)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1 - 0:00:05" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test.entity_1 - 0:00:05" freezer.tick(timedelta(seconds=3)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 freezer.tick(timedelta(seconds=5)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "test.entity_2 - 0:00:10" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "test.entity_2 - 0:00:10" async def test_below_above(hass: HomeAssistant) -> None: @@ -1823,7 +1848,7 @@ async def test_schema_unacceptable_entities(hass: HomeAssistant) -> None: @pytest.mark.parametrize("above", [3, "input_number.value_3"]) async def test_attribute_if_fires_on_entity_change_with_both_filters( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], above: int | str ) -> None: """Test for firing if both filters are match attribute.""" hass.states.async_set("test.entity", "bla", {"test-measurement": 1}) @@ -1847,12 +1872,12 @@ async def test_attribute_if_fires_on_entity_change_with_both_filters( hass.states.async_set("test.entity", "bla", {"test-measurement": 4}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize("above", [3, "input_number.value_3"]) async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( - hass: HomeAssistant, calls: list[ServiceCall], above: int | str + hass: HomeAssistant, service_calls: list[ServiceCall], above: int | str ) -> None: """Test for not firing on entity change with for after stop trigger.""" hass.states.async_set("test.entity", "bla", {"test-measurement": 1}) @@ -1880,10 +1905,10 @@ async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( hass.states.async_set("test.entity", "bla", {"test-measurement": 4}) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 @pytest.mark.parametrize( @@ -1893,7 +1918,7 @@ async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( async def test_variables_priority( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - calls: list[ServiceCall], + service_calls: list[ServiceCall], above: int, below: int, ) -> None: @@ -1941,17 +1966,17 @@ async def test_variables_priority( async_fire_time_changed(hass) hass.states.async_set("test.entity_2", 9) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 freezer.tick(timedelta(seconds=3)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1 - 0:00:05" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test.entity_1 - 0:00:05" @pytest.mark.parametrize("multiplier", [1, 5]) async def test_template_variable( - hass: HomeAssistant, calls: list[ServiceCall], multiplier: int + hass: HomeAssistant, service_calls: list[ServiceCall], multiplier: int ) -> None: """Test template variable.""" hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 11]}) @@ -1976,6 +2001,6 @@ async def test_template_variable( hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 3]}) await hass.async_block_till_done() if multiplier * 3 < 10: - assert len(calls) == 1 + assert len(service_calls) == 1 else: - assert len(calls) == 0 + assert len(service_calls) == 0 diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index a40ecae7579..83157a158a6 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -19,29 +19,18 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import ( - assert_setup_component, - async_fire_time_changed, - async_mock_service, - mock_component, -) - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") +from tests.common import assert_setup_component, async_fire_time_changed, mock_component @pytest.fixture(autouse=True) -def setup_comp(hass): +def setup_comp(hass: HomeAssistant) -> None: """Initialize components.""" mock_component(hass, "group") hass.states.async_set("test.entity", "hello") async def test_if_fires_on_entity_change( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on entity change.""" context = Context() @@ -74,9 +63,12 @@ async def test_if_fires_on_entity_change( hass.states.async_set("test.entity", "world", context=context) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context.parent_id == context.id - assert calls[0].data["some"] == "state - test.entity - hello - world - None - 0" + assert len(service_calls) == 1 + assert service_calls[0].context.parent_id == context.id + assert ( + service_calls[0].data["some"] + == "state - test.entity - hello - world - None - 0" + ) await hass.services.async_call( automation.DOMAIN, @@ -84,13 +76,16 @@ async def test_if_fires_on_entity_change( {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 2 hass.states.async_set("test.entity", "planet") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 2 async def test_if_fires_on_entity_change_uuid( - hass: HomeAssistant, entity_registry: er.EntityRegistry, calls: list[ServiceCall] + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + service_calls: list[ServiceCall], ) -> None: """Test for firing on entity change.""" context = Context() @@ -130,9 +125,11 @@ async def test_if_fires_on_entity_change_uuid( hass.states.async_set("test.beer", "world", context=context) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context.parent_id == context.id - assert calls[0].data["some"] == "state - test.beer - hello - world - None - 0" + assert len(service_calls) == 1 + assert service_calls[0].context.parent_id == context.id + assert ( + service_calls[0].data["some"] == "state - test.beer - hello - world - None - 0" + ) await hass.services.async_call( automation.DOMAIN, @@ -140,13 +137,14 @@ async def test_if_fires_on_entity_change_uuid( {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 2 hass.states.async_set("test.beer", "planet") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 2 async def test_if_fires_on_entity_change_with_from_filter( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on entity change with filter.""" assert await async_setup_component( @@ -167,11 +165,11 @@ async def test_if_fires_on_entity_change_with_from_filter( hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_entity_change_with_not_from_filter( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on entity change inverse filter.""" assert await async_setup_component( @@ -193,15 +191,15 @@ async def test_if_fires_on_entity_change_with_not_from_filter( # Do not fire from hello hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert not calls + assert not service_calls hass.states.async_set("test.entity", "universum") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_entity_change_with_to_filter( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on entity change with to filter.""" assert await async_setup_component( @@ -222,11 +220,11 @@ async def test_if_fires_on_entity_change_with_to_filter( hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_entity_change_with_not_to_filter( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on entity change with to filter.""" assert await async_setup_component( @@ -248,15 +246,15 @@ async def test_if_fires_on_entity_change_with_not_to_filter( # Do not fire to world hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert not calls + assert not service_calls hass.states.async_set("test.entity", "universum") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_entity_change_with_from_filter_all( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on entity change with filter.""" assert await async_setup_component( @@ -278,11 +276,11 @@ async def test_if_fires_on_entity_change_with_from_filter_all( hass.states.async_set("test.entity", "world") hass.states.async_set("test.entity", "world", {"attribute": 5}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_entity_change_with_to_filter_all( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on entity change with to filter.""" assert await async_setup_component( @@ -304,11 +302,11 @@ async def test_if_fires_on_entity_change_with_to_filter_all( hass.states.async_set("test.entity", "world") hass.states.async_set("test.entity", "world", {"attribute": 5}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_attribute_change_with_to_filter( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for not firing on attribute change.""" assert await async_setup_component( @@ -330,11 +328,11 @@ async def test_if_fires_on_attribute_change_with_to_filter( hass.states.async_set("test.entity", "world", {"test_attribute": 11}) hass.states.async_set("test.entity", "world", {"test_attribute": 12}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_entity_change_with_both_filters( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing if both filters are a non match.""" assert await async_setup_component( @@ -356,11 +354,11 @@ async def test_if_fires_on_entity_change_with_both_filters( hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_entity_change_with_not_from_to( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing if not from doesn't match and to match.""" assert await async_setup_component( @@ -383,31 +381,31 @@ async def test_if_fires_on_entity_change_with_not_from_to( # We should not trigger from hello hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert not calls + assert not service_calls # We should not trigger to != galaxy hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert not calls + assert not service_calls # We should trigger to galaxy hass.states.async_set("test.entity", "galaxy") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # We should not trigger from milky way hass.states.async_set("test.entity", "milky_way") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # We should trigger to universe hass.states.async_set("test.entity", "universe") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 async def test_if_fires_on_entity_change_with_from_not_to( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing if not from doesn't match and to match.""" assert await async_setup_component( @@ -430,31 +428,31 @@ async def test_if_fires_on_entity_change_with_from_not_to( # We should trigger to world from hello hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Reset back to hello, should not trigger hass.states.async_set("test.entity", "hello") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # We should not trigger to galaxy hass.states.async_set("test.entity", "galaxy") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # We should trigger form galaxy to milky way hass.states.async_set("test.entity", "milky_way") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 # We should not trigger to universe hass.states.async_set("test.entity", "universe") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 async def test_if_not_fires_if_to_filter_not_match( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for not firing if to filter is not a match.""" assert await async_setup_component( @@ -476,11 +474,11 @@ async def test_if_not_fires_if_to_filter_not_match( hass.states.async_set("test.entity", "moon") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_if_not_fires_if_from_filter_not_match( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for not firing if from filter is not a match.""" hass.states.async_set("test.entity", "bye") @@ -504,11 +502,11 @@ async def test_if_not_fires_if_from_filter_not_match( hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_if_not_fires_if_entity_not_match( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for not firing if entity is not matching.""" assert await async_setup_component( @@ -525,10 +523,10 @@ async def test_if_not_fires_if_entity_not_match( hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 -async def test_if_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: +async def test_if_action(hass: HomeAssistant, service_calls: list[ServiceCall]) -> None: """Test for to action.""" entity_id = "domain.test_entity" test_state = "new_state" @@ -551,18 +549,16 @@ async def test_if_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.states.async_set(entity_id, test_state + "something") hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 -async def test_if_fails_setup_if_to_boolean_value( - hass: HomeAssistant, calls: list[ServiceCall] -) -> None: +async def test_if_fails_setup_if_to_boolean_value(hass: HomeAssistant) -> None: """Test for setup failure for boolean to.""" with assert_setup_component(1, automation.DOMAIN): assert await async_setup_component( @@ -582,9 +578,7 @@ async def test_if_fails_setup_if_to_boolean_value( assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE -async def test_if_fails_setup_if_from_boolean_value( - hass: HomeAssistant, calls: list[ServiceCall] -) -> None: +async def test_if_fails_setup_if_from_boolean_value(hass: HomeAssistant) -> None: """Test for setup failure for boolean from.""" with assert_setup_component(1, automation.DOMAIN): assert await async_setup_component( @@ -604,9 +598,7 @@ async def test_if_fails_setup_if_from_boolean_value( assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE -async def test_if_fails_setup_bad_for( - hass: HomeAssistant, calls: list[ServiceCall] -) -> None: +async def test_if_fails_setup_bad_for(hass: HomeAssistant) -> None: """Test for setup failure for bad for.""" with assert_setup_component(1, automation.DOMAIN): assert await async_setup_component( @@ -628,7 +620,7 @@ async def test_if_fails_setup_bad_for( async def test_if_not_fires_on_entity_change_with_for( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for not firing on entity change with for.""" assert await async_setup_component( @@ -654,11 +646,11 @@ async def test_if_not_fires_on_entity_change_with_for( await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_if_not_fires_on_entities_change_with_for_after_stop( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for not firing on entity change with for after stop trigger.""" assert await async_setup_component( @@ -686,7 +678,7 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop( await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.states.async_set("test.entity_1", "world_no") hass.states.async_set("test.entity_2", "world_no") @@ -700,14 +692,17 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop( {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 2 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 2 async def test_if_fires_on_entity_change_with_for_attribute_change( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing on entity change with for and attribute change.""" assert await async_setup_component( @@ -735,15 +730,17 @@ async def test_if_fires_on_entity_change_with_for_attribute_change( "test.entity", "world", attributes={"mock_attr": "attr_change"} ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 freezer.tick(timedelta(seconds=4)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_entity_change_with_for_multiple_force_update( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing on entity change with for and force update.""" assert await async_setup_component( @@ -770,15 +767,15 @@ async def test_if_fires_on_entity_change_with_for_multiple_force_update( async_fire_time_changed(hass) hass.states.async_set("test.force_entity", "world", None, True) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 freezer.tick(timedelta(seconds=4)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_entity_change_with_for( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on entity change with for.""" assert await async_setup_component( @@ -802,11 +799,11 @@ async def test_if_fires_on_entity_change_with_for( await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_entity_change_with_for_without_to( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on entity change with for.""" assert await async_setup_component( @@ -830,22 +827,24 @@ async def test_if_fires_on_entity_change_with_for_without_to( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set("test.entity", "world") await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=4)) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_does_not_fires_on_entity_change_with_for_without_to_2( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing on entity change with for.""" assert await async_setup_component( @@ -871,11 +870,11 @@ async def test_if_does_not_fires_on_entity_change_with_for_without_to_2( async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_if_fires_on_entity_creation_and_removal( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on entity creation and removal, with to/from constraints.""" # set automations for multiple combinations to/from @@ -917,32 +916,32 @@ async def test_if_fires_on_entity_creation_and_removal( # automation with match_all triggers on creation hass.states.async_set("test.entity_0", "any", context=context_0) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context.parent_id == context_0.id + assert len(service_calls) == 1 + assert service_calls[0].context.parent_id == context_0.id # create entities, trigger on test.entity_2 ('to' matches, no 'from') hass.states.async_set("test.entity_1", "hello", context=context_1) hass.states.async_set("test.entity_2", "world", context=context_2) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].context.parent_id == context_2.id + assert len(service_calls) == 2 + assert service_calls[1].context.parent_id == context_2.id # removal of both, trigger on test.entity_1 ('from' matches, no 'to') assert hass.states.async_remove("test.entity_1", context=context_1) assert hass.states.async_remove("test.entity_2", context=context_2) await hass.async_block_till_done() - assert len(calls) == 3 - assert calls[2].context.parent_id == context_1.id + assert len(service_calls) == 3 + assert service_calls[2].context.parent_id == context_1.id # automation with match_all triggers on removal assert hass.states.async_remove("test.entity_0", context=context_0) await hass.async_block_till_done() - assert len(calls) == 4 - assert calls[3].context.parent_id == context_0.id + assert len(service_calls) == 4 + assert service_calls[3].context.parent_id == context_0.id async def test_if_fires_on_for_condition( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing if condition is on.""" point1 = dt_util.utcnow() @@ -971,17 +970,17 @@ async def test_if_fires_on_for_condition( # not enough time has passed hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 # Time travel 10 secs into the future mock_utcnow.return_value = point2 hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_for_condition_attribute_change( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing if condition is on with attribute change.""" point1 = dt_util.utcnow() @@ -1011,7 +1010,7 @@ async def test_if_fires_on_for_condition_attribute_change( # not enough time has passed hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 # Still not enough time has passed, but an attribute is changed mock_utcnow.return_value = point2 @@ -1020,18 +1019,16 @@ async def test_if_fires_on_for_condition_attribute_change( ) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 # Enough time has now passed mock_utcnow.return_value = point3 hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 -async def test_if_fails_setup_for_without_time( - hass: HomeAssistant, calls: list[ServiceCall] -) -> None: +async def test_if_fails_setup_for_without_time(hass: HomeAssistant) -> None: """Test for setup failure if no time is provided.""" with assert_setup_component(1, automation.DOMAIN): assert await async_setup_component( @@ -1053,9 +1050,7 @@ async def test_if_fails_setup_for_without_time( assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE -async def test_if_fails_setup_for_without_entity( - hass: HomeAssistant, calls: list[ServiceCall] -) -> None: +async def test_if_fails_setup_for_without_entity(hass: HomeAssistant) -> None: """Test for setup failure if no entity is provided.""" with assert_setup_component(1, automation.DOMAIN): assert await async_setup_component( @@ -1077,7 +1072,7 @@ async def test_if_fails_setup_for_without_entity( async def test_wait_template_with_trigger( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test using wait template with 'trigger.entity_id'.""" assert await async_setup_component( @@ -1113,12 +1108,14 @@ async def test_wait_template_with_trigger( hass.states.async_set("test.entity", "world") hass.states.async_set("test.entity", "hello") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "state - test.entity - hello - world" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "state - test.entity - hello - world" async def test_if_fires_on_entities_change_no_overlap( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing on entities change with no overlap.""" assert await async_setup_component( @@ -1146,20 +1143,22 @@ async def test_if_fires_on_entities_change_no_overlap( freezer.tick(timedelta(seconds=10)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test.entity_1" hass.states.async_set("test.entity_2", "world") await hass.async_block_till_done() freezer.tick(timedelta(seconds=10)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "test.entity_2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "test.entity_2" async def test_if_fires_on_entities_change_overlap( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing on entities change with overlap.""" assert await async_setup_component( @@ -1196,22 +1195,22 @@ async def test_if_fires_on_entities_change_overlap( async_fire_time_changed(hass) hass.states.async_set("test.entity_2", "world") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 freezer.tick(timedelta(seconds=3)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test.entity_1" freezer.tick(timedelta(seconds=3)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "test.entity_2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "test.entity_2" async def test_if_fires_on_change_with_for_template_1( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on change with for template.""" assert await async_setup_component( @@ -1232,14 +1231,14 @@ async def test_if_fires_on_change_with_for_template_1( hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_change_with_for_template_2( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on change with for template.""" assert await async_setup_component( @@ -1260,14 +1259,14 @@ async def test_if_fires_on_change_with_for_template_2( hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_change_with_for_template_3( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on change with for template.""" assert await async_setup_component( @@ -1288,14 +1287,14 @@ async def test_if_fires_on_change_with_for_template_3( hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_change_with_for_template_4( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on change with for template.""" assert await async_setup_component( @@ -1317,14 +1316,14 @@ async def test_if_fires_on_change_with_for_template_4( hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_change_from_with_for( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on change with from/for.""" assert await async_setup_component( @@ -1351,11 +1350,11 @@ async def test_if_fires_on_change_from_with_for( await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_not_fires_on_change_from_with_for( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on change with from/for.""" assert await async_setup_component( @@ -1382,12 +1381,10 @@ async def test_if_not_fires_on_change_from_with_for( await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 -async def test_invalid_for_template_1( - hass: HomeAssistant, calls: list[ServiceCall] -) -> None: +async def test_invalid_for_template_1(hass: HomeAssistant) -> None: """Test for invalid for template.""" assert await async_setup_component( hass, @@ -1412,7 +1409,9 @@ async def test_invalid_for_template_1( async def test_if_fires_on_entities_change_overlap_for_template( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing on entities change with overlap and for template.""" assert await async_setup_component( @@ -1452,26 +1451,26 @@ async def test_if_fires_on_entities_change_overlap_for_template( async_fire_time_changed(hass) hass.states.async_set("test.entity_2", "world") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 freezer.tick(timedelta(seconds=3)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1 - 0:00:05" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test.entity_1 - 0:00:05" freezer.tick(timedelta(seconds=3)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 freezer.tick(timedelta(seconds=5)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "test.entity_2 - 0:00:10" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "test.entity_2 - 0:00:10" async def test_attribute_if_fires_on_entity_change_with_both_filters( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing if both filters are match attribute.""" hass.states.async_set("test.entity", "bla", {"name": "hello"}) @@ -1496,11 +1495,11 @@ async def test_attribute_if_fires_on_entity_change_with_both_filters( hass.states.async_set("test.entity", "bla", {"name": "world"}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_attribute_if_fires_on_entity_where_attr_stays_constant( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing if attribute stays the same.""" hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "old_value"}) @@ -1524,21 +1523,21 @@ async def test_attribute_if_fires_on_entity_where_attr_stays_constant( # Leave all attributes the same hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "old_value"}) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 # Change the untracked attribute hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "new_value"}) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 # Change the tracked attribute hass.states.async_set("test.entity", "bla", {"name": "world", "other": "old_value"}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_attribute_if_fires_on_entity_where_attr_stays_constant_filter( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing if attribute stays the same.""" hass.states.async_set("test.entity", "bla", {"name": "other_name"}) @@ -1565,25 +1564,25 @@ async def test_attribute_if_fires_on_entity_where_attr_stays_constant_filter( "test.entity", "bla", {"name": "best_name", "other": "old_value"} ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Change the untracked attribute hass.states.async_set( "test.entity", "bla", {"name": "best_name", "other": "new_value"} ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Change the tracked attribute hass.states.async_set( "test.entity", "bla", {"name": "other_name", "other": "old_value"} ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_attribute_if_fires_on_entity_where_attr_stays_constant_all( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing if attribute stays the same.""" hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "old_value"}) @@ -1610,25 +1609,25 @@ async def test_attribute_if_fires_on_entity_where_attr_stays_constant_all( "test.entity", "bla", {"name": "name_1", "other": "old_value"} ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Change the untracked attribute hass.states.async_set( "test.entity", "bla", {"name": "name_1", "other": "new_value"} ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Change the tracked attribute hass.states.async_set( "test.entity", "bla", {"name": "name_2", "other": "old_value"} ) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for not firing on entity change with for after stop trigger.""" hass.states.async_set("test.entity", "bla", {"name": "hello"}) @@ -1658,33 +1657,33 @@ async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( # Test that the for-check works hass.states.async_set("test.entity", "bla", {"name": "world"}) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) hass.states.async_set("test.entity", "bla", {"name": "world", "something": "else"}) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Now remove state while inside "for" hass.states.async_set("test.entity", "bla", {"name": "hello"}) hass.states.async_set("test.entity", "bla", {"name": "world"}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.states.async_remove("test.entity") await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_attribute_if_fires_on_entity_change_with_both_filters_boolean( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing if both filters are match attribute.""" hass.states.async_set("test.entity", "bla", {"happening": False}) @@ -1709,11 +1708,13 @@ async def test_attribute_if_fires_on_entity_change_with_both_filters_boolean( hass.states.async_set("test.entity", "bla", {"happening": True}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_variables_priority( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test an externally defined trigger variable is overridden.""" assert await async_setup_component( @@ -1754,19 +1755,19 @@ async def test_variables_priority( async_fire_time_changed(hass) hass.states.async_set("test.entity_2", "world") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 freezer.tick(timedelta(seconds=3)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1 - 0:00:05" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test.entity_1 - 0:00:05" freezer.tick(timedelta(seconds=3)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 freezer.tick(timedelta(seconds=5)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "test.entity_2 - 0:00:10" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "test.entity_2 - 0:00:10" diff --git a/tests/components/homeassistant/triggers/test_time.py b/tests/components/homeassistant/triggers/test_time.py index 961bac6c367..76d80120fdd 100644 --- a/tests/components/homeassistant/triggers/test_time.py +++ b/tests/components/homeassistant/triggers/test_time.py @@ -20,28 +20,19 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import ( - assert_setup_component, - async_fire_time_changed, - async_mock_service, - mock_component, -) - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") +from tests.common import assert_setup_component, async_fire_time_changed, mock_component @pytest.fixture(autouse=True) -def setup_comp(hass): +def setup_comp(hass: HomeAssistant) -> None: """Initialize components.""" mock_component(hass, "group") async def test_if_fires_using_at( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing at.""" now = dt_util.now() @@ -71,9 +62,9 @@ async def test_if_fires_using_at( async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "time - 5" - assert calls[0].data["id"] == 0 + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "time - 5" + assert service_calls[0].data["id"] == 0 @pytest.mark.parametrize( @@ -82,7 +73,7 @@ async def test_if_fires_using_at( async def test_if_fires_using_at_input_datetime( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - calls: list[ServiceCall], + service_calls: list[ServiceCall], has_date, has_time, ) -> None: @@ -132,9 +123,9 @@ async def test_if_fires_using_at_input_datetime( async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 2 assert ( - calls[0].data["some"] + service_calls[1].data["some"] == f"time-{trigger_dt.day}-{trigger_dt.hour}-input_datetime.trigger" ) @@ -152,20 +143,23 @@ async def test_if_fires_using_at_input_datetime( }, blocking=True, ) + assert len(service_calls) == 3 await hass.async_block_till_done() async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 4 assert ( - calls[1].data["some"] + service_calls[3].data["some"] == f"time-{trigger_dt.day}-{trigger_dt.hour}-input_datetime.trigger" ) async def test_if_fires_using_multiple_at( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing at.""" @@ -195,18 +189,20 @@ async def test_if_fires_using_multiple_at( async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "time - 5" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "time - 5" async_fire_time_changed(hass, trigger_dt + timedelta(hours=1, seconds=1)) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "time - 6" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "time - 6" async def test_if_not_fires_using_wrong_at( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """YAML translates time values to total seconds. @@ -242,10 +238,12 @@ async def test_if_not_fires_using_wrong_at( ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 -async def test_if_action_before(hass: HomeAssistant, calls: list[ServiceCall]) -> None: +async def test_if_action_before( + hass: HomeAssistant, service_calls: list[ServiceCall] +) -> None: """Test for if action before.""" assert await async_setup_component( hass, @@ -267,16 +265,18 @@ async def test_if_action_before(hass: HomeAssistant, calls: list[ServiceCall]) - hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 with patch("homeassistant.helpers.condition.dt_util.now", return_value=after_10): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 -async def test_if_action_after(hass: HomeAssistant, calls: list[ServiceCall]) -> None: +async def test_if_action_after( + hass: HomeAssistant, service_calls: list[ServiceCall] +) -> None: """Test for if action after.""" assert await async_setup_component( hass, @@ -298,17 +298,17 @@ async def test_if_action_after(hass: HomeAssistant, calls: list[ServiceCall]) -> hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 with patch("homeassistant.helpers.condition.dt_util.now", return_value=after_10): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_action_one_weekday( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for if action with one weekday.""" assert await async_setup_component( @@ -332,17 +332,17 @@ async def test_if_action_one_weekday( hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 with patch("homeassistant.helpers.condition.dt_util.now", return_value=tuesday): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_action_list_weekday( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for action with a list of weekdays.""" assert await async_setup_component( @@ -367,19 +367,19 @@ async def test_if_action_list_weekday( hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 with patch("homeassistant.helpers.condition.dt_util.now", return_value=tuesday): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 with patch("homeassistant.helpers.condition.dt_util.now", return_value=wednesday): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 async def test_untrack_time_change(hass: HomeAssistant) -> None: @@ -416,7 +416,9 @@ async def test_untrack_time_change(hass: HomeAssistant) -> None: async def test_if_fires_using_at_sensor( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing at sensor time.""" now = dt_util.now() @@ -452,9 +454,9 @@ async def test_if_fires_using_at_sensor( async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"time-{trigger_dt.day}-{trigger_dt.hour}-sensor.next_alarm" ) @@ -470,9 +472,9 @@ async def test_if_fires_using_at_sensor( async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[1].data["some"] + service_calls[1].data["some"] == f"time-{trigger_dt.day}-{trigger_dt.hour}-sensor.next_alarm" ) @@ -494,7 +496,7 @@ async def test_if_fires_using_at_sensor( await hass.async_block_till_done() # We should not have listened to anything - assert len(calls) == 2 + assert len(service_calls) == 2 # Now without device class hass.states.async_set( @@ -513,7 +515,7 @@ async def test_if_fires_using_at_sensor( await hass.async_block_till_done() # We should not have listened to anything - assert len(calls) == 2 + assert len(service_calls) == 2 @pytest.mark.parametrize( @@ -544,7 +546,7 @@ def test_schema_invalid(conf) -> None: async def test_datetime_in_past_on_load( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test time trigger works if input_datetime is in past.""" await async_setup_component( @@ -566,6 +568,7 @@ async def test_datetime_in_past_on_load( }, blocking=True, ) + assert len(service_calls) == 1 await hass.async_block_till_done() assert await async_setup_component( @@ -587,7 +590,7 @@ async def test_datetime_in_past_on_load( async_fire_time_changed(hass, now) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 1 await hass.services.async_call( "input_datetime", @@ -598,13 +601,14 @@ async def test_datetime_in_past_on_load( }, blocking=True, ) + assert len(service_calls) == 2 await hass.async_block_till_done() async_fire_time_changed(hass, future + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 3 assert ( - calls[0].data["some"] + service_calls[2].data["some"] == f"time-{future.day}-{future.hour}-input_datetime.my_trigger" ) diff --git a/tests/components/homeassistant/triggers/test_time_pattern.py b/tests/components/homeassistant/triggers/test_time_pattern.py index 327623d373b..7138fd7dd02 100644 --- a/tests/components/homeassistant/triggers/test_time_pattern.py +++ b/tests/components/homeassistant/triggers/test_time_pattern.py @@ -13,23 +13,19 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, async_mock_service, mock_component - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") +from tests.common import async_fire_time_changed, mock_component @pytest.fixture(autouse=True) -def setup_comp(hass): +def setup_comp(hass: HomeAssistant) -> None: """Initialize components.""" mock_component(hass, "group") async def test_if_fires_when_hour_matches( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing if hour is matching.""" now = dt_util.utcnow() @@ -58,7 +54,8 @@ async def test_if_fires_when_hour_matches( async_fire_time_changed(hass, now.replace(year=now.year + 2, day=1, hour=0)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 + assert service_calls[0].data["id"] == 0 await hass.services.async_call( automation.DOMAIN, @@ -66,15 +63,17 @@ async def test_if_fires_when_hour_matches( {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 2 async_fire_time_changed(hass, now.replace(year=now.year + 1, day=1, hour=0)) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["id"] == 0 + assert len(service_calls) == 2 async def test_if_fires_when_minute_matches( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing if minutes are matching.""" now = dt_util.utcnow() @@ -101,11 +100,13 @@ async def test_if_fires_when_minute_matches( async_fire_time_changed(hass, now.replace(year=now.year + 2, day=1, minute=0)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_when_second_matches( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing if seconds are matching.""" now = dt_util.utcnow() @@ -132,11 +133,13 @@ async def test_if_fires_when_second_matches( async_fire_time_changed(hass, now.replace(year=now.year + 2, day=1, second=0)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_when_second_as_string_matches( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing if seconds are matching.""" now = dt_util.utcnow() @@ -165,11 +168,13 @@ async def test_if_fires_when_second_as_string_matches( ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_when_all_matches( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing if everything matches.""" now = dt_util.utcnow() @@ -198,11 +203,13 @@ async def test_if_fires_when_all_matches( ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_periodic_seconds( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing periodically every second.""" now = dt_util.utcnow() @@ -231,11 +238,13 @@ async def test_if_fires_periodic_seconds( ) await hass.async_block_till_done() - assert len(calls) >= 1 + assert len(service_calls) >= 1 async def test_if_fires_periodic_minutes( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing periodically every minute.""" @@ -265,11 +274,13 @@ async def test_if_fires_periodic_minutes( ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_periodic_hours( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing periodically every hour.""" now = dt_util.utcnow() @@ -298,11 +309,13 @@ async def test_if_fires_periodic_hours( ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_default_values( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls: list[ServiceCall] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_calls: list[ServiceCall], ) -> None: """Test for firing at 2 minutes every hour.""" now = dt_util.utcnow() @@ -326,24 +339,24 @@ async def test_default_values( ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async_fire_time_changed( hass, now.replace(year=now.year + 2, day=1, hour=1, minute=2, second=1) ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async_fire_time_changed( hass, now.replace(year=now.year + 2, day=1, hour=2, minute=2, second=0) ) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 -async def test_invalid_schemas(hass: HomeAssistant, calls: list[ServiceCall]) -> None: +async def test_invalid_schemas() -> None: """Test invalid schemas.""" schemas = ( None, From ce54ca9c8e1aa2a663373498d6249aa58a5ee5f3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:27:32 +0200 Subject: [PATCH 0540/2411] Use service_calls fixture in lcn tests (#120931) --- tests/components/lcn/conftest.py | 9 +------ tests/components/lcn/test_device_trigger.py | 30 ++++++++++----------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py index f24fdbc054f..2884bc833c2 100644 --- a/tests/components/lcn/conftest.py +++ b/tests/components/lcn/conftest.py @@ -12,11 +12,10 @@ import pytest from homeassistant.components.lcn.const import DOMAIN from homeassistant.components.lcn.helpers import generate_unique_id from homeassistant.const import CONF_HOST -from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, async_mock_service, load_fixture +from tests.common import MockConfigEntry, load_fixture class MockModuleConnection(ModuleConnection): @@ -78,12 +77,6 @@ def create_config_entry(name): ) -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.fixture(name="entry") def create_config_entry_pchk(): """Return one specific config entry.""" diff --git a/tests/components/lcn/test_device_trigger.py b/tests/components/lcn/test_device_trigger.py index 67bd7568254..6c5ab7d6f4e 100644 --- a/tests/components/lcn/test_device_trigger.py +++ b/tests/components/lcn/test_device_trigger.py @@ -72,7 +72,7 @@ async def test_get_triggers_non_module_device( async def test_if_fires_on_transponder_event( - hass: HomeAssistant, calls: list[ServiceCall], entry, lcn_connection + hass: HomeAssistant, service_calls: list[ServiceCall], entry, lcn_connection ) -> None: """Test for transponder event triggers firing.""" address = (0, 7, False) @@ -111,15 +111,15 @@ async def test_if_fires_on_transponder_event( await lcn_connection.async_process_input(inp) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data == { + assert len(service_calls) == 1 + assert service_calls[0].data == { "test": "test_trigger_transponder", "code": "aabbcc", } async def test_if_fires_on_fingerprint_event( - hass: HomeAssistant, calls: list[ServiceCall], entry, lcn_connection + hass: HomeAssistant, service_calls: list[ServiceCall], entry, lcn_connection ) -> None: """Test for fingerprint event triggers firing.""" address = (0, 7, False) @@ -158,15 +158,15 @@ async def test_if_fires_on_fingerprint_event( await lcn_connection.async_process_input(inp) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data == { + assert len(service_calls) == 1 + assert service_calls[0].data == { "test": "test_trigger_fingerprint", "code": "aabbcc", } async def test_if_fires_on_codelock_event( - hass: HomeAssistant, calls: list[ServiceCall], entry, lcn_connection + hass: HomeAssistant, service_calls: list[ServiceCall], entry, lcn_connection ) -> None: """Test for codelock event triggers firing.""" address = (0, 7, False) @@ -205,15 +205,15 @@ async def test_if_fires_on_codelock_event( await lcn_connection.async_process_input(inp) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data == { + assert len(service_calls) == 1 + assert service_calls[0].data == { "test": "test_trigger_codelock", "code": "aabbcc", } async def test_if_fires_on_transmitter_event( - hass: HomeAssistant, calls: list[ServiceCall], entry, lcn_connection + hass: HomeAssistant, service_calls: list[ServiceCall], entry, lcn_connection ) -> None: """Test for transmitter event triggers firing.""" address = (0, 7, False) @@ -258,8 +258,8 @@ async def test_if_fires_on_transmitter_event( await lcn_connection.async_process_input(inp) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data == { + assert len(service_calls) == 1 + assert service_calls[0].data == { "test": "test_trigger_transmitter", "code": "aabbcc", "level": 0, @@ -269,7 +269,7 @@ async def test_if_fires_on_transmitter_event( async def test_if_fires_on_send_keys_event( - hass: HomeAssistant, calls: list[ServiceCall], entry, lcn_connection + hass: HomeAssistant, service_calls: list[ServiceCall], entry, lcn_connection ) -> None: """Test for send_keys event triggers firing.""" address = (0, 7, False) @@ -309,8 +309,8 @@ async def test_if_fires_on_send_keys_event( await lcn_connection.async_process_input(inp) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data == { + assert len(service_calls) == 1 + assert service_calls[0].data == { "test": "test_trigger_send_keys", "key": "a1", "action": "hit", From 77fc1c991c27ddf207a093f6bee339e655cfaab6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 1 Jul 2024 17:29:57 +0200 Subject: [PATCH 0541/2411] Use fixtures in deCONZ select tests (#120943) --- tests/components/deconz/test_select.py | 181 +++++++++++-------------- 1 file changed, 81 insertions(+), 100 deletions(-) diff --git a/tests/components/deconz/test_select.py b/tests/components/deconz/test_select.py index fb8f41293a2..8a43181efaf 100644 --- a/tests/components/deconz/test_select.py +++ b/tests/components/deconz/test_select.py @@ -1,6 +1,7 @@ """deCONZ select platform tests.""" -from unittest.mock import patch +from collections.abc import Callable +from typing import Any from pydeconz.models.sensor.presence import ( PresenceConfigDeviceMode, @@ -13,54 +14,38 @@ from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from .test_gateway import ( - DECONZ_WEB_REQUEST, - mock_deconz_put_request, - setup_deconz_integration, -) - from tests.test_util.aiohttp import AiohttpClientMocker - -async def test_no_select_entities( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that no sensors in deconz results in no sensor entities.""" - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 - - TEST_DATA = [ ( # Presence Device Mode { - "sensors": { - "1": { - "config": { - "devicemode": "undirected", - "on": True, - "reachable": True, - "sensitivity": 3, - "triggerdistance": "medium", - }, - "etag": "13ff209f9401b317987d42506dd4cd79", - "lastannounced": None, - "lastseen": "2022-06-28T23:13Z", - "manufacturername": "aqara", - "modelid": "lumi.motion.ac01", - "name": "Aqara FP1", - "state": { - "lastupdated": "2022-06-28T23:13:38.577", - "presence": True, - "presenceevent": "leave", - }, - "swversion": "20210121", - "type": "ZHAPresence", - "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", - } + "1": { + "config": { + "devicemode": "undirected", + "on": True, + "reachable": True, + "sensitivity": 3, + "triggerdistance": "medium", + }, + "etag": "13ff209f9401b317987d42506dd4cd79", + "lastannounced": None, + "lastseen": "2022-06-28T23:13Z", + "manufacturername": "aqara", + "modelid": "lumi.motion.ac01", + "name": "Aqara FP1", + "state": { + "lastupdated": "2022-06-28T23:13:38.577", + "presence": True, + "presenceevent": "leave", + }, + "swversion": "20210121", + "type": "ZHAPresence", + "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", } }, { @@ -80,30 +65,28 @@ TEST_DATA = [ ), ( # Presence Sensitivity { - "sensors": { - "1": { - "config": { - "devicemode": "undirected", - "on": True, - "reachable": True, - "sensitivity": 3, - "triggerdistance": "medium", - }, - "etag": "13ff209f9401b317987d42506dd4cd79", - "lastannounced": None, - "lastseen": "2022-06-28T23:13Z", - "manufacturername": "aqara", - "modelid": "lumi.motion.ac01", - "name": "Aqara FP1", - "state": { - "lastupdated": "2022-06-28T23:13:38.577", - "presence": True, - "presenceevent": "leave", - }, - "swversion": "20210121", - "type": "ZHAPresence", - "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", - } + "1": { + "config": { + "devicemode": "undirected", + "on": True, + "reachable": True, + "sensitivity": 3, + "triggerdistance": "medium", + }, + "etag": "13ff209f9401b317987d42506dd4cd79", + "lastannounced": None, + "lastseen": "2022-06-28T23:13Z", + "manufacturername": "aqara", + "modelid": "lumi.motion.ac01", + "name": "Aqara FP1", + "state": { + "lastupdated": "2022-06-28T23:13:38.577", + "presence": True, + "presenceevent": "leave", + }, + "swversion": "20210121", + "type": "ZHAPresence", + "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", } }, { @@ -123,30 +106,28 @@ TEST_DATA = [ ), ( # Presence Trigger Distance { - "sensors": { - "1": { - "config": { - "devicemode": "undirected", - "on": True, - "reachable": True, - "sensitivity": 3, - "triggerdistance": "medium", - }, - "etag": "13ff209f9401b317987d42506dd4cd79", - "lastannounced": None, - "lastseen": "2022-06-28T23:13Z", - "manufacturername": "aqara", - "modelid": "lumi.motion.ac01", - "name": "Aqara FP1", - "state": { - "lastupdated": "2022-06-28T23:13:38.577", - "presence": True, - "presenceevent": "leave", - }, - "swversion": "20210121", - "type": "ZHAPresence", - "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", - } + "1": { + "config": { + "devicemode": "undirected", + "on": True, + "reachable": True, + "sensitivity": 3, + "triggerdistance": "medium", + }, + "etag": "13ff209f9401b317987d42506dd4cd79", + "lastannounced": None, + "lastseen": "2022-06-28T23:13Z", + "manufacturername": "aqara", + "modelid": "lumi.motion.ac01", + "name": "Aqara FP1", + "state": { + "lastupdated": "2022-06-28T23:13:38.577", + "presence": True, + "presenceevent": "leave", + }, + "swversion": "20210121", + "type": "ZHAPresence", + "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", } }, { @@ -167,19 +148,16 @@ TEST_DATA = [ ] -@pytest.mark.parametrize(("raw_data", "expected"), TEST_DATA) +@pytest.mark.parametrize(("sensor_payload", "expected"), TEST_DATA) async def test_select( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, - raw_data, - expected, + config_entry_setup: ConfigEntry, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + expected: dict[str, Any], ) -> None: """Test successful creation of button entities.""" - with patch.dict(DECONZ_WEB_REQUEST, raw_data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == expected["entity_count"] # Verify state data @@ -196,13 +174,16 @@ async def test_select( # Verify device registry data assert ( - len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + len( + dr.async_entries_for_config_entry( + device_registry, config_entry_setup.entry_id + ) + ) == expected["device_count"] ) # Verify selecting option - - mock_deconz_put_request(aioclient_mock, config_entry.data, expected["request"]) + aioclient_mock = mock_put_request(expected["request"]) await hass.services.async_call( SELECT_DOMAIN, @@ -217,11 +198,11 @@ async def test_select( # Unload entry - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE # Remove entry - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 From 8a23e37837eed6bedf4ed30d1fd88d21b88c77f9 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 1 Jul 2024 17:30:23 +0200 Subject: [PATCH 0542/2411] Mark dry/fan-only climate modes as supported for Panasonic room air conditioner (#120939) --- homeassistant/components/matter/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index d2656d59138..c97124f4305 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -60,6 +60,7 @@ SUPPORT_DRY_MODE_DEVICES: set[tuple[int, int]] = { # In the list below specify tuples of (vendorid, productid) of devices that # support dry mode. (0x0001, 0x0108), + (0x0001, 0x010A), (0x1209, 0x8007), } @@ -68,6 +69,7 @@ SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = { # In the list below specify tuples of (vendorid, productid) of devices that # support fan-only mode. (0x0001, 0x0108), + (0x0001, 0x010A), (0x1209, 0x8007), } From c4903dd982d7ba8e6851a154a41b16bd1375d664 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:32:17 +0200 Subject: [PATCH 0543/2411] Use service_calls fixture in media_extractor tests (#120935) --- tests/components/media_extractor/conftest.py | 10 +------ tests/components/media_extractor/test_init.py | 29 ++++++++++--------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/tests/components/media_extractor/conftest.py b/tests/components/media_extractor/conftest.py index 45b3bb698e0..58d51f1cb2e 100644 --- a/tests/components/media_extractor/conftest.py +++ b/tests/components/media_extractor/conftest.py @@ -7,14 +7,12 @@ from unittest.mock import AsyncMock, patch import pytest from homeassistant.components.media_extractor import DOMAIN -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from . import MockYoutubeDL from .const import AUDIO_QUERY -from tests.common import async_mock_service - @pytest.fixture(autouse=True) async def setup_homeassistant(hass: HomeAssistant): @@ -31,12 +29,6 @@ async def setup_media_player(hass: HomeAssistant) -> None: await hass.async_block_till_done() -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "media_player", "play_media") - - @pytest.fixture(name="mock_youtube_dl") async def setup_mock_yt_dlp(hass: HomeAssistant) -> MockYoutubeDL: """Mock YoutubeDL.""" diff --git a/tests/components/media_extractor/test_init.py b/tests/components/media_extractor/test_init.py index 8c8a1407ccc..9708e1c2ad6 100644 --- a/tests/components/media_extractor/test_init.py +++ b/tests/components/media_extractor/test_init.py @@ -100,7 +100,7 @@ async def test_extracting_playlist_no_entries( async def test_play_media_service( hass: HomeAssistant, mock_youtube_dl: MockYoutubeDL, - calls: list[ServiceCall], + service_calls: list[ServiceCall], snapshot: SnapshotAssertion, request: pytest.FixtureRequest, config_fixture: str, @@ -123,13 +123,14 @@ async def test_play_media_service( ) await hass.async_block_till_done() - assert calls[0].data == snapshot + assert len(service_calls) == 2 + assert service_calls[1].data == snapshot async def test_download_error( hass: HomeAssistant, empty_media_extractor_config: dict[str, Any], - calls: list[ServiceCall], + service_calls: list[ServiceCall], caplog: pytest.LogCaptureFixture, ) -> None: """Test handling DownloadError.""" @@ -152,7 +153,7 @@ async def test_download_error( ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 1 assert f"Could not retrieve data for the URL: {YOUTUBE_VIDEO}" in caplog.text @@ -160,7 +161,7 @@ async def test_no_target_entity( hass: HomeAssistant, mock_youtube_dl: MockYoutubeDL, empty_media_extractor_config: dict[str, Any], - calls: list[ServiceCall], + service_calls: list[ServiceCall], snapshot: SnapshotAssertion, ) -> None: """Test having no target entity.""" @@ -179,14 +180,15 @@ async def test_no_target_entity( ) await hass.async_block_till_done() - assert calls[0].data == snapshot + assert len(service_calls) == 2 + assert service_calls[1].data == snapshot async def test_playlist( hass: HomeAssistant, mock_youtube_dl: MockYoutubeDL, empty_media_extractor_config: dict[str, Any], - calls: list[ServiceCall], + service_calls: list[ServiceCall], snapshot: SnapshotAssertion, ) -> None: """Test extracting a playlist.""" @@ -205,14 +207,15 @@ async def test_playlist( ) await hass.async_block_till_done() - assert calls[0].data == snapshot + assert len(service_calls) == 2 + assert service_calls[1].data == snapshot async def test_playlist_no_entries( hass: HomeAssistant, mock_youtube_dl: MockYoutubeDL, empty_media_extractor_config: dict[str, Any], - calls: list[ServiceCall], + service_calls: list[ServiceCall], caplog: pytest.LogCaptureFixture, ) -> None: """Test extracting a playlist without entries.""" @@ -231,7 +234,7 @@ async def test_playlist_no_entries( ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 1 assert ( f"Could not retrieve data for the URL: {YOUTUBE_EMPTY_PLAYLIST}" in caplog.text ) @@ -240,7 +243,7 @@ async def test_playlist_no_entries( async def test_query_error( hass: HomeAssistant, empty_media_extractor_config: dict[str, Any], - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test handling error with query.""" @@ -270,15 +273,13 @@ async def test_query_error( ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 1 async def test_cookiefile_detection( hass: HomeAssistant, mock_youtube_dl: MockYoutubeDL, empty_media_extractor_config: dict[str, Any], - calls: list[ServiceCall], - snapshot: SnapshotAssertion, caplog: pytest.LogCaptureFixture, ) -> None: """Test cookie file detection.""" From 788d1999ff5e8d8fc4e919badf48d5006fd05657 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:37:29 +0200 Subject: [PATCH 0544/2411] Use service_calls fixture in lg_netcast tests (#120932) --- tests/components/lg_netcast/conftest.py | 13 ----------- .../lg_netcast/test_device_trigger.py | 14 +++++++----- tests/components/lg_netcast/test_trigger.py | 22 ++++++++++--------- 3 files changed, 20 insertions(+), 29 deletions(-) delete mode 100644 tests/components/lg_netcast/conftest.py diff --git a/tests/components/lg_netcast/conftest.py b/tests/components/lg_netcast/conftest.py deleted file mode 100644 index eb13d5c8c67..00000000000 --- a/tests/components/lg_netcast/conftest.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Common fixtures and objects for the LG Netcast integration tests.""" - -import pytest - -from homeassistant.core import HomeAssistant, ServiceCall - -from tests.common import async_mock_service - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") diff --git a/tests/components/lg_netcast/test_device_trigger.py b/tests/components/lg_netcast/test_device_trigger.py index 05911acc41d..c8d725afde1 100644 --- a/tests/components/lg_netcast/test_device_trigger.py +++ b/tests/components/lg_netcast/test_device_trigger.py @@ -43,7 +43,9 @@ async def test_get_triggers( async def test_if_fires_on_turn_on_request( - hass: HomeAssistant, calls: list[ServiceCall], device_registry: dr.DeviceRegistry + hass: HomeAssistant, + service_calls: list[ServiceCall], + device_registry: dr.DeviceRegistry, ) -> None: """Test for turn_on triggers firing.""" await setup_lgnetcast(hass) @@ -96,11 +98,11 @@ async def test_if_fires_on_turn_on_request( ) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[0].data["some"] == device.id - assert calls[0].data["id"] == 0 - assert calls[1].data["some"] == ENTITY_ID - assert calls[1].data["id"] == 0 + assert len(service_calls) == 3 + assert service_calls[1].data["some"] == device.id + assert service_calls[1].data["id"] == 0 + assert service_calls[2].data["some"] == ENTITY_ID + assert service_calls[2].data["id"] == 0 async def test_failure_scenarios( diff --git a/tests/components/lg_netcast/test_trigger.py b/tests/components/lg_netcast/test_trigger.py index b0c2a86ec21..d838b931560 100644 --- a/tests/components/lg_netcast/test_trigger.py +++ b/tests/components/lg_netcast/test_trigger.py @@ -18,7 +18,9 @@ from tests.common import MockEntity, MockEntityPlatform async def test_lg_netcast_turn_on_trigger_device_id( - hass: HomeAssistant, calls: list[ServiceCall], device_registry: dr.DeviceRegistry + hass: HomeAssistant, + service_calls: list[ServiceCall], + device_registry: dr.DeviceRegistry, ) -> None: """Test for turn_on trigger by device_id firing.""" await setup_lgnetcast(hass) @@ -56,14 +58,14 @@ async def test_lg_netcast_turn_on_trigger_device_id( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == device.id - assert calls[0].data["id"] == 0 + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == device.id + assert service_calls[1].data["id"] == 0 with patch("homeassistant.config.load_yaml_dict", return_value={}): await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) - calls.clear() + service_calls.clear() with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -74,11 +76,11 @@ async def test_lg_netcast_turn_on_trigger_device_id( ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 1 async def test_lg_netcast_turn_on_trigger_entity_id( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for turn_on triggers by entity firing.""" await setup_lgnetcast(hass) @@ -113,9 +115,9 @@ async def test_lg_netcast_turn_on_trigger_entity_id( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == ENTITY_ID - assert calls[0].data["id"] == 0 + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == ENTITY_ID + assert service_calls[1].data["id"] == 0 async def test_wrong_trigger_platform_type( From 52b743e88a9f2a90f328d95bd24a7928f705116f Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 1 Jul 2024 16:39:01 +0100 Subject: [PATCH 0545/2411] Add exception translations (#120937) --- homeassistant/components/azure_devops/coordinator.py | 5 +++-- homeassistant/components/azure_devops/strings.json | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/azure_devops/coordinator.py b/homeassistant/components/azure_devops/coordinator.py index d7531c130e9..22dbe32c103 100644 --- a/homeassistant/components/azure_devops/coordinator.py +++ b/homeassistant/components/azure_devops/coordinator.py @@ -78,8 +78,9 @@ class AzureDevOpsDataUpdateCoordinator(DataUpdateCoordinator[AzureDevOpsData]): ) if not self.client.authorized: raise ConfigEntryAuthFailed( - "Could not authorize with Azure DevOps. You will need to update your" - " token" + translation_domain=DOMAIN, + translation_key="authentication_failed", + translation_placeholders={"title": self.title}, ) return True diff --git a/homeassistant/components/azure_devops/strings.json b/homeassistant/components/azure_devops/strings.json index 7bd6d8af561..8a17169fb6b 100644 --- a/homeassistant/components/azure_devops/strings.json +++ b/homeassistant/components/azure_devops/strings.json @@ -62,5 +62,10 @@ "name": "{definition_name} latest build url" } } + }, + "exceptions": { + "authentication_failed": { + "message": "Could not authorize with Azure DevOps for {title}. You will need to update your personal access token." + } } } From 361e81821c6a2d04ab397a20226305103f496ef7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 1 Jul 2024 17:42:32 +0200 Subject: [PATCH 0546/2411] Use fixtures in deCONZ scene tests (#120936) --- tests/components/deconz/test_scene.py | 81 +++++++++++---------------- 1 file changed, 34 insertions(+), 47 deletions(-) diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index 2bace605db5..c168bbcdd3a 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -1,44 +1,29 @@ """deCONZ scene platform tests.""" -from unittest.mock import patch +from collections.abc import Callable +from typing import Any import pytest from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, SERVICE_TURN_ON +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from .test_gateway import ( - DECONZ_WEB_REQUEST, - mock_deconz_put_request, - setup_deconz_integration, -) - from tests.test_util.aiohttp import AiohttpClientMocker - -async def test_no_scenes( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that scenes can be loaded without scenes being available.""" - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 - - TEST_DATA = [ ( # Scene { - "groups": { - "1": { - "id": "Light group id", - "name": "Light group", - "type": "LightGroup", - "state": {"all_on": False, "any_on": True}, - "action": {}, - "scenes": [{"id": "1", "name": "Scene"}], - "lights": [], - } + "1": { + "id": "Light group id", + "name": "Light group", + "type": "LightGroup", + "state": {"all_on": False, "any_on": True}, + "action": {}, + "scenes": [{"id": "1", "name": "Scene"}], + "lights": [], } }, { @@ -56,19 +41,16 @@ TEST_DATA = [ ] -@pytest.mark.parametrize(("raw_data", "expected"), TEST_DATA) +@pytest.mark.parametrize(("group_payload", "expected"), TEST_DATA) async def test_scenes( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, - raw_data, - expected, + config_entry_setup: ConfigEntry, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + expected: dict[str, Any], ) -> None: """Test successful creation of scene entities.""" - with patch.dict(DECONZ_WEB_REQUEST, raw_data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == expected["entity_count"] # Verify state data @@ -85,13 +67,17 @@ async def test_scenes( # Verify device registry data assert ( - len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + len( + dr.async_entries_for_config_entry( + device_registry, config_entry_setup.entry_id + ) + ) == expected["device_count"] ) # Verify button press - mock_deconz_put_request(aioclient_mock, config_entry.data, expected["request"]) + aioclient_mock = mock_put_request(expected["request"]) await hass.services.async_call( SCENE_DOMAIN, @@ -103,22 +89,20 @@ async def test_scenes( # Unload entry - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE # Remove entry - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 -async def test_only_new_scenes_are_created( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test that scenes works.""" - data = { - "groups": { +@pytest.mark.parametrize( + "group_payload", + [ + { "1": { "id": "Light group id", "name": "Light group", @@ -129,10 +113,13 @@ async def test_only_new_scenes_are_created( "lights": [], } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_only_new_scenes_are_created( + hass: HomeAssistant, mock_deconz_websocket +) -> None: + """Test that scenes works.""" assert len(hass.states.async_all()) == 2 event_changed_group = { From dcf4e91234fa89fa0a2870526c131151dd2fbb60 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 1 Jul 2024 17:46:46 +0200 Subject: [PATCH 0547/2411] Use fixtures in deCONZ number tests (#120938) --- tests/components/deconz/test_number.py | 87 ++++++++++++-------------- 1 file changed, 39 insertions(+), 48 deletions(-) diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py index 655ae2f42e2..c2ec7203ac2 100644 --- a/tests/components/deconz/test_number.py +++ b/tests/components/deconz/test_number.py @@ -1,6 +1,7 @@ """deCONZ number platform tests.""" -from unittest.mock import patch +from collections.abc import Callable +from typing import Any import pytest @@ -9,41 +10,29 @@ from homeassistant.components.number import ( DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import device_registry as dr, entity_registry as er -from .test_gateway import ( - DECONZ_WEB_REQUEST, - mock_deconz_put_request, - setup_deconz_integration, -) - from tests.test_util.aiohttp import AiohttpClientMocker - -async def test_no_number_entities( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that no sensors in deconz results in no number entities.""" - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 - - TEST_DATA = [ ( # Presence sensor - delay configuration { - "name": "Presence sensor", - "type": "ZHAPresence", - "state": {"dark": False, "presence": False}, - "config": { - "delay": 0, - "on": True, - "reachable": True, - "temperature": 10, - }, - "uniqueid": "00:00:00:00:00:00:00:00-00", + "0": { + "name": "Presence sensor", + "type": "ZHAPresence", + "state": {"dark": False, "presence": False}, + "config": { + "delay": 0, + "on": True, + "reachable": True, + "temperature": 10, + }, + "uniqueid": "00:00:00:00:00:00:00:00-00", + } }, { "entity_count": 3, @@ -70,16 +59,18 @@ TEST_DATA = [ ), ( # Presence sensor - duration configuration { - "name": "Presence sensor", - "type": "ZHAPresence", - "state": {"dark": False, "presence": False}, - "config": { - "duration": 0, - "on": True, - "reachable": True, - "temperature": 10, - }, - "uniqueid": "00:00:00:00:00:00:00:00-00", + "0": { + "name": "Presence sensor", + "type": "ZHAPresence", + "state": {"dark": False, "presence": False}, + "config": { + "duration": 0, + "on": True, + "reachable": True, + "temperature": 10, + }, + "uniqueid": "00:00:00:00:00:00:00:00-00", + } }, { "entity_count": 3, @@ -107,21 +98,17 @@ TEST_DATA = [ ] -@pytest.mark.parametrize(("sensor_data", "expected"), TEST_DATA) +@pytest.mark.parametrize(("sensor_payload", "expected"), TEST_DATA) async def test_number_entities( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + config_entry_setup: ConfigEntry, mock_deconz_websocket, - sensor_data, - expected, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + expected: dict[str, Any], ) -> None: """Test successful creation of number entities.""" - - with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"0": sensor_data}}): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == expected["entity_count"] # Verify state data @@ -139,7 +126,11 @@ async def test_number_entities( # Verify device registry data assert ( - len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + len( + dr.async_entries_for_config_entry( + device_registry, config_entry_setup.entry_id + ) + ) == expected["device_count"] ) @@ -157,7 +148,7 @@ async def test_number_entities( # Verify service calls - mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") + aioclient_mock = mock_put_request("/sensors/0/config") # Service set supported value @@ -200,11 +191,11 @@ async def test_number_entities( # Unload entry - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE # Remove entry - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 From 813fee663e4beb03e544f21b06f8868cd1bc1569 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:47:21 +0200 Subject: [PATCH 0548/2411] Use service_calls fixture in litejet tests (#120933) --- tests/components/litejet/test_trigger.py | 78 +++++++++++------------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/tests/components/litejet/test_trigger.py b/tests/components/litejet/test_trigger.py index 216084c26bc..b4374652955 100644 --- a/tests/components/litejet/test_trigger.py +++ b/tests/components/litejet/test_trigger.py @@ -14,7 +14,7 @@ import homeassistant.util.dt as dt_util from . import async_init_integration -from tests.common import async_fire_time_changed_exact, async_mock_service +from tests.common import async_fire_time_changed_exact @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -30,12 +30,6 @@ ENTITY_OTHER_SWITCH = "switch.mock_switch_2" ENTITY_OTHER_SWITCH_NUMBER = 2 -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def simulate_press(hass, mock_litejet, number): """Test to simulate a press.""" _LOGGER.info("*** simulate press of %d", number) @@ -101,7 +95,7 @@ async def setup_automation(hass, trigger): async def test_simple( - hass: HomeAssistant, calls: list[ServiceCall], mock_litejet + hass: HomeAssistant, service_calls: list[ServiceCall], mock_litejet ) -> None: """Test the simplest form of a LiteJet trigger.""" await setup_automation( @@ -111,12 +105,12 @@ async def test_simple( await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - assert len(calls) == 1 - assert calls[0].data["id"] == 0 + assert len(service_calls) == 1 + assert service_calls[0].data["id"] == 0 async def test_only_release( - hass: HomeAssistant, calls: list[ServiceCall], mock_litejet + hass: HomeAssistant, service_calls: list[ServiceCall], mock_litejet ) -> None: """Test the simplest form of a LiteJet trigger.""" await setup_automation( @@ -125,11 +119,11 @@ async def test_only_release( await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_held_more_than_short( - hass: HomeAssistant, calls: list[ServiceCall], mock_litejet + hass: HomeAssistant, service_calls: list[ServiceCall], mock_litejet ) -> None: """Test a too short hold.""" await setup_automation( @@ -144,11 +138,11 @@ async def test_held_more_than_short( await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) await simulate_time(hass, mock_litejet, timedelta(seconds=1)) await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_held_more_than_long( - hass: HomeAssistant, calls: list[ServiceCall], mock_litejet + hass: HomeAssistant, service_calls: list[ServiceCall], mock_litejet ) -> None: """Test a hold that is long enough.""" await setup_automation( @@ -161,16 +155,16 @@ async def test_held_more_than_long( ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - assert len(calls) == 0 + assert len(service_calls) == 0 await simulate_time(hass, mock_litejet, timedelta(seconds=3)) - assert len(calls) == 1 - assert calls[0].data["id"] == 0 + assert len(service_calls) == 1 + assert service_calls[0].data["id"] == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_held_less_than_short( - hass: HomeAssistant, calls: list[ServiceCall], mock_litejet + hass: HomeAssistant, service_calls: list[ServiceCall], mock_litejet ) -> None: """Test a hold that is short enough.""" await setup_automation( @@ -184,14 +178,14 @@ async def test_held_less_than_short( await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) await simulate_time(hass, mock_litejet, timedelta(seconds=1)) - assert len(calls) == 0 + assert len(service_calls) == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - assert len(calls) == 1 - assert calls[0].data["id"] == 0 + assert len(service_calls) == 1 + assert service_calls[0].data["id"] == 0 async def test_held_less_than_long( - hass: HomeAssistant, calls: list[ServiceCall], mock_litejet + hass: HomeAssistant, service_calls: list[ServiceCall], mock_litejet ) -> None: """Test a hold that is too long.""" await setup_automation( @@ -204,15 +198,15 @@ async def test_held_less_than_long( ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - assert len(calls) == 0 + assert len(service_calls) == 0 await simulate_time(hass, mock_litejet, timedelta(seconds=3)) - assert len(calls) == 0 + assert len(service_calls) == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_held_in_range_short( - hass: HomeAssistant, calls: list[ServiceCall], mock_litejet + hass: HomeAssistant, service_calls: list[ServiceCall], mock_litejet ) -> None: """Test an in-range trigger with a too short hold.""" await setup_automation( @@ -228,11 +222,11 @@ async def test_held_in_range_short( await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) await simulate_time(hass, mock_litejet, timedelta(seconds=0.5)) await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_held_in_range_just_right( - hass: HomeAssistant, calls: list[ServiceCall], mock_litejet + hass: HomeAssistant, service_calls: list[ServiceCall], mock_litejet ) -> None: """Test an in-range trigger with a just right hold.""" await setup_automation( @@ -246,16 +240,16 @@ async def test_held_in_range_just_right( ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - assert len(calls) == 0 + assert len(service_calls) == 0 await simulate_time(hass, mock_litejet, timedelta(seconds=2)) - assert len(calls) == 0 + assert len(service_calls) == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - assert len(calls) == 1 - assert calls[0].data["id"] == 0 + assert len(service_calls) == 1 + assert service_calls[0].data["id"] == 0 async def test_held_in_range_long( - hass: HomeAssistant, calls: list[ServiceCall], mock_litejet + hass: HomeAssistant, service_calls: list[ServiceCall], mock_litejet ) -> None: """Test an in-range trigger with a too long hold.""" await setup_automation( @@ -269,15 +263,15 @@ async def test_held_in_range_long( ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - assert len(calls) == 0 + assert len(service_calls) == 0 await simulate_time(hass, mock_litejet, timedelta(seconds=4)) - assert len(calls) == 0 + assert len(service_calls) == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_reload( - hass: HomeAssistant, calls: list[ServiceCall], mock_litejet + hass: HomeAssistant, service_calls: list[ServiceCall], mock_litejet ) -> None: """Test reloading automation.""" await setup_automation( @@ -312,8 +306,8 @@ async def test_reload( await hass.async_block_till_done() await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - assert len(calls) == 0 + assert len(service_calls) == 1 await simulate_time(hass, mock_litejet, timedelta(seconds=5)) - assert len(calls) == 0 + assert len(service_calls) == 1 await simulate_time(hass, mock_litejet, timedelta(seconds=12.5)) - assert len(calls) == 1 + assert len(service_calls) == 2 From 5ce54c2174fbf97ff152317fcba652bbc2311664 Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 1 Jul 2024 08:48:12 -0700 Subject: [PATCH 0549/2411] Replace GoogleAPICallError with GoogleAPIError (#120902) --- .../google_generative_ai_conversation/__init__.py | 6 +++--- .../google_generative_ai_conversation/config_flow.py | 4 ++-- .../google_generative_ai_conversation/conversation.py | 4 ++-- .../google_generative_ai_conversation/test_conversation.py | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/__init__.py b/homeassistant/components/google_generative_ai_conversation/__init__.py index f115f3923b6..a5c55c2099d 100644 --- a/homeassistant/components/google_generative_ai_conversation/__init__.py +++ b/homeassistant/components/google_generative_ai_conversation/__init__.py @@ -7,7 +7,7 @@ from pathlib import Path from google.ai import generativelanguage_v1beta from google.api_core.client_options import ClientOptions -from google.api_core.exceptions import ClientError, DeadlineExceeded, GoogleAPICallError +from google.api_core.exceptions import ClientError, DeadlineExceeded, GoogleAPIError import google.generativeai as genai import google.generativeai.types as genai_types import voluptuous as vol @@ -71,7 +71,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: try: response = await model.generate_content_async(prompt_parts) except ( - GoogleAPICallError, + GoogleAPIError, ValueError, genai_types.BlockedPromptException, genai_types.StopCandidateException, @@ -111,7 +111,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await client.get_model( name=entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL), timeout=5.0 ) - except (GoogleAPICallError, ValueError) as err: + except (GoogleAPIError, ValueError) as err: if isinstance(err, ClientError) and err.reason == "API_KEY_INVALID": raise ConfigEntryAuthFailed(err) from err if isinstance(err, DeadlineExceeded): diff --git a/homeassistant/components/google_generative_ai_conversation/config_flow.py b/homeassistant/components/google_generative_ai_conversation/config_flow.py index 543deb926a0..ab23ac25f26 100644 --- a/homeassistant/components/google_generative_ai_conversation/config_flow.py +++ b/homeassistant/components/google_generative_ai_conversation/config_flow.py @@ -10,7 +10,7 @@ from typing import Any from google.ai import generativelanguage_v1beta from google.api_core.client_options import ClientOptions -from google.api_core.exceptions import ClientError, GoogleAPICallError +from google.api_core.exceptions import ClientError, GoogleAPIError import google.generativeai as genai import voluptuous as vol @@ -97,7 +97,7 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: try: await validate_input(self.hass, user_input) - except GoogleAPICallError as err: + except GoogleAPIError as err: if isinstance(err, ClientError) and err.reason == "API_KEY_INVALID": errors["base"] = "invalid_auth" else: diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 8052ee66f40..127ca2cae95 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -6,7 +6,7 @@ import codecs from collections.abc import Callable from typing import Any, Literal -from google.api_core.exceptions import GoogleAPICallError +from google.api_core.exceptions import GoogleAPIError import google.generativeai as genai from google.generativeai import protos import google.generativeai.types as genai_types @@ -278,7 +278,7 @@ class GoogleGenerativeAIConversationEntity( try: chat_response = await chat.send_message_async(chat_request) except ( - GoogleAPICallError, + GoogleAPIError, ValueError, genai_types.BlockedPromptException, genai_types.StopCandidateException, diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 30016335f3b..1e45c79a3b6 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from freezegun import freeze_time from google.ai.generativelanguage_v1beta.types.content import FunctionCall -from google.api_core.exceptions import GoogleAPICallError +from google.api_core.exceptions import GoogleAPIError import google.generativeai.types as genai_types import pytest from syrupy.assertion import SnapshotAssertion @@ -447,7 +447,7 @@ async def test_error_handling( with patch("google.generativeai.GenerativeModel") as mock_model: mock_chat = AsyncMock() mock_model.return_value.start_chat.return_value = mock_chat - mock_chat.send_message_async.side_effect = GoogleAPICallError("some error") + mock_chat.send_message_async.side_effect = GoogleAPIError("some error") result = await conversation.async_converse( hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id ) @@ -455,7 +455,7 @@ async def test_error_handling( assert result.response.response_type == intent.IntentResponseType.ERROR, result assert result.response.error_code == "unknown", result assert result.response.as_dict()["speech"]["plain"]["speech"] == ( - "Sorry, I had a problem talking to Google Generative AI: None some error" + "Sorry, I had a problem talking to Google Generative AI: some error" ) From 8354aa434e8ba7612a6870240a6be410f78c40d4 Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 1 Jul 2024 08:48:39 -0700 Subject: [PATCH 0550/2411] Remove a useless line in Google Generative AI test (#120903) --- tests/components/google_generative_ai_conversation/test_init.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index eeaa777f614..3f860c78da1 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -254,5 +254,4 @@ async def test_config_entry_error( assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state == state - mock_config_entry.async_get_active_flows(hass, {"reauth"}) assert any(mock_config_entry.async_get_active_flows(hass, {"reauth"})) == reauth From d53cfbbb4e490e52ccb6b299ad12e619e0183fe2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 1 Jul 2024 08:52:19 -0700 Subject: [PATCH 0551/2411] Bump gcal_sync to 6.1.4 (#120941) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index d40daa89b0e..163ad91fb7c 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.1.1"] + "requirements": ["gcal-sync==6.1.4", "oauth2client==4.1.3", "ical==8.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1b6315d8953..4f53f53a84d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,7 +924,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.1.3 +gcal-sync==6.1.4 # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91dd535f8bf..3c441d56a98 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -762,7 +762,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.1.3 +gcal-sync==6.1.4 # homeassistant.components.geocaching geocachingapi==0.2.1 From c6cfe073ea8c132f09b05080a2fedb1c3fe01171 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Jul 2024 17:52:30 +0200 Subject: [PATCH 0552/2411] Bump openai to 1.35.1 (#120926) Bump openai to 1.35.7 --- homeassistant/components/openai_conversation/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/openai_conversation/manifest.json b/homeassistant/components/openai_conversation/manifest.json index 0c06a3d4cd8..fcbdc996ce5 100644 --- a/homeassistant/components/openai_conversation/manifest.json +++ b/homeassistant/components/openai_conversation/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/openai_conversation", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["openai==1.3.8"] + "requirements": ["openai==1.35.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4f53f53a84d..e2fe45139de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1477,7 +1477,7 @@ open-garage==0.2.0 open-meteo==0.3.1 # homeassistant.components.openai_conversation -openai==1.3.8 +openai==1.35.7 # homeassistant.components.openerz openerz-api==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c441d56a98..f1c5d8553db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1198,7 +1198,7 @@ open-garage==0.2.0 open-meteo==0.3.1 # homeassistant.components.openai_conversation -openai==1.3.8 +openai==1.35.7 # homeassistant.components.openerz openerz-api==0.3.0 From b3a50893cfa77a531142fb247a2c59a52b9f5038 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:55:01 +0200 Subject: [PATCH 0553/2411] Use service_calls fixture in kodi tests (#120929) --- tests/components/kodi/test_device_trigger.py | 28 +++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py index d3de349018e..587aab76931 100644 --- a/tests/components/kodi/test_device_trigger.py +++ b/tests/components/kodi/test_device_trigger.py @@ -12,11 +12,7 @@ from homeassistant.setup import async_setup_component from . import init_integration -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) +from tests.common import MockConfigEntry, async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -24,12 +20,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.fixture async def kodi_media_player(hass): """Get a kodi media player.""" @@ -77,7 +67,7 @@ async def test_get_triggers( async def test_if_fires_on_state_change( hass: HomeAssistant, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], kodi_media_player, ) -> None: """Test for turn_on and turn_off triggers firing.""" @@ -135,8 +125,8 @@ async def test_if_fires_on_state_change( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == f"turn_on - {kodi_media_player} - 0" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == f"turn_on - {kodi_media_player} - 0" await hass.services.async_call( MP_DOMAIN, @@ -146,14 +136,14 @@ async def test_if_fires_on_state_change( ) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == f"turn_off - {kodi_media_player} - 0" + assert len(service_calls) == 4 + assert service_calls[3].data["some"] == f"turn_off - {kodi_media_player} - 0" async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], kodi_media_player, ) -> None: """Test for turn_on and turn_off triggers firing.""" @@ -194,5 +184,5 @@ async def test_if_fires_on_state_change_legacy( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == f"turn_on - {kodi_media_player} - 0" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == f"turn_on - {kodi_media_player} - 0" From afb0a6e0ab6d88a062b9e7c1b9734d8817329659 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:55:39 +0200 Subject: [PATCH 0554/2411] Use service_calls fixture in homekit_controller tests (#120927) --- .../homekit_controller/test_device_trigger.py | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index 43572f56d50..6c4c2ce8191 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -15,7 +15,7 @@ from homeassistant.setup import async_setup_component from .common import setup_test_component -from tests.common import async_get_device_automations, async_mock_service +from tests.common import async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -23,12 +23,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - def create_remote(accessory): """Define characteristics for a button (that is inn a group).""" service_label = accessory.add_service(ServicesTypes.SERVICE_LABEL) @@ -239,7 +233,7 @@ async def test_handle_events( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test that events are handled.""" helper = await setup_test_component(hass, create_remote) @@ -303,8 +297,8 @@ async def test_handle_events( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "device - button1 - single_press - 0" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "device - button1 - single_press - 0" # Make sure automation doesn't trigger for long press helper.pairing.testing.update_named_service( @@ -312,7 +306,7 @@ async def test_handle_events( ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Make sure automation doesn't trigger for double press helper.pairing.testing.update_named_service( @@ -320,7 +314,7 @@ async def test_handle_events( ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Make sure second automation fires for long press helper.pairing.testing.update_named_service( @@ -328,8 +322,8 @@ async def test_handle_events( ) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "device - button2 - long_press - 0" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "device - button2 - long_press - 0" # Turn the automations off await hass.services.async_call( @@ -338,6 +332,7 @@ async def test_handle_events( {"entity_id": "automation.long_press"}, blocking=True, ) + assert len(service_calls) == 3 await hass.services.async_call( "automation", @@ -345,6 +340,7 @@ async def test_handle_events( {"entity_id": "automation.single_press"}, blocking=True, ) + assert len(service_calls) == 4 # Make sure event no longer fires helper.pairing.testing.update_named_service( @@ -352,14 +348,14 @@ async def test_handle_events( ) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 4 async def test_handle_events_late_setup( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test that events are handled when setup happens after startup.""" helper = await setup_test_component(hass, create_remote) @@ -432,8 +428,8 @@ async def test_handle_events_late_setup( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "device - button1 - single_press - 0" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "device - button1 - single_press - 0" # Make sure automation doesn't trigger for a polled None helper.pairing.testing.update_named_service( @@ -441,7 +437,7 @@ async def test_handle_events_late_setup( ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Make sure automation doesn't trigger for long press helper.pairing.testing.update_named_service( @@ -449,7 +445,7 @@ async def test_handle_events_late_setup( ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Make sure automation doesn't trigger for double press helper.pairing.testing.update_named_service( @@ -457,7 +453,7 @@ async def test_handle_events_late_setup( ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Make sure second automation fires for long press helper.pairing.testing.update_named_service( @@ -465,8 +461,8 @@ async def test_handle_events_late_setup( ) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "device - button2 - long_press - 0" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "device - button2 - long_press - 0" # Turn the automations off await hass.services.async_call( @@ -475,6 +471,7 @@ async def test_handle_events_late_setup( {"entity_id": "automation.long_press"}, blocking=True, ) + assert len(service_calls) == 3 await hass.services.async_call( "automation", @@ -482,6 +479,7 @@ async def test_handle_events_late_setup( {"entity_id": "automation.single_press"}, blocking=True, ) + assert len(service_calls) == 4 # Make sure event no longer fires helper.pairing.testing.update_named_service( @@ -489,4 +487,4 @@ async def test_handle_events_late_setup( ) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 4 From 918ac5d67cd361a799e5db6d8662cc702aaf6282 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:56:58 +0200 Subject: [PATCH 0555/2411] Use service_calls fixture in geo_location tests (#120911) --- tests/components/geo_location/test_trigger.py | 60 ++++++++++--------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/tests/components/geo_location/test_trigger.py b/tests/components/geo_location/test_trigger.py index e5fb93dcf8f..7673f357a08 100644 --- a/tests/components/geo_location/test_trigger.py +++ b/tests/components/geo_location/test_trigger.py @@ -29,7 +29,7 @@ def calls(hass: HomeAssistant) -> list[ServiceCall]: @pytest.fixture(autouse=True) -def setup_comp(hass): +def setup_comp(hass: HomeAssistant) -> None: """Initialize components.""" mock_component(hass, "group") hass.loop.run_until_complete( @@ -49,7 +49,7 @@ def setup_comp(hass): async def test_if_fires_on_zone_enter( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on zone enter.""" context = Context() @@ -96,10 +96,10 @@ async def test_if_fires_on_zone_enter( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context.parent_id == context.id + assert len(service_calls) == 1 + assert service_calls[0].context.parent_id == context.id assert ( - calls[0].data["some"] + service_calls[0].data["some"] == "geo_location - geo_location.entity - hello - hello - test - 0" ) @@ -118,6 +118,8 @@ async def test_if_fires_on_zone_enter( blocking=True, ) + assert len(service_calls) == 2 + hass.states.async_set( "geo_location.entity", "hello", @@ -125,11 +127,11 @@ async def test_if_fires_on_zone_enter( ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 2 async def test_if_not_fires_for_enter_on_zone_leave( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for not firing on zone leave.""" hass.states.async_set( @@ -162,11 +164,11 @@ async def test_if_not_fires_for_enter_on_zone_leave( ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_if_fires_on_zone_leave( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on zone leave.""" hass.states.async_set( @@ -199,11 +201,11 @@ async def test_if_fires_on_zone_leave( ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_zone_leave_2( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on zone leave for unavailable entity.""" hass.states.async_set( @@ -236,11 +238,11 @@ async def test_if_fires_on_zone_leave_2( ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_if_not_fires_for_leave_on_zone_enter( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for not firing on zone enter.""" hass.states.async_set( @@ -273,11 +275,11 @@ async def test_if_not_fires_for_leave_on_zone_enter( ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_if_fires_on_zone_appear( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing if entity appears in zone.""" assert await async_setup_component( @@ -317,15 +319,16 @@ async def test_if_fires_on_zone_appear( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context.parent_id == context.id + assert len(service_calls) == 1 + assert service_calls[0].context.parent_id == context.id assert ( - calls[0].data["some"] == "geo_location - geo_location.entity - - hello - test" + service_calls[0].data["some"] + == "geo_location - geo_location.entity - - hello - test" ) async def test_if_fires_on_zone_appear_2( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing if entity appears in zone.""" assert await async_setup_component( @@ -373,16 +376,16 @@ async def test_if_fires_on_zone_appear_2( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context.parent_id == context.id + assert len(service_calls) == 1 + assert service_calls[0].context.parent_id == context.id assert ( - calls[0].data["some"] + service_calls[0].data["some"] == "geo_location - geo_location.entity - goodbye - hello - test" ) async def test_if_fires_on_zone_disappear( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing if entity disappears from zone.""" hass.states.async_set( @@ -423,14 +426,17 @@ async def test_if_fires_on_zone_disappear( hass.states.async_remove("geo_location.entity") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] == "geo_location - geo_location.entity - hello - - test" + service_calls[0].data["some"] + == "geo_location - geo_location.entity - hello - - test" ) async def test_zone_undefined( - hass: HomeAssistant, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + service_calls: list[ServiceCall], + caplog: pytest.LogCaptureFixture, ) -> None: """Test for undefined zone.""" hass.states.async_set( @@ -466,7 +472,7 @@ async def test_zone_undefined( ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 assert ( f"Unable to execute automation automation 0: Zone {zone_does_not_exist} not found" From e5c7ff6a5b55e62fe1b8ae3526b2c758a3c2aaf2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:57:40 +0200 Subject: [PATCH 0556/2411] Use service_calls fixture in conversation tests (#120906) --- tests/components/conversation/test_trigger.py | 58 +++++++++---------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index c5d4382e917..3c3e58e7136 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -11,16 +11,9 @@ from homeassistant.core import Context, HomeAssistant, ServiceCall from homeassistant.helpers import trigger from homeassistant.setup import async_setup_component -from tests.common import async_mock_service from tests.typing import WebSocketGenerator -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.fixture(autouse=True) async def setup_comp(hass: HomeAssistant) -> None: """Initialize components.""" @@ -29,7 +22,7 @@ async def setup_comp(hass: HomeAssistant) -> None: async def test_if_fires_on_event( - hass: HomeAssistant, calls: list[ServiceCall], setup_comp: None + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test the firing of events.""" assert await async_setup_component( @@ -62,8 +55,10 @@ async def test_if_fires_on_event( assert service_response["response"]["speech"]["plain"]["speech"] == "Done" await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["data"] == { + assert len(service_calls) == 2 + assert service_calls[1].domain == "test" + assert service_calls[1].service == "automation" + assert service_calls[1].data["data"] == { "alias": None, "id": "0", "idx": "0", @@ -75,7 +70,7 @@ async def test_if_fires_on_event( } -async def test_response(hass: HomeAssistant, setup_comp) -> None: +async def test_response(hass: HomeAssistant) -> None: """Test the conversation response action.""" response = "I'm sorry, Dave. I'm afraid I can't do that" assert await async_setup_component( @@ -106,7 +101,7 @@ async def test_response(hass: HomeAssistant, setup_comp) -> None: assert service_response["response"]["speech"]["plain"]["speech"] == response -async def test_empty_response(hass: HomeAssistant, setup_comp) -> None: +async def test_empty_response(hass: HomeAssistant) -> None: """Test the conversation response action with an empty response.""" assert await async_setup_component( hass, @@ -137,7 +132,7 @@ async def test_empty_response(hass: HomeAssistant, setup_comp) -> None: async def test_response_same_sentence( - hass: HomeAssistant, calls: list[ServiceCall], setup_comp: None + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test the conversation response action with multiple triggers using the same sentence.""" assert await async_setup_component( @@ -186,8 +181,10 @@ async def test_response_same_sentence( assert service_response["response"]["speech"]["plain"]["speech"] == "response 1" # Service should still have been called - assert len(calls) == 1 - assert calls[0].data["data"] == { + assert len(service_calls) == 2 + assert service_calls[1].domain == "test" + assert service_calls[1].service == "automation" + assert service_calls[1].data["data"] == { "alias": None, "id": "trigger1", "idx": "0", @@ -201,8 +198,6 @@ async def test_response_same_sentence( async def test_response_same_sentence_with_error( hass: HomeAssistant, - calls: list[ServiceCall], - setup_comp: None, caplog: pytest.LogCaptureFixture, ) -> None: """Test the conversation response action with multiple triggers using the same sentence and an error.""" @@ -253,7 +248,7 @@ async def test_response_same_sentence_with_error( async def test_subscribe_trigger_does_not_interfere_with_responses( - hass: HomeAssistant, setup_comp, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test that subscribing to a trigger from the websocket API does not interfere with responses.""" websocket_client = await hass_ws_client() @@ -310,7 +305,7 @@ async def test_subscribe_trigger_does_not_interfere_with_responses( async def test_same_trigger_multiple_sentences( - hass: HomeAssistant, calls: list[ServiceCall], setup_comp: None + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test matching of multiple sentences from the same trigger.""" assert await async_setup_component( @@ -341,8 +336,10 @@ async def test_same_trigger_multiple_sentences( # Only triggers once await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["data"] == { + assert len(service_calls) == 2 + assert service_calls[1].domain == "test" + assert service_calls[1].service == "automation" + assert service_calls[1].data["data"] == { "alias": None, "id": "0", "idx": "0", @@ -355,7 +352,7 @@ async def test_same_trigger_multiple_sentences( async def test_same_sentence_multiple_triggers( - hass: HomeAssistant, calls: list[ServiceCall], setup_comp: None + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test use of the same sentence in multiple triggers.""" assert await async_setup_component( @@ -403,11 +400,12 @@ async def test_same_sentence_multiple_triggers( ) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 3 # The calls may come in any order call_datas: set[tuple[str, str, str]] = set() - for call in calls: + service_calls.pop(0) # First call is the call to conversation.process + for call in service_calls: call_data = call.data["data"] call_datas.add((call_data["id"], call_data["platform"], call_data["sentence"])) @@ -474,9 +472,7 @@ async def test_fails_on_no_sentences(hass: HomeAssistant) -> None: ) -async def test_wildcards( - hass: HomeAssistant, calls: list[ServiceCall], setup_comp: None -) -> None: +async def test_wildcards(hass: HomeAssistant, service_calls: list[ServiceCall]) -> None: """Test wildcards in trigger sentences.""" assert await async_setup_component( hass, @@ -507,8 +503,10 @@ async def test_wildcards( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["data"] == { + assert len(service_calls) == 2 + assert service_calls[1].domain == "test" + assert service_calls[1].service == "automation" + assert service_calls[1].data["data"] == { "alias": None, "id": "0", "idx": "0", @@ -536,8 +534,6 @@ async def test_wildcards( async def test_trigger_with_device_id(hass: HomeAssistant) -> None: """Test that a trigger receives a device_id.""" - assert await async_setup_component(hass, "homeassistant", {}) - assert await async_setup_component(hass, "conversation", {}) assert await async_setup_component( hass, "automation", From 001ee0cc0bd13d37caaa2bf7c3bcfbd827627d04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Jul 2024 09:26:20 -0700 Subject: [PATCH 0557/2411] Downgrade logging previously reported asyncio block to debug (#120942) --- homeassistant/util/loop.py | 123 +++++++++++----- tests/util/test_loop.py | 282 +++++++++++++++++++++---------------- 2 files changed, 244 insertions(+), 161 deletions(-) diff --git a/homeassistant/util/loop.py b/homeassistant/util/loop.py index 866f35e79e2..d7593013046 100644 --- a/homeassistant/util/loop.py +++ b/homeassistant/util/loop.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable import functools +from functools import cache import linecache import logging import threading @@ -26,6 +27,11 @@ def _get_line_from_cache(filename: str, lineno: int) -> str: return (linecache.getline(filename, lineno) or "?").strip() +# Set of previously reported blocking calls +# (integration, filename, lineno) +_PREVIOUSLY_REPORTED: set[tuple[str | None, str, int | Any]] = set() + + def raise_for_blocking_call( func: Callable[..., Any], check_allowed: Callable[[dict[str, Any]], bool] | None = None, @@ -42,28 +48,48 @@ def raise_for_blocking_call( offender_filename = offender_frame.f_code.co_filename offender_lineno = offender_frame.f_lineno offender_line = _get_line_from_cache(offender_filename, offender_lineno) + report_key: tuple[str | None, str, int | Any] try: integration_frame = get_integration_frame() except MissingIntegrationFrame: # Did not source from integration? Hard error. + report_key = (None, offender_filename, offender_lineno) + was_reported = report_key in _PREVIOUSLY_REPORTED + _PREVIOUSLY_REPORTED.add(report_key) if not strict_core: - _LOGGER.warning( - "Detected blocking call to %s with args %s in %s, " - "line %s: %s inside the event loop; " - "This is causing stability issues. " - "Please create a bug report at " - "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n" - "%s\n" - "Traceback (most recent call last):\n%s", - func.__name__, - mapped_args.get("args"), - offender_filename, - offender_lineno, - offender_line, - _dev_help_message(func.__name__), - "".join(traceback.format_stack(f=offender_frame)), - ) + if was_reported: + _LOGGER.debug( + "Detected blocking call to %s with args %s in %s, " + "line %s: %s inside the event loop; " + "This is causing stability issues. " + "Please create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n" + "%s\n", + func.__name__, + mapped_args.get("args"), + offender_filename, + offender_lineno, + offender_line, + _dev_help_message(func.__name__), + ) + else: + _LOGGER.warning( + "Detected blocking call to %s with args %s in %s, " + "line %s: %s inside the event loop; " + "This is causing stability issues. " + "Please create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n" + "%s\n" + "Traceback (most recent call last):\n%s", + func.__name__, + mapped_args.get("args"), + offender_filename, + offender_lineno, + offender_line, + _dev_help_message(func.__name__), + "".join(traceback.format_stack(f=offender_frame)), + ) return if found_frame is None: @@ -77,32 +103,56 @@ def raise_for_blocking_call( f"{_dev_help_message(func.__name__)}" ) + report_key = (integration_frame.integration, offender_filename, offender_lineno) + was_reported = report_key in _PREVIOUSLY_REPORTED + _PREVIOUSLY_REPORTED.add(report_key) + report_issue = async_suggest_report_issue( async_get_hass_or_none(), integration_domain=integration_frame.integration, module=integration_frame.module, ) - _LOGGER.warning( - "Detected blocking call to %s with args %s " - "inside the event loop by %sintegration '%s' " - "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n" - "%s\n" - "Traceback (most recent call last):\n%s", - func.__name__, - mapped_args.get("args"), - "custom " if integration_frame.custom_integration else "", - integration_frame.integration, - integration_frame.relative_filename, - integration_frame.line_number, - integration_frame.line, - offender_filename, - offender_lineno, - offender_line, - report_issue, - _dev_help_message(func.__name__), - "".join(traceback.format_stack(f=integration_frame.frame)), - ) + if was_reported: + _LOGGER.debug( + "Detected blocking call to %s with args %s " + "inside the event loop by %sintegration '%s' " + "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n" + "%s\n", + func.__name__, + mapped_args.get("args"), + "custom " if integration_frame.custom_integration else "", + integration_frame.integration, + integration_frame.relative_filename, + integration_frame.line_number, + integration_frame.line, + offender_filename, + offender_lineno, + offender_line, + report_issue, + _dev_help_message(func.__name__), + ) + else: + _LOGGER.warning( + "Detected blocking call to %s with args %s " + "inside the event loop by %sintegration '%s' " + "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n" + "%s\n" + "Traceback (most recent call last):\n%s", + func.__name__, + mapped_args.get("args"), + "custom " if integration_frame.custom_integration else "", + integration_frame.integration, + integration_frame.relative_filename, + integration_frame.line_number, + integration_frame.line, + offender_filename, + offender_lineno, + offender_line, + report_issue, + _dev_help_message(func.__name__), + "".join(traceback.format_stack(f=integration_frame.frame)), + ) if strict: raise RuntimeError( @@ -117,6 +167,7 @@ def raise_for_blocking_call( ) +@cache def _dev_help_message(what: str) -> str: """Generate help message to guide developers.""" return ( diff --git a/tests/util/test_loop.py b/tests/util/test_loop.py index 585f32a965f..f4846d98898 100644 --- a/tests/util/test_loop.py +++ b/tests/util/test_loop.py @@ -1,5 +1,7 @@ """Tests for async util methods from Python source.""" +from collections.abc import Generator +import contextlib import threading from unittest.mock import Mock, patch @@ -15,57 +17,14 @@ def banned_function(): """Mock banned function.""" -async def test_raise_for_blocking_call_async() -> None: - """Test raise_for_blocking_call detects when called from event loop without integration context.""" - with pytest.raises(RuntimeError): - haloop.raise_for_blocking_call(banned_function) - - -async def test_raise_for_blocking_call_async_non_strict_core( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test non_strict_core raise_for_blocking_call detects from event loop without integration context.""" - haloop.raise_for_blocking_call(banned_function, strict_core=False) - assert "Detected blocking call to banned_function" in caplog.text - assert "Traceback (most recent call last)" in caplog.text - assert ( - "Please create a bug report at https://github.com/home-assistant/core/issues" - in caplog.text - ) - assert ( - "For developers, please see " - "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" - ) in caplog.text - - -async def test_raise_for_blocking_call_async_integration( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test raise_for_blocking_call detects and raises when called from event loop from integration context.""" - frames = extract_stack_to_frame( - [ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ] - ) +@contextlib.contextmanager +def patch_get_current_frame(stack: list[Mock]) -> Generator[None, None, None]: + """Patch get_current_frame.""" + frames = extract_stack_to_frame(stack) with ( - pytest.raises(RuntimeError), patch( "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", + return_value=stack[1].line, ), patch( "homeassistant.util.loop._get_line_from_cache", @@ -79,13 +38,104 @@ async def test_raise_for_blocking_call_async_integration( "homeassistant.helpers.frame.get_current_frame", return_value=frames, ), + ): + yield + + +async def test_raise_for_blocking_call_async() -> None: + """Test raise_for_blocking_call detects when called from event loop without integration context.""" + with pytest.raises(RuntimeError): + haloop.raise_for_blocking_call(banned_function) + + +async def test_raise_for_blocking_call_async_non_strict_core( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test non_strict_core raise_for_blocking_call detects from event loop without integration context.""" + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="12", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="12", + line="self.light.is_on", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] + with patch_get_current_frame(stack): + haloop.raise_for_blocking_call(banned_function, strict_core=False) + assert "Detected blocking call to banned_function" in caplog.text + assert "Traceback (most recent call last)" in caplog.text + assert ( + "Please create a bug report at https://github.com/home-assistant/core/issues" + in caplog.text + ) + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text + + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 1 + caplog.clear() + + # Second call should log at debug + with patch_get_current_frame(stack): + haloop.raise_for_blocking_call(banned_function, strict_core=False) + + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 0 + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text + + # no expensive traceback on debug + assert "Traceback (most recent call last)" not in caplog.text + + +async def test_raise_for_blocking_call_async_integration( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test raise_for_blocking_call detects and raises when called from event loop from integration context.""" + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="18", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="18", + line="self.light.is_on", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="8", + line="something()", + ), + ] + with ( + pytest.raises(RuntimeError), + patch_get_current_frame(stack), ): haloop.raise_for_blocking_call(banned_function) assert ( "Detected blocking call to banned_function with args None" " inside the event loop by integration" - " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on " - "(offender: /home/paulus/aiohue/lights.py, line 2: mock_line), please create " + " 'hue' at homeassistant/components/hue/light.py, line 18: self.light.is_on " + "(offender: /home/paulus/aiohue/lights.py, line 8: mock_line), please create " "a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text ) @@ -99,55 +149,37 @@ async def test_raise_for_blocking_call_async_integration_non_strict( caplog: pytest.LogCaptureFixture, ) -> None: """Test raise_for_blocking_call detects when called from event loop from integration context.""" - frames = extract_stack_to_frame( - [ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ] - ) - with ( - patch( - "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="15", + line="do_something()", ), - patch( - "homeassistant.util.loop._get_line_from_cache", - return_value="mock_line", + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="15", + line="self.light.is_on", ), - patch( - "homeassistant.util.loop.get_current_frame", - return_value=frames, + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="1", + line="something()", ), - patch( - "homeassistant.helpers.frame.get_current_frame", - return_value=frames, - ), - ): + ] + with patch_get_current_frame(stack): haloop.raise_for_blocking_call(banned_function, strict=False) + assert ( "Detected blocking call to banned_function with args None" " inside the event loop by integration" - " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on " - "(offender: /home/paulus/aiohue/lights.py, line 2: mock_line), " + " 'hue' at homeassistant/components/hue/light.py, line 15: self.light.is_on " + "(offender: /home/paulus/aiohue/lights.py, line 1: mock_line), " "please create a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text ) assert "Traceback (most recent call last)" in caplog.text assert ( - 'File "/home/paulus/homeassistant/components/hue/light.py", line 23' + 'File "/home/paulus/homeassistant/components/hue/light.py", line 15' in caplog.text ) assert ( @@ -158,62 +190,62 @@ async def test_raise_for_blocking_call_async_integration_non_strict( "For developers, please see " "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" ) in caplog.text + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 1 + caplog.clear() + + # Second call should log at debug + with patch_get_current_frame(stack): + haloop.raise_for_blocking_call(banned_function, strict=False) + + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 0 + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text + # no expensive traceback on debug + assert "Traceback (most recent call last)" not in caplog.text async def test_raise_for_blocking_call_async_custom( caplog: pytest.LogCaptureFixture, ) -> None: """Test raise_for_blocking_call detects when called from event loop with custom component context.""" - frames = extract_stack_to_frame( - [ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - Mock( - filename="/home/paulus/config/custom_components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ] - ) - with ( - pytest.raises(RuntimeError), - patch( - "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="12", + line="do_something()", ), - patch( - "homeassistant.util.loop._get_line_from_cache", - return_value="mock_line", + Mock( + filename="/home/paulus/config/custom_components/hue/light.py", + lineno="12", + line="self.light.is_on", ), - patch( - "homeassistant.util.loop.get_current_frame", - return_value=frames, + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="3", + line="something()", ), - patch( - "homeassistant.helpers.frame.get_current_frame", - return_value=frames, - ), - ): + ] + with pytest.raises(RuntimeError), patch_get_current_frame(stack): haloop.raise_for_blocking_call(banned_function) assert ( "Detected blocking call to banned_function with args None" " inside the event loop by custom " - "integration 'hue' at custom_components/hue/light.py, line 23: self.light.is_on" - " (offender: /home/paulus/aiohue/lights.py, line 2: mock_line), " + "integration 'hue' at custom_components/hue/light.py, line 12: self.light.is_on" + " (offender: /home/paulus/aiohue/lights.py, line 3: mock_line), " "please create a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" ) in caplog.text assert "Traceback (most recent call last)" in caplog.text assert ( - 'File "/home/paulus/config/custom_components/hue/light.py", line 23' + 'File "/home/paulus/config/custom_components/hue/light.py", line 12' in caplog.text ) assert ( From a0b604f98c25079d70a6afac629894a504c47fe9 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 1 Jul 2024 18:27:40 +0200 Subject: [PATCH 0558/2411] Improve add user error messages (#120909) --- homeassistant/auth/providers/homeassistant.py | 20 ++++++++-------- homeassistant/components/auth/strings.json | 5 +++- .../config/auth_provider_homeassistant.py | 24 ++++--------------- .../test_auth_provider_homeassistant.py | 16 +++++++++++-- 4 files changed, 32 insertions(+), 33 deletions(-) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 4e38260dd2f..ec39bdbdcdc 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -55,13 +55,6 @@ class InvalidUser(HomeAssistantError): Will not be raised when validating authentication. """ - -class InvalidUsername(InvalidUser): - """Raised when invalid username is specified. - - Will not be raised when validating authentication. - """ - def __init__( self, *args: object, @@ -77,6 +70,13 @@ class InvalidUsername(InvalidUser): ) +class InvalidUsername(InvalidUser): + """Raised when invalid username is specified. + + Will not be raised when validating authentication. + """ + + class Data: """Hold the user data.""" @@ -216,7 +216,7 @@ class Data: break if index is None: - raise InvalidUser + raise InvalidUser(translation_key="user_not_found") self.users.pop(index) @@ -232,7 +232,7 @@ class Data: user["password"] = self.hash_password(new_password, True).decode() break else: - raise InvalidUser + raise InvalidUser(translation_key="user_not_found") @callback def _validate_new_username(self, new_username: str) -> None: @@ -275,7 +275,7 @@ class Data: self._async_check_for_not_normalized_usernames(self._data) break else: - raise InvalidUser + raise InvalidUser(translation_key="user_not_found") async def async_save(self) -> None: """Save data.""" diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index 0e4cede78a3..c8622880f0f 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -37,7 +37,10 @@ "message": "Username \"{username}\" already exists" }, "username_not_normalized": { - "message": "Username \"{new_username}\" is not normalized" + "message": "Username \"{new_username}\" is not normalized. Please make sure the username is lowercase and does not contain any whitespace." + }, + "user_not_found": { + "message": "User not found" } }, "issues": { diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 1cfcda6d4b2..8513c53bd07 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -53,11 +53,7 @@ async def websocket_create( ) return - try: - await provider.async_add_auth(msg["username"], msg["password"]) - except auth_ha.InvalidUser: - connection.send_error(msg["id"], "username_exists", "Username already exists") - return + await provider.async_add_auth(msg["username"], msg["password"]) credentials = await provider.async_get_or_create_credentials( {"username": msg["username"]} @@ -94,13 +90,7 @@ async def websocket_delete( connection.send_result(msg["id"]) return - try: - await provider.async_remove_auth(msg["username"]) - except auth_ha.InvalidUser: - connection.send_error( - msg["id"], "auth_not_found", "Given username was not found." - ) - return + await provider.async_remove_auth(msg["username"]) connection.send_result(msg["id"]) @@ -187,14 +177,8 @@ async def websocket_admin_change_password( ) return - try: - await provider.async_change_password(username, msg["password"]) - connection.send_result(msg["id"]) - except auth_ha.InvalidUser: - connection.send_error( - msg["id"], "credentials_not_found", "Credentials not found" - ) - return + await provider.async_change_password(username, msg["password"]) + connection.send_result(msg["id"]) @websocket_api.websocket_command( diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index ffee88f91ec..6b580013968 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -183,7 +183,13 @@ async def test_create_auth_duplicate_username( result = await client.receive_json() assert not result["success"], result - assert result["error"]["code"] == "username_exists" + assert result["error"] == { + "code": "home_assistant_error", + "message": "username_already_exists", + "translation_key": "username_already_exists", + "translation_placeholders": {"username": "test-user"}, + "translation_domain": "auth", + } async def test_delete_removes_just_auth( @@ -282,7 +288,13 @@ async def test_delete_unknown_auth( result = await client.receive_json() assert not result["success"], result - assert result["error"]["code"] == "auth_not_found" + assert result["error"] == { + "code": "home_assistant_error", + "message": "user_not_found", + "translation_key": "user_not_found", + "translation_placeholders": None, + "translation_domain": "auth", + } async def test_change_password( From d506c30b3871bc360b926c961ac71e12355f8bb9 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 1 Jul 2024 18:58:43 +0200 Subject: [PATCH 0559/2411] Use fixtures in deCONZ logbook tests (#120947) --- tests/components/deconz/test_logbook.py | 77 +++++++++++++------------ 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/tests/components/deconz/test_logbook.py b/tests/components/deconz/test_logbook.py index 5940d2e8e34..2303ee3a298 100644 --- a/tests/components/deconz/test_logbook.py +++ b/tests/components/deconz/test_logbook.py @@ -1,6 +1,8 @@ """The tests for deCONZ logbook.""" -from unittest.mock import patch +from typing import Any + +import pytest from homeassistant.components.deconz.const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.deconz_event import ( @@ -21,20 +23,13 @@ from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from homeassistant.util import slugify -from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration - from tests.components.logbook.common import MockRow, mock_humanify -from tests.test_util.aiohttp import AiohttpClientMocker -async def test_humanifying_deconz_alarm_event( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - device_registry: dr.DeviceRegistry, -) -> None: - """Test humanifying deCONZ event.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "config": { "armed": "disarmed", @@ -60,12 +55,17 @@ async def test_humanifying_deconz_alarm_event( "uniqueid": "00:0d:6f:00:13:4f:61:39-01-0501", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - - keypad_event_id = slugify(data["sensors"]["1"]["name"]) - keypad_serial = serial_from_unique_id(data["sensors"]["1"]["uniqueid"]) + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_humanifying_deconz_alarm_event( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + sensor_payload: dict[str, Any], +) -> None: + """Test humanifying deCONZ alarm event.""" + keypad_event_id = slugify(sensor_payload["1"]["name"]) + keypad_serial = serial_from_unique_id(sensor_payload["1"]["uniqueid"]) keypad_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, keypad_serial)} ) @@ -113,14 +113,10 @@ async def test_humanifying_deconz_alarm_event( assert events[1]["message"] == "fired event 'armed_away'" -async def test_humanifying_deconz_event( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - device_registry: dr.DeviceRegistry, -) -> None: - """Test humanifying deCONZ event.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "name": "Switch 1", "type": "ZHASwitch", @@ -152,30 +148,35 @@ async def test_humanifying_deconz_event( "uniqueid": "00:00:00:00:00:00:00:04-00", }, } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - - switch_event_id = slugify(data["sensors"]["1"]["name"]) - switch_serial = serial_from_unique_id(data["sensors"]["1"]["uniqueid"]) + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_humanifying_deconz_event( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + sensor_payload: dict[str, Any], +) -> None: + """Test humanifying deCONZ event.""" + switch_event_id = slugify(sensor_payload["1"]["name"]) + switch_serial = serial_from_unique_id(sensor_payload["1"]["uniqueid"]) switch_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, switch_serial)} ) - hue_remote_event_id = slugify(data["sensors"]["2"]["name"]) - hue_remote_serial = serial_from_unique_id(data["sensors"]["2"]["uniqueid"]) + hue_remote_event_id = slugify(sensor_payload["2"]["name"]) + hue_remote_serial = serial_from_unique_id(sensor_payload["2"]["uniqueid"]) hue_remote_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, hue_remote_serial)} ) - xiaomi_cube_event_id = slugify(data["sensors"]["3"]["name"]) - xiaomi_cube_serial = serial_from_unique_id(data["sensors"]["3"]["uniqueid"]) + xiaomi_cube_event_id = slugify(sensor_payload["3"]["name"]) + xiaomi_cube_serial = serial_from_unique_id(sensor_payload["3"]["uniqueid"]) xiaomi_cube_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, xiaomi_cube_serial)} ) - faulty_event_id = slugify(data["sensors"]["4"]["name"]) - faulty_serial = serial_from_unique_id(data["sensors"]["4"]["uniqueid"]) + faulty_event_id = slugify(sensor_payload["4"]["name"]) + faulty_serial = serial_from_unique_id(sensor_payload["4"]["uniqueid"]) faulty_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, faulty_serial)} ) From 7a3039aecb111bb69a4628c4097f13e0bb77b206 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 1 Jul 2024 19:01:32 +0200 Subject: [PATCH 0560/2411] Use fixtures in deCONZ lock tests (#120948) --- tests/components/deconz/test_lock.py | 77 +++++++++++++--------------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index 03d14802083..cbb3ad92cb2 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -1,12 +1,15 @@ """deCONZ lock platform tests.""" -from unittest.mock import patch +from collections.abc import Callable + +import pytest from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, SERVICE_LOCK, SERVICE_UNLOCK, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, STATE_LOCKED, @@ -15,29 +18,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from .test_gateway import ( - DECONZ_WEB_REQUEST, - mock_deconz_put_request, - setup_deconz_integration, -) - from tests.test_util.aiohttp import AiohttpClientMocker -async def test_no_locks( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that no lock entities are created.""" - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 - - -async def test_lock_from_light( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test that all supported lock entities based on lights are created.""" - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "1": { "etag": "5c2ec06cde4bd654aef3a555fcd8ad12", "hascolor": False, @@ -52,10 +39,15 @@ async def test_lock_from_light( "uniqueid": "00:00:00:00:00:00:00:00-00", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +async def test_lock_from_light( + hass: HomeAssistant, + config_entry_setup: ConfigEntry, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + mock_deconz_websocket, +) -> None: + """Test that all supported lock entities based on lights are created.""" assert len(hass.states.async_all()) == 1 assert hass.states.get("lock.door_lock").state == STATE_UNLOCKED @@ -73,7 +65,7 @@ async def test_lock_from_light( # Verify service calls - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") + aioclient_mock = mock_put_request("/lights/1/state") # Service lock door @@ -95,24 +87,22 @@ async def test_lock_from_light( ) assert aioclient_mock.mock_calls[2][2] == {"on": False} - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) states = hass.states.async_all() assert len(states) == 1 for state in states: assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 -async def test_lock_from_sensor( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test that all supported lock entities based on sensors are created.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "config": { "battery": 100, @@ -135,10 +125,15 @@ async def test_lock_from_sensor( "uniqueid": "00:00:00:00:00:00:00:00-00", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +async def test_lock_from_sensor( + hass: HomeAssistant, + config_entry_setup: ConfigEntry, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + mock_deconz_websocket, +) -> None: + """Test that all supported lock entities based on sensors are created.""" assert len(hass.states.async_all()) == 2 assert hass.states.get("lock.door_lock").state == STATE_UNLOCKED @@ -156,7 +151,7 @@ async def test_lock_from_sensor( # Verify service calls - mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/1/config") + aioclient_mock = mock_put_request("/sensors/1/config") # Service lock door @@ -178,13 +173,13 @@ async def test_lock_from_sensor( ) assert aioclient_mock.mock_calls[2][2] == {"lock": False} - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) states = hass.states.async_all() assert len(states) == 2 for state in states: assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 From a29dc4ef1e923ba576d5cd3b558d44dcbeb09d86 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Mon, 1 Jul 2024 19:02:43 +0200 Subject: [PATCH 0561/2411] Fix Bang & Olufsen jumping volume bar (#120946) --- homeassistant/components/bang_olufsen/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 0eff9f2bb85..07e38d633a1 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -366,7 +366,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): @property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" - if self._volume.level and self._volume.level.level: + if self._volume.level and self._volume.level.level is not None: return float(self._volume.level.level / 100) return None From 1209abc9442948e0c9cadb8b51344adeb863ef2a Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 1 Jul 2024 19:03:32 +0200 Subject: [PATCH 0562/2411] Use fixtures in deCONZ switch tests (#120944) --- tests/components/deconz/test_switch.py | 86 ++++++++++++-------------- 1 file changed, 38 insertions(+), 48 deletions(-) diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 9ef2382a2e2..23e072bef24 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -1,6 +1,8 @@ """deCONZ switch platform tests.""" -from unittest.mock import patch +from collections.abc import Callable + +import pytest from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN @@ -9,33 +11,18 @@ from homeassistant.components.switch import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .test_gateway import ( - DECONZ_WEB_REQUEST, - mock_deconz_put_request, - setup_deconz_integration, -) - from tests.test_util.aiohttp import AiohttpClientMocker -async def test_no_switches( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that no switch entities are created.""" - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 - - -async def test_power_plugs( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test that all supported switch entities are created.""" - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "1": { "name": "On off switch", "type": "On/Off plug-in unit", @@ -61,10 +48,15 @@ async def test_power_plugs( "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) - + ], +) +async def test_power_plugs( + hass: HomeAssistant, + config_entry_setup: ConfigEntry, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + mock_deconz_websocket, +) -> None: + """Test that all supported switch entities are created.""" assert len(hass.states.async_all()) == 4 assert hass.states.get("switch.on_off_switch").state == STATE_ON assert hass.states.get("switch.smart_plug").state == STATE_OFF @@ -85,7 +77,7 @@ async def test_power_plugs( # Verify service calls - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") + aioclient_mock = mock_put_request("/lights/1/state") # Service turn on power plug @@ -107,44 +99,42 @@ async def test_power_plugs( ) assert aioclient_mock.mock_calls[2][2] == {"on": False} - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) states = hass.states.async_all() assert len(states) == 4 for state in states: assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 -async def test_remove_legacy_on_off_output_as_light( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - entity_registry: er.EntityRegistry, -) -> None: - """Test that switch platform cleans up legacy light entities.""" - unique_id = "00:00:00:00:00:00:00:00-00" - - switch_light_entity = entity_registry.async_get_or_create( - LIGHT_DOMAIN, DECONZ_DOMAIN, unique_id - ) - - assert switch_light_entity - - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "1": { "name": "On Off output device", "type": "On/Off output", "state": {"on": True, "reachable": True}, - "uniqueid": unique_id, + "uniqueid": "00:00:00:00:00:00:00:00-00", }, } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) + ], +) +async def test_remove_legacy_on_off_output_as_light( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory: Callable[[], ConfigEntry], +) -> None: + """Test that switch platform cleans up legacy light entities.""" + assert entity_registry.async_get_or_create( + LIGHT_DOMAIN, DECONZ_DOMAIN, "00:00:00:00:00:00:00:00-00" + ) + + await config_entry_factory() assert not entity_registry.async_get("light.on_off_output_device") assert entity_registry.async_get("switch.on_off_output_device") From b5367e5994997436a071b26b581f9e0049d4e3de Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 1 Jul 2024 20:06:56 +0300 Subject: [PATCH 0563/2411] Fix Shelly device shutdown (#120881) --- homeassistant/components/shelly/__init__.py | 6 ++++++ .../components/shelly/config_flow.py | 19 ++++++++++++------- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 184b7c8bb6b..75f66d0bced 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -174,10 +174,13 @@ async def _async_setup_block_entry( await device.initialize() if not device.firmware_supported: async_create_issue_unsupported_firmware(hass, entry) + await device.shutdown() raise ConfigEntryNotReady except (DeviceConnectionError, MacAddressMismatchError) as err: + await device.shutdown() raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: + await device.shutdown() raise ConfigEntryAuthFailed(repr(err)) from err runtime_data.block = ShellyBlockCoordinator(hass, entry, device) @@ -247,10 +250,13 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) await device.initialize() if not device.firmware_supported: async_create_issue_unsupported_firmware(hass, entry) + await device.shutdown() raise ConfigEntryNotReady except (DeviceConnectionError, MacAddressMismatchError) as err: + await device.shutdown() raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: + await device.shutdown() raise ConfigEntryAuthFailed(repr(err)) from err runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index c044d032170..cb3bca6aa47 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -102,10 +102,11 @@ async def validate_input( ws_context, options, ) - await rpc_device.initialize() - await rpc_device.shutdown() - - sleep_period = get_rpc_device_wakeup_period(rpc_device.status) + try: + await rpc_device.initialize() + sleep_period = get_rpc_device_wakeup_period(rpc_device.status) + finally: + await rpc_device.shutdown() return { "title": rpc_device.name, @@ -121,11 +122,15 @@ async def validate_input( coap_context, options, ) - await block_device.initialize() - await block_device.shutdown() + try: + await block_device.initialize() + sleep_period = get_block_device_sleep_period(block_device.settings) + finally: + await block_device.shutdown() + return { "title": block_device.name, - CONF_SLEEP_PERIOD: get_block_device_sleep_period(block_device.settings), + CONF_SLEEP_PERIOD: sleep_period, "model": block_device.model, CONF_GEN: gen, } diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index b1b00e40c66..4076f53c28c 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==10.0.1"], + "requirements": ["aioshelly==11.0.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index e2fe45139de..8781c83f901 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -362,7 +362,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==10.0.1 +aioshelly==11.0.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1c5d8553db..eee7bd70159 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==10.0.1 +aioshelly==11.0.0 # homeassistant.components.skybell aioskybell==22.7.0 From c2dc9e9b67671fcd1eee90f5929adb64b2aaa7f5 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Mon, 1 Jul 2024 19:23:27 +0200 Subject: [PATCH 0564/2411] Simplify Bang & Olufsen media_image_url property (#120951) Simplify media_image_url property --- homeassistant/components/bang_olufsen/media_player.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 07e38d633a1..a9569a755c2 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -398,9 +398,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): @property def media_image_url(self) -> str | None: """Return URL of the currently playing music.""" - if self._media_image: - return self._media_image.url - return None + return self._media_image.url @property def media_image_remotely_accessible(self) -> bool: From 07f095aa4270d3110e26706937e9b8f9ef54b7ad Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Jul 2024 19:27:50 +0200 Subject: [PATCH 0565/2411] Use service_calls fixture in core platform tests [a-l] (#120904) --- .../binary_sensor/test_device_condition.py | 41 ++++------ .../binary_sensor/test_device_trigger.py | 38 ++++----- .../components/button/test_device_trigger.py | 24 ++---- .../climate/test_device_condition.py | 34 +++----- .../components/climate/test_device_trigger.py | 32 +++----- .../components/cover/test_device_condition.py | 79 +++++++++---------- tests/components/cover/test_device_trigger.py | 77 +++++++++--------- .../components/device_automation/test_init.py | 32 +++----- .../device_automation/test_toggle_entity.py | 30 +++---- .../device_tracker/test_device_condition.py | 28 +++---- .../device_tracker/test_device_trigger.py | 28 +++---- tests/components/fan/test_device_condition.py | 28 +++---- tests/components/fan/test_device_trigger.py | 33 +++----- .../humidifier/test_device_condition.py | 38 ++++----- .../humidifier/test_device_trigger.py | 49 ++++++------ tests/components/light/test_device_action.py | 10 +-- .../components/light/test_device_condition.py | 41 ++++------ tests/components/light/test_device_trigger.py | 38 ++++----- .../components/lock/test_device_condition.py | 48 +++++------ tests/components/lock/test_device_trigger.py | 61 +++++++------- 20 files changed, 316 insertions(+), 473 deletions(-) diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index c2bd29fad36..8a0132ff2af 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -22,7 +22,6 @@ from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, setup_test_component_platform, ) @@ -32,12 +31,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_conditions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -239,7 +232,7 @@ async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test for turn_on and turn_off conditions.""" @@ -308,26 +301,26 @@ async def test_if_state( ) await hass.async_block_till_done() assert hass.states.get(entry.entity_id).state == STATE_ON - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_on event - test_event1" hass.states.async_set(entry.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "is_off event - test_event2" async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test for turn_on and turn_off conditions.""" @@ -375,19 +368,19 @@ async def test_if_state_legacy( ) await hass.async_block_till_done() assert hass.states.get(entry.entity_id).state == STATE_ON - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_on event - test_event1" async def test_if_fires_on_for_condition( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test for firing if condition is on with delay.""" @@ -439,26 +432,26 @@ async def test_if_fires_on_for_condition( ) await hass.async_block_till_done() assert hass.states.get(entry.entity_id).state == STATE_ON - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 # Time travel 10 secs into the future time_freeze.move_to(point2) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 # Time travel 20 secs into the future time_freeze.move_to(point3) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_off event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_off event - test_event1" diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index f91a336061d..78e382f77bf 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -22,7 +22,6 @@ from tests.common import ( async_fire_time_changed, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, setup_test_component_platform, ) @@ -32,12 +31,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -240,7 +233,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test for on and off triggers firing.""" @@ -313,21 +306,22 @@ async def test_if_fires_on_state_change( ) await hass.async_block_till_done() assert hass.states.get(entry.entity_id).state == STATE_ON - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"not_bat_low device - {entry.entity_id} - on - off - None" ) hass.states.async_set(entry.entity_id, STATE_ON) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[1].data["some"] == f"bat_low device - {entry.entity_id} - off - on - None" + service_calls[1].data["some"] + == f"bat_low device - {entry.entity_id} - off - on - None" ) @@ -335,7 +329,7 @@ async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test for triggers firing with delay.""" @@ -388,17 +382,17 @@ async def test_if_fires_on_state_change_with_for( ) await hass.async_block_till_done() assert hass.states.get(entry.entity_id).state == STATE_ON - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await hass.async_block_till_done() assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - on - off - 0:00:05" ) @@ -407,7 +401,7 @@ async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test for triggers firing.""" @@ -459,12 +453,12 @@ async def test_if_fires_on_state_change_legacy( ) await hass.async_block_till_done() assert hass.states.get(entry.entity_id).state == STATE_ON - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - on - off - None" ) diff --git a/tests/components/button/test_device_trigger.py b/tests/components/button/test_device_trigger.py index dee8045a71f..f5ade86e1a0 100644 --- a/tests/components/button/test_device_trigger.py +++ b/tests/components/button/test_device_trigger.py @@ -13,17 +13,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") +from tests.common import MockConfigEntry, async_get_device_automations async def test_get_triggers( @@ -109,7 +99,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -158,9 +148,9 @@ async def test_if_fires_on_state_change( # Test triggering device trigger with a to state hass.states.async_set(entry.entity_id, "2021-01-01T23:59:59+00:00") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"to - device - {entry.entity_id} - unknown - 2021-01-01T23:59:59+00:00 - None - 0" ) @@ -169,7 +159,7 @@ async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -218,8 +208,8 @@ async def test_if_fires_on_state_change_legacy( # Test triggering device trigger with a to state hass.states.async_set(entry.entity_id, "2021-01-01T23:59:59+00:00") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"to - device - {entry.entity_id} - unknown - 2021-01-01T23:59:59+00:00 - None - 0" ) diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index 0961bd3dc73..16595f57c6f 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -17,11 +17,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) +from tests.common import MockConfigEntry, async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -29,12 +25,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.mark.parametrize( ("set_state", "features_reg", "features_state", "expected_condition_types"), [ @@ -151,7 +141,7 @@ async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -220,7 +210,7 @@ async def test_if_state( # Should not fire, entity doesn't exist yet hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set( entry.entity_id, @@ -232,8 +222,8 @@ async def test_if_state( hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_hvac_mode - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_hvac_mode - event - test_event1" hass.states.async_set( entry.entity_id, @@ -246,13 +236,13 @@ async def test_if_state( # Should not fire hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_preset_mode - event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "is_preset_mode - event - test_event2" hass.states.async_set( entry.entity_id, @@ -265,14 +255,14 @@ async def test_if_state( # Should not fire hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -323,8 +313,8 @@ async def test_if_state_legacy( hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_hvac_mode - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_hvac_mode - event - test_event1" @pytest.mark.parametrize( diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index e8e5b577bf4..a492d9805b5 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -23,11 +23,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) +from tests.common import MockConfigEntry, async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -35,12 +31,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -151,7 +141,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -236,8 +226,8 @@ async def test_if_fires_on_state_change( }, ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "hvac_mode_changed" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "hvac_mode_changed" # Fake that the temperature is changing hass.states.async_set( @@ -250,8 +240,8 @@ async def test_if_fires_on_state_change( }, ) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "current_temperature_changed" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "current_temperature_changed" # Fake that the humidity is changing hass.states.async_set( @@ -264,15 +254,15 @@ async def test_if_fires_on_state_change( }, ) await hass.async_block_till_done() - assert len(calls) == 3 - assert calls[2].data["some"] == "current_humidity_changed" + assert len(service_calls) == 3 + assert service_calls[2].data["some"] == "current_humidity_changed" async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -329,8 +319,8 @@ async def test_if_fires_on_state_change_legacy( }, ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "hvac_mode_changed" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "hvac_mode_changed" async def test_get_trigger_capabilities_hvac_mode(hass: HomeAssistant) -> None: diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 545bdd6587e..8c1d2d1c9a7 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -26,7 +26,6 @@ from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, setup_test_component_platform, ) @@ -36,12 +35,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.mark.parametrize( ("set_state", "features_reg", "features_state", "expected_condition_types"), [ @@ -359,7 +352,7 @@ async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -473,36 +466,36 @@ async def test_if_state( hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_open - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_open - event - test_event1" hass.states.async_set(entry.entity_id, STATE_CLOSED) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_closed - event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "is_closed - event - test_event2" hass.states.async_set(entry.entity_id, STATE_OPENING) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert len(calls) == 3 - assert calls[2].data["some"] == "is_opening - event - test_event3" + assert len(service_calls) == 3 + assert service_calls[2].data["some"] == "is_opening - event - test_event3" hass.states.async_set(entry.entity_id, STATE_CLOSING) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event4") await hass.async_block_till_done() - assert len(calls) == 4 - assert calls[3].data["some"] == "is_closing - event - test_event4" + assert len(service_calls) == 4 + assert service_calls[3].data["some"] == "is_closing - event - test_event4" async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -550,15 +543,15 @@ async def test_if_state_legacy( hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_open - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_open - event - test_event1" async def test_if_position( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], caplog: pytest.LogCaptureFixture, mock_cover_entities: list[MockCover], ) -> None: @@ -676,10 +669,10 @@ async def test_if_position( await hass.async_block_till_done() hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert len(calls) == 3 - assert calls[0].data["some"] == "is_pos_gt_45 - event - test_event1" - assert calls[1].data["some"] == "is_pos_lt_90 - event - test_event2" - assert calls[2].data["some"] == "is_pos_gt_45_lt_90 - event - test_event3" + assert len(service_calls) == 3 + assert service_calls[0].data["some"] == "is_pos_gt_45 - event - test_event1" + assert service_calls[1].data["some"] == "is_pos_lt_90 - event - test_event2" + assert service_calls[2].data["some"] == "is_pos_gt_45_lt_90 - event - test_event3" hass.states.async_set( ent.entity_id, STATE_CLOSED, attributes={"current_position": 45} @@ -690,9 +683,9 @@ async def test_if_position( await hass.async_block_till_done() hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert len(calls) == 5 - assert calls[3].data["some"] == "is_pos_not_gt_45 - event - test_event1" - assert calls[4].data["some"] == "is_pos_lt_90 - event - test_event2" + assert len(service_calls) == 5 + assert service_calls[3].data["some"] == "is_pos_not_gt_45 - event - test_event1" + assert service_calls[4].data["some"] == "is_pos_lt_90 - event - test_event2" hass.states.async_set( ent.entity_id, STATE_CLOSED, attributes={"current_position": 90} @@ -701,14 +694,14 @@ async def test_if_position( hass.bus.async_fire("test_event2") hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert len(calls) == 6 - assert calls[5].data["some"] == "is_pos_gt_45 - event - test_event1" + assert len(service_calls) == 6 + assert service_calls[5].data["some"] == "is_pos_gt_45 - event - test_event1" hass.states.async_set(ent.entity_id, STATE_UNAVAILABLE, attributes={}) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 7 - assert calls[6].data["some"] == "is_pos_not_gt_45 - event - test_event1" + assert len(service_calls) == 7 + assert service_calls[6].data["some"] == "is_pos_not_gt_45 - event - test_event1" for record in caplog.records: assert record.levelname in ("DEBUG", "INFO") @@ -718,7 +711,7 @@ async def test_if_tilt_position( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], caplog: pytest.LogCaptureFixture, mock_cover_entities: list[MockCover], ) -> None: @@ -836,10 +829,10 @@ async def test_if_tilt_position( await hass.async_block_till_done() hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert len(calls) == 3 - assert calls[0].data["some"] == "is_pos_gt_45 - event - test_event1" - assert calls[1].data["some"] == "is_pos_lt_90 - event - test_event2" - assert calls[2].data["some"] == "is_pos_gt_45_lt_90 - event - test_event3" + assert len(service_calls) == 3 + assert service_calls[0].data["some"] == "is_pos_gt_45 - event - test_event1" + assert service_calls[1].data["some"] == "is_pos_lt_90 - event - test_event2" + assert service_calls[2].data["some"] == "is_pos_gt_45_lt_90 - event - test_event3" hass.states.async_set( ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 45} @@ -850,9 +843,9 @@ async def test_if_tilt_position( await hass.async_block_till_done() hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert len(calls) == 5 - assert calls[3].data["some"] == "is_pos_not_gt_45 - event - test_event1" - assert calls[4].data["some"] == "is_pos_lt_90 - event - test_event2" + assert len(service_calls) == 5 + assert service_calls[3].data["some"] == "is_pos_not_gt_45 - event - test_event1" + assert service_calls[4].data["some"] == "is_pos_lt_90 - event - test_event2" hass.states.async_set( ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 90} @@ -863,14 +856,14 @@ async def test_if_tilt_position( await hass.async_block_till_done() hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert len(calls) == 6 - assert calls[5].data["some"] == "is_pos_gt_45 - event - test_event1" + assert len(service_calls) == 6 + assert service_calls[5].data["some"] == "is_pos_gt_45 - event - test_event1" hass.states.async_set(ent.entity_id, STATE_UNAVAILABLE, attributes={}) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 7 - assert calls[6].data["some"] == "is_pos_not_gt_45 - event - test_event1" + assert len(service_calls) == 7 + assert service_calls[6].data["some"] == "is_pos_not_gt_45 - event - test_event1" for record in caplog.records: assert record.levelname in ("DEBUG", "INFO") diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 419eea05f9f..5eb8cd484b2 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -29,7 +29,6 @@ from tests.common import ( async_fire_time_changed, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, setup_test_component_platform, ) @@ -39,12 +38,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.mark.parametrize( ("set_state", "features_reg", "features_state", "expected_trigger_types"), [ @@ -381,7 +374,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for state triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -496,36 +489,36 @@ async def test_if_fires_on_state_change( # Fake that the entity is opened. hass.states.async_set(entry.entity_id, STATE_OPEN) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"opened - device - {entry.entity_id} - closed - open - None" ) # Fake that the entity is closed. hass.states.async_set(entry.entity_id, STATE_CLOSED) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[1].data["some"] + service_calls[1].data["some"] == f"closed - device - {entry.entity_id} - open - closed - None" ) # Fake that the entity is opening. hass.states.async_set(entry.entity_id, STATE_OPENING) await hass.async_block_till_done() - assert len(calls) == 3 + assert len(service_calls) == 3 assert ( - calls[2].data["some"] + service_calls[2].data["some"] == f"opening - device - {entry.entity_id} - closed - opening - None" ) # Fake that the entity is closing. hass.states.async_set(entry.entity_id, STATE_CLOSING) await hass.async_block_till_done() - assert len(calls) == 4 + assert len(service_calls) == 4 assert ( - calls[3].data["some"] + service_calls[3].data["some"] == f"closing - device - {entry.entity_id} - opening - closing - None" ) @@ -534,7 +527,7 @@ async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for state triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -583,9 +576,9 @@ async def test_if_fires_on_state_change_legacy( # Fake that the entity is opened. hass.states.async_set(entry.entity_id, STATE_OPEN) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"opened - device - {entry.entity_id} - closed - open - None" ) @@ -594,7 +587,7 @@ async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -640,17 +633,17 @@ async def test_if_fires_on_state_change_with_for( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OPEN) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await hass.async_block_till_done() assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - closed - open - 0:00:05" ) @@ -660,7 +653,7 @@ async def test_if_fires_on_position( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, mock_cover_entities: list[MockCover], - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for position triggers.""" setup_test_component_platform(hass, DOMAIN, mock_cover_entities) @@ -769,9 +762,13 @@ async def test_if_fires_on_position( ent.entity_id, STATE_OPEN, attributes={"current_position": 50} ) await hass.async_block_till_done() - assert len(calls) == 3 + assert len(service_calls) == 3 assert sorted( - [calls[0].data["some"], calls[1].data["some"], calls[2].data["some"]] + [ + service_calls[0].data["some"], + service_calls[1].data["some"], + service_calls[2].data["some"], + ] ) == sorted( [ ( @@ -791,9 +788,9 @@ async def test_if_fires_on_position( ent.entity_id, STATE_CLOSED, attributes={"current_position": 45} ) await hass.async_block_till_done() - assert len(calls) == 4 + assert len(service_calls) == 4 assert ( - calls[3].data["some"] + service_calls[3].data["some"] == f"is_pos_lt_90 - device - {entry.entity_id} - closed - closed - None" ) @@ -801,9 +798,9 @@ async def test_if_fires_on_position( ent.entity_id, STATE_CLOSED, attributes={"current_position": 90} ) await hass.async_block_till_done() - assert len(calls) == 5 + assert len(service_calls) == 5 assert ( - calls[4].data["some"] + service_calls[4].data["some"] == f"is_pos_gt_45 - device - {entry.entity_id} - closed - closed - None" ) @@ -812,7 +809,7 @@ async def test_if_fires_on_tilt_position( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_cover_entities: list[MockCover], ) -> None: """Test for tilt position triggers.""" @@ -924,9 +921,13 @@ async def test_if_fires_on_tilt_position( ent.entity_id, STATE_OPEN, attributes={"current_tilt_position": 50} ) await hass.async_block_till_done() - assert len(calls) == 3 + assert len(service_calls) == 3 assert sorted( - [calls[0].data["some"], calls[1].data["some"], calls[2].data["some"]] + [ + service_calls[0].data["some"], + service_calls[1].data["some"], + service_calls[2].data["some"], + ] ) == sorted( [ ( @@ -946,9 +947,9 @@ async def test_if_fires_on_tilt_position( ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 45} ) await hass.async_block_till_done() - assert len(calls) == 4 + assert len(service_calls) == 4 assert ( - calls[3].data["some"] + service_calls[3].data["some"] == f"is_pos_lt_90 - device - {entry.entity_id} - closed - closed - None" ) @@ -956,8 +957,8 @@ async def test_if_fires_on_tilt_position( ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 90} ) await hass.async_block_till_done() - assert len(calls) == 5 + assert len(service_calls) == 5 assert ( - calls[4].data["some"] + service_calls[4].data["some"] == f"is_pos_gt_45 - device - {entry.entity_id} - closed - closed - None" ) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index b270d2ddd7a..750817f3c41 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -23,13 +23,7 @@ from homeassistant.loader import IntegrationNotFound from homeassistant.requirements import RequirementsNotFound from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - MockModule, - async_mock_service, - mock_integration, - mock_platform, -) +from tests.common import MockConfigEntry, MockModule, mock_integration, mock_platform from tests.typing import WebSocketGenerator @@ -1384,15 +1378,9 @@ async def test_automation_with_bad_condition( assert expected_error.format(path="['condition'][0]") in caplog.text -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_automation_with_sub_condition( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: @@ -1492,29 +1480,29 @@ async def test_automation_with_sub_condition( await hass.async_block_till_done() assert hass.states.get(entity_entry1.entity_id).state == STATE_ON assert hass.states.get(entity_entry2.entity_id).state == STATE_OFF - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "or event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "or event - test_event1" hass.states.async_set(entity_entry1.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.states.async_set(entity_entry2.entity_id, STATE_ON) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "or event - test_event1" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "or event - test_event1" hass.states.async_set(entity_entry1.entity_id, STATE_ON) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 4 - assert [calls[2].data["some"], calls[3].data["some"]] == unordered( + assert len(service_calls) == 4 + assert [service_calls[2].data["some"], service_calls[3].data["some"]] == unordered( ["or event - test_event1", "and event - test_event1"] ) diff --git a/tests/components/device_automation/test_toggle_entity.py b/tests/components/device_automation/test_toggle_entity.py index f15730d9525..be4d3bd4c9e 100644 --- a/tests/components/device_automation/test_toggle_entity.py +++ b/tests/components/device_automation/test_toggle_entity.py @@ -11,7 +11,7 @@ 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 tests.common import MockConfigEntry, async_fire_time_changed, async_mock_service +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -19,17 +19,11 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing. @@ -121,20 +115,20 @@ async def test_if_fires_on_state_change( ) await hass.async_block_till_done() assert hass.states.get(entry.entity_id).state == STATE_ON - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 2 - assert {calls[0].data["some"], calls[1].data["some"]} == { + assert len(service_calls) == 2 + assert {service_calls[0].data["some"], service_calls[1].data["some"]} == { f"turn_off device - {entry.entity_id} - on - off - None", f"turn_on_or_off device - {entry.entity_id} - on - off - None", } hass.states.async_set(entry.entity_id, STATE_ON) await hass.async_block_till_done() - assert len(calls) == 4 - assert {calls[2].data["some"], calls[3].data["some"]} == { + assert len(service_calls) == 4 + assert {service_calls[2].data["some"], service_calls[3].data["some"]} == { f"turn_on device - {entry.entity_id} - off - on - None", f"turn_on_or_off device - {entry.entity_id} - off - on - None", } @@ -145,7 +139,7 @@ async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], trigger: str, ) -> None: """Test for triggers firing with delay.""" @@ -193,16 +187,16 @@ async def test_if_fires_on_state_change_with_for( ) await hass.async_block_till_done() assert hass.states.get(entry.entity_id).state == STATE_ON - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await hass.async_block_till_done() assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - on - off - 0:00:05" ) diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py index 6ea4ed7a372..aff020d61a8 100644 --- a/tests/components/device_tracker/test_device_condition.py +++ b/tests/components/device_tracker/test_device_condition.py @@ -12,11 +12,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) +from tests.common import MockConfigEntry, async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -24,12 +20,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_conditions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -114,7 +104,7 @@ async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -184,22 +174,22 @@ async def test_if_state( hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_home - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_home - event - test_event1" hass.states.async_set(entry.entity_id, "school") hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_not_home - event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "is_not_home - event - test_event2" async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -247,5 +237,5 @@ async def test_if_state_legacy( hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_home - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_home - event - test_event1" diff --git a/tests/components/device_tracker/test_device_trigger.py b/tests/components/device_tracker/test_device_trigger.py index 4236e316424..ebff89e1a15 100644 --- a/tests/components/device_tracker/test_device_trigger.py +++ b/tests/components/device_tracker/test_device_trigger.py @@ -17,11 +17,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) +from tests.common import MockConfigEntry, async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -36,12 +32,6 @@ HOME_LATITUDE = 32.880837 HOME_LONGITUDE = -117.237561 -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.fixture(autouse=True) def setup_zone(hass: HomeAssistant) -> None: """Create test zone.""" @@ -145,7 +135,7 @@ async def test_if_fires_on_zone_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for enter and leave triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -228,9 +218,9 @@ async def test_if_fires_on_zone_change( {"latitude": HOME_LATITUDE, "longitude": HOME_LONGITUDE}, ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"enter - device - {entry.entity_id} - -117.235 - -117.238" ) @@ -241,9 +231,9 @@ async def test_if_fires_on_zone_change( {"latitude": AWAY_LATITUDE, "longitude": AWAY_LONGITUDE}, ) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[1].data["some"] + service_calls[1].data["some"] == f"leave - device - {entry.entity_id} - -117.238 - -117.235" ) @@ -252,7 +242,7 @@ async def test_if_fires_on_zone_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for enter and leave triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -311,9 +301,9 @@ async def test_if_fires_on_zone_change_legacy( {"latitude": HOME_LATITUDE, "longitude": HOME_LONGITUDE}, ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"enter - device - {entry.entity_id} - -117.235 - -117.238" ) diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py index 9f9bde1a680..da48f3223af 100644 --- a/tests/components/fan/test_device_condition.py +++ b/tests/components/fan/test_device_condition.py @@ -12,11 +12,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) +from tests.common import MockConfigEntry, async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -24,12 +20,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_conditions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -114,7 +104,7 @@ async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -184,22 +174,22 @@ async def test_if_state( hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_on - event - test_event1" hass.states.async_set(entry.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off - event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "is_off - event - test_event2" async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -246,5 +236,5 @@ async def test_if_state_legacy( ) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_on - event - test_event1" diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index 38f39376592..f4673636637 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -20,7 +20,6 @@ from tests.common import ( async_fire_time_changed, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, ) @@ -29,12 +28,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -180,7 +173,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -273,8 +266,8 @@ async def test_if_fires_on_state_change( # Fake that the entity is turning on. hass.states.async_set(entry.entity_id, STATE_ON) await hass.async_block_till_done() - assert len(calls) == 2 - assert {calls[0].data["some"], calls[1].data["some"]} == { + assert len(service_calls) == 2 + assert {service_calls[0].data["some"], service_calls[1].data["some"]} == { f"turn_on - device - {entry.entity_id} - off - on - None", f"turn_on_or_off - device - {entry.entity_id} - off - on - None", } @@ -282,8 +275,8 @@ async def test_if_fires_on_state_change( # Fake that the entity is turning off. hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 4 - assert {calls[2].data["some"], calls[3].data["some"]} == { + assert len(service_calls) == 4 + assert {service_calls[2].data["some"], service_calls[3].data["some"]} == { f"turn_off - device - {entry.entity_id} - on - off - None", f"turn_on_or_off - device - {entry.entity_id} - on - off - None", } @@ -293,7 +286,7 @@ async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -342,9 +335,9 @@ async def test_if_fires_on_state_change_legacy( # Fake that the entity is turning on. hass.states.async_set(entry.entity_id, STATE_ON) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_on - device - {entry.entity_id} - off - on - None" ) @@ -353,7 +346,7 @@ async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -399,16 +392,16 @@ async def test_if_fires_on_state_change_with_for( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await hass.async_block_till_done() assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - on - off - 0:00:05" ) diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py index 4f4d21adcba..ec8406bfe7b 100644 --- a/tests/components/humidifier/test_device_condition.py +++ b/tests/components/humidifier/test_device_condition.py @@ -17,11 +17,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) +from tests.common import MockConfigEntry, async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -29,12 +25,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.mark.parametrize( ("set_state", "features_reg", "features_state", "expected_condition_types"), [ @@ -153,7 +143,7 @@ async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -238,42 +228,42 @@ async def test_if_state( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_on event - test_event1" hass.states.async_set(entry.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "is_off event - test_event2" hass.states.async_set(entry.entity_id, STATE_ON, {ATTR_MODE: const.MODE_AWAY}) hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert len(calls) == 3 - assert calls[2].data["some"] == "is_mode - event - test_event3" + assert len(service_calls) == 3 + assert service_calls[2].data["some"] == "is_mode - event - test_event3" hass.states.async_set(entry.entity_id, STATE_ON, {ATTR_MODE: const.MODE_HOME}) # Should not fire hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert len(calls) == 3 + assert len(service_calls) == 3 async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -316,15 +306,15 @@ async def test_if_state_legacy( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_ON, {ATTR_MODE: const.MODE_AWAY}) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_mode - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_mode - event - test_event1" @pytest.mark.parametrize( diff --git a/tests/components/humidifier/test_device_trigger.py b/tests/components/humidifier/test_device_trigger.py index 83202e16675..3bb1f8c2551 100644 --- a/tests/components/humidifier/test_device_trigger.py +++ b/tests/components/humidifier/test_device_trigger.py @@ -30,7 +30,6 @@ from tests.common import ( MockConfigEntry, async_fire_time_changed, async_get_device_automations, - async_mock_service, ) @@ -39,12 +38,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -166,7 +159,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -356,8 +349,8 @@ async def test_if_fires_on_state_change( {const.ATTR_HUMIDITY: 7, const.ATTR_CURRENT_HUMIDITY: 35}, ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "target_humidity_changed_below" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "target_humidity_changed_below" # Fake that the current humidity is changing hass.states.async_set( @@ -366,8 +359,8 @@ async def test_if_fires_on_state_change( {const.ATTR_HUMIDITY: 7, const.ATTR_CURRENT_HUMIDITY: 18}, ) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "current_humidity_changed_below" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "current_humidity_changed_below" # Fake that the humidity target is changing hass.states.async_set( @@ -376,8 +369,8 @@ async def test_if_fires_on_state_change( {const.ATTR_HUMIDITY: 37, const.ATTR_CURRENT_HUMIDITY: 18}, ) await hass.async_block_till_done() - assert len(calls) == 3 - assert calls[2].data["some"] == "target_humidity_changed_above" + assert len(service_calls) == 3 + assert service_calls[2].data["some"] == "target_humidity_changed_above" # Fake that the current humidity is changing hass.states.async_set( @@ -386,14 +379,14 @@ async def test_if_fires_on_state_change( {const.ATTR_HUMIDITY: 37, const.ATTR_CURRENT_HUMIDITY: 41}, ) await hass.async_block_till_done() - assert len(calls) == 4 - assert calls[3].data["some"] == "current_humidity_changed_above" + assert len(service_calls) == 4 + assert service_calls[3].data["some"] == "current_humidity_changed_above" # Wait 6 minutes async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=6)) await hass.async_block_till_done() - assert len(calls) == 6 - assert {calls[4].data["some"], calls[5].data["some"]} == { + assert len(service_calls) == 6 + assert {service_calls[4].data["some"], service_calls[5].data["some"]} == { "current_humidity_changed_above_for", "target_humidity_changed_above_for", } @@ -405,8 +398,8 @@ async def test_if_fires_on_state_change( {const.ATTR_HUMIDITY: 37, const.ATTR_CURRENT_HUMIDITY: 41}, ) await hass.async_block_till_done() - assert len(calls) == 8 - assert {calls[6].data["some"], calls[7].data["some"]} == { + assert len(service_calls) == 8 + assert {service_calls[6].data["some"], service_calls[7].data["some"]} == { "turn_off device - humidifier.test_5678 - on - off - None", "turn_on_or_off device - humidifier.test_5678 - on - off - None", } @@ -418,8 +411,8 @@ async def test_if_fires_on_state_change( {const.ATTR_HUMIDITY: 37, const.ATTR_CURRENT_HUMIDITY: 41}, ) await hass.async_block_till_done() - assert len(calls) == 10 - assert {calls[8].data["some"], calls[9].data["some"]} == { + assert len(service_calls) == 10 + assert {service_calls[8].data["some"], service_calls[9].data["some"]} == { "turn_on device - humidifier.test_5678 - off - on - None", "turn_on_or_off device - humidifier.test_5678 - off - on - None", } @@ -429,7 +422,7 @@ async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -479,12 +472,14 @@ async def test_if_fires_on_state_change_legacy( # Fake that the humidity is changing hass.states.async_set(entry.entity_id, STATE_ON, {const.ATTR_HUMIDITY: 7}) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "target_humidity_changed_below" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "target_humidity_changed_below" async def test_invalid_config( - hass: HomeAssistant, entity_registry: er.EntityRegistry, calls: list[ServiceCall] + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" entry = entity_registry.async_get_or_create(DOMAIN, "test", "5678") @@ -528,7 +523,7 @@ async def test_invalid_config( hass.states.async_set(entry.entity_id, STATE_ON, {const.ATTR_HUMIDITY: 7}) await hass.async_block_till_done() # Should not trigger for invalid config - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_get_trigger_capabilities_on(hass: HomeAssistant) -> None: diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 8848ce19621..c2ac7087cf0 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -14,7 +14,7 @@ from homeassistant.components.light import ( LightEntityFeature, ) from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component @@ -32,12 +32,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_actions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -471,7 +465,6 @@ async def test_action( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off actions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -636,7 +629,6 @@ async def test_action_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off actions.""" config_entry = MockConfigEntry(domain="test", data={}) diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 11dea49ea60..94e12ffbfa5 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -22,7 +22,6 @@ from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, setup_test_component_platform, ) @@ -32,12 +31,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_conditions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -186,7 +179,7 @@ async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -252,20 +245,20 @@ async def test_if_state( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_on event - test_event1" hass.states.async_set(entry.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "is_off event - test_event2" @pytest.mark.usefixtures("enable_custom_integrations") @@ -273,7 +266,7 @@ async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -318,20 +311,20 @@ async def test_if_state_legacy( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_on event - test_event1" async def test_if_fires_on_for_condition( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_light_entities: list[MockLight], ) -> None: """Test for firing if condition is on with delay.""" @@ -385,26 +378,26 @@ async def test_if_fires_on_for_condition( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 # Time travel 10 secs into the future freezer.move_to(point2) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 # Time travel 20 secs into the future freezer.move_to(point3) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_off event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_off event - test_event1" diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index ab3babd1b64..4e8414edabc 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -20,7 +20,6 @@ from tests.common import ( async_fire_time_changed, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, ) DATA_TEMPLATE_ATTRIBUTES = ( @@ -37,12 +36,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -189,7 +182,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -258,20 +251,20 @@ async def test_if_fires_on_state_change( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 2 - assert {calls[0].data["some"], calls[1].data["some"]} == { + assert len(service_calls) == 2 + assert {service_calls[0].data["some"], service_calls[1].data["some"]} == { f"turn_off device - {entry.entity_id} - on - off - None", f"turn_on_or_off device - {entry.entity_id} - on - off - None", } hass.states.async_set(entry.entity_id, STATE_ON) await hass.async_block_till_done() - assert len(calls) == 4 - assert {calls[2].data["some"], calls[3].data["some"]} == { + assert len(service_calls) == 4 + assert {service_calls[2].data["some"], service_calls[3].data["some"]} == { f"turn_on device - {entry.entity_id} - off - on - None", f"turn_on_or_off device - {entry.entity_id} - off - on - None", } @@ -282,7 +275,7 @@ async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -321,13 +314,14 @@ async def test_if_fires_on_state_change_legacy( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] == f"turn_on device - {entry.entity_id} - on - off - None" + service_calls[0].data["some"] + == f"turn_on device - {entry.entity_id} - on - off - None" ) @@ -336,7 +330,7 @@ async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -376,16 +370,16 @@ async def test_if_fires_on_state_change_with_for( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await hass.async_block_till_done() assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - on - off - 0:00:05" ) diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py index 97afe9fb759..74910e1909f 100644 --- a/tests/components/lock/test_device_condition.py +++ b/tests/components/lock/test_device_condition.py @@ -21,11 +21,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) +from tests.common import MockConfigEntry, async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -33,12 +29,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_conditions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -139,7 +129,7 @@ async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -291,52 +281,52 @@ async def test_if_state( hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_locked - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_locked - event - test_event1" hass.states.async_set(entry.entity_id, STATE_UNLOCKED) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_unlocked - event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "is_unlocked - event - test_event2" hass.states.async_set(entry.entity_id, STATE_UNLOCKING) hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert len(calls) == 3 - assert calls[2].data["some"] == "is_unlocking - event - test_event3" + assert len(service_calls) == 3 + assert service_calls[2].data["some"] == "is_unlocking - event - test_event3" hass.states.async_set(entry.entity_id, STATE_LOCKING) hass.bus.async_fire("test_event4") await hass.async_block_till_done() - assert len(calls) == 4 - assert calls[3].data["some"] == "is_locking - event - test_event4" + assert len(service_calls) == 4 + assert service_calls[3].data["some"] == "is_locking - event - test_event4" hass.states.async_set(entry.entity_id, STATE_JAMMED) hass.bus.async_fire("test_event5") await hass.async_block_till_done() - assert len(calls) == 5 - assert calls[4].data["some"] == "is_jammed - event - test_event5" + assert len(service_calls) == 5 + assert service_calls[4].data["some"] == "is_jammed - event - test_event5" hass.states.async_set(entry.entity_id, STATE_OPENING) hass.bus.async_fire("test_event6") await hass.async_block_till_done() - assert len(calls) == 6 - assert calls[5].data["some"] == "is_opening - event - test_event6" + assert len(service_calls) == 6 + assert service_calls[5].data["some"] == "is_opening - event - test_event6" hass.states.async_set(entry.entity_id, STATE_OPEN) hass.bus.async_fire("test_event7") await hass.async_block_till_done() - assert len(calls) == 7 - assert calls[6].data["some"] == "is_open - event - test_event7" + assert len(service_calls) == 7 + assert service_calls[6].data["some"] == "is_open - event - test_event7" async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -380,5 +370,5 @@ async def test_if_state_legacy( hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_locked - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_locked - event - test_event1" diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index 3cbfbb1a04c..f64334fa29b 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -29,7 +29,6 @@ from tests.common import ( async_fire_time_changed, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, ) @@ -38,12 +37,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -212,7 +205,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -296,27 +289,27 @@ async def test_if_fires_on_state_change( # Fake that the entity is turning on. hass.states.async_set(entry.entity_id, STATE_LOCKED) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"locked - device - {entry.entity_id} - unlocked - locked - None" ) # Fake that the entity is turning off. hass.states.async_set(entry.entity_id, STATE_UNLOCKED) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[1].data["some"] + service_calls[1].data["some"] == f"unlocked - device - {entry.entity_id} - locked - unlocked - None" ) # Fake that the entity is opens. hass.states.async_set(entry.entity_id, STATE_OPEN) await hass.async_block_till_done() - assert len(calls) == 3 + assert len(service_calls) == 3 assert ( - calls[2].data["some"] + service_calls[2].data["some"] == f"open - device - {entry.entity_id} - unlocked - open - None" ) @@ -325,7 +318,7 @@ async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -371,9 +364,9 @@ async def test_if_fires_on_state_change_legacy( # Fake that the entity is turning on. hass.states.async_set(entry.entity_id, STATE_LOCKED) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"locked - device - {entry.entity_id} - unlocked - locked - None" ) @@ -382,7 +375,7 @@ async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -516,64 +509,64 @@ async def test_if_fires_on_state_change_with_for( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_LOCKED) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await hass.async_block_till_done() assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - unlocked - locked - 0:00:05" ) hass.states.async_set(entry.entity_id, STATE_UNLOCKING) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=16)) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 await hass.async_block_till_done() assert ( - calls[1].data["some"] + service_calls[1].data["some"] == f"turn_on device - {entry.entity_id} - locked - unlocking - 0:00:05" ) hass.states.async_set(entry.entity_id, STATE_JAMMED) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=21)) await hass.async_block_till_done() - assert len(calls) == 3 + assert len(service_calls) == 3 await hass.async_block_till_done() assert ( - calls[2].data["some"] + service_calls[2].data["some"] == f"turn_off device - {entry.entity_id} - unlocking - jammed - 0:00:05" ) hass.states.async_set(entry.entity_id, STATE_LOCKING) await hass.async_block_till_done() - assert len(calls) == 3 + assert len(service_calls) == 3 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=27)) await hass.async_block_till_done() - assert len(calls) == 4 + assert len(service_calls) == 4 await hass.async_block_till_done() assert ( - calls[3].data["some"] + service_calls[3].data["some"] == f"turn_on device - {entry.entity_id} - jammed - locking - 0:00:05" ) hass.states.async_set(entry.entity_id, STATE_OPENING) await hass.async_block_till_done() - assert len(calls) == 4 + assert len(service_calls) == 4 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=27)) await hass.async_block_till_done() - assert len(calls) == 5 + assert len(service_calls) == 5 await hass.async_block_till_done() assert ( - calls[4].data["some"] + service_calls[4].data["some"] == f"turn_on device - {entry.entity_id} - locking - opening - 0:00:05" ) From 3bbf8df6d640bbc748b942996196c124e2585215 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 26 Jun 2024 23:57:41 +0200 Subject: [PATCH 0566/2411] Cleanup mqtt platform tests part 4 (init) (#120574) --- tests/components/mqtt/test_init.py | 91 ++++++++---------------------- 1 file changed, 23 insertions(+), 68 deletions(-) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 231379601c6..bcadf4a6506 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -5,7 +5,6 @@ from copy import deepcopy from datetime import datetime, timedelta from functools import partial import json -import logging import socket import ssl import time @@ -16,15 +15,11 @@ import certifi from freezegun.api import FrozenDateTimeFactory import paho.mqtt.client as paho_mqtt import pytest -from typing_extensions import Generator import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info -from homeassistant.components.mqtt.client import ( - _LOGGER as CLIENT_LOGGER, - RECONNECT_INTERVAL_SECONDS, -) +from homeassistant.components.mqtt.client import RECONNECT_INTERVAL_SECONDS from homeassistant.components.mqtt.models import ( MessageCallbackType, MqttCommandTemplateException, @@ -100,15 +95,6 @@ def mock_storage(hass_storage: dict[str, Any]) -> None: """Autouse hass_storage for the TestCase tests.""" -@pytest.fixture -def client_debug_log() -> Generator[None]: - """Set the mqtt client log level to DEBUG.""" - logger = logging.getLogger("mqtt_client_tests_debug") - logger.setLevel(logging.DEBUG) - with patch.object(CLIENT_LOGGER, "parent", logger): - yield - - def help_assert_message( msg: ReceiveMessage, topic: str | None = None, @@ -130,8 +116,7 @@ def help_assert_message( async def test_mqtt_connects_on_home_assistant_mqtt_setup( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient ) -> None: """Test if client is connected after mqtt init on bootstrap.""" mqtt_client_mock = setup_with_birth_msg_client_mock @@ -150,9 +135,7 @@ async def test_mqtt_does_not_disconnect_on_home_assistant_stop( assert mqtt_client_mock.disconnect.call_count == 0 -async def test_mqtt_await_ack_at_disconnect( - hass: HomeAssistant, -) -> None: +async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: """Test if ACK is awaited correctly when disconnecting.""" class FakeInfo: @@ -208,8 +191,7 @@ async def test_mqtt_await_ack_at_disconnect( @pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) async def test_publish( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient ) -> None: """Test the publish function.""" publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish @@ -340,9 +322,7 @@ async def test_command_template_value(hass: HomeAssistant) -> None: ], ) async def test_command_template_variables( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - config: ConfigType, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, config: ConfigType ) -> None: """Test the rendering of entity variables.""" topic = "test/select" @@ -888,7 +868,7 @@ def test_entity_device_info_schema() -> None: {"identifiers": [], "connections": [], "name": "Beer"} ) - # not an valid URL + # not a valid URL with pytest.raises(vol.Invalid): MQTT_ENTITY_DEVICE_INFO_SCHEMA( { @@ -1049,10 +1029,9 @@ async def test_subscribe_topic( unsub() +@pytest.mark.usefixtures("mqtt_mock_entry") async def test_subscribe_topic_not_initialize( - hass: HomeAssistant, - record_calls: MessageCallbackType, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, record_calls: MessageCallbackType ) -> None: """Test the subscription of a topic when MQTT was not initialized.""" with pytest.raises( @@ -1084,7 +1063,6 @@ async def test_subscribe_mqtt_config_entry_disabled( async def test_subscribe_and_resubscribe( hass: HomeAssistant, - client_debug_log: None, mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, recorded_calls: list[ReceiveMessage], @@ -1892,10 +1870,10 @@ async def test_subscribed_at_highest_qos( assert help_all_subscribe_calls(mqtt_client_mock) == [("test/state", 2)] +@pytest.mark.usefixtures("mqtt_client_mock") async def test_reload_entry_with_restored_subscriptions( hass: HomeAssistant, mock_debouncer: asyncio.Event, - mqtt_client_mock: MqttMockPahoClient, record_calls: MessageCallbackType, recorded_calls: list[ReceiveMessage], ) -> None: @@ -1995,7 +1973,6 @@ async def test_logs_error_if_no_connect_broker( @pytest.mark.parametrize("return_code", [4, 5]) async def test_triggers_reauth_flow_if_auth_fails( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, setup_with_birth_msg_client_mock: MqttMockPahoClient, return_code: int, ) -> None: @@ -2132,9 +2109,7 @@ async def test_handle_message_callback( ], ) async def test_setup_manual_mqtt_with_platform_key( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test set up a manual MQTT item with a platform key.""" assert await mqtt_mock_entry() @@ -2146,9 +2121,7 @@ async def test_setup_manual_mqtt_with_platform_key( @pytest.mark.parametrize("hass_config", [{mqtt.DOMAIN: {"light": {"name": "test"}}}]) async def test_setup_manual_mqtt_with_invalid_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture ) -> None: """Test set up a manual MQTT item with an invalid config.""" assert await mqtt_mock_entry() @@ -2182,9 +2155,7 @@ async def test_setup_manual_mqtt_with_invalid_config( ], ) async def test_setup_mqtt_client_protocol( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - protocol: int, + mqtt_mock_entry: MqttMockHAClientGenerator, protocol: int ) -> None: """Test MQTT client protocol setup.""" with patch( @@ -2383,8 +2354,7 @@ async def test_custom_birth_message( [ENTRY_DEFAULT_BIRTH_MESSAGE], ) async def test_default_birth_message( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient ) -> None: """Test sending birth message.""" mqtt_client_mock = setup_with_birth_msg_client_mock @@ -2470,10 +2440,7 @@ async def test_delayed_birth_message( [ENTRY_DEFAULT_BIRTH_MESSAGE], ) async def test_subscription_done_when_birth_message_is_sent( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, ) -> None: """Test sending birth message until initial subscription has been completed.""" mqtt_client_mock = setup_with_birth_msg_client_mock @@ -2517,7 +2484,6 @@ async def test_custom_will_message( async def test_default_will_message( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test will message.""" @@ -2647,11 +2613,9 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( assert len(mqtt_client_mock.unsubscribe.mock_calls[1][1][0]) == 2 +@pytest.mark.usefixtures("mqtt_client_mock") async def test_default_entry_setting_are_applied( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mqtt_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Test if the MQTT component loads when config entry data not has all default settings.""" data = ( @@ -2704,11 +2668,9 @@ async def test_message_callback_exception_gets_logged( @pytest.mark.no_fail_on_log_exception +@pytest.mark.usefixtures("mock_debouncer", "setup_with_birth_msg_client_mock") async def test_message_partial_callback_exception_gets_logged( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_debouncer: asyncio.Event ) -> None: """Test exception raised by message handler.""" @@ -3730,9 +3692,7 @@ async def test_setup_manual_items_with_unique_ids( ], ) async def test_link_config_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test manual and dynamically setup entities are linked to the config entry.""" # set up manual item @@ -3818,9 +3778,7 @@ async def test_link_config_entry( ], ) async def test_reload_config_entry( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test manual entities reloaded and set up correctly.""" await mqtt_mock_entry() @@ -3966,8 +3924,7 @@ async def test_reload_config_entry( ], ) async def test_reload_with_invalid_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test reloading yaml config fails.""" await mqtt_mock_entry() @@ -4007,8 +3964,7 @@ async def test_reload_with_invalid_config( ], ) async def test_reload_with_empty_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test reloading yaml config fails.""" await mqtt_mock_entry() @@ -4043,8 +3999,7 @@ async def test_reload_with_empty_config( ], ) async def test_reload_with_new_platform_config( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test reloading yaml with new platform config.""" await mqtt_mock_entry() @@ -4389,6 +4344,6 @@ async def test_loop_write_failure( "valid_subscribe_topic", ], ) -async def test_mqtt_integration_level_imports(hass: HomeAssistant, attr: str) -> None: +async def test_mqtt_integration_level_imports(attr: str) -> None: """Test mqtt integration level public published imports are available.""" assert hasattr(mqtt, attr) From 40384b9acdfac3eab46ebe19e902b89de3a6d755 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 27 Jun 2024 19:37:43 +0200 Subject: [PATCH 0567/2411] Split mqtt client tests (#120636) --- tests/components/mqtt/test_client.py | 1980 ++++++++++++++++++++++++++ tests/components/mqtt/test_init.py | 1962 +------------------------ 2 files changed, 1983 insertions(+), 1959 deletions(-) create mode 100644 tests/components/mqtt/test_client.py diff --git a/tests/components/mqtt/test_client.py b/tests/components/mqtt/test_client.py new file mode 100644 index 00000000000..49b590383d1 --- /dev/null +++ b/tests/components/mqtt/test_client.py @@ -0,0 +1,1980 @@ +"""The tests for the MQTT client.""" + +import asyncio +from datetime import datetime, timedelta +import socket +import ssl +from typing import Any +from unittest.mock import MagicMock, Mock, call, patch + +import certifi +import paho.mqtt.client as paho_mqtt +import pytest + +from homeassistant.components import mqtt +from homeassistant.components.mqtt.client import RECONNECT_INTERVAL_SECONDS +from homeassistant.components.mqtt.models import MessageCallbackType, ReceiveMessage +from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState +from homeassistant.const import ( + CONF_PROTOCOL, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + UnitOfTemperature, +) +from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.dt import utcnow + +from .conftest import ENTRY_DEFAULT_BIRTH_MESSAGE +from .test_common import help_all_subscribe_calls + +from tests.common import ( + MockConfigEntry, + async_fire_mqtt_message, + async_fire_time_changed, +) +from tests.typing import MqttMockHAClient, MqttMockHAClientGenerator, MqttMockPahoClient + + +@pytest.fixture(autouse=True) +def mock_storage(hass_storage: dict[str, Any]) -> None: + """Autouse hass_storage for the TestCase tests.""" + + +def help_assert_message( + msg: ReceiveMessage, + topic: str | None = None, + payload: str | None = None, + qos: int | None = None, + retain: bool | None = None, +) -> bool: + """Return True if all of the given attributes match with the message.""" + match: bool = True + if topic is not None: + match &= msg.topic == topic + if payload is not None: + match &= msg.payload == payload + if qos is not None: + match &= msg.qos == qos + if retain is not None: + match &= msg.retain == retain + return match + + +async def test_mqtt_connects_on_home_assistant_mqtt_setup( + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient +) -> None: + """Test if client is connected after mqtt init on bootstrap.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + +async def test_mqtt_does_not_disconnect_on_home_assistant_stop( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test if client is not disconnected on HA stop.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + hass.bus.fire(EVENT_HOMEASSISTANT_STOP) + await mock_debouncer.wait() + assert mqtt_client_mock.disconnect.call_count == 0 + + +async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: + """Test if ACK is awaited correctly when disconnecting.""" + + class FakeInfo: + """Returns a simulated client publish response.""" + + mid = 100 + rc = 0 + + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mqtt_client = mock_client.return_value + mqtt_client.connect = MagicMock( + return_value=0, + side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( + mqtt_client.on_connect, mqtt_client, None, 0, 0, 0 + ), + ) + mqtt_client.publish = MagicMock(return_value=FakeInfo()) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={ + "certificate": "auto", + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_DISCOVERY: False, + }, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + + 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, + ) + await hass.async_block_till_done(wait_background_tasks=True) + + +@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) +async def test_publish( + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient +) -> None: + """Test the publish function.""" + publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish + await mqtt.async_publish(hass, "test-topic", "test-payload") + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic", + "test-payload", + 0, + False, + ) + publish_mock.reset_mock() + + await mqtt.async_publish(hass, "test-topic", "test-payload", 2, True) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic", + "test-payload", + 2, + True, + ) + publish_mock.reset_mock() + + mqtt.publish(hass, "test-topic2", "test-payload2") + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic2", + "test-payload2", + 0, + False, + ) + publish_mock.reset_mock() + + mqtt.publish(hass, "test-topic2", "test-payload2", 2, True) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic2", + "test-payload2", + 2, + True, + ) + publish_mock.reset_mock() + + # test binary pass-through + mqtt.publish( + hass, + "test-topic3", + b"\xde\xad\xbe\xef", + 0, + False, + ) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic3", + b"\xde\xad\xbe\xef", + 0, + False, + ) + publish_mock.reset_mock() + + # test null payload + mqtt.publish( + hass, + "test-topic3", + None, + 0, + False, + ) + await hass.async_block_till_done() + assert publish_mock.called + assert publish_mock.call_args[0] == ( + "test-topic3", + None, + 0, + False, + ) + + publish_mock.reset_mock() + + +async def test_convert_outgoing_payload(hass: HomeAssistant) -> None: + """Test the converting of outgoing MQTT payloads without template.""" + command_template = mqtt.MqttCommandTemplate(None, hass=hass) + assert command_template.async_render(b"\xde\xad\xbe\xef") == b"\xde\xad\xbe\xef" + assert ( + command_template.async_render("b'\\xde\\xad\\xbe\\xef'") + == "b'\\xde\\xad\\xbe\\xef'" + ) + assert command_template.async_render(1234) == 1234 + assert command_template.async_render(1234.56) == 1234.56 + assert command_template.async_render(None) is None + + +async def test_all_subscriptions_run_when_decode_fails( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test all other subscriptions still run when decode fails for one.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic", record_calls, encoding="ascii") + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + async_fire_mqtt_message(hass, "test-topic", UnitOfTemperature.CELSIUS) + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + + +async def test_subscribe_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of a topic.""" + await mqtt_mock_entry() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + + unsub() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + + # Cannot unsubscribe twice + with pytest.raises(HomeAssistantError): + unsub() + + +@pytest.mark.usefixtures("mqtt_mock_entry") +async def test_subscribe_topic_not_initialize( + hass: HomeAssistant, record_calls: MessageCallbackType +) -> None: + """Test the subscription of a topic when MQTT was not initialized.""" + with pytest.raises( + HomeAssistantError, match=r".*make sure MQTT is set up correctly" + ): + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + +async def test_subscribe_mqtt_config_entry_disabled( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, record_calls: MessageCallbackType +) -> None: + """Test the subscription of a topic when MQTT config entry is disabled.""" + mqtt_mock.connected = True + + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + assert mqtt_config_entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) + assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED + + await hass.config_entries.async_set_disabled_by( + mqtt_config_entry.entry_id, ConfigEntryDisabler.USER + ) + mqtt_mock.connected = False + + with pytest.raises(HomeAssistantError, match=r".*MQTT is not enabled"): + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + +async def test_subscribe_and_resubscribe( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test resubscribing within the debounce time.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + with ( + patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.4), + patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.4), + ): + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + # This unsub will be un-done with the following subscribe + # unsubscribe should not be called at the broker + unsub() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + await mock_debouncer.wait() + mock_debouncer.clear() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + # assert unsubscribe was not called + mqtt_client_mock.unsubscribe.assert_not_called() + + mock_debouncer.clear() + unsub() + + await mock_debouncer.wait() + mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) + + +async def test_subscribe_topic_non_async( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of a topic using the non-async function.""" + await mqtt_mock_entry() + await mock_debouncer.wait() + mock_debouncer.clear() + unsub = await hass.async_add_executor_job( + mqtt.subscribe, hass, "test-topic", record_calls + ) + await mock_debouncer.wait() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + + mock_debouncer.clear() + await hass.async_add_executor_job(unsub) + await mock_debouncer.wait() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + assert len(recorded_calls) == 1 + + +async def test_subscribe_bad_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of a topic.""" + await mqtt_mock_entry() + with pytest.raises(HomeAssistantError): + await mqtt.async_subscribe(hass, 55, record_calls) # type: ignore[arg-type] + + +async def test_subscribe_topic_not_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test if subscribed topic is not a match.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + async_fire_mqtt_message(hass, "another-test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) + + async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic/bier/on" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_level_wildcard_no_subtree_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) + + async_fire_mqtt_message(hass, "test-topic/bier", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "test-topic-123", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_subtree_wildcard_subtree_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic/bier/on" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_subtree_wildcard_root_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "test-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_subtree_wildcard_no_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "another-test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "hi/test-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/test-topic/here-iam", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "hi/test-topic/here-iam" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/here-iam/test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_level_wildcard_and_wildcard_no_match( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "hi/another-test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 0 + + +async def test_subscribe_topic_sys_root( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of $ root topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "$test-topic/subtree/on", record_calls) + + async_fire_mqtt_message(hass, "$test-topic/subtree/on", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "$test-topic/subtree/on" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_sys_root_and_wildcard_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of $ root and wildcard topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "$test-topic/#", record_calls) + + async_fire_mqtt_message(hass, "$test-topic/some-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "$test-topic/some-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription of $ root and wildcard subtree topics.""" + await mqtt_mock_entry() + await mqtt.async_subscribe(hass, "$test-topic/subtree/#", record_calls) + + async_fire_mqtt_message(hass, "$test-topic/subtree/some-topic", "test-payload") + + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == "$test-topic/subtree/some-topic" + assert recorded_calls[0].payload == "test-payload" + + +async def test_subscribe_special_characters( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test the subscription to topics with special characters.""" + await mqtt_mock_entry() + topic = "/test-topic/$(.)[^]{-}" + payload = "p4y.l[]a|> ?" + + await mqtt.async_subscribe(hass, topic, record_calls) + + async_fire_mqtt_message(hass, topic, payload) + await hass.async_block_till_done() + assert len(recorded_calls) == 1 + assert recorded_calls[0].topic == topic + assert recorded_calls[0].payload == payload + + +async def test_subscribe_same_topic( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test subscribing to same topic twice and simulate retained messages. + + When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again + for it to resend any retained messages. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_a, qos=0) + # Simulate a non retained message after the first subscription + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + await mock_debouncer.wait() + assert len(calls_a) == 1 + mqtt_client_mock.subscribe.assert_called() + calls_a = [] + mqtt_client_mock.reset_mock() + + await hass.async_block_till_done() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_b, qos=1) + # Simulate an other non retained message after the second subscription + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + await mock_debouncer.wait() + # Both subscriptions should receive updates + assert len(calls_a) == 1 + assert len(calls_b) == 1 + mqtt_client_mock.subscribe.assert_called() + + +async def test_replaying_payload_same_topic( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test replaying retained messages. + + When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again + for it to resend any retained messages for new subscriptions. + Retained messages must only be replayed for new subscriptions, except + when the MQTT client is reconnecting. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_a) + await mock_debouncer.wait() + async_fire_mqtt_message( + hass, "test/state", "online", qos=0, retain=True + ) # Simulate a (retained) message played back + assert len(calls_a) == 1 + mqtt_client_mock.subscribe.assert_called() + calls_a = [] + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", _callback_b) + await mock_debouncer.wait() + + # Simulate edge case where non retained message was received + # after subscription at HA but before the debouncer delay was passed. + # The message without retain flag directly after a subscription should + # be processed by both subscriptions. + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + + # Simulate a (retained) message played back on new subscriptions + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + + # The current subscription only received the message without retain flag + assert len(calls_a) == 1 + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) + # The retained message playback should only be processed by the new subscription. + # The existing subscription already got the latest update, hence the existing + # subscription should not receive the replayed (retained) message. + # Messages without retain flag are received on both subscriptions. + assert len(calls_b) == 2 + assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) + assert help_assert_message(calls_b[1], "test/state", "online", qos=0, retain=True) + mqtt_client_mock.subscribe.assert_called() + + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + + # Simulate new message played back on new subscriptions + # After connecting the retain flag will not be set, even if the + # payload published was retained, we cannot see that + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) + assert len(calls_a) == 1 + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) + assert len(calls_b) == 1 + assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) + + # Now simulate the broker was disconnected shortly + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + # Simulate a (retained) message played back after reconnecting + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + # Both subscriptions now should replay the retained message + assert len(calls_a) == 1 + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) + assert len(calls_b) == 1 + assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=True) + + +async def test_replaying_payload_after_resubscribing( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test replaying and filtering retained messages after resubscribing. + + When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again + for it to resend any retained messages for new subscriptions. + Retained messages must only be replayed for new subscriptions, except + when the MQTT client is reconnection. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + + # Simulate a (retained) message played back + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) + calls_a.clear() + + # Test we get updates + async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=False) + assert help_assert_message(calls_a[0], "test/state", "offline", qos=0, retain=False) + calls_a.clear() + + # Test we filter new retained updates + async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=True) + await hass.async_block_till_done() + assert len(calls_a) == 0 + + # Unsubscribe an resubscribe again + mock_debouncer.clear() + unsub() + unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + + # Simulate we can receive a (retained) played back message again + async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) + assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) + + +async def test_replaying_payload_wildcard_topic( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test replaying retained messages. + + When we have multiple subscriptions to the same wildcard topic, + SUBSCRIBE must be sent to the broker again + for it to resend any retained messages for new subscriptions. + Retained messages should only be replayed for new subscriptions, except + when the MQTT client is reconnection. + """ + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/#", _callback_a) + await mock_debouncer.wait() + # Simulate (retained) messages being played back on new subscriptions + async_fire_mqtt_message(hass, "test/state1", "new_value_1", qos=0, retain=True) + async_fire_mqtt_message(hass, "test/state2", "new_value_2", qos=0, retain=True) + assert len(calls_a) == 2 + mqtt_client_mock.subscribe.assert_called() + calls_a = [] + mqtt_client_mock.reset_mock() + + # resubscribe to the wild card topic again + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/#", _callback_b) + await mock_debouncer.wait() + # Simulate (retained) messages being played back on new subscriptions + async_fire_mqtt_message(hass, "test/state1", "initial_value_1", qos=0, retain=True) + async_fire_mqtt_message(hass, "test/state2", "initial_value_2", qos=0, retain=True) + # The retained messages playback should only be processed for the new subscriptions + assert len(calls_a) == 0 + assert len(calls_b) == 2 + mqtt_client_mock.subscribe.assert_called() + + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + + # Simulate new messages being received + async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=False) + async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=False) + assert len(calls_a) == 2 + assert len(calls_b) == 2 + + # Now simulate the broker was disconnected shortly + calls_a = [] + calls_b = [] + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + await mock_debouncer.wait() + + mqtt_client_mock.subscribe.assert_called() + # Simulate the (retained) messages are played back after reconnecting + # for all subscriptions + async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=True) + async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=True) + # Both subscriptions should replay + assert len(calls_a) == 2 + assert len(calls_b) == 2 + + +async def test_not_calling_unsubscribe_with_active_subscribers( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test not calling unsubscribe() when other subscribers are active.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) + await mqtt.async_subscribe(hass, "test/state", record_calls, 1) + await mock_debouncer.wait() + assert mqtt_client_mock.subscribe.called + + mock_debouncer.clear() + unsub() + await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + assert not mqtt_client_mock.unsubscribe.called + assert not mock_debouncer.is_set() + + +async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + mqtt_mock_entry: MqttMockHAClientGenerator, + record_calls: MessageCallbackType, +) -> None: + """Test not calling subscribe() when it is unsubscribed. + + Make sure subscriptions are cleared if unsubscribed before + the subscribe cool down period has ended. + """ + mqtt_mock = await mqtt_mock_entry() + mqtt_client_mock = mqtt_mock._mqttc + await mock_debouncer.wait() + + mock_debouncer.clear() + mqtt_client_mock.subscribe.reset_mock() + unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) + unsub() + await mock_debouncer.wait() + # The debouncer executes without an pending subscribes + assert not mqtt_client_mock.subscribe.called + + +async def test_unsubscribe_race( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test not calling unsubscribe() when other subscribers are active.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + calls_a: list[ReceiveMessage] = [] + calls_b: list[ReceiveMessage] = [] + + @callback + def _callback_a(msg: ReceiveMessage) -> None: + calls_a.append(msg) + + @callback + def _callback_b(msg: ReceiveMessage) -> None: + calls_b.append(msg) + + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) + unsub() + await mqtt.async_subscribe(hass, "test/state", _callback_b) + await mock_debouncer.wait() + + async_fire_mqtt_message(hass, "test/state", "online") + assert not calls_a + assert calls_b + + # We allow either calls [subscribe, unsubscribe, subscribe], [subscribe, subscribe] or + # when both subscriptions were combined [subscribe] + expected_calls_1 = [ + call.subscribe([("test/state", 0)]), + call.unsubscribe("test/state"), + call.subscribe([("test/state", 0)]), + ] + expected_calls_2 = [ + call.subscribe([("test/state", 0)]), + call.subscribe([("test/state", 0)]), + ] + expected_calls_3 = [ + call.subscribe([("test/state", 0)]), + ] + assert mqtt_client_mock.mock_calls in ( + expected_calls_1, + expected_calls_2, + expected_calls_3, + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], +) +async def test_restore_subscriptions_on_reconnect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test subscriptions are restored on reconnect.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + await mock_debouncer.wait() + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + await mock_debouncer.wait() + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], +) +async def test_restore_all_active_subscriptions_on_reconnect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test active subscriptions are restored correctly on reconnect.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) + # cooldown + await mock_debouncer.wait() + + # the subscription with the highest QoS should survive + expected = [ + call([("test/state", 2)]), + ] + assert mqtt_client_mock.subscribe.mock_calls == expected + + unsub() + assert mqtt_client_mock.unsubscribe.call_count == 0 + + mqtt_client_mock.on_disconnect(None, None, 0) + + mock_debouncer.clear() + mqtt_client_mock.on_connect(None, None, None, 0) + # wait for cooldown + await mock_debouncer.wait() + + expected.append(call([("test/state", 1)])) + for expected_call in expected: + assert mqtt_client_mock.subscribe.hass_call(expected_call) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], +) +async def test_subscribed_at_highest_qos( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test the highest qos as assigned when subscribing to the same topic.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) + await hass.async_block_till_done() + # cooldown + await mock_debouncer.wait() + assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) + await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) + # cooldown + await mock_debouncer.wait() + + # the subscription with the highest QoS should survive + assert help_all_subscribe_calls(mqtt_client_mock) == [("test/state", 2)] + + +async def test_initial_setup_logs_error( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """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.side_effect = MagicMock(return_value=1) + try: + assert await hass.config_entries.async_setup(entry.entry_id) + except HomeAssistantError: + assert True + assert "Failed to connect to MQTT server:" in caplog.text + + +async def test_logs_error_if_no_connect_broker( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test for setup failure if connection to broker is missing.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + # test with rc = 3 -> broker unavailable + mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_connect(Mock(), None, None, 3) + await hass.async_block_till_done() + assert ( + "Unable to connect to the MQTT broker: Connection Refused: broker unavailable." + in caplog.text + ) + + +@pytest.mark.parametrize("return_code", [4, 5]) +async def test_triggers_reauth_flow_if_auth_fails( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + return_code: int, +) -> None: + """Test re-auth is triggered if authentication is failing.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + # test with rc = 4 -> CONNACK_REFUSED_NOT_AUTHORIZED and 5 -> CONNACK_REFUSED_BAD_USERNAME_PASSWORD + mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_connect(Mock(), None, None, return_code) + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["source"] == "reauth" + + +@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) +async def test_handle_mqtt_on_callback( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test receiving an ACK callback before waiting for it.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + with patch.object(mqtt_client_mock, "get_mid", return_value=100): + # Simulate an ACK for mid == 100, this will call mqtt_mock._async_get_mid_future(mid) + mqtt_client_mock.on_publish(mqtt_client_mock, None, 100) + await hass.async_block_till_done() + # Make sure the ACK has been received + await hass.async_block_till_done() + # Now call publish without call back, this will call _async_async_wait_for_mid(msg_info.mid) + await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") + # Since the mid event was already set, we should not see any timeout warning in the log + await hass.async_block_till_done() + assert "No ACK from MQTT server" not in caplog.text + + +async def test_handle_mqtt_on_callback_after_timeout( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test receiving an ACK after a timeout.""" + mqtt_mock = await mqtt_mock_entry() + # Simulate the mid future getting a timeout + mqtt_mock()._async_get_mid_future(101).set_exception(asyncio.TimeoutError) + # Simulate an ACK for mid == 101, being received after the timeout + mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) + await hass.async_block_till_done() + assert "No ACK from MQTT server" not in caplog.text + assert "InvalidStateError" not in caplog.text + + +async def test_publish_error( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """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( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mock_client().connect = lambda *args: 1 + mock_client().publish().rc = 1 + assert await hass.config_entries.async_setup(entry.entry_id) + with pytest.raises(HomeAssistantError): + await mqtt.async_publish( + hass, "some-topic", b"test-payload", qos=0, retain=False, encoding=None + ) + assert "Failed to connect to MQTT server: Out of memory." in caplog.text + + +async def test_subscribe_error( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test publish error.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.reset_mock() + # simulate client is not connected error before subscribing + mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None) + await mqtt.async_subscribe(hass, "some-topic", record_calls) + while mqtt_client_mock.subscribe.call_count == 0: + await hass.async_block_till_done() + await hass.async_block_till_done() + assert ( + "Error talking to MQTT: The client is not currently connected." in caplog.text + ) + + +async def test_handle_message_callback( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test for handling an incoming message callback.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + callbacks = [] + + @callback + def _callback(args) -> None: + callbacks.append(args) + + msg = ReceiveMessage( + "some-topic", b"test-payload", 1, False, "some-topic", datetime.now() + ) + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "some-topic", _callback) + await mock_debouncer.wait() + mqtt_client_mock.reset_mock() + mqtt_client_mock.on_message(None, None, msg) + + assert len(callbacks) == 1 + assert callbacks[0].topic == "some-topic" + assert callbacks[0].qos == 1 + assert callbacks[0].payload == "test-payload" + + +@pytest.mark.parametrize( + ("mqtt_config_entry_data", "protocol"), + [ + ( + { + mqtt.CONF_BROKER: "mock-broker", + CONF_PROTOCOL: "3.1", + }, + 3, + ), + ( + { + mqtt.CONF_BROKER: "mock-broker", + CONF_PROTOCOL: "3.1.1", + }, + 4, + ), + ( + { + mqtt.CONF_BROKER: "mock-broker", + CONF_PROTOCOL: "5", + }, + 5, + ), + ], +) +async def test_setup_mqtt_client_protocol( + mqtt_mock_entry: MqttMockHAClientGenerator, protocol: int +) -> None: + """Test MQTT client protocol setup.""" + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + await mqtt_mock_entry() + + # check if protocol setup was correctly + assert mock_client.call_args[1]["protocol"] == protocol + + +@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) +async def test_handle_mqtt_timeout_on_callback( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_debouncer: asyncio.Event +) -> None: + """Test publish without receiving an ACK callback.""" + mid = 0 + + class FakeInfo: + """Returns a simulated client publish response.""" + + mid = 102 + rc = 0 + + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + + def _mock_ack(topic: str, qos: int = 0) -> tuple[int, int]: + # Handle ACK for subscribe normally + nonlocal mid + mid += 1 + mock_client.on_subscribe(0, 0, mid) + return (0, mid) + + # We want to simulate the publish behaviour MQTT client + mock_client = mock_client.return_value + mock_client.publish.return_value = FakeInfo() + # Mock we get a mid and rc=0 + mock_client.subscribe.side_effect = _mock_ack + mock_client.unsubscribe.side_effect = _mock_ack + mock_client.connect = MagicMock( + return_value=0, + side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( + mock_client.on_connect, mock_client, None, 0, 0, 0 + ), + ) + + entry = MockConfigEntry( + domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} + ) + entry.add_to_hass(hass) + + # Set up the integration + mock_debouncer.clear() + assert await hass.config_entries.async_setup(entry.entry_id) + + # 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() + # 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 + # Ensure we stop lingering background tasks + await hass.config_entries.async_unload(entry.entry_id) + # Assert we did not have any completed subscribes, + # because the debouncer subscribe job failed to receive an ACK, + # and the time auto caused the debouncer job to fail. + assert not mock_debouncer.is_set() + + +async def test_setup_raises_config_entry_not_ready_if_no_connect_broker( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """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( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mock_client().connect = MagicMock(side_effect=OSError("Connection error")) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert "Failed to connect to MQTT server due to exception:" in caplog.text + + +@pytest.mark.parametrize( + ("mqtt_config_entry_data", "insecure_param"), + [ + ({"broker": "test-broker", "certificate": "auto"}, "not set"), + ( + {"broker": "test-broker", "certificate": "auto", "tls_insecure": False}, + False, + ), + ({"broker": "test-broker", "certificate": "auto", "tls_insecure": True}, True), + ], +) +async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + insecure_param: bool | str, +) -> None: + """Test setup uses bundled certs when certificate is set to auto and insecure.""" + calls = [] + insecure_check = {"insecure": "not set"} + + def mock_tls_set( + certificate, certfile=None, keyfile=None, tls_version=None + ) -> None: + calls.append((certificate, certfile, keyfile, tls_version)) + + def mock_tls_insecure_set(insecure_param) -> None: + insecure_check["insecure"] = insecure_param + + with patch( + "homeassistant.components.mqtt.async_client.AsyncMQTTClient" + ) as mock_client: + mock_client().tls_set = mock_tls_set + mock_client().tls_insecure_set = mock_tls_insecure_set + await mqtt_mock_entry() + await hass.async_block_till_done() + + assert calls + + expected_certificate = certifi.where() + assert calls[0][0] == expected_certificate + + # test if insecure is set + assert insecure_check["insecure"] == insecure_param + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_CERTIFICATE: "auto", + } + ], +) +async def test_tls_version( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test setup defaults for tls.""" + await mqtt_mock_entry() + await hass.async_block_till_done() + assert ( + mqtt_client_mock.tls_set.mock_calls[0][2]["tls_version"] + == ssl.PROTOCOL_TLS_CLIENT + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "birth", + mqtt.ATTR_PAYLOAD: "birth", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, + }, + } + ], +) +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) +async def test_custom_birth_message( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test sending birth message.""" + + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + mock_debouncer.clear() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + # discovery cooldown + await mock_debouncer.wait() + # Wait for publish call to finish + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_default_birth_message( + hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient +) -> None: + """Test sending birth message.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], +) +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) +async def test_no_birth_message( + hass: HomeAssistant, + record_calls: MessageCallbackType, + mock_debouncer: asyncio.Event, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test disabling birth message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + mock_debouncer.clear() + assert await hass.config_entries.async_setup(entry.entry_id) + # Wait for discovery cooldown + await mock_debouncer.wait() + # Ensure any publishing could have been processed + await hass.async_block_till_done(wait_background_tasks=True) + mqtt_client_mock.publish.assert_not_called() + + mqtt_client_mock.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "homeassistant/some-topic", record_calls) + # Wait for discovery cooldown + await mock_debouncer.wait() + mqtt_client_mock.subscribe.assert_called() + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.2) +async def test_delayed_birth_message( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test sending birth message does not happen until Home Assistant starts.""" + hass.set_state(CoreState.starting) + await hass.async_block_till_done() + birth = asyncio.Event() + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + + @callback + def wait_birth(msg: ReceiveMessage) -> None: + """Handle birth message.""" + birth.set() + + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + with pytest.raises(TimeoutError): + await asyncio.wait_for(birth.wait(), 0.05) + assert not mqtt_client_mock.publish.called + assert not birth.is_set() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await birth.wait() + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_subscription_done_when_birth_message_is_sent( + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test sending birth message until initial subscription has been completed.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) + assert ("homeassistant/+/+/config", 0) in subscribe_calls + assert ("homeassistant/+/+/+/config", 0) in subscribe_calls + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_WILL_MESSAGE: { + mqtt.ATTR_TOPIC: "death", + mqtt.ATTR_PAYLOAD: "death", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, + }, + } + ], +) +async def test_custom_will_message( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test will message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mqtt_client_mock.will_set.assert_called_with( + topic="death", payload="death", qos=0, retain=False + ) + + +async def test_default_will_message( + setup_with_birth_msg_client_mock: MqttMockPahoClient, +) -> None: + """Test will message.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.will_set.assert_called_with( + topic="homeassistant/status", payload="offline", qos=0, retain=False + ) + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], +) +async def test_no_will_message( + hass: HomeAssistant, + mqtt_config_entry_data: dict[str, Any], + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test will message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mqtt_client_mock.will_set.assert_not_called() + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE | {mqtt.CONF_DISCOVERY: False}], +) +async def test_mqtt_subscribes_topics_on_connect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test subscription to topic on connect.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "topic/test", record_calls) + await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2) + await mqtt.async_subscribe(hass, "still/pending", record_calls) + await mqtt.async_subscribe(hass, "still/pending", record_calls, 1) + await mock_debouncer.wait() + + mqtt_client_mock.on_disconnect(Mock(), None, 0) + + mqtt_client_mock.reset_mock() + + mock_debouncer.clear() + mqtt_client_mock.on_connect(Mock(), None, 0, 0) + await mock_debouncer.wait() + + subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) + assert ("topic/test", 0) in subscribe_calls + assert ("home/sensor", 2) in subscribe_calls + assert ("still/pending", 1) in subscribe_calls + + +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ENTRY_DEFAULT_BIRTH_MESSAGE], +) +async def test_mqtt_subscribes_in_single_call( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test bundled client subscription to topic.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + mqtt_client_mock.subscribe.reset_mock() + mock_debouncer.clear() + await mqtt.async_subscribe(hass, "topic/test", record_calls) + await mqtt.async_subscribe(hass, "home/sensor", record_calls) + # Make sure the debouncer finishes + await mock_debouncer.wait() + + assert mqtt_client_mock.subscribe.call_count == 1 + # Assert we have a single subscription call with both subscriptions + assert mqtt_client_mock.subscribe.mock_calls[0][1][0] in [ + [("topic/test", 0), ("home/sensor", 0)], + [("home/sensor", 0), ("topic/test", 0)], + ] + + +@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) +@patch("homeassistant.components.mqtt.client.MAX_SUBSCRIBES_PER_CALL", 2) +@patch("homeassistant.components.mqtt.client.MAX_UNSUBSCRIBES_PER_CALL", 2) +async def test_mqtt_subscribes_and_unsubscribes_in_chunks( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + record_calls: MessageCallbackType, +) -> None: + """Test chunked client subscriptions.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + + mqtt_client_mock.subscribe.reset_mock() + unsub_tasks: list[CALLBACK_TYPE] = [] + mock_debouncer.clear() + unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test1", record_calls)) + unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor1", record_calls)) + unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test2", record_calls)) + unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor2", record_calls)) + # Make sure the debouncer finishes + await mock_debouncer.wait() + + assert mqtt_client_mock.subscribe.call_count == 2 + # Assert we have a 2 subscription calls with both 2 subscriptions + assert len(mqtt_client_mock.subscribe.mock_calls[0][1][0]) == 2 + assert len(mqtt_client_mock.subscribe.mock_calls[1][1][0]) == 2 + + # Unsubscribe all topics + mock_debouncer.clear() + for task in unsub_tasks: + task() + # Make sure the debouncer finishes + await mock_debouncer.wait() + + assert mqtt_client_mock.unsubscribe.call_count == 2 + # Assert we have a 2 unsubscribe calls with both 2 topic + assert len(mqtt_client_mock.unsubscribe.mock_calls[0][1][0]) == 2 + assert len(mqtt_client_mock.unsubscribe.mock_calls[1][1][0]) == 2 + + +async def test_auto_reconnect( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test reconnection is automatically done.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + mqtt_client_mock.reconnect.reset_mock() + + mqtt_client_mock.disconnect() + mqtt_client_mock.on_disconnect(None, None, 0) + await hass.async_block_till_done() + + mqtt_client_mock.reconnect.side_effect = OSError("foo") + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + assert len(mqtt_client_mock.reconnect.mock_calls) == 1 + assert "Error re-connecting to MQTT server due to exception: foo" in caplog.text + + mqtt_client_mock.reconnect.side_effect = None + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + assert len(mqtt_client_mock.reconnect.mock_calls) == 2 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + + mqtt_client_mock.disconnect() + mqtt_client_mock.on_disconnect(None, None, 0) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) + ) + await hass.async_block_till_done() + # Should not reconnect after stop + assert len(mqtt_client_mock.reconnect.mock_calls) == 2 + + +async def test_server_sock_connect_and_disconnect( + hass: HomeAssistant, + mock_debouncer: asyncio.Event, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) + await hass.async_block_till_done() + + server.close() # mock the server closing the connection on us + + mock_debouncer.clear() + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + await mock_debouncer.wait() + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + mqtt_client_mock.on_socket_unregister_write(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_close(mqtt_client_mock, None, client) + mqtt_client_mock.on_disconnect(mqtt_client_mock, None, client) + await hass.async_block_till_done() + mock_debouncer.clear() + unsub() + await hass.async_block_till_done() + assert not mock_debouncer.is_set() + + # Should have failed + assert len(recorded_calls) == 0 + + +async def test_server_sock_buffer_size( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling the socket buffer size fails.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + with patch.object(client, "setsockopt", side_effect=OSError("foo")): + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) + await hass.async_block_till_done() + assert "Unable to increase the socket buffer size" in caplog.text + + +async def test_server_sock_buffer_size_with_websocket( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling the socket buffer size fails.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + + class FakeWebsocket(paho_mqtt.WebsocketWrapper): + def _do_handshake(self, *args, **kwargs): + pass + + wrapped_socket = FakeWebsocket(client, "127.0.01", 1, False, "/", None) + + with patch.object(client, "setsockopt", side_effect=OSError("foo")): + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, wrapped_socket) + mqtt_client_mock.on_socket_register_write( + mqtt_client_mock, None, wrapped_socket + ) + await hass.async_block_till_done() + assert "Unable to increase the socket buffer size" in caplog.text + + +async def test_client_sock_failure_after_connect( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + recorded_calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_writer(mqtt_client_mock, None, client) + await hass.async_block_till_done() + + mqtt_client_mock.loop_write.side_effect = OSError("foo") + client.close() # close the client socket out from under the client + + assert mqtt_client_mock.connect.call_count == 1 + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + + unsub() + # Should have failed + assert len(recorded_calls) == 0 + + +async def test_loop_write_failure( + hass: HomeAssistant, + setup_with_birth_msg_client_mock: MqttMockPahoClient, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling the socket connected and disconnected.""" + mqtt_client_mock = setup_with_birth_msg_client_mock + assert mqtt_client_mock.connect.call_count == 1 + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) + mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) + mqtt_client_mock.loop_write.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + mqtt_client_mock.loop_read.return_value = paho_mqtt.MQTT_ERR_CONN_LOST + + # Fill up the outgoing buffer to ensure that loop_write + # and loop_read are called that next time control is + # returned to the event loop + try: + for _ in range(1000): + server.send(b"long" * 100) + except BlockingIOError: + pass + + server.close() + # Once for the reader callback + await hass.async_block_till_done() + # Another for the writer callback + await hass.async_block_till_done() + # Final for the disconnect callback + await hass.async_block_till_done() + + assert "Disconnected from MQTT server test-broker:1883" in caplog.text diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index bcadf4a6506..403f7974878 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,25 +1,20 @@ -"""The tests for the MQTT component.""" +"""The tests for the MQTT component setup and helpers.""" import asyncio from copy import deepcopy from datetime import datetime, timedelta from functools import partial import json -import socket -import ssl import time from typing import Any, TypedDict -from unittest.mock import ANY, MagicMock, Mock, call, mock_open, patch +from unittest.mock import ANY, MagicMock, Mock, mock_open, patch -import certifi from freezegun.api import FrozenDateTimeFactory -import paho.mqtt.client as paho_mqtt import pytest import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info -from homeassistant.components.mqtt.client import RECONNECT_INTERVAL_SECONDS from homeassistant.components.mqtt.models import ( MessageCallbackType, MqttCommandTemplateException, @@ -31,16 +26,12 @@ from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import ( ATTR_ASSUMED_STATE, - CONF_PROTOCOL, - EVENT_HOMEASSISTANT_STARTED, - EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, - UnitOfTemperature, ) import homeassistant.core as ha -from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr, entity_registry as er, template from homeassistant.helpers.entity import Entity @@ -50,9 +41,6 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from homeassistant.util.dt import utcnow -from .conftest import ENTRY_DEFAULT_BIRTH_MESSAGE -from .test_common import help_all_subscribe_calls - from tests.common import ( MockConfigEntry, MockEntity, @@ -63,7 +51,6 @@ from tests.common import ( ) from tests.components.sensor.common import MockSensor from tests.typing import ( - MqttMockHAClient, MqttMockHAClientGenerator, MqttMockPahoClient, WebSocketGenerator, @@ -95,205 +82,6 @@ def mock_storage(hass_storage: dict[str, Any]) -> None: """Autouse hass_storage for the TestCase tests.""" -def help_assert_message( - msg: ReceiveMessage, - topic: str | None = None, - payload: str | None = None, - qos: int | None = None, - retain: bool | None = None, -) -> bool: - """Return True if all of the given attributes match with the message.""" - match: bool = True - if topic is not None: - match &= msg.topic == topic - if payload is not None: - match &= msg.payload == payload - if qos is not None: - match &= msg.qos == qos - if retain is not None: - match &= msg.retain == retain - return match - - -async def test_mqtt_connects_on_home_assistant_mqtt_setup( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient -) -> None: - """Test if client is connected after mqtt init on bootstrap.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - -async def test_mqtt_does_not_disconnect_on_home_assistant_stop( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test if client is not disconnected on HA stop.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - await mock_debouncer.wait() - assert mqtt_client_mock.disconnect.call_count == 0 - - -async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: - """Test if ACK is awaited correctly when disconnecting.""" - - class FakeInfo: - """Returns a simulated client publish response.""" - - mid = 100 - rc = 0 - - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mqtt_client = mock_client.return_value - mqtt_client.connect = MagicMock( - return_value=0, - side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( - mqtt_client.on_connect, mqtt_client, None, 0, 0, 0 - ), - ) - mqtt_client.publish = MagicMock(return_value=FakeInfo()) - entry = MockConfigEntry( - domain=mqtt.DOMAIN, - data={ - "certificate": "auto", - mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_DISCOVERY: False, - }, - ) - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - - 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, - ) - await hass.async_block_till_done(wait_background_tasks=True) - - -@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) -async def test_publish( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient -) -> None: - """Test the publish function.""" - publish_mock: MagicMock = setup_with_birth_msg_client_mock.publish - await mqtt.async_publish(hass, "test-topic", "test-payload") - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic", - "test-payload", - 0, - False, - ) - publish_mock.reset_mock() - - await mqtt.async_publish(hass, "test-topic", "test-payload", 2, True) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic", - "test-payload", - 2, - True, - ) - publish_mock.reset_mock() - - mqtt.publish(hass, "test-topic2", "test-payload2") - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic2", - "test-payload2", - 0, - False, - ) - publish_mock.reset_mock() - - mqtt.publish(hass, "test-topic2", "test-payload2", 2, True) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic2", - "test-payload2", - 2, - True, - ) - publish_mock.reset_mock() - - # test binary pass-through - mqtt.publish( - hass, - "test-topic3", - b"\xde\xad\xbe\xef", - 0, - False, - ) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic3", - b"\xde\xad\xbe\xef", - 0, - False, - ) - publish_mock.reset_mock() - - # test null payload - mqtt.publish( - hass, - "test-topic3", - None, - 0, - False, - ) - await hass.async_block_till_done() - assert publish_mock.called - assert publish_mock.call_args[0] == ( - "test-topic3", - None, - 0, - False, - ) - - publish_mock.reset_mock() - - -async def test_convert_outgoing_payload(hass: HomeAssistant) -> None: - """Test the converting of outgoing MQTT payloads without template.""" - command_template = mqtt.MqttCommandTemplate(None, hass=hass) - assert command_template.async_render(b"\xde\xad\xbe\xef") == b"\xde\xad\xbe\xef" - - assert ( - command_template.async_render("b'\\xde\\xad\\xbe\\xef'") - == "b'\\xde\\xad\\xbe\\xef'" - ) - - assert command_template.async_render(1234) == 1234 - - assert command_template.async_render(1234.56) == 1234.56 - - assert command_template.async_render(None) is None - - async def test_command_template_value(hass: HomeAssistant) -> None: """Test the rendering of MQTT command template.""" @@ -983,893 +771,6 @@ async def test_receiving_message_with_non_utf8_topic_gets_logged( ) -async def test_all_subscriptions_run_when_decode_fails( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test all other subscriptions still run when decode fails for one.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic", record_calls, encoding="ascii") - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - async_fire_mqtt_message(hass, "test-topic", UnitOfTemperature.CELSIUS) - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - - -async def test_subscribe_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of a topic.""" - await mqtt_mock_entry() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - - unsub() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - - # Cannot unsubscribe twice - with pytest.raises(HomeAssistantError): - unsub() - - -@pytest.mark.usefixtures("mqtt_mock_entry") -async def test_subscribe_topic_not_initialize( - hass: HomeAssistant, record_calls: MessageCallbackType -) -> None: - """Test the subscription of a topic when MQTT was not initialized.""" - with pytest.raises( - HomeAssistantError, match=r".*make sure MQTT is set up correctly" - ): - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - -async def test_subscribe_mqtt_config_entry_disabled( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, record_calls: MessageCallbackType -) -> None: - """Test the subscription of a topic when MQTT config entry is disabled.""" - mqtt_mock.connected = True - - mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - assert mqtt_config_entry.state is ConfigEntryState.LOADED - - assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) - assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED - - await hass.config_entries.async_set_disabled_by( - mqtt_config_entry.entry_id, ConfigEntryDisabler.USER - ) - mqtt_mock.connected = False - - with pytest.raises(HomeAssistantError, match=r".*MQTT is not enabled"): - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - -async def test_subscribe_and_resubscribe( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test resubscribing within the debounce time.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - with ( - patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.4), - patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.4), - ): - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - # This unsub will be un-done with the following subscribe - # unsubscribe should not be called at the broker - unsub() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - await mock_debouncer.wait() - mock_debouncer.clear() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - # assert unsubscribe was not called - mqtt_client_mock.unsubscribe.assert_not_called() - - mock_debouncer.clear() - unsub() - - await mock_debouncer.wait() - mqtt_client_mock.unsubscribe.assert_called_once_with(["test-topic"]) - - -async def test_subscribe_topic_non_async( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of a topic using the non-async function.""" - await mqtt_mock_entry() - await mock_debouncer.wait() - mock_debouncer.clear() - unsub = await hass.async_add_executor_job( - mqtt.subscribe, hass, "test-topic", record_calls - ) - await mock_debouncer.wait() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - - mock_debouncer.clear() - await hass.async_add_executor_job(unsub) - await mock_debouncer.wait() - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - assert len(recorded_calls) == 1 - - -async def test_subscribe_bad_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of a topic.""" - await mqtt_mock_entry() - with pytest.raises(HomeAssistantError): - await mqtt.async_subscribe(hass, 55, record_calls) # type: ignore[arg-type] - - -async def test_subscribe_topic_not_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test if subscribed topic is not a match.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic", record_calls) - - async_fire_mqtt_message(hass, "another-test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) - - async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic/bier/on" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_level_wildcard_no_subtree_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) - - async_fire_mqtt_message(hass, "test-topic/bier", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "test-topic-123", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_subtree_wildcard_subtree_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic/bier/on" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_subtree_wildcard_root_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "test-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_subtree_wildcard_no_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "another-test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "hi/test-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/test-topic/here-iam", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "hi/test-topic/here-iam" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/here-iam/test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_level_wildcard_and_wildcard_no_match( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "hi/another-test-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 0 - - -async def test_subscribe_topic_sys_root( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of $ root topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "$test-topic/subtree/on", record_calls) - - async_fire_mqtt_message(hass, "$test-topic/subtree/on", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "$test-topic/subtree/on" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_sys_root_and_wildcard_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of $ root and wildcard topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "$test-topic/#", record_calls) - - async_fire_mqtt_message(hass, "$test-topic/some-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "$test-topic/some-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription of $ root and wildcard subtree topics.""" - await mqtt_mock_entry() - await mqtt.async_subscribe(hass, "$test-topic/subtree/#", record_calls) - - async_fire_mqtt_message(hass, "$test-topic/subtree/some-topic", "test-payload") - - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == "$test-topic/subtree/some-topic" - assert recorded_calls[0].payload == "test-payload" - - -async def test_subscribe_special_characters( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test the subscription to topics with special characters.""" - await mqtt_mock_entry() - topic = "/test-topic/$(.)[^]{-}" - payload = "p4y.l[]a|> ?" - - await mqtt.async_subscribe(hass, topic, record_calls) - - async_fire_mqtt_message(hass, topic, payload) - await hass.async_block_till_done() - assert len(recorded_calls) == 1 - assert recorded_calls[0].topic == topic - assert recorded_calls[0].payload == payload - - -async def test_subscribe_same_topic( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test subscribing to same topic twice and simulate retained messages. - - When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again - for it to resend any retained messages. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_a, qos=0) - # Simulate a non retained message after the first subscription - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - await mock_debouncer.wait() - assert len(calls_a) == 1 - mqtt_client_mock.subscribe.assert_called() - calls_a = [] - mqtt_client_mock.reset_mock() - - await hass.async_block_till_done() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_b, qos=1) - # Simulate an other non retained message after the second subscription - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - await mock_debouncer.wait() - # Both subscriptions should receive updates - assert len(calls_a) == 1 - assert len(calls_b) == 1 - mqtt_client_mock.subscribe.assert_called() - - -async def test_replaying_payload_same_topic( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test replaying retained messages. - - When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again - for it to resend any retained messages for new subscriptions. - Retained messages must only be replayed for new subscriptions, except - when the MQTT client is reconnecting. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_a) - await mock_debouncer.wait() - async_fire_mqtt_message( - hass, "test/state", "online", qos=0, retain=True - ) # Simulate a (retained) message played back - assert len(calls_a) == 1 - mqtt_client_mock.subscribe.assert_called() - calls_a = [] - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", _callback_b) - await mock_debouncer.wait() - - # Simulate edge case where non retained message was received - # after subscription at HA but before the debouncer delay was passed. - # The message without retain flag directly after a subscription should - # be processed by both subscriptions. - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - - # Simulate a (retained) message played back on new subscriptions - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - - # The current subscription only received the message without retain flag - assert len(calls_a) == 1 - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) - # The retained message playback should only be processed by the new subscription. - # The existing subscription already got the latest update, hence the existing - # subscription should not receive the replayed (retained) message. - # Messages without retain flag are received on both subscriptions. - assert len(calls_b) == 2 - assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) - assert help_assert_message(calls_b[1], "test/state", "online", qos=0, retain=True) - mqtt_client_mock.subscribe.assert_called() - - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - - # Simulate new message played back on new subscriptions - # After connecting the retain flag will not be set, even if the - # payload published was retained, we cannot see that - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=False) - assert len(calls_a) == 1 - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=False) - assert len(calls_b) == 1 - assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=False) - - # Now simulate the broker was disconnected shortly - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - # Simulate a (retained) message played back after reconnecting - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - # Both subscriptions now should replay the retained message - assert len(calls_a) == 1 - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) - assert len(calls_b) == 1 - assert help_assert_message(calls_b[0], "test/state", "online", qos=0, retain=True) - - -async def test_replaying_payload_after_resubscribing( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test replaying and filtering retained messages after resubscribing. - - When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again - for it to resend any retained messages for new subscriptions. - Retained messages must only be replayed for new subscriptions, except - when the MQTT client is reconnection. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - - # Simulate a (retained) message played back - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) - calls_a.clear() - - # Test we get updates - async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=False) - assert help_assert_message(calls_a[0], "test/state", "offline", qos=0, retain=False) - calls_a.clear() - - # Test we filter new retained updates - async_fire_mqtt_message(hass, "test/state", "offline", qos=0, retain=True) - await hass.async_block_till_done() - assert len(calls_a) == 0 - - # Unsubscribe an resubscribe again - mock_debouncer.clear() - unsub() - unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - - # Simulate we can receive a (retained) played back message again - async_fire_mqtt_message(hass, "test/state", "online", qos=0, retain=True) - assert help_assert_message(calls_a[0], "test/state", "online", qos=0, retain=True) - - -async def test_replaying_payload_wildcard_topic( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test replaying retained messages. - - When we have multiple subscriptions to the same wildcard topic, - SUBSCRIBE must be sent to the broker again - for it to resend any retained messages for new subscriptions. - Retained messages should only be replayed for new subscriptions, except - when the MQTT client is reconnection. - """ - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/#", _callback_a) - await mock_debouncer.wait() - # Simulate (retained) messages being played back on new subscriptions - async_fire_mqtt_message(hass, "test/state1", "new_value_1", qos=0, retain=True) - async_fire_mqtt_message(hass, "test/state2", "new_value_2", qos=0, retain=True) - assert len(calls_a) == 2 - mqtt_client_mock.subscribe.assert_called() - calls_a = [] - mqtt_client_mock.reset_mock() - - # resubscribe to the wild card topic again - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/#", _callback_b) - await mock_debouncer.wait() - # Simulate (retained) messages being played back on new subscriptions - async_fire_mqtt_message(hass, "test/state1", "initial_value_1", qos=0, retain=True) - async_fire_mqtt_message(hass, "test/state2", "initial_value_2", qos=0, retain=True) - # The retained messages playback should only be processed for the new subscriptions - assert len(calls_a) == 0 - assert len(calls_b) == 2 - mqtt_client_mock.subscribe.assert_called() - - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - - # Simulate new messages being received - async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=False) - async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=False) - assert len(calls_a) == 2 - assert len(calls_b) == 2 - - # Now simulate the broker was disconnected shortly - calls_a = [] - calls_b = [] - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - await mock_debouncer.wait() - - mqtt_client_mock.subscribe.assert_called() - # Simulate the (retained) messages are played back after reconnecting - # for all subscriptions - async_fire_mqtt_message(hass, "test/state1", "update_value_1", qos=0, retain=True) - async_fire_mqtt_message(hass, "test/state2", "update_value_2", qos=0, retain=True) - # Both subscriptions should replay - assert len(calls_a) == 2 - assert len(calls_b) == 2 - - -async def test_not_calling_unsubscribe_with_active_subscribers( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test not calling unsubscribe() when other subscribers are active.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, 2) - await mqtt.async_subscribe(hass, "test/state", record_calls, 1) - await mock_debouncer.wait() - assert mqtt_client_mock.subscribe.called - - mock_debouncer.clear() - unsub() - await hass.async_block_till_done() - await hass.async_block_till_done(wait_background_tasks=True) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - assert not mqtt_client_mock.unsubscribe.called - assert not mock_debouncer.is_set() - - -async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - mqtt_mock_entry: MqttMockHAClientGenerator, - record_calls: MessageCallbackType, -) -> None: - """Test not calling subscribe() when it is unsubscribed. - - Make sure subscriptions are cleared if unsubscribed before - the subscribe cool down period has ended. - """ - mqtt_mock = await mqtt_mock_entry() - mqtt_client_mock = mqtt_mock._mqttc - await mock_debouncer.wait() - - mock_debouncer.clear() - mqtt_client_mock.subscribe.reset_mock() - unsub = await mqtt.async_subscribe(hass, "test/state", record_calls) - unsub() - await mock_debouncer.wait() - # The debouncer executes without an pending subscribes - assert not mqtt_client_mock.subscribe.called - - -async def test_unsubscribe_race( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test not calling unsubscribe() when other subscribers are active.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - calls_a: list[ReceiveMessage] = [] - calls_b: list[ReceiveMessage] = [] - - @callback - def _callback_a(msg: ReceiveMessage) -> None: - calls_a.append(msg) - - @callback - def _callback_b(msg: ReceiveMessage) -> None: - calls_b.append(msg) - - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", _callback_a) - unsub() - await mqtt.async_subscribe(hass, "test/state", _callback_b) - await mock_debouncer.wait() - - async_fire_mqtt_message(hass, "test/state", "online") - assert not calls_a - assert calls_b - - # We allow either calls [subscribe, unsubscribe, subscribe], [subscribe, subscribe] or - # when both subscriptions were combined [subscribe] - expected_calls_1 = [ - call.subscribe([("test/state", 0)]), - call.unsubscribe("test/state"), - call.subscribe([("test/state", 0)]), - ] - expected_calls_2 = [ - call.subscribe([("test/state", 0)]), - call.subscribe([("test/state", 0)]), - ] - expected_calls_3 = [ - call.subscribe([("test/state", 0)]), - ] - assert mqtt_client_mock.mock_calls in ( - expected_calls_1, - expected_calls_2, - expected_calls_3, - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], -) -async def test_restore_subscriptions_on_reconnect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test subscriptions are restored on reconnect.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown - await mock_debouncer.wait() - assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) - - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - await mock_debouncer.wait() - assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], -) -async def test_restore_all_active_subscriptions_on_reconnect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test active subscriptions are restored correctly on reconnect.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) - # cooldown - await mock_debouncer.wait() - - # the subscription with the highest QoS should survive - expected = [ - call([("test/state", 2)]), - ] - assert mqtt_client_mock.subscribe.mock_calls == expected - - unsub() - assert mqtt_client_mock.unsubscribe.call_count == 0 - - mqtt_client_mock.on_disconnect(None, None, 0) - - mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) - # wait for cooldown - await mock_debouncer.wait() - - expected.append(call([("test/state", 1)])) - for expected_call in expected: - assert mqtt_client_mock.subscribe.hass_call(expected_call) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], -) -async def test_subscribed_at_highest_qos( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test the highest qos as assigned when subscribing to the same topic.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=0) - await hass.async_block_till_done() - # cooldown - await mock_debouncer.wait() - assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=1) - await mqtt.async_subscribe(hass, "test/state", record_calls, qos=2) - # cooldown - await mock_debouncer.wait() - - # the subscription with the highest QoS should survive - assert help_all_subscribe_calls(mqtt_client_mock) == [("test/state", 2)] - - @pytest.mark.usefixtures("mqtt_client_mock") async def test_reload_entry_with_restored_subscriptions( hass: HomeAssistant, @@ -1937,163 +838,6 @@ async def test_reload_entry_with_restored_subscriptions( assert recorded_calls[1].payload == "wild-card-payload3" -async def test_initial_setup_logs_error( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """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.side_effect = MagicMock(return_value=1) - try: - assert await hass.config_entries.async_setup(entry.entry_id) - except HomeAssistantError: - assert True - assert "Failed to connect to MQTT server:" in caplog.text - - -async def test_logs_error_if_no_connect_broker( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test for setup failure if connection to broker is missing.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - # test with rc = 3 -> broker unavailable - mqtt_client_mock.on_disconnect(Mock(), None, 0) - mqtt_client_mock.on_connect(Mock(), None, None, 3) - await hass.async_block_till_done() - assert ( - "Unable to connect to the MQTT broker: Connection Refused: broker unavailable." - in caplog.text - ) - - -@pytest.mark.parametrize("return_code", [4, 5]) -async def test_triggers_reauth_flow_if_auth_fails( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - return_code: int, -) -> None: - """Test re-auth is triggered if authentication is failing.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - # test with rc = 4 -> CONNACK_REFUSED_NOT_AUTHORIZED and 5 -> CONNACK_REFUSED_BAD_USERNAME_PASSWORD - mqtt_client_mock.on_disconnect(Mock(), None, 0) - mqtt_client_mock.on_connect(Mock(), None, None, return_code) - await hass.async_block_till_done() - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - assert flows[0]["context"]["source"] == "reauth" - - -@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) -async def test_handle_mqtt_on_callback( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test receiving an ACK callback before waiting for it.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - with patch.object(mqtt_client_mock, "get_mid", return_value=100): - # Simulate an ACK for mid == 100, this will call mqtt_mock._async_get_mid_future(mid) - mqtt_client_mock.on_publish(mqtt_client_mock, None, 100) - await hass.async_block_till_done() - # Make sure the ACK has been received - await hass.async_block_till_done() - # Now call publish without call back, this will call _async_async_wait_for_mid(msg_info.mid) - await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") - # Since the mid event was already set, we should not see any timeout warning in the log - await hass.async_block_till_done() - assert "No ACK from MQTT server" not in caplog.text - - -async def test_handle_mqtt_on_callback_after_timeout( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - mqtt_mock_entry: MqttMockHAClientGenerator, - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test receiving an ACK after a timeout.""" - mqtt_mock = await mqtt_mock_entry() - # Simulate the mid future getting a timeout - mqtt_mock()._async_get_mid_future(101).set_exception(asyncio.TimeoutError) - # Simulate an ACK for mid == 101, being received after the timeout - mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) - await hass.async_block_till_done() - assert "No ACK from MQTT server" not in caplog.text - assert "InvalidStateError" not in caplog.text - - -async def test_publish_error( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """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( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mock_client().connect = lambda *args: 1 - mock_client().publish().rc = 1 - assert await hass.config_entries.async_setup(entry.entry_id) - with pytest.raises(HomeAssistantError): - await mqtt.async_publish( - hass, "some-topic", b"test-payload", qos=0, retain=False, encoding=None - ) - assert "Failed to connect to MQTT server: Out of memory." in caplog.text - - -async def test_subscribe_error( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test publish error.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.reset_mock() - # simulate client is not connected error before subscribing - mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None) - await mqtt.async_subscribe(hass, "some-topic", record_calls) - while mqtt_client_mock.subscribe.call_count == 0: - await hass.async_block_till_done() - await hass.async_block_till_done() - assert ( - "Error talking to MQTT: The client is not currently connected." in caplog.text - ) - - -async def test_handle_message_callback( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test for handling an incoming message callback.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - callbacks = [] - - @callback - def _callback(args) -> None: - callbacks.append(args) - - msg = ReceiveMessage( - "some-topic", b"test-payload", 1, False, "some-topic", datetime.now() - ) - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "some-topic", _callback) - await mock_debouncer.wait() - mqtt_client_mock.reset_mock() - mqtt_client_mock.on_message(None, None, msg) - - assert len(callbacks) == 1 - assert callbacks[0].topic == "some-topic" - assert callbacks[0].qos == 1 - assert callbacks[0].payload == "test-payload" - - @pytest.mark.parametrize( "hass_config", [ @@ -2128,491 +872,6 @@ async def test_setup_manual_mqtt_with_invalid_config( assert "required key not provided" in caplog.text -@pytest.mark.parametrize( - ("mqtt_config_entry_data", "protocol"), - [ - ( - { - mqtt.CONF_BROKER: "mock-broker", - CONF_PROTOCOL: "3.1", - }, - 3, - ), - ( - { - mqtt.CONF_BROKER: "mock-broker", - CONF_PROTOCOL: "3.1.1", - }, - 4, - ), - ( - { - mqtt.CONF_BROKER: "mock-broker", - CONF_PROTOCOL: "5", - }, - 5, - ), - ], -) -async def test_setup_mqtt_client_protocol( - mqtt_mock_entry: MqttMockHAClientGenerator, protocol: int -) -> None: - """Test MQTT client protocol setup.""" - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - await mqtt_mock_entry() - - # check if protocol setup was correctly - assert mock_client.call_args[1]["protocol"] == protocol - - -@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) -async def test_handle_mqtt_timeout_on_callback( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_debouncer: asyncio.Event -) -> None: - """Test publish without receiving an ACK callback.""" - mid = 0 - - class FakeInfo: - """Returns a simulated client publish response.""" - - mid = 102 - rc = 0 - - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - - def _mock_ack(topic: str, qos: int = 0) -> tuple[int, int]: - # Handle ACK for subscribe normally - nonlocal mid - mid += 1 - mock_client.on_subscribe(0, 0, mid) - return (0, mid) - - # We want to simulate the publish behaviour MQTT client - mock_client = mock_client.return_value - mock_client.publish.return_value = FakeInfo() - # Mock we get a mid and rc=0 - mock_client.subscribe.side_effect = _mock_ack - mock_client.unsubscribe.side_effect = _mock_ack - mock_client.connect = MagicMock( - return_value=0, - side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( - mock_client.on_connect, mock_client, None, 0, 0, 0 - ), - ) - - entry = MockConfigEntry( - domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} - ) - entry.add_to_hass(hass) - - # Set up the integration - mock_debouncer.clear() - assert await hass.config_entries.async_setup(entry.entry_id) - - # 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() - # 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 - # Ensure we stop lingering background tasks - await hass.config_entries.async_unload(entry.entry_id) - # Assert we did not have any completed subscribes, - # because the debouncer subscribe job failed to receive an ACK, - # and the time auto caused the debouncer job to fail. - assert not mock_debouncer.is_set() - - -async def test_setup_raises_config_entry_not_ready_if_no_connect_broker( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """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( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mock_client().connect = MagicMock(side_effect=OSError("Connection error")) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert "Failed to connect to MQTT server due to exception:" in caplog.text - - -@pytest.mark.parametrize( - ("mqtt_config_entry_data", "insecure_param"), - [ - ({"broker": "test-broker", "certificate": "auto"}, "not set"), - ( - {"broker": "test-broker", "certificate": "auto", "tls_insecure": False}, - False, - ), - ({"broker": "test-broker", "certificate": "auto", "tls_insecure": True}, True), - ], -) -async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - insecure_param: bool | str, -) -> None: - """Test setup uses bundled certs when certificate is set to auto and insecure.""" - calls = [] - insecure_check = {"insecure": "not set"} - - def mock_tls_set( - certificate, certfile=None, keyfile=None, tls_version=None - ) -> None: - calls.append((certificate, certfile, keyfile, tls_version)) - - def mock_tls_insecure_set(insecure_param) -> None: - insecure_check["insecure"] = insecure_param - - with patch( - "homeassistant.components.mqtt.async_client.AsyncMQTTClient" - ) as mock_client: - mock_client().tls_set = mock_tls_set - mock_client().tls_insecure_set = mock_tls_insecure_set - await mqtt_mock_entry() - await hass.async_block_till_done() - - assert calls - - expected_certificate = certifi.where() - assert calls[0][0] == expected_certificate - - # test if insecure is set - assert insecure_check["insecure"] == insecure_param - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_CERTIFICATE: "auto", - } - ], -) -async def test_tls_version( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - mqtt_mock_entry: MqttMockHAClientGenerator, -) -> None: - """Test setup defaults for tls.""" - await mqtt_mock_entry() - await hass.async_block_till_done() - assert ( - mqtt_client_mock.tls_set.mock_calls[0][2]["tls_version"] - == ssl.PROTOCOL_TLS_CLIENT - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "birth", - mqtt.ATTR_PAYLOAD: "birth", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], -) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -async def test_custom_birth_message( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test sending birth message.""" - - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - mock_debouncer.clear() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - # discovery cooldown - await mock_debouncer.wait() - # Wait for publish call to finish - await hass.async_block_till_done(wait_background_tasks=True) - mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -async def test_default_birth_message( - hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient -) -> None: - """Test sending birth message.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - await hass.async_block_till_done(wait_background_tasks=True) - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], -) -@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) -@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) -async def test_no_birth_message( - hass: HomeAssistant, - record_calls: MessageCallbackType, - mock_debouncer: asyncio.Event, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test disabling birth message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - mock_debouncer.clear() - assert await hass.config_entries.async_setup(entry.entry_id) - # Wait for discovery cooldown - await mock_debouncer.wait() - # Ensure any publishing could have been processed - await hass.async_block_till_done(wait_background_tasks=True) - mqtt_client_mock.publish.assert_not_called() - - mqtt_client_mock.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "homeassistant/some-topic", record_calls) - # Wait for discovery cooldown - await mock_debouncer.wait() - mqtt_client_mock.subscribe.assert_called() - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.2) -async def test_delayed_birth_message( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test sending birth message does not happen until Home Assistant starts.""" - hass.set_state(CoreState.starting) - await hass.async_block_till_done() - birth = asyncio.Event() - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - - @callback - def wait_birth(msg: ReceiveMessage) -> None: - """Handle birth message.""" - birth.set() - - await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - with pytest.raises(TimeoutError): - await asyncio.wait_for(birth.wait(), 0.05) - assert not mqtt_client_mock.publish.called - assert not birth.is_set() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await birth.wait() - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -async def test_subscription_done_when_birth_message_is_sent( - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test sending birth message until initial subscription has been completed.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) - assert ("homeassistant/+/+/config", 0) in subscribe_calls - assert ("homeassistant/+/+/+/config", 0) in subscribe_calls - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_WILL_MESSAGE: { - mqtt.ATTR_TOPIC: "death", - mqtt.ATTR_PAYLOAD: "death", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, - }, - } - ], -) -async def test_custom_will_message( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test will message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - mqtt_client_mock.will_set.assert_called_with( - topic="death", payload="death", qos=0, retain=False - ) - - -async def test_default_will_message( - setup_with_birth_msg_client_mock: MqttMockPahoClient, -) -> None: - """Test will message.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.will_set.assert_called_with( - topic="homeassistant/status", payload="offline", qos=0, retain=False - ) - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], -) -async def test_no_will_message( - hass: HomeAssistant, - mqtt_config_entry_data: dict[str, Any], - mqtt_client_mock: MqttMockPahoClient, -) -> None: - """Test will message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) - entry.add_to_hass(hass) - hass.config.components.add(mqtt.DOMAIN) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - mqtt_client_mock.will_set.assert_not_called() - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE | {mqtt.CONF_DISCOVERY: False}], -) -async def test_mqtt_subscribes_topics_on_connect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test subscription to topic on connect.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "topic/test", record_calls) - await mqtt.async_subscribe(hass, "home/sensor", record_calls, 2) - await mqtt.async_subscribe(hass, "still/pending", record_calls) - await mqtt.async_subscribe(hass, "still/pending", record_calls, 1) - await mock_debouncer.wait() - - mqtt_client_mock.on_disconnect(Mock(), None, 0) - - mqtt_client_mock.reset_mock() - - mock_debouncer.clear() - mqtt_client_mock.on_connect(Mock(), None, 0, 0) - await mock_debouncer.wait() - - subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) - assert ("topic/test", 0) in subscribe_calls - assert ("home/sensor", 2) in subscribe_calls - assert ("still/pending", 1) in subscribe_calls - - -@pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], -) -async def test_mqtt_subscribes_in_single_call( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test bundled client subscription to topic.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - mqtt_client_mock.subscribe.reset_mock() - mock_debouncer.clear() - await mqtt.async_subscribe(hass, "topic/test", record_calls) - await mqtt.async_subscribe(hass, "home/sensor", record_calls) - # Make sure the debouncer finishes - await mock_debouncer.wait() - - assert mqtt_client_mock.subscribe.call_count == 1 - # Assert we have a single subscription call with both subscriptions - assert mqtt_client_mock.subscribe.mock_calls[0][1][0] in [ - [("topic/test", 0), ("home/sensor", 0)], - [("home/sensor", 0), ("topic/test", 0)], - ] - - -@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) -@patch("homeassistant.components.mqtt.client.MAX_SUBSCRIBES_PER_CALL", 2) -@patch("homeassistant.components.mqtt.client.MAX_UNSUBSCRIBES_PER_CALL", 2) -async def test_mqtt_subscribes_and_unsubscribes_in_chunks( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - record_calls: MessageCallbackType, -) -> None: - """Test chunked client subscriptions.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - - mqtt_client_mock.subscribe.reset_mock() - unsub_tasks: list[CALLBACK_TYPE] = [] - mock_debouncer.clear() - unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test1", record_calls)) - unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor1", record_calls)) - unsub_tasks.append(await mqtt.async_subscribe(hass, "topic/test2", record_calls)) - unsub_tasks.append(await mqtt.async_subscribe(hass, "home/sensor2", record_calls)) - # Make sure the debouncer finishes - await mock_debouncer.wait() - - assert mqtt_client_mock.subscribe.call_count == 2 - # Assert we have a 2 subscription calls with both 2 subscriptions - assert len(mqtt_client_mock.subscribe.mock_calls[0][1][0]) == 2 - assert len(mqtt_client_mock.subscribe.mock_calls[1][1][0]) == 2 - - # Unsubscribe all topics - mock_debouncer.clear() - for task in unsub_tasks: - task() - # Make sure the debouncer finishes - await mock_debouncer.wait() - - assert mqtt_client_mock.unsubscribe.call_count == 2 - # Assert we have a 2 unsubscribe calls with both 2 topic - assert len(mqtt_client_mock.unsubscribe.mock_calls[0][1][0]) == 2 - assert len(mqtt_client_mock.unsubscribe.mock_calls[1][1][0]) == 2 - - @pytest.mark.usefixtures("mqtt_client_mock") async def test_default_entry_setting_are_applied( hass: HomeAssistant, device_registry: dr.DeviceRegistry @@ -4106,221 +2365,6 @@ async def test_multi_platform_discovery( ) -async def test_auto_reconnect( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test reconnection is automatically done.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - mqtt_client_mock.reconnect.reset_mock() - - mqtt_client_mock.disconnect() - mqtt_client_mock.on_disconnect(None, None, 0) - await hass.async_block_till_done() - - mqtt_client_mock.reconnect.side_effect = OSError("foo") - async_fire_time_changed( - hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) - ) - await hass.async_block_till_done() - assert len(mqtt_client_mock.reconnect.mock_calls) == 1 - assert "Error re-connecting to MQTT server due to exception: foo" in caplog.text - - mqtt_client_mock.reconnect.side_effect = None - async_fire_time_changed( - hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) - ) - await hass.async_block_till_done() - assert len(mqtt_client_mock.reconnect.mock_calls) == 2 - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - - mqtt_client_mock.disconnect() - mqtt_client_mock.on_disconnect(None, None, 0) - await hass.async_block_till_done() - - async_fire_time_changed( - hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) - ) - await hass.async_block_till_done() - # Should not reconnect after stop - assert len(mqtt_client_mock.reconnect.mock_calls) == 2 - - -async def test_server_sock_connect_and_disconnect( - hass: HomeAssistant, - mock_debouncer: asyncio.Event, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test handling the socket connected and disconnected.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) - await hass.async_block_till_done() - - server.close() # mock the server closing the connection on us - - mock_debouncer.clear() - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - await mock_debouncer.wait() - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_CONN_LOST - mqtt_client_mock.on_socket_unregister_write(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_close(mqtt_client_mock, None, client) - mqtt_client_mock.on_disconnect(mqtt_client_mock, None, client) - await hass.async_block_till_done() - mock_debouncer.clear() - unsub() - await hass.async_block_till_done() - assert not mock_debouncer.is_set() - - # Should have failed - assert len(recorded_calls) == 0 - - -async def test_server_sock_buffer_size( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test handling the socket buffer size fails.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - with patch.object(client, "setsockopt", side_effect=OSError("foo")): - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) - await hass.async_block_till_done() - assert "Unable to increase the socket buffer size" in caplog.text - - -async def test_server_sock_buffer_size_with_websocket( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test handling the socket buffer size fails.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - - class FakeWebsocket(paho_mqtt.WebsocketWrapper): - def _do_handshake(self, *args, **kwargs): - pass - - wrapped_socket = FakeWebsocket(client, "127.0.01", 1, False, "/", None) - - with patch.object(client, "setsockopt", side_effect=OSError("foo")): - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, wrapped_socket) - mqtt_client_mock.on_socket_register_write( - mqtt_client_mock, None, wrapped_socket - ) - await hass.async_block_till_done() - assert "Unable to increase the socket buffer size" in caplog.text - - -async def test_client_sock_failure_after_connect( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - recorded_calls: list[ReceiveMessage], - record_calls: MessageCallbackType, -) -> None: - """Test handling the socket connected and disconnected.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_writer(mqtt_client_mock, None, client) - await hass.async_block_till_done() - - mqtt_client_mock.loop_write.side_effect = OSError("foo") - client.close() # close the client socket out from under the client - - assert mqtt_client_mock.connect.call_count == 1 - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) - await hass.async_block_till_done() - - unsub() - # Should have failed - assert len(recorded_calls) == 0 - - -async def test_loop_write_failure( - hass: HomeAssistant, - setup_with_birth_msg_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test handling the socket connected and disconnected.""" - mqtt_client_mock = setup_with_birth_msg_client_mock - assert mqtt_client_mock.connect.call_count == 1 - - mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS - - client, server = socket.socketpair( - family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 - ) - client.setblocking(False) - server.setblocking(False) - mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client) - mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client) - mqtt_client_mock.loop_write.return_value = paho_mqtt.MQTT_ERR_CONN_LOST - mqtt_client_mock.loop_read.return_value = paho_mqtt.MQTT_ERR_CONN_LOST - - # Fill up the outgoing buffer to ensure that loop_write - # and loop_read are called that next time control is - # returned to the event loop - try: - for _ in range(1000): - server.send(b"long" * 100) - except BlockingIOError: - pass - - server.close() - # Once for the reader callback - await hass.async_block_till_done() - # Another for the writer callback - await hass.async_block_till_done() - # Final for the disconnect callback - await hass.async_block_till_done() - - assert "Disconnected from MQTT server test-broker:1883" in caplog.text - - @pytest.mark.parametrize( "attr", [ From 6f716c175387e8dc61c0db0383d3be61d5094e93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Jun 2024 11:06:56 -0500 Subject: [PATCH 0568/2411] Fix publish cancellation handling in MQTT (#120826) --- homeassistant/components/mqtt/client.py | 4 ++-- tests/components/mqtt/test_client.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 7788c1db641..f65769badfa 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -1141,8 +1141,8 @@ class MQTT: # see https://github.com/eclipse/paho.mqtt.python/issues/687 # properties and reason codes are not used in Home Assistant future = self._async_get_mid_future(mid) - if future.done() and future.exception(): - # Timed out + if future.done() and (future.cancelled() or future.exception()): + # Timed out or cancelled return future.set_result(None) diff --git a/tests/components/mqtt/test_client.py b/tests/components/mqtt/test_client.py index 49b590383d1..cd02d805e1c 100644 --- a/tests/components/mqtt/test_client.py +++ b/tests/components/mqtt/test_client.py @@ -1194,6 +1194,23 @@ async def test_handle_mqtt_on_callback( assert "No ACK from MQTT server" not in caplog.text +async def test_handle_mqtt_on_callback_after_cancellation( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test receiving an ACK after a cancellation.""" + mqtt_mock = await mqtt_mock_entry() + # Simulate the mid future getting a cancellation + mqtt_mock()._async_get_mid_future(101).cancel() + # Simulate an ACK for mid == 101, being received after the cancellation + mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) + await hass.async_block_till_done() + assert "No ACK from MQTT server" not in caplog.text + assert "InvalidStateError" not in caplog.text + + async def test_handle_mqtt_on_callback_after_timeout( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, From c19fb35d0233da33b50c357f590a0b84428587c1 Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Mon, 1 Jul 2024 01:30:08 -0400 Subject: [PATCH 0569/2411] Add handling for different STATFLAG formats in APCUPSD (#120870) * Add handling for different STATFLAG formats * Just use removesuffix --- .../components/apcupsd/binary_sensor.py | 6 +++++- .../components/apcupsd/test_binary_sensor.py | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 77b2b8591e5..5f86ceb6eec 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -68,4 +68,8 @@ class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity): """Returns true if the UPS is online.""" # Check if ONLINE bit is set in STATFLAG. key = self.entity_description.key.upper() - return int(self.coordinator.data[key], 16) & _VALUE_ONLINE_MASK != 0 + # The daemon could either report just a hex ("0x05000008"), or a hex with a "Status Flag" + # suffix ("0x05000008 Status Flag") in older versions. + # Here we trim the suffix if it exists to support both. + flag = self.coordinator.data[key].removesuffix(" Status Flag") + return int(flag, 16) & _VALUE_ONLINE_MASK != 0 diff --git a/tests/components/apcupsd/test_binary_sensor.py b/tests/components/apcupsd/test_binary_sensor.py index 7616a960b21..02351109603 100644 --- a/tests/components/apcupsd/test_binary_sensor.py +++ b/tests/components/apcupsd/test_binary_sensor.py @@ -1,5 +1,7 @@ """Test binary sensors of APCUPSd integration.""" +import pytest + from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import slugify @@ -31,3 +33,22 @@ async def test_no_binary_sensor(hass: HomeAssistant) -> None: device_slug = slugify(MOCK_STATUS["UPSNAME"]) state = hass.states.get(f"binary_sensor.{device_slug}_online_status") assert state is None + + +@pytest.mark.parametrize( + ("override", "expected"), + [ + ("0x008", "on"), + ("0x02040010 Status Flag", "off"), + ], +) +async def test_statflag(hass: HomeAssistant, override: str, expected: str) -> None: + """Test binary sensor for different STATFLAG values.""" + status = MOCK_STATUS.copy() + status["STATFLAG"] = override + await async_init_integration(hass, status=status) + + device_slug = slugify(MOCK_STATUS["UPSNAME"]) + assert ( + hass.states.get(f"binary_sensor.{device_slug}_online_status").state == expected + ) From 3a0e85beb8e4cb68f2b1ae00408fae05e7888b31 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Mon, 1 Jul 2024 01:12:33 +0200 Subject: [PATCH 0570/2411] Bump aioautomower to 2024.6.4 (#120875) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 7883b057a3f..f27b04ef0c0 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.6.3"] + "requirements": ["aioautomower==2024.6.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index dd68902baae..358147bfe73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -204,7 +204,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.3 +aioautomower==2024.6.4 # homeassistant.components.azure_devops aioazuredevops==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54e86d60186..9abd9e10de7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -183,7 +183,7 @@ aioaseko==0.1.1 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.3 +aioautomower==2024.6.4 # homeassistant.components.azure_devops aioazuredevops==2.1.1 From a9740faeda331c46d3cfa7b9d0ffc7b8b85a4412 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 1 Jul 2024 20:06:56 +0300 Subject: [PATCH 0571/2411] Fix Shelly device shutdown (#120881) --- homeassistant/components/shelly/__init__.py | 6 ++++++ .../components/shelly/config_flow.py | 19 ++++++++++++------- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 184b7c8bb6b..75f66d0bced 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -174,10 +174,13 @@ async def _async_setup_block_entry( await device.initialize() if not device.firmware_supported: async_create_issue_unsupported_firmware(hass, entry) + await device.shutdown() raise ConfigEntryNotReady except (DeviceConnectionError, MacAddressMismatchError) as err: + await device.shutdown() raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: + await device.shutdown() raise ConfigEntryAuthFailed(repr(err)) from err runtime_data.block = ShellyBlockCoordinator(hass, entry, device) @@ -247,10 +250,13 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) await device.initialize() if not device.firmware_supported: async_create_issue_unsupported_firmware(hass, entry) + await device.shutdown() raise ConfigEntryNotReady except (DeviceConnectionError, MacAddressMismatchError) as err: + await device.shutdown() raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: + await device.shutdown() raise ConfigEntryAuthFailed(repr(err)) from err runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index c044d032170..cb3bca6aa47 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -102,10 +102,11 @@ async def validate_input( ws_context, options, ) - await rpc_device.initialize() - await rpc_device.shutdown() - - sleep_period = get_rpc_device_wakeup_period(rpc_device.status) + try: + await rpc_device.initialize() + sleep_period = get_rpc_device_wakeup_period(rpc_device.status) + finally: + await rpc_device.shutdown() return { "title": rpc_device.name, @@ -121,11 +122,15 @@ async def validate_input( coap_context, options, ) - await block_device.initialize() - await block_device.shutdown() + try: + await block_device.initialize() + sleep_period = get_block_device_sleep_period(block_device.settings) + finally: + await block_device.shutdown() + return { "title": block_device.name, - CONF_SLEEP_PERIOD: get_block_device_sleep_period(block_device.settings), + CONF_SLEEP_PERIOD: sleep_period, "model": block_device.model, CONF_GEN: gen, } diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index b1b00e40c66..4076f53c28c 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==10.0.1"], + "requirements": ["aioshelly==11.0.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 358147bfe73..5649fd1d86c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -362,7 +362,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==10.0.1 +aioshelly==11.0.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9abd9e10de7..d6755f889b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==10.0.1 +aioshelly==11.0.0 # homeassistant.components.skybell aioskybell==22.7.0 From 779a7ddaa23e265e395f0c2f7fd1b15bf3b50576 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 1 Jul 2024 01:46:10 -0700 Subject: [PATCH 0572/2411] Bump ical to 8.1.1 (#120888) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 5fc28d2f398..d40daa89b0e 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.0.1"] + "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.1.1"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 73619b6bfe9..95c65089c79 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==8.0.1"] + "requirements": ["ical==8.1.1"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 4fa8e2982f9..313315a34f6 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==8.0.1"] + "requirements": ["ical==8.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5649fd1d86c..d946ef51dcf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1131,7 +1131,7 @@ ibmiotf==0.3.4 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==8.0.1 +ical==8.1.1 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6755f889b7..7980e3e4b64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -927,7 +927,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==8.0.1 +ical==8.1.1 # homeassistant.components.ping icmplib==3.0 From 5a052feb87b561dda15c8d434f4834b7fbe08a39 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:21:41 -0300 Subject: [PATCH 0573/2411] Add missing translations for device class in Scrape (#120891) --- homeassistant/components/scrape/strings.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index 9b534aed77b..42cf3001b75 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -139,18 +139,19 @@ "selector": { "device_class": { "options": { - "date": "[%key:component::sensor::entity_component::date::name%]", - "duration": "[%key:component::sensor::entity_component::duration::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", "battery": "[%key:component::sensor::entity_component::battery::name%]", - "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", + "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", "current": "[%key:component::sensor::entity_component::current::name%]", "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", "data_size": "[%key:component::sensor::entity_component::data_size::name%]", + "date": "[%key:component::sensor::entity_component::date::name%]", "distance": "[%key:component::sensor::entity_component::distance::name%]", + "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", @@ -168,8 +169,8 @@ "pm1": "[%key:component::sensor::entity_component::pm1::name%]", "pm10": "[%key:component::sensor::entity_component::pm10::name%]", "pm25": "[%key:component::sensor::entity_component::pm25::name%]", - "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "power": "[%key:component::sensor::entity_component::power::name%]", + "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", "pressure": "[%key:component::sensor::entity_component::pressure::name%]", @@ -184,6 +185,7 @@ "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", + "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", From a0f8012f4858e4c6ff3f86515e11e06a6a90414b Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Mon, 1 Jul 2024 02:44:59 -0300 Subject: [PATCH 0574/2411] Add missing translations for device class in SQL (#120892) --- homeassistant/components/sql/strings.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sql/strings.json b/homeassistant/components/sql/strings.json index 361585b8876..cd36ccf7731 100644 --- a/homeassistant/components/sql/strings.json +++ b/homeassistant/components/sql/strings.json @@ -71,18 +71,19 @@ "selector": { "device_class": { "options": { - "date": "[%key:component::sensor::entity_component::date::name%]", - "duration": "[%key:component::sensor::entity_component::duration::name%]", "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", "battery": "[%key:component::sensor::entity_component::battery::name%]", - "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", + "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", "current": "[%key:component::sensor::entity_component::current::name%]", "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", "data_size": "[%key:component::sensor::entity_component::data_size::name%]", + "date": "[%key:component::sensor::entity_component::date::name%]", "distance": "[%key:component::sensor::entity_component::distance::name%]", + "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", @@ -100,8 +101,8 @@ "pm1": "[%key:component::sensor::entity_component::pm1::name%]", "pm10": "[%key:component::sensor::entity_component::pm10::name%]", "pm25": "[%key:component::sensor::entity_component::pm25::name%]", - "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "power": "[%key:component::sensor::entity_component::power::name%]", + "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", "pressure": "[%key:component::sensor::entity_component::pressure::name%]", @@ -116,6 +117,7 @@ "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", "voltage": "[%key:component::sensor::entity_component::voltage::name%]", "volume": "[%key:component::sensor::entity_component::volume::name%]", + "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", From 16d7764f18f65712b4e78b9e3345fff5e060b9e8 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Mon, 1 Jul 2024 02:55:13 -0300 Subject: [PATCH 0575/2411] Add missing translations for device class in Template (#120893) --- homeassistant/components/template/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 4a1377cbf0b..dc481b76ff8 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -105,6 +105,7 @@ "battery": "[%key:component::sensor::entity_component::battery::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", "current": "[%key:component::sensor::entity_component::current::name%]", "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", "data_size": "[%key:component::sensor::entity_component::data_size::name%]", From 88ed43c7792ff8732ad756522a39c925da6de408 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 1 Jul 2024 18:27:40 +0200 Subject: [PATCH 0576/2411] Improve add user error messages (#120909) --- homeassistant/auth/providers/homeassistant.py | 20 ++++++++-------- homeassistant/components/auth/strings.json | 5 +++- .../config/auth_provider_homeassistant.py | 24 ++++--------------- .../test_auth_provider_homeassistant.py | 16 +++++++++++-- 4 files changed, 32 insertions(+), 33 deletions(-) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 4e38260dd2f..ec39bdbdcdc 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -55,13 +55,6 @@ class InvalidUser(HomeAssistantError): Will not be raised when validating authentication. """ - -class InvalidUsername(InvalidUser): - """Raised when invalid username is specified. - - Will not be raised when validating authentication. - """ - def __init__( self, *args: object, @@ -77,6 +70,13 @@ class InvalidUsername(InvalidUser): ) +class InvalidUsername(InvalidUser): + """Raised when invalid username is specified. + + Will not be raised when validating authentication. + """ + + class Data: """Hold the user data.""" @@ -216,7 +216,7 @@ class Data: break if index is None: - raise InvalidUser + raise InvalidUser(translation_key="user_not_found") self.users.pop(index) @@ -232,7 +232,7 @@ class Data: user["password"] = self.hash_password(new_password, True).decode() break else: - raise InvalidUser + raise InvalidUser(translation_key="user_not_found") @callback def _validate_new_username(self, new_username: str) -> None: @@ -275,7 +275,7 @@ class Data: self._async_check_for_not_normalized_usernames(self._data) break else: - raise InvalidUser + raise InvalidUser(translation_key="user_not_found") async def async_save(self) -> None: """Save data.""" diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index 0e4cede78a3..c8622880f0f 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -37,7 +37,10 @@ "message": "Username \"{username}\" already exists" }, "username_not_normalized": { - "message": "Username \"{new_username}\" is not normalized" + "message": "Username \"{new_username}\" is not normalized. Please make sure the username is lowercase and does not contain any whitespace." + }, + "user_not_found": { + "message": "User not found" } }, "issues": { diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 1cfcda6d4b2..8513c53bd07 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -53,11 +53,7 @@ async def websocket_create( ) return - try: - await provider.async_add_auth(msg["username"], msg["password"]) - except auth_ha.InvalidUser: - connection.send_error(msg["id"], "username_exists", "Username already exists") - return + await provider.async_add_auth(msg["username"], msg["password"]) credentials = await provider.async_get_or_create_credentials( {"username": msg["username"]} @@ -94,13 +90,7 @@ async def websocket_delete( connection.send_result(msg["id"]) return - try: - await provider.async_remove_auth(msg["username"]) - except auth_ha.InvalidUser: - connection.send_error( - msg["id"], "auth_not_found", "Given username was not found." - ) - return + await provider.async_remove_auth(msg["username"]) connection.send_result(msg["id"]) @@ -187,14 +177,8 @@ async def websocket_admin_change_password( ) return - try: - await provider.async_change_password(username, msg["password"]) - connection.send_result(msg["id"]) - except auth_ha.InvalidUser: - connection.send_error( - msg["id"], "credentials_not_found", "Credentials not found" - ) - return + await provider.async_change_password(username, msg["password"]) + connection.send_result(msg["id"]) @websocket_api.websocket_command( diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index ffee88f91ec..6b580013968 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -183,7 +183,13 @@ async def test_create_auth_duplicate_username( result = await client.receive_json() assert not result["success"], result - assert result["error"]["code"] == "username_exists" + assert result["error"] == { + "code": "home_assistant_error", + "message": "username_already_exists", + "translation_key": "username_already_exists", + "translation_placeholders": {"username": "test-user"}, + "translation_domain": "auth", + } async def test_delete_removes_just_auth( @@ -282,7 +288,13 @@ async def test_delete_unknown_auth( result = await client.receive_json() assert not result["success"], result - assert result["error"]["code"] == "auth_not_found" + assert result["error"] == { + "code": "home_assistant_error", + "message": "user_not_found", + "translation_key": "user_not_found", + "translation_placeholders": None, + "translation_domain": "auth", + } async def test_change_password( From a787ce863371efc0620d6ea7e557d954dc637874 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 1 Jul 2024 13:06:14 +0200 Subject: [PATCH 0577/2411] Bump incomfort-client dependency to 0.6.3 (#120913) --- homeassistant/components/incomfort/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index c0b536dabe5..93f350a8e2c 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/incomfort", "iot_class": "local_polling", "loggers": ["incomfortclient"], - "requirements": ["incomfort-client==0.6.2"] + "requirements": ["incomfort-client==0.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index d946ef51dcf..6e51947fca1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1152,7 +1152,7 @@ ihcsdk==2.8.5 imgw_pib==1.0.5 # homeassistant.components.incomfort -incomfort-client==0.6.2 +incomfort-client==0.6.3 # homeassistant.components.influxdb influxdb-client==1.24.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7980e3e4b64..a0f675bc256 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -942,7 +942,7 @@ ifaddr==0.2.0 imgw_pib==1.0.5 # homeassistant.components.incomfort -incomfort-client==0.6.2 +incomfort-client==0.6.3 # homeassistant.components.influxdb influxdb-client==1.24.0 From 887ab1dc58c118f69bf3f2009042b3c4ccd93018 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Jul 2024 17:52:30 +0200 Subject: [PATCH 0578/2411] Bump openai to 1.35.1 (#120926) Bump openai to 1.35.7 --- homeassistant/components/openai_conversation/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/openai_conversation/manifest.json b/homeassistant/components/openai_conversation/manifest.json index 0c06a3d4cd8..fcbdc996ce5 100644 --- a/homeassistant/components/openai_conversation/manifest.json +++ b/homeassistant/components/openai_conversation/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/openai_conversation", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["openai==1.3.8"] + "requirements": ["openai==1.35.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6e51947fca1..d96b0266043 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1477,7 +1477,7 @@ open-garage==0.2.0 open-meteo==0.3.1 # homeassistant.components.openai_conversation -openai==1.3.8 +openai==1.35.7 # homeassistant.components.openerz openerz-api==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0f675bc256..2753f42ee93 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1198,7 +1198,7 @@ open-garage==0.2.0 open-meteo==0.3.1 # homeassistant.components.openai_conversation -openai==1.3.8 +openai==1.35.7 # homeassistant.components.openerz openerz-api==0.3.0 From 8a7e2c05a5844d6d2d72d0d5969a439030bd9a0e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 1 Jul 2024 17:30:23 +0200 Subject: [PATCH 0579/2411] Mark dry/fan-only climate modes as supported for Panasonic room air conditioner (#120939) --- homeassistant/components/matter/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index d2656d59138..c97124f4305 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -60,6 +60,7 @@ SUPPORT_DRY_MODE_DEVICES: set[tuple[int, int]] = { # In the list below specify tuples of (vendorid, productid) of devices that # support dry mode. (0x0001, 0x0108), + (0x0001, 0x010A), (0x1209, 0x8007), } @@ -68,6 +69,7 @@ SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = { # In the list below specify tuples of (vendorid, productid) of devices that # support fan-only mode. (0x0001, 0x0108), + (0x0001, 0x010A), (0x1209, 0x8007), } From 4b2be448f0195b7db2d4a093e38ee187cec6b040 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:50:35 +0100 Subject: [PATCH 0580/2411] Bump python-kasa to 0.7.0.2 (#120940) --- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 74b80771c65..1270bb3469b 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -297,5 +297,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.7.0.1"] + "requirements": ["python-kasa[speedups]==0.7.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index d96b0266043..c18ed2f439a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2275,7 +2275,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.1 +python-kasa[speedups]==0.7.0.2 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2753f42ee93..6291a3dddca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1775,7 +1775,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.1 +python-kasa[speedups]==0.7.0.2 # homeassistant.components.matter python-matter-server==6.2.0b1 From d8f55763c50aa6c61b787a9364c5092cd559d223 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Jul 2024 09:26:20 -0700 Subject: [PATCH 0581/2411] Downgrade logging previously reported asyncio block to debug (#120942) --- homeassistant/util/loop.py | 123 +++++++++++----- tests/util/test_loop.py | 282 +++++++++++++++++++++---------------- 2 files changed, 244 insertions(+), 161 deletions(-) diff --git a/homeassistant/util/loop.py b/homeassistant/util/loop.py index 866f35e79e2..d7593013046 100644 --- a/homeassistant/util/loop.py +++ b/homeassistant/util/loop.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable import functools +from functools import cache import linecache import logging import threading @@ -26,6 +27,11 @@ def _get_line_from_cache(filename: str, lineno: int) -> str: return (linecache.getline(filename, lineno) or "?").strip() +# Set of previously reported blocking calls +# (integration, filename, lineno) +_PREVIOUSLY_REPORTED: set[tuple[str | None, str, int | Any]] = set() + + def raise_for_blocking_call( func: Callable[..., Any], check_allowed: Callable[[dict[str, Any]], bool] | None = None, @@ -42,28 +48,48 @@ def raise_for_blocking_call( offender_filename = offender_frame.f_code.co_filename offender_lineno = offender_frame.f_lineno offender_line = _get_line_from_cache(offender_filename, offender_lineno) + report_key: tuple[str | None, str, int | Any] try: integration_frame = get_integration_frame() except MissingIntegrationFrame: # Did not source from integration? Hard error. + report_key = (None, offender_filename, offender_lineno) + was_reported = report_key in _PREVIOUSLY_REPORTED + _PREVIOUSLY_REPORTED.add(report_key) if not strict_core: - _LOGGER.warning( - "Detected blocking call to %s with args %s in %s, " - "line %s: %s inside the event loop; " - "This is causing stability issues. " - "Please create a bug report at " - "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n" - "%s\n" - "Traceback (most recent call last):\n%s", - func.__name__, - mapped_args.get("args"), - offender_filename, - offender_lineno, - offender_line, - _dev_help_message(func.__name__), - "".join(traceback.format_stack(f=offender_frame)), - ) + if was_reported: + _LOGGER.debug( + "Detected blocking call to %s with args %s in %s, " + "line %s: %s inside the event loop; " + "This is causing stability issues. " + "Please create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n" + "%s\n", + func.__name__, + mapped_args.get("args"), + offender_filename, + offender_lineno, + offender_line, + _dev_help_message(func.__name__), + ) + else: + _LOGGER.warning( + "Detected blocking call to %s with args %s in %s, " + "line %s: %s inside the event loop; " + "This is causing stability issues. " + "Please create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%%3Aopen+is%%3Aissue\n" + "%s\n" + "Traceback (most recent call last):\n%s", + func.__name__, + mapped_args.get("args"), + offender_filename, + offender_lineno, + offender_line, + _dev_help_message(func.__name__), + "".join(traceback.format_stack(f=offender_frame)), + ) return if found_frame is None: @@ -77,32 +103,56 @@ def raise_for_blocking_call( f"{_dev_help_message(func.__name__)}" ) + report_key = (integration_frame.integration, offender_filename, offender_lineno) + was_reported = report_key in _PREVIOUSLY_REPORTED + _PREVIOUSLY_REPORTED.add(report_key) + report_issue = async_suggest_report_issue( async_get_hass_or_none(), integration_domain=integration_frame.integration, module=integration_frame.module, ) - _LOGGER.warning( - "Detected blocking call to %s with args %s " - "inside the event loop by %sintegration '%s' " - "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n" - "%s\n" - "Traceback (most recent call last):\n%s", - func.__name__, - mapped_args.get("args"), - "custom " if integration_frame.custom_integration else "", - integration_frame.integration, - integration_frame.relative_filename, - integration_frame.line_number, - integration_frame.line, - offender_filename, - offender_lineno, - offender_line, - report_issue, - _dev_help_message(func.__name__), - "".join(traceback.format_stack(f=integration_frame.frame)), - ) + if was_reported: + _LOGGER.debug( + "Detected blocking call to %s with args %s " + "inside the event loop by %sintegration '%s' " + "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n" + "%s\n", + func.__name__, + mapped_args.get("args"), + "custom " if integration_frame.custom_integration else "", + integration_frame.integration, + integration_frame.relative_filename, + integration_frame.line_number, + integration_frame.line, + offender_filename, + offender_lineno, + offender_line, + report_issue, + _dev_help_message(func.__name__), + ) + else: + _LOGGER.warning( + "Detected blocking call to %s with args %s " + "inside the event loop by %sintegration '%s' " + "at %s, line %s: %s (offender: %s, line %s: %s), please %s\n" + "%s\n" + "Traceback (most recent call last):\n%s", + func.__name__, + mapped_args.get("args"), + "custom " if integration_frame.custom_integration else "", + integration_frame.integration, + integration_frame.relative_filename, + integration_frame.line_number, + integration_frame.line, + offender_filename, + offender_lineno, + offender_line, + report_issue, + _dev_help_message(func.__name__), + "".join(traceback.format_stack(f=integration_frame.frame)), + ) if strict: raise RuntimeError( @@ -117,6 +167,7 @@ def raise_for_blocking_call( ) +@cache def _dev_help_message(what: str) -> str: """Generate help message to guide developers.""" return ( diff --git a/tests/util/test_loop.py b/tests/util/test_loop.py index 585f32a965f..f4846d98898 100644 --- a/tests/util/test_loop.py +++ b/tests/util/test_loop.py @@ -1,5 +1,7 @@ """Tests for async util methods from Python source.""" +from collections.abc import Generator +import contextlib import threading from unittest.mock import Mock, patch @@ -15,57 +17,14 @@ def banned_function(): """Mock banned function.""" -async def test_raise_for_blocking_call_async() -> None: - """Test raise_for_blocking_call detects when called from event loop without integration context.""" - with pytest.raises(RuntimeError): - haloop.raise_for_blocking_call(banned_function) - - -async def test_raise_for_blocking_call_async_non_strict_core( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test non_strict_core raise_for_blocking_call detects from event loop without integration context.""" - haloop.raise_for_blocking_call(banned_function, strict_core=False) - assert "Detected blocking call to banned_function" in caplog.text - assert "Traceback (most recent call last)" in caplog.text - assert ( - "Please create a bug report at https://github.com/home-assistant/core/issues" - in caplog.text - ) - assert ( - "For developers, please see " - "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" - ) in caplog.text - - -async def test_raise_for_blocking_call_async_integration( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test raise_for_blocking_call detects and raises when called from event loop from integration context.""" - frames = extract_stack_to_frame( - [ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ] - ) +@contextlib.contextmanager +def patch_get_current_frame(stack: list[Mock]) -> Generator[None, None, None]: + """Patch get_current_frame.""" + frames = extract_stack_to_frame(stack) with ( - pytest.raises(RuntimeError), patch( "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", + return_value=stack[1].line, ), patch( "homeassistant.util.loop._get_line_from_cache", @@ -79,13 +38,104 @@ async def test_raise_for_blocking_call_async_integration( "homeassistant.helpers.frame.get_current_frame", return_value=frames, ), + ): + yield + + +async def test_raise_for_blocking_call_async() -> None: + """Test raise_for_blocking_call detects when called from event loop without integration context.""" + with pytest.raises(RuntimeError): + haloop.raise_for_blocking_call(banned_function) + + +async def test_raise_for_blocking_call_async_non_strict_core( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test non_strict_core raise_for_blocking_call detects from event loop without integration context.""" + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="12", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="12", + line="self.light.is_on", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] + with patch_get_current_frame(stack): + haloop.raise_for_blocking_call(banned_function, strict_core=False) + assert "Detected blocking call to banned_function" in caplog.text + assert "Traceback (most recent call last)" in caplog.text + assert ( + "Please create a bug report at https://github.com/home-assistant/core/issues" + in caplog.text + ) + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text + + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 1 + caplog.clear() + + # Second call should log at debug + with patch_get_current_frame(stack): + haloop.raise_for_blocking_call(banned_function, strict_core=False) + + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 0 + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text + + # no expensive traceback on debug + assert "Traceback (most recent call last)" not in caplog.text + + +async def test_raise_for_blocking_call_async_integration( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test raise_for_blocking_call detects and raises when called from event loop from integration context.""" + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="18", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="18", + line="self.light.is_on", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="8", + line="something()", + ), + ] + with ( + pytest.raises(RuntimeError), + patch_get_current_frame(stack), ): haloop.raise_for_blocking_call(banned_function) assert ( "Detected blocking call to banned_function with args None" " inside the event loop by integration" - " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on " - "(offender: /home/paulus/aiohue/lights.py, line 2: mock_line), please create " + " 'hue' at homeassistant/components/hue/light.py, line 18: self.light.is_on " + "(offender: /home/paulus/aiohue/lights.py, line 8: mock_line), please create " "a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text ) @@ -99,55 +149,37 @@ async def test_raise_for_blocking_call_async_integration_non_strict( caplog: pytest.LogCaptureFixture, ) -> None: """Test raise_for_blocking_call detects when called from event loop from integration context.""" - frames = extract_stack_to_frame( - [ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ] - ) - with ( - patch( - "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="15", + line="do_something()", ), - patch( - "homeassistant.util.loop._get_line_from_cache", - return_value="mock_line", + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="15", + line="self.light.is_on", ), - patch( - "homeassistant.util.loop.get_current_frame", - return_value=frames, + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="1", + line="something()", ), - patch( - "homeassistant.helpers.frame.get_current_frame", - return_value=frames, - ), - ): + ] + with patch_get_current_frame(stack): haloop.raise_for_blocking_call(banned_function, strict=False) + assert ( "Detected blocking call to banned_function with args None" " inside the event loop by integration" - " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on " - "(offender: /home/paulus/aiohue/lights.py, line 2: mock_line), " + " 'hue' at homeassistant/components/hue/light.py, line 15: self.light.is_on " + "(offender: /home/paulus/aiohue/lights.py, line 1: mock_line), " "please create a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text ) assert "Traceback (most recent call last)" in caplog.text assert ( - 'File "/home/paulus/homeassistant/components/hue/light.py", line 23' + 'File "/home/paulus/homeassistant/components/hue/light.py", line 15' in caplog.text ) assert ( @@ -158,62 +190,62 @@ async def test_raise_for_blocking_call_async_integration_non_strict( "For developers, please see " "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" ) in caplog.text + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 1 + caplog.clear() + + # Second call should log at debug + with patch_get_current_frame(stack): + haloop.raise_for_blocking_call(banned_function, strict=False) + + warnings = [ + record for record in caplog.get_records("call") if record.levelname == "WARNING" + ] + assert len(warnings) == 0 + assert ( + "For developers, please see " + "https://developers.home-assistant.io/docs/asyncio_blocking_operations/#banned_function" + ) in caplog.text + # no expensive traceback on debug + assert "Traceback (most recent call last)" not in caplog.text async def test_raise_for_blocking_call_async_custom( caplog: pytest.LogCaptureFixture, ) -> None: """Test raise_for_blocking_call detects when called from event loop with custom component context.""" - frames = extract_stack_to_frame( - [ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - Mock( - filename="/home/paulus/config/custom_components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ] - ) - with ( - pytest.raises(RuntimeError), - patch( - "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", + stack = [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="12", + line="do_something()", ), - patch( - "homeassistant.util.loop._get_line_from_cache", - return_value="mock_line", + Mock( + filename="/home/paulus/config/custom_components/hue/light.py", + lineno="12", + line="self.light.is_on", ), - patch( - "homeassistant.util.loop.get_current_frame", - return_value=frames, + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="3", + line="something()", ), - patch( - "homeassistant.helpers.frame.get_current_frame", - return_value=frames, - ), - ): + ] + with pytest.raises(RuntimeError), patch_get_current_frame(stack): haloop.raise_for_blocking_call(banned_function) assert ( "Detected blocking call to banned_function with args None" " inside the event loop by custom " - "integration 'hue' at custom_components/hue/light.py, line 23: self.light.is_on" - " (offender: /home/paulus/aiohue/lights.py, line 2: mock_line), " + "integration 'hue' at custom_components/hue/light.py, line 12: self.light.is_on" + " (offender: /home/paulus/aiohue/lights.py, line 3: mock_line), " "please create a bug report at https://github.com/home-assistant/core/issues?" "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" ) in caplog.text assert "Traceback (most recent call last)" in caplog.text assert ( - 'File "/home/paulus/config/custom_components/hue/light.py", line 23' + 'File "/home/paulus/config/custom_components/hue/light.py", line 12' in caplog.text ) assert ( From 2f307d6a8a8b08cbd793f5c2f6de84019fa5641b Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Mon, 1 Jul 2024 19:02:43 +0200 Subject: [PATCH 0582/2411] Fix Bang & Olufsen jumping volume bar (#120946) --- homeassistant/components/bang_olufsen/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 0eff9f2bb85..07e38d633a1 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -366,7 +366,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): @property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" - if self._volume.level and self._volume.level.level: + if self._volume.level and self._volume.level.level is not None: return float(self._volume.level.level / 100) return None From 74687f3b6009715e1685fbe260c00c2a2274ec53 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 1 Jul 2024 19:44:51 +0200 Subject: [PATCH 0583/2411] Bump version to 2024.7.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 e97f14f830c..5f020a02624 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 0f4b25eb0cc..6320551a082 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b6" +version = "2024.7.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 167a8c6613265cc1404924953b008ca84cf3745e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 1 Jul 2024 19:49:12 +0200 Subject: [PATCH 0584/2411] Use fixtures in deCONZ fan tests (#120953) --- tests/components/deconz/test_fan.py | 286 ++-------------------------- 1 file changed, 20 insertions(+), 266 deletions(-) diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index 5da0398c3e6..0f22c0b2b3b 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -1,9 +1,8 @@ """deCONZ fan platform tests.""" -from unittest.mock import patch +from collections.abc import Callable import pytest -from voluptuous.error import MultipleInvalid from homeassistant.components.fan import ( ATTR_PERCENTAGE, @@ -12,14 +11,11 @@ from homeassistant.components.fan import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from .test_gateway import ( - DECONZ_WEB_REQUEST, - mock_deconz_put_request, - setup_deconz_integration, -) +from .test_gateway import setup_deconz_integration from tests.test_util.aiohttp import AiohttpClientMocker @@ -32,12 +28,10 @@ async def test_no_fans( assert len(hass.states.async_all()) == 0 -async def test_fans( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test that all supported fan entities are created.""" - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "1": { "etag": "432f3de28965052961a99e3c5494daf4", "hascolor": False, @@ -56,11 +50,16 @@ async def test_fans( "uniqueid": "00:22:a3:00:00:27:8b:81-01", } } - } - - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +async def test_fans( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + mock_deconz_websocket, +) -> None: + """Test that all supported fan entities are created.""" assert len(hass.states.async_all()) == 2 # Light and fan assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 100 @@ -134,7 +133,7 @@ async def test_fans( # Test service calls - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") + aioclient_mock = mock_put_request("/lights/1/state") # Service turn on fan using saved default_on_speed @@ -231,258 +230,13 @@ async def test_fans( assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert not hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) states = hass.states.async_all() assert len(states) == 2 for state in states: assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - - -async def test_fans_legacy_speed_modes( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test that all supported fan entities are created. - - Legacy fan support. - """ - data = { - "lights": { - "1": { - "etag": "432f3de28965052961a99e3c5494daf4", - "hascolor": False, - "manufacturername": "King Of Fans, Inc.", - "modelid": "HDC52EastwindFan", - "name": "Ceiling fan", - "state": { - "alert": "none", - "bri": 254, - "on": False, - "reachable": True, - "speed": 4, - }, - "swversion": "0000000F", - "type": "Fan", - "uniqueid": "00:22:a3:00:00:27:8b:81-01", - } - } - } - - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - - assert len(hass.states.async_all()) == 2 # Light and fan - assert hass.states.get("fan.ceiling_fan").state == STATE_ON - - # Test states - - event_changed_light = { - "t": "event", - "e": "changed", - "r": "lights", - "id": "1", - "state": {"speed": 1}, - } - await mock_deconz_websocket(data=event_changed_light) - await hass.async_block_till_done() - - assert hass.states.get("fan.ceiling_fan").state == STATE_ON - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 25 - - event_changed_light = { - "t": "event", - "e": "changed", - "r": "lights", - "id": "1", - "state": {"speed": 2}, - } - await mock_deconz_websocket(data=event_changed_light) - await hass.async_block_till_done() - - assert hass.states.get("fan.ceiling_fan").state == STATE_ON - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 50 - - event_changed_light = { - "t": "event", - "e": "changed", - "r": "lights", - "id": "1", - "state": {"speed": 3}, - } - await mock_deconz_websocket(data=event_changed_light) - await hass.async_block_till_done() - - assert hass.states.get("fan.ceiling_fan").state == STATE_ON - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 75 - - event_changed_light = { - "t": "event", - "e": "changed", - "r": "lights", - "id": "1", - "state": {"speed": 4}, - } - await mock_deconz_websocket(data=event_changed_light) - await hass.async_block_till_done() - - assert hass.states.get("fan.ceiling_fan").state == STATE_ON - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 100 - - event_changed_light = { - "t": "event", - "e": "changed", - "r": "lights", - "id": "1", - "state": {"speed": 0}, - } - await mock_deconz_websocket(data=event_changed_light) - await hass.async_block_till_done() - - assert hass.states.get("fan.ceiling_fan").state == STATE_OFF - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 0 - - # Test service calls - - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") - - # Service turn on fan using saved default_on_speed - - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[1][2] == {"speed": 4} - - # Service turn on fan with speed_off - # async_turn_on_compat use speed_to_percentage which will return 0 - - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 0}, - blocking=True, - ) - assert aioclient_mock.mock_calls[2][2] == {"speed": 0} - - # Service turn on fan with bad speed - # async_turn_on_compat use speed_to_percentage which will convert to SPEED_MEDIUM -> 2 - - with pytest.raises(MultipleInvalid): - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: "bad"}, - blocking=True, - ) - - # Service turn on fan to low speed - - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 25}, - blocking=True, - ) - assert aioclient_mock.mock_calls[3][2] == {"speed": 1} - - # Service turn on fan to medium speed - - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 50}, - blocking=True, - ) - assert aioclient_mock.mock_calls[4][2] == {"speed": 2} - - # Service turn on fan to high speed - - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 100}, - blocking=True, - ) - assert aioclient_mock.mock_calls[5][2] == {"speed": 4} - - # Service set fan speed to low - - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 25}, - blocking=True, - ) - assert aioclient_mock.mock_calls[6][2] == {"speed": 1} - - # Service set fan speed to medium - - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 50}, - blocking=True, - ) - assert aioclient_mock.mock_calls[7][2] == {"speed": 2} - - # Service set fan speed to high - - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 100}, - blocking=True, - ) - assert aioclient_mock.mock_calls[8][2] == {"speed": 4} - - # Service set fan speed to off - - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 0}, - blocking=True, - ) - assert aioclient_mock.mock_calls[9][2] == {"speed": 0} - - # Service set fan speed to unsupported value - - with pytest.raises(MultipleInvalid): - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: "bad value"}, - blocking=True, - ) - - # Events with an unsupported speed gets converted to default speed "medium" - - event_changed_light = { - "t": "event", - "e": "changed", - "r": "lights", - "id": "1", - "state": {"speed": 3}, - } - await mock_deconz_websocket(data=event_changed_light) - await hass.async_block_till_done() - - assert hass.states.get("fan.ceiling_fan").state == STATE_ON - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 75 - - await hass.config_entries.async_unload(config_entry.entry_id) - - states = hass.states.async_all() - assert len(states) == 2 - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 From 690164a518971e74a5a3e35508b352c6f887d88f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 1 Jul 2024 20:31:42 +0200 Subject: [PATCH 0585/2411] Use fixtures in deCONZ cover tests (#120954) --- tests/components/deconz/test_cover.py | 94 +++++++++++++-------------- 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 69452c3285e..ec7c41d628c 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -1,6 +1,8 @@ """deCONZ cover platform tests.""" -from unittest.mock import patch +from collections.abc import Callable + +import pytest from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, @@ -17,6 +19,7 @@ from homeassistant.components.cover import ( SERVICE_STOP_COVER, SERVICE_STOP_COVER_TILT, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, STATE_CLOSED, @@ -25,29 +28,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from .test_gateway import ( - DECONZ_WEB_REQUEST, - mock_deconz_put_request, - setup_deconz_integration, -) - from tests.test_util.aiohttp import AiohttpClientMocker -async def test_no_covers( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that no cover entities are created.""" - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 - - -async def test_cover( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test that all supported cover entities are created.""" - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "1": { "name": "Window covering device", "type": "Window covering device", @@ -62,10 +49,15 @@ async def test_cover( "uniqueid": "00:00:00:00:00:00:00:02-00", }, } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +async def test_cover( + hass: HomeAssistant, + config_entry_setup: ConfigEntry, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + mock_deconz_websocket, +) -> None: + """Test that all supported cover entities are created.""" assert len(hass.states.async_all()) == 2 cover = hass.states.get("cover.window_covering_device") assert cover.state == STATE_CLOSED @@ -90,7 +82,7 @@ async def test_cover( # Verify service calls for cover - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") + aioclient_mock = mock_put_request("/lights/1/state") # Service open cover @@ -132,24 +124,22 @@ async def test_cover( ) assert aioclient_mock.mock_calls[4][2] == {"stop": True} - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) states = hass.states.async_all() assert len(states) == 2 for state in states: assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 -async def test_tilt_cover( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that tilting a cover works.""" - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "0": { "etag": "87269755b9b3a046485fdae8d96b252c", "lastannounced": None, @@ -170,10 +160,13 @@ async def test_tilt_cover( "uniqueid": "00:24:46:00:00:12:34:56-01", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_tilt_cover( + hass: HomeAssistant, mock_put_request: Callable[[str, str], AiohttpClientMocker] +) -> None: + """Test that tilting a cover works.""" assert len(hass.states.async_all()) == 1 covering_device = hass.states.get("cover.covering_device") assert covering_device.state == STATE_OPEN @@ -181,7 +174,7 @@ async def test_tilt_cover( # Verify service calls for tilting cover - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state") + aioclient_mock = mock_put_request("/lights/0/state") # Service set tilt cover @@ -224,12 +217,10 @@ async def test_tilt_cover( assert aioclient_mock.mock_calls[4][2] == {"stop": True} -async def test_level_controllable_output_cover( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that tilting a cover works.""" - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "0": { "etag": "4cefc909134c8e99086b55273c2bde67", "hascolor": False, @@ -250,10 +241,13 @@ async def test_level_controllable_output_cover( "uniqueid": "00:22:a3:00:00:00:00:00-01", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_level_controllable_output_cover( + hass: HomeAssistant, mock_put_request: Callable[[str, str], AiohttpClientMocker] +) -> None: + """Test that tilting a cover works.""" assert len(hass.states.async_all()) == 1 covering_device = hass.states.get("cover.vent") assert covering_device.state == STATE_OPEN @@ -261,7 +255,7 @@ async def test_level_controllable_output_cover( # Verify service calls for tilting cover - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state") + aioclient_mock = mock_put_request("/lights/0/state") # Service open cover From 383de9654935d8d45441a0904ef69e7a48eb09eb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 1 Jul 2024 20:36:35 +0200 Subject: [PATCH 0586/2411] Fix missing airgradient string (#120957) --- homeassistant/components/airgradient/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 12049e7b720..6bf7242f2f1 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -16,6 +16,7 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1." }, "error": { From 0ffebd4853c37cfca08f9d881b6c41cd76165273 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 1 Jul 2024 20:48:33 +0200 Subject: [PATCH 0587/2411] Use fixtures in deCONZ button tests (#120958) --- tests/components/deconz/conftest.py | 36 +++++++++++++----------- tests/components/deconz/test_button.py | 38 +++++++++----------------- 2 files changed, 33 insertions(+), 41 deletions(-) diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py index ec254ea1c1e..049dbed2963 100644 --- a/tests/components/deconz/conftest.py +++ b/tests/components/deconz/conftest.py @@ -85,6 +85,11 @@ def fixture_put_request( def fixture_get_request( aioclient_mock: AiohttpClientMocker, config_entry_data: MappingProxyType[str, Any], + config_payload: dict[str, Any], + alarm_system_payload: dict[str, Any], + group_payload: dict[str, Any], + light_payload: dict[str, Any], + sensor_payload: dict[str, Any], deconz_payload: dict[str, Any], ) -> Callable[[str], None]: """Mock default deCONZ requests responses.""" @@ -92,10 +97,21 @@ def fixture_get_request( _port = config_entry_data[CONF_PORT] _api_key = config_entry_data[CONF_API_KEY] + data = deconz_payload + data.setdefault("alarmsystems", alarm_system_payload) + data.setdefault("config", config_payload) + data.setdefault("groups", group_payload) + data.setdefault("lights", light_payload) + data.setdefault("sensors", sensor_payload) + def __mock_requests(host: str = "") -> None: url = f"http://{host or _host}:{_port}/api/{_api_key}" aioclient_mock.get( - url, json=deconz_payload, headers={"content-type": CONTENT_TYPE_JSON} + url, + json=deconz_payload | {"config": config_payload}, + headers={ + "content-type": CONTENT_TYPE_JSON, + }, ) return __mock_requests @@ -105,21 +121,9 @@ def fixture_get_request( @pytest.fixture(name="deconz_payload") -def fixture_data( - alarm_system_payload: dict[str, Any], - config_payload: dict[str, Any], - group_payload: dict[str, Any], - light_payload: dict[str, Any], - sensor_payload: dict[str, Any], -) -> dict[str, Any]: - """DeCONZ data.""" - return { - "alarmsystems": alarm_system_payload, - "config": config_payload, - "groups": group_payload, - "lights": light_payload, - "sensors": sensor_payload, - } +def fixture_data() -> dict[str, Any]: + """Combine multiple payloads with one fixture.""" + return {} @pytest.fixture(name="alarm_system_payload") diff --git a/tests/components/deconz/test_button.py b/tests/components/deconz/test_button.py index 4d85270ddca..1ddcbd8f105 100644 --- a/tests/components/deconz/test_button.py +++ b/tests/components/deconz/test_button.py @@ -1,31 +1,17 @@ """deCONZ button platform tests.""" -from unittest.mock import patch +from collections.abc import Callable import pytest from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from .test_gateway import ( - DECONZ_WEB_REQUEST, - mock_deconz_put_request, - setup_deconz_integration, -) - from tests.test_util.aiohttp import AiohttpClientMocker - -async def test_no_binary_sensors( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that no sensors in deconz results in no sensor entities.""" - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 - - TEST_DATA = [ ( # Store scene button { @@ -100,19 +86,17 @@ TEST_DATA = [ ] -@pytest.mark.parametrize(("raw_data", "expected"), TEST_DATA) +@pytest.mark.parametrize(("deconz_payload", "expected"), TEST_DATA) async def test_button( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, aioclient_mock: AiohttpClientMocker, - raw_data, + config_entry_setup: ConfigEntry, + mock_put_request: Callable[[str, str], AiohttpClientMocker], expected, ) -> None: """Test successful creation of button entities.""" - with patch.dict(DECONZ_WEB_REQUEST, raw_data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == expected["entity_count"] # Verify state data @@ -129,13 +113,17 @@ async def test_button( # Verify device registry data assert ( - len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + len( + dr.async_entries_for_config_entry( + device_registry, config_entry_setup.entry_id + ) + ) == expected["device_count"] ) # Verify button press - mock_deconz_put_request(aioclient_mock, config_entry.data, expected["request"]) + aioclient_mock = mock_put_request(expected["request"]) await hass.services.async_call( BUTTON_DOMAIN, @@ -147,11 +135,11 @@ async def test_button( # Unload entry - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE # Remove entry - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 From 90d622cd02a969e41c8dd94ce64f4a16b9454076 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 2 Jul 2024 08:23:07 +0200 Subject: [PATCH 0588/2411] Minor polishing for tplink (#120868) --- homeassistant/components/tplink/climate.py | 11 ++++--- homeassistant/components/tplink/entity.py | 24 +++++++------- homeassistant/components/tplink/fan.py | 3 +- homeassistant/components/tplink/light.py | 32 +++++++++---------- homeassistant/components/tplink/sensor.py | 18 +---------- homeassistant/components/tplink/switch.py | 19 +---------- .../components/tplink/fixtures/features.json | 2 +- .../tplink/snapshots/test_sensor.ambr | 4 +-- tests/components/tplink/test_light.py | 16 ++++++---- 9 files changed, 51 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/tplink/climate.py b/homeassistant/components/tplink/climate.py index 99a8c43fac3..3bd6aba5c26 100644 --- a/homeassistant/components/tplink/climate.py +++ b/homeassistant/components/tplink/climate.py @@ -77,16 +77,17 @@ class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity): parent: Device, ) -> None: """Initialize the climate entity.""" - super().__init__(device, coordinator, parent=parent) - self._state_feature = self._device.features["state"] - self._mode_feature = self._device.features["thermostat_mode"] - self._temp_feature = self._device.features["temperature"] - self._target_feature = self._device.features["target_temperature"] + self._state_feature = device.features["state"] + self._mode_feature = device.features["thermostat_mode"] + self._temp_feature = device.features["temperature"] + self._target_feature = device.features["target_temperature"] self._attr_min_temp = self._target_feature.minimum_value self._attr_max_temp = self._target_feature.maximum_value self._attr_temperature_unit = UNIT_MAPPING[cast(str, self._temp_feature.unit)] + super().__init__(device, coordinator, parent=parent) + @async_refresh_after async def async_set_temperature(self, **kwargs: Any) -> None: """Set target temperature.""" diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 4e8ec0e0779..4ec0480cf82 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -56,15 +56,21 @@ DEVICETYPES_WITH_SPECIALIZED_PLATFORMS = { DeviceType.Thermostat, } +# Primary features to always include even when the device type has its own platform +FEATURES_ALLOW_LIST = { + # lights have current_consumption and a specialized platform + "current_consumption" +} + + # Features excluded due to future platform additions EXCLUDED_FEATURES = { # update "current_firmware_version", "available_firmware_version", - # fan - "fan_speed_level", } + LEGACY_KEY_MAPPING = { "current": ATTR_CURRENT_A, "current_consumption": ATTR_CURRENT_POWER_W, @@ -179,15 +185,12 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB self._attr_unique_id = self._get_unique_id() + self._async_call_update_attrs() + def _get_unique_id(self) -> str: """Return unique ID for the entity.""" return legacy_device_id(self._device) - async def async_added_to_hass(self) -> None: - """Handle being added to hass.""" - self._async_call_update_attrs() - return await super().async_added_to_hass() - @abstractmethod @callback def _async_update_attrs(self) -> None: @@ -196,11 +199,7 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB @callback def _async_call_update_attrs(self) -> None: - """Call update_attrs and make entity unavailable on error. - - update_attrs can sometimes fail if a device firmware update breaks the - downstream library. - """ + """Call update_attrs and make entity unavailable on errors.""" try: self._async_update_attrs() except Exception as ex: # noqa: BLE001 @@ -358,6 +357,7 @@ class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC): and ( feat.category is not Feature.Category.Primary or device.device_type not in DEVICETYPES_WITH_SPECIALIZED_PLATFORMS + or feat.id in FEATURES_ALLOW_LIST ) and ( desc := cls._description_for_feature( diff --git a/homeassistant/components/tplink/fan.py b/homeassistant/components/tplink/fan.py index 947a9072329..292240bca94 100644 --- a/homeassistant/components/tplink/fan.py +++ b/homeassistant/components/tplink/fan.py @@ -69,11 +69,12 @@ class TPLinkFanEntity(CoordinatedTPLinkEntity, FanEntity): parent: Device | None = None, ) -> None: """Initialize the fan.""" - super().__init__(device, coordinator, parent=parent) self.fan_module = fan_module # If _attr_name is None the entity name will be the device name self._attr_name = None if parent is None else device.alias + super().__init__(device, coordinator, parent=parent) + @async_refresh_after async def async_turn_on( self, diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 633648bbf23..a736a0ba1e1 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -140,9 +140,7 @@ async def async_setup_entry( parent_coordinator = data.parent_coordinator device = parent_coordinator.device entities: list[TPLinkLightEntity | TPLinkLightEffectEntity] = [] - if ( - effect_module := device.modules.get(Module.LightEffect) - ) and effect_module.has_custom_effects: + if effect_module := device.modules.get(Module.LightEffect): entities.append( TPLinkLightEffectEntity( device, @@ -151,17 +149,18 @@ async def async_setup_entry( effect_module=effect_module, ) ) - platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service( - SERVICE_RANDOM_EFFECT, - RANDOM_EFFECT_DICT, - "async_set_random_effect", - ) - platform.async_register_entity_service( - SERVICE_SEQUENCE_EFFECT, - SEQUENCE_EFFECT_DICT, - "async_set_sequence_effect", - ) + if effect_module.has_custom_effects: + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_RANDOM_EFFECT, + RANDOM_EFFECT_DICT, + "async_set_random_effect", + ) + platform.async_register_entity_service( + SERVICE_SEQUENCE_EFFECT, + SEQUENCE_EFFECT_DICT, + "async_set_sequence_effect", + ) elif Module.Light in device.modules: entities.append( TPLinkLightEntity( @@ -197,7 +196,6 @@ class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity): ) -> None: """Initialize the light.""" self._parent = parent - super().__init__(device, coordinator, parent=parent) self._light_module = light_module # If _attr_name is None the entity name will be the device name self._attr_name = None if parent is None else device.alias @@ -215,7 +213,8 @@ class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity): if len(self._attr_supported_color_modes) == 1: # If the light supports only a single color mode, set it now self._fixed_color_mode = next(iter(self._attr_supported_color_modes)) - self._async_call_update_attrs() + + super().__init__(device, coordinator, parent=parent) def _get_unique_id(self) -> str: """Return unique ID for the entity.""" @@ -371,6 +370,7 @@ class TPLinkLightEffectEntity(TPLinkLightEntity): effect_module = self._effect_module if effect_module.effect != LightEffect.LIGHT_EFFECTS_OFF: self._attr_effect = effect_module.effect + self._attr_color_mode = ColorMode.BRIGHTNESS else: self._attr_effect = EFFECT_OFF if effect_list := effect_module.effect_list: diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 474ee6bfacf..3da414d74d3 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import cast -from kasa import Device, Feature +from kasa import Feature from homeassistant.components.sensor import ( SensorDeviceClass, @@ -18,7 +18,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TPLinkConfigEntry from .const import UNIT_MAPPING -from .coordinator import TPLinkDataUpdateCoordinator from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription @@ -144,21 +143,6 @@ class TPLinkSensorEntity(CoordinatedTPLinkFeatureEntity, SensorEntity): entity_description: TPLinkSensorEntityDescription - def __init__( - self, - device: Device, - coordinator: TPLinkDataUpdateCoordinator, - *, - feature: Feature, - description: TPLinkSensorEntityDescription, - parent: Device | None = None, - ) -> None: - """Initialize the sensor.""" - super().__init__( - device, coordinator, description=description, feature=feature, parent=parent - ) - self._async_call_update_attrs() - @callback def _async_update_attrs(self) -> None: """Update the entity's attributes.""" diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 2520de9dd3e..62957d48ac4 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -6,14 +6,13 @@ from dataclasses import dataclass import logging from typing import Any -from kasa import Device, Feature +from kasa import Feature from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TPLinkConfigEntry -from .coordinator import TPLinkDataUpdateCoordinator from .entity import ( CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription, @@ -80,22 +79,6 @@ class TPLinkSwitch(CoordinatedTPLinkFeatureEntity, SwitchEntity): entity_description: TPLinkSwitchEntityDescription - def __init__( - self, - device: Device, - coordinator: TPLinkDataUpdateCoordinator, - *, - feature: Feature, - description: TPLinkSwitchEntityDescription, - parent: Device | None = None, - ) -> None: - """Initialize the switch.""" - super().__init__( - device, coordinator, description=description, feature=feature, parent=parent - ) - - self._async_call_update_attrs() - @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" diff --git a/tests/components/tplink/fixtures/features.json b/tests/components/tplink/fixtures/features.json index daf86a74643..7cfe979ea25 100644 --- a/tests/components/tplink/fixtures/features.json +++ b/tests/components/tplink/fixtures/features.json @@ -73,7 +73,7 @@ "value": 121.1, "type": "Sensor", "category": "Primary", - "unit": "v", + "unit": "V", "precision_hint": 1 }, "device_id": { diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index 46fe897500f..9ea22af45fd 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -770,7 +770,7 @@ 'supported_features': 0, 'translation_key': 'voltage', 'unique_id': '123456789ABCDEFGH_voltage', - 'unit_of_measurement': 'v', + 'unit_of_measurement': 'V', }) # --- # name: test_states[sensor.my_device_voltage-state] @@ -779,7 +779,7 @@ 'device_class': 'voltage', 'friendly_name': 'my_device Voltage', 'state_class': , - 'unit_of_measurement': 'v', + 'unit_of_measurement': 'V', }), 'context': , 'entity_id': 'sensor.my_device_voltage', diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index c2f40f47e3d..6fce04ec454 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -140,13 +140,17 @@ async def test_color_light( assert state.state == "on" attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 - assert attributes[ATTR_COLOR_MODE] == "hs" assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"] - assert attributes[ATTR_MIN_MIREDS] == 111 - assert attributes[ATTR_MAX_MIREDS] == 250 - assert attributes[ATTR_HS_COLOR] == (10, 30) - assert attributes[ATTR_RGB_COLOR] == (255, 191, 178) - assert attributes[ATTR_XY_COLOR] == (0.42, 0.336) + # If effect is active, only the brightness can be controlled + if attributes.get(ATTR_EFFECT) is not None: + assert attributes[ATTR_COLOR_MODE] == "brightness" + else: + assert attributes[ATTR_COLOR_MODE] == "hs" + assert attributes[ATTR_MIN_MIREDS] == 111 + assert attributes[ATTR_MAX_MIREDS] == 250 + assert attributes[ATTR_HS_COLOR] == (10, 30) + assert attributes[ATTR_RGB_COLOR] == (255, 191, 178) + assert attributes[ATTR_XY_COLOR] == (0.42, 0.336) await hass.services.async_call( LIGHT_DOMAIN, "turn_off", BASE_PAYLOAD, blocking=True From 2635573bbcbf47ee7b7765d471f1d7a36359dfde Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Jul 2024 08:23:31 +0200 Subject: [PATCH 0589/2411] Bump airgradient to 0.6.1 (#120962) --- homeassistant/components/airgradient/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index 7b892c4658a..d523aa4ca03 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/airgradient", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["airgradient==0.6.0"], + "requirements": ["airgradient==0.6.1"], "zeroconf": ["_airgradient._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 8781c83f901..d05d5efc27c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ aiowithings==3.0.2 aioymaps==1.2.4 # homeassistant.components.airgradient -airgradient==0.6.0 +airgradient==0.6.1 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eee7bd70159..40a3525964e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -386,7 +386,7 @@ aiowithings==3.0.2 aioymaps==1.2.4 # homeassistant.components.airgradient -airgradient==0.6.0 +airgradient==0.6.1 # homeassistant.components.airly airly==1.1.0 From 2d054fb5dfae86654d4f5a6ce1f1538d671c6998 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 2 Jul 2024 09:25:33 +0200 Subject: [PATCH 0590/2411] Bump reolink-aio to 0.9.4 (#120964) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 172a43a91b3..ee3ebe8a13a 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.9.3"] + "requirements": ["reolink-aio==0.9.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index d05d5efc27c..3d7276393c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2460,7 +2460,7 @@ renault-api==0.2.4 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.3 +reolink-aio==0.9.4 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40a3525964e..f6e24d2efae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1924,7 +1924,7 @@ renault-api==0.2.4 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.3 +reolink-aio==0.9.4 # homeassistant.components.rflink rflink==0.0.66 From 07d80d5ad9e5febf435913d004a82e010e7a68db Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 10:17:41 +0200 Subject: [PATCH 0591/2411] Use service_calls fixture in netatmo tests (#120986) --- .../components/netatmo/test_device_trigger.py | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/tests/components/netatmo/test_device_trigger.py b/tests/components/netatmo/test_device_trigger.py index ad1e9bd8cb9..99709572024 100644 --- a/tests/components/netatmo/test_device_trigger.py +++ b/tests/components/netatmo/test_device_trigger.py @@ -22,16 +22,9 @@ from tests.common import ( MockConfigEntry, async_capture_events, async_get_device_automations, - async_mock_service, ) -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.mark.parametrize( ("platform", "device_type", "event_types"), [ @@ -113,7 +106,7 @@ async def test_get_triggers( ) async def test_if_fires_on_event( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, platform, @@ -175,8 +168,8 @@ async def test_if_fires_on_event( ) await hass.async_block_till_done() assert len(events) == 1 - assert len(calls) == 1 - assert calls[0].data["some"] == f"{event_type} - device - {device.id}" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == f"{event_type} - device - {device.id}" @pytest.mark.parametrize( @@ -196,7 +189,7 @@ async def test_if_fires_on_event( ) async def test_if_fires_on_event_legacy( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, platform, @@ -258,8 +251,8 @@ async def test_if_fires_on_event_legacy( ) await hass.async_block_till_done() assert len(events) == 1 - assert len(calls) == 1 - assert calls[0].data["some"] == f"{event_type} - device - {device.id}" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == f"{event_type} - device - {device.id}" @pytest.mark.parametrize( @@ -275,7 +268,7 @@ async def test_if_fires_on_event_legacy( ) async def test_if_fires_on_event_with_subtype( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, platform, @@ -343,8 +336,11 @@ async def test_if_fires_on_event_with_subtype( ) await hass.async_block_till_done() assert len(events) == 1 - assert len(calls) == 1 - assert calls[0].data["some"] == f"{event_type} - {sub_type} - device - {device.id}" + assert len(service_calls) == 1 + assert ( + service_calls[0].data["some"] + == f"{event_type} - {sub_type} - device - {device.id}" + ) @pytest.mark.parametrize( From 3df3e6d0816c4529efe9a4578259f4e9e9ed5baa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 10:36:50 +0200 Subject: [PATCH 0592/2411] Use service_calls fixture in shelly tests (#120991) --- tests/components/shelly/conftest.py | 10 ++---- .../components/shelly/test_device_trigger.py | 36 +++++++++---------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index a16cc62fbae..65ebdeb6996 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -11,11 +11,11 @@ from homeassistant.components.shelly.const import ( EVENT_SHELLY_CLICK, REST_SENSORS_UPDATE_INTERVAL, ) -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant from . import MOCK_MAC -from tests.common import async_capture_events, async_mock_service +from tests.common import async_capture_events MOCK_SETTINGS = { "name": "Test name", @@ -290,12 +290,6 @@ def mock_ws_server(): yield -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.fixture def events(hass: HomeAssistant): """Yield caught shelly_click events.""" diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index d47cca17460..fb68393304b 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -178,7 +178,7 @@ async def test_get_triggers_for_invalid_device_id( async def test_if_fires_on_click_event_block_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_block_device: Mock, ) -> None: """Test for click_event trigger firing for block device.""" @@ -215,14 +215,14 @@ async def test_if_fires_on_click_event_block_device( hass.bus.async_fire(EVENT_SHELLY_CLICK, message) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test_trigger_single_click" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test_trigger_single_click" async def test_if_fires_on_click_event_rpc_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_rpc_device: Mock, ) -> None: """Test for click_event trigger firing for rpc device.""" @@ -259,14 +259,14 @@ async def test_if_fires_on_click_event_rpc_device( hass.bus.async_fire(EVENT_SHELLY_CLICK, message) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test_trigger_single_push" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test_trigger_single_push" async def test_validate_trigger_block_device_not_ready( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -304,14 +304,14 @@ async def test_validate_trigger_block_device_not_ready( hass.bus.async_fire(EVENT_SHELLY_CLICK, message) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test_trigger_single_click" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test_trigger_single_click" async def test_validate_trigger_rpc_device_not_ready( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -349,8 +349,8 @@ async def test_validate_trigger_rpc_device_not_ready( hass.bus.async_fire(EVENT_SHELLY_CLICK, message) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test_trigger_single_push" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test_trigger_single_push" async def test_validate_trigger_invalid_triggers( @@ -391,7 +391,7 @@ async def test_validate_trigger_invalid_triggers( async def test_rpc_no_runtime_data( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -429,14 +429,14 @@ async def test_rpc_no_runtime_data( hass.bus.async_fire(EVENT_SHELLY_CLICK, message) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test_trigger_single_push" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test_trigger_single_push" async def test_block_no_runtime_data( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -474,5 +474,5 @@ async def test_block_no_runtime_data( hass.bus.async_fire(EVENT_SHELLY_CLICK, message) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test_trigger_single" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test_trigger_single" From fac511aa464da672caf0d5ef6a8bbbce018d7e84 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 10:37:14 +0200 Subject: [PATCH 0593/2411] Use service_calls fixture in samsungtv tests (#120992) --- tests/components/samsungtv/conftest.py | 9 --------- .../samsungtv/test_device_trigger.py | 14 +++++++------ tests/components/samsungtv/test_trigger.py | 20 +++++++++---------- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 15794440343..60cefa4959c 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -21,13 +21,10 @@ from samsungtvws.exceptions import ResponseError from samsungtvws.remote import ChannelEmitCommand from homeassistant.components.samsungtv.const import WEBSOCKET_SSL_PORT -from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.util.dt as dt_util from .const import SAMPLE_DEVICE_INFO_UE48JU6400, SAMPLE_DEVICE_INFO_WIFI -from tests.common import async_mock_service - @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: @@ -299,9 +296,3 @@ def mac_address_fixture() -> Mock: """Patch getmac.get_mac_address.""" with patch("getmac.get_mac_address", return_value=None) as mac: yield mac - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") diff --git a/tests/components/samsungtv/test_device_trigger.py b/tests/components/samsungtv/test_device_trigger.py index e16ea718cbb..acc7ecb904d 100644 --- a/tests/components/samsungtv/test_device_trigger.py +++ b/tests/components/samsungtv/test_device_trigger.py @@ -45,7 +45,9 @@ async def test_get_triggers( @pytest.mark.usefixtures("remoteencws", "rest_api") async def test_if_fires_on_turn_on_request( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, calls: list[ServiceCall] + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) @@ -95,11 +97,11 @@ async def test_if_fires_on_turn_on_request( ) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[0].data["some"] == device.id - assert calls[0].data["id"] == 0 - assert calls[1].data["some"] == entity_id - assert calls[1].data["id"] == 0 + assert len(service_calls) == 3 + assert service_calls[1].data["some"] == device.id + assert service_calls[1].data["id"] == 0 + assert service_calls[2].data["some"] == entity_id + assert service_calls[2].data["id"] == 0 @pytest.mark.usefixtures("remoteencws", "rest_api") diff --git a/tests/components/samsungtv/test_trigger.py b/tests/components/samsungtv/test_trigger.py index 6607c60b8e8..8076ceb2807 100644 --- a/tests/components/samsungtv/test_trigger.py +++ b/tests/components/samsungtv/test_trigger.py @@ -21,7 +21,7 @@ from tests.common import MockEntity, MockEntityPlatform @pytest.mark.parametrize("entity_domain", ["media_player", "remote"]) async def test_turn_on_trigger_device_id( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, entity_domain: str, ) -> None: @@ -60,14 +60,14 @@ async def test_turn_on_trigger_device_id( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == device.id - assert calls[0].data["id"] == 0 + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == device.id + assert service_calls[1].data["id"] == 0 with patch("homeassistant.config.load_yaml_dict", return_value={}): await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) - calls.clear() + service_calls.clear() # Ensure WOL backup is called when trigger not present with patch( @@ -78,14 +78,14 @@ async def test_turn_on_trigger_device_id( ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 1 mock_send_magic_packet.assert_called() @pytest.mark.usefixtures("remoteencws", "rest_api") @pytest.mark.parametrize("entity_domain", ["media_player", "remote"]) async def test_turn_on_trigger_entity_id( - hass: HomeAssistant, calls: list[ServiceCall], entity_domain: str + hass: HomeAssistant, service_calls: list[ServiceCall], entity_domain: str ) -> None: """Test for turn_on triggers by entity_id firing.""" await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) @@ -119,9 +119,9 @@ async def test_turn_on_trigger_entity_id( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == entity_id - assert calls[0].data["id"] == 0 + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == entity_id + assert service_calls[1].data["id"] == 0 @pytest.mark.usefixtures("remoteencws", "rest_api") From e3516be3e3c8387e8dec11cf6c4f45af103bb7f7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:23:46 +0200 Subject: [PATCH 0594/2411] Use service_calls fixture in mqtt tests (#120984) --- tests/components/mqtt/test_device_trigger.py | 110 +++++++++---------- tests/components/mqtt/test_trigger.py | 55 +++++----- 2 files changed, 77 insertions(+), 88 deletions(-) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index ce75bd01a03..10322dd9046 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -17,11 +17,7 @@ from homeassistant.setup import async_setup_component from .test_common import help_test_unload_config_entry -from tests.common import ( - async_fire_mqtt_message, - async_get_device_automations, - async_mock_service, -) +from tests.common import async_fire_mqtt_message, async_get_device_automations from tests.typing import MqttMockHAClient, MqttMockHAClientGenerator, WebSocketGenerator @@ -30,12 +26,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -284,7 +274,7 @@ async def test_update_remove_triggers( async def test_if_fires_on_mqtt_message( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test triggers firing.""" @@ -350,20 +340,20 @@ async def test_if_fires_on_mqtt_message( # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "short_press" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "short_press" # Fake long press. async_fire_mqtt_message(hass, "foobar/triggers/button2", "long_press") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "long_press" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "long_press" async def test_if_discovery_id_is_prefered( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test if discovery is preferred over referencing by type/subtype. @@ -437,21 +427,21 @@ async def test_if_discovery_id_is_prefered( # Fake short press, matching on type and subtype async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "short_press" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "short_press" # Fake long press, matching on discovery_id - calls.clear() + service_calls.clear() async_fire_mqtt_message(hass, "foobar/triggers/button1", "long_press") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "long_press" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "long_press" async def test_non_unique_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, ) -> None: @@ -528,20 +518,20 @@ async def test_non_unique_triggers( # and triggers both attached instances. async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 2 - all_calls = {calls[0].data["some"], calls[1].data["some"]} + assert len(service_calls) == 2 + all_calls = {service_calls[0].data["some"], service_calls[1].data["some"]} assert all_calls == {"press1", "press2"} # Trigger second config references to same trigger # and triggers both attached instances. async_fire_mqtt_message(hass, "foobar/triggers/button2", "long_press") await hass.async_block_till_done() - assert len(calls) == 2 - all_calls = {calls[0].data["some"], calls[1].data["some"]} + assert len(service_calls) == 2 + all_calls = {service_calls[0].data["some"], service_calls[1].data["some"]} assert all_calls == {"press1", "press2"} # Removing the first trigger will clean up - calls.clear() + service_calls.clear() async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", "") await hass.async_block_till_done() await hass.async_block_till_done() @@ -549,13 +539,13 @@ async def test_non_unique_triggers( "Device trigger ('device_automation', 'bla1') has been removed" in caplog.text ) async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_if_fires_on_mqtt_message_template( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test triggers firing with a message template and a shared topic.""" @@ -623,20 +613,20 @@ async def test_if_fires_on_mqtt_message_template( # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button4", '{"button":"short_press"}') await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "short_press" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "short_press" # Fake long press. async_fire_mqtt_message(hass, "foobar/triggers/button4", '{"button":"long_press"}') await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "long_press" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "long_press" async def test_if_fires_on_mqtt_message_late_discover( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test triggers firing of MQTT device triggers discovered after setup.""" @@ -710,20 +700,20 @@ async def test_if_fires_on_mqtt_message_late_discover( # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "short_press" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "short_press" # Fake long press. async_fire_mqtt_message(hass, "foobar/triggers/button2", "long_press") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "long_press" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "long_press" async def test_if_fires_on_mqtt_message_after_update( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, ) -> None: @@ -782,7 +772,7 @@ async def test_if_fires_on_mqtt_message_after_update( # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Update the trigger with existing type/subtype change async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data1) @@ -793,29 +783,29 @@ async def test_if_fires_on_mqtt_message_after_update( async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data3) await hass.async_block_till_done() - calls.clear() + service_calls.clear() async_fire_mqtt_message(hass, "foobar/triggers/button1", "") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 - calls.clear() + service_calls.clear() async_fire_mqtt_message(hass, "foobar/triggers/buttonOne", "") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Update the trigger with same topic async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data3) await hass.async_block_till_done() - calls.clear() + service_calls.clear() async_fire_mqtt_message(hass, "foobar/triggers/button1", "") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 - calls.clear() + service_calls.clear() async_fire_mqtt_message(hass, "foobar/triggers/buttonOne", "") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_no_resubscribe_same_topic( @@ -868,7 +858,7 @@ async def test_no_resubscribe_same_topic( async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test triggers not firing after removal.""" @@ -911,7 +901,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Remove the trigger async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", "") @@ -919,7 +909,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Rediscover the trigger async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) @@ -927,14 +917,14 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 async def test_not_fires_on_mqtt_message_after_remove_from_registry( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Test triggers not firing after removal.""" @@ -982,7 +972,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Remove MQTT from the device mqtt_config_entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -994,7 +984,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_attach_remove( @@ -1684,7 +1674,7 @@ async def test_trigger_debug_info( async def test_unload_entry( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, mqtt_mock: MqttMockHAClient, ) -> None: @@ -1727,7 +1717,7 @@ async def test_unload_entry( # Fake short press 1 async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await help_test_unload_config_entry(hass) @@ -1736,7 +1726,7 @@ async def test_unload_entry( await hass.async_block_till_done() async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Start entry again mqtt_entry = hass.config_entries.async_entries("mqtt")[0] @@ -1747,4 +1737,4 @@ async def test_unload_entry( await hass.async_block_till_done() async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index 2e0506a02ab..5bf36849b13 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -9,7 +9,7 @@ from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_O from homeassistant.core import HassJobType, HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component -from tests.common import async_fire_mqtt_message, async_mock_service, mock_component +from tests.common import async_fire_mqtt_message, mock_component from tests.typing import MqttMockHAClient, MqttMockHAClientGenerator @@ -18,12 +18,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.fixture(autouse=True) async def setup_comp( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator @@ -34,7 +28,7 @@ async def setup_comp( async def test_if_fires_on_topic_match( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test if message is fired on topic match.""" assert await async_setup_component( @@ -57,9 +51,10 @@ async def test_if_fires_on_topic_match( async_fire_mqtt_message(hass, "test-topic", '{ "hello": "world" }') await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] == 'mqtt - test-topic - { "hello": "world" } - world - 0' + service_calls[0].data["some"] + == 'mqtt - test-topic - { "hello": "world" } - world - 0' ) await hass.services.async_call( @@ -68,13 +63,15 @@ async def test_if_fires_on_topic_match( {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 2 + async_fire_mqtt_message(hass, "test-topic", "test_payload") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 2 async def test_if_fires_on_topic_and_payload_match( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test if message is fired on topic and payload match.""" assert await async_setup_component( @@ -94,11 +91,11 @@ async def test_if_fires_on_topic_and_payload_match( async_fire_mqtt_message(hass, "test-topic", "hello") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_topic_and_payload_match2( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test if message is fired on topic and payload match. @@ -121,11 +118,11 @@ async def test_if_fires_on_topic_and_payload_match2( async_fire_mqtt_message(hass, "test-topic", "0") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_templated_topic_and_payload_match( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test if message is fired on templated topic and payload match.""" assert await async_setup_component( @@ -145,19 +142,19 @@ async def test_if_fires_on_templated_topic_and_payload_match( async_fire_mqtt_message(hass, "test-topic-", "foo") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_mqtt_message(hass, "test-topic-4", "foo") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_mqtt_message(hass, "test-topic-4", "bar") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_fires_on_payload_template( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test if message is fired on templated topic and payload match.""" assert await async_setup_component( @@ -178,19 +175,21 @@ async def test_if_fires_on_payload_template( async_fire_mqtt_message(hass, "test-topic", "hello") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_mqtt_message(hass, "test-topic", '{"unwanted_key":"hello"}') await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_mqtt_message(hass, "test-topic", '{"wanted_key":"hello"}') await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_non_allowed_templates( - hass: HomeAssistant, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + service_calls: list[ServiceCall], + caplog: pytest.LogCaptureFixture, ) -> None: """Test non allowed function in template.""" assert await async_setup_component( @@ -214,7 +213,7 @@ async def test_non_allowed_templates( async def test_if_not_fires_on_topic_but_no_payload_match( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test if message is not fired on topic but no payload.""" assert await async_setup_component( @@ -234,11 +233,11 @@ async def test_if_not_fires_on_topic_but_no_payload_match( async_fire_mqtt_message(hass, "test-topic", "no-hello") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_encoding_default( - hass: HomeAssistant, calls: list[ServiceCall], setup_comp + hass: HomeAssistant, service_calls: list[ServiceCall], setup_comp ) -> None: """Test default encoding.""" assert await async_setup_component( @@ -258,7 +257,7 @@ async def test_encoding_default( async def test_encoding_custom( - hass: HomeAssistant, calls: list[ServiceCall], setup_comp + hass: HomeAssistant, service_calls: list[ServiceCall], setup_comp ) -> None: """Test default encoding.""" assert await async_setup_component( From 9ca9377cade8496c560290f5346a4a4dce0857a7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:26:25 +0200 Subject: [PATCH 0595/2411] Use common registry fixtures in hue (#121003) --- tests/components/hue/conftest.py | 8 +------- tests/components/hue/test_device_trigger_v1.py | 10 +++++----- tests/components/hue/test_device_trigger_v2.py | 6 +++--- tests/components/hue/test_sensor_v1.py | 13 ++++++++----- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index 43a3b1518b7..dd4fc7f3d7a 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -20,7 +20,7 @@ from homeassistant.setup import async_setup_component from .const import FAKE_BRIDGE, FAKE_BRIDGE_DEVICE -from tests.common import MockConfigEntry, load_fixture, mock_device_registry +from tests.common import MockConfigEntry, load_fixture @pytest.fixture(autouse=True) @@ -276,9 +276,3 @@ async def setup_platform( # and make sure it completes before going further await hass.async_block_till_done() - - -@pytest.fixture(name="device_reg") -def get_device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) diff --git a/tests/components/hue/test_device_trigger_v1.py b/tests/components/hue/test_device_trigger_v1.py index facd267cad9..c9334052aa6 100644 --- a/tests/components/hue/test_device_trigger_v1.py +++ b/tests/components/hue/test_device_trigger_v1.py @@ -21,7 +21,7 @@ async def test_get_triggers( hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_bridge_v1, - device_reg: dr.DeviceRegistry, + device_registry: dr.DeviceRegistry, ) -> None: """Test we get the expected triggers from a hue remote.""" mock_bridge_v1.mock_sensor_responses.append(REMOTES_RESPONSE) @@ -32,7 +32,7 @@ async def test_get_triggers( assert len(hass.states.async_all()) == 1 # Get triggers for specific tap switch - hue_tap_device = device_reg.async_get_device( + hue_tap_device = device_registry.async_get_device( identifiers={(hue.DOMAIN, "00:00:00:00:00:44:23:08")} ) triggers = await async_get_device_automations( @@ -53,7 +53,7 @@ async def test_get_triggers( assert triggers == unordered(expected_triggers) # Get triggers for specific dimmer switch - hue_dimmer_device = device_reg.async_get_device( + hue_dimmer_device = device_registry.async_get_device( identifiers={(hue.DOMAIN, "00:17:88:01:10:3e:3a:dc")} ) hue_bat_sensor = entity_registry.async_get( @@ -91,7 +91,7 @@ async def test_get_triggers( async def test_if_fires_on_state_change( hass: HomeAssistant, mock_bridge_v1, - device_reg: dr.DeviceRegistry, + device_registry: dr.DeviceRegistry, service_calls: list[ServiceCall], ) -> None: """Test for button press trigger firing.""" @@ -101,7 +101,7 @@ async def test_if_fires_on_state_change( assert len(hass.states.async_all()) == 1 # Set an automation with a specific tap switch trigger - hue_tap_device = device_reg.async_get_device( + hue_tap_device = device_registry.async_get_device( identifiers={(hue.DOMAIN, "00:00:00:00:00:44:23:08")} ) assert await async_setup_component( diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index 0a89b3263c7..0c1ed749ca4 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -8,7 +8,7 @@ from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.hue.v2.device import async_setup_devices from homeassistant.components.hue.v2.hue_event import async_setup_hue_events 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 .conftest import setup_platform @@ -50,14 +50,14 @@ async def test_get_triggers( entity_registry: er.EntityRegistry, mock_bridge_v2, v2_resources_test_data, - device_reg, + device_registry: dr.DeviceRegistry, ) -> None: """Test we get the expected triggers from a hue remote.""" await mock_bridge_v2.api.load_test_data(v2_resources_test_data) await setup_platform(hass, mock_bridge_v2, ["binary_sensor", "sensor"]) # Get triggers for `Wall switch with 2 controls` - hue_wall_switch_device = device_reg.async_get_device( + hue_wall_switch_device = device_registry.async_get_device( identifiers={(hue.DOMAIN, "3ff06175-29e8-44a8-8fe7-af591b0025da")} ) hue_bat_sensor = entity_registry.async_get( diff --git a/tests/components/hue/test_sensor_v1.py b/tests/components/hue/test_sensor_v1.py index b1ef94f8ed0..7406d7e27e4 100644 --- a/tests/components/hue/test_sensor_v1.py +++ b/tests/components/hue/test_sensor_v1.py @@ -10,7 +10,7 @@ from homeassistant.components.hue.const import ATTR_HUE_EVENT from homeassistant.components.hue.v1 import sensor_base from homeassistant.const import EntityCategory 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 .conftest import create_mock_bridge, setup_platform @@ -452,7 +452,10 @@ async def test_update_unauthorized(hass: HomeAssistant, mock_bridge_v1) -> None: async def test_hue_events( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_bridge_v1, device_reg + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_bridge_v1, + device_registry: dr.DeviceRegistry, ) -> None: """Test that hue remotes fire events when pressed.""" mock_bridge_v1.mock_sensor_responses.append(SENSOR_RESPONSE) @@ -464,7 +467,7 @@ async def test_hue_events( assert len(hass.states.async_all()) == 7 assert len(events) == 0 - hue_tap_device = device_reg.async_get_device( + hue_tap_device = device_registry.async_get_device( identifiers={(hue.DOMAIN, "00:00:00:00:00:44:23:08")} ) @@ -495,7 +498,7 @@ async def test_hue_events( "last_updated": "2019-12-28T22:58:03", } - hue_dimmer_device = device_reg.async_get_device( + hue_dimmer_device = device_registry.async_get_device( identifiers={(hue.DOMAIN, "00:17:88:01:10:3e:3a:dc")} ) @@ -594,7 +597,7 @@ async def test_hue_events( async_fire_time_changed(hass) await hass.async_block_till_done() - hue_aurora_device = device_reg.async_get_device( + hue_aurora_device = device_registry.async_get_device( identifiers={(hue.DOMAIN, "ff:ff:00:0f:e7:fd:bc:b7")} ) From 71b7ee40e5cebf5755dee9a8bc2f25bbd2f422f8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:27:15 +0200 Subject: [PATCH 0596/2411] Use common registry fixtures in tplink (#121002) --- tests/components/tplink/conftest.py | 14 +------------- tests/components/tplink/test_init.py | 8 ++++---- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/tests/components/tplink/conftest.py b/tests/components/tplink/conftest.py index ad7c85fa728..661a9c24e3d 100644 --- a/tests/components/tplink/conftest.py +++ b/tests/components/tplink/conftest.py @@ -20,7 +20,7 @@ from . import ( _mocked_device, ) -from tests.common import MockConfigEntry, mock_device_registry, mock_registry +from tests.common import MockConfigEntry @pytest.fixture @@ -72,18 +72,6 @@ def mock_connect(): yield {"connect": mock_connect, "mock_devices": devices} -@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) - - @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index c5c5e2ce6db..fa634cda6e6 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -107,7 +107,7 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( - hass: HomeAssistant, entity_reg: er.EntityRegistry + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test no migration happens if the original entity id still exists.""" config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) @@ -115,14 +115,14 @@ async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( dimmer = _mocked_device(alias="My dimmer", modules=[Module.Light]) rollout_unique_id = MAC_ADDRESS.replace(":", "").upper() original_unique_id = tplink.legacy_device_id(dimmer) - original_dimmer_entity_reg = entity_reg.async_get_or_create( + original_dimmer_entity_reg = entity_registry.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="light", unique_id=original_unique_id, original_name="Original dimmer", ) - rollout_dimmer_entity_reg = entity_reg.async_get_or_create( + rollout_dimmer_entity_reg = entity_registry.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="light", @@ -138,7 +138,7 @@ async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done(wait_background_tasks=True) - migrated_dimmer_entity_reg = entity_reg.async_get_or_create( + migrated_dimmer_entity_reg = entity_registry.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="light", From b13e78f3a3123ea49e96fba91d47bbb409680253 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:27:54 +0200 Subject: [PATCH 0597/2411] Use service_calls fixture in microsoft tests (#120983) --- tests/components/microsoft/test_tts.py | 66 +++++++++++++------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/tests/components/microsoft/test_tts.py b/tests/components/microsoft/test_tts.py index 082def901c5..dca760230ac 100644 --- a/tests/components/microsoft/test_tts.py +++ b/tests/components/microsoft/test_tts.py @@ -8,18 +8,13 @@ from pycsspeechtts import pycsspeechtts import pytest from homeassistant.components import tts -from homeassistant.components.media_player import ( - ATTR_MEDIA_CONTENT_ID, - DOMAIN as DOMAIN_MP, - SERVICE_PLAY_MEDIA, -) +from homeassistant.components.media_player import ATTR_MEDIA_CONTENT_ID from homeassistant.components.microsoft.tts import SUPPORTED_LANGUAGES from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ServiceNotFound from homeassistant.setup import async_setup_component -from tests.common import async_mock_service from tests.components.tts.common import retrieve_media from tests.typing import ClientSessionGenerator @@ -30,12 +25,6 @@ def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: return mock_tts_cache_dir -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Mock media player calls.""" - return async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - - @pytest.fixture(autouse=True) async def setup_internal_url(hass: HomeAssistant): """Set up internal url.""" @@ -58,7 +47,7 @@ async def test_service_say( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_tts, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test service call say.""" @@ -77,9 +66,11 @@ async def test_service_say( blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 2 assert ( - await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await retrieve_media( + hass, hass_client, service_calls[1].data[ATTR_MEDIA_CONTENT_ID] + ) == HTTPStatus.OK ) @@ -102,7 +93,7 @@ async def test_service_say_en_gb_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_tts, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test service call say with en-gb code in the config.""" @@ -130,9 +121,11 @@ async def test_service_say_en_gb_config( blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 2 assert ( - await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await retrieve_media( + hass, hass_client, service_calls[1].data[ATTR_MEDIA_CONTENT_ID] + ) == HTTPStatus.OK ) @@ -154,7 +147,7 @@ async def test_service_say_en_gb_service( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_tts, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test service call say with en-gb code in the service.""" @@ -177,9 +170,11 @@ async def test_service_say_en_gb_service( blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 2 assert ( - await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await retrieve_media( + hass, hass_client, service_calls[1].data[ATTR_MEDIA_CONTENT_ID] + ) == HTTPStatus.OK ) @@ -201,7 +196,7 @@ async def test_service_say_fa_ir_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_tts, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test service call say with fa-ir code in the config.""" @@ -229,9 +224,11 @@ async def test_service_say_fa_ir_config( blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 2 assert ( - await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await retrieve_media( + hass, hass_client, service_calls[1].data[ATTR_MEDIA_CONTENT_ID] + ) == HTTPStatus.OK ) @@ -253,7 +250,7 @@ async def test_service_say_fa_ir_service( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_tts, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test service call say with fa-ir code in the service.""" @@ -280,9 +277,11 @@ async def test_service_say_fa_ir_service( blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 2 assert ( - await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await retrieve_media( + hass, hass_client, service_calls[1].data[ATTR_MEDIA_CONTENT_ID] + ) == HTTPStatus.OK ) @@ -317,9 +316,7 @@ def test_supported_languages() -> None: assert len(SUPPORTED_LANGUAGES) > 100 -async def test_invalid_language( - hass: HomeAssistant, mock_tts, calls: list[ServiceCall] -) -> None: +async def test_invalid_language(hass: HomeAssistant, mock_tts) -> None: """Test setup component with invalid language.""" await async_setup_component( hass, @@ -339,7 +336,6 @@ async def test_invalid_language( blocking=True, ) - assert len(calls) == 0 assert len(mock_tts.mock_calls) == 0 @@ -347,7 +343,7 @@ async def test_service_say_error( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_tts, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test service call say with http error.""" mock_tts.return_value.speak.side_effect = pycsspeechtts.requests.HTTPError @@ -366,9 +362,11 @@ async def test_service_say_error( blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 2 assert ( - await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await retrieve_media( + hass, hass_client, service_calls[1].data[ATTR_MEDIA_CONTENT_ID] + ) == HTTPStatus.NOT_FOUND ) From 4a8436d6bcea5fed91bc6632d3f364793015d16e Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 2 Jul 2024 12:28:32 +0200 Subject: [PATCH 0598/2411] Do not hold core startup with reolink firmware check task (#120985) --- homeassistant/components/reolink/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 150a23dc64e..479976ad078 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -133,7 +133,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) # If camera WAN blocked, firmware check fails and takes long, do not prevent setup - config_entry.async_create_task(hass, firmware_coordinator.async_refresh()) + config_entry.async_create_background_task( + hass, + firmware_coordinator.async_refresh(), + f"Reolink firmware check {config_entry.entry_id}", + ) # Fetch initial data so we have data when entities subscribe try: await device_coordinator.async_config_entry_first_refresh() From 1f6744847d512c70ca20dbe76a9eb617b094e1ee Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:29:21 +0200 Subject: [PATCH 0599/2411] Use service_calls fixture in zone tests (#120995) --- tests/components/zone/test_trigger.py | 56 ++++++++++++++------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/tests/components/zone/test_trigger.py b/tests/components/zone/test_trigger.py index 6ec5e2fd894..e80cee82eee 100644 --- a/tests/components/zone/test_trigger.py +++ b/tests/components/zone/test_trigger.py @@ -8,7 +8,7 @@ from homeassistant.core import Context, HomeAssistant, ServiceCall from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import async_mock_service, mock_component +from tests.common import mock_component @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -16,12 +16,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.fixture(autouse=True) def setup_comp(hass): """Initialize components.""" @@ -43,7 +37,7 @@ def setup_comp(hass): async def test_if_fires_on_zone_enter( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on zone enter.""" context = Context() @@ -88,9 +82,11 @@ async def test_if_fires_on_zone_enter( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context.parent_id == context.id - assert calls[0].data["some"] == "zone - test.entity - hello - hello - test - 0" + assert len(service_calls) == 1 + assert service_calls[0].context.parent_id == context.id + assert ( + service_calls[0].data["some"] == "zone - test.entity - hello - hello - test - 0" + ) # Set out of zone again so we can trigger call hass.states.async_set( @@ -104,17 +100,20 @@ async def test_if_fires_on_zone_enter( {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 2 hass.states.async_set( "test.entity", "hello", {"latitude": 32.880586, "longitude": -117.237564} ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 2 async def test_if_fires_on_zone_enter_uuid( - hass: HomeAssistant, entity_registry: er.EntityRegistry, calls: list[ServiceCall] + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + service_calls: list[ServiceCall], ) -> None: """Test for firing on zone enter when device is specified by entity registry id.""" context = Context() @@ -165,9 +164,11 @@ async def test_if_fires_on_zone_enter_uuid( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context.parent_id == context.id - assert calls[0].data["some"] == "zone - test.entity - hello - hello - test - 0" + assert len(service_calls) == 1 + assert service_calls[0].context.parent_id == context.id + assert ( + service_calls[0].data["some"] == "zone - test.entity - hello - hello - test - 0" + ) # Set out of zone again so we can trigger call hass.states.async_set( @@ -181,17 +182,18 @@ async def test_if_fires_on_zone_enter_uuid( {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 2 hass.states.async_set( "test.entity", "hello", {"latitude": 32.880586, "longitude": -117.237564} ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 2 async def test_if_not_fires_for_enter_on_zone_leave( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for not firing on zone leave.""" hass.states.async_set( @@ -220,11 +222,11 @@ async def test_if_not_fires_for_enter_on_zone_leave( ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_if_fires_on_zone_leave( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for firing on zone leave.""" hass.states.async_set( @@ -253,11 +255,11 @@ async def test_if_fires_on_zone_leave( ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_if_not_fires_for_leave_on_zone_enter( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test for not firing on zone enter.""" hass.states.async_set( @@ -286,10 +288,12 @@ async def test_if_not_fires_for_leave_on_zone_enter( ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 -async def test_zone_condition(hass: HomeAssistant, calls: list[ServiceCall]) -> None: +async def test_zone_condition( + hass: HomeAssistant, service_calls: list[ServiceCall] +) -> None: """Test for zone condition.""" hass.states.async_set( "test.entity", "hello", {"latitude": 32.880586, "longitude": -117.237564} @@ -314,11 +318,11 @@ async def test_zone_condition(hass: HomeAssistant, calls: list[ServiceCall]) -> hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_unknown_zone( - hass: HomeAssistant, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test for firing on zone enter.""" context = Context() From 8819a9aa526ecfe305225da1ca3e7ea0ea55a3f8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:29:51 +0200 Subject: [PATCH 0600/2411] Use service_calls fixture in sun tests (#120990) --- tests/components/sun/test_trigger.py | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index fc1af35faea..a68162048ff 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, async_mock_service, mock_component +from tests.common import async_fire_time_changed, mock_component @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -26,12 +26,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.fixture(autouse=True) def setup_comp(hass): """Initialize components.""" @@ -41,7 +35,9 @@ def setup_comp(hass): ) -async def test_sunset_trigger(hass: HomeAssistant, calls: list[ServiceCall]) -> None: +async def test_sunset_trigger( + hass: HomeAssistant, service_calls: list[ServiceCall] +) -> None: """Test the sunset trigger.""" now = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 2, tzinfo=dt_util.UTC) @@ -67,10 +63,11 @@ async def test_sunset_trigger(hass: HomeAssistant, calls: list[ServiceCall]) -> {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 1 async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 1 with freeze_time(now): await hass.services.async_call( @@ -79,14 +76,17 @@ async def test_sunset_trigger(hass: HomeAssistant, calls: list[ServiceCall]) -> {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) + assert len(service_calls) == 2 async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["id"] == 0 + assert len(service_calls) == 3 + assert service_calls[2].data["id"] == 0 -async def test_sunrise_trigger(hass: HomeAssistant, calls: list[ServiceCall]) -> None: +async def test_sunrise_trigger( + hass: HomeAssistant, service_calls: list[ServiceCall] +) -> None: """Test the sunrise trigger.""" now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 14, tzinfo=dt_util.UTC) @@ -105,11 +105,11 @@ async def test_sunrise_trigger(hass: HomeAssistant, calls: list[ServiceCall]) -> async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_sunset_trigger_with_offset( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test the sunset trigger with offset.""" now = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) @@ -142,12 +142,12 @@ async def test_sunset_trigger_with_offset( async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "sun - sunset - 0:30:00" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "sun - sunset - 0:30:00" async def test_sunrise_trigger_with_offset( - hass: HomeAssistant, calls: list[ServiceCall] + hass: HomeAssistant, service_calls: list[ServiceCall] ) -> None: """Test the sunrise trigger with offset.""" now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC) @@ -171,4 +171,4 @@ async def test_sunrise_trigger_with_offset( async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 From e54455038096b42a5517d2495753e590a1ebca67 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:30:52 +0200 Subject: [PATCH 0601/2411] Use service_calls fixture in yolink tests (#120997) --- .../components/yolink/test_device_trigger.py | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/tests/components/yolink/test_device_trigger.py b/tests/components/yolink/test_device_trigger.py index f6aa9a28ac0..6b48b32fd62 100644 --- a/tests/components/yolink/test_device_trigger.py +++ b/tests/components/yolink/test_device_trigger.py @@ -1,6 +1,5 @@ """The tests for YoLink device triggers.""" -import pytest from pytest_unordered import unordered from yolink.const import ATTR_DEVICE_DIMMER, ATTR_DEVICE_SMART_REMOTER @@ -11,17 +10,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "yolink", "automation") +from tests.common import MockConfigEntry, async_get_device_automations async def test_get_triggers( @@ -120,7 +109,9 @@ async def test_get_triggers_exception( async def test_if_fires_on_event( - hass: HomeAssistant, calls: list[ServiceCall], device_registry: dr.DeviceRegistry + hass: HomeAssistant, + service_calls: list[ServiceCall], + device_registry: dr.DeviceRegistry, ) -> None: """Test for event triggers firing.""" mac_address = "12:34:56:AB:CD:EF" @@ -166,5 +157,5 @@ async def test_if_fires_on_event( }, ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["message"] == "service called" + assert len(service_calls) == 1 + assert service_calls[0].data["message"] == "service called" From 6fd1f0a34f962b2fd951c6bcf847ee8aa6278cc5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:31:10 +0200 Subject: [PATCH 0602/2411] Use common fixtures in philips_js tests (#120988) --- tests/components/philips_js/conftest.py | 27 ++++++++----------- .../philips_js/test_device_trigger.py | 24 +++++++++-------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/tests/components/philips_js/conftest.py b/tests/components/philips_js/conftest.py index 2a1325627ee..4a79fce85a2 100644 --- a/tests/components/philips_js/conftest.py +++ b/tests/components/philips_js/conftest.py @@ -7,10 +7,12 @@ from haphilipsjs import PhilipsTV import pytest from homeassistant.components.philips_js.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from . import MOCK_CONFIG, MOCK_ENTITY_ID, MOCK_NAME, MOCK_SERIAL_NO, MOCK_SYSTEM -from tests.common import MockConfigEntry, mock_device_registry +from tests.common import MockConfigEntry @pytest.fixture @@ -27,11 +29,6 @@ def mock_setup_entry() -> Generator[AsyncMock]: yield mock_setup_entry -@pytest.fixture(autouse=True) -async def setup_notification(hass): - """Configure notification system.""" - - @pytest.fixture(autouse=True) def mock_tv(): """Disable component actual use.""" @@ -62,7 +59,7 @@ def mock_tv(): @pytest.fixture -async def mock_config_entry(hass): +async def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: """Get standard player.""" config_entry = MockConfigEntry( domain=DOMAIN, data=MOCK_CONFIG, title=MOCK_NAME, unique_id=MOCK_SERIAL_NO @@ -72,13 +69,7 @@ async def mock_config_entry(hass): @pytest.fixture -def mock_device_reg(hass): - """Get standard device.""" - return mock_device_registry(hass) - - -@pytest.fixture -async def mock_entity(hass, mock_device_reg, mock_config_entry): +async def mock_entity(hass: HomeAssistant, mock_config_entry: MockConfigEntry) -> str: """Get standard player.""" assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -86,9 +77,13 @@ async def mock_entity(hass, mock_device_reg, mock_config_entry): @pytest.fixture -def mock_device(hass, mock_device_reg, mock_entity, mock_config_entry): +def mock_device( + device_registry: dr.DeviceRegistry, + mock_entity: str, + mock_config_entry: MockConfigEntry, +) -> dr.DeviceEntry: """Get standard device.""" - return mock_device_reg.async_get_or_create( + return device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, identifiers={(DOMAIN, MOCK_SERIAL_NO)}, ) diff --git a/tests/components/philips_js/test_device_trigger.py b/tests/components/philips_js/test_device_trigger.py index b9b7439d2fa..8f2e5543f1e 100644 --- a/tests/components/philips_js/test_device_trigger.py +++ b/tests/components/philips_js/test_device_trigger.py @@ -9,7 +9,7 @@ from homeassistant.components.philips_js.const import DOMAIN from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component -from tests.common import async_get_device_automations, async_mock_service +from tests.common import async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -17,12 +17,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers(hass: HomeAssistant, mock_device) -> None: """Test we get the expected triggers.""" expected_triggers = [ @@ -42,7 +36,11 @@ async def test_get_triggers(hass: HomeAssistant, mock_device) -> None: async def test_if_fires_on_turn_on_request( - hass: HomeAssistant, calls: list[ServiceCall], mock_tv, mock_entity, mock_device + hass: HomeAssistant, + service_calls: list[ServiceCall], + mock_tv, + mock_entity, + mock_device, ) -> None: """Test for turn_on and turn_off triggers firing.""" @@ -80,6 +78,10 @@ async def test_if_fires_on_turn_on_request( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == mock_device.id - assert calls[0].data["id"] == 0 + assert len(service_calls) == 2 + assert service_calls[0].domain == "media_player" + assert service_calls[0].service == "turn_on" + assert service_calls[1].domain == "test" + assert service_calls[1].service == "automation" + assert service_calls[1].data["some"] == mock_device.id + assert service_calls[1].data["id"] == 0 From 76a62028ad2ea5ce2980f93734c4a7ef8fabaa6b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:32:17 +0200 Subject: [PATCH 0603/2411] Use common registry fixtures in lifx (#121004) --- tests/components/lifx/conftest.py | 14 ------- tests/components/lifx/test_migration.py | 54 +++++++++++++++---------- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/tests/components/lifx/conftest.py b/tests/components/lifx/conftest.py index 093f2309e53..5cb7c702f43 100644 --- a/tests/components/lifx/conftest.py +++ b/tests/components/lifx/conftest.py @@ -8,8 +8,6 @@ from homeassistant.components.lifx import config_flow, coordinator, util from . import _patch_discovery -from tests.common import mock_device_registry, mock_registry - @pytest.fixture def mock_discovery(): @@ -61,15 +59,3 @@ def lifx_mock_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_migration.py b/tests/components/lifx/test_migration.py index 0604ee1c8a7..e5b2f9f8167 100644 --- a/tests/components/lifx/test_migration.py +++ b/tests/components/lifx/test_migration.py @@ -11,8 +11,6 @@ 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 @@ -31,20 +29,22 @@ 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 + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """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( + device = device_registry.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( + light_entity_reg = entity_registry.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="light", @@ -67,7 +67,7 @@ async def test_migration_device_online_end_to_end( 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) == [] + assert er.async_entries_for_config_entry(entity_registry, config_entry) == [] hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -84,20 +84,22 @@ async def test_migration_device_online_end_to_end( async def test_discovery_is_more_frequent_during_migration( - hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """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( + device = device_registry.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( + entity_registry.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="light", @@ -160,7 +162,9 @@ async def test_discovery_is_more_frequent_during_migration( async def test_migration_device_online_end_to_end_after_downgrade( - hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test migration from single config entry can happen again after a downgrade.""" config_entry = MockConfigEntry( @@ -172,13 +176,13 @@ async def test_migration_device_online_end_to_end_after_downgrade( 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( + device = device_registry.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( + light_entity_reg = entity_registry.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="light", @@ -197,7 +201,7 @@ async def test_migration_device_online_end_to_end_after_downgrade( 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) == [] + assert er.async_entries_for_config_entry(entity_registry, config_entry) == [] legacy_entry = None for entry in hass.config_entries.async_entries(DOMAIN): @@ -209,7 +213,9 @@ async def test_migration_device_online_end_to_end_after_downgrade( async def test_migration_device_online_end_to_end_ignores_other_devices( - hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test migration from single config entry.""" legacy_config_entry = MockConfigEntry( @@ -221,18 +227,18 @@ async def test_migration_device_online_end_to_end_ignores_other_devices( domain="other_domain", data={}, unique_id="other_domain" ) other_domain_config_entry.add_to_hass(hass) - device = device_reg.async_get_or_create( + device = device_registry.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( + other_device = device_registry.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( + light_entity_reg = entity_registry.async_get_or_create( config_entry=legacy_config_entry, platform=DOMAIN, domain="light", @@ -240,7 +246,7 @@ async def test_migration_device_online_end_to_end_ignores_other_devices( original_name=LABEL, device_id=device.id, ) - ignored_entity_reg = entity_reg.async_get_or_create( + ignored_entity_reg = entity_registry.async_get_or_create( config_entry=other_domain_config_entry, platform=DOMAIN, domain="sensor", @@ -248,7 +254,7 @@ async def test_migration_device_online_end_to_end_ignores_other_devices( original_name=LABEL, device_id=device.id, ) - garbage_entity_reg = entity_reg.async_get_or_create( + garbage_entity_reg = entity_registry.async_get_or_create( config_entry=legacy_config_entry, platform=DOMAIN, domain="sensor", @@ -281,5 +287,11 @@ async def test_migration_device_online_end_to_end_ignores_other_devices( 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) == [] + assert ( + er.async_entries_for_config_entry(entity_registry, legacy_config_entry) + == [] + ) + assert ( + dr.async_entries_for_config_entry(device_registry, legacy_config_entry) + == [] + ) From 22f5f594784555abab6b2c96faaf542d59b57cb8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:34:11 +0200 Subject: [PATCH 0604/2411] Use service_calls fixture in core platform tests [m-z] (#121001) --- .../media_player/test_device_condition.py | 44 +++++------- .../media_player/test_device_trigger.py | 49 ++++++------- tests/components/remote/test_device_action.py | 10 +-- .../remote/test_device_condition.py | 41 +++++------ .../components/remote/test_device_trigger.py | 37 ++++------ .../select/test_device_condition.py | 30 +++----- .../components/select/test_device_trigger.py | 32 +++------ .../sensor/test_device_condition.py | 62 ++++++++--------- .../components/sensor/test_device_trigger.py | 69 +++++++++---------- tests/components/switch/test_device_action.py | 10 +-- .../switch/test_device_condition.py | 41 +++++------ .../components/switch/test_device_trigger.py | 37 ++++------ tests/components/tag/test_trigger.py | 33 ++++----- .../components/update/test_device_trigger.py | 37 ++++------ .../vacuum/test_device_condition.py | 32 +++------ .../components/vacuum/test_device_trigger.py | 33 ++++----- 16 files changed, 237 insertions(+), 360 deletions(-) diff --git a/tests/components/media_player/test_device_condition.py b/tests/components/media_player/test_device_condition.py index 186cd674b39..78d30e2ca6e 100644 --- a/tests/components/media_player/test_device_condition.py +++ b/tests/components/media_player/test_device_condition.py @@ -20,11 +20,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) +from tests.common import MockConfigEntry, async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -32,12 +28,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_conditions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -136,7 +126,7 @@ async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -274,8 +264,8 @@ async def test_if_state( hass.bus.async_fire("test_event5") hass.bus.async_fire("test_event6") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_on - event - test_event1" hass.states.async_set(entry.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") @@ -285,8 +275,8 @@ async def test_if_state( hass.bus.async_fire("test_event5") hass.bus.async_fire("test_event6") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off - event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "is_off - event - test_event2" hass.states.async_set(entry.entity_id, STATE_IDLE) hass.bus.async_fire("test_event1") @@ -296,8 +286,8 @@ async def test_if_state( hass.bus.async_fire("test_event5") hass.bus.async_fire("test_event6") await hass.async_block_till_done() - assert len(calls) == 3 - assert calls[2].data["some"] == "is_idle - event - test_event3" + assert len(service_calls) == 3 + assert service_calls[2].data["some"] == "is_idle - event - test_event3" hass.states.async_set(entry.entity_id, STATE_PAUSED) hass.bus.async_fire("test_event1") @@ -307,8 +297,8 @@ async def test_if_state( hass.bus.async_fire("test_event5") hass.bus.async_fire("test_event6") await hass.async_block_till_done() - assert len(calls) == 4 - assert calls[3].data["some"] == "is_paused - event - test_event4" + assert len(service_calls) == 4 + assert service_calls[3].data["some"] == "is_paused - event - test_event4" hass.states.async_set(entry.entity_id, STATE_PLAYING) hass.bus.async_fire("test_event1") @@ -318,8 +308,8 @@ async def test_if_state( hass.bus.async_fire("test_event5") hass.bus.async_fire("test_event6") await hass.async_block_till_done() - assert len(calls) == 5 - assert calls[4].data["some"] == "is_playing - event - test_event5" + assert len(service_calls) == 5 + assert service_calls[4].data["some"] == "is_playing - event - test_event5" hass.states.async_set(entry.entity_id, STATE_BUFFERING) hass.bus.async_fire("test_event1") @@ -329,15 +319,15 @@ async def test_if_state( hass.bus.async_fire("test_event5") hass.bus.async_fire("test_event6") await hass.async_block_till_done() - assert len(calls) == 6 - assert calls[5].data["some"] == "is_buffering - event - test_event6" + assert len(service_calls) == 6 + assert service_calls[5].data["some"] == "is_buffering - event - test_event6" async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -380,5 +370,5 @@ async def test_if_state_legacy( ) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_on - event - test_event1" diff --git a/tests/components/media_player/test_device_trigger.py b/tests/components/media_player/test_device_trigger.py index e9d5fbd646e..4bb27b73f24 100644 --- a/tests/components/media_player/test_device_trigger.py +++ b/tests/components/media_player/test_device_trigger.py @@ -28,7 +28,6 @@ from tests.common import ( async_fire_time_changed, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, ) @@ -37,12 +36,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -209,7 +202,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -265,8 +258,8 @@ async def test_if_fires_on_state_change( # Fake that the entity is turning on. hass.states.async_set(entry.entity_id, STATE_ON) await hass.async_block_till_done() - assert len(calls) == 2 - assert {calls[0].data["some"], calls[1].data["some"]} == { + assert len(service_calls) == 2 + assert {service_calls[0].data["some"], service_calls[1].data["some"]} == { "turned_on - device - media_player.test_5678 - off - on - None", "changed_states - device - media_player.test_5678 - off - on - None", } @@ -274,8 +267,8 @@ async def test_if_fires_on_state_change( # Fake that the entity is turning off. hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 4 - assert {calls[2].data["some"], calls[3].data["some"]} == { + assert len(service_calls) == 4 + assert {service_calls[2].data["some"], service_calls[3].data["some"]} == { "turned_off - device - media_player.test_5678 - on - off - None", "changed_states - device - media_player.test_5678 - on - off - None", } @@ -283,8 +276,8 @@ async def test_if_fires_on_state_change( # Fake that the entity becomes idle. hass.states.async_set(entry.entity_id, STATE_IDLE) await hass.async_block_till_done() - assert len(calls) == 6 - assert {calls[4].data["some"], calls[5].data["some"]} == { + assert len(service_calls) == 6 + assert {service_calls[4].data["some"], service_calls[5].data["some"]} == { "idle - device - media_player.test_5678 - off - idle - None", "changed_states - device - media_player.test_5678 - off - idle - None", } @@ -292,8 +285,8 @@ async def test_if_fires_on_state_change( # Fake that the entity starts playing. hass.states.async_set(entry.entity_id, STATE_PLAYING) await hass.async_block_till_done() - assert len(calls) == 8 - assert {calls[6].data["some"], calls[7].data["some"]} == { + assert len(service_calls) == 8 + assert {service_calls[6].data["some"], service_calls[7].data["some"]} == { "playing - device - media_player.test_5678 - idle - playing - None", "changed_states - device - media_player.test_5678 - idle - playing - None", } @@ -301,8 +294,8 @@ async def test_if_fires_on_state_change( # Fake that the entity is paused. hass.states.async_set(entry.entity_id, STATE_PAUSED) await hass.async_block_till_done() - assert len(calls) == 10 - assert {calls[8].data["some"], calls[9].data["some"]} == { + assert len(service_calls) == 10 + assert {service_calls[8].data["some"], service_calls[9].data["some"]} == { "paused - device - media_player.test_5678 - playing - paused - None", "changed_states - device - media_player.test_5678 - playing - paused - None", } @@ -310,8 +303,8 @@ async def test_if_fires_on_state_change( # Fake that the entity is buffering. hass.states.async_set(entry.entity_id, STATE_BUFFERING) await hass.async_block_till_done() - assert len(calls) == 12 - assert {calls[10].data["some"], calls[11].data["some"]} == { + assert len(service_calls) == 12 + assert {service_calls[10].data["some"], service_calls[11].data["some"]} == { "buffering - device - media_player.test_5678 - paused - buffering - None", "changed_states - device - media_player.test_5678 - paused - buffering - None", } @@ -321,7 +314,7 @@ async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -369,9 +362,9 @@ async def test_if_fires_on_state_change_legacy( # Fake that the entity is turning on. hass.states.async_set(entry.entity_id, STATE_ON) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == "turned_on - device - media_player.test_5678 - off - on - None" ) @@ -380,7 +373,7 @@ async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -426,16 +419,16 @@ async def test_if_fires_on_state_change_with_for( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_ON) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await hass.async_block_till_done() assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - off - on - 0:00:05" ) diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py index a6e890937b5..e224fcf4939 100644 --- a/tests/components/remote/test_device_action.py +++ b/tests/components/remote/test_device_action.py @@ -7,7 +7,7 @@ from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.remote import DOMAIN from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component @@ -24,12 +24,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_actions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -114,7 +108,6 @@ async def test_action( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off actions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -189,7 +182,6 @@ async def test_action_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off actions.""" config_entry = MockConfigEntry(domain="test", data={}) diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index d13a0480355..6c9334aeac4 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -20,7 +20,6 @@ from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, ) @@ -29,12 +28,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_conditions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -183,7 +176,7 @@ async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -249,20 +242,20 @@ async def test_if_state( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_on event - test_event1" hass.states.async_set(entry.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "is_off event - test_event2" @pytest.mark.usefixtures("enable_custom_integrations") @@ -270,7 +263,7 @@ async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -315,13 +308,13 @@ async def test_if_state_legacy( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_on event - test_event1" @pytest.mark.usefixtures("enable_custom_integrations") @@ -329,7 +322,7 @@ async def test_if_fires_on_for_condition( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for firing if condition is on with delay.""" point1 = dt_util.utcnow() @@ -378,26 +371,26 @@ async def test_if_fires_on_for_condition( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 # Time travel 10 secs into the future freezer.move_to(point2) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 # Time travel 20 secs into the future freezer.move_to(point3) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_off event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_off event - test_event1" diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index 8a1a0c318d7..c647faba2c1 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -20,7 +20,6 @@ from tests.common import ( async_fire_time_changed, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, ) @@ -29,12 +28,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -181,7 +174,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -267,20 +260,20 @@ async def test_if_fires_on_state_change( ] }, ) - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 2 - assert {calls[0].data["some"], calls[1].data["some"]} == { + assert len(service_calls) == 2 + assert {service_calls[0].data["some"], service_calls[1].data["some"]} == { f"turn_off device - {entry.entity_id} - on - off - None", f"turn_on_or_off device - {entry.entity_id} - on - off - None", } hass.states.async_set(entry.entity_id, STATE_ON) await hass.async_block_till_done() - assert len(calls) == 4 - assert {calls[2].data["some"], calls[3].data["some"]} == { + assert len(service_calls) == 4 + assert {service_calls[2].data["some"], service_calls[3].data["some"]} == { f"turn_on device - {entry.entity_id} - off - on - None", f"turn_on_or_off device - {entry.entity_id} - off - on - None", } @@ -291,7 +284,7 @@ async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -335,13 +328,13 @@ async def test_if_fires_on_state_change_legacy( ] }, ) - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - on - off - None" ) @@ -351,7 +344,7 @@ async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -397,16 +390,16 @@ async def test_if_fires_on_state_change_with_for( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await hass.async_block_till_done() assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - on - off - 0:00:05" ) diff --git a/tests/components/select/test_device_condition.py b/tests/components/select/test_device_condition.py index e60df688658..fc35757fa67 100644 --- a/tests/components/select/test_device_condition.py +++ b/tests/components/select/test_device_condition.py @@ -21,17 +21,7 @@ from homeassistant.helpers import ( ) from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") +from tests.common import MockConfigEntry, async_get_device_automations async def test_get_conditions( @@ -115,7 +105,7 @@ async def test_get_conditions_hidden_auxiliary( async def test_if_selected_option( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: @@ -181,7 +171,7 @@ async def test_if_selected_option( hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set( entry.entity_id, "option1", {"options": ["option1", "option2"]} @@ -189,8 +179,8 @@ async def test_if_selected_option( hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["result"] == "option1 - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["result"] == "option1 - event - test_event1" hass.states.async_set( entry.entity_id, "option2", {"options": ["option1", "option2"]} @@ -198,13 +188,13 @@ async def test_if_selected_option( hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["result"] == "option2 - event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["result"] == "option2 - event - test_event2" async def test_if_selected_option_legacy( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: @@ -252,8 +242,8 @@ async def test_if_selected_option_legacy( ) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["result"] == "option1 - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["result"] == "option1 - event - test_event1" async def test_get_condition_capabilities( diff --git a/tests/components/select/test_device_trigger.py b/tests/components/select/test_device_trigger.py index c7a55c56202..dbb4e23d785 100644 --- a/tests/components/select/test_device_trigger.py +++ b/tests/components/select/test_device_trigger.py @@ -21,17 +21,7 @@ from homeassistant.helpers import ( ) from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") +from tests.common import MockConfigEntry, async_get_device_automations async def test_get_triggers( @@ -117,7 +107,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -210,27 +200,27 @@ async def test_if_fires_on_state_change( # Test triggering device trigger with a to state hass.states.async_set(entry.entity_id, "option2") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"to - device - {entry.entity_id} - option1 - option2 - None - 0" ) # Test triggering device trigger with a from state hass.states.async_set(entry.entity_id, "option3") await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[1].data["some"] + service_calls[1].data["some"] == f"from - device - {entry.entity_id} - option2 - option3 - None - 0" ) # Test triggering device trigger with both a from and to state hass.states.async_set(entry.entity_id, "option1") await hass.async_block_till_done() - assert len(calls) == 3 + assert len(service_calls) == 3 assert ( - calls[2].data["some"] + service_calls[2].data["some"] == f"from-to - device - {entry.entity_id} - option3 - option1 - None - 0" ) @@ -239,7 +229,7 @@ async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -289,9 +279,9 @@ async def test_if_fires_on_state_change_legacy( # Test triggering device trigger with a to state hass.states.async_set(entry.entity_id, "option2") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"to - device - {entry.entity_id} - option1 - option2 - None - 0" ) diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 3bc9a660e93..d9a9900b8b1 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -27,7 +27,6 @@ from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, setup_test_component_platform, ) @@ -37,12 +36,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.mark.parametrize( "device_class", [ @@ -470,7 +463,6 @@ async def test_if_state_not_above_below( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], caplog: pytest.LogCaptureFixture, ) -> None: """Test for bad value conditions.""" @@ -513,7 +505,7 @@ async def test_if_state_above( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for value conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -559,22 +551,22 @@ async def test_if_state_above( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 9) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 11) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "event - test_event1" @pytest.mark.usefixtures("enable_custom_integrations") @@ -582,7 +574,7 @@ async def test_if_state_above_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for value conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -628,22 +620,22 @@ async def test_if_state_above_legacy( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 9) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 11) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "event - test_event1" @pytest.mark.usefixtures("enable_custom_integrations") @@ -651,7 +643,7 @@ async def test_if_state_below( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for value conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -697,22 +689,22 @@ async def test_if_state_below( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 11) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 9) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "event - test_event1" @pytest.mark.usefixtures("enable_custom_integrations") @@ -720,7 +712,7 @@ async def test_if_state_between( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for value conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -767,30 +759,30 @@ async def test_if_state_between( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 9) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 11) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "event - test_event1" hass.states.async_set(entry.entity_id, 21) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.states.async_set(entry.entity_id, 19) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "event - test_event1" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "event - test_event1" diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 87a6d9929c3..bb560c824d3 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -31,7 +31,6 @@ from tests.common import ( async_fire_time_changed, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, setup_test_component_platform, ) @@ -41,12 +40,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.mark.parametrize( "device_class", [ @@ -427,7 +420,6 @@ async def test_if_fires_not_on_above_below( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], caplog: pytest.LogCaptureFixture, ) -> None: """Test for value triggers firing.""" @@ -467,7 +459,7 @@ async def test_if_fires_on_state_above( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for value triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -513,17 +505,18 @@ async def test_if_fires_on_state_above( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 9) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 11) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] == f"bat_low device - {entry.entity_id} - 9 - 11 - None" + service_calls[0].data["some"] + == f"bat_low device - {entry.entity_id} - 9 - 11 - None" ) @@ -532,7 +525,7 @@ async def test_if_fires_on_state_below( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for value triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -578,17 +571,18 @@ async def test_if_fires_on_state_below( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 11) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 9) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] == f"bat_low device - {entry.entity_id} - 11 - 9 - None" + service_calls[0].data["some"] + == f"bat_low device - {entry.entity_id} - 11 - 9 - None" ) @@ -597,7 +591,7 @@ async def test_if_fires_on_state_between( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for value triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -644,28 +638,30 @@ async def test_if_fires_on_state_between( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 9) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 11) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] == f"bat_low device - {entry.entity_id} - 9 - 11 - None" + service_calls[0].data["some"] + == f"bat_low device - {entry.entity_id} - 9 - 11 - None" ) hass.states.async_set(entry.entity_id, 21) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 hass.states.async_set(entry.entity_id, 19) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[1].data["some"] == f"bat_low device - {entry.entity_id} - 21 - 19 - None" + service_calls[1].data["some"] + == f"bat_low device - {entry.entity_id} - 21 - 19 - None" ) @@ -674,7 +670,7 @@ async def test_if_fires_on_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for value triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -720,17 +716,18 @@ async def test_if_fires_on_state_legacy( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 9) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 11) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] == f"bat_low device - {entry.entity_id} - 9 - 11 - None" + service_calls[0].data["some"] + == f"bat_low device - {entry.entity_id} - 9 - 11 - None" ) @@ -739,7 +736,7 @@ async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -786,17 +783,17 @@ async def test_if_fires_on_state_change_with_for( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, 10) hass.states.async_set(entry.entity_id, 11) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await hass.async_block_till_done() assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - 10 - 11 - 0:00:05" ) diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index 0b41ce7992d..9751721cbc7 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -7,7 +7,7 @@ from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.switch import DOMAIN from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component @@ -24,12 +24,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_actions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -115,7 +109,6 @@ async def test_action( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off actions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -190,7 +183,6 @@ async def test_action_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off actions.""" config_entry = MockConfigEntry(domain="test", data={}) diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index 2ba2c6adb5c..7c4f434b0a4 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -20,7 +20,6 @@ from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, ) @@ -29,12 +28,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_conditions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -183,7 +176,7 @@ async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -249,20 +242,20 @@ async def test_if_state( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_on event - test_event1" hass.states.async_set(entry.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "is_off event - test_event2" @pytest.mark.usefixtures("enable_custom_integrations") @@ -270,7 +263,7 @@ async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -315,12 +308,12 @@ async def test_if_state_legacy( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_on event - test_event1" @pytest.mark.usefixtures("enable_custom_integrations") @@ -328,7 +321,7 @@ async def test_if_fires_on_for_condition( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for firing if condition is on with delay.""" point1 = dt_util.utcnow() @@ -377,26 +370,26 @@ async def test_if_fires_on_for_condition( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 # Time travel 10 secs into the future freezer.move_to(point2) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 # Time travel 20 secs into the future freezer.move_to(point3) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_off event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_off event - test_event1" diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 092b7a964bb..08e6ab6d0f6 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -20,7 +20,6 @@ from tests.common import ( async_fire_time_changed, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, ) @@ -29,12 +28,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -181,7 +174,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -268,20 +261,20 @@ async def test_if_fires_on_state_change( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 2 - assert {calls[0].data["some"], calls[1].data["some"]} == { + assert len(service_calls) == 2 + assert {service_calls[0].data["some"], service_calls[1].data["some"]} == { f"turn_off device - {entry.entity_id} - on - off - None", f"turn_on_or_off device - {entry.entity_id} - on - off - None", } hass.states.async_set(entry.entity_id, STATE_ON) await hass.async_block_till_done() - assert len(calls) == 4 - assert {calls[2].data["some"], calls[3].data["some"]} == { + assert len(service_calls) == 4 + assert {service_calls[2].data["some"], service_calls[3].data["some"]} == { f"turn_on device - {entry.entity_id} - off - on - None", f"turn_on_or_off device - {entry.entity_id} - off - on - None", } @@ -292,7 +285,7 @@ async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -337,13 +330,13 @@ async def test_if_fires_on_state_change_legacy( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - on - off - None" ) @@ -353,7 +346,7 @@ async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -399,16 +392,16 @@ async def test_if_fires_on_state_change_with_for( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await hass.async_block_till_done() assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - on - off - 0:00:05" ) diff --git a/tests/components/tag/test_trigger.py b/tests/components/tag/test_trigger.py index 60d45abb7b9..5c7e515d322 100644 --- a/tests/components/tag/test_trigger.py +++ b/tests/components/tag/test_trigger.py @@ -11,8 +11,6 @@ from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component -from tests.common import async_mock_service - @pytest.fixture(autouse=True, name="stub_blueprint_populate") def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @@ -39,14 +37,8 @@ def tag_setup(hass: HomeAssistant, hass_storage: dict[str, Any]): return _storage -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_triggers( - hass: HomeAssistant, tag_setup, calls: list[ServiceCall] + hass: HomeAssistant, tag_setup, service_calls: list[ServiceCall] ) -> None: """Test tag triggers.""" assert await tag_setup() @@ -75,9 +67,9 @@ async def test_triggers( await async_scan_tag(hass, "abc123", None) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["message"] == "service called" - assert calls[0].data["id"] == 0 + assert len(service_calls) == 1 + assert service_calls[0].data["message"] == "service called" + assert service_calls[0].data["id"] == 0 await hass.services.async_call( automation.DOMAIN, @@ -85,15 +77,16 @@ async def test_triggers( {ATTR_ENTITY_ID: "automation.test"}, blocking=True, ) + assert len(service_calls) == 2 await async_scan_tag(hass, "abc123", None) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 2 async def test_exception_bad_trigger( - hass: HomeAssistant, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test for exception on event triggers firing.""" @@ -117,7 +110,7 @@ async def test_exception_bad_trigger( async def test_multiple_tags_and_devices_trigger( - hass: HomeAssistant, tag_setup, calls: list[ServiceCall] + hass: HomeAssistant, tag_setup, service_calls: list[ServiceCall] ) -> None: """Test multiple tags and devices triggers.""" assert await tag_setup() @@ -158,8 +151,8 @@ async def test_multiple_tags_and_devices_trigger( await async_scan_tag(hass, "def456", device_id="jkl0123") await hass.async_block_till_done() - assert len(calls) == 4 - assert calls[0].data["message"] == "service called" - assert calls[1].data["message"] == "service called" - assert calls[2].data["message"] == "service called" - assert calls[3].data["message"] == "service called" + assert len(service_calls) == 4 + assert service_calls[0].data["message"] == "service called" + assert service_calls[1].data["message"] == "service called" + assert service_calls[2].data["message"] == "service called" + assert service_calls[3].data["message"] == "service called" diff --git a/tests/components/update/test_device_trigger.py b/tests/components/update/test_device_trigger.py index fa9af863f56..202b3d32509 100644 --- a/tests/components/update/test_device_trigger.py +++ b/tests/components/update/test_device_trigger.py @@ -21,7 +21,6 @@ from tests.common import ( async_fire_time_changed, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, setup_test_component_platform, ) @@ -31,12 +30,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -182,7 +175,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_update_entities: list[MockUpdateEntity], ) -> None: """Test for turn_on and turn_off triggers firing.""" @@ -253,21 +246,21 @@ async def test_if_fires_on_state_change( state = hass.states.get("update.update_available") assert state assert state.state == STATE_ON - assert not calls + assert not service_calls hass.states.async_set("update.update_available", STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == "no_update device - update.update_available - on - off - None" ) hass.states.async_set("update.update_available", STATE_ON) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[1].data["some"] + service_calls[1].data["some"] == "update_available device - update.update_available - off - on - None" ) @@ -276,7 +269,7 @@ async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_update_entities: list[MockUpdateEntity], ) -> None: """Test for turn_on and turn_off triggers firing.""" @@ -326,13 +319,13 @@ async def test_if_fires_on_state_change_legacy( state = hass.states.get("update.update_available") assert state assert state.state == STATE_ON - assert not calls + assert not service_calls hass.states.async_set("update.update_available", STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == "no_update device - update.update_available - on - off - None" ) @@ -341,7 +334,7 @@ async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], mock_update_entities: list[MockUpdateEntity], ) -> None: """Test for triggers firing with delay.""" @@ -392,16 +385,16 @@ async def test_if_fires_on_state_change_with_for( state = hass.states.get("update.update_available") assert state assert state.state == STATE_ON - assert not calls + assert not service_calls hass.states.async_set("update.update_available", STATE_OFF) await hass.async_block_till_done() - assert not calls + assert not service_calls async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await hass.async_block_till_done() assert ( - calls[0].data["some"] + service_calls[0].data["some"] == "turn_off device - update.update_available - on - off - 0:00:05" ) diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py index 5cc222a1833..9a2a67f7141 100644 --- a/tests/components/vacuum/test_device_condition.py +++ b/tests/components/vacuum/test_device_condition.py @@ -17,11 +17,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_get_device_automations, - async_mock_service, -) +from tests.common import MockConfigEntry, async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -29,12 +25,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_conditions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -119,7 +109,7 @@ async def test_if_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -181,30 +171,30 @@ async def test_if_state( hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_docked - event - test_event2" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_docked - event - test_event2" hass.states.async_set(entry.entity_id, STATE_CLEANING) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_cleaning - event - test_event1" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "is_cleaning - event - test_event1" # Returning means it's still cleaning hass.states.async_set(entry.entity_id, STATE_RETURNING) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 3 - assert calls[2].data["some"] == "is_cleaning - event - test_event1" + assert len(service_calls) == 3 + assert service_calls[2].data["some"] == "is_cleaning - event - test_event1" async def test_if_state_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off conditions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -247,5 +237,5 @@ async def test_if_state_legacy( ) hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_cleaning - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "is_cleaning - event - test_event1" diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index 56e351a6446..c186bd4d9eb 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -20,7 +20,6 @@ from tests.common import ( async_fire_time_changed, async_get_device_automation_capabilities, async_get_device_automations, - async_mock_service, ) @@ -29,12 +28,6 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -182,7 +175,7 @@ async def test_if_fires_on_state_change( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -247,18 +240,18 @@ async def test_if_fires_on_state_change( # Fake that the entity is cleaning hass.states.async_set(entry.entity_id, STATE_CLEANING) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"cleaning - device - {entry.entity_id} - docked - cleaning" ) # Fake that the entity is docked hass.states.async_set(entry.entity_id, STATE_DOCKED) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[1].data["some"] + service_calls[1].data["some"] == f"docked - device - {entry.entity_id} - cleaning - docked" ) @@ -267,7 +260,7 @@ async def test_if_fires_on_state_change_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -313,9 +306,9 @@ async def test_if_fires_on_state_change_legacy( # Fake that the entity is cleaning hass.states.async_set(entry.entity_id, STATE_CLEANING) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"cleaning - device - {entry.entity_id} - docked - cleaning" ) @@ -324,7 +317,7 @@ async def test_if_fires_on_state_change_with_for( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for triggers firing with delay.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -370,16 +363,16 @@ async def test_if_fires_on_state_change_with_for( }, ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 hass.states.async_set(entry.entity_id, STATE_CLEANING) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 await hass.async_block_till_done() assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"turn_off device - {entry.entity_id} - docked - cleaning - 0:00:05" ) From cdc38973199c367564dbb0f516db72c52fd7dbdf Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Jul 2024 12:35:10 +0200 Subject: [PATCH 0605/2411] Bump yt-dlp to 2024.07.01 (#120978) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 7ed4e93bb56..cfe44f5176b 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2024.05.27"], + "requirements": ["yt-dlp==2024.07.01"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 3d7276393c0..2d2fafc5b24 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2951,7 +2951,7 @@ youless-api==2.1.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.05.27 +yt-dlp==2024.07.01 # homeassistant.components.zamg zamg==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6e24d2efae..bca291c8016 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2307,7 +2307,7 @@ youless-api==2.1.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.05.27 +yt-dlp==2024.07.01 # homeassistant.components.zamg zamg==0.3.6 From 02dffcde1a74de113734d30eed96d6c236d00718 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:36:30 +0200 Subject: [PATCH 0606/2411] Use common registry fixtures in solarlog (#121005) --- tests/components/solarlog/conftest.py | 20 +------------------- tests/components/solarlog/test_init.py | 10 ++++++---- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/tests/components/solarlog/conftest.py b/tests/components/solarlog/conftest.py index 86cdc870cde..c34d0c011a3 100644 --- a/tests/components/solarlog/conftest.py +++ b/tests/components/solarlog/conftest.py @@ -7,16 +7,10 @@ import pytest from homeassistant.components.solarlog.const import DOMAIN as SOLARLOG_DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.core import HomeAssistant from .const import HOST, NAME -from tests.common import ( - MockConfigEntry, - load_json_object_fixture, - mock_device_registry, - mock_registry, -) +from tests.common import MockConfigEntry, load_json_object_fixture @pytest.fixture @@ -76,15 +70,3 @@ def mock_test_connection(): return_value=True, ): yield - - -@pytest.fixture(name="device_reg") -def device_reg_fixture(hass: HomeAssistant): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - -@pytest.fixture(name="entity_reg") -def entity_reg_fixture(hass: HomeAssistant): - """Return an empty, loaded, registry.""" - return mock_registry(hass) diff --git a/tests/components/solarlog/test_init.py b/tests/components/solarlog/test_init.py index f9f00ef601b..0044d09f20e 100644 --- a/tests/components/solarlog/test_init.py +++ b/tests/components/solarlog/test_init.py @@ -50,7 +50,9 @@ async def test_raise_config_entry_not_ready_when_offline( async def test_migrate_config_entry( - hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry + hass: HomeAssistant, + device_registry: DeviceRegistry, + entity_registry: EntityRegistry, ) -> None: """Test successful migration of entry data.""" entry = MockConfigEntry( @@ -64,13 +66,13 @@ async def test_migrate_config_entry( ) entry.add_to_hass(hass) - device = device_reg.async_get_or_create( + device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, entry.entry_id)}, manufacturer="Solar-Log", name="solarlog", ) - sensor_entity = entity_reg.async_get_or_create( + sensor_entity = entity_registry.async_get_or_create( config_entry=entry, platform=DOMAIN, domain=Platform.SENSOR, @@ -85,7 +87,7 @@ async def test_migrate_config_entry( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - entity_migrated = entity_reg.async_get(sensor_entity.entity_id) + entity_migrated = entity_registry.async_get(sensor_entity.entity_id) assert entity_migrated assert entity_migrated.unique_id == f"{entry.entry_id}_last_updated" From 3adea1ada918e1131943993ec6ea904598aab0a0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:37:21 +0200 Subject: [PATCH 0607/2411] Use service_calls fixture in zwave_js tests (#120994) --- .../zwave_js/test_device_condition.py | 42 ++++------ .../zwave_js/test_device_trigger.py | 82 +++++++++---------- 2 files changed, 56 insertions(+), 68 deletions(-) diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index 61ed2bb35fb..17bc4cf0f5d 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -25,13 +25,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.setup import async_setup_component -from tests.common import async_get_device_automations, async_mock_service - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") +from tests.common import async_get_device_automations async def test_get_conditions( @@ -99,7 +93,7 @@ async def test_node_status_state( client, lock_schlage_be469, integration, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, ) -> None: """Test for node_status conditions.""" @@ -206,8 +200,8 @@ async def test_node_status_state( hass.bus.async_fire("test_event3") hass.bus.async_fire("test_event4") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "alive - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "alive - event - test_event1" event = Event( "wake up", @@ -225,8 +219,8 @@ async def test_node_status_state( hass.bus.async_fire("test_event3") hass.bus.async_fire("test_event4") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "awake - event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "awake - event - test_event2" event = Event( "sleep", @@ -240,8 +234,8 @@ async def test_node_status_state( hass.bus.async_fire("test_event3") hass.bus.async_fire("test_event4") await hass.async_block_till_done() - assert len(calls) == 3 - assert calls[2].data["some"] == "asleep - event - test_event3" + assert len(service_calls) == 3 + assert service_calls[2].data["some"] == "asleep - event - test_event3" event = Event( "dead", @@ -255,8 +249,8 @@ async def test_node_status_state( hass.bus.async_fire("test_event3") hass.bus.async_fire("test_event4") await hass.async_block_till_done() - assert len(calls) == 4 - assert calls[3].data["some"] == "dead - event - test_event4" + assert len(service_calls) == 4 + assert service_calls[3].data["some"] == "dead - event - test_event4" async def test_config_parameter_state( @@ -264,7 +258,7 @@ async def test_config_parameter_state( client, lock_schlage_be469, integration, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, ) -> None: """Test for config_parameter conditions.""" @@ -331,8 +325,8 @@ async def test_config_parameter_state( hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "Beeper - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "Beeper - event - test_event1" # Flip Beeper state to not match condition event = Event( @@ -375,8 +369,8 @@ async def test_config_parameter_state( hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "User Slot Status - event - test_event2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "User Slot Status - event - test_event2" async def test_value_state( @@ -384,7 +378,7 @@ async def test_value_state( client, lock_schlage_be469, integration, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, ) -> None: """Test for value conditions.""" @@ -427,8 +421,8 @@ async def test_value_state( hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "value - event - test_event1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "value - event - test_event1" async def test_get_condition_capabilities_node_status( diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index 0fa228288ec..ccc69f7723d 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -28,13 +28,7 @@ from homeassistant.helpers import ( ) from homeassistant.setup import async_setup_component -from tests.common import async_get_device_automations, async_mock_service - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") +from tests.common import async_get_device_automations async def test_no_controller_triggers( @@ -85,7 +79,7 @@ async def test_if_notification_notification_fires( client, lock_schlage_be469, integration, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for event.notification.notification trigger firing.""" node: Node = lock_schlage_be469 @@ -168,13 +162,13 @@ async def test_if_notification_notification_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"event.notification.notification - device - zwave_js_notification - {CommandClass.NOTIFICATION}" ) assert ( - calls[1].data["some"] + service_calls[1].data["some"] == f"event.notification.notification2 - device - zwave_js_notification - {CommandClass.NOTIFICATION}" ) @@ -221,7 +215,7 @@ async def test_if_entry_control_notification_fires( client, lock_schlage_be469, integration, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for notification.entry_control trigger firing.""" node: Node = lock_schlage_be469 @@ -303,13 +297,13 @@ async def test_if_entry_control_notification_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"event.notification.notification - device - zwave_js_notification - {CommandClass.ENTRY_CONTROL}" ) assert ( - calls[1].data["some"] + service_calls[1].data["some"] == f"event.notification.notification2 - device - zwave_js_notification - {CommandClass.ENTRY_CONTROL}" ) @@ -389,7 +383,7 @@ async def test_if_node_status_change_fires( client, lock_schlage_be469, integration, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for node_status trigger firing.""" node: Node = lock_schlage_be469 @@ -460,9 +454,9 @@ async def test_if_node_status_change_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[0].data["some"] == "state.node_status - device - alive" - assert calls[1].data["some"] == "state.node_status2 - device - alive" + assert len(service_calls) == 2 + assert service_calls[0].data["some"] == "state.node_status - device - alive" + assert service_calls[1].data["some"] == "state.node_status2 - device - alive" async def test_if_node_status_change_fires_legacy( @@ -472,7 +466,7 @@ async def test_if_node_status_change_fires_legacy( client, lock_schlage_be469, integration, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for node_status trigger firing.""" node: Node = lock_schlage_be469 @@ -543,9 +537,9 @@ async def test_if_node_status_change_fires_legacy( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[0].data["some"] == "state.node_status - device - alive" - assert calls[1].data["some"] == "state.node_status2 - device - alive" + assert len(service_calls) == 2 + assert service_calls[0].data["some"] == "state.node_status - device - alive" + assert service_calls[1].data["some"] == "state.node_status2 - device - alive" async def test_get_trigger_capabilities_node_status( @@ -645,7 +639,7 @@ async def test_if_basic_value_notification_fires( client, ge_in_wall_dimmer_switch, integration, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for event.value_notification.basic trigger firing.""" node: Node = ge_in_wall_dimmer_switch @@ -742,13 +736,13 @@ async def test_if_basic_value_notification_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"event.value_notification.basic - device - zwave_js_value_notification - {CommandClass.BASIC}" ) assert ( - calls[1].data["some"] + service_calls[1].data["some"] == f"event.value_notification.basic2 - device - zwave_js_value_notification - {CommandClass.BASIC}" ) @@ -830,7 +824,7 @@ async def test_if_central_scene_value_notification_fires( client, wallmote_central_scene, integration, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for event.value_notification.central_scene trigger firing.""" node: Node = wallmote_central_scene @@ -933,13 +927,13 @@ async def test_if_central_scene_value_notification_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"event.value_notification.central_scene - device - zwave_js_value_notification - {CommandClass.CENTRAL_SCENE}" ) assert ( - calls[1].data["some"] + service_calls[1].data["some"] == f"event.value_notification.central_scene2 - device - zwave_js_value_notification - {CommandClass.CENTRAL_SCENE}" ) @@ -1020,7 +1014,7 @@ async def test_if_scene_activation_value_notification_fires( client, hank_binary_switch, integration, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for event.value_notification.scene_activation trigger firing.""" node: Node = hank_binary_switch @@ -1117,13 +1111,13 @@ async def test_if_scene_activation_value_notification_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == f"event.value_notification.scene_activation - device - zwave_js_value_notification - {CommandClass.SCENE_ACTIVATION}" ) assert ( - calls[1].data["some"] + service_calls[1].data["some"] == f"event.value_notification.scene_activation2 - device - zwave_js_value_notification - {CommandClass.SCENE_ACTIVATION}" ) @@ -1200,7 +1194,7 @@ async def test_if_value_updated_value_fires( client, lock_schlage_be469, integration, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for zwave_js.value_updated.value trigger firing.""" node: Node = lock_schlage_be469 @@ -1261,7 +1255,7 @@ async def test_if_value_updated_value_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 # Publish fake value update that should trigger event = Event( @@ -1283,9 +1277,9 @@ async def test_if_value_updated_value_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == "zwave_js.value_updated.value - zwave_js.value_updated - open" ) @@ -1296,7 +1290,7 @@ async def test_value_updated_value_no_driver( client, lock_schlage_be469, integration, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test zwave_js.value_updated.value trigger with missing driver.""" node: Node = lock_schlage_be469 @@ -1362,7 +1356,7 @@ async def test_value_updated_value_no_driver( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_get_trigger_capabilities_value_updated_value( @@ -1455,7 +1449,7 @@ async def test_if_value_updated_config_parameter_fires( client, lock_schlage_be469, integration, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test for zwave_js.value_updated.config_parameter trigger firing.""" node: Node = lock_schlage_be469 @@ -1517,9 +1511,9 @@ async def test_if_value_updated_config_parameter_fires( ) node.receive_event(event) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 assert ( - calls[0].data["some"] + service_calls[0].data["some"] == "zwave_js.value_updated.config_parameter - zwave_js.value_updated - 255" ) From 326d24d78b098e8a3eb92e7af59c76797f112953 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:37:50 +0200 Subject: [PATCH 0608/2411] Use service_calls fixture in xiaomi_ble tests (#120998) --- .../xiaomi_ble/test_device_trigger.py | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/tests/components/xiaomi_ble/test_device_trigger.py b/tests/components/xiaomi_ble/test_device_trigger.py index 87a4d340d8c..218a382ada5 100644 --- a/tests/components/xiaomi_ble/test_device_trigger.py +++ b/tests/components/xiaomi_ble/test_device_trigger.py @@ -18,7 +18,6 @@ from tests.common import ( MockConfigEntry, async_capture_events, async_get_device_automations, - async_mock_service, ) from tests.components.bluetooth import inject_bluetooth_service_info_bleak @@ -29,12 +28,6 @@ def get_device_id(mac: str) -> tuple[str, str]: return (BLUETOOTH_DOMAIN, mac) -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def _async_setup_xiaomi_device( hass: HomeAssistant, mac: str, data: Any | None = None ): @@ -399,7 +392,9 @@ async def test_get_triggers_for_invalid_device_id( async def test_if_fires_on_button_press( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, calls: list[ServiceCall] + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + service_calls: list[ServiceCall], ) -> None: """Test for button press event trigger firing.""" mac = "54:EF:44:E3:9C:BC" @@ -452,15 +447,17 @@ async def test_if_fires_on_button_press( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test_trigger_button_press" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test_trigger_button_press" assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() async def test_if_fires_on_double_button_long_press( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, calls: list[ServiceCall] + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + service_calls: list[ServiceCall], ) -> None: """Test for button press event trigger firing.""" mac = "DC:ED:83:87:12:73" @@ -513,15 +510,17 @@ async def test_if_fires_on_double_button_long_press( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test_trigger_right_button_press" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test_trigger_right_button_press" assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() async def test_if_fires_on_motion_detected( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, calls: list[ServiceCall] + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + service_calls: list[ServiceCall], ) -> None: """Test for motion event trigger firing.""" mac = "DE:70:E8:B2:39:0C" @@ -567,8 +566,8 @@ async def test_if_fires_on_motion_detected( ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test_trigger_motion_detected" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "test_trigger_motion_detected" assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -676,7 +675,9 @@ async def test_automation_with_invalid_trigger_event_property( async def test_triggers_for_invalid__model( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, calls: list[ServiceCall] + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + service_calls: list[ServiceCall], ) -> None: """Test invalid model doesn't return triggers.""" mac = "DE:70:E8:B2:39:0C" From baf2ebf1f29b1d140f1c88bf74af3d88b93db921 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 2 Jul 2024 12:43:34 +0200 Subject: [PATCH 0609/2411] Use fixtures in deCONZ diagnostics tests (#120968) --- tests/components/deconz/test_diagnostics.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/components/deconz/test_diagnostics.py b/tests/components/deconz/test_diagnostics.py index bfbc27b206d..64e0f417387 100644 --- a/tests/components/deconz/test_diagnostics.py +++ b/tests/components/deconz/test_diagnostics.py @@ -3,29 +3,25 @@ from pydeconz.websocket import State from syrupy import SnapshotAssertion +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .test_gateway import setup_deconz_integration - from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, mock_deconz_websocket, snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - config_entry = await setup_deconz_integration(hass, aioclient_mock) - await mock_deconz_websocket(state=State.RUNNING) await hass.async_block_till_done() assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + await get_diagnostics_for_config_entry(hass, hass_client, config_entry_setup) == snapshot ) From e322cada48dd15aa770070c0e1a0418048e49d2c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 2 Jul 2024 13:31:23 +0200 Subject: [PATCH 0610/2411] Reolink replace automatic removal of devices by manual removal (#120981) Co-authored-by: Robert Resch --- homeassistant/components/reolink/__init__.py | 87 ++++++++++---------- tests/components/reolink/test_init.py | 31 +++++-- 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 479976ad078..1caf4e79cd5 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -151,9 +151,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b firmware_coordinator=firmware_coordinator, ) - # first migrate and then cleanup, otherwise entities lost migrate_entity_ids(hass, config_entry.entry_id, host) - cleanup_disconnected_cams(hass, config_entry.entry_id, host) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -183,6 +181,50 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry +) -> bool: + """Remove a device from a config entry.""" + host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host + (device_uid, ch) = get_device_uid_and_ch(device, host) + + if not host.api.is_nvr or ch is None: + _LOGGER.warning( + "Cannot remove Reolink device %s, because it is not a camera connected " + "to a NVR/Hub, please remove the integration entry instead", + device.name, + ) + return False # Do not remove the host/NVR itself + + if ch not in host.api.channels: + _LOGGER.debug( + "Removing Reolink device %s, " + "since no camera is connected to NVR channel %s anymore", + device.name, + ch, + ) + return True + + await host.api.get_state(cmd="GetChannelstatus") # update the camera_online status + if not host.api.camera_online(ch): + _LOGGER.debug( + "Removing Reolink device %s, " + "since the camera connected to channel %s is offline", + device.name, + ch, + ) + return True + + _LOGGER.warning( + "Cannot remove Reolink device %s on channel %s, because it is still connected " + "to the NVR/Hub, please first remove the camera from the NVR/Hub " + "in the reolink app", + device.name, + ch, + ) + return False + + def get_device_uid_and_ch( device: dr.DeviceEntry, host: ReolinkHost ) -> tuple[list[str], int | None]: @@ -201,47 +243,6 @@ def get_device_uid_and_ch( return (device_uid, ch) -def cleanup_disconnected_cams( - hass: HomeAssistant, config_entry_id: str, host: ReolinkHost -) -> None: - """Clean-up disconnected camera channels.""" - if not host.api.is_nvr: - return - - device_reg = dr.async_get(hass) - devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) - for device in devices: - (device_uid, ch) = get_device_uid_and_ch(device, host) - if ch is None: - continue # Do not consider the NVR itself - - ch_model = host.api.camera_model(ch) - remove = False - if ch not in host.api.channels: - remove = True - _LOGGER.debug( - "Removing Reolink device %s, " - "since no camera is connected to NVR channel %s anymore", - device.name, - ch, - ) - if ch_model not in [device.model, "Unknown"]: - remove = True - _LOGGER.debug( - "Removing Reolink device %s, " - "since the camera model connected to channel %s changed from %s to %s", - device.name, - ch, - device.model, - ch_model, - ) - if not remove: - continue - - # clean device registry and associated entities - device_reg.async_remove_device(device.id) - - def migrate_entity_ids( hass: HomeAssistant, config_entry_id: str, host: ReolinkHost ) -> None: diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index a6c798f9415..f70fd312051 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -36,6 +36,7 @@ from .conftest import ( ) from tests.common import MockConfigEntry, async_fire_time_changed +from tests.typing import WebSocketGenerator pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") @@ -179,16 +180,27 @@ async def test_entry_reloading( None, [TEST_HOST_MODEL, TEST_CAM_MODEL], ), + ( + "is_nvr", + False, + [TEST_HOST_MODEL, TEST_CAM_MODEL], + ), ("channels", [], [TEST_HOST_MODEL]), ( - "camera_model", - Mock(return_value="RLC-567"), - [TEST_HOST_MODEL, "RLC-567"], + "camera_online", + Mock(return_value=False), + [TEST_HOST_MODEL], + ), + ( + "channel_for_uid", + Mock(return_value=-1), + [TEST_HOST_MODEL], ), ], ) -async def test_cleanup_disconnected_cams( +async def test_removing_disconnected_cams( hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, config_entry: MockConfigEntry, reolink_connect: MagicMock, device_registry: dr.DeviceRegistry, @@ -197,8 +209,10 @@ async def test_cleanup_disconnected_cams( value: Any, expected_models: list[str], ) -> None: - """Test device and entity registry are cleaned up when camera is disconnected from NVR.""" + """Test device and entity registry are cleaned up when camera is removed.""" reolink_connect.channels = [0] + assert await async_setup_component(hass, "config", {}) + client = await hass_ws_client(hass) # setup CH 0 and NVR switch entities/device with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -215,6 +229,13 @@ async def test_cleanup_disconnected_cams( setattr(reolink_connect, attr, value) with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): assert await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + expected_success = TEST_CAM_MODEL not in expected_models + for device in device_entries: + if device.model == TEST_CAM_MODEL: + response = await client.remove_device(device.id, config_entry.entry_id) + assert response["success"] == expected_success device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id From 72d706ab5259e6bd7f6af08255eba165d3ddcd69 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 2 Jul 2024 23:51:14 +1200 Subject: [PATCH 0611/2411] [ESPHome] Disable dashboard based update entities by default (#120907) Co-authored-by: Paulus Schoutsen --- homeassistant/components/esphome/update.py | 1 + tests/components/esphome/test_update.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index cb3d36dab9d..e86c88ddf5b 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -97,6 +97,7 @@ class ESPHomeDashboardUpdateEntity( _attr_title = "ESPHome" _attr_name = "Firmware" _attr_release_url = "https://esphome.io/changelog/" + _attr_entity_registry_enabled_default = False def __init__( self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboardCoordinator diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 992a6ad2ba9..c9826c3f347 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -34,6 +34,11 @@ from homeassistant.exceptions import HomeAssistantError from .conftest import MockESPHomeDevice +@pytest.fixture(autouse=True) +def enable_entity(entity_registry_enabled_by_default: None) -> None: + """Enable update entity.""" + + @pytest.fixture def stub_reconnect(): """Stub reconnect.""" From 2edb7eb42ce6bb85536a1b756363136829f8cef2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Jul 2024 13:51:44 +0200 Subject: [PATCH 0612/2411] Remove Aladdin Connect integration (#120980) --- .coveragerc | 5 - CODEOWNERS | 2 - .../components/aladdin_connect/__init__.py | 108 ++------ .../components/aladdin_connect/api.py | 33 --- .../application_credentials.py | 14 -- .../components/aladdin_connect/config_flow.py | 71 +----- .../components/aladdin_connect/const.py | 6 - .../components/aladdin_connect/coordinator.py | 38 --- .../components/aladdin_connect/cover.py | 84 ------- .../components/aladdin_connect/entity.py | 27 -- .../components/aladdin_connect/manifest.json | 8 +- .../components/aladdin_connect/ruff.toml | 5 - .../components/aladdin_connect/sensor.py | 80 ------ .../components/aladdin_connect/strings.json | 29 +-- .../generated/application_credentials.py | 1 - homeassistant/generated/config_flows.py | 1 - homeassistant/generated/integrations.json | 6 - tests/components/aladdin_connect/conftest.py | 29 --- .../aladdin_connect/test_config_flow.py | 230 ------------------ tests/components/aladdin_connect/test_init.py | 50 ++++ 20 files changed, 89 insertions(+), 738 deletions(-) delete mode 100644 homeassistant/components/aladdin_connect/api.py delete mode 100644 homeassistant/components/aladdin_connect/application_credentials.py delete mode 100644 homeassistant/components/aladdin_connect/const.py delete mode 100644 homeassistant/components/aladdin_connect/coordinator.py delete mode 100644 homeassistant/components/aladdin_connect/cover.py delete mode 100644 homeassistant/components/aladdin_connect/entity.py delete mode 100644 homeassistant/components/aladdin_connect/ruff.toml delete mode 100644 homeassistant/components/aladdin_connect/sensor.py delete mode 100644 tests/components/aladdin_connect/conftest.py delete mode 100644 tests/components/aladdin_connect/test_config_flow.py create mode 100644 tests/components/aladdin_connect/test_init.py diff --git a/.coveragerc b/.coveragerc index c3ab7f1006f..65a4c1bfc31 100644 --- a/.coveragerc +++ b/.coveragerc @@ -58,11 +58,6 @@ omit = homeassistant/components/airvisual/sensor.py homeassistant/components/airvisual_pro/__init__.py homeassistant/components/airvisual_pro/sensor.py - homeassistant/components/aladdin_connect/__init__.py - homeassistant/components/aladdin_connect/api.py - homeassistant/components/aladdin_connect/application_credentials.py - homeassistant/components/aladdin_connect/cover.py - homeassistant/components/aladdin_connect/sensor.py homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 355985b6d4c..14f8a7996bc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -80,8 +80,6 @@ build.json @home-assistant/supervisor /tests/components/airzone/ @Noltari /homeassistant/components/airzone_cloud/ @Noltari /tests/components/airzone_cloud/ @Noltari -/homeassistant/components/aladdin_connect/ @swcloudgenie -/tests/components/aladdin_connect/ @swcloudgenie /homeassistant/components/alarm_control_panel/ @home-assistant/core /tests/components/alarm_control_panel/ @home-assistant/core /homeassistant/components/alert/ @home-assistant/core @frenck diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index ed284c0e6bb..6d3f1d642b5 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -1,94 +1,38 @@ """The Aladdin Connect Genie integration.""" -# mypy: ignore-errors from __future__ import annotations -# from genie_partner_sdk.client import AladdinConnectClient -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.config_entry_oauth2_flow import ( - OAuth2Session, - async_get_config_entry_implementation, -) +from homeassistant.helpers import issue_registry as ir -from .api import AsyncConfigEntryAuth -from .const import DOMAIN -from .coordinator import AladdinConnectCoordinator - -PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR] - -type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator] +DOMAIN = "aladdin_connect" -async def async_setup_entry( - hass: HomeAssistant, entry: AladdinConnectConfigEntry -) -> bool: - """Set up Aladdin Connect Genie from a config entry.""" - implementation = await async_get_config_entry_implementation(hass, entry) - - session = OAuth2Session(hass, entry, implementation) - auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session) - coordinator = AladdinConnectCoordinator(hass, AladdinConnectClient(auth)) - - await coordinator.async_setup() - await coordinator.async_config_entry_first_refresh() - - entry.runtime_data = coordinator - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - async_remove_stale_devices(hass, entry) - - return True - - -async def async_unload_entry( - hass: HomeAssistant, entry: AladdinConnectConfigEntry -) -> bool: - """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - -async def async_migrate_entry( - hass: HomeAssistant, config_entry: AladdinConnectConfigEntry -) -> bool: - """Migrate old config.""" - if config_entry.version < 2: - config_entry.async_start_reauth(hass) - hass.config_entries.async_update_entry( - config_entry, - version=2, - minor_version=1, - ) - - return True - - -def async_remove_stale_devices( - hass: HomeAssistant, config_entry: AladdinConnectConfigEntry -) -> None: - """Remove stale devices from device registry.""" - device_registry = dr.async_get(hass) - device_entries = dr.async_entries_for_config_entry( - device_registry, config_entry.entry_id +async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool: + """Set up Aladdin Connect from a config entry.""" + ir.async_create_issue( + hass, + DOMAIN, + DOMAIN, + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="integration_removed", + translation_placeholders={ + "entries": "/config/integrations/integration/aladdin_connect", + }, ) - all_device_ids = {door.unique_id for door in config_entry.runtime_data.doors} - for device_entry in device_entries: - device_id: str | None = None + return True - for identifier in device_entry.identifiers: - if identifier[0] == DOMAIN: - device_id = identifier[1] - break - if device_id is None or device_id not in all_device_ids: - # If device_id is None an invalid device entry was found for this config entry. - # If the device_id is not in existing device ids it's a stale device entry. - # Remove config entry from this device entry in either case. - device_registry.async_update_device( - device_entry.id, remove_config_entry_id=config_entry.entry_id - ) +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if all( + config_entry.state is ConfigEntryState.NOT_LOADED + for config_entry in hass.config_entries.async_entries(DOMAIN) + if config_entry.entry_id != entry.entry_id + ): + ir.async_delete_issue(hass, DOMAIN, DOMAIN) + + return True diff --git a/homeassistant/components/aladdin_connect/api.py b/homeassistant/components/aladdin_connect/api.py deleted file mode 100644 index 4377fc8fbcb..00000000000 --- a/homeassistant/components/aladdin_connect/api.py +++ /dev/null @@ -1,33 +0,0 @@ -"""API for Aladdin Connect Genie bound to Home Assistant OAuth.""" - -# mypy: ignore-errors -from typing import cast - -from aiohttp import ClientSession - -# from genie_partner_sdk.auth import Auth -from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session - -API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1" -API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3" - - -class AsyncConfigEntryAuth(Auth): # type: ignore[misc] - """Provide Aladdin Connect Genie authentication tied to an OAuth2 based config entry.""" - - def __init__( - self, - websession: ClientSession, - oauth_session: OAuth2Session, - ) -> None: - """Initialize Aladdin Connect Genie auth.""" - super().__init__( - websession, API_URL, oauth_session.token["access_token"], API_KEY - ) - self._oauth_session = oauth_session - - async def async_get_access_token(self) -> str: - """Return a valid access token.""" - await self._oauth_session.async_ensure_token_valid() - - return cast(str, self._oauth_session.token["access_token"]) diff --git a/homeassistant/components/aladdin_connect/application_credentials.py b/homeassistant/components/aladdin_connect/application_credentials.py deleted file mode 100644 index e8e959f1fa3..00000000000 --- a/homeassistant/components/aladdin_connect/application_credentials.py +++ /dev/null @@ -1,14 +0,0 @@ -"""application_credentials platform the Aladdin Connect Genie integration.""" - -from homeassistant.components.application_credentials import AuthorizationServer -from homeassistant.core import HomeAssistant - -from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN - - -async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: - """Return authorization server.""" - return AuthorizationServer( - authorize_url=OAUTH2_AUTHORIZE, - token_url=OAUTH2_TOKEN, - ) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 507085fa27f..a508ff89c68 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -1,70 +1,11 @@ -"""Config flow for Aladdin Connect Genie.""" +"""Config flow for Aladdin Connect integration.""" -from collections.abc import Mapping -import logging -from typing import Any +from homeassistant.config_entries import ConfigFlow -import jwt - -from homeassistant.config_entries import ConfigEntry, ConfigFlowResult -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN -from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler - -from .const import DOMAIN +from . import DOMAIN -class AladdinConnectOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): - """Config flow to handle Aladdin Connect Genie OAuth2 authentication.""" +class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Aladdin Connect.""" - DOMAIN = DOMAIN - VERSION = 2 - MINOR_VERSION = 1 - - reauth_entry: ConfigEntry | None = None - - async def async_step_reauth( - self, user_input: Mapping[str, Any] - ) -> ConfigFlowResult: - """Perform reauth upon API auth error or upgrade from v1 to v2.""" - self.reauth_entry = self.hass.config_entries.async_get_entry( - self.context["entry_id"] - ) - return await self.async_step_reauth_confirm() - - async def async_step_reauth_confirm( - self, user_input: Mapping[str, Any] | None = None - ) -> ConfigFlowResult: - """Dialog that informs the user that reauth is required.""" - if user_input is None: - return self.async_show_form(step_id="reauth_confirm") - return await self.async_step_user() - - async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: - """Create an oauth config entry or update existing entry for reauth.""" - token_payload = jwt.decode( - data[CONF_TOKEN][CONF_ACCESS_TOKEN], options={"verify_signature": False} - ) - if not self.reauth_entry: - await self.async_set_unique_id(token_payload["sub"]) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=token_payload["username"], - data=data, - ) - - if self.reauth_entry.unique_id == token_payload["username"]: - return self.async_update_reload_and_abort( - self.reauth_entry, - data=data, - unique_id=token_payload["sub"], - ) - if self.reauth_entry.unique_id == token_payload["sub"]: - return self.async_update_reload_and_abort(self.reauth_entry, data=data) - - return self.async_abort(reason="wrong_account") - - @property - def logger(self) -> logging.Logger: - """Return logger.""" - return logging.getLogger(__name__) + VERSION = 1 diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py deleted file mode 100644 index a87147c8f09..00000000000 --- a/homeassistant/components/aladdin_connect/const.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Constants for the Aladdin Connect Genie integration.""" - -DOMAIN = "aladdin_connect" - -OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html" -OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token" diff --git a/homeassistant/components/aladdin_connect/coordinator.py b/homeassistant/components/aladdin_connect/coordinator.py deleted file mode 100644 index 9af3e330409..00000000000 --- a/homeassistant/components/aladdin_connect/coordinator.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Define an object to coordinate fetching Aladdin Connect data.""" - -# mypy: ignore-errors -from datetime import timedelta -import logging - -# from genie_partner_sdk.client import AladdinConnectClient -# from genie_partner_sdk.model import GarageDoor -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -class AladdinConnectCoordinator(DataUpdateCoordinator[None]): - """Aladdin Connect Data Update Coordinator.""" - - def __init__(self, hass: HomeAssistant, acc: AladdinConnectClient) -> None: - """Initialize.""" - super().__init__( - hass, - logger=_LOGGER, - name=DOMAIN, - update_interval=timedelta(seconds=15), - ) - self.acc = acc - self.doors: list[GarageDoor] = [] - - async def async_setup(self) -> None: - """Fetch initial data.""" - self.doors = await self.acc.get_doors() - - async def _async_update_data(self) -> None: - """Fetch data from API endpoint.""" - for door in self.doors: - await self.acc.update_door(door.device_id, door.door_number) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py deleted file mode 100644 index 1be41e6b516..00000000000 --- a/homeassistant/components/aladdin_connect/cover.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Cover Entity for Genie Garage Door.""" - -# mypy: ignore-errors -from typing import Any - -# from genie_partner_sdk.model import GarageDoor -from homeassistant.components.cover import ( - CoverDeviceClass, - CoverEntity, - CoverEntityFeature, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import AladdinConnectConfigEntry, AladdinConnectCoordinator -from .entity import AladdinConnectEntity - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: AladdinConnectConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the Aladdin Connect platform.""" - coordinator = config_entry.runtime_data - - async_add_entities(AladdinDevice(coordinator, door) for door in coordinator.doors) - - -class AladdinDevice(AladdinConnectEntity, CoverEntity): - """Representation of Aladdin Connect cover.""" - - _attr_device_class = CoverDeviceClass.GARAGE - _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - _attr_name = None - - def __init__( - self, coordinator: AladdinConnectCoordinator, device: GarageDoor - ) -> None: - """Initialize the Aladdin Connect cover.""" - super().__init__(coordinator, device) - self._attr_unique_id = device.unique_id - - async def async_open_cover(self, **kwargs: Any) -> None: - """Issue open command to cover.""" - await self.coordinator.acc.open_door( - self._device.device_id, self._device.door_number - ) - - async def async_close_cover(self, **kwargs: Any) -> None: - """Issue close command to cover.""" - await self.coordinator.acc.close_door( - self._device.device_id, self._device.door_number - ) - - @property - def is_closed(self) -> bool | None: - """Update is closed attribute.""" - value = self.coordinator.acc.get_door_status( - self._device.device_id, self._device.door_number - ) - if value is None: - return None - return bool(value == "closed") - - @property - def is_closing(self) -> bool | None: - """Update is closing attribute.""" - value = self.coordinator.acc.get_door_status( - self._device.device_id, self._device.door_number - ) - if value is None: - return None - return bool(value == "closing") - - @property - def is_opening(self) -> bool | None: - """Update is opening attribute.""" - value = self.coordinator.acc.get_door_status( - self._device.device_id, self._device.door_number - ) - if value is None: - return None - return bool(value == "opening") diff --git a/homeassistant/components/aladdin_connect/entity.py b/homeassistant/components/aladdin_connect/entity.py deleted file mode 100644 index 2615cbc636e..00000000000 --- a/homeassistant/components/aladdin_connect/entity.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Defines a base Aladdin Connect entity.""" -# mypy: ignore-errors -# from genie_partner_sdk.model import GarageDoor - -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import DOMAIN -from .coordinator import AladdinConnectCoordinator - - -class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]): - """Defines a base Aladdin Connect entity.""" - - _attr_has_entity_name = True - - def __init__( - self, coordinator: AladdinConnectCoordinator, device: GarageDoor - ) -> None: - """Initialize the entity.""" - super().__init__(coordinator) - self._device = device - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device.unique_id)}, - name=device.name, - manufacturer="Overhead Door", - ) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index dce95492272..adf0d9c9b5b 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -1,11 +1,9 @@ { "domain": "aladdin_connect", "name": "Aladdin Connect", - "codeowners": ["@swcloudgenie"], - "config_flow": true, - "dependencies": ["application_credentials"], - "disabled": "This integration is disabled because it uses non-open source code to operate.", + "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", + "integration_type": "system", "iot_class": "cloud_polling", - "requirements": ["genie-partner-sdk==1.0.2"] + "requirements": [] } diff --git a/homeassistant/components/aladdin_connect/ruff.toml b/homeassistant/components/aladdin_connect/ruff.toml deleted file mode 100644 index 38f6f586aef..00000000000 --- a/homeassistant/components/aladdin_connect/ruff.toml +++ /dev/null @@ -1,5 +0,0 @@ -extend = "../../../pyproject.toml" - -lint.extend-ignore = [ - "F821" -] diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py deleted file mode 100644 index cd1fff12c97..00000000000 --- a/homeassistant/components/aladdin_connect/sensor.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Support for Aladdin Connect Garage Door sensors.""" - -# mypy: ignore-errors -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass - -# from genie_partner_sdk.client import AladdinConnectClient -# from genie_partner_sdk.model import GarageDoor -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import PERCENTAGE -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import AladdinConnectConfigEntry, AladdinConnectCoordinator -from .entity import AladdinConnectEntity - - -@dataclass(frozen=True, kw_only=True) -class AccSensorEntityDescription(SensorEntityDescription): - """Describes AladdinConnect sensor entity.""" - - value_fn: Callable[[AladdinConnectClient, str, int], float | None] - - -SENSORS: tuple[AccSensorEntityDescription, ...] = ( - AccSensorEntityDescription( - key="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, - ), -) - - -async def async_setup_entry( - hass: HomeAssistant, - entry: AladdinConnectConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Aladdin Connect sensor devices.""" - coordinator = entry.runtime_data - - async_add_entities( - AladdinConnectSensor(coordinator, door, description) - for description in SENSORS - for door in coordinator.doors - ) - - -class AladdinConnectSensor(AladdinConnectEntity, SensorEntity): - """A sensor implementation for Aladdin Connect devices.""" - - entity_description: AccSensorEntityDescription - - def __init__( - self, - coordinator: AladdinConnectCoordinator, - device: GarageDoor, - description: AccSensorEntityDescription, - ) -> None: - """Initialize a sensor for an Aladdin Connect device.""" - super().__init__(coordinator, device) - self.entity_description = description - self._attr_unique_id = f"{device.unique_id}-{description.key}" - - @property - def native_value(self) -> float | None: - """Return the state of the sensor.""" - return self.entity_description.value_fn( - self.coordinator.acc, self._device.device_id, self._device.door_number - ) diff --git a/homeassistant/components/aladdin_connect/strings.json b/homeassistant/components/aladdin_connect/strings.json index 48f9b299a1d..f62e68de64e 100644 --- a/homeassistant/components/aladdin_connect/strings.json +++ b/homeassistant/components/aladdin_connect/strings.json @@ -1,29 +1,8 @@ { - "config": { - "step": { - "pick_implementation": { - "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" - }, - "reauth_confirm": { - "title": "[%key:common::config_flow::title::reauth%]", - "description": "Aladdin Connect needs to re-authenticate your account" - } - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", - "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", - "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", - "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", - "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - }, - "create_entry": { - "default": "[%key:common::config_flow::create_entry::authenticated%]" + "issues": { + "integration_removed": { + "title": "The Aladdin Connect integration has been removed", + "description": "The Aladdin Connect integration has been removed from Home Assistant.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Aladdin Connect integration entries]({entries})." } } } diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index bc6b29e4c23..c576f242e30 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -4,7 +4,6 @@ To update, run python3 -m script.hassfest """ APPLICATION_CREDENTIALS = [ - "aladdin_connect", "electric_kiwi", "fitbit", "geocaching", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 23a13bcbfd8..463a38feb9f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -42,7 +42,6 @@ FLOWS = { "airvisual_pro", "airzone", "airzone_cloud", - "aladdin_connect", "alarmdecoder", "amberelectric", "ambient_network", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3371c8de0fa..0ad8ac09c9e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -180,12 +180,6 @@ } } }, - "aladdin_connect": { - "name": "Aladdin Connect", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_polling" - }, "alarmdecoder": { "name": "AlarmDecoder", "integration_type": "device", diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py deleted file mode 100644 index 8399269b30d..00000000000 --- a/tests/components/aladdin_connect/conftest.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Test fixtures for the Aladdin Connect Garage Door integration.""" - -from collections.abc import Generator -from unittest.mock import AsyncMock, patch - -import pytest - -from tests.common import MockConfigEntry - - -@pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock]: - """Override async_setup_entry.""" - with patch( - "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True - ) as mock_setup_entry: - yield mock_setup_entry - - -@pytest.fixture -def mock_config_entry() -> MockConfigEntry: - """Return an Aladdin Connect config entry.""" - return MockConfigEntry( - domain="aladdin_connect", - data={}, - title="test@test.com", - unique_id="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - version=2, - ) diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py deleted file mode 100644 index 7154c53b9f6..00000000000 --- a/tests/components/aladdin_connect/test_config_flow.py +++ /dev/null @@ -1,230 +0,0 @@ -"""Test the Aladdin Connect Garage Door config flow.""" - -# from unittest.mock import AsyncMock -# -# import pytest -# -# from homeassistant.components.aladdin_connect.const import ( -# DOMAIN, -# OAUTH2_AUTHORIZE, -# OAUTH2_TOKEN, -# ) -# from homeassistant.components.application_credentials import ( -# ClientCredential, -# async_import_client_credential, -# ) -# from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigFlowResult -# from homeassistant.core import HomeAssistant -# from homeassistant.data_entry_flow import FlowResultType -# from homeassistant.helpers import config_entry_oauth2_flow -# from homeassistant.setup import async_setup_component -# -# from tests.common import MockConfigEntry -# from tests.test_util.aiohttp import AiohttpClientMocker -# from tests.typing import ClientSessionGenerator -# -# CLIENT_ID = "1234" -# CLIENT_SECRET = "5678" -# -# EXAMPLE_TOKEN = ( -# "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYWFhYWFhYS1iYmJiLWNjY2MtZGRk" -# "ZC1lZWVlZWVlZWVlZWUiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW" -# "1lIjoidGVzdEB0ZXN0LmNvbSJ9.CTU1YItIrUl8nSM3koJxlFJr5CjLghgc9gS6h45D8dE" -# ) -# -# -# @pytest.fixture -# async def setup_credentials(hass: HomeAssistant) -> None: -# """Fixture to setup credentials.""" -# assert await async_setup_component(hass, "application_credentials", {}) -# await async_import_client_credential( -# hass, -# DOMAIN, -# ClientCredential(CLIENT_ID, CLIENT_SECRET), -# ) -# -# -# async def _oauth_actions( -# hass: HomeAssistant, -# result: ConfigFlowResult, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# ) -> None: -# state = config_entry_oauth2_flow._encode_jwt( -# hass, -# { -# "flow_id": result["flow_id"], -# "redirect_uri": "https://example.com/auth/external/callback", -# }, -# ) -# -# assert result["url"] == ( -# f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" -# "&redirect_uri=https://example.com/auth/external/callback" -# f"&state={state}" -# ) -# -# client = await hass_client_no_auth() -# resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") -# assert resp.status == 200 -# assert resp.headers["content-type"] == "text/html; charset=utf-8" -# -# aioclient_mock.post( -# OAUTH2_TOKEN, -# json={ -# "refresh_token": "mock-refresh-token", -# "access_token": EXAMPLE_TOKEN, -# "type": "Bearer", -# "expires_in": 60, -# }, -# ) -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_full_flow( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Check full flow.""" -# result = await hass.config_entries.flow.async_init( -# DOMAIN, context={"source": SOURCE_USER} -# ) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.CREATE_ENTRY -# assert result["title"] == "test@test.com" -# assert result["data"]["token"]["access_token"] == EXAMPLE_TOKEN -# assert result["data"]["token"]["refresh_token"] == "mock-refresh-token" -# assert result["result"].unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" -# -# assert len(hass.config_entries.async_entries(DOMAIN)) == 1 -# assert len(mock_setup_entry.mock_calls) == 1 -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_duplicate_entry( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_config_entry: MockConfigEntry, -# ) -> None: -# """Test we abort with duplicate entry.""" -# mock_config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, context={"source": SOURCE_USER} -# ) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "already_configured" -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_reauth( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_config_entry: MockConfigEntry, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Test reauthentication.""" -# mock_config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, -# context={ -# "source": SOURCE_REAUTH, -# "entry_id": mock_config_entry.entry_id, -# }, -# data=mock_config_entry.data, -# ) -# assert result["type"] is FlowResultType.FORM -# assert result["step_id"] == "reauth_confirm" -# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "reauth_successful" -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_reauth_wrong_account( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Test reauthentication with wrong account.""" -# config_entry = MockConfigEntry( -# domain=DOMAIN, -# data={}, -# title="test@test.com", -# unique_id="aaaaaaaa-bbbb-ffff-dddd-eeeeeeeeeeee", -# version=2, -# ) -# config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, -# context={ -# "source": SOURCE_REAUTH, -# "entry_id": config_entry.entry_id, -# }, -# data=config_entry.data, -# ) -# assert result["type"] is FlowResultType.FORM -# assert result["step_id"] == "reauth_confirm" -# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "wrong_account" -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_reauth_old_account( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Test reauthentication with old account.""" -# config_entry = MockConfigEntry( -# domain=DOMAIN, -# data={}, -# title="test@test.com", -# unique_id="test@test.com", -# version=2, -# ) -# config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, -# context={ -# "source": SOURCE_REAUTH, -# "entry_id": config_entry.entry_id, -# }, -# data=config_entry.data, -# ) -# assert result["type"] is FlowResultType.FORM -# assert result["step_id"] == "reauth_confirm" -# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "reauth_successful" -# assert config_entry.unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py new file mode 100644 index 00000000000..b01af287b7b --- /dev/null +++ b/tests/components/aladdin_connect/test_init.py @@ -0,0 +1,50 @@ +"""Tests for the Aladdin Connect integration.""" + +from homeassistant.components.aladdin_connect import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from tests.common import MockConfigEntry + + +async def test_aladdin_connect_repair_issue( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test the Aladdin Connect configuration entry loading/unloading handles the repair.""" + config_entry_1 = MockConfigEntry( + title="Example 1", + domain=DOMAIN, + ) + config_entry_1.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_1.entry_id) + await hass.async_block_till_done() + assert config_entry_1.state is ConfigEntryState.LOADED + + # Add a second one + config_entry_2 = MockConfigEntry( + title="Example 2", + domain=DOMAIN, + ) + config_entry_2.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_2.entry_id) + await hass.async_block_till_done() + + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + + # Remove the first one + await hass.config_entries.async_remove(config_entry_1.entry_id) + await hass.async_block_till_done() + + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + + # Remove the second one + await hass.config_entries.async_remove(config_entry_2.entry_id) + await hass.async_block_till_done() + + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.NOT_LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None From bd234db48f91d910e5dbefc493b03592141337a6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 13:58:11 +0200 Subject: [PATCH 0613/2411] Improve type hints in analytics tests (#121012) --- tests/components/analytics/test_analytics.py | 39 ++++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 60882cda874..28272cd8866 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -19,7 +19,6 @@ from homeassistant.components.analytics.const import ( ATTR_STATISTICS, ATTR_USAGE, ) -from homeassistant.components.recorder import Recorder from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -36,7 +35,7 @@ MOCK_VERSION_NIGHTLY = "1970.1.0.dev19700101" @pytest.fixture(autouse=True) -def uuid_mock() -> Generator[Any, Any, None]: +def uuid_mock() -> Generator[None]: """Mock the UUID.""" with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex_mock: hex_mock.return_value = MOCK_UUID @@ -44,7 +43,7 @@ def uuid_mock() -> Generator[Any, Any, None]: @pytest.fixture(autouse=True) -def ha_version_mock() -> Generator[Any, Any, None]: +def ha_version_mock() -> Generator[None]: """Mock the core version.""" with patch( "homeassistant.components.analytics.analytics.HA_VERSION", @@ -54,7 +53,7 @@ def ha_version_mock() -> Generator[Any, Any, None]: @pytest.fixture -def installation_type_mock() -> Generator[Any, Any, None]: +def installation_type_mock() -> Generator[None]: """Mock the async_get_system_info.""" with patch( "homeassistant.components.analytics.analytics.async_get_system_info", @@ -160,11 +159,11 @@ async def test_failed_to_send_raises( assert "Error sending analytics" in caplog.text +@pytest.mark.usefixtures("installation_type_mock") async def test_send_base( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, - installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: """Test send base preferences are defined.""" @@ -231,11 +230,11 @@ async def test_send_base_with_supervisor( assert snapshot == submitted_data +@pytest.mark.usefixtures("installation_type_mock") async def test_send_usage( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, - installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: """Test send usage preferences are defined.""" @@ -331,11 +330,11 @@ async def test_send_usage_with_supervisor( assert snapshot == submitted_data +@pytest.mark.usefixtures("installation_type_mock") async def test_send_statistics( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, - installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: """Test send statistics preferences are defined.""" @@ -382,12 +381,11 @@ async def test_send_statistics_one_integration_fails( assert post_call[2]["integration_count"] == 0 -@pytest.mark.usefixtures("mock_hass_config") +@pytest.mark.usefixtures("installation_type_mock", "mock_hass_config") async def test_send_statistics_disabled_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, - installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: """Test send statistics with disabled integration.""" @@ -420,12 +418,11 @@ async def test_send_statistics_disabled_integration( assert snapshot == submitted_data -@pytest.mark.usefixtures("mock_hass_config") +@pytest.mark.usefixtures("installation_type_mock", "mock_hass_config") async def test_send_statistics_ignored_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, - installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: """Test send statistics with ignored integration.""" @@ -566,12 +563,11 @@ async def test_reusing_uuid( assert analytics.uuid == "NOT_MOCK_UUID" -@pytest.mark.usefixtures("enable_custom_integrations") +@pytest.mark.usefixtures("enable_custom_integrations", "installation_type_mock") async def test_custom_integrations( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, - installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: """Test sending custom integrations.""" @@ -651,12 +647,11 @@ async def test_nightly_endpoint( assert str(payload[1]) == ANALYTICS_ENDPOINT_URL -@pytest.mark.usefixtures("mock_hass_config") +@pytest.mark.usefixtures("installation_type_mock", "mock_hass_config") async def test_send_with_no_energy( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, - installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: """Test send base preferences are defined.""" @@ -688,12 +683,11 @@ async def test_send_with_no_energy( assert snapshot == submitted_data -@pytest.mark.usefixtures("recorder_mock", "mock_hass_config") +@pytest.mark.usefixtures("recorder_mock", "installation_type_mock", "mock_hass_config") async def test_send_with_no_energy_config( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, - installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: """Test send base preferences are defined.""" @@ -720,12 +714,11 @@ async def test_send_with_no_energy_config( ) -@pytest.mark.usefixtures("recorder_mock", "mock_hass_config") +@pytest.mark.usefixtures("recorder_mock", "installation_type_mock", "mock_hass_config") async def test_send_with_energy_config( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, - installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: """Test send base preferences are defined.""" @@ -752,12 +745,11 @@ async def test_send_with_energy_config( ) -@pytest.mark.usefixtures("mock_hass_config") +@pytest.mark.usefixtures("installation_type_mock", "mock_hass_config") async def test_send_usage_with_certificate( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, - installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: """Test send usage preferences with certificate.""" @@ -779,12 +771,11 @@ async def test_send_usage_with_certificate( assert snapshot == submitted_data +@pytest.mark.usefixtures("recorder_mock", "installation_type_mock") async def test_send_with_recorder( - recorder_mock: Recorder, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, - installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: """Test recorder information.""" @@ -849,11 +840,11 @@ async def test_timeout_while_sending( assert "Timeout sending analytics" in caplog.text +@pytest.mark.usefixtures("installation_type_mock") async def test_not_check_config_entries_if_yaml( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, - installation_type_mock: Generator[Any, Any, None], snapshot: SnapshotAssertion, ) -> None: """Test skip config entry check if defined in yaml.""" From faf43ed4c722b7fa6b50f1aeb564c5ea581767c7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 14:12:02 +0200 Subject: [PATCH 0614/2411] Adjust Generator type hints in tests (#121013) --- tests/components/drop_connect/test_sensor.py | 2 +- tests/components/flume/conftest.py | 2 +- tests/components/homekit/conftest.py | 11 +++++------ tests/components/incomfort/conftest.py | 3 +-- tests/components/tibber/conftest.py | 2 +- tests/util/test_loop.py | 2 +- 6 files changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/components/drop_connect/test_sensor.py b/tests/components/drop_connect/test_sensor.py index 4873d1edbd1..cb56522a09d 100644 --- a/tests/components/drop_connect/test_sensor.py +++ b/tests/components/drop_connect/test_sensor.py @@ -47,7 +47,7 @@ from tests.typing import MqttMockHAClient @pytest.fixture(autouse=True) -def only_sensor_platform() -> Generator[[], None]: +def only_sensor_platform() -> Generator[None]: """Only setup the DROP sensor platform.""" with patch("homeassistant.components.drop_connect.PLATFORMS", [Platform.SENSOR]): yield diff --git a/tests/components/flume/conftest.py b/tests/components/flume/conftest.py index 999bbd70ce8..fb0d0157bbc 100644 --- a/tests/components/flume/conftest.py +++ b/tests/components/flume/conftest.py @@ -104,7 +104,7 @@ def encode_access_token() -> str: @pytest.fixture(name="access_token") -def access_token_fixture(requests_mock: Mocker) -> Generator[None, None, None]: +def access_token_fixture(requests_mock: Mocker) -> Generator[None]: """Fixture to setup the access token.""" token_response = { "refresh_token": REFRESH_TOKEN, diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 26333b0b807..7acb11e10ab 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -4,7 +4,6 @@ from asyncio import AbstractEventLoop from collections.abc import Generator from contextlib import suppress import os -from typing import Any from unittest.mock import MagicMock, patch import pytest @@ -19,7 +18,7 @@ from tests.common import async_capture_events @pytest.fixture -def iid_storage(hass): +def iid_storage(hass: HomeAssistant) -> Generator[AccessoryIIDStorage]: """Mock the iid storage.""" with patch.object(AccessoryIIDStorage, "_async_schedule_save"): yield AccessoryIIDStorage(hass, "") @@ -28,7 +27,7 @@ def iid_storage(hass): @pytest.fixture def run_driver( hass: HomeAssistant, event_loop: AbstractEventLoop, iid_storage: AccessoryIIDStorage -) -> Generator[HomeDriver, Any, None]: +) -> Generator[HomeDriver]: """Return a custom AccessoryDriver instance for HomeKit accessory init. This mock does not mock async_stop, so the driver will not be stopped @@ -57,7 +56,7 @@ def run_driver( @pytest.fixture def hk_driver( hass: HomeAssistant, event_loop: AbstractEventLoop, iid_storage: AccessoryIIDStorage -) -> Generator[HomeDriver, Any, None]: +) -> Generator[HomeDriver]: """Return a custom AccessoryDriver instance for HomeKit accessory init.""" with ( patch("pyhap.accessory_driver.AsyncZeroconf"), @@ -89,7 +88,7 @@ def mock_hap( event_loop: AbstractEventLoop, iid_storage: AccessoryIIDStorage, mock_zeroconf: MagicMock, -) -> Generator[HomeDriver, Any, None]: +) -> Generator[HomeDriver]: """Return a custom AccessoryDriver instance for HomeKit accessory init.""" with ( patch("pyhap.accessory_driver.AsyncZeroconf"), @@ -128,7 +127,7 @@ def events(hass): @pytest.fixture -def demo_cleanup(hass): +def demo_cleanup(hass: HomeAssistant) -> Generator[None]: """Clean up device tracker demo file.""" yield with suppress(FileNotFoundError): diff --git a/tests/components/incomfort/conftest.py b/tests/components/incomfort/conftest.py index 122868605c8..f17547a1445 100644 --- a/tests/components/incomfort/conftest.py +++ b/tests/components/incomfort/conftest.py @@ -77,10 +77,9 @@ def mock_room_status() -> dict[str, Any]: @pytest.fixture def mock_incomfort( - hass: HomeAssistant, mock_heater_status: dict[str, Any], mock_room_status: dict[str, Any], -) -> Generator[MagicMock, None]: +) -> Generator[MagicMock]: """Mock the InComfort gateway client.""" class MockRoom: diff --git a/tests/components/tibber/conftest.py b/tests/components/tibber/conftest.py index fc6596444c5..0b48531bde1 100644 --- a/tests/components/tibber/conftest.py +++ b/tests/components/tibber/conftest.py @@ -27,7 +27,7 @@ def config_entry(hass: HomeAssistant) -> MockConfigEntry: @pytest.fixture async def mock_tibber_setup( config_entry: MockConfigEntry, hass: HomeAssistant -) -> AsyncGenerator[None, MagicMock]: +) -> AsyncGenerator[MagicMock]: """Mock tibber entry setup.""" unique_user_id = "unique_user_id" title = "title" diff --git a/tests/util/test_loop.py b/tests/util/test_loop.py index f4846d98898..3ff7128938f 100644 --- a/tests/util/test_loop.py +++ b/tests/util/test_loop.py @@ -18,7 +18,7 @@ def banned_function(): @contextlib.contextmanager -def patch_get_current_frame(stack: list[Mock]) -> Generator[None, None, None]: +def patch_get_current_frame(stack: list[Mock]) -> Generator[None]: """Patch get_current_frame.""" frames = extract_stack_to_frame(stack) with ( From b8b7c23258eb1c8c80cde2d161e0e6261dfa96e5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Jul 2024 15:48:35 +0200 Subject: [PATCH 0615/2411] Create log files in an executor thread (#120912) --- homeassistant/bootstrap.py | 59 +++++++++++++++++++++----------------- tests/test_bootstrap.py | 28 +++++++++--------- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 8435fe73d40..c5229634053 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -8,7 +8,7 @@ import contextlib from functools import partial from itertools import chain import logging -import logging.handlers +from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler import mimetypes from operator import contains, itemgetter import os @@ -257,12 +257,12 @@ async def async_setup_hass( ) -> core.HomeAssistant | None: """Set up Home Assistant.""" - def create_hass() -> core.HomeAssistant: + async def create_hass() -> core.HomeAssistant: """Create the hass object and do basic setup.""" hass = core.HomeAssistant(runtime_config.config_dir) loader.async_setup(hass) - async_enable_logging( + await async_enable_logging( hass, runtime_config.verbose, runtime_config.log_rotate_days, @@ -287,7 +287,7 @@ async def async_setup_hass( async with hass.timeout.async_timeout(10): await hass.async_stop() - hass = create_hass() + hass = await create_hass() if runtime_config.skip_pip or runtime_config.skip_pip_packages: _LOGGER.warning( @@ -326,13 +326,13 @@ async def async_setup_hass( if config_dict is None: recovery_mode = True await stop_hass(hass) - hass = create_hass() + hass = await create_hass() elif not basic_setup_success: _LOGGER.warning("Unable to set up core integrations. Activating recovery mode") recovery_mode = True await stop_hass(hass) - hass = create_hass() + hass = await create_hass() elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS): _LOGGER.warning( @@ -345,7 +345,7 @@ async def async_setup_hass( recovery_mode = True await stop_hass(hass) - hass = create_hass() + hass = await create_hass() if old_logging: hass.data[DATA_LOGGING] = old_logging @@ -523,8 +523,7 @@ async def async_from_config_dict( return hass -@core.callback -def async_enable_logging( +async def async_enable_logging( hass: core.HomeAssistant, verbose: bool = False, log_rotate_days: int | None = None, @@ -607,23 +606,9 @@ def async_enable_logging( if (err_path_exists and os.access(err_log_path, os.W_OK)) or ( not err_path_exists and os.access(err_dir, os.W_OK) ): - err_handler: ( - logging.handlers.RotatingFileHandler - | logging.handlers.TimedRotatingFileHandler + err_handler = await hass.async_add_executor_job( + _create_log_file, err_log_path, log_rotate_days ) - if log_rotate_days: - err_handler = logging.handlers.TimedRotatingFileHandler( - err_log_path, when="midnight", backupCount=log_rotate_days - ) - else: - err_handler = _RotatingFileHandlerWithoutShouldRollOver( - err_log_path, backupCount=1 - ) - - try: - err_handler.doRollover() - except OSError as err: - _LOGGER.error("Error rolling over log file: %s", err) err_handler.setLevel(logging.INFO if verbose else logging.WARNING) err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME)) @@ -640,7 +625,29 @@ def async_enable_logging( async_activate_log_queue_handler(hass) -class _RotatingFileHandlerWithoutShouldRollOver(logging.handlers.RotatingFileHandler): +def _create_log_file( + err_log_path: str, log_rotate_days: int | None +) -> RotatingFileHandler | TimedRotatingFileHandler: + """Create log file and do roll over.""" + err_handler: RotatingFileHandler | TimedRotatingFileHandler + if log_rotate_days: + err_handler = TimedRotatingFileHandler( + err_log_path, when="midnight", backupCount=log_rotate_days + ) + else: + err_handler = _RotatingFileHandlerWithoutShouldRollOver( + err_log_path, backupCount=1 + ) + + try: + err_handler.doRollover() + except OSError as err: + _LOGGER.error("Error rolling over log file: %s", err) + + return err_handler + + +class _RotatingFileHandlerWithoutShouldRollOver(RotatingFileHandler): """RotatingFileHandler that does not check if it should roll over on every log.""" def shouldRollover(self, record: logging.LogRecord) -> bool: diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 7f3793e99e2..7bb5624d112 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -69,7 +69,7 @@ def mock_http_start_stop() -> Generator[None]: yield -@patch("homeassistant.bootstrap.async_enable_logging", Mock()) +@patch("homeassistant.bootstrap.async_enable_logging", AsyncMock()) async def test_home_assistant_core_config_validation(hass: HomeAssistant) -> None: """Test if we pass in wrong information for HA conf.""" # Extensive HA conf validation testing is done @@ -93,10 +93,10 @@ async def test_async_enable_logging( side_effect=OSError, ), ): - bootstrap.async_enable_logging(hass) + await bootstrap.async_enable_logging(hass) mock_async_activate_log_queue_handler.assert_called_once() mock_async_activate_log_queue_handler.reset_mock() - bootstrap.async_enable_logging( + await bootstrap.async_enable_logging( hass, log_rotate_days=5, log_file="test.log", @@ -140,7 +140,7 @@ async def test_config_does_not_turn_off_debug(hass: HomeAssistant) -> None: @pytest.mark.parametrize("hass_config", [{"frontend": {}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_asyncio_debug_on_turns_hass_debug_on( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -597,7 +597,7 @@ def mock_is_virtual_env() -> Generator[Mock]: @pytest.fixture -def mock_enable_logging() -> Generator[Mock]: +def mock_enable_logging() -> Generator[AsyncMock]: """Mock enable logging.""" with patch("homeassistant.bootstrap.async_enable_logging") as enable_logging: yield enable_logging @@ -633,7 +633,7 @@ def mock_ensure_config_exists() -> Generator[AsyncMock]: @pytest.mark.parametrize("hass_config", [{"browser": {}, "frontend": {}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -686,7 +686,7 @@ async def test_setup_hass( @pytest.mark.parametrize("hass_config", [{"browser": {}, "frontend": {}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_takes_longer_than_log_slow_startup( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -727,7 +727,7 @@ async def test_setup_hass_takes_longer_than_log_slow_startup( async def test_setup_hass_invalid_yaml( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -754,7 +754,7 @@ async def test_setup_hass_invalid_yaml( async def test_setup_hass_config_dir_nonexistent( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -780,7 +780,7 @@ async def test_setup_hass_config_dir_nonexistent( async def test_setup_hass_recovery_mode( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -816,7 +816,7 @@ async def test_setup_hass_recovery_mode( @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_safe_mode( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -851,7 +851,7 @@ async def test_setup_hass_safe_mode( @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_recovery_mode_and_safe_mode( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -887,7 +887,7 @@ async def test_setup_hass_recovery_mode_and_safe_mode( @pytest.mark.parametrize("hass_config", [{"homeassistant": {"non-existing": 1}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_invalid_core_config( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -926,7 +926,7 @@ async def test_setup_hass_invalid_core_config( ) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_recovery_mode_if_no_frontend( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, From 5b3998986960b37c7925fdee48f27c3839fb5713 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Jul 2024 15:52:54 +0200 Subject: [PATCH 0616/2411] Fix typo in post_schema_migration (#121017) --- homeassistant/components/recorder/migration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index cf003f72af4..83f89fa8995 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -1181,7 +1181,7 @@ def post_schema_migration( _wipe_old_string_time_columns(instance, instance.engine, instance.event_session) if old_version < 35 <= new_version: # In version 34 we migrated all the created, start, and last_reset - # columns to be timestamps. In version 34 we need to wipe the old columns + # columns to be timestamps. In version 35 we need to wipe the old columns # since they are no longer used and take up a significant amount of space. _wipe_old_string_statistics_columns(instance) From 195f07a18a79034c3583596d0c95325ba1623ce9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:02:17 +0200 Subject: [PATCH 0617/2411] Use service_calls fixture in nest tests (#120987) --- tests/components/nest/test_device_trigger.py | 46 +++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index 1820096d2a6..f818713d382 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -20,7 +20,7 @@ from homeassistant.util.dt import utcnow from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup -from tests.common import async_get_device_automations, async_mock_service +from tests.common import async_get_device_automations DEVICE_NAME = "My Camera" DATA_MESSAGE = {"message": "service-called"} @@ -83,12 +83,6 @@ async def setup_automation(hass, device_id, trigger_type): ) -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -248,7 +242,7 @@ async def test_fires_on_camera_motion( device_registry: dr.DeviceRegistry, create_device: CreateDevice, setup_platform: PlatformSetup, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test camera_motion triggers firing.""" create_device.create( @@ -273,8 +267,8 @@ async def test_fires_on_camera_motion( } hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data == DATA_MESSAGE + assert len(service_calls) == 1 + assert service_calls[0].data == DATA_MESSAGE async def test_fires_on_camera_person( @@ -282,7 +276,7 @@ async def test_fires_on_camera_person( device_registry: dr.DeviceRegistry, create_device: CreateDevice, setup_platform: PlatformSetup, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test camera_person triggers firing.""" create_device.create( @@ -307,8 +301,8 @@ async def test_fires_on_camera_person( } hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data == DATA_MESSAGE + assert len(service_calls) == 1 + assert service_calls[0].data == DATA_MESSAGE async def test_fires_on_camera_sound( @@ -316,7 +310,7 @@ async def test_fires_on_camera_sound( device_registry: dr.DeviceRegistry, create_device: CreateDevice, setup_platform: PlatformSetup, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test camera_sound triggers firing.""" create_device.create( @@ -341,8 +335,8 @@ async def test_fires_on_camera_sound( } hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data == DATA_MESSAGE + assert len(service_calls) == 1 + assert service_calls[0].data == DATA_MESSAGE async def test_fires_on_doorbell_chime( @@ -350,7 +344,7 @@ async def test_fires_on_doorbell_chime( device_registry: dr.DeviceRegistry, create_device: CreateDevice, setup_platform: PlatformSetup, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test doorbell_chime triggers firing.""" create_device.create( @@ -375,8 +369,8 @@ async def test_fires_on_doorbell_chime( } hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data == DATA_MESSAGE + assert len(service_calls) == 1 + assert service_calls[0].data == DATA_MESSAGE async def test_trigger_for_wrong_device_id( @@ -384,7 +378,7 @@ async def test_trigger_for_wrong_device_id( device_registry: dr.DeviceRegistry, create_device: CreateDevice, setup_platform: PlatformSetup, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test messages for the wrong device are ignored.""" create_device.create( @@ -409,7 +403,7 @@ async def test_trigger_for_wrong_device_id( } hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_trigger_for_wrong_event_type( @@ -417,7 +411,7 @@ async def test_trigger_for_wrong_event_type( device_registry: dr.DeviceRegistry, create_device: CreateDevice, setup_platform: PlatformSetup, - calls: list[ServiceCall], + service_calls: list[ServiceCall], ) -> None: """Test that messages for the wrong event type are ignored.""" create_device.create( @@ -442,13 +436,13 @@ async def test_trigger_for_wrong_event_type( } hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_subscriber_automation( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - calls: list[ServiceCall], + service_calls: list[ServiceCall], create_device: CreateDevice, setup_platform: PlatformSetup, subscriber: FakeSubscriber, @@ -488,5 +482,5 @@ async def test_subscriber_automation( await subscriber.async_receive_event(event) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data == DATA_MESSAGE + assert len(service_calls) == 1 + assert service_calls[0].data == DATA_MESSAGE From 592ef59c5a6fccf8b6672fcdd5da8ce2091dd15d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:38:20 +0200 Subject: [PATCH 0618/2411] Use common fixtures in tasmota tests (#121000) --- tests/components/tasmota/conftest.py | 26 +-- .../components/tasmota/test_device_trigger.py | 200 ++++++++++-------- tests/components/tasmota/test_discovery.py | 126 +++++------ tests/components/tasmota/test_init.py | 29 ++- 4 files changed, 173 insertions(+), 208 deletions(-) diff --git a/tests/components/tasmota/conftest.py b/tests/components/tasmota/conftest.py index 07ca8b31825..48cd4012f07 100644 --- a/tests/components/tasmota/conftest.py +++ b/tests/components/tasmota/conftest.py @@ -10,35 +10,11 @@ from homeassistant.components.tasmota.const import ( DEFAULT_PREFIX, DOMAIN, ) -from homeassistant.core import HomeAssistant, ServiceCall -from tests.common import ( - MockConfigEntry, - async_mock_service, - mock_device_registry, - mock_registry, -) +from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa: F401 -@pytest.fixture -def device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - -@pytest.fixture -def entity_reg(hass): - """Return an empty, loaded, registry.""" - return mock_registry(hass) - - -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.fixture(autouse=True) def disable_debounce(): """Set MQTT debounce timer to zero.""" diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index 450ad678ff6..bb474358006 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -30,8 +30,7 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: async def test_get_triggers_btn( hass: HomeAssistant, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, mqtt_mock: MqttMockHAClient, setup_tasmota, ) -> None: @@ -46,7 +45,7 @@ async def test_get_triggers_btn( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) expected_triggers = [ @@ -77,8 +76,7 @@ async def test_get_triggers_btn( async def test_get_triggers_swc( hass: HomeAssistant, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, mqtt_mock: MqttMockHAClient, setup_tasmota, ) -> None: @@ -90,7 +88,7 @@ async def test_get_triggers_swc( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) expected_triggers = [ @@ -112,8 +110,7 @@ async def test_get_triggers_swc( async def test_get_unknown_triggers( hass: HomeAssistant, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, mqtt_mock: MqttMockHAClient, setup_tasmota, ) -> None: @@ -126,7 +123,7 @@ async def test_get_unknown_triggers( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) @@ -161,8 +158,7 @@ async def test_get_unknown_triggers( async def test_get_non_existing_triggers( hass: HomeAssistant, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, mqtt_mock: MqttMockHAClient, setup_tasmota, ) -> None: @@ -175,7 +171,7 @@ async def test_get_non_existing_triggers( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) triggers = await async_get_device_automations( @@ -187,8 +183,7 @@ async def test_get_non_existing_triggers( @pytest.mark.no_fail_on_log_exception async def test_discover_bad_triggers( hass: HomeAssistant, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, mqtt_mock: MqttMockHAClient, setup_tasmota, ) -> None: @@ -207,7 +202,7 @@ async def test_discover_bad_triggers( ) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) triggers = await async_get_device_automations( @@ -243,7 +238,7 @@ async def test_discover_bad_triggers( ) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) triggers = await async_get_device_automations( @@ -274,8 +269,7 @@ async def test_discover_bad_triggers( async def test_update_remove_triggers( hass: HomeAssistant, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, mqtt_mock: MqttMockHAClient, setup_tasmota, ) -> None: @@ -296,7 +290,7 @@ async def test_update_remove_triggers( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) @@ -351,8 +345,8 @@ async def test_update_remove_triggers( async def test_if_fires_on_mqtt_message_btn( hass: HomeAssistant, - device_reg, - calls: list[ServiceCall], + device_registry: dr.DeviceRegistry, + service_calls: list[ServiceCall], mqtt_mock: MqttMockHAClient, setup_tasmota, ) -> None: @@ -366,7 +360,7 @@ async def test_if_fires_on_mqtt_message_btn( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) @@ -412,22 +406,22 @@ async def test_if_fires_on_mqtt_message_btn( hass, "tasmota_49A3BC/stat/RESULT", '{"Button1":{"Action":"SINGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "short_press_1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "short_press_1" # Fake button 3 single press. async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Button3":{"Action":"SINGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "short_press_3" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "short_press_3" async def test_if_fires_on_mqtt_message_swc( hass: HomeAssistant, - device_reg, - calls: list[ServiceCall], + device_registry: dr.DeviceRegistry, + service_calls: list[ServiceCall], mqtt_mock: MqttMockHAClient, setup_tasmota, ) -> None: @@ -442,7 +436,7 @@ async def test_if_fires_on_mqtt_message_swc( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) @@ -502,30 +496,30 @@ async def test_if_fires_on_mqtt_message_swc( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "short_press_1" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "short_press_1" # Fake switch 2 short press. async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch2":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "short_press_2" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "short_press_2" # Fake switch 3 long press. async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"custom_switch":{"Action":"HOLD"}}' ) await hass.async_block_till_done() - assert len(calls) == 3 - assert calls[2].data["some"] == "long_press_3" + assert len(service_calls) == 3 + assert service_calls[2].data["some"] == "long_press_3" async def test_if_fires_on_mqtt_message_late_discover( hass: HomeAssistant, - device_reg, - calls: list[ServiceCall], + device_registry: dr.DeviceRegistry, + service_calls: list[ServiceCall], mqtt_mock: MqttMockHAClient, setup_tasmota, ) -> None: @@ -544,7 +538,7 @@ async def test_if_fires_on_mqtt_message_late_discover( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) @@ -593,22 +587,22 @@ async def test_if_fires_on_mqtt_message_late_discover( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "short_press" + assert len(service_calls) == 1 + assert service_calls[0].data["some"] == "short_press" # Fake long press. async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"custom_switch":{"Action":"HOLD"}}' ) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "double_press" + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == "double_press" async def test_if_fires_on_mqtt_message_after_update( hass: HomeAssistant, - device_reg, - calls: list[ServiceCall], + device_registry: dr.DeviceRegistry, + service_calls: list[ServiceCall], mqtt_mock: MqttMockHAClient, setup_tasmota, ) -> None: @@ -624,7 +618,7 @@ async def test_if_fires_on_mqtt_message_after_update( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) @@ -656,7 +650,7 @@ async def test_if_fires_on_mqtt_message_after_update( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Update the trigger with different topic async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config2)) @@ -666,13 +660,13 @@ async def test_if_fires_on_mqtt_message_after_update( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async_fire_mqtt_message( hass, "tasmota_49A3BC/status/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 # Update the trigger with same topic async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config2)) @@ -682,17 +676,20 @@ async def test_if_fires_on_mqtt_message_after_update( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 async_fire_mqtt_message( hass, "tasmota_49A3BC/status/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 3 + assert len(service_calls) == 3 async def test_no_resubscribe_same_topic( - hass: HomeAssistant, device_reg, mqtt_mock: MqttMockHAClient, setup_tasmota + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mqtt_mock: MqttMockHAClient, + setup_tasmota, ) -> None: """Test subscription to topics without change.""" # Discover a device with device trigger @@ -705,7 +702,7 @@ async def test_no_resubscribe_same_topic( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) @@ -741,8 +738,8 @@ async def test_no_resubscribe_same_topic( async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( hass: HomeAssistant, - device_reg, - calls: list[ServiceCall], + device_registry: dr.DeviceRegistry, + service_calls: list[ServiceCall], mqtt_mock: MqttMockHAClient, setup_tasmota, ) -> None: @@ -757,7 +754,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) @@ -789,7 +786,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Remove the trigger config["swc"][0] = -1 @@ -800,7 +797,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Rediscover the trigger config["swc"][0] = 0 @@ -811,14 +808,14 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 async def test_not_fires_on_mqtt_message_after_remove_from_registry( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - device_reg, - calls: list[ServiceCall], + device_registry: dr.DeviceRegistry, + service_calls: list[ServiceCall], mqtt_mock: MqttMockHAClient, setup_tasmota, ) -> None: @@ -834,7 +831,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) @@ -866,7 +863,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 # Remove the device await remove_device(hass, hass_ws_client, device_entry.id) @@ -876,11 +873,14 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_attach_remove( - hass: HomeAssistant, device_reg, mqtt_mock: MqttMockHAClient, setup_tasmota + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mqtt_mock: MqttMockHAClient, + setup_tasmota, ) -> None: """Test attach and removal of trigger.""" # Discover a device with device trigger @@ -893,14 +893,14 @@ async def test_attach_remove( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) - calls = [] + service_calls = [] def callback(trigger, context): - calls.append(trigger["trigger"]["description"]) + service_calls.append(trigger["trigger"]["description"]) remove = await async_initialize_triggers( hass, @@ -925,8 +925,8 @@ async def test_attach_remove( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0] == "event 'tasmota_event'" + assert len(service_calls) == 1 + assert service_calls[0] == "event 'tasmota_event'" # Remove the trigger remove() @@ -937,11 +937,14 @@ async def test_attach_remove( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_attach_remove_late( - hass: HomeAssistant, device_reg, mqtt_mock: MqttMockHAClient, setup_tasmota + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mqtt_mock: MqttMockHAClient, + setup_tasmota, ) -> None: """Test attach and removal of trigger.""" # Discover a device without device triggers @@ -956,14 +959,14 @@ async def test_attach_remove_late( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) - calls = [] + service_calls = [] def callback(trigger, context): - calls.append(trigger["trigger"]["description"]) + service_calls.append(trigger["trigger"]["description"]) remove = await async_initialize_triggers( hass, @@ -988,7 +991,7 @@ async def test_attach_remove_late( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config2)) await hass.async_block_till_done() @@ -998,8 +1001,8 @@ async def test_attach_remove_late( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0] == "event 'tasmota_event'" + assert len(service_calls) == 1 + assert service_calls[0] == "event 'tasmota_event'" # Remove the trigger remove() @@ -1010,11 +1013,14 @@ async def test_attach_remove_late( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 async def test_attach_remove_late2( - hass: HomeAssistant, device_reg, mqtt_mock: MqttMockHAClient, setup_tasmota + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mqtt_mock: MqttMockHAClient, + setup_tasmota, ) -> None: """Test attach and removal of trigger.""" # Discover a device without device triggers @@ -1029,14 +1035,14 @@ async def test_attach_remove_late2( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) - calls = [] + service_calls = [] def callback(trigger, context): - calls.append(trigger["trigger"]["description"]) + service_calls.append(trigger["trigger"]["description"]) remove = await async_initialize_triggers( hass, @@ -1068,11 +1074,14 @@ async def test_attach_remove_late2( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 0 + assert len(service_calls) == 0 async def test_attach_remove_unknown1( - hass: HomeAssistant, device_reg, mqtt_mock: MqttMockHAClient, setup_tasmota + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mqtt_mock: MqttMockHAClient, + setup_tasmota, ) -> None: """Test attach and removal of unknown trigger.""" # Discover a device without device triggers @@ -1083,7 +1092,7 @@ async def test_attach_remove_unknown1( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) @@ -1113,7 +1122,7 @@ async def test_attach_remove_unknown1( async def test_attach_unknown_remove_device_from_registry( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - device_reg, + device_registry: dr.DeviceRegistry, mqtt_mock: MqttMockHAClient, setup_tasmota, ) -> None: @@ -1136,7 +1145,7 @@ async def test_attach_unknown_remove_device_from_registry( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) @@ -1164,7 +1173,10 @@ async def test_attach_unknown_remove_device_from_registry( async def test_attach_remove_config_entry( - hass: HomeAssistant, device_reg, mqtt_mock: MqttMockHAClient, setup_tasmota + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mqtt_mock: MqttMockHAClient, + setup_tasmota, ) -> None: """Test trigger cleanup when removing a Tasmota config entry.""" # Discover a device with device trigger @@ -1177,14 +1189,14 @@ async def test_attach_remove_config_entry( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) - calls = [] + service_calls = [] def callback(trigger, context): - calls.append(trigger["trigger"]["description"]) + service_calls.append(trigger["trigger"]["description"]) await async_initialize_triggers( hass, @@ -1209,8 +1221,8 @@ async def test_attach_remove_config_entry( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0] == "event 'tasmota_event'" + assert len(service_calls) == 1 + assert service_calls[0] == "event 'tasmota_event'" # Remove the Tasmota config entry config_entries = hass.config_entries.async_entries("tasmota") @@ -1222,4 +1234,4 @@ async def test_attach_remove_config_entry( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 91832f1f2f0..35ea79f7749 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -124,9 +124,8 @@ async def test_invalid_mac( async def test_correct_config_discovery( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, - caplog: pytest.LogCaptureFixture, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, setup_tasmota, ) -> None: """Test receiving valid discovery message.""" @@ -142,11 +141,11 @@ async def test_correct_config_discovery( await hass.async_block_till_done() # Verify device and registry entries are created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - entity_entry = entity_reg.async_get("switch.tasmota_test") + entity_entry = entity_registry.async_get("switch.tasmota_test") assert entity_entry is not None state = hass.states.get("switch.tasmota_test") @@ -159,9 +158,7 @@ async def test_correct_config_discovery( async def test_device_discover( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, - caplog: pytest.LogCaptureFixture, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, setup_tasmota, ) -> None: """Test setting up a device.""" @@ -176,7 +173,7 @@ async def test_device_discover( await hass.async_block_till_done() # Verify device and registry entries are created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -190,9 +187,7 @@ async def test_device_discover( async def test_device_discover_deprecated( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, - caplog: pytest.LogCaptureFixture, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, setup_tasmota, ) -> None: """Test setting up a device with deprecated discovery message.""" @@ -207,7 +202,7 @@ async def test_device_discover_deprecated( await hass.async_block_till_done() # Verify device and registry entries are created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -220,9 +215,7 @@ async def test_device_discover_deprecated( async def test_device_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, - caplog: pytest.LogCaptureFixture, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, setup_tasmota, ) -> None: """Test updating a device.""" @@ -240,7 +233,7 @@ async def test_device_update( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -258,7 +251,7 @@ async def test_device_update( await hass.async_block_till_done() # Verify device entry is updated - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -270,9 +263,7 @@ async def test_device_update( async def test_device_remove( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, - caplog: pytest.LogCaptureFixture, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, setup_tasmota, ) -> None: """Test removing a discovered device.""" @@ -287,7 +278,7 @@ async def test_device_remove( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -300,7 +291,7 @@ async def test_device_remove( await hass.async_block_till_done() # Verify device entry is removed - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None @@ -309,9 +300,7 @@ async def test_device_remove( async def test_device_remove_multiple_config_entries_1( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, - caplog: pytest.LogCaptureFixture, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, setup_tasmota, ) -> None: """Test removing a discovered device.""" @@ -321,7 +310,7 @@ async def test_device_remove_multiple_config_entries_1( mock_entry = MockConfigEntry(domain="test") mock_entry.add_to_hass(hass) - device_reg.async_get_or_create( + device_registry.async_get_or_create( config_entry_id=mock_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, mac)}, ) @@ -336,7 +325,7 @@ async def test_device_remove_multiple_config_entries_1( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -350,7 +339,7 @@ async def test_device_remove_multiple_config_entries_1( await hass.async_block_till_done() # Verify device entry is not removed - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -360,9 +349,7 @@ async def test_device_remove_multiple_config_entries_1( async def test_device_remove_multiple_config_entries_2( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, - caplog: pytest.LogCaptureFixture, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, setup_tasmota, ) -> None: """Test removing a discovered device.""" @@ -372,12 +359,12 @@ async def test_device_remove_multiple_config_entries_2( mock_entry = MockConfigEntry(domain="test") mock_entry.add_to_hass(hass) - device_reg.async_get_or_create( + device_registry.async_get_or_create( config_entry_id=mock_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, mac)}, ) - other_device_entry = device_reg.async_get_or_create( + other_device_entry = device_registry.async_get_or_create( config_entry_id=mock_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "other_device")}, ) @@ -392,7 +379,7 @@ async def test_device_remove_multiple_config_entries_2( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -400,13 +387,13 @@ async def test_device_remove_multiple_config_entries_2( assert other_device_entry.id != device_entry.id # Remove other config entry from the device - device_reg.async_update_device( + device_registry.async_update_device( device_entry.id, remove_config_entry_id=mock_entry.entry_id ) await hass.async_block_till_done() # Verify device entry is not removed - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -414,7 +401,7 @@ async def test_device_remove_multiple_config_entries_2( mqtt_mock.async_publish.assert_not_called() # Remove other config entry from the other device - Tasmota should not do any cleanup - device_reg.async_update_device( + device_registry.async_update_device( other_device_entry.id, remove_config_entry_id=mock_entry.entry_id ) await hass.async_block_till_done() @@ -425,8 +412,7 @@ async def test_device_remove_stale( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mqtt_mock: MqttMockHAClient, - caplog: pytest.LogCaptureFixture, - device_reg, + device_registry: dr.DeviceRegistry, setup_tasmota, ) -> None: """Test removing a stale (undiscovered) device does not throw.""" @@ -436,13 +422,13 @@ async def test_device_remove_stale( config_entry = hass.config_entries.async_entries("tasmota")[0] # Create a device - device_reg.async_get_or_create( + device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, mac)}, ) # Verify device entry was created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -451,7 +437,7 @@ async def test_device_remove_stale( await remove_device(hass, hass_ws_client, device_entry.id) # Verify device entry is removed - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None @@ -460,9 +446,7 @@ async def test_device_remove_stale( async def test_device_rediscover( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, - caplog: pytest.LogCaptureFixture, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, setup_tasmota, ) -> None: """Test removing a device.""" @@ -477,7 +461,7 @@ async def test_device_rediscover( await hass.async_block_till_done() # Verify device entry is created - device_entry1 = device_reg.async_get_device( + device_entry1 = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry1 is not None @@ -490,7 +474,7 @@ async def test_device_rediscover( await hass.async_block_till_done() # Verify device entry is removed - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None @@ -503,7 +487,7 @@ async def test_device_rediscover( await hass.async_block_till_done() # Verify device entry is created, and id is reused - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -576,9 +560,8 @@ async def test_entity_duplicate_removal( async def test_same_topic( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, - caplog: pytest.LogCaptureFixture, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, setup_tasmota, issue_registry: ir.IssueRegistry, ) -> None: @@ -605,7 +588,7 @@ async def test_same_topic( # Verify device registry entries are created for both devices for config in configs[0:2]: - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, config["mac"])} ) assert device_entry is not None @@ -616,14 +599,14 @@ async def test_same_topic( assert device_entry.sw_version == config["sw"] # Verify entities are created only for the first device - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, configs[0]["mac"])} ) - assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 1 - device_entry = device_reg.async_get_device( + assert len(er.async_entries_for_device(entity_registry, device_entry.id, True)) == 1 + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, configs[1]["mac"])} ) - assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 0 + assert len(er.async_entries_for_device(entity_registry, device_entry.id, True)) == 0 # Verify a repairs issue was created issue_id = "topic_duplicated_tasmota_49A3BC/cmnd/" @@ -639,7 +622,7 @@ async def test_same_topic( await hass.async_block_till_done() # Verify device registry entries was created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, configs[2]["mac"])} ) assert device_entry is not None @@ -650,10 +633,10 @@ async def test_same_topic( assert device_entry.sw_version == configs[2]["sw"] # Verify no entities were created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, configs[2]["mac"])} ) - assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 0 + assert len(er.async_entries_for_device(entity_registry, device_entry.id, True)) == 0 # Verify the repairs issue has been updated issue = issue_registry.async_get_issue("tasmota", issue_id) @@ -669,10 +652,10 @@ async def test_same_topic( await hass.async_block_till_done() # Verify entities are created also for the third device - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, configs[2]["mac"])} ) - assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 1 + assert len(er.async_entries_for_device(entity_registry, device_entry.id, True)) == 1 # Verify the repairs issue has been updated issue = issue_registry.async_get_issue("tasmota", issue_id) @@ -688,10 +671,10 @@ async def test_same_topic( await hass.async_block_till_done() # Verify entities are created also for the second device - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, configs[1]["mac"])} ) - assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 1 + assert len(er.async_entries_for_device(entity_registry, device_entry.id, True)) == 1 # Verify the repairs issue has been removed assert issue_registry.async_get_issue("tasmota", issue_id) is None @@ -700,9 +683,8 @@ async def test_same_topic( async def test_topic_no_prefix( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, - caplog: pytest.LogCaptureFixture, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, setup_tasmota, issue_registry: ir.IssueRegistry, ) -> None: @@ -719,7 +701,7 @@ async def test_topic_no_prefix( await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, config["mac"])} ) assert device_entry is not None @@ -730,10 +712,10 @@ async def test_topic_no_prefix( assert device_entry.sw_version == config["sw"] # Verify entities are not created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, config["mac"])} ) - assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 0 + assert len(er.async_entries_for_device(entity_registry, device_entry.id, True)) == 0 # Verify a repairs issue was created issue_id = "topic_no_prefix_00000049A3BC" @@ -749,10 +731,10 @@ async def test_topic_no_prefix( await hass.async_block_till_done() # Verify entities are created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, config["mac"])} ) - assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 1 + assert len(er.async_entries_for_device(entity_registry, device_entry.id, True)) == 1 # Verify the repairs issue has been removed assert ("tasmota", issue_id) not in issue_registry.issues diff --git a/tests/components/tasmota/test_init.py b/tests/components/tasmota/test_init.py index 0123421d5ae..125dba811e6 100644 --- a/tests/components/tasmota/test_init.py +++ b/tests/components/tasmota/test_init.py @@ -4,8 +4,6 @@ import copy import json from unittest.mock import call -import pytest - from homeassistant.components.tasmota.const import DEFAULT_PREFIX, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -26,9 +24,7 @@ async def test_device_remove( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mqtt_mock: MqttMockHAClient, - caplog: pytest.LogCaptureFixture, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, setup_tasmota, ) -> None: """Test removing a discovered device through device registry.""" @@ -44,7 +40,7 @@ async def test_device_remove( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -53,7 +49,7 @@ async def test_device_remove( await hass.async_block_till_done() # Verify device entry is removed - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None @@ -70,7 +66,7 @@ async def test_device_remove( async def test_device_remove_non_tasmota_device( hass: HomeAssistant, - device_reg, + device_registry: dr.DeviceRegistry, hass_ws_client: WebSocketGenerator, mqtt_mock: MqttMockHAClient, setup_tasmota, @@ -92,7 +88,7 @@ async def test_device_remove_non_tasmota_device( config_entry.add_to_hass(hass) mac = "12:34:56:AB:CD:EF" - device_entry = device_reg.async_get_or_create( + device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, mac)}, ) @@ -102,7 +98,7 @@ async def test_device_remove_non_tasmota_device( await hass.async_block_till_done() # Verify device entry is removed - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None @@ -113,7 +109,7 @@ async def test_device_remove_non_tasmota_device( async def test_device_remove_stale_tasmota_device( hass: HomeAssistant, - device_reg, + device_registry: dr.DeviceRegistry, hass_ws_client: WebSocketGenerator, mqtt_mock: MqttMockHAClient, setup_tasmota, @@ -123,7 +119,7 @@ async def test_device_remove_stale_tasmota_device( config_entry = hass.config_entries.async_entries("tasmota")[0] mac = "12:34:56:AB:CD:EF" - device_entry = device_reg.async_get_or_create( + device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, mac)}, ) @@ -133,7 +129,7 @@ async def test_device_remove_stale_tasmota_device( await hass.async_block_till_done() # Verify device entry is removed - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None @@ -144,8 +140,7 @@ async def test_device_remove_stale_tasmota_device( async def test_tasmota_ws_remove_discovered_device( hass: HomeAssistant, - device_reg, - entity_reg, + device_registry: dr.DeviceRegistry, hass_ws_client: WebSocketGenerator, mqtt_mock: MqttMockHAClient, setup_tasmota, @@ -159,7 +154,7 @@ async def test_tasmota_ws_remove_discovered_device( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -170,7 +165,7 @@ async def test_tasmota_ws_remove_discovered_device( ) # Verify device entry is cleared - device_entry = device_reg.async_get_device( + device_entry = device_registry.async_get_device( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None From ba7e45e157ff85436bb61761069ef8e3104d77c6 Mon Sep 17 00:00:00 2001 From: Myles Eftos Date: Wed, 3 Jul 2024 03:40:30 +1000 Subject: [PATCH 0619/2411] Bump amberelectric to 1.1.1 (#121010) --- homeassistant/components/amberelectric/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/amberelectric/manifest.json b/homeassistant/components/amberelectric/manifest.json index 13a9f257adb..51be42cfa68 100644 --- a/homeassistant/components/amberelectric/manifest.json +++ b/homeassistant/components/amberelectric/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/amberelectric", "iot_class": "cloud_polling", "loggers": ["amberelectric"], - "requirements": ["amberelectric==1.1.0"] + "requirements": ["amberelectric==1.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2d2fafc5b24..f4ed86670a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -434,7 +434,7 @@ airtouch5py==0.2.10 alpha-vantage==2.3.1 # homeassistant.components.amberelectric -amberelectric==1.1.0 +amberelectric==1.1.1 # homeassistant.components.amcrest amcrest==1.9.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bca291c8016..e752924a235 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -404,7 +404,7 @@ airtouch4pyapi==1.0.5 airtouch5py==0.2.10 # homeassistant.components.amberelectric -amberelectric==1.1.0 +amberelectric==1.1.1 # homeassistant.components.androidtv androidtv[async]==0.0.73 From 52627b9aed0a0a2a847909c771dedd5855dcbb3e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 2 Jul 2024 21:02:29 +0200 Subject: [PATCH 0620/2411] Handle mains power for Matter appliances (#121023) --- homeassistant/components/matter/climate.py | 7 +++++++ homeassistant/components/matter/fan.py | 16 +++++++++++++++- tests/components/matter/test_climate.py | 9 +++++++-- tests/components/matter/test_fan.py | 6 ++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index c97124f4305..f0eec7955cc 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -227,6 +227,13 @@ class MatterClimate(MatterEntity, ClimateEntity): self._attr_current_temperature = self._get_temperature_in_degrees( clusters.Thermostat.Attributes.LocalTemperature ) + if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False: + # special case: the appliance has a dedicated Power switch on the OnOff cluster + # if the mains power is off - treat it as if the HVAC mode is off + self._attr_hvac_mode = HVACMode.OFF + self._attr_hvac_action = None + return + # update hvac_mode from SystemMode system_mode_value = int( self.get_matter_attribute_value(clusters.Thermostat.Attributes.SystemMode) diff --git a/homeassistant/components/matter/fan.py b/homeassistant/components/matter/fan.py index 0ce42f14d39..86f03dc7a03 100644 --- a/homeassistant/components/matter/fan.py +++ b/homeassistant/components/matter/fan.py @@ -170,6 +170,14 @@ class MatterFan(MatterEntity, FanEntity): """Update from device.""" if not hasattr(self, "_attr_preset_modes"): self._calculate_features() + + if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False: + # special case: the appliance has a dedicated Power switch on the OnOff cluster + # if the mains power is off - treat it as if the fan mode is off + self._attr_preset_mode = None + self._attr_percentage = 0 + return + if self._attr_supported_features & FanEntityFeature.DIRECTION: direction_value = self.get_matter_attribute_value( clusters.FanControl.Attributes.AirflowDirection @@ -200,7 +208,13 @@ class MatterFan(MatterEntity, FanEntity): wind_setting = self.get_matter_attribute_value( clusters.FanControl.Attributes.WindSetting ) - if ( + fan_mode = self.get_matter_attribute_value( + clusters.FanControl.Attributes.FanMode + ) + if fan_mode == clusters.FanControl.Enums.FanModeEnum.kOff: + self._attr_preset_mode = None + self._attr_percentage = 0 + elif ( self._attr_preset_modes and PRESET_NATURAL_WIND in self._attr_preset_modes and wind_setting & WindBitmap.kNaturalWind diff --git a/tests/components/matter/test_climate.py b/tests/components/matter/test_climate.py index 6a4cf34a640..e0015e8b445 100644 --- a/tests/components/matter/test_climate.py +++ b/tests/components/matter/test_climate.py @@ -315,14 +315,19 @@ async def test_room_airconditioner( state = hass.states.get("climate.room_airconditioner_thermostat") assert state assert state.attributes["current_temperature"] == 20 - assert state.attributes["min_temp"] == 16 - assert state.attributes["max_temp"] == 32 + # room airconditioner has mains power on OnOff cluster with value set to False + assert state.state == HVACMode.OFF # test supported features correctly parsed # WITHOUT temperature_range support mask = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF assert state.attributes["supported_features"] & mask == mask + # set mains power to ON (OnOff cluster) + set_node_attribute(room_airconditioner, 1, 6, 0, True) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.room_airconditioner_thermostat") + # test supported HVAC modes include fan and dry modes assert state.attributes["hvac_modes"] == [ HVACMode.OFF, diff --git a/tests/components/matter/test_fan.py b/tests/components/matter/test_fan.py index 30bd7f4a009..7e964d672ca 100644 --- a/tests/components/matter/test_fan.py +++ b/tests/components/matter/test_fan.py @@ -92,6 +92,12 @@ async def test_fan_base( await trigger_subscription_callback(hass, matter_client) state = hass.states.get(entity_id) assert state.attributes["preset_mode"] == "sleep_wind" + # set mains power to OFF (OnOff cluster) + set_node_attribute(air_purifier, 1, 6, 0, False) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get(entity_id) + assert state.attributes["preset_mode"] is None + assert state.attributes["percentage"] == 0 async def test_fan_turn_on_with_percentage( From 0d0ca22103f7ee1a6e015544f3d5efee096f9fd8 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 2 Jul 2024 21:03:01 +0200 Subject: [PATCH 0621/2411] Fix setting target temperature for single setpoint Matter thermostat (#121011) --- homeassistant/components/matter/climate.py | 28 ++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index f0eec7955cc..192cb6b3bb4 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -274,19 +274,13 @@ class MatterClimate(MatterEntity, ClimateEntity): self._attr_hvac_action = HVACAction.FAN case _: self._attr_hvac_action = HVACAction.OFF - # update target_temperature - if self._attr_hvac_mode == HVACMode.HEAT_COOL: - self._attr_target_temperature = None - elif self._attr_hvac_mode == HVACMode.COOL: - self._attr_target_temperature = self._get_temperature_in_degrees( - clusters.Thermostat.Attributes.OccupiedCoolingSetpoint - ) - else: - self._attr_target_temperature = self._get_temperature_in_degrees( - clusters.Thermostat.Attributes.OccupiedHeatingSetpoint - ) # update target temperature high/low - if self._attr_hvac_mode == HVACMode.HEAT_COOL: + supports_range = ( + self._attr_supported_features + & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) + if supports_range and self._attr_hvac_mode == HVACMode.HEAT_COOL: + self._attr_target_temperature = None self._attr_target_temperature_high = self._get_temperature_in_degrees( clusters.Thermostat.Attributes.OccupiedCoolingSetpoint ) @@ -296,6 +290,16 @@ class MatterClimate(MatterEntity, ClimateEntity): else: self._attr_target_temperature_high = None self._attr_target_temperature_low = None + # update target_temperature + if self._attr_hvac_mode == HVACMode.COOL: + self._attr_target_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedCoolingSetpoint + ) + else: + self._attr_target_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedHeatingSetpoint + ) + # update min_temp if self._attr_hvac_mode == HVACMode.COOL: attribute = clusters.Thermostat.Attributes.AbsMinCoolSetpointLimit From 1e6dc74812000d4fd98de4031b3157afa71517b5 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 2 Jul 2024 08:23:07 +0200 Subject: [PATCH 0622/2411] Minor polishing for tplink (#120868) --- homeassistant/components/tplink/climate.py | 11 ++++--- homeassistant/components/tplink/entity.py | 24 +++++++------- homeassistant/components/tplink/fan.py | 3 +- homeassistant/components/tplink/light.py | 32 +++++++++---------- homeassistant/components/tplink/sensor.py | 18 +---------- homeassistant/components/tplink/switch.py | 19 +---------- .../components/tplink/fixtures/features.json | 2 +- .../tplink/snapshots/test_sensor.ambr | 4 +-- tests/components/tplink/test_light.py | 16 ++++++---- 9 files changed, 51 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/tplink/climate.py b/homeassistant/components/tplink/climate.py index 99a8c43fac3..3bd6aba5c26 100644 --- a/homeassistant/components/tplink/climate.py +++ b/homeassistant/components/tplink/climate.py @@ -77,16 +77,17 @@ class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity): parent: Device, ) -> None: """Initialize the climate entity.""" - super().__init__(device, coordinator, parent=parent) - self._state_feature = self._device.features["state"] - self._mode_feature = self._device.features["thermostat_mode"] - self._temp_feature = self._device.features["temperature"] - self._target_feature = self._device.features["target_temperature"] + self._state_feature = device.features["state"] + self._mode_feature = device.features["thermostat_mode"] + self._temp_feature = device.features["temperature"] + self._target_feature = device.features["target_temperature"] self._attr_min_temp = self._target_feature.minimum_value self._attr_max_temp = self._target_feature.maximum_value self._attr_temperature_unit = UNIT_MAPPING[cast(str, self._temp_feature.unit)] + super().__init__(device, coordinator, parent=parent) + @async_refresh_after async def async_set_temperature(self, **kwargs: Any) -> None: """Set target temperature.""" diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 4e8ec0e0779..4ec0480cf82 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -56,15 +56,21 @@ DEVICETYPES_WITH_SPECIALIZED_PLATFORMS = { DeviceType.Thermostat, } +# Primary features to always include even when the device type has its own platform +FEATURES_ALLOW_LIST = { + # lights have current_consumption and a specialized platform + "current_consumption" +} + + # Features excluded due to future platform additions EXCLUDED_FEATURES = { # update "current_firmware_version", "available_firmware_version", - # fan - "fan_speed_level", } + LEGACY_KEY_MAPPING = { "current": ATTR_CURRENT_A, "current_consumption": ATTR_CURRENT_POWER_W, @@ -179,15 +185,12 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB self._attr_unique_id = self._get_unique_id() + self._async_call_update_attrs() + def _get_unique_id(self) -> str: """Return unique ID for the entity.""" return legacy_device_id(self._device) - async def async_added_to_hass(self) -> None: - """Handle being added to hass.""" - self._async_call_update_attrs() - return await super().async_added_to_hass() - @abstractmethod @callback def _async_update_attrs(self) -> None: @@ -196,11 +199,7 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB @callback def _async_call_update_attrs(self) -> None: - """Call update_attrs and make entity unavailable on error. - - update_attrs can sometimes fail if a device firmware update breaks the - downstream library. - """ + """Call update_attrs and make entity unavailable on errors.""" try: self._async_update_attrs() except Exception as ex: # noqa: BLE001 @@ -358,6 +357,7 @@ class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC): and ( feat.category is not Feature.Category.Primary or device.device_type not in DEVICETYPES_WITH_SPECIALIZED_PLATFORMS + or feat.id in FEATURES_ALLOW_LIST ) and ( desc := cls._description_for_feature( diff --git a/homeassistant/components/tplink/fan.py b/homeassistant/components/tplink/fan.py index 947a9072329..292240bca94 100644 --- a/homeassistant/components/tplink/fan.py +++ b/homeassistant/components/tplink/fan.py @@ -69,11 +69,12 @@ class TPLinkFanEntity(CoordinatedTPLinkEntity, FanEntity): parent: Device | None = None, ) -> None: """Initialize the fan.""" - super().__init__(device, coordinator, parent=parent) self.fan_module = fan_module # If _attr_name is None the entity name will be the device name self._attr_name = None if parent is None else device.alias + super().__init__(device, coordinator, parent=parent) + @async_refresh_after async def async_turn_on( self, diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 633648bbf23..a736a0ba1e1 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -140,9 +140,7 @@ async def async_setup_entry( parent_coordinator = data.parent_coordinator device = parent_coordinator.device entities: list[TPLinkLightEntity | TPLinkLightEffectEntity] = [] - if ( - effect_module := device.modules.get(Module.LightEffect) - ) and effect_module.has_custom_effects: + if effect_module := device.modules.get(Module.LightEffect): entities.append( TPLinkLightEffectEntity( device, @@ -151,17 +149,18 @@ async def async_setup_entry( effect_module=effect_module, ) ) - platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service( - SERVICE_RANDOM_EFFECT, - RANDOM_EFFECT_DICT, - "async_set_random_effect", - ) - platform.async_register_entity_service( - SERVICE_SEQUENCE_EFFECT, - SEQUENCE_EFFECT_DICT, - "async_set_sequence_effect", - ) + if effect_module.has_custom_effects: + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_RANDOM_EFFECT, + RANDOM_EFFECT_DICT, + "async_set_random_effect", + ) + platform.async_register_entity_service( + SERVICE_SEQUENCE_EFFECT, + SEQUENCE_EFFECT_DICT, + "async_set_sequence_effect", + ) elif Module.Light in device.modules: entities.append( TPLinkLightEntity( @@ -197,7 +196,6 @@ class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity): ) -> None: """Initialize the light.""" self._parent = parent - super().__init__(device, coordinator, parent=parent) self._light_module = light_module # If _attr_name is None the entity name will be the device name self._attr_name = None if parent is None else device.alias @@ -215,7 +213,8 @@ class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity): if len(self._attr_supported_color_modes) == 1: # If the light supports only a single color mode, set it now self._fixed_color_mode = next(iter(self._attr_supported_color_modes)) - self._async_call_update_attrs() + + super().__init__(device, coordinator, parent=parent) def _get_unique_id(self) -> str: """Return unique ID for the entity.""" @@ -371,6 +370,7 @@ class TPLinkLightEffectEntity(TPLinkLightEntity): effect_module = self._effect_module if effect_module.effect != LightEffect.LIGHT_EFFECTS_OFF: self._attr_effect = effect_module.effect + self._attr_color_mode = ColorMode.BRIGHTNESS else: self._attr_effect = EFFECT_OFF if effect_list := effect_module.effect_list: diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 474ee6bfacf..3da414d74d3 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import cast -from kasa import Device, Feature +from kasa import Feature from homeassistant.components.sensor import ( SensorDeviceClass, @@ -18,7 +18,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TPLinkConfigEntry from .const import UNIT_MAPPING -from .coordinator import TPLinkDataUpdateCoordinator from .entity import CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription @@ -144,21 +143,6 @@ class TPLinkSensorEntity(CoordinatedTPLinkFeatureEntity, SensorEntity): entity_description: TPLinkSensorEntityDescription - def __init__( - self, - device: Device, - coordinator: TPLinkDataUpdateCoordinator, - *, - feature: Feature, - description: TPLinkSensorEntityDescription, - parent: Device | None = None, - ) -> None: - """Initialize the sensor.""" - super().__init__( - device, coordinator, description=description, feature=feature, parent=parent - ) - self._async_call_update_attrs() - @callback def _async_update_attrs(self) -> None: """Update the entity's attributes.""" diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 2520de9dd3e..62957d48ac4 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -6,14 +6,13 @@ from dataclasses import dataclass import logging from typing import Any -from kasa import Device, Feature +from kasa import Feature from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TPLinkConfigEntry -from .coordinator import TPLinkDataUpdateCoordinator from .entity import ( CoordinatedTPLinkFeatureEntity, TPLinkFeatureEntityDescription, @@ -80,22 +79,6 @@ class TPLinkSwitch(CoordinatedTPLinkFeatureEntity, SwitchEntity): entity_description: TPLinkSwitchEntityDescription - def __init__( - self, - device: Device, - coordinator: TPLinkDataUpdateCoordinator, - *, - feature: Feature, - description: TPLinkSwitchEntityDescription, - parent: Device | None = None, - ) -> None: - """Initialize the switch.""" - super().__init__( - device, coordinator, description=description, feature=feature, parent=parent - ) - - self._async_call_update_attrs() - @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" diff --git a/tests/components/tplink/fixtures/features.json b/tests/components/tplink/fixtures/features.json index daf86a74643..7cfe979ea25 100644 --- a/tests/components/tplink/fixtures/features.json +++ b/tests/components/tplink/fixtures/features.json @@ -73,7 +73,7 @@ "value": 121.1, "type": "Sensor", "category": "Primary", - "unit": "v", + "unit": "V", "precision_hint": 1 }, "device_id": { diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index 46fe897500f..9ea22af45fd 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -770,7 +770,7 @@ 'supported_features': 0, 'translation_key': 'voltage', 'unique_id': '123456789ABCDEFGH_voltage', - 'unit_of_measurement': 'v', + 'unit_of_measurement': 'V', }) # --- # name: test_states[sensor.my_device_voltage-state] @@ -779,7 +779,7 @@ 'device_class': 'voltage', 'friendly_name': 'my_device Voltage', 'state_class': , - 'unit_of_measurement': 'v', + 'unit_of_measurement': 'V', }), 'context': , 'entity_id': 'sensor.my_device_voltage', diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index c2f40f47e3d..6fce04ec454 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -140,13 +140,17 @@ async def test_color_light( assert state.state == "on" attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 - assert attributes[ATTR_COLOR_MODE] == "hs" assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"] - assert attributes[ATTR_MIN_MIREDS] == 111 - assert attributes[ATTR_MAX_MIREDS] == 250 - assert attributes[ATTR_HS_COLOR] == (10, 30) - assert attributes[ATTR_RGB_COLOR] == (255, 191, 178) - assert attributes[ATTR_XY_COLOR] == (0.42, 0.336) + # If effect is active, only the brightness can be controlled + if attributes.get(ATTR_EFFECT) is not None: + assert attributes[ATTR_COLOR_MODE] == "brightness" + else: + assert attributes[ATTR_COLOR_MODE] == "hs" + assert attributes[ATTR_MIN_MIREDS] == 111 + assert attributes[ATTR_MAX_MIREDS] == 250 + assert attributes[ATTR_HS_COLOR] == (10, 30) + assert attributes[ATTR_RGB_COLOR] == (255, 191, 178) + assert attributes[ATTR_XY_COLOR] == (0.42, 0.336) await hass.services.async_call( LIGHT_DOMAIN, "turn_off", BASE_PAYLOAD, blocking=True From 3b6acd538042a3b2d4b7c164ae0f992c944c2fb5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 2 Jul 2024 23:51:14 +1200 Subject: [PATCH 0623/2411] [ESPHome] Disable dashboard based update entities by default (#120907) Co-authored-by: Paulus Schoutsen --- homeassistant/components/esphome/update.py | 1 + tests/components/esphome/test_update.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index cb3d36dab9d..e86c88ddf5b 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -97,6 +97,7 @@ class ESPHomeDashboardUpdateEntity( _attr_title = "ESPHome" _attr_name = "Firmware" _attr_release_url = "https://esphome.io/changelog/" + _attr_entity_registry_enabled_default = False def __init__( self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboardCoordinator diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index fc845299142..cca1dd1851f 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -33,6 +33,11 @@ from homeassistant.exceptions import HomeAssistantError from .conftest import MockESPHomeDevice +@pytest.fixture(autouse=True) +def enable_entity(entity_registry_enabled_by_default: None) -> None: + """Enable update entity.""" + + @pytest.fixture def stub_reconnect(): """Stub reconnect.""" From efd3252849aa3e4a9d2994bb623f6952ba834c49 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Jul 2024 15:48:35 +0200 Subject: [PATCH 0624/2411] Create log files in an executor thread (#120912) --- homeassistant/bootstrap.py | 59 +++++++++++++++++++++----------------- tests/test_bootstrap.py | 28 +++++++++--------- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 8435fe73d40..c5229634053 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -8,7 +8,7 @@ import contextlib from functools import partial from itertools import chain import logging -import logging.handlers +from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler import mimetypes from operator import contains, itemgetter import os @@ -257,12 +257,12 @@ async def async_setup_hass( ) -> core.HomeAssistant | None: """Set up Home Assistant.""" - def create_hass() -> core.HomeAssistant: + async def create_hass() -> core.HomeAssistant: """Create the hass object and do basic setup.""" hass = core.HomeAssistant(runtime_config.config_dir) loader.async_setup(hass) - async_enable_logging( + await async_enable_logging( hass, runtime_config.verbose, runtime_config.log_rotate_days, @@ -287,7 +287,7 @@ async def async_setup_hass( async with hass.timeout.async_timeout(10): await hass.async_stop() - hass = create_hass() + hass = await create_hass() if runtime_config.skip_pip or runtime_config.skip_pip_packages: _LOGGER.warning( @@ -326,13 +326,13 @@ async def async_setup_hass( if config_dict is None: recovery_mode = True await stop_hass(hass) - hass = create_hass() + hass = await create_hass() elif not basic_setup_success: _LOGGER.warning("Unable to set up core integrations. Activating recovery mode") recovery_mode = True await stop_hass(hass) - hass = create_hass() + hass = await create_hass() elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS): _LOGGER.warning( @@ -345,7 +345,7 @@ async def async_setup_hass( recovery_mode = True await stop_hass(hass) - hass = create_hass() + hass = await create_hass() if old_logging: hass.data[DATA_LOGGING] = old_logging @@ -523,8 +523,7 @@ async def async_from_config_dict( return hass -@core.callback -def async_enable_logging( +async def async_enable_logging( hass: core.HomeAssistant, verbose: bool = False, log_rotate_days: int | None = None, @@ -607,23 +606,9 @@ def async_enable_logging( if (err_path_exists and os.access(err_log_path, os.W_OK)) or ( not err_path_exists and os.access(err_dir, os.W_OK) ): - err_handler: ( - logging.handlers.RotatingFileHandler - | logging.handlers.TimedRotatingFileHandler + err_handler = await hass.async_add_executor_job( + _create_log_file, err_log_path, log_rotate_days ) - if log_rotate_days: - err_handler = logging.handlers.TimedRotatingFileHandler( - err_log_path, when="midnight", backupCount=log_rotate_days - ) - else: - err_handler = _RotatingFileHandlerWithoutShouldRollOver( - err_log_path, backupCount=1 - ) - - try: - err_handler.doRollover() - except OSError as err: - _LOGGER.error("Error rolling over log file: %s", err) err_handler.setLevel(logging.INFO if verbose else logging.WARNING) err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME)) @@ -640,7 +625,29 @@ def async_enable_logging( async_activate_log_queue_handler(hass) -class _RotatingFileHandlerWithoutShouldRollOver(logging.handlers.RotatingFileHandler): +def _create_log_file( + err_log_path: str, log_rotate_days: int | None +) -> RotatingFileHandler | TimedRotatingFileHandler: + """Create log file and do roll over.""" + err_handler: RotatingFileHandler | TimedRotatingFileHandler + if log_rotate_days: + err_handler = TimedRotatingFileHandler( + err_log_path, when="midnight", backupCount=log_rotate_days + ) + else: + err_handler = _RotatingFileHandlerWithoutShouldRollOver( + err_log_path, backupCount=1 + ) + + try: + err_handler.doRollover() + except OSError as err: + _LOGGER.error("Error rolling over log file: %s", err) + + return err_handler + + +class _RotatingFileHandlerWithoutShouldRollOver(RotatingFileHandler): """RotatingFileHandler that does not check if it should roll over on every log.""" def shouldRollover(self, record: logging.LogRecord) -> bool: diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index ca864006852..56599a15d34 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -70,7 +70,7 @@ def mock_http_start_stop() -> Generator[None]: yield -@patch("homeassistant.bootstrap.async_enable_logging", Mock()) +@patch("homeassistant.bootstrap.async_enable_logging", AsyncMock()) async def test_home_assistant_core_config_validation(hass: HomeAssistant) -> None: """Test if we pass in wrong information for HA conf.""" # Extensive HA conf validation testing is done @@ -94,10 +94,10 @@ async def test_async_enable_logging( side_effect=OSError, ), ): - bootstrap.async_enable_logging(hass) + await bootstrap.async_enable_logging(hass) mock_async_activate_log_queue_handler.assert_called_once() mock_async_activate_log_queue_handler.reset_mock() - bootstrap.async_enable_logging( + await bootstrap.async_enable_logging( hass, log_rotate_days=5, log_file="test.log", @@ -141,7 +141,7 @@ async def test_config_does_not_turn_off_debug(hass: HomeAssistant) -> None: @pytest.mark.parametrize("hass_config", [{"frontend": {}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_asyncio_debug_on_turns_hass_debug_on( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -598,7 +598,7 @@ def mock_is_virtual_env() -> Generator[Mock]: @pytest.fixture -def mock_enable_logging() -> Generator[Mock]: +def mock_enable_logging() -> Generator[AsyncMock]: """Mock enable logging.""" with patch("homeassistant.bootstrap.async_enable_logging") as enable_logging: yield enable_logging @@ -634,7 +634,7 @@ def mock_ensure_config_exists() -> Generator[AsyncMock]: @pytest.mark.parametrize("hass_config", [{"browser": {}, "frontend": {}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -687,7 +687,7 @@ async def test_setup_hass( @pytest.mark.parametrize("hass_config", [{"browser": {}, "frontend": {}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_takes_longer_than_log_slow_startup( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -728,7 +728,7 @@ async def test_setup_hass_takes_longer_than_log_slow_startup( async def test_setup_hass_invalid_yaml( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -755,7 +755,7 @@ async def test_setup_hass_invalid_yaml( async def test_setup_hass_config_dir_nonexistent( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -781,7 +781,7 @@ async def test_setup_hass_config_dir_nonexistent( async def test_setup_hass_recovery_mode( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -817,7 +817,7 @@ async def test_setup_hass_recovery_mode( @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_safe_mode( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -852,7 +852,7 @@ async def test_setup_hass_safe_mode( @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_recovery_mode_and_safe_mode( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -888,7 +888,7 @@ async def test_setup_hass_recovery_mode_and_safe_mode( @pytest.mark.parametrize("hass_config", [{"homeassistant": {"non-existing": 1}}]) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_hass_invalid_core_config( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, @@ -927,7 +927,7 @@ async def test_setup_hass_invalid_core_config( ) @pytest.mark.usefixtures("mock_hass_config") async def test_setup_recovery_mode_if_no_frontend( - mock_enable_logging: Mock, + mock_enable_logging: AsyncMock, mock_is_virtual_env: Mock, mock_mount_local_lib_path: AsyncMock, mock_ensure_config_exists: AsyncMock, From de458493f895be7fce2a194eea19db3dbd0c1907 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 1 Jul 2024 20:36:35 +0200 Subject: [PATCH 0625/2411] Fix missing airgradient string (#120957) --- homeassistant/components/airgradient/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 12049e7b720..6bf7242f2f1 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -16,6 +16,7 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1." }, "error": { From 23b905b4226ec00d4432c3ce89cd9ce9b685b607 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Jul 2024 08:23:31 +0200 Subject: [PATCH 0626/2411] Bump airgradient to 0.6.1 (#120962) --- homeassistant/components/airgradient/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index 7b892c4658a..d523aa4ca03 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/airgradient", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["airgradient==0.6.0"], + "requirements": ["airgradient==0.6.1"], "zeroconf": ["_airgradient._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index c18ed2f439a..2683ff24549 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ aiowithings==3.0.2 aioymaps==1.2.2 # homeassistant.components.airgradient -airgradient==0.6.0 +airgradient==0.6.1 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6291a3dddca..72c0b47ad61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -386,7 +386,7 @@ aiowithings==3.0.2 aioymaps==1.2.2 # homeassistant.components.airgradient -airgradient==0.6.0 +airgradient==0.6.1 # homeassistant.components.airly airly==1.1.0 From 65d2ca53cb25209dc207b314818b3aa6074d71bd Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 2 Jul 2024 09:25:33 +0200 Subject: [PATCH 0627/2411] Bump reolink-aio to 0.9.4 (#120964) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 172a43a91b3..ee3ebe8a13a 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.9.3"] + "requirements": ["reolink-aio==0.9.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2683ff24549..93d38bf3b73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2460,7 +2460,7 @@ renault-api==0.2.4 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.3 +reolink-aio==0.9.4 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72c0b47ad61..9a5c062d76e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1924,7 +1924,7 @@ renault-api==0.2.4 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.3 +reolink-aio==0.9.4 # homeassistant.components.rflink rflink==0.0.66 From 24afbde79e3caad272ff46ac1a5cf4b9f656373f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Jul 2024 12:35:10 +0200 Subject: [PATCH 0628/2411] Bump yt-dlp to 2024.07.01 (#120978) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 7ed4e93bb56..cfe44f5176b 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2024.05.27"], + "requirements": ["yt-dlp==2024.07.01"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 93d38bf3b73..7ba781583f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2951,7 +2951,7 @@ youless-api==2.1.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.05.27 +yt-dlp==2024.07.01 # homeassistant.components.zamg zamg==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a5c062d76e..65f9b4b1770 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2307,7 +2307,7 @@ youless-api==2.1.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.05.27 +yt-dlp==2024.07.01 # homeassistant.components.zamg zamg==0.3.6 From 98a2e46d4ac10fb9874108c9a74f0cc2bdf11b50 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Jul 2024 13:51:44 +0200 Subject: [PATCH 0629/2411] Remove Aladdin Connect integration (#120980) --- .coveragerc | 5 - CODEOWNERS | 2 - .../components/aladdin_connect/__init__.py | 108 ++------ .../components/aladdin_connect/api.py | 33 --- .../application_credentials.py | 14 -- .../components/aladdin_connect/config_flow.py | 71 +----- .../components/aladdin_connect/const.py | 6 - .../components/aladdin_connect/coordinator.py | 38 --- .../components/aladdin_connect/cover.py | 84 ------- .../components/aladdin_connect/entity.py | 27 -- .../components/aladdin_connect/manifest.json | 8 +- .../components/aladdin_connect/ruff.toml | 5 - .../components/aladdin_connect/sensor.py | 80 ------ .../components/aladdin_connect/strings.json | 29 +-- .../generated/application_credentials.py | 1 - homeassistant/generated/config_flows.py | 1 - homeassistant/generated/integrations.json | 6 - tests/components/aladdin_connect/conftest.py | 29 --- .../aladdin_connect/test_config_flow.py | 230 ------------------ tests/components/aladdin_connect/test_init.py | 50 ++++ 20 files changed, 89 insertions(+), 738 deletions(-) delete mode 100644 homeassistant/components/aladdin_connect/api.py delete mode 100644 homeassistant/components/aladdin_connect/application_credentials.py delete mode 100644 homeassistant/components/aladdin_connect/const.py delete mode 100644 homeassistant/components/aladdin_connect/coordinator.py delete mode 100644 homeassistant/components/aladdin_connect/cover.py delete mode 100644 homeassistant/components/aladdin_connect/entity.py delete mode 100644 homeassistant/components/aladdin_connect/ruff.toml delete mode 100644 homeassistant/components/aladdin_connect/sensor.py delete mode 100644 tests/components/aladdin_connect/conftest.py delete mode 100644 tests/components/aladdin_connect/test_config_flow.py create mode 100644 tests/components/aladdin_connect/test_init.py diff --git a/.coveragerc b/.coveragerc index 0784977ff55..99a48360b41 100644 --- a/.coveragerc +++ b/.coveragerc @@ -58,11 +58,6 @@ omit = homeassistant/components/airvisual/sensor.py homeassistant/components/airvisual_pro/__init__.py homeassistant/components/airvisual_pro/sensor.py - homeassistant/components/aladdin_connect/__init__.py - homeassistant/components/aladdin_connect/api.py - homeassistant/components/aladdin_connect/application_credentials.py - homeassistant/components/aladdin_connect/cover.py - homeassistant/components/aladdin_connect/sensor.py homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 7834add43f6..765f1624c33 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -80,8 +80,6 @@ build.json @home-assistant/supervisor /tests/components/airzone/ @Noltari /homeassistant/components/airzone_cloud/ @Noltari /tests/components/airzone_cloud/ @Noltari -/homeassistant/components/aladdin_connect/ @swcloudgenie -/tests/components/aladdin_connect/ @swcloudgenie /homeassistant/components/alarm_control_panel/ @home-assistant/core /tests/components/alarm_control_panel/ @home-assistant/core /homeassistant/components/alert/ @home-assistant/core @frenck diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index ed284c0e6bb..6d3f1d642b5 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -1,94 +1,38 @@ """The Aladdin Connect Genie integration.""" -# mypy: ignore-errors from __future__ import annotations -# from genie_partner_sdk.client import AladdinConnectClient -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.config_entry_oauth2_flow import ( - OAuth2Session, - async_get_config_entry_implementation, -) +from homeassistant.helpers import issue_registry as ir -from .api import AsyncConfigEntryAuth -from .const import DOMAIN -from .coordinator import AladdinConnectCoordinator - -PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR] - -type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator] +DOMAIN = "aladdin_connect" -async def async_setup_entry( - hass: HomeAssistant, entry: AladdinConnectConfigEntry -) -> bool: - """Set up Aladdin Connect Genie from a config entry.""" - implementation = await async_get_config_entry_implementation(hass, entry) - - session = OAuth2Session(hass, entry, implementation) - auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session) - coordinator = AladdinConnectCoordinator(hass, AladdinConnectClient(auth)) - - await coordinator.async_setup() - await coordinator.async_config_entry_first_refresh() - - entry.runtime_data = coordinator - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - async_remove_stale_devices(hass, entry) - - return True - - -async def async_unload_entry( - hass: HomeAssistant, entry: AladdinConnectConfigEntry -) -> bool: - """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - -async def async_migrate_entry( - hass: HomeAssistant, config_entry: AladdinConnectConfigEntry -) -> bool: - """Migrate old config.""" - if config_entry.version < 2: - config_entry.async_start_reauth(hass) - hass.config_entries.async_update_entry( - config_entry, - version=2, - minor_version=1, - ) - - return True - - -def async_remove_stale_devices( - hass: HomeAssistant, config_entry: AladdinConnectConfigEntry -) -> None: - """Remove stale devices from device registry.""" - device_registry = dr.async_get(hass) - device_entries = dr.async_entries_for_config_entry( - device_registry, config_entry.entry_id +async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool: + """Set up Aladdin Connect from a config entry.""" + ir.async_create_issue( + hass, + DOMAIN, + DOMAIN, + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="integration_removed", + translation_placeholders={ + "entries": "/config/integrations/integration/aladdin_connect", + }, ) - all_device_ids = {door.unique_id for door in config_entry.runtime_data.doors} - for device_entry in device_entries: - device_id: str | None = None + return True - for identifier in device_entry.identifiers: - if identifier[0] == DOMAIN: - device_id = identifier[1] - break - if device_id is None or device_id not in all_device_ids: - # If device_id is None an invalid device entry was found for this config entry. - # If the device_id is not in existing device ids it's a stale device entry. - # Remove config entry from this device entry in either case. - device_registry.async_update_device( - device_entry.id, remove_config_entry_id=config_entry.entry_id - ) +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if all( + config_entry.state is ConfigEntryState.NOT_LOADED + for config_entry in hass.config_entries.async_entries(DOMAIN) + if config_entry.entry_id != entry.entry_id + ): + ir.async_delete_issue(hass, DOMAIN, DOMAIN) + + return True diff --git a/homeassistant/components/aladdin_connect/api.py b/homeassistant/components/aladdin_connect/api.py deleted file mode 100644 index 4377fc8fbcb..00000000000 --- a/homeassistant/components/aladdin_connect/api.py +++ /dev/null @@ -1,33 +0,0 @@ -"""API for Aladdin Connect Genie bound to Home Assistant OAuth.""" - -# mypy: ignore-errors -from typing import cast - -from aiohttp import ClientSession - -# from genie_partner_sdk.auth import Auth -from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session - -API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1" -API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3" - - -class AsyncConfigEntryAuth(Auth): # type: ignore[misc] - """Provide Aladdin Connect Genie authentication tied to an OAuth2 based config entry.""" - - def __init__( - self, - websession: ClientSession, - oauth_session: OAuth2Session, - ) -> None: - """Initialize Aladdin Connect Genie auth.""" - super().__init__( - websession, API_URL, oauth_session.token["access_token"], API_KEY - ) - self._oauth_session = oauth_session - - async def async_get_access_token(self) -> str: - """Return a valid access token.""" - await self._oauth_session.async_ensure_token_valid() - - return cast(str, self._oauth_session.token["access_token"]) diff --git a/homeassistant/components/aladdin_connect/application_credentials.py b/homeassistant/components/aladdin_connect/application_credentials.py deleted file mode 100644 index e8e959f1fa3..00000000000 --- a/homeassistant/components/aladdin_connect/application_credentials.py +++ /dev/null @@ -1,14 +0,0 @@ -"""application_credentials platform the Aladdin Connect Genie integration.""" - -from homeassistant.components.application_credentials import AuthorizationServer -from homeassistant.core import HomeAssistant - -from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN - - -async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: - """Return authorization server.""" - return AuthorizationServer( - authorize_url=OAUTH2_AUTHORIZE, - token_url=OAUTH2_TOKEN, - ) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 507085fa27f..a508ff89c68 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -1,70 +1,11 @@ -"""Config flow for Aladdin Connect Genie.""" +"""Config flow for Aladdin Connect integration.""" -from collections.abc import Mapping -import logging -from typing import Any +from homeassistant.config_entries import ConfigFlow -import jwt - -from homeassistant.config_entries import ConfigEntry, ConfigFlowResult -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN -from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler - -from .const import DOMAIN +from . import DOMAIN -class AladdinConnectOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): - """Config flow to handle Aladdin Connect Genie OAuth2 authentication.""" +class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Aladdin Connect.""" - DOMAIN = DOMAIN - VERSION = 2 - MINOR_VERSION = 1 - - reauth_entry: ConfigEntry | None = None - - async def async_step_reauth( - self, user_input: Mapping[str, Any] - ) -> ConfigFlowResult: - """Perform reauth upon API auth error or upgrade from v1 to v2.""" - self.reauth_entry = self.hass.config_entries.async_get_entry( - self.context["entry_id"] - ) - return await self.async_step_reauth_confirm() - - async def async_step_reauth_confirm( - self, user_input: Mapping[str, Any] | None = None - ) -> ConfigFlowResult: - """Dialog that informs the user that reauth is required.""" - if user_input is None: - return self.async_show_form(step_id="reauth_confirm") - return await self.async_step_user() - - async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: - """Create an oauth config entry or update existing entry for reauth.""" - token_payload = jwt.decode( - data[CONF_TOKEN][CONF_ACCESS_TOKEN], options={"verify_signature": False} - ) - if not self.reauth_entry: - await self.async_set_unique_id(token_payload["sub"]) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=token_payload["username"], - data=data, - ) - - if self.reauth_entry.unique_id == token_payload["username"]: - return self.async_update_reload_and_abort( - self.reauth_entry, - data=data, - unique_id=token_payload["sub"], - ) - if self.reauth_entry.unique_id == token_payload["sub"]: - return self.async_update_reload_and_abort(self.reauth_entry, data=data) - - return self.async_abort(reason="wrong_account") - - @property - def logger(self) -> logging.Logger: - """Return logger.""" - return logging.getLogger(__name__) + VERSION = 1 diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py deleted file mode 100644 index a87147c8f09..00000000000 --- a/homeassistant/components/aladdin_connect/const.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Constants for the Aladdin Connect Genie integration.""" - -DOMAIN = "aladdin_connect" - -OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html" -OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token" diff --git a/homeassistant/components/aladdin_connect/coordinator.py b/homeassistant/components/aladdin_connect/coordinator.py deleted file mode 100644 index 9af3e330409..00000000000 --- a/homeassistant/components/aladdin_connect/coordinator.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Define an object to coordinate fetching Aladdin Connect data.""" - -# mypy: ignore-errors -from datetime import timedelta -import logging - -# from genie_partner_sdk.client import AladdinConnectClient -# from genie_partner_sdk.model import GarageDoor -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -class AladdinConnectCoordinator(DataUpdateCoordinator[None]): - """Aladdin Connect Data Update Coordinator.""" - - def __init__(self, hass: HomeAssistant, acc: AladdinConnectClient) -> None: - """Initialize.""" - super().__init__( - hass, - logger=_LOGGER, - name=DOMAIN, - update_interval=timedelta(seconds=15), - ) - self.acc = acc - self.doors: list[GarageDoor] = [] - - async def async_setup(self) -> None: - """Fetch initial data.""" - self.doors = await self.acc.get_doors() - - async def _async_update_data(self) -> None: - """Fetch data from API endpoint.""" - for door in self.doors: - await self.acc.update_door(door.device_id, door.door_number) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py deleted file mode 100644 index 1be41e6b516..00000000000 --- a/homeassistant/components/aladdin_connect/cover.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Cover Entity for Genie Garage Door.""" - -# mypy: ignore-errors -from typing import Any - -# from genie_partner_sdk.model import GarageDoor -from homeassistant.components.cover import ( - CoverDeviceClass, - CoverEntity, - CoverEntityFeature, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import AladdinConnectConfigEntry, AladdinConnectCoordinator -from .entity import AladdinConnectEntity - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: AladdinConnectConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the Aladdin Connect platform.""" - coordinator = config_entry.runtime_data - - async_add_entities(AladdinDevice(coordinator, door) for door in coordinator.doors) - - -class AladdinDevice(AladdinConnectEntity, CoverEntity): - """Representation of Aladdin Connect cover.""" - - _attr_device_class = CoverDeviceClass.GARAGE - _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - _attr_name = None - - def __init__( - self, coordinator: AladdinConnectCoordinator, device: GarageDoor - ) -> None: - """Initialize the Aladdin Connect cover.""" - super().__init__(coordinator, device) - self._attr_unique_id = device.unique_id - - async def async_open_cover(self, **kwargs: Any) -> None: - """Issue open command to cover.""" - await self.coordinator.acc.open_door( - self._device.device_id, self._device.door_number - ) - - async def async_close_cover(self, **kwargs: Any) -> None: - """Issue close command to cover.""" - await self.coordinator.acc.close_door( - self._device.device_id, self._device.door_number - ) - - @property - def is_closed(self) -> bool | None: - """Update is closed attribute.""" - value = self.coordinator.acc.get_door_status( - self._device.device_id, self._device.door_number - ) - if value is None: - return None - return bool(value == "closed") - - @property - def is_closing(self) -> bool | None: - """Update is closing attribute.""" - value = self.coordinator.acc.get_door_status( - self._device.device_id, self._device.door_number - ) - if value is None: - return None - return bool(value == "closing") - - @property - def is_opening(self) -> bool | None: - """Update is opening attribute.""" - value = self.coordinator.acc.get_door_status( - self._device.device_id, self._device.door_number - ) - if value is None: - return None - return bool(value == "opening") diff --git a/homeassistant/components/aladdin_connect/entity.py b/homeassistant/components/aladdin_connect/entity.py deleted file mode 100644 index 2615cbc636e..00000000000 --- a/homeassistant/components/aladdin_connect/entity.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Defines a base Aladdin Connect entity.""" -# mypy: ignore-errors -# from genie_partner_sdk.model import GarageDoor - -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import DOMAIN -from .coordinator import AladdinConnectCoordinator - - -class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]): - """Defines a base Aladdin Connect entity.""" - - _attr_has_entity_name = True - - def __init__( - self, coordinator: AladdinConnectCoordinator, device: GarageDoor - ) -> None: - """Initialize the entity.""" - super().__init__(coordinator) - self._device = device - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device.unique_id)}, - name=device.name, - manufacturer="Overhead Door", - ) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index dce95492272..adf0d9c9b5b 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -1,11 +1,9 @@ { "domain": "aladdin_connect", "name": "Aladdin Connect", - "codeowners": ["@swcloudgenie"], - "config_flow": true, - "dependencies": ["application_credentials"], - "disabled": "This integration is disabled because it uses non-open source code to operate.", + "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", + "integration_type": "system", "iot_class": "cloud_polling", - "requirements": ["genie-partner-sdk==1.0.2"] + "requirements": [] } diff --git a/homeassistant/components/aladdin_connect/ruff.toml b/homeassistant/components/aladdin_connect/ruff.toml deleted file mode 100644 index 38f6f586aef..00000000000 --- a/homeassistant/components/aladdin_connect/ruff.toml +++ /dev/null @@ -1,5 +0,0 @@ -extend = "../../../pyproject.toml" - -lint.extend-ignore = [ - "F821" -] diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py deleted file mode 100644 index cd1fff12c97..00000000000 --- a/homeassistant/components/aladdin_connect/sensor.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Support for Aladdin Connect Garage Door sensors.""" - -# mypy: ignore-errors -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass - -# from genie_partner_sdk.client import AladdinConnectClient -# from genie_partner_sdk.model import GarageDoor -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import PERCENTAGE -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import AladdinConnectConfigEntry, AladdinConnectCoordinator -from .entity import AladdinConnectEntity - - -@dataclass(frozen=True, kw_only=True) -class AccSensorEntityDescription(SensorEntityDescription): - """Describes AladdinConnect sensor entity.""" - - value_fn: Callable[[AladdinConnectClient, str, int], float | None] - - -SENSORS: tuple[AccSensorEntityDescription, ...] = ( - AccSensorEntityDescription( - key="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, - ), -) - - -async def async_setup_entry( - hass: HomeAssistant, - entry: AladdinConnectConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Aladdin Connect sensor devices.""" - coordinator = entry.runtime_data - - async_add_entities( - AladdinConnectSensor(coordinator, door, description) - for description in SENSORS - for door in coordinator.doors - ) - - -class AladdinConnectSensor(AladdinConnectEntity, SensorEntity): - """A sensor implementation for Aladdin Connect devices.""" - - entity_description: AccSensorEntityDescription - - def __init__( - self, - coordinator: AladdinConnectCoordinator, - device: GarageDoor, - description: AccSensorEntityDescription, - ) -> None: - """Initialize a sensor for an Aladdin Connect device.""" - super().__init__(coordinator, device) - self.entity_description = description - self._attr_unique_id = f"{device.unique_id}-{description.key}" - - @property - def native_value(self) -> float | None: - """Return the state of the sensor.""" - return self.entity_description.value_fn( - self.coordinator.acc, self._device.device_id, self._device.door_number - ) diff --git a/homeassistant/components/aladdin_connect/strings.json b/homeassistant/components/aladdin_connect/strings.json index 48f9b299a1d..f62e68de64e 100644 --- a/homeassistant/components/aladdin_connect/strings.json +++ b/homeassistant/components/aladdin_connect/strings.json @@ -1,29 +1,8 @@ { - "config": { - "step": { - "pick_implementation": { - "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" - }, - "reauth_confirm": { - "title": "[%key:common::config_flow::title::reauth%]", - "description": "Aladdin Connect needs to re-authenticate your account" - } - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", - "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", - "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", - "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", - "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - }, - "create_entry": { - "default": "[%key:common::config_flow::create_entry::authenticated%]" + "issues": { + "integration_removed": { + "title": "The Aladdin Connect integration has been removed", + "description": "The Aladdin Connect integration has been removed from Home Assistant.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Aladdin Connect integration entries]({entries})." } } } diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index bc6b29e4c23..c576f242e30 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -4,7 +4,6 @@ To update, run python3 -m script.hassfest """ APPLICATION_CREDENTIALS = [ - "aladdin_connect", "electric_kiwi", "fitbit", "geocaching", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 23a13bcbfd8..463a38feb9f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -42,7 +42,6 @@ FLOWS = { "airvisual_pro", "airzone", "airzone_cloud", - "aladdin_connect", "alarmdecoder", "amberelectric", "ambient_network", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3371c8de0fa..0ad8ac09c9e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -180,12 +180,6 @@ } } }, - "aladdin_connect": { - "name": "Aladdin Connect", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_polling" - }, "alarmdecoder": { "name": "AlarmDecoder", "integration_type": "device", diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py deleted file mode 100644 index 2c158998f49..00000000000 --- a/tests/components/aladdin_connect/conftest.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Test fixtures for the Aladdin Connect Garage Door integration.""" - -from unittest.mock import AsyncMock, patch - -import pytest -from typing_extensions import Generator - -from tests.common import MockConfigEntry - - -@pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock]: - """Override async_setup_entry.""" - with patch( - "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True - ) as mock_setup_entry: - yield mock_setup_entry - - -@pytest.fixture -def mock_config_entry() -> MockConfigEntry: - """Return an Aladdin Connect config entry.""" - return MockConfigEntry( - domain="aladdin_connect", - data={}, - title="test@test.com", - unique_id="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - version=2, - ) diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py deleted file mode 100644 index 7154c53b9f6..00000000000 --- a/tests/components/aladdin_connect/test_config_flow.py +++ /dev/null @@ -1,230 +0,0 @@ -"""Test the Aladdin Connect Garage Door config flow.""" - -# from unittest.mock import AsyncMock -# -# import pytest -# -# from homeassistant.components.aladdin_connect.const import ( -# DOMAIN, -# OAUTH2_AUTHORIZE, -# OAUTH2_TOKEN, -# ) -# from homeassistant.components.application_credentials import ( -# ClientCredential, -# async_import_client_credential, -# ) -# from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigFlowResult -# from homeassistant.core import HomeAssistant -# from homeassistant.data_entry_flow import FlowResultType -# from homeassistant.helpers import config_entry_oauth2_flow -# from homeassistant.setup import async_setup_component -# -# from tests.common import MockConfigEntry -# from tests.test_util.aiohttp import AiohttpClientMocker -# from tests.typing import ClientSessionGenerator -# -# CLIENT_ID = "1234" -# CLIENT_SECRET = "5678" -# -# EXAMPLE_TOKEN = ( -# "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYWFhYWFhYS1iYmJiLWNjY2MtZGRk" -# "ZC1lZWVlZWVlZWVlZWUiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW" -# "1lIjoidGVzdEB0ZXN0LmNvbSJ9.CTU1YItIrUl8nSM3koJxlFJr5CjLghgc9gS6h45D8dE" -# ) -# -# -# @pytest.fixture -# async def setup_credentials(hass: HomeAssistant) -> None: -# """Fixture to setup credentials.""" -# assert await async_setup_component(hass, "application_credentials", {}) -# await async_import_client_credential( -# hass, -# DOMAIN, -# ClientCredential(CLIENT_ID, CLIENT_SECRET), -# ) -# -# -# async def _oauth_actions( -# hass: HomeAssistant, -# result: ConfigFlowResult, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# ) -> None: -# state = config_entry_oauth2_flow._encode_jwt( -# hass, -# { -# "flow_id": result["flow_id"], -# "redirect_uri": "https://example.com/auth/external/callback", -# }, -# ) -# -# assert result["url"] == ( -# f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" -# "&redirect_uri=https://example.com/auth/external/callback" -# f"&state={state}" -# ) -# -# client = await hass_client_no_auth() -# resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") -# assert resp.status == 200 -# assert resp.headers["content-type"] == "text/html; charset=utf-8" -# -# aioclient_mock.post( -# OAUTH2_TOKEN, -# json={ -# "refresh_token": "mock-refresh-token", -# "access_token": EXAMPLE_TOKEN, -# "type": "Bearer", -# "expires_in": 60, -# }, -# ) -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_full_flow( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Check full flow.""" -# result = await hass.config_entries.flow.async_init( -# DOMAIN, context={"source": SOURCE_USER} -# ) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.CREATE_ENTRY -# assert result["title"] == "test@test.com" -# assert result["data"]["token"]["access_token"] == EXAMPLE_TOKEN -# assert result["data"]["token"]["refresh_token"] == "mock-refresh-token" -# assert result["result"].unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" -# -# assert len(hass.config_entries.async_entries(DOMAIN)) == 1 -# assert len(mock_setup_entry.mock_calls) == 1 -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_duplicate_entry( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_config_entry: MockConfigEntry, -# ) -> None: -# """Test we abort with duplicate entry.""" -# mock_config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, context={"source": SOURCE_USER} -# ) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "already_configured" -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_reauth( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_config_entry: MockConfigEntry, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Test reauthentication.""" -# mock_config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, -# context={ -# "source": SOURCE_REAUTH, -# "entry_id": mock_config_entry.entry_id, -# }, -# data=mock_config_entry.data, -# ) -# assert result["type"] is FlowResultType.FORM -# assert result["step_id"] == "reauth_confirm" -# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "reauth_successful" -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_reauth_wrong_account( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Test reauthentication with wrong account.""" -# config_entry = MockConfigEntry( -# domain=DOMAIN, -# data={}, -# title="test@test.com", -# unique_id="aaaaaaaa-bbbb-ffff-dddd-eeeeeeeeeeee", -# version=2, -# ) -# config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, -# context={ -# "source": SOURCE_REAUTH, -# "entry_id": config_entry.entry_id, -# }, -# data=config_entry.data, -# ) -# assert result["type"] is FlowResultType.FORM -# assert result["step_id"] == "reauth_confirm" -# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "wrong_account" -# -# -# @pytest.mark.skip(reason="Integration disabled") -# @pytest.mark.usefixtures("current_request_with_host") -# async def test_reauth_old_account( -# hass: HomeAssistant, -# hass_client_no_auth: ClientSessionGenerator, -# aioclient_mock: AiohttpClientMocker, -# setup_credentials: None, -# mock_setup_entry: AsyncMock, -# ) -> None: -# """Test reauthentication with old account.""" -# config_entry = MockConfigEntry( -# domain=DOMAIN, -# data={}, -# title="test@test.com", -# unique_id="test@test.com", -# version=2, -# ) -# config_entry.add_to_hass(hass) -# result = await hass.config_entries.flow.async_init( -# DOMAIN, -# context={ -# "source": SOURCE_REAUTH, -# "entry_id": config_entry.entry_id, -# }, -# data=config_entry.data, -# ) -# assert result["type"] is FlowResultType.FORM -# assert result["step_id"] == "reauth_confirm" -# result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) -# await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) -# -# result = await hass.config_entries.flow.async_configure(result["flow_id"]) -# assert result["type"] is FlowResultType.ABORT -# assert result["reason"] == "reauth_successful" -# assert config_entry.unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py new file mode 100644 index 00000000000..b01af287b7b --- /dev/null +++ b/tests/components/aladdin_connect/test_init.py @@ -0,0 +1,50 @@ +"""Tests for the Aladdin Connect integration.""" + +from homeassistant.components.aladdin_connect import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from tests.common import MockConfigEntry + + +async def test_aladdin_connect_repair_issue( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test the Aladdin Connect configuration entry loading/unloading handles the repair.""" + config_entry_1 = MockConfigEntry( + title="Example 1", + domain=DOMAIN, + ) + config_entry_1.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_1.entry_id) + await hass.async_block_till_done() + assert config_entry_1.state is ConfigEntryState.LOADED + + # Add a second one + config_entry_2 = MockConfigEntry( + title="Example 2", + domain=DOMAIN, + ) + config_entry_2.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_2.entry_id) + await hass.async_block_till_done() + + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + + # Remove the first one + await hass.config_entries.async_remove(config_entry_1.entry_id) + await hass.async_block_till_done() + + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + + # Remove the second one + await hass.config_entries.async_remove(config_entry_2.entry_id) + await hass.async_block_till_done() + + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.NOT_LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None From 5cb41106b5628df354c17b81ec65429f6276c27c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 2 Jul 2024 13:31:23 +0200 Subject: [PATCH 0630/2411] Reolink replace automatic removal of devices by manual removal (#120981) Co-authored-by: Robert Resch --- homeassistant/components/reolink/__init__.py | 87 ++++++++++---------- tests/components/reolink/test_init.py | 31 +++++-- 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 150a23dc64e..02d3cc16419 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -147,9 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b firmware_coordinator=firmware_coordinator, ) - # first migrate and then cleanup, otherwise entities lost migrate_entity_ids(hass, config_entry.entry_id, host) - cleanup_disconnected_cams(hass, config_entry.entry_id, host) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -179,6 +177,50 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry +) -> bool: + """Remove a device from a config entry.""" + host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host + (device_uid, ch) = get_device_uid_and_ch(device, host) + + if not host.api.is_nvr or ch is None: + _LOGGER.warning( + "Cannot remove Reolink device %s, because it is not a camera connected " + "to a NVR/Hub, please remove the integration entry instead", + device.name, + ) + return False # Do not remove the host/NVR itself + + if ch not in host.api.channels: + _LOGGER.debug( + "Removing Reolink device %s, " + "since no camera is connected to NVR channel %s anymore", + device.name, + ch, + ) + return True + + await host.api.get_state(cmd="GetChannelstatus") # update the camera_online status + if not host.api.camera_online(ch): + _LOGGER.debug( + "Removing Reolink device %s, " + "since the camera connected to channel %s is offline", + device.name, + ch, + ) + return True + + _LOGGER.warning( + "Cannot remove Reolink device %s on channel %s, because it is still connected " + "to the NVR/Hub, please first remove the camera from the NVR/Hub " + "in the reolink app", + device.name, + ch, + ) + return False + + def get_device_uid_and_ch( device: dr.DeviceEntry, host: ReolinkHost ) -> tuple[list[str], int | None]: @@ -197,47 +239,6 @@ def get_device_uid_and_ch( return (device_uid, ch) -def cleanup_disconnected_cams( - hass: HomeAssistant, config_entry_id: str, host: ReolinkHost -) -> None: - """Clean-up disconnected camera channels.""" - if not host.api.is_nvr: - return - - device_reg = dr.async_get(hass) - devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) - for device in devices: - (device_uid, ch) = get_device_uid_and_ch(device, host) - if ch is None: - continue # Do not consider the NVR itself - - ch_model = host.api.camera_model(ch) - remove = False - if ch not in host.api.channels: - remove = True - _LOGGER.debug( - "Removing Reolink device %s, " - "since no camera is connected to NVR channel %s anymore", - device.name, - ch, - ) - if ch_model not in [device.model, "Unknown"]: - remove = True - _LOGGER.debug( - "Removing Reolink device %s, " - "since the camera model connected to channel %s changed from %s to %s", - device.name, - ch, - device.model, - ch_model, - ) - if not remove: - continue - - # clean device registry and associated entities - device_reg.async_remove_device(device.id) - - def migrate_entity_ids( hass: HomeAssistant, config_entry_id: str, host: ReolinkHost ) -> None: diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index a6c798f9415..f70fd312051 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -36,6 +36,7 @@ from .conftest import ( ) from tests.common import MockConfigEntry, async_fire_time_changed +from tests.typing import WebSocketGenerator pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") @@ -179,16 +180,27 @@ async def test_entry_reloading( None, [TEST_HOST_MODEL, TEST_CAM_MODEL], ), + ( + "is_nvr", + False, + [TEST_HOST_MODEL, TEST_CAM_MODEL], + ), ("channels", [], [TEST_HOST_MODEL]), ( - "camera_model", - Mock(return_value="RLC-567"), - [TEST_HOST_MODEL, "RLC-567"], + "camera_online", + Mock(return_value=False), + [TEST_HOST_MODEL], + ), + ( + "channel_for_uid", + Mock(return_value=-1), + [TEST_HOST_MODEL], ), ], ) -async def test_cleanup_disconnected_cams( +async def test_removing_disconnected_cams( hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, config_entry: MockConfigEntry, reolink_connect: MagicMock, device_registry: dr.DeviceRegistry, @@ -197,8 +209,10 @@ async def test_cleanup_disconnected_cams( value: Any, expected_models: list[str], ) -> None: - """Test device and entity registry are cleaned up when camera is disconnected from NVR.""" + """Test device and entity registry are cleaned up when camera is removed.""" reolink_connect.channels = [0] + assert await async_setup_component(hass, "config", {}) + client = await hass_ws_client(hass) # setup CH 0 and NVR switch entities/device with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -215,6 +229,13 @@ async def test_cleanup_disconnected_cams( setattr(reolink_connect, attr, value) with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): assert await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + expected_success = TEST_CAM_MODEL not in expected_models + for device in device_entries: + if device.model == TEST_CAM_MODEL: + response = await client.remove_device(device.id, config_entry.entry_id) + assert response["success"] == expected_success device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id From 807ed0ce106c4ee448682561775b49bfd3ef7d35 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 2 Jul 2024 12:28:32 +0200 Subject: [PATCH 0631/2411] Do not hold core startup with reolink firmware check task (#120985) --- homeassistant/components/reolink/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 02d3cc16419..1caf4e79cd5 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -133,7 +133,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) # If camera WAN blocked, firmware check fails and takes long, do not prevent setup - config_entry.async_create_task(hass, firmware_coordinator.async_refresh()) + config_entry.async_create_background_task( + hass, + firmware_coordinator.async_refresh(), + f"Reolink firmware check {config_entry.entry_id}", + ) # Fetch initial data so we have data when entities subscribe try: await device_coordinator.async_config_entry_first_refresh() From b3e833f677bf027efbc825193b271d49c6285761 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 2 Jul 2024 21:03:01 +0200 Subject: [PATCH 0632/2411] Fix setting target temperature for single setpoint Matter thermostat (#121011) --- homeassistant/components/matter/climate.py | 28 ++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index c97124f4305..2c05fd3373e 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -267,19 +267,13 @@ class MatterClimate(MatterEntity, ClimateEntity): self._attr_hvac_action = HVACAction.FAN case _: self._attr_hvac_action = HVACAction.OFF - # update target_temperature - if self._attr_hvac_mode == HVACMode.HEAT_COOL: - self._attr_target_temperature = None - elif self._attr_hvac_mode == HVACMode.COOL: - self._attr_target_temperature = self._get_temperature_in_degrees( - clusters.Thermostat.Attributes.OccupiedCoolingSetpoint - ) - else: - self._attr_target_temperature = self._get_temperature_in_degrees( - clusters.Thermostat.Attributes.OccupiedHeatingSetpoint - ) # update target temperature high/low - if self._attr_hvac_mode == HVACMode.HEAT_COOL: + supports_range = ( + self._attr_supported_features + & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) + if supports_range and self._attr_hvac_mode == HVACMode.HEAT_COOL: + self._attr_target_temperature = None self._attr_target_temperature_high = self._get_temperature_in_degrees( clusters.Thermostat.Attributes.OccupiedCoolingSetpoint ) @@ -289,6 +283,16 @@ class MatterClimate(MatterEntity, ClimateEntity): else: self._attr_target_temperature_high = None self._attr_target_temperature_low = None + # update target_temperature + if self._attr_hvac_mode == HVACMode.COOL: + self._attr_target_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedCoolingSetpoint + ) + else: + self._attr_target_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedHeatingSetpoint + ) + # update min_temp if self._attr_hvac_mode == HVACMode.COOL: attribute = clusters.Thermostat.Attributes.AbsMinCoolSetpointLimit From 1fa6972a665052cc5f7c7dbb3d7fc5b32a8209fd Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 2 Jul 2024 21:02:29 +0200 Subject: [PATCH 0633/2411] Handle mains power for Matter appliances (#121023) --- homeassistant/components/matter/climate.py | 7 +++++++ homeassistant/components/matter/fan.py | 16 +++++++++++++++- tests/components/matter/test_climate.py | 9 +++++++-- tests/components/matter/test_fan.py | 6 ++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index 2c05fd3373e..192cb6b3bb4 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -227,6 +227,13 @@ class MatterClimate(MatterEntity, ClimateEntity): self._attr_current_temperature = self._get_temperature_in_degrees( clusters.Thermostat.Attributes.LocalTemperature ) + if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False: + # special case: the appliance has a dedicated Power switch on the OnOff cluster + # if the mains power is off - treat it as if the HVAC mode is off + self._attr_hvac_mode = HVACMode.OFF + self._attr_hvac_action = None + return + # update hvac_mode from SystemMode system_mode_value = int( self.get_matter_attribute_value(clusters.Thermostat.Attributes.SystemMode) diff --git a/homeassistant/components/matter/fan.py b/homeassistant/components/matter/fan.py index 0ce42f14d39..86f03dc7a03 100644 --- a/homeassistant/components/matter/fan.py +++ b/homeassistant/components/matter/fan.py @@ -170,6 +170,14 @@ class MatterFan(MatterEntity, FanEntity): """Update from device.""" if not hasattr(self, "_attr_preset_modes"): self._calculate_features() + + if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False: + # special case: the appliance has a dedicated Power switch on the OnOff cluster + # if the mains power is off - treat it as if the fan mode is off + self._attr_preset_mode = None + self._attr_percentage = 0 + return + if self._attr_supported_features & FanEntityFeature.DIRECTION: direction_value = self.get_matter_attribute_value( clusters.FanControl.Attributes.AirflowDirection @@ -200,7 +208,13 @@ class MatterFan(MatterEntity, FanEntity): wind_setting = self.get_matter_attribute_value( clusters.FanControl.Attributes.WindSetting ) - if ( + fan_mode = self.get_matter_attribute_value( + clusters.FanControl.Attributes.FanMode + ) + if fan_mode == clusters.FanControl.Enums.FanModeEnum.kOff: + self._attr_preset_mode = None + self._attr_percentage = 0 + elif ( self._attr_preset_modes and PRESET_NATURAL_WIND in self._attr_preset_modes and wind_setting & WindBitmap.kNaturalWind diff --git a/tests/components/matter/test_climate.py b/tests/components/matter/test_climate.py index 6a4cf34a640..e0015e8b445 100644 --- a/tests/components/matter/test_climate.py +++ b/tests/components/matter/test_climate.py @@ -315,14 +315,19 @@ async def test_room_airconditioner( state = hass.states.get("climate.room_airconditioner_thermostat") assert state assert state.attributes["current_temperature"] == 20 - assert state.attributes["min_temp"] == 16 - assert state.attributes["max_temp"] == 32 + # room airconditioner has mains power on OnOff cluster with value set to False + assert state.state == HVACMode.OFF # test supported features correctly parsed # WITHOUT temperature_range support mask = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF assert state.attributes["supported_features"] & mask == mask + # set mains power to ON (OnOff cluster) + set_node_attribute(room_airconditioner, 1, 6, 0, True) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.room_airconditioner_thermostat") + # test supported HVAC modes include fan and dry modes assert state.attributes["hvac_modes"] == [ HVACMode.OFF, diff --git a/tests/components/matter/test_fan.py b/tests/components/matter/test_fan.py index 30bd7f4a009..7e964d672ca 100644 --- a/tests/components/matter/test_fan.py +++ b/tests/components/matter/test_fan.py @@ -92,6 +92,12 @@ async def test_fan_base( await trigger_subscription_callback(hass, matter_client) state = hass.states.get(entity_id) assert state.attributes["preset_mode"] == "sleep_wind" + # set mains power to OFF (OnOff cluster) + set_node_attribute(air_purifier, 1, 6, 0, False) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get(entity_id) + assert state.attributes["preset_mode"] is None + assert state.attributes["percentage"] == 0 async def test_fan_turn_on_with_percentage( From 6b045a7d7bd0e7fb8cc916c78de2a41ca4f0858a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Jul 2024 21:09:55 +0200 Subject: [PATCH 0634/2411] Bump version to 2024.7.0b8 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5f020a02624..3828f2cfbf7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 6320551a082..bd34e19c555 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b7" +version = "2024.7.0b8" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 0e52d149e47d15db4d97c7693c95956ee65b914b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 2 Jul 2024 21:57:09 +0200 Subject: [PATCH 0635/2411] Update voluptuous to 0.15.2 (#120631) * Update voluptuous to 0.15.1 * Fix typing issues * Add type ignores for json result type * Update voluptuous to 0.15.2 --------- Co-authored-by: J. Nick Koston --- homeassistant/components/auth/login_flow.py | 2 +- homeassistant/components/auth/mfa_setup_flow.py | 2 +- homeassistant/components/automation/config.py | 4 ++-- homeassistant/components/websocket_api/decorators.py | 2 +- homeassistant/config.py | 2 +- homeassistant/data_entry_flow.py | 6 ++---- homeassistant/helpers/data_entry_flow.py | 2 +- homeassistant/package_constraints.txt | 2 +- homeassistant/util/yaml/objects.py | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 11 files changed, 13 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 5bad0dbb999..3664c3ca5c9 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -215,7 +215,7 @@ def _prepare_result_json( data = result.copy() if (schema := data["data_schema"]) is None: - data["data_schema"] = [] + data["data_schema"] = [] # type: ignore[typeddict-item] # json result type else: data["data_schema"] = voluptuous_serialize.convert(schema) diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 35d87cafd4f..8ae55396fa9 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -156,7 +156,7 @@ def _prepare_result_json( data = result.copy() if (schema := data["data_schema"]) is None: - data["data_schema"] = [] + data["data_schema"] = [] # type: ignore[typeddict-item] # json result type else: data["data_schema"] = voluptuous_serialize.convert(schema) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 676aba946f4..cc4e9aba7fb 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Mapping from contextlib import suppress from enum import StrEnum -from typing import Any, cast +from typing import Any import voluptuous as vol from voluptuous.humanize import humanize_error @@ -90,7 +90,7 @@ async def _async_validate_config_item( # noqa: C901 def _humanize(err: Exception, config: ConfigType) -> str: """Humanize vol.Invalid, stringify other exceptions.""" if isinstance(err, vol.Invalid): - return cast(str, humanize_error(config, err)) + return humanize_error(config, err) return str(err) def _log_invalid_automation( diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index b9924bc91d1..2c8a6cc02f1 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -145,7 +145,7 @@ def websocket_command( def decorate(func: const.WebSocketCommandHandler) -> const.WebSocketCommandHandler: """Decorate ws command function.""" - if is_dict and len(schema) == 1: # type only empty schema + if is_dict and len(schema) == 1: # type: ignore[arg-type] # type only empty schema func._ws_schema = False # type: ignore[attr-defined] # noqa: SLF001 elif is_dict: func._ws_schema = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend(schema) # type: ignore[attr-defined] # noqa: SLF001 diff --git a/homeassistant/config.py b/homeassistant/config.py index ff679d4df51..96bc94636a2 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -947,7 +947,7 @@ def _log_pkg_error( def _identify_config_schema(module: ComponentProtocol) -> str | None: """Extract the schema and identify list or dict based.""" if not isinstance(module.CONFIG_SCHEMA, vol.Schema): - return None + return None # type: ignore[unreachable] schema = module.CONFIG_SCHEMA.schema diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index f632e3e4dde..3ac5f85dfc8 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -112,9 +112,7 @@ class UnknownStep(FlowError): """Unknown step specified.""" -# ignore misc is required as vol.Invalid is not typed -# mypy error: Class cannot subclass "Invalid" (has type "Any") -class InvalidData(vol.Invalid): # type: ignore[misc] +class InvalidData(vol.Invalid): """Invalid data provided.""" def __init__( @@ -386,7 +384,7 @@ class FlowManager(abc.ABC, Generic[_FlowResultT, _HandlerT]): ) is not None and user_input is not None: data_schema = cast(vol.Schema, data_schema) try: - user_input = data_schema(user_input) # type: ignore[operator] + user_input = data_schema(user_input) except vol.Invalid as ex: raised_errors = [ex] if isinstance(ex, vol.MultipleInvalid): diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 2adab32195b..b2cad292e3d 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -47,7 +47,7 @@ class _BaseFlowManagerView(HomeAssistantView, Generic[_FlowManagerT]): data = result.copy() if (schema := data["data_schema"]) is None: - data["data_schema"] = [] + data["data_schema"] = [] # type: ignore[typeddict-item] # json result type else: data["data_schema"] = voluptuous_serialize.convert( schema, custom_serializer=cv.custom_serializer diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index af165f29ae4..c058152c2c4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -60,7 +60,7 @@ ulid-transform==0.9.0 urllib3>=1.26.5,<2 voluptuous-openapi==0.0.4 voluptuous-serialize==2.6.0 -voluptuous==0.13.1 +voluptuous==0.15.2 webrtc-noise-gain==1.2.3 yarl==1.9.4 zeroconf==0.132.2 diff --git a/homeassistant/util/yaml/objects.py b/homeassistant/util/yaml/objects.py index d35ba11d25e..7e4019331c6 100644 --- a/homeassistant/util/yaml/objects.py +++ b/homeassistant/util/yaml/objects.py @@ -29,7 +29,7 @@ class NodeStrClass(str): def __voluptuous_compile__(self, schema: vol.Schema) -> Any: """Needed because vol.Schema.compile does not handle str subclasses.""" - return _compile_scalar(self) + return _compile_scalar(self) # type: ignore[no-untyped-call] class NodeDictClass(dict): diff --git a/pyproject.toml b/pyproject.toml index d09e4b13d69..6322b330d24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ dependencies = [ # Temporary setting an upper bound, to prevent compat issues with urllib3>=2 # https://github.com/home-assistant/core/issues/97248 "urllib3>=1.26.5,<2", - "voluptuous==0.13.1", + "voluptuous==0.15.2", "voluptuous-serialize==2.6.0", "voluptuous-openapi==0.0.4", "yarl==1.9.4", diff --git a/requirements.txt b/requirements.txt index f41fca19ecc..a5349c4dd91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,7 +39,7 @@ SQLAlchemy==2.0.31 typing-extensions>=4.12.2,<5.0 ulid-transform==0.9.0 urllib3>=1.26.5,<2 -voluptuous==0.13.1 +voluptuous==0.15.2 voluptuous-serialize==2.6.0 voluptuous-openapi==0.0.4 yarl==1.9.4 From 9749cf113a5c496f491a5db8679430386c3d3fad Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Jul 2024 22:13:07 +0200 Subject: [PATCH 0636/2411] Update frontend to 20240702.0 (#121032) --- 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 70f1f5f4f4f..0d32624cf57 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240628.0"] + "requirements": ["home-assistant-frontend==20240702.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c058152c2c4..b81e86fcdb4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240628.0 +home-assistant-frontend==20240702.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index f4ed86670a5..35268eff566 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240628.0 +home-assistant-frontend==20240702.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e752924a235..10b8d51218f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240628.0 +home-assistant-frontend==20240702.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From 7d31d553d041c251c2d60b78289932028267978f Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 2 Jul 2024 22:13:19 +0200 Subject: [PATCH 0637/2411] Temporarily set apprise log level to debug in tests (#121029) Co-authored-by: Franck Nijhof --- tests/components/apprise/test_notify.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py index 7d37d7a5d99..d73fa72d6c7 100644 --- a/tests/components/apprise/test_notify.py +++ b/tests/components/apprise/test_notify.py @@ -1,14 +1,27 @@ """The tests for the apprise notification platform.""" +import logging from pathlib import Path from unittest.mock import MagicMock, patch +import pytest + from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component BASE_COMPONENT = "notify" +@pytest.fixture(autouse=True) +def reset_log_level(): + """Set and reset log level after each test case.""" + logger = logging.getLogger("apprise") + orig_level = logger.level + logger.setLevel(logging.DEBUG) + yield + logger.setLevel(orig_level) + + async def test_apprise_config_load_fail01(hass: HomeAssistant) -> None: """Test apprise configuration failures 1.""" From 4377f4cbea5782686e83950e56854a1d90fc7a4b Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 2 Jul 2024 22:13:19 +0200 Subject: [PATCH 0638/2411] Temporarily set apprise log level to debug in tests (#121029) Co-authored-by: Franck Nijhof --- tests/components/apprise/test_notify.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py index 7d37d7a5d99..d73fa72d6c7 100644 --- a/tests/components/apprise/test_notify.py +++ b/tests/components/apprise/test_notify.py @@ -1,14 +1,27 @@ """The tests for the apprise notification platform.""" +import logging from pathlib import Path from unittest.mock import MagicMock, patch +import pytest + from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component BASE_COMPONENT = "notify" +@pytest.fixture(autouse=True) +def reset_log_level(): + """Set and reset log level after each test case.""" + logger = logging.getLogger("apprise") + orig_level = logger.level + logger.setLevel(logging.DEBUG) + yield + logger.setLevel(orig_level) + + async def test_apprise_config_load_fail01(hass: HomeAssistant) -> None: """Test apprise configuration failures 1.""" From d1e76d5c3cf374e5d4ace63e0a6734d36e4e0037 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Jul 2024 22:13:07 +0200 Subject: [PATCH 0639/2411] Update frontend to 20240702.0 (#121032) --- 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 70f1f5f4f4f..0d32624cf57 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240628.0"] + "requirements": ["home-assistant-frontend==20240702.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7cccd58d73f..3ffa9d92f63 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240628.0 +home-assistant-frontend==20240702.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7ba781583f5..de7cc9fa13f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240628.0 +home-assistant-frontend==20240702.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65f9b4b1770..0a63b696617 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240628.0 +home-assistant-frontend==20240702.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From 1b9f27fab753997148f4cd962dee89847970a31e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Jul 2024 22:15:17 +0200 Subject: [PATCH 0640/2411] Bump version to 2024.7.0b9 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3828f2cfbf7..5d20a8507bf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index bd34e19c555..1ebd3acf1e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b8" +version = "2024.7.0b9" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From bdc68057713275e0ed1d99be6a6ad7ae7d128001 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Jul 2024 13:17:25 -0700 Subject: [PATCH 0641/2411] Bump orjson to 3.10.6 (#121028) --- 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 b81e86fcdb4..37f26b99225 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 mutagen==1.47.0 -orjson==3.9.15 +orjson==3.10.6 packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.3.0 diff --git a/pyproject.toml b/pyproject.toml index 6322b330d24..cd622c964b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dependencies = [ "cryptography==42.0.8", "Pillow==10.3.0", "pyOpenSSL==24.1.0", - "orjson==3.9.15", + "orjson==3.10.6", "packaging>=23.1", "pip>=21.3.1", "psutil-home-assistant==0.0.1", diff --git a/requirements.txt b/requirements.txt index a5349c4dd91..969ca2d86ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ PyJWT==2.8.0 cryptography==42.0.8 Pillow==10.3.0 pyOpenSSL==24.1.0 -orjson==3.9.15 +orjson==3.10.6 packaging>=23.1 pip>=21.3.1 psutil-home-assistant==0.0.1 From 476efb1d369ccac25f347793979ace9356b32807 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Jul 2024 22:19:33 +0200 Subject: [PATCH 0642/2411] Improve type hints in home_connect tests (#121014) --- tests/components/home_connect/conftest.py | 4 ++-- .../home_connect/test_binary_sensor.py | 8 +++----- tests/components/home_connect/test_init.py | 16 +++++++--------- tests/components/home_connect/test_sensor.py | 10 ++++------ 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/tests/components/home_connect/conftest.py b/tests/components/home_connect/conftest.py index f4c19320826..4a92545ff2f 100644 --- a/tests/components/home_connect/conftest.py +++ b/tests/components/home_connect/conftest.py @@ -94,7 +94,7 @@ async def bypass_throttle(hass: HomeAssistant, config_entry: MockConfigEntry): @pytest.fixture(name="bypass_throttle") -def mock_bypass_throttle(): +def mock_bypass_throttle() -> Generator[None]: """Fixture to bypass the throttle decorator in __init__.""" with patch( "homeassistant.components.home_connect.update_all_devices", @@ -122,7 +122,7 @@ async def mock_integration_setup( @pytest.fixture(name="get_appliances") -def mock_get_appliances() -> Generator[None, Any, None]: +def mock_get_appliances() -> Generator[MagicMock]: """Mock ConfigEntryAuth parent (HomeAssistantAPI) method.""" with patch( "homeassistant.components.home_connect.api.ConfigEntryAuth.get_appliances", diff --git a/tests/components/home_connect/test_binary_sensor.py b/tests/components/home_connect/test_binary_sensor.py index d21aec35045..39502507439 100644 --- a/tests/components/home_connect/test_binary_sensor.py +++ b/tests/components/home_connect/test_binary_sensor.py @@ -1,7 +1,6 @@ """Tests for home_connect binary_sensor entities.""" -from collections.abc import Awaitable, Callable, Generator -from typing import Any +from collections.abc import Awaitable, Callable from unittest.mock import MagicMock, Mock import pytest @@ -26,9 +25,8 @@ def platforms() -> list[str]: return [Platform.BINARY_SENSOR] +@pytest.mark.usefixtures("bypass_throttle") async def test_binary_sensors( - bypass_throttle: Generator[None, Any, None], - hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[], Awaitable[bool]], setup_credentials: None, @@ -51,10 +49,10 @@ async def test_binary_sensors( ("", "unavailable"), ], ) +@pytest.mark.usefixtures("bypass_throttle") async def test_binary_sensors_door_states( expected: str, state: str, - bypass_throttle: Generator[None, Any, None], hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[], Awaitable[bool]], diff --git a/tests/components/home_connect/test_init.py b/tests/components/home_connect/test_init.py index 616a82edebc..f9b1d5c543e 100644 --- a/tests/components/home_connect/test_init.py +++ b/tests/components/home_connect/test_init.py @@ -1,6 +1,6 @@ """Test the integration init functionality.""" -from collections.abc import Awaitable, Callable, Generator +from collections.abc import Awaitable, Callable from typing import Any from unittest.mock import MagicMock, Mock @@ -117,8 +117,8 @@ SERVICE_APPLIANCE_METHOD_MAPPING = { } +@pytest.mark.usefixtures("bypass_throttle") async def test_api_setup( - bypass_throttle: Generator[None, Any, None], hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[], Awaitable[bool]], @@ -137,9 +137,8 @@ async def test_api_setup( assert config_entry.state == ConfigEntryState.NOT_LOADED +@pytest.mark.usefixtures("bypass_throttle") async def test_exception_handling( - bypass_throttle: Generator[None, Any, None], - hass: HomeAssistant, integration_setup: Callable[[], Awaitable[bool]], config_entry: MockConfigEntry, setup_credentials: None, @@ -154,8 +153,8 @@ async def test_exception_handling( @pytest.mark.parametrize("token_expiration_time", [12345]) +@pytest.mark.usefixtures("bypass_throttle") async def test_token_refresh_success( - bypass_throttle: Generator[None, Any, None], integration_setup: Callable[[], Awaitable[bool]], config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, @@ -227,9 +226,8 @@ async def test_update_throttle( assert get_appliances.call_count == 0 +@pytest.mark.usefixtures("bypass_throttle") async def test_http_error( - bypass_throttle: Generator[None, Any, None], - hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[], Awaitable[bool]], setup_credentials: None, @@ -247,9 +245,9 @@ async def test_http_error( "service_call", SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS, ) +@pytest.mark.usefixtures("bypass_throttle") async def test_services( service_call: list[dict[str, Any]], - bypass_throttle: Generator[None, Any, None], hass: HomeAssistant, device_registry: dr.DeviceRegistry, config_entry: MockConfigEntry, @@ -279,8 +277,8 @@ async def test_services( ) +@pytest.mark.usefixtures("bypass_throttle") async def test_services_exception( - bypass_throttle: Generator[None, Any, None], hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[], Awaitable[bool]], diff --git a/tests/components/home_connect/test_sensor.py b/tests/components/home_connect/test_sensor.py index f30f017d6d3..661ac62403f 100644 --- a/tests/components/home_connect/test_sensor.py +++ b/tests/components/home_connect/test_sensor.py @@ -1,7 +1,6 @@ """Tests for home_connect sensor entities.""" -from collections.abc import Awaitable, Callable, Generator -from typing import Any +from collections.abc import Awaitable, Callable from unittest.mock import MagicMock, Mock from freezegun.api import FrozenDateTimeFactory @@ -69,9 +68,8 @@ def platforms() -> list[str]: return [Platform.SENSOR] +@pytest.mark.usefixtures("bypass_throttle") async def test_sensors( - bypass_throttle: Generator[None, Any, None], - hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[], Awaitable[bool]], setup_credentials: None, @@ -131,12 +129,12 @@ ENTITY_ID_STATES = { ) ), ) +@pytest.mark.usefixtures("bypass_throttle") async def test_event_sensors( appliance: Mock, states: tuple, event_run: dict, freezer: FrozenDateTimeFactory, - bypass_throttle: Generator[None, Any, None], hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[], Awaitable[bool]], @@ -180,10 +178,10 @@ ENTITY_ID_EDGE_CASE_STATES = [ @pytest.mark.parametrize("appliance", [TEST_HC_APP], indirect=True) +@pytest.mark.usefixtures("bypass_throttle") async def test_remaining_prog_time_edge_cases( appliance: Mock, freezer: FrozenDateTimeFactory, - bypass_throttle: Generator[None, Any, None], hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[], Awaitable[bool]], From 510315732a790f86d4f260aac574125e08d29fdc Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Tue, 2 Jul 2024 22:22:22 +0200 Subject: [PATCH 0643/2411] Add Beoconnect Core as selectable Bang & Olufsen device (#121015) Add Beoconnect Core as available device --- homeassistant/components/bang_olufsen/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/bang_olufsen/const.py b/homeassistant/components/bang_olufsen/const.py index 25e7f8e15dc..5db1437a737 100644 --- a/homeassistant/components/bang_olufsen/const.py +++ b/homeassistant/components/bang_olufsen/const.py @@ -62,6 +62,7 @@ class BangOlufsenMediaType(StrEnum): class BangOlufsenModel(StrEnum): """Enum for compatible model names.""" + BEOCONNECT_CORE = "Beoconnect Core" BEOLAB_8 = "BeoLab 8" BEOLAB_28 = "BeoLab 28" BEOSOUND_2 = "Beosound 2 3rd Gen" From a4d889e9588a977d5b7cc320975912a064b660ba Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 3 Jul 2024 01:29:12 +0200 Subject: [PATCH 0644/2411] Remove BaseTableManager active attribute (#121020) --- homeassistant/components/recorder/table_managers/__init__.py | 1 - homeassistant/components/recorder/table_managers/event_data.py | 1 - homeassistant/components/recorder/table_managers/event_types.py | 2 ++ .../components/recorder/table_managers/state_attributes.py | 1 - homeassistant/components/recorder/table_managers/states_meta.py | 2 ++ 5 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/table_managers/__init__.py b/homeassistant/components/recorder/table_managers/__init__.py index bc053562c14..82a08ebfc68 100644 --- a/homeassistant/components/recorder/table_managers/__init__.py +++ b/homeassistant/components/recorder/table_managers/__init__.py @@ -24,7 +24,6 @@ class BaseTableManager[_DataT]: for a table. When data is committed to the database, the manager will move the data from the pending to the id map. """ - self.active = False self.recorder = recorder self._pending: dict[EventType[Any] | str, _DataT] = {} diff --git a/homeassistant/components/recorder/table_managers/event_data.py b/homeassistant/components/recorder/table_managers/event_data.py index 1d2fa580b3c..1bab49ec543 100644 --- a/homeassistant/components/recorder/table_managers/event_data.py +++ b/homeassistant/components/recorder/table_managers/event_data.py @@ -32,7 +32,6 @@ class EventDataManager(BaseLRUTableManager[EventData]): def __init__(self, recorder: Recorder) -> None: """Initialize the event type manager.""" super().__init__(recorder, CACHE_SIZE) - self.active = True # always active def serialize_from_event(self, event: Event) -> bytes | None: """Serialize event data.""" diff --git a/homeassistant/components/recorder/table_managers/event_types.py b/homeassistant/components/recorder/table_managers/event_types.py index 266c970fe1f..81bddce948d 100644 --- a/homeassistant/components/recorder/table_managers/event_types.py +++ b/homeassistant/components/recorder/table_managers/event_types.py @@ -28,6 +28,8 @@ CACHE_SIZE = 2048 class EventTypeManager(BaseLRUTableManager[EventTypes]): """Manage the EventTypes table.""" + active = False + def __init__(self, recorder: Recorder) -> None: """Initialize the event type manager.""" super().__init__(recorder, CACHE_SIZE) diff --git a/homeassistant/components/recorder/table_managers/state_attributes.py b/homeassistant/components/recorder/table_managers/state_attributes.py index 5ed67b0504f..aa7e6f3e926 100644 --- a/homeassistant/components/recorder/table_managers/state_attributes.py +++ b/homeassistant/components/recorder/table_managers/state_attributes.py @@ -37,7 +37,6 @@ class StateAttributesManager(BaseLRUTableManager[StateAttributes]): def __init__(self, recorder: Recorder) -> None: """Initialize the event type manager.""" super().__init__(recorder, CACHE_SIZE) - self.active = True # always active def serialize_from_event(self, event: Event[EventStateChangedData]) -> bytes | None: """Serialize event data.""" diff --git a/homeassistant/components/recorder/table_managers/states_meta.py b/homeassistant/components/recorder/table_managers/states_meta.py index 0ea2c7415b9..80d20dbec94 100644 --- a/homeassistant/components/recorder/table_managers/states_meta.py +++ b/homeassistant/components/recorder/table_managers/states_meta.py @@ -24,6 +24,8 @@ CACHE_SIZE = 8192 class StatesMetaManager(BaseLRUTableManager[StatesMeta]): """Manage the StatesMeta table.""" + active = False + def __init__(self, recorder: Recorder) -> None: """Initialize the states meta manager.""" self._did_first_load = False From 399548a9737b2bc76f8a68da4f2f5ce18ca67a63 Mon Sep 17 00:00:00 2001 From: Christophe Gagnier Date: Tue, 2 Jul 2024 20:32:34 -0400 Subject: [PATCH 0645/2411] Bump pytechnove to 1.3.0 (#120975) --- homeassistant/components/technove/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/technove/manifest.json b/homeassistant/components/technove/manifest.json index c63151560f8..b4dec10c2ef 100644 --- a/homeassistant/components/technove/manifest.json +++ b/homeassistant/components/technove/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/technove", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["python-technove==1.2.2"], + "requirements": ["python-technove==1.3.0"], "zeroconf": ["_technove-stations._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 35268eff566..6fd6ca3dbd3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2324,7 +2324,7 @@ python-songpal==0.16.2 python-tado==0.17.6 # homeassistant.components.technove -python-technove==1.2.2 +python-technove==1.3.0 # homeassistant.components.telegram_bot python-telegram-bot[socks]==21.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10b8d51218f..760197c11d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1818,7 +1818,7 @@ python-songpal==0.16.2 python-tado==0.17.6 # homeassistant.components.technove -python-technove==1.2.2 +python-technove==1.3.0 # homeassistant.components.telegram_bot python-telegram-bot[socks]==21.0.1 From 23e061ccbdabebf56fc6ff332bc18791bcaf12d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Jul 2024 19:11:31 -0700 Subject: [PATCH 0646/2411] Bump uiprotect to 5.0.0 (#121034) changelog: https://github.com/uilibs/uiprotect/compare/v4.2.0...v5.0.0 Breaking change in the lib is not auto converting enum values to their underlying values. They are mostly StrEnums so this should not have any impact unless I missed one. --- 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 6691d738cd0..26b1f424db1 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==4.2.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==5.0.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 6fd6ca3dbd3..668cbdcf592 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2789,7 +2789,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==4.2.0 +uiprotect==5.0.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 760197c11d2..cf98bca1fd5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2169,7 +2169,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==4.2.0 +uiprotect==5.0.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From c33cbf83122910bd5e78538a19472a3937c2bf99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Jul 2024 19:12:17 -0700 Subject: [PATCH 0647/2411] Bump inkbird-ble to 0.5.7 (#121039) changelog: https://github.com/Bluetooth-Devices/inkbird-ble/compare/v0.5.6...v0.5.7 --- homeassistant/components/inkbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index fcd95eadf9c..fb74d1c565a 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -28,5 +28,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/inkbird", "iot_class": "local_push", - "requirements": ["inkbird-ble==0.5.6"] + "requirements": ["inkbird-ble==0.5.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 668cbdcf592..126bdb85ee6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.6 +inkbird-ble==0.5.7 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf98bca1fd5..39b71966bcb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -951,7 +951,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.6 +inkbird-ble==0.5.7 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 From 1665cb40acc05cf4263aa473f2c7dd74c9e1bb51 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 1 Jul 2024 08:52:19 -0700 Subject: [PATCH 0648/2411] Bump gcal_sync to 6.1.4 (#120941) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index d40daa89b0e..163ad91fb7c 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.1.1"] + "requirements": ["gcal-sync==6.1.4", "oauth2client==4.1.3", "ical==8.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index de7cc9fa13f..cfc4f6f72bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,7 +924,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.1.3 +gcal-sync==6.1.4 # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a63b696617..775d3533c4c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -762,7 +762,7 @@ gardena-bluetooth==1.4.2 gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.1.3 +gcal-sync==6.1.4 # homeassistant.components.geocaching geocachingapi==0.2.1 From febd1a377203e55544c38d48906a8b4fb163a0a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Jul 2024 19:12:17 -0700 Subject: [PATCH 0649/2411] Bump inkbird-ble to 0.5.7 (#121039) changelog: https://github.com/Bluetooth-Devices/inkbird-ble/compare/v0.5.6...v0.5.7 --- homeassistant/components/inkbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index fcd95eadf9c..fb74d1c565a 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -28,5 +28,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/inkbird", "iot_class": "local_push", - "requirements": ["inkbird-ble==0.5.6"] + "requirements": ["inkbird-ble==0.5.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index cfc4f6f72bb..480fbe162d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.6 +inkbird-ble==0.5.7 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 775d3533c4c..a2cffe6a526 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -951,7 +951,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.6 +inkbird-ble==0.5.7 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 From 84204c38be7c0dd0b49a5f7216e8454b69ea7408 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Jul 2024 08:59:52 +0200 Subject: [PATCH 0650/2411] Bump version to 2024.7.0b10 --- 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 5d20a8507bf..b6800b44063 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b9" +PATCH_VERSION: Final = "0b10" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 1ebd3acf1e0..36ca9abe1b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b9" +version = "2024.7.0b10" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ac57eb761469627b06faeb9331350a6b3feeec43 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:08:40 +0200 Subject: [PATCH 0651/2411] Add monkeypatch type hints to shelly tests (#121057) * Add monkeypatch type hints to shelly tests * Improve --- .../shelly/bluetooth/test_scanner.py | 20 ++++++++++++++----- tests/components/shelly/test_switch.py | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/components/shelly/bluetooth/test_scanner.py b/tests/components/shelly/bluetooth/test_scanner.py index c7bbb5cb708..1076691a768 100644 --- a/tests/components/shelly/bluetooth/test_scanner.py +++ b/tests/components/shelly/bluetooth/test_scanner.py @@ -12,7 +12,9 @@ from homeassistant.core import HomeAssistant from .. import init_integration, inject_rpc_device_event -async def test_scanner_v1(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> None: +async def test_scanner_v1( + hass: HomeAssistant, mock_rpc_device, monkeypatch: pytest.MonkeyPatch +) -> None: """Test injecting data into the scanner v1.""" await init_integration( hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} @@ -50,7 +52,9 @@ async def test_scanner_v1(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> assert ble_device is None -async def test_scanner_v2(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> None: +async def test_scanner_v2( + hass: HomeAssistant, mock_rpc_device, monkeypatch: pytest.MonkeyPatch +) -> None: """Test injecting data into the scanner v2.""" await init_integration( hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} @@ -93,7 +97,7 @@ async def test_scanner_v2(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> async def test_scanner_ignores_non_ble_events( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, mock_rpc_device, monkeypatch: pytest.MonkeyPatch ) -> None: """Test injecting non ble data into the scanner.""" await init_integration( @@ -119,7 +123,10 @@ async def test_scanner_ignores_non_ble_events( async def test_scanner_ignores_wrong_version_and_logs( - hass: HomeAssistant, mock_rpc_device, monkeypatch, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + mock_rpc_device, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, ) -> None: """Test injecting wrong version of ble data into the scanner.""" await init_integration( @@ -152,7 +159,10 @@ async def test_scanner_ignores_wrong_version_and_logs( async def test_scanner_warns_on_corrupt_event( - hass: HomeAssistant, mock_rpc_device, monkeypatch, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + mock_rpc_device, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, ) -> None: """Test injecting garbage ble data into the scanner.""" await init_integration( diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index 637a92a7fbe..de87d11d255 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -187,7 +187,7 @@ async def test_block_device_unique_ids( async def test_block_set_state_connection_error( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, mock_block_device, monkeypatch: pytest.MonkeyPatch ) -> None: """Test block device set state connection error.""" monkeypatch.setattr( From a885bdfe76153d6538e43a294570a56387d482d0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 3 Jul 2024 14:30:51 +0200 Subject: [PATCH 0652/2411] Add conversation supported feature CONTROL (#121036) --- homeassistant/components/conversation/__init__.py | 2 ++ homeassistant/components/conversation/const.py | 8 ++++++++ .../components/conversation/default_agent.py | 3 ++- homeassistant/components/conversation/entity.py | 2 ++ .../components/conversation/test_default_agent.py | 15 ++++++++++++++- 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 6441dcab4ca..36929ac65f0 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -40,6 +40,7 @@ from .const import ( OLD_HOME_ASSISTANT_AGENT, SERVICE_PROCESS, SERVICE_RELOAD, + ConversationEntityFeature, ) from .default_agent import async_get_default_agent, async_setup_default_agent from .entity import ConversationEntity @@ -58,6 +59,7 @@ __all__ = [ "ConversationEntity", "ConversationInput", "ConversationResult", + "ConversationEntityFeature", ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/conversation/const.py b/homeassistant/components/conversation/const.py index 70a598e8b56..14b2d1d4955 100644 --- a/homeassistant/components/conversation/const.py +++ b/homeassistant/components/conversation/const.py @@ -1,5 +1,7 @@ """Const for conversation integration.""" +from enum import IntFlag + DOMAIN = "conversation" DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"} HOME_ASSISTANT_AGENT = "conversation.home_assistant" @@ -12,3 +14,9 @@ ATTR_CONVERSATION_ID = "conversation_id" SERVICE_PROCESS = "process" SERVICE_RELOAD = "reload" + + +class ConversationEntityFeature(IntFlag): + """Supported features of the conversation entity.""" + + CONTROL = 1 diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 71b14f8d299..757abd845b3 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -44,7 +44,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_added_domain from homeassistant.util.json import JsonObjectType, json_loads_object -from .const import DEFAULT_EXPOSED_ATTRIBUTES, DOMAIN +from .const import DEFAULT_EXPOSED_ATTRIBUTES, DOMAIN, ConversationEntityFeature from .entity import ConversationEntity from .models import ConversationInput, ConversationResult @@ -147,6 +147,7 @@ class DefaultAgent(ConversationEntity): """Default agent for conversation agent.""" _attr_name = "Home Assistant" + _attr_supported_features = ConversationEntityFeature.CONTROL def __init__( self, hass: core.HomeAssistant, config_intents: dict[str, Any] diff --git a/homeassistant/components/conversation/entity.py b/homeassistant/components/conversation/entity.py index 12dbea41344..d9598dee7eb 100644 --- a/homeassistant/components/conversation/entity.py +++ b/homeassistant/components/conversation/entity.py @@ -7,6 +7,7 @@ from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import dt as dt_util +from .const import ConversationEntityFeature from .models import ConversationInput, ConversationResult @@ -14,6 +15,7 @@ class ConversationEntity(RestoreEntity): """Entity that supports conversations.""" _attr_should_poll = False + _attr_supported_features = ConversationEntityFeature(0) __last_activity: str | None = None @property diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index f8a021475d5..1d1c7078e3d 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -17,7 +17,12 @@ from homeassistant.components.intent import ( TimerInfo, async_register_timer_handler, ) -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, STATE_CLOSED +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + STATE_CLOSED, + STATE_UNKNOWN, +) from homeassistant.core import DOMAIN as HASS_DOMAIN, Context, HomeAssistant, callback from homeassistant.helpers import ( area_registry as ar, @@ -173,6 +178,14 @@ async def test_conversation_agent(hass: HomeAssistant) -> None: ): assert agent.supported_languages == ["dwarvish", "elvish", "entish"] + state = hass.states.get(agent.entity_id) + assert state + assert state.state == STATE_UNKNOWN + assert ( + state.attributes["supported_features"] + == conversation.ConversationEntityFeature.CONTROL + ) + async def test_expose_flag_automatically_set( hass: HomeAssistant, From fbb98a668cfdf98204657239bca690ee53a9ee3e Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Wed, 3 Jul 2024 15:35:08 +0200 Subject: [PATCH 0653/2411] Bump here-transit to 1.2.1 (#120900) --- homeassistant/components/here_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 2d6621c7c61..0365cf51d97 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "iot_class": "cloud_polling", "loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"], - "requirements": ["here-routing==1.0.1", "here-transit==1.2.0"] + "requirements": ["here-routing==1.0.1", "here-transit==1.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 126bdb85ee6..5ef5f30ab64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1068,7 +1068,7 @@ heatmiserV3==1.1.18 here-routing==1.0.1 # homeassistant.components.here_travel_time -here-transit==1.2.0 +here-transit==1.2.1 # homeassistant.components.hikvisioncam hikvision==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39b71966bcb..a2231ed8f96 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -879,7 +879,7 @@ hdate==0.10.9 here-routing==1.0.1 # homeassistant.components.here_travel_time -here-transit==1.2.0 +here-transit==1.2.1 # homeassistant.components.hko hko==0.3.2 From 9b2233e65e7ddb91dee3eb14c66605d13d1c76d1 Mon Sep 17 00:00:00 2001 From: Anton Tolchanov <1687799+knyar@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:41:01 +0100 Subject: [PATCH 0654/2411] Generate Prometheus metrics in an executor job (#121058) --- homeassistant/components/prometheus/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 2159656f129..a0f0d69ce46 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -26,7 +26,7 @@ from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, ) -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.sensor import SensorDeviceClass @@ -729,7 +729,11 @@ class PrometheusView(HomeAssistantView): """Handle request for Prometheus metrics.""" _LOGGER.debug("Received Prometheus metrics request") + hass = request.app[KEY_HASS] + body = await hass.async_add_executor_job( + prometheus_client.generate_latest, prometheus_client.REGISTRY + ) return web.Response( - body=prometheus_client.generate_latest(prometheus_client.REGISTRY), + body=body, content_type=CONTENT_TYPE_TEXT_PLAIN, ) From e7ffd7b9ad1741d9bb926a0eadbe7b2e2b7c0dbb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Jul 2024 15:41:43 +0200 Subject: [PATCH 0655/2411] Update frontend to 20240703.0 (#121063) --- 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 0d32624cf57..525ba507121 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240702.0"] + "requirements": ["home-assistant-frontend==20240703.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 37f26b99225..a8bdb5ba255 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240702.0 +home-assistant-frontend==20240703.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5ef5f30ab64..7f200c7db62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240702.0 +home-assistant-frontend==20240703.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2231ed8f96..6d24c77130f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240702.0 +home-assistant-frontend==20240703.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From 87f7703f3c1af6992e99869d0430ba838dd8dad6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 3 Jul 2024 15:56:05 +0200 Subject: [PATCH 0656/2411] Use async_setup_recorder_instance fixture in recorder migration tests (#121050) --- tests/components/recorder/test_migrate.py | 100 ++++++++-------------- tests/conftest.py | 7 +- 2 files changed, 42 insertions(+), 65 deletions(-) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index cb8e402f65a..e75aa5588c4 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -19,7 +19,6 @@ from sqlalchemy.exc import ( from sqlalchemy.orm import Session, scoped_session, sessionmaker 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.db_schema import ( @@ -36,6 +35,14 @@ import homeassistant.util.dt as dt_util from .common import async_wait_recording_done, create_engine_test from tests.common import async_fire_time_changed +from tests.typing import RecorderInstanceGenerator + + +@pytest.fixture +async def mock_recorder_before_hass( + async_setup_recorder_instance: RecorderInstanceGenerator, +) -> None: + """Set up recorder.""" def _get_native_states(hass, entity_id): @@ -49,12 +56,13 @@ def _get_native_states(hass, entity_id): return states -async def test_schema_update_calls(recorder_db_url: str, hass: HomeAssistant) -> None: +async def test_schema_update_calls( + hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator +) -> None: """Test that schema migrations occur in correct order.""" assert recorder.util.async_migration_in_progress(hass) is False with ( - patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.core.create_engine", new=create_engine_test, @@ -64,10 +72,7 @@ async def test_schema_update_calls(recorder_db_url: str, hass: HomeAssistant) -> wraps=migration._apply_update, ) as update, ): - recorder_helper.async_initialize_recorder(hass) - await async_setup_component( - hass, "recorder", {"recorder": {"db_url": recorder_db_url}} - ) + await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) assert recorder.util.async_migration_in_progress(hass) is False @@ -82,7 +87,11 @@ async def test_schema_update_calls(recorder_db_url: str, hass: HomeAssistant) -> ) -async def test_migration_in_progress(recorder_db_url: str, hass: HomeAssistant) -> None: +async def test_migration_in_progress( + hass: HomeAssistant, + recorder_db_url: str, + async_setup_recorder_instance: RecorderInstanceGenerator, +) -> None: """Test that we can check for migration in progress.""" if recorder_db_url.startswith("mysql://"): # The database drop at the end of this test currently hangs on MySQL @@ -95,16 +104,12 @@ async def test_migration_in_progress(recorder_db_url: str, hass: HomeAssistant) assert recorder.util.async_migration_in_progress(hass) is False 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": recorder_db_url}} - ) + await async_setup_recorder_instance(hass, wait_recorder=False) 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) @@ -114,13 +119,12 @@ async def test_migration_in_progress(recorder_db_url: str, hass: HomeAssistant) async def test_database_migration_failed( - recorder_db_url: str, hass: HomeAssistant + hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator ) -> None: """Test we notify if the migration fails.""" assert recorder.util.async_migration_in_progress(hass) is False with ( - patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.core.create_engine", new=create_engine_test, @@ -138,10 +142,7 @@ async def test_database_migration_failed( side_effect=pn.dismiss, ) as mock_dismiss, ): - recorder_helper.async_initialize_recorder(hass) - await async_setup_component( - hass, "recorder", {"recorder": {"db_url": recorder_db_url}} - ) + await async_setup_recorder_instance(hass, wait_recorder=False) hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() @@ -154,7 +155,9 @@ async def test_database_migration_failed( async def test_database_migration_encounters_corruption( - recorder_db_url: str, hass: HomeAssistant + hass: HomeAssistant, + recorder_db_url: str, + async_setup_recorder_instance: RecorderInstanceGenerator, ) -> None: """Test we move away the database if its corrupt.""" if recorder_db_url.startswith(("mysql://", "postgresql://")): @@ -168,7 +171,6 @@ async def test_database_migration_encounters_corruption( sqlite3_exception.__cause__ = sqlite3.DatabaseError() with ( - patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.migration._schema_is_current", side_effect=[False], @@ -180,14 +182,8 @@ async def test_database_migration_encounters_corruption( patch( "homeassistant.components.recorder.core.move_away_broken_database" ) as move_away, - patch( - "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", - ), ): - recorder_helper.async_initialize_recorder(hass) - await async_setup_component( - hass, "recorder", {"recorder": {"db_url": recorder_db_url}} - ) + await async_setup_recorder_instance(hass) hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await async_wait_recording_done(hass) @@ -197,13 +193,12 @@ async def test_database_migration_encounters_corruption( async def test_database_migration_encounters_corruption_not_sqlite( - recorder_db_url: str, hass: HomeAssistant + hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator ) -> None: """Test we fail on database error when we cannot recover.""" assert recorder.util.async_migration_in_progress(hass) is False with ( - patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.migration._schema_is_current", side_effect=[False], @@ -224,10 +219,7 @@ async def test_database_migration_encounters_corruption_not_sqlite( side_effect=pn.dismiss, ) as mock_dismiss, ): - recorder_helper.async_initialize_recorder(hass) - await async_setup_component( - hass, "recorder", {"recorder": {"db_url": recorder_db_url}} - ) + await async_setup_recorder_instance(hass, wait_recorder=False) hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() @@ -241,28 +233,19 @@ async def test_database_migration_encounters_corruption_not_sqlite( async def test_events_during_migration_are_queued( - recorder_db_url: str, hass: HomeAssistant + hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator ) -> None: """Test that events during migration are queued.""" assert recorder.util.async_migration_in_progress(hass) is False 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": recorder_db_url, "commit_interval": 0}}, - ) + await async_setup_recorder_instance(hass, {"commit_interval": 0}) hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() @@ -280,14 +263,13 @@ async def test_events_during_migration_are_queued( async def test_events_during_migration_queue_exhausted( - recorder_db_url: str, hass: HomeAssistant + hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator ) -> None: """Test that events during migration takes so long the queue is exhausted.""" assert recorder.util.async_migration_in_progress(hass) is False with ( - patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.core.create_engine", new=create_engine_test, @@ -295,11 +277,8 @@ async def test_events_during_migration_queue_exhausted( patch.object(recorder.core, "MAX_QUEUE_BACKLOG_MIN_VALUE", 1), patch.object(recorder.core, "QUEUE_PERCENTAGE_ALLOWED_AVAILABLE_MEMORY", 0), ): - recorder_helper.async_initialize_recorder(hass) - await async_setup_component( - hass, - "recorder", - {"recorder": {"db_url": recorder_db_url, "commit_interval": 0}}, + await async_setup_recorder_instance( + hass, {"commit_interval": 0}, wait_recorder=False ) hass.states.async_set("my.entity", "on", {}) await hass.async_block_till_done() @@ -329,7 +308,11 @@ async def test_events_during_migration_queue_exhausted( [(0, True), (16, True), (18, True), (22, True), (25, True)], ) async def test_schema_migrate( - recorder_db_url: str, hass: HomeAssistant, start_version, live + hass: HomeAssistant, + recorder_db_url: str, + async_setup_recorder_instance: RecorderInstanceGenerator, + start_version, + live, ) -> None: """Test the full schema migration logic. @@ -408,7 +391,6 @@ async def test_schema_migrate( real_create_index(*args) with ( - patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.core.create_engine", new=_create_engine_test, @@ -431,9 +413,6 @@ async def test_schema_migrate( "homeassistant.components.recorder.migration._create_index", wraps=_sometimes_failing_create_index, ), - patch( - "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", - ), patch( "homeassistant.components.recorder.Recorder._process_state_changed_event_into_session", ), @@ -444,12 +423,7 @@ async def test_schema_migrate( "homeassistant.components.recorder.Recorder._pre_process_startup_events", ), ): - recorder_helper.async_initialize_recorder(hass) - hass.async_create_task( - async_setup_component( - hass, "recorder", {"recorder": {"db_url": recorder_db_url}} - ) - ) + await async_setup_recorder_instance(hass, wait_recorder=False) await recorder_helper.async_wait_recorder(hass) assert recorder.util.async_migration_in_progress(hass) is True diff --git a/tests/conftest.py b/tests/conftest.py index 3cef2dd0279..50258db8f3b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1478,14 +1478,17 @@ async def async_setup_recorder_instance( ): async def async_setup_recorder( - hass: HomeAssistant, config: ConfigType | None = None + hass: HomeAssistant, + config: ConfigType | None = None, + *, + wait_recorder: bool = True, ) -> recorder.Recorder: """Setup and return recorder instance.""" # noqa: D401 await _async_init_recorder_component(hass, config, recorder_db_url) await hass.async_block_till_done() instance = hass.data[recorder.DATA_INSTANCE] # The recorder's worker is not started until Home Assistant is running - if hass.state is CoreState.running: + if hass.state is CoreState.running and wait_recorder: await async_recorder_block_till_done(hass) return instance From 8709c668cc6a61974fbe1295347f9c88348b7ebd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:57:02 +0200 Subject: [PATCH 0657/2411] Remove unused diagnostics fixtures (#121066) --- tests/components/google/test_diagnostics.py | 7 ------- tests/components/local_calendar/test_diagnostics.py | 7 ------- 2 files changed, 14 deletions(-) diff --git a/tests/components/google/test_diagnostics.py b/tests/components/google/test_diagnostics.py index 5d6259309b8..78eb6d7ceea 100644 --- a/tests/components/google/test_diagnostics.py +++ b/tests/components/google/test_diagnostics.py @@ -11,7 +11,6 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.auth.models import Credentials from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .conftest import TEST_EVENT, ApiResult, ComponentSetup @@ -55,12 +54,6 @@ def _get_test_client_generator( return auth_client -@pytest.fixture(autouse=True) -async def setup_diag(hass): - """Set up diagnostics platform.""" - assert await async_setup_component(hass, "diagnostics", {}) - - @freeze_time("2023-03-13 12:05:00-07:00") @pytest.mark.usefixtures("socket_enabled") async def test_diagnostics( diff --git a/tests/components/local_calendar/test_diagnostics.py b/tests/components/local_calendar/test_diagnostics.py index ed12391f8a9..30c857dad98 100644 --- a/tests/components/local_calendar/test_diagnostics.py +++ b/tests/components/local_calendar/test_diagnostics.py @@ -7,7 +7,6 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.auth.models import Credentials from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY, Client @@ -41,12 +40,6 @@ def _get_test_client_generator( return auth_client -@pytest.fixture(autouse=True) -async def setup_diag(hass): - """Set up diagnostics platform.""" - assert await async_setup_component(hass, "diagnostics", {}) - - @freeze_time("2023-03-13 12:05:00-07:00") @pytest.mark.usefixtures("socket_enabled") async def test_empty_calendar( From 1332e39f9e1a6b342229a91b5f97750b44c2245a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:57:32 +0200 Subject: [PATCH 0658/2411] Cleanup deprecated json utils (#121069) * Cleanup deprectated json utils * Adjust pylint --- homeassistant/util/json.py | 58 ---------------------------------- pylint/plugins/hass_imports.py | 6 ---- tests/util/test_json.py | 28 ---------------- 3 files changed, 92 deletions(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 1479550b615..fa67f6b1dcc 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -2,8 +2,6 @@ from __future__ import annotations -from collections.abc import Callable -import json import logging from os import PathLike from typing import Any @@ -12,8 +10,6 @@ import orjson from homeassistant.exceptions import HomeAssistantError -from .file import WriteError # noqa: F401 - _SENTINEL = object() _LOGGER = logging.getLogger(__name__) @@ -129,63 +125,9 @@ def load_json_object( raise HomeAssistantError(f"Expected JSON to be parsed as a dict got {type(value)}") -def save_json( - filename: str, - data: list | dict, - private: bool = False, - *, - encoder: type[json.JSONEncoder] | None = None, - atomic_writes: bool = False, -) -> None: - """Save JSON data to a file.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.helpers.frame import report - - report( - ( - "uses save_json from homeassistant.util.json module." - " This is deprecated and will stop working in Home Assistant 2022.4, it" - " should be updated to use homeassistant.helpers.json module instead" - ), - error_if_core=False, - ) - - # pylint: disable-next=import-outside-toplevel - import homeassistant.helpers.json as json_helper - - json_helper.save_json( - filename, data, private, encoder=encoder, atomic_writes=atomic_writes - ) - - def format_unserializable_data(data: dict[str, Any]) -> str: """Format output of find_paths in a friendly way. Format is comma separated: =() """ return ", ".join(f"{path}={value}({type(value)}" for path, value in data.items()) - - -def find_paths_unserializable_data( - bad_data: Any, *, dump: Callable[[Any], str] = json.dumps -) -> dict[str, Any]: - """Find the paths to unserializable data. - - This method is slow! Only use for error handling. - """ - # pylint: disable-next=import-outside-toplevel - from homeassistant.helpers.frame import report - - report( - ( - "uses find_paths_unserializable_data from homeassistant.util.json module." - " This is deprecated and will stop working in Home Assistant 2022.4, it" - " should be updated to use homeassistant.helpers.json module instead" - ), - error_if_core=False, - ) - - # pylint: disable-next=import-outside-toplevel - import homeassistant.helpers.json as json_helper - - return json_helper.find_paths_unserializable_data(bad_data, dump=dump) diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index 3ec8b6c3cd9..57b71560b53 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -392,12 +392,6 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { constant=re.compile(r"^IMPERIAL_SYSTEM$"), ), ], - "homeassistant.util.json": [ - ObsoleteImportMatch( - reason="moved to homeassistant.helpers.json", - constant=re.compile(r"^save_json|find_paths_unserializable_data$"), - ), - ], } diff --git a/tests/util/test_json.py b/tests/util/test_json.py index 3a314bb5a1b..05dab46002d 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -131,34 +131,6 @@ def test_json_loads_object() -> None: json_loads_object("null") -async def test_deprecated_test_find_unserializable_data( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test deprecated test_find_unserializable_data logs a warning.""" - # pylint: disable-next=hass-deprecated-import,import-outside-toplevel - from homeassistant.util.json import find_paths_unserializable_data - - find_paths_unserializable_data(1) - assert ( - "uses find_paths_unserializable_data from homeassistant.util.json" - in caplog.text - ) - assert "should be updated to use homeassistant.helpers.json module" in caplog.text - - -async def test_deprecated_save_json( - caplog: pytest.LogCaptureFixture, tmp_path: Path -) -> None: - """Test deprecated save_json logs a warning.""" - # pylint: disable-next=hass-deprecated-import,import-outside-toplevel - from homeassistant.util.json import save_json - - fname = tmp_path / "test1.json" - save_json(fname, TEST_JSON_A) - assert "uses save_json from homeassistant.util.json" in caplog.text - assert "should be updated to use homeassistant.helpers.json module" in caplog.text - - async def test_loading_derived_class() -> None: """Test loading data from classes derived from str.""" From f284aa41ebacaca7fc73a2a040dba60c8ca5241f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 3 Jul 2024 16:16:13 +0200 Subject: [PATCH 0659/2411] Bump axis to v62 (#121070) --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 2f057f96286..e028736f4ca 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -30,7 +30,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==61"], + "requirements": ["axis==62"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index 7f200c7db62..c4aa01aa869 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -520,7 +520,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==61 +axis==62 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d24c77130f..e0f7a6ef1f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -460,7 +460,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==61 +axis==62 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 From 976cb434c95d5aa3e586353b1f58fefcc8220faf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 3 Jul 2024 16:19:46 +0200 Subject: [PATCH 0660/2411] Add CONTROL supported feature to OpenAI conversation entities (#121064) Add CONTROL supported feature to OpenAI --- .../openai_conversation/conversation.py | 4 +++ .../openai_conversation/test_conversation.py | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 46be803bcad..cd45381fee6 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -92,6 +92,10 @@ class OpenAIConversationEntity( model="ChatGPT", entry_type=dr.DeviceEntryType.SERVICE, ) + if self.entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) @property def supported_languages(self) -> list[str] | Literal["*"]: diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index 1008482847c..fee1543a0d7 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -27,6 +27,33 @@ from homeassistant.util import ulid from tests.common import MockConfigEntry +async def test_entity( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, +) -> None: + """Test entity properties.""" + state = hass.states.get("conversation.openai") + assert state + assert state.attributes["supported_features"] == 0 + + hass.config_entries.async_update_entry( + mock_config_entry, + options={ + **mock_config_entry.options, + CONF_LLM_HASS_API: "assist", + }, + ) + await hass.config_entries.async_reload(mock_config_entry.entry_id) + + state = hass.states.get("conversation.openai") + assert state + assert ( + state.attributes["supported_features"] + == conversation.ConversationEntityFeature.CONTROL + ) + + async def test_error_handling( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component ) -> None: From e26b4554e6e90434c18cc6c77ee48446d0f1ba22 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 3 Jul 2024 16:22:21 +0200 Subject: [PATCH 0661/2411] Improve logic when retrying establishing database connection (#121047) --- homeassistant/components/recorder/core.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 4e5ac04c3bf..9715d9e9f10 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -958,7 +958,9 @@ class Recorder(threading.Thread): self.db_retry_wait, ) tries += 1 - time.sleep(self.db_retry_wait) + + if tries <= self.db_max_retries: + time.sleep(self.db_retry_wait) return False From 355c7399d700ea3247bc19ea69191f36f84d3617 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 3 Jul 2024 16:27:45 +0200 Subject: [PATCH 0662/2411] Bump python-matter-server to 6.2.2 (#121072) --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 8c88fcc8be2..1dac5ef0cb2 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,6 +6,6 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==6.2.0b1"], + "requirements": ["python-matter-server==6.2.2"], "zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index c4aa01aa869..f49efb7e419 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2281,7 +2281,7 @@ python-kasa[speedups]==0.7.0.2 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==6.2.0b1 +python-matter-server==6.2.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0f7a6ef1f7..d47427e90ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1778,7 +1778,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.0.2 # homeassistant.components.matter -python-matter-server==6.2.0b1 +python-matter-server==6.2.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From 0b76d5c9ca35abb493dd683dfc6c47907c6103f2 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 3 Jul 2024 09:55:21 -0500 Subject: [PATCH 0663/2411] Bump intents to 2024.7.3 (#121076) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 2302d03bf4c..6eeb461d79d 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.26"] + "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.7.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a8bdb5ba255..efd41e39775 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240703.0 -home-assistant-intents==2024.6.26 +home-assistant-intents==2024.7.3 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index f49efb7e419..5e6a94d827d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ holidays==0.51 home-assistant-frontend==20240703.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.26 +home-assistant-intents==2024.7.3 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d47427e90ba..8809b4e2414 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -898,7 +898,7 @@ holidays==0.51 home-assistant-frontend==20240703.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.26 +home-assistant-intents==2024.7.3 # homeassistant.components.home_connect homeconnect==0.7.2 From 46a488d87112a282783198b64d855830247f80db Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 3 Jul 2024 17:04:39 +0200 Subject: [PATCH 0664/2411] Use async_setup_recorder_instance fixture in recorder auto_repairs tests (#121077) --- .../statistics/test_duplicates.py | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/tests/components/recorder/auto_repairs/statistics/test_duplicates.py b/tests/components/recorder/auto_repairs/statistics/test_duplicates.py index 175cb6ecd1a..5ed86698c58 100644 --- a/tests/components/recorder/auto_repairs/statistics/test_duplicates.py +++ b/tests/components/recorder/auto_repairs/statistics/test_duplicates.py @@ -19,8 +19,6 @@ from homeassistant.components.recorder.const import SQLITE_URL_PREFIX from homeassistant.components.recorder.statistics import async_add_external_statistics from homeassistant.components.recorder.util import session_scope from homeassistant.core import HomeAssistant -from homeassistant.helpers import recorder as recorder_helper -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from ...common import async_wait_recording_done @@ -141,7 +139,9 @@ def _create_engine_28(*args, **kwargs): async def test_delete_metadata_duplicates( - caplog: pytest.LogCaptureFixture, tmp_path: Path + async_setup_recorder_instance: RecorderInstanceGenerator, + caplog: pytest.LogCaptureFixture, + tmp_path: Path, ) -> None: """Test removal of duplicated statistics.""" test_dir = tmp_path.joinpath("sqlite") @@ -206,10 +206,7 @@ async def test_delete_metadata_duplicates( ), ): async with async_test_home_assistant() as hass: - recorder_helper.async_initialize_recorder(hass) - await async_setup_component( - hass, "recorder", {"recorder": {"db_url": dburl}} - ) + await async_setup_recorder_instance(hass, {"db_url": dburl}) await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -229,8 +226,7 @@ async def test_delete_metadata_duplicates( # Test that the duplicates are removed during migration from schema 28 async with async_test_home_assistant() as hass: - recorder_helper.async_initialize_recorder(hass) - await async_setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + await async_setup_recorder_instance(hass, {"db_url": dburl}) await hass.async_start() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -248,7 +244,9 @@ async def test_delete_metadata_duplicates( async def test_delete_metadata_duplicates_many( - caplog: pytest.LogCaptureFixture, tmp_path: Path + async_setup_recorder_instance: RecorderInstanceGenerator, + caplog: pytest.LogCaptureFixture, + tmp_path: Path, ) -> None: """Test removal of duplicated statistics.""" test_dir = tmp_path.joinpath("sqlite") @@ -325,10 +323,7 @@ async def test_delete_metadata_duplicates_many( ), ): async with async_test_home_assistant() as hass: - recorder_helper.async_initialize_recorder(hass) - await async_setup_component( - hass, "recorder", {"recorder": {"db_url": dburl}} - ) + await async_setup_recorder_instance(hass, {"db_url": dburl}) await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -339,8 +334,7 @@ async def test_delete_metadata_duplicates_many( # Test that the duplicates are removed during migration from schema 28 async with async_test_home_assistant() as hass: - recorder_helper.async_initialize_recorder(hass) - await async_setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + await async_setup_recorder_instance(hass, {"db_url": dburl}) await hass.async_start() await async_wait_recording_done(hass) await async_wait_recording_done(hass) From 1a715d7b8951ac0cd343e0715e905df5f9da3b14 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 3 Jul 2024 17:11:09 +0200 Subject: [PATCH 0665/2411] Bump deebot-client to 8.1.0 (#121078) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index c042027baa8..03f99725a6d 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==8.0.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5e6a94d827d..329a389f483 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -709,7 +709,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==8.0.0 +deebot-client==8.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8809b4e2414..c8c2ea999d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -590,7 +590,7 @@ dbus-fast==2.22.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==8.0.0 +deebot-client==8.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 7a9792c1115dfd6cf59b4a76a36d420da435a635 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 3 Jul 2024 17:16:51 +0200 Subject: [PATCH 0666/2411] Matter fix Energy sensor discovery schemas (#121080) --- homeassistant/components/matter/sensor.py | 98 +++++++++++++++++++++-- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index d91d4d33471..9c19be7ee08 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -6,7 +6,11 @@ from dataclasses import dataclass from chip.clusters import Objects as clusters from chip.clusters.Types import Nullable, NullValue -from matter_server.common.custom_clusters import EveCluster +from matter_server.common.custom_clusters import ( + EveCluster, + NeoCluster, + ThirdRealityMeteringCluster, +) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -171,9 +175,6 @@ DISCOVERY_SCHEMAS = [ ), entity_class=MatterSensor, required_attributes=(EveCluster.Attributes.Watt,), - # Add OnOff Attribute as optional attribute to poll - # the primary value when the relay is toggled - optional_attributes=(clusters.OnOff.Attributes.OnOff,), ), MatterDiscoverySchema( platform=Platform.SENSOR, @@ -213,9 +214,6 @@ DISCOVERY_SCHEMAS = [ ), entity_class=MatterSensor, required_attributes=(EveCluster.Attributes.Current,), - # Add OnOff Attribute as optional attribute to poll - # the primary value when the relay is toggled - optional_attributes=(clusters.OnOff.Attributes.OnOff,), ), MatterDiscoverySchema( platform=Platform.SENSOR, @@ -364,4 +362,90 @@ DISCOVERY_SCHEMAS = [ clusters.ActivatedCarbonFilterMonitoring.Attributes.Condition, ), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ThirdRealityEnergySensorWatt", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=2, + state_class=SensorStateClass.MEASUREMENT, + measurement_to_ha=lambda x: x / 1000, + ), + entity_class=MatterSensor, + required_attributes=( + ThirdRealityMeteringCluster.Attributes.InstantaneousDemand, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ThirdRealityEnergySensorWattAccumulated", + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=3, + state_class=SensorStateClass.TOTAL_INCREASING, + measurement_to_ha=lambda x: x / 1000, + ), + entity_class=MatterSensor, + required_attributes=( + ThirdRealityMeteringCluster.Attributes.CurrentSummationDelivered, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorWatt", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=2, + state_class=SensorStateClass.MEASUREMENT, + measurement_to_ha=lambda x: x / 10, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.Watt,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorWattAccumulated", + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_display_precision=1, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.WattAccumulated,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorVoltage", + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + suggested_display_precision=0, + state_class=SensorStateClass.MEASUREMENT, + measurement_to_ha=lambda x: x / 10, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.Voltage,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorWattCurrent", + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE, + suggested_display_precision=0, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.Current,), + ), ] From bc363c385fb487415ff1bad36e3e297c95984684 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 3 Jul 2024 17:17:37 +0200 Subject: [PATCH 0667/2411] Fix async knocki function (#121048) --- homeassistant/components/knocki/coordinator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knocki/coordinator.py b/homeassistant/components/knocki/coordinator.py index f70fbdf79a7..c1e32b817e1 100644 --- a/homeassistant/components/knocki/coordinator.py +++ b/homeassistant/components/knocki/coordinator.py @@ -3,7 +3,7 @@ from knocki import Event, KnockiClient, KnockiConnectionError, Trigger from homeassistant.components.event import DOMAIN as EVENT_DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -33,7 +33,7 @@ class KnockiCoordinator(DataUpdateCoordinator[dict[int, Trigger]]): } removed_triggers = self._known_triggers - current_triggers for trigger in removed_triggers: - await self._delete_device(trigger) + self._async_delete_device(trigger) self._known_triggers = current_triggers return {trigger.details.trigger_id: trigger for trigger in triggers} @@ -46,7 +46,8 @@ class KnockiCoordinator(DataUpdateCoordinator[dict[int, Trigger]]): (event.payload.device_id, event.payload.details.trigger_id) ) - async def _delete_device(self, trigger: tuple[str, int]) -> None: + @callback + def _async_delete_device(self, trigger: tuple[str, int]) -> None: """Delete a device from the coordinator.""" device_id, trigger_id = trigger entity_registry = er.async_get(self.hass) From e4a0a21b6756fc0be5599325c50eb2034aca2525 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 08:18:46 -0700 Subject: [PATCH 0668/2411] Bump uiprotect to 5.2.0 (#121079) --- 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 26b1f424db1..95fb8600135 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==5.0.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==5.2.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 329a389f483..1a7a1f3bbdf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2789,7 +2789,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==5.0.0 +uiprotect==5.2.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8c2ea999d8..73a85e77973 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2169,7 +2169,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==5.0.0 +uiprotect==5.2.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From c4956b66b0da95811cdd2b493ea836c74a9f55a3 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 1 Jul 2024 00:23:42 +0200 Subject: [PATCH 0669/2411] Bump here-routing to 1.0.1 (#120877) --- homeassistant/components/here_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 19c5c4d73d9..2d6621c7c61 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "iot_class": "cloud_polling", "loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"], - "requirements": ["here-routing==0.2.0", "here-transit==1.2.0"] + "requirements": ["here-routing==1.0.1", "here-transit==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 480fbe162d6..a04485044d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1065,7 +1065,7 @@ hdate==0.10.9 heatmiserV3==1.1.18 # homeassistant.components.here_travel_time -here-routing==0.2.0 +here-routing==1.0.1 # homeassistant.components.here_travel_time here-transit==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2cffe6a526..2a93dd5d739 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hassil==1.7.1 hdate==0.10.9 # homeassistant.components.here_travel_time -here-routing==0.2.0 +here-routing==1.0.1 # homeassistant.components.here_travel_time here-transit==1.2.0 From 16827ea09e1942f175ff5616f76bd10fe84ea0c3 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Wed, 3 Jul 2024 15:35:08 +0200 Subject: [PATCH 0670/2411] Bump here-transit to 1.2.1 (#120900) --- homeassistant/components/here_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 2d6621c7c61..0365cf51d97 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "iot_class": "cloud_polling", "loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"], - "requirements": ["here-routing==1.0.1", "here-transit==1.2.0"] + "requirements": ["here-routing==1.0.1", "here-transit==1.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a04485044d5..1a6fd81ecaa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1068,7 +1068,7 @@ heatmiserV3==1.1.18 here-routing==1.0.1 # homeassistant.components.here_travel_time -here-transit==1.2.0 +here-transit==1.2.1 # homeassistant.components.hikvisioncam hikvision==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a93dd5d739..b4042f70640 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -879,7 +879,7 @@ hdate==0.10.9 here-routing==1.0.1 # homeassistant.components.here_travel_time -here-transit==1.2.0 +here-transit==1.2.1 # homeassistant.components.hko hko==0.3.2 From 36e74cd9a6afae0b3d7733b449babf4486a47c1d Mon Sep 17 00:00:00 2001 From: Anton Tolchanov <1687799+knyar@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:41:01 +0100 Subject: [PATCH 0671/2411] Generate Prometheus metrics in an executor job (#121058) --- homeassistant/components/prometheus/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 2159656f129..a0f0d69ce46 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -26,7 +26,7 @@ from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, ) -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.sensor import SensorDeviceClass @@ -729,7 +729,11 @@ class PrometheusView(HomeAssistantView): """Handle request for Prometheus metrics.""" _LOGGER.debug("Received Prometheus metrics request") + hass = request.app[KEY_HASS] + body = await hass.async_add_executor_job( + prometheus_client.generate_latest, prometheus_client.REGISTRY + ) return web.Response( - body=prometheus_client.generate_latest(prometheus_client.REGISTRY), + body=body, content_type=CONTENT_TYPE_TEXT_PLAIN, ) From 6621cf475a7b66f5e5a377bbbc09e5ebe1363734 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Jul 2024 15:41:43 +0200 Subject: [PATCH 0672/2411] Update frontend to 20240703.0 (#121063) --- 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 0d32624cf57..525ba507121 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240702.0"] + "requirements": ["home-assistant-frontend==20240703.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3ffa9d92f63..04dc11f0183 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240702.0 +home-assistant-frontend==20240703.0 home-assistant-intents==2024.6.26 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1a6fd81ecaa..ee0a07a2148 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240702.0 +home-assistant-frontend==20240703.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4042f70640..0033bda8949 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 holidays==0.51 # homeassistant.components.frontend -home-assistant-frontend==20240702.0 +home-assistant-frontend==20240703.0 # homeassistant.components.conversation home-assistant-intents==2024.6.26 From 13631250b460f138ef4d78eae1004378c3668b5f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 3 Jul 2024 16:16:13 +0200 Subject: [PATCH 0673/2411] Bump axis to v62 (#121070) --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 2f057f96286..e028736f4ca 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -30,7 +30,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==61"], + "requirements": ["axis==62"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index ee0a07a2148..dbc5f480b0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -520,7 +520,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==61 +axis==62 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0033bda8949..6920608e656 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -460,7 +460,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==61 +axis==62 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 From c89a9b5ce04967f73674f12a3f63637c8371836e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 3 Jul 2024 16:27:45 +0200 Subject: [PATCH 0674/2411] Bump python-matter-server to 6.2.2 (#121072) --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 8c88fcc8be2..1dac5ef0cb2 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,6 +6,6 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==6.2.0b1"], + "requirements": ["python-matter-server==6.2.2"], "zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index dbc5f480b0f..092536e3012 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2281,7 +2281,7 @@ python-kasa[speedups]==0.7.0.2 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==6.2.0b1 +python-matter-server==6.2.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6920608e656..cf29ec479a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1778,7 +1778,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.0.2 # homeassistant.components.matter -python-matter-server==6.2.0b1 +python-matter-server==6.2.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From e8bcb3e11eb7b212db4f76827da7418bafbf650b Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 3 Jul 2024 09:55:21 -0500 Subject: [PATCH 0675/2411] Bump intents to 2024.7.3 (#121076) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 2302d03bf4c..6eeb461d79d 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.26"] + "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.7.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 04dc11f0183..6058a781e2a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240703.0 -home-assistant-intents==2024.6.26 +home-assistant-intents==2024.7.3 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index 092536e3012..3a8edbba7a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ holidays==0.51 home-assistant-frontend==20240703.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.26 +home-assistant-intents==2024.7.3 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf29ec479a7..a56a072d62a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -898,7 +898,7 @@ holidays==0.51 home-assistant-frontend==20240703.0 # homeassistant.components.conversation -home-assistant-intents==2024.6.26 +home-assistant-intents==2024.7.3 # homeassistant.components.home_connect homeconnect==0.7.2 From 547b24ce583f91a223ea7b44e00dce43b6eb5c79 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 3 Jul 2024 17:11:09 +0200 Subject: [PATCH 0676/2411] Bump deebot-client to 8.1.0 (#121078) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index d14291576ff..9568bf2c3ac 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==8.0.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3a8edbba7a2..5fd773147dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -709,7 +709,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==8.0.0 +deebot-client==8.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a56a072d62a..d5c4945da95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -590,7 +590,7 @@ dbus-fast==2.22.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==8.0.0 +deebot-client==8.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 85168239cdcfa8f3fcb04812f30e495c99ce3ff8 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 3 Jul 2024 17:16:51 +0200 Subject: [PATCH 0677/2411] Matter fix Energy sensor discovery schemas (#121080) --- homeassistant/components/matter/sensor.py | 98 +++++++++++++++++++++-- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index d91d4d33471..9c19be7ee08 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -6,7 +6,11 @@ from dataclasses import dataclass from chip.clusters import Objects as clusters from chip.clusters.Types import Nullable, NullValue -from matter_server.common.custom_clusters import EveCluster +from matter_server.common.custom_clusters import ( + EveCluster, + NeoCluster, + ThirdRealityMeteringCluster, +) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -171,9 +175,6 @@ DISCOVERY_SCHEMAS = [ ), entity_class=MatterSensor, required_attributes=(EveCluster.Attributes.Watt,), - # Add OnOff Attribute as optional attribute to poll - # the primary value when the relay is toggled - optional_attributes=(clusters.OnOff.Attributes.OnOff,), ), MatterDiscoverySchema( platform=Platform.SENSOR, @@ -213,9 +214,6 @@ DISCOVERY_SCHEMAS = [ ), entity_class=MatterSensor, required_attributes=(EveCluster.Attributes.Current,), - # Add OnOff Attribute as optional attribute to poll - # the primary value when the relay is toggled - optional_attributes=(clusters.OnOff.Attributes.OnOff,), ), MatterDiscoverySchema( platform=Platform.SENSOR, @@ -364,4 +362,90 @@ DISCOVERY_SCHEMAS = [ clusters.ActivatedCarbonFilterMonitoring.Attributes.Condition, ), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ThirdRealityEnergySensorWatt", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=2, + state_class=SensorStateClass.MEASUREMENT, + measurement_to_ha=lambda x: x / 1000, + ), + entity_class=MatterSensor, + required_attributes=( + ThirdRealityMeteringCluster.Attributes.InstantaneousDemand, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ThirdRealityEnergySensorWattAccumulated", + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=3, + state_class=SensorStateClass.TOTAL_INCREASING, + measurement_to_ha=lambda x: x / 1000, + ), + entity_class=MatterSensor, + required_attributes=( + ThirdRealityMeteringCluster.Attributes.CurrentSummationDelivered, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorWatt", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=2, + state_class=SensorStateClass.MEASUREMENT, + measurement_to_ha=lambda x: x / 10, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.Watt,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorWattAccumulated", + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_display_precision=1, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.WattAccumulated,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorVoltage", + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + suggested_display_precision=0, + state_class=SensorStateClass.MEASUREMENT, + measurement_to_ha=lambda x: x / 10, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.Voltage,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="NeoEnergySensorWattCurrent", + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE, + suggested_display_precision=0, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=(NeoCluster.Attributes.Current,), + ), ] From d94b36cfbbf486f3d873cc55aeb9b8411bdbb2be Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Jul 2024 17:29:08 +0200 Subject: [PATCH 0678/2411] Bump version to 2024.7.0b11 --- 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 b6800b44063..c76db961e8b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b10" +PATCH_VERSION: Final = "0b11" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 36ca9abe1b5..198d3438fc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b10" +version = "2024.7.0b11" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 44c89e6c3b1f0057f908d880db863022e529f039 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 08:45:19 -0700 Subject: [PATCH 0679/2411] Cleanup v32 recorder migration test (#121083) --- .../components/recorder/test_v32_migration.py | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index e3398fbf0e3..b71bc4cefb8 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -320,27 +320,6 @@ async def test_migrate_can_resume_entity_id_post_migration( assert "ix_states_event_id" in states_index_names assert "ix_states_entity_id_last_updated_ts" in states_index_names - with patch("homeassistant.components.recorder.Recorder._post_migrate_entity_ids"): - async with async_test_home_assistant() as hass: - recorder_helper.async_initialize_recorder(hass) - assert await async_setup_component( - hass, "recorder", {"recorder": {"db_url": dburl}} - ) - await hass.async_block_till_done() - - # We need to wait for all the migration tasks to complete - # before we can check the database. - for _ in range(number_of_migrations): - await recorder.get_instance(hass).async_block_till_done() - await async_wait_recording_done(hass) - - states_indexes = await recorder.get_instance(hass).async_add_executor_job( - _get_states_index_names - ) - states_index_names = {index["name"] for index in states_indexes} - await hass.async_stop() - await hass.async_block_till_done() - async with async_test_home_assistant() as hass: recorder_helper.async_initialize_recorder(hass) assert await async_setup_component( From 1080a4ef1e99a5f567f4a2b5aca058b84acf7352 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Jul 2024 17:55:58 +0200 Subject: [PATCH 0680/2411] Bump version to 2024.7.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c76db961e8b..d5c64823890 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b11" +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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 198d3438fc5..777ec8bb6a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.0b11" +version = "2024.7.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 48172b042656d2b73afe545cd739c552418f7944 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 10:16:41 -0700 Subject: [PATCH 0681/2411] Small speed up to writing entity state (#121043) --- homeassistant/core.py | 55 ++++++++++++++++++++++++--------- homeassistant/helpers/entity.py | 2 +- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index cf5373ad8c2..1714cff216d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -2243,16 +2243,45 @@ class StateMachine: This method must be run in the event loop. """ - new_state = str(new_state) - attributes = attributes or {} - old_state = self._states_data.get(entity_id) - if old_state is None: - # If the state is missing, try to convert the entity_id to lowercase - # and try again. - entity_id = entity_id.lower() - old_state = self._states_data.get(entity_id) + self.async_set_internal( + entity_id.lower(), + str(new_state), + attributes or {}, + force_update, + context, + state_info, + timestamp or time.time(), + ) - if old_state is None: + @callback + def async_set_internal( + self, + entity_id: str, + new_state: str, + attributes: Mapping[str, Any] | None, + force_update: bool, + context: Context | None, + state_info: StateInfo | None, + timestamp: float, + ) -> None: + """Set the state of an entity, add entity if it does not exist. + + This method is intended to only be used by core internally + and should not be considered a stable API. We will make + breaking changes to this function in the future and it + should not be used in integrations. + + This method must be run in the event loop. + """ + # Most cases the key will be in the dict + # so we optimize for the happy path as + # python 3.11+ has near zero overhead for + # try when it does not raise an exception. + old_state: State | None + try: + old_state = self._states_data[entity_id] + except KeyError: + old_state = None same_state = False same_attr = False last_changed = None @@ -2272,10 +2301,11 @@ class StateMachine: # timestamp implementation: # https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L6387 # https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L6323 - if timestamp is None: - timestamp = time.time() now = dt_util.utc_from_timestamp(timestamp) + if context is None: + context = Context(id=ulid_at_time(timestamp)) + if same_state and same_attr: # mypy does not understand this is only possible if old_state is not None old_last_reported = old_state.last_reported # type: ignore[union-attr] @@ -2294,9 +2324,6 @@ class StateMachine: ) return - if context is None: - context = Context(id=ulid_at_time(timestamp)) - if same_attr: if TYPE_CHECKING: assert old_state is not None diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index cf910a5cba8..f5b93263692 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1202,7 +1202,7 @@ class Entity( self._context_set = None try: - hass.states.async_set( + hass.states.async_set_internal( entity_id, state, attr, From 61f1c8d9631bab1a4236eee405c5b0b03e8be22e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 3 Jul 2024 19:17:52 +0200 Subject: [PATCH 0682/2411] Fix leak of SQLAlchemy engine objects in recorder (#121085) --- homeassistant/components/recorder/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 9715d9e9f10..98a5aea4a57 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -960,6 +960,7 @@ class Recorder(threading.Thread): tries += 1 if tries <= self.db_max_retries: + self._close_connection() time.sleep(self.db_retry_wait) return False @@ -1452,6 +1453,7 @@ class Recorder(threading.Thread): if self._using_file_sqlite: validate_or_move_away_sqlite_database(self.db_url) + assert not self.engine self.engine = create_engine(self.db_url, **kwargs, future=True) self._dialect_name = try_parse_enum(SupportedDialect, self.engine.dialect.name) self.__dict__.pop("dialect_name", None) From 5029da69198b37245134e0c8b9916b208c07292a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 3 Jul 2024 21:05:34 +0200 Subject: [PATCH 0683/2411] Make the async_setup_recorder_instance fixture a context manager (#121086) --- pylint/plugins/hass_enforce_type_hints.py | 2 +- tests/components/history/conftest.py | 2 +- .../statistics/test_duplicates.py | 2 +- .../recorder/test_entity_registry.py | 2 +- tests/components/recorder/test_history.py | 2 +- .../recorder/test_history_db_schema_30.py | 2 +- .../recorder/test_history_db_schema_32.py | 2 +- .../recorder/test_history_db_schema_42.py | 2 +- tests/components/recorder/test_init.py | 2 +- tests/components/recorder/test_migrate.py | 2 +- tests/components/recorder/test_statistics.py | 2 +- tests/components/recorder/test_util.py | 2 +- tests/components/sensor/test_recorder.py | 2 +- tests/conftest.py | 47 +++++++++++++++---- 14 files changed, 51 insertions(+), 22 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index f5d5b86635a..ec129d97c16 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -100,7 +100,7 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "aiohttp_client": "ClientSessionGenerator", "aiohttp_server": "Callable[[], TestServer]", "area_registry": "AreaRegistry", - "async_setup_recorder_instance": "RecorderInstanceGenerator", + "async_test_recorder": "RecorderInstanceGenerator", "caplog": "pytest.LogCaptureFixture", "capsys": "pytest.CaptureFixture[str]", "current_request_with_host": "None", diff --git a/tests/components/history/conftest.py b/tests/components/history/conftest.py index 075909dfd63..dd10fccccdc 100644 --- a/tests/components/history/conftest.py +++ b/tests/components/history/conftest.py @@ -13,7 +13,7 @@ from tests.typing import RecorderInstanceGenerator @pytest.fixture async def mock_recorder_before_hass( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/auto_repairs/statistics/test_duplicates.py b/tests/components/recorder/auto_repairs/statistics/test_duplicates.py index 5ed86698c58..457f180bb91 100644 --- a/tests/components/recorder/auto_repairs/statistics/test_duplicates.py +++ b/tests/components/recorder/auto_repairs/statistics/test_duplicates.py @@ -29,7 +29,7 @@ from tests.typing import RecorderInstanceGenerator @pytest.fixture async def mock_recorder_before_hass( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/test_entity_registry.py b/tests/components/recorder/test_entity_registry.py index a74992525b1..ad438dcc525 100644 --- a/tests/components/recorder/test_entity_registry.py +++ b/tests/components/recorder/test_entity_registry.py @@ -40,7 +40,7 @@ def _count_entity_id_in_states_meta( @pytest.fixture async def mock_recorder_before_hass( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index af846353467..e031909edd6 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -47,7 +47,7 @@ from tests.typing import RecorderInstanceGenerator @pytest.fixture async def mock_recorder_before_hass( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/test_history_db_schema_30.py b/tests/components/recorder/test_history_db_schema_30.py index e5e80b0cdb9..0e5f6cf7f79 100644 --- a/tests/components/recorder/test_history_db_schema_30.py +++ b/tests/components/recorder/test_history_db_schema_30.py @@ -33,7 +33,7 @@ from tests.typing import RecorderInstanceGenerator @pytest.fixture async def mock_recorder_before_hass( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/test_history_db_schema_32.py b/tests/components/recorder/test_history_db_schema_32.py index 8a3e6a58ab3..3ee6edd8e1e 100644 --- a/tests/components/recorder/test_history_db_schema_32.py +++ b/tests/components/recorder/test_history_db_schema_32.py @@ -33,7 +33,7 @@ from tests.typing import RecorderInstanceGenerator @pytest.fixture async def mock_recorder_before_hass( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/test_history_db_schema_42.py b/tests/components/recorder/test_history_db_schema_42.py index 083d4c0930e..974a642fc31 100644 --- a/tests/components/recorder/test_history_db_schema_42.py +++ b/tests/components/recorder/test_history_db_schema_42.py @@ -42,7 +42,7 @@ from tests.typing import RecorderInstanceGenerator @pytest.fixture async def mock_recorder_before_hass( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 52a220662ae..48138bbc952 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -104,7 +104,7 @@ from tests.typing import RecorderInstanceGenerator @pytest.fixture async def mock_recorder_before_hass( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index e75aa5588c4..f31d3530500 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -40,7 +40,7 @@ from tests.typing import RecorderInstanceGenerator @pytest.fixture async def mock_recorder_before_hass( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 7d8bc6e3415..cd8cd1a51df 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -51,7 +51,7 @@ from tests.typing import RecorderInstanceGenerator, WebSocketGenerator @pytest.fixture async def mock_recorder_before_hass( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index d72978c57bb..089d1938227 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -48,7 +48,7 @@ from tests.typing import RecorderInstanceGenerator @pytest.fixture async def mock_recorder_before_hass( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, ) -> None: """Set up recorder.""" diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 62cb66d2053..afa543ac12d 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -95,7 +95,7 @@ KW_SENSOR_ATTRIBUTES = { @pytest.fixture async def mock_recorder_before_hass( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, ) -> None: """Set up recorder patches.""" diff --git a/tests/conftest.py b/tests/conftest.py index 50258db8f3b..565e0e42534 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import AsyncGenerator, Callable, Coroutine, Generator -from contextlib import asynccontextmanager, contextmanager +from contextlib import AsyncExitStack, asynccontextmanager, contextmanager import datetime import functools import gc @@ -1385,7 +1385,7 @@ async def _async_init_recorder_component( @pytest.fixture -async def async_setup_recorder_instance( +async def async_test_recorder( recorder_db_url: str, enable_nightly_purge: bool, enable_statistics: bool, @@ -1394,7 +1394,7 @@ async def async_setup_recorder_instance( enable_migrate_event_type_ids: bool, enable_migrate_entity_ids: bool, ) -> AsyncGenerator[RecorderInstanceGenerator]: - """Yield callable to setup recorder instance.""" + """Yield context manager to setup recorder instance.""" # pylint: disable-next=import-outside-toplevel from homeassistant.components import recorder @@ -1477,12 +1477,13 @@ async def async_setup_recorder_instance( ), ): - async def async_setup_recorder( + @asynccontextmanager + async def async_test_recorder( hass: HomeAssistant, config: ConfigType | None = None, *, wait_recorder: bool = True, - ) -> recorder.Recorder: + ) -> AsyncGenerator[recorder.Recorder]: """Setup and return recorder instance.""" # noqa: D401 await _async_init_recorder_component(hass, config, recorder_db_url) await hass.async_block_till_done() @@ -1490,7 +1491,34 @@ async def async_setup_recorder_instance( # The recorder's worker is not started until Home Assistant is running if hass.state is CoreState.running and wait_recorder: await async_recorder_block_till_done(hass) - return instance + try: + yield instance + finally: + if instance.is_alive(): + await instance._async_shutdown(None) + + yield async_test_recorder + + +@pytest.fixture +async def async_setup_recorder_instance( + async_test_recorder: RecorderInstanceGenerator, +) -> AsyncGenerator[RecorderInstanceGenerator]: + """Yield callable to setup recorder instance.""" + + async with AsyncExitStack() as stack: + + async def async_setup_recorder( + hass: HomeAssistant, + config: ConfigType | None = None, + *, + wait_recorder: bool = True, + ) -> AsyncGenerator[recorder.Recorder]: + """Set up and return recorder instance.""" + + return await stack.enter_async_context( + async_test_recorder(hass, config, wait_recorder=wait_recorder) + ) yield async_setup_recorder @@ -1498,11 +1526,12 @@ async def async_setup_recorder_instance( @pytest.fixture async def recorder_mock( recorder_config: dict[str, Any] | None, - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, hass: HomeAssistant, -) -> recorder.Recorder: +) -> AsyncGenerator[recorder.Recorder]: """Fixture with in-memory recorder.""" - return await async_setup_recorder_instance(hass, recorder_config) + async with async_test_recorder(hass, recorder_config) as instance: + yield instance @pytest.fixture From 48812058311cfffb343b44bdfcac985eb38a0df8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 12:22:38 -0700 Subject: [PATCH 0684/2411] Fix event loop blocking I/O in command_line tests (#121098) --- tests/components/command_line/test_notify.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 98bfb856bb8..c775d87fedb 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -3,6 +3,7 @@ from __future__ import annotations import os +from pathlib import Path import subprocess import tempfile from unittest.mock import patch @@ -78,9 +79,7 @@ async def test_command_line_output(hass: HomeAssistant) -> None: await hass.services.async_call( NOTIFY_DOMAIN, "test3", {"message": message}, blocking=True ) - with open(filename, encoding="UTF-8") as handle: - # the echo command adds a line break - assert message == handle.read() + assert message == await hass.async_add_executor_job(Path(filename).read_text) @pytest.mark.parametrize( From 53767b6159c086f265d3eb3fe37f85364f4481b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 12:24:25 -0700 Subject: [PATCH 0685/2411] Fix event loop blocking I/O in generic tests (#121100) --- tests/components/generic/test_config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 7e76d8f3891..8c2d52f173b 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -4,6 +4,7 @@ import contextlib import errno from http import HTTPStatus import os.path +from pathlib import Path from unittest.mock import AsyncMock, PropertyMock, patch import httpx @@ -274,8 +275,8 @@ async def test_form_only_still_sample( ) -> None: """Test various sample images #69037.""" image_path = os.path.join(os.path.dirname(__file__), image_file) - with open(image_path, "rb") as image: - respx.get("http://127.0.0.1/testurl/1").respond(stream=image.read()) + image_bytes = await hass.async_add_executor_job(Path(image_path).read_bytes) + respx.get("http://127.0.0.1/testurl/1").respond(stream=image_bytes) data = TESTDATA.copy() data.pop(CONF_STREAM_SOURCE) with patch("homeassistant.components.generic.async_setup_entry", return_value=True): From 2040c285b10c68afd214ea1eaec0b100863d9762 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 3 Jul 2024 21:35:20 +0200 Subject: [PATCH 0686/2411] Remove schema option for mqtt vacuum configs (#121093) --- homeassistant/components/mqtt/strings.json | 4 -- homeassistant/components/mqtt/vacuum.py | 66 ++++----------------- tests/components/mqtt/test_legacy_vacuum.py | 19 +++--- tests/components/mqtt/test_vacuum.py | 9 +-- 4 files changed, 25 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 6034197aec7..f1e740d7f35 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -1,9 +1,5 @@ { "issues": { - "deprecation_mqtt_schema_vacuum_yaml": { - "title": "MQTT vacuum entities with deprecated `schema` config settings found in your configuration.yaml", - "description": "The `schema` setting for MQTT vacuum entities is deprecated and should be removed. Please adjust your configuration.yaml and restart Home Assistant to fix this issue." - }, "deprecated_color_handling": { "title": "Deprecated color handling used for MQTT light", "description": "An MQTT light config (with `json` schema) found in `configuration.yaml` uses deprecated color handling flags.\n\nConfiguration found:\n```yaml\n{config}\n```\nDeprecated flags: **{deprecated_flags}**.\n\nUse the `supported_color_modes` option instead and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue." diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index 8aa42270016..c9898465184 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -7,7 +7,6 @@ from __future__ import annotations -from collections.abc import Callable import logging from typing import Any, cast @@ -30,23 +29,16 @@ from homeassistant.const import ( STATE_IDLE, STATE_PAUSED, ) -from homeassistant.core import HomeAssistant, async_get_hass, callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.json import json_dumps from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolSchemaType from homeassistant.util.json import json_loads_object from . import subscription from .config import MQTT_BASE_SCHEMA -from .const import ( - CONF_COMMAND_TOPIC, - CONF_RETAIN, - CONF_SCHEMA, - CONF_STATE_TOPIC, - DOMAIN, -) +from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_SCHEMA, CONF_STATE_TOPIC from .mixins import MqttEntity, async_setup_entity_entry_helper from .models import ReceiveMessage from .schemas import MQTT_ENTITY_COMMON_SCHEMA @@ -157,47 +149,6 @@ MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset( MQTT_VACUUM_DOCS_URL = "https://www.home-assistant.io/integrations/vacuum.mqtt/" -def _fail_legacy_config(discovery: bool) -> Callable[[ConfigType], ConfigType]: - @callback - def _fail_legacy_config_callback(config: ConfigType) -> ConfigType: - """Fail the legacy schema.""" - if CONF_SCHEMA not in config: - return config - - if config[CONF_SCHEMA] == "legacy": - raise vol.Invalid( - "The support for the `legacy` MQTT vacuum schema has been removed" - ) - - if discovery: - _LOGGER.warning( - "The `schema` option is deprecated for MQTT %s, but " - "it was used in a discovery payload. Please contact the maintainer " - "of the integration or service that supplies the config, and suggest " - "to remove the option. Got %s at discovery topic %s", - vacuum.DOMAIN, - config, - getattr(config, "discovery_data")["discovery_topic"], - ) - return config - - translation_key = "deprecation_mqtt_schema_vacuum_yaml" - hass = async_get_hass() - async_create_issue( - hass, - DOMAIN, - translation_key, - breaks_in_ha_version="2024.8.0", - is_fixable=False, - translation_key=translation_key, - learn_more_url=MQTT_VACUUM_DOCS_URL, - severity=IssueSeverity.WARNING, - ) - return config - - return _fail_legacy_config_callback - - VACUUM_BASE_SCHEMA = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( @@ -227,15 +178,20 @@ VACUUM_BASE_SCHEMA = MQTT_BASE_SCHEMA.extend( ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) DISCOVERY_SCHEMA = vol.All( - _fail_legacy_config(discovery=True), VACUUM_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), - cv.deprecated(CONF_SCHEMA), + # Do not fail a config is the schema option is still present, + # De option was deprecated with HA Core 2024.2 and removed with HA Core 2024.8. + # As we allow extra options, and we will remove this check silently + # with HA Core 2025.8.0, we will only warn, + # if a adiscovery config still uses this option. + cv.removed(CONF_SCHEMA, raise_if_present=False), ) PLATFORM_SCHEMA_MODERN = vol.All( - _fail_legacy_config(discovery=False), VACUUM_BASE_SCHEMA, - cv.deprecated(CONF_SCHEMA), + # The schema options was removed with HA Core 2024.8, + # the cleanup is planned for HA Core 2025.8. + cv.removed(CONF_SCHEMA, raise_if_present=True), ) diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index e4f5e3cd481..9b45b65d2cc 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -23,7 +23,7 @@ DEFAULT_CONFIG = {mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}} [ ({mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test", "schema": "legacy"}}}, True), ({mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}}, False), - ({mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test", "schema": "state"}}}, False), + ({mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test", "schema": "state"}}}, True), ], ) async def test_removed_support_yaml( @@ -39,8 +39,8 @@ async def test_removed_support_yaml( if removed: assert entity is None assert ( - "The support for the `legacy` MQTT " - "vacuum schema has been removed" in caplog.text + "The 'schema' option has been removed, " + "please remove it from your configuration" in caplog.text ) else: assert entity is not None @@ -51,7 +51,7 @@ async def test_removed_support_yaml( [ ({"name": "test", "schema": "legacy"}, True), ({"name": "test"}, False), - ({"name": "test", "schema": "state"}, False), + ({"name": "test", "schema": "state"}, True), ], ) async def test_removed_support_discovery( @@ -69,12 +69,15 @@ async def test_removed_support_discovery( await hass.async_block_till_done() entity = hass.states.get("vacuum.test") + assert entity is not None if removed: - assert entity is None assert ( - "The support for the `legacy` MQTT " - "vacuum schema has been removed" in caplog.text + "The 'schema' option has been removed, " + "please remove it from your configuration" in caplog.text ) else: - assert entity is not None + assert ( + "The 'schema' option has been removed, " + "please remove it from your configuration" not in caplog.text + ) diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index a7a5280c3e1..7fc4ff981fd 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -119,16 +119,13 @@ async def test_warning_schema_option( await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("vacuum.test") + # We do not fail if the schema option is still in the payload, but we log an error assert state is not None with caplog.at_level(logging.WARNING): assert ( - "The `schema` option is deprecated for MQTT vacuum, but it was used in a " - "discovery payload. Please contact the maintainer of the integration or " - "service that supplies the config, and suggest to remove the option." - in caplog.text + "The 'schema' option has been removed, " + "please remove it from your configuration" in caplog.text ) - assert "https://example.com/support" in caplog.text - assert "at discovery topic homeassistant/vacuum/bla/config" in caplog.text @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) From 8017bc6776c6826c0a3b485f167c38906c33a5fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 12:37:29 -0700 Subject: [PATCH 0687/2411] Fix blocking I/O in demo mailbox (#121097) --- homeassistant/components/demo/mailbox.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/demo/mailbox.py b/homeassistant/components/demo/mailbox.py index 3ec80e47118..e0cdd05782d 100644 --- a/homeassistant/components/demo/mailbox.py +++ b/homeassistant/components/demo/mailbox.py @@ -66,14 +66,17 @@ class DemoMailbox(Mailbox): """Return if messages have attached media files.""" return True + def _get_media(self) -> bytes: + """Return the media blob for the msgid.""" + audio_path = os.path.join(os.path.dirname(__file__), "tts.mp3") + with open(audio_path, "rb") as file: + return file.read() + async def async_get_media(self, msgid: str) -> bytes: """Return the media blob for the msgid.""" if msgid not in self._messages: raise StreamError("Message not found") - - audio_path = os.path.join(os.path.dirname(__file__), "tts.mp3") - with open(audio_path, "rb") as file: - return file.read() + return await self.hass.async_add_executor_job(self._get_media) async def async_get_messages(self) -> list[dict[str, Any]]: """Return a list of the current messages.""" From f85c3565227215f0e11f1aedd103eb4cbec86688 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 3 Jul 2024 22:19:59 +0200 Subject: [PATCH 0688/2411] Fix HmIP-ESI GAS sensor DeviceClass (#121106) should be SensorDeviceClass:GAS instead of SensorDeviceClass:VOLUME to be supported in the Energy Dashboard --- homeassistant/components/homematicip_cloud/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 6cdff6caef3..6bf128a1663 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -516,7 +516,7 @@ SENSORS_ESI = { HmipEsiSensorEntityDescription( key=ESI_TYPE_CURRENT_GAS_VOLUME, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, - device_class=SensorDeviceClass.VOLUME, + device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda device: device.functional_channel.gasVolume, exists_fn=lambda channel: channel.gasVolume is not None, From 291f309c0ea99528229e39dfcacb3a9faaf62487 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 13:24:41 -0700 Subject: [PATCH 0689/2411] Remove unnecessary lambdas in timeout tests (#121101) --- tests/util/test_timeout.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/util/test_timeout.py b/tests/util/test_timeout.py index 797c849db3c..496096bd740 100644 --- a/tests/util/test_timeout.py +++ b/tests/util/test_timeout.py @@ -25,7 +25,7 @@ async def test_simple_global_timeout_with_executor_job(hass: HomeAssistant) -> N with pytest.raises(TimeoutError): async with timeout.async_timeout(0.1): - await hass.async_add_executor_job(lambda: time.sleep(0.2)) + await hass.async_add_executor_job(time.sleep, 0.2) async def test_simple_global_timeout_freeze() -> None: @@ -133,7 +133,7 @@ async def test_mix_global_timeout_freeze_and_zone_freeze_inside_executor_job_sec async with timeout.async_timeout(0.1): async with timeout.async_timeout(0.2, zone_name="recorder"): await hass.async_add_executor_job(_some_sync_work) - await hass.async_add_executor_job(lambda: time.sleep(0.2)) + await hass.async_add_executor_job(time.sleep, 0.2) async def test_simple_global_timeout_freeze_with_executor_job( @@ -143,7 +143,7 @@ async def test_simple_global_timeout_freeze_with_executor_job( timeout = TimeoutManager() async with timeout.async_timeout(0.2), timeout.async_freeze(): - await hass.async_add_executor_job(lambda: time.sleep(0.3)) + await hass.async_add_executor_job(time.sleep, 0.3) async def test_simple_global_timeout_freeze_reset() -> None: From 7958c0825e123008dc39ae02a7b44c2f94b5aaca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 13:26:03 -0700 Subject: [PATCH 0690/2411] Fix blocking process call in process tests (#121104) Discovered by ruff in https://github.com/home-assistant/core/pull/120799 --- tests/util/test_process.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/util/test_process.py b/tests/util/test_process.py index c6125b656a5..999abe0476f 100644 --- a/tests/util/test_process.py +++ b/tests/util/test_process.py @@ -1,20 +1,25 @@ """Test process util.""" +from functools import partial import os import subprocess import pytest +from homeassistant.core import HomeAssistant from homeassistant.util import process -async def test_kill_process() -> None: +async def test_kill_process(hass: HomeAssistant) -> None: """Test killing a process.""" - sleeper = subprocess.Popen( # noqa: S602 # shell by design - "sleep 1000", - shell=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, + sleeper = await hass.async_add_executor_job( + partial( # noqa: S604 # shell by design + subprocess.Popen, + "sleep 1000", + shell=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) ) pid = sleeper.pid From c9240b8e34d5991a90894ac17c029b89e5412d01 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 3 Jul 2024 22:49:01 +0200 Subject: [PATCH 0691/2411] Add monkeypatch type hints to switcher_kis tests (#121055) * Add monkeypatch type hints to switch_kis * Improve --- tests/components/switcher_kis/test_button.py | 9 +++++++-- tests/components/switcher_kis/test_climate.py | 16 +++++++--------- tests/components/switcher_kis/test_cover.py | 4 +++- .../components/switcher_kis/test_diagnostics.py | 7 ++++++- tests/components/switcher_kis/test_sensor.py | 4 +++- tests/components/switcher_kis/test_services.py | 2 +- tests/components/switcher_kis/test_switch.py | 6 ++++-- 7 files changed, 31 insertions(+), 17 deletions(-) diff --git a/tests/components/switcher_kis/test_button.py b/tests/components/switcher_kis/test_button.py index 264c163e111..d0604487370 100644 --- a/tests/components/switcher_kis/test_button.py +++ b/tests/components/switcher_kis/test_button.py @@ -63,7 +63,12 @@ async def test_assume_button( ) @pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) async def test_swing_button( - hass: HomeAssistant, entity, swing, mock_bridge, mock_api, monkeypatch + hass: HomeAssistant, + entity, + swing, + mock_bridge, + mock_api, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test vertical swing on/off button.""" monkeypatch.setattr(DEVICE, "remote_id", "ELEC7022") @@ -88,7 +93,7 @@ async def test_swing_button( @pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) async def test_control_device_fail( - hass: HomeAssistant, mock_bridge, mock_api, monkeypatch + hass: HomeAssistant, mock_bridge, mock_api, monkeypatch: pytest.MonkeyPatch ) -> None: """Test control device fail.""" await init_integration(hass) diff --git a/tests/components/switcher_kis/test_climate.py b/tests/components/switcher_kis/test_climate.py index 759f7f1bd98..5da9684bf2a 100644 --- a/tests/components/switcher_kis/test_climate.py +++ b/tests/components/switcher_kis/test_climate.py @@ -37,7 +37,7 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{slugify(DEVICE.name)}" @pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) async def test_climate_hvac_mode( - hass: HomeAssistant, mock_bridge, mock_api, monkeypatch + hass: HomeAssistant, mock_bridge, mock_api, monkeypatch: pytest.MonkeyPatch ) -> None: """Test climate hvac mode service.""" await init_integration(hass) @@ -92,7 +92,7 @@ async def test_climate_hvac_mode( @pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) async def test_climate_temperature( - hass: HomeAssistant, mock_bridge, mock_api, monkeypatch + hass: HomeAssistant, mock_bridge, mock_api, monkeypatch: pytest.MonkeyPatch ) -> None: """Test climate temperature service.""" await init_integration(hass) @@ -144,7 +144,7 @@ async def test_climate_temperature( @pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) async def test_climate_fan_level( - hass: HomeAssistant, mock_bridge, mock_api, monkeypatch + hass: HomeAssistant, mock_bridge, mock_api, monkeypatch: pytest.MonkeyPatch ) -> None: """Test climate fan level service.""" await init_integration(hass) @@ -179,7 +179,7 @@ async def test_climate_fan_level( @pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) async def test_climate_swing( - hass: HomeAssistant, mock_bridge, mock_api, monkeypatch + hass: HomeAssistant, mock_bridge, mock_api, monkeypatch: pytest.MonkeyPatch ) -> None: """Test climate swing service.""" await init_integration(hass) @@ -234,9 +234,7 @@ async def test_climate_swing( @pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) -async def test_control_device_fail( - hass: HomeAssistant, mock_bridge, mock_api, monkeypatch -) -> None: +async def test_control_device_fail(hass: HomeAssistant, mock_bridge, mock_api) -> None: """Test control device fail.""" await init_integration(hass) assert mock_bridge @@ -295,7 +293,7 @@ async def test_control_device_fail( @pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) async def test_bad_update_discard( - hass: HomeAssistant, mock_bridge, mock_api, monkeypatch + hass: HomeAssistant, mock_bridge, mock_api, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that a bad update from device is discarded.""" await init_integration(hass) @@ -318,7 +316,7 @@ async def test_bad_update_discard( @pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) async def test_climate_control_errors( - hass: HomeAssistant, mock_bridge, mock_api, monkeypatch + hass: HomeAssistant, mock_bridge, mock_api, monkeypatch: pytest.MonkeyPatch ) -> None: """Test control with settings not supported by device.""" await init_integration(hass) diff --git a/tests/components/switcher_kis/test_cover.py b/tests/components/switcher_kis/test_cover.py index 07f349d1a72..57e2f98915e 100644 --- a/tests/components/switcher_kis/test_cover.py +++ b/tests/components/switcher_kis/test_cover.py @@ -31,7 +31,9 @@ ENTITY_ID = f"{COVER_DOMAIN}.{slugify(DEVICE.name)}" @pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) -async def test_cover(hass: HomeAssistant, mock_bridge, mock_api, monkeypatch) -> None: +async def test_cover( + hass: HomeAssistant, mock_bridge, mock_api, monkeypatch: pytest.MonkeyPatch +) -> None: """Test cover services.""" await init_integration(hass) assert mock_bridge diff --git a/tests/components/switcher_kis/test_diagnostics.py b/tests/components/switcher_kis/test_diagnostics.py index f49ab99ba6c..107a48a1062 100644 --- a/tests/components/switcher_kis/test_diagnostics.py +++ b/tests/components/switcher_kis/test_diagnostics.py @@ -1,5 +1,7 @@ """Tests for the diagnostics data provided by Switcher.""" +import pytest + from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant @@ -11,7 +13,10 @@ from tests.typing import ClientSessionGenerator async def test_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_bridge, monkeypatch + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_bridge, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test diagnostics.""" entry = await init_integration(hass) diff --git a/tests/components/switcher_kis/test_sensor.py b/tests/components/switcher_kis/test_sensor.py index 1be2efed987..8ccc33f2d37 100644 --- a/tests/components/switcher_kis/test_sensor.py +++ b/tests/components/switcher_kis/test_sensor.py @@ -74,7 +74,9 @@ async def test_sensor_disabled( @pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True) -async def test_sensor_update(hass: HomeAssistant, mock_bridge, monkeypatch) -> None: +async def test_sensor_update( + hass: HomeAssistant, mock_bridge, monkeypatch: pytest.MonkeyPatch +) -> None: """Test sensor update.""" await init_integration(hass) assert mock_bridge diff --git a/tests/components/switcher_kis/test_services.py b/tests/components/switcher_kis/test_services.py index 039daec4c97..26c54ee53ed 100644 --- a/tests/components/switcher_kis/test_services.py +++ b/tests/components/switcher_kis/test_services.py @@ -30,7 +30,7 @@ from .consts import ( @pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True) async def test_turn_on_with_timer_service( - hass: HomeAssistant, mock_bridge, mock_api, monkeypatch + hass: HomeAssistant, mock_bridge, mock_api, monkeypatch: pytest.MonkeyPatch ) -> None: """Test the turn on with timer service.""" await init_integration(hass) diff --git a/tests/components/switcher_kis/test_switch.py b/tests/components/switcher_kis/test_switch.py index 058546ac2ae..f14a8f5b1ca 100644 --- a/tests/components/switcher_kis/test_switch.py +++ b/tests/components/switcher_kis/test_switch.py @@ -23,7 +23,9 @@ from .consts import DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE @pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True) -async def test_switch(hass: HomeAssistant, mock_bridge, mock_api, monkeypatch) -> None: +async def test_switch( + hass: HomeAssistant, mock_bridge, mock_api, monkeypatch: pytest.MonkeyPatch +) -> None: """Test the switch.""" await init_integration(hass) assert mock_bridge @@ -75,7 +77,7 @@ async def test_switch_control_fail( hass: HomeAssistant, mock_bridge, mock_api, - monkeypatch, + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Test switch control fail.""" From 73716ea5297550c591da0aca4f050c597330b6ec Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 3 Jul 2024 22:49:31 +0200 Subject: [PATCH 0692/2411] Add monkeypatch type hints to webostv tests (#121054) * Add monkeypatch type hints to webostv * Improve --- tests/components/webostv/test_config_flow.py | 6 ++- tests/components/webostv/test_init.py | 9 +++- tests/components/webostv/test_media_player.py | 45 +++++++++++++------ tests/components/webostv/test_notify.py | 11 +++-- 4 files changed, 50 insertions(+), 21 deletions(-) diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index afda36d913f..406bb9c8804 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -295,7 +295,9 @@ async def test_form_abort_uuid_configured(hass: HomeAssistant, client) -> None: assert entry.data[CONF_HOST] == "new_host" -async def test_reauth_successful(hass: HomeAssistant, client, monkeypatch) -> None: +async def test_reauth_successful( + hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch +) -> None: """Test that the reauthorization is successful.""" entry = await setup_webostv(hass) assert client @@ -331,7 +333,7 @@ async def test_reauth_successful(hass: HomeAssistant, client, monkeypatch) -> No ], ) async def test_reauth_errors( - hass: HomeAssistant, client, monkeypatch, side_effect, reason + hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch, side_effect, reason ) -> None: """Test reauthorization errors.""" entry = await setup_webostv(hass) diff --git a/tests/components/webostv/test_init.py b/tests/components/webostv/test_init.py index a2961a81a4e..e2638c86f5e 100644 --- a/tests/components/webostv/test_init.py +++ b/tests/components/webostv/test_init.py @@ -3,6 +3,7 @@ from unittest.mock import Mock from aiowebostv import WebOsTvPairError +import pytest from homeassistant.components.webostv.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState @@ -12,7 +13,9 @@ from homeassistant.core import HomeAssistant from . import setup_webostv -async def test_reauth_setup_entry(hass: HomeAssistant, client, monkeypatch) -> None: +async def test_reauth_setup_entry( + hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch +) -> None: """Test reauth flow triggered by setup entry.""" monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError)) @@ -32,7 +35,9 @@ async def test_reauth_setup_entry(hass: HomeAssistant, client, monkeypatch) -> N assert flow["context"].get("entry_id") == entry.entry_id -async def test_key_update_setup_entry(hass: HomeAssistant, client, monkeypatch) -> None: +async def test_key_update_setup_entry( + hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch +) -> None: """Test key update from setup entry.""" monkeypatch.setattr(client, "client_key", "new_key") entry = await setup_webostv(hass) diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 6c4aeb5e984..775a3eb9383 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -144,7 +144,7 @@ async def test_media_play_pause(hass: HomeAssistant, client) -> None: ], ) async def test_media_next_previous_track( - hass: HomeAssistant, client, service, client_call, monkeypatch + hass: HomeAssistant, client, service, client_call, monkeypatch: pytest.MonkeyPatch ) -> None: """Test media next/previous track services.""" await setup_webostv(hass) @@ -270,7 +270,10 @@ async def test_select_sound_output(hass: HomeAssistant, client) -> None: async def test_device_info_startup_off( - hass: HomeAssistant, client, monkeypatch, device_registry: dr.DeviceRegistry + hass: HomeAssistant, + client, + monkeypatch: pytest.MonkeyPatch, + device_registry: dr.DeviceRegistry, ) -> None: """Test device info when device is off at startup.""" monkeypatch.setattr(client, "system_info", None) @@ -291,7 +294,10 @@ async def test_device_info_startup_off( async def test_entity_attributes( - hass: HomeAssistant, client, monkeypatch, device_registry: dr.DeviceRegistry + hass: HomeAssistant, + client, + monkeypatch: pytest.MonkeyPatch, + device_registry: dr.DeviceRegistry, ) -> None: """Test entity attributes.""" entry = await setup_webostv(hass) @@ -383,7 +389,7 @@ async def test_play_media(hass: HomeAssistant, client, media_id, ch_id) -> None: async def test_update_sources_live_tv_find( - hass: HomeAssistant, client, monkeypatch + hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch ) -> None: """Test finding live TV app id in update sources.""" await setup_webostv(hass) @@ -466,7 +472,9 @@ async def test_update_sources_live_tv_find( assert len(sources) == 1 -async def test_client_disconnected(hass: HomeAssistant, client, monkeypatch) -> None: +async def test_client_disconnected( + hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch +) -> None: """Test error not raised when client is disconnected.""" await setup_webostv(hass) monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) @@ -477,7 +485,10 @@ async def test_client_disconnected(hass: HomeAssistant, client, monkeypatch) -> async def test_control_error_handling( - hass: HomeAssistant, client, caplog: pytest.LogCaptureFixture, monkeypatch + hass: HomeAssistant, + client, + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test control errors handling.""" await setup_webostv(hass) @@ -507,7 +518,9 @@ async def test_control_error_handling( ) -async def test_supported_features(hass: HomeAssistant, client, monkeypatch) -> None: +async def test_supported_features( + hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch +) -> None: """Test test supported features.""" monkeypatch.setattr(client, "sound_output", "lineout") await setup_webostv(hass) @@ -565,7 +578,7 @@ async def test_supported_features(hass: HomeAssistant, client, monkeypatch) -> N async def test_cached_supported_features( - hass: HomeAssistant, client, monkeypatch + hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch ) -> None: """Test test supported features.""" monkeypatch.setattr(client, "is_on", False) @@ -672,7 +685,7 @@ async def test_cached_supported_features( async def test_supported_features_no_cache( - hass: HomeAssistant, client, monkeypatch + hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch ) -> None: """Test supported features if device is off and no cache.""" monkeypatch.setattr(client, "is_on", False) @@ -716,7 +729,7 @@ async def test_get_image_http( client, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - monkeypatch, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test get image via http.""" url = "http://something/valid_icon" @@ -742,7 +755,7 @@ async def test_get_image_http_error( hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, - monkeypatch, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test get image via http error.""" url = "http://something/icon_error" @@ -769,7 +782,7 @@ async def test_get_image_https( client, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - monkeypatch, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test get image via http.""" url = "https://something/valid_icon_https" @@ -789,7 +802,9 @@ async def test_get_image_https( assert content == b"https_image" -async def test_reauth_reconnect(hass: HomeAssistant, client, monkeypatch) -> None: +async def test_reauth_reconnect( + hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch +) -> None: """Test reauth flow triggered by reconnect.""" entry = await setup_webostv(hass) monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) @@ -814,7 +829,9 @@ async def test_reauth_reconnect(hass: HomeAssistant, client, monkeypatch) -> Non assert flow["context"].get("entry_id") == entry.entry_id -async def test_update_media_state(hass: HomeAssistant, client, monkeypatch) -> None: +async def test_update_media_state( + hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch +) -> None: """Test updating media state.""" await setup_webostv(hass) diff --git a/tests/components/webostv/test_notify.py b/tests/components/webostv/test_notify.py index a1c37b9bf97..75c2e148310 100644 --- a/tests/components/webostv/test_notify.py +++ b/tests/components/webostv/test_notify.py @@ -72,7 +72,9 @@ async def test_notify(hass: HomeAssistant, client) -> None: ) -async def test_notify_not_connected(hass: HomeAssistant, client, monkeypatch) -> None: +async def test_notify_not_connected( + hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch +) -> None: """Test sending a message when client is not connected.""" await setup_webostv(hass) assert hass.services.has_service(NOTIFY_DOMAIN, TV_NAME) @@ -95,7 +97,10 @@ async def test_notify_not_connected(hass: HomeAssistant, client, monkeypatch) -> async def test_icon_not_found( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, client, monkeypatch + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + client, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test notify icon not found error.""" await setup_webostv(hass) @@ -130,7 +135,7 @@ async def test_connection_errors( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, client, - monkeypatch, + monkeypatch: pytest.MonkeyPatch, side_effect, error, ) -> None: From 408e524551a47fec638be30662ece949baf28169 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Jul 2024 00:02:46 +0200 Subject: [PATCH 0693/2411] Add recorder test fixture for skipping tests by DB engine (#121118) * Add recorder test fixture for skipping tests by DB engine * Fix mistake --- .../recorder/auto_repairs/test_schema.py | 40 ++++++--------- tests/components/recorder/conftest.py | 24 +++++++++ tests/components/recorder/test_history.py | 33 ++++++++----- .../recorder/test_history_db_schema_42.py | 33 ++++++++----- tests/components/recorder/test_init.py | 49 ++++++++++++------- tests/components/recorder/test_migrate.py | 39 ++++++++------- ..._migration_run_time_migrations_remember.py | 8 +-- tests/components/recorder/test_purge.py | 12 +++-- .../recorder/test_purge_v32_schema.py | 21 +++++--- .../components/recorder/test_system_health.py | 20 +++++--- tests/components/recorder/test_util.py | 27 +++++----- 11 files changed, 185 insertions(+), 121 deletions(-) diff --git a/tests/components/recorder/auto_repairs/test_schema.py b/tests/components/recorder/auto_repairs/test_schema.py index d921c0cdbf8..bdc01447096 100644 --- a/tests/components/recorder/auto_repairs/test_schema.py +++ b/tests/components/recorder/auto_repairs/test_schema.py @@ -40,6 +40,8 @@ async def test_validate_db_schema( assert "Database is about to correct DB schema errors" not in caplog.text +@pytest.mark.skip_on_db_engine(["postgresql", "sqlite"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_fix_utf8_issue_good_schema( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -47,9 +49,6 @@ async def test_validate_db_schema_fix_utf8_issue_good_schema( caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema with MySQL when the schema is correct.""" - if not recorder_db_url.startswith("mysql://"): - # This problem only happens on MySQL - return await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) instance = get_instance(hass) @@ -59,6 +58,8 @@ async def test_validate_db_schema_fix_utf8_issue_good_schema( assert schema_errors == set() +@pytest.mark.skip_on_db_engine(["postgresql", "sqlite"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_fix_utf8_issue_with_broken_schema( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -66,9 +67,6 @@ async def test_validate_db_schema_fix_utf8_issue_with_broken_schema( caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema with MySQL when the schema is broken and repairing it.""" - if not recorder_db_url.startswith("mysql://"): - # This problem only happens on MySQL - return await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) instance = get_instance(hass) @@ -102,6 +100,8 @@ async def test_validate_db_schema_fix_utf8_issue_with_broken_schema( assert schema_errors == set() +@pytest.mark.skip_on_db_engine(["postgresql", "sqlite"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_fix_incorrect_collation( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -109,9 +109,6 @@ async def test_validate_db_schema_fix_incorrect_collation( caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema with MySQL when the collation is incorrect.""" - if not recorder_db_url.startswith("mysql://"): - # This problem only happens on MySQL - return await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) instance = get_instance(hass) @@ -144,6 +141,8 @@ async def test_validate_db_schema_fix_incorrect_collation( assert schema_errors == set() +@pytest.mark.skip_on_db_engine(["postgresql", "sqlite"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_precision_correct_collation( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -151,9 +150,6 @@ async def test_validate_db_schema_precision_correct_collation( caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema when the schema is correct with the correct collation.""" - if not recorder_db_url.startswith("mysql://"): - # This problem only happens on MySQL - return await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) instance = get_instance(hass) @@ -165,6 +161,8 @@ async def test_validate_db_schema_precision_correct_collation( assert schema_errors == set() +@pytest.mark.skip_on_db_engine(["postgresql", "sqlite"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_fix_utf8_issue_with_broken_schema_unrepairable( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -172,9 +170,6 @@ async def test_validate_db_schema_fix_utf8_issue_with_broken_schema_unrepairable caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema with MySQL when the schema is broken and cannot be repaired.""" - if not recorder_db_url.startswith("mysql://"): - # This problem only happens on MySQL - return await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) instance = get_instance(hass) @@ -206,6 +201,8 @@ async def test_validate_db_schema_fix_utf8_issue_with_broken_schema_unrepairable assert "Error when validating DB schema" in caplog.text +@pytest.mark.skip_on_db_engine(["sqlite"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_precision_good_schema( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -213,9 +210,6 @@ async def test_validate_db_schema_precision_good_schema( caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema when the schema is correct.""" - if not recorder_db_url.startswith(("mysql://", "postgresql://")): - # This problem only happens on MySQL and PostgreSQL - return await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) instance = get_instance(hass) @@ -227,6 +221,8 @@ async def test_validate_db_schema_precision_good_schema( assert schema_errors == set() +@pytest.mark.skip_on_db_engine(["sqlite"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_precision_with_broken_schema( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -234,9 +230,6 @@ async def test_validate_db_schema_precision_with_broken_schema( caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema when the schema is broken and than repair it.""" - if not recorder_db_url.startswith(("mysql://", "postgresql://")): - # This problem only happens on MySQL and PostgreSQL - return await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) instance = get_instance(hass) @@ -275,6 +268,8 @@ async def test_validate_db_schema_precision_with_broken_schema( assert schema_errors == set() +@pytest.mark.skip_on_db_engine(["postgresql", "sqlite"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_precision_with_unrepairable_broken_schema( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -282,9 +277,6 @@ async def test_validate_db_schema_precision_with_unrepairable_broken_schema( caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema when the schema is broken and cannot be repaired.""" - if not recorder_db_url.startswith("mysql://"): - # This problem only happens on MySQL - return await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) instance = get_instance(hass) diff --git a/tests/components/recorder/conftest.py b/tests/components/recorder/conftest.py index a1ff8dc2413..1a3c25ec727 100644 --- a/tests/components/recorder/conftest.py +++ b/tests/components/recorder/conftest.py @@ -9,6 +9,30 @@ from homeassistant.components import recorder from homeassistant.core import HomeAssistant +def pytest_configure(config): + """Add custom skip_on_db_engine marker.""" + config.addinivalue_line( + "markers", + "skip_on_db_engine(engine): mark test to run only on named DB engine(s)", + ) + + +@pytest.fixture +def skip_by_db_engine(request: pytest.FixtureRequest, recorder_db_url: str) -> None: + """Fixture to skip tests on unsupported DB engines. + + Mark the test with @pytest.mark.skip_on_db_engine("mysql") to skip on mysql, or + @pytest.mark.skip_on_db_engine(["mysql", "sqlite"]) to skip on mysql and sqlite. + """ + if request.node.get_closest_marker("skip_on_db_engine"): + skip_on_db_engine = request.node.get_closest_marker("skip_on_db_engine").args[0] + if isinstance(skip_on_db_engine, str): + skip_on_db_engine = [skip_on_db_engine] + db_engine = recorder_db_url.partition("://")[0] + if db_engine in skip_on_db_engine: + pytest.skip(f"skipped for DB engine: {db_engine}") + + @pytest.fixture def recorder_dialect_name(hass: HomeAssistant, db_engine: str) -> Generator[None]: """Patch the recorder dialect.""" diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index e031909edd6..3923c72107a 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -891,14 +891,17 @@ def record_states( return zero, four, states +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_state_changes_during_period_query_during_migration_to_schema_25( hass: HomeAssistant, recorder_db_url: str, ) -> None: - """Test we can query data prior to schema 25 and during migration to schema 25.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes - return + """Test we can query data prior to schema 25 and during migration to schema 25. + + This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop the + state_attributes table. + """ instance = recorder.get_instance(hass) @@ -957,14 +960,17 @@ async def test_state_changes_during_period_query_during_migration_to_schema_25( assert state.attributes == {"name": "the light"} +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_get_states_query_during_migration_to_schema_25( hass: HomeAssistant, recorder_db_url: str, ) -> None: - """Test we can query data prior to schema 25 and during migration to schema 25.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes - return + """Test we can query data prior to schema 25 and during migration to schema 25. + + This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop the + state_attributes table. + """ instance = recorder.get_instance(hass) @@ -1007,14 +1013,17 @@ async def test_get_states_query_during_migration_to_schema_25( assert state.attributes == {"name": "the light"} +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_get_states_query_during_migration_to_schema_25_multiple_entities( hass: HomeAssistant, recorder_db_url: str, ) -> None: - """Test we can query data prior to schema 25 and during migration to schema 25.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes - return + """Test we can query data prior to schema 25 and during migration to schema 25. + + This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop the + state_attributes table. + """ instance = recorder.get_instance(hass) diff --git a/tests/components/recorder/test_history_db_schema_42.py b/tests/components/recorder/test_history_db_schema_42.py index 974a642fc31..5d9444e9cfe 100644 --- a/tests/components/recorder/test_history_db_schema_42.py +++ b/tests/components/recorder/test_history_db_schema_42.py @@ -893,14 +893,17 @@ def record_states( return zero, four, states +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_state_changes_during_period_query_during_migration_to_schema_25( hass: HomeAssistant, recorder_db_url: str, ) -> None: - """Test we can query data prior to schema 25 and during migration to schema 25.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes - return + """Test we can query data prior to schema 25 and during migration to schema 25. + + This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop the + state_attributes table. + """ instance = recorder.get_instance(hass) @@ -959,14 +962,17 @@ async def test_state_changes_during_period_query_during_migration_to_schema_25( assert state.attributes == {"name": "the light"} +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_get_states_query_during_migration_to_schema_25( hass: HomeAssistant, recorder_db_url: str, ) -> None: - """Test we can query data prior to schema 25 and during migration to schema 25.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes - return + """Test we can query data prior to schema 25 and during migration to schema 25. + + This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop the + state_attributes table. + """ instance = recorder.get_instance(hass) @@ -1009,14 +1015,17 @@ async def test_get_states_query_during_migration_to_schema_25( assert state.attributes == {"name": "the light"} +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_get_states_query_during_migration_to_schema_25_multiple_entities( hass: HomeAssistant, recorder_db_url: str, ) -> None: - """Test we can query data prior to schema 25 and during migration to schema 25.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes - return + """Test we can query data prior to schema 25 and during migration to schema 25. + + This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop the + state_attributes table. + """ instance = recorder.get_instance(hass) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 48138bbc952..dd6625bec77 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1815,16 +1815,18 @@ async def test_entity_id_filter( assert len(db_events) == idx + 1, data +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_database_lock_and_unlock( hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator, recorder_db_url: str, tmp_path: Path, ) -> None: - """Test writing events during lock getting written after unlocking.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # Database locking is only used for SQLite - return + """Test writing events during lock getting written after unlocking. + + This test is specific for SQLite: Locking is not implemented for other engines. + """ if recorder_db_url == "sqlite://": # Use file DB, in memory DB cannot do write locks. @@ -1869,6 +1871,8 @@ async def test_database_lock_and_unlock( assert len(db_events) == 1 +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_database_lock_and_overflow( hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator, @@ -1877,10 +1881,10 @@ async def test_database_lock_and_overflow( caplog: pytest.LogCaptureFixture, issue_registry: ir.IssueRegistry, ) -> None: - """Test writing events during lock leading to overflow the queue causes the database to unlock.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # Database locking is only used for SQLite - return pytest.skip("Database locking is only used for SQLite") + """Test writing events during lock leading to overflow the queue causes the database to unlock. + + This test is specific for SQLite: Locking is not implemented for other engines. + """ # Use file DB, in memory DB cannot do write locks. if recorder_db_url == "sqlite://": @@ -1935,6 +1939,8 @@ async def test_database_lock_and_overflow( assert start_time.count(":") == 2 +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_database_lock_and_overflow_checks_available_memory( hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator, @@ -1943,9 +1949,10 @@ async def test_database_lock_and_overflow_checks_available_memory( caplog: pytest.LogCaptureFixture, issue_registry: ir.IssueRegistry, ) -> None: - """Test writing events during lock leading to overflow the queue causes the database to unlock.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - return pytest.skip("Database locking is only used for SQLite") + """Test writing events during lock leading to overflow the queue causes the database to unlock. + + This test is specific for SQLite: Locking is not implemented for other engines. + """ # Use file DB, in memory DB cannot do write locks. if recorder_db_url == "sqlite://": @@ -2025,13 +2032,15 @@ async def test_database_lock_and_overflow_checks_available_memory( assert start_time.count(":") == 2 +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_database_lock_timeout( hass: HomeAssistant, setup_recorder: None, recorder_db_url: str ) -> None: - """Test locking database timeout when recorder stopped.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test is specific for SQLite: Locking is not implemented for other engines - return + """Test locking database timeout when recorder stopped. + + This test is specific for SQLite: Locking is not implemented for other engines. + """ hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) @@ -2099,16 +2108,18 @@ async def test_database_connection_keep_alive( assert "Sending keepalive" in caplog.text +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_database_connection_keep_alive_disabled_on_sqlite( hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, recorder_db_url: str, ) -> None: - """Test we do not do keep alive for sqlite.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test is specific for SQLite, keepalive runs on other engines - return + """Test we do not do keep alive for sqlite. + + This test is specific for SQLite, keepalive runs on other engines. + """ instance = await async_setup_recorder_instance(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index f31d3530500..423462f333f 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -154,16 +154,18 @@ async def test_database_migration_failed( assert len(mock_dismiss.mock_calls) == 1 +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_database_migration_encounters_corruption( hass: HomeAssistant, recorder_db_url: str, async_setup_recorder_instance: RecorderInstanceGenerator, ) -> None: - """Test we move away the database if its corrupt.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test is specific for SQLite, wiping the database on error only happens - # with SQLite. - return + """Test we move away the database if its corrupt. + + This test is specific for SQLite, wiping the database on error only happens + with SQLite. + """ assert recorder.util.async_migration_in_progress(hass) is False @@ -610,12 +612,13 @@ def test_raise_if_exception_missing_empty_cause_str() -> None: migration.raise_if_exception_missing_str(programming_exc, ["not present"]) +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") def test_rebuild_sqlite_states_table(recorder_db_url: str) -> None: - """Test that we can rebuild the states table in SQLite.""" - if not recorder_db_url.startswith("sqlite://"): - # This test is specific for SQLite - return + """Test that we can rebuild the states table in SQLite. + This test is specific for SQLite. + """ engine = create_engine(recorder_db_url) session_maker = scoped_session(sessionmaker(bind=engine, future=True)) with session_scope(session=session_maker()) as session: @@ -633,14 +636,15 @@ def test_rebuild_sqlite_states_table(recorder_db_url: str) -> None: engine.dispose() +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") def test_rebuild_sqlite_states_table_missing_fails( recorder_db_url: str, caplog: pytest.LogCaptureFixture ) -> None: - """Test handling missing states table when attempting rebuild.""" - if not recorder_db_url.startswith("sqlite://"): - # This test is specific for SQLite - return + """Test handling missing states table when attempting rebuild. + This test is specific for SQLite. + """ engine = create_engine(recorder_db_url) session_maker = scoped_session(sessionmaker(bind=engine, future=True)) with session_scope(session=session_maker()) as session: @@ -667,14 +671,15 @@ def test_rebuild_sqlite_states_table_missing_fails( engine.dispose() +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") def test_rebuild_sqlite_states_table_extra_columns( recorder_db_url: str, caplog: pytest.LogCaptureFixture ) -> None: - """Test handling extra columns when rebuilding the states table.""" - if not recorder_db_url.startswith("sqlite://"): - # This test is specific for SQLite - return + """Test handling extra columns when rebuilding the states table. + This test is specific for SQLite. + """ engine = create_engine(recorder_db_url) session_maker = scoped_session(sessionmaker(bind=engine, future=True)) with session_scope(session=session_maker()) as session: diff --git a/tests/components/recorder/test_migration_run_time_migrations_remember.py b/tests/components/recorder/test_migration_run_time_migrations_remember.py index 4f59edb097f..f3ade40d4af 100644 --- a/tests/components/recorder/test_migration_run_time_migrations_remember.py +++ b/tests/components/recorder/test_migration_run_time_migrations_remember.py @@ -62,6 +62,8 @@ def _create_engine_test(*args, **kwargs): return engine +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") @pytest.mark.parametrize("enable_migrate_context_ids", [True]) async def test_migration_changes_prevent_trying_to_migrate_again( async_setup_recorder_instance: RecorderInstanceGenerator, @@ -75,11 +77,9 @@ async def test_migration_changes_prevent_trying_to_migrate_again( 1. With schema 32 to populate the data 2. With current schema so the migration happens 3. With current schema to verify we do not have to query to see if the migration is done + + This test uses a test database between runs so its SQLite specific. WHY, this makes no sense.??? """ - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test uses a test database between runs so its - # SQLite specific - return config = { recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db"), diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index df5a6a77cfc..b21bbd36d28 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -193,16 +193,18 @@ async def test_purge_old_states( assert state_attributes.count() == 3 +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_purge_old_states_encouters_database_corruption( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, recorder_db_url: str, ) -> None: - """Test database image image is malformed while deleting old states.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test is specific for SQLite, wiping the database on error only happens - # with SQLite. - return + """Test database image image is malformed while deleting old states. + + This test is specific for SQLite, wiping the database on error only happens + with SQLite. + """ await async_setup_recorder_instance(hass) diff --git a/tests/components/recorder/test_purge_v32_schema.py b/tests/components/recorder/test_purge_v32_schema.py index 94ea2d51db3..a3b91ce54a9 100644 --- a/tests/components/recorder/test_purge_v32_schema.py +++ b/tests/components/recorder/test_purge_v32_schema.py @@ -155,16 +155,18 @@ async def test_purge_old_states( assert state_attributes.count() == 3 +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_purge_old_states_encouters_database_corruption( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, recorder_db_url: str, ) -> None: - """Test database image image is malformed while deleting old states.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test is specific for SQLite, wiping the database on error only happens - # with SQLite. - return + """Test database image image is malformed while deleting old states. + + This test is specific for SQLite, wiping the database on error only happens + with SQLite. + """ await async_setup_recorder_instance(hass) await async_attach_db_engine(hass) @@ -1097,14 +1099,17 @@ async def test_purge_can_mix_legacy_and_new_format( assert states_without_event_id.count() == 1 +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_purge_can_mix_legacy_and_new_format_with_detached_state( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, recorder_db_url: str, ) -> None: - """Test purging with legacy and new events with a detached state.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - return pytest.skip("This tests disables foreign key checks on SQLite") + """Test purging with legacy and new events with a detached state. + + This tests disables foreign key checks on SQLite. + """ instance = await async_setup_recorder_instance(hass) await async_attach_db_engine(hass) diff --git a/tests/components/recorder/test_system_health.py b/tests/components/recorder/test_system_health.py index fbcefa0b13e..0efaa82e5e5 100644 --- a/tests/components/recorder/test_system_health.py +++ b/tests/components/recorder/test_system_health.py @@ -15,13 +15,15 @@ from tests.common import get_system_health_info from tests.typing import RecorderInstanceGenerator +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_recorder_system_health( recorder_mock: Recorder, hass: HomeAssistant, recorder_db_url: str ) -> None: - """Test recorder system health.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test is specific for SQLite - return + """Test recorder system health. + + This test is specific for SQLite. + """ assert await async_setup_component(hass, "system_health", {}) await async_wait_recording_done(hass) @@ -100,15 +102,17 @@ async def test_recorder_system_health_db_url_missing_host( } +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_recorder_system_health_crashed_recorder_runs_table( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, recorder_db_url: str, ) -> None: - """Test recorder system health with crashed recorder runs table.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test is specific for SQLite - return + """Test recorder system health with crashed recorder runs table. + + This test is specific for SQLite. + """ with patch( "homeassistant.components.recorder.table_managers.recorder_runs.RecorderRunsManager.load_from_db" diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 089d1938227..da8dfd61a17 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -719,14 +719,15 @@ async def test_no_issue_for_mariadb_with_MDEV_25020( assert database_engine.optimizer.slow_range_in_select is False +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_basic_sanity_check( hass: HomeAssistant, setup_recorder: None, recorder_db_url: str ) -> None: - """Test the basic sanity checks with a missing table.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test is specific for SQLite - return + """Test the basic sanity checks with a missing table. + This test is specific for SQLite. + """ cursor = util.get_instance(hass).engine.raw_connection().cursor() assert util.basic_sanity_check(cursor) is True @@ -737,17 +738,18 @@ async def test_basic_sanity_check( util.basic_sanity_check(cursor) +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_combined_checks( hass: HomeAssistant, setup_recorder: None, caplog: pytest.LogCaptureFixture, recorder_db_url: str, ) -> None: - """Run Checks on the open database.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test is specific for SQLite - return + """Run Checks on the open database. + This test is specific for SQLite. + """ instance = util.get_instance(hass) instance.db_retry_wait = 0 @@ -829,14 +831,15 @@ async def test_end_incomplete_runs( assert "Ended unfinished session" in caplog.text +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") async def test_periodic_db_cleanups( hass: HomeAssistant, setup_recorder: None, recorder_db_url: str ) -> None: - """Test periodic db cleanups.""" - if recorder_db_url.startswith(("mysql://", "postgresql://")): - # This test is specific for SQLite - return + """Test periodic db cleanups. + This test is specific for SQLite. + """ with patch.object(util.get_instance(hass).engine, "connect") as connect_mock: util.periodic_db_cleanups(util.get_instance(hass)) From 595e688c56d30d375893c1698ec781a23db90621 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 15:35:02 -0700 Subject: [PATCH 0694/2411] Fix blocking I/O in event loop in kira test (#121127) --- tests/components/kira/test_init.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/kira/test_init.py b/tests/components/kira/test_init.py index e57519667ce..8e6c70c83a4 100644 --- a/tests/components/kira/test_init.py +++ b/tests/components/kira/test_init.py @@ -1,6 +1,7 @@ """The tests for Kira.""" import os +from pathlib import Path import shutil import tempfile from unittest.mock import patch @@ -76,10 +77,9 @@ async def test_kira_creates_codes(work_dir) -> None: assert os.path.exists(code_path), "Kira component didn't create codes file" -async def test_load_codes(work_dir) -> None: +async def test_load_codes(hass: HomeAssistant, work_dir) -> None: """Kira should ignore invalid codes.""" code_path = os.path.join(work_dir, "codes.yaml") - with open(code_path, "w", encoding="utf8") as code_file: - code_file.write(KIRA_CODES) + await hass.async_add_executor_job(Path(code_path).write_text, KIRA_CODES) res = kira.load_codes(code_path) assert len(res) == 1, "Expected exactly 1 valid Kira code" From 84d8bc711d03a3d289ae39922f8b1ec47d5bafa2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 15:35:23 -0700 Subject: [PATCH 0695/2411] Fix blocking I/O in event loop in google_assistant test (#121126) found by ruff in #120799 --- tests/components/google_assistant/test_http.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index b041f69828f..273aac1559e 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -4,6 +4,7 @@ from datetime import UTC, datetime, timedelta from http import HTTPStatus import json import os +from pathlib import Path from typing import Any from unittest.mock import ANY, patch from uuid import uuid4 @@ -655,9 +656,7 @@ async def test_async_get_users( ) path = hass.config.config_dir / ".storage" / GoogleConfigStore._STORAGE_KEY os.makedirs(os.path.dirname(path), exist_ok=True) - with open(path, "w", encoding="utf8") as f: - f.write(store_data) - + await hass.async_add_executor_job(Path(path).write_text, store_data) assert await async_get_users(hass) == expected_users await hass.async_stop() From cfef09d653c95036addff83018b7c02cd3978a73 Mon Sep 17 00:00:00 2001 From: MeIchthys Date: Wed, 3 Jul 2024 19:08:38 -0400 Subject: [PATCH 0696/2411] Bump nextcloudmonitor to 1.5.1 (#120356) --- homeassistant/components/nextcloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextcloud/manifest.json b/homeassistant/components/nextcloud/manifest.json index 64fda8c18ba..ca35a6c3fd8 100644 --- a/homeassistant/components/nextcloud/manifest.json +++ b/homeassistant/components/nextcloud/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nextcloud", "iot_class": "cloud_polling", - "requirements": ["nextcloudmonitor==1.5.0"] + "requirements": ["nextcloudmonitor==1.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1a7a1f3bbdf..00444556d22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1398,7 +1398,7 @@ neurio==0.3.1 nexia==2.0.8 # homeassistant.components.nextcloud -nextcloudmonitor==1.5.0 +nextcloudmonitor==1.5.1 # homeassistant.components.discord nextcord==2.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73a85e77973..829acca9591 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1137,7 +1137,7 @@ nettigo-air-monitor==3.2.0 nexia==2.0.8 # homeassistant.components.nextcloud -nextcloudmonitor==1.5.0 +nextcloudmonitor==1.5.1 # homeassistant.components.discord nextcord==2.6.0 From b6a23fad3537e0baa026b2a7eb5bff8a4246780b Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 4 Jul 2024 01:18:28 +0200 Subject: [PATCH 0697/2411] Fix broken pathlib import in august integration (#121135) --- homeassistant/components/august/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index eec794896f6..53aa3cdffd8 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -2,10 +2,10 @@ from __future__ import annotations +from pathlib import Path from typing import cast from aiohttp import ClientResponseError -from path import Path from yalexs.exceptions import AugustApiAIOHTTPError from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from yalexs.manager.gateway import Config as YaleXSConfig From cc2782edc789031eeb9221019129caa60952adba Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Jul 2024 01:38:01 +0200 Subject: [PATCH 0698/2411] Use async_test_recorder fixture in recorder auto_repairs tests (#121125) --- .../auto_repairs/events/test_schema.py | 25 ++-- .../auto_repairs/states/test_schema.py | 31 ++-- .../statistics/test_duplicates.py | 42 +++--- .../auto_repairs/statistics/test_schema.py | 25 ++-- .../recorder/auto_repairs/test_schema.py | 134 ++++++++---------- 5 files changed, 133 insertions(+), 124 deletions(-) diff --git a/tests/components/recorder/auto_repairs/events/test_schema.py b/tests/components/recorder/auto_repairs/events/test_schema.py index e3b2638eded..cae181a6270 100644 --- a/tests/components/recorder/auto_repairs/events/test_schema.py +++ b/tests/components/recorder/auto_repairs/events/test_schema.py @@ -11,11 +11,18 @@ from ...common import async_wait_recording_done from tests.typing import RecorderInstanceGenerator +@pytest.fixture +async def mock_recorder_before_hass( + async_test_recorder: RecorderInstanceGenerator, +) -> None: + """Set up recorder.""" + + @pytest.mark.parametrize("enable_schema_validation", [True]) @pytest.mark.parametrize("db_engine", ["mysql", "postgresql"]) async def test_validate_db_schema_fix_float_issue( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -33,8 +40,8 @@ async def test_validate_db_schema_fix_float_issue( "homeassistant.components.recorder.migration._modify_columns" ) as modify_columns_mock, ): - await async_setup_recorder_instance(hass) - await async_wait_recording_done(hass) + async with async_test_recorder(hass): + await async_wait_recording_done(hass) assert "Schema validation failed" not in caplog.text assert ( @@ -50,8 +57,8 @@ async def test_validate_db_schema_fix_float_issue( @pytest.mark.parametrize("enable_schema_validation", [True]) @pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_utf8_issue_event_data( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -66,8 +73,8 @@ async def test_validate_db_schema_fix_utf8_issue_event_data( return_value={"event_data.4-byte UTF-8"}, ), ): - await async_setup_recorder_instance(hass) - await async_wait_recording_done(hass) + async with async_test_recorder(hass): + await async_wait_recording_done(hass) assert "Schema validation failed" not in caplog.text assert ( @@ -83,8 +90,8 @@ async def test_validate_db_schema_fix_utf8_issue_event_data( @pytest.mark.parametrize("enable_schema_validation", [True]) @pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_collation_issue( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -99,8 +106,8 @@ async def test_validate_db_schema_fix_collation_issue( return_value={"events.utf8mb4_unicode_ci"}, ), ): - await async_setup_recorder_instance(hass) - await async_wait_recording_done(hass) + async with async_test_recorder(hass): + await async_wait_recording_done(hass) assert "Schema validation failed" not in caplog.text assert ( diff --git a/tests/components/recorder/auto_repairs/states/test_schema.py b/tests/components/recorder/auto_repairs/states/test_schema.py index 58910a4441a..915ac1f3500 100644 --- a/tests/components/recorder/auto_repairs/states/test_schema.py +++ b/tests/components/recorder/auto_repairs/states/test_schema.py @@ -11,11 +11,18 @@ from ...common import async_wait_recording_done from tests.typing import RecorderInstanceGenerator +@pytest.fixture +async def mock_recorder_before_hass( + async_test_recorder: RecorderInstanceGenerator, +) -> None: + """Set up recorder.""" + + @pytest.mark.parametrize("enable_schema_validation", [True]) @pytest.mark.parametrize("db_engine", ["mysql", "postgresql"]) async def test_validate_db_schema_fix_float_issue( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -33,8 +40,8 @@ async def test_validate_db_schema_fix_float_issue( "homeassistant.components.recorder.migration._modify_columns" ) as modify_columns_mock, ): - await async_setup_recorder_instance(hass) - await async_wait_recording_done(hass) + async with async_test_recorder(hass): + await async_wait_recording_done(hass) assert "Schema validation failed" not in caplog.text assert ( @@ -52,8 +59,8 @@ async def test_validate_db_schema_fix_float_issue( @pytest.mark.parametrize("enable_schema_validation", [True]) @pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_utf8_issue_states( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -68,8 +75,8 @@ async def test_validate_db_schema_fix_utf8_issue_states( return_value={"states.4-byte UTF-8"}, ), ): - await async_setup_recorder_instance(hass) - await async_wait_recording_done(hass) + async with async_test_recorder(hass): + await async_wait_recording_done(hass) assert "Schema validation failed" not in caplog.text assert ( @@ -84,8 +91,8 @@ async def test_validate_db_schema_fix_utf8_issue_states( @pytest.mark.parametrize("enable_schema_validation", [True]) @pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_utf8_issue_state_attributes( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -100,8 +107,8 @@ async def test_validate_db_schema_fix_utf8_issue_state_attributes( return_value={"state_attributes.4-byte UTF-8"}, ), ): - await async_setup_recorder_instance(hass) - await async_wait_recording_done(hass) + async with async_test_recorder(hass): + await async_wait_recording_done(hass) assert "Schema validation failed" not in caplog.text assert ( @@ -117,8 +124,8 @@ async def test_validate_db_schema_fix_utf8_issue_state_attributes( @pytest.mark.parametrize("enable_schema_validation", [True]) @pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_collation_issue( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -133,8 +140,8 @@ async def test_validate_db_schema_fix_collation_issue( return_value={"states.utf8mb4_unicode_ci"}, ), ): - await async_setup_recorder_instance(hass) - await async_wait_recording_done(hass) + async with async_test_recorder(hass): + await async_wait_recording_done(hass) assert "Schema validation failed" not in caplog.text assert ( diff --git a/tests/components/recorder/auto_repairs/statistics/test_duplicates.py b/tests/components/recorder/auto_repairs/statistics/test_duplicates.py index 457f180bb91..67a67226cd6 100644 --- a/tests/components/recorder/auto_repairs/statistics/test_duplicates.py +++ b/tests/components/recorder/auto_repairs/statistics/test_duplicates.py @@ -10,7 +10,7 @@ from sqlalchemy import create_engine from sqlalchemy.orm import Session from homeassistant.components import recorder -from homeassistant.components.recorder import Recorder, statistics +from homeassistant.components.recorder import statistics from homeassistant.components.recorder.auto_repairs.statistics.duplicates import ( delete_statistics_duplicates, delete_statistics_meta_duplicates, @@ -34,15 +34,10 @@ async def mock_recorder_before_hass( """Set up recorder.""" -@pytest.fixture -def setup_recorder(recorder_mock: Recorder) -> None: - """Set up recorder.""" - - +@pytest.mark.usefixtures("recorder_mock") async def test_delete_duplicates_no_duplicates( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - setup_recorder: None, ) -> None: """Test removal of duplicated statistics.""" await async_wait_recording_done(hass) @@ -54,10 +49,10 @@ async def test_delete_duplicates_no_duplicates( assert "Found duplicated" not in caplog.text +@pytest.mark.usefixtures("recorder_mock") async def test_duplicate_statistics_handle_integrity_error( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - setup_recorder: None, ) -> None: """Test the recorder does not blow up if statistics is duplicated.""" await async_wait_recording_done(hass) @@ -139,7 +134,7 @@ def _create_engine_28(*args, **kwargs): async def test_delete_metadata_duplicates( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, tmp_path: Path, ) -> None: @@ -205,8 +200,10 @@ async def test_delete_metadata_duplicates( new=_create_engine_28, ), ): - async with async_test_home_assistant() as hass: - await async_setup_recorder_instance(hass, {"db_url": dburl}) + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass, {"db_url": dburl}), + ): await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -225,8 +222,10 @@ async def test_delete_metadata_duplicates( await hass.async_stop() # Test that the duplicates are removed during migration from schema 28 - async with async_test_home_assistant() as hass: - await async_setup_recorder_instance(hass, {"db_url": dburl}) + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass, {"db_url": dburl}), + ): await hass.async_start() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -244,7 +243,7 @@ async def test_delete_metadata_duplicates( async def test_delete_metadata_duplicates_many( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, tmp_path: Path, ) -> None: @@ -322,8 +321,10 @@ async def test_delete_metadata_duplicates_many( new=_create_engine_28, ), ): - async with async_test_home_assistant() as hass: - await async_setup_recorder_instance(hass, {"db_url": dburl}) + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass, {"db_url": dburl}), + ): await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -333,8 +334,10 @@ async def test_delete_metadata_duplicates_many( await hass.async_stop() # Test that the duplicates are removed during migration from schema 28 - async with async_test_home_assistant() as hass: - await async_setup_recorder_instance(hass, {"db_url": dburl}) + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass, {"db_url": dburl}), + ): await hass.async_start() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -353,8 +356,9 @@ async def test_delete_metadata_duplicates_many( await hass.async_stop() +@pytest.mark.usefixtures("recorder_mock") async def test_delete_metadata_duplicates_no_duplicates( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, setup_recorder: None + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test removal of duplicated statistics.""" await async_wait_recording_done(hass) diff --git a/tests/components/recorder/auto_repairs/statistics/test_schema.py b/tests/components/recorder/auto_repairs/statistics/test_schema.py index f4e1d74aadf..34a075afbc7 100644 --- a/tests/components/recorder/auto_repairs/statistics/test_schema.py +++ b/tests/components/recorder/auto_repairs/statistics/test_schema.py @@ -11,11 +11,18 @@ from ...common import async_wait_recording_done from tests.typing import RecorderInstanceGenerator +@pytest.fixture +async def mock_recorder_before_hass( + async_test_recorder: RecorderInstanceGenerator, +) -> None: + """Set up recorder.""" + + @pytest.mark.parametrize("db_engine", ["mysql"]) @pytest.mark.parametrize("enable_schema_validation", [True]) async def test_validate_db_schema_fix_utf8_issue( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -30,8 +37,8 @@ async def test_validate_db_schema_fix_utf8_issue( return_value={"statistics_meta.4-byte UTF-8"}, ), ): - await async_setup_recorder_instance(hass) - await async_wait_recording_done(hass) + async with async_test_recorder(hass): + await async_wait_recording_done(hass) assert "Schema validation failed" not in caplog.text assert ( @@ -48,8 +55,8 @@ async def test_validate_db_schema_fix_utf8_issue( @pytest.mark.parametrize("table", ["statistics_short_term", "statistics"]) @pytest.mark.parametrize("db_engine", ["mysql", "postgresql"]) async def test_validate_db_schema_fix_float_issue( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, table: str, db_engine: str, @@ -68,8 +75,8 @@ async def test_validate_db_schema_fix_float_issue( "homeassistant.components.recorder.migration._modify_columns" ) as modify_columns_mock, ): - await async_setup_recorder_instance(hass) - await async_wait_recording_done(hass) + async with async_test_recorder(hass): + await async_wait_recording_done(hass) assert "Schema validation failed" not in caplog.text assert ( @@ -92,8 +99,8 @@ async def test_validate_db_schema_fix_float_issue( @pytest.mark.parametrize("enable_schema_validation", [True]) @pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_collation_issue( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, recorder_dialect_name: None, db_engine: str, @@ -108,8 +115,8 @@ async def test_validate_db_schema_fix_collation_issue( return_value={"statistics.utf8mb4_unicode_ci"}, ), ): - await async_setup_recorder_instance(hass) - await async_wait_recording_done(hass) + async with async_test_recorder(hass): + await async_wait_recording_done(hass) assert "Schema validation failed" not in caplog.text assert ( diff --git a/tests/components/recorder/auto_repairs/test_schema.py b/tests/components/recorder/auto_repairs/test_schema.py index bdc01447096..3d623b6bf8a 100644 --- a/tests/components/recorder/auto_repairs/test_schema.py +++ b/tests/components/recorder/auto_repairs/test_schema.py @@ -3,6 +3,7 @@ import pytest from sqlalchemy import text +from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.auto_repairs.schema import ( correct_db_schema_precision, correct_db_schema_utf8, @@ -12,7 +13,7 @@ from homeassistant.components.recorder.auto_repairs.schema import ( ) from homeassistant.components.recorder.db_schema import States from homeassistant.components.recorder.migration import _modify_columns -from homeassistant.components.recorder.util import get_instance, session_scope +from homeassistant.components.recorder.util import session_scope from homeassistant.core import HomeAssistant from ..common import async_wait_recording_done @@ -20,11 +21,18 @@ from ..common import async_wait_recording_done from tests.typing import RecorderInstanceGenerator +@pytest.fixture +async def mock_recorder_before_hass( + async_test_recorder: RecorderInstanceGenerator, +) -> None: + """Set up recorder.""" + + @pytest.mark.parametrize("enable_schema_validation", [True]) @pytest.mark.parametrize("db_engine", ["mysql", "postgresql"]) async def test_validate_db_schema( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + recorder_mock: Recorder, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -33,7 +41,6 @@ async def test_validate_db_schema( Note: The test uses SQLite, the purpose is only to exercise the code. """ - await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) assert "Schema validation failed" not in caplog.text assert "Detected statistics schema errors" not in caplog.text @@ -43,17 +50,14 @@ async def test_validate_db_schema( @pytest.mark.skip_on_db_engine(["postgresql", "sqlite"]) @pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_fix_utf8_issue_good_schema( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, - recorder_db_url: str, + recorder_mock: Recorder, caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema with MySQL when the schema is correct.""" - await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) - instance = get_instance(hass) - schema_errors = await instance.async_add_executor_job( - validate_table_schema_supports_utf8, instance, States, (States.state,) + schema_errors = await recorder_mock.async_add_executor_job( + validate_table_schema_supports_utf8, recorder_mock, States, (States.state,) ) assert schema_errors == set() @@ -61,16 +65,13 @@ async def test_validate_db_schema_fix_utf8_issue_good_schema( @pytest.mark.skip_on_db_engine(["postgresql", "sqlite"]) @pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_fix_utf8_issue_with_broken_schema( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, - recorder_db_url: str, + recorder_mock: Recorder, caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema with MySQL when the schema is broken and repairing it.""" - await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) - instance = get_instance(hass) - session_maker = instance.get_session + session_maker = recorder_mock.get_session def _break_states_schema(): with session_scope(session=session_maker()) as session: @@ -82,20 +83,20 @@ async def test_validate_db_schema_fix_utf8_issue_with_broken_schema( ) ) - await instance.async_add_executor_job(_break_states_schema) - schema_errors = await instance.async_add_executor_job( - validate_table_schema_supports_utf8, instance, States, (States.state,) + await recorder_mock.async_add_executor_job(_break_states_schema) + schema_errors = await recorder_mock.async_add_executor_job( + validate_table_schema_supports_utf8, recorder_mock, States, (States.state,) ) assert schema_errors == {"states.4-byte UTF-8"} # Now repair the schema - await instance.async_add_executor_job( - correct_db_schema_utf8, instance, States, schema_errors + await recorder_mock.async_add_executor_job( + correct_db_schema_utf8, recorder_mock, States, schema_errors ) # Now validate the schema again - schema_errors = await instance.async_add_executor_job( - validate_table_schema_supports_utf8, instance, States, ("state",) + schema_errors = await recorder_mock.async_add_executor_job( + validate_table_schema_supports_utf8, recorder_mock, States, ("state",) ) assert schema_errors == set() @@ -103,16 +104,13 @@ async def test_validate_db_schema_fix_utf8_issue_with_broken_schema( @pytest.mark.skip_on_db_engine(["postgresql", "sqlite"]) @pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_fix_incorrect_collation( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, - recorder_db_url: str, + recorder_mock: Recorder, caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema with MySQL when the collation is incorrect.""" - await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) - instance = get_instance(hass) - session_maker = instance.get_session + session_maker = recorder_mock.get_session def _break_states_schema(): with session_scope(session=session_maker()) as session: @@ -123,20 +121,20 @@ async def test_validate_db_schema_fix_incorrect_collation( ) ) - await instance.async_add_executor_job(_break_states_schema) - schema_errors = await instance.async_add_executor_job( - validate_table_schema_has_correct_collation, instance, States + await recorder_mock.async_add_executor_job(_break_states_schema) + schema_errors = await recorder_mock.async_add_executor_job( + validate_table_schema_has_correct_collation, recorder_mock, States ) assert schema_errors == {"states.utf8mb4_unicode_ci"} # Now repair the schema - await instance.async_add_executor_job( - correct_db_schema_utf8, instance, States, schema_errors + await recorder_mock.async_add_executor_job( + correct_db_schema_utf8, recorder_mock, States, schema_errors ) # Now validate the schema again - schema_errors = await instance.async_add_executor_job( - validate_table_schema_has_correct_collation, instance, States + schema_errors = await recorder_mock.async_add_executor_job( + validate_table_schema_has_correct_collation, recorder_mock, States ) assert schema_errors == set() @@ -144,18 +142,15 @@ async def test_validate_db_schema_fix_incorrect_collation( @pytest.mark.skip_on_db_engine(["postgresql", "sqlite"]) @pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_precision_correct_collation( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, - recorder_db_url: str, + recorder_mock: Recorder, caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema when the schema is correct with the correct collation.""" - await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) - instance = get_instance(hass) - schema_errors = await instance.async_add_executor_job( + schema_errors = await recorder_mock.async_add_executor_job( validate_table_schema_has_correct_collation, - instance, + recorder_mock, States, ) assert schema_errors == set() @@ -164,16 +159,13 @@ async def test_validate_db_schema_precision_correct_collation( @pytest.mark.skip_on_db_engine(["postgresql", "sqlite"]) @pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_fix_utf8_issue_with_broken_schema_unrepairable( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, - recorder_db_url: str, + recorder_mock: Recorder, caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema with MySQL when the schema is broken and cannot be repaired.""" - await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) - instance = get_instance(hass) - session_maker = instance.get_session + session_maker = recorder_mock.get_session def _break_states_schema(): with session_scope(session=session_maker()) as session: @@ -186,16 +178,16 @@ async def test_validate_db_schema_fix_utf8_issue_with_broken_schema_unrepairable ) _modify_columns( session_maker, - instance.engine, + recorder_mock.engine, "states", [ "entity_id VARCHAR(255) NOT NULL", ], ) - await instance.async_add_executor_job(_break_states_schema) - schema_errors = await instance.async_add_executor_job( - validate_table_schema_supports_utf8, instance, States, ("state",) + await recorder_mock.async_add_executor_job(_break_states_schema) + schema_errors = await recorder_mock.async_add_executor_job( + validate_table_schema_supports_utf8, recorder_mock, States, ("state",) ) assert schema_errors == set() assert "Error when validating DB schema" in caplog.text @@ -204,18 +196,15 @@ async def test_validate_db_schema_fix_utf8_issue_with_broken_schema_unrepairable @pytest.mark.skip_on_db_engine(["sqlite"]) @pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_precision_good_schema( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, - recorder_db_url: str, + recorder_mock: Recorder, caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema when the schema is correct.""" - await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) - instance = get_instance(hass) - schema_errors = await instance.async_add_executor_job( + schema_errors = await recorder_mock.async_add_executor_job( validate_db_schema_precision, - instance, + recorder_mock, States, ) assert schema_errors == set() @@ -224,21 +213,18 @@ async def test_validate_db_schema_precision_good_schema( @pytest.mark.skip_on_db_engine(["sqlite"]) @pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_precision_with_broken_schema( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, - recorder_db_url: str, + recorder_mock: Recorder, caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema when the schema is broken and than repair it.""" - await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) - instance = get_instance(hass) - session_maker = instance.get_session + session_maker = recorder_mock.get_session def _break_states_schema(): _modify_columns( session_maker, - instance.engine, + recorder_mock.engine, "states", [ "last_updated_ts FLOAT(4)", @@ -246,23 +232,23 @@ async def test_validate_db_schema_precision_with_broken_schema( ], ) - await instance.async_add_executor_job(_break_states_schema) - schema_errors = await instance.async_add_executor_job( + await recorder_mock.async_add_executor_job(_break_states_schema) + schema_errors = await recorder_mock.async_add_executor_job( validate_db_schema_precision, - instance, + recorder_mock, States, ) assert schema_errors == {"states.double precision"} # Now repair the schema - await instance.async_add_executor_job( - correct_db_schema_precision, instance, States, schema_errors + await recorder_mock.async_add_executor_job( + correct_db_schema_precision, recorder_mock, States, schema_errors ) # Now validate the schema again - schema_errors = await instance.async_add_executor_job( + schema_errors = await recorder_mock.async_add_executor_job( validate_db_schema_precision, - instance, + recorder_mock, States, ) assert schema_errors == set() @@ -271,21 +257,19 @@ async def test_validate_db_schema_precision_with_broken_schema( @pytest.mark.skip_on_db_engine(["postgresql", "sqlite"]) @pytest.mark.usefixtures("skip_by_db_engine") async def test_validate_db_schema_precision_with_unrepairable_broken_schema( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + recorder_mock: Recorder, recorder_db_url: str, caplog: pytest.LogCaptureFixture, ) -> None: """Test validating DB schema when the schema is broken and cannot be repaired.""" - await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) - instance = get_instance(hass) - session_maker = instance.get_session + session_maker = recorder_mock.get_session def _break_states_schema(): _modify_columns( session_maker, - instance.engine, + recorder_mock.engine, "states", [ "state VARCHAR(255) NOT NULL", @@ -294,10 +278,10 @@ async def test_validate_db_schema_precision_with_unrepairable_broken_schema( ], ) - await instance.async_add_executor_job(_break_states_schema) - schema_errors = await instance.async_add_executor_job( + await recorder_mock.async_add_executor_job(_break_states_schema) + schema_errors = await recorder_mock.async_add_executor_job( validate_db_schema_precision, - instance, + recorder_mock, States, ) assert "Error when validating DB schema" in caplog.text From c59fc4e3c7a2ef7bb67476f7f3dd33d5771c0bd0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 23:08:53 -0500 Subject: [PATCH 0699/2411] Fix blocking I/O in media_extractor tests (#121139) --- tests/components/media_extractor/test_init.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/components/media_extractor/test_init.py b/tests/components/media_extractor/test_init.py index 9708e1c2ad6..bc80e063697 100644 --- a/tests/components/media_extractor/test_init.py +++ b/tests/components/media_extractor/test_init.py @@ -290,16 +290,19 @@ async def test_cookiefile_detection( cookies_dir = os.path.join(hass.config.config_dir, "media_extractor") cookies_file = os.path.join(cookies_dir, "cookies.txt") - if not os.path.exists(cookies_dir): - os.makedirs(cookies_dir) + def _write_cookies_file() -> None: + if not os.path.exists(cookies_dir): + os.makedirs(cookies_dir) - with open(cookies_file, "w+", encoding="utf-8") as f: - f.write( - """# Netscape HTTP Cookie File + with open(cookies_file, "w+", encoding="utf-8") as f: + f.write( + """# Netscape HTTP Cookie File - .youtube.com TRUE / TRUE 1701708706 GPS 1 - """ - ) + .youtube.com TRUE / TRUE 1701708706 GPS 1 + """ + ) + + await hass.async_add_executor_job(_write_cookies_file) await hass.services.async_call( DOMAIN, @@ -314,7 +317,7 @@ async def test_cookiefile_detection( assert "Media extractor loaded cookies file" in caplog.text - os.remove(cookies_file) + await hass.async_add_executor_job(os.remove, cookies_file) await hass.services.async_call( DOMAIN, From 70020421192e23b6eb347ee1e7a268c71b3d225f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 23:13:13 -0500 Subject: [PATCH 0700/2411] Fix blocking I/O in mqtt tests (#121140) --- tests/components/mqtt/test_discovery.py | 31 ++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 8c51e295998..58de3c53c52 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1363,24 +1363,29 @@ EXCLUDED_MODULES = { async def test_missing_discover_abbreviations( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: """Check MQTT platforms for missing abbreviations.""" await mqtt_mock_entry() - missing = [] + missing: list[str] = [] regex = re.compile(r"(CONF_[a-zA-Z\d_]*) *= *[\'\"]([a-zA-Z\d_]*)[\'\"]") - for fil in Path(mqtt.__file__).parent.rglob("*.py"): - if fil.name in EXCLUDED_MODULES: - continue - with open(fil, encoding="utf-8") as file: - matches = re.findall(regex, file.read()) - missing.extend( - f"{fil}: no abbreviation for {match[1]} ({match[0]})" - for match in matches - if match[1] not in ABBREVIATIONS.values() - and match[1] not in DEVICE_ABBREVIATIONS.values() - and match[0] not in ABBREVIATIONS_WHITE_LIST - ) + + def _add_missing(): + for fil in Path(mqtt.__file__).parent.rglob("*.py"): + if fil.name in EXCLUDED_MODULES: + continue + with open(fil, encoding="utf-8") as file: + matches = re.findall(regex, file.read()) + missing.extend( + f"{fil}: no abbreviation for {match[1]} ({match[0]})" + for match in matches + if match[1] not in ABBREVIATIONS.values() + and match[1] not in DEVICE_ABBREVIATIONS.values() + and match[0] not in ABBREVIATIONS_WHITE_LIST + ) + + await hass.async_add_executor_job(_add_missing) assert not missing From a4d4fc6827cf924f82e3565905cf52d834c7b625 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 23:15:26 -0500 Subject: [PATCH 0701/2411] Fix blocking I/O in stream tests (#121142) --- tests/components/stream/test_recorder.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 515f3fff82d..c2229219422 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -305,7 +305,5 @@ async def test_record_stream_rotate(hass: HomeAssistant, filename, h264_video) - # Assert assert os.path.exists(filename) - with open(filename, "rb") as rotated_mp4: - assert_mp4_has_transform_matrix( - rotated_mp4.read(), stream.dynamic_stream_settings.orientation - ) + data = await hass.async_add_executor_job(Path(filename).read_bytes) + assert_mp4_has_transform_matrix(data, stream.dynamic_stream_settings.orientation) From e8ef2c28228894785389406399ec11a072c18c10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 23:21:01 -0500 Subject: [PATCH 0702/2411] Fix blocking I/O in tts tests (#121143) --- tests/components/tts/test_init.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index e0354170b06..0a7813415d4 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -1054,9 +1054,7 @@ async def test_setup_legacy_cache_dir( mock_tts_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_test.mp3" ) - with open(cache_file, "wb") as voice_file: - voice_file.write(tts_data) - + await hass.async_add_executor_job(Path(cache_file).write_bytes, tts_data) await mock_setup(hass, mock_provider) await hass.services.async_call( @@ -1090,9 +1088,7 @@ async def test_setup_cache_dir( "42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_tts.test.mp3" ) - with open(cache_file, "wb") as voice_file: - voice_file.write(tts_data) - + await hass.async_add_executor_job(Path(cache_file).write_bytes, tts_data) await mock_config_entry_setup(hass, mock_tts_entity) await hass.services.async_call( @@ -1195,9 +1191,7 @@ async def test_load_cache_legacy_retrieve_without_mem_cache( mock_tts_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_test.mp3" ) - with open(cache_file, "wb") as voice_file: - voice_file.write(tts_data) - + await hass.async_add_executor_job(Path(cache_file).write_bytes, tts_data) await mock_setup(hass, mock_provider) client = await hass_client() @@ -1221,9 +1215,7 @@ async def test_load_cache_retrieve_without_mem_cache( "42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_tts.test.mp3" ) - with open(cache_file, "wb") as voice_file: - voice_file.write(tts_data) - + await hass.async_add_executor_job(Path(cache_file).write_bytes, tts_data) await mock_config_entry_setup(hass, mock_tts_entity) client = await hass_client() From 1144e23d8d96543050291cafdc858d8fc82cab24 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 23:21:30 -0500 Subject: [PATCH 0703/2411] Fix blocking I/O in config tests (#121144) --- tests/test_config.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index ae6cbb3ba5e..e15dcf31726 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,6 +7,7 @@ import contextlib import copy import logging import os +from pathlib import Path from typing import Any from unittest import mock from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -412,11 +413,10 @@ async def test_ensure_config_exists_creates_config(hass: HomeAssistant) -> None: async def test_ensure_config_exists_uses_existing_config(hass: HomeAssistant) -> None: """Test that calling ensure_config_exists uses existing config.""" - create_file(YAML_PATH) + await hass.async_add_executor_job(create_file, YAML_PATH) await config_util.async_ensure_config_exists(hass) - with open(YAML_PATH, encoding="utf8") as fp: - content = fp.read() + content = await hass.async_add_executor_job(Path(YAML_PATH).read_text) # File created with create_file are empty assert content == "" @@ -424,12 +424,11 @@ async def test_ensure_config_exists_uses_existing_config(hass: HomeAssistant) -> async def test_ensure_existing_files_is_not_overwritten(hass: HomeAssistant) -> None: """Test that calling async_create_default_config does not overwrite existing files.""" - create_file(SECRET_PATH) + await hass.async_add_executor_job(create_file, SECRET_PATH) await config_util.async_create_default_config(hass) - with open(SECRET_PATH, encoding="utf8") as fp: - content = fp.read() + content = await hass.async_add_executor_job(Path(SECRET_PATH).read_text) # File created with create_file are empty assert content == "" From 3dbab1a58031afd2720fb2e94e9579436f9b6994 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jul 2024 23:51:05 -0500 Subject: [PATCH 0704/2411] Bump inkbird-ble to 0.5.8 (#121134) --- homeassistant/components/inkbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index fb74d1c565a..c1922004317 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -28,5 +28,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/inkbird", "iot_class": "local_push", - "requirements": ["inkbird-ble==0.5.7"] + "requirements": ["inkbird-ble==0.5.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 00444556d22..4fc41f50cd9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.7 +inkbird-ble==0.5.8 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 829acca9591..7a95d86fef4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -951,7 +951,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.7 +inkbird-ble==0.5.8 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 From 3b023367d7121a522342f57ff266d73d903bcfe3 Mon Sep 17 00:00:00 2001 From: Christophe Gagnier Date: Thu, 4 Jul 2024 00:55:36 -0400 Subject: [PATCH 0705/2411] Update pytechnove to 1.3.1 (#121146) --- homeassistant/components/technove/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/technove/manifest.json b/homeassistant/components/technove/manifest.json index b4dec10c2ef..722aa4004e1 100644 --- a/homeassistant/components/technove/manifest.json +++ b/homeassistant/components/technove/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/technove", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["python-technove==1.3.0"], + "requirements": ["python-technove==1.3.1"], "zeroconf": ["_technove-stations._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 4fc41f50cd9..b6c15541eeb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2324,7 +2324,7 @@ python-songpal==0.16.2 python-tado==0.17.6 # homeassistant.components.technove -python-technove==1.3.0 +python-technove==1.3.1 # homeassistant.components.telegram_bot python-telegram-bot[socks]==21.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a95d86fef4..35aca7faced 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1818,7 +1818,7 @@ python-songpal==0.16.2 python-tado==0.17.6 # homeassistant.components.technove -python-technove==1.3.0 +python-technove==1.3.1 # homeassistant.components.telegram_bot python-telegram-bot[socks]==21.0.1 From 0e9acf2685b29128f391d007be7008c63648daf7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Jul 2024 00:42:49 -0500 Subject: [PATCH 0706/2411] Bump thermobeacon-ble to 0.7.0 (#121136) changelog: https://github.com/Bluetooth-Devices/thermobeacon-ble/compare/v0.6.2...v0.7.0 --- homeassistant/components/thermobeacon/manifest.json | 8 +++++++- homeassistant/generated/bluetooth.py | 9 +++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/thermobeacon/manifest.json b/homeassistant/components/thermobeacon/manifest.json index 29443acaa3d..ce6a3f71ef3 100644 --- a/homeassistant/components/thermobeacon/manifest.json +++ b/homeassistant/components/thermobeacon/manifest.json @@ -32,6 +32,12 @@ "manufacturer_data_start": [0], "connectable": false }, + { + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 48, + "manufacturer_data_start": [0], + "connectable": false + }, { "local_name": "ThermoBeacon", "connectable": false @@ -42,5 +48,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/thermobeacon", "iot_class": "local_push", - "requirements": ["thermobeacon-ble==0.6.2"] + "requirements": ["thermobeacon-ble==0.7.0"] } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 17461225851..33bd7456fa6 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -624,6 +624,15 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [ "manufacturer_id": 27, "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", }, + { + "connectable": False, + "domain": "thermobeacon", + "manufacturer_data_start": [ + 0, + ], + "manufacturer_id": 48, + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + }, { "connectable": False, "domain": "thermobeacon", diff --git a/requirements_all.txt b/requirements_all.txt index b6c15541eeb..711978dea7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2726,7 +2726,7 @@ tessie-api==0.0.9 # tf-models-official==2.5.0 # homeassistant.components.thermobeacon -thermobeacon-ble==0.6.2 +thermobeacon-ble==0.7.0 # homeassistant.components.thermopro thermopro-ble==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 35aca7faced..f810aa89cae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2121,7 +2121,7 @@ tesla-wall-connector==1.0.2 tessie-api==0.0.9 # homeassistant.components.thermobeacon -thermobeacon-ble==0.6.2 +thermobeacon-ble==0.7.0 # homeassistant.components.thermopro thermopro-ble==0.10.0 From 8a5b201d751c81429d769132b146be073bf264d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Jul 2024 00:43:46 -0500 Subject: [PATCH 0707/2411] Fix blocking I/O in event loop in core test (#121128) --- tests/test_core.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 5f824f9e53a..29e3bf89137 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -9,6 +9,7 @@ import functools import gc import logging import os +from pathlib import Path import re from tempfile import TemporaryDirectory import threading @@ -2009,8 +2010,9 @@ async def test_config_is_allowed_path() -> None: config.allowlist_external_dirs = {os.path.realpath(tmp_dir)} test_file = os.path.join(tmp_dir, "test.jpg") - with open(test_file, "w", encoding="utf8") as tmp_file: - tmp_file.write("test") + await asyncio.get_running_loop().run_in_executor( + None, Path(test_file).write_text, "test" + ) valid = [test_file, tmp_dir, os.path.join(tmp_dir, "notfound321")] for path in valid: From ac9c08f52c29cac1c2563a27421f9baadab9ff6e Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Thu, 4 Jul 2024 09:07:41 +0200 Subject: [PATCH 0708/2411] Add port mapping entry count sensor to upnp (#120263) Add port mapping entry count sensor --- homeassistant/components/upnp/const.py | 1 + homeassistant/components/upnp/device.py | 5 +++++ homeassistant/components/upnp/icons.json | 3 +++ homeassistant/components/upnp/sensor.py | 7 +++++++ homeassistant/components/upnp/strings.json | 3 +++ 5 files changed, 19 insertions(+) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index e7b44329546..5d68a83d4d4 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -21,6 +21,7 @@ TIMESTAMP = "timestamp" DATA_PACKETS = "packets" DATA_RATE_PACKETS_PER_SECOND = f"{DATA_PACKETS}/{UnitOfTime.SECONDS}" WAN_STATUS = "wan_status" +PORT_MAPPING_NUMBER_OF_ENTRIES_IPV4 = "port_mapping_number_of_entries" ROUTER_IP = "ip" ROUTER_UPTIME = "uptime" CONFIG_ENTRY_ST = "st" diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index e819a16f2d2..923d4828879 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -30,6 +30,7 @@ from .const import ( PACKETS_PER_SEC_SENT, PACKETS_RECEIVED, PACKETS_SENT, + PORT_MAPPING_NUMBER_OF_ENTRIES_IPV4, ROUTER_IP, ROUTER_UPTIME, TIMESTAMP, @@ -48,6 +49,7 @@ TYPE_STATE_ITEM_MAPPING = { ROUTER_IP: IgdStateItem.EXTERNAL_IP_ADDRESS, ROUTER_UPTIME: IgdStateItem.UPTIME, WAN_STATUS: IgdStateItem.CONNECTION_STATUS, + PORT_MAPPING_NUMBER_OF_ENTRIES_IPV4: IgdStateItem.PORT_MAPPING_NUMBER_OF_ENTRIES, } @@ -254,4 +256,7 @@ class Device: KIBIBYTES_PER_SEC_SENT: igd_state.kibibytes_per_sec_sent, PACKETS_PER_SEC_RECEIVED: igd_state.packets_per_sec_received, PACKETS_PER_SEC_SENT: igd_state.packets_per_sec_sent, + PORT_MAPPING_NUMBER_OF_ENTRIES_IPV4: get_value( + igd_state.port_mapping_number_of_entries + ), } diff --git a/homeassistant/components/upnp/icons.json b/homeassistant/components/upnp/icons.json index 1d4ebaf183d..b6451f0fca8 100644 --- a/homeassistant/components/upnp/icons.json +++ b/homeassistant/components/upnp/icons.json @@ -33,6 +33,9 @@ }, "packet_upload_speed": { "default": "mdi:server-network" + }, + "port_mapping_number_of_entries_ipv4": { + "default": "mdi:server-network" } } } diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index d72dce55eab..d6da50c877d 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -33,6 +33,7 @@ from .const import ( PACKETS_PER_SEC_SENT, PACKETS_RECEIVED, PACKETS_SENT, + PORT_MAPPING_NUMBER_OF_ENTRIES_IPV4, ROUTER_IP, ROUTER_UPTIME, WAN_STATUS, @@ -99,6 +100,12 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), + UpnpSensorEntityDescription( + key=PORT_MAPPING_NUMBER_OF_ENTRIES_IPV4, + translation_key="port_mapping_number_of_entries_ipv4", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), UpnpSensorEntityDescription( key=BYTES_RECEIVED, translation_key="download_speed", diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index 7ce1798c351..bb95978c8dc 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -65,6 +65,9 @@ }, "wan_status": { "name": "WAN status" + }, + "port_mapping_number_of_entries_ipv4": { + "name": "Number of port mapping entries (IPv4)" } } } From 24f6e6e8854e7009d0c1cc2034caee9f101d8d6a Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 4 Jul 2024 09:20:55 +0200 Subject: [PATCH 0709/2411] Fix locking/unlocking transition state in Matter lock platform (#121099) --- homeassistant/components/matter/lock.py | 30 ++++++++++++++--------- tests/components/matter/test_door_lock.py | 10 +++----- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index 5456554a535..1cc85fa897e 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -90,6 +90,9 @@ class MatterLock(MatterEntity, LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Lock the lock with pin if needed.""" + # optimistically signal locking to state machine + self._attr_is_locking = True + self.async_write_ha_state() code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None await self.send_device_command( @@ -98,6 +101,9 @@ class MatterLock(MatterEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock with pin if needed.""" + # optimistically signal unlocking to state machine + self._attr_is_unlocking = True + self.async_write_ha_state() code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None if self.supports_unbolt: @@ -114,6 +120,9 @@ class MatterLock(MatterEntity, LockEntity): async def async_open(self, **kwargs: Any) -> None: """Open the door latch.""" + # optimistically signal unlocking to state machine + self._attr_is_unlocking = True + self.async_write_ha_state() code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None await self.send_device_command( @@ -135,26 +144,23 @@ class MatterLock(MatterEntity, LockEntity): clusters.DoorLock.Attributes.LockState ) + # always reset the optimisically (un)locking state on state update + self._attr_is_locking = False + self._attr_is_unlocking = False + LOGGER.debug("Lock state: %s for %s", lock_state, self.entity_id) if lock_state is clusters.DoorLock.Enums.DlLockState.kLocked: self._attr_is_locked = True - self._attr_is_locking = False - self._attr_is_unlocking = False - elif lock_state is clusters.DoorLock.Enums.DlLockState.kUnlocked: + elif lock_state in ( + clusters.DoorLock.Enums.DlLockState.kUnlocked, + clusters.DoorLock.Enums.DlLockState.kUnlatched, + clusters.DoorLock.Enums.DlLockState.kNotFullyLocked, + ): self._attr_is_locked = False - self._attr_is_locking = False - self._attr_is_unlocking = False - elif lock_state is clusters.DoorLock.Enums.DlLockState.kNotFullyLocked: - if self.is_locked is True: - self._attr_is_unlocking = True - elif self.is_locked is False: - self._attr_is_locking = True else: # According to the matter docs a null state can happen during device startup. self._attr_is_locked = None - self._attr_is_locking = None - self._attr_is_unlocking = None if self.supports_door_position_sensor: door_state = self.get_matter_attribute_value( diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_door_lock.py index a0664612aba..84f0e58a647 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_door_lock.py @@ -8,13 +8,11 @@ import pytest from homeassistant.components.lock import ( STATE_LOCKED, - STATE_LOCKING, STATE_OPEN, STATE_UNLOCKED, - STATE_UNLOCKING, LockEntityFeature, ) -from homeassistant.const import ATTR_CODE, STATE_UNKNOWN +from homeassistant.const import ATTR_CODE, STATE_LOCKING, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError import homeassistant.helpers.entity_registry as er @@ -68,14 +66,14 @@ async def test_lock( state = hass.states.get("lock.mock_door_lock_lock") assert state - assert state.state == STATE_LOCKED + assert state.state == STATE_LOCKING set_node_attribute(door_lock, 1, 257, 0, 0) await trigger_subscription_callback(hass, matter_client) state = hass.states.get("lock.mock_door_lock_lock") assert state - assert state.state == STATE_UNLOCKING + assert state.state == STATE_UNLOCKED set_node_attribute(door_lock, 1, 257, 0, 2) await trigger_subscription_callback(hass, matter_client) @@ -89,7 +87,7 @@ async def test_lock( state = hass.states.get("lock.mock_door_lock_lock") assert state - assert state.state == STATE_LOCKING + assert state.state == STATE_UNLOCKED set_node_attribute(door_lock, 1, 257, 0, None) await trigger_subscription_callback(hass, matter_client) From d55d02623a900d00dca9f3e4263062d74f5aa249 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Jul 2024 09:59:37 +0200 Subject: [PATCH 0710/2411] Add recorder test fixture to enable persistent SQLite database (#121137) * Add recorder test fixture to enable persistent SQLite database * Fix tests directly using async_test_home_assistant context manager --- .../statistics/test_duplicates.py | 26 ++--- tests/components/recorder/test_init.py | 97 ++++++++----------- ..._migration_run_time_migrations_remember.py | 10 +- .../recorder/test_statistics_v23_migration.py | 72 +++++++------- tests/components/recorder/test_util.py | 29 ++++-- .../components/recorder/test_v32_migration.py | 33 +++---- tests/conftest.py | 27 +++++- 7 files changed, 147 insertions(+), 147 deletions(-) diff --git a/tests/components/recorder/auto_repairs/statistics/test_duplicates.py b/tests/components/recorder/auto_repairs/statistics/test_duplicates.py index 67a67226cd6..a2cf41578c7 100644 --- a/tests/components/recorder/auto_repairs/statistics/test_duplicates.py +++ b/tests/components/recorder/auto_repairs/statistics/test_duplicates.py @@ -1,7 +1,6 @@ """Test removing statistics duplicates.""" import importlib -from pathlib import Path import sys from unittest.mock import patch @@ -15,7 +14,6 @@ from homeassistant.components.recorder.auto_repairs.statistics.duplicates import delete_statistics_duplicates, delete_statistics_meta_duplicates, ) -from homeassistant.components.recorder.const import SQLITE_URL_PREFIX from homeassistant.components.recorder.statistics import async_add_external_statistics from homeassistant.components.recorder.util import session_scope from homeassistant.core import HomeAssistant @@ -133,17 +131,13 @@ def _create_engine_28(*args, **kwargs): return engine +@pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_delete_metadata_duplicates( async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, - tmp_path: Path, ) -> None: """Test removal of duplicated statistics.""" - test_dir = tmp_path.joinpath("sqlite") - test_dir.mkdir() - test_db_file = test_dir.joinpath("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - module = "tests.components.recorder.db_schema_28" importlib.import_module(module) old_db_schema = sys.modules[module] @@ -202,7 +196,7 @@ async def test_delete_metadata_duplicates( ): async with ( async_test_home_assistant() as hass, - async_test_recorder(hass, {"db_url": dburl}), + async_test_recorder(hass), ): await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -224,7 +218,7 @@ async def test_delete_metadata_duplicates( # Test that the duplicates are removed during migration from schema 28 async with ( async_test_home_assistant() as hass, - async_test_recorder(hass, {"db_url": dburl}), + async_test_recorder(hass), ): await hass.async_start() await async_wait_recording_done(hass) @@ -242,17 +236,13 @@ async def test_delete_metadata_duplicates( await hass.async_stop() +@pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_delete_metadata_duplicates_many( async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, - tmp_path: Path, ) -> None: """Test removal of duplicated statistics.""" - test_dir = tmp_path.joinpath("sqlite") - test_dir.mkdir() - test_db_file = test_dir.joinpath("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - module = "tests.components.recorder.db_schema_28" importlib.import_module(module) old_db_schema = sys.modules[module] @@ -323,7 +313,7 @@ async def test_delete_metadata_duplicates_many( ): async with ( async_test_home_assistant() as hass, - async_test_recorder(hass, {"db_url": dburl}), + async_test_recorder(hass), ): await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -336,7 +326,7 @@ async def test_delete_metadata_duplicates_many( # Test that the duplicates are removed during migration from schema 28 async with ( async_test_home_assistant() as hass, - async_test_recorder(hass, {"db_url": dburl}), + async_test_recorder(hass), ): await hass.async_start() await async_wait_recording_done(hass) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index dd6625bec77..9d4e85eccf1 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -5,7 +5,6 @@ from __future__ import annotations import asyncio from collections.abc import Generator from datetime import datetime, timedelta -from pathlib import Path import sqlite3 import threading from typing import Any, cast @@ -26,7 +25,6 @@ from homeassistant.components.recorder import ( CONF_DB_URL, CONFIG_SCHEMA, DOMAIN, - SQLITE_URL_PREFIX, Recorder, db_schema, get_instance, @@ -140,19 +138,16 @@ def _default_recorder(hass): ) +@pytest.mark.parametrize("persistent_database", [True]) async def test_shutdown_before_startup_finishes( hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator, - recorder_db_url: str, - tmp_path: Path, ) -> None: - """Test shutdown before recorder starts is clean.""" - if recorder_db_url == "sqlite://": - # On-disk database because this test does not play nice with the - # MutexPool - recorder_db_url = "sqlite:///" + str(tmp_path / "pytest.db") + """Test shutdown before recorder starts is clean. + + On-disk database because this test does not play nice with the MutexPool. + """ config = { - recorder.CONF_DB_URL: recorder_db_url, recorder.CONF_COMMIT_INTERVAL: 1, } hass.set_state(CoreState.not_running) @@ -1371,15 +1366,13 @@ async def test_statistics_runs_initiated( @pytest.mark.freeze_time("2022-09-13 09:00:00+02:00") +@pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_compile_missing_statistics( - tmp_path: Path, freezer: FrozenDateTimeFactory + recorder_db_url: str, freezer: FrozenDateTimeFactory ) -> None: """Test missing statistics are compiled on startup.""" now = dt_util.utcnow().replace(minute=0, second=0, microsecond=0) - test_dir = tmp_path.joinpath("sqlite") - test_dir.mkdir() - test_db_file = test_dir.joinpath("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" def get_statistic_runs(hass: HomeAssistant) -> list: with session_scope(hass=hass, read_only=True) as session: @@ -1387,7 +1380,9 @@ async def test_compile_missing_statistics( async with async_test_home_assistant() as hass: recorder_helper.async_initialize_recorder(hass) - await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) + await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_DB_URL: recorder_db_url}} + ) await hass.async_start() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -1428,7 +1423,9 @@ async def test_compile_missing_statistics( ) recorder_helper.async_initialize_recorder(hass) - await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) + await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_DB_URL: recorder_db_url}} + ) await hass.async_start() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -1633,12 +1630,10 @@ async def test_service_disable_states_not_recording( ) -async def test_service_disable_run_information_recorded(tmp_path: Path) -> None: +@pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage +async def test_service_disable_run_information_recorded(recorder_db_url: str) -> None: """Test that runs are still recorded when recorder is disabled.""" - test_dir = tmp_path.joinpath("sqlite") - test_dir.mkdir() - test_db_file = test_dir.joinpath("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" def get_recorder_runs(hass: HomeAssistant) -> list: with session_scope(hass=hass, read_only=True) as session: @@ -1646,7 +1641,9 @@ async def test_service_disable_run_information_recorded(tmp_path: Path) -> None: async with async_test_home_assistant() as hass: recorder_helper.async_initialize_recorder(hass) - await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) + await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_DB_URL: recorder_db_url}} + ) await hass.async_start() await async_wait_recording_done(hass) @@ -1668,7 +1665,9 @@ async def test_service_disable_run_information_recorded(tmp_path: Path) -> None: async with async_test_home_assistant() as hass: recorder_helper.async_initialize_recorder(hass) - await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) + await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_DB_URL: recorder_db_url}} + ) await hass.async_start() await async_wait_recording_done(hass) @@ -1687,22 +1686,16 @@ class CannotSerializeMe: """A class that the JSONEncoder cannot serialize.""" +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.parametrize("persistent_database", [True]) async def test_database_corruption_while_running( - hass: HomeAssistant, tmp_path: Path, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, recorder_db_url: str, caplog: pytest.LogCaptureFixture ) -> None: """Test we can recover from sqlite3 db corruption.""" - - def _create_tmpdir_for_test_db() -> Path: - test_dir = tmp_path.joinpath("sqlite") - test_dir.mkdir() - return test_dir.joinpath("test.db") - - 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}} + hass, DOMAIN, {DOMAIN: {CONF_DB_URL: recorder_db_url, CONF_COMMIT_INTERVAL: 0}} ) await hass.async_block_till_done() caplog.clear() @@ -1722,6 +1715,7 @@ async def test_database_corruption_while_running( side_effect=OperationalError("statement", {}, []), ): await async_wait_recording_done(hass) + test_db_file = recorder_db_url.removeprefix("sqlite:///") await hass.async_add_executor_job(corrupt_db_file, test_db_file) await async_wait_recording_done(hass) @@ -1817,23 +1811,19 @@ async def test_entity_id_filter( @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) @pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.parametrize("persistent_database", [True]) async def test_database_lock_and_unlock( hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator, - recorder_db_url: str, - tmp_path: Path, ) -> None: """Test writing events during lock getting written after unlocking. This test is specific for SQLite: Locking is not implemented for other engines. - """ - if recorder_db_url == "sqlite://": - # Use file DB, in memory DB cannot do write locks. - recorder_db_url = "sqlite:///" + str(tmp_path / "pytest.db") + Use file DB, in memory DB cannot do write locks. + """ config = { recorder.CONF_COMMIT_INTERVAL: 0, - recorder.CONF_DB_URL: recorder_db_url, } await async_setup_recorder_instance(hass, config) await hass.async_block_till_done() @@ -1873,26 +1863,21 @@ async def test_database_lock_and_unlock( @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) @pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.parametrize("persistent_database", [True]) async def test_database_lock_and_overflow( hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator, - recorder_db_url: str, - tmp_path: Path, caplog: pytest.LogCaptureFixture, issue_registry: ir.IssueRegistry, ) -> None: """Test writing events during lock leading to overflow the queue causes the database to unlock. This test is specific for SQLite: Locking is not implemented for other engines. - """ - # Use file DB, in memory DB cannot do write locks. - if recorder_db_url == "sqlite://": - # Use file DB, in memory DB cannot do write locks. - recorder_db_url = "sqlite:///" + str(tmp_path / "pytest.db") + Use file DB, in memory DB cannot do write locks. + """ config = { recorder.CONF_COMMIT_INTERVAL: 0, - recorder.CONF_DB_URL: recorder_db_url, } def _get_db_events(): @@ -1941,26 +1926,21 @@ async def test_database_lock_and_overflow( @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) @pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.parametrize("persistent_database", [True]) async def test_database_lock_and_overflow_checks_available_memory( hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator, - recorder_db_url: str, - tmp_path: Path, caplog: pytest.LogCaptureFixture, issue_registry: ir.IssueRegistry, ) -> None: """Test writing events during lock leading to overflow the queue causes the database to unlock. This test is specific for SQLite: Locking is not implemented for other engines. - """ - # Use file DB, in memory DB cannot do write locks. - if recorder_db_url == "sqlite://": - # Use file DB, in memory DB cannot do write locks. - recorder_db_url = "sqlite:///" + str(tmp_path / "pytest.db") + Use file DB, in memory DB cannot do write locks. + """ config = { recorder.CONF_COMMIT_INTERVAL: 0, - recorder.CONF_DB_URL: recorder_db_url, } def _get_db_events(): @@ -2659,7 +2639,6 @@ async def test_commit_before_commits_pending_writes( hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator, recorder_db_url: str, - tmp_path: Path, ) -> None: """Test commit_before with a non-zero commit interval. diff --git a/tests/components/recorder/test_migration_run_time_migrations_remember.py b/tests/components/recorder/test_migration_run_time_migrations_remember.py index f3ade40d4af..5ef8a4b32e9 100644 --- a/tests/components/recorder/test_migration_run_time_migrations_remember.py +++ b/tests/components/recorder/test_migration_run_time_migrations_remember.py @@ -1,7 +1,6 @@ """Test run time migrations are remembered in the migration_changes table.""" import importlib -from pathlib import Path import sys from unittest.mock import patch @@ -62,13 +61,11 @@ def _create_engine_test(*args, **kwargs): return engine -@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) -@pytest.mark.usefixtures("skip_by_db_engine") @pytest.mark.parametrize("enable_migrate_context_ids", [True]) +@pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migration_changes_prevent_trying_to_migrate_again( async_setup_recorder_instance: RecorderInstanceGenerator, - tmp_path: Path, - recorder_db_url: str, ) -> None: """Test that we do not try to migrate when migration_changes indicate its already migrated. @@ -77,12 +74,9 @@ async def test_migration_changes_prevent_trying_to_migrate_again( 1. With schema 32 to populate the data 2. With current schema so the migration happens 3. With current schema to verify we do not have to query to see if the migration is done - - This test uses a test database between runs so its SQLite specific. WHY, this makes no sense.??? """ config = { - recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db"), recorder.CONF_COMMIT_INTERVAL: 1, } importlib.import_module(SCHEMA_MODULE) diff --git a/tests/components/recorder/test_statistics_v23_migration.py b/tests/components/recorder/test_statistics_v23_migration.py index af784692612..dfa87fc9391 100644 --- a/tests/components/recorder/test_statistics_v23_migration.py +++ b/tests/components/recorder/test_statistics_v23_migration.py @@ -15,7 +15,7 @@ from unittest.mock import patch import pytest from homeassistant.components import recorder -from homeassistant.components.recorder import SQLITE_URL_PREFIX, get_instance +from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.util import session_scope from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import setup_component @@ -34,13 +34,16 @@ SCHEMA_VERSION_POSTFIX = "23_with_newer_columns" SCHEMA_MODULE = get_schema_module_path(SCHEMA_VERSION_POSTFIX) -def test_delete_duplicates(caplog: pytest.LogCaptureFixture, tmp_path: Path) -> None: - """Test removal of duplicated statistics.""" - test_dir = tmp_path.joinpath("sqlite") - test_dir.mkdir() - test_db_file = test_dir.joinpath("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.parametrize("persistent_database", [True]) +def test_delete_duplicates( + recorder_db_url: str, caplog: pytest.LogCaptureFixture +) -> None: + """Test removal of duplicated statistics. + The test only works with SQLite. + """ importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] @@ -176,7 +179,7 @@ def test_delete_duplicates(caplog: pytest.LogCaptureFixture, tmp_path: Path) -> get_test_home_assistant() as hass, ): recorder_helper.async_initialize_recorder(hass) - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + setup_component(hass, "recorder", {"recorder": {"db_url": recorder_db_url}}) get_instance(hass).recorder_and_worker_thread_ids.add(threading.get_ident()) wait_recording_done(hass) wait_recording_done(hass) @@ -204,7 +207,7 @@ def test_delete_duplicates(caplog: pytest.LogCaptureFixture, tmp_path: Path) -> # Test that the duplicates are removed during migration from schema 23 with get_test_home_assistant() as hass: recorder_helper.async_initialize_recorder(hass) - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + setup_component(hass, "recorder", {"recorder": {"db_url": recorder_db_url}}) hass.start() wait_recording_done(hass) wait_recording_done(hass) @@ -215,15 +218,16 @@ def test_delete_duplicates(caplog: pytest.LogCaptureFixture, tmp_path: Path) -> assert "Found duplicated" not in caplog.text +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.parametrize("persistent_database", [True]) def test_delete_duplicates_many( - caplog: pytest.LogCaptureFixture, tmp_path: Path + recorder_db_url: str, caplog: pytest.LogCaptureFixture ) -> None: - """Test removal of duplicated statistics.""" - test_dir = tmp_path.joinpath("sqlite") - test_dir.mkdir() - test_db_file = test_dir.joinpath("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + """Test removal of duplicated statistics. + The test only works with SQLite. + """ importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] @@ -359,7 +363,7 @@ def test_delete_duplicates_many( get_test_home_assistant() as hass, ): recorder_helper.async_initialize_recorder(hass) - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + setup_component(hass, "recorder", {"recorder": {"db_url": recorder_db_url}}) get_instance(hass).recorder_and_worker_thread_ids.add(threading.get_ident()) wait_recording_done(hass) wait_recording_done(hass) @@ -393,7 +397,7 @@ def test_delete_duplicates_many( # Test that the duplicates are removed during migration from schema 23 with get_test_home_assistant() as hass: recorder_helper.async_initialize_recorder(hass) - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + setup_component(hass, "recorder", {"recorder": {"db_url": recorder_db_url}}) hass.start() wait_recording_done(hass) wait_recording_done(hass) @@ -405,15 +409,16 @@ def test_delete_duplicates_many( @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.parametrize("persistent_database", [True]) def test_delete_duplicates_non_identical( - caplog: pytest.LogCaptureFixture, tmp_path: Path + recorder_db_url: str, caplog: pytest.LogCaptureFixture, tmp_path: Path ) -> None: - """Test removal of duplicated statistics.""" - test_dir = tmp_path.joinpath("sqlite") - test_dir.mkdir() - test_db_file = test_dir.joinpath("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + """Test removal of duplicated statistics. + The test only works with SQLite. + """ importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] @@ -519,7 +524,7 @@ def test_delete_duplicates_non_identical( get_test_home_assistant() as hass, ): recorder_helper.async_initialize_recorder(hass) - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + setup_component(hass, "recorder", {"recorder": {"db_url": recorder_db_url}}) get_instance(hass).recorder_and_worker_thread_ids.add(threading.get_ident()) wait_recording_done(hass) wait_recording_done(hass) @@ -543,7 +548,7 @@ def test_delete_duplicates_non_identical( with get_test_home_assistant() as hass: hass.config.config_dir = tmp_path recorder_helper.async_initialize_recorder(hass) - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + setup_component(hass, "recorder", {"recorder": {"db_url": recorder_db_url}}) hass.start() wait_recording_done(hass) wait_recording_done(hass) @@ -589,15 +594,16 @@ def test_delete_duplicates_non_identical( ] +@pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") def test_delete_duplicates_short_term( - caplog: pytest.LogCaptureFixture, tmp_path: Path + recorder_db_url: str, caplog: pytest.LogCaptureFixture, tmp_path: Path ) -> None: - """Test removal of duplicated statistics.""" - test_dir = tmp_path.joinpath("sqlite") - test_dir.mkdir() - test_db_file = test_dir.joinpath("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + """Test removal of duplicated statistics. + The test only works with SQLite. + """ importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] @@ -634,7 +640,7 @@ def test_delete_duplicates_short_term( get_test_home_assistant() as hass, ): recorder_helper.async_initialize_recorder(hass) - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + setup_component(hass, "recorder", {"recorder": {"db_url": recorder_db_url}}) get_instance(hass).recorder_and_worker_thread_ids.add(threading.get_ident()) wait_recording_done(hass) wait_recording_done(hass) @@ -657,7 +663,7 @@ def test_delete_duplicates_short_term( with get_test_home_assistant() as hass: hass.config.config_dir = tmp_path recorder_helper.async_initialize_recorder(hass) - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + setup_component(hass, "recorder", {"recorder": {"db_url": recorder_db_url}}) hass.start() wait_recording_done(hass) wait_recording_done(hass) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index da8dfd61a17..16bf06204e2 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -116,12 +116,18 @@ def test_validate_or_move_away_sqlite_database( assert util.validate_or_move_away_sqlite_database(dburl) is True +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_last_run_was_recently_clean( - async_setup_recorder_instance: RecorderInstanceGenerator, tmp_path: Path + async_setup_recorder_instance: RecorderInstanceGenerator, ) -> None: - """Test we can check if the last recorder run was recently clean.""" + """Test we can check if the last recorder run was recently clean. + + This is only implemented for SQLite. + """ config = { - recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db"), recorder.CONF_COMMIT_INTERVAL: 1, } async with async_test_home_assistant() as hass: @@ -850,17 +856,22 @@ async def test_periodic_db_cleanups( assert str(text_obj) == "PRAGMA wal_checkpoint(TRUNCATE);" +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.parametrize("persistent_database", [True]) async def test_write_lock_db( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, - tmp_path: Path, + recorder_db_url: str, ) -> None: - """Test database write lock.""" + """Test database write lock. - # Use file DB, in memory DB cannot do write locks. - config = { - recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db?timeout=0.1") - } + This is only supported for SQLite. + + Use file DB, in memory DB cannot do write locks. + """ + + config = {recorder.CONF_DB_URL: recorder_db_url + "?timeout=0.1"} instance = await async_setup_recorder_instance(hass, config) await hass.async_block_till_done() diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index b71bc4cefb8..039f1c87aee 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -2,7 +2,6 @@ from datetime import timedelta import importlib -from pathlib import Path import sys from unittest.mock import patch @@ -11,7 +10,7 @@ from sqlalchemy import create_engine, inspect from sqlalchemy.orm import Session from homeassistant.components import recorder -from homeassistant.components.recorder import SQLITE_URL_PREFIX, core, statistics +from homeassistant.components.recorder import core, statistics from homeassistant.components.recorder.queries import select_event_type_ids from homeassistant.components.recorder.util import session_scope from homeassistant.core import EVENT_STATE_CHANGED, Event, EventOrigin, State @@ -49,13 +48,13 @@ def _create_engine_test(*args, **kwargs): return engine -async def test_migrate_times(caplog: pytest.LogCaptureFixture, tmp_path: Path) -> None: +@pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage +async def test_migrate_times( + caplog: pytest.LogCaptureFixture, + recorder_db_url: str, +) -> None: """Test we can migrate times.""" - test_dir = tmp_path.joinpath("sqlite") - test_dir.mkdir() - test_db_file = test_dir.joinpath("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] now = dt_util.utcnow() @@ -123,7 +122,7 @@ async def test_migrate_times(caplog: pytest.LogCaptureFixture, tmp_path: Path) - async with async_test_home_assistant() as hass: recorder_helper.async_initialize_recorder(hass) assert await async_setup_component( - hass, "recorder", {"recorder": {"db_url": dburl}} + hass, "recorder", {"recorder": {"db_url": recorder_db_url}} ) await hass.async_block_till_done() await async_wait_recording_done(hass) @@ -153,7 +152,7 @@ async def test_migrate_times(caplog: pytest.LogCaptureFixture, tmp_path: Path) - async with async_test_home_assistant() as hass: recorder_helper.async_initialize_recorder(hass) assert await async_setup_component( - hass, "recorder", {"recorder": {"db_url": dburl}} + hass, "recorder", {"recorder": {"db_url": recorder_db_url}} ) await hass.async_block_till_done() @@ -220,15 +219,13 @@ async def test_migrate_times(caplog: pytest.LogCaptureFixture, tmp_path: Path) - await hass.async_stop() +@pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_can_resume_entity_id_post_migration( - caplog: pytest.LogCaptureFixture, tmp_path: Path + caplog: pytest.LogCaptureFixture, + recorder_db_url: str, ) -> None: """Test we resume the entity id post migration after a restart.""" - test_dir = tmp_path.joinpath("sqlite") - test_dir.mkdir() - test_db_file = test_dir.joinpath("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] now = dt_util.utcnow() @@ -293,7 +290,7 @@ async def test_migrate_can_resume_entity_id_post_migration( async with async_test_home_assistant() as hass: recorder_helper.async_initialize_recorder(hass) assert await async_setup_component( - hass, "recorder", {"recorder": {"db_url": dburl}} + hass, "recorder", {"recorder": {"db_url": recorder_db_url}} ) await hass.async_block_till_done() await async_wait_recording_done(hass) @@ -323,7 +320,7 @@ async def test_migrate_can_resume_entity_id_post_migration( async with async_test_home_assistant() as hass: recorder_helper.async_initialize_recorder(hass) assert await async_setup_component( - hass, "recorder", {"recorder": {"db_url": dburl}} + hass, "recorder", {"recorder": {"db_url": recorder_db_url}} ) await hass.async_block_till_done() diff --git a/tests/conftest.py b/tests/conftest.py index 565e0e42534..b96bd783331 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ import itertools import logging import os import reprlib +from shutil import rmtree import sqlite3 import ssl import threading @@ -1309,16 +1310,36 @@ def recorder_config() -> dict[str, Any] | None: return None +@pytest.fixture +def persistent_database() -> bool: + """Fixture to control if database should persist when recorder is shut down in test. + + When using sqlite, this uses on disk database instead of in memory database. + This does nothing when using mysql or postgresql. + + Note that the database is always destroyed in between tests. + + To use a persistent database, tests can be marked with: + @pytest.mark.parametrize("persistent_database", [True]) + """ + return False + + @pytest.fixture def recorder_db_url( pytestconfig: pytest.Config, hass_fixture_setup: list[bool], + persistent_database: str, + tmp_path_factory: pytest.TempPathFactory, ) -> Generator[str]: """Prepare a default database for tests and return a connection URL.""" assert not hass_fixture_setup db_url = cast(str, pytestconfig.getoption("dburl")) - if db_url.startswith("mysql://"): + if db_url == "sqlite://" and persistent_database: + tmp_path = tmp_path_factory.mktemp("recorder") + db_url = "sqlite:///" + str(tmp_path / "pytest.db") + elif db_url.startswith("mysql://"): # pylint: disable-next=import-outside-toplevel import sqlalchemy_utils @@ -1332,7 +1353,9 @@ def recorder_db_url( assert not sqlalchemy_utils.database_exists(db_url) sqlalchemy_utils.create_database(db_url, encoding="utf8") yield db_url - if db_url.startswith("mysql://"): + if db_url == "sqlite://" and persistent_database: + rmtree(tmp_path, ignore_errors=True) + elif db_url.startswith("mysql://"): # pylint: disable-next=import-outside-toplevel import sqlalchemy as sa From 4589be2d11f02ee0af5ffdbe23a4970cd1ffe6ae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:22:39 +0200 Subject: [PATCH 0711/2411] Improve type hints in group tests (#121174) --- tests/components/group/test_cover.py | 49 ++++++++++++++++++---------- tests/components/group/test_fan.py | 25 +++++++++----- 2 files changed, 48 insertions(+), 26 deletions(-) diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index 5b5d8fa873c..c687ca21e2d 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta +from typing import Any import pytest @@ -90,7 +91,9 @@ CONFIG_ATTRIBUTES = { @pytest.fixture -async def setup_comp(hass, config_count): +async def setup_comp( + hass: HomeAssistant, config_count: tuple[dict[str, Any], int] +) -> None: """Set up group cover component.""" config, count = config_count with assert_setup_component(count, DOMAIN): @@ -101,7 +104,8 @@ async def setup_comp(hass, config_count): @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) -async def test_state(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_state(hass: HomeAssistant) -> None: """Test handling of state. The group state is unknown if all group members are unknown or unavailable. @@ -250,8 +254,9 @@ async def test_state(hass: HomeAssistant, setup_comp) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) +@pytest.mark.usefixtures("setup_comp") async def test_attributes( - hass: HomeAssistant, entity_registry: er.EntityRegistry, setup_comp + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test handling of state attributes.""" state = hass.states.get(COVER_GROUP) @@ -416,9 +421,8 @@ async def test_attributes( @pytest.mark.parametrize("config_count", [(CONFIG_TILT_ONLY, 2)]) -async def test_cover_that_only_supports_tilt_removed( - hass: HomeAssistant, setup_comp -) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_cover_that_only_supports_tilt_removed(hass: HomeAssistant) -> None: """Test removing a cover that support tilt.""" hass.states.async_set( DEMO_COVER_TILT, @@ -446,7 +450,8 @@ async def test_cover_that_only_supports_tilt_removed( @pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) -async def test_open_covers(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_open_covers(hass: HomeAssistant) -> None: """Test open cover function.""" await hass.services.async_call( DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True @@ -467,7 +472,8 @@ async def test_open_covers(hass: HomeAssistant, setup_comp) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) -async def test_close_covers(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_close_covers(hass: HomeAssistant) -> None: """Test close cover function.""" await hass.services.async_call( DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True @@ -488,7 +494,8 @@ async def test_close_covers(hass: HomeAssistant, setup_comp) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) -async def test_toggle_covers(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_toggle_covers(hass: HomeAssistant) -> None: """Test toggle cover function.""" # Start covers in open state await hass.services.async_call( @@ -538,7 +545,8 @@ async def test_toggle_covers(hass: HomeAssistant, setup_comp) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) -async def test_stop_covers(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_stop_covers(hass: HomeAssistant) -> None: """Test stop cover function.""" await hass.services.async_call( DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True @@ -564,7 +572,8 @@ async def test_stop_covers(hass: HomeAssistant, setup_comp) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) -async def test_set_cover_position(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_set_cover_position(hass: HomeAssistant) -> None: """Test set cover position function.""" await hass.services.async_call( DOMAIN, @@ -587,7 +596,8 @@ async def test_set_cover_position(hass: HomeAssistant, setup_comp) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) -async def test_open_tilts(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_open_tilts(hass: HomeAssistant) -> None: """Test open tilt function.""" await hass.services.async_call( DOMAIN, SERVICE_OPEN_COVER_TILT, {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True @@ -607,7 +617,8 @@ async def test_open_tilts(hass: HomeAssistant, setup_comp) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) -async def test_close_tilts(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_close_tilts(hass: HomeAssistant) -> None: """Test close tilt function.""" await hass.services.async_call( DOMAIN, SERVICE_CLOSE_COVER_TILT, {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True @@ -625,7 +636,8 @@ async def test_close_tilts(hass: HomeAssistant, setup_comp) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) -async def test_toggle_tilts(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_toggle_tilts(hass: HomeAssistant) -> None: """Test toggle tilt function.""" # Start tilted open await hass.services.async_call( @@ -678,7 +690,8 @@ async def test_toggle_tilts(hass: HomeAssistant, setup_comp) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) -async def test_stop_tilts(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_stop_tilts(hass: HomeAssistant) -> None: """Test stop tilts function.""" await hass.services.async_call( DOMAIN, SERVICE_OPEN_COVER_TILT, {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True @@ -702,7 +715,8 @@ async def test_stop_tilts(hass: HomeAssistant, setup_comp) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) -async def test_set_tilt_positions(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_set_tilt_positions(hass: HomeAssistant) -> None: """Test set tilt position function.""" await hass.services.async_call( DOMAIN, @@ -723,7 +737,8 @@ async def test_set_tilt_positions(hass: HomeAssistant, setup_comp) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_POS, 2)]) -async def test_is_opening_closing(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_is_opening_closing(hass: HomeAssistant) -> None: """Test is_opening property.""" await hass.services.async_call( DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index 6aa6fc2933d..184693f7618 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -1,6 +1,7 @@ """The tests for the group fan platform.""" import asyncio +from typing import Any from unittest.mock import patch import pytest @@ -102,7 +103,9 @@ CONFIG_ATTRIBUTES = { @pytest.fixture -async def setup_comp(hass, config_count): +async def setup_comp( + hass: HomeAssistant, config_count: tuple[dict[str, Any], int] +) -> None: """Set up group fan component.""" config, count = config_count with assert_setup_component(count, DOMAIN): @@ -113,9 +116,8 @@ async def setup_comp(hass, config_count): @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) -async def test_state( - hass: HomeAssistant, entity_registry: er.EntityRegistry, setup_comp -) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_state(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: """Test handling of state. The group state is on if at least one group member is on. @@ -210,7 +212,8 @@ async def test_state( @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) -async def test_attributes(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_attributes(hass: HomeAssistant) -> None: """Test handling of state attributes.""" state = hass.states.get(FAN_GROUP) assert state.state == STATE_UNAVAILABLE @@ -267,7 +270,8 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_FULL_SUPPORT, 2)]) -async def test_direction_oscillating(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_direction_oscillating(hass: HomeAssistant) -> None: """Test handling of direction and oscillating attributes.""" hass.states.async_set( @@ -378,7 +382,8 @@ async def test_direction_oscillating(hass: HomeAssistant, setup_comp) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_MISSING_FAN, 2)]) -async def test_state_missing_entity_id(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_state_missing_entity_id(hass: HomeAssistant) -> None: """Test we can still setup with a missing entity id.""" state = hass.states.get(FAN_GROUP) await hass.async_block_till_done() @@ -398,7 +403,8 @@ async def test_setup_before_started(hass: HomeAssistant) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_MISSING_FAN, 2)]) -async def test_reload(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_reload(hass: HomeAssistant) -> None: """Test the ability to reload fans.""" await hass.async_block_till_done() await hass.async_start() @@ -421,7 +427,8 @@ async def test_reload(hass: HomeAssistant, setup_comp) -> None: @pytest.mark.parametrize("config_count", [(CONFIG_FULL_SUPPORT, 2)]) -async def test_service_calls(hass: HomeAssistant, setup_comp) -> None: +@pytest.mark.usefixtures("setup_comp") +async def test_service_calls(hass: HomeAssistant) -> None: """Test calling services.""" await hass.services.async_call( DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: FAN_GROUP}, blocking=True From c9acd1711ca86bd8aa4baf3cf6aed5e49766090a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:23:14 +0200 Subject: [PATCH 0712/2411] Improve type hints in gpslogger tests (#121173) --- tests/components/gpslogger/test_init.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 68b95df1702..fab6aaa4e84 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -45,7 +45,7 @@ async def gpslogger_client( @pytest.fixture(autouse=True) -async def setup_zones(hass): +async def setup_zones(hass: HomeAssistant) -> None: """Set up Zone config in HA.""" assert await async_setup_component( hass, @@ -63,7 +63,7 @@ async def setup_zones(hass): @pytest.fixture -async def webhook_id(hass, gpslogger_client): +async def webhook_id(hass: HomeAssistant, gpslogger_client: TestClient) -> str: """Initialize the GPSLogger component and get the webhook_id.""" await async_process_ha_core_config( hass, @@ -81,7 +81,9 @@ async def webhook_id(hass, gpslogger_client): return result["result"].data["webhook_id"] -async def test_missing_data(hass: HomeAssistant, gpslogger_client, webhook_id) -> None: +async def test_missing_data( + hass: HomeAssistant, gpslogger_client: TestClient, webhook_id: str +) -> None: """Test missing data.""" url = f"/api/webhook/{webhook_id}" @@ -111,8 +113,8 @@ async def test_enter_and_exit( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, - gpslogger_client, - webhook_id, + gpslogger_client: TestClient, + webhook_id: str, ) -> None: """Test when there is a known zone.""" url = f"/api/webhook/{webhook_id}" @@ -148,7 +150,7 @@ async def test_enter_and_exit( async def test_enter_with_attrs( - hass: HomeAssistant, gpslogger_client, webhook_id + hass: HomeAssistant, gpslogger_client: TestClient, webhook_id: str ) -> None: """Test when additional attributes are present.""" url = f"/api/webhook/{webhook_id}" @@ -210,7 +212,7 @@ async def test_enter_with_attrs( reason="The device_tracker component does not support unloading yet." ) async def test_load_unload_entry( - hass: HomeAssistant, gpslogger_client, webhook_id + hass: HomeAssistant, gpslogger_client: TestClient, webhook_id: str ) -> None: """Test that the appropriate dispatch signals are added and removed.""" url = f"/api/webhook/{webhook_id}" From dd8ba0828a11ed2c551cecafb37219915dc050df Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:27:37 +0200 Subject: [PATCH 0713/2411] Improve type hints in geofency tests (#121168) --- tests/components/geofency/test_init.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 2228cea80ee..3a98c6480bd 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -137,7 +137,7 @@ async def geofency_client( @pytest.fixture(autouse=True) -async def setup_zones(hass): +async def setup_zones(hass: HomeAssistant) -> None: """Set up Zone config in HA.""" assert await async_setup_component( hass, @@ -155,7 +155,7 @@ async def setup_zones(hass): @pytest.fixture -async def webhook_id(hass, geofency_client): +async def webhook_id(hass: HomeAssistant) -> str: """Initialize the Geofency component and get the webhook_id.""" await async_process_ha_core_config( hass, @@ -173,7 +173,7 @@ async def webhook_id(hass, geofency_client): return result["result"].data["webhook_id"] -async def test_data_validation(geofency_client, webhook_id) -> None: +async def test_data_validation(geofency_client: TestClient, webhook_id: str) -> None: """Test data validation.""" url = f"/api/webhook/{webhook_id}" @@ -195,8 +195,8 @@ async def test_gps_enter_and_exit_home( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, - geofency_client, - webhook_id, + geofency_client: TestClient, + webhook_id: str, ) -> None: """Test GPS based zone enter and exit.""" url = f"/api/webhook/{webhook_id}" @@ -240,7 +240,7 @@ async def test_gps_enter_and_exit_home( async def test_beacon_enter_and_exit_home( - hass: HomeAssistant, geofency_client, webhook_id + hass: HomeAssistant, geofency_client: TestClient, webhook_id: str ) -> None: """Test iBeacon based zone enter and exit - a.k.a stationary iBeacon.""" url = f"/api/webhook/{webhook_id}" @@ -263,7 +263,7 @@ async def test_beacon_enter_and_exit_home( async def test_beacon_enter_and_exit_car( - hass: HomeAssistant, geofency_client, webhook_id + hass: HomeAssistant, geofency_client: TestClient, webhook_id: str ) -> None: """Test use of mobile iBeacon.""" url = f"/api/webhook/{webhook_id}" @@ -305,7 +305,7 @@ async def test_beacon_enter_and_exit_car( async def test_load_unload_entry( - hass: HomeAssistant, geofency_client, webhook_id + hass: HomeAssistant, geofency_client: TestClient, webhook_id: str ) -> None: """Test that the appropriate dispatch signals are added and removed.""" url = f"/api/webhook/{webhook_id}" From 1f22f0d89b69dd12bebe55420dcdfabadbd621a1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:27:55 +0200 Subject: [PATCH 0714/2411] Improve type hints in google_travel_time tests (#121171) --- .../components/google_travel_time/conftest.py | 23 +++++++----- .../google_travel_time/test_config_flow.py | 36 +++++++++++++------ .../google_travel_time/test_sensor.py | 9 ++--- 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/tests/components/google_travel_time/conftest.py b/tests/components/google_travel_time/conftest.py index 141b40eff29..7d1e4791eee 100644 --- a/tests/components/google_travel_time/conftest.py +++ b/tests/components/google_travel_time/conftest.py @@ -1,17 +1,22 @@ """Fixtures for Google Time Travel tests.""" -from unittest.mock import patch +from collections.abc import Generator +from typing import Any +from unittest.mock import MagicMock, patch from googlemaps.exceptions import ApiError, Timeout, TransportError import pytest from homeassistant.components.google_travel_time.const import DOMAIN +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @pytest.fixture(name="mock_config") -async def mock_config_fixture(hass, data, options): +async def mock_config_fixture( + hass: HomeAssistant, data: dict[str, Any], options: dict[str, Any] +) -> MockConfigEntry: """Mock a Google Travel Time config entry.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -26,7 +31,7 @@ async def mock_config_fixture(hass, data, options): @pytest.fixture(name="bypass_setup") -def bypass_setup_fixture(): +def bypass_setup_fixture() -> Generator[None]: """Bypass entry setup.""" with patch( "homeassistant.components.google_travel_time.async_setup_entry", @@ -36,7 +41,7 @@ def bypass_setup_fixture(): @pytest.fixture(name="bypass_platform_setup") -def bypass_platform_setup_fixture(): +def bypass_platform_setup_fixture() -> Generator[None]: """Bypass platform setup.""" with patch( "homeassistant.components.google_travel_time.sensor.async_setup_entry", @@ -46,7 +51,7 @@ def bypass_platform_setup_fixture(): @pytest.fixture(name="validate_config_entry") -def validate_config_entry_fixture(): +def validate_config_entry_fixture() -> Generator[MagicMock]: """Return valid config entry.""" with ( patch("homeassistant.components.google_travel_time.helpers.Client"), @@ -59,24 +64,24 @@ def validate_config_entry_fixture(): @pytest.fixture(name="invalidate_config_entry") -def invalidate_config_entry_fixture(validate_config_entry): +def invalidate_config_entry_fixture(validate_config_entry: MagicMock) -> None: """Return invalid config entry.""" validate_config_entry.side_effect = ApiError("test") @pytest.fixture(name="invalid_api_key") -def invalid_api_key_fixture(validate_config_entry): +def invalid_api_key_fixture(validate_config_entry: MagicMock) -> None: """Throw a REQUEST_DENIED ApiError.""" validate_config_entry.side_effect = ApiError("REQUEST_DENIED", "Invalid API key.") @pytest.fixture(name="timeout") -def timeout_fixture(validate_config_entry): +def timeout_fixture(validate_config_entry: MagicMock) -> None: """Throw a Timeout exception.""" validate_config_entry.side_effect = Timeout() @pytest.fixture(name="transport_error") -def transport_error_fixture(validate_config_entry): +def transport_error_fixture(validate_config_entry: MagicMock) -> None: """Throw a TransportError exception.""" validate_config_entry.side_effect = TransportError("Unknown.") diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index 270b82272d8..d16d1c1ffc9 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -29,6 +29,8 @@ from homeassistant.data_entry_flow import FlowResultType from .const import MOCK_CONFIG, RECONFIGURE_CONFIG +from tests.common import MockConfigEntry + async def assert_common_reconfigure_steps( hass: HomeAssistant, reconfigure_result: config_entries.ConfigFlowResult @@ -194,7 +196,7 @@ async def test_malformed_api_key(hass: HomeAssistant) -> None: ], ) @pytest.mark.usefixtures("validate_config_entry", "bypass_setup") -async def test_reconfigure(hass: HomeAssistant, mock_config) -> None: +async def test_reconfigure(hass: HomeAssistant, mock_config: MockConfigEntry) -> None: """Test reconfigure flow.""" reconfigure_result = await hass.config_entries.flow.async_init( DOMAIN, @@ -223,7 +225,7 @@ async def test_reconfigure(hass: HomeAssistant, mock_config) -> None: ) @pytest.mark.usefixtures("invalidate_config_entry") async def test_reconfigure_invalid_config_entry( - hass: HomeAssistant, mock_config + hass: HomeAssistant, mock_config: MockConfigEntry ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -259,7 +261,9 @@ async def test_reconfigure_invalid_config_entry( ], ) @pytest.mark.usefixtures("invalid_api_key") -async def test_reconfigure_invalid_api_key(hass: HomeAssistant, mock_config) -> None: +async def test_reconfigure_invalid_api_key( + hass: HomeAssistant, mock_config: MockConfigEntry +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -293,7 +297,9 @@ async def test_reconfigure_invalid_api_key(hass: HomeAssistant, mock_config) -> ], ) @pytest.mark.usefixtures("transport_error") -async def test_reconfigure_transport_error(hass: HomeAssistant, mock_config) -> None: +async def test_reconfigure_transport_error( + hass: HomeAssistant, mock_config: MockConfigEntry +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -327,7 +333,9 @@ async def test_reconfigure_transport_error(hass: HomeAssistant, mock_config) -> ], ) @pytest.mark.usefixtures("timeout") -async def test_reconfigure_timeout(hass: HomeAssistant, mock_config) -> None: +async def test_reconfigure_timeout( + hass: HomeAssistant, mock_config: MockConfigEntry +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -361,7 +369,7 @@ async def test_reconfigure_timeout(hass: HomeAssistant, mock_config) -> None: ], ) @pytest.mark.usefixtures("validate_config_entry") -async def test_options_flow(hass: HomeAssistant, mock_config) -> None: +async def test_options_flow(hass: HomeAssistant, mock_config: MockConfigEntry) -> None: """Test options flow.""" result = await hass.config_entries.options.async_init( mock_config.entry_id, data=None @@ -422,7 +430,9 @@ async def test_options_flow(hass: HomeAssistant, mock_config) -> None: ], ) @pytest.mark.usefixtures("validate_config_entry") -async def test_options_flow_departure_time(hass: HomeAssistant, mock_config) -> None: +async def test_options_flow_departure_time( + hass: HomeAssistant, mock_config: MockConfigEntry +) -> None: """Test options flow with departure time.""" result = await hass.config_entries.options.async_init( mock_config.entry_id, data=None @@ -492,7 +502,9 @@ async def test_options_flow_departure_time(hass: HomeAssistant, mock_config) -> ], ) @pytest.mark.usefixtures("validate_config_entry") -async def test_reset_departure_time(hass: HomeAssistant, mock_config) -> None: +async def test_reset_departure_time( + hass: HomeAssistant, mock_config: MockConfigEntry +) -> None: """Test resetting departure time.""" result = await hass.config_entries.options.async_init( mock_config.entry_id, data=None @@ -538,7 +550,9 @@ async def test_reset_departure_time(hass: HomeAssistant, mock_config) -> None: ], ) @pytest.mark.usefixtures("validate_config_entry") -async def test_reset_arrival_time(hass: HomeAssistant, mock_config) -> None: +async def test_reset_arrival_time( + hass: HomeAssistant, mock_config: MockConfigEntry +) -> None: """Test resetting arrival time.""" result = await hass.config_entries.options.async_init( mock_config.entry_id, data=None @@ -582,7 +596,9 @@ async def test_reset_arrival_time(hass: HomeAssistant, mock_config) -> None: ], ) @pytest.mark.usefixtures("validate_config_entry") -async def test_reset_options_flow_fields(hass: HomeAssistant, mock_config) -> None: +async def test_reset_options_flow_fields( + hass: HomeAssistant, mock_config: MockConfigEntry +) -> None: """Test resetting options flow fields that are not time related to None.""" result = await hass.config_entries.options.async_init( mock_config.entry_id, data=None diff --git a/tests/components/google_travel_time/test_sensor.py b/tests/components/google_travel_time/test_sensor.py index 57f3d7a0b98..5ac9ecad482 100644 --- a/tests/components/google_travel_time/test_sensor.py +++ b/tests/components/google_travel_time/test_sensor.py @@ -1,6 +1,7 @@ """Test the Google Maps Travel Time sensors.""" -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import MagicMock, patch import pytest @@ -25,7 +26,7 @@ from tests.common import MockConfigEntry @pytest.fixture(name="mock_update") -def mock_update_fixture(): +def mock_update_fixture() -> Generator[MagicMock]: """Mock an update to the sensor.""" with ( patch("homeassistant.components.google_travel_time.sensor.Client"), @@ -56,7 +57,7 @@ def mock_update_fixture(): @pytest.fixture(name="mock_update_duration") -def mock_update_duration_fixture(mock_update): +def mock_update_duration_fixture(mock_update: MagicMock) -> MagicMock: """Mock an update to the sensor returning no duration_in_traffic.""" mock_update.return_value = { "rows": [ @@ -77,7 +78,7 @@ def mock_update_duration_fixture(mock_update): @pytest.fixture(name="mock_update_empty") -def mock_update_empty_fixture(mock_update): +def mock_update_empty_fixture(mock_update: MagicMock) -> MagicMock: """Mock an update to the sensor with an empty response.""" mock_update.return_value = None return mock_update From 1eec49696a7537fdc7e7732d43615af049cf4a39 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:30:17 +0200 Subject: [PATCH 0715/2411] Improve type hints in generic_hygrostat/thermostat tests (#121167) --- .../generic_hygrostat/test_humidifier.py | 186 ++++++++++-------- .../generic_thermostat/test_climate.py | 145 +++++++------- 2 files changed, 180 insertions(+), 151 deletions(-) diff --git a/tests/components/generic_hygrostat/test_humidifier.py b/tests/components/generic_hygrostat/test_humidifier.py index 15d80885d27..a97d5a7c1a6 100644 --- a/tests/components/generic_hygrostat/test_humidifier.py +++ b/tests/components/generic_hygrostat/test_humidifier.py @@ -87,13 +87,14 @@ async def test_valid_conf(hass: HomeAssistant) -> None: @pytest.fixture -async def setup_comp_1(hass): +async def setup_comp_1(hass: HomeAssistant) -> None: """Initialize components.""" assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() -async def test_humidifier_input_boolean(hass: HomeAssistant, setup_comp_1) -> None: +@pytest.mark.usefixtures("setup_comp_1") +async def test_humidifier_input_boolean(hass: HomeAssistant) -> None: """Test humidifier switching input_boolean.""" humidifier_switch = "input_boolean.test" assert await async_setup_component( @@ -132,8 +133,9 @@ async def test_humidifier_input_boolean(hass: HomeAssistant, setup_comp_1) -> No assert hass.states.get(ENTITY).attributes.get("action") == "humidifying" +@pytest.mark.usefixtures("setup_comp_1") async def test_humidifier_switch( - hass: HomeAssistant, setup_comp_1, mock_switch_entities: list[MockSwitch] + hass: HomeAssistant, mock_switch_entities: list[MockSwitch] ) -> None: """Test humidifier switching test switch.""" setup_test_component_platform(hass, switch.DOMAIN, mock_switch_entities) @@ -176,8 +178,9 @@ async def test_humidifier_switch( assert hass.states.get(ENTITY).attributes.get("action") == "humidifying" +@pytest.mark.usefixtures("setup_comp_1") async def test_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, setup_comp_1 + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test setting a unique ID.""" unique_id = "some_unique_id" @@ -209,7 +212,7 @@ def _setup_sensor(hass, humidity): @pytest.fixture -async def setup_comp_0(hass): +async def setup_comp_0(hass: HomeAssistant) -> None: """Initialize components.""" _setup_sensor(hass, 45) hass.states.async_set(ENT_SWITCH, STATE_OFF) @@ -235,7 +238,7 @@ async def setup_comp_0(hass): @pytest.fixture -async def setup_comp_2(hass): +async def setup_comp_2(hass: HomeAssistant) -> None: """Initialize components.""" _setup_sensor(hass, 45) hass.states.async_set(ENT_SWITCH, STATE_OFF) @@ -307,7 +310,8 @@ async def test_setup_defaults_to_unknown(hass: HomeAssistant) -> None: assert hass.states.get(ENTITY).state == STATE_UNAVAILABLE -async def test_default_setup_params(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_default_setup_params(hass: HomeAssistant) -> None: """Test the setup with default parameters.""" state = hass.states.get(ENTITY) assert state.attributes.get("min_humidity") == 0 @@ -316,9 +320,8 @@ async def test_default_setup_params(hass: HomeAssistant, setup_comp_2) -> None: assert state.attributes.get("action") == "idle" -async def test_default_setup_params_dehumidifier( - hass: HomeAssistant, setup_comp_0 -) -> None: +@pytest.mark.usefixtures("setup_comp_0") +async def test_default_setup_params_dehumidifier(hass: HomeAssistant) -> None: """Test the setup with default parameters for dehumidifier.""" state = hass.states.get(ENTITY) assert state.attributes.get("min_humidity") == 0 @@ -327,14 +330,16 @@ async def test_default_setup_params_dehumidifier( assert state.attributes.get("action") == "idle" -async def test_get_modes(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_get_modes(hass: HomeAssistant) -> None: """Test that the attributes returns the correct modes.""" state = hass.states.get(ENTITY) modes = state.attributes.get("available_modes") assert modes == [MODE_NORMAL, MODE_AWAY] -async def test_set_target_humidity(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_set_target_humidity(hass: HomeAssistant) -> None: """Test the setting of the target humidity.""" await hass.services.async_call( DOMAIN, @@ -357,7 +362,8 @@ async def test_set_target_humidity(hass: HomeAssistant, setup_comp_2) -> None: assert state.attributes.get("humidity") == 40 -async def test_set_away_mode(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_set_away_mode(hass: HomeAssistant) -> None: """Test the setting away mode.""" await hass.services.async_call( DOMAIN, @@ -377,9 +383,8 @@ async def test_set_away_mode(hass: HomeAssistant, setup_comp_2) -> None: assert state.attributes.get("humidity") == 35 -async def test_set_away_mode_and_restore_prev_humidity( - hass: HomeAssistant, setup_comp_2 -) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_set_away_mode_and_restore_prev_humidity(hass: HomeAssistant) -> None: """Test the setting and removing away mode. Verify original humidity is restored. @@ -411,8 +416,9 @@ async def test_set_away_mode_and_restore_prev_humidity( assert state.attributes.get("humidity") == 44 +@pytest.mark.usefixtures("setup_comp_2") async def test_set_away_mode_twice_and_restore_prev_humidity( - hass: HomeAssistant, setup_comp_2 + hass: HomeAssistant, ) -> None: """Test the setting away mode twice in a row. @@ -452,7 +458,8 @@ async def test_set_away_mode_twice_and_restore_prev_humidity( assert state.attributes.get("humidity") == 44 -async def test_sensor_affects_attribute(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_sensor_affects_attribute(hass: HomeAssistant) -> None: """Test that the sensor changes are reflected in the current_humidity attribute.""" state = hass.states.get(ENTITY) assert state.attributes.get("current_humidity") == 45 @@ -464,7 +471,8 @@ async def test_sensor_affects_attribute(hass: HomeAssistant, setup_comp_2) -> No assert state.attributes.get("current_humidity") == 47 -async def test_sensor_bad_value(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_sensor_bad_value(hass: HomeAssistant) -> None: """Test sensor that have None as state.""" assert hass.states.get(ENTITY).state == STATE_ON @@ -474,8 +482,9 @@ async def test_sensor_bad_value(hass: HomeAssistant, setup_comp_2) -> None: assert hass.states.get(ENTITY).state == STATE_UNAVAILABLE +@pytest.mark.usefixtures("setup_comp_2") async def test_sensor_bad_value_twice( - hass: HomeAssistant, setup_comp_2, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test sensor that the second bad value is not logged as warning.""" assert hass.states.get(ENTITY).state == STATE_ON @@ -503,9 +512,8 @@ async def test_sensor_bad_value_twice( ] == ["DEBUG"] -async def test_set_target_humidity_humidifier_on( - hass: HomeAssistant, setup_comp_2 -) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_set_target_humidity_humidifier_on(hass: HomeAssistant) -> None: """Test if target humidity turn humidifier on.""" calls = await _setup_switch(hass, False) _setup_sensor(hass, 36) @@ -524,9 +532,8 @@ async def test_set_target_humidity_humidifier_on( assert call.data["entity_id"] == ENT_SWITCH -async def test_set_target_humidity_humidifier_off( - hass: HomeAssistant, setup_comp_2 -) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_set_target_humidity_humidifier_off(hass: HomeAssistant) -> None: """Test if target humidity turn humidifier off.""" calls = await _setup_switch(hass, True) _setup_sensor(hass, 45) @@ -545,8 +552,9 @@ async def test_set_target_humidity_humidifier_off( assert call.data["entity_id"] == ENT_SWITCH +@pytest.mark.usefixtures("setup_comp_2") async def test_humidity_change_humidifier_on_within_tolerance( - hass: HomeAssistant, setup_comp_2 + hass: HomeAssistant, ) -> None: """Test if humidity change doesn't turn on within tolerance.""" calls = await _setup_switch(hass, False) @@ -562,8 +570,9 @@ async def test_humidity_change_humidifier_on_within_tolerance( assert len(calls) == 0 +@pytest.mark.usefixtures("setup_comp_2") async def test_humidity_change_humidifier_on_outside_tolerance( - hass: HomeAssistant, setup_comp_2 + hass: HomeAssistant, ) -> None: """Test if humidity change turn humidifier on outside dry tolerance.""" calls = await _setup_switch(hass, False) @@ -583,8 +592,9 @@ async def test_humidity_change_humidifier_on_outside_tolerance( assert call.data["entity_id"] == ENT_SWITCH +@pytest.mark.usefixtures("setup_comp_2") async def test_humidity_change_humidifier_off_within_tolerance( - hass: HomeAssistant, setup_comp_2 + hass: HomeAssistant, ) -> None: """Test if humidity change doesn't turn off within tolerance.""" calls = await _setup_switch(hass, True) @@ -600,8 +610,9 @@ async def test_humidity_change_humidifier_off_within_tolerance( assert len(calls) == 0 +@pytest.mark.usefixtures("setup_comp_2") async def test_humidity_change_humidifier_off_outside_tolerance( - hass: HomeAssistant, setup_comp_2 + hass: HomeAssistant, ) -> None: """Test if humidity change turn humidifier off outside wet tolerance.""" calls = await _setup_switch(hass, True) @@ -621,7 +632,8 @@ async def test_humidity_change_humidifier_off_outside_tolerance( assert call.data["entity_id"] == ENT_SWITCH -async def test_operation_mode_humidify(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_operation_mode_humidify(hass: HomeAssistant) -> None: """Test change mode from OFF to HUMIDIFY. Switch turns on when humidity below setpoint and mode changes. @@ -675,7 +687,7 @@ async def _setup_switch(hass, is_on): @pytest.fixture -async def setup_comp_3(hass): +async def setup_comp_3(hass: HomeAssistant) -> None: """Initialize components.""" assert await async_setup_component( hass, @@ -698,7 +710,8 @@ async def setup_comp_3(hass): await hass.async_block_till_done() -async def test_set_target_humidity_dry_off(hass: HomeAssistant, setup_comp_3) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_set_target_humidity_dry_off(hass: HomeAssistant) -> None: """Test if target humidity turn dry off.""" calls = await _setup_switch(hass, True) _setup_sensor(hass, 50) @@ -718,7 +731,8 @@ async def test_set_target_humidity_dry_off(hass: HomeAssistant, setup_comp_3) -> assert hass.states.get(ENTITY).attributes.get("action") == "drying" -async def test_turn_away_mode_on_drying(hass: HomeAssistant, setup_comp_3) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_turn_away_mode_on_drying(hass: HomeAssistant) -> None: """Test the setting away mode when drying.""" await _setup_switch(hass, True) _setup_sensor(hass, 50) @@ -741,7 +755,8 @@ async def test_turn_away_mode_on_drying(hass: HomeAssistant, setup_comp_3) -> No assert state.attributes.get("humidity") == 30 -async def test_operation_mode_dry(hass: HomeAssistant, setup_comp_3) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_operation_mode_dry(hass: HomeAssistant) -> None: """Test change mode from OFF to DRY. Switch turns on when humidity below setpoint and state changes. @@ -774,7 +789,8 @@ async def test_operation_mode_dry(hass: HomeAssistant, setup_comp_3) -> None: assert call.data["entity_id"] == ENT_SWITCH -async def test_set_target_humidity_dry_on(hass: HomeAssistant, setup_comp_3) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_set_target_humidity_dry_on(hass: HomeAssistant) -> None: """Test if target humidity turn dry on.""" calls = await _setup_switch(hass, False) _setup_sensor(hass, 45) @@ -786,7 +802,8 @@ async def test_set_target_humidity_dry_on(hass: HomeAssistant, setup_comp_3) -> assert call.data["entity_id"] == ENT_SWITCH -async def test_init_ignores_tolerance(hass: HomeAssistant, setup_comp_3) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_init_ignores_tolerance(hass: HomeAssistant) -> None: """Test if tolerance is ignored on initialization.""" calls = await _setup_switch(hass, True) _setup_sensor(hass, 39) @@ -798,9 +815,8 @@ async def test_init_ignores_tolerance(hass: HomeAssistant, setup_comp_3) -> None assert call.data["entity_id"] == ENT_SWITCH -async def test_humidity_change_dry_off_within_tolerance( - hass: HomeAssistant, setup_comp_3 -) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_humidity_change_dry_off_within_tolerance(hass: HomeAssistant) -> None: """Test if humidity change doesn't turn dry off within tolerance.""" calls = await _setup_switch(hass, True) _setup_sensor(hass, 45) @@ -809,8 +825,9 @@ async def test_humidity_change_dry_off_within_tolerance( assert len(calls) == 0 +@pytest.mark.usefixtures("setup_comp_3") async def test_set_humidity_change_dry_off_outside_tolerance( - hass: HomeAssistant, setup_comp_3 + hass: HomeAssistant, ) -> None: """Test if humidity change turn dry off.""" calls = await _setup_switch(hass, True) @@ -823,9 +840,8 @@ async def test_set_humidity_change_dry_off_outside_tolerance( assert call.data["entity_id"] == ENT_SWITCH -async def test_humidity_change_dry_on_within_tolerance( - hass: HomeAssistant, setup_comp_3 -) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_humidity_change_dry_on_within_tolerance(hass: HomeAssistant) -> None: """Test if humidity change doesn't turn dry on within tolerance.""" calls = await _setup_switch(hass, False) _setup_sensor(hass, 37) @@ -834,9 +850,8 @@ async def test_humidity_change_dry_on_within_tolerance( assert len(calls) == 0 -async def test_humidity_change_dry_on_outside_tolerance( - hass: HomeAssistant, setup_comp_3 -) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_humidity_change_dry_on_outside_tolerance(hass: HomeAssistant) -> None: """Test if humidity change turn dry on.""" calls = await _setup_switch(hass, False) _setup_sensor(hass, 45) @@ -848,9 +863,8 @@ async def test_humidity_change_dry_on_outside_tolerance( assert call.data["entity_id"] == ENT_SWITCH -async def test_running_when_operating_mode_is_off_2( - hass: HomeAssistant, setup_comp_3 -) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_running_when_operating_mode_is_off_2(hass: HomeAssistant) -> None: """Test that the switch turns off when enabled is set False.""" calls = await _setup_switch(hass, True) _setup_sensor(hass, 45) @@ -870,9 +884,8 @@ async def test_running_when_operating_mode_is_off_2( assert hass.states.get(ENTITY).attributes.get("action") == "off" -async def test_no_state_change_when_operation_mode_off_2( - hass: HomeAssistant, setup_comp_3 -) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_no_state_change_when_operation_mode_off_2(hass: HomeAssistant) -> None: """Test that the switch doesn't turn on when enabled is False.""" calls = await _setup_switch(hass, False) _setup_sensor(hass, 30) @@ -891,7 +904,7 @@ async def test_no_state_change_when_operation_mode_off_2( @pytest.fixture -async def setup_comp_4(hass): +async def setup_comp_4(hass: HomeAssistant) -> None: """Initialize components.""" assert await async_setup_component( hass, @@ -914,8 +927,9 @@ async def setup_comp_4(hass): await hass.async_block_till_done() +@pytest.mark.usefixtures("setup_comp_4") async def test_humidity_change_dry_trigger_on_not_long_enough( - hass: HomeAssistant, setup_comp_4 + hass: HomeAssistant, ) -> None: """Test if humidity change turn dry on.""" calls = await _setup_switch(hass, False) @@ -928,9 +942,8 @@ async def test_humidity_change_dry_trigger_on_not_long_enough( assert len(calls) == 0 -async def test_humidity_change_dry_trigger_on_long_enough( - hass: HomeAssistant, setup_comp_4 -) -> None: +@pytest.mark.usefixtures("setup_comp_4") +async def test_humidity_change_dry_trigger_on_long_enough(hass: HomeAssistant) -> None: """Test if humidity change turn dry on.""" fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=datetime.UTC) with freeze_time(fake_changed): @@ -948,8 +961,9 @@ async def test_humidity_change_dry_trigger_on_long_enough( assert call.data["entity_id"] == ENT_SWITCH +@pytest.mark.usefixtures("setup_comp_4") async def test_humidity_change_dry_trigger_off_not_long_enough( - hass: HomeAssistant, setup_comp_4 + hass: HomeAssistant, ) -> None: """Test if humidity change turn dry on.""" calls = await _setup_switch(hass, True) @@ -962,9 +976,8 @@ async def test_humidity_change_dry_trigger_off_not_long_enough( assert len(calls) == 0 -async def test_humidity_change_dry_trigger_off_long_enough( - hass: HomeAssistant, setup_comp_4 -) -> None: +@pytest.mark.usefixtures("setup_comp_4") +async def test_humidity_change_dry_trigger_off_long_enough(hass: HomeAssistant) -> None: """Test if humidity change turn dry on.""" fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=datetime.UTC) with freeze_time(fake_changed): @@ -982,9 +995,8 @@ async def test_humidity_change_dry_trigger_off_long_enough( assert call.data["entity_id"] == ENT_SWITCH -async def test_mode_change_dry_trigger_off_not_long_enough( - hass: HomeAssistant, setup_comp_4 -) -> None: +@pytest.mark.usefixtures("setup_comp_4") +async def test_mode_change_dry_trigger_off_not_long_enough(hass: HomeAssistant) -> None: """Test if mode change turns dry off despite minimum cycle.""" calls = await _setup_switch(hass, True) _setup_sensor(hass, 45) @@ -1004,9 +1016,8 @@ async def test_mode_change_dry_trigger_off_not_long_enough( assert call.data["entity_id"] == ENT_SWITCH -async def test_mode_change_dry_trigger_on_not_long_enough( - hass: HomeAssistant, setup_comp_4 -) -> None: +@pytest.mark.usefixtures("setup_comp_4") +async def test_mode_change_dry_trigger_on_not_long_enough(hass: HomeAssistant) -> None: """Test if mode change turns dry on despite minimum cycle.""" calls = await _setup_switch(hass, False) _setup_sensor(hass, 35) @@ -1036,7 +1047,7 @@ async def test_mode_change_dry_trigger_on_not_long_enough( @pytest.fixture -async def setup_comp_6(hass): +async def setup_comp_6(hass: HomeAssistant) -> None: """Initialize components.""" assert await async_setup_component( hass, @@ -1058,8 +1069,9 @@ async def setup_comp_6(hass): await hass.async_block_till_done() +@pytest.mark.usefixtures("setup_comp_6") async def test_humidity_change_humidifier_trigger_off_not_long_enough( - hass: HomeAssistant, setup_comp_6 + hass: HomeAssistant, ) -> None: """Test if humidity change doesn't turn humidifier off because of time.""" calls = await _setup_switch(hass, True) @@ -1072,8 +1084,9 @@ async def test_humidity_change_humidifier_trigger_off_not_long_enough( assert len(calls) == 0 +@pytest.mark.usefixtures("setup_comp_6") async def test_humidity_change_humidifier_trigger_on_not_long_enough( - hass: HomeAssistant, setup_comp_6 + hass: HomeAssistant, ) -> None: """Test if humidity change doesn't turn humidifier on because of time.""" calls = await _setup_switch(hass, False) @@ -1086,8 +1099,9 @@ async def test_humidity_change_humidifier_trigger_on_not_long_enough( assert len(calls) == 0 +@pytest.mark.usefixtures("setup_comp_6") async def test_humidity_change_humidifier_trigger_on_long_enough( - hass: HomeAssistant, setup_comp_6 + hass: HomeAssistant, ) -> None: """Test if humidity change turn humidifier on after min cycle.""" fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=datetime.UTC) @@ -1106,8 +1120,9 @@ async def test_humidity_change_humidifier_trigger_on_long_enough( assert call.data["entity_id"] == ENT_SWITCH +@pytest.mark.usefixtures("setup_comp_6") async def test_humidity_change_humidifier_trigger_off_long_enough( - hass: HomeAssistant, setup_comp_6 + hass: HomeAssistant, ) -> None: """Test if humidity change turn humidifier off after min cycle.""" fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=datetime.UTC) @@ -1126,8 +1141,9 @@ async def test_humidity_change_humidifier_trigger_off_long_enough( assert call.data["entity_id"] == ENT_SWITCH +@pytest.mark.usefixtures("setup_comp_6") async def test_mode_change_humidifier_trigger_off_not_long_enough( - hass: HomeAssistant, setup_comp_6 + hass: HomeAssistant, ) -> None: """Test if mode change turns humidifier off despite minimum cycle.""" calls = await _setup_switch(hass, True) @@ -1149,8 +1165,9 @@ async def test_mode_change_humidifier_trigger_off_not_long_enough( assert call.data["entity_id"] == ENT_SWITCH +@pytest.mark.usefixtures("setup_comp_6") async def test_mode_change_humidifier_trigger_on_not_long_enough( - hass: HomeAssistant, setup_comp_6 + hass: HomeAssistant, ) -> None: """Test if mode change turns humidifier on despite minimum cycle.""" calls = await _setup_switch(hass, False) @@ -1186,7 +1203,7 @@ async def test_mode_change_humidifier_trigger_on_not_long_enough( @pytest.fixture -async def setup_comp_7(hass): +async def setup_comp_7(hass: HomeAssistant) -> None: """Initialize components.""" assert await async_setup_component( hass, @@ -1210,8 +1227,9 @@ async def setup_comp_7(hass): await hass.async_block_till_done() +@pytest.mark.usefixtures("setup_comp_7") async def test_humidity_change_dry_trigger_on_long_enough_3( - hass: HomeAssistant, setup_comp_7 + hass: HomeAssistant, ) -> None: """Test if turn on signal is sent at keep-alive intervals.""" calls = await _setup_switch(hass, True) @@ -1230,8 +1248,9 @@ async def test_humidity_change_dry_trigger_on_long_enough_3( assert call.data["entity_id"] == ENT_SWITCH +@pytest.mark.usefixtures("setup_comp_7") async def test_humidity_change_dry_trigger_off_long_enough_3( - hass: HomeAssistant, setup_comp_7 + hass: HomeAssistant, ) -> None: """Test if turn on signal is sent at keep-alive intervals.""" calls = await _setup_switch(hass, False) @@ -1251,7 +1270,7 @@ async def test_humidity_change_dry_trigger_off_long_enough_3( @pytest.fixture -async def setup_comp_8(hass): +async def setup_comp_8(hass: HomeAssistant) -> None: """Initialize components.""" assert await async_setup_component( hass, @@ -1274,8 +1293,9 @@ async def setup_comp_8(hass): await hass.async_block_till_done() +@pytest.mark.usefixtures("setup_comp_8") async def test_humidity_change_humidifier_trigger_on_long_enough_2( - hass: HomeAssistant, setup_comp_8 + hass: HomeAssistant, ) -> None: """Test if turn on signal is sent at keep-alive intervals.""" calls = await _setup_switch(hass, True) @@ -1294,8 +1314,9 @@ async def test_humidity_change_humidifier_trigger_on_long_enough_2( assert call.data["entity_id"] == ENT_SWITCH +@pytest.mark.usefixtures("setup_comp_8") async def test_humidity_change_humidifier_trigger_off_long_enough_2( - hass: HomeAssistant, setup_comp_8 + hass: HomeAssistant, ) -> None: """Test if turn on signal is sent at keep-alive intervals.""" calls = await _setup_switch(hass, False) @@ -1706,8 +1727,9 @@ async def test_away_fixed_humidity_mode(hass: HomeAssistant) -> None: assert state.state == STATE_OFF +@pytest.mark.usefixtures("setup_comp_1") async def test_sensor_stale_duration( - hass: HomeAssistant, setup_comp_1, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test turn off on sensor stale.""" diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 7fb3e11e189..3ea38a22c3c 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -103,14 +103,15 @@ async def test_valid_conf(hass: HomeAssistant) -> None: @pytest.fixture -async def setup_comp_1(hass): +async def setup_comp_1(hass: HomeAssistant) -> None: """Initialize components.""" hass.config.units = METRIC_SYSTEM assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() -async def test_heater_input_boolean(hass: HomeAssistant, setup_comp_1) -> None: +@pytest.mark.usefixtures("setup_comp_1") +async def test_heater_input_boolean(hass: HomeAssistant) -> None: """Test heater switching input_boolean.""" heater_switch = "input_boolean.test" assert await async_setup_component( @@ -142,8 +143,9 @@ async def test_heater_input_boolean(hass: HomeAssistant, setup_comp_1) -> None: assert hass.states.get(heater_switch).state == STATE_ON +@pytest.mark.usefixtures("setup_comp_1") async def test_heater_switch( - hass: HomeAssistant, setup_comp_1, mock_switch_entities: list[MockSwitch] + hass: HomeAssistant, mock_switch_entities: list[MockSwitch] ) -> None: """Test heater switching test switch.""" setup_test_component_platform(hass, switch.DOMAIN, mock_switch_entities) @@ -178,8 +180,9 @@ async def test_heater_switch( assert hass.states.get(heater_switch).state == STATE_ON +@pytest.mark.usefixtures("setup_comp_1") async def test_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, setup_comp_1 + hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test setting a unique ID.""" unique_id = "some_unique_id" @@ -211,7 +214,7 @@ def _setup_sensor(hass, temp): @pytest.fixture -async def setup_comp_2(hass): +async def setup_comp_2(hass: HomeAssistant) -> None: """Initialize components.""" hass.config.units = METRIC_SYSTEM assert await async_setup_component( @@ -284,7 +287,8 @@ async def test_setup_gets_current_temp_from_sensor(hass: HomeAssistant) -> None: assert hass.states.get(ENTITY).attributes["current_temperature"] == 18 -async def test_default_setup_params(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_default_setup_params(hass: HomeAssistant) -> None: """Test the setup with default parameters.""" state = hass.states.get(ENTITY) assert state.attributes.get("min_temp") == 7 @@ -293,14 +297,16 @@ async def test_default_setup_params(hass: HomeAssistant, setup_comp_2) -> None: assert state.attributes.get("target_temp_step") == 0.1 -async def test_get_hvac_modes(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_get_hvac_modes(hass: HomeAssistant) -> None: """Test that the operation list returns the correct modes.""" state = hass.states.get(ENTITY) modes = state.attributes.get("hvac_modes") assert modes == [HVACMode.HEAT, HVACMode.OFF] -async def test_set_target_temp(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_set_target_temp(hass: HomeAssistant) -> None: """Test the setting of the target temperature.""" await common.async_set_temperature(hass, 30) state = hass.states.get(ENTITY) @@ -323,7 +329,8 @@ async def test_set_target_temp(hass: HomeAssistant, setup_comp_2) -> None: (PRESET_ACTIVITY, 21), ], ) -async def test_set_away_mode(hass: HomeAssistant, setup_comp_2, preset, temp) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_set_away_mode(hass: HomeAssistant, preset, temp) -> None: """Test the setting away mode.""" await common.async_set_temperature(hass, 23) await common.async_set_preset_mode(hass, preset) @@ -343,8 +350,9 @@ async def test_set_away_mode(hass: HomeAssistant, setup_comp_2, preset, temp) -> (PRESET_ACTIVITY, 21), ], ) +@pytest.mark.usefixtures("setup_comp_2") async def test_set_away_mode_and_restore_prev_temp( - hass: HomeAssistant, setup_comp_2, preset, temp + hass: HomeAssistant, preset, temp ) -> None: """Test the setting and removing away mode. @@ -371,8 +379,9 @@ async def test_set_away_mode_and_restore_prev_temp( (PRESET_ACTIVITY, 21), ], ) +@pytest.mark.usefixtures("setup_comp_2") async def test_set_away_mode_twice_and_restore_prev_temp( - hass: HomeAssistant, setup_comp_2, preset, temp + hass: HomeAssistant, preset, temp ) -> None: """Test the setting away mode twice in a row. @@ -388,7 +397,8 @@ async def test_set_away_mode_twice_and_restore_prev_temp( assert state.attributes.get("temperature") == 23 -async def test_set_preset_mode_invalid(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_set_preset_mode_invalid(hass: HomeAssistant) -> None: """Test an invalid mode raises an error and ignore case when checking modes.""" await common.async_set_temperature(hass, 23) await common.async_set_preset_mode(hass, "away") @@ -403,7 +413,8 @@ async def test_set_preset_mode_invalid(hass: HomeAssistant, setup_comp_2) -> Non assert state.attributes.get("preset_mode") == "none" -async def test_sensor_bad_value(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_sensor_bad_value(hass: HomeAssistant) -> None: """Test sensor that have None as state.""" state = hass.states.get(ENTITY) temp = state.attributes.get("current_temperature") @@ -464,7 +475,8 @@ async def test_sensor_unavailable(hass: HomeAssistant) -> None: assert state.attributes.get("current_temperature") is None -async def test_set_target_temp_heater_on(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_set_target_temp_heater_on(hass: HomeAssistant) -> None: """Test if target temperature turn heater on.""" calls = _setup_switch(hass, False) _setup_sensor(hass, 25) @@ -477,7 +489,8 @@ async def test_set_target_temp_heater_on(hass: HomeAssistant, setup_comp_2) -> N assert call.data["entity_id"] == ENT_SWITCH -async def test_set_target_temp_heater_off(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_set_target_temp_heater_off(hass: HomeAssistant) -> None: """Test if target temperature turn heater off.""" calls = _setup_switch(hass, True) _setup_sensor(hass, 30) @@ -490,9 +503,8 @@ async def test_set_target_temp_heater_off(hass: HomeAssistant, setup_comp_2) -> assert call.data["entity_id"] == ENT_SWITCH -async def test_temp_change_heater_on_within_tolerance( - hass: HomeAssistant, setup_comp_2 -) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_temp_change_heater_on_within_tolerance(hass: HomeAssistant) -> None: """Test if temperature change doesn't turn on within tolerance.""" calls = _setup_switch(hass, False) await common.async_set_temperature(hass, 30) @@ -501,9 +513,8 @@ async def test_temp_change_heater_on_within_tolerance( assert len(calls) == 0 -async def test_temp_change_heater_on_outside_tolerance( - hass: HomeAssistant, setup_comp_2 -) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_temp_change_heater_on_outside_tolerance(hass: HomeAssistant) -> None: """Test if temperature change turn heater on outside cold tolerance.""" calls = _setup_switch(hass, False) await common.async_set_temperature(hass, 30) @@ -516,9 +527,8 @@ async def test_temp_change_heater_on_outside_tolerance( assert call.data["entity_id"] == ENT_SWITCH -async def test_temp_change_heater_off_within_tolerance( - hass: HomeAssistant, setup_comp_2 -) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_temp_change_heater_off_within_tolerance(hass: HomeAssistant) -> None: """Test if temperature change doesn't turn off within tolerance.""" calls = _setup_switch(hass, True) await common.async_set_temperature(hass, 30) @@ -527,9 +537,8 @@ async def test_temp_change_heater_off_within_tolerance( assert len(calls) == 0 -async def test_temp_change_heater_off_outside_tolerance( - hass: HomeAssistant, setup_comp_2 -) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_temp_change_heater_off_outside_tolerance(hass: HomeAssistant) -> None: """Test if temperature change turn heater off outside hot tolerance.""" calls = _setup_switch(hass, True) await common.async_set_temperature(hass, 30) @@ -542,7 +551,8 @@ async def test_temp_change_heater_off_outside_tolerance( assert call.data["entity_id"] == ENT_SWITCH -async def test_running_when_hvac_mode_is_off(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_running_when_hvac_mode_is_off(hass: HomeAssistant) -> None: """Test that the switch turns off when enabled is set False.""" calls = _setup_switch(hass, True) await common.async_set_temperature(hass, 30) @@ -554,9 +564,8 @@ async def test_running_when_hvac_mode_is_off(hass: HomeAssistant, setup_comp_2) assert call.data["entity_id"] == ENT_SWITCH -async def test_no_state_change_when_hvac_mode_off( - hass: HomeAssistant, setup_comp_2 -) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_no_state_change_when_hvac_mode_off(hass: HomeAssistant) -> None: """Test that the switch doesn't turn on when enabled is False.""" calls = _setup_switch(hass, False) await common.async_set_temperature(hass, 30) @@ -566,7 +575,8 @@ async def test_no_state_change_when_hvac_mode_off( assert len(calls) == 0 -async def test_hvac_mode_heat(hass: HomeAssistant, setup_comp_2) -> None: +@pytest.mark.usefixtures("setup_comp_2") +async def test_hvac_mode_heat(hass: HomeAssistant) -> None: """Test change mode from OFF to HEAT. Switch turns on when temp below setpoint and mode changes. @@ -601,7 +611,7 @@ def _setup_switch(hass, is_on): @pytest.fixture -async def setup_comp_3(hass): +async def setup_comp_3(hass: HomeAssistant) -> None: """Initialize components.""" hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( @@ -624,7 +634,8 @@ async def setup_comp_3(hass): await hass.async_block_till_done() -async def test_set_target_temp_ac_off(hass: HomeAssistant, setup_comp_3) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_set_target_temp_ac_off(hass: HomeAssistant) -> None: """Test if target temperature turn ac off.""" calls = _setup_switch(hass, True) _setup_sensor(hass, 25) @@ -637,7 +648,8 @@ async def test_set_target_temp_ac_off(hass: HomeAssistant, setup_comp_3) -> None assert call.data["entity_id"] == ENT_SWITCH -async def test_turn_away_mode_on_cooling(hass: HomeAssistant, setup_comp_3) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_turn_away_mode_on_cooling(hass: HomeAssistant) -> None: """Test the setting away mode when cooling.""" _setup_switch(hass, True) _setup_sensor(hass, 25) @@ -648,7 +660,8 @@ async def test_turn_away_mode_on_cooling(hass: HomeAssistant, setup_comp_3) -> N assert state.attributes.get("temperature") == 30 -async def test_hvac_mode_cool(hass: HomeAssistant, setup_comp_3) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_hvac_mode_cool(hass: HomeAssistant) -> None: """Test change mode from OFF to COOL. Switch turns on when temp below setpoint and mode changes. @@ -666,7 +679,8 @@ async def test_hvac_mode_cool(hass: HomeAssistant, setup_comp_3) -> None: assert call.data["entity_id"] == ENT_SWITCH -async def test_set_target_temp_ac_on(hass: HomeAssistant, setup_comp_3) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_set_target_temp_ac_on(hass: HomeAssistant) -> None: """Test if target temperature turn ac on.""" calls = _setup_switch(hass, False) _setup_sensor(hass, 30) @@ -679,9 +693,8 @@ async def test_set_target_temp_ac_on(hass: HomeAssistant, setup_comp_3) -> None: assert call.data["entity_id"] == ENT_SWITCH -async def test_temp_change_ac_off_within_tolerance( - hass: HomeAssistant, setup_comp_3 -) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_temp_change_ac_off_within_tolerance(hass: HomeAssistant) -> None: """Test if temperature change doesn't turn ac off within tolerance.""" calls = _setup_switch(hass, True) await common.async_set_temperature(hass, 30) @@ -690,9 +703,8 @@ async def test_temp_change_ac_off_within_tolerance( assert len(calls) == 0 -async def test_set_temp_change_ac_off_outside_tolerance( - hass: HomeAssistant, setup_comp_3 -) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_set_temp_change_ac_off_outside_tolerance(hass: HomeAssistant) -> None: """Test if temperature change turn ac off.""" calls = _setup_switch(hass, True) await common.async_set_temperature(hass, 30) @@ -705,9 +717,8 @@ async def test_set_temp_change_ac_off_outside_tolerance( assert call.data["entity_id"] == ENT_SWITCH -async def test_temp_change_ac_on_within_tolerance( - hass: HomeAssistant, setup_comp_3 -) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_temp_change_ac_on_within_tolerance(hass: HomeAssistant) -> None: """Test if temperature change doesn't turn ac on within tolerance.""" calls = _setup_switch(hass, False) await common.async_set_temperature(hass, 25) @@ -716,9 +727,8 @@ async def test_temp_change_ac_on_within_tolerance( assert len(calls) == 0 -async def test_temp_change_ac_on_outside_tolerance( - hass: HomeAssistant, setup_comp_3 -) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_temp_change_ac_on_outside_tolerance(hass: HomeAssistant) -> None: """Test if temperature change turn ac on.""" calls = _setup_switch(hass, False) await common.async_set_temperature(hass, 25) @@ -731,9 +741,8 @@ async def test_temp_change_ac_on_outside_tolerance( assert call.data["entity_id"] == ENT_SWITCH -async def test_running_when_operating_mode_is_off_2( - hass: HomeAssistant, setup_comp_3 -) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_running_when_operating_mode_is_off_2(hass: HomeAssistant) -> None: """Test that the switch turns off when enabled is set False.""" calls = _setup_switch(hass, True) await common.async_set_temperature(hass, 30) @@ -745,9 +754,8 @@ async def test_running_when_operating_mode_is_off_2( assert call.data["entity_id"] == ENT_SWITCH -async def test_no_state_change_when_operation_mode_off_2( - hass: HomeAssistant, setup_comp_3 -) -> None: +@pytest.mark.usefixtures("setup_comp_3") +async def test_no_state_change_when_operation_mode_off_2(hass: HomeAssistant) -> None: """Test that the switch doesn't turn on when enabled is False.""" calls = _setup_switch(hass, False) await common.async_set_temperature(hass, 30) @@ -912,7 +920,7 @@ async def test_hvac_mode_change_toggles_heating_cooling_switch_even_when_within_ @pytest.fixture -async def setup_comp_7(hass): +async def setup_comp_7(hass: HomeAssistant) -> None: """Initialize components.""" hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( @@ -938,9 +946,8 @@ async def setup_comp_7(hass): await hass.async_block_till_done() -async def test_temp_change_ac_trigger_on_long_enough_3( - hass: HomeAssistant, setup_comp_7 -) -> None: +@pytest.mark.usefixtures("setup_comp_7") +async def test_temp_change_ac_trigger_on_long_enough_3(hass: HomeAssistant) -> None: """Test if turn on signal is sent at keep-alive intervals.""" calls = _setup_switch(hass, True) await hass.async_block_till_done() @@ -963,9 +970,8 @@ async def test_temp_change_ac_trigger_on_long_enough_3( assert call.data["entity_id"] == ENT_SWITCH -async def test_temp_change_ac_trigger_off_long_enough_3( - hass: HomeAssistant, setup_comp_7 -) -> None: +@pytest.mark.usefixtures("setup_comp_7") +async def test_temp_change_ac_trigger_off_long_enough_3(hass: HomeAssistant) -> None: """Test if turn on signal is sent at keep-alive intervals.""" calls = _setup_switch(hass, False) await hass.async_block_till_done() @@ -989,7 +995,7 @@ async def test_temp_change_ac_trigger_off_long_enough_3( @pytest.fixture -async def setup_comp_8(hass): +async def setup_comp_8(hass: HomeAssistant) -> None: """Initialize components.""" hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( @@ -1013,9 +1019,8 @@ async def setup_comp_8(hass): await hass.async_block_till_done() -async def test_temp_change_heater_trigger_on_long_enough_2( - hass: HomeAssistant, setup_comp_8 -) -> None: +@pytest.mark.usefixtures("setup_comp_8") +async def test_temp_change_heater_trigger_on_long_enough_2(hass: HomeAssistant) -> None: """Test if turn on signal is sent at keep-alive intervals.""" calls = _setup_switch(hass, True) await hass.async_block_till_done() @@ -1038,8 +1043,9 @@ async def test_temp_change_heater_trigger_on_long_enough_2( assert call.data["entity_id"] == ENT_SWITCH +@pytest.mark.usefixtures("setup_comp_8") async def test_temp_change_heater_trigger_off_long_enough_2( - hass: HomeAssistant, setup_comp_8 + hass: HomeAssistant, ) -> None: """Test if turn on signal is sent at keep-alive intervals.""" calls = _setup_switch(hass, False) @@ -1064,7 +1070,7 @@ async def test_temp_change_heater_trigger_off_long_enough_2( @pytest.fixture -async def setup_comp_9(hass): +async def setup_comp_9(hass: HomeAssistant) -> None: """Initialize components.""" assert await async_setup_component( hass, @@ -1087,7 +1093,8 @@ async def setup_comp_9(hass): await hass.async_block_till_done() -async def test_precision(hass: HomeAssistant, setup_comp_9) -> None: +@pytest.mark.usefixtures("setup_comp_9") +async def test_precision(hass: HomeAssistant) -> None: """Test that setting precision to tenths works as intended.""" hass.config.units = US_CUSTOMARY_SYSTEM await common.async_set_temperature(hass, 23.27) From cf96084ea3d161ca2607ff7d2f2edd54b4161627 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:32:00 +0200 Subject: [PATCH 0716/2411] Improve type hints in generic tests (#121166) --- tests/components/generic/conftest.py | 28 +++--- tests/components/generic/test_camera.py | 28 +++--- tests/components/generic/test_config_flow.py | 96 +++++++++++++------- tests/components/generic/test_diagnostics.py | 5 +- 4 files changed, 99 insertions(+), 58 deletions(-) diff --git a/tests/components/generic/conftest.py b/tests/components/generic/conftest.py index 92a9298cbd5..34062aab954 100644 --- a/tests/components/generic/conftest.py +++ b/tests/components/generic/conftest.py @@ -1,7 +1,9 @@ """Test fixtures for the generic component.""" +from __future__ import annotations + from io import BytesIO -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, _patch, patch from PIL import Image import pytest @@ -9,12 +11,14 @@ import respx from homeassistant import config_entries from homeassistant.components.generic.const import DOMAIN +from homeassistant.config_entries import ConfigFlowResult +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @pytest.fixture(scope="package") -def fakeimgbytes_png(): +def fakeimgbytes_png() -> bytes: """Fake image in RAM for testing.""" buf = BytesIO() Image.new("RGB", (1, 1)).save(buf, format="PNG") @@ -22,7 +26,7 @@ def fakeimgbytes_png(): @pytest.fixture(scope="package") -def fakeimgbytes_jpg(): +def fakeimgbytes_jpg() -> bytes: """Fake image in RAM for testing.""" buf = BytesIO() # fake image in ram for testing. Image.new("RGB", (1, 1)).save(buf, format="jpeg") @@ -30,7 +34,7 @@ def fakeimgbytes_jpg(): @pytest.fixture(scope="package") -def fakeimgbytes_svg(): +def fakeimgbytes_svg() -> bytes: """Fake image in RAM for testing.""" return bytes( '', @@ -39,7 +43,7 @@ def fakeimgbytes_svg(): @pytest.fixture(scope="package") -def fakeimgbytes_gif(): +def fakeimgbytes_gif() -> bytes: """Fake image in RAM for testing.""" buf = BytesIO() # fake image in ram for testing. Image.new("RGB", (1, 1)).save(buf, format="gif") @@ -47,19 +51,19 @@ def fakeimgbytes_gif(): @pytest.fixture -def fakeimg_png(fakeimgbytes_png): +def fakeimg_png(fakeimgbytes_png: bytes) -> None: """Set up respx to respond to test url with fake image bytes.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) @pytest.fixture -def fakeimg_gif(fakeimgbytes_gif): +def fakeimg_gif(fakeimgbytes_gif: bytes) -> None: """Set up respx to respond to test url with fake image bytes.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_gif) @pytest.fixture(scope="package") -def mock_create_stream(): +def mock_create_stream() -> _patch[MagicMock]: """Mock create stream.""" mock_stream = Mock() mock_provider = Mock() @@ -75,7 +79,7 @@ def mock_create_stream(): @pytest.fixture -async def user_flow(hass): +async def user_flow(hass: HomeAssistant) -> ConfigFlowResult: """Initiate a user flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -87,7 +91,7 @@ async def user_flow(hass): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass): +def config_entry_fixture(hass: HomeAssistant) -> MockConfigEntry: """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, @@ -112,7 +116,9 @@ def config_entry_fixture(hass): @pytest.fixture -async def setup_entry(hass, config_entry): +async def setup_entry( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> MockConfigEntry: """Set up a config entry ready to be used in tests.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 72a7c32ba25..59ff513ccc9 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -73,7 +73,7 @@ async def help_setup_mock_config_entry( async def test_fetching_url( hass: HomeAssistant, hass_client: ClientSessionGenerator, - fakeimgbytes_png, + fakeimgbytes_png: bytes, caplog: pytest.LogCaptureFixture, ) -> None: """Test that it fetches the given url.""" @@ -132,7 +132,7 @@ async def test_image_caching( hass: HomeAssistant, hass_client: ClientSessionGenerator, freezer: FrozenDateTimeFactory, - fakeimgbytes_png, + fakeimgbytes_png: bytes, ) -> None: """Test that the image is cached and not fetched more often than the framerate indicates.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) @@ -197,7 +197,7 @@ async def test_image_caching( @respx.mock async def test_fetching_without_verify_ssl( - hass: HomeAssistant, hass_client: ClientSessionGenerator, fakeimgbytes_png + hass: HomeAssistant, hass_client: ClientSessionGenerator, fakeimgbytes_png: bytes ) -> None: """Test that it fetches the given url when ssl verify is off.""" respx.get("https://example.com").respond(stream=fakeimgbytes_png) @@ -221,7 +221,7 @@ async def test_fetching_without_verify_ssl( @respx.mock async def test_fetching_url_with_verify_ssl( - hass: HomeAssistant, hass_client: ClientSessionGenerator, fakeimgbytes_png + hass: HomeAssistant, hass_client: ClientSessionGenerator, fakeimgbytes_png: bytes ) -> None: """Test that it fetches the given url when ssl verify is explicitly on.""" respx.get("https://example.com").respond(stream=fakeimgbytes_png) @@ -247,8 +247,8 @@ async def test_fetching_url_with_verify_ssl( async def test_limit_refetch( hass: HomeAssistant, hass_client: ClientSessionGenerator, - fakeimgbytes_png, - fakeimgbytes_jpg, + fakeimgbytes_png: bytes, + fakeimgbytes_jpg: bytes, ) -> None: """Test that it fetches the given url.""" respx.get("http://example.com/0a").respond(stream=fakeimgbytes_png) @@ -319,7 +319,7 @@ async def test_stream_source( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_ws_client: WebSocketGenerator, - fakeimgbytes_png, + fakeimgbytes_png: bytes, ) -> None: """Test that the stream source is rendered.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) @@ -376,7 +376,7 @@ async def test_stream_source_error( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_ws_client: WebSocketGenerator, - fakeimgbytes_png, + fakeimgbytes_png: bytes, ) -> None: """Test that the stream source has an error.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) @@ -418,7 +418,7 @@ async def test_stream_source_error( @respx.mock async def test_setup_alternative_options( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, fakeimgbytes_png + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, fakeimgbytes_png: bytes ) -> None: """Test that the stream source is setup with different config options.""" respx.get("https://example.com").respond(stream=fakeimgbytes_png) @@ -442,7 +442,7 @@ async def test_no_stream_source( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_ws_client: WebSocketGenerator, - fakeimgbytes_png, + fakeimgbytes_png: bytes, ) -> None: """Test a stream request without stream source option set.""" respx.get("https://example.com").respond(stream=fakeimgbytes_png) @@ -482,8 +482,8 @@ async def test_no_stream_source( async def test_camera_content_type( hass: HomeAssistant, hass_client: ClientSessionGenerator, - fakeimgbytes_svg, - fakeimgbytes_jpg, + fakeimgbytes_svg: bytes, + fakeimgbytes_jpg: bytes, ) -> None: """Test generic camera with custom content_type.""" urlsvg = "https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg" @@ -532,8 +532,8 @@ async def test_camera_content_type( async def test_timeout_cancelled( hass: HomeAssistant, hass_client: ClientSessionGenerator, - fakeimgbytes_png, - fakeimgbytes_jpg, + fakeimgbytes_png: bytes, + fakeimgbytes_jpg: bytes, ) -> None: """Test that timeouts and cancellations return last image.""" diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 8c2d52f173b..c62142fb4d3 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -1,11 +1,13 @@ """Test The generic (IP Camera) config flow.""" +from __future__ import annotations + import contextlib import errno from http import HTTPStatus import os.path from pathlib import Path -from unittest.mock import AsyncMock, PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock, PropertyMock, _patch, patch import httpx import pytest @@ -28,7 +30,7 @@ from homeassistant.components.stream import ( CONF_USE_WALLCLOCK_AS_TIMESTAMPS, ) from homeassistant.components.stream.worker import StreamWorkerError -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import ConfigEntryState, ConfigFlowResult from homeassistant.const import ( CONF_AUTHENTICATION, CONF_NAME, @@ -68,10 +70,10 @@ TESTDATA_YAML = { @respx.mock async def test_form( hass: HomeAssistant, - fakeimgbytes_png, + fakeimgbytes_png: bytes, hass_client: ClientSessionGenerator, - user_flow, - mock_create_stream, + user_flow: ConfigFlowResult, + mock_create_stream: _patch[MagicMock], ) -> None: """Test the form with a normal set of settings.""" @@ -122,8 +124,9 @@ async def test_form( @respx.mock +@pytest.mark.usefixtures("fakeimg_png") async def test_form_only_stillimage( - hass: HomeAssistant, fakeimg_png, user_flow + hass: HomeAssistant, user_flow: ConfigFlowResult ) -> None: """Test we complete ok if the user wants still images only.""" result = await hass.config_entries.flow.async_init( @@ -164,7 +167,10 @@ async def test_form_only_stillimage( @respx.mock async def test_form_reject_still_preview( - hass: HomeAssistant, fakeimgbytes_png, mock_create_stream, user_flow + hass: HomeAssistant, + fakeimgbytes_png: bytes, + mock_create_stream: _patch[MagicMock], + user_flow: ConfigFlowResult, ) -> None: """Test we go back to the config screen if the user rejects the still preview.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) @@ -184,11 +190,11 @@ async def test_form_reject_still_preview( @respx.mock +@pytest.mark.usefixtures("fakeimg_png") async def test_form_still_preview_cam_off( hass: HomeAssistant, - fakeimg_png, - mock_create_stream, - user_flow, + mock_create_stream: _patch[MagicMock], + user_flow: ConfigFlowResult, hass_client: ClientSessionGenerator, ) -> None: """Test camera errors are triggered during preview.""" @@ -213,8 +219,9 @@ async def test_form_still_preview_cam_off( @respx.mock +@pytest.mark.usefixtures("fakeimg_gif") async def test_form_only_stillimage_gif( - hass: HomeAssistant, fakeimg_gif, user_flow + hass: HomeAssistant, user_flow: ConfigFlowResult ) -> None: """Test we complete ok if the user wants a gif.""" data = TESTDATA.copy() @@ -237,7 +244,7 @@ async def test_form_only_stillimage_gif( @respx.mock async def test_form_only_svg_whitespace( - hass: HomeAssistant, fakeimgbytes_svg, user_flow + hass: HomeAssistant, fakeimgbytes_svg: bytes, user_flow: ConfigFlowResult ) -> None: """Test we complete ok if svg starts with whitespace, issue #68889.""" fakeimgbytes_wspace_svg = bytes(" \n ", encoding="utf-8") + fakeimgbytes_svg @@ -271,7 +278,7 @@ async def test_form_only_svg_whitespace( ], ) async def test_form_only_still_sample( - hass: HomeAssistant, user_flow, image_file + hass: HomeAssistant, user_flow: ConfigFlowResult, image_file ) -> None: """Test various sample images #69037.""" image_path = os.path.join(os.path.dirname(__file__), image_file) @@ -333,8 +340,8 @@ async def test_form_only_still_sample( ) async def test_still_template( hass: HomeAssistant, - user_flow, - fakeimgbytes_png, + user_flow: ConfigFlowResult, + fakeimgbytes_png: bytes, template, url, expected_result, @@ -359,8 +366,11 @@ async def test_still_template( @respx.mock +@pytest.mark.usefixtures("fakeimg_png") async def test_form_rtsp_mode( - hass: HomeAssistant, fakeimg_png, user_flow, mock_create_stream + hass: HomeAssistant, + user_flow: ConfigFlowResult, + mock_create_stream: _patch[MagicMock], ) -> None: """Test we complete ok if the user enters a stream url.""" data = TESTDATA.copy() @@ -399,7 +409,10 @@ async def test_form_rtsp_mode( async def test_form_only_stream( - hass: HomeAssistant, fakeimgbytes_jpg, user_flow, mock_create_stream + hass: HomeAssistant, + fakeimgbytes_jpg: bytes, + user_flow: ConfigFlowResult, + mock_create_stream: _patch[MagicMock], ) -> None: """Test we complete ok if the user wants stream only.""" data = TESTDATA.copy() @@ -435,7 +448,7 @@ async def test_form_only_stream( async def test_form_still_and_stream_not_provided( - hass: HomeAssistant, user_flow + hass: HomeAssistant, user_flow: ConfigFlowResult ) -> None: """Test we show a suitable error if neither still or stream URL are provided.""" result2 = await hass.config_entries.flow.async_configure( @@ -482,7 +495,11 @@ async def test_form_still_and_stream_not_provided( ], ) async def test_form_image_http_exceptions( - side_effect, expected_message, hass: HomeAssistant, user_flow, mock_create_stream + side_effect, + expected_message, + hass: HomeAssistant, + user_flow: ConfigFlowResult, + mock_create_stream: _patch[MagicMock], ) -> None: """Test we handle image http exceptions.""" respx.get("http://127.0.0.1/testurl/1").side_effect = [ @@ -502,7 +519,9 @@ async def test_form_image_http_exceptions( @respx.mock async def test_form_stream_invalidimage( - hass: HomeAssistant, user_flow, mock_create_stream + hass: HomeAssistant, + user_flow: ConfigFlowResult, + mock_create_stream: _patch[MagicMock], ) -> None: """Test we handle invalid image when a stream is specified.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=b"invalid") @@ -519,7 +538,9 @@ async def test_form_stream_invalidimage( @respx.mock async def test_form_stream_invalidimage2( - hass: HomeAssistant, user_flow, mock_create_stream + hass: HomeAssistant, + user_flow: ConfigFlowResult, + mock_create_stream: _patch[MagicMock], ) -> None: """Test we handle invalid image when a stream is specified.""" respx.get("http://127.0.0.1/testurl/1").respond(content=None) @@ -536,7 +557,9 @@ async def test_form_stream_invalidimage2( @respx.mock async def test_form_stream_invalidimage3( - hass: HomeAssistant, user_flow, mock_create_stream + hass: HomeAssistant, + user_flow: ConfigFlowResult, + mock_create_stream: _patch[MagicMock], ) -> None: """Test we handle invalid image when a stream is specified.""" respx.get("http://127.0.0.1/testurl/1").respond(content=bytes([0xFF])) @@ -552,7 +575,10 @@ async def test_form_stream_invalidimage3( @respx.mock -async def test_form_stream_timeout(hass: HomeAssistant, fakeimg_png, user_flow) -> None: +@pytest.mark.usefixtures("fakeimg_png") +async def test_form_stream_timeout( + hass: HomeAssistant, user_flow: ConfigFlowResult +) -> None: """Test we handle invalid auth.""" with patch( "homeassistant.components.generic.config_flow.create_stream" @@ -571,8 +597,9 @@ async def test_form_stream_timeout(hass: HomeAssistant, fakeimg_png, user_flow) @respx.mock +@pytest.mark.usefixtures("fakeimg_png") async def test_form_stream_worker_error( - hass: HomeAssistant, fakeimg_png, user_flow + hass: HomeAssistant, user_flow: ConfigFlowResult ) -> None: """Test we handle a StreamWorkerError and pass the message through.""" with patch( @@ -589,7 +616,7 @@ async def test_form_stream_worker_error( @respx.mock async def test_form_stream_permission_error( - hass: HomeAssistant, fakeimgbytes_png, user_flow + hass: HomeAssistant, fakeimgbytes_png: bytes, user_flow: ConfigFlowResult ) -> None: """Test we handle permission error.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) @@ -606,8 +633,9 @@ async def test_form_stream_permission_error( @respx.mock +@pytest.mark.usefixtures("fakeimg_png") async def test_form_no_route_to_host( - hass: HomeAssistant, fakeimg_png, user_flow + hass: HomeAssistant, user_flow: ConfigFlowResult ) -> None: """Test we handle no route to host.""" with patch( @@ -623,8 +651,9 @@ async def test_form_no_route_to_host( @respx.mock +@pytest.mark.usefixtures("fakeimg_png") async def test_form_stream_io_error( - hass: HomeAssistant, fakeimg_png, user_flow + hass: HomeAssistant, user_flow: ConfigFlowResult ) -> None: """Test we handle no io error when setting up stream.""" with patch( @@ -640,7 +669,8 @@ async def test_form_stream_io_error( @respx.mock -async def test_form_oserror(hass: HomeAssistant, fakeimg_png, user_flow) -> None: +@pytest.mark.usefixtures("fakeimg_png") +async def test_form_oserror(hass: HomeAssistant, user_flow: ConfigFlowResult) -> None: """Test we handle OS error when setting up stream.""" with ( patch( @@ -657,7 +687,7 @@ async def test_form_oserror(hass: HomeAssistant, fakeimg_png, user_flow) -> None @respx.mock async def test_options_template_error( - hass: HomeAssistant, fakeimgbytes_png, mock_create_stream + hass: HomeAssistant, fakeimgbytes_png: bytes, mock_create_stream: _patch[MagicMock] ) -> None: """Test the options flow with a template error.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) @@ -755,7 +785,7 @@ async def test_slug(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> No @respx.mock async def test_options_only_stream( - hass: HomeAssistant, fakeimgbytes_png, mock_create_stream + hass: HomeAssistant, fakeimgbytes_png: bytes, mock_create_stream: _patch[MagicMock] ) -> None: """Test the options flow without a still_image_url.""" respx.get("http://127.0.0.1/testurl/2").respond(stream=fakeimgbytes_png) @@ -792,7 +822,8 @@ async def test_options_only_stream( assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg" -async def test_unload_entry(hass: HomeAssistant, fakeimg_png) -> None: +@pytest.mark.usefixtures("fakeimg_png") +async def test_unload_entry(hass: HomeAssistant) -> None: """Test unloading the generic IP Camera entry.""" mock_entry = MockConfigEntry(domain=DOMAIN, options=TESTDATA) mock_entry.add_to_hass(hass) @@ -862,8 +893,9 @@ async def test_migrate_existing_ids( @respx.mock +@pytest.mark.usefixtures("fakeimg_png") async def test_use_wallclock_as_timestamps_option( - hass: HomeAssistant, fakeimg_png, mock_create_stream + hass: HomeAssistant, mock_create_stream: _patch[MagicMock] ) -> None: """Test the use_wallclock_as_timestamps option flow.""" diff --git a/tests/components/generic/test_diagnostics.py b/tests/components/generic/test_diagnostics.py index f68c3ba4bc6..80fa5fd4d4e 100644 --- a/tests/components/generic/test_diagnostics.py +++ b/tests/components/generic/test_diagnostics.py @@ -6,12 +6,15 @@ from homeassistant.components.diagnostics import REDACTED from homeassistant.components.generic.diagnostics import redact_url from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator, setup_entry + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + setup_entry: MockConfigEntry, ) -> None: """Test config entry diagnostics.""" From 869f24df4978f43fd53dab16201a56da79ecb6bc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 4 Jul 2024 10:41:31 +0200 Subject: [PATCH 0717/2411] Convert async_get_conversation_languages from async to callback (#121162) * Convert get_languages to callback * One more callback --- homeassistant/components/assist_pipeline/pipeline.py | 9 +++++---- .../components/assist_pipeline/websocket_api.py | 6 +++--- homeassistant/components/conversation/__init__.py | 3 ++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 339417c253a..c6aa14bff15 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -115,7 +115,8 @@ AUDIO_PROCESSOR_SAMPLES: Final = 160 # 10 ms @ 16 Khz AUDIO_PROCESSOR_BYTES: Final = AUDIO_PROCESSOR_SAMPLES * 2 # 16-bit samples -async def _async_resolve_default_pipeline_settings( +@callback +def _async_resolve_default_pipeline_settings( hass: HomeAssistant, stt_engine_id: str | None, tts_engine_id: str | None, @@ -139,7 +140,7 @@ async def _async_resolve_default_pipeline_settings( # Find a matching language supported by the Home Assistant conversation agent conversation_languages = language_util.matches( hass.config.language, - await conversation.async_get_conversation_languages( + conversation.async_get_conversation_languages( hass, conversation.HOME_ASSISTANT_AGENT ), country=hass.config.country, @@ -222,7 +223,7 @@ async def _async_create_default_pipeline( The default pipeline will use the homeassistant conversation agent and the default stt / tts engines. """ - pipeline_settings = await _async_resolve_default_pipeline_settings( + pipeline_settings = _async_resolve_default_pipeline_settings( hass, stt_engine_id=None, tts_engine_id=None, pipeline_name="Home Assistant" ) return await pipeline_store.async_create_item(pipeline_settings) @@ -241,7 +242,7 @@ async def async_create_default_pipeline( """ pipeline_data: PipelineData = hass.data[DOMAIN] pipeline_store = pipeline_data.pipeline_store - pipeline_settings = await _async_resolve_default_pipeline_settings( + pipeline_settings = _async_resolve_default_pipeline_settings( hass, stt_engine_id, tts_engine_id, pipeline_name=pipeline_name ) if ( diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index 7dea960d940..3855bd7afc5 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -378,8 +378,8 @@ def websocket_get_run( vol.Required("type"): "assist_pipeline/language/list", } ) -@websocket_api.async_response -async def websocket_list_languages( +@callback +def websocket_list_languages( hass: HomeAssistant, connection: websocket_api.connection.ActiveConnection, msg: dict[str, Any], @@ -389,7 +389,7 @@ async def websocket_list_languages( This will return a list of languages which are supported by at least one stt, tts and conversation engine respectively. """ - conv_language_tags = await conversation.async_get_conversation_languages(hass) + conv_language_tags = conversation.async_get_conversation_languages(hass) stt_language_tags = stt.async_get_speech_to_text_languages(hass) tts_language_tags = tts.async_get_text_to_speech_languages(hass) pipeline_languages: set[str] | None = None diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 36929ac65f0..40b0cc54e99 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -118,7 +118,8 @@ def async_unset_agent( get_agent_manager(hass).async_unset_agent(config_entry.entry_id) -async def async_get_conversation_languages( +@callback +def async_get_conversation_languages( hass: HomeAssistant, agent_id: str | None = None ) -> set[str] | Literal["*"]: """Return languages supported by conversation agents. From 43e4223a8efb9a1739b7d8a16733381735bae401 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 11:09:34 +0200 Subject: [PATCH 0718/2411] Improve type hints in google_generative_ai tests (#121170) --- .../conftest.py | 13 ++++++--- .../test_config_flow.py | 4 +-- .../test_conversation.py | 29 ++++++++++--------- .../test_init.py | 28 +++++------------- 4 files changed, 34 insertions(+), 40 deletions(-) diff --git a/tests/components/google_generative_ai_conversation/conftest.py b/tests/components/google_generative_ai_conversation/conftest.py index 1761516e4f5..28c21a9b791 100644 --- a/tests/components/google_generative_ai_conversation/conftest.py +++ b/tests/components/google_generative_ai_conversation/conftest.py @@ -1,5 +1,6 @@ """Tests helpers.""" +from collections.abc import Generator from unittest.mock import patch import pytest @@ -14,14 +15,14 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_genai(): +def mock_genai() -> Generator[None]: """Mock the genai call in async_setup_entry.""" with patch("google.ai.generativelanguage_v1beta.ModelServiceAsyncClient.get_model"): yield @pytest.fixture -def mock_config_entry(hass, mock_genai): +def mock_config_entry(hass: HomeAssistant, mock_genai: None) -> MockConfigEntry: """Mock a config entry.""" entry = MockConfigEntry( domain="google_generative_ai_conversation", @@ -35,7 +36,9 @@ def mock_config_entry(hass, mock_genai): @pytest.fixture -def mock_config_entry_with_assist(hass, mock_config_entry): +def mock_config_entry_with_assist( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> MockConfigEntry: """Mock a config entry with assist.""" hass.config_entries.async_update_entry( mock_config_entry, options={CONF_LLM_HASS_API: llm.LLM_API_ASSIST} @@ -44,7 +47,9 @@ def mock_config_entry_with_assist(hass, mock_config_entry): @pytest.fixture -async def mock_init_component(hass: HomeAssistant, mock_config_entry: ConfigEntry): +async def mock_init_component( + hass: HomeAssistant, mock_config_entry: ConfigEntry +) -> None: """Initialize integration.""" assert await async_setup_component(hass, "google_generative_ai_conversation", {}) await hass.async_block_till_done() diff --git a/tests/components/google_generative_ai_conversation/test_config_flow.py b/tests/components/google_generative_ai_conversation/test_config_flow.py index c835a4d3b13..d4992c732e1 100644 --- a/tests/components/google_generative_ai_conversation/test_config_flow.py +++ b/tests/components/google_generative_ai_conversation/test_config_flow.py @@ -154,10 +154,10 @@ async def test_form(hass: HomeAssistant) -> None: ), ], ) +@pytest.mark.usefixtures("mock_init_component") async def test_options_switching( hass: HomeAssistant, - mock_config_entry, - mock_init_component, + mock_config_entry: MockConfigEntry, mock_models, current_options, new_options, diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 1e45c79a3b6..dc713f09454 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -44,10 +44,10 @@ def freeze_the_time(): {CONF_LLM_HASS_API: llm.LLM_API_ASSIST}, ], ) +@pytest.mark.usefixtures("mock_init_component") async def test_default_prompt( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, snapshot: SnapshotAssertion, agent_id: str | None, config_entry_options: {}, @@ -102,10 +102,10 @@ async def test_default_prompt( ("model_name", "supports_system_instruction"), [("models/gemini-1.5-pro", True), ("models/gemini-1.0-pro", False)], ) +@pytest.mark.usefixtures("mock_init_component") async def test_chat_history( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, model_name: str, supports_system_instruction: bool, snapshot: SnapshotAssertion, @@ -167,11 +167,11 @@ async def test_chat_history( @patch( "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" ) +@pytest.mark.usefixtures("mock_init_component") async def test_function_call( mock_get_tools, hass: HomeAssistant, mock_config_entry_with_assist: MockConfigEntry, - mock_init_component, snapshot: SnapshotAssertion, ) -> None: """Test function calling.""" @@ -277,11 +277,11 @@ async def test_function_call( @patch( "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" ) +@pytest.mark.usefixtures("mock_init_component") async def test_function_call_without_parameters( mock_get_tools, hass: HomeAssistant, mock_config_entry_with_assist: MockConfigEntry, - mock_init_component, snapshot: SnapshotAssertion, ) -> None: """Test function calling without parameters.""" @@ -358,11 +358,11 @@ async def test_function_call_without_parameters( @patch( "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" ) +@pytest.mark.usefixtures("mock_init_component") async def test_function_exception( mock_get_tools, hass: HomeAssistant, mock_config_entry_with_assist: MockConfigEntry, - mock_init_component, ) -> None: """Test exception in function calling.""" agent_id = mock_config_entry_with_assist.entry_id @@ -440,8 +440,9 @@ async def test_function_exception( ) +@pytest.mark.usefixtures("mock_init_component") async def test_error_handling( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component + hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test that client errors are caught.""" with patch("google.generativeai.GenerativeModel") as mock_model: @@ -459,8 +460,9 @@ async def test_error_handling( ) +@pytest.mark.usefixtures("mock_init_component") async def test_blocked_response( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component + hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test blocked response.""" with patch("google.generativeai.GenerativeModel") as mock_model: @@ -480,8 +482,9 @@ async def test_blocked_response( ) +@pytest.mark.usefixtures("mock_init_component") async def test_empty_response( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component + hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test empty response.""" with patch("google.generativeai.GenerativeModel") as mock_model: @@ -501,10 +504,9 @@ async def test_empty_response( ) +@pytest.mark.usefixtures("mock_init_component") async def test_invalid_llm_api( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, + hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test handling of invalid llm api.""" hass.config_entries.async_update_entry( @@ -593,10 +595,9 @@ async def test_template_variables( assert "The user id is 12345." in mock_model.mock_calls[0][2]["system_instruction"] +@pytest.mark.usefixtures("mock_init_component") async def test_conversation_agent( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, + hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test GoogleGenerativeAIAgent.""" agent = conversation.get_agent_manager(hass).async_get_agent( diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index 3f860c78da1..4875323d094 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -14,11 +14,9 @@ from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry +@pytest.mark.usefixtures("mock_init_component") async def test_generate_content_service_without_images( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, - snapshot: SnapshotAssertion, + hass: HomeAssistant, snapshot: SnapshotAssertion ) -> None: """Test generate content service.""" stubbed_generated_content = ( @@ -46,11 +44,9 @@ async def test_generate_content_service_without_images( assert [tuple(mock_call) for mock_call in mock_model.mock_calls] == snapshot +@pytest.mark.usefixtures("mock_init_component") async def test_generate_content_service_with_image( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, - snapshot: SnapshotAssertion, + hass: HomeAssistant, snapshot: SnapshotAssertion ) -> None: """Test generate content service.""" stubbed_generated_content = ( @@ -134,11 +130,9 @@ async def test_generate_content_response_has_empty_parts( ) +@pytest.mark.usefixtures("mock_init_component") async def test_generate_content_service_with_image_not_allowed_path( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, - snapshot: SnapshotAssertion, ) -> None: """Test generate content service with an image in a not allowed path.""" with ( @@ -165,11 +159,9 @@ async def test_generate_content_service_with_image_not_allowed_path( ) +@pytest.mark.usefixtures("mock_init_component") async def test_generate_content_service_with_image_not_exists( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, - snapshot: SnapshotAssertion, ) -> None: """Test generate content service with an image that does not exist.""" with ( @@ -192,12 +184,8 @@ async def test_generate_content_service_with_image_not_exists( ) -async def test_generate_content_service_with_non_image( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, - snapshot: SnapshotAssertion, -) -> None: +@pytest.mark.usefixtures("mock_init_component") +async def test_generate_content_service_with_non_image(hass: HomeAssistant) -> None: """Test generate content service with a non image.""" with ( patch("pathlib.Path.exists", return_value=True), From aa74ad006137fc54d042995dc37bcd2960c2edcf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Jul 2024 04:17:57 -0500 Subject: [PATCH 0719/2411] Enable ruff asyncio event loop blocking detection rules (#120799) --- pyproject.toml | 6 ++++++ tests/components/srp_energy/test_sensor.py | 2 +- tests/test_block_async_io.py | 14 +++++++------- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cd622c964b9..2edfa38d97e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -681,6 +681,12 @@ required-version = ">=0.5.0" [tool.ruff.lint] select = [ "A001", # Variable {name} is shadowing a Python builtin + "ASYNC210", # Async functions should not call blocking HTTP methods + "ASYNC220", # Async functions should not create subprocesses with blocking methods + "ASYNC221", # Async functions should not run processes with blocking methods + "ASYNC222", # Async functions should not wait on processes with blocking methods + "ASYNC230", # Async functions should not open files with blocking methods like open + "ASYNC251", # Async functions should not call time.sleep "B002", # Python does not support the unary prefix increment "B005", # Using .strip() with multi-character strings is misleading "B007", # Loop control variable {name} not used within loop body diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index 7369d07f77a..0c1eed11c96 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -80,7 +80,7 @@ async def test_srp_entity_timeout( ): client = srp_energy_mock.return_value client.validate.return_value = True - client.usage = lambda _, __, ___: time.sleep(1) + client.usage = lambda _, __, ___: time.sleep(1) # noqa: ASYNC251 mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index ae77fbee217..ef4f9df60f6 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -44,7 +44,7 @@ async def test_protect_loop_debugger_sleep(caplog: pytest.LogCaptureFixture) -> return_value=frames, ), ): - time.sleep(0) + time.sleep(0) # noqa: ASYNC251 assert "Detected blocking call inside the event loop" not in caplog.text @@ -71,7 +71,7 @@ async def test_protect_loop_sleep() -> None: return_value=frames, ), ): - time.sleep(0) + time.sleep(0) # noqa: ASYNC251 async def test_protect_loop_sleep_get_current_frame_raises() -> None: @@ -97,7 +97,7 @@ async def test_protect_loop_sleep_get_current_frame_raises() -> None: return_value=frames, ), ): - time.sleep(0) + time.sleep(0) # noqa: ASYNC251 async def test_protect_loop_importlib_import_module_non_integration( @@ -211,7 +211,7 @@ async def test_protect_loop_open(caplog: pytest.LogCaptureFixture) -> None: block_async_io.enable() with ( contextlib.suppress(FileNotFoundError), - open("/proc/does_not_exist", encoding="utf8"), + open("/proc/does_not_exist", encoding="utf8"), # noqa: ASYNC230 ): pass assert "Detected blocking call to open with args" not in caplog.text @@ -223,7 +223,7 @@ async def test_protect_open(caplog: pytest.LogCaptureFixture) -> None: block_async_io.enable() with ( contextlib.suppress(FileNotFoundError), - open("/config/data_not_exist", encoding="utf8"), + open("/config/data_not_exist", encoding="utf8"), # noqa: ASYNC230 ): pass @@ -253,7 +253,7 @@ async def test_protect_open_path(path: Any, caplog: pytest.LogCaptureFixture) -> """Test opening a file by path in the event loop logs.""" with patch.object(block_async_io, "_IN_TESTS", False): block_async_io.enable() - with contextlib.suppress(FileNotFoundError), open(path, encoding="utf8"): + with contextlib.suppress(FileNotFoundError), open(path, encoding="utf8"): # noqa: ASYNC230 pass assert "Detected blocking call to open with args" in caplog.text @@ -336,7 +336,7 @@ async def test_open_calls_ignored_in_tests(caplog: pytest.LogCaptureFixture) -> block_async_io.enable() with ( contextlib.suppress(FileNotFoundError), - open("/config/data_not_exist", encoding="utf8"), + open("/config/data_not_exist", encoding="utf8"), # noqa: ASYNC230 ): pass From 8aedb1201d7872adfe84e816f03044c997e9b1f8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 11:31:35 +0200 Subject: [PATCH 0720/2411] Improve type hints in google_pubsub tests (#121172) * Improve type hints in google_pubsub tests * Remove from .coveragerc --- .coveragerc | 1 - tests/components/google_pubsub/test_init.py | 15 ++++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.coveragerc b/.coveragerc index 65a4c1bfc31..7980a9c5cdf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -487,7 +487,6 @@ omit = homeassistant/components/goodwe/sensor.py homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py - homeassistant/components/google_pubsub/__init__.py homeassistant/components/gpsd/__init__.py homeassistant/components/gpsd/sensor.py homeassistant/components/greenwave/light.py diff --git a/tests/components/google_pubsub/test_init.py b/tests/components/google_pubsub/test_init.py index a793ade5312..d1cc2d88ab0 100644 --- a/tests/components/google_pubsub/test_init.py +++ b/tests/components/google_pubsub/test_init.py @@ -1,9 +1,10 @@ """The tests for the Google Pub/Sub component.""" +from collections.abc import Generator from dataclasses import dataclass from datetime import datetime import os -from unittest import mock +from unittest.mock import MagicMock, Mock, patch import pytest @@ -40,21 +41,21 @@ async def test_nested() -> None: @pytest.fixture(autouse=True, name="mock_client") -def mock_client_fixture(): +def mock_client_fixture() -> Generator[MagicMock]: """Mock the pubsub client.""" - with mock.patch(f"{GOOGLE_PUBSUB_PATH}.PublisherClient") as client: + with patch(f"{GOOGLE_PUBSUB_PATH}.PublisherClient") as client: setattr( client, "from_service_account_json", - mock.MagicMock(return_value=mock.MagicMock()), + MagicMock(return_value=MagicMock()), ) yield client @pytest.fixture(autouse=True, name="mock_is_file") -def mock_is_file_fixture(): +def mock_is_file_fixture() -> Generator[MagicMock]: """Mock os.path.isfile.""" - with mock.patch(f"{GOOGLE_PUBSUB_PATH}.os.path.isfile") as is_file: + with patch(f"{GOOGLE_PUBSUB_PATH}.os.path.isfile") as is_file: is_file.return_value = True yield is_file @@ -63,7 +64,7 @@ def mock_is_file_fixture(): def mock_json(hass, monkeypatch): """Mock the event bus listener and os component.""" monkeypatch.setattr( - f"{GOOGLE_PUBSUB_PATH}.json.dumps", mock.Mock(return_value=mock.MagicMock()) + f"{GOOGLE_PUBSUB_PATH}.json.dumps", Mock(return_value=MagicMock()) ) From 4958e8e5c1a570c2c82b7f00be76bb927b7c5631 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:31:58 +0100 Subject: [PATCH 0721/2411] Bump python-kasa to 0.7.0.3 (#121183) --- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 1270bb3469b..3786a2565c2 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -297,5 +297,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.7.0.2"] + "requirements": ["python-kasa[speedups]==0.7.0.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 711978dea7f..7b07f994f80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2275,7 +2275,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.2 +python-kasa[speedups]==0.7.0.3 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f810aa89cae..44d60bfa13a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1775,7 +1775,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.2 +python-kasa[speedups]==0.7.0.3 # homeassistant.components.matter python-matter-server==6.2.2 From a6f6221f166f1cb12a3017125ef1471f31ad7b47 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 11:33:47 +0200 Subject: [PATCH 0722/2411] Add monkeypatch type hints to numato tests (#121056) * Add monkeypatch type hints to numato tests * Adjust * Improve --- tests/components/numato/conftest.py | 9 +++++---- tests/components/numato/test_binary_sensor.py | 2 +- tests/components/numato/test_init.py | 6 +++--- tests/components/numato/test_sensor.py | 6 ++++-- tests/components/numato/test_switch.py | 6 ++++-- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/tests/components/numato/conftest.py b/tests/components/numato/conftest.py index c6fd13a099e..f3ae4d5f32b 100644 --- a/tests/components/numato/conftest.py +++ b/tests/components/numato/conftest.py @@ -1,17 +1,18 @@ """Fixtures for numato tests.""" from copy import deepcopy +from typing import Any import pytest from homeassistant.components import numato -from . import numato_mock from .common import NUMATO_CFG +from .numato_mock import NumatoModuleMock @pytest.fixture -def config(): +def config() -> dict[str, Any]: """Provide a copy of the numato domain's test configuration. This helps to quickly change certain aspects of the configuration scoped @@ -21,8 +22,8 @@ def config(): @pytest.fixture -def numato_fixture(monkeypatch): +def numato_fixture(monkeypatch: pytest.MonkeyPatch) -> NumatoModuleMock: """Inject the numato mockup into numato homeassistant module.""" - module_mock = numato_mock.NumatoModuleMock() + module_mock = NumatoModuleMock() monkeypatch.setattr(numato, "gpio", module_mock) return module_mock diff --git a/tests/components/numato/test_binary_sensor.py b/tests/components/numato/test_binary_sensor.py index 524589af198..08506349247 100644 --- a/tests/components/numato/test_binary_sensor.py +++ b/tests/components/numato/test_binary_sensor.py @@ -21,7 +21,7 @@ MOCKUP_ENTITY_IDS = { async def test_failing_setups_no_entities( - hass: HomeAssistant, numato_fixture, monkeypatch + hass: HomeAssistant, numato_fixture, monkeypatch: pytest.MonkeyPatch ) -> None: """When port setup fails, no entity shall be created.""" monkeypatch.setattr(numato_fixture.NumatoDeviceMock, "setup", mockup_raise) diff --git a/tests/components/numato/test_init.py b/tests/components/numato/test_init.py index 35dd102ec9e..4695265f37f 100644 --- a/tests/components/numato/test_init.py +++ b/tests/components/numato/test_init.py @@ -11,7 +11,7 @@ from .common import NUMATO_CFG, mockup_raise, mockup_return async def test_setup_no_devices( - hass: HomeAssistant, numato_fixture, monkeypatch + hass: HomeAssistant, numato_fixture, monkeypatch: pytest.MonkeyPatch ) -> None: """Test handling of an 'empty' discovery. @@ -24,7 +24,7 @@ async def test_setup_no_devices( async def test_fail_setup_raising_discovery( - hass: HomeAssistant, numato_fixture, caplog: pytest.LogCaptureFixture, monkeypatch + hass: HomeAssistant, numato_fixture, monkeypatch: pytest.MonkeyPatch ) -> None: """Test handling of an exception during discovery. @@ -57,7 +57,7 @@ async def test_hass_numato_api_wrong_port_directions( async def test_hass_numato_api_errors( - hass: HomeAssistant, numato_fixture, monkeypatch + hass: HomeAssistant, numato_fixture, monkeypatch: pytest.MonkeyPatch ) -> None: """Test whether Home Assistant numato API (re-)raises errors.""" numato_fixture.discover() diff --git a/tests/components/numato/test_sensor.py b/tests/components/numato/test_sensor.py index 30a9f174941..c652df9b086 100644 --- a/tests/components/numato/test_sensor.py +++ b/tests/components/numato/test_sensor.py @@ -1,5 +1,7 @@ """Tests for the numato sensor platform.""" +import pytest + from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery @@ -13,7 +15,7 @@ MOCKUP_ENTITY_IDS = { async def test_failing_setups_no_entities( - hass: HomeAssistant, numato_fixture, monkeypatch + hass: HomeAssistant, numato_fixture, monkeypatch: pytest.MonkeyPatch ) -> None: """When port setup fails, no entity shall be created.""" monkeypatch.setattr(numato_fixture.NumatoDeviceMock, "setup", mockup_raise) @@ -24,7 +26,7 @@ async def test_failing_setups_no_entities( async def test_failing_sensor_update( - hass: HomeAssistant, numato_fixture, monkeypatch + hass: HomeAssistant, numato_fixture, monkeypatch: pytest.MonkeyPatch ) -> None: """Test condition when a sensor update fails.""" monkeypatch.setattr(numato_fixture.NumatoDeviceMock, "adc_read", mockup_raise) diff --git a/tests/components/numato/test_switch.py b/tests/components/numato/test_switch.py index e69b3481b1d..42102ea4869 100644 --- a/tests/components/numato/test_switch.py +++ b/tests/components/numato/test_switch.py @@ -1,5 +1,7 @@ """Tests for the numato switch platform.""" +import pytest + from homeassistant.components import switch from homeassistant.const import ( ATTR_ENTITY_ID, @@ -20,7 +22,7 @@ MOCKUP_ENTITY_IDS = { async def test_failing_setups_no_entities( - hass: HomeAssistant, numato_fixture, monkeypatch + hass: HomeAssistant, numato_fixture, monkeypatch: pytest.MonkeyPatch ) -> None: """When port setup fails, no entity shall be created.""" monkeypatch.setattr(numato_fixture.NumatoDeviceMock, "setup", mockup_raise) @@ -69,7 +71,7 @@ async def test_regular_hass_operations(hass: HomeAssistant, numato_fixture) -> N async def test_failing_hass_operations( - hass: HomeAssistant, numato_fixture, monkeypatch + hass: HomeAssistant, numato_fixture, monkeypatch: pytest.MonkeyPatch ) -> None: """Test failing operations called from within Home Assistant. From 0be1f773a265c4ea0d63148ad6f32ff752060591 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Jul 2024 04:41:56 -0500 Subject: [PATCH 0723/2411] Add event platform to doorbird (#121114) --- .coveragerc | 1 + homeassistant/components/doorbird/const.py | 2 +- homeassistant/components/doorbird/device.py | 35 +++++++- homeassistant/components/doorbird/event.py | 88 +++++++++++++++++++ .../components/doorbird/strings.json | 20 +++++ 5 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/doorbird/event.py diff --git a/.coveragerc b/.coveragerc index 7980a9c5cdf..27001654d72 100644 --- a/.coveragerc +++ b/.coveragerc @@ -245,6 +245,7 @@ omit = homeassistant/components/doorbird/camera.py homeassistant/components/doorbird/device.py homeassistant/components/doorbird/entity.py + homeassistant/components/doorbird/event.py homeassistant/components/doorbird/util.py homeassistant/components/doorbird/view.py homeassistant/components/dormakaba_dkey/__init__.py diff --git a/homeassistant/components/doorbird/const.py b/homeassistant/components/doorbird/const.py index 1bd13496e3a..4985b9ac9ea 100644 --- a/homeassistant/components/doorbird/const.py +++ b/homeassistant/components/doorbird/const.py @@ -3,7 +3,7 @@ from homeassistant.const import Platform DOMAIN = "doorbird" -PLATFORMS = [Platform.BUTTON, Platform.CAMERA] +PLATFORMS = [Platform.BUTTON, Platform.CAMERA, Platform.EVENT] DOOR_STATION = "door_station" DOOR_STATION_INFO = "door_station_info" DOOR_STATION_EVENT_ENTITY_IDS = "door_station_event_entity_ids" diff --git a/homeassistant/components/doorbird/device.py b/homeassistant/components/doorbird/device.py index e0fb02fcb8d..23c0055cbe0 100644 --- a/homeassistant/components/doorbird/device.py +++ b/homeassistant/components/doorbird/device.py @@ -2,10 +2,12 @@ from __future__ import annotations +from dataclasses import dataclass +from functools import cached_property import logging from typing import Any, cast -from doorbirdpy import DoorBird +from doorbirdpy import DoorBird, DoorBirdScheduleEntry from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant @@ -17,6 +19,14 @@ from .const import API_URL _LOGGER = logging.getLogger(__name__) +@dataclass(slots=True) +class DoorbirdEvent: + """Describes a doorbird event.""" + + event: str + event_type: str + + class ConfiguredDoorBird: """Attach additional information to pass along with configured device.""" @@ -36,6 +46,7 @@ class ConfiguredDoorBird: self._event_entity_ids = event_entity_ids self.events: list[str] = [] self.door_station_events: list[str] = [] + self.event_descriptions: list[DoorbirdEvent] = [] def update_events(self, events: list[str]) -> None: """Update the doorbird events.""" @@ -84,7 +95,27 @@ class ConfiguredDoorBird: "Successfully registered URL for %s on %s", event, self.name ) - @property + schedule: list[DoorBirdScheduleEntry] = self.device.schedule() + http_fav: dict[str, dict[str, Any]] = favorites.get("http") or {} + events: list[DoorbirdEvent] = [] + favorite_input_type: dict[str, str] = {} + for entry in schedule: + input_type = entry.input + for output in entry.output: + if output.event == "http": + favorite_input_type[output.param] = input_type + + for identifier, data in http_fav.items(): + title: str | None = data.get("title") + if not title or not title.startswith("Home Assistant"): + continue + event = title.split("(")[1].strip(")") + if input_type := favorite_input_type.get(identifier): + events.append(DoorbirdEvent(event, input_type)) + + self.event_descriptions = events + + @cached_property def slug(self) -> str: """Get device slug.""" return slugify(self._name) diff --git a/homeassistant/components/doorbird/event.py b/homeassistant/components/doorbird/event.py new file mode 100644 index 00000000000..082ddb6d487 --- /dev/null +++ b/homeassistant/components/doorbird/event.py @@ -0,0 +1,88 @@ +"""Support for doorbird events.""" + +from typing import TYPE_CHECKING + +from homeassistant.components.event import ( + EventDeviceClass, + EventEntity, + EventEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import DoorbirdEvent +from .entity import DoorBirdEntity +from .models import DoorBirdData + +EVENT_DESCRIPTIONS = { + "doorbell": EventEntityDescription( + key="doorbell", + translation_key="doorbell", + device_class=EventDeviceClass.DOORBELL, + event_types=["ring"], + ), + "motion": EventEntityDescription( + key="motion", + translation_key="motion", + device_class=EventDeviceClass.MOTION, + event_types=["motion"], + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the DoorBird event platform.""" + config_entry_id = config_entry.entry_id + door_bird_data: DoorBirdData = hass.data[DOMAIN][config_entry_id] + async_add_entities( + DoorBirdEventEntity(door_bird_data, doorbird_event, description) + for doorbird_event in door_bird_data.door_station.event_descriptions + if (description := EVENT_DESCRIPTIONS.get(doorbird_event.event_type)) + ) + + +class DoorBirdEventEntity(DoorBirdEntity, EventEntity): + """A relay in a DoorBird device.""" + + entity_description: EventEntityDescription + _attr_has_entity_name = True + + def __init__( + self, + door_bird_data: DoorBirdData, + doorbird_event: DoorbirdEvent, + entity_description: EventEntityDescription, + ) -> None: + """Initialize an event for a doorbird device.""" + super().__init__(door_bird_data) + self._doorbird_event = doorbird_event + self.entity_description = entity_description + event = doorbird_event.event + self._attr_unique_id = f"{self._mac_addr}_{event}" + slug_name = event.removeprefix(self._door_station.slug).strip("_") + friendly_name = slug_name.replace("_", " ") + self._attr_name = friendly_name[0:1].upper() + friendly_name[1:].lower() + + async def async_added_to_hass(self) -> None: + """Subscribe to device events.""" + self.async_on_remove( + self.hass.bus.async_listen( + f"{DOMAIN}_{self._doorbird_event.event}", + self._async_handle_event, + ) + ) + + @callback + def _async_handle_event(self, event: Event) -> None: + """Handle a device event.""" + event_types = self.entity_description.event_types + if TYPE_CHECKING: + assert event_types is not None + self._trigger_event(event_type=event_types[0]) + self.async_write_ha_state() diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index c851de379d4..7bb55739fcf 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -48,6 +48,26 @@ "last_motion": { "name": "Last motion" } + }, + "event": { + "doorbell": { + "state_attributes": { + "event_type": { + "state": { + "ring": "Ring" + } + } + } + }, + "motion": { + "state_attributes": { + "event_type": { + "state": { + "motion": "Motion" + } + } + } + } } } } From 02e7707f914b251000673dc150cacb89cc0fd7c1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 4 Jul 2024 11:49:46 +0200 Subject: [PATCH 0724/2411] Use fixtures in deCONZ config flow tests PT1 (#121121) --- tests/components/deconz/conftest.py | 10 ++++- tests/components/deconz/test_config_flow.py | 45 ++++++++------------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py index 049dbed2963..619e31b1f88 100644 --- a/tests/components/deconz/conftest.py +++ b/tests/components/deconz/conftest.py @@ -11,7 +11,7 @@ from pydeconz.websocket import Signal import pytest from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_USER, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant @@ -32,6 +32,7 @@ def fixture_config_entry( hass: HomeAssistant, config_entry_data: MappingProxyType[str, Any], config_entry_options: MappingProxyType[str, Any], + config_entry_source: str, ) -> ConfigEntry: """Define a config entry fixture.""" config_entry = MockConfigEntry( @@ -40,6 +41,7 @@ def fixture_config_entry( unique_id=BRIDGEID, data=config_entry_data, options=config_entry_options, + source=config_entry_source, ) config_entry.add_to_hass(hass) return config_entry @@ -61,6 +63,12 @@ def fixture_config_entry_options() -> MappingProxyType[str, Any]: return {} +@pytest.fixture(name="config_entry_source") +def fixture_config_entry_source() -> str: + """Define a config entry source fixture.""" + return SOURCE_USER + + # Request mocks diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 6da940e0918..fb1239dba61 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -27,6 +27,7 @@ from homeassistant.config_entries import ( SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER, + ConfigEntry, ) from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant @@ -481,11 +482,9 @@ async def test_flow_ssdp_discovery( async def test_ssdp_discovery_update_configuration( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test if a discovered bridge is configured but updates with new attributes.""" - config_entry = await setup_deconz_integration(hass, aioclient_mock) - with patch( "homeassistant.components.deconz.async_setup_entry", return_value=True, @@ -507,15 +506,14 @@ async def test_ssdp_discovery_update_configuration( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert config_entry.data[CONF_HOST] == "2.3.4.5" + assert config_entry_setup.data[CONF_HOST] == "2.3.4.5" assert len(mock_setup_entry.mock_calls) == 1 async def test_ssdp_discovery_dont_update_configuration( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test if a discovered bridge has already been configured.""" - config_entry = await setup_deconz_integration(hass, aioclient_mock) result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, @@ -533,17 +531,14 @@ async def test_ssdp_discovery_dont_update_configuration( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert config_entry.data[CONF_HOST] == "1.2.3.4" + assert config_entry_setup.data[CONF_HOST] == "1.2.3.4" +@pytest.mark.parametrize("config_entry_source", [SOURCE_HASSIO]) async def test_ssdp_discovery_dont_update_existing_hassio_configuration( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test to ensure the SSDP discovery does not update an Hass.io entry.""" - config_entry = await setup_deconz_integration( - hass, aioclient_mock, source=SOURCE_HASSIO - ) - result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, data=ssdp.SsdpServiceInfo( @@ -560,7 +555,7 @@ async def test_ssdp_discovery_dont_update_existing_hassio_configuration( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert config_entry.data[CONF_HOST] == "1.2.3.4" + assert config_entry_setup.data[CONF_HOST] == "1.2.3.4" async def test_flow_hassio_discovery(hass: HomeAssistant) -> None: @@ -610,11 +605,10 @@ async def test_flow_hassio_discovery(hass: HomeAssistant) -> None: async def test_hassio_discovery_update_configuration( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + config_entry_setup: ConfigEntry, ) -> None: """Test we can update an existing config entry.""" - config_entry = await setup_deconz_integration(hass, aioclient_mock) - with patch( "homeassistant.components.deconz.async_setup_entry", return_value=True, @@ -638,18 +632,15 @@ async def test_hassio_discovery_update_configuration( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert config_entry.data[CONF_HOST] == "2.3.4.5" - assert config_entry.data[CONF_PORT] == 8080 - assert config_entry.data[CONF_API_KEY] == "updated" + assert config_entry_setup.data[CONF_HOST] == "2.3.4.5" + assert config_entry_setup.data[CONF_PORT] == 8080 + assert config_entry_setup.data[CONF_API_KEY] == "updated" assert len(mock_setup_entry.mock_calls) == 1 -async def test_hassio_discovery_dont_update_configuration( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@pytest.mark.usefixtures("config_entry_setup") +async def test_hassio_discovery_dont_update_configuration(hass: HomeAssistant) -> None: """Test we can update an existing config entry.""" - await setup_deconz_integration(hass, aioclient_mock) - result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, data=HassioServiceInfo( @@ -671,12 +662,10 @@ async def test_hassio_discovery_dont_update_configuration( async def test_option_flow( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test config flow options.""" - config_entry = await setup_deconz_integration(hass, aioclient_mock) - - result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry_setup.entry_id) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "deconz_devices" From d429bcef16c3489c0c798012d58b51da659bdc9c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 4 Jul 2024 11:50:23 +0200 Subject: [PATCH 0725/2411] Use fixtures in deCONZ sensor tests PT1 (#121116) --- tests/components/deconz/test_sensor.py | 395 ++++++++++++------------- 1 file changed, 190 insertions(+), 205 deletions(-) diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 1e1ca6efe7c..72dad8a17aa 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -1,6 +1,8 @@ """deCONZ sensor platform tests.""" +from collections.abc import Callable from datetime import timedelta +from typing import Any from unittest.mock import patch import pytest @@ -11,7 +13,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorStateClass, ) -from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, @@ -982,12 +984,10 @@ async def test_sensors( assert len(hass.states.async_all()) == 0 -async def test_not_allow_clip_sensor( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that CLIP sensors are not allowed.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "name": "CLIP temperature sensor", "type": "CLIPTemperature", @@ -996,22 +996,19 @@ async def test_not_allow_clip_sensor( "uniqueid": "00:00:00:00:00:00:00:02-00", }, } - } - - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration( - hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: False} - ) - + ], +) +@pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: False}]) +@pytest.mark.usefixtures("config_entry_setup") +async def test_not_allow_clip_sensor(hass: HomeAssistant) -> None: + """Test that CLIP sensors are not allowed.""" assert len(hass.states.async_all()) == 0 -async def test_allow_clip_sensors( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that CLIP sensors can be allowed.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "name": "Light level sensor", "type": "ZHALightLevel", @@ -1039,14 +1036,13 @@ async def test_allow_clip_sensors( "uniqueid": "/sensors/3", }, } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration( - hass, - aioclient_mock, - options={CONF_ALLOW_CLIP_SENSOR: True}, - ) - + ], +) +@pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: True}]) +async def test_allow_clip_sensors( + hass: HomeAssistant, config_entry_setup: ConfigEntry +) -> None: + """Test that CLIP sensors can be allowed.""" assert len(hass.states.async_all()) == 4 assert hass.states.get("sensor.clip_light_level_sensor").state == "999.8" assert hass.states.get("sensor.clip_flur").state == "0" @@ -1054,7 +1050,7 @@ async def test_allow_clip_sensors( # Disallow clip sensors hass.config_entries.async_update_entry( - config_entry, options={CONF_ALLOW_CLIP_SENSOR: False} + config_entry_setup, options={CONF_ALLOW_CLIP_SENSOR: False} ) await hass.async_block_till_done() @@ -1065,7 +1061,7 @@ async def test_allow_clip_sensors( # Allow clip sensors hass.config_entries.async_update_entry( - config_entry, options={CONF_ALLOW_CLIP_SENSOR: True} + config_entry_setup, options={CONF_ALLOW_CLIP_SENSOR: True} ) await hass.async_block_till_done() @@ -1074,9 +1070,8 @@ async def test_allow_clip_sensors( assert hass.states.get("sensor.clip_flur").state == "0" -async def test_add_new_sensor( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: +@pytest.mark.usefixtures("config_entry_setup") +async def test_add_new_sensor(hass: HomeAssistant, mock_deconz_websocket) -> None: """Test that adding a new sensor works.""" event_added_sensor = { "t": "event", @@ -1093,8 +1088,6 @@ async def test_add_new_sensor( }, } - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 await mock_deconz_websocket(data=event_added_sensor) @@ -1115,34 +1108,30 @@ BAD_SENSOR_DATA = [ @pytest.mark.parametrize(("sensor_type", "sensor_property"), BAD_SENSOR_DATA) async def test_dont_add_sensor_if_state_is_none( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - sensor_type, - sensor_property, + config_entry_factory: Callable[[], ConfigEntry], + sensor_payload: dict[str, Any], + sensor_type: str, + sensor_property: str, ) -> None: """Test sensor with scaled data is not created if state is None.""" - data = { - "sensors": { - "1": { - "name": "Sensor 1", - "type": sensor_type, - "state": {sensor_property: None}, - "config": {}, - "uniqueid": "00:00:00:00:00:00:00:00-00", - } + sensor_payload |= { + "1": { + "name": "Sensor 1", + "type": sensor_type, + "state": {sensor_property: None}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:00-00", } } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) + await config_entry_factory() assert len(hass.states.async_all()) == 0 -async def test_air_quality_sensor_without_ppb( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test sensor with scaled data is not created if state is None.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "config": { "on": True, @@ -1163,23 +1152,18 @@ async def test_air_quality_sensor_without_ppb( "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) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_air_quality_sensor_without_ppb(hass: HomeAssistant) -> None: + """Test sensor with scaled data is not created if state is None.""" assert len(hass.states.async_all()) == 1 -async def test_add_battery_later( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test that a battery sensor can be created later on. - - Without an initial battery state a battery sensor - can be created once a value is reported. - """ - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "name": "Switch 1", "type": "ZHASwitch", @@ -1195,10 +1179,15 @@ async def test_add_battery_later( "uniqueid": "00:00:00:00:00:00:00:00-00-0001", }, } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_add_battery_later(hass: HomeAssistant, mock_deconz_websocket) -> None: + """Test that a battery sensor can be created later on. + Without an initial battery state a battery sensor + can be created once a value is reported. + """ assert len(hass.states.async_all()) == 0 event_changed_sensor = { @@ -1230,155 +1219,151 @@ async def test_add_battery_later( @pytest.mark.parametrize("model_id", ["0x8030", "0x8031", "0x8034", "0x8035"]) async def test_special_danfoss_battery_creation( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, model_id + hass: HomeAssistant, + config_entry_factory: Callable[[], ConfigEntry], + sensor_payload: dict[str, Any], + model_id: str, ) -> None: """Test the special Danfoss battery creation works. Normally there should only be one battery sensor per device from deCONZ. With specific Danfoss devices each endpoint can report its own battery state. """ - data = { - "sensors": { - "1": { - "config": { - "battery": 70, - "heatsetpoint": 2300, - "offset": 0, - "on": True, - "reachable": True, - "schedule": {}, - "schedule_on": False, - }, - "ep": 1, - "etag": "982d9acc38bee5b251e24a9be26558e4", - "lastseen": "2021-02-15T12:23Z", - "manufacturername": "Danfoss", - "modelid": model_id, - "name": "0x8030", - "state": { - "lastupdated": "2021-02-15T12:23:07.994", - "on": False, - "temperature": 2307, - }, - "swversion": "YYYYMMDD", - "type": "ZHAThermostat", - "uniqueid": "58:8e:81:ff:fe:00:11:22-01-0201", + sensor_payload |= { + "1": { + "config": { + "battery": 70, + "heatsetpoint": 2300, + "offset": 0, + "on": True, + "reachable": True, + "schedule": {}, + "schedule_on": False, }, - "2": { - "config": { - "battery": 86, - "heatsetpoint": 2300, - "offset": 0, - "on": True, - "reachable": True, - "schedule": {}, - "schedule_on": False, - }, - "ep": 2, - "etag": "62f12749f9f51c950086aff37dd02b61", - "lastseen": "2021-02-15T12:23Z", - "manufacturername": "Danfoss", - "modelid": model_id, - "name": "0x8030", - "state": { - "lastupdated": "2021-02-15T12:23:22.399", - "on": False, - "temperature": 2316, - }, - "swversion": "YYYYMMDD", - "type": "ZHAThermostat", - "uniqueid": "58:8e:81:ff:fe:00:11:22-02-0201", + "ep": 1, + "etag": "982d9acc38bee5b251e24a9be26558e4", + "lastseen": "2021-02-15T12:23Z", + "manufacturername": "Danfoss", + "modelid": model_id, + "name": "0x8030", + "state": { + "lastupdated": "2021-02-15T12:23:07.994", + "on": False, + "temperature": 2307, }, - "3": { - "config": { - "battery": 86, - "heatsetpoint": 2350, - "offset": 0, - "on": True, - "reachable": True, - "schedule": {}, - "schedule_on": False, - }, - "ep": 3, - "etag": "f50061174bb7f18a3d95789bab8b646d", - "lastseen": "2021-02-15T12:23Z", - "manufacturername": "Danfoss", - "modelid": model_id, - "name": "0x8030", - "state": { - "lastupdated": "2021-02-15T12:23:25.466", - "on": False, - "temperature": 2337, - }, - "swversion": "YYYYMMDD", - "type": "ZHAThermostat", - "uniqueid": "58:8e:81:ff:fe:00:11:22-03-0201", + "swversion": "YYYYMMDD", + "type": "ZHAThermostat", + "uniqueid": "58:8e:81:ff:fe:00:11:22-01-0201", + }, + "2": { + "config": { + "battery": 86, + "heatsetpoint": 2300, + "offset": 0, + "on": True, + "reachable": True, + "schedule": {}, + "schedule_on": False, }, - "4": { - "config": { - "battery": 85, - "heatsetpoint": 2300, - "offset": 0, - "on": True, - "reachable": True, - "schedule": {}, - "schedule_on": False, - }, - "ep": 4, - "etag": "eea97adf8ce1b971b8b6a3a31793f96b", - "lastseen": "2021-02-15T12:23Z", - "manufacturername": "Danfoss", - "modelid": model_id, - "name": "0x8030", - "state": { - "lastupdated": "2021-02-15T12:23:41.939", - "on": False, - "temperature": 2333, - }, - "swversion": "YYYYMMDD", - "type": "ZHAThermostat", - "uniqueid": "58:8e:81:ff:fe:00:11:22-04-0201", + "ep": 2, + "etag": "62f12749f9f51c950086aff37dd02b61", + "lastseen": "2021-02-15T12:23Z", + "manufacturername": "Danfoss", + "modelid": model_id, + "name": "0x8030", + "state": { + "lastupdated": "2021-02-15T12:23:22.399", + "on": False, + "temperature": 2316, }, - "5": { - "config": { - "battery": 83, - "heatsetpoint": 2300, - "offset": 0, - "on": True, - "reachable": True, - "schedule": {}, - "schedule_on": False, - }, - "ep": 5, - "etag": "1f7cd1a5d66dc27ac5eb44b8c47362fb", - "lastseen": "2021-02-15T12:23Z", - "manufacturername": "Danfoss", - "modelid": model_id, - "name": "0x8030", - "state": {"lastupdated": "none", "on": False, "temperature": 2325}, - "swversion": "YYYYMMDD", - "type": "ZHAThermostat", - "uniqueid": "58:8e:81:ff:fe:00:11:22-05-0201", + "swversion": "YYYYMMDD", + "type": "ZHAThermostat", + "uniqueid": "58:8e:81:ff:fe:00:11:22-02-0201", + }, + "3": { + "config": { + "battery": 86, + "heatsetpoint": 2350, + "offset": 0, + "on": True, + "reachable": True, + "schedule": {}, + "schedule_on": False, }, - } + "ep": 3, + "etag": "f50061174bb7f18a3d95789bab8b646d", + "lastseen": "2021-02-15T12:23Z", + "manufacturername": "Danfoss", + "modelid": model_id, + "name": "0x8030", + "state": { + "lastupdated": "2021-02-15T12:23:25.466", + "on": False, + "temperature": 2337, + }, + "swversion": "YYYYMMDD", + "type": "ZHAThermostat", + "uniqueid": "58:8e:81:ff:fe:00:11:22-03-0201", + }, + "4": { + "config": { + "battery": 85, + "heatsetpoint": 2300, + "offset": 0, + "on": True, + "reachable": True, + "schedule": {}, + "schedule_on": False, + }, + "ep": 4, + "etag": "eea97adf8ce1b971b8b6a3a31793f96b", + "lastseen": "2021-02-15T12:23Z", + "manufacturername": "Danfoss", + "modelid": model_id, + "name": "0x8030", + "state": { + "lastupdated": "2021-02-15T12:23:41.939", + "on": False, + "temperature": 2333, + }, + "swversion": "YYYYMMDD", + "type": "ZHAThermostat", + "uniqueid": "58:8e:81:ff:fe:00:11:22-04-0201", + }, + "5": { + "config": { + "battery": 83, + "heatsetpoint": 2300, + "offset": 0, + "on": True, + "reachable": True, + "schedule": {}, + "schedule_on": False, + }, + "ep": 5, + "etag": "1f7cd1a5d66dc27ac5eb44b8c47362fb", + "lastseen": "2021-02-15T12:23Z", + "manufacturername": "Danfoss", + "modelid": model_id, + "name": "0x8030", + "state": {"lastupdated": "none", "on": False, "temperature": 2325}, + "swversion": "YYYYMMDD", + "type": "ZHAThermostat", + "uniqueid": "58:8e:81:ff:fe:00:11:22-05-0201", + }, } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) + + await config_entry_factory() assert len(hass.states.async_all()) == 10 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 5 -async def test_unsupported_sensor( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@pytest.mark.parametrize( + "sensor_payload", + [{"0": {"type": "not supported", "name": "name", "state": {}, "config": {}}}], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_unsupported_sensor(hass: HomeAssistant) -> None: """Test that unsupported sensors doesn't break anything.""" - data = { - "sensors": { - "0": {"type": "not supported", "name": "name", "state": {}, "config": {}} - } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 From 67a4c2c8847ad2885b7d4384ef03ae2ab6b86b4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Jul 2024 04:50:50 -0500 Subject: [PATCH 0726/2411] Add support for event entity motion sensors to HomeKit (#121123) --- homeassistant/components/homekit/__init__.py | 7 +- .../components/homekit/type_cameras.py | 36 +++++-- tests/components/homekit/test_homekit.py | 21 ++-- tests/components/homekit/test_type_cameras.py | 97 +++++++++++++++++++ 4 files changed, 145 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 7e1bbad70b4..37927758862 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -163,6 +163,7 @@ BATTERY_CHARGING_SENSOR = ( BinarySensorDeviceClass.BATTERY_CHARGING, ) BATTERY_SENSOR = (SENSOR_DOMAIN, SensorDeviceClass.BATTERY) +MOTION_EVENT_SENSOR = (EVENT_DOMAIN, EventDeviceClass.MOTION) MOTION_SENSOR = (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.MOTION) DOORBELL_EVENT_SENSOR = (EVENT_DOMAIN, EventDeviceClass.DOORBELL) DOORBELL_BINARY_SENSOR = (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.OCCUPANCY) @@ -1121,7 +1122,11 @@ class HomeKit: ) if domain == CAMERA_DOMAIN: - if motion_binary_sensor_entity_id := lookup.get(MOTION_SENSOR): + if motion_event_entity_id := lookup.get(MOTION_EVENT_SENSOR): + config[entity_id].setdefault( + CONF_LINKED_MOTION_SENSOR, motion_event_entity_id + ) + elif motion_binary_sensor_entity_id := lookup.get(MOTION_SENSOR): config[entity_id].setdefault( CONF_LINKED_MOTION_SENSOR, motion_binary_sensor_entity_id ) diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index ca3a2f0d021..40fd6b2aade 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -222,15 +222,19 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] ) self._char_motion_detected = None - self.linked_motion_sensor = self.config.get(CONF_LINKED_MOTION_SENSOR) - if self.linked_motion_sensor: - state = self.hass.states.get(self.linked_motion_sensor) - if state: + self.linked_motion_sensor: str | None = self.config.get( + CONF_LINKED_MOTION_SENSOR + ) + self.motion_is_event = False + if linked_motion_sensor := self.linked_motion_sensor: + self.motion_is_event = linked_motion_sensor.startswith("event.") + if state := self.hass.states.get(linked_motion_sensor): serv_motion = self.add_preload_service(SERV_MOTION_SENSOR) self._char_motion_detected = serv_motion.configure_char( CHAR_MOTION_DETECTED, value=False ) - self._async_update_motion_state(state) + if not self.motion_is_event: + self._async_update_motion_state(state) self._char_doorbell_detected = None self._char_doorbell_detected_switch = None @@ -309,12 +313,26 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] if not new_state: return - detected = new_state.state == STATE_ON - assert self._char_motion_detected - if self._char_motion_detected.value == detected: + state = new_state.state + char = self._char_motion_detected + assert char is not None + if self.motion_is_event: + if state in (STATE_UNKNOWN, STATE_UNAVAILABLE): + return + _LOGGER.debug( + "%s: Set linked motion %s sensor to True/False", + self.entity_id, + self.linked_motion_sensor, + ) + char.set_value(True) + char.set_value(False) return - self._char_motion_detected.set_value(detected) + detected = state == STATE_ON + if char.value == detected: + return + + char.set_value(detected) _LOGGER.debug( "%s: Set linked motion %s sensor to %d", self.entity_id, diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index da755dc26f3..45da90b5446 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1939,12 +1939,21 @@ async def test_homekit_ignored_missing_devices( ) +@pytest.mark.parametrize( + ("domain", "device_class"), + [ + ("binary_sensor", BinarySensorDeviceClass.MOTION), + ("event", EventDeviceClass.MOTION), + ], +) @pytest.mark.usefixtures("mock_async_zeroconf") async def test_homekit_finds_linked_motion_sensors( hass: HomeAssistant, hk_driver, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + domain: str, + device_class: EventDeviceClass | BinarySensorDeviceClass, ) -> None: """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -1964,21 +1973,21 @@ async def test_homekit_finds_linked_motion_sensors( connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - binary_motion_sensor = entity_registry.async_get_or_create( - "binary_sensor", + entry = entity_registry.async_get_or_create( + domain, "camera", "motion_sensor", device_id=device_entry.id, - original_device_class=BinarySensorDeviceClass.MOTION, + original_device_class=device_class, ) camera = entity_registry.async_get_or_create( "camera", "camera", "demo", device_id=device_entry.id ) hass.states.async_set( - binary_motion_sensor.entity_id, + entry.entity_id, STATE_ON, - {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION}, + {ATTR_DEVICE_CLASS: device_class}, ) hass.states.async_set(camera.entity_id, STATE_ON) @@ -2001,7 +2010,7 @@ async def test_homekit_finds_linked_motion_sensors( "model": "Camera Server", "platform": "test", "sw_version": "0.16.0", - "linked_motion_sensor": "binary_sensor.camera_motion_sensor", + "linked_motion_sensor": entry.entity_id, }, ) diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 510af680eaa..fd5d1835641 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -795,6 +795,103 @@ async def test_camera_with_linked_motion_sensor( assert char.value is True +async def test_camera_with_linked_motion_event( + hass: HomeAssistant, run_driver, events +) -> None: + """Test a camera with a linked motion event entity can update.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + motion_entity_id = "event.motion" + + hass.states.async_set( + motion_entity_id, + dt_util.utcnow().isoformat(), + {ATTR_DEVICE_CLASS: EventDeviceClass.MOTION}, + ) + await hass.async_block_till_done() + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera( + hass, + run_driver, + "Camera", + entity_id, + 2, + { + CONF_STREAM_SOURCE: "/dev/null", + CONF_SUPPORT_AUDIO: True, + CONF_VIDEO_CODEC: VIDEO_CODEC_H264_OMX, + CONF_AUDIO_CODEC: AUDIO_CODEC_COPY, + CONF_LINKED_MOTION_SENSOR: motion_entity_id, + }, + ) + bridge = HomeBridge("hass", run_driver, "Test Bridge") + bridge.add_accessory(acc) + + acc.run() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + service = acc.get_service(SERV_MOTION_SENSOR) + assert service + char = service.get_characteristic(CHAR_MOTION_DETECTED) + assert char + + assert char.value is False + broker = MagicMock() + char.broker = broker + + hass.states.async_set( + motion_entity_id, STATE_UNKNOWN, {ATTR_DEVICE_CLASS: EventDeviceClass.MOTION} + ) + await hass.async_block_till_done() + assert len(broker.mock_calls) == 0 + broker.reset_mock() + assert char.value is False + + char.set_value(True) + fire_time = dt_util.utcnow().isoformat() + hass.states.async_set( + motion_entity_id, fire_time, {ATTR_DEVICE_CLASS: EventDeviceClass.MOTION} + ) + await hass.async_block_till_done() + assert len(broker.mock_calls) == 4 + broker.reset_mock() + assert char.value is False + + hass.states.async_set( + motion_entity_id, + fire_time, + {ATTR_DEVICE_CLASS: EventDeviceClass.MOTION}, + force_update=True, + ) + await hass.async_block_till_done() + assert len(broker.mock_calls) == 0 + broker.reset_mock() + + hass.states.async_set( + motion_entity_id, + fire_time, + {ATTR_DEVICE_CLASS: EventDeviceClass.MOTION, "other": "attr"}, + ) + await hass.async_block_till_done() + assert len(broker.mock_calls) == 0 + broker.reset_mock() + # Ensure we do not throw when the linked + # motion sensor is removed + hass.states.async_remove(motion_entity_id) + await hass.async_block_till_done() + acc.run() + await hass.async_block_till_done() + assert char.value is False + + async def test_camera_with_a_missing_linked_motion_sensor( hass: HomeAssistant, run_driver, events ) -> None: From 3c69301365e3e1a50dd279e715e36e7abe2bc110 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 11:53:49 +0200 Subject: [PATCH 0727/2411] Improve type hints in guardian tests (#121175) --- tests/components/guardian/conftest.py | 72 ++++++++++--------- tests/components/guardian/test_config_flow.py | 17 +++-- tests/components/guardian/test_diagnostics.py | 5 +- 3 files changed, 51 insertions(+), 43 deletions(-) diff --git a/tests/components/guardian/conftest.py b/tests/components/guardian/conftest.py index 0063375f6ff..61813cb1df5 100644 --- a/tests/components/guardian/conftest.py +++ b/tests/components/guardian/conftest.py @@ -1,16 +1,18 @@ """Define fixtures for Elexa Guardian tests.""" -from collections.abc import Generator -import json +from collections.abc import AsyncGenerator, Generator +from typing import Any from unittest.mock import AsyncMock, patch import pytest from homeassistant.components.guardian import CONF_UID, DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util.json import JsonObjectType -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, load_json_object_fixture @pytest.fixture @@ -23,7 +25,9 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, unique_id): +def config_entry_fixture( + hass: HomeAssistant, config: dict[str, Any], unique_id: str +) -> MockConfigEntry: """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, @@ -35,7 +39,7 @@ def config_entry_fixture(hass, config, unique_id): @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture() -> dict[str, Any]: """Define a config entry data fixture.""" return { CONF_IP_ADDRESS: "192.168.1.100", @@ -44,68 +48,68 @@ def config_fixture(hass): @pytest.fixture(name="data_sensor_pair_dump", scope="package") -def data_sensor_pair_dump_fixture(): +def data_sensor_pair_dump_fixture() -> JsonObjectType: """Define data from a successful sensor_pair_dump response.""" - return json.loads(load_fixture("sensor_pair_dump_data.json", "guardian")) + return load_json_object_fixture("sensor_pair_dump_data.json", "guardian") @pytest.fixture(name="data_sensor_pair_sensor", scope="package") -def data_sensor_pair_sensor_fixture(): +def data_sensor_pair_sensor_fixture() -> JsonObjectType: """Define data from a successful sensor_pair_sensor response.""" - return json.loads(load_fixture("sensor_pair_sensor_data.json", "guardian")) + return load_json_object_fixture("sensor_pair_sensor_data.json", "guardian") @pytest.fixture(name="data_sensor_paired_sensor_status", scope="package") -def data_sensor_paired_sensor_status_fixture(): +def data_sensor_paired_sensor_status_fixture() -> JsonObjectType: """Define data from a successful sensor_paired_sensor_status response.""" - return json.loads(load_fixture("sensor_paired_sensor_status_data.json", "guardian")) + return load_json_object_fixture("sensor_paired_sensor_status_data.json", "guardian") @pytest.fixture(name="data_system_diagnostics", scope="package") -def data_system_diagnostics_fixture(): +def data_system_diagnostics_fixture() -> JsonObjectType: """Define data from a successful system_diagnostics response.""" - return json.loads(load_fixture("system_diagnostics_data.json", "guardian")) + return load_json_object_fixture("system_diagnostics_data.json", "guardian") @pytest.fixture(name="data_system_onboard_sensor_status", scope="package") -def data_system_onboard_sensor_status_fixture(): +def data_system_onboard_sensor_status_fixture() -> JsonObjectType: """Define data from a successful system_onboard_sensor_status response.""" - return json.loads( - load_fixture("system_onboard_sensor_status_data.json", "guardian") + return load_json_object_fixture( + "system_onboard_sensor_status_data.json", "guardian" ) @pytest.fixture(name="data_system_ping", scope="package") -def data_system_ping_fixture(): +def data_system_ping_fixture() -> JsonObjectType: """Define data from a successful system_ping response.""" - return json.loads(load_fixture("system_ping_data.json", "guardian")) + return load_json_object_fixture("system_ping_data.json", "guardian") @pytest.fixture(name="data_valve_status", scope="package") -def data_valve_status_fixture(): +def data_valve_status_fixture() -> JsonObjectType: """Define data from a successful valve_status response.""" - return json.loads(load_fixture("valve_status_data.json", "guardian")) + return load_json_object_fixture("valve_status_data.json", "guardian") @pytest.fixture(name="data_wifi_status", scope="package") -def data_wifi_status_fixture(): +def data_wifi_status_fixture() -> JsonObjectType: """Define data from a successful wifi_status response.""" - return json.loads(load_fixture("wifi_status_data.json", "guardian")) + return load_json_object_fixture("wifi_status_data.json", "guardian") @pytest.fixture(name="setup_guardian") async def setup_guardian_fixture( - hass, - config, - data_sensor_pair_dump, - data_sensor_pair_sensor, - data_sensor_paired_sensor_status, - data_system_diagnostics, - data_system_onboard_sensor_status, - data_system_ping, - data_valve_status, - data_wifi_status, -): + hass: HomeAssistant, + config: dict[str, Any], + data_sensor_pair_dump: JsonObjectType, + data_sensor_pair_sensor: JsonObjectType, + data_sensor_paired_sensor_status: JsonObjectType, + data_system_diagnostics: JsonObjectType, + data_system_onboard_sensor_status: JsonObjectType, + data_system_ping: JsonObjectType, + data_valve_status: JsonObjectType, + data_wifi_status: JsonObjectType, +) -> AsyncGenerator[None]: """Define a fixture to set up Guardian.""" with ( patch("aioguardian.client.Client.connect"), @@ -155,6 +159,6 @@ async def setup_guardian_fixture( @pytest.fixture(name="unique_id") -def unique_id_fixture(hass): +def unique_id_fixture() -> str: """Define a config entry unique ID fixture.""" return "guardian_3456" diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index 0f99578768a..6c06171a45f 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -1,6 +1,7 @@ """Define tests for the Elexa Guardian config flow.""" from ipaddress import ip_address +from typing import Any from unittest.mock import patch from aioguardian.errors import GuardianError @@ -22,9 +23,8 @@ from tests.common import MockConfigEntry pytestmark = pytest.mark.usefixtures("mock_setup_entry") -async def test_duplicate_error( - hass: HomeAssistant, config, config_entry, setup_guardian -) -> None: +@pytest.mark.usefixtures("config_entry", "setup_guardian") +async def test_duplicate_error(hass: HomeAssistant, config: dict[str, Any]) -> None: """Test that errors are shown when duplicate entries are added.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config @@ -33,7 +33,7 @@ async def test_duplicate_error( assert result["reason"] == "already_configured" -async def test_connect_error(hass: HomeAssistant, config) -> None: +async def test_connect_error(hass: HomeAssistant, config: dict[str, Any]) -> None: """Test that the config entry errors out if the device cannot connect.""" with patch( "aioguardian.client.Client.connect", @@ -58,7 +58,8 @@ async def test_get_pin_from_uid() -> None: assert pin == "3456" -async def test_step_user(hass: HomeAssistant, config, setup_guardian) -> None: +@pytest.mark.usefixtures("setup_guardian") +async def test_step_user(hass: HomeAssistant, config: dict[str, Any]) -> None: """Test the user step.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -78,7 +79,8 @@ async def test_step_user(hass: HomeAssistant, config, setup_guardian) -> None: } -async def test_step_zeroconf(hass: HomeAssistant, setup_guardian) -> None: +@pytest.mark.usefixtures("setup_guardian") +async def test_step_zeroconf(hass: HomeAssistant) -> None: """Test the zeroconf step.""" zeroconf_data = zeroconf.ZeroconfServiceInfo( ip_address=ip_address("192.168.1.100"), @@ -133,7 +135,8 @@ async def test_step_zeroconf_already_in_progress(hass: HomeAssistant) -> None: assert result["reason"] == "already_in_progress" -async def test_step_dhcp(hass: HomeAssistant, setup_guardian) -> None: +@pytest.mark.usefixtures("setup_guardian") +async def test_step_dhcp(hass: HomeAssistant) -> None: """Test the dhcp step.""" dhcp_data = dhcp.DhcpServiceInfo( ip="192.168.1.100", diff --git a/tests/components/guardian/test_diagnostics.py b/tests/components/guardian/test_diagnostics.py index 02b620b8e01..6ec7376f3ef 100644 --- a/tests/components/guardian/test_diagnostics.py +++ b/tests/components/guardian/test_diagnostics.py @@ -4,15 +4,16 @@ from homeassistant.components.diagnostics import REDACTED from homeassistant.components.guardian import DOMAIN, GuardianData from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( hass: HomeAssistant, - config_entry, + config_entry: MockConfigEntry, hass_client: ClientSessionGenerator, - setup_guardian, + setup_guardian: None, # relies on config_entry fixture ) -> None: """Test config entry diagnostics.""" data: GuardianData = hass.data[DOMAIN][config_entry.entry_id] From 255778d0c728688fc33150d18ca7cd7e63208af7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Jul 2024 11:54:40 +0200 Subject: [PATCH 0728/2411] Use recorder test fixtures in recorder init tests (#121176) --- tests/components/recorder/test_init.py | 58 ++++++++++++-------------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 9d4e85eccf1..0504fcc8f91 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1367,9 +1367,10 @@ async def test_statistics_runs_initiated( @pytest.mark.freeze_time("2022-09-13 09:00:00+02:00") @pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.parametrize("enable_statistics", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_compile_missing_statistics( - recorder_db_url: str, freezer: FrozenDateTimeFactory + async_test_recorder: RecorderInstanceGenerator, freezer: FrozenDateTimeFactory ) -> None: """Test missing statistics are compiled on startup.""" now = dt_util.utcnow().replace(minute=0, second=0, microsecond=0) @@ -1378,16 +1379,14 @@ async def test_compile_missing_statistics( with session_scope(hass=hass, read_only=True) as session: return list(session.query(StatisticsRuns)) - async with async_test_home_assistant() as hass: - recorder_helper.async_initialize_recorder(hass) - await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_DB_URL: recorder_db_url}} - ) + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass, wait_recorder=False) as instance, + ): await hass.async_start() await async_wait_recording_done(hass) await async_wait_recording_done(hass) - instance = recorder.get_instance(hass) statistics_runs = await instance.async_add_executor_job( get_statistic_runs, hass ) @@ -1413,7 +1412,10 @@ async def test_compile_missing_statistics( stats_hourly.append(event) freezer.tick(timedelta(hours=1)) - async with async_test_home_assistant() as hass: + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass, wait_recorder=False) as instance, + ): hass.bus.async_listen( EVENT_RECORDER_5MIN_STATISTICS_GENERATED, async_5min_stats_updated_listener ) @@ -1422,15 +1424,9 @@ async def test_compile_missing_statistics( async_hourly_stats_updated_listener, ) - recorder_helper.async_initialize_recorder(hass) - await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_DB_URL: recorder_db_url}} - ) - await hass.async_start() await async_wait_recording_done(hass) await async_wait_recording_done(hass) - instance = recorder.get_instance(hass) statistics_runs = await instance.async_add_executor_job( get_statistic_runs, hass ) @@ -1632,22 +1628,22 @@ async def test_service_disable_states_not_recording( @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage -async def test_service_disable_run_information_recorded(recorder_db_url: str) -> None: +async def test_service_disable_run_information_recorded( + async_test_recorder: RecorderInstanceGenerator, +) -> None: """Test that runs are still recorded when recorder is disabled.""" def get_recorder_runs(hass: HomeAssistant) -> list: with session_scope(hass=hass, read_only=True) as session: return list(session.query(RecorderRuns)) - async with async_test_home_assistant() as hass: - recorder_helper.async_initialize_recorder(hass) - await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_DB_URL: recorder_db_url}} - ) + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): await hass.async_start() await async_wait_recording_done(hass) - instance = recorder.get_instance(hass) db_run_info = await instance.async_add_executor_job(get_recorder_runs, hass) assert len(db_run_info) == 1 assert db_run_info[0].start is not None @@ -1663,15 +1659,13 @@ async def test_service_disable_run_information_recorded(recorder_db_url: str) -> await async_wait_recording_done(hass) await hass.async_stop() - async with async_test_home_assistant() as hass: - recorder_helper.async_initialize_recorder(hass) - await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_DB_URL: recorder_db_url}} - ) + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): await hass.async_start() await async_wait_recording_done(hass) - instance = recorder.get_instance(hass) db_run_info = await instance.async_add_executor_job(get_recorder_runs, hass) assert len(db_run_info) == 2 assert db_run_info[0].start is not None @@ -1689,14 +1683,14 @@ class CannotSerializeMe: @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) @pytest.mark.usefixtures("skip_by_db_engine") @pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.parametrize("recorder_config", [{CONF_COMMIT_INTERVAL: 0}]) async def test_database_corruption_while_running( - hass: HomeAssistant, recorder_db_url: str, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + recorder_mock: Recorder, + recorder_db_url: str, + caplog: pytest.LogCaptureFixture, ) -> None: """Test we can recover from sqlite3 db corruption.""" - recorder_helper.async_initialize_recorder(hass) - assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_DB_URL: recorder_db_url, CONF_COMMIT_INTERVAL: 0}} - ) await hass.async_block_till_done() caplog.clear() From ad1ba1a5e5a9a7250cf5c7db9eee606972ade03a Mon Sep 17 00:00:00 2001 From: Giuliano Riccio Date: Thu, 4 Jul 2024 12:47:32 +0200 Subject: [PATCH 0729/2411] Fix Google assistant SDK broadcasting command for italian (#116198) Fixed broadcasting command for italian Co-authored-by: tronikos --- homeassistant/components/google_assistant_sdk/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant_sdk/notify.py b/homeassistant/components/google_assistant_sdk/notify.py index 8ea3d37d5b6..ffe34eefdfd 100644 --- a/homeassistant/components/google_assistant_sdk/notify.py +++ b/homeassistant/components/google_assistant_sdk/notify.py @@ -21,7 +21,7 @@ LANG_TO_BROADCAST_COMMAND = { ), "es": ("Anuncia {0}", "Anuncia en {1} {0}"), "fr": ("Diffuse {0}", "Diffuse dans {1} {0}"), - "it": ("Trasmetti {0}", "Trasmetti in {1} {0}"), + "it": ("Trasmetti a tutti {0}", "Trasmetti in {1} {0}"), "ja": ("{0}とブロードキャストして", "{0}と{1}にブロードキャストして"), "ko": ("{0} 라고 방송해 줘", "{0} 라고 {1}에 방송해 줘"), "pt": ("Transmitir {0}", "Transmitir {0} para {1}"), From d1264655a0d110e9459c4cde5796262c59d27627 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Jul 2024 13:04:52 +0200 Subject: [PATCH 0730/2411] Fix some typos in core.py (#121189) --- homeassistant/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 1714cff216d..5d223b9f19f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -168,7 +168,7 @@ class EventStateEventData(TypedDict): class EventStateChangedData(EventStateEventData): """EVENT_STATE_CHANGED data. - A state changed event is fired when on state write when the state is changed. + A state changed event is fired when on state write the state is changed. """ old_state: State | None @@ -177,7 +177,7 @@ class EventStateChangedData(EventStateEventData): class EventStateReportedData(EventStateEventData): """EVENT_STATE_REPORTED data. - A state reported event is fired when on state write when the state is unchanged. + A state reported event is fired when on state write the state is unchanged. """ old_last_reported: datetime.datetime From f1d6ad90737ac849985fab0e966c3d85a65f1907 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Jul 2024 13:10:08 +0200 Subject: [PATCH 0731/2411] Add test fixture to control recorder migration (#121180) * Add test fixture to control recorder migration * Update tests/components/recorder/conftest.py Co-authored-by: J. Nick Koston * Update tests/components/recorder/conftest.py --------- Co-authored-by: J. Nick Koston --- tests/components/recorder/conftest.py | 75 +++++++++++++++- tests/components/recorder/test_migrate.py | 81 +++++++---------- .../components/recorder/test_websocket_api.py | 90 +++++++++---------- 3 files changed, 147 insertions(+), 99 deletions(-) diff --git a/tests/components/recorder/conftest.py b/tests/components/recorder/conftest.py index 1a3c25ec727..b0e648befcf 100644 --- a/tests/components/recorder/conftest.py +++ b/tests/components/recorder/conftest.py @@ -1,11 +1,15 @@ """Fixtures for the recorder component tests.""" -from collections.abc import Generator -from unittest.mock import patch +from dataclasses import dataclass +import threading +from unittest.mock import Mock, patch import pytest +from typing_extensions import AsyncGenerator, Generator from homeassistant.components import recorder +from homeassistant.components.recorder import db_schema +from homeassistant.components.recorder.util import session_scope from homeassistant.core import HomeAssistant @@ -46,3 +50,70 @@ def recorder_dialect_name(hass: HomeAssistant, db_engine: str) -> Generator[None "homeassistant.components.recorder.Recorder.dialect_name", db_engine ): yield + + +@dataclass(slots=True) +class InstrumentedMigration: + """Container to aid controlling migration progress.""" + + migration_done: threading.Event + migration_stall: threading.Event + migration_started: threading.Event + migration_version: int | None + apply_update_mock: Mock + + +@pytest.fixture +async def instrument_migration( + hass: HomeAssistant, +) -> AsyncGenerator[InstrumentedMigration]: + """Instrument recorder migration.""" + + real_migrate_schema = recorder.migration.migrate_schema + real_apply_update = recorder.migration._apply_update + + def _instrument_migrate_schema(*args): + """Control migration progress and check results.""" + instrumented_migration.migration_started.set() + + try: + real_migrate_schema(*args) + except Exception: + instrumented_migration.migration_done.set() + raise + + # Check and report the outcome of the migration; if migration fails + # the recorder will silently create a new database. + with session_scope(hass=hass, read_only=True) as session: + res = ( + session.query(db_schema.SchemaChanges) + .order_by(db_schema.SchemaChanges.change_id.desc()) + .first() + ) + instrumented_migration.migration_version = res.schema_version + instrumented_migration.migration_done.set() + + def _instrument_apply_update(*args): + """Control migration progress.""" + instrumented_migration.migration_stall.wait() + real_apply_update(*args) + + with ( + patch( + "homeassistant.components.recorder.migration.migrate_schema", + wraps=_instrument_migrate_schema, + ), + patch( + "homeassistant.components.recorder.migration._apply_update", + wraps=_instrument_apply_update, + ) as apply_update_mock, + ): + instrumented_migration = InstrumentedMigration( + migration_done=threading.Event(), + migration_stall=threading.Event(), + migration_started=threading.Event(), + migration_version=None, + apply_update_mock=apply_update_mock, + ) + + yield instrumented_migration diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 423462f333f..cd9650779b5 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -4,7 +4,6 @@ import datetime import importlib import sqlite3 import sys -import threading from unittest.mock import Mock, PropertyMock, call, patch import pytest @@ -33,6 +32,7 @@ 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 +from .conftest import InstrumentedMigration from tests.common import async_fire_time_changed from tests.typing import RecorderInstanceGenerator @@ -91,6 +91,7 @@ async def test_migration_in_progress( hass: HomeAssistant, recorder_db_url: str, async_setup_recorder_instance: RecorderInstanceGenerator, + instrument_migration: InstrumentedMigration, ) -> None: """Test that we can check for migration in progress.""" if recorder_db_url.startswith("mysql://"): @@ -110,8 +111,11 @@ async def test_migration_in_progress( ), ): await async_setup_recorder_instance(hass, wait_recorder=False) - await recorder.get_instance(hass).async_migration_event.wait() + await hass.async_add_executor_job(instrument_migration.migration_started.wait) assert recorder.util.async_migration_in_progress(hass) is True + + # Let migration finish + instrument_migration.migration_stall.set() await async_wait_recording_done(hass) assert recorder.util.async_migration_in_progress(hass) is False @@ -235,7 +239,9 @@ async def test_database_migration_encounters_corruption_not_sqlite( async def test_events_during_migration_are_queued( - hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator + hass: HomeAssistant, + async_setup_recorder_instance: RecorderInstanceGenerator, + instrument_migration: InstrumentedMigration, ) -> None: """Test that events during migration are queued.""" @@ -247,13 +253,20 @@ async def test_events_during_migration_are_queued( new=create_engine_test, ), ): - await async_setup_recorder_instance(hass, {"commit_interval": 0}) + await async_setup_recorder_instance( + hass, {"commit_interval": 0}, wait_recorder=False + ) + await hass.async_add_executor_job(instrument_migration.migration_started.wait) + assert recorder.util.async_migration_in_progress(hass) is True hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() 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)) + + # Let migration finish + instrument_migration.migration_stall.set() await recorder.get_instance(hass).async_recorder_ready.wait() await async_wait_recording_done(hass) @@ -265,7 +278,9 @@ async def test_events_during_migration_are_queued( async def test_events_during_migration_queue_exhausted( - hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator + hass: HomeAssistant, + async_setup_recorder_instance: RecorderInstanceGenerator, + instrument_migration: InstrumentedMigration, ) -> None: """Test that events during migration takes so long the queue is exhausted.""" @@ -282,6 +297,8 @@ async def test_events_during_migration_queue_exhausted( await async_setup_recorder_instance( hass, {"commit_interval": 0}, wait_recorder=False ) + await hass.async_add_executor_job(instrument_migration.migration_started.wait) + assert recorder.util.async_migration_in_progress(hass) is True hass.states.async_set("my.entity", "on", {}) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) @@ -289,6 +306,9 @@ async def test_events_during_migration_queue_exhausted( 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", {}) + + # Let migration finish + instrument_migration.migration_stall.set() await recorder.get_instance(hass).async_recorder_ready.wait() await async_wait_recording_done(hass) @@ -313,6 +333,7 @@ async def test_schema_migrate( hass: HomeAssistant, recorder_db_url: str, async_setup_recorder_instance: RecorderInstanceGenerator, + instrument_migration: InstrumentedMigration, start_version, live, ) -> None: @@ -323,11 +344,6 @@ async def test_schema_migrate( inspection could quickly become quite cumbersome. """ - migration_done = threading.Event() - migration_stall = threading.Event() - migration_version = None - real_migrate_schema = recorder.migration.migrate_schema - real_apply_update = recorder.migration._apply_update real_create_index = recorder.migration._create_index create_calls = 0 @@ -354,33 +370,6 @@ async def test_schema_migrate( start=self.recorder_runs_manager.recording_start, created=dt_util.utcnow() ) - def _instrument_migrate_schema(*args): - """Control migration progress and check results.""" - nonlocal migration_done - nonlocal migration_version - try: - real_migrate_schema(*args) - except Exception: - migration_done.set() - raise - - # Check and report the outcome of the migration; if migration fails - # the recorder will silently create a new database. - with session_scope(hass=hass, read_only=True) as session: - res = ( - session.query(db_schema.SchemaChanges) - .order_by(db_schema.SchemaChanges.change_id.desc()) - .first() - ) - 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) - def _sometimes_failing_create_index(*args): """Make the first index create raise a retryable error to ensure we retry.""" if recorder_db_url.startswith("mysql://"): @@ -402,14 +391,6 @@ async def test_schema_migrate( side_effect=_mock_setup_run, autospec=True, ) as setup_run, - patch( - "homeassistant.components.recorder.migration.migrate_schema", - wraps=_instrument_migrate_schema, - ), - patch( - "homeassistant.components.recorder.migration._apply_update", - wraps=_instrument_apply_update, - ) as apply_update_mock, patch("homeassistant.components.recorder.util.time.sleep"), patch( "homeassistant.components.recorder.migration._create_index", @@ -426,18 +407,20 @@ async def test_schema_migrate( ), ): await async_setup_recorder_instance(hass, wait_recorder=False) + await hass.async_add_executor_job(instrument_migration.migration_started.wait) + assert recorder.util.async_migration_in_progress(hass) is True 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() + instrument_migration.migration_stall.set() await hass.async_block_till_done() - await hass.async_add_executor_job(migration_done.wait) + await hass.async_add_executor_job(instrument_migration.migration_done.wait) await async_wait_recording_done(hass) - assert migration_version == db_schema.SCHEMA_VERSION + assert instrument_migration.migration_version == db_schema.SCHEMA_VERSION assert setup_run.called assert recorder.util.async_migration_in_progress(hass) is not True - assert apply_update_mock.called + assert instrument_migration.apply_update_mock.called def test_invalid_update(hass: HomeAssistant) -> None: diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index cc187a1e6ad..508848b9cc7 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -3,7 +3,6 @@ import datetime from datetime import timedelta from statistics import fmean -import threading from unittest.mock import ANY, patch from freezegun import freeze_time @@ -37,9 +36,18 @@ from .common import ( do_adhoc_statistics, statistics_during_period, ) +from .conftest import InstrumentedMigration from tests.common import async_fire_time_changed -from tests.typing import WebSocketGenerator +from tests.typing import RecorderInstanceGenerator, WebSocketGenerator + + +@pytest.fixture +async def mock_recorder_before_hass( + async_setup_recorder_instance: RecorderInstanceGenerator, +) -> None: + """Set up recorder.""" + DISTANCE_SENSOR_FT_ATTRIBUTES = { "device_class": "distance", @@ -2493,70 +2501,56 @@ async def test_recorder_info_no_instance( async def test_recorder_info_migration_queue_exhausted( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + async_test_recorder: RecorderInstanceGenerator, + instrument_migration: InstrumentedMigration, ) -> None: """Test getting recorder status when recorder queue is exhausted.""" assert recorder.util.async_migration_in_progress(hass) is False - migration_done = threading.Event() - - real_migration = recorder.migration._apply_update - - def stalled_migration(*args): - """Make migration stall.""" - nonlocal migration_done - migration_done.wait() - return real_migration(*args) - with ( - patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), - patch("homeassistant.components.recorder.Recorder.async_periodic_statistics"), patch( "homeassistant.components.recorder.core.create_engine", new=create_engine_test, ), patch.object(recorder.core, "MAX_QUEUE_BACKLOG_MIN_VALUE", 1), patch.object(recorder.core, "QUEUE_PERCENTAGE_ALLOWED_AVAILABLE_MEMORY", 0), - patch( - "homeassistant.components.recorder.migration._apply_update", - wraps=stalled_migration, - ), ): - recorder_helper.async_initialize_recorder(hass) - hass.create_task( - async_setup_component( - hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + async with async_test_recorder(hass, wait_recorder=False): + await hass.async_add_executor_job( + instrument_migration.migration_started.wait ) - ) - await recorder_helper.async_wait_recorder(hass) - hass.states.async_set("my.entity", "on", {}) - await hass.async_block_till_done() + assert recorder.util.async_migration_in_progress(hass) is True + await recorder_helper.async_wait_recorder(hass) + hass.states.async_set("my.entity", "on", {}) + await hass.async_block_till_done() - # Detect queue full - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=2)) - await hass.async_block_till_done() + # Detect queue full + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() - client = await hass_ws_client() + client = await hass_ws_client() - # Check the status - await client.send_json_auto_id({"type": "recorder/info"}) - response = await client.receive_json() - assert response["success"] - assert response["result"]["migration_in_progress"] is True - assert response["result"]["recording"] is False - assert response["result"]["thread_running"] is True + # Check the status + await client.send_json_auto_id({"type": "recorder/info"}) + response = await client.receive_json() + assert response["success"] + assert response["result"]["migration_in_progress"] is True + assert response["result"]["recording"] is False + assert response["result"]["thread_running"] is True - # Let migration finish - migration_done.set() - await async_wait_recording_done(hass) + # Let migration finish + instrument_migration.migration_stall.set() + await async_wait_recording_done(hass) - # Check the status after migration finished - await client.send_json_auto_id({"type": "recorder/info"}) - response = await client.receive_json() - assert response["success"] - assert response["result"]["migration_in_progress"] is False - assert response["result"]["recording"] is True - assert response["result"]["thread_running"] is True + # Check the status after migration finished + await client.send_json_auto_id({"type": "recorder/info"}) + response = await client.receive_json() + assert response["success"] + assert response["result"]["migration_in_progress"] is False + assert response["result"]["recording"] is True + assert response["result"]["thread_running"] is True async def test_backup_start_no_recorder( From 31ed32da6cb27a82feed0c930d75e3102a71ce90 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 4 Jul 2024 14:20:57 +0200 Subject: [PATCH 0732/2411] Use fixtures in deCONZ alarm control panel tests (#120967) --- .../deconz/test_alarm_control_panel.py | 71 ++++++++----------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py index c855076de2f..1f1b65aff23 100644 --- a/tests/components/deconz/test_alarm_control_panel.py +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -1,12 +1,14 @@ """deCONZ alarm control panel platform tests.""" -from unittest.mock import patch +from collections.abc import Callable from pydeconz.models.sensor.ancillary_control import AncillaryControlPanel +import pytest from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, @@ -26,29 +28,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from .test_gateway import ( - DECONZ_WEB_REQUEST, - mock_deconz_put_request, - setup_deconz_integration, -) - from tests.test_util.aiohttp import AiohttpClientMocker -async def test_no_sensors( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that no sensors in deconz results in no climate entities.""" - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 - - -async def test_alarm_control_panel( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test successful creation of alarm control panel entities.""" - data = { - "alarmsystems": { +@pytest.mark.parametrize( + "alarm_system_payload", + [ + { "0": { "name": "default", "config": { @@ -75,8 +61,13 @@ async def test_alarm_control_panel( }, }, } - }, - "sensors": { + } + ], +) +@pytest.mark.parametrize( + "sensor_payload", + [ + { "0": { "config": { "battery": 95, @@ -103,11 +94,17 @@ async def test_alarm_control_panel( "type": "ZHAAncillaryControl", "uniqueid": "00:00:00:00:00:00:00:00-00", } - }, - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + } + ], +) +async def test_alarm_control_panel( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + mock_deconz_websocket, +) -> None: + """Test successful creation of alarm control panel entities.""" assert len(hass.states.async_all()) == 4 assert hass.states.get("alarm_control_panel.keypad").state == STATE_UNKNOWN @@ -240,9 +237,7 @@ async def test_alarm_control_panel( # Service set alarm to away mode - mock_deconz_put_request( - aioclient_mock, config_entry.data, "/alarmsystems/0/arm_away" - ) + aioclient_mock = mock_put_request("/alarmsystems/0/arm_away") await hass.services.async_call( ALARM_CONTROL_PANEL_DOMAIN, @@ -254,9 +249,7 @@ async def test_alarm_control_panel( # Service set alarm to home mode - mock_deconz_put_request( - aioclient_mock, config_entry.data, "/alarmsystems/0/arm_stay" - ) + aioclient_mock = mock_put_request("/alarmsystems/0/arm_stay") await hass.services.async_call( ALARM_CONTROL_PANEL_DOMAIN, @@ -268,9 +261,7 @@ async def test_alarm_control_panel( # Service set alarm to night mode - mock_deconz_put_request( - aioclient_mock, config_entry.data, "/alarmsystems/0/arm_night" - ) + aioclient_mock = mock_put_request("/alarmsystems/0/arm_night") await hass.services.async_call( ALARM_CONTROL_PANEL_DOMAIN, @@ -282,7 +273,7 @@ async def test_alarm_control_panel( # Service set alarm to disarmed - mock_deconz_put_request(aioclient_mock, config_entry.data, "/alarmsystems/0/disarm") + aioclient_mock = mock_put_request("/alarmsystems/0/disarm") await hass.services.async_call( ALARM_CONTROL_PANEL_DOMAIN, @@ -292,13 +283,13 @@ async def test_alarm_control_panel( ) assert aioclient_mock.mock_calls[4][2] == {"code0": "4567"} - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) states = hass.states.async_all() assert len(states) == 4 for state in states: assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 From 873d96bab356e5fa91d2d4b40cd22e4b21b578f0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 4 Jul 2024 14:21:26 +0200 Subject: [PATCH 0733/2411] Use fixtures in deCONZ binary sensor tests (#120966) --- tests/components/deconz/conftest.py | 10 +- tests/components/deconz/test_binary_sensor.py | 163 ++++++++---------- 2 files changed, 80 insertions(+), 93 deletions(-) diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py index 619e31b1f88..30bee23cbd2 100644 --- a/tests/components/deconz/conftest.py +++ b/tests/components/deconz/conftest.py @@ -168,8 +168,16 @@ def fixture_light_data() -> dict[str, Any]: @pytest.fixture(name="sensor_payload") -def fixture_sensor_data() -> dict[str, Any]: +def fixture_sensor_data(sensor_1_payload: dict[str, Any]) -> dict[str, Any]: """Sensor data.""" + if sensor_1_payload: + return {"1": sensor_1_payload} + return {} + + +@pytest.fixture(name="sensor_1_payload") +def fixture_sensor_1_data() -> dict[str, Any]: + """Sensor 1 data.""" return {} diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 6ab5f2f5477..76962ec8f46 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -1,6 +1,7 @@ """deCONZ binary sensor platform tests.""" -from unittest.mock import patch +from collections.abc import Callable +from typing import Any import pytest @@ -12,6 +13,7 @@ from homeassistant.components.deconz.const import ( DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, STATE_OFF, @@ -22,23 +24,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from .test_gateway import ( - DECONZ_WEB_REQUEST, - mock_deconz_request, - setup_deconz_integration, -) - -from tests.test_util.aiohttp import AiohttpClientMocker - - -async def test_no_binary_sensors( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that no sensors in deconz results in no sensor entities.""" - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 - - TEST_DATA = [ ( # Alarm binary sensor { @@ -466,22 +451,17 @@ TEST_DATA = [ ] -@pytest.mark.parametrize(("sensor_data", "expected"), TEST_DATA) +@pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: True}]) +@pytest.mark.parametrize(("sensor_1_payload", "expected"), TEST_DATA) async def test_binary_sensors( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, mock_deconz_websocket, - sensor_data, - expected, + expected: dict[str, Any], ) -> None: """Test successful creation of binary sensor entities.""" - with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"1": sensor_data}}): - config_entry = await setup_deconz_integration( - hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True} - ) - assert len(hass.states.async_all()) == expected["entity_count"] # Verify state data @@ -500,7 +480,11 @@ async def test_binary_sensors( # Verify device registry data assert ( - len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + len( + dr.async_entries_for_config_entry( + device_registry, config_entry_setup.entry_id + ) + ) == expected["device_count"] ) @@ -519,46 +503,39 @@ async def test_binary_sensors( # Unload entry - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE # Remove entry - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 -async def test_not_allow_clip_sensor( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that CLIP sensors are not allowed.""" - data = { - "sensors": { - "1": { - "name": "CLIP presence sensor", - "type": "CLIPPresence", - "state": {"presence": False}, - "config": {}, - "uniqueid": "00:00:00:00:00:00:00:02-00", - }, +@pytest.mark.parametrize( + "sensor_1_payload", + [ + { + "name": "CLIP presence sensor", + "type": "CLIPPresence", + "state": {"presence": False}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:02-00", } - } - - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration( - hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: False} - ) - + ], +) +@pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: False}]) +@pytest.mark.usefixtures("config_entry_setup") +async def test_not_allow_clip_sensor(hass: HomeAssistant) -> None: + """Test that CLIP sensors are not allowed.""" assert len(hass.states.async_all()) == 0 -async def test_allow_clip_sensor( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that CLIP sensors can be allowed.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "name": "Presence sensor", "type": "ZHAPresence", @@ -585,12 +562,11 @@ async def test_allow_clip_sensor( "uniqueid": "/sensors/3", }, } - } - - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration( - hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True} - ) + ], +) +@pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: True}]) +async def test_allow_clip_sensor(hass: HomeAssistant, config_entry_setup) -> None: + """Test that CLIP sensors can be allowed.""" assert len(hass.states.async_all()) == 3 assert hass.states.get("binary_sensor.presence_sensor").state == STATE_OFF @@ -600,7 +576,7 @@ async def test_allow_clip_sensor( # Disallow clip sensors hass.config_entries.async_update_entry( - config_entry, options={CONF_ALLOW_CLIP_SENSOR: False} + config_entry_setup, options={CONF_ALLOW_CLIP_SENSOR: False} ) await hass.async_block_till_done() @@ -611,7 +587,7 @@ async def test_allow_clip_sensor( # Allow clip sensors hass.config_entries.async_update_entry( - config_entry, options={CONF_ALLOW_CLIP_SENSOR: True} + config_entry_setup, options={CONF_ALLOW_CLIP_SENSOR: True} ) await hass.async_block_till_done() @@ -620,10 +596,13 @@ async def test_allow_clip_sensor( assert hass.states.get("binary_sensor.clip_flag_boot_time").state == STATE_ON +@pytest.mark.usefixtures("config_entry_setup") async def test_add_new_binary_sensor( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket + hass: HomeAssistant, mock_deconz_websocket ) -> None: """Test that adding a new binary sensor works.""" + assert len(hass.states.async_all()) == 0 + event_added_sensor = { "t": "event", "e": "added", @@ -638,10 +617,6 @@ async def test_add_new_binary_sensor( "uniqueid": "00:00:00:00:00:00:00:00-00", }, } - - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 - await mock_deconz_websocket(data=event_added_sensor) await hass.async_block_till_done() @@ -649,10 +624,15 @@ async def test_add_new_binary_sensor( assert hass.states.get("binary_sensor.presence_sensor").state == STATE_OFF +@pytest.mark.parametrize( + "config_entry_options", [{CONF_MASTER_GATEWAY: True, CONF_ALLOW_NEW_DEVICES: False}] +) async def test_add_new_binary_sensor_ignored_load_entities_on_service_call( hass: HomeAssistant, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, + deconz_payload: dict[str, Any], + mock_requests: Callable[[str], None], mock_deconz_websocket, ) -> None: """Test that adding a new binary sensor is not allowed.""" @@ -671,12 +651,6 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_service_call( "sensor": sensor, } - config_entry = await setup_deconz_integration( - hass, - aioclient_mock, - options={CONF_MASTER_GATEWAY: True, CONF_ALLOW_NEW_DEVICES: False}, - ) - assert len(hass.states.async_all()) == 0 await mock_deconz_websocket(data=event_added_sensor) @@ -686,13 +660,16 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_service_call( assert not hass.states.get("binary_sensor.presence_sensor") assert ( - len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + len( + er.async_entries_for_config_entry( + entity_registry, config_entry_setup.entry_id + ) + ) == 0 ) - aioclient_mock.clear_requests() - data = {"config": {}, "groups": {}, "lights": {}, "sensors": {"1": sensor}} - mock_deconz_request(aioclient_mock, config_entry.data, data) + deconz_payload["sensors"] = {"1": sensor} + mock_requests() await hass.services.async_call(DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH) await hass.async_block_till_done() @@ -701,10 +678,15 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_service_call( assert hass.states.get("binary_sensor.presence_sensor") +@pytest.mark.parametrize( + "config_entry_options", [{CONF_MASTER_GATEWAY: True, CONF_ALLOW_NEW_DEVICES: False}] +) async def test_add_new_binary_sensor_ignored_load_entities_on_options_change( hass: HomeAssistant, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, + deconz_payload: dict[str, Any], + mock_requests: Callable[[str], None], mock_deconz_websocket, ) -> None: """Test that adding a new binary sensor is not allowed.""" @@ -723,12 +705,6 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_options_change( "sensor": sensor, } - config_entry = await setup_deconz_integration( - hass, - aioclient_mock, - options={CONF_MASTER_GATEWAY: True, CONF_ALLOW_NEW_DEVICES: False}, - ) - assert len(hass.states.async_all()) == 0 await mock_deconz_websocket(data=event_added_sensor) @@ -738,16 +714,19 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_options_change( assert not hass.states.get("binary_sensor.presence_sensor") assert ( - len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + len( + er.async_entries_for_config_entry( + entity_registry, config_entry_setup.entry_id + ) + ) == 0 ) - aioclient_mock.clear_requests() - data = {"config": {}, "groups": {}, "lights": {}, "sensors": {"1": sensor}} - mock_deconz_request(aioclient_mock, config_entry.data, data) + deconz_payload["sensors"] = {"1": sensor} + mock_requests() hass.config_entries.async_update_entry( - config_entry, options={CONF_ALLOW_NEW_DEVICES: True} + config_entry_setup, options={CONF_ALLOW_NEW_DEVICES: True} ) await hass.async_block_till_done() From 1d8382a498539839368fe7a5119f98bc52c1e0b3 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 4 Jul 2024 14:22:10 +0200 Subject: [PATCH 0734/2411] Use fixtures in deCONZ light tests PT1 (#121112) Use fixtures in deCONZ light tests part 1 --- tests/components/deconz/test_light.py | 274 ++++++++++++++------------ 1 file changed, 151 insertions(+), 123 deletions(-) diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index d964361df57..411a0552bd1 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -1,5 +1,7 @@ """deCONZ light platform tests.""" +from collections.abc import Callable +from typing import Any from unittest.mock import patch import pytest @@ -29,6 +31,7 @@ from homeassistant.components.light import ( ColorMode, LightEntityFeature, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -987,6 +990,47 @@ async def test_groups( assert len(hass.states.async_all()) == 0 +@pytest.mark.parametrize( + "light_payload", + [ + { + "1": { + "name": "RGB light", + "state": { + "bri": 255, + "colormode": "xy", + "effect": "colorloop", + "hue": 53691, + "on": True, + "reachable": True, + "sat": 141, + "xy": (0.5, 0.5), + }, + "type": "Extended color light", + "uniqueid": "00:00:00:00:00:00:00:00-00", + }, + "2": { + "ctmax": 454, + "ctmin": 155, + "name": "Tunable white light", + "state": { + "on": True, + "colormode": "ct", + "ct": 2500, + "reachable": True, + }, + "type": "Tunable white light", + "uniqueid": "00:00:00:00:00:00:00:01-00", + }, + "3": { + "name": "Dimmable light", + "type": "Dimmable light", + "state": {"bri": 254, "on": True, "reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, + } + ], +) @pytest.mark.parametrize( ("input", "expected"), [ @@ -1045,62 +1089,28 @@ async def test_groups( ], ) async def test_group_service_calls( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, input, expected + hass: HomeAssistant, + config_entry_factory: Callable[[], ConfigEntry], + group_payload: dict[str, Any], + mock_put_request: Callable[[str, str], AiohttpClientMocker], + input: dict[str, Any], + expected: dict[str, Any], ) -> None: """Verify expected group web request from different service calls.""" - data = { - "groups": { - "0": { - "id": "Light group id", - "name": "Group", - "type": "LightGroup", - "state": {"all_on": False, "any_on": input["group_on"]}, - "action": {}, - "scenes": [], - "lights": input["lights"], - }, - }, - "lights": { - "1": { - "name": "RGB light", - "state": { - "bri": 255, - "colormode": "xy", - "effect": "colorloop", - "hue": 53691, - "on": True, - "reachable": True, - "sat": 141, - "xy": (0.5, 0.5), - }, - "type": "Extended color light", - "uniqueid": "00:00:00:00:00:00:00:00-00", - }, - "2": { - "ctmax": 454, - "ctmin": 155, - "name": "Tunable white light", - "state": { - "on": True, - "colormode": "ct", - "ct": 2500, - "reachable": True, - }, - "type": "Tunable white light", - "uniqueid": "00:00:00:00:00:00:00:01-00", - }, - "3": { - "name": "Dimmable light", - "type": "Dimmable light", - "state": {"bri": 254, "on": True, "reachable": True}, - "uniqueid": "00:00:00:00:00:00:00:02-00", - }, + group_payload |= { + "0": { + "id": "Light group id", + "name": "Group", + "type": "LightGroup", + "state": {"all_on": False, "any_on": input["group_on"]}, + "action": {}, + "scenes": [], + "lights": input["lights"], }, } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) + await config_entry_factory() - mock_deconz_put_request(aioclient_mock, config_entry.data, "/groups/0/action") + aioclient_mock = mock_put_request("/groups/0/action") await hass.services.async_call( LIGHT_DOMAIN, @@ -1114,12 +1124,10 @@ async def test_group_service_calls( assert len(aioclient_mock.mock_calls) == 1 # not called -async def test_empty_group( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Verify that a group without a list of lights is not created.""" - data = { - "groups": { +@pytest.mark.parametrize( + "group_payload", + [ + { "0": { "id": "Empty group id", "name": "Empty group", @@ -1129,21 +1137,20 @@ async def test_empty_group( "scenes": [], "lights": [], }, - }, - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - + } + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_empty_group(hass: HomeAssistant) -> None: + """Verify that a group without a list of lights is not created.""" assert len(hass.states.async_all()) == 0 assert not hass.states.get("light.empty_group") -async def test_disable_light_groups( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test disallowing light groups work.""" - data = { - "groups": { +@pytest.mark.parametrize( + "group_payload", + [ + { "1": { "id": "Light group id", "name": "Light group", @@ -1162,8 +1169,13 @@ async def test_disable_light_groups( "scenes": [], "lights": [], }, - }, - "lights": { + } + ], +) +@pytest.mark.parametrize( + "light_payload", + [ + { "1": { "ctmax": 454, "ctmin": 155, @@ -1172,22 +1184,22 @@ async def test_disable_light_groups( "type": "Tunable white light", "uniqueid": "00:00:00:00:00:00:00:01-00", }, - }, - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration( - hass, - aioclient_mock, - options={CONF_ALLOW_DECONZ_GROUPS: False}, - ) - + } + ], +) +@pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_DECONZ_GROUPS: False}]) +async def test_disable_light_groups( + hass: HomeAssistant, + config_entry_setup: ConfigEntry, +) -> None: + """Test disallowing light groups work.""" assert len(hass.states.async_all()) == 1 assert hass.states.get("light.tunable_white_light") assert not hass.states.get("light.light_group") assert not hass.states.get("light.empty_group") hass.config_entries.async_update_entry( - config_entry, options={CONF_ALLOW_DECONZ_GROUPS: True} + config_entry_setup, options={CONF_ALLOW_DECONZ_GROUPS: True} ) await hass.async_block_till_done() @@ -1195,7 +1207,7 @@ async def test_disable_light_groups( assert hass.states.get("light.light_group") hass.config_entries.async_update_entry( - config_entry, options={CONF_ALLOW_DECONZ_GROUPS: False} + config_entry_setup, options={CONF_ALLOW_DECONZ_GROUPS: False} ) await hass.async_block_till_done() @@ -1203,16 +1215,10 @@ async def test_disable_light_groups( assert not hass.states.get("light.light_group") -async def test_non_color_light_reports_color( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Verify hs_color does not crash when a group gets updated with a bad color value. - - After calling a scene color temp light of certain manufacturers - report color temp in color space. - """ - data = { - "groups": { +@pytest.mark.parametrize( + "group_payload", + [ + { "0": { "action": { "alert": "none", @@ -1234,8 +1240,13 @@ async def test_non_color_light_reports_color( "state": {"all_on": False, "any_on": True}, "type": "LightGroup", } - }, - "lights": { + } + ], +) +@pytest.mark.parametrize( + "light_payload", + [ + { "0": { "ctmax": 500, "ctmin": 153, @@ -1285,11 +1296,18 @@ async def test_non_color_light_reports_color( "type": "Color temperature light", "uniqueid": "ec:1b:bd:ff:fe:ee:ed:dd-01", }, - }, - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) + } + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_non_color_light_reports_color( + hass: HomeAssistant, mock_deconz_websocket +) -> None: + """Verify hs_color does not crash when a group gets updated with a bad color value. + After calling a scene color temp light of certain manufacturers + report color temp in color space. + """ assert len(hass.states.async_all()) == 3 assert hass.states.get("light.group").attributes[ATTR_SUPPORTED_COLOR_MODES] == [ ColorMode.COLOR_TEMP, @@ -1328,12 +1346,10 @@ async def test_non_color_light_reports_color( assert group.attributes.get(ATTR_COLOR_TEMP) is None -async def test_verify_group_supported_features( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that group supported features reflect what included lights support.""" - data = { - "groups": { +@pytest.mark.parametrize( + "group_payload", + [ + { "1": { "id": "Group1", "name": "Group", @@ -1343,8 +1359,13 @@ async def test_verify_group_supported_features( "scenes": [], "lights": ["1", "2", "3"], }, - }, - "lights": { + } + ], +) +@pytest.mark.parametrize( + "light_payload", + [ + { "1": { "name": "Dimmable light", "state": {"on": True, "bri": 255, "reachable": True}, @@ -1372,11 +1393,12 @@ async def test_verify_group_supported_features( "type": "Tunable white light", "uniqueid": "00:00:00:00:00:00:00:03-00", }, - }, - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - + } + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_verify_group_supported_features(hass: HomeAssistant) -> None: + """Test that group supported features reflect what included lights support.""" assert len(hass.states.async_all()) == 4 group_state = hass.states.get("light.group") @@ -1390,12 +1412,10 @@ async def test_verify_group_supported_features( ) -async def test_verify_group_color_mode_fallback( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test that group supported features reflect what included lights support.""" - data = { - "groups": { +@pytest.mark.parametrize( + "group_payload", + [ + { "43": { "action": { "alert": "none", @@ -1443,8 +1463,13 @@ async def test_verify_group_color_mode_fallback( "state": {"all_on": False, "any_on": False}, "type": "LightGroup", }, - }, - "lights": { + } + ], +) +@pytest.mark.parametrize( + "light_payload", + [ + { "13": { "capabilities": { "alerts": [ @@ -1486,11 +1511,14 @@ async def test_verify_group_color_mode_fallback( "type": "Dimmable light", "uniqueid": "00:17:88:01:08:11:22:33-01", }, - }, - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - + } + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_verify_group_color_mode_fallback( + hass: HomeAssistant, mock_deconz_websocket +) -> None: + """Test that group supported features reflect what included lights support.""" group_state = hass.states.get("light.opbergruimte") assert group_state.state == STATE_OFF assert group_state.attributes[ATTR_COLOR_MODE] is None From ece8b749676f2973093219c25a544f0967c9c826 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 4 Jul 2024 14:23:15 +0200 Subject: [PATCH 0735/2411] Use fixtures in deCONZ device trigger tests (#121103) --- .../components/deconz/test_device_trigger.py | 135 ++++++++---------- 1 file changed, 61 insertions(+), 74 deletions(-) diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 5f17da89a4b..9daf34f6665 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -1,6 +1,6 @@ """deCONZ device automation tests.""" -from unittest.mock import Mock, patch +from unittest.mock import Mock import pytest from pytest_unordered import unordered @@ -18,6 +18,7 @@ from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.device_trigger import CONF_SUBTYPE from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, @@ -32,10 +33,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.setup import async_setup_component -from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration - from tests.common import async_get_device_automations -from tests.test_util.aiohttp import AiohttpClientMocker @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -43,15 +41,10 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -async def test_get_triggers( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, -) -> None: - """Test triggers work.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "config": { "alert": "none", @@ -72,10 +65,15 @@ async def test_get_triggers( "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_get_triggers( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test triggers work.""" device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")} ) @@ -149,15 +147,10 @@ async def test_get_triggers( assert triggers == unordered(expected_triggers) -async def test_get_triggers_for_alarm_event( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, -) -> None: - """Test triggers work.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "config": { "battery": 95, @@ -185,10 +178,15 @@ async def test_get_triggers_for_alarm_event( "uniqueid": "00:00:00:00:00:00:00:00-00", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_get_triggers_for_alarm_event( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test triggers work.""" device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:00")} ) @@ -246,14 +244,10 @@ async def test_get_triggers_for_alarm_event( assert triggers == unordered(expected_triggers) -async def test_get_triggers_manage_unsupported_remotes( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - device_registry: dr.DeviceRegistry, -) -> None: - """Verify no triggers for an unsupported remote.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "config": { "alert": "none", @@ -273,10 +267,13 @@ async def test_get_triggers_manage_unsupported_remotes( "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_get_triggers_manage_unsupported_remotes( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Verify no triggers for an unsupported remote.""" device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")} ) @@ -290,17 +287,10 @@ async def test_get_triggers_manage_unsupported_remotes( assert triggers == unordered(expected_triggers) -async def test_functional_device_trigger( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - mock_deconz_websocket, - service_calls: list[ServiceCall], - device_registry: dr.DeviceRegistry, -) -> None: - """Test proper matching and attachment of device trigger automation.""" - - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "config": { "alert": "none", @@ -321,10 +311,16 @@ async def test_functional_device_trigger( "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_functional_device_trigger( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + service_calls: list[ServiceCall], + mock_deconz_websocket, +) -> None: + """Test proper matching and attachment of device trigger automation.""" device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")} ) @@ -368,12 +364,9 @@ async def test_functional_device_trigger( @pytest.mark.skip(reason="Temporarily disabled until automation validation is improved") -async def test_validate_trigger_unknown_device( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@pytest.mark.usefixtures("config_entry_setup") +async def test_validate_trigger_unknown_device(hass: HomeAssistant) -> None: """Test unknown device does not return a trigger config.""" - await setup_deconz_integration(hass, aioclient_mock) - assert await async_setup_component( hass, AUTOMATION_DOMAIN, @@ -402,14 +395,12 @@ async def test_validate_trigger_unknown_device( async def test_validate_trigger_unsupported_device( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, device_registry: dr.DeviceRegistry, + config_entry_setup: ConfigEntry, ) -> None: """Test unsupported device doesn't return a trigger config.""" - config_entry = await setup_deconz_integration(hass, aioclient_mock) - device = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, + config_entry_id=config_entry_setup.entry_id, identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}, model="unsupported", ) @@ -444,14 +435,12 @@ async def test_validate_trigger_unsupported_device( async def test_validate_trigger_unsupported_trigger( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, device_registry: dr.DeviceRegistry, + config_entry_setup: ConfigEntry, ) -> None: """Test unsupported trigger does not return a trigger config.""" - config_entry = await setup_deconz_integration(hass, aioclient_mock) - device = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, + config_entry_id=config_entry_setup.entry_id, identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}, model="TRADFRI on/off switch", ) @@ -488,14 +477,12 @@ async def test_validate_trigger_unsupported_trigger( async def test_attach_trigger_no_matching_event( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, device_registry: dr.DeviceRegistry, + config_entry_setup: ConfigEntry, ) -> None: """Test no matching event for device doesn't return a trigger config.""" - config_entry = await setup_deconz_integration(hass, aioclient_mock) - device = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, + config_entry_id=config_entry_setup.entry_id, identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}, name="Tradfri switch", model="TRADFRI on/off switch", From f78933235d1da6c9e57d9d6b2e0d201e1821e16d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 4 Jul 2024 14:23:40 +0200 Subject: [PATCH 0736/2411] Use fixtures in deCONZ service tests (#121108) * Use fixtures in deCONZ service tests * Update tests/components/deconz/test_services.py Co-authored-by: J. Nick Koston --------- Co-authored-by: J. Nick Koston --- tests/components/deconz/test_services.py | 187 ++++++++++++----------- 1 file changed, 98 insertions(+), 89 deletions(-) diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index de061fc4e8c..2ce3387de15 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -1,6 +1,7 @@ """deCONZ service tests.""" -from unittest.mock import patch +from collections.abc import Callable +from typing import Any import pytest import voluptuous as vol @@ -20,34 +21,29 @@ from homeassistant.components.deconz.services import ( SERVICE_REMOVE_ORPHANED_ENTRIES, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from .test_gateway import ( - BRIDGEID, - DECONZ_WEB_REQUEST, - mock_deconz_put_request, - mock_deconz_request, - setup_deconz_integration, -) +from .test_gateway import BRIDGEID from tests.common import async_capture_events from tests.test_util.aiohttp import AiohttpClientMocker +@pytest.mark.usefixtures("config_entry_setup") async def test_configure_service_with_field( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_put_request: Callable[[str, str], AiohttpClientMocker], ) -> None: """Test that service invokes pydeconz with the correct path and data.""" - config_entry = await setup_deconz_integration(hass, aioclient_mock) - data = { SERVICE_FIELD: "/lights/2", CONF_BRIDGE_ID: BRIDGEID, SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/2") + aioclient_mock = mock_put_request("/lights/2") await hass.services.async_call( DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data, blocking=True @@ -55,12 +51,10 @@ async def test_configure_service_with_field( assert aioclient_mock.mock_calls[1][2] == {"on": True, "attr1": 10, "attr2": 20} -async def test_configure_service_with_entity( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that service invokes pydeconz with the correct path and data.""" - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "1": { "name": "Test", "state": {"reachable": True}, @@ -68,16 +62,19 @@ async def test_configure_service_with_entity( "uniqueid": "00:00:00:00:00:00:00:01-00", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_configure_service_with_entity( + hass: HomeAssistant, + mock_put_request: Callable[[str, str], AiohttpClientMocker], +) -> None: + """Test that service invokes pydeconz with the correct path and data.""" data = { SERVICE_ENTITY: "light.test", SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } - - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1") + aioclient_mock = mock_put_request("/lights/1") await hass.services.async_call( DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data, blocking=True @@ -85,12 +82,10 @@ async def test_configure_service_with_entity( assert aioclient_mock.mock_calls[1][2] == {"on": True, "attr1": 10, "attr2": 20} -async def test_configure_service_with_entity_and_field( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that service invokes pydeconz with the correct path and data.""" - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "1": { "name": "Test", "state": {"reachable": True}, @@ -98,17 +93,20 @@ async def test_configure_service_with_entity_and_field( "uniqueid": "00:00:00:00:00:00:00:01-00", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_configure_service_with_entity_and_field( + hass: HomeAssistant, + mock_put_request: Callable[[str, str], AiohttpClientMocker], +) -> None: + """Test that service invokes pydeconz with the correct path and data.""" data = { SERVICE_ENTITY: "light.test", SERVICE_FIELD: "/state", SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } - - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") + aioclient_mock = mock_put_request("/lights/1/state") await hass.services.async_call( DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data, blocking=True @@ -116,11 +114,11 @@ async def test_configure_service_with_entity_and_field( assert aioclient_mock.mock_calls[1][2] == {"on": True, "attr1": 10, "attr2": 20} +@pytest.mark.usefixtures("config_entry_setup") async def test_configure_service_with_faulty_bridgeid( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that service fails on a bad bridge id.""" - await setup_deconz_integration(hass, aioclient_mock) aioclient_mock.clear_requests() data = { @@ -137,12 +135,9 @@ async def test_configure_service_with_faulty_bridgeid( assert len(aioclient_mock.mock_calls) == 0 -async def test_configure_service_with_faulty_field( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@pytest.mark.usefixtures("config_entry_setup") +async def test_configure_service_with_faulty_field(hass: HomeAssistant) -> None: """Test that service fails on a bad field.""" - await setup_deconz_integration(hass, aioclient_mock) - data = {SERVICE_FIELD: "light/2", SERVICE_DATA: {}} with pytest.raises(vol.Invalid): @@ -151,11 +146,11 @@ async def test_configure_service_with_faulty_field( ) +@pytest.mark.usefixtures("config_entry_setup") async def test_configure_service_with_faulty_entity( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that service on a non existing entity.""" - await setup_deconz_integration(hass, aioclient_mock) aioclient_mock.clear_requests() data = { @@ -171,13 +166,12 @@ async def test_configure_service_with_faulty_entity( assert len(aioclient_mock.mock_calls) == 0 +@pytest.mark.parametrize("config_entry_options", [{CONF_MASTER_GATEWAY: False}]) +@pytest.mark.usefixtures("config_entry_setup") async def test_calling_service_with_no_master_gateway_fails( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that service call fails when no master gateway exist.""" - await setup_deconz_integration( - hass, aioclient_mock, options={CONF_MASTER_GATEWAY: False} - ) aioclient_mock.clear_requests() data = { @@ -193,18 +187,19 @@ async def test_calling_service_with_no_master_gateway_fails( assert len(aioclient_mock.mock_calls) == 0 +@pytest.mark.usefixtures("config_entry_setup") async def test_service_refresh_devices( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + deconz_payload: dict[str, Any], + mock_requests: Callable[[], None], ) -> None: """Test that service can refresh devices.""" - config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 aioclient_mock.clear_requests() - data = { - "config": {}, + deconz_payload |= { "groups": { "1": { "id": "Group 1 id", @@ -234,8 +229,7 @@ async def test_service_refresh_devices( } }, } - - mock_deconz_request(aioclient_mock, config_entry.data, data) + mock_requests() await hass.services.async_call( DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH, service_data={CONF_BRIDGE_ID: BRIDGEID} @@ -245,12 +239,10 @@ async def test_service_refresh_devices( assert len(hass.states.async_all()) == 5 -async def test_service_refresh_devices_trigger_no_state_update( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Verify that gateway.ignore_state_updates are honored.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "name": "Switch 1", "type": "ZHASwitch", @@ -259,18 +251,23 @@ async def test_service_refresh_devices_trigger_no_state_update( "uniqueid": "00:00:00:00:00:00:00:01-00", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_service_refresh_devices_trigger_no_state_update( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + deconz_payload: dict[str, Any], + mock_requests, +) -> None: + """Verify that gateway.ignore_state_updates are honored.""" assert len(hass.states.async_all()) == 1 captured_events = async_capture_events(hass, CONF_DECONZ_EVENT) aioclient_mock.clear_requests() - data = { - "config": {}, + deconz_payload |= { "groups": { "1": { "id": "Group 1 id", @@ -300,8 +297,7 @@ async def test_service_refresh_devices_trigger_no_state_update( } }, } - - mock_deconz_request(aioclient_mock, config_entry.data, data) + mock_requests() await hass.services.async_call( DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH, service_data={CONF_BRIDGE_ID: BRIDGEID} @@ -312,23 +308,23 @@ async def test_service_refresh_devices_trigger_no_state_update( assert len(captured_events) == 0 -async def test_remove_orphaned_entries_service( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, -) -> None: - """Test service works and also don't remove more than expected.""" - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "1": { "name": "Light 1 name", "state": {"reachable": True}, "type": "Light", "uniqueid": "00:00:00:00:00:00:00:01-00", } - }, - "sensors": { + } + ], +) +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "name": "Switch 1", "type": "ZHASwitch", @@ -336,13 +332,18 @@ async def test_remove_orphaned_entries_service( "config": {"battery": 100}, "uniqueid": "00:00:00:00:00:00:00:03-00", }, - }, - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + } + ], +) +async def test_remove_orphaned_entries_service( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + config_entry_setup: ConfigEntry, +) -> None: + """Test service works and also don't remove more than expected.""" device = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, + config_entry_id=config_entry_setup.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "123")}, ) @@ -351,7 +352,7 @@ async def test_remove_orphaned_entries_service( [ entry for entry in device_registry.devices.values() - if config_entry.entry_id in entry.config_entries + if config_entry_setup.entry_id in entry.config_entries ] ) == 5 # Host, gateway, light, switch and orphan @@ -362,12 +363,16 @@ async def test_remove_orphaned_entries_service( DECONZ_DOMAIN, "12345", suggested_object_id="Orphaned sensor", - config_entry=config_entry, + config_entry=config_entry_setup, device_id=device.id, ) assert ( - len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + len( + er.async_entries_for_config_entry( + entity_registry, config_entry_setup.entry_id + ) + ) == 3 # Light, switch battery and orphan ) @@ -383,13 +388,17 @@ async def test_remove_orphaned_entries_service( [ entry for entry in device_registry.devices.values() - if config_entry.entry_id in entry.config_entries + if config_entry_setup.entry_id in entry.config_entries ] ) == 4 # Host, gateway, light and switch ) assert ( - len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + len( + er.async_entries_for_config_entry( + entity_registry, config_entry_setup.entry_id + ) + ) == 2 # Light and switch battery ) From b949240d4a06750dd2716ceb7b7385d9c59963b7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 14:56:01 +0200 Subject: [PATCH 0737/2411] Improve type hints in google tests (#121169) --- tests/components/google/conftest.py | 6 ++--- tests/components/google/test_calendar.py | 8 +++--- tests/components/google/test_config_flow.py | 2 +- tests/components/google/test_init.py | 30 ++++++++++----------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 0f9f2a9395d..791e5613b0b 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -293,7 +293,7 @@ def mock_calendars_list( @pytest.fixture def mock_calendar_get( aioclient_mock: AiohttpClientMocker, -) -> Callable[[...], None]: +) -> Callable[..., None]: """Fixture for returning a calendar get response.""" def _result( @@ -315,7 +315,7 @@ def mock_calendar_get( @pytest.fixture def mock_insert_event( aioclient_mock: AiohttpClientMocker, -) -> Callable[[...], None]: +) -> Callable[..., None]: """Fixture for capturing event creation.""" def _expect_result( @@ -330,7 +330,7 @@ def mock_insert_event( @pytest.fixture(autouse=True) -async def set_time_zone(hass): +async def set_time_zone(hass: HomeAssistant) -> None: """Set the time zone for the tests.""" # Set our timezone to CST/Regina so we can check calculations # This keeps UTC-6 all year round diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 5fe26585fe5..903b68a5cf2 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -839,7 +839,7 @@ async def test_websocket_create( hass: HomeAssistant, component_setup: ComponentSetup, test_api_calendar: dict[str, Any], - mock_insert_event: Callable[[str, dict[str, Any]], None], + mock_insert_event: Callable[..., None], mock_events_list: ApiResult, aioclient_mock: AiohttpClientMocker, ws_client: ClientFixture, @@ -881,7 +881,7 @@ async def test_websocket_create_all_day( hass: HomeAssistant, component_setup: ComponentSetup, test_api_calendar: dict[str, Any], - mock_insert_event: Callable[[str, dict[str, Any]], None], + mock_insert_event: Callable[..., None], mock_events_list: ApiResult, aioclient_mock: AiohttpClientMocker, ws_client: ClientFixture, @@ -1078,7 +1078,7 @@ async def test_readonly_websocket_create( hass: HomeAssistant, component_setup: ComponentSetup, test_api_calendar: dict[str, Any], - mock_insert_event: Callable[[str, dict[str, Any]], None], + mock_insert_event: Callable[..., None], mock_events_list: ApiResult, aioclient_mock: AiohttpClientMocker, ws_client: ClientFixture, @@ -1129,7 +1129,7 @@ async def test_readonly_search_calendar( hass: HomeAssistant, component_setup: ComponentSetup, mock_calendars_yaml, - mock_insert_event: Callable[[str, dict[str, Any]], None], + mock_insert_event: Callable[..., None], mock_events_list: ApiResult, aioclient_mock: AiohttpClientMocker, ws_client: ClientFixture, diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 12281f6d348..47156299b57 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -116,7 +116,7 @@ async def primary_calendar_status() -> HTTPStatus | None: @pytest.fixture(autouse=True) async def primary_calendar( - mock_calendar_get: Callable[[...], None], + mock_calendar_get: Callable[..., None], primary_calendar_error: ClientError | None, primary_calendar_status: HTTPStatus | None, primary_calendar_email: str, diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index de5e2ea9145..cfcda18df3a 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -82,7 +82,7 @@ def assert_state(actual: State | None, expected: State | None) -> None: def add_event_call_service( hass: HomeAssistant, request: pytest.FixtureRequest, -) -> Callable[dict[str, Any], Awaitable[None]]: +) -> Callable[[dict[str, Any]], Awaitable[None]]: """Fixture for calling the add or create event service.""" (domain, service_call, data, target) = request.param @@ -422,7 +422,7 @@ async def test_add_event_invalid_params( mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_events_list: ApiResult, - add_event_call_service: Callable[dict[str, Any], Awaitable[None]], + add_event_call_service: Callable[[dict[str, Any]], Awaitable[None]], date_fields: dict[str, Any], expected_error: type[Exception], error_match: str | None, @@ -457,14 +457,14 @@ async def test_add_event_date_in_x( hass: HomeAssistant, component_setup: ComponentSetup, mock_calendars_list: ApiResult, - mock_insert_event: Callable[[..., dict[str, Any]], None], + mock_insert_event: Callable[..., None], test_api_calendar: dict[str, Any], mock_events_list: ApiResult, date_fields: dict[str, Any], start_timedelta: datetime.timedelta, end_timedelta: datetime.timedelta, aioclient_mock: AiohttpClientMocker, - add_event_call_service: Callable[dict[str, Any], Awaitable[None]], + add_event_call_service: Callable[[dict[str, Any]], Awaitable[None]], ) -> None: """Test service call that adds an event with various time ranges.""" @@ -496,10 +496,10 @@ async def test_add_event_date( component_setup: ComponentSetup, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], - mock_insert_event: Callable[[str, dict[str, Any]], None], + mock_insert_event: Callable[..., None], mock_events_list: ApiResult, aioclient_mock: AiohttpClientMocker, - add_event_call_service: Callable[dict[str, Any], Awaitable[None]], + add_event_call_service: Callable[[dict[str, Any]], Awaitable[None]], ) -> None: """Test service call that sets a date range.""" @@ -535,11 +535,11 @@ async def test_add_event_date_time( hass: HomeAssistant, component_setup: ComponentSetup, mock_calendars_list: ApiResult, - mock_insert_event: Callable[[str, dict[str, Any]], None], + mock_insert_event: Callable[..., None], test_api_calendar: dict[str, Any], mock_events_list: ApiResult, aioclient_mock: AiohttpClientMocker, - add_event_call_service: Callable[dict[str, Any], Awaitable[None]], + add_event_call_service: Callable[[dict[str, Any]], Awaitable[None]], ) -> None: """Test service call that adds an event with a date time range.""" @@ -599,7 +599,7 @@ async def test_unsupported_create_event( mock_calendars_yaml: Mock, component_setup: ComponentSetup, mock_calendars_list: ApiResult, - mock_insert_event: Callable[[str, dict[str, Any]], None], + mock_insert_event: Callable[..., None], test_api_calendar: dict[str, Any], mock_events_list: ApiResult, aioclient_mock: AiohttpClientMocker, @@ -636,8 +636,8 @@ async def test_add_event_failure( mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_events_list: ApiResult, - mock_insert_event: Callable[[..., dict[str, Any]], None], - add_event_call_service: Callable[dict[str, Any], Awaitable[None]], + mock_insert_event: Callable[..., None], + add_event_call_service: Callable[[dict[str, Any]], Awaitable[None]], ) -> None: """Test service calls with incorrect fields.""" @@ -661,10 +661,10 @@ async def test_add_event_location( component_setup: ComponentSetup, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], - mock_insert_event: Callable[[str, dict[str, Any]], None], + mock_insert_event: Callable[..., None], mock_events_list: ApiResult, aioclient_mock: AiohttpClientMocker, - add_event_call_service: Callable[dict[str, Any], Awaitable[None]], + add_event_call_service: Callable[[dict[str, Any]], Awaitable[None]], ) -> None: """Test service call that sets a location field.""" @@ -879,7 +879,7 @@ async def test_assign_unique_id( mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_events_list: ApiResult, - mock_calendar_get: Callable[[...], None], + mock_calendar_get: Callable[..., None], config_entry: MockConfigEntry, ) -> None: """Test an existing config is updated to have unique id if it does not exist.""" @@ -918,7 +918,7 @@ async def test_assign_unique_id_failure( test_api_calendar: dict[str, Any], config_entry: MockConfigEntry, mock_events_list: ApiResult, - mock_calendar_get: Callable[[...], None], + mock_calendar_get: Callable[..., None], request_status: http.HTTPStatus, config_entry_status: ConfigEntryState, ) -> None: From 24c82c24758ff2b129f4c3116d25a1d4affdc01c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 4 Jul 2024 15:48:49 +0200 Subject: [PATCH 0738/2411] Use fixtures in deCONZ sensor tests PT2 (#121204) --- tests/components/deconz/test_sensor.py | 27 ++++++++++++-------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 72dad8a17aa..c3fc84a827a 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -3,7 +3,6 @@ from collections.abc import Callable from datetime import timedelta from typing import Any -from unittest.mock import patch import pytest @@ -26,7 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt as dt_util -from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import setup_deconz_integration from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker @@ -911,23 +910,17 @@ TEST_DATA = [ ] -@pytest.mark.parametrize(("sensor_data", "expected"), TEST_DATA) +@pytest.mark.parametrize(("sensor_1_payload", "expected"), TEST_DATA) +@pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: True}]) async def test_sensors( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, mock_deconz_websocket, - sensor_data, - expected, + expected: dict[str, Any], ) -> None: """Test successful creation of sensor entities.""" - - with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"1": sensor_data}}): - config_entry = await setup_deconz_integration( - hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True} - ) - # Enable in entity registry if expected.get("enable_entity"): entity_registry.async_update_entity( @@ -960,7 +953,11 @@ async def test_sensors( # Verify device registry assert ( - len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + len( + dr.async_entries_for_config_entry( + device_registry, config_entry_setup.entry_id + ) + ) == expected["device_count"] ) @@ -974,12 +971,12 @@ async def test_sensors( # Unload entry - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE # Remove entry - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 From d12f2384c57d2cbfd8244ab4dc8da1a7e1c3bf9e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 4 Jul 2024 15:53:25 +0200 Subject: [PATCH 0739/2411] Use fixtures in deCONZ config flow tests PT2 (#121203) * Use fixtures in deCONZ config flow tests PT2 * Update tests/components/deconz/test_config_flow.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- tests/components/deconz/test_config_flow.py | 28 +++++++++++---------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index fb1239dba61..e9e452e6d73 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -33,7 +33,7 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONTENT_TYPE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .test_gateway import API_KEY, BRIDGEID, setup_deconz_integration +from .test_gateway import API_KEY, BRIDGEID from tests.test_util.aiohttp import AiohttpClientMocker @@ -223,11 +223,11 @@ async def test_manual_configuration_after_discovery_ResponseError( async def test_manual_configuration_update_configuration( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, ) -> None: """Test that manual configuration can update existing config entry.""" - config_entry = await setup_deconz_integration(hass, aioclient_mock) - aioclient_mock.get( pydeconz.utils.URL_DISCOVER, json=[], @@ -267,15 +267,14 @@ async def test_manual_configuration_update_configuration( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert config_entry.data[CONF_HOST] == "2.3.4.5" + assert config_entry_setup.data[CONF_HOST] == "2.3.4.5" +@pytest.mark.usefixtures("config_entry_setup") async def test_manual_configuration_dont_update_configuration( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that _create_entry work and that bridgeid can be requested.""" - await setup_deconz_integration(hass, aioclient_mock) - aioclient_mock.get( pydeconz.utils.URL_DISCOVER, json=[], @@ -368,7 +367,10 @@ async def test_manual_configuration_timeout_get_bridge( ], ) async def test_link_step_fails( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, raised_error, error_string + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + raised_error: Exception, + error_string: str, ) -> None: """Test config flow should abort if no API key was possible to retrieve.""" aioclient_mock.get( @@ -400,14 +402,14 @@ async def test_link_step_fails( async def test_reauth_flow_update_configuration( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + config_entry_setup: ConfigEntry, ) -> None: """Verify reauth flow can update gateway API key.""" - config_entry = await setup_deconz_integration(hass, aioclient_mock) - result = await hass.config_entries.flow.async_init( DECONZ_DOMAIN, - data=config_entry.data, + data=config_entry_setup.data, context={"source": SOURCE_REAUTH}, ) @@ -434,7 +436,7 @@ async def test_reauth_flow_update_configuration( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert config_entry.data[CONF_API_KEY] == new_api_key + assert config_entry_setup.data[CONF_API_KEY] == new_api_key async def test_flow_ssdp_discovery( From 9a1f7f020c2710ad2a16d222beab7ac93e7428f6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 16:17:18 +0200 Subject: [PATCH 0740/2411] Add type hints to matrix events fixtures (#121213) --- tests/components/matrix/conftest.py | 6 +++--- tests/components/matrix/test_send_message.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/matrix/conftest.py b/tests/components/matrix/conftest.py index 11347302177..0b84aff5434 100644 --- a/tests/components/matrix/conftest.py +++ b/tests/components/matrix/conftest.py @@ -48,7 +48,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.setup import async_setup_component from tests.common import async_capture_events @@ -294,13 +294,13 @@ async def matrix_bot( @pytest.fixture -def matrix_events(hass: HomeAssistant): +def matrix_events(hass: HomeAssistant) -> list[Event]: """Track event calls.""" return async_capture_events(hass, MATRIX_DOMAIN) @pytest.fixture -def command_events(hass: HomeAssistant): +def command_events(hass: HomeAssistant) -> list[Event]: """Track event calls.""" return async_capture_events(hass, EVENT_MATRIX_COMMAND) diff --git a/tests/components/matrix/test_send_message.py b/tests/components/matrix/test_send_message.py index cdea2270cf9..3db2877e789 100644 --- a/tests/components/matrix/test_send_message.py +++ b/tests/components/matrix/test_send_message.py @@ -10,7 +10,7 @@ from homeassistant.components.matrix import ( ) from homeassistant.components.matrix.const import FORMAT_HTML, SERVICE_SEND_MESSAGE from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from .conftest import TEST_BAD_ROOM, TEST_JOINABLE_ROOMS @@ -19,7 +19,7 @@ async def test_send_message( hass: HomeAssistant, matrix_bot: MatrixBot, image_path, - matrix_events, + matrix_events: list[Event], caplog: pytest.LogCaptureFixture, ) -> None: """Test the send_message service.""" @@ -63,7 +63,7 @@ async def test_send_message( async def test_unsendable_message( hass: HomeAssistant, matrix_bot: MatrixBot, - matrix_events, + matrix_events: list[Event], caplog: pytest.LogCaptureFixture, ) -> None: """Test the send_message service with an invalid room.""" From 411630429d2e3e003867551e0d57bb5c538d27ee Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 16:27:57 +0200 Subject: [PATCH 0741/2411] Improve type hints in habitica tests (#121212) --- tests/components/habitica/test_init.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py index 24c55c473b9..5dbff3b71e8 100644 --- a/tests/components/habitica/test_init.py +++ b/tests/components/habitica/test_init.py @@ -14,7 +14,7 @@ from homeassistant.components.habitica.const import ( SERVICE_API_CALL, ) from homeassistant.const import ATTR_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from tests.common import MockConfigEntry, async_capture_events from tests.test_util.aiohttp import AiohttpClientMocker @@ -24,13 +24,13 @@ TEST_USER_NAME = "test_user" @pytest.fixture -def capture_api_call_success(hass): +def capture_api_call_success(hass: HomeAssistant) -> list[Event]: """Capture api_call events.""" return async_capture_events(hass, EVENT_API_CALL_SUCCESS) @pytest.fixture -def habitica_entry(hass): +def habitica_entry(hass: HomeAssistant) -> MockConfigEntry: """Test entry for the following tests.""" entry = MockConfigEntry( domain=DOMAIN, @@ -98,8 +98,9 @@ def common_requests(aioclient_mock: AiohttpClientMocker) -> AiohttpClientMocker: return aioclient_mock +@pytest.mark.usefixtures("common_requests") async def test_entry_setup_unload( - hass: HomeAssistant, habitica_entry, common_requests + hass: HomeAssistant, habitica_entry: MockConfigEntry ) -> None: """Test integration setup and unload.""" assert await hass.config_entries.async_setup(habitica_entry.entry_id) @@ -112,8 +113,11 @@ async def test_entry_setup_unload( assert not hass.services.has_service(DOMAIN, SERVICE_API_CALL) +@pytest.mark.usefixtures("common_requests") async def test_service_call( - hass: HomeAssistant, habitica_entry, common_requests, capture_api_call_success + hass: HomeAssistant, + habitica_entry: MockConfigEntry, + capture_api_call_success: list[Event], ) -> None: """Test integration setup, service call and unload.""" From 28f06cb5a00910a43d37e899f2d24de01c1f8d2c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 16:32:01 +0200 Subject: [PATCH 0742/2411] Add type hints to homekit events fixture (#121211) --- tests/components/homekit/conftest.py | 4 +- tests/components/homekit/test_accessories.py | 6 +- tests/components/homekit/test_diagnostics.py | 4 +- tests/components/homekit/test_type_cameras.py | 36 +++++------ tests/components/homekit/test_type_covers.py | 32 +++++----- tests/components/homekit/test_type_fans.py | 26 +++++--- .../homekit/test_type_humidifiers.py | 24 +++++--- tests/components/homekit/test_type_lights.py | 40 ++++++------- tests/components/homekit/test_type_locks.py | 8 ++- .../homekit/test_type_media_players.py | 23 ++++--- tests/components/homekit/test_type_remote.py | 9 ++- .../homekit/test_type_security_systems.py | 14 +++-- tests/components/homekit/test_type_sensors.py | 2 +- .../components/homekit/test_type_switches.py | 34 +++++++---- .../homekit/test_type_thermostats.py | 60 +++++++++++-------- .../components/homekit/test_type_triggers.py | 4 +- 16 files changed, 182 insertions(+), 144 deletions(-) diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 7acb11e10ab..6bdad5d2b4c 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -12,7 +12,7 @@ from homeassistant.components.device_tracker.legacy import YAML_DEVICES from homeassistant.components.homekit.accessories import HomeDriver from homeassistant.components.homekit.const import BRIDGE_NAME, EVENT_HOMEKIT_CHANGED from homeassistant.components.homekit.iidmanager import AccessoryIIDStorage -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from tests.common import async_capture_events @@ -121,7 +121,7 @@ def mock_hap( @pytest.fixture -def events(hass): +def events(hass: HomeAssistant) -> list[Event]: """Yield caught homekit_changed events.""" return async_capture_events(hass, EVENT_HOMEKIT_CHANGED) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 32cd6622492..c37cac84b8a 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -47,7 +47,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, __version__ as hass_version, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from tests.common import async_mock_service @@ -667,7 +667,9 @@ async def test_battery_appears_after_startup( assert acc._char_battery is None -async def test_call_service(hass: HomeAssistant, hk_driver, events) -> None: +async def test_call_service( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test call_service method.""" entity_id = "homekit.accessory" hass.states.async_set(entity_id, None) diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index 728624da0d0..ce3c954c447 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -12,7 +12,7 @@ from homeassistant.components.homekit.const import ( ) from homeassistant.const import CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from .util import async_init_integration @@ -321,9 +321,7 @@ async def test_config_entry_with_trigger_accessory( hass: HomeAssistant, hass_client: ClientSessionGenerator, hk_driver, - events, demo_cleanup, - device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: """Test generating diagnostics for a bridge config entry with a trigger accessory.""" diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index fd5d1835641..69f76006163 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -132,9 +132,7 @@ def _get_failing_mock_ffmpeg(): return ffmpeg -async def test_camera_stream_source_configured( - hass: HomeAssistant, run_driver, events -) -> None: +async def test_camera_stream_source_configured(hass: HomeAssistant, run_driver) -> None: """Test a camera that can stream with a configured source.""" await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) await async_setup_component( @@ -254,7 +252,7 @@ async def test_camera_stream_source_configured( async def test_camera_stream_source_configured_with_failing_ffmpeg( - hass: HomeAssistant, run_driver, events + hass: HomeAssistant, run_driver ) -> None: """Test a camera that can stream with a configured source with ffmpeg failing.""" await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) @@ -310,9 +308,7 @@ async def test_camera_stream_source_configured_with_failing_ffmpeg( await _async_stop_all_streams(hass, acc) -async def test_camera_stream_source_found( - hass: HomeAssistant, run_driver, events -) -> None: +async def test_camera_stream_source_found(hass: HomeAssistant, run_driver) -> None: """Test a camera that can stream and we get the source from the entity.""" await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) await async_setup_component( @@ -398,9 +394,7 @@ async def test_camera_stream_source_found( ) -async def test_camera_stream_source_fails( - hass: HomeAssistant, run_driver, events -) -> None: +async def test_camera_stream_source_fails(hass: HomeAssistant, run_driver) -> None: """Test a camera that can stream and we cannot get the source from the entity.""" await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) await async_setup_component( @@ -441,7 +435,7 @@ async def test_camera_stream_source_fails( await _async_stop_all_streams(hass, acc) -async def test_camera_with_no_stream(hass: HomeAssistant, run_driver, events) -> None: +async def test_camera_with_no_stream(hass: HomeAssistant, run_driver) -> None: """Test a camera that cannot stream.""" await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) await async_setup_component(hass, camera.DOMAIN, {camera.DOMAIN: {}}) @@ -474,7 +468,7 @@ async def test_camera_with_no_stream(hass: HomeAssistant, run_driver, events) -> async def test_camera_stream_source_configured_and_copy_codec( - hass: HomeAssistant, run_driver, events + hass: HomeAssistant, run_driver ) -> None: """Test a camera that can stream with a configured source.""" await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) @@ -549,7 +543,7 @@ async def test_camera_stream_source_configured_and_copy_codec( async def test_camera_stream_source_configured_and_override_profile_names( - hass: HomeAssistant, run_driver, events + hass: HomeAssistant, run_driver ) -> None: """Test a camera that can stream with a configured source over overridden profile names.""" await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) @@ -625,7 +619,7 @@ async def test_camera_stream_source_configured_and_override_profile_names( async def test_camera_streaming_fails_after_starting_ffmpeg( - hass: HomeAssistant, run_driver, events + hass: HomeAssistant, run_driver ) -> None: """Test a camera that can stream with a configured source.""" await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) @@ -702,7 +696,7 @@ async def test_camera_streaming_fails_after_starting_ffmpeg( async def test_camera_with_linked_motion_sensor( - hass: HomeAssistant, run_driver, events + hass: HomeAssistant, run_driver ) -> None: """Test a camera with a linked motion sensor can update.""" await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) @@ -795,9 +789,7 @@ async def test_camera_with_linked_motion_sensor( assert char.value is True -async def test_camera_with_linked_motion_event( - hass: HomeAssistant, run_driver, events -) -> None: +async def test_camera_with_linked_motion_event(hass: HomeAssistant, run_driver) -> None: """Test a camera with a linked motion event entity can update.""" await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) await async_setup_component( @@ -893,7 +885,7 @@ async def test_camera_with_linked_motion_event( async def test_camera_with_a_missing_linked_motion_sensor( - hass: HomeAssistant, run_driver, events + hass: HomeAssistant, run_driver ) -> None: """Test a camera with a configured linked motion sensor that is missing.""" await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) @@ -925,7 +917,7 @@ async def test_camera_with_a_missing_linked_motion_sensor( async def test_camera_with_linked_doorbell_sensor( - hass: HomeAssistant, run_driver, events + hass: HomeAssistant, run_driver ) -> None: """Test a camera with a linked doorbell sensor can update.""" await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) @@ -1041,7 +1033,7 @@ async def test_camera_with_linked_doorbell_sensor( async def test_camera_with_linked_doorbell_event( - hass: HomeAssistant, run_driver, events + hass: HomeAssistant, run_driver ) -> None: """Test a camera with a linked doorbell event can update.""" await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) @@ -1158,7 +1150,7 @@ async def test_camera_with_linked_doorbell_event( async def test_camera_with_a_missing_linked_doorbell_sensor( - hass: HomeAssistant, run_driver, events + hass: HomeAssistant, run_driver ) -> None: """Test a camera with a configured linked doorbell sensor that is missing.""" await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 6efd9118092..b3125c6581c 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -40,13 +40,15 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import CoreState, HomeAssistant +from homeassistant.core import CoreState, Event, HomeAssistant from homeassistant.helpers import entity_registry as er from tests.common import async_mock_service -async def test_garage_door_open_close(hass: HomeAssistant, hk_driver, events) -> None: +async def test_garage_door_open_close( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if accessory and HA are updated accordingly.""" entity_id = "cover.garage_door" @@ -132,9 +134,7 @@ async def test_garage_door_open_close(hass: HomeAssistant, hk_driver, events) -> assert events[-1].data[ATTR_VALUE] is None -async def test_door_instantiate_set_position( - hass: HomeAssistant, hk_driver, events -) -> None: +async def test_door_instantiate_set_position(hass: HomeAssistant, hk_driver) -> None: """Test if Door accessory is instantiated correctly and can set position.""" entity_id = "cover.door" @@ -185,7 +185,7 @@ async def test_door_instantiate_set_position( async def test_windowcovering_set_cover_position( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver, events: list[Event] ) -> None: """Test if accessory and HA are updated accordingly.""" entity_id = "cover.window" @@ -295,9 +295,7 @@ async def test_windowcovering_set_cover_position( assert events[-1].data[ATTR_VALUE] == 75 -async def test_window_instantiate_set_position( - hass: HomeAssistant, hk_driver, events -) -> None: +async def test_window_instantiate_set_position(hass: HomeAssistant, hk_driver) -> None: """Test if Window accessory is instantiated correctly and can set position.""" entity_id = "cover.window" @@ -348,7 +346,7 @@ async def test_window_instantiate_set_position( async def test_windowcovering_cover_set_tilt( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver, events: list[Event] ) -> None: """Test if accessory and HA update slat tilt accordingly.""" entity_id = "cover.window" @@ -418,7 +416,7 @@ async def test_windowcovering_cover_set_tilt( assert events[-1].data[ATTR_VALUE] == 75 -async def test_windowcovering_tilt_only(hass: HomeAssistant, hk_driver, events) -> None: +async def test_windowcovering_tilt_only(hass: HomeAssistant, hk_driver) -> None: """Test we lock the window covering closed when its tilt only.""" entity_id = "cover.window" @@ -442,7 +440,7 @@ async def test_windowcovering_tilt_only(hass: HomeAssistant, hk_driver, events) async def test_windowcovering_open_close( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver, events: list[Event] ) -> None: """Test if accessory and HA are updated accordingly.""" entity_id = "cover.window" @@ -525,7 +523,7 @@ async def test_windowcovering_open_close( async def test_windowcovering_open_close_stop( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver, events: list[Event] ) -> None: """Test if accessory and HA are updated accordingly.""" entity_id = "cover.window" @@ -574,7 +572,7 @@ async def test_windowcovering_open_close_stop( async def test_windowcovering_open_close_with_position_and_stop( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver, events: list[Event] ) -> None: """Test if accessory and HA are updated accordingly.""" entity_id = "cover.stop_window" @@ -608,7 +606,7 @@ async def test_windowcovering_open_close_with_position_and_stop( async def test_windowcovering_basic_restore( - hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver, events + hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver ) -> None: """Test setting up an entity from state in the event registry.""" hass.set_state(CoreState.not_running) @@ -646,7 +644,7 @@ async def test_windowcovering_basic_restore( async def test_windowcovering_restore( - hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver, events + hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver ) -> None: """Test setting up an entity from state in the event entity_registry.""" hass.set_state(CoreState.not_running) @@ -684,7 +682,7 @@ async def test_windowcovering_restore( async def test_garage_door_with_linked_obstruction_sensor( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver ) -> None: """Test if accessory and HA are updated accordingly with a linked obstruction sensor.""" linked_obstruction_sensor_entity_id = "binary_sensor.obstruction" diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index d971b8c06d2..1808767c614 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -24,13 +24,13 @@ from homeassistant.const import ( STATE_ON, STATE_UNKNOWN, ) -from homeassistant.core import CoreState, HomeAssistant +from homeassistant.core import CoreState, Event, HomeAssistant from homeassistant.helpers import entity_registry as er from tests.common import async_mock_service -async def test_fan_basic(hass: HomeAssistant, hk_driver, events) -> None: +async def test_fan_basic(hass: HomeAssistant, hk_driver, events: list[Event]) -> None: """Test fan with char state.""" entity_id = "fan.demo" @@ -108,7 +108,9 @@ async def test_fan_basic(hass: HomeAssistant, hk_driver, events) -> None: assert events[-1].data[ATTR_VALUE] is None -async def test_fan_direction(hass: HomeAssistant, hk_driver, events) -> None: +async def test_fan_direction( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test fan with direction.""" entity_id = "fan.demo" @@ -186,7 +188,9 @@ async def test_fan_direction(hass: HomeAssistant, hk_driver, events) -> None: assert events[-1].data[ATTR_VALUE] == DIRECTION_REVERSE -async def test_fan_oscillate(hass: HomeAssistant, hk_driver, events) -> None: +async def test_fan_oscillate( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test fan with oscillate.""" entity_id = "fan.demo" @@ -259,7 +263,7 @@ async def test_fan_oscillate(hass: HomeAssistant, hk_driver, events) -> None: assert events[-1].data[ATTR_VALUE] is True -async def test_fan_speed(hass: HomeAssistant, hk_driver, events) -> None: +async def test_fan_speed(hass: HomeAssistant, hk_driver, events: list[Event]) -> None: """Test fan with speed.""" entity_id = "fan.demo" @@ -361,7 +365,9 @@ async def test_fan_speed(hass: HomeAssistant, hk_driver, events) -> None: assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id -async def test_fan_set_all_one_shot(hass: HomeAssistant, hk_driver, events) -> None: +async def test_fan_set_all_one_shot( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test fan with speed.""" entity_id = "fan.demo" @@ -555,7 +561,7 @@ async def test_fan_set_all_one_shot(hass: HomeAssistant, hk_driver, events) -> N async def test_fan_restore( - hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver, events + hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver ) -> None: """Test setting up an entity from state in the event registry.""" hass.set_state(CoreState.not_running) @@ -597,7 +603,7 @@ async def test_fan_restore( async def test_fan_multiple_preset_modes( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver, events: list[Event] ) -> None: """Test fan with multiple preset modes.""" entity_id = "fan.demo" @@ -678,7 +684,9 @@ async def test_fan_multiple_preset_modes( assert len(events) == 2 -async def test_fan_single_preset_mode(hass: HomeAssistant, hk_driver, events) -> None: +async def test_fan_single_preset_mode( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test fan with a single preset mode.""" entity_id = "fan.demo" diff --git a/tests/components/homekit/test_type_humidifiers.py b/tests/components/homekit/test_type_humidifiers.py index fdd01e05a91..fbb72333c9b 100644 --- a/tests/components/homekit/test_type_humidifiers.py +++ b/tests/components/homekit/test_type_humidifiers.py @@ -42,12 +42,12 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from tests.common import async_mock_service -async def test_humidifier(hass: HomeAssistant, hk_driver, events) -> None: +async def test_humidifier(hass: HomeAssistant, hk_driver, events: list[Event]) -> None: """Test if humidifier accessory and HA are updated accordingly.""" entity_id = "humidifier.test" @@ -132,7 +132,9 @@ async def test_humidifier(hass: HomeAssistant, hk_driver, events) -> None: assert events[-1].data[ATTR_VALUE] == "RelativeHumidityHumidifierThreshold to 39.0%" -async def test_dehumidifier(hass: HomeAssistant, hk_driver, events) -> None: +async def test_dehumidifier( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if dehumidifier accessory and HA are updated accordingly.""" entity_id = "humidifier.test" @@ -220,7 +222,9 @@ async def test_dehumidifier(hass: HomeAssistant, hk_driver, events) -> None: ) -async def test_hygrostat_power_state(hass: HomeAssistant, hk_driver, events) -> None: +async def test_hygrostat_power_state( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if accessory and HA are updated accordingly.""" entity_id = "humidifier.test" @@ -301,7 +305,7 @@ async def test_hygrostat_power_state(hass: HomeAssistant, hk_driver, events) -> async def test_hygrostat_get_humidity_range( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver, events: list[Event] ) -> None: """Test if humidity range is evaluated correctly.""" entity_id = "humidifier.test" @@ -452,7 +456,10 @@ async def test_humidifier_with_a_missing_linked_humidity_sensor( async def test_humidifier_as_dehumidifier( - hass: HomeAssistant, hk_driver, events, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + hk_driver, + events: list[Event], + caplog: pytest.LogCaptureFixture, ) -> None: """Test an invalid char_target_humidifier_dehumidifier from HomeKit.""" entity_id = "humidifier.test" @@ -495,7 +502,10 @@ async def test_humidifier_as_dehumidifier( async def test_dehumidifier_as_humidifier( - hass: HomeAssistant, hk_driver, events, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + hk_driver, + events: list[Event], + caplog: pytest.LogCaptureFixture, ) -> None: """Test an invalid char_target_humidifier_dehumidifier from HomeKit.""" entity_id = "humidifier.test" diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 8d2978fb0bd..02532a91e6d 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -39,7 +39,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNKNOWN, ) -from homeassistant.core import CoreState, HomeAssistant +from homeassistant.core import CoreState, Event, HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util @@ -53,7 +53,7 @@ async def _wait_for_light_coalesce(hass): await hass.async_block_till_done() -async def test_light_basic(hass: HomeAssistant, hk_driver, events) -> None: +async def test_light_basic(hass: HomeAssistant, hk_driver, events: list[Event]) -> None: """Test light with char state.""" entity_id = "light.demo" @@ -127,7 +127,7 @@ async def test_light_basic(hass: HomeAssistant, hk_driver, events) -> None: [[ColorMode.BRIGHTNESS], [ColorMode.HS], [ColorMode.COLOR_TEMP]], ) async def test_light_brightness( - hass: HomeAssistant, hk_driver, events, supported_color_modes + hass: HomeAssistant, hk_driver, events: list[Event], supported_color_modes ) -> None: """Test light with brightness.""" entity_id = "light.demo" @@ -274,7 +274,9 @@ async def test_light_brightness( assert acc.char_brightness.value == 1 -async def test_light_color_temperature(hass: HomeAssistant, hk_driver, events) -> None: +async def test_light_color_temperature( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test light with color temperature.""" entity_id = "light.demo" @@ -323,7 +325,7 @@ async def test_light_color_temperature(hass: HomeAssistant, hk_driver, events) - [["color_temp", "hs"], ["color_temp", "rgb"], ["color_temp", "xy"]], ) async def test_light_color_temperature_and_rgb_color( - hass: HomeAssistant, hk_driver, events, supported_color_modes + hass: HomeAssistant, hk_driver, events: list[Event], supported_color_modes ) -> None: """Test light with color temperature and rgb color not exposing temperature.""" entity_id = "light.demo" @@ -524,7 +526,7 @@ async def test_light_color_temperature_and_rgb_color( "supported_color_modes", [[ColorMode.HS], [ColorMode.RGB], [ColorMode.XY]] ) async def test_light_rgb_color( - hass: HomeAssistant, hk_driver, events, supported_color_modes + hass: HomeAssistant, hk_driver, events: list[Event], supported_color_modes ) -> None: """Test light with rgb_color.""" entity_id = "light.demo" @@ -578,7 +580,7 @@ async def test_light_rgb_color( async def test_light_restore( - hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver, events + hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver ) -> None: """Test setting up an entity from state in the event registry.""" hass.set_state(CoreState.not_running) @@ -642,7 +644,7 @@ async def test_light_restore( async def test_light_rgb_with_color_temp( hass: HomeAssistant, hk_driver, - events, + events: list[Event], supported_color_modes, state_props, turn_on_props_with_brightness, @@ -762,7 +764,7 @@ async def test_light_rgb_with_color_temp( async def test_light_rgbwx_with_color_temp_and_brightness( hass: HomeAssistant, hk_driver, - events, + events: list[Event], supported_color_modes, state_props, turn_on_props_with_brightness, @@ -824,7 +826,7 @@ async def test_light_rgbwx_with_color_temp_and_brightness( async def test_light_rgb_or_w_lights( hass: HomeAssistant, hk_driver, - events, + events: list[Event], ) -> None: """Test lights with RGB or W lights.""" entity_id = "light.demo" @@ -957,7 +959,7 @@ async def test_light_rgb_or_w_lights( async def test_light_rgb_with_white_switch_to_temp( hass: HomeAssistant, hk_driver, - events, + events: list[Event], supported_color_modes, state_props, ) -> None: @@ -1034,11 +1036,7 @@ async def test_light_rgb_with_white_switch_to_temp( assert acc.char_brightness.value == 100 -async def test_light_rgb_with_hs_color_none( - hass: HomeAssistant, - hk_driver, - events, -) -> None: +async def test_light_rgb_with_hs_color_none(hass: HomeAssistant, hk_driver) -> None: """Test lights hs color set to None.""" entity_id = "light.demo" @@ -1071,7 +1069,7 @@ async def test_light_rgb_with_hs_color_none( async def test_light_rgbww_with_color_temp_conversion( hass: HomeAssistant, hk_driver, - events, + events: list[Event], ) -> None: """Test lights with RGBWW convert color temp as expected.""" entity_id = "light.demo" @@ -1192,7 +1190,7 @@ async def test_light_rgbww_with_color_temp_conversion( async def test_light_rgbw_with_color_temp_conversion( hass: HomeAssistant, hk_driver, - events, + events: list[Event], ) -> None: """Test lights with RGBW convert color temp as expected.""" entity_id = "light.demo" @@ -1280,7 +1278,7 @@ async def test_light_rgbw_with_color_temp_conversion( async def test_light_set_brightness_and_color( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver, events: list[Event] ) -> None: """Test light with all chars in one go.""" entity_id = "light.demo" @@ -1365,7 +1363,7 @@ async def test_light_set_brightness_and_color( ) -async def test_light_min_max_mireds(hass: HomeAssistant, hk_driver, events) -> None: +async def test_light_min_max_mireds(hass: HomeAssistant, hk_driver) -> None: """Test mireds are forced to ints.""" entity_id = "light.demo" @@ -1386,7 +1384,7 @@ async def test_light_min_max_mireds(hass: HomeAssistant, hk_driver, events) -> N async def test_light_set_brightness_and_color_temp( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver, events: list[Event] ) -> None: """Test light with all chars in one go.""" entity_id = "light.demo" diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 4d83fe41f48..31f03b1964f 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -18,12 +18,12 @@ from homeassistant.const import ( STATE_UNKNOWN, STATE_UNLOCKED, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from tests.common import async_mock_service -async def test_lock_unlock(hass: HomeAssistant, hk_driver, events) -> None: +async def test_lock_unlock(hass: HomeAssistant, hk_driver, events: list[Event]) -> None: """Test if accessory and HA are updated accordingly.""" code = "1234" config = {ATTR_CODE: code} @@ -121,7 +121,9 @@ async def test_lock_unlock(hass: HomeAssistant, hk_driver, events) -> None: @pytest.mark.parametrize("config", [{}, {ATTR_CODE: None}]) -async def test_no_code(hass: HomeAssistant, hk_driver, config, events) -> None: +async def test_no_code( + hass: HomeAssistant, hk_driver, config, events: list[Event] +) -> None: """Test accessory if lock doesn't require a code.""" entity_id = "lock.kitchen_door" diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index fb7233e5262..14c21f0a5f5 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -40,13 +40,15 @@ from homeassistant.const import ( STATE_PLAYING, STATE_STANDBY, ) -from homeassistant.core import CoreState, HomeAssistant +from homeassistant.core import CoreState, Event, HomeAssistant from homeassistant.helpers import entity_registry as er from tests.common import async_mock_service -async def test_media_player_set_state(hass: HomeAssistant, hk_driver, events) -> None: +async def test_media_player_set_state( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if accessory and HA are updated accordingly.""" config = { CONF_FEATURE_LIST: { @@ -177,7 +179,10 @@ async def test_media_player_set_state(hass: HomeAssistant, hk_driver, events) -> async def test_media_player_television( - hass: HomeAssistant, hk_driver, events, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + hk_driver, + events: list[Event], + caplog: pytest.LogCaptureFixture, ) -> None: """Test if television accessory and HA are updated accordingly.""" entity_id = "media_player.television" @@ -366,7 +371,7 @@ async def test_media_player_television( async def test_media_player_television_basic( - hass: HomeAssistant, hk_driver, events, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, hk_driver, caplog: pytest.LogCaptureFixture ) -> None: """Test if basic television accessory and HA are updated accordingly.""" entity_id = "media_player.television" @@ -409,7 +414,7 @@ async def test_media_player_television_basic( async def test_media_player_television_supports_source_select_no_sources( - hass: HomeAssistant, hk_driver, events, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, hk_driver ) -> None: """Test if basic tv that supports source select but is missing a source list.""" entity_id = "media_player.television" @@ -429,7 +434,7 @@ async def test_media_player_television_supports_source_select_no_sources( async def test_tv_restore( - hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver, events + hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver ) -> None: """Test setting up an entity from state in the event registry.""" hass.set_state(CoreState.not_running) @@ -482,7 +487,7 @@ async def test_tv_restore( async def test_media_player_television_max_sources( - hass: HomeAssistant, hk_driver, events, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, hk_driver ) -> None: """Test if television accessory that reaches the maximum number of sources.""" entity_id = "media_player.television" @@ -541,7 +546,7 @@ async def test_media_player_television_max_sources( async def test_media_player_television_duplicate_sources( - hass: HomeAssistant, hk_driver, events, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, hk_driver ) -> None: """Test if television accessory with duplicate sources.""" entity_id = "media_player.television" @@ -586,7 +591,7 @@ async def test_media_player_television_duplicate_sources( async def test_media_player_television_unsafe_chars( - hass: HomeAssistant, hk_driver, events, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, hk_driver, events: list[Event] ) -> None: """Test if television accessory with unsafe characters.""" entity_id = "media_player.television" diff --git a/tests/components/homekit/test_type_remote.py b/tests/components/homekit/test_type_remote.py index bd4ead58a7b..dedf3ae34db 100644 --- a/tests/components/homekit/test_type_remote.py +++ b/tests/components/homekit/test_type_remote.py @@ -26,13 +26,13 @@ from homeassistant.const import ( STATE_ON, STATE_STANDBY, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from tests.common import async_mock_service async def test_activity_remote( - hass: HomeAssistant, hk_driver: HomeDriver, events, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, hk_driver: HomeDriver, events: list[Event] ) -> None: """Test if remote accessory and HA are updated accordingly.""" entity_id = "remote.harmony" @@ -156,7 +156,10 @@ async def test_activity_remote( async def test_activity_remote_bad_names( - hass: HomeAssistant, hk_driver, events, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + hk_driver, + events: list[Event], + caplog: pytest.LogCaptureFixture, ) -> None: """Test if remote accessory with invalid names works as expected.""" entity_id = "remote.harmony" diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 18434a345ce..27580949ec2 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -21,12 +21,14 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from tests.common import async_mock_service -async def test_switch_set_state(hass: HomeAssistant, hk_driver, events) -> None: +async def test_switch_set_state( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if accessory and HA are updated accordingly.""" code = "1234" config = {ATTR_CODE: code} @@ -118,7 +120,9 @@ async def test_switch_set_state(hass: HomeAssistant, hk_driver, events) -> None: @pytest.mark.parametrize("config", [{}, {ATTR_CODE: None}]) -async def test_no_alarm_code(hass: HomeAssistant, hk_driver, config, events) -> None: +async def test_no_alarm_code( + hass: HomeAssistant, hk_driver, config, events: list[Event] +) -> None: """Test accessory if security_system doesn't require an alarm_code.""" entity_id = "alarm_control_panel.test" @@ -139,7 +143,7 @@ async def test_no_alarm_code(hass: HomeAssistant, hk_driver, config, events) -> assert events[-1].data[ATTR_VALUE] is None -async def test_arming(hass: HomeAssistant, hk_driver, events) -> None: +async def test_arming(hass: HomeAssistant, hk_driver) -> None: """Test to make sure arming sets the right state.""" entity_id = "alarm_control_panel.test" @@ -190,7 +194,7 @@ async def test_arming(hass: HomeAssistant, hk_driver, events) -> None: assert acc.char_current_state.value == 4 -async def test_supported_states(hass: HomeAssistant, hk_driver, events) -> None: +async def test_supported_states(hass: HomeAssistant, hk_driver) -> None: """Test different supported states.""" code = "1234" config = {ATTR_CODE: code} diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index fc68b7c8ecf..3b26ec8d36e 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -601,7 +601,7 @@ async def test_binary_device_classes(hass: HomeAssistant, hk_driver) -> None: async def test_sensor_restore( - hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver, events + hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver ) -> None: """Test setting up an entity from state in the event registry.""" hass.set_state(CoreState.not_running) diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index a2c88d7e1ab..9b708f18b8a 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -42,13 +42,15 @@ from homeassistant.const import ( STATE_ON, STATE_OPEN, ) -from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.core import Event, HomeAssistant, split_entity_id import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, async_mock_service -async def test_outlet_set_state(hass: HomeAssistant, hk_driver, events) -> None: +async def test_outlet_set_state( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if Outlet accessory and HA are updated accordingly.""" entity_id = "switch.outlet_test" @@ -101,7 +103,7 @@ async def test_outlet_set_state(hass: HomeAssistant, hk_driver, events) -> None: ], ) async def test_switch_set_state( - hass: HomeAssistant, hk_driver, entity_id, attrs, events + hass: HomeAssistant, hk_driver, entity_id, attrs, events: list[Event] ) -> None: """Test if accessory and HA are updated accordingly.""" domain = split_entity_id(entity_id)[0] @@ -145,7 +147,9 @@ async def test_switch_set_state( assert events[-1].data[ATTR_VALUE] is None -async def test_valve_switch_set_state(hass: HomeAssistant, hk_driver, events) -> None: +async def test_valve_switch_set_state( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if Valve accessory and HA are updated accordingly.""" entity_id = "switch.valve_test" @@ -214,7 +218,9 @@ async def test_valve_switch_set_state(hass: HomeAssistant, hk_driver, events) -> assert events[-1].data[ATTR_VALUE] is None -async def test_valve_set_state(hass: HomeAssistant, hk_driver, events) -> None: +async def test_valve_set_state( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if Valve accessory and HA are updated accordingly.""" entity_id = "valve.valve_test" @@ -264,7 +270,7 @@ async def test_valve_set_state(hass: HomeAssistant, hk_driver, events) -> None: async def test_vacuum_set_state_with_returnhome_and_start_support( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver, events: list[Event] ) -> None: """Test if Vacuum accessory and HA are updated accordingly.""" entity_id = "vacuum.roomba" @@ -333,7 +339,7 @@ async def test_vacuum_set_state_with_returnhome_and_start_support( async def test_vacuum_set_state_without_returnhome_and_start_support( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver, events: list[Event] ) -> None: """Test if Vacuum accessory and HA are updated accordingly.""" entity_id = "vacuum.roomba" @@ -378,7 +384,9 @@ async def test_vacuum_set_state_without_returnhome_and_start_support( assert events[-1].data[ATTR_VALUE] is None -async def test_reset_switch(hass: HomeAssistant, hk_driver, events) -> None: +async def test_reset_switch( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if switch accessory is reset correctly.""" domain = "scene" entity_id = "scene.test" @@ -422,7 +430,9 @@ async def test_reset_switch(hass: HomeAssistant, hk_driver, events) -> None: assert len(events) == 1 -async def test_script_switch(hass: HomeAssistant, hk_driver, events) -> None: +async def test_script_switch( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if script switch accessory is reset correctly.""" domain = "script" entity_id = "script.test" @@ -471,7 +481,7 @@ async def test_script_switch(hass: HomeAssistant, hk_driver, events) -> None: ["input_select", "select"], ) async def test_input_select_switch( - hass: HomeAssistant, hk_driver, events, domain + hass: HomeAssistant, hk_driver, events: list[Event], domain ) -> None: """Test if select switch accessory is handled correctly.""" entity_id = f"{domain}.test" @@ -526,7 +536,9 @@ async def test_input_select_switch( "domain", ["button", "input_button"], ) -async def test_button_switch(hass: HomeAssistant, hk_driver, events, domain) -> None: +async def test_button_switch( + hass: HomeAssistant, hk_driver, events: list[Event], domain +) -> None: """Test switch accessory from a (input) button entity.""" entity_id = f"{domain}.test" diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index ca2a02cb440..3a32e94e491 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -74,13 +74,13 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import CoreState, HomeAssistant +from homeassistant.core import CoreState, Event, HomeAssistant from homeassistant.helpers import entity_registry as er from tests.common import async_mock_service -async def test_thermostat(hass: HomeAssistant, hk_driver, events) -> None: +async def test_thermostat(hass: HomeAssistant, hk_driver, events: list[Event]) -> None: """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" base_attrs = { @@ -375,7 +375,9 @@ async def test_thermostat(hass: HomeAssistant, hk_driver, events) -> None: assert events[-1].data[ATTR_VALUE] == "TargetHeatingCoolingState to 3" -async def test_thermostat_auto(hass: HomeAssistant, hk_driver, events) -> None: +async def test_thermostat_auto( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" base_attrs = { @@ -509,7 +511,7 @@ async def test_thermostat_auto(hass: HomeAssistant, hk_driver, events) -> None: async def test_thermostat_mode_and_temp_change( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver, events: list[Event] ) -> None: """Test if accessory where the mode and temp change in the same call.""" entity_id = "climate.test" @@ -616,7 +618,9 @@ async def test_thermostat_mode_and_temp_change( ) -async def test_thermostat_humidity(hass: HomeAssistant, hk_driver, events) -> None: +async def test_thermostat_humidity( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if accessory and HA are updated accordingly with humidity.""" entity_id = "climate.test" base_attrs = {ATTR_SUPPORTED_FEATURES: 4} @@ -680,7 +684,7 @@ async def test_thermostat_humidity(hass: HomeAssistant, hk_driver, events) -> No async def test_thermostat_humidity_with_target_humidity( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver ) -> None: """Test if accessory and HA are updated accordingly with humidity without target hudmidity. @@ -704,7 +708,9 @@ async def test_thermostat_humidity_with_target_humidity( assert acc.char_current_humidity.value == 65 -async def test_thermostat_power_state(hass: HomeAssistant, hk_driver, events) -> None: +async def test_thermostat_power_state( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" base_attrs = { @@ -812,7 +818,9 @@ async def test_thermostat_power_state(hass: HomeAssistant, hk_driver, events) -> assert acc.char_target_heat_cool.value == 2 -async def test_thermostat_fahrenheit(hass: HomeAssistant, hk_driver, events) -> None: +async def test_thermostat_fahrenheit( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" @@ -969,7 +977,7 @@ async def test_thermostat_temperature_step_whole( async def test_thermostat_restore( - hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver, events + hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver ) -> None: """Test setting up an entity from state in the event registry.""" hass.set_state(CoreState.not_running) @@ -1500,7 +1508,7 @@ async def test_thermostat_hvac_modes_without_off( async def test_thermostat_without_target_temp_only_range( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver, events: list[Event] ) -> None: """Test a thermostat that only supports a range.""" entity_id = "climate.test" @@ -1662,7 +1670,9 @@ async def test_thermostat_without_target_temp_only_range( assert events[-1].data[ATTR_VALUE] == "HeatingThresholdTemperature to 27.0°C" -async def test_water_heater(hass: HomeAssistant, hk_driver, events) -> None: +async def test_water_heater( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if accessory and HA are updated accordingly.""" entity_id = "water_heater.test" @@ -1736,7 +1746,9 @@ async def test_water_heater(hass: HomeAssistant, hk_driver, events) -> None: assert acc.char_target_heat_cool.value == 1 -async def test_water_heater_fahrenheit(hass: HomeAssistant, hk_driver, events) -> None: +async def test_water_heater_fahrenheit( + hass: HomeAssistant, hk_driver, events: list[Event] +) -> None: """Test if accessory and HA are update accordingly.""" entity_id = "water_heater.test" @@ -1799,7 +1811,7 @@ async def test_water_heater_get_temperature_range( async def test_water_heater_restore( - hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver, events + hass: HomeAssistant, entity_registry: er.EntityRegistry, hk_driver ) -> None: """Test setting up an entity from state in the event registry.""" hass.set_state(CoreState.not_running) @@ -1849,7 +1861,7 @@ async def test_water_heater_restore( async def test_thermostat_with_no_modes_when_we_first_see( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver ) -> None: """Test if a thermostat that is not ready when we first see it.""" entity_id = "climate.test" @@ -1903,7 +1915,7 @@ async def test_thermostat_with_no_modes_when_we_first_see( async def test_thermostat_with_no_off_after_recheck( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver ) -> None: """Test if a thermostat that is not ready when we first see it that actually does not have off.""" entity_id = "climate.test" @@ -1956,9 +1968,7 @@ async def test_thermostat_with_no_off_after_recheck( assert mock_reload.called -async def test_thermostat_with_temp_clamps( - hass: HomeAssistant, hk_driver, events -) -> None: +async def test_thermostat_with_temp_clamps(hass: HomeAssistant, hk_driver) -> None: """Test that temperatures are clamped to valid values to prevent homekit crash.""" entity_id = "climate.test" base_attrs = { @@ -2013,7 +2023,7 @@ async def test_thermostat_with_temp_clamps( async def test_thermostat_with_fan_modes_with_auto( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver ) -> None: """Test a thermostate with fan modes with an auto fan mode.""" entity_id = "climate.test" @@ -2219,7 +2229,7 @@ async def test_thermostat_with_fan_modes_with_auto( async def test_thermostat_with_fan_modes_with_off( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver ) -> None: """Test a thermostate with fan modes that can turn off.""" entity_id = "climate.test" @@ -2328,7 +2338,7 @@ async def test_thermostat_with_fan_modes_with_off( async def test_thermostat_with_fan_modes_set_to_none( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver ) -> None: """Test a thermostate with fan modes set to None.""" entity_id = "climate.test" @@ -2372,7 +2382,7 @@ async def test_thermostat_with_fan_modes_set_to_none( async def test_thermostat_with_fan_modes_set_to_none_not_supported( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver ) -> None: """Test a thermostate with fan modes set to None and supported feature missing.""" entity_id = "climate.test" @@ -2415,7 +2425,7 @@ async def test_thermostat_with_fan_modes_set_to_none_not_supported( async def test_thermostat_with_supported_features_target_temp_but_fan_mode_set( - hass: HomeAssistant, hk_driver, events + hass: HomeAssistant, hk_driver ) -> None: """Test a thermostate with fan mode and supported feature missing.""" entity_id = "climate.test" @@ -2452,9 +2462,7 @@ async def test_thermostat_with_supported_features_target_temp_but_fan_mode_set( assert not acc.fan_chars -async def test_thermostat_handles_unknown_state( - hass: HomeAssistant, hk_driver, events -) -> None: +async def test_thermostat_handles_unknown_state(hass: HomeAssistant, hk_driver) -> None: """Test a thermostat can handle unknown state.""" entity_id = "climate.test" attrs = { diff --git a/tests/components/homekit/test_type_triggers.py b/tests/components/homekit/test_type_triggers.py index 7471e0bff1c..f7415ef5599 100644 --- a/tests/components/homekit/test_type_triggers.py +++ b/tests/components/homekit/test_type_triggers.py @@ -7,7 +7,7 @@ from homeassistant.components.homekit.const import CHAR_PROGRAMMABLE_SWITCH_EVEN from homeassistant.components.homekit.type_triggers import DeviceTriggerAccessory from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, async_get_device_automations @@ -16,9 +16,7 @@ from tests.common import MockConfigEntry, async_get_device_automations async def test_programmable_switch_button_fires_on_trigger( hass: HomeAssistant, hk_driver, - events, demo_cleanup, - device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: """Test that DeviceTriggerAccessory fires the programmable switch event on trigger.""" From 092e362f01ab345b3d99b88538c4bcff328a8ae3 Mon Sep 17 00:00:00 2001 From: cnico Date: Thu, 4 Jul 2024 16:45:20 +0200 Subject: [PATCH 0743/2411] Add new integration for Dio Chacon cover devices (#116267) * Dio Chacon integration addition with config flow and cover entity * Addition of model information for device * Addition of light and service to force reloading states * Logger improvements * Convert light to switch and usage of v1.0.0 of the api * 100% for tests coverage * Invalid credential implementation and rebase on latest ha dev code * Simplify PR with only one platform * Ruff correction * restore original .gitignore content * Correction of cover state bug when using cover when using actions on cover group. * Begin of corrections following review. * unit tests correction * Refactor with a coordinator as asked by review * Implemented a post constructor callback init method via dio-chacon-api-1.0.2. Improved typing. * Corrections for 2nd review * Reimplemented without coordinator as reviewed with Joostlek * Review improvement * generalize callback in entity * Other review improvements * Refactored tests for readability * Test 100% operationals * Tests review corrections * Tests review corrections * Review tests improvements * simplified tests with snapshots and callback method * Final fixes * Final fixes * Final fixes * Rename to chacon_dio --------- Co-authored-by: Joostlek --- CODEOWNERS | 2 + .../components/chacon_dio/__init__.py | 80 +++++++++ .../components/chacon_dio/config_flow.py | 67 ++++++++ homeassistant/components/chacon_dio/const.py | 5 + homeassistant/components/chacon_dio/cover.py | 124 ++++++++++++++ homeassistant/components/chacon_dio/entity.py | 53 ++++++ .../components/chacon_dio/manifest.json | 10 ++ .../components/chacon_dio/strings.json | 20 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/chacon_dio/__init__.py | 13 ++ tests/components/chacon_dio/conftest.py | 71 ++++++++ .../chacon_dio/snapshots/test_cover.ambr | 50 ++++++ .../components/chacon_dio/test_config_flow.py | 122 ++++++++++++++ tests/components/chacon_dio/test_cover.py | 157 ++++++++++++++++++ tests/components/chacon_dio/test_init.py | 43 +++++ 18 files changed, 830 insertions(+) create mode 100644 homeassistant/components/chacon_dio/__init__.py create mode 100644 homeassistant/components/chacon_dio/config_flow.py create mode 100644 homeassistant/components/chacon_dio/const.py create mode 100644 homeassistant/components/chacon_dio/cover.py create mode 100644 homeassistant/components/chacon_dio/entity.py create mode 100644 homeassistant/components/chacon_dio/manifest.json create mode 100644 homeassistant/components/chacon_dio/strings.json create mode 100644 tests/components/chacon_dio/__init__.py create mode 100644 tests/components/chacon_dio/conftest.py create mode 100644 tests/components/chacon_dio/snapshots/test_cover.ambr create mode 100644 tests/components/chacon_dio/test_config_flow.py create mode 100644 tests/components/chacon_dio/test_cover.py create mode 100644 tests/components/chacon_dio/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 14f8a7996bc..7add25202e9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -237,6 +237,8 @@ build.json @home-assistant/supervisor /tests/components/ccm15/ @ocalvo /homeassistant/components/cert_expiry/ @jjlawren /tests/components/cert_expiry/ @jjlawren +/homeassistant/components/chacon_dio/ @cnico +/tests/components/chacon_dio/ @cnico /homeassistant/components/cisco_ios/ @fbradyirl /homeassistant/components/cisco_mobility_express/ @fbradyirl /homeassistant/components/cisco_webex_teams/ @fbradyirl diff --git a/homeassistant/components/chacon_dio/__init__.py b/homeassistant/components/chacon_dio/__init__.py new file mode 100644 index 00000000000..00558572fca --- /dev/null +++ b/homeassistant/components/chacon_dio/__init__.py @@ -0,0 +1,80 @@ +"""The chacon_dio integration.""" + +from dataclasses import dataclass +import logging +from typing import Any + +from dio_chacon_wifi_api import DIOChaconAPIClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, + Platform, +) +from homeassistant.core import Event, HomeAssistant + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: list[Platform] = [Platform.COVER] + + +@dataclass +class ChaconDioData: + """Chacon Dio data class.""" + + client: DIOChaconAPIClient + list_devices: list[dict[str, Any]] + + +type ChaconDioConfigEntry = ConfigEntry[ChaconDioData] + + +async def async_setup_entry(hass: HomeAssistant, entry: ChaconDioConfigEntry) -> bool: + """Set up chacon_dio from a config entry.""" + + config = entry.data + + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + dio_chacon_id = entry.unique_id + + _LOGGER.debug("Initializing Chacon Dio client %s, %s", username, dio_chacon_id) + client = DIOChaconAPIClient( + username, + password, + dio_chacon_id, + ) + + found_devices = await client.search_all_devices(with_state=True) + list_devices = list(found_devices.values()) + _LOGGER.debug("List of devices %s", list_devices) + + entry.runtime_data = ChaconDioData( + client=client, + list_devices=list_devices, + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + # Disconnect the permanent websocket connection of home assistant on shutdown + async def _async_disconnect_websocket(_: Event) -> None: + await client.disconnect() + + entry.async_on_unload( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_disconnect_websocket + ) + ) + + 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): + await entry.runtime_data.client.disconnect() + + return unload_ok diff --git a/homeassistant/components/chacon_dio/config_flow.py b/homeassistant/components/chacon_dio/config_flow.py new file mode 100644 index 00000000000..54604b81153 --- /dev/null +++ b/homeassistant/components/chacon_dio/config_flow.py @@ -0,0 +1,67 @@ +"""Config flow for chacon_dio integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from dio_chacon_wifi_api import DIOChaconAPIClient +from dio_chacon_wifi_api.exceptions import DIOChaconAPIError, DIOChaconInvalidAuthError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class ChaconDioConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for chacon_dio.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + + errors: dict[str, str] = {} + + if user_input is not None: + client = DIOChaconAPIClient( + user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ) + try: + _user_id: str = await client.get_user_id() + except DIOChaconAPIError: + errors["base"] = "cannot_connect" + except DIOChaconInvalidAuthError: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + else: + await self.async_set_unique_id(_user_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=f"Chacon DiO {user_input[CONF_USERNAME]}", + data=user_input, + ) + + finally: + await client.disconnect() + + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/chacon_dio/const.py b/homeassistant/components/chacon_dio/const.py new file mode 100644 index 00000000000..631db533a12 --- /dev/null +++ b/homeassistant/components/chacon_dio/const.py @@ -0,0 +1,5 @@ +"""Constants for the chacon_dio integration.""" + +DOMAIN = "chacon_dio" + +MANUFACTURER = "Chacon" diff --git a/homeassistant/components/chacon_dio/cover.py b/homeassistant/components/chacon_dio/cover.py new file mode 100644 index 00000000000..3a4955adf5c --- /dev/null +++ b/homeassistant/components/chacon_dio/cover.py @@ -0,0 +1,124 @@ +"""Cover Platform for Chacon Dio REV-SHUTTER devices.""" + +import logging +from typing import Any + +from dio_chacon_wifi_api.const import DeviceTypeEnum, ShutterMoveEnum + +from homeassistant.components.cover import ( + ATTR_POSITION, + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ChaconDioConfigEntry +from .entity import ChaconDioEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ChaconDioConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Chacon Dio cover devices.""" + data = config_entry.runtime_data + client = data.client + + async_add_entities( + ChaconDioCover(client, device) + for device in data.list_devices + if device["type"] == DeviceTypeEnum.SHUTTER.value + ) + + +class ChaconDioCover(ChaconDioEntity, CoverEntity): + """Object for controlling a Chacon Dio cover.""" + + _attr_device_class = CoverDeviceClass.SHUTTER + _attr_name = None + + _attr_supported_features = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + | CoverEntityFeature.SET_POSITION + ) + + def _update_attr(self, data: dict[str, Any]) -> None: + """Recomputes the attributes values either at init or when the device state changes.""" + self._attr_available = data["connected"] + self._attr_current_cover_position = data["openlevel"] + self._attr_is_closing = data["movement"] == ShutterMoveEnum.DOWN.value + self._attr_is_opening = data["movement"] == ShutterMoveEnum.UP.value + self._attr_is_closed = self._attr_current_cover_position == 0 + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover. + + Closed status is effective after the server callback that triggers callback_device_state. + """ + + _LOGGER.debug( + "Close cover %s , %s, %s", + self.target_id, + self._attr_name, + self.is_closed, + ) + + # closes effectively only if cover is not already closing and not fully closed + if not self._attr_is_closing and not self.is_closed: + self._attr_is_closing = True + self.async_write_ha_state() + + await self.client.move_shutter_direction( + self.target_id, ShutterMoveEnum.DOWN + ) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover. + + Opened status is effective after the server callback that triggers callback_device_state. + """ + + _LOGGER.debug( + "Open cover %s , %s, %s", + self.target_id, + self._attr_name, + self.current_cover_position, + ) + + # opens effectively only if cover is not already opening and not fully opened + if not self._attr_is_opening and self.current_cover_position != 100: + self._attr_is_opening = True + self.async_write_ha_state() + + await self.client.move_shutter_direction(self.target_id, ShutterMoveEnum.UP) + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + + _LOGGER.debug("Stop cover %s , %s", self.target_id, self._attr_name) + + self._attr_is_opening = False + self._attr_is_closing = False + self.async_write_ha_state() + + await self.client.move_shutter_direction(self.target_id, ShutterMoveEnum.STOP) + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Set the cover open position in percentage. + + Closing or opening status is effective after the server callback that triggers callback_device_state. + """ + position: int = kwargs[ATTR_POSITION] + + _LOGGER.debug( + "Set cover position %i, %s , %s", position, self.target_id, self._attr_name + ) + + await self.client.move_shutter_percentage(self.target_id, position) diff --git a/homeassistant/components/chacon_dio/entity.py b/homeassistant/components/chacon_dio/entity.py new file mode 100644 index 00000000000..38f3d7f5831 --- /dev/null +++ b/homeassistant/components/chacon_dio/entity.py @@ -0,0 +1,53 @@ +"""Base entity for the Chacon Dio entity.""" + +import logging +from typing import Any + +from dio_chacon_wifi_api import DIOChaconAPIClient + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +class ChaconDioEntity(Entity): + """Implements a common class elements representing the Chacon Dio entity.""" + + _attr_should_poll = False + _attr_has_entity_name = True + + def __init__(self, client: DIOChaconAPIClient, device: dict[str, Any]) -> None: + """Initialize Chacon Dio entity.""" + + self.client = client + + self.target_id: str = device["id"] + self._attr_unique_id = self.target_id + self._attr_device_info: DeviceInfo | None = DeviceInfo( + identifiers={(DOMAIN, self.target_id)}, + manufacturer=MANUFACTURER, + name=device["name"], + model=device["model"], + ) + + self._update_attr(device) + + def _update_attr(self, data: dict[str, Any]) -> None: + """Recomputes the attributes values.""" + + async def async_added_to_hass(self) -> None: + """Register the callback for server side events.""" + await super().async_added_to_hass() + self.client.set_callback_device_state_by_device( + self.target_id, self.callback_device_state + ) + + def callback_device_state(self, data: dict[str, Any]) -> None: + """Receive callback for device state notification pushed from the server.""" + + _LOGGER.debug("Data received from server %s", data) + self._update_attr(data) + self.async_write_ha_state() diff --git a/homeassistant/components/chacon_dio/manifest.json b/homeassistant/components/chacon_dio/manifest.json new file mode 100644 index 00000000000..d077b130da9 --- /dev/null +++ b/homeassistant/components/chacon_dio/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "chacon_dio", + "name": "Chacon DiO", + "codeowners": ["@cnico"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/chacon_dio", + "iot_class": "cloud_push", + "loggers": ["dio_chacon_api"], + "requirements": ["dio-chacon-wifi-api==1.1.0"] +} diff --git a/homeassistant/components/chacon_dio/strings.json b/homeassistant/components/chacon_dio/strings.json new file mode 100644 index 00000000000..96dc8b371d1 --- /dev/null +++ b/homeassistant/components/chacon_dio/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 463a38feb9f..a715a0ccd76 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -98,6 +98,7 @@ FLOWS = { "cast", "ccm15", "cert_expiry", + "chacon_dio", "cloudflare", "co2signal", "coinbase", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0ad8ac09c9e..b2ff70eefe1 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -878,6 +878,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "chacon_dio": { + "name": "Chacon DiO", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_push" + }, "channels": { "name": "Channels", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 7b07f994f80..0e8be70b427 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -734,6 +734,9 @@ devolo-home-control-api==0.18.3 # homeassistant.components.devolo_home_network devolo-plc-api==1.4.1 +# homeassistant.components.chacon_dio +dio-chacon-wifi-api==1.1.0 + # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44d60bfa13a..bf109a087c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -615,6 +615,9 @@ devolo-home-control-api==0.18.3 # homeassistant.components.devolo_home_network devolo-plc-api==1.4.1 +# homeassistant.components.chacon_dio +dio-chacon-wifi-api==1.1.0 + # homeassistant.components.directv directv==0.4.0 diff --git a/tests/components/chacon_dio/__init__.py b/tests/components/chacon_dio/__init__.py new file mode 100644 index 00000000000..2a340097eb2 --- /dev/null +++ b/tests/components/chacon_dio/__init__.py @@ -0,0 +1,13 @@ +"""Tests for the Chacon Dio integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/chacon_dio/conftest.py b/tests/components/chacon_dio/conftest.py new file mode 100644 index 00000000000..f837403f14e --- /dev/null +++ b/tests/components/chacon_dio/conftest.py @@ -0,0 +1,71 @@ +"""Common fixtures for the chacon_dio tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.chacon_dio.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +MOCK_COVER_DEVICE = { + "L4HActuator_idmock1": { + "id": "L4HActuator_idmock1", + "name": "Shutter mock 1", + "type": "SHUTTER", + "model": "CERSwd-3B_1.0.6", + "connected": True, + "openlevel": 75, + "movement": "stop", + } +} + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.chacon_dio.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock the config entry.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id="test_entry_unique_id", + data={ + CONF_USERNAME: "dummylogin", + CONF_PASSWORD: "dummypass", + }, + ) + + +@pytest.fixture +def mock_dio_chacon_client() -> Generator[AsyncMock]: + """Mock a Dio Chacon client.""" + + with ( + patch( + "homeassistant.components.chacon_dio.DIOChaconAPIClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.chacon_dio.config_flow.DIOChaconAPIClient", + new=mock_client, + ), + ): + client = mock_client.return_value + + # Default values for the tests using this mock : + client.get_user_id.return_value = "dummy-user-id" + client.search_all_devices.return_value = MOCK_COVER_DEVICE + + client.move_shutter_direction.return_value = {} + client.disconnect.return_value = {} + + yield client diff --git a/tests/components/chacon_dio/snapshots/test_cover.ambr b/tests/components/chacon_dio/snapshots/test_cover.ambr new file mode 100644 index 00000000000..b2febe20070 --- /dev/null +++ b/tests/components/chacon_dio/snapshots/test_cover.ambr @@ -0,0 +1,50 @@ +# serializer version: 1 +# name: test_entities[cover.shutter_mock_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.shutter_mock_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'chacon_dio', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'L4HActuator_idmock1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[cover.shutter_mock_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 75, + 'device_class': 'shutter', + 'friendly_name': 'Shutter mock 1', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.shutter_mock_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- diff --git a/tests/components/chacon_dio/test_config_flow.py b/tests/components/chacon_dio/test_config_flow.py new file mode 100644 index 00000000000..d72b5a7dec3 --- /dev/null +++ b/tests/components/chacon_dio/test_config_flow.py @@ -0,0 +1,122 @@ +"""Test the chacon_dio config flow.""" + +from unittest.mock import AsyncMock + +from dio_chacon_wifi_api.exceptions import DIOChaconAPIError, DIOChaconInvalidAuthError +import pytest + +from homeassistant.components.chacon_dio.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_flow( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_dio_chacon_client: AsyncMock +) -> None: + """Test the full flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert not result["errors"] + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_USERNAME: "dummylogin", + CONF_PASSWORD: "dummypass", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Chacon DiO dummylogin" + assert result["result"].unique_id == "dummy-user-id" + assert result["data"] == { + CONF_USERNAME: "dummylogin", + CONF_PASSWORD: "dummypass", + } + + +@pytest.mark.parametrize( + ("exception", "expected"), + [ + (Exception("Bad request Boy :) --"), {"base": "unknown"}), + (DIOChaconInvalidAuthError, {"base": "invalid_auth"}), + (DIOChaconAPIError, {"base": "cannot_connect"}), + ], +) +async def test_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_dio_chacon_client: AsyncMock, + exception: Exception, + expected: dict[str, str], +) -> None: + """Test we handle any error.""" + mock_dio_chacon_client.get_user_id.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_USERNAME: "nada", + CONF_PASSWORD: "nadap", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == expected + + # Test of recover in normal state after correction of the 1st error + mock_dio_chacon_client.get_user_id.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "dummylogin", + CONF_PASSWORD: "dummypass", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Chacon DiO dummylogin" + assert result["result"].unique_id == "dummy-user-id" + assert result["data"] == { + CONF_USERNAME: "dummylogin", + CONF_PASSWORD: "dummypass", + } + + +async def test_duplicate_entry( + hass: HomeAssistant, + mock_dio_chacon_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test abort when setting up duplicate entry.""" + + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + + mock_dio_chacon_client.get_user_id.return_value = "test_entry_unique_id" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "dummylogin", + CONF_PASSWORD: "dummypass", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/chacon_dio/test_cover.py b/tests/components/chacon_dio/test_cover.py new file mode 100644 index 00000000000..be606e67e1e --- /dev/null +++ b/tests/components/chacon_dio/test_cover.py @@ -0,0 +1,157 @@ +"""Test the Chacon Dio cover.""" + +from collections.abc import Callable +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + +COVER_ENTITY_ID = "cover.shutter_mock_1" + + +async def test_entities( + hass: HomeAssistant, + mock_dio_chacon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the creation and values of the Chacon Dio covers.""" + + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_cover_actions( + hass: HomeAssistant, + mock_dio_chacon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the creation and values of the Chacon Dio covers.""" + + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: COVER_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get(COVER_ENTITY_ID) + assert state.state == STATE_CLOSING + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: COVER_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get(COVER_ENTITY_ID) + assert state.state == STATE_OPEN + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: COVER_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get(COVER_ENTITY_ID) + assert state.state == STATE_OPENING + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_POSITION: 25, ATTR_ENTITY_ID: COVER_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get(COVER_ENTITY_ID) + assert state.state == STATE_OPENING + + +async def test_cover_callbacks( + hass: HomeAssistant, + mock_dio_chacon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test the creation and values of the Chacon Dio covers.""" + + await setup_integration(hass, mock_config_entry) + + # Server side callback tests + # We find the callback method on the mock client + callback_device_state_function: Callable = ( + mock_dio_chacon_client.set_callback_device_state_by_device.call_args[0][1] + ) + + # Define a method to simply call it + async def _callback_device_state_function(open_level: int, movement: str) -> None: + callback_device_state_function( + { + "id": "L4HActuator_idmock1", + "connected": True, + "openlevel": open_level, + "movement": movement, + } + ) + await hass.async_block_till_done() + + # And call it to effectively launch the callback as the server would do + await _callback_device_state_function(79, "stop") + state = hass.states.get(COVER_ENTITY_ID) + assert state + assert state.attributes.get(ATTR_CURRENT_POSITION) == 79 + assert state.state == STATE_OPEN + + await _callback_device_state_function(90, "up") + state = hass.states.get(COVER_ENTITY_ID) + assert state + assert state.attributes.get(ATTR_CURRENT_POSITION) == 90 + assert state.state == STATE_OPENING + + await _callback_device_state_function(60, "down") + state = hass.states.get(COVER_ENTITY_ID) + assert state + assert state.attributes.get(ATTR_CURRENT_POSITION) == 60 + assert state.state == STATE_CLOSING + + +async def test_no_cover_found( + hass: HomeAssistant, + mock_dio_chacon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test the cover absence.""" + + mock_dio_chacon_client.search_all_devices.return_value = None + + await setup_integration(hass, mock_config_entry) + + assert not hass.states.get(COVER_ENTITY_ID) diff --git a/tests/components/chacon_dio/test_init.py b/tests/components/chacon_dio/test_init.py new file mode 100644 index 00000000000..78f1a85c71a --- /dev/null +++ b/tests/components/chacon_dio/test_init.py @@ -0,0 +1,43 @@ +"""Test the Dio Chacon Cover init.""" + +from unittest.mock import AsyncMock + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_cover_unload_entry( + hass: HomeAssistant, + mock_dio_chacon_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the creation and values of the Dio Chacon covers.""" + + await setup_integration(hass, mock_config_entry) + + 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 mock_config_entry.state is ConfigEntryState.NOT_LOADED + mock_dio_chacon_client.disconnect.assert_called() + + +async def test_cover_shutdown_event( + hass: HomeAssistant, + mock_dio_chacon_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the creation and values of the Dio Chacon covers.""" + + await setup_integration(hass, mock_config_entry) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + mock_dio_chacon_client.disconnect.assert_called() From a1e6f8c2ec7c27fb6ee7123e18dcbfa5ab01dc52 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Jul 2024 17:39:13 +0200 Subject: [PATCH 0744/2411] Drop use of async_setup_recorder_instance fixture in recorder migration tests (#121196) --- .../recorder/test_migration_from_schema_32.py | 130 +++++++++--------- ..._migration_run_time_migrations_remember.py | 48 ++++--- 2 files changed, 91 insertions(+), 87 deletions(-) diff --git a/tests/components/recorder/test_migration_from_schema_32.py b/tests/components/recorder/test_migration_from_schema_32.py index ec307632826..91358388614 100644 --- a/tests/components/recorder/test_migration_from_schema_32.py +++ b/tests/components/recorder/test_migration_from_schema_32.py @@ -1,6 +1,5 @@ """The tests for the recorder filter matching the EntityFilter component.""" -from collections.abc import AsyncGenerator import datetime import importlib import sys @@ -60,6 +59,13 @@ CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" SCHEMA_MODULE = "tests.components.recorder.db_schema_32" +@pytest.fixture +async def mock_recorder_before_hass( + async_test_recorder: RecorderInstanceGenerator, +) -> None: + """Set up recorder.""" + + async def _async_wait_migration_done(hass: HomeAssistant) -> None: """Wait for the migration to be done.""" await recorder.get_instance(hass).async_block_till_done() @@ -116,21 +122,11 @@ def db_schema_32(): yield -@pytest.fixture(name="legacy_recorder_mock") -async def legacy_recorder_mock_fixture( - recorder_mock: Recorder, -) -> AsyncGenerator[Recorder]: - """Fixture for legacy recorder mock.""" - with patch.object(recorder_mock.states_meta_manager, "active", False): - yield recorder_mock - - @pytest.mark.parametrize("enable_migrate_context_ids", [True]) async def test_migrate_events_context_ids( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test we can migrate old uuid context ids and ulid context ids to binary format.""" - instance = await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] @@ -224,7 +220,7 @@ async def test_migrate_events_context_ids( ) ) - await instance.async_add_executor_job(_insert_events) + await recorder_mock.async_add_executor_job(_insert_events) await async_wait_recording_done(hass) now = dt_util.utcnow() @@ -233,7 +229,7 @@ async def test_migrate_events_context_ids( with freeze_time(now): # This is a threadsafe way to add a task to the recorder - instance.queue_task(EventsContextIDMigrationTask()) + recorder_mock.queue_task(EventsContextIDMigrationTask()) await _async_wait_migration_done(hass) def _object_as_dict(obj): @@ -260,7 +256,7 @@ async def test_migrate_events_context_ids( assert len(events) == 6 return {event.event_type: _object_as_dict(event) for event in events} - events_by_type = await instance.async_add_executor_job(_fetch_migrated_events) + events_by_type = await recorder_mock.async_add_executor_job(_fetch_migrated_events) old_uuid_context_id_event = events_by_type["old_uuid_context_id_event"] assert old_uuid_context_id_event["context_id"] is None @@ -331,7 +327,9 @@ async def test_migrate_events_context_ids( event_with_garbage_context_id_no_time_fired_ts["context_parent_id_bin"] is None ) - migration_changes = await instance.async_add_executor_job(_get_migration_id, hass) + migration_changes = await recorder_mock.async_add_executor_job( + _get_migration_id, hass + ) assert ( migration_changes[migration.EventsContextIDMigration.migration_id] == migration.EventsContextIDMigration.migration_version @@ -340,10 +338,9 @@ async def test_migrate_events_context_ids( @pytest.mark.parametrize("enable_migrate_context_ids", [True]) async def test_migrate_states_context_ids( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test we can migrate old uuid context ids and ulid context ids to binary format.""" - instance = await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] @@ -419,10 +416,10 @@ async def test_migrate_states_context_ids( ) ) - await instance.async_add_executor_job(_insert_states) + await recorder_mock.async_add_executor_job(_insert_states) await async_wait_recording_done(hass) - instance.queue_task(StatesContextIDMigrationTask()) + recorder_mock.queue_task(StatesContextIDMigrationTask()) await _async_wait_migration_done(hass) def _object_as_dict(obj): @@ -449,7 +446,9 @@ async def test_migrate_states_context_ids( assert len(events) == 6 return {state.entity_id: _object_as_dict(state) for state in events} - states_by_entity_id = await instance.async_add_executor_job(_fetch_migrated_states) + states_by_entity_id = await recorder_mock.async_add_executor_job( + _fetch_migrated_states + ) old_uuid_context_id = states_by_entity_id["state.old_uuid_context_id"] assert old_uuid_context_id["context_id"] is None @@ -524,7 +523,9 @@ async def test_migrate_states_context_ids( == b"\n\xe2\x97\x99\xeeNOE\x81\x16\xf5\x82\xd7\xd3\xeee" ) - migration_changes = await instance.async_add_executor_job(_get_migration_id, hass) + migration_changes = await recorder_mock.async_add_executor_job( + _get_migration_id, hass + ) assert ( migration_changes[migration.StatesContextIDMigration.migration_id] == migration.StatesContextIDMigration.migration_version @@ -533,10 +534,9 @@ async def test_migrate_states_context_ids( @pytest.mark.parametrize("enable_migrate_event_type_ids", [True]) async def test_migrate_event_type_ids( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test we can migrate event_types to the EventTypes table.""" - instance = await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] @@ -563,11 +563,11 @@ async def test_migrate_event_type_ids( ) ) - await instance.async_add_executor_job(_insert_events) + await recorder_mock.async_add_executor_job(_insert_events) await async_wait_recording_done(hass) # This is a threadsafe way to add a task to the recorder - instance.queue_task(EventTypeIDMigrationTask()) + recorder_mock.queue_task(EventTypeIDMigrationTask()) await _async_wait_migration_done(hass) def _fetch_migrated_events(): @@ -599,21 +599,23 @@ async def test_migrate_event_type_ids( ) return result - events_by_type = await instance.async_add_executor_job(_fetch_migrated_events) + events_by_type = await recorder_mock.async_add_executor_job(_fetch_migrated_events) assert len(events_by_type["event_type_one"]) == 2 assert len(events_by_type["event_type_two"]) == 1 def _get_many(): with session_scope(hass=hass, read_only=True) as session: - return instance.event_type_manager.get_many( + return recorder_mock.event_type_manager.get_many( ("event_type_one", "event_type_two"), session ) - mapped = await instance.async_add_executor_job(_get_many) + mapped = await recorder_mock.async_add_executor_job(_get_many) assert mapped["event_type_one"] is not None assert mapped["event_type_two"] is not None - migration_changes = await instance.async_add_executor_job(_get_migration_id, hass) + migration_changes = await recorder_mock.async_add_executor_job( + _get_migration_id, hass + ) assert ( migration_changes[migration.EventTypeIDMigration.migration_id] == migration.EventTypeIDMigration.migration_version @@ -621,11 +623,8 @@ async def test_migrate_event_type_ids( @pytest.mark.parametrize("enable_migrate_entity_ids", [True]) -async def test_migrate_entity_ids( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant -) -> None: +async def test_migrate_entity_ids(hass: HomeAssistant, recorder_mock: Recorder) -> None: """Test we can migrate entity_ids to the StatesMeta table.""" - instance = await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] @@ -652,11 +651,11 @@ async def test_migrate_entity_ids( ) ) - await instance.async_add_executor_job(_insert_states) + await recorder_mock.async_add_executor_job(_insert_states) await _async_wait_migration_done(hass) # This is a threadsafe way to add a task to the recorder - instance.queue_task(EntityIDMigrationTask()) + recorder_mock.queue_task(EntityIDMigrationTask()) await _async_wait_migration_done(hass) def _fetch_migrated_states(): @@ -683,11 +682,15 @@ async def test_migrate_entity_ids( ) return result - states_by_entity_id = await instance.async_add_executor_job(_fetch_migrated_states) + states_by_entity_id = await recorder_mock.async_add_executor_job( + _fetch_migrated_states + ) assert len(states_by_entity_id["sensor.two"]) == 2 assert len(states_by_entity_id["sensor.one"]) == 1 - migration_changes = await instance.async_add_executor_job(_get_migration_id, hass) + migration_changes = await recorder_mock.async_add_executor_job( + _get_migration_id, hass + ) assert ( migration_changes[migration.EntityIDMigration.migration_id] == migration.EntityIDMigration.migration_version @@ -696,10 +699,9 @@ async def test_migrate_entity_ids( @pytest.mark.parametrize("enable_migrate_entity_ids", [True]) async def test_post_migrate_entity_ids( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test we can migrate entity_ids to the StatesMeta table.""" - instance = await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] @@ -726,11 +728,11 @@ async def test_post_migrate_entity_ids( ) ) - await instance.async_add_executor_job(_insert_events) + await recorder_mock.async_add_executor_job(_insert_events) await _async_wait_migration_done(hass) # This is a threadsafe way to add a task to the recorder - instance.queue_task(EntityIDPostMigrationTask()) + recorder_mock.queue_task(EntityIDPostMigrationTask()) await _async_wait_migration_done(hass) def _fetch_migrated_states(): @@ -742,7 +744,7 @@ async def test_post_migrate_entity_ids( assert len(states) == 3 return {state.state: state.entity_id for state in states} - states_by_state = await instance.async_add_executor_job(_fetch_migrated_states) + states_by_state = await recorder_mock.async_add_executor_job(_fetch_migrated_states) assert states_by_state["one_1"] is None assert states_by_state["two_2"] is None assert states_by_state["two_1"] is None @@ -750,10 +752,9 @@ async def test_post_migrate_entity_ids( @pytest.mark.parametrize("enable_migrate_entity_ids", [True]) async def test_migrate_null_entity_ids( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test we can migrate entity_ids to the StatesMeta table.""" - instance = await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] @@ -783,11 +784,11 @@ async def test_migrate_null_entity_ids( ), ) - await instance.async_add_executor_job(_insert_states) + await recorder_mock.async_add_executor_job(_insert_states) await _async_wait_migration_done(hass) # This is a threadsafe way to add a task to the recorder - instance.queue_task(EntityIDMigrationTask()) + recorder_mock.queue_task(EntityIDMigrationTask()) await _async_wait_migration_done(hass) def _fetch_migrated_states(): @@ -814,7 +815,9 @@ async def test_migrate_null_entity_ids( ) return result - states_by_entity_id = await instance.async_add_executor_job(_fetch_migrated_states) + states_by_entity_id = await recorder_mock.async_add_executor_job( + _fetch_migrated_states + ) assert len(states_by_entity_id[migration._EMPTY_ENTITY_ID]) == 1000 assert len(states_by_entity_id["sensor.one"]) == 2 @@ -822,7 +825,7 @@ async def test_migrate_null_entity_ids( with session_scope(hass=hass, read_only=True) as session: return dict(execute_stmt_lambda_element(session, get_migration_changes())) - migration_changes = await instance.async_add_executor_job(_get_migration_id) + migration_changes = await recorder_mock.async_add_executor_job(_get_migration_id) assert ( migration_changes[migration.EntityIDMigration.migration_id] == migration.EntityIDMigration.migration_version @@ -831,10 +834,9 @@ async def test_migrate_null_entity_ids( @pytest.mark.parametrize("enable_migrate_event_type_ids", [True]) async def test_migrate_null_event_type_ids( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test we can migrate event_types to the EventTypes table when the event_type is NULL.""" - instance = await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] @@ -864,11 +866,11 @@ async def test_migrate_null_event_type_ids( ), ) - await instance.async_add_executor_job(_insert_events) + await recorder_mock.async_add_executor_job(_insert_events) await _async_wait_migration_done(hass) # This is a threadsafe way to add a task to the recorder - instance.queue_task(EventTypeIDMigrationTask()) + recorder_mock.queue_task(EventTypeIDMigrationTask()) await _async_wait_migration_done(hass) def _fetch_migrated_events(): @@ -900,7 +902,7 @@ async def test_migrate_null_event_type_ids( ) return result - events_by_type = await instance.async_add_executor_job(_fetch_migrated_events) + events_by_type = await recorder_mock.async_add_executor_job(_fetch_migrated_events) assert len(events_by_type["event_type_one"]) == 2 assert len(events_by_type[migration._EMPTY_EVENT_TYPE]) == 1000 @@ -908,7 +910,7 @@ async def test_migrate_null_event_type_ids( with session_scope(hass=hass, read_only=True) as session: return dict(execute_stmt_lambda_element(session, get_migration_changes())) - migration_changes = await instance.async_add_executor_job(_get_migration_id) + migration_changes = await recorder_mock.async_add_executor_job(_get_migration_id) assert ( migration_changes[migration.EventTypeIDMigration.migration_id] == migration.EventTypeIDMigration.migration_version @@ -916,11 +918,9 @@ async def test_migrate_null_event_type_ids( async def test_stats_timestamp_conversion_is_reentrant( - async_setup_recorder_instance: RecorderInstanceGenerator, - hass: HomeAssistant, + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test stats migration is reentrant.""" - instance = await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) await async_attach_db_engine(hass) importlib.import_module(SCHEMA_MODULE) @@ -932,7 +932,7 @@ async def test_stats_timestamp_conversion_is_reentrant( def _do_migration(): migration._migrate_statistics_columns_to_timestamp_removing_duplicates( - hass, instance, instance.get_session, instance.engine + hass, recorder_mock, recorder_mock.get_session, recorder_mock.engine ) def _insert_fake_metadata(): @@ -1070,11 +1070,9 @@ async def test_stats_timestamp_conversion_is_reentrant( async def test_stats_timestamp_with_one_by_one( - async_setup_recorder_instance: RecorderInstanceGenerator, - hass: HomeAssistant, + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test stats migration with one by one.""" - instance = await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) await async_attach_db_engine(hass) importlib.import_module(SCHEMA_MODULE) @@ -1091,7 +1089,7 @@ async def test_stats_timestamp_with_one_by_one( side_effect=IntegrityError("test", "test", "test"), ): migration._migrate_statistics_columns_to_timestamp_removing_duplicates( - hass, instance, instance.get_session, instance.engine + hass, recorder_mock, recorder_mock.get_session, recorder_mock.engine ) def _insert_fake_metadata(): @@ -1291,11 +1289,9 @@ async def test_stats_timestamp_with_one_by_one( async def test_stats_timestamp_with_one_by_one_removes_duplicates( - async_setup_recorder_instance: RecorderInstanceGenerator, - hass: HomeAssistant, + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test stats migration with one by one removes duplicates.""" - instance = await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) await async_attach_db_engine(hass) importlib.import_module(SCHEMA_MODULE) @@ -1319,7 +1315,7 @@ async def test_stats_timestamp_with_one_by_one_removes_duplicates( ), ): migration._migrate_statistics_columns_to_timestamp_removing_duplicates( - hass, instance, instance.get_session, instance.engine + hass, recorder_mock, recorder_mock.get_session, recorder_mock.engine ) def _insert_fake_metadata(): diff --git a/tests/components/recorder/test_migration_run_time_migrations_remember.py b/tests/components/recorder/test_migration_run_time_migrations_remember.py index 5ef8a4b32e9..ec81711c215 100644 --- a/tests/components/recorder/test_migration_run_time_migrations_remember.py +++ b/tests/components/recorder/test_migration_run_time_migrations_remember.py @@ -28,6 +28,13 @@ CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" SCHEMA_MODULE = "tests.components.recorder.db_schema_32" +@pytest.fixture +async def mock_recorder_before_hass( + async_test_recorder: RecorderInstanceGenerator, +) -> None: + """Set up recorder.""" + + async def _async_wait_migration_done(hass: HomeAssistant) -> None: """Wait for the migration to be done.""" await recorder.get_instance(hass).async_block_till_done() @@ -65,7 +72,7 @@ def _create_engine_test(*args, **kwargs): @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migration_changes_prevent_trying_to_migrate_again( - async_setup_recorder_instance: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceGenerator, ) -> None: """Test that we do not try to migrate when migration_changes indicate its already migrated. @@ -76,9 +83,7 @@ async def test_migration_changes_prevent_trying_to_migrate_again( 3. With current schema to verify we do not have to query to see if the migration is done """ - config = { - recorder.CONF_COMMIT_INTERVAL: 1, - } + config = {recorder.CONF_COMMIT_INTERVAL: 1} importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] @@ -97,8 +102,10 @@ async def test_migration_changes_prevent_trying_to_migrate_again( patch.object(migration.EntityIDMigration, "task", core.RecorderTask), patch(CREATE_ENGINE_TARGET, new=_create_engine_test), ): - async with async_test_home_assistant() as hass: - await async_setup_recorder_instance(hass, config) + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass, config), + ): await hass.async_block_till_done() await async_wait_recording_done(hass) await _async_wait_migration_done(hass) @@ -107,8 +114,7 @@ async def test_migration_changes_prevent_trying_to_migrate_again( await hass.async_stop() # Now start again with current db schema - async with async_test_home_assistant() as hass: - await async_setup_recorder_instance(hass, config) + async with async_test_home_assistant() as hass, async_test_recorder(hass, config): await hass.async_block_till_done() await async_wait_recording_done(hass) await _async_wait_migration_done(hass) @@ -132,19 +138,21 @@ async def test_migration_changes_prevent_trying_to_migrate_again( original_queue_task(self, task) # Finally verify we did not call needs_migrate_query on StatesContextIDMigration - async with async_test_home_assistant() as hass: - with ( - patch( - "homeassistant.components.recorder.core.Recorder.queue_task", - _queue_task, - ), - patch.object( - migration.StatesContextIDMigration, - "needs_migrate_query", - side_effect=RuntimeError("Should not be called"), - ), + with ( + patch( + "homeassistant.components.recorder.core.Recorder.queue_task", + _queue_task, + ), + patch.object( + migration.StatesContextIDMigration, + "needs_migrate_query", + side_effect=RuntimeError("Should not be called"), + ), + ): + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass, config), ): - await async_setup_recorder_instance(hass, config) await hass.async_block_till_done() await async_wait_recording_done(hass) await _async_wait_migration_done(hass) From 6df15ad8fc57086834be1a8275b95c79e3e29cc1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Jul 2024 17:39:24 +0200 Subject: [PATCH 0745/2411] Drop use of async_setup_recorder_instance fixture in recorder purge tests (#121193) --- tests/components/recorder/test_purge.py | 312 ++++++++---------- .../recorder/test_purge_v32_schema.py | 181 +++++----- 2 files changed, 229 insertions(+), 264 deletions(-) diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index b21bbd36d28..5e6a413d64e 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -12,7 +12,7 @@ from sqlalchemy.exc import DatabaseError, OperationalError from sqlalchemy.orm.session import Session from voluptuous.error import MultipleInvalid -from homeassistant.components import recorder +from homeassistant.components.recorder import DOMAIN as RECORDER_DOMAIN, Recorder from homeassistant.components.recorder.const import SupportedDialect from homeassistant.components.recorder.db_schema import ( Events, @@ -35,7 +35,6 @@ from homeassistant.components.recorder.tasks import PurgeTask from homeassistant.components.recorder.util import session_scope from homeassistant.const import EVENT_STATE_CHANGED, EVENT_THEMES_UPDATED, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util from .common import ( @@ -58,6 +57,13 @@ TEST_EVENT_TYPES = ( ) +@pytest.fixture +async def mock_recorder_before_hass( + async_test_recorder: RecorderInstanceGenerator, +) -> None: + """Set up recorder.""" + + @pytest.fixture(name="use_sqlite") def mock_use_sqlite(request: pytest.FixtureRequest) -> Generator[None]: """Pytest fixture to switch purge method.""" @@ -70,20 +76,15 @@ def mock_use_sqlite(request: pytest.FixtureRequest) -> Generator[None]: yield -async def test_purge_big_database( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant -) -> None: +async def test_purge_big_database(hass: HomeAssistant, recorder_mock: Recorder) -> None: """Test deleting 2/3 old states from a big database.""" - - instance = await async_setup_recorder_instance(hass) - for _ in range(12): await _add_test_states(hass, wait_recording_done=False) await async_wait_recording_done(hass) with ( - patch.object(instance, "max_bind_vars", 72), - patch.object(instance.database_engine, "max_bind_vars", 72), + patch.object(recorder_mock, "max_bind_vars", 72), + patch.object(recorder_mock.database_engine, "max_bind_vars", 72), session_scope(hass=hass) as session, ): states = session.query(States) @@ -94,7 +95,7 @@ async def test_purge_big_database( purge_before = dt_util.utcnow() - timedelta(days=4) finished = purge_old_data( - instance, + recorder_mock, purge_before, states_batch_size=1, events_batch_size=1, @@ -105,12 +106,8 @@ async def test_purge_big_database( assert state_attributes.count() == 1 -async def test_purge_old_states( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant -) -> None: +async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> None: """Test deleting old states.""" - instance = await async_setup_recorder_instance(hass) - await _add_test_states(hass) # make sure we start with 6 states @@ -125,13 +122,13 @@ async def test_purge_old_states( events = session.query(Events).filter(Events.event_type == "state_changed") assert events.count() == 0 - assert "test.recorder2" in instance.states_manager._last_committed_id + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id purge_before = dt_util.utcnow() - timedelta(days=4) # run purge_old_data() finished = purge_old_data( - instance, + recorder_mock, purge_before, states_batch_size=1, events_batch_size=1, @@ -141,7 +138,7 @@ async def test_purge_old_states( assert states.count() == 2 assert state_attributes.count() == 1 - assert "test.recorder2" in instance.states_manager._last_committed_id + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id states_after_purge = list(session.query(States)) # Since these states are deleted in batches, we can't guarantee the order @@ -153,17 +150,17 @@ async def test_purge_old_states( assert dontpurgeme_5.old_state_id == dontpurgeme_4.state_id assert dontpurgeme_4.old_state_id is None - finished = purge_old_data(instance, purge_before, repack=False) + finished = purge_old_data(recorder_mock, purge_before, repack=False) assert finished assert states.count() == 2 assert state_attributes.count() == 1 - assert "test.recorder2" in instance.states_manager._last_committed_id + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id # run purge_old_data again purge_before = dt_util.utcnow() finished = purge_old_data( - instance, + recorder_mock, purge_before, states_batch_size=1, events_batch_size=1, @@ -173,7 +170,7 @@ async def test_purge_old_states( assert states.count() == 0 assert state_attributes.count() == 0 - assert "test.recorder2" not in instance.states_manager._last_committed_id + assert "test.recorder2" not in recorder_mock.states_manager._last_committed_id # Add some more states await _add_test_states(hass) @@ -187,27 +184,22 @@ async def test_purge_old_states( events = session.query(Events).filter(Events.event_type == "state_changed") assert events.count() == 0 - assert "test.recorder2" in instance.states_manager._last_committed_id + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id state_attributes = session.query(StateAttributes) assert state_attributes.count() == 3 @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) -@pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.usefixtures("recorder_mock", "skip_by_db_engine") async def test_purge_old_states_encouters_database_corruption( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, - recorder_db_url: str, ) -> None: """Test database image image is malformed while deleting old states. This test is specific for SQLite, wiping the database on error only happens with SQLite. """ - - await async_setup_recorder_instance(hass) - await _add_test_states(hass) await async_wait_recording_done(hass) @@ -223,7 +215,7 @@ async def test_purge_old_states_encouters_database_corruption( side_effect=sqlite3_exception, ), ): - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) @@ -236,13 +228,11 @@ async def test_purge_old_states_encouters_database_corruption( async def test_purge_old_states_encounters_temporary_mysql_error( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + recorder_mock: Recorder, caplog: pytest.LogCaptureFixture, ) -> None: """Test retry on specific mysql operational errors.""" - instance = await async_setup_recorder_instance(hass) - await _add_test_states(hass) await async_wait_recording_done(hass) @@ -255,9 +245,9 @@ async def test_purge_old_states_encounters_temporary_mysql_error( "homeassistant.components.recorder.purge._purge_old_recorder_runs", side_effect=[mysql_exception, None], ), - patch.object(instance.engine.dialect, "name", "mysql"), + patch.object(recorder_mock.engine.dialect, "name", "mysql"), ): - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -266,14 +256,12 @@ async def test_purge_old_states_encounters_temporary_mysql_error( assert sleep_mock.called +@pytest.mark.usefixtures("recorder_mock") async def test_purge_old_states_encounters_operational_error( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, ) -> None: """Test error on operational errors that are not mysql does not retry.""" - await async_setup_recorder_instance(hass) - await _add_test_states(hass) await async_wait_recording_done(hass) @@ -283,7 +271,7 @@ async def test_purge_old_states_encounters_operational_error( "homeassistant.components.recorder.purge._purge_old_recorder_runs", side_effect=exception, ): - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -292,12 +280,8 @@ async def test_purge_old_states_encounters_operational_error( assert "Error executing purge" in caplog.text -async def test_purge_old_events( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant -) -> None: +async def test_purge_old_events(hass: HomeAssistant, recorder_mock: Recorder) -> None: """Test deleting old events.""" - instance = await async_setup_recorder_instance(hass) - await _add_test_events(hass) with session_scope(hass=hass) as session: @@ -310,7 +294,7 @@ async def test_purge_old_events( # run purge_old_data() finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, events_batch_size=1, @@ -322,7 +306,7 @@ async def test_purge_old_events( # we should only have 2 events left finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, events_batch_size=1, @@ -333,11 +317,9 @@ async def test_purge_old_events( async def test_purge_old_recorder_runs( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test deleting old recorder runs keeps current run.""" - instance = await async_setup_recorder_instance(hass) - await _add_test_recorder_runs(hass) # make sure we start with 7 recorder runs @@ -349,7 +331,7 @@ async def test_purge_old_recorder_runs( # run purge_old_data() finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, events_batch_size=1, @@ -358,7 +340,7 @@ async def test_purge_old_recorder_runs( assert not finished finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, events_batch_size=1, @@ -369,11 +351,9 @@ async def test_purge_old_recorder_runs( async def test_purge_old_statistics_runs( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test deleting old statistics runs keeps the latest run.""" - instance = await async_setup_recorder_instance(hass) - await _add_test_statistics_runs(hass) # make sure we start with 7 statistics runs @@ -384,17 +364,17 @@ async def test_purge_old_statistics_runs( purge_before = dt_util.utcnow() # run purge_old_data() - finished = purge_old_data(instance, purge_before, repack=False) + finished = purge_old_data(recorder_mock, purge_before, repack=False) assert not finished - finished = purge_old_data(instance, purge_before, repack=False) + finished = purge_old_data(recorder_mock, purge_before, repack=False) assert finished assert statistics_runs.count() == 1 @pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) +@pytest.mark.usefixtures("recorder_mock") async def test_purge_method( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, use_sqlite: bool, @@ -412,8 +392,6 @@ async def test_purge_method( assert run1.run_id == run2.run_id assert run1.start == run2.start - await async_setup_recorder_instance(hass) - service_data = {"keep_days": 4} await _add_test_events(hass) await _add_test_states(hass) @@ -519,8 +497,8 @@ async def test_purge_method( @pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) async def test_purge_edge_case( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + recorder_mock: Recorder, use_sqlite: bool, ) -> None: """Test states and events are purged even if they occurred shortly before purge_before.""" @@ -554,11 +532,9 @@ async def test_purge_edge_case( attributes_id=1002, ) ) - instance = recorder.get_instance(hass) - convert_pending_events_to_event_types(instance, session) - convert_pending_states_to_meta(instance, session) + convert_pending_events_to_event_types(recorder_mock, session) + convert_pending_states_to_meta(recorder_mock, session) - await async_setup_recorder_instance(hass, None) await async_wait_purge_done(hass) service_data = {"keep_days": 2} @@ -577,7 +553,7 @@ async def test_purge_edge_case( ) assert events.count() == 1 - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -592,10 +568,7 @@ async def test_purge_edge_case( assert events.count() == 0 -async def test_purge_cutoff_date( - async_setup_recorder_instance: RecorderInstanceGenerator, - hass: HomeAssistant, -) -> None: +async def test_purge_cutoff_date(hass: HomeAssistant, recorder_mock: Recorder) -> None: """Test states and events are purged only if they occurred before "now() - keep_days".""" async def _add_db_entries(hass: HomeAssistant, cutoff: datetime, rows: int) -> None: @@ -658,10 +631,9 @@ async def test_purge_cutoff_date( attributes_id=1000 + row, ) ) - convert_pending_events_to_event_types(instance, session) - convert_pending_states_to_meta(instance, session) + convert_pending_events_to_event_types(recorder_mock, session) + convert_pending_states_to_meta(recorder_mock, session) - instance = await async_setup_recorder_instance(hass, None) await async_wait_purge_done(hass) service_data = {"keep_days": 2} @@ -697,7 +669,7 @@ async def test_purge_cutoff_date( == 1 ) - instance.queue_task(PurgeTask(cutoff, repack=False, apply_filter=False)) + recorder_mock.queue_task(PurgeTask(cutoff, repack=False, apply_filter=False)) await hass.async_block_till_done() await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -738,7 +710,9 @@ async def test_purge_cutoff_date( ) # Make sure we can purge everything - instance.queue_task(PurgeTask(dt_util.utcnow(), repack=False, apply_filter=False)) + recorder_mock.queue_task( + PurgeTask(dt_util.utcnow(), repack=False, apply_filter=False) + ) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -749,7 +723,9 @@ async def test_purge_cutoff_date( assert state_attributes.count() == 0 # Make sure we can purge everything when the db is already empty - instance.queue_task(PurgeTask(dt_util.utcnow(), repack=False, apply_filter=False)) + recorder_mock.queue_task( + PurgeTask(dt_util.utcnow(), repack=False, apply_filter=False) + ) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -761,15 +737,16 @@ async def test_purge_cutoff_date( @pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) +@pytest.mark.parametrize( + "recorder_config", [{"exclude": {"entities": ["sensor.excluded"]}}] +) async def test_purge_filtered_states( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + recorder_mock: Recorder, use_sqlite: bool, ) -> None: """Test filtered states are purged.""" - config: ConfigType = {"exclude": {"entities": ["sensor.excluded"]}} - instance = await async_setup_recorder_instance(hass, config) - assert instance.entity_filter("sensor.excluded") is False + assert recorder_mock.entity_filter("sensor.excluded") is False def _add_db_entries(hass: HomeAssistant) -> None: with session_scope(hass=hass) as session: @@ -852,8 +829,8 @@ async def test_purge_filtered_states( time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) - convert_pending_states_to_meta(instance, session) - convert_pending_events_to_event_types(instance, session) + convert_pending_states_to_meta(recorder_mock, session) + convert_pending_events_to_event_types(recorder_mock, session) service_data = {"keep_days": 10} _add_db_entries(hass) @@ -867,7 +844,7 @@ async def test_purge_filtered_states( assert events_keep.count() == 1 # Normal purge doesn't remove excluded entities - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -883,7 +860,7 @@ async def test_purge_filtered_states( # Test with 'apply_filter' = True service_data["apply_filter"] = True - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -931,7 +908,7 @@ async def test_purge_filtered_states( assert session.query(StateAttributes).count() == 11 # Do it again to make sure nothing changes - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -943,7 +920,7 @@ async def test_purge_filtered_states( assert session.query(StateAttributes).count() == 11 service_data = {"keep_days": 0} - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -956,15 +933,16 @@ async def test_purge_filtered_states( @pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) +@pytest.mark.parametrize( + "recorder_config", [{"exclude": {"entities": ["sensor.excluded"]}}] +) async def test_purge_filtered_states_to_empty( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + recorder_mock: Recorder, use_sqlite: bool, ) -> None: """Test filtered states are purged all the way to an empty db.""" - config: ConfigType = {"exclude": {"entities": ["sensor.excluded"]}} - instance = await async_setup_recorder_instance(hass, config) - assert instance.entity_filter("sensor.excluded") is False + assert recorder_mock.entity_filter("sensor.excluded") is False def _add_db_entries(hass: HomeAssistant) -> None: with session_scope(hass=hass) as session: @@ -979,7 +957,7 @@ async def test_purge_filtered_states_to_empty( timestamp, event_id * days, ) - convert_pending_states_to_meta(instance, session) + convert_pending_states_to_meta(recorder_mock, session) service_data = {"keep_days": 10} _add_db_entries(hass) @@ -992,7 +970,7 @@ async def test_purge_filtered_states_to_empty( # Test with 'apply_filter' = True service_data["apply_filter"] = True - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -1004,21 +982,22 @@ async def test_purge_filtered_states_to_empty( # Do it again to make sure nothing changes # Why do we do this? Should we check the end result? - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) +@pytest.mark.parametrize( + "recorder_config", [{"exclude": {"entities": ["sensor.old_format"]}}] +) async def test_purge_without_state_attributes_filtered_states_to_empty( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + recorder_mock: Recorder, use_sqlite: bool, ) -> None: """Test filtered legacy states without state attributes are purged all the way to an empty db.""" - config: ConfigType = {"exclude": {"entities": ["sensor.old_format"]}} - instance = await async_setup_recorder_instance(hass, config) - assert instance.entity_filter("sensor.old_format") is False + assert recorder_mock.entity_filter("sensor.old_format") is False def _add_db_entries(hass: HomeAssistant) -> None: with session_scope(hass=hass) as session: @@ -1055,8 +1034,8 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) - convert_pending_states_to_meta(instance, session) - convert_pending_events_to_event_types(instance, session) + convert_pending_states_to_meta(recorder_mock, session) + convert_pending_events_to_event_types(recorder_mock, session) service_data = {"keep_days": 10} _add_db_entries(hass) @@ -1069,7 +1048,7 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( # Test with 'apply_filter' = True service_data["apply_filter"] = True - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -1081,18 +1060,18 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( # Do it again to make sure nothing changes # Why do we do this? Should we check the end result? - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) +@pytest.mark.parametrize( + "recorder_config", [{"exclude": {"event_types": ["EVENT_PURGE"]}}] +) async def test_purge_filtered_events( - async_setup_recorder_instance: RecorderInstanceGenerator, - hass: HomeAssistant, + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test filtered events are purged.""" - config: ConfigType = {"exclude": {"event_types": ["EVENT_PURGE"]}} - instance = await async_setup_recorder_instance(hass, config) await async_wait_recording_done(hass) def _add_db_entries(hass: HomeAssistant) -> None: @@ -1121,11 +1100,11 @@ async def test_purge_filtered_events( timestamp, event_id, ) - convert_pending_events_to_event_types(instance, session) - convert_pending_states_to_meta(instance, session) + convert_pending_events_to_event_types(recorder_mock, session) + convert_pending_states_to_meta(recorder_mock, session) service_data = {"keep_days": 10} - await instance.async_add_executor_job(_add_db_entries, hass) + await recorder_mock.async_add_executor_job(_add_db_entries, hass) await async_wait_recording_done(hass) with session_scope(hass=hass, read_only=True) as session: @@ -1137,7 +1116,7 @@ async def test_purge_filtered_events( assert states.count() == 10 # Normal purge doesn't remove excluded events - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -1153,7 +1132,7 @@ async def test_purge_filtered_events( # Test with 'apply_filter' = True service_data["apply_filter"] = True - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -1171,23 +1150,26 @@ async def test_purge_filtered_events( assert states.count() == 10 +@pytest.mark.parametrize( + "recorder_config", + [ + { + "exclude": { + "event_types": ["excluded_event"], + "entities": ["sensor.excluded", "sensor.old_format"], + } + } + ], +) async def test_purge_filtered_events_state_changed( - async_setup_recorder_instance: RecorderInstanceGenerator, - hass: HomeAssistant, + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test filtered state_changed events are purged. This should also remove all states.""" - config: ConfigType = { - "exclude": { - "event_types": ["excluded_event"], - "entities": ["sensor.excluded", "sensor.old_format"], - } - } - instance = await async_setup_recorder_instance(hass, config) # Assert entity_id is NOT excluded - assert instance.entity_filter("sensor.excluded") is False - assert instance.entity_filter("sensor.old_format") is False - assert instance.entity_filter("sensor.keep") is True - assert "excluded_event" in instance.exclude_event_types + assert recorder_mock.entity_filter("sensor.excluded") is False + assert recorder_mock.entity_filter("sensor.old_format") is False + assert recorder_mock.entity_filter("sensor.keep") is True + assert "excluded_event" in recorder_mock.exclude_event_types def _add_db_entries(hass: HomeAssistant) -> None: with session_scope(hass=hass) as session: @@ -1260,8 +1242,8 @@ async def test_purge_filtered_events_state_changed( last_updated_ts=dt_util.utc_to_timestamp(timestamp), ) ) - convert_pending_events_to_event_types(instance, session) - convert_pending_states_to_meta(instance, session) + convert_pending_events_to_event_types(recorder_mock, session) + convert_pending_states_to_meta(recorder_mock, session) service_data = {"keep_days": 10, "apply_filter": True} _add_db_entries(hass) @@ -1279,7 +1261,7 @@ async def test_purge_filtered_events_state_changed( assert events_purge.count() == 1 assert states.count() == 64 - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() for _ in range(4): @@ -1313,11 +1295,8 @@ async def test_purge_filtered_events_state_changed( ) # should have been kept -async def test_purge_entities( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant -) -> None: +async def test_purge_entities(hass: HomeAssistant, recorder_mock: Recorder) -> None: """Test purging of specific entities.""" - instance = await async_setup_recorder_instance(hass) async def _purge_entities(hass, entity_ids, domains, entity_globs): service_data = { @@ -1327,7 +1306,7 @@ async def test_purge_entities( } await hass.services.async_call( - recorder.DOMAIN, SERVICE_PURGE_ENTITIES, service_data + RECORDER_DOMAIN, SERVICE_PURGE_ENTITIES, service_data ) await hass.async_block_till_done() @@ -1365,8 +1344,8 @@ async def test_purge_entities( timestamp, event_id * days, ) - convert_pending_states_to_meta(instance, session) - convert_pending_events_to_event_types(instance, session) + convert_pending_states_to_meta(recorder_mock, session) + convert_pending_events_to_event_types(recorder_mock, session) def _add_keep_records(hass: HomeAssistant) -> None: with session_scope(hass=hass) as session: @@ -1380,8 +1359,8 @@ async def test_purge_entities( timestamp, event_id, ) - convert_pending_states_to_meta(instance, session) - convert_pending_events_to_event_types(instance, session) + convert_pending_states_to_meta(recorder_mock, session) + convert_pending_events_to_event_types(recorder_mock, session) _add_purge_records(hass) _add_keep_records(hass) @@ -1659,15 +1638,14 @@ def _add_state_with_state_attributes( @pytest.mark.timeout(30) async def test_purge_many_old_events( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test deleting old events.""" old_events_count = 5 - instance = await async_setup_recorder_instance(hass) with ( - patch.object(instance, "max_bind_vars", old_events_count), - patch.object(instance.database_engine, "max_bind_vars", old_events_count), + patch.object(recorder_mock, "max_bind_vars", old_events_count), + patch.object(recorder_mock.database_engine, "max_bind_vars", old_events_count), ): await _add_test_events(hass, old_events_count) @@ -1681,7 +1659,7 @@ async def test_purge_many_old_events( # run purge_old_data() finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, states_batch_size=3, @@ -1692,7 +1670,7 @@ async def test_purge_many_old_events( # we should only have 2 groups of events left finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, states_batch_size=3, @@ -1703,7 +1681,7 @@ async def test_purge_many_old_events( # we should now purge everything finished = purge_old_data( - instance, + recorder_mock, dt_util.utcnow(), repack=False, states_batch_size=20, @@ -1714,11 +1692,10 @@ async def test_purge_many_old_events( async def test_purge_old_events_purges_the_event_type_ids( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test deleting old events purges event type ids.""" - instance = await async_setup_recorder_instance(hass) - assert instance.event_type_manager.active is True + assert recorder_mock.event_type_manager.active is True utcnow = dt_util.utcnow() five_days_ago = utcnow - timedelta(days=5) @@ -1762,7 +1739,7 @@ async def test_purge_old_events_purges_the_event_type_ids( time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) - return instance.event_type_manager.get_many( + return recorder_mock.event_type_manager.get_many( [ "EVENT_TEST_AUTOPURGE", "EVENT_TEST_PURGE", @@ -1772,7 +1749,7 @@ async def test_purge_old_events_purges_the_event_type_ids( session, ) - event_type_to_id = await instance.async_add_executor_job(_insert_events) + event_type_to_id = await recorder_mock.async_add_executor_job(_insert_events) test_event_type_ids = event_type_to_id.values() with session_scope(hass=hass) as session: events = session.query(Events).where( @@ -1787,7 +1764,7 @@ async def test_purge_old_events_purges_the_event_type_ids( # run purge_old_data() finished = purge_old_data( - instance, + recorder_mock, far_past, repack=False, ) @@ -1796,12 +1773,12 @@ async def test_purge_old_events_purges_the_event_type_ids( # We should remove the unused event type assert event_types.count() == 3 - assert "EVENT_TEST_UNUSED" not in instance.event_type_manager._id_map + assert "EVENT_TEST_UNUSED" not in recorder_mock.event_type_manager._id_map # we should only have 10 events left since # only one event type was recorded now finished = purge_old_data( - instance, + recorder_mock, utcnow, repack=False, ) @@ -1811,7 +1788,7 @@ async def test_purge_old_events_purges_the_event_type_ids( # Purge everything finished = purge_old_data( - instance, + recorder_mock, utcnow + timedelta(seconds=1), repack=False, ) @@ -1821,11 +1798,10 @@ async def test_purge_old_events_purges_the_event_type_ids( async def test_purge_old_states_purges_the_state_metadata_ids( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test deleting old states purges state metadata_ids.""" - instance = await async_setup_recorder_instance(hass) - assert instance.states_meta_manager.active is True + assert recorder_mock.states_meta_manager.active is True utcnow = dt_util.utcnow() five_days_ago = utcnow - timedelta(days=5) @@ -1869,13 +1845,15 @@ async def test_purge_old_states_purges_the_state_metadata_ids( last_updated_ts=dt_util.utc_to_timestamp(timestamp), ) ) - return instance.states_meta_manager.get_many( + return recorder_mock.states_meta_manager.get_many( ["sensor.one", "sensor.two", "sensor.three", "sensor.unused"], session, True, ) - entity_id_to_metadata_id = await instance.async_add_executor_job(_insert_states) + entity_id_to_metadata_id = await recorder_mock.async_add_executor_job( + _insert_states + ) test_metadata_ids = entity_id_to_metadata_id.values() with session_scope(hass=hass) as session: states = session.query(States).where(States.metadata_id.in_(test_metadata_ids)) @@ -1888,7 +1866,7 @@ async def test_purge_old_states_purges_the_state_metadata_ids( # run purge_old_data() finished = purge_old_data( - instance, + recorder_mock, far_past, repack=False, ) @@ -1897,12 +1875,12 @@ async def test_purge_old_states_purges_the_state_metadata_ids( # We should remove the unused entity_id assert states_meta.count() == 3 - assert "sensor.unused" not in instance.event_type_manager._id_map + assert "sensor.unused" not in recorder_mock.event_type_manager._id_map # we should only have 10 states left since # only one event type was recorded now finished = purge_old_data( - instance, + recorder_mock, utcnow, repack=False, ) @@ -1912,7 +1890,7 @@ async def test_purge_old_states_purges_the_state_metadata_ids( # Purge everything finished = purge_old_data( - instance, + recorder_mock, utcnow + timedelta(seconds=1), repack=False, ) @@ -1922,11 +1900,9 @@ async def test_purge_old_states_purges_the_state_metadata_ids( async def test_purge_entities_keep_days( - async_setup_recorder_instance: RecorderInstanceGenerator, - hass: HomeAssistant, + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test purging states with an entity filter and keep_days.""" - instance = await async_setup_recorder_instance(hass, {}) await hass.async_block_till_done() await async_wait_recording_done(hass) start = dt_util.utcnow() @@ -1948,7 +1924,7 @@ async def test_purge_entities_keep_days( hass.states.async_set("sensor.keep", "now") await async_recorder_block_till_done(hass) - states = await instance.async_add_executor_job( + states = await recorder_mock.async_add_executor_job( get_significant_states, hass, one_month_ago, @@ -1959,7 +1935,7 @@ async def test_purge_entities_keep_days( assert len(states["sensor.purge"]) == 3 await hass.services.async_call( - recorder.DOMAIN, + RECORDER_DOMAIN, SERVICE_PURGE_ENTITIES, { "entity_id": "sensor.purge", @@ -1969,7 +1945,7 @@ async def test_purge_entities_keep_days( await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) - states = await instance.async_add_executor_job( + states = await recorder_mock.async_add_executor_job( get_significant_states, hass, one_month_ago, @@ -1980,7 +1956,7 @@ async def test_purge_entities_keep_days( assert len(states["sensor.purge"]) == 1 await hass.services.async_call( - recorder.DOMAIN, + RECORDER_DOMAIN, SERVICE_PURGE_ENTITIES, { "entity_id": "sensor.purge", @@ -1989,7 +1965,7 @@ async def test_purge_entities_keep_days( await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) - states = await instance.async_add_executor_job( + states = await recorder_mock.async_add_executor_job( get_significant_states, hass, one_month_ago, diff --git a/tests/components/recorder/test_purge_v32_schema.py b/tests/components/recorder/test_purge_v32_schema.py index a3b91ce54a9..51424c31ea2 100644 --- a/tests/components/recorder/test_purge_v32_schema.py +++ b/tests/components/recorder/test_purge_v32_schema.py @@ -12,8 +12,11 @@ from sqlalchemy import text, update from sqlalchemy.exc import DatabaseError, OperationalError from sqlalchemy.orm.session import Session -from homeassistant.components import recorder -from homeassistant.components.recorder import migration +from homeassistant.components.recorder import ( + DOMAIN as RECORDER_DOMAIN, + Recorder, + migration, +) from homeassistant.components.recorder.const import SupportedDialect from homeassistant.components.recorder.history import get_significant_states from homeassistant.components.recorder.purge import purge_old_data @@ -47,6 +50,13 @@ from .db_schema_32 import ( from tests.typing import RecorderInstanceGenerator +@pytest.fixture +async def mock_recorder_before_hass( + async_test_recorder: RecorderInstanceGenerator, +) -> None: + """Set up recorder.""" + + @pytest.fixture(autouse=True) def db_schema_32(): """Fixture to initialize the db with the old schema 32.""" @@ -66,11 +76,8 @@ def mock_use_sqlite(request: pytest.FixtureRequest) -> Generator[None]: yield -async def test_purge_old_states( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant -) -> None: +async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> None: """Test deleting old states.""" - instance = await async_setup_recorder_instance(hass) await async_attach_db_engine(hass) await _add_test_states(hass) @@ -87,13 +94,13 @@ async def test_purge_old_states( events = session.query(Events).filter(Events.event_type == "state_changed") assert events.count() == 0 - assert "test.recorder2" in instance.states_manager._last_committed_id + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id purge_before = dt_util.utcnow() - timedelta(days=4) # run purge_old_data() finished = purge_old_data( - instance, + recorder_mock, purge_before, states_batch_size=1, events_batch_size=1, @@ -103,7 +110,7 @@ async def test_purge_old_states( assert states.count() == 2 assert state_attributes.count() == 1 - assert "test.recorder2" in instance.states_manager._last_committed_id + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id states_after_purge = list(session.query(States)) # Since these states are deleted in batches, we can't guarantee the order @@ -115,17 +122,17 @@ async def test_purge_old_states( assert dontpurgeme_5.old_state_id == dontpurgeme_4.state_id assert dontpurgeme_4.old_state_id is None - finished = purge_old_data(instance, purge_before, repack=False) + finished = purge_old_data(recorder_mock, purge_before, repack=False) assert finished assert states.count() == 2 assert state_attributes.count() == 1 - assert "test.recorder2" in instance.states_manager._last_committed_id + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id # run purge_old_data again purge_before = dt_util.utcnow() finished = purge_old_data( - instance, + recorder_mock, purge_before, states_batch_size=1, events_batch_size=1, @@ -135,7 +142,7 @@ async def test_purge_old_states( assert states.count() == 0 assert state_attributes.count() == 0 - assert "test.recorder2" not in instance.states_manager._last_committed_id + assert "test.recorder2" not in recorder_mock.states_manager._last_committed_id # Add some more states await _add_test_states(hass) @@ -149,26 +156,22 @@ async def test_purge_old_states( events = session.query(Events).filter(Events.event_type == "state_changed") assert events.count() == 0 - assert "test.recorder2" in instance.states_manager._last_committed_id + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id state_attributes = session.query(StateAttributes) assert state_attributes.count() == 3 @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) -@pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.usefixtures("recorder_mock", "skip_by_db_engine") async def test_purge_old_states_encouters_database_corruption( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, - recorder_db_url: str, ) -> None: """Test database image image is malformed while deleting old states. This test is specific for SQLite, wiping the database on error only happens with SQLite. """ - - await async_setup_recorder_instance(hass) await async_attach_db_engine(hass) await _add_test_states(hass) @@ -186,7 +189,7 @@ async def test_purge_old_states_encouters_database_corruption( side_effect=sqlite3_exception, ), ): - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) @@ -199,12 +202,11 @@ async def test_purge_old_states_encouters_database_corruption( async def test_purge_old_states_encounters_temporary_mysql_error( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + recorder_mock: Recorder, caplog: pytest.LogCaptureFixture, ) -> None: """Test retry on specific mysql operational errors.""" - instance = await async_setup_recorder_instance(hass) await async_attach_db_engine(hass) await _add_test_states(hass) @@ -219,9 +221,9 @@ async def test_purge_old_states_encounters_temporary_mysql_error( "homeassistant.components.recorder.purge._purge_old_recorder_runs", side_effect=[mysql_exception, None], ), - patch.object(instance.engine.dialect, "name", "mysql"), + patch.object(recorder_mock.engine.dialect, "name", "mysql"), ): - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -230,13 +232,12 @@ async def test_purge_old_states_encounters_temporary_mysql_error( assert sleep_mock.called +@pytest.mark.usefixtures("recorder_mock") async def test_purge_old_states_encounters_operational_error( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, ) -> None: """Test error on operational errors that are not mysql does not retry.""" - await async_setup_recorder_instance(hass) await async_attach_db_engine(hass) await _add_test_states(hass) @@ -248,7 +249,7 @@ async def test_purge_old_states_encounters_operational_error( "homeassistant.components.recorder.purge._purge_old_recorder_runs", side_effect=exception, ): - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -257,11 +258,8 @@ async def test_purge_old_states_encounters_operational_error( assert "Error executing purge" in caplog.text -async def test_purge_old_events( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant -) -> None: +async def test_purge_old_events(hass: HomeAssistant, recorder_mock: Recorder) -> None: """Test deleting old events.""" - instance = await async_setup_recorder_instance(hass) await async_attach_db_engine(hass) await _add_test_events(hass) @@ -274,7 +272,7 @@ async def test_purge_old_events( # run purge_old_data() finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, events_batch_size=1, @@ -285,7 +283,7 @@ async def test_purge_old_events( # we should only have 2 events left finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, events_batch_size=1, @@ -296,10 +294,9 @@ async def test_purge_old_events( async def test_purge_old_recorder_runs( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test deleting old recorder runs keeps current run.""" - instance = await async_setup_recorder_instance(hass) await async_attach_db_engine(hass) await _add_test_recorder_runs(hass) @@ -313,7 +310,7 @@ async def test_purge_old_recorder_runs( # run purge_old_data() finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, events_batch_size=1, @@ -322,7 +319,7 @@ async def test_purge_old_recorder_runs( assert not finished finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, events_batch_size=1, @@ -333,10 +330,9 @@ async def test_purge_old_recorder_runs( async def test_purge_old_statistics_runs( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test deleting old statistics runs keeps the latest run.""" - instance = await async_setup_recorder_instance(hass) await async_attach_db_engine(hass) await _add_test_statistics_runs(hass) @@ -349,17 +345,17 @@ async def test_purge_old_statistics_runs( purge_before = dt_util.utcnow() # run purge_old_data() - finished = purge_old_data(instance, purge_before, repack=False) + finished = purge_old_data(recorder_mock, purge_before, repack=False) assert not finished - finished = purge_old_data(instance, purge_before, repack=False) + finished = purge_old_data(recorder_mock, purge_before, repack=False) assert finished assert statistics_runs.count() == 1 @pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) +@pytest.mark.usefixtures("recorder_mock") async def test_purge_method( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, use_sqlite: bool, @@ -377,7 +373,6 @@ async def test_purge_method( assert run1.run_id == run2.run_id assert run1.start == run2.start - await async_setup_recorder_instance(hass) await async_attach_db_engine(hass) service_data = {"keep_days": 4} @@ -478,11 +473,8 @@ async def test_purge_method( @pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) -async def test_purge_edge_case( - async_setup_recorder_instance: RecorderInstanceGenerator, - hass: HomeAssistant, - use_sqlite: bool, -) -> None: +@pytest.mark.usefixtures("recorder_mock") +async def test_purge_edge_case(hass: HomeAssistant, use_sqlite: bool) -> None: """Test states and events are purged even if they occurred shortly before purge_before.""" async def _add_db_entries(hass: HomeAssistant, timestamp: datetime) -> None: @@ -515,7 +507,6 @@ async def test_purge_edge_case( ) ) - await async_setup_recorder_instance(hass, None) await async_attach_db_engine(hass) await async_wait_purge_done(hass) @@ -534,7 +525,7 @@ async def test_purge_edge_case( events = session.query(Events).filter(Events.event_type == "EVENT_TEST_PURGE") assert events.count() == 1 - await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) + await hass.services.async_call(RECORDER_DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -547,10 +538,7 @@ async def test_purge_edge_case( assert events.count() == 0 -async def test_purge_cutoff_date( - async_setup_recorder_instance: RecorderInstanceGenerator, - hass: HomeAssistant, -) -> None: +async def test_purge_cutoff_date(hass: HomeAssistant, recorder_mock: Recorder) -> None: """Test states and events are purged only if they occurred before "now() - keep_days".""" async def _add_db_entries(hass: HomeAssistant, cutoff: datetime, rows: int) -> None: @@ -614,7 +602,6 @@ async def test_purge_cutoff_date( ) ) - instance = await async_setup_recorder_instance(hass, None) await async_attach_db_engine(hass) await async_wait_purge_done(hass) @@ -643,7 +630,7 @@ async def test_purge_cutoff_date( assert events.filter(Events.event_type == "PURGE").count() == rows - 1 assert events.filter(Events.event_type == "KEEP").count() == 1 - instance.queue_task(PurgeTask(cutoff, repack=False, apply_filter=False)) + recorder_mock.queue_task(PurgeTask(cutoff, repack=False, apply_filter=False)) await hass.async_block_till_done() await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -674,7 +661,9 @@ async def test_purge_cutoff_date( assert events.filter(Events.event_type == "KEEP").count() == 1 # Make sure we can purge everything - instance.queue_task(PurgeTask(dt_util.utcnow(), repack=False, apply_filter=False)) + recorder_mock.queue_task( + PurgeTask(dt_util.utcnow(), repack=False, apply_filter=False) + ) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -685,7 +674,9 @@ async def test_purge_cutoff_date( assert state_attributes.count() == 0 # Make sure we can purge everything when the db is already empty - instance.queue_task(PurgeTask(dt_util.utcnow(), repack=False, apply_filter=False)) + recorder_mock.queue_task( + PurgeTask(dt_util.utcnow(), repack=False, apply_filter=False) + ) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -938,16 +929,15 @@ def _add_state_and_state_changed_event( async def test_purge_many_old_events( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test deleting old events.""" - instance = await async_setup_recorder_instance(hass) await async_attach_db_engine(hass) old_events_count = 5 with ( - patch.object(instance, "max_bind_vars", old_events_count), - patch.object(instance.database_engine, "max_bind_vars", old_events_count), + patch.object(recorder_mock, "max_bind_vars", old_events_count), + patch.object(recorder_mock.database_engine, "max_bind_vars", old_events_count), ): await _add_test_events(hass, old_events_count) @@ -959,7 +949,7 @@ async def test_purge_many_old_events( # run purge_old_data() finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, states_batch_size=3, @@ -970,7 +960,7 @@ async def test_purge_many_old_events( # we should only have 2 groups of events left finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, states_batch_size=3, @@ -981,7 +971,7 @@ async def test_purge_many_old_events( # we should now purge everything finished = purge_old_data( - instance, + recorder_mock, dt_util.utcnow(), repack=False, states_batch_size=20, @@ -992,23 +982,24 @@ async def test_purge_many_old_events( async def test_purge_can_mix_legacy_and_new_format( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test purging with legacy and new events.""" - instance = await async_setup_recorder_instance(hass) await async_attach_db_engine(hass) await async_wait_recording_done(hass) # New databases are no longer created with the legacy events index - assert instance.use_legacy_events_index is False + assert recorder_mock.use_legacy_events_index is False def _recreate_legacy_events_index(): """Recreate the legacy events index since its no longer created on new instances.""" - migration._create_index(instance.get_session, "states", "ix_states_event_id") - instance.use_legacy_events_index = True + migration._create_index( + recorder_mock.get_session, "states", "ix_states_event_id" + ) + recorder_mock.use_legacy_events_index = True - await instance.async_add_executor_job(_recreate_legacy_events_index) - assert instance.use_legacy_events_index is True + await recorder_mock.async_add_executor_job(_recreate_legacy_events_index) + assert recorder_mock.use_legacy_events_index is True utcnow = dt_util.utcnow() eleven_days_ago = utcnow - timedelta(days=11) @@ -1049,7 +1040,7 @@ async def test_purge_can_mix_legacy_and_new_format( purge_before = dt_util.utcnow() - timedelta(days=4) finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, ) @@ -1060,7 +1051,7 @@ async def test_purge_can_mix_legacy_and_new_format( # and we switch methods purge_before = dt_util.utcnow() - timedelta(days=4) finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, events_batch_size=1, @@ -1073,7 +1064,7 @@ async def test_purge_can_mix_legacy_and_new_format( assert states_with_event_id.count() == 0 assert states_without_event_id.count() == 1 finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, events_batch_size=100, @@ -1088,7 +1079,7 @@ async def test_purge_can_mix_legacy_and_new_format( assert states_with_event_id.count() == 0 assert states_without_event_id.count() == 2 finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, ) @@ -1102,29 +1093,29 @@ async def test_purge_can_mix_legacy_and_new_format( @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) @pytest.mark.usefixtures("skip_by_db_engine") async def test_purge_can_mix_legacy_and_new_format_with_detached_state( - async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, + recorder_mock: Recorder, recorder_db_url: str, ) -> None: """Test purging with legacy and new events with a detached state. This tests disables foreign key checks on SQLite. """ - - instance = await async_setup_recorder_instance(hass) await async_attach_db_engine(hass) await async_wait_recording_done(hass) # New databases are no longer created with the legacy events index - assert instance.use_legacy_events_index is False + assert recorder_mock.use_legacy_events_index is False def _recreate_legacy_events_index(): """Recreate the legacy events index since its no longer created on new instances.""" - migration._create_index(instance.get_session, "states", "ix_states_event_id") - instance.use_legacy_events_index = True + migration._create_index( + recorder_mock.get_session, "states", "ix_states_event_id" + ) + recorder_mock.use_legacy_events_index = True - await instance.async_add_executor_job(_recreate_legacy_events_index) - assert instance.use_legacy_events_index is True + await recorder_mock.async_add_executor_job(_recreate_legacy_events_index) + assert recorder_mock.use_legacy_events_index is True with session_scope(hass=hass) as session: session.execute(text("PRAGMA foreign_keys = OFF")) @@ -1196,7 +1187,7 @@ async def test_purge_can_mix_legacy_and_new_format_with_detached_state( purge_before = dt_util.utcnow() - timedelta(days=4) finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, ) @@ -1207,7 +1198,7 @@ async def test_purge_can_mix_legacy_and_new_format_with_detached_state( # and we switch methods purge_before = dt_util.utcnow() - timedelta(days=4) finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, events_batch_size=1, @@ -1220,7 +1211,7 @@ async def test_purge_can_mix_legacy_and_new_format_with_detached_state( assert states_with_event_id.count() == 0 assert states_without_event_id.count() == 1 finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, events_batch_size=100, @@ -1235,7 +1226,7 @@ async def test_purge_can_mix_legacy_and_new_format_with_detached_state( assert states_with_event_id.count() == 0 assert states_without_event_id.count() == 2 finished = purge_old_data( - instance, + recorder_mock, purge_before, repack=False, ) @@ -1247,11 +1238,9 @@ async def test_purge_can_mix_legacy_and_new_format_with_detached_state( async def test_purge_entities_keep_days( - async_setup_recorder_instance: RecorderInstanceGenerator, - hass: HomeAssistant, + hass: HomeAssistant, recorder_mock: Recorder ) -> None: """Test purging states with an entity filter and keep_days.""" - instance = await async_setup_recorder_instance(hass, {}) await async_attach_db_engine(hass) await hass.async_block_till_done() @@ -1275,7 +1264,7 @@ async def test_purge_entities_keep_days( hass.states.async_set("sensor.keep", "now") await async_recorder_block_till_done(hass) - states = await instance.async_add_executor_job( + states = await recorder_mock.async_add_executor_job( get_significant_states, hass, one_month_ago, @@ -1286,7 +1275,7 @@ async def test_purge_entities_keep_days( assert len(states["sensor.purge"]) == 3 await hass.services.async_call( - recorder.DOMAIN, + RECORDER_DOMAIN, SERVICE_PURGE_ENTITIES, { "entity_id": "sensor.purge", @@ -1296,7 +1285,7 @@ async def test_purge_entities_keep_days( await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) - states = await instance.async_add_executor_job( + states = await recorder_mock.async_add_executor_job( get_significant_states, hass, one_month_ago, @@ -1307,7 +1296,7 @@ async def test_purge_entities_keep_days( assert len(states["sensor.purge"]) == 1 await hass.services.async_call( - recorder.DOMAIN, + RECORDER_DOMAIN, SERVICE_PURGE_ENTITIES, { "entity_id": "sensor.purge", @@ -1316,7 +1305,7 @@ async def test_purge_entities_keep_days( await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) - states = await instance.async_add_executor_job( + states = await recorder_mock.async_add_executor_job( get_significant_states, hass, one_month_ago, From 950c72a04c89c4a0f0e7403e4bdfb0942f804743 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Thu, 4 Jul 2024 12:05:22 -0400 Subject: [PATCH 0746/2411] Bump anova-wifi to 0.15.0 (#121222) --- homeassistant/components/anova/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/anova/manifest.json b/homeassistant/components/anova/manifest.json index d75a791a6f5..7e605edc217 100644 --- a/homeassistant/components/anova/manifest.json +++ b/homeassistant/components/anova/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/anova", "iot_class": "cloud_push", "loggers": ["anova_wifi"], - "requirements": ["anova-wifi==0.14.0"] + "requirements": ["anova-wifi==0.15.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0e8be70b427..b086edd36c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ androidtvremote2==0.1.1 anel-pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.anova -anova-wifi==0.14.0 +anova-wifi==0.15.0 # homeassistant.components.anthemav anthemav==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf109a087c0..396ba24aa99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -413,7 +413,7 @@ androidtv[async]==0.0.73 androidtvremote2==0.1.1 # homeassistant.components.anova -anova-wifi==0.14.0 +anova-wifi==0.15.0 # homeassistant.components.anthemav anthemav==1.4.1 From d5135d49564c6af18058eff4edffbeafa5e29bfd Mon Sep 17 00:00:00 2001 From: jvmahon Date: Thu, 4 Jul 2024 12:29:10 -0400 Subject: [PATCH 0747/2411] Add support for the Select platform in Matter (#119769) * Add support for ModeSelect Cluster * Update discovery.py * Add files via upload * refactor part 1 * Update discovery.py * add remaining mode discovery schemas * add test * type alias --------- Co-authored-by: Marcel van der Veldt --- homeassistant/components/matter/discovery.py | 5 +- homeassistant/components/matter/select.py | 199 +++++++++ homeassistant/components/matter/strings.json | 5 + .../matter/fixtures/nodes/dimmable-light.json | 143 ++++++- .../matter/fixtures/nodes/microwave-oven.json | 405 ++++++++++++++++++ tests/components/matter/test_select.py | 109 +++++ 6 files changed, 863 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/matter/select.py create mode 100644 tests/components/matter/fixtures/nodes/microwave-oven.json create mode 100644 tests/components/matter/test_select.py diff --git a/homeassistant/components/matter/discovery.py b/homeassistant/components/matter/discovery.py index 510b77c24c7..774b67258f1 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -2,10 +2,9 @@ from __future__ import annotations -from collections.abc import Generator - from chip.clusters.Objects import ClusterAttributeDescriptor from matter_server.client.models.node import MatterEndpoint +from typing_extensions import Generator from homeassistant.const import Platform from homeassistant.core import callback @@ -19,6 +18,7 @@ from .light import DISCOVERY_SCHEMAS as LIGHT_SCHEMAS from .lock import DISCOVERY_SCHEMAS as LOCK_SCHEMAS from .models import MatterDiscoverySchema, MatterEntityInfo from .number import DISCOVERY_SCHEMAS as NUMBER_SCHEMAS +from .select import DISCOVERY_SCHEMAS as SELECT_SCHEMAS from .sensor import DISCOVERY_SCHEMAS as SENSOR_SCHEMAS from .switch import DISCOVERY_SCHEMAS as SWITCH_SCHEMAS @@ -33,6 +33,7 @@ DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = { Platform.NUMBER: NUMBER_SCHEMAS, Platform.SENSOR: SENSOR_SCHEMAS, Platform.SWITCH: SWITCH_SCHEMAS, + Platform.SELECT: SELECT_SCHEMAS, } SUPPORTED_PLATFORMS = tuple(DISCOVERY_SCHEMAS) diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py new file mode 100644 index 00000000000..bf528077b32 --- /dev/null +++ b/homeassistant/components/matter/select.py @@ -0,0 +1,199 @@ +"""Matter ModeSelect Cluster Support.""" + +from __future__ import annotations + +from chip.clusters import Objects as clusters + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory, Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import MatterEntity +from .helpers import get_matter +from .models import MatterDiscoverySchema + +type SelectCluster = ( + clusters.ModeSelect + | clusters.OvenMode + | clusters.LaundryWasherMode + | clusters.RefrigeratorAndTemperatureControlledCabinetMode + | clusters.RvcRunMode + | clusters.RvcCleanMode + | clusters.DishwasherMode + | clusters.MicrowaveOvenMode + | clusters.EnergyEvseMode + | clusters.DeviceEnergyManagementMode +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Matter ModeSelect from Config Entry.""" + matter = get_matter(hass) + matter.register_platform_handler(Platform.SELECT, async_add_entities) + + +class MatterModeSelectEntity(MatterEntity, SelectEntity): + """Representation of a select entity from Matter (Mode) Cluster attribute(s).""" + + async def async_select_option(self, option: str) -> None: + """Change the selected mode.""" + cluster: SelectCluster = self._endpoint.get_cluster( + self._entity_info.primary_attribute.cluster_id + ) + # select the mode ID from the label string + for mode in cluster.supportedModes: + if mode.label != option: + continue + await self.matter_client.send_device_command( + node_id=self._endpoint.node.node_id, + endpoint_id=self._endpoint.endpoint_id, + command=cluster.Commands.ChangeToMode(newMode=mode.mode), + ) + break + + @callback + def _update_from_device(self) -> None: + """Update from device.""" + # NOTE: cluster can be ModeSelect or a variant of that, + # such as DishwasherMode. They all have the same characteristics. + cluster: SelectCluster = self._endpoint.get_cluster( + self._entity_info.primary_attribute.cluster_id + ) + modes = {mode.mode: mode.label for mode in cluster.supportedModes} + self._attr_options = list(modes.values()) + self._attr_current_option = modes[cluster.currentMode] + # handle optional Description attribute as descriptive name for the mode + if desc := getattr(cluster, "description", None): + self._attr_name = desc + + +# Discovery schema(s) to map Matter Attributes to HA entities +DISCOVERY_SCHEMAS = [ + MatterDiscoverySchema( + platform=Platform.SELECT, + entity_description=SelectEntityDescription( + key="MatterModeSelect", + entity_category=EntityCategory.CONFIG, + translation_key="mode", + ), + entity_class=MatterModeSelectEntity, + required_attributes=( + clusters.ModeSelect.Attributes.CurrentMode, + clusters.ModeSelect.Attributes.SupportedModes, + ), + ), + MatterDiscoverySchema( + platform=Platform.SELECT, + entity_description=SelectEntityDescription( + key="MatterOvenMode", + translation_key="mode", + ), + entity_class=MatterModeSelectEntity, + required_attributes=( + clusters.OvenMode.Attributes.CurrentMode, + clusters.OvenMode.Attributes.SupportedModes, + ), + ), + MatterDiscoverySchema( + platform=Platform.SELECT, + entity_description=SelectEntityDescription( + key="MatterLaundryWasherMode", + translation_key="mode", + ), + entity_class=MatterModeSelectEntity, + required_attributes=( + clusters.LaundryWasherMode.Attributes.CurrentMode, + clusters.LaundryWasherMode.Attributes.SupportedModes, + ), + ), + MatterDiscoverySchema( + platform=Platform.SELECT, + entity_description=SelectEntityDescription( + key="MatterRefrigeratorAndTemperatureControlledCabinetMode", + translation_key="mode", + ), + entity_class=MatterModeSelectEntity, + required_attributes=( + clusters.RefrigeratorAndTemperatureControlledCabinetMode.Attributes.CurrentMode, + clusters.RefrigeratorAndTemperatureControlledCabinetMode.Attributes.SupportedModes, + ), + ), + MatterDiscoverySchema( + platform=Platform.SELECT, + entity_description=SelectEntityDescription( + key="MatterRvcRunMode", + translation_key="mode", + ), + entity_class=MatterModeSelectEntity, + required_attributes=( + clusters.RvcRunMode.Attributes.CurrentMode, + clusters.RvcRunMode.Attributes.SupportedModes, + ), + ), + MatterDiscoverySchema( + platform=Platform.SELECT, + entity_description=SelectEntityDescription( + key="MatterRvcCleanMode", + translation_key="mode", + ), + entity_class=MatterModeSelectEntity, + required_attributes=( + clusters.RvcCleanMode.Attributes.CurrentMode, + clusters.RvcCleanMode.Attributes.SupportedModes, + ), + ), + MatterDiscoverySchema( + platform=Platform.SELECT, + entity_description=SelectEntityDescription( + key="MatterDishwasherMode", + translation_key="mode", + ), + entity_class=MatterModeSelectEntity, + required_attributes=( + clusters.DishwasherMode.Attributes.CurrentMode, + clusters.DishwasherMode.Attributes.SupportedModes, + ), + ), + MatterDiscoverySchema( + platform=Platform.SELECT, + entity_description=SelectEntityDescription( + key="MatterMicrowaveOvenMode", + translation_key="mode", + ), + entity_class=MatterModeSelectEntity, + required_attributes=( + clusters.MicrowaveOvenMode.Attributes.CurrentMode, + clusters.MicrowaveOvenMode.Attributes.SupportedModes, + ), + ), + MatterDiscoverySchema( + platform=Platform.SELECT, + entity_description=SelectEntityDescription( + key="MatterEnergyEvseMode", + translation_key="mode", + ), + entity_class=MatterModeSelectEntity, + required_attributes=( + clusters.EnergyEvseMode.Attributes.CurrentMode, + clusters.EnergyEvseMode.Attributes.SupportedModes, + ), + ), + MatterDiscoverySchema( + platform=Platform.SELECT, + entity_description=SelectEntityDescription( + key="MatterDeviceEnergyManagementMode", + translation_key="mode", + ), + entity_class=MatterModeSelectEntity, + required_attributes=( + clusters.DeviceEnergyManagementMode.Attributes.CurrentMode, + clusters.DeviceEnergyManagementMode.Attributes.SupportedModes, + ), + ), +] diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 3389a4bfe81..0a823d5aa80 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -125,6 +125,11 @@ "name": "[%key:component::lock::title%]" } }, + "select": { + "mode": { + "name": "Mode" + } + }, "sensor": { "activated_carbon_filter_condition": { "name": "Activated carbon filter condition" diff --git a/tests/components/matter/fixtures/nodes/dimmable-light.json b/tests/components/matter/fixtures/nodes/dimmable-light.json index 74f132a88a9..aad0afdfdcd 100644 --- a/tests/components/matter/fixtures/nodes/dimmable-light.json +++ b/tests/components/matter/fixtures/nodes/dimmable-light.json @@ -365,7 +365,148 @@ "1/29/65533": 1, "1/29/65528": [], "1/29/65529": [], - "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "6/80/0": "LED Color", + "6/80/1": 0, + "6/80/2": [ + { + "0": "Red", + "1": 0, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Orange", + "1": 1, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Lemon", + "1": 2, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Lime", + "1": 3, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Green", + "1": 4, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Teal", + "1": 5, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Cyan", + "1": 6, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Aqua", + "1": 7, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Blue", + "1": 8, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Violet", + "1": 9, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Magenta", + "1": 10, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "Pink", + "1": 11, + "2": [ + { + "0": 0, + "1": 0 + } + ] + }, + { + "0": "White", + "1": 12, + "2": [ + { + "0": 0, + "1": 0 + } + ] + } + ], + "6/80/3": 7, + "6/80/65532": 0, + "6/80/65533": 1, + "6/80/65528": [], + "6/80/65529": [0], + "6/80/65530": [], + "6/80/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533] }, "available": true, "attribute_subscriptions": [] diff --git a/tests/components/matter/fixtures/nodes/microwave-oven.json b/tests/components/matter/fixtures/nodes/microwave-oven.json new file mode 100644 index 00000000000..ed0a4accd6a --- /dev/null +++ b/tests/components/matter/fixtures/nodes/microwave-oven.json @@ -0,0 +1,405 @@ +{ + "node_id": 157, + "date_commissioned": "2024-07-04T12:31:22.759270", + "last_interview": "2024-07-04T12:31:22.759275", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 44, 48, 49, 51, 54, 60, 62, 63], + "0/29/2": [], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 2, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 1 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 17, + "0/40/1": "Mock", + "0/40/2": 65521, + "0/40/3": "Microwave Oven", + "0/40/4": 32769, + "0/40/5": "", + "0/40/6": "**REDACTED**", + "0/40/7": 0, + "0/40/8": "TEST_VERSION", + "0/40/9": 1, + "0/40/10": "1.0", + "0/40/11": "20200101", + "0/40/12": "", + "0/40/13": "", + "0/40/14": "", + "0/40/15": "TEST_SN", + "0/40/16": false, + "0/40/18": "D5908CF5E1382F42", + "0/40/19": { + "0": 3, + "1": 65535 + }, + "0/40/20": null, + "0/40/21": 16973824, + "0/40/22": 1, + "0/40/65532": 0, + "0/40/65533": 3, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, + 22, 65528, 65529, 65531, 65532, 65533 + ], + "0/44/0": 0, + "0/44/65532": 0, + "0/44/65533": 1, + "0/44/65528": [], + "0/44/65529": [], + "0/44/65531": [0, 65528, 65529, 65531, 65532, 65533], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 2, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/49/0": 1, + "0/49/1": [ + { + "0": "ZW5kMA==", + "1": true + } + ], + "0/49/2": 0, + "0/49/3": 0, + "0/49/4": true, + "0/49/5": null, + "0/49/6": null, + "0/49/7": null, + "0/49/65532": 4, + "0/49/65533": 2, + "0/49/65528": [], + "0/49/65529": [], + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533], + "0/51/0": [ + { + "0": "vethd3cc78a", + "1": true, + "2": null, + "3": null, + "4": "RiMoOM7I", + "5": [], + "6": ["/oAAAAAAAABEIyj//jjOyA=="], + "7": 0 + }, + { + "0": "veth86f4b74", + "1": true, + "2": null, + "3": null, + "4": "ehLA7XI6", + "5": [], + "6": ["/oAAAAAAAAB4EsD//u1yOg=="], + "7": 0 + }, + { + "0": "veth36c1460", + "1": true, + "2": null, + "3": null, + "4": "0sdiwOO7", + "5": [], + "6": ["/oAAAAAAAADQx2L//sDjuw=="], + "7": 0 + }, + { + "0": "veth55a0982", + "1": true, + "2": null, + "3": null, + "4": "fuu5VpgB", + "5": [], + "6": ["/oAAAAAAAAB867n//laYAQ=="], + "7": 0 + }, + { + "0": "vethd446fa5", + "1": true, + "2": null, + "3": null, + "4": "QsY5wCp1", + "5": [], + "6": ["/oAAAAAAAABAxjn//sAqdQ=="], + "7": 0 + }, + { + "0": "vethfc6e4d6", + "1": true, + "2": null, + "3": null, + "4": "IsHWia4E", + "5": [], + "6": ["/oAAAAAAAAAgwdb//omuBA=="], + "7": 0 + }, + { + "0": "veth4b35142", + "1": true, + "2": null, + "3": null, + "4": "RizM/XJz", + "5": [], + "6": ["/oAAAAAAAABELMz//v1ycw=="], + "7": 0 + }, + { + "0": "vetha0a808d", + "1": true, + "2": null, + "3": null, + "4": "JrxkpiTq", + "5": [], + "6": ["/oAAAAAAAAAkvGT//qYk6g=="], + "7": 0 + }, + { + "0": "hassio", + "1": true, + "2": null, + "3": null, + "4": "AkL+6fKF", + "5": ["rB4gAQ=="], + "6": ["/oAAAAAAAAAAQv7//unyhQ=="], + "7": 0 + }, + { + "0": "docker0", + "1": true, + "2": null, + "3": null, + "4": "AkKzcIpP", + "5": ["rB7oAQ=="], + "6": ["/oAAAAAAAAAAQrP//nCKTw=="], + "7": 0 + }, + { + "0": "end0", + "1": true, + "2": null, + "3": null, + "4": "5F8BoroJ", + "5": ["wKgBAg=="], + "6": [ + "KgKkZACnAAHGF8Tinim+lQ==", + "/XH1Cm7wY08fhLPRgO32Uw==", + "/oAAAAAAAAAENYnD2gV25w==" + ], + "7": 2 + }, + { + "0": "lo", + "1": true, + "2": null, + "3": null, + "4": "AAAAAAAA", + "5": ["fwAAAQ=="], + "6": ["AAAAAAAAAAAAAAAAAAAAAQ=="], + "7": 0 + } + ], + "0/51/1": 1, + "0/51/2": 16, + "0/51/3": 0, + "0/51/4": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 2, + "0/51/65528": [2], + "0/51/65529": [0, 1], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ], + "0/54/0": null, + "0/54/1": null, + "0/54/2": null, + "0/54/3": null, + "0/54/4": null, + "0/54/5": null, + "0/54/6": null, + "0/54/7": null, + "0/54/8": null, + "0/54/9": null, + "0/54/10": null, + "0/54/11": null, + "0/54/12": null, + "0/54/65532": 3, + "0/54/65533": 1, + "0/54/65528": [], + "0/54/65529": [0], + "0/54/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65528, 65529, 65531, 65532, + 65533 + ], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 0, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/62/0": [ + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVASQRnRgkBwEkCAEwCUEEleMInA+X+lZO6bSa7ysHaAvYS13Fg9GoRuhiFk+wvtjLUrouyH+DUp3p3purrVdfUWTp03damVsxp9Lv48goDzcKNQEoARgkAgE2AwQCBAEYMAQUrD2d44zyVXjKbyYgNaEibaXFI7IwBRTphWiJ/NqGe3Cx3Nj8H02NgGioSRgwC0CaASOOwmsHE8cNw7FhQDtRhh0ztvwdfZKANU93vrX/+ww8UifrTjUIgvobgixpCGxmGvEmk3RN7TX6lgX4Qz7MGA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEYztrLK2UY1ORHUEFLO7PDfVjw/MnMDNX5kjdHHDU7npeITnSyg/kxxUM+pD7ccxfDuHQKHbBq9+qbJi8oGik8DcKNQEpARgkAmAwBBTphWiJ/NqGe3Cx3Nj8H02NgGioSTAFFMnf5ZkBCRaBluhSmLJkvcVXxHxTGDALQOOcZAL8XEktvE5sjrUmFNhkP2g3Ef+4BHtogItdZYyA9E/WbzW25E0UxZInwjjIzH3YimDUZVoEWGML8NV2kCEY", + "254": 1 + } + ], + "0/62/1": [ + { + "1": "BAg5aeR7RuFKZhukCxMGglCd00dKlhxGq8BbjeyZClKz5kN2Ytzav0xWsiWEEb3s9uvMIYFoQYULnSJvOMTcD14=", + "2": 65521, + "3": 1, + "4": 157, + "5": "", + "254": 1 + } + ], + "0/62/2": 16, + "0/62/3": 1, + "0/62/4": [ + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEECDlp5HtG4UpmG6QLEwaCUJ3TR0qWHEarwFuN7JkKUrPmQ3Zi3Nq/TFayJYQRvez268whgWhBhQudIm84xNwPXjcKNQEpARgkAmAwBBTJ3+WZAQkWgZboUpiyZL3FV8R8UzAFFMnf5ZkBCRaBluhSmLJkvcVXxHxTGDALQO9QSAdvJkM6b/wIc07MCw1ma46lTyGYG8nvpn0ICI73nuD3QeaWwGIQTkVGEpzF+TuDK7gtTz7YUrR+PSnvMk8Y" + ], + "0/62/5": 1, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 4, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 2, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 0, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/29/0": [ + { + "0": 121, + "1": 1 + } + ], + "1/29/1": [3, 29, 94, 95, 96], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 2, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/94/0": [ + { + "0": "Normal", + "1": 0, + "2": [ + { + "1": 16384 + } + ] + }, + { + "0": "Defrost", + "1": 1, + "2": [ + { + "1": 16385 + } + ] + } + ], + "1/94/1": 0, + "1/94/65532": 0, + "1/94/65533": 1, + "1/94/65528": [], + "1/94/65529": [], + "1/94/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/95/0": 30, + "1/95/1": 86400, + "1/95/2": 90, + "1/95/3": 20, + "1/95/4": 90, + "1/95/5": 10, + "1/95/8": 1000, + "1/95/65532": 5, + "1/95/65533": 1, + "1/95/65528": [], + "1/95/65529": [0, 1], + "1/95/65531": [0, 1, 2, 3, 4, 5, 8, 65528, 65529, 65531, 65532, 65533], + "1/96/0": null, + "1/96/1": null, + "1/96/2": 30, + "1/96/3": [ + { + "0": 0 + }, + { + "0": 1 + }, + { + "0": 2 + }, + { + "0": 3 + } + ], + "1/96/4": 0, + "1/96/5": { + "0": 0 + }, + "1/96/65532": 0, + "1/96/65533": 2, + "1/96/65528": [4], + "1/96/65529": [0, 1, 2, 3], + "1/96/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/test_select.py b/tests/components/matter/test_select.py new file mode 100644 index 00000000000..0d4d5e71b81 --- /dev/null +++ b/tests/components/matter/test_select.py @@ -0,0 +1,109 @@ +"""Test Matter select entities.""" + +from unittest.mock import MagicMock, call + +from chip.clusters import Objects as clusters +from matter_server.client.models.node import MatterNode +import pytest + +from homeassistant.core import HomeAssistant + +from .common import ( + set_node_attribute, + setup_integration_with_node_fixture, + trigger_subscription_callback, +) + + +@pytest.fixture(name="light_node") +async def dimmable_light_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a dimmable light node.""" + return await setup_integration_with_node_fixture( + hass, "dimmable-light", matter_client + ) + + +@pytest.fixture(name="microwave_oven_node") +async def microwave_oven_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a microwave oven node.""" + return await setup_integration_with_node_fixture( + hass, "microwave-oven", matter_client + ) + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_mode_select_entities( + hass: HomeAssistant, + matter_client: MagicMock, + light_node: MatterNode, +) -> None: + """Test select entities are created for the ModeSelect cluster attributes.""" + state = hass.states.get("select.mock_dimmable_light_led_color") + assert state + assert state.state == "Aqua" + assert state.attributes["options"] == [ + "Red", + "Orange", + "Lemon", + "Lime", + "Green", + "Teal", + "Cyan", + "Aqua", + "Blue", + "Violet", + "Magenta", + "Pink", + "White", + ] + # name should be derived from description attribute + assert state.attributes["friendly_name"] == "Mock Dimmable Light LED Color" + set_node_attribute(light_node, 6, 80, 3, 1) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("select.mock_dimmable_light_led_color") + assert state.state == "Orange" + # test select option + await hass.services.async_call( + "select", + "select_option", + { + "entity_id": "select.mock_dimmable_light_led_color", + "option": "Lime", + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=light_node.node_id, + endpoint_id=6, + command=clusters.ModeSelect.Commands.ChangeToMode(newMode=3), + ) + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_microwave_select_entities( + hass: HomeAssistant, + matter_client: MagicMock, + microwave_oven_node: MatterNode, +) -> None: + """Test select entities are created for the MicrowaveOvenMode cluster attributes.""" + state = hass.states.get("select.microwave_oven_mode") + assert state + assert state.state == "Normal" + assert state.attributes["options"] == [ + "Normal", + "Defrost", + ] + # name should just be Mode (from the translation key) + assert state.attributes["friendly_name"] == "Microwave Oven Mode" + set_node_attribute(microwave_oven_node, 1, 94, 1, 1) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("select.microwave_oven_mode") + assert state.state == "Defrost" From 7d5341cab286308b79a23bd760161c41f8726ce3 Mon Sep 17 00:00:00 2001 From: Patrick Koenig Date: Thu, 4 Jul 2024 12:55:30 -0400 Subject: [PATCH 0748/2411] Update short_name in web app manifest (#121223) --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index dac0f51f608..8fe3a98864b 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -189,7 +189,7 @@ MANIFEST_JSON = Manifest( ], "lang": "en-US", "name": "Home Assistant", - "short_name": "Assistant", + "short_name": "Home Assistant", "start_url": "/?homescreen=1", "id": "/?homescreen=1", "theme_color": DEFAULT_THEME_COLOR, From 79f4cc9c12fc3e8374914efb9e175e72764df70e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Jul 2024 12:57:30 -0500 Subject: [PATCH 0749/2411] Update uiprotect to 5.2.2 (#121227) --- homeassistant/components/unifiprotect/manifest.json | 2 +- homeassistant/components/unifiprotect/number.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 95fb8600135..b369b91264d 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==5.2.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==5.2.2", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index a0d360af80b..4bb5cba6f15 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -53,7 +53,7 @@ async def _set_auto_close(obj: Doorlock, value: float) -> None: def _get_chime_duration(obj: Camera) -> int: - return int(obj.chime_duration.total_seconds()) + return int(obj.chime_duration_seconds) CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( diff --git a/requirements_all.txt b/requirements_all.txt index b086edd36c4..7c7fc152fd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2792,7 +2792,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==5.2.0 +uiprotect==5.2.2 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 396ba24aa99..c6937d2e23a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2172,7 +2172,7 @@ twitchAPI==4.0.0 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==5.2.0 +uiprotect==5.2.2 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From df1b02d44e5e658a943b1abbf19f192aaacf75d6 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 4 Jul 2024 20:06:23 +0200 Subject: [PATCH 0750/2411] Use Generator from abc instead of typing_extensions in Matter discovery (#121236) Use Generator from abc instead of typing_extensions --- homeassistant/components/matter/discovery.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matter/discovery.py b/homeassistant/components/matter/discovery.py index 774b67258f1..912bf7bd7c2 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -2,9 +2,10 @@ from __future__ import annotations +from collections.abc import Generator + from chip.clusters.Objects import ClusterAttributeDescriptor from matter_server.client.models.node import MatterEndpoint -from typing_extensions import Generator from homeassistant.const import Platform from homeassistant.core import callback From e07bf61f03a92b0ab128252a243b11e99689931b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 20:24:27 +0200 Subject: [PATCH 0751/2411] Import AsyncGenerator from collections.abc in tests (#121019) * Force import of Async/Generator from collections.abc * Adjust * Don't force --- tests/components/twitch/__init__.py | 3 +-- tests/components/youtube/__init__.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/components/twitch/__init__.py b/tests/components/twitch/__init__.py index 0238bbdadba..2d70aaf9649 100644 --- a/tests/components/twitch/__init__.py +++ b/tests/components/twitch/__init__.py @@ -1,10 +1,9 @@ """Tests for the Twitch component.""" -from collections.abc import AsyncIterator +from collections.abc import AsyncGenerator, AsyncIterator from typing import Any, Generic, TypeVar from twitchAPI.object.base import TwitchObject -from typing_extensions import AsyncGenerator from homeassistant.components.twitch import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/youtube/__init__.py b/tests/components/youtube/__init__.py index 1b559f0f1c4..31125d3a71e 100644 --- a/tests/components/youtube/__init__.py +++ b/tests/components/youtube/__init__.py @@ -1,8 +1,8 @@ """Tests for the YouTube integration.""" +from collections.abc import AsyncGenerator import json -from typing_extensions import AsyncGenerator from youtubeaio.models import YouTubeChannel, YouTubePlaylistItem, YouTubeSubscription from youtubeaio.types import AuthScope From 04a6285e6208f6d60c32e21bd17f85dda8690e15 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 20:46:36 +0200 Subject: [PATCH 0752/2411] Add .coveragerc to core files (#121182) --- .core_files.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.core_files.yaml b/.core_files.yaml index 067a6a2b41d..a6f856209cc 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -116,6 +116,7 @@ components: &components # Testing related files that affect the whole test/linting suite tests: &tests + - .coveragerc - codecov.yaml - pylint/** - requirements_test_pre_commit.txt From fe0bafd06739543da8c376b4998f0e6d47f45837 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 20:48:08 +0200 Subject: [PATCH 0753/2411] Add warnings for deprecated json helpers (#121161) --- homeassistant/helpers/json.py | 32 ++++++++++++++++++-- tests/helpers/test_json.py | 57 +++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 28b3d509a0c..1145d785ed3 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -13,13 +13,39 @@ import orjson from homeassistant.util.file import write_utf8_file, write_utf8_file_atomic from homeassistant.util.json import ( # noqa: F401 - JSON_DECODE_EXCEPTIONS, - JSON_ENCODE_EXCEPTIONS, + JSON_DECODE_EXCEPTIONS as _JSON_DECODE_EXCEPTIONS, + JSON_ENCODE_EXCEPTIONS as _JSON_ENCODE_EXCEPTIONS, SerializationError, format_unserializable_data, - json_loads, + json_loads as _json_loads, ) +from .deprecation import ( + DeprecatedConstant, + all_with_deprecated_constants, + check_if_deprecated_constant, + deprecated_function, + dir_with_deprecated_constants, +) + +_DEPRECATED_JSON_DECODE_EXCEPTIONS = DeprecatedConstant( + _JSON_DECODE_EXCEPTIONS, "homeassistant.util.json.JSON_DECODE_EXCEPTIONS", "2025.8" +) +_DEPRECATED_JSON_ENCODE_EXCEPTIONS = DeprecatedConstant( + _JSON_ENCODE_EXCEPTIONS, "homeassistant.util.json.JSON_ENCODE_EXCEPTIONS", "2025.8" +) +json_loads = deprecated_function( + "homeassistant.util.json.json_loads", breaks_in_ha_version="2025.8" +)(_json_loads) + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) + + _LOGGER = logging.getLogger(__name__) diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index 061faed6f93..123731de68d 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -13,6 +13,7 @@ from unittest.mock import Mock, patch import pytest from homeassistant.core import Event, HomeAssistant, State +from homeassistant.helpers import json as json_helper from homeassistant.helpers.json import ( ExtendedJSONEncoder, JSONEncoder as DefaultHASSJSONEncoder, @@ -25,9 +26,14 @@ from homeassistant.helpers.json import ( ) from homeassistant.util import dt as dt_util from homeassistant.util.color import RGBColor -from homeassistant.util.json import SerializationError, load_json +from homeassistant.util.json import ( + JSON_DECODE_EXCEPTIONS, + JSON_ENCODE_EXCEPTIONS, + SerializationError, + load_json, +) -from tests.common import json_round_trip +from tests.common import import_and_test_deprecated_constant, json_round_trip # Test data that can be saved as JSON TEST_JSON_A = {"a": 1, "B": "two"} @@ -335,3 +341,50 @@ def test_find_unserializable_data() -> None: BadData(), dump=partial(json.dumps, cls=MockJSONEncoder), ) == {"$(BadData).bla": bad_data} + + +def test_deprecated_json_loads(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated json_loads function. + + It was moved from helpers to util in #88099 + """ + json_helper.json_loads("{}") + assert ( + "json_loads is a deprecated function which will be removed in " + "HA Core 2025.8. Use homeassistant.util.json.json_loads instead" + ) in caplog.text + + +@pytest.mark.parametrize( + ("constant_name", "replacement_name", "replacement"), + [ + ( + "JSON_DECODE_EXCEPTIONS", + "homeassistant.util.json.JSON_DECODE_EXCEPTIONS", + JSON_DECODE_EXCEPTIONS, + ), + ( + "JSON_ENCODE_EXCEPTIONS", + "homeassistant.util.json.JSON_ENCODE_EXCEPTIONS", + JSON_ENCODE_EXCEPTIONS, + ), + ], +) +def test_deprecated_aliases( + caplog: pytest.LogCaptureFixture, + constant_name: str, + replacement_name: str, + replacement: Any, +) -> None: + """Test deprecated JSON_DECODE_EXCEPTIONS and JSON_ENCODE_EXCEPTIONS constants. + + They were moved from helpers to util in #88099 + """ + import_and_test_deprecated_constant( + caplog, + json_helper, + constant_name, + replacement_name, + replacement, + "2025.8", + ) From 6ab6ce30efea9e342d34ffcd68edae2210f0bd37 Mon Sep 17 00:00:00 2001 From: Gerben Jongerius Date: Thu, 4 Jul 2024 20:51:57 +0200 Subject: [PATCH 0754/2411] Bump youless library version 2.1.2 (#121181) --- homeassistant/components/youless/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/youless/snapshots/test_sensor.ambr | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/youless/manifest.json b/homeassistant/components/youless/manifest.json index 9a81de38388..1ccc8cda0ff 100644 --- a/homeassistant/components/youless/manifest.json +++ b/homeassistant/components/youless/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/youless", "iot_class": "local_polling", "loggers": ["youless_api"], - "requirements": ["youless-api==2.1.0"] + "requirements": ["youless-api==2.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7c7fc152fd6..eff9518e247 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2948,7 +2948,7 @@ yeelightsunflower==0.0.10 yolink-api==0.4.4 # homeassistant.components.youless -youless-api==2.1.0 +youless-api==2.1.2 # homeassistant.components.youtube youtubeaio==1.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6937d2e23a..bc75be8426a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2304,7 +2304,7 @@ yeelight==0.7.14 yolink-api==0.4.4 # homeassistant.components.youless -youless-api==2.1.0 +youless-api==2.1.2 # homeassistant.components.youtube youtubeaio==1.1.5 diff --git a/tests/components/youless/snapshots/test_sensor.ambr b/tests/components/youless/snapshots/test_sensor.ambr index 22e480c390e..bcfd0139e5c 100644 --- a/tests/components/youless/snapshots/test_sensor.ambr +++ b/tests/components/youless/snapshots/test_sensor.ambr @@ -47,7 +47,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '0.0', }) # --- # name: test_sensors[sensor.energy_delivery_low-entry] @@ -98,7 +98,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '0.029', }) # --- # name: test_sensors[sensor.energy_high-entry] @@ -405,7 +405,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1234.564', + 'state': '1624.264', }) # --- # name: test_sensors[sensor.phase_1_current-entry] @@ -967,6 +967,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '1234.564', }) # --- From 10d3c3d341dd50eb84f66189e84c742af79a6b13 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 4 Jul 2024 21:03:33 +0200 Subject: [PATCH 0755/2411] Bump deebot-client to 8.1.1 (#121241) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 03f99725a6d..0dadcba7beb 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==8.1.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index eff9518e247..29d7b2c7e97 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -709,7 +709,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==8.1.0 +deebot-client==8.1.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc75be8426a..dfd9bca4a07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -590,7 +590,7 @@ dbus-fast==2.22.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==8.1.0 +deebot-client==8.1.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns From ebe7a4747d35a48269fd404a996b85295999a8af Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 4 Jul 2024 21:09:19 +0200 Subject: [PATCH 0756/2411] Bump pytrafikverket to 1.0.0 (#121210) * Update all related files and tests to new version * Fix missed modal * Bump requirements --- .../trafikverket_camera/config_flow.py | 9 +-- .../trafikverket_camera/coordinator.py | 7 +- .../trafikverket_camera/manifest.json | 2 +- .../trafikverket_ferry/coordinator.py | 4 +- .../trafikverket_ferry/manifest.json | 2 +- .../trafikverket_train/coordinator.py | 16 ++--- .../trafikverket_train/manifest.json | 2 +- .../coordinator.py | 7 +- .../trafikverket_weatherstation/manifest.json | 2 +- .../trafikverket_weatherstation/sensor.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../trafikverket_camera/conftest.py | 24 ++++--- .../trafikverket_camera/test_binary_sensor.py | 4 +- .../trafikverket_camera/test_camera.py | 4 +- .../trafikverket_camera/test_config_flow.py | 10 +-- .../trafikverket_camera/test_coordinator.py | 8 +-- .../trafikverket_camera/test_init.py | 14 ++-- .../trafikverket_camera/test_recorder.py | 4 +- .../trafikverket_camera/test_sensor.py | 4 +- .../components/trafikverket_ferry/conftest.py | 71 +++++++++++-------- .../trafikverket_ferry/test_coordinator.py | 4 +- .../trafikverket_ferry/test_init.py | 10 ++- .../trafikverket_ferry/test_sensor.py | 4 +- .../components/trafikverket_train/conftest.py | 40 +++++------ .../trafikverket_train/test_config_flow.py | 6 +- .../trafikverket_train/test_init.py | 12 ++-- .../trafikverket_train/test_sensor.py | 14 ++-- 28 files changed, 158 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/trafikverket_camera/config_flow.py b/homeassistant/components/trafikverket_camera/config_flow.py index 1c2e025ece9..501ccb7e0e0 100644 --- a/homeassistant/components/trafikverket_camera/config_flow.py +++ b/homeassistant/components/trafikverket_camera/config_flow.py @@ -6,7 +6,8 @@ from collections.abc import Mapping from typing import Any from pytrafikverket.exceptions import InvalidAuthentication, NoCameraFound, UnknownError -from pytrafikverket.trafikverket_camera import CameraInfo, TrafikverketCamera +from pytrafikverket.models import CameraInfoModel +from pytrafikverket.trafikverket_camera import TrafikverketCamera import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult @@ -29,15 +30,15 @@ class TVCameraConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 3 entry: ConfigEntry | None - cameras: list[CameraInfo] + cameras: list[CameraInfoModel] api_key: str async def validate_input( self, sensor_api: str, location: str - ) -> tuple[dict[str, str], list[CameraInfo] | None]: + ) -> tuple[dict[str, str], list[CameraInfoModel] | None]: """Validate input from user input.""" errors: dict[str, str] = {} - cameras: list[CameraInfo] | None = None + cameras: list[CameraInfoModel] | None = None web_session = async_get_clientsession(self.hass) camera_api = TrafikverketCamera(web_session, sensor_api) diff --git a/homeassistant/components/trafikverket_camera/coordinator.py b/homeassistant/components/trafikverket_camera/coordinator.py index cceea9afc5c..8ead479fd1c 100644 --- a/homeassistant/components/trafikverket_camera/coordinator.py +++ b/homeassistant/components/trafikverket_camera/coordinator.py @@ -14,7 +14,8 @@ from pytrafikverket.exceptions import ( NoCameraFound, UnknownError, ) -from pytrafikverket.trafikverket_camera import CameraInfo, TrafikverketCamera +from pytrafikverket.models import CameraInfoModel +from pytrafikverket.trafikverket_camera import TrafikverketCamera from homeassistant.const import CONF_API_KEY, CONF_ID from homeassistant.core import HomeAssistant @@ -35,7 +36,7 @@ TIME_BETWEEN_UPDATES = timedelta(minutes=5) class CameraData: """Dataclass for Camera data.""" - data: CameraInfo + data: CameraInfoModel image: bytes | None @@ -60,7 +61,7 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[CameraData]): async def _async_update_data(self) -> CameraData: """Fetch data from Trafikverket.""" - camera_data: CameraInfo + camera_data: CameraInfoModel image: bytes | None = None try: camera_data = await self._camera_api.async_get_camera(self._id) diff --git a/homeassistant/components/trafikverket_camera/manifest.json b/homeassistant/components/trafikverket_camera/manifest.json index ac8570d8a02..f424f47f7c5 100644 --- a/homeassistant/components/trafikverket_camera/manifest.json +++ b/homeassistant/components/trafikverket_camera/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/trafikverket_camera", "iot_class": "cloud_polling", "loggers": ["pytrafikverket"], - "requirements": ["pytrafikverket==0.3.10"] + "requirements": ["pytrafikverket==1.0.0"] } diff --git a/homeassistant/components/trafikverket_ferry/coordinator.py b/homeassistant/components/trafikverket_ferry/coordinator.py index 6cfed88b79c..fdde6766185 100644 --- a/homeassistant/components/trafikverket_ferry/coordinator.py +++ b/homeassistant/components/trafikverket_ferry/coordinator.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any from pytrafikverket import TrafikverketFerry from pytrafikverket.exceptions import InvalidAuthentication, NoFerryFound -from pytrafikverket.trafikverket_ferry import FerryStop +from pytrafikverket.models import FerryStopModel from homeassistant.const import CONF_API_KEY, CONF_WEEKDAY, WEEKDAYS from homeassistant.core import HomeAssistant @@ -86,7 +86,7 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator): try: routedata: list[ - FerryStop + FerryStopModel ] = await self._ferry_api.async_get_next_ferry_stops( self._from, self._to, when, 3 ) diff --git a/homeassistant/components/trafikverket_ferry/manifest.json b/homeassistant/components/trafikverket_ferry/manifest.json index 99feccf983f..0b7b056754c 100644 --- a/homeassistant/components/trafikverket_ferry/manifest.json +++ b/homeassistant/components/trafikverket_ferry/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/trafikverket_ferry", "iot_class": "cloud_polling", "loggers": ["pytrafikverket"], - "requirements": ["pytrafikverket==0.3.10"] + "requirements": ["pytrafikverket==1.0.0"] } diff --git a/homeassistant/components/trafikverket_train/coordinator.py b/homeassistant/components/trafikverket_train/coordinator.py index c202473da79..66ef3e6a1d2 100644 --- a/homeassistant/components/trafikverket_train/coordinator.py +++ b/homeassistant/components/trafikverket_train/coordinator.py @@ -13,7 +13,7 @@ from pytrafikverket.exceptions import ( NoTrainAnnouncementFound, UnknownError, ) -from pytrafikverket.trafikverket_train import StationInfo, TrainStop +from pytrafikverket.models import StationInfoModel, TrainStopModel from homeassistant.const import CONF_API_KEY, CONF_WEEKDAY from homeassistant.core import HomeAssistant @@ -35,7 +35,7 @@ class TrainData: departure_time: datetime | None departure_state: str - cancelled: bool + cancelled: bool | None delayed_time: int | None planned_time: datetime | None estimated_time: datetime | None @@ -73,8 +73,8 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[TrainData]): def __init__( self, hass: HomeAssistant, - to_station: StationInfo, - from_station: StationInfo, + to_station: StationInfoModel, + from_station: StationInfoModel, ) -> None: """Initialize the Trafikverket coordinator.""" super().__init__( @@ -86,8 +86,8 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[TrainData]): self._train_api = TrafikverketTrain( async_get_clientsession(hass), self.config_entry.data[CONF_API_KEY] ) - self.from_station: StationInfo = from_station - self.to_station: StationInfo = to_station + self.from_station: StationInfoModel = from_station + self.to_station: StationInfoModel = to_station self._time: time | None = dt_util.parse_time(self.config_entry.data[CONF_TIME]) self._weekdays: list[str] = self.config_entry.data[CONF_WEEKDAY] self._filter_product: str | None = self.config_entry.options.get( @@ -98,8 +98,8 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[TrainData]): """Fetch data from Trafikverket.""" when = dt_util.now() - state: TrainStop | None = None - states: list[TrainStop] | None = None + state: TrainStopModel | None = None + states: list[TrainStopModel] | None = None if self._time: departure_day = next_departuredate(self._weekdays) when = datetime.combine( diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index 6a09821f729..222b23dbe9a 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", "iot_class": "cloud_polling", "loggers": ["pytrafikverket"], - "requirements": ["pytrafikverket==0.3.10"] + "requirements": ["pytrafikverket==1.0.0"] } diff --git a/homeassistant/components/trafikverket_weatherstation/coordinator.py b/homeassistant/components/trafikverket_weatherstation/coordinator.py index e0319b1b932..22ecf6fc1b5 100644 --- a/homeassistant/components/trafikverket_weatherstation/coordinator.py +++ b/homeassistant/components/trafikverket_weatherstation/coordinator.py @@ -11,7 +11,8 @@ from pytrafikverket.exceptions import ( MultipleWeatherStationsFound, NoWeatherStationFound, ) -from pytrafikverket.trafikverket_weather import TrafikverketWeather, WeatherStationInfo +from pytrafikverket.models import WeatherStationInfoModel +from pytrafikverket.trafikverket_weather import TrafikverketWeather from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant @@ -28,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) TIME_BETWEEN_UPDATES = timedelta(minutes=10) -class TVDataUpdateCoordinator(DataUpdateCoordinator[WeatherStationInfo]): +class TVDataUpdateCoordinator(DataUpdateCoordinator[WeatherStationInfoModel]): """A Sensibo Data Update Coordinator.""" config_entry: TVWeatherConfigEntry @@ -46,7 +47,7 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[WeatherStationInfo]): ) self._station = self.config_entry.data[CONF_STATION] - async def _async_update_data(self) -> WeatherStationInfo: + async def _async_update_data(self) -> WeatherStationInfoModel: """Fetch data from Trafikverket.""" try: weatherdata = await self._weather_api.async_get_weather(self._station) diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index 430d240761f..85838726178 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", "iot_class": "cloud_polling", "loggers": ["pytrafikverket"], - "requirements": ["pytrafikverket==0.3.10"] + "requirements": ["pytrafikverket==1.0.0"] } diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 4bd14448546..36c6350280e 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -6,7 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import datetime -from pytrafikverket.trafikverket_weather import WeatherStationInfo +from pytrafikverket.models import WeatherStationInfoModel from homeassistant.components.sensor import ( SensorDeviceClass, @@ -47,7 +47,7 @@ PRECIPITATION_TYPE = [ class TrafikverketSensorEntityDescription(SensorEntityDescription): """Describes Trafikverket sensor entity.""" - value_fn: Callable[[WeatherStationInfo], StateType | datetime] + value_fn: Callable[[WeatherStationInfoModel], StateType | datetime] def add_utc_timezone(date_time: datetime | None) -> datetime | None: diff --git a/requirements_all.txt b/requirements_all.txt index 29d7b2c7e97..79594c458c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2358,7 +2358,7 @@ pytradfri[async]==9.0.1 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.3.10 +pytrafikverket==1.0.0 # homeassistant.components.v2c pytrydan==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dfd9bca4a07..be3fe27ddbe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1843,7 +1843,7 @@ pytradfri[async]==9.0.1 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.3.10 +pytrafikverket==1.0.0 # homeassistant.components.v2c pytrydan==0.7.0 diff --git a/tests/components/trafikverket_camera/conftest.py b/tests/components/trafikverket_camera/conftest.py index 61eebb623b2..cef85af2228 100644 --- a/tests/components/trafikverket_camera/conftest.py +++ b/tests/components/trafikverket_camera/conftest.py @@ -6,7 +6,7 @@ from datetime import datetime from unittest.mock import patch import pytest -from pytrafikverket.trafikverket_camera import CameraInfo +from pytrafikverket.models import CameraInfoModel from homeassistant.components.trafikverket_camera.const import DOMAIN from homeassistant.config_entries import SOURCE_USER @@ -21,7 +21,9 @@ from tests.test_util.aiohttp import AiohttpClientMocker @pytest.fixture(name="load_int") async def load_integration_from_entry( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, get_camera: CameraInfo + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + get_camera: CameraInfoModel, ) -> MockConfigEntry: """Set up the Trafikverket Camera integration in Home Assistant.""" aioclient_mock.get( @@ -51,10 +53,10 @@ async def load_integration_from_entry( @pytest.fixture(name="get_camera") -def fixture_get_camera() -> CameraInfo: +def fixture_get_camera() -> CameraInfoModel: """Construct Camera Mock.""" - return CameraInfo( + return CameraInfoModel( camera_name="Test Camera", camera_id="1234", active=True, @@ -72,10 +74,10 @@ def fixture_get_camera() -> CameraInfo: @pytest.fixture(name="get_camera2") -def fixture_get_camera2() -> CameraInfo: +def fixture_get_camera2() -> CameraInfoModel: """Construct Camera Mock 2.""" - return CameraInfo( + return CameraInfoModel( camera_name="Test Camera2", camera_id="5678", active=True, @@ -93,11 +95,11 @@ def fixture_get_camera2() -> CameraInfo: @pytest.fixture(name="get_cameras") -def fixture_get_cameras() -> CameraInfo: +def fixture_get_cameras() -> CameraInfoModel: """Construct Camera Mock with multiple cameras.""" return [ - CameraInfo( + CameraInfoModel( camera_name="Test Camera", camera_id="1234", active=True, @@ -112,7 +114,7 @@ def fixture_get_cameras() -> CameraInfo: status="Running", camera_type="Road", ), - CameraInfo( + CameraInfoModel( camera_name="Test Camera2", camera_id="5678", active=True, @@ -131,10 +133,10 @@ def fixture_get_cameras() -> CameraInfo: @pytest.fixture(name="get_camera_no_location") -def fixture_get_camera_no_location() -> CameraInfo: +def fixture_get_camera_no_location() -> CameraInfoModel: """Construct Camera Mock.""" - return CameraInfo( + return CameraInfoModel( camera_name="Test Camera", camera_id="1234", active=True, diff --git a/tests/components/trafikverket_camera/test_binary_sensor.py b/tests/components/trafikverket_camera/test_binary_sensor.py index 6c694f76233..6750c05772b 100644 --- a/tests/components/trafikverket_camera/test_binary_sensor.py +++ b/tests/components/trafikverket_camera/test_binary_sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import pytest -from pytrafikverket.trafikverket_camera import CameraInfo +from pytrafikverket.models import CameraInfoModel from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant async def test_sensor( hass: HomeAssistant, load_int: ConfigEntry, - get_camera: CameraInfo, + get_camera: CameraInfoModel, ) -> None: """Test the Trafikverket Camera binary sensor.""" diff --git a/tests/components/trafikverket_camera/test_camera.py b/tests/components/trafikverket_camera/test_camera.py index 1bf742b5f08..51d4563c19b 100644 --- a/tests/components/trafikverket_camera/test_camera.py +++ b/tests/components/trafikverket_camera/test_camera.py @@ -7,7 +7,7 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest -from pytrafikverket.trafikverket_camera import CameraInfo +from pytrafikverket.models import CameraInfoModel from homeassistant.components.camera import async_get_image from homeassistant.config_entries import ConfigEntry @@ -24,7 +24,7 @@ async def test_camera( freezer: FrozenDateTimeFactory, monkeypatch: pytest.MonkeyPatch, aioclient_mock: AiohttpClientMocker, - get_camera: CameraInfo, + get_camera: CameraInfoModel, ) -> None: """Test the Trafikverket Camera sensor.""" state1 = hass.states.get("camera.test_camera") diff --git a/tests/components/trafikverket_camera/test_config_flow.py b/tests/components/trafikverket_camera/test_config_flow.py index 8162db076fa..2e9e34f4c35 100644 --- a/tests/components/trafikverket_camera/test_config_flow.py +++ b/tests/components/trafikverket_camera/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from pytrafikverket.exceptions import InvalidAuthentication, NoCameraFound, UnknownError -from pytrafikverket.trafikverket_camera import CameraInfo +from pytrafikverket.models import CameraInfoModel from homeassistant import config_entries from homeassistant.components.trafikverket_camera.const import DOMAIN @@ -17,7 +17,7 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant, get_camera: CameraInfo) -> None: +async def test_form(hass: HomeAssistant, get_camera: CameraInfoModel) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -56,7 +56,9 @@ async def test_form(hass: HomeAssistant, get_camera: CameraInfo) -> None: async def test_form_multiple_cameras( - hass: HomeAssistant, get_cameras: list[CameraInfo], get_camera2: CameraInfo + hass: HomeAssistant, + get_cameras: list[CameraInfoModel], + get_camera2: CameraInfoModel, ) -> None: """Test we get the form with multiple cameras.""" @@ -108,7 +110,7 @@ async def test_form_multiple_cameras( async def test_form_no_location_data( - hass: HomeAssistant, get_camera_no_location: CameraInfo + hass: HomeAssistant, get_camera_no_location: CameraInfoModel ) -> None: """Test we get the form.""" diff --git a/tests/components/trafikverket_camera/test_coordinator.py b/tests/components/trafikverket_camera/test_coordinator.py index 3f37ad05575..f50ab56724e 100644 --- a/tests/components/trafikverket_camera/test_coordinator.py +++ b/tests/components/trafikverket_camera/test_coordinator.py @@ -11,9 +11,9 @@ from pytrafikverket.exceptions import ( NoCameraFound, UnknownError, ) +from pytrafikverket.models import CameraInfoModel from homeassistant.components.trafikverket_camera.const import DOMAIN -from homeassistant.components.trafikverket_camera.coordinator import CameraData from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -28,7 +28,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_coordinator( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - get_camera: CameraData, + get_camera: CameraInfoModel, ) -> None: """Test the Trafikverket Camera coordinator.""" aioclient_mock.get( @@ -86,7 +86,7 @@ async def test_coordinator( async def test_coordinator_failed_update( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - get_camera: CameraData, + get_camera: CameraInfoModel, sideeffect: str, p_error: Exception, entry_state: str, @@ -123,7 +123,7 @@ async def test_coordinator_failed_update( async def test_coordinator_failed_get_image( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - get_camera: CameraData, + get_camera: CameraInfoModel, ) -> None: """Test the Trafikverket Camera coordinator.""" aioclient_mock.get( diff --git a/tests/components/trafikverket_camera/test_init.py b/tests/components/trafikverket_camera/test_init.py index f21d36fda27..aaa4c3cfed7 100644 --- a/tests/components/trafikverket_camera/test_init.py +++ b/tests/components/trafikverket_camera/test_init.py @@ -7,7 +7,7 @@ from unittest.mock import patch import pytest from pytrafikverket.exceptions import UnknownError -from pytrafikverket.trafikverket_camera import CameraInfo +from pytrafikverket.models import CameraInfoModel from homeassistant.components.trafikverket_camera import async_migrate_entry from homeassistant.components.trafikverket_camera.const import DOMAIN @@ -23,7 +23,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_setup_entry( hass: HomeAssistant, - get_camera: CameraInfo, + get_camera: CameraInfoModel, aioclient_mock: AiohttpClientMocker, ) -> None: """Test setup entry.""" @@ -55,7 +55,7 @@ async def test_setup_entry( async def test_unload_entry( hass: HomeAssistant, - get_camera: CameraInfo, + get_camera: CameraInfoModel, aioclient_mock: AiohttpClientMocker, ) -> None: """Test unload an entry.""" @@ -89,7 +89,7 @@ async def test_unload_entry( async def test_migrate_entry( hass: HomeAssistant, - get_camera: CameraInfo, + get_camera: CameraInfoModel, aioclient_mock: AiohttpClientMocker, ) -> None: """Test migrate entry to version 2.""" @@ -136,7 +136,7 @@ async def test_migrate_entry( ) async def test_migrate_entry_fails_with_error( hass: HomeAssistant, - get_camera: CameraInfo, + get_camera: CameraInfoModel, aioclient_mock: AiohttpClientMocker, version: int, unique_id: str, @@ -205,7 +205,7 @@ async def test_migrate_entry_fails_no_id( ) entry.add_to_hass(hass) - _camera = CameraInfo( + _camera = CameraInfoModel( camera_name="Test_camera", camera_id=None, active=True, @@ -236,7 +236,7 @@ async def test_migrate_entry_fails_no_id( async def test_no_migration_needed( hass: HomeAssistant, - get_camera: CameraInfo, + get_camera: CameraInfoModel, aioclient_mock: AiohttpClientMocker, ) -> None: """Test migrate entry fails, camera returns no id.""" diff --git a/tests/components/trafikverket_camera/test_recorder.py b/tests/components/trafikverket_camera/test_recorder.py index 23ebd3f2189..d9778ab851a 100644 --- a/tests/components/trafikverket_camera/test_recorder.py +++ b/tests/components/trafikverket_camera/test_recorder.py @@ -3,7 +3,7 @@ from __future__ import annotations import pytest -from pytrafikverket.trafikverket_camera import CameraInfo +from pytrafikverket.models import CameraInfoModel from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states @@ -22,7 +22,7 @@ async def test_exclude_attributes( load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch, aioclient_mock: AiohttpClientMocker, - get_camera: CameraInfo, + get_camera: CameraInfoModel, ) -> None: """Test camera has description and location excluded from recording.""" state1 = hass.states.get("camera.test_camera") diff --git a/tests/components/trafikverket_camera/test_sensor.py b/tests/components/trafikverket_camera/test_sensor.py index 18ccbe56070..0f4ef02a850 100644 --- a/tests/components/trafikverket_camera/test_sensor.py +++ b/tests/components/trafikverket_camera/test_sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import pytest -from pytrafikverket.trafikverket_camera import CameraInfo +from pytrafikverket.models import CameraInfoModel from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant async def test_sensor( hass: HomeAssistant, load_int: ConfigEntry, - get_camera: CameraInfo, + get_camera: CameraInfoModel, ) -> None: """Test the Trafikverket Camera sensor.""" diff --git a/tests/components/trafikverket_ferry/conftest.py b/tests/components/trafikverket_ferry/conftest.py index 3491b8474af..99f3ad10636 100644 --- a/tests/components/trafikverket_ferry/conftest.py +++ b/tests/components/trafikverket_ferry/conftest.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta from unittest.mock import patch import pytest -from pytrafikverket.trafikverket_ferry import FerryStop +from pytrafikverket.models import FerryStopModel from homeassistant.components.trafikverket_ferry.const import DOMAIN from homeassistant.config_entries import SOURCE_USER @@ -20,7 +20,7 @@ from tests.common import MockConfigEntry @pytest.fixture(name="load_int") async def load_integration_from_entry( - hass: HomeAssistant, get_ferries: list[FerryStop] + hass: HomeAssistant, get_ferries: list[FerryStopModel] ) -> MockConfigEntry: """Set up the Trafikverket Ferry integration in Home Assistant.""" config_entry = MockConfigEntry( @@ -44,40 +44,51 @@ async def load_integration_from_entry( @pytest.fixture(name="get_ferries") -def fixture_get_ferries() -> list[FerryStop]: +def fixture_get_ferries() -> list[FerryStopModel]: """Construct FerryStop Mock.""" - depart1 = FerryStop( - "13", - False, - datetime(dt_util.now().year + 1, 5, 1, 12, 0, tzinfo=dt_util.UTC), - [""], - "0", - datetime(dt_util.now().year, 5, 1, 12, 0, tzinfo=dt_util.UTC), - "Harbor 1", - "Harbor 2", + depart1 = FerryStopModel( + ferry_stop_id="13", + ferry_stop_name="Harbor1lane", + short_name="Harle", + deleted=False, + departure_time=datetime( + dt_util.now().year + 1, 5, 1, 12, 0, tzinfo=dt_util.UTC + ), + other_information=[""], + deviation_id="0", + modified_time=datetime(dt_util.now().year, 5, 1, 12, 0, tzinfo=dt_util.UTC), + from_harbor_name="Harbor 1", + to_harbor_name="Harbor 2", + type_name="Turnaround", ) - depart2 = FerryStop( - "14", - False, - datetime(dt_util.now().year + 1, 5, 1, 12, 0, tzinfo=dt_util.UTC) + depart2 = FerryStopModel( + ferry_stop_id="14", + ferry_stop_name="Harbor1lane", + short_name="Harle", + deleted=False, + departure_time=datetime(dt_util.now().year + 1, 5, 1, 12, 0, tzinfo=dt_util.UTC) + timedelta(minutes=15), - [""], - "0", - datetime(dt_util.now().year, 5, 1, 12, 0, tzinfo=dt_util.UTC), - "Harbor 1", - "Harbor 2", + other_information=[""], + deviation_id="0", + modified_time=datetime(dt_util.now().year, 5, 1, 12, 0, tzinfo=dt_util.UTC), + from_harbor_name="Harbor 1", + to_harbor_name="Harbor 2", + type_name="Turnaround", ) - depart3 = FerryStop( - "15", - False, - datetime(dt_util.now().year + 1, 5, 1, 12, 0, tzinfo=dt_util.UTC) + depart3 = FerryStopModel( + ferry_stop_id="15", + ferry_stop_name="Harbor1lane", + short_name="Harle", + deleted=False, + departure_time=datetime(dt_util.now().year + 1, 5, 1, 12, 0, tzinfo=dt_util.UTC) + timedelta(minutes=30), - [""], - "0", - datetime(dt_util.now().year, 5, 1, 12, 0, tzinfo=dt_util.UTC), - "Harbor 1", - "Harbor 2", + other_information=[""], + deviation_id="0", + modified_time=datetime(dt_util.now().year, 5, 1, 12, 0, tzinfo=dt_util.UTC), + from_harbor_name="Harbor 1", + to_harbor_name="Harbor 2", + type_name="Turnaround", ) return [depart1, depart2, depart3] diff --git a/tests/components/trafikverket_ferry/test_coordinator.py b/tests/components/trafikverket_ferry/test_coordinator.py index ef6329bfd82..ae9a8fc3626 100644 --- a/tests/components/trafikverket_ferry/test_coordinator.py +++ b/tests/components/trafikverket_ferry/test_coordinator.py @@ -8,7 +8,7 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest from pytrafikverket.exceptions import InvalidAuthentication, NoFerryFound -from pytrafikverket.trafikverket_ferry import FerryStop +from pytrafikverket.models import FerryStopModel from homeassistant.components.trafikverket_ferry.const import DOMAIN from homeassistant.components.trafikverket_ferry.coordinator import next_departuredate @@ -27,7 +27,7 @@ async def test_coordinator( hass: HomeAssistant, freezer: FrozenDateTimeFactory, monkeypatch: pytest.MonkeyPatch, - get_ferries: list[FerryStop], + get_ferries: list[FerryStopModel], ) -> None: """Test the Trafikverket Ferry coordinator.""" entry = MockConfigEntry( diff --git a/tests/components/trafikverket_ferry/test_init.py b/tests/components/trafikverket_ferry/test_init.py index 22ada7e0f40..827711363ff 100644 --- a/tests/components/trafikverket_ferry/test_init.py +++ b/tests/components/trafikverket_ferry/test_init.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import patch -from pytrafikverket.trafikverket_ferry import FerryStop +from pytrafikverket.models import FerryStopModel from homeassistant.components.trafikverket_ferry.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState @@ -15,7 +15,9 @@ from . import ENTRY_CONFIG from tests.common import MockConfigEntry -async def test_setup_entry(hass: HomeAssistant, get_ferries: list[FerryStop]) -> None: +async def test_setup_entry( + hass: HomeAssistant, get_ferries: list[FerryStopModel] +) -> None: """Test setup entry.""" entry = MockConfigEntry( domain=DOMAIN, @@ -37,7 +39,9 @@ async def test_setup_entry(hass: HomeAssistant, get_ferries: list[FerryStop]) -> assert len(mock_tvt_ferry.mock_calls) == 1 -async def test_unload_entry(hass: HomeAssistant, get_ferries: list[FerryStop]) -> None: +async def test_unload_entry( + hass: HomeAssistant, get_ferries: list[FerryStopModel] +) -> None: """Test unload an entry.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/trafikverket_ferry/test_sensor.py b/tests/components/trafikverket_ferry/test_sensor.py index fc8fa557714..bc5510b0b1d 100644 --- a/tests/components/trafikverket_ferry/test_sensor.py +++ b/tests/components/trafikverket_ferry/test_sensor.py @@ -6,7 +6,7 @@ from datetime import timedelta from unittest.mock import patch import pytest -from pytrafikverket.trafikverket_ferry import FerryStop +from pytrafikverket.models import FerryStopModel from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -19,7 +19,7 @@ async def test_sensor( hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch, - get_ferries: list[FerryStop], + get_ferries: list[FerryStopModel], ) -> None: """Test the Trafikverket Ferry sensor.""" state1 = hass.states.get("sensor.harbor1_departure_from") diff --git a/tests/components/trafikverket_train/conftest.py b/tests/components/trafikverket_train/conftest.py index 7221d96bae2..4915635e316 100644 --- a/tests/components/trafikverket_train/conftest.py +++ b/tests/components/trafikverket_train/conftest.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta from unittest.mock import patch import pytest -from pytrafikverket.trafikverket_train import TrainStop +from pytrafikverket.models import TrainStopModel from homeassistant.components.trafikverket_train.const import DOMAIN from homeassistant.config_entries import SOURCE_USER @@ -21,8 +21,8 @@ from tests.common import MockConfigEntry @pytest.fixture(name="load_int") async def load_integration_from_entry( hass: HomeAssistant, - get_trains: list[TrainStop], - get_train_stop: TrainStop, + get_trains: list[TrainStopModel], + get_train_stop: TrainStopModel, ) -> MockConfigEntry: """Set up the Trafikverket Train integration in Home Assistant.""" @@ -69,11 +69,11 @@ async def load_integration_from_entry( @pytest.fixture(name="get_trains") -def fixture_get_trains() -> list[TrainStop]: +def fixture_get_trains() -> list[TrainStopModel]: """Construct TrainStop Mock.""" - depart1 = TrainStop( - id=13, + depart1 = TrainStopModel( + train_stop_id=13, canceled=False, advertised_time_at_location=datetime(2023, 5, 1, 12, 0, tzinfo=dt_util.UTC), estimated_time_at_location=datetime(2023, 5, 1, 12, 0, tzinfo=dt_util.UTC), @@ -83,8 +83,8 @@ def fixture_get_trains() -> list[TrainStop]: modified_time=datetime(2023, 5, 1, 12, 0, tzinfo=dt_util.UTC), product_description=["Regionaltåg"], ) - depart2 = TrainStop( - id=14, + depart2 = TrainStopModel( + train_stop_id=14, canceled=False, advertised_time_at_location=datetime(2023, 5, 1, 12, 0, tzinfo=dt_util.UTC) + timedelta(minutes=15), @@ -95,8 +95,8 @@ def fixture_get_trains() -> list[TrainStop]: modified_time=datetime(2023, 5, 1, 12, 0, tzinfo=dt_util.UTC), product_description=["Regionaltåg"], ) - depart3 = TrainStop( - id=15, + depart3 = TrainStopModel( + train_stop_id=15, canceled=False, advertised_time_at_location=datetime(2023, 5, 1, 12, 0, tzinfo=dt_util.UTC) + timedelta(minutes=30), @@ -112,11 +112,11 @@ def fixture_get_trains() -> list[TrainStop]: @pytest.fixture(name="get_trains_next") -def fixture_get_trains_next() -> list[TrainStop]: +def fixture_get_trains_next() -> list[TrainStopModel]: """Construct TrainStop Mock.""" - depart1 = TrainStop( - id=13, + depart1 = TrainStopModel( + train_stop_id=13, canceled=False, advertised_time_at_location=datetime(2023, 5, 1, 17, 0, tzinfo=dt_util.UTC), estimated_time_at_location=datetime(2023, 5, 1, 17, 0, tzinfo=dt_util.UTC), @@ -126,8 +126,8 @@ def fixture_get_trains_next() -> list[TrainStop]: modified_time=datetime(2023, 5, 1, 12, 0, tzinfo=dt_util.UTC), product_description=["Regionaltåg"], ) - depart2 = TrainStop( - id=14, + depart2 = TrainStopModel( + train_stop_id=14, canceled=False, advertised_time_at_location=datetime(2023, 5, 1, 17, 0, tzinfo=dt_util.UTC) + timedelta(minutes=15), @@ -138,8 +138,8 @@ def fixture_get_trains_next() -> list[TrainStop]: modified_time=datetime(2023, 5, 1, 12, 0, tzinfo=dt_util.UTC), product_description=["Regionaltåg"], ) - depart3 = TrainStop( - id=15, + depart3 = TrainStopModel( + train_stop_id=15, canceled=False, advertised_time_at_location=datetime(2023, 5, 1, 17, 0, tzinfo=dt_util.UTC) + timedelta(minutes=30), @@ -155,11 +155,11 @@ def fixture_get_trains_next() -> list[TrainStop]: @pytest.fixture(name="get_train_stop") -def fixture_get_train_stop() -> TrainStop: +def fixture_get_train_stop() -> TrainStopModel: """Construct TrainStop Mock.""" - return TrainStop( - id=13, + return TrainStopModel( + train_stop_id=13, canceled=False, advertised_time_at_location=datetime(2023, 5, 1, 11, 0, tzinfo=dt_util.UTC), estimated_time_at_location=None, diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py index a6ba82a85bc..400f396d355 100644 --- a/tests/components/trafikverket_train/test_config_flow.py +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -12,7 +12,7 @@ from pytrafikverket.exceptions import ( NoTrainStationFound, UnknownError, ) -from pytrafikverket.trafikverket_train import TrainStop +from pytrafikverket.models import TrainStopModel from homeassistant import config_entries from homeassistant.components.trafikverket_train.const import ( @@ -479,8 +479,8 @@ async def test_reauth_flow_error_departures( async def test_options_flow( hass: HomeAssistant, - get_trains: list[TrainStop], - get_train_stop: TrainStop, + get_trains: list[TrainStopModel], + get_train_stop: TrainStopModel, ) -> None: """Test a reauthentication flow.""" entry = MockConfigEntry( diff --git a/tests/components/trafikverket_train/test_init.py b/tests/components/trafikverket_train/test_init.py index 329d8d716d0..06598297dd1 100644 --- a/tests/components/trafikverket_train/test_init.py +++ b/tests/components/trafikverket_train/test_init.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import patch from pytrafikverket.exceptions import InvalidAuthentication, NoTrainStationFound -from pytrafikverket.trafikverket_train import TrainStop +from pytrafikverket.models import TrainStopModel from syrupy.assertion import SnapshotAssertion from homeassistant.components.trafikverket_train.const import DOMAIN @@ -18,7 +18,9 @@ from . import ENTRY_CONFIG, OPTIONS_CONFIG from tests.common import MockConfigEntry -async def test_unload_entry(hass: HomeAssistant, get_trains: list[TrainStop]) -> None: +async def test_unload_entry( + hass: HomeAssistant, get_trains: list[TrainStopModel] +) -> None: """Test unload an entry.""" entry = MockConfigEntry( domain=DOMAIN, @@ -52,7 +54,7 @@ async def test_unload_entry(hass: HomeAssistant, get_trains: list[TrainStop]) -> async def test_auth_failed( hass: HomeAssistant, - get_trains: list[TrainStop], + get_trains: list[TrainStopModel], snapshot: SnapshotAssertion, ) -> None: """Test authentication failed.""" @@ -82,7 +84,7 @@ async def test_auth_failed( async def test_no_stations( hass: HomeAssistant, - get_trains: list[TrainStop], + get_trains: list[TrainStopModel], snapshot: SnapshotAssertion, ) -> None: """Test stations are missing.""" @@ -108,7 +110,7 @@ async def test_no_stations( async def test_migrate_entity_unique_id( hass: HomeAssistant, - get_trains: list[TrainStop], + get_trains: list[TrainStopModel], snapshot: SnapshotAssertion, entity_registry: EntityRegistry, ) -> None: diff --git a/tests/components/trafikverket_train/test_sensor.py b/tests/components/trafikverket_train/test_sensor.py index f21561dd287..f4da3526cb2 100644 --- a/tests/components/trafikverket_train/test_sensor.py +++ b/tests/components/trafikverket_train/test_sensor.py @@ -8,7 +8,7 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest from pytrafikverket.exceptions import InvalidAuthentication, NoTrainAnnouncementFound -from pytrafikverket.trafikverket_train import TrainStop +from pytrafikverket.models import TrainStopModel from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry @@ -23,8 +23,8 @@ async def test_sensor_next( hass: HomeAssistant, freezer: FrozenDateTimeFactory, load_int: ConfigEntry, - get_trains_next: list[TrainStop], - get_train_stop: TrainStop, + get_trains_next: list[TrainStopModel], + get_train_stop: TrainStopModel, snapshot: SnapshotAssertion, ) -> None: """Test the Trafikverket Train sensor.""" @@ -70,7 +70,7 @@ async def test_sensor_single_stop( hass: HomeAssistant, freezer: FrozenDateTimeFactory, load_int: ConfigEntry, - get_trains_next: list[TrainStop], + get_trains_next: list[TrainStopModel], snapshot: SnapshotAssertion, ) -> None: """Test the Trafikverket Train sensor.""" @@ -86,7 +86,7 @@ async def test_sensor_update_auth_failure( hass: HomeAssistant, freezer: FrozenDateTimeFactory, load_int: ConfigEntry, - get_trains_next: list[TrainStop], + get_trains_next: list[TrainStopModel], snapshot: SnapshotAssertion, ) -> None: """Test the Trafikverket Train sensor with authentication update failure.""" @@ -119,7 +119,7 @@ async def test_sensor_update_failure( hass: HomeAssistant, freezer: FrozenDateTimeFactory, load_int: ConfigEntry, - get_trains_next: list[TrainStop], + get_trains_next: list[TrainStopModel], snapshot: SnapshotAssertion, ) -> None: """Test the Trafikverket Train sensor with update failure.""" @@ -149,7 +149,7 @@ async def test_sensor_update_failure_no_state( hass: HomeAssistant, freezer: FrozenDateTimeFactory, load_int: ConfigEntry, - get_trains_next: list[TrainStop], + get_trains_next: list[TrainStopModel], snapshot: SnapshotAssertion, ) -> None: """Test the Trafikverket Train sensor with update failure from empty state.""" From df7be501d3c7e93df919dac06dcb110f95f69eb9 Mon Sep 17 00:00:00 2001 From: Matthew FitzGerald-Chamberlain Date: Thu, 4 Jul 2024 14:27:56 -0500 Subject: [PATCH 0757/2411] Fix AprilAire case (#120895) * Fix AprilAire case * Fix test --- homeassistant/components/aprilaire/config_flow.py | 2 +- homeassistant/components/aprilaire/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- tests/components/aprilaire/test_config_flow.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/aprilaire/config_flow.py b/homeassistant/components/aprilaire/config_flow.py index 4acc1b9dd9e..f6c33f75e53 100644 --- a/homeassistant/components/aprilaire/config_flow.py +++ b/homeassistant/components/aprilaire/config_flow.py @@ -62,7 +62,7 @@ class AprilaireConfigFlow(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() - return self.async_create_entry(title="Aprilaire", data=user_input) + return self.async_create_entry(title="AprilAire", data=user_input) return self.async_show_form( step_id="user", diff --git a/homeassistant/components/aprilaire/manifest.json b/homeassistant/components/aprilaire/manifest.json index 3cc44786989..179a101885b 100644 --- a/homeassistant/components/aprilaire/manifest.json +++ b/homeassistant/components/aprilaire/manifest.json @@ -1,6 +1,6 @@ { "domain": "aprilaire", - "name": "Aprilaire", + "name": "AprilAire", "codeowners": ["@chamberlain2007"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aprilaire", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index b2ff70eefe1..701650f14a5 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -396,7 +396,7 @@ "iot_class": "cloud_push" }, "aprilaire": { - "name": "Aprilaire", + "name": "AprilAire", "integration_type": "device", "config_flow": true, "iot_class": "local_push" diff --git a/tests/components/aprilaire/test_config_flow.py b/tests/components/aprilaire/test_config_flow.py index c9cba2b3fd6..e4b7c167256 100644 --- a/tests/components/aprilaire/test_config_flow.py +++ b/tests/components/aprilaire/test_config_flow.py @@ -104,7 +104,7 @@ async def test_config_flow_data(client: AprilaireClient, hass: HomeAssistant) -> abort_if_unique_id_configured_mock.assert_called_once() create_entry_mock.assert_called_once_with( - title="Aprilaire", + title="AprilAire", data={ "host": "localhost", "port": 7000, From 0afebf744f8c43435ba47fc5cc2b1c3bb433c955 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Thu, 4 Jul 2024 21:30:02 +0200 Subject: [PATCH 0758/2411] Add MINI Connected virtual integration (#120874) --- homeassistant/components/mini_connected/__init__.py | 1 + homeassistant/components/mini_connected/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 homeassistant/components/mini_connected/__init__.py create mode 100644 homeassistant/components/mini_connected/manifest.json diff --git a/homeassistant/components/mini_connected/__init__.py b/homeassistant/components/mini_connected/__init__.py new file mode 100644 index 00000000000..4f0af581f58 --- /dev/null +++ b/homeassistant/components/mini_connected/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: MINI Connected.""" diff --git a/homeassistant/components/mini_connected/manifest.json b/homeassistant/components/mini_connected/manifest.json new file mode 100644 index 00000000000..dfe9a64c9e0 --- /dev/null +++ b/homeassistant/components/mini_connected/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "mini_connected", + "name": "MINI Connected", + "integration_type": "virtual", + "supported_by": "bmw_connected_drive" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 701650f14a5..628a8735cf1 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3701,6 +3701,11 @@ "config_flow": true, "iot_class": "local_polling" }, + "mini_connected": { + "name": "MINI Connected", + "integration_type": "virtual", + "supported_by": "bmw_connected_drive" + }, "minio": { "name": "Minio", "integration_type": "hub", From 50cc31e9cc8a7c42d8dc3bd09cd66cfab907c524 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Thu, 4 Jul 2024 16:36:31 -0300 Subject: [PATCH 0759/2411] Add device class translations in Random (#120890) --- homeassistant/components/random/strings.json | 90 ++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/homeassistant/components/random/strings.json b/homeassistant/components/random/strings.json index 0faad1d8093..98072a21fe1 100644 --- a/homeassistant/components/random/strings.json +++ b/homeassistant/components/random/strings.json @@ -44,5 +44,95 @@ "title": "[%key:component::random::config::step::sensor::title%]" } } + }, + + "selector": { + "binary_sensor_device_class": { + "options": { + "battery": "[%key:component::binary_sensor::entity_component::battery::name%]", + "battery_charging": "[%key:component::binary_sensor::entity_component::battery_charging::name%]", + "carbon_monoxide": "[%key:component::binary_sensor::entity_component::carbon_monoxide::name%]", + "connectivity": "[%key:component::binary_sensor::entity_component::connectivity::name%]", + "cold": "[%key:component::binary_sensor::entity_component::cold::name%]", + "door": "[%key:component::binary_sensor::entity_component::door::name%]", + "garage_door": "[%key:component::binary_sensor::entity_component::garage_door::name%]", + "gas": "[%key:component::binary_sensor::entity_component::gas::name%]", + "heat": "[%key:component::binary_sensor::entity_component::heat::name%]", + "light": "[%key:component::binary_sensor::entity_component::light::name%]", + "lock": "[%key:component::binary_sensor::entity_component::lock::name%]", + "moisture": "[%key:component::binary_sensor::entity_component::moisture::name%]", + "motion": "[%key:component::binary_sensor::entity_component::motion::name%]", + "moving": "[%key:component::binary_sensor::entity_component::moving::name%]", + "occupancy": "[%key:component::binary_sensor::entity_component::occupancy::name%]", + "opening": "[%key:component::binary_sensor::entity_component::opening::name%]", + "plug": "[%key:component::binary_sensor::entity_component::plug::name%]", + "power": "[%key:component::binary_sensor::entity_component::power::name%]", + "presence": "[%key:component::binary_sensor::entity_component::presence::name%]", + "problem": "[%key:component::binary_sensor::entity_component::problem::name%]", + "running": "[%key:component::binary_sensor::entity_component::running::name%]", + "safety": "[%key:component::binary_sensor::entity_component::safety::name%]", + "smoke": "[%key:component::binary_sensor::entity_component::smoke::name%]", + "sound": "[%key:component::binary_sensor::entity_component::sound::name%]", + "tamper": "[%key:component::binary_sensor::entity_component::tamper::name%]", + "update": "[%key:component::binary_sensor::entity_component::update::name%]", + "vibration": "[%key:component::binary_sensor::entity_component::vibration::name%]", + "window": "[%key:component::binary_sensor::entity_component::window::name%]" + } + }, + "sensor_device_class": { + "options": { + "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", + "aqi": "[%key:component::sensor::entity_component::aqi::name%]", + "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", + "battery": "[%key:component::sensor::entity_component::battery::name%]", + "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", + "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", + "current": "[%key:component::sensor::entity_component::current::name%]", + "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", + "data_size": "[%key:component::sensor::entity_component::data_size::name%]", + "date": "[%key:component::sensor::entity_component::date::name%]", + "distance": "[%key:component::sensor::entity_component::distance::name%]", + "duration": "[%key:component::sensor::entity_component::duration::name%]", + "energy": "[%key:component::sensor::entity_component::energy::name%]", + "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", + "frequency": "[%key:component::sensor::entity_component::frequency::name%]", + "gas": "[%key:component::sensor::entity_component::gas::name%]", + "humidity": "[%key:component::sensor::entity_component::humidity::name%]", + "illuminance": "[%key:component::sensor::entity_component::illuminance::name%]", + "irradiance": "[%key:component::sensor::entity_component::irradiance::name%]", + "moisture": "[%key:component::sensor::entity_component::moisture::name%]", + "monetary": "[%key:component::sensor::entity_component::monetary::name%]", + "nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]", + "nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]", + "nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]", + "ozone": "[%key:component::sensor::entity_component::ozone::name%]", + "ph": "[%key:component::sensor::entity_component::ph::name%]", + "pm1": "[%key:component::sensor::entity_component::pm1::name%]", + "pm10": "[%key:component::sensor::entity_component::pm10::name%]", + "pm25": "[%key:component::sensor::entity_component::pm25::name%]", + "power": "[%key:component::sensor::entity_component::power::name%]", + "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", + "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", + "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", + "pressure": "[%key:component::sensor::entity_component::pressure::name%]", + "reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]", + "signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]", + "sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]", + "speed": "[%key:component::sensor::entity_component::speed::name%]", + "sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]", + "temperature": "[%key:component::sensor::entity_component::temperature::name%]", + "timestamp": "[%key:component::sensor::entity_component::timestamp::name%]", + "volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "voltage": "[%key:component::sensor::entity_component::voltage::name%]", + "volume": "[%key:component::sensor::entity_component::volume::name%]", + "volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]", + "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", + "water": "[%key:component::sensor::entity_component::water::name%]", + "weight": "[%key:component::sensor::entity_component::weight::name%]", + "wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]" + } + } } } From 001bb3a04e952905a5323d82b22cac41fc3a9eec Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 4 Jul 2024 21:40:25 +0200 Subject: [PATCH 0760/2411] Bump `nettigo_air_monitor` to version 3.3.0 (#120901) * Bump nam library * Update snaphots (increasing accuracy) * Update lib and snapshot --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/nam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../nam/snapshots/test_diagnostics.ambr | 40 +++++++++---------- .../components/nam/snapshots/test_sensor.ambr | 40 +++++++++---------- tests/components/nam/test_init.py | 2 +- tests/components/nam/test_sensor.py | 6 +-- 7 files changed, 47 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index 3b6dba65325..7b37d1f7ede 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["nettigo_air_monitor"], "quality_scale": "platinum", - "requirements": ["nettigo-air-monitor==3.2.0"], + "requirements": ["nettigo-air-monitor==3.3.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 79594c458c1..feeddae7386 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1392,7 +1392,7 @@ netdata==1.1.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==3.2.0 +nettigo-air-monitor==3.3.0 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be3fe27ddbe..a48e25b0f65 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1134,7 +1134,7 @@ nessclient==1.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==3.2.0 +nettigo-air-monitor==3.3.0 # homeassistant.components.nexia nexia==2.0.8 diff --git a/tests/components/nam/snapshots/test_diagnostics.ambr b/tests/components/nam/snapshots/test_diagnostics.ambr index c187dec2866..e92e02fa1d8 100644 --- a/tests/components/nam/snapshots/test_diagnostics.ambr +++ b/tests/components/nam/snapshots/test_diagnostics.ambr @@ -2,18 +2,18 @@ # name: test_entry_diagnostics dict({ 'data': dict({ - 'bme280_humidity': 45.7, - 'bme280_pressure': 1011.012, - 'bme280_temperature': 7.6, - 'bmp180_pressure': 1032.012, - 'bmp180_temperature': 7.6, - 'bmp280_pressure': 1022.012, - 'bmp280_temperature': 5.6, - 'dht22_humidity': 46.2, - 'dht22_temperature': 6.3, - 'ds18b20_temperature': 12.6, - 'heca_humidity': 50.0, - 'heca_temperature': 8.0, + 'bme280_humidity': 45.69, + 'bme280_pressure': 1011.0117, + 'bme280_temperature': 7.56, + 'bmp180_pressure': 1032.0118, + 'bmp180_temperature': 7.56, + 'bmp280_pressure': 1022.0117999999999, + 'bmp280_temperature': 5.56, + 'dht22_humidity': 46.23, + 'dht22_temperature': 6.26, + 'ds18b20_temperature': 12.56, + 'heca_humidity': 49.97, + 'heca_temperature': 7.95, 'mhz14a_carbon_dioxide': 865.0, 'pms_caqi': 19, 'pms_caqi_level': 'very_low', @@ -22,17 +22,17 @@ 'pms_p2': 11.0, 'sds011_caqi': 19, 'sds011_caqi_level': 'very_low', - 'sds011_p1': 18.6, - 'sds011_p2': 11.0, - 'sht3x_humidity': 34.7, - 'sht3x_temperature': 6.3, + 'sds011_p1': 18.65, + 'sds011_p2': 11.03, + 'sht3x_humidity': 34.69, + 'sht3x_temperature': 6.28, 'signal': -72.0, 'sps30_caqi': 54, 'sps30_caqi_level': 'medium', - 'sps30_p0': 31.2, - 'sps30_p1': 21.2, - 'sps30_p2': 34.3, - 'sps30_p4': 24.7, + 'sps30_p0': 31.23, + 'sps30_p1': 21.23, + 'sps30_p2': 34.32, + 'sps30_p4': 24.72, 'uptime': 456987, }), 'info': dict({ diff --git a/tests/components/nam/snapshots/test_sensor.ambr b/tests/components/nam/snapshots/test_sensor.ambr index ea47998f3de..426b2ff2e03 100644 --- a/tests/components/nam/snapshots/test_sensor.ambr +++ b/tests/components/nam/snapshots/test_sensor.ambr @@ -97,7 +97,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '45.7', + 'state': '45.69', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_bme280_pressure-entry] @@ -151,7 +151,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1011.012', + 'state': '1011.0117', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_bme280_temperature-entry] @@ -205,7 +205,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '7.6', + 'state': '7.56', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_bmp180_pressure-entry] @@ -259,7 +259,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1032.012', + 'state': '1032.0118', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_bmp180_temperature-entry] @@ -313,7 +313,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '7.6', + 'state': '7.56', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_bmp280_pressure-entry] @@ -367,7 +367,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1022.012', + 'state': '1022.0118', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_bmp280_temperature-entry] @@ -421,7 +421,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5.6', + 'state': '5.56', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_dht22_humidity-entry] @@ -475,7 +475,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '46.2', + 'state': '46.23', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_dht22_temperature-entry] @@ -529,7 +529,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '6.3', + 'state': '6.26', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_ds18b20_temperature-entry] @@ -583,7 +583,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '12.6', + 'state': '12.56', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_heca_humidity-entry] @@ -637,7 +637,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '50.0', + 'state': '49.97', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_heca_temperature-entry] @@ -691,7 +691,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '8.0', + 'state': '7.95', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_last_restart-entry] @@ -1224,7 +1224,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '18.6', + 'state': '18.65', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_sds011_pm2_5-entry] @@ -1278,7 +1278,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '11.0', + 'state': '11.03', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_sht3x_humidity-entry] @@ -1332,7 +1332,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '34.7', + 'state': '34.69', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_sht3x_temperature-entry] @@ -1386,7 +1386,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '6.3', + 'state': '6.28', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_signal_strength-entry] @@ -1602,7 +1602,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '31.2', + 'state': '31.23', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_sps30_pm10-entry] @@ -1656,7 +1656,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '21.2', + 'state': '21.23', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_sps30_pm2_5-entry] @@ -1710,7 +1710,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '34.3', + 'state': '34.32', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_sps30_pm4-entry] @@ -1763,6 +1763,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '24.7', + 'state': '24.72', }) # --- diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py index 8b8c3a4835a..13bde1432b3 100644 --- a/tests/components/nam/test_init.py +++ b/tests/components/nam/test_init.py @@ -23,7 +23,7 @@ async def test_async_setup_entry(hass: HomeAssistant) -> None: state = hass.states.get("sensor.nettigo_air_monitor_sds011_pm2_5") assert state is not None assert state.state != STATE_UNAVAILABLE - assert state.state == "11.0" + assert state.state == "11.03" async def test_config_not_ready(hass: HomeAssistant) -> None: diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 53945e1c8a2..6924af48f01 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -77,7 +77,7 @@ async def test_incompleta_data_after_device_restart(hass: HomeAssistant) -> None state = hass.states.get("sensor.nettigo_air_monitor_heca_temperature") assert state - assert state.state == "8.0" + assert state.state == "7.95" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS @@ -110,7 +110,7 @@ async def test_availability( state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") assert state assert state.state != STATE_UNAVAILABLE - assert state.state == "7.6" + assert state.state == "7.56" with ( patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), @@ -142,7 +142,7 @@ async def test_availability( state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") assert state assert state.state != STATE_UNAVAILABLE - assert state.state == "7.6" + assert state.state == "7.56" async def test_manual_update_entity(hass: HomeAssistant) -> None: From 3ca66be2681828db3c6d4c577bce9a308bf707c3 Mon Sep 17 00:00:00 2001 From: Pavel Skuratovich Date: Thu, 4 Jul 2024 22:54:39 +0300 Subject: [PATCH 0761/2411] Starline: Fix "Error updating SLNet token" message in Log (#121122) Fixes https://github.com/home-assistant/core/issues/116715 --- homeassistant/components/starline/account.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py index 6122ccbb3c2..4b1425ae7d9 100644 --- a/homeassistant/components/starline/account.py +++ b/homeassistant/components/starline/account.py @@ -9,7 +9,7 @@ from typing import Any from starline import StarlineApi, StarlineDevice from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util @@ -65,9 +65,9 @@ class StarlineAccount: ) self._api.set_slnet_token(slnet_token) self._api.set_user_id(user_id) - self._hass.config_entries.async_update_entry( - self._config_entry, - data={ + self._hass.add_job( + self._save_slnet_token, + { **self._config_entry.data, DATA_SLNET_TOKEN: slnet_token, DATA_EXPIRES: slnet_token_expires, @@ -77,6 +77,13 @@ class StarlineAccount: except Exception as err: # noqa: BLE001 _LOGGER.error("Error updating SLNet token: %s", err) + @callback + def _save_slnet_token(self, data) -> None: + self._hass.config_entries.async_update_entry( + self._config_entry, + data=data, + ) + def _update_data(self): """Update StarLine data.""" self._check_slnet_token(self._update_interval) From 84a8259103ce5bf6476bc0db303c5351a6daff11 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 21:59:02 +0200 Subject: [PATCH 0762/2411] Improve type hints in ezviz tests (#120679) --- tests/components/ezviz/__init__.py | 7 ++-- tests/components/ezviz/conftest.py | 9 +++-- tests/components/ezviz/test_config_flow.py | 46 ++++++++++++---------- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/tests/components/ezviz/__init__.py b/tests/components/ezviz/__init__.py index 9fc297be099..78bbee0b0ad 100644 --- a/tests/components/ezviz/__init__.py +++ b/tests/components/ezviz/__init__.py @@ -1,6 +1,6 @@ """Tests for the EZVIZ integration.""" -from unittest.mock import patch +from unittest.mock import _patch, patch from homeassistant.components.ezviz.const import ( ATTR_SERIAL, @@ -83,10 +83,11 @@ API_LOGIN_RETURN_VALIDATE = { } -def _patch_async_setup_entry(return_value=True): +def patch_async_setup_entry() -> _patch: + """Patch async_setup_entry.""" return patch( "homeassistant.components.ezviz.async_setup_entry", - return_value=return_value, + return_value=True, ) diff --git a/tests/components/ezviz/conftest.py b/tests/components/ezviz/conftest.py index 10fd0406a1c..891eacd1ab4 100644 --- a/tests/components/ezviz/conftest.py +++ b/tests/components/ezviz/conftest.py @@ -5,6 +5,9 @@ from unittest.mock import MagicMock, patch from pyezviz import EzvizClient from pyezviz.test_cam_rtsp import TestRTSPAuth import pytest +from typing_extensions import Generator + +from homeassistant.core import HomeAssistant ezviz_login_token_return = { "session_id": "fake_token", @@ -14,13 +17,13 @@ ezviz_login_token_return = { @pytest.fixture(autouse=True) -def mock_ffmpeg(hass): +def mock_ffmpeg(hass: HomeAssistant) -> None: """Mock ffmpeg is loaded.""" hass.config.components.add("ffmpeg") @pytest.fixture -def ezviz_test_rtsp_config_flow(hass): +def ezviz_test_rtsp_config_flow() -> Generator[MagicMock]: """Mock the EzvizApi for easier testing.""" with ( patch.object(TestRTSPAuth, "main", return_value=True), @@ -40,7 +43,7 @@ def ezviz_test_rtsp_config_flow(hass): @pytest.fixture -def ezviz_config_flow(hass): +def ezviz_config_flow() -> Generator[MagicMock]: """Mock the EzvizAPI for easier config flow testing.""" with ( patch.object(EzvizClient, "login", return_value=True), diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index 57c3ae0600e..f9459635f2c 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -1,6 +1,6 @@ """Test the EZVIZ config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch from pyezviz.exceptions import ( AuthTestResultFailed, @@ -10,6 +10,7 @@ from pyezviz.exceptions import ( InvalidURL, PyEzvizError, ) +import pytest from homeassistant.components.ezviz.const import ( ATTR_SERIAL, @@ -40,12 +41,13 @@ from . import ( API_LOGIN_RETURN_VALIDATE, DISCOVERY_INFO, USER_INPUT_VALIDATE, - _patch_async_setup_entry, init_integration, + patch_async_setup_entry, ) -async def test_user_form(hass: HomeAssistant, ezviz_config_flow) -> None: +@pytest.mark.usefixtures("ezviz_config_flow") +async def test_user_form(hass: HomeAssistant) -> None: """Test the user initiated form.""" result = await hass.config_entries.flow.async_init( @@ -55,7 +57,7 @@ async def test_user_form(hass: HomeAssistant, ezviz_config_flow) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with _patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT_VALIDATE, @@ -75,7 +77,8 @@ async def test_user_form(hass: HomeAssistant, ezviz_config_flow) -> None: assert result["reason"] == "already_configured_account" -async def test_user_custom_url(hass: HomeAssistant, ezviz_config_flow) -> None: +@pytest.mark.usefixtures("ezviz_config_flow") +async def test_user_custom_url(hass: HomeAssistant) -> None: """Test custom url step.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -94,7 +97,7 @@ async def test_user_custom_url(hass: HomeAssistant, ezviz_config_flow) -> None: assert result["step_id"] == "user_custom_url" assert result["errors"] == {} - with _patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_URL: "test-user"}, @@ -107,7 +110,8 @@ async def test_user_custom_url(hass: HomeAssistant, ezviz_config_flow) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_async_step_reauth(hass: HomeAssistant, ezviz_config_flow) -> None: +@pytest.mark.usefixtures("ezviz_config_flow") +async def test_async_step_reauth(hass: HomeAssistant) -> None: """Test the reauth step.""" result = await hass.config_entries.flow.async_init( @@ -117,7 +121,7 @@ async def test_async_step_reauth(hass: HomeAssistant, ezviz_config_flow) -> None assert result["step_id"] == "user" assert result["errors"] == {} - with _patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT_VALIDATE, @@ -185,9 +189,8 @@ async def test_step_reauth_abort_if_cloud_account_missing(hass: HomeAssistant) - assert result["reason"] == "ezviz_cloud_account_missing" -async def test_async_step_integration_discovery( - hass: HomeAssistant, ezviz_config_flow, ezviz_test_rtsp_config_flow -) -> None: +@pytest.mark.usefixtures("ezviz_config_flow", "ezviz_test_rtsp_config_flow") +async def test_async_step_integration_discovery(hass: HomeAssistant) -> None: """Test discovery and confirm step.""" with patch("homeassistant.components.ezviz.PLATFORMS_BY_TYPE", []): await init_integration(hass) @@ -199,7 +202,7 @@ async def test_async_step_integration_discovery( assert result["step_id"] == "confirm" assert result["errors"] == {} - with _patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -221,7 +224,7 @@ async def test_async_step_integration_discovery( async def test_options_flow(hass: HomeAssistant) -> None: """Test updating options.""" - with _patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry: entry = await init_integration(hass) assert entry.options[CONF_FFMPEG_ARGUMENTS] == DEFAULT_FFMPEG_ARGUMENTS @@ -245,7 +248,9 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> None: +async def test_user_form_exception( + hass: HomeAssistant, ezviz_config_flow: MagicMock +) -> None: """Test we handle exception on user form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -311,7 +316,7 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No async def test_discover_exception_step1( hass: HomeAssistant, - ezviz_config_flow, + ezviz_config_flow: MagicMock, ) -> None: """Test we handle unexpected exception on discovery.""" with patch("homeassistant.components.ezviz.PLATFORMS_BY_TYPE", []): @@ -397,10 +402,9 @@ async def test_discover_exception_step1( assert result["reason"] == "unknown" +@pytest.mark.usefixtures("ezviz_config_flow") async def test_discover_exception_step3( - hass: HomeAssistant, - ezviz_config_flow, - ezviz_test_rtsp_config_flow, + hass: HomeAssistant, ezviz_test_rtsp_config_flow: MagicMock ) -> None: """Test we handle unexpected exception on discovery.""" with patch("homeassistant.components.ezviz.PLATFORMS_BY_TYPE", []): @@ -459,7 +463,7 @@ async def test_discover_exception_step3( async def test_user_custom_url_exception( - hass: HomeAssistant, ezviz_config_flow + hass: HomeAssistant, ezviz_config_flow: MagicMock ) -> None: """Test we handle unexpected exception.""" ezviz_config_flow.side_effect = PyEzvizError() @@ -534,7 +538,7 @@ async def test_user_custom_url_exception( async def test_async_step_reauth_exception( - hass: HomeAssistant, ezviz_config_flow + hass: HomeAssistant, ezviz_config_flow: MagicMock ) -> None: """Test the reauth step exceptions.""" @@ -545,7 +549,7 @@ async def test_async_step_reauth_exception( assert result["step_id"] == "user" assert result["errors"] == {} - with _patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT_VALIDATE, From 83fac6192d4287a565fe0d7995a4f669fa41dfe3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:07:38 +0200 Subject: [PATCH 0763/2411] Use service_calls fixture in webostv tests (#120999) --- tests/components/webostv/conftest.py | 9 --------- .../components/webostv/test_device_trigger.py | 12 +++++------ tests/components/webostv/test_trigger.py | 20 +++++++++---------- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/tests/components/webostv/conftest.py b/tests/components/webostv/conftest.py index d25d1c7b031..a30ae933cca 100644 --- a/tests/components/webostv/conftest.py +++ b/tests/components/webostv/conftest.py @@ -6,12 +6,9 @@ from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant.components.webostv.const import LIVE_TV_APP_ID -from homeassistant.core import HomeAssistant, ServiceCall from .const import CHANNEL_1, CHANNEL_2, CLIENT_KEY, FAKE_UUID, MOCK_APPS, MOCK_INPUTS -from tests.common import async_mock_service - @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: @@ -22,12 +19,6 @@ def mock_setup_entry() -> Generator[AsyncMock]: yield mock_setup_entry -@pytest.fixture -def calls(hass: HomeAssistant) -> list[ServiceCall]: - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - @pytest.fixture(name="client") def client_fixture(): """Patch of client library for tests.""" diff --git a/tests/components/webostv/test_device_trigger.py b/tests/components/webostv/test_device_trigger.py index 29c75d4440b..41045969335 100644 --- a/tests/components/webostv/test_device_trigger.py +++ b/tests/components/webostv/test_device_trigger.py @@ -44,7 +44,7 @@ async def test_get_triggers( async def test_if_fires_on_turn_on_request( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, client, ) -> None: @@ -97,11 +97,11 @@ async def test_if_fires_on_turn_on_request( blocking=True, ) - assert len(calls) == 2 - assert calls[0].data["some"] == device.id - assert calls[0].data["id"] == 0 - assert calls[1].data["some"] == ENTITY_ID - assert calls[1].data["id"] == 0 + assert len(service_calls) == 3 + assert service_calls[1].data["some"] == device.id + assert service_calls[1].data["id"] == 0 + assert service_calls[2].data["some"] == ENTITY_ID + assert service_calls[2].data["id"] == 0 async def test_failure_scenarios( diff --git a/tests/components/webostv/test_trigger.py b/tests/components/webostv/test_trigger.py index 918666cf4bf..d7eeae28ea3 100644 --- a/tests/components/webostv/test_trigger.py +++ b/tests/components/webostv/test_trigger.py @@ -20,7 +20,7 @@ from tests.common import MockEntity, MockEntityPlatform async def test_webostv_turn_on_trigger_device_id( hass: HomeAssistant, - calls: list[ServiceCall], + service_calls: list[ServiceCall], device_registry: dr.DeviceRegistry, client, ) -> None: @@ -58,14 +58,14 @@ async def test_webostv_turn_on_trigger_device_id( blocking=True, ) - assert len(calls) == 1 - assert calls[0].data["some"] == device.id - assert calls[0].data["id"] == 0 + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == device.id + assert service_calls[1].data["id"] == 0 with patch("homeassistant.config.load_yaml_dict", return_value={}): await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) - calls.clear() + service_calls.clear() with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -75,11 +75,11 @@ async def test_webostv_turn_on_trigger_device_id( blocking=True, ) - assert len(calls) == 0 + assert len(service_calls) == 1 async def test_webostv_turn_on_trigger_entity_id( - hass: HomeAssistant, calls: list[ServiceCall], client + hass: HomeAssistant, service_calls: list[ServiceCall], client ) -> None: """Test for turn_on triggers by entity_id firing.""" await setup_webostv(hass) @@ -113,9 +113,9 @@ async def test_webostv_turn_on_trigger_entity_id( blocking=True, ) - assert len(calls) == 1 - assert calls[0].data["some"] == ENTITY_ID - assert calls[0].data["id"] == 0 + assert len(service_calls) == 2 + assert service_calls[1].data["some"] == ENTITY_ID + assert service_calls[1].data["id"] == 0 async def test_wrong_trigger_platform_type( From 276f6c7ee732e4a024cf29fb4cc63cc5eaf48a80 Mon Sep 17 00:00:00 2001 From: xLarry Date: Thu, 4 Jul 2024 22:08:50 +0200 Subject: [PATCH 0764/2411] Update laundrify_aio to v1.2.2 (#121068) * refactor: upgrade laundrify_aio to v1.2.1 * refactor: update laundrify_aio to v1.2.2 --- .../components/laundrify/binary_sensor.py | 16 +++++++++------- homeassistant/components/laundrify/const.py | 2 +- .../components/laundrify/coordinator.py | 5 ++--- homeassistant/components/laundrify/manifest.json | 2 +- homeassistant/components/laundrify/model.py | 14 -------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/laundrify/conftest.py | 6 +++++- .../components/laundrify/fixtures/machines.json | 4 +++- 9 files changed, 23 insertions(+), 30 deletions(-) delete mode 100644 homeassistant/components/laundrify/model.py diff --git a/homeassistant/components/laundrify/binary_sensor.py b/homeassistant/components/laundrify/binary_sensor.py index 80732bdc470..925aea99f39 100644 --- a/homeassistant/components/laundrify/binary_sensor.py +++ b/homeassistant/components/laundrify/binary_sensor.py @@ -4,6 +4,8 @@ from __future__ import annotations import logging +from laundrify_aio import LaundrifyDevice + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -14,9 +16,8 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, MANUFACTURER, MODEL +from .const import DOMAIN, MANUFACTURER, MODELS from .coordinator import LaundrifyUpdateCoordinator -from .model import LaundrifyDevice _LOGGER = logging.getLogger(__name__) @@ -52,14 +53,15 @@ class LaundrifyPowerPlug( """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator) self._device = device - unique_id = device["_id"] + unique_id = device.id self._attr_unique_id = unique_id self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id)}, - name=device["name"], + name=device.name, manufacturer=MANUFACTURER, - model=MODEL, - sw_version=device["firmwareVersion"], + model=MODELS[device.model], + sw_version=device.firmwareVersion, + configuration_url=f"http://{device.internalIP}", ) @property @@ -73,7 +75,7 @@ class LaundrifyPowerPlug( @property def is_on(self) -> bool: """Return entity state.""" - return self._device["status"] == "ON" + return bool(self._device.status == "ON") @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/laundrify/const.py b/homeassistant/components/laundrify/const.py index c312b895234..5e2998aa7c4 100644 --- a/homeassistant/components/laundrify/const.py +++ b/homeassistant/components/laundrify/const.py @@ -3,7 +3,7 @@ DOMAIN = "laundrify" MANUFACTURER = "laundrify" -MODEL = "WLAN-Adapter (SU02)" +MODELS = {"SU02": "WLAN-Adapter classic", "M01": "WLAN-Adapter mini"} DEFAULT_POLL_INTERVAL = 60 diff --git a/homeassistant/components/laundrify/coordinator.py b/homeassistant/components/laundrify/coordinator.py index c3fdc265174..22f68a7c5ae 100644 --- a/homeassistant/components/laundrify/coordinator.py +++ b/homeassistant/components/laundrify/coordinator.py @@ -4,7 +4,7 @@ import asyncio from datetime import timedelta import logging -from laundrify_aio import LaundrifyAPI +from laundrify_aio import LaundrifyAPI, LaundrifyDevice from laundrify_aio.exceptions import ApiConnectionException, UnauthorizedException from homeassistant.core import HomeAssistant @@ -12,7 +12,6 @@ from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, REQUEST_TIMEOUT -from .model import LaundrifyDevice _LOGGER = logging.getLogger(__name__) @@ -38,7 +37,7 @@ class LaundrifyUpdateCoordinator(DataUpdateCoordinator[dict[str, LaundrifyDevice # Note: TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with asyncio.timeout(REQUEST_TIMEOUT): - return {m["_id"]: m for m in await self.laundrify_api.get_machines()} + return {m.id: m for m in await self.laundrify_api.get_machines()} except UnauthorizedException as err: # Raising ConfigEntryAuthFailed will cancel future updates # and start a config flow with SOURCE_REAUTH (async_step_reauth) diff --git a/homeassistant/components/laundrify/manifest.json b/homeassistant/components/laundrify/manifest.json index 8dca67058b7..99f03bcb5bb 100644 --- a/homeassistant/components/laundrify/manifest.json +++ b/homeassistant/components/laundrify/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/laundrify", "iot_class": "cloud_polling", - "requirements": ["laundrify-aio==1.1.2"] + "requirements": ["laundrify-aio==1.2.2"] } diff --git a/homeassistant/components/laundrify/model.py b/homeassistant/components/laundrify/model.py deleted file mode 100644 index 862824c3154..00000000000 --- a/homeassistant/components/laundrify/model.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Models for laundrify platform.""" - -from __future__ import annotations - -from typing import TypedDict - - -class LaundrifyDevice(TypedDict): - """laundrify Power Plug.""" - - _id: str - name: str - status: str - firmwareVersion: str diff --git a/requirements_all.txt b/requirements_all.txt index feeddae7386..5931c2f7b4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1224,7 +1224,7 @@ lacrosse-view==1.0.1 lakeside==0.13 # homeassistant.components.laundrify -laundrify-aio==1.1.2 +laundrify-aio==1.2.2 # homeassistant.components.ld2410_ble ld2410-ble==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a48e25b0f65..2fedcdc2b0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -999,7 +999,7 @@ krakenex==2.1.0 lacrosse-view==1.0.1 # homeassistant.components.laundrify -laundrify-aio==1.1.2 +laundrify-aio==1.2.2 # homeassistant.components.ld2410_ble ld2410-ble==0.1.1 diff --git a/tests/components/laundrify/conftest.py b/tests/components/laundrify/conftest.py index 91aeebf81ee..2f6496c06a5 100644 --- a/tests/components/laundrify/conftest.py +++ b/tests/components/laundrify/conftest.py @@ -3,6 +3,7 @@ import json from unittest.mock import patch +from laundrify_aio import LaundrifyAPI, LaundrifyDevice import pytest from .const import VALID_ACCESS_TOKEN, VALID_ACCOUNT_ID @@ -49,7 +50,10 @@ def laundrify_api_fixture(laundrify_exchange_code, laundrify_validate_token): ), patch( "laundrify_aio.LaundrifyAPI.get_machines", - return_value=json.loads(load_fixture("laundrify/machines.json")), + return_value=[ + LaundrifyDevice(machine, LaundrifyAPI) + for machine in json.loads(load_fixture("laundrify/machines.json")) + ], ) as get_machines_mock, ): yield get_machines_mock diff --git a/tests/components/laundrify/fixtures/machines.json b/tests/components/laundrify/fixtures/machines.json index ab1a737cb45..3397212659f 100644 --- a/tests/components/laundrify/fixtures/machines.json +++ b/tests/components/laundrify/fixtures/machines.json @@ -1,8 +1,10 @@ [ { - "_id": "14", + "id": "14", "name": "Demo Waschmaschine", "status": "OFF", + "internalIP": "192.168.0.123", + "model": "SU02", "firmwareVersion": "2.1.0" } ] From b14f22926ac0c6ad3bdd95c457fd0a6a235fe1bc Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 5 Jul 2024 00:19:24 +0300 Subject: [PATCH 0765/2411] Fix WebOS TV media player status when OFF after IDLE (#121251) --- homeassistant/components/webostv/media_player.py | 3 ++- tests/components/webostv/test_media_player.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 6aef47515db..099b5a73784 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -239,7 +239,8 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): self._attr_assumed_state = True if ( - self._client.media_state is not None + self._client.is_on + and self._client.media_state is not None and self._client.media_state.get("foregroundAppInfo") is not None ): self._attr_assumed_state = False diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 775a3eb9383..e4c02e680bd 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -849,3 +849,7 @@ async def test_update_media_state( monkeypatch.setattr(client, "media_state", data) await client.mock_state_update() assert hass.states.get(ENTITY_ID).state == MediaPlayerState.IDLE + + monkeypatch.setattr(client, "is_on", False) + await client.mock_state_update() + assert hass.states.get(ENTITY_ID).state == STATE_OFF From b406438fa747b516fd3b584d80cab01a1d67a5d6 Mon Sep 17 00:00:00 2001 From: Jordi Date: Fri, 5 Jul 2024 00:05:35 +0200 Subject: [PATCH 0766/2411] Bump aioaquacell to 0.1.8 (#121253) --- homeassistant/components/aquacell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aquacell/manifest.json b/homeassistant/components/aquacell/manifest.json index 1f43fa214d3..559bdf345bb 100644 --- a/homeassistant/components/aquacell/manifest.json +++ b/homeassistant/components/aquacell/manifest.json @@ -8,5 +8,5 @@ "integration_type": "device", "iot_class": "cloud_polling", "loggers": ["aioaquacell"], - "requirements": ["aioaquacell==0.1.7"] + "requirements": ["aioaquacell==0.1.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5931c2f7b4a..b0cd29ae68d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -195,7 +195,7 @@ aioambient==2024.01.0 aioapcaccess==0.4.2 # homeassistant.components.aquacell -aioaquacell==0.1.7 +aioaquacell==0.1.8 # homeassistant.components.aseko_pool_live aioaseko==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2fedcdc2b0b..6e2f1787dca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -174,7 +174,7 @@ aioambient==2024.01.0 aioapcaccess==0.4.2 # homeassistant.components.aquacell -aioaquacell==0.1.7 +aioaquacell==0.1.8 # homeassistant.components.aseko_pool_live aioaseko==0.1.1 From d799a4575bd94e306420327a6109c93537249228 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 5 Jul 2024 01:27:56 +0300 Subject: [PATCH 0767/2411] Bump aiowebostv to 0.4.2 (#121258) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index bcafb82a4b0..679bad9b9f5 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_push", "loggers": ["aiowebostv"], "quality_scale": "platinum", - "requirements": ["aiowebostv==0.4.1"], + "requirements": ["aiowebostv==0.4.2"], "ssdp": [ { "st": "urn:lge-com:service:webos-second-screen:1" diff --git a/requirements_all.txt b/requirements_all.txt index b0cd29ae68d..3d640790bfd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.4.1 +aiowebostv==0.4.2 # homeassistant.components.withings aiowithings==3.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e2f1787dca..3ce0f695f8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -377,7 +377,7 @@ aiowaqi==3.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.4.1 +aiowebostv==0.4.2 # homeassistant.components.withings aiowithings==3.0.2 From e47cbf3cf7c4d51c52ccb0d6ba9b286d0c0a0082 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 5 Jul 2024 00:49:51 +0200 Subject: [PATCH 0768/2411] Use async_setup_recorder_instance fixture in recorder v32_migration tests (#121081) Co-authored-by: J. Nick Koston --- .../components/recorder/test_v32_migration.py | 85 +++++++++---------- 1 file changed, 39 insertions(+), 46 deletions(-) diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index 039f1c87aee..4e809d02446 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -14,13 +14,12 @@ from homeassistant.components.recorder import core, statistics from homeassistant.components.recorder.queries import select_event_type_ids from homeassistant.components.recorder.util import session_scope from homeassistant.core import EVENT_STATE_CHANGED, Event, EventOrigin, State -from homeassistant.helpers import recorder as recorder_helper -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from .common import async_wait_recording_done from tests.common import async_test_home_assistant +from tests.typing import RecorderInstanceGenerator CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" SCHEMA_MODULE = "tests.components.recorder.db_schema_32" @@ -48,11 +47,14 @@ def _create_engine_test(*args, **kwargs): return engine +@pytest.mark.parametrize("enable_migrate_context_ids", [True]) +@pytest.mark.parametrize("enable_migrate_event_type_ids", [True]) +@pytest.mark.parametrize("enable_migrate_entity_ids", [True]) @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_times( + async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, - recorder_db_url: str, ) -> None: """Test we can migrate times.""" importlib.import_module(SCHEMA_MODULE) @@ -119,11 +121,10 @@ async def test_migrate_times( "homeassistant.components.recorder.Recorder._cleanup_legacy_states_event_ids" ), ): - async with async_test_home_assistant() as hass: - recorder_helper.async_initialize_recorder(hass) - assert await async_setup_component( - hass, "recorder", {"recorder": {"db_url": recorder_db_url}} - ) + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): await hass.async_block_till_done() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -133,15 +134,15 @@ async def test_migrate_times( session.add(old_db_schema.Events.from_event(custom_event)) session.add(old_db_schema.States.from_event(state_changed_event)) - await recorder.get_instance(hass).async_add_executor_job(_add_data) + await instance.async_add_executor_job(_add_data) await hass.async_block_till_done() - await recorder.get_instance(hass).async_block_till_done() + await instance.async_block_till_done() - states_indexes = await recorder.get_instance(hass).async_add_executor_job( + states_indexes = await instance.async_add_executor_job( _get_states_index_names ) states_index_names = {index["name"] for index in states_indexes} - assert recorder.get_instance(hass).use_legacy_events_index is True + assert instance.use_legacy_events_index is True await hass.async_stop() await hass.async_block_till_done() @@ -149,17 +150,16 @@ async def test_migrate_times( assert "ix_states_event_id" in states_index_names # Test that the duplicates are removed during migration from schema 23 - async with async_test_home_assistant() as hass: - recorder_helper.async_initialize_recorder(hass) - assert await async_setup_component( - hass, "recorder", {"recorder": {"db_url": recorder_db_url}} - ) + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): await hass.async_block_till_done() # We need to wait for all the migration tasks to complete # before we can check the database. for _ in range(number_of_migrations): - await recorder.get_instance(hass).async_block_till_done() + await instance.async_block_till_done() await async_wait_recording_done(hass) def _get_test_data_from_db(): @@ -183,9 +183,9 @@ async def test_migrate_times( session.expunge_all() return events_result, states_result - events_result, states_result = await recorder.get_instance( - hass - ).async_add_executor_job(_get_test_data_from_db) + events_result, states_result = await instance.async_add_executor_job( + _get_test_data_from_db + ) assert len(events_result) == 1 assert events_result[0].time_fired_ts == now_timestamp @@ -197,24 +197,20 @@ async def test_migrate_times( with session_scope(hass=hass) as session: return inspect(session.connection()).get_indexes("events") - events_indexes = await recorder.get_instance(hass).async_add_executor_job( - _get_events_index_names - ) + events_indexes = await instance.async_add_executor_job(_get_events_index_names) events_index_names = {index["name"] for index in events_indexes} assert "ix_events_context_id_bin" in events_index_names assert "ix_events_context_id" not in events_index_names - states_indexes = await recorder.get_instance(hass).async_add_executor_job( - _get_states_index_names - ) + states_indexes = await instance.async_add_executor_job(_get_states_index_names) states_index_names = {index["name"] for index in states_indexes} # sqlite does not support dropping foreign keys so we had to # create a new table and copy the data over assert "ix_states_event_id" not in states_index_names - assert recorder.get_instance(hass).use_legacy_events_index is False + assert instance.use_legacy_events_index is False await hass.async_stop() @@ -222,6 +218,7 @@ async def test_migrate_times( @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_can_resume_entity_id_post_migration( + async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, recorder_db_url: str, ) -> None: @@ -287,11 +284,10 @@ async def test_migrate_can_resume_entity_id_post_migration( "homeassistant.components.recorder.Recorder._cleanup_legacy_states_event_ids" ), ): - async with async_test_home_assistant() as hass: - recorder_helper.async_initialize_recorder(hass) - assert await async_setup_component( - hass, "recorder", {"recorder": {"db_url": recorder_db_url}} - ) + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): await hass.async_block_till_done() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -301,15 +297,15 @@ async def test_migrate_can_resume_entity_id_post_migration( session.add(old_db_schema.Events.from_event(custom_event)) session.add(old_db_schema.States.from_event(state_changed_event)) - await recorder.get_instance(hass).async_add_executor_job(_add_data) + await instance.async_add_executor_job(_add_data) await hass.async_block_till_done() - await recorder.get_instance(hass).async_block_till_done() + await instance.async_block_till_done() - states_indexes = await recorder.get_instance(hass).async_add_executor_job( + states_indexes = await instance.async_add_executor_job( _get_states_index_names ) states_index_names = {index["name"] for index in states_indexes} - assert recorder.get_instance(hass).use_legacy_events_index is True + assert instance.use_legacy_events_index is True await hass.async_stop() await hass.async_block_till_done() @@ -317,22 +313,19 @@ async def test_migrate_can_resume_entity_id_post_migration( assert "ix_states_event_id" in states_index_names assert "ix_states_entity_id_last_updated_ts" in states_index_names - async with async_test_home_assistant() as hass: - recorder_helper.async_initialize_recorder(hass) - assert await async_setup_component( - hass, "recorder", {"recorder": {"db_url": recorder_db_url}} - ) + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): await hass.async_block_till_done() # We need to wait for all the migration tasks to complete # before we can check the database. for _ in range(number_of_migrations): - await recorder.get_instance(hass).async_block_till_done() + await instance.async_block_till_done() await async_wait_recording_done(hass) - states_indexes = await recorder.get_instance(hass).async_add_executor_job( - _get_states_index_names - ) + states_indexes = await instance.async_add_executor_job(_get_states_index_names) states_index_names = {index["name"] for index in states_indexes} assert "ix_states_entity_id_last_updated_ts" not in states_index_names From 62d9020261bb18fbbc40a5952448fe3854f1ba66 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Jul 2024 01:17:15 +0200 Subject: [PATCH 0769/2411] Remove legacy method from deCONZ fan and sensor tests (#121244) --- tests/components/deconz/test_fan.py | 10 ---------- tests/components/deconz/test_sensor.py | 12 ------------ 2 files changed, 22 deletions(-) diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index 0f22c0b2b3b..351ec798909 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -15,19 +15,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from .test_gateway import setup_deconz_integration - from tests.test_util.aiohttp import AiohttpClientMocker -async def test_no_fans( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that no fan entities are created.""" - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 - - @pytest.mark.parametrize( "light_payload", [ diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index c3fc84a827a..e7e2521cc16 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -25,19 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt as dt_util -from .test_gateway import setup_deconz_integration - from tests.common import async_fire_time_changed -from tests.test_util.aiohttp import AiohttpClientMocker - - -async def test_no_sensors( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that no sensors in deconz results in no sensor entities.""" - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 - TEST_DATA = [ ( # Air quality sensor From b3a62a97b4a6ee9342014e6e7cab951579a69c1a Mon Sep 17 00:00:00 2001 From: Sarabveer Singh <4297171+sarabveer@users.noreply.github.com> Date: Thu, 4 Jul 2024 23:10:35 -0400 Subject: [PATCH 0770/2411] Update HomeKit PM2.5 mappings based on new 2024 US EPA AQI (#109900) --- homeassistant/components/homekit/util.py | 4 ++-- tests/components/homekit/test_type_sensors.py | 10 ++++++++++ tests/components/homekit/test_util.py | 9 +++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 8fbd7c6b13b..d521fd6db0c 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -433,13 +433,13 @@ def temperature_to_states(temperature: float, unit: str) -> float: def density_to_air_quality(density: float) -> int: """Map PM2.5 µg/m3 density to HomeKit AirQuality level.""" - if density <= 12: # US AQI 0-50 (HomeKit: Excellent) + if density <= 9: # US AQI 0-50 (HomeKit: Excellent) return 1 if density <= 35.4: # US AQI 51-100 (HomeKit: Good) return 2 if density <= 55.4: # US AQI 101-150 (HomeKit: Fair) return 3 - if density <= 150.4: # US AQI 151-200 (HomeKit: Inferior) + if density <= 125.4: # US AQI 151-200 (HomeKit: Inferior) return 4 return 5 # US AQI 201+ (HomeKit: Poor) diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 3b26ec8d36e..3e8e05fdcfd 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -213,6 +213,16 @@ async def test_pm25(hass: HomeAssistant, hk_driver) -> None: assert acc.char_density.value == 0 assert acc.char_quality.value == 0 + hass.states.async_set(entity_id, "8") + await hass.async_block_till_done() + assert acc.char_density.value == 8 + assert acc.char_quality.value == 1 + + hass.states.async_set(entity_id, "12") + await hass.async_block_till_done() + assert acc.char_density.value == 12 + assert acc.char_quality.value == 2 + hass.states.async_set(entity_id, "23") await hass.async_block_till_done() assert acc.char_density.value == 23 diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 24999242dc1..ff6ee0c6aa8 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -230,14 +230,15 @@ def test_temperature_to_states() -> None: def test_density_to_air_quality() -> None: """Test map PM2.5 density to HomeKit AirQuality level.""" assert density_to_air_quality(0) == 1 - assert density_to_air_quality(12) == 1 - assert density_to_air_quality(12.1) == 2 + assert density_to_air_quality(9) == 1 + assert density_to_air_quality(9.1) == 2 + assert density_to_air_quality(12) == 2 assert density_to_air_quality(35.4) == 2 assert density_to_air_quality(35.5) == 3 assert density_to_air_quality(55.4) == 3 assert density_to_air_quality(55.5) == 4 - assert density_to_air_quality(150.4) == 4 - assert density_to_air_quality(150.5) == 5 + assert density_to_air_quality(125.4) == 4 + assert density_to_air_quality(125.5) == 5 assert density_to_air_quality(200) == 5 From 6c42596bdda0ba3e731de8a5e285ecbfaf7c8e49 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 5 Jul 2024 13:26:44 +1000 Subject: [PATCH 0771/2411] Bump aiolifx to 1.0.4 (#121267) --- CODEOWNERS | 2 ++ homeassistant/components/lifx/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7add25202e9..72a11c63a47 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -779,6 +779,8 @@ build.json @home-assistant/supervisor /tests/components/lg_netcast/ @Drafteed @splinter98 /homeassistant/components/lidarr/ @tkdrob /tests/components/lidarr/ @tkdrob +/homeassistant/components/lifx/ @Djelibeybi +/tests/components/lifx/ @Djelibeybi /homeassistant/components/light/ @home-assistant/core /tests/components/light/ @home-assistant/core /homeassistant/components/linear_garage_door/ @IceBotYT diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 6aa7fdc6305..5e68c1bab35 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -1,7 +1,7 @@ { "domain": "lifx", "name": "LIFX", - "codeowners": [], + "codeowners": ["@Djelibeybi"], "config_flow": true, "dependencies": ["network"], "dhcp": [ @@ -48,7 +48,7 @@ "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"], "requirements": [ - "aiolifx==1.0.2", + "aiolifx==1.0.4", "aiolifx-effects==0.3.2", "aiolifx-themes==0.4.15" ] diff --git a/requirements_all.txt b/requirements_all.txt index 3d640790bfd..8b7b2951221 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -282,7 +282,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.4.15 # homeassistant.components.lifx -aiolifx==1.0.2 +aiolifx==1.0.4 # homeassistant.components.livisi aiolivisi==0.0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ce0f695f8e..958e3723796 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -255,7 +255,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.4.15 # homeassistant.components.lifx -aiolifx==1.0.2 +aiolifx==1.0.4 # homeassistant.components.livisi aiolivisi==0.0.19 From adee8094e78fd30e6965a34efcd36a7013afe13b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Jul 2024 01:30:07 -0500 Subject: [PATCH 0772/2411] Cache is_official_image/is_docker_env in bootstrap to fix blocking I/O (#121261) * Cache is_official_image and is_docker_env in bootstrap to fix blocking I/O These do blocking I/O later in the startup process discovered in https://github.com/home-assistant/core/pull/120273 * comment --- homeassistant/bootstrap.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index c5229634053..f4cfc8c87c8 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -88,7 +88,7 @@ from .helpers import ( ) from .helpers.dispatcher import async_dispatcher_send_internal from .helpers.storage import get_internal_store_manager -from .helpers.system_info import async_get_system_info +from .helpers.system_info import async_get_system_info, is_official_image from .helpers.typing import ConfigType from .setup import ( # _setup_started is marked as protected to make it clear @@ -104,7 +104,7 @@ from .setup import ( from .util.async_ import create_eager_task from .util.hass_dict import HassKey from .util.logging import async_activate_log_queue_handler -from .util.package import async_get_user_site, is_virtual_env +from .util.package import async_get_user_site, is_docker_env, is_virtual_env with contextlib.suppress(ImportError): # Ensure anyio backend is imported to avoid it being imported in the event loop @@ -407,6 +407,10 @@ def _init_blocking_io_modules_in_executor() -> None: # Initialize the mimetypes module to avoid blocking calls # to the filesystem to load the mime.types file. mimetypes.init() + # Initialize is_official_image and is_docker_env to avoid blocking calls + # to the filesystem. + is_official_image() + is_docker_env() async def async_load_base_functionality(hass: core.HomeAssistant) -> None: From cdb2ec4231c1df207786b618eed096eeed6cf1ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Jul 2024 01:56:20 -0500 Subject: [PATCH 0773/2411] Small speed up to entity state calculation (#121273) --- homeassistant/helpers/entity.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f5b93263692..f730223ab8f 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -263,8 +263,6 @@ class CalculatedState: attributes: dict[str, Any] # Capability attributes returned by the capability_attributes property capability_attributes: Mapping[str, Any] | None - # Attributes which may be overridden by the entity registry - shadowed_attributes: Mapping[str, Any] class CachedProperties(type): @@ -1042,18 +1040,20 @@ class Entity( @callback def _async_calculate_state(self) -> CalculatedState: """Calculate state string and attribute mapping.""" - return CalculatedState(*self.__async_calculate_state()) + state, attr, capabilities, _, _ = self.__async_calculate_state() + return CalculatedState(state, attr, capabilities) def __async_calculate_state( self, - ) -> tuple[str, dict[str, Any], Mapping[str, Any] | None, Mapping[str, Any]]: + ) -> tuple[str, dict[str, Any], Mapping[str, Any] | None, str | None, int | None]: """Calculate state string and attribute mapping. - Returns a tuple (state, attr, capability_attr, shadowed_attr). + Returns a tuple: state - the stringified state attr - the attribute dictionary capability_attr - a mapping with capability attributes - shadowed_attr - a mapping with attributes which may be overridden + original_device_class - the device class which may be overridden + supported_features - the supported features This method is called when writing the state to avoid the overhead of creating a dataclass object. @@ -1062,7 +1062,6 @@ class Entity( capability_attr = self.capability_attributes attr = capability_attr.copy() if capability_attr else {} - shadowed_attr = {} available = self.available # only call self.available once per update cycle state = self._stringify_state(available) @@ -1081,30 +1080,27 @@ class Entity( if (attribution := self.attribution) is not None: attr[ATTR_ATTRIBUTION] = attribution - shadowed_attr[ATTR_DEVICE_CLASS] = self.device_class + original_device_class = self.device_class if ( - device_class := (entry and entry.device_class) - or shadowed_attr[ATTR_DEVICE_CLASS] + device_class := (entry and entry.device_class) or original_device_class ) is not None: attr[ATTR_DEVICE_CLASS] = str(device_class) if (entity_picture := self.entity_picture) is not None: attr[ATTR_ENTITY_PICTURE] = entity_picture - shadowed_attr[ATTR_ICON] = self.icon - if (icon := (entry and entry.icon) or shadowed_attr[ATTR_ICON]) is not None: + if (icon := (entry and entry.icon) or self.icon) is not None: attr[ATTR_ICON] = icon - shadowed_attr[ATTR_FRIENDLY_NAME] = self._friendly_name_internal() if ( - name := (entry and entry.name) or shadowed_attr[ATTR_FRIENDLY_NAME] + name := (entry and entry.name) or self._friendly_name_internal() ) is not None: attr[ATTR_FRIENDLY_NAME] = name if (supported_features := self.supported_features) is not None: attr[ATTR_SUPPORTED_FEATURES] = supported_features - return (state, attr, capability_attr, shadowed_attr) + return (state, attr, capability_attr, original_device_class, supported_features) @callback def _async_write_ha_state(self) -> None: @@ -1130,14 +1126,15 @@ class Entity( return state_calculate_start = timer() - state, attr, capabilities, shadowed_attr = self.__async_calculate_state() + state, attr, capabilities, original_device_class, supported_features = ( + self.__async_calculate_state() + ) time_now = timer() if entry: # Make sure capabilities in the entity registry are up to date. Capabilities # include capability attributes, device class and supported features - original_device_class: str | None = shadowed_attr[ATTR_DEVICE_CLASS] - supported_features: int = attr.get(ATTR_SUPPORTED_FEATURES) or 0 + supported_features = supported_features or 0 if ( capabilities != entry.capabilities or original_device_class != entry.original_device_class From e71f6c5948d9cca0ae2598adb434d9d48cd5d4aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Jul 2024 01:57:08 -0500 Subject: [PATCH 0774/2411] Small speedup to processing entity customize (#121271) --- homeassistant/helpers/entity.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f730223ab8f..dbc1a036ef6 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1185,11 +1185,18 @@ class Entity( report_issue, ) - # Overwrite properties that have been set in the config file. - if (customize := hass.data.get(DATA_CUSTOMIZE)) and ( - custom := customize.get(entity_id) - ): - attr.update(custom) + try: + # Most of the time this will already be + # set and since try is near zero cost + # on py3.11+ its faster to assume it is + # set and catch the exception if it is not. + customize = hass.data[DATA_CUSTOMIZE] + except KeyError: + pass + else: + # Overwrite properties that have been set in the config file. + if custom := customize.get(entity_id): + attr.update(custom) if ( self._context_set is not None From 5a24ee0bc0138c7612e9dd5a938c4f26362242d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Jul 2024 01:58:30 -0500 Subject: [PATCH 0775/2411] Fix blocking I/O while validating config schema (#121263) --- homeassistant/config.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 96bc94636a2..a61fcbdbb0c 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -815,7 +815,9 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non This method is a coroutine. """ - config = CORE_CONFIG_SCHEMA(config) + # CORE_CONFIG_SCHEMA is not async safe since it uses vol.IsDir + # so we need to run it in an executor job. + config = await hass.async_add_executor_job(CORE_CONFIG_SCHEMA, config) # Only load auth during startup. if not hasattr(hass, "auth"): @@ -1529,9 +1531,15 @@ async def async_process_component_config( return IntegrationConfigInfo(None, config_exceptions) # No custom config validator, proceed with schema validation - if hasattr(component, "CONFIG_SCHEMA"): + if config_schema := getattr(component, "CONFIG_SCHEMA", None): try: - return IntegrationConfigInfo(component.CONFIG_SCHEMA(config), []) + if domain in config: + # cv.isdir, cv.isfile, cv.isdevice are not async + # friendly so we need to run this in executor + schema = await hass.async_add_executor_job(config_schema, config) + else: + schema = config_schema(config) + return IntegrationConfigInfo(schema, []) except vol.Invalid as exc: exc_info = ConfigExceptionInfo( exc, From dcef25c0fa74ef3811401071e6fb487b754130aa Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Jul 2024 09:01:45 +0200 Subject: [PATCH 0776/2411] Use fixtures in deCONZ climate tests (#121242) --- tests/components/deconz/test_climate.py | 266 ++++++++++++------------ 1 file changed, 128 insertions(+), 138 deletions(-) diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 0e51f31cec4..63d1badc7bc 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -1,6 +1,6 @@ """deCONZ climate platform tests.""" -from unittest.mock import patch +from collections.abc import Callable import pytest @@ -35,6 +35,7 @@ from homeassistant.components.deconz.climate import ( DECONZ_PRESET_MANUAL, ) from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, @@ -44,32 +45,13 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError -from .test_gateway import ( - DECONZ_WEB_REQUEST, - mock_deconz_put_request, - setup_deconz_integration, -) - from tests.test_util.aiohttp import AiohttpClientMocker -async def test_no_sensors( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that no sensors in deconz results in no climate entities.""" - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 - - -async def test_simple_climate_device( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test successful creation of climate entities. - - This is a simple water heater that only supports setting temperature and on and off. - """ - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "0": { "config": { "battery": 59, @@ -101,10 +83,18 @@ async def test_simple_climate_device( "uniqueid": "14:b4:57:ff:fe:d5:4e:77-01-0201", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_simple_climate_device( + hass: HomeAssistant, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + mock_deconz_websocket, +) -> None: + """Test successful creation of climate entities. + This is a simple water heater that only supports setting temperature and on and off. + """ assert len(hass.states.async_all()) == 2 climate_thermostat = hass.states.get("climate.thermostat") assert climate_thermostat.state == HVACMode.HEAT @@ -156,7 +146,7 @@ async def test_simple_climate_device( # Verify service calls - mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") + aioclient_mock = mock_put_request("/sensors/0/config") # Service turn on thermostat @@ -189,12 +179,10 @@ async def test_simple_climate_device( ) -async def test_climate_device_without_cooling_support( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test successful creation of sensor entities.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "name": "Thermostat", "type": "ZHAThermostat", @@ -209,10 +197,15 @@ async def test_climate_device_without_cooling_support( "uniqueid": "00:00:00:00:00:00:00:00-00", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +async def test_climate_device_without_cooling_support( + hass: HomeAssistant, + config_entry_setup: ConfigEntry, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + mock_deconz_websocket, +) -> None: + """Test successful creation of sensor entities.""" assert len(hass.states.async_all()) == 2 climate_thermostat = hass.states.get("climate.thermostat") assert climate_thermostat.state == HVACMode.AUTO @@ -289,7 +282,7 @@ async def test_climate_device_without_cooling_support( # Verify service calls - mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/1/config") + aioclient_mock = mock_put_request("/sensors/1/config") # Service set HVAC mode to auto @@ -355,24 +348,22 @@ async def test_climate_device_without_cooling_support( blocking=True, ) - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) states = hass.states.async_all() assert len(states) == 2 for state in states: assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 -async def test_climate_device_with_cooling_support( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test successful creation of sensor entities.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "0": { "config": { "battery": 25, @@ -399,10 +390,15 @@ async def test_climate_device_with_cooling_support( "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_climate_device_with_cooling_support( + hass: HomeAssistant, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + mock_deconz_websocket, +) -> None: + """Test successful creation of sensor entities.""" assert len(hass.states.async_all()) == 2 climate_thermostat = hass.states.get("climate.zen_01") assert climate_thermostat.state == HVACMode.HEAT @@ -458,7 +454,7 @@ async def test_climate_device_with_cooling_support( # Verify service calls - mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") + aioclient_mock = mock_put_request("/sensors/0/config") # Service set temperature to 20 @@ -471,12 +467,10 @@ async def test_climate_device_with_cooling_support( assert aioclient_mock.mock_calls[1][2] == {"coolsetpoint": 2000.0} -async def test_climate_device_with_fan_support( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test successful creation of sensor entities.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "0": { "config": { "battery": 25, @@ -503,10 +497,15 @@ async def test_climate_device_with_fan_support( "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_climate_device_with_fan_support( + hass: HomeAssistant, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + mock_deconz_websocket, +) -> None: + """Test successful creation of sensor entities.""" assert len(hass.states.async_all()) == 2 climate_thermostat = hass.states.get("climate.zen_01") assert climate_thermostat.state == HVACMode.HEAT @@ -580,7 +579,7 @@ async def test_climate_device_with_fan_support( # Verify service calls - mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") + aioclient_mock = mock_put_request("/sensors/0/config") # Service set fan mode to off @@ -613,12 +612,10 @@ async def test_climate_device_with_fan_support( ) -async def test_climate_device_with_preset( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test successful creation of sensor entities.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "0": { "config": { "battery": 25, @@ -646,10 +643,15 @@ async def test_climate_device_with_preset( "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_climate_device_with_preset( + hass: HomeAssistant, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + mock_deconz_websocket, +) -> None: + """Test successful creation of sensor entities.""" assert len(hass.states.async_all()) == 2 climate_zen_01 = hass.states.get("climate.zen_01") @@ -703,7 +705,7 @@ async def test_climate_device_with_preset( # Verify service calls - mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") + aioclient_mock = mock_put_request("/sensors/0/config") # Service set preset to HASS preset @@ -736,12 +738,10 @@ async def test_climate_device_with_preset( ) -async def test_clip_climate_device( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test successful creation of sensor entities.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "name": "Thermostat", "type": "ZHAThermostat", @@ -763,12 +763,13 @@ async def test_clip_climate_device( "uniqueid": "00:00:00:00:00:00:00:02-00", }, } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration( - hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True} - ) - + ], +) +@pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: True}]) +async def test_clip_climate_device( + hass: HomeAssistant, config_entry_setup: ConfigEntry +) -> None: + """Test successful creation of sensor entities.""" assert len(hass.states.async_all()) == 3 assert hass.states.get("climate.clip_thermostat").state == HVACMode.HEAT assert ( @@ -779,7 +780,7 @@ async def test_clip_climate_device( # Disallow clip sensors hass.config_entries.async_update_entry( - config_entry, options={CONF_ALLOW_CLIP_SENSOR: False} + config_entry_setup, options={CONF_ALLOW_CLIP_SENSOR: False} ) await hass.async_block_till_done() @@ -789,7 +790,7 @@ async def test_clip_climate_device( # Allow clip sensors hass.config_entries.async_update_entry( - config_entry, options={CONF_ALLOW_CLIP_SENSOR: True} + config_entry_setup, options={CONF_ALLOW_CLIP_SENSOR: True} ) await hass.async_block_till_done() @@ -801,12 +802,10 @@ async def test_clip_climate_device( ) -async def test_verify_state_update( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test that state update properly.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "name": "Thermostat", "type": "ZHAThermostat", @@ -821,10 +820,11 @@ async def test_verify_state_update( "uniqueid": "00:00:00:00:00:00:00:00-00", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_verify_state_update(hass: HomeAssistant, mock_deconz_websocket) -> None: + """Test that state update properly.""" assert hass.states.get("climate.thermostat").state == HVACMode.AUTO assert ( hass.states.get("climate.thermostat").attributes["hvac_action"] @@ -848,8 +848,9 @@ async def test_verify_state_update( ) +@pytest.mark.usefixtures("config_entry_setup") async def test_add_new_climate_device( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket + hass: HomeAssistant, mock_deconz_websocket ) -> None: """Test that adding a new climate device works.""" event_added_sensor = { @@ -873,7 +874,6 @@ async def test_add_new_climate_device( }, } - await setup_deconz_integration(hass, aioclient_mock) assert len(hass.states.async_all()) == 0 await mock_deconz_websocket(data=event_added_sensor) @@ -888,12 +888,10 @@ async def test_add_new_climate_device( ) -async def test_not_allow_clip_thermostat( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that CLIP thermostats are not allowed.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "1": { "name": "CLIP thermostat sensor", "type": "CLIPThermostat", @@ -902,22 +900,19 @@ async def test_not_allow_clip_thermostat( "uniqueid": "00:00:00:00:00:00:00:00-00", }, } - } - - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration( - hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: False} - ) - + ], +) +@pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: False}]) +@pytest.mark.usefixtures("config_entry_setup") +async def test_not_allow_clip_thermostat(hass: HomeAssistant) -> None: + """Test that CLIP thermostats are not allowed.""" assert len(hass.states.async_all()) == 0 -async def test_no_mode_no_state( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test that a climate device without mode and state works.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "0": { "config": { "battery": 25, @@ -939,10 +934,11 @@ async def test_no_mode_no_state( "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_no_mode_no_state(hass: HomeAssistant) -> None: + """Test that a climate device without mode and state works.""" assert len(hass.states.async_all()) == 2 climate_thermostat = hass.states.get("climate.zen_01") @@ -951,16 +947,11 @@ async def test_no_mode_no_state( assert climate_thermostat.attributes["preset_mode"] is DECONZ_PRESET_AUTO assert climate_thermostat.attributes["hvac_action"] is HVACAction.IDLE - # Verify service calls - mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") - -async def test_boost_mode( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Test that a climate device with boost mode and different state works.""" - data = { - "sensors": { +@pytest.mark.parametrize( + "sensor_payload", + [ + { "0": { "config": { "battery": 58, @@ -994,9 +985,11 @@ async def test_boost_mode( "uniqueid": "84:fd:27:ff:fe:8a:eb:89-01-0201", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_boost_mode(hass: HomeAssistant, mock_deconz_websocket) -> None: + """Test that a climate device with boost mode and different state works.""" assert len(hass.states.async_all()) == 3 @@ -1023,6 +1016,3 @@ async def test_boost_mode( climate_thermostat = hass.states.get("climate.thermostat") assert climate_thermostat.attributes["preset_mode"] is PRESET_BOOST assert climate_thermostat.attributes["hvac_action"] is HVACAction.HEATING - - # Verify service calls - mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") From b054c037fe9429f6b08a974f8c82cb62b0ce3a0d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 5 Jul 2024 09:05:21 +0200 Subject: [PATCH 0777/2411] Improve type hints in hassio tests (#121221) --- tests/components/hassio/conftest.py | 24 ++++++--- tests/components/hassio/test_addon_panel.py | 5 +- tests/components/hassio/test_auth.py | 29 +++++------ tests/components/hassio/test_discovery.py | 21 ++++++-- tests/components/hassio/test_handler.py | 18 ++++--- tests/components/hassio/test_http.py | 51 ++++++++++--------- tests/components/hassio/test_init.py | 9 ++-- tests/components/hassio/test_issues.py | 43 ++++++++-------- tests/components/hassio/test_repairs.py | 21 ++++---- tests/components/hassio/test_websocket_api.py | 11 ++-- 10 files changed, 135 insertions(+), 97 deletions(-) diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 7b79dfe6179..db1a07c4df3 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -1,5 +1,6 @@ """Fixtures for Hass.io.""" +from collections.abc import Generator import os import re from unittest.mock import Mock, patch @@ -7,6 +8,7 @@ from unittest.mock import Mock, patch from aiohttp.test_utils import TestClient import pytest +from homeassistant.auth.models import RefreshToken from homeassistant.components.hassio.handler import HassIO, HassioAPIError from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -19,7 +21,7 @@ from tests.typing import ClientSessionGenerator @pytest.fixture(autouse=True) -def disable_security_filter(): +def disable_security_filter() -> Generator[None]: """Disable the security filter to ensure the integration is secure.""" with patch( "homeassistant.components.http.security_filter.FILTERS", @@ -29,7 +31,7 @@ def disable_security_filter(): @pytest.fixture -def hassio_env(): +def hassio_env() -> Generator[None]: """Fixture to inject hassio env.""" with ( patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), @@ -48,11 +50,11 @@ def hassio_env(): @pytest.fixture def hassio_stubs( - hassio_env, + hassio_env: None, hass: HomeAssistant, hass_client: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, -): +) -> RefreshToken: """Create mock hassio http client.""" with ( patch( @@ -86,7 +88,7 @@ def hassio_stubs( @pytest.fixture def hassio_client( - hassio_stubs, hass: HomeAssistant, hass_client: ClientSessionGenerator + hassio_stubs: RefreshToken, hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> TestClient: """Return a Hass.io HTTP client.""" return hass.loop.run_until_complete(hass_client()) @@ -94,7 +96,9 @@ def hassio_client( @pytest.fixture def hassio_noauth_client( - hassio_stubs, hass: HomeAssistant, aiohttp_client: ClientSessionGenerator + hassio_stubs: RefreshToken, + hass: HomeAssistant, + aiohttp_client: ClientSessionGenerator, ) -> TestClient: """Return a Hass.io HTTP client without auth.""" return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @@ -102,7 +106,9 @@ def hassio_noauth_client( @pytest.fixture async def hassio_client_supervisor( - hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, hassio_stubs + hass: HomeAssistant, + aiohttp_client: ClientSessionGenerator, + hassio_stubs: RefreshToken, ) -> TestClient: """Return an authenticated HTTP client.""" access_token = hass.auth.async_create_access_token(hassio_stubs) @@ -113,7 +119,9 @@ async def hassio_client_supervisor( @pytest.fixture -async def hassio_handler(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): +def hassio_handler( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> Generator[HassIO]: """Create mock hassio handler.""" with patch.dict(os.environ, {"SUPERVISOR_TOKEN": SUPERVISOR_TOKEN}): yield HassIO(hass.loop, async_get_clientsession(hass), "127.0.0.1") diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py index 8436b3393b9..f7407152f7e 100644 --- a/tests/components/hassio/test_addon_panel.py +++ b/tests/components/hassio/test_addon_panel.py @@ -24,8 +24,9 @@ def mock_all(aioclient_mock: AiohttpClientMocker) -> None: ) +@pytest.mark.usefixtures("hassio_env") async def test_hassio_addon_panel_startup( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hassio_env + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test startup and panel setup after event.""" aioclient_mock.get( @@ -68,10 +69,10 @@ async def test_hassio_addon_panel_startup( ) +@pytest.mark.usefixtures("hassio_env") async def test_hassio_addon_panel_api( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - hassio_env, hass_client: ClientSessionGenerator, ) -> None: """Test panel api after event.""" diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index 175d9061d56..ad96b58e99d 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -3,11 +3,12 @@ from http import HTTPStatus from unittest.mock import Mock, patch +from aiohttp.test_utils import TestClient + from homeassistant.auth.providers.homeassistant import InvalidAuth -from homeassistant.core import HomeAssistant -async def test_auth_success(hass: HomeAssistant, hassio_client_supervisor) -> None: +async def test_auth_success(hassio_client_supervisor: TestClient) -> None: """Test no auth needed for .""" with patch( "homeassistant.auth.providers.homeassistant." @@ -23,7 +24,7 @@ async def test_auth_success(hass: HomeAssistant, hassio_client_supervisor) -> No mock_login.assert_called_with("test", "123456") -async def test_auth_fails_no_supervisor(hass: HomeAssistant, hassio_client) -> None: +async def test_auth_fails_no_supervisor(hassio_client: TestClient) -> None: """Test if only supervisor can access.""" with patch( "homeassistant.auth.providers.homeassistant." @@ -39,7 +40,7 @@ async def test_auth_fails_no_supervisor(hass: HomeAssistant, hassio_client) -> N assert not mock_login.called -async def test_auth_fails_no_auth(hass: HomeAssistant, hassio_noauth_client) -> None: +async def test_auth_fails_no_auth(hassio_noauth_client: TestClient) -> None: """Test if only supervisor can access.""" with patch( "homeassistant.auth.providers.homeassistant." @@ -55,7 +56,7 @@ async def test_auth_fails_no_auth(hass: HomeAssistant, hassio_noauth_client) -> assert not mock_login.called -async def test_login_error(hass: HomeAssistant, hassio_client_supervisor) -> None: +async def test_login_error(hassio_client_supervisor: TestClient) -> None: """Test no auth needed for error.""" with patch( "homeassistant.auth.providers.homeassistant." @@ -72,7 +73,7 @@ async def test_login_error(hass: HomeAssistant, hassio_client_supervisor) -> Non mock_login.assert_called_with("test", "123456") -async def test_login_no_data(hass: HomeAssistant, hassio_client_supervisor) -> None: +async def test_login_no_data(hassio_client_supervisor: TestClient) -> None: """Test auth with no data -> error.""" with patch( "homeassistant.auth.providers.homeassistant." @@ -86,7 +87,7 @@ async def test_login_no_data(hass: HomeAssistant, hassio_client_supervisor) -> N assert not mock_login.called -async def test_login_no_username(hass: HomeAssistant, hassio_client_supervisor) -> None: +async def test_login_no_username(hassio_client_supervisor: TestClient) -> None: """Test auth with no username in data -> error.""" with patch( "homeassistant.auth.providers.homeassistant." @@ -102,9 +103,7 @@ async def test_login_no_username(hass: HomeAssistant, hassio_client_supervisor) assert not mock_login.called -async def test_login_success_extra( - hass: HomeAssistant, hassio_client_supervisor -) -> None: +async def test_login_success_extra(hassio_client_supervisor: TestClient) -> None: """Test auth with extra data.""" with patch( "homeassistant.auth.providers.homeassistant." @@ -125,7 +124,7 @@ async def test_login_success_extra( mock_login.assert_called_with("test", "123456") -async def test_password_success(hass: HomeAssistant, hassio_client_supervisor) -> None: +async def test_password_success(hassio_client_supervisor: TestClient) -> None: """Test no auth needed for .""" with patch( "homeassistant.auth.providers.homeassistant." @@ -141,7 +140,7 @@ async def test_password_success(hass: HomeAssistant, hassio_client_supervisor) - mock_change.assert_called_with("test", "123456") -async def test_password_fails_no_supervisor(hass: HomeAssistant, hassio_client) -> None: +async def test_password_fails_no_supervisor(hassio_client: TestClient) -> None: """Test if only supervisor can access.""" resp = await hassio_client.post( "/api/hassio_auth/password_reset", @@ -152,9 +151,7 @@ async def test_password_fails_no_supervisor(hass: HomeAssistant, hassio_client) assert resp.status == HTTPStatus.UNAUTHORIZED -async def test_password_fails_no_auth( - hass: HomeAssistant, hassio_noauth_client -) -> None: +async def test_password_fails_no_auth(hassio_noauth_client: TestClient) -> None: """Test if only supervisor can access.""" resp = await hassio_noauth_client.post( "/api/hassio_auth/password_reset", @@ -165,7 +162,7 @@ async def test_password_fails_no_auth( assert resp.status == HTTPStatus.UNAUTHORIZED -async def test_password_no_user(hass: HomeAssistant, hassio_client_supervisor) -> None: +async def test_password_no_user(hassio_client_supervisor: TestClient) -> None: """Test changing password for invalid user.""" resp = await hassio_client_supervisor.post( "/api/hassio_auth/password_reset", diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 0783ee77932..305b863b3af 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -1,8 +1,10 @@ """Test config flow.""" +from collections.abc import Generator from http import HTTPStatus from unittest.mock import AsyncMock, Mock, patch +from aiohttp.test_utils import TestClient import pytest from homeassistant import config_entries @@ -18,7 +20,9 @@ from tests.test_util.aiohttp import AiohttpClientMocker @pytest.fixture(name="mock_mqtt") -async def mock_mqtt_fixture(hass): +def mock_mqtt_fixture( + hass: HomeAssistant, +) -> Generator[type[config_entries.ConfigFlow]]: """Mock the MQTT integration's config flow.""" mock_integration(hass, MockModule(MQTT_DOMAIN)) mock_platform(hass, f"{MQTT_DOMAIN}.config_flow", None) @@ -34,8 +38,11 @@ async def mock_mqtt_fixture(hass): yield MqttFlow +@pytest.mark.usefixtures("hassio_client") async def test_hassio_discovery_startup( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hassio_client, mock_mqtt + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_mqtt: type[config_entries.ConfigFlow], ) -> None: """Test startup and discovery after event.""" aioclient_mock.get( @@ -90,8 +97,11 @@ async def test_hassio_discovery_startup( ) +@pytest.mark.usefixtures("hassio_client") async def test_hassio_discovery_startup_done( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hassio_client, mock_mqtt + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_mqtt: type[config_entries.ConfigFlow], ) -> None: """Test startup and discovery with hass discovery.""" aioclient_mock.post( @@ -159,7 +169,10 @@ async def test_hassio_discovery_startup_done( async def test_hassio_discovery_webhook( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hassio_client, mock_mqtt + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hassio_client: TestClient, + mock_mqtt: type[config_entries.ConfigFlow], ) -> None: """Test discovery webhook.""" aioclient_mock.get( diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index c418576a802..c5fa6ff8254 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -365,8 +365,9 @@ async def test_api_headers( assert received_request.headers[hdrs.CONTENT_TYPE] == "application/octet-stream" +@pytest.mark.usefixtures("hassio_stubs") async def test_api_get_green_settings( - hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test setup with API ping.""" aioclient_mock.get( @@ -389,8 +390,9 @@ async def test_api_get_green_settings( assert aioclient_mock.call_count == 1 +@pytest.mark.usefixtures("hassio_stubs") async def test_api_set_green_settings( - hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test setup with API ping.""" aioclient_mock.post( @@ -407,8 +409,9 @@ async def test_api_set_green_settings( assert aioclient_mock.call_count == 1 +@pytest.mark.usefixtures("hassio_stubs") async def test_api_get_yellow_settings( - hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test setup with API ping.""" aioclient_mock.get( @@ -427,8 +430,9 @@ async def test_api_get_yellow_settings( assert aioclient_mock.call_count == 1 +@pytest.mark.usefixtures("hassio_stubs") async def test_api_set_yellow_settings( - hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test setup with API ping.""" aioclient_mock.post( @@ -445,8 +449,9 @@ async def test_api_set_yellow_settings( assert aioclient_mock.call_count == 1 +@pytest.mark.usefixtures("hassio_stubs") async def test_api_reboot_host( - hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test setup with API ping.""" aioclient_mock.post( @@ -458,7 +463,8 @@ async def test_api_reboot_host( assert aioclient_mock.call_count == 1 -async def test_send_command_invalid_command(hass: HomeAssistant, hassio_stubs) -> None: +@pytest.mark.usefixtures("hassio_stubs") +async def test_send_command_invalid_command(hass: HomeAssistant) -> None: """Test send command fails when command is invalid.""" hassio: HassIO = hass.data["hassio"] with pytest.raises(HassioAPIError): diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index a5ffb4f0d83..404c047a56c 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -1,9 +1,11 @@ """The tests for the hassio component.""" +from collections.abc import Generator from http import HTTPStatus from unittest.mock import patch from aiohttp import StreamReader +from aiohttp.test_utils import TestClient import pytest from tests.common import MockUser @@ -11,7 +13,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker @pytest.fixture -def mock_not_onboarded(): +def mock_not_onboarded() -> Generator[None]: """Mock that we're not onboarded.""" with patch( "homeassistant.components.hassio.http.async_is_onboarded", return_value=False @@ -20,7 +22,9 @@ def mock_not_onboarded(): @pytest.fixture -def hassio_user_client(hassio_client, hass_admin_user: MockUser): +def hassio_user_client( + hassio_client: TestClient, hass_admin_user: MockUser +) -> TestClient: """Return a Hass.io HTTP client tied to a non-admin user.""" hass_admin_user.groups = [] return hassio_client @@ -35,7 +39,7 @@ def hassio_user_client(hassio_client, hass_admin_user: MockUser): ], ) async def test_forward_request_onboarded_user_get( - hassio_user_client, aioclient_mock: AiohttpClientMocker, path: str + hassio_user_client: TestClient, aioclient_mock: AiohttpClientMocker, path: str ) -> None: """Test fetching normal path.""" aioclient_mock.get(f"http://127.0.0.1/{path}", text="response") @@ -55,7 +59,7 @@ async def test_forward_request_onboarded_user_get( @pytest.mark.parametrize("method", ["POST", "PUT", "DELETE", "RANDOM"]) async def test_forward_request_onboarded_user_unallowed_methods( - hassio_user_client, aioclient_mock: AiohttpClientMocker, method: str + hassio_user_client: TestClient, aioclient_mock: AiohttpClientMocker, method: str ) -> None: """Test fetching normal path.""" resp = await hassio_user_client.post("/api/hassio/app/entrypoint.js") @@ -82,7 +86,7 @@ async def test_forward_request_onboarded_user_unallowed_methods( ], ) async def test_forward_request_onboarded_user_unallowed_paths( - hassio_user_client, + hassio_user_client: TestClient, aioclient_mock: AiohttpClientMocker, bad_path: str, expected_status: int, @@ -105,7 +109,7 @@ async def test_forward_request_onboarded_user_unallowed_paths( ], ) async def test_forward_request_onboarded_noauth_get( - hassio_noauth_client, aioclient_mock: AiohttpClientMocker, path: str + hassio_noauth_client: TestClient, aioclient_mock: AiohttpClientMocker, path: str ) -> None: """Test fetching normal path.""" aioclient_mock.get(f"http://127.0.0.1/{path}", text="response") @@ -125,7 +129,7 @@ async def test_forward_request_onboarded_noauth_get( @pytest.mark.parametrize("method", ["POST", "PUT", "DELETE", "RANDOM"]) async def test_forward_request_onboarded_noauth_unallowed_methods( - hassio_noauth_client, aioclient_mock: AiohttpClientMocker, method: str + hassio_noauth_client: TestClient, aioclient_mock: AiohttpClientMocker, method: str ) -> None: """Test fetching normal path.""" resp = await hassio_noauth_client.post("/api/hassio/app/entrypoint.js") @@ -152,7 +156,7 @@ async def test_forward_request_onboarded_noauth_unallowed_methods( ], ) async def test_forward_request_onboarded_noauth_unallowed_paths( - hassio_noauth_client, + hassio_noauth_client: TestClient, aioclient_mock: AiohttpClientMocker, bad_path: str, expected_status: int, @@ -176,7 +180,7 @@ async def test_forward_request_onboarded_noauth_unallowed_paths( ], ) async def test_forward_request_not_onboarded_get( - hassio_noauth_client, + hassio_noauth_client: TestClient, aioclient_mock: AiohttpClientMocker, path: str, authenticated: bool, @@ -212,7 +216,7 @@ async def test_forward_request_not_onboarded_get( ], ) async def test_forward_request_not_onboarded_post( - hassio_noauth_client, + hassio_noauth_client: TestClient, aioclient_mock: AiohttpClientMocker, path: str, mock_not_onboarded, @@ -238,7 +242,7 @@ async def test_forward_request_not_onboarded_post( @pytest.mark.parametrize("method", ["POST", "PUT", "DELETE", "RANDOM"]) async def test_forward_request_not_onboarded_unallowed_methods( - hassio_noauth_client, aioclient_mock: AiohttpClientMocker, method: str + hassio_noauth_client: TestClient, aioclient_mock: AiohttpClientMocker, method: str ) -> None: """Test fetching normal path.""" resp = await hassio_noauth_client.post("/api/hassio/app/entrypoint.js") @@ -265,7 +269,7 @@ async def test_forward_request_not_onboarded_unallowed_methods( ], ) async def test_forward_request_not_onboarded_unallowed_paths( - hassio_noauth_client, + hassio_noauth_client: TestClient, aioclient_mock: AiohttpClientMocker, bad_path: str, expected_status: int, @@ -294,7 +298,7 @@ async def test_forward_request_not_onboarded_unallowed_paths( ], ) async def test_forward_request_admin_get( - hassio_client, + hassio_client: TestClient, aioclient_mock: AiohttpClientMocker, path: str, authenticated: bool, @@ -329,7 +333,7 @@ async def test_forward_request_admin_get( ], ) async def test_forward_request_admin_post( - hassio_client, + hassio_client: TestClient, aioclient_mock: AiohttpClientMocker, path: str, ) -> None: @@ -354,7 +358,7 @@ async def test_forward_request_admin_post( @pytest.mark.parametrize("method", ["POST", "PUT", "DELETE", "RANDOM"]) async def test_forward_request_admin_unallowed_methods( - hassio_client, aioclient_mock: AiohttpClientMocker, method: str + hassio_client: TestClient, aioclient_mock: AiohttpClientMocker, method: str ) -> None: """Test fetching normal path.""" resp = await hassio_client.post("/api/hassio/app/entrypoint.js") @@ -379,7 +383,7 @@ async def test_forward_request_admin_unallowed_methods( ], ) async def test_forward_request_admin_unallowed_paths( - hassio_client, + hassio_client: TestClient, aioclient_mock: AiohttpClientMocker, bad_path: str, expected_status: int, @@ -394,7 +398,7 @@ async def test_forward_request_admin_unallowed_paths( async def test_bad_gateway_when_cannot_find_supervisor( - hassio_client, aioclient_mock: AiohttpClientMocker + hassio_client: TestClient, aioclient_mock: AiohttpClientMocker ) -> None: """Test we get a bad gateway error if we can't find supervisor.""" aioclient_mock.get("http://127.0.0.1/app/entrypoint.js", exc=TimeoutError) @@ -404,9 +408,8 @@ async def test_bad_gateway_when_cannot_find_supervisor( async def test_backup_upload_headers( - hassio_client, + hassio_client: TestClient, aioclient_mock: AiohttpClientMocker, - caplog: pytest.LogCaptureFixture, mock_not_onboarded, ) -> None: """Test that we forward the full header for backup upload.""" @@ -427,7 +430,7 @@ async def test_backup_upload_headers( async def test_backup_download_headers( - hassio_client, aioclient_mock: AiohttpClientMocker, mock_not_onboarded + hassio_client: TestClient, aioclient_mock: AiohttpClientMocker, mock_not_onboarded ) -> None: """Test that we forward the full header for backup download.""" content_disposition = "attachment; filename=test.tar" @@ -449,7 +452,9 @@ async def test_backup_download_headers( assert resp.headers["Content-Disposition"] == content_disposition -async def test_stream(hassio_client, aioclient_mock: AiohttpClientMocker) -> None: +async def test_stream( + hassio_client: TestClient, aioclient_mock: AiohttpClientMocker +) -> None: """Verify that the request is a stream.""" content_type = "multipart/form-data; boundary='--webkit'" aioclient_mock.post("http://127.0.0.1/backups/new/upload") @@ -462,7 +467,7 @@ async def test_stream(hassio_client, aioclient_mock: AiohttpClientMocker) -> Non async def test_simple_get_no_stream( - hassio_client, aioclient_mock: AiohttpClientMocker + hassio_client: TestClient, aioclient_mock: AiohttpClientMocker ) -> None: """Verify that a simple GET request is not a stream.""" aioclient_mock.get("http://127.0.0.1/app/entrypoint.js") @@ -472,7 +477,7 @@ async def test_simple_get_no_stream( async def test_entrypoint_cache_control( - hassio_client, aioclient_mock: AiohttpClientMocker + hassio_client: TestClient, aioclient_mock: AiohttpClientMocker ) -> None: """Test that we return cache control for requests to the entrypoint only.""" aioclient_mock.get("http://127.0.0.1/app/entrypoint.js") diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 0246b557ee4..d71e8acfbe0 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -486,7 +486,8 @@ async def test_warn_when_cannot_connect( assert "Not connected with the supervisor / system too busy!" in caplog.text -async def test_service_register(hassio_env, hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("hassio_env") +async def test_service_register(hass: HomeAssistant) -> None: """Check if service will be setup.""" assert await async_setup_component(hass, "hassio", {}) assert hass.services.has_service("hassio", "addon_start") @@ -717,8 +718,9 @@ async def test_addon_service_call_with_complex_slug( await hass.services.async_call("hassio", "addon_start", {"addon": "test.a_1-2"}) +@pytest.mark.usefixtures("hassio_env") async def test_service_calls_core( - hassio_env, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Call core service and check the API calls behind that.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -1116,8 +1118,9 @@ async def test_setup_hardware_integration( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("hassio_stubs") async def test_get_store_addon_info( - hass: HomeAssistant, hassio_stubs, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test get store add-on info from Supervisor API.""" aioclient_mock.clear_requests() diff --git a/tests/components/hassio/test_issues.py b/tests/components/hassio/test_issues.py index ff0e4a8dd92..1a3d3d83f95 100644 --- a/tests/components/hassio/test_issues.py +++ b/tests/components/hassio/test_issues.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator from datetime import timedelta from http import HTTPStatus import os @@ -22,13 +23,13 @@ from tests.typing import WebSocketGenerator @pytest.fixture(autouse=True) -async def setup_repairs(hass): +async def setup_repairs(hass: HomeAssistant) -> None: """Set up the repairs integration.""" assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) @pytest.fixture(autouse=True) -async def fixture_supervisor_environ(): +def fixture_supervisor_environ() -> Generator[None]: """Mock os environ for supervisor.""" with patch.dict(os.environ, MOCK_ENVIRON): yield @@ -40,7 +41,7 @@ def mock_resolution_info( unhealthy: list[str] | None = None, issues: list[dict[str, str]] | None = None, suggestion_result: str = "ok", -): +) -> None: """Mock resolution/info endpoint with unsupported/unhealthy reasons and/or issues.""" aioclient_mock.get( "http://127.0.0.1/resolution/info", @@ -80,7 +81,9 @@ def mock_resolution_info( ) -def assert_repair_in_list(issues: list[dict[str, Any]], unhealthy: bool, reason: str): +def assert_repair_in_list( + issues: list[dict[str, Any]], unhealthy: bool, reason: str +) -> None: """Assert repair for unhealthy/unsupported in list.""" repair_type = "unhealthy" if unhealthy else "unsupported" assert { @@ -108,7 +111,7 @@ def assert_issue_repair_in_list( *, reference: str | None = None, placeholders: dict[str, str] | None = None, -): +) -> None: """Assert repair for unhealthy/unsupported in list.""" if reference: placeholders = (placeholders or {}) | {"reference": reference} @@ -128,11 +131,11 @@ def assert_issue_repair_in_list( } in issues +@pytest.mark.usefixtures("all_setup_requests") async def test_unhealthy_issues( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, - all_setup_requests, ) -> None: """Test issues added for unhealthy systems.""" mock_resolution_info(aioclient_mock, unhealthy=["docker", "setup"]) @@ -150,11 +153,11 @@ async def test_unhealthy_issues( assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="setup") +@pytest.mark.usefixtures("all_setup_requests") async def test_unsupported_issues( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, - all_setup_requests, ) -> None: """Test issues added for unsupported systems.""" mock_resolution_info(aioclient_mock, unsupported=["content_trust", "os"]) @@ -174,11 +177,11 @@ async def test_unsupported_issues( assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os") +@pytest.mark.usefixtures("all_setup_requests") async def test_unhealthy_issues_add_remove( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, - all_setup_requests, ) -> None: """Test unhealthy issues added and removed from dispatches.""" mock_resolution_info(aioclient_mock) @@ -231,11 +234,11 @@ async def test_unhealthy_issues_add_remove( assert msg["result"] == {"issues": []} +@pytest.mark.usefixtures("all_setup_requests") async def test_unsupported_issues_add_remove( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, - all_setup_requests, ) -> None: """Test unsupported issues added and removed from dispatches.""" mock_resolution_info(aioclient_mock) @@ -288,11 +291,11 @@ async def test_unsupported_issues_add_remove( assert msg["result"] == {"issues": []} +@pytest.mark.usefixtures("all_setup_requests") async def test_reset_issues_supervisor_restart( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, - all_setup_requests, ) -> None: """All issues reset on supervisor restart.""" mock_resolution_info( @@ -352,11 +355,11 @@ async def test_reset_issues_supervisor_restart( assert msg["result"] == {"issues": []} +@pytest.mark.usefixtures("all_setup_requests") async def test_reasons_added_and_removed( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, - all_setup_requests, ) -> None: """Test an unsupported/unhealthy reasons being added and removed at same time.""" mock_resolution_info(aioclient_mock, unsupported=["os"], unhealthy=["docker"]) @@ -402,11 +405,11 @@ async def test_reasons_added_and_removed( ) +@pytest.mark.usefixtures("all_setup_requests") async def test_ignored_unsupported_skipped( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, - all_setup_requests, ) -> None: """Unsupported reasons which have an identical unhealthy reason are ignored.""" mock_resolution_info( @@ -425,11 +428,11 @@ async def test_ignored_unsupported_skipped( assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="privileged") +@pytest.mark.usefixtures("all_setup_requests") async def test_new_unsupported_unhealthy_reason( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, - all_setup_requests, ) -> None: """New unsupported/unhealthy reasons result in a generic repair until next core update.""" mock_resolution_info( @@ -475,11 +478,11 @@ async def test_new_unsupported_unhealthy_reason( } in msg["result"]["issues"] +@pytest.mark.usefixtures("all_setup_requests") async def test_supervisor_issues( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, - all_setup_requests, ) -> None: """Test repairs added for supervisor issue.""" mock_resolution_info( @@ -541,12 +544,12 @@ async def test_supervisor_issues( ) +@pytest.mark.usefixtures("all_setup_requests") async def test_supervisor_issues_initial_failure( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, freezer: FrozenDateTimeFactory, - all_setup_requests, ) -> None: """Test issues manager retries after initial update failure.""" responses = [ @@ -619,11 +622,11 @@ async def test_supervisor_issues_initial_failure( assert len(msg["result"]["issues"]) == 1 +@pytest.mark.usefixtures("all_setup_requests") async def test_supervisor_issues_add_remove( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, - all_setup_requests, ) -> None: """Test supervisor issues added and removed from dispatches.""" mock_resolution_info(aioclient_mock) @@ -730,11 +733,11 @@ async def test_supervisor_issues_add_remove( assert msg["result"] == {"issues": []} +@pytest.mark.usefixtures("all_setup_requests") async def test_supervisor_issues_suggestions_fail( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, - all_setup_requests, ) -> None: """Test failing to get suggestions for issue skips it.""" aioclient_mock.get( @@ -776,11 +779,11 @@ async def test_supervisor_issues_suggestions_fail( assert len(msg["result"]["issues"]) == 0 +@pytest.mark.usefixtures("all_setup_requests") async def test_supervisor_remove_missing_issue_without_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, - all_setup_requests, ) -> None: """Test HA skips message to remove issue that it didn't know about (sync issue).""" mock_resolution_info(aioclient_mock) @@ -810,11 +813,11 @@ async def test_supervisor_remove_missing_issue_without_error( await hass.async_block_till_done() +@pytest.mark.usefixtures("all_setup_requests") async def test_system_is_not_ready( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, - all_setup_requests, ) -> None: """Ensure hassio starts despite error.""" aioclient_mock.get( @@ -832,11 +835,11 @@ async def test_system_is_not_ready( @pytest.mark.parametrize( "all_setup_requests", [{"include_addons": True}], indirect=True ) +@pytest.mark.usefixtures("all_setup_requests") async def test_supervisor_issues_detached_addon_missing( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, - all_setup_requests, ) -> None: """Test supervisor issue for detached addon due to missing repository.""" mock_resolution_info(aioclient_mock) diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index 8d0bbfac87c..907529ec9c4 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -1,5 +1,6 @@ """Test supervisor repairs.""" +from collections.abc import Generator from http import HTTPStatus import os from unittest.mock import patch @@ -18,18 +19,18 @@ from tests.typing import ClientSessionGenerator @pytest.fixture(autouse=True) -async def fixture_supervisor_environ(): +def fixture_supervisor_environ() -> Generator[None]: """Mock os environ for supervisor.""" with patch.dict(os.environ, MOCK_ENVIRON): yield +@pytest.mark.usefixtures("all_setup_requests") async def test_supervisor_issue_repair_flow( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_client: ClientSessionGenerator, issue_registry: ir.IssueRegistry, - all_setup_requests, ) -> None: """Test fix flow for supervisor issue.""" mock_resolution_info( @@ -103,12 +104,12 @@ async def test_supervisor_issue_repair_flow( ) +@pytest.mark.usefixtures("all_setup_requests") async def test_supervisor_issue_repair_flow_with_multiple_suggestions( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_client: ClientSessionGenerator, issue_registry: ir.IssueRegistry, - all_setup_requests, ) -> None: """Test fix flow for supervisor issue with multiple suggestions.""" mock_resolution_info( @@ -197,12 +198,12 @@ async def test_supervisor_issue_repair_flow_with_multiple_suggestions( ) +@pytest.mark.usefixtures("all_setup_requests") async def test_supervisor_issue_repair_flow_with_multiple_suggestions_and_confirmation( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_client: ClientSessionGenerator, issue_registry: ir.IssueRegistry, - all_setup_requests, ) -> None: """Test fix flow for supervisor issue with multiple suggestions and choice requires confirmation.""" mock_resolution_info( @@ -310,12 +311,12 @@ async def test_supervisor_issue_repair_flow_with_multiple_suggestions_and_confir ) +@pytest.mark.usefixtures("all_setup_requests") async def test_supervisor_issue_repair_flow_skip_confirmation( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_client: ClientSessionGenerator, issue_registry: ir.IssueRegistry, - all_setup_requests, ) -> None: """Test confirmation skipped for fix flow for supervisor issue with one suggestion.""" mock_resolution_info( @@ -389,12 +390,12 @@ async def test_supervisor_issue_repair_flow_skip_confirmation( ) +@pytest.mark.usefixtures("all_setup_requests") async def test_mount_failed_repair_flow_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_client: ClientSessionGenerator, issue_registry: ir.IssueRegistry, - all_setup_requests, ) -> None: """Test repair flow fails when repair fails to apply.""" mock_resolution_info( @@ -461,12 +462,12 @@ async def test_mount_failed_repair_flow_error( assert issue_registry.async_get_issue(domain="hassio", issue_id="1234") +@pytest.mark.usefixtures("all_setup_requests") async def test_mount_failed_repair_flow( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_client: ClientSessionGenerator, issue_registry: ir.IssueRegistry, - all_setup_requests, ) -> None: """Test repair flow for mount_failed issue.""" mock_resolution_info( @@ -562,12 +563,12 @@ async def test_mount_failed_repair_flow( @pytest.mark.parametrize( "all_setup_requests", [{"include_addons": True}], indirect=True ) +@pytest.mark.usefixtures("all_setup_requests") async def test_supervisor_issue_docker_config_repair_flow( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_client: ClientSessionGenerator, issue_registry: ir.IssueRegistry, - all_setup_requests, ) -> None: """Test fix flow for supervisor issue.""" mock_resolution_info( @@ -669,12 +670,12 @@ async def test_supervisor_issue_docker_config_repair_flow( ) +@pytest.mark.usefixtures("all_setup_requests") async def test_supervisor_issue_repair_flow_multiple_data_disks( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_client: ClientSessionGenerator, issue_registry: ir.IssueRegistry, - all_setup_requests, ) -> None: """Test fix flow for multiple data disks supervisor issue.""" mock_resolution_info( @@ -785,12 +786,12 @@ async def test_supervisor_issue_repair_flow_multiple_data_disks( @pytest.mark.parametrize( "all_setup_requests", [{"include_addons": True}], indirect=True ) +@pytest.mark.usefixtures("all_setup_requests") async def test_supervisor_issue_detached_addon_removed( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_client: ClientSessionGenerator, issue_registry: ir.IssueRegistry, - all_setup_requests, ) -> None: """Test fix flow for supervisor issue.""" mock_resolution_info( diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index f3be391d9b7..7d8f07bfaec 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -79,8 +79,9 @@ def mock_all(aioclient_mock: AiohttpClientMocker) -> None: ) +@pytest.mark.usefixtures("hassio_env") async def test_ws_subscription( - hassio_env, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test websocket subscription.""" assert await async_setup_component(hass, "hassio", {}) @@ -116,8 +117,8 @@ async def test_ws_subscription( assert response["success"] +@pytest.mark.usefixtures("hassio_env") async def test_websocket_supervisor_api( - hassio_env, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, aioclient_mock: AiohttpClientMocker, @@ -160,8 +161,8 @@ async def test_websocket_supervisor_api( } +@pytest.mark.usefixtures("hassio_env") async def test_websocket_supervisor_api_error( - hassio_env, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, aioclient_mock: AiohttpClientMocker, @@ -189,8 +190,8 @@ async def test_websocket_supervisor_api_error( assert msg["error"]["message"] == "example error" +@pytest.mark.usefixtures("hassio_env") async def test_websocket_supervisor_api_error_without_msg( - hassio_env, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, aioclient_mock: AiohttpClientMocker, @@ -218,8 +219,8 @@ async def test_websocket_supervisor_api_error_without_msg( assert msg["error"]["message"] == "" +@pytest.mark.usefixtures("hassio_env") async def test_websocket_non_admin_user( - hassio_env, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, aioclient_mock: AiohttpClientMocker, From c3830a58cc89a2b26870731dbdcf48b8f9d450e3 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Fri, 5 Jul 2024 09:12:47 +0200 Subject: [PATCH 0778/2411] Bump velbusaio to 2024.7.5 (#121156) * Bump velbusaio to 2024.7.4 * bump to 2024.7.5 to remove print functions --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index f778533cad8..4e9478ae575 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -13,7 +13,7 @@ "velbus-packet", "velbus-protocol" ], - "requirements": ["velbus-aio==2024.5.1"], + "requirements": ["velbus-aio==2024.7.5"], "usb": [ { "vid": "10CF", diff --git a/requirements_all.txt b/requirements_all.txt index 8b7b2951221..d7826847b5f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2833,7 +2833,7 @@ vallox-websocket-api==5.3.0 vehicle==2.2.1 # homeassistant.components.velbus -velbus-aio==2024.5.1 +velbus-aio==2024.7.5 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 958e3723796..98a89013d56 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2207,7 +2207,7 @@ vallox-websocket-api==5.3.0 vehicle==2.2.1 # homeassistant.components.velbus -velbus-aio==2024.5.1 +velbus-aio==2024.7.5 # homeassistant.components.venstar venstarcolortouch==0.19 From def27a082c9d0a792713709bdd40cde29381d5ed Mon Sep 17 00:00:00 2001 From: Matthew FitzGerald-Chamberlain Date: Fri, 5 Jul 2024 02:19:24 -0500 Subject: [PATCH 0779/2411] Add Aprilaire humidifier (#120270) --- .coveragerc | 1 + .../components/aprilaire/__init__.py | 1 + .../components/aprilaire/humidifier.py | 194 ++++++++++++++++++ .../components/aprilaire/strings.json | 8 + 4 files changed, 204 insertions(+) create mode 100644 homeassistant/components/aprilaire/humidifier.py diff --git a/.coveragerc b/.coveragerc index 27001654d72..a548ba81f6f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -82,6 +82,7 @@ omit = homeassistant/components/aprilaire/climate.py homeassistant/components/aprilaire/coordinator.py homeassistant/components/aprilaire/entity.py + homeassistant/components/aprilaire/humidifier.py homeassistant/components/aprilaire/select.py homeassistant/components/aprilaire/sensor.py homeassistant/components/apsystems/__init__.py diff --git a/homeassistant/components/aprilaire/__init__.py b/homeassistant/components/aprilaire/__init__.py index 9747a4d40a4..fd7fd745c5d 100644 --- a/homeassistant/components/aprilaire/__init__.py +++ b/homeassistant/components/aprilaire/__init__.py @@ -17,6 +17,7 @@ from .coordinator import AprilaireCoordinator PLATFORMS: list[Platform] = [ Platform.CLIMATE, + Platform.HUMIDIFIER, Platform.SELECT, Platform.SENSOR, ] diff --git a/homeassistant/components/aprilaire/humidifier.py b/homeassistant/components/aprilaire/humidifier.py new file mode 100644 index 00000000000..62c8a184be2 --- /dev/null +++ b/homeassistant/components/aprilaire/humidifier.py @@ -0,0 +1,194 @@ +"""The Aprilaire humidifier component.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any, cast + +from pyaprilaire.const import Attribute + +from homeassistant.components.humidifier import ( + HumidifierAction, + HumidifierDeviceClass, + HumidifierEntity, + HumidifierEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import DOMAIN +from .coordinator import AprilaireCoordinator +from .entity import BaseAprilaireEntity + +HUMIDIFIER_ACTION_MAP: dict[StateType, HumidifierAction] = { + 0: HumidifierAction.IDLE, + 1: HumidifierAction.IDLE, + 2: HumidifierAction.HUMIDIFYING, + 3: HumidifierAction.OFF, +} + +DEHUMIDIFIER_ACTION_MAP: dict[StateType, HumidifierAction] = { + 0: HumidifierAction.IDLE, + 1: HumidifierAction.IDLE, + 2: HumidifierAction.DRYING, + 3: HumidifierAction.DRYING, + 4: HumidifierAction.OFF, +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Aprilaire humidifier devices.""" + + coordinator: AprilaireCoordinator = hass.data[DOMAIN][config_entry.unique_id] + + assert config_entry.unique_id is not None + + descriptions: list[AprilaireHumidifierDescription] = [] + + if coordinator.data.get(Attribute.HUMIDIFICATION_AVAILABLE) in (0, 1, 2): + descriptions.append( + AprilaireHumidifierDescription( + key="humidifier", + translation_key="humidifier", + device_class=HumidifierDeviceClass.HUMIDIFIER, + action_key=Attribute.HUMIDIFICATION_STATUS, + action_map=HUMIDIFIER_ACTION_MAP, + current_humidity_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE, + target_humidity_key=Attribute.HUMIDIFICATION_SETPOINT, + min_humidity=10, + max_humidity=50, + default_humidity=30, + set_humidity_fn=coordinator.client.set_humidification_setpoint, + ) + ) + + if coordinator.data.get(Attribute.DEHUMIDIFICATION_AVAILABLE) in (0, 1): + descriptions.append( + AprilaireHumidifierDescription( + key="dehumidifier", + translation_key="dehumidifier", + device_class=HumidifierDeviceClass.DEHUMIDIFIER, + action_key=Attribute.DEHUMIDIFICATION_STATUS, + action_map=DEHUMIDIFIER_ACTION_MAP, + current_humidity_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE, + target_humidity_key=Attribute.DEHUMIDIFICATION_SETPOINT, + min_humidity=40, + max_humidity=90, + default_humidity=60, + set_humidity_fn=coordinator.client.set_dehumidification_setpoint, + ) + ) + + async_add_entities( + AprilaireHumidifierEntity(coordinator, description, config_entry.unique_id) + for description in descriptions + ) + + +@dataclass(frozen=True, kw_only=True) +class AprilaireHumidifierDescription(HumidifierEntityDescription): + """Class describing Aprilaire humidifier entities.""" + + action_key: str + action_map: dict[StateType, HumidifierAction] + current_humidity_key: str + target_humidity_key: str + min_humidity: int + max_humidity: int + default_humidity: int + set_humidity_fn: Callable[[int], Awaitable] + + +class AprilaireHumidifierEntity(BaseAprilaireEntity, HumidifierEntity): + """Base humidity entity for Aprilaire.""" + + entity_description: AprilaireHumidifierDescription + last_target_humidity: int | None = None + + def __init__( + self, + coordinator: AprilaireCoordinator, + description: AprilaireHumidifierDescription, + unique_id: str, + ) -> None: + """Initialize a select for an Aprilaire device.""" + + self.entity_description = description + + super().__init__(coordinator, unique_id) + + @property + def action(self) -> HumidifierAction | None: + """Get the current action.""" + + action = self.coordinator.data.get(self.entity_description.action_key) + + return self.entity_description.action_map.get(action, HumidifierAction.OFF) + + @property + def is_on(self) -> bool: + """Get whether the humidifier is on.""" + + return self.target_humidity is not None and self.target_humidity > 0 + + @property + def current_humidity(self) -> float | None: + """Get the current humidity.""" + + return cast( + float, + self.coordinator.data.get(self.entity_description.current_humidity_key), + ) + + @property + def target_humidity(self) -> float | None: + """Get the target humidity.""" + + target_humidity = cast( + float, + self.coordinator.data.get(self.entity_description.target_humidity_key), + ) + + if target_humidity is not None and target_humidity > 0: + self.last_target_humidity = int(target_humidity) + + return target_humidity + + @property + def min_humidity(self) -> float: + """Return the minimum humidity.""" + + return self.entity_description.min_humidity + + @property + def max_humidity(self) -> float: + """Return the maximum humidity.""" + + return self.entity_description.max_humidity + + async def async_set_humidity(self, humidity: int) -> None: + """Set the humidity.""" + + await self.entity_description.set_humidity_fn(humidity) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + + if self.last_target_humidity is None or self.last_target_humidity == 0: + target_humidity = self.entity_description.default_humidity + else: + target_humidity = self.last_target_humidity + + await self.entity_description.set_humidity_fn(target_humidity) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + + await self.entity_description.set_humidity_fn(0) diff --git a/homeassistant/components/aprilaire/strings.json b/homeassistant/components/aprilaire/strings.json index 0849f2255dd..e8122fe0a7b 100644 --- a/homeassistant/components/aprilaire/strings.json +++ b/homeassistant/components/aprilaire/strings.json @@ -24,6 +24,14 @@ "name": "Thermostat" } }, + "humidifier": { + "humidifier": { + "name": "[%key:component::humidifier::title%]" + }, + "dehumidifier": { + "name": "[%key:component::humidifier::entity_component::dehumidifier::name%]" + } + }, "select": { "air_cleaning_event": { "name": "Air cleaning event", From 213bbae63c6c8114896e53343ab60e7255ce10e4 Mon Sep 17 00:00:00 2001 From: Filipe Pina <636320+fopina@users.noreply.github.com> Date: Fri, 5 Jul 2024 08:24:02 +0100 Subject: [PATCH 0780/2411] Respect icloud `Enable polling updates` (#117984) Co-authored-by: J. Nick Koston --- homeassistant/components/icloud/account.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 2b3d1a22f21..988073384f8 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -161,6 +161,7 @@ class IcloudAccount: """Update iCloud devices.""" if self.api is None: return + _LOGGER.debug("Updating devices") if self.api.requires_2fa: self._require_reauth() @@ -173,11 +174,7 @@ class IcloudAccount: _LOGGER.error("Unknown iCloud error: %s", err) self._fetch_interval = 2 dispatcher_send(self.hass, self.signal_device_update) - track_point_in_utc_time( - self.hass, - self.keep_alive, - utcnow() + timedelta(minutes=self._fetch_interval), - ) + self._schedule_next_fetch() return # Gets devices infos @@ -223,11 +220,7 @@ class IcloudAccount: if new_device: dispatcher_send(self.hass, self.signal_device_new) - track_point_in_utc_time( - self.hass, - self.keep_alive, - utcnow() + timedelta(minutes=self._fetch_interval), - ) + self._schedule_next_fetch() def _require_reauth(self): """Require the user to log in again.""" @@ -306,6 +299,14 @@ class IcloudAccount: self._max_interval, ) + def _schedule_next_fetch(self) -> None: + if not self._config_entry.pref_disable_polling: + track_point_in_utc_time( + self.hass, + self.keep_alive, + utcnow() + timedelta(minutes=self._fetch_interval), + ) + def keep_alive(self, now=None) -> None: """Keep the API alive.""" if self.api is None: From 2b9bddc3fcf0f72c999128b593bbe1e5fde438c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Jul 2024 02:25:57 -0500 Subject: [PATCH 0781/2411] Make device_tracker fallback defaults cached_property (#121260) --- .../components/device_tracker/config_entry.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 0372dff3a86..14b2d02b5f4 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from functools import cached_property from typing import final from homeassistant.components import zone @@ -168,7 +169,7 @@ class BaseTrackerEntity(Entity): _attr_device_info: None = None _attr_entity_category = EntityCategory.DIAGNOSTIC - @property + @cached_property def battery_level(self) -> int | None: """Return the battery level of the device. @@ -195,7 +196,7 @@ class BaseTrackerEntity(Entity): class TrackerEntity(BaseTrackerEntity): """Base class for a tracked device.""" - @property + @cached_property def should_poll(self) -> bool: """No polling for entities that have location pushed.""" return False @@ -205,7 +206,7 @@ class TrackerEntity(BaseTrackerEntity): """All updates need to be written to the state machine if we're not polling.""" return not self.should_poll - @property + @cached_property def location_accuracy(self) -> int: """Return the location accuracy of the device. @@ -213,17 +214,17 @@ class TrackerEntity(BaseTrackerEntity): """ return 0 - @property + @cached_property def location_name(self) -> str | None: """Return a location name for the current location of the device.""" return None - @property + @cached_property def latitude(self) -> float | None: """Return latitude value of the device.""" return None - @property + @cached_property def longitude(self) -> float | None: """Return longitude value of the device.""" return None @@ -266,17 +267,17 @@ class TrackerEntity(BaseTrackerEntity): class ScannerEntity(BaseTrackerEntity): """Base class for a tracked device that is on a scanned network.""" - @property + @cached_property def ip_address(self) -> str | None: """Return the primary ip address of the device.""" return None - @property + @cached_property def mac_address(self) -> str | None: """Return the mac address of the device.""" return None - @property + @cached_property def hostname(self) -> str | None: """Return hostname of the device.""" return None From 22718ca32a0e249a66b85eb95192b2ce81dd4911 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 Jul 2024 09:26:32 +0200 Subject: [PATCH 0782/2411] Assist Pipeline minor cleanup (#121187) --- .../components/assist_pipeline/pipeline.py | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index c6aa14bff15..ce6f3e8d024 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -5,7 +5,7 @@ from __future__ import annotations import array import asyncio from collections import defaultdict, deque -from collections.abc import AsyncGenerator, AsyncIterable, Callable, Iterable +from collections.abc import AsyncGenerator, AsyncIterable, Callable from dataclasses import asdict, dataclass, field from enum import StrEnum import logging @@ -118,8 +118,10 @@ AUDIO_PROCESSOR_BYTES: Final = AUDIO_PROCESSOR_SAMPLES * 2 # 16-bit samples @callback def _async_resolve_default_pipeline_settings( hass: HomeAssistant, - stt_engine_id: str | None, - tts_engine_id: str | None, + *, + conversation_engine_id: str | None = None, + stt_engine_id: str | None = None, + tts_engine_id: str | None = None, pipeline_name: str, ) -> dict[str, str | None]: """Resolve settings for a default pipeline. @@ -137,12 +139,13 @@ def _async_resolve_default_pipeline_settings( wake_word_entity = None wake_word_id = None + if conversation_engine_id is None: + conversation_engine_id = conversation.HOME_ASSISTANT_AGENT + # Find a matching language supported by the Home Assistant conversation agent conversation_languages = language_util.matches( hass.config.language, - conversation.async_get_conversation_languages( - hass, conversation.HOME_ASSISTANT_AGENT - ), + conversation.async_get_conversation_languages(hass, conversation_engine_id), country=hass.config.country, ) if conversation_languages: @@ -201,7 +204,7 @@ def _async_resolve_default_pipeline_settings( tts_engine_id = None return { - "conversation_engine": conversation.HOME_ASSISTANT_AGENT, + "conversation_engine": conversation_engine_id, "conversation_language": conversation_language, "language": hass.config.language, "name": pipeline_name, @@ -224,7 +227,7 @@ async def _async_create_default_pipeline( default stt / tts engines. """ pipeline_settings = _async_resolve_default_pipeline_settings( - hass, stt_engine_id=None, tts_engine_id=None, pipeline_name="Home Assistant" + hass, pipeline_name="Home Assistant" ) return await pipeline_store.async_create_item(pipeline_settings) @@ -243,7 +246,10 @@ async def async_create_default_pipeline( pipeline_data: PipelineData = hass.data[DOMAIN] pipeline_store = pipeline_data.pipeline_store pipeline_settings = _async_resolve_default_pipeline_settings( - hass, stt_engine_id, tts_engine_id, pipeline_name=pipeline_name + hass, + stt_engine_id=stt_engine_id, + tts_engine_id=tts_engine_id, + pipeline_name=pipeline_name, ) if ( pipeline_settings["stt_engine"] != stt_engine_id @@ -274,11 +280,11 @@ def async_get_pipeline(hass: HomeAssistant, pipeline_id: str | None = None) -> P @callback -def async_get_pipelines(hass: HomeAssistant) -> Iterable[Pipeline]: +def async_get_pipelines(hass: HomeAssistant) -> list[Pipeline]: """Get all pipelines.""" pipeline_data: PipelineData = hass.data[DOMAIN] - return pipeline_data.pipeline_store.data.values() + return list(pipeline_data.pipeline_store.data.values()) async def async_update_pipeline( @@ -1675,7 +1681,7 @@ class PipelineStorageCollectionWebsocket( connection.send_result( msg["id"], { - "pipelines": self.storage_collection.async_items(), + "pipelines": async_get_pipelines(hass), "preferred_pipeline": self.storage_collection.async_get_preferred_item(), }, ) From d3f424227fe1527b8a401afa0cd5a96d8f174df7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Jul 2024 02:31:31 -0500 Subject: [PATCH 0783/2411] Cleanup unifiprotect entity classes (#121184) --- .../components/unifiprotect/binary_sensor.py | 29 ++---- .../components/unifiprotect/button.py | 76 +++++++-------- .../components/unifiprotect/camera.py | 5 +- .../components/unifiprotect/entity.py | 95 ++++++++++--------- .../components/unifiprotect/event.py | 11 +-- .../components/unifiprotect/light.py | 11 +-- homeassistant/components/unifiprotect/lock.py | 5 +- .../components/unifiprotect/media_player.py | 11 +-- .../components/unifiprotect/number.py | 5 +- .../components/unifiprotect/select.py | 5 +- .../components/unifiprotect/sensor.py | 7 +- .../components/unifiprotect/switch.py | 29 +++--- homeassistant/components/unifiprotect/text.py | 5 +- 13 files changed, 129 insertions(+), 165 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index c4e1aa87df2..fe2017d2f05 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -8,11 +8,9 @@ import dataclasses from uiprotect.data import ( NVR, Camera, - Light, ModelType, MountType, ProtectAdoptableDeviceModel, - ProtectModelWithId, Sensor, SmartDetectObjectType, ) @@ -27,11 +25,12 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .data import ProtectData, UFPConfigEntry +from .data import ProtectData, ProtectDeviceType, UFPConfigEntry from .entity import ( BaseProtectEntity, EventEntityMixin, ProtectDeviceEntity, + ProtectIsOnEntity, ProtectNVREntity, async_all_device_entities, ) @@ -623,31 +622,22 @@ _MOUNTABLE_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription } -class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity): +class ProtectDeviceBinarySensor( + ProtectIsOnEntity, ProtectDeviceEntity, BinarySensorEntity +): """A UniFi Protect Device Binary Sensor.""" - device: Camera | Light | Sensor entity_description: ProtectBinaryEntityDescription - _state_attrs: tuple[str, ...] = ("_attr_available", "_attr_is_on") - - @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - super()._async_update_device_from_protect(device) - self._attr_is_on = self.entity_description.get_ufp_value(self.device) class MountableProtectDeviceBinarySensor(ProtectDeviceBinarySensor): """A UniFi Protect Device Binary Sensor that can change device class at runtime.""" device: Sensor - _state_attrs: tuple[str, ...] = ( - "_attr_available", - "_attr_is_on", - "_attr_device_class", - ) + _state_attrs = ("_attr_available", "_attr_is_on", "_attr_device_class") @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: super()._async_update_device_from_protect(device) # UP Sense can be any of the 3 contact sensor device classes self._attr_device_class = MOUNT_DEVICE_CLASS_MAP.get( @@ -673,7 +663,6 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): self._disk = disk # backwards compat with old unique IDs index = self._disk.slot - 1 - description = dataclasses.replace( description, key=f"{description.key}_{index}", @@ -682,7 +671,7 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): super().__init__(data, device, description) @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: super()._async_update_device_from_protect(device) slot = self._disk.slot self._attr_available = False @@ -712,7 +701,7 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity): self._attr_extra_state_attributes = {} @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: description = self.entity_description prev_event = self._event diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 6c0ef37e1df..79985b9c7b2 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -4,10 +4,11 @@ from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass +from functools import partial import logging -from typing import Final +from typing import TYPE_CHECKING, Final -from uiprotect.data import ModelType, ProtectAdoptableDeviceModel, ProtectModelWithId +from uiprotect.data import ModelType, ProtectAdoptableDeviceModel from homeassistant.components.button import ( ButtonDeviceClass, @@ -21,7 +22,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DEVICES_THAT_ADOPT, DOMAIN -from .data import UFPConfigEntry +from .data import ProtectDeviceType, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectEntityDescription, ProtectSetableKeysMixin, T @@ -38,7 +39,6 @@ class ProtectButtonEntityDescription( DEVICE_CLASS_CHIME_BUTTON: Final = "unifiprotect__chime_button" -KEY_ADOPT = "adopt" ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( @@ -61,7 +61,7 @@ ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ) ADOPT_BUTTON = ProtectButtonEntityDescription[ProtectAdoptableDeviceModel]( - key=KEY_ADOPT, + key="adopt", name="Adopt device", icon="mdi:plus-circle", ufp_press="adopt", @@ -119,17 +119,25 @@ async def async_setup_entry( """Discover devices on a UniFi Protect NVR.""" data = entry.runtime_data + adopt_entities = partial( + async_all_device_entities, + data, + ProtectAdoptButton, + unadopted_descs=[ADOPT_BUTTON], + ) + base_entities = partial( + async_all_device_entities, + data, + ProtectButton, + all_descs=ALL_DEVICE_BUTTONS, + model_descriptions=_MODEL_DESCRIPTIONS, + ) + @callback def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: - entities = async_all_device_entities( - data, - ProtectButton, - all_descs=ALL_DEVICE_BUTTONS, - unadopted_descs=[ADOPT_BUTTON], - model_descriptions=_MODEL_DESCRIPTIONS, - ufp_device=device, + async_add_entities( + [*base_entities(ufp_device=device), *adopt_entities(ufp_device=device)] ) - async_add_entities(entities) _async_remove_adopt_button(hass, device) @callback @@ -137,29 +145,13 @@ async def async_setup_entry( if not device.can_adopt or not device.can_create(data.api.bootstrap.auth_user): _LOGGER.debug("Device is not adoptable: %s", device.id) return - async_add_entities( - async_all_device_entities( - data, - ProtectButton, - unadopted_descs=[ADOPT_BUTTON], - ufp_device=device, - ) - ) + async_add_entities(adopt_entities(ufp_device=device)) data.async_subscribe_adopt(_add_new_device) entry.async_on_unload( async_dispatcher_connect(hass, data.add_signal, _async_add_unadopted_device) ) - - async_add_entities( - async_all_device_entities( - data, - ProtectButton, - all_descs=ALL_DEVICE_BUTTONS, - unadopted_descs=[ADOPT_BUTTON], - model_descriptions=_MODEL_DESCRIPTIONS, - ) - ) + async_add_entities([*base_entities(), *adopt_entities()]) for device in data.get_by_types(DEVICES_THAT_ADOPT): _async_remove_adopt_button(hass, device) @@ -170,16 +162,20 @@ class ProtectButton(ProtectDeviceEntity, ButtonEntity): entity_description: ProtectButtonEntityDescription - @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - super()._async_update_device_from_protect(device) - if self.entity_description.key == KEY_ADOPT: - device = self.device - self._attr_available = device.can_adopt and device.can_create( - self.data.api.bootstrap.auth_user - ) - async def async_press(self) -> None: """Press the button.""" if self.entity_description.ufp_press is not None: await getattr(self.device, self.entity_description.ufp_press)() + + +class ProtectAdoptButton(ProtectButton): + """A Ubiquiti UniFi Protect Adopt button.""" + + @callback + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: + super()._async_update_device_from_protect(device) + if TYPE_CHECKING: + assert isinstance(device, ProtectAdoptableDeviceModel) + self._attr_available = device.can_adopt and device.can_create( + self.data.api.bootstrap.auth_user + ) diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 73cdb4a2c31..62c35d00171 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -9,7 +9,6 @@ from uiprotect.data import ( Camera as UFPCamera, CameraChannel, ProtectAdoptableDeviceModel, - ProtectModelWithId, StateType, ) @@ -28,7 +27,7 @@ from .const import ( ATTR_WIDTH, DOMAIN, ) -from .data import ProtectData, UFPConfigEntry +from .data import ProtectData, ProtectDeviceType, UFPConfigEntry from .entity import ProtectDeviceEntity from .utils import get_camera_base_name @@ -216,7 +215,7 @@ class ProtectCamera(ProtectDeviceEntity, Camera): self._attr_supported_features = _EMPTY_CAMERA_FEATURES @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: super()._async_update_device_from_protect(device) updated_device = self.device channel = updated_device.channels[self.channel.id] diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 7eceb861955..f29d18ce35b 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -9,14 +9,7 @@ import logging from operator import attrgetter from typing import TYPE_CHECKING -from uiprotect.data import ( - NVR, - Event, - ModelType, - ProtectAdoptableDeviceModel, - ProtectModelWithId, - StateType, -) +from uiprotect.data import NVR, Event, ModelType, ProtectAdoptableDeviceModel, StateType from homeassistant.core import callback import homeassistant.helpers.device_registry as dr @@ -30,7 +23,7 @@ from .const import ( DEFAULT_BRAND, DOMAIN, ) -from .data import ProtectData +from .data import ProtectData, ProtectDeviceType from .models import PermRequired, ProtectEntityDescription, ProtectEventMixin _LOGGER = logging.getLogger(__name__) @@ -160,7 +153,7 @@ def async_all_device_entities( class BaseProtectEntity(Entity): """Base class for UniFi protect entities.""" - device: ProtectAdoptableDeviceModel | NVR + device: ProtectDeviceType _attr_should_poll = False _attr_attribution = DEFAULT_ATTRIBUTION @@ -171,7 +164,7 @@ class BaseProtectEntity(Entity): def __init__( self, data: ProtectData, - device: ProtectAdoptableDeviceModel | NVR, + device: ProtectDeviceType, description: EntityDescription | None = None, ) -> None: """Initialize the entity.""" @@ -203,37 +196,32 @@ class BaseProtectEntity(Entity): @callback def _async_set_device_info(self) -> None: - self._attr_device_info = DeviceInfo( - name=self.device.display_name, - manufacturer=DEFAULT_BRAND, - model=self.device.type, - via_device=(DOMAIN, self.data.api.bootstrap.nvr.mac), - sw_version=self.device.firmware_version, - connections={(dr.CONNECTION_NETWORK_MAC, self.device.mac)}, - configuration_url=self.device.protect_url, - ) + """Set device info.""" @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: """Update Entity object from Protect device.""" - if TYPE_CHECKING: - assert isinstance(device, ProtectAdoptableDeviceModel) - - if last_update_success := self.data.last_update_success: + was_available = self._attr_available + if last_updated_success := self.data.last_update_success: self.device = device - async_get_ufp_enabled = self._async_get_ufp_enabled - self._attr_available = ( - last_update_success - and ( - device.state is StateType.CONNECTED - or (not device.is_adopted_by_us and device.can_adopt) + if device.model is ModelType.NVR: + available = last_updated_success + else: + if TYPE_CHECKING: + assert isinstance(device, ProtectAdoptableDeviceModel) + connected = device.state is StateType.CONNECTED or ( + not device.is_adopted_by_us and device.can_adopt ) - and (not async_get_ufp_enabled or async_get_ufp_enabled(device)) - ) + async_get_ufp_enabled = self._async_get_ufp_enabled + enabled = not async_get_ufp_enabled or async_get_ufp_enabled(device) + available = last_updated_success and connected and enabled + + if available != was_available: + self._attr_available = available @callback - def _async_updated_event(self, device: ProtectAdoptableDeviceModel | NVR) -> None: + def _async_updated_event(self, device: ProtectDeviceType) -> None: """When device is updated from Protect.""" previous_attrs = [getter() for getter in self._state_getters] self._async_update_device_from_protect(device) @@ -266,10 +254,36 @@ class BaseProtectEntity(Entity): ) +class ProtectIsOnEntity(BaseProtectEntity): + """Base class for entities with is_on property.""" + + _state_attrs: tuple[str, ...] = ("_attr_available", "_attr_is_on") + _attr_is_on: bool | None + entity_description: ProtectEntityDescription + + def _async_update_device_from_protect( + self, device: ProtectAdoptableDeviceModel | NVR + ) -> None: + super()._async_update_device_from_protect(device) + was_on = self._attr_is_on + if was_on != (is_on := self.entity_description.get_ufp_value(device) is True): + self._attr_is_on = is_on + + class ProtectDeviceEntity(BaseProtectEntity): """Base class for UniFi protect entities.""" - device: ProtectAdoptableDeviceModel + @callback + def _async_set_device_info(self) -> None: + self._attr_device_info = DeviceInfo( + name=self.device.display_name, + manufacturer=DEFAULT_BRAND, + model=self.device.type, + via_device=(DOMAIN, self.data.api.bootstrap.nvr.mac), + sw_version=self.device.firmware_version, + connections={(dr.CONNECTION_NETWORK_MAC, self.device.mac)}, + configuration_url=self.device.protect_url, + ) class ProtectNVREntity(BaseProtectEntity): @@ -289,14 +303,6 @@ class ProtectNVREntity(BaseProtectEntity): configuration_url=self.device.api.base_url, ) - @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - data = self.data - if last_update_success := data.last_update_success: - self.device = data.api.bootstrap.nvr - - self._attr_available = last_update_success - class EventEntityMixin(ProtectDeviceEntity): """Adds motion event attributes to sensor.""" @@ -338,9 +344,8 @@ class EventEntityMixin(ProtectDeviceEntity): event object so we need to check the datetime object that was saved from the last time the entity was updated. """ - event = self._event return bool( - event + (event := self._event) and event.end and prev_event and prev_event_end diff --git a/homeassistant/components/unifiprotect/event.py b/homeassistant/components/unifiprotect/event.py index 4e2fd7fce44..c8269e36326 100644 --- a/homeassistant/components/unifiprotect/event.py +++ b/homeassistant/components/unifiprotect/event.py @@ -4,12 +4,7 @@ from __future__ import annotations import dataclasses -from uiprotect.data import ( - Camera, - EventType, - ProtectAdoptableDeviceModel, - ProtectModelWithId, -) +from uiprotect.data import Camera, EventType, ProtectAdoptableDeviceModel from homeassistant.components.event import ( EventDeviceClass, @@ -20,7 +15,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_EVENT_ID -from .data import ProtectData, UFPConfigEntry +from .data import ProtectData, ProtectDeviceType, UFPConfigEntry from .entity import EventEntityMixin, ProtectDeviceEntity from .models import ProtectEventMixin @@ -50,7 +45,7 @@ class ProtectDeviceEventEntity(EventEntityMixin, ProtectDeviceEntity, EventEntit entity_description: ProtectEventEntityDescription @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: description = self.entity_description prev_event = self._event diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index 651b9c7d3d4..486a8956e0c 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -5,18 +5,13 @@ from __future__ import annotations import logging from typing import Any -from uiprotect.data import ( - Light, - ModelType, - ProtectAdoptableDeviceModel, - ProtectModelWithId, -) +from uiprotect.data import Light, ModelType, ProtectAdoptableDeviceModel from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .data import UFPConfigEntry +from .data import ProtectDeviceType, UFPConfigEntry from .entity import ProtectDeviceEntity _LOGGER = logging.getLogger(__name__) @@ -66,7 +61,7 @@ class ProtectLight(ProtectDeviceEntity, LightEntity): _state_attrs = ("_attr_available", "_attr_is_on", "_attr_brightness") @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: super()._async_update_device_from_protect(device) updated_device = self.device self._attr_is_on = updated_device.is_light_on diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index b649813135b..3e9372db0e5 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -10,14 +10,13 @@ from uiprotect.data import ( LockStatusType, ModelType, ProtectAdoptableDeviceModel, - ProtectModelWithId, ) from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .data import UFPConfigEntry +from .data import ProtectDeviceType, UFPConfigEntry from .entity import ProtectDeviceEntity _LOGGER = logging.getLogger(__name__) @@ -60,7 +59,7 @@ class ProtectLock(ProtectDeviceEntity, LockEntity): ) @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: super()._async_update_device_from_protect(device) lock_status = self.device.lock_status diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index d9b2dad7220..5f9991b257b 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -5,12 +5,7 @@ from __future__ import annotations import logging from typing import Any -from uiprotect.data import ( - Camera, - ProtectAdoptableDeviceModel, - ProtectModelWithId, - StateType, -) +from uiprotect.data import Camera, ProtectAdoptableDeviceModel, StateType from uiprotect.exceptions import StreamError from homeassistant.components import media_source @@ -28,7 +23,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .data import UFPConfigEntry +from .data import ProtectDeviceType, UFPConfigEntry from .entity import ProtectDeviceEntity _LOGGER = logging.getLogger(__name__) @@ -77,7 +72,7 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): _state_attrs = ("_attr_available", "_attr_state", "_attr_volume_level") @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: super()._async_update_device_from_protect(device) updated_device = self.device self._attr_volume_level = float(updated_device.speaker_settings.volume / 100) diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 4bb5cba6f15..2de3ef9f2cd 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -12,7 +12,6 @@ from uiprotect.data import ( Light, ModelType, ProtectAdoptableDeviceModel, - ProtectModelWithId, ) from homeassistant.components.number import NumberEntity, NumberEntityDescription @@ -20,7 +19,7 @@ from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .data import ProtectData, UFPConfigEntry +from .data import ProtectData, ProtectDeviceType, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectEntityDescription, ProtectSetableKeysMixin, T @@ -268,7 +267,7 @@ class ProtectNumbers(ProtectDeviceEntity, NumberEntity): self._attr_native_step = self.entity_description.ufp_step @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: super()._async_update_device_from_protect(device) self._attr_native_value = self.entity_description.get_ufp_value(self.device) diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 9e742caa9ce..e06ae7bfbec 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -21,7 +21,6 @@ from uiprotect.data import ( ModelType, MountType, ProtectAdoptableDeviceModel, - ProtectModelWithId, RecordingMode, Sensor, Viewer, @@ -33,7 +32,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import TYPE_EMPTY_VALUE -from .data import ProtectData, UFPConfigEntry +from .data import ProtectData, ProtectDeviceType, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectEntityDescription, ProtectSetableKeysMixin, T from .utils import async_get_light_motion_current @@ -371,7 +370,7 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity): super().__init__(data, device, description) @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: super()._async_update_device_from_protect(device) entity_description = self.entity_description # entities with categories are not exposed for voice diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 84cac342d00..786c5bd66c8 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -16,7 +16,6 @@ from uiprotect.data import ( ModelType, ProtectAdoptableDeviceModel, ProtectDeviceModel, - ProtectModelWithId, Sensor, SmartDetectObjectType, ) @@ -41,7 +40,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .data import ProtectData, UFPConfigEntry +from .data import ProtectData, ProtectDeviceType, UFPConfigEntry from .entity import ( BaseProtectEntity, EventEntityMixin, @@ -721,7 +720,7 @@ class BaseProtectSensor(BaseProtectEntity, SensorEntity): entity_description: ProtectSensorEntityDescription _state_attrs = ("_attr_available", "_attr_native_value") - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: super()._async_update_device_from_protect(device) self._attr_native_value = self.entity_description.get_ufp_value(self.device) @@ -756,7 +755,7 @@ class ProtectLicensePlateEventSensor(ProtectEventSensor): self._attr_extra_state_attributes = {} @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: description = self.entity_description prev_event = self._event diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index ca56a602209..6eac572cc3e 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -5,14 +5,12 @@ from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass from functools import partial -import logging from typing import Any from uiprotect.data import ( Camera, ModelType, ProtectAdoptableDeviceModel, - ProtectModelWithId, RecordingMode, VideoMode, ) @@ -23,16 +21,16 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .data import ProtectData, UFPConfigEntry +from .data import ProtectData, ProtectDeviceType, UFPConfigEntry from .entity import ( BaseProtectEntity, ProtectDeviceEntity, + ProtectIsOnEntity, ProtectNVREntity, async_all_device_entities, ) from .models import PermRequired, ProtectEntityDescription, ProtectSetableKeysMixin, T -_LOGGER = logging.getLogger(__name__) ATTR_PREV_MIC = "prev_mic_level" ATTR_PREV_RECORD = "prev_record_mode" @@ -45,10 +43,7 @@ class ProtectSwitchEntityDescription( async def _set_highfps(obj: Camera, value: bool) -> None: - if value: - await obj.set_video_mode(VideoMode.HIGH_FPS) - else: - await obj.set_video_mode(VideoMode.DEFAULT) + await obj.set_video_mode(VideoMode.HIGH_FPS if value else VideoMode.DEFAULT) CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( @@ -472,15 +467,10 @@ _PRIVACY_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = { } -class ProtectBaseSwitch(BaseProtectEntity, SwitchEntity): +class ProtectBaseSwitch(ProtectIsOnEntity): """Base class for UniFi Protect Switch.""" entity_description: ProtectSwitchEntityDescription - _state_attrs = ("_attr_available", "_attr_is_on") - - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - super()._async_update_device_from_protect(device) - self._attr_is_on = self.entity_description.get_ufp_value(self.device) is True async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" @@ -491,18 +481,23 @@ class ProtectBaseSwitch(BaseProtectEntity, SwitchEntity): await self.entity_description.ufp_set(self.device, False) -class ProtectSwitch(ProtectBaseSwitch, ProtectDeviceEntity): +class ProtectSwitch(ProtectDeviceEntity, ProtectBaseSwitch, SwitchEntity): """A UniFi Protect Switch.""" + entity_description: ProtectSwitchEntityDescription -class ProtectNVRSwitch(ProtectBaseSwitch, ProtectNVREntity): + +class ProtectNVRSwitch(ProtectNVREntity, ProtectBaseSwitch, SwitchEntity): """A UniFi Protect NVR Switch.""" + entity_description: ProtectSwitchEntityDescription + class ProtectPrivacyModeSwitch(RestoreEntity, ProtectSwitch): """A UniFi Protect Switch.""" device: Camera + entity_description: ProtectSwitchEntityDescription def __init__( self, @@ -533,7 +528,7 @@ class ProtectPrivacyModeSwitch(RestoreEntity, ProtectSwitch): self._attr_extra_state_attributes = {} @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: super()._async_update_device_from_protect(device) # do not add extra state attribute on initialize if self.entity_id: diff --git a/homeassistant/components/unifiprotect/text.py b/homeassistant/components/unifiprotect/text.py index e01a6b31f11..9af946a7e11 100644 --- a/homeassistant/components/unifiprotect/text.py +++ b/homeassistant/components/unifiprotect/text.py @@ -10,7 +10,6 @@ from uiprotect.data import ( DoorbellMessageType, ModelType, ProtectAdoptableDeviceModel, - ProtectModelWithId, ) from homeassistant.components.text import TextEntity, TextEntityDescription @@ -18,7 +17,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .data import UFPConfigEntry +from .data import ProtectDeviceType, UFPConfigEntry from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectEntityDescription, ProtectSetableKeysMixin, T @@ -89,7 +88,7 @@ class ProtectDeviceText(ProtectDeviceEntity, TextEntity): _state_attrs = ("_attr_available", "_attr_native_value") @callback - def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: super()._async_update_device_from_protect(device) self._attr_native_value = self.entity_description.get_ufp_value(self.device) From b28f528a7a6484fc0b20729f7958bb123ab7b8a8 Mon Sep 17 00:00:00 2001 From: Christophe Gagnier Date: Fri, 5 Jul 2024 03:39:58 -0400 Subject: [PATCH 0784/2411] Add max current number entity for TechnoVE (#121148) Co-authored-by: Robert Resch --- homeassistant/components/technove/__init__.py | 2 +- homeassistant/components/technove/number.py | 106 +++++++++ .../components/technove/strings.json | 10 + .../technove/fixtures/station_charging.json | 2 +- .../snapshots/test_binary_sensor.ambr | 2 +- .../technove/snapshots/test_number.ambr | 57 +++++ tests/components/technove/test_number.py | 201 ++++++++++++++++++ 7 files changed, 377 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/technove/number.py create mode 100644 tests/components/technove/snapshots/test_number.ambr create mode 100644 tests/components/technove/test_number.py diff --git a/homeassistant/components/technove/__init__.py b/homeassistant/components/technove/__init__.py index d2d5b4255ba..7315f6f785c 100644 --- a/homeassistant/components/technove/__init__.py +++ b/homeassistant/components/technove/__init__.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import TechnoVEDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/technove/number.py b/homeassistant/components/technove/number.py new file mode 100644 index 00000000000..9f2af47c24f --- /dev/null +++ b/homeassistant/components/technove/number.py @@ -0,0 +1,106 @@ +"""Support for TechnoVE number entities.""" + +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any + +from technove import MIN_CURRENT, TechnoVE + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import TechnoVEDataUpdateCoordinator +from .entity import TechnoVEEntity +from .helpers import technove_exception_handler + + +@dataclass(frozen=True, kw_only=True) +class TechnoVENumberDescription(NumberEntityDescription): + """Describes TechnoVE number entity.""" + + native_max_value_fn: Callable[[TechnoVE], float] + native_value_fn: Callable[[TechnoVE], float] + set_value_fn: Callable[ + [TechnoVEDataUpdateCoordinator, float], Coroutine[Any, Any, None] + ] + + +async def _set_max_current( + coordinator: TechnoVEDataUpdateCoordinator, value: float +) -> None: + if coordinator.data.info.in_sharing_mode: + raise ServiceValidationError( + translation_domain=DOMAIN, translation_key="max_current_in_sharing_mode" + ) + await coordinator.technove.set_max_current(value) + + +NUMBERS = [ + TechnoVENumberDescription( + key="max_current", + translation_key="max_current", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.CURRENT, + mode=NumberMode.BOX, + native_step=1, + native_min_value=MIN_CURRENT, + native_max_value_fn=lambda station: station.info.max_station_current, + native_value_fn=lambda station: station.info.max_current, + set_value_fn=_set_max_current, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up TechnoVE number entity based on a config entry.""" + coordinator: TechnoVEDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + TechnoVENumberEntity(coordinator, description) for description in NUMBERS + ) + + +class TechnoVENumberEntity(TechnoVEEntity, NumberEntity): + """Defines a TechnoVE number entity.""" + + entity_description: TechnoVENumberDescription + + def __init__( + self, + coordinator: TechnoVEDataUpdateCoordinator, + description: TechnoVENumberDescription, + ) -> None: + """Initialize a TechnoVE switch entity.""" + self.entity_description = description + super().__init__(coordinator, description.key) + + @property + def native_max_value(self) -> float: + """Return the max value of the TechnoVE number entity.""" + return self.entity_description.native_max_value_fn(self.coordinator.data) + + @property + def native_value(self) -> float: + """Return the native value of the TechnoVE number entity.""" + return self.entity_description.native_value_fn(self.coordinator.data) + + @technove_exception_handler + async def async_set_native_value(self, value: float) -> None: + """Set the value for the TechnoVE number entity.""" + await self.entity_description.set_value_fn(self.coordinator, value) diff --git a/homeassistant/components/technove/strings.json b/homeassistant/components/technove/strings.json index 1e7550c8842..8799909d95c 100644 --- a/homeassistant/components/technove/strings.json +++ b/homeassistant/components/technove/strings.json @@ -39,6 +39,11 @@ "name": "Static IP" } }, + "number": { + "max_current": { + "name": "Maximum current" + } + }, "sensor": { "voltage_in": { "name": "Input voltage" @@ -74,5 +79,10 @@ "name": "Auto charge" } } + }, + "exceptions": { + "max_current_in_sharing_mode": { + "message": "Cannot set the max current when power sharing mode is enabled." + } } } diff --git a/tests/components/technove/fixtures/station_charging.json b/tests/components/technove/fixtures/station_charging.json index ea98dc0b071..63e68d0db0e 100644 --- a/tests/components/technove/fixtures/station_charging.json +++ b/tests/components/technove/fixtures/station_charging.json @@ -11,7 +11,7 @@ "normalPeriodActive": false, "maxChargePourcentage": 0.9, "isBatteryProtected": false, - "inSharingMode": true, + "inSharingMode": false, "energySession": 12.34, "energyTotal": 1234, "version": "1.82", diff --git a/tests/components/technove/snapshots/test_binary_sensor.ambr b/tests/components/technove/snapshots/test_binary_sensor.ambr index 140526b9391..cc2dcf4a04a 100644 --- a/tests/components/technove/snapshots/test_binary_sensor.ambr +++ b/tests/components/technove/snapshots/test_binary_sensor.ambr @@ -181,7 +181,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'off', }) # --- # name: test_sensors[binary_sensor.technove_station_static_ip-entry] diff --git a/tests/components/technove/snapshots/test_number.ambr b/tests/components/technove/snapshots/test_number.ambr new file mode 100644 index 00000000000..622c04d542a --- /dev/null +++ b/tests/components/technove/snapshots/test_number.ambr @@ -0,0 +1,57 @@ +# serializer version: 1 +# name: test_numbers[number.technove_station_maximum_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 32, + 'min': 8, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.technove_station_maximum_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Maximum current', + 'platform': 'technove', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'max_current', + 'unique_id': 'AA:AA:AA:AA:AA:BB_max_current', + 'unit_of_measurement': None, + }) +# --- +# name: test_numbers[number.technove_station_maximum_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'TechnoVE Station Maximum current', + 'max': 32, + 'min': 8, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.technove_station_maximum_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24', + }) +# --- diff --git a/tests/components/technove/test_number.py b/tests/components/technove/test_number.py new file mode 100644 index 00000000000..c9f39cd9200 --- /dev/null +++ b/tests/components/technove/test_number.py @@ -0,0 +1,201 @@ +"""Tests for the TechnoVE number platform.""" + +from unittest.mock import MagicMock + +import pytest +from syrupy.assertion import SnapshotAssertion +from technove import TechnoVEConnectionError, TechnoVEError + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import entity_registry as er + +from . import setup_with_selected_platforms + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_technove") +async def test_numbers( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test the creation and values of the TechnoVE numbers.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.NUMBER]) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("entity_id", "method", "called_with_value"), + [ + ( + "number.technove_station_maximum_current", + "set_max_current", + {"max_current": 10}, + ), + ], +) +@pytest.mark.usefixtures("init_integration") +async def test_number_expected_value( + hass: HomeAssistant, + mock_technove: MagicMock, + entity_id: str, + method: str, + called_with_value: dict[str, bool | int], +) -> None: + """Test set value services with valid values.""" + state = hass.states.get(entity_id) + method_mock = getattr(mock_technove, method) + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: called_with_value["max_current"]}, + blocking=True, + ) + + assert method_mock.call_count == 1 + method_mock.assert_called_with(**called_with_value) + + +@pytest.mark.parametrize( + ("entity_id", "value"), + [ + ( + "number.technove_station_maximum_current", + 1, + ), + ( + "number.technove_station_maximum_current", + 1000, + ), + ], +) +@pytest.mark.usefixtures("init_integration") +async def test_number_out_of_bound( + hass: HomeAssistant, + entity_id: str, + value: float, +) -> None: + """Test set value services with out of bound values.""" + state = hass.states.get(entity_id) + + with pytest.raises(ServiceValidationError, match="is outside valid range"): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: value}, + blocking=True, + ) + + assert (state := hass.states.get(state.entity_id)) + assert state.state != STATE_UNAVAILABLE + + +@pytest.mark.usefixtures("init_integration") +async def test_set_max_current_sharing_mode( + hass: HomeAssistant, + mock_technove: MagicMock, +) -> None: + """Test failure to set the max current when the station is in sharing mode.""" + entity_id = "number.technove_station_maximum_current" + state = hass.states.get(entity_id) + + # Enable power sharing mode + device = mock_technove.update.return_value + device.info.in_sharing_mode = True + + with pytest.raises( + ServiceValidationError, + match="power sharing mode is enabled", + ): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 10, + }, + blocking=True, + ) + + assert (state := hass.states.get(state.entity_id)) + assert state.state != STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + ("entity_id", "method"), + [ + ( + "number.technove_station_maximum_current", + "set_max_current", + ), + ], +) +@pytest.mark.usefixtures("init_integration") +async def test_invalid_response( + hass: HomeAssistant, + mock_technove: MagicMock, + entity_id: str, + method: str, +) -> None: + """Test invalid response, not becoming unavailable.""" + state = hass.states.get(entity_id) + method_mock = getattr(mock_technove, method) + + method_mock.side_effect = TechnoVEError + with pytest.raises(HomeAssistantError, match="Invalid response from TechnoVE API"): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: 10}, + blocking=True, + ) + + assert method_mock.call_count == 1 + assert (state := hass.states.get(state.entity_id)) + assert state.state != STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + ("entity_id", "method"), + [ + ( + "number.technove_station_maximum_current", + "set_max_current", + ), + ], +) +@pytest.mark.usefixtures("init_integration") +async def test_connection_error( + hass: HomeAssistant, + mock_technove: MagicMock, + entity_id: str, + method: str, +) -> None: + """Test connection error, leading to becoming unavailable.""" + state = hass.states.get(entity_id) + method_mock = getattr(mock_technove, method) + + method_mock.side_effect = TechnoVEConnectionError + with pytest.raises( + HomeAssistantError, match="Error communicating with TechnoVE API" + ): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: 10}, + blocking=True, + ) + + assert method_mock.call_count == 1 + assert (state := hass.states.get(state.entity_id)) + assert state.state == STATE_UNAVAILABLE From dbe98de82a28769b0a7e66a3cc4366262d9c9044 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 5 Jul 2024 09:40:43 +0200 Subject: [PATCH 0785/2411] Fix `pulse counter frequency` sensors for Shelly Plus Uni (#121178) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/sensor.py | 29 +++++++++++++++-------- tests/components/shelly/conftest.py | 4 +++- tests/components/shelly/test_sensor.py | 26 ++++++++++++++++++++ 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 743c7c7ff01..5a6f03fd90c 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -960,14 +960,18 @@ RPC_SENSORS: Final = { name="Analog input", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, - removal_condition=lambda config, _status, key: (config[key]["enable"] is False), + removal_condition=lambda config, _, key: ( + config[key]["type"] != "analog" or config[key]["enable"] is False + ), ), "analoginput_xpercent": RpcSensorDescription( key="input", sub_key="xpercent", name="Analog value", removal_condition=lambda config, status, key: ( - config[key]["enable"] is False or status[key].get("xpercent") is None + config[key]["type"] != "analog" + or config[key]["enable"] is False + or status[key].get("xpercent") is None ), ), "pulse_counter": RpcSensorDescription( @@ -977,7 +981,9 @@ RPC_SENSORS: Final = { native_unit_of_measurement="pulse", state_class=SensorStateClass.TOTAL, value=lambda status, _: status["total"], - removal_condition=lambda config, _status, key: (config[key]["enable"] is False), + removal_condition=lambda config, _status, key: ( + config[key]["type"] != "count" or config[key]["enable"] is False + ), ), "counter_value": RpcSensorDescription( key="input", @@ -985,26 +991,29 @@ RPC_SENSORS: Final = { name="Counter value", value=lambda status, _: status["xtotal"], removal_condition=lambda config, status, key: ( - config[key]["enable"] is False + config[key]["type"] != "count" + or config[key]["enable"] is False or status[key]["counts"].get("xtotal") is None ), ), "counter_frequency": RpcSensorDescription( key="input", - sub_key="counts", + sub_key="freq", name="Pulse counter frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, state_class=SensorStateClass.MEASUREMENT, - value=lambda status, _: status["freq"], - removal_condition=lambda config, status, key: (config[key]["enable"] is False), + removal_condition=lambda config, _, key: ( + config[key]["type"] != "count" or config[key]["enable"] is False + ), ), "counter_frequency_value": RpcSensorDescription( key="input", - sub_key="counts", + sub_key="xfreq", name="Pulse counter frequency value", - value=lambda status, _: status["xfreq"], removal_condition=lambda config, status, key: ( - config[key]["enable"] is False or status[key]["counts"].get("xfreq") is None + config[key]["type"] != "count" + or config[key]["enable"] is False + or status[key].get("xfreq") is None ), ), } diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 65ebdeb6996..e9662b55cf5 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -228,7 +228,9 @@ MOCK_STATUS_RPC = { "input:1": {"id": 1, "percent": 89, "xpercent": 8.9}, "input:2": { "id": 2, - "counts": {"total": 56174, "xtotal": 561.74, "freq": 208.00, "xfreq": 6.11}, + "counts": {"total": 56174, "xtotal": 561.74}, + "freq": 208.00, + "xfreq": 6.11, }, "light:0": {"output": True, "brightness": 53.0}, "light:1": {"output": True, "brightness": 53.0}, diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 513bcd875e2..c62a1f6f6ca 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -828,3 +828,29 @@ async def test_rpc_pulse_counter_frequency_sensors( entry = entity_registry.async_get(entity_id) assert entry assert entry.unique_id == "123456789ABC-input:2-counter_frequency_value" + + +async def test_rpc_disabled_xfreq( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test RPC input with the xfreq sensor disabled.""" + status = deepcopy(mock_rpc_device.status) + status["input:2"] = { + "id": 2, + "counts": {"total": 56174, "xtotal": 561.74}, + "freq": 208.00, + } + monkeypatch.setattr(mock_rpc_device, "status", status) + + await init_integration(hass, 2) + + entity_id = f"{SENSOR_DOMAIN}.gas_pulse_counter_frequency_value" + + state = hass.states.get(entity_id) + assert not state + + entry = entity_registry.async_get(entity_id) + assert not entry From 1b42b32ac10d46e1a13cae80452288abf4b6a670 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Fri, 5 Jul 2024 09:41:21 +0200 Subject: [PATCH 0786/2411] Fix work area sensor in Husqvarna Automower (#121228) --- .../components/husqvarna_automower/sensor.py | 11 +++++-- .../husqvarna_automower/strings.json | 3 +- .../snapshots/test_sensor.ambr | 2 ++ .../husqvarna_automower/test_sensor.py | 32 +++++++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index 146ef17a6e4..2c8d369ea3a 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -184,6 +184,8 @@ RESTRICTED_REASONS: list = [ RestrictedReasons.WEEK_SCHEDULE.lower(), ] +STATE_NO_WORK_AREA_ACTIVE = "no_work_area_active" + @callback def _get_work_area_names(data: MowerAttributes) -> list[str]: @@ -191,16 +193,21 @@ def _get_work_area_names(data: MowerAttributes) -> list[str]: if TYPE_CHECKING: # Sensor does not get created if it is None assert data.work_areas is not None - return [data.work_areas[work_area_id].name for work_area_id in data.work_areas] + work_area_list = [ + data.work_areas[work_area_id].name for work_area_id in data.work_areas + ] + work_area_list.append(STATE_NO_WORK_AREA_ACTIVE) + return work_area_list @callback def _get_current_work_area_name(data: MowerAttributes) -> str: """Return the name of the current work area.""" + if data.mower.work_area_id is None: + return STATE_NO_WORK_AREA_ACTIVE if TYPE_CHECKING: # Sensor does not get created if values are None assert data.work_areas is not None - assert data.mower.work_area_id is not None return data.work_areas[data.mower.work_area_id].name diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index 6cb1c17421a..be17cc25e32 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -252,7 +252,8 @@ "work_area": { "name": "Work area", "state": { - "my_lawn": "My lawn" + "my_lawn": "My lawn", + "no_work_area_active": "No work area active" } } }, diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index 0b0d76620d3..935303e48fb 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -1059,6 +1059,7 @@ 'Front lawn', 'Back lawn', 'my_lawn', + 'no_work_area_active', ]), }), 'config_entry_id': , @@ -1097,6 +1098,7 @@ 'Front lawn', 'Back lawn', 'my_lawn', + 'no_work_area_active', ]), }), 'context': , diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index 8f30a3dcb04..314bcaaa00c 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -87,6 +87,38 @@ async def test_next_start_sensor( assert state.state == STATE_UNKNOWN +async def test_work_area_sensor( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the work area sensor.""" + await setup_integration(hass, mock_config_entry) + state = hass.states.get("sensor.test_mower_1_work_area") + assert state is not None + assert state.state == "Front lawn" + + values = mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower.json", DOMAIN) + ) + values[TEST_MOWER_ID].mower.work_area_id = None + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_mower_1_work_area") + assert state.state == "no_work_area_active" + + values[TEST_MOWER_ID].mower.work_area_id = 0 + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_mower_1_work_area") + assert state.state == "my_lawn" + + @pytest.mark.parametrize( ("sensor_to_test"), [ From 98dfb47448a03283070ed7eba1a71205cacbabd8 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 5 Jul 2024 09:41:41 +0200 Subject: [PATCH 0787/2411] Fix Matter light discovery schema for DimmerSwitch (#121185) --- homeassistant/components/matter/light.py | 2 ++ homeassistant/components/matter/switch.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 749d82fd661..9ff6f45177e 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -446,6 +446,8 @@ DISCOVERY_SCHEMAS = [ device_types.DimmablePlugInUnit, device_types.ExtendedColorLight, device_types.OnOffLight, + device_types.DimmerSwitch, + device_types.ColorDimmerSwitch, ), ), # Additional schema to match (HS Color) lights with incorrect/missing device type diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index efa78446fc5..2fb325b8808 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -114,6 +114,7 @@ DISCOVERY_SCHEMAS = [ device_types.ColorTemperatureLight, device_types.DimmableLight, device_types.ExtendedColorLight, + device_types.DimmerSwitch, device_types.ColorDimmerSwitch, device_types.OnOffLight, device_types.AirPurifier, From 1c1e1a7bfac60aad91f3441adaadd0ac18f87891 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 5 Jul 2024 09:41:57 +0200 Subject: [PATCH 0788/2411] Listen for attribute changes of OnOff cluster in appliances (#121198) --- homeassistant/components/matter/climate.py | 1 + homeassistant/components/matter/fan.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index 192cb6b3bb4..713aadf5620 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -350,6 +350,7 @@ DISCOVERY_SCHEMAS = [ clusters.Thermostat.Attributes.TemperatureSetpointHold, clusters.Thermostat.Attributes.UnoccupiedCoolingSetpoint, clusters.Thermostat.Attributes.UnoccupiedHeatingSetpoint, + clusters.OnOff.Attributes.OnOff, ), device_type=(device_types.Thermostat, device_types.RoomAirConditioner), ), diff --git a/homeassistant/components/matter/fan.py b/homeassistant/components/matter/fan.py index 86f03dc7a03..8cbd24977e3 100644 --- a/homeassistant/components/matter/fan.py +++ b/homeassistant/components/matter/fan.py @@ -313,6 +313,7 @@ DISCOVERY_SCHEMAS = [ clusters.FanControl.Attributes.RockSetting, clusters.FanControl.Attributes.WindSetting, clusters.FanControl.Attributes.AirflowDirection, + clusters.OnOff.Attributes.OnOff, ), ), ] From 700675042bfec5e8eb81ff98eeacc1cb3271aa0c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 5 Jul 2024 00:42:29 -0700 Subject: [PATCH 0789/2411] Improve redaction for stream error messages (#120867) --- homeassistant/components/stream/worker.py | 17 +++++++++++++---- tests/components/stream/test_worker.py | 7 +++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index f51a3f98b01..354cc476186 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -47,6 +47,14 @@ class StreamWorkerError(Exception): """An exception thrown while processing a stream.""" +def redact_av_error_string(err: av.AVError) -> str: + """Return an error string with credentials redacted from the url.""" + parts = [str(err.type), err.strerror] + if err.filename is not None: + parts.append(redact_credentials(err.filename)) + return ", ".join(parts) + + class StreamEndedError(StreamWorkerError): """Raised when the stream is complete, exposed for facilitating testing.""" @@ -516,8 +524,7 @@ def stream_worker( container = av.open(source, options=pyav_options, timeout=SOURCE_TIMEOUT) except av.AVError as err: raise StreamWorkerError( - f"Error opening stream ({err.type}, {err.strerror})" - f" {redact_credentials(str(source))}" + f"Error opening stream ({redact_av_error_string(err)})" ) from err try: video_stream = container.streams.video[0] @@ -592,7 +599,7 @@ def stream_worker( except av.AVError as ex: container.close() raise StreamWorkerError( - f"Error demuxing stream while finding first packet: {ex!s}" + f"Error demuxing stream while finding first packet ({redact_av_error_string(ex)})" ) from ex muxer = StreamMuxer( @@ -617,7 +624,9 @@ def stream_worker( except StopIteration as ex: raise StreamEndedError("Stream ended; no additional packets") from ex except av.AVError as ex: - raise StreamWorkerError(f"Error demuxing stream: {ex!s}") from ex + raise StreamWorkerError( + f"Error demuxing stream ({redact_av_error_string(ex)})" + ) from ex muxer.mux_packet(packet) diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 2cb90c5ee9a..7226adc7d7e 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -772,12 +772,15 @@ async def test_worker_log( with patch("av.open") as av_open: # pylint: disable-next=c-extension-no-member - av_open.side_effect = av.error.InvalidDataError(-2, "error") + av_open.side_effect = av.error.InvalidDataError( + code=-2, message="Invalid data", filename=stream_url + ) with pytest.raises(StreamWorkerError) as err: run_worker(hass, stream, stream_url) await hass.async_block_till_done() assert ( - str(err.value) == f"Error opening stream (ERRORTYPE_-2, error) {redacted_url}" + str(err.value) + == f"Error opening stream (ERRORTYPE_-2, Invalid data, {redacted_url})" ) assert stream_url not in caplog.text From 97de9c9f69d35b00ad67088f4679757e5f0aeb63 Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Fri, 5 Jul 2024 09:45:20 +0200 Subject: [PATCH 0790/2411] Revert Homematic IP Cloud unique ID changes (#121231) --- .../homematicip_cloud/generic_entity.py | 11 +- .../components/homematicip_cloud/sensor.py | 288 +++++++++++------- .../fixtures/homematicip_cloud.json | 135 ++++++++ .../homematicip_cloud/test_device.py | 2 +- .../homematicip_cloud/test_sensor.py | 36 +++ 5 files changed, 351 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index 5cd48515ad7..163f3eec75e 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -216,14 +216,13 @@ class HomematicipGenericEntity(Entity): @property def unique_id(self) -> str: """Return a unique ID.""" - suffix = "" - if self._post is not None: - suffix = f"_{self._post}" - + unique_id = f"{self.__class__.__name__}_{self._device.id}" if self._is_multi_channel: - return f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}{suffix}" + unique_id = ( + f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}" + ) - return f"{self.__class__.__name__}_{self._device.id}{suffix}" + return unique_id @property def icon(self) -> str | None: diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 6bf128a1663..1f76c6cce1f 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Callable -from dataclasses import dataclass from typing import Any from homematicip.aio.device import ( @@ -36,7 +35,6 @@ from homematicip.base.functionalChannels import FunctionalChannel from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, - SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry @@ -163,19 +161,28 @@ async def async_setup_entry( for ch in get_channels_from_device( device, FunctionalChannelType.ENERGY_SENSORS_INTERFACE_CHANNEL ): - if ch.connectedEnergySensorType not in SENSORS_ESI: - continue + if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_IEC: + if ch.currentPowerConsumption is not None: + entities.append(HmipEsiIecPowerConsumption(hap, device)) + if ch.energyCounterOneType != ESI_TYPE_UNKNOWN: + entities.append(HmipEsiIecEnergyCounterHighTariff(hap, device)) + if ch.energyCounterTwoType != ESI_TYPE_UNKNOWN: + entities.append(HmipEsiIecEnergyCounterLowTariff(hap, device)) + if ch.energyCounterThreeType != ESI_TYPE_UNKNOWN: + entities.append( + HmipEsiIecEnergyCounterInputSingleTariff(hap, device) + ) - new_entities = [ - HmipEsiSensorEntity(hap, device, ch.index, description) - for description in SENSORS_ESI[ch.connectedEnergySensorType] - ] + if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_GAS: + if ch.currentGasFlow is not None: + entities.append(HmipEsiGasCurrentGasFlow(hap, device)) + if ch.gasVolume is not None: + entities.append(HmipEsiGasGasVolume(hap, device)) - entities.extend( - entity - for entity in new_entities - if entity.entity_description.exists_fn(ch) - ) + if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_LED: + if ch.currentPowerConsumption is not None: + entities.append(HmipEsiLedCurrentPowerConsumption(hap, device)) + entities.append(HmipEsiLedEnergyCounterHighTariff(hap, device)) async_add_entities(entities) @@ -434,132 +441,185 @@ class HomematicpTemperatureExternalSensorDelta(HomematicipGenericEntity, SensorE return self._device.temperatureExternalDelta -@dataclass(kw_only=True, frozen=True) -class HmipEsiSensorEntityDescription(SensorEntityDescription): - """SensorEntityDescription for HmIP Sensors.""" - - value_fn: Callable[[AsyncEnergySensorsInterface], StateType] - exists_fn: Callable[[FunctionalChannel], bool] - type_fn: Callable[[AsyncEnergySensorsInterface], str] - - -SENSORS_ESI = { - ESI_CONNECTED_SENSOR_TYPE_IEC: [ - HmipEsiSensorEntityDescription( - key=ESI_TYPE_CURRENT_POWER_CONSUMPTION, - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda device: device.functional_channel.currentPowerConsumption, - exists_fn=lambda channel: channel.currentPowerConsumption is not None, - type_fn=lambda device: "CurrentPowerConsumption", - ), - HmipEsiSensorEntityDescription( - key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda device: device.functional_channel.energyCounterOne, - exists_fn=lambda channel: channel.energyCounterOneType != ESI_TYPE_UNKNOWN, - type_fn=lambda device: device.functional_channel.energyCounterOneType, - ), - HmipEsiSensorEntityDescription( - key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF, - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda device: device.functional_channel.energyCounterTwo, - exists_fn=lambda channel: channel.energyCounterTwoType != ESI_TYPE_UNKNOWN, - type_fn=lambda device: device.functional_channel.energyCounterTwoType, - ), - HmipEsiSensorEntityDescription( - key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF, - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda device: device.functional_channel.energyCounterThree, - exists_fn=lambda channel: channel.energyCounterThreeType - != ESI_TYPE_UNKNOWN, - type_fn=lambda device: device.functional_channel.energyCounterThreeType, - ), - ], - ESI_CONNECTED_SENSOR_TYPE_LED: [ - HmipEsiSensorEntityDescription( - key=ESI_TYPE_CURRENT_POWER_CONSUMPTION, - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda device: device.functional_channel.currentPowerConsumption, - exists_fn=lambda channel: channel.currentPowerConsumption is not None, - type_fn=lambda device: "CurrentPowerConsumption", - ), - HmipEsiSensorEntityDescription( - key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda device: device.functional_channel.energyCounterOne, - exists_fn=lambda channel: channel.energyCounterOne is not None, - type_fn=lambda device: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, - ), - ], - ESI_CONNECTED_SENSOR_TYPE_GAS: [ - HmipEsiSensorEntityDescription( - key=ESI_TYPE_CURRENT_GAS_FLOW, - native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, - device_class=SensorDeviceClass.VOLUME_FLOW_RATE, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda device: device.functional_channel.currentGasFlow, - exists_fn=lambda channel: channel.currentGasFlow is not None, - type_fn=lambda device: "CurrentGasFlow", - ), - HmipEsiSensorEntityDescription( - key=ESI_TYPE_CURRENT_GAS_VOLUME, - native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda device: device.functional_channel.gasVolume, - exists_fn=lambda channel: channel.gasVolume is not None, - type_fn=lambda device: "GasVolume", - ), - ], -} - - class HmipEsiSensorEntity(HomematicipGenericEntity, SensorEntity): """EntityDescription for HmIP-ESI Sensors.""" - entity_description: HmipEsiSensorEntityDescription - def __init__( self, hap: HomematicipHAP, device: HomematicipGenericEntity, - channel_index: int, - entity_description: HmipEsiSensorEntityDescription, + key: str, + value_fn: Callable[[FunctionalChannel], StateType], + type_fn: Callable[[FunctionalChannel], str], ) -> None: """Initialize Sensor Entity.""" super().__init__( hap=hap, device=device, - channel=channel_index, - post=entity_description.key, + channel=1, + post=key, is_multi_channel=False, ) - self.entity_description = entity_description + + self._value_fn = value_fn + self._type_fn = type_fn @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the esi sensor.""" state_attr = super().extra_state_attributes - state_attr[ATTR_ESI_TYPE] = self.entity_description.type_fn(self) + state_attr[ATTR_ESI_TYPE] = self._type_fn(self.functional_channel) return state_attr @property def native_value(self) -> str | None: """Return the state of the sensor.""" - return str(self.entity_description.value_fn(self)) + return str(self._value_fn(self.functional_channel)) + + +class HmipEsiIecPowerConsumption(HmipEsiSensorEntity): + """Representation of the Hmip-ESI IEC currentPowerConsumption sensor.""" + + _attr_device_class = SensorDeviceClass.POWER + _attr_native_unit_of_measurement = UnitOfPower.WATT + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key="CurrentPowerConsumption", + value_fn=lambda channel: channel.currentPowerConsumption, + type_fn=lambda channel: "CurrentPowerConsumption", + ) + + +class HmipEsiIecEnergyCounterHighTariff(HmipEsiSensorEntity): + """Representation of the Hmip-ESI IEC energyCounterOne sensor.""" + + _attr_device_class = SensorDeviceClass.ENERGY + _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + _attr_state_class = SensorStateClass.TOTAL_INCREASING + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, + value_fn=lambda channel: channel.energyCounterOne, + type_fn=lambda channel: channel.energyCounterOneType, + ) + + +class HmipEsiIecEnergyCounterLowTariff(HmipEsiSensorEntity): + """Representation of the Hmip-ESI IEC energyCounterTwo sensor.""" + + _attr_device_class = SensorDeviceClass.ENERGY + _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + _attr_state_class = SensorStateClass.TOTAL_INCREASING + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF, + value_fn=lambda channel: channel.energyCounterTwo, + type_fn=lambda channel: channel.energyCounterTwoType, + ) + + +class HmipEsiIecEnergyCounterInputSingleTariff(HmipEsiSensorEntity): + """Representation of the Hmip-ESI IEC energyCounterThree sensor.""" + + _attr_device_class = SensorDeviceClass.ENERGY + _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + _attr_state_class = SensorStateClass.TOTAL_INCREASING + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF, + value_fn=lambda channel: channel.energyCounterThree, + type_fn=lambda channel: channel.energyCounterThreeType, + ) + + +class HmipEsiGasCurrentGasFlow(HmipEsiSensorEntity): + """Representation of the Hmip-ESI Gas currentGasFlow sensor.""" + + _attr_device_class = SensorDeviceClass.VOLUME_FLOW_RATE + _attr_native_unit_of_measurement = UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key="CurrentGasFlow", + value_fn=lambda channel: channel.currentGasFlow, + type_fn=lambda channel: "CurrentGasFlow", + ) + + +class HmipEsiGasGasVolume(HmipEsiSensorEntity): + """Representation of the Hmip-ESI Gas gasVolume sensor.""" + + _attr_device_class = SensorDeviceClass.GAS + _attr_native_unit_of_measurement = UnitOfVolume.CUBIC_METERS + _attr_state_class = SensorStateClass.TOTAL_INCREASING + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key="GasVolume", + value_fn=lambda channel: channel.gasVolume, + type_fn=lambda channel: "GasVolume", + ) + + +class HmipEsiLedCurrentPowerConsumption(HmipEsiSensorEntity): + """Representation of the Hmip-ESI LED currentPowerConsumption sensor.""" + + _attr_device_class = SensorDeviceClass.POWER + _attr_native_unit_of_measurement = UnitOfPower.WATT + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key="CurrentPowerConsumption", + value_fn=lambda channel: channel.currentPowerConsumption, + type_fn=lambda channel: "CurrentPowerConsumption", + ) + + +class HmipEsiLedEnergyCounterHighTariff(HmipEsiSensorEntity): + """Representation of the Hmip-ESI LED energyCounterOne sensor.""" + + _attr_device_class = SensorDeviceClass.ENERGY + _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + _attr_state_class = SensorStateClass.TOTAL_INCREASING + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__( + hap, + device, + key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, + value_fn=lambda channel: channel.energyCounterOne, + type_fn=lambda channel: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF, + ) class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEntity): diff --git a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json index eba2c803b1f..e67ffd78467 100644 --- a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json +++ b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json @@ -7757,6 +7757,141 @@ "serializedGlobalTradeItemNumber": "3014F711000000000ESIIEC2", "type": "ENERGY_SENSORS_INTERFACE", "updateState": "UP_TO_DATE" + }, + "3014F7110000000000ESIIE3": { + "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", + "deviceArchetype": "HMIP", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "controlsMountingOrientation": null, + "daliBusState": null, + "defaultLinkedGroup": [], + "deviceCommunicationError": null, + "deviceDriveError": null, + "deviceDriveModeError": null, + "deviceId": "3014F7110000000000ESIIE3", + "deviceOperationMode": null, + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": null, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000031"], + "index": 0, + "label": "", + "lockJammed": null, + "lowBat": false, + "mountingOrientation": null, + "multicastRoutingEnabled": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -94, + "rssiPeerValue": null, + "sensorCommunicationError": false, + "sensorError": true, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceCommunicationError": false, + "IFeatureDeviceDaliBusError": false, + "IFeatureDeviceDriveError": false, + "IFeatureDeviceDriveModeError": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceSensorCommunicationError": true, + "IFeatureDeviceSensorError": true, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDefaultLinkedGroup": false, + "IOptionalFeatureDeviceErrorLockJammed": false, + "IOptionalFeatureDeviceOperationMode": false, + "IOptionalFeatureDisplayContrast": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true, + "IOptionalFeatureMountingOrientation": false + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "channelRole": "ENERGY_SENSOR", + "connectedEnergySensorType": "ES_LED", + "currentGasFlow": null, + "currentPowerConsumption": 189.15, + "deviceId": "3014F7110000000000ESIIE3", + "energyCounterOne": 23825.748, + "energyCounterOneType": "UNKNOWN", + "energyCounterThree": null, + "energyCounterThreeType": "UNKNOWN", + "energyCounterTwo": null, + "energyCounterTwoType": "UNKNOWN", + "functionalChannelType": "ENERGY_SENSORS_INTERFACE_CHANNEL", + "gasVolume": null, + "gasVolumePerImpulse": 0.01, + "groupIndex": 1, + "groups": ["00000000-0000-0000-0000-000000000057"], + "impulsesPerKWH": 1000, + "index": 1, + "label": "", + "supportedOptionalFeatures": { + "IOptionalFeatureCounterOffset": true, + "IOptionalFeatureCurrentGasFlow": false, + "IOptionalFeatureCurrentPowerConsumption": true, + "IOptionalFeatureEnergyCounterOne": true, + "IOptionalFeatureEnergyCounterThree": false, + "IOptionalFeatureEnergyCounterTwo": false, + "IOptionalFeatureGasVolume": false, + "IOptionalFeatureGasVolumePerImpulse": false, + "IOptionalFeatureImpulsesPerKWH": true + } + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000ESIIE3", + "label": "esi_led", + "lastStatusUpdate": 1702420986697, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manuallyUpdateForced": false, + "manufacturerCode": 1, + "measuredAttributes": {}, + "modelId": 509, + "modelType": "HmIP-ESI", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000ESIIE3", + "type": "ENERGY_SENSORS_INTERFACE", + "updateState": "UP_TO_DATE" } }, "groups": { diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 348171b3187..074a30e94b2 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -26,7 +26,7 @@ async def test_hmip_load_all_supported_devices( test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 290 + assert len(mock_hap.hmip_device_by_entity_id) == 293 async def test_hmip_remove_device( diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 6951b750b2f..2b62c46fd72 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -634,3 +634,39 @@ async def test_hmip_esi_gas_gas_volume( ) assert ha_state.state == "1019.26" + + +async def test_hmip_esi_led_current_power_consumption( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC currentPowerConsumption Sensor.""" + entity_id = "sensor.esi_led_currentPowerConsumption" + entity_name = "esi_led CurrentPowerConsumption" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_led"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "189.15" + + +async def test_hmip_esi_led_energy_counter_usage_high_tariff( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test ESI-IEC ENERGY_COUNTER_USAGE_HIGH_TARIFF.""" + entity_id = "sensor.esi_led_energy_counter_usage_high_tariff" + entity_name = "esi_led ENERGY_COUNTER_USAGE_HIGH_TARIFF" + device_model = "HmIP-ESI" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["esi_led"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "23825.748" From 229e54d0b15c52d2a33cfbe27dd496587017245e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Jul 2024 02:46:12 -0500 Subject: [PATCH 0791/2411] Remove unneeded blocking sleep in srp_energy tests (#121141) --- tests/components/srp_energy/test_sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index 0c1eed11c96..025d9fe49ca 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -1,6 +1,5 @@ """Tests for the srp_energy sensor platform.""" -import time from unittest.mock import patch from requests.models import HTTPError @@ -80,7 +79,7 @@ async def test_srp_entity_timeout( ): client = srp_energy_mock.return_value client.validate.return_value = True - client.usage = lambda _, __, ___: time.sleep(1) # noqa: ASYNC251 + client.usage = lambda _, __, ___: None mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) From daaf35d4c1a914919bea732f74c07f3ef57684b5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 Jul 2024 09:46:36 +0200 Subject: [PATCH 0792/2411] Simplify conversation tests (#121060) Co-authored-by: Franck Nijhof --- tests/components/conversation/test_init.py | 39 ++++------------------ 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index dc940dba81b..e42510ee1f1 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -65,7 +65,6 @@ async def test_http_processing_intent( hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator, - hass_admin_user: MockUser, agent_id, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, @@ -96,7 +95,6 @@ async def test_http_processing_intent_target_ha_agent( hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator, - hass_admin_user: MockUser, mock_conversation_agent: MockAgent, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, @@ -127,7 +125,6 @@ async def test_http_processing_intent_entity_added_removed( hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator, - hass_admin_user: MockUser, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: @@ -161,7 +158,6 @@ async def test_http_processing_intent_entity_added_removed( ) hass.states.async_set("light.late", "off", {"friendly_name": "friendly light"}) - client = await hass_client() resp = await client.post( "/api/conversation/process", json={"text": "turn on friendly light"} ) @@ -175,7 +171,6 @@ async def test_http_processing_intent_entity_added_removed( # Now add an alias entity_registry.async_update_entity("light.late", aliases={"late added light"}) - client = await hass_client() resp = await client.post( "/api/conversation/process", json={"text": "turn on late added light"} ) @@ -189,7 +184,6 @@ async def test_http_processing_intent_entity_added_removed( # Now delete the entity hass.states.async_remove("light.late") - client = await hass_client() resp = await client.post( "/api/conversation/process", json={"text": "turn on late added light"} ) @@ -204,7 +198,6 @@ async def test_http_processing_intent_alias_added_removed( hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator, - hass_admin_user: MockUser, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: @@ -263,7 +256,6 @@ async def test_http_processing_intent_entity_renamed( hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator, - hass_admin_user: MockUser, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: @@ -351,7 +343,6 @@ async def test_http_processing_intent_entity_exposed( hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator, - hass_admin_user: MockUser, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: @@ -386,8 +377,7 @@ async def test_http_processing_intent_entity_exposed( assert data == snapshot assert data["response"]["response_type"] == "action_done" - calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") - client = await hass_client() + calls.clear() resp = await client.post( "/api/conversation/process", json={"text": "turn on my cool light"} ) @@ -403,7 +393,6 @@ async def test_http_processing_intent_entity_exposed( expose_entity(hass, "light.kitchen", False) await hass.async_block_till_done() - client = await hass_client() resp = await client.post( "/api/conversation/process", json={"text": "turn on kitchen light"} ) @@ -413,7 +402,6 @@ async def test_http_processing_intent_entity_exposed( assert data == snapshot assert data["response"]["response_type"] == "error" - client = await hass_client() resp = await client.post( "/api/conversation/process", json={"text": "turn on my cool light"} ) @@ -427,7 +415,6 @@ async def test_http_processing_intent_entity_exposed( expose_entity(hass, "light.kitchen", True) await hass.async_block_till_done() - client = await hass_client() resp = await client.post( "/api/conversation/process", json={"text": "turn on kitchen light"} ) @@ -438,7 +425,6 @@ async def test_http_processing_intent_entity_exposed( assert data == snapshot assert data["response"]["response_type"] == "action_done" - client = await hass_client() resp = await client.post( "/api/conversation/process", json={"text": "turn on my cool light"} ) @@ -662,6 +648,7 @@ async def test_http_api_wrong_data( assert resp.status == HTTPStatus.BAD_REQUEST +@pytest.mark.usefixtures("init_components") async def test_custom_agent( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -670,10 +657,6 @@ async def test_custom_agent( snapshot: SnapshotAssertion, ) -> None: """Test a custom conversation agent.""" - assert await async_setup_component(hass, "homeassistant", {}) - assert await async_setup_component(hass, "conversation", {}) - assert await async_setup_component(hass, "intent", {}) - client = await hass_client() data = { @@ -733,13 +716,12 @@ async def test_custom_agent( ) async def test_ws_api( hass: HomeAssistant, + init_components, hass_ws_client: WebSocketGenerator, payload, snapshot: SnapshotAssertion, ) -> None: """Test the Websocket conversation API.""" - assert await async_setup_component(hass, "homeassistant", {}) - assert await async_setup_component(hass, "conversation", {}) client = await hass_ws_client(hass) await client.send_json_auto_id({"type": "conversation/process", **payload}) @@ -753,11 +735,9 @@ async def test_ws_api( @pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS) async def test_ws_prepare( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, agent_id + hass: HomeAssistant, init_components, hass_ws_client: WebSocketGenerator, agent_id ) -> None: """Test the Websocket prepare conversation API.""" - assert await async_setup_component(hass, "homeassistant", {}) - assert await async_setup_component(hass, "conversation", {}) agent = default_agent.async_get_default_agent(hass) assert isinstance(agent, default_agent.DefaultAgent) @@ -781,15 +761,11 @@ async def test_ws_prepare( async def test_custom_sentences( hass: HomeAssistant, + init_components, hass_client: ClientSessionGenerator, - hass_admin_user: MockUser, snapshot: SnapshotAssertion, ) -> None: """Test custom sentences with a custom intent.""" - assert await async_setup_component(hass, "homeassistant", {}) - assert await async_setup_component(hass, "conversation", {}) - assert await async_setup_component(hass, "intent", {}) - # Expecting testing_config/custom_sentences/en/beer.yaml intent.async_register(hass, OrderBeerIntentHandler()) @@ -819,7 +795,6 @@ async def test_custom_sentences( async def test_custom_sentences_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_admin_user: MockUser, snapshot: SnapshotAssertion, ) -> None: """Test custom sentences with a custom intent in config.""" @@ -853,11 +828,9 @@ async def test_custom_sentences_config( assert data["response"]["speech"]["plain"]["speech"] == "Stealth mode engaged" -async def test_prepare_reload(hass: HomeAssistant) -> None: +async def test_prepare_reload(hass: HomeAssistant, init_components) -> None: """Test calling the reload service.""" language = hass.config.language - assert await async_setup_component(hass, "homeassistant", {}) - assert await async_setup_component(hass, "conversation", {}) # Load intents agent = default_agent.async_get_default_agent(hass) From ad02afe7be071d69d6fdf52a07e0c591f402a0b7 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Fri, 5 Jul 2024 10:02:38 +0200 Subject: [PATCH 0793/2411] Extend wrapper for sending commands to all platforms in Husqvarna Automower (#120255) --- .../components/husqvarna_automower/entity.py | 40 ++++++++++++- .../husqvarna_automower/lawn_mower.py | 35 ++---------- .../components/husqvarna_automower/number.py | 35 +++--------- .../components/husqvarna_automower/select.py | 16 ++---- .../components/husqvarna_automower/switch.py | 56 +++++-------------- .../husqvarna_automower/test_number.py | 4 +- .../husqvarna_automower/test_select.py | 2 +- .../husqvarna_automower/test_switch.py | 4 +- 8 files changed, 76 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/entity.py b/homeassistant/components/husqvarna_automower/entity.py index 80a936c2caf..1da49322989 100644 --- a/homeassistant/components/husqvarna_automower/entity.py +++ b/homeassistant/components/husqvarna_automower/entity.py @@ -1,14 +1,20 @@ """Platform for Husqvarna Automower base entity.""" +import asyncio +from collections.abc import Awaitable, Callable, Coroutine +import functools import logging +from typing import Any +from aioautomower.exceptions import ApiException from aioautomower.model import MowerActivities, MowerAttributes, MowerStates +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import AutomowerDataUpdateCoordinator -from .const import DOMAIN +from .const import DOMAIN, EXECUTION_TIME_DELAY _LOGGER = logging.getLogger(__name__) @@ -28,6 +34,38 @@ ERROR_STATES = [ ] +def handle_sending_exception( + poll_after_sending: bool = False, +) -> Callable[ + [Callable[..., Awaitable[Any]]], Callable[..., Coroutine[Any, Any, None]] +]: + """Handle exceptions while sending a command and optionally refresh coordinator.""" + + def decorator( + func: Callable[..., Awaitable[Any]], + ) -> Callable[..., Coroutine[Any, Any, None]]: + @functools.wraps(func) + async def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: + try: + await func(self, *args, **kwargs) + except ApiException as exception: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="command_send_failed", + translation_placeholders={"exception": str(exception)}, + ) from exception + else: + if poll_after_sending: + # As there are no updates from the websocket for this attribute, + # we need to wait until the command is executed and then poll the API. + await asyncio.sleep(EXECUTION_TIME_DELAY) + await self.coordinator.async_request_refresh() + + return wrapper + + return decorator + + class AutomowerBaseEntity(CoordinatorEntity[AutomowerDataUpdateCoordinator]): """Defining the Automower base Entity.""" diff --git a/homeassistant/components/husqvarna_automower/lawn_mower.py b/homeassistant/components/husqvarna_automower/lawn_mower.py index e59d9e635e9..dd2129599fb 100644 --- a/homeassistant/components/husqvarna_automower/lawn_mower.py +++ b/homeassistant/components/husqvarna_automower/lawn_mower.py @@ -1,12 +1,8 @@ """Husqvarna Automower lawn mower entity.""" -from collections.abc import Awaitable, Callable, Coroutine from datetime import timedelta -import functools import logging -from typing import Any -from aioautomower.exceptions import ApiException from aioautomower.model import MowerActivities, MowerStates import voluptuous as vol @@ -16,14 +12,12 @@ from homeassistant.components.lawn_mower import ( LawnMowerEntityFeature, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AutomowerConfigEntry -from .const import DOMAIN from .coordinator import AutomowerDataUpdateCoordinator -from .entity import AutomowerAvailableEntity +from .entity import AutomowerAvailableEntity, handle_sending_exception DOCKED_ACTIVITIES = (MowerActivities.PARKED_IN_CS, MowerActivities.CHARGING) MOWING_ACTIVITIES = ( @@ -49,25 +43,6 @@ OVERRIDE_MODES = [MOW, PARK] _LOGGER = logging.getLogger(__name__) -def handle_sending_exception( - func: Callable[..., Awaitable[Any]], -) -> Callable[..., Coroutine[Any, Any, None]]: - """Handle exceptions while sending a command.""" - - @functools.wraps(func) - async def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: - try: - return await func(self, *args, **kwargs) - except ApiException as exception: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="command_send_failed", - translation_placeholders={"exception": str(exception)}, - ) from exception - - return wrapper - - async def async_setup_entry( hass: HomeAssistant, entry: AutomowerConfigEntry, @@ -123,22 +98,22 @@ class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity): return LawnMowerActivity.DOCKED return LawnMowerActivity.ERROR - @handle_sending_exception + @handle_sending_exception() async def async_start_mowing(self) -> None: """Resume schedule.""" await self.coordinator.api.commands.resume_schedule(self.mower_id) - @handle_sending_exception + @handle_sending_exception() async def async_pause(self) -> None: """Pauses the mower.""" await self.coordinator.api.commands.pause_mowing(self.mower_id) - @handle_sending_exception + @handle_sending_exception() async def async_dock(self) -> None: """Parks the mower until next schedule.""" await self.coordinator.api.commands.park_until_next_schedule(self.mower_id) - @handle_sending_exception + @handle_sending_exception() async def async_override_schedule( self, override_mode: str, duration: timedelta ) -> None: diff --git a/homeassistant/components/husqvarna_automower/number.py b/homeassistant/components/husqvarna_automower/number.py index f6d55389195..540f6aa712e 100644 --- a/homeassistant/components/husqvarna_automower/number.py +++ b/homeassistant/components/husqvarna_automower/number.py @@ -1,26 +1,22 @@ """Creates the number entities for the mower.""" -import asyncio from collections.abc import Awaitable, Callable from dataclasses import dataclass import logging from typing import TYPE_CHECKING, Any -from aioautomower.exceptions import ApiException from aioautomower.model import MowerAttributes, WorkArea from aioautomower.session import AutomowerSession from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.const import PERCENTAGE, EntityCategory, Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AutomowerConfigEntry -from .const import EXECUTION_TIME_DELAY from .coordinator import AutomowerDataUpdateCoordinator -from .entity import AutomowerControlEntity +from .entity import AutomowerControlEntity, handle_sending_exception _LOGGER = logging.getLogger(__name__) @@ -160,16 +156,12 @@ class AutomowerNumberEntity(AutomowerControlEntity, NumberEntity): """Return the state of the number.""" return self.entity_description.value_fn(self.mower_attributes) + @handle_sending_exception() async def async_set_native_value(self, value: float) -> None: """Change to new number value.""" - try: - await self.entity_description.set_value_fn( - self.coordinator.api, self.mower_id, value - ) - except ApiException as exception: - raise HomeAssistantError( - f"Command couldn't be sent to the command queue: {exception}" - ) from exception + await self.entity_description.set_value_fn( + self.coordinator.api, self.mower_id, value + ) class AutomowerWorkAreaNumberEntity(AutomowerControlEntity, NumberEntity): @@ -208,21 +200,12 @@ class AutomowerWorkAreaNumberEntity(AutomowerControlEntity, NumberEntity): """Return the state of the number.""" return self.entity_description.value_fn(self.work_area) + @handle_sending_exception(poll_after_sending=True) async def async_set_native_value(self, value: float) -> None: """Change to new number value.""" - try: - await self.entity_description.set_value_fn( - self.coordinator, self.mower_id, value, self.work_area_id - ) - except ApiException as exception: - raise HomeAssistantError( - f"Command couldn't be sent to the command queue: {exception}" - ) from exception - else: - # As there are no updates from the websocket regarding work area changes, - # we need to wait 5s and then poll the API. - await asyncio.sleep(EXECUTION_TIME_DELAY) - await self.coordinator.async_request_refresh() + await self.entity_description.set_value_fn( + self.coordinator, self.mower_id, value, self.work_area_id + ) @callback diff --git a/homeassistant/components/husqvarna_automower/select.py b/homeassistant/components/husqvarna_automower/select.py index b647407581f..a9431acaae3 100644 --- a/homeassistant/components/husqvarna_automower/select.py +++ b/homeassistant/components/husqvarna_automower/select.py @@ -3,18 +3,16 @@ import logging from typing import cast -from aioautomower.exceptions import ApiException from aioautomower.model import HeadlightModes from homeassistant.components.select import SelectEntity from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AutomowerConfigEntry from .coordinator import AutomowerDataUpdateCoordinator -from .entity import AutomowerControlEntity +from .entity import AutomowerControlEntity, handle_sending_exception _LOGGER = logging.getLogger(__name__) @@ -64,13 +62,9 @@ class AutomowerSelectEntity(AutomowerControlEntity, SelectEntity): HeadlightModes, self.mower_attributes.settings.headlight.mode ).lower() + @handle_sending_exception() async def async_select_option(self, option: str) -> None: """Change the selected option.""" - try: - await self.coordinator.api.commands.set_headlight_mode( - self.mower_id, cast(HeadlightModes, option.upper()) - ) - except ApiException as exception: - raise HomeAssistantError( - f"Command couldn't be sent to the command queue: {exception}" - ) from exception + await self.coordinator.api.commands.set_headlight_mode( + self.mower_id, cast(HeadlightModes, option.upper()) + ) diff --git a/homeassistant/components/husqvarna_automower/switch.py b/homeassistant/components/husqvarna_automower/switch.py index 8a450b8e81a..a4b60054583 100644 --- a/homeassistant/components/husqvarna_automower/switch.py +++ b/homeassistant/components/husqvarna_automower/switch.py @@ -1,23 +1,19 @@ """Creates a switch entity for the mower.""" -import asyncio import logging from typing import TYPE_CHECKING, Any -from aioautomower.exceptions import ApiException from aioautomower.model import MowerModes, StayOutZones, Zone from homeassistant.components.switch import SwitchEntity from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AutomowerConfigEntry -from .const import EXECUTION_TIME_DELAY from .coordinator import AutomowerDataUpdateCoordinator -from .entity import AutomowerControlEntity +from .entity import AutomowerControlEntity, handle_sending_exception _LOGGER = logging.getLogger(__name__) @@ -67,23 +63,15 @@ class AutomowerScheduleSwitchEntity(AutomowerControlEntity, SwitchEntity): """Return the state of the switch.""" return self.mower_attributes.mower.mode != MowerModes.HOME + @handle_sending_exception() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - try: - await self.coordinator.api.commands.park_until_further_notice(self.mower_id) - except ApiException as exception: - raise HomeAssistantError( - f"Command couldn't be sent to the command queue: {exception}" - ) from exception + await self.coordinator.api.commands.park_until_further_notice(self.mower_id) + @handle_sending_exception() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - try: - await self.coordinator.api.commands.resume_schedule(self.mower_id) - except ApiException as exception: - raise HomeAssistantError( - f"Command couldn't be sent to the command queue: {exception}" - ) from exception + await self.coordinator.api.commands.resume_schedule(self.mower_id) class AutomowerStayOutZoneSwitchEntity(AutomowerControlEntity, SwitchEntity): @@ -128,37 +116,19 @@ class AutomowerStayOutZoneSwitchEntity(AutomowerControlEntity, SwitchEntity): """Return True if the device is available and the zones are not `dirty`.""" return super().available and not self.stay_out_zones.dirty + @handle_sending_exception(poll_after_sending=True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - try: - await self.coordinator.api.commands.switch_stay_out_zone( - self.mower_id, self.stay_out_zone_uid, False - ) - except ApiException as exception: - raise HomeAssistantError( - f"Command couldn't be sent to the command queue: {exception}" - ) from exception - else: - # As there are no updates from the websocket regarding stay out zone changes, - # we need to wait until the command is executed and then poll the API. - await asyncio.sleep(EXECUTION_TIME_DELAY) - await self.coordinator.async_request_refresh() + await self.coordinator.api.commands.switch_stay_out_zone( + self.mower_id, self.stay_out_zone_uid, False + ) + @handle_sending_exception(poll_after_sending=True) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - try: - await self.coordinator.api.commands.switch_stay_out_zone( - self.mower_id, self.stay_out_zone_uid, True - ) - except ApiException as exception: - raise HomeAssistantError( - f"Command couldn't be sent to the command queue: {exception}" - ) from exception - else: - # As there are no updates from the websocket regarding stay out zone changes, - # we need to wait until the command is executed and then poll the API. - await asyncio.sleep(EXECUTION_TIME_DELAY) - await self.coordinator.async_request_refresh() + await self.coordinator.api.commands.switch_stay_out_zone( + self.mower_id, self.stay_out_zone_uid, True + ) @callback diff --git a/tests/components/husqvarna_automower/test_number.py b/tests/components/husqvarna_automower/test_number.py index 0547d6a9b2e..ac7353386ac 100644 --- a/tests/components/husqvarna_automower/test_number.py +++ b/tests/components/husqvarna_automower/test_number.py @@ -41,7 +41,7 @@ async def test_number_commands( mocked_method.side_effect = ApiException("Test error") with pytest.raises( HomeAssistantError, - match="Command couldn't be sent to the command queue: Test error", + match="Failed to send command: Test error", ): await hass.services.async_call( domain="number", @@ -85,7 +85,7 @@ async def test_number_workarea_commands( mocked_method.side_effect = ApiException("Test error") with pytest.raises( HomeAssistantError, - match="Command couldn't be sent to the command queue: Test error", + match="Failed to send command: Test error", ): await hass.services.async_call( domain="number", diff --git a/tests/components/husqvarna_automower/test_select.py b/tests/components/husqvarna_automower/test_select.py index 2728bb5e672..e885a4d3487 100644 --- a/tests/components/husqvarna_automower/test_select.py +++ b/tests/components/husqvarna_automower/test_select.py @@ -88,7 +88,7 @@ async def test_select_commands( mocked_method.side_effect = ApiException("Test error") with pytest.raises( HomeAssistantError, - match="Command couldn't be sent to the command queue: Test error", + match="Failed to send command: Test error", ): await hass.services.async_call( domain="select", diff --git a/tests/components/husqvarna_automower/test_switch.py b/tests/components/husqvarna_automower/test_switch.py index 08450158876..24fd63be749 100644 --- a/tests/components/husqvarna_automower/test_switch.py +++ b/tests/components/husqvarna_automower/test_switch.py @@ -83,7 +83,7 @@ async def test_switch_commands( mocked_method.side_effect = ApiException("Test error") with pytest.raises( HomeAssistantError, - match="Command couldn't be sent to the command queue: Test error", + match="Failed to send command: Test error", ): await hass.services.async_call( domain="switch", @@ -134,7 +134,7 @@ async def test_stay_out_zone_switch_commands( mocked_method.side_effect = ApiException("Test error") with pytest.raises( HomeAssistantError, - match="Command couldn't be sent to the command queue: Test error", + match="Failed to send command: Test error", ): await hass.services.async_call( domain="switch", From 0cf5b4f965a03a6315e55cc8a282070180e88482 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 5 Jul 2024 10:02:52 +0200 Subject: [PATCH 0794/2411] Bump python-holidays to 0.52 (#121283) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index cb67039f374..075285bbdb9 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.51", "babel==2.15.0"] + "requirements": ["holidays==0.52", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 1148f46e2d1..ad609954a57 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.51"] + "requirements": ["holidays==0.52"] } diff --git a/requirements_all.txt b/requirements_all.txt index d7826847b5f..49b7f73ec17 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.51 +holidays==0.52 # homeassistant.components.frontend home-assistant-frontend==20240703.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 98a89013d56..1552310a95e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.51 +holidays==0.52 # homeassistant.components.frontend home-assistant-frontend==20240703.0 From 2ab02c06c6e9e244e3315b512590ed03cd77ac3c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 5 Jul 2024 10:54:16 +0200 Subject: [PATCH 0795/2411] Add monkeypatch type hints to rflink tests (#121053) --- tests/components/rflink/test_binary_sensor.py | 15 +++- tests/components/rflink/test_cover.py | 42 ++++++++--- tests/components/rflink/test_init.py | 73 ++++++++++++++----- tests/components/rflink/test_light.py | 50 ++++++++++--- tests/components/rflink/test_sensor.py | 24 ++++-- tests/components/rflink/test_switch.py | 30 ++++++-- tests/components/rflink/test_utils.py | 3 +- 7 files changed, 178 insertions(+), 59 deletions(-) diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index c92eaa30fe8..9329edb3a00 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -7,6 +7,7 @@ automatic sensor creation. from datetime import timedelta from freezegun import freeze_time +import pytest from homeassistant.components.rflink import CONF_RECONNECT_INTERVAL from homeassistant.const import ( @@ -45,7 +46,9 @@ CONFIG = { } -async def test_default_setup(hass: HomeAssistant, monkeypatch) -> None: +async def test_default_setup( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test all basic functionality of the rflink sensor component.""" # setup mocking rflink module event_callback, create, _, _ = await mock_rflink(hass, CONFIG, DOMAIN, monkeypatch) @@ -84,7 +87,9 @@ async def test_default_setup(hass: HomeAssistant, monkeypatch) -> None: assert hass.states.get("binary_sensor.test").state == STATE_OFF -async def test_entity_availability(hass: HomeAssistant, monkeypatch) -> None: +async def test_entity_availability( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """If Rflink device is disconnected, entities should become unavailable.""" # Make sure Rflink mock does not 'recover' to quickly from the # disconnect or else the unavailability cannot be measured @@ -125,7 +130,7 @@ async def test_entity_availability(hass: HomeAssistant, monkeypatch) -> None: assert hass.states.get("binary_sensor.test").state == STATE_ON -async def test_off_delay(hass: HomeAssistant, monkeypatch) -> None: +async def test_off_delay(hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch) -> None: """Test off_delay option.""" # setup mocking rflink module event_callback, create, _, _ = await mock_rflink(hass, CONFIG, DOMAIN, monkeypatch) @@ -188,7 +193,9 @@ async def test_off_delay(hass: HomeAssistant, monkeypatch) -> None: assert len(events) == 3 -async def test_restore_state(hass: HomeAssistant, monkeypatch) -> None: +async def test_restore_state( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Ensure states are restored on startup.""" mock_restore_cache( hass, (State(f"{DOMAIN}.test", STATE_ON), State(f"{DOMAIN}.test2", STATE_ON)) diff --git a/tests/components/rflink/test_cover.py b/tests/components/rflink/test_cover.py index 0829fddef51..0f14e76620f 100644 --- a/tests/components/rflink/test_cover.py +++ b/tests/components/rflink/test_cover.py @@ -5,6 +5,8 @@ control of RFLink cover devices. """ +import pytest + from homeassistant.components.rflink import EVENT_BUTTON_PRESSED from homeassistant.const import ( ATTR_ENTITY_ID, @@ -37,7 +39,9 @@ CONFIG = { } -async def test_default_setup(hass: HomeAssistant, monkeypatch) -> None: +async def test_default_setup( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test all basic functionality of the RFLink cover component.""" # setup mocking rflink module event_callback, create, protocol, _ = await mock_rflink( @@ -107,7 +111,9 @@ async def test_default_setup(hass: HomeAssistant, monkeypatch) -> None: assert protocol.send_command_ack.call_args_list[1][0][1] == "UP" -async def test_firing_bus_event(hass: HomeAssistant, monkeypatch) -> None: +async def test_firing_bus_event( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Incoming RFLink command events should be put on the HA event bus.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -142,7 +148,9 @@ async def test_firing_bus_event(hass: HomeAssistant, monkeypatch) -> None: assert calls[0].data == {"state": "down", "entity_id": f"{DOMAIN}.test"} -async def test_signal_repetitions(hass: HomeAssistant, monkeypatch) -> None: +async def test_signal_repetitions( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Command should be sent amount of configured repetitions.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -180,7 +188,9 @@ async def test_signal_repetitions(hass: HomeAssistant, monkeypatch) -> None: assert protocol.send_command_ack.call_count == 5 -async def test_signal_repetitions_alternation(hass: HomeAssistant, monkeypatch) -> None: +async def test_signal_repetitions_alternation( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Simultaneously switching entities must alternate repetitions.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -211,7 +221,9 @@ async def test_signal_repetitions_alternation(hass: HomeAssistant, monkeypatch) assert protocol.send_command_ack.call_args_list[3][0][0] == "protocol_0_1" -async def test_signal_repetitions_cancelling(hass: HomeAssistant, monkeypatch) -> None: +async def test_signal_repetitions_cancelling( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Cancel outstanding repetitions when state changed.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -240,7 +252,9 @@ async def test_signal_repetitions_cancelling(hass: HomeAssistant, monkeypatch) - assert protocol.send_command_ack.call_args_list[3][0][1] == "UP" -async def test_group_alias(hass: HomeAssistant, monkeypatch) -> None: +async def test_group_alias( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Group aliases should only respond to group commands (allon/alloff).""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -270,7 +284,9 @@ async def test_group_alias(hass: HomeAssistant, monkeypatch) -> None: assert hass.states.get(f"{DOMAIN}.test").state == STATE_OPEN -async def test_nogroup_alias(hass: HomeAssistant, monkeypatch) -> None: +async def test_nogroup_alias( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Non group aliases should not respond to group commands.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -303,7 +319,9 @@ async def test_nogroup_alias(hass: HomeAssistant, monkeypatch) -> None: assert hass.states.get(f"{DOMAIN}.test").state == STATE_OPEN -async def test_nogroup_device_id(hass: HomeAssistant, monkeypatch) -> None: +async def test_nogroup_device_id( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Device id that do not respond to group commands (allon/alloff).""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -331,7 +349,9 @@ async def test_nogroup_device_id(hass: HomeAssistant, monkeypatch) -> None: assert hass.states.get(f"{DOMAIN}.test").state == STATE_OPEN -async def test_restore_state(hass: HomeAssistant, monkeypatch) -> None: +async def test_restore_state( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Ensure states are restored on startup.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -377,7 +397,9 @@ async def test_restore_state(hass: HomeAssistant, monkeypatch) -> None: # The code checks the ID, it will use the # 'inverted' class when the name starts with # 'newkaku' -async def test_inverted_cover(hass: HomeAssistant, monkeypatch) -> None: +async def test_inverted_cover( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Ensure states are restored on startup.""" config = { "rflink": {"port": "/dev/ttyABC0"}, diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index f901e46aea1..47062fe250a 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -31,7 +31,12 @@ from homeassistant.helpers import entity_registry as er async def mock_rflink( - hass, config, domain, monkeypatch, failures=None, failcommand=False + hass: HomeAssistant, + config, + domain, + monkeypatch: pytest.MonkeyPatch, + failures=None, + failcommand=False, ): """Create mock RFLink asyncio protocol, test component setup.""" transport, protocol = (Mock(), Mock()) @@ -77,7 +82,9 @@ async def mock_rflink( return event_callback, mock_create, protocol, disconnect_callback -async def test_version_banner(hass: HomeAssistant, monkeypatch) -> None: +async def test_version_banner( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test sending unknown commands doesn't cause issues.""" # use sensor domain during testing main platform domain = "sensor" @@ -102,7 +109,9 @@ async def test_version_banner(hass: HomeAssistant, monkeypatch) -> None: ) -async def test_send_no_wait(hass: HomeAssistant, monkeypatch) -> None: +async def test_send_no_wait( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test command sending without ack.""" domain = "switch" config = { @@ -126,7 +135,9 @@ async def test_send_no_wait(hass: HomeAssistant, monkeypatch) -> None: assert protocol.send_command.call_args_list[0][0][1] == "off" -async def test_cover_send_no_wait(hass: HomeAssistant, monkeypatch) -> None: +async def test_cover_send_no_wait( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test command sending to a cover device without ack.""" domain = "cover" config = { @@ -150,7 +161,9 @@ async def test_cover_send_no_wait(hass: HomeAssistant, monkeypatch) -> None: assert protocol.send_command.call_args_list[0][0][1] == "STOP" -async def test_send_command(hass: HomeAssistant, monkeypatch) -> None: +async def test_send_command( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test send_command service.""" domain = "rflink" config = {"rflink": {"port": "/dev/ttyABC0"}} @@ -168,7 +181,9 @@ async def test_send_command(hass: HomeAssistant, monkeypatch) -> None: assert protocol.send_command_ack.call_args_list[0][0][1] == "on" -async def test_send_command_invalid_arguments(hass: HomeAssistant, monkeypatch) -> None: +async def test_send_command_invalid_arguments( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test send_command service.""" domain = "rflink" config = {"rflink": {"port": "/dev/ttyABC0"}} @@ -201,7 +216,9 @@ async def test_send_command_invalid_arguments(hass: HomeAssistant, monkeypatch) assert not success, "send command should not succeed for unknown command" -async def test_send_command_event_propagation(hass: HomeAssistant, monkeypatch) -> None: +async def test_send_command_event_propagation( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test event propagation for send_command service.""" domain = "light" config = { @@ -243,7 +260,9 @@ async def test_send_command_event_propagation(hass: HomeAssistant, monkeypatch) assert hass.states.get(f"{domain}.test1").state == "off" -async def test_reconnecting_after_disconnect(hass: HomeAssistant, monkeypatch) -> None: +async def test_reconnecting_after_disconnect( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """An unexpected disconnect should cause a reconnect.""" domain = "sensor" config = { @@ -267,7 +286,9 @@ async def test_reconnecting_after_disconnect(hass: HomeAssistant, monkeypatch) - assert mock_create.call_count == 2 -async def test_reconnecting_after_failure(hass: HomeAssistant, monkeypatch) -> None: +async def test_reconnecting_after_failure( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """A failure to reconnect should be retried.""" domain = "sensor" config = { @@ -294,7 +315,9 @@ async def test_reconnecting_after_failure(hass: HomeAssistant, monkeypatch) -> N assert mock_create.call_count == 3 -async def test_error_when_not_connected(hass: HomeAssistant, monkeypatch) -> None: +async def test_error_when_not_connected( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Sending command should error when not connected.""" domain = "switch" config = { @@ -324,7 +347,9 @@ async def test_error_when_not_connected(hass: HomeAssistant, monkeypatch) -> Non assert not success, "changing state should not succeed when disconnected" -async def test_async_send_command_error(hass: HomeAssistant, monkeypatch) -> None: +async def test_async_send_command_error( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Sending command should error when protocol fails.""" domain = "rflink" config = {"rflink": {"port": "/dev/ttyABC0"}} @@ -345,7 +370,9 @@ async def test_async_send_command_error(hass: HomeAssistant, monkeypatch) -> Non assert protocol.send_command_ack.call_args_list[0][0][1] == SERVICE_TURN_OFF -async def test_race_condition(hass: HomeAssistant, monkeypatch) -> None: +async def test_race_condition( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test race condition for unknown components.""" domain = "light" config = {"rflink": {"port": "/dev/ttyABC0"}, domain: {"platform": "rflink"}} @@ -381,7 +408,7 @@ async def test_race_condition(hass: HomeAssistant, monkeypatch) -> None: assert new_sensor.state == "on" -async def test_not_connected(hass: HomeAssistant, monkeypatch) -> None: +async def test_not_connected() -> None: """Test Error when sending commands to a disconnected device.""" test_device = RflinkCommand("DUMMY_DEVICE") RflinkCommand.set_rflink_protocol(None) @@ -390,7 +417,9 @@ async def test_not_connected(hass: HomeAssistant, monkeypatch) -> None: async def test_keepalive( - hass: HomeAssistant, monkeypatch, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, ) -> None: """Validate negative keepalive values.""" keepalive_value = -3 @@ -418,7 +447,9 @@ async def test_keepalive( async def test_keepalive_2( - hass: HomeAssistant, monkeypatch, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, ) -> None: """Validate very short keepalive values.""" keepalive_value = 30 @@ -446,7 +477,9 @@ async def test_keepalive_2( async def test_keepalive_3( - hass: HomeAssistant, monkeypatch, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, ) -> None: """Validate keepalive=0 value.""" domain = RFLINK_DOMAIN @@ -466,7 +499,9 @@ async def test_keepalive_3( async def test_default_keepalive( - hass: HomeAssistant, monkeypatch, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, ) -> None: """Validate keepalive=0 value.""" domain = RFLINK_DOMAIN @@ -485,7 +520,9 @@ async def test_default_keepalive( async def test_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry, monkeypatch + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Validate the device unique_id.""" diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py index 5ee2375bc36..ceb2b19e192 100644 --- a/tests/components/rflink/test_light.py +++ b/tests/components/rflink/test_light.py @@ -5,6 +5,8 @@ control of RFLink switch devices. """ +import pytest + from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.rflink import EVENT_BUTTON_PRESSED from homeassistant.const import ( @@ -38,7 +40,9 @@ CONFIG = { } -async def test_default_setup(hass: HomeAssistant, monkeypatch) -> None: +async def test_default_setup( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test all basic functionality of the RFLink switch component.""" # setup mocking rflink module event_callback, create, protocol, _ = await mock_rflink( @@ -146,7 +150,9 @@ async def test_default_setup(hass: HomeAssistant, monkeypatch) -> None: assert protocol.send_command_ack.call_args_list[5][0][1] == "7" -async def test_firing_bus_event(hass: HomeAssistant, monkeypatch) -> None: +async def test_firing_bus_event( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Incoming RFLink command events should be put on the HA event bus.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -181,7 +187,9 @@ async def test_firing_bus_event(hass: HomeAssistant, monkeypatch) -> None: assert calls[0].data == {"state": "off", "entity_id": f"{DOMAIN}.test"} -async def test_signal_repetitions(hass: HomeAssistant, monkeypatch) -> None: +async def test_signal_repetitions( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Command should be sent amount of configured repetitions.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -237,7 +245,9 @@ async def test_signal_repetitions(hass: HomeAssistant, monkeypatch) -> None: assert protocol.send_command_ack.call_count == 8 -async def test_signal_repetitions_alternation(hass: HomeAssistant, monkeypatch) -> None: +async def test_signal_repetitions_alternation( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Simultaneously switching entities must alternate repetitions.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -268,7 +278,9 @@ async def test_signal_repetitions_alternation(hass: HomeAssistant, monkeypatch) assert protocol.send_command_ack.call_args_list[3][0][0] == "protocol_0_1" -async def test_signal_repetitions_cancelling(hass: HomeAssistant, monkeypatch) -> None: +async def test_signal_repetitions_cancelling( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Cancel outstanding repetitions when state changed.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -302,7 +314,9 @@ async def test_signal_repetitions_cancelling(hass: HomeAssistant, monkeypatch) - ] -async def test_type_toggle(hass: HomeAssistant, monkeypatch) -> None: +async def test_type_toggle( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test toggle type lights (on/on).""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -347,7 +361,9 @@ async def test_type_toggle(hass: HomeAssistant, monkeypatch) -> None: assert hass.states.get(f"{DOMAIN}.toggle_test").state == "off" -async def test_set_level_command(hass: HomeAssistant, monkeypatch) -> None: +async def test_set_level_command( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test 'set_level=XX' events.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -434,7 +450,9 @@ async def test_set_level_command(hass: HomeAssistant, monkeypatch) -> None: assert state.attributes[ATTR_BRIGHTNESS] == 0 -async def test_group_alias(hass: HomeAssistant, monkeypatch) -> None: +async def test_group_alias( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Group aliases should only respond to group commands (allon/alloff).""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -471,7 +489,9 @@ async def test_group_alias(hass: HomeAssistant, monkeypatch) -> None: assert hass.states.get(f"{DOMAIN}.test2").state == "on" -async def test_nogroup_alias(hass: HomeAssistant, monkeypatch) -> None: +async def test_nogroup_alias( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Non group aliases should not respond to group commands.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -504,7 +524,9 @@ async def test_nogroup_alias(hass: HomeAssistant, monkeypatch) -> None: assert hass.states.get(f"{DOMAIN}.test").state == "on" -async def test_nogroup_device_id(hass: HomeAssistant, monkeypatch) -> None: +async def test_nogroup_device_id( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Device id that do not respond to group commands (allon/alloff).""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -532,7 +554,9 @@ async def test_nogroup_device_id(hass: HomeAssistant, monkeypatch) -> None: assert hass.states.get(f"{DOMAIN}.test").state == "on" -async def test_disable_automatic_add(hass: HomeAssistant, monkeypatch) -> None: +async def test_disable_automatic_add( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """If disabled new devices should not be automatically added.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -550,7 +574,9 @@ async def test_disable_automatic_add(hass: HomeAssistant, monkeypatch) -> None: assert not hass.states.get(f"{DOMAIN}.protocol_0_0") -async def test_restore_state(hass: HomeAssistant, monkeypatch) -> None: +async def test_restore_state( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Ensure states are restored on startup.""" config = { "rflink": {"port": "/dev/ttyABC0"}, diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py index e375f3ae863..278dd45a114 100644 --- a/tests/components/rflink/test_sensor.py +++ b/tests/components/rflink/test_sensor.py @@ -5,6 +5,8 @@ automatic sensor creation. """ +import pytest + from homeassistant.components.rflink import ( CONF_RECONNECT_INTERVAL, DATA_ENTITY_LOOKUP, @@ -39,7 +41,9 @@ CONFIG = { } -async def test_default_setup(hass: HomeAssistant, monkeypatch) -> None: +async def test_default_setup( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test all basic functionality of the rflink sensor component.""" # setup mocking rflink module event_callback, create, _, _ = await mock_rflink(hass, CONFIG, DOMAIN, monkeypatch) @@ -100,7 +104,9 @@ async def test_default_setup(hass: HomeAssistant, monkeypatch) -> None: assert bat_sensor.attributes[ATTR_ICON] == "mdi:battery" -async def test_disable_automatic_add(hass: HomeAssistant, monkeypatch) -> None: +async def test_disable_automatic_add( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """If disabled new devices should not be automatically added.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -125,7 +131,9 @@ async def test_disable_automatic_add(hass: HomeAssistant, monkeypatch) -> None: assert not hass.states.get("sensor.test2") -async def test_entity_availability(hass: HomeAssistant, monkeypatch) -> None: +async def test_entity_availability( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """If Rflink device is disconnected, entities should become unavailable.""" # Make sure Rflink mock does not 'recover' to quickly from the # disconnect or else the unavailability cannot be measured @@ -160,7 +168,7 @@ async def test_entity_availability(hass: HomeAssistant, monkeypatch) -> None: assert hass.states.get("sensor.test").state == STATE_UNKNOWN -async def test_aliases(hass: HomeAssistant, monkeypatch) -> None: +async def test_aliases(hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch) -> None: """Validate the response to sensor's alias (with aliases).""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -202,7 +210,9 @@ async def test_aliases(hass: HomeAssistant, monkeypatch) -> None: assert updated_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE -async def test_race_condition(hass: HomeAssistant, monkeypatch) -> None: +async def test_race_condition( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test race condition for unknown components.""" config = {"rflink": {"port": "/dev/ttyABC0"}, DOMAIN: {"platform": "rflink"}} tmp_entity = TMP_ENTITY.format("test3") @@ -241,7 +251,9 @@ async def test_race_condition(hass: HomeAssistant, monkeypatch) -> None: assert new_sensor.state == "ko" -async def test_sensor_attributes(hass: HomeAssistant, monkeypatch) -> None: +async def test_sensor_attributes( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Validate the sensor attributes.""" config = { diff --git a/tests/components/rflink/test_switch.py b/tests/components/rflink/test_switch.py index 705856565ae..2aab145f847 100644 --- a/tests/components/rflink/test_switch.py +++ b/tests/components/rflink/test_switch.py @@ -5,6 +5,8 @@ control of Rflink switch devices. """ +import pytest + from homeassistant.components.rflink import EVENT_BUTTON_PRESSED from homeassistant.const import ( ATTR_ENTITY_ID, @@ -33,7 +35,9 @@ CONFIG = { } -async def test_default_setup(hass: HomeAssistant, monkeypatch) -> None: +async def test_default_setup( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Test all basic functionality of the rflink switch component.""" # setup mocking rflink module event_callback, create, protocol, _ = await mock_rflink( @@ -93,7 +97,9 @@ async def test_default_setup(hass: HomeAssistant, monkeypatch) -> None: assert protocol.send_command_ack.call_args_list[1][0][1] == "on" -async def test_group_alias(hass: HomeAssistant, monkeypatch) -> None: +async def test_group_alias( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Group aliases should only respond to group commands (allon/alloff).""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -123,7 +129,9 @@ async def test_group_alias(hass: HomeAssistant, monkeypatch) -> None: assert hass.states.get(f"{DOMAIN}.test").state == "on" -async def test_nogroup_alias(hass: HomeAssistant, monkeypatch) -> None: +async def test_nogroup_alias( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Non group aliases should not respond to group commands.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -156,7 +164,9 @@ async def test_nogroup_alias(hass: HomeAssistant, monkeypatch) -> None: assert hass.states.get(f"{DOMAIN}.test").state == "on" -async def test_nogroup_device_id(hass: HomeAssistant, monkeypatch) -> None: +async def test_nogroup_device_id( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Device id that do not respond to group commands (allon/alloff).""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -184,7 +194,9 @@ async def test_nogroup_device_id(hass: HomeAssistant, monkeypatch) -> None: assert hass.states.get(f"{DOMAIN}.test").state == "on" -async def test_device_defaults(hass: HomeAssistant, monkeypatch) -> None: +async def test_device_defaults( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Event should fire if device_defaults config says so.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -216,7 +228,9 @@ async def test_device_defaults(hass: HomeAssistant, monkeypatch) -> None: assert calls[0].data == {"state": "off", "entity_id": f"{DOMAIN}.test"} -async def test_not_firing_default(hass: HomeAssistant, monkeypatch) -> None: +async def test_not_firing_default( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """By default no bus events should be fired.""" config = { "rflink": {"port": "/dev/ttyABC0"}, @@ -246,7 +260,9 @@ async def test_not_firing_default(hass: HomeAssistant, monkeypatch) -> None: assert not calls, "an event has been fired" -async def test_restore_state(hass: HomeAssistant, monkeypatch) -> None: +async def test_restore_state( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: """Ensure states are restored on startup.""" config = { "rflink": {"port": "/dev/ttyABC0"}, diff --git a/tests/components/rflink/test_utils.py b/tests/components/rflink/test_utils.py index 170a05f8623..38804d14ecc 100644 --- a/tests/components/rflink/test_utils.py +++ b/tests/components/rflink/test_utils.py @@ -4,10 +4,9 @@ from homeassistant.components.rflink.utils import ( brightness_to_rflink, rflink_to_brightness, ) -from homeassistant.core import HomeAssistant -async def test_utils(hass: HomeAssistant, monkeypatch) -> None: +async def test_utils() -> None: """Test all utils methods.""" # test brightness_to_rflink assert brightness_to_rflink(0) == 0 From eb5a98e7ea86647e9e1f1140ab8cf0fb478d5e6d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Jul 2024 10:57:17 +0200 Subject: [PATCH 0796/2411] Use fixtures in deCONZ light tests PT2 (#121208) --- tests/components/deconz/conftest.py | 8 + tests/components/deconz/test_light.py | 312 +++++++++++++------------- 2 files changed, 160 insertions(+), 160 deletions(-) diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py index 30bee23cbd2..bcd63af3d5e 100644 --- a/tests/components/deconz/conftest.py +++ b/tests/components/deconz/conftest.py @@ -162,6 +162,14 @@ def fixture_group_data() -> dict[str, Any]: @pytest.fixture(name="light_payload") +def fixture_light_0_data(light_0_payload: dict[str, Any]) -> dict[str, Any]: + """Light data.""" + if light_0_payload: + return {"0": light_0_payload} + return {} + + +@pytest.fixture(name="light_0_payload") def fixture_light_data() -> dict[str, Any]: """Light data.""" return {} diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 411a0552bd1..ad67fd9d2c1 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -2,7 +2,6 @@ from collections.abc import Callable from typing import Any -from unittest.mock import patch import pytest @@ -41,25 +40,11 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from .test_gateway import ( - DECONZ_WEB_REQUEST, - mock_deconz_put_request, - setup_deconz_integration, -) - from tests.test_util.aiohttp import AiohttpClientMocker -async def test_no_lights_or_groups( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that no lights or groups entities are created.""" - await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 0 - - @pytest.mark.parametrize( - ("input", "expected"), + ("light_0_payload", "expected"), [ ( # RGB light in color temp color mode { @@ -426,13 +411,11 @@ async def test_no_lights_or_groups( ], ) async def test_lights( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, input, expected + hass: HomeAssistant, + config_entry_setup: ConfigEntry, + expected: dict[str, Any], ) -> None: """Test that different light entities are created with expected values.""" - data = {"lights": {"0": input}} - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 1 light = hass.states.get(expected["entity_id"]) @@ -440,23 +423,21 @@ async def test_lights( for attribute, expected_value in expected["attributes"].items(): assert light.attributes[attribute] == expected_value - await hass.config_entries.async_unload(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry_setup.entry_id) states = hass.states.async_all() for state in states: assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_remove(config_entry.entry_id) + await hass.config_entries.async_remove(config_entry_setup.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 -async def test_light_state_change( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket -) -> None: - """Verify light can change state on websocket event.""" - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "0": { "colorcapabilities": 31, "ctmax": 500, @@ -485,10 +466,11 @@ async def test_light_state_change( "uniqueid": "00:17:88:01:01:23:45:67-00", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_light_state_change(hass: HomeAssistant, mock_deconz_websocket) -> None: + """Verify light can change state on websocket event.""" assert hass.states.get("light.hue_go").state == STATE_ON event_changed_light = { @@ -642,44 +624,47 @@ async def test_light_state_change( ], ) async def test_light_service_calls( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, input, expected + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + config_entry_factory: Callable[[], ConfigEntry], + light_payload: dict[str, Any], + mock_put_request: Callable[[str, str], AiohttpClientMocker], + input: dict[str, Any], + expected: dict[str, Any], ) -> None: """Verify light can change state on websocket event.""" - data = { - "lights": { - "0": { - "colorcapabilities": 31, - "ctmax": 500, - "ctmin": 153, - "etag": "055485a82553e654f156d41c9301b7cf", - "hascolor": True, - "lastannounced": None, - "lastseen": "2021-06-10T20:25Z", - "manufacturername": "Philips", - "modelid": "LLC020", - "name": "Hue Go", - "state": { - "alert": "none", - "bri": 254, - "colormode": "ct", - "ct": 375, - "effect": "none", - "hue": 8348, - "on": input["light_on"], - "reachable": True, - "sat": 147, - "xy": [0.462, 0.4111], - }, - "swversion": "5.127.1.26420", - "type": "Extended color light", - "uniqueid": "00:17:88:01:01:23:45:67-00", - } + light_payload |= { + "0": { + "colorcapabilities": 31, + "ctmax": 500, + "ctmin": 153, + "etag": "055485a82553e654f156d41c9301b7cf", + "hascolor": True, + "lastannounced": None, + "lastseen": "2021-06-10T20:25Z", + "manufacturername": "Philips", + "modelid": "LLC020", + "name": "Hue Go", + "state": { + "alert": "none", + "bri": 254, + "colormode": "ct", + "ct": 375, + "effect": "none", + "hue": 8348, + "on": input["light_on"], + "reachable": True, + "sat": 147, + "xy": [0.462, 0.4111], + }, + "swversion": "5.127.1.26420", + "type": "Extended color light", + "uniqueid": "00:17:88:01:01:23:45:67-00", } } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) + await config_entry_factory() - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state") + aioclient_mock = mock_put_request("/lights/0/state") await hass.services.async_call( LIGHT_DOMAIN, @@ -693,12 +678,10 @@ async def test_light_service_calls( assert len(aioclient_mock.mock_calls) == 1 # not called -async def test_ikea_default_transition_time( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Verify that service calls to IKEA lights always extend with transition tinme 0 if absent.""" - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "0": { "colorcapabilities": 0, "ctmax": 65535, @@ -721,13 +704,17 @@ async def test_ikea_default_transition_time( "swversion": "2.0.022", "type": "Color temperature light", "uniqueid": "ec:1b:bd:ff:fe:ee:ed:dd-01", - }, - }, - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state") + } + } + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_ikea_default_transition_time( + hass: HomeAssistant, + mock_put_request: Callable[[str, str], AiohttpClientMocker], +) -> None: + """Verify that service calls to IKEA lights always extend with transition tinme 0 if absent.""" + aioclient_mock = mock_put_request("/lights/0/state") await hass.services.async_call( LIGHT_DOMAIN, @@ -761,12 +748,10 @@ async def test_ikea_default_transition_time( } -async def test_lidl_christmas_light( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that lights or groups entities are created.""" - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "0": { "etag": "87a89542bf9b9d0aa8134919056844f8", "hascolor": True, @@ -789,12 +774,15 @@ async def test_lidl_christmas_light( "uniqueid": "58:8e:81:ff:fe:db:7b:be-01", } } - } - - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state") + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_lidl_christmas_light( + hass: HomeAssistant, + mock_put_request: Callable[[str, str], AiohttpClientMocker], +) -> None: + """Test that lights or groups entities are created.""" + aioclient_mock = mock_put_request("/lights/0/state") await hass.services.async_call( LIGHT_DOMAIN, @@ -806,16 +794,13 @@ async def test_lidl_christmas_light( blocking=True, ) assert aioclient_mock.mock_calls[1][2] == {"on": True, "hue": 3640, "sat": 76} - assert hass.states.get("light.lidl_xmas_light") -async def test_configuration_tool( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Verify that configuration tool is not created.""" - data = { - "lights": { +@pytest.mark.parametrize( + "light_payload", + [ + { "0": { "etag": "26839cb118f5bf7ba1f2108256644010", "hascolor": False, @@ -830,13 +815,53 @@ async def test_configuration_tool( "uniqueid": "00:21:2e:ff:ff:05:a7:a3-01", } } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_configuration_tool(hass: HomeAssistant) -> None: + """Verify that configuration tool is not created.""" assert len(hass.states.async_all()) == 0 +@pytest.mark.parametrize( + "light_payload", + [ + { + "1": { + "name": "RGB light", + "state": { + "on": True, + "bri": 50, + "colormode": "xy", + "effect": "colorloop", + "xy": (0.5, 0.5), + "reachable": True, + }, + "type": "Extended color light", + "uniqueid": "00:00:00:00:00:00:00:00-00", + }, + "2": { + "ctmax": 454, + "ctmin": 155, + "name": "Tunable white light", + "state": { + "on": True, + "colormode": "ct", + "ct": 2500, + "reachable": True, + }, + "type": "Tunable white light", + "uniqueid": "00:00:00:00:00:00:00:01-00", + }, + "3": { + "name": "Dimmable light", + "type": "Dimmable light", + "state": {"bri": 255, "on": True, "reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, + } + ], +) @pytest.mark.parametrize( ("input", "expected"), [ @@ -908,69 +933,36 @@ async def test_configuration_tool( ], ) async def test_groups( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, input, expected + hass: HomeAssistant, + config_entry_factory: Callable[[], ConfigEntry], + group_payload: dict[str, Any], + input: dict[str, list[str]], + expected: dict[str, Any], ) -> None: """Test that different group entities are created with expected values.""" - data = { - "groups": { - "0": { - "id": "Light group id", - "name": "Group", - "type": "LightGroup", - "state": {"all_on": False, "any_on": True}, - "action": { - "alert": "none", - "bri": 127, - "colormode": "hs", - "ct": 0, - "effect": "none", - "hue": 0, - "on": True, - "sat": 127, - "scene": None, - "xy": [0, 0], - }, - "scenes": [], - "lights": input["lights"], - }, - }, - "lights": { - "1": { - "name": "RGB light", - "state": { - "on": True, - "bri": 50, - "colormode": "xy", - "effect": "colorloop", - "xy": (0.5, 0.5), - "reachable": True, - }, - "type": "Extended color light", - "uniqueid": "00:00:00:00:00:00:00:00-00", - }, - "2": { - "ctmax": 454, - "ctmin": 155, - "name": "Tunable white light", - "state": { - "on": True, - "colormode": "ct", - "ct": 2500, - "reachable": True, - }, - "type": "Tunable white light", - "uniqueid": "00:00:00:00:00:00:00:01-00", - }, - "3": { - "name": "Dimmable light", - "type": "Dimmable light", - "state": {"bri": 255, "on": True, "reachable": True}, - "uniqueid": "00:00:00:00:00:00:00:02-00", + group_payload |= { + "0": { + "id": "Light group id", + "name": "Group", + "type": "LightGroup", + "state": {"all_on": False, "any_on": True}, + "action": { + "alert": "none", + "bri": 127, + "colormode": "hs", + "ct": 0, + "effect": "none", + "hue": 0, + "on": True, + "sat": 127, + "scene": None, + "xy": [0, 0], }, + "scenes": [], + "lights": input["lights"], }, } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) + config_entry = await config_entry_factory() assert len(hass.states.async_all()) == 4 From afb184db730abf510815ed796141b62942dedd8d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 5 Jul 2024 11:03:31 +0200 Subject: [PATCH 0797/2411] Remove coveragerc (#121286) --- .core_files.yaml | 1 - .coveragerc | 1730 ------------------------------ .github/PULL_REQUEST_TEMPLATE.md | 1 - .pre-commit-config.yaml | 2 +- codecov.yml | 2 +- pyproject.toml | 17 + script/hassfest/__main__.py | 2 - script/hassfest/coverage.py | 181 ---- 8 files changed, 19 insertions(+), 1917 deletions(-) delete mode 100644 .coveragerc delete mode 100644 script/hassfest/coverage.py diff --git a/.core_files.yaml b/.core_files.yaml index a6f856209cc..067a6a2b41d 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -116,7 +116,6 @@ components: &components # Testing related files that affect the whole test/linting suite tests: &tests - - .coveragerc - codecov.yaml - pylint/** - requirements_test_pre_commit.txt diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index a548ba81f6f..00000000000 --- a/.coveragerc +++ /dev/null @@ -1,1730 +0,0 @@ -# Sorted by hassfest. -# -# To sort, run python3 -m script.hassfest -p coverage - -[run] -source = homeassistant -omit = - homeassistant/__main__.py - homeassistant/helpers/backports/aiohttp_resolver.py - homeassistant/helpers/signal.py - homeassistant/scripts/__init__.py - homeassistant/scripts/benchmark/__init__.py - homeassistant/scripts/check_config.py - homeassistant/scripts/ensure_config.py - homeassistant/scripts/macos/__init__.py - - # omit pieces of code that rely on external devices being present - homeassistant/components/acer_projector/* - homeassistant/components/acmeda/__init__.py - homeassistant/components/acmeda/base.py - homeassistant/components/acmeda/cover.py - homeassistant/components/acmeda/errors.py - homeassistant/components/acmeda/helpers.py - homeassistant/components/acmeda/hub.py - homeassistant/components/acmeda/sensor.py - homeassistant/components/actiontec/const.py - homeassistant/components/actiontec/device_tracker.py - homeassistant/components/actiontec/model.py - homeassistant/components/adax/__init__.py - homeassistant/components/adax/climate.py - homeassistant/components/adguard/__init__.py - homeassistant/components/adguard/entity.py - homeassistant/components/adguard/sensor.py - homeassistant/components/adguard/switch.py - homeassistant/components/ads/* - homeassistant/components/aftership/__init__.py - homeassistant/components/aftership/sensor.py - homeassistant/components/agent_dvr/alarm_control_panel.py - homeassistant/components/agent_dvr/camera.py - homeassistant/components/agent_dvr/helpers.py - homeassistant/components/airnow/__init__.py - homeassistant/components/airnow/coordinator.py - homeassistant/components/airnow/sensor.py - homeassistant/components/airq/__init__.py - homeassistant/components/airq/coordinator.py - homeassistant/components/airq/sensor.py - homeassistant/components/airthings/__init__.py - homeassistant/components/airthings/sensor.py - homeassistant/components/airthings_ble/__init__.py - homeassistant/components/airthings_ble/sensor.py - homeassistant/components/airtouch4/__init__.py - homeassistant/components/airtouch4/climate.py - homeassistant/components/airtouch4/coordinator.py - homeassistant/components/airtouch5/__init__.py - homeassistant/components/airtouch5/climate.py - homeassistant/components/airtouch5/entity.py - homeassistant/components/airvisual/__init__.py - homeassistant/components/airvisual/sensor.py - homeassistant/components/airvisual_pro/__init__.py - homeassistant/components/airvisual_pro/sensor.py - homeassistant/components/alarmdecoder/__init__.py - homeassistant/components/alarmdecoder/alarm_control_panel.py - homeassistant/components/alarmdecoder/binary_sensor.py - homeassistant/components/alarmdecoder/entity.py - homeassistant/components/alarmdecoder/sensor.py - homeassistant/components/alpha_vantage/sensor.py - homeassistant/components/amazon_polly/* - homeassistant/components/ambient_station/__init__.py - homeassistant/components/ambient_station/binary_sensor.py - homeassistant/components/ambient_station/entity.py - homeassistant/components/ambient_station/sensor.py - homeassistant/components/amcrest/* - homeassistant/components/ampio/* - homeassistant/components/android_ip_webcam/switch.py - homeassistant/components/anel_pwrctrl/switch.py - homeassistant/components/anthemav/media_player.py - homeassistant/components/apple_tv/__init__.py - homeassistant/components/apple_tv/browse_media.py - homeassistant/components/apple_tv/media_player.py - homeassistant/components/apple_tv/remote.py - homeassistant/components/aprilaire/__init__.py - homeassistant/components/aprilaire/climate.py - homeassistant/components/aprilaire/coordinator.py - homeassistant/components/aprilaire/entity.py - homeassistant/components/aprilaire/humidifier.py - homeassistant/components/aprilaire/select.py - homeassistant/components/aprilaire/sensor.py - homeassistant/components/apsystems/__init__.py - homeassistant/components/apsystems/coordinator.py - homeassistant/components/apsystems/entity.py - homeassistant/components/apsystems/number.py - homeassistant/components/apsystems/sensor.py - homeassistant/components/aqualogic/* - homeassistant/components/aquostv/media_player.py - homeassistant/components/arcam_fmj/__init__.py - homeassistant/components/arcam_fmj/media_player.py - homeassistant/components/arest/binary_sensor.py - homeassistant/components/arest/sensor.py - homeassistant/components/arest/switch.py - homeassistant/components/arris_tg2492lg/* - homeassistant/components/aruba/device_tracker.py - homeassistant/components/arwn/sensor.py - homeassistant/components/aseko_pool_live/__init__.py - homeassistant/components/aseko_pool_live/binary_sensor.py - homeassistant/components/aseko_pool_live/coordinator.py - homeassistant/components/aseko_pool_live/entity.py - homeassistant/components/aseko_pool_live/sensor.py - homeassistant/components/asterisk_cdr/mailbox.py - homeassistant/components/asterisk_mbox/mailbox.py - homeassistant/components/aten_pe/* - homeassistant/components/atome/* - homeassistant/components/aurora/__init__.py - homeassistant/components/aurora/binary_sensor.py - homeassistant/components/aurora/coordinator.py - homeassistant/components/aurora/entity.py - homeassistant/components/aurora/sensor.py - homeassistant/components/avea/light.py - homeassistant/components/avion/light.py - homeassistant/components/awair/coordinator.py - homeassistant/components/azure_service_bus/* - homeassistant/components/baf/__init__.py - homeassistant/components/baf/binary_sensor.py - homeassistant/components/baf/climate.py - homeassistant/components/baf/entity.py - homeassistant/components/baf/fan.py - homeassistant/components/baf/light.py - homeassistant/components/baf/number.py - homeassistant/components/baf/sensor.py - homeassistant/components/baf/switch.py - homeassistant/components/baidu/tts.py - homeassistant/components/bang_olufsen/entity.py - homeassistant/components/bang_olufsen/media_player.py - homeassistant/components/bang_olufsen/util.py - homeassistant/components/bang_olufsen/websocket.py - homeassistant/components/bbox/device_tracker.py - homeassistant/components/bbox/sensor.py - homeassistant/components/beewi_smartclim/sensor.py - homeassistant/components/bitcoin/sensor.py - homeassistant/components/bizkaibus/sensor.py - homeassistant/components/blink/__init__.py - homeassistant/components/blink/alarm_control_panel.py - homeassistant/components/blink/binary_sensor.py - homeassistant/components/blink/camera.py - homeassistant/components/blink/sensor.py - homeassistant/components/blink/switch.py - homeassistant/components/blinksticklight/light.py - homeassistant/components/blockchain/sensor.py - homeassistant/components/bloomsky/* - homeassistant/components/bluesound/* - homeassistant/components/bluetooth_tracker/* - homeassistant/components/bmw_connected_drive/notify.py - homeassistant/components/bosch_shc/__init__.py - homeassistant/components/bosch_shc/binary_sensor.py - homeassistant/components/bosch_shc/cover.py - homeassistant/components/bosch_shc/entity.py - homeassistant/components/bosch_shc/sensor.py - homeassistant/components/bosch_shc/switch.py - homeassistant/components/braviatv/button.py - homeassistant/components/braviatv/coordinator.py - homeassistant/components/braviatv/media_player.py - homeassistant/components/braviatv/remote.py - homeassistant/components/bring/coordinator.py - homeassistant/components/bring/todo.py - homeassistant/components/broadlink/climate.py - homeassistant/components/broadlink/light.py - homeassistant/components/broadlink/remote.py - homeassistant/components/broadlink/switch.py - homeassistant/components/broadlink/updater.py - homeassistant/components/brottsplatskartan/sensor.py - homeassistant/components/browser/* - homeassistant/components/brunt/__init__.py - homeassistant/components/brunt/cover.py - homeassistant/components/bsblan/climate.py - homeassistant/components/bt_home_hub_5/device_tracker.py - homeassistant/components/bt_smarthub/device_tracker.py - homeassistant/components/buienradar/sensor.py - homeassistant/components/buienradar/util.py - homeassistant/components/buienradar/weather.py - homeassistant/components/canary/camera.py - homeassistant/components/cert_expiry/helper.py - homeassistant/components/channels/* - homeassistant/components/cisco_ios/device_tracker.py - homeassistant/components/cisco_mobility_express/device_tracker.py - homeassistant/components/cisco_webex_teams/notify.py - homeassistant/components/citybikes/sensor.py - homeassistant/components/clementine/media_player.py - homeassistant/components/clickatell/notify.py - homeassistant/components/clicksend/notify.py - homeassistant/components/clicksend_tts/notify.py - homeassistant/components/cmus/media_player.py - homeassistant/components/coinbase/sensor.py - homeassistant/components/comed_hourly_pricing/sensor.py - homeassistant/components/comelit/__init__.py - homeassistant/components/comelit/alarm_control_panel.py - homeassistant/components/comelit/climate.py - homeassistant/components/comelit/coordinator.py - homeassistant/components/comelit/cover.py - homeassistant/components/comelit/humidifier.py - homeassistant/components/comelit/light.py - homeassistant/components/comelit/sensor.py - homeassistant/components/comelit/switch.py - homeassistant/components/comfoconnect/fan.py - homeassistant/components/concord232/alarm_control_panel.py - homeassistant/components/concord232/binary_sensor.py - homeassistant/components/control4/__init__.py - homeassistant/components/control4/director_utils.py - homeassistant/components/control4/light.py - homeassistant/components/control4/media_player.py - homeassistant/components/coolmaster/coordinator.py - homeassistant/components/cppm_tracker/device_tracker.py - homeassistant/components/crownstone/__init__.py - homeassistant/components/crownstone/devices.py - homeassistant/components/crownstone/entry_manager.py - homeassistant/components/crownstone/helpers.py - homeassistant/components/crownstone/light.py - homeassistant/components/crownstone/listeners.py - homeassistant/components/cups/sensor.py - homeassistant/components/currencylayer/sensor.py - homeassistant/components/daikin/climate.py - homeassistant/components/daikin/sensor.py - homeassistant/components/daikin/switch.py - homeassistant/components/danfoss_air/* - homeassistant/components/ddwrt/device_tracker.py - homeassistant/components/decora/light.py - homeassistant/components/decora_wifi/light.py - homeassistant/components/delijn/* - homeassistant/components/deluge/__init__.py - homeassistant/components/deluge/coordinator.py - homeassistant/components/deluge/sensor.py - homeassistant/components/deluge/switch.py - homeassistant/components/denon/media_player.py - homeassistant/components/denonavr/__init__.py - homeassistant/components/denonavr/media_player.py - homeassistant/components/denonavr/receiver.py - homeassistant/components/digital_ocean/* - homeassistant/components/discogs/sensor.py - homeassistant/components/discord/__init__.py - homeassistant/components/discord/notify.py - homeassistant/components/dlib_face_detect/image_processing.py - homeassistant/components/dlib_face_identify/image_processing.py - homeassistant/components/dlink/data.py - homeassistant/components/dominos/* - homeassistant/components/doods/* - homeassistant/components/doorbird/__init__.py - homeassistant/components/doorbird/button.py - homeassistant/components/doorbird/camera.py - homeassistant/components/doorbird/device.py - homeassistant/components/doorbird/entity.py - homeassistant/components/doorbird/event.py - homeassistant/components/doorbird/util.py - homeassistant/components/doorbird/view.py - homeassistant/components/dormakaba_dkey/__init__.py - homeassistant/components/dormakaba_dkey/binary_sensor.py - homeassistant/components/dormakaba_dkey/entity.py - homeassistant/components/dormakaba_dkey/lock.py - homeassistant/components/dormakaba_dkey/sensor.py - homeassistant/components/dovado/* - homeassistant/components/downloader/__init__.py - homeassistant/components/dte_energy_bridge/sensor.py - homeassistant/components/dublin_bus_transport/sensor.py - homeassistant/components/dunehd/__init__.py - homeassistant/components/dunehd/media_player.py - homeassistant/components/duotecno/__init__.py - homeassistant/components/duotecno/binary_sensor.py - homeassistant/components/duotecno/climate.py - homeassistant/components/duotecno/cover.py - homeassistant/components/duotecno/entity.py - homeassistant/components/duotecno/light.py - homeassistant/components/duotecno/switch.py - homeassistant/components/dwd_weather_warnings/coordinator.py - homeassistant/components/dwd_weather_warnings/sensor.py - homeassistant/components/dweet/* - homeassistant/components/ebox/sensor.py - homeassistant/components/ebusd/* - homeassistant/components/ecoal_boiler/* - homeassistant/components/ecobee/__init__.py - homeassistant/components/ecobee/binary_sensor.py - homeassistant/components/ecobee/climate.py - homeassistant/components/ecobee/notify.py - homeassistant/components/ecobee/sensor.py - homeassistant/components/ecobee/weather.py - homeassistant/components/ecoforest/__init__.py - homeassistant/components/ecoforest/coordinator.py - homeassistant/components/ecoforest/entity.py - homeassistant/components/ecoforest/number.py - homeassistant/components/ecoforest/sensor.py - homeassistant/components/ecoforest/switch.py - homeassistant/components/econet/__init__.py - homeassistant/components/econet/binary_sensor.py - homeassistant/components/econet/climate.py - homeassistant/components/econet/sensor.py - homeassistant/components/econet/water_heater.py - homeassistant/components/ecovacs/controller.py - homeassistant/components/ecovacs/entity.py - homeassistant/components/ecovacs/image.py - homeassistant/components/ecovacs/number.py - homeassistant/components/ecovacs/util.py - homeassistant/components/ecovacs/vacuum.py - homeassistant/components/ecowitt/__init__.py - homeassistant/components/ecowitt/binary_sensor.py - homeassistant/components/ecowitt/entity.py - homeassistant/components/ecowitt/sensor.py - homeassistant/components/eddystone_temperature/sensor.py - homeassistant/components/edimax/switch.py - homeassistant/components/edl21/__init__.py - homeassistant/components/edl21/sensor.py - homeassistant/components/egardia/* - homeassistant/components/electrasmart/__init__.py - homeassistant/components/electrasmart/climate.py - homeassistant/components/electric_kiwi/__init__.py - homeassistant/components/electric_kiwi/api.py - homeassistant/components/electric_kiwi/coordinator.py - homeassistant/components/electric_kiwi/oauth2.py - homeassistant/components/electric_kiwi/select.py - 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/light.py - homeassistant/components/elkm1/sensor.py - homeassistant/components/elkm1/switch.py - homeassistant/components/elmax/__init__.py - homeassistant/components/elmax/alarm_control_panel.py - homeassistant/components/elmax/binary_sensor.py - homeassistant/components/elmax/coordinator.py - homeassistant/components/elmax/cover.py - homeassistant/components/elmax/switch.py - homeassistant/components/elv/* - homeassistant/components/elvia/__init__.py - homeassistant/components/elvia/importer.py - homeassistant/components/emby/media_player.py - homeassistant/components/emoncms/sensor.py - homeassistant/components/emoncms_history/* - homeassistant/components/emonitor/__init__.py - homeassistant/components/emonitor/sensor.py - homeassistant/components/enigma2/media_player.py - homeassistant/components/enocean/__init__.py - homeassistant/components/enocean/binary_sensor.py - homeassistant/components/enocean/device.py - homeassistant/components/enocean/dongle.py - homeassistant/components/enocean/light.py - homeassistant/components/enocean/sensor.py - homeassistant/components/enocean/switch.py - homeassistant/components/enphase_envoy/__init__.py - homeassistant/components/enphase_envoy/binary_sensor.py - homeassistant/components/enphase_envoy/coordinator.py - homeassistant/components/enphase_envoy/entity.py - homeassistant/components/enphase_envoy/number.py - homeassistant/components/enphase_envoy/select.py - homeassistant/components/enphase_envoy/sensor.py - homeassistant/components/enphase_envoy/switch.py - homeassistant/components/entur_public_transport/* - homeassistant/components/environment_canada/__init__.py - homeassistant/components/environment_canada/camera.py - homeassistant/components/environment_canada/sensor.py - homeassistant/components/environment_canada/weather.py - homeassistant/components/envisalink/* - homeassistant/components/ephember/climate.py - homeassistant/components/epic_games_store/__init__.py - homeassistant/components/epic_games_store/coordinator.py - homeassistant/components/epion/__init__.py - homeassistant/components/epion/coordinator.py - homeassistant/components/epion/sensor.py - homeassistant/components/epson/__init__.py - homeassistant/components/epson/media_player.py - homeassistant/components/eq3btsmart/__init__.py - homeassistant/components/eq3btsmart/climate.py - homeassistant/components/eq3btsmart/entity.py - homeassistant/components/escea/__init__.py - homeassistant/components/escea/climate.py - homeassistant/components/escea/discovery.py - homeassistant/components/etherscan/sensor.py - homeassistant/components/eufy/* - homeassistant/components/eufylife_ble/__init__.py - homeassistant/components/eufylife_ble/sensor.py - homeassistant/components/everlights/light.py - homeassistant/components/evohome/* - homeassistant/components/ezviz/__init__.py - homeassistant/components/ezviz/alarm_control_panel.py - homeassistant/components/ezviz/binary_sensor.py - homeassistant/components/ezviz/button.py - homeassistant/components/ezviz/camera.py - homeassistant/components/ezviz/coordinator.py - homeassistant/components/ezviz/entity.py - homeassistant/components/ezviz/image.py - homeassistant/components/ezviz/light.py - homeassistant/components/ezviz/number.py - homeassistant/components/ezviz/select.py - homeassistant/components/ezviz/sensor.py - homeassistant/components/ezviz/siren.py - homeassistant/components/ezviz/switch.py - homeassistant/components/ezviz/update.py - homeassistant/components/faa_delays/__init__.py - homeassistant/components/faa_delays/binary_sensor.py - homeassistant/components/faa_delays/coordinator.py - homeassistant/components/familyhub/camera.py - homeassistant/components/ffmpeg/camera.py - homeassistant/components/fibaro/__init__.py - homeassistant/components/fibaro/binary_sensor.py - homeassistant/components/fibaro/climate.py - homeassistant/components/fibaro/cover.py - homeassistant/components/fibaro/event.py - homeassistant/components/fibaro/light.py - homeassistant/components/fibaro/lock.py - homeassistant/components/fibaro/sensor.py - homeassistant/components/fibaro/switch.py - homeassistant/components/fints/sensor.py - homeassistant/components/fireservicerota/__init__.py - homeassistant/components/fireservicerota/binary_sensor.py - homeassistant/components/fireservicerota/sensor.py - homeassistant/components/fireservicerota/switch.py - homeassistant/components/firmata/__init__.py - homeassistant/components/firmata/binary_sensor.py - homeassistant/components/firmata/board.py - homeassistant/components/firmata/entity.py - homeassistant/components/firmata/light.py - homeassistant/components/firmata/pin.py - homeassistant/components/firmata/sensor.py - homeassistant/components/firmata/switch.py - homeassistant/components/fivem/__init__.py - homeassistant/components/fivem/binary_sensor.py - homeassistant/components/fivem/coordinator.py - homeassistant/components/fivem/entity.py - homeassistant/components/fivem/sensor.py - homeassistant/components/fixer/sensor.py - homeassistant/components/fjaraskupan/__init__.py - homeassistant/components/fjaraskupan/binary_sensor.py - homeassistant/components/fjaraskupan/coordinator.py - homeassistant/components/fjaraskupan/fan.py - homeassistant/components/fjaraskupan/light.py - homeassistant/components/fjaraskupan/number.py - homeassistant/components/fjaraskupan/sensor.py - homeassistant/components/fleetgo/device_tracker.py - homeassistant/components/flexit/climate.py - homeassistant/components/flexit_bacnet/climate.py - homeassistant/components/flic/binary_sensor.py - homeassistant/components/flick_electric/__init__.py - homeassistant/components/flick_electric/sensor.py - homeassistant/components/flock/notify.py - homeassistant/components/flume/binary_sensor.py - homeassistant/components/flume/coordinator.py - homeassistant/components/flume/entity.py - homeassistant/components/flume/sensor.py - homeassistant/components/flume/util.py - homeassistant/components/folder_watcher/__init__.py - homeassistant/components/foobot/sensor.py - homeassistant/components/fortios/device_tracker.py - homeassistant/components/foscam/__init__.py - homeassistant/components/foscam/camera.py - homeassistant/components/foscam/coordinator.py - homeassistant/components/foscam/entity.py - homeassistant/components/foursquare/* - homeassistant/components/free_mobile/notify.py - homeassistant/components/freebox/camera.py - homeassistant/components/freebox/home_base.py - homeassistant/components/freebox/switch.py - homeassistant/components/fritz/coordinator.py - homeassistant/components/fritz/entity.py - homeassistant/components/fritz/services.py - homeassistant/components/fritz/switch.py - homeassistant/components/fritzbox_callmonitor/__init__.py - homeassistant/components/fritzbox_callmonitor/base.py - homeassistant/components/fritzbox_callmonitor/sensor.py - homeassistant/components/frontier_silicon/__init__.py - homeassistant/components/frontier_silicon/browse_media.py - homeassistant/components/frontier_silicon/media_player.py - homeassistant/components/futurenow/light.py - homeassistant/components/garadget/cover.py - homeassistant/components/garages_amsterdam/__init__.py - homeassistant/components/garages_amsterdam/binary_sensor.py - homeassistant/components/garages_amsterdam/entity.py - homeassistant/components/garages_amsterdam/sensor.py - homeassistant/components/gc100/* - homeassistant/components/geniushub/* - homeassistant/components/geocaching/__init__.py - homeassistant/components/geocaching/coordinator.py - homeassistant/components/geocaching/oauth.py - homeassistant/components/geocaching/sensor.py - homeassistant/components/github/coordinator.py - homeassistant/components/gitlab_ci/sensor.py - homeassistant/components/gitter/sensor.py - homeassistant/components/glances/sensor.py - homeassistant/components/goodwe/__init__.py - homeassistant/components/goodwe/button.py - homeassistant/components/goodwe/coordinator.py - homeassistant/components/goodwe/number.py - homeassistant/components/goodwe/select.py - homeassistant/components/goodwe/sensor.py - homeassistant/components/google_cloud/tts.py - homeassistant/components/google_maps/device_tracker.py - homeassistant/components/gpsd/__init__.py - homeassistant/components/gpsd/sensor.py - homeassistant/components/greenwave/light.py - homeassistant/components/growatt_server/__init__.py - homeassistant/components/growatt_server/sensor.py - homeassistant/components/growatt_server/sensor_types/* - homeassistant/components/gstreamer/media_player.py - homeassistant/components/gtfs/sensor.py - homeassistant/components/guardian/__init__.py - homeassistant/components/guardian/binary_sensor.py - homeassistant/components/guardian/button.py - homeassistant/components/guardian/coordinator.py - homeassistant/components/guardian/sensor.py - homeassistant/components/guardian/switch.py - homeassistant/components/guardian/util.py - homeassistant/components/guardian/valve.py - homeassistant/components/habitica/__init__.py - homeassistant/components/habitica/coordinator.py - homeassistant/components/habitica/sensor.py - homeassistant/components/harman_kardon_avr/media_player.py - homeassistant/components/harmony/data.py - homeassistant/components/harmony/remote.py - homeassistant/components/harmony/util.py - homeassistant/components/haveibeenpwned/sensor.py - homeassistant/components/heatmiser/climate.py - homeassistant/components/hikvision/binary_sensor.py - homeassistant/components/hikvisioncam/switch.py - homeassistant/components/hisense_aehw4a1/__init__.py - homeassistant/components/hisense_aehw4a1/climate.py - homeassistant/components/hitron_coda/device_tracker.py - homeassistant/components/hive/__init__.py - homeassistant/components/hive/alarm_control_panel.py - homeassistant/components/hive/binary_sensor.py - homeassistant/components/hive/climate.py - homeassistant/components/hive/light.py - homeassistant/components/hive/sensor.py - homeassistant/components/hive/switch.py - homeassistant/components/hive/water_heater.py - homeassistant/components/hko/__init__.py - homeassistant/components/hko/coordinator.py - homeassistant/components/hko/weather.py - homeassistant/components/hlk_sw16/__init__.py - homeassistant/components/hlk_sw16/switch.py - homeassistant/components/home_connect/entity.py - homeassistant/components/home_connect/light.py - homeassistant/components/home_connect/switch.py - homeassistant/components/homematic/__init__.py - homeassistant/components/homematic/binary_sensor.py - homeassistant/components/homematic/climate.py - homeassistant/components/homematic/cover.py - homeassistant/components/homematic/entity.py - homeassistant/components/homematic/light.py - homeassistant/components/homematic/lock.py - homeassistant/components/homematic/notify.py - homeassistant/components/homematic/sensor.py - homeassistant/components/homematic/switch.py - homeassistant/components/horizon/media_player.py - homeassistant/components/hp_ilo/sensor.py - homeassistant/components/huawei_lte/__init__.py - homeassistant/components/huawei_lte/binary_sensor.py - homeassistant/components/huawei_lte/device_tracker.py - homeassistant/components/huawei_lte/notify.py - homeassistant/components/huawei_lte/sensor.py - homeassistant/components/huawei_lte/switch.py - homeassistant/components/hunterdouglas_powerview/__init__.py - homeassistant/components/hunterdouglas_powerview/button.py - homeassistant/components/hunterdouglas_powerview/coordinator.py - homeassistant/components/hunterdouglas_powerview/cover.py - homeassistant/components/hunterdouglas_powerview/entity.py - homeassistant/components/hunterdouglas_powerview/number.py - homeassistant/components/hunterdouglas_powerview/select.py - homeassistant/components/hunterdouglas_powerview/sensor.py - homeassistant/components/hunterdouglas_powerview/shade_data.py - homeassistant/components/hunterdouglas_powerview/util.py - homeassistant/components/huum/__init__.py - homeassistant/components/huum/climate.py - homeassistant/components/hvv_departures/__init__.py - homeassistant/components/hvv_departures/binary_sensor.py - homeassistant/components/hvv_departures/sensor.py - homeassistant/components/ialarm/alarm_control_panel.py - homeassistant/components/iammeter/const.py - homeassistant/components/iammeter/sensor.py - homeassistant/components/iaqualink/binary_sensor.py - homeassistant/components/iaqualink/climate.py - homeassistant/components/iaqualink/light.py - homeassistant/components/iaqualink/sensor.py - homeassistant/components/iaqualink/switch.py - homeassistant/components/icloud/__init__.py - homeassistant/components/icloud/account.py - homeassistant/components/icloud/device_tracker.py - homeassistant/components/icloud/sensor.py - homeassistant/components/idteck_prox/* - homeassistant/components/ifttt/__init__.py - homeassistant/components/ifttt/alarm_control_panel.py - homeassistant/components/iglo/light.py - homeassistant/components/ihc/* - homeassistant/components/incomfort/__init__.py - homeassistant/components/incomfort/climate.py - homeassistant/components/incomfort/water_heater.py - homeassistant/components/insteon/binary_sensor.py - homeassistant/components/insteon/climate.py - homeassistant/components/insteon/cover.py - homeassistant/components/insteon/fan.py - homeassistant/components/insteon/insteon_entity.py - homeassistant/components/insteon/light.py - homeassistant/components/insteon/schemas.py - homeassistant/components/insteon/switch.py - homeassistant/components/insteon/utils.py - homeassistant/components/intellifire/__init__.py - homeassistant/components/intellifire/binary_sensor.py - homeassistant/components/intellifire/climate.py - homeassistant/components/intellifire/coordinator.py - homeassistant/components/intellifire/entity.py - homeassistant/components/intellifire/fan.py - homeassistant/components/intellifire/light.py - homeassistant/components/intellifire/number.py - homeassistant/components/intellifire/sensor.py - homeassistant/components/intellifire/switch.py - homeassistant/components/intesishome/* - homeassistant/components/ios/__init__.py - homeassistant/components/ios/notify.py - homeassistant/components/ios/sensor.py - homeassistant/components/iperf3/* - homeassistant/components/iqvia/__init__.py - homeassistant/components/iqvia/sensor.py - homeassistant/components/irish_rail_transport/sensor.py - homeassistant/components/iss/__init__.py - homeassistant/components/iss/sensor.py - homeassistant/components/ista_ecotrend/coordinator.py - homeassistant/components/isy994/__init__.py - homeassistant/components/isy994/binary_sensor.py - homeassistant/components/isy994/button.py - homeassistant/components/isy994/climate.py - homeassistant/components/isy994/cover.py - homeassistant/components/isy994/entity.py - homeassistant/components/isy994/fan.py - homeassistant/components/isy994/helpers.py - homeassistant/components/isy994/light.py - homeassistant/components/isy994/lock.py - homeassistant/components/isy994/models.py - homeassistant/components/isy994/number.py - homeassistant/components/isy994/select.py - homeassistant/components/isy994/sensor.py - homeassistant/components/isy994/services.py - homeassistant/components/isy994/switch.py - homeassistant/components/isy994/util.py - homeassistant/components/itach/remote.py - homeassistant/components/itunes/media_player.py - homeassistant/components/izone/__init__.py - homeassistant/components/izone/climate.py - homeassistant/components/izone/discovery.py - homeassistant/components/joaoapps_join/* - homeassistant/components/juicenet/__init__.py - homeassistant/components/juicenet/device.py - homeassistant/components/juicenet/entity.py - homeassistant/components/juicenet/number.py - homeassistant/components/juicenet/sensor.py - homeassistant/components/juicenet/switch.py - homeassistant/components/justnimbus/coordinator.py - homeassistant/components/justnimbus/entity.py - homeassistant/components/justnimbus/sensor.py - homeassistant/components/kaiterra/* - homeassistant/components/kankun/switch.py - homeassistant/components/keba/* - homeassistant/components/keenetic_ndms2/__init__.py - homeassistant/components/keenetic_ndms2/binary_sensor.py - homeassistant/components/keenetic_ndms2/device_tracker.py - homeassistant/components/keenetic_ndms2/router.py - homeassistant/components/kef/* - homeassistant/components/keyboard/* - homeassistant/components/keyboard_remote/* - homeassistant/components/keymitt_ble/__init__.py - homeassistant/components/keymitt_ble/coordinator.py - homeassistant/components/keymitt_ble/entity.py - homeassistant/components/keymitt_ble/switch.py - homeassistant/components/kitchen_sink/weather.py - homeassistant/components/kiwi/lock.py - homeassistant/components/kodi/__init__.py - homeassistant/components/kodi/browse_media.py - homeassistant/components/kodi/media_player.py - homeassistant/components/kodi/notify.py - homeassistant/components/konnected/__init__.py - homeassistant/components/konnected/panel.py - homeassistant/components/konnected/switch.py - homeassistant/components/kostal_plenticore/__init__.py - homeassistant/components/kostal_plenticore/coordinator.py - homeassistant/components/kostal_plenticore/helper.py - homeassistant/components/kostal_plenticore/select.py - homeassistant/components/kostal_plenticore/sensor.py - homeassistant/components/kostal_plenticore/switch.py - homeassistant/components/kwb/sensor.py - homeassistant/components/lacrosse/sensor.py - homeassistant/components/lannouncer/notify.py - homeassistant/components/launch_library/__init__.py - homeassistant/components/launch_library/sensor.py - homeassistant/components/lcn/climate.py - homeassistant/components/lcn/helpers.py - homeassistant/components/lcn/services.py - homeassistant/components/ld2410_ble/__init__.py - homeassistant/components/ld2410_ble/binary_sensor.py - homeassistant/components/ld2410_ble/coordinator.py - homeassistant/components/ld2410_ble/sensor.py - homeassistant/components/led_ble/__init__.py - homeassistant/components/led_ble/light.py - homeassistant/components/lg_netcast/media_player.py - homeassistant/components/lg_soundbar/__init__.py - homeassistant/components/lg_soundbar/media_player.py - homeassistant/components/lightwave/* - homeassistant/components/limitlessled/light.py - homeassistant/components/linksys_smart/device_tracker.py - homeassistant/components/linode/* - homeassistant/components/linux_battery/sensor.py - homeassistant/components/lirc/* - homeassistant/components/livisi/__init__.py - homeassistant/components/livisi/binary_sensor.py - homeassistant/components/livisi/climate.py - homeassistant/components/livisi/coordinator.py - homeassistant/components/livisi/entity.py - homeassistant/components/livisi/switch.py - homeassistant/components/llamalab_automate/notify.py - homeassistant/components/logi_circle/__init__.py - homeassistant/components/logi_circle/camera.py - homeassistant/components/logi_circle/sensor.py - homeassistant/components/london_underground/sensor.py - homeassistant/components/lookin/__init__.py - homeassistant/components/lookin/climate.py - homeassistant/components/lookin/coordinator.py - homeassistant/components/lookin/entity.py - homeassistant/components/lookin/light.py - homeassistant/components/lookin/media_player.py - homeassistant/components/lookin/sensor.py - homeassistant/components/loqed/sensor.py - homeassistant/components/luci/device_tracker.py - homeassistant/components/lupusec/__init__.py - homeassistant/components/lupusec/alarm_control_panel.py - homeassistant/components/lupusec/binary_sensor.py - homeassistant/components/lupusec/entity.py - homeassistant/components/lupusec/switch.py - homeassistant/components/lutron/__init__.py - homeassistant/components/lutron/binary_sensor.py - homeassistant/components/lutron/cover.py - homeassistant/components/lutron/entity.py - homeassistant/components/lutron/event.py - homeassistant/components/lutron/fan.py - homeassistant/components/lutron/light.py - homeassistant/components/lutron/switch.py - homeassistant/components/lutron_caseta/__init__.py - homeassistant/components/lutron_caseta/binary_sensor.py - homeassistant/components/lutron_caseta/cover.py - homeassistant/components/lutron_caseta/fan.py - homeassistant/components/lutron_caseta/light.py - homeassistant/components/lutron_caseta/switch.py - homeassistant/components/lw12wifi/light.py - homeassistant/components/lyric/__init__.py - homeassistant/components/lyric/api.py - homeassistant/components/lyric/climate.py - homeassistant/components/lyric/sensor.py - homeassistant/components/mailgun/notify.py - homeassistant/components/mastodon/notify.py - homeassistant/components/matrix/__init__.py - homeassistant/components/matrix/notify.py - homeassistant/components/matter/__init__.py - homeassistant/components/matter/fan.py - homeassistant/components/meater/__init__.py - homeassistant/components/meater/sensor.py - homeassistant/components/medcom_ble/__init__.py - homeassistant/components/medcom_ble/sensor.py - homeassistant/components/mediaroom/media_player.py - homeassistant/components/melcloud/__init__.py - homeassistant/components/melcloud/climate.py - homeassistant/components/melcloud/sensor.py - homeassistant/components/melcloud/water_heater.py - homeassistant/components/melnor/__init__.py - homeassistant/components/message_bird/notify.py - homeassistant/components/met/weather.py - homeassistant/components/met_eireann/__init__.py - homeassistant/components/met_eireann/weather.py - homeassistant/components/meteo_france/__init__.py - homeassistant/components/meteo_france/sensor.py - homeassistant/components/meteo_france/weather.py - homeassistant/components/meteoalarm/* - homeassistant/components/meteoclimatic/__init__.py - homeassistant/components/meteoclimatic/sensor.py - homeassistant/components/meteoclimatic/weather.py - homeassistant/components/microbees/__init__.py - homeassistant/components/microbees/api.py - homeassistant/components/microbees/application_credentials.py - homeassistant/components/microbees/binary_sensor.py - homeassistant/components/microbees/button.py - homeassistant/components/microbees/climate.py - homeassistant/components/microbees/coordinator.py - homeassistant/components/microbees/cover.py - homeassistant/components/microbees/entity.py - homeassistant/components/microbees/light.py - homeassistant/components/microbees/sensor.py - homeassistant/components/microbees/switch.py - homeassistant/components/microsoft/tts.py - homeassistant/components/mikrotik/coordinator.py - homeassistant/components/mill/climate.py - homeassistant/components/mill/sensor.py - homeassistant/components/minio/minio_helper.py - homeassistant/components/mjpeg/camera.py - homeassistant/components/mjpeg/util.py - homeassistant/components/mochad/__init__.py - homeassistant/components/mochad/light.py - homeassistant/components/mochad/switch.py - homeassistant/components/modem_callerid/button.py - homeassistant/components/modem_callerid/sensor.py - homeassistant/components/moehlenhoff_alpha2/climate.py - homeassistant/components/moehlenhoff_alpha2/coordinator.py - homeassistant/components/monzo/__init__.py - homeassistant/components/monzo/api.py - homeassistant/components/motion_blinds/__init__.py - homeassistant/components/motion_blinds/coordinator.py - homeassistant/components/motion_blinds/cover.py - homeassistant/components/motion_blinds/entity.py - homeassistant/components/motion_blinds/sensor.py - homeassistant/components/motionblinds_ble/__init__.py - homeassistant/components/motionblinds_ble/button.py - homeassistant/components/motionblinds_ble/cover.py - homeassistant/components/motionblinds_ble/entity.py - homeassistant/components/motionblinds_ble/select.py - homeassistant/components/motionblinds_ble/sensor.py - homeassistant/components/motionmount/__init__.py - homeassistant/components/motionmount/binary_sensor.py - homeassistant/components/motionmount/entity.py - homeassistant/components/motionmount/number.py - homeassistant/components/motionmount/select.py - homeassistant/components/motionmount/sensor.py - homeassistant/components/mpd/media_player.py - homeassistant/components/mqtt_room/sensor.py - homeassistant/components/msteams/notify.py - homeassistant/components/mullvad/__init__.py - homeassistant/components/mullvad/binary_sensor.py - homeassistant/components/mutesync/__init__.py - homeassistant/components/mutesync/binary_sensor.py - homeassistant/components/mvglive/sensor.py - homeassistant/components/mycroft/* - homeassistant/components/mysensors/__init__.py - homeassistant/components/mysensors/climate.py - homeassistant/components/mysensors/cover.py - homeassistant/components/mysensors/gateway.py - homeassistant/components/mysensors/handler.py - homeassistant/components/mysensors/helpers.py - homeassistant/components/mysensors/light.py - homeassistant/components/mysensors/switch.py - homeassistant/components/mystrom/binary_sensor.py - homeassistant/components/mystrom/light.py - homeassistant/components/mystrom/sensor.py - homeassistant/components/mystrom/switch.py - homeassistant/components/myuplink/__init__.py - homeassistant/components/myuplink/api.py - homeassistant/components/myuplink/application_credentials.py - homeassistant/components/myuplink/coordinator.py - homeassistant/components/myuplink/entity.py - homeassistant/components/myuplink/helpers.py - homeassistant/components/myuplink/sensor.py - homeassistant/components/nad/media_player.py - homeassistant/components/nanoleaf/__init__.py - homeassistant/components/nanoleaf/button.py - homeassistant/components/nanoleaf/coordinator.py - homeassistant/components/nanoleaf/entity.py - homeassistant/components/nanoleaf/event.py - homeassistant/components/nanoleaf/light.py - homeassistant/components/neato/__init__.py - homeassistant/components/neato/api.py - homeassistant/components/neato/button.py - homeassistant/components/neato/camera.py - homeassistant/components/neato/entity.py - homeassistant/components/neato/hub.py - homeassistant/components/neato/sensor.py - homeassistant/components/neato/switch.py - homeassistant/components/neato/vacuum.py - homeassistant/components/nederlandse_spoorwegen/sensor.py - homeassistant/components/netdata/sensor.py - homeassistant/components/netgear/__init__.py - homeassistant/components/netgear/button.py - homeassistant/components/netgear/device_tracker.py - homeassistant/components/netgear/entity.py - homeassistant/components/netgear/router.py - homeassistant/components/netgear/sensor.py - homeassistant/components/netgear/switch.py - homeassistant/components/netgear/update.py - homeassistant/components/netgear_lte/__init__.py - homeassistant/components/netgear_lte/notify.py - homeassistant/components/netio/switch.py - homeassistant/components/neurio_energy/sensor.py - homeassistant/components/nexia/climate.py - homeassistant/components/nexia/entity.py - homeassistant/components/nexia/switch.py - homeassistant/components/nextcloud/__init__.py - homeassistant/components/nextcloud/binary_sensor.py - homeassistant/components/nextcloud/coordinator.py - homeassistant/components/nextcloud/entity.py - homeassistant/components/nextcloud/sensor.py - homeassistant/components/nextcloud/update.py - homeassistant/components/nfandroidtv/__init__.py - homeassistant/components/nfandroidtv/notify.py - homeassistant/components/nibe_heatpump/__init__.py - homeassistant/components/nibe_heatpump/binary_sensor.py - homeassistant/components/nibe_heatpump/select.py - homeassistant/components/nibe_heatpump/sensor.py - homeassistant/components/nibe_heatpump/switch.py - homeassistant/components/nibe_heatpump/water_heater.py - homeassistant/components/niko_home_control/light.py - homeassistant/components/nilu/air_quality.py - homeassistant/components/nissan_leaf/* - homeassistant/components/nmap_tracker/__init__.py - homeassistant/components/nmap_tracker/device_tracker.py - homeassistant/components/nmbs/sensor.py - homeassistant/components/noaa_tides/sensor.py - homeassistant/components/nobo_hub/__init__.py - homeassistant/components/nobo_hub/climate.py - homeassistant/components/nobo_hub/select.py - homeassistant/components/nobo_hub/sensor.py - homeassistant/components/norway_air/air_quality.py - homeassistant/components/notify_events/notify.py - homeassistant/components/notion/__init__.py - homeassistant/components/notion/binary_sensor.py - homeassistant/components/notion/coordinator.py - homeassistant/components/notion/sensor.py - homeassistant/components/notion/util.py - homeassistant/components/nsw_fuel_station/sensor.py - homeassistant/components/nuki/__init__.py - homeassistant/components/nuki/coordinator.py - homeassistant/components/nuki/lock.py - homeassistant/components/nx584/alarm_control_panel.py - homeassistant/components/oasa_telematics/sensor.py - homeassistant/components/obihai/__init__.py - homeassistant/components/obihai/button.py - homeassistant/components/obihai/connectivity.py - homeassistant/components/obihai/sensor.py - homeassistant/components/octoprint/__init__.py - homeassistant/components/octoprint/coordinator.py - homeassistant/components/oem/climate.py - homeassistant/components/ohmconnect/sensor.py - homeassistant/components/ombi/* - homeassistant/components/omnilogic/__init__.py - homeassistant/components/omnilogic/coordinator.py - homeassistant/components/omnilogic/sensor.py - homeassistant/components/omnilogic/switch.py - homeassistant/components/ondilo_ico/__init__.py - homeassistant/components/ondilo_ico/api.py - homeassistant/components/ondilo_ico/coordinator.py - homeassistant/components/ondilo_ico/sensor.py - homeassistant/components/onkyo/media_player.py - homeassistant/components/onvif/__init__.py - homeassistant/components/onvif/binary_sensor.py - homeassistant/components/onvif/camera.py - homeassistant/components/onvif/device.py - homeassistant/components/onvif/event.py - homeassistant/components/onvif/parsers.py - homeassistant/components/onvif/sensor.py - homeassistant/components/onvif/util.py - homeassistant/components/open_meteo/weather.py - homeassistant/components/openevse/sensor.py - homeassistant/components/openexchangerates/__init__.py - homeassistant/components/openexchangerates/coordinator.py - homeassistant/components/openexchangerates/sensor.py - homeassistant/components/opengarage/__init__.py - homeassistant/components/opengarage/binary_sensor.py - homeassistant/components/opengarage/cover.py - homeassistant/components/opengarage/entity.py - homeassistant/components/opengarage/sensor.py - homeassistant/components/openhardwaremonitor/sensor.py - homeassistant/components/openhome/__init__.py - homeassistant/components/openhome/media_player.py - homeassistant/components/opensensemap/air_quality.py - homeassistant/components/opentherm_gw/__init__.py - homeassistant/components/opentherm_gw/binary_sensor.py - homeassistant/components/opentherm_gw/climate.py - homeassistant/components/opentherm_gw/sensor.py - homeassistant/components/openuv/__init__.py - homeassistant/components/openuv/binary_sensor.py - homeassistant/components/openuv/coordinator.py - homeassistant/components/openuv/sensor.py - homeassistant/components/openweathermap/__init__.py - homeassistant/components/openweathermap/coordinator.py - homeassistant/components/openweathermap/repairs.py - homeassistant/components/openweathermap/sensor.py - homeassistant/components/openweathermap/weather.py - homeassistant/components/opnsense/__init__.py - homeassistant/components/opnsense/device_tracker.py - homeassistant/components/opower/__init__.py - homeassistant/components/opower/coordinator.py - homeassistant/components/opower/sensor.py - homeassistant/components/opple/light.py - homeassistant/components/oru/* - homeassistant/components/orvibo/switch.py - homeassistant/components/osoenergy/__init__.py - homeassistant/components/osoenergy/binary_sensor.py - homeassistant/components/osoenergy/entity.py - homeassistant/components/osoenergy/sensor.py - homeassistant/components/osoenergy/water_heater.py - homeassistant/components/osramlightify/light.py - homeassistant/components/otp/sensor.py - homeassistant/components/overkiz/__init__.py - homeassistant/components/overkiz/alarm_control_panel.py - homeassistant/components/overkiz/binary_sensor.py - homeassistant/components/overkiz/button.py - homeassistant/components/overkiz/climate.py - homeassistant/components/overkiz/climate_entities/* - homeassistant/components/overkiz/coordinator.py - homeassistant/components/overkiz/cover.py - homeassistant/components/overkiz/cover_entities/* - homeassistant/components/overkiz/entity.py - homeassistant/components/overkiz/executor.py - homeassistant/components/overkiz/light.py - homeassistant/components/overkiz/lock.py - homeassistant/components/overkiz/number.py - homeassistant/components/overkiz/select.py - homeassistant/components/overkiz/sensor.py - homeassistant/components/overkiz/siren.py - homeassistant/components/overkiz/switch.py - homeassistant/components/overkiz/water_heater.py - homeassistant/components/overkiz/water_heater_entities/* - homeassistant/components/ovo_energy/__init__.py - homeassistant/components/ovo_energy/sensor.py - homeassistant/components/panasonic_bluray/media_player.py - homeassistant/components/panasonic_viera/media_player.py - homeassistant/components/pandora/media_player.py - homeassistant/components/pencom/switch.py - homeassistant/components/permobil/__init__.py - homeassistant/components/permobil/binary_sensor.py - homeassistant/components/permobil/coordinator.py - homeassistant/components/permobil/entity.py - homeassistant/components/permobil/sensor.py - homeassistant/components/philips_js/__init__.py - homeassistant/components/philips_js/coordinator.py - homeassistant/components/philips_js/light.py - homeassistant/components/philips_js/media_player.py - homeassistant/components/philips_js/remote.py - homeassistant/components/philips_js/switch.py - homeassistant/components/pi_hole/sensor.py - homeassistant/components/picotts/tts.py - homeassistant/components/pilight/base_class.py - homeassistant/components/pilight/binary_sensor.py - homeassistant/components/pilight/light.py - homeassistant/components/pilight/switch.py - homeassistant/components/ping/__init__.py - homeassistant/components/ping/helpers.py - homeassistant/components/pioneer/media_player.py - homeassistant/components/plaato/__init__.py - homeassistant/components/plaato/binary_sensor.py - homeassistant/components/plaato/entity.py - homeassistant/components/plaato/sensor.py - homeassistant/components/plex/cast.py - homeassistant/components/plex/media_player.py - homeassistant/components/plex/view.py - homeassistant/components/plum_lightpad/light.py - homeassistant/components/pocketcasts/sensor.py - homeassistant/components/point/__init__.py - homeassistant/components/point/alarm_control_panel.py - homeassistant/components/point/binary_sensor.py - homeassistant/components/point/sensor.py - homeassistant/components/powerwall/__init__.py - homeassistant/components/progettihwsw/__init__.py - homeassistant/components/progettihwsw/binary_sensor.py - homeassistant/components/progettihwsw/switch.py - homeassistant/components/proliphix/climate.py - homeassistant/components/prowl/notify.py - homeassistant/components/proxmoxve/* - homeassistant/components/proxy/camera.py - homeassistant/components/pulseaudio_loopback/switch.py - homeassistant/components/purpleair/coordinator.py - homeassistant/components/pushbullet/api.py - homeassistant/components/pushbullet/notify.py - homeassistant/components/pushbullet/sensor.py - homeassistant/components/pushover/notify.py - homeassistant/components/pushsafer/notify.py - homeassistant/components/qbittorrent/__init__.py - homeassistant/components/qbittorrent/coordinator.py - homeassistant/components/qbittorrent/sensor.py - homeassistant/components/qnap/__init__.py - homeassistant/components/qnap/coordinator.py - homeassistant/components/qnap/sensor.py - homeassistant/components/qrcode/image_processing.py - homeassistant/components/quantum_gateway/device_tracker.py - homeassistant/components/qvr_pro/* - homeassistant/components/rabbitair/__init__.py - homeassistant/components/rabbitair/coordinator.py - homeassistant/components/rabbitair/entity.py - homeassistant/components/rabbitair/fan.py - homeassistant/components/rachio/__init__.py - homeassistant/components/rachio/binary_sensor.py - homeassistant/components/rachio/coordinator.py - homeassistant/components/rachio/device.py - homeassistant/components/rachio/entity.py - homeassistant/components/rachio/switch.py - homeassistant/components/rachio/webhooks.py - homeassistant/components/radio_browser/__init__.py - homeassistant/components/radiotherm/__init__.py - homeassistant/components/radiotherm/climate.py - homeassistant/components/radiotherm/coordinator.py - homeassistant/components/radiotherm/data.py - homeassistant/components/radiotherm/entity.py - homeassistant/components/radiotherm/switch.py - homeassistant/components/radiotherm/util.py - homeassistant/components/raincloud/* - homeassistant/components/rainmachine/__init__.py - homeassistant/components/rainmachine/binary_sensor.py - homeassistant/components/rainmachine/button.py - homeassistant/components/rainmachine/coordinator.py - homeassistant/components/rainmachine/select.py - homeassistant/components/rainmachine/sensor.py - homeassistant/components/rainmachine/switch.py - homeassistant/components/rainmachine/update.py - homeassistant/components/rainmachine/util.py - homeassistant/components/raspyrfm/* - homeassistant/components/recollect_waste/sensor.py - homeassistant/components/recorder/repack.py - homeassistant/components/recswitch/switch.py - homeassistant/components/reddit/sensor.py - homeassistant/components/refoss/__init__.py - homeassistant/components/refoss/bridge.py - homeassistant/components/refoss/coordinator.py - homeassistant/components/refoss/entity.py - homeassistant/components/refoss/sensor.py - homeassistant/components/refoss/switch.py - homeassistant/components/refoss/util.py - homeassistant/components/rejseplanen/sensor.py - homeassistant/components/remember_the_milk/__init__.py - homeassistant/components/remote_rpi_gpio/* - homeassistant/components/renson/__init__.py - homeassistant/components/renson/binary_sensor.py - homeassistant/components/renson/button.py - homeassistant/components/renson/coordinator.py - homeassistant/components/renson/entity.py - homeassistant/components/renson/fan.py - homeassistant/components/renson/number.py - homeassistant/components/renson/sensor.py - homeassistant/components/renson/switch.py - homeassistant/components/renson/time.py - homeassistant/components/reolink/binary_sensor.py - homeassistant/components/reolink/button.py - homeassistant/components/reolink/camera.py - homeassistant/components/reolink/entity.py - homeassistant/components/reolink/host.py - homeassistant/components/reolink/light.py - homeassistant/components/reolink/number.py - homeassistant/components/reolink/select.py - homeassistant/components/reolink/sensor.py - homeassistant/components/reolink/siren.py - homeassistant/components/reolink/switch.py - homeassistant/components/reolink/update.py - homeassistant/components/repetier/__init__.py - homeassistant/components/repetier/sensor.py - homeassistant/components/rest/notify.py - homeassistant/components/rest/switch.py - homeassistant/components/ridwell/__init__.py - homeassistant/components/ridwell/calendar.py - homeassistant/components/ridwell/coordinator.py - homeassistant/components/ridwell/switch.py - homeassistant/components/ring/camera.py - homeassistant/components/ripple/sensor.py - homeassistant/components/roborock/coordinator.py - homeassistant/components/rocketchat/notify.py - homeassistant/components/romy/__init__.py - homeassistant/components/romy/binary_sensor.py - homeassistant/components/romy/coordinator.py - homeassistant/components/romy/entity.py - homeassistant/components/romy/sensor.py - homeassistant/components/romy/vacuum.py - homeassistant/components/roomba/__init__.py - homeassistant/components/roomba/binary_sensor.py - homeassistant/components/roomba/braava.py - homeassistant/components/roomba/irobot_base.py - homeassistant/components/roomba/roomba.py - homeassistant/components/roomba/sensor.py - homeassistant/components/roomba/vacuum.py - homeassistant/components/roon/__init__.py - homeassistant/components/roon/event.py - homeassistant/components/roon/media_browser.py - homeassistant/components/roon/media_player.py - homeassistant/components/roon/server.py - homeassistant/components/route53/* - homeassistant/components/rpi_camera/* - homeassistant/components/rtorrent/sensor.py - homeassistant/components/russound_rio/media_player.py - homeassistant/components/russound_rnet/media_player.py - homeassistant/components/ruuvi_gateway/__init__.py - homeassistant/components/ruuvi_gateway/bluetooth.py - homeassistant/components/ruuvi_gateway/coordinator.py - homeassistant/components/rympro/__init__.py - homeassistant/components/rympro/coordinator.py - homeassistant/components/rympro/sensor.py - homeassistant/components/sabnzbd/__init__.py - homeassistant/components/sabnzbd/coordinator.py - homeassistant/components/sabnzbd/sensor.py - homeassistant/components/saj/sensor.py - homeassistant/components/satel_integra/* - homeassistant/components/schluter/* - homeassistant/components/screenlogic/binary_sensor.py - homeassistant/components/screenlogic/climate.py - homeassistant/components/screenlogic/coordinator.py - homeassistant/components/screenlogic/entity.py - homeassistant/components/screenlogic/light.py - homeassistant/components/screenlogic/number.py - homeassistant/components/screenlogic/sensor.py - homeassistant/components/screenlogic/switch.py - homeassistant/components/scsgate/* - homeassistant/components/sendgrid/notify.py - homeassistant/components/sense/__init__.py - homeassistant/components/sense/binary_sensor.py - homeassistant/components/sense/sensor.py - homeassistant/components/senz/__init__.py - homeassistant/components/senz/api.py - homeassistant/components/senz/climate.py - homeassistant/components/serial/sensor.py - homeassistant/components/serial_pm/sensor.py - homeassistant/components/sesame/lock.py - homeassistant/components/seven_segments/image_processing.py - homeassistant/components/shodan/sensor.py - homeassistant/components/sia/__init__.py - homeassistant/components/sia/alarm_control_panel.py - homeassistant/components/sia/binary_sensor.py - homeassistant/components/sia/hub.py - homeassistant/components/sia/sia_entity_base.py - homeassistant/components/sia/utils.py - homeassistant/components/simplepush/__init__.py - homeassistant/components/simplepush/notify.py - homeassistant/components/simplisafe/__init__.py - homeassistant/components/simplisafe/alarm_control_panel.py - homeassistant/components/simplisafe/binary_sensor.py - homeassistant/components/simplisafe/button.py - homeassistant/components/simplisafe/lock.py - homeassistant/components/simplisafe/sensor.py - homeassistant/components/sinch/* - homeassistant/components/sisyphus/* - homeassistant/components/sky_hub/* - homeassistant/components/skybeacon/sensor.py - homeassistant/components/skybell/__init__.py - homeassistant/components/skybell/camera.py - homeassistant/components/skybell/light.py - homeassistant/components/skybell/sensor.py - homeassistant/components/skybell/switch.py - homeassistant/components/slack/__init__.py - homeassistant/components/slack/notify.py - homeassistant/components/slack/sensor.py - homeassistant/components/slide/* - homeassistant/components/slimproto/__init__.py - homeassistant/components/slimproto/media_player.py - homeassistant/components/sma/__init__.py - homeassistant/components/sma/sensor.py - homeassistant/components/smappee/__init__.py - homeassistant/components/smappee/api.py - homeassistant/components/smappee/binary_sensor.py - homeassistant/components/smappee/sensor.py - homeassistant/components/smappee/switch.py - homeassistant/components/smarty/* - homeassistant/components/sms/__init__.py - homeassistant/components/sms/coordinator.py - homeassistant/components/sms/gateway.py - homeassistant/components/sms/notify.py - homeassistant/components/sms/sensor.py - homeassistant/components/smtp/notify.py - homeassistant/components/snapcast/__init__.py - homeassistant/components/snapcast/media_player.py - homeassistant/components/snapcast/server.py - homeassistant/components/snmp/device_tracker.py - homeassistant/components/snmp/sensor.py - homeassistant/components/snmp/switch.py - homeassistant/components/snooz/__init__.py - homeassistant/components/solaredge/__init__.py - homeassistant/components/solaredge/coordinator.py - homeassistant/components/solaredge_local/sensor.py - homeassistant/components/solax/__init__.py - homeassistant/components/solax/sensor.py - homeassistant/components/soma/__init__.py - homeassistant/components/soma/cover.py - homeassistant/components/soma/sensor.py - homeassistant/components/soma/utils.py - homeassistant/components/somfy_mylink/__init__.py - homeassistant/components/somfy_mylink/cover.py - homeassistant/components/sonos/__init__.py - homeassistant/components/sonos/alarms.py - homeassistant/components/sonos/entity.py - homeassistant/components/sonos/favorites.py - homeassistant/components/sonos/helpers.py - homeassistant/components/sonos/household_coordinator.py - homeassistant/components/sonos/media.py - homeassistant/components/sonos/media_browser.py - homeassistant/components/sonos/media_player.py - homeassistant/components/sonos/speaker.py - homeassistant/components/sonos/switch.py - homeassistant/components/sony_projector/switch.py - homeassistant/components/spc/__init__.py - homeassistant/components/spc/alarm_control_panel.py - homeassistant/components/spc/binary_sensor.py - homeassistant/components/spider/__init__.py - homeassistant/components/spider/climate.py - homeassistant/components/spider/sensor.py - homeassistant/components/spider/switch.py - homeassistant/components/splunk/* - homeassistant/components/spotify/__init__.py - homeassistant/components/spotify/browse_media.py - homeassistant/components/spotify/media_player.py - homeassistant/components/spotify/system_health.py - homeassistant/components/spotify/util.py - homeassistant/components/squeezebox/__init__.py - homeassistant/components/squeezebox/browse_media.py - homeassistant/components/squeezebox/media_player.py - homeassistant/components/starline/__init__.py - homeassistant/components/starline/account.py - homeassistant/components/starline/binary_sensor.py - homeassistant/components/starline/button.py - homeassistant/components/starline/device_tracker.py - homeassistant/components/starline/entity.py - homeassistant/components/starline/lock.py - homeassistant/components/starline/sensor.py - homeassistant/components/starline/switch.py - homeassistant/components/starlingbank/sensor.py - homeassistant/components/starlink/__init__.py - homeassistant/components/starlink/binary_sensor.py - homeassistant/components/starlink/button.py - homeassistant/components/starlink/coordinator.py - homeassistant/components/starlink/device_tracker.py - homeassistant/components/starlink/sensor.py - homeassistant/components/starlink/switch.py - homeassistant/components/starlink/time.py - homeassistant/components/steam_online/sensor.py - homeassistant/components/stiebel_eltron/* - homeassistant/components/stookalert/__init__.py - homeassistant/components/stookalert/binary_sensor.py - homeassistant/components/stookwijzer/__init__.py - homeassistant/components/stookwijzer/sensor.py - homeassistant/components/stream/__init__.py - homeassistant/components/stream/core.py - homeassistant/components/stream/fmp4utils.py - homeassistant/components/stream/hls.py - homeassistant/components/stream/worker.py - homeassistant/components/streamlabswater/__init__.py - homeassistant/components/streamlabswater/binary_sensor.py - homeassistant/components/streamlabswater/coordinator.py - homeassistant/components/streamlabswater/sensor.py - homeassistant/components/suez_water/__init__.py - homeassistant/components/suez_water/sensor.py - homeassistant/components/supervisord/sensor.py - homeassistant/components/supla/* - homeassistant/components/surepetcare/__init__.py - homeassistant/components/surepetcare/binary_sensor.py - homeassistant/components/surepetcare/coordinator.py - homeassistant/components/surepetcare/entity.py - homeassistant/components/surepetcare/sensor.py - homeassistant/components/swiss_hydrological_data/sensor.py - homeassistant/components/swiss_public_transport/__init__.py - homeassistant/components/swiss_public_transport/coordinator.py - homeassistant/components/swiss_public_transport/sensor.py - homeassistant/components/swisscom/device_tracker.py - homeassistant/components/switchbee/__init__.py - homeassistant/components/switchbee/button.py - homeassistant/components/switchbee/climate.py - homeassistant/components/switchbee/coordinator.py - homeassistant/components/switchbee/cover.py - homeassistant/components/switchbee/entity.py - homeassistant/components/switchbee/light.py - homeassistant/components/switchbee/switch.py - homeassistant/components/switchbot/__init__.py - homeassistant/components/switchbot/binary_sensor.py - homeassistant/components/switchbot/coordinator.py - homeassistant/components/switchbot/cover.py - homeassistant/components/switchbot/entity.py - homeassistant/components/switchbot/humidifier.py - homeassistant/components/switchbot/light.py - homeassistant/components/switchbot/lock.py - homeassistant/components/switchbot/sensor.py - homeassistant/components/switchbot/switch.py - homeassistant/components/switchbot_cloud/climate.py - homeassistant/components/switchbot_cloud/coordinator.py - homeassistant/components/switchbot_cloud/entity.py - homeassistant/components/switchbot_cloud/sensor.py - homeassistant/components/switchbot_cloud/switch.py - homeassistant/components/switchmate/switch.py - homeassistant/components/syncthing/__init__.py - homeassistant/components/syncthing/sensor.py - homeassistant/components/syncthru/__init__.py - homeassistant/components/syncthru/sensor.py - homeassistant/components/synology_chat/notify.py - homeassistant/components/synology_dsm/__init__.py - homeassistant/components/synology_dsm/binary_sensor.py - homeassistant/components/synology_dsm/button.py - homeassistant/components/synology_dsm/camera.py - homeassistant/components/synology_dsm/common.py - homeassistant/components/synology_dsm/coordinator.py - homeassistant/components/synology_dsm/entity.py - homeassistant/components/synology_dsm/sensor.py - homeassistant/components/synology_dsm/service.py - homeassistant/components/synology_dsm/switch.py - homeassistant/components/synology_dsm/update.py - homeassistant/components/synology_srm/device_tracker.py - homeassistant/components/syslog/notify.py - homeassistant/components/system_bridge/__init__.py - homeassistant/components/system_bridge/binary_sensor.py - homeassistant/components/system_bridge/coordinator.py - homeassistant/components/system_bridge/entity.py - homeassistant/components/system_bridge/media_player.py - homeassistant/components/system_bridge/notify.py - homeassistant/components/system_bridge/sensor.py - homeassistant/components/system_bridge/update.py - homeassistant/components/tado/__init__.py - homeassistant/components/tado/binary_sensor.py - homeassistant/components/tado/climate.py - homeassistant/components/tado/device_tracker.py - homeassistant/components/tado/sensor.py - homeassistant/components/tado/water_heater.py - homeassistant/components/tami4/button.py - homeassistant/components/tank_utility/sensor.py - homeassistant/components/tapsaff/binary_sensor.py - homeassistant/components/tautulli/__init__.py - homeassistant/components/tautulli/coordinator.py - homeassistant/components/tautulli/sensor.py - homeassistant/components/ted5000/sensor.py - homeassistant/components/telegram/notify.py - homeassistant/components/telegram_bot/__init__.py - homeassistant/components/telegram_bot/polling.py - homeassistant/components/telegram_bot/webhooks.py - homeassistant/components/tellduslive/__init__.py - homeassistant/components/tellduslive/binary_sensor.py - homeassistant/components/tellduslive/cover.py - homeassistant/components/tellduslive/entry.py - homeassistant/components/tellduslive/light.py - homeassistant/components/tellduslive/sensor.py - homeassistant/components/tellduslive/switch.py - homeassistant/components/tellstick/* - homeassistant/components/telnet/switch.py - homeassistant/components/temper/sensor.py - homeassistant/components/tensorflow/image_processing.py - homeassistant/components/tfiac/climate.py - homeassistant/components/thermoworks_smoke/sensor.py - homeassistant/components/thingspeak/* - homeassistant/components/thinkingcleaner/* - homeassistant/components/thomson/device_tracker.py - homeassistant/components/tibber/__init__.py - homeassistant/components/tibber/coordinator.py - homeassistant/components/tibber/sensor.py - homeassistant/components/tikteck/light.py - homeassistant/components/tile/__init__.py - homeassistant/components/tile/device_tracker.py - homeassistant/components/time_date/sensor.py - homeassistant/components/tmb/sensor.py - homeassistant/components/todoist/calendar.py - homeassistant/components/tolo/__init__.py - homeassistant/components/tolo/binary_sensor.py - homeassistant/components/tolo/button.py - homeassistant/components/tolo/climate.py - homeassistant/components/tolo/fan.py - homeassistant/components/tolo/light.py - homeassistant/components/tolo/number.py - homeassistant/components/tolo/select.py - homeassistant/components/tolo/sensor.py - homeassistant/components/tolo/switch.py - homeassistant/components/toon/__init__.py - homeassistant/components/toon/binary_sensor.py - homeassistant/components/toon/climate.py - homeassistant/components/toon/coordinator.py - homeassistant/components/toon/helpers.py - homeassistant/components/toon/models.py - homeassistant/components/toon/oauth2.py - homeassistant/components/toon/sensor.py - homeassistant/components/toon/switch.py - homeassistant/components/torque/sensor.py - homeassistant/components/totalconnect/__init__.py - homeassistant/components/touchline/climate.py - homeassistant/components/tplink_lte/* - homeassistant/components/tplink_omada/__init__.py - homeassistant/components/tplink_omada/binary_sensor.py - homeassistant/components/tplink_omada/controller.py - homeassistant/components/tplink_omada/update.py - homeassistant/components/traccar/device_tracker.py - homeassistant/components/traccar_server/__init__.py - homeassistant/components/traccar_server/coordinator.py - homeassistant/components/traccar_server/device_tracker.py - homeassistant/components/traccar_server/entity.py - homeassistant/components/traccar_server/helpers.py - homeassistant/components/traccar_server/sensor.py - homeassistant/components/tradfri/__init__.py - homeassistant/components/tradfri/base_class.py - homeassistant/components/tradfri/coordinator.py - homeassistant/components/tradfri/cover.py - homeassistant/components/tradfri/fan.py - homeassistant/components/tradfri/light.py - homeassistant/components/tradfri/sensor.py - homeassistant/components/tradfri/switch.py - homeassistant/components/trafikverket_weatherstation/__init__.py - homeassistant/components/trafikverket_weatherstation/coordinator.py - homeassistant/components/trafikverket_weatherstation/sensor.py - homeassistant/components/transmission/__init__.py - homeassistant/components/transmission/coordinator.py - homeassistant/components/transmission/sensor.py - homeassistant/components/transmission/switch.py - homeassistant/components/travisci/sensor.py - homeassistant/components/tuya/__init__.py - homeassistant/components/tuya/alarm_control_panel.py - homeassistant/components/tuya/base.py - homeassistant/components/tuya/binary_sensor.py - homeassistant/components/tuya/button.py - homeassistant/components/tuya/camera.py - homeassistant/components/tuya/climate.py - homeassistant/components/tuya/cover.py - homeassistant/components/tuya/fan.py - homeassistant/components/tuya/humidifier.py - homeassistant/components/tuya/light.py - homeassistant/components/tuya/number.py - homeassistant/components/tuya/select.py - homeassistant/components/tuya/sensor.py - homeassistant/components/tuya/siren.py - homeassistant/components/tuya/switch.py - homeassistant/components/tuya/util.py - homeassistant/components/tuya/vacuum.py - homeassistant/components/twilio_call/notify.py - homeassistant/components/twilio_sms/notify.py - homeassistant/components/twitter/notify.py - homeassistant/components/ubus/device_tracker.py - homeassistant/components/ue_smart_radio/media_player.py - homeassistant/components/ukraine_alarm/__init__.py - homeassistant/components/ukraine_alarm/binary_sensor.py - homeassistant/components/unifi_direct/device_tracker.py - homeassistant/components/unifiled/* - homeassistant/components/upb/__init__.py - homeassistant/components/upb/light.py - homeassistant/components/upc_connect/* - homeassistant/components/upcloud/__init__.py - homeassistant/components/upcloud/binary_sensor.py - homeassistant/components/upcloud/switch.py - homeassistant/components/upnp/__init__.py - homeassistant/components/upnp/device.py - homeassistant/components/upnp/sensor.py - homeassistant/components/v2c/__init__.py - homeassistant/components/v2c/binary_sensor.py - homeassistant/components/v2c/coordinator.py - homeassistant/components/v2c/entity.py - homeassistant/components/v2c/number.py - homeassistant/components/v2c/switch.py - homeassistant/components/vallox/__init__.py - homeassistant/components/vallox/coordinator.py - homeassistant/components/vasttrafik/sensor.py - homeassistant/components/velbus/__init__.py - homeassistant/components/velbus/binary_sensor.py - homeassistant/components/velbus/button.py - homeassistant/components/velbus/climate.py - homeassistant/components/velbus/cover.py - homeassistant/components/velbus/entity.py - homeassistant/components/velbus/light.py - homeassistant/components/velbus/select.py - homeassistant/components/velbus/sensor.py - homeassistant/components/velbus/switch.py - homeassistant/components/velux/__init__.py - homeassistant/components/velux/cover.py - homeassistant/components/velux/light.py - homeassistant/components/venstar/climate.py - homeassistant/components/venstar/coordinator.py - homeassistant/components/venstar/sensor.py - homeassistant/components/verisure/__init__.py - homeassistant/components/verisure/alarm_control_panel.py - homeassistant/components/verisure/binary_sensor.py - homeassistant/components/verisure/camera.py - homeassistant/components/verisure/coordinator.py - homeassistant/components/verisure/lock.py - homeassistant/components/verisure/sensor.py - homeassistant/components/verisure/switch.py - homeassistant/components/versasense/* - homeassistant/components/vesync/__init__.py - homeassistant/components/vesync/fan.py - homeassistant/components/vesync/light.py - homeassistant/components/vesync/sensor.py - homeassistant/components/vesync/switch.py - homeassistant/components/viaggiatreno/sensor.py - homeassistant/components/vicare/__init__.py - homeassistant/components/vicare/button.py - homeassistant/components/vicare/climate.py - homeassistant/components/vicare/entity.py - homeassistant/components/vicare/number.py - homeassistant/components/vicare/sensor.py - homeassistant/components/vicare/types.py - homeassistant/components/vicare/utils.py - homeassistant/components/vicare/water_heater.py - homeassistant/components/vilfo/__init__.py - homeassistant/components/vilfo/sensor.py - homeassistant/components/vivotek/camera.py - homeassistant/components/vlc/media_player.py - homeassistant/components/vlc_telnet/__init__.py - homeassistant/components/vlc_telnet/media_player.py - homeassistant/components/vodafone_station/__init__.py - homeassistant/components/vodafone_station/button.py - homeassistant/components/vodafone_station/coordinator.py - homeassistant/components/vodafone_station/device_tracker.py - homeassistant/components/vodafone_station/sensor.py - homeassistant/components/volkszaehler/sensor.py - homeassistant/components/volumio/__init__.py - homeassistant/components/volumio/browse_media.py - homeassistant/components/volumio/media_player.py - homeassistant/components/volvooncall/__init__.py - homeassistant/components/volvooncall/binary_sensor.py - homeassistant/components/volvooncall/device_tracker.py - homeassistant/components/volvooncall/lock.py - homeassistant/components/volvooncall/sensor.py - homeassistant/components/volvooncall/switch.py - homeassistant/components/vulcan/__init__.py - homeassistant/components/vulcan/calendar.py - homeassistant/components/vulcan/fetch_data.py - homeassistant/components/w800rf32/* - homeassistant/components/waqi/sensor.py - homeassistant/components/waterfurnace/* - homeassistant/components/watson_iot/* - homeassistant/components/watson_tts/tts.py - homeassistant/components/watttime/__init__.py - homeassistant/components/watttime/sensor.py - homeassistant/components/weatherflow/__init__.py - homeassistant/components/weatherflow/sensor.py - homeassistant/components/weatherflow_cloud/__init__.py - homeassistant/components/weatherflow_cloud/coordinator.py - homeassistant/components/weatherflow_cloud/weather.py - homeassistant/components/wiffi/__init__.py - homeassistant/components/wiffi/binary_sensor.py - homeassistant/components/wiffi/sensor.py - homeassistant/components/wiffi/wiffi_strings.py - homeassistant/components/wirelesstag/* - homeassistant/components/wolflink/__init__.py - homeassistant/components/wolflink/sensor.py - homeassistant/components/worldtidesinfo/sensor.py - homeassistant/components/worxlandroid/sensor.py - homeassistant/components/x10/light.py - homeassistant/components/xbox/__init__.py - homeassistant/components/xbox/api.py - homeassistant/components/xbox/base_sensor.py - homeassistant/components/xbox/binary_sensor.py - homeassistant/components/xbox/browse_media.py - homeassistant/components/xbox/coordinator.py - homeassistant/components/xbox/media_player.py - homeassistant/components/xbox/remote.py - homeassistant/components/xbox/sensor.py - homeassistant/components/xeoma/camera.py - homeassistant/components/xiaomi/camera.py - homeassistant/components/xiaomi_aqara/__init__.py - homeassistant/components/xiaomi_aqara/binary_sensor.py - homeassistant/components/xiaomi_aqara/cover.py - homeassistant/components/xiaomi_aqara/light.py - homeassistant/components/xiaomi_aqara/lock.py - homeassistant/components/xiaomi_aqara/sensor.py - homeassistant/components/xiaomi_aqara/switch.py - homeassistant/components/xiaomi_miio/__init__.py - homeassistant/components/xiaomi_miio/air_quality.py - homeassistant/components/xiaomi_miio/alarm_control_panel.py - homeassistant/components/xiaomi_miio/binary_sensor.py - homeassistant/components/xiaomi_miio/button.py - homeassistant/components/xiaomi_miio/device.py - homeassistant/components/xiaomi_miio/device_tracker.py - homeassistant/components/xiaomi_miio/fan.py - homeassistant/components/xiaomi_miio/gateway.py - homeassistant/components/xiaomi_miio/humidifier.py - homeassistant/components/xiaomi_miio/light.py - homeassistant/components/xiaomi_miio/number.py - homeassistant/components/xiaomi_miio/remote.py - homeassistant/components/xiaomi_miio/sensor.py - homeassistant/components/xiaomi_miio/switch.py - homeassistant/components/xiaomi_miio/typing.py - homeassistant/components/xiaomi_tv/media_player.py - homeassistant/components/xmpp/notify.py - homeassistant/components/xs1/* - homeassistant/components/yale_smart_alarm/__init__.py - homeassistant/components/yale_smart_alarm/alarm_control_panel.py - homeassistant/components/yale_smart_alarm/entity.py - homeassistant/components/yalexs_ble/__init__.py - homeassistant/components/yalexs_ble/binary_sensor.py - homeassistant/components/yalexs_ble/entity.py - homeassistant/components/yalexs_ble/lock.py - homeassistant/components/yalexs_ble/sensor.py - homeassistant/components/yalexs_ble/util.py - homeassistant/components/yamaha_musiccast/__init__.py - homeassistant/components/yamaha_musiccast/media_player.py - homeassistant/components/yamaha_musiccast/number.py - homeassistant/components/yamaha_musiccast/select.py - homeassistant/components/yamaha_musiccast/switch.py - homeassistant/components/yandex_transport/sensor.py - homeassistant/components/yardian/__init__.py - homeassistant/components/yardian/coordinator.py - homeassistant/components/yardian/switch.py - homeassistant/components/yeelightsunflower/light.py - homeassistant/components/yi/camera.py - homeassistant/components/yolink/__init__.py - homeassistant/components/yolink/api.py - homeassistant/components/yolink/binary_sensor.py - homeassistant/components/yolink/climate.py - homeassistant/components/yolink/coordinator.py - homeassistant/components/yolink/cover.py - homeassistant/components/yolink/entity.py - homeassistant/components/yolink/light.py - homeassistant/components/yolink/lock.py - homeassistant/components/yolink/number.py - homeassistant/components/yolink/sensor.py - homeassistant/components/yolink/services.py - homeassistant/components/yolink/siren.py - homeassistant/components/yolink/switch.py - homeassistant/components/yolink/valve.py - homeassistant/components/zabbix/* - homeassistant/components/zamg/coordinator.py - homeassistant/components/zengge/light.py - homeassistant/components/zeroconf/usage.py - homeassistant/components/zestimate/sensor.py - homeassistant/components/zha/core/cluster_handlers/* - homeassistant/components/zha/core/device.py - homeassistant/components/zha/core/gateway.py - homeassistant/components/zha/core/helpers.py - homeassistant/components/zha/light.py - homeassistant/components/zha/websocket_api.py - homeassistant/components/zhong_hong/climate.py - homeassistant/components/ziggo_mediabox_xl/media_player.py - homeassistant/components/zoneminder/* - homeassistant/components/zwave_me/__init__.py - homeassistant/components/zwave_me/binary_sensor.py - homeassistant/components/zwave_me/button.py - homeassistant/components/zwave_me/climate.py - homeassistant/components/zwave_me/cover.py - homeassistant/components/zwave_me/fan.py - homeassistant/components/zwave_me/helpers.py - homeassistant/components/zwave_me/light.py - homeassistant/components/zwave_me/lock.py - homeassistant/components/zwave_me/number.py - homeassistant/components/zwave_me/sensor.py - homeassistant/components/zwave_me/siren.py - homeassistant/components/zwave_me/switch.py - - -[report] -# Regexes for lines to exclude from consideration -exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - - # Don't complain about missing debug-only code: - def __repr__ - - # Don't complain if tests don't hit defensive assertion code: - raise AssertionError - raise NotImplementedError - - # TYPE_CHECKING and @overload blocks are never executed during pytest run - if TYPE_CHECKING: - @overload diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d69b1ac0c7d..23365feffb7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -74,7 +74,6 @@ If the code communicates with devices, web services, or third-party tools: - [ ] New or updated dependencies have been added to `requirements_all.txt`. Updated by running `python3 -m script.gen_requirements_all`. - [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description. -- [ ] Untested files have been added to `.coveragerc`. ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry() # 2: user(None): scan --> user({...}) --> create_entry() + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + """Get the options flow for this handler.""" + return UpnpOptionsFlowHandler(config_entry) + @property def _discoveries(self) -> dict[str, SsdpServiceInfo]: """Get current discoveries.""" @@ -249,9 +264,14 @@ class UpnpFlowHandler(ConfigFlow, domain=DOMAIN): CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"], CONFIG_ENTRY_LOCATION: get_preferred_location(discovery.ssdp_all_locations), } + options = { + CONFIG_ENTRY_FORCE_POLL: False, + } await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) - return self.async_create_entry(title=user_input["title"], data=data) + return self.async_create_entry( + title=user_input["title"], data=data, options=options + ) async def _async_create_entry_from_discovery( self, @@ -273,4 +293,30 @@ class UpnpFlowHandler(ConfigFlow, domain=DOMAIN): CONFIG_ENTRY_MAC_ADDRESS: mac_address, CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"], } - return self.async_create_entry(title=title, data=data) + options = { + CONFIG_ENTRY_FORCE_POLL: False, + } + return self.async_create_entry(title=title, data=data, options=options) + + +class UpnpOptionsFlowHandler(OptionsFlowWithConfigEntry): + """Handle an options flow.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Optional( + CONFIG_ENTRY_FORCE_POLL, + default=self.options.get( + CONFIG_ENTRY_FORCE_POLL, DEFAULT_CONFIG_ENTRY_FORCE_POLL + ), + ): bool, + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 5d68a83d4d4..d85675d8a4d 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -24,6 +24,7 @@ WAN_STATUS = "wan_status" PORT_MAPPING_NUMBER_OF_ENTRIES_IPV4 = "port_mapping_number_of_entries" ROUTER_IP = "ip" ROUTER_UPTIME = "uptime" +CONFIG_ENTRY_FORCE_POLL = "force_poll" CONFIG_ENTRY_ST = "st" CONFIG_ENTRY_UDN = "udn" CONFIG_ENTRY_ORIGINAL_UDN = "original_udn" @@ -33,5 +34,6 @@ CONFIG_ENTRY_HOST = "host" IDENTIFIER_HOST = "upnp_host" IDENTIFIER_SERIAL_NUMBER = "upnp_serial_number" DEFAULT_SCAN_INTERVAL = timedelta(seconds=30).total_seconds() +DEFAULT_CONFIG_ENTRY_FORCE_POLL = False ST_IGD_V1 = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" ST_IGD_V2 = "urn:schemas-upnp-org:device:InternetGatewayDevice:2" diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index bb95978c8dc..bb414fa95f8 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -21,7 +21,8 @@ "step": { "init": { "data": { - "scan_interval": "Update interval (seconds, minimal 30)" + "scan_interval": "Update interval (seconds, minimal 30)", + "force_poll": "Force polling of all data" } } } diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index 35e96ff7284..1431ce2c9ef 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -248,7 +248,7 @@ async def mock_config_entry( ssdp_instant_discovery, mock_igd_device: IgdDevice, mock_mac_address_from_host, -): +) -> MockConfigEntry: """Create an initialized integration.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index b8a08d3f592..8799e0faab3 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -9,6 +9,7 @@ import pytest from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( + CONFIG_ENTRY_FORCE_POLL, CONFIG_ENTRY_HOST, CONFIG_ENTRY_LOCATION, CONFIG_ENTRY_MAC_ADDRESS, @@ -473,3 +474,28 @@ async def test_flow_ssdp_with_mismatched_udn(hass: HomeAssistant) -> None: CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, CONFIG_ENTRY_HOST: TEST_HOST, } + + +async def test_options_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test that the options flow works.""" + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + user_input = { + CONFIG_ENTRY_FORCE_POLL: True, + } + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONFIG_ENTRY_FORCE_POLL: True, + } + assert mock_config_entry.options == { + CONFIG_ENTRY_FORCE_POLL: True, + } diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 422d8c9e33a..f87696b0bd1 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -10,6 +10,7 @@ import pytest from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( + CONFIG_ENTRY_FORCE_POLL, CONFIG_ENTRY_LOCATION, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, @@ -46,6 +47,9 @@ async def test_async_setup_entry_default( CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, }, + options={ + CONFIG_ENTRY_FORCE_POLL: False, + }, ) # Load config_entry. @@ -68,6 +72,9 @@ async def test_async_setup_entry_default_no_mac_address(hass: HomeAssistant) -> CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_MAC_ADDRESS: None, }, + options={ + CONFIG_ENTRY_FORCE_POLL: False, + }, ) # Load config_entry. @@ -96,6 +103,9 @@ async def test_async_setup_entry_multi_location( CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, }, + options={ + CONFIG_ENTRY_FORCE_POLL: False, + }, ) # Load config_entry. @@ -124,6 +134,9 @@ async def test_async_setup_udn_mismatch( CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, }, + options={ + CONFIG_ENTRY_FORCE_POLL: False, + }, ) # Set up device discovery callback. @@ -148,3 +161,34 @@ async def test_async_setup_udn_mismatch( # Ensure that the IPv4 location is used. mock_async_create_device.assert_called_once_with(TEST_LOCATION) + + +@pytest.mark.usefixtures( + "ssdp_instant_discovery", + "mock_get_source_ip", + "mock_mac_address_from_host", +) +async def test_async_setup_entry_force_poll( + hass: HomeAssistant, mock_igd_device: IgdDevice +) -> None: + """Test async_setup_entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_USN, + data={ + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, + CONFIG_ENTRY_LOCATION: TEST_LOCATION, + CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, + }, + options={ + CONFIG_ENTRY_FORCE_POLL: True, + }, + ) + + # Load config_entry. + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) is True + + mock_igd_device.async_subscribe_services.assert_not_called() From cafff3eddf585dd787c3e59c27bb928b6e2c3be7 Mon Sep 17 00:00:00 2001 From: Pierre Mavro Date: Fri, 19 Jul 2024 19:15:42 +0200 Subject: [PATCH 1350/2411] Add PrusaLink nozzle and mmu support (#120436) Co-authored-by: Stefan Agner --- .../components/prusalink/__init__.py | 9 +- .../components/prusalink/binary_sensor.py | 96 +++++++++++++++++++ .../components/prusalink/coordinator.py | 16 +++- homeassistant/components/prusalink/icons.json | 3 + homeassistant/components/prusalink/sensor.py | 14 ++- .../components/prusalink/strings.json | 8 ++ .../prusalink/test_binary_sensor.py | 33 +++++++ tests/components/prusalink/test_sensor.py | 8 ++ 8 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/prusalink/binary_sensor.py create mode 100644 tests/components/prusalink/test_binary_sensor.py diff --git a/homeassistant/components/prusalink/__init__.py b/homeassistant/components/prusalink/__init__.py index 9d6096748dd..62eeb91d3e1 100644 --- a/homeassistant/components/prusalink/__init__.py +++ b/homeassistant/components/prusalink/__init__.py @@ -23,13 +23,19 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .config_flow import ConfigFlow from .const import DOMAIN from .coordinator import ( + InfoUpdateCoordinator, JobUpdateCoordinator, LegacyStatusCoordinator, PrusaLinkUpdateCoordinator, StatusCoordinator, ) -PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.CAMERA, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CAMERA, + Platform.SENSOR, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -48,6 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "legacy_status": LegacyStatusCoordinator(hass, api), "status": StatusCoordinator(hass, api), "job": JobUpdateCoordinator(hass, api), + "info": InfoUpdateCoordinator(hass, api), } for coordinator in coordinators.values(): await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/prusalink/binary_sensor.py b/homeassistant/components/prusalink/binary_sensor.py new file mode 100644 index 00000000000..abeb79c2876 --- /dev/null +++ b/homeassistant/components/prusalink/binary_sensor.py @@ -0,0 +1,96 @@ +"""PrusaLink binary sensors.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic, TypeVar + +from pyprusalink.types import JobInfo, PrinterInfo, PrinterStatus +from pyprusalink.types_legacy import LegacyPrinterStatus + +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import PrusaLinkEntity +from .const import DOMAIN +from .coordinator import PrusaLinkUpdateCoordinator + +T = TypeVar("T", PrinterStatus, LegacyPrinterStatus, JobInfo, PrinterInfo) + + +@dataclass(frozen=True) +class PrusaLinkBinarySensorEntityDescriptionMixin(Generic[T]): + """Mixin for required keys.""" + + value_fn: Callable[[T], bool] + + +@dataclass(frozen=True) +class PrusaLinkBinarySensorEntityDescription( + BinarySensorEntityDescription, + PrusaLinkBinarySensorEntityDescriptionMixin[T], + Generic[T], +): + """Describes PrusaLink sensor entity.""" + + available_fn: Callable[[T], bool] = lambda _: True + + +BINARY_SENSORS: dict[str, tuple[PrusaLinkBinarySensorEntityDescription, ...]] = { + "info": ( + PrusaLinkBinarySensorEntityDescription[PrinterInfo]( + key="info.mmu", + translation_key="mmu", + value_fn=lambda data: data["mmu"], + entity_registry_enabled_default=False, + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up PrusaLink sensor based on a config entry.""" + coordinators: dict[str, PrusaLinkUpdateCoordinator] = hass.data[DOMAIN][ + entry.entry_id + ] + + entities: list[PrusaLinkEntity] = [] + for coordinator_type, binary_sensors in BINARY_SENSORS.items(): + coordinator = coordinators[coordinator_type] + entities.extend( + PrusaLinkBinarySensorEntity(coordinator, sensor_description) + for sensor_description in binary_sensors + ) + + async_add_entities(entities) + + +class PrusaLinkBinarySensorEntity(PrusaLinkEntity, BinarySensorEntity): + """Defines a PrusaLink binary sensor.""" + + entity_description: PrusaLinkBinarySensorEntityDescription + + def __init__( + self, + coordinator: PrusaLinkUpdateCoordinator, + description: PrusaLinkBinarySensorEntityDescription, + ) -> None: + """Initialize a PrusaLink sensor entity.""" + super().__init__(coordinator=coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/prusalink/coordinator.py b/homeassistant/components/prusalink/coordinator.py index 1d1989119fa..1d887983931 100644 --- a/homeassistant/components/prusalink/coordinator.py +++ b/homeassistant/components/prusalink/coordinator.py @@ -10,7 +10,13 @@ from time import monotonic from typing import TypeVar from httpx import ConnectError -from pyprusalink import JobInfo, LegacyPrinterStatus, PrinterStatus, PrusaLink +from pyprusalink import ( + JobInfo, + LegacyPrinterStatus, + PrinterInfo, + PrinterStatus, + PrusaLink, +) from pyprusalink.types import InvalidAuth, PrusaLinkError from homeassistant.config_entries import ConfigEntry @@ -94,3 +100,11 @@ class JobUpdateCoordinator(PrusaLinkUpdateCoordinator[JobInfo]): async def _fetch_data(self) -> JobInfo: """Fetch the printer data.""" return await self.api.get_job() + + +class InfoUpdateCoordinator(PrusaLinkUpdateCoordinator[PrinterInfo]): + """Info update coordinator.""" + + async def _fetch_data(self) -> PrinterInfo: + """Fetch the printer data.""" + return await self.api.get_info() diff --git a/homeassistant/components/prusalink/icons.json b/homeassistant/components/prusalink/icons.json index 4d97ea76ddd..578cb5e5d0c 100644 --- a/homeassistant/components/prusalink/icons.json +++ b/homeassistant/components/prusalink/icons.json @@ -24,6 +24,9 @@ "filename": { "default": "mdi:file-image-outline" }, + "nozzle_diameter": { + "default": "mdi:printer-3d-nozzle" + }, "print_start": { "default": "mdi:clock-start" }, diff --git a/homeassistant/components/prusalink/sensor.py b/homeassistant/components/prusalink/sensor.py index 80998d680d2..96cd4979b11 100644 --- a/homeassistant/components/prusalink/sensor.py +++ b/homeassistant/components/prusalink/sensor.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta from typing import Generic, TypeVar, cast -from pyprusalink.types import JobInfo, PrinterState, PrinterStatus +from pyprusalink.types import JobInfo, PrinterInfo, PrinterState, PrinterStatus from pyprusalink.types_legacy import LegacyPrinterStatus from homeassistant.components.sensor import ( @@ -33,7 +33,7 @@ from . import PrusaLinkEntity from .const import DOMAIN from .coordinator import PrusaLinkUpdateCoordinator -T = TypeVar("T", PrinterStatus, LegacyPrinterStatus, JobInfo) +T = TypeVar("T", PrinterStatus, LegacyPrinterStatus, JobInfo, PrinterInfo) @dataclass(frozen=True) @@ -189,6 +189,16 @@ SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { ), ), ), + "info": ( + PrusaLinkSensorEntityDescription[PrinterInfo]( + key="info.nozzle_diameter", + translation_key="nozzle_diameter", + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + device_class=SensorDeviceClass.DISTANCE, + value_fn=lambda data: cast(str, data["nozzle_diameter"]), + entity_registry_enabled_default=False, + ), + ), } diff --git a/homeassistant/components/prusalink/strings.json b/homeassistant/components/prusalink/strings.json index bb32770e357..7c6f0bbf2dd 100644 --- a/homeassistant/components/prusalink/strings.json +++ b/homeassistant/components/prusalink/strings.json @@ -23,6 +23,11 @@ } }, "entity": { + "binary_sensor": { + "mmu": { + "name": "MMU" + } + }, "sensor": { "printer_state": { "state": { @@ -78,6 +83,9 @@ }, "z_height": { "name": "Z-Height" + }, + "nozzle_diameter": { + "name": "Nozzle Diameter" } }, "button": { diff --git a/tests/components/prusalink/test_binary_sensor.py b/tests/components/prusalink/test_binary_sensor.py new file mode 100644 index 00000000000..c39b15471c6 --- /dev/null +++ b/tests/components/prusalink/test_binary_sensor.py @@ -0,0 +1,33 @@ +"""Test Prusalink sensors.""" + +from unittest.mock import PropertyMock, patch + +import pytest + +from homeassistant.const import STATE_OFF, Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +def setup_binary_sensor_platform_only(): + """Only setup sensor platform.""" + with ( + patch("homeassistant.components.prusalink.PLATFORMS", [Platform.BINARY_SENSOR]), + patch( + "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", + PropertyMock(return_value=True), + ), + ): + yield + + +async def test_binary_sensors_no_job( + hass: HomeAssistant, mock_config_entry, mock_api +) -> None: + """Test sensors while no job active.""" + assert await async_setup_component(hass, "prusalink", {}) + + state = hass.states.get("binary_sensor.mock_title_mmu") + assert state is not None + assert state.state == STATE_OFF diff --git a/tests/components/prusalink/test_sensor.py b/tests/components/prusalink/test_sensor.py index b15e9198da6..c0693626600 100644 --- a/tests/components/prusalink/test_sensor.py +++ b/tests/components/prusalink/test_sensor.py @@ -101,6 +101,10 @@ async def test_sensors_no_job(hass: HomeAssistant, mock_config_entry, mock_api) assert state is not None assert state.state == "PLA" + state = hass.states.get("sensor.mock_title_nozzle_diameter") + assert state is not None + assert state.state == "0.4" + state = hass.states.get("sensor.mock_title_print_flow") assert state is not None assert state.state == "100" @@ -205,6 +209,10 @@ async def test_sensors_idle_job_mk3( assert state is not None assert state.state == "PLA" + state = hass.states.get("sensor.mock_title_nozzle_diameter") + assert state is not None + assert state.state == "0.4" + state = hass.states.get("sensor.mock_title_print_flow") assert state is not None assert state.state == "100" From 099110767a677174efb345fd19aac3ce263c23c1 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:35:44 +0100 Subject: [PATCH 1351/2411] Add tests for ring camera platform for 100% coverage (#122197) --- homeassistant/components/ring/camera.py | 12 +- tests/components/ring/device_mocks.py | 3 + tests/components/ring/test_camera.py | 182 +++++++++++++++++++++++- 3 files changed, 190 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index a5144777eaa..ba75b68434d 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any +from typing import TYPE_CHECKING, Any from aiohttp import web from haffmpeg.camera import CameraMjpeg @@ -145,8 +145,9 @@ class RingCam(RingEntity[RingDoorBell], Camera): self._attr_motion_detection_enabled = self._device.motion_detection self.async_write_ha_state() - if self._last_event is None: - return + if TYPE_CHECKING: + # _last_event is set before calling update so will never be None + assert self._last_event if self._last_event["recording"]["status"] != "ready": return @@ -165,8 +166,9 @@ class RingCam(RingEntity[RingDoorBell], Camera): @exception_wrap def _get_video(self) -> str | None: - if self._last_event is None: - return None + if TYPE_CHECKING: + # _last_event is set before calling update so will never be None + assert self._last_event event_id = self._last_event.get("id") assert event_id and isinstance(event_id, int) return self._device.recording_url(event_id) diff --git a/tests/components/ring/device_mocks.py b/tests/components/ring/device_mocks.py index f43370c918d..88ad37bdd36 100644 --- a/tests/components/ring/device_mocks.py +++ b/tests/components/ring/device_mocks.py @@ -142,6 +142,9 @@ def _mocked_ring_device(device_dict, device_family, device_class, capabilities): DOORBOT_HISTORY if device_family != "other" else INTERCOM_HISTORY ) + if has_capability(RingCapability.VIDEO): + mock_device.recording_url = MagicMock(return_value="http://dummy.url") + if has_capability(RingCapability.MOTION_DETECTION): mock_device.configure_mock( motion_detection=device_dict["settings"].get("motion_detection_enabled"), diff --git a/tests/components/ring/test_camera.py b/tests/components/ring/test_camera.py index 20a9ed5f0c9..49b7dc10f05 100644 --- a/tests/components/ring/test_camera.py +++ b/tests/components/ring/test_camera.py @@ -1,18 +1,33 @@ """The tests for the Ring switch platform.""" -from unittest.mock import PropertyMock +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch +from aiohttp.test_utils import make_mocked_request +from freezegun.api import FrozenDateTimeFactory import pytest import ring_doorbell +from homeassistant.components import camera +from homeassistant.components.ring.camera import FORCE_REFRESH_INTERVAL +from homeassistant.components.ring.const import SCAN_INTERVAL from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er +from homeassistant.util.aiohttp import MockStreamReader from .common import setup_platform +from tests.common import async_fire_time_changed + +SMALLEST_VALID_JPEG = ( + "ffd8ffe000104a46494600010101004800480000ffdb00430003020202020203020202030303030406040404040408060" + "6050609080a0a090809090a0c0f0c0a0b0e0b09090d110d0e0f101011100a0c12131210130f101010ffc9000b08000100" + "0101011100ffcc000600101005ffda0008010100003f00d2cf20ffd9" +) +SMALLEST_VALID_JPEG_BYTES = bytes.fromhex(SMALLEST_VALID_JPEG) + async def test_entity_registry( hass: HomeAssistant, @@ -52,7 +67,7 @@ async def test_camera_motion_detection_state_reports_correctly( assert state.attributes.get("friendly_name") == friendly_name -async def test_camera_motion_detection_can_be_turned_on( +async def test_camera_motion_detection_can_be_turned_on_and_off( hass: HomeAssistant, mock_ring_client ) -> None: """Tests the siren turns on correctly.""" @@ -73,6 +88,55 @@ async def test_camera_motion_detection_can_be_turned_on( state = hass.states.get("camera.front") assert state.attributes.get("motion_detection") is True + await hass.services.async_call( + "camera", + "disable_motion_detection", + {"entity_id": "camera.front"}, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get("camera.front") + assert state.attributes.get("motion_detection") is None + + +async def test_camera_motion_detection_not_supported( + hass: HomeAssistant, + mock_ring_client, + mock_ring_devices, + caplog: pytest.LogCaptureFixture, +) -> None: + """Tests the siren turns on correctly.""" + front_camera_mock = mock_ring_devices.get_device(765432) + has_capability = front_camera_mock.has_capability.side_effect + + def _has_capability(capability): + if capability == "motion_detection": + return False + return has_capability(capability) + + front_camera_mock.has_capability.side_effect = _has_capability + + await setup_platform(hass, Platform.CAMERA) + + state = hass.states.get("camera.front") + assert state.attributes.get("motion_detection") is None + + await hass.services.async_call( + "camera", + "enable_motion_detection", + {"entity_id": "camera.front"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("camera.front") + assert state.attributes.get("motion_detection") is None + assert ( + "Entity camera.front does not have motion detection capability" in caplog.text + ) + async def test_updates_work( hass: HomeAssistant, mock_ring_client, mock_ring_devices @@ -136,3 +200,117 @@ async def test_motion_detection_errors_when_turned_on( ) == reauth_expected ) + + +async def test_camera_handle_mjpeg_stream( + hass: HomeAssistant, + mock_ring_client, + mock_ring_devices, + freezer: FrozenDateTimeFactory, +) -> None: + """Test camera returns handle mjpeg stream when available.""" + await setup_platform(hass, Platform.CAMERA) + + front_camera_mock = mock_ring_devices.get_device(765432) + front_camera_mock.recording_url.return_value = None + + state = hass.states.get("camera.front") + assert state is not None + + mock_request = make_mocked_request("GET", "/", headers={"token": "x"}) + + # history not updated yet + front_camera_mock.history.assert_not_called() + front_camera_mock.recording_url.assert_not_called() + stream = await camera.async_get_mjpeg_stream(hass, mock_request, "camera.front") + assert stream is None + + # Video url will be none so no stream + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + front_camera_mock.history.assert_called_once() + front_camera_mock.recording_url.assert_called_once() + + stream = await camera.async_get_mjpeg_stream(hass, mock_request, "camera.front") + assert stream is None + + # Stop the history updating so we can update the values manually + front_camera_mock.history = MagicMock() + front_camera_mock.last_history[0]["recording"]["status"] = "not ready" + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + front_camera_mock.recording_url.assert_called_once() + stream = await camera.async_get_mjpeg_stream(hass, mock_request, "camera.front") + assert stream is None + + # If the history id hasn't changed the camera will not check again for the video url + # until the FORCE_REFRESH_INTERVAL has passed + front_camera_mock.last_history[0]["recording"]["status"] = "ready" + front_camera_mock.recording_url = MagicMock(return_value="http://dummy.url") + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + front_camera_mock.recording_url.assert_not_called() + + stream = await camera.async_get_mjpeg_stream(hass, mock_request, "camera.front") + assert stream is None + + freezer.tick(FORCE_REFRESH_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + front_camera_mock.recording_url.assert_called_once() + + # Now the stream should be returned + stream_reader = MockStreamReader(SMALLEST_VALID_JPEG_BYTES) + with patch("homeassistant.components.ring.camera.CameraMjpeg") as mock_camera: + mock_camera.return_value.get_reader = AsyncMock(return_value=stream_reader) + mock_camera.return_value.open_camera = AsyncMock() + mock_camera.return_value.close = AsyncMock() + + stream = await camera.async_get_mjpeg_stream(hass, mock_request, "camera.front") + assert stream is not None + # Check the stream has been read + assert not await stream_reader.read(-1) + + +async def test_camera_image( + hass: HomeAssistant, + mock_ring_client, + mock_ring_devices, + freezer: FrozenDateTimeFactory, +) -> None: + """Test camera will return still image when available.""" + await setup_platform(hass, Platform.CAMERA) + + front_camera_mock = mock_ring_devices.get_device(765432) + + state = hass.states.get("camera.front") + assert state is not None + + # history not updated yet + front_camera_mock.history.assert_not_called() + front_camera_mock.recording_url.assert_not_called() + with ( + patch( + "homeassistant.components.ring.camera.ffmpeg.async_get_image", + return_value=SMALLEST_VALID_JPEG_BYTES, + ), + pytest.raises(HomeAssistantError), + ): + image = await camera.async_get_image(hass, "camera.front") + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + # history updated so image available + front_camera_mock.history.assert_called_once() + front_camera_mock.recording_url.assert_called_once() + + with patch( + "homeassistant.components.ring.camera.ffmpeg.async_get_image", + return_value=SMALLEST_VALID_JPEG_BYTES, + ): + image = await camera.async_get_image(hass, "camera.front") + assert image.content == SMALLEST_VALID_JPEG_BYTES From 75b1700ed329380ef19fa4a60756bb35ef4cf89f Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Fri, 19 Jul 2024 14:49:11 -0300 Subject: [PATCH 1352/2411] Move constants to `const.py` in generic Thermostat (#120789) --- .../components/generic_thermostat/__init__.py | 5 +-- .../components/generic_thermostat/climate.py | 43 +++++++------------ .../generic_thermostat/config_flow.py | 2 +- .../components/generic_thermostat/const.py | 34 +++++++++++++++ .../generic_thermostat/test_climate.py | 2 +- .../generic_thermostat/test_config_flow.py | 4 +- .../generic_thermostat/test_init.py | 2 +- 7 files changed, 56 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/generic_thermostat/const.py diff --git a/homeassistant/components/generic_thermostat/__init__.py b/homeassistant/components/generic_thermostat/__init__.py index fcec36b8d35..dc43049a262 100644 --- a/homeassistant/components/generic_thermostat/__init__.py +++ b/homeassistant/components/generic_thermostat/__init__.py @@ -1,15 +1,12 @@ """The generic_thermostat component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device import ( async_remove_stale_devices_links_keep_entity_device, ) -CONF_HEATER = "heater" -DOMAIN = "generic_thermostat" -PLATFORMS = [Platform.CLIMATE] +from .const import CONF_HEATER, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 1b19def9cf4..22001b2acc4 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -14,13 +14,7 @@ import voluptuous as vol from homeassistant.components.climate import ( ATTR_PRESET_MODE, PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, - PRESET_ACTIVITY, - PRESET_AWAY, - PRESET_COMFORT, - PRESET_ECO, - PRESET_HOME, PRESET_NONE, - PRESET_SLEEP, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -64,36 +58,31 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolDictType -from . import CONF_HEATER, DOMAIN, PLATFORMS +from .const import ( + CONF_AC_MODE, + CONF_COLD_TOLERANCE, + CONF_HEATER, + CONF_HOT_TOLERANCE, + CONF_MIN_DUR, + CONF_PRESETS, + CONF_SENSOR, + DEFAULT_TOLERANCE, + DOMAIN, + PLATFORMS, +) _LOGGER = logging.getLogger(__name__) -DEFAULT_TOLERANCE = 0.3 DEFAULT_NAME = "Generic Thermostat" -CONF_SENSOR = "target_sensor" + +CONF_INITIAL_HVAC_MODE = "initial_hvac_mode" +CONF_KEEP_ALIVE = "keep_alive" CONF_MIN_TEMP = "min_temp" CONF_MAX_TEMP = "max_temp" -CONF_TARGET_TEMP = "target_temp" -CONF_AC_MODE = "ac_mode" -CONF_MIN_DUR = "min_cycle_duration" -CONF_COLD_TOLERANCE = "cold_tolerance" -CONF_HOT_TOLERANCE = "hot_tolerance" -CONF_KEEP_ALIVE = "keep_alive" -CONF_INITIAL_HVAC_MODE = "initial_hvac_mode" CONF_PRECISION = "precision" +CONF_TARGET_TEMP = "target_temp" CONF_TEMP_STEP = "target_temp_step" -CONF_PRESETS = { - p: f"{p}_temp" - for p in ( - PRESET_AWAY, - PRESET_COMFORT, - PRESET_ECO, - PRESET_HOME, - PRESET_SLEEP, - PRESET_ACTIVITY, - ) -} PRESETS_SCHEMA: VolDictType = { vol.Optional(v): vol.Coerce(float) for v in CONF_PRESETS.values() diff --git a/homeassistant/components/generic_thermostat/config_flow.py b/homeassistant/components/generic_thermostat/config_flow.py index 29e3d69c2da..e9079a9f41a 100644 --- a/homeassistant/components/generic_thermostat/config_flow.py +++ b/homeassistant/components/generic_thermostat/config_flow.py @@ -16,7 +16,7 @@ from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, ) -from .climate import ( +from .const import ( CONF_AC_MODE, CONF_COLD_TOLERANCE, CONF_HEATER, diff --git a/homeassistant/components/generic_thermostat/const.py b/homeassistant/components/generic_thermostat/const.py new file mode 100644 index 00000000000..51927297b63 --- /dev/null +++ b/homeassistant/components/generic_thermostat/const.py @@ -0,0 +1,34 @@ +"""Constants for the Generic Thermostat helper.""" + +from homeassistant.components.climate import ( + PRESET_ACTIVITY, + PRESET_AWAY, + PRESET_COMFORT, + PRESET_ECO, + PRESET_HOME, + PRESET_SLEEP, +) +from homeassistant.const import Platform + +DOMAIN = "generic_thermostat" + +PLATFORMS = [Platform.CLIMATE] + +CONF_AC_MODE = "ac_mode" +CONF_COLD_TOLERANCE = "cold_tolerance" +CONF_HEATER = "heater" +CONF_HOT_TOLERANCE = "hot_tolerance" +CONF_MIN_DUR = "min_cycle_duration" +CONF_PRESETS = { + p: f"{p}_temp" + for p in ( + PRESET_AWAY, + PRESET_COMFORT, + PRESET_ECO, + PRESET_HOME, + PRESET_SLEEP, + PRESET_ACTIVITY, + ) +} +CONF_SENSOR = "target_sensor" +DEFAULT_TOLERANCE = 0.3 diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 3ea38a22c3c..dcf1cd695e2 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -21,7 +21,7 @@ from homeassistant.components.climate import ( PRESET_SLEEP, HVACMode, ) -from homeassistant.components.generic_thermostat import ( +from homeassistant.components.generic_thermostat.const import ( DOMAIN as GENERIC_THERMOSTAT_DOMAIN, ) from homeassistant.const import ( diff --git a/tests/components/generic_thermostat/test_config_flow.py b/tests/components/generic_thermostat/test_config_flow.py index 81e06146a14..7a7fdabc6e6 100644 --- a/tests/components/generic_thermostat/test_config_flow.py +++ b/tests/components/generic_thermostat/test_config_flow.py @@ -6,12 +6,11 @@ from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.climate import PRESET_AWAY -from homeassistant.components.generic_thermostat.climate import ( +from homeassistant.components.generic_thermostat.const import ( CONF_AC_MODE, CONF_COLD_TOLERANCE, CONF_HEATER, CONF_HOT_TOLERANCE, - CONF_NAME, CONF_PRESETS, CONF_SENSOR, DOMAIN, @@ -21,6 +20,7 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, STATE_OFF, UnitOfTemperature, ) diff --git a/tests/components/generic_thermostat/test_init.py b/tests/components/generic_thermostat/test_init.py index 0d6e106237c..addae2f684e 100644 --- a/tests/components/generic_thermostat/test_init.py +++ b/tests/components/generic_thermostat/test_init.py @@ -2,7 +2,7 @@ from __future__ import annotations -from homeassistant.components.generic_thermostat import DOMAIN +from homeassistant.components.generic_thermostat.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er From e6e748ae0abe4fbad7430883b4c5fdc30a6ba555 Mon Sep 17 00:00:00 2001 From: Sean Chen Date: Fri, 19 Jul 2024 12:50:38 -0500 Subject: [PATCH 1353/2411] Add timestamp sensor for observation (#121752) --- homeassistant/components/nws/sensor.py | 11 ++++++++++- tests/components/nws/const.py | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index 872e1588244..d1992056d47 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime from types import MappingProxyType from typing import Any @@ -28,6 +29,7 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, TimestampDataUpdateCoordinator, ) +from homeassistant.util.dt import parse_datetime from homeassistant.util.unit_conversion import ( DistanceConverter, PressureConverter, @@ -137,6 +139,11 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfLength.METERS, unit_convert=UnitOfLength.MILES, ), + NWSSensorEntityDescription( + key="timestamp", + name="Latest Observation Time", + device_class=SensorDeviceClass.TIMESTAMP, + ), ) @@ -190,7 +197,7 @@ class NWSSensor(CoordinatorEntity[TimestampDataUpdateCoordinator[None]], SensorE ) @property - def native_value(self) -> float | None: + def native_value(self) -> float | datetime | None: """Return the state.""" if ( not (observation := self._nws.observation) @@ -223,4 +230,6 @@ class NWSSensor(CoordinatorEntity[TimestampDataUpdateCoordinator[None]], SensorE return round(value, 1) if unit_of_measurement == PERCENTAGE: return round(value) + if self.device_class == SensorDeviceClass.TIMESTAMP: + return parse_datetime(value) return value diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py index 06aef2c8da7..39e954af15a 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -66,6 +66,7 @@ CLEAR_NIGHT_OBSERVATION = DEFAULT_OBSERVATION.copy() CLEAR_NIGHT_OBSERVATION["iconTime"] = "night" SENSOR_EXPECTED_OBSERVATION_METRIC = { + "timestamp": "2019-08-12T23:53:00+00:00", "dewpoint": "5", "temperature": "10", "windChill": "5", @@ -80,6 +81,7 @@ SENSOR_EXPECTED_OBSERVATION_METRIC = { } SENSOR_EXPECTED_OBSERVATION_IMPERIAL = { + "timestamp": "2019-08-12T23:53:00+00:00", "dewpoint": str( round( TemperatureConverter.convert( From 7e0970e9173c4cdaeb23f7d0e95dfd8a2c0fb0c1 Mon Sep 17 00:00:00 2001 From: HarvsG <11440490+HarvsG@users.noreply.github.com> Date: Fri, 19 Jul 2024 19:43:38 +0100 Subject: [PATCH 1354/2411] Log timeouts for `assist_pipeline` end of speech detection (#122182) * log timeouts * import logger the right way --- homeassistant/components/assist_pipeline/vad.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/assist_pipeline/vad.py b/homeassistant/components/assist_pipeline/vad.py index 6dacd2ff8e9..5b3d1408f58 100644 --- a/homeassistant/components/assist_pipeline/vad.py +++ b/homeassistant/components/assist_pipeline/vad.py @@ -6,8 +6,11 @@ from abc import ABC, abstractmethod from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum +import logging from typing import Final, cast +_LOGGER = logging.getLogger(__name__) + _SAMPLE_RATE: Final = 16000 # Hz _SAMPLE_WIDTH: Final = 2 # bytes @@ -159,6 +162,10 @@ class VoiceCommandSegmenter: """ self._timeout_seconds_left -= chunk_seconds if self._timeout_seconds_left <= 0: + _LOGGER.warning( + "VAD end of speech detection timed out after %s seconds", + self.timeout_seconds, + ) self.reset() return False From 288faf48e7d9228dda8d389a0299d6b4335e1259 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 19 Jul 2024 21:20:43 +0200 Subject: [PATCH 1355/2411] Add config flow to Wake on LAN (#121605) --- .../components/wake_on_lan/__init__.py | 23 +++- .../components/wake_on_lan/button.py | 87 ++++++++++++++ .../components/wake_on_lan/config_flow.py | 80 +++++++++++++ homeassistant/components/wake_on_lan/const.py | 8 ++ .../components/wake_on_lan/manifest.json | 1 + .../components/wake_on_lan/strings.json | 48 +++++++- .../components/wake_on_lan/switch.py | 13 +-- homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- tests/components/wake_on_lan/conftest.py | 57 ++++++++- tests/components/wake_on_lan/test_button.py | 54 +++++++++ .../wake_on_lan/test_config_flow.py | 109 ++++++++++++++++++ tests/components/wake_on_lan/test_init.py | 12 ++ tests/components/wake_on_lan/test_switch.py | 10 +- 14 files changed, 483 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/wake_on_lan/button.py create mode 100644 homeassistant/components/wake_on_lan/config_flow.py create mode 100644 tests/components/wake_on_lan/test_button.py create mode 100644 tests/components/wake_on_lan/test_config_flow.py diff --git a/homeassistant/components/wake_on_lan/__init__.py b/homeassistant/components/wake_on_lan/__init__.py index 37837da683a..efd72c4564c 100644 --- a/homeassistant/components/wake_on_lan/__init__.py +++ b/homeassistant/components/wake_on_lan/__init__.py @@ -6,12 +6,13 @@ import logging import voluptuous as vol import wakeonlan +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -43,7 +44,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if broadcast_port is not None: service_kwargs["port"] = broadcast_port - _LOGGER.info( + _LOGGER.debug( "Send magic packet to mac %s (broadcast: %s, port: %s)", mac_address, broadcast_address, @@ -62,3 +63,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a Wake on LAN component entry.""" + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/wake_on_lan/button.py b/homeassistant/components/wake_on_lan/button.py new file mode 100644 index 00000000000..0818fd11f08 --- /dev/null +++ b/homeassistant/components/wake_on_lan/button.py @@ -0,0 +1,87 @@ +"""Support for button entity in wake on lan.""" + +from __future__ import annotations + +from functools import partial +import logging +from typing import Any + +import wakeonlan + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Wake on LAN sensor entry.""" + broadcast_address: str | None = entry.options.get(CONF_BROADCAST_ADDRESS) + broadcast_port: int | None = entry.options.get(CONF_BROADCAST_PORT) + mac_address: str = entry.options[CONF_MAC] + name: str = entry.title + + async_add_entities( + [ + WolSwitch( + name, + mac_address, + broadcast_address, + broadcast_port, + ) + ] + ) + + +class WolSwitch(ButtonEntity): + """Representation of a wake on lan button.""" + + _attr_name = None + + def __init__( + self, + name: str, + mac_address: str, + broadcast_address: str | None, + broadcast_port: int | None, + ) -> None: + """Initialize the WOL button.""" + self._mac_address = mac_address + self._broadcast_address = broadcast_address + self._broadcast_port = broadcast_port + self._attr_unique_id = dr.format_mac(mac_address) + self._attr_device_info = dr.DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self._attr_unique_id)}, + identifiers={(DOMAIN, self._attr_unique_id)}, + manufacturer="Wake on LAN", + name=name, + ) + + async def async_press(self) -> None: + """Press the button.""" + service_kwargs: dict[str, Any] = {} + if self._broadcast_address is not None: + service_kwargs["ip_address"] = self._broadcast_address + if self._broadcast_port is not None: + service_kwargs["port"] = self._broadcast_port + + _LOGGER.debug( + "Send magic packet to mac %s (broadcast: %s, port: %s)", + self._mac_address, + self._broadcast_address, + self._broadcast_port, + ) + + await self.hass.async_add_executor_job( + partial(wakeonlan.send_magic_packet, self._mac_address, **service_kwargs) + ) diff --git a/homeassistant/components/wake_on_lan/config_flow.py b/homeassistant/components/wake_on_lan/config_flow.py new file mode 100644 index 00000000000..a7c406cefb7 --- /dev/null +++ b/homeassistant/components/wake_on_lan/config_flow.py @@ -0,0 +1,80 @@ +"""Config flow for Wake on lan integration.""" + +from collections.abc import Mapping +from typing import Any + +import voluptuous as vol + +from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, + SchemaConfigFlowHandler, + SchemaFlowFormStep, +) +from homeassistant.helpers.selector import ( + NumberSelector, + NumberSelectorConfig, + NumberSelectorMode, + TextSelector, +) + +from .const import DEFAULT_NAME, DOMAIN + + +async def validate( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate input setup.""" + user_input = await validate_options(handler, user_input) + + user_input[CONF_MAC] = dr.format_mac(user_input[CONF_MAC]) + + # Mac address needs to be unique + handler.parent_handler._async_abort_entries_match({CONF_MAC: user_input[CONF_MAC]}) # noqa: SLF001 + + return user_input + + +async def validate_options( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate input options.""" + if CONF_BROADCAST_PORT in user_input: + # Convert float to int for broadcast port + user_input[CONF_BROADCAST_PORT] = int(user_input[CONF_BROADCAST_PORT]) + return user_input + + +DATA_SCHEMA = {vol.Required(CONF_MAC): TextSelector()} +OPTIONS_SCHEMA = { + vol.Optional(CONF_BROADCAST_ADDRESS): TextSelector(), + vol.Optional(CONF_BROADCAST_PORT): NumberSelector( + NumberSelectorConfig(min=0, max=65535, step=1, mode=NumberSelectorMode.BOX) + ), +} + + +CONFIG_FLOW = { + "user": SchemaFlowFormStep( + schema=vol.Schema(DATA_SCHEMA).extend(OPTIONS_SCHEMA), + validate_user_input=validate, + ) +} +OPTIONS_FLOW = { + "init": SchemaFlowFormStep( + vol.Schema(OPTIONS_SCHEMA), validate_user_input=validate_options + ), +} + + +class StatisticsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config flow for Statistics.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + mac: str = options[CONF_MAC] + return f"{DEFAULT_NAME} {mac}" diff --git a/homeassistant/components/wake_on_lan/const.py b/homeassistant/components/wake_on_lan/const.py index 2560ef40382..20b9573cfde 100644 --- a/homeassistant/components/wake_on_lan/const.py +++ b/homeassistant/components/wake_on_lan/const.py @@ -1,3 +1,11 @@ """Constants for the Wake-On-LAN component.""" +from homeassistant.const import Platform + DOMAIN = "wake_on_lan" +PLATFORMS = [Platform.BUTTON] + +CONF_OFF_ACTION = "turn_off" + +DEFAULT_NAME = "Wake on LAN" +DEFAULT_PING_TIMEOUT = 1 diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json index a62980b3010..c716a851ae4 100644 --- a/homeassistant/components/wake_on_lan/manifest.json +++ b/homeassistant/components/wake_on_lan/manifest.json @@ -2,6 +2,7 @@ "domain": "wake_on_lan", "name": "Wake on LAN", "codeowners": ["@ntilley905"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wake_on_lan", "iot_class": "local_push", "requirements": ["wakeonlan==2.1.0"] diff --git a/homeassistant/components/wake_on_lan/strings.json b/homeassistant/components/wake_on_lan/strings.json index 8395bc7503a..89bc30e405a 100644 --- a/homeassistant/components/wake_on_lan/strings.json +++ b/homeassistant/components/wake_on_lan/strings.json @@ -1,20 +1,56 @@ { + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, + "step": { + "user": { + "data": { + "mac": "MAC address", + "broadcast_address": "Broadcast address", + "broadcast_port": "Broadcast port" + }, + "data_description": { + "mac": "MAC address of the device to wake up.", + "broadcast_address": "The IP address of the host to send the magic packet to. Defaults to `255.255.255.255` and is normally not changed.", + "broadcast_port": "The port to send the magic packet to. Defaults to `9` and is normally not changed." + } + } + } + }, + "options": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, + "step": { + "init": { + "data": { + "broadcast_address": "[%key:component::wake_on_lan::config::step::user::data::broadcast_address%]", + "broadcast_port": "[%key:component::wake_on_lan::config::step::user::data::broadcast_port%]" + }, + "data_description": { + "broadcast_address": "[%key:component::wake_on_lan::config::step::user::data_description::broadcast_address%]", + "broadcast_port": "[%key:component::wake_on_lan::config::step::user::data_description::broadcast_port%]" + } + } + } + }, "services": { "send_magic_packet": { "name": "Send magic packet", "description": "Sends a 'magic packet' to wake up a device with 'Wake-On-LAN' capabilities.", "fields": { "mac": { - "name": "MAC address", - "description": "MAC address of the device to wake up." + "name": "[%key:component::wake_on_lan::config::step::user::data::mac%]", + "description": "[%key:component::wake_on_lan::config::step::user::data_description::mac%]" }, "broadcast_address": { - "name": "Broadcast address", - "description": "Broadcast IP where to send the magic packet." + "name": "[%key:component::wake_on_lan::config::step::user::data::broadcast_address%]", + "description": "[%key:component::wake_on_lan::config::step::user::data_description::broadcast_address%]" }, "broadcast_port": { - "name": "Broadcast port", - "description": "Port where to send the magic packet." + "name": "[%key:component::wake_on_lan::config::step::user::data::broadcast_port%]", + "description": "[%key:component::wake_on_lan::config::step::user::data_description::broadcast_port%]" } } } diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index cf38d05de38..f4949ec6901 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -27,15 +27,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN +from .const import CONF_OFF_ACTION, DEFAULT_NAME, DEFAULT_PING_TIMEOUT, DOMAIN _LOGGER = logging.getLogger(__name__) -CONF_OFF_ACTION = "turn_off" - -DEFAULT_NAME = "Wake on LAN" -DEFAULT_PING_TIMEOUT = 1 - PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): cv.string, @@ -48,10 +43,10 @@ PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up a wake on lan switch.""" @@ -62,7 +57,7 @@ def setup_platform( name: str = config[CONF_NAME] off_action: list[Any] | None = config.get(CONF_OFF_ACTION) - add_entities( + async_add_entities( [ WolSwitch( hass, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7556bbb7ddc..b8614705823 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -625,6 +625,7 @@ FLOWS = { "volumio", "volvooncall", "vulcan", + "wake_on_lan", "wallbox", "waqi", "watttime", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 5d144bf5654..84d69c868db 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6743,7 +6743,7 @@ "wake_on_lan": { "name": "Wake on LAN", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_push" }, "wallbox": { diff --git a/tests/components/wake_on_lan/conftest.py b/tests/components/wake_on_lan/conftest.py index c3c58ec4c69..8a1cb3f41eb 100644 --- a/tests/components/wake_on_lan/conftest.py +++ b/tests/components/wake_on_lan/conftest.py @@ -3,13 +3,23 @@ from __future__ import annotations from collections.abc import Generator +from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest +from homeassistant.components.wake_on_lan.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +DEFAULT_MAC = "00:01:02:03:04:05" + @pytest.fixture -def mock_send_magic_packet() -> AsyncMock: +def mock_send_magic_packet() -> Generator[AsyncMock]: """Mock magic packet.""" with patch("wakeonlan.send_magic_packet") as mock_send: yield mock_send @@ -27,3 +37,48 @@ def mock_subprocess_call(subprocess_call_return_value: int) -> Generator[MagicMo with patch("homeassistant.components.wake_on_lan.switch.sp.call") as mock_sp: mock_sp.return_value = subprocess_call_return_value yield mock_sp + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Automatically path uuid generator.""" + with patch( + "homeassistant.components.wake_on_lan.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(name="get_config") +async def get_config_to_integration_load() -> dict[str, Any]: + """Return configuration. + + To override the config, tests can be marked with: + @pytest.mark.parametrize("get_config", [{...}]) + """ + return { + CONF_MAC: DEFAULT_MAC, + CONF_BROADCAST_ADDRESS: "255.255.255.255", + CONF_BROADCAST_PORT: 9, + } + + +@pytest.fixture(name="loaded_entry") +async def load_integration( + hass: HomeAssistant, get_config: dict[str, Any] +) -> MockConfigEntry: + """Set up the Statistics integration in Home Assistant.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title=f"Wake on LAN {DEFAULT_MAC}", + source=SOURCE_USER, + options=get_config, + entry_id="1", + ) + + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/wake_on_lan/test_button.py b/tests/components/wake_on_lan/test_button.py new file mode 100644 index 00000000000..abcae686a1b --- /dev/null +++ b/tests/components/wake_on_lan/test_button.py @@ -0,0 +1,54 @@ +"""The tests for the wake on lan button platform.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +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 tests.common import MockConfigEntry + + +async def test_state( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + loaded_entry: MockConfigEntry, +) -> None: + """Test button default state.""" + + state = hass.states.get("button.wake_on_lan_00_01_02_03_04_05") + assert state is not None + assert state.state == STATE_UNKNOWN + + entry = entity_registry.async_get("button.wake_on_lan_00_01_02_03_04_05") + assert entry + assert entry.unique_id == "00:01:02:03:04:05" + + +async def test_service_calls( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + loaded_entry: MockConfigEntry, + mock_send_magic_packet: AsyncMock, +) -> None: + """Test service call.""" + + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + freezer.move_to(now) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.wake_on_lan_00_01_02_03_04_05"}, + blocking=True, + ) + + assert ( + hass.states.get("button.wake_on_lan_00_01_02_03_04_05").state == now.isoformat() + ) diff --git a/tests/components/wake_on_lan/test_config_flow.py b/tests/components/wake_on_lan/test_config_flow.py new file mode 100644 index 00000000000..b565fba505e --- /dev/null +++ b/tests/components/wake_on_lan/test_config_flow.py @@ -0,0 +1,109 @@ +"""Test the Scrape config flow.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +from homeassistant import config_entries +from homeassistant.components.wake_on_lan.const import DOMAIN +from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .conftest import DEFAULT_MAC + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_MAC: DEFAULT_MAC, + CONF_BROADCAST_ADDRESS: "255.255.255.255", + CONF_BROADCAST_PORT: 9, + }, + ) + await hass.async_block_till_done(wait_background_tasks=True) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["version"] == 1 + assert result["options"] == { + CONF_MAC: DEFAULT_MAC, + CONF_BROADCAST_ADDRESS: "255.255.255.255", + CONF_BROADCAST_PORT: 9, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_options_flow(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: + """Test options flow.""" + + result = await hass.config_entries.options.async_init(loaded_entry.entry_id) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_BROADCAST_ADDRESS: "192.168.255.255", + CONF_BROADCAST_PORT: 10, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_MAC: DEFAULT_MAC, + CONF_BROADCAST_ADDRESS: "192.168.255.255", + CONF_BROADCAST_PORT: 10, + } + + await hass.async_block_till_done() + + assert loaded_entry.options == { + CONF_MAC: DEFAULT_MAC, + CONF_BROADCAST_ADDRESS: "192.168.255.255", + CONF_BROADCAST_PORT: 10, + } + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 1 + + state = hass.states.get("button.wake_on_lan_00_01_02_03_04_05") + assert state is not None + + +async def test_entry_already_exist( + hass: HomeAssistant, loaded_entry: MockConfigEntry +) -> None: + """Test abort when entry already exist.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_MAC: DEFAULT_MAC, + CONF_BROADCAST_ADDRESS: "255.255.255.255", + CONF_BROADCAST_PORT: 9, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/wake_on_lan/test_init.py b/tests/components/wake_on_lan/test_init.py index 8cfb0e6491e..1784f8ef12d 100644 --- a/tests/components/wake_on_lan/test_init.py +++ b/tests/components/wake_on_lan/test_init.py @@ -8,9 +8,21 @@ import pytest import voluptuous as vol from homeassistant.components.wake_on_lan import DOMAIN, SERVICE_SEND_MAGIC_PACKET +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: + """Test unload an entry.""" + + assert loaded_entry.state is ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(loaded_entry.entry_id) + await hass.async_block_till_done() + assert loaded_entry.state is ConfigEntryState.NOT_LOADED + async def test_send_magic_packet(hass: HomeAssistant) -> None: """Test of send magic packet service call.""" diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index 77e1ba55519..9a478b46175 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -13,6 +13,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -64,7 +65,7 @@ async def test_broadcast_config_ip_and_port( hass: HomeAssistant, mock_send_magic_packet: AsyncMock ) -> None: """Test with broadcast address and broadcast port config.""" - mac = "00-01-02-03-04-05" + mac = "00:01:02:03:04:05" broadcast_address = "255.255.255.255" port = 999 @@ -92,6 +93,7 @@ async def test_broadcast_config_ip_and_port( blocking=True, ) + mac = dr.format_mac(mac) mock_send_magic_packet.assert_called_with( mac, ip_address=broadcast_address, port=port ) @@ -102,7 +104,7 @@ async def test_broadcast_config_ip( ) -> None: """Test with only broadcast address.""" - mac = "00-01-02-03-04-05" + mac = "00:01:02:03:04:05" broadcast_address = "255.255.255.255" assert await async_setup_component( @@ -128,6 +130,7 @@ async def test_broadcast_config_ip( blocking=True, ) + mac = dr.format_mac(mac) mock_send_magic_packet.assert_called_with(mac, ip_address=broadcast_address) @@ -136,7 +139,7 @@ async def test_broadcast_config_port( ) -> None: """Test with only broadcast port config.""" - mac = "00-01-02-03-04-05" + mac = "00:01:02:03:04:05" port = 999 assert await async_setup_component( @@ -156,6 +159,7 @@ async def test_broadcast_config_port( blocking=True, ) + mac = dr.format_mac(mac) mock_send_magic_packet.assert_called_with(mac, port=port) From a46fffd55025661fcdb4d726748a7548606fb964 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Sat, 20 Jul 2024 00:40:29 +0200 Subject: [PATCH 1356/2411] Fix wrong deprecation date in Habitica integration (#122206) Fix wrong deprecation date --- homeassistant/components/habitica/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 4ce507afffd..5696e6f9911 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -136,7 +136,7 @@ "issues": { "deprecated_task_entity": { "title": "The Habitica `{task_name}` sensor is deprecated", - "description": "The Habitica entity `{entity}` is deprecated and will be removed in `2024.12`.\nPlease update your automations and scripts to replace the sensor entity with the newly added todo entity.\nWhen you are done migrating you can disable `{entity}`." + "description": "The Habitica entity `{entity}` is deprecated and will be removed in a future release.\nPlease update your automations and scripts to replace the sensor entity with the newly added todo entity.\nWhen you are done migrating you can disable `{entity}`." } }, "services": { From 8e024ad20f96bd875b45784af098e13091b6bd7d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 02:46:27 +0200 Subject: [PATCH 1357/2411] Fix invalid Any annotations (#122212) --- tests/components/lastfm/__init__.py | 3 ++- tests/components/unifi/test_sensor.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/lastfm/__init__.py b/tests/components/lastfm/__init__.py index 9fe946f8dff..e4eb476f62d 100644 --- a/tests/components/lastfm/__init__.py +++ b/tests/components/lastfm/__init__.py @@ -1,5 +1,6 @@ """The tests for lastfm.""" +from typing import Any from unittest.mock import patch from pylast import PyLastError, Track @@ -91,7 +92,7 @@ class MockUser: """Get mock now playing.""" return self._now_playing_result - def get_friends(self) -> list[any]: + def get_friends(self) -> list[Any]: """Get mock friends.""" if len(self._friends) == 0: raise PyLastError("network", "status", "Page not found") diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 281c4583399..e1893922f60 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -740,9 +740,9 @@ async def test_outlet_power_readings( device_payload: list[dict[str, Any]], entity_id: str, expected_unique_id: str, - expected_value: any, + expected_value: Any, changed_data: dict | None, - expected_update_value: any, + expected_update_value: Any, ) -> None: """Test the outlet power reporting on PDU devices.""" assert len(hass.states.async_all()) == 13 From 458c81cdae7867ca1fd1fc415d1e5fe1c7b7c156 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 02:50:12 +0200 Subject: [PATCH 1358/2411] Improve vizio tests typing (#122213) --- tests/components/vizio/conftest.py | 35 +-- tests/components/vizio/test_config_flow.py | 269 +++++++------------- tests/components/vizio/test_init.py | 27 +- tests/components/vizio/test_media_player.py | 108 +++----- 4 files changed, 157 insertions(+), 282 deletions(-) diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index b06ce2e1eb7..f33c7839c72 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -1,5 +1,6 @@ """Configure py.test.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest @@ -35,13 +36,13 @@ class MockInput: self.name = name -def get_mock_inputs(input_list): +def get_mock_inputs(input_list) -> list[MockInput]: """Return list of MockInput.""" return [MockInput(device_input) for device_input in input_list] @pytest.fixture(name="vizio_get_unique_id", autouse=True) -def vizio_get_unique_id_fixture(): +def vizio_get_unique_id_fixture() -> Generator[None]: """Mock get vizio unique ID.""" with patch( "homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id", @@ -51,7 +52,7 @@ def vizio_get_unique_id_fixture(): @pytest.fixture(name="vizio_data_coordinator_update", autouse=True) -def vizio_data_coordinator_update_fixture(): +def vizio_data_coordinator_update_fixture() -> Generator[None]: """Mock get data coordinator update.""" with patch( "homeassistant.components.vizio.coordinator.gen_apps_list_from_url", @@ -61,7 +62,7 @@ def vizio_data_coordinator_update_fixture(): @pytest.fixture(name="vizio_data_coordinator_update_failure") -def vizio_data_coordinator_update_failure_fixture(): +def vizio_data_coordinator_update_failure_fixture() -> Generator[None]: """Mock get data coordinator update failure.""" with patch( "homeassistant.components.vizio.coordinator.gen_apps_list_from_url", @@ -71,7 +72,7 @@ def vizio_data_coordinator_update_failure_fixture(): @pytest.fixture(name="vizio_no_unique_id") -def vizio_no_unique_id_fixture(): +def vizio_no_unique_id_fixture() -> Generator[None]: """Mock no vizio unique ID returrned.""" with patch( "homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id", @@ -81,7 +82,7 @@ def vizio_no_unique_id_fixture(): @pytest.fixture(name="vizio_connect") -def vizio_connect_fixture(): +def vizio_connect_fixture() -> Generator[None]: """Mock valid vizio device and entry setup.""" with patch( "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", @@ -91,7 +92,7 @@ def vizio_connect_fixture(): @pytest.fixture(name="vizio_complete_pairing") -def vizio_complete_pairing_fixture(): +def vizio_complete_pairing_fixture() -> Generator[None]: """Mock complete vizio pairing workflow.""" with ( patch( @@ -107,7 +108,7 @@ def vizio_complete_pairing_fixture(): @pytest.fixture(name="vizio_start_pairing_failure") -def vizio_start_pairing_failure_fixture(): +def vizio_start_pairing_failure_fixture() -> Generator[None]: """Mock vizio start pairing failure.""" with patch( "homeassistant.components.vizio.config_flow.VizioAsync.start_pair", @@ -117,7 +118,7 @@ def vizio_start_pairing_failure_fixture(): @pytest.fixture(name="vizio_invalid_pin_failure") -def vizio_invalid_pin_failure_fixture(): +def vizio_invalid_pin_failure_fixture() -> Generator[None]: """Mock vizio failure due to invalid pin.""" with ( patch( @@ -133,14 +134,14 @@ def vizio_invalid_pin_failure_fixture(): @pytest.fixture(name="vizio_bypass_setup") -def vizio_bypass_setup_fixture(): +def vizio_bypass_setup_fixture() -> Generator[None]: """Mock component setup.""" with patch("homeassistant.components.vizio.async_setup_entry", return_value=True): yield @pytest.fixture(name="vizio_bypass_update") -def vizio_bypass_update_fixture(): +def vizio_bypass_update_fixture() -> Generator[None]: """Mock component update.""" with ( patch( @@ -153,7 +154,7 @@ def vizio_bypass_update_fixture(): @pytest.fixture(name="vizio_guess_device_type") -def vizio_guess_device_type_fixture(): +def vizio_guess_device_type_fixture() -> Generator[None]: """Mock vizio async_guess_device_type function.""" with patch( "homeassistant.components.vizio.config_flow.async_guess_device_type", @@ -163,7 +164,7 @@ def vizio_guess_device_type_fixture(): @pytest.fixture(name="vizio_cant_connect") -def vizio_cant_connect_fixture(): +def vizio_cant_connect_fixture() -> Generator[None]: """Mock vizio device can't connect with valid auth.""" with ( patch( @@ -179,7 +180,7 @@ def vizio_cant_connect_fixture(): @pytest.fixture(name="vizio_update") -def vizio_update_fixture(): +def vizio_update_fixture() -> Generator[None]: """Mock valid updates to vizio device.""" with ( patch( @@ -223,7 +224,7 @@ def vizio_update_fixture(): @pytest.fixture(name="vizio_update_with_apps") -def vizio_update_with_apps_fixture(vizio_update: pytest.fixture): +def vizio_update_with_apps_fixture(vizio_update: None) -> Generator[None]: """Mock valid updates to vizio device that supports apps.""" with ( patch( @@ -243,7 +244,7 @@ def vizio_update_with_apps_fixture(vizio_update: pytest.fixture): @pytest.fixture(name="vizio_update_with_apps_on_input") -def vizio_update_with_apps_on_input_fixture(vizio_update: pytest.fixture): +def vizio_update_with_apps_on_input_fixture(vizio_update: None) -> Generator[None]: """Mock valid updates to vizio device that supports apps but is on a TV input.""" with ( patch( @@ -263,7 +264,7 @@ def vizio_update_with_apps_on_input_fixture(vizio_update: pytest.fixture): @pytest.fixture(name="vizio_hostname_check") -def vizio_hostname_check(): +def vizio_hostname_check() -> Generator[None]: """Mock vizio hostname resolution.""" with patch( "homeassistant.components.vizio.config_flow.socket.gethostbyname", diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 712dd2a31b5..42d4394ca80 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -57,11 +57,8 @@ from .const import ( from tests.common import MockConfigEntry -async def test_user_flow_minimum_fields( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_setup") +async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: """Test user config flow with minimum fields.""" # test form shows result = await hass.config_entries.flow.async_init( @@ -81,11 +78,8 @@ async def test_user_flow_minimum_fields( assert result["data"][CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.SPEAKER -async def test_user_flow_all_fields( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_setup") +async def test_user_flow_all_fields(hass: HomeAssistant) -> None: """Test user config flow with all fields.""" # test form shows result = await hass.config_entries.flow.async_init( @@ -108,11 +102,8 @@ async def test_user_flow_all_fields( assert CONF_APPS not in result["data"] -async def test_speaker_options_flow( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_update") +async def test_speaker_options_flow(hass: HomeAssistant) -> None: """Test options config flow for speaker.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_SPEAKER_CONFIG @@ -136,11 +127,8 @@ async def test_speaker_options_flow( assert CONF_APPS not in result["data"] -async def test_tv_options_flow_no_apps( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_update") +async def test_tv_options_flow_no_apps(hass: HomeAssistant) -> None: """Test options config flow for TV without providing apps option.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG @@ -167,11 +155,8 @@ async def test_tv_options_flow_no_apps( assert CONF_APPS not in result["data"] -async def test_tv_options_flow_with_apps( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_update") +async def test_tv_options_flow_with_apps(hass: HomeAssistant) -> None: """Test options config flow for TV with providing apps option.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG @@ -199,11 +184,8 @@ async def test_tv_options_flow_with_apps( assert result["data"][CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]} -async def test_tv_options_flow_start_with_volume( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_update") +async def test_tv_options_flow_start_with_volume(hass: HomeAssistant) -> None: """Test options config flow for TV with providing apps option after providing volume step in initial config.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG @@ -241,11 +223,8 @@ async def test_tv_options_flow_start_with_volume( assert result["data"][CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]} -async def test_user_host_already_configured( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_setup") +async def test_user_host_already_configured(hass: HomeAssistant) -> None: """Test host is already configured during user setup.""" entry = MockConfigEntry( domain=DOMAIN, @@ -265,11 +244,8 @@ async def test_user_host_already_configured( assert result["errors"] == {CONF_HOST: "existing_config_entry_found"} -async def test_user_serial_number_already_exists( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_setup") +async def test_user_serial_number_already_exists(hass: HomeAssistant) -> None: """Test serial_number is already configured with different host and name during user setup.""" # Set up new entry MockConfigEntry( @@ -289,9 +265,8 @@ async def test_user_serial_number_already_exists( assert result["errors"] == {CONF_HOST: "existing_config_entry_found"} -async def test_user_error_on_could_not_connect( - hass: HomeAssistant, vizio_no_unique_id: pytest.fixture -) -> None: +@pytest.mark.usefixtures("vizio_no_unique_id") +async def test_user_error_on_could_not_connect(hass: HomeAssistant) -> None: """Test with could_not_connect during user setup due to no connectivity.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG @@ -301,8 +276,9 @@ async def test_user_error_on_could_not_connect( assert result["errors"] == {CONF_HOST: "cannot_connect"} +@pytest.mark.usefixtures("vizio_cant_connect") async def test_user_error_on_could_not_connect_invalid_token( - hass: HomeAssistant, vizio_cant_connect: pytest.fixture + hass: HomeAssistant, ) -> None: """Test with could_not_connect during user setup due to invalid token.""" result = await hass.config_entries.flow.async_init( @@ -313,12 +289,10 @@ async def test_user_error_on_could_not_connect_invalid_token( assert result["errors"] == {"base": "cannot_connect"} -async def test_user_tv_pairing_no_apps( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, - vizio_complete_pairing: pytest.fixture, -) -> None: +@pytest.mark.usefixtures( + "vizio_connect", "vizio_bypass_setup", "vizio_complete_pairing" +) +async def test_user_tv_pairing_no_apps(hass: HomeAssistant) -> None: """Test pairing config flow when access token not provided for tv during user entry and no apps configured.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN @@ -344,12 +318,10 @@ async def test_user_tv_pairing_no_apps( assert CONF_APPS not in result["data"] -async def test_user_start_pairing_failure( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, - vizio_start_pairing_failure: pytest.fixture, -) -> None: +@pytest.mark.usefixtures( + "vizio_connect", "vizio_bypass_setup", "vizio_start_pairing_failure" +) +async def test_user_start_pairing_failure(hass: HomeAssistant) -> None: """Test failure to start pairing from user config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN @@ -360,12 +332,10 @@ async def test_user_start_pairing_failure( assert result["errors"] == {"base": "cannot_connect"} -async def test_user_invalid_pin( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, - vizio_invalid_pin_failure: pytest.fixture, -) -> None: +@pytest.mark.usefixtures( + "vizio_connect", "vizio_bypass_setup", "vizio_invalid_pin_failure" +) +async def test_user_invalid_pin(hass: HomeAssistant) -> None: """Test failure to complete pairing from user config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN @@ -383,11 +353,8 @@ async def test_user_invalid_pin( assert result["errors"] == {CONF_PIN: "complete_pairing_failed"} -async def test_user_ignore( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_setup") +async def test_user_ignore(hass: HomeAssistant) -> None: """Test user config flow doesn't throw an error when there's an existing ignored source.""" entry = MockConfigEntry( domain=DOMAIN, @@ -403,11 +370,8 @@ async def test_user_ignore( assert result["type"] is FlowResultType.CREATE_ENTRY -async def test_import_flow_minimum_fields( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_setup") +async def test_import_flow_minimum_fields(hass: HomeAssistant) -> None: """Test import config flow with minimum fields.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -425,11 +389,8 @@ async def test_import_flow_minimum_fields( assert result["data"][CONF_VOLUME_STEP] == DEFAULT_VOLUME_STEP -async def test_import_flow_all_fields( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_setup") +async def test_import_flow_all_fields(hass: HomeAssistant) -> None: """Test import config flow with all fields.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -446,11 +407,8 @@ async def test_import_flow_all_fields( assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP -async def test_import_entity_already_configured( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_setup") +async def test_import_entity_already_configured(hass: HomeAssistant) -> None: """Test entity is already configured during import setup.""" entry = MockConfigEntry( domain=DOMAIN, @@ -468,11 +426,8 @@ async def test_import_entity_already_configured( assert result["reason"] == "already_configured_device" -async def test_import_flow_update_options( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_update") +async def test_import_flow_update_options(hass: HomeAssistant) -> None: """Test import config flow with updated options.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -499,11 +454,8 @@ async def test_import_flow_update_options( assert config_entry.options[CONF_VOLUME_STEP] == VOLUME_STEP + 1 -async def test_import_flow_update_name_and_apps( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_update") +async def test_import_flow_update_name_and_apps(hass: HomeAssistant) -> None: """Test import config flow with updated name and apps.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -533,11 +485,8 @@ async def test_import_flow_update_name_and_apps( assert config_entry.options[CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]} -async def test_import_flow_update_remove_apps( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_update") +async def test_import_flow_update_remove_apps(hass: HomeAssistant) -> None: """Test import config flow with removed apps.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -566,12 +515,10 @@ async def test_import_flow_update_remove_apps( assert CONF_APPS not in config_entry.options -async def test_import_needs_pairing( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, - vizio_complete_pairing: pytest.fixture, -) -> None: +@pytest.mark.usefixtures( + "vizio_connect", "vizio_bypass_setup", "vizio_complete_pairing" +) +async def test_import_needs_pairing(hass: HomeAssistant) -> None: """Test pairing config flow when access token not provided for tv during import.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_TV_CONFIG_NO_TOKEN @@ -603,12 +550,10 @@ async def test_import_needs_pairing( assert result["data"][CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV -async def test_import_with_apps_needs_pairing( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, - vizio_complete_pairing: pytest.fixture, -) -> None: +@pytest.mark.usefixtures( + "vizio_connect", "vizio_bypass_setup", "vizio_complete_pairing" +) +async def test_import_with_apps_needs_pairing(hass: HomeAssistant) -> None: """Test pairing config flow when access token not provided for tv but apps are included during import.""" import_config = MOCK_TV_CONFIG_NO_TOKEN.copy() import_config[CONF_APPS] = {CONF_INCLUDE: [CURRENT_APP]} @@ -646,11 +591,8 @@ async def test_import_with_apps_needs_pairing( assert result["data"][CONF_APPS][CONF_INCLUDE] == [CURRENT_APP] -async def test_import_flow_additional_configs( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_update") +async def test_import_flow_additional_configs(hass: HomeAssistant) -> None: """Test import config flow with additional configs defined in CONF_APPS.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -666,10 +608,9 @@ async def test_import_flow_additional_configs( assert CONF_APPS not in config_entry.options +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_setup") async def test_import_error( hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, caplog: pytest.LogCaptureFixture, ) -> None: """Test that error is logged when import config has an error.""" @@ -700,11 +641,8 @@ async def test_import_error( assert len(vizio_log_list) == 1 -async def test_import_ignore( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_setup") +async def test_import_ignore(hass: HomeAssistant) -> None: """Test import config flow doesn't throw an error when there's an existing ignored source.""" entry = MockConfigEntry( domain=DOMAIN, @@ -723,12 +661,10 @@ async def test_import_ignore( assert result["type"] is FlowResultType.CREATE_ENTRY -async def test_zeroconf_flow( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, - vizio_guess_device_type: pytest.fixture, -) -> None: +@pytest.mark.usefixtures( + "vizio_connect", "vizio_bypass_setup", "vizio_guess_device_type" +) +async def test_zeroconf_flow(hass: HomeAssistant) -> None: """Test zeroconf config flow.""" discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) result = await hass.config_entries.flow.async_init( @@ -760,12 +696,10 @@ async def test_zeroconf_flow( assert result["data"][CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.SPEAKER -async def test_zeroconf_flow_already_configured( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, - vizio_guess_device_type: pytest.fixture, -) -> None: +@pytest.mark.usefixtures( + "vizio_connect", "vizio_bypass_setup", "vizio_guess_device_type" +) +async def test_zeroconf_flow_already_configured(hass: HomeAssistant) -> None: """Test entity is already configured during zeroconf setup.""" entry = MockConfigEntry( domain=DOMAIN, @@ -786,12 +720,10 @@ async def test_zeroconf_flow_already_configured( assert result["reason"] == "already_configured" -async def test_zeroconf_flow_with_port_in_host( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, - vizio_guess_device_type: pytest.fixture, -) -> None: +@pytest.mark.usefixtures( + "vizio_connect", "vizio_bypass_setup", "vizio_guess_device_type" +) +async def test_zeroconf_flow_with_port_in_host(hass: HomeAssistant) -> None: """Test entity is already configured during zeroconf setup when port is in host.""" entry = MockConfigEntry( domain=DOMAIN, @@ -814,12 +746,10 @@ async def test_zeroconf_flow_with_port_in_host( assert result["reason"] == "already_configured" -async def test_zeroconf_dupe_fail( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, - vizio_guess_device_type: pytest.fixture, -) -> None: +@pytest.mark.usefixtures( + "vizio_connect", "vizio_bypass_setup", "vizio_guess_device_type" +) +async def test_zeroconf_dupe_fail(hass: HomeAssistant) -> None: """Test zeroconf config flow when device gets discovered multiple times.""" discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) result = await hass.config_entries.flow.async_init( @@ -840,12 +770,10 @@ async def test_zeroconf_dupe_fail( assert result["reason"] == "already_in_progress" -async def test_zeroconf_ignore( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, - vizio_guess_device_type: pytest.fixture, -) -> None: +@pytest.mark.usefixtures( + "vizio_connect", "vizio_bypass_setup", "vizio_guess_device_type" +) +async def test_zeroconf_ignore(hass: HomeAssistant) -> None: """Test zeroconf discovery doesn't throw an error when there's an existing ignored source.""" entry = MockConfigEntry( domain=DOMAIN, @@ -863,11 +791,8 @@ async def test_zeroconf_ignore( assert result["type"] is FlowResultType.FORM -async def test_zeroconf_no_unique_id( - hass: HomeAssistant, - vizio_guess_device_type: pytest.fixture, - vizio_no_unique_id: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_guess_device_type", "vizio_no_unique_id") +async def test_zeroconf_no_unique_id(hass: HomeAssistant) -> None: """Test zeroconf discovery aborts when unique_id is None.""" discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) @@ -879,12 +804,10 @@ async def test_zeroconf_no_unique_id( assert result["reason"] == "cannot_connect" -async def test_zeroconf_abort_when_ignored( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, - vizio_guess_device_type: pytest.fixture, -) -> None: +@pytest.mark.usefixtures( + "vizio_connect", "vizio_bypass_setup", "vizio_guess_device_type" +) +async def test_zeroconf_abort_when_ignored(hass: HomeAssistant) -> None: """Test zeroconf discovery aborts when the same host has been ignored.""" entry = MockConfigEntry( domain=DOMAIN, @@ -904,13 +827,13 @@ async def test_zeroconf_abort_when_ignored( assert result["reason"] == "already_configured" -async def test_zeroconf_flow_already_configured_hostname( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, - vizio_hostname_check: pytest.fixture, - vizio_guess_device_type: pytest.fixture, -) -> None: +@pytest.mark.usefixtures( + "vizio_connect", + "vizio_bypass_setup", + "vizio_hostname_check", + "vizio_guess_device_type", +) +async def test_zeroconf_flow_already_configured_hostname(hass: HomeAssistant) -> None: """Test entity is already configured during zeroconf setup when existing entry uses hostname.""" config = MOCK_SPEAKER_CONFIG.copy() config[CONF_HOST] = "hostname" @@ -933,12 +856,8 @@ async def test_zeroconf_flow_already_configured_hostname( assert result["reason"] == "already_configured" -async def test_import_flow_already_configured_hostname( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, - vizio_hostname_check: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_setup", "vizio_hostname_check") +async def test_import_flow_already_configured_hostname(hass: HomeAssistant) -> None: """Test entity is already configured during import setup when existing entry uses hostname.""" config = MOCK_SPEAKER_CONFIG.copy() config[CONF_HOST] = "hostname" diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index eba5af437b1..c2b19377809 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -15,11 +15,8 @@ from .const import MOCK_SPEAKER_CONFIG, MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID from tests.common import MockConfigEntry, async_fire_time_changed -async def test_setup_component( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_update") +async def test_setup_component(hass: HomeAssistant) -> None: """Test component setup.""" assert await async_setup_component( hass, DOMAIN, {DOMAIN: MOCK_USER_VALID_TV_CONFIG} @@ -28,11 +25,8 @@ async def test_setup_component( assert len(hass.states.async_entity_ids(Platform.MEDIA_PLAYER)) == 1 -async def test_tv_load_and_unload( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_update") +async def test_tv_load_and_unload(hass: HomeAssistant) -> None: """Test loading and unloading TV entry.""" config_entry = MockConfigEntry( domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID @@ -52,11 +46,8 @@ async def test_tv_load_and_unload( assert DOMAIN not in hass.data -async def test_speaker_load_and_unload( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_update") +async def test_speaker_load_and_unload(hass: HomeAssistant) -> None: """Test loading and unloading speaker entry.""" config_entry = MockConfigEntry( domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID @@ -76,11 +67,11 @@ async def test_speaker_load_and_unload( assert DOMAIN not in hass.data +@pytest.mark.usefixtures( + "vizio_connect", "vizio_bypass_update", "vizio_data_coordinator_update_failure" +) async def test_coordinator_update_failure( hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_bypass_update: pytest.fixture, - vizio_data_coordinator_update_failure: pytest.fixture, caplog: pytest.LogCaptureFixture, ) -> None: """Test coordinator update failure after 10 days.""" diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 52a5732706d..12e19077c8e 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import AsyncIterator from contextlib import asynccontextmanager from datetime import timedelta from typing import Any @@ -129,7 +130,7 @@ def _get_attr_and_assert_base_attr( @asynccontextmanager async def _cm_for_test_setup_without_apps( all_settings: dict[str, Any], vizio_power_state: bool | None -) -> None: +) -> AsyncIterator[None]: """Context manager to setup test for Vizio devices without including app specific patches.""" with ( patch( @@ -211,7 +212,7 @@ async def _test_setup_speaker( @asynccontextmanager async def _cm_for_test_setup_tv_with_apps( hass: HomeAssistant, device_config: dict[str, Any], app_config: dict[str, Any] -) -> None: +) -> AsyncIterator[None]: """Context manager to setup test for Vizio TV with support for apps.""" config_entry = MockConfigEntry( domain=DOMAIN, data=vol.Schema(VIZIO_SCHEMA)(device_config), unique_id=UNIQUE_ID @@ -280,63 +281,46 @@ async def _test_service( assert service_call.call_args == call(*args, **kwargs) -async def test_speaker_on( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_update") +async def test_speaker_on(hass: HomeAssistant) -> None: """Test Vizio Speaker entity setup when on.""" await _test_setup_speaker(hass, True) -async def test_speaker_off( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_update") +async def test_speaker_off(hass: HomeAssistant) -> None: """Test Vizio Speaker entity setup when off.""" await _test_setup_speaker(hass, False) +@pytest.mark.usefixtures("vizio_connect", "vizio_update") async def test_speaker_unavailable( hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update: pytest.fixture, ) -> None: """Test Vizio Speaker entity setup when unavailable.""" await _test_setup_speaker(hass, None) -async def test_init_tv_on( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_update") +async def test_init_tv_on(hass: HomeAssistant) -> None: """Test Vizio TV entity setup when on.""" await _test_setup_tv(hass, True) -async def test_init_tv_off( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_update") +async def test_init_tv_off(hass: HomeAssistant) -> None: """Test Vizio TV entity setup when off.""" await _test_setup_tv(hass, False) -async def test_init_tv_unavailable( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_update") +async def test_init_tv_unavailable(hass: HomeAssistant) -> None: """Test Vizio TV entity setup when unavailable.""" await _test_setup_tv(hass, None) -async def test_setup_unavailable_speaker( - hass: HomeAssistant, vizio_cant_connect: pytest.fixture -) -> None: +@pytest.mark.usefixtures("vizio_cant_connect") +async def test_setup_unavailable_speaker(hass: HomeAssistant) -> None: """Test speaker entity sets up as unavailable.""" config_entry = MockConfigEntry( domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID @@ -346,9 +330,8 @@ async def test_setup_unavailable_speaker( assert hass.states.get("media_player.vizio").state == STATE_UNAVAILABLE -async def test_setup_unavailable_tv( - hass: HomeAssistant, vizio_cant_connect: pytest.fixture -) -> None: +@pytest.mark.usefixtures("vizio_cant_connect") +async def test_setup_unavailable_tv(hass: HomeAssistant) -> None: """Test TV entity sets up as unavailable.""" config_entry = MockConfigEntry( domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID @@ -358,11 +341,8 @@ async def test_setup_unavailable_tv( assert hass.states.get("media_player.vizio").state == STATE_UNAVAILABLE -async def test_services( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_update") +async def test_services(hass: HomeAssistant) -> None: """Test all Vizio media player entity services.""" await _test_setup_tv(hass, True) @@ -449,11 +429,8 @@ async def test_services( await _test_service(hass, MP_DOMAIN, "pause", SERVICE_MEDIA_PAUSE, None) -async def test_options_update( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_update") +async def test_options_update(hass: HomeAssistant) -> None: """Test when config entry update event fires.""" await _test_setup_speaker(hass, True) config_entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -476,7 +453,7 @@ async def _test_update_availability_switch( hass: HomeAssistant, initial_power_state: bool | None, final_power_state: bool | None, - caplog: pytest.fixture, + caplog: pytest.LogCaptureFixture, ) -> None: now = dt_util.utcnow() future_interval = timedelta(minutes=1) @@ -516,30 +493,27 @@ async def _test_update_availability_switch( assert len(vizio_log_list) == 1 +@pytest.mark.usefixtures("vizio_connect", "vizio_update") async def test_update_unavailable_to_available( hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update: pytest.fixture, caplog: pytest.LogCaptureFixture, ) -> None: """Test device becomes available after being unavailable.""" await _test_update_availability_switch(hass, None, True, caplog) +@pytest.mark.usefixtures("vizio_connect", "vizio_update") async def test_update_available_to_unavailable( hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update: pytest.fixture, caplog: pytest.LogCaptureFixture, ) -> None: """Test device becomes unavailable after being available.""" await _test_update_availability_switch(hass, True, None, caplog) +@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps") async def test_setup_with_apps( hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update_with_apps: pytest.fixture, caplog: pytest.LogCaptureFixture, ) -> None: """Test device setup with apps.""" @@ -564,10 +538,9 @@ async def test_setup_with_apps( ) +@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps") async def test_setup_with_apps_include( hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update_with_apps: pytest.fixture, caplog: pytest.LogCaptureFixture, ) -> None: """Test device setup with apps and apps["include"] in config.""" @@ -582,10 +555,9 @@ async def test_setup_with_apps_include( assert "app_id" not in attr +@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps") async def test_setup_with_apps_exclude( hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update_with_apps: pytest.fixture, caplog: pytest.LogCaptureFixture, ) -> None: """Test device setup with apps and apps["exclude"] in config.""" @@ -600,10 +572,9 @@ async def test_setup_with_apps_exclude( assert "app_id" not in attr +@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps") async def test_setup_with_apps_additional_apps_config( hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update_with_apps: pytest.fixture, caplog: pytest.LogCaptureFixture, ) -> None: """Test device setup with apps and apps["additional_configs"] in config.""" @@ -679,10 +650,9 @@ def test_invalid_apps_config(hass: HomeAssistant) -> None: vol.Schema(vol.All(VIZIO_SCHEMA, validate_apps))(MOCK_SPEAKER_APPS_FAILURE) +@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps") async def test_setup_with_unknown_app_config( hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update_with_apps: pytest.fixture, caplog: pytest.LogCaptureFixture, ) -> None: """Test device setup with apps where app config returned is unknown.""" @@ -696,10 +666,9 @@ async def test_setup_with_unknown_app_config( assert attr["app_id"] == UNKNOWN_APP_CONFIG +@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps") async def test_setup_with_no_running_app( hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update_with_apps: pytest.fixture, caplog: pytest.LogCaptureFixture, ) -> None: """Test device setup with apps where no app is running.""" @@ -713,11 +682,8 @@ async def test_setup_with_no_running_app( assert "app_name" not in attr -async def test_setup_tv_without_mute( - hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update: pytest.fixture, -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_update") +async def test_setup_tv_without_mute(hass: HomeAssistant) -> None: """Test Vizio TV entity setup when mute property isn't returned by Vizio API.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -737,10 +703,9 @@ async def test_setup_tv_without_mute( assert "is_volume_muted" not in attr +@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps") async def test_apps_update( hass: HomeAssistant, - vizio_connect: pytest.fixture, - vizio_update_with_apps: pytest.fixture, caplog: pytest.LogCaptureFixture, ) -> None: """Test device setup with apps where no app is running.""" @@ -772,9 +737,8 @@ async def test_apps_update( assert len(apps) == len(APP_LIST) -async def test_vizio_update_with_apps_on_input( - hass: HomeAssistant, vizio_connect, vizio_update_with_apps_on_input -) -> None: +@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps_on_input") +async def test_vizio_update_with_apps_on_input(hass: HomeAssistant) -> None: """Test a vizio TV with apps that is on a TV input.""" config_entry = MockConfigEntry( domain=DOMAIN, From 4ee2c445d1249a559e97b9cb261cc14919288527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 20 Jul 2024 05:28:04 +0200 Subject: [PATCH 1359/2411] Update home_connect to v0.8.0 (#121788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/home_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index ec1247f6855..389386e42af 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/home_connect", "iot_class": "cloud_push", "loggers": ["homeconnect"], - "requirements": ["homeconnect==0.7.2"] + "requirements": ["homeconnect==0.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index da331467fde..59d5fac3361 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ home-assistant-frontend==20240719.0 home-assistant-intents==2024.7.10 # homeassistant.components.home_connect -homeconnect==0.7.2 +homeconnect==0.8.0 # homeassistant.components.homematicip_cloud homematicip==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b1ac5c2994..1f390190f5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -910,7 +910,7 @@ home-assistant-frontend==20240719.0 home-assistant-intents==2024.7.10 # homeassistant.components.home_connect -homeconnect==0.7.2 +homeconnect==0.8.0 # homeassistant.components.homematicip_cloud homematicip==1.1.1 From a0332d049b4ebdb24fed4a46c2da58e9edee0962 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 20 Jul 2024 11:09:52 +0200 Subject: [PATCH 1360/2411] Fix flaky recorder test (#122205) --- tests/components/recorder/test_v32_migration.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index 2d3c339ae5c..f78f5191bc9 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -95,9 +95,11 @@ async def test_migrate_times( with ( patch.object(recorder, "db_schema", old_db_schema), - patch.object( - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION - ), + patch.object(migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION), + patch.object(migration.EventsContextIDMigration, "migrate_data"), + patch.object(migration.StatesContextIDMigration, "migrate_data"), + patch.object(migration.EventTypeIDMigration, "migrate_data"), + patch.object(migration.EntityIDMigration, "migrate_data"), patch.object(core, "StatesMeta", old_db_schema.StatesMeta), patch.object(core, "EventTypes", old_db_schema.EventTypes), patch.object(core, "EventData", old_db_schema.EventData), From 768d20c6455c2cfebbb1f900a797f1a1ed8995a4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 11:10:25 +0200 Subject: [PATCH 1361/2411] Fix recorder datetime annotations (#122214) --- .../recorder/test_migration_from_schema_32.py | 12 ++++++------ tests/components/recorder/test_websocket_api.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/components/recorder/test_migration_from_schema_32.py b/tests/components/recorder/test_migration_from_schema_32.py index 8a542ed8764..b2a83ae8313 100644 --- a/tests/components/recorder/test_migration_from_schema_32.py +++ b/tests/components/recorder/test_migration_from_schema_32.py @@ -950,7 +950,7 @@ async def test_stats_timestamp_conversion_is_reentrant( ) ) - def _insert_pre_timestamp_stat(date_time: datetime) -> None: + def _insert_pre_timestamp_stat(date_time: datetime.datetime) -> None: with session_scope(hass=hass) as session: session.add( old_db_schema.StatisticsShortTerm( @@ -965,7 +965,7 @@ async def test_stats_timestamp_conversion_is_reentrant( ) ) - def _insert_post_timestamp_stat(date_time: datetime) -> None: + def _insert_post_timestamp_stat(date_time: datetime.datetime) -> None: with session_scope(hass=hass) as session: session.add( db_schema.StatisticsShortTerm( @@ -1107,7 +1107,7 @@ async def test_stats_timestamp_with_one_by_one( ) ) - def _insert_pre_timestamp_stat(date_time: datetime) -> None: + def _insert_pre_timestamp_stat(date_time: datetime.datetime) -> None: with session_scope(hass=hass) as session: session.add_all( ( @@ -1134,7 +1134,7 @@ async def test_stats_timestamp_with_one_by_one( ) ) - def _insert_post_timestamp_stat(date_time: datetime) -> None: + def _insert_post_timestamp_stat(date_time: datetime.datetime) -> None: with session_scope(hass=hass) as session: session.add_all( ( @@ -1333,7 +1333,7 @@ async def test_stats_timestamp_with_one_by_one_removes_duplicates( ) ) - def _insert_pre_timestamp_stat(date_time: datetime) -> None: + def _insert_pre_timestamp_stat(date_time: datetime.datetime) -> None: with session_scope(hass=hass) as session: session.add_all( ( @@ -1360,7 +1360,7 @@ async def test_stats_timestamp_with_one_by_one_removes_duplicates( ) ) - def _insert_post_timestamp_stat(date_time: datetime) -> None: + def _insert_post_timestamp_stat(date_time: datetime.datetime) -> None: with session_scope(hass=hass) as session: session.add_all( ( diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 7467ebe5c4c..5f3b1b35c78 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -819,7 +819,7 @@ async def test_statistic_during_period_partial_overlap( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, freezer: FrozenDateTimeFactory, - frozen_time: datetime, + frozen_time: datetime.datetime, ) -> None: """Test statistic_during_period.""" client = await hass_ws_client() From f8c4ffc060909ddbb41ccb88784c748da2f82bd5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 11:10:46 +0200 Subject: [PATCH 1362/2411] Update freezegun to 1.5.1 (#122219) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index e433c066380..c719b4eca64 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ -r requirements_test_pre_commit.txt astroid==3.2.3 coverage==7.5.3 -freezegun==1.5.0 +freezegun==1.5.1 mock-open==1.4.0 mypy-dev==1.12.0a1 pre-commit==3.7.1 From f0b9a806d16584069d0f8818839b72bb207f54d0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 11:11:16 +0200 Subject: [PATCH 1363/2411] Fix missing type[..] annotation in tests (#122217) --- tests/components/climate/test_init.py | 2 +- tests/components/cover/test_init.py | 2 +- tests/components/humidifier/test_init.py | 2 +- tests/components/weather/__init__.py | 2 +- tests/test_const.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 4756c265aea..ced75ff7ef7 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -158,7 +158,7 @@ async def test_sync_turn_off(hass: HomeAssistant) -> None: assert climate.turn_off.called -def _create_tuples(enum: Enum, constant_prefix: str) -> list[tuple[Enum, str]]: +def _create_tuples(enum: type[Enum], constant_prefix: str) -> list[tuple[Enum, str]]: return [ (enum_field, constant_prefix) for enum_field in enum diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py index 7da6c6efe21..37740260c2f 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_init.py @@ -156,7 +156,7 @@ def is_closing(hass, ent): return hass.states.is_state(ent.entity_id, STATE_CLOSING) -def _create_tuples(enum: Enum, constant_prefix: str) -> list[tuple[Enum, str]]: +def _create_tuples(enum: type[Enum], constant_prefix: str) -> list[tuple[Enum, str]]: return [(enum_field, constant_prefix) for enum_field in enum] diff --git a/tests/components/humidifier/test_init.py b/tests/components/humidifier/test_init.py index b90e7084dd1..b31750a3a3b 100644 --- a/tests/components/humidifier/test_init.py +++ b/tests/components/humidifier/test_init.py @@ -48,7 +48,7 @@ async def test_sync_turn_off(hass: HomeAssistant) -> None: assert humidifier.turn_off.called -def _create_tuples(enum: Enum, constant_prefix: str) -> list[tuple[Enum, str]]: +def _create_tuples(enum: type[Enum], constant_prefix: str) -> list[tuple[Enum, str]]: return [(enum_field, constant_prefix) for enum_field in enum] diff --git a/tests/components/weather/__init__.py b/tests/components/weather/__init__.py index c24baad5237..2dbffbbd617 100644 --- a/tests/components/weather/__init__.py +++ b/tests/components/weather/__init__.py @@ -61,7 +61,7 @@ class MockWeatherTest(WeatherPlatform.MockWeather): async def create_entity( hass: HomeAssistant, - mock_weather: WeatherPlatform.MockWeather, + mock_weather: type[WeatherPlatform.MockWeather], manifest_extra: dict[str, Any] | None, **kwargs, ) -> WeatherPlatform.MockWeather: diff --git a/tests/test_const.py b/tests/test_const.py index a6a2387b091..64ccb875cf5 100644 --- a/tests/test_const.py +++ b/tests/test_const.py @@ -15,7 +15,7 @@ from .common import ( def _create_tuples( - value: Enum | list[Enum], constant_prefix: str + value: type[Enum] | list[Enum], constant_prefix: str ) -> list[tuple[Enum, str]]: return [(enum, constant_prefix) for enum in value] From 24b12bc50915608acf3d875c66d56d0dd3dfbb98 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 11:12:02 +0200 Subject: [PATCH 1364/2411] Improve HA snapshot serializer typing (#122218) --- tests/syrupy.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/syrupy.py b/tests/syrupy.py index 52bd5756798..9dc8e50e5f1 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -12,13 +12,7 @@ import attr import attrs from syrupy.extensions.amber import AmberDataSerializer, AmberSnapshotExtension from syrupy.location import PyTestLocation -from syrupy.types import ( - PropertyFilter, - PropertyMatcher, - PropertyPath, - SerializableData, - SerializedData, -) +from syrupy.types import PropertyFilter, PropertyMatcher, PropertyPath, SerializableData import voluptuous as vol import voluptuous_serialize @@ -90,7 +84,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): matcher: PropertyMatcher | None = None, path: PropertyPath = (), visited: set[Any] | None = None, - ) -> SerializedData: + ) -> str: """Pre-process data before serializing. This allows us to handle specific cases for Home Assistant data structures. @@ -111,7 +105,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): serializable_data = voluptuous_serialize.convert(data) elif isinstance(data, ConfigEntry): serializable_data = cls._serializable_config_entry(data) - elif dataclasses.is_dataclass(data): + elif dataclasses.is_dataclass(type(data)): serializable_data = dataclasses.asdict(data) elif isinstance(data, IntFlag): # The repr of an enum.IntFlag has changed between Python 3.10 and 3.11 @@ -120,7 +114,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): else: serializable_data = data with suppress(TypeError): - if attr.has(data): + if attr.has(type(data)): serializable_data = attrs.asdict(data) return super()._serialize( @@ -136,7 +130,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): @classmethod def _serializable_area_registry_entry(cls, data: ar.AreaEntry) -> SerializableData: """Prepare a Home Assistant area registry entry for serialization.""" - serialized = AreaRegistryEntrySnapshot(attrs.asdict(data) | {"id": ANY}) + serialized = AreaRegistryEntrySnapshot(dataclasses.asdict(data) | {"id": ANY}) serialized.pop("_json_repr") return serialized From e9f5c4188eec41db6f3a18eeb23b243a43bda5f9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 11:12:41 +0200 Subject: [PATCH 1365/2411] Fix incompatible signature overwrite async_turn_on + off (#122208) --- homeassistant/components/ecoforest/switch.py | 5 +++-- homeassistant/components/v2c/switch.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ecoforest/switch.py b/homeassistant/components/ecoforest/switch.py index f59970aa751..d643217bebc 100644 --- a/homeassistant/components/ecoforest/switch.py +++ b/homeassistant/components/ecoforest/switch.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from dataclasses import dataclass +from typing import Any from pyecoforest.api import EcoforestApi from pyecoforest.models.device import Device @@ -61,12 +62,12 @@ class EcoforestSwitchEntity(EcoforestEntity, SwitchEntity): """Return the state of the ecoforest device.""" return self.entity_description.value_fn(self.data) - async def async_turn_on(self): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the ecoforest device.""" await self.entity_description.switch_fn(self.coordinator.api, True) await self.coordinator.async_request_refresh() - async def async_turn_off(self): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the ecoforest device.""" await self.entity_description.switch_fn(self.coordinator.api, False) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/v2c/switch.py b/homeassistant/components/v2c/switch.py index cd89e954275..cca7da70e48 100644 --- a/homeassistant/components/v2c/switch.py +++ b/homeassistant/components/v2c/switch.py @@ -111,12 +111,12 @@ class V2CSwitchEntity(V2CBaseEntity, SwitchEntity): """Return the state of the EVSE switch.""" return self.entity_description.value_fn(self.data) - async def async_turn_on(self): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the EVSE switch.""" await self.entity_description.turn_on_fn(self.coordinator.evse) await self.coordinator.async_request_refresh() - async def async_turn_off(self): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the EVSE switch.""" await self.entity_description.turn_off_fn(self.coordinator.evse) await self.coordinator.async_request_refresh() From 0637e342f612f7390791a4caf32bd5b697e157c1 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 11:13:13 +0200 Subject: [PATCH 1366/2411] Fix ConfigFlowResult annotations in tests (#122215) --- .../here_travel_time/test_config_flow.py | 26 +++++++++++-------- .../nibe_heatpump/test_config_flow.py | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index eb958991c71..9b15a42dd56 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -47,7 +47,9 @@ def bypass_setup_fixture(): @pytest.fixture(name="user_step_result") -async def user_step_result_fixture(hass: HomeAssistant) -> FlowResultType: +async def user_step_result_fixture( + hass: HomeAssistant, +) -> config_entries.ConfigFlowResult: """Provide the result of a completed user step.""" init_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -65,7 +67,9 @@ async def user_step_result_fixture(hass: HomeAssistant) -> FlowResultType: @pytest.fixture(name="option_init_result") -async def option_init_result_fixture(hass: HomeAssistant) -> FlowResultType: +async def option_init_result_fixture( + hass: HomeAssistant, +) -> config_entries.ConfigFlowResult: """Provide the result of a completed options init step.""" entry = MockConfigEntry( domain=DOMAIN, @@ -94,8 +98,8 @@ async def option_init_result_fixture(hass: HomeAssistant) -> FlowResultType: @pytest.fixture(name="origin_step_result") async def origin_step_result_fixture( - hass: HomeAssistant, user_step_result: FlowResultType -) -> FlowResultType: + hass: HomeAssistant, user_step_result: config_entries.ConfigFlowResult +) -> config_entries.ConfigFlowResult: """Provide the result of a completed origin by coordinates step.""" origin_menu_result = await hass.config_entries.flow.async_configure( user_step_result["flow_id"], {"next_step_id": "origin_coordinates"} @@ -142,7 +146,7 @@ async def test_step_user(hass: HomeAssistant, menu_options) -> None: @pytest.mark.usefixtures("valid_response") async def test_step_origin_coordinates( - hass: HomeAssistant, user_step_result: FlowResultType + hass: HomeAssistant, user_step_result: config_entries.ConfigFlowResult ) -> None: """Test the origin coordinates step.""" menu_result = await hass.config_entries.flow.async_configure( @@ -165,7 +169,7 @@ async def test_step_origin_coordinates( @pytest.mark.usefixtures("valid_response") async def test_step_origin_entity( - hass: HomeAssistant, user_step_result: FlowResultType + hass: HomeAssistant, user_step_result: config_entries.ConfigFlowResult ) -> None: """Test the origin coordinates step.""" menu_result = await hass.config_entries.flow.async_configure( @@ -182,7 +186,7 @@ async def test_step_origin_entity( @pytest.mark.usefixtures("valid_response") async def test_step_destination_coordinates( - hass: HomeAssistant, origin_step_result: FlowResultType + hass: HomeAssistant, origin_step_result: config_entries.ConfigFlowResult ) -> None: """Test the origin coordinates step.""" menu_result = await hass.config_entries.flow.async_configure( @@ -216,7 +220,7 @@ async def test_step_destination_coordinates( @pytest.mark.usefixtures("valid_response") async def test_step_destination_entity( hass: HomeAssistant, - origin_step_result: FlowResultType, + origin_step_result: config_entries.ConfigFlowResult, ) -> None: """Test the origin coordinates step.""" menu_result = await hass.config_entries.flow.async_configure( @@ -322,7 +326,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("valid_response") async def test_options_flow_arrival_time_step( - hass: HomeAssistant, option_init_result: FlowResultType + hass: HomeAssistant, option_init_result: config_entries.ConfigFlowResult ) -> None: """Test the options flow arrival time type.""" menu_result = await hass.config_entries.options.async_configure( @@ -346,7 +350,7 @@ async def test_options_flow_arrival_time_step( @pytest.mark.usefixtures("valid_response") async def test_options_flow_departure_time_step( - hass: HomeAssistant, option_init_result: FlowResultType + hass: HomeAssistant, option_init_result: config_entries.ConfigFlowResult ) -> None: """Test the options flow departure time type.""" menu_result = await hass.config_entries.options.async_configure( @@ -370,7 +374,7 @@ async def test_options_flow_departure_time_step( @pytest.mark.usefixtures("valid_response") async def test_options_flow_no_time_step( - hass: HomeAssistant, option_init_result: FlowResultType + hass: HomeAssistant, option_init_result: config_entries.ConfigFlowResult ) -> None: """Test the options flow arrival time type.""" menu_result = await hass.config_entries.options.async_configure( diff --git a/tests/components/nibe_heatpump/test_config_flow.py b/tests/components/nibe_heatpump/test_config_flow.py index 471f7f4c593..de5f577fa7d 100644 --- a/tests/components/nibe_heatpump/test_config_flow.py +++ b/tests/components/nibe_heatpump/test_config_flow.py @@ -38,7 +38,7 @@ pytestmark = pytest.mark.usefixtures("mock_setup_entry") async def _get_connection_form( hass: HomeAssistant, connection_type: str -) -> FlowResultType: +) -> config_entries.ConfigFlowResult: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} From a6068dcdf242e782c7db4735ed8fca98407f011c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 11:16:04 +0200 Subject: [PATCH 1367/2411] Update import locations in tests (#122216) --- tests/auth/providers/test_trusted_networks.py | 16 +++---- tests/components/config/test_automation.py | 2 +- tests/components/config/test_core.py | 2 +- tests/components/config/test_scene.py | 2 +- tests/components/config/test_script.py | 2 +- .../conversation/test_default_agent.py | 6 +-- tests/components/esphome/test_sensor.py | 2 +- tests/components/fido/test_sensor.py | 2 +- .../google_assistant/test_smart_home.py | 3 +- .../components/google_assistant/test_trait.py | 8 +--- .../test_hardware.py | 3 +- tests/components/homekit/test_homekit.py | 3 +- tests/components/http/test_static.py | 3 +- tests/components/intent_script/test_init.py | 2 +- tests/components/mailbox/test_init.py | 2 +- tests/components/melnor/test_sensor.py | 10 +++-- tests/components/melnor/test_time.py | 4 +- tests/components/plex/test_update.py | 3 +- tests/components/recorder/test_filters.py | 9 +--- .../test_filters_with_entityfilter.py | 11 +++-- ...est_filters_with_entityfilter_schema_37.py | 11 +++-- .../components/recorder/test_v32_migration.py | 3 +- tests/components/rflink/test_init.py | 2 +- tests/components/schedule/test_init.py | 3 +- tests/components/script/test_init.py | 6 +-- tests/components/snips/test_init.py | 2 +- tests/components/spc/test_init.py | 2 +- tests/components/startca/test_sensor.py | 2 +- .../synology_dsm/test_media_source.py | 3 +- tests/components/system_log/test_init.py | 2 +- tests/components/template/test_init.py | 2 +- tests/components/trace/test_websocket_api.py | 2 +- tests/components/websocket_api/test_sensor.py | 2 +- tests/conftest.py | 7 ++-- tests/helpers/test_aiohttp_client.py | 3 +- tests/helpers/test_discovery_flow.py | 3 +- tests/helpers/test_entity.py | 13 +++--- tests/helpers/test_entity_platform.py | 10 ++--- tests/helpers/test_httpx_client.py | 3 +- tests/helpers/test_integration_platform.py | 3 +- tests/helpers/test_script_variables.py | 5 ++- tests/test_bootstrap.py | 7 +++- tests/test_config.py | 42 ++++++++----------- tests/test_core.py | 10 ++--- tests/test_runner.py | 9 ++-- tests/util/yaml/test_init.py | 4 +- 46 files changed, 131 insertions(+), 125 deletions(-) diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index f16c066a7e8..e738e8f0911 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -189,11 +189,11 @@ async def test_validate_access(provider: tn_auth.TrustedNetworksAuthProvider) -> provider.async_validate_access(ip_address("::1")) provider.async_validate_access(ip_address("fd01:db8::ff00:42:8329")) - with pytest.raises(tn_auth.InvalidAuthError): + with pytest.raises(auth.InvalidAuthError): provider.async_validate_access(ip_address("192.168.0.2")) - with pytest.raises(tn_auth.InvalidAuthError): + with pytest.raises(auth.InvalidAuthError): provider.async_validate_access(ip_address("127.0.0.1")) - with pytest.raises(tn_auth.InvalidAuthError): + with pytest.raises(auth.InvalidAuthError): provider.async_validate_access(ip_address("2001:db8::ff00:42:8329")) @@ -214,11 +214,11 @@ async def test_validate_access_proxy( ) provider.async_validate_access(ip_address("192.168.128.2")) provider.async_validate_access(ip_address("fd00::2")) - with pytest.raises(tn_auth.InvalidAuthError): + with pytest.raises(auth.InvalidAuthError): provider.async_validate_access(ip_address("192.168.128.0")) - with pytest.raises(tn_auth.InvalidAuthError): + with pytest.raises(auth.InvalidAuthError): provider.async_validate_access(ip_address("192.168.128.1")) - with pytest.raises(tn_auth.InvalidAuthError): + with pytest.raises(auth.InvalidAuthError): provider.async_validate_access(ip_address("fd00::1")) @@ -241,7 +241,7 @@ async def test_validate_access_cloud( provider.async_validate_access(ip_address("192.168.128.2")) remote.is_cloud_request.set(True) - with pytest.raises(tn_auth.InvalidAuthError): + with pytest.raises(auth.InvalidAuthError): provider.async_validate_access(ip_address("192.168.128.2")) @@ -250,7 +250,7 @@ async def test_validate_refresh_token( ) -> None: """Verify re-validation of refresh token.""" with patch.object(provider, "async_validate_access") as mock: - with pytest.raises(tn_auth.InvalidAuthError): + with pytest.raises(auth.InvalidAuthError): provider.async_validate_refresh_token(Mock(), None) provider.async_validate_refresh_token(Mock(), "127.0.0.1") diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index f907732109d..89113070367 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -7,12 +7,12 @@ from unittest.mock import patch import pytest -from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.components.config import automation from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component from homeassistant.util import yaml from tests.typing import ClientSessionGenerator diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 7d02063b2b9..4550f2e08e5 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -5,11 +5,11 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.components.config import core from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, location from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index 22bcfa345a2..c4c207f33f9 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -7,11 +7,11 @@ from unittest.mock import ANY, patch import pytest -from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.components.config import scene from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component from tests.typing import ClientSessionGenerator diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 4771576ed6e..88245eb567f 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -7,12 +7,12 @@ from unittest.mock import patch import pytest -from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.components.config import script from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component from homeassistant.util import yaml from tests.typing import ClientSessionGenerator diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index d227625e7ce..10a81a024ca 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -30,12 +30,12 @@ from homeassistant.const import ( STATE_CLOSED, STATE_ON, STATE_UNKNOWN, + EntityCategory, ) from homeassistant.core import DOMAIN as HASS_DOMAIN, Context, HomeAssistant, callback from homeassistant.helpers import ( area_registry as ar, device_registry as dr, - entity, entity_registry as er, floor_registry as fr, intent, @@ -79,8 +79,8 @@ async def init_components(hass: HomeAssistant) -> None: [ {"hidden_by": er.RegistryEntryHider.USER}, {"hidden_by": er.RegistryEntryHider.INTEGRATION}, - {"entity_category": entity.EntityCategory.CONFIG}, - {"entity_category": entity.EntityCategory.DIAGNOSTIC}, + {"entity_category": EntityCategory.CONFIG}, + {"entity_category": EntityCategory.DIAGNOSTIC}, ], ) @pytest.mark.usefixtures("init_components") diff --git a/tests/components/esphome/test_sensor.py b/tests/components/esphome/test_sensor.py index bebfaaa69d4..76f71b53167 100644 --- a/tests/components/esphome/test_sensor.py +++ b/tests/components/esphome/test_sensor.py @@ -28,10 +28,10 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, + EntityCategory, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity import EntityCategory from .conftest import MockESPHomeDevice diff --git a/tests/components/fido/test_sensor.py b/tests/components/fido/test_sensor.py index a067f060af8..d47c7ce8e9f 100644 --- a/tests/components/fido/test_sensor.py +++ b/tests/components/fido/test_sensor.py @@ -6,9 +6,9 @@ from unittest.mock import MagicMock, patch from pyfido.client import PyFidoError import pytest -from homeassistant.bootstrap import async_setup_component from homeassistant.components.fido import sensor as fido from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import assert_setup_component diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 2eeb3d16b81..ea8f6957e38 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -25,11 +25,12 @@ from homeassistant.components.google_assistant import ( from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + EVENT_CALL_SERVICE, Platform, UnitOfTemperature, __version__, ) -from homeassistant.core import EVENT_CALL_SERVICE, HomeAssistant, State +from homeassistant.core import HomeAssistant, State from homeassistant.helpers import ( area_registry as ar, device_registry as dr, diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 63a34c01dac..5308b5608ea 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -60,6 +60,7 @@ from homeassistant.const import ( ATTR_MODE, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, + EVENT_CALL_SERVICE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ALARM_ARMED_AWAY, @@ -75,12 +76,7 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import ( - DOMAIN as HA_DOMAIN, - EVENT_CALL_SERVICE, - HomeAssistant, - State, -) +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, State from homeassistant.util import color, dt as dt_util from homeassistant.util.unit_conversion import TemperatureConverter diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index 888ed27a3c0..f39e648b0f2 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -1,7 +1,8 @@ """Test the Home Assistant SkyConnect hardware platform.""" from homeassistant.components.homeassistant_sky_connect.const import DOMAIN -from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, HomeAssistant +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 45da90b5446..9653acdfabb 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -59,7 +59,8 @@ from homeassistant.const import ( STATE_ON, EntityCategory, ) -from homeassistant.core import HomeAssistant, HomeAssistantError, State +from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( device_registry as dr, entity_registry as er, diff --git a/tests/components/http/test_static.py b/tests/components/http/test_static.py index 3e3f21d5002..52a5db5daa7 100644 --- a/tests/components/http/test_static.py +++ b/tests/components/http/test_static.py @@ -9,7 +9,8 @@ import pytest from homeassistant.components.http import StaticPathConfig from homeassistant.components.http.static import CachingStaticResource, _get_file_path -from homeassistant.core import EVENT_HOMEASSISTANT_START, HomeAssistant +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import HomeAssistant from homeassistant.helpers.http import KEY_ALLOW_CONFIGURED_CORS from homeassistant.setup import async_setup_component diff --git a/tests/components/intent_script/test_init.py b/tests/components/intent_script/test_init.py index 5f4c7b97b63..86f3a7aba46 100644 --- a/tests/components/intent_script/test_init.py +++ b/tests/components/intent_script/test_init.py @@ -3,11 +3,11 @@ from unittest.mock import patch from homeassistant import config as hass_config -from homeassistant.bootstrap import async_setup_component from homeassistant.components.intent_script import DOMAIN from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant from homeassistant.helpers import intent +from homeassistant.setup import async_setup_component from tests.common import async_mock_service, get_fixture_path diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index 31e831c3bae..6fcf9176aae 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -8,11 +8,11 @@ from typing import Any from aiohttp.test_utils import TestClient import pytest -from homeassistant.bootstrap import async_setup_component from homeassistant.components import mailbox from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from tests.common import MockModule, mock_integration, mock_platform diff --git a/tests/components/melnor/test_sensor.py b/tests/components/melnor/test_sensor.py index d04494d44ad..a2ba23d9e61 100644 --- a/tests/components/melnor/test_sensor.py +++ b/tests/components/melnor/test_sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations +from datetime import timedelta + from freezegun import freeze_time from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass @@ -51,7 +53,7 @@ async def test_minutes_remaining_sensor(hass: HomeAssistant) -> None: entry = mock_config_entry(hass) device = mock_melnor_device() - end_time = now + dt_util.dt.timedelta(minutes=10) + end_time = now + timedelta(minutes=10) # we control this mock @@ -76,7 +78,7 @@ async def test_minutes_remaining_sensor(hass: HomeAssistant) -> None: # Turn valve on device.zone1._is_watering = True - async_fire_time_changed(hass, now + dt_util.dt.timedelta(seconds=10)) + async_fire_time_changed(hass, now + timedelta(seconds=10)) await hass.async_block_till_done() # Valve is on, report 10 @@ -94,7 +96,7 @@ async def test_schedule_next_cycle_sensor(hass: HomeAssistant) -> None: entry = mock_config_entry(hass) device = mock_melnor_device() - next_cycle = now + dt_util.dt.timedelta(minutes=10) + next_cycle = now + timedelta(minutes=10) # we control this mock device.zone1.frequency._next_run_time = next_cycle @@ -118,7 +120,7 @@ async def test_schedule_next_cycle_sensor(hass: HomeAssistant) -> None: # Turn valve on device.zone1._schedule_enabled = True - async_fire_time_changed(hass, now + dt_util.dt.timedelta(seconds=10)) + async_fire_time_changed(hass, now + timedelta(seconds=10)) await hass.async_block_till_done() # Valve is on, report 10 diff --git a/tests/components/melnor/test_time.py b/tests/components/melnor/test_time.py index 1d12c3b47f8..50b51d31ff8 100644 --- a/tests/components/melnor/test_time.py +++ b/tests/components/melnor/test_time.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import time +from datetime import time, timedelta from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -46,7 +46,7 @@ async def test_schedule_start_time(hass: HomeAssistant) -> None: blocking=True, ) - async_fire_time_changed(hass, now + dt_util.dt.timedelta(seconds=10)) + async_fire_time_changed(hass, now + timedelta(seconds=10)) await hass.async_block_till_done() time_entity = hass.states.get("time.zone_1_schedule_start_time") diff --git a/tests/components/plex/test_update.py b/tests/components/plex/test_update.py index 942162665af..7ad2481a726 100644 --- a/tests/components/plex/test_update.py +++ b/tests/components/plex/test_update.py @@ -9,7 +9,8 @@ from homeassistant.components.update import ( SERVICE_INSTALL, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant, HomeAssistantError +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/recorder/test_filters.py b/tests/components/recorder/test_filters.py index 13a2a325f1e..2841cabda1b 100644 --- a/tests/components/recorder/test_filters.py +++ b/tests/components/recorder/test_filters.py @@ -7,13 +7,8 @@ from homeassistant.components.recorder.filters import ( extract_include_exclude_filter_conf, merge_include_exclude_filters, ) -from homeassistant.helpers.entityfilter import ( - CONF_DOMAINS, - CONF_ENTITIES, - CONF_ENTITY_GLOBS, - CONF_EXCLUDE, - CONF_INCLUDE, -) +from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE +from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS EMPTY_INCLUDE_FILTER = { CONF_INCLUDE: { diff --git a/tests/components/recorder/test_filters_with_entityfilter.py b/tests/components/recorder/test_filters_with_entityfilter.py index 1ee127a9989..97839803619 100644 --- a/tests/components/recorder/test_filters_with_entityfilter.py +++ b/tests/components/recorder/test_filters_with_entityfilter.py @@ -13,14 +13,17 @@ from homeassistant.components.recorder.filters import ( sqlalchemy_filter_from_include_exclude_conf, ) from homeassistant.components.recorder.util import session_scope -from homeassistant.const import ATTR_ENTITY_ID, STATE_ON -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entityfilter import ( +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_DOMAINS, CONF_ENTITIES, - CONF_ENTITY_GLOBS, CONF_EXCLUDE, CONF_INCLUDE, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entityfilter import ( + CONF_ENTITY_GLOBS, convert_include_exclude_filter, ) diff --git a/tests/components/recorder/test_filters_with_entityfilter_schema_37.py b/tests/components/recorder/test_filters_with_entityfilter_schema_37.py index 6269d2bf903..d3024df4ed6 100644 --- a/tests/components/recorder/test_filters_with_entityfilter_schema_37.py +++ b/tests/components/recorder/test_filters_with_entityfilter_schema_37.py @@ -16,14 +16,17 @@ from homeassistant.components.recorder.filters import ( sqlalchemy_filter_from_include_exclude_conf, ) from homeassistant.components.recorder.util import session_scope -from homeassistant.const import ATTR_ENTITY_ID, STATE_ON -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entityfilter import ( +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_DOMAINS, CONF_ENTITIES, - CONF_ENTITY_GLOBS, CONF_EXCLUDE, CONF_INCLUDE, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entityfilter import ( + CONF_ENTITY_GLOBS, convert_include_exclude_filter, ) diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index f78f5191bc9..188e81d0230 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -13,7 +13,8 @@ from homeassistant.components import recorder from homeassistant.components.recorder import core, migration, statistics from homeassistant.components.recorder.queries import select_event_type_ids from homeassistant.components.recorder.util import session_scope -from homeassistant.core import EVENT_STATE_CHANGED, Event, EventOrigin, State +from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.core import Event, EventOrigin, State import homeassistant.util.dt as dt_util from .common import async_wait_recording_done diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index 47062fe250a..1caae302748 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -5,7 +5,6 @@ from unittest.mock import Mock import pytest from voluptuous.error import MultipleInvalid -from homeassistant.bootstrap import async_setup_component from homeassistant.components.rflink import ( CONF_KEEPALIVE_IDLE, CONF_RECONNECT_INTERVAL, @@ -28,6 +27,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component async def mock_rflink( diff --git a/tests/components/schedule/test_init.py b/tests/components/schedule/test_init.py index c43b2500ccb..7cd59f19033 100644 --- a/tests/components/schedule/test_init.py +++ b/tests/components/schedule/test_init.py @@ -31,11 +31,12 @@ from homeassistant.const import ( CONF_ICON, CONF_ID, CONF_NAME, + EVENT_STATE_CHANGED, SERVICE_RELOAD, STATE_OFF, STATE_ON, ) -from homeassistant.core import EVENT_STATE_CHANGED, Context, HomeAssistant +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 1d5100916b7..8362dfbcfb2 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -29,8 +29,8 @@ from homeassistant.core import ( callback, split_entity_id, ) -from homeassistant.exceptions import ServiceNotFound -from homeassistant.helpers import device_registry as dr, entity_registry as er, template +from homeassistant.exceptions import ServiceNotFound, TemplateError +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import ( SCRIPT_MODE_CHOICES, @@ -1197,7 +1197,7 @@ async def test_script_variables( assert mock_calls[2].data["value"] == "from_service" assert "Error rendering variables" not in caplog.text - with pytest.raises(template.TemplateError): + with pytest.raises(TemplateError): await hass.services.async_call("script", "script3", blocking=True) assert "Error rendering variables" in caplog.text assert len(mock_calls) == 3 diff --git a/tests/components/snips/test_init.py b/tests/components/snips/test_init.py index 89ee211b38f..82dbf1cd281 100644 --- a/tests/components/snips/test_init.py +++ b/tests/components/snips/test_init.py @@ -6,10 +6,10 @@ import logging import pytest import voluptuous as vol -from homeassistant.bootstrap import async_setup_component from homeassistant.components import snips from homeassistant.core import HomeAssistant from homeassistant.helpers.intent import ServiceIntentHandler, async_register +from homeassistant.setup import async_setup_component from tests.common import async_fire_mqtt_message, async_mock_intent, async_mock_service from tests.typing import MqttMockHAClient diff --git a/tests/components/spc/test_init.py b/tests/components/spc/test_init.py index d2636d9333c..4f335e2f980 100644 --- a/tests/components/spc/test_init.py +++ b/tests/components/spc/test_init.py @@ -5,10 +5,10 @@ from unittest.mock import Mock, PropertyMock, patch import pyspcwebgw from pyspcwebgw.const import AreaMode -from homeassistant.bootstrap import async_setup_component from homeassistant.components.spc import DATA_API from homeassistant.const import STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component async def test_valid_device_config(hass: HomeAssistant) -> None: diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py index b0d43af1cae..be5524eb650 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -2,11 +2,11 @@ from http import HTTPStatus -from homeassistant.bootstrap import async_setup_component from homeassistant.components.startca.sensor import StartcaData from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, UnitOfInformation from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.setup import async_setup_component from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/synology_dsm/test_media_source.py b/tests/components/synology_dsm/test_media_source.py index 433a4b15c23..f7ab26997ba 100644 --- a/tests/components/synology_dsm/test_media_source.py +++ b/tests/components/synology_dsm/test_media_source.py @@ -4,6 +4,7 @@ from pathlib import Path import tempfile from unittest.mock import AsyncMock, MagicMock, patch +from aiohttp import web import pytest from synology_dsm.api.photos import SynoPhotosAlbum, SynoPhotosItem from synology_dsm.exceptions import SynologyDSMException @@ -30,7 +31,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.util.aiohttp import MockRequest, web +from homeassistant.util.aiohttp import MockRequest from .consts import HOST, MACS, PASSWORD, PORT, USE_SSL, USERNAME diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 918d995fab9..fb46d120acf 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -10,10 +10,10 @@ import traceback from typing import Any from unittest.mock import MagicMock, patch -from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.typing import ConfigType +from homeassistant.setup import async_setup_component from tests.common import async_capture_events from tests.typing import WebSocketGenerator diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 1face4bfda0..fe08e1f4963 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -7,9 +7,9 @@ import pytest from homeassistant import config from homeassistant.components.template import DOMAIN +from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.reload import SERVICE_RELOAD from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index c7e445833ae..b0b982d4825 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -9,11 +9,11 @@ from unittest.mock import patch import pytest from pytest_unordered import unordered -from homeassistant.bootstrap import async_setup_component from homeassistant.components.trace.const import DEFAULT_STORED_TRACES from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Context, CoreState, HomeAssistant, callback from homeassistant.helpers.typing import UNDEFINED +from homeassistant.setup import async_setup_component from homeassistant.util.uuid import random_uuid_hex from tests.common import load_fixture diff --git a/tests/components/websocket_api/test_sensor.py b/tests/components/websocket_api/test_sensor.py index 3af02dc8f2b..2e5f0c6c605 100644 --- a/tests/components/websocket_api/test_sensor.py +++ b/tests/components/websocket_api/test_sensor.py @@ -1,10 +1,10 @@ """Test cases for the API stream sensor.""" from homeassistant.auth.providers.homeassistant import HassAuthProvider -from homeassistant.bootstrap import async_setup_component from homeassistant.components.websocket_api.auth import TYPE_AUTH_REQUIRED from homeassistant.components.websocket_api.http import URL from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from .test_auth import test_auth_active_with_token diff --git a/tests/conftest.py b/tests/conftest.py index 1e4396313d2..2c8c351f165 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,6 +37,7 @@ import requests_mock from syrupy.assertion import SnapshotAssertion from homeassistant import block_async_io +from homeassistant.exceptions import ServiceNotFound # Setup patching if dt_util time functions before any other Home Assistant imports from . import patch_time # noqa: F401, isort:skip @@ -54,7 +55,7 @@ from homeassistant.components.websocket_api.auth import ( from homeassistant.components.websocket_api.http import URL from homeassistant.config import YAML_CONFIG_FILE from homeassistant.config_entries import ConfigEntries, ConfigEntry, ConfigEntryState -from homeassistant.const import HASSIO_USER_NAME +from homeassistant.const import BASE_PLATFORMS, HASSIO_USER_NAME from homeassistant.core import ( Context, CoreState, @@ -77,7 +78,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.translation import _TranslationsCacheData from homeassistant.helpers.typing import ConfigType -from homeassistant.setup import BASE_PLATFORMS, async_setup_component +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, location from homeassistant.util.async_ import create_eager_task from homeassistant.util.json import json_loads @@ -1767,7 +1768,7 @@ def service_calls(hass: HomeAssistant) -> Generator[list[ServiceCall]]: target, return_response, ) - except ha.ServiceNotFound: + except ServiceNotFound: _LOGGER.debug("Ignoring unknown service call to %s.%s", domain, service) return None diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 7dd34fd2c64..c0f61238329 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -16,9 +16,10 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_CLOSE, HTTP_BASIC_AUTHENTICATION, ) -from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE, HomeAssistant +from homeassistant.core import HomeAssistant import homeassistant.helpers.aiohttp_client as client from homeassistant.util.color import RGBColor diff --git a/tests/helpers/test_discovery_flow.py b/tests/helpers/test_discovery_flow.py index c834f60e91e..0fa315d684b 100644 --- a/tests/helpers/test_discovery_flow.py +++ b/tests/helpers/test_discovery_flow.py @@ -6,7 +6,8 @@ from unittest.mock import AsyncMock, call, patch import pytest from homeassistant import config_entries -from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, CoreState, HomeAssistant +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers import discovery_flow diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index f76b8555580..283a5b4fb37 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -22,15 +22,16 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE, STATE_UNKNOWN, + EntityCategory, ) from homeassistant.core import ( Context, HassJobType, HomeAssistant, - HomeAssistantError, ReleaseChannel, callback, ) +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.typing import UNDEFINED, UndefinedType @@ -922,13 +923,13 @@ async def test_entity_category_property(hass: HomeAssistant) -> None: key="abc", entity_category="ignore_me" ) mock_entity1.entity_id = "hello.world" - mock_entity1._attr_entity_category = entity.EntityCategory.CONFIG + mock_entity1._attr_entity_category = EntityCategory.CONFIG assert mock_entity1.entity_category == "config" mock_entity2 = entity.Entity() mock_entity2.hass = hass mock_entity2.entity_description = entity.EntityDescription( - key="abc", entity_category=entity.EntityCategory.CONFIG + key="abc", entity_category=EntityCategory.CONFIG ) mock_entity2.entity_id = "hello.world" assert mock_entity2.entity_category == "config" @@ -937,8 +938,8 @@ async def test_entity_category_property(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("value", "expected"), [ - ("config", entity.EntityCategory.CONFIG), - ("diagnostic", entity.EntityCategory.DIAGNOSTIC), + ("config", EntityCategory.CONFIG), + ("diagnostic", EntityCategory.DIAGNOSTIC), ], ) def test_entity_category_schema(value, expected) -> None: @@ -946,7 +947,7 @@ def test_entity_category_schema(value, expected) -> None: schema = vol.Schema(entity.ENTITY_CATEGORIES_SCHEMA) result = schema(value) assert result == expected - assert isinstance(result, entity.EntityCategory) + assert isinstance(result, EntityCategory) @pytest.mark.parametrize("value", [None, "non_existing"]) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 4e761a21e8c..ff08eb5de04 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -9,7 +9,7 @@ from unittest.mock import ANY, AsyncMock, Mock, patch import pytest -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, PERCENTAGE +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, PERCENTAGE, EntityCategory from homeassistant.core import ( CoreState, HomeAssistant, @@ -26,12 +26,8 @@ from homeassistant.helpers import ( entity_registry as er, issue_registry as ir, ) -from homeassistant.helpers.entity import ( - DeviceInfo, - Entity, - EntityCategory, - async_generate_entity_id, -) +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import ( DEFAULT_SCAN_INTERVAL, EntityComponent, diff --git a/tests/helpers/test_httpx_client.py b/tests/helpers/test_httpx_client.py index 60bdbe607e3..ccfccb3d698 100644 --- a/tests/helpers/test_httpx_client.py +++ b/tests/helpers/test_httpx_client.py @@ -5,7 +5,8 @@ from unittest.mock import Mock, patch import httpx import pytest -from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE, HomeAssistant +from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE +from homeassistant.core import HomeAssistant import homeassistant.helpers.httpx_client as client from tests.common import MockModule, extract_stack_to_frame, mock_integration diff --git a/tests/helpers/test_integration_platform.py b/tests/helpers/test_integration_platform.py index 81eb1f2fd38..497bae5fb88 100644 --- a/tests/helpers/test_integration_platform.py +++ b/tests/helpers/test_integration_platform.py @@ -7,12 +7,13 @@ from unittest.mock import Mock, patch import pytest from homeassistant import loader +from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) -from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED +from homeassistant.setup import ATTR_COMPONENT from tests.common import mock_platform diff --git a/tests/helpers/test_script_variables.py b/tests/helpers/test_script_variables.py index ca942acdf66..3675c857279 100644 --- a/tests/helpers/test_script_variables.py +++ b/tests/helpers/test_script_variables.py @@ -3,7 +3,8 @@ import pytest from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, template +from homeassistant.exceptions import TemplateError +from homeassistant.helpers import config_validation as cv async def test_static_vars() -> None: @@ -110,5 +111,5 @@ async def test_template_vars_run_args_no_default(hass: HomeAssistant) -> None: async def test_template_vars_error(hass: HomeAssistant) -> None: """Test template vars.""" var = cv.SCRIPT_VARIABLES_SCHEMA({"hello": "{{ canont.work }}"}) - with pytest.raises(template.TemplateError): + with pytest.raises(TemplateError): var.async_render(hass, None) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 7bb5624d112..8eb411fc4ee 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -15,14 +15,17 @@ import pytest from homeassistant import bootstrap, loader, runner import homeassistant.config as config_util from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEBUG, SIGNAL_BOOTSTRAP_INTEGRATIONS +from homeassistant.const import ( + BASE_PLATFORMS, + CONF_DEBUG, + SIGNAL_BOOTSTRAP_INTEGRATIONS, +) from homeassistant.core import CoreState, HomeAssistant, async_get_hass, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.translation import async_translations_loaded from homeassistant.helpers.typing import ConfigType from homeassistant.loader import Integration -from homeassistant.setup import BASE_PLATFORMS from .common import ( MockConfigEntry, diff --git a/tests/test_config.py b/tests/test_config.py index e15dcf31726..9ea227767db 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -29,15 +29,11 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, + CONF_PACKAGES, __version__, ) -from homeassistant.core import ( - DOMAIN as HA_DOMAIN, - ConfigSource, - HomeAssistant, - HomeAssistantError, -) -from homeassistant.exceptions import ConfigValidationError +from homeassistant.core import DOMAIN as HA_DOMAIN, ConfigSource, HomeAssistant +from homeassistant.exceptions import ConfigValidationError, HomeAssistantError from homeassistant.helpers import ( check_config, config_validation as cv, @@ -1070,11 +1066,7 @@ async def test_check_ha_config_file_wrong(mock_check, hass: HomeAssistant) -> No "hass_config", [ { - HA_DOMAIN: { - config_util.CONF_PACKAGES: { - "pack_dict": {"input_boolean": {"ib1": None}} - } - }, + HA_DOMAIN: {CONF_PACKAGES: {"pack_dict": {"input_boolean": {"ib1": None}}}}, "input_boolean": {"ib2": None}, "light": {"platform": "test"}, } @@ -1088,7 +1080,7 @@ async def test_async_hass_config_yaml_merge( conf = await config_util.async_hass_config_yaml(hass) assert merge_log_err.call_count == 0 - assert conf[HA_DOMAIN].get(config_util.CONF_PACKAGES) is not None + assert conf[HA_DOMAIN].get(CONF_PACKAGES) is not None assert len(conf) == 3 assert len(conf["input_boolean"]) == 2 assert len(conf["light"]) == 1 @@ -1116,7 +1108,7 @@ async def test_merge(merge_log_err: MagicMock, hass: HomeAssistant) -> None: }, } config = { - HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {CONF_PACKAGES: packages}, "input_boolean": {"ib2": None}, "light": {"platform": "test"}, "automation": [], @@ -1143,7 +1135,7 @@ async def test_merge_try_falsy(merge_log_err: MagicMock, hass: HomeAssistant) -> "pack_list2": {"light": OrderedDict()}, } config = { - HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {CONF_PACKAGES: packages}, "automation": {"do": "something"}, "light": {"some": "light"}, } @@ -1166,7 +1158,7 @@ async def test_merge_new(merge_log_err: MagicMock, hass: HomeAssistant) -> None: "api": {}, }, } - config = {HA_DOMAIN: {config_util.CONF_PACKAGES: packages}} + config = {HA_DOMAIN: {CONF_PACKAGES: packages}} await config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 0 @@ -1186,7 +1178,7 @@ async def test_merge_type_mismatch( "pack_2": {"light": {"ib1": None}}, # light gets merged - ensure_list } config = { - HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {CONF_PACKAGES: packages}, "input_boolean": {"ib2": None}, "input_select": [{"ib2": None}], "light": [{"platform": "two"}], @@ -1204,13 +1196,13 @@ async def test_merge_once_only_keys( ) -> None: """Test if we have a merge for a comp that may occur only once. Keys.""" packages = {"pack_2": {"api": None}} - config = {HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, "api": None} + config = {HA_DOMAIN: {CONF_PACKAGES: packages}, "api": None} await config_util.merge_packages_config(hass, config, packages) assert config["api"] == OrderedDict() packages = {"pack_2": {"api": {"key_3": 3}}} config = { - HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {CONF_PACKAGES: packages}, "api": {"key_1": 1, "key_2": 2}, } await config_util.merge_packages_config(hass, config, packages) @@ -1219,7 +1211,7 @@ async def test_merge_once_only_keys( # Duplicate keys error packages = {"pack_2": {"api": {"key": 2}}} config = { - HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {CONF_PACKAGES: packages}, "api": {"key": 1}, } await config_util.merge_packages_config(hass, config, packages) @@ -1234,7 +1226,7 @@ async def test_merge_once_only_lists(hass: HomeAssistant) -> None: } } config = { - HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {CONF_PACKAGES: packages}, "api": {"list_1": ["item_1"]}, } await config_util.merge_packages_config(hass, config, packages) @@ -1257,7 +1249,7 @@ async def test_merge_once_only_dictionaries(hass: HomeAssistant) -> None: } } config = { - HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {CONF_PACKAGES: packages}, "api": {"dict_1": {"key_1": 1, "dict_1.1": {"key_1.1": 1.1}}}, } await config_util.merge_packages_config(hass, config, packages) @@ -1293,7 +1285,7 @@ async def test_merge_duplicate_keys( """Test if keys in dicts are duplicates.""" packages = {"pack_1": {"input_select": {"ib1": None}}} config = { - HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {CONF_PACKAGES: packages}, "input_select": {"ib1": 1}, } await config_util.merge_packages_config(hass, config, packages) @@ -1451,7 +1443,7 @@ async def test_merge_split_component_definition(hass: HomeAssistant) -> None: "pack_1": {"light one": {"l1": None}}, "pack_2": {"light two": {"l2": None}, "light three": {"l3": None}}, } - config = {HA_DOMAIN: {config_util.CONF_PACKAGES: packages}} + config = {HA_DOMAIN: {CONF_PACKAGES: packages}} await config_util.merge_packages_config(hass, config, packages) assert len(config) == 4 @@ -2340,7 +2332,7 @@ async def test_packages_schema_validation_error( ] assert error_records == snapshot - assert len(config[HA_DOMAIN][config_util.CONF_PACKAGES]) == 0 + assert len(config[HA_DOMAIN][CONF_PACKAGES]) == 0 def test_extract_domain_configs() -> None: diff --git a/tests/test_core.py b/tests/test_core.py index 671b9fe400b..8035236fd08 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1629,7 +1629,7 @@ async def test_serviceregistry_call_non_existing_with_blocking( hass: HomeAssistant, ) -> None: """Test non-existing with blocking.""" - with pytest.raises(ha.ServiceNotFound): + with pytest.raises(ServiceNotFound): await hass.services.async_call("test_domain", "i_do_not_exist", blocking=True) @@ -2529,14 +2529,14 @@ async def test_reserving_states(hass: HomeAssistant) -> None: hass.states.async_set("light.bedroom", "on") assert hass.states.async_available("light.bedroom") is False - with pytest.raises(ha.HomeAssistantError): + with pytest.raises(HomeAssistantError): hass.states.async_reserve("light.bedroom") hass.states.async_remove("light.bedroom") assert hass.states.async_available("light.bedroom") is True hass.states.async_set("light.bedroom", "on") - with pytest.raises(ha.HomeAssistantError): + with pytest.raises(HomeAssistantError): hass.states.async_reserve("light.bedroom") assert hass.states.async_available("light.bedroom") is False @@ -2840,7 +2840,7 @@ async def test_state_change_events_context_id_match_state_time( hass: HomeAssistant, ) -> None: """Test last_updated, timed_fired, and the ulid all have the same time.""" - events = async_capture_events(hass, ha.EVENT_STATE_CHANGED) + events = async_capture_events(hass, EVENT_STATE_CHANGED) hass.states.async_set("light.bedroom", "on") await hass.async_block_till_done() state: State = hass.states.get("light.bedroom") @@ -2859,7 +2859,7 @@ async def test_state_change_events_match_time_with_limits_of_precision( a bit better than the precision of datetime.now() which is used for last_updated on some platforms. """ - events = async_capture_events(hass, ha.EVENT_STATE_CHANGED) + events = async_capture_events(hass, EVENT_STATE_CHANGED) hass.states.async_set("light.bedroom", "on") await hass.async_block_till_done() state: State = hass.states.get("light.bedroom") diff --git a/tests/test_runner.py b/tests/test_runner.py index 90678454adf..141af4f4bc7 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -2,6 +2,7 @@ import asyncio from collections.abc import Iterator +import subprocess import threading from unittest.mock import patch @@ -169,21 +170,21 @@ def test_enable_posix_spawn() -> None: yield from packaging.tags.parse_tag("cp311-cp311-musllinux_1_1_x86_64") with ( - patch.object(runner.subprocess, "_USE_POSIX_SPAWN", False), + patch.object(subprocess, "_USE_POSIX_SPAWN", False), patch( "homeassistant.runner.packaging.tags.sys_tags", side_effect=_mock_sys_tags_musl, ), ): runner._enable_posix_spawn() - assert runner.subprocess._USE_POSIX_SPAWN is True + assert subprocess._USE_POSIX_SPAWN is True with ( - patch.object(runner.subprocess, "_USE_POSIX_SPAWN", False), + patch.object(subprocess, "_USE_POSIX_SPAWN", False), patch( "homeassistant.runner.packaging.tags.sys_tags", side_effect=_mock_sys_tags_any, ), ): runner._enable_posix_spawn() - assert runner.subprocess._USE_POSIX_SPAWN is False + assert subprocess._USE_POSIX_SPAWN is False diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index d94de23088b..ece65504ed6 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -566,8 +566,8 @@ def test_no_recursive_secrets() -> None: def test_input_class() -> None: """Test input class.""" - yaml_input = yaml_loader.Input("hello") - yaml_input2 = yaml_loader.Input("hello") + yaml_input = yaml.Input("hello") + yaml_input2 = yaml.Input("hello") assert yaml_input.name == "hello" assert yaml_input == yaml_input2 From d1d2ce1270c55d86abbc632de7f100423eaf3fb7 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Sat, 20 Jul 2024 05:16:48 -0400 Subject: [PATCH 1368/2411] Sonos tests snapshot and restore services (#122198) --- tests/components/sonos/test_media_player.py | 67 ++++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index ab9b598bb04..0a9b1960910 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -2,6 +2,7 @@ import logging from typing import Any +from unittest.mock import patch import pytest @@ -12,8 +13,16 @@ from homeassistant.components.media_player import ( SERVICE_SELECT_SOURCE, MediaPlayerEnqueue, ) -from homeassistant.components.sonos.const import SOURCE_LINEIN, SOURCE_TV -from homeassistant.components.sonos.media_player import LONG_SERVICE_TIMEOUT +from homeassistant.components.sonos.const import ( + DOMAIN as SONOS_DOMAIN, + SOURCE_LINEIN, + SOURCE_TV, +) +from homeassistant.components.sonos.media_player import ( + LONG_SERVICE_TIMEOUT, + SERVICE_RESTORE, + SERVICE_SNAPSHOT, +) from homeassistant.const import STATE_IDLE from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError @@ -22,8 +31,9 @@ from homeassistant.helpers.device_registry import ( CONNECTION_UPNP, DeviceRegistry, ) +from homeassistant.setup import async_setup_component -from .conftest import MockMusicServiceItem, SoCoMockFactory +from .conftest import MockMusicServiceItem, MockSoCo, SoCoMockFactory async def test_device_registry( @@ -707,3 +717,54 @@ async def test_play_media_favorite_item_id( blocking=True, ) assert "UNKNOWN_ID" in str(sve.value) + + +async def _setup_hass(hass: HomeAssistant): + await async_setup_component( + hass, + SONOS_DOMAIN, + { + "sonos": { + "media_player": { + "interface_addr": "127.0.0.1", + "hosts": ["10.10.10.1", "10.10.10.2"], + } + } + }, + ) + await hass.async_block_till_done() + + +async def test_service_snapshot_restore( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, +) -> None: + """Test the snapshot and restore services.""" + soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room") + soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom") + await _setup_hass(hass) + with patch( + "homeassistant.components.sonos.speaker.Snapshot.snapshot" + ) as mock_snapshot: + await hass.services.async_call( + SONOS_DOMAIN, + SERVICE_SNAPSHOT, + { + "entity_id": ["media_player.living_room", "media_player.bedroom"], + }, + blocking=True, + ) + assert mock_snapshot.call_count == 2 + + with patch( + "homeassistant.components.sonos.speaker.Snapshot.restore" + ) as mock_restore: + await hass.services.async_call( + SONOS_DOMAIN, + SERVICE_RESTORE, + { + "entity_id": ["media_player.living_room", "media_player.bedroom"], + }, + blocking=True, + ) + assert mock_restore.call_count == 2 From 153b69c971d81d7b1fb7de3d3640dfdca2f11a89 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 20 Jul 2024 11:17:40 +0200 Subject: [PATCH 1369/2411] Fix recorder setup hanging if non live schema migration fails (#122207) --- homeassistant/components/recorder/core.py | 1 + tests/components/recorder/test_migrate.py | 16 ++++++++++++-- tests/conftest.py | 27 ++++++++++++++++++----- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 2b8f45703b5..3f284bdd83d 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -773,6 +773,7 @@ class Recorder(threading.Thread): "Database Migration Failed", "recorder_database_migration", ) + self.hass.add_job(self._async_startup_failed) return if not database_was_ready: diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 682c0a55767..f32f5c4aaaf 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -200,8 +200,14 @@ async def test_database_migration_encounters_corruption( assert move_away.called +@pytest.mark.parametrize( + ("live_migration", "expected_setup_result"), [(True, True), (False, False)] +) async def test_database_migration_encounters_corruption_not_sqlite( - hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator + hass: HomeAssistant, + async_setup_recorder_instance: RecorderInstanceGenerator, + live_migration: bool, + expected_setup_result: bool, ) -> None: """Test we fail on database error when we cannot recover.""" assert recorder.util.async_migration_in_progress(hass) is False @@ -226,8 +232,14 @@ async def test_database_migration_encounters_corruption_not_sqlite( "homeassistant.components.persistent_notification.dismiss", side_effect=pn.dismiss, ) as mock_dismiss, + patch( + "homeassistant.components.recorder.core.migration.live_migration", + return_value=live_migration, + ), ): - await async_setup_recorder_instance(hass, wait_recorder=False) + await async_setup_recorder_instance( + hass, wait_recorder=False, expected_setup_result=expected_setup_result + ) hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() diff --git a/tests/conftest.py b/tests/conftest.py index 2c8c351f165..f21dfbec5e7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1394,6 +1394,8 @@ async def _async_init_recorder_component( hass: HomeAssistant, add_config: dict[str, Any] | None = None, db_url: str | None = None, + *, + expected_setup_result: bool, ) -> None: """Initialize the recorder asynchronously.""" # pylint: disable-next=import-outside-toplevel @@ -1408,10 +1410,13 @@ async def _async_init_recorder_component( with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True): if recorder.DOMAIN not in hass.data: recorder_helper.async_initialize_recorder(hass) - assert await async_setup_component( - hass, recorder.DOMAIN, {recorder.DOMAIN: config} + setup_task = asyncio.ensure_future( + async_setup_component(hass, recorder.DOMAIN, {recorder.DOMAIN: config}) ) - assert recorder.DOMAIN in hass.config.components + # Wait for recorder integration to setup + setup_result = await setup_task + assert setup_result == expected_setup_result + assert (recorder.DOMAIN in hass.config.components) == expected_setup_result _LOGGER.info( "Test recorder successfully started, database location: %s", config[recorder.CONF_DB_URL], @@ -1527,10 +1532,16 @@ async def async_test_recorder( hass: HomeAssistant, config: ConfigType | None = None, *, + expected_setup_result: bool = True, wait_recorder: bool = True, ) -> AsyncGenerator[recorder.Recorder]: """Setup and return recorder instance.""" # noqa: D401 - await _async_init_recorder_component(hass, config, recorder_db_url) + await _async_init_recorder_component( + hass, + config, + recorder_db_url, + expected_setup_result=expected_setup_result, + ) await hass.async_block_till_done() instance = hass.data[recorder.DATA_INSTANCE] # The recorder's worker is not started until Home Assistant is running @@ -1557,12 +1568,18 @@ async def async_setup_recorder_instance( hass: HomeAssistant, config: ConfigType | None = None, *, + expected_setup_result: bool = True, wait_recorder: bool = True, ) -> AsyncGenerator[recorder.Recorder]: """Set up and return recorder instance.""" return await stack.enter_async_context( - async_test_recorder(hass, config, wait_recorder=wait_recorder) + async_test_recorder( + hass, + config, + expected_setup_result=expected_setup_result, + wait_recorder=wait_recorder, + ) ) yield async_setup_recorder From 221480add1d37aca66bb902f812802ec99b4d451 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sat, 20 Jul 2024 11:20:46 +0200 Subject: [PATCH 1370/2411] Improve switch platform test COV for enphase_envoy (#122227) --- tests/components/enphase_envoy/test_switch.py | 109 +++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/tests/components/enphase_envoy/test_switch.py b/tests/components/enphase_envoy/test_switch.py index 092e63213a9..5a549257685 100644 --- a/tests/components/enphase_envoy/test_switch.py +++ b/tests/components/enphase_envoy/test_switch.py @@ -12,6 +12,8 @@ from homeassistant.const import ( SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_CLOSED, + STATE_OFF, STATE_ON, ) from homeassistant.core import HomeAssistant @@ -64,12 +66,12 @@ async def test_no_switch( @pytest.mark.parametrize( ("mock_envoy"), ["envoy_metered_batt_relay"], indirect=["mock_envoy"] ) -async def test_switch_operation( +async def test_switch_grid_operation( hass: HomeAssistant, mock_envoy: AsyncMock, config_entry: MockConfigEntry, ) -> None: - """Test switch platform operation.""" + """Test switch platform operation for grid switches.""" with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SWITCH]): await setup_integration(hass, config_entry) @@ -106,3 +108,106 @@ async def test_switch_operation( blocking=True, ) mock_envoy.go_off_grid.assert_awaited_once_with() + mock_envoy.go_off_grid.reset_mock() + + test_entity = f"{Platform.SWITCH}.enpower_{sn}_charge_from_grid" + + # validate envoy value is reflected in entity + assert (entity_state := hass.states.get(test_entity)) + assert entity_state.state == STATE_ON + + # test grid status switch operation + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: test_entity}, + blocking=True, + ) + mock_envoy.disable_charge_from_grid.assert_awaited_once_with() + mock_envoy.disable_charge_from_grid.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: test_entity}, + blocking=True, + ) + mock_envoy.enable_charge_from_grid.assert_awaited_once_with() + mock_envoy.enable_charge_from_grid.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: test_entity}, + blocking=True, + ) + mock_envoy.disable_charge_from_grid.assert_awaited_once_with() + mock_envoy.disable_charge_from_grid.reset_mock() + + +@pytest.mark.parametrize( + ("mock_envoy"), ["envoy_metered_batt_relay"], indirect=["mock_envoy"] +) +async def test_switch_relay_operation( + hass: HomeAssistant, + mock_envoy: AsyncMock, + config_entry: MockConfigEntry, +) -> None: + """Test enphase_envoy switch relay entities operation.""" + with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SWITCH]): + await setup_integration(hass, config_entry) + + entity_base = f"{Platform.SWITCH}." + + for contact_id, dry_contact in mock_envoy.data.dry_contact_settings.items(): + name = dry_contact.load_name.lower().replace(" ", "_") + test_entity = f"{entity_base}{name}" + target_value = mock_envoy.data.dry_contact_status[contact_id].status + assert (entity_state := hass.states.get(test_entity)) + assert ( + entity_state.state == STATE_ON + if target_value == STATE_CLOSED + else STATE_OFF + ) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: test_entity}, + blocking=True, + ) + + mock_envoy.open_dry_contact.assert_awaited_once_with(contact_id) + mock_envoy.close_dry_contact.assert_not_awaited() + mock_envoy.open_dry_contact.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: test_entity}, + blocking=True, + ) + + mock_envoy.close_dry_contact.assert_awaited_once_with(contact_id) + mock_envoy.open_dry_contact.assert_not_awaited() + mock_envoy.close_dry_contact.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: test_entity}, + blocking=True, + ) + + assert ( + mock_envoy.open_dry_contact.await_count + if target_value == STATE_CLOSED + else mock_envoy.close_dry_contact.await_count + ) == 1 + assert ( + mock_envoy.close_dry_contact.await_count + if target_value == STATE_CLOSED + else mock_envoy.open_dry_contact.await_count + ) == 0 + mock_envoy.open_dry_contact.reset_mock() + mock_envoy.close_dry_contact.reset_mock() From 6f9e39cd3f5106f38a41f560189ae5008ea01d85 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 20 Jul 2024 19:22:15 +1000 Subject: [PATCH 1371/2411] Add diagnostics to Tesla Fleet (#122223) --- .../components/tesla_fleet/diagnostics.py | 54 +++ .../snapshots/test_diagnostics.ambr | 436 ++++++++++++++++++ .../tesla_fleet/test_diagnostics.py | 27 ++ 3 files changed, 517 insertions(+) create mode 100644 homeassistant/components/tesla_fleet/diagnostics.py create mode 100644 tests/components/tesla_fleet/snapshots/test_diagnostics.ambr create mode 100644 tests/components/tesla_fleet/test_diagnostics.py diff --git a/homeassistant/components/tesla_fleet/diagnostics.py b/homeassistant/components/tesla_fleet/diagnostics.py new file mode 100644 index 00000000000..0dc4cddbfc9 --- /dev/null +++ b/homeassistant/components/tesla_fleet/diagnostics.py @@ -0,0 +1,54 @@ +"""Provides diagnostics for Tesla Fleet.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.core import HomeAssistant + +from . import TeslaFleetConfigEntry + +VEHICLE_REDACT = [ + "id", + "user_id", + "vehicle_id", + "vin", + "tokens", + "id_s", + "drive_state_active_route_latitude", + "drive_state_active_route_longitude", + "drive_state_latitude", + "drive_state_longitude", + "drive_state_native_latitude", + "drive_state_native_longitude", +] + +ENERGY_LIVE_REDACT = ["vin"] +ENERGY_INFO_REDACT = ["installation_date", "serial_number"] + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: TeslaFleetConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + vehicles = [ + { + "data": async_redact_data(x.coordinator.data, VEHICLE_REDACT), + } + for x in entry.runtime_data.vehicles + ] + energysites = [ + { + "live": async_redact_data(x.live_coordinator.data, ENERGY_LIVE_REDACT), + "info": async_redact_data(x.info_coordinator.data, ENERGY_INFO_REDACT), + } + for x in entry.runtime_data.energysites + ] + + # Return only the relevant children + return { + "vehicles": vehicles, + "energysites": energysites, + "scopes": entry.runtime_data.scopes, + } diff --git a/tests/components/tesla_fleet/snapshots/test_diagnostics.ambr b/tests/components/tesla_fleet/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..902c7af131e --- /dev/null +++ b/tests/components/tesla_fleet/snapshots/test_diagnostics.ambr @@ -0,0 +1,436 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'energysites': list([ + dict({ + 'info': dict({ + 'backup_reserve_percent': 0, + 'battery_count': 2, + 'components_backup': True, + 'components_backup_time_remaining_enabled': True, + 'components_batteries': list([ + dict({ + 'device_id': 'battery-1-id', + 'din': 'battery-1-din', + 'nameplate_energy': 13500, + 'nameplate_max_charge_power': 5000, + 'nameplate_max_discharge_power': 5000, + 'part_name': 'Powerwall 2', + 'part_number': '3012170-10-B', + 'part_type': 2, + 'serial_number': '**REDACTED**', + }), + dict({ + 'device_id': 'battery-2-id', + 'din': 'battery-2-din', + 'nameplate_energy': 13500, + 'nameplate_max_charge_power': 5000, + 'nameplate_max_discharge_power': 5000, + 'part_name': 'Powerwall 2', + 'part_number': '3012170-05-C', + 'part_type': 2, + 'serial_number': '**REDACTED**', + }), + ]), + 'components_battery': True, + 'components_battery_solar_offset_view_enabled': True, + 'components_battery_type': 'ac_powerwall', + 'components_car_charging_data_supported': False, + 'components_configurable': True, + 'components_customer_preferred_export_rule': 'pv_only', + 'components_disallow_charge_from_grid_with_solar_installed': True, + 'components_energy_service_self_scheduling_enabled': True, + 'components_energy_value_header': 'Energy Value', + 'components_energy_value_subheader': 'Estimated Value', + 'components_flex_energy_request_capable': False, + 'components_gateway': 'teg', + 'components_gateways': list([ + dict({ + 'device_id': 'gateway-id', + 'din': 'gateway-din', + 'firmware_version': '24.4.0 0fe780c9', + 'is_active': True, + 'part_name': 'Tesla Backup Gateway 2', + 'part_number': '1152100-14-J', + 'part_type': 10, + 'serial_number': '**REDACTED**', + 'site_id': '1234-abcd', + 'updated_datetime': '2024-05-14T00:00:00.000Z', + }), + ]), + 'components_grid': True, + 'components_grid_services_enabled': False, + 'components_load_meter': True, + 'components_net_meter_mode': 'battery_ok', + 'components_off_grid_vehicle_charging_reserve_supported': True, + 'components_set_islanding_mode_enabled': True, + 'components_show_grid_import_battery_source_cards': True, + 'components_solar': True, + 'components_solar_type': 'pv_panel', + 'components_solar_value_enabled': True, + 'components_storm_mode_capable': True, + 'components_system_alerts_enabled': True, + 'components_tou_capable': True, + 'components_vehicle_charging_performance_view_enabled': False, + 'components_vehicle_charging_solar_offset_view_enabled': False, + 'components_wall_connectors': list([ + dict({ + 'device_id': '123abc', + 'din': 'abd-123', + 'is_active': True, + 'part_name': 'Gen 3 Wall Connector', + }), + dict({ + 'device_id': '234bcd', + 'din': 'bcd-234', + 'is_active': True, + 'part_name': 'Gen 3 Wall Connector', + }), + ]), + 'components_wifi_commissioning_enabled': True, + 'default_real_mode': 'self_consumption', + 'id': '1233-abcd', + 'installation_date': '**REDACTED**', + 'installation_time_zone': '', + 'max_site_meter_power_ac': 1000000000, + 'min_site_meter_power_ac': -1000000000, + 'nameplate_energy': 40500, + 'nameplate_power': 15000, + 'site_name': 'Site', + 'tou_settings_optimization_strategy': 'economics', + 'tou_settings_schedule': list([ + dict({ + 'end_seconds': 3600, + 'start_seconds': 0, + 'target': 'off_peak', + 'week_days': list([ + 1, + 0, + ]), + }), + dict({ + 'end_seconds': 0, + 'start_seconds': 3600, + 'target': 'peak', + 'week_days': list([ + 1, + 0, + ]), + }), + ]), + 'user_settings_breaker_alert_enabled': False, + 'user_settings_go_off_grid_test_banner_enabled': False, + 'user_settings_powerwall_onboarding_settings_set': True, + 'user_settings_powerwall_tesla_electric_interested_in': False, + 'user_settings_storm_mode_enabled': True, + 'user_settings_sync_grid_alert_enabled': True, + 'user_settings_vpp_tour_enabled': True, + 'version': '23.44.0 eb113390', + 'vpp_backup_reserve_percent': 0, + }), + 'live': dict({ + 'backup_capable': True, + 'battery_power': 5060, + 'energy_left': 38896.47368421053, + 'generator_power': 0, + 'grid_power': 0, + 'grid_services_active': False, + 'grid_services_power': 0, + 'grid_status': 'Active', + 'island_status': 'on_grid', + 'load_power': 6245, + 'percentage_charged': 95.50537403739663, + 'solar_power': 1185, + 'storm_mode_active': False, + 'timestamp': '2024-01-01T00:00:00+00:00', + 'total_pack_energy': 40727, + 'wall_connectors': dict({ + 'abd-123': dict({ + 'din': 'abd-123', + 'wall_connector_fault_state': 2, + 'wall_connector_power': 0, + 'wall_connector_state': 2, + }), + 'bcd-234': dict({ + 'din': 'bcd-234', + 'wall_connector_fault_state': 2, + 'wall_connector_power': 0, + 'wall_connector_state': 2, + }), + }), + }), + }), + ]), + 'scopes': list([ + 'openid', + 'offline_access', + 'vehicle_device_data', + 'vehicle_cmds', + 'vehicle_charging_cmds', + 'energy_device_data', + 'energy_cmds', + ]), + 'vehicles': list([ + dict({ + 'data': dict({ + 'access_type': 'OWNER', + 'api_version': 71, + 'backseat_token': None, + 'backseat_token_updated_at': None, + 'ble_autopair_enrolled': False, + 'calendar_enabled': True, + 'charge_state_battery_heater_on': False, + 'charge_state_battery_level': 77, + 'charge_state_battery_range': 266.87, + 'charge_state_charge_amps': 16, + 'charge_state_charge_current_request': 16, + 'charge_state_charge_current_request_max': 16, + 'charge_state_charge_enable_request': True, + 'charge_state_charge_energy_added': 0, + 'charge_state_charge_limit_soc': 80, + 'charge_state_charge_limit_soc_max': 100, + 'charge_state_charge_limit_soc_min': 50, + 'charge_state_charge_limit_soc_std': 80, + 'charge_state_charge_miles_added_ideal': 0, + 'charge_state_charge_miles_added_rated': 0, + 'charge_state_charge_port_cold_weather_mode': False, + 'charge_state_charge_port_color': '', + 'charge_state_charge_port_door_open': True, + 'charge_state_charge_port_latch': 'Engaged', + 'charge_state_charge_rate': 0, + 'charge_state_charger_actual_current': 0, + 'charge_state_charger_phases': None, + 'charge_state_charger_pilot_current': 16, + 'charge_state_charger_power': 0, + 'charge_state_charger_voltage': 2, + 'charge_state_charging_state': 'Stopped', + 'charge_state_conn_charge_cable': 'IEC', + 'charge_state_est_battery_range': 275.04, + 'charge_state_fast_charger_brand': '', + 'charge_state_fast_charger_present': False, + 'charge_state_fast_charger_type': 'ACSingleWireCAN', + 'charge_state_ideal_battery_range': 266.87, + 'charge_state_max_range_charge_counter': 0, + 'charge_state_minutes_to_full_charge': 0, + 'charge_state_not_enough_power_to_heat': None, + 'charge_state_off_peak_charging_enabled': False, + 'charge_state_off_peak_charging_times': 'all_week', + 'charge_state_off_peak_hours_end_time': 900, + 'charge_state_preconditioning_enabled': False, + 'charge_state_preconditioning_times': 'all_week', + 'charge_state_scheduled_charging_mode': 'Off', + 'charge_state_scheduled_charging_pending': False, + 'charge_state_scheduled_charging_start_time': None, + 'charge_state_scheduled_charging_start_time_app': 600, + 'charge_state_scheduled_departure_time': 1704837600, + 'charge_state_scheduled_departure_time_minutes': 480, + 'charge_state_supercharger_session_trip_planner': False, + 'charge_state_time_to_full_charge': 0, + 'charge_state_timestamp': 1705707520649, + 'charge_state_trip_charging': False, + 'charge_state_usable_battery_level': 77, + 'charge_state_user_charge_enable_request': None, + 'climate_state_allow_cabin_overheat_protection': True, + 'climate_state_auto_seat_climate_left': True, + 'climate_state_auto_seat_climate_right': True, + 'climate_state_auto_steering_wheel_heat': False, + 'climate_state_battery_heater': False, + 'climate_state_battery_heater_no_power': None, + 'climate_state_cabin_overheat_protection': 'On', + 'climate_state_cabin_overheat_protection_actively_cooling': False, + 'climate_state_climate_keeper_mode': 'keep', + 'climate_state_cop_activation_temperature': 'High', + 'climate_state_defrost_mode': 0, + 'climate_state_driver_temp_setting': 22, + 'climate_state_fan_status': 0, + 'climate_state_hvac_auto_request': 'On', + 'climate_state_inside_temp': 29.8, + 'climate_state_is_auto_conditioning_on': False, + 'climate_state_is_climate_on': True, + 'climate_state_is_front_defroster_on': False, + 'climate_state_is_preconditioning': False, + 'climate_state_is_rear_defroster_on': False, + 'climate_state_left_temp_direction': 251, + 'climate_state_max_avail_temp': 28, + 'climate_state_min_avail_temp': 15, + 'climate_state_outside_temp': 30, + 'climate_state_passenger_temp_setting': 22, + 'climate_state_remote_heater_control_enabled': False, + 'climate_state_right_temp_direction': 251, + 'climate_state_seat_heater_left': 0, + 'climate_state_seat_heater_rear_center': 0, + 'climate_state_seat_heater_rear_left': 0, + 'climate_state_seat_heater_rear_right': 0, + 'climate_state_seat_heater_right': 0, + 'climate_state_side_mirror_heaters': False, + 'climate_state_steering_wheel_heat_level': 0, + 'climate_state_steering_wheel_heater': False, + 'climate_state_supports_fan_only_cabin_overheat_protection': True, + 'climate_state_timestamp': 1705707520649, + 'climate_state_wiper_blade_heater': False, + 'color': None, + 'drive_state_active_route_latitude': '**REDACTED**', + 'drive_state_active_route_longitude': '**REDACTED**', + 'drive_state_active_route_miles_to_arrival': 0.039491, + 'drive_state_active_route_minutes_to_arrival': 0.103577, + 'drive_state_active_route_traffic_minutes_delay': 0, + 'drive_state_gps_as_of': 1701129612, + 'drive_state_heading': 185, + 'drive_state_latitude': '**REDACTED**', + 'drive_state_longitude': '**REDACTED**', + 'drive_state_native_latitude': '**REDACTED**', + 'drive_state_native_location_supported': 1, + 'drive_state_native_longitude': '**REDACTED**', + 'drive_state_native_type': 'wgs', + 'drive_state_power': -7, + 'drive_state_shift_state': None, + 'drive_state_speed': None, + 'drive_state_timestamp': 1705707520649, + 'granular_access_hide_private': False, + 'gui_settings_gui_24_hour_time': False, + 'gui_settings_gui_charge_rate_units': 'kW', + 'gui_settings_gui_distance_units': 'km/hr', + 'gui_settings_gui_range_display': 'Rated', + 'gui_settings_gui_temperature_units': 'C', + 'gui_settings_gui_tirepressure_units': 'Psi', + 'gui_settings_show_range_units': False, + 'gui_settings_timestamp': 1705707520649, + 'id': '**REDACTED**', + 'id_s': '**REDACTED**', + 'in_service': False, + 'state': 'online', + 'tokens': '**REDACTED**', + 'user_id': '**REDACTED**', + 'vehicle_config_aux_park_lamps': 'Eu', + 'vehicle_config_badge_version': 1, + 'vehicle_config_can_accept_navigation_requests': True, + 'vehicle_config_can_actuate_trunks': True, + 'vehicle_config_car_special_type': 'base', + 'vehicle_config_car_type': 'model3', + 'vehicle_config_charge_port_type': 'CCS', + 'vehicle_config_cop_user_set_temp_supported': True, + 'vehicle_config_dashcam_clip_save_supported': True, + 'vehicle_config_default_charge_to_max': False, + 'vehicle_config_driver_assist': 'TeslaAP3', + 'vehicle_config_ece_restrictions': False, + 'vehicle_config_efficiency_package': 'M32021', + 'vehicle_config_eu_vehicle': True, + 'vehicle_config_exterior_color': 'DeepBlue', + 'vehicle_config_exterior_trim': 'Black', + 'vehicle_config_exterior_trim_override': '', + 'vehicle_config_has_air_suspension': False, + 'vehicle_config_has_ludicrous_mode': False, + 'vehicle_config_has_seat_cooling': False, + 'vehicle_config_headlamp_type': 'Global', + 'vehicle_config_interior_trim_type': 'White2', + 'vehicle_config_key_version': 2, + 'vehicle_config_motorized_charge_port': True, + 'vehicle_config_paint_color_override': '0,9,25,0.7,0.04', + 'vehicle_config_performance_package': 'Base', + 'vehicle_config_plg': True, + 'vehicle_config_pws': True, + 'vehicle_config_rear_drive_unit': 'PM216MOSFET', + 'vehicle_config_rear_seat_heaters': 1, + 'vehicle_config_rear_seat_type': 0, + 'vehicle_config_rhd': True, + 'vehicle_config_roof_color': 'RoofColorGlass', + 'vehicle_config_seat_type': None, + 'vehicle_config_spoiler_type': 'None', + 'vehicle_config_sun_roof_installed': True, + 'vehicle_config_supports_qr_pairing': False, + 'vehicle_config_third_row_seats': 'None', + 'vehicle_config_timestamp': 1705707520649, + 'vehicle_config_trim_badging': '74d', + 'vehicle_config_use_range_badging': True, + 'vehicle_config_utc_offset': 36000, + 'vehicle_config_webcam_selfie_supported': True, + 'vehicle_config_webcam_supported': True, + 'vehicle_config_wheel_type': 'Pinwheel18CapKit', + 'vehicle_id': '**REDACTED**', + 'vehicle_state_api_version': 71, + 'vehicle_state_autopark_state_v2': 'unavailable', + 'vehicle_state_calendar_supported': True, + 'vehicle_state_car_version': '2023.44.30.8 06f534d46010', + 'vehicle_state_center_display_state': 0, + 'vehicle_state_dashcam_clip_save_available': True, + 'vehicle_state_dashcam_state': 'Recording', + 'vehicle_state_df': 0, + 'vehicle_state_dr': 0, + 'vehicle_state_fd_window': 0, + 'vehicle_state_feature_bitmask': 'fbdffbff,187f', + 'vehicle_state_fp_window': 0, + 'vehicle_state_ft': 0, + 'vehicle_state_is_user_present': False, + 'vehicle_state_locked': False, + 'vehicle_state_media_info_a2dp_source_name': 'Pixel 8 Pro', + 'vehicle_state_media_info_audio_volume': 1.6667, + 'vehicle_state_media_info_audio_volume_increment': 0.333333, + 'vehicle_state_media_info_audio_volume_max': 10.333333, + 'vehicle_state_media_info_media_playback_status': 'Playing', + 'vehicle_state_media_info_now_playing_album': 'Elon Musk', + 'vehicle_state_media_info_now_playing_artist': 'Walter Isaacson', + 'vehicle_state_media_info_now_playing_duration': 651000, + 'vehicle_state_media_info_now_playing_elapsed': 1000, + 'vehicle_state_media_info_now_playing_source': 'Audible', + 'vehicle_state_media_info_now_playing_station': 'Elon Musk', + 'vehicle_state_media_info_now_playing_title': 'Chapter 51: Cybertruck: Tesla, 2018–2019', + 'vehicle_state_media_state_remote_control_enabled': True, + 'vehicle_state_notifications_supported': True, + 'vehicle_state_odometer': 6481.019282, + 'vehicle_state_parsed_calendar_supported': True, + 'vehicle_state_pf': 0, + 'vehicle_state_pr': 0, + 'vehicle_state_rd_window': 0, + 'vehicle_state_remote_start': False, + 'vehicle_state_remote_start_enabled': True, + 'vehicle_state_remote_start_supported': True, + 'vehicle_state_rp_window': 0, + 'vehicle_state_rt': 0, + 'vehicle_state_santa_mode': 0, + 'vehicle_state_sentry_mode': False, + 'vehicle_state_sentry_mode_available': True, + 'vehicle_state_service_mode': False, + 'vehicle_state_service_mode_plus': False, + 'vehicle_state_software_update_download_perc': 100, + 'vehicle_state_software_update_expected_duration_sec': 2700, + 'vehicle_state_software_update_install_perc': 1, + 'vehicle_state_software_update_status': 'available', + 'vehicle_state_software_update_version': '2024.12.0.0', + 'vehicle_state_speed_limit_mode_active': False, + 'vehicle_state_speed_limit_mode_current_limit_mph': 69, + 'vehicle_state_speed_limit_mode_max_limit_mph': 120, + 'vehicle_state_speed_limit_mode_min_limit_mph': 50, + 'vehicle_state_speed_limit_mode_pin_code_set': True, + 'vehicle_state_sun_roof_state': 'open', + 'vehicle_state_timestamp': 1705707520649, + 'vehicle_state_tpms_hard_warning_fl': False, + 'vehicle_state_tpms_hard_warning_fr': False, + 'vehicle_state_tpms_hard_warning_rl': False, + 'vehicle_state_tpms_hard_warning_rr': False, + 'vehicle_state_tpms_last_seen_pressure_time_fl': 1705700812, + 'vehicle_state_tpms_last_seen_pressure_time_fr': 1705700793, + 'vehicle_state_tpms_last_seen_pressure_time_rl': 1705700794, + 'vehicle_state_tpms_last_seen_pressure_time_rr': 1705700823, + 'vehicle_state_tpms_pressure_fl': 2.775, + 'vehicle_state_tpms_pressure_fr': 2.8, + 'vehicle_state_tpms_pressure_rl': 2.775, + 'vehicle_state_tpms_pressure_rr': 2.775, + 'vehicle_state_tpms_rcp_front_value': 2.9, + 'vehicle_state_tpms_rcp_rear_value': 2.9, + 'vehicle_state_tpms_soft_warning_fl': False, + 'vehicle_state_tpms_soft_warning_fr': False, + 'vehicle_state_tpms_soft_warning_rl': False, + 'vehicle_state_tpms_soft_warning_rr': False, + 'vehicle_state_valet_mode': False, + 'vehicle_state_valet_pin_needed': False, + 'vehicle_state_vehicle_name': 'Test', + 'vehicle_state_vehicle_self_test_progress': 0, + 'vehicle_state_vehicle_self_test_requested': False, + 'vehicle_state_vehicle_state_sun_roof_percent_open': 20, + 'vehicle_state_webcam_available': True, + 'vin': '**REDACTED**', + }), + }), + ]), + }) +# --- diff --git a/tests/components/tesla_fleet/test_diagnostics.py b/tests/components/tesla_fleet/test_diagnostics.py new file mode 100644 index 00000000000..e0ef24097bb --- /dev/null +++ b/tests/components/tesla_fleet/test_diagnostics.py @@ -0,0 +1,27 @@ +"""Test the Tesla Fleet Diagnostics.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from . import setup_platform + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, + normal_config_entry: MockConfigEntry, +) -> None: + """Test diagnostics.""" + + await setup_platform(hass, normal_config_entry) + + diag = await get_diagnostics_for_config_entry( + hass, hass_client, normal_config_entry + ) + assert diag == snapshot From ecffae0b4f526423a3781a01e5ca919ba22e79a3 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 20 Jul 2024 11:25:00 +0200 Subject: [PATCH 1372/2411] Improve fixture usage for light based deCONZ tests (#122209) --- tests/components/deconz/conftest.py | 17 +- tests/components/deconz/test_cover.py | 85 +++--- tests/components/deconz/test_fan.py | 76 ++---- tests/components/deconz/test_light.py | 320 +++++++++++------------ tests/components/deconz/test_lock.py | 37 ++- tests/components/deconz/test_services.py | 34 +-- tests/components/deconz/test_siren.py | 30 +-- tests/components/deconz/test_switch.py | 31 +-- 8 files changed, 268 insertions(+), 362 deletions(-) diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py index 7794a2a305a..9beabdc2b15 100644 --- a/tests/components/deconz/conftest.py +++ b/tests/components/deconz/conftest.py @@ -119,6 +119,8 @@ def fixture_get_request( data.setdefault("alarmsystems", alarm_system_payload) data.setdefault("config", config_payload) data.setdefault("groups", group_payload) + if "state" in light_payload: + light_payload = {"0": light_payload} data.setdefault("lights", light_payload) data.setdefault("sensors", sensor_payload) @@ -172,16 +174,13 @@ def fixture_group_data() -> dict[str, Any]: @pytest.fixture(name="light_payload") -def fixture_light_0_data(light_0_payload: dict[str, Any]) -> dict[str, Any]: - """Light data.""" - if light_0_payload: - return {"0": light_0_payload} - return {} - - -@pytest.fixture(name="light_0_payload") def fixture_light_data() -> dict[str, Any]: - """Light data.""" + """Light data. + + Should be + - one light data payload {"state": ...} + - multiple lights {"1": ..., "2": ...} + """ return {} diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index abf358244c1..0d3c7aa7587 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -37,14 +37,14 @@ from tests.test_util.aiohttp import AiohttpClientMocker "light_payload", [ { - "1": { + "0": { "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", }, - "2": { + "1": { "name": "Unsupported cover", "type": "Not a cover", "state": {"reachable": True}, @@ -68,12 +68,7 @@ async def test_cover( # Event signals cover is open - event_changed_light = { - "r": "lights", - "id": "1", - "state": {"lift": 0, "open": True}, - } - await mock_websocket_data(event_changed_light) + await mock_websocket_data({"r": "lights", "state": {"lift": 0, "open": True}}) await hass.async_block_till_done() cover = hass.states.get("cover.window_covering_device") @@ -82,7 +77,7 @@ async def test_cover( # Verify service calls for cover - aioclient_mock = mock_put_request("/lights/1/state") + aioclient_mock = mock_put_request("/lights/0/state") # Service open cover @@ -140,25 +135,23 @@ async def test_cover( "light_payload", [ { - "0": { - "etag": "87269755b9b3a046485fdae8d96b252c", - "lastannounced": None, - "lastseen": "2020-08-01T16:22:05Z", - "manufacturername": "AXIS", - "modelid": "Gear", - "name": "Covering device", - "state": { - "bri": 0, - "lift": 0, - "on": False, - "open": True, - "reachable": True, - "tilt": 0, - }, - "swversion": "100-5.3.5.1122", - "type": "Window covering device", - "uniqueid": "00:24:46:00:00:12:34:56-01", - } + "etag": "87269755b9b3a046485fdae8d96b252c", + "lastannounced": None, + "lastseen": "2020-08-01T16:22:05Z", + "manufacturername": "AXIS", + "modelid": "Gear", + "name": "Covering device", + "state": { + "bri": 0, + "lift": 0, + "on": False, + "open": True, + "reachable": True, + "tilt": 0, + }, + "swversion": "100-5.3.5.1122", + "type": "Window covering device", + "uniqueid": "00:24:46:00:00:12:34:56-01", } ], ) @@ -221,25 +214,23 @@ async def test_tilt_cover( "light_payload", [ { - "0": { - "etag": "4cefc909134c8e99086b55273c2bde67", - "hascolor": False, - "lastannounced": "2022-08-08T12:06:18Z", - "lastseen": "2022-08-14T14:22Z", - "manufacturername": "Keen Home Inc", - "modelid": "SV01-410-MP-1.0", - "name": "Vent", - "state": { - "alert": "none", - "bri": 242, - "on": False, - "reachable": True, - "sat": 10, - }, - "swversion": "0x00000012", - "type": "Level controllable output", - "uniqueid": "00:22:a3:00:00:00:00:00-01", - } + "etag": "4cefc909134c8e99086b55273c2bde67", + "hascolor": False, + "lastannounced": "2022-08-08T12:06:18Z", + "lastseen": "2022-08-14T14:22Z", + "manufacturername": "Keen Home Inc", + "modelid": "SV01-410-MP-1.0", + "name": "Vent", + "state": { + "alert": "none", + "bri": 242, + "on": False, + "reachable": True, + "sat": 10, + }, + "swversion": "0x00000012", + "type": "Level controllable output", + "uniqueid": "00:22:a3:00:00:00:00:00-01", } ], ) diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index 3460ced84b2..1933b39c0b0 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -24,23 +24,21 @@ from tests.test_util.aiohttp import AiohttpClientMocker "light_payload", [ { - "1": { - "etag": "432f3de28965052961a99e3c5494daf4", - "hascolor": False, - "manufacturername": "King Of Fans, Inc.", - "modelid": "HDC52EastwindFan", - "name": "Ceiling fan", - "state": { - "alert": "none", - "bri": 254, - "on": False, - "reachable": True, - "speed": 4, - }, - "swversion": "0000000F", - "type": "Fan", - "uniqueid": "00:22:a3:00:00:27:8b:81-01", - } + "etag": "432f3de28965052961a99e3c5494daf4", + "hascolor": False, + "manufacturername": "King Of Fans, Inc.", + "modelid": "HDC52EastwindFan", + "name": "Ceiling fan", + "state": { + "alert": "none", + "bri": 254, + "on": False, + "reachable": True, + "speed": 4, + }, + "swversion": "0000000F", + "type": "Fan", + "uniqueid": "00:22:a3:00:00:27:8b:81-01", } ], ) @@ -58,56 +56,31 @@ async def test_fans( # Test states - event_changed_light = { - "r": "lights", - "id": "1", - "state": {"speed": 1}, - } - await mock_websocket_data(event_changed_light) + await mock_websocket_data({"r": "lights", "state": {"speed": 1}}) await hass.async_block_till_done() assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 25 - event_changed_light = { - "r": "lights", - "id": "1", - "state": {"speed": 2}, - } - await mock_websocket_data(event_changed_light) + await mock_websocket_data({"r": "lights", "state": {"speed": 2}}) await hass.async_block_till_done() assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 50 - event_changed_light = { - "r": "lights", - "id": "1", - "state": {"speed": 3}, - } - await mock_websocket_data(event_changed_light) + await mock_websocket_data({"r": "lights", "state": {"speed": 3}}) await hass.async_block_till_done() assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 75 - event_changed_light = { - "r": "lights", - "id": "1", - "state": {"speed": 4}, - } - await mock_websocket_data(event_changed_light) + await mock_websocket_data({"r": "lights", "state": {"speed": 4}}) await hass.async_block_till_done() assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 100 - event_changed_light = { - "r": "lights", - "id": "1", - "state": {"speed": 0}, - } - await mock_websocket_data(event_changed_light) + await mock_websocket_data({"r": "lights", "state": {"speed": 0}}) await hass.async_block_till_done() assert hass.states.get("fan.ceiling_fan").state == STATE_OFF @@ -115,7 +88,7 @@ async def test_fans( # Test service calls - aioclient_mock = mock_put_request("/lights/1/state") + aioclient_mock = mock_put_request("/lights/0/state") # Service turn on fan using saved default_on_speed @@ -199,12 +172,7 @@ async def test_fans( # Events with an unsupported speed does not get converted - event_changed_light = { - "r": "lights", - "id": "1", - "state": {"speed": 5}, - } - await mock_websocket_data(event_changed_light) + await mock_websocket_data({"r": "lights", "state": {"speed": 5}}) await hass.async_block_till_done() assert hass.states.get("fan.ceiling_fan").state == STATE_ON diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 340b0abd940..942a763ce94 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -46,7 +46,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker @pytest.mark.parametrize( - ("light_0_payload", "expected"), + ("light_payload", "expected"), [ ( # RGB light in color temp color mode { @@ -440,33 +440,31 @@ async def test_lights( "light_payload", [ { - "0": { - "colorcapabilities": 31, - "ctmax": 500, - "ctmin": 153, - "etag": "055485a82553e654f156d41c9301b7cf", - "hascolor": True, - "lastannounced": None, - "lastseen": "2021-06-10T20:25Z", - "manufacturername": "Philips", - "modelid": "LLC020", - "name": "Hue Go", - "state": { - "alert": "none", - "bri": 254, - "colormode": "ct", - "ct": 375, - "effect": "none", - "hue": 8348, - "on": True, - "reachable": True, - "sat": 147, - "xy": [0.462, 0.4111], - }, - "swversion": "5.127.1.26420", - "type": "Extended color light", - "uniqueid": "00:17:88:01:01:23:45:67-00", - } + "colorcapabilities": 31, + "ctmax": 500, + "ctmin": 153, + "etag": "055485a82553e654f156d41c9301b7cf", + "hascolor": True, + "lastannounced": None, + "lastseen": "2021-06-10T20:25Z", + "manufacturername": "Philips", + "modelid": "LLC020", + "name": "Hue Go", + "state": { + "alert": "none", + "bri": 254, + "colormode": "ct", + "ct": 375, + "effect": "none", + "hue": 8348, + "on": True, + "reachable": True, + "sat": 147, + "xy": [0.462, 0.4111], + }, + "swversion": "5.127.1.26420", + "type": "Extended color light", + "uniqueid": "00:17:88:01:01:23:45:67-00", } ], ) @@ -478,11 +476,7 @@ async def test_light_state_change( """Verify light can change state on websocket event.""" assert hass.states.get("light.hue_go").state == STATE_ON - event_changed_light = { - "r": "lights", - "state": {"on": False}, - } - await mock_websocket_data(event_changed_light) + await mock_websocket_data({"r": "lights", "state": {"on": False}}) await hass.async_block_till_done() assert hass.states.get("light.hue_go").state == STATE_OFF @@ -635,34 +629,32 @@ async def test_light_service_calls( expected: dict[str, Any], ) -> None: """Verify light can change state on websocket event.""" - light_payload |= { - "0": { - "colorcapabilities": 31, - "ctmax": 500, - "ctmin": 153, - "etag": "055485a82553e654f156d41c9301b7cf", - "hascolor": True, - "lastannounced": None, - "lastseen": "2021-06-10T20:25Z", - "manufacturername": "Philips", - "modelid": "LLC020", - "name": "Hue Go", - "state": { - "alert": "none", - "bri": 254, - "colormode": "ct", - "ct": 375, - "effect": "none", - "hue": 8348, - "on": input["light_on"], - "reachable": True, - "sat": 147, - "xy": [0.462, 0.4111], - }, - "swversion": "5.127.1.26420", - "type": "Extended color light", - "uniqueid": "00:17:88:01:01:23:45:67-00", - } + light_payload[0] = { + "colorcapabilities": 31, + "ctmax": 500, + "ctmin": 153, + "etag": "055485a82553e654f156d41c9301b7cf", + "hascolor": True, + "lastannounced": None, + "lastseen": "2021-06-10T20:25Z", + "manufacturername": "Philips", + "modelid": "LLC020", + "name": "Hue Go", + "state": { + "alert": "none", + "bri": 254, + "colormode": "ct", + "ct": 375, + "effect": "none", + "hue": 8348, + "on": input["light_on"], + "reachable": True, + "sat": 147, + "xy": [0.462, 0.4111], + }, + "swversion": "5.127.1.26420", + "type": "Extended color light", + "uniqueid": "00:17:88:01:01:23:45:67-00", } await config_entry_factory() @@ -684,29 +676,27 @@ async def test_light_service_calls( "light_payload", [ { - "0": { - "colorcapabilities": 0, - "ctmax": 65535, - "ctmin": 0, - "etag": "9dd510cd474791481f189d2a68a3c7f1", - "hascolor": True, - "lastannounced": "2020-12-17T17:44:38Z", - "lastseen": "2021-01-11T18:36Z", - "manufacturername": "IKEA of Sweden", - "modelid": "TRADFRI bulb E27 WS opal 1000lm", - "name": "IKEA light", - "state": { - "alert": "none", - "bri": 156, - "colormode": "ct", - "ct": 250, - "on": True, - "reachable": True, - }, - "swversion": "2.0.022", - "type": "Color temperature light", - "uniqueid": "ec:1b:bd:ff:fe:ee:ed:dd-01", - } + "colorcapabilities": 0, + "ctmax": 65535, + "ctmin": 0, + "etag": "9dd510cd474791481f189d2a68a3c7f1", + "hascolor": True, + "lastannounced": "2020-12-17T17:44:38Z", + "lastseen": "2021-01-11T18:36Z", + "manufacturername": "IKEA of Sweden", + "modelid": "TRADFRI bulb E27 WS opal 1000lm", + "name": "IKEA light", + "state": { + "alert": "none", + "bri": 156, + "colormode": "ct", + "ct": 250, + "on": True, + "reachable": True, + }, + "swversion": "2.0.022", + "type": "Color temperature light", + "uniqueid": "ec:1b:bd:ff:fe:ee:ed:dd-01", } ], ) @@ -754,27 +744,25 @@ async def test_ikea_default_transition_time( "light_payload", [ { - "0": { - "etag": "87a89542bf9b9d0aa8134919056844f8", - "hascolor": True, - "lastannounced": None, - "lastseen": "2020-12-05T22:57Z", - "manufacturername": "_TZE200_s8gkrkxk", - "modelid": "TS0601", - "name": "LIDL xmas light", - "state": { - "bri": 25, - "colormode": "hs", - "effect": "none", - "hue": 53691, - "on": True, - "reachable": True, - "sat": 141, - }, - "swversion": None, - "type": "Color dimmable light", - "uniqueid": "58:8e:81:ff:fe:db:7b:be-01", - } + "etag": "87a89542bf9b9d0aa8134919056844f8", + "hascolor": True, + "lastannounced": None, + "lastseen": "2020-12-05T22:57Z", + "manufacturername": "_TZE200_s8gkrkxk", + "modelid": "TS0601", + "name": "LIDL xmas light", + "state": { + "bri": 25, + "colormode": "hs", + "effect": "none", + "hue": 53691, + "on": True, + "reachable": True, + "sat": 141, + }, + "swversion": None, + "type": "Color dimmable light", + "uniqueid": "58:8e:81:ff:fe:db:7b:be-01", } ], ) @@ -803,19 +791,17 @@ async def test_lidl_christmas_light( "light_payload", [ { - "0": { - "etag": "26839cb118f5bf7ba1f2108256644010", - "hascolor": False, - "lastannounced": None, - "lastseen": "2020-11-22T11:27Z", - "manufacturername": "dresden elektronik", - "modelid": "ConBee II", - "name": "Configuration tool 1", - "state": {"reachable": True}, - "swversion": "0x264a0700", - "type": "Configuration tool", - "uniqueid": "00:21:2e:ff:ff:05:a7:a3-01", - } + "etag": "26839cb118f5bf7ba1f2108256644010", + "hascolor": False, + "lastannounced": None, + "lastseen": "2020-11-22T11:27Z", + "manufacturername": "dresden elektronik", + "modelid": "ConBee II", + "name": "Configuration tool 1", + "state": {"reachable": True}, + "swversion": "0x264a0700", + "type": "Configuration tool", + "uniqueid": "00:21:2e:ff:ff:05:a7:a3-01", } ], ) @@ -1152,7 +1138,7 @@ async def test_empty_group(hass: HomeAssistant) -> None: "state": {"all_on": False, "any_on": True}, "action": {}, "scenes": [], - "lights": ["1"], + "lights": ["0"], }, "2": { "id": "Empty group id", @@ -1170,14 +1156,12 @@ async def test_empty_group(hass: HomeAssistant) -> None: "light_payload", [ { - "1": { - "ctmax": 454, - "ctmin": 155, - "name": "Tunable white light", - "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True}, - "type": "Tunable white light", - "uniqueid": "00:00:00:00:00:00:00:01-00", - }, + "ctmax": 454, + "ctmin": 155, + "name": "Tunable white light", + "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True}, + "type": "Tunable white light", + "uniqueid": "00:00:00:00:00:00:00:01-00", } ], ) @@ -1425,7 +1409,7 @@ async def test_verify_group_supported_features(hass: HomeAssistant) -> None: "devicemembership": [], "etag": "4548e982c4cfff942f7af80958abb2a0", "id": "43", - "lights": ["13"], + "lights": ["0"], "name": "Opbergruimte", "scenes": [ { @@ -1463,47 +1447,45 @@ async def test_verify_group_supported_features(hass: HomeAssistant) -> None: "light_payload", [ { - "13": { - "capabilities": { - "alerts": [ - "none", - "select", - "lselect", - "blink", - "breathe", - "okay", - "channelchange", - "finish", - "stop", - ], - "bri": {"min_dim_level": 5}, - }, - "config": { - "bri": {"execute_if_off": True, "startup": "previous"}, - "groups": ["43"], - "on": {"startup": "previous"}, - }, - "etag": "ca0ed7763eca37f5e6b24f6d46f8a518", - "hascolor": False, - "lastannounced": None, - "lastseen": "2024-03-02T20:08Z", - "manufacturername": "Signify Netherlands B.V.", - "modelid": "LWA001", - "name": "Opbergruimte Lamp Plafond", - "productid": "Philips-LWA001-1-A19DLv5", - "productname": "Hue white lamp", - "state": { - "alert": "none", - "bri": 76, - "effect": "none", - "on": False, - "reachable": True, - }, - "swconfigid": "87169548", - "swversion": "1.104.2", - "type": "Dimmable light", - "uniqueid": "00:17:88:01:08:11:22:33-01", + "capabilities": { + "alerts": [ + "none", + "select", + "lselect", + "blink", + "breathe", + "okay", + "channelchange", + "finish", + "stop", + ], + "bri": {"min_dim_level": 5}, }, + "config": { + "bri": {"execute_if_off": True, "startup": "previous"}, + "groups": ["43"], + "on": {"startup": "previous"}, + }, + "etag": "ca0ed7763eca37f5e6b24f6d46f8a518", + "hascolor": False, + "lastannounced": None, + "lastseen": "2024-03-02T20:08Z", + "manufacturername": "Signify Netherlands B.V.", + "modelid": "LWA001", + "name": "Opbergruimte Lamp Plafond", + "productid": "Philips-LWA001-1-A19DLv5", + "productname": "Hue white lamp", + "state": { + "alert": "none", + "bri": 76, + "effect": "none", + "on": False, + "reachable": True, + }, + "swconfigid": "87169548", + "swversion": "1.104.2", + "type": "Dimmable light", + "uniqueid": "00:17:88:01:08:11:22:33-01", } ], ) @@ -1519,7 +1501,7 @@ async def test_verify_group_color_mode_fallback( await mock_websocket_data( { - "id": "13", + "id": "0", "r": "lights", "state": { "alert": "none", diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index 66260193012..3ebd4fea978 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -27,19 +27,17 @@ from tests.test_util.aiohttp import AiohttpClientMocker "light_payload", [ { - "1": { - "etag": "5c2ec06cde4bd654aef3a555fcd8ad12", - "hascolor": False, - "lastannounced": None, - "lastseen": "2020-08-22T15:29:03Z", - "manufacturername": "Danalock", - "modelid": "V3-BTZB", - "name": "Door lock", - "state": {"alert": "none", "on": False, "reachable": True}, - "swversion": "19042019", - "type": "Door Lock", - "uniqueid": "00:00:00:00:00:00:00:00-00", - } + "etag": "5c2ec06cde4bd654aef3a555fcd8ad12", + "hascolor": False, + "lastannounced": None, + "lastseen": "2020-08-22T15:29:03Z", + "manufacturername": "Danalock", + "modelid": "V3-BTZB", + "name": "Door lock", + "state": {"alert": "none", "on": False, "reachable": True}, + "swversion": "19042019", + "type": "Door Lock", + "uniqueid": "00:00:00:00:00:00:00:00-00", } ], ) @@ -53,19 +51,14 @@ async def test_lock_from_light( assert len(hass.states.async_all()) == 1 assert hass.states.get("lock.door_lock").state == STATE_UNLOCKED - event_changed_light = { - "r": "lights", - "id": "1", - "state": {"on": True}, - } - await mock_websocket_data(event_changed_light) + await mock_websocket_data({"r": "lights", "state": {"on": True}}) await hass.async_block_till_done() assert hass.states.get("lock.door_lock").state == STATE_LOCKED # Verify service calls - aioclient_mock = mock_put_request("/lights/1/state") + aioclient_mock = mock_put_request("/lights/0/state") # Service lock door @@ -137,12 +130,12 @@ async def test_lock_from_sensor( assert len(hass.states.async_all()) == 2 assert hass.states.get("lock.door_lock").state == STATE_UNLOCKED - event_changed_light = { + event_changed_sensor = { "r": "sensors", "id": "1", "state": {"lockstate": "locked"}, } - await mock_websocket_data(event_changed_light) + await mock_websocket_data(event_changed_sensor) await hass.async_block_till_done() assert hass.states.get("lock.door_lock").state == STATE_LOCKED diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 2ce3387de15..ec9ace90116 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -55,12 +55,10 @@ async def test_configure_service_with_field( "light_payload", [ { - "1": { - "name": "Test", - "state": {"reachable": True}, - "type": "Light", - "uniqueid": "00:00:00:00:00:00:00:01-00", - } + "name": "Test", + "state": {"reachable": True}, + "type": "Light", + "uniqueid": "00:00:00:00:00:00:00:01-00", } ], ) @@ -74,7 +72,7 @@ async def test_configure_service_with_entity( SERVICE_ENTITY: "light.test", SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } - aioclient_mock = mock_put_request("/lights/1") + aioclient_mock = mock_put_request("/lights/0") await hass.services.async_call( DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data, blocking=True @@ -86,12 +84,10 @@ async def test_configure_service_with_entity( "light_payload", [ { - "1": { - "name": "Test", - "state": {"reachable": True}, - "type": "Light", - "uniqueid": "00:00:00:00:00:00:00:01-00", - } + "name": "Test", + "state": {"reachable": True}, + "type": "Light", + "uniqueid": "00:00:00:00:00:00:00:01-00", } ], ) @@ -106,7 +102,7 @@ async def test_configure_service_with_entity_and_field( SERVICE_FIELD: "/state", SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } - aioclient_mock = mock_put_request("/lights/1/state") + aioclient_mock = mock_put_request("/lights/0/state") await hass.services.async_call( DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data, blocking=True @@ -312,12 +308,10 @@ async def test_service_refresh_devices_trigger_no_state_update( "light_payload", [ { - "1": { - "name": "Light 1 name", - "state": {"reachable": True}, - "type": "Light", - "uniqueid": "00:00:00:00:00:00:00:01-00", - } + "name": "Light 0 name", + "state": {"reachable": True}, + "type": "Light", + "uniqueid": "00:00:00:00:00:00:00:01-00", } ], ) diff --git a/tests/components/deconz/test_siren.py b/tests/components/deconz/test_siren.py index f745565c7e0..b8224365457 100644 --- a/tests/components/deconz/test_siren.py +++ b/tests/components/deconz/test_siren.py @@ -25,18 +25,10 @@ from tests.test_util.aiohttp import AiohttpClientMocker "light_payload", [ { - "1": { - "name": "Warning device", - "type": "Warning device", - "state": {"alert": "lselect", "reachable": True}, - "uniqueid": "00:00:00:00:00:00:00:00-00", - }, - "2": { - "name": "Unsupported siren", - "type": "Not a siren", - "state": {"reachable": True}, - "uniqueid": "00:00:00:00:00:00:00:01-00", - }, + "name": "Warning device", + "type": "Warning device", + "state": {"alert": "lselect", "reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:00-00", } ], ) @@ -47,23 +39,17 @@ async def test_sirens( mock_put_request: Callable[[str, str], AiohttpClientMocker], ) -> None: """Test that siren entities are created.""" - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 1 assert hass.states.get("siren.warning_device").state == STATE_ON - assert not hass.states.get("siren.unsupported_siren") - event_changed_light = { - "r": "lights", - "id": "1", - "state": {"alert": None}, - } - await mock_websocket_data(event_changed_light) + await mock_websocket_data({"r": "lights", "state": {"alert": None}}) await hass.async_block_till_done() assert hass.states.get("siren.warning_device").state == STATE_OFF # Verify service calls - aioclient_mock = mock_put_request("/lights/1/state") + aioclient_mock = mock_put_request("/lights/0/state") # Service turn on siren @@ -98,7 +84,7 @@ async def test_sirens( await hass.config_entries.async_unload(config_entry_setup.entry_id) states = hass.states.async_all() - assert len(states) == 2 + assert len(states) == 1 for state in states: assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 063fa3ba8ab..e6c3e93048e 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -25,29 +25,29 @@ from tests.test_util.aiohttp import AiohttpClientMocker "light_payload", [ { - "1": { + "0": { "name": "On off switch", "type": "On/Off plug-in unit", "state": {"on": True, "reachable": True}, "uniqueid": "00:00:00:00:00:00:00:00-00", }, - "2": { + "1": { "name": "Smart plug", "type": "Smart plug", "state": {"on": False, "reachable": True}, "uniqueid": "00:00:00:00:00:00:00:01-00", }, - "3": { + "2": { "name": "Unsupported switch", "type": "Not a switch", "state": {"reachable": True}, - "uniqueid": "00:00:00:00:00:00:00:03-00", + "uniqueid": "00:00:00:00:00:00:00:02-00", }, - "4": { + "3": { "name": "On off relay", "state": {"on": True, "reachable": True}, "type": "On/Off light", - "uniqueid": "00:00:00:00:00:00:00:04-00", + "uniqueid": "00:00:00:00:00:00:00:03-00", }, } ], @@ -65,19 +65,14 @@ async def test_power_plugs( assert hass.states.get("switch.on_off_relay").state == STATE_ON assert hass.states.get("switch.unsupported_switch") is None - event_changed_light = { - "r": "lights", - "id": "1", - "state": {"on": False}, - } - await mock_websocket_data(event_changed_light) + await mock_websocket_data({"r": "lights", "state": {"on": False}}) await hass.async_block_till_done() assert hass.states.get("switch.on_off_switch").state == STATE_OFF # Verify service calls - aioclient_mock = mock_put_request("/lights/1/state") + aioclient_mock = mock_put_request("/lights/0/state") # Service turn on power plug @@ -115,12 +110,10 @@ async def test_power_plugs( "light_payload", [ { - "1": { - "name": "On Off output device", - "type": "On/Off output", - "state": {"on": True, "reachable": True}, - "uniqueid": "00:00:00:00:00:00:00:00-00", - }, + "name": "On Off output device", + "type": "On/Off output", + "state": {"on": True, "reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:00-00", } ], ) From 2b93de1348bb1df9ec2ee4d3970838d83e2d76e0 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 20 Jul 2024 19:28:30 +1000 Subject: [PATCH 1373/2411] Add binary sensor to Tesla Fleet (#122225) --- .../components/tesla_fleet/__init__.py | 2 +- .../components/tesla_fleet/binary_sensor.py | 275 +++ .../components/tesla_fleet/icons.json | 38 + .../components/tesla_fleet/strings.json | 80 + .../snapshots/test_binary_sensors.ambr | 1571 +++++++++++++++++ .../tesla_fleet/test_binary_sensors.py | 64 + 6 files changed, 2029 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tesla_fleet/binary_sensor.py create mode 100644 tests/components/tesla_fleet/snapshots/test_binary_sensors.ambr create mode 100644 tests/components/tesla_fleet/test_binary_sensors.py diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index d43d3d51a41..0613f42ee61 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -34,7 +34,7 @@ from .coordinator import ( ) from .models import TeslaFleetData, TeslaFleetEnergyData, TeslaFleetVehicleData -PLATFORMS: Final = [Platform.SENSOR] +PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR] type TeslaFleetConfigEntry = ConfigEntry[TeslaFleetData] diff --git a/homeassistant/components/tesla_fleet/binary_sensor.py b/homeassistant/components/tesla_fleet/binary_sensor.py new file mode 100644 index 00000000000..2469092513a --- /dev/null +++ b/homeassistant/components/tesla_fleet/binary_sensor.py @@ -0,0 +1,275 @@ +"""Binary Sensor platform for Tesla Fleet integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from itertools import chain +from typing import cast + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import TeslaFleetConfigEntry +from .const import TeslaFleetState +from .entity import ( + TeslaFleetEnergyInfoEntity, + TeslaFleetEnergyLiveEntity, + TeslaFleetVehicleEntity, +) +from .models import TeslaFleetEnergyData, TeslaFleetVehicleData + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class TeslaFleetBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes Tesla Fleet binary sensor entity.""" + + is_on: Callable[[StateType], bool] = bool + + +VEHICLE_DESCRIPTIONS: tuple[TeslaFleetBinarySensorEntityDescription, ...] = ( + TeslaFleetBinarySensorEntityDescription( + key="state", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + is_on=lambda x: x == TeslaFleetState.ONLINE, + ), + TeslaFleetBinarySensorEntityDescription( + key="charge_state_battery_heater_on", + device_class=BinarySensorDeviceClass.HEAT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslaFleetBinarySensorEntityDescription( + key="charge_state_charger_phases", + is_on=lambda x: cast(int, x) > 1, + entity_registry_enabled_default=False, + ), + TeslaFleetBinarySensorEntityDescription( + key="charge_state_preconditioning_enabled", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslaFleetBinarySensorEntityDescription( + key="climate_state_is_preconditioning", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslaFleetBinarySensorEntityDescription( + key="charge_state_scheduled_charging_pending", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslaFleetBinarySensorEntityDescription( + key="charge_state_trip_charging", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslaFleetBinarySensorEntityDescription( + key="charge_state_conn_charge_cable", + is_on=lambda x: x != "", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), + TeslaFleetBinarySensorEntityDescription( + key="climate_state_cabin_overheat_protection_actively_cooling", + device_class=BinarySensorDeviceClass.HEAT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslaFleetBinarySensorEntityDescription( + key="vehicle_state_dashcam_state", + device_class=BinarySensorDeviceClass.RUNNING, + is_on=lambda x: x == "Recording", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslaFleetBinarySensorEntityDescription( + key="vehicle_state_is_user_present", + device_class=BinarySensorDeviceClass.PRESENCE, + ), + TeslaFleetBinarySensorEntityDescription( + key="vehicle_state_tpms_soft_warning_fl", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslaFleetBinarySensorEntityDescription( + key="vehicle_state_tpms_soft_warning_fr", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslaFleetBinarySensorEntityDescription( + key="vehicle_state_tpms_soft_warning_rl", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslaFleetBinarySensorEntityDescription( + key="vehicle_state_tpms_soft_warning_rr", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslaFleetBinarySensorEntityDescription( + key="vehicle_state_fd_window", + device_class=BinarySensorDeviceClass.WINDOW, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TeslaFleetBinarySensorEntityDescription( + key="vehicle_state_fp_window", + device_class=BinarySensorDeviceClass.WINDOW, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TeslaFleetBinarySensorEntityDescription( + key="vehicle_state_rd_window", + device_class=BinarySensorDeviceClass.WINDOW, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TeslaFleetBinarySensorEntityDescription( + key="vehicle_state_rp_window", + device_class=BinarySensorDeviceClass.WINDOW, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TeslaFleetBinarySensorEntityDescription( + key="vehicle_state_df", + device_class=BinarySensorDeviceClass.DOOR, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TeslaFleetBinarySensorEntityDescription( + key="vehicle_state_dr", + device_class=BinarySensorDeviceClass.DOOR, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TeslaFleetBinarySensorEntityDescription( + key="vehicle_state_pf", + device_class=BinarySensorDeviceClass.DOOR, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TeslaFleetBinarySensorEntityDescription( + key="vehicle_state_pr", + device_class=BinarySensorDeviceClass.DOOR, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + +ENERGY_LIVE_DESCRIPTIONS: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription(key="backup_capable"), + BinarySensorEntityDescription(key="grid_services_active"), +) + + +ENERGY_INFO_DESCRIPTIONS: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="components_grid_services_enabled", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: TeslaFleetConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Tesla Fleet binary sensor platform from a config entry.""" + + async_add_entities( + chain( + ( # Vehicles + TeslaFleetVehicleBinarySensorEntity(vehicle, description) + for vehicle in entry.runtime_data.vehicles + for description in VEHICLE_DESCRIPTIONS + ), + ( # Energy Site Live + TeslaFleetEnergyLiveBinarySensorEntity(energysite, description) + for energysite in entry.runtime_data.energysites + for description in ENERGY_LIVE_DESCRIPTIONS + if energysite.info_coordinator.data.get("components_battery") + ), + ( # Energy Site Info + TeslaFleetEnergyInfoBinarySensorEntity(energysite, description) + for energysite in entry.runtime_data.energysites + for description in ENERGY_INFO_DESCRIPTIONS + if energysite.info_coordinator.data.get("components_battery") + ), + ) + ) + + +class TeslaFleetVehicleBinarySensorEntity(TeslaFleetVehicleEntity, BinarySensorEntity): + """Base class for Tesla Fleet vehicle binary sensors.""" + + entity_description: TeslaFleetBinarySensorEntityDescription + + def __init__( + self, + data: TeslaFleetVehicleData, + description: TeslaFleetBinarySensorEntityDescription, + ) -> None: + """Initialize the binary sensor.""" + self.entity_description = description + super().__init__(data, description.key) + + def _async_update_attrs(self) -> None: + """Update the attributes of the binary sensor.""" + + if self.coordinator.updated_once: + if self._value is None: + self._attr_available = False + self._attr_is_on = None + else: + self._attr_available = True + self._attr_is_on = self.entity_description.is_on(self._value) + else: + self._attr_is_on = None + + +class TeslaFleetEnergyLiveBinarySensorEntity( + TeslaFleetEnergyLiveEntity, BinarySensorEntity +): + """Base class for Tesla Fleet energy live binary sensors.""" + + entity_description: BinarySensorEntityDescription + + def __init__( + self, + data: TeslaFleetEnergyData, + description: BinarySensorEntityDescription, + ) -> None: + """Initialize the binary sensor.""" + self.entity_description = description + super().__init__(data, description.key) + + def _async_update_attrs(self) -> None: + """Update the attributes of the binary sensor.""" + self._attr_is_on = self._value + + +class TeslaFleetEnergyInfoBinarySensorEntity( + TeslaFleetEnergyInfoEntity, BinarySensorEntity +): + """Base class for Tesla Fleet energy info binary sensors.""" + + entity_description: BinarySensorEntityDescription + + def __init__( + self, + data: TeslaFleetEnergyData, + description: BinarySensorEntityDescription, + ) -> None: + """Initialize the binary sensor.""" + self.entity_description = description + super().__init__(data, description.key) + + def _async_update_attrs(self) -> None: + """Update the attributes of the binary sensor.""" + self._attr_is_on = self._value diff --git a/homeassistant/components/tesla_fleet/icons.json b/homeassistant/components/tesla_fleet/icons.json index 2180611ea94..5556219ed82 100644 --- a/homeassistant/components/tesla_fleet/icons.json +++ b/homeassistant/components/tesla_fleet/icons.json @@ -1,5 +1,43 @@ { "entity": { + "binary_sensor": { + "climate_state_is_preconditioning": { + "state": { + "off": "mdi:hvac-off", + "on": "mdi:hvac" + } + }, + "vehicle_state_is_user_present": { + "state": { + "off": "mdi:account-remove-outline", + "on": "mdi:account" + } + }, + "vehicle_state_tpms_soft_warning_fl": { + "state": { + "off": "mdi:tire", + "on": "mdi:car-tire-alert" + } + }, + "vehicle_state_tpms_soft_warning_fr": { + "state": { + "off": "mdi:tire", + "on": "mdi:car-tire-alert" + } + }, + "vehicle_state_tpms_soft_warning_rl": { + "state": { + "off": "mdi:tire", + "on": "mdi:car-tire-alert" + } + }, + "vehicle_state_tpms_soft_warning_rr": { + "state": { + "off": "mdi:tire", + "on": "mdi:car-tire-alert" + } + } + }, "sensor": { "battery_power": { "default": "mdi:home-battery" diff --git a/homeassistant/components/tesla_fleet/strings.json b/homeassistant/components/tesla_fleet/strings.json index 4a571ae0a2e..5b66879ac0d 100644 --- a/homeassistant/components/tesla_fleet/strings.json +++ b/homeassistant/components/tesla_fleet/strings.json @@ -27,6 +27,86 @@ } }, "entity": { + "binary_sensor": { + "backup_capable": { + "name": "Backup capable" + }, + "charge_state_battery_heater_on": { + "name": "Battery heater" + }, + "charge_state_charger_phases": { + "name": "Charger has multiple phases" + }, + "charge_state_conn_charge_cable": { + "name": "Charge cable" + }, + "charge_state_preconditioning_enabled": { + "name": "Preconditioning enabled" + }, + "charge_state_scheduled_charging_pending": { + "name": "Scheduled charging pending" + }, + "charge_state_trip_charging": { + "name": "Trip charging" + }, + "climate_state_cabin_overheat_protection_actively_cooling": { + "name": "Cabin overheat protection actively cooling" + }, + "climate_state_is_preconditioning": { + "name": "Preconditioning" + }, + "components_grid_services_enabled": { + "name": "Grid services enabled" + }, + "grid_services_active": { + "name": "Grid services active" + }, + "state": { + "name": "Status" + }, + "vehicle_state_dashcam_state": { + "name": "Dashcam" + }, + "vehicle_state_df": { + "name": "Front driver door" + }, + "vehicle_state_dr": { + "name": "Rear driver door" + }, + "vehicle_state_fd_window": { + "name": "Front driver window" + }, + "vehicle_state_fp_window": { + "name": "Front passenger window" + }, + "vehicle_state_is_user_present": { + "name": "User present" + }, + "vehicle_state_pf": { + "name": "Front passenger door" + }, + "vehicle_state_pr": { + "name": "Rear passenger door" + }, + "vehicle_state_rd_window": { + "name": "Rear driver window" + }, + "vehicle_state_rp_window": { + "name": "Rear passenger window" + }, + "vehicle_state_tpms_soft_warning_fl": { + "name": "Tire pressure warning front left" + }, + "vehicle_state_tpms_soft_warning_fr": { + "name": "Tire pressure warning front right" + }, + "vehicle_state_tpms_soft_warning_rl": { + "name": "Tire pressure warning rear left" + }, + "vehicle_state_tpms_soft_warning_rr": { + "name": "Tire pressure warning rear right" + } + }, "sensor": { "battery_power": { "name": "Battery power" diff --git a/tests/components/tesla_fleet/snapshots/test_binary_sensors.ambr b/tests/components/tesla_fleet/snapshots/test_binary_sensors.ambr new file mode 100644 index 00000000000..05ef4879de6 --- /dev/null +++ b/tests/components/tesla_fleet/snapshots/test_binary_sensors.ambr @@ -0,0 +1,1571 @@ +# serializer version: 1 +# name: test_binary_sensor[binary_sensor.energy_site_backup_capable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.energy_site_backup_capable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Backup capable', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'backup_capable', + 'unique_id': '123456-backup_capable', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.energy_site_backup_capable-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Energy Site Backup capable', + }), + 'context': , + 'entity_id': 'binary_sensor.energy_site_backup_capable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[binary_sensor.energy_site_grid_services_active-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.energy_site_grid_services_active', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Grid services active', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_services_active', + 'unique_id': '123456-grid_services_active', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.energy_site_grid_services_active-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Energy Site Grid services active', + }), + 'context': , + 'entity_id': 'binary_sensor.energy_site_grid_services_active', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.energy_site_grid_services_enabled-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.energy_site_grid_services_enabled', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Grid services enabled', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'components_grid_services_enabled', + 'unique_id': '123456-components_grid_services_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.energy_site_grid_services_enabled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Energy Site Grid services enabled', + }), + 'context': , + 'entity_id': 'binary_sensor.energy_site_grid_services_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_battery_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_battery_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery heater', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_battery_heater_on', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_battery_heater_on', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_battery_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'heat', + 'friendly_name': 'Test Battery heater', + }), + 'context': , + 'entity_id': 'binary_sensor.test_battery_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_cabin_overheat_protection_actively_cooling-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_cabin_overheat_protection_actively_cooling', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cabin overheat protection actively cooling', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_state_cabin_overheat_protection_actively_cooling', + 'unique_id': 'LRWXF7EK4KC700000-climate_state_cabin_overheat_protection_actively_cooling', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_cabin_overheat_protection_actively_cooling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'heat', + 'friendly_name': 'Test Cabin overheat protection actively cooling', + }), + 'context': , + 'entity_id': 'binary_sensor.test_cabin_overheat_protection_actively_cooling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_charge_cable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_charge_cable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charge cable', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_conn_charge_cable', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_conn_charge_cable', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_charge_cable-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Test Charge cable', + }), + 'context': , + 'entity_id': 'binary_sensor.test_charge_cable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_charger_has_multiple_phases-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_charger_has_multiple_phases', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charger has multiple phases', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charger_phases', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_charger_phases', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_charger_has_multiple_phases-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charger has multiple phases', + }), + 'context': , + 'entity_id': 'binary_sensor.test_charger_has_multiple_phases', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_dashcam-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_dashcam', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Dashcam', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_dashcam_state', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_dashcam_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_dashcam-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Test Dashcam', + }), + 'context': , + 'entity_id': 'binary_sensor.test_dashcam', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_front_driver_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_front_driver_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Front driver door', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_df', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_df', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_front_driver_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Test Front driver door', + }), + 'context': , + 'entity_id': 'binary_sensor.test_front_driver_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_front_driver_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_front_driver_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Front driver window', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_fd_window', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_fd_window', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_front_driver_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Test Front driver window', + }), + 'context': , + 'entity_id': 'binary_sensor.test_front_driver_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_front_passenger_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_front_passenger_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Front passenger door', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_pf', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_pf', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_front_passenger_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Test Front passenger door', + }), + 'context': , + 'entity_id': 'binary_sensor.test_front_passenger_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_front_passenger_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_front_passenger_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Front passenger window', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_fp_window', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_fp_window', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_front_passenger_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Test Front passenger window', + }), + 'context': , + 'entity_id': 'binary_sensor.test_front_passenger_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_preconditioning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_preconditioning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Preconditioning', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_state_is_preconditioning', + 'unique_id': 'LRWXF7EK4KC700000-climate_state_is_preconditioning', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_preconditioning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Preconditioning', + }), + 'context': , + 'entity_id': 'binary_sensor.test_preconditioning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_preconditioning_enabled-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_preconditioning_enabled', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Preconditioning enabled', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_preconditioning_enabled', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_preconditioning_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_preconditioning_enabled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Preconditioning enabled', + }), + 'context': , + 'entity_id': 'binary_sensor.test_preconditioning_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_rear_driver_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_rear_driver_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Rear driver door', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_dr', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_dr', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_rear_driver_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Test Rear driver door', + }), + 'context': , + 'entity_id': 'binary_sensor.test_rear_driver_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_rear_driver_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_rear_driver_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Rear driver window', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_rd_window', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_rd_window', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_rear_driver_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Test Rear driver window', + }), + 'context': , + 'entity_id': 'binary_sensor.test_rear_driver_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_rear_passenger_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_rear_passenger_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Rear passenger door', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_pr', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_pr', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_rear_passenger_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Test Rear passenger door', + }), + 'context': , + 'entity_id': 'binary_sensor.test_rear_passenger_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_rear_passenger_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_rear_passenger_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Rear passenger window', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_rp_window', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_rp_window', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_rear_passenger_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Test Rear passenger window', + }), + 'context': , + 'entity_id': 'binary_sensor.test_rear_passenger_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_scheduled_charging_pending-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_scheduled_charging_pending', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Scheduled charging pending', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_scheduled_charging_pending', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_scheduled_charging_pending', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_scheduled_charging_pending-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Scheduled charging pending', + }), + 'context': , + 'entity_id': 'binary_sensor.test_scheduled_charging_pending', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Status', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'state', + 'unique_id': 'LRWXF7EK4KC700000-state', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Test Status', + }), + 'context': , + 'entity_id': 'binary_sensor.test_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_tire_pressure_warning_front_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_tire_pressure_warning_front_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tire pressure warning front left', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_tpms_soft_warning_fl', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_soft_warning_fl', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_tire_pressure_warning_front_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Test Tire pressure warning front left', + }), + 'context': , + 'entity_id': 'binary_sensor.test_tire_pressure_warning_front_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_tire_pressure_warning_front_right-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_tire_pressure_warning_front_right', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tire pressure warning front right', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_tpms_soft_warning_fr', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_soft_warning_fr', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_tire_pressure_warning_front_right-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Test Tire pressure warning front right', + }), + 'context': , + 'entity_id': 'binary_sensor.test_tire_pressure_warning_front_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_tire_pressure_warning_rear_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_tire_pressure_warning_rear_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tire pressure warning rear left', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_tpms_soft_warning_rl', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_soft_warning_rl', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_tire_pressure_warning_rear_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Test Tire pressure warning rear left', + }), + 'context': , + 'entity_id': 'binary_sensor.test_tire_pressure_warning_rear_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_tire_pressure_warning_rear_right-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_tire_pressure_warning_rear_right', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tire pressure warning rear right', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_tpms_soft_warning_rr', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_soft_warning_rr', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_tire_pressure_warning_rear_right-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Test Tire pressure warning rear right', + }), + 'context': , + 'entity_id': 'binary_sensor.test_tire_pressure_warning_rear_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_trip_charging-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_trip_charging', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Trip charging', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_trip_charging', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_trip_charging', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_trip_charging-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Trip charging', + }), + 'context': , + 'entity_id': 'binary_sensor.test_trip_charging', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_user_present-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_user_present', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'User present', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_is_user_present', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_is_user_present', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_user_present-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'presence', + 'friendly_name': 'Test User present', + }), + 'context': , + 'entity_id': 'binary_sensor.test_user_present', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.energy_site_backup_capable-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Energy Site Backup capable', + }), + 'context': , + 'entity_id': 'binary_sensor.energy_site_backup_capable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.energy_site_grid_services_active-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Energy Site Grid services active', + }), + 'context': , + 'entity_id': 'binary_sensor.energy_site_grid_services_active', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.energy_site_grid_services_enabled-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Energy Site Grid services enabled', + }), + 'context': , + 'entity_id': 'binary_sensor.energy_site_grid_services_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_battery_heater-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'heat', + 'friendly_name': 'Test Battery heater', + }), + 'context': , + 'entity_id': 'binary_sensor.test_battery_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_cabin_overheat_protection_actively_cooling-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'heat', + 'friendly_name': 'Test Cabin overheat protection actively cooling', + }), + 'context': , + 'entity_id': 'binary_sensor.test_cabin_overheat_protection_actively_cooling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_charge_cable-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Test Charge cable', + }), + 'context': , + 'entity_id': 'binary_sensor.test_charge_cable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_charger_has_multiple_phases-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charger has multiple phases', + }), + 'context': , + 'entity_id': 'binary_sensor.test_charger_has_multiple_phases', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_dashcam-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Test Dashcam', + }), + 'context': , + 'entity_id': 'binary_sensor.test_dashcam', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_front_driver_door-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Test Front driver door', + }), + 'context': , + 'entity_id': 'binary_sensor.test_front_driver_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_front_driver_window-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Test Front driver window', + }), + 'context': , + 'entity_id': 'binary_sensor.test_front_driver_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_front_passenger_door-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Test Front passenger door', + }), + 'context': , + 'entity_id': 'binary_sensor.test_front_passenger_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_front_passenger_window-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Test Front passenger window', + }), + 'context': , + 'entity_id': 'binary_sensor.test_front_passenger_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_preconditioning-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Preconditioning', + }), + 'context': , + 'entity_id': 'binary_sensor.test_preconditioning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_preconditioning_enabled-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Preconditioning enabled', + }), + 'context': , + 'entity_id': 'binary_sensor.test_preconditioning_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_rear_driver_door-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Test Rear driver door', + }), + 'context': , + 'entity_id': 'binary_sensor.test_rear_driver_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_rear_driver_window-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Test Rear driver window', + }), + 'context': , + 'entity_id': 'binary_sensor.test_rear_driver_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_rear_passenger_door-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Test Rear passenger door', + }), + 'context': , + 'entity_id': 'binary_sensor.test_rear_passenger_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_rear_passenger_window-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'window', + 'friendly_name': 'Test Rear passenger window', + }), + 'context': , + 'entity_id': 'binary_sensor.test_rear_passenger_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_scheduled_charging_pending-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Scheduled charging pending', + }), + 'context': , + 'entity_id': 'binary_sensor.test_scheduled_charging_pending', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_status-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Test Status', + }), + 'context': , + 'entity_id': 'binary_sensor.test_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_tire_pressure_warning_front_left-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Test Tire pressure warning front left', + }), + 'context': , + 'entity_id': 'binary_sensor.test_tire_pressure_warning_front_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_tire_pressure_warning_front_right-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Test Tire pressure warning front right', + }), + 'context': , + 'entity_id': 'binary_sensor.test_tire_pressure_warning_front_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_tire_pressure_warning_rear_left-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Test Tire pressure warning rear left', + }), + 'context': , + 'entity_id': 'binary_sensor.test_tire_pressure_warning_rear_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_tire_pressure_warning_rear_right-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Test Tire pressure warning rear right', + }), + 'context': , + 'entity_id': 'binary_sensor.test_tire_pressure_warning_rear_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_trip_charging-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Trip charging', + }), + 'context': , + 'entity_id': 'binary_sensor.test_trip_charging', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_user_present-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'presence', + 'friendly_name': 'Test User present', + }), + 'context': , + 'entity_id': 'binary_sensor.test_user_present', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/tesla_fleet/test_binary_sensors.py b/tests/components/tesla_fleet/test_binary_sensors.py new file mode 100644 index 00000000000..ffbaac5e6d8 --- /dev/null +++ b/tests/components/tesla_fleet/test_binary_sensors.py @@ -0,0 +1,64 @@ +"""Test the Tesla Fleet binary sensor platform.""" + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy import SnapshotAssertion +from tesla_fleet_api.exceptions import VehicleOffline + +from homeassistant.components.tesla_fleet.coordinator import VEHICLE_INTERVAL +from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import assert_entities, assert_entities_alt, setup_platform +from .const import VEHICLE_DATA_ALT + +from tests.common import MockConfigEntry, async_fire_time_changed + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_binary_sensor( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + normal_config_entry: MockConfigEntry, +) -> None: + """Tests that the binary sensor entities are correct.""" + + await setup_platform(hass, normal_config_entry, [Platform.BINARY_SENSOR]) + assert_entities(hass, normal_config_entry.entry_id, entity_registry, snapshot) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_binary_sensor_refresh( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_vehicle_data, + freezer: FrozenDateTimeFactory, + normal_config_entry: MockConfigEntry, +) -> None: + """Tests that the binary sensor entities are correct.""" + + await setup_platform(hass, normal_config_entry, [Platform.BINARY_SENSOR]) + + # Refresh + mock_vehicle_data.return_value = VEHICLE_DATA_ALT + freezer.tick(VEHICLE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert_entities_alt(hass, normal_config_entry.entry_id, entity_registry, snapshot) + + +async def test_binary_sensor_offline( + hass: HomeAssistant, + mock_vehicle_data, + normal_config_entry: MockConfigEntry, +) -> None: + """Tests that the binary sensor entities are correct when offline.""" + + mock_vehicle_data.side_effect = VehicleOffline + await setup_platform(hass, normal_config_entry, [Platform.BINARY_SENSOR]) + state = hass.states.get("binary_sensor.test_status") + assert state.state == STATE_UNKNOWN From 436a38c1d2a74ab7da8e4148eed9c5911271a492 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 20 Jul 2024 12:29:08 +0200 Subject: [PATCH 1374/2411] Revert "Fix recorder setup hanging if non live schema migration fails" (#122232) --- homeassistant/components/recorder/core.py | 1 - tests/components/recorder/test_migrate.py | 16 ++------------ tests/conftest.py | 27 +++++------------------ 3 files changed, 7 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 3f284bdd83d..2b8f45703b5 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -773,7 +773,6 @@ class Recorder(threading.Thread): "Database Migration Failed", "recorder_database_migration", ) - self.hass.add_job(self._async_startup_failed) return if not database_was_ready: diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index f32f5c4aaaf..682c0a55767 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -200,14 +200,8 @@ async def test_database_migration_encounters_corruption( assert move_away.called -@pytest.mark.parametrize( - ("live_migration", "expected_setup_result"), [(True, True), (False, False)] -) async def test_database_migration_encounters_corruption_not_sqlite( - hass: HomeAssistant, - async_setup_recorder_instance: RecorderInstanceGenerator, - live_migration: bool, - expected_setup_result: bool, + hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator ) -> None: """Test we fail on database error when we cannot recover.""" assert recorder.util.async_migration_in_progress(hass) is False @@ -232,14 +226,8 @@ async def test_database_migration_encounters_corruption_not_sqlite( "homeassistant.components.persistent_notification.dismiss", side_effect=pn.dismiss, ) as mock_dismiss, - patch( - "homeassistant.components.recorder.core.migration.live_migration", - return_value=live_migration, - ), ): - await async_setup_recorder_instance( - hass, wait_recorder=False, expected_setup_result=expected_setup_result - ) + await async_setup_recorder_instance(hass, wait_recorder=False) hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() diff --git a/tests/conftest.py b/tests/conftest.py index f21dfbec5e7..2c8c351f165 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1394,8 +1394,6 @@ async def _async_init_recorder_component( hass: HomeAssistant, add_config: dict[str, Any] | None = None, db_url: str | None = None, - *, - expected_setup_result: bool, ) -> None: """Initialize the recorder asynchronously.""" # pylint: disable-next=import-outside-toplevel @@ -1410,13 +1408,10 @@ async def _async_init_recorder_component( with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True): if recorder.DOMAIN not in hass.data: recorder_helper.async_initialize_recorder(hass) - setup_task = asyncio.ensure_future( - async_setup_component(hass, recorder.DOMAIN, {recorder.DOMAIN: config}) + assert await async_setup_component( + hass, recorder.DOMAIN, {recorder.DOMAIN: config} ) - # Wait for recorder integration to setup - setup_result = await setup_task - assert setup_result == expected_setup_result - assert (recorder.DOMAIN in hass.config.components) == expected_setup_result + assert recorder.DOMAIN in hass.config.components _LOGGER.info( "Test recorder successfully started, database location: %s", config[recorder.CONF_DB_URL], @@ -1532,16 +1527,10 @@ async def async_test_recorder( hass: HomeAssistant, config: ConfigType | None = None, *, - expected_setup_result: bool = True, wait_recorder: bool = True, ) -> AsyncGenerator[recorder.Recorder]: """Setup and return recorder instance.""" # noqa: D401 - await _async_init_recorder_component( - hass, - config, - recorder_db_url, - expected_setup_result=expected_setup_result, - ) + await _async_init_recorder_component(hass, config, recorder_db_url) await hass.async_block_till_done() instance = hass.data[recorder.DATA_INSTANCE] # The recorder's worker is not started until Home Assistant is running @@ -1568,18 +1557,12 @@ async def async_setup_recorder_instance( hass: HomeAssistant, config: ConfigType | None = None, *, - expected_setup_result: bool = True, wait_recorder: bool = True, ) -> AsyncGenerator[recorder.Recorder]: """Set up and return recorder instance.""" return await stack.enter_async_context( - async_test_recorder( - hass, - config, - expected_setup_result=expected_setup_result, - wait_recorder=wait_recorder, - ) + async_test_recorder(hass, config, wait_recorder=wait_recorder) ) yield async_setup_recorder From 13da20ddf4982d3e40b42e695f08a8bbf8d1d84b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 12:58:49 +0200 Subject: [PATCH 1375/2411] Update Pillow to 10.4.0 (#122237) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/image_upload/manifest.json | 2 +- homeassistant/components/matrix/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 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 6a198ab34e7..fabb2c30190 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/doods", "iot_class": "local_polling", "loggers": ["pydoods"], - "requirements": ["pydoods==1.0.2", "Pillow==10.3.0"] + "requirements": ["pydoods==1.0.2", "Pillow==10.4.0"] } diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 34f8025737f..b19d6d6293e 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/generic", "integration_type": "device", "iot_class": "local_push", - "requirements": ["ha-av==10.1.1", "Pillow==10.3.0"] + "requirements": ["ha-av==10.1.1", "Pillow==10.4.0"] } diff --git a/homeassistant/components/image_upload/manifest.json b/homeassistant/components/image_upload/manifest.json index 7cbc484b830..963721a0476 100644 --- a/homeassistant/components/image_upload/manifest.json +++ b/homeassistant/components/image_upload/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/image_upload", "integration_type": "system", "quality_scale": "internal", - "requirements": ["Pillow==10.3.0"] + "requirements": ["Pillow==10.4.0"] } diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index 2ea310aa5a6..7e854a85434 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/matrix", "iot_class": "cloud_push", "loggers": ["matrix_client"], - "requirements": ["matrix-nio==0.24.0", "Pillow==10.3.0"] + "requirements": ["matrix-nio==0.24.0", "Pillow==10.4.0"] } diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 42770d71792..1e70c4d3e10 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -3,5 +3,5 @@ "name": "Camera Proxy", "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["Pillow==10.3.0"] + "requirements": ["Pillow==10.4.0"] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 476f4e8c3c9..14f2d093f37 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/qrcode", "iot_class": "calculated", "loggers": ["pyzbar"], - "requirements": ["Pillow==10.3.0", "pyzbar==0.1.7"] + "requirements": ["Pillow==10.4.0", "pyzbar==0.1.7"] } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 5e05f496d1d..2f39644d6d3 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@fabaff"], "documentation": "https://www.home-assistant.io/integrations/seven_segments", "iot_class": "local_polling", - "requirements": ["Pillow==10.3.0"] + "requirements": ["Pillow==10.4.0"] } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index b97ccc5f9cf..875c98acb6d 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/sighthound", "iot_class": "cloud_polling", "loggers": ["simplehound"], - "requirements": ["Pillow==10.3.0", "simplehound==0.3"] + "requirements": ["Pillow==10.4.0", "simplehound==0.3"] } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 40dbadca64d..941ec130db2 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -10,6 +10,6 @@ "tf-models-official==2.5.0", "pycocotools==2.0.6", "numpy==1.26.0", - "Pillow==10.3.0" + "Pillow==10.4.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 16d72eb03d3..3b977869294 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -42,7 +42,7 @@ mutagen==1.47.0 orjson==3.10.6 packaging>=23.1 paho-mqtt==1.6.1 -Pillow==10.3.0 +Pillow==10.4.0 pip>=21.3.1 psutil-home-assistant==0.0.1 PyJWT==2.8.0 diff --git a/pyproject.toml b/pyproject.toml index 6d3bdcab7e6..0ec5f782bc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ dependencies = [ "PyJWT==2.8.0", # PyJWT has loose dependency. We want the latest one. "cryptography==42.0.8", - "Pillow==10.3.0", + "Pillow==10.4.0", "pyOpenSSL==24.1.0", "orjson==3.10.6", "packaging>=23.1", diff --git a/requirements.txt b/requirements.txt index d2bae8096cd..6f9bdbb3f10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ Jinja2==3.1.4 lru-dict==1.3.0 PyJWT==2.8.0 cryptography==42.0.8 -Pillow==10.3.0 +Pillow==10.4.0 pyOpenSSL==24.1.0 orjson==3.10.6 packaging>=23.1 diff --git a/requirements_all.txt b/requirements_all.txt index 59d5fac3361..7138997879f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -33,7 +33,7 @@ Mastodon.py==1.8.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -Pillow==10.3.0 +Pillow==10.4.0 # homeassistant.components.plex PlexAPI==4.15.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f390190f5e..43b43b1ec25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -30,7 +30,7 @@ HATasmota==0.9.2 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -Pillow==10.3.0 +Pillow==10.4.0 # homeassistant.components.plex PlexAPI==4.15.14 From ab2f38216d26ed91d1e0b94b424b41aa22ca36cd Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 12:59:08 +0200 Subject: [PATCH 1376/2411] Update coverage to 7.6.0 (#122238) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index c719b4eca64..6baedbca59f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt astroid==3.2.3 -coverage==7.5.3 +coverage==7.6.0 freezegun==1.5.1 mock-open==1.4.0 mypy-dev==1.12.0a1 From b54b08479d119a8bee1aeb82ab3b841faadf05fd Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 12:59:44 +0200 Subject: [PATCH 1377/2411] Update pipdeptree to 2.23.1 (#122239) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 6baedbca59f..5771677baf9 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -16,7 +16,7 @@ pre-commit==3.7.1 pydantic==1.10.17 pylint==3.2.5 pylint-per-file-ignores==1.3.2 -pipdeptree==2.19.0 +pipdeptree==2.23.1 pip-licenses==4.4.0 pytest-asyncio==0.23.6 pytest-aiohttp==1.0.5 From 0fe7aa1a43d712604ea0885ac47ff89b0839706f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 13:06:22 +0200 Subject: [PATCH 1378/2411] Update bcrypt to 4.1.3 (#122236) --- 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 3b977869294..e5e1a85c02f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ async-upnp-client==0.39.0 atomicwrites-homeassistant==1.4.1 attrs==23.2.0 awesomeversion==24.6.0 -bcrypt==4.1.2 +bcrypt==4.1.3 bleak-retry-connector==3.5.0 bleak==0.22.2 bluetooth-adapters==0.19.3 diff --git a/pyproject.toml b/pyproject.toml index 0ec5f782bc2..d0bbc36a5ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "attrs==23.2.0", "atomicwrites-homeassistant==1.4.1", "awesomeversion==24.6.0", - "bcrypt==4.1.2", + "bcrypt==4.1.3", "certifi>=2021.5.30", "ciso8601==2.3.1", "fnv-hash-fast==0.5.0", diff --git a/requirements.txt b/requirements.txt index 6f9bdbb3f10..3eba90158a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ async-interrupt==1.1.2 attrs==23.2.0 atomicwrites-homeassistant==1.4.1 awesomeversion==24.6.0 -bcrypt==4.1.2 +bcrypt==4.1.3 certifi>=2021.5.30 ciso8601==2.3.1 fnv-hash-fast==0.5.0 From 293ad99daee351edef6b4b6b39863321559a0c5d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 13:10:09 +0200 Subject: [PATCH 1379/2411] Update pytest-asyncio to 0.23.8 (#122241) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 5771677baf9..b15f24c22eb 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -18,7 +18,7 @@ pylint==3.2.5 pylint-per-file-ignores==1.3.2 pipdeptree==2.23.1 pip-licenses==4.4.0 -pytest-asyncio==0.23.6 +pytest-asyncio==0.23.8 pytest-aiohttp==1.0.5 pytest-cov==5.0.0 pytest-freezer==0.4.8 From 2f47312eeb4d41d5e873e08e0f2dc103845fe83e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 20 Jul 2024 13:10:23 +0200 Subject: [PATCH 1380/2411] Fix recorder setup hanging if non live schema migration fails (#122242) --- homeassistant/components/recorder/core.py | 32 +++++++++++------------ tests/components/recorder/test_migrate.py | 16 ++++++++++-- tests/conftest.py | 27 +++++++++++++++---- 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 2b8f45703b5..52f2c38f8bb 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -488,7 +488,7 @@ class Recorder(threading.Thread): async_at_started(self.hass, self._async_hass_started) @callback - def _async_startup_failed(self) -> None: + def _async_startup_done(self, startup_failed: bool) -> None: """Report startup failure.""" # If a live migration failed, we were able to connect (async_db_connected # marked True), the database was marked ready (async_db_ready marked @@ -499,11 +499,12 @@ class Recorder(threading.Thread): self.async_db_connected.set_result(False) if not self.async_db_ready.done(): self.async_db_ready.set_result(False) - persistent_notification.async_create( - self.hass, - "The recorder could not start, check [the logs](/config/logs)", - "Recorder", - ) + if startup_failed: + persistent_notification.async_create( + self.hass, + "The recorder could not start, check [the logs](/config/logs)", + "Recorder", + ) self._async_stop_listeners() @callback @@ -1484,16 +1485,15 @@ class Recorder(threading.Thread): def _shutdown(self) -> None: """Save end time for current run.""" _LOGGER.debug("Shutting down recorder") - if not self.schema_version or self.schema_version != SCHEMA_VERSION: - # If the schema version is not set, we never had a working - # connection to the database or the schema never reached a - # good state. - # - # In either case, we want to mark startup as failed. - # - self.hass.add_job(self._async_startup_failed) - else: - self.hass.add_job(self._async_stop_listeners) + + # If the schema version is not set, we never had a working + # connection to the database or the schema never reached a + # good state. + # In either case, we want to mark startup as failed. + startup_failed = ( + not self.schema_version or self.schema_version != SCHEMA_VERSION + ) + self.hass.add_job(self._async_startup_done, startup_failed) try: self._end_session() diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 682c0a55767..f32f5c4aaaf 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -200,8 +200,14 @@ async def test_database_migration_encounters_corruption( assert move_away.called +@pytest.mark.parametrize( + ("live_migration", "expected_setup_result"), [(True, True), (False, False)] +) async def test_database_migration_encounters_corruption_not_sqlite( - hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator + hass: HomeAssistant, + async_setup_recorder_instance: RecorderInstanceGenerator, + live_migration: bool, + expected_setup_result: bool, ) -> None: """Test we fail on database error when we cannot recover.""" assert recorder.util.async_migration_in_progress(hass) is False @@ -226,8 +232,14 @@ async def test_database_migration_encounters_corruption_not_sqlite( "homeassistant.components.persistent_notification.dismiss", side_effect=pn.dismiss, ) as mock_dismiss, + patch( + "homeassistant.components.recorder.core.migration.live_migration", + return_value=live_migration, + ), ): - await async_setup_recorder_instance(hass, wait_recorder=False) + await async_setup_recorder_instance( + hass, wait_recorder=False, expected_setup_result=expected_setup_result + ) hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() diff --git a/tests/conftest.py b/tests/conftest.py index 2c8c351f165..f21dfbec5e7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1394,6 +1394,8 @@ async def _async_init_recorder_component( hass: HomeAssistant, add_config: dict[str, Any] | None = None, db_url: str | None = None, + *, + expected_setup_result: bool, ) -> None: """Initialize the recorder asynchronously.""" # pylint: disable-next=import-outside-toplevel @@ -1408,10 +1410,13 @@ async def _async_init_recorder_component( with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True): if recorder.DOMAIN not in hass.data: recorder_helper.async_initialize_recorder(hass) - assert await async_setup_component( - hass, recorder.DOMAIN, {recorder.DOMAIN: config} + setup_task = asyncio.ensure_future( + async_setup_component(hass, recorder.DOMAIN, {recorder.DOMAIN: config}) ) - assert recorder.DOMAIN in hass.config.components + # Wait for recorder integration to setup + setup_result = await setup_task + assert setup_result == expected_setup_result + assert (recorder.DOMAIN in hass.config.components) == expected_setup_result _LOGGER.info( "Test recorder successfully started, database location: %s", config[recorder.CONF_DB_URL], @@ -1527,10 +1532,16 @@ async def async_test_recorder( hass: HomeAssistant, config: ConfigType | None = None, *, + expected_setup_result: bool = True, wait_recorder: bool = True, ) -> AsyncGenerator[recorder.Recorder]: """Setup and return recorder instance.""" # noqa: D401 - await _async_init_recorder_component(hass, config, recorder_db_url) + await _async_init_recorder_component( + hass, + config, + recorder_db_url, + expected_setup_result=expected_setup_result, + ) await hass.async_block_till_done() instance = hass.data[recorder.DATA_INSTANCE] # The recorder's worker is not started until Home Assistant is running @@ -1557,12 +1568,18 @@ async def async_setup_recorder_instance( hass: HomeAssistant, config: ConfigType | None = None, *, + expected_setup_result: bool = True, wait_recorder: bool = True, ) -> AsyncGenerator[recorder.Recorder]: """Set up and return recorder instance.""" return await stack.enter_async_context( - async_test_recorder(hass, config, wait_recorder=wait_recorder) + async_test_recorder( + hass, + config, + expected_setup_result=expected_setup_result, + wait_recorder=wait_recorder, + ) ) yield async_setup_recorder From ee49c57e955e9357d83837d70eace50fdb23dcd9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 13:16:36 +0200 Subject: [PATCH 1381/2411] Update pytest to 8.2.2 (#122244) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index b15f24c22eb..ce4385e6e76 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -29,7 +29,7 @@ pytest-timeout==2.3.1 pytest-unordered==0.6.0 pytest-picked==0.5.0 pytest-xdist==3.6.1 -pytest==8.2.0 +pytest==8.2.2 requests-mock==1.12.1 respx==0.21.1 syrupy==4.6.1 From c6713edc8b198c6ce0c202fc676e71262e97dcd3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 13:24:01 +0200 Subject: [PATCH 1382/2411] Update pytest-unordered to 0.6.1 (#122243) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index ce4385e6e76..4fe078e77a9 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -26,7 +26,7 @@ pytest-github-actions-annotate-failures==0.2.0 pytest-socket==0.7.0 pytest-sugar==1.0.0 pytest-timeout==2.3.1 -pytest-unordered==0.6.0 +pytest-unordered==0.6.1 pytest-picked==0.5.0 pytest-xdist==3.6.1 pytest==8.2.2 From 651fb950100b0d7dc95746f0f5cfe6ab713da736 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 13:24:21 +0200 Subject: [PATCH 1383/2411] Update uv to 0.2.27 (#122246) --- Dockerfile | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 925f6370624..7ead7bc7e4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ ENV \ ARG QEMU_CPU # Install uv -RUN pip3 install uv==0.2.13 +RUN pip3 install uv==0.2.27 WORKDIR /usr/src diff --git a/requirements_test.txt b/requirements_test.txt index 4fe078e77a9..62095a0a628 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -51,4 +51,4 @@ types-pytz==2024.1.0.20240417 types-PyYAML==6.0.12.20240311 types-requests==2.31.0.3 types-xmltodict==0.13.0.3 -uv==0.2.13 +uv==0.2.27 From 55abbc51a4b52a18ba4059660abc39ab46398a7b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 13:52:55 +0200 Subject: [PATCH 1384/2411] Update pip-licenses to 4.5.1 (#122240) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 62095a0a628..9d59642f19b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -17,7 +17,7 @@ pydantic==1.10.17 pylint==3.2.5 pylint-per-file-ignores==1.3.2 pipdeptree==2.23.1 -pip-licenses==4.4.0 +pip-licenses==4.5.1 pytest-asyncio==0.23.8 pytest-aiohttp==1.0.5 pytest-cov==5.0.0 From 5fd3b929f4d5b04f8841ce56126a12619751a1b6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 14:09:10 +0200 Subject: [PATCH 1385/2411] Update types packages (#122245) --- requirements_test.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9d59642f19b..2305750ae92 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -34,7 +34,7 @@ requests-mock==1.12.1 respx==0.21.1 syrupy==4.6.1 tqdm==4.66.4 -types-aiofiles==23.2.0.20240403 +types-aiofiles==23.2.0.20240623 types-atomicwrites==1.4.5.1 types-croniter==2.0.0.20240423 types-beautifulsoup4==4.12.0.20240511 @@ -42,9 +42,9 @@ types-caldav==1.3.0.20240331 types-chardet==0.1.5 types-decorator==5.1.8.20240310 types-paho-mqtt==1.6.0.20240321 -types-pillow==10.2.0.20240511 +types-pillow==10.2.0.20240520 types-protobuf==4.24.0.20240106 -types-psutil==5.9.5.20240511 +types-psutil==6.0.0.20240621 types-python-dateutil==2.9.0.20240316 types-python-slugify==8.0.2.20240310 types-pytz==2024.1.0.20240417 From 6be4ef8a1fd4cccaffeb26708f83136dc72c2688 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 14:09:37 +0200 Subject: [PATCH 1386/2411] Improve contextmanager typing (#122250) --- homeassistant/components/zha/radio_manager.py | 3 ++- tests/components/automation/test_blueprint.py | 6 +++++- tests/components/bluetooth/test_wrappers.py | 3 ++- .../homeassistant_sky_connect/test_config_flow.py | 4 ++-- tests/components/netatmo/common.py | 5 +++-- tests/components/renault/conftest.py | 4 ++-- tests/test_util/aiohttp.py | 3 ++- 7 files changed, 18 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 9278b5da75f..2b7a65f4997 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections.abc import AsyncIterator import contextlib from contextlib import suppress import copy @@ -157,7 +158,7 @@ class ZhaRadioManager: return mgr @contextlib.asynccontextmanager - async def connect_zigpy_app(self) -> ControllerApplication: + async def connect_zigpy_app(self) -> AsyncIterator[ControllerApplication]: """Connect to the radio with the current config and then clean up.""" assert self.radio_type is not None diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index ee3fa631d00..2c92d7a5242 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -1,8 +1,10 @@ """Test built-in blueprints.""" import asyncio +from collections.abc import Iterator import contextlib from datetime import timedelta +from os import PathLike import pathlib from typing import Any from unittest.mock import patch @@ -23,7 +25,9 @@ BUILTIN_BLUEPRINT_FOLDER = pathlib.Path(automation.__file__).parent / "blueprint @contextlib.contextmanager -def patch_blueprint(blueprint_path: str, data_path): +def patch_blueprint( + blueprint_path: str, data_path: str | PathLike[str] +) -> Iterator[None]: """Patch blueprint loading from a different source.""" orig_load = models.DomainBlueprints._load_blueprint diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index 0c5645b3f71..5fc3d70c97a 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Iterator from contextlib import contextmanager from unittest.mock import patch @@ -27,7 +28,7 @@ from . import _get_manager, generate_advertisement_data, generate_ble_device @contextmanager -def mock_shutdown(manager: HomeAssistantBluetoothManager) -> None: +def mock_shutdown(manager: HomeAssistantBluetoothManager) -> Iterator[None]: """Mock shutdown of the HomeAssistantBluetoothManager.""" manager.shutdown = True yield diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index a4b7b4fb81d..48b774d5aeb 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Home Assistant SkyConnect config flow.""" import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Iterator import contextlib from typing import Any from unittest.mock import AsyncMock, Mock, call, patch @@ -80,7 +80,7 @@ def mock_addon_info( update_available=False, version=None, ), -): +) -> Iterator[tuple[Mock, Mock]]: """Mock the main addon states for the config flow.""" mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) mock_flasher_manager.addon_name = "Silicon Labs Flasher" diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py index 08c8679acf3..d9fe5e5b277 100644 --- a/tests/components/netatmo/common.py +++ b/tests/components/netatmo/common.py @@ -1,9 +1,10 @@ """Common methods used across tests for Netatmo.""" +from collections.abc import Iterator from contextlib import contextmanager import json from typing import Any -from unittest.mock import AsyncMock, patch +from unittest.mock import patch from syrupy import SnapshotAssertion @@ -109,7 +110,7 @@ async def simulate_webhook(hass: HomeAssistant, webhook_id: str, response) -> No @contextmanager -def selected_platforms(platforms: list[Platform]) -> AsyncMock: +def selected_platforms(platforms: list[Platform]) -> Iterator[None]: """Restrict loaded platforms to list given.""" with ( patch("homeassistant.components.netatmo.data_handler.PLATFORMS", platforms), diff --git a/tests/components/renault/conftest.py b/tests/components/renault/conftest.py index 00e35e1fa76..9be41eb7ba0 100644 --- a/tests/components/renault/conftest.py +++ b/tests/components/renault/conftest.py @@ -1,6 +1,6 @@ """Provide common Renault fixtures.""" -from collections.abc import Generator +from collections.abc import Generator, Iterator import contextlib from types import MappingProxyType from typing import Any @@ -200,7 +200,7 @@ def patch_fixtures_with_no_data(): @contextlib.contextmanager -def _patch_fixtures_with_side_effect(side_effect: Any): +def _patch_fixtures_with_side_effect(side_effect: Any) -> Iterator[None]: """Mock fixtures.""" with ( patch( diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index b4b8cfa4b6d..d0bd7fbeb2f 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -1,6 +1,7 @@ """Aiohttp test utils.""" import asyncio +from collections.abc import Iterator from contextlib import contextmanager from http import HTTPStatus import re @@ -296,7 +297,7 @@ class AiohttpClientMockResponse: @contextmanager -def mock_aiohttp_client(): +def mock_aiohttp_client() -> Iterator[AiohttpClientMocker]: """Context manager to mock aiohttp client.""" mocker = AiohttpClientMocker() From 0f079454bb4b5e5a8b6f00c9f258617da63abd6b Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 20 Jul 2024 22:37:57 +1000 Subject: [PATCH 1387/2411] Add device tracker to Tesla Fleet (#122222) --- .../components/tesla_fleet/__init__.py | 2 +- .../components/tesla_fleet/device_tracker.py | 106 ++++++++++++++++++ .../components/tesla_fleet/icons.json | 8 ++ .../components/tesla_fleet/strings.json | 8 ++ .../snapshots/test_device_tracker.ambr | 101 +++++++++++++++++ .../tesla_fleet/test_device_tracker.py | 37 ++++++ 6 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tesla_fleet/device_tracker.py create mode 100644 tests/components/tesla_fleet/snapshots/test_device_tracker.ambr create mode 100644 tests/components/tesla_fleet/test_device_tracker.py diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 0613f42ee61..892859cefd1 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -34,7 +34,7 @@ from .coordinator import ( ) from .models import TeslaFleetData, TeslaFleetEnergyData, TeslaFleetVehicleData -PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR] type TeslaFleetConfigEntry = ConfigEntry[TeslaFleetData] diff --git a/homeassistant/components/tesla_fleet/device_tracker.py b/homeassistant/components/tesla_fleet/device_tracker.py new file mode 100644 index 00000000000..1d396286d7c --- /dev/null +++ b/homeassistant/components/tesla_fleet/device_tracker.py @@ -0,0 +1,106 @@ +"""Device Tracker platform for Tesla Fleet integration.""" + +from __future__ import annotations + +from homeassistant.components.device_tracker import SourceType +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity + +from .entity import TeslaFleetVehicleEntity +from .models import TeslaFleetVehicleData + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Tesla Fleet device tracker platform from a config entry.""" + + async_add_entities( + klass(vehicle) + for klass in ( + TeslaFleetDeviceTrackerLocationEntity, + TeslaFleetDeviceTrackerRouteEntity, + ) + for vehicle in entry.runtime_data.vehicles + ) + + +class TeslaFleetDeviceTrackerEntity( + TeslaFleetVehicleEntity, TrackerEntity, RestoreEntity +): + """Base class for Tesla Fleet device tracker entities.""" + + _attr_latitude: float | None = None + _attr_longitude: float | None = None + + def __init__( + self, + vehicle: TeslaFleetVehicleData, + ) -> None: + """Initialize the device tracker.""" + super().__init__(vehicle, self.key) + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + if ( + (state := await self.async_get_last_state()) is not None + and self._attr_latitude is None + and self._attr_longitude is None + ): + self._attr_latitude = state.attributes.get("latitude") + self._attr_longitude = state.attributes.get("longitude") + + @property + def latitude(self) -> float | None: + """Return latitude value of the device.""" + return self._attr_latitude + + @property + def longitude(self) -> float | None: + """Return longitude value of the device.""" + return self._attr_longitude + + @property + def source_type(self) -> SourceType | str: + """Return the source type of the device tracker.""" + return SourceType.GPS + + +class TeslaFleetDeviceTrackerLocationEntity(TeslaFleetDeviceTrackerEntity): + """Vehicle Location device tracker Class.""" + + key = "location" + + def _async_update_attrs(self) -> None: + """Update the attributes of the entity.""" + + self._attr_latitude = self.get("drive_state_latitude") + self._attr_longitude = self.get("drive_state_longitude") + self._attr_available = not ( + self.get("drive_state_longitude", False) is None + or self.get("drive_state_latitude", False) is None + ) + + +class TeslaFleetDeviceTrackerRouteEntity(TeslaFleetDeviceTrackerEntity): + """Vehicle Navigation device tracker Class.""" + + key = "route" + + def _async_update_attrs(self) -> None: + """Update the attributes of the device tracker.""" + self._attr_latitude = self.get("drive_state_active_route_latitude") + self._attr_longitude = self.get("drive_state_active_route_longitude") + self._attr_available = not ( + self.get("drive_state_active_route_longitude", False) is None + or self.get("drive_state_active_route_latitude", False) is None + ) + + @property + def location_name(self) -> str | None: + """Return a location name for the current location of the device.""" + return self.get("drive_state_active_route_destination") diff --git a/homeassistant/components/tesla_fleet/icons.json b/homeassistant/components/tesla_fleet/icons.json index 5556219ed82..2dbde45ee08 100644 --- a/homeassistant/components/tesla_fleet/icons.json +++ b/homeassistant/components/tesla_fleet/icons.json @@ -38,6 +38,14 @@ } } }, + "device_tracker": { + "location": { + "default": "mdi:map-marker" + }, + "route": { + "default": "mdi:routes" + } + }, "sensor": { "battery_power": { "default": "mdi:home-battery" diff --git a/homeassistant/components/tesla_fleet/strings.json b/homeassistant/components/tesla_fleet/strings.json index 5b66879ac0d..6e74714ddd5 100644 --- a/homeassistant/components/tesla_fleet/strings.json +++ b/homeassistant/components/tesla_fleet/strings.json @@ -107,6 +107,14 @@ "name": "Tire pressure warning rear right" } }, + "device_tracker": { + "location": { + "name": "Location" + }, + "route": { + "name": "Route" + } + }, "sensor": { "battery_power": { "name": "Battery power" diff --git a/tests/components/tesla_fleet/snapshots/test_device_tracker.ambr b/tests/components/tesla_fleet/snapshots/test_device_tracker.ambr new file mode 100644 index 00000000000..194eda6fcff --- /dev/null +++ b/tests/components/tesla_fleet/snapshots/test_device_tracker.ambr @@ -0,0 +1,101 @@ +# serializer version: 1 +# name: test_device_tracker[device_tracker.test_location-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'device_tracker', + 'entity_category': , + 'entity_id': 'device_tracker.test_location', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Location', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'location', + 'unique_id': 'LRWXF7EK4KC700000-location', + 'unit_of_measurement': None, + }) +# --- +# name: test_device_tracker[device_tracker.test_location-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Location', + 'gps_accuracy': 0, + 'latitude': -30.222626, + 'longitude': -97.6236871, + 'source_type': , + }), + 'context': , + 'entity_id': 'device_tracker.test_location', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'not_home', + }) +# --- +# name: test_device_tracker[device_tracker.test_route-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'device_tracker', + 'entity_category': , + 'entity_id': 'device_tracker.test_route', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Route', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'route', + 'unique_id': 'LRWXF7EK4KC700000-route', + 'unit_of_measurement': None, + }) +# --- +# name: test_device_tracker[device_tracker.test_route-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Route', + 'gps_accuracy': 0, + 'latitude': 30.2226265, + 'longitude': -97.6236871, + 'source_type': , + }), + 'context': , + 'entity_id': 'device_tracker.test_route', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'not_home', + }) +# --- diff --git a/tests/components/tesla_fleet/test_device_tracker.py b/tests/components/tesla_fleet/test_device_tracker.py new file mode 100644 index 00000000000..66a0c06de7f --- /dev/null +++ b/tests/components/tesla_fleet/test_device_tracker.py @@ -0,0 +1,37 @@ +"""Test the Tesla Fleet device tracker platform.""" + +from syrupy import SnapshotAssertion +from tesla_fleet_api.exceptions import VehicleOffline + +from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import assert_entities, setup_platform + +from tests.common import MockConfigEntry + + +async def test_device_tracker( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + normal_config_entry: MockConfigEntry, +) -> None: + """Tests that the device tracker entities are correct.""" + + await setup_platform(hass, normal_config_entry, [Platform.DEVICE_TRACKER]) + assert_entities(hass, normal_config_entry.entry_id, entity_registry, snapshot) + + +async def test_device_tracker_offline( + hass: HomeAssistant, + mock_vehicle_data, + normal_config_entry: MockConfigEntry, +) -> None: + """Tests that the device tracker entities are correct when offline.""" + + mock_vehicle_data.side_effect = VehicleOffline + await setup_platform(hass, normal_config_entry, [Platform.DEVICE_TRACKER]) + state = hass.states.get("device_tracker.test_location") + assert state.state == STATE_UNKNOWN From 63b0feeae7507a5300d5cee30d7a2a03c41df37e Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Sat, 20 Jul 2024 10:38:51 -0400 Subject: [PATCH 1388/2411] Add calendar for Rachio smart hose timer (#120030) --- homeassistant/components/rachio/__init__.py | 5 +- .../components/rachio/binary_sensor.py | 4 +- homeassistant/components/rachio/calendar.py | 177 ++++++++++++++++++ homeassistant/components/rachio/const.py | 11 ++ .../components/rachio/coordinator.py | 69 ++++++- homeassistant/components/rachio/device.py | 20 +- homeassistant/components/rachio/strings.json | 5 + homeassistant/components/rachio/switch.py | 4 +- 8 files changed, 283 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/rachio/calendar.py diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index f91a7b4fa75..a5922e0cb95 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -23,7 +23,7 @@ from .webhooks import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.CALENDAR, Platform.SWITCH] CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -96,7 +96,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) for base in person.base_stations: - await base.coordinator.async_config_entry_first_refresh() + await base.status_coordinator.async_config_entry_first_refresh() + await base.schedule_coordinator.async_config_entry_first_refresh() # Enable platform hass.data.setdefault(DOMAIN, {})[entry.entry_id] = person diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index 5a8b5856db7..189a08e998d 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -60,9 +60,9 @@ def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Ent entities.append(RachioControllerOnlineBinarySensor(controller)) entities.append(RachioRainSensor(controller)) entities.extend( - RachioHoseTimerBattery(valve, base_station.coordinator) + RachioHoseTimerBattery(valve, base_station.status_coordinator) for base_station in person.base_stations - for valve in base_station.coordinator.data.values() + for valve in base_station.status_coordinator.data.values() ) return entities diff --git a/homeassistant/components/rachio/calendar.py b/homeassistant/components/rachio/calendar.py new file mode 100644 index 00000000000..5c7e13c748a --- /dev/null +++ b/homeassistant/components/rachio/calendar.py @@ -0,0 +1,177 @@ +"""Rachio smart hose timer calendar.""" + +from datetime import datetime, timedelta +import logging +from typing import Any + +from homeassistant.components.calendar import ( + CalendarEntity, + CalendarEntityFeature, + CalendarEvent, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt as dt_util + +from .const import ( + DOMAIN as DOMAIN_RACHIO, + KEY_ADDRESS, + KEY_DURATION_SECONDS, + KEY_ID, + KEY_LOCALITY, + KEY_PROGRAM_ID, + KEY_PROGRAM_NAME, + KEY_RUN_SUMMARIES, + KEY_SERIAL_NUMBER, + KEY_SKIP, + KEY_SKIPPABLE, + KEY_START_TIME, + KEY_TOTAL_RUN_DURATION, + KEY_VALVE_NAME, +) +from .coordinator import RachioScheduleUpdateCoordinator +from .device import RachioPerson + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up entry for Rachio smart hose timer calendar.""" + person: RachioPerson = hass.data[DOMAIN_RACHIO][config_entry.entry_id] + async_add_entities( + RachioCalendarEntity(base_station.schedule_coordinator, base_station) + for base_station in person.base_stations + ) + + +class RachioCalendarEntity( + CoordinatorEntity[RachioScheduleUpdateCoordinator], CalendarEntity +): + """Rachio calendar entity.""" + + _attr_has_entity_name = True + _attr_translation_key = "calendar" + _attr_supported_features = CalendarEntityFeature.DELETE_EVENT + + def __init__( + self, coordinator: RachioScheduleUpdateCoordinator, base_station + ) -> None: + """Initialize a Rachio calendar entity.""" + super().__init__(coordinator) + self.base_station = base_station + self._event: CalendarEvent | None = None + self._location = coordinator.base_station[KEY_ADDRESS][KEY_LOCALITY] + self._attr_translation_placeholders = { + "base": coordinator.base_station[KEY_SERIAL_NUMBER] + } + self._attr_unique_id = f"{coordinator.base_station[KEY_ID]}-calendar" + self._previous_event: dict[str, Any] | None = None + + @property + def event(self) -> CalendarEvent | None: + """Return the next upcoming event.""" + if not (event := self._handle_upcoming_event()): + return None + start_time = dt_util.parse_datetime(event[KEY_START_TIME], raise_on_error=True) + valves = ", ".join( + [event[KEY_VALVE_NAME] for event in event[KEY_RUN_SUMMARIES]] + ) + return CalendarEvent( + summary=event[KEY_PROGRAM_NAME], + start=dt_util.as_local(start_time), + end=dt_util.as_local(start_time) + + timedelta(seconds=int(event[KEY_TOTAL_RUN_DURATION])), + description=valves, + location=self._location, + ) + + def _handle_upcoming_event(self) -> dict[str, Any] | None: + """Handle current or next event.""" + # Currently when an event starts, it disappears from the + # API until the event ends. So we store the upcoming event and use + # the stored version if it's within the event time window. + if self._previous_event: + start_time = dt_util.parse_datetime( + self._previous_event[KEY_START_TIME], raise_on_error=True + ) + end_time = start_time + timedelta( + seconds=int(self._previous_event[KEY_TOTAL_RUN_DURATION]) + ) + if start_time <= dt_util.now() <= end_time: + return self._previous_event + + schedule = iter(self.coordinator.data) + event = next(schedule, None) + if not event: # Schedule is empty + return None + while ( + not event[KEY_SKIPPABLE] or KEY_SKIP in event[KEY_RUN_SUMMARIES][0] + ): # Not being skippable indicates the event is in the past + event = next(schedule, None) + if not event: # Schedule only has past or skipped events + return None + self._previous_event = event # Store for future use + return event + + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[CalendarEvent]: + """Get all events in a specific time frame.""" + if not self.coordinator.data: + raise HomeAssistantError("No events scheduled") + schedule = self.coordinator.data + event_list: list[CalendarEvent] = [] + + for run in schedule: + event_start = dt_util.as_local( + dt_util.parse_datetime(run[KEY_START_TIME], raise_on_error=True) + ) + if event_start > end_date: + break + if run[KEY_SKIPPABLE]: # Future events + event_end = event_start + timedelta( + seconds=int(run[KEY_TOTAL_RUN_DURATION]) + ) + else: # Past events + event_end = event_start + timedelta( + seconds=int(run[KEY_RUN_SUMMARIES][0][KEY_DURATION_SECONDS]) + ) + + if ( + event_end > start_date + and event_start < end_date + and KEY_SKIP not in run[KEY_RUN_SUMMARIES][0] + ): + valves = ", ".join( + [event[KEY_VALVE_NAME] for event in run[KEY_RUN_SUMMARIES]] + ) + event = CalendarEvent( + summary=run[KEY_PROGRAM_NAME], + start=event_start, + end=event_end, + description=valves, + location=self._location, + uid=f"{run[KEY_PROGRAM_ID]}/{run[KEY_START_TIME]}", + ) + event_list.append(event) + return event_list + + async def async_delete_event( + self, + uid: str, + recurrence_id: str | None = None, + recurrence_range: str | None = None, + ) -> None: + """Skip an upcoming event on the calendar.""" + program, timestamp = uid.split("/") + await self.hass.async_add_executor_job( + self.base_station.create_skip, program, timestamp + ) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index 891e92f55a1..ad670fc3608 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -51,6 +51,7 @@ KEY_CUSTOM_SLOPE = "customSlope" # Smart Hose timer KEY_BASE_STATIONS = "baseStations" KEY_VALVES = "valves" +KEY_VALVE_NAME = "valveName" KEY_REPORTED_STATE = "reportedState" KEY_STATE = "state" KEY_CONNECTED = "connected" @@ -64,6 +65,16 @@ KEY_DEFAULT_RUNTIME = "defaultRuntimeSeconds" KEY_DURATION_SECONDS = "durationSeconds" KEY_FLOW_DETECTED = "flowDetected" KEY_START_TIME = "start" +KEY_DAY_VIEWS = "valveDayViews" +KEY_RUN_SUMMARIES = "valveRunSummaries" +KEY_PROGRAM_ID = "programId" +KEY_PROGRAM_NAME = "programName" +KEY_PROGRAM_RUN_SUMMARIES = "valveProgramRunSummaries" +KEY_TOTAL_RUN_DURATION = "totalRunDurationSeconds" +KEY_ADDRESS = "address" +KEY_LOCALITY = "locality" +KEY_SKIP = "skip" +KEY_SKIPPABLE = "skippable" STATUS_ONLINE = "ONLINE" diff --git a/homeassistant/components/rachio/coordinator.py b/homeassistant/components/rachio/coordinator.py index 4f8cc87daef..25c40bd6656 100644 --- a/homeassistant/components/rachio/coordinator.py +++ b/homeassistant/components/rachio/coordinator.py @@ -1,7 +1,8 @@ """Coordinator object for the Rachio integration.""" -from datetime import timedelta +from datetime import datetime, timedelta import logging +from operator import itemgetter from typing import Any from rachiopy import Rachio @@ -10,11 +11,23 @@ from requests.exceptions import Timeout from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util -from .const import DOMAIN, KEY_ID, KEY_VALVES +from .const import ( + DOMAIN, + KEY_DAY_VIEWS, + KEY_ID, + KEY_PROGRAM_RUN_SUMMARIES, + KEY_START_TIME, + KEY_VALVES, +) _LOGGER = logging.getLogger(__name__) +DAY = "day" +MONTH = "month" +YEAR = "year" + UPDATE_DELAY_TIME = 8 @@ -54,3 +67,55 @@ class RachioUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): except Timeout as err: raise UpdateFailed(f"Could not connect to the Rachio API: {err}") from err return {valve[KEY_ID]: valve for valve in data[1][KEY_VALVES]} + + +class RachioScheduleUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]): + """Coordinator for fetching hose timer schedules.""" + + def __init__( + self, + hass: HomeAssistant, + rachio: Rachio, + base_station, + ) -> None: + """Initialize a Rachio schedule coordinator.""" + self.hass = hass + self.rachio = rachio + self.base_station = base_station + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN} schedule update coordinator", + update_interval=timedelta(minutes=30), + ) + + async def _async_update_data(self) -> list[dict[str, Any]]: + """Retrieve data for the past week and the next 60 days.""" + _now: datetime = dt_util.now() + _time_start = _now - timedelta(days=7) + _time_end = _now + timedelta(days=60) + start: dict[str, int] = { + YEAR: _time_start.year, + MONTH: _time_start.month, + DAY: _time_start.day, + } + end: dict[str, int] = { + YEAR: _time_end.year, + MONTH: _time_end.month, + DAY: _time_end.day, + } + + try: + schedule = await self.hass.async_add_executor_job( + self.rachio.summary.get_valve_day_views, + self.base_station[KEY_ID], + start, + end, + ) + except Timeout as err: + raise UpdateFailed(f"Could not connect to the Rachio API: {err}") from err + events = [] + # Flatten and sort dates + for event in schedule[1][KEY_DAY_VIEWS]: + events.extend(event[KEY_PROGRAM_RUN_SUMMARIES]) + return sorted(events, key=itemgetter(KEY_START_TIME)) diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index 09f7eaf1b06..0bbb862753e 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -38,7 +38,7 @@ from .const import ( SERVICE_STOP_WATERING, WEBHOOK_CONST_ID, ) -from .coordinator import RachioUpdateCoordinator +from .coordinator import RachioScheduleUpdateCoordinator, RachioUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -187,7 +187,10 @@ class RachioPerson: base_count = len(base_stations) self._base_stations.extend( RachioBaseStation( - rachio, base, RachioUpdateCoordinator(hass, rachio, base, base_count) + rachio, + base, + RachioUpdateCoordinator(hass, rachio, base, base_count), + RachioScheduleUpdateCoordinator(hass, rachio, base), ) for base in base_stations ) @@ -348,12 +351,17 @@ class RachioBaseStation: """Represent a smart hose timer base station.""" def __init__( - self, rachio: Rachio, data: dict[str, Any], coordinator: RachioUpdateCoordinator + self, + rachio: Rachio, + data: dict[str, Any], + status_coordinator: RachioUpdateCoordinator, + schedule_coordinator: RachioScheduleUpdateCoordinator, ) -> None: """Initialize a smart hose timer base station.""" self.rachio = rachio self._id = data[KEY_ID] - self.coordinator = coordinator + self.status_coordinator = status_coordinator + self.schedule_coordinator = schedule_coordinator def start_watering(self, valve_id: str, duration: int) -> None: """Start watering on this valve.""" @@ -363,6 +371,10 @@ class RachioBaseStation: """Stop watering on this valve.""" self.rachio.valve.stop_watering(valve_id) + def create_skip(self, program_id: str, timestamp: str) -> None: + """Create a skip for a scheduled event.""" + self.rachio.program.create_skip_overrides(program_id, timestamp) + def is_invalid_auth_code(http_status_code: int) -> bool: """HTTP status codes that mean invalid auth.""" diff --git a/homeassistant/components/rachio/strings.json b/homeassistant/components/rachio/strings.json index 2e4de262d21..ad7a277d23a 100644 --- a/homeassistant/components/rachio/strings.json +++ b/homeassistant/components/rachio/strings.json @@ -33,6 +33,11 @@ "name": "Rain" } }, + "calendar": { + "calendar": { + "name": "Rachio Base Station {base}" + } + }, "switch": { "standby": { "name": "Standby" diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 8a35225b9b2..92e7c0ea2ba 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -195,9 +195,9 @@ def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Ent for schedule in schedules + flex_schedules ) entities.extend( - RachioValve(person, base_station, valve, base_station.coordinator) + RachioValve(person, base_station, valve, base_station.status_coordinator) for base_station in person.base_stations - for valve in base_station.coordinator.data.values() + for valve in base_station.status_coordinator.data.values() ) return entities From 43aeaf7a9b5bfef2fc2e7da58fb2900c09fe9612 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Jul 2024 09:43:10 -0500 Subject: [PATCH 1389/2411] Upgrade CI to use ubuntu 24.04 (#122254) --- .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 caad898028c..142839e77ff 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -86,7 +86,7 @@ jobs: tests_glob: ${{ steps.info.outputs.tests_glob }} tests: ${{ steps.info.outputs.tests }} skip_coverage: ${{ steps.info.outputs.skip_coverage }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Check out code from GitHub uses: actions/checkout@v4.1.7 @@ -218,7 +218,7 @@ jobs: pre-commit: name: Prepare pre-commit base - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: | github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' @@ -266,7 +266,7 @@ jobs: lint-ruff-format: name: Check ruff-format - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: - info - pre-commit @@ -306,7 +306,7 @@ jobs: lint-ruff: name: Check ruff - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: - info - pre-commit @@ -345,7 +345,7 @@ jobs: RUFF_OUTPUT_FORMAT: github lint-other: name: Check other linters - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: - info - pre-commit @@ -437,7 +437,7 @@ jobs: base: name: Prepare dependencies - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: info timeout-minutes: 60 strategy: @@ -514,7 +514,7 @@ jobs: hassfest: name: Check hassfest - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: | github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' @@ -552,7 +552,7 @@ jobs: gen-requirements-all: name: Check all requirements - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: | github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' @@ -584,7 +584,7 @@ jobs: audit-licenses: name: Audit licenses - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: - info - base @@ -624,7 +624,7 @@ jobs: pylint: name: Check pylint - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 timeout-minutes: 20 if: | github.event.inputs.mypy-only != 'true' @@ -669,7 +669,7 @@ jobs: pylint-tests: name: Check pylint on tests - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 timeout-minutes: 20 if: | (github.event.inputs.mypy-only != 'true' || github.event.inputs.pylint-only == 'true') @@ -714,7 +714,7 @@ jobs: mypy: name: Check mypy - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: | github.event.inputs.pylint-only != 'true' || github.event.inputs.mypy-only == 'true' @@ -775,7 +775,7 @@ jobs: mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }} prepare-pytest-full: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: | (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') && github.event.inputs.lint-only != 'true' @@ -825,7 +825,7 @@ jobs: overwrite: true pytest-full: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: | (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') && github.event.inputs.lint-only != 'true' @@ -936,7 +936,7 @@ jobs: ./script/check_dirty pytest-mariadb: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 services: mariadb: image: ${{ matrix.mariadb-group }} @@ -1189,7 +1189,7 @@ jobs: coverage-full: name: Upload test coverage to Codecov (full suite) if: needs.info.outputs.skip_coverage != 'true' - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: - info - pytest-full @@ -1213,7 +1213,7 @@ jobs: version: v0.6.0 pytest-partial: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: | (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') && github.event.inputs.lint-only != 'true' @@ -1328,7 +1328,7 @@ jobs: coverage-partial: name: Upload test coverage to Codecov (partial suite) if: needs.info.outputs.skip_coverage != 'true' - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: - info - pytest-partial From 5e8b0222468fba0829099b426cf6090cf465c3b0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 16:46:39 +0200 Subject: [PATCH 1390/2411] Improve shopping_list test typing (#122255) --- tests/components/shopping_list/test_todo.py | 35 ++++++++++----------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/tests/components/shopping_list/test_todo.py b/tests/components/shopping_list/test_todo.py index f10479adf6c..c54a6abfd6f 100644 --- a/tests/components/shopping_list/test_todo.py +++ b/tests/components/shopping_list/test_todo.py @@ -1,6 +1,6 @@ """Test shopping list todo platform.""" -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine from typing import Any import pytest @@ -20,11 +20,12 @@ from tests.typing import WebSocketGenerator TEST_ENTITY = "todo.shopping_list" +type WsGetItemsType = Callable[[], Coroutine[Any, Any, list[dict[str, str]]]] +type WsMoveItemType = Callable[[str, str | None], Coroutine[Any, Any, dict[str, Any]]] + @pytest.fixture -async def ws_get_items( - hass_ws_client: WebSocketGenerator, -) -> Callable[[], Awaitable[dict[str, str]]]: +async def ws_get_items(hass_ws_client: WebSocketGenerator) -> WsGetItemsType: """Fixture to fetch items from the todo websocket.""" async def get() -> list[dict[str, str]]: @@ -44,9 +45,7 @@ async def ws_get_items( @pytest.fixture -async def ws_move_item( - hass_ws_client: WebSocketGenerator, -) -> Callable[[str, str | None], Awaitable[None]]: +async def ws_move_item(hass_ws_client: WebSocketGenerator) -> WsMoveItemType: """Fixture to move an item in the todo list.""" async def move(uid: str, previous_uid: str | None) -> dict[str, Any]: @@ -69,7 +68,7 @@ async def test_get_items( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, sl_setup: None, - ws_get_items: Callable[[], Awaitable[dict[str, str]]], + ws_get_items: WsGetItemsType, ) -> None: """Test creating a shopping list item with the WS API and verifying with To-do API.""" client = await hass_ws_client(hass) @@ -100,7 +99,7 @@ async def test_get_items( async def test_add_item( hass: HomeAssistant, sl_setup: None, - ws_get_items: Callable[[], Awaitable[dict[str, str]]], + ws_get_items: WsGetItemsType, ) -> None: """Test adding shopping_list item and listing it.""" await hass.services.async_call( @@ -127,7 +126,7 @@ async def test_add_item( async def test_remove_item( hass: HomeAssistant, sl_setup: None, - ws_get_items: Callable[[], Awaitable[dict[str, str]]], + ws_get_items: WsGetItemsType, ) -> None: """Test removing a todo item.""" await hass.services.async_call( @@ -168,7 +167,7 @@ async def test_remove_item( async def test_bulk_remove( hass: HomeAssistant, sl_setup: None, - ws_get_items: Callable[[], Awaitable[dict[str, str]]], + ws_get_items: WsGetItemsType, ) -> None: """Test removing a todo item.""" @@ -212,7 +211,7 @@ async def test_bulk_remove( async def test_update_item( hass: HomeAssistant, sl_setup: None, - ws_get_items: Callable[[], Awaitable[dict[str, str]]], + ws_get_items: WsGetItemsType, ) -> None: """Test updating a todo item.""" @@ -265,7 +264,7 @@ async def test_update_item( async def test_partial_update_item( hass: HomeAssistant, sl_setup: None, - ws_get_items: Callable[[], Awaitable[dict[str, str]]], + ws_get_items: WsGetItemsType, ) -> None: """Test updating a todo item with partial information.""" @@ -341,7 +340,7 @@ async def test_partial_update_item( async def test_update_invalid_item( hass: HomeAssistant, sl_setup: None, - ws_get_items: Callable[[], Awaitable[dict[str, str]]], + ws_get_items: WsGetItemsType, ) -> None: """Test updating a todo item that does not exist.""" @@ -387,8 +386,8 @@ async def test_update_invalid_item( async def test_move_item( hass: HomeAssistant, sl_setup: None, - ws_get_items: Callable[[], Awaitable[dict[str, str]]], - ws_move_item: Callable[[str, str | None], Awaitable[dict[str, Any]]], + ws_get_items: WsGetItemsType, + ws_move_item: WsMoveItemType, src_idx: int, dst_idx: int | None, expected_items: list[str], @@ -429,8 +428,8 @@ async def test_move_item( async def test_move_invalid_item( hass: HomeAssistant, sl_setup: None, - ws_get_items: Callable[[], Awaitable[dict[str, str]]], - ws_move_item: Callable[[str, int | None], Awaitable[dict[str, Any]]], + ws_get_items: WsGetItemsType, + ws_move_item: WsMoveItemType, ) -> None: """Test moving an item that does not exist.""" From 90e7d820497ec20e6614afa14e45ba83cf41445b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 17:33:48 +0200 Subject: [PATCH 1391/2411] Use correct enum in UnitSystem tests (#122256) --- tests/util/test_unit_system.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index 033631563f4..15500777212 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -15,6 +15,7 @@ from homeassistant.const import ( WIND_SPEED, UnitOfLength, UnitOfMass, + UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, UnitOfTemperature, @@ -42,7 +43,7 @@ def test_invalid_units() -> None: with pytest.raises(ValueError): UnitSystem( SYSTEM_NAME, - accumulated_precipitation=UnitOfLength.MILLIMETERS, + accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS, conversions={}, length=UnitOfLength.METERS, mass=UnitOfMass.GRAMS, @@ -55,7 +56,7 @@ def test_invalid_units() -> None: with pytest.raises(ValueError): UnitSystem( SYSTEM_NAME, - accumulated_precipitation=UnitOfLength.MILLIMETERS, + accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS, conversions={}, length=INVALID_UNIT, mass=UnitOfMass.GRAMS, @@ -68,7 +69,7 @@ def test_invalid_units() -> None: with pytest.raises(ValueError): UnitSystem( SYSTEM_NAME, - accumulated_precipitation=UnitOfLength.MILLIMETERS, + accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS, conversions={}, length=UnitOfLength.METERS, mass=UnitOfMass.GRAMS, @@ -81,7 +82,7 @@ def test_invalid_units() -> None: with pytest.raises(ValueError): UnitSystem( SYSTEM_NAME, - accumulated_precipitation=UnitOfLength.MILLIMETERS, + accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS, conversions={}, length=UnitOfLength.METERS, mass=UnitOfMass.GRAMS, @@ -94,7 +95,7 @@ def test_invalid_units() -> None: with pytest.raises(ValueError): UnitSystem( SYSTEM_NAME, - accumulated_precipitation=UnitOfLength.MILLIMETERS, + accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS, conversions={}, length=UnitOfLength.METERS, mass=INVALID_UNIT, @@ -107,7 +108,7 @@ def test_invalid_units() -> None: with pytest.raises(ValueError): UnitSystem( SYSTEM_NAME, - accumulated_precipitation=UnitOfLength.MILLIMETERS, + accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS, conversions={}, length=UnitOfLength.METERS, mass=UnitOfMass.GRAMS, From 769d7214a31bbb6df9ae9d09832e843e18016dfc Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 20 Jul 2024 17:34:43 +0200 Subject: [PATCH 1392/2411] Improve tests.common typing (#122257) --- tests/common.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/common.py b/tests/common.py index 40745a1df9e..55f7cadfd4b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -8,6 +8,8 @@ from collections.abc import ( Callable, Coroutine, Generator, + Iterable, + Iterator, Mapping, Sequence, ) @@ -30,6 +32,7 @@ from unittest.mock import AsyncMock, Mock, patch from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401 import pytest from syrupy import SnapshotAssertion +from typing_extensions import TypeVar import voluptuous as vol from homeassistant import auth, bootstrap, config_entries, loader @@ -90,6 +93,7 @@ from homeassistant.helpers.json import JSONEncoder, _orjson_default_encoder, jso from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util +from homeassistant.util.event_type import EventType from homeassistant.util.json import ( JsonArrayType, JsonObjectType, @@ -107,6 +111,8 @@ from .testing_config.custom_components.test_constant_deprecation import ( import_deprecated_constant, ) +_DataT = TypeVar("_DataT", bound=Mapping[str, Any], default=dict[str, Any]) + _LOGGER = logging.getLogger(__name__) INSTANCES = [] CLIENT_ID = "https://example.com/app" @@ -1434,7 +1440,7 @@ async def get_system_health_info(hass: HomeAssistant, domain: str) -> dict[str, @contextmanager -def mock_config_flow(domain: str, config_flow: type[ConfigFlow]) -> None: +def mock_config_flow(domain: str, config_flow: type[ConfigFlow]) -> Iterator[None]: """Mock a config flow handler.""" original_handler = config_entries.HANDLERS.get(domain) config_entries.HANDLERS[domain] = config_flow @@ -1502,12 +1508,14 @@ def mock_platform( module_cache[platform_path] = module or Mock() -def async_capture_events(hass: HomeAssistant, event_name: str) -> list[Event]: +def async_capture_events( + hass: HomeAssistant, event_name: EventType[_DataT] | str +) -> list[Event[_DataT]]: """Create a helper that captures events.""" - events = [] + events: list[Event[_DataT]] = [] @callback - def capture_events(event: Event) -> None: + def capture_events(event: Event[_DataT]) -> None: events.append(event) hass.bus.async_listen(event_name, capture_events) @@ -1516,14 +1524,14 @@ def async_capture_events(hass: HomeAssistant, event_name: str) -> list[Event]: @callback -def async_mock_signal( - hass: HomeAssistant, signal: SignalType[Any] | str -) -> list[tuple[Any]]: +def async_mock_signal[*_Ts]( + hass: HomeAssistant, signal: SignalType[*_Ts] | str +) -> list[tuple[*_Ts]]: """Catch all dispatches to a signal.""" - calls = [] + calls: list[tuple[*_Ts]] = [] @callback - def mock_signal_handler(*args: Any) -> None: + def mock_signal_handler(*args: *_Ts) -> None: """Mock service call.""" calls.append(args) @@ -1723,7 +1731,7 @@ def extract_stack_to_frame(extract_stack: list[Mock]) -> FrameType: def setup_test_component_platform( hass: HomeAssistant, domain: str, - entities: Sequence[Entity], + entities: Iterable[Entity], from_config_entry: bool = False, built_in: bool = True, ) -> MockPlatform: From ae4360b0e5715f866477b9f80f7881b29024d42d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 20 Jul 2024 21:26:00 +0200 Subject: [PATCH 1393/2411] Bump airgradient to 0.7.0 (#122268) --- homeassistant/components/airgradient/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index d523aa4ca03..af345bc25ed 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/airgradient", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["airgradient==0.6.1"], + "requirements": ["airgradient==0.7.0"], "zeroconf": ["_airgradient._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 7138997879f..491748872e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -410,7 +410,7 @@ aiowithings==3.0.2 aioymaps==1.2.4 # homeassistant.components.airgradient -airgradient==0.6.1 +airgradient==0.7.0 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 43b43b1ec25..938d6e591ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -389,7 +389,7 @@ aiowithings==3.0.2 aioymaps==1.2.4 # homeassistant.components.airgradient -airgradient==0.6.1 +airgradient==0.7.0 # homeassistant.components.airly airly==1.1.0 From 24b6f71f941d80ce931fc1e385a800bb7f39d344 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 20 Jul 2024 21:29:51 +0200 Subject: [PATCH 1394/2411] Bump twitchAPI to 4.2.1 (#122269) --- homeassistant/components/twitch/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/twitch/manifest.json b/homeassistant/components/twitch/manifest.json index 810982d0cb4..12ae1d1ee72 100644 --- a/homeassistant/components/twitch/manifest.json +++ b/homeassistant/components/twitch/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/twitch", "iot_class": "cloud_polling", "loggers": ["twitch"], - "requirements": ["twitchAPI==4.0.0"] + "requirements": ["twitchAPI==4.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 491748872e2..b34aac62079 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2789,7 +2789,7 @@ twentemilieu==2.0.1 twilio==6.32.0 # homeassistant.components.twitch -twitchAPI==4.0.0 +twitchAPI==4.2.1 # homeassistant.components.ukraine_alarm uasiren==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 938d6e591ca..f0e36e72382 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2181,7 +2181,7 @@ twentemilieu==2.0.1 twilio==6.32.0 # homeassistant.components.twitch -twitchAPI==4.0.0 +twitchAPI==4.2.1 # homeassistant.components.ukraine_alarm uasiren==0.0.1 From 1e28ae49f909e5d38a475a4cd528f3ea118e252a Mon Sep 17 00:00:00 2001 From: ilan <31193909+iloveicedgreentea@users.noreply.github.com> Date: Sat, 20 Jul 2024 13:44:14 -0700 Subject: [PATCH 1395/2411] Bump py-madvr to 1.6.29 (#122275) chore: bump version --- homeassistant/components/madvr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/madvr/manifest.json b/homeassistant/components/madvr/manifest.json index 9aa2c5a9b5d..ce6336acabc 100644 --- a/homeassistant/components/madvr/manifest.json +++ b/homeassistant/components/madvr/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/madvr", "integration_type": "device", "iot_class": "local_push", - "requirements": ["py-madvr2==1.6.27"] + "requirements": ["py-madvr2==1.6.29"] } diff --git a/requirements_all.txt b/requirements_all.txt index b34aac62079..07e193b202d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1638,7 +1638,7 @@ py-dormakaba-dkey==1.0.5 py-improv-ble-client==1.0.3 # homeassistant.components.madvr -py-madvr2==1.6.27 +py-madvr2==1.6.29 # homeassistant.components.melissa py-melissa-climate==2.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0e36e72382..09965389dcf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1324,7 +1324,7 @@ py-dormakaba-dkey==1.0.5 py-improv-ble-client==1.0.3 # homeassistant.components.madvr -py-madvr2==1.6.27 +py-madvr2==1.6.29 # homeassistant.components.melissa py-melissa-climate==2.1.4 From 9b9db86f1c1c8a6c1849d4ceefc49f8a949945e0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 21 Jul 2024 00:00:31 +0200 Subject: [PATCH 1396/2411] Bump aiomealie to 0.7.0 (#122278) --- homeassistant/components/mealie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index e869d72664b..d8dc827cc20 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mealie", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["aiomealie==0.6.0"] + "requirements": ["aiomealie==0.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 07e193b202d..0eededeecb2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -288,7 +288,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.6.0 +aiomealie==0.7.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09965389dcf..0eba65194a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.6.0 +aiomealie==0.7.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 From b3698a59e18122ee6b5daeaddcfd2a19a25fbbb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Jul 2024 17:24:16 -0500 Subject: [PATCH 1397/2411] Bump uiprotect to 5.4.0 (#122282) --- 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 3f607ab1938..afc4b9a06e6 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==5.3.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==5.4.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 0eededeecb2..010379f95b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2795,7 +2795,7 @@ twitchAPI==4.2.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==5.3.0 +uiprotect==5.4.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0eba65194a2..14f071947cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2187,7 +2187,7 @@ twitchAPI==4.2.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==5.3.0 +uiprotect==5.4.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 537a76d04950330336a5e19c080b2c9c9f35fdb7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 21 Jul 2024 08:41:42 +0200 Subject: [PATCH 1398/2411] Add model id to airgradient (#122271) --- homeassistant/components/airgradient/__init__.py | 11 +++++++---- tests/components/airgradient/snapshots/test_init.ambr | 8 ++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/airgradient/__init__.py b/homeassistant/components/airgradient/__init__.py index fe01d239f3c..69f1e70c6af 100644 --- a/homeassistant/components/airgradient/__init__.py +++ b/homeassistant/components/airgradient/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass -from airgradient import AirGradientClient +from airgradient import AirGradientClient, get_model_name from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform @@ -35,7 +35,7 @@ class AirGradientData: type AirGradientConfigEntry = ConfigEntry[AirGradientData] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry) -> bool: """Set up Airgradient from a config entry.""" client = AirGradientClient( @@ -53,7 +53,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: config_entry_id=entry.entry_id, identifiers={(DOMAIN, measurement_coordinator.serial_number)}, manufacturer="AirGradient", - model=measurement_coordinator.data.model, + model=get_model_name(measurement_coordinator.data.model), + model_id=measurement_coordinator.data.model, serial_number=measurement_coordinator.data.serial_number, sw_version=measurement_coordinator.data.firmware_version, ) @@ -68,6 +69,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: AirGradientConfigEntry +) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/tests/components/airgradient/snapshots/test_init.ambr b/tests/components/airgradient/snapshots/test_init.ambr index 3c4cea48ff2..e47c5b38bbc 100644 --- a/tests/components/airgradient/snapshots/test_init.ambr +++ b/tests/components/airgradient/snapshots/test_init.ambr @@ -20,8 +20,8 @@ 'labels': set({ }), 'manufacturer': 'AirGradient', - 'model': 'I-9PSL', - 'model_id': None, + 'model': 'AirGradient ONE', + 'model_id': 'I-9PSL', 'name': 'Airgradient', 'name_by_user': None, 'primary_config_entry': , @@ -52,8 +52,8 @@ 'labels': set({ }), 'manufacturer': 'AirGradient', - 'model': 'O-1PPT', - 'model_id': None, + 'model': 'AirGradient Open Air', + 'model_id': 'O-1PPT', 'name': 'Airgradient', 'name_by_user': None, 'primary_config_entry': , From 5075f0aac82a1d65d671c740fa1d73fdb27ada55 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sun, 21 Jul 2024 08:42:06 +0200 Subject: [PATCH 1399/2411] Bump ruff to 0.5.4 (#122289) --- .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 0e44d5ff402..c711f98f5d6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.3 + rev: v0.5.4 hooks: - id: ruff args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 3c4da0141cd..f7c7a18f3f3 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.3.0 -ruff==0.5.3 +ruff==0.5.4 yamllint==1.35.1 From fcca475e368408b1081907dfb68f48b303837492 Mon Sep 17 00:00:00 2001 From: ilan <31193909+iloveicedgreentea@users.noreply.github.com> Date: Sat, 20 Jul 2024 23:43:52 -0700 Subject: [PATCH 1400/2411] Add sensor platform to MadVR (#121617) * feat: add sensors * feat: add tests for sensors * feat: add options flow * feat: add tests for options flow * fix: remove options flow * fix: remove names and mac sensor, add incoming signal type * feat: add enum types to supported sensors * fix: consolidate tests into snapshot * fix: use consts * fix: update names and use snapshot platform * fix: fix test name for new translations * fix: comment * fix: improve sensor names * fix: address comments * feat: disable uncommon sensors by default * fix: update sensors * fix: revert config_flow change --- homeassistant/components/madvr/__init__.py | 2 +- homeassistant/components/madvr/const.py | 28 + homeassistant/components/madvr/icons.json | 83 + homeassistant/components/madvr/sensor.py | 280 ++++ homeassistant/components/madvr/strings.json | 80 + .../madvr/snapshots/test_binary_sensors.ambr | 188 --- .../madvr/snapshots/test_sensors.ambr | 1359 +++++++++++++++++ tests/components/madvr/test_sensors.py | 91 ++ 8 files changed, 1922 insertions(+), 189 deletions(-) create mode 100644 homeassistant/components/madvr/sensor.py create mode 100644 tests/components/madvr/snapshots/test_sensors.ambr create mode 100644 tests/components/madvr/test_sensors.py diff --git a/homeassistant/components/madvr/__init__.py b/homeassistant/components/madvr/__init__.py index b73c1f0dece..a6ad3b2d1fd 100644 --- a/homeassistant/components/madvr/__init__.py +++ b/homeassistant/components/madvr/__init__.py @@ -12,7 +12,7 @@ from homeassistant.core import Event, HomeAssistant, callback from .coordinator import MadVRCoordinator -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.REMOTE] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.REMOTE, Platform.SENSOR] type MadVRConfigEntry = ConfigEntry[MadVRCoordinator] diff --git a/homeassistant/components/madvr/const.py b/homeassistant/components/madvr/const.py index f0adeb9b6a5..3ac80725478 100644 --- a/homeassistant/components/madvr/const.py +++ b/homeassistant/components/madvr/const.py @@ -4,3 +4,31 @@ DOMAIN = "madvr" DEFAULT_NAME = "envy" DEFAULT_PORT = 44077 + +# Sensor keys +TEMP_GPU = "temp_gpu" +TEMP_HDMI = "temp_hdmi" +TEMP_CPU = "temp_cpu" +TEMP_MAINBOARD = "temp_mainboard" +INCOMING_RES = "incoming_res" +INCOMING_SIGNAL_TYPE = "incoming_signal_type" +INCOMING_FRAME_RATE = "incoming_frame_rate" +INCOMING_COLOR_SPACE = "incoming_color_space" +INCOMING_BIT_DEPTH = "incoming_bit_depth" +INCOMING_COLORIMETRY = "incoming_colorimetry" +INCOMING_BLACK_LEVELS = "incoming_black_levels" +INCOMING_ASPECT_RATIO = "incoming_aspect_ratio" +OUTGOING_RES = "outgoing_res" +OUTGOING_SIGNAL_TYPE = "outgoing_signal_type" +OUTGOING_FRAME_RATE = "outgoing_frame_rate" +OUTGOING_COLOR_SPACE = "outgoing_color_space" +OUTGOING_BIT_DEPTH = "outgoing_bit_depth" +OUTGOING_COLORIMETRY = "outgoing_colorimetry" +OUTGOING_BLACK_LEVELS = "outgoing_black_levels" +ASPECT_RES = "aspect_res" +ASPECT_DEC = "aspect_dec" +ASPECT_INT = "aspect_int" +ASPECT_NAME = "aspect_name" +MASKING_RES = "masking_res" +MASKING_DEC = "masking_dec" +MASKING_INT = "masking_int" diff --git a/homeassistant/components/madvr/icons.json b/homeassistant/components/madvr/icons.json index 42645787767..2f9f7217f5b 100644 --- a/homeassistant/components/madvr/icons.json +++ b/homeassistant/components/madvr/icons.json @@ -25,6 +25,89 @@ "off": "mdi:signal-off" } } + }, + "sensor": { + "mac_address": { + "default": "mdi:ethernet" + }, + "temp_gpu": { + "default": "mdi:thermometer" + }, + "temp_hdmi": { + "default": "mdi:thermometer" + }, + "temp_cpu": { + "default": "mdi:thermometer" + }, + "temp_mainboard": { + "default": "mdi:thermometer" + }, + "incoming_res": { + "default": "mdi:television" + }, + "incoming_frame_rate": { + "default": "mdi:television" + }, + "incoming_color_space": { + "default": "mdi:television" + }, + "incoming_bit_depth": { + "default": "mdi:television" + }, + "incoming_colorimetry": { + "default": "mdi:television" + }, + "incoming_black_levels": { + "default": "mdi:television" + }, + "incoming_aspect_ratio": { + "default": "mdi:aspect-ratio" + }, + "outgoing_res": { + "default": "mdi:television" + }, + "outgoing_frame_rate": { + "default": "mdi:television" + }, + "outgoing_color_space": { + "default": "mdi:television" + }, + "outgoing_bit_depth": { + "default": "mdi:television" + }, + "outgoing_colorimetry": { + "default": "mdi:television" + }, + "outgoing_black_levels": { + "default": "mdi:television" + }, + "aspect_res": { + "default": "mdi:aspect-ratio" + }, + "incoming_signal_type": { + "default": "mdi:video-image" + }, + "outgoing_signal_type": { + "default": "mdi:video-image" + }, + "aspect_dec": { + "default": "mdi:aspect-ratio" + }, + "aspect_int": { + "default": "mdi:aspect-ratio" + }, + "aspect_name": { + "default": "mdi:aspect-ratio" + }, + "masking_res": { + "default": "mdi:television" + }, + "masking_dec": { + "default": "mdi:television" + }, + "masking_int": { + "default": "mdi:television" + } } } } diff --git a/homeassistant/components/madvr/sensor.py b/homeassistant/components/madvr/sensor.py new file mode 100644 index 00000000000..d0f5556cc5d --- /dev/null +++ b/homeassistant/components/madvr/sensor.py @@ -0,0 +1,280 @@ +"""Sensor entities for the madVR integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import MadVRConfigEntry +from .const import ( + ASPECT_DEC, + ASPECT_INT, + ASPECT_NAME, + ASPECT_RES, + INCOMING_ASPECT_RATIO, + INCOMING_BIT_DEPTH, + INCOMING_BLACK_LEVELS, + INCOMING_COLOR_SPACE, + INCOMING_COLORIMETRY, + INCOMING_FRAME_RATE, + INCOMING_RES, + INCOMING_SIGNAL_TYPE, + MASKING_DEC, + MASKING_INT, + MASKING_RES, + OUTGOING_BIT_DEPTH, + OUTGOING_BLACK_LEVELS, + OUTGOING_COLOR_SPACE, + OUTGOING_COLORIMETRY, + OUTGOING_FRAME_RATE, + OUTGOING_RES, + OUTGOING_SIGNAL_TYPE, + TEMP_CPU, + TEMP_GPU, + TEMP_HDMI, + TEMP_MAINBOARD, +) +from .coordinator import MadVRCoordinator +from .entity import MadVREntity + + +def is_valid_temperature(value: float | None) -> bool: + """Check if the temperature value is valid.""" + return value is not None and value > 0 + + +def get_temperature(coordinator: MadVRCoordinator, key: str) -> float | None: + """Get temperature value if valid, otherwise return None.""" + try: + temp = float(coordinator.data.get(key, 0)) + except ValueError: + return None + else: + return temp if is_valid_temperature(temp) else None + + +@dataclass(frozen=True, kw_only=True) +class MadvrSensorEntityDescription(SensorEntityDescription): + """Describe madVR sensor entity.""" + + value_fn: Callable[[MadVRCoordinator], StateType] + + +SENSORS: tuple[MadvrSensorEntityDescription, ...] = ( + MadvrSensorEntityDescription( + key=TEMP_GPU, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda coordinator: get_temperature(coordinator, TEMP_GPU), + translation_key=TEMP_GPU, + entity_registry_enabled_default=False, + ), + MadvrSensorEntityDescription( + key=TEMP_HDMI, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda coordinator: get_temperature(coordinator, TEMP_HDMI), + translation_key=TEMP_HDMI, + entity_registry_enabled_default=False, + ), + MadvrSensorEntityDescription( + key=TEMP_CPU, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda coordinator: get_temperature(coordinator, TEMP_CPU), + translation_key=TEMP_CPU, + entity_registry_enabled_default=False, + ), + MadvrSensorEntityDescription( + key=TEMP_MAINBOARD, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda coordinator: get_temperature(coordinator, TEMP_MAINBOARD), + translation_key=TEMP_MAINBOARD, + entity_registry_enabled_default=False, + ), + MadvrSensorEntityDescription( + key=INCOMING_RES, + value_fn=lambda coordinator: coordinator.data.get(INCOMING_RES), + translation_key=INCOMING_RES, + ), + MadvrSensorEntityDescription( + key=INCOMING_SIGNAL_TYPE, + value_fn=lambda coordinator: coordinator.data.get(INCOMING_SIGNAL_TYPE), + translation_key=INCOMING_SIGNAL_TYPE, + device_class=SensorDeviceClass.ENUM, + options=["2D", "3D"], + entity_registry_enabled_default=False, + ), + MadvrSensorEntityDescription( + key=INCOMING_FRAME_RATE, + value_fn=lambda coordinator: coordinator.data.get(INCOMING_FRAME_RATE), + translation_key=INCOMING_FRAME_RATE, + ), + MadvrSensorEntityDescription( + key=INCOMING_COLOR_SPACE, + value_fn=lambda coordinator: coordinator.data.get(INCOMING_COLOR_SPACE), + translation_key=INCOMING_COLOR_SPACE, + device_class=SensorDeviceClass.ENUM, + options=["RGB", "444", "422", "420"], + ), + MadvrSensorEntityDescription( + key=INCOMING_BIT_DEPTH, + value_fn=lambda coordinator: coordinator.data.get(INCOMING_BIT_DEPTH), + translation_key=INCOMING_BIT_DEPTH, + device_class=SensorDeviceClass.ENUM, + options=["8bit", "10bit", "12bit"], + ), + MadvrSensorEntityDescription( + key=INCOMING_COLORIMETRY, + value_fn=lambda coordinator: coordinator.data.get(INCOMING_COLORIMETRY), + translation_key=INCOMING_COLORIMETRY, + device_class=SensorDeviceClass.ENUM, + options=["SDR", "HDR10", "HLG 601", "PAL", "709", "DCI", "2020"], + ), + MadvrSensorEntityDescription( + key=INCOMING_BLACK_LEVELS, + value_fn=lambda coordinator: coordinator.data.get(INCOMING_BLACK_LEVELS), + translation_key=INCOMING_BLACK_LEVELS, + device_class=SensorDeviceClass.ENUM, + options=["TV", "PC"], + ), + MadvrSensorEntityDescription( + key=INCOMING_ASPECT_RATIO, + value_fn=lambda coordinator: coordinator.data.get(INCOMING_ASPECT_RATIO), + translation_key=INCOMING_ASPECT_RATIO, + device_class=SensorDeviceClass.ENUM, + options=["16:9", "4:3"], + entity_registry_enabled_default=False, + ), + MadvrSensorEntityDescription( + key=OUTGOING_RES, + value_fn=lambda coordinator: coordinator.data.get(OUTGOING_RES), + translation_key=OUTGOING_RES, + ), + MadvrSensorEntityDescription( + key=OUTGOING_SIGNAL_TYPE, + value_fn=lambda coordinator: coordinator.data.get(OUTGOING_SIGNAL_TYPE), + translation_key=OUTGOING_SIGNAL_TYPE, + device_class=SensorDeviceClass.ENUM, + options=["2D", "3D"], + entity_registry_enabled_default=False, + ), + MadvrSensorEntityDescription( + key=OUTGOING_FRAME_RATE, + value_fn=lambda coordinator: coordinator.data.get(OUTGOING_FRAME_RATE), + translation_key=OUTGOING_FRAME_RATE, + ), + MadvrSensorEntityDescription( + key=OUTGOING_COLOR_SPACE, + value_fn=lambda coordinator: coordinator.data.get(OUTGOING_COLOR_SPACE), + translation_key=OUTGOING_COLOR_SPACE, + device_class=SensorDeviceClass.ENUM, + options=["RGB", "444", "422", "420"], + ), + MadvrSensorEntityDescription( + key=OUTGOING_BIT_DEPTH, + value_fn=lambda coordinator: coordinator.data.get(OUTGOING_BIT_DEPTH), + translation_key=OUTGOING_BIT_DEPTH, + device_class=SensorDeviceClass.ENUM, + options=["8bit", "10bit", "12bit"], + ), + MadvrSensorEntityDescription( + key=OUTGOING_COLORIMETRY, + value_fn=lambda coordinator: coordinator.data.get(OUTGOING_COLORIMETRY), + translation_key=OUTGOING_COLORIMETRY, + device_class=SensorDeviceClass.ENUM, + options=["SDR", "HDR10", "HLG 601", "PAL", "709", "DCI", "2020"], + ), + MadvrSensorEntityDescription( + key=OUTGOING_BLACK_LEVELS, + value_fn=lambda coordinator: coordinator.data.get(OUTGOING_BLACK_LEVELS), + translation_key=OUTGOING_BLACK_LEVELS, + device_class=SensorDeviceClass.ENUM, + options=["TV", "PC"], + ), + MadvrSensorEntityDescription( + key=ASPECT_RES, + value_fn=lambda coordinator: coordinator.data.get(ASPECT_RES), + translation_key=ASPECT_RES, + entity_registry_enabled_default=False, + ), + MadvrSensorEntityDescription( + key=ASPECT_DEC, + value_fn=lambda coordinator: coordinator.data.get(ASPECT_DEC), + translation_key=ASPECT_DEC, + ), + MadvrSensorEntityDescription( + key=ASPECT_INT, + value_fn=lambda coordinator: coordinator.data.get(ASPECT_INT), + translation_key=ASPECT_INT, + entity_registry_enabled_default=False, + ), + MadvrSensorEntityDescription( + key=ASPECT_NAME, + value_fn=lambda coordinator: coordinator.data.get(ASPECT_NAME), + translation_key=ASPECT_NAME, + entity_registry_enabled_default=False, + ), + MadvrSensorEntityDescription( + key=MASKING_RES, + value_fn=lambda coordinator: coordinator.data.get(MASKING_RES), + translation_key=MASKING_RES, + entity_registry_enabled_default=False, + ), + MadvrSensorEntityDescription( + key=MASKING_DEC, + value_fn=lambda coordinator: coordinator.data.get(MASKING_DEC), + translation_key=MASKING_DEC, + ), + MadvrSensorEntityDescription( + key=MASKING_INT, + value_fn=lambda coordinator: coordinator.data.get(MASKING_INT), + translation_key=MASKING_INT, + entity_registry_enabled_default=False, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: MadVRConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sensor entities.""" + coordinator = entry.runtime_data + async_add_entities(MadvrSensor(coordinator, description) for description in SENSORS) + + +class MadvrSensor(MadVREntity, SensorEntity): + """Base class for madVR sensors.""" + + def __init__( + self, + coordinator: MadVRCoordinator, + description: MadvrSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self.entity_description: MadvrSensorEntityDescription = description + self._attr_unique_id = f"{coordinator.mac}_{description.key}" + + @property + def native_value(self) -> float | str | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator) diff --git a/homeassistant/components/madvr/strings.json b/homeassistant/components/madvr/strings.json index c8b9851e780..3e8e786f775 100644 --- a/homeassistant/components/madvr/strings.json +++ b/homeassistant/components/madvr/strings.json @@ -36,6 +36,86 @@ "signal_state": { "name": "Signal state" } + }, + "sensor": { + "temp_gpu": { + "name": "GPU temperature" + }, + "temp_hdmi": { + "name": "HDMI temperature" + }, + "temp_cpu": { + "name": "CPU temperature" + }, + "temp_mainboard": { + "name": "Mainboard temperature" + }, + "incoming_res": { + "name": "Incoming resolution" + }, + "incoming_signal_type": { + "name": "Incoming signal type" + }, + "incoming_frame_rate": { + "name": "Incoming frame rate" + }, + "incoming_color_space": { + "name": "Incoming color space" + }, + "incoming_bit_depth": { + "name": "Incoming bit depth" + }, + "incoming_colorimetry": { + "name": "Incoming colorimetry" + }, + "incoming_black_levels": { + "name": "Incoming black levels" + }, + "incoming_aspect_ratio": { + "name": "Incoming aspect ratio" + }, + "outgoing_res": { + "name": "Outgoing resolution" + }, + "outgoing_signal_type": { + "name": "Outgoing signal type" + }, + "outgoing_frame_rate": { + "name": "Outgoing frame rate" + }, + "outgoing_color_space": { + "name": "Outgoing color space" + }, + "outgoing_bit_depth": { + "name": "Outgoing bit depth" + }, + "outgoing_colorimetry": { + "name": "Outgoing colorimetry" + }, + "outgoing_black_levels": { + "name": "Outgoing black levels" + }, + "aspect_res": { + "name": "Aspect resolution" + }, + "aspect_dec": { + "name": "Aspect decimal" + }, + "aspect_int": { + "name": "Aspect integer" + }, + "aspect_name": { + "name": "Aspect name" + }, + "masking_res": { + "name": "Masking resolution" + }, + "masking_dec": { + "name": "Masking decimal" + }, + "masking_int": { + "name": "Masking integer" + } } } } diff --git a/tests/components/madvr/snapshots/test_binary_sensors.ambr b/tests/components/madvr/snapshots/test_binary_sensors.ambr index 87069542eb1..7fd54a7c240 100644 --- a/tests/components/madvr/snapshots/test_binary_sensors.ambr +++ b/tests/components/madvr/snapshots/test_binary_sensors.ambr @@ -45,194 +45,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_hdr_flag-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.madvr_envy_madvr_hdr_flag', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:hdr-off', - 'original_name': 'madvr HDR Flag', - 'platform': 'madvr', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:11:22:33:44:55_hdr_flag', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_hdr_flag-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'madVR Envy madvr HDR Flag', - 'icon': 'mdi:hdr-off', - }), - 'context': , - 'entity_id': 'binary_sensor.madvr_envy_madvr_hdr_flag', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_outgoing_hdr_flag-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.madvr_envy_madvr_outgoing_hdr_flag', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:hdr-off', - 'original_name': 'madvr Outgoing HDR Flag', - 'platform': 'madvr', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:11:22:33:44:55_outgoing_hdr_flag', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_outgoing_hdr_flag-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'madVR Envy madvr Outgoing HDR Flag', - 'icon': 'mdi:hdr-off', - }), - 'context': , - 'entity_id': 'binary_sensor.madvr_envy_madvr_outgoing_hdr_flag', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_power_state-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.madvr_envy_madvr_power_state', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:power-off', - 'original_name': 'madvr Power State', - 'platform': 'madvr', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:11:22:33:44:55_power_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_power_state-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'madVR Envy madvr Power State', - 'icon': 'mdi:power-off', - }), - 'context': , - 'entity_id': 'binary_sensor.madvr_envy_madvr_power_state', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_signal_state-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.madvr_envy_madvr_signal_state', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:signal-off', - 'original_name': 'madvr Signal State', - 'platform': 'madvr', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:11:22:33:44:55_signal_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_signal_state-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'madVR Envy madvr Signal State', - 'icon': 'mdi:signal-off', - }), - 'context': , - 'entity_id': 'binary_sensor.madvr_envy_madvr_signal_state', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_binary_sensor_setup[binary_sensor.madvr_envy_outgoing_hdr_flag-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/madvr/snapshots/test_sensors.ambr b/tests/components/madvr/snapshots/test_sensors.ambr new file mode 100644 index 00000000000..7b0dd254f77 --- /dev/null +++ b/tests/components/madvr/snapshots/test_sensors.ambr @@ -0,0 +1,1359 @@ +# serializer version: 1 +# name: test_sensor_setup_and_states[sensor.madvr_envy_aspect_decimal-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_aspect_decimal', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Aspect decimal', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'aspect_dec', + 'unique_id': '00:11:22:33:44:55_aspect_dec', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_aspect_decimal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'madVR Envy Aspect decimal', + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_aspect_decimal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.78', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_aspect_integer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_aspect_integer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Aspect integer', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'aspect_int', + 'unique_id': '00:11:22:33:44:55_aspect_int', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_aspect_integer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'madVR Envy Aspect integer', + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_aspect_integer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '178', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_aspect_name-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_aspect_name', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Aspect name', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'aspect_name', + 'unique_id': '00:11:22:33:44:55_aspect_name', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_aspect_name-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'madVR Envy Aspect name', + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_aspect_name', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Widescreen', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_aspect_resolution-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_aspect_resolution', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Aspect resolution', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'aspect_res', + 'unique_id': '00:11:22:33:44:55_aspect_res', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_aspect_resolution-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'madVR Envy Aspect resolution', + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_aspect_resolution', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3840:2160', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_cpu_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_cpu_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CPU temperature', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temp_cpu', + 'unique_id': '00:11:22:33:44:55_temp_cpu', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_cpu_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'madVR Envy CPU temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_cpu_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50.2', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_gpu_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_gpu_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'GPU temperature', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temp_gpu', + 'unique_id': '00:11:22:33:44:55_temp_gpu', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_gpu_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'madVR Envy GPU temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_gpu_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45.5', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_hdmi_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_hdmi_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HDMI temperature', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temp_hdmi', + 'unique_id': '00:11:22:33:44:55_temp_hdmi', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_hdmi_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'madVR Envy HDMI temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_hdmi_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.0', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_aspect_ratio-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '16:9', + '4:3', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_incoming_aspect_ratio', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Incoming aspect ratio', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'incoming_aspect_ratio', + 'unique_id': '00:11:22:33:44:55_incoming_aspect_ratio', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_aspect_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'madVR Envy Incoming aspect ratio', + 'options': list([ + '16:9', + '4:3', + ]), + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_incoming_aspect_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16:9', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_bit_depth-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '8bit', + '10bit', + '12bit', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_incoming_bit_depth', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Incoming bit depth', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'incoming_bit_depth', + 'unique_id': '00:11:22:33:44:55_incoming_bit_depth', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_bit_depth-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'madVR Envy Incoming bit depth', + 'options': list([ + '8bit', + '10bit', + '12bit', + ]), + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_incoming_bit_depth', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10bit', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_black_levels-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'TV', + 'PC', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_incoming_black_levels', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Incoming black levels', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'incoming_black_levels', + 'unique_id': '00:11:22:33:44:55_incoming_black_levels', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_black_levels-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'madVR Envy Incoming black levels', + 'options': list([ + 'TV', + 'PC', + ]), + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_incoming_black_levels', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'PC', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_color_space-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'RGB', + '444', + '422', + '420', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_incoming_color_space', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Incoming color space', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'incoming_color_space', + 'unique_id': '00:11:22:33:44:55_incoming_color_space', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_color_space-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'madVR Envy Incoming color space', + 'options': list([ + 'RGB', + '444', + '422', + '420', + ]), + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_incoming_color_space', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'RGB', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_colorimetry-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'SDR', + 'HDR10', + 'HLG 601', + 'PAL', + '709', + 'DCI', + '2020', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_incoming_colorimetry', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Incoming colorimetry', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'incoming_colorimetry', + 'unique_id': '00:11:22:33:44:55_incoming_colorimetry', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_colorimetry-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'madVR Envy Incoming colorimetry', + 'options': list([ + 'SDR', + 'HDR10', + 'HLG 601', + 'PAL', + '709', + 'DCI', + '2020', + ]), + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_incoming_colorimetry', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2020', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_frame_rate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_incoming_frame_rate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Incoming frame rate', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'incoming_frame_rate', + 'unique_id': '00:11:22:33:44:55_incoming_frame_rate', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_frame_rate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'madVR Envy Incoming frame rate', + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_incoming_frame_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60p', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_resolution-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_incoming_resolution', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Incoming resolution', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'incoming_res', + 'unique_id': '00:11:22:33:44:55_incoming_res', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_resolution-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'madVR Envy Incoming resolution', + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_incoming_resolution', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3840x2160', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_signal_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '2D', + '3D', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_incoming_signal_type', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Incoming signal type', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'incoming_signal_type', + 'unique_id': '00:11:22:33:44:55_incoming_signal_type', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_incoming_signal_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'madVR Envy Incoming signal type', + 'options': list([ + '2D', + '3D', + ]), + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_incoming_signal_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3D', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_mainboard_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_mainboard_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Mainboard temperature', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temp_mainboard', + 'unique_id': '00:11:22:33:44:55_temp_mainboard', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_mainboard_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'madVR Envy Mainboard temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_mainboard_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35.8', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_masking_decimal-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_masking_decimal', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Masking decimal', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'masking_dec', + 'unique_id': '00:11:22:33:44:55_masking_dec', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_masking_decimal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'madVR Envy Masking decimal', + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_masking_decimal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.78', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_masking_integer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_masking_integer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Masking integer', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'masking_int', + 'unique_id': '00:11:22:33:44:55_masking_int', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_masking_integer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'madVR Envy Masking integer', + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_masking_integer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '178', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_masking_resolution-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_masking_resolution', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Masking resolution', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'masking_res', + 'unique_id': '00:11:22:33:44:55_masking_res', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_masking_resolution-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'madVR Envy Masking resolution', + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_masking_resolution', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3840:2160', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_outgoing_bit_depth-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '8bit', + '10bit', + '12bit', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_outgoing_bit_depth', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outgoing bit depth', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'outgoing_bit_depth', + 'unique_id': '00:11:22:33:44:55_outgoing_bit_depth', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_outgoing_bit_depth-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'madVR Envy Outgoing bit depth', + 'options': list([ + '8bit', + '10bit', + '12bit', + ]), + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_outgoing_bit_depth', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10bit', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_outgoing_black_levels-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'TV', + 'PC', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_outgoing_black_levels', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outgoing black levels', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'outgoing_black_levels', + 'unique_id': '00:11:22:33:44:55_outgoing_black_levels', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_outgoing_black_levels-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'madVR Envy Outgoing black levels', + 'options': list([ + 'TV', + 'PC', + ]), + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_outgoing_black_levels', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'PC', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_outgoing_color_space-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'RGB', + '444', + '422', + '420', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_outgoing_color_space', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outgoing color space', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'outgoing_color_space', + 'unique_id': '00:11:22:33:44:55_outgoing_color_space', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_outgoing_color_space-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'madVR Envy Outgoing color space', + 'options': list([ + 'RGB', + '444', + '422', + '420', + ]), + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_outgoing_color_space', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'RGB', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_outgoing_colorimetry-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'SDR', + 'HDR10', + 'HLG 601', + 'PAL', + '709', + 'DCI', + '2020', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_outgoing_colorimetry', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outgoing colorimetry', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'outgoing_colorimetry', + 'unique_id': '00:11:22:33:44:55_outgoing_colorimetry', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_outgoing_colorimetry-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'madVR Envy Outgoing colorimetry', + 'options': list([ + 'SDR', + 'HDR10', + 'HLG 601', + 'PAL', + '709', + 'DCI', + '2020', + ]), + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_outgoing_colorimetry', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2020', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_outgoing_frame_rate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_outgoing_frame_rate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Outgoing frame rate', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'outgoing_frame_rate', + 'unique_id': '00:11:22:33:44:55_outgoing_frame_rate', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_outgoing_frame_rate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'madVR Envy Outgoing frame rate', + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_outgoing_frame_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60p', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_outgoing_resolution-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_outgoing_resolution', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Outgoing resolution', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'outgoing_res', + 'unique_id': '00:11:22:33:44:55_outgoing_res', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_outgoing_resolution-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'madVR Envy Outgoing resolution', + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_outgoing_resolution', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3840x2160', + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_outgoing_signal_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '2D', + '3D', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.madvr_envy_outgoing_signal_type', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outgoing signal type', + 'platform': 'madvr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'outgoing_signal_type', + 'unique_id': '00:11:22:33:44:55_outgoing_signal_type', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.madvr_envy_outgoing_signal_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'madVR Envy Outgoing signal type', + 'options': list([ + '2D', + '3D', + ]), + }), + 'context': , + 'entity_id': 'sensor.madvr_envy_outgoing_signal_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2D', + }) +# --- diff --git a/tests/components/madvr/test_sensors.py b/tests/components/madvr/test_sensors.py new file mode 100644 index 00000000000..5a918dcd433 --- /dev/null +++ b/tests/components/madvr/test_sensors.py @@ -0,0 +1,91 @@ +"""Tests for the MadVR sensor entities.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er + +from . import setup_integration +from .conftest import get_update_callback + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor_setup_and_states( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + mock_madvr_client: AsyncMock, +) -> None: + """Test setup of the sensor entities and their states.""" + with patch("homeassistant.components.madvr.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + update_callback = get_update_callback(mock_madvr_client) + + # Create a big data update with all sensor values + update_data = { + "temp_gpu": 45.5, + "temp_hdmi": 40.0, + "temp_cpu": 50.2, + "temp_mainboard": 35.8, + "incoming_res": "3840x2160", + "incoming_frame_rate": "60p", + "outgoing_signal_type": "2D", + "incoming_signal_type": "3D", + "incoming_color_space": "RGB", + "incoming_bit_depth": "10bit", + "incoming_colorimetry": "2020", + "incoming_black_levels": "PC", + "incoming_aspect_ratio": "16:9", + "outgoing_res": "3840x2160", + "outgoing_frame_rate": "60p", + "outgoing_color_space": "RGB", + "outgoing_bit_depth": "10bit", + "outgoing_colorimetry": "2020", + "outgoing_black_levels": "PC", + "aspect_res": "3840:2160", + "aspect_dec": "1.78", + "aspect_int": "178", + "aspect_name": "Widescreen", + "masking_res": "3840:2160", + "masking_dec": "1.78", + "masking_int": "178", + } + + # Update all sensors at once + update_callback(update_data) + await hass.async_block_till_done() + + # Snapshot all entity states + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + # Test invalid temperature value + update_callback({"temp_gpu": -1}) + await hass.async_block_till_done() + assert hass.states.get("sensor.madvr_envy_gpu_temperature").state == STATE_UNKNOWN + + # Test sensor unknown + update_callback({"incoming_res": None}) + await hass.async_block_till_done() + assert ( + hass.states.get("sensor.madvr_envy_incoming_resolution").state == STATE_UNKNOWN + ) + + # Test sensor becomes known again + update_callback({"incoming_res": "1920x1080"}) + await hass.async_block_till_done() + assert hass.states.get("sensor.madvr_envy_incoming_resolution").state == "1920x1080" + + # Test temperature sensor + update_callback({"temp_gpu": 41.2}) + await hass.async_block_till_done() + assert hass.states.get("sensor.madvr_envy_gpu_temperature").state == "41.2" From f629364dc44aeb3d9a7f86f0e2eb9c9ae773010d Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Sun, 21 Jul 2024 12:24:54 +0200 Subject: [PATCH 1401/2411] Use pyblu library in bluesound (#117257) * Integrate pypi libraray: pyblu * Raise PlatformNotReady if _sync_status is not available yet * Revert "Raise PlatformNotReady if _sync_status is not available yet" This reverts commit a649a6bccd00cf16f80e40dc169ca8797ed3b6b2. * Replace 'async with timeout' with parameter in library * Set timeout back to 10 seconds * ruff fixes * Update homeassistant/components/bluesound/media_player.py Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- .../components/bluesound/manifest.json | 2 +- .../components/bluesound/media_player.py | 594 ++++++------------ requirements_all.txt | 4 +- requirements_test_all.txt | 1 - 4 files changed, 193 insertions(+), 408 deletions(-) diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index efd7dd0f347..e41a2ac21b9 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@thrawnarn"], "documentation": "https://www.home-assistant.io/integrations/bluesound", "iot_class": "local_polling", - "requirements": ["xmltodict==0.13.0"] + "requirements": ["pyblu==0.4.0"] } diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 0e752ac1f72..52bbf813dcc 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -3,18 +3,15 @@ from __future__ import annotations import asyncio -from asyncio import CancelledError, timeout -from datetime import timedelta -from http import HTTPStatus +from asyncio import CancelledError +from contextlib import suppress +from datetime import datetime, timedelta import logging from typing import Any, NamedTuple -from urllib import parse -import aiohttp from aiohttp.client_exceptions import ClientError -from aiohttp.hdrs import CONNECTION, KEEP_ALIVE +from pyblu import Input, Player, Preset, Status, SyncStatus import voluptuous as vol -import xmltodict from homeassistant.components import media_source from homeassistant.components.media_player import ( @@ -36,6 +33,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -109,7 +107,7 @@ SERVICE_TO_METHOD = { } -def _add_player(hass, async_add_entities, host, port=None, name=None): +def _add_player(hass: HomeAssistant, async_add_entities, host, port=None, name=None): """Add Bluesound players.""" @callback @@ -123,7 +121,7 @@ def _add_player(hass, async_add_entities, host, port=None, name=None): player.start_polling() @callback - def _stop_polling(): + def _stop_polling(event=None): """Stop polling.""" player.stop_polling() @@ -213,38 +211,38 @@ class BluesoundPlayer(MediaPlayerEntity): _attr_media_content_type = MediaType.MUSIC - def __init__(self, hass, host, port=None, name=None, init_callback=None): + def __init__( + self, hass: HomeAssistant, host, port=None, name=None, init_callback=None + ) -> None: """Initialize the media player.""" self.host = host self._hass = hass self.port = port - self._polling_session = async_get_clientsession(hass) self._polling_task = None # The actual polling task. self._name = name self._id = None - self._capture_items = [] - self._services_items = [] - self._preset_items = [] - self._sync_status = {} - self._status = None self._last_status_update = None + self._sync_status: SyncStatus | None = None + self._status: Status | None = None + self._inputs: list[Input] = [] + self._presets: list[Preset] = [] self._is_online = False self._retry_remove = None self._muted = False - self._master = None + self._master: BluesoundPlayer | None = None self._is_master = False self._group_name = None - self._group_list = [] + self._group_list: list[str] = [] self._bluesound_device_name = None + self._player = Player( + host, port, async_get_clientsession(hass), default_timeout=10 + ) self._init_callback = init_callback if self.port is None: self.port = DEFAULT_PORT - class _TimeoutException(Exception): - pass - @staticmethod def _try_get_index(string, search_string): """Get the index.""" @@ -253,28 +251,22 @@ class BluesoundPlayer(MediaPlayerEntity): except ValueError: return -1 - async def force_update_sync_status(self, on_updated_cb=None, raise_timeout=False): + async def force_update_sync_status(self, on_updated_cb=None) -> bool: """Update the internal status.""" - resp = await self.send_bluesound_command( - "SyncStatus", raise_timeout, raise_timeout - ) + sync_status = await self._player.sync_status() - if not resp: - return None - self._sync_status = resp["SyncStatus"].copy() + self._sync_status = sync_status if not self._name: - self._name = self._sync_status.get("@name", self.host) + self._name = sync_status.name if sync_status.name else self.host if not self._id: - self._id = self._sync_status.get("@id", None) + self._id = sync_status.id if not self._bluesound_device_name: - self._bluesound_device_name = self._sync_status.get("@name", self.host) + self._bluesound_device_name = self._name - if (master := self._sync_status.get("master")) is not None: + if sync_status.master is not None: self._is_master = False - master_host = master.get("#text") - master_port = master.get("@port", "11000") - master_id = f"{master_host}:{master_port}" + master_id = f"{sync_status.master.ip}:{sync_status.master.port}" master_device = [ device for device in self._hass.data[DATA_BLUESOUND] @@ -289,7 +281,7 @@ class BluesoundPlayer(MediaPlayerEntity): else: if self._master is not None: self._master = None - slaves = self._sync_status.get("slave") + slaves = self._sync_status.slaves self._is_master = slaves is not None if on_updated_cb: @@ -302,7 +294,7 @@ class BluesoundPlayer(MediaPlayerEntity): while True: await self.async_update_status() - except (TimeoutError, ClientError, BluesoundPlayer._TimeoutException): + except (TimeoutError, ClientError): _LOGGER.info("Node %s:%s is offline, retrying later", self.name, self.port) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) self.start_polling() @@ -328,7 +320,7 @@ class BluesoundPlayer(MediaPlayerEntity): self._retry_remove() self._retry_remove = None - await self.force_update_sync_status(self._init_callback, True) + await self.force_update_sync_status(self._init_callback) except (TimeoutError, ClientError): _LOGGER.info("Node %s:%s is offline, retrying later", self.host, self.port) self._retry_remove = async_track_time_interval( @@ -345,110 +337,48 @@ class BluesoundPlayer(MediaPlayerEntity): if not self._is_online: return - await self.async_update_sync_status() - await self.async_update_presets() - await self.async_update_captures() - await self.async_update_services() - - async def send_bluesound_command( - self, method, raise_timeout=False, allow_offline=False - ): - """Send command to the player.""" - if not self._is_online and not allow_offline: - return None - - if method[0] == "/": - method = method[1:] - url = f"http://{self.host}:{self.port}/{method}" - - _LOGGER.debug("Calling URL: %s", url) - response = None - - try: - websession = async_get_clientsession(self._hass) - async with timeout(10): - response = await websession.get(url) - - if response.status == HTTPStatus.OK: - result = await response.text() - if result: - data = xmltodict.parse(result) - else: - data = None - elif response.status == 595: - _LOGGER.info("Status 595 returned, treating as timeout") - raise BluesoundPlayer._TimeoutException - else: - _LOGGER.error("Error %s on %s", response.status, url) - return None - - except (TimeoutError, aiohttp.ClientError): - if raise_timeout: - _LOGGER.info("Timeout: %s:%s", self.host, self.port) - raise - _LOGGER.debug("Failed communicating: %s:%s", self.host, self.port) - return None - - return data + with suppress(TimeoutError): + await self.async_update_sync_status() + await self.async_update_presets() + await self.async_update_captures() async def async_update_status(self): """Use the poll session to always get the status of the player.""" - response = None - - url = "Status" - etag = "" + etag = None if self._status is not None: - etag = self._status.get("@etag", "") - - if etag != "": - url = f"Status?etag={etag}&timeout=120.0" - url = f"http://{self.host}:{self.port}/{url}" - - _LOGGER.debug("Calling URL: %s", url) + etag = self._status.etag try: - async with timeout(125): - response = await self._polling_session.get( - url, headers={CONNECTION: KEEP_ALIVE} - ) + status = await self._player.status(etag=etag, poll_timeout=120, timeout=125) - if response.status == HTTPStatus.OK: - result = await response.text() - self._is_online = True - self._last_status_update = dt_util.utcnow() - self._status = xmltodict.parse(result)["status"].copy() + self._is_online = True + self._last_status_update = dt_util.utcnow() + self._status = status - group_name = self._status.get("groupName") - if group_name != self._group_name: - _LOGGER.debug("Group name change detected on device: %s", self.id) - self._group_name = group_name + group_name = status.group_name + if group_name != self._group_name: + _LOGGER.debug("Group name change detected on device: %s", self.id) + self._group_name = group_name - # rebuild ordered list of entity_ids that are in the group, master is first - self._group_list = self.rebuild_bluesound_group() + # rebuild ordered list of entity_ids that are in the group, master is first + self._group_list = self.rebuild_bluesound_group() - # the sleep is needed to make sure that the - # devices is synced - await asyncio.sleep(1) - await self.async_trigger_sync_on_all() - elif self.is_grouped: - # when player is grouped we need to fetch volume from - # sync_status. We will force an update if the player is - # grouped this isn't a foolproof solution. A better - # solution would be to fetch sync_status more often when - # the device is playing. This would solve a lot of - # problems. This change will be done when the - # communication is moved to a separate library + # the sleep is needed to make sure that the + # devices is synced + await asyncio.sleep(1) + await self.async_trigger_sync_on_all() + elif self.is_grouped: + # when player is grouped we need to fetch volume from + # sync_status. We will force an update if the player is + # grouped this isn't a foolproof solution. A better + # solution would be to fetch sync_status more often when + # the device is playing. This would solve a lot of + # problems. This change will be done when the + # communication is moved to a separate library + with suppress(TimeoutError): await self.force_update_sync_status() - self.async_write_ha_state() - elif response.status == 595: - _LOGGER.info("Status 595 returned, treating as timeout") - raise BluesoundPlayer._TimeoutException - else: - _LOGGER.error( - "Error %s on %s. Trying one more time", response.status, url - ) - + self.async_write_ha_state() except (TimeoutError, ClientError): self._is_online = False self._last_status_update = None @@ -458,9 +388,10 @@ class BluesoundPlayer(MediaPlayerEntity): raise @property - def unique_id(self): + def unique_id(self) -> str | None: """Return an unique ID.""" - return f"{format_mac(self._sync_status['@mac'])}-{self.port}" + assert self._sync_status is not None + return f"{format_mac(self._sync_status.mac)}-{self.port}" async def async_trigger_sync_on_all(self): """Trigger sync status update on all devices.""" @@ -470,95 +401,25 @@ class BluesoundPlayer(MediaPlayerEntity): await player.force_update_sync_status() @Throttle(SYNC_STATUS_INTERVAL) - async def async_update_sync_status(self, on_updated_cb=None, raise_timeout=False): + async def async_update_sync_status(self, on_updated_cb=None): """Update sync status.""" - await self.force_update_sync_status(on_updated_cb, raise_timeout=False) + await self.force_update_sync_status(on_updated_cb) @Throttle(UPDATE_CAPTURE_INTERVAL) - async def async_update_captures(self): + async def async_update_captures(self) -> list[Input] | None: """Update Capture sources.""" - resp = await self.send_bluesound_command("RadioBrowse?service=Capture") - if not resp: - return None - self._capture_items = [] + inputs = await self._player.inputs() + self._inputs = inputs - def _create_capture_item(item): - self._capture_items.append( - { - "title": item.get("@text", ""), - "name": item.get("@text", ""), - "type": item.get("@serviceType", "Capture"), - "image": item.get("@image", ""), - "url": item.get("@URL", ""), - } - ) - - if "radiotime" in resp and "item" in resp["radiotime"]: - if isinstance(resp["radiotime"]["item"], list): - for item in resp["radiotime"]["item"]: - _create_capture_item(item) - else: - _create_capture_item(resp["radiotime"]["item"]) - - return self._capture_items + return inputs @Throttle(UPDATE_PRESETS_INTERVAL) - async def async_update_presets(self): + async def async_update_presets(self) -> list[Preset] | None: """Update Presets.""" - resp = await self.send_bluesound_command("Presets") - if not resp: - return None - self._preset_items = [] + presets = await self._player.presets() + self._presets = presets - def _create_preset_item(item): - self._preset_items.append( - { - "title": item.get("@name", ""), - "name": item.get("@name", ""), - "type": "preset", - "image": item.get("@image", ""), - "is_raw_url": True, - "url2": item.get("@url", ""), - "url": f"Preset?id={item.get('@id', '')}", - } - ) - - if "presets" in resp and "preset" in resp["presets"]: - if isinstance(resp["presets"]["preset"], list): - for item in resp["presets"]["preset"]: - _create_preset_item(item) - else: - _create_preset_item(resp["presets"]["preset"]) - - return self._preset_items - - @Throttle(UPDATE_SERVICES_INTERVAL) - async def async_update_services(self): - """Update Services.""" - resp = await self.send_bluesound_command("Services") - if not resp: - return None - self._services_items = [] - - def _create_service_item(item): - self._services_items.append( - { - "title": item.get("@displayname", ""), - "name": item.get("@name", ""), - "type": item.get("@type", ""), - "image": item.get("@icon", ""), - "url": item.get("@name", ""), - } - ) - - if "services" in resp and "service" in resp["services"]: - if isinstance(resp["services"]["service"], list): - for item in resp["services"]["service"]: - _create_service_item(item) - else: - _create_service_item(resp["services"]["service"]) - - return self._services_items + return presets @property def state(self) -> MediaPlayerState: @@ -569,7 +430,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self.is_grouped and not self.is_master: return MediaPlayerState.IDLE - status = self._status.get("state") + status = self._status.state if status in ("pause", "stop"): return MediaPlayerState.PAUSED if status in ("stream", "play"): @@ -577,15 +438,15 @@ class BluesoundPlayer(MediaPlayerEntity): return MediaPlayerState.IDLE @property - def media_title(self): + def media_title(self) -> str | None: """Title of current playing media.""" if self._status is None or (self.is_grouped and not self.is_master): return None - return self._status.get("title1") + return self._status.name @property - def media_artist(self): + def media_artist(self) -> str | None: """Artist of current playing media (Music track only).""" if self._status is None: return None @@ -593,35 +454,33 @@ class BluesoundPlayer(MediaPlayerEntity): if self.is_grouped and not self.is_master: return self._group_name - if not (artist := self._status.get("artist")): - artist = self._status.get("title2") - return artist + return self._status.artist @property - def media_album_name(self): + def media_album_name(self) -> str | None: """Artist of current playing media (Music track only).""" if self._status is None or (self.is_grouped and not self.is_master): return None - if not (album := self._status.get("album")): - album = self._status.get("title3") - return album + return self._status.album @property - def media_image_url(self): + def media_image_url(self) -> str | None: """Image url of current playing media.""" if self._status is None or (self.is_grouped and not self.is_master): return None - if not (url := self._status.get("image")): + url = self._status.image + if url is None: return None + if url[0] == "/": url = f"http://{self.host}:{self.port}{url}" return url @property - def media_position(self): + def media_position(self) -> int | None: """Position of current playing media in seconds.""" if self._status is None or (self.is_grouped and not self.is_master): return None @@ -630,154 +489,101 @@ class BluesoundPlayer(MediaPlayerEntity): if self._last_status_update is None or mediastate == MediaPlayerState.IDLE: return None - if (position := self._status.get("secs")) is None: + position = self._status.seconds + if position is None: return None - position = float(position) if mediastate == MediaPlayerState.PLAYING: position += (dt_util.utcnow() - self._last_status_update).total_seconds() - return position + return int(position) @property - def media_duration(self): + def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" if self._status is None or (self.is_grouped and not self.is_master): return None - if (duration := self._status.get("totlen")) is None: + duration = self._status.total_seconds + if duration is None: return None - return float(duration) + + return duration @property - def media_position_updated_at(self): + def media_position_updated_at(self) -> datetime | None: """Last time status was updated.""" return self._last_status_update @property - def volume_level(self): + def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" - volume = self._status.get("volume") - if self.is_grouped: - volume = self._sync_status.get("@volume") + volume = None - if volume is not None: - return int(volume) / 100 - return None + if self._status is not None: + volume = self._status.volume + if self.is_grouped and self._sync_status is not None: + volume = self._sync_status.volume + + if volume is None: + return None + + return volume / 100 @property - def is_volume_muted(self): + def is_volume_muted(self) -> bool: """Boolean if volume is currently muted.""" - mute = self._status.get("mute") - if self.is_grouped: - mute = self._sync_status.get("@mute") + mute = False + + if self._status is not None: + mute = self._status.mute + if self.is_grouped and self._sync_status is not None: + mute = self._sync_status.mute_volume is not None - if mute is not None: - mute = bool(int(mute)) return mute @property - def id(self): + def id(self) -> str | None: """Get id of device.""" return self._id @property - def name(self): + def name(self) -> str | None: """Return the name of the device.""" return self._name @property - def bluesound_device_name(self): + def bluesound_device_name(self) -> str | None: """Return the device name as returned by the device.""" return self._bluesound_device_name @property - def source_list(self): + def source_list(self) -> list[str] | None: """List of available input sources.""" if self._status is None or (self.is_grouped and not self.is_master): return None - sources = [source["title"] for source in self._preset_items] - - sources.extend( - source["title"] - for source in self._services_items - if source["type"] in ("LocalMusic", "RadioService") - ) - - sources.extend(source["title"] for source in self._capture_items) + sources = [x.text for x in self._inputs] + sources += [x.name for x in self._presets] return sources @property - def source(self): + def source(self) -> str | None: """Name of the current input source.""" if self._status is None or (self.is_grouped and not self.is_master): return None - if (current_service := self._status.get("service", "")) == "": - return "" - stream_url = self._status.get("streamUrl", "") + if self._status.input_id is not None: + for input_ in self._inputs: + if input_.id == self._status.input_id: + return input_.text - if self._status.get("is_preset", "") == "1" and stream_url != "": - # This check doesn't work with all presets, for example playlists. - # But it works with radio service_items will catch playlists. - items = [ - x - for x in self._preset_items - if "url2" in x and parse.unquote(x["url2"]) == stream_url - ] - if items: - return items[0]["title"] + for preset in self._presets: + if preset.url == self._status.stream_url: + return preset.name - # This could be a bit difficult to detect. Bluetooth could be named - # different things and there is not any way to match chooses in - # capture list to current playing. It's a bit of guesswork. - # This method will be needing some tweaking over time. - title = self._status.get("title1", "").lower() - if title == "bluetooth" or stream_url == "Capture:hw:2,0/44100/16/2": - items = [ - x - for x in self._capture_items - if x["url"] == "Capture%3Abluez%3Abluetooth" - ] - if items: - return items[0]["title"] - - items = [x for x in self._capture_items if x["url"] == stream_url] - if items: - return items[0]["title"] - - if stream_url[:8] == "Capture:": - stream_url = stream_url[8:] - - idx = BluesoundPlayer._try_get_index(stream_url, ":") - if idx > 0: - stream_url = stream_url[:idx] - for item in self._capture_items: - url = parse.unquote(item["url"]) - if url[:8] == "Capture:": - url = url[8:] - idx = BluesoundPlayer._try_get_index(url, ":") - if idx > 0: - url = url[:idx] - if url.lower() == stream_url.lower(): - return item["title"] - - items = [x for x in self._capture_items if x["name"] == current_service] - if items: - return items[0]["title"] - - items = [x for x in self._services_items if x["name"] == current_service] - if items: - return items[0]["title"] - - if self._status.get("streamUrl", "") != "": - _LOGGER.debug( - "Couldn't find source of stream URL: %s", - self._status.get("streamUrl", ""), - ) - return None + return self._status.service @property def supported_features(self) -> MediaPlayerEntityFeature: @@ -797,7 +603,7 @@ class BluesoundPlayer(MediaPlayerEntity): | MediaPlayerEntityFeature.BROWSE_MEDIA ) - if self._status.get("indexing", "0") == "0": + if not self._status.indexing: supported = ( supported | MediaPlayerEntityFeature.PAUSE @@ -819,25 +625,29 @@ class BluesoundPlayer(MediaPlayerEntity): | MediaPlayerEntityFeature.VOLUME_MUTE ) - if self._status.get("canSeek", "") == "1": + if self._status.can_seek: supported = supported | MediaPlayerEntityFeature.SEEK return supported @property - def is_master(self): + def is_master(self) -> bool: """Return true if player is a coordinator.""" return self._is_master @property - def is_grouped(self): + def is_grouped(self) -> bool: """Return true if player is a coordinator.""" return self._master is not None or self._is_master @property - def shuffle(self): + def shuffle(self) -> bool: """Return true if shuffle is active.""" - return self._status.get("shuffle", "0") == "1" + shuffle = False + if self._status is not None: + shuffle = self._status.shuffle + + return shuffle async def async_join(self, master): """Join the player to a group.""" @@ -847,7 +657,10 @@ class BluesoundPlayer(MediaPlayerEntity): if device.entity_id == master ] - if master_device: + if len(master_device) > 0: + if self.id == master_device[0].id: + raise ServiceValidationError("Cannot join player to itself") + _LOGGER.debug( "Trying to join player: %s to master: %s", self.id, @@ -859,9 +672,9 @@ class BluesoundPlayer(MediaPlayerEntity): _LOGGER.error("Master not found %s", master_device) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """List members in group.""" - attributes = {} + attributes: dict[str, Any] = {} if self._group_list: attributes = {ATTR_BLUESOUND_GROUP: self._group_list} @@ -869,10 +682,10 @@ class BluesoundPlayer(MediaPlayerEntity): return attributes - def rebuild_bluesound_group(self): + def rebuild_bluesound_group(self) -> list[str]: """Rebuild the list of entities in speaker group.""" if self._group_name is None: - return None + return [] device_group = self._group_name.split("+") @@ -895,121 +708,92 @@ class BluesoundPlayer(MediaPlayerEntity): _LOGGER.debug("Trying to unjoin player: %s", self.id) await self._master.async_remove_slave(self) - async def async_add_slave(self, slave_device): + async def async_add_slave(self, slave_device: BluesoundPlayer): """Add slave to master.""" - return await self.send_bluesound_command( - f"/AddSlave?slave={slave_device.host}&port={slave_device.port}" - ) + await self._player.add_slave(slave_device.host, slave_device.port) - async def async_remove_slave(self, slave_device): + async def async_remove_slave(self, slave_device: BluesoundPlayer): """Remove slave to master.""" - return await self.send_bluesound_command( - f"/RemoveSlave?slave={slave_device.host}&port={slave_device.port}" - ) + await self._player.remove_slave(slave_device.host, slave_device.port) - async def async_increase_timer(self): + async def async_increase_timer(self) -> int: """Increase sleep time on player.""" - sleep_time = await self.send_bluesound_command("/Sleep") - if sleep_time is None: - _LOGGER.error("Error while increasing sleep time on player: %s", self.id) - return 0 - - return int(sleep_time.get("sleep", "0")) + return await self._player.sleep_timer() async def async_clear_timer(self): """Clear sleep timer on player.""" sleep = 1 while sleep > 0: - sleep = await self.async_increase_timer() + sleep = await self._player.sleep_timer() async def async_set_shuffle(self, shuffle: bool) -> None: """Enable or disable shuffle mode.""" - value = "1" if shuffle else "0" - return await self.send_bluesound_command(f"/Shuffle?state={value}") + await self._player.shuffle(shuffle) async def async_select_source(self, source: str) -> None: """Select input source.""" if self.is_grouped and not self.is_master: return - items = [x for x in self._preset_items if x["title"] == source] + # presets and inputs might have the same name; presets have priority + url: str | None = None + for input_ in self._inputs: + if input_.text == source: + url = input_.url + for preset in self._presets: + if preset.name == source: + url = preset.url - if not items: - items = [x for x in self._services_items if x["title"] == source] - if not items: - items = [x for x in self._capture_items if x["title"] == source] - - if not items: - return - - selected_source = items[0] - url = f"Play?url={selected_source['url']}&preset_id&image={selected_source['image']}" - - if selected_source.get("is_raw_url"): - url = selected_source["url"] - - await self.send_bluesound_command(url) + await self._player.play_url(url) async def async_clear_playlist(self) -> None: """Clear players playlist.""" if self.is_grouped and not self.is_master: return - await self.send_bluesound_command("Clear") + await self._player.clear() async def async_media_next_track(self) -> None: """Send media_next command to media player.""" if self.is_grouped and not self.is_master: return - cmd = "Skip" - if self._status and "actions" in self._status: - for action in self._status["actions"]["action"]: - if "@name" in action and "@url" in action and action["@name"] == "skip": - cmd = action["@url"] - - await self.send_bluesound_command(cmd) + await self._player.skip() async def async_media_previous_track(self) -> None: """Send media_previous command to media player.""" if self.is_grouped and not self.is_master: return - cmd = "Back" - if self._status and "actions" in self._status: - for action in self._status["actions"]["action"]: - if "@name" in action and "@url" in action and action["@name"] == "back": - cmd = action["@url"] - - await self.send_bluesound_command(cmd) + await self._player.back() async def async_media_play(self) -> None: """Send media_play command to media player.""" if self.is_grouped and not self.is_master: return - await self.send_bluesound_command("Play") + await self._player.play() async def async_media_pause(self) -> None: """Send media_pause command to media player.""" if self.is_grouped and not self.is_master: return - await self.send_bluesound_command("Pause") + await self._player.pause() async def async_media_stop(self) -> None: """Send stop command.""" if self.is_grouped and not self.is_master: return - await self.send_bluesound_command("Pause") + await self._player.stop() async def async_media_seek(self, position: float) -> None: """Send media_seek command to media player.""" if self.is_grouped and not self.is_master: return - await self.send_bluesound_command(f"Play?seek={float(position)}") + await self._player.play(seek=int(position)) async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any @@ -1024,39 +808,39 @@ class BluesoundPlayer(MediaPlayerEntity): ) media_id = play_item.url - media_id = async_process_play_media_url(self.hass, media_id) + url = async_process_play_media_url(self.hass, media_id) - url = f"Play?url={media_id}" - - await self.send_bluesound_command(url) + await self._player.play_url(url) async def async_volume_up(self) -> None: """Volume up the media player.""" - current_vol = self.volume_level - if not current_vol or current_vol >= 1: - return - await self.async_set_volume_level(current_vol + 0.01) + if self.volume_level is None: + return None + + new_volume = self.volume_level + 0.01 + new_volume = min(1, new_volume) + return await self.async_set_volume_level(new_volume) async def async_volume_down(self) -> None: """Volume down the media player.""" - current_vol = self.volume_level - if not current_vol or current_vol <= 0: - return - await self.async_set_volume_level(current_vol - 0.01) + if self.volume_level is None: + return None + + new_volume = self.volume_level - 0.01 + new_volume = max(0, new_volume) + return await self.async_set_volume_level(new_volume) async def async_set_volume_level(self, volume: float) -> None: """Send volume_up command to media player.""" - if volume < 0: - volume = 0 - elif volume > 1: - volume = 1 - await self.send_bluesound_command(f"Volume?level={float(volume) * 100}") + volume = int(volume * 100) + volume = min(100, volume) + volume = max(0, volume) + + await self._player.volume(level=volume) async def async_mute_volume(self, mute: bool) -> None: """Send mute command to media player.""" - if mute: - await self.send_bluesound_command("Volume?mute=1") - await self.send_bluesound_command("Volume?mute=0") + await self._player.volume(mute=mute) async def async_browse_media( self, diff --git a/requirements_all.txt b/requirements_all.txt index 010379f95b5..b9ed2e4f192 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1740,6 +1740,9 @@ pybbox==0.0.5-alpha # homeassistant.components.blackbird pyblackbird==0.6 +# homeassistant.components.bluesound +pyblu==0.4.0 + # homeassistant.components.neato pybotvac==0.0.25 @@ -2920,7 +2923,6 @@ xknx==2.12.2 # homeassistant.components.knx xknxproject==3.7.1 -# homeassistant.components.bluesound # homeassistant.components.fritz # homeassistant.components.rest # homeassistant.components.startca diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 14f071947cd..bc8b75d9e1c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2294,7 +2294,6 @@ xknx==2.12.2 # homeassistant.components.knx xknxproject==3.7.1 -# homeassistant.components.bluesound # homeassistant.components.fritz # homeassistant.components.rest # homeassistant.components.startca From 8da630f8c63c6102411acef6db20fcfce27d9556 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sun, 21 Jul 2024 12:26:32 +0200 Subject: [PATCH 1402/2411] Improve sensor test coverage for enphase_envoy (#122229) * Improve sensor platform test COV for enphase_envoy * Use async_fire_time_changed to trigger next data update in enphase_envoy test --- tests/components/enphase_envoy/test_sensor.py | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/tests/components/enphase_envoy/test_sensor.py b/tests/components/enphase_envoy/test_sensor.py index d0d347d9df0..1b066ca9f59 100644 --- a/tests/components/enphase_envoy/test_sensor.py +++ b/tests/components/enphase_envoy/test_sensor.py @@ -3,12 +3,14 @@ from itertools import chain from unittest.mock import AsyncMock, patch +from freezegun.api import FrozenDateTimeFactory from pyenphase.const import PHASENAMES import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.enphase_envoy.const import Platform -from homeassistant.const import UnitOfTemperature +from homeassistant.components.enphase_envoy.coordinator import SCAN_INTERVAL +from homeassistant.const import STATE_UNKNOWN, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util @@ -16,7 +18,7 @@ from homeassistant.util.unit_conversion import TemperatureConverter from . import setup_integration -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @pytest.mark.parametrize( @@ -843,3 +845,64 @@ def integration_disabled_entities( ) if entity_entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION ] + + +@pytest.mark.parametrize( + ("mock_envoy"), + [ + "envoy_metered_batt_relay", + ], + indirect=["mock_envoy"], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor_missing_data( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_envoy: AsyncMock, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test enphase_envoy sensor platform midding data handling.""" + with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, config_entry) + + ENTITY_BASE = f"{Platform.SENSOR}.envoy_{mock_envoy.serial_number}" + + # force missing data to test 'if == none' code sections + mock_envoy.data.system_production_phases["L2"] = None + mock_envoy.data.system_consumption_phases["L2"] = None + mock_envoy.data.ctmeter_production = None + mock_envoy.data.ctmeter_consumption = None + mock_envoy.data.ctmeter_storage = None + mock_envoy.data.ctmeter_production_phases = None + mock_envoy.data.ctmeter_consumption_phases = None + mock_envoy.data.ctmeter_storage_phases = None + + # use different inverter serial to test 'expected inverter missing' code + mock_envoy.data.inverters["2"] = mock_envoy.data.inverters.pop("1") + + # force HA to detect changed data by changing raw + mock_envoy.data.raw = {"I": "am changed"} + + # MOve time to next update + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + # all these should now be in unknown state + for entity in ( + "lifetime_energy_production_l2", + "lifetime_energy_consumption_l2", + "metering_status_production_ct", + "metering_status_net_consumption_ct", + "metering_status_storage_ct", + "metering_status_production_ct_l2", + "metering_status_net_consumption_ct_l2", + "metering_status_storage_ct_l2", + ): + assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{entity}")) + assert entity_state.state == STATE_UNKNOWN + + # test the original inverter is now unknown + assert (entity_state := hass.states.get("sensor.inverter_1")) + assert entity_state.state == STATE_UNKNOWN From 87e377cf8489646f62e76e5644f57a2e6524a260 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 21 Jul 2024 12:36:06 +0200 Subject: [PATCH 1403/2411] Ensure mqtt subscriptions are in a set (#122201) --- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mqtt/client.py | 8 ++++---- homeassistant/components/mqtt/models.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ed87a99386b..5f7f1b1d330 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -254,7 +254,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: mqtt_data.client.async_restore_tracked_subscriptions( mqtt_data.subscriptions_to_restore ) - mqtt_data.subscriptions_to_restore = [] + mqtt_data.subscriptions_to_restore = set() mqtt_data.reload_dispatchers.append( entry.add_update_listener(_async_config_entry_updated) ) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 2ebd105b432..6762f440c5a 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -427,12 +427,12 @@ class MQTT: await self.async_init_client() @property - def subscriptions(self) -> list[Subscription]: + def subscriptions(self) -> set[Subscription]: """Return the tracked subscriptions.""" - return [ + return { *chain.from_iterable(self._simple_subscriptions.values()), *self._wildcard_subscriptions, - ] + } def cleanup(self) -> None: """Clean up listeners.""" @@ -735,7 +735,7 @@ class MQTT: @callback def async_restore_tracked_subscriptions( - self, subscriptions: list[Subscription] + self, subscriptions: set[Subscription] ) -> None: """Restore tracked subscriptions after reload.""" for subscription in subscriptions: diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index e5a9a9c44da..c355510a5c2 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -423,7 +423,7 @@ class MqttData: reload_handlers: dict[str, CALLBACK_TYPE] = field(default_factory=dict) reload_schema: dict[str, VolSchemaType] = field(default_factory=dict) state_write_requests: EntityTopicState = field(default_factory=EntityTopicState) - subscriptions_to_restore: list[Subscription] = field(default_factory=list) + subscriptions_to_restore: set[Subscription] = field(default_factory=set) tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict) From 48661054d99e01da74039ba8d15c314623b0f3e1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 21 Jul 2024 13:56:16 +0200 Subject: [PATCH 1404/2411] Improve fixture usage for sensor based deCONZ tests (#122297) --- tests/components/deconz/conftest.py | 17 +- .../deconz/test_alarm_control_panel.py | 50 ++- tests/components/deconz/test_binary_sensor.py | 17 +- tests/components/deconz/test_climate.py | 378 ++++++++---------- tests/components/deconz/test_deconz_event.py | 146 +++---- .../components/deconz/test_device_trigger.py | 157 ++++---- tests/components/deconz/test_gateway.py | 12 +- tests/components/deconz/test_lock.py | 43 +- tests/components/deconz/test_logbook.py | 50 ++- tests/components/deconz/test_number.py | 44 +- tests/components/deconz/test_select.py | 138 +++---- tests/components/deconz/test_sensor.py | 71 ++-- tests/components/deconz/test_services.py | 26 +- 13 files changed, 530 insertions(+), 619 deletions(-) diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py index 9beabdc2b15..b0d64e3231f 100644 --- a/tests/components/deconz/conftest.py +++ b/tests/components/deconz/conftest.py @@ -122,6 +122,8 @@ def fixture_get_request( if "state" in light_payload: light_payload = {"0": light_payload} data.setdefault("lights", light_payload) + if "state" in sensor_payload or "config" in sensor_payload: + sensor_payload = {"0": sensor_payload} data.setdefault("sensors", sensor_payload) def __mock_requests(host: str = "") -> None: @@ -185,16 +187,13 @@ def fixture_light_data() -> dict[str, Any]: @pytest.fixture(name="sensor_payload") -def fixture_sensor_data(sensor_1_payload: dict[str, Any]) -> dict[str, Any]: - """Sensor data.""" - if sensor_1_payload: - return {"1": sensor_1_payload} - return {} +def fixture_sensor_data() -> dict[str, Any]: + """Sensor data. - -@pytest.fixture(name="sensor_1_payload") -def fixture_sensor_1_data() -> dict[str, Any]: - """Sensor 1 data.""" + Should be + - one sensor data payload {"config": ..., "state": ...} ("0") + - multiple sensors {"1": ..., "2": ...} + """ return {} diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py index 7836a3ee3b4..7dd7dc49603 100644 --- a/tests/components/deconz/test_alarm_control_panel.py +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -70,32 +70,30 @@ from tests.test_util.aiohttp import AiohttpClientMocker "sensor_payload", [ { - "0": { - "config": { - "battery": 95, - "enrolled": 1, - "on": True, - "pending": [], - "reachable": True, - }, - "ep": 1, - "etag": "5aaa1c6bae8501f59929539c6e8f44d6", - "lastseen": "2021-07-25T18:07Z", - "manufacturername": "lk", - "modelid": "ZB-KeypadGeneric-D0002", - "name": "Keypad", - "state": { - "action": "armed_stay", - "lastupdated": "2021-07-25T18:02:51.172", - "lowbattery": False, - "panel": "none", - "seconds_remaining": 55, - "tampered": False, - }, - "swversion": "3.13", - "type": "ZHAAncillaryControl", - "uniqueid": "00:00:00:00:00:00:00:00-00", - } + "config": { + "battery": 95, + "enrolled": 1, + "on": True, + "pending": [], + "reachable": True, + }, + "ep": 1, + "etag": "5aaa1c6bae8501f59929539c6e8f44d6", + "lastseen": "2021-07-25T18:07Z", + "manufacturername": "lk", + "modelid": "ZB-KeypadGeneric-D0002", + "name": "Keypad", + "state": { + "action": "armed_stay", + "lastupdated": "2021-07-25T18:02:51.172", + "lowbattery": False, + "panel": "none", + "seconds_remaining": 55, + "tampered": False, + }, + "swversion": "3.13", + "type": "ZHAAncillaryControl", + "uniqueid": "00:00:00:00:00:00:00:00-00", } ], ) diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 79939288ace..4d6c89ccc4d 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -454,7 +454,7 @@ TEST_DATA = [ @pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: True}]) -@pytest.mark.parametrize(("sensor_1_payload", "expected"), TEST_DATA) +@pytest.mark.parametrize(("sensor_payload", "expected"), TEST_DATA) async def test_binary_sensors( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -492,11 +492,7 @@ async def test_binary_sensors( # Change state - event_changed_sensor = { - "r": "sensors", - "id": "1", - "state": expected["websocket_event"], - } + event_changed_sensor = {"r": "sensors", "state": expected["websocket_event"]} await mock_websocket_data(event_changed_sensor) await hass.async_block_till_done() assert hass.states.get(expected["entity_id"]).state == expected["next_state"] @@ -514,7 +510,7 @@ async def test_binary_sensors( @pytest.mark.parametrize( - "sensor_1_payload", + "sensor_payload", [ { "name": "CLIP presence sensor", @@ -607,7 +603,6 @@ async def test_add_new_binary_sensor( event_added_sensor = { "e": "added", "r": "sensors", - "id": "1", "sensor": { "id": "Presence sensor id", "name": "Presence sensor", @@ -646,7 +641,6 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_service_call( event_added_sensor = { "e": "added", "r": "sensors", - "id": "1", "sensor": sensor, } @@ -667,7 +661,7 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_service_call( == 0 ) - deconz_payload["sensors"] = {"1": sensor} + deconz_payload["sensors"]["0"] = sensor mock_requests() await hass.services.async_call(DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH) @@ -699,7 +693,6 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_options_change( event_added_sensor = { "e": "added", "r": "sensors", - "id": "1", "sensor": sensor, } @@ -720,7 +713,7 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_options_change( == 0 ) - deconz_payload["sensors"] = {"1": sensor} + deconz_payload["sensors"]["0"] = sensor mock_requests() hass.config_entries.async_update_entry( diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 37e06148201..94b4a30b8d2 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -54,36 +54,34 @@ from tests.test_util.aiohttp import AiohttpClientMocker "sensor_payload", [ { - "0": { - "config": { - "battery": 59, - "displayflipped": None, - "heatsetpoint": 2100, - "locked": True, - "mountingmode": None, - "offset": 0, - "on": True, - "reachable": True, - }, - "ep": 1, - "etag": "6130553ac247174809bae47144ee23f8", - "lastseen": "2020-11-29T19:31Z", - "manufacturername": "Danfoss", - "modelid": "eTRV0100", - "name": "thermostat", - "state": { - "errorcode": None, - "lastupdated": "2020-11-29T19:28:40.665", - "mountingmodeactive": False, - "on": True, - "temperature": 2102, - "valve": 24, - "windowopen": "Closed", - }, - "swversion": "01.02.0008 01.02", - "type": "ZHAThermostat", - "uniqueid": "14:b4:57:ff:fe:d5:4e:77-01-0201", - } + "config": { + "battery": 59, + "displayflipped": None, + "heatsetpoint": 2100, + "locked": True, + "mountingmode": None, + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "6130553ac247174809bae47144ee23f8", + "lastseen": "2020-11-29T19:31Z", + "manufacturername": "Danfoss", + "modelid": "eTRV0100", + "name": "thermostat", + "state": { + "errorcode": None, + "lastupdated": "2020-11-29T19:28:40.665", + "mountingmodeactive": False, + "on": True, + "temperature": 2102, + "valve": 24, + "windowopen": "Closed", + }, + "swversion": "01.02.0008 01.02", + "type": "ZHAThermostat", + "uniqueid": "14:b4:57:ff:fe:d5:4e:77-01-0201", } ], ) @@ -179,19 +177,17 @@ async def test_simple_climate_device( "sensor_payload", [ { - "1": { - "name": "Thermostat", - "type": "ZHAThermostat", - "state": {"on": True, "temperature": 2260, "valve": 30}, - "config": { - "battery": 100, - "heatsetpoint": 2200, - "mode": "auto", - "offset": 10, - "reachable": True, - }, - "uniqueid": "00:00:00:00:00:00:00:00-00", - } + "name": "Thermostat", + "type": "ZHAThermostat", + "state": {"on": True, "temperature": 2260, "valve": 30}, + "config": { + "battery": 100, + "heatsetpoint": 2200, + "mode": "auto", + "offset": 10, + "reachable": True, + }, + "uniqueid": "00:00:00:00:00:00:00:00-00", } ], ) @@ -225,7 +221,6 @@ async def test_climate_device_without_cooling_support( event_changed_sensor = { "r": "sensors", - "id": "1", "config": {"mode": "off"}, } await mock_websocket_data(event_changed_sensor) @@ -241,7 +236,6 @@ async def test_climate_device_without_cooling_support( event_changed_sensor = { "r": "sensors", - "id": "1", "config": {"mode": "other"}, "state": {"on": True}, } @@ -258,7 +252,6 @@ async def test_climate_device_without_cooling_support( event_changed_sensor = { "r": "sensors", - "id": "1", "state": {"on": False}, } await mock_websocket_data(event_changed_sensor) @@ -272,7 +265,7 @@ async def test_climate_device_without_cooling_support( # Verify service calls - aioclient_mock = mock_put_request("/sensors/1/config") + aioclient_mock = mock_put_request("/sensors/0/config") # Service set HVAC mode to auto @@ -354,31 +347,29 @@ async def test_climate_device_without_cooling_support( "sensor_payload", [ { - "0": { - "config": { - "battery": 25, - "coolsetpoint": 1111, - "fanmode": None, - "heatsetpoint": 2222, - "mode": "heat", - "offset": 0, - "on": True, - "reachable": True, - }, - "ep": 1, - "etag": "074549903686a77a12ef0f06c499b1ef", - "lastseen": "2020-11-27T13:45Z", - "manufacturername": "Zen Within", - "modelid": "Zen-01", - "name": "Zen-01", - "state": { - "lastupdated": "2020-11-27T13:42:40.863", - "on": False, - "temperature": 2320, - }, - "type": "ZHAThermostat", - "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", - } + "config": { + "battery": 25, + "coolsetpoint": 1111, + "fanmode": None, + "heatsetpoint": 2222, + "mode": "heat", + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "074549903686a77a12ef0f06c499b1ef", + "lastseen": "2020-11-27T13:45Z", + "manufacturername": "Zen Within", + "modelid": "Zen-01", + "name": "Zen-01", + "state": { + "lastupdated": "2020-11-27T13:42:40.863", + "on": False, + "temperature": 2320, + }, + "type": "ZHAThermostat", + "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", } ], ) @@ -455,31 +446,29 @@ async def test_climate_device_with_cooling_support( "sensor_payload", [ { - "0": { - "config": { - "battery": 25, - "coolsetpoint": None, - "fanmode": "auto", - "heatsetpoint": 2222, - "mode": "heat", - "offset": 0, - "on": True, - "reachable": True, - }, - "ep": 1, - "etag": "074549903686a77a12ef0f06c499b1ef", - "lastseen": "2020-11-27T13:45Z", - "manufacturername": "Zen Within", - "modelid": "Zen-01", - "name": "Zen-01", - "state": { - "lastupdated": "2020-11-27T13:42:40.863", - "on": False, - "temperature": 2320, - }, - "type": "ZHAThermostat", - "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", - } + "config": { + "battery": 25, + "coolsetpoint": None, + "fanmode": "auto", + "heatsetpoint": 2222, + "mode": "heat", + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "074549903686a77a12ef0f06c499b1ef", + "lastseen": "2020-11-27T13:45Z", + "manufacturername": "Zen Within", + "modelid": "Zen-01", + "name": "Zen-01", + "state": { + "lastupdated": "2020-11-27T13:42:40.863", + "on": False, + "temperature": 2320, + }, + "type": "ZHAThermostat", + "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", } ], ) @@ -591,32 +580,30 @@ async def test_climate_device_with_fan_support( "sensor_payload", [ { - "0": { - "config": { - "battery": 25, - "coolsetpoint": None, - "fanmode": None, - "heatsetpoint": 2222, - "mode": "heat", - "preset": "auto", - "offset": 0, - "on": True, - "reachable": True, - }, - "ep": 1, - "etag": "074549903686a77a12ef0f06c499b1ef", - "lastseen": "2020-11-27T13:45Z", - "manufacturername": "Zen Within", - "modelid": "Zen-01", - "name": "Zen-01", - "state": { - "lastupdated": "2020-11-27T13:42:40.863", - "on": False, - "temperature": 2320, - }, - "type": "ZHAThermostat", - "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", - } + "config": { + "battery": 25, + "coolsetpoint": None, + "fanmode": None, + "heatsetpoint": 2222, + "mode": "heat", + "preset": "auto", + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "074549903686a77a12ef0f06c499b1ef", + "lastseen": "2020-11-27T13:45Z", + "manufacturername": "Zen Within", + "modelid": "Zen-01", + "name": "Zen-01", + "state": { + "lastupdated": "2020-11-27T13:42:40.863", + "on": False, + "temperature": 2320, + }, + "type": "ZHAThermostat", + "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", } ], ) @@ -775,19 +762,17 @@ async def test_clip_climate_device( "sensor_payload", [ { - "1": { - "name": "Thermostat", - "type": "ZHAThermostat", - "state": {"on": True, "temperature": 2260, "valve": 30}, - "config": { - "battery": 100, - "heatsetpoint": 2200, - "mode": "auto", - "offset": 10, - "reachable": True, - }, - "uniqueid": "00:00:00:00:00:00:00:00-00", - } + "name": "Thermostat", + "type": "ZHAThermostat", + "state": {"on": True, "temperature": 2260, "valve": 30}, + "config": { + "battery": 100, + "heatsetpoint": 2200, + "mode": "auto", + "offset": 10, + "reachable": True, + }, + "uniqueid": "00:00:00:00:00:00:00:00-00", } ], ) @@ -803,11 +788,7 @@ async def test_verify_state_update( == HVACAction.HEATING ) - event_changed_sensor = { - "r": "sensors", - "id": "1", - "state": {"on": False}, - } + event_changed_sensor = {"r": "sensors", "state": {"on": False}} await mock_websocket_data(event_changed_sensor) await hass.async_block_till_done() @@ -827,7 +808,6 @@ async def test_add_new_climate_device( event_added_sensor = { "e": "added", "r": "sensors", - "id": "1", "sensor": { "id": "Thermostat id", "name": "Thermostat", @@ -862,14 +842,12 @@ async def test_add_new_climate_device( "sensor_payload", [ { - "1": { - "name": "CLIP thermostat sensor", - "type": "CLIPThermostat", - "state": {}, - "config": {}, - "uniqueid": "00:00:00:00:00:00:00:00-00", - }, - } + "name": "CLIP thermostat sensor", + "type": "CLIPThermostat", + "state": {}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:00-00", + }, ], ) @pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: False}]) @@ -883,26 +861,24 @@ async def test_not_allow_clip_thermostat(hass: HomeAssistant) -> None: "sensor_payload", [ { - "0": { - "config": { - "battery": 25, - "heatsetpoint": 2222, - "mode": None, - "preset": "auto", - "offset": 0, - "on": True, - "reachable": True, - }, - "ep": 1, - "etag": "074549903686a77a12ef0f06c499b1ef", - "lastseen": "2020-11-27T13:45Z", - "manufacturername": "Zen Within", - "modelid": "Zen-01", - "name": "Zen-01", - "state": {"lastupdated": "none", "on": None, "temperature": 2290}, - "type": "ZHAThermostat", - "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", - } + "config": { + "battery": 25, + "heatsetpoint": 2222, + "mode": None, + "preset": "auto", + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "074549903686a77a12ef0f06c499b1ef", + "lastseen": "2020-11-27T13:45Z", + "manufacturername": "Zen Within", + "modelid": "Zen-01", + "name": "Zen-01", + "state": {"lastupdated": "none", "on": None, "temperature": 2290}, + "type": "ZHAThermostat", + "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", } ], ) @@ -922,38 +898,36 @@ async def test_no_mode_no_state(hass: HomeAssistant) -> None: "sensor_payload", [ { - "0": { - "config": { - "battery": 58, - "heatsetpoint": 2200, - "locked": False, - "mode": "heat", - "offset": -200, - "on": True, - "preset": "manual", - "reachable": True, - "schedule": {}, - "schedule_on": False, - "setvalve": False, - "windowopen_set": False, - }, - "ep": 1, - "etag": "404c15db68c318ebe7832ce5aa3d1e30", - "lastannounced": "2022-08-31T03:00:59Z", - "lastseen": "2022-09-19T11:58Z", - "manufacturername": "_TZE200_b6wax7g0", - "modelid": "TS0601", - "name": "Thermostat", - "state": { - "lastupdated": "2022-09-19T11:58:24.204", - "lowbattery": False, - "on": False, - "temperature": 2200, - "valve": 0, - }, - "type": "ZHAThermostat", - "uniqueid": "84:fd:27:ff:fe:8a:eb:89-01-0201", - } + "config": { + "battery": 58, + "heatsetpoint": 2200, + "locked": False, + "mode": "heat", + "offset": -200, + "on": True, + "preset": "manual", + "reachable": True, + "schedule": {}, + "schedule_on": False, + "setvalve": False, + "windowopen_set": False, + }, + "ep": 1, + "etag": "404c15db68c318ebe7832ce5aa3d1e30", + "lastannounced": "2022-08-31T03:00:59Z", + "lastseen": "2022-09-19T11:58Z", + "manufacturername": "_TZE200_b6wax7g0", + "modelid": "TS0601", + "name": "Thermostat", + "state": { + "lastupdated": "2022-09-19T11:58:24.204", + "lowbattery": False, + "on": False, + "temperature": 2200, + "valve": 0, + }, + "type": "ZHAThermostat", + "uniqueid": "84:fd:27:ff:fe:8a:eb:89-01-0201", } ], ) diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 0e998776174..adbea618efb 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -243,32 +243,30 @@ async def test_deconz_events( "sensor_payload", [ { - "1": { - "config": { - "battery": 95, - "enrolled": 1, - "on": True, - "pending": [], - "reachable": True, - }, - "ep": 1, - "etag": "5aaa1c6bae8501f59929539c6e8f44d6", - "lastseen": "2021-07-25T18:07Z", - "manufacturername": "lk", - "modelid": "ZB-KeypadGeneric-D0002", - "name": "Keypad", - "state": { - "action": "invalid_code", - "lastupdated": "2021-07-25T18:02:51.172", - "lowbattery": False, - "panel": "exit_delay", - "seconds_remaining": 55, - "tampered": False, - }, - "swversion": "3.13", - "type": "ZHAAncillaryControl", - "uniqueid": "00:00:00:00:00:00:00:01-00", - } + "config": { + "battery": 95, + "enrolled": 1, + "on": True, + "pending": [], + "reachable": True, + }, + "ep": 1, + "etag": "5aaa1c6bae8501f59929539c6e8f44d6", + "lastseen": "2021-07-25T18:07Z", + "manufacturername": "lk", + "modelid": "ZB-KeypadGeneric-D0002", + "name": "Keypad", + "state": { + "action": "invalid_code", + "lastupdated": "2021-07-25T18:02:51.172", + "lowbattery": False, + "panel": "exit_delay", + "seconds_remaining": 55, + "tampered": False, + }, + "swversion": "3.13", + "type": "ZHAAncillaryControl", + "uniqueid": "00:00:00:00:00:00:00:01-00", } ], ) @@ -296,7 +294,6 @@ async def test_deconz_alarm_events( event_changed_sensor = { "r": "sensors", - "id": "1", "state": {"action": AncillaryControlAction.EMERGENCY}, } await mock_websocket_data(event_changed_sensor) @@ -318,7 +315,6 @@ async def test_deconz_alarm_events( event_changed_sensor = { "r": "sensors", - "id": "1", "state": {"action": AncillaryControlAction.FIRE}, } await mock_websocket_data(event_changed_sensor) @@ -340,7 +336,6 @@ async def test_deconz_alarm_events( event_changed_sensor = { "r": "sensors", - "id": "1", "state": {"action": AncillaryControlAction.INVALID_CODE}, } await mock_websocket_data(event_changed_sensor) @@ -362,7 +357,6 @@ async def test_deconz_alarm_events( event_changed_sensor = { "r": "sensors", - "id": "1", "state": {"action": AncillaryControlAction.PANIC}, } await mock_websocket_data(event_changed_sensor) @@ -384,7 +378,6 @@ async def test_deconz_alarm_events( event_changed_sensor = { "r": "sensors", - "id": "1", "state": {"action": AncillaryControlAction.ARMED_AWAY}, } await mock_websocket_data(event_changed_sensor) @@ -396,7 +389,6 @@ async def test_deconz_alarm_events( event_changed_sensor = { "r": "sensors", - "id": "1", "state": {"panel": AncillaryControlPanel.ARMED_AWAY}, } await mock_websocket_data(event_changed_sensor) @@ -420,29 +412,27 @@ async def test_deconz_alarm_events( "sensor_payload", [ { - "1": { - "config": { - "devicemode": "undirected", - "on": True, - "reachable": True, - "sensitivity": 3, - "triggerdistance": "medium", - }, - "etag": "13ff209f9401b317987d42506dd4cd79", - "lastannounced": None, - "lastseen": "2022-06-28T23:13Z", - "manufacturername": "aqara", - "modelid": "lumi.motion.ac01", - "name": "Aqara FP1", - "state": { - "lastupdated": "2022-06-28T23:13:38.577", - "presence": True, - "presenceevent": "leave", - }, - "swversion": "20210121", - "type": "ZHAPresence", - "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", - } + "config": { + "devicemode": "undirected", + "on": True, + "reachable": True, + "sensitivity": 3, + "triggerdistance": "medium", + }, + "etag": "13ff209f9401b317987d42506dd4cd79", + "lastannounced": None, + "lastseen": "2022-06-28T23:13Z", + "manufacturername": "aqara", + "modelid": "lumi.motion.ac01", + "name": "Aqara FP1", + "state": { + "lastupdated": "2022-06-28T23:13:38.577", + "presence": True, + "presenceevent": "leave", + }, + "swversion": "20210121", + "type": "ZHAPresence", + "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", } ], ) @@ -481,7 +471,6 @@ async def test_deconz_presence_events( ): event_changed_sensor = { "r": "sensors", - "id": "1", "state": {"presenceevent": presence_event}, } await mock_websocket_data(event_changed_sensor) @@ -500,7 +489,6 @@ async def test_deconz_presence_events( event_changed_sensor = { "r": "sensors", - "id": "1", "state": {"presenceevent": PresenceStatePresenceEvent.NINE}, } await mock_websocket_data(event_changed_sensor) @@ -524,28 +512,26 @@ async def test_deconz_presence_events( "sensor_payload", [ { - "1": { - "config": { - "battery": 100, - "on": True, - "reachable": True, - }, - "etag": "463728970bdb7d04048fc4373654f45a", - "lastannounced": "2022-07-03T13:57:59Z", - "lastseen": "2022-07-03T14:02Z", - "manufacturername": "Signify Netherlands B.V.", - "modelid": "RDM002", - "name": "RDM002 44", - "state": { - "expectedeventduration": 400, - "expectedrotation": 75, - "lastupdated": "2022-07-03T11:37:49.586", - "rotaryevent": 2, - }, - "swversion": "2.59.19", - "type": "ZHARelativeRotary", - "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-14-fc00", - } + "config": { + "battery": 100, + "on": True, + "reachable": True, + }, + "etag": "463728970bdb7d04048fc4373654f45a", + "lastannounced": "2022-07-03T13:57:59Z", + "lastseen": "2022-07-03T14:02Z", + "manufacturername": "Signify Netherlands B.V.", + "modelid": "RDM002", + "name": "RDM002 44", + "state": { + "expectedeventduration": 400, + "expectedrotation": 75, + "lastupdated": "2022-07-03T11:37:49.586", + "rotaryevent": 2, + }, + "swversion": "2.59.19", + "type": "ZHARelativeRotary", + "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-14-fc00", } ], ) @@ -575,7 +561,6 @@ async def test_deconz_relative_rotary_events( for rotary_event, duration, rotation in ((1, 100, 50), (2, 200, -50)): event_changed_sensor = { "r": "sensors", - "id": "1", "state": { "rotaryevent": rotary_event, "expectedeventduration": duration, @@ -600,7 +585,6 @@ async def test_deconz_relative_rotary_events( event_changed_sensor = { "r": "sensors", - "id": "1", "name": "123", } await mock_websocket_data(event_changed_sensor) diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 97197589442..1d3196ba8e9 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -47,25 +47,23 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: "sensor_payload", [ { - "1": { - "config": { - "alert": "none", - "battery": 60, - "group": "10", - "on": True, - "reachable": True, - }, - "ep": 1, - "etag": "1b355c0b6d2af28febd7ca9165881952", - "manufacturername": "IKEA of Sweden", - "mode": 1, - "modelid": "TRADFRI on/off switch", - "name": "TRÅDFRI on/off switch ", - "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"}, - "swversion": "1.4.018", - "type": "ZHASwitch", - "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000", - } + "config": { + "alert": "none", + "battery": 60, + "group": "10", + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "1b355c0b6d2af28febd7ca9165881952", + "manufacturername": "IKEA of Sweden", + "mode": 1, + "modelid": "TRADFRI on/off switch", + "name": "TRÅDFRI on/off switch ", + "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"}, + "swversion": "1.4.018", + "type": "ZHASwitch", + "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000", } ], ) @@ -153,32 +151,30 @@ async def test_get_triggers( "sensor_payload", [ { - "1": { - "config": { - "battery": 95, - "enrolled": 1, - "on": True, - "pending": [], - "reachable": True, - }, - "ep": 1, - "etag": "5aaa1c6bae8501f59929539c6e8f44d6", - "lastseen": "2021-07-25T18:07Z", - "manufacturername": "lk", - "modelid": "ZB-KeypadGeneric-D0002", - "name": "Keypad", - "state": { - "action": "armed_stay", - "lastupdated": "2021-07-25T18:02:51.172", - "lowbattery": False, - "panel": "exit_delay", - "seconds_remaining": 55, - "tampered": False, - }, - "swversion": "3.13", - "type": "ZHAAncillaryControl", - "uniqueid": "00:00:00:00:00:00:00:00-00", - } + "config": { + "battery": 95, + "enrolled": 1, + "on": True, + "pending": [], + "reachable": True, + }, + "ep": 1, + "etag": "5aaa1c6bae8501f59929539c6e8f44d6", + "lastseen": "2021-07-25T18:07Z", + "manufacturername": "lk", + "modelid": "ZB-KeypadGeneric-D0002", + "name": "Keypad", + "state": { + "action": "armed_stay", + "lastupdated": "2021-07-25T18:02:51.172", + "lowbattery": False, + "panel": "exit_delay", + "seconds_remaining": 55, + "tampered": False, + }, + "swversion": "3.13", + "type": "ZHAAncillaryControl", + "uniqueid": "00:00:00:00:00:00:00:00-00", } ], ) @@ -250,24 +246,22 @@ async def test_get_triggers_for_alarm_event( "sensor_payload", [ { - "1": { - "config": { - "alert": "none", - "group": "10", - "on": True, - "reachable": True, - }, - "ep": 1, - "etag": "1b355c0b6d2af28febd7ca9165881952", - "manufacturername": "IKEA of Sweden", - "mode": 1, - "modelid": "Unsupported model", - "name": "TRÅDFRI on/off switch ", - "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"}, - "swversion": "1.4.018", - "type": "ZHASwitch", - "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000", - } + "config": { + "alert": "none", + "group": "10", + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "1b355c0b6d2af28febd7ca9165881952", + "manufacturername": "IKEA of Sweden", + "mode": 1, + "modelid": "Unsupported model", + "name": "TRÅDFRI on/off switch ", + "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"}, + "swversion": "1.4.018", + "type": "ZHASwitch", + "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000", } ], ) @@ -293,25 +287,23 @@ async def test_get_triggers_manage_unsupported_remotes( "sensor_payload", [ { - "1": { - "config": { - "alert": "none", - "battery": 60, - "group": "10", - "on": True, - "reachable": True, - }, - "ep": 1, - "etag": "1b355c0b6d2af28febd7ca9165881952", - "manufacturername": "IKEA of Sweden", - "mode": 1, - "modelid": "TRADFRI on/off switch", - "name": "TRÅDFRI on/off switch ", - "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"}, - "swversion": "1.4.018", - "type": "ZHASwitch", - "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000", - } + "config": { + "alert": "none", + "battery": 60, + "group": "10", + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "1b355c0b6d2af28febd7ca9165881952", + "manufacturername": "IKEA of Sweden", + "mode": 1, + "modelid": "TRADFRI on/off switch", + "name": "TRÅDFRI on/off switch ", + "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"}, + "swversion": "1.4.018", + "type": "ZHASwitch", + "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000", } ], ) @@ -353,7 +345,6 @@ async def test_functional_device_trigger( event_changed_sensor = { "r": "sensors", - "id": "1", "state": {"buttonevent": 1002}, } await mock_websocket_data(event_changed_sensor) diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index cafead19d69..bc7c3362d6b 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -121,13 +121,11 @@ async def test_gateway_device_configuration_url_when_addon( "sensor_payload", [ { - "1": { - "name": "presence", - "type": "ZHAPresence", - "state": {"presence": False}, - "config": {"on": True, "reachable": True}, - "uniqueid": "00:00:00:00:00:00:00:00-00", - } + "name": "presence", + "type": "ZHAPresence", + "state": {"presence": False}, + "config": {"on": True, "reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:00-00", } ], ) diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index 3ebd4fea978..923e8d768c8 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -96,27 +96,25 @@ async def test_lock_from_light( "sensor_payload", [ { - "1": { - "config": { - "battery": 100, - "lock": False, - "on": True, - "reachable": True, - }, - "ep": 11, - "etag": "a43862f76b7fa48b0fbb9107df123b0e", - "lastseen": "2021-03-06T22:25Z", - "manufacturername": "Onesti Products AS", - "modelid": "easyCodeTouch_v1", - "name": "Door lock", - "state": { - "lastupdated": "2021-03-06T21:25:45.624", - "lockstate": "unlocked", - }, - "swversion": "20201211", - "type": "ZHADoorLock", - "uniqueid": "00:00:00:00:00:00:00:00-00", - } + "config": { + "battery": 100, + "lock": False, + "on": True, + "reachable": True, + }, + "ep": 11, + "etag": "a43862f76b7fa48b0fbb9107df123b0e", + "lastseen": "2021-03-06T22:25Z", + "manufacturername": "Onesti Products AS", + "modelid": "easyCodeTouch_v1", + "name": "Door lock", + "state": { + "lastupdated": "2021-03-06T21:25:45.624", + "lockstate": "unlocked", + }, + "swversion": "20201211", + "type": "ZHADoorLock", + "uniqueid": "00:00:00:00:00:00:00:00-00", } ], ) @@ -132,7 +130,6 @@ async def test_lock_from_sensor( event_changed_sensor = { "r": "sensors", - "id": "1", "state": {"lockstate": "locked"}, } await mock_websocket_data(event_changed_sensor) @@ -142,7 +139,7 @@ async def test_lock_from_sensor( # Verify service calls - aioclient_mock = mock_put_request("/sensors/1/config") + aioclient_mock = mock_put_request("/sensors/0/config") # Service lock door diff --git a/tests/components/deconz/test_logbook.py b/tests/components/deconz/test_logbook.py index 2303ee3a298..d23680225f1 100644 --- a/tests/components/deconz/test_logbook.py +++ b/tests/components/deconz/test_logbook.py @@ -30,30 +30,28 @@ from tests.components.logbook.common import MockRow, mock_humanify "sensor_payload", [ { - "1": { - "config": { - "armed": "disarmed", - "enrolled": 0, - "on": True, - "panel": "disarmed", - "pending": [], - "reachable": True, - }, - "ep": 1, - "etag": "3c4008d74035dfaa1f0bb30d24468b12", - "lastseen": "2021-04-02T13:07Z", - "manufacturername": "Universal Electronics Inc", - "modelid": "URC4450BC0-X-R", - "name": "Keypad", - "state": { - "action": "armed_away,1111,55", - "lastupdated": "2021-04-02T13:08:18.937", - "lowbattery": False, - "tampered": True, - }, - "type": "ZHAAncillaryControl", - "uniqueid": "00:0d:6f:00:13:4f:61:39-01-0501", - } + "config": { + "armed": "disarmed", + "enrolled": 0, + "on": True, + "panel": "disarmed", + "pending": [], + "reachable": True, + }, + "ep": 1, + "etag": "3c4008d74035dfaa1f0bb30d24468b12", + "lastseen": "2021-04-02T13:07Z", + "manufacturername": "Universal Electronics Inc", + "modelid": "URC4450BC0-X-R", + "name": "Keypad", + "state": { + "action": "armed_away,1111,55", + "lastupdated": "2021-04-02T13:08:18.937", + "lowbattery": False, + "tampered": True, + }, + "type": "ZHAAncillaryControl", + "uniqueid": "00:0d:6f:00:13:4f:61:39-01-0501", } ], ) @@ -64,8 +62,8 @@ async def test_humanifying_deconz_alarm_event( sensor_payload: dict[str, Any], ) -> None: """Test humanifying deCONZ alarm event.""" - keypad_event_id = slugify(sensor_payload["1"]["name"]) - keypad_serial = serial_from_unique_id(sensor_payload["1"]["uniqueid"]) + keypad_event_id = slugify(sensor_payload["name"]) + keypad_serial = serial_from_unique_id(sensor_payload["uniqueid"]) keypad_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, keypad_serial)} ) diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py index c2f40bd9ff5..f027e6b5a9f 100644 --- a/tests/components/deconz/test_number.py +++ b/tests/components/deconz/test_number.py @@ -23,18 +23,16 @@ from tests.test_util.aiohttp import AiohttpClientMocker TEST_DATA = [ ( # Presence sensor - delay configuration { - "0": { - "name": "Presence sensor", - "type": "ZHAPresence", - "state": {"dark": False, "presence": False}, - "config": { - "delay": 0, - "on": True, - "reachable": True, - "temperature": 10, - }, - "uniqueid": "00:00:00:00:00:00:00:00-00", - } + "name": "Presence sensor", + "type": "ZHAPresence", + "state": {"dark": False, "presence": False}, + "config": { + "delay": 0, + "on": True, + "reachable": True, + "temperature": 10, + }, + "uniqueid": "00:00:00:00:00:00:00:00-00", }, { "entity_count": 3, @@ -61,18 +59,16 @@ TEST_DATA = [ ), ( # Presence sensor - duration configuration { - "0": { - "name": "Presence sensor", - "type": "ZHAPresence", - "state": {"dark": False, "presence": False}, - "config": { - "duration": 0, - "on": True, - "reachable": True, - "temperature": 10, - }, - "uniqueid": "00:00:00:00:00:00:00:00-00", - } + "name": "Presence sensor", + "type": "ZHAPresence", + "state": {"dark": False, "presence": False}, + "config": { + "duration": 0, + "on": True, + "reachable": True, + "temperature": 10, + }, + "uniqueid": "00:00:00:00:00:00:00:00-00", }, { "entity_count": 3, diff --git a/tests/components/deconz/test_select.py b/tests/components/deconz/test_select.py index 8a43181efaf..3864af65cd4 100644 --- a/tests/components/deconz/test_select.py +++ b/tests/components/deconz/test_select.py @@ -24,29 +24,27 @@ from tests.test_util.aiohttp import AiohttpClientMocker TEST_DATA = [ ( # Presence Device Mode { - "1": { - "config": { - "devicemode": "undirected", - "on": True, - "reachable": True, - "sensitivity": 3, - "triggerdistance": "medium", - }, - "etag": "13ff209f9401b317987d42506dd4cd79", - "lastannounced": None, - "lastseen": "2022-06-28T23:13Z", - "manufacturername": "aqara", - "modelid": "lumi.motion.ac01", - "name": "Aqara FP1", - "state": { - "lastupdated": "2022-06-28T23:13:38.577", - "presence": True, - "presenceevent": "leave", - }, - "swversion": "20210121", - "type": "ZHAPresence", - "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", - } + "config": { + "devicemode": "undirected", + "on": True, + "reachable": True, + "sensitivity": 3, + "triggerdistance": "medium", + }, + "etag": "13ff209f9401b317987d42506dd4cd79", + "lastannounced": None, + "lastseen": "2022-06-28T23:13Z", + "manufacturername": "aqara", + "modelid": "lumi.motion.ac01", + "name": "Aqara FP1", + "state": { + "lastupdated": "2022-06-28T23:13:38.577", + "presence": True, + "presenceevent": "leave", + }, + "swversion": "20210121", + "type": "ZHAPresence", + "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", }, { "entity_count": 5, @@ -59,35 +57,33 @@ TEST_DATA = [ "options": ["leftright", "undirected"], }, "option": PresenceConfigDeviceMode.LEFT_AND_RIGHT.value, - "request": "/sensors/1/config", + "request": "/sensors/0/config", "request_data": {"devicemode": "leftright"}, }, ), ( # Presence Sensitivity { - "1": { - "config": { - "devicemode": "undirected", - "on": True, - "reachable": True, - "sensitivity": 3, - "triggerdistance": "medium", - }, - "etag": "13ff209f9401b317987d42506dd4cd79", - "lastannounced": None, - "lastseen": "2022-06-28T23:13Z", - "manufacturername": "aqara", - "modelid": "lumi.motion.ac01", - "name": "Aqara FP1", - "state": { - "lastupdated": "2022-06-28T23:13:38.577", - "presence": True, - "presenceevent": "leave", - }, - "swversion": "20210121", - "type": "ZHAPresence", - "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", - } + "config": { + "devicemode": "undirected", + "on": True, + "reachable": True, + "sensitivity": 3, + "triggerdistance": "medium", + }, + "etag": "13ff209f9401b317987d42506dd4cd79", + "lastannounced": None, + "lastseen": "2022-06-28T23:13Z", + "manufacturername": "aqara", + "modelid": "lumi.motion.ac01", + "name": "Aqara FP1", + "state": { + "lastupdated": "2022-06-28T23:13:38.577", + "presence": True, + "presenceevent": "leave", + }, + "swversion": "20210121", + "type": "ZHAPresence", + "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", }, { "entity_count": 5, @@ -100,35 +96,33 @@ TEST_DATA = [ "options": ["High", "Medium", "Low"], }, "option": "Medium", - "request": "/sensors/1/config", + "request": "/sensors/0/config", "request_data": {"sensitivity": 2}, }, ), ( # Presence Trigger Distance { - "1": { - "config": { - "devicemode": "undirected", - "on": True, - "reachable": True, - "sensitivity": 3, - "triggerdistance": "medium", - }, - "etag": "13ff209f9401b317987d42506dd4cd79", - "lastannounced": None, - "lastseen": "2022-06-28T23:13Z", - "manufacturername": "aqara", - "modelid": "lumi.motion.ac01", - "name": "Aqara FP1", - "state": { - "lastupdated": "2022-06-28T23:13:38.577", - "presence": True, - "presenceevent": "leave", - }, - "swversion": "20210121", - "type": "ZHAPresence", - "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", - } + "config": { + "devicemode": "undirected", + "on": True, + "reachable": True, + "sensitivity": 3, + "triggerdistance": "medium", + }, + "etag": "13ff209f9401b317987d42506dd4cd79", + "lastannounced": None, + "lastseen": "2022-06-28T23:13Z", + "manufacturername": "aqara", + "modelid": "lumi.motion.ac01", + "name": "Aqara FP1", + "state": { + "lastupdated": "2022-06-28T23:13:38.577", + "presence": True, + "presenceevent": "leave", + }, + "swversion": "20210121", + "type": "ZHAPresence", + "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", }, { "entity_count": 5, @@ -141,7 +135,7 @@ TEST_DATA = [ "options": ["far", "medium", "near"], }, "option": PresenceConfigTriggerDistance.FAR.value, - "request": "/sensors/1/config", + "request": "/sensors/0/config", "request_data": {"triggerdistance": "far"}, }, ), diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 7c83452c6be..c29ed09c4c0 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -899,7 +899,7 @@ TEST_DATA = [ ] -@pytest.mark.parametrize(("sensor_1_payload", "expected"), TEST_DATA) +@pytest.mark.parametrize(("sensor_payload", "expected"), TEST_DATA) @pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: True}]) async def test_sensors( hass: HomeAssistant, @@ -952,7 +952,7 @@ async def test_sensors( # Change state - event_changed_sensor = {"r": "sensors", "id": "1"} + event_changed_sensor = {"r": "sensors"} event_changed_sensor |= expected["websocket_event"] await mock_websocket_data(event_changed_sensor) await hass.async_block_till_done() @@ -974,14 +974,12 @@ async def test_sensors( "sensor_payload", [ { - "1": { - "name": "CLIP temperature sensor", - "type": "CLIPTemperature", - "state": {"temperature": 2600}, - "config": {}, - "uniqueid": "00:00:00:00:00:00:00:02-00", - }, - } + "name": "CLIP temperature sensor", + "type": "CLIPTemperature", + "state": {"temperature": 2600}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, ], ) @pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: False}]) @@ -1065,7 +1063,6 @@ async def test_add_new_sensor( event_added_sensor = { "e": "added", "r": "sensors", - "id": "1", "sensor": { "id": "Light sensor id", "name": "Light level sensor", @@ -1102,14 +1099,12 @@ async def test_dont_add_sensor_if_state_is_none( sensor_property: str, ) -> None: """Test sensor with scaled data is not created if state is None.""" - sensor_payload |= { - "1": { - "name": "Sensor 1", - "type": sensor_type, - "state": {sensor_property: None}, - "config": {}, - "uniqueid": "00:00:00:00:00:00:00:00-00", - } + sensor_payload["0"] = { + "name": "Sensor 1", + "type": sensor_type, + "state": {sensor_property: None}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:00-00", } await config_entry_factory() @@ -1120,25 +1115,23 @@ async def test_dont_add_sensor_if_state_is_none( "sensor_payload", [ { - "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", - } + "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", } ], ) @@ -1350,7 +1343,7 @@ async def test_special_danfoss_battery_creation( @pytest.mark.parametrize( "sensor_payload", - [{"0": {"type": "not supported", "name": "name", "state": {}, "config": {}}}], + [{"type": "not supported", "name": "name", "state": {}, "config": {}}], ) @pytest.mark.usefixtures("config_entry_setup") async def test_unsupported_sensor(hass: HomeAssistant) -> None: diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index ec9ace90116..72d69c4559e 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -239,13 +239,11 @@ async def test_service_refresh_devices( "sensor_payload", [ { - "1": { - "name": "Switch 1", - "type": "ZHASwitch", - "state": {"buttonevent": 1000}, - "config": {"battery": 100}, - "uniqueid": "00:00:00:00:00:00:00:01-00", - } + "name": "Switch 1", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {"battery": 100}, + "uniqueid": "00:00:00:00:00:00:00:01-00", } ], ) @@ -284,7 +282,7 @@ async def test_service_refresh_devices_trigger_no_state_update( } }, "sensors": { - "1": { + "0": { "name": "Switch 1", "type": "ZHASwitch", "state": {"buttonevent": 1000}, @@ -319,13 +317,11 @@ async def test_service_refresh_devices_trigger_no_state_update( "sensor_payload", [ { - "1": { - "name": "Switch 1", - "type": "ZHASwitch", - "state": {"buttonevent": 1000, "gesture": 1}, - "config": {"battery": 100}, - "uniqueid": "00:00:00:00:00:00:00:03-00", - }, + "name": "Switch 1", + "type": "ZHASwitch", + "state": {"buttonevent": 1000, "gesture": 1}, + "config": {"battery": 100}, + "uniqueid": "00:00:00:00:00:00:00:03-00", } ], ) From 0ab1ccc5ae2f7b7dd32c8dc3b1203477e3ff7271 Mon Sep 17 00:00:00 2001 From: Marcel Vriend <92307684+marcelvriend@users.noreply.github.com> Date: Sun, 21 Jul 2024 14:08:58 +0200 Subject: [PATCH 1405/2411] Fix to prevent Azure Data Explorer JSON serialization from failing (#122300) --- homeassistant/components/azure_data_explorer/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/azure_data_explorer/__init__.py b/homeassistant/components/azure_data_explorer/__init__.py index 319f7e4389b..34f2c438d14 100644 --- a/homeassistant/components/azure_data_explorer/__init__.py +++ b/homeassistant/components/azure_data_explorer/__init__.py @@ -18,7 +18,7 @@ from homeassistant.core import Event, HomeAssistant, State from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.json import ExtendedJSONEncoder from homeassistant.helpers.typing import ConfigType from homeassistant.util.dt import utcnow @@ -203,9 +203,7 @@ class AzureDataExplorer: return None, dropped if (utcnow() - time_fired).seconds > DEFAULT_MAX_DELAY + self._send_interval: return None, dropped + 1 - if "\n" in state.state: - return None, dropped + 1 - json_event = json.dumps(obj=state, cls=JSONEncoder) + json_event = json.dumps(obj=state, cls=ExtendedJSONEncoder) return (json_event, dropped) From a8cbfe5159b48f37c7a9e09da931767bd9ac8ad8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Jul 2024 07:49:59 -0500 Subject: [PATCH 1406/2411] Make TemplateStateBase.entity_id a cached_property (#122279) --- homeassistant/helpers/template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index c21523baa38..7742418c5a7 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -10,7 +10,7 @@ from collections.abc import Callable, Generator, Iterable from contextlib import AbstractContextManager from contextvars import ContextVar from datetime import date, datetime, time, timedelta -from functools import cache, lru_cache, partial, wraps +from functools import cache, cached_property, lru_cache, partial, wraps import json import logging import math @@ -1022,7 +1022,7 @@ class TemplateStateBase(State): return self.state_with_unit raise KeyError - @property + @cached_property def entity_id(self) -> str: # type: ignore[override] """Wrap State.entity_id. From 7f82fb8cb8cfb5be7a9b7cc4beccaf0c0098776c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 21 Jul 2024 14:52:18 +0200 Subject: [PATCH 1407/2411] Bump aiomealie to 0.8.0 (#122295) * Bump aiomealie to 0.8.0 * Bump aiomealie to 0.8.0 --- homeassistant/components/mealie/manifest.json | 2 +- homeassistant/components/mealie/services.py | 6 +----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/mealie/test_services.py | 6 +----- tests/components/mealie/test_todo.py | 3 +-- 6 files changed, 6 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index d8dc827cc20..acfe30aecaa 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mealie", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["aiomealie==0.7.0"] + "requirements": ["aiomealie==0.8.0"] } diff --git a/homeassistant/components/mealie/services.py b/homeassistant/components/mealie/services.py index 7671c65b41f..d7be0885f3c 100644 --- a/homeassistant/components/mealie/services.py +++ b/homeassistant/components/mealie/services.py @@ -4,11 +4,7 @@ from dataclasses import asdict from datetime import date from typing import cast -from aiomealie.exceptions import ( - MealieConnectionError, - MealieNotFoundError, - MealieValidationError, -) +from aiomealie import MealieConnectionError, MealieNotFoundError, MealieValidationError import voluptuous as vol from homeassistant.config_entries import ConfigEntryState diff --git a/requirements_all.txt b/requirements_all.txt index b9ed2e4f192..5203599a368 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -288,7 +288,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.7.0 +aiomealie==0.8.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc8b75d9e1c..4d1562f340a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.7.0 +aiomealie==0.8.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/tests/components/mealie/test_services.py b/tests/components/mealie/test_services.py index c655d899416..7af1bc251d4 100644 --- a/tests/components/mealie/test_services.py +++ b/tests/components/mealie/test_services.py @@ -3,11 +3,7 @@ from datetime import date from unittest.mock import AsyncMock -from aiomealie.exceptions import ( - MealieConnectionError, - MealieNotFoundError, - MealieValidationError, -) +from aiomealie import MealieConnectionError, MealieNotFoundError, MealieValidationError from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion diff --git a/tests/components/mealie/test_todo.py b/tests/components/mealie/test_todo.py index 36bcaa05124..920cfc47397 100644 --- a/tests/components/mealie/test_todo.py +++ b/tests/components/mealie/test_todo.py @@ -3,8 +3,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch -from aiomealie import ShoppingListsResponse -from aiomealie.exceptions import MealieError +from aiomealie import MealieError, ShoppingListsResponse from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion From 874b1ae8739104d21774a44b7b33ebcb85f144b8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 21 Jul 2024 14:59:22 +0200 Subject: [PATCH 1408/2411] Add sensor platform to Mealie (#122280) * Bump aiomealie to 0.7.0 * Add sensor platform to Mealie * Fix --- homeassistant/components/mealie/__init__.py | 7 +- .../components/mealie/coordinator.py | 25 ++ homeassistant/components/mealie/icons.json | 17 ++ homeassistant/components/mealie/sensor.py | 94 +++++++ homeassistant/components/mealie/strings.json | 17 ++ tests/components/mealie/conftest.py | 4 + .../mealie/fixtures/statistics.json | 7 + .../mealie/snapshots/test_sensor.ambr | 251 ++++++++++++++++++ tests/components/mealie/test_sensor.py | 27 ++ 9 files changed, 447 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/mealie/sensor.py create mode 100644 tests/components/mealie/fixtures/statistics.json create mode 100644 tests/components/mealie/snapshots/test_sensor.ambr create mode 100644 tests/components/mealie/test_sensor.py diff --git a/homeassistant/components/mealie/__init__.py b/homeassistant/components/mealie/__init__.py index 87b3e3988a2..393ef1e5ecd 100644 --- a/homeassistant/components/mealie/__init__.py +++ b/homeassistant/components/mealie/__init__.py @@ -22,11 +22,12 @@ from .coordinator import ( MealieData, MealieMealplanCoordinator, MealieShoppingListCoordinator, + MealieStatisticsCoordinator, ) from .services import setup_services from .utils import create_version -PLATFORMS: list[Platform] = [Platform.CALENDAR, Platform.TODO] +PLATFORMS: list[Platform] = [Platform.CALENDAR, Platform.SENSOR, Platform.TODO] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) @@ -75,12 +76,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo mealplan_coordinator = MealieMealplanCoordinator(hass, client) shoppinglist_coordinator = MealieShoppingListCoordinator(hass, client) + statistics_coordinator = MealieStatisticsCoordinator(hass, client) await mealplan_coordinator.async_config_entry_first_refresh() await shoppinglist_coordinator.async_config_entry_first_refresh() + await statistics_coordinator.async_config_entry_first_refresh() entry.runtime_data = MealieData( - client, mealplan_coordinator, shoppinglist_coordinator + client, mealplan_coordinator, shoppinglist_coordinator, statistics_coordinator ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/mealie/coordinator.py b/homeassistant/components/mealie/coordinator.py index bb97b3c26a3..a4507c88985 100644 --- a/homeassistant/components/mealie/coordinator.py +++ b/homeassistant/components/mealie/coordinator.py @@ -13,6 +13,7 @@ from aiomealie import ( MealplanEntryType, ShoppingItem, ShoppingList, + Statistics, ) from homeassistant.config_entries import ConfigEntry @@ -33,6 +34,7 @@ class MealieData: client: MealieClient mealplan_coordinator: MealieMealplanCoordinator shoppinglist_coordinator: MealieShoppingListCoordinator + statistics_coordinator: MealieStatisticsCoordinator type MealieConfigEntry = ConfigEntry[MealieData] @@ -139,3 +141,26 @@ class MealieShoppingListCoordinator( except MealieConnectionError as error: raise UpdateFailed(error) from error return shopping_list_items + + +class MealieStatisticsCoordinator(MealieDataUpdateCoordinator[Statistics]): + """Class to manage fetching Mealie Statistics data.""" + + def __init__(self, hass: HomeAssistant, client: MealieClient) -> None: + """Initialize coordinator.""" + super().__init__( + hass, + name="MealieStatistics", + client=client, + update_interval=timedelta(minutes=15), + ) + + async def _async_update_data( + self, + ) -> Statistics: + try: + return await self.client.get_statistics() + except MealieAuthenticationError as error: + raise ConfigEntryAuthFailed from error + except MealieConnectionError as error: + raise UpdateFailed(error) from error diff --git a/homeassistant/components/mealie/icons.json b/homeassistant/components/mealie/icons.json index 87aefc3d91f..f509985eb72 100644 --- a/homeassistant/components/mealie/icons.json +++ b/homeassistant/components/mealie/icons.json @@ -4,6 +4,23 @@ "shopping_list": { "default": "mdi:basket" } + }, + "sensor": { + "recipes": { + "default": "mdi:food" + }, + "users": { + "default": "mdi:account-multiple" + }, + "categories": { + "default": "mdi:shape" + }, + "tags": { + "default": "mdi:tag-multiple" + }, + "tools": { + "default": "mdi:tools" + } } }, "services": { diff --git a/homeassistant/components/mealie/sensor.py b/homeassistant/components/mealie/sensor.py new file mode 100644 index 00000000000..b4baac34ebe --- /dev/null +++ b/homeassistant/components/mealie/sensor.py @@ -0,0 +1,94 @@ +"""Support for Mealie sensors.""" + +from collections.abc import Callable +from dataclasses import dataclass + +from aiomealie import Statistics + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .coordinator import MealieConfigEntry, MealieStatisticsCoordinator +from .entity import MealieEntity + + +@dataclass(frozen=True, kw_only=True) +class MealieStatisticsSensorEntityDescription(SensorEntityDescription): + """Describes Mealie Statistics sensor entity.""" + + value_fn: Callable[[Statistics], StateType] + + +SENSOR_TYPES: tuple[MealieStatisticsSensorEntityDescription, ...] = ( + MealieStatisticsSensorEntityDescription( + key="recipes", + native_unit_of_measurement="recipes", + state_class=SensorStateClass.TOTAL, + value_fn=lambda statistics: statistics.total_recipes, + ), + MealieStatisticsSensorEntityDescription( + key="users", + native_unit_of_measurement="users", + state_class=SensorStateClass.TOTAL, + value_fn=lambda statistics: statistics.total_users, + ), + MealieStatisticsSensorEntityDescription( + key="categories", + native_unit_of_measurement="categories", + state_class=SensorStateClass.TOTAL, + value_fn=lambda statistics: statistics.total_categories, + ), + MealieStatisticsSensorEntityDescription( + key="tags", + native_unit_of_measurement="tags", + state_class=SensorStateClass.TOTAL, + value_fn=lambda statistics: statistics.total_tags, + ), + MealieStatisticsSensorEntityDescription( + key="tools", + native_unit_of_measurement="tools", + state_class=SensorStateClass.TOTAL, + value_fn=lambda statistics: statistics.total_tools, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: MealieConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Mealie sensors based on a config entry.""" + coordinator = entry.runtime_data.statistics_coordinator + + async_add_entities( + MealieStatisticSensors(coordinator, description) for description in SENSOR_TYPES + ) + + +class MealieStatisticSensors(MealieEntity, SensorEntity): + """Defines a Mealie sensor.""" + + entity_description: MealieStatisticsSensorEntityDescription + coordinator: MealieStatisticsCoordinator + + def __init__( + self, + coordinator: MealieStatisticsCoordinator, + description: MealieStatisticsSensorEntityDescription, + ) -> None: + """Initialize Mealie sensor.""" + super().__init__(coordinator, description.key) + self.entity_description = description + self._attr_translation_key = description.key + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json index a0b0dcbfc4f..7e1b307d18b 100644 --- a/homeassistant/components/mealie/strings.json +++ b/homeassistant/components/mealie/strings.json @@ -53,6 +53,23 @@ "side": { "name": "Side" } + }, + "sensor": { + "recipes": { + "name": "Recipes" + }, + "users": { + "name": "Users" + }, + "categories": { + "name": "Categories" + }, + "tags": { + "name": "Tags" + }, + "tools": { + "name": "Tools" + } } }, "exceptions": { diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py index be9f939267a..2916159a799 100644 --- a/tests/components/mealie/conftest.py +++ b/tests/components/mealie/conftest.py @@ -10,6 +10,7 @@ from aiomealie import ( Recipe, ShoppingItemsResponse, ShoppingListsResponse, + Statistics, UserInfo, ) from mashumaro.codecs.orjson import ORJSONDecoder @@ -70,6 +71,9 @@ def mock_mealie_client() -> Generator[AsyncMock]: client.get_shopping_items.return_value = ShoppingItemsResponse.from_json( load_fixture("get_shopping_items.json", DOMAIN) ) + client.get_statistics.return_value = Statistics.from_json( + load_fixture("statistics.json", DOMAIN) + ) yield client diff --git a/tests/components/mealie/fixtures/statistics.json b/tests/components/mealie/fixtures/statistics.json new file mode 100644 index 00000000000..350bf1fd9ff --- /dev/null +++ b/tests/components/mealie/fixtures/statistics.json @@ -0,0 +1,7 @@ +{ + "totalRecipes": 765, + "totalUsers": 3, + "totalCategories": 24, + "totalTags": 454, + "totalTools": 11 +} diff --git a/tests/components/mealie/snapshots/test_sensor.ambr b/tests/components/mealie/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..e645cf4c45f --- /dev/null +++ b/tests/components/mealie/snapshots/test_sensor.ambr @@ -0,0 +1,251 @@ +# serializer version: 1 +# name: test_entities[sensor.mealie_categories-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mealie_categories', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Categories', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'categories', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_categories', + 'unit_of_measurement': 'categories', + }) +# --- +# name: test_entities[sensor.mealie_categories-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mealie Categories', + 'state_class': , + 'unit_of_measurement': 'categories', + }), + 'context': , + 'entity_id': 'sensor.mealie_categories', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24', + }) +# --- +# name: test_entities[sensor.mealie_recipes-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mealie_recipes', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Recipes', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'recipes', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_recipes', + 'unit_of_measurement': 'recipes', + }) +# --- +# name: test_entities[sensor.mealie_recipes-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mealie Recipes', + 'state_class': , + 'unit_of_measurement': 'recipes', + }), + 'context': , + 'entity_id': 'sensor.mealie_recipes', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '765', + }) +# --- +# name: test_entities[sensor.mealie_tags-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mealie_tags', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tags', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tags', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_tags', + 'unit_of_measurement': 'tags', + }) +# --- +# name: test_entities[sensor.mealie_tags-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mealie Tags', + 'state_class': , + 'unit_of_measurement': 'tags', + }), + 'context': , + 'entity_id': 'sensor.mealie_tags', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '454', + }) +# --- +# name: test_entities[sensor.mealie_tools-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mealie_tools', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tools', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tools', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_tools', + 'unit_of_measurement': 'tools', + }) +# --- +# name: test_entities[sensor.mealie_tools-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mealie Tools', + 'state_class': , + 'unit_of_measurement': 'tools', + }), + 'context': , + 'entity_id': 'sensor.mealie_tools', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11', + }) +# --- +# name: test_entities[sensor.mealie_users-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mealie_users', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Users', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'users', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_users', + 'unit_of_measurement': 'users', + }) +# --- +# name: test_entities[sensor.mealie_users-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mealie Users', + 'state_class': , + 'unit_of_measurement': 'users', + }), + 'context': , + 'entity_id': 'sensor.mealie_users', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- diff --git a/tests/components/mealie/test_sensor.py b/tests/components/mealie/test_sensor.py new file mode 100644 index 00000000000..5a55b89ad21 --- /dev/null +++ b/tests/components/mealie/test_sensor.py @@ -0,0 +1,27 @@ +"""Tests for the Mealie sensors.""" + +from unittest.mock import AsyncMock, patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the sensor entities.""" + with patch("homeassistant.components.mealie.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From 8994c18f730dd050ac002177bb8e85128043f9ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Jul 2024 08:19:33 -0500 Subject: [PATCH 1409/2411] Update xiaomi-ble to use entry.runtime_data (#122306) --- .../components/xiaomi_ble/__init__.py | 62 ++++++++----------- .../components/xiaomi_ble/binary_sensor.py | 14 ++--- .../components/xiaomi_ble/coordinator.py | 4 +- homeassistant/components/xiaomi_ble/event.py | 9 +-- homeassistant/components/xiaomi_ble/sensor.py | 14 ++--- homeassistant/components/xiaomi_ble/types.py | 10 +++ 6 files changed, 49 insertions(+), 64 deletions(-) create mode 100644 homeassistant/components/xiaomi_ble/types.py diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 4a9753bfe85..fae5e4d0c91 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -2,12 +2,12 @@ from __future__ import annotations +from functools import partial import logging from typing import cast from xiaomi_ble import EncryptionScheme, SensorUpdate, XiaomiBluetoothDeviceData -from homeassistant import config_entries from homeassistant.components.bluetooth import ( DOMAIN as BLUETOOTH_DOMAIN, BluetoothScanningMode, @@ -29,6 +29,7 @@ from .const import ( XiaomiBleEvent, ) from .coordinator import XiaomiActiveBluetoothProcessorCoordinator +from .types import XiaomiBLEConfigEntry PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.EVENT, Platform.SENSOR] @@ -37,16 +38,14 @@ _LOGGER = logging.getLogger(__name__) def process_service_info( hass: HomeAssistant, - entry: config_entries.ConfigEntry, - data: XiaomiBluetoothDeviceData, - service_info: BluetoothServiceInfoBleak, + entry: XiaomiBLEConfigEntry, device_registry: DeviceRegistry, + service_info: BluetoothServiceInfoBleak, ) -> SensorUpdate: """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" + coordinator = entry.runtime_data + data = coordinator.device_data update = data.update(service_info) - coordinator: XiaomiActiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ - entry.entry_id - ] discovered_event_classes = coordinator.discovered_event_classes if entry.data.get(CONF_SLEEPY_DEVICE, False) != data.sleepy_device: hass.config_entries.async_update_entry( @@ -165,38 +164,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await data.async_poll(connectable_device) device_registry = dr.async_get(hass) - coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( - XiaomiActiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=lambda service_info: process_service_info( - hass, entry, data, service_info, device_registry - ), - needs_poll_method=_needs_poll, - device_data=data, - discovered_event_classes=set( - entry.data.get(CONF_DISCOVERED_EVENT_CLASSES, []) - ), - poll_method=_async_poll, - # We will take advertisements from non-connectable devices - # since we will trade the BLEDevice for a connectable one - # if we need to poll it - connectable=False, - entry=entry, - ) + coordinator = XiaomiActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=partial(process_service_info, hass, entry, device_registry), + needs_poll_method=_needs_poll, + device_data=data, + discovered_event_classes=set(entry.data.get(CONF_DISCOVERED_EVENT_CLASSES, [])), + poll_method=_async_poll, + # We will take advertisements from non-connectable devices + # since we will trade the BLEDevice for a connectable one + # if we need to poll it + connectable=False, + entry=entry, ) + entry.runtime_data = coordinator 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 + # only start after all platforms have had a chance to subscribe + entry.async_on_unload(coordinator.async_start()) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: XiaomiBLEConfigEntry) -> 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 + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 8734f45c405..5336c4d8f7f 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -8,7 +8,6 @@ from xiaomi_ble.parser import ( SensorUpdate, ) -from homeassistant import config_entries from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -22,12 +21,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info -from .const import DOMAIN -from .coordinator import ( - XiaomiActiveBluetoothProcessorCoordinator, - XiaomiPassiveBluetoothDataProcessor, -) +from .coordinator import XiaomiPassiveBluetoothDataProcessor from .device import device_key_to_bluetooth_entity_key +from .types import XiaomiBLEConfigEntry BINARY_SENSOR_DESCRIPTIONS = { XiaomiBinarySensorDeviceClass.BATTERY: BinarySensorEntityDescription( @@ -134,13 +130,11 @@ def sensor_update_to_bluetooth_data_update( async def async_setup_entry( hass: HomeAssistant, - entry: config_entries.ConfigEntry, + entry: XiaomiBLEConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Xiaomi BLE sensors.""" - coordinator: XiaomiActiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ - entry.entry_id - ] + coordinator = entry.runtime_data processor = XiaomiPassiveBluetoothDataProcessor( sensor_update_to_bluetooth_data_update ) diff --git a/homeassistant/components/xiaomi_ble/coordinator.py b/homeassistant/components/xiaomi_ble/coordinator.py index 1cd49e851ea..69fc427013a 100644 --- a/homeassistant/components/xiaomi_ble/coordinator.py +++ b/homeassistant/components/xiaomi_ble/coordinator.py @@ -16,11 +16,11 @@ from homeassistant.components.bluetooth.active_update_processor import ( from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer from .const import CONF_SLEEPY_DEVICE +from .types import XiaomiBLEConfigEntry class XiaomiActiveBluetoothProcessorCoordinator( @@ -45,7 +45,7 @@ class XiaomiActiveBluetoothProcessorCoordinator( ] | None = None, poll_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None, - entry: ConfigEntry, + entry: XiaomiBLEConfigEntry, connectable: bool = True, ) -> None: """Initialize the Xiaomi Bluetooth Active Update Processor Coordinator.""" diff --git a/homeassistant/components/xiaomi_ble/event.py b/homeassistant/components/xiaomi_ble/event.py index e39a4adb3c7..7265bcd112c 100644 --- a/homeassistant/components/xiaomi_ble/event.py +++ b/homeassistant/components/xiaomi_ble/event.py @@ -9,7 +9,6 @@ from homeassistant.components.event import ( EventEntity, EventEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -29,7 +28,7 @@ from .const import ( EVENT_TYPE, XiaomiBleEvent, ) -from .coordinator import XiaomiActiveBluetoothProcessorCoordinator +from .types import XiaomiBLEConfigEntry DESCRIPTIONS_BY_EVENT_CLASS = { EVENT_CLASS_BUTTON: EventEntityDescription( @@ -183,13 +182,11 @@ class XiaomiEventEntity(EventEntity): async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: XiaomiBLEConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Xiaomi event.""" - coordinator: XiaomiActiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ - entry.entry_id - ] + coordinator = entry.runtime_data address = coordinator.address ent_reg = er.async_get(hass) async_add_entities( diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 65b33c3c559..3108c285dbe 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -7,7 +7,6 @@ from typing import cast from xiaomi_ble import DeviceClass, SensorUpdate, Units from xiaomi_ble.parser import ExtendedSensorDeviceClass -from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataUpdate, PassiveBluetoothProcessorEntity, @@ -35,12 +34,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info -from .const import DOMAIN -from .coordinator import ( - XiaomiActiveBluetoothProcessorCoordinator, - XiaomiPassiveBluetoothDataProcessor, -) +from .coordinator import XiaomiPassiveBluetoothDataProcessor from .device import device_key_to_bluetooth_entity_key +from .types import XiaomiBLEConfigEntry SENSOR_DESCRIPTIONS = { (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( @@ -193,13 +189,11 @@ def sensor_update_to_bluetooth_data_update( async def async_setup_entry( hass: HomeAssistant, - entry: config_entries.ConfigEntry, + entry: XiaomiBLEConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Xiaomi BLE sensors.""" - coordinator: XiaomiActiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ - entry.entry_id - ] + coordinator = entry.runtime_data processor = XiaomiPassiveBluetoothDataProcessor( sensor_update_to_bluetooth_data_update ) diff --git a/homeassistant/components/xiaomi_ble/types.py b/homeassistant/components/xiaomi_ble/types.py new file mode 100644 index 00000000000..f0de8af9d06 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/types.py @@ -0,0 +1,10 @@ +"""Support for xiaomi ble.""" + +from typing import TYPE_CHECKING + +from homeassistant.config_entries import ConfigEntry + +if TYPE_CHECKING: + from .coordinator import XiaomiActiveBluetoothProcessorCoordinator + +type XiaomiBLEConfigEntry = ConfigEntry[XiaomiActiveBluetoothProcessorCoordinator] From 7f852d0f738c570bfc5a5ec736dbb9550e4c6f54 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Jul 2024 08:19:46 -0500 Subject: [PATCH 1410/2411] Update bthome to use entry.runtime_data (#122304) --- homeassistant/components/bthome/__init__.py | 55 ++++++++----------- .../components/bthome/binary_sensor.py | 14 ++--- .../components/bthome/coordinator.py | 4 +- homeassistant/components/bthome/event.py | 9 +-- homeassistant/components/bthome/sensor.py | 14 ++--- homeassistant/components/bthome/types.py | 10 ++++ 6 files changed, 46 insertions(+), 60 deletions(-) create mode 100644 homeassistant/components/bthome/types.py diff --git a/homeassistant/components/bthome/__init__.py b/homeassistant/components/bthome/__init__.py index 6f17adeeca7..f5e634b774d 100644 --- a/homeassistant/components/bthome/__init__.py +++ b/homeassistant/components/bthome/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial import logging from bthome_ble import BTHomeBluetoothDeviceData, SensorUpdate @@ -12,7 +13,6 @@ from homeassistant.components.bluetooth import ( BluetoothScanningMode, BluetoothServiceInfoBleak, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -29,6 +29,7 @@ from .const import ( BTHomeBleEvent, ) from .coordinator import BTHomePassiveBluetoothProcessorCoordinator +from .types import BTHomeConfigEntry PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.EVENT, Platform.SENSOR] @@ -37,16 +38,14 @@ _LOGGER = logging.getLogger(__name__) def process_service_info( hass: HomeAssistant, - entry: ConfigEntry, - data: BTHomeBluetoothDeviceData, - service_info: BluetoothServiceInfoBleak, + entry: BTHomeConfigEntry, device_registry: DeviceRegistry, + service_info: BluetoothServiceInfoBleak, ) -> SensorUpdate: """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" + coordinator = entry.runtime_data + data = coordinator.device_data update = data.update(service_info) - coordinator: BTHomePassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ - entry.entry_id - ] discovered_event_classes = coordinator.discovered_event_classes if entry.data.get(CONF_SLEEPY_DEVICE, False) != data.sleepy_device: hass.config_entries.async_update_entry( @@ -117,7 +116,7 @@ def format_discovered_event_class(address: str) -> SignalType[str, BTHomeBleEven return SignalType(f"{DOMAIN}_discovered_event_class_{address}") -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: BTHomeConfigEntry) -> bool: """Set up BTHome Bluetooth from a config entry.""" address = entry.unique_id assert address is not None @@ -128,34 +127,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = BTHomeBluetoothDeviceData(**kwargs) device_registry = dr.async_get(hass) - coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( - BTHomePassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=lambda service_info: process_service_info( - hass, entry, data, service_info, device_registry - ), - device_data=data, - discovered_event_classes=set( - entry.data.get(CONF_DISCOVERED_EVENT_CLASSES, []) - ), - connectable=False, - entry=entry, - ) + event_classes = set(entry.data.get(CONF_DISCOVERED_EVENT_CLASSES, ())) + coordinator = BTHomePassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=partial(process_service_info, hass, entry, device_registry), + device_data=data, + discovered_event_classes=event_classes, + connectable=False, + entry=entry, ) + entry.runtime_data = coordinator 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 + # only start after all platforms have had a chance to subscribe + entry.async_on_unload(coordinator.async_start()) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: BTHomeConfigEntry) -> 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 + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/bthome/binary_sensor.py b/homeassistant/components/bthome/binary_sensor.py index 1a311f9f3a4..bcc420df4a8 100644 --- a/homeassistant/components/bthome/binary_sensor.py +++ b/homeassistant/components/bthome/binary_sensor.py @@ -7,7 +7,6 @@ from bthome_ble import ( SensorUpdate, ) -from homeassistant import config_entries from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -21,12 +20,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info -from .const import DOMAIN -from .coordinator import ( - BTHomePassiveBluetoothDataProcessor, - BTHomePassiveBluetoothProcessorCoordinator, -) +from .coordinator import BTHomePassiveBluetoothDataProcessor from .device import device_key_to_bluetooth_entity_key +from .types import BTHomeConfigEntry BINARY_SENSOR_DESCRIPTIONS = { BTHomeBinarySensorDeviceClass.BATTERY: BinarySensorEntityDescription( @@ -172,13 +168,11 @@ def sensor_update_to_bluetooth_data_update( async def async_setup_entry( hass: HomeAssistant, - entry: config_entries.ConfigEntry, + entry: BTHomeConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the BTHome BLE binary sensors.""" - coordinator: BTHomePassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ - entry.entry_id - ] + coordinator = entry.runtime_data processor = BTHomePassiveBluetoothDataProcessor( sensor_update_to_bluetooth_data_update ) diff --git a/homeassistant/components/bthome/coordinator.py b/homeassistant/components/bthome/coordinator.py index cb2abef6a43..2ef29541f40 100644 --- a/homeassistant/components/bthome/coordinator.py +++ b/homeassistant/components/bthome/coordinator.py @@ -13,10 +13,10 @@ from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothProcessorCoordinator, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import CONF_SLEEPY_DEVICE +from .types import BTHomeConfigEntry class BTHomePassiveBluetoothProcessorCoordinator( @@ -33,7 +33,7 @@ class BTHomePassiveBluetoothProcessorCoordinator( update_method: Callable[[BluetoothServiceInfoBleak], SensorUpdate], device_data: BTHomeBluetoothDeviceData, discovered_event_classes: set[str], - entry: ConfigEntry, + entry: BTHomeConfigEntry, connectable: bool = False, ) -> None: """Initialize the BTHome Bluetooth Passive Update Processor Coordinator.""" diff --git a/homeassistant/components/bthome/event.py b/homeassistant/components/bthome/event.py index a0f59c0ddb7..128d1e8388f 100644 --- a/homeassistant/components/bthome/event.py +++ b/homeassistant/components/bthome/event.py @@ -9,7 +9,6 @@ from homeassistant.components.event import ( EventEntity, EventEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -24,7 +23,7 @@ from .const import ( EVENT_TYPE, BTHomeBleEvent, ) -from .coordinator import BTHomePassiveBluetoothProcessorCoordinator +from .types import BTHomeConfigEntry DESCRIPTIONS_BY_EVENT_CLASS = { EVENT_CLASS_BUTTON: EventEntityDescription( @@ -103,13 +102,11 @@ class BTHomeEventEntity(EventEntity): async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: BTHomeConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up BTHome event.""" - coordinator: BTHomePassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ - entry.entry_id - ] + coordinator = entry.runtime_data address = coordinator.address ent_reg = er.async_get(hass) async_add_entities( diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 2178481b21a..656addad620 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -9,7 +9,6 @@ from bthome_ble.const import ( ExtendedSensorDeviceClass as BTHomeExtendedSensorDeviceClass, ) -from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataUpdate, PassiveBluetoothProcessorEntity, @@ -45,12 +44,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info -from .const import DOMAIN -from .coordinator import ( - BTHomePassiveBluetoothDataProcessor, - BTHomePassiveBluetoothProcessorCoordinator, -) +from .coordinator import BTHomePassiveBluetoothDataProcessor from .device import device_key_to_bluetooth_entity_key +from .types import BTHomeConfigEntry SENSOR_DESCRIPTIONS = { # Acceleration (m/s²) @@ -394,13 +390,11 @@ def sensor_update_to_bluetooth_data_update( async def async_setup_entry( hass: HomeAssistant, - entry: config_entries.ConfigEntry, + entry: BTHomeConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the BTHome BLE sensors.""" - coordinator: BTHomePassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ - entry.entry_id - ] + coordinator = entry.runtime_data processor = BTHomePassiveBluetoothDataProcessor( sensor_update_to_bluetooth_data_update ) diff --git a/homeassistant/components/bthome/types.py b/homeassistant/components/bthome/types.py new file mode 100644 index 00000000000..f89caa22b10 --- /dev/null +++ b/homeassistant/components/bthome/types.py @@ -0,0 +1,10 @@ +"""The BTHome Bluetooth integration.""" + +from typing import TYPE_CHECKING + +from homeassistant.config_entries import ConfigEntry + +if TYPE_CHECKING: + from .coordinator import BTHomePassiveBluetoothProcessorCoordinator + +type BTHomeConfigEntry = ConfigEntry[BTHomePassiveBluetoothProcessorCoordinator] From 272f0bc21cf2e8d35665923d6c05b3dbd0cda0d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Jul 2024 08:19:58 -0500 Subject: [PATCH 1411/2411] Migrate oncue to use entry.runtime_data (#122307) --- homeassistant/components/oncue/__init__.py | 16 +++++----- .../components/oncue/binary_sensor.py | 31 ++++++------------- homeassistant/components/oncue/sensor.py | 24 ++++++-------- homeassistant/components/oncue/types.py | 10 ++++++ 4 files changed, 36 insertions(+), 45 deletions(-) create mode 100644 homeassistant/components/oncue/types.py diff --git a/homeassistant/components/oncue/__init__.py b/homeassistant/components/oncue/__init__.py index f960b1a8b81..53443b9ed81 100644 --- a/homeassistant/components/oncue/__init__.py +++ b/homeassistant/components/oncue/__init__.py @@ -7,21 +7,21 @@ import logging from aiooncue import LoginFailedException, Oncue, OncueDevice -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import CONNECTION_EXCEPTIONS, DOMAIN +from .const import CONNECTION_EXCEPTIONS, DOMAIN # noqa: F401 +from .types import OncueConfigEntry PLATFORMS: list[str] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: OncueConfigEntry) -> bool: """Set up Oncue from a config entry.""" data = entry.data websession = async_get_clientsession(hass) @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except LoginFailedException as ex: raise ConfigEntryAuthFailed from ex - coordinator = DataUpdateCoordinator( + coordinator = DataUpdateCoordinator[dict[str, OncueDevice]]( hass, _LOGGER, name=f"Oncue {entry.data[CONF_USERNAME]}", @@ -50,13 +50,11 @@ 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 + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: OncueConfigEntry) -> 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 + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/oncue/binary_sensor.py b/homeassistant/components/oncue/binary_sensor.py index 8adf422d656..961b082a5c5 100644 --- a/homeassistant/components/oncue/binary_sensor.py +++ b/homeassistant/components/oncue/binary_sensor.py @@ -2,21 +2,17 @@ from __future__ import annotations -from aiooncue import OncueDevice - from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN from .entity import OncueEntity +from .types import OncueConfigEntry SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( @@ -31,25 +27,18 @@ SENSOR_MAP = {description.key: description for description in SENSOR_TYPES} async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: OncueConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up sensors.""" - coordinator: DataUpdateCoordinator[dict[str, OncueDevice]] = hass.data[DOMAIN][ - config_entry.entry_id - ] - entities: list[OncueBinarySensorEntity] = [] + """Set up binary sensors.""" + coordinator = config_entry.runtime_data devices = coordinator.data - for device_id, device in devices.items(): - entities.extend( - OncueBinarySensorEntity( - coordinator, device_id, device, sensor, SENSOR_MAP[key] - ) - for key, sensor in device.sensors.items() - if key in SENSOR_MAP - ) - - async_add_entities(entities) + async_add_entities( + OncueBinarySensorEntity(coordinator, device_id, device, sensor, SENSOR_MAP[key]) + for device_id, device in devices.items() + for key, sensor in device.sensors.items() + if key in SENSOR_MAP + ) class OncueBinarySensorEntity(OncueEntity, BinarySensorEntity): diff --git a/homeassistant/components/oncue/sensor.py b/homeassistant/components/oncue/sensor.py index f79beed38b2..a0f275ef692 100644 --- a/homeassistant/components/oncue/sensor.py +++ b/homeassistant/components/oncue/sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, EntityCategory, @@ -26,8 +25,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN from .entity import OncueEntity +from .types import OncueConfigEntry SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -180,23 +179,18 @@ UNIT_MAPPINGS = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: OncueConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors.""" - coordinator: DataUpdateCoordinator[dict[str, OncueDevice]] = hass.data[DOMAIN][ - config_entry.entry_id - ] - entities: list[OncueSensorEntity] = [] + coordinator = config_entry.runtime_data devices = coordinator.data - for device_id, device in devices.items(): - entities.extend( - OncueSensorEntity(coordinator, device_id, device, sensor, SENSOR_MAP[key]) - for key, sensor in device.sensors.items() - if key in SENSOR_MAP - ) - - async_add_entities(entities) + async_add_entities( + OncueSensorEntity(coordinator, device_id, device, sensor, SENSOR_MAP[key]) + for device_id, device in devices.items() + for key, sensor in device.sensors.items() + if key in SENSOR_MAP + ) class OncueSensorEntity(OncueEntity, SensorEntity): diff --git a/homeassistant/components/oncue/types.py b/homeassistant/components/oncue/types.py new file mode 100644 index 00000000000..89dd7095d59 --- /dev/null +++ b/homeassistant/components/oncue/types.py @@ -0,0 +1,10 @@ +"""Support for Oncue types.""" + +from __future__ import annotations + +from aiooncue import OncueDevice + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +type OncueConfigEntry = ConfigEntry[DataUpdateCoordinator[dict[str, OncueDevice]]] From 30373a668cf3c6637cb08b8850979c9b76cd15b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Jul 2024 09:06:51 -0500 Subject: [PATCH 1412/2411] Migrate harmony to use entry.runtime_data (#122312) --- homeassistant/components/harmony/__init__.py | 43 ++++++------------- .../components/harmony/config_flow.py | 8 ++-- homeassistant/components/harmony/const.py | 5 --- homeassistant/components/harmony/data.py | 4 ++ homeassistant/components/harmony/remote.py | 11 +++-- homeassistant/components/harmony/select.py | 12 +++--- 6 files changed, 31 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index a1c513a4654..12f7d903f0d 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -3,26 +3,18 @@ import logging from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import ( - CANCEL_LISTENER, - CANCEL_STOP, - DOMAIN, - HARMONY_DATA, - HARMONY_OPTIONS_UPDATE, - PLATFORMS, -) -from .data import HarmonyData +from .const import DOMAIN, HARMONY_OPTIONS_UPDATE, PLATFORMS # noqa: F401 +from .data import HarmonyConfigEntry, HarmonyData _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: HarmonyConfigEntry) -> bool: """Set up Logitech Harmony Hub from a config entry.""" # As there currently is no way to import options from yaml # when setting up a config entry, we fallback to adding @@ -37,19 +29,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await _migrate_old_unique_ids(hass, entry.entry_id, data) - cancel_listener = entry.add_update_listener(_update_listener) + entry.async_on_unload(entry.add_update_listener(_update_listener)) async def _async_on_stop(event: Event) -> None: await data.shutdown() - cancel_stop = hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_on_stop) - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - HARMONY_DATA: data, - CANCEL_LISTENER: cancel_listener, - CANCEL_STOP: cancel_stop, - } + entry.async_on_unload( + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_on_stop) + ) + entry.runtime_data = data await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -84,7 +73,7 @@ async def _migrate_old_unique_ids( @callback def _async_import_options_from_data_if_missing( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: HarmonyConfigEntry ) -> None: options = dict(entry.options) modified = 0 @@ -97,24 +86,16 @@ def _async_import_options_from_data_if_missing( hass.config_entries.async_update_entry(entry, options=options) -async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def _update_listener(hass: HomeAssistant, entry: HarmonyConfigEntry) -> None: """Handle options update.""" async_dispatcher_send( hass, f"{HARMONY_OPTIONS_UPDATE}-{entry.unique_id}", entry.options ) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: HarmonyConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - # Shutdown a harmony remote for removal - entry_data = hass.data[DOMAIN][entry.entry_id] - entry_data[CANCEL_LISTENER]() - entry_data[CANCEL_STOP]() - await entry_data[HARMONY_DATA].shutdown() - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - + await entry.runtime_data.shutdown() return unload_ok diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 629c54a3571..87eb657a0a9 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -27,7 +27,8 @@ from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from .const import DOMAIN, HARMONY_DATA, PREVIOUS_ACTIVE_ACTIVITY, UNIQUE_ID +from .const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY, UNIQUE_ID +from .data import HarmonyConfigEntry from .util import ( find_best_name_for_remote, find_unique_id_for_remote, @@ -185,7 +186,7 @@ def _options_from_user_input(user_input: dict[str, Any]) -> dict[str, Any]: class OptionsFlowHandler(OptionsFlow): """Handle a option flow for Harmony.""" - def __init__(self, config_entry: ConfigEntry) -> None: + def __init__(self, config_entry: HarmonyConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry @@ -196,8 +197,7 @@ class OptionsFlowHandler(OptionsFlow): if user_input is not None: return self.async_create_entry(title="", data=user_input) - remote = self.hass.data[DOMAIN][self.config_entry.entry_id][HARMONY_DATA] - + remote = self.config_entry.runtime_data data_schema = vol.Schema( { vol.Optional( diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py index f474783b736..3b8f0e40fe3 100644 --- a/homeassistant/components/harmony/const.py +++ b/homeassistant/components/harmony/const.py @@ -13,8 +13,3 @@ ATTR_DEVICES_LIST = "devices_list" ATTR_LAST_ACTIVITY = "last_activity" ATTR_ACTIVITY_STARTING = "activity_starting" PREVIOUS_ACTIVE_ACTIVITY = "Previous Active Activity" - - -HARMONY_DATA = "harmony_data" -CANCEL_LISTENER = "cancel_listener" -CANCEL_STOP = "cancel_stop" diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index cdb31b4388c..41c55bfc855 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -9,6 +9,7 @@ from aioharmony.const import ClientCallbackType, SendCommandDevice import aioharmony.exceptions as aioexc from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import DeviceInfo @@ -19,6 +20,9 @@ from .subscriber import HarmonySubscriberMixin _LOGGER = logging.getLogger(__name__) +type HarmonyConfigEntry = ConfigEntry[HarmonyData] + + class HarmonyData(HarmonySubscriberMixin): """HarmonyData registers for Harmony hub updates.""" diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index a52f298dc41..d30aa475944 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -19,7 +19,6 @@ from homeassistant.components.remote import ( RemoteEntity, RemoteEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HassJob, HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv @@ -34,13 +33,12 @@ from .const import ( ATTR_DEVICES_LIST, ATTR_LAST_ACTIVITY, DOMAIN, - HARMONY_DATA, HARMONY_OPTIONS_UPDATE, PREVIOUS_ACTIVE_ACTIVITY, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC, ) -from .data import HarmonyData +from .data import HarmonyConfigEntry, HarmonyData from .entity import HarmonyEntity from .subscriber import HarmonyCallback @@ -57,11 +55,12 @@ HARMONY_CHANGE_CHANNEL_SCHEMA: VolDictType = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: HarmonyConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Harmony config entry.""" - - data: HarmonyData = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] + data = entry.runtime_data _LOGGER.debug("HarmonyData : %s", data) diff --git a/homeassistant/components/harmony/select.py b/homeassistant/components/harmony/select.py index 0bb8f462419..731b6836386 100644 --- a/homeassistant/components/harmony/select.py +++ b/homeassistant/components/harmony/select.py @@ -5,12 +5,11 @@ from __future__ import annotations import logging from homeassistant.components.select import SelectEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HassJob, HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ACTIVITY_POWER_OFF, DOMAIN, HARMONY_DATA -from .data import HarmonyData +from .const import ACTIVITY_POWER_OFF, DOMAIN +from .data import HarmonyConfigEntry, HarmonyData from .entity import HarmonyEntity from .subscriber import HarmonyCallback @@ -20,11 +19,12 @@ TRANSLATABLE_POWER_OFF = "power_off" async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: HarmonyConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up harmony activities select.""" - data: HarmonyData = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] - async_add_entities([HarmonyActivitySelect(data)]) + async_add_entities([HarmonyActivitySelect(entry.runtime_data)]) class HarmonyActivitySelect(HarmonyEntity, SelectEntity): From b0a4140b4d835c5a8a2552f8de5fc8914d262772 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Jul 2024 09:11:18 -0500 Subject: [PATCH 1413/2411] Convert sensorpush to use entry.runtime_data (#122315) --- .../components/sensorpush/__init__.py | 32 ++++++++----------- homeassistant/components/sensorpush/sensor.py | 10 ++---- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py index 1a479caacf2..c15dafb01d6 100644 --- a/homeassistant/components/sensorpush/__init__.py +++ b/homeassistant/components/sensorpush/__init__.py @@ -14,37 +14,31 @@ 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__) +type SensorPushConfigEntry = ConfigEntry[PassiveBluetoothProcessorCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: SensorPushConfigEntry) -> bool: """Set up SensorPush BLE device from a config entry.""" address = entry.unique_id assert address is not None - data = SensorPushBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( - PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=data.update, - ) + coordinator = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=SensorPushBluetoothDeviceData().update, ) + entry.runtime_data = coordinator 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 + # only start after all platforms have had a chance to subscribe + entry.async_on_unload(coordinator.async_start()) 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 + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py index 541af23783f..6eea5c10f78 100644 --- a/homeassistant/components/sensorpush/sensor.py +++ b/homeassistant/components/sensorpush/sensor.py @@ -4,12 +4,10 @@ from __future__ import annotations from sensorpush_ble import DeviceClass, DeviceKey, 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 ( @@ -28,7 +26,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info -from .const import DOMAIN +from . import SensorPushConfigEntry SENSOR_DESCRIPTIONS = { (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( @@ -98,13 +96,11 @@ def sensor_update_to_bluetooth_data_update( async def async_setup_entry( hass: HomeAssistant, - entry: config_entries.ConfigEntry, + entry: SensorPushConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the SensorPush BLE sensors.""" - coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ - entry.entry_id - ] + coordinator = entry.runtime_data processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( From e8796cd7252562248b2e341a6f4d5bd1932373b2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 21 Jul 2024 16:21:45 +0200 Subject: [PATCH 1414/2411] Improve Hive typing (#122314) --- .../components/hive/binary_sensor.py | 10 ++++- homeassistant/components/hive/climate.py | 14 +++--- homeassistant/components/hive/config_flow.py | 44 ++++++++++++------- homeassistant/components/hive/sensor.py | 10 ++++- homeassistant/components/hive/switch.py | 9 +++- homeassistant/components/hive/water_heater.py | 7 +-- 6 files changed, 68 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index af1df7d4d62..8e64afa1771 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -1,6 +1,9 @@ """Support for the Hive binary sensors.""" from datetime import timedelta +from typing import Any + +from apyhiveapi import Hive from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -68,7 +71,12 @@ async def async_setup_entry( class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): """Representation of a Hive binary sensor.""" - def __init__(self, hive, hive_device, entity_description): + def __init__( + self, + hive: Hive, + hive_device: dict[str, Any], + entity_description: BinarySensorEntityDescription, + ) -> None: """Initialise hive binary sensor.""" super().__init__(hive, hive_device) self.entity_description = entity_description diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 8f6db11babe..f4c8e678702 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging from typing import Any +from apyhiveapi import Hive import voluptuous as vol from homeassistant.components.climate import ( @@ -46,7 +47,10 @@ HIVE_TO_HASS_HVAC_ACTION = { True: HVACAction.HEATING, } -TEMP_UNIT = {"C": UnitOfTemperature.CELSIUS, "F": UnitOfTemperature.FAHRENHEIT} +TEMP_UNIT = { + "C": UnitOfTemperature.CELSIUS, + "F": UnitOfTemperature.FAHRENHEIT, +} PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) _LOGGER = logging.getLogger() @@ -97,11 +101,11 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): ) _enable_turn_on_off_backwards_compatibility = False - def __init__(self, hive_session, hive_device): + def __init__(self, hive: Hive, hive_device: dict[str, Any]) -> None: """Initialize the Climate device.""" - super().__init__(hive_session, hive_device) + super().__init__(hive, hive_device) self.thermostat_node_id = hive_device["device_id"] - self._attr_temperature_unit = TEMP_UNIT.get(hive_device["temperatureunit"]) + self._attr_temperature_unit = TEMP_UNIT[hive_device["temperatureunit"]] @refresh_system async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: @@ -132,7 +136,7 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): await self.hive.heating.setBoostOn(self.device, time_period, temperature) @refresh_system - async def async_heating_boost_off(self): + async def async_heating_boost_off(self) -> None: """Handle boost heating service call.""" await self.hive.heating.setBoostOff(self.device) diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py index 3b11e7a8246..f8cb089834a 100644 --- a/homeassistant/components/hive/config_flow.py +++ b/homeassistant/components/hive/config_flow.py @@ -31,19 +31,21 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Hive config flow.""" VERSION = CONFIG_ENTRY_VERSION + hive_auth: Auth - def __init__(self): + def __init__(self) -> None: """Initialize the config flow.""" - self.hive_auth = None - self.data = {} - self.tokens = {} - self.entry = None - self.device_registration = False + self.data: dict[str, Any] = {} + self.tokens: dict[str, str] = {} + self.entry: ConfigEntry | None = None + self.device_registration: bool = False self.device_name = "Home Assistant" - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Prompt user input. Create or edit entry.""" - errors = {} + errors: dict[str, str] = {} # Login to Hive with user data. if user_input is not None: self.data.update(user_input) @@ -83,7 +85,9 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN): ) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_2fa(self, user_input=None): + async def async_step_2fa( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle 2fa step.""" errors = {} @@ -108,7 +112,9 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN): schema = vol.Schema({vol.Required(CONF_CODE): str}) return self.async_show_form(step_id="2fa", data_schema=schema, errors=errors) - async def async_step_configuration(self, user_input=None): + async def async_step_configuration( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle hive configuration step.""" errors = {} @@ -130,7 +136,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN): step_id="configuration", data_schema=schema, errors=errors ) - async def async_setup_hive_entry(self): + async def async_setup_hive_entry(self) -> ConfigFlowResult: """Finish setup and create the config entry.""" if "AuthenticationResult" not in self.tokens: @@ -139,6 +145,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN): # Setup the config entry self.data["tokens"] = self.tokens if self.context["source"] == SOURCE_REAUTH: + assert self.entry self.hass.config_entries.async_update_entry( self.entry, title=self.data["username"], data=self.data ) @@ -156,7 +163,9 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN): } return await self.async_step_user(data) - async def async_step_import(self, user_input=None): + async def async_step_import( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Import user.""" return await self.async_step_user(user_input) @@ -178,16 +187,21 @@ class HiveOptionsFlowHandler(OptionsFlow): self.config_entry = config_entry self.interval = config_entry.options.get(CONF_SCAN_INTERVAL, 120) - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Manage the options.""" return await self.async_step_user() - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" self.hive = self.hass.data["hive"][self.config_entry.entry_id] - errors = {} + errors: dict[str, str] = {} if user_input is not None: new_interval = user_input.get(CONF_SCAN_INTERVAL) + assert self.hive await self.hive.updateInterval(new_interval) return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index 5f750642385..4e81b1a5d85 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -1,6 +1,9 @@ """Support for the Hive sensors.""" from datetime import timedelta +from typing import Any + +from apyhiveapi import Hive from homeassistant.components.sensor import ( SensorDeviceClass, @@ -70,7 +73,12 @@ async def async_setup_entry( class HiveSensorEntity(HiveEntity, SensorEntity): """Hive Sensor Entity.""" - def __init__(self, hive, hive_device, entity_description): + def __init__( + self, + hive: Hive, + hive_device: dict[str, Any], + entity_description: SensorEntityDescription, + ) -> None: """Initialise hive sensor.""" super().__init__(hive, hive_device) self.entity_description = entity_description diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 6bcbfc6345c..136f03de195 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -5,6 +5,8 @@ from __future__ import annotations from datetime import timedelta from typing import Any +from apyhiveapi import Hive + from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory @@ -52,7 +54,12 @@ async def async_setup_entry( class HiveSwitch(HiveEntity, SwitchEntity): """Hive Active Plug.""" - def __init__(self, hive, hive_device, entity_description): + def __init__( + self, + hive: Hive, + hive_device: dict[str, Any], + entity_description: SwitchEntityDescription, + ) -> None: """Initialise hive switch.""" super().__init__(hive, hive_device) self.entity_description = entity_description diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 127fb80ef18..2e582e19567 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -1,6 +1,7 @@ """Support for hive water heaters.""" from datetime import timedelta +from typing import Any import voluptuous as vol @@ -76,12 +77,12 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): _attr_operation_list = SUPPORT_WATER_HEATER @refresh_system - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on hotwater.""" await self.hive.hotwater.setMode(self.device, "MANUAL") @refresh_system - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn on hotwater.""" await self.hive.hotwater.setMode(self.device, "OFF") @@ -92,7 +93,7 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): await self.hive.hotwater.setMode(self.device, new_mode) @refresh_system - async def async_hot_water_boost(self, time_period, on_off): + async def async_hot_water_boost(self, time_period: int, on_off: str) -> None: """Handle the service call.""" if on_off == "on": await self.hive.hotwater.setBoostOn(self.device, time_period) From a78d6b8c365a9fc14b0ba6c5b469a5a06fbfd187 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 21 Jul 2024 16:22:45 +0200 Subject: [PATCH 1415/2411] Set polling interval for airgradient to 1 minute (#122266) --- homeassistant/components/airgradient/coordinator.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/airgradient/coordinator.py b/homeassistant/components/airgradient/coordinator.py index fbc1505f9c3..c3def0b1f33 100644 --- a/homeassistant/components/airgradient/coordinator.py +++ b/homeassistant/components/airgradient/coordinator.py @@ -19,7 +19,6 @@ if TYPE_CHECKING: class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]): """Class to manage fetching AirGradient data.""" - _update_interval: timedelta config_entry: AirGradientConfigEntry def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None: @@ -28,7 +27,7 @@ class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]): hass, logger=LOGGER, name=f"AirGradient {client.host}", - update_interval=self._update_interval, + update_interval=timedelta(minutes=1), ) self.client = client assert self.config_entry.unique_id @@ -47,8 +46,6 @@ class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]): class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]): """Class to manage fetching AirGradient data.""" - _update_interval = timedelta(minutes=1) - async def _update_data(self) -> Measures: return await self.client.get_current_measures() @@ -56,7 +53,5 @@ class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]): class AirGradientConfigCoordinator(AirGradientCoordinator[Config]): """Class to manage fetching AirGradient data.""" - _update_interval = timedelta(minutes=5) - async def _update_data(self) -> Config: return await self.client.get_config() From 8d01ad98ebf43054387354878a5ab258059bbd05 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 21 Jul 2024 16:24:46 +0200 Subject: [PATCH 1416/2411] Clean up Mealie coordinator (#122310) * Clean up Mealie coordinator * Clean up Mealie coordinator * Clean up Mealie coordinator * Fix * Fix --- .../components/mealie/coordinator.py | 115 +++++++----------- tests/components/mealie/test_init.py | 12 +- 2 files changed, 55 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/mealie/coordinator.py b/homeassistant/components/mealie/coordinator.py index a4507c88985..051586e53c2 100644 --- a/homeassistant/components/mealie/coordinator.py +++ b/homeassistant/components/mealie/coordinator.py @@ -2,6 +2,7 @@ from __future__ import annotations +from abc import abstractmethod from dataclasses import dataclass from datetime import timedelta @@ -44,56 +45,50 @@ class MealieDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]): """Base coordinator.""" config_entry: MealieConfigEntry + _name: str + _update_interval: timedelta - def __init__( - self, - hass: HomeAssistant, - name: str, - client: MealieClient, - update_interval: timedelta, - ) -> None: - """Initialize the Withings data coordinator.""" + def __init__(self, hass: HomeAssistant, client: MealieClient) -> None: + """Initialize the Mealie data coordinator.""" super().__init__( hass, LOGGER, - name=name, - update_interval=update_interval, + name=self._name, + update_interval=self._update_interval, ) self.client = client + async def _async_update_data(self) -> _DataT: + """Fetch data from Mealie.""" + try: + return await self._async_update_internal() + except MealieAuthenticationError as error: + raise ConfigEntryAuthFailed from error + except MealieConnectionError as error: + raise UpdateFailed(error) from error + + @abstractmethod + async def _async_update_internal(self) -> _DataT: + """Fetch data from Mealie.""" + class MealieMealplanCoordinator( MealieDataUpdateCoordinator[dict[MealplanEntryType, list[Mealplan]]] ): """Class to manage fetching Mealie data.""" - def __init__(self, hass: HomeAssistant, client: MealieClient) -> None: - """Initialize coordinator.""" - super().__init__( - hass, - name="MealieMealplan", - client=client, - update_interval=timedelta(hours=1), - ) - self.client = client + _name = "MealieMealplan" + _update_interval = timedelta(hours=1) - async def _async_update_data(self) -> dict[MealplanEntryType, list[Mealplan]]: + async def _async_update_internal(self) -> dict[MealplanEntryType, list[Mealplan]]: next_week = dt_util.now() + WEEK - try: - data = ( - await self.client.get_mealplans(dt_util.now().date(), next_week.date()) - ).items - except MealieAuthenticationError as error: - raise ConfigEntryAuthFailed from error - except MealieConnectionError as error: - raise UpdateFailed(error) from error + current_date = dt_util.now().date() + next_week_date = next_week.date() + response = await self.client.get_mealplans(current_date, next_week_date) res: dict[MealplanEntryType, list[Mealplan]] = { - MealplanEntryType.BREAKFAST: [], - MealplanEntryType.LUNCH: [], - MealplanEntryType.DINNER: [], - MealplanEntryType.SIDE: [], + type_: [] for type_ in MealplanEntryType } - for meal in data: + for meal in response.items: res[meal.entry_type].append(meal) return res @@ -111,56 +106,34 @@ class MealieShoppingListCoordinator( ): """Class to manage fetching Mealie Shopping list data.""" - def __init__(self, hass: HomeAssistant, client: MealieClient) -> None: - """Initialize coordinator.""" - super().__init__( - hass, - name="MealieShoppingLists", - client=client, - update_interval=timedelta(minutes=5), - ) + _name = "MealieShoppingList" + _update_interval = timedelta(minutes=5) - async def _async_update_data( + async def _async_update_internal( self, ) -> dict[str, ShoppingListData]: shopping_list_items = {} - try: - shopping_lists = (await self.client.get_shopping_lists()).items - for shopping_list in shopping_lists: - shopping_list_id = shopping_list.list_id + shopping_lists = (await self.client.get_shopping_lists()).items + for shopping_list in shopping_lists: + shopping_list_id = shopping_list.list_id - shopping_items = ( - await self.client.get_shopping_items(shopping_list_id) - ).items + shopping_items = ( + await self.client.get_shopping_items(shopping_list_id) + ).items - shopping_list_items[shopping_list_id] = ShoppingListData( - shopping_list=shopping_list, items=shopping_items - ) - except MealieAuthenticationError as error: - raise ConfigEntryAuthFailed from error - except MealieConnectionError as error: - raise UpdateFailed(error) from error + shopping_list_items[shopping_list_id] = ShoppingListData( + shopping_list=shopping_list, items=shopping_items + ) return shopping_list_items class MealieStatisticsCoordinator(MealieDataUpdateCoordinator[Statistics]): """Class to manage fetching Mealie Statistics data.""" - def __init__(self, hass: HomeAssistant, client: MealieClient) -> None: - """Initialize coordinator.""" - super().__init__( - hass, - name="MealieStatistics", - client=client, - update_interval=timedelta(minutes=15), - ) + _name = "MealieStatistics" + _update_interval = timedelta(minutes=15) - async def _async_update_data( + async def _async_update_internal( self, ) -> Statistics: - try: - return await self.client.get_statistics() - except MealieAuthenticationError as error: - raise ConfigEntryAuthFailed from error - except MealieConnectionError as error: - raise UpdateFailed(error) from error + return await self.client.get_statistics() diff --git a/tests/components/mealie/test_init.py b/tests/components/mealie/test_init.py index 0050aa58bb8..ed5b1290a9b 100644 --- a/tests/components/mealie/test_init.py +++ b/tests/components/mealie/test_init.py @@ -32,6 +32,15 @@ async def test_device_info( assert device_entry == snapshot +@pytest.mark.parametrize( + "field", + [ + "get_about", + "get_mealplans", + "get_shopping_lists", + "get_statistics", + ], +) @pytest.mark.parametrize( ("exc", "state"), [ @@ -43,11 +52,12 @@ async def test_setup_failure( hass: HomeAssistant, mock_mealie_client: AsyncMock, mock_config_entry: MockConfigEntry, + field: str, exc: Exception, state: ConfigEntryState, ) -> None: """Test setup failure.""" - mock_mealie_client.get_about.side_effect = exc + getattr(mock_mealie_client, field).side_effect = exc await setup_integration(hass, mock_config_entry) From 7e82b3ecdb554939a0efb9fa3fbcefd6d4d4bd3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Jul 2024 09:32:58 -0500 Subject: [PATCH 1417/2411] Add event platform to govee-ble (#122031) Co-authored-by: Joost Lekkerkerker --- .../components/govee_ble/__init__.py | 24 ++-- .../components/govee_ble/config_flow.py | 30 +++-- homeassistant/components/govee_ble/const.py | 2 + .../components/govee_ble/coordinator.py | 79 +++++++++++++ homeassistant/components/govee_ble/event.py | 107 ++++++++++++++++++ homeassistant/components/govee_ble/sensor.py | 2 +- .../components/govee_ble/strings.json | 73 ++++++++++++ tests/components/govee_ble/__init__.py | 53 +++++++++ .../components/govee_ble/test_config_flow.py | 8 +- tests/components/govee_ble/test_event.py | 75 ++++++++++++ 10 files changed, 429 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/govee_ble/coordinator.py create mode 100644 homeassistant/components/govee_ble/event.py create mode 100644 tests/components/govee_ble/test_event.py diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py index a79f1e522b4..c4bc0aaf000 100644 --- a/homeassistant/components/govee_ble/__init__.py +++ b/homeassistant/components/govee_ble/__init__.py @@ -2,38 +2,40 @@ from __future__ import annotations +from functools import partial import logging -from govee_ble import GoveeBluetoothDeviceData, SensorUpdate +from govee_ble import GoveeBluetoothDeviceData from homeassistant.components.bluetooth import BluetoothScanningMode -from homeassistant.components.bluetooth.passive_update_processor import ( - PassiveBluetoothProcessorCoordinator, -) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -PLATFORMS: list[Platform] = [Platform.SENSOR] +from .coordinator import ( + GoveeBLEBluetoothProcessorCoordinator, + GoveeBLEConfigEntry, + process_service_info, +) + +PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) -GoveeBLEConfigEntry = ConfigEntry[PassiveBluetoothProcessorCoordinator[SensorUpdate]] - async def async_setup_entry(hass: HomeAssistant, entry: GoveeBLEConfigEntry) -> bool: """Set up Govee BLE device from a config entry.""" address = entry.unique_id assert address is not None data = GoveeBluetoothDeviceData() - coordinator = PassiveBluetoothProcessorCoordinator( + entry.runtime_data = coordinator = GoveeBLEBluetoothProcessorCoordinator( hass, _LOGGER, address=address, mode=BluetoothScanningMode.ACTIVE, - update_method=data.update, + update_method=partial(process_service_info, hass, entry), + device_data=data, + entry=entry, ) - entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # only start after all platforms have had a chance to subscribe entry.async_on_unload(coordinator.async_start()) diff --git a/homeassistant/components/govee_ble/config_flow.py b/homeassistant/components/govee_ble/config_flow.py index f580fca68d8..2cc47435abf 100644 --- a/homeassistant/components/govee_ble/config_flow.py +++ b/homeassistant/components/govee_ble/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.components.bluetooth import ( from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from .const import DOMAIN +from .const import CONF_DEVICE_TYPE, DOMAIN class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): @@ -26,7 +26,9 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): """Initialize the config flow.""" self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None - self._discovered_devices: dict[str, str] = {} + self._discovered_devices: dict[ + str, tuple[DeviceData, BluetoothServiceInfoBleak] + ] = {} async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak @@ -51,7 +53,9 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): 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=title, data={CONF_DEVICE_TYPE: device.device_type} + ) self._set_confirm_only() placeholders = {"name": title} @@ -68,8 +72,10 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) self._abort_if_unique_id_configured() + device, service_info = self._discovered_devices[address] + title = device.title or device.get_device_name() or service_info.name return self.async_create_entry( - title=self._discovered_devices[address], data={} + title=title, data={CONF_DEVICE_TYPE: device.device_type} ) current_addresses = self._async_current_ids() @@ -79,9 +85,7 @@ class GoveeConfigFlow(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] = (device, discovery_info) if not self._discovered_devices: return self.async_abort(reason="no_devices_found") @@ -89,6 +93,16 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=vol.Schema( - {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + { + vol.Required(CONF_ADDRESS): vol.In( + { + address: f"{device.get_device_name(None) or discovery_info.name} ({address})" + for address, ( + device, + discovery_info, + ) in self._discovered_devices.items() + } + ) + } ), ) diff --git a/homeassistant/components/govee_ble/const.py b/homeassistant/components/govee_ble/const.py index 4f30ee5023f..6651c315b93 100644 --- a/homeassistant/components/govee_ble/const.py +++ b/homeassistant/components/govee_ble/const.py @@ -1,3 +1,5 @@ """Constants for the Govee Bluetooth integration.""" DOMAIN = "govee_ble" + +CONF_DEVICE_TYPE = "device_type" diff --git a/homeassistant/components/govee_ble/coordinator.py b/homeassistant/components/govee_ble/coordinator.py new file mode 100644 index 00000000000..0d0ff5f93bd --- /dev/null +++ b/homeassistant/components/govee_ble/coordinator.py @@ -0,0 +1,79 @@ +"""The govee Bluetooth integration.""" + +from collections.abc import Callable +from logging import Logger + +from govee_ble import GoveeBluetoothDeviceData, ModelInfo, SensorUpdate, get_model_info + +from homeassistant.components.bluetooth import ( + BluetoothScanningMode, + BluetoothServiceInfoBleak, +) +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CoreState, HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import CONF_DEVICE_TYPE, DOMAIN + +type GoveeBLEConfigEntry = ConfigEntry[GoveeBLEBluetoothProcessorCoordinator] + + +def process_service_info( + hass: HomeAssistant, + entry: GoveeBLEConfigEntry, + service_info: BluetoothServiceInfoBleak, +) -> SensorUpdate: + """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" + coordinator = entry.runtime_data + data = coordinator.device_data + update = data.update(service_info) + if not coordinator.model_info and (device_type := data.device_type): + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_DEVICE_TYPE: device_type} + ) + coordinator.set_model_info(device_type) + if update.events and hass.state is CoreState.running: + # Do not fire events on data restore + address = service_info.device.address + for event in update.events.values(): + key = event.device_key.key + signal = format_event_dispatcher_name(address, key) + async_dispatcher_send(hass, signal) + + return update + + +def format_event_dispatcher_name(address: str, key: str) -> str: + """Format an event dispatcher name.""" + return f"{DOMAIN}_{address}_{key}" + + +class GoveeBLEBluetoothProcessorCoordinator( + PassiveBluetoothProcessorCoordinator[SensorUpdate] +): + """Define a govee ble Bluetooth Passive Update Processor Coordinator.""" + + def __init__( + self, + hass: HomeAssistant, + logger: Logger, + address: str, + mode: BluetoothScanningMode, + update_method: Callable[[BluetoothServiceInfoBleak], SensorUpdate], + device_data: GoveeBluetoothDeviceData, + entry: ConfigEntry, + ) -> None: + """Initialize the Govee BLE Bluetooth Passive Update Processor Coordinator.""" + super().__init__(hass, logger, address, mode, update_method) + self.device_data = device_data + self.entry = entry + self.model_info: ModelInfo | None = None + if device_type := entry.data.get(CONF_DEVICE_TYPE): + self.set_model_info(device_type) + + def set_model_info(self, device_type: str) -> None: + """Set the model info.""" + self.model_info = get_model_info(device_type) diff --git a/homeassistant/components/govee_ble/event.py b/homeassistant/components/govee_ble/event.py new file mode 100644 index 00000000000..67e0b0b86fb --- /dev/null +++ b/homeassistant/components/govee_ble/event.py @@ -0,0 +1,107 @@ +"""Support for govee_ble event entities.""" + +from __future__ import annotations + +from govee_ble import ModelInfo, SensorType + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_last_service_info, +) +from homeassistant.components.event import ( + EventDeviceClass, + EventEntity, + EventEntityDescription, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import GoveeBLEConfigEntry, format_event_dispatcher_name + +BUTTON_DESCRIPTIONS = [ + EventEntityDescription( + key=f"button_{i}", + translation_key=f"button_{i}", + event_types=["press"], + device_class=EventDeviceClass.BUTTON, + ) + for i in range(6) +] +MOTION_DESCRIPTION = EventEntityDescription( + key="motion", + event_types=["motion"], + device_class=EventDeviceClass.MOTION, +) + + +class GoveeBluetoothEventEntity(EventEntity): + """Representation of a govee ble event entity.""" + + _attr_should_poll = False + _attr_has_entity_name = True + + def __init__( + self, + model_info: ModelInfo, + service_info: BluetoothServiceInfoBleak | None, + address: str, + description: EventEntityDescription, + ) -> None: + """Initialise a govee ble event entity.""" + self.entity_description = description + # Matches logic in PassiveBluetoothProcessorEntity + name = service_info.name if service_info else model_info.model_id + self._attr_device_info = dr.DeviceInfo( + name=name, + identifiers={(DOMAIN, address)}, + connections={(dr.CONNECTION_BLUETOOTH, address)}, + ) + self._attr_unique_id = f"{address}-{description.key}" + self._address = address + self._signal = format_event_dispatcher_name( + self._address, self.entity_description.key + ) + + async def async_added_to_hass(self) -> None: + """Entity added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._signal, + self._async_handle_event, + ) + ) + + @callback + def _async_handle_event(self) -> None: + self._trigger_event(self.event_types[0]) + self.async_write_ha_state() + + +async def async_setup_entry( + hass: HomeAssistant, + entry: GoveeBLEConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a govee ble event.""" + coordinator = entry.runtime_data + if not (model_info := coordinator.model_info): + return + address = coordinator.address + sensor_type = model_info.sensor_type + if sensor_type is SensorType.MOTION: + descriptions = [MOTION_DESCRIPTION] + elif sensor_type is SensorType.BUTTON: + button_count = model_info.button_count + descriptions = BUTTON_DESCRIPTIONS[0:button_count] + else: + return + last_service_info = async_last_service_info(hass, address, False) + async_add_entities( + GoveeBluetoothEventEntity(model_info, last_service_info, address, description) + for description in descriptions + ) diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py index 61d2a971810..a0102cf629e 100644 --- a/homeassistant/components/govee_ble/sensor.py +++ b/homeassistant/components/govee_ble/sensor.py @@ -27,7 +27,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info -from . import GoveeBLEConfigEntry +from .coordinator import GoveeBLEConfigEntry SENSOR_DESCRIPTIONS = { (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( diff --git a/homeassistant/components/govee_ble/strings.json b/homeassistant/components/govee_ble/strings.json index 4e12a84b653..7608e6c5c82 100644 --- a/homeassistant/components/govee_ble/strings.json +++ b/homeassistant/components/govee_ble/strings.json @@ -17,5 +17,78 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "event": { + "motion": { + "state_attributes": { + "event_type": { + "state": { + "motion": "[%key:component::event::entity_component::motion::name%]" + } + } + } + }, + "button_0": { + "name": "Button 1", + "state_attributes": { + "event_type": { + "state": { + "press": "Press" + } + } + } + }, + "button_1": { + "name": "Button 2", + "state_attributes": { + "event_type": { + "state": { + "press": "[%key:component::govee_ble::entity::event::button_0::state_attributes::event_type::state::press%]" + } + } + } + }, + "button_2": { + "name": "Button 3", + "state_attributes": { + "event_type": { + "state": { + "press": "[%key:component::govee_ble::entity::event::button_0::state_attributes::event_type::state::press%]" + } + } + } + }, + "button_3": { + "name": "Button 4", + "state_attributes": { + "event_type": { + "state": { + "press": "[%key:component::govee_ble::entity::event::button_0::state_attributes::event_type::state::press%]" + } + } + } + }, + "button_4": { + "name": "Button 5", + "state_attributes": { + "event_type": { + "state": { + "press": "[%key:component::govee_ble::entity::event::button_0::state_attributes::event_type::state::press%]" + } + } + } + }, + "button_5": { + "name": "Button 6", + "state_attributes": { + "event_type": { + "state": { + "press": "[%key:component::govee_ble::entity::event::button_0::state_attributes::event_type::state::press%]" + } + } + } + } + } } } diff --git a/tests/components/govee_ble/__init__.py b/tests/components/govee_ble/__init__.py index 60930d1dd0e..b26bfba5830 100644 --- a/tests/components/govee_ble/__init__.py +++ b/tests/components/govee_ble/__init__.py @@ -83,3 +83,56 @@ GVH5106_SERVICE_INFO = BluetoothServiceInfo( service_data={}, source="local", ) + + +GV5125_BUTTON_0_SERVICE_INFO = BluetoothServiceInfo( + name="GV51255367", + address="C1:37:37:32:0F:45", + rssi=-36, + manufacturer_data={ + 60552: b"\x01\n.\xaf\xd9085Sg\x01\x01", + 61320: b".\xaf\x00\x00b\\\xae\x92\x15\xb6\xa8\n\xd4\x81K\xcaK_s\xd9E40\x02", + }, + service_data={}, + service_uuids=[], + source="24:4C:AB:03:E6:B8", +) + +GV5125_BUTTON_1_SERVICE_INFO = BluetoothServiceInfo( + name="GV51255367", + address="C1:37:37:32:0F:45", + rssi=-36, + manufacturer_data={ + 60552: b"\x01\n.\xaf\xd9085Sg\x01\x01", + 61320: b".\xaf\x00\x00\xfb\x0e\xc9h\xd7\x05l\xaf*\xf3\x1b\xe8w\xf1\xe1\xe8\xe3\xa7\xf8\xc6", + }, + service_data={}, + service_uuids=[], + source="24:4C:AB:03:E6:B8", +) + + +GV5121_MOTION_SERVICE_INFO = BluetoothServiceInfo( + name="GV5121195A", + address="C1:37:37:32:0F:45", + rssi=-36, + manufacturer_data={ + 61320: b"Y\x94\x00\x00\xf0\xb9\x197\xaeP\xb67,\x86j\xc2\xf3\xd0a\xe7\x17\xc0,\xef" + }, + service_data={}, + service_uuids=[], + source="24:4C:AB:03:E6:B8", +) + + +GV5121_MOTION_SERVICE_INFO_2 = BluetoothServiceInfo( + name="GV5121195A", + address="C1:37:37:32:0F:45", + rssi=-36, + manufacturer_data={ + 61320: b"Y\x94\x00\x06\xa3f6e\xc8\xe6\xfdv\x04\xaf\xe7k\xbf\xab\xeb\xbf\xb3\xa3\xd5\x19" + }, + service_data={}, + service_uuids=[], + source="24:4C:AB:03:E6:B8", +) diff --git a/tests/components/govee_ble/test_config_flow.py b/tests/components/govee_ble/test_config_flow.py index 0c340c01f2a..eb0719f832c 100644 --- a/tests/components/govee_ble/test_config_flow.py +++ b/tests/components/govee_ble/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant import config_entries -from homeassistant.components.govee_ble.const import DOMAIN +from homeassistant.components.govee_ble.const import CONF_DEVICE_TYPE, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -29,7 +29,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "H5075 2762" - assert result2["data"] == {} + assert result2["data"] == {CONF_DEVICE_TYPE: "H5075"} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" @@ -75,7 +75,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "H5177 2EC8" - assert result2["data"] == {} + assert result2["data"] == {CONF_DEVICE_TYPE: "H5177"} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -198,7 +198,7 @@ async def test_async_step_user_takes_precedence_over_discovery( ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "H5177 2EC8" - assert result2["data"] == {} + assert result2["data"] == {CONF_DEVICE_TYPE: "H5177"} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" # Verify the original one was aborted diff --git a/tests/components/govee_ble/test_event.py b/tests/components/govee_ble/test_event.py new file mode 100644 index 00000000000..c2e215188ff --- /dev/null +++ b/tests/components/govee_ble/test_event.py @@ -0,0 +1,75 @@ +"""Test the Govee BLE events.""" + +from homeassistant.components.govee_ble.const import CONF_DEVICE_TYPE, DOMAIN +from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from . import ( + GV5121_MOTION_SERVICE_INFO, + GV5121_MOTION_SERVICE_INFO_2, + GV5125_BUTTON_0_SERVICE_INFO, + GV5125_BUTTON_1_SERVICE_INFO, +) + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_motion_sensor(hass: HomeAssistant) -> None: + """Test setting up creates the motion sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=GV5121_MOTION_SERVICE_INFO.address, + data={CONF_DEVICE_TYPE: "H5121"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + inject_bluetooth_service_info(hass, GV5121_MOTION_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + motion_sensor = hass.states.get("event.h5121_motion") + first_time = motion_sensor.state + assert motion_sensor.state != STATE_UNKNOWN + + inject_bluetooth_service_info(hass, GV5121_MOTION_SERVICE_INFO_2) + await hass.async_block_till_done() + + motion_sensor = hass.states.get("event.h5121_motion") + assert motion_sensor.state != first_time + assert motion_sensor.state != STATE_UNKNOWN + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_button(hass: HomeAssistant) -> None: + """Test setting up creates the buttons.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=GV5125_BUTTON_1_SERVICE_INFO.address, + data={CONF_DEVICE_TYPE: "H5125"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 6 + inject_bluetooth_service_info(hass, GV5125_BUTTON_1_SERVICE_INFO) + await hass.async_block_till_done() + + button_1 = hass.states.get("event.h5125_button_1") + assert button_1.state == STATE_UNKNOWN + + inject_bluetooth_service_info(hass, GV5125_BUTTON_0_SERVICE_INFO) + await hass.async_block_till_done() + button_1 = hass.states.get("event.h5125_button_1") + assert button_1.state != STATE_UNKNOWN + assert len(hass.states.async_all()) == 7 + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 39068bb78605803953bfe4feafeaaff2157e6959 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Jul 2024 09:38:00 -0500 Subject: [PATCH 1418/2411] Add sleepy device support to govee-ble (#122085) --- homeassistant/components/govee_ble/coordinator.py | 9 +++++++++ homeassistant/components/govee_ble/sensor.py | 11 +++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/govee_ble/coordinator.py b/homeassistant/components/govee_ble/coordinator.py index 0d0ff5f93bd..011a89e565b 100644 --- a/homeassistant/components/govee_ble/coordinator.py +++ b/homeassistant/components/govee_ble/coordinator.py @@ -10,6 +10,7 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, ) from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, PassiveBluetoothProcessorCoordinator, ) from homeassistant.config_entries import ConfigEntry @@ -77,3 +78,11 @@ class GoveeBLEBluetoothProcessorCoordinator( def set_model_info(self, device_type: str) -> None: """Set the model info.""" self.model_info = get_model_info(device_type) + + +class GoveeBLEPassiveBluetoothDataProcessor[_T]( + PassiveBluetoothDataProcessor[_T, SensorUpdate] +): + """Define a govee-ble Bluetooth Passive Update Data Processor.""" + + coordinator: GoveeBLEBluetoothProcessorCoordinator diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py index a0102cf629e..8c9812249a3 100644 --- a/homeassistant/components/govee_ble/sensor.py +++ b/homeassistant/components/govee_ble/sensor.py @@ -27,7 +27,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info -from .coordinator import GoveeBLEConfigEntry +from .coordinator import GoveeBLEConfigEntry, GoveeBLEPassiveBluetoothDataProcessor SENSOR_DESCRIPTIONS = { (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( @@ -130,12 +130,15 @@ class GoveeBluetoothSensorEntity( ): """Representation of a govee ble sensor.""" + processor: GoveeBLEPassiveBluetoothDataProcessor + @property def available(self) -> bool: """Return False if sensor is in error.""" - return ( - self.processor.entity_data.get(self.entity_key) != ERROR - and super().available + coordinator = self.processor.coordinator + return self.processor.entity_data.get(self.entity_key) != ERROR and ( + ((model_info := coordinator.model_info) and model_info.sleepy) + or super().available ) @property From 6f4a8a4a14261fa11da65fe07c0850f43ab0e48f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 21 Jul 2024 16:43:46 +0200 Subject: [PATCH 1419/2411] Add Mealie service to set a random mealplan (#122313) * Add Mealie service to set a random mealplan * Fix coverage * Fix coverage --- homeassistant/components/mealie/const.py | 1 + homeassistant/components/mealie/icons.json | 3 +- homeassistant/components/mealie/services.py | 42 +++++++++- homeassistant/components/mealie/services.yaml | 22 ++++++ homeassistant/components/mealie/strings.json | 28 +++++++ tests/components/mealie/conftest.py | 3 + .../components/mealie/fixtures/mealplan.json | 34 ++++++++ .../mealie/snapshots/test_services.ambr | 24 ++++++ tests/components/mealie/test_services.py | 78 ++++++++++++++++++- 9 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 tests/components/mealie/fixtures/mealplan.json diff --git a/homeassistant/components/mealie/const.py b/homeassistant/components/mealie/const.py index 800cfd21db3..95802bfc02a 100644 --- a/homeassistant/components/mealie/const.py +++ b/homeassistant/components/mealie/const.py @@ -14,5 +14,6 @@ ATTR_END_DATE = "end_date" ATTR_RECIPE_ID = "recipe_id" ATTR_URL = "url" ATTR_INCLUDE_TAGS = "include_tags" +ATTR_ENTRY_TYPE = "entry_type" MIN_REQUIRED_MEALIE_VERSION = AwesomeVersion("v1.0.0") diff --git a/homeassistant/components/mealie/icons.json b/homeassistant/components/mealie/icons.json index f509985eb72..883779a8fb0 100644 --- a/homeassistant/components/mealie/icons.json +++ b/homeassistant/components/mealie/icons.json @@ -26,6 +26,7 @@ "services": { "get_mealplan": "mdi:food", "get_recipe": "mdi:map", - "import_recipe": "mdi:map-search" + "import_recipe": "mdi:map-search", + "set_random_mealplan": "mdi:dice-multiple" } } diff --git a/homeassistant/components/mealie/services.py b/homeassistant/components/mealie/services.py index d7be0885f3c..3b1257ff16d 100644 --- a/homeassistant/components/mealie/services.py +++ b/homeassistant/components/mealie/services.py @@ -4,10 +4,16 @@ from dataclasses import asdict from datetime import date from typing import cast -from aiomealie import MealieConnectionError, MealieNotFoundError, MealieValidationError +from aiomealie import ( + MealieConnectionError, + MealieNotFoundError, + MealieValidationError, + MealplanEntryType, +) import voluptuous as vol from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_DATE from homeassistant.core import ( HomeAssistant, ServiceCall, @@ -20,6 +26,7 @@ from homeassistant.helpers import config_validation as cv from .const import ( ATTR_CONFIG_ENTRY_ID, ATTR_END_DATE, + ATTR_ENTRY_TYPE, ATTR_INCLUDE_TAGS, ATTR_RECIPE_ID, ATTR_START_DATE, @@ -54,6 +61,15 @@ SERVICE_IMPORT_RECIPE_SCHEMA = vol.Schema( } ) +SERVICE_SET_RANDOM_MEALPLAN = "set_random_mealplan" +SERVICE_SET_RANDOM_MEALPLAN_SCHEMA = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): str, + vol.Required(ATTR_DATE): cv.date, + vol.Required(ATTR_ENTRY_TYPE): vol.In([x.lower() for x in MealplanEntryType]), + } +) + def async_get_entry(hass: HomeAssistant, config_entry_id: str) -> MealieConfigEntry: """Get the Mealie config entry.""" @@ -137,6 +153,23 @@ def setup_services(hass: HomeAssistant) -> None: return {"recipe": asdict(recipe)} return None + async def async_set_random_mealplan(call: ServiceCall) -> ServiceResponse: + """Set a random mealplan.""" + entry = async_get_entry(hass, call.data[ATTR_CONFIG_ENTRY_ID]) + mealplan_date = call.data[ATTR_DATE] + entry_type = MealplanEntryType(call.data[ATTR_ENTRY_TYPE]) + client = entry.runtime_data.client + try: + mealplan = await client.random_mealplan(mealplan_date, entry_type) + except MealieConnectionError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="connection_error", + ) from err + if call.return_response: + return {"mealplan": asdict(mealplan)} + return None + hass.services.async_register( DOMAIN, SERVICE_GET_MEALPLAN, @@ -158,3 +191,10 @@ def setup_services(hass: HomeAssistant) -> None: schema=SERVICE_IMPORT_RECIPE_SCHEMA, supports_response=SupportsResponse.OPTIONAL, ) + hass.services.async_register( + DOMAIN, + SERVICE_SET_RANDOM_MEALPLAN, + async_set_random_mealplan, + schema=SERVICE_SET_RANDOM_MEALPLAN_SCHEMA, + supports_response=SupportsResponse.OPTIONAL, + ) diff --git a/homeassistant/components/mealie/services.yaml b/homeassistant/components/mealie/services.yaml index 21043112579..c569df956e2 100644 --- a/homeassistant/components/mealie/services.yaml +++ b/homeassistant/components/mealie/services.yaml @@ -11,6 +11,7 @@ get_mealplan: end_date: selector: date: + get_recipe: fields: config_entry_id: @@ -22,6 +23,7 @@ get_recipe: required: true selector: text: + import_recipe: fields: config_entry_id: @@ -36,3 +38,23 @@ import_recipe: include_tags: selector: boolean: + +set_random_mealplan: + fields: + config_entry_id: + required: true + selector: + config_entry: + integration: mealie + date: + selector: + date: + entry_type: + selector: + select: + options: + - breakfast + - lunch + - dinner + - side + translation_key: mealplan_entry_type diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json index 7e1b307d18b..3524b1a5fb3 100644 --- a/homeassistant/components/mealie/strings.json +++ b/homeassistant/components/mealie/strings.json @@ -157,6 +157,34 @@ "description": "Include tags from the website to the recipe." } } + }, + "set_random_mealplan": { + "name": "Set random mealplan", + "description": "Set a random mealplan for a specific date", + "fields": { + "config_entry_id": { + "name": "[%key:component::mealie::services::get_mealplan::fields::config_entry_id::name%]", + "description": "[%key:component::mealie::services::get_mealplan::fields::config_entry_id::description%]" + }, + "date": { + "name": "Date", + "description": "The date to set the mealplan for." + }, + "entry_type": { + "name": "Entry type", + "description": "The type of dish to randomize." + } + } + } + }, + "selector": { + "mealplan_entry_type": { + "options": { + "breakfast": "[%key:component::mealie::entity::calendar::breakfast::name%]", + "lunch": "[%key:component::mealie::entity::calendar::lunch::name%]", + "dinner": "[%key:component::mealie::entity::calendar::dinner::name%]", + "side": "[%key:component::mealie::entity::calendar::side::name%]" + } } } } diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py index 2916159a799..208dd47ddf2 100644 --- a/tests/components/mealie/conftest.py +++ b/tests/components/mealie/conftest.py @@ -74,6 +74,9 @@ def mock_mealie_client() -> Generator[AsyncMock]: client.get_statistics.return_value = Statistics.from_json( load_fixture("statistics.json", DOMAIN) ) + client.random_mealplan.return_value = Mealplan.from_json( + load_fixture("mealplan.json", DOMAIN) + ) yield client diff --git a/tests/components/mealie/fixtures/mealplan.json b/tests/components/mealie/fixtures/mealplan.json new file mode 100644 index 00000000000..b540280d83f --- /dev/null +++ b/tests/components/mealie/fixtures/mealplan.json @@ -0,0 +1,34 @@ +{ + "date": "2024-01-22", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "c5f00a93-71a2-4e48-900f-d9ad0bb9de93", + "id": 230, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "c5f00a93-71a2-4e48-900f-d9ad0bb9de93", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Zoete aardappel curry traybake", + "slug": "zoete-aardappel-curry-traybake", + "image": "AiIo", + "recipeYield": "2 servings", + "totalTime": "40 Minutes", + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://chickslovefood.com/recept/zoete-aardappel-curry-traybake/", + "dateAdded": "2024-01-22", + "dateUpdated": "2024-01-22T00:27:46.324512", + "createdAt": "2024-01-22T00:27:46.327546", + "updateAt": "2024-01-22T00:27:46.327548", + "lastMade": null + } +} diff --git a/tests/components/mealie/snapshots/test_services.ambr b/tests/components/mealie/snapshots/test_services.ambr index 7bda79e14a6..293a1d8ee1d 100644 --- a/tests/components/mealie/snapshots/test_services.ambr +++ b/tests/components/mealie/snapshots/test_services.ambr @@ -675,3 +675,27 @@ }), }) # --- +# name: test_service_set_random_mealplan + dict({ + 'mealplan': dict({ + 'description': None, + 'entry_type': , + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'mealplan_date': datetime.date(2024, 1, 22), + 'mealplan_id': 230, + 'recipe': dict({ + 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'image': 'AiIo', + 'name': 'Zoete aardappel curry traybake', + 'original_url': 'https://chickslovefood.com/recept/zoete-aardappel-curry-traybake/', + 'recipe_id': 'c5f00a93-71a2-4e48-900f-d9ad0bb9de93', + 'recipe_yield': '2 servings', + 'slug': 'zoete-aardappel-curry-traybake', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + 'title': None, + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + }) +# --- diff --git a/tests/components/mealie/test_services.py b/tests/components/mealie/test_services.py index 7af1bc251d4..b44d67d5eec 100644 --- a/tests/components/mealie/test_services.py +++ b/tests/components/mealie/test_services.py @@ -3,7 +3,12 @@ from datetime import date from unittest.mock import AsyncMock -from aiomealie import MealieConnectionError, MealieNotFoundError, MealieValidationError +from aiomealie import ( + MealieConnectionError, + MealieNotFoundError, + MealieValidationError, + MealplanEntryType, +) from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion @@ -11,6 +16,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.mealie.const import ( ATTR_CONFIG_ENTRY_ID, ATTR_END_DATE, + ATTR_ENTRY_TYPE, ATTR_INCLUDE_TAGS, ATTR_RECIPE_ID, ATTR_START_DATE, @@ -21,7 +27,9 @@ from homeassistant.components.mealie.services import ( SERVICE_GET_MEALPLAN, SERVICE_GET_RECIPE, SERVICE_IMPORT_RECIPE, + SERVICE_SET_RANDOM_MEALPLAN, ) +from homeassistant.const import ATTR_DATE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError @@ -246,6 +254,74 @@ async def test_service_import_recipe_exceptions( ) +async def test_service_set_random_mealplan( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test the set_random_mealplan service.""" + + await setup_integration(hass, mock_config_entry) + + response = await hass.services.async_call( + DOMAIN, + SERVICE_SET_RANDOM_MEALPLAN, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATE: "2023-10-21", + ATTR_ENTRY_TYPE: "lunch", + }, + blocking=True, + return_response=True, + ) + assert response == snapshot + mock_mealie_client.random_mealplan.assert_called_with( + date(2023, 10, 21), MealplanEntryType.LUNCH + ) + + mock_mealie_client.random_mealplan.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_RANDOM_MEALPLAN, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATE: "2023-10-21", + ATTR_ENTRY_TYPE: "lunch", + }, + blocking=True, + return_response=False, + ) + mock_mealie_client.random_mealplan.assert_called_with( + date(2023, 10, 21), MealplanEntryType.LUNCH + ) + + +async def test_service_set_random_mealplan_exceptions( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the exceptions of the set_random_mealplan service.""" + + await setup_integration(hass, mock_config_entry) + + mock_mealie_client.random_mealplan.side_effect = MealieConnectionError + + with pytest.raises(HomeAssistantError, match="Error connecting to Mealie instance"): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_RANDOM_MEALPLAN, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATE: "2023-10-21", + ATTR_ENTRY_TYPE: "lunch", + }, + blocking=True, + return_response=True, + ) + + async def test_service_mealplan_connection_error( hass: HomeAssistant, mock_mealie_client: AsyncMock, From 5f4dedb4a8473d6c3d64b2ed683b1487d26000d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Jul 2024 09:47:59 -0500 Subject: [PATCH 1420/2411] Add binary sensor platform to govee-ble (#122111) --- .../components/govee_ble/__init__.py | 2 +- .../components/govee_ble/binary_sensor.py | 104 ++++++++++++++++++ homeassistant/components/govee_ble/device.py | 16 +++ homeassistant/components/govee_ble/sensor.py | 17 +-- tests/components/govee_ble/__init__.py | 26 +++++ .../govee_ble/test_binary_sensor.py | 39 +++++++ 6 files changed, 191 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/govee_ble/binary_sensor.py create mode 100644 homeassistant/components/govee_ble/device.py create mode 100644 tests/components/govee_ble/test_binary_sensor.py diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py index c4bc0aaf000..07f7ded5447 100644 --- a/homeassistant/components/govee_ble/__init__.py +++ b/homeassistant/components/govee_ble/__init__.py @@ -17,7 +17,7 @@ from .coordinator import ( process_service_info, ) -PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.EVENT, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/govee_ble/binary_sensor.py b/homeassistant/components/govee_ble/binary_sensor.py new file mode 100644 index 00000000000..82033300797 --- /dev/null +++ b/homeassistant/components/govee_ble/binary_sensor.py @@ -0,0 +1,104 @@ +"""Support for govee-ble binary sensors.""" + +from __future__ import annotations + +from govee_ble import ( + BinarySensorDeviceClass as GoveeBLEBinarySensorDeviceClass, + SensorUpdate, +) +from govee_ble.parser import ERROR + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorEntity, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info + +from .coordinator import GoveeBLEPassiveBluetoothDataProcessor +from .device import device_key_to_bluetooth_entity_key + +BINARY_SENSOR_DESCRIPTIONS = { + GoveeBLEBinarySensorDeviceClass.WINDOW: BinarySensorEntityDescription( + key=GoveeBLEBinarySensorDeviceClass.WINDOW, + device_class=BinarySensorDeviceClass.WINDOW, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass_device_info(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): BINARY_SENSOR_DESCRIPTIONS[ + description.device_class + ] + for device_key, description in sensor_update.binary_entity_descriptions.items() + if description.device_class + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the govee-ble BLE sensors.""" + coordinator = entry.runtime_data + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + GoveeBluetoothBinarySensorEntity, async_add_entities + ) + ) + entry.async_on_unload( + coordinator.async_register_processor(processor, BinarySensorEntityDescription) + ) + + +class GoveeBluetoothBinarySensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[bool | None, SensorUpdate] + ], + BinarySensorEntity, +): + """Representation of a govee-ble binary sensor.""" + + processor: GoveeBLEPassiveBluetoothDataProcessor + + @property + def available(self) -> bool: + """Return False if sensor is in error.""" + coordinator = self.processor.coordinator + return self.processor.entity_data.get(self.entity_key) != ERROR and ( + ((model_info := coordinator.model_info) and model_info.sleepy) + or super().available + ) + + @property + def is_on(self) -> bool | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/govee_ble/device.py b/homeassistant/components/govee_ble/device.py new file mode 100644 index 00000000000..90b602780a2 --- /dev/null +++ b/homeassistant/components/govee_ble/device.py @@ -0,0 +1,16 @@ +"""Support for govee-ble devices.""" + +from __future__ import annotations + +from govee_ble import DeviceKey + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) + + +def device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py index 8c9812249a3..a94610ef0e1 100644 --- a/homeassistant/components/govee_ble/sensor.py +++ b/homeassistant/components/govee_ble/sensor.py @@ -2,13 +2,12 @@ from __future__ import annotations -from govee_ble import DeviceClass, DeviceKey, SensorUpdate, Units +from govee_ble import DeviceClass, SensorUpdate, Units from govee_ble.parser import ERROR from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothEntityKey, PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( @@ -28,6 +27,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .coordinator import GoveeBLEConfigEntry, GoveeBLEPassiveBluetoothDataProcessor +from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS = { (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( @@ -70,13 +70,6 @@ SENSOR_DESCRIPTIONS = { } -def _device_key_to_bluetooth_entity_key( - device_key: DeviceKey, -) -> PassiveBluetoothEntityKey: - """Convert a device key to an entity key.""" - return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) - - def sensor_update_to_bluetooth_data_update( sensor_update: SensorUpdate, ) -> PassiveBluetoothDataUpdate: @@ -87,18 +80,18 @@ def sensor_update_to_bluetooth_data_update( for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ - _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ (description.device_class, description.native_unit_of_measurement) ] for device_key, description in sensor_update.entity_descriptions.items() if description.device_class and description.native_unit_of_measurement }, entity_data={ - _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value for device_key, sensor_values in sensor_update.entity_values.items() }, entity_names={ - _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + device_key_to_bluetooth_entity_key(device_key): sensor_values.name for device_key, sensor_values in sensor_update.entity_values.items() }, ) diff --git a/tests/components/govee_ble/__init__.py b/tests/components/govee_ble/__init__.py index b26bfba5830..11f4065b506 100644 --- a/tests/components/govee_ble/__init__.py +++ b/tests/components/govee_ble/__init__.py @@ -136,3 +136,29 @@ GV5121_MOTION_SERVICE_INFO_2 = BluetoothServiceInfo( service_uuids=[], source="24:4C:AB:03:E6:B8", ) + + +GV5123_OPEN_SERVICE_INFO = BluetoothServiceInfo( + name="GV51230B3D", + address="C1:37:37:32:0F:45", + rssi=-36, + manufacturer_data={ + 61320: b"=\xec\x00\x00\xdeCw\xd5^U\xf9\x91In6\xbd\xc6\x7f\x8b,'\x06t\x97" + }, + service_data={}, + service_uuids=[], + source="24:4C:AB:03:E6:B8", +) + + +GV5123_CLOSED_SERVICE_INFO = BluetoothServiceInfo( + name="GV51230B3D", + address="C1:37:37:32:0F:45", + rssi=-36, + manufacturer_data={ + 61320: b"=\xec\x00\x01Y\xdbk\xd9\xbe\xd7\xaf\xf7*&\xaaK\xd7-\xfa\x94W>[\xe9" + }, + service_data={}, + service_uuids=[], + source="24:4C:AB:03:E6:B8", +) diff --git a/tests/components/govee_ble/test_binary_sensor.py b/tests/components/govee_ble/test_binary_sensor.py new file mode 100644 index 00000000000..a0acf4c461e --- /dev/null +++ b/tests/components/govee_ble/test_binary_sensor.py @@ -0,0 +1,39 @@ +"""Test the Govee BLE binary_sensor.""" + +from homeassistant.components.govee_ble.const import CONF_DEVICE_TYPE, DOMAIN +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant + +from . import GV5123_CLOSED_SERVICE_INFO, GV5123_OPEN_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_window_sensor(hass: HomeAssistant) -> None: + """Test setting up creates the window sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=GV5123_OPEN_SERVICE_INFO.address, + data={CONF_DEVICE_TYPE: "H5123"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info(hass, GV5123_OPEN_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + motion_sensor = hass.states.get("binary_sensor.51230f45_window") + assert motion_sensor.state == STATE_ON + + inject_bluetooth_service_info(hass, GV5123_CLOSED_SERVICE_INFO) + await hass.async_block_till_done() + + motion_sensor = hass.states.get("binary_sensor.51230f45_window") + assert motion_sensor.state == STATE_OFF + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 273dc0998fcf3db167fcbf11c3b25d714e0b4710 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 21 Jul 2024 17:15:28 +0200 Subject: [PATCH 1421/2411] Clean up Mealie service tests (#122316) --- tests/components/mealie/test_services.py | 213 +++++++++++------------ 1 file changed, 99 insertions(+), 114 deletions(-) diff --git a/tests/components/mealie/test_services.py b/tests/components/mealie/test_services.py index b44d67d5eec..06ed714ea01 100644 --- a/tests/components/mealie/test_services.py +++ b/tests/components/mealie/test_services.py @@ -188,72 +188,6 @@ async def test_service_import_recipe( ) -@pytest.mark.parametrize( - ("exception", "raised_exception"), - [ - (MealieNotFoundError, ServiceValidationError), - (MealieConnectionError, HomeAssistantError), - ], -) -async def test_service_recipe_exceptions( - hass: HomeAssistant, - mock_mealie_client: AsyncMock, - mock_config_entry: MockConfigEntry, - exception: Exception, - raised_exception: type[Exception], -) -> None: - """Test the get_recipe service.""" - - await setup_integration(hass, mock_config_entry) - - mock_mealie_client.get_recipe.side_effect = exception - - with pytest.raises(raised_exception): - await hass.services.async_call( - DOMAIN, - SERVICE_GET_RECIPE, - { - ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, - ATTR_RECIPE_ID: "recipe_id", - }, - blocking=True, - return_response=True, - ) - - -@pytest.mark.parametrize( - ("exception", "raised_exception"), - [ - (MealieValidationError, ServiceValidationError), - (MealieConnectionError, HomeAssistantError), - ], -) -async def test_service_import_recipe_exceptions( - hass: HomeAssistant, - mock_mealie_client: AsyncMock, - mock_config_entry: MockConfigEntry, - exception: Exception, - raised_exception: type[Exception], -) -> None: - """Test the exceptions of the import_recipe service.""" - - await setup_integration(hass, mock_config_entry) - - mock_mealie_client.import_recipe.side_effect = exception - - with pytest.raises(raised_exception): - await hass.services.async_call( - DOMAIN, - SERVICE_IMPORT_RECIPE, - { - ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, - ATTR_URL: "http://example.com", - }, - blocking=True, - return_response=True, - ) - - async def test_service_set_random_mealplan( hass: HomeAssistant, mock_mealie_client: AsyncMock, @@ -297,77 +231,128 @@ async def test_service_set_random_mealplan( ) -async def test_service_set_random_mealplan_exceptions( - hass: HomeAssistant, - mock_mealie_client: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test the exceptions of the set_random_mealplan service.""" - - await setup_integration(hass, mock_config_entry) - - mock_mealie_client.random_mealplan.side_effect = MealieConnectionError - - with pytest.raises(HomeAssistantError, match="Error connecting to Mealie instance"): - await hass.services.async_call( - DOMAIN, - SERVICE_SET_RANDOM_MEALPLAN, - { - ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, - ATTR_DATE: "2023-10-21", - ATTR_ENTRY_TYPE: "lunch", - }, - blocking=True, - return_response=True, - ) - - -async def test_service_mealplan_connection_error( - hass: HomeAssistant, - mock_mealie_client: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test a connection error in the get_mealplans service.""" - - await setup_integration(hass, mock_config_entry) - - mock_mealie_client.get_mealplans.side_effect = MealieConnectionError - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - DOMAIN, +@pytest.mark.parametrize( + ("service", "payload", "function", "exception", "raised_exception", "message"), + [ + ( SERVICE_GET_MEALPLAN, - {ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id}, + {}, + "get_mealplans", + MealieConnectionError, + HomeAssistantError, + "Error connecting to Mealie instance", + ), + ( + SERVICE_GET_RECIPE, + {ATTR_RECIPE_ID: "recipe_id"}, + "get_recipe", + MealieConnectionError, + HomeAssistantError, + "Error connecting to Mealie instance", + ), + ( + SERVICE_GET_RECIPE, + {ATTR_RECIPE_ID: "recipe_id"}, + "get_recipe", + MealieNotFoundError, + ServiceValidationError, + "Recipe with ID or slug `recipe_id` not found", + ), + ( + SERVICE_IMPORT_RECIPE, + {ATTR_URL: "http://example.com"}, + "import_recipe", + MealieConnectionError, + HomeAssistantError, + "Error connecting to Mealie instance", + ), + ( + SERVICE_IMPORT_RECIPE, + {ATTR_URL: "http://example.com"}, + "import_recipe", + MealieValidationError, + ServiceValidationError, + "Mealie could not import the recipe from the URL", + ), + ( + SERVICE_SET_RANDOM_MEALPLAN, + {ATTR_DATE: "2023-10-21", ATTR_ENTRY_TYPE: "lunch"}, + "random_mealplan", + MealieConnectionError, + HomeAssistantError, + "Error connecting to Mealie instance", + ), + ], +) +async def test_services_connection_error( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + service: str, + payload: dict[str, str], + function: str, + exception: Exception, + raised_exception: type[Exception], + message: str, +) -> None: + """Test a connection error in the services.""" + + await setup_integration(hass, mock_config_entry) + + getattr(mock_mealie_client, function).side_effect = exception + + with pytest.raises(raised_exception, match=message): + await hass.services.async_call( + DOMAIN, + service, + {ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id} | payload, blocking=True, return_response=True, ) -async def test_service_mealplan_without_entry( +@pytest.mark.parametrize( + ("service", "payload"), + [ + (SERVICE_GET_MEALPLAN, {}), + (SERVICE_GET_RECIPE, {ATTR_RECIPE_ID: "recipe_id"}), + (SERVICE_IMPORT_RECIPE, {ATTR_URL: "http://example.com"}), + ( + SERVICE_SET_RANDOM_MEALPLAN, + {ATTR_DATE: "2023-10-21", ATTR_ENTRY_TYPE: "lunch"}, + ), + ], +) +async def test_service_entry_availability( hass: HomeAssistant, + mock_mealie_client: AsyncMock, mock_config_entry: MockConfigEntry, + service: str, + payload: dict[str, str], ) -> None: - """Test the get_mealplan service without entry.""" + """Test the services without valid entry.""" mock_config_entry.add_to_hass(hass) mock_config_entry2 = MockConfigEntry(domain=DOMAIN) mock_config_entry2.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - with pytest.raises(ServiceValidationError): + with pytest.raises(ServiceValidationError, match="Mock Title is not loaded"): await hass.services.async_call( DOMAIN, - SERVICE_GET_MEALPLAN, - {ATTR_CONFIG_ENTRY_ID: mock_config_entry2.entry_id}, + service, + {ATTR_CONFIG_ENTRY_ID: mock_config_entry2.entry_id} | payload, blocking=True, return_response=True, ) - with pytest.raises(ServiceValidationError): + with pytest.raises( + ServiceValidationError, match='Integration "mealie" not found in registry' + ): await hass.services.async_call( DOMAIN, - SERVICE_GET_MEALPLAN, - {ATTR_CONFIG_ENTRY_ID: "bad-config_id"}, + service, + {ATTR_CONFIG_ENTRY_ID: "bad-config_id"} | payload, blocking=True, return_response=True, ) From 6de824e875d44dba0a49548c904edbdfbb84dea1 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 21 Jul 2024 18:50:00 +0200 Subject: [PATCH 1422/2411] Fix test RuntimeWarning for upb (#122325) --- tests/components/upb/test_config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py index 54aeb00e89a..efa6d60c344 100644 --- a/tests/components/upb/test_config_flow.py +++ b/tests/components/upb/test_config_flow.py @@ -1,7 +1,7 @@ """Test the UPB Control config flow.""" from asyncio import TimeoutError -from unittest.mock import AsyncMock, PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from homeassistant import config_entries from homeassistant.components.upb.const import DOMAIN @@ -18,6 +18,7 @@ def mocked_upb(sync_complete=True, config_ok=True): upb_mock = AsyncMock() type(upb_mock).network_id = PropertyMock(return_value="42") type(upb_mock).config_ok = PropertyMock(return_value=config_ok) + type(upb_mock).disconnect = MagicMock() if sync_complete: upb_mock.async_connect.side_effect = _upb_lib_connect return patch( From 890b54e36f3406a436ca5d6b40daa887dd0b7e7d Mon Sep 17 00:00:00 2001 From: GeoffAtHome Date: Sun, 21 Jul 2024 18:57:41 +0100 Subject: [PATCH 1423/2411] Add config flow to Genius hub (#116173) * Adding config flow * Fix setup issues. * Added test for config_flow * Refactor schemas. * Fixed ruff-format on const.py * Added geniushub-cleint to requirements_test_all.txt * Updates from review. * Correct multiple logger comment errors. * User menu rather than check box. * Correct logger messages. * Correct test_config_flow * Import config entry from YAML * Config flow integration * Refactor genius hub test_config_flow. * Improvements and simplification from code review. * Correct tests * Stop device being added twice. * Correct validate_input. * Changes to meet code review three week ago. * Fix Ruff undefined error * Update homeassistant/components/geniushub/config_flow.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/geniushub/config_flow.py Co-authored-by: Joost Lekkerkerker * Change case Cloud and Local to CLOUD and LOCAL. * More from code review * Fix * Fix * Update homeassistant/components/geniushub/strings.json --------- Co-authored-by: Joost Lekkerkerker --- CODEOWNERS | 1 + .../components/geniushub/__init__.py | 117 ++++- .../components/geniushub/binary_sensor.py | 20 +- homeassistant/components/geniushub/climate.py | 20 +- .../components/geniushub/config_flow.py | 136 +++++ homeassistant/components/geniushub/const.py | 19 + .../components/geniushub/manifest.json | 1 + homeassistant/components/geniushub/sensor.py | 14 +- .../components/geniushub/strings.json | 35 ++ homeassistant/components/geniushub/switch.py | 21 +- .../components/geniushub/water_heater.py | 22 +- homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- requirements_test_all.txt | 3 + tests/components/geniushub/__init__.py | 1 + tests/components/geniushub/conftest.py | 65 +++ .../components/geniushub/test_config_flow.py | 482 ++++++++++++++++++ 17 files changed, 869 insertions(+), 91 deletions(-) create mode 100644 homeassistant/components/geniushub/config_flow.py create mode 100644 homeassistant/components/geniushub/const.py create mode 100644 tests/components/geniushub/__init__.py create mode 100644 tests/components/geniushub/conftest.py create mode 100644 tests/components/geniushub/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index f79da235bb6..b382d63cf44 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -505,6 +505,7 @@ build.json @home-assistant/supervisor /homeassistant/components/generic_hygrostat/ @Shulyaka /tests/components/generic_hygrostat/ @Shulyaka /homeassistant/components/geniushub/ @manzanotti +/tests/components/geniushub/ @manzanotti /homeassistant/components/geo_json_events/ @exxamalte /tests/components/geo_json_events/ @exxamalte /homeassistant/components/geo_location/ @home-assistant/core diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 05afb121d44..84e835ac2bb 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -10,6 +10,8 @@ import aiohttp from geniushubclient import GeniusHub import voluptuous as vol +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, @@ -21,23 +23,29 @@ from homeassistant.const import ( Platform, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + ServiceCall, + callback, +) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN -DOMAIN = "geniushub" +_LOGGER = logging.getLogger(__name__) # temperature is repeated here, as it gives access to high-precision temps GH_ZONE_ATTRS = ["mode", "temperature", "type", "occupied", "override"] @@ -54,13 +62,15 @@ SCAN_INTERVAL = timedelta(seconds=60) MAC_ADDRESS_REGEXP = r"^([0-9A-F]{2}:){5}([0-9A-F]{2})$" -V1_API_SCHEMA = vol.Schema( +CLOUD_API_SCHEMA = vol.Schema( { vol.Required(CONF_TOKEN): cv.string, vol.Required(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP), } ) -V3_API_SCHEMA = vol.Schema( + + +LOCAL_API_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, @@ -68,8 +78,9 @@ V3_API_SCHEMA = vol.Schema( vol.Optional(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP), } ) + CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Any(V3_API_SCHEMA, V1_API_SCHEMA)}, extra=vol.ALLOW_EXTRA + {DOMAIN: vol.Any(LOCAL_API_SCHEMA, CLOUD_API_SCHEMA)}, extra=vol.ALLOW_EXTRA ) ATTR_ZONE_MODE = "mode" @@ -106,20 +117,78 @@ PLATFORMS = ( ) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def _async_import(hass: HomeAssistant, base_config: ConfigType) -> None: + """Import a config entry from configuration.yaml.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=base_config[DOMAIN], + ) + if ( + result["type"] is FlowResultType.CREATE_ENTRY + or result["reason"] == "already_configured" + ): + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Genius Hub", + }, + ) + return + async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_import_issue_{result['reason']}", + breaks_in_ha_version="2024.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Genius Hub", + }, + ) + + +async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool: + """Set up a Genius Hub system.""" + if DOMAIN in base_config: + hass.async_create_task(_async_import(hass, base_config)) + return True + + +type GeniusHubConfigEntry = ConfigEntry[GeniusBroker] + + +async def async_setup_entry(hass: HomeAssistant, entry: GeniusHubConfigEntry) -> bool: """Create a Genius Hub system.""" - hass.data[DOMAIN] = {} - kwargs = dict(config[DOMAIN]) - if CONF_HOST in kwargs: - args = (kwargs.pop(CONF_HOST),) + session = async_get_clientsession(hass) + if CONF_HOST in entry.data: + client = GeniusHub( + entry.data[CONF_HOST], + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + session=session, + ) else: - args = (kwargs.pop(CONF_TOKEN),) - hub_uid = kwargs.pop(CONF_MAC, None) + client = GeniusHub(entry.data[CONF_TOKEN], session=session) - client = GeniusHub(*args, **kwargs, session=async_get_clientsession(hass)) + unique_id = entry.unique_id or entry.entry_id - broker = hass.data[DOMAIN]["broker"] = GeniusBroker(hass, client, hub_uid) + broker = entry.runtime_data = GeniusBroker( + hass, client, entry.data.get(CONF_MAC, unique_id) + ) try: await client.update() @@ -130,11 +199,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_track_time_interval(hass, broker.async_update, SCAN_INTERVAL) - for platform in PLATFORMS: - hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config)) - setup_service_functions(hass, broker) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True @@ -175,20 +243,13 @@ def setup_service_functions(hass: HomeAssistant, broker): class GeniusBroker: """Container for geniushub client and data.""" - def __init__( - self, hass: HomeAssistant, client: GeniusHub, hub_uid: str | None - ) -> None: + def __init__(self, hass: HomeAssistant, client: GeniusHub, hub_uid: str) -> None: """Initialize the geniushub client.""" self.hass = hass self.client = client - self._hub_uid = hub_uid + self.hub_uid = hub_uid self._connect_error = False - @property - def hub_uid(self) -> str: - """Return the Hub UID (MAC address).""" - return self._hub_uid if self._hub_uid is not None else self.client.uid - async def async_update(self, now, **kwargs) -> None: """Update the geniushub client's data.""" try: diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index f078bb4b363..2d6acf0c955 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -5,33 +5,27 @@ from __future__ import annotations 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 DOMAIN, GeniusDevice +from . import GeniusDevice, GeniusHubConfigEntry GH_STATE_ATTR = "outputOnOff" GH_TYPE = "Receiver" -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: GeniusHubConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Genius Hub sensor entities.""" - if discovery_info is None: - return + """Set up the Genius Hub binary sensor entities.""" - broker = hass.data[DOMAIN]["broker"] + broker = entry.runtime_data - switches = [ + async_add_entities( GeniusBinarySensor(broker, d, GH_STATE_ATTR) for d in broker.client.device_objs if GH_TYPE in d.data["type"] - ] - - async_add_entities(switches, update_before_add=True) + ) class GeniusBinarySensor(GeniusDevice, BinarySensorEntity): diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index 02038ced198..ea2a79be767 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -12,9 +12,8 @@ from homeassistant.components.climate import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, GeniusHeatingZone +from . import GeniusHeatingZone, GeniusHubConfigEntry # GeniusHub Zones support: Off, Timer, Override/Boost, Footprint & Linked modes HA_HVAC_TO_GH = {HVACMode.OFF: "off", HVACMode.HEAT: "timer"} @@ -26,24 +25,19 @@ GH_PRESET_TO_HA = {v: k for k, v in HA_PRESET_TO_GH.items()} GH_ZONES = ["radiator", "wet underfloor"] -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: GeniusHubConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Genius Hub climate entities.""" - if discovery_info is None: - return - broker = hass.data[DOMAIN]["broker"] + broker = entry.runtime_data async_add_entities( - [ - GeniusClimateZone(broker, z) - for z in broker.client.zone_objs - if z.data.get("type") in GH_ZONES - ] + GeniusClimateZone(broker, z) + for z in broker.client.zone_objs + if z.data.get("type") in GH_ZONES ) diff --git a/homeassistant/components/geniushub/config_flow.py b/homeassistant/components/geniushub/config_flow.py new file mode 100644 index 00000000000..5f026c91ee1 --- /dev/null +++ b/homeassistant/components/geniushub/config_flow.py @@ -0,0 +1,136 @@ +"""Config flow for Geniushub integration.""" + +from __future__ import annotations + +from http import HTTPStatus +import logging +import socket +from typing import Any + +import aiohttp +from geniushubclient import GeniusService +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +CLOUD_API_SCHEMA = vol.Schema( + { + vol.Required(CONF_TOKEN): str, + } +) + + +LOCAL_API_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class GeniusHubConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Geniushub.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """User config step for determine cloud or local.""" + return self.async_show_menu( + step_id="user", + menu_options=["local_api", "cloud_api"], + ) + + async def async_step_local_api( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Version 3 configuration.""" + errors: dict[str, str] = {} + if user_input is not None: + self._async_abort_entries_match( + { + CONF_HOST: user_input[CONF_HOST], + CONF_USERNAME: user_input[CONF_USERNAME], + } + ) + service = GeniusService( + user_input[CONF_HOST], + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + session=async_get_clientsession(self.hass), + ) + try: + response = await service.request("GET", "auth/release") + except socket.gaierror: + errors["base"] = "invalid_host" + except aiohttp.ClientResponseError as err: + if err.status == HTTPStatus.UNAUTHORIZED: + errors["base"] = "invalid_auth" + else: + errors["base"] = "invalid_host" + except (TimeoutError, aiohttp.ClientConnectionError): + errors["base"] = "cannot_connect" + except Exception: # noqa: BLE001 + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(response["data"]["UID"]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_HOST], data=user_input + ) + + return self.async_show_form( + step_id="local_api", errors=errors, data_schema=LOCAL_API_SCHEMA + ) + + async def async_step_cloud_api( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Version 1 configuration.""" + errors: dict[str, str] = {} + if user_input is not None: + self._async_abort_entries_match(user_input) + service = GeniusService( + user_input[CONF_TOKEN], session=async_get_clientsession(self.hass) + ) + try: + await service.request("GET", "version") + except aiohttp.ClientResponseError as err: + if err.status == HTTPStatus.UNAUTHORIZED: + errors["base"] = "invalid_auth" + else: + errors["base"] = "invalid_host" + except socket.gaierror: + errors["base"] = "invalid_host" + except (TimeoutError, aiohttp.ClientConnectionError): + errors["base"] = "cannot_connect" + except Exception: # noqa: BLE001 + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title="Genius hub", data=user_input) + + return self.async_show_form( + step_id="cloud_api", errors=errors, data_schema=CLOUD_API_SCHEMA + ) + + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: + """Import the yaml config.""" + if CONF_HOST in user_input: + result = await self.async_step_local_api(user_input) + else: + result = await self.async_step_cloud_api(user_input) + if result["type"] is FlowResultType.FORM: + assert result["errors"] + return self.async_abort(reason=result["errors"]["base"]) + return result diff --git a/homeassistant/components/geniushub/const.py b/homeassistant/components/geniushub/const.py new file mode 100644 index 00000000000..4601eca5f9b --- /dev/null +++ b/homeassistant/components/geniushub/const.py @@ -0,0 +1,19 @@ +"""Constants for Genius Hub.""" + +from datetime import timedelta + +from homeassistant.const import Platform + +DOMAIN = "geniushub" + +SCAN_INTERVAL = timedelta(seconds=60) + +SENSOR_PREFIX = "Genius" + +PLATFORMS = ( + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.SENSOR, + Platform.SWITCH, + Platform.WATER_HEATER, +) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 28079293821..c6444bdb95d 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -2,6 +2,7 @@ "domain": "geniushub", "name": "Genius Hub", "codeowners": ["@manzanotti"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geniushub", "iot_class": "local_polling", "loggers": ["geniushubclient"], diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index f5cd8625e8b..ee65e679498 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -9,10 +9,9 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import PERCENTAGE 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 -from . import DOMAIN, GeniusDevice, GeniusEntity +from . import GeniusDevice, GeniusEntity, GeniusHubConfigEntry GH_STATE_ATTR = "batteryLevel" @@ -23,17 +22,14 @@ GH_LEVEL_MAPPING = { } -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: GeniusHubConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Genius Hub sensor entities.""" - if discovery_info is None: - return - broker = hass.data[DOMAIN]["broker"] + broker = entry.runtime_data entities: list[GeniusBattery | GeniusIssue] = [ GeniusBattery(broker, d, GH_STATE_ATTR) @@ -42,7 +38,7 @@ async def async_setup_platform( ] entities.extend([GeniusIssue(broker, i) for i in list(GH_LEVEL_MAPPING)]) - async_add_entities(entities, update_before_add=True) + async_add_entities(entities) class GeniusBattery(GeniusDevice, SensorEntity): diff --git a/homeassistant/components/geniushub/strings.json b/homeassistant/components/geniushub/strings.json index ac057f5c639..faf5011d752 100644 --- a/homeassistant/components/geniushub/strings.json +++ b/homeassistant/components/geniushub/strings.json @@ -1,4 +1,39 @@ { + "config": { + "step": { + "user": { + "title": "Genius Hub configuration", + "menu_options": { + "local_api": "Local: IP address and user credentials", + "cloud_api": "Cloud: API token" + } + }, + "local_api": { + "title": "Genius Hub local configuration", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + } + }, + "cloud_api": { + "title": "Genius Hub cloud configuration", + "data": { + "token": "[%key:common::config_flow::data::access_token%]" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, + "services": { "set_zone_mode": { "name": "Set zone mode", diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index 85f7f1bb03a..2fffbddde01 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -11,9 +11,9 @@ from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolDictType +from homeassistant.helpers.typing import VolDictType -from . import ATTR_DURATION, DOMAIN, GeniusZone +from . import ATTR_DURATION, GeniusHubConfigEntry, GeniusZone GH_ON_OFF_ZONE = "on / off" @@ -27,24 +27,19 @@ SET_SWITCH_OVERRIDE_SCHEMA: VolDictType = { } -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: GeniusHubConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Genius Hub switch entities.""" - if discovery_info is None: - return - broker = hass.data[DOMAIN]["broker"] + broker = entry.runtime_data async_add_entities( - [ - GeniusSwitch(broker, z) - for z in broker.client.zone_objs - if z.data.get("type") == GH_ON_OFF_ZONE - ] + GeniusSwitch(broker, z) + for z in broker.client.zone_objs + if z.data.get("type") == GH_ON_OFF_ZONE ) # Register custom services diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index f17560ebc62..6d3da570547 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -9,9 +9,8 @@ from homeassistant.components.water_heater import ( from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, GeniusHeatingZone +from . import GeniusHeatingZone, GeniusHubConfigEntry STATE_AUTO = "auto" STATE_MANUAL = "manual" @@ -33,24 +32,19 @@ GH_STATE_TO_HA = { GH_HEATERS = ["hot water temperature"] -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: GeniusHubConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Genius Hub water_heater entities.""" - if discovery_info is None: - return + """Set up the Genius Hub water heater entities.""" - broker = hass.data[DOMAIN]["broker"] + broker = entry.runtime_data async_add_entities( - [ - GeniusWaterHeater(broker, z) - for z in broker.client.zone_objs - if z.data.get("type") in GH_HEATERS - ] + GeniusWaterHeater(broker, z) + for z in broker.client.zone_objs + if z.data.get("type") in GH_HEATERS ) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b8614705823..96875e247f1 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -202,6 +202,7 @@ FLOWS = { "gardena_bluetooth", "gdacs", "generic", + "geniushub", "geo_json_events", "geocaching", "geofency", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 84d69c868db..f60028240fb 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2124,7 +2124,7 @@ "geniushub": { "name": "Genius Hub", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "geo_json_events": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d1562f340a..449c95e88e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -773,6 +773,9 @@ gassist-text==0.0.11 # homeassistant.components.google gcal-sync==6.1.4 +# homeassistant.components.geniushub +geniushub-client==0.7.1 + # homeassistant.components.geocaching geocachingapi==0.2.1 diff --git a/tests/components/geniushub/__init__.py b/tests/components/geniushub/__init__.py new file mode 100644 index 00000000000..15886486e38 --- /dev/null +++ b/tests/components/geniushub/__init__.py @@ -0,0 +1 @@ +"""Tests for the geniushub integration.""" diff --git a/tests/components/geniushub/conftest.py b/tests/components/geniushub/conftest.py new file mode 100644 index 00000000000..125f1cfa80c --- /dev/null +++ b/tests/components/geniushub/conftest.py @@ -0,0 +1,65 @@ +"""GeniusHub tests configuration.""" + +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from homeassistant.components.geniushub.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME + +from tests.common import MockConfigEntry +from tests.components.smhi.common import AsyncMock + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.geniushub.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_geniushub_client() -> Generator[AsyncMock]: + """Mock a GeniusHub client.""" + with patch( + "homeassistant.components.geniushub.config_flow.GeniusService", + autospec=True, + ) as mock_client: + client = mock_client.return_value + client.request.return_value = { + "data": { + "UID": "aa:bb:cc:dd:ee:ff", + } + } + yield client + + +@pytest.fixture +def mock_local_config_entry() -> MockConfigEntry: + """Mock a local config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="aa:bb:cc:dd:ee:ff", + data={ + CONF_HOST: "10.0.0.131", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + unique_id="aa:bb:cc:dd:ee:ff", + ) + + +@pytest.fixture +def mock_cloud_config_entry() -> MockConfigEntry: + """Mock a cloud config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Genius hub", + data={ + CONF_TOKEN: "abcdef", + }, + ) diff --git a/tests/components/geniushub/test_config_flow.py b/tests/components/geniushub/test_config_flow.py new file mode 100644 index 00000000000..9234e03e35a --- /dev/null +++ b/tests/components/geniushub/test_config_flow.py @@ -0,0 +1,482 @@ +"""Test the Geniushub config flow.""" + +from http import HTTPStatus +import socket +from typing import Any +from unittest.mock import AsyncMock + +from aiohttp import ClientConnectionError, ClientResponseError +import pytest + +from homeassistant.components.geniushub import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_PASSWORD, + CONF_TOKEN, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_local_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_geniushub_client: AsyncMock, +) -> None: + """Test full local flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "local_api"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local_api" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "10.0.0.130" + assert result["data"] == { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + } + assert result["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (socket.gaierror, "invalid_host"), + ( + ClientResponseError(AsyncMock(), (), status=HTTPStatus.UNAUTHORIZED), + "invalid_auth", + ), + ( + ClientResponseError(AsyncMock(), (), status=HTTPStatus.NOT_FOUND), + "invalid_host", + ), + (TimeoutError, "cannot_connect"), + (ClientConnectionError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_local_flow_exceptions( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_geniushub_client: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test local flow exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "local_api"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local_api" + + mock_geniushub_client.request.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_geniushub_client.request.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_local_duplicate_data( + hass: HomeAssistant, + mock_geniushub_client: AsyncMock, + mock_local_config_entry: MockConfigEntry, +) -> None: + """Test local flow aborts on duplicate data.""" + mock_local_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "local_api"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local_api" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_local_duplicate_mac( + hass: HomeAssistant, + mock_geniushub_client: AsyncMock, + mock_local_config_entry: MockConfigEntry, +) -> None: + """Test local flow aborts on duplicate MAC.""" + mock_local_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "local_api"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local_api" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "10.0.0.131", + CONF_USERNAME: "test-username1", + CONF_PASSWORD: "test-password", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_full_cloud_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_geniushub_client: AsyncMock, +) -> None: + """Test full cloud flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "cloud_api"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "cloud_api" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_TOKEN: "abcdef", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Genius hub" + assert result["data"] == { + CONF_TOKEN: "abcdef", + } + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (socket.gaierror, "invalid_host"), + ( + ClientResponseError(AsyncMock(), (), status=HTTPStatus.UNAUTHORIZED), + "invalid_auth", + ), + ( + ClientResponseError(AsyncMock(), (), status=HTTPStatus.NOT_FOUND), + "invalid_host", + ), + (TimeoutError, "cannot_connect"), + (ClientConnectionError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_cloud_flow_exceptions( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_geniushub_client: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test cloud flow exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "cloud_api"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "cloud_api" + + mock_geniushub_client.request.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_TOKEN: "abcdef", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_geniushub_client.request.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_TOKEN: "abcdef", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_cloud_duplicate( + hass: HomeAssistant, + mock_geniushub_client: AsyncMock, + mock_cloud_config_entry: MockConfigEntry, +) -> None: + """Test cloud flow aborts on duplicate data.""" + mock_cloud_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "cloud_api"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "cloud_api" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_TOKEN: "abcdef", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("data"), + [ + { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + }, + ], +) +async def test_import_local_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_geniushub_client: AsyncMock, + data: dict[str, Any], +) -> None: + """Test full local import flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=data, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "10.0.0.130" + assert result["data"] == data + assert result["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +@pytest.mark.parametrize( + ("data"), + [ + { + CONF_TOKEN: "abcdef", + }, + { + CONF_TOKEN: "abcdef", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + }, + ], +) +async def test_import_cloud_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_geniushub_client: AsyncMock, + data: dict[str, Any], +) -> None: + """Test full cloud import flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=data, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Genius hub" + assert result["data"] == data + + +@pytest.mark.parametrize( + ("data"), + [ + { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + }, + { + CONF_TOKEN: "abcdef", + }, + { + CONF_TOKEN: "abcdef", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + }, + ], +) +@pytest.mark.parametrize( + ("exception", "reason"), + [ + (socket.gaierror, "invalid_host"), + ( + ClientResponseError(AsyncMock(), (), status=HTTPStatus.UNAUTHORIZED), + "invalid_auth", + ), + ( + ClientResponseError(AsyncMock(), (), status=HTTPStatus.NOT_FOUND), + "invalid_host", + ), + (TimeoutError, "cannot_connect"), + (ClientConnectionError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_import_flow_exceptions( + hass: HomeAssistant, + mock_geniushub_client: AsyncMock, + data: dict[str, Any], + exception: Exception, + reason: str, +) -> None: + """Test import flow exceptions.""" + mock_geniushub_client.request.side_effect = exception + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=data, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == reason + + +@pytest.mark.parametrize( + ("data"), + [ + { + CONF_HOST: "10.0.0.130", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + { + CONF_HOST: "10.0.0.131", + CONF_USERNAME: "test-username1", + CONF_PASSWORD: "test-password", + }, + ], +) +async def test_import_flow_local_duplicate( + hass: HomeAssistant, + mock_geniushub_client: AsyncMock, + mock_local_config_entry: MockConfigEntry, + data: dict[str, Any], +) -> None: + """Test import flow aborts on local duplicate data.""" + mock_local_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=data, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_import_flow_cloud_duplicate( + hass: HomeAssistant, + mock_geniushub_client: AsyncMock, + mock_cloud_config_entry: MockConfigEntry, +) -> None: + """Test import flow aborts on cloud duplicate data.""" + mock_cloud_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_TOKEN: "abcdef", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" From d418a4085646404b5c44a99d6133d7ecbe32aab4 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 21 Jul 2024 20:01:48 +0200 Subject: [PATCH 1424/2411] Create, update and delete KNX entities from UI / WS-commands (#104079) * knx entity CRUD - initial commit - switch * platform dependent schema * coerce empty GA-lists to None * read entity configuration from WS * use entity_id instead of unique_id for lookup * Add device support * Rename KNXEntityStore to KNXConfigStore * fix test after rename * Send schema options for creating / editing entities * Return entity_id after entity creation * remove device_class config in favour of more-info-dialog settings * refactor group address schema for custom selector * Rename GA keys and remove invalid keys from schema * fix rebase * Fix deleting devices and their entities * Validate entity schema in extra step - return validation infos * Use exception to signal validation error; return validated data * Forward validation result when editing entities * Get proper validation error message for optional GAs * Add entity validation only WS command * use ulid instead of uuid * Fix error handling for edit unknown entity * Remove unused optional group address sets from validated schema * Add optional dpt field for ga_schema * Move knx config things to sub-key * Add light platform * async_forward_entry_setups only once * Test crate and remove devices * Test removing entities of a removed device * Test entity creation and storage * Test deleting entities * Test unsuccessful entity creation * Test updating entity data * Test get entity config * Test validate entity * Update entity data by entity_id instead of unique_id * Remove unnecessary uid unique check * remove schema_options * test fixture for entity creation * clean up group address schema class can be used to add custom serializer later * Revert: Add light platfrom * remove unused optional_ga_schema * Test GASelector * lint tests * Review * group entities before adding * fix / ignore mypy * always has_entity_name * Entity name: check for empty string when no device * use constants instead of strings in schema * Fix mypy errors for voluptuous schemas --------- Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/knx/__init__.py | 49 ++- homeassistant/components/knx/config_flow.py | 2 +- homeassistant/components/knx/const.py | 13 +- .../knx/{helpers => storage}/__init__.py | 0 .../components/knx/storage/config_store.py | 145 ++++++ homeassistant/components/knx/storage/const.py | 14 + .../knx/storage/entity_store_schema.py | 94 ++++ .../knx/storage/entity_store_validation.py | 69 +++ .../knx/{helpers => storage}/keyring.py | 0 .../components/knx/storage/knx_selector.py | 81 ++++ homeassistant/components/knx/switch.py | 114 ++++- homeassistant/components/knx/validation.py | 15 + homeassistant/components/knx/websocket.py | 220 ++++++++++ tests/components/knx/README.md | 3 +- tests/components/knx/__init__.py | 6 + tests/components/knx/conftest.py | 75 +++- .../components/knx/fixtures/config_store.json | 29 ++ tests/components/knx/test_config_flow.py | 4 +- tests/components/knx/test_config_store.py | 412 ++++++++++++++++++ tests/components/knx/test_device.py | 77 ++++ tests/components/knx/test_interface_device.py | 31 +- tests/components/knx/test_knx_selectors.py | 122 ++++++ tests/components/knx/test_switch.py | 27 +- tests/components/knx/test_websocket.py | 9 +- 24 files changed, 1557 insertions(+), 54 deletions(-) rename homeassistant/components/knx/{helpers => storage}/__init__.py (100%) create mode 100644 homeassistant/components/knx/storage/config_store.py create mode 100644 homeassistant/components/knx/storage/const.py create mode 100644 homeassistant/components/knx/storage/entity_store_schema.py create mode 100644 homeassistant/components/knx/storage/entity_store_validation.py rename homeassistant/components/knx/{helpers => storage}/keyring.py (100%) create mode 100644 homeassistant/components/knx/storage/knx_selector.py create mode 100644 tests/components/knx/fixtures/config_store.json create mode 100644 tests/components/knx/test_config_store.py create mode 100644 tests/components/knx/test_device.py create mode 100644 tests/components/knx/test_knx_selectors.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 9c64b4e1b31..f7e9b161962 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -31,6 +31,7 @@ from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import ConfigType @@ -62,7 +63,8 @@ from .const import ( DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, - SUPPORTED_PLATFORMS, + SUPPORTED_PLATFORMS_UI, + SUPPORTED_PLATFORMS_YAML, TELEGRAM_LOG_DEFAULT, ) from .device import KNXInterfaceDevice @@ -90,6 +92,7 @@ from .schema import ( WeatherSchema, ) from .services import register_knx_services +from .storage.config_store import KNXConfigStore from .telegrams import STORAGE_KEY as TELEGRAMS_STORAGE_KEY, Telegrams from .websocket import register_panel @@ -190,10 +193,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: knx_module.exposures.append( create_knx_exposure(hass, knx_module.xknx, expose_config) ) - # always forward sensor for system entities (telegram counter, etc.) - platforms = {platform for platform in SUPPORTED_PLATFORMS if platform in config} - platforms.add(Platform.SENSOR) - await hass.config_entries.async_forward_entry_setups(entry, platforms) + await hass.config_entries.async_forward_entry_setups( + entry, + { + Platform.SENSOR, # always forward sensor for system entities (telegram counter, etc.) + *SUPPORTED_PLATFORMS_UI, # forward all platforms that support UI entity management + *{ # forward yaml-only managed platforms on demand + platform for platform in SUPPORTED_PLATFORMS_YAML if platform in config + }, + }, + ) # set up notify service for backwards compatibility - remove 2024.11 if NotifySchema.PLATFORM in config: @@ -220,15 +229,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms( entry, - [ + { Platform.SENSOR, # always unload system entities (telegram counter, etc.) - *[ + *SUPPORTED_PLATFORMS_UI, # unload all platforms that support UI entity management + *{ # unload yaml-only managed platforms if configured platform - for platform in SUPPORTED_PLATFORMS + for platform in SUPPORTED_PLATFORMS_YAML if platform in hass.data[DATA_KNX_CONFIG] - and platform is not Platform.SENSOR - ], - ], + }, + }, ) if unload_ok: await knx_module.stop() @@ -263,6 +272,22 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: await hass.async_add_executor_job(remove_files, storage_dir, knxkeys_filename) +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove a config entry from a device.""" + knx_module: KNXModule = hass.data[DOMAIN] + if not device_entry.identifiers.isdisjoint( + knx_module.interface_device.device_info["identifiers"] + ): + # can not remove interface device + return False + for entity in knx_module.config_store.get_entity_entries(): + if entity.device_id == device_entry.id: + await knx_module.config_store.delete_entity(entity.entity_id) + return True + + class KNXModule: """Representation of KNX Object.""" @@ -278,6 +303,7 @@ class KNXModule: self.entry = entry self.project = KNXProject(hass=hass, entry=entry) + self.config_store = KNXConfigStore(hass=hass, entry=entry) self.xknx = XKNX( connection_config=self.connection_config(), @@ -309,6 +335,7 @@ class KNXModule: async def start(self) -> None: """Start XKNX object. Connect to tunneling or Routing device.""" await self.project.load_project() + await self.config_store.load_data() await self.telegrams.load_history() await self.xknx.start() diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 7d6443bd9ef..2fc1f49800c 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -62,7 +62,7 @@ from .const import ( TELEGRAM_LOG_MAX, KNXConfigEntryData, ) -from .helpers.keyring import DEFAULT_KNX_KEYRING_FILENAME, save_uploaded_knxkeys_file +from .storage.keyring import DEFAULT_KNX_KEYRING_FILENAME, save_uploaded_knxkeys_file from .validation import ia_validator, ip_v4_validator CONF_KNX_GATEWAY: Final = "gateway" diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 6cec901adc7..c63a31f0bb5 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -127,12 +127,13 @@ class KNXConfigEntryData(TypedDict, total=False): class ColorTempModes(Enum): """Color temperature modes for config validation.""" - ABSOLUTE = "DPT-7.600" - ABSOLUTE_FLOAT = "DPT-9" - RELATIVE = "DPT-5.001" + # YAML uses Enum.name (with vol.Upper), UI uses Enum.value for lookup + ABSOLUTE = "7.600" + ABSOLUTE_FLOAT = "9" + RELATIVE = "5.001" -SUPPORTED_PLATFORMS: Final = [ +SUPPORTED_PLATFORMS_YAML: Final = { Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, @@ -150,7 +151,9 @@ SUPPORTED_PLATFORMS: Final = [ Platform.TEXT, Platform.TIME, Platform.WEATHER, -] +} + +SUPPORTED_PLATFORMS_UI: Final = {Platform.SWITCH} # Map KNX controller modes to HA modes. This list might not be complete. CONTROLLER_MODES: Final = { diff --git a/homeassistant/components/knx/helpers/__init__.py b/homeassistant/components/knx/storage/__init__.py similarity index 100% rename from homeassistant/components/knx/helpers/__init__.py rename to homeassistant/components/knx/storage/__init__.py diff --git a/homeassistant/components/knx/storage/config_store.py b/homeassistant/components/knx/storage/config_store.py new file mode 100644 index 00000000000..7ea61e1dd3e --- /dev/null +++ b/homeassistant/components/knx/storage/config_store.py @@ -0,0 +1,145 @@ +"""KNX entity configuration store.""" + +from collections.abc import Callable +import logging +from typing import TYPE_CHECKING, Any, Final, TypedDict + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PLATFORM, Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.storage import Store +from homeassistant.util.ulid import ulid_now + +from ..const import DOMAIN +from .const import CONF_DATA + +if TYPE_CHECKING: + from ..knx_entity import KnxEntity + +_LOGGER = logging.getLogger(__name__) + +STORAGE_VERSION: Final = 1 +STORAGE_KEY: Final = f"{DOMAIN}/config_store.json" + +KNXPlatformStoreModel = dict[str, dict[str, Any]] # unique_id: configuration +KNXEntityStoreModel = dict[ + str, KNXPlatformStoreModel +] # platform: KNXPlatformStoreModel + + +class KNXConfigStoreModel(TypedDict): + """Represent KNX configuration store data.""" + + entities: KNXEntityStoreModel + + +class KNXConfigStore: + """Manage KNX config store data.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + ) -> None: + """Initialize config store.""" + self.hass = hass + self._store = Store[KNXConfigStoreModel](hass, STORAGE_VERSION, STORAGE_KEY) + self.data = KNXConfigStoreModel(entities={}) + + # entities and async_add_entity are filled by platform setups + self.entities: dict[str, KnxEntity] = {} # unique_id as key + self.async_add_entity: dict[ + Platform, Callable[[str, dict[str, Any]], None] + ] = {} + + async def load_data(self) -> None: + """Load config store data from storage.""" + if data := await self._store.async_load(): + self.data = KNXConfigStoreModel(**data) + _LOGGER.debug( + "Loaded KNX config data from storage. %s entity platforms", + len(self.data["entities"]), + ) + + async def create_entity( + self, platform: Platform, data: dict[str, Any] + ) -> str | None: + """Create a new entity.""" + if platform not in self.async_add_entity: + raise ConfigStoreException(f"Entity platform not ready: {platform}") + unique_id = f"knx_es_{ulid_now()}" + self.async_add_entity[platform](unique_id, data) + # store data after entity was added to be sure config didn't raise exceptions + self.data["entities"].setdefault(platform, {})[unique_id] = data + await self._store.async_save(self.data) + + entity_registry = er.async_get(self.hass) + return entity_registry.async_get_entity_id(platform, DOMAIN, unique_id) + + @callback + def get_entity_config(self, entity_id: str) -> dict[str, Any]: + """Return KNX entity configuration.""" + entity_registry = er.async_get(self.hass) + if (entry := entity_registry.async_get(entity_id)) is None: + raise ConfigStoreException(f"Entity not found: {entity_id}") + try: + return { + CONF_PLATFORM: entry.domain, + CONF_DATA: self.data["entities"][entry.domain][entry.unique_id], + } + except KeyError as err: + raise ConfigStoreException(f"Entity data not found: {entity_id}") from err + + async def update_entity( + self, platform: Platform, entity_id: str, data: dict[str, Any] + ) -> None: + """Update an existing entity.""" + if platform not in self.async_add_entity: + raise ConfigStoreException(f"Entity platform not ready: {platform}") + entity_registry = er.async_get(self.hass) + if (entry := entity_registry.async_get(entity_id)) is None: + raise ConfigStoreException(f"Entity not found: {entity_id}") + unique_id = entry.unique_id + if ( + platform not in self.data["entities"] + or unique_id not in self.data["entities"][platform] + ): + raise ConfigStoreException( + f"Entity not found in storage: {entity_id} - {unique_id}" + ) + await self.entities.pop(unique_id).async_remove() + self.async_add_entity[platform](unique_id, data) + # store data after entity is added to make sure config doesn't raise exceptions + self.data["entities"][platform][unique_id] = data + await self._store.async_save(self.data) + + async def delete_entity(self, entity_id: str) -> None: + """Delete an existing entity.""" + entity_registry = er.async_get(self.hass) + if (entry := entity_registry.async_get(entity_id)) is None: + raise ConfigStoreException(f"Entity not found: {entity_id}") + try: + del self.data["entities"][entry.domain][entry.unique_id] + except KeyError as err: + raise ConfigStoreException( + f"Entity not found in {entry.domain}: {entry.unique_id}" + ) from err + try: + del self.entities[entry.unique_id] + except KeyError: + _LOGGER.warning("Entity not initialized when deleted: %s", entity_id) + entity_registry.async_remove(entity_id) + await self._store.async_save(self.data) + + def get_entity_entries(self) -> list[er.RegistryEntry]: + """Get entity_ids of all configured entities by platform.""" + return [ + entity.registry_entry + for entity in self.entities.values() + if entity.registry_entry is not None + ] + + +class ConfigStoreException(Exception): + """KNX config store exception.""" diff --git a/homeassistant/components/knx/storage/const.py b/homeassistant/components/knx/storage/const.py new file mode 100644 index 00000000000..6453b77ed3b --- /dev/null +++ b/homeassistant/components/knx/storage/const.py @@ -0,0 +1,14 @@ +"""Constants used in KNX config store.""" + +from typing import Final + +CONF_DATA: Final = "data" +CONF_ENTITY: Final = "entity" +CONF_DEVICE_INFO: Final = "device_info" +CONF_GA_WRITE: Final = "write" +CONF_GA_STATE: Final = "state" +CONF_GA_PASSIVE: Final = "passive" +CONF_DPT: Final = "dpt" + + +CONF_GA_SWITCH: Final = "ga_switch" diff --git a/homeassistant/components/knx/storage/entity_store_schema.py b/homeassistant/components/knx/storage/entity_store_schema.py new file mode 100644 index 00000000000..e2f9e786300 --- /dev/null +++ b/homeassistant/components/knx/storage/entity_store_schema.py @@ -0,0 +1,94 @@ +"""KNX entity store schema.""" + +import voluptuous as vol + +from homeassistant.const import ( + CONF_ENTITY_CATEGORY, + CONF_ENTITY_ID, + CONF_NAME, + CONF_PLATFORM, + Platform, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA +from homeassistant.helpers.typing import VolDictType, VolSchemaType + +from ..const import ( + CONF_INVERT, + CONF_RESPOND_TO_READ, + CONF_SYNC_STATE, + DOMAIN, + SUPPORTED_PLATFORMS_UI, +) +from ..validation import sync_state_validator +from .const import CONF_DATA, CONF_DEVICE_INFO, CONF_ENTITY, CONF_GA_SWITCH +from .knx_selector import GASelector + +BASE_ENTITY_SCHEMA = vol.All( + { + vol.Optional(CONF_NAME, default=None): vol.Maybe(str), + vol.Optional(CONF_DEVICE_INFO, default=None): vol.Maybe(str), + vol.Optional(CONF_ENTITY_CATEGORY, default=None): vol.Any( + ENTITY_CATEGORIES_SCHEMA, vol.SetTo(None) + ), + }, + vol.Any( + vol.Schema( + { + vol.Required(CONF_NAME): vol.All(str, vol.IsTrue()), + }, + extra=vol.ALLOW_EXTRA, + ), + vol.Schema( + { + vol.Required(CONF_DEVICE_INFO): str, + }, + extra=vol.ALLOW_EXTRA, + ), + msg="One of `Device` or `Name` is required", + ), +) + +SWITCH_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY): BASE_ENTITY_SCHEMA, + vol.Required(DOMAIN): { + vol.Optional(CONF_INVERT, default=False): bool, + vol.Required(CONF_GA_SWITCH): GASelector(write_required=True), + vol.Optional(CONF_RESPOND_TO_READ, default=False): bool, + vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, + }, + } +) + + +ENTITY_STORE_DATA_SCHEMA: VolSchemaType = vol.All( + vol.Schema( + { + vol.Required(CONF_PLATFORM): vol.All( + vol.Coerce(Platform), + vol.In(SUPPORTED_PLATFORMS_UI), + ), + vol.Required(CONF_DATA): dict, + }, + extra=vol.ALLOW_EXTRA, + ), + cv.key_value_schemas( + CONF_PLATFORM, + { + Platform.SWITCH: vol.Schema( + {vol.Required(CONF_DATA): SWITCH_SCHEMA}, extra=vol.ALLOW_EXTRA + ), + }, + ), +) + +CREATE_ENTITY_BASE_SCHEMA: VolDictType = { + vol.Required(CONF_PLATFORM): str, + vol.Required(CONF_DATA): dict, # validated by ENTITY_STORE_DATA_SCHEMA for platform +} + +UPDATE_ENTITY_BASE_SCHEMA = { + vol.Required(CONF_ENTITY_ID): str, + **CREATE_ENTITY_BASE_SCHEMA, +} diff --git a/homeassistant/components/knx/storage/entity_store_validation.py b/homeassistant/components/knx/storage/entity_store_validation.py new file mode 100644 index 00000000000..e9997bd9f1a --- /dev/null +++ b/homeassistant/components/knx/storage/entity_store_validation.py @@ -0,0 +1,69 @@ +"""KNX Entity Store Validation.""" + +from typing import Literal, TypedDict + +import voluptuous as vol + +from .entity_store_schema import ENTITY_STORE_DATA_SCHEMA + + +class _ErrorDescription(TypedDict): + path: list[str] | None + error_message: str + error_class: str + + +class EntityStoreValidationError(TypedDict): + """Negative entity store validation result.""" + + success: Literal[False] + error_base: str + errors: list[_ErrorDescription] + + +class EntityStoreValidationSuccess(TypedDict): + """Positive entity store validation result.""" + + success: Literal[True] + entity_id: str | None + + +def parse_invalid(exc: vol.Invalid) -> _ErrorDescription: + """Parse a vol.Invalid exception.""" + return _ErrorDescription( + path=[str(path) for path in exc.path], # exc.path: str | vol.Required + error_message=exc.msg, + error_class=type(exc).__name__, + ) + + +def validate_entity_data(entity_data: dict) -> dict: + """Validate entity data. Return validated data or raise EntityStoreValidationException.""" + try: + # return so defaults are applied + return ENTITY_STORE_DATA_SCHEMA(entity_data) # type: ignore[no-any-return] + except vol.MultipleInvalid as exc: + raise EntityStoreValidationException( + validation_error={ + "success": False, + "error_base": str(exc), + "errors": [parse_invalid(invalid) for invalid in exc.errors], + } + ) from exc + except vol.Invalid as exc: + raise EntityStoreValidationException( + validation_error={ + "success": False, + "error_base": str(exc), + "errors": [parse_invalid(exc)], + } + ) from exc + + +class EntityStoreValidationException(Exception): + """Entity store validation exception.""" + + def __init__(self, validation_error: EntityStoreValidationError) -> None: + """Initialize.""" + super().__init__(validation_error) + self.validation_error = validation_error diff --git a/homeassistant/components/knx/helpers/keyring.py b/homeassistant/components/knx/storage/keyring.py similarity index 100% rename from homeassistant/components/knx/helpers/keyring.py rename to homeassistant/components/knx/storage/keyring.py diff --git a/homeassistant/components/knx/storage/knx_selector.py b/homeassistant/components/knx/storage/knx_selector.py new file mode 100644 index 00000000000..396cde67fbd --- /dev/null +++ b/homeassistant/components/knx/storage/knx_selector.py @@ -0,0 +1,81 @@ +"""Selectors for KNX.""" + +from enum import Enum +from typing import Any + +import voluptuous as vol + +from ..validation import ga_validator, maybe_ga_validator +from .const import CONF_DPT, CONF_GA_PASSIVE, CONF_GA_STATE, CONF_GA_WRITE + + +class GASelector: + """Selector for a KNX group address structure.""" + + schema: vol.Schema + + def __init__( + self, + write: bool = True, + state: bool = True, + passive: bool = True, + write_required: bool = False, + state_required: bool = False, + dpt: type[Enum] | None = None, + ) -> None: + """Initialize the group address selector.""" + self.write = write + self.state = state + self.passive = passive + self.write_required = write_required + self.state_required = state_required + self.dpt = dpt + + self.schema = self.build_schema() + + def __call__(self, data: Any) -> Any: + """Validate the passed data.""" + return self.schema(data) + + def build_schema(self) -> vol.Schema: + """Create the schema based on configuration.""" + schema: dict[vol.Marker, Any] = {} # will be modified in-place + self._add_group_addresses(schema) + self._add_passive(schema) + self._add_dpt(schema) + return vol.Schema(schema) + + def _add_group_addresses(self, schema: dict[vol.Marker, Any]) -> None: + """Add basic group address items to the schema.""" + + def add_ga_item(key: str, allowed: bool, required: bool) -> None: + """Add a group address item validator to the schema.""" + if not allowed: + schema[vol.Remove(key)] = object + return + if required: + schema[vol.Required(key)] = ga_validator + else: + schema[vol.Optional(key, default=None)] = maybe_ga_validator + + add_ga_item(CONF_GA_WRITE, self.write, self.write_required) + add_ga_item(CONF_GA_STATE, self.state, self.state_required) + + def _add_passive(self, schema: dict[vol.Marker, Any]) -> None: + """Add passive group addresses validator to the schema.""" + if self.passive: + schema[vol.Optional(CONF_GA_PASSIVE, default=list)] = vol.Any( + [ga_validator], + vol.All( # Coerce `None` to an empty list if passive is allowed + vol.IsFalse(), vol.SetTo(list) + ), + ) + else: + schema[vol.Remove(CONF_GA_PASSIVE)] = object + + def _add_dpt(self, schema: dict[vol.Marker, Any]) -> None: + """Add DPT validator to the schema.""" + if self.dpt is not None: + schema[vol.Required(CONF_DPT)] = vol.In([item.value for item in self.dpt]) + else: + schema[vol.Remove(CONF_DPT)] = object diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 096ce235e2c..94f5592db90 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -18,14 +18,30 @@ from homeassistant.const import ( STATE_UNKNOWN, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType -from .const import CONF_RESPOND_TO_READ, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS +from . import KNXModule +from .const import ( + CONF_INVERT, + CONF_RESPOND_TO_READ, + DATA_KNX_CONFIG, + DOMAIN, + KNX_ADDRESS, +) from .knx_entity import KnxEntity from .schema import SwitchSchema +from .storage.const import ( + CONF_DEVICE_INFO, + CONF_ENTITY, + CONF_GA_PASSIVE, + CONF_GA_STATE, + CONF_GA_SWITCH, + CONF_GA_WRITE, +) async def async_setup_entry( @@ -34,33 +50,35 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up switch(es) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.SWITCH] + knx_module: KNXModule = hass.data[DOMAIN] - async_add_entities(KNXSwitch(xknx, entity_config) for entity_config in config) + entities: list[KnxEntity] = [] + if yaml_config := hass.data[DATA_KNX_CONFIG].get(Platform.SWITCH): + entities.extend( + KnxYamlSwitch(knx_module.xknx, entity_config) + for entity_config in yaml_config + ) + if ui_config := knx_module.config_store.data["entities"].get(Platform.SWITCH): + entities.extend( + KnxUiSwitch(knx_module, unique_id, config) + for unique_id, config in ui_config.items() + ) + if entities: + async_add_entities(entities) + + @callback + def add_new_ui_switch(unique_id: str, config: dict[str, Any]) -> None: + """Add KNX entity at runtime.""" + async_add_entities([KnxUiSwitch(knx_module, unique_id, config)]) + + knx_module.config_store.async_add_entity[Platform.SWITCH] = add_new_ui_switch -class KNXSwitch(KnxEntity, SwitchEntity, RestoreEntity): - """Representation of a KNX switch.""" +class _KnxSwitch(KnxEntity, SwitchEntity, RestoreEntity): + """Base class for a KNX switch.""" _device: XknxSwitch - def __init__(self, xknx: XKNX, config: ConfigType) -> None: - """Initialize of KNX switch.""" - super().__init__( - device=XknxSwitch( - xknx, - name=config[CONF_NAME], - group_address=config[KNX_ADDRESS], - group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), - respond_to_read=config[CONF_RESPOND_TO_READ], - invert=config[SwitchSchema.CONF_INVERT], - ) - ) - self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) - self._attr_device_class = config.get(CONF_DEVICE_CLASS) - self._attr_unique_id = str(self._device.switch.group_address) - async def async_added_to_hass(self) -> None: """Restore last state.""" await super().async_added_to_hass() @@ -82,3 +100,53 @@ class KNXSwitch(KnxEntity, SwitchEntity, RestoreEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._device.set_off() + + +class KnxYamlSwitch(_KnxSwitch): + """Representation of a KNX switch configured from YAML.""" + + def __init__(self, xknx: XKNX, config: ConfigType) -> None: + """Initialize of KNX switch.""" + super().__init__( + device=XknxSwitch( + xknx, + name=config[CONF_NAME], + group_address=config[KNX_ADDRESS], + group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), + respond_to_read=config[CONF_RESPOND_TO_READ], + invert=config[SwitchSchema.CONF_INVERT], + ) + ) + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + self._attr_unique_id = str(self._device.switch.group_address) + + +class KnxUiSwitch(_KnxSwitch): + """Representation of a KNX switch configured from UI.""" + + _attr_has_entity_name = True + + def __init__( + self, knx_module: KNXModule, unique_id: str, config: dict[str, Any] + ) -> None: + """Initialize of KNX switch.""" + super().__init__( + device=XknxSwitch( + knx_module.xknx, + name=config[CONF_ENTITY][CONF_NAME], + group_address=config[DOMAIN][CONF_GA_SWITCH][CONF_GA_WRITE], + group_address_state=[ + config[DOMAIN][CONF_GA_SWITCH][CONF_GA_STATE], + *config[DOMAIN][CONF_GA_SWITCH][CONF_GA_PASSIVE], + ], + respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ], + invert=config[DOMAIN][CONF_INVERT], + ) + ) + self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY] + self._attr_unique_id = unique_id + if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO): + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)}) + + knx_module.config_store.entities[unique_id] = self diff --git a/homeassistant/components/knx/validation.py b/homeassistant/components/knx/validation.py index 4e56314a677..9ed4f32c920 100644 --- a/homeassistant/components/knx/validation.py +++ b/homeassistant/components/knx/validation.py @@ -50,12 +50,27 @@ def ga_validator(value: Any) -> str | int: return value +def maybe_ga_validator(value: Any) -> str | int | None: + """Validate a group address or None.""" + # this is a version of vol.Maybe(ga_validator) that delivers the + # error message of ga_validator if validation fails. + return ga_validator(value) if value is not None else None + + ga_list_validator = vol.All( cv.ensure_list, [ga_validator], vol.IsTrue("value must be a group address or a list containing group addresses"), ) +ga_list_validator_optional = vol.Maybe( + vol.All( + cv.ensure_list, + [ga_validator], + vol.Any(vol.IsTrue(), vol.SetTo(None)), # avoid empty lists -> None + ) +) + ia_validator = vol.Any( vol.All(str, str.strip, cv.matches_regex(IndividualAddress.ADDRESS_RE.pattern)), vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), diff --git a/homeassistant/components/knx/websocket.py b/homeassistant/components/knx/websocket.py index 0ac5a21d333..bca1b119ef7 100644 --- a/homeassistant/components/knx/websocket.py +++ b/homeassistant/components/knx/websocket.py @@ -10,9 +10,24 @@ from xknxproject.exceptions import XknxProjectException from homeassistant.components import panel_custom, websocket_api from homeassistant.components.http import StaticPathConfig +from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import UNDEFINED +from homeassistant.util.ulid import ulid_now from .const import DOMAIN +from .storage.config_store import ConfigStoreException +from .storage.const import CONF_DATA +from .storage.entity_store_schema import ( + CREATE_ENTITY_BASE_SCHEMA, + UPDATE_ENTITY_BASE_SCHEMA, +) +from .storage.entity_store_validation import ( + EntityStoreValidationException, + EntityStoreValidationSuccess, + validate_entity_data, +) from .telegrams import TelegramDict if TYPE_CHECKING: @@ -30,6 +45,13 @@ async def register_panel(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_group_monitor_info) websocket_api.async_register_command(hass, ws_subscribe_telegram) websocket_api.async_register_command(hass, ws_get_knx_project) + websocket_api.async_register_command(hass, ws_validate_entity) + websocket_api.async_register_command(hass, ws_create_entity) + websocket_api.async_register_command(hass, ws_update_entity) + websocket_api.async_register_command(hass, ws_delete_entity) + websocket_api.async_register_command(hass, ws_get_entity_config) + websocket_api.async_register_command(hass, ws_get_entity_entries) + websocket_api.async_register_command(hass, ws_create_device) if DOMAIN not in hass.data.get("frontend_panels", {}): await hass.http.async_register_static_paths( @@ -213,3 +235,201 @@ def ws_subscribe_telegram( name="KNX GroupMonitor subscription", ) connection.send_result(msg["id"]) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "knx/validate_entity", + **CREATE_ENTITY_BASE_SCHEMA, + } +) +@callback +def ws_validate_entity( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Validate entity data.""" + try: + validate_entity_data(msg) + except EntityStoreValidationException as exc: + connection.send_result(msg["id"], exc.validation_error) + return + connection.send_result( + msg["id"], EntityStoreValidationSuccess(success=True, entity_id=None) + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "knx/create_entity", + **CREATE_ENTITY_BASE_SCHEMA, + } +) +@websocket_api.async_response +async def ws_create_entity( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Create entity in entity store and load it.""" + try: + validated_data = validate_entity_data(msg) + except EntityStoreValidationException as exc: + connection.send_result(msg["id"], exc.validation_error) + return + knx: KNXModule = hass.data[DOMAIN] + try: + entity_id = await knx.config_store.create_entity( + # use validation result so defaults are applied + validated_data[CONF_PLATFORM], + validated_data[CONF_DATA], + ) + except ConfigStoreException as err: + connection.send_error( + msg["id"], websocket_api.const.ERR_HOME_ASSISTANT_ERROR, str(err) + ) + return + connection.send_result( + msg["id"], EntityStoreValidationSuccess(success=True, entity_id=entity_id) + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "knx/update_entity", + **UPDATE_ENTITY_BASE_SCHEMA, + } +) +@websocket_api.async_response +async def ws_update_entity( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Update entity in entity store and reload it.""" + try: + validated_data = validate_entity_data(msg) + except EntityStoreValidationException as exc: + connection.send_result(msg["id"], exc.validation_error) + return + knx: KNXModule = hass.data[DOMAIN] + try: + await knx.config_store.update_entity( + validated_data[CONF_PLATFORM], + validated_data[CONF_ENTITY_ID], + validated_data[CONF_DATA], + ) + except ConfigStoreException as err: + connection.send_error( + msg["id"], websocket_api.const.ERR_HOME_ASSISTANT_ERROR, str(err) + ) + return + connection.send_result( + msg["id"], EntityStoreValidationSuccess(success=True, entity_id=None) + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "knx/delete_entity", + vol.Required(CONF_ENTITY_ID): str, + } +) +@websocket_api.async_response +async def ws_delete_entity( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Delete entity from entity store and remove it.""" + knx: KNXModule = hass.data[DOMAIN] + try: + await knx.config_store.delete_entity(msg[CONF_ENTITY_ID]) + except ConfigStoreException as err: + connection.send_error( + msg["id"], websocket_api.const.ERR_HOME_ASSISTANT_ERROR, str(err) + ) + return + connection.send_result(msg["id"]) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "knx/get_entity_entries", + } +) +@callback +def ws_get_entity_entries( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Get entities configured from entity store.""" + knx: KNXModule = hass.data[DOMAIN] + entity_entries = [ + entry.extended_dict for entry in knx.config_store.get_entity_entries() + ] + connection.send_result(msg["id"], entity_entries) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "knx/get_entity_config", + vol.Required(CONF_ENTITY_ID): str, + } +) +@callback +def ws_get_entity_config( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Get entity configuration from entity store.""" + knx: KNXModule = hass.data[DOMAIN] + try: + config_info = knx.config_store.get_entity_config(msg[CONF_ENTITY_ID]) + except ConfigStoreException as err: + connection.send_error( + msg["id"], websocket_api.const.ERR_HOME_ASSISTANT_ERROR, str(err) + ) + return + connection.send_result(msg["id"], config_info) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "knx/create_device", + vol.Required("name"): str, + vol.Optional("area_id"): str, + } +) +@callback +def ws_create_device( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Create a new KNX device.""" + knx: KNXModule = hass.data[DOMAIN] + identifier = f"knx_vdev_{ulid_now()}" + device_registry = dr.async_get(hass) + _device = device_registry.async_get_or_create( + config_entry_id=knx.entry.entry_id, + manufacturer="KNX", + name=msg["name"], + identifiers={(DOMAIN, identifier)}, + ) + device_registry.async_update_device( + _device.id, + area_id=msg.get("area_id") or UNDEFINED, + configuration_url=f"homeassistant://knx/entities/view?device_id={_device.id}", + ) + connection.send_result(msg["id"], _device.dict_repr) diff --git a/tests/components/knx/README.md b/tests/components/knx/README.md index 930b9e71c28..8778feb2251 100644 --- a/tests/components/knx/README.md +++ b/tests/components/knx/README.md @@ -24,9 +24,10 @@ All outgoing telegrams are pushed to an assertion queue. Assert them in order th Asserts that no telegram was sent (assertion queue is empty). - `knx.assert_telegram_count(count: int)` Asserts that `count` telegrams were sent. -- `knx.assert_read(group_address: str)` +- `knx.assert_read(group_address: str, response: int | tuple[int, ...] | None = None)` Asserts that a GroupValueRead telegram was sent to `group_address`. The telegram will be removed from the assertion queue. + Optionally inject incoming GroupValueResponse telegram after reception to clear the value reader waiting task. This can also be done manually with `knx.receive_response`. - `knx.assert_response(group_address: str, payload: int | tuple[int, ...])` Asserts that a GroupValueResponse telegram with `payload` was sent to `group_address`. The telegram will be removed from the assertion queue. diff --git a/tests/components/knx/__init__.py b/tests/components/knx/__init__.py index eaa84714dc5..76ae91a193d 100644 --- a/tests/components/knx/__init__.py +++ b/tests/components/knx/__init__.py @@ -1 +1,7 @@ """Tests for the KNX integration.""" + +from collections.abc import Awaitable, Callable + +from homeassistant.helpers import entity_registry as er + +KnxEntityGenerator = Callable[..., Awaitable[er.RegistryEntry]] diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index cd7146b565b..749d1c4252a 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -import json from typing import Any from unittest.mock import DEFAULT, AsyncMock, Mock, patch @@ -30,13 +29,22 @@ from homeassistant.components.knx.const import ( DOMAIN as KNX_DOMAIN, ) from homeassistant.components.knx.project import STORAGE_KEY as KNX_PROJECT_STORAGE_KEY +from homeassistant.components.knx.storage.config_store import ( + STORAGE_KEY as KNX_CONFIG_STORAGE_KEY, +) +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, load_fixture +from . import KnxEntityGenerator -FIXTURE_PROJECT_DATA = json.loads(load_fixture("project.json", KNX_DOMAIN)) +from tests.common import MockConfigEntry, load_json_object_fixture +from tests.typing import WebSocketGenerator + +FIXTURE_PROJECT_DATA = load_json_object_fixture("project.json", KNX_DOMAIN) +FIXTURE_CONFIG_STORAGE_DATA = load_json_object_fixture("config_store.json", KNX_DOMAIN) class KNXTestKit: @@ -166,9 +174,16 @@ class KNXTestKit: telegram.payload.value.value == payload # type: ignore[attr-defined] ), f"Payload mismatch in {telegram} - Expected: {payload}" - async def assert_read(self, group_address: str) -> None: - """Assert outgoing GroupValueRead telegram. One by one in timely order.""" + async def assert_read( + self, group_address: str, response: int | tuple[int, ...] | None = None + ) -> None: + """Assert outgoing GroupValueRead telegram. One by one in timely order. + + Optionally inject incoming GroupValueResponse telegram after reception. + """ await self.assert_telegram(group_address, None, GroupValueRead) + if response is not None: + await self.receive_response(group_address, response) async def assert_response( self, group_address: str, payload: int | tuple[int, ...] @@ -280,3 +295,53 @@ def load_knxproj(hass_storage: dict[str, Any]) -> None: "version": 1, "data": FIXTURE_PROJECT_DATA, } + + +@pytest.fixture +def load_config_store(hass_storage: dict[str, Any]) -> None: + """Mock KNX config store data.""" + hass_storage[KNX_CONFIG_STORAGE_KEY] = FIXTURE_CONFIG_STORAGE_DATA + + +@pytest.fixture +async def create_ui_entity( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], +) -> KnxEntityGenerator: + """Return a helper to create a KNX entities via WS. + + The KNX integration must be set up before using the helper. + """ + ws_client = await hass_ws_client(hass) + + async def _create_ui_entity( + platform: Platform, + knx_data: dict[str, Any], + entity_data: dict[str, Any] | None = None, + ) -> er.RegistryEntry: + """Create a KNX entity from WS with given configuration.""" + if entity_data is None: + entity_data = {"name": "Test"} + + await ws_client.send_json_auto_id( + { + "type": "knx/create_entity", + "platform": platform, + "data": { + "entity": entity_data, + "knx": knx_data, + }, + } + ) + res = await ws_client.receive_json() + assert res["success"], res + assert res["result"]["success"] is True + entity_id = res["result"]["entity_id"] + + entity = entity_registry.async_get(entity_id) + assert entity + return entity + + return _create_ui_entity diff --git a/tests/components/knx/fixtures/config_store.json b/tests/components/knx/fixtures/config_store.json new file mode 100644 index 00000000000..971b692ade1 --- /dev/null +++ b/tests/components/knx/fixtures/config_store.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "minor_version": 1, + "key": "knx/config_store.json", + "data": { + "entities": { + "switch": { + "knx_es_9d97829f47f1a2a3176a7c5b4216070c": { + "entity": { + "entity_category": null, + "name": "test", + "device_info": "knx_vdev_4c80a564f5fe5da701ed293966d6384d" + }, + "knx": { + "ga_switch": { + "write": "1/1/45", + "state": "1/0/45", + "passive": [] + }, + "invert": false, + "sync_state": true, + "respond_to_read": false + } + } + }, + "light": {} + } + } +} diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index f12a57f97ba..3dad9320e21 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -76,10 +76,10 @@ def patch_file_upload(return_value=FIXTURE_KEYRING, side_effect=None): """Patch file upload. Yields the Keyring instance (return_value).""" with ( patch( - "homeassistant.components.knx.helpers.keyring.process_uploaded_file" + "homeassistant.components.knx.storage.keyring.process_uploaded_file" ) as file_upload_mock, patch( - "homeassistant.components.knx.helpers.keyring.sync_load_keyring", + "homeassistant.components.knx.storage.keyring.sync_load_keyring", return_value=return_value, side_effect=side_effect, ), diff --git a/tests/components/knx/test_config_store.py b/tests/components/knx/test_config_store.py new file mode 100644 index 00000000000..116f4b5d839 --- /dev/null +++ b/tests/components/knx/test_config_store.py @@ -0,0 +1,412 @@ +"""Test KNX config store.""" + +from typing import Any + +import pytest + +from homeassistant.components.knx.storage.config_store import ( + STORAGE_KEY as KNX_CONFIG_STORAGE_KEY, +) +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import KnxEntityGenerator +from .conftest import KNXTestKit + +from tests.typing import WebSocketGenerator + + +async def test_create_entity( + hass: HomeAssistant, + knx: KNXTestKit, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], + create_ui_entity: KnxEntityGenerator, +) -> None: + """Test entity creation.""" + await knx.setup_integration({}) + client = await hass_ws_client(hass) + + test_name = "Test no device" + test_entity = await create_ui_entity( + platform=Platform.SWITCH, + knx_data={"ga_switch": {"write": "1/2/3"}}, + entity_data={"name": test_name}, + ) + + # Test if entity is correctly stored in registry + await client.send_json_auto_id({"type": "knx/get_entity_entries"}) + res = await client.receive_json() + assert res["success"], res + assert res["result"] == [ + test_entity.extended_dict, + ] + # Test if entity is correctly stored in config store + test_storage_data = next( + iter( + hass_storage[KNX_CONFIG_STORAGE_KEY]["data"]["entities"]["switch"].values() + ) + ) + assert test_storage_data == { + "entity": { + "name": test_name, + "device_info": None, + "entity_category": None, + }, + "knx": { + "ga_switch": {"write": "1/2/3", "state": None, "passive": []}, + "invert": False, + "respond_to_read": False, + "sync_state": True, + }, + } + + +async def test_create_entity_error( + hass: HomeAssistant, + knx: KNXTestKit, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test unsuccessful entity creation.""" + await knx.setup_integration({}) + client = await hass_ws_client(hass) + + # create entity with invalid platform + await client.send_json_auto_id( + { + "type": "knx/create_entity", + "platform": "invalid_platform", + "data": { + "entity": {"name": "Test invalid platform"}, + "knx": {"ga_switch": {"write": "1/2/3"}}, + }, + } + ) + res = await client.receive_json() + assert res["success"], res + assert not res["result"]["success"] + assert res["result"]["errors"][0]["path"] == ["platform"] + assert res["result"]["error_base"].startswith("expected Platform or one of") + + # create entity with unsupported platform + await client.send_json_auto_id( + { + "type": "knx/create_entity", + "platform": Platform.TTS, # "tts" is not a supported platform (and is unlikely to ever be) + "data": { + "entity": {"name": "Test invalid platform"}, + "knx": {"ga_switch": {"write": "1/2/3"}}, + }, + } + ) + res = await client.receive_json() + assert res["success"], res + assert not res["result"]["success"] + assert res["result"]["errors"][0]["path"] == ["platform"] + assert res["result"]["error_base"].startswith("value must be one of") + + +async def test_update_entity( + hass: HomeAssistant, + knx: KNXTestKit, + entity_registry: er.EntityRegistry, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], + create_ui_entity: KnxEntityGenerator, +) -> None: + """Test entity update.""" + await knx.setup_integration({}) + client = await hass_ws_client(hass) + + test_entity = await create_ui_entity( + platform=Platform.SWITCH, + knx_data={"ga_switch": {"write": "1/2/3"}}, + entity_data={"name": "Test"}, + ) + test_entity_id = test_entity.entity_id + + # update entity + new_name = "Updated name" + new_ga_switch_write = "4/5/6" + await client.send_json_auto_id( + { + "type": "knx/update_entity", + "platform": Platform.SWITCH, + "entity_id": test_entity_id, + "data": { + "entity": {"name": new_name}, + "knx": {"ga_switch": {"write": new_ga_switch_write}}, + }, + } + ) + res = await client.receive_json() + assert res["success"], res + assert res["result"]["success"] + + entity = entity_registry.async_get(test_entity_id) + assert entity + assert entity.original_name == new_name + + assert ( + hass_storage[KNX_CONFIG_STORAGE_KEY]["data"]["entities"]["switch"][ + test_entity.unique_id + ]["knx"]["ga_switch"]["write"] + == new_ga_switch_write + ) + + +async def test_update_entity_error( + hass: HomeAssistant, + knx: KNXTestKit, + hass_ws_client: WebSocketGenerator, + create_ui_entity: KnxEntityGenerator, +) -> None: + """Test entity update.""" + await knx.setup_integration({}) + client = await hass_ws_client(hass) + + test_entity = await create_ui_entity( + platform=Platform.SWITCH, + knx_data={"ga_switch": {"write": "1/2/3"}}, + entity_data={"name": "Test"}, + ) + + # update unsupported platform + new_name = "Updated name" + new_ga_switch_write = "4/5/6" + await client.send_json_auto_id( + { + "type": "knx/update_entity", + "platform": Platform.TTS, + "entity_id": test_entity.entity_id, + "data": { + "entity": {"name": new_name}, + "knx": {"ga_switch": {"write": new_ga_switch_write}}, + }, + } + ) + res = await client.receive_json() + assert res["success"], res + assert not res["result"]["success"] + assert res["result"]["errors"][0]["path"] == ["platform"] + assert res["result"]["error_base"].startswith("value must be one of") + + # entity not found + await client.send_json_auto_id( + { + "type": "knx/update_entity", + "platform": Platform.SWITCH, + "entity_id": "non_existing_entity_id", + "data": { + "entity": {"name": new_name}, + "knx": {"ga_switch": {"write": new_ga_switch_write}}, + }, + } + ) + res = await client.receive_json() + assert not res["success"], res + assert res["error"]["code"] == "home_assistant_error" + assert res["error"]["message"].startswith("Entity not found:") + + # entity not in storage + await client.send_json_auto_id( + { + "type": "knx/update_entity", + "platform": Platform.SWITCH, + # `sensor` isn't yet supported, but we only have sensor entities automatically + # created with no configuration - it doesn't ,atter for the test though + "entity_id": "sensor.knx_interface_individual_address", + "data": { + "entity": {"name": new_name}, + "knx": {"ga_switch": {"write": new_ga_switch_write}}, + }, + } + ) + res = await client.receive_json() + assert not res["success"], res + assert res["error"]["code"] == "home_assistant_error" + assert res["error"]["message"].startswith("Entity not found in storage") + + +async def test_delete_entity( + hass: HomeAssistant, + knx: KNXTestKit, + entity_registry: er.EntityRegistry, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], + create_ui_entity: KnxEntityGenerator, +) -> None: + """Test entity deletion.""" + await knx.setup_integration({}) + client = await hass_ws_client(hass) + + test_entity = await create_ui_entity( + platform=Platform.SWITCH, + knx_data={"ga_switch": {"write": "1/2/3"}}, + entity_data={"name": "Test"}, + ) + test_entity_id = test_entity.entity_id + + # delete entity + await client.send_json_auto_id( + { + "type": "knx/delete_entity", + "entity_id": test_entity_id, + } + ) + res = await client.receive_json() + assert res["success"], res + + assert not entity_registry.async_get(test_entity_id) + assert not hass_storage[KNX_CONFIG_STORAGE_KEY]["data"]["entities"].get("switch") + + +async def test_delete_entity_error( + hass: HomeAssistant, + knx: KNXTestKit, + entity_registry: er.EntityRegistry, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], +) -> None: + """Test unsuccessful entity deletion.""" + await knx.setup_integration({}) + client = await hass_ws_client(hass) + + # delete unknown entity + await client.send_json_auto_id( + { + "type": "knx/delete_entity", + "entity_id": "switch.non_existing_entity", + } + ) + res = await client.receive_json() + assert not res["success"], res + assert res["error"]["code"] == "home_assistant_error" + assert res["error"]["message"].startswith("Entity not found") + + # delete entity not in config store + test_entity_id = "sensor.knx_interface_individual_address" + assert entity_registry.async_get(test_entity_id) + await client.send_json_auto_id( + { + "type": "knx/delete_entity", + "entity_id": test_entity_id, + } + ) + res = await client.receive_json() + assert not res["success"], res + assert res["error"]["code"] == "home_assistant_error" + assert res["error"]["message"].startswith("Entity not found") + + +async def test_get_entity_config( + hass: HomeAssistant, + knx: KNXTestKit, + hass_ws_client: WebSocketGenerator, + create_ui_entity: KnxEntityGenerator, +) -> None: + """Test entity config retrieval.""" + await knx.setup_integration({}) + client = await hass_ws_client(hass) + + test_entity = await create_ui_entity( + platform=Platform.SWITCH, + knx_data={"ga_switch": {"write": "1/2/3"}}, + entity_data={"name": "Test"}, + ) + + await client.send_json_auto_id( + { + "type": "knx/get_entity_config", + "entity_id": test_entity.entity_id, + } + ) + res = await client.receive_json() + assert res["success"], res + assert res["result"]["platform"] == Platform.SWITCH + assert res["result"]["data"] == { + "entity": { + "name": "Test", + "device_info": None, + "entity_category": None, + }, + "knx": { + "ga_switch": {"write": "1/2/3", "passive": [], "state": None}, + "respond_to_read": False, + "invert": False, + "sync_state": True, + }, + } + + +@pytest.mark.parametrize( + ("test_entity_id", "error_message_start"), + [ + ("switch.non_existing_entity", "Entity not found"), + ("sensor.knx_interface_individual_address", "Entity data not found"), + ], +) +async def test_get_entity_config_error( + hass: HomeAssistant, + knx: KNXTestKit, + hass_ws_client: WebSocketGenerator, + test_entity_id: str, + error_message_start: str, +) -> None: + """Test entity config retrieval errors.""" + await knx.setup_integration({}) + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "knx/get_entity_config", + "entity_id": test_entity_id, + } + ) + res = await client.receive_json() + assert not res["success"], res + assert res["error"]["code"] == "home_assistant_error" + assert res["error"]["message"].startswith(error_message_start) + + +async def test_validate_entity( + hass: HomeAssistant, + knx: KNXTestKit, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test entity validation.""" + await knx.setup_integration({}) + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "knx/validate_entity", + "platform": Platform.SWITCH, + "data": { + "entity": {"name": "test_name"}, + "knx": {"ga_switch": {"write": "1/2/3"}}, + }, + } + ) + res = await client.receive_json() + assert res["success"], res + assert res["result"]["success"] is True + + # invalid data + await client.send_json_auto_id( + { + "type": "knx/validate_entity", + "platform": Platform.SWITCH, + "data": { + "entity": {"name": "test_name"}, + "knx": {"ga_switch": {}}, + }, + } + ) + res = await client.receive_json() + assert res["success"], res + assert res["result"]["success"] is False + assert res["result"]["errors"][0]["path"] == ["data", "knx", "ga_switch", "write"] + assert res["result"]["errors"][0]["error_message"] == "required key not provided" + assert res["result"]["error_base"].startswith("required key not provided") diff --git a/tests/components/knx/test_device.py b/tests/components/knx/test_device.py new file mode 100644 index 00000000000..330fd854a50 --- /dev/null +++ b/tests/components/knx/test_device.py @@ -0,0 +1,77 @@ +"""Test KNX devices.""" + +from typing import Any + +from homeassistant.components.knx.const import DOMAIN +from homeassistant.components.knx.storage.config_store import ( + STORAGE_KEY as KNX_CONFIG_STORAGE_KEY, +) +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 .conftest import KNXTestKit + +from tests.typing import WebSocketGenerator + + +async def test_create_device( + hass: HomeAssistant, + knx: KNXTestKit, + device_registry: dr.DeviceRegistry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test device creation.""" + await knx.setup_integration({}) + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "knx/create_device", + "name": "Test Device", + } + ) + res = await client.receive_json() + assert res["success"], res + assert res["result"]["name"] == "Test Device" + assert res["result"]["manufacturer"] == "KNX" + assert res["result"]["identifiers"] + assert res["result"]["config_entries"][0] == knx.mock_config_entry.entry_id + + device_identifier = res["result"]["identifiers"][0][1] + assert device_registry.async_get_device({(DOMAIN, device_identifier)}) + device_id = res["result"]["id"] + assert device_registry.async_get(device_id) + + +async def test_remove_device( + hass: HomeAssistant, + knx: KNXTestKit, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + hass_ws_client: WebSocketGenerator, + load_config_store: None, + hass_storage: dict[str, Any], +) -> None: + """Test device removal.""" + assert await async_setup_component(hass, "config", {}) + await knx.setup_integration({}) + client = await hass_ws_client(hass) + + await knx.assert_read("1/0/45", response=True) + + assert hass_storage[KNX_CONFIG_STORAGE_KEY]["data"]["entities"].get("switch") + test_device = device_registry.async_get_device( + {(DOMAIN, "knx_vdev_4c80a564f5fe5da701ed293966d6384d")} + ) + device_id = test_device.id + device_entities = entity_registry.entities.get_entries_for_device_id(device_id) + assert len(device_entities) == 1 + + response = await client.remove_device(device_id, knx.mock_config_entry.entry_id) + assert response["success"] + assert not device_registry.async_get_device( + {(DOMAIN, "knx_vdev_4c80a564f5fe5da701ed293966d6384d")} + ) + assert not entity_registry.entities.get_entries_for_device_id(device_id) + assert not hass_storage[KNX_CONFIG_STORAGE_KEY]["data"]["entities"].get("switch") diff --git a/tests/components/knx/test_interface_device.py b/tests/components/knx/test_interface_device.py index 6cf5d8026b9..c21c25b6fad 100644 --- a/tests/components/knx/test_interface_device.py +++ b/tests/components/knx/test_interface_device.py @@ -1,4 +1,4 @@ -"""Test KNX scene.""" +"""Test KNX interface device.""" from unittest.mock import patch @@ -8,12 +8,14 @@ from xknx.telegram import IndividualAddress from homeassistant.components.knx.sensor import SCAN_INTERVAL from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from .conftest import KNXTestKit from tests.common import async_capture_events, async_fire_time_changed +from tests.typing import WebSocketGenerator async def test_diagnostic_entities( @@ -111,3 +113,28 @@ async def test_removed_entity( ) await hass.async_block_till_done() unregister_mock.assert_called_once() + + +async def test_remove_interface_device( + hass: HomeAssistant, + knx: KNXTestKit, + device_registry: dr.DeviceRegistry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test device removal.""" + assert await async_setup_component(hass, "config", {}) + await knx.setup_integration({}) + client = await hass_ws_client(hass) + knx_devices = device_registry.devices.get_devices_for_config_entry_id( + knx.mock_config_entry.entry_id + ) + assert len(knx_devices) == 1 + assert knx_devices[0].name == "KNX Interface" + device_id = knx_devices[0].id + # interface device can't be removed + res = await client.remove_device(device_id, knx.mock_config_entry.entry_id) + assert not res["success"] + assert ( + res["error"]["message"] + == "Failed to remove device entry, rejected by integration" + ) diff --git a/tests/components/knx/test_knx_selectors.py b/tests/components/knx/test_knx_selectors.py new file mode 100644 index 00000000000..432a0fb9f80 --- /dev/null +++ b/tests/components/knx/test_knx_selectors.py @@ -0,0 +1,122 @@ +"""Test KNX selectors.""" + +import pytest +import voluptuous as vol + +from homeassistant.components.knx.const import ColorTempModes +from homeassistant.components.knx.storage.knx_selector import GASelector + +INVALID = "invalid" + + +@pytest.mark.parametrize( + ("selector_config", "data", "expected"), + [ + ( + {}, + {}, + {"write": None, "state": None, "passive": []}, + ), + ( + {}, + {"write": "1/2/3"}, + {"write": "1/2/3", "state": None, "passive": []}, + ), + ( + {}, + {"state": "1/2/3"}, + {"write": None, "state": "1/2/3", "passive": []}, + ), + ( + {}, + {"passive": ["1/2/3"]}, + {"write": None, "state": None, "passive": ["1/2/3"]}, + ), + ( + {}, + {"write": "1", "state": 2, "passive": ["1/2/3"]}, + {"write": "1", "state": 2, "passive": ["1/2/3"]}, + ), + ( + {"write": False}, + {"write": "1/2/3"}, + {"state": None, "passive": []}, + ), + ( + {"write": False}, + {"state": "1/2/3"}, + {"state": "1/2/3", "passive": []}, + ), + ( + {"write": False}, + {"passive": ["1/2/3"]}, + {"state": None, "passive": ["1/2/3"]}, + ), + ( + {"passive": False}, + {"passive": ["1/2/3"]}, + {"write": None, "state": None}, + ), + ( + {"passive": False}, + {"write": "1/2/3"}, + {"write": "1/2/3", "state": None}, + ), + # required keys + ( + {"write_required": True}, + {}, + INVALID, + ), + ( + {"state_required": True}, + {}, + INVALID, + ), + ( + {"write_required": True}, + {"write": "1/2/3"}, + {"write": "1/2/3", "state": None, "passive": []}, + ), + ( + {"state_required": True}, + {"state": "1/2/3"}, + {"write": None, "state": "1/2/3", "passive": []}, + ), + ( + {"write_required": True}, + {"state": "1/2/3"}, + INVALID, + ), + ( + {"state_required": True}, + {"write": "1/2/3"}, + INVALID, + ), + # dpt key + ( + {"dpt": ColorTempModes}, + {"write": "1/2/3"}, + INVALID, + ), + ( + {"dpt": ColorTempModes}, + {"write": "1/2/3", "dpt": "7.600"}, + {"write": "1/2/3", "state": None, "passive": [], "dpt": "7.600"}, + ), + ( + {"dpt": ColorTempModes}, + {"write": "1/2/3", "state": None, "passive": [], "dpt": "invalid"}, + INVALID, + ), + ], +) +def test_ga_selector(selector_config, data, expected): + """Test GASelector.""" + selector = GASelector(**selector_config) + if expected == INVALID: + with pytest.raises(vol.Invalid): + selector(data) + else: + result = selector(data) + assert result == expected diff --git a/tests/components/knx/test_switch.py b/tests/components/knx/test_switch.py index 8dce4cf9c27..bc0a6b27675 100644 --- a/tests/components/knx/test_switch.py +++ b/tests/components/knx/test_switch.py @@ -6,9 +6,10 @@ from homeassistant.components.knx.const import ( KNX_ADDRESS, ) from homeassistant.components.knx.schema import SwitchSchema -from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant, State +from . import KnxEntityGenerator from .conftest import KNXTestKit from tests.common import mock_restore_cache @@ -146,3 +147,27 @@ async def test_switch_restore_and_respond(hass: HomeAssistant, knx) -> None: # respond to new state await knx.receive_read(_ADDRESS) await knx.assert_response(_ADDRESS, False) + + +async def test_switch_ui_create( + hass: HomeAssistant, + knx: KNXTestKit, + create_ui_entity: KnxEntityGenerator, +) -> None: + """Test creating a switch.""" + await knx.setup_integration({}) + await create_ui_entity( + platform=Platform.SWITCH, + entity_data={"name": "test"}, + knx_data={ + "ga_switch": {"write": "1/1/1", "state": "2/2/2"}, + "respond_to_read": True, + "sync_state": True, + "invert": False, + }, + ) + # created entity sends read-request to KNX bus + await knx.assert_read("2/2/2") + await knx.receive_response("2/2/2", True) + state = hass.states.get("switch.test") + assert state.state is STATE_ON diff --git a/tests/components/knx/test_websocket.py b/tests/components/knx/test_websocket.py index ca60905b0ba..eb22bac85bc 100644 --- a/tests/components/knx/test_websocket.py +++ b/tests/components/knx/test_websocket.py @@ -4,6 +4,7 @@ from typing import Any from unittest.mock import patch from homeassistant.components.knx import DOMAIN, KNX_ADDRESS, SwitchSchema +from homeassistant.components.knx.project import STORAGE_KEY as KNX_PROJECT_STORAGE_KEY from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -87,6 +88,7 @@ async def test_knx_project_file_process( assert res["success"], res assert hass.data[DOMAIN].project.loaded + assert hass_storage[KNX_PROJECT_STORAGE_KEY]["data"] == _parse_result async def test_knx_project_file_process_error( @@ -126,19 +128,20 @@ async def test_knx_project_file_remove( knx: KNXTestKit, hass_ws_client: WebSocketGenerator, load_knxproj: None, + hass_storage: dict[str, Any], ) -> None: """Test knx/project_file_remove command.""" await knx.setup_integration({}) + assert hass_storage[KNX_PROJECT_STORAGE_KEY] client = await hass_ws_client(hass) assert hass.data[DOMAIN].project.loaded await client.send_json({"id": 6, "type": "knx/project_file_remove"}) - with patch("homeassistant.helpers.storage.Store.async_remove") as remove_mock: - res = await client.receive_json() - remove_mock.assert_called_once_with() + res = await client.receive_json() assert res["success"], res assert not hass.data[DOMAIN].project.loaded + assert not hass_storage.get(KNX_PROJECT_STORAGE_KEY) async def test_knx_get_project( From 94ce02f40678574be2c5c1016dfd38927e4e6320 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 21 Jul 2024 20:18:43 +0200 Subject: [PATCH 1425/2411] Bump renault-api to 2.0.5 (#122326) * Bump renault-api to 2.0.5 * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index ffa1cd6acef..6691921e850 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "platinum", - "requirements": ["renault-api==0.2.4"] + "requirements": ["renault-api==0.2.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5203599a368..2dd24182707 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2462,7 +2462,7 @@ refoss-ha==1.2.4 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.4 +renault-api==0.2.5 # homeassistant.components.renson renson-endura-delta==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 449c95e88e4..84cf2487545 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1935,7 +1935,7 @@ refoss-ha==1.2.4 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.4 +renault-api==0.2.5 # homeassistant.components.renson renson-endura-delta==1.7.1 From 7e1fb88e4ef04a6a4da16032cf1b9862e708aa60 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 21 Jul 2024 20:55:02 +0200 Subject: [PATCH 1426/2411] Post merge review for Feedreader (#122327) * remove unneccessary typing * assert type list while type checking * remove summary, since feedparser parse it already into content * add further tests --- .../components/feedreader/coordinator.py | 6 +++- homeassistant/components/feedreader/event.py | 4 +-- tests/components/feedreader/conftest.py | 12 ++++++++ .../feedreader/fixtures/feedreader7.xml | 11 +++++++ .../feedreader/fixtures/feedreader8.xml | 21 ++++++++++++++ tests/components/feedreader/test_event.py | 14 +++++++-- tests/components/feedreader/test_init.py | 29 +++++++++++++++++++ 7 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 tests/components/feedreader/fixtures/feedreader7.xml create mode 100644 tests/components/feedreader/fixtures/feedreader8.xml diff --git a/homeassistant/components/feedreader/coordinator.py b/homeassistant/components/feedreader/coordinator.py index e429979b1da..6608c4312fe 100644 --- a/homeassistant/components/feedreader/coordinator.py +++ b/homeassistant/components/feedreader/coordinator.py @@ -6,6 +6,7 @@ from calendar import timegm from datetime import datetime from logging import getLogger from time import gmtime, struct_time +from typing import TYPE_CHECKING from urllib.error import URLError import feedparser @@ -120,10 +121,13 @@ class FeedReaderCoordinator( len(self._feed.entries), self.url, ) - if not isinstance(self._feed.entries, list): + if not self._feed.entries: self._log_no_entries() return None + if TYPE_CHECKING: + assert isinstance(self._feed.entries, list) + self._filter_entries() self._publish_new_entries() diff --git a/homeassistant/components/feedreader/event.py b/homeassistant/components/feedreader/event.py index c9bf39e83ca..48c18c4e70d 100644 --- a/homeassistant/components/feedreader/event.py +++ b/homeassistant/components/feedreader/event.py @@ -29,7 +29,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up event entities for feedreader.""" - coordinator: FeedReaderCoordinator = entry.runtime_data + coordinator = entry.runtime_data async_add_entities([FeedReaderEvent(coordinator)]) @@ -76,8 +76,6 @@ class FeedReaderEvent(CoordinatorEntity[FeedReaderCoordinator], EventEntity): if content := feed_data.get("content"): if isinstance(content, list) and isinstance(content[0], dict): content = content[0].get("value") - else: - content = feed_data.get("summary") self._trigger_event( EVENT_FEEDREADER, diff --git a/tests/components/feedreader/conftest.py b/tests/components/feedreader/conftest.py index 0a5342615a9..8eeb89e00cd 100644 --- a/tests/components/feedreader/conftest.py +++ b/tests/components/feedreader/conftest.py @@ -52,6 +52,18 @@ def fixture_feed_identically_timed_events(hass: HomeAssistant) -> bytes: return load_fixture_bytes("feedreader6.xml") +@pytest.fixture(name="feed_without_items") +def fixture_feed_without_items(hass: HomeAssistant) -> bytes: + """Load test feed without any items.""" + return load_fixture_bytes("feedreader7.xml") + + +@pytest.fixture(name="feed_only_summary") +def fixture_feed_only_summary(hass: HomeAssistant) -> bytes: + """Load test feed data with one event containing only a summary, no content.""" + return load_fixture_bytes("feedreader8.xml") + + @pytest.fixture(name="events") async def fixture_events(hass: HomeAssistant) -> list[Event]: """Fixture that catches alexa events.""" diff --git a/tests/components/feedreader/fixtures/feedreader7.xml b/tests/components/feedreader/fixtures/feedreader7.xml new file mode 100644 index 00000000000..0ffac8dd2ee --- /dev/null +++ b/tests/components/feedreader/fixtures/feedreader7.xml @@ -0,0 +1,11 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 30 Apr 2018 12:00:00 +1000 + Mon, 30 Apr 2018 15:00:00 +1000 + 1800 + + diff --git a/tests/components/feedreader/fixtures/feedreader8.xml b/tests/components/feedreader/fixtures/feedreader8.xml new file mode 100644 index 00000000000..d1c167352f8 --- /dev/null +++ b/tests/components/feedreader/fixtures/feedreader8.xml @@ -0,0 +1,21 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 30 Apr 2018 12:00:00 +1000 + Mon, 30 Apr 2018 15:00:00 +1000 + 1800 + + + Title 1 + Description 1 + http://www.example.com/link/1 + GUID 1 + Mon, 30 Apr 2018 15:10:00 +1000 + This is a summary + + + + diff --git a/tests/components/feedreader/test_event.py b/tests/components/feedreader/test_event.py index 23fec371860..5d903383c05 100644 --- a/tests/components/feedreader/test_event.py +++ b/tests/components/feedreader/test_event.py @@ -18,14 +18,14 @@ from tests.common import async_fire_time_changed async def test_event_entity( - hass: HomeAssistant, feed_one_event, feed_two_event + hass: HomeAssistant, feed_one_event, feed_two_event, feed_only_summary ) -> None: """Test feed event entity.""" entry = create_mock_entry(VALID_CONFIG_DEFAULT) entry.add_to_hass(hass) with patch( "homeassistant.components.feedreader.coordinator.feedparser.http.get", - side_effect=[feed_one_event, feed_two_event], + side_effect=[feed_one_event, feed_two_event, feed_only_summary], ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -45,3 +45,13 @@ async def test_event_entity( assert state.attributes[ATTR_TITLE] == "Title 2" assert state.attributes[ATTR_LINK] == "http://www.example.com/link/2" assert state.attributes[ATTR_CONTENT] == "Content 2" + + future = dt_util.utcnow() + timedelta(hours=2, seconds=2) + async_fire_time_changed(hass, future) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get("event.mock_title") + assert state + assert state.attributes[ATTR_TITLE] == "Title 1" + assert state.attributes[ATTR_LINK] == "http://www.example.com/link/1" + assert state.attributes[ATTR_CONTENT] == "This is a summary" diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index 61e3f13ced7..d7700d79e3b 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -165,6 +165,21 @@ async def test_feed_identical_timestamps( ) +async def test_feed_with_only_summary( + hass: HomeAssistant, events, feed_only_summary +) -> None: + """Test simple feed with only summary, no content.""" + assert await async_setup_config_entry( + hass, VALID_CONFIG_DEFAULT, return_value=feed_only_summary + ) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data.title == "Title 1" + assert events[0].data.description == "Description 1" + assert events[0].data.content[0].value == "This is a summary" + + async def test_feed_updates( hass: HomeAssistant, events, feed_one_event, feed_two_event ) -> None: @@ -247,6 +262,20 @@ async def test_feed_with_unrecognized_publication_date( assert len(events) == 1 +async def test_feed_without_items( + hass: HomeAssistant, events, feed_without_items, caplog: pytest.LogCaptureFixture +) -> None: + """Test simple feed without any items.""" + assert "No new entries to be published in feed" not in caplog.text + assert await async_setup_config_entry( + hass, VALID_CONFIG_DEFAULT, return_value=feed_without_items + ) + await hass.async_block_till_done() + + assert "No new entries to be published in feed" in caplog.text + assert len(events) == 0 + + async def test_feed_invalid_data(hass: HomeAssistant, events) -> None: """Test feed with invalid data.""" assert await async_setup_config_entry( From 7d468908043ae5469258b0d5fe6c1cbf199eb3ac Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 21 Jul 2024 20:57:49 +0200 Subject: [PATCH 1427/2411] Add support for grouping notify entities (#122123) * Add support for grouping notify entities * Add support for grouping notify entities * Add support for grouping notify entities * Fix test * Fix feedback * Update homeassistant/components/group/notify.py Co-authored-by: TheJulianJES * Test config flow changes * Test config flow changes --------- Co-authored-by: TheJulianJES --- homeassistant/components/group/config_flow.py | 12 ++ homeassistant/components/group/notify.py | 87 ++++++++- homeassistant/components/group/strings.json | 15 ++ tests/components/group/test_config_flow.py | 7 + tests/components/group/test_notify.py | 173 +++++++++++++++++- 5 files changed, 289 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 54ef7d0626f..ee8d11d035d 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -32,6 +32,7 @@ from .fan import async_create_preview_fan from .light import async_create_preview_light from .lock import async_create_preview_lock from .media_player import MediaPlayerGroup, async_create_preview_media_player +from .notify import async_create_preview_notify from .sensor import async_create_preview_sensor from .switch import async_create_preview_switch @@ -154,6 +155,7 @@ GROUP_TYPES = [ "light", "lock", "media_player", + "notify", "sensor", "switch", ] @@ -222,6 +224,11 @@ CONFIG_FLOW = { preview="group", validate_user_input=set_group_type("media_player"), ), + "notify": SchemaFlowFormStep( + basic_group_config_schema("notify"), + preview="group", + validate_user_input=set_group_type("notify"), + ), "sensor": SchemaFlowFormStep( SENSOR_CONFIG_SCHEMA, preview="group", @@ -269,6 +276,10 @@ OPTIONS_FLOW = { partial(basic_group_options_schema, "media_player"), preview="group", ), + "notify": SchemaFlowFormStep( + partial(basic_group_options_schema, "notify"), + preview="group", + ), "sensor": SchemaFlowFormStep( partial(sensor_options_schema, "sensor"), preview="group", @@ -293,6 +304,7 @@ CREATE_PREVIEW_ENTITY: dict[ "light": async_create_preview_light, "lock": async_create_preview_lock, "media_player": async_create_preview_media_player, + "notify": async_create_preview_notify, "sensor": async_create_preview_sensor, "switch": async_create_preview_switch, } diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index 444658a6112..8294b55be5e 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -12,15 +12,28 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, + ATTR_TITLE, DOMAIN, PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, + SERVICE_SEND_MESSAGE, BaseNotificationService, + NotifyEntity, ) -from homeassistant.const import ATTR_SERVICE -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SERVICE, + CONF_ENTITIES, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.helpers.entity_registry as er from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .entity import GroupEntity + CONF_SERVICES = "services" PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( @@ -82,3 +95,73 @@ class GroupNotifyPlatform(BaseNotificationService): if tasks: await asyncio.wait(tasks) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Notify Group config entry.""" + registry = er.async_get(hass) + entities = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + + async_add_entities( + [NotifyGroup(config_entry.entry_id, config_entry.title, entities)] + ) + + +@callback +def async_create_preview_notify( + hass: HomeAssistant, name: str, validated_config: dict[str, Any] +) -> NotifyGroup: + """Create a preview notify group.""" + return NotifyGroup( + None, + name, + validated_config[CONF_ENTITIES], + ) + + +class NotifyGroup(GroupEntity, NotifyEntity): + """Representation of a NotifyGroup.""" + + _attr_available: bool = False + + def __init__( + self, + unique_id: str | None, + name: str, + entity_ids: list[str], + ) -> None: + """Initialize a NotifyGroup.""" + self._entity_ids = entity_ids + self._attr_name = name + self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} + self._attr_unique_id = unique_id + + async def async_send_message(self, message: str, title: str | None = None) -> None: + """Send a message to all members of the group.""" + await self.hass.services.async_call( + DOMAIN, + SERVICE_SEND_MESSAGE, + { + ATTR_MESSAGE: message, + ATTR_TITLE: title, + ATTR_ENTITY_ID: self._entity_ids, + }, + blocking=True, + context=self._context, + ) + + @callback + def async_update_group_state(self) -> None: + """Query all members and determine the notify group state.""" + # Set group as unavailable if all members are unavailable or missing + self._attr_available = any( + state.state != STATE_UNAVAILABLE + for entity_id in self._entity_ids + if (state := self.hass.states.get(entity_id)) is not None + ) diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index dc850804d94..dbb6fb01f7b 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -14,6 +14,7 @@ "light": "Light group", "lock": "Lock group", "media_player": "Media player group", + "notify": "Notify group", "sensor": "Sensor group", "switch": "Switch group" } @@ -84,6 +85,14 @@ "name": "[%key:common::config_flow::data::name%]" } }, + "notify": { + "title": "[%key:component::group::config::step::user::title%]", + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", + "name": "[%key:common::config_flow::data::name%]" + } + }, "sensor": { "title": "[%key:component::group::config::step::user::title%]", "data": { @@ -156,6 +165,12 @@ "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" } }, + "notify": { + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + } + }, "sensor": { "description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values.", "data": { diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index dc40b647e2e..461df19ebf8 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -46,6 +46,7 @@ from tests.typing import WebSocketGenerator ("fan", "on", "on", {}, {}, {}, {}), ("light", "on", "on", {}, {}, {}, {}), ("lock", "locked", "locked", {}, {}, {}, {}), + ("notify", STATE_UNKNOWN, "2021-01-01T23:59:59.123+00:00", {}, {}, {}, {}), ("media_player", "on", "on", {}, {}, {}, {}), ( "sensor", @@ -142,6 +143,7 @@ async def test_config_flow( ("fan", {}), ("light", {}), ("lock", {}), + ("notify", {}), ("media_player", {}), ("switch", {}), ], @@ -220,6 +222,7 @@ def get_suggested(schema, key): ("fan", "on", {}, {}), ("light", "on", {"all": False}, {}), ("lock", "locked", {}, {}), + ("notify", "2021-01-01T23:59:59.123+00:00", {}, {}), ("media_player", "on", {}, {}), ( "sensor", @@ -405,6 +408,7 @@ async def test_all_options( ("fan", {}), ("light", {}), ("lock", {}), + ("notify", {}), ("media_player", {}), ("switch", {}), ], @@ -487,6 +491,7 @@ LIGHT_ATTRS = [ {"color_mode": "unknown"}, ] LOCK_ATTRS = [{"supported_features": 1}, {}] +NOTIFY_ATTRS = [{"supported_features": 0}, {}] MEDIA_PLAYER_ATTRS = [{"supported_features": 0}, {}] SENSOR_ATTRS = [{"icon": "mdi:calculator"}, {"max_entity_id": "sensor.input_two"}] @@ -501,6 +506,7 @@ SENSOR_ATTRS = [{"icon": "mdi:calculator"}, {"max_entity_id": "sensor.input_two" ("fan", {}, ["on", "off"], "on", FAN_ATTRS), ("light", {}, ["on", "off"], "on", LIGHT_ATTRS), ("lock", {}, ["unlocked", "locked"], "unlocked", LOCK_ATTRS), + ("notify", {}, ["", ""], "unknown", NOTIFY_ATTRS), ("media_player", {}, ["on", "off"], "on", MEDIA_PLAYER_ATTRS), ("sensor", {"type": "max"}, ["10", "20"], "20.0", SENSOR_ATTRS), ("switch", {}, ["on", "off"], "on", [{}, {}]), @@ -611,6 +617,7 @@ async def test_config_flow_preview( ("fan", {}, {}, ["on", "off"], "on", FAN_ATTRS), ("light", {}, {}, ["on", "off"], "on", LIGHT_ATTRS), ("lock", {}, {}, ["unlocked", "locked"], "unlocked", LOCK_ATTRS), + ("notify", {}, {}, ["", ""], "unknown", NOTIFY_ATTRS), ("media_player", {}, {}, ["on", "off"], "on", MEDIA_PLAYER_ATTRS), ( "sensor", diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index dfd200a1542..2595b211dae 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -1,18 +1,44 @@ """The tests for the notify.group platform.""" -from collections.abc import Mapping +from collections.abc import Generator, Mapping from pathlib import Path from typing import Any from unittest.mock import MagicMock, call, patch +import pytest + from homeassistant import config as hass_config from homeassistant.components import notify -from homeassistant.components.group import SERVICE_RELOAD +from homeassistant.components.group import DOMAIN, SERVICE_RELOAD +from homeassistant.components.notify import ( + ATTR_MESSAGE, + ATTR_TITLE, + DOMAIN as NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, + NotifyEntity, +) +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import async_setup_component -from tests.common import MockPlatform, get_fixture_path, mock_platform +from tests.common import ( + MockConfigEntry, + MockEntity, + MockModule, + MockPlatform, + get_fixture_path, + mock_config_flow, + mock_integration, + mock_platform, + setup_test_component_platform, +) class MockNotifyPlatform(MockPlatform): @@ -217,3 +243,144 @@ async def test_reload_notify(hass: HomeAssistant, tmp_path: Path) -> None: assert hass.services.has_service(notify.DOMAIN, "test_service2") assert not hass.services.has_service(notify.DOMAIN, "group_notify") assert hass.services.has_service(notify.DOMAIN, "new_group_notify") + + +class MockFlow(ConfigFlow): + """Test flow.""" + + +@pytest.fixture +def config_flow_fixture(hass: HomeAssistant) -> Generator[None]: + """Mock config flow.""" + mock_platform(hass, "test.config_flow") + + with mock_config_flow("test", MockFlow): + yield + + +class MockNotifyEntity(MockEntity, NotifyEntity): + """Mock Email notifier entity to use in tests.""" + + def __init__(self, **values: Any) -> None: + """Initialize the mock entity.""" + super().__init__(**values) + self.send_message_mock_calls = MagicMock() + + async def async_send_message(self, message: str, title: str | None = None) -> None: + """Send a notification message.""" + self.send_message_mock_calls(message, title=title) + + +async def help_async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry +) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups( + config_entry, [Platform.NOTIFY] + ) + return True + + +async def help_async_unload_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> bool: + """Unload test config entry.""" + return await hass.config_entries.async_unload_platforms( + config_entry, [Platform.NOTIFY] + ) + + +@pytest.fixture +async def mock_notifiers( + hass: HomeAssistant, config_flow_fixture: None +) -> list[NotifyEntity]: + """Set up the notify entities.""" + entity = MockNotifyEntity(name="test", entity_id="notify.test") + entity2 = MockNotifyEntity(name="test2", entity_id="notify.test2") + entities = [entity, entity2] + test_entry = MockConfigEntry(domain="test") + test_entry.add_to_hass(hass) + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=help_async_setup_entry_init, + async_unload_entry=help_async_unload_entry, + ), + ) + setup_test_component_platform(hass, NOTIFY_DOMAIN, entities, from_config_entry=True) + assert await hass.config_entries.async_setup(test_entry.entry_id) + await hass.async_block_till_done() + return entities + + +async def test_notify_entity_group( + hass: HomeAssistant, mock_notifiers: list[NotifyEntity] +) -> None: + """Test sending a message to a notify group.""" + entity, entity2 = mock_notifiers + assert entity.send_message_mock_calls.call_count == 0 + assert entity2.send_message_mock_calls.call_count == 0 + + config_entry = MockConfigEntry( + domain=DOMAIN, + options={ + "group_type": "notify", + "name": "Test Group", + "entities": ["notify.test", "notify.test2"], + "hide_members": True, + }, + title="Test Group", + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, + { + ATTR_MESSAGE: "Hello", + ATTR_TITLE: "Test notification", + ATTR_ENTITY_ID: "notify.test_group", + }, + blocking=True, + ) + + assert entity.send_message_mock_calls.call_count == 1 + assert entity.send_message_mock_calls.call_args == call( + "Hello", title="Test notification" + ) + assert entity2.send_message_mock_calls.call_count == 1 + assert entity2.send_message_mock_calls.call_args == call( + "Hello", title="Test notification" + ) + + +async def test_state_reporting(hass: HomeAssistant) -> None: + """Test sending a message to a notify group.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + options={ + "group_type": "notify", + "name": "Test Group", + "entities": ["notify.test", "notify.test2"], + "hide_members": True, + }, + title="Test Group", + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("notify.test_group").state == STATE_UNAVAILABLE + + hass.states.async_set("notify.test", STATE_UNAVAILABLE) + hass.states.async_set("notify.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("notify.test_group").state == STATE_UNAVAILABLE + + hass.states.async_set("notify.test", "2021-01-01T23:59:59.123+00:00") + hass.states.async_set("notify.test2", "2021-01-01T23:59:59.123+00:00") + await hass.async_block_till_done() + assert hass.states.get("notify.test_group").state == STATE_UNKNOWN From 453848bcdc1ae0a05263d3e36354d25ac470d65e Mon Sep 17 00:00:00 2001 From: Lorzware Date: Sun, 21 Jul 2024 22:03:41 +0200 Subject: [PATCH 1428/2411] APSystems - add configuration option 'port' in config flow (#122144) * Add configuration option 'port' in config flow --- .../components/apsystems/__init__.py | 9 +++- .../components/apsystems/config_flow.py | 14 ++++-- homeassistant/components/apsystems/const.py | 1 + .../components/apsystems/strings.json | 6 ++- .../components/apsystems/test_config_flow.py | 47 ++++++++++++++++++- 5 files changed, 69 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/apsystems/__init__.py b/homeassistant/components/apsystems/__init__.py index 2df267dda0b..40e62a32475 100644 --- a/homeassistant/components/apsystems/__init__.py +++ b/homeassistant/components/apsystems/__init__.py @@ -7,9 +7,10 @@ from dataclasses import dataclass from APsystemsEZ1 import APsystemsEZ1M from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, Platform +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform from homeassistant.core import HomeAssistant +from .const import DEFAULT_PORT from .coordinator import ApSystemsDataCoordinator PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR] @@ -28,7 +29,11 @@ type ApSystemsConfigEntry = ConfigEntry[ApSystemsData] async def async_setup_entry(hass: HomeAssistant, entry: ApSystemsConfigEntry) -> bool: """Set up this integration using UI.""" - api = APsystemsEZ1M(ip_address=entry.data[CONF_IP_ADDRESS], timeout=8) + api = APsystemsEZ1M( + ip_address=entry.data[CONF_IP_ADDRESS], + port=entry.data.get(CONF_PORT, DEFAULT_PORT), + timeout=8, + ) coordinator = ApSystemsDataCoordinator(hass, api) await coordinator.async_config_entry_first_refresh() assert entry.unique_id diff --git a/homeassistant/components/apsystems/config_flow.py b/homeassistant/components/apsystems/config_flow.py index f49237ce450..5f2f1393aa0 100644 --- a/homeassistant/components/apsystems/config_flow.py +++ b/homeassistant/components/apsystems/config_flow.py @@ -7,14 +7,16 @@ from APsystemsEZ1 import APsystemsEZ1M import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv -from .const import DOMAIN +from .const import DEFAULT_PORT, DOMAIN DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_IP_ADDRESS): str, + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_PORT): cv.port, } ) @@ -32,7 +34,11 @@ class APsystemsLocalAPIFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: session = async_get_clientsession(self.hass, False) - api = APsystemsEZ1M(user_input[CONF_IP_ADDRESS], session=session) + api = APsystemsEZ1M( + ip_address=user_input[CONF_IP_ADDRESS], + port=user_input.get(CONF_PORT, DEFAULT_PORT), + session=session, + ) try: device_info = await api.get_device_info() except (TimeoutError, ClientConnectionError): diff --git a/homeassistant/components/apsystems/const.py b/homeassistant/components/apsystems/const.py index 857652aeae8..4edf0f4122a 100644 --- a/homeassistant/components/apsystems/const.py +++ b/homeassistant/components/apsystems/const.py @@ -4,3 +4,4 @@ from logging import Logger, getLogger LOGGER: Logger = getLogger(__package__) DOMAIN = "apsystems" +DEFAULT_PORT = 8050 diff --git a/homeassistant/components/apsystems/strings.json b/homeassistant/components/apsystems/strings.json index cfd24675311..95499e96b4d 100644 --- a/homeassistant/components/apsystems/strings.json +++ b/homeassistant/components/apsystems/strings.json @@ -3,7 +3,11 @@ "step": { "user": { "data": { - "ip_address": "[%key:common::config_flow::data::ip%]" + "ip_address": "[%key:common::config_flow::data::ip%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "port": "The integration will default to 8050, if not set, which should be suitable for most installs" } } }, diff --git a/tests/components/apsystems/test_config_flow.py b/tests/components/apsystems/test_config_flow.py index e3fcdf67dcc..3d78524a529 100644 --- a/tests/components/apsystems/test_config_flow.py +++ b/tests/components/apsystems/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock from homeassistant.components.apsystems.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -27,6 +27,24 @@ async def test_form_create_success( assert result["data"].get(CONF_IP_ADDRESS) == "127.0.0.1" +async def test_form_create_success_custom_port( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_apsystems: AsyncMock +) -> None: + """Test we handle creating with custom port with success.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_IP_ADDRESS: "127.0.0.1", + CONF_PORT: 8042, + }, + ) + assert result["result"].unique_id == "MY_SERIAL_NUMBER" + assert result.get("type") is FlowResultType.CREATE_ENTRY + assert result["data"].get(CONF_IP_ADDRESS) == "127.0.0.1" + assert result["data"].get(CONF_PORT) == 8042 + + async def test_form_cannot_connect_and_recover( hass: HomeAssistant, mock_apsystems: AsyncMock, mock_setup_entry: AsyncMock ) -> None: @@ -57,6 +75,33 @@ async def test_form_cannot_connect_and_recover( assert result2["data"].get(CONF_IP_ADDRESS) == "127.0.0.1" +async def test_form_cannot_connect_and_recover_custom_port( + hass: HomeAssistant, mock_apsystems: AsyncMock, mock_setup_entry: AsyncMock +) -> None: + """Test we handle cannot connect error but recovering with custom port.""" + + mock_apsystems.get_device_info.side_effect = TimeoutError + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_IP_ADDRESS: "127.0.0.2", CONF_PORT: 8042}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + mock_apsystems.get_device_info.side_effect = None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_IP_ADDRESS: "127.0.0.1", CONF_PORT: 8042}, + ) + assert result2["result"].unique_id == "MY_SERIAL_NUMBER" + assert result2.get("type") is FlowResultType.CREATE_ENTRY + assert result2["data"].get(CONF_IP_ADDRESS) == "127.0.0.1" + assert result2["data"].get(CONF_PORT) == 8042 + + async def test_form_unique_id_already_configured( hass: HomeAssistant, mock_setup_entry: AsyncMock, From c98c80ce69f55cfd5e138ac228e1bf495379ffd3 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 21 Jul 2024 13:37:18 -0700 Subject: [PATCH 1429/2411] Change OpenAI default recommended model to gpt-4o-mini (#122333) --- homeassistant/components/openai_conversation/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index f362f4278a1..e8ee003fcca 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -8,7 +8,7 @@ LOGGER = logging.getLogger(__package__) CONF_RECOMMENDED = "recommended" CONF_PROMPT = "prompt" CONF_CHAT_MODEL = "chat_model" -RECOMMENDED_CHAT_MODEL = "gpt-4o" +RECOMMENDED_CHAT_MODEL = "gpt-4o-mini" CONF_MAX_TOKENS = "max_tokens" RECOMMENDED_MAX_TOKENS = 150 CONF_TOP_P = "top_p" From 9a3c7111f7d8da6e4d06d55ce21654d49c9775ea Mon Sep 17 00:00:00 2001 From: Alexander Schneider Date: Sun, 21 Jul 2024 14:51:10 -0700 Subject: [PATCH 1430/2411] Add Z-Wave discovery schema for ZVIDAR roller shades (#122332) Add discovery schema for ZVIDAR roller shades --- .../components/zwave_js/discovery.py | 9 + tests/components/zwave_js/conftest.py | 14 + .../zwave_js/fixtures/cover_zvidar_state.json | 1120 +++++++++++++++++ tests/components/zwave_js/test_discovery.py | 12 + 4 files changed, 1155 insertions(+) create mode 100644 tests/components/zwave_js/fixtures/cover_zvidar_state.json diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index c3a2884cb7a..6e750ee8b2d 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -579,6 +579,15 @@ DISCOVERY_SCHEMAS = [ ), entity_registry_enabled_default=False, ), + # ZVIDAR Z-CM-V01 (SmartWings/Deyi WM25L/V Z-Wave Motor for Roller Shade) + ZWaveDiscoverySchema( + platform=Platform.COVER, + hint="shade", + manufacturer_id={0x045A}, + product_id={0x0507}, + product_type={0x0904}, + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, + ), # Vision Security ZL7432 In Wall Dual Relay Switch ZWaveDiscoverySchema( platform=Platform.SWITCH, diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index a98a057b293..60deb7dbce8 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -488,6 +488,12 @@ def iblinds_v3_state_fixture(): return json.loads(load_fixture("zwave_js/cover_iblinds_v3_state.json")) +@pytest.fixture(name="zvidar_state", scope="package") +def zvidar_state_fixture(): + """Load the ZVIDAR node state fixture data.""" + return json.loads(load_fixture("zwave_js/cover_zvidar_state.json")) + + @pytest.fixture(name="qubino_shutter_state", scope="package") def qubino_shutter_state_fixture(): """Load the Qubino Shutter node state fixture data.""" @@ -1097,6 +1103,14 @@ def iblinds_v3_cover_fixture(client, iblinds_v3_state): return node +@pytest.fixture(name="zvidar") +def zvidar_cover_fixture(client, zvidar_state): + """Mock a ZVIDAR window cover node.""" + node = Node(client, copy.deepcopy(zvidar_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="qubino_shutter") def qubino_shutter_cover_fixture(client, qubino_shutter_state): """Mock a Qubino flush shutter node.""" diff --git a/tests/components/zwave_js/fixtures/cover_zvidar_state.json b/tests/components/zwave_js/fixtures/cover_zvidar_state.json new file mode 100644 index 00000000000..05118931026 --- /dev/null +++ b/tests/components/zwave_js/fixtures/cover_zvidar_state.json @@ -0,0 +1,1120 @@ +{ + "nodeId": 270, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": false, + "isSecure": true, + "manufacturerId": 1114, + "productId": 1287, + "productType": 2308, + "firmwareVersion": "1.10.0", + "zwavePlusVersion": 2, + "name": "Window Blind Controller", + "location": "**REDACTED**", + "deviceConfig": { + "filename": "/snapshot/build/node_modules/@zwave-js/config/config/devices/0x045a/Z-CM-V01.json", + "isEmbedded": true, + "manufacturer": "ZVIDAR", + "manufacturerId": 1114, + "label": "Z-CM-V01", + "description": "Smart Curtain Motor", + "devices": [ + { + "productType": 2308, + "productId": 1287 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "paramInformation": { + "_map": {} + }, + "compat": { + "removeCCs": {} + } + }, + "label": "Z-CM-V01", + "interviewAttempts": 0, + "isFrequentListening": "1000ms", + "maxDataRate": 100000, + "supportedDataRates": [100000], + "protocolVersion": 3, + "supportsBeaming": false, + "supportsSecurity": true, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 7, + "deviceClass": { + "basic": { + "key": 3, + "label": "End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x045a:0x0904:0x0507:1.10.0", + "statistics": { + "commandsTX": 2, + "commandsRX": 1, + "commandsDroppedRX": 1, + "commandsDroppedTX": 0, + "timeoutResponse": 0, + "rtt": 357.6, + "lastSeen": "2024-07-21T16:42:38.086Z", + "rssi": -89, + "lwr": { + "protocolDataRate": 4, + "repeaters": [], + "rssi": -91, + "repeaterRSSI": [] + } + }, + "highestSecurityClass": 1, + "isControllerNode": false, + "keepAwake": false, + "lastSeen": "2024-07-21T16:42:38.086Z", + "protocol": 1, + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": "unknown" + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "states": { + "true": "Restore" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Hand Button Action", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Hand Button Action", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Close", + "1": "Open" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Motor Direction", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Motor Direction", + "default": 1, + "min": 1, + "max": 3, + "states": { + "1": "Forward", + "2": "Opposite", + "3": "Reverse" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Manually Set Open Boundary", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Manually Set Open Boundary", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Cancel", + "1": "Start" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Manually Set Closed Boundary", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Manually Set Closed Boundary", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Cancel", + "1": "Start" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Control Motor", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Control Motor", + "default": 3, + "min": 1, + "max": 3, + "states": { + "1": "Open (Up)", + "2": "Close (Down)", + "3": "Stop" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Calibrate Limit Position", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Calibrate Limit Position", + "default": 1, + "min": 1, + "max": 3, + "states": { + "1": "Upper limit", + "2": "Lower limit", + "3": "Third limit" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Delete Limit Position", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Delete Limit Position", + "default": 0, + "min": 0, + "max": 3, + "states": { + "0": "All limits", + "1": "Only upper limit", + "2": "Only lower limit", + "3": "Only third limit" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Low Battery Level Alarm Threshold", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Low Battery Level Alarm Threshold", + "default": 10, + "min": 0, + "max": 50, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Battery Report Interval", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Battery Report Interval", + "default": 3600, + "min": 0, + "max": 2678400, + "unit": "seconds", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3600 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Battery Change Report Threshold", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Battery Change Report Threshold", + "default": 5, + "min": 0, + "max": 50, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Mains status", + "propertyName": "Power Management", + "propertyKeyName": "Mains status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Mains status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "2": "AC mains disconnected", + "3": "AC mains re-connected" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 1114 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 2308 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 1287 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%", + "stateful": true, + "secret": false + }, + "value": 86 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.16" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["1.10"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version", + "stateful": true, + "secret": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version", + "stateful": true, + "secret": false + }, + "value": "7.16.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version", + "stateful": true, + "secret": false + }, + "value": "10.16.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number", + "stateful": true, + "secret": false + }, + "value": 297 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version", + "stateful": true, + "secret": false + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number", + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.16.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number", + "stateful": true, + "secret": false + }, + "value": 297 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version", + "stateful": true, + "secret": false + }, + "value": "1.10.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number", + "stateful": true, + "secret": false + }, + "value": 43707 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 3, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "Node Identify - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 4, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "Node Identify - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 5, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "Node Identify - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "value", + "propertyName": "value", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Indicator value", + "ccSpecific": { + "indicatorId": 0 + }, + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "identify", + "propertyName": "identify", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Identify", + "states": { + "true": "Identify" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "timeout", + "propertyName": "timeout", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "Timeout", + "stateful": true, + "secret": false + } + } + ], + "endpoints": [ + { + "nodeId": 261, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "deviceClass": { + "basic": { + "key": 3, + "label": "End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": true + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 135, + "name": "Indicator", + "version": 3, + "isSecure": true + } + ] + } + ] +} diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 1179d8e843c..57841ef2a83 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -49,6 +49,18 @@ async def test_iblinds_v2(hass: HomeAssistant, client, iblinds_v2, integration) assert state +async def test_zvidar_state(hass: HomeAssistant, client, zvidar, integration) -> None: + """Test that an ZVIDAR Z-CM-V01 multilevel switch value is discovered as a cover.""" + node = zvidar + assert node.device_class.specific.label == "Unused" + + state = hass.states.get("light.window_blind_controller") + assert not state + + state = hass.states.get("cover.window_blind_controller") + assert state + + async def test_ge_12730(hass: HomeAssistant, client, ge_12730, integration) -> None: """Test GE 12730 Fan Controller v2.0 multilevel switch is discovered as a fan.""" node = ge_12730 From bc5849e4ef8723a488eb2e5c9cfec60a399fbd69 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 22 Jul 2024 01:11:05 +0200 Subject: [PATCH 1431/2411] Bump `aiotractive` to 0.6.0 (#121155) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> Co-authored-by: J. Nick Koston --- homeassistant/components/tractive/__init__.py | 2 +- homeassistant/components/tractive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index fd5abe24c06..4f0de7b14cd 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -132,7 +132,7 @@ async def _generate_trackables( trackable = await trackable.details() # Check that the pet has tracker linked. - if not trackable["device_id"]: + if not trackable.get("device_id"): return None if "details" not in trackable: diff --git a/homeassistant/components/tractive/manifest.json b/homeassistant/components/tractive/manifest.json index 75ddf065bd7..903c5347d52 100644 --- a/homeassistant/components/tractive/manifest.json +++ b/homeassistant/components/tractive/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "cloud_push", "loggers": ["aiotractive"], - "requirements": ["aiotractive==0.5.6"] + "requirements": ["aiotractive==0.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2dd24182707..08511d1b191 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -383,7 +383,7 @@ aiosyncthing==0.5.1 aiotankerkoenig==0.4.1 # homeassistant.components.tractive -aiotractive==0.5.6 +aiotractive==0.6.0 # homeassistant.components.unifi aiounifi==79 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 84cf2487545..a4d8964da6d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -362,7 +362,7 @@ aiosyncthing==0.5.1 aiotankerkoenig==0.4.1 # homeassistant.components.tractive -aiotractive==0.5.6 +aiotractive==0.6.0 # homeassistant.components.unifi aiounifi==79 From 4eb096cb0a593bc35f9cd9a2b3c797d2700a4aff Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 22 Jul 2024 01:44:52 +0200 Subject: [PATCH 1432/2411] Update pylint to 3.2.6 (#122338) --- requirements_test.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 2305750ae92..30e6445b8a7 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,14 +7,14 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt -astroid==3.2.3 +astroid==3.2.4 coverage==7.6.0 freezegun==1.5.1 mock-open==1.4.0 mypy-dev==1.12.0a1 pre-commit==3.7.1 pydantic==1.10.17 -pylint==3.2.5 +pylint==3.2.6 pylint-per-file-ignores==1.3.2 pipdeptree==2.23.1 pip-licenses==4.5.1 From ac1ad9680bc8200b8ca17b37e0e7a49b0eae8513 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Mon, 22 Jul 2024 07:54:31 +0300 Subject: [PATCH 1433/2411] Goofle Generative AI: Fix string format (#122348) * Ignore format for string tool args * Add tests --- .../google_generative_ai_conversation/conversation.py | 2 ++ .../snapshots/test_conversation.ambr | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 127ca2cae95..4a93f6ca569 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -81,6 +81,8 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: key = "type_" val = val.upper() elif key == "format": + if schema.get("type") == "string" and val != "enum": + continue key = "format_" elif key == "items": val = _format_schema(val) diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index 7f28c172970..66caf4c7218 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -449,7 +449,6 @@ description: "Test parameters" items { type_: STRING - format_: "lower" } } } From db9fc27a5ce8fda73222b528c33b692bf3e216bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Jul 2024 00:44:00 -0500 Subject: [PATCH 1434/2411] Convert enphase_envoy to use entry.runtime_data (#122345) --- .../components/enphase_envoy/__init__.py | 20 ++++++++----------- .../components/enphase_envoy/binary_sensor.py | 7 +++---- .../components/enphase_envoy/coordinator.py | 7 ++++++- .../components/enphase_envoy/diagnostics.py | 9 ++++----- .../components/enphase_envoy/number.py | 7 +++---- .../components/enphase_envoy/select.py | 7 +++---- .../components/enphase_envoy/sensor.py | 7 +++---- .../components/enphase_envoy/switch.py | 10 +++------- 8 files changed, 33 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 322f909437a..f6438230789 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations from pyenphase import Envoy -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -12,10 +11,10 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.httpx_client import get_async_client from .const import DOMAIN, PLATFORMS -from .coordinator import EnphaseUpdateCoordinator +from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> bool: """Set up Enphase Envoy from a config entry.""" host = entry.data[CONF_HOST] @@ -37,29 +36,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"found {envoy.serial_number}" ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> bool: """Unload a config entry.""" - coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: EnphaseUpdateCoordinator = entry.runtime_data coordinator.async_cancel_token_refresh() - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_remove_config_entry_device( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry + hass: HomeAssistant, config_entry: EnphaseConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove an enphase_envoy config entry from a device.""" dev_ids = {dev_id[1] for dev_id in device_entry.identifiers if dev_id[0] == DOMAIN} - coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data envoy_data = coordinator.envoy.data envoy_serial_num = config_entry.unique_id if envoy_serial_num in dev_ids: diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index dbd8498467f..6be29d19ecb 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -13,14 +13,13 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import EnphaseUpdateCoordinator +from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator from .entity import EnvoyBaseEntity @@ -74,11 +73,11 @@ ENPOWER_SENSORS = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: EnphaseConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up envoy binary sensor platform.""" - coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data envoy_data = coordinator.envoy.data assert envoy_data is not None entities: list[BinarySensorEntity] = [] diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index 04f93098ad9..e91e245658c 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -28,12 +28,17 @@ STALE_TOKEN_THRESHOLD = timedelta(days=30).total_seconds() _LOGGER = logging.getLogger(__name__) +type EnphaseConfigEntry = ConfigEntry[EnphaseUpdateCoordinator] + + class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """DataUpdateCoordinator to gather data from any envoy.""" envoy_serial_number: str - def __init__(self, hass: HomeAssistant, envoy: Envoy, entry: ConfigEntry) -> None: + def __init__( + self, hass: HomeAssistant, envoy: Envoy, entry: EnphaseConfigEntry + ) -> None: """Initialize DataUpdateCoordinator for the envoy.""" self.envoy = envoy entry_data = entry.data diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py index 0fe7be8aaef..b3323687e7c 100644 --- a/homeassistant/components/enphase_envoy/diagnostics.py +++ b/homeassistant/components/enphase_envoy/diagnostics.py @@ -10,7 +10,6 @@ from pyenphase.envoy import Envoy from pyenphase.exceptions import EnvoyError from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -23,8 +22,8 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.json import json_dumps from homeassistant.util.json import json_loads -from .const import DOMAIN, OPTION_DIAGNOSTICS_INCLUDE_FIXTURES -from .coordinator import EnphaseUpdateCoordinator +from .const import OPTION_DIAGNOSTICS_INCLUDE_FIXTURES +from .coordinator import EnphaseConfigEntry CONF_TITLE = "title" CLEAN_TEXT = "<>" @@ -81,10 +80,10 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]: async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: EnphaseConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data if TYPE_CHECKING: assert coordinator.envoy.data diff --git a/homeassistant/components/enphase_envoy/number.py b/homeassistant/components/enphase_envoy/number.py index 63c5879cfe8..2c0708d9215 100644 --- a/homeassistant/components/enphase_envoy/number.py +++ b/homeassistant/components/enphase_envoy/number.py @@ -16,14 +16,13 @@ from homeassistant.components.number import ( NumberEntity, NumberEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import EnphaseUpdateCoordinator +from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator from .entity import EnvoyBaseEntity @@ -71,11 +70,11 @@ STORAGE_RESERVE_SOC_ENTITY = EnvoyStorageSettingsNumberEntityDescription( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: EnphaseConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Enphase Envoy number platform.""" - coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data envoy_data = coordinator.envoy.data assert envoy_data is not None entities: list[NumberEntity] = [] diff --git a/homeassistant/components/enphase_envoy/select.py b/homeassistant/components/enphase_envoy/select.py index 0971c7b5715..78ebaa26d13 100644 --- a/homeassistant/components/enphase_envoy/select.py +++ b/homeassistant/components/enphase_envoy/select.py @@ -12,13 +12,12 @@ from pyenphase.models.dry_contacts import DryContactAction, DryContactMode from pyenphase.models.tariff import EnvoyStorageMode, EnvoyStorageSettings from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import EnphaseUpdateCoordinator +from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator from .entity import EnvoyBaseEntity @@ -126,11 +125,11 @@ STORAGE_MODE_ENTITY = EnvoyStorageSettingsSelectEntityDescription( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: EnphaseConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Enphase Envoy select platform.""" - coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data envoy_data = coordinator.envoy.data assert envoy_data is not None entities: list[SelectEntity] = [] diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 13445d8897a..e6c7a585eb7 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -33,7 +33,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, UnitOfApparentPower, @@ -50,7 +49,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util from .const import DOMAIN -from .coordinator import EnphaseUpdateCoordinator +from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator from .entity import EnvoyBaseEntity ICON = "mdi:flash" @@ -579,11 +578,11 @@ ENCHARGE_AGGREGATE_SENSORS = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: EnphaseConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up envoy sensor platform.""" - coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data envoy_data = coordinator.envoy.data assert envoy_data is not None _LOGGER.debug("Envoy data: %s", envoy_data) diff --git a/homeassistant/components/enphase_envoy/switch.py b/homeassistant/components/enphase_envoy/switch.py index dbe14ee94ea..09711cd5908 100644 --- a/homeassistant/components/enphase_envoy/switch.py +++ b/homeassistant/components/enphase_envoy/switch.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass -import logging from typing import Any from pyenphase import Envoy, EnvoyDryContactStatus, EnvoyEnpower @@ -13,17 +12,14 @@ from pyenphase.models.dry_contacts import DryContactStatus from pyenphase.models.tariff import EnvoyStorageSettings from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import EnphaseUpdateCoordinator +from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator from .entity import EnvoyBaseEntity -_LOGGER = logging.getLogger(__name__) - @dataclass(frozen=True, kw_only=True) class EnvoyEnpowerSwitchEntityDescription(SwitchEntityDescription): @@ -78,11 +74,11 @@ CHARGE_FROM_GRID_SWITCH = EnvoyStorageSettingsSwitchEntityDescription( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: EnphaseConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Enphase Envoy switch platform.""" - coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data envoy_data = coordinator.envoy.data assert envoy_data is not None entities: list[SwitchEntity] = [] From f30c6e01f9e7deeb398b1be350014ebce5890982 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Mon, 22 Jul 2024 02:56:48 -0400 Subject: [PATCH 1435/2411] Bump aiorussound to 2.0.6 (#122354) bump aiorussound to 2.0.6 --- .../components/russound_rio/config_flow.py | 11 +-- .../components/russound_rio/manifest.json | 2 +- .../components/russound_rio/media_player.py | 98 +++++++------------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/russound_rio/conftest.py | 4 +- tests/components/russound_rio/const.py | 5 + .../russound_rio/test_config_flow.py | 8 +- 8 files changed, 57 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/russound_rio/config_flow.py b/homeassistant/components/russound_rio/config_flow.py index 9ad0d25ff94..e25ac7dde2e 100644 --- a/homeassistant/components/russound_rio/config_flow.py +++ b/homeassistant/components/russound_rio/config_flow.py @@ -6,7 +6,7 @@ import asyncio import logging from typing import Any -from aiorussound import Russound +from aiorussound import Controller, Russound import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -31,13 +31,12 @@ _LOGGER = logging.getLogger(__name__) def find_primary_controller_metadata( - controllers: list[tuple[int, str, str]], + controllers: dict[int, Controller], ) -> tuple[str, str]: """Find the mac address of the primary Russound controller.""" - for controller_id, mac_address, controller_type in controllers: - # The integration only cares about the primary controller linked by IP and not any downstream controllers - if controller_id == 1: - return (mac_address, controller_type) + if 1 in controllers: + c = controllers[1] + return c.mac_address, c.controller_type raise NoPrimaryControllerException diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 43cf8e7850f..7dcdf228244 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/russound_rio", "iot_class": "local_push", "loggers": ["aiorussound"], - "requirements": ["aiorussound==1.1.2"] + "requirements": ["aiorussound==2.0.6"] } diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index e3eae51eb9e..a96269ab906 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -4,6 +4,8 @@ from __future__ import annotations import logging +from aiorussound import Source, Zone + from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, @@ -80,21 +82,18 @@ async def async_setup_entry( """Set up the Russound RIO platform.""" russ = entry.runtime_data - # Discover sources and zones - sources = await russ.enumerate_sources() - valid_zones = await russ.enumerate_zones() + # Discover controllers + controllers = await russ.enumerate_controllers() entities = [] - for zone_id, name in valid_zones: - if zone_id.controller > 6: - _LOGGER.debug( - "Zone ID %s exceeds RIO controller maximum, skipping", - zone_id.device_str(), - ) - continue - await russ.watch_zone(zone_id) - zone = RussoundZoneDevice(russ, zone_id, name, sources) - entities.append(zone) + for controller in controllers: + sources = controller.sources + for source in sources.values(): + await source.watch() + for zone in controller.zones.values(): + await zone.watch() + mp = RussoundZoneDevice(zone, sources) + entities.append(mp) @callback def on_stop(event): @@ -119,56 +118,35 @@ class RussoundZoneDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE ) - def __init__(self, russ, zone_id, name, sources) -> None: + def __init__(self, zone: Zone, sources: dict[int, Source]) -> None: """Initialize the zone device.""" super().__init__() - self._name = name - self._russ = russ - self._zone_id = zone_id + self._zone = zone self._sources = sources - def _zone_var(self, name, default=None): - return self._russ.get_cached_zone_variable(self._zone_id, name, default) - - def _source_var(self, name, default=None): - current = int(self._zone_var("currentsource", 0)) - if current: - return self._russ.get_cached_source_variable(current, name, default) - return default - - def _source_na_var(self, name): - """Will replace invalid values with None.""" - current = int(self._zone_var("currentsource", 0)) - if current: - value = self._russ.get_cached_source_variable(current, name, None) - if value in (None, "", "------"): - return None - return value - return None - - def _zone_callback_handler(self, zone_id, *args): - if zone_id == self._zone_id: - self.schedule_update_ha_state() - - def _source_callback_handler(self, source_id, *args): - current = int(self._zone_var("currentsource", 0)) - if source_id == current: + def _callback_handler(self, device_str, *args): + if ( + device_str == self._zone.device_str() + or device_str == self._current_source().device_str() + ): self.schedule_update_ha_state() async def async_added_to_hass(self) -> None: """Register callback handlers.""" - self._russ.add_zone_callback(self._zone_callback_handler) - self._russ.add_source_callback(self._source_callback_handler) + self._zone.add_callback(self._callback_handler) + + def _current_source(self) -> Source: + return self._zone.fetch_current_source() @property def name(self): """Return the name of the zone.""" - return self._zone_var("name", self._name) + return self._zone.name @property def state(self) -> MediaPlayerState | None: """Return the state of the device.""" - status = self._zone_var("status", "OFF") + status = self._zone.status if status == "ON": return MediaPlayerState.ON if status == "OFF": @@ -178,32 +156,32 @@ class RussoundZoneDevice(MediaPlayerEntity): @property def source(self): """Get the currently selected source.""" - return self._source_na_var("name") + return self._current_source().name @property def source_list(self): """Return a list of available input sources.""" - return [x[1] for x in self._sources] + return [x.name for x in self._sources.values()] @property def media_title(self): """Title of current playing media.""" - return self._source_na_var("songname") + return self._current_source().song_name @property def media_artist(self): """Artist of current playing media, music track only.""" - return self._source_na_var("artistname") + return self._current_source().artist_name @property def media_album_name(self): """Album name of current playing media, music track only.""" - return self._source_na_var("albumname") + return self._current_source().album_name @property def media_image_url(self): """Image url of current playing media.""" - return self._source_na_var("coverarturl") + return self._current_source().cover_art_url @property def volume_level(self): @@ -212,25 +190,25 @@ class RussoundZoneDevice(MediaPlayerEntity): Value is returned based on a range (0..50). Therefore float divide by 50 to get to the required range. """ - return float(self._zone_var("volume", 0)) / 50.0 + return float(self._zone.volume or "0") / 50.0 async def async_turn_off(self) -> None: """Turn off the zone.""" - await self._russ.send_zone_event(self._zone_id, "ZoneOff") + await self._zone.send_event("ZoneOff") async def async_turn_on(self) -> None: """Turn on the zone.""" - await self._russ.send_zone_event(self._zone_id, "ZoneOn") + await self._zone.send_event("ZoneOn") async def async_set_volume_level(self, volume: float) -> None: """Set the volume level.""" rvol = int(volume * 50.0) - await self._russ.send_zone_event(self._zone_id, "KeyPress", "Volume", rvol) + await self._zone.send_event("KeyPress", "Volume", rvol) async def async_select_source(self, source: str) -> None: """Select the source input for this zone.""" - for source_id, name in self._sources: - if name.lower() != source.lower(): + for source_id, src in self._sources.items(): + if src.name.lower() != source.lower(): continue - await self._russ.send_zone_event(self._zone_id, "SelectSource", source_id) + await self._zone.send_event("SelectSource", source_id) break diff --git a/requirements_all.txt b/requirements_all.txt index 08511d1b191..b348e734c8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==1.1.2 +aiorussound==2.0.6 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4d8964da6d..0fb54bc922d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==1.1.2 +aiorussound==2.0.6 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/tests/components/russound_rio/conftest.py b/tests/components/russound_rio/conftest.py index 49cb719dfc2..a87d0a74fa8 100644 --- a/tests/components/russound_rio/conftest.py +++ b/tests/components/russound_rio/conftest.py @@ -8,7 +8,7 @@ import pytest from homeassistant.components.russound_rio.const import DOMAIN from homeassistant.core import HomeAssistant -from .const import HARDWARE_MAC, MOCK_CONFIG, MODEL +from .const import HARDWARE_MAC, MOCK_CONFIG, MOCK_CONTROLLERS, MODEL from tests.common import MockConfigEntry @@ -44,5 +44,5 @@ def mock_russound() -> Generator[AsyncMock]: return_value=mock_client, ), ): - mock_client.enumerate_controllers.return_value = [(1, HARDWARE_MAC, MODEL)] + mock_client.enumerate_controllers.return_value = MOCK_CONTROLLERS yield mock_client diff --git a/tests/components/russound_rio/const.py b/tests/components/russound_rio/const.py index 92aed6494d9..d1f6aa7eead 100644 --- a/tests/components/russound_rio/const.py +++ b/tests/components/russound_rio/const.py @@ -1,5 +1,7 @@ """Constants for russound_rio tests.""" +from collections import namedtuple + HOST = "127.0.0.1" PORT = 9621 MODEL = "MCA-C5" @@ -9,3 +11,6 @@ MOCK_CONFIG = { "host": HOST, "port": PORT, } + +_CONTROLLER = namedtuple("Controller", ["mac_address", "controller_type"]) +MOCK_CONTROLLERS = {1: _CONTROLLER(mac_address=HARDWARE_MAC, controller_type=MODEL)} diff --git a/tests/components/russound_rio/test_config_flow.py b/tests/components/russound_rio/test_config_flow.py index 195e4af9b11..8bc7bd738a1 100644 --- a/tests/components/russound_rio/test_config_flow.py +++ b/tests/components/russound_rio/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .const import HARDWARE_MAC, MOCK_CONFIG, MODEL +from .const import MOCK_CONFIG, MOCK_CONTROLLERS, MODEL async def test_form( @@ -64,7 +64,7 @@ async def test_no_primary_controller( hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound: AsyncMock ) -> None: """Test we handle no primary controller error.""" - mock_russound.enumerate_controllers.return_value = [] + mock_russound.enumerate_controllers.return_value = {} result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -79,7 +79,7 @@ async def test_no_primary_controller( assert result["errors"] == {"base": "no_primary_controller"} # Recover with correct information - mock_russound.enumerate_controllers.return_value = [(1, HARDWARE_MAC, MODEL)] + mock_russound.enumerate_controllers.return_value = MOCK_CONTROLLERS result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, @@ -125,7 +125,7 @@ async def test_import_no_primary_controller( hass: HomeAssistant, mock_russound: AsyncMock ) -> None: """Test import with no primary controller error.""" - mock_russound.enumerate_controllers.return_value = [] + mock_russound.enumerate_controllers.return_value = {} result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_CONFIG From 34e72ea16a986a214ff3e4c0cb5e7b60112a4a3a Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 22 Jul 2024 09:35:29 +0200 Subject: [PATCH 1436/2411] Add support for KNX UI to create light entities (#122342) * Add light to KNX UI-createable entity platforms * review from switch * Add a test --- homeassistant/components/knx/const.py | 2 +- homeassistant/components/knx/light.py | 227 ++++++++++++++++-- homeassistant/components/knx/storage/const.py | 16 +- .../knx/storage/entity_store_schema.py | 142 ++++++++++- .../components/knx/storage/knx_selector.py | 2 +- homeassistant/components/knx/switch.py | 2 + tests/components/knx/conftest.py | 2 +- tests/components/knx/test_light.py | 26 +- 8 files changed, 388 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index c63a31f0bb5..0b7b517dca5 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -153,7 +153,7 @@ SUPPORTED_PLATFORMS_YAML: Final = { Platform.WEATHER, } -SUPPORTED_PLATFORMS_UI: Final = {Platform.SWITCH} +SUPPORTED_PLATFORMS_UI: Final = {Platform.SWITCH, Platform.LIGHT} # Map KNX controller modes to HA modes. This list might not be complete. CONTROLLER_MODES: Final = { diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index b1c1681a817..425640a9915 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -19,14 +19,41 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util -from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, ColorTempModes +from . import KNXModule +from .const import CONF_SYNC_STATE, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, ColorTempModes from .knx_entity import KnxEntity from .schema import LightSchema +from .storage.const import ( + CONF_COLOR_TEMP_MAX, + CONF_COLOR_TEMP_MIN, + CONF_DEVICE_INFO, + CONF_DPT, + CONF_ENTITY, + CONF_GA_BLUE_BRIGHTNESS, + CONF_GA_BLUE_SWITCH, + CONF_GA_BRIGHTNESS, + CONF_GA_COLOR, + CONF_GA_COLOR_TEMP, + CONF_GA_GREEN_BRIGHTNESS, + CONF_GA_GREEN_SWITCH, + CONF_GA_HUE, + CONF_GA_PASSIVE, + CONF_GA_RED_BRIGHTNESS, + CONF_GA_RED_SWITCH, + CONF_GA_SATURATION, + CONF_GA_STATE, + CONF_GA_SWITCH, + CONF_GA_WHITE_BRIGHTNESS, + CONF_GA_WHITE_SWITCH, + CONF_GA_WRITE, +) +from .storage.entity_store_schema import LightColorMode async def async_setup_entry( @@ -35,13 +62,31 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up light(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.LIGHT] + knx_module: KNXModule = hass.data[DOMAIN] - async_add_entities(KNXLight(xknx, entity_config) for entity_config in config) + entities: list[KnxEntity] = [] + if yaml_config := hass.data[DATA_KNX_CONFIG].get(Platform.LIGHT): + entities.extend( + KnxYamlLight(knx_module.xknx, entity_config) + for entity_config in yaml_config + ) + if ui_config := knx_module.config_store.data["entities"].get(Platform.LIGHT): + entities.extend( + KnxUiLight(knx_module, unique_id, config) + for unique_id, config in ui_config.items() + ) + if entities: + async_add_entities(entities) + + @callback + def add_new_ui_light(unique_id: str, config: dict[str, Any]) -> None: + """Add KNX entity at runtime.""" + async_add_entities([KnxUiLight(knx_module, unique_id, config)]) + + knx_module.config_store.async_add_entity[Platform.LIGHT] = add_new_ui_light -def _create_light(xknx: XKNX, config: ConfigType) -> XknxLight: +def _create_yaml_light(xknx: XKNX, config: ConfigType) -> XknxLight: """Return a KNX Light device to be used within XKNX.""" def individual_color_addresses(color: str, feature: str) -> Any | None: @@ -151,29 +196,111 @@ def _create_light(xknx: XKNX, config: ConfigType) -> XknxLight: ) -class KNXLight(KnxEntity, LightEntity): +def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight: + """Return a KNX Light device to be used within XKNX.""" + + def get_write(key: str) -> str | None: + """Get the write group address.""" + return knx_config[key][CONF_GA_WRITE] if key in knx_config else None + + def get_state(key: str) -> list[Any] | None: + """Get the state group address.""" + return ( + [knx_config[key][CONF_GA_STATE], *knx_config[key][CONF_GA_PASSIVE]] + if key in knx_config + else None + ) + + def get_dpt(key: str) -> str | None: + """Get the DPT.""" + return knx_config[key].get(CONF_DPT) if key in knx_config else None + + group_address_tunable_white = None + group_address_tunable_white_state = None + group_address_color_temp = None + group_address_color_temp_state = None + color_temperature_type = ColorTemperatureType.UINT_2_BYTE + if ga_color_temp := knx_config.get(CONF_GA_COLOR_TEMP): + if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE: + group_address_tunable_white = ga_color_temp[CONF_GA_WRITE] + group_address_tunable_white_state = [ + ga_color_temp[CONF_GA_STATE], + *ga_color_temp[CONF_GA_PASSIVE], + ] + else: + # absolute uint or float + group_address_color_temp = ga_color_temp[CONF_GA_WRITE] + group_address_color_temp_state = [ + ga_color_temp[CONF_GA_STATE], + *ga_color_temp[CONF_GA_PASSIVE], + ] + if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT: + color_temperature_type = ColorTemperatureType.FLOAT_2_BYTE + + _color_dpt = get_dpt(CONF_GA_COLOR) + return XknxLight( + xknx, + name=name, + group_address_switch=get_write(CONF_GA_SWITCH), + group_address_switch_state=get_state(CONF_GA_SWITCH), + group_address_brightness=get_write(CONF_GA_BRIGHTNESS), + group_address_brightness_state=get_state(CONF_GA_BRIGHTNESS), + group_address_color=get_write(CONF_GA_COLOR) + if _color_dpt == LightColorMode.RGB + else None, + group_address_color_state=get_state(CONF_GA_COLOR) + if _color_dpt == LightColorMode.RGB + else None, + group_address_rgbw=get_write(CONF_GA_COLOR) + if _color_dpt == LightColorMode.RGBW + else None, + group_address_rgbw_state=get_state(CONF_GA_COLOR) + if _color_dpt == LightColorMode.RGBW + else None, + group_address_hue=get_write(CONF_GA_HUE), + group_address_hue_state=get_state(CONF_GA_HUE), + group_address_saturation=get_write(CONF_GA_SATURATION), + group_address_saturation_state=get_state(CONF_GA_SATURATION), + group_address_xyy_color=get_write(CONF_GA_COLOR) + if _color_dpt == LightColorMode.XYY + else None, + group_address_xyy_color_state=get_write(CONF_GA_COLOR) + if _color_dpt == LightColorMode.XYY + else None, + group_address_tunable_white=group_address_tunable_white, + group_address_tunable_white_state=group_address_tunable_white_state, + group_address_color_temperature=group_address_color_temp, + group_address_color_temperature_state=group_address_color_temp_state, + group_address_switch_red=get_write(CONF_GA_RED_SWITCH), + group_address_switch_red_state=get_state(CONF_GA_RED_SWITCH), + group_address_brightness_red=get_write(CONF_GA_RED_BRIGHTNESS), + group_address_brightness_red_state=get_state(CONF_GA_RED_BRIGHTNESS), + group_address_switch_green=get_write(CONF_GA_GREEN_SWITCH), + group_address_switch_green_state=get_state(CONF_GA_GREEN_SWITCH), + group_address_brightness_green=get_write(CONF_GA_GREEN_BRIGHTNESS), + group_address_brightness_green_state=get_state(CONF_GA_GREEN_BRIGHTNESS), + group_address_switch_blue=get_write(CONF_GA_BLUE_SWITCH), + group_address_switch_blue_state=get_state(CONF_GA_BLUE_SWITCH), + group_address_brightness_blue=get_write(CONF_GA_BLUE_BRIGHTNESS), + group_address_brightness_blue_state=get_state(CONF_GA_BLUE_BRIGHTNESS), + group_address_switch_white=get_write(CONF_GA_WHITE_SWITCH), + group_address_switch_white_state=get_state(CONF_GA_WHITE_SWITCH), + group_address_brightness_white=get_write(CONF_GA_WHITE_BRIGHTNESS), + group_address_brightness_white_state=get_state(CONF_GA_WHITE_BRIGHTNESS), + color_temperature_type=color_temperature_type, + min_kelvin=knx_config[CONF_COLOR_TEMP_MIN], + max_kelvin=knx_config[CONF_COLOR_TEMP_MAX], + sync_state=knx_config[CONF_SYNC_STATE], + ) + + +class _KnxLight(KnxEntity, LightEntity): """Representation of a KNX light.""" + _attr_max_color_temp_kelvin: int + _attr_min_color_temp_kelvin: int _device: XknxLight - def __init__(self, xknx: XKNX, config: ConfigType) -> None: - """Initialize of KNX light.""" - super().__init__(_create_light(xknx, config)) - self._attr_max_color_temp_kelvin: int = config[LightSchema.CONF_MAX_KELVIN] - self._attr_min_color_temp_kelvin: int = config[LightSchema.CONF_MIN_KELVIN] - self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) - self._attr_unique_id = self._device_unique_id() - - def _device_unique_id(self) -> str: - """Return unique id for this device.""" - if self._device.switch.group_address is not None: - return f"{self._device.switch.group_address}" - return ( - f"{self._device.red.brightness.group_address}_" - f"{self._device.green.brightness.group_address}_" - f"{self._device.blue.brightness.group_address}" - ) - @property def is_on(self) -> bool: """Return true if light is on.""" @@ -392,3 +519,53 @@ class KNXLight(KnxEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._device.set_off() + + +class KnxYamlLight(_KnxLight): + """Representation of a KNX light.""" + + _device: XknxLight + + def __init__(self, xknx: XKNX, config: ConfigType) -> None: + """Initialize of KNX light.""" + super().__init__(_create_yaml_light(xknx, config)) + self._attr_max_color_temp_kelvin: int = config[LightSchema.CONF_MAX_KELVIN] + self._attr_min_color_temp_kelvin: int = config[LightSchema.CONF_MIN_KELVIN] + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) + self._attr_unique_id = self._device_unique_id() + + def _device_unique_id(self) -> str: + """Return unique id for this device.""" + if self._device.switch.group_address is not None: + return f"{self._device.switch.group_address}" + return ( + f"{self._device.red.brightness.group_address}_" + f"{self._device.green.brightness.group_address}_" + f"{self._device.blue.brightness.group_address}" + ) + + +class KnxUiLight(_KnxLight): + """Representation of a KNX light.""" + + _device: XknxLight + _attr_has_entity_name = True + + def __init__( + self, knx_module: KNXModule, unique_id: str, config: ConfigType + ) -> None: + """Initialize of KNX light.""" + super().__init__( + _create_ui_light( + knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME] + ) + ) + self._attr_max_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MAX] + self._attr_min_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MIN] + + self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY] + self._attr_unique_id = unique_id + if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO): + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)}) + + knx_module.config_store.entities[unique_id] = self diff --git a/homeassistant/components/knx/storage/const.py b/homeassistant/components/knx/storage/const.py index 6453b77ed3b..42b76a5a0fd 100644 --- a/homeassistant/components/knx/storage/const.py +++ b/homeassistant/components/knx/storage/const.py @@ -10,5 +10,19 @@ CONF_GA_STATE: Final = "state" CONF_GA_PASSIVE: Final = "passive" CONF_DPT: Final = "dpt" - CONF_GA_SWITCH: Final = "ga_switch" +CONF_GA_COLOR_TEMP: Final = "ga_color_temp" +CONF_COLOR_TEMP_MIN: Final = "color_temp_min" +CONF_COLOR_TEMP_MAX: Final = "color_temp_max" +CONF_GA_BRIGHTNESS: Final = "ga_brightness" +CONF_GA_COLOR: Final = "ga_color" +CONF_GA_RED_BRIGHTNESS: Final = "ga_red_brightness" +CONF_GA_RED_SWITCH: Final = "ga_red_switch" +CONF_GA_GREEN_BRIGHTNESS: Final = "ga_green_brightness" +CONF_GA_GREEN_SWITCH: Final = "ga_green_switch" +CONF_GA_BLUE_BRIGHTNESS: Final = "ga_blue_brightness" +CONF_GA_BLUE_SWITCH: Final = "ga_blue_switch" +CONF_GA_WHITE_BRIGHTNESS: Final = "ga_white_brightness" +CONF_GA_WHITE_SWITCH: Final = "ga_white_switch" +CONF_GA_HUE: Final = "ga_hue" +CONF_GA_SATURATION: Final = "ga_saturation" diff --git a/homeassistant/components/knx/storage/entity_store_schema.py b/homeassistant/components/knx/storage/entity_store_schema.py index e2f9e786300..84854d2ec85 100644 --- a/homeassistant/components/knx/storage/entity_store_schema.py +++ b/homeassistant/components/knx/storage/entity_store_schema.py @@ -1,5 +1,7 @@ """KNX entity store schema.""" +from enum import StrEnum, unique + import voluptuous as vol from homeassistant.const import ( @@ -19,9 +21,33 @@ from ..const import ( CONF_SYNC_STATE, DOMAIN, SUPPORTED_PLATFORMS_UI, + ColorTempModes, ) from ..validation import sync_state_validator -from .const import CONF_DATA, CONF_DEVICE_INFO, CONF_ENTITY, CONF_GA_SWITCH +from .const import ( + CONF_COLOR_TEMP_MAX, + CONF_COLOR_TEMP_MIN, + CONF_DATA, + CONF_DEVICE_INFO, + CONF_ENTITY, + CONF_GA_BLUE_BRIGHTNESS, + CONF_GA_BLUE_SWITCH, + CONF_GA_BRIGHTNESS, + CONF_GA_COLOR, + CONF_GA_COLOR_TEMP, + CONF_GA_GREEN_BRIGHTNESS, + CONF_GA_GREEN_SWITCH, + CONF_GA_HUE, + CONF_GA_PASSIVE, + CONF_GA_RED_BRIGHTNESS, + CONF_GA_RED_SWITCH, + CONF_GA_SATURATION, + CONF_GA_STATE, + CONF_GA_SWITCH, + CONF_GA_WHITE_BRIGHTNESS, + CONF_GA_WHITE_SWITCH, + CONF_GA_WRITE, +) from .knx_selector import GASelector BASE_ENTITY_SCHEMA = vol.All( @@ -49,6 +75,25 @@ BASE_ENTITY_SCHEMA = vol.All( ), ) + +def optional_ga_schema(key: str, ga_selector: GASelector) -> VolDictType: + """Validate group address schema or remove key if no address is set.""" + # frontend will return {key: {"write": None, "state": None}} for unused GA sets + # -> remove this entirely for optional keys + # if one GA is set, validate as usual + return { + vol.Optional(key): ga_selector, + vol.Remove(key): vol.Schema( + { + vol.Optional(CONF_GA_WRITE): None, + vol.Optional(CONF_GA_STATE): None, + vol.Optional(CONF_GA_PASSIVE): vol.IsFalse(), # None or empty list + }, + extra=vol.ALLOW_EXTRA, + ), + } + + SWITCH_SCHEMA = vol.Schema( { vol.Required(CONF_ENTITY): BASE_ENTITY_SCHEMA, @@ -62,6 +107,98 @@ SWITCH_SCHEMA = vol.Schema( ) +@unique +class LightColorMode(StrEnum): + """Enum for light color mode.""" + + RGB = "232.600" + RGBW = "251.600" + XYY = "242.600" + + +@unique +class LightColorModeSchema(StrEnum): + """Enum for light color mode.""" + + DEFAULT = "default" + INDIVIDUAL = "individual" + HSV = "hsv" + + +_LIGHT_COLOR_MODE_SCHEMA = "_light_color_mode_schema" + +_COMMON_LIGHT_SCHEMA = vol.Schema( + { + vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, + **optional_ga_schema( + CONF_GA_COLOR_TEMP, GASelector(write_required=True, dpt=ColorTempModes) + ), + vol.Optional(CONF_COLOR_TEMP_MIN, default=2700): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Optional(CONF_COLOR_TEMP_MAX, default=6000): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + }, + extra=vol.REMOVE_EXTRA, +) + +_DEFAULT_LIGHT_SCHEMA = _COMMON_LIGHT_SCHEMA.extend( + { + vol.Required(_LIGHT_COLOR_MODE_SCHEMA): LightColorModeSchema.DEFAULT.value, + vol.Required(CONF_GA_SWITCH): GASelector(write_required=True), + **optional_ga_schema(CONF_GA_BRIGHTNESS, GASelector(write_required=True)), + **optional_ga_schema( + CONF_GA_COLOR, + GASelector(write_required=True, dpt=LightColorMode), + ), + } +) + +_INDIVIDUAL_LIGHT_SCHEMA = _COMMON_LIGHT_SCHEMA.extend( + { + vol.Required(_LIGHT_COLOR_MODE_SCHEMA): LightColorModeSchema.INDIVIDUAL.value, + **optional_ga_schema(CONF_GA_SWITCH, GASelector(write_required=True)), + **optional_ga_schema(CONF_GA_BRIGHTNESS, GASelector(write_required=True)), + vol.Required(CONF_GA_RED_BRIGHTNESS): GASelector(write_required=True), + **optional_ga_schema(CONF_GA_RED_SWITCH, GASelector(write_required=False)), + vol.Required(CONF_GA_GREEN_BRIGHTNESS): GASelector(write_required=True), + **optional_ga_schema(CONF_GA_GREEN_SWITCH, GASelector(write_required=False)), + vol.Required(CONF_GA_BLUE_BRIGHTNESS): GASelector(write_required=True), + **optional_ga_schema(CONF_GA_BLUE_SWITCH, GASelector(write_required=False)), + **optional_ga_schema(CONF_GA_WHITE_BRIGHTNESS, GASelector(write_required=True)), + **optional_ga_schema(CONF_GA_WHITE_SWITCH, GASelector(write_required=False)), + } +) + +_HSV_LIGHT_SCHEMA = _COMMON_LIGHT_SCHEMA.extend( + { + vol.Required(_LIGHT_COLOR_MODE_SCHEMA): LightColorModeSchema.HSV.value, + vol.Required(CONF_GA_SWITCH): GASelector(write_required=True), + vol.Required(CONF_GA_BRIGHTNESS): GASelector(write_required=True), + vol.Required(CONF_GA_HUE): GASelector(write_required=True), + vol.Required(CONF_GA_SATURATION): GASelector(write_required=True), + } +) + + +LIGHT_KNX_SCHEMA = cv.key_value_schemas( + _LIGHT_COLOR_MODE_SCHEMA, + default_schema=_DEFAULT_LIGHT_SCHEMA, + value_schemas={ + LightColorModeSchema.DEFAULT: _DEFAULT_LIGHT_SCHEMA, + LightColorModeSchema.INDIVIDUAL: _INDIVIDUAL_LIGHT_SCHEMA, + LightColorModeSchema.HSV: _HSV_LIGHT_SCHEMA, + }, +) + +LIGHT_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY): BASE_ENTITY_SCHEMA, + vol.Required(DOMAIN): LIGHT_KNX_SCHEMA, + } +) + ENTITY_STORE_DATA_SCHEMA: VolSchemaType = vol.All( vol.Schema( { @@ -79,6 +216,9 @@ ENTITY_STORE_DATA_SCHEMA: VolSchemaType = vol.All( Platform.SWITCH: vol.Schema( {vol.Required(CONF_DATA): SWITCH_SCHEMA}, extra=vol.ALLOW_EXTRA ), + Platform.LIGHT: vol.Schema( + {vol.Required("data"): LIGHT_SCHEMA}, extra=vol.ALLOW_EXTRA + ), }, ), ) diff --git a/homeassistant/components/knx/storage/knx_selector.py b/homeassistant/components/knx/storage/knx_selector.py index 396cde67fbd..1ac99d192b8 100644 --- a/homeassistant/components/knx/storage/knx_selector.py +++ b/homeassistant/components/knx/storage/knx_selector.py @@ -76,6 +76,6 @@ class GASelector: def _add_dpt(self, schema: dict[vol.Marker, Any]) -> None: """Add DPT validator to the schema.""" if self.dpt is not None: - schema[vol.Required(CONF_DPT)] = vol.In([item.value for item in self.dpt]) + schema[vol.Required(CONF_DPT)] = vol.In({item.value for item in self.dpt}) else: schema[vol.Remove(CONF_DPT)] = object diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 94f5592db90..0a8a1dff964 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -28,6 +28,7 @@ from . import KNXModule from .const import ( CONF_INVERT, CONF_RESPOND_TO_READ, + CONF_SYNC_STATE, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, @@ -141,6 +142,7 @@ class KnxUiSwitch(_KnxSwitch): *config[DOMAIN][CONF_GA_SWITCH][CONF_GA_PASSIVE], ], respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ], + sync_state=config[DOMAIN][CONF_SYNC_STATE], invert=config[DOMAIN][CONF_INVERT], ) ) diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 749d1c4252a..76f1b6f3ebc 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -337,7 +337,7 @@ async def create_ui_entity( ) res = await ws_client.receive_json() assert res["success"], res - assert res["result"]["success"] is True + assert res["result"]["success"] is True, res["result"] entity_id = res["result"]["entity_id"] entity = entity_registry.async_get(entity_id) diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index a14d1bb32ae..0c7a37979a8 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -7,7 +7,7 @@ from datetime import timedelta from xknx.core import XknxConnectionState from xknx.devices.light import Light as XknxLight -from homeassistant.components.knx.const import CONF_STATE_ADDRESS, KNX_ADDRESS +from homeassistant.components.knx.const import CONF_STATE_ADDRESS, KNX_ADDRESS, Platform from homeassistant.components.knx.schema import LightSchema from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -21,6 +21,7 @@ from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util +from . import KnxEntityGenerator from .conftest import KNXTestKit from tests.common import async_fire_time_changed @@ -1151,3 +1152,26 @@ async def test_light_rgbw_brightness(hass: HomeAssistant, knx: KNXTestKit) -> No knx.assert_state( "light.test", STATE_ON, brightness=50, rgbw_color=(100, 200, 55, 12) ) + + +async def test_light_ui_create( + hass: HomeAssistant, + knx: KNXTestKit, + create_ui_entity: KnxEntityGenerator, +) -> None: + """Test creating a switch.""" + await knx.setup_integration({}) + await create_ui_entity( + platform=Platform.LIGHT, + entity_data={"name": "test"}, + knx_data={ + "ga_switch": {"write": "1/1/1", "state": "2/2/2"}, + "_light_color_mode_schema": "default", + "sync_state": True, + }, + ) + # created entity sends read-request to KNX bus + await knx.assert_read("2/2/2") + await knx.receive_response("2/2/2", True) + state = hass.states.get("light.test") + assert state.state is STATE_ON From 5b32efb6d61a4fde28bc03a14a95918f94ba9fc5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 10:05:15 +0200 Subject: [PATCH 1437/2411] Bump github/codeql-action from 3.25.12 to 3.25.13 (#122362) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6e3a869cce0..fd37005a59f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.1.7 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.25.12 + uses: github/codeql-action/init@v3.25.13 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.25.12 + uses: github/codeql-action/analyze@v3.25.13 with: category: "/language:python" From 02c64c78615a917f5374e881a8aade3d1b56db87 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Jul 2024 03:15:02 -0500 Subject: [PATCH 1438/2411] Bump cryptography to 43.0.0 and pyOpenSSL to 24.2.1 and chacha20poly1305-reuseable >= 0.13.0 (#122308) --- homeassistant/package_constraints.txt | 8 ++++---- pyproject.toml | 4 ++-- requirements.txt | 4 ++-- script/gen_requirements_all.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e5e1a85c02f..3eaa0b06619 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ bluetooth-data-tools==1.19.3 cached_ipaddress==0.3.0 certifi>=2021.5.30 ciso8601==2.3.1 -cryptography==42.0.8 +cryptography==43.0.0 dbus-fast==2.22.1 fnv-hash-fast==0.5.0 ha-av==10.1.1 @@ -47,7 +47,7 @@ pip>=21.3.1 psutil-home-assistant==0.0.1 PyJWT==2.8.0 PyNaCl==1.5.0 -pyOpenSSL==24.1.0 +pyOpenSSL==24.2.1 pyserial==3.5 python-slugify==8.0.4 PyTurboJPEG==1.7.1 @@ -187,8 +187,8 @@ dacite>=1.7.0 # Musle wheels for pandas 2.2.0 cannot be build for any architecture. pandas==2.1.4 -# chacha20poly1305-reuseable==0.12.0 is incompatible with cryptography==42.0.x -chacha20poly1305-reuseable>=0.12.1 +# chacha20poly1305-reuseable==0.12.x is incompatible with cryptography==43.0.x +chacha20poly1305-reuseable>=0.13.0 # pycountry<23.12.11 imports setuptools at run time # https://github.com/pycountry/pycountry/blob/ea69bab36f00df58624a0e490fdad4ccdc14268b/HISTORY.txt#L39 diff --git a/pyproject.toml b/pyproject.toml index d0bbc36a5ef..113fef6bfbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,9 +50,9 @@ dependencies = [ "lru-dict==1.3.0", "PyJWT==2.8.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==42.0.8", + "cryptography==43.0.0", "Pillow==10.4.0", - "pyOpenSSL==24.1.0", + "pyOpenSSL==24.2.1", "orjson==3.10.6", "packaging>=23.1", "pip>=21.3.1", diff --git a/requirements.txt b/requirements.txt index 3eba90158a2..a729f09472b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,9 +25,9 @@ ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 PyJWT==2.8.0 -cryptography==42.0.8 +cryptography==43.0.0 Pillow==10.4.0 -pyOpenSSL==24.1.0 +pyOpenSSL==24.2.1 orjson==3.10.6 packaging>=23.1 pip>=21.3.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3c593a2bdf7..f887f8113a7 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -208,8 +208,8 @@ dacite>=1.7.0 # Musle wheels for pandas 2.2.0 cannot be build for any architecture. pandas==2.1.4 -# chacha20poly1305-reuseable==0.12.0 is incompatible with cryptography==42.0.x -chacha20poly1305-reuseable>=0.12.1 +# chacha20poly1305-reuseable==0.12.x is incompatible with cryptography==43.0.x +chacha20poly1305-reuseable>=0.13.0 # pycountry<23.12.11 imports setuptools at run time # https://github.com/pycountry/pycountry/blob/ea69bab36f00df58624a0e490fdad4ccdc14268b/HISTORY.txt#L39 From c70e6118220d50ebf0c099d727773a3486b28abe Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 22 Jul 2024 10:19:08 +0200 Subject: [PATCH 1439/2411] Fix homewizard api close not being awaited on unload (#122324) --- homeassistant/components/homewizard/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homewizard/__init__.py b/homeassistant/components/homewizard/__init__.py index 2baa6fed2da..4733bc67073 100644 --- a/homeassistant/components/homewizard/__init__.py +++ b/homeassistant/components/homewizard/__init__.py @@ -10,7 +10,7 @@ from .coordinator import HWEnergyDeviceUpdateCoordinator type HomeWizardConfigEntry = ConfigEntry[HWEnergyDeviceUpdateCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: HomeWizardConfigEntry) -> bool: """Set up Homewizard from a config entry.""" coordinator = HWEnergyDeviceUpdateCoordinator(hass) try: @@ -35,13 +35,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.flow.async_abort(progress_flow["flow_id"]) # Finalize + entry.async_on_unload(coordinator.api.close) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: HomeWizardConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - entry.runtime_data.api.close() - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) From 0c6dc9e43bacc8c115982ce7a2c5b5f8546cc2d6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 22 Jul 2024 11:09:03 +0200 Subject: [PATCH 1440/2411] Bump reolink-aio to 0.9.5 (#122366) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index ee3ebe8a13a..c329289790b 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.9.4"] + "requirements": ["reolink-aio==0.9.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index b348e734c8b..5b63cdc49e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2468,7 +2468,7 @@ renault-api==0.2.5 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.4 +reolink-aio==0.9.5 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fb54bc922d..accb22fa24b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1941,7 +1941,7 @@ renault-api==0.2.5 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.4 +reolink-aio==0.9.5 # homeassistant.components.rflink rflink==0.0.66 From 064d7261b482d8dc45e238ed0434118275bcf358 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Mon, 22 Jul 2024 12:11:09 +0300 Subject: [PATCH 1441/2411] Ensure script llm tool name does not start with a digit (#122349) * Ensure script tool name does not start with a digit * Fix test name --- homeassistant/helpers/llm.py | 5 ++++- tests/helpers/test_llm.py | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 52d7271c196..177e3735bc0 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -617,6 +617,9 @@ class ScriptTool(Tool): entity_registry = er.async_get(hass) self.name = split_entity_id(script_entity_id)[1] + if self.name[0].isdigit(): + self.name = "_" + self.name + self._entity_id = script_entity_id self.parameters = vol.Schema({}) entity_entry = entity_registry.async_get(script_entity_id) if entity_entry and entity_entry.unique_id: @@ -717,7 +720,7 @@ class ScriptTool(Tool): SCRIPT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: SCRIPT_DOMAIN + "." + self.name, + ATTR_ENTITY_ID: self._entity_id, ATTR_VARIABLES: tool_input.tool_args, }, context=llm_context.context, diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 81fa573852e..e1f55942d10 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -780,6 +780,46 @@ async def test_script_tool( } +async def test_script_tool_name(hass: HomeAssistant) -> None: + """Test that script tool name is not started with a digit.""" + assert await async_setup_component(hass, "homeassistant", {}) + context = Context() + llm_context = llm.LLMContext( + platform="test_platform", + context=context, + user_prompt="test_text", + language="*", + assistant="conversation", + device_id=None, + ) + + # Create a script with a unique ID + assert await async_setup_component( + hass, + "script", + { + "script": { + "123456": { + "description": "This is a test script", + "sequence": [], + "fields": { + "beer": {"description": "Number of beers", "required": True}, + }, + }, + } + }, + ) + async_expose_entity(hass, "conversation", "script.123456", True) + + api = await llm.async_get_api(hass, "assist", llm_context) + + tools = [tool for tool in api.tools if isinstance(tool, llm.ScriptTool)] + assert len(tools) == 1 + + tool = tools[0] + assert tool.name == "_123456" + + async def test_selector_serializer( hass: HomeAssistant, llm_context: llm.LLMContext ) -> None: From 9793aa0a5eaa2f0bde6274b9ffb6442a93893282 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:16:05 +0200 Subject: [PATCH 1442/2411] Update pytest to 8.3.1 (#122368) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 30e6445b8a7..0088fa8e17e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -29,7 +29,7 @@ pytest-timeout==2.3.1 pytest-unordered==0.6.1 pytest-picked==0.5.0 pytest-xdist==3.6.1 -pytest==8.2.2 +pytest==8.3.1 requests-mock==1.12.1 respx==0.21.1 syrupy==4.6.1 From 8d538fcd52797aebf707b53a02549904f521fba3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 22 Jul 2024 11:20:02 +0200 Subject: [PATCH 1443/2411] Add Reolink model_id / item number (#122371) --- homeassistant/components/reolink/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index cf582c69e2d..c07983175ae 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -64,6 +64,7 @@ class ReolinkHostCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[None] connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)}, name=self._host.api.nvr_name, model=self._host.api.model, + model_id=self._host.api.item_number, manufacturer=self._host.api.manufacturer, hw_version=self._host.api.hardware_version, sw_version=self._host.api.sw_version, From 33f0840a2607655e6f1bc3142fc4e46e819663fd Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 22 Jul 2024 11:21:54 +0200 Subject: [PATCH 1444/2411] Add translations for xiaomi miio fan preset modes (#122367) --- homeassistant/components/xiaomi_miio/fan.py | 6 ++++-- homeassistant/components/xiaomi_miio/icons.json | 14 ++++++++++++++ homeassistant/components/xiaomi_miio/strings.json | 12 ++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 8e58cb07ec8..f075ff8816f 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -98,8 +98,8 @@ _LOGGER = logging.getLogger(__name__) DATA_KEY = "fan.xiaomi_miio" -ATTR_MODE_NATURE = "Nature" -ATTR_MODE_NORMAL = "Normal" +ATTR_MODE_NATURE = "nature" +ATTR_MODE_NORMAL = "normal" # Air Purifier ATTR_BRIGHTNESS = "brightness" @@ -845,6 +845,8 @@ class XiaomiAirFreshT2017(XiaomiAirFreshA1): class XiaomiGenericFan(XiaomiGenericDevice): """Representation of a generic Xiaomi Fan.""" + _attr_translation_key = "generic_fan" + def __init__(self, device, entry, unique_id, coordinator): """Initialize the fan.""" super().__init__(device, entry, unique_id, coordinator) diff --git a/homeassistant/components/xiaomi_miio/icons.json b/homeassistant/components/xiaomi_miio/icons.json index bbd3f6607d7..2e5084a1f6c 100644 --- a/homeassistant/components/xiaomi_miio/icons.json +++ b/homeassistant/components/xiaomi_miio/icons.json @@ -1,4 +1,18 @@ { + "entity": { + "fan": { + "generic_fan": { + "state_attributes": { + "preset_mode": { + "state": { + "nature": "mdi:leaf", + "normal": "mdi:weather-windy" + } + } + } + } + } + }, "services": { "fan_reset_filter": "mdi:refresh", "fan_set_extra_features": "mdi:cog", diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 5037b2c3180..bbdc3f5737d 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -93,6 +93,18 @@ "high": "High" } } + }, + "fan": { + "generic_fan": { + "state_attributes": { + "preset_mode": { + "state": { + "nature": "Nature", + "normal": "Normal" + } + } + } + } } }, "services": { From 5612e3a92bfcdab937e9da83a9c54b856fc3caef Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:26:38 +0200 Subject: [PATCH 1445/2411] Bumb python-homewizard-energy to 6.1.1 to embed model in upstream library (#122365) --- homeassistant/components/homewizard/entity.py | 18 +- .../components/homewizard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../snapshots/test_diagnostics.ambr | 48 ++++++ .../homewizard/snapshots/test_sensor.ambr | 160 +++++++++--------- .../homewizard/snapshots/test_switch.ambr | 10 +- 7 files changed, 140 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/homewizard/entity.py b/homeassistant/components/homewizard/entity.py index 51559984fea..0aea899c044 100644 --- a/homeassistant/components/homewizard/entity.py +++ b/homeassistant/components/homewizard/entity.py @@ -2,23 +2,13 @@ from __future__ import annotations -from homeassistant.const import ATTR_CONNECTIONS, ATTR_IDENTIFIERS, ATTR_MODEL +from homeassistant.const import ATTR_CONNECTIONS, ATTR_IDENTIFIERS from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import HWEnergyDeviceUpdateCoordinator -TYPE_MODEL_MAP = { - "HWE-P1": "Wi-Fi P1 Meter", - "HWE-SKT": "Wi-Fi Energy Socket", - "HWE-WTR": "Wi-Fi Water Meter", - "HWE-KWH1": "Wi-Fi kWh Meter", - "HWE-KWH3": "Wi-Fi kWh Meter", - "SDM230-wifi": "Wi-Fi kWh Meter", - "SDM630-wifi": "Wi-Fi kWh Meter", -} - class HomeWizardEntity(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator]): """Defines a HomeWizard entity.""" @@ -32,11 +22,11 @@ class HomeWizardEntity(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator]): manufacturer="HomeWizard", sw_version=coordinator.data.device.firmware_version, model_id=coordinator.data.device.product_type, + model=coordinator.data.device.product.name + if coordinator.data.device.product + else None, ) - if product_type := coordinator.data.device.product_type: - self._attr_device_info[ATTR_MODEL] = TYPE_MODEL_MAP.get(product_type) - if (serial_number := coordinator.data.device.serial) is not None: self._attr_device_info[ATTR_CONNECTIONS] = { (CONNECTION_NETWORK_MAC, serial_number) diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 02ba264d99e..474d63e943d 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "loggers": ["homewizard_energy"], "quality_scale": "platinum", - "requirements": ["python-homewizard-energy==v6.0.0"], + "requirements": ["python-homewizard-energy==v6.1.1"], "zeroconf": ["_hwenergy._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 5b63cdc49e4..37db9d8748e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2268,7 +2268,7 @@ python-gitlab==1.6.0 python-homeassistant-analytics==0.7.0 # homeassistant.components.homewizard -python-homewizard-energy==v6.0.0 +python-homewizard-energy==v6.1.1 # homeassistant.components.hp_ilo python-hpilo==4.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index accb22fa24b..b0f8f28321e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1783,7 +1783,7 @@ python-fullykiosk==0.0.14 python-homeassistant-analytics==0.7.0 # homeassistant.components.homewizard -python-homewizard-energy==v6.0.0 +python-homewizard-energy==v6.1.1 # homeassistant.components.izone python-izone==1.2.9 diff --git a/tests/components/homewizard/snapshots/test_diagnostics.ambr b/tests/components/homewizard/snapshots/test_diagnostics.ambr index 7b82056aacb..f8ac80f2536 100644 --- a/tests/components/homewizard/snapshots/test_diagnostics.ambr +++ b/tests/components/homewizard/snapshots/test_diagnostics.ambr @@ -65,6 +65,12 @@ 'device': dict({ 'api_version': 'v1', 'firmware_version': '3.06', + 'product': dict({ + 'description': 'Measure solar panels, car chargers and more.', + 'model': 'HWE-KWH1', + 'name': 'Wi-Fi kWh Meter 1-phase', + 'url': 'https://www.homewizard.com/kwh-meter/', + }), 'product_name': 'kWh meter', 'product_type': 'HWE-KWH1', 'serial': '**REDACTED**', @@ -148,6 +154,12 @@ 'device': dict({ 'api_version': 'v1', 'firmware_version': '3.06', + 'product': dict({ + 'description': 'Measure solar panels, car chargers and more.', + 'model': 'HWE-KWH3', + 'name': 'Wi-Fi kWh Meter 3-phase', + 'url': 'https://www.homewizard.com/kwh-meter/', + }), 'product_name': 'KWh meter 3-phase', 'product_type': 'HWE-KWH3', 'serial': '**REDACTED**', @@ -282,6 +294,12 @@ 'device': dict({ 'api_version': 'v1', 'firmware_version': '4.19', + 'product': dict({ + 'description': 'The HomeWizard P1 Meter gives you detailed insight in your electricity-, gas consumption and solar surplus.', + 'model': 'HWE-P1', + 'name': 'Wi-Fi P1 Meter', + 'url': 'https://www.homewizard.com/p1-meter/', + }), 'product_name': 'P1 meter', 'product_type': 'HWE-P1', 'serial': '**REDACTED**', @@ -365,6 +383,12 @@ 'device': dict({ 'api_version': 'v1', 'firmware_version': '3.03', + 'product': dict({ + 'description': 'Measure and switch every device.', + 'model': 'HWE-SKT', + 'name': 'Wi-Fi Energy Socket', + 'url': 'https://www.homewizard.com/energy-socket/', + }), 'product_name': 'Energy Socket', 'product_type': 'HWE-SKT', 'serial': '**REDACTED**', @@ -452,6 +476,12 @@ 'device': dict({ 'api_version': 'v1', 'firmware_version': '4.07', + 'product': dict({ + 'description': 'Measure and switch every device.', + 'model': 'HWE-SKT', + 'name': 'Wi-Fi Energy Socket', + 'url': 'https://www.homewizard.com/energy-socket/', + }), 'product_name': 'Energy Socket', 'product_type': 'HWE-SKT', 'serial': '**REDACTED**', @@ -539,6 +569,12 @@ 'device': dict({ 'api_version': 'v1', 'firmware_version': '2.03', + 'product': dict({ + 'description': 'Real-time water consumption insights', + 'model': 'HWE-WTR', + 'name': 'Wi-Fi Watermeter', + 'url': 'https://www.homewizard.com/watermeter/', + }), 'product_name': 'Watermeter', 'product_type': 'HWE-WTR', 'serial': '**REDACTED**', @@ -622,6 +658,12 @@ 'device': dict({ 'api_version': 'v1', 'firmware_version': '3.06', + 'product': dict({ + 'description': 'Measure solar panels, car chargers and more.', + 'model': 'SDM230-wifi', + 'name': 'Wi-Fi kWh Meter 1-phase', + 'url': 'https://www.homewizard.com/kwh-meter/', + }), 'product_name': 'kWh meter', 'product_type': 'SDM230-wifi', 'serial': '**REDACTED**', @@ -705,6 +747,12 @@ 'device': dict({ 'api_version': 'v1', 'firmware_version': '3.06', + 'product': dict({ + 'description': 'Measure solar panels, car chargers and more.', + 'model': 'SDM630-wifi', + 'name': 'Wi-Fi kWh Meter 3-phase', + 'url': 'https://www.homewizard.com/kwh-meter/', + }), 'product_name': 'KWh meter 3-phase', 'product_type': 'SDM630-wifi', 'serial': '**REDACTED**', diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index 6750da5bb8b..63ee9312a13 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -57,7 +57,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, @@ -144,7 +144,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, @@ -231,7 +231,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, @@ -318,7 +318,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, @@ -405,7 +405,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, @@ -492,7 +492,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, @@ -582,7 +582,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, @@ -669,7 +669,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, @@ -756,7 +756,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, @@ -843,7 +843,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, @@ -925,7 +925,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, @@ -1011,7 +1011,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -1098,7 +1098,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -1185,7 +1185,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -1272,7 +1272,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -1359,7 +1359,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -1446,7 +1446,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -1533,7 +1533,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -1620,7 +1620,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -1707,7 +1707,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -1794,7 +1794,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -1881,7 +1881,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -1968,7 +1968,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -2058,7 +2058,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -2145,7 +2145,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -2232,7 +2232,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -2319,7 +2319,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -2409,7 +2409,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -2499,7 +2499,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -2589,7 +2589,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -2676,7 +2676,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -2763,7 +2763,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -2850,7 +2850,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -2937,7 +2937,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -3024,7 +3024,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -3111,7 +3111,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -3198,7 +3198,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -3280,7 +3280,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -15204,7 +15204,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi Water Meter', + 'model': 'Wi-Fi Watermeter', 'model_id': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, @@ -15291,7 +15291,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi Water Meter', + 'model': 'Wi-Fi Watermeter', 'model_id': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, @@ -15377,7 +15377,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi Water Meter', + 'model': 'Wi-Fi Watermeter', 'model_id': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, @@ -15459,7 +15459,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi Water Meter', + 'model': 'Wi-Fi Watermeter', 'model_id': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, @@ -15545,7 +15545,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, @@ -15632,7 +15632,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, @@ -15719,7 +15719,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, @@ -15806,7 +15806,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, @@ -15893,7 +15893,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, @@ -15980,7 +15980,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, @@ -16070,7 +16070,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, @@ -16157,7 +16157,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, @@ -16244,7 +16244,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, @@ -16331,7 +16331,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, @@ -16413,7 +16413,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, @@ -16499,7 +16499,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -16586,7 +16586,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -16673,7 +16673,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -16760,7 +16760,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -16847,7 +16847,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -16934,7 +16934,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -17021,7 +17021,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -17108,7 +17108,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -17195,7 +17195,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -17282,7 +17282,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -17369,7 +17369,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -17456,7 +17456,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -17546,7 +17546,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -17633,7 +17633,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -17720,7 +17720,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -17807,7 +17807,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -17897,7 +17897,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -17987,7 +17987,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -18077,7 +18077,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -18164,7 +18164,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -18251,7 +18251,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -18338,7 +18338,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -18425,7 +18425,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -18512,7 +18512,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -18599,7 +18599,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -18686,7 +18686,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, @@ -18768,7 +18768,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, diff --git a/tests/components/homewizard/snapshots/test_switch.ambr b/tests/components/homewizard/snapshots/test_switch.ambr index 11dea4453b5..68a351c1ebb 100644 --- a/tests/components/homewizard/snapshots/test_switch.ambr +++ b/tests/components/homewizard/snapshots/test_switch.ambr @@ -70,7 +70,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'HWE-KWH1', 'name': 'Device', 'name_by_user': None, @@ -152,7 +152,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'HWE-KWH3', 'name': 'Device', 'name_by_user': None, @@ -728,7 +728,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi Water Meter', + 'model': 'Wi-Fi Watermeter', 'model_id': 'HWE-WTR', 'name': 'Device', 'name_by_user': None, @@ -810,7 +810,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 1-phase', 'model_id': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, @@ -892,7 +892,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'Wi-Fi kWh Meter', + 'model': 'Wi-Fi kWh Meter 3-phase', 'model_id': 'SDM630-wifi', 'name': 'Device', 'name_by_user': None, From cbe94c470609ba3a28f1a061973832b4038b59c3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Jul 2024 12:02:17 +0200 Subject: [PATCH 1446/2411] Fix typo in recorder persistent notification (#122374) --- homeassistant/components/recorder/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 52f2c38f8bb..a7e968fe544 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -957,7 +957,7 @@ class Recorder(threading.Thread): "System performance will temporarily degrade during the database" " upgrade. Do not power down or restart the system until the upgrade" " completes. Integrations that read the database, such as logbook," - " history, and statistics may return inconsistent results until the " + " history, and statistics may return inconsistent results until the" " upgrade completes. This notification will be automatically dismissed" " when the upgrade completes." ), From ea94cdb668e13ca16bade003abfc4f703cdff83e Mon Sep 17 00:00:00 2001 From: cdnninja Date: Mon, 22 Jul 2024 05:09:08 -0600 Subject: [PATCH 1447/2411] Bump pyvesync to 2.1.12 (#122318) --- homeassistant/components/vesync/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../vesync/snapshots/test_diagnostics.ambr | 23 ++++++++++--------- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index ff3f56dd184..c5926cc224a 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/vesync", "iot_class": "cloud_polling", "loggers": ["pyvesync"], - "requirements": ["pyvesync==2.1.10"] + "requirements": ["pyvesync==2.1.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index 37db9d8748e..d065a99d3f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2384,7 +2384,7 @@ pyvera==0.3.13 pyversasense==0.0.6 # homeassistant.components.vesync -pyvesync==2.1.10 +pyvesync==2.1.12 # homeassistant.components.vizio pyvizio==0.1.61 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0f8f28321e..164d9003ad7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1872,7 +1872,7 @@ pyuptimerobot==22.2.0 pyvera==0.3.13 # homeassistant.components.vesync -pyvesync==2.1.10 +pyvesync==2.1.12 # homeassistant.components.vizio pyvizio==0.1.61 diff --git a/tests/components/vesync/snapshots/test_diagnostics.ambr b/tests/components/vesync/snapshots/test_diagnostics.ambr index 0db0a629e68..54ed8acf2d7 100644 --- a/tests/components/vesync/snapshots/test_diagnostics.ambr +++ b/tests/components/vesync/snapshots/test_diagnostics.ambr @@ -38,13 +38,7 @@ 'setDisplay', 'setLevel', ]), - 'cid': 'abcdefghabcdefghabcdefghabcdefgh', - 'config': dict({ - 'auto_target_humidity': 60, - 'automatic_stop': True, - 'display': True, - }), - 'config_dict': dict({ + '_config_dict': dict({ 'features': list([ 'warm_mist', 'nightlight', @@ -71,6 +65,7 @@ 'LUH-A602S-WEUR', 'LUH-A602S-WEU', 'LUH-A602S-WJP', + 'LUH-A602S-WUSC', ]), 'module': 'VeSyncHumid200300S', 'warm_mist_levels': list([ @@ -80,6 +75,16 @@ 3, ]), }), + '_features': list([ + 'warm_mist', + 'nightlight', + ]), + 'cid': 'abcdefghabcdefghabcdefghabcdefgh', + 'config': dict({ + 'auto_target_humidity': 60, + 'automatic_stop': True, + 'display': True, + }), 'config_module': 'WFON_AHM_LUH-A602S-WUS_US', 'connection_status': 'online', 'connection_type': 'WiFi+BTOnboarding+BTNotify', @@ -105,10 +110,6 @@ 'device_type': 'LUH-A602S-WUS', 'enabled': False, 'extension': None, - 'features': list([ - 'warm_mist', - 'nightlight', - ]), 'mac_id': '**REDACTED**', 'manager': '**REDACTED**', 'mist_levels': list([ From 31d3b3b675907e9ef228008c1d20606a2af50d5f Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 22 Jul 2024 21:14:15 +1000 Subject: [PATCH 1448/2411] Handle empty energy sites in Tesla integrations (#122355) --- homeassistant/components/tesla_fleet/__init__.py | 13 ++++++++++++- homeassistant/components/teslemetry/__init__.py | 11 +++++++++++ homeassistant/components/tessie/__init__.py | 11 +++++++++++ tests/components/tesla_fleet/fixtures/products.json | 12 +++++++++++- tests/components/teslemetry/fixtures/products.json | 12 +++++++++++- tests/components/tessie/fixtures/products.json | 12 +++++++++++- 6 files changed, 67 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 892859cefd1..2c5ee1b5c75 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo -from .const import DOMAIN, MODELS +from .const import DOMAIN, LOGGER, MODELS from .coordinator import ( TeslaFleetEnergySiteInfoCoordinator, TeslaFleetEnergySiteLiveCoordinator, @@ -113,6 +113,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - ) elif "energy_site_id" in product and tesla.energy: site_id = product["energy_site_id"] + if not ( + product["components"]["battery"] + or product["components"]["solar"] + or "wall_connectors" in product["components"] + ): + LOGGER.debug( + "Skipping Energy Site %s as it has no components", + site_id, + ) + continue + api = EnergySpecific(tesla.energy, site_id) live_coordinator = TeslaFleetEnergySiteLiveCoordinator(hass, api) diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index b65f5fb64ce..6308d62f3a1 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -108,6 +108,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - ) elif "energy_site_id" in product and Scope.ENERGY_DEVICE_DATA in scopes: site_id = product["energy_site_id"] + if not ( + product["components"]["battery"] + or product["components"]["solar"] + or "wall_connectors" in product["components"] + ): + LOGGER.debug( + "Skipping Energy Site %s as it has no components", + site_id, + ) + continue + api = EnergySpecific(teslemetry.energy, site_id) live_coordinator = TeslemetryEnergySiteLiveCoordinator(hass, api) info_coordinator = TeslemetryEnergySiteInfoCoordinator(hass, api, product) diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index 1d6e2a27786..a0bc58896e4 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -111,6 +111,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo for product in products: if "energy_site_id" in product: site_id = product["energy_site_id"] + if not ( + product["components"]["battery"] + or product["components"]["solar"] + or "wall_connectors" in product["components"] + ): + _LOGGER.debug( + "Skipping Energy Site %s as it has no components", + site_id, + ) + continue + api = EnergySpecific(tessie.energy, site_id) energysites.append( TessieEnergyData( diff --git a/tests/components/tesla_fleet/fixtures/products.json b/tests/components/tesla_fleet/fixtures/products.json index e1b76e4cefb..8da921a33f4 100644 --- a/tests/components/tesla_fleet/fixtures/products.json +++ b/tests/components/tesla_fleet/fixtures/products.json @@ -115,7 +115,17 @@ "features": { "rate_plan_manager_no_pricing_constraint": true } + }, + { + "energy_site_id": 98765, + "components": { + "battery": false, + "solar": false, + "grid": false, + "load_meter": false, + "market_type": "residential" + } } ], - "count": 2 + "count": 3 } diff --git a/tests/components/teslemetry/fixtures/products.json b/tests/components/teslemetry/fixtures/products.json index e1b76e4cefb..8da921a33f4 100644 --- a/tests/components/teslemetry/fixtures/products.json +++ b/tests/components/teslemetry/fixtures/products.json @@ -115,7 +115,17 @@ "features": { "rate_plan_manager_no_pricing_constraint": true } + }, + { + "energy_site_id": 98765, + "components": { + "battery": false, + "solar": false, + "grid": false, + "load_meter": false, + "market_type": "residential" + } } ], - "count": 2 + "count": 3 } diff --git a/tests/components/tessie/fixtures/products.json b/tests/components/tessie/fixtures/products.json index e1b76e4cefb..8da921a33f4 100644 --- a/tests/components/tessie/fixtures/products.json +++ b/tests/components/tessie/fixtures/products.json @@ -115,7 +115,17 @@ "features": { "rate_plan_manager_no_pricing_constraint": true } + }, + { + "energy_site_id": 98765, + "components": { + "battery": false, + "solar": false, + "grid": false, + "load_meter": false, + "market_type": "residential" + } } ], - "count": 2 + "count": 3 } From 7ec332f857a13840d654b92135d4897ffc0bd0a1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 22 Jul 2024 04:15:05 -0700 Subject: [PATCH 1449/2411] Fix platforms on media pause and unpause intents (#122357) --- homeassistant/components/media_player/intent.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/intent.py b/homeassistant/components/media_player/intent.py index 77220a87622..8a5d824112a 100644 --- a/homeassistant/components/media_player/intent.py +++ b/homeassistant/components/media_player/intent.py @@ -107,8 +107,6 @@ async def async_setup_intents(hass: HomeAssistant) -> None: class MediaPauseHandler(intent.ServiceIntentHandler): """Handler for pause intent. Records last paused media players.""" - platforms = {DOMAIN} - def __init__(self, last_paused: LastPaused) -> None: """Initialize handler.""" super().__init__( @@ -119,6 +117,7 @@ class MediaPauseHandler(intent.ServiceIntentHandler): required_features=MediaPlayerEntityFeature.PAUSE, required_states={MediaPlayerState.PLAYING}, description="Pauses a media player", + platforms={DOMAIN}, ) self.last_paused = last_paused @@ -144,8 +143,6 @@ class MediaPauseHandler(intent.ServiceIntentHandler): class MediaUnpauseHandler(intent.ServiceIntentHandler): """Handler for unpause/resume intent. Uses last paused media players.""" - platforms = {DOMAIN} - def __init__(self, last_paused: LastPaused) -> None: """Initialize handler.""" super().__init__( @@ -155,6 +152,7 @@ class MediaUnpauseHandler(intent.ServiceIntentHandler): required_domains={DOMAIN}, required_states={MediaPlayerState.PAUSED}, description="Resumes a media player", + platforms={DOMAIN}, ) self.last_paused = last_paused From d421525f1b57a6803003cd5aeda4ef8ec57fbcf4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Jul 2024 06:15:43 -0500 Subject: [PATCH 1450/2411] Fix missing translation key for august doorbells (#122251) --- homeassistant/components/august/binary_sensor.py | 1 + homeassistant/components/august/strings.json | 3 +++ tests/components/august/test_binary_sensor.py | 16 ++++++++-------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 1e257138ba9..6a56692bcd6 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -81,6 +81,7 @@ SENSOR_TYPES_VIDEO_DOORBELL = ( SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = ( AugustDoorbellBinarySensorEntityDescription( key="ding", + translation_key="ding", device_class=BinarySensorDeviceClass.OCCUPANCY, value_fn=retrieve_ding_activity, is_time_based=True, diff --git a/homeassistant/components/august/strings.json b/homeassistant/components/august/strings.json index 2b2058c1822..772a8dca479 100644 --- a/homeassistant/components/august/strings.json +++ b/homeassistant/components/august/strings.json @@ -40,6 +40,9 @@ }, "entity": { "binary_sensor": { + "ding": { + "name": "Doorbell ding" + }, "image_capture": { "name": "Image capture" } diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index 3eb9c80fc8a..33d582de8d8 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -94,7 +94,7 @@ async def test_create_doorbell(hass: HomeAssistant) -> None: ) assert binary_sensor_k98gidt45gul_name_online.state == STATE_ON binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_occupancy" + "binary_sensor.k98gidt45gul_name_doorbell_ding" ) assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF binary_sensor_k98gidt45gul_name_motion = hass.states.get( @@ -121,7 +121,7 @@ async def test_create_doorbell_offline(hass: HomeAssistant) -> None: ) assert binary_sensor_tmt100_name_online.state == STATE_OFF binary_sensor_tmt100_name_ding = hass.states.get( - "binary_sensor.tmt100_name_occupancy" + "binary_sensor.tmt100_name_doorbell_ding" ) assert binary_sensor_tmt100_name_ding.state == STATE_UNAVAILABLE @@ -143,7 +143,7 @@ async def test_create_doorbell_with_motion(hass: HomeAssistant) -> None: ) assert binary_sensor_k98gidt45gul_name_online.state == STATE_ON binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_occupancy" + "binary_sensor.k98gidt45gul_name_doorbell_ding" ) assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) @@ -173,7 +173,7 @@ async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: ) assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_occupancy" + "binary_sensor.k98gidt45gul_name_doorbell_ding" ) assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF @@ -241,7 +241,7 @@ async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_occupancy" + "binary_sensor.k98gidt45gul_name_doorbell_ding" ) assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF @@ -272,7 +272,7 @@ async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: await hass.async_block_till_done() binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_occupancy" + "binary_sensor.k98gidt45gul_name_doorbell_ding" ) assert binary_sensor_k98gidt45gul_name_ding.state == STATE_ON new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) @@ -285,7 +285,7 @@ async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: await hass.async_block_till_done() binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_occupancy" + "binary_sensor.k98gidt45gul_name_doorbell_ding" ) assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF @@ -403,6 +403,6 @@ async def test_create_lock_with_doorbell(hass: HomeAssistant) -> None: await _create_august_with_devices(hass, [lock_one]) ding_sensor = hass.states.get( - "binary_sensor.a6697750d607098bae8d6baa11ef8063_name_occupancy" + "binary_sensor.a6697750d607098bae8d6baa11ef8063_name_doorbell_ding" ) assert ding_sensor.state == STATE_OFF From bd97a09caec7d5b46f56cec06e10ab3e9c0a7790 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Jul 2024 06:57:43 -0500 Subject: [PATCH 1451/2411] Complete coverage for doorbird init (#122272) --- tests/components/doorbird/__init__.py | 4 +- tests/components/doorbird/conftest.py | 2 + tests/components/doorbird/test_init.py | 53 +++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/tests/components/doorbird/__init__.py b/tests/components/doorbird/__init__.py index dc0f041ba31..c342fac20e9 100644 --- a/tests/components/doorbird/__init__.py +++ b/tests/components/doorbird/__init__.py @@ -39,6 +39,7 @@ def get_mock_doorbird_api( info: dict[str, Any] | None = None, info_side_effect: Exception | None = None, schedule: list[DoorBirdScheduleEntry] | None = None, + favorites_side_effect: Exception | None = None, ) -> DoorBird: """Return a mock DoorBirdAPI object with return values.""" doorbirdapi_mock = MagicMock(spec_set=DoorBird) @@ -46,7 +47,8 @@ def get_mock_doorbird_api( side_effect=info_side_effect, return_value=info ) type(doorbirdapi_mock).favorites = AsyncMock( - return_value={"http": {"x": {"value": "http://webhook"}}} + side_effect=favorites_side_effect, + return_value={"http": {"x": {"value": "http://webhook"}}}, ) type(doorbirdapi_mock).change_favorite = AsyncMock(return_value=True) type(doorbirdapi_mock).schedule = AsyncMock(return_value=schedule) diff --git a/tests/components/doorbird/conftest.py b/tests/components/doorbird/conftest.py index 98dbec18b4c..f98fcf0eac8 100644 --- a/tests/components/doorbird/conftest.py +++ b/tests/components/doorbird/conftest.py @@ -81,6 +81,7 @@ async def doorbird_mocker( info: dict[str, Any] | None = None, info_side_effect: Exception | None = None, schedule: list[DoorBirdScheduleEntry] | None = None, + favorites_side_effect: Exception | None = None, ) -> MockDoorbirdEntry: """Create a MockDoorbirdEntry from defaults or specific values.""" entry = entry or MockConfigEntry( @@ -93,6 +94,7 @@ async def doorbird_mocker( info=info or doorbird_info, info_side_effect=info_side_effect, schedule=schedule or doorbird_schedule, + favorites_side_effect=favorites_side_effect, ) entry.add_to_hass(hass) with patch_doorbird_api_entry_points(api): diff --git a/tests/components/doorbird/test_init.py b/tests/components/doorbird/test_init.py index 6bbf694dd7c..fb8bad2fb46 100644 --- a/tests/components/doorbird/test_init.py +++ b/tests/components/doorbird/test_init.py @@ -1,10 +1,12 @@ """Test DoorBird init.""" +import pytest + from homeassistant.components.doorbird.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from . import mock_unauthorized_exception +from . import mock_not_found_exception, mock_unauthorized_exception from .conftest import DoorbirdMockerType @@ -30,3 +32,52 @@ async def test_auth_fails( flows = hass.config_entries.flow.async_progress(DOMAIN) assert len(flows) == 1 assert flows[0]["step_id"] == "reauth_confirm" + + +@pytest.mark.parametrize( + "side_effect", + [OSError, mock_not_found_exception()], +) +async def test_http_info_request_fails( + doorbird_mocker: DoorbirdMockerType, side_effect: Exception +) -> None: + """Test basic setup with an http failure.""" + doorbird_entry = await doorbird_mocker(info_side_effect=side_effect) + assert doorbird_entry.entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_http_favorites_request_fails( + doorbird_mocker: DoorbirdMockerType, +) -> None: + """Test basic setup with an http failure.""" + doorbird_entry = await doorbird_mocker( + favorites_side_effect=mock_not_found_exception() + ) + assert doorbird_entry.entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_events_changed( + hass: HomeAssistant, + doorbird_mocker: DoorbirdMockerType, +) -> None: + """Test basic setup.""" + doorbird_entry = await doorbird_mocker() + entry = doorbird_entry.entry + assert entry.state is ConfigEntryState.LOADED + api = doorbird_entry.api + api.favorites.reset_mock() + api.change_favorite.reset_mock() + api.schedule.reset_mock() + + hass.config_entries.async_update_entry(entry, options={"events": ["xyz"]}) + await hass.async_block_till_done() + assert len(api.favorites.mock_calls) == 2 + assert len(api.schedule.mock_calls) == 1 + + assert len(api.change_favorite.mock_calls) == 1 + favorite_type, title, url = api.change_favorite.mock_calls[0][1] + assert favorite_type == "http" + assert title == "Home Assistant (mydoorbird_xyz)" + assert url == ( + f"http://10.10.10.10:8123/api/doorbird/mydoorbird_xyz?token={entry.entry_id}" + ) From 243a68fb1f70adb607d4b2874c6f0d2f80b589ce Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 22 Jul 2024 14:10:16 +0200 Subject: [PATCH 1452/2411] Frontend wants a timestamp for the created_at/modified_at attributes (#122377) --- .../components/config/floor_registry.py | 2 + .../components/config/label_registry.py | 2 + homeassistant/helpers/area_registry.py | 4 +- tests/components/config/test_area_registry.py | 36 +++++++------ .../components/config/test_floor_registry.py | 38 +++++++++++++- .../components/config/test_label_registry.py | 52 ++++++++++++++++++- 6 files changed, 113 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/config/floor_registry.py b/homeassistant/components/config/floor_registry.py index 05d563325e8..f3c9793d25e 100644 --- a/homeassistant/components/config/floor_registry.py +++ b/homeassistant/components/config/floor_registry.py @@ -132,8 +132,10 @@ def _entry_dict(entry: FloorEntry) -> dict[str, Any]: """Convert entry to API format.""" return { "aliases": list(entry.aliases), + "created_at": entry.created_at.timestamp(), "floor_id": entry.floor_id, "icon": entry.icon, "level": entry.level, "name": entry.name, + "modified_at": entry.modified_at.timestamp(), } diff --git a/homeassistant/components/config/label_registry.py b/homeassistant/components/config/label_registry.py index 07b2f1bbd2e..d02b9849d46 100644 --- a/homeassistant/components/config/label_registry.py +++ b/homeassistant/components/config/label_registry.py @@ -157,8 +157,10 @@ def _entry_dict(entry: LabelEntry) -> dict[str, Any]: """Convert entry to API format.""" return { "color": entry.color, + "created_at": entry.created_at.timestamp(), "description": entry.description, "icon": entry.icon, "label_id": entry.label_id, "name": entry.name, + "modified_at": entry.modified_at.timestamp(), } diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 67af70ea22c..bf6dd0d6fcb 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -87,8 +87,8 @@ class AreaEntry(NormalizedNameBaseRegistryEntry): "labels": list(self.labels), "name": self.name, "picture": self.picture, - "created_at": self.created_at.isoformat(), - "modified_at": self.modified_at.isoformat(), + "created_at": self.created_at.timestamp(), + "modified_at": self.modified_at.timestamp(), } ) ) diff --git a/tests/components/config/test_area_registry.py b/tests/components/config/test_area_registry.py index ed2c1866ad9..03a8272e586 100644 --- a/tests/components/config/test_area_registry.py +++ b/tests/components/config/test_area_registry.py @@ -1,5 +1,7 @@ """Test area_registry API.""" +from datetime import datetime + from freezegun.api import FrozenDateTimeFactory import pytest from pytest_unordered import unordered @@ -28,11 +30,11 @@ async def test_list_areas( freezer: FrozenDateTimeFactory, ) -> None: """Test list entries.""" - created_area1 = "2024-07-16T13:30:00.900075+00:00" + created_area1 = datetime.fromisoformat("2024-07-16T13:30:00.900075+00:00") freezer.move_to(created_area1) area1 = area_registry.async_create("mock 1") - created_area2 = "2024-07-16T13:45:00.900075+00:00" + created_area2 = datetime.fromisoformat("2024-07-16T13:45:00.900075+00:00") freezer.move_to(created_area2) area2 = area_registry.async_create( "mock 2", @@ -55,8 +57,8 @@ async def test_list_areas( "labels": [], "name": "mock 1", "picture": None, - "created_at": created_area1, - "modified_at": created_area1, + "created_at": created_area1.timestamp(), + "modified_at": created_area1.timestamp(), }, { "aliases": unordered(["alias_1", "alias_2"]), @@ -66,8 +68,8 @@ async def test_list_areas( "labels": unordered(["label_1", "label_2"]), "name": "mock 2", "picture": "/image/example.png", - "created_at": created_area2, - "modified_at": created_area2, + "created_at": created_area2.timestamp(), + "modified_at": created_area2.timestamp(), }, ] @@ -93,8 +95,8 @@ async def test_create_area( "labels": [], "name": "mock", "picture": None, - "created_at": utcnow().isoformat(), - "modified_at": utcnow().isoformat(), + "created_at": utcnow().timestamp(), + "modified_at": utcnow().timestamp(), } assert len(area_registry.areas) == 1 @@ -121,8 +123,8 @@ async def test_create_area( "labels": unordered(["label_1", "label_2"]), "name": "mock 2", "picture": "/image/example.png", - "created_at": utcnow().isoformat(), - "modified_at": utcnow().isoformat(), + "created_at": utcnow().timestamp(), + "modified_at": utcnow().timestamp(), } assert len(area_registry.areas) == 2 @@ -185,10 +187,10 @@ async def test_update_area( freezer: FrozenDateTimeFactory, ) -> None: """Test update entry.""" - created_at = "2024-07-16T13:30:00.900075+00:00" + created_at = datetime.fromisoformat("2024-07-16T13:30:00.900075+00:00") freezer.move_to(created_at) area = area_registry.async_create("mock 1") - modified_at = "2024-07-16T13:45:00.900075+00:00" + modified_at = datetime.fromisoformat("2024-07-16T13:45:00.900075+00:00") freezer.move_to(modified_at) await client.send_json_auto_id( @@ -214,12 +216,12 @@ async def test_update_area( "labels": unordered(["label_1", "label_2"]), "name": "mock 2", "picture": "/image/example.png", - "created_at": created_at, - "modified_at": modified_at, + "created_at": created_at.timestamp(), + "modified_at": modified_at.timestamp(), } assert len(area_registry.areas) == 1 - modified_at = "2024-07-16T13:50:00.900075+00:00" + modified_at = datetime.fromisoformat("2024-07-16T13:50:00.900075+00:00") freezer.move_to(modified_at) await client.send_json_auto_id( @@ -244,8 +246,8 @@ async def test_update_area( "labels": [], "name": "mock 2", "picture": None, - "created_at": created_at, - "modified_at": modified_at, + "created_at": created_at.timestamp(), + "modified_at": modified_at.timestamp(), } assert len(area_registry.areas) == 1 diff --git a/tests/components/config/test_floor_registry.py b/tests/components/config/test_floor_registry.py index b4e3907bc4d..da6e550b1f6 100644 --- a/tests/components/config/test_floor_registry.py +++ b/tests/components/config/test_floor_registry.py @@ -1,11 +1,15 @@ """Test floor registry API.""" +from datetime import datetime + +from freezegun.api import FrozenDateTimeFactory import pytest from pytest_unordered import unordered from homeassistant.components.config import floor_registry from homeassistant.core import HomeAssistant from homeassistant.helpers import floor_registry as fr +from homeassistant.util.dt import utcnow from tests.typing import MockHAClientWebSocket, WebSocketGenerator @@ -22,9 +26,15 @@ async def client_fixture( async def test_list_floors( client: MockHAClientWebSocket, floor_registry: fr.FloorRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test list entries.""" + created_1 = datetime.fromisoformat("2024-07-16T13:30:00.900075+00:00") + freezer.move_to(created_1) floor_registry.async_create("First floor") + + created_2 = datetime.fromisoformat("2024-07-16T13:45:00.900075+00:00") + freezer.move_to(created_2) floor_registry.async_create( name="Second floor", aliases={"top floor", "attic"}, @@ -34,6 +44,12 @@ async def test_list_floors( assert len(floor_registry.floors) == 2 + # update first floor to change modified_at + floor_registry.async_update( + "first_floor", + name="First floor...", + ) + await client.send_json_auto_id({"type": "config/floor_registry/list"}) msg = await client.receive_json() @@ -41,20 +57,25 @@ async def test_list_floors( assert len(msg["result"]) == len(floor_registry.floors) assert msg["result"][0] == { "aliases": [], + "created_at": created_1.timestamp(), "icon": None, "floor_id": "first_floor", - "name": "First floor", + "modified_at": created_2.timestamp(), + "name": "First floor...", "level": None, } assert msg["result"][1] == { "aliases": unordered(["top floor", "attic"]), + "created_at": created_2.timestamp(), "icon": "mdi:home-floor-2", "floor_id": "second_floor", + "modified_at": created_2.timestamp(), "name": "Second floor", "level": 2, } +@pytest.mark.usefixtures("freezer") async def test_create_floor( client: MockHAClientWebSocket, floor_registry: fr.FloorRegistry, @@ -69,8 +90,10 @@ async def test_create_floor( assert len(floor_registry.floors) == 1 assert msg["result"] == { "aliases": [], + "created_at": utcnow().timestamp(), "icon": None, "floor_id": "first_floor", + "modified_at": utcnow().timestamp(), "name": "First floor", "level": None, } @@ -90,8 +113,10 @@ async def test_create_floor( assert len(floor_registry.floors) == 2 assert msg["result"] == { "aliases": unordered(["top floor", "attic"]), + "created_at": utcnow().timestamp(), "icon": "mdi:home-floor-2", "floor_id": "second_floor", + "modified_at": utcnow().timestamp(), "name": "Second floor", "level": 2, } @@ -163,10 +188,15 @@ async def test_delete_non_existing_floor( async def test_update_floor( client: MockHAClientWebSocket, floor_registry: fr.FloorRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test update entry.""" + created_at = datetime.fromisoformat("2024-07-16T13:30:00.900075+00:00") + freezer.move_to(created_at) floor = floor_registry.async_create("First floor") assert len(floor_registry.floors) == 1 + modified_at = datetime.fromisoformat("2024-07-16T13:45:00.900075+00:00") + freezer.move_to(modified_at) await client.send_json_auto_id( { @@ -184,12 +214,16 @@ async def test_update_floor( assert len(floor_registry.floors) == 1 assert msg["result"] == { "aliases": unordered(["top floor", "attic"]), + "created_at": created_at.timestamp(), "icon": "mdi:home-floor-2", "floor_id": floor.floor_id, + "modified_at": modified_at.timestamp(), "name": "Second floor", "level": 2, } + modified_at = datetime.fromisoformat("2024-07-16T13:50:00.900075+00:00") + freezer.move_to(modified_at) await client.send_json_auto_id( { "floor_id": floor.floor_id, @@ -206,8 +240,10 @@ async def test_update_floor( assert len(floor_registry.floors) == 1 assert msg["result"] == { "aliases": [], + "created_at": created_at.timestamp(), "icon": None, "floor_id": floor.floor_id, + "modified_at": modified_at.timestamp(), "name": "First floor", "level": None, } diff --git a/tests/components/config/test_label_registry.py b/tests/components/config/test_label_registry.py index 040b3bfe28a..3eff759132f 100644 --- a/tests/components/config/test_label_registry.py +++ b/tests/components/config/test_label_registry.py @@ -1,5 +1,8 @@ """Test label registry API.""" +from datetime import datetime + +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.config import label_registry @@ -21,9 +24,15 @@ async def client_fixture( async def test_list_labels( client: MockHAClientWebSocket, label_registry: lr.LabelRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test list entries.""" + created_1 = datetime.fromisoformat("2024-07-16T13:30:00.900075+00:00") + freezer.move_to(created_1) label_registry.async_create("mock 1") + + created_2 = datetime.fromisoformat("2024-07-16T13:45:00.900075+00:00") + freezer.move_to(created_2) label_registry.async_create( name="mock 2", color="#00FF00", @@ -33,6 +42,12 @@ async def test_list_labels( assert len(label_registry.labels) == 2 + # update mock 1 to change modified_at + label_registry.async_update( + "mock_1", + name="Mock 1...", + ) + await client.send_json_auto_id({"type": "config/label_registry/list"}) msg = await client.receive_json() @@ -40,16 +55,20 @@ async def test_list_labels( assert len(msg["result"]) == len(label_registry.labels) assert msg["result"][0] == { "color": None, + "created_at": created_1.timestamp(), "description": None, "icon": None, "label_id": "mock_1", - "name": "mock 1", + "modified_at": created_2.timestamp(), + "name": "Mock 1...", } assert msg["result"][1] == { "color": "#00FF00", + "created_at": created_2.timestamp(), "description": "This is the second label", "icon": "mdi:two", "label_id": "mock_2", + "modified_at": created_2.timestamp(), "name": "mock 2", } @@ -57,8 +76,11 @@ async def test_list_labels( async def test_create_label( client: MockHAClientWebSocket, label_registry: lr.LabelRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test create entry.""" + created_1 = datetime.fromisoformat("2024-07-16T13:30:00.900075+00:00") + freezer.move_to(created_1) await client.send_json_auto_id( { "name": "MOCK", @@ -71,12 +93,16 @@ async def test_create_label( assert len(label_registry.labels) == 1 assert msg["result"] == { "color": None, + "created_at": created_1.timestamp(), "description": None, "icon": None, "label_id": "mock", "name": "MOCK", + "modified_at": created_1.timestamp(), } + created_2 = datetime.fromisoformat("2024-07-17T13:30:00.900075+00:00") + freezer.move_to(created_2) await client.send_json_auto_id( { "id": 2, @@ -93,12 +119,16 @@ async def test_create_label( assert len(label_registry.labels) == 2 assert msg["result"] == { "color": "#00FF00", + "created_at": created_2.timestamp(), "description": "This is the second label", "icon": "mdi:two", "label_id": "mockery", + "modified_at": created_2.timestamp(), "name": "MOCKERY", } + created_3 = datetime.fromisoformat("2024-07-18T13:30:00.900075+00:00") + freezer.move_to(created_3) await client.send_json_auto_id( { "name": "MAGIC", @@ -114,9 +144,11 @@ async def test_create_label( assert len(label_registry.labels) == 3 assert msg["result"] == { "color": "indigo", + "created_at": created_3.timestamp(), "description": "This is the third label", "icon": "mdi:three", "label_id": "magic", + "modified_at": created_3.timestamp(), "name": "MAGIC", } @@ -182,11 +214,17 @@ async def test_delete_non_existing_label( async def test_update_label( client: MockHAClientWebSocket, label_registry: lr.LabelRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test update entry.""" + created_at = datetime.fromisoformat("2024-07-16T13:30:00.900075+00:00") + freezer.move_to(created_at) label = label_registry.async_create("mock") assert len(label_registry.labels) == 1 + modified_at = datetime.fromisoformat("2024-07-16T13:45:00.900075+00:00") + freezer.move_to(modified_at) + await client.send_json_auto_id( { "label_id": label.label_id, @@ -203,12 +241,17 @@ async def test_update_label( assert len(label_registry.labels) == 1 assert msg["result"] == { "color": "#00FF00", + "created_at": created_at.timestamp(), "description": "This is a label description", "icon": "mdi:test", "label_id": "mock", + "modified_at": modified_at.timestamp(), "name": "UPDATED", } + modified_at = datetime.fromisoformat("2024-07-16T13:50:00.900075+00:00") + freezer.move_to(modified_at) + await client.send_json_auto_id( { "label_id": label.label_id, @@ -225,12 +268,17 @@ async def test_update_label( assert len(label_registry.labels) == 1 assert msg["result"] == { "color": None, + "created_at": created_at.timestamp(), "description": None, "icon": None, "label_id": "mock", + "modified_at": modified_at.timestamp(), "name": "UPDATED AGAIN", } + modified_at = datetime.fromisoformat("2024-07-16T13:55:00.900075+00:00") + freezer.move_to(modified_at) + await client.send_json_auto_id( { "label_id": label.label_id, @@ -247,9 +295,11 @@ async def test_update_label( assert len(label_registry.labels) == 1 assert msg["result"] == { "color": "primary", + "created_at": created_at.timestamp(), "description": None, "icon": None, "label_id": "mock", + "modified_at": modified_at.timestamp(), "name": "UPDATED YET AGAIN", } From 186ca49b16874a46d99b20bb175e35b80f60e20f Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Mon, 22 Jul 2024 08:30:23 -0400 Subject: [PATCH 1453/2411] Fix group media player `play_media` not passing kwargs (#122258) --- .../components/group/media_player.py | 2 + tests/components/group/test_media_player.py | 62 ++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index 4b71cf7f81d..7d2ce46b107 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -365,6 +365,8 @@ class MediaPlayerGroup(MediaPlayerEntity): ATTR_MEDIA_CONTENT_ID: media_id, ATTR_MEDIA_CONTENT_TYPE: media_type, } + if kwargs: + data.update(kwargs) await self.hass.services.async_call( DOMAIN, SERVICE_PLAY_MEDIA, diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py index 451aae200b3..23cdd1598dd 100644 --- a/tests/components/group/test_media_player.py +++ b/tests/components/group/test_media_player.py @@ -1,14 +1,16 @@ """The tests for the Media group platform.""" import asyncio -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch import pytest from homeassistant.components.group import DOMAIN from homeassistant.components.media_player import ( + ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_EXTRA, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SHUFFLE, ATTR_MEDIA_TRACK, @@ -45,7 +47,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import entity_platform, entity_registry as er from homeassistant.setup import async_setup_component @@ -598,3 +600,59 @@ async def test_nested_group(hass: HomeAssistant) -> None: assert hass.states.get("media_player.kitchen").state == STATE_OFF assert hass.states.get("media_player.group_1").state == STATE_OFF assert hass.states.get("media_player.nested_group").state == STATE_OFF + + +async def test_service_play_media_kwargs(hass: HomeAssistant) -> None: + """Test that kwargs get passed through on play_media service call.""" + await async_setup_component( + hass, + MEDIA_DOMAIN, + { + MEDIA_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": [ + "media_player.bedroom", + "media_player.living_room", + ], + }, + ] + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + platform = entity_platform.async_get_platforms(hass, "media_player")[0] + mp_bedroom = platform.domain_entities["media_player.bedroom"] + mp_bedroom.play_media = MagicMock() + + mp_living_room = platform.domain_entities["media_player.living_room"] + mp_living_room.play_media = MagicMock() + + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: "media_player.media_group", + ATTR_MEDIA_CONTENT_TYPE: "some_type", + ATTR_MEDIA_CONTENT_ID: "some_id", + ATTR_MEDIA_ANNOUNCE: "true", + ATTR_MEDIA_EXTRA: { + "volume": 20, + }, + }, + ) + await hass.async_block_till_done() + + assert mp_bedroom.play_media.call_count == 1 + mp_bedroom.play_media.assert_called_with( + "some_type", "some_id", announce=True, extra={"volume": 20} + ) + + assert mp_living_room.play_media.call_count == 1 + mp_living_room.play_media.assert_called_with( + "some_type", "some_id", announce=True, extra={"volume": 20} + ) From debebcfd2565f8ffeb34715b1e7a249eb4e20254 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Jul 2024 15:32:36 +0200 Subject: [PATCH 1454/2411] Improve language in loader error messages (#122387) --- homeassistant/loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 9acc1682602..90b88ba2109 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -945,7 +945,7 @@ class Integration: except IntegrationNotFound as err: _LOGGER.error( ( - "Unable to resolve dependencies for %s: we are unable to resolve" + "Unable to resolve dependencies for %s: unable to resolve" " (sub)dependency %s" ), self.domain, @@ -954,7 +954,7 @@ class Integration: except CircularDependency as err: _LOGGER.error( ( - "Unable to resolve dependencies for %s: it contains a circular" + "Unable to resolve dependencies for %s: it contains a circular" " dependency: %s -> %s" ), self.domain, From 7ec41275d5b3a6246cdfad062731783eb6eb13ce Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 22 Jul 2024 15:34:10 +0200 Subject: [PATCH 1455/2411] Add mealie service to set mealplan (#122317) --- homeassistant/components/mealie/const.py | 2 + homeassistant/components/mealie/icons.json | 3 +- homeassistant/components/mealie/services.py | 57 ++++++++++++ homeassistant/components/mealie/services.yaml | 29 ++++++ homeassistant/components/mealie/strings.json | 30 +++++++ tests/components/mealie/conftest.py | 6 +- .../mealie/snapshots/test_services.ambr | 48 ++++++++++ tests/components/mealie/test_services.py | 88 +++++++++++++++++++ 8 files changed, 259 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mealie/const.py b/homeassistant/components/mealie/const.py index 95802bfc02a..c040d665794 100644 --- a/homeassistant/components/mealie/const.py +++ b/homeassistant/components/mealie/const.py @@ -15,5 +15,7 @@ ATTR_RECIPE_ID = "recipe_id" ATTR_URL = "url" ATTR_INCLUDE_TAGS = "include_tags" ATTR_ENTRY_TYPE = "entry_type" +ATTR_NOTE_TITLE = "note_title" +ATTR_NOTE_TEXT = "note_text" MIN_REQUIRED_MEALIE_VERSION = AwesomeVersion("v1.0.0") diff --git a/homeassistant/components/mealie/icons.json b/homeassistant/components/mealie/icons.json index 883779a8fb0..16176391701 100644 --- a/homeassistant/components/mealie/icons.json +++ b/homeassistant/components/mealie/icons.json @@ -27,6 +27,7 @@ "get_mealplan": "mdi:food", "get_recipe": "mdi:map", "import_recipe": "mdi:map-search", - "set_random_mealplan": "mdi:dice-multiple" + "set_random_mealplan": "mdi:dice-multiple", + "set_mealplan": "mdi:food" } } diff --git a/homeassistant/components/mealie/services.py b/homeassistant/components/mealie/services.py index 3b1257ff16d..f195be37b11 100644 --- a/homeassistant/components/mealie/services.py +++ b/homeassistant/components/mealie/services.py @@ -28,6 +28,8 @@ from .const import ( ATTR_END_DATE, ATTR_ENTRY_TYPE, ATTR_INCLUDE_TAGS, + ATTR_NOTE_TEXT, + ATTR_NOTE_TITLE, ATTR_RECIPE_ID, ATTR_START_DATE, ATTR_URL, @@ -70,6 +72,31 @@ SERVICE_SET_RANDOM_MEALPLAN_SCHEMA = vol.Schema( } ) +SERVICE_SET_MEALPLAN = "set_mealplan" +SERVICE_SET_MEALPLAN_SCHEMA = vol.Any( + vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): str, + vol.Required(ATTR_DATE): cv.date, + vol.Required(ATTR_ENTRY_TYPE): vol.In( + [x.lower() for x in MealplanEntryType] + ), + vol.Required(ATTR_RECIPE_ID): str, + } + ), + vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): str, + vol.Required(ATTR_DATE): cv.date, + vol.Required(ATTR_ENTRY_TYPE): vol.In( + [x.lower() for x in MealplanEntryType] + ), + vol.Required(ATTR_NOTE_TITLE): str, + vol.Required(ATTR_NOTE_TEXT): str, + } + ), +) + def async_get_entry(hass: HomeAssistant, config_entry_id: str) -> MealieConfigEntry: """Get the Mealie config entry.""" @@ -170,6 +197,29 @@ def setup_services(hass: HomeAssistant) -> None: return {"mealplan": asdict(mealplan)} return None + async def async_set_mealplan(call: ServiceCall) -> ServiceResponse: + """Set a mealplan.""" + entry = async_get_entry(hass, call.data[ATTR_CONFIG_ENTRY_ID]) + mealplan_date = call.data[ATTR_DATE] + entry_type = MealplanEntryType(call.data[ATTR_ENTRY_TYPE]) + client = entry.runtime_data.client + try: + mealplan = await client.set_mealplan( + mealplan_date, + entry_type, + recipe_id=call.data.get(ATTR_RECIPE_ID), + note_title=call.data.get(ATTR_NOTE_TITLE), + note_text=call.data.get(ATTR_NOTE_TEXT), + ) + except MealieConnectionError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="connection_error", + ) from err + if call.return_response: + return {"mealplan": asdict(mealplan)} + return None + hass.services.async_register( DOMAIN, SERVICE_GET_MEALPLAN, @@ -198,3 +248,10 @@ def setup_services(hass: HomeAssistant) -> None: schema=SERVICE_SET_RANDOM_MEALPLAN_SCHEMA, supports_response=SupportsResponse.OPTIONAL, ) + hass.services.async_register( + DOMAIN, + SERVICE_SET_MEALPLAN, + async_set_mealplan, + schema=SERVICE_SET_MEALPLAN_SCHEMA, + supports_response=SupportsResponse.OPTIONAL, + ) diff --git a/homeassistant/components/mealie/services.yaml b/homeassistant/components/mealie/services.yaml index c569df956e2..47a79ba5756 100644 --- a/homeassistant/components/mealie/services.yaml +++ b/homeassistant/components/mealie/services.yaml @@ -58,3 +58,32 @@ set_random_mealplan: - dinner - side translation_key: mealplan_entry_type + +set_mealplan: + fields: + config_entry_id: + required: true + selector: + config_entry: + integration: mealie + date: + selector: + date: + entry_type: + selector: + select: + options: + - breakfast + - lunch + - dinner + - side + translation_key: mealplan_entry_type + recipe_id: + selector: + text: + note_title: + selector: + text: + note_text: + selector: + text: diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json index 3524b1a5fb3..785dd98fea6 100644 --- a/homeassistant/components/mealie/strings.json +++ b/homeassistant/components/mealie/strings.json @@ -175,6 +175,36 @@ "description": "The type of dish to randomize." } } + }, + "set_mealplan": { + "name": "Set a mealplan", + "description": "Set a mealplan for a specific date", + "fields": { + "config_entry_id": { + "name": "[%key:component::mealie::services::get_mealplan::fields::config_entry_id::name%]", + "description": "[%key:component::mealie::services::get_mealplan::fields::config_entry_id::description%]" + }, + "date": { + "name": "[%key:component::mealie::services::set_random_mealplan::fields::date::name%]", + "description": "[%key:component::mealie::services::set_random_mealplan::fields::date::description%]" + }, + "entry_type": { + "name": "[%key:component::mealie::services::set_random_mealplan::fields::entry_type::name%]", + "description": "The type of dish to set the recipe to." + }, + "recipe_id": { + "name": "[%key:component::mealie::services::get_recipe::fields::recipe_id::name%]", + "description": "[%key:component::mealie::services::get_recipe::fields::recipe_id::description%]" + }, + "note_title": { + "name": "Meal note title", + "description": "Meal note title for when planning without recipe." + }, + "note_text": { + "name": "Note text", + "description": "Meal note text for when planning without recipe." + } + } } }, "selector": { diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py index 208dd47ddf2..ba42d16e56e 100644 --- a/tests/components/mealie/conftest.py +++ b/tests/components/mealie/conftest.py @@ -74,9 +74,9 @@ def mock_mealie_client() -> Generator[AsyncMock]: client.get_statistics.return_value = Statistics.from_json( load_fixture("statistics.json", DOMAIN) ) - client.random_mealplan.return_value = Mealplan.from_json( - load_fixture("mealplan.json", DOMAIN) - ) + mealplan = Mealplan.from_json(load_fixture("mealplan.json", DOMAIN)) + client.random_mealplan.return_value = mealplan + client.set_mealplan.return_value = mealplan yield client diff --git a/tests/components/mealie/snapshots/test_services.ambr b/tests/components/mealie/snapshots/test_services.ambr index 293a1d8ee1d..3ae158f1d2d 100644 --- a/tests/components/mealie/snapshots/test_services.ambr +++ b/tests/components/mealie/snapshots/test_services.ambr @@ -675,6 +675,54 @@ }), }) # --- +# name: test_service_set_mealplan[payload0-kwargs0] + dict({ + 'mealplan': dict({ + 'description': None, + 'entry_type': , + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'mealplan_date': datetime.date(2024, 1, 22), + 'mealplan_id': 230, + 'recipe': dict({ + 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'image': 'AiIo', + 'name': 'Zoete aardappel curry traybake', + 'original_url': 'https://chickslovefood.com/recept/zoete-aardappel-curry-traybake/', + 'recipe_id': 'c5f00a93-71a2-4e48-900f-d9ad0bb9de93', + 'recipe_yield': '2 servings', + 'slug': 'zoete-aardappel-curry-traybake', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + 'title': None, + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + }) +# --- +# name: test_service_set_mealplan[payload1-kwargs1] + dict({ + 'mealplan': dict({ + 'description': None, + 'entry_type': , + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'mealplan_date': datetime.date(2024, 1, 22), + 'mealplan_id': 230, + 'recipe': dict({ + 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'image': 'AiIo', + 'name': 'Zoete aardappel curry traybake', + 'original_url': 'https://chickslovefood.com/recept/zoete-aardappel-curry-traybake/', + 'recipe_id': 'c5f00a93-71a2-4e48-900f-d9ad0bb9de93', + 'recipe_yield': '2 servings', + 'slug': 'zoete-aardappel-curry-traybake', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + 'title': None, + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + }) +# --- # name: test_service_set_random_mealplan dict({ 'mealplan': dict({ diff --git a/tests/components/mealie/test_services.py b/tests/components/mealie/test_services.py index 06ed714ea01..1c8c6f19de7 100644 --- a/tests/components/mealie/test_services.py +++ b/tests/components/mealie/test_services.py @@ -18,6 +18,8 @@ from homeassistant.components.mealie.const import ( ATTR_END_DATE, ATTR_ENTRY_TYPE, ATTR_INCLUDE_TAGS, + ATTR_NOTE_TEXT, + ATTR_NOTE_TITLE, ATTR_RECIPE_ID, ATTR_START_DATE, ATTR_URL, @@ -27,6 +29,7 @@ from homeassistant.components.mealie.services import ( SERVICE_GET_MEALPLAN, SERVICE_GET_RECIPE, SERVICE_IMPORT_RECIPE, + SERVICE_SET_MEALPLAN, SERVICE_SET_RANDOM_MEALPLAN, ) from homeassistant.const import ATTR_DATE @@ -231,6 +234,71 @@ async def test_service_set_random_mealplan( ) +@pytest.mark.parametrize( + ("payload", "kwargs"), + [ + ( + { + ATTR_RECIPE_ID: "recipe_id", + }, + {"recipe_id": "recipe_id", "note_title": None, "note_text": None}, + ), + ( + { + ATTR_NOTE_TITLE: "Note Title", + ATTR_NOTE_TEXT: "Note Text", + }, + {"recipe_id": None, "note_title": "Note Title", "note_text": "Note Text"}, + ), + ], +) +async def test_service_set_mealplan( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + payload: dict[str, str], + kwargs: dict[str, str], +) -> None: + """Test the set_mealplan service.""" + + await setup_integration(hass, mock_config_entry) + + response = await hass.services.async_call( + DOMAIN, + SERVICE_SET_MEALPLAN, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATE: "2023-10-21", + ATTR_ENTRY_TYPE: "lunch", + } + | payload, + blocking=True, + return_response=True, + ) + assert response == snapshot + mock_mealie_client.set_mealplan.assert_called_with( + date(2023, 10, 21), MealplanEntryType.LUNCH, **kwargs + ) + + mock_mealie_client.random_mealplan.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MEALPLAN, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATE: "2023-10-21", + ATTR_ENTRY_TYPE: "lunch", + } + | payload, + blocking=True, + return_response=False, + ) + mock_mealie_client.set_mealplan.assert_called_with( + date(2023, 10, 21), MealplanEntryType.LUNCH, **kwargs + ) + + @pytest.mark.parametrize( ("service", "payload", "function", "exception", "raised_exception", "message"), [ @@ -282,6 +350,18 @@ async def test_service_set_random_mealplan( HomeAssistantError, "Error connecting to Mealie instance", ), + ( + SERVICE_SET_MEALPLAN, + { + ATTR_DATE: "2023-10-21", + ATTR_ENTRY_TYPE: "lunch", + ATTR_RECIPE_ID: "recipe_id", + }, + "set_mealplan", + MealieConnectionError, + HomeAssistantError, + "Error connecting to Mealie instance", + ), ], ) async def test_services_connection_error( @@ -321,6 +401,14 @@ async def test_services_connection_error( SERVICE_SET_RANDOM_MEALPLAN, {ATTR_DATE: "2023-10-21", ATTR_ENTRY_TYPE: "lunch"}, ), + ( + SERVICE_SET_MEALPLAN, + { + ATTR_DATE: "2023-10-21", + ATTR_ENTRY_TYPE: "lunch", + ATTR_RECIPE_ID: "recipe_id", + }, + ), ], ) async def test_service_entry_availability( From c73e7ae1784c2d2b4912f13809c92fb3fea8be8a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Jul 2024 15:41:55 +0200 Subject: [PATCH 1456/2411] Handle integration with missing dependencies (#122386) --- homeassistant/bootstrap.py | 8 +++++++- tests/test_bootstrap.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index f4cfc8c87c8..a16fd1fa3e9 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -906,7 +906,13 @@ async def _async_resolve_domains_to_setup( await asyncio.gather(*resolve_dependencies_tasks) for itg in integrations_to_process: - for dep in itg.all_dependencies: + try: + all_deps = itg.all_dependencies + except RuntimeError: + # Integration.all_dependencies raises RuntimeError if + # dependencies could not be resolved + continue + for dep in all_deps: if dep in domains_to_setup: continue domains_to_setup.add(dep) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 8eb411fc4ee..153bb9a07f7 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1329,6 +1329,34 @@ async def test_bootstrap_dependencies( ) +@pytest.mark.parametrize("load_registries", [False]) +async def test_bootstrap_dependency_not_found( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test setup when an integration has missing dependencies.""" + mock_integration( + hass, + MockModule("good_integration", dependencies=[]), + ) + # Simulate an integration with missing dependencies. While a core integration + # can't have missing dependencies thanks to checks by hassfest, there's no such + # guarantee for custom integrations. + mock_integration( + hass, + MockModule("bad_integration", dependencies=["hahaha_crash_and_burn"]), + ) + + assert await bootstrap.async_from_config_dict( + {"good_integration": {}, "bad_integration": {}}, hass + ) + + assert "good_integration" in hass.config.components + assert "bad_integration" not in hass.config.components + + assert "Unable to resolve dependencies for bad_integration" in caplog.text + + async def test_pre_import_no_requirements(hass: HomeAssistant) -> None: """Test pre-imported and do not have any requirements.""" pre_imports = [ From e8b88557ee455b0da6238c0e5f5f80c9a0e783a8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Jul 2024 16:53:54 +0200 Subject: [PATCH 1457/2411] Refactor recorder schema migration (#122372) * Refactor recorder schema migration * Simplify * Remove unused imports * Refactor _migrate_schema according to review comments * Add comment --- homeassistant/components/recorder/core.py | 117 +++++++++++------- .../components/recorder/migration.py | 72 ++++++++--- tests/components/recorder/conftest.py | 17 ++- tests/components/recorder/test_init.py | 13 +- tests/components/recorder/test_migrate.py | 23 +++- .../components/recorder/test_websocket_api.py | 2 +- 6 files changed, 168 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index a7e968fe544..f77305277c8 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -716,6 +716,15 @@ class Recorder(threading.Thread): self._event_session_has_pending_writes = True session.add(obj) + def _notify_migration_failed(self) -> None: + """Notify the user schema migration failed.""" + persistent_notification.create( + self.hass, + "The database migration failed, check [the logs](/config/logs).", + "Database Migration Failed", + "recorder_database_migration", + ) + def _run(self) -> None: """Start processing events to save.""" thread_id = threading.get_ident() @@ -741,26 +750,36 @@ class Recorder(threading.Thread): self.migration_is_live = migration.live_migration(schema_status) self.hass.add_job(self.async_connection_success) - database_was_ready = self.migration_is_live or schema_status.valid - - if database_was_ready: - # If the migrate is live or the schema is valid, we need to - # wait for startup to complete. If its not live, we need to continue - # on. - self._activate_and_set_db_ready() - - # We wait to start a live migration until startup has finished - # since it can be cpu intensive and we do not want it to compete - # with startup which is also cpu intensive - if self._wait_startup_or_shutdown() is SHUTDOWN_TASK: - # Shutdown happened before Home Assistant finished starting - self.migration_in_progress = False - # Make sure we cleanly close the run if - # we restart before startup finishes - return + # First do non-live migration steps, if needed if not schema_status.valid: - if self._migrate_schema_and_setup_run(schema_status): + result, schema_status = self._migrate_schema_offline(schema_status) + if not result: + self._notify_migration_failed() + self.migration_in_progress = False + return + self.schema_version = schema_status.current_version + # Non-live migration is now completed, remaining steps are live + self.migration_is_live = True + + # After non-live migration, activate the recorder + self._activate_and_set_db_ready(schema_status) + # We wait to start a live migration until startup has finished + # since it can be cpu intensive and we do not want it to compete + # with startup which is also cpu intensive + if self._wait_startup_or_shutdown() is SHUTDOWN_TASK: + # Shutdown happened before Home Assistant finished starting + self.migration_in_progress = False + # Make sure we cleanly close the run if + # we restart before startup finishes + return + + # Do live migration steps, if needed + if not schema_status.valid: + result, schema_status = self._migrate_schema_live_and_setup_run( + schema_status + ) + if result: self.schema_version = SCHEMA_VERSION if not self._event_listener: # If the schema migration takes so long that the end @@ -768,17 +787,9 @@ class Recorder(threading.Thread): # was True, we need to reinitialize the listener. self.hass.add_job(self.async_initialize) else: - persistent_notification.create( - self.hass, - "The database migration failed, check [the logs](/config/logs).", - "Database Migration Failed", - "recorder_database_migration", - ) + self._notify_migration_failed() return - if not database_was_ready: - self._activate_and_set_db_ready() - # Catch up with missed statistics self._schedule_compile_missing_statistics() _LOGGER.debug("Recorder processing the queue") @@ -786,7 +797,9 @@ class Recorder(threading.Thread): self.hass.add_job(self._async_set_recorder_ready_migration_done) self._run_event_loop() - def _activate_and_set_db_ready(self) -> None: + def _activate_and_set_db_ready( + self, schema_status: migration.SchemaValidationStatus + ) -> None: """Activate the table managers or schedule migrations and mark the db as ready.""" with session_scope(session=self.get_session()) as session: # Prime the statistics meta manager as soon as possible @@ -808,7 +821,7 @@ class Recorder(threading.Thread): EventTypeIDMigration, EntityIDMigration, ): - migrator = migrator_cls(schema_version, migration_changes) + migrator = migrator_cls(schema_status.start_version, migration_changes) migrator.do_migrate(self, session) if self.schema_version > LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION: @@ -947,9 +960,15 @@ class Recorder(threading.Thread): """Set the migration started event.""" self.async_migration_event.set() - def _migrate_schema_and_setup_run( + def _migrate_schema_offline( self, schema_status: migration.SchemaValidationStatus - ) -> bool: + ) -> tuple[bool, migration.SchemaValidationStatus]: + """Migrate schema to the latest version.""" + return self._migrate_schema(schema_status, False) + + def _migrate_schema_live_and_setup_run( + self, schema_status: migration.SchemaValidationStatus + ) -> tuple[bool, migration.SchemaValidationStatus]: """Migrate schema to the latest version.""" persistent_notification.create( self.hass, @@ -965,26 +984,40 @@ class Recorder(threading.Thread): "recorder_database_migration", ) self.hass.add_job(self._async_migration_started) - try: - assert self.engine is not None - migration.migrate_schema( + migration_result, schema_status = self._migrate_schema(schema_status, True) + if migration_result: + self._setup_run() + return migration_result, schema_status + finally: + self.migration_in_progress = False + persistent_notification.dismiss(self.hass, "recorder_database_migration") + + def _migrate_schema( + self, + schema_status: migration.SchemaValidationStatus, + live: bool, + ) -> tuple[bool, migration.SchemaValidationStatus]: + """Migrate schema to the latest version.""" + assert self.engine is not None + try: + if live: + migrator = migration.migrate_schema_live + else: + migrator = migration.migrate_schema_non_live + new_schema_status = migrator( self, self.hass, self.engine, self.get_session, schema_status ) except exc.DatabaseError as err: if self._handle_database_error(err): - return True + return (True, schema_status) _LOGGER.exception("Database error during schema migration") - return False + return (False, schema_status) except Exception: _LOGGER.exception("Error during schema migration") - return False + return (False, schema_status) else: - self._setup_run() - return True - finally: - self.migration_in_progress = False - persistent_notification.dismiss(self.hass, "recorder_database_migration") + return (True, new_schema_status) def _lock_database(self, task: DatabaseLockTask) -> None: @callback diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index d0beb4f9895..0af0788a42a 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -188,12 +188,13 @@ def get_schema_version(session_maker: Callable[[], Session]) -> int | None: return None -@dataclass +@dataclass(frozen=True) class SchemaValidationStatus: """Store schema validation status.""" current_version: int schema_errors: set[str] + start_version: int valid: bool @@ -224,7 +225,9 @@ def validate_db_schema( valid = is_current and not schema_errors - return SchemaValidationStatus(current_version, schema_errors, valid) + return SchemaValidationStatus( + current_version, schema_errors, current_version, valid + ) def _find_schema_errors( @@ -260,35 +263,30 @@ def pre_migrate_schema(engine: Engine) -> None: ) -def migrate_schema( +def _migrate_schema( instance: Recorder, hass: HomeAssistant, engine: Engine, session_maker: Callable[[], Session], schema_status: SchemaValidationStatus, -) -> None: + end_version: int, +) -> SchemaValidationStatus: """Check if the schema needs to be upgraded.""" current_version = schema_status.current_version - if current_version != SCHEMA_VERSION: + start_version = schema_status.start_version + + if current_version < end_version: _LOGGER.warning( "Database is about to upgrade from schema version: %s to: %s", current_version, - SCHEMA_VERSION, + end_version, ) - db_ready = False - for version in range(current_version, SCHEMA_VERSION): - if ( - live_migration(dataclass_replace(schema_status, current_version=version)) - and not db_ready - ): - db_ready = True - instance.migration_is_live = True - hass.add_job(instance.async_set_db_ready) + schema_status = dataclass_replace(schema_status, current_version=end_version) + + for version in range(current_version, end_version): new_version = version + 1 _LOGGER.info("Upgrading recorder db schema to version %s", new_version) - _apply_update( - instance, hass, engine, session_maker, new_version, current_version - ) + _apply_update(instance, hass, engine, session_maker, new_version, start_version) with session_scope(session=session_maker()) as session: session.add(SchemaChanges(schema_version=new_version)) @@ -296,6 +294,37 @@ def migrate_schema( # so its clear that the upgrade is done _LOGGER.warning("Upgrade to version %s done", new_version) + return schema_status + + +def migrate_schema_non_live( + instance: Recorder, + hass: HomeAssistant, + engine: Engine, + session_maker: Callable[[], Session], + schema_status: SchemaValidationStatus, +) -> SchemaValidationStatus: + """Check if the schema needs to be upgraded.""" + end_version = LIVE_MIGRATION_MIN_SCHEMA_VERSION - 1 + return _migrate_schema( + instance, hass, engine, session_maker, schema_status, end_version + ) + + +def migrate_schema_live( + instance: Recorder, + hass: HomeAssistant, + engine: Engine, + session_maker: Callable[[], Session], + schema_status: SchemaValidationStatus, +) -> SchemaValidationStatus: + """Check if the schema needs to be upgraded.""" + end_version = SCHEMA_VERSION + schema_status = _migrate_schema( + instance, hass, engine, session_maker, schema_status, end_version + ) + + # Repairs are currently done during the live migration if schema_errors := schema_status.schema_errors: _LOGGER.warning( "Database is about to correct DB schema errors: %s", @@ -305,12 +334,15 @@ def migrate_schema( states_correct_db_schema(instance, schema_errors) events_correct_db_schema(instance, schema_errors) - if current_version != SCHEMA_VERSION: - instance.queue_task(PostSchemaMigrationTask(current_version, SCHEMA_VERSION)) + start_version = schema_status.start_version + if start_version != SCHEMA_VERSION: + instance.queue_task(PostSchemaMigrationTask(start_version, SCHEMA_VERSION)) # Make sure the post schema migration task is committed in case # the next task does not have commit_before = True instance.queue_task(CommitTask()) + return schema_status + def _create_index( session_maker: Callable[[], Session], table_name: str, index_name: str diff --git a/tests/components/recorder/conftest.py b/tests/components/recorder/conftest.py index fb58ad581d3..f562ba163ba 100644 --- a/tests/components/recorder/conftest.py +++ b/tests/components/recorder/conftest.py @@ -2,6 +2,7 @@ from collections.abc import AsyncGenerator, Generator from dataclasses import dataclass +from functools import partial import threading from unittest.mock import Mock, patch @@ -69,15 +70,16 @@ async def instrument_migration( ) -> AsyncGenerator[InstrumentedMigration]: """Instrument recorder migration.""" - real_migrate_schema = recorder.migration.migrate_schema + real_migrate_schema_live = recorder.migration.migrate_schema_live + real_migrate_schema_non_live = recorder.migration.migrate_schema_non_live real_apply_update = recorder.migration._apply_update - def _instrument_migrate_schema(*args): + def _instrument_migrate_schema(real_func, *args): """Control migration progress and check results.""" instrumented_migration.migration_started.set() try: - real_migrate_schema(*args) + migration_result = real_func(*args) except Exception: instrumented_migration.migration_done.set() raise @@ -92,6 +94,7 @@ async def instrument_migration( ) instrumented_migration.migration_version = res.schema_version instrumented_migration.migration_done.set() + return migration_result def _instrument_apply_update(*args): """Control migration progress.""" @@ -100,8 +103,12 @@ async def instrument_migration( with ( patch( - "homeassistant.components.recorder.migration.migrate_schema", - wraps=_instrument_migrate_schema, + "homeassistant.components.recorder.migration.migrate_schema_live", + wraps=partial(_instrument_migrate_schema, real_migrate_schema_live), + ), + patch( + "homeassistant.components.recorder.migration.migrate_schema_non_live", + wraps=partial(_instrument_migrate_schema, real_migrate_schema_non_live), ), patch( "homeassistant.components.recorder.migration._apply_update", diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 5715e994d2e..3cd4c3ab4b6 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -2569,7 +2569,13 @@ async def test_clean_shutdown_when_recorder_thread_raises_during_validate_db_sch assert instance.engine is None -async def test_clean_shutdown_when_schema_migration_fails(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("func_to_patch", "expected_setup_result"), + [("migrate_schema_non_live", False), ("migrate_schema_live", False)], +) +async def test_clean_shutdown_when_schema_migration_fails( + hass: HomeAssistant, func_to_patch: str, expected_setup_result: bool +) -> None: """Test we still shutdown cleanly when schema migration fails.""" with ( patch.object( @@ -2580,13 +2586,13 @@ async def test_clean_shutdown_when_schema_migration_fails(hass: HomeAssistant) - patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch.object( migration, - "migrate_schema", + func_to_patch, side_effect=Exception, ), ): if recorder.DOMAIN not in hass.data: recorder_helper.async_initialize_recorder(hass) - assert await async_setup_component( + setup_result = await async_setup_component( hass, recorder.DOMAIN, { @@ -2597,6 +2603,7 @@ async def test_clean_shutdown_when_schema_migration_fails(hass: HomeAssistant) - } }, ) + assert setup_result == expected_setup_result await hass.async_block_till_done() instance = recorder.get_instance(hass) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index f32f5c4aaaf..3bfbcad35fc 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -184,7 +184,7 @@ async def test_database_migration_encounters_corruption( side_effect=[False], ), patch( - "homeassistant.components.recorder.migration.migrate_schema", + "homeassistant.components.recorder.migration.migrate_schema_non_live", side_effect=sqlite3_exception, ), patch( @@ -201,13 +201,26 @@ async def test_database_migration_encounters_corruption( @pytest.mark.parametrize( - ("live_migration", "expected_setup_result"), [(True, True), (False, False)] + ( + "live_migration", + "func_to_patch", + "expected_setup_result", + "expected_pn_create", + "expected_pn_dismiss", + ), + [ + (True, "migrate_schema_live", True, 2, 1), + (False, "migrate_schema_non_live", False, 1, 0), + ], ) async def test_database_migration_encounters_corruption_not_sqlite( hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator, live_migration: bool, + func_to_patch: str, expected_setup_result: bool, + expected_pn_create: int, + expected_pn_dismiss: int, ) -> None: """Test we fail on database error when we cannot recover.""" assert recorder.util.async_migration_in_progress(hass) is False @@ -218,7 +231,7 @@ async def test_database_migration_encounters_corruption_not_sqlite( side_effect=[False], ), patch( - "homeassistant.components.recorder.migration.migrate_schema", + f"homeassistant.components.recorder.migration.{func_to_patch}", side_effect=DatabaseError("statement", {}, []), ), patch( @@ -248,8 +261,8 @@ async def test_database_migration_encounters_corruption_not_sqlite( assert recorder.util.async_migration_in_progress(hass) is False assert not move_away.called - assert len(mock_create.mock_calls) == 2 - assert len(mock_dismiss.mock_calls) == 1 + assert len(mock_create.mock_calls) == expected_pn_create + assert len(mock_dismiss.mock_calls) == expected_pn_dismiss async def test_events_during_migration_are_queued( diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 5f3b1b35c78..1bf56372620 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -2466,7 +2466,7 @@ async def test_recorder_info_bad_recorder_config( client = await hass_ws_client() - with patch("homeassistant.components.recorder.migration.migrate_schema"): + 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} From b14e8d160942b46c401279254f6992c9ca4140fb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Jul 2024 17:33:13 +0200 Subject: [PATCH 1458/2411] Remove SchemaValidationStatus.valid (#122394) --- homeassistant/components/recorder/core.py | 8 ++++---- homeassistant/components/recorder/migration.py | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index f77305277c8..50a49f1d4ce 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -743,7 +743,7 @@ class Recorder(threading.Thread): return self.schema_version = schema_status.current_version - if schema_status.valid: + if not schema_status.migration_needed and not schema_status.schema_errors: self._setup_run() else: self.migration_in_progress = True @@ -752,7 +752,7 @@ class Recorder(threading.Thread): self.hass.add_job(self.async_connection_success) # First do non-live migration steps, if needed - if not schema_status.valid: + if schema_status.migration_needed: result, schema_status = self._migrate_schema_offline(schema_status) if not result: self._notify_migration_failed() @@ -774,8 +774,8 @@ class Recorder(threading.Thread): # we restart before startup finishes return - # Do live migration steps, if needed - if not schema_status.valid: + # Do live migration steps and repairs, if needed + if schema_status.migration_needed or schema_status.schema_errors: result, schema_status = self._migrate_schema_live_and_setup_run( schema_status ) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 0af0788a42a..a5bb61e1fc5 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -193,9 +193,9 @@ class SchemaValidationStatus: """Store schema validation status.""" current_version: int + migration_needed: bool schema_errors: set[str] start_version: int - valid: bool def _schema_is_current(current_version: int) -> bool: @@ -223,10 +223,8 @@ def validate_db_schema( # columns may otherwise not exist etc. schema_errors = _find_schema_errors(hass, instance, session_maker) - valid = is_current and not schema_errors - return SchemaValidationStatus( - current_version, schema_errors, current_version, valid + current_version, not is_current, schema_errors, current_version ) From 02c34ba3f8fec29c8c5220ecca9b7f4243dc0505 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Mon, 22 Jul 2024 12:01:54 -0400 Subject: [PATCH 1459/2411] Bump aiorussound to 2.0.7 (#122389) --- homeassistant/components/russound_rio/manifest.json | 2 +- homeassistant/components/russound_rio/media_player.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 7dcdf228244..4c4f18325d5 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/russound_rio", "iot_class": "local_push", "loggers": ["aiorussound"], - "requirements": ["aiorussound==2.0.6"] + "requirements": ["aiorussound==2.0.7"] } diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index a96269ab906..14d330242e5 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -86,7 +86,7 @@ async def async_setup_entry( controllers = await russ.enumerate_controllers() entities = [] - for controller in controllers: + for controller in controllers.values(): sources = controller.sources for source in sources.values(): await source.watch() diff --git a/requirements_all.txt b/requirements_all.txt index d065a99d3f0..3ffe9b0abc2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.0.6 +aiorussound==2.0.7 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 164d9003ad7..8ebc242ac47 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.0.6 +aiorussound==2.0.7 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 From 76cd53a864cc598698c36f499fc251120630077d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Jul 2024 18:55:12 +0200 Subject: [PATCH 1460/2411] Improve error handling when recorder schema migration fails (#122397) --- homeassistant/components/recorder/core.py | 33 +++++++++++---- tests/components/recorder/test_migrate.py | 50 ++++++++++++++++++++++- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 50a49f1d4ce..6fab6a024ae 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -725,6 +725,10 @@ class Recorder(threading.Thread): "recorder_database_migration", ) + def _dismiss_migration_in_progress(self) -> None: + """Dismiss notification about migration in progress.""" + persistent_notification.dismiss(self.hass, "recorder_database_migration") + def _run(self) -> None: """Start processing events to save.""" thread_id = threading.get_ident() @@ -787,9 +791,16 @@ class Recorder(threading.Thread): # was True, we need to reinitialize the listener. self.hass.add_job(self.async_initialize) else: + self.migration_in_progress = False + self._dismiss_migration_in_progress() self._notify_migration_failed() return + # Schema migration and repair is now completed + if self.migration_in_progress: + self.migration_in_progress = False + self._dismiss_migration_in_progress() + # Catch up with missed statistics self._schedule_compile_missing_statistics() _LOGGER.debug("Recorder processing the queue") @@ -984,14 +995,10 @@ class Recorder(threading.Thread): "recorder_database_migration", ) self.hass.add_job(self._async_migration_started) - try: - migration_result, schema_status = self._migrate_schema(schema_status, True) - if migration_result: - self._setup_run() - return migration_result, schema_status - finally: - self.migration_in_progress = False - persistent_notification.dismiss(self.hass, "recorder_database_migration") + migration_result, schema_status = self._migrate_schema(schema_status, True) + if migration_result: + self._setup_run() + return migration_result, schema_status def _migrate_schema( self, @@ -1010,7 +1017,15 @@ class Recorder(threading.Thread): ) except exc.DatabaseError as err: if self._handle_database_error(err): - return (True, schema_status) + # If _handle_database_error returns True, we have a new database + # which does not need migration or repair. + new_schema_status = migration.SchemaValidationStatus( + current_version=SCHEMA_VERSION, + migration_needed=False, + schema_errors=set(), + start_version=SCHEMA_VERSION, + ) + return (True, new_schema_status) _LOGGER.exception("Database error during schema migration") return (False, schema_status) except Exception: diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 3bfbcad35fc..2b3980b2b67 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -160,7 +160,7 @@ async def test_database_migration_failed( @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) @pytest.mark.usefixtures("skip_by_db_engine") -async def test_database_migration_encounters_corruption( +async def test_live_database_migration_encounters_corruption( hass: HomeAssistant, recorder_db_url: str, async_setup_recorder_instance: RecorderInstanceGenerator, @@ -183,6 +183,51 @@ async def test_database_migration_encounters_corruption( "homeassistant.components.recorder.migration._schema_is_current", side_effect=[False], ), + patch( + "homeassistant.components.recorder.migration.migrate_schema_live", + side_effect=sqlite3_exception, + ), + patch( + "homeassistant.components.recorder.core.move_away_broken_database" + ) as move_away, + ): + await async_setup_recorder_instance(hass) + hass.states.async_set("my.entity", "on", {}) + hass.states.async_set("my.entity", "off", {}) + await async_wait_recording_done(hass) + + assert recorder.util.async_migration_in_progress(hass) is False + move_away.assert_called_once() + + +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.usefixtures("skip_by_db_engine") +async def test_non_live_database_migration_encounters_corruption( + hass: HomeAssistant, + recorder_db_url: str, + async_setup_recorder_instance: RecorderInstanceGenerator, +) -> None: + """Test we move away the database if its corrupt. + + This test is specific for SQLite, wiping the database on error only happens + with SQLite. + """ + + assert recorder.util.async_migration_in_progress(hass) is False + + sqlite3_exception = DatabaseError("statement", {}, []) + sqlite3_exception.__cause__ = sqlite3.DatabaseError( + "database disk image is malformed" + ) + + with ( + patch( + "homeassistant.components.recorder.migration._schema_is_current", + side_effect=[False], + ), + patch( + "homeassistant.components.recorder.migration.migrate_schema_live", + ) as migrate_schema_live, patch( "homeassistant.components.recorder.migration.migrate_schema_non_live", side_effect=sqlite3_exception, @@ -197,7 +242,8 @@ async def test_database_migration_encounters_corruption( await async_wait_recording_done(hass) assert recorder.util.async_migration_in_progress(hass) is False - assert move_away.called + move_away.assert_called_once() + migrate_schema_live.assert_not_called() @pytest.mark.parametrize( From 19d9a91392c202e8d22d523a62e4935e408de9b6 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Mon, 22 Jul 2024 13:06:13 -0400 Subject: [PATCH 1461/2411] Add device info to Russound RIO (#122395) * Add device info to Russound RIO * Set device info name to Russound model * Add device class to Russound media player * Move device info to constructor * Use connections instead of identifiers for russound * Add via_device attr to Russound * Reinstate russound identifiers * Move has entity name attr --- .../components/russound_rio/media_player.py | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 14d330242e5..faec7ceff99 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -7,6 +7,7 @@ import logging from aiorussound import Source, Zone from homeassistant.components.media_player import ( + MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -16,6 +17,7 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -108,8 +110,10 @@ async def async_setup_entry( class RussoundZoneDevice(MediaPlayerEntity): """Representation of a Russound Zone.""" + _attr_device_class = MediaPlayerDeviceClass.SPEAKER _attr_media_content_type = MediaType.MUSIC _attr_should_poll = False + _attr_has_entity_name = True _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.VOLUME_SET @@ -120,9 +124,25 @@ class RussoundZoneDevice(MediaPlayerEntity): def __init__(self, zone: Zone, sources: dict[int, Source]) -> None: """Initialize the zone device.""" - super().__init__() + self._controller = zone.controller self._zone = zone self._sources = sources + self._attr_name = zone.name + self._attr_unique_id = f"{self._controller.mac_address}-{zone.device_str()}" + self._attr_device_info = DeviceInfo( + # Use MAC address of Russound device as identifier + identifiers={(DOMAIN, self._controller.mac_address)}, + connections={(CONNECTION_NETWORK_MAC, self._controller.mac_address)}, + manufacturer="Russound", + name=self._controller.controller_type, + model=self._controller.controller_type, + sw_version=self._controller.firmware_version, + ) + if self._controller.parent_controller: + self._attr_device_info["via_device"] = ( + DOMAIN, + self._controller.parent_controller.mac_address, + ) def _callback_handler(self, device_str, *args): if ( @@ -138,11 +158,6 @@ class RussoundZoneDevice(MediaPlayerEntity): def _current_source(self) -> Source: return self._zone.fetch_current_source() - @property - def name(self): - """Return the name of the zone.""" - return self._zone.name - @property def state(self) -> MediaPlayerState | None: """Return the state of the device.""" From 4c853803f1e7e1d87a0809702988c9777ed5b777 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 22 Jul 2024 19:15:23 +0200 Subject: [PATCH 1462/2411] Add created_at/modified_at to device registry (#122369) --- homeassistant/helpers/device_registry.py | 30 ++++++++ .../components/config/test_device_registry.py | 34 +++++++++ .../enphase_envoy/test_diagnostics.py | 2 + .../homekit_controller/test_init.py | 2 + tests/helpers/test_device_registry.py | 69 +++++++++++++++++++ tests/syrupy.py | 11 ++- 6 files changed, 147 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index e3f8df136c5..b8fb3ae0219 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections import defaultdict from collections.abc import Mapping +from datetime import datetime from enum import StrEnum from functools import cached_property, lru_cache, partial import logging @@ -23,6 +24,7 @@ from homeassistant.core import ( ) from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import async_suggest_report_issue +from homeassistant.util.dt import utc_from_timestamp, utcnow from homeassistant.util.event_type import EventType from homeassistant.util.hass_dict import HassKey from homeassistant.util.json import format_unserializable_data @@ -94,6 +96,7 @@ class DeviceInfo(TypedDict, total=False): configuration_url: str | URL | None connections: set[tuple[str, str]] + created_at: str default_manufacturer: str default_model: str default_name: str @@ -102,6 +105,7 @@ class DeviceInfo(TypedDict, total=False): manufacturer: str | None model: str | None model_id: str | None + modified_at: str name: str | None serial_number: str | None suggested_area: str | None @@ -281,6 +285,7 @@ class DeviceEntry: config_entries: set[str] = attr.ib(converter=set, factory=set) configuration_url: str | None = attr.ib(default=None) connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) + created_at: datetime = attr.ib(factory=utcnow) disabled_by: DeviceEntryDisabler | None = attr.ib(default=None) entry_type: DeviceEntryType | None = attr.ib(default=None) hw_version: str | None = attr.ib(default=None) @@ -290,6 +295,7 @@ class DeviceEntry: manufacturer: str | None = attr.ib(default=None) model: str | None = attr.ib(default=None) model_id: str | None = attr.ib(default=None) + modified_at: datetime = attr.ib(factory=utcnow) name_by_user: str | None = attr.ib(default=None) name: str | None = attr.ib(default=None) primary_config_entry: str | None = attr.ib(default=None) @@ -316,6 +322,7 @@ class DeviceEntry: "configuration_url": self.configuration_url, "config_entries": list(self.config_entries), "connections": list(self.connections), + "created_at": self.created_at.timestamp(), "disabled_by": self.disabled_by, "entry_type": self.entry_type, "hw_version": self.hw_version, @@ -325,6 +332,7 @@ class DeviceEntry: "manufacturer": self.manufacturer, "model": self.model, "model_id": self.model_id, + "modified_at": self.modified_at.timestamp(), "name_by_user": self.name_by_user, "name": self.name, "primary_config_entry": self.primary_config_entry, @@ -359,6 +367,7 @@ class DeviceEntry: "config_entries": list(self.config_entries), "configuration_url": self.configuration_url, "connections": list(self.connections), + "created_at": self.created_at.isoformat(), "disabled_by": self.disabled_by, "entry_type": self.entry_type, "hw_version": self.hw_version, @@ -368,6 +377,7 @@ class DeviceEntry: "manufacturer": self.manufacturer, "model": self.model, "model_id": self.model_id, + "modified_at": self.modified_at.isoformat(), "name_by_user": self.name_by_user, "name": self.name, "primary_config_entry": self.primary_config_entry, @@ -388,6 +398,8 @@ class DeletedDeviceEntry: identifiers: set[tuple[str, str]] = attr.ib() id: str = attr.ib() orphaned_timestamp: float | None = attr.ib() + created_at: datetime = attr.ib(factory=utcnow) + modified_at: datetime = attr.ib(factory=utcnow) def to_device_entry( self, @@ -400,6 +412,7 @@ class DeletedDeviceEntry: # type ignores: likely https://github.com/python/mypy/issues/8625 config_entries={config_entry_id}, # type: ignore[arg-type] connections=self.connections & connections, # type: ignore[arg-type] + created_at=self.created_at, identifiers=self.identifiers & identifiers, # type: ignore[arg-type] id=self.id, is_new=True, @@ -413,9 +426,11 @@ class DeletedDeviceEntry: { "config_entries": list(self.config_entries), "connections": list(self.connections), + "created_at": self.created_at.isoformat(), "identifiers": list(self.identifiers), "id": self.id, "orphaned_timestamp": self.orphaned_timestamp, + "modified_at": self.modified_at.isoformat(), } ) ) @@ -490,8 +505,12 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): device.setdefault("primary_config_entry", None) if old_minor_version < 7: # Introduced in 2024.8 + created_at = utc_from_timestamp(0).isoformat() for device in old_data["devices"]: device.setdefault("model_id", None) + device["created_at"] = device["modified_at"] = created_at + for device in old_data["deleted_devices"]: + device["created_at"] = device["modified_at"] = created_at if old_major_version > 1: raise NotImplementedError @@ -688,6 +707,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): config_entry_id: str, configuration_url: str | URL | None | UndefinedType = UNDEFINED, connections: set[tuple[str, str]] | None | UndefinedType = UNDEFINED, + created_at: str | datetime | UndefinedType = UNDEFINED, # will be ignored default_manufacturer: str | None | UndefinedType = UNDEFINED, default_model: str | None | UndefinedType = UNDEFINED, default_name: str | None | UndefinedType = UNDEFINED, @@ -699,6 +719,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): manufacturer: str | None | UndefinedType = UNDEFINED, model: str | None | UndefinedType = UNDEFINED, model_id: str | None | UndefinedType = UNDEFINED, + modified_at: str | datetime | UndefinedType = UNDEFINED, # will be ignored name: str | None | UndefinedType = UNDEFINED, serial_number: str | None | UndefinedType = UNDEFINED, suggested_area: str | None | UndefinedType = UNDEFINED, @@ -1035,6 +1056,10 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): if not new_values: return old + if not RUNTIME_ONLY_ATTRS.issuperset(new_values): + # Change modified_at if we are changing something that we store + new_values["modified_at"] = utcnow() + self.hass.verify_event_loop_thread("device_registry.async_update_device") new = attr.evolve(old, **new_values) self.devices[device_id] = new @@ -1114,6 +1139,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): self.deleted_devices[device_id] = DeletedDeviceEntry( config_entries=device.config_entries, connections=device.connections, + created_at=device.created_at, identifiers=device.identifiers, id=device.id, orphaned_timestamp=None, @@ -1149,6 +1175,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): tuple(conn) # type: ignore[misc] for conn in device["connections"] }, + created_at=datetime.fromisoformat(device["created_at"]), disabled_by=( DeviceEntryDisabler(device["disabled_by"]) if device["disabled_by"] @@ -1169,6 +1196,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): manufacturer=device["manufacturer"], model=device["model"], model_id=device["model_id"], + modified_at=datetime.fromisoformat(device["modified_at"]), name_by_user=device["name_by_user"], name=device["name"], primary_config_entry=device["primary_config_entry"], @@ -1181,8 +1209,10 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): deleted_devices[device["id"]] = DeletedDeviceEntry( config_entries=set(device["config_entries"]), connections={tuple(conn) for conn in device["connections"]}, + created_at=datetime.fromisoformat(device["created_at"]), identifiers={tuple(iden) for iden in device["identifiers"]}, id=device["id"], + modified_at=datetime.fromisoformat(device["modified_at"]), orphaned_timestamp=device["orphaned_timestamp"], ) diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 6e82cc8ee25..aab898f5fd6 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -1,5 +1,8 @@ """Test device_registry API.""" +from datetime import datetime + +from freezegun.api import FrozenDateTimeFactory import pytest from pytest_unordered import unordered @@ -7,6 +10,7 @@ from homeassistant.components.config import device_registry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, MockModule, mock_integration from tests.typing import MockHAClientWebSocket, WebSocketGenerator @@ -26,6 +30,7 @@ async def client_fixture( return await hass_ws_client(hass) +@pytest.mark.usefixtures("freezer") async def test_list_devices( hass: HomeAssistant, client: MockHAClientWebSocket, @@ -61,6 +66,7 @@ async def test_list_devices( "config_entries": [entry.entry_id], "configuration_url": None, "connections": [["ethernet", "12:34:56:78:90:AB:CD:EF"]], + "created_at": utcnow().timestamp(), "disabled_by": None, "entry_type": None, "hw_version": None, @@ -69,6 +75,7 @@ async def test_list_devices( "manufacturer": "manufacturer", "model": "model", "model_id": None, + "modified_at": utcnow().timestamp(), "name_by_user": None, "name": None, "primary_config_entry": entry.entry_id, @@ -81,6 +88,7 @@ async def test_list_devices( "config_entries": [entry.entry_id], "configuration_url": None, "connections": [], + "created_at": utcnow().timestamp(), "disabled_by": None, "entry_type": dr.DeviceEntryType.SERVICE, "hw_version": None, @@ -89,6 +97,7 @@ async def test_list_devices( "manufacturer": "manufacturer", "model": "model", "model_id": None, + "modified_at": utcnow().timestamp(), "name_by_user": None, "name": None, "primary_config_entry": entry.entry_id, @@ -113,6 +122,7 @@ async def test_list_devices( "config_entries": [entry.entry_id], "configuration_url": None, "connections": [["ethernet", "12:34:56:78:90:AB:CD:EF"]], + "created_at": utcnow().timestamp(), "disabled_by": None, "entry_type": None, "hw_version": None, @@ -122,6 +132,7 @@ async def test_list_devices( "manufacturer": "manufacturer", "model": "model", "model_id": None, + "modified_at": utcnow().timestamp(), "name_by_user": None, "name": None, "primary_config_entry": entry.entry_id, @@ -151,12 +162,15 @@ async def test_update_device( hass: HomeAssistant, client: MockHAClientWebSocket, device_registry: dr.DeviceRegistry, + freezer: FrozenDateTimeFactory, payload_key: str, payload_value: str | dr.DeviceEntryDisabler | None, ) -> None: """Test update entry.""" entry = MockConfigEntry(title=None) entry.add_to_hass(hass) + created_at = datetime.fromisoformat("2024-07-16T13:30:00.900075+00:00") + freezer.move_to(created_at) device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, @@ -167,6 +181,9 @@ async def test_update_device( assert not getattr(device, payload_key) + modified_at = datetime.fromisoformat("2024-07-16T13:45:00.900075+00:00") + freezer.move_to(modified_at) + await client.send_json_auto_id( { "type": "config/device_registry/update", @@ -186,6 +203,12 @@ async def test_update_device( assert msg["result"][payload_key] == payload_value assert getattr(device, payload_key) == payload_value + for key, value in ( + ("created_at", created_at), + ("modified_at", modified_at if payload_value is not None else created_at), + ): + assert msg["result"][key] == value.timestamp() + assert getattr(device, key) == value assert isinstance(device.disabled_by, (dr.DeviceEntryDisabler, type(None))) @@ -194,10 +217,13 @@ async def test_update_device_labels( hass: HomeAssistant, client: MockHAClientWebSocket, device_registry: dr.DeviceRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test update entry labels.""" entry = MockConfigEntry(title=None) entry.add_to_hass(hass) + created_at = datetime.fromisoformat("2024-07-16T13:30:00.900075+00:00") + freezer.move_to(created_at) device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, @@ -207,6 +233,8 @@ async def test_update_device_labels( ) assert not device.labels + modified_at = datetime.fromisoformat("2024-07-16T13:45:00.900075+00:00") + freezer.move_to(modified_at) await client.send_json_auto_id( { @@ -227,6 +255,12 @@ async def test_update_device_labels( assert msg["result"]["labels"] == unordered(["label1", "label2"]) assert device.labels == {"label1", "label2"} + for key, value in ( + ("created_at", created_at), + ("modified_at", modified_at), + ): + assert msg["result"][key] == value.timestamp() + assert getattr(device, key) == value async def test_remove_config_entry_from_device( diff --git a/tests/components/enphase_envoy/test_diagnostics.py b/tests/components/enphase_envoy/test_diagnostics.py index c6494f7743f..186ee5c46f3 100644 --- a/tests/components/enphase_envoy/test_diagnostics.py +++ b/tests/components/enphase_envoy/test_diagnostics.py @@ -26,6 +26,8 @@ TO_EXCLUDE = { "last_updated", "last_changed", "last_reported", + "created_at", + "modified_at", } diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index f3952298326..02e57734b3a 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -293,6 +293,8 @@ async def test_snapshots( device_dict = asdict(device) device_dict.pop("id", None) device_dict.pop("via_device_id", None) + device_dict.pop("created_at", None) + device_dict.pop("modified_at", None) devices.append({"device": device_dict, "entities": entities}) assert snapshot == devices diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 569da219ef1..ffbc78ac463 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -2,11 +2,13 @@ from collections.abc import Iterable from contextlib import AbstractContextManager, nullcontext +from datetime import datetime from functools import partial import time from typing import Any from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from yarl import URL @@ -19,6 +21,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) +from homeassistant.util.dt import utcnow from tests.common import ( MockConfigEntry, @@ -177,12 +180,15 @@ async def test_multiple_config_entries( @pytest.mark.parametrize("load_registries", [False]) +@pytest.mark.usefixtures("freezer") async def test_loading_from_storage( hass: HomeAssistant, hass_storage: dict[str, Any], mock_config_entry: MockConfigEntry, ) -> None: """Test loading stored devices on start.""" + created_at = "2024-01-01T00:00:00+00:00" + modified_at = "2024-02-01T00:00:00+00:00" hass_storage[dr.STORAGE_KEY] = { "version": dr.STORAGE_VERSION_MAJOR, "minor_version": dr.STORAGE_VERSION_MINOR, @@ -193,6 +199,7 @@ async def test_loading_from_storage( "config_entries": [mock_config_entry.entry_id], "configuration_url": "https://example.com/config", "connections": [["Zigbee", "01.23.45.67.89"]], + "created_at": created_at, "disabled_by": dr.DeviceEntryDisabler.USER, "entry_type": dr.DeviceEntryType.SERVICE, "hw_version": "hw_version", @@ -202,6 +209,7 @@ async def test_loading_from_storage( "manufacturer": "manufacturer", "model": "model", "model_id": "model_id", + "modified_at": modified_at, "name_by_user": "Test Friendly Name", "name": "name", "primary_config_entry": mock_config_entry.entry_id, @@ -214,8 +222,10 @@ async def test_loading_from_storage( { "config_entries": [mock_config_entry.entry_id], "connections": [["Zigbee", "23.45.67.89.01"]], + "created_at": created_at, "id": "bcdefghijklmn", "identifiers": [["serial", "3456ABCDEF12"]], + "modified_at": modified_at, "orphaned_timestamp": None, } ], @@ -227,6 +237,16 @@ async def test_loading_from_storage( assert len(registry.devices) == 1 assert len(registry.deleted_devices) == 1 + assert registry.deleted_devices["bcdefghijklmn"] == dr.DeletedDeviceEntry( + config_entries={mock_config_entry.entry_id}, + connections={("Zigbee", "23.45.67.89.01")}, + created_at=datetime.fromisoformat(created_at), + id="bcdefghijklmn", + identifiers={("serial", "3456ABCDEF12")}, + modified_at=datetime.fromisoformat(modified_at), + orphaned_timestamp=None, + ) + entry = registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, connections={("Zigbee", "01.23.45.67.89")}, @@ -239,6 +259,7 @@ async def test_loading_from_storage( config_entries={mock_config_entry.entry_id}, configuration_url="https://example.com/config", connections={("Zigbee", "01.23.45.67.89")}, + created_at=datetime.fromisoformat(created_at), disabled_by=dr.DeviceEntryDisabler.USER, entry_type=dr.DeviceEntryType.SERVICE, hw_version="hw_version", @@ -248,6 +269,7 @@ async def test_loading_from_storage( manufacturer="manufacturer", model="model", model_id="model_id", + modified_at=datetime.fromisoformat(modified_at), name_by_user="Test Friendly Name", name="name", primary_config_entry=mock_config_entry.entry_id, @@ -270,10 +292,12 @@ async def test_loading_from_storage( assert entry == dr.DeviceEntry( config_entries={mock_config_entry.entry_id}, connections={("Zigbee", "23.45.67.89.01")}, + created_at=datetime.fromisoformat(created_at), id="bcdefghijklmn", identifiers={("serial", "3456ABCDEF12")}, manufacturer="manufacturer", model="model", + modified_at=utcnow(), primary_config_entry=mock_config_entry.entry_id, ) assert entry.id == "bcdefghijklmn" @@ -283,6 +307,7 @@ async def test_loading_from_storage( @pytest.mark.parametrize("load_registries", [False]) +@pytest.mark.usefixtures("freezer") async def test_migration_1_1_to_1_7( hass: HomeAssistant, hass_storage: dict[str, Any], @@ -367,6 +392,7 @@ async def test_migration_1_1_to_1_7( "config_entries": [mock_config_entry.entry_id], "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], + "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, "entry_type": "service", "hw_version": None, @@ -376,6 +402,7 @@ async def test_migration_1_1_to_1_7( "manufacturer": "manufacturer", "model": "model", "model_id": None, + "modified_at": utcnow().isoformat(), "name": "name", "name_by_user": None, "primary_config_entry": mock_config_entry.entry_id, @@ -388,6 +415,7 @@ async def test_migration_1_1_to_1_7( "config_entries": [None], "configuration_url": None, "connections": [], + "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, "entry_type": None, "hw_version": None, @@ -397,6 +425,7 @@ async def test_migration_1_1_to_1_7( "manufacturer": None, "model": None, "model_id": None, + "modified_at": "1970-01-01T00:00:00+00:00", "name_by_user": None, "name": None, "primary_config_entry": None, @@ -409,8 +438,10 @@ async def test_migration_1_1_to_1_7( { "config_entries": ["123456"], "connections": [], + "created_at": "1970-01-01T00:00:00+00:00", "id": "deletedid", "identifiers": [["serial", "123456ABCDFF"]], + "modified_at": "1970-01-01T00:00:00+00:00", "orphaned_timestamp": None, } ], @@ -419,6 +450,7 @@ async def test_migration_1_1_to_1_7( @pytest.mark.parametrize("load_registries", [False]) +@pytest.mark.usefixtures("freezer") async def test_migration_1_2_to_1_7( hass: HomeAssistant, hass_storage: dict[str, Any], @@ -442,6 +474,7 @@ async def test_migration_1_2_to_1_7( "identifiers": [["serial", "123456ABCDEF"]], "manufacturer": "manufacturer", "model": "model", + "modified_at": utcnow().isoformat(), "name": "name", "name_by_user": None, "sw_version": "version", @@ -458,6 +491,7 @@ async def test_migration_1_2_to_1_7( "identifiers": [["serial", "mock-id-invalid-entry"]], "manufacturer": None, "model": None, + "modified_at": "1970-01-01T00:00:00+00:00", "name_by_user": None, "name": None, "sw_version": None, @@ -502,6 +536,7 @@ async def test_migration_1_2_to_1_7( "config_entries": [mock_config_entry.entry_id], "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], + "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, "entry_type": "service", "hw_version": None, @@ -511,6 +546,7 @@ async def test_migration_1_2_to_1_7( "manufacturer": "manufacturer", "model": "model", "model_id": None, + "modified_at": utcnow().isoformat(), "name": "name", "name_by_user": None, "primary_config_entry": mock_config_entry.entry_id, @@ -523,6 +559,7 @@ async def test_migration_1_2_to_1_7( "config_entries": [None], "configuration_url": None, "connections": [], + "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, "entry_type": None, "hw_version": None, @@ -532,6 +569,7 @@ async def test_migration_1_2_to_1_7( "manufacturer": None, "model": None, "model_id": None, + "modified_at": "1970-01-01T00:00:00+00:00", "name_by_user": None, "name": None, "primary_config_entry": None, @@ -546,6 +584,7 @@ async def test_migration_1_2_to_1_7( @pytest.mark.parametrize("load_registries", [False]) +@pytest.mark.usefixtures("freezer") async def test_migration_1_3_to_1_7( hass: HomeAssistant, hass_storage: dict[str, Any], @@ -631,6 +670,7 @@ async def test_migration_1_3_to_1_7( "config_entries": [mock_config_entry.entry_id], "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], + "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, "entry_type": "service", "hw_version": "hw_version", @@ -640,6 +680,7 @@ async def test_migration_1_3_to_1_7( "manufacturer": "manufacturer", "model": "model", "model_id": None, + "modified_at": utcnow().isoformat(), "name": "name", "name_by_user": None, "primary_config_entry": mock_config_entry.entry_id, @@ -652,6 +693,7 @@ async def test_migration_1_3_to_1_7( "config_entries": [None], "configuration_url": None, "connections": [], + "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, "entry_type": None, "hw_version": None, @@ -661,6 +703,7 @@ async def test_migration_1_3_to_1_7( "manufacturer": None, "model": None, "model_id": None, + "modified_at": "1970-01-01T00:00:00+00:00", "name": None, "name_by_user": None, "primary_config_entry": None, @@ -675,6 +718,7 @@ async def test_migration_1_3_to_1_7( @pytest.mark.parametrize("load_registries", [False]) +@pytest.mark.usefixtures("freezer") async def test_migration_1_4_to_1_7( hass: HomeAssistant, hass_storage: dict[str, Any], @@ -762,6 +806,7 @@ async def test_migration_1_4_to_1_7( "config_entries": [mock_config_entry.entry_id], "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], + "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, "entry_type": "service", "hw_version": "hw_version", @@ -771,6 +816,7 @@ async def test_migration_1_4_to_1_7( "manufacturer": "manufacturer", "model": "model", "model_id": None, + "modified_at": utcnow().isoformat(), "name": "name", "name_by_user": None, "primary_config_entry": mock_config_entry.entry_id, @@ -783,6 +829,7 @@ async def test_migration_1_4_to_1_7( "config_entries": [None], "configuration_url": None, "connections": [], + "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, "entry_type": None, "hw_version": None, @@ -792,6 +839,7 @@ async def test_migration_1_4_to_1_7( "manufacturer": None, "model": None, "model_id": None, + "modified_at": "1970-01-01T00:00:00+00:00", "name_by_user": None, "name": None, "primary_config_entry": None, @@ -806,6 +854,7 @@ async def test_migration_1_4_to_1_7( @pytest.mark.parametrize("load_registries", [False]) +@pytest.mark.usefixtures("freezer") async def test_migration_1_5_to_1_7( hass: HomeAssistant, hass_storage: dict[str, Any], @@ -895,6 +944,7 @@ async def test_migration_1_5_to_1_7( "config_entries": [mock_config_entry.entry_id], "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], + "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, "entry_type": "service", "hw_version": "hw_version", @@ -905,6 +955,7 @@ async def test_migration_1_5_to_1_7( "model": "model", "name": "name", "model_id": None, + "modified_at": utcnow().isoformat(), "name_by_user": None, "primary_config_entry": mock_config_entry.entry_id, "serial_number": None, @@ -916,6 +967,7 @@ async def test_migration_1_5_to_1_7( "config_entries": [None], "configuration_url": None, "connections": [], + "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, "entry_type": None, "hw_version": None, @@ -925,6 +977,7 @@ async def test_migration_1_5_to_1_7( "manufacturer": None, "model": None, "model_id": None, + "modified_at": "1970-01-01T00:00:00+00:00", "name_by_user": None, "name": None, "primary_config_entry": None, @@ -939,6 +992,7 @@ async def test_migration_1_5_to_1_7( @pytest.mark.parametrize("load_registries", [False]) +@pytest.mark.usefixtures("freezer") async def test_migration_1_6_to_1_7( hass: HomeAssistant, hass_storage: dict[str, Any], @@ -1030,6 +1084,7 @@ async def test_migration_1_6_to_1_7( "config_entries": [mock_config_entry.entry_id], "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], + "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, "entry_type": "service", "hw_version": "hw_version", @@ -1040,6 +1095,7 @@ async def test_migration_1_6_to_1_7( "model": "model", "name": "name", "model_id": None, + "modified_at": "1970-01-01T00:00:00+00:00", "name_by_user": None, "primary_config_entry": mock_config_entry.entry_id, "serial_number": None, @@ -1051,6 +1107,7 @@ async def test_migration_1_6_to_1_7( "config_entries": [None], "configuration_url": None, "connections": [], + "created_at": "1970-01-01T00:00:00+00:00", "disabled_by": None, "entry_type": None, "hw_version": None, @@ -1060,6 +1117,7 @@ async def test_migration_1_6_to_1_7( "manufacturer": None, "model": None, "model_id": None, + "modified_at": "1970-01-01T00:00:00+00:00", "name_by_user": None, "name": None, "primary_config_entry": None, @@ -1546,8 +1604,11 @@ async def test_update( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, ) -> None: """Verify that we can update some attributes of a device.""" + created_at = datetime.fromisoformat("2024-01-01T01:00:00+00:00") + freezer.move_to(created_at) update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED) entry = device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, @@ -1559,7 +1620,11 @@ async def test_update( assert not entry.area_id assert not entry.labels assert not entry.name_by_user + assert entry.created_at == created_at + assert entry.modified_at == created_at + modified_at = datetime.fromisoformat("2024-02-01T01:00:00+00:00") + freezer.move_to(modified_at) with patch.object(device_registry, "async_schedule_save") as mock_save: updated_entry = device_registry.async_update_device( entry.id, @@ -1589,6 +1654,7 @@ async def test_update( config_entries={mock_config_entry.entry_id}, configuration_url="https://example.com/config", connections={("mac", "65:43:21:fe:dc:ba")}, + created_at=created_at, disabled_by=dr.DeviceEntryDisabler.USER, entry_type=dr.DeviceEntryType.SERVICE, hw_version="hw_version", @@ -1598,6 +1664,7 @@ async def test_update( manufacturer="Test Producer", model="Test Model", model_id="Test Model Name", + modified_at=modified_at, name_by_user="Test Friendly Name", name="name", serial_number="serial_no", @@ -2616,6 +2683,7 @@ async def test_loading_invalid_configuration_url_from_storage( "config_entries": ["1234"], "configuration_url": "invalid", "connections": [], + "created_at": "2024-01-01T00:00:00+00:00", "disabled_by": None, "entry_type": dr.DeviceEntryType.SERVICE, "hw_version": None, @@ -2625,6 +2693,7 @@ async def test_loading_invalid_configuration_url_from_storage( "manufacturer": None, "model": None, "model_id": None, + "modified_at": "2024-02-01T00:00:00+00:00", "name_by_user": None, "name": None, "primary_config_entry": "1234", diff --git a/tests/syrupy.py b/tests/syrupy.py index 9dc8e50e5f1..80d955f0de1 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -155,7 +155,16 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): serialized["via_device_id"] = ANY if serialized["primary_config_entry"] is not None: serialized["primary_config_entry"] = ANY - return serialized + return cls._remove_created_and_modified_at(serialized) + + @classmethod + def _remove_created_and_modified_at( + cls, data: SerializableData + ) -> SerializableData: + """Remove created_at and modified_at from the data.""" + data.pop("created_at", None) + data.pop("modified_at", None) + return data @classmethod def _serializable_entity_registry_entry( From 20fc5233a1b61f6200f397ce8256020aaf617c93 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Jul 2024 20:04:01 +0200 Subject: [PATCH 1463/2411] Add recorder data migrator class to clean up states table (#122069) --- homeassistant/components/recorder/core.py | 49 +--------- .../components/recorder/migration.py | 93 ++++++++++++++++--- homeassistant/components/recorder/tasks.py | 14 --- .../components/recorder/test_v32_migration.py | 25 +++-- tests/conftest.py | 6 +- 5 files changed, 100 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 6fab6a024ae..67c823259ea 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -16,14 +16,7 @@ import time from typing import TYPE_CHECKING, Any, cast import psutil_home_assistant as ha_psutil -from sqlalchemy import ( - create_engine, - event as sqlalchemy_event, - exc, - inspect, - select, - update, -) +from sqlalchemy import create_engine, event as sqlalchemy_event, exc, select, update from sqlalchemy.engine import Engine from sqlalchemy.engine.interfaces import DBAPIConnection from sqlalchemy.exc import SQLAlchemyError @@ -62,7 +55,6 @@ from .const import ( DOMAIN, KEEPALIVE_TIME, LAST_REPORTED_SCHEMA_VERSION, - LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION, MARIADB_PYMYSQL_URL_PREFIX, MARIADB_URL_PREFIX, MAX_QUEUE_BACKLOG_MIN_VALUE, @@ -75,9 +67,7 @@ from .const import ( SupportedDialect, ) from .db_schema import ( - LEGACY_STATES_EVENT_ID_INDEX, SCHEMA_VERSION, - TABLE_STATES, Base, EventData, Events, @@ -91,6 +81,7 @@ from .db_schema import ( from .executor import DBInterruptibleThreadPoolExecutor from .migration import ( EntityIDMigration, + EventIDPostMigration, EventsContextIDMigration, EventTypeIDMigration, StatesContextIDMigration, @@ -113,7 +104,6 @@ from .tasks import ( CommitTask, CompileMissingStatisticsTask, DatabaseLockTask, - EventIdMigrationTask, ImportStatisticsTask, KeepAliveTask, PerodicCleanupTask, @@ -132,7 +122,6 @@ from .util import ( dburl_to_path, end_incomplete_runs, execute_stmt_lambda_element, - get_index_by_name, is_second_sunday, move_away_broken_database, session_scope, @@ -831,24 +820,11 @@ class Recorder(threading.Thread): EventsContextIDMigration, EventTypeIDMigration, EntityIDMigration, + EventIDPostMigration, ): migrator = migrator_cls(schema_status.start_version, migration_changes) migrator.do_migrate(self, session) - if self.schema_version > LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION: - with contextlib.suppress(SQLAlchemyError): - # If the index of event_ids on the states table is still present - # or the event_id foreign key still exists we need to queue a - # task to remove it. - if ( - get_index_by_name( - session, TABLE_STATES, LEGACY_STATES_EVENT_ID_INDEX - ) - or self._legacy_event_id_foreign_key_exists() - ): - self.queue_task(EventIdMigrationTask()) - self.use_legacy_events_index = True - # We must only set the db ready after we have set the table managers # to active if there is no data to migrate. # @@ -1327,29 +1303,10 @@ class Recorder(threading.Thread): """Run post schema migration tasks.""" migration.post_schema_migration(self, old_version, new_version) - def _legacy_event_id_foreign_key_exists(self) -> bool: - """Check if the legacy event_id foreign key exists.""" - engine = self.engine - assert engine is not None - return bool( - next( - ( - fk - for fk in inspect(engine).get_foreign_keys(TABLE_STATES) - if fk["constrained_columns"] == ["event_id"] - ), - None, - ) - ) - def _post_migrate_entity_ids(self) -> bool: """Post migrate entity_ids if needed.""" return migration.post_migrate_entity_ids(self) - def _cleanup_legacy_states_event_ids(self) -> bool: - """Cleanup legacy event_ids if needed.""" - return migration.cleanup_legacy_states_event_ids(self) - def _send_keep_alive(self) -> None: """Send a keep alive to keep the db connection open.""" assert self.event_session is not None diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index a5bb61e1fc5..138d4530bb9 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -52,6 +52,7 @@ from .auto_repairs.statistics.schema import ( from .const import ( CONTEXT_ID_AS_BINARY_SCHEMA_VERSION, EVENT_TYPE_IDS_SCHEMA_VERSION, + LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION, STATES_META_SCHEMA_VERSION, SupportedDialect, ) @@ -1949,6 +1950,7 @@ def cleanup_legacy_states_event_ids(instance: Recorder) -> bool: ) _drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX) instance.use_legacy_events_index = False + _mark_migration_done(session, EventIDPostMigration) return True @@ -2018,6 +2020,14 @@ class CommitBeforeMigrationTask(MigrationTask): commit_before = True +@dataclass(frozen=True, kw_only=True) +class NeedsMigrateResult: + """Container for the return value of BaseRunTimeMigration.needs_migrate_impl.""" + + needs_migrate: bool + migration_done: bool + + class BaseRunTimeMigration(ABC): """Base class for run time migrations.""" @@ -2033,7 +2043,7 @@ class BaseRunTimeMigration(ABC): def do_migrate(self, instance: Recorder, session: Session) -> None: """Start migration if needed.""" - if self.needs_migrate(session): + if self.needs_migrate(instance, session): instance.queue_task(self.task(self)) else: self.migration_done(instance) @@ -2047,10 +2057,12 @@ class BaseRunTimeMigration(ABC): """Will be called after migrate returns True or if migration is not needed.""" @abstractmethod - def needs_migrate_query(self) -> StatementLambdaElement: - """Return the query to check if the migration needs to run.""" + def needs_migrate_impl( + self, instance: Recorder, session: Session + ) -> NeedsMigrateResult: + """Return if the migration needs to run and if it is done.""" - def needs_migrate(self, session: Session) -> bool: + def needs_migrate(self, instance: Recorder, session: Session) -> bool: """Return if the migration needs to run. If the migration needs to run, it will return True. @@ -2068,13 +2080,30 @@ class BaseRunTimeMigration(ABC): # We do not know if the migration is done from the # migration changes table so we must check the data # This is the slow path - if not execute_stmt_lambda_element(session, self.needs_migrate_query()): + needs_migrate = self.needs_migrate_impl(instance, session) + if needs_migrate.migration_done: _mark_migration_done(session, self.__class__) - return False - return True + return needs_migrate.needs_migrate -class StatesContextIDMigration(BaseRunTimeMigration): +class BaseRunTimeMigrationWithQuery(BaseRunTimeMigration): + """Base class for run time migrations.""" + + @abstractmethod + def needs_migrate_query(self) -> StatementLambdaElement: + """Return the query to check if the migration needs to run.""" + + def needs_migrate_impl( + self, instance: Recorder, session: Session + ) -> NeedsMigrateResult: + """Return if the migration needs to run.""" + needs_migrate = execute_stmt_lambda_element(session, self.needs_migrate_query()) + return NeedsMigrateResult( + needs_migrate=bool(needs_migrate), migration_done=not needs_migrate + ) + + +class StatesContextIDMigration(BaseRunTimeMigrationWithQuery): """Migration to migrate states context_ids to binary format.""" required_schema_version = CONTEXT_ID_AS_BINARY_SCHEMA_VERSION @@ -2123,7 +2152,7 @@ class StatesContextIDMigration(BaseRunTimeMigration): return has_states_context_ids_to_migrate() -class EventsContextIDMigration(BaseRunTimeMigration): +class EventsContextIDMigration(BaseRunTimeMigrationWithQuery): """Migration to migrate events context_ids to binary format.""" required_schema_version = CONTEXT_ID_AS_BINARY_SCHEMA_VERSION @@ -2172,7 +2201,7 @@ class EventsContextIDMigration(BaseRunTimeMigration): return has_events_context_ids_to_migrate() -class EventTypeIDMigration(BaseRunTimeMigration): +class EventTypeIDMigration(BaseRunTimeMigrationWithQuery): """Migration to migrate event_type to event_type_ids.""" required_schema_version = EVENT_TYPE_IDS_SCHEMA_VERSION @@ -2255,7 +2284,7 @@ class EventTypeIDMigration(BaseRunTimeMigration): return has_event_type_to_migrate() -class EntityIDMigration(BaseRunTimeMigration): +class EntityIDMigration(BaseRunTimeMigrationWithQuery): """Migration to migrate entity_ids to states_meta.""" required_schema_version = STATES_META_SCHEMA_VERSION @@ -2367,6 +2396,48 @@ class EntityIDMigration(BaseRunTimeMigration): return has_entity_ids_to_migrate() +class EventIDPostMigration(BaseRunTimeMigration): + """Migration to remove old event_id index from states.""" + + migration_id = "event_id_post_migration" + task = MigrationTask + + @staticmethod + def migrate_data(instance: Recorder) -> bool: + """Migrate some data, returns True if migration is completed.""" + return cleanup_legacy_states_event_ids(instance) + + @staticmethod + def _legacy_event_id_foreign_key_exists(instance: Recorder) -> bool: + """Check if the legacy event_id foreign key exists.""" + engine = instance.engine + assert engine is not None + inspector = sqlalchemy.inspect(engine) + return bool( + next( + ( + fk + for fk in inspector.get_foreign_keys(TABLE_STATES) + if fk["constrained_columns"] == ["event_id"] + ), + None, + ) + ) + + def needs_migrate_impl( + self, instance: Recorder, session: Session + ) -> NeedsMigrateResult: + """Return if the migration needs to run.""" + if self.schema_version <= LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION: + return NeedsMigrateResult(needs_migrate=False, migration_done=False) + if get_index_by_name( + session, TABLE_STATES, LEGACY_STATES_EVENT_ID_INDEX + ) is not None or self._legacy_event_id_foreign_key_exists(instance): + instance.use_legacy_events_index = True + return NeedsMigrateResult(needs_migrate=True, migration_done=False) + return NeedsMigrateResult(needs_migrate=False, migration_done=True) + + def _mark_migration_done( session: Session, migration: type[BaseRunTimeMigration] ) -> None: diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 6072c5cdde7..46e529d4909 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -371,20 +371,6 @@ class EntityIDPostMigrationTask(RecorderTask): instance.queue_task(EntityIDPostMigrationTask()) -@dataclass(slots=True) -class EventIdMigrationTask(RecorderTask): - """An object to insert into the recorder queue to cleanup legacy event_ids in the states table. - - This task should only be queued if the ix_states_event_id index exists - since it is used to scan the states table and it will be removed after this - task is run if its no longer needed. - """ - - def run(self, instance: Recorder) -> None: - """Clean up the legacy event_id index on states.""" - instance._cleanup_legacy_states_event_ids() # noqa: SLF001 - - @dataclass(slots=True) class RefreshEventTypesTask(RecorderTask): """An object to insert into the recorder queue to refresh event types.""" diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index 188e81d0230..9956fec8a09 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -3,7 +3,7 @@ from datetime import timedelta import importlib import sys -from unittest.mock import DEFAULT, patch +from unittest.mock import patch import pytest from sqlalchemy import create_engine, inspect @@ -107,10 +107,9 @@ async def test_migrate_times( patch.object(core, "States", old_db_schema.States), patch.object(core, "Events", old_db_schema.Events), patch(CREATE_ENGINE_TARGET, new=_create_engine_test), - patch.multiple( - "homeassistant.components.recorder.Recorder", - _post_migrate_entity_ids=DEFAULT, - _cleanup_legacy_states_event_ids=DEFAULT, + patch("homeassistant.components.recorder.Recorder._post_migrate_entity_ids"), + patch( + "homeassistant.components.recorder.migration.cleanup_legacy_states_event_ids" ), ): async with ( @@ -259,10 +258,9 @@ async def test_migrate_can_resume_entity_id_post_migration( patch.object(core, "States", old_db_schema.States), patch.object(core, "Events", old_db_schema.Events), patch(CREATE_ENGINE_TARGET, new=_create_engine_test), - patch.multiple( - "homeassistant.components.recorder.Recorder", - _post_migrate_entity_ids=DEFAULT, - _cleanup_legacy_states_event_ids=DEFAULT, + patch("homeassistant.components.recorder.Recorder._post_migrate_entity_ids"), + patch( + "homeassistant.components.recorder.migration.cleanup_legacy_states_event_ids" ), ): async with ( @@ -314,6 +312,7 @@ async def test_migrate_can_resume_entity_id_post_migration( await hass.async_stop() +@pytest.mark.parametrize("enable_migrate_event_ids", [True]) @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_can_resume_ix_states_event_id_removed( @@ -381,10 +380,9 @@ async def test_migrate_can_resume_ix_states_event_id_removed( patch.object(core, "States", old_db_schema.States), patch.object(core, "Events", old_db_schema.Events), patch(CREATE_ENGINE_TARGET, new=_create_engine_test), - patch.multiple( - "homeassistant.components.recorder.Recorder", - _post_migrate_entity_ids=DEFAULT, - _cleanup_legacy_states_event_ids=DEFAULT, + patch("homeassistant.components.recorder.Recorder._post_migrate_entity_ids"), + patch( + "homeassistant.components.recorder.migration.cleanup_legacy_states_event_ids" ), ): async with ( @@ -440,6 +438,7 @@ async def test_migrate_can_resume_ix_states_event_id_removed( states_indexes = await instance.async_add_executor_job(_get_states_index_names) states_index_names = {index["name"] for index in states_indexes} + assert instance.use_legacy_events_index is False assert "ix_states_entity_id_last_updated_ts" not in states_index_names assert "ix_states_event_id" not in states_index_names assert await instance.async_add_executor_job(_get_event_id_foreign_keys) is None diff --git a/tests/conftest.py b/tests/conftest.py index f21dfbec5e7..935ceffa108 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1475,9 +1475,9 @@ async def async_test_recorder( migration.EntityIDMigration.migrate_data if enable_migrate_entity_ids else None ) legacy_event_id_foreign_key_exists = ( - recorder.Recorder._legacy_event_id_foreign_key_exists + migration.EventIDPostMigration._legacy_event_id_foreign_key_exists if enable_migrate_event_ids - else None + else lambda _: None ) with ( patch( @@ -1516,7 +1516,7 @@ async def async_test_recorder( autospec=True, ), patch( - "homeassistant.components.recorder.Recorder._legacy_event_id_foreign_key_exists", + "homeassistant.components.recorder.migration.EventIDPostMigration._legacy_event_id_foreign_key_exists", side_effect=legacy_event_id_foreign_key_exists, autospec=True, ), From 3dc36cf068b93a7934cef2dd186a41f0e58d268c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Jul 2024 21:16:11 +0200 Subject: [PATCH 1464/2411] Improve error handling when creating new SQLite database (#122406) Co-authored-by: J. Nick Koston --- homeassistant/components/recorder/core.py | 25 ++++++++++------------- tests/components/recorder/test_migrate.py | 12 +++++++++++ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 67c823259ea..3024eb9507f 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -769,9 +769,7 @@ class Recorder(threading.Thread): # Do live migration steps and repairs, if needed if schema_status.migration_needed or schema_status.schema_errors: - result, schema_status = self._migrate_schema_live_and_setup_run( - schema_status - ) + result, schema_status = self._migrate_schema_live(schema_status) if result: self.schema_version = SCHEMA_VERSION if not self._event_listener: @@ -789,6 +787,7 @@ class Recorder(threading.Thread): if self.migration_in_progress: self.migration_in_progress = False self._dismiss_migration_in_progress() + self._setup_run() # Catch up with missed statistics self._schedule_compile_missing_statistics() @@ -907,7 +906,7 @@ class Recorder(threading.Thread): self._commit_event_session_or_retry() task.run(self) except exc.DatabaseError as err: - if self._handle_database_error(err): + if self._handle_database_error(err, setup_run=True): return _LOGGER.exception("Unhandled database error while processing task %s", task) except SQLAlchemyError: @@ -953,7 +952,7 @@ class Recorder(threading.Thread): """Migrate schema to the latest version.""" return self._migrate_schema(schema_status, False) - def _migrate_schema_live_and_setup_run( + def _migrate_schema_live( self, schema_status: migration.SchemaValidationStatus ) -> tuple[bool, migration.SchemaValidationStatus]: """Migrate schema to the latest version.""" @@ -971,10 +970,7 @@ class Recorder(threading.Thread): "recorder_database_migration", ) self.hass.add_job(self._async_migration_started) - migration_result, schema_status = self._migrate_schema(schema_status, True) - if migration_result: - self._setup_run() - return migration_result, schema_status + return self._migrate_schema(schema_status, True) def _migrate_schema( self, @@ -992,7 +988,7 @@ class Recorder(threading.Thread): self, self.hass, self.engine, self.get_session, schema_status ) except exc.DatabaseError as err: - if self._handle_database_error(err): + if self._handle_database_error(err, setup_run=False): # If _handle_database_error returns True, we have a new database # which does not need migration or repair. new_schema_status = migration.SchemaValidationStatus( @@ -1179,7 +1175,7 @@ class Recorder(threading.Thread): self._add_to_session(session, dbstate) - def _handle_database_error(self, err: Exception) -> bool: + def _handle_database_error(self, err: Exception, *, setup_run: bool) -> bool: """Handle a database error that may result in moving away the corrupt db.""" if ( (cause := err.__cause__) @@ -1193,7 +1189,7 @@ class Recorder(threading.Thread): _LOGGER.exception( "Unrecoverable sqlite3 database corruption detected: %s", err ) - self._handle_sqlite_corruption() + self._handle_sqlite_corruption(setup_run) return True return False @@ -1260,7 +1256,7 @@ class Recorder(threading.Thread): self._commits_without_expire = 0 session.expire_all() - def _handle_sqlite_corruption(self) -> None: + def _handle_sqlite_corruption(self, setup_run: bool) -> None: """Handle the sqlite3 database being corrupt.""" try: self._close_event_session() @@ -1269,7 +1265,8 @@ class Recorder(threading.Thread): move_away_broken_database(dburl_to_path(self.db_url)) self.recorder_runs_manager.reset() self._setup_recorder() - self._setup_run() + if setup_run: + self._setup_run() def _close_event_session(self) -> None: """Close the event session.""" diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 2b3980b2b67..e287b150fa6 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -190,6 +190,11 @@ async def test_live_database_migration_encounters_corruption( patch( "homeassistant.components.recorder.core.move_away_broken_database" ) as move_away, + patch( + "homeassistant.components.recorder.core.Recorder._setup_run", + autospec=True, + wraps=recorder.Recorder._setup_run, + ) as setup_run, ): await async_setup_recorder_instance(hass) hass.states.async_set("my.entity", "on", {}) @@ -198,6 +203,7 @@ async def test_live_database_migration_encounters_corruption( assert recorder.util.async_migration_in_progress(hass) is False move_away.assert_called_once() + setup_run.assert_called_once() @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) @@ -235,6 +241,11 @@ async def test_non_live_database_migration_encounters_corruption( patch( "homeassistant.components.recorder.core.move_away_broken_database" ) as move_away, + patch( + "homeassistant.components.recorder.core.Recorder._setup_run", + autospec=True, + wraps=recorder.Recorder._setup_run, + ) as setup_run, ): await async_setup_recorder_instance(hass) hass.states.async_set("my.entity", "on", {}) @@ -244,6 +255,7 @@ async def test_non_live_database_migration_encounters_corruption( assert recorder.util.async_migration_in_progress(hass) is False move_away.assert_called_once() migrate_schema_live.assert_not_called() + setup_run.assert_called_once() @pytest.mark.parametrize( From db6704271c7812b2f12faf97fab71803d2734f29 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Jul 2024 21:36:36 +0200 Subject: [PATCH 1465/2411] Avoid repeated calls to utc_from_timestamp(0).isoformat() when migrating (#122413) --- homeassistant/helpers/area_registry.py | 5 ++--- homeassistant/helpers/floor_registry.py | 5 ++--- homeassistant/helpers/label_registry.py | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index bf6dd0d6fcb..3e101f185ed 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -133,10 +133,9 @@ class AreaRegistryStore(Store[AreasRegistryStoreData]): if old_minor_version < 7: # Version 1.7 adds created_at and modiefied_at + created_at = utc_from_timestamp(0).isoformat() for area in old_data["areas"]: - area["created_at"] = area["modified_at"] = utc_from_timestamp( - 0 - ).isoformat() + area["created_at"] = area["modified_at"] = created_at if old_major_version > 1: raise NotImplementedError diff --git a/homeassistant/helpers/floor_registry.py b/homeassistant/helpers/floor_registry.py index 257da3dc47e..f14edef293a 100644 --- a/homeassistant/helpers/floor_registry.py +++ b/homeassistant/helpers/floor_registry.py @@ -87,10 +87,9 @@ class FloorRegistryStore(Store[FloorRegistryStoreData]): if old_major_version == 1: if old_minor_version < 2: # Version 1.2 implements migration and adds created_at and modified_at + created_at = utc_from_timestamp(0).isoformat() for floor in old_data["floors"]: - floor["created_at"] = floor["modified_at"] = utc_from_timestamp( - 0 - ).isoformat() + floor["created_at"] = floor["modified_at"] = created_at return old_data # type: ignore[return-value] diff --git a/homeassistant/helpers/label_registry.py b/homeassistant/helpers/label_registry.py index ab6fdc847fa..1007b17bc5d 100644 --- a/homeassistant/helpers/label_registry.py +++ b/homeassistant/helpers/label_registry.py @@ -87,10 +87,9 @@ class LabelRegistryStore(Store[LabelRegistryStoreData]): if old_major_version == 1: if old_minor_version < 2: # Version 1.2 implements migration and adds created_at and modified_at + created_at = utc_from_timestamp(0).isoformat() for label in old_data["labels"]: - label["created_at"] = label["modified_at"] = utc_from_timestamp( - 0 - ).isoformat() + label["created_at"] = label["modified_at"] = created_at return old_data # type: ignore[return-value] From d3df903d1e53f9edcbb04f524a0c3b9219c0a458 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Jul 2024 21:37:47 +0200 Subject: [PATCH 1466/2411] Make device registry migration unconditional (#122414) --- homeassistant/helpers/device_registry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index b8fb3ae0219..96bba6c8c4c 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -498,16 +498,16 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): if old_minor_version < 5: # Introduced in 2024.3 for device in old_data["devices"]: - device["labels"] = device.get("labels", []) + device["labels"] = [] if old_minor_version < 6: # Introduced in 2024.7 for device in old_data["devices"]: - device.setdefault("primary_config_entry", None) + device["primary_config_entry"] = None if old_minor_version < 7: # Introduced in 2024.8 created_at = utc_from_timestamp(0).isoformat() for device in old_data["devices"]: - device.setdefault("model_id", None) + device["model_id"] = None device["created_at"] = device["modified_at"] = created_at for device in old_data["deleted_devices"]: device["created_at"] = device["modified_at"] = created_at From c61efe931a6827df0f6e40f0920df8c01b67e9b7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 22 Jul 2024 21:37:58 +0200 Subject: [PATCH 1467/2411] Deduplicate more fixture data related to deCONZ websocket sensor (#122412) --- tests/components/deconz/conftest.py | 13 +++ .../deconz/test_alarm_control_panel.py | 50 ++------- tests/components/deconz/test_binary_sensor.py | 28 ++--- tests/components/deconz/test_climate.py | 103 ++++-------------- tests/components/deconz/test_deconz_event.py | 98 ++++------------- .../components/deconz/test_device_trigger.py | 8 +- tests/components/deconz/test_lock.py | 8 +- tests/components/deconz/test_number.py | 5 +- tests/components/deconz/test_sensor.py | 29 ++--- 9 files changed, 86 insertions(+), 256 deletions(-) diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py index b0d64e3231f..5ff8aba6f60 100644 --- a/tests/components/deconz/conftest.py +++ b/tests/components/deconz/conftest.py @@ -269,6 +269,19 @@ def fixture_websocket_data(_mock_websocket: _WebsocketMock) -> WebsocketDataType return change_websocket_data +@pytest.fixture(name="sensor_ws_data") +def fixture_sensor_websocket_data( + mock_websocket_data: WebsocketDataType, +) -> WebsocketDataType: + """Fixture to send sensor data over websocket.""" + + async def send_sensor_data(data: dict[str, Any]) -> None: + """Send sensor data on the websocket.""" + await mock_websocket_data({"r": "sensors"} | data) + + return send_sensor_data + + @pytest.fixture(name="mock_websocket_state") def fixture_websocket_state(_mock_websocket: _WebsocketMock) -> WebsocketStateType: """Fixture to set websocket state.""" diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py index 7dd7dc49603..76b35fd06da 100644 --- a/tests/components/deconz/test_alarm_control_panel.py +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -102,7 +102,7 @@ async def test_alarm_control_panel( aioclient_mock: AiohttpClientMocker, config_entry_setup: ConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test successful creation of alarm control panel entities.""" assert len(hass.states.async_all()) == 4 @@ -110,22 +110,14 @@ async def test_alarm_control_panel( # Event signals alarm control panel armed away - event_changed_sensor = { - "r": "sensors", - "state": {"panel": AncillaryControlPanel.ARMED_AWAY}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"panel": AncillaryControlPanel.ARMED_AWAY}}) await hass.async_block_till_done() assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_AWAY # Event signals alarm control panel armed night - event_changed_sensor = { - "r": "sensors", - "state": {"panel": AncillaryControlPanel.ARMED_NIGHT}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"panel": AncillaryControlPanel.ARMED_NIGHT}}) await hass.async_block_till_done() assert ( @@ -134,22 +126,14 @@ async def test_alarm_control_panel( # Event signals alarm control panel armed home - event_changed_sensor = { - "r": "sensors", - "state": {"panel": AncillaryControlPanel.ARMED_STAY}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"panel": AncillaryControlPanel.ARMED_STAY}}) await hass.async_block_till_done() assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_HOME # Event signals alarm control panel disarmed - event_changed_sensor = { - "r": "sensors", - "state": {"panel": AncillaryControlPanel.DISARMED}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"panel": AncillaryControlPanel.DISARMED}}) await hass.async_block_till_done() assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_DISARMED @@ -161,11 +145,7 @@ async def test_alarm_control_panel( AncillaryControlPanel.ARMING_NIGHT, AncillaryControlPanel.ARMING_STAY, ): - event_changed_sensor = { - "r": "sensors", - "state": {"panel": arming_event}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"panel": arming_event}}) await hass.async_block_till_done() assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMING @@ -176,11 +156,7 @@ async def test_alarm_control_panel( AncillaryControlPanel.ENTRY_DELAY, AncillaryControlPanel.EXIT_DELAY, ): - event_changed_sensor = { - "r": "sensors", - "state": {"panel": pending_event}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"panel": pending_event}}) await hass.async_block_till_done() assert ( @@ -189,22 +165,14 @@ async def test_alarm_control_panel( # Event signals alarm control panel triggered - event_changed_sensor = { - "r": "sensors", - "state": {"panel": AncillaryControlPanel.IN_ALARM}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"panel": AncillaryControlPanel.IN_ALARM}}) await hass.async_block_till_done() assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_TRIGGERED # Event signals alarm control panel unknown state keeps previous state - event_changed_sensor = { - "r": "sensors", - "state": {"panel": AncillaryControlPanel.NOT_READY}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"panel": AncillaryControlPanel.NOT_READY}}) await hass.async_block_till_done() assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_TRIGGERED diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 4d6c89ccc4d..a40a1175f5b 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -460,7 +460,7 @@ async def test_binary_sensors( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, config_entry_setup: ConfigEntry, - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, expected: dict[str, Any], ) -> None: """Test successful creation of binary sensor entities.""" @@ -492,8 +492,7 @@ async def test_binary_sensors( # Change state - event_changed_sensor = {"r": "sensors", "state": expected["websocket_event"]} - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": expected["websocket_event"]}) await hass.async_block_till_done() assert hass.states.get(expected["entity_id"]).state == expected["next_state"] @@ -595,14 +594,13 @@ async def test_allow_clip_sensor(hass: HomeAssistant, config_entry_setup) -> Non @pytest.mark.usefixtures("config_entry_setup") async def test_add_new_binary_sensor( hass: HomeAssistant, - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test that adding a new binary sensor works.""" assert len(hass.states.async_all()) == 0 event_added_sensor = { "e": "added", - "r": "sensors", "sensor": { "id": "Presence sensor id", "name": "Presence sensor", @@ -612,7 +610,7 @@ async def test_add_new_binary_sensor( "uniqueid": "00:00:00:00:00:00:00:00-00", }, } - await mock_websocket_data(event_added_sensor) + await sensor_ws_data(event_added_sensor) await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 @@ -628,7 +626,7 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_service_call( config_entry_setup: ConfigEntry, deconz_payload: dict[str, Any], mock_requests: Callable[[str], None], - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test that adding a new binary sensor is not allowed.""" sensor = { @@ -638,15 +636,10 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_service_call( "config": {"on": True, "reachable": True}, "uniqueid": "00:00:00:00:00:00:00:00-00", } - event_added_sensor = { - "e": "added", - "r": "sensors", - "sensor": sensor, - } assert len(hass.states.async_all()) == 0 - await mock_websocket_data(event_added_sensor) + await sensor_ws_data({"e": "added", "sensor": sensor}) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 @@ -680,7 +673,7 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_options_change( config_entry_setup: ConfigEntry, deconz_payload: dict[str, Any], mock_requests: Callable[[str], None], - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test that adding a new binary sensor is not allowed.""" sensor = { @@ -690,15 +683,10 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_options_change( "config": {"on": True, "reachable": True}, "uniqueid": "00:00:00:00:00:00:00:00-00", } - event_added_sensor = { - "e": "added", - "r": "sensors", - "sensor": sensor, - } assert len(hass.states.async_all()) == 0 - await mock_websocket_data(event_added_sensor) + await sensor_ws_data({"e": "added", "sensor": sensor}) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 94b4a30b8d2..14d21b0a281 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -89,7 +89,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_simple_climate_device( hass: HomeAssistant, mock_put_request: Callable[[str, str], AiohttpClientMocker], - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test successful creation of climate entities. @@ -110,11 +110,7 @@ async def test_simple_climate_device( # Event signals thermostat configured off - event_changed_sensor = { - "r": "sensors", - "state": {"on": False}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"on": False}}) await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == STATE_OFF @@ -125,11 +121,7 @@ async def test_simple_climate_device( # Event signals thermostat state on - event_changed_sensor = { - "r": "sensors", - "state": {"on": True}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"on": True}}) await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == HVACMode.HEAT @@ -195,7 +187,7 @@ async def test_climate_device_without_cooling_support( hass: HomeAssistant, config_entry_setup: ConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test successful creation of sensor entities.""" assert len(hass.states.async_all()) == 2 @@ -219,11 +211,7 @@ async def test_climate_device_without_cooling_support( # Event signals thermostat configured off - event_changed_sensor = { - "r": "sensors", - "config": {"mode": "off"}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"config": {"mode": "off"}}) await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == STATE_OFF @@ -234,12 +222,7 @@ async def test_climate_device_without_cooling_support( # Event signals thermostat state on - event_changed_sensor = { - "r": "sensors", - "config": {"mode": "other"}, - "state": {"on": True}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"config": {"mode": "other"}, "state": {"on": True}}) await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == HVACMode.HEAT @@ -250,11 +233,7 @@ async def test_climate_device_without_cooling_support( # Event signals thermostat state off - event_changed_sensor = { - "r": "sensors", - "state": {"on": False}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"on": False}}) await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == STATE_OFF @@ -377,7 +356,7 @@ async def test_climate_device_without_cooling_support( async def test_climate_device_with_cooling_support( hass: HomeAssistant, mock_put_request: Callable[[str, str], AiohttpClientMocker], - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test successful creation of sensor entities.""" assert len(hass.states.async_all()) == 2 @@ -398,11 +377,7 @@ async def test_climate_device_with_cooling_support( # Event signals thermostat mode cool - event_changed_sensor = { - "r": "sensors", - "config": {"mode": "cool"}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"config": {"mode": "cool"}}) await hass.async_block_till_done() await hass.async_block_till_done() @@ -414,11 +389,7 @@ async def test_climate_device_with_cooling_support( # Event signals thermostat state on - event_changed_sensor = { - "r": "sensors", - "state": {"on": True}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"on": True}}) await hass.async_block_till_done() assert hass.states.get("climate.zen_01").state == HVACMode.COOL @@ -476,7 +447,7 @@ async def test_climate_device_with_cooling_support( async def test_climate_device_with_fan_support( hass: HomeAssistant, mock_put_request: Callable[[str, str], AiohttpClientMocker], - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test successful creation of sensor entities.""" assert len(hass.states.async_all()) == 2 @@ -498,11 +469,7 @@ async def test_climate_device_with_fan_support( # Event signals fan mode defaults to off - event_changed_sensor = { - "r": "sensors", - "config": {"fanmode": "unsupported"}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"config": {"fanmode": "unsupported"}}) await hass.async_block_till_done() assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_OFF @@ -512,12 +479,7 @@ async def test_climate_device_with_fan_support( # Event signals unsupported fan mode - event_changed_sensor = { - "r": "sensors", - "config": {"fanmode": "unsupported"}, - "state": {"on": True}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"config": {"fanmode": "unsupported"}, "state": {"on": True}}) await hass.async_block_till_done() assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON @@ -528,11 +490,7 @@ async def test_climate_device_with_fan_support( # Event signals unsupported fan mode - event_changed_sensor = { - "r": "sensors", - "config": {"fanmode": "unsupported"}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"config": {"fanmode": "unsupported"}}) await hass.async_block_till_done() assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON @@ -611,7 +569,7 @@ async def test_climate_device_with_fan_support( async def test_climate_device_with_preset( hass: HomeAssistant, mock_put_request: Callable[[str, str], AiohttpClientMocker], - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test successful creation of sensor entities.""" assert len(hass.states.async_all()) == 2 @@ -636,11 +594,7 @@ async def test_climate_device_with_preset( # Event signals deCONZ preset - event_changed_sensor = { - "r": "sensors", - "config": {"preset": "manual"}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"config": {"preset": "manual"}}) await hass.async_block_till_done() assert ( @@ -650,11 +604,7 @@ async def test_climate_device_with_preset( # Event signals unknown preset - event_changed_sensor = { - "r": "sensors", - "config": {"preset": "unsupported"}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"config": {"preset": "unsupported"}}) await hass.async_block_till_done() assert hass.states.get("climate.zen_01").attributes["preset_mode"] is None @@ -779,7 +729,7 @@ async def test_clip_climate_device( @pytest.mark.usefixtures("config_entry_setup") async def test_verify_state_update( hass: HomeAssistant, - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test that state update properly.""" assert hass.states.get("climate.thermostat").state == HVACMode.AUTO @@ -788,8 +738,7 @@ async def test_verify_state_update( == HVACAction.HEATING ) - event_changed_sensor = {"r": "sensors", "state": {"on": False}} - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"on": False}}) await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == HVACMode.AUTO @@ -802,12 +751,11 @@ async def test_verify_state_update( @pytest.mark.usefixtures("config_entry_setup") async def test_add_new_climate_device( hass: HomeAssistant, - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test that adding a new climate device works.""" event_added_sensor = { "e": "added", - "r": "sensors", "sensor": { "id": "Thermostat id", "name": "Thermostat", @@ -826,7 +774,7 @@ async def test_add_new_climate_device( assert len(hass.states.async_all()) == 0 - await mock_websocket_data(event_added_sensor) + await sensor_ws_data(event_added_sensor) await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 @@ -934,7 +882,7 @@ async def test_no_mode_no_state(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("config_entry_setup") async def test_boost_mode( hass: HomeAssistant, - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test that a climate device with boost mode and different state works.""" @@ -948,13 +896,8 @@ async def test_boost_mode( assert climate_thermostat.attributes["hvac_action"] is HVACAction.IDLE # Event signals thermostat preset boost and valve 100 (real data) - event_changed_sensor = { - "r": "sensors", - "config": {"preset": "boost"}, - "state": {"valve": 100}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"config": {"preset": "boost"}, "state": {"valve": 100}}) await hass.async_block_till_done() climate_thermostat = hass.states.get("climate.thermostat") diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index adbea618efb..5867e0e3dec 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -79,7 +79,7 @@ async def test_deconz_events( hass: HomeAssistant, device_registry: dr.DeviceRegistry, config_entry_setup: ConfigEntry, - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test successful creation of deconz events.""" assert len(hass.states.async_all()) == 3 @@ -98,12 +98,7 @@ async def test_deconz_events( captured_events = async_capture_events(hass, CONF_DECONZ_EVENT) - event_changed_sensor = { - "r": "sensors", - "id": "1", - "state": {"buttonevent": 2000}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"id": "1", "state": {"buttonevent": 2000}}) await hass.async_block_till_done() device = device_registry.async_get_device( @@ -118,12 +113,7 @@ async def test_deconz_events( "device_id": device.id, } - event_changed_sensor = { - "r": "sensors", - "id": "3", - "state": {"buttonevent": 2000}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"id": "3", "state": {"buttonevent": 2000}}) await hass.async_block_till_done() device = device_registry.async_get_device( @@ -139,12 +129,7 @@ async def test_deconz_events( "device_id": device.id, } - event_changed_sensor = { - "r": "sensors", - "id": "4", - "state": {"gesture": 0}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"id": "4", "state": {"gesture": 0}}) await hass.async_block_till_done() device = device_registry.async_get_device( @@ -161,11 +146,10 @@ async def test_deconz_events( } event_changed_sensor = { - "r": "sensors", "id": "5", "state": {"buttonevent": 6002, "angle": 110, "xy": [0.5982, 0.3897]}, } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data(event_changed_sensor) await hass.async_block_till_done() device = device_registry.async_get_device( @@ -184,12 +168,7 @@ async def test_deconz_events( # Unsupported event - event_changed_sensor = { - "r": "sensors", - "id": "1", - "name": "other name", - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"id": "1", "name": "other name"}) await hass.async_block_till_done() assert len(captured_events) == 4 @@ -274,7 +253,7 @@ async def test_deconz_alarm_events( hass: HomeAssistant, device_registry: dr.DeviceRegistry, config_entry_setup: ConfigEntry, - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test successful creation of deconz alarm events.""" assert len(hass.states.async_all()) == 4 @@ -292,11 +271,7 @@ async def test_deconz_alarm_events( # Emergency event - event_changed_sensor = { - "r": "sensors", - "state": {"action": AncillaryControlAction.EMERGENCY}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"action": AncillaryControlAction.EMERGENCY}}) await hass.async_block_till_done() device = device_registry.async_get_device( @@ -313,11 +288,7 @@ async def test_deconz_alarm_events( # Fire event - event_changed_sensor = { - "r": "sensors", - "state": {"action": AncillaryControlAction.FIRE}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"action": AncillaryControlAction.FIRE}}) await hass.async_block_till_done() device = device_registry.async_get_device( @@ -334,11 +305,7 @@ async def test_deconz_alarm_events( # Invalid code event - event_changed_sensor = { - "r": "sensors", - "state": {"action": AncillaryControlAction.INVALID_CODE}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"action": AncillaryControlAction.INVALID_CODE}}) await hass.async_block_till_done() device = device_registry.async_get_device( @@ -355,11 +322,7 @@ async def test_deconz_alarm_events( # Panic event - event_changed_sensor = { - "r": "sensors", - "state": {"action": AncillaryControlAction.PANIC}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"action": AncillaryControlAction.PANIC}}) await hass.async_block_till_done() device = device_registry.async_get_device( @@ -376,22 +339,14 @@ async def test_deconz_alarm_events( # Only care for changes to specific action events - event_changed_sensor = { - "r": "sensors", - "state": {"action": AncillaryControlAction.ARMED_AWAY}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"action": AncillaryControlAction.ARMED_AWAY}}) await hass.async_block_till_done() assert len(captured_events) == 4 # Only care for action events - event_changed_sensor = { - "r": "sensors", - "state": {"panel": AncillaryControlPanel.ARMED_AWAY}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"panel": AncillaryControlPanel.ARMED_AWAY}}) await hass.async_block_till_done() assert len(captured_events) == 4 @@ -440,7 +395,7 @@ async def test_deconz_presence_events( hass: HomeAssistant, device_registry: dr.DeviceRegistry, config_entry_setup: ConfigEntry, - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test successful creation of deconz presence events.""" assert len(hass.states.async_all()) == 5 @@ -469,11 +424,7 @@ async def test_deconz_presence_events( PresenceStatePresenceEvent.LEFT_LEAVE, PresenceStatePresenceEvent.RIGHT_LEAVE, ): - event_changed_sensor = { - "r": "sensors", - "state": {"presenceevent": presence_event}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"presenceevent": presence_event}}) await hass.async_block_till_done() assert len(captured_events) == 1 @@ -487,11 +438,7 @@ async def test_deconz_presence_events( # Unsupported presence event - event_changed_sensor = { - "r": "sensors", - "state": {"presenceevent": PresenceStatePresenceEvent.NINE}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"presenceevent": PresenceStatePresenceEvent.NINE}}) await hass.async_block_till_done() assert len(captured_events) == 0 @@ -539,7 +486,7 @@ async def test_deconz_relative_rotary_events( hass: HomeAssistant, device_registry: dr.DeviceRegistry, config_entry_setup: ConfigEntry, - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test successful creation of deconz relative rotary events.""" assert len(hass.states.async_all()) == 1 @@ -560,14 +507,13 @@ async def test_deconz_relative_rotary_events( for rotary_event, duration, rotation in ((1, 100, 50), (2, 200, -50)): event_changed_sensor = { - "r": "sensors", "state": { "rotaryevent": rotary_event, "expectedeventduration": duration, "expectedrotation": rotation, - }, + } } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data(event_changed_sensor) await hass.async_block_till_done() assert len(captured_events) == 1 @@ -583,11 +529,7 @@ async def test_deconz_relative_rotary_events( # Unsupported relative rotary event - event_changed_sensor = { - "r": "sensors", - "name": "123", - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"name": "123"}) await hass.async_block_till_done() assert len(captured_events) == 0 diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 1d3196ba8e9..9e22c91794f 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -312,7 +312,7 @@ async def test_functional_device_trigger( hass: HomeAssistant, device_registry: dr.DeviceRegistry, service_calls: list[ServiceCall], - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test proper matching and attachment of device trigger automation.""" device = device_registry.async_get_device( @@ -343,11 +343,7 @@ async def test_functional_device_trigger( assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 1 - event_changed_sensor = { - "r": "sensors", - "state": {"buttonevent": 1002}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"buttonevent": 1002}}) await hass.async_block_till_done() assert len(service_calls) == 1 diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index 923e8d768c8..28cd57633cc 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -122,17 +122,13 @@ async def test_lock_from_sensor( hass: HomeAssistant, config_entry_setup: ConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test that all supported lock entities based on sensors are created.""" assert len(hass.states.async_all()) == 2 assert hass.states.get("lock.door_lock").state == STATE_UNLOCKED - event_changed_sensor = { - "r": "sensors", - "state": {"lockstate": "locked"}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"state": {"lockstate": "locked"}}) await hass.async_block_till_done() assert hass.states.get("lock.door_lock").state == STATE_LOCKED diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py index f027e6b5a9f..330d8d80e47 100644 --- a/tests/components/deconz/test_number.py +++ b/tests/components/deconz/test_number.py @@ -102,7 +102,7 @@ async def test_number_entities( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, config_entry_setup: ConfigEntry, - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, mock_put_request: Callable[[str, str], AiohttpClientMocker], expected: dict[str, Any], ) -> None: @@ -134,8 +134,7 @@ async def test_number_entities( # Change state - event_changed_sensor = {"r": "sensors"} | expected["websocket_event"] - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data(expected["websocket_event"]) await hass.async_block_till_done() assert hass.states.get(expected["entity_id"]).state == expected["next_state"] diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index c29ed09c4c0..ce5e0fb69e3 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -906,7 +906,7 @@ async def test_sensors( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, config_entry_setup: ConfigEntry, - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, expected: dict[str, Any], ) -> None: """Test successful creation of sensor entities.""" @@ -952,9 +952,7 @@ async def test_sensors( # Change state - event_changed_sensor = {"r": "sensors"} - event_changed_sensor |= expected["websocket_event"] - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data(expected["websocket_event"]) await hass.async_block_till_done() assert hass.states.get(expected["entity_id"]).state == expected["next_state"] @@ -1057,12 +1055,11 @@ async def test_allow_clip_sensors( @pytest.mark.usefixtures("config_entry_setup") async def test_add_new_sensor( hass: HomeAssistant, - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test that adding a new sensor works.""" event_added_sensor = { "e": "added", - "r": "sensors", "sensor": { "id": "Light sensor id", "name": "Light level sensor", @@ -1075,7 +1072,7 @@ async def test_add_new_sensor( assert len(hass.states.async_all()) == 0 - await mock_websocket_data(event_added_sensor) + await sensor_ws_data(event_added_sensor) await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 @@ -1165,7 +1162,7 @@ async def test_air_quality_sensor_without_ppb(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("config_entry_setup") async def test_add_battery_later( hass: HomeAssistant, - mock_websocket_data: WebsocketDataType, + sensor_ws_data: WebsocketDataType, ) -> None: """Test that a battery sensor can be created later on. @@ -1174,24 +1171,12 @@ async def test_add_battery_later( """ assert len(hass.states.async_all()) == 0 - event_changed_sensor = { - "e": "changed", - "r": "sensors", - "id": "2", - "config": {"battery": 50}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"id": "2", "config": {"battery": 50}}) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 - event_changed_sensor = { - "e": "changed", - "r": "sensors", - "id": "1", - "config": {"battery": 50}, - } - await mock_websocket_data(event_changed_sensor) + await sensor_ws_data({"id": "1", "config": {"battery": 50}}) await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 From fed17a49052452440839d2530137fa915d4040ba Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Mon, 22 Jul 2024 21:39:22 +0200 Subject: [PATCH 1468/2411] Add DeviceInfo to OTP integration (#122392) --- homeassistant/components/otp/sensor.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index 2e166859729..4119d02da8b 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -15,6 +15,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_TOKEN from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType @@ -76,13 +77,20 @@ class TOTPSensor(SensorEntity): _attr_should_poll = False _attr_native_value: StateType = None _next_expiration: float | None = None + _attr_has_entity_name = True + _attr_name = None def __init__(self, name: str, token: str, entry_id: str) -> None: """Initialize the sensor.""" - self._attr_name = name self._attr_unique_id = entry_id self._otp = pyotp.TOTP(token) + self.device_info = DeviceInfo( + name=name, + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry_id)}, + ) + async def async_added_to_hass(self) -> None: """Handle when an entity is about to be added to Home Assistant.""" self._call_loop() From a1cdd91d23b29aca3eb614d83e6aad6b584c3255 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 22 Jul 2024 21:41:24 +0200 Subject: [PATCH 1469/2411] Continue transition from legacy dict to attr in dsmr (#121906) --- homeassistant/components/dsmr/sensor.py | 133 ++++++++----------- tests/components/dsmr/test_mbus_migration.py | 2 +- tests/components/dsmr/test_sensor.py | 34 +++-- 3 files changed, 77 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 39b90f2060b..ae7b08b7f62 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -10,7 +10,6 @@ from dataclasses import dataclass from datetime import timedelta from functools import partial -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, @@ -81,7 +80,7 @@ class DSMRSensorEntityDescription(SensorEntityDescription): SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="timestamp", - obis_reference=obis_references.P1_MESSAGE_TIMESTAMP, + obis_reference="P1_MESSAGE_TIMESTAMP", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -89,21 +88,21 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="current_electricity_usage", translation_key="current_electricity_usage", - obis_reference=obis_references.CURRENT_ELECTRICITY_USAGE, + obis_reference="CURRENT_ELECTRICITY_USAGE", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key="current_electricity_delivery", translation_key="current_electricity_delivery", - obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY, + obis_reference="CURRENT_ELECTRICITY_DELIVERY", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( key="electricity_active_tariff", translation_key="electricity_active_tariff", - obis_reference=obis_references.ELECTRICITY_ACTIVE_TARIFF, + obis_reference="ELECTRICITY_ACTIVE_TARIFF", dsmr_versions={"2.2", "4", "5", "5B", "5L"}, device_class=SensorDeviceClass.ENUM, options=["low", "normal"], @@ -111,7 +110,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="electricity_used_tariff_1", translation_key="electricity_used_tariff_1", - obis_reference=obis_references.ELECTRICITY_USED_TARIFF_1, + obis_reference="ELECTRICITY_USED_TARIFF_1", dsmr_versions={"2.2", "4", "5", "5B", "5L"}, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -119,7 +118,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="electricity_used_tariff_2", translation_key="electricity_used_tariff_2", - obis_reference=obis_references.ELECTRICITY_USED_TARIFF_2, + obis_reference="ELECTRICITY_USED_TARIFF_2", dsmr_versions={"2.2", "4", "5", "5B", "5L"}, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -127,7 +126,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="electricity_delivered_tariff_1", translation_key="electricity_delivered_tariff_1", - obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, + obis_reference="ELECTRICITY_DELIVERED_TARIFF_1", dsmr_versions={"2.2", "4", "5", "5B", "5L"}, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -135,7 +134,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="electricity_delivered_tariff_2", translation_key="electricity_delivered_tariff_2", - obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, + obis_reference="ELECTRICITY_DELIVERED_TARIFF_2", dsmr_versions={"2.2", "4", "5", "5B", "5L"}, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -143,7 +142,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="instantaneous_active_power_l1_positive", translation_key="instantaneous_active_power_l1_positive", - obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + obis_reference="INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, @@ -151,7 +150,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="instantaneous_active_power_l2_positive", translation_key="instantaneous_active_power_l2_positive", - obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + obis_reference="INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, @@ -159,7 +158,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="instantaneous_active_power_l3_positive", translation_key="instantaneous_active_power_l3_positive", - obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + obis_reference="INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, @@ -167,7 +166,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="instantaneous_active_power_l1_negative", translation_key="instantaneous_active_power_l1_negative", - obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, + obis_reference="INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, @@ -175,7 +174,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="instantaneous_active_power_l2_negative", translation_key="instantaneous_active_power_l2_negative", - obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, + obis_reference="INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, @@ -183,7 +182,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="instantaneous_active_power_l3_negative", translation_key="instantaneous_active_power_l3_negative", - obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, + obis_reference="INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, @@ -191,7 +190,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="short_power_failure_count", translation_key="short_power_failure_count", - obis_reference=obis_references.SHORT_POWER_FAILURE_COUNT, + obis_reference="SHORT_POWER_FAILURE_COUNT", dsmr_versions={"2.2", "4", "5", "5L"}, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -199,7 +198,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="long_power_failure_count", translation_key="long_power_failure_count", - obis_reference=obis_references.LONG_POWER_FAILURE_COUNT, + obis_reference="LONG_POWER_FAILURE_COUNT", dsmr_versions={"2.2", "4", "5", "5L"}, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -207,7 +206,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="voltage_sag_l1_count", translation_key="voltage_sag_l1_count", - obis_reference=obis_references.VOLTAGE_SAG_L1_COUNT, + obis_reference="VOLTAGE_SAG_L1_COUNT", dsmr_versions={"2.2", "4", "5", "5L"}, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -215,7 +214,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="voltage_sag_l2_count", translation_key="voltage_sag_l2_count", - obis_reference=obis_references.VOLTAGE_SAG_L2_COUNT, + obis_reference="VOLTAGE_SAG_L2_COUNT", dsmr_versions={"2.2", "4", "5", "5L"}, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -223,7 +222,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="voltage_sag_l3_count", translation_key="voltage_sag_l3_count", - obis_reference=obis_references.VOLTAGE_SAG_L3_COUNT, + obis_reference="VOLTAGE_SAG_L3_COUNT", dsmr_versions={"2.2", "4", "5", "5L"}, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -231,7 +230,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="voltage_swell_l1_count", translation_key="voltage_swell_l1_count", - obis_reference=obis_references.VOLTAGE_SWELL_L1_COUNT, + obis_reference="VOLTAGE_SWELL_L1_COUNT", dsmr_versions={"2.2", "4", "5", "5L"}, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -239,7 +238,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="voltage_swell_l2_count", translation_key="voltage_swell_l2_count", - obis_reference=obis_references.VOLTAGE_SWELL_L2_COUNT, + obis_reference="VOLTAGE_SWELL_L2_COUNT", dsmr_versions={"2.2", "4", "5", "5L"}, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -247,7 +246,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="voltage_swell_l3_count", translation_key="voltage_swell_l3_count", - obis_reference=obis_references.VOLTAGE_SWELL_L3_COUNT, + obis_reference="VOLTAGE_SWELL_L3_COUNT", dsmr_versions={"2.2", "4", "5", "5L"}, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -255,7 +254,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="instantaneous_voltage_l1", translation_key="instantaneous_voltage_l1", - obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L1, + obis_reference="INSTANTANEOUS_VOLTAGE_L1", device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, @@ -264,7 +263,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="instantaneous_voltage_l2", translation_key="instantaneous_voltage_l2", - obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L2, + obis_reference="INSTANTANEOUS_VOLTAGE_L2", device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, @@ -273,7 +272,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="instantaneous_voltage_l3", translation_key="instantaneous_voltage_l3", - obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L3, + obis_reference="INSTANTANEOUS_VOLTAGE_L3", device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, @@ -282,7 +281,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="instantaneous_current_l1", translation_key="instantaneous_current_l1", - obis_reference=obis_references.INSTANTANEOUS_CURRENT_L1, + obis_reference="INSTANTANEOUS_CURRENT_L1", device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, @@ -291,7 +290,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="instantaneous_current_l2", translation_key="instantaneous_current_l2", - obis_reference=obis_references.INSTANTANEOUS_CURRENT_L2, + obis_reference="INSTANTANEOUS_CURRENT_L2", device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, @@ -300,7 +299,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="instantaneous_current_l3", translation_key="instantaneous_current_l3", - obis_reference=obis_references.INSTANTANEOUS_CURRENT_L3, + obis_reference="INSTANTANEOUS_CURRENT_L3", device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, @@ -309,7 +308,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="belgium_max_power_per_phase", translation_key="max_power_per_phase", - obis_reference=obis_references.BELGIUM_MAX_POWER_PER_PHASE, + obis_reference="ACTUAL_TRESHOLD_ELECTRICITY", dsmr_versions={"5B"}, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -319,7 +318,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="belgium_max_current_per_phase", translation_key="max_current_per_phase", - obis_reference=obis_references.BELGIUM_MAX_CURRENT_PER_PHASE, + obis_reference="BELGIUM_MAX_CURRENT_PER_PHASE", dsmr_versions={"5B"}, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, @@ -329,7 +328,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="electricity_imported_total", translation_key="electricity_imported_total", - obis_reference=obis_references.ELECTRICITY_IMPORTED_TOTAL, + obis_reference="ELECTRICITY_IMPORTED_TOTAL", dsmr_versions={"5L", "5S", "Q3D"}, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -337,7 +336,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="electricity_exported_total", translation_key="electricity_exported_total", - obis_reference=obis_references.ELECTRICITY_EXPORTED_TOTAL, + obis_reference="ELECTRICITY_EXPORTED_TOTAL", dsmr_versions={"5L", "5S", "Q3D"}, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -345,7 +344,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="belgium_current_average_demand", translation_key="current_average_demand", - obis_reference=obis_references.BELGIUM_CURRENT_AVERAGE_DEMAND, + obis_reference="BELGIUM_CURRENT_AVERAGE_DEMAND", dsmr_versions={"5B"}, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -353,7 +352,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="belgium_maximum_demand_current_month", translation_key="maximum_demand_current_month", - obis_reference=obis_references.BELGIUM_MAXIMUM_DEMAND_MONTH, + obis_reference="BELGIUM_MAXIMUM_DEMAND_MONTH", dsmr_versions={"5B"}, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -361,7 +360,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="hourly_gas_meter_reading", translation_key="gas_meter_reading", - obis_reference=obis_references.HOURLY_GAS_METER_READING, + obis_reference="HOURLY_GAS_METER_READING", dsmr_versions={"4", "5", "5L"}, is_gas=True, device_class=SensorDeviceClass.GAS, @@ -370,7 +369,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="gas_meter_reading", translation_key="gas_meter_reading", - obis_reference=obis_references.GAS_METER_READING, + obis_reference="GAS_METER_READING", dsmr_versions={"2.2"}, is_gas=True, device_class=SensorDeviceClass.GAS, @@ -383,36 +382,20 @@ def create_mbus_entity( mbus: int, mtype: int, telegram: Telegram ) -> DSMRSensorEntityDescription | None: """Create a new MBUS Entity.""" - if ( - mtype == 3 - and ( - obis_reference := getattr( - obis_references, f"BELGIUM_MBUS{mbus}_METER_READING2" - ) - ) - in telegram - ): + if mtype == 3 and hasattr(telegram, f"BELGIUM_MBUS{mbus}_METER_READING2"): return DSMRSensorEntityDescription( key=f"mbus{mbus}_gas_reading", translation_key="gas_meter_reading", - obis_reference=obis_reference, + obis_reference=f"BELGIUM_MBUS{mbus}_METER_READING2", is_gas=True, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, ) - if ( - mtype == 7 - and ( - obis_reference := getattr( - obis_references, f"BELGIUM_MBUS{mbus}_METER_READING1" - ) - ) - in telegram - ): + if mtype == 7 and (hasattr(telegram, f"BELGIUM_MBUS{mbus}_METER_READING1")): return DSMRSensorEntityDescription( key=f"mbus{mbus}_water_reading", translation_key="water_meter_reading", - obis_reference=obis_reference, + obis_reference=f"BELGIUM_MBUS{mbus}_METER_READING1", is_water=True, device_class=SensorDeviceClass.WATER, state_class=SensorStateClass.TOTAL_INCREASING, @@ -425,7 +408,7 @@ def device_class_and_uom( entity_description: DSMRSensorEntityDescription, ) -> tuple[SensorDeviceClass | None, str | None]: """Get native unit of measurement from telegram,.""" - dsmr_object = telegram[entity_description.obis_reference] + dsmr_object = getattr(telegram, entity_description.obis_reference) uom: str | None = getattr(dsmr_object, "unit") or None with suppress(ValueError): if entity_description.device_class == SensorDeviceClass.GAS and ( @@ -484,18 +467,15 @@ def create_mbus_entities( entities = [] for idx in range(1, 5): if ( - device_type := getattr(obis_references, f"BELGIUM_MBUS{idx}_DEVICE_TYPE") - ) not in telegram: + device_type := getattr(telegram, f"BELGIUM_MBUS{idx}_DEVICE_TYPE", None) + ) is None: continue - if (type_ := int(telegram[device_type].value)) not in (3, 7): + if (type_ := int(device_type.value)) not in (3, 7): continue - if ( - identifier := getattr( - obis_references, - f"BELGIUM_MBUS{idx}_EQUIPMENT_IDENTIFIER", - ) - ) in telegram: - serial_ = telegram[identifier].value + if identifier := getattr( + telegram, f"BELGIUM_MBUS{idx}_EQUIPMENT_IDENTIFIER", None + ): + serial_ = identifier.value rename_old_gas_to_mbus(hass, entry, serial_) else: serial_ = "" @@ -547,7 +527,7 @@ async def async_setup_entry( or dsmr_version in description.dsmr_versions ) and (not description.is_gas or CONF_SERIAL_ID_GAS in entry.data) - and description.obis_reference in telegram + and hasattr(telegram, description.obis_reference) ] ) async_add_entities(entities) @@ -756,21 +736,21 @@ class DSMREntity(SensorEntity): """Update data.""" self.telegram = telegram if self.hass and ( - telegram is None or self.entity_description.obis_reference in telegram + telegram is None + or hasattr(telegram, self.entity_description.obis_reference) ): 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.telegram is None - or self.entity_description.obis_reference not in self.telegram + if self.telegram is None or not hasattr( + self.telegram, self.entity_description.obis_reference ): return None # Get the attribute value if the object has it - dsmr_object = self.telegram[self.entity_description.obis_reference] + dsmr_object = getattr(self.telegram, self.entity_description.obis_reference) attr: str | None = getattr(dsmr_object, attribute) return attr @@ -786,10 +766,7 @@ class DSMREntity(SensorEntity): if (value := self.get_dsmr_object_attr("value")) is None: return None - if ( - self.entity_description.obis_reference - == obis_references.ELECTRICITY_ACTIVE_TARIFF - ): + if self.entity_description.obis_reference == "ELECTRICITY_ACTIVE_TARIFF": return self.translate_tariff(value, self._entry.data[CONF_DSMR_VERSION]) with suppress(TypeError): diff --git a/tests/components/dsmr/test_mbus_migration.py b/tests/components/dsmr/test_mbus_migration.py index a8b7ef9c356..cd3db27be8c 100644 --- a/tests/components/dsmr/test_mbus_migration.py +++ b/tests/components/dsmr/test_mbus_migration.py @@ -68,7 +68,7 @@ async def test_migrate_gas_to_mbus( telegram = Telegram() telegram.add( BELGIUM_MBUS1_DEVICE_TYPE, - CosemObject((0, 0), [{"value": "003", "unit": ""}]), + CosemObject((0, 1), [{"value": "003", "unit": ""}]), "BELGIUM_MBUS1_DEVICE_TYPE", ) telegram.add( diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index a7c4a98be1e..5b0cf6d7a15 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -11,6 +11,7 @@ from decimal import Decimal from itertools import chain, repeat from unittest.mock import DEFAULT, MagicMock +from dsmr_parser import obis_references from dsmr_parser.obis_references import ( BELGIUM_CURRENT_AVERAGE_DEMAND, BELGIUM_MAXIMUM_DEMAND_MONTH, @@ -40,6 +41,7 @@ from dsmr_parser.obis_references import ( from dsmr_parser.objects import CosemObject, MBusObject, Telegram import pytest +from homeassistant.components.dsmr.sensor import SENSORS from homeassistant.components.sensor import ( ATTR_OPTIONS, ATTR_STATE_CLASS, @@ -585,7 +587,7 @@ async def test_belgian_meter( ) telegram.add( BELGIUM_MBUS2_DEVICE_TYPE, - CosemObject((0, 0), [{"value": "007", "unit": ""}]), + CosemObject((0, 2), [{"value": "007", "unit": ""}]), "BELGIUM_MBUS2_DEVICE_TYPE", ) telegram.add( @@ -609,7 +611,7 @@ async def test_belgian_meter( ) telegram.add( BELGIUM_MBUS3_DEVICE_TYPE, - CosemObject((0, 0), [{"value": "003", "unit": ""}]), + CosemObject((0, 3), [{"value": "003", "unit": ""}]), "BELGIUM_MBUS3_DEVICE_TYPE", ) telegram.add( @@ -633,7 +635,7 @@ async def test_belgian_meter( ) telegram.add( BELGIUM_MBUS4_DEVICE_TYPE, - CosemObject((0, 0), [{"value": "007", "unit": ""}]), + CosemObject((0, 4), [{"value": "007", "unit": ""}]), "BELGIUM_MBUS4_DEVICE_TYPE", ) telegram.add( @@ -776,7 +778,7 @@ async def test_belgian_meter_alt( telegram = Telegram() telegram.add( BELGIUM_MBUS1_DEVICE_TYPE, - CosemObject((0, 0), [{"value": "007", "unit": ""}]), + CosemObject((0, 1), [{"value": "007", "unit": ""}]), "BELGIUM_MBUS1_DEVICE_TYPE", ) telegram.add( @@ -785,7 +787,7 @@ async def test_belgian_meter_alt( (0, 1), [{"value": "37464C4F32313139303333373331", "unit": ""}], ), - "BELGIUM_MBUS1_DEVICE_TYPE", + "BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER", ) telegram.add( BELGIUM_MBUS1_METER_READING1, @@ -800,7 +802,7 @@ async def test_belgian_meter_alt( ) telegram.add( BELGIUM_MBUS2_DEVICE_TYPE, - CosemObject((0, 0), [{"value": "003", "unit": ""}]), + CosemObject((0, 2), [{"value": "003", "unit": ""}]), "BELGIUM_MBUS2_DEVICE_TYPE", ) telegram.add( @@ -820,11 +822,11 @@ async def test_belgian_meter_alt( {"value": Decimal(678.901), "unit": "m3"}, ], ), - BELGIUM_MBUS2_METER_READING2, + "BELGIUM_MBUS2_METER_READING2", ) telegram.add( BELGIUM_MBUS3_DEVICE_TYPE, - CosemObject((0, 0), [{"value": "007", "unit": ""}]), + CosemObject((0, 3), [{"value": "007", "unit": ""}]), "BELGIUM_MBUS3_DEVICE_TYPE", ) telegram.add( @@ -848,7 +850,7 @@ async def test_belgian_meter_alt( ) telegram.add( BELGIUM_MBUS4_DEVICE_TYPE, - CosemObject((0, 0), [{"value": "003", "unit": ""}]), + CosemObject((0, 4), [{"value": "003", "unit": ""}]), "BELGIUM_MBUS4_DEVICE_TYPE", ) telegram.add( @@ -969,7 +971,7 @@ async def test_belgian_meter_mbus( ) telegram.add( BELGIUM_MBUS1_DEVICE_TYPE, - CosemObject((0, 0), [{"value": "006", "unit": ""}]), + CosemObject((0, 1), [{"value": "006", "unit": ""}]), "BELGIUM_MBUS1_DEVICE_TYPE", ) telegram.add( @@ -982,7 +984,7 @@ async def test_belgian_meter_mbus( ) telegram.add( BELGIUM_MBUS2_DEVICE_TYPE, - CosemObject((0, 0), [{"value": "003", "unit": ""}]), + CosemObject((0, 2), [{"value": "003", "unit": ""}]), "BELGIUM_MBUS2_DEVICE_TYPE", ) telegram.add( @@ -995,7 +997,7 @@ async def test_belgian_meter_mbus( ) telegram.add( BELGIUM_MBUS3_DEVICE_TYPE, - CosemObject((0, 0), [{"value": "007", "unit": ""}]), + CosemObject((0, 3), [{"value": "007", "unit": ""}]), "BELGIUM_MBUS3_DEVICE_TYPE", ) telegram.add( @@ -1019,7 +1021,7 @@ async def test_belgian_meter_mbus( ) telegram.add( BELGIUM_MBUS4_DEVICE_TYPE, - CosemObject((0, 0), [{"value": "007", "unit": ""}]), + CosemObject((0, 4), [{"value": "007", "unit": ""}]), "BELGIUM_MBUS4_DEVICE_TYPE", ) telegram.add( @@ -1518,3 +1520,9 @@ async def test_gas_meter_providing_energy_reading( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.GIGA_JOULE ) + + +def test_all_obis_references_exists(): + """Verify that all attributes exist by name in database.""" + for sensor in SENSORS: + assert hasattr(obis_references, sensor.obis_reference) From 489457c47b6c9fdac34b23356032c516b9a627fd Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Mon, 22 Jul 2024 21:47:01 +0200 Subject: [PATCH 1470/2411] Add async_update_data to emoncms coordinator (#122416) * Add async_update_data to coordinator * Add const module --- homeassistant/components/emoncms/const.py | 11 +++++++++++ homeassistant/components/emoncms/coordinator.py | 16 +++++++++++----- homeassistant/components/emoncms/sensor.py | 10 +++------- 3 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/emoncms/const.py diff --git a/homeassistant/components/emoncms/const.py b/homeassistant/components/emoncms/const.py new file mode 100644 index 00000000000..dc43e7a07dc --- /dev/null +++ b/homeassistant/components/emoncms/const.py @@ -0,0 +1,11 @@ +"""Constants for the emoncms integration.""" + +import logging + +CONF_EXCLUDE_FEEDID = "exclude_feed_id" +CONF_ONLY_INCLUDE_FEEDID = "include_only_feed_id" +CONF_MESSAGE = "message" +CONF_SUCCESS = "success" + + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/emoncms/coordinator.py b/homeassistant/components/emoncms/coordinator.py index 16258a11f4d..d1f6a2858c7 100644 --- a/homeassistant/components/emoncms/coordinator.py +++ b/homeassistant/components/emoncms/coordinator.py @@ -1,15 +1,14 @@ """DataUpdateCoordinator for the emoncms integration.""" from datetime import timedelta -import logging from typing import Any from pyemoncms import EmoncmsClient from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -_LOGGER = logging.getLogger(__name__) +from .const import CONF_MESSAGE, CONF_SUCCESS, LOGGER class EmoncmsCoordinator(DataUpdateCoordinator[list[dict[str, Any]] | None]): @@ -24,8 +23,15 @@ class EmoncmsCoordinator(DataUpdateCoordinator[list[dict[str, Any]] | None]): """Initialize the emoncms data coordinator.""" super().__init__( hass, - _LOGGER, + LOGGER, name="emoncms_coordinator", - update_method=emoncms_client.async_list_feeds, update_interval=scan_interval, ) + self.emoncms_client = emoncms_client + + async def _async_update_data(self) -> list[dict[str, Any]]: + """Fetch data from API endpoint.""" + data = await self.emoncms_client.async_request("/feed/list.json") + if not data[CONF_SUCCESS]: + raise UpdateFailed + return data[CONF_MESSAGE] diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index e239ffd6c21..c299c5a1b9f 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import timedelta -import logging from typing import Any from pyemoncms import EmoncmsClient @@ -33,10 +32,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .const import CONF_EXCLUDE_FEEDID, CONF_ONLY_INCLUDE_FEEDID from .coordinator import EmoncmsCoordinator -_LOGGER = logging.getLogger(__name__) - ATTR_FEEDID = "FeedId" ATTR_FEEDNAME = "FeedName" ATTR_LASTUPDATETIME = "LastUpdated" @@ -45,8 +43,6 @@ ATTR_SIZE = "Size" ATTR_TAG = "Tag" ATTR_USERID = "UserId" -CONF_EXCLUDE_FEEDID = "exclude_feed_id" -CONF_ONLY_INCLUDE_FEEDID = "include_only_feed_id" CONF_SENSOR_NAMES = "sensor_names" DECIMALS = 2 @@ -98,7 +94,7 @@ async def async_setup_platform( coordinator = EmoncmsCoordinator(hass, emoncms_client, scan_interval) await coordinator.async_refresh() elems = coordinator.data - if elems is None: + if not elems: return sensors: list[EmonCmsSensor] = [] @@ -208,7 +204,7 @@ class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity): self._attr_native_value = None if self._value_template is not None: self._attr_native_value = ( - self._value_template.render_with_possible_json_value( + self._value_template.async_render_with_possible_json_value( elem["value"], STATE_UNKNOWN ) ) From ee30510b2303f75ee36855c419bea24e15f8d739 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Mon, 22 Jul 2024 22:57:48 +0300 Subject: [PATCH 1471/2411] Remove deprecated DALL-E image formats (#122388) --- .../openai_conversation/__init__.py | 33 ++----------------- .../openai_conversation/test_init.py | 27 --------------- 2 files changed, 3 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index 0ba7b53795b..75b5db23094 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -2,8 +2,6 @@ from __future__ import annotations -from typing import Literal, cast - import openai import voluptuous as vol @@ -20,11 +18,7 @@ from homeassistant.exceptions import ( HomeAssistantError, ServiceValidationError, ) -from homeassistant.helpers import ( - config_validation as cv, - issue_registry as ir, - selector, -) +from homeassistant.helpers import config_validation as cv, selector from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, LOGGER @@ -53,32 +47,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: client: openai.AsyncClient = entry.runtime_data - if call.data["size"] in ("256", "512", "1024"): - ir.async_create_issue( - hass, - DOMAIN, - "image_size_deprecated_format", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - is_persistent=True, - learn_more_url="https://www.home-assistant.io/integrations/openai_conversation/", - severity=ir.IssueSeverity.WARNING, - translation_key="image_size_deprecated_format", - ) - size = "1024x1024" - else: - size = call.data["size"] - - size = cast( - Literal["256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"], - size, - ) # size is selector, so no need to check further - try: response = await client.images.generate( model="dall-e-3", prompt=call.data["prompt"], - size=size, + size=call.data["size"], quality=call.data["quality"], style=call.data["style"], response_format="url", @@ -102,7 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ), vol.Required("prompt"): cv.string, vol.Optional("size", default="1024x1024"): vol.In( - ("1024x1024", "1024x1792", "1792x1024", "256", "512", "1024") + ("1024x1024", "1024x1792", "1792x1024") ), vol.Optional("quality", default="standard"): vol.In(("standard", "hd")), vol.Optional("style", default="vivid"): vol.In(("vivid", "natural")), diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index c9431aa1083..d78ce398c92 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -60,33 +60,6 @@ from tests.common import MockConfigEntry "style": "natural", }, ), - ( - {"prompt": "Picture of a dog", "size": "256"}, - { - "prompt": "Picture of a dog", - "size": "1024x1024", - "quality": "standard", - "style": "vivid", - }, - ), - ( - {"prompt": "Picture of a dog", "size": "512"}, - { - "prompt": "Picture of a dog", - "size": "1024x1024", - "quality": "standard", - "style": "vivid", - }, - ), - ( - {"prompt": "Picture of a dog", "size": "1024"}, - { - "prompt": "Picture of a dog", - "size": "1024x1024", - "quality": "standard", - "style": "vivid", - }, - ), ], ) async def test_generate_image_service( From 3df6b34a032e5fd21da3e91e0376d4194af770e0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Jul 2024 23:07:49 +0200 Subject: [PATCH 1472/2411] Split recorder and frontend bootstrap steps (#122420) --- homeassistant/bootstrap.py | 6 ++++-- homeassistant/components/recorder/manifest.json | 1 - tests/test_bootstrap.py | 3 --- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a16fd1fa3e9..43f4d451497 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -223,8 +223,10 @@ CRITICAL_INTEGRATIONS = { SETUP_ORDER = ( # Load logging and http deps as soon as possible ("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS), - # Setup frontend and recorder - ("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}), + # Setup frontend + ("frontend", FRONTEND_INTEGRATIONS), + # Setup recorder + ("recorder", RECORDER_INTEGRATIONS), # Start up debuggers. Start these first in case they want to wait. ("debugger", DEBUGGER_INTEGRATIONS), ) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index febd1bb8c7c..7d5576e4672 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -1,7 +1,6 @@ { "domain": "recorder", "name": "Recorder", - "after_dependencies": ["http"], "codeowners": ["@home-assistant/core"], "documentation": "https://www.home-assistant.io/integrations/recorder", "integration_type": "system", diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 153bb9a07f7..278bfc631fd 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -436,9 +436,6 @@ async def test_setup_frontend_before_recorder(hass: HomeAssistant) -> None: MockModule( domain="recorder", async_setup=gen_domain_setup("recorder"), - partial_manifest={ - "after_dependencies": ["http"], - }, ), ) From ba276a5cb69a296d34a41e91ef802d9b28f0ea64 Mon Sep 17 00:00:00 2001 From: ribbal <30695106+ribbal@users.noreply.github.com> Date: Mon, 22 Jul 2024 22:15:36 +0100 Subject: [PATCH 1473/2411] Add missing binary sensors to Hive integration (#122296) * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors --- .../components/hive/binary_sensor.py | 68 ++++++++++++++++--- homeassistant/components/hive/icons.json | 13 ++++ 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 8e64afa1771..512b06ece6d 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -47,6 +47,25 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( ), ) +SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="Heating_State", + translation_key="heating", + ), + BinarySensorEntityDescription( + key="Heating_Boost", + translation_key="heating", + ), + BinarySensorEntityDescription( + key="Hotwater_State", + translation_key="hot_water", + ), + BinarySensorEntityDescription( + key="Hotwater_Boost", + translation_key="hot_water", + ), +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -54,19 +73,27 @@ async def async_setup_entry( """Set up Hive thermostat based on a config entry.""" hive = hass.data[DOMAIN][entry.entry_id] + + sensors: list[BinarySensorEntity] = [] + devices = hive.session.deviceList.get("binary_sensor") - if not devices: - return - async_add_entities( - ( - HiveBinarySensorEntity(hive, dev, description) - for dev in devices - for description in BINARY_SENSOR_TYPES - if dev["hiveType"] == description.key - ), - True, + sensors.extend( + HiveBinarySensorEntity(hive, dev, description) + for dev in devices + for description in BINARY_SENSOR_TYPES + if dev["hiveType"] == description.key ) + devices = hive.session.deviceList.get("sensor") + sensors.extend( + HiveSensorEntity(hive, dev, description) + for dev in devices + for description in SENSOR_TYPES + if dev["hiveType"] == description.key + ) + + async_add_entities(sensors, True) + class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): """Representation of a Hive binary sensor.""" @@ -91,3 +118,24 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): self._attr_available = self.device["deviceData"].get("online") else: self._attr_available = True + + +class HiveSensorEntity(HiveEntity, BinarySensorEntity): + """Hive Sensor Entity.""" + + def __init__( + self, + hive: Hive, + hive_device: dict[str, Any], + entity_description: BinarySensorEntityDescription, + ) -> None: + """Initialise hive sensor.""" + super().__init__(hive, hive_device) + self.entity_description = entity_description + + async def async_update(self) -> None: + """Update all Node data from Hive.""" + await self.hive.session.updateData(self.device) + self.device = await self.hive.sensor.getSensor(self.device) + self._attr_is_on = self.device["status"]["state"] == "ON" + self._attr_available = self.device["deviceData"].get("online") diff --git a/homeassistant/components/hive/icons.json b/homeassistant/components/hive/icons.json index 671426f6253..186724cd563 100644 --- a/homeassistant/components/hive/icons.json +++ b/homeassistant/components/hive/icons.json @@ -1,4 +1,17 @@ { + "entity": { + "binary_sensor": { + "heating": { + "default": "mdi:radiator" + }, + "hot_water": { + "default": "mdi:hand-water" + }, + "temperature": { + "default": "mdi:thermometer" + } + } + }, "services": { "boost_heating_on": "mdi:radiator", "boost_heating_off": "mdi:radiator-off", From 42716723e6b4c95b2d6d03d4c4740cf35c9123f4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Jul 2024 23:26:52 +0200 Subject: [PATCH 1474/2411] Register WS command recorder/info early (#122425) --- .../recorder/basic_websocket_api.py | 56 +++++++++++++++++++ .../components/recorder/websocket_api.py | 39 ------------- homeassistant/helpers/recorder.py | 4 ++ .../components/recorder/test_websocket_api.py | 2 +- 4 files changed, 61 insertions(+), 40 deletions(-) create mode 100644 homeassistant/components/recorder/basic_websocket_api.py diff --git a/homeassistant/components/recorder/basic_websocket_api.py b/homeassistant/components/recorder/basic_websocket_api.py new file mode 100644 index 00000000000..9cbc77b30c0 --- /dev/null +++ b/homeassistant/components/recorder/basic_websocket_api.py @@ -0,0 +1,56 @@ +"""The Recorder websocket API.""" + +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +from .util import get_instance + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the recorder websocket API.""" + websocket_api.async_register_command(hass, ws_info) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/info", + } +) +@callback +def ws_info( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Return status of the recorder.""" + if instance := get_instance(hass): + backlog = instance.backlog + migration_in_progress = instance.migration_in_progress + migration_is_live = instance.migration_is_live + recording = instance.recording + # We avoid calling is_alive() as it can block waiting + # for the thread state lock which will block the event loop. + is_running = instance.is_running + max_backlog = instance.max_backlog + else: + backlog = None + migration_in_progress = False + migration_is_live = False + recording = False + is_running = False + max_backlog = None + + recorder_info = { + "backlog": backlog, + "max_backlog": max_backlog, + "migration_in_progress": migration_in_progress, + "migration_is_live": migration_is_live, + "recording": recording, + "thread_running": is_running, + } + connection.send_result(msg["id"], recorder_info) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 195d3d3efb0..5e0eef37721 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -79,7 +79,6 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_get_statistics_metadata) websocket_api.async_register_command(hass, ws_list_statistic_ids) websocket_api.async_register_command(hass, ws_import_statistics) - websocket_api.async_register_command(hass, ws_info) websocket_api.async_register_command(hass, ws_update_statistics_metadata) websocket_api.async_register_command(hass, ws_validate_statistics) @@ -475,41 +474,3 @@ def ws_import_statistics( else: async_add_external_statistics(hass, metadata, stats) connection.send_result(msg["id"]) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "recorder/info", - } -) -@callback -def ws_info( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] -) -> None: - """Return status of the recorder.""" - if instance := get_instance(hass): - backlog = instance.backlog - migration_in_progress = instance.migration_in_progress - migration_is_live = instance.migration_is_live - recording = instance.recording - # We avoid calling is_alive() as it can block waiting - # for the thread state lock which will block the event loop. - is_running = instance.is_running - max_backlog = instance.max_backlog - else: - backlog = None - migration_in_progress = False - migration_is_live = False - recording = False - is_running = False - max_backlog = None - - recorder_info = { - "backlog": backlog, - "max_backlog": max_backlog, - "migration_in_progress": migration_in_progress, - "migration_is_live": migration_is_live, - "recording": recording, - "thread_running": is_running, - } - connection.send_result(msg["id"], recorder_info) diff --git a/homeassistant/helpers/recorder.py b/homeassistant/helpers/recorder.py index 6155fc9b320..ac534a7230a 100644 --- a/homeassistant/helpers/recorder.py +++ b/homeassistant/helpers/recorder.py @@ -33,7 +33,11 @@ def async_migration_in_progress(hass: HomeAssistant) -> bool: @callback def async_initialize_recorder(hass: HomeAssistant) -> None: """Initialize recorder data.""" + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.recorder.basic_websocket_api import async_setup + hass.data[DOMAIN] = RecorderData() + async_setup(hass) async def async_wait_recorder(hass: HomeAssistant) -> bool: diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 1bf56372620..bcdf07502b0 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -2491,7 +2491,7 @@ async def test_recorder_info_no_instance( client = await hass_ws_client() with patch( - "homeassistant.components.recorder.websocket_api.get_instance", + "homeassistant.components.recorder.basic_websocket_api.get_instance", return_value=None, ): await client.send_json_auto_id({"type": "recorder/info"}) From 9b2118e556c167575da70f0a5f474b53c280b78a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Jul 2024 23:50:05 +0200 Subject: [PATCH 1475/2411] Remove recorder from websocket_api after dependencies (#122422) Co-authored-by: J. Nick Koston --- homeassistant/components/websocket_api/manifest.json | 1 - tests/test_requirements.py | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/websocket_api/manifest.json b/homeassistant/components/websocket_api/manifest.json index 116bd0ccee8..315411ea4cf 100644 --- a/homeassistant/components/websocket_api/manifest.json +++ b/homeassistant/components/websocket_api/manifest.json @@ -1,7 +1,6 @@ { "domain": "websocket_api", "name": "Home Assistant WebSocket API", - "after_dependencies": ["recorder"], "codeowners": ["@home-assistant/core"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/websocket_api", diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 161214160aa..2885fa30036 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -602,12 +602,12 @@ async def test_discovery_requirements_ssdp(hass: HomeAssistant) -> None: ) as mock_process: await async_get_integration_with_requirements(hass, "ssdp_comp") - assert len(mock_process.mock_calls) == 3 + assert len(mock_process.mock_calls) == 2 assert mock_process.mock_calls[0][1][1] == ssdp.requirements assert { + mock_process.mock_calls[0][1][0], mock_process.mock_calls[1][1][0], - mock_process.mock_calls[2][1][0], - } == {"network", "recorder"} + } == {"network", "ssdp"} @pytest.mark.parametrize( @@ -631,7 +631,7 @@ async def test_discovery_requirements_zeroconf( ) as mock_process: await async_get_integration_with_requirements(hass, "comp") - assert len(mock_process.mock_calls) == 3 + assert len(mock_process.mock_calls) == 2 assert mock_process.mock_calls[0][1][1] == zeroconf.requirements From d0ba5534cd0a29440ceafa2a2a7a1c5b03655992 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Jul 2024 17:04:29 -0500 Subject: [PATCH 1476/2411] Bump async-upnp-client to 0.40.0 (#122427) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 963a22850df..1120ec3a2f1 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "iot_class": "local_push", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.39.0", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.40.0", "getmac==0.9.4"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index e02326376b3..62defe0e2e3 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["async-upnp-client==0.39.0"], + "requirements": ["async-upnp-client==0.40.0"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 7d9a8a9a0a8..aecde9e4c26 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -39,7 +39,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.6.0", "wakeonlan==2.1.0", - "async-upnp-client==0.39.0" + "async-upnp-client==0.40.0" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 304ee4b6410..8b94b8c5895 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["async_upnp_client"], "quality_scale": "internal", - "requirements": ["async-upnp-client==0.39.0"] + "requirements": ["async-upnp-client==0.40.0"] } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index b2972fc7790..30054af0512 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.39.0", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.40.0", "getmac==0.9.4"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 4c63ab79baf..efb08e26b5a 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], "quality_scale": "platinum", - "requirements": ["yeelight==0.7.14", "async-upnp-client==0.39.0"], + "requirements": ["yeelight==0.7.14", "async-upnp-client==0.40.0"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3eaa0b06619..c013415c794 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 async-interrupt==1.1.2 -async-upnp-client==0.39.0 +async-upnp-client==0.40.0 atomicwrites-homeassistant==1.4.1 attrs==23.2.0 awesomeversion==24.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3ffe9b0abc2..fab0282bc3e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -487,7 +487,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.39.0 +async-upnp-client==0.40.0 # homeassistant.components.arve asyncarve==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ebc242ac47..3af90c5675e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.39.0 +async-upnp-client==0.40.0 # homeassistant.components.arve asyncarve==0.1.1 From 96de0a4c949a133bba29a4bbd24daff0a2124ffa Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 23 Jul 2024 00:30:31 +0200 Subject: [PATCH 1477/2411] Correct off-by-one bug in recorder non live schema migration (#122428) * Correct off-by-one bug in recorder non live schema migration * Remove change from the future --- homeassistant/components/recorder/migration.py | 2 +- tests/components/recorder/test_migrate.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 138d4530bb9..3ef9b65e259 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -304,7 +304,7 @@ def migrate_schema_non_live( schema_status: SchemaValidationStatus, ) -> SchemaValidationStatus: """Check if the schema needs to be upgraded.""" - end_version = LIVE_MIGRATION_MIN_SCHEMA_VERSION - 1 + end_version = LIVE_MIGRATION_MIN_SCHEMA_VERSION return _migrate_schema( instance, hass, engine, session_maker, schema_status, end_version ) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index e287b150fa6..d753f908e76 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -71,6 +71,10 @@ async def test_schema_update_calls( "homeassistant.components.recorder.migration._apply_update", wraps=migration._apply_update, ) as update, + patch( + "homeassistant.components.recorder.migration._migrate_schema", + wraps=migration._migrate_schema, + ) as migrate_schema, ): await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) @@ -85,6 +89,11 @@ async def test_schema_update_calls( for version in range(db_schema.SCHEMA_VERSION) ] ) + migrate_schema.assert_has_calls( + [ + call(instance, hass, engine, session_maker, ANY, db_schema.SCHEMA_VERSION), + ] + ) async def test_migration_in_progress( From f4125eaf4c2c65d3a57b109e0d4b8c3d7b96843e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 23 Jul 2024 00:56:06 +0200 Subject: [PATCH 1478/2411] Remove loop shutdown indicator when done with test hass (#122432) --- tests/common.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/common.py b/tests/common.py index 55f7cadfd4b..64e11ee7b51 100644 --- a/tests/common.py +++ b/tests/common.py @@ -13,7 +13,7 @@ from collections.abc import ( Mapping, Sequence, ) -from contextlib import asynccontextmanager, contextmanager +from contextlib import asynccontextmanager, contextmanager, suppress from datetime import UTC, datetime, timedelta from enum import Enum import functools as ft @@ -91,7 +91,10 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.json import JSONEncoder, _orjson_default_encoder, json_dumps from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.async_ import ( + _SHUTDOWN_RUN_CALLBACK_THREADSAFE, + run_callback_threadsafe, +) import homeassistant.util.dt as dt_util from homeassistant.util.event_type import EventType from homeassistant.util.json import ( @@ -376,6 +379,9 @@ async def async_test_home_assistant( finally: # Restore timezone, it is set when creating the hass object dt_util.set_default_time_zone(orig_tz) + # Remove loop shutdown indicator to not interfere with additional hass objects + with suppress(AttributeError): + delattr(hass.loop, _SHUTDOWN_RUN_CALLBACK_THREADSAFE) def async_mock_service( From 5d3c57ecfe275e6e1afee3713381a3721c7d848d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 23 Jul 2024 01:48:55 +0200 Subject: [PATCH 1479/2411] Freeze integration setup timeout for recorder during non-live migration (#122431) --- homeassistant/components/recorder/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 3024eb9507f..31c36be9c88 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -950,7 +950,8 @@ class Recorder(threading.Thread): self, schema_status: migration.SchemaValidationStatus ) -> tuple[bool, migration.SchemaValidationStatus]: """Migrate schema to the latest version.""" - return self._migrate_schema(schema_status, False) + with self.hass.timeout.freeze(DOMAIN): + return self._migrate_schema(schema_status, False) def _migrate_schema_live( self, schema_status: migration.SchemaValidationStatus From 975cfa6457e051c5a58054e03046493baa96e7d6 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Tue, 23 Jul 2024 03:56:13 +0300 Subject: [PATCH 1480/2411] Fix gemini api format conversion (#122403) * Fix gemini api format conversion * add tests * fix tests * fix tests * fix coverage --- .../conversation.py | 18 +++++++++++++++++- .../snapshots/test_conversation.ambr | 18 ++++++++++++++++++ .../test_conversation.py | 4 +++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 4a93f6ca569..8dec62ad26b 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -73,6 +73,14 @@ SUPPORTED_SCHEMA_KEYS = { def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: """Format the schema to protobuf.""" + if (subschemas := schema.get("anyOf")) or (subschemas := schema.get("allOf")): + for subschema in subschemas: # Gemini API does not support anyOf and allOf keys + if "type" in subschema: # Fallback to first subschema with 'type' field + return _format_schema(subschema) + return _format_schema( + subschemas[0] + ) # Or, if not found, to any of the subschemas + result = {} for key, val in schema.items(): if key not in SUPPORTED_SCHEMA_KEYS: @@ -81,7 +89,9 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: key = "type_" val = val.upper() elif key == "format": - if schema.get("type") == "string" and val != "enum": + if (schema.get("type") == "string" and val != "enum") or ( + schema.get("type") not in ("number", "integer", "string") + ): continue key = "format_" elif key == "items": @@ -89,6 +99,12 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: elif key == "properties": val = {k: _format_schema(v) for k, v in val.items()} result[key] = val + + if result.get("type_") == "OBJECT" and not result.get("properties"): + # An object with undefined properties is not supported by Gemini API. + # Fallback to JSON string. This will probably fail for most tools that want it, + # but we don't have a better fallback strategy so far. + result["properties"] = {"json": {"type_": "STRING"}} return result diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index 66caf4c7218..abd3658e869 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -442,6 +442,24 @@ description: "Test function" parameters { type_: OBJECT + properties { + key: "param3" + value { + type_: OBJECT + properties { + key: "json" + value { + type_: STRING + } + } + } + } + properties { + key: "param2" + value { + type_: NUMBER + } + } properties { key: "param1" value { diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index dc713f09454..afeb6d01faa 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -185,7 +185,9 @@ async def test_function_call( { vol.Optional("param1", description="Test parameters"): [ vol.All(str, vol.Lower) - ] + ], + vol.Optional("param2"): vol.Any(float, int), + vol.Optional("param3"): dict, } ) From 0039f1bb493153c28a6d4e0991b0f7aa69a840b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Jul 2024 01:39:18 -0500 Subject: [PATCH 1481/2411] Make frontend url and route a cached_property (#122430) --- homeassistant/components/frontend/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 6b0d69ba99d..5b462842e4a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Iterator -from functools import lru_cache, partial +from functools import cached_property, lru_cache, partial import logging import os import pathlib @@ -588,12 +588,12 @@ class IndexView(web_urldispatcher.AbstractResource): self.hass = hass self._template_cache: jinja2.Template | None = None - @property + @cached_property def canonical(self) -> str: """Return resource's canonical path.""" return "/" - @property + @cached_property def _route(self) -> web_urldispatcher.ResourceRoute: """Return the index route.""" return web_urldispatcher.ResourceRoute("GET", self.get, self) From 4ee256633bdd8be3f0c6116172855d134321d0a4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 23 Jul 2024 08:40:30 +0200 Subject: [PATCH 1482/2411] Deduplicate light data with deCONZ websocket fixture (#122421) --- tests/components/deconz/conftest.py | 13 +++++++++ .../deconz/test_alarm_control_panel.py | 16 ---------- tests/components/deconz/test_binary_sensor.py | 7 ----- tests/components/deconz/test_climate.py | 29 ------------------- tests/components/deconz/test_cover.py | 6 ++-- tests/components/deconz/test_deconz_event.py | 20 ------------- .../components/deconz/test_device_trigger.py | 2 -- tests/components/deconz/test_fan.py | 26 +++++------------ tests/components/deconz/test_light.py | 13 +++------ tests/components/deconz/test_lock.py | 8 ++--- tests/components/deconz/test_number.py | 1 - tests/components/deconz/test_scene.py | 2 -- tests/components/deconz/test_sensor.py | 8 ----- tests/components/deconz/test_siren.py | 6 ++-- tests/components/deconz/test_switch.py | 6 ++-- 15 files changed, 32 insertions(+), 131 deletions(-) diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py index 5ff8aba6f60..19e0418c1a3 100644 --- a/tests/components/deconz/conftest.py +++ b/tests/components/deconz/conftest.py @@ -269,6 +269,19 @@ def fixture_websocket_data(_mock_websocket: _WebsocketMock) -> WebsocketDataType return change_websocket_data +@pytest.fixture(name="light_ws_data") +def fixture_light_websocket_data( + mock_websocket_data: WebsocketDataType, +) -> WebsocketDataType: + """Fixture to send light data over websocket.""" + + async def send_light_data(data: dict[str, Any]) -> None: + """Send light data on the websocket.""" + await mock_websocket_data({"r": "lights"} | data) + + return send_light_data + + @pytest.fixture(name="sensor_ws_data") def fixture_sensor_websocket_data( mock_websocket_data: WebsocketDataType, diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py index 76b35fd06da..3c901d94593 100644 --- a/tests/components/deconz/test_alarm_control_panel.py +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -111,15 +111,11 @@ async def test_alarm_control_panel( # Event signals alarm control panel armed away await sensor_ws_data({"state": {"panel": AncillaryControlPanel.ARMED_AWAY}}) - await hass.async_block_till_done() - assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_AWAY # Event signals alarm control panel armed night await sensor_ws_data({"state": {"panel": AncillaryControlPanel.ARMED_NIGHT}}) - await hass.async_block_till_done() - assert ( hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_NIGHT ) @@ -127,15 +123,11 @@ async def test_alarm_control_panel( # Event signals alarm control panel armed home await sensor_ws_data({"state": {"panel": AncillaryControlPanel.ARMED_STAY}}) - await hass.async_block_till_done() - assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_HOME # Event signals alarm control panel disarmed await sensor_ws_data({"state": {"panel": AncillaryControlPanel.DISARMED}}) - await hass.async_block_till_done() - assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_DISARMED # Event signals alarm control panel arming @@ -146,8 +138,6 @@ async def test_alarm_control_panel( AncillaryControlPanel.ARMING_STAY, ): await sensor_ws_data({"state": {"panel": arming_event}}) - await hass.async_block_till_done() - assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMING # Event signals alarm control panel pending @@ -157,8 +147,6 @@ async def test_alarm_control_panel( AncillaryControlPanel.EXIT_DELAY, ): await sensor_ws_data({"state": {"panel": pending_event}}) - await hass.async_block_till_done() - assert ( hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_PENDING ) @@ -166,15 +154,11 @@ async def test_alarm_control_panel( # Event signals alarm control panel triggered await sensor_ws_data({"state": {"panel": AncillaryControlPanel.IN_ALARM}}) - await hass.async_block_till_done() - assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_TRIGGERED # Event signals alarm control panel unknown state keeps previous state await sensor_ws_data({"state": {"panel": AncillaryControlPanel.NOT_READY}}) - await hass.async_block_till_done() - assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_TRIGGERED # Verify service calls diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index a40a1175f5b..18939a816e4 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -493,7 +493,6 @@ async def test_binary_sensors( # Change state await sensor_ws_data({"state": expected["websocket_event"]}) - await hass.async_block_till_done() assert hass.states.get(expected["entity_id"]).state == expected["next_state"] # Unload entry @@ -611,8 +610,6 @@ async def test_add_new_binary_sensor( }, } await sensor_ws_data(event_added_sensor) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 assert hass.states.get("binary_sensor.presence_sensor").state == STATE_OFF @@ -640,8 +637,6 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_service_call( assert len(hass.states.async_all()) == 0 await sensor_ws_data({"e": "added", "sensor": sensor}) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 assert not hass.states.get("binary_sensor.presence_sensor") @@ -687,8 +682,6 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_options_change( assert len(hass.states.async_all()) == 0 await sensor_ws_data({"e": "added", "sensor": sensor}) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 assert not hass.states.get("binary_sensor.presence_sensor") diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 14d21b0a281..2188b1be475 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -111,8 +111,6 @@ async def test_simple_climate_device( # Event signals thermostat configured off await sensor_ws_data({"state": {"on": False}}) - await hass.async_block_till_done() - assert hass.states.get("climate.thermostat").state == STATE_OFF assert ( hass.states.get("climate.thermostat").attributes["hvac_action"] @@ -122,8 +120,6 @@ async def test_simple_climate_device( # Event signals thermostat state on await sensor_ws_data({"state": {"on": True}}) - await hass.async_block_till_done() - assert hass.states.get("climate.thermostat").state == HVACMode.HEAT assert ( hass.states.get("climate.thermostat").attributes["hvac_action"] @@ -212,8 +208,6 @@ async def test_climate_device_without_cooling_support( # Event signals thermostat configured off await sensor_ws_data({"config": {"mode": "off"}}) - await hass.async_block_till_done() - assert hass.states.get("climate.thermostat").state == STATE_OFF assert ( hass.states.get("climate.thermostat").attributes["hvac_action"] @@ -223,8 +217,6 @@ async def test_climate_device_without_cooling_support( # Event signals thermostat state on await sensor_ws_data({"config": {"mode": "other"}, "state": {"on": True}}) - await hass.async_block_till_done() - assert hass.states.get("climate.thermostat").state == HVACMode.HEAT assert ( hass.states.get("climate.thermostat").attributes["hvac_action"] @@ -234,8 +226,6 @@ async def test_climate_device_without_cooling_support( # Event signals thermostat state off await sensor_ws_data({"state": {"on": False}}) - await hass.async_block_till_done() - assert hass.states.get("climate.thermostat").state == STATE_OFF assert ( hass.states.get("climate.thermostat").attributes["hvac_action"] @@ -378,9 +368,6 @@ async def test_climate_device_with_cooling_support( # Event signals thermostat mode cool await sensor_ws_data({"config": {"mode": "cool"}}) - await hass.async_block_till_done() - await hass.async_block_till_done() - assert hass.states.get("climate.zen_01").state == HVACMode.COOL assert hass.states.get("climate.zen_01").attributes["temperature"] == 11.1 assert ( @@ -390,8 +377,6 @@ async def test_climate_device_with_cooling_support( # Event signals thermostat state on await sensor_ws_data({"state": {"on": True}}) - await hass.async_block_till_done() - assert hass.states.get("climate.zen_01").state == HVACMode.COOL assert ( hass.states.get("climate.zen_01").attributes["hvac_action"] @@ -470,8 +455,6 @@ async def test_climate_device_with_fan_support( # Event signals fan mode defaults to off await sensor_ws_data({"config": {"fanmode": "unsupported"}}) - await hass.async_block_till_done() - assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_OFF assert ( hass.states.get("climate.zen_01").attributes["hvac_action"] == HVACAction.IDLE @@ -480,8 +463,6 @@ async def test_climate_device_with_fan_support( # Event signals unsupported fan mode await sensor_ws_data({"config": {"fanmode": "unsupported"}, "state": {"on": True}}) - await hass.async_block_till_done() - assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON assert ( hass.states.get("climate.zen_01").attributes["hvac_action"] @@ -491,8 +472,6 @@ async def test_climate_device_with_fan_support( # Event signals unsupported fan mode await sensor_ws_data({"config": {"fanmode": "unsupported"}}) - await hass.async_block_till_done() - assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON assert ( hass.states.get("climate.zen_01").attributes["hvac_action"] @@ -595,8 +574,6 @@ async def test_climate_device_with_preset( # Event signals deCONZ preset await sensor_ws_data({"config": {"preset": "manual"}}) - await hass.async_block_till_done() - assert ( hass.states.get("climate.zen_01").attributes["preset_mode"] == DECONZ_PRESET_MANUAL @@ -605,8 +582,6 @@ async def test_climate_device_with_preset( # Event signals unknown preset await sensor_ws_data({"config": {"preset": "unsupported"}}) - await hass.async_block_till_done() - assert hass.states.get("climate.zen_01").attributes["preset_mode"] is None # Verify service calls @@ -739,8 +714,6 @@ async def test_verify_state_update( ) await sensor_ws_data({"state": {"on": False}}) - await hass.async_block_till_done() - assert hass.states.get("climate.thermostat").state == HVACMode.AUTO assert ( hass.states.get("climate.thermostat").attributes["hvac_action"] @@ -775,7 +748,6 @@ async def test_add_new_climate_device( assert len(hass.states.async_all()) == 0 await sensor_ws_data(event_added_sensor) - await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 assert hass.states.get("climate.thermostat").state == HVACMode.AUTO @@ -898,7 +870,6 @@ async def test_boost_mode( # Event signals thermostat preset boost and valve 100 (real data) await sensor_ws_data({"config": {"preset": "boost"}, "state": {"valve": 100}}) - await hass.async_block_till_done() climate_thermostat = hass.states.get("climate.thermostat") assert climate_thermostat.attributes["preset_mode"] is PRESET_BOOST diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 0d3c7aa7587..d04fb43a0d7 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -57,7 +57,7 @@ async def test_cover( hass: HomeAssistant, config_entry_setup: ConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], - mock_websocket_data: WebsocketDataType, + light_ws_data: WebsocketDataType, ) -> None: """Test that all supported cover entities are created.""" assert len(hass.states.async_all()) == 2 @@ -68,9 +68,7 @@ async def test_cover( # Event signals cover is open - await mock_websocket_data({"r": "lights", "state": {"lift": 0, "open": True}}) - await hass.async_block_till_done() - + await light_ws_data({"state": {"lift": 0, "open": True}}) cover = hass.states.get("cover.window_covering_device") assert cover.state == STATE_OPEN assert cover.attributes[ATTR_CURRENT_POSITION] == 100 diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 5867e0e3dec..8057605f1c5 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -99,7 +99,6 @@ async def test_deconz_events( captured_events = async_capture_events(hass, CONF_DECONZ_EVENT) await sensor_ws_data({"id": "1", "state": {"buttonevent": 2000}}) - await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:01")} @@ -114,7 +113,6 @@ async def test_deconz_events( } await sensor_ws_data({"id": "3", "state": {"buttonevent": 2000}}) - await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:03")} @@ -130,7 +128,6 @@ async def test_deconz_events( } await sensor_ws_data({"id": "4", "state": {"gesture": 0}}) - await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:04")} @@ -150,7 +147,6 @@ async def test_deconz_events( "state": {"buttonevent": 6002, "angle": 110, "xy": [0.5982, 0.3897]}, } await sensor_ws_data(event_changed_sensor) - await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:05")} @@ -169,8 +165,6 @@ async def test_deconz_events( # Unsupported event await sensor_ws_data({"id": "1", "name": "other name"}) - await hass.async_block_till_done() - assert len(captured_events) == 4 await hass.config_entries.async_unload(config_entry_setup.entry_id) @@ -272,7 +266,6 @@ async def test_deconz_alarm_events( # Emergency event await sensor_ws_data({"state": {"action": AncillaryControlAction.EMERGENCY}}) - await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:01")} @@ -289,7 +282,6 @@ async def test_deconz_alarm_events( # Fire event await sensor_ws_data({"state": {"action": AncillaryControlAction.FIRE}}) - await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:01")} @@ -306,7 +298,6 @@ async def test_deconz_alarm_events( # Invalid code event await sensor_ws_data({"state": {"action": AncillaryControlAction.INVALID_CODE}}) - await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:01")} @@ -323,7 +314,6 @@ async def test_deconz_alarm_events( # Panic event await sensor_ws_data({"state": {"action": AncillaryControlAction.PANIC}}) - await hass.async_block_till_done() device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:01")} @@ -340,15 +330,11 @@ async def test_deconz_alarm_events( # Only care for changes to specific action events await sensor_ws_data({"state": {"action": AncillaryControlAction.ARMED_AWAY}}) - await hass.async_block_till_done() - assert len(captured_events) == 4 # Only care for action events await sensor_ws_data({"state": {"panel": AncillaryControlPanel.ARMED_AWAY}}) - await hass.async_block_till_done() - assert len(captured_events) == 4 await hass.config_entries.async_unload(config_entry_setup.entry_id) @@ -425,7 +411,6 @@ async def test_deconz_presence_events( PresenceStatePresenceEvent.RIGHT_LEAVE, ): await sensor_ws_data({"state": {"presenceevent": presence_event}}) - await hass.async_block_till_done() assert len(captured_events) == 1 assert captured_events[0].data == { @@ -439,8 +424,6 @@ async def test_deconz_presence_events( # Unsupported presence event await sensor_ws_data({"state": {"presenceevent": PresenceStatePresenceEvent.NINE}}) - await hass.async_block_till_done() - assert len(captured_events) == 0 await hass.config_entries.async_unload(config_entry_setup.entry_id) @@ -514,7 +497,6 @@ async def test_deconz_relative_rotary_events( } } await sensor_ws_data(event_changed_sensor) - await hass.async_block_till_done() assert len(captured_events) == 1 assert captured_events[0].data == { @@ -530,8 +512,6 @@ async def test_deconz_relative_rotary_events( # Unsupported relative rotary event await sensor_ws_data({"name": "123"}) - await hass.async_block_till_done() - assert len(captured_events) == 0 await hass.config_entries.async_unload(config_entry_setup.entry_id) diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 9e22c91794f..46d36229488 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -344,8 +344,6 @@ async def test_functional_device_trigger( assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 1 await sensor_ws_data({"state": {"buttonevent": 1002}}) - await hass.async_block_till_done() - assert len(service_calls) == 1 assert service_calls[0].data["some"] == "test_trigger_button_press" diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index 1933b39c0b0..0da48812ea3 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -47,7 +47,7 @@ async def test_fans( aioclient_mock: AiohttpClientMocker, config_entry_setup: ConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], - mock_websocket_data: WebsocketDataType, + light_ws_data: WebsocketDataType, ) -> None: """Test that all supported fan entities are created.""" assert len(hass.states.async_all()) == 2 # Light and fan @@ -56,33 +56,23 @@ async def test_fans( # Test states - await mock_websocket_data({"r": "lights", "state": {"speed": 1}}) - await hass.async_block_till_done() - + await light_ws_data({"state": {"speed": 1}}) assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 25 - await mock_websocket_data({"r": "lights", "state": {"speed": 2}}) - await hass.async_block_till_done() - + await light_ws_data({"state": {"speed": 2}}) assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 50 - await mock_websocket_data({"r": "lights", "state": {"speed": 3}}) - await hass.async_block_till_done() - + await light_ws_data({"state": {"speed": 3}}) assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 75 - await mock_websocket_data({"r": "lights", "state": {"speed": 4}}) - await hass.async_block_till_done() - + await light_ws_data({"state": {"speed": 4}}) assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 100 - await mock_websocket_data({"r": "lights", "state": {"speed": 0}}) - await hass.async_block_till_done() - + await light_ws_data({"state": {"speed": 0}}) assert hass.states.get("fan.ceiling_fan").state == STATE_OFF assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 0 @@ -172,9 +162,7 @@ async def test_fans( # Events with an unsupported speed does not get converted - await mock_websocket_data({"r": "lights", "state": {"speed": 5}}) - await hass.async_block_till_done() - + await light_ws_data({"state": {"speed": 5}}) assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert not hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 942a763ce94..750661a8ba7 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -471,14 +471,12 @@ async def test_lights( @pytest.mark.usefixtures("config_entry_setup") async def test_light_state_change( hass: HomeAssistant, - mock_websocket_data: WebsocketDataType, + light_ws_data: WebsocketDataType, ) -> None: """Verify light can change state on websocket event.""" assert hass.states.get("light.hue_go").state == STATE_ON - await mock_websocket_data({"r": "lights", "state": {"on": False}}) - await hass.async_block_till_done() - + await light_ws_data({"state": {"on": False}}) assert hass.states.get("light.hue_go").state == STATE_OFF @@ -1280,7 +1278,7 @@ async def test_disable_light_groups( @pytest.mark.usefixtures("config_entry_setup") async def test_non_color_light_reports_color( hass: HomeAssistant, - mock_websocket_data: WebsocketDataType, + light_ws_data: WebsocketDataType, ) -> None: """Verify hs_color does not crash when a group gets updated with a bad color value. @@ -1303,7 +1301,6 @@ async def test_non_color_light_reports_color( # for a non-color light causing an exception in hs_color event_changed_light = { "id": "1", - "r": "lights", "state": { "alert": None, "bri": 216, @@ -1314,9 +1311,7 @@ async def test_non_color_light_reports_color( }, "uniqueid": "ec:1b:bd:ff:fe:ee:ed:dd-01", } - await mock_websocket_data(event_changed_light) - await hass.async_block_till_done() - + await light_ws_data(event_changed_light) group = hass.states.get("light.group") assert group.attributes[ATTR_COLOR_MODE] == ColorMode.XY assert group.attributes[ATTR_HS_COLOR] == (40.571, 41.176) diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index 28cd57633cc..a370261616b 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -45,15 +45,13 @@ async def test_lock_from_light( hass: HomeAssistant, config_entry_setup: ConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], - mock_websocket_data: WebsocketDataType, + light_ws_data: WebsocketDataType, ) -> None: """Test that all supported lock entities based on lights are created.""" assert len(hass.states.async_all()) == 1 assert hass.states.get("lock.door_lock").state == STATE_UNLOCKED - await mock_websocket_data({"r": "lights", "state": {"on": True}}) - await hass.async_block_till_done() - + await light_ws_data({"state": {"on": True}}) assert hass.states.get("lock.door_lock").state == STATE_LOCKED # Verify service calls @@ -129,8 +127,6 @@ async def test_lock_from_sensor( assert hass.states.get("lock.door_lock").state == STATE_UNLOCKED await sensor_ws_data({"state": {"lockstate": "locked"}}) - await hass.async_block_till_done() - assert hass.states.get("lock.door_lock").state == STATE_LOCKED # Verify service calls diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py index 330d8d80e47..7b34402600d 100644 --- a/tests/components/deconz/test_number.py +++ b/tests/components/deconz/test_number.py @@ -135,7 +135,6 @@ async def test_number_entities( # Change state await sensor_ws_data(expected["websocket_event"]) - await hass.async_block_till_done() assert hass.states.get(expected["entity_id"]).state == expected["next_state"] # Verify service calls diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index f430711deab..60746311928 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -131,6 +131,4 @@ async def test_only_new_scenes_are_created( "scenes": [{"id": "1", "name": "Scene"}], } await mock_websocket_data(event_changed_group) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index ce5e0fb69e3..76da8628da1 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -953,7 +953,6 @@ async def test_sensors( # Change state await sensor_ws_data(expected["websocket_event"]) - await hass.async_block_till_done() assert hass.states.get(expected["entity_id"]).state == expected["next_state"] # Unload entry @@ -1073,8 +1072,6 @@ async def test_add_new_sensor( assert len(hass.states.async_all()) == 0 await sensor_ws_data(event_added_sensor) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 assert hass.states.get("sensor.light_level_sensor").state == "999.8" @@ -1172,15 +1169,10 @@ async def test_add_battery_later( assert len(hass.states.async_all()) == 0 await sensor_ws_data({"id": "2", "config": {"battery": 50}}) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 await sensor_ws_data({"id": "1", "config": {"battery": 50}}) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 - assert hass.states.get("sensor.switch_1_battery").state == "50" diff --git a/tests/components/deconz/test_siren.py b/tests/components/deconz/test_siren.py index b8224365457..2d11468bfad 100644 --- a/tests/components/deconz/test_siren.py +++ b/tests/components/deconz/test_siren.py @@ -35,16 +35,14 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_sirens( hass: HomeAssistant, config_entry_setup: ConfigEntry, - mock_websocket_data: WebsocketDataType, + light_ws_data: WebsocketDataType, mock_put_request: Callable[[str, str], AiohttpClientMocker], ) -> None: """Test that siren entities are created.""" assert len(hass.states.async_all()) == 1 assert hass.states.get("siren.warning_device").state == STATE_ON - await mock_websocket_data({"r": "lights", "state": {"alert": None}}) - await hass.async_block_till_done() - + await light_ws_data({"state": {"alert": None}}) assert hass.states.get("siren.warning_device").state == STATE_OFF # Verify service calls diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index e6c3e93048e..1b28c8d3939 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -56,7 +56,7 @@ async def test_power_plugs( hass: HomeAssistant, config_entry_setup: ConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], - mock_websocket_data: WebsocketDataType, + light_ws_data: WebsocketDataType, ) -> None: """Test that all supported switch entities are created.""" assert len(hass.states.async_all()) == 4 @@ -65,9 +65,7 @@ async def test_power_plugs( assert hass.states.get("switch.on_off_relay").state == STATE_ON assert hass.states.get("switch.unsupported_switch") is None - await mock_websocket_data({"r": "lights", "state": {"on": False}}) - await hass.async_block_till_done() - + await light_ws_data({"state": {"on": False}}) assert hass.states.get("switch.on_off_switch").state == STATE_OFF # Verify service calls From 4674502b926d4b7d4dba2879b56614c5a100c54b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 09:24:52 +0200 Subject: [PATCH 1483/2411] Bump docker/login-action from 3.2.0 to 3.3.0 (#122440) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index be4ca304950..d0edc631762 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -190,7 +190,7 @@ jobs: echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE - name: Login to GitHub Container Registry - uses: docker/login-action@v3.2.0 + uses: docker/login-action@v3.3.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -256,7 +256,7 @@ jobs: fi - name: Login to GitHub Container Registry - uses: docker/login-action@v3.2.0 + uses: docker/login-action@v3.3.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -329,14 +329,14 @@ jobs: - name: Login to DockerHub if: matrix.registry == 'docker.io/homeassistant' - uses: docker/login-action@v3.2.0 + uses: docker/login-action@v3.3.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry if: matrix.registry == 'ghcr.io/home-assistant' - uses: docker/login-action@v3.2.0 + uses: docker/login-action@v3.3.0 with: registry: ghcr.io username: ${{ github.repository_owner }} From be4c7291bd519fea4cf2eb7284e79ad513fff586 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 23 Jul 2024 00:31:22 -0700 Subject: [PATCH 1484/2411] Update google tasks to return completed items (#122437) --- homeassistant/components/google_tasks/api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_tasks/api.py b/homeassistant/components/google_tasks/api.py index 22e5e80229a..c8b30c173eb 100644 --- a/homeassistant/components/google_tasks/api.py +++ b/homeassistant/components/google_tasks/api.py @@ -68,7 +68,10 @@ class AsyncConfigEntryAuth: """Get all Task resources for the task list.""" service = await self._get_service() cmd: HttpRequest = service.tasks().list( - tasklist=task_list_id, maxResults=MAX_TASK_RESULTS + tasklist=task_list_id, + maxResults=MAX_TASK_RESULTS, + showCompleted=True, + showHidden=True, ) result = await self._execute(cmd) return result["items"] From cd4827867182791d2cedc717fa1bfe30667b4ac2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 23 Jul 2024 09:34:00 +0200 Subject: [PATCH 1485/2411] Extract Geniushub base entities in separate module (#122331) --- .../components/geniushub/__init__.py | 167 +---------------- .../components/geniushub/binary_sensor.py | 3 +- homeassistant/components/geniushub/climate.py | 3 +- homeassistant/components/geniushub/entity.py | 168 ++++++++++++++++++ homeassistant/components/geniushub/sensor.py | 3 +- homeassistant/components/geniushub/switch.py | 3 +- .../components/geniushub/water_heater.py | 3 +- 7 files changed, 180 insertions(+), 170 deletions(-) create mode 100644 homeassistant/components/geniushub/entity.py diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 84e835ac2bb..836add310b6 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -2,9 +2,8 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import timedelta import logging -from typing import Any import aiohttp from geniushubclient import GeniusHub @@ -21,7 +20,6 @@ from homeassistant.const import ( CONF_TOKEN, CONF_USERNAME, Platform, - UnitOfTemperature, ) from homeassistant.core import ( DOMAIN as HOMEASSISTANT_DOMAIN, @@ -32,31 +30,16 @@ from homeassistant.core import ( from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.typing import ConfigType -import homeassistant.util.dt as dt_util from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -# temperature is repeated here, as it gives access to high-precision temps -GH_ZONE_ATTRS = ["mode", "temperature", "type", "occupied", "override"] -GH_DEVICE_ATTRS = { - "luminance": "luminance", - "measuredTemperature": "measured_temperature", - "occupancyTrigger": "occupancy_trigger", - "setback": "setback", - "setTemperature": "set_temperature", - "wakeupInterval": "wakeup_interval", -} SCAN_INTERVAL = timedelta(seconds=60) @@ -279,149 +262,3 @@ class GeniusBroker: self.client._zones, # noqa: SLF001 self.client._devices, # noqa: SLF001 ) - - -class GeniusEntity(Entity): - """Base for all Genius Hub entities.""" - - _attr_should_poll = False - - def __init__(self) -> None: - """Initialize the entity.""" - self._unique_id: str | None = None - - async def async_added_to_hass(self) -> None: - """Set up a listener when this entity is added to HA.""" - self.async_on_remove(async_dispatcher_connect(self.hass, DOMAIN, self._refresh)) - - async def _refresh(self, payload: dict | None = None) -> None: - """Process any signals.""" - self.async_schedule_update_ha_state(force_refresh=True) - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return self._unique_id - - -class GeniusDevice(GeniusEntity): - """Base for all Genius Hub devices.""" - - def __init__(self, broker, device) -> None: - """Initialize the Device.""" - super().__init__() - - self._device = device - self._unique_id = f"{broker.hub_uid}_device_{device.id}" - self._last_comms: datetime | None = None - self._state_attr = None - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the device state attributes.""" - attrs = {} - attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] - if self._last_comms: - attrs["last_comms"] = self._last_comms.isoformat() - - state = dict(self._device.data["state"]) - if "_state" in self._device.data: # only via v3 API - state.update(self._device.data["_state"]) - - attrs["state"] = { - GH_DEVICE_ATTRS[k]: v for k, v in state.items() if k in GH_DEVICE_ATTRS - } - - return attrs - - async def async_update(self) -> None: - """Update an entity's state data.""" - if "_state" in self._device.data: # only via v3 API - self._last_comms = dt_util.utc_from_timestamp( - self._device.data["_state"]["lastComms"] - ) - - -class GeniusZone(GeniusEntity): - """Base for all Genius Hub zones.""" - - def __init__(self, broker, zone) -> None: - """Initialize the Zone.""" - super().__init__() - - self._zone = zone - self._unique_id = f"{broker.hub_uid}_zone_{zone.id}" - - async def _refresh(self, payload: dict | None = None) -> None: - """Process any signals.""" - if payload is None: - self.async_schedule_update_ha_state(force_refresh=True) - return - - if payload["unique_id"] != self._unique_id: - return - - if payload["service"] == SVC_SET_ZONE_OVERRIDE: - temperature = round(payload["data"][ATTR_TEMPERATURE] * 10) / 10 - duration = payload["data"].get(ATTR_DURATION, timedelta(hours=1)) - - await self._zone.set_override(temperature, int(duration.total_seconds())) - return - - mode = payload["data"][ATTR_ZONE_MODE] - - if mode == "footprint" and not self._zone._has_pir: # noqa: SLF001 - raise TypeError( - f"'{self.entity_id}' cannot support footprint mode (it has no PIR)" - ) - - await self._zone.set_mode(mode) - - @property - def name(self) -> str: - """Return the name of the climate device.""" - return self._zone.name - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the device state attributes.""" - status = {k: v for k, v in self._zone.data.items() if k in GH_ZONE_ATTRS} - return {"status": status} - - -class GeniusHeatingZone(GeniusZone): - """Base for Genius Heating Zones.""" - - _max_temp: float - _min_temp: float - - @property - def current_temperature(self) -> float | None: - """Return the current temperature.""" - return self._zone.data.get("temperature") - - @property - def target_temperature(self) -> float: - """Return the temperature we try to reach.""" - return self._zone.data["setpoint"] - - @property - def min_temp(self) -> float: - """Return max valid temperature that can be set.""" - return self._min_temp - - @property - def max_temp(self) -> float: - """Return max valid temperature that can be set.""" - return self._max_temp - - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return UnitOfTemperature.CELSIUS - - async def async_set_temperature(self, **kwargs) -> None: - """Set a new target temperature for this zone.""" - await self._zone.set_override( - kwargs[ATTR_TEMPERATURE], kwargs.get(ATTR_DURATION, 3600) - ) diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index 2d6acf0c955..01ccc950fd6 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -6,7 +6,8 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import GeniusDevice, GeniusHubConfigEntry +from . import GeniusHubConfigEntry +from .entity import GeniusDevice GH_STATE_ATTR = "outputOnOff" GH_TYPE = "Receiver" diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index ea2a79be767..99d1bde8099 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -13,7 +13,8 @@ from homeassistant.components.climate import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import GeniusHeatingZone, GeniusHubConfigEntry +from . import GeniusHubConfigEntry +from .entity import GeniusHeatingZone # GeniusHub Zones support: Off, Timer, Override/Boost, Footprint & Linked modes HA_HVAC_TO_GH = {HVACMode.OFF: "off", HVACMode.HEAT: "timer"} diff --git a/homeassistant/components/geniushub/entity.py b/homeassistant/components/geniushub/entity.py new file mode 100644 index 00000000000..7c40c41bda5 --- /dev/null +++ b/homeassistant/components/geniushub/entity.py @@ -0,0 +1,168 @@ +"""Base entity for Geniushub.""" + +from datetime import datetime, timedelta +from typing import Any + +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util + +from . import ATTR_DURATION, ATTR_ZONE_MODE, DOMAIN, SVC_SET_ZONE_OVERRIDE + +# temperature is repeated here, as it gives access to high-precision temps +GH_ZONE_ATTRS = ["mode", "temperature", "type", "occupied", "override"] +GH_DEVICE_ATTRS = { + "luminance": "luminance", + "measuredTemperature": "measured_temperature", + "occupancyTrigger": "occupancy_trigger", + "setback": "setback", + "setTemperature": "set_temperature", + "wakeupInterval": "wakeup_interval", +} + + +class GeniusEntity(Entity): + """Base for all Genius Hub entities.""" + + _attr_should_poll = False + + def __init__(self) -> None: + """Initialize the entity.""" + self._unique_id: str | None = None + + async def async_added_to_hass(self) -> None: + """Set up a listener when this entity is added to HA.""" + self.async_on_remove(async_dispatcher_connect(self.hass, DOMAIN, self._refresh)) + + async def _refresh(self, payload: dict | None = None) -> None: + """Process any signals.""" + self.async_schedule_update_ha_state(force_refresh=True) + + @property + def unique_id(self) -> str | None: + """Return a unique ID.""" + return self._unique_id + + +class GeniusDevice(GeniusEntity): + """Base for all Genius Hub devices.""" + + def __init__(self, broker, device) -> None: + """Initialize the Device.""" + super().__init__() + + self._device = device + self._unique_id = f"{broker.hub_uid}_device_{device.id}" + self._last_comms: datetime | None = None + self._state_attr = None + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the device state attributes.""" + attrs = {} + attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] + if self._last_comms: + attrs["last_comms"] = self._last_comms.isoformat() + + state = dict(self._device.data["state"]) + if "_state" in self._device.data: # only via v3 API + state.update(self._device.data["_state"]) + + attrs["state"] = { + GH_DEVICE_ATTRS[k]: v for k, v in state.items() if k in GH_DEVICE_ATTRS + } + + return attrs + + async def async_update(self) -> None: + """Update an entity's state data.""" + if "_state" in self._device.data: # only via v3 API + self._last_comms = dt_util.utc_from_timestamp( + self._device.data["_state"]["lastComms"] + ) + + +class GeniusZone(GeniusEntity): + """Base for all Genius Hub zones.""" + + def __init__(self, broker, zone) -> None: + """Initialize the Zone.""" + super().__init__() + + self._zone = zone + self._unique_id = f"{broker.hub_uid}_zone_{zone.id}" + + async def _refresh(self, payload: dict | None = None) -> None: + """Process any signals.""" + if payload is None: + self.async_schedule_update_ha_state(force_refresh=True) + return + + if payload["unique_id"] != self._unique_id: + return + + if payload["service"] == SVC_SET_ZONE_OVERRIDE: + temperature = round(payload["data"][ATTR_TEMPERATURE] * 10) / 10 + duration = payload["data"].get(ATTR_DURATION, timedelta(hours=1)) + + await self._zone.set_override(temperature, int(duration.total_seconds())) + return + + mode = payload["data"][ATTR_ZONE_MODE] + + if mode == "footprint" and not self._zone._has_pir: # noqa: SLF001 + raise TypeError( + f"'{self.entity_id}' cannot support footprint mode (it has no PIR)" + ) + + await self._zone.set_mode(mode) + + @property + def name(self) -> str: + """Return the name of the climate device.""" + return self._zone.name + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the device state attributes.""" + status = {k: v for k, v in self._zone.data.items() if k in GH_ZONE_ATTRS} + return {"status": status} + + +class GeniusHeatingZone(GeniusZone): + """Base for Genius Heating Zones.""" + + _max_temp: float + _min_temp: float + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self._zone.data.get("temperature") + + @property + def target_temperature(self) -> float: + """Return the temperature we try to reach.""" + return self._zone.data["setpoint"] + + @property + def min_temp(self) -> float: + """Return max valid temperature that can be set.""" + return self._min_temp + + @property + def max_temp(self) -> float: + """Return max valid temperature that can be set.""" + return self._max_temp + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return UnitOfTemperature.CELSIUS + + async def async_set_temperature(self, **kwargs) -> None: + """Set a new target temperature for this zone.""" + await self._zone.set_override( + kwargs[ATTR_TEMPERATURE], kwargs.get(ATTR_DURATION, 3600) + ) diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index ee65e679498..cfe4107428c 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -11,7 +11,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from . import GeniusDevice, GeniusEntity, GeniusHubConfigEntry +from . import GeniusHubConfigEntry +from .entity import GeniusDevice, GeniusEntity GH_STATE_ATTR = "batteryLevel" diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index 2fffbddde01..3af82eb4e92 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -13,7 +13,8 @@ from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import VolDictType -from . import ATTR_DURATION, GeniusHubConfigEntry, GeniusZone +from . import ATTR_DURATION, GeniusHubConfigEntry +from .entity import GeniusZone GH_ON_OFF_ZONE = "on / off" diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index 6d3da570547..2807bd60611 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -10,7 +10,8 @@ from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import GeniusHeatingZone, GeniusHubConfigEntry +from . import GeniusHubConfigEntry +from .entity import GeniusHeatingZone STATE_AUTO = "auto" STATE_MANUAL = "manual" From 108dc3795eaffbeafed255c14b0c00001b21ca2d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 23 Jul 2024 09:39:27 +0200 Subject: [PATCH 1486/2411] Remove incorrect use of Mock.assert_has_calls from recorder tests (#122439) * Remove incorrect use of Mock.assert_has_calls from recorder tests * Fix test --- tests/components/recorder/test_migrate.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index d753f908e76..3eea231a659 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -83,17 +83,15 @@ async def test_schema_update_calls( instance = recorder.get_instance(hass) engine = instance.engine session_maker = instance.get_session - update.assert_has_calls( - [ - call(instance, hass, engine, session_maker, version + 1, 0) - for version in range(db_schema.SCHEMA_VERSION) - ] - ) - migrate_schema.assert_has_calls( - [ - call(instance, hass, engine, session_maker, ANY, db_schema.SCHEMA_VERSION), - ] - ) + assert update.mock_calls == [ + call(instance, hass, engine, session_maker, version + 1, 0) + for version in range(db_schema.SCHEMA_VERSION) + ] + status = migration.SchemaValidationStatus(0, True, set(), 0) + assert migrate_schema.mock_calls == [ + call(instance, hass, engine, session_maker, status, 0), + call(instance, hass, engine, session_maker, status, db_schema.SCHEMA_VERSION), + ] async def test_migration_in_progress( From d3d91a83e52b8637c504f447f18d582b6bddf0c7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 23 Jul 2024 09:56:23 +0200 Subject: [PATCH 1487/2411] Update wled to 0.20.0 (#122441) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wled/snapshots/test_diagnostics.ambr | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 85d8e957120..b19e5f16ccb 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "platinum", - "requirements": ["wled==0.19.2"], + "requirements": ["wled==0.20.0"], "zeroconf": ["_wled._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index fab0282bc3e..328e9bf840c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2903,7 +2903,7 @@ wiffi==1.1.2 wirelesstagpy==0.8.1 # homeassistant.components.wled -wled==0.19.2 +wled==0.20.0 # homeassistant.components.wolflink wolf-comm==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3af90c5675e..9ff5e7e8c02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2277,7 +2277,7 @@ whois==0.9.27 wiffi==1.1.2 # homeassistant.components.wled -wled==0.19.2 +wled==0.20.0 # homeassistant.components.wolflink wolf-comm==0.0.9 diff --git a/tests/components/wled/snapshots/test_diagnostics.ambr b/tests/components/wled/snapshots/test_diagnostics.ambr index fea53e8ac03..90732c02c36 100644 --- a/tests/components/wled/snapshots/test_diagnostics.ambr +++ b/tests/components/wled/snapshots/test_diagnostics.ambr @@ -317,6 +317,7 @@ 'seg': dict({ '0': dict({ 'bri': 255, + 'cct': 127, 'cln': -1, 'col': list([ list([ @@ -349,6 +350,7 @@ }), '1': dict({ 'bri': 255, + 'cct': 127, 'cln': -1, 'col': list([ list([ From 632dec614a2e8d01b80d43e73181fb91909e9117 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 23 Jul 2024 10:10:32 +0200 Subject: [PATCH 1488/2411] Fix several issues with the Matter Generic Switch Cluster (#122191) --- homeassistant/components/matter/event.py | 43 ++++++++++++++--- homeassistant/components/matter/sensor.py | 14 ++++++ homeassistant/components/matter/strings.json | 21 ++++++--- .../fixtures/nodes/generic-switch-multi.json | 6 ++- .../matter/fixtures/nodes/generic-switch.json | 2 +- tests/components/matter/test_event.py | 46 +++++++++---------- 6 files changed, 92 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/matter/event.py b/homeassistant/components/matter/event.py index dcb67d50523..885ba83ce07 100644 --- a/homeassistant/components/matter/event.py +++ b/homeassistant/components/matter/event.py @@ -58,17 +58,32 @@ class MatterEventEntity(MatterEntity, EventEntity): self.get_matter_attribute_value(clusters.Switch.Attributes.FeatureMap) ) if feature_map & SwitchFeature.kLatchingSwitch: + # a latching switch only supports switch_latched event event_types.append("switch_latched") - if feature_map & SwitchFeature.kMomentarySwitch: + elif feature_map & SwitchFeature.kMomentarySwitchMultiPress: + # Momentary switch with multi press support + # NOTE: We ignore 'multi press ongoing' as it doesn't make a lot + # of sense and many devices do not support it. + # Instead we we report on the 'multi press complete' event with the number + # of presses. + max_presses_supported = self.get_matter_attribute_value( + clusters.Switch.Attributes.MultiPressMax + ) + max_presses_supported = min(max_presses_supported or 1, 8) + for i in range(max_presses_supported): + event_types.append(f"multi_press_{i + 1}") # noqa: PERF401 + elif feature_map & SwitchFeature.kMomentarySwitch: + # momentary switch without multi press support event_types.append("initial_press") - if feature_map & SwitchFeature.kMomentarySwitchRelease: - event_types.append("short_release") + if feature_map & SwitchFeature.kMomentarySwitchRelease: + # momentary switch without multi press support can optionally support release + event_types.append("short_release") + + # a momentary switch can optionally support long press if feature_map & SwitchFeature.kMomentarySwitchLongPress: event_types.append("long_press") event_types.append("long_release") - if feature_map & SwitchFeature.kMomentarySwitchMultiPress: - event_types.append("multi_press_ongoing") - event_types.append("multi_press_complete") + self._attr_event_types = event_types async def async_added_to_hass(self) -> None: @@ -96,7 +111,20 @@ class MatterEventEntity(MatterEntity, EventEntity): """Call on NodeEvent.""" if data.endpoint_id != self._endpoint.endpoint_id: return - self._trigger_event(EVENT_TYPES_MAP[data.event_id], data.data) + if data.event_id == clusters.Switch.Events.MultiPressComplete.event_id: + # multi press event + presses = (data.data or {}).get("totalNumberOfPressesCounted", 1) + event_type = f"multi_press_{presses}" + else: + event_type = EVENT_TYPES_MAP[data.event_id] + + if event_type not in self.event_types: + # this should not happen, but guard for bad things + # some remotes send events that they do not report as supported (sigh...) + return + + # pass the rest of the data as-is (such as the advanced Position data) + self._trigger_event(event_type, data.data) self.async_write_ha_state() @@ -119,5 +147,6 @@ DISCOVERY_SCHEMAS = [ clusters.Switch.Attributes.NumberOfPositions, clusters.FixedLabel.Attributes.LabelList, ), + allow_multi=True, # also used for sensor (current position) entity ), ] diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 9c19be7ee08..c3ab18072f0 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -448,4 +448,18 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterSensor, required_attributes=(NeoCluster.Attributes.Current,), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="SwitchCurrentPosition", + native_unit_of_measurement=None, + device_class=None, + state_class=SensorStateClass.MEASUREMENT, + translation_key="switch_current_position", + entity_category=EntityCategory.DIAGNOSTIC, + ), + entity_class=MatterSensor, + required_attributes=(clusters.Switch.Attributes.CurrentPosition,), + allow_multi=True, # also used for event entity + ), ] diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 0a823d5aa80..3c50ccbaa21 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -73,12 +73,18 @@ "event_type": { "state": { "switch_latched": "Switch latched", - "initial_press": "Initial press", - "long_press": "Long press", - "short_release": "Short release", - "long_release": "Long release", - "multi_press_ongoing": "Multi press ongoing", - "multi_press_complete": "Multi press complete" + "initial_press": "Pressed", + "long_press": "Hold down", + "short_release": "Released after being pressed", + "long_release": "Released after being held down", + "multi_press_1": "Pressed once", + "multi_press_2": "Pressed twice", + "multi_press_3": "Pressed 3 times", + "multi_press_4": "Pressed 4 times", + "multi_press_5": "Pressed 5 times", + "multi_press_6": "Pressed 6 times", + "multi_press_7": "Pressed 7 times", + "multi_press_8": "Pressed 8 times" } } } @@ -151,6 +157,9 @@ }, "hepa_filter_condition": { "name": "Hepa filter condition" + }, + "switch_current_position": { + "name": "Current switch position" } }, "switch": { diff --git a/tests/components/matter/fixtures/nodes/generic-switch-multi.json b/tests/components/matter/fixtures/nodes/generic-switch-multi.json index f564e91a1ce..8923198c31e 100644 --- a/tests/components/matter/fixtures/nodes/generic-switch-multi.json +++ b/tests/components/matter/fixtures/nodes/generic-switch-multi.json @@ -72,8 +72,9 @@ "1/59/0": 2, "1/59/65533": 1, "1/59/1": 0, + "1/59/2": 2, "1/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], - "1/59/65532": 14, + "1/59/65532": 30, "1/59/65528": [], "1/64/0": [ { @@ -101,8 +102,9 @@ "2/59/0": 2, "2/59/65533": 1, "2/59/1": 0, + "2/59/2": 2, "2/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], - "2/59/65532": 14, + "2/59/65532": 30, "2/59/65528": [], "2/64/0": [ { diff --git a/tests/components/matter/fixtures/nodes/generic-switch.json b/tests/components/matter/fixtures/nodes/generic-switch.json index 80773915748..9b334c5fb54 100644 --- a/tests/components/matter/fixtures/nodes/generic-switch.json +++ b/tests/components/matter/fixtures/nodes/generic-switch.json @@ -73,7 +73,7 @@ "1/59/65533": 1, "1/59/1": 0, "1/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], - "1/59/65532": 30, + "1/59/65532": 14, "1/59/65528": [] }, "available": true, diff --git a/tests/components/matter/test_event.py b/tests/components/matter/test_event.py index a7bd7c91f7b..183867642f5 100644 --- a/tests/components/matter/test_event.py +++ b/tests/components/matter/test_event.py @@ -50,8 +50,6 @@ async def test_generic_switch_node( "short_release", "long_press", "long_release", - "multi_press_ongoing", - "multi_press_complete", ] # trigger firing a new event from the device await trigger_subscription_callback( @@ -72,26 +70,6 @@ async def test_generic_switch_node( ) state = hass.states.get("event.mock_generic_switch_button") assert state.attributes[ATTR_EVENT_TYPE] == "initial_press" - # trigger firing a multi press event - await trigger_subscription_callback( - hass, - matter_client, - EventType.NODE_EVENT, - MatterNodeEvent( - node_id=generic_switch_node.node_id, - endpoint_id=1, - cluster_id=59, - event_id=5, - event_number=0, - priority=1, - timestamp=0, - timestamp_type=0, - data={"NewPosition": 3}, - ), - ) - state = hass.states.get("event.mock_generic_switch_button") - assert state.attributes[ATTR_EVENT_TYPE] == "multi_press_ongoing" - assert state.attributes["NewPosition"] == 3 # This tests needs to be adjusted to remove lingering tasks @@ -109,8 +87,8 @@ async def test_generic_switch_multi_node( assert state_button_1.name == "Mock Generic Switch Button (1)" # check event_types from featuremap 14 assert state_button_1.attributes[ATTR_EVENT_TYPES] == [ - "initial_press", - "short_release", + "multi_press_1", + "multi_press_2", "long_press", "long_release", ] @@ -120,3 +98,23 @@ async def test_generic_switch_multi_node( assert state_button_1.state == "unknown" # name should be 'DeviceName Fancy Button' due to the label set to 'Fancy Button' assert state_button_1.name == "Mock Generic Switch Fancy Button" + + # trigger firing a multi press event + await trigger_subscription_callback( + hass, + matter_client, + EventType.NODE_EVENT, + MatterNodeEvent( + node_id=generic_switch_multi_node.node_id, + endpoint_id=1, + cluster_id=59, + event_id=6, + event_number=0, + priority=1, + timestamp=0, + timestamp_type=0, + data={"totalNumberOfPressesCounted": 2}, + ), + ) + state = hass.states.get("event.mock_generic_switch_button_1") + assert state.attributes[ATTR_EVENT_TYPE] == "multi_press_2" From 77282ed4b021f84f70d6e4c6731d7ab34426c370 Mon Sep 17 00:00:00 2001 From: fustom Date: Tue, 23 Jul 2024 12:30:06 +0200 Subject: [PATCH 1489/2411] Use external temp if needed in Broadlink (#118375) * Use external temp for current temp depends on the sensor state * Add SensorMode enum * Add tests for Broadlink climate * Check is the sensor included in the data * Use IntEnum as parent of SensorMode * Use SensorMode enum value for sensor test data * Parametrizing tests * Readd accidentally removed assert * Use local sensor variable Co-authored-by: Robert Resch * Refactor test_climate. Check call_counts. * Add parameter types Co-authored-by: Robert Resch * Update homeassistant/components/broadlink/climate.py --------- Co-authored-by: Robert Resch Co-authored-by: Joost Lekkerkerker --- homeassistant/components/broadlink/climate.py | 22 ++- tests/components/broadlink/test_climate.py | 180 ++++++++++++++++++ 2 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 tests/components/broadlink/test_climate.py diff --git a/homeassistant/components/broadlink/climate.py b/homeassistant/components/broadlink/climate.py index 0573c342490..dbfd982795c 100644 --- a/homeassistant/components/broadlink/climate.py +++ b/homeassistant/components/broadlink/climate.py @@ -1,5 +1,6 @@ """Support for Broadlink climate devices.""" +from enum import IntEnum from typing import Any from homeassistant.components.climate import ( @@ -19,6 +20,14 @@ from .device import BroadlinkDevice from .entity import BroadlinkEntity +class SensorMode(IntEnum): + """Thermostat sensor modes.""" + + INNER_SENSOR_CONTROL = 0 + OUTER_SENSOR_CONTROL = 1 + INNER_SENSOR_CONTROL_OUTER_LIMIT = 2 + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -50,6 +59,7 @@ class BroadlinkThermostat(BroadlinkEntity, ClimateEntity): super().__init__(device) self._attr_unique_id = device.unique_id self._attr_hvac_mode = None + self.sensor_mode = SensorMode.INNER_SENSOR_CONTROL async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" @@ -61,6 +71,8 @@ class BroadlinkThermostat(BroadlinkEntity, ClimateEntity): @callback def _update_state(self, data: dict[str, Any]) -> None: """Update data.""" + if (sensor := data.get("sensor")) is not None: + self.sensor_mode = SensorMode(sensor) if data.get("power"): if data.get("auto_mode"): self._attr_hvac_mode = HVACMode.AUTO @@ -74,8 +86,10 @@ class BroadlinkThermostat(BroadlinkEntity, ClimateEntity): else: self._attr_hvac_mode = HVACMode.OFF self._attr_hvac_action = HVACAction.OFF - - self._attr_current_temperature = data.get("room_temp") + if self.sensor_mode is SensorMode.OUTER_SENSOR_CONTROL: + self._attr_current_temperature = data.get("external_temp") + else: + self._attr_current_temperature = data.get("room_temp") self._attr_target_temperature = data.get("thermostat_temp") async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: @@ -85,7 +99,9 @@ class BroadlinkThermostat(BroadlinkEntity, ClimateEntity): else: await self._device.async_request(self._device.api.set_power, 1) mode = 0 if hvac_mode == HVACMode.HEAT else 1 - await self._device.async_request(self._device.api.set_mode, mode, 0) + await self._device.async_request( + self._device.api.set_mode, mode, 0, self.sensor_mode.value + ) self._attr_hvac_mode = hvac_mode self.async_write_ha_state() diff --git a/tests/components/broadlink/test_climate.py b/tests/components/broadlink/test_climate.py new file mode 100644 index 00000000000..6b39d1895b1 --- /dev/null +++ b/tests/components/broadlink/test_climate.py @@ -0,0 +1,180 @@ +"""Tests for Broadlink climate.""" + +from typing import Any + +import pytest + +from homeassistant.components.broadlink.climate import SensorMode +from homeassistant.components.broadlink.const import DOMAIN +from homeassistant.components.climate import ( + ATTR_TEMPERATURE, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + HVACAction, + HVACMode, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity + +from . import get_device + + +@pytest.mark.parametrize( + ( + "api_return_value", + "expected_state", + "expected_current_temperature", + "expected_temperature", + "expected_hvac_action", + ), + [ + ( + { + "sensor": SensorMode.INNER_SENSOR_CONTROL.value, + "power": 1, + "auto_mode": 0, + "active": 1, + "room_temp": 22, + "thermostat_temp": 23, + "external_temp": 30, + }, + HVACMode.HEAT, + 22, + 23, + HVACAction.HEATING, + ), + ( + { + "sensor": SensorMode.OUTER_SENSOR_CONTROL.value, + "power": 1, + "auto_mode": 1, + "active": 0, + "room_temp": 22, + "thermostat_temp": 23, + "external_temp": 30, + }, + HVACMode.AUTO, + 30, + 23, + HVACAction.IDLE, + ), + ( + { + "sensor": SensorMode.INNER_SENSOR_CONTROL.value, + "power": 0, + "auto_mode": 0, + "active": 0, + "room_temp": 22, + "thermostat_temp": 23, + "external_temp": 30, + }, + HVACMode.OFF, + 22, + 23, + HVACAction.OFF, + ), + ], +) +async def test_climate( + api_return_value: dict[str, Any], + expected_state: HVACMode, + expected_current_temperature: int, + expected_temperature: int, + expected_hvac_action: HVACAction, + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test Broadlink climate.""" + + device = get_device("Guest room") + mock_setup = await device.setup_entry(hass) + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, mock_setup.entry.unique_id)} + ) + entries = er.async_entries_for_device(entity_registry, device_entry.id) + climates = [entry for entry in entries if entry.domain == Platform.CLIMATE] + assert len(climates) == 1 + + climate = climates[0] + + mock_setup.api.get_full_status.return_value = api_return_value + + await async_update_entity(hass, climate.entity_id) + assert mock_setup.api.get_full_status.call_count == 2 + state = hass.states.get(climate.entity_id) + assert state.state == expected_state + assert state.attributes["current_temperature"] == expected_current_temperature + assert state.attributes["temperature"] == expected_temperature + assert state.attributes["hvac_action"] == expected_hvac_action + + +async def test_climate_set_temperature_turn_off_turn_on( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test Broadlink climate.""" + + device = get_device("Guest room") + mock_setup = await device.setup_entry(hass) + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, mock_setup.entry.unique_id)} + ) + entries = er.async_entries_for_device(entity_registry, device_entry.id) + climates = [entry for entry in entries if entry.domain == Platform.CLIMATE] + assert len(climates) == 1 + + climate = climates[0] + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: climate.entity_id, + ATTR_TEMPERATURE: "24", + }, + blocking=True, + ) + state = hass.states.get(climate.entity_id) + + assert mock_setup.api.set_temp.call_count == 1 + assert mock_setup.api.set_power.call_count == 0 + assert mock_setup.api.set_mode.call_count == 0 + assert state.attributes["temperature"] == 24 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: climate.entity_id, + }, + blocking=True, + ) + state = hass.states.get(climate.entity_id) + + assert mock_setup.api.set_temp.call_count == 1 + assert mock_setup.api.set_power.call_count == 1 + assert mock_setup.api.set_mode.call_count == 0 + assert state.state == HVACMode.OFF + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: climate.entity_id, + }, + blocking=True, + ) + state = hass.states.get(climate.entity_id) + + assert mock_setup.api.set_temp.call_count == 1 + assert mock_setup.api.set_power.call_count == 2 + assert mock_setup.api.set_mode.call_count == 1 + assert state.state == HVACMode.HEAT From 8d14095cb92dc936cf3def0adcc7bb60069ed8a9 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 23 Jul 2024 20:59:25 +1000 Subject: [PATCH 1490/2411] Improve API calls in Teslemetry (#122449) * Improve API calls * Small tweak * typing fixtures --- .../components/teslemetry/coordinator.py | 11 ++++++++++- tests/components/teslemetry/conftest.py | 7 ++++--- tests/components/teslemetry/test_init.py | 19 ++++++++++++++++++- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py index cc29bc8ad18..11fc49e86ee 100644 --- a/homeassistant/components/teslemetry/coordinator.py +++ b/homeassistant/components/teslemetry/coordinator.py @@ -75,7 +75,16 @@ class TeslemetryVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.update_interval = VEHICLE_INTERVAL try: - data = (await self.api.vehicle_data(endpoints=ENDPOINTS))["response"] + if self.data["state"] != TeslemetryState.ONLINE: + response = await self.api.vehicle() + self.data["state"] = response["response"]["state"] + + if self.data["state"] != TeslemetryState.ONLINE: + return self.data + + response = await self.api.vehicle_data(endpoints=ENDPOINTS) + data = response["response"] + except VehicleOffline: self.data["state"] = TeslemetryState.OFFLINE return self.data diff --git a/tests/components/teslemetry/conftest.py b/tests/components/teslemetry/conftest.py index 410eaa62b69..03b9e2c6eb6 100644 --- a/tests/components/teslemetry/conftest.py +++ b/tests/components/teslemetry/conftest.py @@ -2,8 +2,9 @@ from __future__ import annotations +from collections.abc import Generator from copy import deepcopy -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest @@ -37,7 +38,7 @@ def mock_products(): @pytest.fixture(autouse=True) -def mock_vehicle_data(): +def mock_vehicle_data() -> Generator[AsyncMock]: """Mock Tesla Fleet API Vehicle Specific vehicle_data method.""" with patch( "homeassistant.components.teslemetry.VehicleSpecific.vehicle_data", @@ -57,7 +58,7 @@ def mock_wake_up(): @pytest.fixture(autouse=True) -def mock_vehicle(): +def mock_vehicle() -> Generator[AsyncMock]: """Mock Tesla Fleet API Vehicle Specific vehicle method.""" with patch( "homeassistant.components.teslemetry.VehicleSpecific.vehicle", diff --git a/tests/components/teslemetry/test_init.py b/tests/components/teslemetry/test_init.py index 31b4202b521..5520a5549bd 100644 --- a/tests/components/teslemetry/test_init.py +++ b/tests/components/teslemetry/test_init.py @@ -1,5 +1,7 @@ """Test the Teslemetry init.""" +from unittest.mock import AsyncMock + from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion @@ -21,7 +23,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from . import setup_platform -from .const import VEHICLE_DATA_ALT +from .const import VEHICLE_DATA_ALT, WAKE_UP_ASLEEP from tests.common import async_fire_time_changed @@ -68,6 +70,21 @@ async def test_devices( # Vehicle Coordinator +async def test_vehicle_refresh_asleep( + hass: HomeAssistant, + mock_vehicle: AsyncMock, + mock_vehicle_data: AsyncMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test coordinator refresh with an error.""" + + mock_vehicle.return_value = WAKE_UP_ASLEEP + entry = await setup_platform(hass, [Platform.CLIMATE]) + assert entry.state is ConfigEntryState.LOADED + mock_vehicle.assert_called_once() + mock_vehicle_data.assert_not_called() + + async def test_vehicle_refresh_offline( hass: HomeAssistant, mock_vehicle_data, freezer: FrozenDateTimeFactory ) -> None: From 0d765a27c915d9273a4297259d619b74bbe74880 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 23 Jul 2024 13:12:29 +0200 Subject: [PATCH 1491/2411] Add created_at/modified_at to entity registry (#122444) --- homeassistant/helpers/entity_registry.py | 172 ++++++++++-------- .../components/config/test_entity_registry.py | 102 ++++++++++- .../homekit_controller/test_init.py | 2 + tests/helpers/test_entity_platform.py | 3 + tests/helpers/test_entity_registry.py | 79 +++++++- tests/syrupy.py | 2 +- 6 files changed, 277 insertions(+), 83 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index dabe2e61917..a0bc63786d8 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -48,6 +48,7 @@ from homeassistant.core import ( from homeassistant.exceptions import MaxLengthExceeded from homeassistant.loader import async_suggest_report_issue from homeassistant.util import slugify, uuid as uuid_util +from homeassistant.util.dt import utc_from_timestamp, utcnow from homeassistant.util.event_type import EventType from homeassistant.util.hass_dict import HassKey from homeassistant.util.json import format_unserializable_data @@ -74,7 +75,7 @@ EVENT_ENTITY_REGISTRY_UPDATED: EventType[EventEntityRegistryUpdatedData] = Event _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 14 +STORAGE_VERSION_MINOR = 15 STORAGE_KEY = "core.entity_registry" CLEANUP_INTERVAL = 3600 * 24 @@ -174,6 +175,7 @@ class RegistryEntry: categories: dict[str, str] = attr.ib(factory=dict) capabilities: Mapping[str, Any] | None = attr.ib(default=None) config_entry_id: str | None = attr.ib(default=None) + created_at: datetime = attr.ib(factory=utcnow) device_class: str | None = attr.ib(default=None) device_id: str | None = attr.ib(default=None) domain: str = attr.ib(init=False, repr=False) @@ -187,6 +189,7 @@ class RegistryEntry: ) has_entity_name: bool = attr.ib(default=False) labels: set[str] = attr.ib(factory=set) + modified_at: datetime = attr.ib(factory=utcnow) name: str | None = attr.ib(default=None) options: ReadOnlyEntityOptionsType = attr.ib( default=None, converter=_protect_entity_options @@ -271,6 +274,7 @@ class RegistryEntry: "area_id": self.area_id, "categories": self.categories, "config_entry_id": self.config_entry_id, + "created_at": self.created_at.timestamp(), "device_id": self.device_id, "disabled_by": self.disabled_by, "entity_category": self.entity_category, @@ -280,6 +284,7 @@ class RegistryEntry: "icon": self.icon, "id": self.id, "labels": list(self.labels), + "modified_at": self.modified_at.timestamp(), "name": self.name, "options": self.options, "original_name": self.original_name, @@ -330,6 +335,7 @@ class RegistryEntry: "categories": self.categories, "capabilities": self.capabilities, "config_entry_id": self.config_entry_id, + "created_at": self.created_at.isoformat(), "device_class": self.device_class, "device_id": self.device_id, "disabled_by": self.disabled_by, @@ -340,6 +346,7 @@ class RegistryEntry: "id": self.id, "has_entity_name": self.has_entity_name, "labels": list(self.labels), + "modified_at": self.modified_at.isoformat(), "name": self.name, "options": self.options, "original_device_class": self.original_device_class, @@ -395,6 +402,8 @@ class DeletedRegistryEntry: domain: str = attr.ib(init=False, repr=False) id: str = attr.ib() orphaned_timestamp: float | None = attr.ib() + created_at: datetime = attr.ib(factory=utcnow) + modified_at: datetime = attr.ib(factory=utcnow) @domain.default def _domain_default(self) -> str: @@ -408,8 +417,10 @@ class DeletedRegistryEntry: json_bytes( { "config_entry_id": self.config_entry_id, + "created_at": self.created_at.isoformat(), "entity_id": self.entity_id, "id": self.id, + "modified_at": self.modified_at.isoformat(), "orphaned_timestamp": self.orphaned_timestamp, "platform": self.platform, "unique_id": self.unique_id, @@ -429,88 +440,97 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): ) -> dict: """Migrate to the new version.""" data = old_data - if old_major_version == 1 and old_minor_version < 2: - # Version 1.2 implements migration and freezes the available keys - for entity in data["entities"]: - # Populate keys which were introduced before version 1.2 - entity.setdefault("area_id", None) - entity.setdefault("capabilities", {}) - entity.setdefault("config_entry_id", None) - entity.setdefault("device_class", None) - entity.setdefault("device_id", None) - entity.setdefault("disabled_by", None) - entity.setdefault("entity_category", None) - entity.setdefault("icon", None) - entity.setdefault("name", None) - entity.setdefault("original_icon", None) - entity.setdefault("original_name", None) - entity.setdefault("supported_features", 0) - entity.setdefault("unit_of_measurement", None) + if old_major_version == 1: + if old_minor_version < 2: + # Version 1.2 implements migration and freezes the available keys + for entity in data["entities"]: + # Populate keys which were introduced before version 1.2 + entity.setdefault("area_id", None) + entity.setdefault("capabilities", {}) + entity.setdefault("config_entry_id", None) + entity.setdefault("device_class", None) + entity.setdefault("device_id", None) + entity.setdefault("disabled_by", None) + entity.setdefault("entity_category", None) + entity.setdefault("icon", None) + entity.setdefault("name", None) + entity.setdefault("original_icon", None) + entity.setdefault("original_name", None) + entity.setdefault("supported_features", 0) + entity.setdefault("unit_of_measurement", None) - if old_major_version == 1 and old_minor_version < 3: - # Version 1.3 adds original_device_class - for entity in data["entities"]: - # Move device_class to original_device_class - entity["original_device_class"] = entity["device_class"] - entity["device_class"] = None + if old_minor_version < 3: + # Version 1.3 adds original_device_class + for entity in data["entities"]: + # Move device_class to original_device_class + entity["original_device_class"] = entity["device_class"] + entity["device_class"] = None - if old_major_version == 1 and old_minor_version < 4: - # Version 1.4 adds id - for entity in data["entities"]: - entity["id"] = uuid_util.random_uuid_hex() + if old_minor_version < 4: + # Version 1.4 adds id + for entity in data["entities"]: + entity["id"] = uuid_util.random_uuid_hex() - if old_major_version == 1 and old_minor_version < 5: - # Version 1.5 adds entity options - for entity in data["entities"]: - entity["options"] = {} + if old_minor_version < 5: + # Version 1.5 adds entity options + for entity in data["entities"]: + entity["options"] = {} - if old_major_version == 1 and old_minor_version < 6: - # Version 1.6 adds hidden_by - for entity in data["entities"]: - entity["hidden_by"] = None + if old_minor_version < 6: + # Version 1.6 adds hidden_by + for entity in data["entities"]: + entity["hidden_by"] = None - if old_major_version == 1 and old_minor_version < 7: - # Version 1.7 adds has_entity_name - for entity in data["entities"]: - entity["has_entity_name"] = False + if old_minor_version < 7: + # Version 1.7 adds has_entity_name + for entity in data["entities"]: + entity["has_entity_name"] = False - if old_major_version == 1 and old_minor_version < 8: - # Cleanup after frontend bug which incorrectly updated device_class - # Fixed by frontend PR #13551 - for entity in data["entities"]: - domain = split_entity_id(entity["entity_id"])[0] - if domain in [Platform.BINARY_SENSOR, Platform.COVER]: - continue - entity["device_class"] = None + if old_minor_version < 8: + # Cleanup after frontend bug which incorrectly updated device_class + # Fixed by frontend PR #13551 + for entity in data["entities"]: + domain = split_entity_id(entity["entity_id"])[0] + if domain in [Platform.BINARY_SENSOR, Platform.COVER]: + continue + entity["device_class"] = None - if old_major_version == 1 and old_minor_version < 9: - # Version 1.9 adds translation_key - for entity in data["entities"]: - entity["translation_key"] = None + if old_minor_version < 9: + # Version 1.9 adds translation_key + for entity in data["entities"]: + entity["translation_key"] = None - if old_major_version == 1 and old_minor_version < 10: - # Version 1.10 adds aliases - for entity in data["entities"]: - entity["aliases"] = [] + if old_minor_version < 10: + # Version 1.10 adds aliases + for entity in data["entities"]: + entity["aliases"] = [] - if old_major_version == 1 and old_minor_version < 11: - # Version 1.11 adds deleted_entities - data["deleted_entities"] = data.get("deleted_entities", []) + if old_minor_version < 11: + # Version 1.11 adds deleted_entities + data["deleted_entities"] = data.get("deleted_entities", []) - if old_major_version == 1 and old_minor_version < 12: - # Version 1.12 adds previous_unique_id - for entity in data["entities"]: - entity["previous_unique_id"] = None + if old_minor_version < 12: + # Version 1.12 adds previous_unique_id + for entity in data["entities"]: + entity["previous_unique_id"] = None - if old_major_version == 1 and old_minor_version < 13: - # Version 1.13 adds labels - for entity in data["entities"]: - entity["labels"] = [] + if old_minor_version < 13: + # Version 1.13 adds labels + for entity in data["entities"]: + entity["labels"] = [] - if old_major_version == 1 and old_minor_version < 14: - # Version 1.14 adds categories - for entity in data["entities"]: - entity["categories"] = {} + if old_minor_version < 14: + # Version 1.14 adds categories + for entity in data["entities"]: + entity["categories"] = {} + + if old_minor_version < 15: + # Version 1.15 adds created_at and modified_at + created_at = utc_from_timestamp(0).isoformat() + for entity in data["entities"]: + entity["created_at"] = entity["modified_at"] = created_at + for entity in data["deleted_entities"]: + entity["created_at"] = entity["modified_at"] = created_at if old_major_version > 1: raise NotImplementedError @@ -837,10 +857,12 @@ class EntityRegistry(BaseRegistry): ) entity_registry_id: str | None = None + created_at = utcnow() deleted_entity = self.deleted_entities.pop((domain, platform, unique_id), None) if deleted_entity is not None: # Restore id entity_registry_id = deleted_entity.id + created_at = deleted_entity.created_at entity_id = self.async_generate_entity_id( domain, @@ -865,6 +887,7 @@ class EntityRegistry(BaseRegistry): entry = RegistryEntry( capabilities=none_if_undefined(capabilities), config_entry_id=none_if_undefined(config_entry_id), + created_at=created_at, device_id=none_if_undefined(device_id), disabled_by=disabled_by, entity_category=none_if_undefined(entity_category), @@ -906,6 +929,7 @@ class EntityRegistry(BaseRegistry): orphaned_timestamp = None if config_entry_id else time.time() self.deleted_entities[key] = DeletedRegistryEntry( config_entry_id=config_entry_id, + created_at=entity.created_at, entity_id=entity_id, id=entity.id, orphaned_timestamp=orphaned_timestamp, @@ -1093,6 +1117,8 @@ class EntityRegistry(BaseRegistry): if not new_values: return old + new_values["modified_at"] = utcnow() + self.hass.verify_event_loop_thread("entity_registry.async_update_entity") new = self.entities[entity_id] = attr.evolve(old, **new_values) @@ -1260,6 +1286,7 @@ class EntityRegistry(BaseRegistry): categories=entity["categories"], capabilities=entity["capabilities"], config_entry_id=entity["config_entry_id"], + created_at=entity["created_at"], device_class=entity["device_class"], device_id=entity["device_id"], disabled_by=RegistryEntryDisabler(entity["disabled_by"]) @@ -1276,6 +1303,7 @@ class EntityRegistry(BaseRegistry): id=entity["id"], has_entity_name=entity["has_entity_name"], labels=set(entity["labels"]), + modified_at=entity["modified_at"], name=entity["name"], options=entity["options"], original_device_class=entity["original_device_class"], @@ -1307,8 +1335,10 @@ class EntityRegistry(BaseRegistry): ) deleted_entities[key] = DeletedRegistryEntry( config_entry_id=entity["config_entry_id"], + created_at=entity["created_at"], entity_id=entity["entity_id"], id=entity["id"], + modified_at=entity["modified_at"], orphaned_timestamp=entity["orphaned_timestamp"], platform=entity["platform"], unique_id=entity["unique_id"], diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 813ec654abb..60657d4a77b 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -1,5 +1,8 @@ """Test entity_registry API.""" +from datetime import datetime + +from freezegun.api import FrozenDateTimeFactory import pytest from pytest_unordered import unordered @@ -13,6 +16,7 @@ from homeassistant.helpers.entity_registry import ( RegistryEntryDisabler, RegistryEntryHider, ) +from homeassistant.util.dt import utcnow from tests.common import ( ANY, @@ -33,6 +37,7 @@ async def client( return await hass_ws_client(hass) +@pytest.mark.usefixtures("freezer") async def test_list_entities( hass: HomeAssistant, client: MockHAClientWebSocket ) -> None: @@ -62,6 +67,7 @@ async def test_list_entities( "area_id": None, "categories": {}, "config_entry_id": None, + "created_at": utcnow().timestamp(), "device_id": None, "disabled_by": None, "entity_category": None, @@ -71,6 +77,7 @@ async def test_list_entities( "icon": None, "id": ANY, "labels": [], + "modified_at": utcnow().timestamp(), "name": "Hello World", "options": {}, "original_name": None, @@ -82,6 +89,7 @@ async def test_list_entities( "area_id": None, "categories": {}, "config_entry_id": None, + "created_at": utcnow().timestamp(), "device_id": None, "disabled_by": None, "entity_category": None, @@ -91,6 +99,7 @@ async def test_list_entities( "icon": None, "id": ANY, "labels": [], + "modified_at": utcnow().timestamp(), "name": None, "options": {}, "original_name": None, @@ -129,6 +138,7 @@ async def test_list_entities( "area_id": None, "categories": {}, "config_entry_id": None, + "created_at": utcnow().timestamp(), "device_id": None, "disabled_by": None, "entity_category": None, @@ -138,6 +148,7 @@ async def test_list_entities( "icon": None, "id": ANY, "labels": [], + "modified_at": utcnow().timestamp(), "name": "Hello World", "options": {}, "original_name": None, @@ -325,6 +336,8 @@ async def test_list_entities_for_display( async def test_get_entity(hass: HomeAssistant, client: MockHAClientWebSocket) -> None: """Test get entry.""" + name_created_at = datetime(1994, 2, 14, 12, 0, 0) + no_name_created_at = datetime(2024, 2, 14, 12, 0, 1) mock_registry( hass, { @@ -333,11 +346,15 @@ async def test_get_entity(hass: HomeAssistant, client: MockHAClientWebSocket) -> unique_id="1234", platform="test_platform", name="Hello World", + created_at=name_created_at, + modified_at=name_created_at, ), "test_domain.no_name": RegistryEntry( entity_id="test_domain.no_name", unique_id="6789", platform="test_platform", + created_at=no_name_created_at, + modified_at=no_name_created_at, ), }, ) @@ -353,6 +370,7 @@ async def test_get_entity(hass: HomeAssistant, client: MockHAClientWebSocket) -> "capabilities": None, "categories": {}, "config_entry_id": None, + "created_at": name_created_at.timestamp(), "device_class": None, "device_id": None, "disabled_by": None, @@ -363,6 +381,7 @@ async def test_get_entity(hass: HomeAssistant, client: MockHAClientWebSocket) -> "icon": None, "id": ANY, "labels": [], + "modified_at": name_created_at.timestamp(), "name": "Hello World", "options": {}, "original_device_class": None, @@ -387,6 +406,7 @@ async def test_get_entity(hass: HomeAssistant, client: MockHAClientWebSocket) -> "capabilities": None, "categories": {}, "config_entry_id": None, + "created_at": no_name_created_at.timestamp(), "device_class": None, "device_id": None, "disabled_by": None, @@ -397,6 +417,7 @@ async def test_get_entity(hass: HomeAssistant, client: MockHAClientWebSocket) -> "icon": None, "id": ANY, "labels": [], + "modified_at": no_name_created_at.timestamp(), "name": None, "options": {}, "original_device_class": None, @@ -410,6 +431,8 @@ async def test_get_entity(hass: HomeAssistant, client: MockHAClientWebSocket) -> async def test_get_entities(hass: HomeAssistant, client: MockHAClientWebSocket) -> None: """Test get entry.""" + name_created_at = datetime(1994, 2, 14, 12, 0, 0) + no_name_created_at = datetime(2024, 2, 14, 12, 0, 1) mock_registry( hass, { @@ -418,11 +441,15 @@ async def test_get_entities(hass: HomeAssistant, client: MockHAClientWebSocket) unique_id="1234", platform="test_platform", name="Hello World", + created_at=name_created_at, + modified_at=name_created_at, ), "test_domain.no_name": RegistryEntry( entity_id="test_domain.no_name", unique_id="6789", platform="test_platform", + created_at=no_name_created_at, + modified_at=no_name_created_at, ), }, ) @@ -446,6 +473,7 @@ async def test_get_entities(hass: HomeAssistant, client: MockHAClientWebSocket) "capabilities": None, "categories": {}, "config_entry_id": None, + "created_at": name_created_at.timestamp(), "device_class": None, "device_id": None, "disabled_by": None, @@ -456,6 +484,7 @@ async def test_get_entities(hass: HomeAssistant, client: MockHAClientWebSocket) "icon": None, "id": ANY, "labels": [], + "modified_at": name_created_at.timestamp(), "name": "Hello World", "options": {}, "original_device_class": None, @@ -471,6 +500,7 @@ async def test_get_entities(hass: HomeAssistant, client: MockHAClientWebSocket) "capabilities": None, "categories": {}, "config_entry_id": None, + "created_at": no_name_created_at.timestamp(), "device_class": None, "device_id": None, "disabled_by": None, @@ -481,6 +511,7 @@ async def test_get_entities(hass: HomeAssistant, client: MockHAClientWebSocket) "icon": None, "id": ANY, "labels": [], + "modified_at": no_name_created_at.timestamp(), "name": None, "options": {}, "original_device_class": None, @@ -495,9 +526,11 @@ async def test_get_entities(hass: HomeAssistant, client: MockHAClientWebSocket) async def test_update_entity( - hass: HomeAssistant, client: MockHAClientWebSocket + hass: HomeAssistant, client: MockHAClientWebSocket, freezer: FrozenDateTimeFactory ) -> None: """Test updating entity.""" + created = datetime.fromisoformat("2024-02-14T12:00:00.900075+00:00") + freezer.move_to(created) registry = mock_registry( hass, { @@ -520,6 +553,9 @@ async def test_update_entity( assert state.name == "before update" assert state.attributes[ATTR_ICON] == "icon:before update" + modified = datetime.fromisoformat("2024-07-17T13:30:00.900075+00:00") + freezer.move_to(modified) + # Update area, categories, device_class, hidden_by, icon, labels & name await client.send_json_auto_id( { @@ -544,6 +580,7 @@ async def test_update_entity( "area_id": "mock-area-id", "capabilities": None, "categories": {"scope1": "id", "scope2": "id"}, + "created_at": created.timestamp(), "config_entry_id": None, "device_class": "custom_device_class", "device_id": None, @@ -555,6 +592,7 @@ async def test_update_entity( "icon": "icon:after update", "id": ANY, "labels": unordered(["label1", "label2"]), + "modified_at": modified.timestamp(), "name": "after update", "options": {}, "original_device_class": None, @@ -570,6 +608,9 @@ async def test_update_entity( assert state.name == "after update" assert state.attributes[ATTR_ICON] == "icon:after update" + modified = datetime.fromisoformat("2024-07-20T00:00:00.900075+00:00") + freezer.move_to(modified) + # Update hidden_by to illegal value await client.send_json_auto_id( { @@ -597,9 +638,13 @@ async def test_update_entity( assert msg["success"] assert hass.states.get("test_domain.world") is None - assert ( - registry.entities["test_domain.world"].disabled_by is RegistryEntryDisabler.USER - ) + entry = registry.entities["test_domain.world"] + assert entry.disabled_by is RegistryEntryDisabler.USER + assert entry.created_at == created + assert entry.modified_at == modified + + modified = datetime.fromisoformat("2024-07-21T00:00:00.900075+00:00") + freezer.move_to(modified) # Update disabled_by to None await client.send_json_auto_id( @@ -619,6 +664,7 @@ async def test_update_entity( "capabilities": None, "categories": {"scope1": "id", "scope2": "id"}, "config_entry_id": None, + "created_at": created.timestamp(), "device_class": "custom_device_class", "device_id": None, "disabled_by": None, @@ -629,6 +675,7 @@ async def test_update_entity( "icon": "icon:after update", "id": ANY, "labels": unordered(["label1", "label2"]), + "modified_at": modified.timestamp(), "name": "after update", "options": {}, "original_device_class": None, @@ -641,6 +688,9 @@ async def test_update_entity( "require_restart": True, } + modified = datetime.fromisoformat("2024-07-22T00:00:00.900075+00:00") + freezer.move_to(modified) + # Update entity option await client.send_json_auto_id( { @@ -660,6 +710,7 @@ async def test_update_entity( "capabilities": None, "categories": {"scope1": "id", "scope2": "id"}, "config_entry_id": None, + "created_at": created.timestamp(), "device_class": "custom_device_class", "device_id": None, "disabled_by": None, @@ -670,6 +721,7 @@ async def test_update_entity( "icon": "icon:after update", "id": ANY, "labels": unordered(["label1", "label2"]), + "modified_at": modified.timestamp(), "name": "after update", "options": {"sensor": {"unit_of_measurement": "beard_second"}}, "original_device_class": None, @@ -681,6 +733,9 @@ async def test_update_entity( }, } + modified = datetime.fromisoformat("2024-07-23T00:00:00.900075+00:00") + freezer.move_to(modified) + # Add a category to the entity await client.send_json_auto_id( { @@ -700,6 +755,7 @@ async def test_update_entity( "capabilities": None, "categories": {"scope1": "id", "scope2": "id", "scope3": "id"}, "config_entry_id": None, + "created_at": created.timestamp(), "device_class": "custom_device_class", "device_id": None, "disabled_by": None, @@ -710,6 +766,7 @@ async def test_update_entity( "icon": "icon:after update", "id": ANY, "labels": unordered(["label1", "label2"]), + "modified_at": modified.timestamp(), "name": "after update", "options": {"sensor": {"unit_of_measurement": "beard_second"}}, "original_device_class": None, @@ -721,6 +778,9 @@ async def test_update_entity( }, } + modified = datetime.fromisoformat("2024-07-24T00:00:00.900075+00:00") + freezer.move_to(modified) + # Move the entity to a different category await client.send_json_auto_id( { @@ -740,6 +800,7 @@ async def test_update_entity( "capabilities": None, "categories": {"scope1": "id", "scope2": "id", "scope3": "other_id"}, "config_entry_id": None, + "created_at": created.timestamp(), "device_class": "custom_device_class", "device_id": None, "disabled_by": None, @@ -750,6 +811,7 @@ async def test_update_entity( "icon": "icon:after update", "id": ANY, "labels": unordered(["label1", "label2"]), + "modified_at": modified.timestamp(), "name": "after update", "options": {"sensor": {"unit_of_measurement": "beard_second"}}, "original_device_class": None, @@ -761,6 +823,9 @@ async def test_update_entity( }, } + modified = datetime.fromisoformat("2024-07-23T10:00:00.900075+00:00") + freezer.move_to(modified) + # Move the entity to a different category await client.send_json_auto_id( { @@ -780,6 +845,7 @@ async def test_update_entity( "capabilities": None, "categories": {"scope1": "id", "scope3": "other_id"}, "config_entry_id": None, + "created_at": created.timestamp(), "device_class": "custom_device_class", "device_id": None, "disabled_by": None, @@ -790,6 +856,7 @@ async def test_update_entity( "icon": "icon:after update", "id": ANY, "labels": unordered(["label1", "label2"]), + "modified_at": modified.timestamp(), "name": "after update", "options": {"sensor": {"unit_of_measurement": "beard_second"}}, "original_device_class": None, @@ -803,9 +870,11 @@ async def test_update_entity( async def test_update_entity_require_restart( - hass: HomeAssistant, client: MockHAClientWebSocket + hass: HomeAssistant, client: MockHAClientWebSocket, freezer: FrozenDateTimeFactory ) -> None: """Test updating entity.""" + created = datetime.fromisoformat("2024-02-14T12:00:00+00:00") + freezer.move_to(created) entity_id = "test_domain.test_platform_1234" config_entry = MockConfigEntry(domain="test_platform") config_entry.add_to_hass(hass) @@ -817,6 +886,9 @@ async def test_update_entity_require_restart( state = hass.states.get(entity_id) assert state is not None + modified = datetime.fromisoformat("2024-07-20T13:30:00+00:00") + freezer.move_to(modified) + # UPDATE DISABLED_BY TO NONE await client.send_json_auto_id( { @@ -835,6 +907,7 @@ async def test_update_entity_require_restart( "capabilities": None, "categories": {}, "config_entry_id": config_entry.entry_id, + "created_at": created.timestamp(), "device_class": None, "device_id": None, "disabled_by": None, @@ -845,6 +918,7 @@ async def test_update_entity_require_restart( "icon": None, "id": ANY, "labels": [], + "modified_at": created.timestamp(), "name": None, "options": {}, "original_device_class": None, @@ -909,9 +983,11 @@ async def test_enable_entity_disabled_device( async def test_update_entity_no_changes( - hass: HomeAssistant, client: MockHAClientWebSocket + hass: HomeAssistant, client: MockHAClientWebSocket, freezer: FrozenDateTimeFactory ) -> None: """Test update entity with no changes.""" + created = datetime.fromisoformat("2024-02-14T12:00:00.900075+00:00") + freezer.move_to(created) mock_registry( hass, { @@ -932,6 +1008,9 @@ async def test_update_entity_no_changes( assert state is not None assert state.name == "name of entity" + modified = datetime.fromisoformat("2024-07-20T13:30:00.900075+00:00") + freezer.move_to(modified) + await client.send_json_auto_id( { "type": "config/entity_registry/update", @@ -949,6 +1028,7 @@ async def test_update_entity_no_changes( "capabilities": None, "categories": {}, "config_entry_id": None, + "created_at": created.timestamp(), "device_class": None, "device_id": None, "disabled_by": None, @@ -959,6 +1039,7 @@ async def test_update_entity_no_changes( "icon": None, "id": ANY, "labels": [], + "modified_at": created.timestamp(), "name": "name of entity", "options": {}, "original_device_class": None, @@ -1002,9 +1083,11 @@ async def test_update_nonexisting_entity(client: MockHAClientWebSocket) -> None: async def test_update_entity_id( - hass: HomeAssistant, client: MockHAClientWebSocket + hass: HomeAssistant, client: MockHAClientWebSocket, freezer: FrozenDateTimeFactory ) -> None: """Test update entity id.""" + created = datetime.fromisoformat("2024-02-14T12:00:00.900075+00:00") + freezer.move_to(created) mock_registry( hass, { @@ -1022,6 +1105,9 @@ async def test_update_entity_id( assert hass.states.get("test_domain.world") is not None + modified = datetime.fromisoformat("2024-07-20T13:30:00.900075+00:00") + freezer.move_to(modified) + await client.send_json_auto_id( { "type": "config/entity_registry/update", @@ -1039,6 +1125,7 @@ async def test_update_entity_id( "capabilities": None, "categories": {}, "config_entry_id": None, + "created_at": created.timestamp(), "device_class": None, "device_id": None, "disabled_by": None, @@ -1049,6 +1136,7 @@ async def test_update_entity_id( "icon": None, "id": ANY, "labels": [], + "modified_at": modified.timestamp(), "name": None, "options": {}, "original_device_class": None, diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 02e57734b3a..c443e56b3a4 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -287,6 +287,8 @@ async def test_snapshots( entry = asdict(entity_entry) entry.pop("id", None) entry.pop("device_id", None) + entry.pop("created_at", None) + entry.pop("modified_at", None) entities.append({"entry": entry, "state": state_dict}) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index ff08eb5de04..75a41945a91 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1422,6 +1422,7 @@ async def test_entity_hidden_by_integration( assert entry_hidden.hidden_by is er.RegistryEntryHider.INTEGRATION +@pytest.mark.usefixtures("freezer") async def test_entity_info_added_to_entity_registry( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: @@ -1450,11 +1451,13 @@ async def test_entity_info_added_to_entity_registry( "default", "test_domain", capabilities={"max": 100}, + created_at=dt_util.utcnow(), device_class=None, entity_category=EntityCategory.CONFIG, has_entity_name=True, icon=None, id=ANY, + modified_at=dt_util.utcnow(), name=None, original_device_class="mock-device-class", original_icon="nice:icon", diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 4dc8d79be3f..afcd0d0ed2e 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1,6 +1,6 @@ """Tests for the Entity Registry.""" -from datetime import timedelta +from datetime import datetime, timedelta from functools import partial from typing import Any from unittest.mock import patch @@ -21,6 +21,7 @@ from homeassistant.exceptions import MaxLengthExceeded from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import ( + ANY, MockConfigEntry, async_capture_events, async_fire_time_changed, @@ -69,9 +70,14 @@ def test_get_or_create_suggested_object_id(entity_registry: er.EntityRegistry) - assert entry.entity_id == "light.beer" -def test_get_or_create_updates_data(entity_registry: er.EntityRegistry) -> None: +def test_get_or_create_updates_data( + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: """Test that we update data in get_or_create.""" orig_config_entry = MockConfigEntry(domain="light") + created = datetime.fromisoformat("2024-02-14T12:00:00.0+00:00") + freezer.move_to(created) orig_entry = entity_registry.async_get_or_create( "light", @@ -100,6 +106,7 @@ def test_get_or_create_updates_data(entity_registry: er.EntityRegistry) -> None: "hue", capabilities={"max": 100}, config_entry_id=orig_config_entry.entry_id, + created_at=created, device_class=None, device_id="mock-dev-id", disabled_by=er.RegistryEntryDisabler.HASS, @@ -108,6 +115,7 @@ def test_get_or_create_updates_data(entity_registry: er.EntityRegistry) -> None: hidden_by=er.RegistryEntryHider.INTEGRATION, icon=None, id=orig_entry.id, + modified_at=created, name=None, original_device_class="mock-device-class", original_icon="initial-original_icon", @@ -118,6 +126,8 @@ def test_get_or_create_updates_data(entity_registry: er.EntityRegistry) -> None: ) new_config_entry = MockConfigEntry(domain="light") + modified = created + timedelta(minutes=5) + freezer.move_to(modified) new_entry = entity_registry.async_get_or_create( "light", @@ -146,6 +156,7 @@ def test_get_or_create_updates_data(entity_registry: er.EntityRegistry) -> None: area_id=None, capabilities={"new-max": 150}, config_entry_id=new_config_entry.entry_id, + created_at=created, device_class=None, device_id="new-mock-dev-id", disabled_by=er.RegistryEntryDisabler.HASS, # Should not be updated @@ -154,6 +165,7 @@ def test_get_or_create_updates_data(entity_registry: er.EntityRegistry) -> None: hidden_by=er.RegistryEntryHider.INTEGRATION, # Should not be updated icon=None, id=orig_entry.id, + modified_at=modified, name=None, original_device_class="new-mock-device-class", original_icon="updated-original_icon", @@ -164,6 +176,8 @@ def test_get_or_create_updates_data(entity_registry: er.EntityRegistry) -> None: ) assert set(entity_registry.async_device_ids()) == {"new-mock-dev-id"} + modified = created + timedelta(minutes=5) + freezer.move_to(modified) new_entry = entity_registry.async_get_or_create( "light", @@ -192,6 +206,7 @@ def test_get_or_create_updates_data(entity_registry: er.EntityRegistry) -> None: area_id=None, capabilities=None, config_entry_id=None, + created_at=created, device_class=None, device_id=None, disabled_by=er.RegistryEntryDisabler.HASS, # Should not be updated @@ -200,6 +215,7 @@ def test_get_or_create_updates_data(entity_registry: er.EntityRegistry) -> None: hidden_by=er.RegistryEntryHider.INTEGRATION, # Should not be updated icon=None, id=orig_entry.id, + modified_at=modified, name=None, original_device_class=None, original_icon=None, @@ -309,8 +325,12 @@ async def test_loading_saving_data( assert orig_entry1 == new_entry1 assert orig_entry2 == new_entry2 - assert orig_entry3 == new_entry3 - assert orig_entry4 == new_entry4 + + # By converting a deleted device to a active device, the modified_at will be updated + assert orig_entry3.modified_at < new_entry3.modified_at + assert attr.evolve(orig_entry3, modified_at=new_entry3.modified_at) == new_entry3 + assert orig_entry4.modified_at < new_entry4.modified_at + assert attr.evolve(orig_entry4, modified_at=new_entry4.modified_at) == new_entry4 assert new_entry2.area_id == "mock-area-id" assert new_entry2.categories == {"scope", "id"} @@ -453,6 +473,7 @@ async def test_load_bad_data( "capabilities": None, "categories": {}, "config_entry_id": None, + "created_at": "2024-02-14T12:00:00.900075+00:00", "device_class": None, "device_id": None, "disabled_by": None, @@ -463,6 +484,7 @@ async def test_load_bad_data( "icon": None, "id": "00001", "labels": [], + "modified_at": "2024-02-14T12:00:00.900075+00:00", "name": None, "options": None, "original_device_class": None, @@ -481,6 +503,7 @@ async def test_load_bad_data( "capabilities": None, "categories": {}, "config_entry_id": None, + "created_at": "2024-02-14T12:00:00.900075+00:00", "device_class": None, "device_id": None, "disabled_by": None, @@ -491,6 +514,7 @@ async def test_load_bad_data( "icon": None, "id": "00002", "labels": [], + "modified_at": "2024-02-14T12:00:00.900075+00:00", "name": None, "options": None, "original_device_class": None, @@ -507,16 +531,20 @@ async def test_load_bad_data( "deleted_entities": [ { "config_entry_id": None, + "created_at": "2024-02-14T12:00:00.900075+00:00", "entity_id": "test.test3", "id": "00003", + "modified_at": "2024-02-14T12:00:00.900075+00:00", "orphaned_timestamp": None, "platform": "super_platform", "unique_id": 234, # Should not load }, { "config_entry_id": None, + "created_at": "2024-02-14T12:00:00.900075+00:00", "entity_id": "test.test4", "id": "00004", + "modified_at": "2024-02-14T12:00:00.900075+00:00", "orphaned_timestamp": None, "platform": "super_platform", "unique_id": ["also", "not", "valid"], # Should not load @@ -695,6 +723,49 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any]) assert entry.device_class is None assert entry.original_device_class == "best_class" + # Check we store migrated data + await flush_store(registry._store) + assert hass_storage[er.STORAGE_KEY] == { + "version": er.STORAGE_VERSION_MAJOR, + "minor_version": er.STORAGE_VERSION_MINOR, + "key": er.STORAGE_KEY, + "data": { + "entities": [ + { + "aliases": [], + "area_id": None, + "capabilities": {}, + "categories": {}, + "config_entry_id": None, + "created_at": "1970-01-01T00:00:00+00:00", + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test.entity", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "id": ANY, + "labels": [], + "modified_at": "1970-01-01T00:00:00+00:00", + "name": None, + "options": {}, + "original_device_class": "best_class", + "original_icon": None, + "original_name": None, + "platform": "super_platform", + "previous_unique_id": None, + "supported_features": 0, + "translation_key": None, + "unique_id": "very_unique", + "unit_of_measurement": None, + "device_class": None, + } + ], + "deleted_entities": [], + }, + } + @pytest.mark.parametrize("load_registries", [False]) async def test_migration_1_7(hass: HomeAssistant, hass_storage: dict[str, Any]) -> None: diff --git a/tests/syrupy.py b/tests/syrupy.py index 80d955f0de1..09e18428015 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -181,7 +181,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): } ) serialized.pop("categories") - return serialized + return cls._remove_created_and_modified_at(serialized) @classmethod def _serializable_flow_result(cls, data: FlowResult) -> SerializableData: From 1fd3c9d6dd705a9097900cb2277fac73dcb49c27 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 23 Jul 2024 13:28:33 +0200 Subject: [PATCH 1492/2411] Replace Reolink HDR switch by HDR select entity (#122373) * Add HDR select * Update strings.json * Update strings.json * add icon * remove HDR switch * cleanup old HDR switch * add tests * Keep HDR switch entity around untill HA 2025.2.0 * Add repair issue * Update strings.json * fixes and review comments * Add tests * Update homeassistant/components/reolink/strings.json Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/reolink/switch.py Co-authored-by: Joost Lekkerkerker * fixes and simplify --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/reolink/icons.json | 3 + homeassistant/components/reolink/select.py | 12 +++ homeassistant/components/reolink/strings.json | 14 +++- homeassistant/components/reolink/switch.py | 48 ++++++++--- tests/components/reolink/test_switch.py | 81 +++++++++++++++++++ 5 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 tests/components/reolink/test_switch.py diff --git a/homeassistant/components/reolink/icons.json b/homeassistant/components/reolink/icons.json index a4620bd95d5..539c2461204 100644 --- a/homeassistant/components/reolink/icons.json +++ b/homeassistant/components/reolink/icons.json @@ -203,6 +203,9 @@ }, "status_led": { "default": "mdi:lightning-bolt-circle" + }, + "hdr": { + "default": "mdi:hdr" } }, "sensor": { diff --git a/homeassistant/components/reolink/select.py b/homeassistant/components/reolink/select.py index 907cc90b8af..cf32d7b45f9 100644 --- a/homeassistant/components/reolink/select.py +++ b/homeassistant/components/reolink/select.py @@ -9,6 +9,7 @@ from typing import Any from reolink_aio.api import ( DayNightEnum, + HDREnum, Host, SpotlightModeEnum, StatusLedEnum, @@ -118,6 +119,17 @@ SELECT_ENTITIES = ( api.set_status_led(ch, StatusLedEnum[name].value, doorbell=True) ), ), + ReolinkSelectEntityDescription( + key="hdr", + cmd_key="GetIsp", + translation_key="hdr", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + get_options=[method.name for method in HDREnum], + supported=lambda api, ch: api.supported(ch, "HDR"), + value=lambda api, ch: HDREnum(api.HDR_state(ch)).name, + method=lambda api, ch, name: api.set_HDR(ch, HDREnum[name].value), + ), ) diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index aa141818ec6..3b9aba84634 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -69,6 +69,10 @@ "firmware_update": { "title": "Reolink firmware update required", "description": "\"{name}\" with model \"{model}\" and hardware version \"{hw_version}\" is running a old firmware version \"{current_firmware}\", while at least firmware version \"{required_firmware}\" is required for proper operation of the Reolink integration. The latest firmware can be downloaded from the [Reolink download center]({download_link})." + }, + "hdr_switch_deprecated": { + "title": "Reolink HDR switch deprecated", + "description": "The Reolink HDR switch entity is deprecated and will be removed in HA 2025.2.0. It has been replaced by a HDR select entity offering options `on`, `off` and `auto`. To remove this issue, please adjust automations accordingly and disable the HDR switch entity." } }, "services": { @@ -478,6 +482,14 @@ "alwaysonatnight": "Auto & always on at night", "alwayson": "Always on" } + }, + "hdr": { + "name": "HDR", + "state": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]", + "auto": "Auto" + } } }, "sensor": { @@ -554,7 +566,7 @@ "name": "Doorbell button sound" }, "hdr": { - "name": "HDR" + "name": "[%key:component::reolink::entity::select::hdr::name%]" }, "pir_enabled": { "name": "PIR enabled" diff --git a/homeassistant/components/reolink/switch.py b/homeassistant/components/reolink/switch.py index 9dfce88f93a..cd74d774bb1 100644 --- a/homeassistant/components/reolink/switch.py +++ b/homeassistant/components/reolink/switch.py @@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ReolinkData @@ -173,16 +174,6 @@ SWITCH_ENTITIES = ( value=lambda api, ch: api.doorbell_button_sound(ch), method=lambda api, ch, value: api.set_volume(ch, doorbell_button_sound=value), ), - ReolinkSwitchEntityDescription( - key="hdr", - cmd_key="GetIsp", - translation_key="hdr", - entity_category=EntityCategory.CONFIG, - entity_registry_enabled_default=False, - supported=lambda api, ch: api.supported(ch, "HDR"), - value=lambda api, ch: api.HDR_on(ch) is True, - method=lambda api, ch, value: api.set_HDR(ch, value), - ), ReolinkSwitchEntityDescription( key="pir_enabled", cmd_key="GetPirInfo", @@ -254,6 +245,18 @@ NVR_SWITCH_ENTITIES = ( ), ) +# Can be removed in HA 2025.2.0 +DEPRECATED_HDR = ReolinkSwitchEntityDescription( + key="hdr", + cmd_key="GetIsp", + translation_key="hdr", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + supported=lambda api, ch: api.supported(ch, "HDR"), + value=lambda api, ch: api.HDR_on(ch) is True, + method=lambda api, ch, value: api.set_HDR(ch, value), +) + async def async_setup_entry( hass: HomeAssistant, @@ -276,6 +279,31 @@ async def async_setup_entry( if entity_description.supported(reolink_data.host.api) ] ) + + # Can be removed in HA 2025.2.0 + entity_reg = er.async_get(hass) + reg_entities = er.async_entries_for_config_entry(entity_reg, config_entry.entry_id) + for entity in reg_entities: + if entity.domain == "switch" and entity.unique_id.endswith("_hdr"): + if entity.disabled: + entity_reg.async_remove(entity.entity_id) + continue + + ir.async_create_issue( + hass, + DOMAIN, + "hdr_switch_deprecated", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="hdr_switch_deprecated", + ) + entities.extend( + ReolinkSwitchEntity(reolink_data, channel, DEPRECATED_HDR) + for channel in reolink_data.host.api.channels + if DEPRECATED_HDR.supported(reolink_data.host.api, channel) + ) + break + async_add_entities(entities) diff --git a/tests/components/reolink/test_switch.py b/tests/components/reolink/test_switch.py new file mode 100644 index 00000000000..ebf805b593d --- /dev/null +++ b/tests/components/reolink/test_switch.py @@ -0,0 +1,81 @@ +"""Test the Reolink switch platform.""" + +from unittest.mock import MagicMock, patch + +from homeassistant.components.reolink import const +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er, issue_registry as ir + +from .conftest import TEST_UID + +from tests.common import MockConfigEntry + + +async def test_cleanup_hdr_switch_( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test cleanup of the HDR switch entity.""" + original_id = f"{TEST_UID}_hdr" + domain = Platform.SWITCH + + reolink_connect.channels = [0] + reolink_connect.supported.return_value = True + + entity_registry.async_get_or_create( + domain=domain, + platform=const.DOMAIN, + unique_id=original_id, + config_entry=config_entry, + suggested_object_id=original_id, + disabled_by=er.RegistryEntryDisabler.USER, + ) + + assert entity_registry.async_get_entity_id(domain, const.DOMAIN, original_id) + + # setup CH 0 and host entities/device + with patch("homeassistant.components.reolink.PLATFORMS", [domain]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + entity_registry.async_get_entity_id(domain, const.DOMAIN, original_id) is None + ) + + +async def test_hdr_switch_deprecated_repair_issue( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repairs issue is raised when hdr switch entity used.""" + original_id = f"{TEST_UID}_hdr" + domain = Platform.SWITCH + + reolink_connect.channels = [0] + reolink_connect.supported.return_value = True + + entity_registry.async_get_or_create( + domain=domain, + platform=const.DOMAIN, + unique_id=original_id, + config_entry=config_entry, + suggested_object_id=original_id, + disabled_by=None, + ) + + assert entity_registry.async_get_entity_id(domain, const.DOMAIN, original_id) + + # setup CH 0 and host entities/device + with patch("homeassistant.components.reolink.PLATFORMS", [domain]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert entity_registry.async_get_entity_id(domain, const.DOMAIN, original_id) + + assert (const.DOMAIN, "hdr_switch_deprecated") in issue_registry.issues From 73ea62edd4546287b6fa5cb973f07bbf51f50ce7 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 23 Jul 2024 13:43:12 +0200 Subject: [PATCH 1493/2411] Disable polling for Matter entities by default (#122452) Matter entities don't implement async_update, they get their update from the Matter subscriptions through the WebSocket from the Matter Server. This change disables polling for all Matter Entities by default. --- homeassistant/components/matter/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index aaaaf074ddd..61e29477585 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -44,6 +44,7 @@ class MatterEntity(Entity): """Entity class for Matter devices.""" _attr_has_entity_name = True + _attr_should_poll = False _name_postfix: str | None = None def __init__( From 92acfc1464629eb21b21ba865e103b9f28422eee Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 23 Jul 2024 14:13:08 +0200 Subject: [PATCH 1494/2411] Indicate database migration in /api/core/state response (#122445) * Indicate database migration in /api/core/state response * Change API response according to review comment * Adjust API response * Update test * Add test --- homeassistant/components/api/__init__.py | 9 ++++-- homeassistant/helpers/recorder.py | 12 +++++-- tests/components/api/test_init.py | 41 +++++++++++++++++++++++- tests/helpers/test_recorder.py | 27 ++++++++++------ 4 files changed, 73 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 73751daa6cb..9572ed3fbd1 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -45,7 +45,7 @@ from homeassistant.exceptions import ( TemplateError, Unauthorized, ) -from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers import config_validation as cv, recorder, template from homeassistant.helpers.json import json_dumps, json_fragment from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.typing import ConfigType @@ -118,8 +118,11 @@ class APICoreStateView(HomeAssistantView): Home Assistant core is running. Its primary use case is for supervisor to check if Home Assistant is running. """ - hass = request.app[KEY_HASS] - return self.json({"state": hass.state.value}) + hass: HomeAssistant = request.app[KEY_HASS] + migration = recorder.async_migration_in_progress(hass) + live = recorder.async_migration_is_live(hass) + recorder_state = {"migration_in_progress": migration, "migration_is_live": live} + return self.json({"state": hass.state.value, "recorder_state": recorder_state}) class APIEventStream(HomeAssistantView): diff --git a/homeassistant/helpers/recorder.py b/homeassistant/helpers/recorder.py index ac534a7230a..f6657efc6d7 100644 --- a/homeassistant/helpers/recorder.py +++ b/homeassistant/helpers/recorder.py @@ -20,16 +20,24 @@ class RecorderData: db_connected: asyncio.Future[bool] = field(default_factory=asyncio.Future) +@callback def async_migration_in_progress(hass: HomeAssistant) -> bool: """Check to see if a recorder migration is in progress.""" - if "recorder" not in hass.config.components: - return False # pylint: disable-next=import-outside-toplevel from homeassistant.components import recorder return recorder.util.async_migration_in_progress(hass) +@callback +def async_migration_is_live(hass: HomeAssistant) -> bool: + """Check to see if a recorder migration is live.""" + # pylint: disable-next=import-outside-toplevel + from homeassistant.components import recorder + + return recorder.util.async_migration_is_live(hass) + + @callback def async_initialize_recorder(hass: HomeAssistant) -> None: """Initialize recorder data.""" diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index a1453315dbf..c283aeb718e 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -770,4 +770,43 @@ async def test_api_core_state(hass: HomeAssistant, mock_api_client: TestClient) resp = await mock_api_client.get("/api/core/state") assert resp.status == HTTPStatus.OK json = await resp.json() - assert json["state"] == "RUNNING" + assert json == { + "state": "RUNNING", + "recorder_state": {"migration_in_progress": False, "migration_is_live": False}, + } + + +@pytest.mark.parametrize( + ("migration_in_progress", "migration_is_live"), + [ + (False, False), + (False, True), + (True, False), + (True, True), + ], +) +async def test_api_core_state_recorder_migrating( + hass: HomeAssistant, + mock_api_client: TestClient, + migration_in_progress: bool, + migration_is_live: bool, +) -> None: + """Test getting core status.""" + with ( + patch( + "homeassistant.helpers.recorder.async_migration_in_progress", + return_value=migration_in_progress, + ), + patch( + "homeassistant.helpers.recorder.async_migration_is_live", + return_value=migration_is_live, + ), + ): + resp = await mock_api_client.get("/api/core/state") + assert resp.status == HTTPStatus.OK + json = await resp.json() + expected_recorder_state = { + "migration_in_progress": migration_in_progress, + "migration_is_live": migration_is_live, + } + assert json == {"state": "RUNNING", "recorder_state": expected_recorder_state} diff --git a/tests/helpers/test_recorder.py b/tests/helpers/test_recorder.py index 94f30d812bc..8fb8450bcb8 100644 --- a/tests/helpers/test_recorder.py +++ b/tests/helpers/test_recorder.py @@ -18,18 +18,25 @@ async def test_async_migration_in_progress( ): assert recorder.async_migration_in_progress(hass) is False - # The recorder is not loaded - with patch( - "homeassistant.components.recorder.util.async_migration_in_progress", - return_value=True, - ): - assert recorder.async_migration_in_progress(hass) is False - - await async_setup_recorder_instance(hass) - - # The recorder is now loaded with patch( "homeassistant.components.recorder.util.async_migration_in_progress", return_value=True, ): assert recorder.async_migration_in_progress(hass) is True + + +async def test_async_migration_is_live( + async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant +) -> None: + """Test async_migration_in_progress wraps the recorder.""" + with patch( + "homeassistant.components.recorder.util.async_migration_is_live", + return_value=False, + ): + assert recorder.async_migration_is_live(hass) is False + + with patch( + "homeassistant.components.recorder.util.async_migration_is_live", + return_value=True, + ): + assert recorder.async_migration_is_live(hass) is True From 545514c5cd78d17361010184c8b6e5d6215b83bf Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 23 Jul 2024 14:39:38 +0200 Subject: [PATCH 1495/2411] Add created_at/modified_at to category registry (#122454) --- .../components/config/category_registry.py | 2 + homeassistant/helpers/category_registry.py | 46 +++++- .../config/test_category_registry.py | 39 +++++ tests/helpers/test_category_registry.py | 151 ++++++++++++++++-- 4 files changed, 218 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/config/category_registry.py b/homeassistant/components/config/category_registry.py index 5fc705a5844..ade35fddadc 100644 --- a/homeassistant/components/config/category_registry.py +++ b/homeassistant/components/config/category_registry.py @@ -130,6 +130,8 @@ def _entry_dict(entry: cr.CategoryEntry) -> dict[str, Any]: """Convert entry to API format.""" return { "category_id": entry.category_id, + "created_at": entry.created_at.timestamp(), "icon": entry.icon, + "modified_at": entry.modified_at.timestamp(), "name": entry.name, } diff --git a/homeassistant/helpers/category_registry.py b/homeassistant/helpers/category_registry.py index 6498859e2ab..41fa82084b3 100644 --- a/homeassistant/helpers/category_registry.py +++ b/homeassistant/helpers/category_registry.py @@ -5,9 +5,11 @@ from __future__ import annotations from collections.abc import Iterable import dataclasses from dataclasses import dataclass, field -from typing import Literal, TypedDict +from datetime import datetime +from typing import Any, Literal, TypedDict from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.util.dt import utc_from_timestamp, utcnow from homeassistant.util.event_type import EventType from homeassistant.util.hass_dict import HassKey from homeassistant.util.ulid import ulid_now @@ -23,13 +25,16 @@ EVENT_CATEGORY_REGISTRY_UPDATED: EventType[EventCategoryRegistryUpdatedData] = ( ) STORAGE_KEY = "core.category_registry" STORAGE_VERSION_MAJOR = 1 +STORAGE_VERSION_MINOR = 2 class _CategoryStoreData(TypedDict): """Data type for individual category. Used in CategoryRegistryStoreData.""" category_id: str + created_at: str icon: str | None + modified_at: str name: str @@ -55,10 +60,36 @@ class CategoryEntry: """Category registry entry.""" category_id: str = field(default_factory=ulid_now) + created_at: datetime = field(default_factory=utcnow) icon: str | None = None + modified_at: datetime = field(default_factory=utcnow) name: str +class CategoryRegistryStore(Store[CategoryRegistryStoreData]): + """Store category registry data.""" + + async def _async_migrate_func( + self, + old_major_version: int, + old_minor_version: int, + old_data: dict[str, dict[str, list[dict[str, Any]]]], + ) -> CategoryRegistryStoreData: + """Migrate to the new version.""" + if old_major_version > STORAGE_VERSION_MAJOR: + raise ValueError("Can't migrate to future version") + + if old_major_version == 1: + if old_minor_version < 2: + # Version 1.2 implements migration and adds created_at and modified_at + created_at = utc_from_timestamp(0).isoformat() + for categories in old_data["categories"].values(): + for category in categories: + category["created_at"] = category["modified_at"] = created_at + + return old_data # type: ignore[return-value] + + class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]): """Class to hold a registry of categories by scope.""" @@ -66,11 +97,12 @@ class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]): """Initialize the category registry.""" self.hass = hass self.categories: dict[str, dict[str, CategoryEntry]] = {} - self._store = Store( + self._store = CategoryRegistryStore( hass, STORAGE_VERSION_MAJOR, STORAGE_KEY, atomic_writes=True, + minor_version=STORAGE_VERSION_MINOR, ) @callback @@ -145,7 +177,7 @@ class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]): ) -> CategoryEntry: """Update name or icon of the category.""" old = self.categories[scope][category_id] - changes = {} + changes: dict[str, Any] = {} if icon is not UNDEFINED and icon != old.icon: changes["icon"] = icon @@ -157,8 +189,10 @@ class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]): if not changes: return old + changes["modified_at"] = utcnow() + self.hass.verify_event_loop_thread("category_registry.async_update") - new = self.categories[scope][category_id] = dataclasses.replace(old, **changes) # type: ignore[arg-type] + new = self.categories[scope][category_id] = dataclasses.replace(old, **changes) self.async_schedule_save() self.hass.bus.async_fire_internal( @@ -180,7 +214,9 @@ class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]): category_entries[scope] = { category["category_id"]: CategoryEntry( category_id=category["category_id"], + created_at=datetime.fromisoformat(category["created_at"]), icon=category["icon"], + modified_at=datetime.fromisoformat(category["modified_at"]), name=category["name"], ) for category in categories @@ -196,7 +232,9 @@ class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]): scope: [ { "category_id": entry.category_id, + "created_at": entry.created_at.isoformat(), "icon": entry.icon, + "modified_at": entry.modified_at.isoformat(), "name": entry.name, } for entry in entries.values() diff --git a/tests/components/config/test_category_registry.py b/tests/components/config/test_category_registry.py index b4d171535b6..d4fe6a0c9b9 100644 --- a/tests/components/config/test_category_registry.py +++ b/tests/components/config/test_category_registry.py @@ -1,10 +1,14 @@ """Test category registry API.""" +from datetime import datetime + +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.config import category_registry from homeassistant.core import HomeAssistant from homeassistant.helpers import category_registry as cr +from homeassistant.util.dt import utcnow from tests.common import ANY from tests.typing import MockHAClientWebSocket, WebSocketGenerator @@ -19,6 +23,7 @@ async def client_fixture( return await hass_ws_client(hass) +@pytest.mark.usefixtures("freezer") async def test_list_categories( client: MockHAClientWebSocket, category_registry: cr.CategoryRegistry, @@ -53,11 +58,15 @@ async def test_list_categories( assert len(msg["result"]) == 2 assert msg["result"][0] == { "category_id": category1.category_id, + "created_at": utcnow().timestamp(), + "modified_at": utcnow().timestamp(), "name": "Energy saving", "icon": "mdi:leaf", } assert msg["result"][1] == { "category_id": category2.category_id, + "created_at": utcnow().timestamp(), + "modified_at": utcnow().timestamp(), "name": "Something else", "icon": "mdi:home", } @@ -71,6 +80,8 @@ async def test_list_categories( assert len(msg["result"]) == 1 assert msg["result"][0] == { "category_id": category3.category_id, + "created_at": utcnow().timestamp(), + "modified_at": utcnow().timestamp(), "name": "Grocery stores", "icon": "mdi:store", } @@ -79,8 +90,11 @@ async def test_list_categories( async def test_create_category( client: MockHAClientWebSocket, category_registry: cr.CategoryRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test create entry.""" + created1 = datetime(2024, 2, 14, 12, 0, 0) + freezer.move_to(created1) await client.send_json_auto_id( { "type": "config/category_registry/create", @@ -98,9 +112,14 @@ async def test_create_category( assert msg["result"] == { "icon": "mdi:leaf", "category_id": ANY, + "created_at": created1.timestamp(), + "modified_at": created1.timestamp(), "name": "Energy saving", } + created2 = datetime(2024, 3, 14, 12, 0, 0) + freezer.move_to(created2) + await client.send_json_auto_id( { "scope": "automation", @@ -117,9 +136,14 @@ async def test_create_category( assert msg["result"] == { "icon": None, "category_id": ANY, + "created_at": created2.timestamp(), + "modified_at": created2.timestamp(), "name": "Something else", } + created3 = datetime(2024, 4, 14, 12, 0, 0) + freezer.move_to(created3) + # Test adding the same one again in a different scope await client.send_json_auto_id( { @@ -139,6 +163,8 @@ async def test_create_category( assert msg["result"] == { "icon": "mdi:leaf", "category_id": ANY, + "created_at": created3.timestamp(), + "modified_at": created3.timestamp(), "name": "Energy saving", } @@ -249,8 +275,11 @@ async def test_delete_non_existing_category( async def test_update_category( client: MockHAClientWebSocket, category_registry: cr.CategoryRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test update entry.""" + created = datetime(2024, 2, 14, 12, 0, 0) + freezer.move_to(created) category = category_registry.async_create( scope="automation", name="Energy saving", @@ -258,6 +287,9 @@ async def test_update_category( assert len(category_registry.categories) == 1 assert len(category_registry.categories["automation"]) == 1 + modified = datetime(2024, 3, 14, 12, 0, 0) + freezer.move_to(modified) + await client.send_json_auto_id( { "scope": "automation", @@ -275,9 +307,14 @@ async def test_update_category( assert msg["result"] == { "icon": "mdi:left", "category_id": category.category_id, + "created_at": created.timestamp(), + "modified_at": modified.timestamp(), "name": "ENERGY SAVING", } + modified = datetime(2024, 4, 14, 12, 0, 0) + freezer.move_to(modified) + await client.send_json_auto_id( { "scope": "automation", @@ -295,6 +332,8 @@ async def test_update_category( assert msg["result"] == { "icon": None, "category_id": category.category_id, + "created_at": created.timestamp(), + "modified_at": modified.timestamp(), "name": "Energy saving", } diff --git a/tests/helpers/test_category_registry.py b/tests/helpers/test_category_registry.py index 1317750ebec..cad997fd50f 100644 --- a/tests/helpers/test_category_registry.py +++ b/tests/helpers/test_category_registry.py @@ -1,13 +1,16 @@ """Tests for the category registry.""" +from datetime import datetime from functools import partial import re from typing import Any +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.core import HomeAssistant from homeassistant.helpers import category_registry as cr +from homeassistant.util.dt import UTC from tests.common import async_capture_events, flush_store @@ -152,9 +155,13 @@ async def test_delete_non_existing_category( async def test_update_category( - hass: HomeAssistant, category_registry: cr.CategoryRegistry + hass: HomeAssistant, + category_registry: cr.CategoryRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Make sure that we can update categories.""" + created = datetime(2024, 2, 14, 12, 0, 0, tzinfo=UTC) + freezer.move_to(created) update_events = async_capture_events(hass, cr.EVENT_CATEGORY_REGISTRY_UPDATED) category = category_registry.async_create( scope="automation", @@ -162,9 +169,16 @@ async def test_update_category( ) assert len(category_registry.categories["automation"]) == 1 - assert category.category_id - assert category.name == "Energy saving" - assert category.icon is None + assert category == cr.CategoryEntry( + category_id=category.category_id, + created_at=created, + modified_at=created, + name="Energy saving", + icon=None, + ) + + modified = datetime(2024, 3, 14, 12, 0, 0, tzinfo=UTC) + freezer.move_to(modified) updated_category = category_registry.async_update( scope="automation", @@ -174,9 +188,13 @@ async def test_update_category( ) assert updated_category != category - assert updated_category.category_id == category.category_id - assert updated_category.name == "ENERGY SAVING" - assert updated_category.icon == "mdi:leaf" + assert updated_category == cr.CategoryEntry( + category_id=category.category_id, + created_at=created, + modified_at=modified, + name="ENERGY SAVING", + icon="mdi:leaf", + ) assert len(category_registry.categories["automation"]) == 1 @@ -343,18 +361,25 @@ async def test_loading_categories_from_storage( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test loading stored categories on start.""" + date_1 = datetime(2024, 2, 14, 12, 0, 0) + date_2 = datetime(2024, 2, 14, 12, 0, 0) hass_storage[cr.STORAGE_KEY] = { "version": cr.STORAGE_VERSION_MAJOR, + "minor_version": cr.STORAGE_VERSION_MINOR, "data": { "categories": { "automation": [ { "category_id": "uuid1", + "created_at": date_1.isoformat(), + "modified_at": date_1.isoformat(), "name": "Energy saving", "icon": "mdi:leaf", }, { "category_id": "uuid2", + "created_at": date_1.isoformat(), + "modified_at": date_2.isoformat(), "name": "Something else", "icon": None, }, @@ -362,6 +387,8 @@ async def test_loading_categories_from_storage( "zone": [ { "category_id": "uuid3", + "created_at": date_2.isoformat(), + "modified_at": date_2.isoformat(), "name": "Grocery stores", "icon": "mdi:store", }, @@ -380,21 +407,33 @@ async def test_loading_categories_from_storage( category1 = category_registry.async_get_category( scope="automation", category_id="uuid1" ) - assert category1.category_id == "uuid1" - assert category1.name == "Energy saving" - assert category1.icon == "mdi:leaf" + assert category1 == cr.CategoryEntry( + category_id="uuid1", + created_at=date_1, + modified_at=date_1, + name="Energy saving", + icon="mdi:leaf", + ) category2 = category_registry.async_get_category( scope="automation", category_id="uuid2" ) - assert category2.category_id == "uuid2" - assert category2.name == "Something else" - assert category2.icon is None + assert category2 == cr.CategoryEntry( + category_id="uuid2", + created_at=date_1, + modified_at=date_2, + name="Something else", + icon=None, + ) category3 = category_registry.async_get_category(scope="zone", category_id="uuid3") - assert category3.category_id == "uuid3" - assert category3.name == "Grocery stores" - assert category3.icon == "mdi:store" + assert category3 == cr.CategoryEntry( + category_id="uuid3", + created_at=date_2, + modified_at=date_2, + name="Grocery stores", + icon="mdi:store", + ) async def test_async_create_thread_safety( @@ -447,3 +486,83 @@ async def test_async_update_thread_safety( name="new name", ) ) + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_migration_from_1_1( + hass: HomeAssistant, hass_storage: dict[str, Any] +) -> None: + """Test migration from version 1.1.""" + hass_storage[cr.STORAGE_KEY] = { + "version": 1, + "data": { + "categories": { + "automation": [ + { + "category_id": "uuid1", + "name": "Energy saving", + "icon": "mdi:leaf", + }, + { + "category_id": "uuid2", + "name": "Something else", + "icon": None, + }, + ], + "zone": [ + { + "category_id": "uuid3", + "name": "Grocery stores", + "icon": "mdi:store", + }, + ], + } + }, + } + + await cr.async_load(hass) + registry = cr.async_get(hass) + + # Test data was loaded + assert len(registry.categories) == 2 + assert len(registry.categories["automation"]) == 2 + assert len(registry.categories["zone"]) == 1 + + assert registry.async_get_category(scope="automation", category_id="uuid1") + + # Check we store migrated data + await flush_store(registry._store) + assert hass_storage[cr.STORAGE_KEY] == { + "version": cr.STORAGE_VERSION_MAJOR, + "minor_version": cr.STORAGE_VERSION_MINOR, + "key": cr.STORAGE_KEY, + "data": { + "categories": { + "automation": [ + { + "category_id": "uuid1", + "created_at": "1970-01-01T00:00:00+00:00", + "modified_at": "1970-01-01T00:00:00+00:00", + "name": "Energy saving", + "icon": "mdi:leaf", + }, + { + "category_id": "uuid2", + "created_at": "1970-01-01T00:00:00+00:00", + "modified_at": "1970-01-01T00:00:00+00:00", + "name": "Something else", + "icon": None, + }, + ], + "zone": [ + { + "category_id": "uuid3", + "created_at": "1970-01-01T00:00:00+00:00", + "modified_at": "1970-01-01T00:00:00+00:00", + "name": "Grocery stores", + "icon": "mdi:store", + }, + ], + } + }, + } From 156a2427ffa2bc9b0bc4ac136780822bf0d23e0c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:20:04 +0200 Subject: [PATCH 1496/2411] Use aiohttp.ClientTimeout for timeout (#122458) --- homeassistant/components/amcrest/camera.py | 5 ++++- homeassistant/components/auth/indieauth.py | 2 +- homeassistant/components/buienradar/camera.py | 4 +++- homeassistant/components/cast/helpers.py | 2 +- homeassistant/components/discord/notify.py | 3 ++- homeassistant/components/system_health/__init__.py | 2 +- homeassistant/components/trafikverket_camera/coordinator.py | 5 ++++- homeassistant/util/location.py | 3 ++- 8 files changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index a55f9c81e64..b9b2701eac6 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -8,6 +8,7 @@ from datetime import timedelta import logging from typing import TYPE_CHECKING, Any +import aiohttp from aiohttp import web from amcrest import AmcrestError from haffmpeg.camera import CameraMjpeg @@ -244,7 +245,9 @@ class AmcrestCam(Camera): websession = async_get_clientsession(self.hass) streaming_url = self._api.mjpeg_url(typeno=self._resolution) stream_coro = websession.get( - streaming_url, auth=self._token, timeout=CAMERA_WEB_SESSION_TIMEOUT + streaming_url, + auth=self._token, + timeout=aiohttp.ClientTimeout(total=CAMERA_WEB_SESSION_TIMEOUT), ) return await async_aiohttp_proxy_web(self.hass, request, stream_coro) diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index 45de94d5a70..dec6767d807 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -94,7 +94,7 @@ async def fetch_redirect_uris(hass: HomeAssistant, url: str) -> list[str]: try: async with ( aiohttp.ClientSession() as session, - session.get(url, timeout=5) as resp, + session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp, ): async for data in resp.content.iter_chunked(1024): parser.feed(data.decode()) diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 72bf6b7a3eb..e9a7d2517cb 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -115,7 +115,9 @@ class BuienradarCam(Camera): headers = {} try: - async with session.get(url, timeout=5, headers=headers) as res: + async with session.get( + url, timeout=aiohttp.ClientTimeout(total=5), headers=headers + ) as res: res.raise_for_status() if res.status == 304: diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 137bc7ec3c0..865ea1ac3f6 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -248,7 +248,7 @@ async def _fetch_playlist(hass, url, supported_content_types): """Fetch a playlist from the given url.""" try: session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False) - async with session.get(url, timeout=5) as resp: + async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp: charset = resp.charset or "utf-8" if resp.content_type in supported_content_types: raise PlaylistSupported diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index c574b458333..8a98d172913 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -7,6 +7,7 @@ import logging import os.path from typing import Any, cast +import aiohttp import nextcord from nextcord.abc import Messageable @@ -81,7 +82,7 @@ class DiscordNotificationService(BaseNotificationService): async with session.get( url, ssl=verify_ssl, - timeout=30, + timeout=aiohttp.ClientTimeout(total=30), raise_for_status=True, ) as resp: content_length = resp.headers.get("Content-Length") diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index ca1d4026ea9..ce80f6303d9 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -235,7 +235,7 @@ async def async_check_can_reach_url( session = aiohttp_client.async_get_clientsession(hass) try: - await session.get(url, timeout=5) + await session.get(url, timeout=aiohttp.ClientTimeout(total=5)) except aiohttp.ClientError: data = {"type": "failed", "error": "unreachable"} except TimeoutError: diff --git a/homeassistant/components/trafikverket_camera/coordinator.py b/homeassistant/components/trafikverket_camera/coordinator.py index 8ead479fd1c..7bc5c556c00 100644 --- a/homeassistant/components/trafikverket_camera/coordinator.py +++ b/homeassistant/components/trafikverket_camera/coordinator.py @@ -8,6 +8,7 @@ from io import BytesIO import logging from typing import TYPE_CHECKING +import aiohttp from pytrafikverket.exceptions import ( InvalidAuthentication, MultipleCamerasFound, @@ -77,7 +78,9 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[CameraData]): if camera_data.fullsizephoto: image_url = f"{camera_data.photourl}?type=fullsize" - async with self.session.get(image_url, timeout=10) as get_image: + async with self.session.get( + image_url, timeout=aiohttp.ClientTimeout(total=10) + ) as get_image: if get_image.status not in range(200, 299): raise UpdateFailed("Could not retrieve image") image = BytesIO(await get_image.read()).getvalue() diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 24c49c5427c..c00cf88699e 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -163,7 +163,8 @@ async def _get_whoami(session: aiohttp.ClientSession) -> dict[str, Any] | None: """Query whoami.home-assistant.io for location data.""" try: resp = await session.get( - WHOAMI_URL_DEV if HA_VERSION.endswith("0.dev0") else WHOAMI_URL, timeout=30 + WHOAMI_URL_DEV if HA_VERSION.endswith("0.dev0") else WHOAMI_URL, + timeout=aiohttp.ClientTimeout(total=30), ) except (aiohttp.ClientError, TimeoutError): return None From b46b74df908d26b35ff6998511280f228711604b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 23 Jul 2024 15:22:23 +0200 Subject: [PATCH 1497/2411] Check for incompatible special chars in Reolink password (#122461) --- homeassistant/components/reolink/__init__.py | 4 ++-- homeassistant/components/reolink/config_flow.py | 13 +++++++++++-- homeassistant/components/reolink/exceptions.py | 4 ++++ homeassistant/components/reolink/host.py | 16 ++++++++++++++-- homeassistant/components/reolink/strings.json | 1 + tests/components/reolink/test_config_flow.py | 17 ++++++++++++++++- 6 files changed, 48 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 1caf4e79cd5..2077b4a5e29 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN -from .exceptions import ReolinkException, UserNotAdmin +from .exceptions import PasswordIncompatible, ReolinkException, UserNotAdmin from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: await host.async_init() - except (UserNotAdmin, CredentialsInvalidError) as err: + except (UserNotAdmin, CredentialsInvalidError, PasswordIncompatible) as err: await host.stop() raise ConfigEntryAuthFailed(err) from err except ( diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index be897a69d7d..6d0381b025f 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -6,6 +6,7 @@ from collections.abc import Mapping import logging from typing import Any +from reolink_aio.api import ALLOWED_SPECIAL_CHARS from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError import voluptuous as vol @@ -29,7 +30,12 @@ from homeassistant.helpers import config_validation as cv, selector from homeassistant.helpers.device_registry import format_mac from .const import CONF_USE_HTTPS, DOMAIN -from .exceptions import ReolinkException, ReolinkWebhookException, UserNotAdmin +from .exceptions import ( + PasswordIncompatible, + ReolinkException, + ReolinkWebhookException, + UserNotAdmin, +) from .host import ReolinkHost from .util import is_connected @@ -206,8 +212,11 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN): errors[CONF_USERNAME] = "not_admin" placeholders["username"] = host.api.username placeholders["userlevel"] = host.api.user_level + except PasswordIncompatible: + errors[CONF_PASSWORD] = "password_incompatible" + placeholders["special_chars"] = ALLOWED_SPECIAL_CHARS except CredentialsInvalidError: - errors[CONF_HOST] = "invalid_auth" + errors[CONF_PASSWORD] = "invalid_auth" except ApiError as err: placeholders["error"] = str(err) errors[CONF_HOST] = "api_error" diff --git a/homeassistant/components/reolink/exceptions.py b/homeassistant/components/reolink/exceptions.py index d166b438f31..2a5a955b9e6 100644 --- a/homeassistant/components/reolink/exceptions.py +++ b/homeassistant/components/reolink/exceptions.py @@ -17,3 +17,7 @@ class ReolinkWebhookException(ReolinkException): class UserNotAdmin(ReolinkException): """Raised when user is not admin.""" + + +class PasswordIncompatible(ReolinkException): + """Raised when the password contains special chars that are incompatible.""" diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index c9989f2c02b..310188b720e 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -11,7 +11,7 @@ from typing import Any, Literal import aiohttp from aiohttp.web import Request -from reolink_aio.api import Host +from reolink_aio.api import ALLOWED_SPECIAL_CHARS, Host from reolink_aio.enums import SubType from reolink_aio.exceptions import NotSupportedError, ReolinkError, SubscriptionError @@ -31,7 +31,12 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.network import NoURLAvailableError, get_url from .const import CONF_USE_HTTPS, DOMAIN -from .exceptions import ReolinkSetupException, ReolinkWebhookException, UserNotAdmin +from .exceptions import ( + PasswordIncompatible, + ReolinkSetupException, + ReolinkWebhookException, + UserNotAdmin, +) DEFAULT_TIMEOUT = 30 FIRST_ONVIF_TIMEOUT = 10 @@ -123,6 +128,13 @@ class ReolinkHost: async def async_init(self) -> None: """Connect to Reolink host.""" + if not self._api.valid_password(): + raise PasswordIncompatible( + "Reolink password contains incompatible special character, " + "please change the password to only contain characters: " + f"a-z, A-Z, 0-9 or {ALLOWED_SPECIAL_CHARS}" + ) + await self._api.get_host_data() if self._api.mac_address is None: diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 3b9aba84634..bcf1c71934d 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -29,6 +29,7 @@ "cannot_connect": "Failed to connect, check the IP address of the camera", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "not_admin": "User needs to be admin, user \"{username}\" has authorisation level \"{userlevel}\"", + "password_incompatible": "Password contains incompatible special character, only these characters are allowed: a-z, A-Z, 0-9 or {special_chars}", "unknown": "[%key:common::config_flow::error::unknown%]", "webhook_exception": "Home Assistant URL is not available, go to Settings > System > Network > Home Assistant URL and correct the URLs, see {more_info}" }, diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index ba845dc1697..6e57a7924e7 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -166,8 +166,23 @@ async def test_config_flow_errors( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - assert result["errors"] == {CONF_HOST: "invalid_auth"} + assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} + reolink_connect.valid_password.return_value = False + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_HOST: TEST_HOST, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_PASSWORD: "password_incompatible"} + + reolink_connect.valid_password.return_value = True reolink_connect.get_host_data.side_effect = ApiError("Test error") result = await hass.config_entries.flow.async_configure( result["flow_id"], From 07b2a7537b471f2c06d4130953d5c4b0e3af74c7 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 23 Jul 2024 15:25:02 +0200 Subject: [PATCH 1498/2411] Add Matter update entities for devices with OTA requestor (#120304) * Add Matter update entities for devices with OTA requestor Matter devices which support the OTA requestor cluster can receive updates from a OTA provider. The Home Assistant Python Matter Server implements such an OTA provider now. Add update entities for devices which support the OTA requestor cluster and check for available updates. Allow the user to update the firmware. The update progress will be read directly from the devices' OTA requestor cluster. * Update homeassistant/components/matter/update.py Co-authored-by: TheJulianJES * Bump python-matter-server to 6.3.0 This includes models and commands required for device firmware updates. * Fix tests by including the new bluetooth_enabled field * Add update entity tests * Fix update entity test * Update entity picture docstring * Add note about reasons for progress state change update * Enable polling for update entities by default Matter entities don't enable polling any longer. Enable polling for update entities by default. * Add comment about why Update entities are polled --------- Co-authored-by: TheJulianJES --- homeassistant/components/matter/discovery.py | 4 +- homeassistant/components/matter/manifest.json | 2 +- homeassistant/components/matter/update.py | 232 ++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/matter/conftest.py | 1 + .../fixtures/config_entry_diagnostics.json | 3 +- .../config_entry_diagnostics_redacted.json | 3 +- .../matter/fixtures/nodes/dimmable-light.json | 2 +- tests/components/matter/test_update.py | 171 +++++++++++++ 10 files changed, 415 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/matter/update.py create mode 100644 tests/components/matter/test_update.py diff --git a/homeassistant/components/matter/discovery.py b/homeassistant/components/matter/discovery.py index 912bf7bd7c2..33c8bb47e6a 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -22,6 +22,7 @@ from .number import DISCOVERY_SCHEMAS as NUMBER_SCHEMAS from .select import DISCOVERY_SCHEMAS as SELECT_SCHEMAS from .sensor import DISCOVERY_SCHEMAS as SENSOR_SCHEMAS from .switch import DISCOVERY_SCHEMAS as SWITCH_SCHEMAS +from .update import DISCOVERY_SCHEMAS as UPDATE_SCHEMAS DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = { Platform.BINARY_SENSOR: BINARY_SENSOR_SCHEMAS, @@ -32,9 +33,10 @@ DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = { Platform.LIGHT: LIGHT_SCHEMAS, Platform.LOCK: LOCK_SCHEMAS, Platform.NUMBER: NUMBER_SCHEMAS, + Platform.SELECT: SELECT_SCHEMAS, Platform.SENSOR: SENSOR_SCHEMAS, Platform.SWITCH: SWITCH_SCHEMAS, - Platform.SELECT: SELECT_SCHEMAS, + Platform.UPDATE: UPDATE_SCHEMAS, } SUPPORTED_PLATFORMS = tuple(DISCOVERY_SCHEMAS) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 1dac5ef0cb2..5488df01e4e 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,6 +6,6 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==6.2.2"], + "requirements": ["python-matter-server==6.3.0"], "zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."] } diff --git a/homeassistant/components/matter/update.py b/homeassistant/components/matter/update.py new file mode 100644 index 00000000000..4e6733db045 --- /dev/null +++ b/homeassistant/components/matter/update.py @@ -0,0 +1,232 @@ +"""Matter update.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timedelta +from typing import Any + +from chip.clusters import Objects as clusters +from matter_server.common.errors import UpdateCheckError, UpdateError +from matter_server.common.models import MatterSoftwareVersion + +from homeassistant.components.update import ( + ATTR_LATEST_VERSION, + UpdateDeviceClass, + UpdateEntity, + UpdateEntityDescription, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.restore_state import ExtraStoredData + +from .entity import MatterEntity +from .helpers import get_matter +from .models import MatterDiscoverySchema + +SCAN_INTERVAL = timedelta(hours=12) +POLL_AFTER_INSTALL = 10 + +ATTR_SOFTWARE_UPDATE = "software_update" + + +@dataclass +class MatterUpdateExtraStoredData(ExtraStoredData): + """Extra stored data for Matter node firmware update entity.""" + + software_update: MatterSoftwareVersion | None = None + + def as_dict(self) -> dict[str, Any]: + """Return a dict representation of the extra data.""" + return { + ATTR_SOFTWARE_UPDATE: self.software_update.as_dict() + if self.software_update is not None + else None, + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> MatterUpdateExtraStoredData: + """Initialize the extra data from a dict.""" + if data[ATTR_SOFTWARE_UPDATE] is None: + return cls() + return cls(MatterSoftwareVersion.from_dict(data[ATTR_SOFTWARE_UPDATE])) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Matter lock from Config Entry.""" + matter = get_matter(hass) + matter.register_platform_handler(Platform.UPDATE, async_add_entities) + + +class MatterUpdate(MatterEntity, UpdateEntity): + """Representation of a Matter node capable of updating.""" + + # Matter attribute changes are generally not polled, but the update check + # itself is. The update check is not done by the device itself, but by the + # Matter server. + _attr_should_poll = True + _software_update: MatterSoftwareVersion | None = None + _cancel_update: CALLBACK_TYPE | None = None + + @callback + def _update_from_device(self) -> None: + """Update from device.""" + + self._attr_installed_version = self.get_matter_attribute_value( + clusters.BasicInformation.Attributes.SoftwareVersionString + ) + + if self.get_matter_attribute_value( + clusters.OtaSoftwareUpdateRequestor.Attributes.UpdatePossible + ): + self._attr_supported_features = ( + UpdateEntityFeature.INSTALL + | UpdateEntityFeature.PROGRESS + | UpdateEntityFeature.SPECIFIC_VERSION + ) + + update_state: clusters.OtaSoftwareUpdateRequestor.Enums.UpdateStateEnum = ( + self.get_matter_attribute_value( + clusters.OtaSoftwareUpdateRequestor.Attributes.UpdateState + ) + ) + if ( + update_state + == clusters.OtaSoftwareUpdateRequestor.Enums.UpdateStateEnum.kIdle + ): + self._attr_in_progress = False + return + + update_progress: int = self.get_matter_attribute_value( + clusters.OtaSoftwareUpdateRequestor.Attributes.UpdateStateProgress + ) + + if ( + update_state + == clusters.OtaSoftwareUpdateRequestor.Enums.UpdateStateEnum.kDownloading + and update_progress is not None + and update_progress > 0 + ): + self._attr_in_progress = update_progress + else: + self._attr_in_progress = True + + async def async_update(self) -> None: + """Call when the entity needs to be updated.""" + try: + update_information = await self.matter_client.check_node_update( + node_id=self._endpoint.node.node_id + ) + if not update_information: + self._attr_latest_version = self._attr_installed_version + return + + self._software_update = update_information + self._attr_latest_version = update_information.software_version_string + self._attr_release_url = update_information.release_notes_url + except UpdateCheckError as err: + raise HomeAssistantError(f"Error finding applicable update: {err}") from err + + async def async_added_to_hass(self) -> None: + """Call when the entity is added to hass.""" + await super().async_added_to_hass() + + if state := await self.async_get_last_state(): + self._attr_latest_version = state.attributes.get(ATTR_LATEST_VERSION) + + if (extra_data := await self.async_get_last_extra_data()) and ( + matter_extra_data := MatterUpdateExtraStoredData.from_dict( + extra_data.as_dict() + ) + ): + self._software_update = matter_extra_data.software_update + else: + # Check for updates when added the first time. + await self.async_update() + + @property + def extra_restore_state_data(self) -> MatterUpdateExtraStoredData: + """Return Matter specific state data to be restored.""" + return MatterUpdateExtraStoredData(self._software_update) + + @property + def entity_picture(self) -> str | None: + """Return the entity picture to use in the frontend. + + This overrides UpdateEntity.entity_picture because the Matter brand picture + is not appropriate for a matter device which has its own brand. + """ + return None + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install a new software version.""" + + software_version: str | int | None = version + if self._software_update is not None and ( + version is None or version == self._software_update.software_version_string + ): + # Update to the version previously fetched and shown. + # We can pass the integer version directly to speedup download. + software_version = self._software_update.software_version + + if software_version is None: + raise HomeAssistantError("No software version specified") + + self._attr_in_progress = True + # Immediately update the progress state change to make frontend feel responsive. + # Progress updates from the device usually take few seconds to come in. + self.async_write_ha_state() + try: + await self.matter_client.update_node( + node_id=self._endpoint.node.node_id, + software_version=software_version, + ) + except UpdateCheckError as err: + raise HomeAssistantError(f"Error finding applicable update: {err}") from err + except UpdateError as err: + raise HomeAssistantError(f"Error updating: {err}") from err + finally: + # Check for updates right after the update since Matter devices + # can have strict update paths (e.g. Eve) + self._cancel_update = async_call_later( + self.hass, POLL_AFTER_INSTALL, self._async_update_future + ) + + async def _async_update_future(self, now: datetime | None = None) -> None: + """Request update.""" + await self.async_update() + + async def async_will_remove_from_hass(self) -> None: + """Entity removed.""" + await super().async_will_remove_from_hass() + if self._cancel_update is not None: + self._cancel_update() + + +DISCOVERY_SCHEMAS = [ + MatterDiscoverySchema( + platform=Platform.UPDATE, + entity_description=UpdateEntityDescription( + key="MatterUpdate", device_class=UpdateDeviceClass.FIRMWARE, name=None + ), + entity_class=MatterUpdate, + required_attributes=( + clusters.BasicInformation.Attributes.SoftwareVersion, + clusters.BasicInformation.Attributes.SoftwareVersionString, + clusters.OtaSoftwareUpdateRequestor.Attributes.UpdatePossible, + clusters.OtaSoftwareUpdateRequestor.Attributes.UpdateState, + clusters.OtaSoftwareUpdateRequestor.Attributes.UpdateStateProgress, + ), + ), +] diff --git a/requirements_all.txt b/requirements_all.txt index 328e9bf840c..010f0ed32a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2289,7 +2289,7 @@ python-kasa[speedups]==0.7.0.5 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==6.2.2 +python-matter-server==6.3.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ff5e7e8c02..54079d42273 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1795,7 +1795,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.0.5 # homeassistant.components.matter -python-matter-server==6.2.2 +python-matter-server==6.3.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index d561f6db1f9..f3d8740a73b 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -51,6 +51,7 @@ async def matter_client_fixture() -> AsyncGenerator[MagicMock]: wifi_credentials_set=True, thread_credentials_set=True, min_supported_schema_version=SCHEMA_VERSION, + bluetooth_enabled=False, ) yield client diff --git a/tests/components/matter/fixtures/config_entry_diagnostics.json b/tests/components/matter/fixtures/config_entry_diagnostics.json index f591709fbda..000b0d4e2e6 100644 --- a/tests/components/matter/fixtures/config_entry_diagnostics.json +++ b/tests/components/matter/fixtures/config_entry_diagnostics.json @@ -6,7 +6,8 @@ "sdk_version": "2022.12.0", "wifi_credentials_set": true, "thread_credentials_set": false, - "min_supported_schema_version": 1 + "min_supported_schema_version": 1, + "bluetooth_enabled": false }, "nodes": [ { diff --git a/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json b/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json index 503fd3b9a7a..95447783bbc 100644 --- a/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json +++ b/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json @@ -7,7 +7,8 @@ "sdk_version": "2022.12.0", "wifi_credentials_set": true, "thread_credentials_set": false, - "min_supported_schema_version": 1 + "min_supported_schema_version": 1, + "bluetooth_enabled": false }, "nodes": [ { diff --git a/tests/components/matter/fixtures/nodes/dimmable-light.json b/tests/components/matter/fixtures/nodes/dimmable-light.json index aad0afdfdcd..58c22f1b807 100644 --- a/tests/components/matter/fixtures/nodes/dimmable-light.json +++ b/tests/components/matter/fixtures/nodes/dimmable-light.json @@ -78,7 +78,7 @@ ], "0/42/0": [], "0/42/1": true, - "0/42/2": 0, + "0/42/2": 1, "0/42/3": 0, "0/42/65532": 0, "0/42/65533": 1, diff --git a/tests/components/matter/test_update.py b/tests/components/matter/test_update.py new file mode 100644 index 00000000000..73c69407bbc --- /dev/null +++ b/tests/components/matter/test_update.py @@ -0,0 +1,171 @@ +"""Test Matter number entities.""" + +from typing import Any +from unittest.mock import AsyncMock, MagicMock + +from chip.clusters import Objects as clusters +from chip.clusters.ClusterObjects import ClusterAttributeDescriptor +from matter_server.client.models.node import MatterNode +from matter_server.common.models import MatterSoftwareVersion, UpdateSource +import pytest + +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .common import ( + set_node_attribute, + setup_integration_with_node_fixture, + trigger_subscription_callback, +) + + +def set_node_attribute_typed( + node: MatterNode, + endpoint: int, + attribute: ClusterAttributeDescriptor, + value: Any, +) -> None: + """Set a node attribute.""" + set_node_attribute( + node, endpoint, attribute.cluster_id, attribute.attribute_id, value + ) + + +@pytest.fixture(name="check_node_update") +async def check_node_update_fixture(matter_client: MagicMock) -> AsyncMock: + """Fixture for a flow sensor node.""" + matter_client.check_node_update = AsyncMock(return_value=None) + return matter_client.check_node_update + + +@pytest.fixture(name="updateable_node") +async def updateable_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a flow sensor node.""" + return await setup_integration_with_node_fixture( + hass, "dimmable-light", matter_client + ) + + +async def test_update_entity( + hass: HomeAssistant, + matter_client: MagicMock, + check_node_update: AsyncMock, + updateable_node: MatterNode, +) -> None: + """Test update entity exists and update check got made.""" + state = hass.states.get("update.mock_dimmable_light") + assert state + assert state.state == STATE_OFF + + assert matter_client.check_node_update.call_count == 1 + + +async def test_update_install( + hass: HomeAssistant, + matter_client: MagicMock, + check_node_update: AsyncMock, + updateable_node: MatterNode, +) -> None: + """Test update entity exists and update check got made.""" + state = hass.states.get("update.mock_dimmable_light") + assert state + assert state.state == STATE_OFF + assert state.attributes.get("installed_version") == "v1.0" + + await async_setup_component(hass, "homeassistant", {}) + + check_node_update.return_value = MatterSoftwareVersion( + vid=65521, + pid=32768, + software_version=2, + software_version_string="v2.0", + firmware_information="", + min_applicable_software_version=0, + max_applicable_software_version=1, + release_notes_url="http://home-assistant.io/non-existing-product", + update_source=UpdateSource.LOCAL, + ) + + await hass.services.async_call( + "homeassistant", + "update_entity", + { + ATTR_ENTITY_ID: "update.mock_dimmable_light", + }, + blocking=True, + ) + + assert matter_client.check_node_update.call_count == 2 + + state = hass.states.get("update.mock_dimmable_light") + assert state + assert state.state == STATE_ON + assert state.attributes.get("latest_version") == "v2.0" + assert ( + state.attributes.get("release_url") + == "http://home-assistant.io/non-existing-product" + ) + + await async_setup_component(hass, "update", {}) + + await hass.services.async_call( + "update", + "install", + { + ATTR_ENTITY_ID: "update.mock_dimmable_light", + }, + blocking=True, + ) + + set_node_attribute_typed( + updateable_node, + 0, + clusters.OtaSoftwareUpdateRequestor.Attributes.UpdateState, + clusters.OtaSoftwareUpdateRequestor.Enums.UpdateStateEnum.kDownloading, + ) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("update.mock_dimmable_light") + assert state + assert state.state == STATE_ON + assert state.attributes.get("in_progress") + + set_node_attribute_typed( + updateable_node, + 0, + clusters.OtaSoftwareUpdateRequestor.Attributes.UpdateStateProgress, + 50, + ) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("update.mock_dimmable_light") + assert state + assert state.state == STATE_ON + assert state.attributes.get("in_progress") == 50 + + set_node_attribute_typed( + updateable_node, + 0, + clusters.OtaSoftwareUpdateRequestor.Attributes.UpdateState, + clusters.OtaSoftwareUpdateRequestor.Enums.UpdateStateEnum.kIdle, + ) + set_node_attribute_typed( + updateable_node, + 0, + clusters.BasicInformation.Attributes.SoftwareVersion, + 2, + ) + set_node_attribute_typed( + updateable_node, + 0, + clusters.BasicInformation.Attributes.SoftwareVersionString, + "v2.0", + ) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("update.mock_dimmable_light") + assert state.state == STATE_OFF + assert state.attributes.get("installed_version") == "v2.0" From 51ef5cd3ba3735bfdb00ddb2a2911d4ada24fb35 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 23 Jul 2024 15:28:16 +0200 Subject: [PATCH 1499/2411] Add model_id to Ecovacs integration (#122457) --- homeassistant/components/ecovacs/entity.py | 1 + tests/components/ecovacs/snapshots/test_init.ambr | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecovacs/entity.py b/homeassistant/components/ecovacs/entity.py index c038c54497a..5b586eaf9ef 100644 --- a/homeassistant/components/ecovacs/entity.py +++ b/homeassistant/components/ecovacs/entity.py @@ -53,6 +53,7 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]): manufacturer="Ecovacs", sw_version=self._device.fw_version, serial_number=device_info["name"], + model_id=device_info["class"], ) if nick := device_info.get("nick"): diff --git a/tests/components/ecovacs/snapshots/test_init.ambr b/tests/components/ecovacs/snapshots/test_init.ambr index 047b61c8a92..9113445cc31 100644 --- a/tests/components/ecovacs/snapshots/test_init.ambr +++ b/tests/components/ecovacs/snapshots/test_init.ambr @@ -21,7 +21,7 @@ }), 'manufacturer': 'Ecovacs', 'model': 'DEEBOT OZMO 950 Series', - 'model_id': None, + 'model_id': 'yna5xi', 'name': 'Ozmo 950', 'name_by_user': None, 'primary_config_entry': , From 32cd54b1e367f8ce3fc720cffba5d02685bf0c89 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 23 Jul 2024 15:35:02 +0200 Subject: [PATCH 1500/2411] Fix flaky Reolink tests (#122451) --- tests/components/reolink/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index ac606017837..c74cac76192 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -35,6 +35,7 @@ TEST_NVR_NAME = "test_reolink_name" TEST_NVR_NAME2 = "test2_reolink_name" TEST_USE_HTTPS = True TEST_HOST_MODEL = "RLN8-410" +TEST_ITEM_NUMBER = "P000" TEST_CAM_MODEL = "RLC-123" @@ -83,6 +84,7 @@ def reolink_connect_class() -> Generator[MagicMock]: host_mock.sw_version = "v1.0.0.0.0.0000" host_mock.manufacturer = "Reolink" host_mock.model = TEST_HOST_MODEL + host_mock.item_number = TEST_ITEM_NUMBER host_mock.camera_model.return_value = TEST_CAM_MODEL host_mock.camera_name.return_value = TEST_NVR_NAME host_mock.camera_hardware_version.return_value = "IPC_00001" From 5727f30026f8e64c1e688346b72b2650cef73330 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:37:26 +0200 Subject: [PATCH 1501/2411] Changes for aiohttp 3.10.0 (#122463) --- homeassistant/components/lifx_cloud/scene.py | 2 +- homeassistant/components/no_ip/__init__.py | 2 +- homeassistant/helpers/aiohttp_client.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py index 8e7ab0c2d46..b40cb081ed7 100644 --- a/homeassistant/components/lifx_cloud/scene.py +++ b/homeassistant/components/lifx_cloud/scene.py @@ -42,7 +42,7 @@ async def async_setup_platform( token = config.get(CONF_TOKEN) timeout = config.get(CONF_TIMEOUT) - headers = {AUTHORIZATION: f"Bearer {token}"} + headers: dict[str, str] = {AUTHORIZATION: f"Bearer {token}"} url = "https://api.lifx.com/v1/scenes" diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index 9680464c9fa..cb02490ac08 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -94,7 +94,7 @@ async def _update_no_ip( params = {"hostname": domain} - headers = { + headers: dict[str, str] = { AUTHORIZATION: f"Basic {auth_str.decode('utf-8')}", USER_AGENT: HA_USER_AGENT, } diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 5c4ead4e611..245d07af5a0 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable from contextlib import suppress +import socket from ssl import SSLContext import sys from types import MappingProxyType @@ -300,7 +301,7 @@ def _async_get_connector( ssl_context = ssl_util.get_default_no_verify_context() connector = aiohttp.TCPConnector( - family=family, + family=socket.AddressFamily(family), enable_cleanup_closed=ENABLE_CLEANUP_CLOSED, ssl=ssl_context, limit=MAXIMUM_CONNECTIONS, From ff467463f8fc577c58b40d1a0dd8443418e41cf0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:50:14 +0200 Subject: [PATCH 1502/2411] Update pytest warnings filter (#122459) --- pyproject.toml | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 113fef6bfbc..5d788393bca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -458,16 +458,14 @@ filterwarnings = [ # Ignore custom pytest marks "ignore:Unknown pytest.mark.disable_autouse_fixture:pytest.PytestUnknownMarkWarning:tests.components.met", "ignore:Unknown pytest.mark.dataset:pytest.PytestUnknownMarkWarning:tests.components.screenlogic", - # https://github.com/rokam/sunweg/blob/3.0.1/sunweg/plant.py#L96 - v3.0.1 - 2024-05-29 + # https://github.com/rokam/sunweg/blob/3.0.2/sunweg/plant.py#L96 - v3.0.2 - 2024-07-10 "ignore:The '(kwh_per_kwp|performance_rate)' property is deprecated and will return 0:DeprecationWarning:tests.components.sunweg.test_init", # -- design choice 3rd party # https://github.com/gwww/elkm1/blob/2.2.7/elkm1_lib/util.py#L8-L19 "ignore:ssl.TLSVersion.TLSv1 is deprecated:DeprecationWarning:elkm1_lib.util", - # https://github.com/michaeldavie/env_canada/blob/v0.6.2/env_canada/ec_cache.py - "ignore:Inheritance class CacheClientSession from ClientSession is discouraged:DeprecationWarning:env_canada.ec_cache", # https://github.com/allenporter/ical/pull/215 - # https://github.com/allenporter/ical/blob/8.0.0/ical/util.py#L20-L22 + # https://github.com/allenporter/ical/blob/8.1.1/ical/util.py#L21-L23 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:ical.util", # https://github.com/bachya/regenmaschine/blob/2024.03.0/regenmaschine/client.py#L52 "ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client", @@ -479,11 +477,11 @@ filterwarnings = [ "ignore:Deprecated call to `pkg_resources.declare_namespace\\(('azure'|'google.*'|'pywinusb'|'repoze'|'xbox'|'zope')\\)`:DeprecationWarning:pkg_resources", # -- tracked upstream / open PRs + # https://github.com/ronf/asyncssh/issues/674 - v2.15.0 + "ignore:ARC4 has been moved to cryptography.hazmat.decrepit.ciphers.algorithms.ARC4 and will be removed from this module in 48.0.0:UserWarning:asyncssh.crypto.cipher", + "ignore:TripleDES has been moved to cryptography.hazmat.decrepit.ciphers.algorithms.TripleDES and will be removed from this module in 48.0.0:UserWarning:asyncssh.crypto.cipher", # https://github.com/certbot/certbot/issues/9828 - v2.10.0 "ignore:X509Extension support in pyOpenSSL is deprecated. You should use the APIs in cryptography:DeprecationWarning:acme.crypto_util", - # https://github.com/influxdata/influxdb-client-python/issues/603 - v1.42.0 - # https://github.com/influxdata/influxdb-client-python/pull/652 - "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:influxdb_client.client.write.point", # https://github.com/beetbox/mediafile/issues/67 - v0.12.0 "ignore:'imghdr' is deprecated and slated for removal in Python 3.13:DeprecationWarning:mediafile", # https://github.com/foxel/python_ndms2_client/issues/6 - v0.1.3 @@ -501,8 +499,9 @@ filterwarnings = [ "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:devialet.devialet_api", # https://github.com/httplib2/httplib2/pull/226 - >=0.21.0 "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:httplib2", - # https://github.com/jaraco/jaraco.abode/commit/9e3e789efc96cddcaa15f920686bbeb79a7469e0 - update jaraco.abode to >=5.1.0 - "ignore:`jaraco.functools.call_aside` is deprecated, use `jaraco.functools.invoke` instead:DeprecationWarning:jaraco.abode.helpers.timeline", + # https://github.com/influxdata/influxdb-client-python/issues/603 >1.45.0 + # https://github.com/influxdata/influxdb-client-python/pull/652 + "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:influxdb_client.client.write.point", # https://github.com/majuss/lupupy/pull/15 - >0.3.2 "ignore:\"is not\" with 'str' literal. Did you mean \"!=\"?:SyntaxWarning:.*lupupy.devices.alarm", # https://github.com/nextcord/nextcord/pull/1095 - >2.6.1 @@ -523,10 +522,6 @@ filterwarnings = [ "ignore:invalid escape sequence:SyntaxWarning:.*stringcase", # https://github.com/mvantellingen/python-zeep/pull/1364 - >4.2.1 "ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning:zeep.utils", - # https://github.com/timmo001/system-bridge-connector/pull/27 - >=4.1.0 - "ignore:pkg_resources is deprecated as an API:DeprecationWarning:systembridgeconnector.version", - # https://github.com/jschlyter/ttls/commit/d64f1251397b8238cf6a35bea64784de25e3386c - >=1.8.1 - "ignore:pkg_resources is deprecated as an API:DeprecationWarning:ttls", # -- fixed for Python 3.13 # https://github.com/rhasspy/wyoming/commit/e34af30d455b6f2bb9e5cfb25fad8d276914bc54 - >=1.4.2 @@ -560,6 +555,9 @@ filterwarnings = [ # https://pypi.org/project/aprslib/ - v0.7.2 - 2022-07-10 "ignore:invalid escape sequence:SyntaxWarning:.*aprslib.parsing.common", "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:aprslib.parsing.common", + # https://pypi.org/project/panasonic-viera/ - v0.4.2 - 2024-04-24 + # https://github.com/florianholzapfel/panasonic-viera/blob/0.4.2/panasonic_viera/__init__.py#L789 + "ignore:invalid escape sequence:SyntaxWarning:.*panasonic_viera", # https://pypi.org/project/pyblackbird/ - v0.6 - 2023-03-15 # https://github.com/koolsb/pyblackbird/pull/9 -> closed "ignore:invalid escape sequence:SyntaxWarning:.*pyblackbird", @@ -584,9 +582,14 @@ filterwarnings = [ "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pybotvac.version", # https://github.com/home-assistant-ecosystem/python-mystrom/blob/2.2.0/pymystrom/__init__.py#L10 - v2.2.0 - 2023-05-21 "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pymystrom", - # https://pypi.org/project/velbus-aio/ - v2024.4.1 - 2024-04-07 - # https://github.com/Cereal2nd/velbus-aio/blob/2024.4.1/velbusaio/handler.py#L12 + # https://pypi.org/project/velbus-aio/ - v2024.7.5 - 2024-07-05 + # https://github.com/Cereal2nd/velbus-aio/blob/2024.7.5/velbusaio/handler.py#L22 "ignore:pkg_resources is deprecated as an API:DeprecationWarning:velbusaio.handler", + # - pyOpenSSL v24.2.1 + # https://pypi.org/project/acme/ - v2.11.0 - 2024-06-06 + "ignore:CSR support in pyOpenSSL is deprecated. You should use the APIs in cryptography:DeprecationWarning:acme.crypto_util", + # https://pypi.org/project/josepy/ - v1.14.0 - 2023-11-01 + "ignore:CSR support in pyOpenSSL is deprecated. You should use the APIs in cryptography:DeprecationWarning:josepy.util", # -- Python 3.13 # HomeAssistant @@ -658,10 +661,6 @@ filterwarnings = [ "ignore:\"is\" with 'int' literal. Did you mean \"==\"?:SyntaxWarning:.*pyiss", # https://pypi.org/project/PyMetEireann/ - v2021.8.0 - 2021-08-16 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:meteireann", - # https://pypi.org/project/pyowm/ - v3.3.0 - 2022-02-14 - # https://github.com/csparpa/pyowm/issues/435 - # https://github.com/csparpa/pyowm/blob/3.3.0/pyowm/commons/cityidregistry.py#L7 - "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pyowm.commons.cityidregistry", # https://pypi.org/project/PyPasser/ - v0.0.5 - 2021-10-21 "ignore:invalid escape sequence:SyntaxWarning:.*pypasser.utils", # https://pypi.org/project/pyqwikswitch/ - v0.94 - 2019-08-19 @@ -671,8 +670,6 @@ filterwarnings = [ "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:rx.internal.constants", # https://pypi.org/project/rxv/ - v0.7.0 - 2021-10-10 "ignore:defusedxml.cElementTree is deprecated, import from defusedxml.ElementTree instead:DeprecationWarning:rxv.ssdp", - # https://pypi.org/project/webrtcvad/ - v2.0.10 - 2017-01-08 - "ignore:pkg_resources is deprecated as an API:DeprecationWarning:webrtcvad", ] [tool.coverage.run] From f260d63c588495e019cab6dba7cf88c2d38ece4d Mon Sep 17 00:00:00 2001 From: "Phill (pssc)" Date: Tue, 23 Jul 2024 14:53:58 +0100 Subject: [PATCH 1503/2411] Add squeezebox server device with common init (#122396) * squeezebox moves common elements into __init__ to allow for server sensors and device, improves player device * Update with feedback from PR * squeezebox Formating fixes, Logging Fixes, remove nasty stored callback * squeezebox Formating fixes, Logging Fixes, remove nasty stored callback * squeezebox refactor to use own ConfigEntry and Data * squeezebox remove own data class * Update homeassistant/components/squeezebox/__init__.py Correct typo Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/squeezebox/media_player.py Stronger typing on entry setup SqueezeboxConfigEntry Co-authored-by: Joost Lekkerkerker * squeezebox add SqueezeboxConfigEntry * squeezebox fix mypy type errors * squeezebox use right Callable --------- Co-authored-by: Joost Lekkerkerker --- .../components/squeezebox/__init__.py | 81 +++++++++++++++--- homeassistant/components/squeezebox/const.py | 16 ++-- .../components/squeezebox/media_player.py | 82 ++++++++----------- 3 files changed, 112 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index baaddbef0b6..b6c7f049311 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -1,20 +1,80 @@ """The Squeezebox integration.""" +from asyncio import timeout import logging -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from pysqueezebox import Server -from .const import DISCOVERY_TASK, DOMAIN, PLAYER_DISCOVERY_UNSUB +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( + CONF_HTTPS, + DISCOVERY_TASK, + DOMAIN, + STATUS_API_TIMEOUT, + STATUS_QUERY_LIBRARYNAME, + STATUS_QUERY_UUID, +) _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.MEDIA_PLAYER] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Squeezebox from a config entry.""" +type SqueezeboxConfigEntry = ConfigEntry[Server] + + +async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -> bool: + """Set up an LMS Server from a config entry.""" + config = entry.data + session = async_get_clientsession(hass) + _LOGGER.debug( + "Reached async_setup_entry for host=%s(%s)", config[CONF_HOST], entry.entry_id + ) + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + https = config.get(CONF_HTTPS, False) + host = config[CONF_HOST] + port = config[CONF_PORT] + + lms = Server(session, host, port, username, password, https=https) + _LOGGER.debug("LMS object for %s", lms) + + try: + async with timeout(STATUS_API_TIMEOUT): + status = await lms.async_query( + "serverstatus", "-", "-", "prefs:libraryname" + ) + except Exception as err: + raise ConfigEntryNotReady( + f"Error communicating config not read for {host}" + ) from err + + if not status: + raise ConfigEntryNotReady(f"Error Config Not read for {host}") + _LOGGER.debug("LMS Status for setup = %s", status) + + lms.uuid = status[STATUS_QUERY_UUID] + lms.name = ( + (STATUS_QUERY_LIBRARYNAME in status and status[STATUS_QUERY_LIBRARYNAME]) + and status[STATUS_QUERY_LIBRARYNAME] + or host + ) + _LOGGER.debug("LMS %s = '%s' with uuid = %s ", lms.name, host, lms.uuid) + + entry.runtime_data = lms + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -22,10 +82,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" # Stop player discovery task for this config entry. - hass.data[DOMAIN][entry.entry_id][PLAYER_DISCOVERY_UNSUB]() - - # Remove stored data for this config entry - hass.data[DOMAIN].pop(entry.entry_id) + _LOGGER.debug( + "Reached async_unload_entry for LMS=%s(%s)", + entry.runtime_data.name or "Unknown", + entry.entry_id, + ) # Stop server discovery task if this is the last config entry. current_entries = hass.config_entries.async_entries(DOMAIN) diff --git a/homeassistant/components/squeezebox/const.py b/homeassistant/components/squeezebox/const.py index 96a541a16ba..a814cf6ecc4 100644 --- a/homeassistant/components/squeezebox/const.py +++ b/homeassistant/components/squeezebox/const.py @@ -1,10 +1,12 @@ """Constants for the Squeezebox component.""" -DOMAIN = "squeezebox" -ENTRY_PLAYERS = "entry_players" -KNOWN_PLAYERS = "known_players" -PLAYER_DISCOVERY_UNSUB = "player_discovery_unsub" -DISCOVERY_TASK = "discovery_task" -DEFAULT_PORT = 9000 -SQUEEZEBOX_SOURCE_STRINGS = ("source:", "wavin:", "spotify:") CONF_HTTPS = "https" +DISCOVERY_TASK = "discovery_task" +DOMAIN = "squeezebox" +DEFAULT_PORT = 9000 +KNOWN_PLAYERS = "known_players" +SENSOR_UPDATE_INTERVAL = 60 +STATUS_API_TIMEOUT = 10 +STATUS_QUERY_LIBRARYNAME = "libraryname" +STATUS_QUERY_UUID = "uuid" +SQUEEZEBOX_SOURCE_STRINGS = ("source:", "wavin:", "spotify:") diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index bf1ad1d77c4..aaf64c34ddf 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -2,12 +2,13 @@ from __future__ import annotations +from collections.abc import Callable from datetime import datetime import json import logging from typing import Any -from pysqueezebox import Server, async_discover +from pysqueezebox import Player, async_discover import voluptuous as vol from homeassistant.components import media_source @@ -21,22 +22,19 @@ from homeassistant.components.media_player import ( RepeatMode, async_process_play_media_url, ) -from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry -from homeassistant.const import ( - ATTR_COMMAND, - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, -) +from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY +from homeassistant.const import ATTR_COMMAND, CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, discovery_flow, entity_platform, ) -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceInfo, format_mac +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + DeviceInfo, + format_mac, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -46,20 +44,14 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.start import async_at_start from homeassistant.util.dt import utcnow +from . import SqueezeboxConfigEntry from .browse_media import ( build_item_response, generate_playlist, library_payload, media_source_content_filter, ) -from .const import ( - CONF_HTTPS, - DISCOVERY_TASK, - DOMAIN, - KNOWN_PLAYERS, - PLAYER_DISCOVERY_UNSUB, - SQUEEZEBOX_SOURCE_STRINGS, -) +from .const import DISCOVERY_TASK, DOMAIN, KNOWN_PLAYERS, SQUEEZEBOX_SOURCE_STRINGS SERVICE_CALL_METHOD = "call_method" SERVICE_CALL_QUERY = "call_query" @@ -118,29 +110,15 @@ async def start_server_discovery(hass: HomeAssistant) -> None: async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: SqueezeboxConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up an LMS Server from a config entry.""" - config = config_entry.data - _LOGGER.debug("Reached async_setup_entry for host=%s", config[CONF_HOST]) - - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - host = config[CONF_HOST] - port = config[CONF_PORT] - https = config.get(CONF_HTTPS, False) - + """Set up an player discovery from a config entry.""" hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN].setdefault(config_entry.entry_id, {}) - known_players = hass.data[DOMAIN].setdefault(KNOWN_PLAYERS, []) + lms = entry.runtime_data - session = async_get_clientsession(hass) - _LOGGER.debug("Creating LMS object for %s", host) - lms = Server(session, host, port, username, password, https=https) - - async def _discovery(now=None): + async def _player_discovery(now=None): """Discover squeezebox players by polling server.""" async def _discovered_player(player): @@ -169,13 +147,15 @@ async def async_setup_entry( for player in players: hass.async_create_task(_discovered_player(player)) - hass.data[DOMAIN][config_entry.entry_id][PLAYER_DISCOVERY_UNSUB] = ( - async_call_later(hass, DISCOVERY_INTERVAL, _discovery) + entry.async_on_unload( + async_call_later(hass, DISCOVERY_INTERVAL, _player_discovery) ) - _LOGGER.debug("Adding player discovery job for LMS server: %s", host) - config_entry.async_create_background_task( - hass, _discovery(), "squeezebox.media_player.discovery" + _LOGGER.debug( + "Adding player discovery job for LMS server: %s", entry.data[CONF_HOST] + ) + entry.async_create_background_task( + hass, _player_discovery(), "squeezebox.media_player.player_discovery" ) # Register entity services @@ -208,7 +188,7 @@ async def async_setup_entry( platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync") # Start server discovery task if not already running - config_entry.async_on_unload(async_at_start(hass, start_server_discovery)) + entry.async_on_unload(async_at_start(hass, start_server_discovery)) class SqueezeBoxEntity(MediaPlayerEntity): @@ -241,14 +221,16 @@ class SqueezeBoxEntity(MediaPlayerEntity): _last_update: datetime | None = None _attr_available = True - def __init__(self, player): + def __init__(self, player: Player) -> None: """Initialize the SqueezeBox device.""" self._player = player - self._query_result = {} - self._remove_dispatcher = None + self._query_result: bool | dict = {} + self._remove_dispatcher: Callable | None = None self._attr_unique_id = format_mac(player.player_id) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._attr_unique_id)}, name=player.name + identifiers={(DOMAIN, self._attr_unique_id)}, + name=player.name, + connections={(CONNECTION_NETWORK_MAC, self._attr_unique_id)}, ) @property @@ -265,7 +247,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): """Make a player available again.""" if unique_id == self.unique_id and connected: self._attr_available = True - _LOGGER.info("Player %s is available again", self.name) + _LOGGER.debug("Player %s is available again", self.name) self._remove_dispatcher() @property @@ -286,7 +268,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): if self.media_position != last_media_position: self._last_update = utcnow() if self._player.connected is False: - _LOGGER.info("Player %s is not available", self.name) + _LOGGER.debug("Player %s is not available", self.name) self._attr_available = False # start listening for restored players @@ -573,7 +555,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): if other_player_id := player_ids.get(other_player): await self._player.async_sync(other_player_id) else: - _LOGGER.info( + _LOGGER.debug( "Could not find player_id for %s. Not syncing", other_player ) From da6a7ebd4215640b4ed19816a4826b512f4cf32d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 23 Jul 2024 16:04:09 +0200 Subject: [PATCH 1504/2411] Update clientsession socket family typing (#122464) --- homeassistant/helpers/aiohttp_client.py | 18 ++++++++++++------ tests/helpers/test_aiohttp_client.py | 10 +++++++++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 245d07af5a0..0291c6926a5 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -83,7 +83,9 @@ class HassClientResponse(aiohttp.ClientResponse): @callback @bind_hass def async_get_clientsession( - hass: HomeAssistant, verify_ssl: bool = True, family: int = 0 + hass: HomeAssistant, + verify_ssl: bool = True, + family: socket.AddressFamily = socket.AF_UNSPEC, ) -> aiohttp.ClientSession: """Return default aiohttp ClientSession. @@ -112,7 +114,7 @@ def async_create_clientsession( hass: HomeAssistant, verify_ssl: bool = True, auto_cleanup: bool = True, - family: int = 0, + family: socket.AddressFamily = socket.AF_UNSPEC, **kwargs: Any, ) -> aiohttp.ClientSession: """Create a new ClientSession with kwargs, i.e. for cookies. @@ -143,7 +145,7 @@ def _async_create_clientsession( verify_ssl: bool = True, auto_cleanup_method: Callable[[HomeAssistant, aiohttp.ClientSession], None] | None = None, - family: int = 0, + family: socket.AddressFamily = socket.AF_UNSPEC, **kwargs: Any, ) -> aiohttp.ClientSession: """Create a new ClientSession with kwargs, i.e. for cookies.""" @@ -276,14 +278,18 @@ def _async_register_default_clientsession_shutdown( @callback -def _make_key(verify_ssl: bool = True, family: int = 0) -> tuple[bool, int]: +def _make_key( + verify_ssl: bool = True, family: socket.AddressFamily = socket.AF_UNSPEC +) -> tuple[bool, socket.AddressFamily]: """Make a key for connector or session pool.""" return (verify_ssl, family) @callback def _async_get_connector( - hass: HomeAssistant, verify_ssl: bool = True, family: int = 0 + hass: HomeAssistant, + verify_ssl: bool = True, + family: socket.AddressFamily = socket.AF_UNSPEC, ) -> aiohttp.BaseConnector: """Return the connector pool for aiohttp. @@ -301,7 +307,7 @@ def _async_get_connector( ssl_context = ssl_util.get_default_no_verify_context() connector = aiohttp.TCPConnector( - family=socket.AddressFamily(family), + family=family, enable_cleanup_closed=ENABLE_CLEANUP_CLOSED, ssl=ssl_context, limit=MAXIMUM_CONNECTIONS, diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index c0f61238329..4feb03493e9 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -1,5 +1,6 @@ """Test the aiohttp client helper.""" +import socket from unittest.mock import Mock, patch import aiohttp @@ -83,7 +84,14 @@ async def test_get_clientsession_without_ssl(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("verify_ssl", "expected_family"), - [(True, 0), (False, 0), (True, 4), (False, 4), (True, 6), (False, 6)], + [ + (True, socket.AF_UNSPEC), + (False, socket.AF_UNSPEC), + (True, socket.AF_INET), + (False, socket.AF_INET), + (True, socket.AF_INET6), + (False, socket.AF_INET6), + ], ) async def test_get_clientsession( hass: HomeAssistant, verify_ssl: bool, expected_family: int From 42b9c0448c94a1b467099e81da987a3c2446be5e Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 23 Jul 2024 15:47:53 +0100 Subject: [PATCH 1505/2411] Add coordinator to evohome and prune async_update code (#119432) * functional programming tweak * doctweak * typing hint * rename symbol * Switch to DataUpdateCoordinator * move from async_setup to EvoBroker * tweaks - add v1 back in * tidy up * tidy up docstring * lint * remove redundant logging * rename symbol * split back to inject authenticator clas * rename symbols * rename symbol * Update homeassistant/components/evohome/__init__.py Co-authored-by: Joakim Plate * allow exception to pass through * allow re-authentication with diff credentials * lint * undo unrelated change * use async_refresh instead of async_config_entry_first_refresh * assign None instead of empty dict as Falsey value * use class attrs instead of type hints * speed up mypy hint * speed up mypy check * small tidy up * small tidy up --------- Co-authored-by: Joakim Plate Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/evohome/__init__.py | 210 +++++++++++------- homeassistant/components/evohome/climate.py | 8 +- .../components/evohome/coordinator.py | 140 ++++++------ 3 files changed, 209 insertions(+), 149 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 13673caebb3..4cf8561fc3b 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -14,20 +14,14 @@ import evohomeasync as ev1 from evohomeasync.schema import SZ_SESSION_ID import evohomeasync2 as evo from evohomeasync2.schema.const import ( - SZ_ALLOWED_SYSTEM_MODES, SZ_AUTO_WITH_RESET, SZ_CAN_BE_TEMPORARY, - SZ_GATEWAY_ID, - SZ_GATEWAY_INFO, SZ_HEAT_SETPOINT, - SZ_LOCATION_ID, - SZ_LOCATION_INFO, SZ_SETPOINT_STATUS, SZ_STATE_STATUS, SZ_SYSTEM_MODE, SZ_SYSTEM_MODE_STATUS, SZ_TIME_UNTIL, - SZ_TIME_ZONE, SZ_TIMING_MODE, SZ_UNTIL, ) @@ -50,13 +44,14 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator import homeassistant.util.dt as dt_util from .const import ( + ACCESS_TOKEN, ACCESS_TOKEN_EXPIRES, ATTR_DURATION_DAYS, ATTR_DURATION_HOURS, @@ -65,12 +60,11 @@ from .const import ( ATTR_ZONE_TEMP, CONF_LOCATION_IDX, DOMAIN, - GWS, + REFRESH_TOKEN, SCAN_INTERVAL_DEFAULT, SCAN_INTERVAL_MINIMUM, STORAGE_KEY, STORAGE_VER, - TCS, USER_DATA, EvoService, ) @@ -79,6 +73,7 @@ from .helpers import ( convert_dict, convert_until, dt_aware_to_naive, + dt_local_to_aware, handle_evo_exception, ) @@ -118,91 +113,158 @@ SET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Create a (EMEA/EU-based) Honeywell TCC system.""" +class EvoSession: + """Class for evohome client instantiation & authentication.""" - async def load_auth_tokens(store: Store) -> tuple[dict, dict | None]: - app_storage = await store.async_load() - tokens = dict(app_storage or {}) + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the evohome broker and its data structure.""" - if tokens.pop(CONF_USERNAME, None) != config[DOMAIN][CONF_USERNAME]: + self.hass = hass + + self._session = async_get_clientsession(hass) + self._store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY) + + # the main client, which uses the newer API + self.client_v2: evo.EvohomeClient | None = None + self._tokens: dict[str, Any] = {} + + # the older client can be used to obtain high-precision temps (only) + self.client_v1: ev1.EvohomeClient | None = None + self.session_id: str | None = None + + async def authenticate(self, username: str, password: str) -> None: + """Check the user credentials against the web API. + + Will raise evo.AuthenticationFailed if the credentials are invalid. + """ + + if ( + self.client_v2 is None + or username != self.client_v2.username + or password != self.client_v2.password + ): + await self._load_auth_tokens(username) + + client_v2 = evo.EvohomeClient( + username, + password, + **self._tokens, + session=self._session, + ) + + else: # force a re-authentication + client_v2 = self.client_v2 + client_v2._user_account = None # noqa: SLF001 + + await client_v2.login() + await self.save_auth_tokens() + + self.client_v2 = client_v2 + + self.client_v1 = ev1.EvohomeClient( + username, + password, + session_id=self.session_id, + session=self._session, + ) + + async def _load_auth_tokens(self, username: str) -> None: + """Load access tokens and session_id from the store and validate them. + + Sets self._tokens and self._session_id to the latest values. + """ + + app_storage: dict[str, Any] = dict(await self._store.async_load() or {}) + + if app_storage.pop(CONF_USERNAME, None) != username: # any tokens won't be valid, and store might be corrupt - await store.async_save({}) - return ({}, {}) + await self._store.async_save({}) + + self.session_id = None + self._tokens = {} + + return # evohomeasync2 requires naive/local datetimes as strings - if tokens.get(ACCESS_TOKEN_EXPIRES) is not None and ( - expires := dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES]) + if app_storage.get(ACCESS_TOKEN_EXPIRES) is not None and ( + expires := dt_util.parse_datetime(app_storage[ACCESS_TOKEN_EXPIRES]) ): - tokens[ACCESS_TOKEN_EXPIRES] = dt_aware_to_naive(expires) + app_storage[ACCESS_TOKEN_EXPIRES] = dt_aware_to_naive(expires) - user_data = tokens.pop(USER_DATA, {}) - return (tokens, user_data) + user_data: dict[str, str] = app_storage.pop(USER_DATA, {}) - store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY) - tokens, user_data = await load_auth_tokens(store) + self.session_id = user_data.get(SZ_SESSION_ID) + self._tokens = app_storage - client_v2 = evo.EvohomeClient( - config[DOMAIN][CONF_USERNAME], - config[DOMAIN][CONF_PASSWORD], - **tokens, - session=async_get_clientsession(hass), - ) + async def save_auth_tokens(self) -> None: + """Save access tokens and session_id to the store. + + Sets self._tokens and self._session_id to the latest values. + """ + + if self.client_v2 is None: + await self._store.async_save({}) + return + + # evohomeasync2 uses naive/local datetimes + access_token_expires = dt_local_to_aware( + self.client_v2.access_token_expires # type: ignore[arg-type] + ) + + self._tokens = { + CONF_USERNAME: self.client_v2.username, + REFRESH_TOKEN: self.client_v2.refresh_token, + ACCESS_TOKEN: self.client_v2.access_token, + ACCESS_TOKEN_EXPIRES: access_token_expires.isoformat(), + } + + self.session_id = self.client_v1.broker.session_id if self.client_v1 else None + + app_storage = self._tokens + if self.client_v1: + app_storage[USER_DATA] = {SZ_SESSION_ID: self.session_id} + + await self._store.async_save(app_storage) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Evohome integration.""" + + sess = EvoSession(hass) try: - await client_v2.login() + await sess.authenticate( + config[DOMAIN][CONF_USERNAME], + config[DOMAIN][CONF_PASSWORD], + ) + except evo.AuthenticationFailed as err: handle_evo_exception(err) return False + finally: config[DOMAIN][CONF_PASSWORD] = "REDACTED" - assert isinstance(client_v2.installation_info, list) # mypy + broker = EvoBroker(sess) - loc_idx = config[DOMAIN][CONF_LOCATION_IDX] - try: - loc_config = client_v2.installation_info[loc_idx] - except IndexError: - _LOGGER.error( - ( - "Config error: '%s' = %s, but the valid range is 0-%s. " - "Unable to continue. Fix any configuration errors and restart HA" - ), - CONF_LOCATION_IDX, - loc_idx, - len(client_v2.installation_info) - 1, - ) + if not broker.validate_location( + config[DOMAIN][CONF_LOCATION_IDX], + ): return False - if _LOGGER.isEnabledFor(logging.DEBUG): - loc_info = { - SZ_LOCATION_ID: loc_config[SZ_LOCATION_INFO][SZ_LOCATION_ID], - SZ_TIME_ZONE: loc_config[SZ_LOCATION_INFO][SZ_TIME_ZONE], - } - gwy_info = { - SZ_GATEWAY_ID: loc_config[GWS][0][SZ_GATEWAY_INFO][SZ_GATEWAY_ID], - TCS: loc_config[GWS][0][TCS], - } - _config = { - SZ_LOCATION_INFO: loc_info, - GWS: [{SZ_GATEWAY_INFO: gwy_info}], - } - _LOGGER.debug("Config = %s", _config) - - client_v1 = ev1.EvohomeClient( - client_v2.username, - client_v2.password, - session_id=user_data.get(SZ_SESSION_ID) if user_data else None, # STORAGE_VER 1 - session=async_get_clientsession(hass), + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{DOMAIN}_coordinator", + update_interval=config[DOMAIN][CONF_SCAN_INTERVAL], + update_method=broker.async_update, ) - hass.data[DOMAIN] = {} - hass.data[DOMAIN]["broker"] = broker = EvoBroker( - hass, client_v2, client_v1, store, config[DOMAIN] - ) + hass.data[DOMAIN] = {"broker": broker, "coordinator": coordinator} - await broker.save_auth_tokens() - await broker.async_update() # get initial state + # without a listener, _schedule_refresh() won't be invoked by _async_refresh() + coordinator.async_add_listener(lambda: None) + await coordinator.async_refresh() # get initial state hass.async_create_task( async_load_platform(hass, Platform.CLIMATE, DOMAIN, {}, config) @@ -212,10 +274,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_load_platform(hass, Platform.WATER_HEATER, DOMAIN, {}, config) ) - async_track_time_interval( - hass, broker.async_update, config[DOMAIN][CONF_SCAN_INTERVAL] - ) - setup_service_functions(hass, broker) return True @@ -272,7 +330,7 @@ def setup_service_functions(hass: HomeAssistant, broker: EvoBroker) -> None: hass.services.async_register(DOMAIN, EvoService.REFRESH_SYSTEM, force_refresh) # Enumerate which operating modes are supported by this system - modes = broker.config[SZ_ALLOWED_SYSTEM_MODES] + modes = broker.tcs.allowedSystemModes # Not all systems support "AutoWithReset": register this handler only if required if [m[SZ_SYSTEM_MODE] for m in modes if m[SZ_SYSTEM_MODE] == SZ_AUTO_WITH_RESET]: diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 8b3e8a46e2c..42ffe84121e 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, Any import evohomeasync2 as evo from evohomeasync2.schema.const import ( SZ_ACTIVE_FAULTS, - SZ_ALLOWED_SYSTEM_MODES, SZ_SETPOINT_STATUS, SZ_SYSTEM_ID, SZ_SYSTEM_MODE, @@ -44,7 +43,6 @@ from .const import ( ATTR_DURATION_UNTIL, ATTR_SYSTEM_MODE, ATTR_ZONE_TEMP, - CONF_LOCATION_IDX, DOMAIN, EVO_AUTO, EVO_AUTOECO, @@ -112,8 +110,8 @@ async def async_setup_platform( "Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)", broker.tcs.modelType, broker.tcs.systemId, - broker.tcs.location.name, - broker.params[CONF_LOCATION_IDX], + broker.loc.name, + broker.loc_idx, ) entities: list[EvoClimateEntity] = [EvoController(broker, broker.tcs)] @@ -367,7 +365,7 @@ class EvoController(EvoClimateEntity): self._attr_unique_id = evo_device.systemId self._attr_name = evo_device.location.name - modes = [m[SZ_SYSTEM_MODE] for m in evo_broker.config[SZ_ALLOWED_SYSTEM_MODES]] + modes = [m[SZ_SYSTEM_MODE] for m in evo_broker.tcs.allowedSystemModes] self._attr_preset_modes = [ TCS_PRESET_TO_HA[m] for m in modes if m in list(TCS_PRESET_TO_HA) ] diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index 6b54c5f4640..b83d2d20c6a 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -5,85 +5,93 @@ from __future__ import annotations from collections.abc import Awaitable from datetime import timedelta import logging -from typing import Any +from typing import TYPE_CHECKING, Any import evohomeasync as ev1 -from evohomeasync.schema import SZ_ID, SZ_SESSION_ID, SZ_TEMP +from evohomeasync.schema import SZ_ID, SZ_TEMP import evohomeasync2 as evo +from evohomeasync2.schema.const import ( + SZ_GATEWAY_ID, + SZ_GATEWAY_INFO, + SZ_LOCATION_ID, + SZ_LOCATION_INFO, + SZ_TIME_ZONE, +) -from homeassistant.const import CONF_USERNAME -from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType -from .const import ( - ACCESS_TOKEN, - ACCESS_TOKEN_EXPIRES, - CONF_LOCATION_IDX, - DOMAIN, - GWS, - REFRESH_TOKEN, - TCS, - USER_DATA, - UTC_OFFSET, -) -from .helpers import dt_local_to_aware, handle_evo_exception +from .const import CONF_LOCATION_IDX, DOMAIN, GWS, TCS, UTC_OFFSET +from .helpers import handle_evo_exception + +if TYPE_CHECKING: + from . import EvoSession _LOGGER = logging.getLogger(__name__.rpartition(".")[0]) class EvoBroker: - """Container for evohome client and data.""" + """Broker for evohome client broker.""" - def __init__( - self, - hass: HomeAssistant, - client: evo.EvohomeClient, - client_v1: ev1.EvohomeClient | None, - store: Store[dict[str, Any]], - params: ConfigType, - ) -> None: - """Initialize the evohome client and its data structure.""" - self.hass = hass - self.client = client - self.client_v1 = client_v1 - self._store = store - self.params = params + loc_idx: int + loc: evo.Location + loc_utc_offset: timedelta + tcs: evo.ControlSystem - loc_idx = params[CONF_LOCATION_IDX] - self._location: evo.Location = client.locations[loc_idx] + def __init__(self, sess: EvoSession) -> None: + """Initialize the evohome broker and its data structure.""" - assert isinstance(client.installation_info, list) # mypy + self._sess = sess + self.hass = sess.hass + + assert sess.client_v2 is not None # mypy + + self.client = sess.client_v2 + self.client_v1 = sess.client_v1 - self.config = client.installation_info[loc_idx][GWS][0][TCS][0] - self.tcs: evo.ControlSystem = self._location._gateways[0]._control_systems[0] # noqa: SLF001 - self.loc_utc_offset = timedelta(minutes=self._location.timeZone[UTC_OFFSET]) self.temps: dict[str, float | None] = {} - async def save_auth_tokens(self) -> None: - """Save access tokens and session IDs to the store for later use.""" - # evohomeasync2 uses naive/local datetimes - access_token_expires = dt_local_to_aware( - self.client.access_token_expires # type: ignore[arg-type] - ) + def validate_location(self, loc_idx: int) -> bool: + """Get the default TCS of the specified location.""" - app_storage: dict[str, Any] = { - CONF_USERNAME: self.client.username, - REFRESH_TOKEN: self.client.refresh_token, - ACCESS_TOKEN: self.client.access_token, - ACCESS_TOKEN_EXPIRES: access_token_expires.isoformat(), - } + self.loc_idx = loc_idx - if self.client_v1: - app_storage[USER_DATA] = { - SZ_SESSION_ID: self.client_v1.broker.session_id, - } # this is the schema for STORAGE_VER == 1 - else: - app_storage[USER_DATA] = {} + assert self.client.installation_info is not None # mypy - await self._store.async_save(app_storage) + try: + loc_config = self.client.installation_info[loc_idx] + except IndexError: + _LOGGER.error( + ( + "Config error: '%s' = %s, but the valid range is 0-%s. " + "Unable to continue. Fix any configuration errors and restart HA" + ), + CONF_LOCATION_IDX, + loc_idx, + len(self.client.installation_info) - 1, + ) + return False + + self.loc = self.client.locations[loc_idx] + self.loc_utc_offset = timedelta(minutes=self.loc.timeZone[UTC_OFFSET]) + self.tcs = self.loc._gateways[0]._control_systems[0] # noqa: SLF001 + + if _LOGGER.isEnabledFor(logging.DEBUG): + loc_info = { + SZ_LOCATION_ID: loc_config[SZ_LOCATION_INFO][SZ_LOCATION_ID], + SZ_TIME_ZONE: loc_config[SZ_LOCATION_INFO][SZ_TIME_ZONE], + } + gwy_info = { + SZ_GATEWAY_ID: loc_config[GWS][0][SZ_GATEWAY_INFO][SZ_GATEWAY_ID], + TCS: loc_config[GWS][0][TCS], + } + config = { + SZ_LOCATION_INFO: loc_info, + GWS: [{SZ_GATEWAY_INFO: gwy_info}], + } + _LOGGER.debug("Config = %s", config) + + return True async def call_client_api( self, @@ -108,11 +116,7 @@ class EvoBroker: assert self.client_v1 is not None # mypy check - def get_session_id(client_v1: ev1.EvohomeClient) -> str | None: - user_data = client_v1.user_data if client_v1 else None - return user_data.get(SZ_SESSION_ID) if user_data else None # type: ignore[return-value] - - session_id = get_session_id(self.client_v1) + old_session_id = self._sess.session_id try: temps = await self.client_v1.get_temperatures() @@ -146,7 +150,7 @@ class EvoBroker: raise else: - if str(self.client_v1.location_id) != self._location.locationId: + if str(self.client_v1.location_id) != self.loc.locationId: _LOGGER.warning( "The v2 API's configured location doesn't match " "the v1 API's default location (there is more than one location), " @@ -157,8 +161,8 @@ class EvoBroker: self.temps = {str(i[SZ_ID]): i[SZ_TEMP] for i in temps} finally: - if self.client_v1 and session_id != self.client_v1.broker.session_id: - await self.save_auth_tokens() + if self.client_v1 and self.client_v1.broker.session_id != old_session_id: + await self._sess.save_auth_tokens() _LOGGER.debug("Temperatures = %s", self.temps) @@ -168,7 +172,7 @@ class EvoBroker: access_token = self.client.access_token # maybe receive a new token? try: - status = await self._location.refresh_status() + status = await self.loc.refresh_status() except evo.RequestFailed as err: handle_evo_exception(err) else: @@ -176,7 +180,7 @@ class EvoBroker: _LOGGER.debug("Status = %s", status) finally: if access_token != self.client.access_token: - await self.save_auth_tokens() + await self._sess.save_auth_tokens() async def async_update(self, *args: Any) -> None: """Get the latest state data of an entire Honeywell TCC Location. From d7b0d1a50ece8deb0b7610e2b54dc4f027eff7ec Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 23 Jul 2024 17:47:45 +0200 Subject: [PATCH 1506/2411] Use dispatcher for KNX GroupMonitor instead of custom HassJob (#122384) --- homeassistant/components/knx/telegrams.py | 22 +--------------------- homeassistant/components/knx/websocket.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/knx/telegrams.py b/homeassistant/components/knx/telegrams.py index 6945bb50746..82df78e748e 100644 --- a/homeassistant/components/knx/telegrams.py +++ b/homeassistant/components/knx/telegrams.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections import deque -from collections.abc import Callable from typing import Final, TypedDict from xknx import XKNX @@ -12,7 +11,7 @@ from xknx.exceptions import XKNXException from xknx.telegram import Telegram from xknx.telegram.apci import GroupValueResponse, GroupValueWrite -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store import homeassistant.util.dt as dt_util @@ -68,7 +67,6 @@ class Telegrams: self._history_store = Store[list[TelegramDict]]( hass, STORAGE_VERSION, STORAGE_KEY ) - self._jobs: list[HassJob[[TelegramDict], None]] = [] self._xknx_telegram_cb_handle = ( xknx.telegram_queue.register_telegram_received_cb( telegram_received_cb=self._xknx_telegram_cb, @@ -100,24 +98,6 @@ class Telegrams: telegram_dict = self.telegram_to_dict(telegram) self.recent_telegrams.append(telegram_dict) async_dispatcher_send(self.hass, SIGNAL_KNX_TELEGRAM, telegram, telegram_dict) - for job in self._jobs: - self.hass.async_run_hass_job(job, telegram_dict) - - @callback - def async_listen_telegram( - self, - action: Callable[[TelegramDict], None], - name: str = "KNX telegram listener", - ) -> CALLBACK_TYPE: - """Register callback to listen for telegrams.""" - job = HassJob(action, name=name) - self._jobs.append(job) - - def remove_listener() -> None: - """Remove the listener.""" - self._jobs.remove(job) - - return remove_listener def telegram_to_dict(self, telegram: Telegram) -> TelegramDict: """Convert a Telegram to a dict.""" diff --git a/homeassistant/components/knx/websocket.py b/homeassistant/components/knx/websocket.py index bca1b119ef7..97758dc87c9 100644 --- a/homeassistant/components/knx/websocket.py +++ b/homeassistant/components/knx/websocket.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Final import knx_frontend as knx_panel import voluptuous as vol +from xknx.telegram import Telegram from xknxproject.exceptions import XknxProjectException from homeassistant.components import panel_custom, websocket_api @@ -13,6 +14,7 @@ from homeassistant.components.http import StaticPathConfig from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import UNDEFINED from homeassistant.util.ulid import ulid_now @@ -28,7 +30,7 @@ from .storage.entity_store_validation import ( EntityStoreValidationSuccess, validate_entity_data, ) -from .telegrams import TelegramDict +from .telegrams import SIGNAL_KNX_TELEGRAM, TelegramDict if TYPE_CHECKING: from . import KNXModule @@ -220,19 +222,19 @@ def ws_subscribe_telegram( msg: dict, ) -> None: """Subscribe to incoming and outgoing KNX telegrams.""" - knx: KNXModule = hass.data[DOMAIN] @callback - def forward_telegram(telegram: TelegramDict) -> None: + def forward_telegram(_telegram: Telegram, telegram_dict: TelegramDict) -> None: """Forward telegram to websocket subscription.""" connection.send_event( msg["id"], - telegram, + telegram_dict, ) - connection.subscriptions[msg["id"]] = knx.telegrams.async_listen_telegram( - action=forward_telegram, - name="KNX GroupMonitor subscription", + connection.subscriptions[msg["id"]] = async_dispatcher_connect( + hass, + signal=SIGNAL_KNX_TELEGRAM, + target=forward_telegram, ) connection.send_result(msg["id"]) From 3ba2a0518e3b01a50140689773b3695c78f0a7de Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 23 Jul 2024 10:57:54 -0500 Subject: [PATCH 1507/2411] Switch to official ollama library, update models (#122471) * Switch to mainstream ollama library, update models * Fix mypy error --- homeassistant/components/ollama/const.py | 41 ++++++++++++++----- homeassistant/components/ollama/manifest.json | 2 +- homeassistant/components/ollama/models.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/ollama/const.py b/homeassistant/components/ollama/const.py index e804ccedd85..b3bce3624c2 100644 --- a/homeassistant/components/ollama/const.py +++ b/homeassistant/components/ollama/const.py @@ -86,41 +86,60 @@ MAX_HISTORY_SECONDS = 60 * 60 # 1 hour MODEL_NAMES = [ # https://ollama.com/library "alfred", "all-minilm", + "aya", "bakllava", "codebooga", + "codegeex4", "codegemma", "codellama", "codeqwen", + "codestral", "codeup", "command-r", "command-r-plus", "dbrx", "deepseek-coder", + "deepseek-coder-v2", "deepseek-llm", + "deepseek-v2", + "dolphincoder", "dolphin-llama3", "dolphin-mistral", "dolphin-mixtral", "dolphin-phi", - "dolphincoder", "duckdb-nsql", "everythinglm", "falcon", + "falcon2", + "firefunction-v2", "gemma", + "gemma2", + "glm4", "goliath", - "llama-pro", + "granite-code", + "internlm2", "llama2", "llama2-chinese", "llama2-uncensored", "llama3", + "llama3-chatqa", + "llama3-gradient", + "llama3-groq-tool-use", + "llama-pro", "llava", + "llava-llama3", + "llava-phi3", "magicoder", + "mathstral", "meditron", "medllama2", "megadolphin", "mistral", - "mistral-openorca", "mistrallite", + "mistral-nemo", + "mistral-openorca", "mixtral", + "moondream", "mxbai-embed-large", "neural-chat", "nexusraven", @@ -130,36 +149,38 @@ MODEL_NAMES = [ # https://ollama.com/library "nous-hermes", "nous-hermes2", "nous-hermes2-mixtral", - "open-orca-platypus2", + "nuextract", "openchat", "openhermes", - "orca-mini", + "open-orca-platypus2", "orca2", + "orca-mini", "phi", "phi3", "phind-codellama", "qwen", + "qwen2", "samantha-mistral", "snowflake-arctic-embed", "solar", "sqlcoder", "stable-beluga", "stable-code", - "stablelm-zephyr", "stablelm2", + "stablelm-zephyr", "starcoder", "starcoder2", "starling-lm", "tinydolphin", "tinyllama", "vicuna", + "wizardcoder", + "wizardlm", + "wizardlm2", + "wizardlm-uncensored", "wizard-math", "wizard-vicuna", "wizard-vicuna-uncensored", - "wizardcoder", - "wizardlm", - "wizardlm-uncensored", - "wizardlm2", "xwinlm", "yarn-llama2", "yarn-mistral", diff --git a/homeassistant/components/ollama/manifest.json b/homeassistant/components/ollama/manifest.json index 7afaaa3dbd4..f7265d87aab 100644 --- a/homeassistant/components/ollama/manifest.json +++ b/homeassistant/components/ollama/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/ollama", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["ollama-hass==0.1.7"] + "requirements": ["ollama==0.3.0"] } diff --git a/homeassistant/components/ollama/models.py b/homeassistant/components/ollama/models.py index ce0f858bb8c..56cc552fad1 100644 --- a/homeassistant/components/ollama/models.py +++ b/homeassistant/components/ollama/models.py @@ -29,7 +29,7 @@ class MessageHistory: @property def num_user_messages(self) -> int: """Return a count of user messages.""" - return sum(m["role"] == MessageRole.USER for m in self.messages) + return sum(m["role"] == MessageRole.USER.value for m in self.messages) @dataclass(frozen=True) diff --git a/requirements_all.txt b/requirements_all.txt index 010f0ed32a0..09c147d4948 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1463,7 +1463,7 @@ odp-amsterdam==6.0.2 oemthermostat==1.1.1 # homeassistant.components.ollama -ollama-hass==0.1.7 +ollama==0.3.0 # homeassistant.components.omnilogic omnilogic==0.4.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54079d42273..c05868bbb7d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1199,7 +1199,7 @@ objgraph==3.5.0 odp-amsterdam==6.0.2 # homeassistant.components.ollama -ollama-hass==0.1.7 +ollama==0.3.0 # homeassistant.components.omnilogic omnilogic==0.4.5 From 0a62a4459f71ea5c3dd41e8037be8707e196f4cc Mon Sep 17 00:00:00 2001 From: ilan <31193909+iloveicedgreentea@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:06:26 -0400 Subject: [PATCH 1508/2411] Add 100% test coverage to Madvr (#122350) * feat: add 100% test coverage * fix: dont patch logger * fix: better names * fix: use consts * fix: use built in const --- homeassistant/components/madvr/sensor.py | 2 +- tests/components/madvr/const.py | 8 +++ tests/components/madvr/test_remote.py | 78 ++++++++++++++++++++++-- tests/components/madvr/test_sensors.py | 4 ++ 4 files changed, 87 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/madvr/sensor.py b/homeassistant/components/madvr/sensor.py index d0f5556cc5d..6f0933ac879 100644 --- a/homeassistant/components/madvr/sensor.py +++ b/homeassistant/components/madvr/sensor.py @@ -58,7 +58,7 @@ def get_temperature(coordinator: MadVRCoordinator, key: str) -> float | None: """Get temperature value if valid, otherwise return None.""" try: temp = float(coordinator.data.get(key, 0)) - except ValueError: + except (AttributeError, ValueError): return None else: return temp if is_valid_temperature(temp) else None diff --git a/tests/components/madvr/const.py b/tests/components/madvr/const.py index 6bfa3a77167..8c5e122377b 100644 --- a/tests/components/madvr/const.py +++ b/tests/components/madvr/const.py @@ -8,3 +8,11 @@ MOCK_CONFIG = { } MOCK_MAC = "00:11:22:33:44:55" + +TEST_CON_ERROR = ConnectionError("Connection failed") +TEST_IMP_ERROR = NotImplementedError("Not implemented") + +TEST_FAILED_ON = "Failed to turn on device" +TEST_FAILED_OFF = "Failed to turn off device" +TEST_FAILED_CMD = "Failed to send command" +TEST_COMMAND = "test" diff --git a/tests/components/madvr/test_remote.py b/tests/components/madvr/test_remote.py index fc6471bf664..6fc507534d6 100644 --- a/tests/components/madvr/test_remote.py +++ b/tests/components/madvr/test_remote.py @@ -4,10 +4,15 @@ from __future__ import annotations from unittest.mock import AsyncMock, patch +import pytest from syrupy import SnapshotAssertion -from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN +from homeassistant.components.remote import ( + DOMAIN as REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, +) from homeassistant.const import ( + ATTR_COMMAND, ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -18,6 +23,14 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er from . import setup_integration +from .const import ( + TEST_COMMAND, + TEST_CON_ERROR, + TEST_FAILED_CMD, + TEST_FAILED_OFF, + TEST_FAILED_ON, + TEST_IMP_ERROR, +) from tests.common import MockConfigEntry, snapshot_platform @@ -39,6 +52,7 @@ async def test_remote_power( hass: HomeAssistant, mock_madvr_client: AsyncMock, mock_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, ) -> None: """Test turning on the remote entity.""" @@ -61,11 +75,47 @@ async def test_remote_power( mock_madvr_client.power_on.assert_called_once() + # cover exception cases + caplog.clear() + mock_madvr_client.power_off.side_effect = TEST_CON_ERROR + await hass.services.async_call( + REMOTE_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert TEST_FAILED_OFF in caplog.text + + # Test turning off with NotImplementedError + caplog.clear() + mock_madvr_client.power_off.side_effect = TEST_IMP_ERROR + await hass.services.async_call( + REMOTE_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert TEST_FAILED_OFF in caplog.text + + # Reset side_effect for power_off + mock_madvr_client.power_off.side_effect = None + + # Test turning on with ConnectionError + caplog.clear() + mock_madvr_client.power_on.side_effect = TEST_CON_ERROR + await hass.services.async_call( + REMOTE_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert TEST_FAILED_ON in caplog.text + + # Test turning on with NotImplementedError + caplog.clear() + mock_madvr_client.power_on.side_effect = TEST_IMP_ERROR + await hass.services.async_call( + REMOTE_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert TEST_FAILED_ON in caplog.text + async def test_send_command( hass: HomeAssistant, mock_madvr_client: AsyncMock, mock_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, ) -> None: """Test sending command to the remote entity.""" @@ -77,9 +127,29 @@ async def test_send_command( await hass.services.async_call( REMOTE_DOMAIN, - "send_command", - {ATTR_ENTITY_ID: entity_id, "command": "test"}, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: TEST_COMMAND}, blocking=True, ) - mock_madvr_client.add_command_to_queue.assert_called_once_with(["test"]) + mock_madvr_client.add_command_to_queue.assert_called_once_with([TEST_COMMAND]) + # cover exceptions + # Test ConnectionError + mock_madvr_client.add_command_to_queue.side_effect = TEST_CON_ERROR + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: TEST_COMMAND}, + blocking=True, + ) + assert TEST_FAILED_CMD in caplog.text + + # Test NotImplementedError + mock_madvr_client.add_command_to_queue.side_effect = TEST_IMP_ERROR + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: TEST_COMMAND}, + blocking=True, + ) + assert TEST_FAILED_CMD in caplog.text diff --git a/tests/components/madvr/test_sensors.py b/tests/components/madvr/test_sensors.py index 5a918dcd433..25dcc1cdcca 100644 --- a/tests/components/madvr/test_sensors.py +++ b/tests/components/madvr/test_sensors.py @@ -7,6 +7,7 @@ from unittest.mock import AsyncMock, patch import pytest from syrupy import SnapshotAssertion +from homeassistant.components.madvr.sensor import get_temperature from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er @@ -89,3 +90,6 @@ async def test_sensor_setup_and_states( update_callback({"temp_gpu": 41.2}) await hass.async_block_till_done() assert hass.states.get("sensor.madvr_envy_gpu_temperature").state == "41.2" + + # test get_temperature ValueError + assert get_temperature(None, "temp_key") is None From d78327a72f495ea8d7af7f0cfd9d0d4bc8ea6616 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:37:24 -0400 Subject: [PATCH 1509/2411] Add dynamic media player features to Russound (#122475) Add dynamic media player features --- homeassistant/components/russound_rio/const.py | 7 +++++++ homeassistant/components/russound_rio/media_player.py | 8 +++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/russound_rio/const.py b/homeassistant/components/russound_rio/const.py index e5bf81e464a..d1f4e1c4c0e 100644 --- a/homeassistant/components/russound_rio/const.py +++ b/homeassistant/components/russound_rio/const.py @@ -3,6 +3,9 @@ import asyncio from aiorussound import CommandException +from aiorussound.const import FeatureFlag + +from homeassistant.components.media_player import MediaPlayerEntityFeature DOMAIN = "russound_rio" @@ -19,3 +22,7 @@ class NoPrimaryControllerException(Exception): CONNECT_TIMEOUT = 5 + +MP_FEATURES_BY_FLAG = { + FeatureFlag.COMMANDS_ZONE_MUTE_OFF_ON: MediaPlayerEntityFeature.VOLUME_MUTE +} diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index faec7ceff99..89ef68346c2 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -23,7 +23,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import RussoundConfigEntry -from .const import DOMAIN +from .const import DOMAIN, MP_FEATURES_BY_FLAG _LOGGER = logging.getLogger(__name__) @@ -115,8 +115,7 @@ class RussoundZoneDevice(MediaPlayerEntity): _attr_should_poll = False _attr_has_entity_name = True _attr_supported_features = ( - MediaPlayerEntityFeature.VOLUME_MUTE - | MediaPlayerEntityFeature.VOLUME_SET + MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.SELECT_SOURCE @@ -143,6 +142,9 @@ class RussoundZoneDevice(MediaPlayerEntity): DOMAIN, self._controller.parent_controller.mac_address, ) + for flag, feature in MP_FEATURES_BY_FLAG.items(): + if flag in zone.instance.supported_features: + self._attr_supported_features |= feature def _callback_handler(self, device_str, *args): if ( From 6bdc5be43302b730d0451d004d3c1427c744d142 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Jul 2024 12:10:22 -0500 Subject: [PATCH 1510/2411] Bump aiohttp to 3.10.0b1 (#122409) --- homeassistant/components/http/__init__.py | 5 - homeassistant/helpers/aiohttp_client.py | 2 +- homeassistant/helpers/backports/__init__.py | 1 - .../helpers/backports/aiohttp_resolver.py | 116 ------------------ homeassistant/package_constraints.txt | 3 +- pyproject.toml | 3 +- requirements.txt | 3 +- script/licenses.py | 1 - tests/components/websocket_api/test_http.py | 4 +- 9 files changed, 6 insertions(+), 132 deletions(-) delete mode 100644 homeassistant/helpers/backports/__init__.py delete mode 100644 homeassistant/helpers/backports/aiohttp_resolver.py diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 0d86ab57d3f..5b68f91e494 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -22,7 +22,6 @@ from aiohttp.streams import StreamReader from aiohttp.typedefs import JSONDecoder, StrOrURL from aiohttp.web_exceptions import HTTPMovedPermanently, HTTPRedirection from aiohttp.web_protocol import RequestHandler -from aiohttp_fast_url_dispatcher import FastUrlDispatcher, attach_fast_url_dispatcher from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa @@ -335,10 +334,6 @@ class HomeAssistantHTTP: "max_field_size": MAX_LINE_SIZE, }, ) - # By default aiohttp does a linear search for routing rules, - # we have a lot of routes, so use a dict lookup with a fallback - # to the linear search. - attach_fast_url_dispatcher(self.app, FastUrlDispatcher()) self.hass = hass self.ssl_certificate = ssl_certificate self.ssl_peer_certificate = ssl_peer_certificate diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 0291c6926a5..6f52569c38c 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -14,6 +14,7 @@ from typing import TYPE_CHECKING, Any import aiohttp from aiohttp import web from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT +from aiohttp.resolver import AsyncResolver from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout from homeassistant import config_entries @@ -24,7 +25,6 @@ from homeassistant.util import ssl as ssl_util from homeassistant.util.hass_dict import HassKey from homeassistant.util.json import json_loads -from .backports.aiohttp_resolver import AsyncResolver from .frame import warn_use from .json import json_dumps diff --git a/homeassistant/helpers/backports/__init__.py b/homeassistant/helpers/backports/__init__.py deleted file mode 100644 index e672fe1d3d2..00000000000 --- a/homeassistant/helpers/backports/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Backports for helpers.""" diff --git a/homeassistant/helpers/backports/aiohttp_resolver.py b/homeassistant/helpers/backports/aiohttp_resolver.py deleted file mode 100644 index efa4ba4bb85..00000000000 --- a/homeassistant/helpers/backports/aiohttp_resolver.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Backport of aiohttp's AsyncResolver for Home Assistant. - -This is a backport of the AsyncResolver class from aiohttp 3.10. - -Before aiohttp 3.10, on system with IPv6 support, AsyncResolver would not fallback -to providing A records when AAAA records were not available. - -Additionally, unlike the ThreadedResolver, AsyncResolver -did not handle link-local addresses correctly. -""" - -from __future__ import annotations - -import asyncio -import socket -import sys -from typing import Any, TypedDict - -import aiodns -from aiohttp.abc import AbstractResolver - -# This is a backport of https://github.com/aio-libs/aiohttp/pull/8270 -# This can be removed once aiohttp 3.10 is the minimum supported version. - -_NUMERIC_SOCKET_FLAGS = socket.AI_NUMERICHOST | socket.AI_NUMERICSERV -_SUPPORTS_SCOPE_ID = sys.version_info >= (3, 9, 0) - - -class ResolveResult(TypedDict): - """Resolve result. - - This is the result returned from an AbstractResolver's - resolve method. - - :param hostname: The hostname that was provided. - :param host: The IP address that was resolved. - :param port: The port that was resolved. - :param family: The address family that was resolved. - :param proto: The protocol that was resolved. - :param flags: The flags that were resolved. - """ - - hostname: str - host: str - port: int - family: int - proto: int - flags: int - - -class AsyncResolver(AbstractResolver): - """Use the `aiodns` package to make asynchronous DNS lookups.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize the resolver.""" - if aiodns is None: - raise RuntimeError("Resolver requires aiodns library") - - self._loop = asyncio.get_running_loop() - self._resolver = aiodns.DNSResolver(*args, loop=self._loop, **kwargs) # type: ignore[misc] - - async def resolve( # type: ignore[override] - self, host: str, port: int = 0, family: int = socket.AF_INET - ) -> list[ResolveResult]: - """Resolve a host name to an IP address.""" - try: - resp = await self._resolver.getaddrinfo( - host, - port=port, - type=socket.SOCK_STREAM, - family=family, # type: ignore[arg-type] - flags=socket.AI_ADDRCONFIG, - ) - except aiodns.error.DNSError as exc: - msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed" - raise OSError(msg) from exc - hosts: list[ResolveResult] = [] - for node in resp.nodes: - address: tuple[bytes, int] | tuple[bytes, int, int, int] = node.addr - family = node.family - if family == socket.AF_INET6: - if len(address) > 3 and address[3] and _SUPPORTS_SCOPE_ID: - # This is essential for link-local IPv6 addresses. - # LL IPv6 is a VERY rare case. Strictly speaking, we should use - # getnameinfo() unconditionally, but performance makes sense. - result = await self._resolver.getnameinfo( - (address[0].decode("ascii"), *address[1:]), - _NUMERIC_SOCKET_FLAGS, - ) - resolved_host = result.node - else: - resolved_host = address[0].decode("ascii") - port = address[1] - else: # IPv4 - assert family == socket.AF_INET - resolved_host = address[0].decode("ascii") - port = address[1] - hosts.append( - ResolveResult( - hostname=host, - host=resolved_host, - port=port, - family=family, - proto=0, - flags=_NUMERIC_SOCKET_FLAGS, - ) - ) - - if not hosts: - raise OSError("DNS lookup failed") - - return hosts - - async def close(self) -> None: - """Close the resolver.""" - self._resolver.cancel() diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c013415c794..80eaa3bc31d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -3,9 +3,8 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 -aiohttp-fast-url-dispatcher==0.3.0 aiohttp-fast-zlib==0.1.1 -aiohttp==3.9.5 +aiohttp==3.10.0b1 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index 5d788393bca..c3a9b864df9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,9 +24,8 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.9.5", + "aiohttp==3.10.0b1", "aiohttp_cors==0.7.0", - "aiohttp-fast-url-dispatcher==0.3.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", "astral==2.2", diff --git a/requirements.txt b/requirements.txt index a729f09472b..6f6a11b03a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +4,8 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.9.5 +aiohttp==3.10.0b1 aiohttp_cors==0.7.0 -aiohttp-fast-url-dispatcher==0.3.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 astral==2.2 diff --git a/script/licenses.py b/script/licenses.py index 52e8fd0f3e7..358e0e03791 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -125,7 +125,6 @@ EXCEPTIONS = { "aiocomelit", # https://github.com/chemelli74/aiocomelit/pull/138 "aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180 "aiohappyeyeballs", # PSF-2.0 license - "aiohttp-fast-url-dispatcher", # https://github.com/bdraco/aiohttp-fast-url-dispatcher/pull/10 "aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94 "aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8 "aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6 diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index 794dd410661..11665da11b4 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -5,7 +5,7 @@ from datetime import timedelta from typing import Any, cast from unittest.mock import patch -from aiohttp import ServerDisconnectedError, WSMsgType, web +from aiohttp import WSMsgType, WSServerHandshakeError, web import pytest from homeassistant.components.websocket_api import ( @@ -374,7 +374,7 @@ async def test_prepare_fail( "homeassistant.components.websocket_api.http.web.WebSocketResponse.prepare", side_effect=(TimeoutError, web.WebSocketResponse.prepare), ), - pytest.raises(ServerDisconnectedError), + pytest.raises(WSServerHandshakeError), ): await hass_ws_client(hass) From 1b7fb9ae12261a1b907cd7eb6f54789a6ee78156 Mon Sep 17 00:00:00 2001 From: ribbal <30695106+ribbal@users.noreply.github.com> Date: Tue, 23 Jul 2024 18:21:58 +0100 Subject: [PATCH 1511/2411] Create additional sensors in Hive integration (#122453) * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors --- homeassistant/components/hive/icons.json | 9 +++- homeassistant/components/hive/sensor.py | 53 +++++++++++++++++++--- homeassistant/components/hive/strings.json | 18 ++++++++ 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hive/icons.json b/homeassistant/components/hive/icons.json index 186724cd563..2704317779c 100644 --- a/homeassistant/components/hive/icons.json +++ b/homeassistant/components/hive/icons.json @@ -6,9 +6,14 @@ }, "hot_water": { "default": "mdi:hand-water" + } + }, + "sensor": { + "heating": { + "default": "mdi:radiator" }, - "temperature": { - "default": "mdi:thermometer" + "hot_water": { + "default": "mdi:hand-water" } } }, diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index 4e81b1a5d85..3e594c19058 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -1,5 +1,7 @@ """Support for the Hive sensors.""" +from collections.abc import Callable +from dataclasses import dataclass from datetime import timedelta from typing import Any @@ -20,6 +22,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from . import HiveEntity from .const import DOMAIN @@ -27,27 +30,61 @@ from .const import DOMAIN PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( + +@dataclass(frozen=True) +class HiveSensorEntityDescription(SensorEntityDescription): + """Describes Hive sensor entity.""" + + fn: Callable[[StateType], StateType] = lambda x: x + + +SENSOR_TYPES: tuple[HiveSensorEntityDescription, ...] = ( + HiveSensorEntityDescription( key="Battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + HiveSensorEntityDescription( key="Power", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + HiveSensorEntityDescription( key="Current_Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, ), + HiveSensorEntityDescription( + key="Heating_Current_Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + HiveSensorEntityDescription( + key="Heating_Target_Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + HiveSensorEntityDescription( + key="Heating_Mode", + device_class=SensorDeviceClass.ENUM, + options=["schedule", "on", "off"], + translation_key="heating", + fn=lambda x: x.lower() if isinstance(x, str) else None, + ), + HiveSensorEntityDescription( + key="Hotwater_Mode", + device_class=SensorDeviceClass.ENUM, + options=["schedule", "on", "off"], + translation_key="hot_water", + fn=lambda x: x.lower() if isinstance(x, str) else None, + ), ) @@ -73,11 +110,13 @@ async def async_setup_entry( class HiveSensorEntity(HiveEntity, SensorEntity): """Hive Sensor Entity.""" + entity_description: HiveSensorEntityDescription + def __init__( self, hive: Hive, hive_device: dict[str, Any], - entity_description: SensorEntityDescription, + entity_description: HiveSensorEntityDescription, ) -> None: """Initialise hive sensor.""" super().__init__(hive, hive_device) @@ -87,4 +126,6 @@ class HiveSensorEntity(HiveEntity, SensorEntity): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.sensor.getSensor(self.device) - self._attr_native_value = self.device["status"]["state"] + self._attr_native_value = self.entity_description.fn( + self.device["status"]["state"] + ) diff --git a/homeassistant/components/hive/strings.json b/homeassistant/components/hive/strings.json index 277a1aac754..c3252238131 100644 --- a/homeassistant/components/hive/strings.json +++ b/homeassistant/components/hive/strings.json @@ -100,5 +100,23 @@ } } } + }, + "entity": { + "sensor": { + "heating": { + "state": { + "on": "[%key:common::state::on%]", + "off": "[%key:common::state::off%]", + "schedule": "Schedule" + } + }, + "hot_water": { + "state": { + "on": "[%key:common::state::on%]", + "off": "[%key:common::state::off%]", + "schedule": "[%key:component::hive::entity::sensor::heating::state::schedule%]" + } + } + } } } From b53800a69dc78a0db1b6f676d3efd3e3a5397b27 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 23 Jul 2024 19:27:38 +0200 Subject: [PATCH 1512/2411] Tweak axis test fixtures (#122469) * Don't automatically add config entry to hass * Improve RTSP fixture typing * Improve typing of config entry factory and remove unnecessary use of it * Remove redundant fixture in config flow tests * Parametrize config flow error test --- tests/components/axis/conftest.py | 64 ++++++---- tests/components/axis/test_binary_sensor.py | 6 +- tests/components/axis/test_camera.py | 11 +- tests/components/axis/test_config_flow.py | 124 ++++++++------------ tests/components/axis/test_hub.py | 8 +- tests/components/axis/test_init.py | 3 + tests/components/axis/test_light.py | 9 +- tests/components/axis/test_switch.py | 6 +- 8 files changed, 108 insertions(+), 123 deletions(-) diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index da58e9576a8..0cbfdc007c0 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Callable, Generator +from collections.abc import Callable, Coroutine, Generator from copy import deepcopy from types import MappingProxyType -from typing import Any +from typing import Any, Protocol from unittest.mock import AsyncMock, patch from axis.rtsp import Signal, State @@ -13,7 +13,6 @@ import pytest import respx from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_MODEL, @@ -47,6 +46,30 @@ from .const import ( from tests.common import MockConfigEntry +type ConfigEntryFactoryType = Callable[[], Coroutine[Any, Any, MockConfigEntry]] +type RtspStateType = Callable[[bool], None] + + +class RtspEventMock(Protocol): + """Fixture to allow mocking received RTSP events.""" + + def __call__( + self, + topic: str, + data_type: str, + data_value: str, + operation: str = "Initialized", + source_name: str = "", + source_idx: str = "", + ) -> None: + """Send RTSP event.""" + + +class _RtspClientMock(Protocol): + async def __call__( + self, data: dict[str, Any] | None = None, state: str = "" + ) -> None: ... + @pytest.fixture(name="mock_setup_entry") def fixture_setup_entry() -> Generator[AsyncMock]: @@ -66,9 +89,9 @@ def fixture_config_entry( config_entry_data: MappingProxyType[str, Any], config_entry_options: MappingProxyType[str, Any], config_entry_version: int, -) -> ConfigEntry: +) -> MockConfigEntry: """Define a config entry fixture.""" - config_entry = MockConfigEntry( + return MockConfigEntry( domain=AXIS_DOMAIN, entry_id="676abe5b73621446e6550a2e86ffe3dd", unique_id=FORMATTED_MAC, @@ -76,8 +99,6 @@ def fixture_config_entry( options=config_entry_options, version=config_entry_version, ) - config_entry.add_to_hass(hass) - return config_entry @pytest.fixture(name="config_entry_version") @@ -255,12 +276,13 @@ def fixture_default_requests(mock_requests: Callable[[str], None]) -> None: @pytest.fixture(name="config_entry_factory") async def fixture_config_entry_factory( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MockConfigEntry, mock_requests: Callable[[str], None], -) -> Callable[[], ConfigEntry]: +) -> ConfigEntryFactoryType: """Fixture factory to set up Axis network device.""" - async def __mock_setup_config_entry() -> ConfigEntry: + async def __mock_setup_config_entry() -> MockConfigEntry: + config_entry.add_to_hass(hass) mock_requests(config_entry.data[CONF_HOST]) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -271,8 +293,8 @@ async def fixture_config_entry_factory( @pytest.fixture(name="config_entry_setup") async def fixture_config_entry_setup( - hass: HomeAssistant, config_entry_factory: Callable[[], ConfigEntry] -) -> ConfigEntry: + config_entry_factory: ConfigEntryFactoryType, +) -> MockConfigEntry: """Define a fixture to set up Axis network device.""" return await config_entry_factory() @@ -280,8 +302,8 @@ async def fixture_config_entry_setup( # RTSP fixtures -@pytest.fixture(autouse=True, name="mock_axis_rtspclient") -def fixture_axis_rtspclient() -> Generator[Callable[[dict | None, str], None]]: +@pytest.fixture(autouse=True, name="_mock_rtsp_client") +def fixture_axis_rtsp_client() -> Generator[_RtspClientMock]: """No real RTSP communication allowed.""" with patch("axis.stream_manager.RTSPClient") as rtsp_client_mock: rtsp_client_mock.return_value.session.state = State.STOPPED @@ -298,7 +320,7 @@ def fixture_axis_rtspclient() -> Generator[Callable[[dict | None, str], None]]: rtsp_client_mock.return_value.stop = stop_stream - def make_rtsp_call(data: dict | None = None, state: str = "") -> None: + def make_rtsp_call(data: dict[str, Any] | None = None, state: str = "") -> None: """Generate a RTSP call.""" axis_streammanager_session_callback = rtsp_client_mock.call_args[0][4] @@ -314,9 +336,7 @@ def fixture_axis_rtspclient() -> Generator[Callable[[dict | None, str], None]]: @pytest.fixture(autouse=True, name="mock_rtsp_event") -def fixture_rtsp_event( - mock_axis_rtspclient: Callable[[dict | None, str], None], -) -> Callable[[str, str, str, str, str, str], None]: +def fixture_rtsp_event(_mock_rtsp_client: _RtspClientMock) -> RtspEventMock: """Fixture to allow mocking received RTSP events.""" def send_event( @@ -361,20 +381,18 @@ def fixture_rtsp_event( """ - mock_axis_rtspclient(data=event.encode("utf-8")) + _mock_rtsp_client(data=event.encode("utf-8")) return send_event @pytest.fixture(autouse=True, name="mock_rtsp_signal_state") -def fixture_rtsp_signal_state( - mock_axis_rtspclient: Callable[[dict | None, str], None], -) -> Callable[[bool], None]: +def fixture_rtsp_signal_state(_mock_rtsp_client: _RtspClientMock) -> RtspStateType: """Fixture to allow mocking RTSP state signalling.""" def send_signal(connected: bool) -> None: """Signal state change of RTSP connection.""" signal = Signal.PLAYING if connected else Signal.FAILED - mock_axis_rtspclient(state=signal) + _mock_rtsp_client(state=signal) return send_signal diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 4fc10bcbb38..1ba1520aa6a 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -1,6 +1,5 @@ """Axis binary sensor platform tests.""" -from collections.abc import Callable from typing import Any import pytest @@ -12,6 +11,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from .conftest import RtspEventMock from .const import NAME @@ -176,7 +176,7 @@ from .const import NAME @pytest.mark.usefixtures("config_entry_setup") async def test_binary_sensors( hass: HomeAssistant, - mock_rtsp_event: Callable[[str, str, str, str, str, str], None], + mock_rtsp_event: RtspEventMock, event: dict[str, str], entity: dict[str, Any], ) -> None: @@ -228,7 +228,7 @@ async def test_binary_sensors( @pytest.mark.usefixtures("config_entry_setup") async def test_unsupported_events( hass: HomeAssistant, - mock_rtsp_event: Callable[[str, str, str, str, str, str], None], + mock_rtsp_event: RtspEventMock, event: dict[str, str], ) -> None: """Validate nothing breaks with unsupported events.""" diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index 55692b2dca3..c1590717983 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -1,7 +1,5 @@ """Axis camera platform tests.""" -from collections.abc import Callable - import pytest from homeassistant.components import camera @@ -10,7 +8,6 @@ from homeassistant.components.axis.const import ( DOMAIN as AXIS_DOMAIN, ) from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN -from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -82,13 +79,11 @@ root.Properties.Firmware.BuildDate=Feb 15 2019 09:42 root.Properties.Firmware.BuildNumber=26 root.Properties.Firmware.Version=9.10.1 root.Properties.System.SerialNumber={MAC} -""" +""" # No image format data to signal camera support @pytest.mark.parametrize("param_properties_payload", [PROPERTY_DATA]) -async def test_camera_disabled( - hass: HomeAssistant, config_entry_factory: Callable[[], ConfigEntry] -) -> None: +@pytest.mark.usefixtures("config_entry_setup") +async def test_camera_disabled(hass: HomeAssistant) -> None: """Test that Axis camera platform is loaded properly but does not create camera entity.""" - await config_entry_factory() assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 0 diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 8ba17ced01b..5ceb6588fbd 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -2,7 +2,7 @@ from collections.abc import Callable from ipaddress import ip_address -from unittest.mock import AsyncMock, patch +from unittest.mock import patch import pytest @@ -17,13 +17,11 @@ from homeassistant.components.axis.const import ( ) from homeassistant.config_entries import ( SOURCE_DHCP, - SOURCE_IGNORE, SOURCE_REAUTH, SOURCE_RECONFIGURE, SOURCE_SSDP, SOURCE_USER, SOURCE_ZEROCONF, - ConfigEntry, ) from homeassistant.const import ( CONF_HOST, @@ -45,21 +43,9 @@ from tests.common import MockConfigEntry DHCP_FORMATTED_MAC = dr.format_mac(MAC).replace(":", "") -@pytest.fixture(name="mock_config_entry") -async def mock_config_entry_fixture( - hass: HomeAssistant, config_entry: MockConfigEntry, mock_setup_entry: AsyncMock -) -> MockConfigEntry: - """Mock config entry and setup entry.""" - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - return config_entry - - -@pytest.mark.usefixtures("mock_default_requests", "mock_setup_entry") +@pytest.mark.usefixtures("mock_default_requests") async def test_flow_manual_configuration(hass: HomeAssistant) -> None: """Test that config flow works.""" - MockConfigEntry(domain=AXIS_DOMAIN, source=SOURCE_IGNORE).add_to_hass(hass) - result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, context={"source": SOURCE_USER} ) @@ -93,11 +79,11 @@ async def test_flow_manual_configuration(hass: HomeAssistant) -> None: async def test_manual_configuration_update_configuration( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, + config_entry_setup: MockConfigEntry, mock_requests: Callable[[str], None], ) -> None: """Test that config flow fails on already configured device.""" - assert mock_config_entry.data[CONF_HOST] == "1.2.3.4" + assert config_entry_setup.data[CONF_HOST] == "1.2.3.4" result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, context={"source": SOURCE_USER} @@ -121,10 +107,19 @@ async def test_manual_configuration_update_configuration( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert mock_config_entry.data[CONF_HOST] == "2.3.4.5" + assert config_entry_setup.data[CONF_HOST] == "2.3.4.5" -async def test_flow_fails_faulty_credentials(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("exc", "error"), + [ + (config_flow.AuthenticationRequired, "invalid_auth"), + (config_flow.CannotConnect, "cannot_connect"), + ], +) +async def test_flow_fails_on_api( + hass: HomeAssistant, exc: Exception, error: str +) -> None: """Test that config flow fails on faulty credentials.""" result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, context={"source": SOURCE_USER} @@ -135,7 +130,7 @@ async def test_flow_fails_faulty_credentials(hass: HomeAssistant) -> None: with patch( "homeassistant.components.axis.config_flow.get_axis_api", - side_effect=config_flow.AuthenticationRequired, + side_effect=exc, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -148,37 +143,10 @@ async def test_flow_fails_faulty_credentials(hass: HomeAssistant) -> None: }, ) - assert result["errors"] == {"base": "invalid_auth"} + assert result["errors"] == {"base": error} -async def test_flow_fails_cannot_connect(hass: HomeAssistant) -> None: - """Test that config flow fails on cannot connect.""" - result = await hass.config_entries.flow.async_init( - AXIS_DOMAIN, context={"source": SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - - with patch( - "homeassistant.components.axis.config_flow.get_axis_api", - side_effect=config_flow.CannotConnect, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_PROTOCOL: "http", - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - CONF_PORT: 80, - }, - ) - - assert result["errors"] == {"base": "cannot_connect"} - - -@pytest.mark.usefixtures("mock_default_requests", "mock_setup_entry") +@pytest.mark.usefixtures("mock_default_requests") async def test_flow_create_entry_multiple_existing_entries_of_same_model( hass: HomeAssistant, ) -> None: @@ -229,18 +197,18 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model( async def test_reauth_flow_update_configuration( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, + config_entry_setup: MockConfigEntry, mock_requests: Callable[[str], None], ) -> None: """Test that config flow fails on already configured device.""" - assert mock_config_entry.data[CONF_HOST] == "1.2.3.4" - assert mock_config_entry.data[CONF_USERNAME] == "root" - assert mock_config_entry.data[CONF_PASSWORD] == "pass" + assert config_entry_setup.data[CONF_HOST] == "1.2.3.4" + assert config_entry_setup.data[CONF_USERNAME] == "root" + assert config_entry_setup.data[CONF_PASSWORD] == "pass" result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, context={"source": SOURCE_REAUTH}, - data=mock_config_entry.data, + data=config_entry_setup.data, ) assert result["type"] is FlowResultType.FORM @@ -261,28 +229,28 @@ async def test_reauth_flow_update_configuration( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert mock_config_entry.data[CONF_PROTOCOL] == "https" - assert mock_config_entry.data[CONF_HOST] == "2.3.4.5" - assert mock_config_entry.data[CONF_PORT] == 443 - assert mock_config_entry.data[CONF_USERNAME] == "user2" - assert mock_config_entry.data[CONF_PASSWORD] == "pass2" + assert config_entry_setup.data[CONF_PROTOCOL] == "https" + assert config_entry_setup.data[CONF_HOST] == "2.3.4.5" + assert config_entry_setup.data[CONF_PORT] == 443 + assert config_entry_setup.data[CONF_USERNAME] == "user2" + assert config_entry_setup.data[CONF_PASSWORD] == "pass2" async def test_reconfiguration_flow_update_configuration( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, + config_entry_setup: MockConfigEntry, mock_requests: Callable[[str], None], ) -> None: """Test that config flow reconfiguration updates configured device.""" - assert mock_config_entry.data[CONF_HOST] == "1.2.3.4" - assert mock_config_entry.data[CONF_USERNAME] == "root" - assert mock_config_entry.data[CONF_PASSWORD] == "pass" + assert config_entry_setup.data[CONF_HOST] == "1.2.3.4" + assert config_entry_setup.data[CONF_USERNAME] == "root" + assert config_entry_setup.data[CONF_PASSWORD] == "pass" result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, context={ "source": SOURCE_RECONFIGURE, - "entry_id": mock_config_entry.entry_id, + "entry_id": config_entry_setup.entry_id, }, ) @@ -301,11 +269,11 @@ async def test_reconfiguration_flow_update_configuration( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert mock_config_entry.data[CONF_PROTOCOL] == "http" - assert mock_config_entry.data[CONF_HOST] == "2.3.4.5" - assert mock_config_entry.data[CONF_PORT] == 80 - assert mock_config_entry.data[CONF_USERNAME] == "user" - assert mock_config_entry.data[CONF_PASSWORD] == "pass" + assert config_entry_setup.data[CONF_PROTOCOL] == "http" + assert config_entry_setup.data[CONF_HOST] == "2.3.4.5" + assert config_entry_setup.data[CONF_PORT] == 80 + assert config_entry_setup.data[CONF_USERNAME] == "user" + assert config_entry_setup.data[CONF_PASSWORD] == "pass" @pytest.mark.parametrize( @@ -372,7 +340,7 @@ async def test_reconfiguration_flow_update_configuration( ), ], ) -@pytest.mark.usefixtures("mock_default_requests", "mock_setup_entry") +@pytest.mark.usefixtures("mock_default_requests") async def test_discovery_flow( hass: HomeAssistant, source: str, @@ -455,12 +423,12 @@ async def test_discovery_flow( ) async def test_discovered_device_already_configured( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, + config_entry_setup: MockConfigEntry, source: str, discovery_info: BaseServiceInfo, ) -> None: """Test that discovery doesn't setup already configured devices.""" - assert mock_config_entry.data[CONF_HOST] == DEFAULT_HOST + assert config_entry_setup.data[CONF_HOST] == DEFAULT_HOST result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, data=discovery_info, context={"source": source} @@ -468,7 +436,7 @@ async def test_discovered_device_already_configured( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert mock_config_entry.data[CONF_HOST] == DEFAULT_HOST + assert config_entry_setup.data[CONF_HOST] == DEFAULT_HOST @pytest.mark.parametrize( @@ -513,14 +481,14 @@ async def test_discovered_device_already_configured( ) async def test_discovery_flow_updated_configuration( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, + config_entry_setup: MockConfigEntry, mock_requests: Callable[[str], None], source: str, discovery_info: BaseServiceInfo, expected_port: int, ) -> None: """Test that discovery flow update configuration with new parameters.""" - assert mock_config_entry.data == { + assert config_entry_setup.data == { CONF_HOST: DEFAULT_HOST, CONF_PORT: 80, CONF_USERNAME: "root", @@ -537,7 +505,7 @@ async def test_discovery_flow_updated_configuration( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert mock_config_entry.data == { + assert config_entry_setup.data == { CONF_HOST: "2.3.4.5", CONF_PORT: expected_port, CONF_USERNAME: "root", @@ -646,7 +614,7 @@ async def test_discovery_flow_ignore_link_local_address( async def test_option_flow( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Test config flow options.""" assert CONF_STREAM_PROFILE not in config_entry_setup.options diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index ff7e6b30cd6..012cef3bc5b 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -25,6 +25,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from .conftest import RtspEventMock, RtspStateType from .const import ( API_DISCOVERY_BASIC_DEVICE_INFO, API_DISCOVERY_MQTT, @@ -152,8 +153,8 @@ async def test_update_address( @pytest.mark.usefixtures("config_entry_setup") async def test_device_unavailable( hass: HomeAssistant, - mock_rtsp_event: Callable[[str, str, str, str, str, str], None], - mock_rtsp_signal_state: Callable[[bool], None], + mock_rtsp_event: RtspEventMock, + mock_rtsp_signal_state: RtspStateType, ) -> None: """Successful setup.""" # Provide an entity that can be used to verify connection state on @@ -191,6 +192,7 @@ async def test_device_not_accessible( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Failed setup schedules a retry of setup.""" + config_entry.add_to_hass(hass) with patch.object(axis, "get_axis_api", side_effect=axis.errors.CannotConnect): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -202,6 +204,7 @@ async def test_device_trigger_reauth_flow( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Failed authentication trigger a reauthentication flow.""" + config_entry.add_to_hass(hass) with ( patch.object( axis, "get_axis_api", side_effect=axis.errors.AuthenticationRequired @@ -219,6 +222,7 @@ async def test_device_unknown_error( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Unknown errors are handled.""" + config_entry.add_to_hass(hass) with patch.object(axis, "get_axis_api", side_effect=Exception): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 2ffd21073af..acfcb8d48ec 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -18,6 +18,8 @@ async def test_setup_entry_fails( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Test successful setup of entry.""" + config_entry.add_to_hass(hass) + mock_device = Mock() mock_device.async_setup = AsyncMock(return_value=False) @@ -42,6 +44,7 @@ async def test_unload_entry( @pytest.mark.parametrize("config_entry_version", [1]) async def test_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Test successful migration of entry data.""" + config_entry.add_to_hass(hass) assert config_entry.version == 1 mock_device = Mock() diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index 47e00d9c341..9f68aa6fdd3 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -1,6 +1,5 @@ """Axis light platform tests.""" -from collections.abc import Callable from typing import Any from unittest.mock import patch @@ -18,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant +from .conftest import RtspEventMock from .const import DEFAULT_HOST, NAME API_DISCOVERY_LIGHT_CONTROL = { @@ -72,7 +72,7 @@ def light_control_fixture(light_control_items: list[dict[str, Any]]) -> None: @pytest.mark.usefixtures("config_entry_setup") async def test_no_light_entity_without_light_control_representation( hass: HomeAssistant, - mock_rtsp_event: Callable[[str, str, str, str, str, str], None], + mock_rtsp_event: RtspEventMock, ) -> None: """Verify no lights entities get created without light control representation.""" mock_rtsp_event( @@ -89,10 +89,7 @@ async def test_no_light_entity_without_light_control_representation( @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_LIGHT_CONTROL]) @pytest.mark.usefixtures("config_entry_setup") -async def test_lights( - hass: HomeAssistant, - mock_rtsp_event: Callable[[str, str, str, str, str, str], None], -) -> None: +async def test_lights(hass: HomeAssistant, mock_rtsp_event: RtspEventMock) -> None: """Test that lights are loaded properly.""" # Add light respx.post( diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index a211a42217c..8a93c844042 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -1,6 +1,5 @@ """Axis switch platform tests.""" -from collections.abc import Callable from unittest.mock import patch from axis.models.api import CONTEXT @@ -16,6 +15,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant +from .conftest import RtspEventMock from .const import API_DISCOVERY_PORT_MANAGEMENT, NAME PORT_DATA = """root.IOPort.I0.Configurable=yes @@ -33,7 +33,7 @@ root.IOPort.I1.Output.Active=open @pytest.mark.usefixtures("config_entry_setup") async def test_switches_with_port_cgi( hass: HomeAssistant, - mock_rtsp_event: Callable[[str, str, str, str, str, str], None], + mock_rtsp_event: RtspEventMock, ) -> None: """Test that switches are loaded properly using port.cgi.""" mock_rtsp_event( @@ -118,7 +118,7 @@ PORT_MANAGEMENT_RESPONSE = { @pytest.mark.usefixtures("config_entry_setup") async def test_switches_with_port_management( hass: HomeAssistant, - mock_rtsp_event: Callable[[str, str, str, str, str, str], None], + mock_rtsp_event: RtspEventMock, ) -> None: """Test that switches are loaded properly using port management.""" mock_rtsp_event( From 34f1443a5adb072482d776ba08606b6058e0743a Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 23 Jul 2024 19:54:43 +0200 Subject: [PATCH 1513/2411] Improve Axis hub tests (#122472) * Improve some of the hub tests * Replace constant "name" with "home" * Add snapshot * Simplify * Clean up --- tests/components/axis/const.py | 4 +- .../axis/snapshots/test_diagnostics.ambr | 4 +- tests/components/axis/snapshots/test_hub.ambr | 73 +++++++++++++++++++ tests/components/axis/test_hub.py | 63 +++------------- 4 files changed, 88 insertions(+), 56 deletions(-) create mode 100644 tests/components/axis/snapshots/test_hub.ambr diff --git a/tests/components/axis/const.py b/tests/components/axis/const.py index 16b9d17f99e..2efb464efd7 100644 --- a/tests/components/axis/const.py +++ b/tests/components/axis/const.py @@ -4,8 +4,8 @@ from axis.models.api import CONTEXT MAC = "00408C123456" FORMATTED_MAC = "00:40:8c:12:34:56" -MODEL = "model" -NAME = "name" +MODEL = "A1234" +NAME = "home" DEFAULT_HOST = "1.2.3.4" diff --git a/tests/components/axis/snapshots/test_diagnostics.ambr b/tests/components/axis/snapshots/test_diagnostics.ambr index 8ea316d00cf..3a643f55d3e 100644 --- a/tests/components/axis/snapshots/test_diagnostics.ambr +++ b/tests/components/axis/snapshots/test_diagnostics.ambr @@ -30,8 +30,8 @@ 'config': dict({ 'data': dict({ 'host': '1.2.3.4', - 'model': 'model', - 'name': 'name', + 'model': 'A1234', + 'name': 'home', 'password': '**REDACTED**', 'port': 80, 'username': '**REDACTED**', diff --git a/tests/components/axis/snapshots/test_hub.ambr b/tests/components/axis/snapshots/test_hub.ambr new file mode 100644 index 00000000000..16579287f09 --- /dev/null +++ b/tests/components/axis/snapshots/test_hub.ambr @@ -0,0 +1,73 @@ +# serializer version: 1 +# name: test_device_registry_entry[api_discovery_items0] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': 'http://1.2.3.4:80', + 'connections': set({ + tuple( + 'mac', + '00:40:8c:12:34:56', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'axis', + '00:40:8c:12:34:56', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Axis Communications AB', + 'model': 'A1234 Network Camera', + 'model_id': None, + 'name': 'home', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '00:40:8c:12:34:56', + 'suggested_area': None, + 'sw_version': '9.10.1', + 'via_device_id': None, + }) +# --- +# name: test_device_registry_entry[api_discovery_items1] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': 'http://1.2.3.4:80', + 'connections': set({ + tuple( + 'mac', + '00:40:8c:12:34:56', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'axis', + '00:40:8c:12:34:56', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Axis Communications AB', + 'model': 'A1234 Network Camera', + 'model_id': None, + 'name': 'home', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '00:40:8c:12:34:56', + 'suggested_area': None, + 'sw_version': '9.80.1', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index 012cef3bc5b..a28f6f4dabc 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -1,27 +1,21 @@ """Test Axis device.""" -from collections.abc import Callable, Generator +from collections.abc import Callable from ipaddress import ip_address from types import MappingProxyType from typing import Any from unittest import mock -from unittest.mock import ANY, AsyncMock, Mock, call, patch +from unittest.mock import ANY, Mock, call, patch import axis as axislib import pytest +from syrupy import SnapshotAssertion from homeassistant.components import axis, zeroconf from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_MODEL, - CONF_NAME, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, -) +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -38,54 +32,19 @@ from tests.common import async_fire_mqtt_message from tests.typing import MqttMockHAClient -@pytest.fixture(name="forward_entry_setups") -def hass_mock_forward_entry_setup(hass: HomeAssistant) -> Generator[AsyncMock]: - """Mock async_forward_entry_setups.""" - with patch.object( - hass.config_entries, "async_forward_entry_setups" - ) as forward_mock: - yield forward_mock - - -async def test_device_setup( - forward_entry_setups: AsyncMock, - config_entry_data: MappingProxyType[str, Any], +@pytest.mark.parametrize( + "api_discovery_items", [({}), (API_DISCOVERY_BASIC_DEVICE_INFO)] +) +async def test_device_registry_entry( config_entry_setup: ConfigEntry, device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, ) -> None: """Successful setup.""" - hub = config_entry_setup.runtime_data - - assert hub.api.vapix.firmware_version == "9.10.1" - assert hub.api.vapix.product_number == "M1065-LW" - assert hub.api.vapix.product_type == "Network Camera" - assert hub.api.vapix.serial_number == "00408C123456" - - assert len(forward_entry_setups.mock_calls) == 1 - platforms = set(forward_entry_setups.mock_calls[0][1][1]) - assert platforms == {"binary_sensor", "camera", "light", "switch"} - - assert hub.config.host == config_entry_data[CONF_HOST] - assert hub.config.model == config_entry_data[CONF_MODEL] - assert hub.config.name == config_entry_data[CONF_NAME] - assert hub.unique_id == FORMATTED_MAC - device_entry = device_registry.async_get_device( - identifiers={(AXIS_DOMAIN, hub.unique_id)} + identifiers={(AXIS_DOMAIN, config_entry_setup.unique_id)} ) - - assert device_entry.configuration_url == hub.api.config.url - - -@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_BASIC_DEVICE_INFO]) -async def test_device_info(config_entry_setup: ConfigEntry) -> None: - """Verify other path of device information works.""" - hub = config_entry_setup.runtime_data - - assert hub.api.vapix.firmware_version == "9.80.1" - assert hub.api.vapix.product_number == "M1065-LW" - assert hub.api.vapix.product_type == "Network Camera" - assert hub.api.vapix.serial_number == "00408C123456" + assert device_entry == snapshot @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_MQTT]) From 57c554f51689169f448294d8f12839904ce07da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 23 Jul 2024 20:48:34 +0200 Subject: [PATCH 1514/2411] Update AEMET-OpenData to v0.5.3 (#122480) --- homeassistant/components/aemet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index 8a22385f82b..d2e5c5fdc5a 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aemet", "iot_class": "cloud_polling", "loggers": ["aemet_opendata"], - "requirements": ["AEMET-OpenData==0.5.2"] + "requirements": ["AEMET-OpenData==0.5.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 09c147d4948..669d9a62356 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,7 +4,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.5.2 +AEMET-OpenData==0.5.3 # homeassistant.components.honeywell AIOSomecomfort==0.0.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c05868bbb7d..75401e23683 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.5.2 +AEMET-OpenData==0.5.3 # homeassistant.components.honeywell AIOSomecomfort==0.0.25 From e00f159ebeb0cb5f0b6ff27f7d8005b5ff51c096 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 23 Jul 2024 21:15:12 +0200 Subject: [PATCH 1515/2411] Fix loading created_at/modified_at from entity registry store (#122486) --- homeassistant/helpers/entity_registry.py | 8 ++++---- tests/helpers/test_entity_registry.py | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index a0bc63786d8..5d17c0c46b1 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -1286,7 +1286,7 @@ class EntityRegistry(BaseRegistry): categories=entity["categories"], capabilities=entity["capabilities"], config_entry_id=entity["config_entry_id"], - created_at=entity["created_at"], + created_at=datetime.fromisoformat(entity["created_at"]), device_class=entity["device_class"], device_id=entity["device_id"], disabled_by=RegistryEntryDisabler(entity["disabled_by"]) @@ -1303,7 +1303,7 @@ class EntityRegistry(BaseRegistry): id=entity["id"], has_entity_name=entity["has_entity_name"], labels=set(entity["labels"]), - modified_at=entity["modified_at"], + modified_at=datetime.fromisoformat(entity["modified_at"]), name=entity["name"], options=entity["options"], original_device_class=entity["original_device_class"], @@ -1335,10 +1335,10 @@ class EntityRegistry(BaseRegistry): ) deleted_entities[key] = DeletedRegistryEntry( config_entry_id=entity["config_entry_id"], - created_at=entity["created_at"], + created_at=datetime.fromisoformat(entity["created_at"]), entity_id=entity["entity_id"], id=entity["id"], - modified_at=entity["modified_at"], + modified_at=datetime.fromisoformat(entity["modified_at"]), orphaned_timestamp=entity["orphaned_timestamp"], platform=entity["platform"], unique_id=entity["unique_id"], diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index afcd0d0ed2e..9b1d68c7777 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -19,6 +19,7 @@ from homeassistant.const import ( from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import MaxLengthExceeded from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.dt import utc_from_timestamp from tests.common import ( ANY, @@ -442,6 +443,8 @@ async def test_filter_on_load( assert entry_with_name.name == "registry override" assert entry_without_name.name is None assert not entry_with_name.disabled + assert entry_with_name.created_at == utc_from_timestamp(0) + assert entry_with_name.modified_at == utc_from_timestamp(0) entry_disabled_hass = registry.async_get_or_create( "test", "super_platform", "disabled-hass" From 2730713b39b94a769d500b94d92c5f0a3085f4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 23 Jul 2024 21:22:42 +0200 Subject: [PATCH 1516/2411] Update aioairzone to v0.8.1 (#122481) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas Co-authored-by: J. Nick Koston --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airzone/test_climate.py | 2 +- tests/components/airzone/test_sensor.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 0a5b4b891aa..0c32787d8ae 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.8.0"] + "requirements": ["aioairzone==0.8.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 669d9a62356..bb6f2f79378 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -179,7 +179,7 @@ aioairq==0.3.2 aioairzone-cloud==0.5.4 # homeassistant.components.airzone -aioairzone==0.8.0 +aioairzone==0.8.1 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75401e23683..f5f5318df24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ aioairq==0.3.2 aioairzone-cloud==0.5.4 # homeassistant.components.airzone -aioairzone==0.8.0 +aioairzone==0.8.1 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index fa972bd3899..0f23c151e0e 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -248,7 +248,7 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: ), ): async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("climate.salon") assert state.attributes.get(ATTR_MAX_TEMP) == 25 diff --git a/tests/components/airzone/test_sensor.py b/tests/components/airzone/test_sensor.py index 3d75599d2d2..352994d6313 100644 --- a/tests/components/airzone/test_sensor.py +++ b/tests/components/airzone/test_sensor.py @@ -113,7 +113,7 @@ async def test_airzone_sensors_availability(hass: HomeAssistant) -> None: ), ): async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.dorm_ppal_temperature") assert state.state == STATE_UNAVAILABLE From e6ef8a34a7f34b129a4ae03049ac8cceda41484d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 23 Jul 2024 21:29:48 +0200 Subject: [PATCH 1517/2411] Tweak deCONZ init and hub tests (#122484) * Improve typing of init tests * Clean up gateway test * Validate deconz device registry entry * Rename gateway to hub * Snake case BRIDGEID to BRIDGE_ID --- tests/components/deconz/conftest.py | 7 +- .../components/deconz/snapshots/test_hub.ambr | 33 ++++++ tests/components/deconz/test_config_flow.py | 40 +++---- .../deconz/{test_gateway.py => test_hub.py} | 100 ++---------------- tests/components/deconz/test_init.py | 21 +--- tests/components/deconz/test_services.py | 10 +- 6 files changed, 74 insertions(+), 137 deletions(-) create mode 100644 tests/components/deconz/snapshots/test_hub.ambr rename tests/components/deconz/{test_gateway.py => test_hub.py} (52%) diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py index 19e0418c1a3..b468e402c34 100644 --- a/tests/components/deconz/conftest.py +++ b/tests/components/deconz/conftest.py @@ -35,7 +35,7 @@ class _WebsocketMock(Protocol): # Config entry fixtures API_KEY = "1234567890ABCDEF" -BRIDGEID = "01234E56789A" +BRIDGE_ID = "01234E56789A" HOST = "1.2.3.4" PORT = 80 @@ -50,7 +50,7 @@ def fixture_config_entry( return MockConfigEntry( domain=DECONZ_DOMAIN, entry_id="1", - unique_id=BRIDGEID, + unique_id=BRIDGE_ID, data=config_entry_data, options=config_entry_options, source=config_entry_source, @@ -158,7 +158,7 @@ def fixture_alarm_system_data() -> dict[str, Any]: def fixture_config_data() -> dict[str, Any]: """Config data.""" return { - "bridgeid": BRIDGEID, + "bridgeid": BRIDGE_ID, "ipaddress": HOST, "mac": "00:11:22:33:44:55", "modelid": "deCONZ", @@ -219,7 +219,6 @@ async def fixture_config_entry_factory( @pytest.fixture(name="config_entry_setup") async def fixture_config_entry_setup( - hass: HomeAssistant, config_entry_factory: Callable[[], Coroutine[Any, Any, MockConfigEntry]], ) -> MockConfigEntry: """Fixture providing a set up instance of deCONZ integration.""" diff --git a/tests/components/deconz/snapshots/test_hub.ambr b/tests/components/deconz/snapshots/test_hub.ambr new file mode 100644 index 00000000000..f3aa9a5e65d --- /dev/null +++ b/tests/components/deconz/snapshots/test_hub.ambr @@ -0,0 +1,33 @@ +# serializer version: 1 +# name: test_device_registry_entry + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': 'http://1.2.3.4:80', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'deconz', + '01234E56789A', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Dresden Elektronik', + 'model': 'deCONZ', + 'model_id': None, + 'name': 'deCONZ mock gateway', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 2d1f321fcd1..434856549c6 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -33,7 +33,7 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONTENT_TYPE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .conftest import API_KEY, BRIDGEID +from .conftest import API_KEY, BRIDGE_ID from tests.test_util.aiohttp import AiohttpClientMocker @@ -48,7 +48,7 @@ async def test_flow_discovered_bridges( aioclient_mock.get( pydeconz.utils.URL_DISCOVER, json=[ - {"id": BRIDGEID, "internalipaddress": "1.2.3.4", "internalport": 80}, + {"id": BRIDGE_ID, "internalipaddress": "1.2.3.4", "internalport": 80}, {"id": "1234E567890A", "internalipaddress": "5.6.7.8", "internalport": 80}, ], headers={"content-type": CONTENT_TYPE_JSON}, @@ -79,7 +79,7 @@ async def test_flow_discovered_bridges( ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == BRIDGEID + assert result["title"] == BRIDGE_ID assert result["data"] == { CONF_HOST: "1.2.3.4", CONF_PORT: 80, @@ -93,7 +93,7 @@ async def test_flow_manual_configuration_decision( """Test that config flow for one discovered bridge works.""" aioclient_mock.get( pydeconz.utils.URL_DISCOVER, - json=[{"id": BRIDGEID, "internalipaddress": "1.2.3.4", "internalport": 80}], + json=[{"id": BRIDGE_ID, "internalipaddress": "1.2.3.4", "internalport": 80}], headers={"content-type": CONTENT_TYPE_JSON}, ) @@ -124,7 +124,7 @@ async def test_flow_manual_configuration_decision( aioclient_mock.get( f"http://1.2.3.4:80/api/{API_KEY}/config", - json={"bridgeid": BRIDGEID}, + json={"bridgeid": BRIDGE_ID}, headers={"content-type": CONTENT_TYPE_JSON}, ) @@ -133,7 +133,7 @@ async def test_flow_manual_configuration_decision( ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == BRIDGEID + assert result["title"] == BRIDGE_ID assert result["data"] == { CONF_HOST: "1.2.3.4", CONF_PORT: 80, @@ -175,7 +175,7 @@ async def test_flow_manual_configuration( aioclient_mock.get( f"http://1.2.3.4:80/api/{API_KEY}/config", - json={"bridgeid": BRIDGEID}, + json={"bridgeid": BRIDGE_ID}, headers={"content-type": CONTENT_TYPE_JSON}, ) @@ -184,7 +184,7 @@ async def test_flow_manual_configuration( ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == BRIDGEID + assert result["title"] == BRIDGE_ID assert result["data"] == { CONF_HOST: "1.2.3.4", CONF_PORT: 80, @@ -257,7 +257,7 @@ async def test_manual_configuration_update_configuration( aioclient_mock.get( f"http://2.3.4.5:80/api/{API_KEY}/config", - json={"bridgeid": BRIDGEID}, + json={"bridgeid": BRIDGE_ID}, headers={"content-type": CONTENT_TYPE_JSON}, ) @@ -304,7 +304,7 @@ async def test_manual_configuration_dont_update_configuration( aioclient_mock.get( f"http://1.2.3.4:80/api/{API_KEY}/config", - json={"bridgeid": BRIDGEID}, + json={"bridgeid": BRIDGE_ID}, headers={"content-type": CONTENT_TYPE_JSON}, ) @@ -375,7 +375,7 @@ async def test_link_step_fails( """Test config flow should abort if no API key was possible to retrieve.""" aioclient_mock.get( pydeconz.utils.URL_DISCOVER, - json=[{"id": BRIDGEID, "internalipaddress": "1.2.3.4", "internalport": 80}], + json=[{"id": BRIDGE_ID, "internalipaddress": "1.2.3.4", "internalport": 80}], headers={"content-type": CONTENT_TYPE_JSON}, ) @@ -426,7 +426,7 @@ async def test_reauth_flow_update_configuration( aioclient_mock.get( f"http://1.2.3.4:80/api/{new_api_key}/config", - json={"bridgeid": BRIDGEID}, + json={"bridgeid": BRIDGE_ID}, headers={"content-type": CONTENT_TYPE_JSON}, ) @@ -451,7 +451,7 @@ async def test_flow_ssdp_discovery( ssdp_location="http://1.2.3.4:80/", upnp={ ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, - ATTR_UPNP_SERIAL: BRIDGEID, + ATTR_UPNP_SERIAL: BRIDGE_ID, }, ), context={"source": SOURCE_SSDP}, @@ -475,7 +475,7 @@ async def test_flow_ssdp_discovery( ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == BRIDGEID + assert result["title"] == BRIDGE_ID assert result["data"] == { CONF_HOST: "1.2.3.4", CONF_PORT: 80, @@ -499,7 +499,7 @@ async def test_ssdp_discovery_update_configuration( ssdp_location="http://2.3.4.5:80/", upnp={ ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, - ATTR_UPNP_SERIAL: BRIDGEID, + ATTR_UPNP_SERIAL: BRIDGE_ID, }, ), context={"source": SOURCE_SSDP}, @@ -525,7 +525,7 @@ async def test_ssdp_discovery_dont_update_configuration( ssdp_location="http://1.2.3.4:80/", upnp={ ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, - ATTR_UPNP_SERIAL: BRIDGEID, + ATTR_UPNP_SERIAL: BRIDGE_ID, }, ), context={"source": SOURCE_SSDP}, @@ -549,7 +549,7 @@ async def test_ssdp_discovery_dont_update_existing_hassio_configuration( ssdp_location="http://1.2.3.4:80/", upnp={ ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, - ATTR_UPNP_SERIAL: BRIDGEID, + ATTR_UPNP_SERIAL: BRIDGE_ID, }, ), context={"source": SOURCE_SSDP}, @@ -569,7 +569,7 @@ async def test_flow_hassio_discovery(hass: HomeAssistant) -> None: "addon": "Mock Addon", CONF_HOST: "mock-deconz", CONF_PORT: 80, - CONF_SERIAL: BRIDGEID, + CONF_SERIAL: BRIDGE_ID, CONF_API_KEY: API_KEY, }, name="Mock Addon", @@ -622,7 +622,7 @@ async def test_hassio_discovery_update_configuration( CONF_HOST: "2.3.4.5", CONF_PORT: 8080, CONF_API_KEY: "updated", - CONF_SERIAL: BRIDGEID, + CONF_SERIAL: BRIDGE_ID, }, name="Mock Addon", slug="deconz", @@ -650,7 +650,7 @@ async def test_hassio_discovery_dont_update_configuration(hass: HomeAssistant) - CONF_HOST: "1.2.3.4", CONF_PORT: 80, CONF_API_KEY: API_KEY, - CONF_SERIAL: BRIDGEID, + CONF_SERIAL: BRIDGE_ID, }, name="Mock Addon", slug="deconz", diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_hub.py similarity index 52% rename from tests/components/deconz/test_gateway.py rename to tests/components/deconz/test_hub.py index bc7c3362d6b..3a334a47838 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_hub.py @@ -1,120 +1,40 @@ """Test deCONZ gateway.""" -from collections.abc import Callable from unittest.mock import patch import pydeconz from pydeconz.websocket import State import pytest +from syrupy import SnapshotAssertion from homeassistant.components import ssdp -from homeassistant.components.alarm_control_panel import ( - DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, -) -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.components.deconz.config_flow import DECONZ_MANUFACTURERURL from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.errors import AuthenticationRequired, CannotConnect from homeassistant.components.deconz.hub import DeconzHub, get_deconz_api -from homeassistant.components.fan import DOMAIN as FAN_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN -from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN -from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN -from homeassistant.components.select import DOMAIN as SELECT_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN from homeassistant.components.ssdp import ( ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL, ATTR_UPNP_UDN, ) -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import SOURCE_HASSIO, SOURCE_SSDP, ConfigEntry +from homeassistant.config_entries import SOURCE_SSDP, ConfigEntry from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from .conftest import BRIDGEID, HOST, PORT +from .conftest import BRIDGE_ID -async def test_gateway_setup( - hass: HomeAssistant, +async def test_device_registry_entry( + config_entry_setup: ConfigEntry, device_registry: dr.DeviceRegistry, - config_entry_factory: Callable[[], ConfigEntry], + snapshot: SnapshotAssertion, ) -> None: """Successful setup.""" - # Patching async_forward_entry_setup* is not advisable, and should be refactored - # in the future. - with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups", - return_value=True, - ) as forward_entry_setup: - config_entry = await config_entry_factory() - gateway = DeconzHub.get_hub(hass, config_entry) - assert gateway.bridgeid == BRIDGEID - assert gateway.master is True - assert gateway.config.allow_clip_sensor is False - assert gateway.config.allow_deconz_groups is True - assert gateway.config.allow_new_devices is True - - assert len(gateway.deconz_ids) == 0 - assert len(hass.states.async_all()) == 0 - - assert forward_entry_setup.mock_calls[0][1] == ( - config_entry, - [ - ALARM_CONTROL_PANEL_DOMAIN, - BINARY_SENSOR_DOMAIN, - BUTTON_DOMAIN, - CLIMATE_DOMAIN, - COVER_DOMAIN, - FAN_DOMAIN, - LIGHT_DOMAIN, - LOCK_DOMAIN, - NUMBER_DOMAIN, - SCENE_DOMAIN, - SELECT_DOMAIN, - SENSOR_DOMAIN, - SIREN_DOMAIN, - SWITCH_DOMAIN, - ], - ) - - gateway_entry = device_registry.async_get_device( - identifiers={(DECONZ_DOMAIN, gateway.bridgeid)} - ) - - assert gateway_entry.configuration_url == f"http://{HOST}:{PORT}" - assert gateway_entry.entry_type is dr.DeviceEntryType.SERVICE - - -@pytest.mark.parametrize("config_entry_source", [SOURCE_HASSIO]) -async def test_gateway_device_configuration_url_when_addon( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - config_entry_factory: Callable[[], ConfigEntry], -) -> None: - """Successful setup.""" - # Patching async_forward_entry_setup* is not advisable, and should be refactored - # in the future. - with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups", - return_value=True, - ): - config_entry = await config_entry_factory() - gateway = DeconzHub.get_hub(hass, config_entry) - - gateway_entry = device_registry.async_get_device( - identifiers={(DECONZ_DOMAIN, gateway.bridgeid)} - ) - - assert ( - gateway_entry.configuration_url == "homeassistant://hassio/ingress/core_deconz" + device_entry = device_registry.async_get_device( + identifiers={(DECONZ_DOMAIN, config_entry_setup.unique_id)} ) + assert device_entry == snapshot @pytest.mark.parametrize( @@ -166,7 +86,7 @@ async def test_update_address( ssdp_location="http://2.3.4.5:80/", upnp={ ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, - ATTR_UPNP_SERIAL: BRIDGEID, + ATTR_UPNP_SERIAL: BRIDGE_ID, ATTR_UPNP_UDN: "uuid:456DEF", }, ), diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index c8b2ef698d2..0a4e63de6ab 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -16,22 +16,9 @@ from homeassistant.core import HomeAssistant from .conftest import ConfigEntryFactoryType from tests.common import MockConfigEntry -from tests.test_util.aiohttp import AiohttpClientMocker - -ENTRY1_HOST = "1.2.3.4" -ENTRY1_PORT = 80 -ENTRY1_API_KEY = "1234567890ABCDEF" -ENTRY1_BRIDGEID = "12345ABC" -ENTRY1_UUID = "456DEF" - -ENTRY2_HOST = "2.3.4.5" -ENTRY2_PORT = 80 -ENTRY2_API_KEY = "1234567890ABCDEF" -ENTRY2_BRIDGEID = "23456DEF" -ENTRY2_UUID = "789ACE" -async def setup_entry(hass, entry): +async def setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Test that setup entry works.""" with ( patch.object(DeconzHub, "async_setup", return_value=True), @@ -109,9 +96,7 @@ async def test_unload_entry( async def test_unload_entry_multiple_gateways( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - config_entry_factory, + hass: HomeAssistant, config_entry_factory: ConfigEntryFactoryType ) -> None: """Test being able to unload an entry and master gateway gets moved.""" config_entry = await config_entry_factory() @@ -133,7 +118,7 @@ async def test_unload_entry_multiple_gateways( async def test_unload_entry_multiple_gateways_parallel( - hass: HomeAssistant, config_entry_factory + hass: HomeAssistant, config_entry_factory: ConfigEntryFactoryType ) -> None: """Test race condition when unloading multiple config entries in parallel.""" config_entry = await config_entry_factory() diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 72d69c4559e..4b0d8ab1405 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -25,7 +25,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from .test_gateway import BRIDGEID +from .test_hub import BRIDGE_ID from tests.common import async_capture_events from tests.test_util.aiohttp import AiohttpClientMocker @@ -39,7 +39,7 @@ async def test_configure_service_with_field( """Test that service invokes pydeconz with the correct path and data.""" data = { SERVICE_FIELD: "/lights/2", - CONF_BRIDGE_ID: BRIDGEID, + CONF_BRIDGE_ID: BRIDGE_ID, SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } @@ -228,7 +228,7 @@ async def test_service_refresh_devices( mock_requests() await hass.services.async_call( - DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH, service_data={CONF_BRIDGE_ID: BRIDGEID} + DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH, service_data={CONF_BRIDGE_ID: BRIDGE_ID} ) await hass.async_block_till_done() @@ -294,7 +294,7 @@ async def test_service_refresh_devices_trigger_no_state_update( mock_requests() await hass.services.async_call( - DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH, service_data={CONF_BRIDGE_ID: BRIDGEID} + DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH, service_data={CONF_BRIDGE_ID: BRIDGE_ID} ) await hass.async_block_till_done() @@ -369,7 +369,7 @@ async def test_remove_orphaned_entries_service( await hass.services.async_call( DECONZ_DOMAIN, SERVICE_REMOVE_ORPHANED_ENTRIES, - service_data={CONF_BRIDGE_ID: BRIDGEID}, + service_data={CONF_BRIDGE_ID: BRIDGE_ID}, ) await hass.async_block_till_done() From ad38b9e9e1d0cdf349609c30776e21b5972a0ab4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 23 Jul 2024 21:30:03 +0200 Subject: [PATCH 1518/2411] Use snapshot validation on Axis binary sensor (#122483) * Use snapshot validation on binary sensor * Use snapshot_platform * Clean up * Improve typign --- .../axis/snapshots/test_binary_sensor.ambr | 1343 +++++++++++++++++ tests/components/axis/test_binary_sensor.py | 124 +- 2 files changed, 1370 insertions(+), 97 deletions(-) create mode 100644 tests/components/axis/snapshots/test_binary_sensor.ambr diff --git a/tests/components/axis/snapshots/test_binary_sensor.ambr b/tests/components/axis/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..94b1cc2fc2e --- /dev/null +++ b/tests/components/axis/snapshots/test_binary_sensor.ambr @@ -0,0 +1,1343 @@ +# serializer version: 1 +# name: test_binary_sensors[event0-binary_sensor.name_daynight_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'light', + 'friendly_name': 'name DayNight 1', + }), + 'context': , + 'entity_id': 'binary_sensor.name_daynight_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event0-daynight_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'light', + 'friendly_name': 'name DayNight 1', + }), + 'context': , + 'entity_id': 'binary_sensor.name_daynight_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event0-daynight_1][binary_sensor.home_daynight_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_daynight_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DayNight 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tns1:VideoSource/tnsaxis:DayNightVision-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event0-daynight_1][binary_sensor.home_daynight_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'light', + 'friendly_name': 'home DayNight 1', + }), + 'context': , + 'entity_id': 'binary_sensor.home_daynight_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event0][binary_sensor.home_daynight_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_daynight_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DayNight 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tns1:VideoSource/tnsaxis:DayNightVision-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event0][binary_sensor.home_daynight_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'light', + 'friendly_name': 'home DayNight 1', + }), + 'context': , + 'entity_id': 'binary_sensor.home_daynight_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event1-binary_sensor.name_sound_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'sound', + 'friendly_name': 'name Sound 1', + }), + 'context': , + 'entity_id': 'binary_sensor.name_sound_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[event1-sound_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'sound', + 'friendly_name': 'name Sound 1', + }), + 'context': , + 'entity_id': 'binary_sensor.name_sound_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[event1-sound_1][binary_sensor.home_sound_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_sound_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Sound 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tns1:AudioSource/tnsaxis:TriggerLevel-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event1-sound_1][binary_sensor.home_sound_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'sound', + 'friendly_name': 'home Sound 1', + }), + 'context': , + 'entity_id': 'binary_sensor.home_sound_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[event10-binary_sensor.name_object_analytics_device1scenario8] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name Object Analytics Device1Scenario8', + }), + 'context': , + 'entity_id': 'binary_sensor.name_object_analytics_device1scenario8', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event10-object_analytics_device1scenario8] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name Object Analytics Device1Scenario8', + }), + 'context': , + 'entity_id': 'binary_sensor.name_object_analytics_device1scenario8', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event10-object_analytics_device1scenario8][binary_sensor.home_object_analytics_device1scenario8-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_object_analytics_device1scenario8', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Object Analytics Device1Scenario8', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario8-Device1Scenario8', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event10-object_analytics_device1scenario8][binary_sensor.home_object_analytics_device1scenario8-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home Object Analytics Device1Scenario8', + }), + 'context': , + 'entity_id': 'binary_sensor.home_object_analytics_device1scenario8', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event10][binary_sensor.home_object_analytics_device1scenario8-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_object_analytics_device1scenario8', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Object Analytics Device1Scenario8', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario8-Device1Scenario8', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event10][binary_sensor.home_object_analytics_device1scenario8-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home Object Analytics Device1Scenario8', + }), + 'context': , + 'entity_id': 'binary_sensor.home_object_analytics_device1scenario8', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event1][binary_sensor.home_sound_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_sound_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Sound 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tns1:AudioSource/tnsaxis:TriggerLevel-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event1][binary_sensor.home_sound_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'sound', + 'friendly_name': 'home Sound 1', + }), + 'context': , + 'entity_id': 'binary_sensor.home_sound_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[event2-binary_sensor.name_pir_sensor] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'name PIR sensor', + }), + 'context': , + 'entity_id': 'binary_sensor.name_pir_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[event2-pir_sensor] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'name PIR sensor', + }), + 'context': , + 'entity_id': 'binary_sensor.name_pir_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[event2-pir_sensor][binary_sensor.home_pir_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_pir_sensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PIR sensor', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tns1:Device/tnsaxis:IO/Port-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event2-pir_sensor][binary_sensor.home_pir_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'home PIR sensor', + }), + 'context': , + 'entity_id': 'binary_sensor.home_pir_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[event2][binary_sensor.home_pir_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_pir_sensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PIR sensor', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tns1:Device/tnsaxis:IO/Port-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event2][binary_sensor.home_pir_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'home PIR sensor', + }), + 'context': , + 'entity_id': 'binary_sensor.home_pir_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[event3-binary_sensor.name_pir_0] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name PIR 0', + }), + 'context': , + 'entity_id': 'binary_sensor.name_pir_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[event3-pir_0] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name PIR 0', + }), + 'context': , + 'entity_id': 'binary_sensor.name_pir_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[event3-pir_0][binary_sensor.home_pir_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_pir_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PIR 0', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tns1:Device/tnsaxis:Sensor/PIR-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event3-pir_0][binary_sensor.home_pir_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home PIR 0', + }), + 'context': , + 'entity_id': 'binary_sensor.home_pir_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[event3][binary_sensor.home_pir_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_pir_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PIR 0', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tns1:Device/tnsaxis:Sensor/PIR-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event3][binary_sensor.home_pir_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home PIR 0', + }), + 'context': , + 'entity_id': 'binary_sensor.home_pir_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[event4-binary_sensor.name_fence_guard_profile_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name Fence Guard Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.name_fence_guard_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event4-fence_guard_profile_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name Fence Guard Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.name_fence_guard_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event4-fence_guard_profile_1][binary_sensor.home_fence_guard_profile_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_fence_guard_profile_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fence Guard Profile 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/FenceGuard/Camera1Profile1-Camera1Profile1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event4-fence_guard_profile_1][binary_sensor.home_fence_guard_profile_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home Fence Guard Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.home_fence_guard_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event4][binary_sensor.home_fence_guard_profile_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_fence_guard_profile_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fence Guard Profile 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/FenceGuard/Camera1Profile1-Camera1Profile1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event4][binary_sensor.home_fence_guard_profile_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home Fence Guard Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.home_fence_guard_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event5-binary_sensor.name_motion_guard_profile_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name Motion Guard Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.name_motion_guard_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event5-motion_guard_profile_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name Motion Guard Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.name_motion_guard_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event5-motion_guard_profile_1][binary_sensor.home_motion_guard_profile_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_motion_guard_profile_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion Guard Profile 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/MotionGuard/Camera1Profile1-Camera1Profile1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event5-motion_guard_profile_1][binary_sensor.home_motion_guard_profile_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home Motion Guard Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.home_motion_guard_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event5][binary_sensor.home_motion_guard_profile_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_motion_guard_profile_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion Guard Profile 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/MotionGuard/Camera1Profile1-Camera1Profile1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event5][binary_sensor.home_motion_guard_profile_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home Motion Guard Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.home_motion_guard_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event6-binary_sensor.name_loitering_guard_profile_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name Loitering Guard Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.name_loitering_guard_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event6-loitering_guard_profile_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name Loitering Guard Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.name_loitering_guard_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event6-loitering_guard_profile_1][binary_sensor.home_loitering_guard_profile_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_loitering_guard_profile_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Loitering Guard Profile 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/LoiteringGuard/Camera1Profile1-Camera1Profile1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event6-loitering_guard_profile_1][binary_sensor.home_loitering_guard_profile_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home Loitering Guard Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.home_loitering_guard_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event6][binary_sensor.home_loitering_guard_profile_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_loitering_guard_profile_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Loitering Guard Profile 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/LoiteringGuard/Camera1Profile1-Camera1Profile1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event6][binary_sensor.home_loitering_guard_profile_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home Loitering Guard Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.home_loitering_guard_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event7-binary_sensor.name_vmd4_profile_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name VMD4 Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.name_vmd4_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event7-vmd4_profile_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name VMD4 Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.name_vmd4_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event7-vmd4_profile_1][binary_sensor.home_vmd4_profile_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_vmd4_profile_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VMD4 Profile 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1-Camera1Profile1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event7-vmd4_profile_1][binary_sensor.home_vmd4_profile_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home VMD4 Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.home_vmd4_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event7][binary_sensor.home_vmd4_profile_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_vmd4_profile_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VMD4 Profile 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1-Camera1Profile1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event7][binary_sensor.home_vmd4_profile_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home VMD4 Profile 1', + }), + 'context': , + 'entity_id': 'binary_sensor.home_vmd4_profile_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event8-binary_sensor.name_object_analytics_scenario_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name Object Analytics Scenario 1', + }), + 'context': , + 'entity_id': 'binary_sensor.name_object_analytics_scenario_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event8-object_analytics_scenario_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name Object Analytics Scenario 1', + }), + 'context': , + 'entity_id': 'binary_sensor.name_object_analytics_scenario_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event8-object_analytics_scenario_1][binary_sensor.home_object_analytics_scenario_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_object_analytics_scenario_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Object Analytics Scenario 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1-Device1Scenario1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event8-object_analytics_scenario_1][binary_sensor.home_object_analytics_scenario_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home Object Analytics Scenario 1', + }), + 'context': , + 'entity_id': 'binary_sensor.home_object_analytics_scenario_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event8][binary_sensor.home_object_analytics_scenario_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_object_analytics_scenario_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Object Analytics Scenario 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1-Device1Scenario1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event8][binary_sensor.home_object_analytics_scenario_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home Object Analytics Scenario 1', + }), + 'context': , + 'entity_id': 'binary_sensor.home_object_analytics_scenario_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event9-binary_sensor.name_vmd4_camera1profile9] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name VMD4 Camera1Profile9', + }), + 'context': , + 'entity_id': 'binary_sensor.name_vmd4_camera1profile9', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event9-vmd4_camera1profile9] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'name VMD4 Camera1Profile9', + }), + 'context': , + 'entity_id': 'binary_sensor.name_vmd4_camera1profile9', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event9-vmd4_camera1profile9][binary_sensor.home_vmd4_camera1profile9-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_vmd4_camera1profile9', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VMD4 Camera1Profile9', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile9-Camera1Profile9', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event9-vmd4_camera1profile9][binary_sensor.home_vmd4_camera1profile9-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home VMD4 Camera1Profile9', + }), + 'context': , + 'entity_id': 'binary_sensor.home_vmd4_camera1profile9', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[event9][binary_sensor.home_vmd4_camera1profile9-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.home_vmd4_camera1profile9', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VMD4 Camera1Profile9', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile9-Camera1Profile9', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[event9][binary_sensor.home_vmd4_camera1profile9-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'home VMD4 Camera1Profile9', + }), + 'context': , + 'entity_id': 'binary_sensor.home_vmd4_camera1profile9', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 1ba1520aa6a..a1cf1e129d5 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -1,22 +1,22 @@ """Axis binary sensor platform tests.""" -from typing import Any +from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion -from homeassistant.components.binary_sensor import ( - DOMAIN as BINARY_SENSOR_DOMAIN, - BinarySensorDeviceClass, -) -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er -from .conftest import RtspEventMock -from .const import NAME +from .conftest import ConfigEntryFactoryType, RtspEventMock + +from tests.common import snapshot_platform @pytest.mark.parametrize( - ("event", "entity"), + "event", [ ( { @@ -25,13 +25,7 @@ from .const import NAME "source_idx": "1", "data_type": "DayNight", "data_value": "1", - }, - { - "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_daynight_1", - "state": STATE_ON, - "name": f"{NAME} DayNight 1", - "device_class": BinarySensorDeviceClass.LIGHT, - }, + } ), ( { @@ -40,13 +34,7 @@ from .const import NAME "source_idx": "1", "data_type": "Sound", "data_value": "0", - }, - { - "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1", - "state": STATE_OFF, - "name": f"{NAME} Sound 1", - "device_class": BinarySensorDeviceClass.SOUND, - }, + } ), ( { @@ -56,13 +44,7 @@ from .const import NAME "operation": "Initialized", "source_name": "port", "source_idx": "0", - }, - { - "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_pir_sensor", - "state": STATE_OFF, - "name": f"{NAME} PIR sensor", - "device_class": BinarySensorDeviceClass.CONNECTIVITY, - }, + } ), ( { @@ -71,78 +53,42 @@ from .const import NAME "data_value": "0", "source_name": "sensor", "source_idx": "0", - }, - { - "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_pir_0", - "state": STATE_OFF, - "name": f"{NAME} PIR 0", - "device_class": BinarySensorDeviceClass.MOTION, - }, + } ), ( { "topic": "tnsaxis:CameraApplicationPlatform/FenceGuard/Camera1Profile1", "data_type": "active", "data_value": "1", - }, - { - "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_fence_guard_profile_1", - "state": STATE_ON, - "name": f"{NAME} Fence Guard Profile 1", - "device_class": BinarySensorDeviceClass.MOTION, - }, + } ), ( { "topic": "tnsaxis:CameraApplicationPlatform/MotionGuard/Camera1Profile1", "data_type": "active", "data_value": "1", - }, - { - "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_motion_guard_profile_1", - "state": STATE_ON, - "name": f"{NAME} Motion Guard Profile 1", - "device_class": BinarySensorDeviceClass.MOTION, - }, + } ), ( { "topic": "tnsaxis:CameraApplicationPlatform/LoiteringGuard/Camera1Profile1", "data_type": "active", "data_value": "1", - }, - { - "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_loitering_guard_profile_1", - "state": STATE_ON, - "name": f"{NAME} Loitering Guard Profile 1", - "device_class": BinarySensorDeviceClass.MOTION, - }, + } ), ( { "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1", "data_type": "active", "data_value": "1", - }, - { - "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_vmd4_profile_1", - "state": STATE_ON, - "name": f"{NAME} VMD4 Profile 1", - "device_class": BinarySensorDeviceClass.MOTION, - }, + } ), ( { "topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1", "data_type": "active", "data_value": "1", - }, - { - "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_object_analytics_scenario_1", - "state": STATE_ON, - "name": f"{NAME} Object Analytics Scenario 1", - "device_class": BinarySensorDeviceClass.MOTION, - }, + } ), # Events with names generated from event ID and topic ( @@ -150,50 +96,35 @@ from .const import NAME "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile9", "data_type": "active", "data_value": "1", - }, - { - "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_vmd4_camera1profile9", - "state": STATE_ON, - "name": f"{NAME} VMD4 Camera1Profile9", - "device_class": BinarySensorDeviceClass.MOTION, - }, + } ), ( { "topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario8", "data_type": "active", "data_value": "1", - }, - { - "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_object_analytics_device1scenario8", - "state": STATE_ON, - "name": f"{NAME} Object Analytics Device1Scenario8", - "device_class": BinarySensorDeviceClass.MOTION, - }, + } ), ], ) -@pytest.mark.usefixtures("config_entry_setup") async def test_binary_sensors( hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + config_entry_factory: ConfigEntryFactoryType, mock_rtsp_event: RtspEventMock, event: dict[str, str], - entity: dict[str, Any], ) -> None: """Test that sensors are loaded properly.""" + with patch("homeassistant.components.axis.PLATFORMS", [Platform.BINARY_SENSOR]): + config_entry = await config_entry_factory() mock_rtsp_event(**event) - await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 1 - - state = hass.states.get(entity["id"]) - assert state.state == entity["state"] - assert state.name == entity["name"] - assert state.attributes["device_class"] == entity["device_class"] + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) @pytest.mark.parametrize( - ("event"), + "event", [ # Event with unsupported topic { @@ -233,5 +164,4 @@ async def test_unsupported_events( ) -> None: """Validate nothing breaks with unsupported events.""" mock_rtsp_event(**event) - await hass.async_block_till_done() assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 0 From ac867385727e08d3817aa144ac9b425b1bc2d3bb Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 23 Jul 2024 21:41:16 +0200 Subject: [PATCH 1519/2411] Update pylutron to 0.2.15 (#122455) --- homeassistant/components/lutron/manifest.json | 2 +- pyproject.toml | 5 +---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index f3aeb5feb90..d9432f77bba 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/lutron", "iot_class": "local_polling", "loggers": ["pylutron"], - "requirements": ["pylutron==0.2.13"] + "requirements": ["pylutron==0.2.15"] } diff --git a/pyproject.toml b/pyproject.toml index c3a9b864df9..172bb0139d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -539,7 +539,7 @@ filterwarnings = [ # https://pypi.org/project/emulated-roku/ - v0.3.0 - 2023-12-19 # https://github.com/martonperei/emulated_roku "ignore:loop argument is deprecated:DeprecationWarning:emulated_roku", - # https://github.com/thecynic/pylutron - v0.2.13 + # https://github.com/thecynic/pylutron - v0.2.15 "ignore:setDaemon\\(\\) is deprecated, set the daemon attribute instead:DeprecationWarning:pylutron", # https://github.com/pschmitt/pynuki/blob/1.6.3/pynuki/utils.py#L21 - v1.6.3 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pynuki.utils", @@ -598,9 +598,6 @@ filterwarnings = [ # https://github.com/nextcord/nextcord/issues/1174 # https://github.com/nextcord/nextcord/blob/v2.6.1/nextcord/player.py#L5 "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning:nextcord.player", - # https://pypi.org/project/pylutron/ - v0.2.12 - 2024-02-12 - # https://github.com/thecynic/pylutron/issues/89 - "ignore:'telnetlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:pylutron", # https://pypi.org/project/SpeechRecognition/ - v3.10.4 - 2024-05-05 # https://github.com/Uberi/speech_recognition/blob/3.10.4/speech_recognition/__init__.py#L7 "ignore:'aifc' is deprecated and slated for removal in Python 3.13:DeprecationWarning:speech_recognition", diff --git a/requirements_all.txt b/requirements_all.txt index bb6f2f79378..c2392984415 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1987,7 +1987,7 @@ pylitterbot==2023.5.0 pylutron-caseta==0.20.0 # homeassistant.components.lutron -pylutron==0.2.13 +pylutron==0.2.15 # homeassistant.components.mailgun pymailgunner==1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5f5318df24..df43ecb834b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1577,7 +1577,7 @@ pylitterbot==2023.5.0 pylutron-caseta==0.20.0 # homeassistant.components.lutron -pylutron==0.2.13 +pylutron==0.2.15 # homeassistant.components.mailgun pymailgunner==1.4 From fd6f1cfbdc5985018d0620e41de59e89a05b7a06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 23 Jul 2024 21:55:18 +0200 Subject: [PATCH 1520/2411] Update aioairzone-cloud to v0.5.5 (#122482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas Co-authored-by: J. Nick Koston --- homeassistant/components/airzone_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/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index e317dd82366..38d50cb02c8 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_push", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.5.4"] + "requirements": ["aioairzone-cloud==0.5.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index c2392984415..ab198332739 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -176,7 +176,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.5.4 +aioairzone-cloud==0.5.5 # homeassistant.components.airzone aioairzone==0.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df43ecb834b..18033e541bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.5.4 +aioairzone-cloud==0.5.5 # homeassistant.components.airzone aioairzone==0.8.1 From f135d3d16cf0dee692c9092a4f9332b50ff5d98d Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Tue, 23 Jul 2024 21:56:46 +0200 Subject: [PATCH 1521/2411] Fix device class on sensor in ViCare (#122334) update device class on init --- homeassistant/components/vicare/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 0e98729e40f..0271ffc9798 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -950,6 +950,8 @@ class ViCareSensor(ViCareEntity, SensorEntity): """Initialize the sensor.""" super().__init__(device_config, api, description.key) self.entity_description = description + # run update to have device_class set depending on unit_of_measurement + self.update() @property def available(self) -> bool: From e1e64be3c9a2597ca17f77ba1ae6c8dbb063eef3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Jul 2024 15:47:27 -0500 Subject: [PATCH 1522/2411] Remigrate device_registry created_at/modified_at (#122490) * Remigrate device_registry created_at/modified_at Nightly current does not boot up because the device registry will have KeyError: created_at if the previous nightly was installed. * reduce * split migration per discord comments --- homeassistant/helpers/device_registry.py | 7 +- tests/helpers/test_device_registry.py | 146 ++++++++++++++++++++++- 2 files changed, 149 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 96bba6c8c4c..30001a64474 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -57,7 +57,7 @@ EVENT_DEVICE_REGISTRY_UPDATED: EventType[EventDeviceRegistryUpdatedData] = Event ) STORAGE_KEY = "core.device_registry" STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 7 +STORAGE_VERSION_MINOR = 8 CLEANUP_DELAY = 10 @@ -505,9 +505,12 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): device["primary_config_entry"] = None if old_minor_version < 7: # Introduced in 2024.8 - created_at = utc_from_timestamp(0).isoformat() for device in old_data["devices"]: device["model_id"] = None + if old_minor_version < 8: + # Introduced in 2024.8 + created_at = utc_from_timestamp(0).isoformat() + for device in old_data["devices"]: device["created_at"] = device["modified_at"] = created_at for device in old_data["deleted_devices"]: device["created_at"] = device["modified_at"] = created_at diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index ffbc78ac463..129c6b0d37c 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -993,12 +993,12 @@ async def test_migration_1_5_to_1_7( @pytest.mark.parametrize("load_registries", [False]) @pytest.mark.usefixtures("freezer") -async def test_migration_1_6_to_1_7( +async def test_migration_1_6_to_1_8( hass: HomeAssistant, hass_storage: dict[str, Any], mock_config_entry: MockConfigEntry, ) -> None: - """Test migration from version 1.6 to 1.7.""" + """Test migration from version 1.6 to 1.8.""" hass_storage[dr.STORAGE_KEY] = { "version": 1, "minor_version": 6, @@ -1131,6 +1131,148 @@ async def test_migration_1_6_to_1_7( } +@pytest.mark.parametrize("load_registries", [False]) +@pytest.mark.usefixtures("freezer") +async def test_migration_1_7_to_1_8( + hass: HomeAssistant, + hass_storage: dict[str, Any], + mock_config_entry: MockConfigEntry, +) -> None: + """Test migration from version 1.7 to 1.8.""" + hass_storage[dr.STORAGE_KEY] = { + "version": 1, + "minor_version": 7, + "key": dr.STORAGE_KEY, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": [mock_config_entry.entry_id], + "configuration_url": None, + "connections": [["Zigbee", "01.23.45.67.89"]], + "disabled_by": None, + "entry_type": "service", + "hw_version": "hw_version", + "id": "abcdefghijklm", + "identifiers": [["serial", "123456ABCDEF"]], + "labels": ["blah"], + "manufacturer": "manufacturer", + "model": "model", + "model_id": None, + "name": "name", + "name_by_user": None, + "primary_config_entry": mock_config_entry.entry_id, + "serial_number": None, + "sw_version": "new_version", + "via_device_id": None, + }, + { + "area_id": None, + "config_entries": [None], + "configuration_url": None, + "connections": [], + "disabled_by": None, + "entry_type": None, + "hw_version": None, + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "labels": ["blah"], + "manufacturer": None, + "model": None, + "model_id": None, + "name_by_user": None, + "primary_config_entry": None, + "name": None, + "serial_number": None, + "sw_version": None, + "via_device_id": None, + }, + ], + "deleted_devices": [], + }, + } + + await dr.async_load(hass) + registry = dr.async_get(hass) + + # Test data was loaded + entry = registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={("Zigbee", "01.23.45.67.89")}, + identifiers={("serial", "123456ABCDEF")}, + ) + assert entry.id == "abcdefghijklm" + + # Update to trigger a store + entry = registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={("Zigbee", "01.23.45.67.89")}, + identifiers={("serial", "123456ABCDEF")}, + sw_version="new_version", + ) + assert entry.id == "abcdefghijklm" + + # Check we store migrated data + await flush_store(registry._store) + + assert hass_storage[dr.STORAGE_KEY] == { + "version": dr.STORAGE_VERSION_MAJOR, + "minor_version": dr.STORAGE_VERSION_MINOR, + "key": dr.STORAGE_KEY, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": [mock_config_entry.entry_id], + "configuration_url": None, + "connections": [["Zigbee", "01.23.45.67.89"]], + "created_at": "1970-01-01T00:00:00+00:00", + "disabled_by": None, + "entry_type": "service", + "hw_version": "hw_version", + "id": "abcdefghijklm", + "identifiers": [["serial", "123456ABCDEF"]], + "labels": ["blah"], + "manufacturer": "manufacturer", + "model": "model", + "name": "name", + "model_id": None, + "modified_at": "1970-01-01T00:00:00+00:00", + "name_by_user": None, + "primary_config_entry": mock_config_entry.entry_id, + "serial_number": None, + "sw_version": "new_version", + "via_device_id": None, + }, + { + "area_id": None, + "config_entries": [None], + "configuration_url": None, + "connections": [], + "created_at": "1970-01-01T00:00:00+00:00", + "disabled_by": None, + "entry_type": None, + "hw_version": None, + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "labels": ["blah"], + "manufacturer": None, + "model": None, + "model_id": None, + "modified_at": "1970-01-01T00:00:00+00:00", + "name_by_user": None, + "name": None, + "primary_config_entry": None, + "serial_number": None, + "sw_version": None, + "via_device_id": None, + }, + ], + "deleted_devices": [], + }, + } + + async def test_removing_config_entries( hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: From b7b3094a4980fe5b422fbb7ecd9478713b4abdf7 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Wed, 24 Jul 2024 02:40:54 -0400 Subject: [PATCH 1523/2411] Bump aiorussound to 2.2.0 (#122500) --- homeassistant/components/russound_rio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 4c4f18325d5..be5dd86793f 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/russound_rio", "iot_class": "local_push", "loggers": ["aiorussound"], - "requirements": ["aiorussound==2.0.7"] + "requirements": ["aiorussound==2.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ab198332739..334dc4c87e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.0.7 +aiorussound==2.2.0 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18033e541bf..9a5226d144b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.0.7 +aiorussound==2.2.0 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 From 902bf4ae8656a2929b2ec9a361e9335f708437bb Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 24 Jul 2024 08:44:10 +0200 Subject: [PATCH 1524/2411] Use snapshot in deCONZ button tests (#122505) Use snapshot in button tests --- .../deconz/snapshots/test_button.ambr | 95 +++++++++++++++++++ tests/components/deconz/test_button.py | 60 +++--------- 2 files changed, 109 insertions(+), 46 deletions(-) create mode 100644 tests/components/deconz/snapshots/test_button.ambr diff --git a/tests/components/deconz/snapshots/test_button.ambr b/tests/components/deconz/snapshots/test_button.ambr new file mode 100644 index 00000000000..1ef5248ebc3 --- /dev/null +++ b/tests/components/deconz/snapshots/test_button.ambr @@ -0,0 +1,95 @@ +# serializer version: 1 +# name: test_button[deconz_payload0-expected0][button.light_group_scene_store_current_scene-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.light_group_scene_store_current_scene', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:inbox-arrow-down', + 'original_name': 'Scene Store Current Scene', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01234E56789A/groups/1/scenes/1-store', + 'unit_of_measurement': None, + }) +# --- +# name: test_button[deconz_payload0-expected0][button.light_group_scene_store_current_scene-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Light group Scene Store Current Scene', + 'icon': 'mdi:inbox-arrow-down', + }), + 'context': , + 'entity_id': 'button.light_group_scene_store_current_scene', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_button[deconz_payload1-expected1][button.aqara_fp1_reset_presence-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.aqara_fp1_reset_presence', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Aqara FP1 Reset Presence', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-reset_presence', + 'unit_of_measurement': None, + }) +# --- +# name: test_button[deconz_payload1-expected1][button.aqara_fp1_reset_presence-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'Aqara FP1 Reset Presence', + }), + 'context': , + 'entity_id': 'button.aqara_fp1_reset_presence', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/deconz/test_button.py b/tests/components/deconz/test_button.py index 1ddcbd8f105..8acc3bbb819 100644 --- a/tests/components/deconz/test_button.py +++ b/tests/components/deconz/test_button.py @@ -1,15 +1,19 @@ """deCONZ button platform tests.""" from collections.abc import Callable +from typing import Any +from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, EntityCategory +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er +from tests.common import snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker TEST_DATA = [ @@ -28,15 +32,7 @@ TEST_DATA = [ } }, { - "entity_count": 2, - "device_count": 3, "entity_id": "button.light_group_scene_store_current_scene", - "unique_id": "01234E56789A/groups/1/scenes/1-store", - "entity_category": EntityCategory.CONFIG, - "attributes": { - "icon": "mdi:inbox-arrow-down", - "friendly_name": "Light group Scene Store Current Scene", - }, "request": "/groups/1/scenes/1/store", "request_data": {}, }, @@ -70,15 +66,7 @@ TEST_DATA = [ } }, { - "entity_count": 5, - "device_count": 3, "entity_id": "button.aqara_fp1_reset_presence", - "unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406-reset_presence", - "entity_category": EntityCategory.CONFIG, - "attributes": { - "device_class": "restart", - "friendly_name": "Aqara FP1 Reset Presence", - }, "request": "/sensors/1/config", "request_data": {"resetpresence": True}, }, @@ -90,36 +78,16 @@ TEST_DATA = [ async def test_button( hass: HomeAssistant, entity_registry: er.EntityRegistry, - device_registry: dr.DeviceRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, + config_entry_factory: ConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], - expected, + expected: dict[str, Any], + snapshot: SnapshotAssertion, ) -> None: """Test successful creation of button entities.""" - assert len(hass.states.async_all()) == expected["entity_count"] - - # Verify state data - - button = hass.states.get(expected["entity_id"]) - assert button.attributes == expected["attributes"] - - # Verify entity registry data - - ent_reg_entry = entity_registry.async_get(expected["entity_id"]) - assert ent_reg_entry.entity_category is expected["entity_category"] - assert ent_reg_entry.unique_id == expected["unique_id"] - - # Verify device registry data - - assert ( - len( - dr.async_entries_for_config_entry( - device_registry, config_entry_setup.entry_id - ) - ) - == expected["device_count"] - ) + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.BUTTON]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Verify button press @@ -135,11 +103,11 @@ async def test_button( # Unload entry - await hass.config_entries.async_unload(config_entry_setup.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE # Remove entry - await hass.config_entries.async_remove(config_entry_setup.entry_id) + await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 From 67f7e97b4c0c370f649fe15ce7651b67df8d3c89 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Wed, 24 Jul 2024 16:46:29 +1000 Subject: [PATCH 1525/2411] Bump aiolifx-themes to v0.5.0 (#122503) Signed-off-by: Avi Miller --- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 3d0bd1d73d1..da4eb3296f2 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -50,6 +50,6 @@ "requirements": [ "aiolifx==1.0.5", "aiolifx-effects==0.3.2", - "aiolifx-themes==0.4.15" + "aiolifx-themes==0.5.0" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 334dc4c87e2..2ebe8be165a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -273,7 +273,7 @@ aiokef==0.2.16 aiolifx-effects==0.3.2 # homeassistant.components.lifx -aiolifx-themes==0.4.15 +aiolifx-themes==0.5.0 # homeassistant.components.lifx aiolifx==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a5226d144b..6e91d2334b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -252,7 +252,7 @@ aiokafka==0.10.0 aiolifx-effects==0.3.2 # homeassistant.components.lifx -aiolifx-themes==0.4.15 +aiolifx-themes==0.5.0 # homeassistant.components.lifx aiolifx==1.0.5 From 6dd43be6ace0cce560c92da7c2b89653a1479f63 Mon Sep 17 00:00:00 2001 From: ribbal <30695106+ribbal@users.noreply.github.com> Date: Wed, 24 Jul 2024 07:50:44 +0100 Subject: [PATCH 1526/2411] Fix incorrect enum option in Hive heating sensor (#122496) * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add missing sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * add temperature and mode sensors * Fix defect with Hive heating sensor options --- homeassistant/components/hive/sensor.py | 2 +- homeassistant/components/hive/strings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index 3e594c19058..d51acecc9f6 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -74,7 +74,7 @@ SENSOR_TYPES: tuple[HiveSensorEntityDescription, ...] = ( HiveSensorEntityDescription( key="Heating_Mode", device_class=SensorDeviceClass.ENUM, - options=["schedule", "on", "off"], + options=["schedule", "manual", "off"], translation_key="heating", fn=lambda x: x.lower() if isinstance(x, str) else None, ), diff --git a/homeassistant/components/hive/strings.json b/homeassistant/components/hive/strings.json index c3252238131..bd4e95618e4 100644 --- a/homeassistant/components/hive/strings.json +++ b/homeassistant/components/hive/strings.json @@ -105,7 +105,7 @@ "sensor": { "heating": { "state": { - "on": "[%key:common::state::on%]", + "manual": "Manual", "off": "[%key:common::state::off%]", "schedule": "Schedule" } From 99aa68c93fc536f2037eb30a9b0e468bf1be6a07 Mon Sep 17 00:00:00 2001 From: Jan Stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Wed, 24 Jul 2024 08:53:01 +0200 Subject: [PATCH 1527/2411] Use runtime_data instead of hass.data for Jellyfin (#122410) * Use runtime_data instead of hass.data * Process review --- homeassistant/components/jellyfin/__init__.py | 17 ++++++++--------- .../components/jellyfin/config_flow.py | 16 +++++++--------- .../components/jellyfin/coordinator.py | 3 --- .../components/jellyfin/diagnostics.py | 8 +++----- .../components/jellyfin/media_player.py | 7 +++---- .../components/jellyfin/media_source.py | 9 ++++----- homeassistant/components/jellyfin/sensor.py | 10 ++++------ tests/components/jellyfin/test_init.py | 2 -- 8 files changed, 29 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py index ade030af9dd..0dc51ebd9b3 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -12,11 +12,11 @@ from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, PLATFORMS from .coordinator import JellyfinDataUpdateCoordinator, SessionsDataUpdateCoordinator from .models import JellyfinData +type JellyfinConfigEntry = ConfigEntry[JellyfinData] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) -> bool: """Set up Jellyfin from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - if CONF_CLIENT_DEVICE_ID not in entry.data: entry_data = entry.data.copy() entry_data[CONF_CLIENT_DEVICE_ID] = entry.entry_id @@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for coordinator in coordinators.values(): await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = JellyfinData( + entry.runtime_data = JellyfinData( client_device_id=entry.data[CONF_CLIENT_DEVICE_ID], jellyfin_client=client, coordinators=coordinators, @@ -56,21 +56,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) -> bool: """Unload a config entry.""" unloaded = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unloaded: - data = hass.data[DOMAIN].pop(entry.entry_id) - data.jellyfin_client.stop() + entry.runtime_data.jellyfin_client.stop() return unloaded async def async_remove_config_entry_device( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry + hass: HomeAssistant, config_entry: JellyfinConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove device from a config entry.""" - data = hass.data[DOMAIN][config_entry.entry_id] + data = config_entry.runtime_data coordinator = data.coordinators["sessions"] return not device_entry.identifiers.intersection( diff --git a/homeassistant/components/jellyfin/config_flow.py b/homeassistant/components/jellyfin/config_flow.py index baecbcfb941..7b5426cffde 100644 --- a/homeassistant/components/jellyfin/config_flow.py +++ b/homeassistant/components/jellyfin/config_flow.py @@ -9,15 +9,15 @@ from typing import Any import voluptuous as vol from homeassistant.config_entries import ( - ConfigEntry, ConfigFlow, ConfigFlowResult, - OptionsFlow, + OptionsFlowWithConfigEntry, ) from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import callback from homeassistant.util.uuid import random_uuid_hex +from . import JellyfinConfigEntry from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, SUPPORTED_AUDIO_CODECS @@ -56,7 +56,7 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the Jellyfin config flow.""" self.client_device_id: str | None = None - self.entry: ConfigEntry | None = None + self.entry: JellyfinConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -146,18 +146,16 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + def async_get_options_flow( + config_entry: JellyfinConfigEntry, + ) -> OptionsFlowWithConfigEntry: """Create the options flow.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(OptionsFlow): +class OptionsFlowHandler(OptionsFlowWithConfigEntry): """Handle an option flow for jellyfin.""" - 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 ) -> ConfigFlowResult: diff --git a/homeassistant/components/jellyfin/coordinator.py b/homeassistant/components/jellyfin/coordinator.py index 4d907ac1531..bbd0dfe7496 100644 --- a/homeassistant/components/jellyfin/coordinator.py +++ b/homeassistant/components/jellyfin/coordinator.py @@ -8,7 +8,6 @@ from typing import Any, TypeVar from jellyfin_apiclient_python import JellyfinClient -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -23,8 +22,6 @@ JellyfinDataT = TypeVar( class JellyfinDataUpdateCoordinator(DataUpdateCoordinator[JellyfinDataT], ABC): """Data update coordinator for the Jellyfin integration.""" - config_entry: ConfigEntry - def __init__( self, hass: HomeAssistant, diff --git a/homeassistant/components/jellyfin/diagnostics.py b/homeassistant/components/jellyfin/diagnostics.py index ecc66868bd0..80bbd78c9ad 100644 --- a/homeassistant/components/jellyfin/diagnostics.py +++ b/homeassistant/components/jellyfin/diagnostics.py @@ -5,21 +5,19 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .models import JellyfinData +from . import JellyfinConfigEntry TO_REDACT = {CONF_PASSWORD} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: JellyfinConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data: JellyfinData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data sessions = data.coordinators["sessions"] return { diff --git a/homeassistant/components/jellyfin/media_player.py b/homeassistant/components/jellyfin/media_player.py index 954ac7af69e..d24d15f1dfa 100644 --- a/homeassistant/components/jellyfin/media_player.py +++ b/homeassistant/components/jellyfin/media_player.py @@ -12,27 +12,26 @@ from homeassistant.components.media_player import ( MediaType, ) from homeassistant.components.media_player.browse_media import BrowseMedia -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import parse_datetime +from . import JellyfinConfigEntry from .browse_media import build_item_response, build_root_response from .client_wrapper import get_artwork_url from .const import CONTENT_TYPE_MAP, DOMAIN, LOGGER from .coordinator import JellyfinDataUpdateCoordinator from .entity import JellyfinEntity -from .models import JellyfinData async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: JellyfinConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Jellyfin media_player from a config entry.""" - jellyfin_data: JellyfinData = hass.data[DOMAIN][entry.entry_id] + jellyfin_data = entry.runtime_data coordinator = jellyfin_data.coordinators["sessions"] @callback diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 8901e9e32c0..4b3e8b0146a 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -17,9 +17,9 @@ from homeassistant.components.media_source.models import ( MediaSourceItem, PlayMedia, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from . import JellyfinConfigEntry from .const import ( COLLECTION_TYPE_MOVIES, COLLECTION_TYPE_MUSIC, @@ -48,7 +48,6 @@ from .const import ( PLAYABLE_ITEM_TYPES, SUPPORTED_COLLECTION_TYPES, ) -from .models import JellyfinData _LOGGER = logging.getLogger(__name__) @@ -56,8 +55,8 @@ _LOGGER = logging.getLogger(__name__) async def async_get_media_source(hass: HomeAssistant) -> MediaSource: """Set up Jellyfin media source.""" # Currently only a single Jellyfin server is supported - entry = hass.config_entries.async_entries(DOMAIN)[0] - jellyfin_data: JellyfinData = hass.data[DOMAIN][entry.entry_id] + entry: JellyfinConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] + jellyfin_data = entry.runtime_data return JellyfinSource(hass, jellyfin_data.jellyfin_client, entry) @@ -68,7 +67,7 @@ class JellyfinSource(MediaSource): name: str = "Jellyfin" def __init__( - self, hass: HomeAssistant, client: JellyfinClient, entry: ConfigEntry + self, hass: HomeAssistant, client: JellyfinClient, entry: JellyfinConfigEntry ) -> None: """Initialize the Jellyfin media source.""" super().__init__(DOMAIN) diff --git a/homeassistant/components/jellyfin/sensor.py b/homeassistant/components/jellyfin/sensor.py index 85c7e9e9ee1..3be4ccf2559 100644 --- a/homeassistant/components/jellyfin/sensor.py +++ b/homeassistant/components/jellyfin/sensor.py @@ -6,15 +6,13 @@ from collections.abc import Callable from dataclasses import dataclass from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import DOMAIN +from . import JellyfinConfigEntry from .coordinator import JellyfinDataT from .entity import JellyfinEntity -from .models import JellyfinData @dataclass(frozen=True, kw_only=True) @@ -46,14 +44,14 @@ SENSOR_TYPES: dict[str, JellyfinSensorEntityDescription] = { async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: JellyfinConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Jellyfin sensor based on a config entry.""" - jellyfin_data: JellyfinData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data async_add_entities( - JellyfinSensor(jellyfin_data.coordinators[coordinator_type], description) + JellyfinSensor(data.coordinators[coordinator_type], description) for coordinator_type, description in SENSOR_TYPES.items() ) diff --git a/tests/components/jellyfin/test_init.py b/tests/components/jellyfin/test_init.py index 51d7af2ae94..1af59737296 100644 --- a/tests/components/jellyfin/test_init.py +++ b/tests/components/jellyfin/test_init.py @@ -68,12 +68,10 @@ async def test_load_unload_config_entry( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.entry_id in hass.data[DOMAIN] 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 mock_config_entry.entry_id not in hass.data[DOMAIN] assert mock_config_entry.state is ConfigEntryState.NOT_LOADED From 4c7828fd50a3b4017bf29659188ecc91c8152548 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 24 Jul 2024 09:01:09 +0200 Subject: [PATCH 1528/2411] Improve deCONZ fan tests (#122493) * Improve fan tests * Use snapshots --- .../components/deconz/snapshots/test_fan.ambr | 54 +++++++++ tests/components/deconz/test_fan.py | 106 ++++++------------ 2 files changed, 89 insertions(+), 71 deletions(-) create mode 100644 tests/components/deconz/snapshots/test_fan.ambr diff --git a/tests/components/deconz/snapshots/test_fan.ambr b/tests/components/deconz/snapshots/test_fan.ambr new file mode 100644 index 00000000000..8b7dbba64e4 --- /dev/null +++ b/tests/components/deconz/snapshots/test_fan.ambr @@ -0,0 +1,54 @@ +# serializer version: 1 +# name: test_fans[light_payload0][fan.ceiling_fan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': None, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.ceiling_fan', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Ceiling fan', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:22:a3:00:00:27:8b:81-01', + 'unit_of_measurement': None, + }) +# --- +# name: test_fans[light_payload0][fan.ceiling_fan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ceiling fan', + 'percentage': 100, + 'percentage_step': 1.0, + 'preset_mode': None, + 'preset_modes': None, + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.ceiling_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index 0da48812ea3..cccf894c249 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -1,8 +1,10 @@ """deCONZ fan platform tests.""" from collections.abc import Callable +from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion from homeassistant.components.fan import ( ATTR_PERCENTAGE, @@ -12,11 +14,19 @@ from homeassistant.components.fan import ( SERVICE_TURN_ON, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + Platform, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .conftest import WebsocketDataType +from tests.common import snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker @@ -44,33 +54,25 @@ from tests.test_util.aiohttp import AiohttpClientMocker ) async def test_fans( hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, + config_entry_factory: ConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], light_ws_data: WebsocketDataType, ) -> None: """Test that all supported fan entities are created.""" - assert len(hass.states.async_all()) == 2 # Light and fan - assert hass.states.get("fan.ceiling_fan").state == STATE_ON - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 100 + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.FAN]): + config_entry = await config_entry_factory() + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Test states - await light_ws_data({"state": {"speed": 1}}) - assert hass.states.get("fan.ceiling_fan").state == STATE_ON - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 25 - - await light_ws_data({"state": {"speed": 2}}) - assert hass.states.get("fan.ceiling_fan").state == STATE_ON - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 50 - - await light_ws_data({"state": {"speed": 3}}) - assert hass.states.get("fan.ceiling_fan").state == STATE_ON - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 75 - - await light_ws_data({"state": {"speed": 4}}) - assert hass.states.get("fan.ceiling_fan").state == STATE_ON - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 100 + for speed, percent in (1, 25), (2, 50), (3, 75), (4, 100): + await light_ws_data({"state": {"speed": speed}}) + assert hass.states.get("fan.ceiling_fan").state == STATE_ON + assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == percent await light_ws_data({"state": {"speed": 0}}) assert hass.states.get("fan.ceiling_fan").state == STATE_OFF @@ -110,55 +112,17 @@ async def test_fans( ) assert aioclient_mock.mock_calls[3][2] == {"speed": 1} - # Service set fan percentage to 20% + # Service set fan percentage - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 20}, - blocking=True, - ) - assert aioclient_mock.mock_calls[4][2] == {"speed": 1} - - # Service set fan percentage to 40% - - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 40}, - blocking=True, - ) - assert aioclient_mock.mock_calls[5][2] == {"speed": 2} - - # Service set fan percentage to 60% - - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 60}, - blocking=True, - ) - assert aioclient_mock.mock_calls[6][2] == {"speed": 3} - - # Service set fan percentage to 80% - - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 80}, - blocking=True, - ) - assert aioclient_mock.mock_calls[7][2] == {"speed": 4} - - # Service set fan percentage to 0% does not equal off - - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 0}, - blocking=True, - ) - assert aioclient_mock.mock_calls[8][2] == {"speed": 0} + for percent, speed in (20, 1), (40, 2), (60, 3), (80, 4), (0, 0): + aioclient_mock.mock_calls.clear() + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: percent}, + blocking=True, + ) + assert aioclient_mock.mock_calls[0][2] == {"speed": speed} # Events with an unsupported speed does not get converted @@ -166,13 +130,13 @@ async def test_fans( assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert not hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] - await hass.config_entries.async_unload(config_entry_setup.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) states = hass.states.async_all() - assert len(states) == 2 + assert len(states) == 1 for state in states: assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_remove(config_entry_setup.entry_id) + await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 From a90d41d9e7994bdf20fb8feeef275884ff6de240 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 24 Jul 2024 09:01:34 +0200 Subject: [PATCH 1529/2411] Use snapshot in deCONZ binary sensor tests (#122507) * Use snapshot in deCONZ binary sensor tests * Fix typing in button test --- .../deconz/snapshots/test_binary_sensor.ambr | 1014 +++++++++++++++++ tests/components/deconz/test_binary_sensor.py | 178 +-- tests/components/deconz/test_button.py | 5 +- 3 files changed, 1031 insertions(+), 166 deletions(-) create mode 100644 tests/components/deconz/snapshots/test_binary_sensor.ambr diff --git a/tests/components/deconz/snapshots/test_binary_sensor.ambr b/tests/components/deconz/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..584575c23af --- /dev/null +++ b/tests/components/deconz/snapshots/test_binary_sensor.ambr @@ -0,0 +1,1014 @@ +# serializer version: 1 +# name: test_binary_sensors[sensor_payload0-expected0-config_entry_options0][binary_sensor.alarm_10-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.alarm_10', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Alarm 10', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:b5:d1:80-01-0500-alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload0-expected0-config_entry_options0][binary_sensor.alarm_10-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'safety', + 'friendly_name': 'Alarm 10', + 'on': True, + 'temperature': 26.0, + }), + 'context': , + 'entity_id': 'binary_sensor.alarm_10', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload1-expected1-config_entry_options0][binary_sensor.cave_co-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.cave_co', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cave CO', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:a5:21:24-01-0101-carbon_monoxide', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload1-expected1-config_entry_options0][binary_sensor.cave_co-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_monoxide', + 'friendly_name': 'Cave CO', + 'on': True, + }), + 'context': , + 'entity_id': 'binary_sensor.cave_co', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload1-expected1-config_entry_options0][binary_sensor.cave_co_low_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.cave_co_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cave CO Low Battery', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:a5:21:24-01-0101-low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload1-expected1-config_entry_options0][binary_sensor.cave_co_low_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Cave CO Low Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.cave_co_low_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload1-expected1-config_entry_options0][binary_sensor.cave_co_tampered-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.cave_co_tampered', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cave CO Tampered', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:a5:21:24-01-0101-tampered', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload1-expected1-config_entry_options0][binary_sensor.cave_co_tampered-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Cave CO Tampered', + }), + 'context': , + 'entity_id': 'binary_sensor.cave_co_tampered', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload10-expected10-config_entry_options0][binary_sensor.presence_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.presence_sensor', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Presence sensor', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00-presence', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload10-expected10-config_entry_options0][binary_sensor.presence_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'dark': False, + 'device_class': 'motion', + 'friendly_name': 'Presence sensor', + 'on': True, + 'temperature': 0.1, + }), + 'context': , + 'entity_id': 'binary_sensor.presence_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload10-expected10-config_entry_options0][binary_sensor.presence_sensor_low_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.presence_sensor_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Presence sensor Low Battery', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00-low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload10-expected10-config_entry_options0][binary_sensor.presence_sensor_low_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Presence sensor Low Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.presence_sensor_low_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload10-expected10-config_entry_options0][binary_sensor.presence_sensor_tampered-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.presence_sensor_tampered', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Presence sensor Tampered', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00-tampered', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload10-expected10-config_entry_options0][binary_sensor.presence_sensor_tampered-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Presence sensor Tampered', + }), + 'context': , + 'entity_id': 'binary_sensor.presence_sensor_tampered', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload2-expected2-config_entry_options0][binary_sensor.sensor_kitchen_smoke-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.sensor_kitchen_smoke', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'sensor_kitchen_smoke', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:01:d9:3e:7c-01-0500-fire', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload2-expected2-config_entry_options0][binary_sensor.sensor_kitchen_smoke-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'sensor_kitchen_smoke', + 'on': True, + }), + 'context': , + 'entity_id': 'binary_sensor.sensor_kitchen_smoke', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload2-expected2-config_entry_options0][binary_sensor.sensor_kitchen_smoke_test_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.sensor_kitchen_smoke_test_mode', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'sensor_kitchen_smoke Test Mode', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:01:d9:3e:7c-01-0500-in_test_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload2-expected2-config_entry_options0][binary_sensor.sensor_kitchen_smoke_test_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'sensor_kitchen_smoke Test Mode', + }), + 'context': , + 'entity_id': 'binary_sensor.sensor_kitchen_smoke_test_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload3-expected3-config_entry_options0][binary_sensor.sensor_kitchen_smoke-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.sensor_kitchen_smoke', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'sensor_kitchen_smoke', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:01:d9:3e:7c-01-0500-fire', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload3-expected3-config_entry_options0][binary_sensor.sensor_kitchen_smoke-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'sensor_kitchen_smoke', + 'on': True, + }), + 'context': , + 'entity_id': 'binary_sensor.sensor_kitchen_smoke', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload3-expected3-config_entry_options0][binary_sensor.sensor_kitchen_smoke_test_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.sensor_kitchen_smoke_test_mode', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'sensor_kitchen_smoke Test Mode', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:01:d9:3e:7c-01-0500-in_test_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload3-expected3-config_entry_options0][binary_sensor.sensor_kitchen_smoke_test_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'sensor_kitchen_smoke Test Mode', + }), + 'context': , + 'entity_id': 'binary_sensor.sensor_kitchen_smoke_test_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload4-expected4-config_entry_options0][binary_sensor.kitchen_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.kitchen_switch', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Kitchen Switch', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'kitchen-switch-flag', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload4-expected4-config_entry_options0][binary_sensor.kitchen_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Kitchen Switch', + 'on': True, + }), + 'context': , + 'entity_id': 'binary_sensor.kitchen_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[sensor_payload5-expected5-config_entry_options0][binary_sensor.back_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.back_door', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Back Door', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:2b:96:b4-01-0006-open', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload5-expected5-config_entry_options0][binary_sensor.back_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'opening', + 'friendly_name': 'Back Door', + 'on': True, + 'temperature': 33.0, + }), + 'context': , + 'entity_id': 'binary_sensor.back_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload6-expected6-config_entry_options0][binary_sensor.motion_sensor_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.motion_sensor_4', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion sensor 4', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:17:88:01:03:28:8c:9b-02-0406-presence', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload6-expected6-config_entry_options0][binary_sensor.motion_sensor_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'dark': False, + 'device_class': 'motion', + 'friendly_name': 'Motion sensor 4', + 'on': True, + }), + 'context': , + 'entity_id': 'binary_sensor.motion_sensor_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload7-expected7-config_entry_options0][binary_sensor.water2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.water2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'water2', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:2f:07:db-01-0500-water', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload7-expected7-config_entry_options0][binary_sensor.water2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'moisture', + 'friendly_name': 'water2', + 'on': True, + 'temperature': 25.0, + }), + 'context': , + 'entity_id': 'binary_sensor.water2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload7-expected7-config_entry_options0][binary_sensor.water2_low_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.water2_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'water2 Low Battery', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:2f:07:db-01-0500-low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload7-expected7-config_entry_options0][binary_sensor.water2_low_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'water2 Low Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.water2_low_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload7-expected7-config_entry_options0][binary_sensor.water2_tampered-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.water2_tampered', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'water2 Tampered', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:2f:07:db-01-0500-tampered', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload7-expected7-config_entry_options0][binary_sensor.water2_tampered-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'water2 Tampered', + }), + 'context': , + 'entity_id': 'binary_sensor.water2_tampered', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload8-expected8-config_entry_options0][binary_sensor.vibration_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.vibration_1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Vibration 1', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:a5:21:24-01-0101-vibration', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload8-expected8-config_entry_options0][binary_sensor.vibration_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'vibration', + 'friendly_name': 'Vibration 1', + 'on': True, + 'orientation': list([ + 10, + 1059, + 0, + ]), + 'temperature': 32.0, + 'tiltangle': 83, + 'vibrationstrength': 114, + }), + 'context': , + 'entity_id': 'binary_sensor.vibration_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[sensor_payload9-expected9-config_entry_options0][binary_sensor.presence_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.presence_sensor', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Presence sensor', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00-presence', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload9-expected9-config_entry_options0][binary_sensor.presence_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'dark': False, + 'device_class': 'motion', + 'friendly_name': 'Presence sensor', + 'on': True, + 'temperature': 0.1, + }), + 'context': , + 'entity_id': 'binary_sensor.presence_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload9-expected9-config_entry_options0][binary_sensor.presence_sensor_low_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.presence_sensor_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Presence sensor Low Battery', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00-low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload9-expected9-config_entry_options0][binary_sensor.presence_sensor_low_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Presence sensor Low Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.presence_sensor_low_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[sensor_payload9-expected9-config_entry_options0][binary_sensor.presence_sensor_tampered-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.presence_sensor_tampered', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Presence sensor Tampered', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00-tampered', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[sensor_payload9-expected9-config_entry_options0][binary_sensor.presence_sensor_tampered-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Presence sensor Tampered', + }), + 'context': , + 'entity_id': 'binary_sensor.presence_sensor_tampered', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 18939a816e4..b3e80942981 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -2,10 +2,11 @@ from collections.abc import Callable from typing import Any +from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion -from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_NEW_DEVICES, @@ -14,17 +15,13 @@ from homeassistant.components.deconz.const import ( ) from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, - EntityCategory, -) +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er -from .conftest import WebsocketDataType +from .conftest import ConfigEntryFactoryType, WebsocketDataType + +from tests.common import snapshot_platform TEST_DATA = [ ( # Alarm binary sensor @@ -51,19 +48,7 @@ TEST_DATA = [ "uniqueid": "00:15:8d:00:02:b5:d1:80-01-0500", }, { - "entity_count": 3, - "device_count": 3, "entity_id": "binary_sensor.alarm_10", - "unique_id": "00:15:8d:00:02:b5:d1:80-01-0500-alarm", - "state": STATE_OFF, - "entity_category": None, - "device_class": BinarySensorDeviceClass.SAFETY, - "attributes": { - "on": True, - "temperature": 26.0, - "device_class": "safety", - "friendly_name": "Alarm 10", - }, "websocket_event": {"alarm": True}, "next_state": STATE_ON, }, @@ -92,18 +77,7 @@ TEST_DATA = [ "uniqueid": "00:15:8d:00:02:a5:21:24-01-0101", }, { - "entity_count": 4, - "device_count": 3, "entity_id": "binary_sensor.cave_co", - "unique_id": "00:15:8d:00:02:a5:21:24-01-0101-carbon_monoxide", - "state": STATE_OFF, - "entity_category": None, - "device_class": BinarySensorDeviceClass.CO, - "attributes": { - "on": True, - "device_class": "carbon_monoxide", - "friendly_name": "Cave CO", - }, "websocket_event": {"carbonmonoxide": True}, "next_state": STATE_ON, }, @@ -127,18 +101,7 @@ TEST_DATA = [ "uniqueid": "00:15:8d:00:01:d9:3e:7c-01-0500", }, { - "entity_count": 2, - "device_count": 3, "entity_id": "binary_sensor.sensor_kitchen_smoke", - "unique_id": "00:15:8d:00:01:d9:3e:7c-01-0500-fire", - "state": STATE_OFF, - "entity_category": None, - "device_class": BinarySensorDeviceClass.SMOKE, - "attributes": { - "on": True, - "device_class": "smoke", - "friendly_name": "sensor_kitchen_smoke", - }, "websocket_event": {"fire": True}, "next_state": STATE_ON, }, @@ -163,17 +126,7 @@ TEST_DATA = [ "uniqueid": "00:15:8d:00:01:d9:3e:7c-01-0500", }, { - "entity_count": 2, - "device_count": 3, "entity_id": "binary_sensor.sensor_kitchen_smoke_test_mode", - "unique_id": "00:15:8d:00:01:d9:3e:7c-01-0500-in_test_mode", - "state": STATE_OFF, - "entity_category": EntityCategory.DIAGNOSTIC, - "device_class": BinarySensorDeviceClass.SMOKE, - "attributes": { - "device_class": "smoke", - "friendly_name": "sensor_kitchen_smoke Test Mode", - }, "websocket_event": {"test": True}, "next_state": STATE_ON, }, @@ -195,17 +148,7 @@ TEST_DATA = [ "uniqueid": "kitchen-switch", }, { - "entity_count": 1, - "device_count": 2, "entity_id": "binary_sensor.kitchen_switch", - "unique_id": "kitchen-switch-flag", - "state": STATE_ON, - "entity_category": None, - "device_class": None, - "attributes": { - "on": True, - "friendly_name": "Kitchen Switch", - }, "websocket_event": {"flag": False}, "next_state": STATE_OFF, }, @@ -232,19 +175,7 @@ TEST_DATA = [ "uniqueid": "00:15:8d:00:02:2b:96:b4-01-0006", }, { - "entity_count": 3, - "device_count": 3, "entity_id": "binary_sensor.back_door", - "unique_id": "00:15:8d:00:02:2b:96:b4-01-0006-open", - "state": STATE_OFF, - "entity_category": None, - "device_class": BinarySensorDeviceClass.OPENING, - "attributes": { - "on": True, - "temperature": 33.0, - "device_class": "opening", - "friendly_name": "Back Door", - }, "websocket_event": {"open": True}, "next_state": STATE_ON, }, @@ -278,19 +209,7 @@ TEST_DATA = [ "uniqueid": "00:17:88:01:03:28:8c:9b-02-0406", }, { - "entity_count": 3, - "device_count": 3, "entity_id": "binary_sensor.motion_sensor_4", - "unique_id": "00:17:88:01:03:28:8c:9b-02-0406-presence", - "state": STATE_OFF, - "entity_category": None, - "device_class": BinarySensorDeviceClass.MOTION, - "attributes": { - "on": True, - "dark": False, - "device_class": "motion", - "friendly_name": "Motion sensor 4", - }, "websocket_event": {"presence": True}, "next_state": STATE_ON, }, @@ -319,19 +238,7 @@ TEST_DATA = [ "uniqueid": "00:15:8d:00:02:2f:07:db-01-0500", }, { - "entity_count": 5, - "device_count": 3, "entity_id": "binary_sensor.water2", - "unique_id": "00:15:8d:00:02:2f:07:db-01-0500-water", - "state": STATE_OFF, - "entity_category": None, - "device_class": BinarySensorDeviceClass.MOISTURE, - "attributes": { - "on": True, - "temperature": 25.0, - "device_class": "moisture", - "friendly_name": "water2", - }, "websocket_event": {"water": True}, "next_state": STATE_ON, }, @@ -364,22 +271,7 @@ TEST_DATA = [ "uniqueid": "00:15:8d:00:02:a5:21:24-01-0101", }, { - "entity_count": 3, - "device_count": 3, "entity_id": "binary_sensor.vibration_1", - "unique_id": "00:15:8d:00:02:a5:21:24-01-0101-vibration", - "state": STATE_ON, - "entity_category": None, - "device_class": BinarySensorDeviceClass.VIBRATION, - "attributes": { - "on": True, - "temperature": 32.0, - "orientation": [10, 1059, 0], - "tiltangle": 83, - "vibrationstrength": 114, - "device_class": "vibration", - "friendly_name": "Vibration 1", - }, "websocket_event": {"vibration": False}, "next_state": STATE_OFF, }, @@ -402,17 +294,7 @@ TEST_DATA = [ "uniqueid": "00:00:00:00:00:00:00:00-00", }, { - "entity_count": 4, - "device_count": 3, "entity_id": "binary_sensor.presence_sensor_tampered", - "unique_id": "00:00:00:00:00:00:00:00-00-tampered", - "state": STATE_OFF, - "entity_category": EntityCategory.DIAGNOSTIC, - "device_class": BinarySensorDeviceClass.TAMPER, - "attributes": { - "device_class": "tamper", - "friendly_name": "Presence sensor Tampered", - }, "websocket_event": {"tampered": True}, "next_state": STATE_ON, }, @@ -435,17 +317,7 @@ TEST_DATA = [ "uniqueid": "00:00:00:00:00:00:00:00-00", }, { - "entity_count": 4, - "device_count": 3, "entity_id": "binary_sensor.presence_sensor_low_battery", - "unique_id": "00:00:00:00:00:00:00:00-00-low_battery", - "state": STATE_OFF, - "entity_category": EntityCategory.DIAGNOSTIC, - "device_class": BinarySensorDeviceClass.BATTERY, - "attributes": { - "device_class": "battery", - "friendly_name": "Presence sensor Low Battery", - }, "websocket_event": {"lowbattery": True}, "next_state": STATE_ON, }, @@ -457,38 +329,16 @@ TEST_DATA = [ @pytest.mark.parametrize(("sensor_payload", "expected"), TEST_DATA) async def test_binary_sensors( hass: HomeAssistant, - device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - config_entry_setup: ConfigEntry, + config_entry_factory: ConfigEntryFactoryType, sensor_ws_data: WebsocketDataType, expected: dict[str, Any], + snapshot: SnapshotAssertion, ) -> None: """Test successful creation of binary sensor entities.""" - assert len(hass.states.async_all()) == expected["entity_count"] - - # Verify state data - - sensor = hass.states.get(expected["entity_id"]) - assert sensor.state == expected["state"] - assert sensor.attributes.get(ATTR_DEVICE_CLASS) == expected["device_class"] - assert sensor.attributes == expected["attributes"] - - # Verify entity registry data - - ent_reg_entry = entity_registry.async_get(expected["entity_id"]) - assert ent_reg_entry.entity_category is expected["entity_category"] - assert ent_reg_entry.unique_id == expected["unique_id"] - - # Verify device registry data - - assert ( - len( - dr.async_entries_for_config_entry( - device_registry, config_entry_setup.entry_id - ) - ) - == expected["device_count"] - ) + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.BINARY_SENSOR]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Change state @@ -497,12 +347,12 @@ async def test_binary_sensors( # Unload entry - await hass.config_entries.async_unload(config_entry_setup.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE # Remove entry - await hass.config_entries.async_remove(config_entry_setup.entry_id) + await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_button.py b/tests/components/deconz/test_button.py index 8acc3bbb819..76fcd784634 100644 --- a/tests/components/deconz/test_button.py +++ b/tests/components/deconz/test_button.py @@ -8,11 +8,12 @@ import pytest from syrupy import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from .conftest import ConfigEntryFactoryType + from tests.common import snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker @@ -79,7 +80,7 @@ async def test_button( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_factory: ConfigEntry, + config_entry_factory: ConfigEntryFactoryType, mock_put_request: Callable[[str, str], AiohttpClientMocker], expected: dict[str, Any], snapshot: SnapshotAssertion, From c9f0fe3c5d55bd1306aee167c8aa76f888ce0498 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 24 Jul 2024 11:37:49 +0200 Subject: [PATCH 1530/2411] Rename recorder INTEGRATION_PLATFORMS_LOAD_IN_RECORDER_THREAD (#122511) --- homeassistant/components/recorder/__init__.py | 4 ++-- homeassistant/components/recorder/const.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index f5e72912224..b9ba90caf3f 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -33,7 +33,7 @@ from .const import ( # noqa: F401 DATA_INSTANCE, DOMAIN, INTEGRATION_PLATFORM_COMPILE_STATISTICS, - INTEGRATION_PLATFORMS_LOAD_IN_RECORDER_THREAD, + INTEGRATION_PLATFORMS_RUN_IN_RECORDER_THREAD, SQLITE_URL_PREFIX, SupportedDialect, ) @@ -191,7 +191,7 @@ async def _async_setup_integration_platform( # add it to the recorder queue to be processed. if any( hasattr(platform, _attr) - for _attr in INTEGRATION_PLATFORMS_LOAD_IN_RECORDER_THREAD + for _attr in INTEGRATION_PLATFORMS_RUN_IN_RECORDER_THREAD ): instance.queue_task(AddRecorderPlatformTask(domain, platform)) diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 00121608b4c..31870a5db2d 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -70,7 +70,7 @@ INTEGRATION_PLATFORM_COMPILE_STATISTICS = "compile_statistics" INTEGRATION_PLATFORM_VALIDATE_STATISTICS = "validate_statistics" INTEGRATION_PLATFORM_LIST_STATISTIC_IDS = "list_statistic_ids" -INTEGRATION_PLATFORMS_LOAD_IN_RECORDER_THREAD = { +INTEGRATION_PLATFORMS_RUN_IN_RECORDER_THREAD = { INTEGRATION_PLATFORM_COMPILE_STATISTICS, INTEGRATION_PLATFORM_VALIDATE_STATISTICS, INTEGRATION_PLATFORM_LIST_STATISTIC_IDS, From 9ecdee3b78746b5986f7d5eebbbfd2a0f763d8e3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 24 Jul 2024 13:22:48 +0200 Subject: [PATCH 1531/2411] Extract Evohome base entities to separate module (#122515) * Extract Evohome base entities to separate module * Extract Evohome base entities to separate module --- homeassistant/components/evohome/__init__.py | 207 +---------------- homeassistant/components/evohome/climate.py | 2 +- homeassistant/components/evohome/entity.py | 210 ++++++++++++++++++ .../components/evohome/water_heater.py | 2 +- 4 files changed, 215 insertions(+), 206 deletions(-) create mode 100644 homeassistant/components/evohome/entity.py diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 4cf8561fc3b..2a9a44de717 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -6,7 +6,7 @@ others. from __future__ import annotations -from datetime import datetime, timedelta, timezone +from datetime import timedelta import logging from typing import Any, Final @@ -16,14 +16,8 @@ import evohomeasync2 as evo from evohomeasync2.schema.const import ( SZ_AUTO_WITH_RESET, SZ_CAN_BE_TEMPORARY, - SZ_HEAT_SETPOINT, - SZ_SETPOINT_STATUS, - SZ_STATE_STATUS, SZ_SYSTEM_MODE, - SZ_SYSTEM_MODE_STATUS, - SZ_TIME_UNTIL, SZ_TIMING_MODE, - SZ_UNTIL, ) import voluptuous as vol @@ -39,11 +33,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType @@ -69,13 +59,7 @@ from .const import ( EvoService, ) from .coordinator import EvoBroker -from .helpers import ( - convert_dict, - convert_until, - dt_aware_to_naive, - dt_local_to_aware, - handle_evo_exception, -) +from .helpers import dt_aware_to_naive, dt_local_to_aware, handle_evo_exception _LOGGER = logging.getLogger(__name__) @@ -396,188 +380,3 @@ def setup_service_functions(hass: HomeAssistant, broker: EvoBroker) -> None: set_zone_override, schema=SET_ZONE_OVERRIDE_SCHEMA, ) - - -class EvoDevice(Entity): - """Base for any evohome device. - - This includes the Controller, (up to 12) Heating Zones and (optionally) a - DHW controller. - """ - - _attr_should_poll = False - - def __init__( - self, - evo_broker: EvoBroker, - evo_device: evo.ControlSystem | evo.HotWater | evo.Zone, - ) -> None: - """Initialize the evohome entity.""" - self._evo_device = evo_device - self._evo_broker = evo_broker - self._evo_tcs = evo_broker.tcs - - self._device_state_attrs: dict[str, Any] = {} - - async def async_refresh(self, payload: dict | None = None) -> None: - """Process any signals.""" - if payload is None: - self.async_schedule_update_ha_state(force_refresh=True) - return - if payload["unique_id"] != self._attr_unique_id: - return - if payload["service"] in ( - EvoService.SET_ZONE_OVERRIDE, - EvoService.RESET_ZONE_OVERRIDE, - ): - await self.async_zone_svc_request(payload["service"], payload["data"]) - return - await self.async_tcs_svc_request(payload["service"], payload["data"]) - - 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: str, data: dict[str, Any]) -> None: - """Process a service request (setpoint override) for a zone.""" - raise NotImplementedError - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the evohome-specific state attributes.""" - status = self._device_state_attrs - if SZ_SYSTEM_MODE_STATUS in status: - convert_until(status[SZ_SYSTEM_MODE_STATUS], SZ_TIME_UNTIL) - if SZ_SETPOINT_STATUS in status: - convert_until(status[SZ_SETPOINT_STATUS], SZ_UNTIL) - if SZ_STATE_STATUS in status: - convert_until(status[SZ_STATE_STATUS], SZ_UNTIL) - - return {"status": convert_dict(status)} - - 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) - - -class EvoChild(EvoDevice): - """Base for any evohome child. - - This includes (up to 12) Heating Zones and (optionally) a DHW controller. - """ - - _evo_id: str # mypy hint - - def __init__( - self, evo_broker: EvoBroker, evo_device: evo.HotWater | evo.Zone - ) -> None: - """Initialize a evohome Controller (hub).""" - super().__init__(evo_broker, evo_device) - - self._schedule: dict[str, Any] = {} - self._setpoints: dict[str, Any] = {} - - @property - def current_temperature(self) -> float | None: - """Return the current temperature of a Zone.""" - - assert isinstance(self._evo_device, evo.HotWater | evo.Zone) # mypy check - - if (temp := self._evo_broker.temps.get(self._evo_id)) is not None: - # use high-precision temps if available - return temp - return self._evo_device.temperature - - @property - def setpoints(self) -> dict[str, Any]: - """Return the current/next setpoints from the schedule. - - Only Zones & DHW controllers (but not the TCS) can have schedules. - """ - - def _dt_evo_to_aware(dt_naive: datetime, utc_offset: timedelta) -> datetime: - dt_aware = dt_naive.replace(tzinfo=dt_util.UTC) - utc_offset - return dt_util.as_local(dt_aware) - - if not (schedule := self._schedule.get("DailySchedules")): - return {} # no scheduled setpoints when {'DailySchedules': []} - - # get dt in the same TZ as the TCS location, so we can compare schedule times - day_time = dt_util.now().astimezone(timezone(self._evo_broker.loc_utc_offset)) - day_of_week = day_time.weekday() # for evohome, 0 is Monday - time_of_day = day_time.strftime("%H:%M:%S") - - try: - # Iterate today's switchpoints until past the current time of day... - day = schedule[day_of_week] - sp_idx = -1 # last switchpoint of the day before - for i, tmp in enumerate(day["Switchpoints"]): - if time_of_day > tmp["TimeOfDay"]: - sp_idx = i # current setpoint - else: - break - - # Did this setpoint start yesterday? Does the next setpoint start tomorrow? - this_sp_day = -1 if sp_idx == -1 else 0 - next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 - - for key, offset, idx in ( - ("this", this_sp_day, sp_idx), - ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), - ): - sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") - day = schedule[(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 is not None # mypy check - dt_aware = _dt_evo_to_aware( - switchpoint_time_of_day, self._evo_broker.loc_utc_offset - ) - - self._setpoints[f"{key}_sp_from"] = dt_aware.isoformat() - try: - self._setpoints[f"{key}_sp_temp"] = switchpoint[SZ_HEAT_SETPOINT] - except KeyError: - self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"] - - except IndexError: - self._setpoints = {} - _LOGGER.warning( - "Failed to get setpoints, report as an issue if this error persists", - exc_info=True, - ) - - return self._setpoints - - async def _update_schedule(self) -> None: - """Get the latest schedule, if any.""" - - assert isinstance(self._evo_device, evo.HotWater | evo.Zone) # mypy check - - try: - schedule = await self._evo_broker.call_client_api( - self._evo_device.get_schedule(), update_state=False - ) - except evo.InvalidSchedule as err: - _LOGGER.warning( - "%s: Unable to retrieve a valid schedule: %s", - self._evo_device, - err, - ) - self._schedule = {} - else: - self._schedule = schedule or {} - - _LOGGER.debug("Schedule['%s'] = %s", self.name, self._schedule) - - 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") - 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/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 42ffe84121e..07601474062 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -36,7 +36,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -from . import EvoChild, EvoDevice from .const import ( ATTR_DURATION_DAYS, ATTR_DURATION_HOURS, @@ -56,6 +55,7 @@ from .const import ( EVO_TEMPOVER, EvoService, ) +from .entity import EvoChild, EvoDevice if TYPE_CHECKING: from . import EvoBroker diff --git a/homeassistant/components/evohome/entity.py b/homeassistant/components/evohome/entity.py new file mode 100644 index 00000000000..4f85791572c --- /dev/null +++ b/homeassistant/components/evohome/entity.py @@ -0,0 +1,210 @@ +"""Base for evohome entity.""" + +from datetime import datetime, timedelta, timezone +import logging +from typing import Any + +import evohomeasync2 as evo +from evohomeasync2.schema.const import ( + SZ_HEAT_SETPOINT, + SZ_SETPOINT_STATUS, + SZ_STATE_STATUS, + SZ_SYSTEM_MODE_STATUS, + SZ_TIME_UNTIL, + SZ_UNTIL, +) + +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util + +from . import EvoBroker, EvoService +from .const import DOMAIN +from .helpers import convert_dict, convert_until + +_LOGGER = logging.getLogger(__name__) + + +class EvoDevice(Entity): + """Base for any evohome device. + + This includes the Controller, (up to 12) Heating Zones and (optionally) a + DHW controller. + """ + + _attr_should_poll = False + + def __init__( + self, + evo_broker: EvoBroker, + evo_device: evo.ControlSystem | evo.HotWater | evo.Zone, + ) -> None: + """Initialize the evohome entity.""" + self._evo_device = evo_device + self._evo_broker = evo_broker + self._evo_tcs = evo_broker.tcs + + self._device_state_attrs: dict[str, Any] = {} + + async def async_refresh(self, payload: dict | None = None) -> None: + """Process any signals.""" + if payload is None: + self.async_schedule_update_ha_state(force_refresh=True) + return + if payload["unique_id"] != self._attr_unique_id: + return + if payload["service"] in ( + EvoService.SET_ZONE_OVERRIDE, + EvoService.RESET_ZONE_OVERRIDE, + ): + await self.async_zone_svc_request(payload["service"], payload["data"]) + return + await self.async_tcs_svc_request(payload["service"], payload["data"]) + + 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: str, data: dict[str, Any]) -> None: + """Process a service request (setpoint override) for a zone.""" + raise NotImplementedError + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the evohome-specific state attributes.""" + status = self._device_state_attrs + if SZ_SYSTEM_MODE_STATUS in status: + convert_until(status[SZ_SYSTEM_MODE_STATUS], SZ_TIME_UNTIL) + if SZ_SETPOINT_STATUS in status: + convert_until(status[SZ_SETPOINT_STATUS], SZ_UNTIL) + if SZ_STATE_STATUS in status: + convert_until(status[SZ_STATE_STATUS], SZ_UNTIL) + + return {"status": convert_dict(status)} + + 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) + + +class EvoChild(EvoDevice): + """Base for any evohome child. + + This includes (up to 12) Heating Zones and (optionally) a DHW controller. + """ + + _evo_id: str # mypy hint + + def __init__( + self, evo_broker: EvoBroker, evo_device: evo.HotWater | evo.Zone + ) -> None: + """Initialize a evohome Controller (hub).""" + super().__init__(evo_broker, evo_device) + + self._schedule: dict[str, Any] = {} + self._setpoints: dict[str, Any] = {} + + @property + def current_temperature(self) -> float | None: + """Return the current temperature of a Zone.""" + + assert isinstance(self._evo_device, evo.HotWater | evo.Zone) # mypy check + + if (temp := self._evo_broker.temps.get(self._evo_id)) is not None: + # use high-precision temps if available + return temp + return self._evo_device.temperature + + @property + def setpoints(self) -> dict[str, Any]: + """Return the current/next setpoints from the schedule. + + Only Zones & DHW controllers (but not the TCS) can have schedules. + """ + + def _dt_evo_to_aware(dt_naive: datetime, utc_offset: timedelta) -> datetime: + dt_aware = dt_naive.replace(tzinfo=dt_util.UTC) - utc_offset + return dt_util.as_local(dt_aware) + + if not (schedule := self._schedule.get("DailySchedules")): + return {} # no scheduled setpoints when {'DailySchedules': []} + + # get dt in the same TZ as the TCS location, so we can compare schedule times + day_time = dt_util.now().astimezone(timezone(self._evo_broker.loc_utc_offset)) + day_of_week = day_time.weekday() # for evohome, 0 is Monday + time_of_day = day_time.strftime("%H:%M:%S") + + try: + # Iterate today's switchpoints until past the current time of day... + day = schedule[day_of_week] + sp_idx = -1 # last switchpoint of the day before + for i, tmp in enumerate(day["Switchpoints"]): + if time_of_day > tmp["TimeOfDay"]: + sp_idx = i # current setpoint + else: + break + + # Did this setpoint start yesterday? Does the next setpoint start tomorrow? + this_sp_day = -1 if sp_idx == -1 else 0 + next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 + + for key, offset, idx in ( + ("this", this_sp_day, sp_idx), + ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), + ): + sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") + day = schedule[(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 is not None # mypy check + dt_aware = _dt_evo_to_aware( + switchpoint_time_of_day, self._evo_broker.loc_utc_offset + ) + + self._setpoints[f"{key}_sp_from"] = dt_aware.isoformat() + try: + self._setpoints[f"{key}_sp_temp"] = switchpoint[SZ_HEAT_SETPOINT] + except KeyError: + self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"] + + except IndexError: + self._setpoints = {} + _LOGGER.warning( + "Failed to get setpoints, report as an issue if this error persists", + exc_info=True, + ) + + return self._setpoints + + async def _update_schedule(self) -> None: + """Get the latest schedule, if any.""" + + assert isinstance(self._evo_device, evo.HotWater | evo.Zone) # mypy check + + try: + schedule = await self._evo_broker.call_client_api( + self._evo_device.get_schedule(), update_state=False + ) + except evo.InvalidSchedule as err: + _LOGGER.warning( + "%s: Unable to retrieve a valid schedule: %s", + self._evo_device, + err, + ) + self._schedule = {} + else: + self._schedule = schedule or {} + + _LOGGER.debug("Schedule['%s'] = %s", self.name, self._schedule) + + 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") + 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/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 66ba7f46a70..abf3e2f3926 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -31,8 +31,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -from . import EvoChild from .const import DOMAIN, EVO_FOLLOW, EVO_PERMOVER +from .entity import EvoChild if TYPE_CHECKING: from . import EvoBroker From cae992f5e61da3a14a8a10b47955b96f69b25170 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Wed, 24 Jul 2024 08:44:44 -0400 Subject: [PATCH 1532/2411] Add volume step to Russound media player (#122523) * Add volume step to Russound media player * Add return types --- .../components/russound_rio/media_player.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 89ef68346c2..1489f12e59c 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -116,6 +116,7 @@ class RussoundZoneDevice(MediaPlayerEntity): _attr_has_entity_name = True _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.SELECT_SOURCE @@ -211,21 +212,29 @@ class RussoundZoneDevice(MediaPlayerEntity): async def async_turn_off(self) -> None: """Turn off the zone.""" - await self._zone.send_event("ZoneOff") + await self._zone.zone_off() async def async_turn_on(self) -> None: """Turn on the zone.""" - await self._zone.send_event("ZoneOn") + await self._zone.zone_on() async def async_set_volume_level(self, volume: float) -> None: """Set the volume level.""" rvol = int(volume * 50.0) - await self._zone.send_event("KeyPress", "Volume", rvol) + await self._zone.set_volume(rvol) async def async_select_source(self, source: str) -> None: """Select the source input for this zone.""" for source_id, src in self._sources.items(): if src.name.lower() != source.lower(): continue - await self._zone.send_event("SelectSource", source_id) + await self._zone.select_source(source_id) break + + async def async_volume_up(self) -> None: + """Step the volume up.""" + await self._zone.volume_up() + + async def async_volume_down(self) -> None: + """Step the volume down.""" + await self._zone.volume_down() From c81b9d624bd0f7099cef4cf6af24ccc72a7d93a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Jul 2024 09:23:42 -0500 Subject: [PATCH 1533/2411] Convert oralb to use entry.runtime_data (#122527) --- homeassistant/components/oralb/__init__.py | 45 ++++++++++------------ homeassistant/components/oralb/sensor.py | 10 ++--- 2 files changed, 23 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/oralb/__init__.py b/homeassistant/components/oralb/__init__.py index 71bcb2f2deb..a274c5414d2 100644 --- a/homeassistant/components/oralb/__init__.py +++ b/homeassistant/components/oralb/__init__.py @@ -18,14 +18,15 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import CoreState, HomeAssistant -from .const import DOMAIN - PLATFORMS: list[Platform] = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +type OralBConfigEntry = ConfigEntry[ActiveBluetoothProcessorCoordinator] + + +async def async_setup_entry(hass: HomeAssistant, entry: OralBConfigEntry) -> bool: """Set up OralB BLE device from a config entry.""" address = entry.unique_id assert address is not None @@ -65,31 +66,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return await data.async_poll(connectable_device) - coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( - ActiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=data.update, - needs_poll_method=_needs_poll, - poll_method=_async_poll, - # We will take advertisements from non-connectable devices - # since we will trade the BLEDevice for a connectable one - # if we need to poll it - connectable=False, - ) + coordinator = entry.runtime_data = ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + needs_poll_method=_needs_poll, + poll_method=_async_poll, + # We will take advertisements from non-connectable devices + # since we will trade the BLEDevice for a connectable one + # if we need to poll it + connectable=False, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload( - coordinator.async_start() - ) # only start after all platforms have had a chance to subscribe + # only start after all platforms have had a chance to subscribe + entry.async_on_unload(coordinator.async_start()) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: OralBConfigEntry) -> 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 + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/oralb/sensor.py b/homeassistant/components/oralb/sensor.py index 328a2a1f98a..9994bfc6443 100644 --- a/homeassistant/components/oralb/sensor.py +++ b/homeassistant/components/oralb/sensor.py @@ -4,11 +4,9 @@ from __future__ import annotations from oralb_ble import OralBSensor, SensorUpdate -from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( @@ -27,7 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info -from .const import DOMAIN +from . import OralBConfigEntry from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { @@ -109,13 +107,11 @@ def sensor_update_to_bluetooth_data_update( async def async_setup_entry( hass: HomeAssistant, - entry: config_entries.ConfigEntry, + entry: OralBConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the OralB BLE sensors.""" - coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ - entry.entry_id - ] + coordinator = entry.runtime_data processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( From 6393f1f02d3109a09d30d5178677b6b1a12c24b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Jul 2024 09:52:14 -0500 Subject: [PATCH 1534/2411] Convert rainmachine to use entry.runtime_data (#122532) --- .../components/rainmachine/__init__.py | 41 +++++++++++-------- .../components/rainmachine/binary_sensor.py | 27 ++++++------ .../components/rainmachine/button.py | 12 +++--- .../components/rainmachine/coordinator.py | 10 +++-- .../components/rainmachine/diagnostics.py | 9 ++-- .../components/rainmachine/select.py | 10 +++-- .../components/rainmachine/sensor.py | 10 +++-- .../components/rainmachine/switch.py | 14 +++++-- .../components/rainmachine/update.py | 12 +++--- 9 files changed, 80 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 0891d22b641..cfbc95cf009 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from datetime import timedelta from functools import partial, wraps -from typing import Any +from typing import Any, cast from regenmaschine import Client from regenmaschine.controller import Controller @@ -169,9 +169,12 @@ COORDINATOR_UPDATE_INTERVAL_MAP = { } +type RainMachineConfigEntry = ConfigEntry[RainMachineData] + + @dataclass class RainMachineData: - """Define an object to be stored in `hass.data`.""" + """Define an object to be stored in `entry.runtime_data`.""" controller: Controller coordinators: dict[str, RainMachineDataUpdateCoordinator] @@ -180,7 +183,7 @@ class RainMachineData: @callback def async_get_entry_for_service_call( hass: HomeAssistant, call: ServiceCall -) -> ConfigEntry: +) -> RainMachineConfigEntry: """Get the controller related to a service call (by device ID).""" device_id = call.data[CONF_DEVICE_ID] device_registry = dr.async_get(hass) @@ -192,20 +195,20 @@ def async_get_entry_for_service_call( if (entry := hass.config_entries.async_get_entry(entry_id)) is None: continue if entry.domain == DOMAIN: - return entry + return cast(RainMachineConfigEntry, entry) raise ValueError(f"No controller for device ID: {device_id}") async def async_update_programs_and_zones( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: RainMachineConfigEntry ) -> None: """Update program and zone DataUpdateCoordinators. Program and zone updates always go together because of how linked they are: programs affect zones and certain combinations of zones affect programs. """ - data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data # No gather here to allow http keep-alive to reuse # the connection for each coordinator. await data.coordinators[DATA_PROGRAMS].async_refresh() @@ -213,7 +216,7 @@ async def async_update_programs_and_zones( async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: RainMachineConfigEntry ) -> bool: """Set up RainMachine as config entry.""" websession = aiohttp_client.async_get_clientsession(hass) @@ -315,8 +318,7 @@ async def async_setup_entry( # noqa: C901 # connection for each coordinator. await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = RainMachineData( + entry.runtime_data = RainMachineData( controller=controller, coordinators=coordinators ) @@ -341,7 +343,7 @@ async def async_setup_entry( # noqa: C901 async def wrapper(call: ServiceCall) -> None: """Wrap the service function.""" entry = async_get_entry_for_service_call(hass, call) - data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data try: await func(call, data.controller) @@ -461,16 +463,15 @@ async def async_setup_entry( # noqa: C901 return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: RainMachineConfigEntry +) -> bool: """Unload an RainMachine config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - loaded_entries = [ entry for entry in hass.config_entries.async_entries(DOMAIN) - if entry.state == ConfigEntryState.LOADED + if entry.state is ConfigEntryState.LOADED ] if len(loaded_entries) == 1: # If this is the last loaded instance of RainMachine, deregister any services @@ -489,7 +490,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_migrate_entry( + hass: HomeAssistant, entry: RainMachineConfigEntry +) -> bool: """Migrate an old config entry.""" version = entry.version @@ -521,7 +524,9 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_reload_entry( + hass: HomeAssistant, entry: RainMachineConfigEntry +) -> None: """Handle an options update.""" await hass.config_entries.async_reload(entry.entry_id) @@ -533,7 +538,7 @@ class RainMachineEntity(CoordinatorEntity[RainMachineDataUpdateCoordinator]): def __init__( self, - entry: ConfigEntry, + entry: RainMachineConfigEntry, data: RainMachineData, description: RainMachineEntityDescription, ) -> None: diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 866cddbabbd..574f458ec47 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -7,13 +7,12 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RainMachineData, RainMachineEntity -from .const import DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_CURRENT, DOMAIN +from . import RainMachineConfigEntry, RainMachineEntity +from .const import DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_CURRENT from .model import RainMachineEntityDescription from .util import ( EntityDomainReplacementStrategy, @@ -93,10 +92,12 @@ BINARY_SENSOR_DESCRIPTIONS = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: RainMachineConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up RainMachine binary sensors based on a config entry.""" - data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data async_finish_entity_domain_replacements( hass, @@ -125,15 +126,13 @@ async def async_setup_entry( } async_add_entities( - [ - api_category_sensor_map[description.api_category](entry, data, description) - for description in BINARY_SENSOR_DESCRIPTIONS - if ( - (coordinator := data.coordinators[description.api_category]) is not None - and coordinator.data - and key_exists(coordinator.data, description.data_key) - ) - ] + api_category_sensor_map[description.api_category](entry, data, description) + for description in BINARY_SENSOR_DESCRIPTIONS + if ( + (coordinator := data.coordinators[description.api_category]) is not None + and coordinator.data + and key_exists(coordinator.data, description.data_key) + ) ) diff --git a/homeassistant/components/rainmachine/button.py b/homeassistant/components/rainmachine/button.py index 24486a34b88..7087e5e5b8e 100644 --- a/homeassistant/components/rainmachine/button.py +++ b/homeassistant/components/rainmachine/button.py @@ -13,15 +13,14 @@ from homeassistant.components.button import ( ButtonEntity, ButtonEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RainMachineData, RainMachineEntity -from .const import DATA_PROVISION_SETTINGS, DOMAIN +from . import RainMachineConfigEntry, RainMachineEntity +from .const import DATA_PROVISION_SETTINGS from .model import RainMachineEntityDescription @@ -52,11 +51,12 @@ BUTTON_DESCRIPTIONS = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: RainMachineConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up RainMachine buttons based on a config entry.""" - data: RainMachineData = hass.data[DOMAIN][entry.entry_id] - + data = entry.runtime_data async_add_entities( RainMachineButton(entry, data, description) for description in BUTTON_DESCRIPTIONS diff --git a/homeassistant/components/rainmachine/coordinator.py b/homeassistant/components/rainmachine/coordinator.py index 620bdb2da9b..df7972ef31d 100644 --- a/homeassistant/components/rainmachine/coordinator.py +++ b/homeassistant/components/rainmachine/coordinator.py @@ -4,9 +4,8 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from datetime import timedelta -from typing import Any +from typing import TYPE_CHECKING, Any -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -19,17 +18,20 @@ from .const import LOGGER SIGNAL_REBOOT_COMPLETED = "rainmachine_reboot_completed_{0}" SIGNAL_REBOOT_REQUESTED = "rainmachine_reboot_requested_{0}" +if TYPE_CHECKING: + from . import RainMachineConfigEntry + class RainMachineDataUpdateCoordinator(DataUpdateCoordinator[dict]): """Define an extended DataUpdateCoordinator.""" - config_entry: ConfigEntry + config_entry: RainMachineConfigEntry def __init__( self, hass: HomeAssistant, *, - entry: ConfigEntry, + entry: RainMachineConfigEntry, name: str, api_category: str, update_interval: timedelta, diff --git a/homeassistant/components/rainmachine/diagnostics.py b/homeassistant/components/rainmachine/diagnostics.py index 5564ee693a4..598b8aefa5f 100644 --- a/homeassistant/components/rainmachine/diagnostics.py +++ b/homeassistant/components/rainmachine/diagnostics.py @@ -7,7 +7,6 @@ from typing import Any from regenmaschine.errors import RainMachineError from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ELEVATION, CONF_LATITUDE, @@ -17,8 +16,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from . import RainMachineData -from .const import DOMAIN, LOGGER +from . import RainMachineConfigEntry +from .const import LOGGER CONF_STATION_ID = "stationID" CONF_STATION_NAME = "stationName" @@ -40,10 +39,10 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: RainMachineConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data try: controller_diagnostics = await data.controller.diagnostics.current() diff --git a/homeassistant/components/rainmachine/select.py b/homeassistant/components/rainmachine/select.py index bb622330897..73de33cc8ed 100644 --- a/homeassistant/components/rainmachine/select.py +++ b/homeassistant/components/rainmachine/select.py @@ -14,8 +14,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM, UnitSystem -from . import RainMachineData, RainMachineEntity -from .const import DATA_RESTRICTIONS_UNIVERSAL, DOMAIN +from . import RainMachineConfigEntry, RainMachineData, RainMachineEntity +from .const import DATA_RESTRICTIONS_UNIVERSAL from .model import RainMachineEntityDescription from .util import key_exists @@ -81,10 +81,12 @@ SELECT_DESCRIPTIONS = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: RainMachineConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up RainMachine selects based on a config entry.""" - data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data entity_map = { TYPE_FREEZE_PROTECTION_TEMPERATURE: FreezeProtectionTemperatureSelect, diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 15188e86963..5363000a8ac 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -20,8 +20,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utc_from_timestamp, utcnow -from . import RainMachineData, RainMachineEntity -from .const import DATA_PROGRAMS, DATA_PROVISION_SETTINGS, DATA_ZONES, DOMAIN +from . import RainMachineConfigEntry, RainMachineData, RainMachineEntity +from .const import DATA_PROGRAMS, DATA_PROVISION_SETTINGS, DATA_ZONES from .model import RainMachineEntityDescription from .util import ( RUN_STATE_MAP, @@ -151,10 +151,12 @@ SENSOR_DESCRIPTIONS = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: RainMachineConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up RainMachine sensors based on a config entry.""" - data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data async_finish_entity_domain_replacements( hass, diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 667e609e11c..d4c0064219e 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -20,7 +20,12 @@ from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import VolDictType -from . import RainMachineData, RainMachineEntity, async_update_programs_and_zones +from . import ( + RainMachineConfigEntry, + RainMachineData, + RainMachineEntity, + async_update_programs_and_zones, +) from .const import ( CONF_ALLOW_INACTIVE_ZONES_TO_RUN, CONF_DEFAULT_ZONE_RUN_TIME, @@ -31,7 +36,6 @@ from .const import ( DATA_RESTRICTIONS_UNIVERSAL, DATA_ZONES, DEFAULT_ZONE_RUN, - DOMAIN, ) from .model import RainMachineEntityDescription from .util import RUN_STATE_MAP, key_exists @@ -173,7 +177,9 @@ RESTRICTIONS_SWITCH_DESCRIPTIONS = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: RainMachineConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up RainMachine switches based on a config entry.""" platform = entity_platform.async_get_current_platform() @@ -195,7 +201,7 @@ async def async_setup_entry( schema_dict = cast(VolDictType, schema) platform.async_register_entity_service(service_name, schema_dict, method) - data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data entities: list[RainMachineBaseSwitch] = [] for kind, api_category, switch_class, switch_enabled_class in ( diff --git a/homeassistant/components/rainmachine/update.py b/homeassistant/components/rainmachine/update.py index 38bf74effa0..a7c11061718 100644 --- a/homeassistant/components/rainmachine/update.py +++ b/homeassistant/components/rainmachine/update.py @@ -12,13 +12,12 @@ from homeassistant.components.update import ( UpdateEntity, UpdateEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RainMachineData, RainMachineEntity -from .const import DATA_MACHINE_FIRMWARE_UPDATE_STATUS, DOMAIN +from . import RainMachineConfigEntry, RainMachineEntity +from .const import DATA_MACHINE_FIRMWARE_UPDATE_STATUS from .model import RainMachineEntityDescription @@ -50,11 +49,12 @@ UPDATE_DESCRIPTION = RainMachineEntityDescription( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: RainMachineConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Rainmachine update based on a config entry.""" - data: RainMachineData = hass.data[DOMAIN][entry.entry_id] - + data = entry.runtime_data async_add_entities([RainMachineUpdateEntity(entry, data, UPDATE_DESCRIPTION)]) From c5f9ff6ac5c299d13b1f6f85f4e8a2eec487dc07 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 24 Jul 2024 17:14:40 +0200 Subject: [PATCH 1535/2411] Use snapshot in deCONZ cover tests (#122537) --- .../deconz/snapshots/test_cover.ambr | 150 ++++++++++++++++++ tests/components/deconz/test_cover.py | 61 +++---- 2 files changed, 181 insertions(+), 30 deletions(-) create mode 100644 tests/components/deconz/snapshots/test_cover.ambr diff --git a/tests/components/deconz/snapshots/test_cover.ambr b/tests/components/deconz/snapshots/test_cover.ambr new file mode 100644 index 00000000000..5c50923453c --- /dev/null +++ b/tests/components/deconz/snapshots/test_cover.ambr @@ -0,0 +1,150 @@ +# serializer version: 1 +# name: test_cover[light_payload0][cover.window_covering_device-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.window_covering_device', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Window covering device', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_cover[light_payload0][cover.window_covering_device-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 0, + 'device_class': 'shade', + 'friendly_name': 'Window covering device', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.window_covering_device', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'closed', + }) +# --- +# name: test_level_controllable_output_cover[light_payload0][cover.vent-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.vent', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Vent', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:22:a3:00:00:00:00:00-01', + 'unit_of_measurement': None, + }) +# --- +# name: test_level_controllable_output_cover[light_payload0][cover.vent-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 5, + 'current_tilt_position': 97, + 'device_class': 'damper', + 'friendly_name': 'Vent', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.vent', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- +# name: test_tilt_cover[light_payload0][cover.covering_device-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.covering_device', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Covering device', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:24:46:00:00:12:34:56-01', + 'unit_of_measurement': None, + }) +# --- +# name: test_tilt_cover[light_payload0][cover.covering_device-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 100, + 'current_tilt_position': 100, + 'device_class': 'shade', + 'friendly_name': 'Covering device', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.covering_device', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index d04fb43a0d7..a93d40b4d1e 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -1,12 +1,13 @@ """deCONZ cover platform tests.""" from collections.abc import Callable +from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, - ATTR_CURRENT_TILT_POSITION, ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN as COVER_DOMAIN, @@ -19,17 +20,13 @@ from homeassistant.components.cover import ( SERVICE_STOP_COVER, SERVICE_STOP_COVER_TILT, ) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ENTITY_ID, - STATE_CLOSED, - STATE_OPEN, - STATE_UNAVAILABLE, -) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OPEN, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er -from .conftest import WebsocketDataType +from .conftest import ConfigEntryFactoryType, WebsocketDataType +from tests.common import snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker @@ -55,16 +52,16 @@ from tests.test_util.aiohttp import AiohttpClientMocker ) async def test_cover( hass: HomeAssistant, - config_entry_setup: ConfigEntry, + entity_registry: er.EntityRegistry, + config_entry_factory: ConfigEntryFactoryType, mock_put_request: Callable[[str, str], AiohttpClientMocker], light_ws_data: WebsocketDataType, + snapshot: SnapshotAssertion, ) -> None: """Test that all supported cover entities are created.""" - 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") + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.COVER]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Event signals cover is open @@ -117,14 +114,14 @@ async def test_cover( ) assert aioclient_mock.mock_calls[4][2] == {"stop": True} - await hass.config_entries.async_unload(config_entry_setup.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) states = hass.states.async_all() - assert len(states) == 2 + assert len(states) == 1 for state in states: assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_remove(config_entry_setup.entry_id) + await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 @@ -153,15 +150,17 @@ async def test_cover( } ], ) -@pytest.mark.usefixtures("config_entry_setup") async def test_tilt_cover( - hass: HomeAssistant, mock_put_request: Callable[[str, str], AiohttpClientMocker] + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory: ConfigEntryFactoryType, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + snapshot: SnapshotAssertion, ) -> None: """Test that tilting a cover works.""" - assert len(hass.states.async_all()) == 1 - covering_device = hass.states.get("cover.covering_device") - assert covering_device.state == STATE_OPEN - assert covering_device.attributes[ATTR_CURRENT_TILT_POSITION] == 100 + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.COVER]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Verify service calls for tilting cover @@ -232,15 +231,17 @@ async def test_tilt_cover( } ], ) -@pytest.mark.usefixtures("config_entry_setup") async def test_level_controllable_output_cover( - hass: HomeAssistant, mock_put_request: Callable[[str, str], AiohttpClientMocker] + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory: ConfigEntryFactoryType, + mock_put_request: Callable[[str, str], AiohttpClientMocker], + snapshot: SnapshotAssertion, ) -> None: """Test that tilting a cover works.""" - assert len(hass.states.async_all()) == 1 - covering_device = hass.states.get("cover.vent") - assert covering_device.state == STATE_OPEN - assert covering_device.attributes[ATTR_CURRENT_TILT_POSITION] == 97 + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.COVER]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Verify service calls for tilting cover From 50da3c5c5bf3e6ad797b33957d6f29e5ee53d9ec Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 24 Jul 2024 17:15:01 +0200 Subject: [PATCH 1536/2411] Use snapshot in deCONZ climate tests (#122535) --- .../deconz/snapshots/test_climate.ambr | 545 ++++++++++++++++++ tests/components/deconz/test_climate.py | 162 ++---- 2 files changed, 595 insertions(+), 112 deletions(-) create mode 100644 tests/components/deconz/snapshots/test_climate.ambr diff --git a/tests/components/deconz/snapshots/test_climate.ambr b/tests/components/deconz/snapshots/test_climate.ambr new file mode 100644 index 00000000000..4e33e11534e --- /dev/null +++ b/tests/components/deconz/snapshots/test_climate.ambr @@ -0,0 +1,545 @@ +# serializer version: 1 +# name: test_climate_device_with_cooling_support[sensor_payload0][climate.zen_01-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'smart', + 'auto', + 'high', + 'medium', + 'low', + 'on', + 'off', + ]), + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.zen_01', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Zen-01', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:24:46:00:00:11:6f:56-01-0201', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_device_with_cooling_support[sensor_payload0][climate.zen_01-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 23.2, + 'fan_mode': 'off', + 'fan_modes': list([ + 'smart', + 'auto', + 'high', + 'medium', + 'low', + 'on', + 'off', + ]), + 'friendly_name': 'Zen-01', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'offset': 0, + 'supported_features': , + 'temperature': 22.2, + }), + 'context': , + 'entity_id': 'climate.zen_01', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_climate_device_with_fan_support[sensor_payload0][climate.zen_01-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'smart', + 'auto', + 'high', + 'medium', + 'low', + 'on', + 'off', + ]), + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.zen_01', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Zen-01', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:24:46:00:00:11:6f:56-01-0201', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_device_with_fan_support[sensor_payload0][climate.zen_01-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 23.2, + 'fan_mode': 'auto', + 'fan_modes': list([ + 'smart', + 'auto', + 'high', + 'medium', + 'low', + 'on', + 'off', + ]), + 'friendly_name': 'Zen-01', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'offset': 0, + 'supported_features': , + 'temperature': 22.2, + }), + 'context': , + 'entity_id': 'climate.zen_01', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_climate_device_with_preset[sensor_payload0][climate.zen_01-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'smart', + 'auto', + 'high', + 'medium', + 'low', + 'on', + 'off', + ]), + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'preset_modes': list([ + 'auto', + 'boost', + 'comfort', + 'complex', + 'eco', + 'holiday', + 'manual', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.zen_01', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Zen-01', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:24:46:00:00:11:6f:56-01-0201', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_device_with_preset[sensor_payload0][climate.zen_01-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 23.2, + 'fan_mode': 'off', + 'fan_modes': list([ + 'smart', + 'auto', + 'high', + 'medium', + 'low', + 'on', + 'off', + ]), + 'friendly_name': 'Zen-01', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'offset': 0, + 'preset_mode': 'auto', + 'preset_modes': list([ + 'auto', + 'boost', + 'comfort', + 'complex', + 'eco', + 'holiday', + 'manual', + ]), + 'supported_features': , + 'temperature': 22.2, + }), + 'context': , + 'entity_id': 'climate.zen_01', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_climate_device_without_cooling_support[sensor_payload0][climate.thermostat-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.thermostat', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Thermostat', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_device_without_cooling_support[sensor_payload0][climate.thermostat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 22.6, + 'friendly_name': 'Thermostat', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'offset': 10, + 'supported_features': , + 'temperature': 22.0, + 'valve': 30, + }), + 'context': , + 'entity_id': 'climate.thermostat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'auto', + }) +# --- +# name: test_clip_climate_device[config_entry_options0-sensor_payload0][climate.clip_thermostat-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.clip_thermostat', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CLIP thermostat', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:02-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_clip_climate_device[config_entry_options0-sensor_payload0][climate.clip_thermostat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 22.6, + 'friendly_name': 'CLIP thermostat', + 'hvac_action': , + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'supported_features': , + 'temperature': None, + 'valve': 30, + }), + 'context': , + 'entity_id': 'climate.clip_thermostat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_clip_climate_device[config_entry_options0-sensor_payload0][climate.thermostat-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.thermostat', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Thermostat', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_clip_climate_device[config_entry_options0-sensor_payload0][climate.thermostat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 22.6, + 'friendly_name': 'Thermostat', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'offset': 10, + 'supported_features': , + 'temperature': 22.0, + 'valve': 30, + }), + 'context': , + 'entity_id': 'climate.thermostat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'auto', + }) +# --- +# name: test_simple_climate_device[sensor_payload0][climate.thermostat-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.thermostat', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'thermostat', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '14:b4:57:ff:fe:d5:4e:77-01-0201', + 'unit_of_measurement': None, + }) +# --- +# name: test_simple_climate_device[sensor_payload0][climate.thermostat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 21.0, + 'friendly_name': 'thermostat', + 'hvac_action': , + 'hvac_modes': list([ + , + , + ]), + 'locked': True, + 'max_temp': 35, + 'min_temp': 7, + 'offset': 0, + 'supported_features': , + 'temperature': 21.0, + 'valve': 24, + }), + 'context': , + 'entity_id': 'climate.thermostat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 2188b1be475..c9104a5583a 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -1,8 +1,10 @@ """deCONZ climate platform tests.""" from collections.abc import Callable +from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion from homeassistant.components.climate import ( ATTR_FAN_MODE, @@ -11,15 +13,10 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE_DOMAIN, - FAN_AUTO, - FAN_HIGH, - FAN_LOW, - FAN_MEDIUM, FAN_OFF, FAN_ON, PRESET_BOOST, PRESET_COMFORT, - PRESET_ECO, SERVICE_SET_FAN_MODE, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, @@ -30,23 +27,23 @@ from homeassistant.components.climate import ( from homeassistant.components.deconz.climate import ( DECONZ_FAN_SMART, DECONZ_PRESET_AUTO, - DECONZ_PRESET_COMPLEX, - DECONZ_PRESET_HOLIDAY, DECONZ_PRESET_MANUAL, ) from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_OFF, STATE_UNAVAILABLE, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er -from .conftest import WebsocketDataType +from .conftest import ConfigEntryFactoryType, WebsocketDataType +from tests.common import snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker @@ -85,28 +82,21 @@ from tests.test_util.aiohttp import AiohttpClientMocker } ], ) -@pytest.mark.usefixtures("config_entry_setup") async def test_simple_climate_device( hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory: ConfigEntryFactoryType, mock_put_request: Callable[[str, str], AiohttpClientMocker], sensor_ws_data: WebsocketDataType, + snapshot: SnapshotAssertion, ) -> None: """Test successful creation of climate entities. This is a simple water heater that only supports setting temperature and on and off. """ - assert len(hass.states.async_all()) == 2 - climate_thermostat = hass.states.get("climate.thermostat") - assert climate_thermostat.state == HVACMode.HEAT - assert climate_thermostat.attributes["hvac_modes"] == [ - HVACMode.HEAT, - HVACMode.OFF, - ] - assert climate_thermostat.attributes["current_temperature"] == 21.0 - assert climate_thermostat.attributes["temperature"] == 21.0 - assert climate_thermostat.attributes["locked"] is True - assert hass.states.get("sensor.thermostat_battery").state == "59" - assert climate_thermostat.attributes["hvac_action"] == HVACAction.HEATING + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.CLIMATE]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Event signals thermostat configured off @@ -181,29 +171,16 @@ async def test_simple_climate_device( ) async def test_climate_device_without_cooling_support( hass: HomeAssistant, - config_entry_setup: ConfigEntry, + entity_registry: er.EntityRegistry, + config_entry_factory: ConfigEntryFactoryType, mock_put_request: Callable[[str, str], AiohttpClientMocker], sensor_ws_data: WebsocketDataType, + snapshot: SnapshotAssertion, ) -> None: """Test successful creation of sensor entities.""" - assert len(hass.states.async_all()) == 2 - climate_thermostat = hass.states.get("climate.thermostat") - assert climate_thermostat.state == HVACMode.AUTO - assert climate_thermostat.attributes["hvac_modes"] == [ - HVACMode.HEAT, - HVACMode.OFF, - HVACMode.AUTO, - ] - assert climate_thermostat.attributes["current_temperature"] == 22.6 - assert climate_thermostat.attributes["temperature"] == 22.0 - assert hass.states.get("sensor.thermostat") is None - assert hass.states.get("sensor.thermostat_battery").state == "100" - assert hass.states.get("climate.presence_sensor") is None - assert hass.states.get("climate.clip_thermostat") is None - assert ( - hass.states.get("climate.thermostat").attributes["hvac_action"] - == HVACAction.HEATING - ) + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.CLIMATE]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Event signals thermostat configured off @@ -300,14 +277,14 @@ async def test_climate_device_without_cooling_support( blocking=True, ) - await hass.config_entries.async_unload(config_entry_setup.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) states = hass.states.async_all() - assert len(states) == 2 + assert len(states) == 1 for state in states: assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_remove(config_entry_setup.entry_id) + await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 @@ -342,28 +319,18 @@ async def test_climate_device_without_cooling_support( } ], ) -@pytest.mark.usefixtures("config_entry_setup") async def test_climate_device_with_cooling_support( hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory: ConfigEntryFactoryType, mock_put_request: Callable[[str, str], AiohttpClientMocker], sensor_ws_data: WebsocketDataType, + snapshot: SnapshotAssertion, ) -> None: """Test successful creation of sensor entities.""" - assert len(hass.states.async_all()) == 2 - climate_thermostat = hass.states.get("climate.zen_01") - assert climate_thermostat.state == HVACMode.HEAT - assert climate_thermostat.attributes["hvac_modes"] == [ - HVACMode.HEAT, - HVACMode.OFF, - HVACMode.AUTO, - HVACMode.COOL, - ] - assert climate_thermostat.attributes["current_temperature"] == 23.2 - assert climate_thermostat.attributes["temperature"] == 22.2 - assert hass.states.get("sensor.zen_01_battery").state == "25" - assert ( - hass.states.get("climate.zen_01").attributes["hvac_action"] == HVACAction.IDLE - ) + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.CLIMATE]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Event signals thermostat mode cool @@ -428,29 +395,18 @@ async def test_climate_device_with_cooling_support( } ], ) -@pytest.mark.usefixtures("config_entry_setup") async def test_climate_device_with_fan_support( hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory: ConfigEntryFactoryType, mock_put_request: Callable[[str, str], AiohttpClientMocker], sensor_ws_data: WebsocketDataType, + snapshot: SnapshotAssertion, ) -> None: """Test successful creation of sensor entities.""" - assert len(hass.states.async_all()) == 2 - climate_thermostat = hass.states.get("climate.zen_01") - assert climate_thermostat.state == HVACMode.HEAT - assert climate_thermostat.attributes["fan_mode"] == FAN_AUTO - assert climate_thermostat.attributes["fan_modes"] == [ - DECONZ_FAN_SMART, - FAN_AUTO, - FAN_HIGH, - FAN_MEDIUM, - FAN_LOW, - FAN_ON, - FAN_OFF, - ] - assert ( - hass.states.get("climate.zen_01").attributes["hvac_action"] == HVACAction.IDLE - ) + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.CLIMATE]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Event signals fan mode defaults to off @@ -544,32 +500,18 @@ async def test_climate_device_with_fan_support( } ], ) -@pytest.mark.usefixtures("config_entry_setup") async def test_climate_device_with_preset( hass: HomeAssistant, + entity_registry: er.EntityRegistry, mock_put_request: Callable[[str, str], AiohttpClientMocker], sensor_ws_data: WebsocketDataType, + config_entry_factory: ConfigEntryFactoryType, + snapshot: SnapshotAssertion, ) -> None: """Test successful creation of sensor entities.""" - assert len(hass.states.async_all()) == 2 - - climate_zen_01 = hass.states.get("climate.zen_01") - assert climate_zen_01.state == HVACMode.HEAT - assert climate_zen_01.attributes["current_temperature"] == 23.2 - assert climate_zen_01.attributes["temperature"] == 22.2 - assert climate_zen_01.attributes["preset_mode"] == DECONZ_PRESET_AUTO - assert climate_zen_01.attributes["preset_modes"] == [ - DECONZ_PRESET_AUTO, - PRESET_BOOST, - PRESET_COMFORT, - DECONZ_PRESET_COMPLEX, - PRESET_ECO, - DECONZ_PRESET_HOLIDAY, - DECONZ_PRESET_MANUAL, - ] - assert ( - hass.states.get("climate.zen_01").attributes["hvac_action"] == HVACAction.IDLE - ) + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.CLIMATE]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Event signals deCONZ preset @@ -648,34 +590,34 @@ async def test_climate_device_with_preset( ) @pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: True}]) async def test_clip_climate_device( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory: ConfigEntryFactoryType, + snapshot: SnapshotAssertion, ) -> None: """Test successful creation of sensor entities.""" - assert len(hass.states.async_all()) == 3 - assert hass.states.get("climate.clip_thermostat").state == HVACMode.HEAT - assert ( - hass.states.get("climate.clip_thermostat").attributes["hvac_action"] - == HVACAction.HEATING - ) + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.CLIMATE]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Disallow clip sensors hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_ALLOW_CLIP_SENSOR: False} + config_entry, options={CONF_ALLOW_CLIP_SENSOR: False} ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 1 assert not hass.states.get("climate.clip_thermostat") # Allow clip sensors hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_ALLOW_CLIP_SENSOR: True} + config_entry, options={CONF_ALLOW_CLIP_SENSOR: True} ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 2 assert hass.states.get("climate.clip_thermostat").state == HVACMode.HEAT assert ( hass.states.get("climate.clip_thermostat").attributes["hvac_action"] @@ -808,7 +750,6 @@ async def test_no_mode_no_state(hass: HomeAssistant) -> None: assert len(hass.states.async_all()) == 2 climate_thermostat = hass.states.get("climate.zen_01") - assert climate_thermostat.state is STATE_OFF assert climate_thermostat.attributes["preset_mode"] is DECONZ_PRESET_AUTO assert climate_thermostat.attributes["hvac_action"] is HVACAction.IDLE @@ -857,13 +798,10 @@ async def test_boost_mode( sensor_ws_data: WebsocketDataType, ) -> None: """Test that a climate device with boost mode and different state works.""" - assert len(hass.states.async_all()) == 3 climate_thermostat = hass.states.get("climate.thermostat") - assert climate_thermostat.state == HVACMode.HEAT - assert climate_thermostat.attributes["preset_mode"] is DECONZ_PRESET_MANUAL assert climate_thermostat.attributes["hvac_action"] is HVACAction.IDLE From a8e60a6c53269e48dea20d6948c7b6327dfc4137 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 24 Jul 2024 17:28:47 +0200 Subject: [PATCH 1537/2411] Use snapshot in deCONZ number tests (#122538) --- .../deconz/snapshots/test_number.ambr | 211 ++++++++++++++++++ tests/components/deconz/test_number.py | 69 ++---- 2 files changed, 224 insertions(+), 56 deletions(-) create mode 100644 tests/components/deconz/snapshots/test_number.ambr diff --git a/tests/components/deconz/snapshots/test_number.ambr b/tests/components/deconz/snapshots/test_number.ambr new file mode 100644 index 00000000000..5311addc7a1 --- /dev/null +++ b/tests/components/deconz/snapshots/test_number.ambr @@ -0,0 +1,211 @@ +# serializer version: 1 +# name: test_number_entities[sensor_payload0-expected0][binary_sensor.presence_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.presence_sensor', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Presence sensor', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00-presence', + 'unit_of_measurement': None, + }) +# --- +# name: test_number_entities[sensor_payload0-expected0][binary_sensor.presence_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'dark': False, + 'device_class': 'motion', + 'friendly_name': 'Presence sensor', + 'on': True, + 'temperature': 0.1, + }), + 'context': , + 'entity_id': 'binary_sensor.presence_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_number_entities[sensor_payload0-expected0][number.presence_sensor_delay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65535, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.presence_sensor_delay', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Presence sensor Delay', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00-delay', + 'unit_of_measurement': None, + }) +# --- +# name: test_number_entities[sensor_payload0-expected0][number.presence_sensor_delay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Presence sensor Delay', + 'max': 65535, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.presence_sensor_delay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_number_entities[sensor_payload1-expected1][binary_sensor.presence_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.presence_sensor', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Presence sensor', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00-presence', + 'unit_of_measurement': None, + }) +# --- +# name: test_number_entities[sensor_payload1-expected1][binary_sensor.presence_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'dark': False, + 'device_class': 'motion', + 'friendly_name': 'Presence sensor', + 'on': True, + 'temperature': 0.1, + }), + 'context': , + 'entity_id': 'binary_sensor.presence_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_number_entities[sensor_payload1-expected1][number.presence_sensor_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65535, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.presence_sensor_duration', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Presence sensor Duration', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00-duration', + 'unit_of_measurement': None, + }) +# --- +# name: test_number_entities[sensor_payload1-expected1][number.presence_sensor_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Presence sensor Duration', + 'max': 65535, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.presence_sensor_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py index 7b34402600d..66eccdc6b4c 100644 --- a/tests/components/deconz/test_number.py +++ b/tests/components/deconz/test_number.py @@ -2,22 +2,24 @@ from collections.abc import Callable from typing import Any +from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion from homeassistant.components.number import ( ATTR_VALUE, DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, EntityCategory +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er -from .conftest import WebsocketDataType +from .conftest import ConfigEntryFactoryType, WebsocketDataType +from tests.common import snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker TEST_DATA = [ @@ -35,19 +37,7 @@ TEST_DATA = [ "uniqueid": "00:00:00:00:00:00:00:00-00", }, { - "entity_count": 3, - "device_count": 3, "entity_id": "number.presence_sensor_delay", - "unique_id": "00:00:00:00:00:00:00:00-00-delay", - "state": "0", - "entity_category": EntityCategory.CONFIG, - "attributes": { - "min": 0, - "max": 65535, - "step": 1, - "mode": "auto", - "friendly_name": "Presence sensor Delay", - }, "websocket_event": {"config": {"delay": 10}}, "next_state": "10", "supported_service_value": 111, @@ -71,19 +61,7 @@ TEST_DATA = [ "uniqueid": "00:00:00:00:00:00:00:00-00", }, { - "entity_count": 3, - "device_count": 3, "entity_id": "number.presence_sensor_duration", - "unique_id": "00:00:00:00:00:00:00:00-00-duration", - "state": "0", - "entity_category": EntityCategory.CONFIG, - "attributes": { - "min": 0, - "max": 65535, - "step": 1, - "mode": "auto", - "friendly_name": "Presence sensor Duration", - }, "websocket_event": {"config": {"duration": 10}}, "next_state": "10", "supported_service_value": 111, @@ -99,38 +77,17 @@ TEST_DATA = [ @pytest.mark.parametrize(("sensor_payload", "expected"), TEST_DATA) async def test_number_entities( hass: HomeAssistant, - device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - config_entry_setup: ConfigEntry, + config_entry_factory: ConfigEntryFactoryType, sensor_ws_data: WebsocketDataType, mock_put_request: Callable[[str, str], AiohttpClientMocker], expected: dict[str, Any], + snapshot: SnapshotAssertion, ) -> None: """Test successful creation of number entities.""" - assert len(hass.states.async_all()) == expected["entity_count"] - - # Verify state data - - entity = hass.states.get(expected["entity_id"]) - assert entity.state == expected["state"] - assert entity.attributes == expected["attributes"] - - # Verify entity registry data - - ent_reg_entry = entity_registry.async_get(expected["entity_id"]) - assert ent_reg_entry.entity_category is expected["entity_category"] - assert ent_reg_entry.unique_id == expected["unique_id"] - - # Verify device registry data - - assert ( - len( - dr.async_entries_for_config_entry( - device_registry, config_entry_setup.entry_id - ) - ) - == expected["device_count"] - ) + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.NUMBER]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Change state @@ -182,11 +139,11 @@ async def test_number_entities( # Unload entry - await hass.config_entries.async_unload(config_entry_setup.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE # Remove entry - await hass.config_entries.async_remove(config_entry_setup.entry_id) + await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 From 5bda07214183d7475741ffe9a4faaf0a00ba6353 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 24 Jul 2024 17:32:57 +0200 Subject: [PATCH 1538/2411] Use snapshot in deCONZ scene tests (#122540) --- .../deconz/snapshots/test_scene.ambr | 47 +++++++++++++++++ tests/components/deconz/test_scene.py | 51 +++++-------------- 2 files changed, 60 insertions(+), 38 deletions(-) create mode 100644 tests/components/deconz/snapshots/test_scene.ambr diff --git a/tests/components/deconz/snapshots/test_scene.ambr b/tests/components/deconz/snapshots/test_scene.ambr new file mode 100644 index 00000000000..85a5ab92c5c --- /dev/null +++ b/tests/components/deconz/snapshots/test_scene.ambr @@ -0,0 +1,47 @@ +# serializer version: 1 +# name: test_scenes[group_payload0-expected0][scene.light_group_scene-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'scene', + 'entity_category': None, + 'entity_id': 'scene.light_group_scene', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Scene', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01234E56789A/groups/1/scenes/1', + 'unit_of_measurement': None, + }) +# --- +# name: test_scenes[group_payload0-expected0][scene.light_group_scene-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Light group Scene', + }), + 'context': , + 'entity_id': 'scene.light_group_scene', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index 60746311928..d3a5725f7b1 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -2,17 +2,19 @@ from collections.abc import Callable from typing import Any +from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, SERVICE_TURN_ON -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er -from .conftest import WebsocketDataType +from .conftest import ConfigEntryFactoryType, WebsocketDataType +from tests.common import snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker TEST_DATA = [ @@ -29,14 +31,7 @@ TEST_DATA = [ } }, { - "entity_count": 2, - "device_count": 3, "entity_id": "scene.light_group_scene", - "unique_id": "01234E56789A/groups/1/scenes/1", - "entity_category": None, - "attributes": { - "friendly_name": "Light group Scene", - }, "request": "/groups/1/scenes/1/recall", }, ), @@ -46,36 +41,16 @@ TEST_DATA = [ @pytest.mark.parametrize(("group_payload", "expected"), TEST_DATA) async def test_scenes( hass: HomeAssistant, - device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - config_entry_setup: ConfigEntry, + config_entry_factory: ConfigEntryFactoryType, mock_put_request: Callable[[str, str], AiohttpClientMocker], expected: dict[str, Any], + snapshot: SnapshotAssertion, ) -> None: """Test successful creation of scene entities.""" - assert len(hass.states.async_all()) == expected["entity_count"] - - # Verify state data - - scene = hass.states.get(expected["entity_id"]) - assert scene.attributes == expected["attributes"] - - # Verify entity registry data - - ent_reg_entry = entity_registry.async_get(expected["entity_id"]) - assert ent_reg_entry.entity_category is expected["entity_category"] - assert ent_reg_entry.unique_id == expected["unique_id"] - - # Verify device registry data - - assert ( - len( - dr.async_entries_for_config_entry( - device_registry, config_entry_setup.entry_id - ) - ) - == expected["device_count"] - ) + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.SCENE]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Verify button press @@ -91,12 +66,12 @@ async def test_scenes( # Unload entry - await hass.config_entries.async_unload(config_entry_setup.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE # Remove entry - await hass.config_entries.async_remove(config_entry_setup.entry_id) + await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 From 277883e7565a5fc6d31664a07dcb250fecf2d0f9 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 24 Jul 2024 18:07:18 +0200 Subject: [PATCH 1539/2411] Use snapshot in deCONZ sensor tests (#122543) --- .../deconz/snapshots/test_sensor.ambr | 2297 +++++++++++++++++ tests/components/deconz/test_sensor.py | 361 +-- 2 files changed, 2321 insertions(+), 337 deletions(-) create mode 100644 tests/components/deconz/snapshots/test_sensor.ambr diff --git a/tests/components/deconz/snapshots/test_sensor.ambr b/tests/components/deconz/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..7f12292abbd --- /dev/null +++ b/tests/components/deconz/snapshots/test_sensor.ambr @@ -0,0 +1,2297 @@ +# serializer version: 1 +# name: test_allow_clip_sensors[config_entry_options0-sensor_payload0][sensor.clip_flur-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.clip_flur', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CLIP Flur', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/sensors/3-status', + 'unit_of_measurement': None, + }) +# --- +# name: test_allow_clip_sensors[config_entry_options0-sensor_payload0][sensor.clip_flur-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CLIP Flur', + 'on': True, + }), + 'context': , + 'entity_id': 'sensor.clip_flur', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_allow_clip_sensors[config_entry_options0-sensor_payload0][sensor.clip_light_level_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.clip_light_level_sensor', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CLIP light level sensor', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-00-light_level', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_allow_clip_sensors[config_entry_options0-sensor_payload0][sensor.clip_light_level_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'CLIP light level sensor', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.clip_light_level_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '999.8', + }) +# --- +# name: test_allow_clip_sensors[config_entry_options0-sensor_payload0][sensor.light_level_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.light_level_sensor', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Light level sensor', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00-light_level', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_allow_clip_sensors[config_entry_options0-sensor_payload0][sensor.light_level_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'dark': False, + 'device_class': 'illuminance', + 'friendly_name': 'Light level sensor', + 'on': True, + 'state_class': , + 'temperature': 0.1, + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.light_level_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '999.8', + }) +# --- +# name: test_allow_clip_sensors[config_entry_options0-sensor_payload0][sensor.light_level_sensor_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.light_level_sensor_temperature', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Light level sensor Temperature', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00-internal_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_allow_clip_sensors[config_entry_options0-sensor_payload0][sensor.light_level_sensor_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Light level sensor Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.light_level_sensor_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.1', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload0-expected0][sensor.bosch_air_quality_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bosch_air_quality_sensor', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'BOSCH Air quality sensor', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:12:4b:00:14:4d:00:07-02-fdef-air_quality', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload0-expected0][sensor.bosch_air_quality_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'BOSCH Air quality sensor', + }), + 'context': , + 'entity_id': 'sensor.bosch_air_quality_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'poor', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload0-expected0][sensor.bosch_air_quality_sensor_ppb-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bosch_air_quality_sensor_ppb', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'BOSCH Air quality sensor PPB', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:12:4b:00:14:4d:00:07-02-fdef-air_quality_ppb', + 'unit_of_measurement': 'ppb', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload0-expected0][sensor.bosch_air_quality_sensor_ppb-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'BOSCH Air quality sensor PPB', + 'state_class': , + 'unit_of_measurement': 'ppb', + }), + 'context': , + 'entity_id': 'sensor.bosch_air_quality_sensor_ppb', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '809', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload1-expected1][sensor.bosch_air_quality_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bosch_air_quality_sensor', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'BOSCH Air quality sensor', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:12:4b:00:14:4d:00:07-02-fdef-air_quality', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload1-expected1][sensor.bosch_air_quality_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'BOSCH Air quality sensor', + }), + 'context': , + 'entity_id': 'sensor.bosch_air_quality_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'poor', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload1-expected1][sensor.bosch_air_quality_sensor_ppb-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bosch_air_quality_sensor_ppb', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'BOSCH Air quality sensor PPB', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:12:4b:00:14:4d:00:07-02-fdef-air_quality_ppb', + 'unit_of_measurement': 'ppb', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload1-expected1][sensor.bosch_air_quality_sensor_ppb-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'BOSCH Air quality sensor PPB', + 'state_class': , + 'unit_of_measurement': 'ppb', + }), + 'context': , + 'entity_id': 'sensor.bosch_air_quality_sensor_ppb', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '809', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload10-expected10][sensor.fsm_state_motion_stair-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.fsm_state_motion_stair', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'FSM_STATE Motion stair', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'fsm-state-1520195376277-status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload10-expected10][sensor.fsm_state_motion_stair-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'FSM_STATE Motion stair', + 'on': True, + }), + 'context': , + 'entity_id': 'sensor.fsm_state_motion_stair', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload11-expected11][sensor.mi_temperature_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mi_temperature_1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Mi temperature 1', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:45:dc:53-01-0405-humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload11-expected11][sensor.mi_temperature_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Mi temperature 1', + 'on': True, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.mi_temperature_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35.55', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload11-expected11][sensor.mi_temperature_1_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mi_temperature_1_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Mi temperature 1 Battery', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:45:dc:53-01-0405-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload11-expected11][sensor.mi_temperature_1_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Mi temperature 1 Battery', + 'on': True, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.mi_temperature_1_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload12-expected12][binary_sensor.soil_sensor_low_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.soil_sensor_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Soil Sensor Low Battery', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a4:c1:38:fe:86:8f:07:a3-01-0408-low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload12-expected12][binary_sensor.soil_sensor_low_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Soil Sensor Low Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.soil_sensor_low_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload12-expected12][sensor.soil_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.soil_sensor', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Soil Sensor', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a4:c1:38:fe:86:8f:07:a3-01-0408-moisture', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload12-expected12][sensor.soil_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'moisture', + 'friendly_name': 'Soil Sensor', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.soil_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '72.13', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload12-expected12][sensor.soil_sensor_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.soil_sensor_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Soil Sensor Battery', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a4:c1:38:fe:86:8f:07:a3-01-0408-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload12-expected12][sensor.soil_sensor_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Soil Sensor Battery', + 'on': True, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.soil_sensor_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload13-expected13][sensor.motion_sensor_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.motion_sensor_4', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion sensor 4', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:17:88:01:03:28:8c:9b-02-0400-light_level', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload13-expected13][sensor.motion_sensor_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'dark': True, + 'daylight': False, + 'device_class': 'illuminance', + 'friendly_name': 'Motion sensor 4', + 'on': True, + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.motion_sensor_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload13-expected13][sensor.motion_sensor_4_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.motion_sensor_4_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion sensor 4 Battery', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:17:88:01:03:28:8c:9b-02-0400-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload13-expected13][sensor.motion_sensor_4_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'dark': True, + 'daylight': False, + 'device_class': 'battery', + 'friendly_name': 'Motion sensor 4 Battery', + 'on': True, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.motion_sensor_4_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload14-expected14][sensor.starkvind_airpurifier_pm25-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.starkvind_airpurifier_pm25', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'STARKVIND AirPurifier PM25', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-042a-particulate_matter_pm2_5', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload14-expected14][sensor.starkvind_airpurifier_pm25-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'STARKVIND AirPurifier PM25', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.starkvind_airpurifier_pm25', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload15-expected15][sensor.power_16-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.power_16', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power 16', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:0d:6f:00:0b:7a:64:29-01-0b04-power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload15-expected15][sensor.power_16-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current': 34, + 'device_class': 'power', + 'friendly_name': 'Power 16', + 'on': True, + 'state_class': , + 'unit_of_measurement': , + 'voltage': 231, + }), + 'context': , + 'entity_id': 'sensor.power_16', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '64', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload16-expected16][sensor.mi_temperature_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mi_temperature_1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Mi temperature 1', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:45:dc:53-01-0403-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload16-expected16][sensor.mi_temperature_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Mi temperature 1', + 'on': True, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mi_temperature_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1010', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload16-expected16][sensor.mi_temperature_1_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mi_temperature_1_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Mi temperature 1 Battery', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:45:dc:53-01-0403-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload16-expected16][sensor.mi_temperature_1_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Mi temperature 1 Battery', + 'on': True, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.mi_temperature_1_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload17-expected17][sensor.mi_temperature_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mi_temperature_1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Mi temperature 1', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:45:dc:53-01-0402-temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload17-expected17][sensor.mi_temperature_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Mi temperature 1', + 'on': True, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mi_temperature_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.82', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload17-expected17][sensor.mi_temperature_1_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mi_temperature_1_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Mi temperature 1 Battery', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:45:dc:53-01-0402-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload17-expected17][sensor.mi_temperature_1_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Mi temperature 1 Battery', + 'on': True, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.mi_temperature_1_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload18-expected18][sensor.etrv_sejour-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.etrv_sejour', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'eTRV Séjour', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'cc:cc:cc:ff:fe:38:4d:b3-01-000a-last_set', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload18-expected18][sensor.etrv_sejour-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'eTRV Séjour', + }), + 'context': , + 'entity_id': 'sensor.etrv_sejour', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2020-11-19T08:07:08+00:00', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload18-expected18][sensor.etrv_sejour_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.etrv_sejour_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'eTRV Séjour Battery', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'cc:cc:cc:ff:fe:38:4d:b3-01-000a-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload18-expected18][sensor.etrv_sejour_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'eTRV Séjour Battery', + 'on': True, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.etrv_sejour_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload19-expected19][binary_sensor.alarm_10-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.alarm_10', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Alarm 10', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:b5:d1:80-01-0500-alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload19-expected19][binary_sensor.alarm_10-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'safety', + 'friendly_name': 'Alarm 10', + 'on': True, + 'temperature': 26.0, + }), + 'context': , + 'entity_id': 'binary_sensor.alarm_10', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload19-expected19][sensor.alarm_10_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.alarm_10_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Alarm 10 Battery', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:b5:d1:80-01-0500-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload19-expected19][sensor.alarm_10_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Alarm 10 Battery', + 'on': True, + 'state_class': , + 'temperature': 26.0, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.alarm_10_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload19-expected19][sensor.alarm_10_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.alarm_10_temperature', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Alarm 10 Temperature', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:02:b5:d1:80-01-0500-internal_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload19-expected19][sensor.alarm_10_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Alarm 10 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.alarm_10_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26.0', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload2-expected2][sensor.airquality_1_ch2o-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airquality_1_ch2o', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AirQuality 1 CH2O', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload2-expected2][sensor.airquality_1_ch2o-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volatile_organic_compounds', + 'friendly_name': 'AirQuality 1 CH2O', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.airquality_1_ch2o', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload2-expected2][sensor.airquality_1_co2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airquality_1_co2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AirQuality 1 CO2', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_co2', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload2-expected2][sensor.airquality_1_co2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'AirQuality 1 CO2', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.airquality_1_co2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '359', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload2-expected2][sensor.airquality_1_pm25-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airquality_1_pm25', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AirQuality 1 PM25', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload2-expected2][sensor.airquality_1_pm25-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'AirQuality 1 PM25', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.airquality_1_pm25', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload2-expected2][sensor.airquality_1_ppb-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airquality_1_ppb', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'AirQuality 1 PPB', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_ppb', + 'unit_of_measurement': 'ppb', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload2-expected2][sensor.airquality_1_ppb-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AirQuality 1 PPB', + 'state_class': , + 'unit_of_measurement': 'ppb', + }), + 'context': , + 'entity_id': 'sensor.airquality_1_ppb', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload20-expected20][sensor.dimmer_switch_3_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dimmer_switch_3_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Dimmer switch 3 Battery', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:17:88:01:02:0e:32:a3-02-fc00-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload20-expected20][sensor.dimmer_switch_3_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'event_id': 'dimmer_switch_3', + 'friendly_name': 'Dimmer switch 3 Battery', + 'on': True, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.dimmer_switch_3_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '90', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload3-expected3][sensor.airquality_1_ch2o-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airquality_1_ch2o', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AirQuality 1 CH2O', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload3-expected3][sensor.airquality_1_ch2o-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volatile_organic_compounds', + 'friendly_name': 'AirQuality 1 CH2O', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.airquality_1_ch2o', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload3-expected3][sensor.airquality_1_co2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airquality_1_co2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AirQuality 1 CO2', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_co2', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload3-expected3][sensor.airquality_1_co2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'AirQuality 1 CO2', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.airquality_1_co2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '359', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload3-expected3][sensor.airquality_1_pm25-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airquality_1_pm25', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AirQuality 1 PM25', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload3-expected3][sensor.airquality_1_pm25-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'AirQuality 1 PM25', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.airquality_1_pm25', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload3-expected3][sensor.airquality_1_ppb-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airquality_1_ppb', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'AirQuality 1 PPB', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_ppb', + 'unit_of_measurement': 'ppb', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload3-expected3][sensor.airquality_1_ppb-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AirQuality 1 PPB', + 'state_class': , + 'unit_of_measurement': 'ppb', + }), + 'context': , + 'entity_id': 'sensor.airquality_1_ppb', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload4-expected4][sensor.airquality_1_ch2o-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airquality_1_ch2o', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AirQuality 1 CH2O', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload4-expected4][sensor.airquality_1_ch2o-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volatile_organic_compounds', + 'friendly_name': 'AirQuality 1 CH2O', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.airquality_1_ch2o', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload4-expected4][sensor.airquality_1_co2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airquality_1_co2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AirQuality 1 CO2', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_co2', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload4-expected4][sensor.airquality_1_co2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'AirQuality 1 CO2', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.airquality_1_co2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '359', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload4-expected4][sensor.airquality_1_pm25-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airquality_1_pm25', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AirQuality 1 PM25', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload4-expected4][sensor.airquality_1_pm25-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'AirQuality 1 PM25', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.airquality_1_pm25', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload4-expected4][sensor.airquality_1_ppb-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.airquality_1_ppb', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'AirQuality 1 PPB', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_ppb', + 'unit_of_measurement': 'ppb', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload4-expected4][sensor.airquality_1_ppb-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AirQuality 1 PPB', + 'state_class': , + 'unit_of_measurement': 'ppb', + }), + 'context': , + 'entity_id': 'sensor.airquality_1_ppb', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload5-expected5][sensor.fyrtur_block_out_roller_blind_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.fyrtur_block_out_roller_blind_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'FYRTUR block-out roller blind Battery', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:0d:6f:ff:fe:01:23:45-01-0001-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload5-expected5][sensor.fyrtur_block_out_roller_blind_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'FYRTUR block-out roller blind Battery', + 'on': True, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fyrtur_block_out_roller_blind_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload6-expected6][sensor.carbondioxide_35-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.carbondioxide_35', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CarbonDioxide 35', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-040d-carbon_dioxide', + 'unit_of_measurement': 'ppb', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload6-expected6][sensor.carbondioxide_35-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'CarbonDioxide 35', + 'state_class': , + 'unit_of_measurement': 'ppb', + }), + 'context': , + 'entity_id': 'sensor.carbondioxide_35', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '370', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload7-expected7][sensor.consumption_15-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.consumption_15', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumption 15', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:0d:6f:00:0b:7a:64:29-01-0702-consumption', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload7-expected7][sensor.consumption_15-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Consumption 15', + 'on': True, + 'power': 123, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.consumption_15', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.342', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload8-expected8][sensor.daylight-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.daylight', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:white-balance-sunny', + 'original_name': 'Daylight', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01:23:4E:FF:FF:56:78:9A-01-daylight_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload8-expected8][sensor.daylight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'daylight': True, + 'friendly_name': 'Daylight', + 'icon': 'mdi:white-balance-sunny', + 'on': True, + }), + 'context': , + 'entity_id': 'sensor.daylight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'solar_noon', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload9-expected9][sensor.formaldehyde_34-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.formaldehyde_34', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Formaldehyde 34', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-042b-formaldehyde', + 'unit_of_measurement': 'ppb', + }) +# --- +# name: test_sensors[config_entry_options0-sensor_payload9-expected9][sensor.formaldehyde_34-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volatile_organic_compounds', + 'friendly_name': 'Formaldehyde 34', + 'state_class': , + 'unit_of_measurement': 'ppb', + }), + 'context': , + 'entity_id': 'sensor.formaldehyde_34', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 76da8628da1..b50032d9c9f 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -2,31 +2,22 @@ from datetime import timedelta from typing import Any +from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR -from homeassistant.components.sensor import ( - DOMAIN as SENSOR_DOMAIN, - SensorDeviceClass, - SensorStateClass, -) -from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, ConfigEntry -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_BILLION, - CONCENTRATION_PARTS_PER_MILLION, - STATE_UNAVAILABLE, - EntityCategory, -) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util from .conftest import ConfigEntryFactoryType, WebsocketDataType -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, snapshot_platform TEST_DATA = [ ( # Air quality sensor @@ -51,17 +42,7 @@ TEST_DATA = [ "uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef", }, { - "entity_count": 2, - "device_count": 3, "entity_id": "sensor.bosch_air_quality_sensor", - "unique_id": "00:12:4b:00:14:4d:00:07-02-fdef-air_quality", - "state": "poor", - "entity_category": None, - "device_class": None, - "state_class": None, - "attributes": { - "friendly_name": "BOSCH Air quality sensor", - }, "websocket_event": {"state": {"airquality": "excellent"}}, "next_state": "excellent", }, @@ -88,19 +69,7 @@ TEST_DATA = [ "uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef", }, { - "entity_count": 2, - "device_count": 3, "entity_id": "sensor.bosch_air_quality_sensor_ppb", - "unique_id": "00:12:4b:00:14:4d:00:07-02-fdef-air_quality_ppb", - "state": "809", - "entity_category": None, - "device_class": None, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "friendly_name": "BOSCH Air quality sensor PPB", - "state_class": "measurement", - "unit_of_measurement": CONCENTRATION_PARTS_PER_BILLION, - }, "websocket_event": {"state": {"airqualityppb": 1000}}, "next_state": "1000", }, @@ -127,20 +96,7 @@ TEST_DATA = [ "uniqueid": "00:00:00:00:00:00:00:01-02-0113", }, { - "entity_count": 4, - "device_count": 3, "entity_id": "sensor.airquality_1_co2", - "unique_id": "00:00:00:00:00:00:00:01-02-0113-air_quality_co2", - "state": "359", - "entity_category": None, - "device_class": SensorDeviceClass.CO2, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "friendly_name": "AirQuality 1 CO2", - "device_class": SensorDeviceClass.CO2, - "state_class": SensorStateClass.MEASUREMENT, - "unit_of_measurement": CONCENTRATION_PARTS_PER_MILLION, - }, "websocket_event": {"state": {"airquality_co2_density": 332}}, "next_state": "332", }, @@ -167,20 +123,7 @@ TEST_DATA = [ "uniqueid": "00:00:00:00:00:00:00:01-02-0113", }, { - "entity_count": 4, - "device_count": 3, "entity_id": "sensor.airquality_1_ch2o", - "unique_id": "00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde", - "state": "4", - "entity_category": None, - "device_class": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "friendly_name": "AirQuality 1 CH2O", - "device_class": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, - "state_class": SensorStateClass.MEASUREMENT, - "unit_of_measurement": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, "websocket_event": {"state": {"airquality_formaldehyde_density": 5}}, "next_state": "5", }, @@ -207,20 +150,7 @@ TEST_DATA = [ "uniqueid": "00:00:00:00:00:00:00:01-02-0113", }, { - "entity_count": 4, - "device_count": 3, "entity_id": "sensor.airquality_1_pm25", - "unique_id": "00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5", - "state": "8", - "entity_category": None, - "device_class": SensorDeviceClass.PM25, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "friendly_name": "AirQuality 1 PM25", - "device_class": SensorDeviceClass.PM25, - "state_class": SensorStateClass.MEASUREMENT, - "unit_of_measurement": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, "websocket_event": {"state": {"pm2_5": 11}}, "next_state": "11", }, @@ -246,21 +176,7 @@ TEST_DATA = [ "uniqueid": "00:0d:6f:ff:fe:01:23:45-01-0001", }, { - "entity_count": 1, - "device_count": 3, "entity_id": "sensor.fyrtur_block_out_roller_blind_battery", - "unique_id": "00:0d:6f:ff:fe:01:23:45-01-0001-battery", - "state": "100", - "entity_category": EntityCategory.DIAGNOSTIC, - "device_class": SensorDeviceClass.BATTERY, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "state_class": "measurement", - "on": True, - "unit_of_measurement": "%", - "device_class": "battery", - "friendly_name": "FYRTUR block-out roller blind Battery", - }, "websocket_event": {"state": {"battery": 50}}, "next_state": "50", }, @@ -290,20 +206,7 @@ TEST_DATA = [ "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-040d", }, { - "entity_count": 1, - "device_count": 3, "entity_id": "sensor.carbondioxide_35", - "unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-040d-carbon_dioxide", - "state": "370", - "entity_category": None, - "device_class": SensorDeviceClass.CO2, - "state_class": CONCENTRATION_PARTS_PER_BILLION, - "attributes": { - "device_class": "carbon_dioxide", - "friendly_name": "CarbonDioxide 35", - "state_class": SensorStateClass.MEASUREMENT, - "unit_of_measurement": CONCENTRATION_PARTS_PER_BILLION, - }, "websocket_event": {"state": {"measured_value": 500}}, "next_state": "500", }, @@ -325,22 +228,7 @@ TEST_DATA = [ "uniqueid": "00:0d:6f:00:0b:7a:64:29-01-0702", }, { - "entity_count": 1, - "device_count": 3, "entity_id": "sensor.consumption_15", - "unique_id": "00:0d:6f:00:0b:7a:64:29-01-0702-consumption", - "state": "11.342", - "entity_category": None, - "device_class": SensorDeviceClass.ENERGY, - "state_class": SensorStateClass.TOTAL_INCREASING, - "attributes": { - "state_class": "total_increasing", - "on": True, - "power": 123, - "unit_of_measurement": "kWh", - "device_class": "energy", - "friendly_name": "Consumption 15", - }, "websocket_event": {"state": {"consumption": 10000}}, "next_state": "10.0", }, @@ -368,21 +256,7 @@ TEST_DATA = [ }, { "enable_entity": True, - "entity_count": 1, - "device_count": 3, "entity_id": "sensor.daylight", - "unique_id": "01:23:4E:FF:FF:56:78:9A-01-daylight_status", - "old-unique_id": "01:23:4E:FF:FF:56:78:9A-01", - "state": "solar_noon", - "entity_category": None, - "device_class": None, - "state_class": None, - "attributes": { - "on": True, - "daylight": True, - "icon": "mdi:white-balance-sunny", - "friendly_name": "Daylight", - }, "websocket_event": {"state": {"status": 210}}, "next_state": "dusk", }, @@ -412,20 +286,7 @@ TEST_DATA = [ "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-042b", }, { - "entity_count": 1, - "device_count": 3, "entity_id": "sensor.formaldehyde_34", - "unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-042b-formaldehyde", - "state": "1", - "entity_category": None, - "device_class": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "device_class": "volatile_organic_compounds", - "friendly_name": "Formaldehyde 34", - "state_class": SensorStateClass.MEASUREMENT, - "unit_of_measurement": CONCENTRATION_PARTS_PER_BILLION, - }, "websocket_event": {"state": {"measured_value": 2}}, "next_state": "2", }, @@ -449,18 +310,7 @@ TEST_DATA = [ "uniqueid": "fsm-state-1520195376277", }, { - "entity_count": 1, - "device_count": 2, "entity_id": "sensor.fsm_state_motion_stair", - "unique_id": "fsm-state-1520195376277-status", - "state": "0", - "entity_category": None, - "device_class": None, - "state_class": None, - "attributes": { - "on": True, - "friendly_name": "FSM_STATE Motion stair", - }, "websocket_event": {"state": {"status": 1}}, "next_state": "1", }, @@ -487,24 +337,7 @@ TEST_DATA = [ "uniqueid": "00:15:8d:00:02:45:dc:53-01-0405", }, { - "entity_count": 2, - "device_count": 3, "entity_id": "sensor.mi_temperature_1", - "unique_id": "00:15:8d:00:02:45:dc:53-01-0405-humidity", - "state": "35.55", - "entity_category": None, - "device_class": SensorDeviceClass.HUMIDITY, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "state_class": "measurement", - "on": True, - "unit_of_measurement": "%", - "device_class": "humidity", - "friendly_name": "Mi temperature 1", - }, - "options": { - "suggested_display_precision": 1, - }, "websocket_event": {"state": {"humidity": 1000}}, "next_state": "10.0", }, @@ -528,20 +361,7 @@ TEST_DATA = [ "uniqueid": "a4:c1:38:fe:86:8f:07:a3-01-0408", }, { - "entity_count": 3, - "device_count": 3, "entity_id": "sensor.soil_sensor", - "unique_id": "a4:c1:38:fe:86:8f:07:a3-01-0408-moisture", - "state": "72.13", - "entity_category": None, - "device_class": SensorDeviceClass.MOISTURE, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "state_class": "measurement", - "unit_of_measurement": "%", - "device_class": "moisture", - "friendly_name": "Soil Sensor", - }, "websocket_event": {"state": {"moisture": 6923}}, "next_state": "69.23", }, @@ -576,23 +396,7 @@ TEST_DATA = [ "uniqueid": "00:17:88:01:03:28:8c:9b-02-0400", }, { - "entity_count": 2, - "device_count": 3, "entity_id": "sensor.motion_sensor_4", - "unique_id": "00:17:88:01:03:28:8c:9b-02-0400-light_level", - "state": "5.0", - "entity_category": None, - "device_class": SensorDeviceClass.ILLUMINANCE, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "on": True, - "dark": True, - "daylight": False, - "unit_of_measurement": "lx", - "device_class": "illuminance", - "friendly_name": "Motion sensor 4", - "state_class": "measurement", - }, "websocket_event": {"state": {"lightlevel": 1000}}, "next_state": "1.3", }, @@ -628,20 +432,7 @@ TEST_DATA = [ "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-042a", }, { - "entity_count": 1, - "device_count": 3, "entity_id": "sensor.starkvind_airpurifier_pm25", - "unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-042a-particulate_matter_pm2_5", - "state": "1", - "entity_category": None, - "device_class": SensorDeviceClass.PM25, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "friendly_name": "STARKVIND AirPurifier PM25", - "device_class": SensorDeviceClass.PM25, - "state_class": SensorStateClass.MEASUREMENT, - "unit_of_measurement": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, "websocket_event": {"state": {"measured_value": 2}}, "next_state": "2", }, @@ -667,23 +458,7 @@ TEST_DATA = [ "uniqueid": "00:0d:6f:00:0b:7a:64:29-01-0b04", }, { - "entity_count": 1, - "device_count": 3, "entity_id": "sensor.power_16", - "unique_id": "00:0d:6f:00:0b:7a:64:29-01-0b04-power", - "state": "64", - "entity_category": None, - "device_class": SensorDeviceClass.POWER, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "state_class": "measurement", - "on": True, - "current": 34, - "voltage": 231, - "unit_of_measurement": "W", - "device_class": "power", - "friendly_name": "Power 16", - }, "websocket_event": {"state": {"power": 1000}}, "next_state": "1000", }, @@ -709,21 +484,7 @@ TEST_DATA = [ "uniqueid": "00:15:8d:00:02:45:dc:53-01-0403", }, { - "entity_count": 2, - "device_count": 3, "entity_id": "sensor.mi_temperature_1", - "unique_id": "00:15:8d:00:02:45:dc:53-01-0403-pressure", - "state": "1010", - "entity_category": None, - "device_class": SensorDeviceClass.PRESSURE, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "state_class": "measurement", - "on": True, - "unit_of_measurement": "hPa", - "device_class": "pressure", - "friendly_name": "Mi temperature 1", - }, "websocket_event": {"state": {"pressure": 500}}, "next_state": "500", }, @@ -750,24 +511,7 @@ TEST_DATA = [ "uniqueid": "00:15:8d:00:02:45:dc:53-01-0402", }, { - "entity_count": 2, - "device_count": 3, "entity_id": "sensor.mi_temperature_1", - "unique_id": "00:15:8d:00:02:45:dc:53-01-0402-temperature", - "state": "21.82", - "entity_category": None, - "device_class": SensorDeviceClass.TEMPERATURE, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "state_class": "measurement", - "on": True, - "unit_of_measurement": "°C", - "device_class": "temperature", - "friendly_name": "Mi temperature 1", - }, - "options": { - "suggested_display_precision": 1, - }, "websocket_event": {"state": {"temperature": 1800}}, "next_state": "18.0", }, @@ -796,17 +540,7 @@ TEST_DATA = [ "uniqueid": "cc:cc:cc:ff:fe:38:4d:b3-01-000a", }, { - "entity_count": 2, - "device_count": 3, "entity_id": "sensor.etrv_sejour", - "unique_id": "cc:cc:cc:ff:fe:38:4d:b3-01-000a-last_set", - "state": "2020-11-19T08:07:08+00:00", - "entity_category": None, - "device_class": SensorDeviceClass.TIMESTAMP, - "attributes": { - "device_class": "timestamp", - "friendly_name": "eTRV Séjour", - }, "websocket_event": {"state": {"lastset": "2020-12-14T10:12:14Z"}}, "next_state": "2020-12-14T10:12:14+00:00", }, @@ -835,20 +569,7 @@ TEST_DATA = [ "uniqueid": "00:15:8d:00:02:b5:d1:80-01-0500", }, { - "entity_count": 3, - "device_count": 3, "entity_id": "sensor.alarm_10_temperature", - "unique_id": "00:15:8d:00:02:b5:d1:80-01-0500-internal_temperature", - "state": "26.0", - "entity_category": None, - "device_class": SensorDeviceClass.TEMPERATURE, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "state_class": "measurement", - "unit_of_measurement": "°C", - "device_class": "temperature", - "friendly_name": "Alarm 10 Temperature", - }, "websocket_event": {"state": {"temperature": 1800}}, "next_state": "26.0", }, @@ -876,22 +597,7 @@ TEST_DATA = [ "uniqueid": "00:17:88:01:02:0e:32:a3-02-fc00", }, { - "entity_count": 1, - "device_count": 3, "entity_id": "sensor.dimmer_switch_3_battery", - "unique_id": "00:17:88:01:02:0e:32:a3-02-fc00-battery", - "state": "90", - "entity_category": EntityCategory.DIAGNOSTIC, - "device_class": SensorDeviceClass.BATTERY, - "state_class": SensorStateClass.MEASUREMENT, - "attributes": { - "state_class": "measurement", - "on": True, - "event_id": "dimmer_switch_3", - "unit_of_measurement": "%", - "device_class": "battery", - "friendly_name": "Dimmer switch 3 Battery", - }, "websocket_event": {"config": {"battery": 80}}, "next_state": "80", }, @@ -903,13 +609,16 @@ TEST_DATA = [ @pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: True}]) async def test_sensors( hass: HomeAssistant, - device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - config_entry_setup: ConfigEntry, + config_entry_factory: ConfigEntryFactoryType, sensor_ws_data: WebsocketDataType, expected: dict[str, Any], + snapshot: SnapshotAssertion, ) -> None: """Test successful creation of sensor entities.""" + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.SENSOR]): + config_entry = await config_entry_factory() + # Enable in entity registry if expected.get("enable_entity"): entity_registry.async_update_entity( @@ -923,32 +632,7 @@ async def test_sensors( ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == expected["entity_count"] - - # Verify entity state - sensor = hass.states.get(expected["entity_id"]) - assert sensor.state == expected["state"] - assert sensor.attributes.get(ATTR_DEVICE_CLASS) == expected["device_class"] - assert sensor.attributes == expected["attributes"] - - # Verify entity registry - assert ( - entity_registry.async_get(expected["entity_id"]).entity_category - is expected["entity_category"] - ) - ent_reg_entry = entity_registry.async_get(expected["entity_id"]) - assert ent_reg_entry.entity_category is expected["entity_category"] - assert ent_reg_entry.unique_id == expected["unique_id"] - - # Verify device registry - assert ( - len( - dr.async_entries_for_config_entry( - device_registry, config_entry_setup.entry_id - ) - ) - == expected["device_count"] - ) + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Change state @@ -957,12 +641,12 @@ async def test_sensors( # Unload entry - await hass.config_entries.async_unload(config_entry_setup.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE # Remove entry - await hass.config_entries.async_remove(config_entry_setup.entry_id) + await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 @@ -1021,17 +705,20 @@ async def test_not_allow_clip_sensor(hass: HomeAssistant) -> None: ) @pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: True}]) async def test_allow_clip_sensors( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory: ConfigEntryFactoryType, + snapshot: SnapshotAssertion, ) -> None: """Test that CLIP sensors can be allowed.""" - assert len(hass.states.async_all()) == 4 - assert hass.states.get("sensor.clip_light_level_sensor").state == "999.8" - assert hass.states.get("sensor.clip_flur").state == "0" + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.SENSOR]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Disallow clip sensors hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_ALLOW_CLIP_SENSOR: False} + config_entry, options={CONF_ALLOW_CLIP_SENSOR: False} ) await hass.async_block_till_done() @@ -1042,7 +729,7 @@ async def test_allow_clip_sensors( # Allow clip sensors hass.config_entries.async_update_entry( - config_entry_setup, options={CONF_ALLOW_CLIP_SENSOR: True} + config_entry, options={CONF_ALLOW_CLIP_SENSOR: True} ) await hass.async_block_till_done() From 3c4f2c2dcf2a0f8294e99da6f387985e1b3c4559 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 24 Jul 2024 18:07:40 +0200 Subject: [PATCH 1540/2411] Use snapshot in deCONZ select tests (#122541) --- .../deconz/snapshots/test_select.ambr | 508 ++++++++++++++++++ tests/components/deconz/test_select.py | 68 +-- 2 files changed, 522 insertions(+), 54 deletions(-) create mode 100644 tests/components/deconz/snapshots/test_select.ambr diff --git a/tests/components/deconz/snapshots/test_select.ambr b/tests/components/deconz/snapshots/test_select.ambr new file mode 100644 index 00000000000..12966709947 --- /dev/null +++ b/tests/components/deconz/snapshots/test_select.ambr @@ -0,0 +1,508 @@ +# serializer version: 1 +# name: test_select[sensor_payload0-expected0][select.aqara_fp1_device_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'leftright', + 'undirected', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.aqara_fp1_device_mode', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Aqara FP1 Device Mode', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-device_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_select[sensor_payload0-expected0][select.aqara_fp1_device_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aqara FP1 Device Mode', + 'options': list([ + 'leftright', + 'undirected', + ]), + }), + 'context': , + 'entity_id': 'select.aqara_fp1_device_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'undirected', + }) +# --- +# name: test_select[sensor_payload0-expected0][select.aqara_fp1_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'High', + 'Medium', + 'Low', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.aqara_fp1_sensitivity', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Aqara FP1 Sensitivity', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_select[sensor_payload0-expected0][select.aqara_fp1_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aqara FP1 Sensitivity', + 'options': list([ + 'High', + 'Medium', + 'Low', + ]), + }), + 'context': , + 'entity_id': 'select.aqara_fp1_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) +# --- +# name: test_select[sensor_payload0-expected0][select.aqara_fp1_trigger_distance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'far', + 'medium', + 'near', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.aqara_fp1_trigger_distance', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Aqara FP1 Trigger Distance', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-trigger_distance', + 'unit_of_measurement': None, + }) +# --- +# name: test_select[sensor_payload0-expected0][select.aqara_fp1_trigger_distance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aqara FP1 Trigger Distance', + 'options': list([ + 'far', + 'medium', + 'near', + ]), + }), + 'context': , + 'entity_id': 'select.aqara_fp1_trigger_distance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'medium', + }) +# --- +# name: test_select[sensor_payload1-expected1][select.aqara_fp1_device_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'leftright', + 'undirected', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.aqara_fp1_device_mode', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Aqara FP1 Device Mode', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-device_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_select[sensor_payload1-expected1][select.aqara_fp1_device_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aqara FP1 Device Mode', + 'options': list([ + 'leftright', + 'undirected', + ]), + }), + 'context': , + 'entity_id': 'select.aqara_fp1_device_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'undirected', + }) +# --- +# name: test_select[sensor_payload1-expected1][select.aqara_fp1_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'High', + 'Medium', + 'Low', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.aqara_fp1_sensitivity', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Aqara FP1 Sensitivity', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_select[sensor_payload1-expected1][select.aqara_fp1_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aqara FP1 Sensitivity', + 'options': list([ + 'High', + 'Medium', + 'Low', + ]), + }), + 'context': , + 'entity_id': 'select.aqara_fp1_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) +# --- +# name: test_select[sensor_payload1-expected1][select.aqara_fp1_trigger_distance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'far', + 'medium', + 'near', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.aqara_fp1_trigger_distance', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Aqara FP1 Trigger Distance', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-trigger_distance', + 'unit_of_measurement': None, + }) +# --- +# name: test_select[sensor_payload1-expected1][select.aqara_fp1_trigger_distance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aqara FP1 Trigger Distance', + 'options': list([ + 'far', + 'medium', + 'near', + ]), + }), + 'context': , + 'entity_id': 'select.aqara_fp1_trigger_distance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'medium', + }) +# --- +# name: test_select[sensor_payload2-expected2][select.aqara_fp1_device_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'leftright', + 'undirected', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.aqara_fp1_device_mode', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Aqara FP1 Device Mode', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-device_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_select[sensor_payload2-expected2][select.aqara_fp1_device_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aqara FP1 Device Mode', + 'options': list([ + 'leftright', + 'undirected', + ]), + }), + 'context': , + 'entity_id': 'select.aqara_fp1_device_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'undirected', + }) +# --- +# name: test_select[sensor_payload2-expected2][select.aqara_fp1_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'High', + 'Medium', + 'Low', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.aqara_fp1_sensitivity', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Aqara FP1 Sensitivity', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_select[sensor_payload2-expected2][select.aqara_fp1_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aqara FP1 Sensitivity', + 'options': list([ + 'High', + 'Medium', + 'Low', + ]), + }), + 'context': , + 'entity_id': 'select.aqara_fp1_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) +# --- +# name: test_select[sensor_payload2-expected2][select.aqara_fp1_trigger_distance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'far', + 'medium', + 'near', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.aqara_fp1_trigger_distance', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Aqara FP1 Trigger Distance', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-trigger_distance', + 'unit_of_measurement': None, + }) +# --- +# name: test_select[sensor_payload2-expected2][select.aqara_fp1_trigger_distance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aqara FP1 Trigger Distance', + 'options': list([ + 'far', + 'medium', + 'near', + ]), + }), + 'context': , + 'entity_id': 'select.aqara_fp1_trigger_distance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'medium', + }) +# --- diff --git a/tests/components/deconz/test_select.py b/tests/components/deconz/test_select.py index 3864af65cd4..cee133f9999 100644 --- a/tests/components/deconz/test_select.py +++ b/tests/components/deconz/test_select.py @@ -2,23 +2,27 @@ from collections.abc import Callable from typing import Any +from unittest.mock import patch from pydeconz.models.sensor.presence import ( PresenceConfigDeviceMode, PresenceConfigTriggerDistance, ) import pytest +from syrupy import SnapshotAssertion from homeassistant.components.select import ( ATTR_OPTION, DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, EntityCategory +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er +from .conftest import ConfigEntryFactoryType + +from tests.common import snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker TEST_DATA = [ @@ -47,15 +51,7 @@ TEST_DATA = [ "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", }, { - "entity_count": 5, - "device_count": 3, "entity_id": "select.aqara_fp1_device_mode", - "unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406-device_mode", - "entity_category": EntityCategory.CONFIG, - "attributes": { - "friendly_name": "Aqara FP1 Device Mode", - "options": ["leftright", "undirected"], - }, "option": PresenceConfigDeviceMode.LEFT_AND_RIGHT.value, "request": "/sensors/0/config", "request_data": {"devicemode": "leftright"}, @@ -86,15 +82,7 @@ TEST_DATA = [ "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", }, { - "entity_count": 5, - "device_count": 3, "entity_id": "select.aqara_fp1_sensitivity", - "unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406-sensitivity", - "entity_category": EntityCategory.CONFIG, - "attributes": { - "friendly_name": "Aqara FP1 Sensitivity", - "options": ["High", "Medium", "Low"], - }, "option": "Medium", "request": "/sensors/0/config", "request_data": {"sensitivity": 2}, @@ -125,15 +113,7 @@ TEST_DATA = [ "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406", }, { - "entity_count": 5, - "device_count": 3, "entity_id": "select.aqara_fp1_trigger_distance", - "unique_id": "xx:xx:xx:xx:xx:xx:xx:xx-01-0406-trigger_distance", - "entity_category": EntityCategory.CONFIG, - "attributes": { - "friendly_name": "Aqara FP1 Trigger Distance", - "options": ["far", "medium", "near"], - }, "option": PresenceConfigTriggerDistance.FAR.value, "request": "/sensors/0/config", "request_data": {"triggerdistance": "far"}, @@ -145,36 +125,16 @@ TEST_DATA = [ @pytest.mark.parametrize(("sensor_payload", "expected"), TEST_DATA) async def test_select( hass: HomeAssistant, - device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - config_entry_setup: ConfigEntry, + config_entry_factory: ConfigEntryFactoryType, mock_put_request: Callable[[str, str], AiohttpClientMocker], expected: dict[str, Any], + snapshot: SnapshotAssertion, ) -> None: """Test successful creation of button entities.""" - assert len(hass.states.async_all()) == expected["entity_count"] - - # Verify state data - - button = hass.states.get(expected["entity_id"]) - assert button.attributes == expected["attributes"] - - # Verify entity registry data - - ent_reg_entry = entity_registry.async_get(expected["entity_id"]) - assert ent_reg_entry.entity_category is expected["entity_category"] - assert ent_reg_entry.unique_id == expected["unique_id"] - - # Verify device registry data - - assert ( - len( - dr.async_entries_for_config_entry( - device_registry, config_entry_setup.entry_id - ) - ) - == expected["device_count"] - ) + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.SELECT]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) # Verify selecting option aioclient_mock = mock_put_request(expected["request"]) @@ -192,11 +152,11 @@ async def test_select( # Unload entry - await hass.config_entries.async_unload(config_entry_setup.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE # Remove entry - await hass.config_entries.async_remove(config_entry_setup.entry_id) + await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 From 3e8d3083ac5af9d745a8619680336c753cd74463 Mon Sep 17 00:00:00 2001 From: Ian Date: Wed, 24 Jul 2024 09:18:21 -0700 Subject: [PATCH 1541/2411] Refactor NextBus integration to use new API (#121133) * Refactor NextBus integration to use new API This removes the `messages`, `directions`, and `attribution` attributes from the sensor. Those may be added back in the future with additional refactoring. Some existing sensors may be broken today because of deprecated Agency names. This patch will not migrate them as the migration path is ambiguous. Setting up again should work though. * Move result indexing outside of try/except --- .../components/nextbus/config_flow.py | 47 ++--- .../components/nextbus/coordinator.py | 61 +++--- .../components/nextbus/manifest.json | 2 +- homeassistant/components/nextbus/sensor.py | 40 ++-- homeassistant/components/nextbus/util.py | 9 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nextbus/conftest.py | 96 +++++++-- tests/components/nextbus/test_config_flow.py | 8 +- tests/components/nextbus/test_sensor.py | 188 ++++-------------- 10 files changed, 191 insertions(+), 264 deletions(-) diff --git a/homeassistant/components/nextbus/config_flow.py b/homeassistant/components/nextbus/config_flow.py index c7e5ed3f36f..05290733bd9 100644 --- a/homeassistant/components/nextbus/config_flow.py +++ b/homeassistant/components/nextbus/config_flow.py @@ -37,52 +37,33 @@ def _dict_to_select_selector(options: dict[str, str]) -> SelectSelector: def _get_agency_tags(client: NextBusClient) -> dict[str, str]: - return {a["tag"]: a["title"] for a in client.get_agency_list()["agency"]} + return {a["id"]: a["name"] for a in client.agencies()} def _get_route_tags(client: NextBusClient, agency_tag: str) -> dict[str, str]: - return {a["tag"]: a["title"] for a in client.get_route_list(agency_tag)["route"]} + return {a["id"]: a["title"] for a in client.routes(agency_tag)} def _get_stop_tags( client: NextBusClient, agency_tag: str, route_tag: str ) -> dict[str, str]: - route_config = client.get_route_config(route_tag, agency_tag) - tags = {a["tag"]: a["title"] for a in route_config["route"]["stop"]} - title_counts = Counter(tags.values()) + route_config = client.route_details(route_tag, agency_tag) + stop_ids = {a["id"]: a["name"] for a in route_config["stops"]} + title_counts = Counter(stop_ids.values()) stop_directions: dict[str, str] = {} - for direction in listify(route_config["route"]["direction"]): - for stop in direction["stop"]: - stop_directions[stop["tag"]] = direction["name"] + for direction in listify(route_config["directions"]): + if not direction["useForUi"]: + continue + for stop in direction["stops"]: + stop_directions[stop] = direction["name"] # Append directions for stops with shared titles - for tag, title in tags.items(): + for stop_id, title in stop_ids.items(): if title_counts[title] > 1: - tags[tag] = f"{title} ({stop_directions.get(tag, tag)})" + stop_ids[stop_id] = f"{title} ({stop_directions.get(stop_id, stop_id)})" - return tags - - -def _validate_import( - client: NextBusClient, agency_tag: str, route_tag: str, stop_tag: str -) -> str | tuple[str, str, str]: - agency_tags = _get_agency_tags(client) - agency = agency_tags.get(agency_tag) - if not agency: - return "invalid_agency" - - route_tags = _get_route_tags(client, agency_tag) - route = route_tags.get(route_tag) - if not route: - return "invalid_route" - - stop_tags = _get_stop_tags(client, agency_tag, route_tag) - stop = stop_tags.get(stop_tag) - if not stop: - return "invalid_stop" - - return agency, route, stop + return stop_ids def _unique_id_from_data(data: dict[str, str]) -> str: @@ -101,7 +82,7 @@ class NextBusFlowHandler(ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize NextBus config flow.""" self.data: dict[str, str] = {} - self._client = NextBusClient(output_format="json") + self._client = NextBusClient() async def async_step_user( self, diff --git a/homeassistant/components/nextbus/coordinator.py b/homeassistant/components/nextbus/coordinator.py index 15377bce56b..6c438f6f808 100644 --- a/homeassistant/components/nextbus/coordinator.py +++ b/homeassistant/components/nextbus/coordinator.py @@ -2,16 +2,16 @@ from datetime import timedelta import logging -from typing import Any, cast +from typing import Any from py_nextbus import NextBusClient -from py_nextbus.client import NextBusFormatError, NextBusHTTPError, RouteStop +from py_nextbus.client import NextBusFormatError, NextBusHTTPError from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN -from .util import listify +from .util import RouteStop _LOGGER = logging.getLogger(__name__) @@ -27,53 +27,48 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator): name=DOMAIN, update_interval=timedelta(seconds=30), ) - self.client = NextBusClient(output_format="json", agency=agency) + self.client = NextBusClient(agency_id=agency) self._agency = agency - self._stop_routes: set[RouteStop] = set() + self._route_stops: set[RouteStop] = set() self._predictions: dict[RouteStop, dict[str, Any]] = {} - def add_stop_route(self, stop_tag: str, route_tag: str) -> None: + def add_stop_route(self, stop_id: str, route_id: str) -> None: """Tell coordinator to start tracking a given stop and route.""" - self._stop_routes.add(RouteStop(route_tag, stop_tag)) + self._route_stops.add(RouteStop(route_id, stop_id)) - def remove_stop_route(self, stop_tag: str, route_tag: str) -> None: + def remove_stop_route(self, stop_id: str, route_id: str) -> None: """Tell coordinator to stop tracking a given stop and route.""" - self._stop_routes.remove(RouteStop(route_tag, stop_tag)) + self._route_stops.remove(RouteStop(route_id, stop_id)) - def get_prediction_data( - self, stop_tag: str, route_tag: str - ) -> dict[str, Any] | None: + def get_prediction_data(self, stop_id: str, route_id: str) -> dict[str, Any] | None: """Get prediction result for a given stop and route.""" - return self._predictions.get(RouteStop(route_tag, stop_tag)) - - def _calc_predictions(self, data: dict[str, Any]) -> None: - self._predictions = { - RouteStop(prediction["routeTag"], prediction["stopTag"]): prediction - for prediction in listify(data.get("predictions", [])) - } - - def get_attribution(self) -> str | None: - """Get attribution from api results.""" - return self.data.get("copyright") + return self._predictions.get(RouteStop(route_id, stop_id)) def has_routes(self) -> bool: """Check if this coordinator is tracking any routes.""" - return len(self._stop_routes) > 0 + return len(self._route_stops) > 0 async def _async_update_data(self) -> dict[str, Any]: """Fetch data from NextBus.""" - self.logger.debug("Updating data from API. Routes: %s", str(self._stop_routes)) + self.logger.debug("Updating data from API. Routes: %s", str(self._route_stops)) def _update_data() -> dict: """Fetch data from NextBus.""" self.logger.debug("Updating data from API (executor)") - try: - data = self.client.get_predictions_for_multi_stops(self._stop_routes) - # Casting here because we expect dict and not a str due to the input format selected being JSON - data = cast(dict[str, Any], data) - self._calc_predictions(data) - except (NextBusHTTPError, NextBusFormatError) as ex: - raise UpdateFailed("Failed updating nextbus data", ex) from ex - return data + predictions: dict[RouteStop, dict[str, Any]] = {} + for route_stop in self._route_stops: + prediction_results: list[dict[str, Any]] = [] + try: + prediction_results = self.client.predictions_for_stop( + route_stop.stop_id, route_stop.route_id + ) + except (NextBusHTTPError, NextBusFormatError) as ex: + raise UpdateFailed("Failed updating nextbus data", ex) from ex + + if prediction_results: + predictions[route_stop] = prediction_results[0] + self._predictions = predictions + + return predictions return await self.hass.async_add_executor_job(_update_data) diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index d8f4018ada2..27fec1bfba9 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nextbus", "iot_class": "cloud_polling", "loggers": ["py_nextbus"], - "requirements": ["py-nextbusnext==1.0.2"] + "requirements": ["py-nextbusnext==2.0.3"] } diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 8cd0d177835..8ef5323858f 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations -from itertools import chain import logging from typing import cast @@ -16,7 +15,7 @@ from homeassistant.util.dt import utc_from_timestamp from .const import CONF_AGENCY, CONF_ROUTE, DOMAIN from .coordinator import NextBusDataUpdateCoordinator -from .util import listify, maybe_first +from .util import maybe_first _LOGGER = logging.getLogger(__name__) @@ -76,7 +75,11 @@ class NextBusDepartureSensor( self.agency = agency self.route = route self.stop = stop - self._attr_extra_state_attributes: dict[str, str] = {} + self._attr_extra_state_attributes: dict[str, str] = { + "agency": agency, + "route": route, + "stop": stop, + } self._attr_unique_id = unique_id self._attr_name = name @@ -99,11 +102,10 @@ class NextBusDepartureSensor( def _handle_coordinator_update(self) -> None: """Update sensor with new departures times.""" results = self.coordinator.get_prediction_data(self.stop, self.route) - self._attr_attribution = self.coordinator.get_attribution() self._log_debug("Predictions results: %s", results) - if not results or "Error" in results: + if not results: self._log_err("Error getting predictions: %s", str(results)) self._attr_native_value = None self._attr_extra_state_attributes.pop("upcoming", None) @@ -112,31 +114,13 @@ class NextBusDepartureSensor( # Set detailed attributes self._attr_extra_state_attributes.update( { - "agency": str(results.get("agencyTitle")), - "route": str(results.get("routeTitle")), - "stop": str(results.get("stopTitle")), + "route": str(results["route"]["title"]), + "stop": str(results["stop"]["name"]), } ) - # List all messages in the attributes - messages = listify(results.get("message", [])) - self._log_debug("Messages: %s", messages) - self._attr_extra_state_attributes["message"] = " -- ".join( - message.get("text", "") for message in messages - ) - - # List out all directions in the attributes - directions = listify(results.get("direction", [])) - self._attr_extra_state_attributes["direction"] = ", ".join( - direction.get("title", "") for direction in directions - ) - # Chain all predictions together - predictions = list( - chain( - *(listify(direction.get("prediction", [])) for direction in directions) - ) - ) + predictions = results["values"] # Short circuit if we don't have any actual bus predictions if not predictions: @@ -146,12 +130,12 @@ class NextBusDepartureSensor( else: # Generate list of upcoming times self._attr_extra_state_attributes["upcoming"] = ", ".join( - sorted((p["minutes"] for p in predictions), key=int) + str(p["minutes"]) for p in predictions ) latest_prediction = maybe_first(predictions) self._attr_native_value = utc_from_timestamp( - int(latest_prediction["epochTime"]) / 1000 + latest_prediction["timestamp"] / 1000 ) self.async_write_ha_state() diff --git a/homeassistant/components/nextbus/util.py b/homeassistant/components/nextbus/util.py index e9a1e1fd254..814e3a9294c 100644 --- a/homeassistant/components/nextbus/util.py +++ b/homeassistant/components/nextbus/util.py @@ -1,6 +1,6 @@ """Utils for NextBus integration module.""" -from typing import Any +from typing import Any, NamedTuple def listify(maybe_list: Any) -> list[Any]: @@ -24,3 +24,10 @@ def maybe_first(maybe_list: list[Any] | None) -> Any: return maybe_list[0] return maybe_list + + +class RouteStop(NamedTuple): + """NamedTuple for a route and stop combination.""" + + route_id: str + stop_id: str diff --git a/requirements_all.txt b/requirements_all.txt index 2ebe8be165a..05010ccacae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1644,7 +1644,7 @@ py-madvr2==1.6.29 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==1.0.2 +py-nextbusnext==2.0.3 # homeassistant.components.nightscout py-nightscout==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e91d2334b8..8b7344dd3eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1333,7 +1333,7 @@ py-madvr2==1.6.29 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==1.0.2 +py-nextbusnext==2.0.3 # homeassistant.components.nightscout py-nightscout==1.2.2 diff --git a/tests/components/nextbus/conftest.py b/tests/components/nextbus/conftest.py index 84445905c2e..231faccf907 100644 --- a/tests/components/nextbus/conftest.py +++ b/tests/components/nextbus/conftest.py @@ -8,15 +8,32 @@ import pytest @pytest.fixture( params=[ - {"name": "Outbound", "stop": [{"tag": "5650"}]}, [ { "name": "Outbound", - "stop": [{"tag": "5650"}], + "shortName": "Outbound", + "useForUi": True, + "stops": ["5184"], + }, + { + "name": "Outbound - Hidden", + "shortName": "Outbound - Hidden", + "useForUi": False, + "stops": ["5651"], + }, + ], + [ + { + "name": "Outbound", + "shortName": "Outbound", + "useForUi": True, + "stops": ["5184"], }, { "name": "Inbound", - "stop": [{"tag": "5651"}], + "shortName": "Inbound", + "useForUi": True, + "stops": ["5651"], }, ], ] @@ -35,22 +52,65 @@ def mock_nextbus_lists( ) -> MagicMock: """Mock all list functions in nextbus to test validate logic.""" instance = mock_nextbus.return_value - instance.get_agency_list.return_value = { - "agency": [{"tag": "sf-muni", "title": "San Francisco Muni"}] - } - instance.get_route_list.return_value = { - "route": [{"tag": "F", "title": "F - Market & Wharves"}] - } - instance.get_route_config.return_value = { - "route": { - "stop": [ - {"tag": "5650", "title": "Market St & 7th St"}, - {"tag": "5651", "title": "Market St & 7th St"}, - # Error case test. Duplicate title with no unique direction - {"tag": "5652", "title": "Market St & 7th St"}, - ], - "direction": route_config_direction, + instance.agencies.return_value = [ + { + "id": "sfmta-cis", + "name": "San Francisco Muni CIS", + "shortName": "SF Muni CIS", + "region": "", + "website": "", + "logo": "", + "nxbs2RedirectUrl": "", } + ] + + instance.routes.return_value = [ + { + "id": "F", + "rev": 1057, + "title": "F Market & Wharves", + "description": "7am-10pm daily", + "color": "", + "textColor": "", + "hidden": False, + "timestamp": "2024-06-23T03:06:58Z", + }, + ] + + instance.route_details.return_value = { + "id": "F", + "rev": 1057, + "title": "F Market & Wharves", + "description": "7am-10pm daily", + "color": "", + "textColor": "", + "hidden": False, + "boundingBox": {}, + "stops": [ + { + "id": "5184", + "lat": 37.8071299, + "lon": -122.41732, + "name": "Jones St & Beach St", + "code": "15184", + "hidden": False, + "showDestinationSelector": True, + "directions": ["F_0_var1", "F_0_var0"], + }, + { + "id": "5651", + "lat": 37.8071299, + "lon": -122.41732, + "name": "Jones St & Beach St", + "code": "15651", + "hidden": False, + "showDestinationSelector": True, + "directions": ["F_0_var1", "F_0_var0"], + }, + ], + "directions": route_config_direction, + "paths": [], + "timestamp": "2024-06-23T03:06:58Z", } return instance diff --git a/tests/components/nextbus/test_config_flow.py b/tests/components/nextbus/test_config_flow.py index da8e47ff3e8..4e5b933a189 100644 --- a/tests/components/nextbus/test_config_flow.py +++ b/tests/components/nextbus/test_config_flow.py @@ -44,7 +44,7 @@ async def test_user_config( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_AGENCY: "sf-muni", + CONF_AGENCY: "sfmta-cis", }, ) await hass.async_block_till_done() @@ -68,16 +68,16 @@ async def test_user_config( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_STOP: "5650", + CONF_STOP: "5184", }, ) await hass.async_block_till_done() assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("data") == { - "agency": "sf-muni", + "agency": "sfmta-cis", "route": "F", - "stop": "5650", + "stop": "5184", } assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index 7cdcd58937a..dd0346c3e7a 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -18,9 +18,9 @@ from homeassistant.helpers.update_coordinator import UpdateFailed from tests.common import MockConfigEntry -VALID_AGENCY = "sf-muni" +VALID_AGENCY = "sfmta-cis" VALID_ROUTE = "F" -VALID_STOP = "5650" +VALID_STOP = "5184" VALID_AGENCY_TITLE = "San Francisco Muni" VALID_ROUTE_TITLE = "F-Market & Wharves" VALID_STOP_TITLE = "Market St & 7th St" @@ -44,25 +44,38 @@ CONFIG_BASIC = { } } -BASIC_RESULTS = { - "predictions": { - "agencyTitle": VALID_AGENCY_TITLE, - "agencyTag": VALID_AGENCY, - "routeTitle": VALID_ROUTE_TITLE, - "routeTag": VALID_ROUTE, - "stopTitle": VALID_STOP_TITLE, - "stopTag": VALID_STOP, - "direction": { - "title": "Outbound", - "prediction": [ - {"minutes": "1", "epochTime": "1553807371000"}, - {"minutes": "2", "epochTime": "1553807372000"}, - {"minutes": "3", "epochTime": "1553807373000"}, - {"minutes": "10", "epochTime": "1553807380000"}, - ], +BASIC_RESULTS = [ + { + "route": { + "title": VALID_ROUTE_TITLE, + "id": VALID_ROUTE, }, + "stop": { + "name": VALID_STOP_TITLE, + "id": VALID_STOP, + }, + "values": [ + {"minutes": 1, "timestamp": 1553807371000}, + {"minutes": 2, "timestamp": 1553807372000}, + {"minutes": 3, "timestamp": 1553807373000}, + {"minutes": 10, "timestamp": 1553807380000}, + ], } -} +] + +NO_UPCOMING = [ + { + "route": { + "title": VALID_ROUTE_TITLE, + "id": VALID_ROUTE, + }, + "stop": { + "name": VALID_STOP_TITLE, + "id": VALID_STOP, + }, + "values": [], + } +] @pytest.fixture @@ -78,9 +91,9 @@ def mock_nextbus_predictions( ) -> Generator[MagicMock]: """Create a mock of NextBusClient predictions.""" instance = mock_nextbus.return_value - instance.get_predictions_for_multi_stops.return_value = BASIC_RESULTS + instance.predictions_for_stop.return_value = BASIC_RESULTS - return instance.get_predictions_for_multi_stops + return instance.predictions_for_stop async def assert_setup_sensor( @@ -105,117 +118,23 @@ async def assert_setup_sensor( return config_entry -async def test_message_dict( - hass: HomeAssistant, - mock_nextbus: MagicMock, - mock_nextbus_lists: MagicMock, - mock_nextbus_predictions: MagicMock, -) -> None: - """Verify that a single dict message is rendered correctly.""" - mock_nextbus_predictions.return_value = { - "predictions": { - "agencyTitle": VALID_AGENCY_TITLE, - "agencyTag": VALID_AGENCY, - "routeTitle": VALID_ROUTE_TITLE, - "routeTag": VALID_ROUTE, - "stopTitle": VALID_STOP_TITLE, - "stopTag": VALID_STOP, - "message": {"text": "Message"}, - "direction": { - "title": "Outbound", - "prediction": [ - {"minutes": "1", "epochTime": "1553807371000"}, - {"minutes": "2", "epochTime": "1553807372000"}, - {"minutes": "3", "epochTime": "1553807373000"}, - ], - }, - } - } - - await assert_setup_sensor(hass, CONFIG_BASIC) - - state = hass.states.get(SENSOR_ID) - assert state is not None - assert state.attributes["message"] == "Message" - - -async def test_message_list( +async def test_predictions( hass: HomeAssistant, mock_nextbus: MagicMock, mock_nextbus_lists: MagicMock, mock_nextbus_predictions: MagicMock, ) -> None: """Verify that a list of messages are rendered correctly.""" - mock_nextbus_predictions.return_value = { - "predictions": { - "agencyTitle": VALID_AGENCY_TITLE, - "agencyTag": VALID_AGENCY, - "routeTitle": VALID_ROUTE_TITLE, - "routeTag": VALID_ROUTE, - "stopTitle": VALID_STOP_TITLE, - "stopTag": VALID_STOP, - "message": [{"text": "Message 1"}, {"text": "Message 2"}], - "direction": { - "title": "Outbound", - "prediction": [ - {"minutes": "1", "epochTime": "1553807371000"}, - {"minutes": "2", "epochTime": "1553807372000"}, - {"minutes": "3", "epochTime": "1553807373000"}, - ], - }, - } - } - - await assert_setup_sensor(hass, CONFIG_BASIC) - - state = hass.states.get(SENSOR_ID) - assert state is not None - assert state.attributes["message"] == "Message 1 -- Message 2" - - -async def test_direction_list( - hass: HomeAssistant, - mock_nextbus: MagicMock, - mock_nextbus_lists: MagicMock, - mock_nextbus_predictions: MagicMock, -) -> None: - """Verify that a list of messages are rendered correctly.""" - mock_nextbus_predictions.return_value = { - "predictions": { - "agencyTitle": VALID_AGENCY_TITLE, - "agencyTag": VALID_AGENCY, - "routeTitle": VALID_ROUTE_TITLE, - "routeTag": VALID_ROUTE, - "stopTitle": VALID_STOP_TITLE, - "stopTag": VALID_STOP, - "message": [{"text": "Message 1"}, {"text": "Message 2"}], - "direction": [ - { - "title": "Outbound", - "prediction": [ - {"minutes": "1", "epochTime": "1553807371000"}, - {"minutes": "2", "epochTime": "1553807372000"}, - {"minutes": "3", "epochTime": "1553807373000"}, - ], - }, - { - "title": "Outbound 2", - "prediction": {"minutes": "0", "epochTime": "1553807374000"}, - }, - ], - } - } await assert_setup_sensor(hass, CONFIG_BASIC) state = hass.states.get(SENSOR_ID) assert state is not None assert state.state == "2019-03-28T21:09:31+00:00" - assert state.attributes["agency"] == VALID_AGENCY_TITLE + assert state.attributes["agency"] == VALID_AGENCY assert state.attributes["route"] == VALID_ROUTE_TITLE assert state.attributes["stop"] == VALID_STOP_TITLE - assert state.attributes["direction"] == "Outbound, Outbound 2" - assert state.attributes["upcoming"] == "0, 1, 2, 3" + assert state.attributes["upcoming"] == "1, 2, 3, 10" @pytest.mark.parametrize( @@ -256,27 +175,19 @@ async def test_custom_name( assert state.name == "Custom Name" -@pytest.mark.parametrize( - "prediction_results", - [ - {}, - {"Error": "Failed"}, - ], -) -async def test_no_predictions( +async def test_verify_no_predictions( hass: HomeAssistant, mock_nextbus: MagicMock, - mock_nextbus_predictions: MagicMock, mock_nextbus_lists: MagicMock, - prediction_results: dict[str, str], + mock_nextbus_predictions: MagicMock, ) -> None: - """Verify there are no exceptions when no predictions are returned.""" - mock_nextbus_predictions.return_value = prediction_results - + """Verify attributes are set despite no upcoming times.""" + mock_nextbus_predictions.return_value = [] await assert_setup_sensor(hass, CONFIG_BASIC) state = hass.states.get(SENSOR_ID) assert state is not None + assert "upcoming" not in state.attributes assert state.state == "unknown" @@ -287,21 +198,10 @@ async def test_verify_no_upcoming( mock_nextbus_predictions: MagicMock, ) -> None: """Verify attributes are set despite no upcoming times.""" - mock_nextbus_predictions.return_value = { - "predictions": { - "agencyTitle": VALID_AGENCY_TITLE, - "agencyTag": VALID_AGENCY, - "routeTitle": VALID_ROUTE_TITLE, - "routeTag": VALID_ROUTE, - "stopTitle": VALID_STOP_TITLE, - "stopTag": VALID_STOP, - "direction": {"title": "Outbound", "prediction": []}, - } - } - + mock_nextbus_predictions.return_value = NO_UPCOMING await assert_setup_sensor(hass, CONFIG_BASIC) state = hass.states.get(SENSOR_ID) assert state is not None - assert state.state == "unknown" assert state.attributes["upcoming"] == "No upcoming predictions" + assert state.state == "unknown" From 943b1afb557f7d3cc61568c4367f26ecdae3c598 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Wed, 24 Jul 2024 17:19:12 +0100 Subject: [PATCH 1542/2411] Fix target service attribute on Mastodon integration (#122546) * Fix target * Fix --- homeassistant/components/mastodon/notify.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mastodon/notify.py b/homeassistant/components/mastodon/notify.py index f15b8c6f0ab..99999275aeb 100644 --- a/homeassistant/components/mastodon/notify.py +++ b/homeassistant/components/mastodon/notify.py @@ -3,7 +3,7 @@ from __future__ import annotations import mimetypes -from typing import Any +from typing import Any, cast from mastodon import Mastodon from mastodon.Mastodon import MastodonAPIError, MastodonUnauthorizedError @@ -71,11 +71,15 @@ class MastodonNotificationService(BaseNotificationService): def send_message(self, message: str = "", **kwargs: Any) -> None: """Toot a message, with media perhaps.""" + + target = None + if (target_list := kwargs.get(ATTR_TARGET)) is not None: + target = cast(list[str], target_list)[0] + data = kwargs.get(ATTR_DATA) media = None mediadata = None - target = None sensitive = False content_warning = None @@ -87,7 +91,6 @@ class MastodonNotificationService(BaseNotificationService): return mediadata = self._upload_media(media) - target = data.get(ATTR_TARGET) sensitive = data.get(ATTR_MEDIA_WARNING) content_warning = data.get(ATTR_CONTENT_WARNING) From be8e432beab9b6f1efe2e8177ceb6b8deca7471a Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 24 Jul 2024 20:08:06 +0200 Subject: [PATCH 1543/2411] Use snapshot in deCONZ alarm control panel tests (#122551) * Use snapshot in deCONZ alarm control panel tests * Clean up comments --- .../snapshots/test_alarm_control_panel.ambr | 51 ++++++ .../deconz/test_alarm_control_panel.py | 164 ++++++------------ 2 files changed, 105 insertions(+), 110 deletions(-) create mode 100644 tests/components/deconz/snapshots/test_alarm_control_panel.ambr diff --git a/tests/components/deconz/snapshots/test_alarm_control_panel.ambr b/tests/components/deconz/snapshots/test_alarm_control_panel.ambr new file mode 100644 index 00000000000..86b97a62dfe --- /dev/null +++ b/tests/components/deconz/snapshots/test_alarm_control_panel.ambr @@ -0,0 +1,51 @@ +# serializer version: 1 +# name: test_alarm_control_panel[sensor_payload0-alarm_system_payload0][alarm_control_panel.keypad-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'alarm_control_panel', + 'entity_category': None, + 'entity_id': 'alarm_control_panel.keypad', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Keypad', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_alarm_control_panel[sensor_payload0-alarm_system_payload0][alarm_control_panel.keypad-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'changed_by': None, + 'code_arm_required': True, + 'code_format': , + 'friendly_name': 'Keypad', + 'supported_features': , + }), + 'context': , + 'entity_id': 'alarm_control_panel.keypad', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py index 3c901d94593..712dddc7225 100644 --- a/tests/components/deconz/test_alarm_control_panel.py +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -1,14 +1,15 @@ """deCONZ alarm control panel platform tests.""" from collections.abc import Callable +from unittest.mock import patch from pydeconz.models.sensor.ancillary_control import AncillaryControlPanel import pytest +from syrupy import SnapshotAssertion from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, @@ -24,12 +25,14 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, - STATE_UNKNOWN, + Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er -from .conftest import WebsocketDataType +from .conftest import ConfigEntryFactoryType, WebsocketDataType +from tests.common import snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker @@ -99,125 +102,66 @@ from tests.test_util.aiohttp import AiohttpClientMocker ) async def test_alarm_control_panel( hass: HomeAssistant, + entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, + config_entry_factory: ConfigEntryFactoryType, mock_put_request: Callable[[str, str], AiohttpClientMocker], sensor_ws_data: WebsocketDataType, + snapshot: SnapshotAssertion, ) -> None: """Test successful creation of alarm control panel entities.""" - assert len(hass.states.async_all()) == 4 - assert hass.states.get("alarm_control_panel.keypad").state == STATE_UNKNOWN - - # Event signals alarm control panel armed away - - await sensor_ws_data({"state": {"panel": AncillaryControlPanel.ARMED_AWAY}}) - assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_AWAY - - # Event signals alarm control panel armed night - - await sensor_ws_data({"state": {"panel": AncillaryControlPanel.ARMED_NIGHT}}) - assert ( - hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_NIGHT - ) - - # Event signals alarm control panel armed home - - await sensor_ws_data({"state": {"panel": AncillaryControlPanel.ARMED_STAY}}) - assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMED_HOME - - # Event signals alarm control panel disarmed - - await sensor_ws_data({"state": {"panel": AncillaryControlPanel.DISARMED}}) - assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_DISARMED - - # Event signals alarm control panel arming - - for arming_event in ( - AncillaryControlPanel.ARMING_AWAY, - AncillaryControlPanel.ARMING_NIGHT, - AncillaryControlPanel.ARMING_STAY, + with patch( + "homeassistant.components.deconz.PLATFORMS", [Platform.ALARM_CONTROL_PANEL] ): - await sensor_ws_data({"state": {"panel": arming_event}}) - assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_ARMING + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) - # Event signals alarm control panel pending - - for pending_event in ( - AncillaryControlPanel.ENTRY_DELAY, - AncillaryControlPanel.EXIT_DELAY, + for action, state in ( + # Event signals alarm control panel armed state + (AncillaryControlPanel.ARMED_AWAY, STATE_ALARM_ARMED_AWAY), + (AncillaryControlPanel.ARMED_NIGHT, STATE_ALARM_ARMED_NIGHT), + (AncillaryControlPanel.ARMED_STAY, STATE_ALARM_ARMED_HOME), + (AncillaryControlPanel.DISARMED, STATE_ALARM_DISARMED), + # Event signals alarm control panel arming state + (AncillaryControlPanel.ARMING_AWAY, STATE_ALARM_ARMING), + (AncillaryControlPanel.ARMING_NIGHT, STATE_ALARM_ARMING), + (AncillaryControlPanel.ARMING_STAY, STATE_ALARM_ARMING), + # Event signals alarm control panel pending state + (AncillaryControlPanel.ENTRY_DELAY, STATE_ALARM_PENDING), + (AncillaryControlPanel.EXIT_DELAY, STATE_ALARM_PENDING), + # Event signals alarm control panel triggered state + (AncillaryControlPanel.IN_ALARM, STATE_ALARM_TRIGGERED), + # Event signals alarm control panel unknown state keeps previous state + (AncillaryControlPanel.NOT_READY, STATE_ALARM_TRIGGERED), ): - await sensor_ws_data({"state": {"panel": pending_event}}) - assert ( - hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_PENDING - ) - - # Event signals alarm control panel triggered - - await sensor_ws_data({"state": {"panel": AncillaryControlPanel.IN_ALARM}}) - assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_TRIGGERED - - # Event signals alarm control panel unknown state keeps previous state - - await sensor_ws_data({"state": {"panel": AncillaryControlPanel.NOT_READY}}) - assert hass.states.get("alarm_control_panel.keypad").state == STATE_ALARM_TRIGGERED + await sensor_ws_data({"state": {"panel": action}}) + assert hass.states.get("alarm_control_panel.keypad").state == state # Verify service calls - # Service set alarm to away mode + for path, service, code in ( + # Service set alarm to away mode + ("arm_away", SERVICE_ALARM_ARM_AWAY, "1234"), + # Service set alarm to home mode + ("arm_stay", SERVICE_ALARM_ARM_HOME, "2345"), + # Service set alarm to night mode + ("arm_night", SERVICE_ALARM_ARM_NIGHT, "3456"), + # Service set alarm to disarmed + ("disarm", SERVICE_ALARM_DISARM, "4567"), + ): + aioclient_mock.mock_calls.clear() + aioclient_mock = mock_put_request(f"/alarmsystems/0/{path}") + await hass.services.async_call( + ALARM_CONTROL_PANEL_DOMAIN, + service, + {ATTR_ENTITY_ID: "alarm_control_panel.keypad", ATTR_CODE: code}, + blocking=True, + ) + assert aioclient_mock.mock_calls[0][2] == {"code0": code} - aioclient_mock = mock_put_request("/alarmsystems/0/arm_away") + await hass.config_entries.async_unload(config_entry.entry_id) + assert hass.states.get("alarm_control_panel.keypad").state == STATE_UNAVAILABLE - await hass.services.async_call( - ALARM_CONTROL_PANEL_DOMAIN, - SERVICE_ALARM_ARM_AWAY, - {ATTR_ENTITY_ID: "alarm_control_panel.keypad", ATTR_CODE: "1234"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[1][2] == {"code0": "1234"} - - # Service set alarm to home mode - - aioclient_mock = mock_put_request("/alarmsystems/0/arm_stay") - - await hass.services.async_call( - ALARM_CONTROL_PANEL_DOMAIN, - SERVICE_ALARM_ARM_HOME, - {ATTR_ENTITY_ID: "alarm_control_panel.keypad", ATTR_CODE: "2345"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[2][2] == {"code0": "2345"} - - # Service set alarm to night mode - - aioclient_mock = mock_put_request("/alarmsystems/0/arm_night") - - await hass.services.async_call( - ALARM_CONTROL_PANEL_DOMAIN, - SERVICE_ALARM_ARM_NIGHT, - {ATTR_ENTITY_ID: "alarm_control_panel.keypad", ATTR_CODE: "3456"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[3][2] == {"code0": "3456"} - - # Service set alarm to disarmed - - aioclient_mock = mock_put_request("/alarmsystems/0/disarm") - - await hass.services.async_call( - ALARM_CONTROL_PANEL_DOMAIN, - SERVICE_ALARM_DISARM, - {ATTR_ENTITY_ID: "alarm_control_panel.keypad", ATTR_CODE: "4567"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[4][2] == {"code0": "4567"} - - await hass.config_entries.async_unload(config_entry_setup.entry_id) - - states = hass.states.async_all() - assert len(states) == 4 - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry_setup.entry_id) + await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 From 919823446580594411863e6429a13e02d2d99271 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 24 Jul 2024 20:08:42 +0200 Subject: [PATCH 1544/2411] Use snapshot in deCONZ light tests (#122548) --- .../deconz/snapshots/test_light.ambr | 2971 +++++++++++++++++ tests/components/deconz/test_light.py | 256 +- 2 files changed, 3002 insertions(+), 225 deletions(-) create mode 100644 tests/components/deconz/snapshots/test_light.ambr diff --git a/tests/components/deconz/snapshots/test_light.ambr b/tests/components/deconz/snapshots/test_light.ambr new file mode 100644 index 00000000000..46b6611dcbe --- /dev/null +++ b/tests/components/deconz/snapshots/test_light.ambr @@ -0,0 +1,2971 @@ +# serializer version: 1 +# name: test_groups[input0-expected0-light_payload0][light.dimmable_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.dimmable_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Dimmable light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:02-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input0-expected0-light_payload0][light.dimmable_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'friendly_name': 'Dimmable light', + 'is_deconz_group': False, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.dimmable_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input0-expected0-light_payload0][light.group-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.group', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '01234E56789A-/groups/0', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input0-expected0-light_payload0][light.group-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_on': False, + 'brightness': 255, + 'color_mode': , + 'color_temp': 2500, + 'color_temp_kelvin': 400, + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'Group', + 'hs_color': tuple( + 15.981, + 100.0, + ), + 'is_deconz_group': True, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 67, + 0, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.674, + 0.322, + ), + }), + 'context': , + 'entity_id': 'light.group', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input0-expected0-light_payload0][light.rgb_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.rgb_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'RGB light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input0-expected0-light_payload0][light.rgb_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 50, + 'color_mode': , + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'RGB light', + 'hs_color': tuple( + 52.0, + 100.0, + ), + 'is_deconz_group': False, + 'rgb_color': tuple( + 255, + 221, + 0, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.5, + 0.5, + ), + }), + 'context': , + 'entity_id': 'light.rgb_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input0-expected0-light_payload0][light.tunable_white_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6451, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 155, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.tunable_white_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tunable white light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input0-expected0-light_payload0][light.tunable_white_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': , + 'color_temp': 2500, + 'color_temp_kelvin': 400, + 'friendly_name': 'Tunable white light', + 'hs_color': tuple( + 15.981, + 100.0, + ), + 'is_deconz_group': False, + 'max_color_temp_kelvin': 6451, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 155, + 'rgb_color': tuple( + 255, + 67, + 0, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.674, + 0.322, + ), + }), + 'context': , + 'entity_id': 'light.tunable_white_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input0-light_payload0][light.dimmable_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.dimmable_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Dimmable light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:02-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input0-light_payload0][light.dimmable_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'friendly_name': 'Dimmable light', + 'is_deconz_group': False, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.dimmable_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input0-light_payload0][light.group-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.group', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '01234E56789A-/groups/0', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input0-light_payload0][light.group-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_on': False, + 'brightness': 255, + 'color_mode': , + 'color_temp': 2500, + 'color_temp_kelvin': 400, + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'Group', + 'hs_color': tuple( + 15.981, + 100.0, + ), + 'is_deconz_group': True, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 67, + 0, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.674, + 0.322, + ), + }), + 'context': , + 'entity_id': 'light.group', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input0-light_payload0][light.rgb_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.rgb_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'RGB light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input0-light_payload0][light.rgb_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 50, + 'color_mode': , + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'RGB light', + 'hs_color': tuple( + 52.0, + 100.0, + ), + 'is_deconz_group': False, + 'rgb_color': tuple( + 255, + 221, + 0, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.5, + 0.5, + ), + }), + 'context': , + 'entity_id': 'light.rgb_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input0-light_payload0][light.tunable_white_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6451, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 155, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.tunable_white_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tunable white light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input0-light_payload0][light.tunable_white_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': , + 'color_temp': 2500, + 'color_temp_kelvin': 400, + 'friendly_name': 'Tunable white light', + 'hs_color': tuple( + 15.981, + 100.0, + ), + 'is_deconz_group': False, + 'max_color_temp_kelvin': 6451, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 155, + 'rgb_color': tuple( + 255, + 67, + 0, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.674, + 0.322, + ), + }), + 'context': , + 'entity_id': 'light.tunable_white_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input1-expected1-light_payload0][light.dimmable_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.dimmable_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Dimmable light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:02-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input1-expected1-light_payload0][light.dimmable_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'friendly_name': 'Dimmable light', + 'is_deconz_group': False, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.dimmable_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input1-expected1-light_payload0][light.group-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.group', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '01234E56789A-/groups/0', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input1-expected1-light_payload0][light.group-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_on': False, + 'brightness': 50, + 'color_mode': , + 'color_temp': 2500, + 'color_temp_kelvin': 400, + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'Group', + 'hs_color': tuple( + 15.981, + 100.0, + ), + 'is_deconz_group': True, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 67, + 0, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.674, + 0.322, + ), + }), + 'context': , + 'entity_id': 'light.group', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input1-expected1-light_payload0][light.rgb_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.rgb_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'RGB light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input1-expected1-light_payload0][light.rgb_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 50, + 'color_mode': , + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'RGB light', + 'hs_color': tuple( + 52.0, + 100.0, + ), + 'is_deconz_group': False, + 'rgb_color': tuple( + 255, + 221, + 0, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.5, + 0.5, + ), + }), + 'context': , + 'entity_id': 'light.rgb_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input1-expected1-light_payload0][light.tunable_white_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6451, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 155, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.tunable_white_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tunable white light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input1-expected1-light_payload0][light.tunable_white_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': , + 'color_temp': 2500, + 'color_temp_kelvin': 400, + 'friendly_name': 'Tunable white light', + 'hs_color': tuple( + 15.981, + 100.0, + ), + 'is_deconz_group': False, + 'max_color_temp_kelvin': 6451, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 155, + 'rgb_color': tuple( + 255, + 67, + 0, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.674, + 0.322, + ), + }), + 'context': , + 'entity_id': 'light.tunable_white_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input1-light_payload0][light.dimmable_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.dimmable_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Dimmable light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:02-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input1-light_payload0][light.dimmable_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'friendly_name': 'Dimmable light', + 'is_deconz_group': False, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.dimmable_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input1-light_payload0][light.group-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.group', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '01234E56789A-/groups/0', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input1-light_payload0][light.group-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_on': False, + 'brightness': 50, + 'color_mode': , + 'color_temp': 2500, + 'color_temp_kelvin': 400, + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'Group', + 'hs_color': tuple( + 15.981, + 100.0, + ), + 'is_deconz_group': True, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 67, + 0, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.674, + 0.322, + ), + }), + 'context': , + 'entity_id': 'light.group', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input1-light_payload0][light.rgb_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.rgb_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'RGB light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input1-light_payload0][light.rgb_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 50, + 'color_mode': , + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'RGB light', + 'hs_color': tuple( + 52.0, + 100.0, + ), + 'is_deconz_group': False, + 'rgb_color': tuple( + 255, + 221, + 0, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.5, + 0.5, + ), + }), + 'context': , + 'entity_id': 'light.rgb_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input1-light_payload0][light.tunable_white_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6451, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 155, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.tunable_white_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tunable white light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input1-light_payload0][light.tunable_white_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': , + 'color_temp': 2500, + 'color_temp_kelvin': 400, + 'friendly_name': 'Tunable white light', + 'hs_color': tuple( + 15.981, + 100.0, + ), + 'is_deconz_group': False, + 'max_color_temp_kelvin': 6451, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 155, + 'rgb_color': tuple( + 255, + 67, + 0, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.674, + 0.322, + ), + }), + 'context': , + 'entity_id': 'light.tunable_white_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input2-expected2-light_payload0][light.dimmable_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.dimmable_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Dimmable light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:02-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input2-expected2-light_payload0][light.dimmable_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'friendly_name': 'Dimmable light', + 'is_deconz_group': False, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.dimmable_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input2-expected2-light_payload0][light.group-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.group', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '01234E56789A-/groups/0', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input2-expected2-light_payload0][light.group-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_on': False, + 'brightness': 50, + 'color_mode': , + 'color_temp': None, + 'color_temp_kelvin': None, + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'Group', + 'hs_color': tuple( + 52.0, + 100.0, + ), + 'is_deconz_group': True, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 221, + 0, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.5, + 0.5, + ), + }), + 'context': , + 'entity_id': 'light.group', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input2-expected2-light_payload0][light.rgb_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.rgb_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'RGB light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input2-expected2-light_payload0][light.rgb_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 50, + 'color_mode': , + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'RGB light', + 'hs_color': tuple( + 52.0, + 100.0, + ), + 'is_deconz_group': False, + 'rgb_color': tuple( + 255, + 221, + 0, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.5, + 0.5, + ), + }), + 'context': , + 'entity_id': 'light.rgb_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input2-expected2-light_payload0][light.tunable_white_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6451, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 155, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.tunable_white_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tunable white light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input2-expected2-light_payload0][light.tunable_white_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': , + 'color_temp': 2500, + 'color_temp_kelvin': 400, + 'friendly_name': 'Tunable white light', + 'hs_color': tuple( + 15.981, + 100.0, + ), + 'is_deconz_group': False, + 'max_color_temp_kelvin': 6451, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 155, + 'rgb_color': tuple( + 255, + 67, + 0, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.674, + 0.322, + ), + }), + 'context': , + 'entity_id': 'light.tunable_white_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input2-light_payload0][light.dimmable_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.dimmable_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Dimmable light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:02-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input2-light_payload0][light.dimmable_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'friendly_name': 'Dimmable light', + 'is_deconz_group': False, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.dimmable_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input2-light_payload0][light.group-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.group', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '01234E56789A-/groups/0', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input2-light_payload0][light.group-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_on': False, + 'brightness': 50, + 'color_mode': , + 'color_temp': None, + 'color_temp_kelvin': None, + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'Group', + 'hs_color': tuple( + 52.0, + 100.0, + ), + 'is_deconz_group': True, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 221, + 0, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.5, + 0.5, + ), + }), + 'context': , + 'entity_id': 'light.group', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input2-light_payload0][light.rgb_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.rgb_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'RGB light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:00-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input2-light_payload0][light.rgb_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 50, + 'color_mode': , + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'RGB light', + 'hs_color': tuple( + 52.0, + 100.0, + ), + 'is_deconz_group': False, + 'rgb_color': tuple( + 255, + 221, + 0, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.5, + 0.5, + ), + }), + 'context': , + 'entity_id': 'light.rgb_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_groups[input2-light_payload0][light.tunable_white_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6451, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 155, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.tunable_white_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tunable white light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:00:00:00:01-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_groups[input2-light_payload0][light.tunable_white_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': , + 'color_temp': 2500, + 'color_temp_kelvin': 400, + 'friendly_name': 'Tunable white light', + 'hs_color': tuple( + 15.981, + 100.0, + ), + 'is_deconz_group': False, + 'max_color_temp_kelvin': 6451, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 155, + 'rgb_color': tuple( + 255, + 67, + 0, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.674, + 0.322, + ), + }), + 'context': , + 'entity_id': 'light.tunable_white_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_lights[light_payload0-expected0][light.hue_go-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_go', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue Go', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:17:88:01:01:23:45:67-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[light_payload0-expected0][light.hue_go-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 254, + 'color_mode': , + 'color_temp': 375, + 'color_temp_kelvin': 2666, + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'Hue Go', + 'hs_color': tuple( + 28.47, + 66.821, + ), + 'is_deconz_group': False, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 165, + 84, + ), + 'supported_color_modes': list([ + , + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.53, + 0.388, + ), + }), + 'context': , + 'entity_id': 'light.hue_go', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_lights[light_payload0][light.hue_go-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_go', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue Go', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:17:88:01:01:23:45:67-00', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[light_payload0][light.hue_go-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 254, + 'color_mode': , + 'color_temp': 375, + 'color_temp_kelvin': 2666, + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'Hue Go', + 'hs_color': tuple( + 28.47, + 66.821, + ), + 'is_deconz_group': False, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 165, + 84, + ), + 'supported_color_modes': list([ + , + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.53, + 0.388, + ), + }), + 'context': , + 'entity_id': 'light.hue_go', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_lights[light_payload1-expected1][light.hue_ensis-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'max_color_temp_kelvin': 7142, + 'max_mireds': 650, + 'min_color_temp_kelvin': 1538, + 'min_mireds': 140, + 'supported_color_modes': list([ + , + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_ensis', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue Ensis', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:17:88:01:01:23:45:67-01', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[light_payload1-expected1][light.hue_ensis-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 254, + 'color_mode': , + 'color_temp': None, + 'color_temp_kelvin': None, + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'Hue Ensis', + 'hs_color': tuple( + 29.691, + 38.039, + ), + 'is_deconz_group': False, + 'max_color_temp_kelvin': 7142, + 'max_mireds': 650, + 'min_color_temp_kelvin': 1538, + 'min_mireds': 140, + 'rgb_color': tuple( + 255, + 206, + 158, + ), + 'supported_color_modes': list([ + , + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.427, + 0.373, + ), + }), + 'context': , + 'entity_id': 'light.hue_ensis', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_lights[light_payload1][light.hue_ensis-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'max_color_temp_kelvin': 7142, + 'max_mireds': 650, + 'min_color_temp_kelvin': 1538, + 'min_mireds': 140, + 'supported_color_modes': list([ + , + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_ensis', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue Ensis', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:17:88:01:01:23:45:67-01', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[light_payload1][light.hue_ensis-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 254, + 'color_mode': , + 'color_temp': None, + 'color_temp_kelvin': None, + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'Hue Ensis', + 'hs_color': tuple( + 29.691, + 38.039, + ), + 'is_deconz_group': False, + 'max_color_temp_kelvin': 7142, + 'max_mireds': 650, + 'min_color_temp_kelvin': 1538, + 'min_mireds': 140, + 'rgb_color': tuple( + 255, + 206, + 158, + ), + 'supported_color_modes': list([ + , + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.427, + 0.373, + ), + }), + 'context': , + 'entity_id': 'light.hue_ensis', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_lights[light_payload2-expected2][light.lidl_xmas_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'carnival', + 'collide', + 'fading', + 'fireworks', + 'flag', + 'glow', + 'rainbow', + 'snake', + 'snow', + 'sparkles', + 'steady', + 'strobe', + 'twinkle', + 'updown', + 'vintage', + 'waves', + ]), + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.lidl_xmas_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'LIDL xmas light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '58:8e:81:ff:fe:db:7b:be-01', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[light_payload2-expected2][light.lidl_xmas_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 25, + 'color_mode': , + 'effect': None, + 'effect_list': list([ + 'carnival', + 'collide', + 'fading', + 'fireworks', + 'flag', + 'glow', + 'rainbow', + 'snake', + 'snow', + 'sparkles', + 'steady', + 'strobe', + 'twinkle', + 'updown', + 'vintage', + 'waves', + ]), + 'friendly_name': 'LIDL xmas light', + 'hs_color': tuple( + 294.938, + 55.294, + ), + 'is_deconz_group': False, + 'rgb_color': tuple( + 243, + 113, + 255, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.357, + 0.188, + ), + }), + 'context': , + 'entity_id': 'light.lidl_xmas_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_lights[light_payload2][light.lidl_xmas_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'carnival', + 'collide', + 'fading', + 'fireworks', + 'flag', + 'glow', + 'rainbow', + 'snake', + 'snow', + 'sparkles', + 'steady', + 'strobe', + 'twinkle', + 'updown', + 'vintage', + 'waves', + ]), + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.lidl_xmas_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'LIDL xmas light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '58:8e:81:ff:fe:db:7b:be-01', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[light_payload2][light.lidl_xmas_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 25, + 'color_mode': , + 'effect': None, + 'effect_list': list([ + 'carnival', + 'collide', + 'fading', + 'fireworks', + 'flag', + 'glow', + 'rainbow', + 'snake', + 'snow', + 'sparkles', + 'steady', + 'strobe', + 'twinkle', + 'updown', + 'vintage', + 'waves', + ]), + 'friendly_name': 'LIDL xmas light', + 'hs_color': tuple( + 294.938, + 55.294, + ), + 'is_deconz_group': False, + 'rgb_color': tuple( + 243, + 113, + 255, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.357, + 0.188, + ), + }), + 'context': , + 'entity_id': 'light.lidl_xmas_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_lights[light_payload3-expected3][light.hue_white_ambiance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_white_ambiance', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue White Ambiance', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:17:88:01:01:23:45:67-02', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[light_payload3-expected3][light.hue_white_ambiance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 254, + 'color_mode': , + 'color_temp': 396, + 'color_temp_kelvin': 2525, + 'friendly_name': 'Hue White Ambiance', + 'hs_color': tuple( + 28.809, + 71.624, + ), + 'is_deconz_group': False, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 160, + 72, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.544, + 0.389, + ), + }), + 'context': , + 'entity_id': 'light.hue_white_ambiance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_lights[light_payload3][light.hue_white_ambiance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_white_ambiance', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue White Ambiance', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:17:88:01:01:23:45:67-02', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[light_payload3][light.hue_white_ambiance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 254, + 'color_mode': , + 'color_temp': 396, + 'color_temp_kelvin': 2525, + 'friendly_name': 'Hue White Ambiance', + 'hs_color': tuple( + 28.809, + 71.624, + ), + 'is_deconz_group': False, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 454, + 'min_color_temp_kelvin': 2202, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 160, + 72, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.544, + 0.389, + ), + }), + 'context': , + 'entity_id': 'light.hue_white_ambiance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_lights[light_payload4-expected4][light.hue_filament-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_filament', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue Filament', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:17:88:01:01:23:45:67-03', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[light_payload4-expected4][light.hue_filament-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 254, + 'color_mode': , + 'friendly_name': 'Hue Filament', + 'is_deconz_group': False, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.hue_filament', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_lights[light_payload4][light.hue_filament-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hue_filament', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hue Filament', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:17:88:01:01:23:45:67-03', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[light_payload4][light.hue_filament-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 254, + 'color_mode': , + 'friendly_name': 'Hue Filament', + 'is_deconz_group': False, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.hue_filament', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_lights[light_payload5-expected5][light.simple_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.simple_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Simple Light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:01:23:45:67-01', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[light_payload5-expected5][light.simple_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': , + 'friendly_name': 'Simple Light', + 'is_deconz_group': False, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.simple_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_lights[light_payload5][light.simple_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.simple_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Simple Light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:15:8d:00:01:23:45:67-01', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[light_payload5][light.simple_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': , + 'friendly_name': 'Simple Light', + 'is_deconz_group': False, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.simple_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_lights[light_payload6-expected6][light.gradient_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.gradient_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Gradient light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:17:88:01:0b:0c:0d:0e-0f', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[light_payload6-expected6][light.gradient_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 184, + 'color_mode': , + 'color_temp': None, + 'color_temp_kelvin': None, + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'Gradient light', + 'hs_color': tuple( + 98.095, + 74.118, + ), + 'is_deconz_group': False, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 135, + 255, + 66, + ), + 'supported_color_modes': list([ + , + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.2727, + 0.6226, + ), + }), + 'context': , + 'entity_id': 'light.gradient_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_lights[light_payload6][light.gradient_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'colorloop', + ]), + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.gradient_light', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Gradient light', + 'platform': 'deconz', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:17:88:01:0b:0c:0d:0e-0f', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[light_payload6][light.gradient_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 184, + 'color_mode': , + 'color_temp': None, + 'color_temp_kelvin': None, + 'effect': None, + 'effect_list': list([ + 'colorloop', + ]), + 'friendly_name': 'Gradient light', + 'hs_color': tuple( + 98.095, + 74.118, + ), + 'is_deconz_group': False, + 'max_color_temp_kelvin': 6535, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 135, + 255, + 66, + ), + 'supported_color_modes': list([ + , + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.2727, + 0.6226, + ), + }), + 'context': , + 'entity_id': 'light.gradient_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 750661a8ba7..7bc2d961d13 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -2,22 +2,19 @@ from collections.abc import Callable from typing import Any +from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion -from homeassistant.components.deconz.const import ATTR_ON, CONF_ALLOW_DECONZ_GROUPS -from homeassistant.components.deconz.light import DECONZ_GROUP +from homeassistant.components.deconz.const import CONF_ALLOW_DECONZ_GROUPS from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, - ATTR_EFFECT_LIST, ATTR_FLASH, ATTR_HS_COLOR, - ATTR_MAX_MIREDS, - ATTR_MIN_MIREDS, - ATTR_RGB_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_XY_COLOR, @@ -37,16 +34,19 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .conftest import ConfigEntryFactoryType, WebsocketDataType +from tests.common import snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker @pytest.mark.parametrize( - ("light_payload", "expected"), + "light_payload", [ ( # RGB light in color temp color mode { @@ -75,28 +75,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker "swversion": "5.127.1.26420", "type": "Extended color light", "uniqueid": "00:17:88:01:01:23:45:67-00", - }, - { - "entity_id": "light.hue_go", - "state": STATE_ON, - "attributes": { - ATTR_BRIGHTNESS: 254, - ATTR_COLOR_TEMP: 375, - ATTR_EFFECT_LIST: [EFFECT_COLORLOOP], - ATTR_SUPPORTED_COLOR_MODES: [ - ColorMode.COLOR_TEMP, - ColorMode.HS, - ColorMode.XY, - ], - ATTR_COLOR_MODE: ColorMode.COLOR_TEMP, - ATTR_MIN_MIREDS: 153, - ATTR_MAX_MIREDS: 500, - ATTR_SUPPORTED_FEATURES: LightEntityFeature.TRANSITION - | LightEntityFeature.FLASH - | LightEntityFeature.EFFECT, - DECONZ_GROUP: False, - }, - }, + } ), ( # RGB light in XY color mode { @@ -125,30 +104,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker "swversion": "1.65.9_hB3217DF4", "type": "Extended color light", "uniqueid": "00:17:88:01:01:23:45:67-01", - }, - { - "entity_id": "light.hue_ensis", - "state": STATE_ON, - "attributes": { - ATTR_MIN_MIREDS: 140, - ATTR_MAX_MIREDS: 650, - ATTR_EFFECT_LIST: [EFFECT_COLORLOOP], - ATTR_SUPPORTED_COLOR_MODES: [ - ColorMode.COLOR_TEMP, - ColorMode.HS, - ColorMode.XY, - ], - ATTR_COLOR_MODE: ColorMode.XY, - ATTR_BRIGHTNESS: 254, - ATTR_HS_COLOR: (29.691, 38.039), - ATTR_RGB_COLOR: (255, 206, 158), - ATTR_XY_COLOR: (0.427, 0.373), - DECONZ_GROUP: False, - ATTR_SUPPORTED_FEATURES: LightEntityFeature.TRANSITION - | LightEntityFeature.FLASH - | LightEntityFeature.EFFECT, - }, - }, + } ), ( # RGB light with only HS color mode { @@ -171,41 +127,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker "swversion": None, "type": "Color dimmable light", "uniqueid": "58:8e:81:ff:fe:db:7b:be-01", - }, - { - "entity_id": "light.lidl_xmas_light", - "state": STATE_ON, - "attributes": { - ATTR_EFFECT_LIST: [ - "carnival", - "collide", - "fading", - "fireworks", - "flag", - "glow", - "rainbow", - "snake", - "snow", - "sparkles", - "steady", - "strobe", - "twinkle", - "updown", - "vintage", - "waves", - ], - ATTR_SUPPORTED_COLOR_MODES: [ColorMode.HS], - ATTR_COLOR_MODE: ColorMode.HS, - ATTR_BRIGHTNESS: 25, - ATTR_HS_COLOR: (294.938, 55.294), - ATTR_RGB_COLOR: (243, 113, 255), - ATTR_XY_COLOR: (0.357, 0.188), - DECONZ_GROUP: False, - ATTR_SUPPORTED_FEATURES: LightEntityFeature.TRANSITION - | LightEntityFeature.FLASH - | LightEntityFeature.EFFECT, - }, - }, + } ), ( # Tunable white light in CT color mode { @@ -230,22 +152,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker "swversion": "1.46.13_r26312", "type": "Color temperature light", "uniqueid": "00:17:88:01:01:23:45:67-02", - }, - { - "entity_id": "light.hue_white_ambiance", - "state": STATE_ON, - "attributes": { - ATTR_MIN_MIREDS: 153, - ATTR_MAX_MIREDS: 454, - ATTR_SUPPORTED_COLOR_MODES: [ColorMode.COLOR_TEMP], - ATTR_COLOR_MODE: ColorMode.COLOR_TEMP, - ATTR_BRIGHTNESS: 254, - ATTR_COLOR_TEMP: 396, - DECONZ_GROUP: False, - ATTR_SUPPORTED_FEATURES: LightEntityFeature.TRANSITION - | LightEntityFeature.FLASH, - }, - }, + } ), ( # Dimmable light { @@ -260,19 +167,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker "swversion": "1.55.8_r28815", "type": "Dimmable light", "uniqueid": "00:17:88:01:01:23:45:67-03", - }, - { - "entity_id": "light.hue_filament", - "state": STATE_ON, - "attributes": { - ATTR_SUPPORTED_COLOR_MODES: [ColorMode.BRIGHTNESS], - ATTR_COLOR_MODE: ColorMode.BRIGHTNESS, - ATTR_BRIGHTNESS: 254, - DECONZ_GROUP: False, - ATTR_SUPPORTED_FEATURES: LightEntityFeature.TRANSITION - | LightEntityFeature.FLASH, - }, - }, + } ), ( # On/Off light { @@ -287,17 +182,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker "swversion": "2.0", "type": "Simple light", "uniqueid": "00:15:8d:00:01:23:45:67-01", - }, - { - "entity_id": "light.simple_light", - "state": STATE_ON, - "attributes": { - ATTR_SUPPORTED_COLOR_MODES: [ColorMode.ONOFF], - ATTR_COLOR_MODE: ColorMode.ONOFF, - DECONZ_GROUP: False, - ATTR_SUPPORTED_FEATURES: 0, - }, - }, + } ), ( # Gradient light { @@ -396,42 +281,28 @@ from tests.test_util.aiohttp import AiohttpClientMocker "swversion": "1.104.2", "type": "Extended color light", "uniqueid": "00:17:88:01:0b:0c:0d:0e-0f", - }, - { - "entity_id": "light.gradient_light", - "state": STATE_ON, - "attributes": { - ATTR_SUPPORTED_COLOR_MODES: [ - ColorMode.COLOR_TEMP, - ColorMode.HS, - ColorMode.XY, - ], - ATTR_COLOR_MODE: ColorMode.XY, - }, - }, + } ), ], ) async def test_lights( hass: HomeAssistant, - config_entry_setup: ConfigEntry, - expected: dict[str, Any], + entity_registry: er.EntityRegistry, + config_entry_factory: ConfigEntryFactoryType, + snapshot: SnapshotAssertion, ) -> None: """Test that different light entities are created with expected values.""" - assert len(hass.states.async_all()) == 1 + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.LIGHT]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) - light = hass.states.get(expected["entity_id"]) - assert light.state == expected["state"] - for attribute, expected_value in expected["attributes"].items(): - assert light.attributes[attribute] == expected_value - - await hass.config_entries.async_unload(config_entry_setup.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) states = hass.states.async_all() for state in states: assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_remove(config_entry_setup.entry_id) + await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 @@ -849,81 +720,20 @@ async def test_configuration_tool(hass: HomeAssistant) -> None: ], ) @pytest.mark.parametrize( - ("input", "expected"), + "input", [ - ( - { - "lights": ["1", "2", "3"], - }, - { - "entity_id": "light.group", - "state": ATTR_ON, - "attributes": { - ATTR_MIN_MIREDS: 153, - ATTR_MAX_MIREDS: 500, - ATTR_SUPPORTED_COLOR_MODES: [ColorMode.COLOR_TEMP, ColorMode.XY], - ATTR_COLOR_MODE: ColorMode.COLOR_TEMP, - ATTR_BRIGHTNESS: 255, - ATTR_EFFECT_LIST: [EFFECT_COLORLOOP], - "all_on": False, - DECONZ_GROUP: True, - ATTR_SUPPORTED_FEATURES: 44, - }, - }, - ), - ( - { - "lights": ["3", "1", "2"], - }, - { - "entity_id": "light.group", - "state": ATTR_ON, - "attributes": { - ATTR_MIN_MIREDS: 153, - ATTR_MAX_MIREDS: 500, - ATTR_SUPPORTED_COLOR_MODES: [ColorMode.COLOR_TEMP, ColorMode.XY], - ATTR_COLOR_MODE: ColorMode.COLOR_TEMP, - ATTR_BRIGHTNESS: 50, - ATTR_EFFECT_LIST: [EFFECT_COLORLOOP], - "all_on": False, - DECONZ_GROUP: True, - ATTR_SUPPORTED_FEATURES: LightEntityFeature.TRANSITION - | LightEntityFeature.FLASH - | LightEntityFeature.EFFECT, - }, - }, - ), - ( - { - "lights": ["2", "3", "1"], - }, - { - "entity_id": "light.group", - "state": ATTR_ON, - "attributes": { - ATTR_MIN_MIREDS: 153, - ATTR_MAX_MIREDS: 500, - ATTR_SUPPORTED_COLOR_MODES: [ColorMode.COLOR_TEMP, ColorMode.XY], - ATTR_COLOR_MODE: ColorMode.XY, - ATTR_HS_COLOR: (52.0, 100.0), - ATTR_RGB_COLOR: (255, 221, 0), - ATTR_XY_COLOR: (0.5, 0.5), - "all_on": False, - DECONZ_GROUP: True, - ATTR_SUPPORTED_FEATURES: LightEntityFeature.TRANSITION - | LightEntityFeature.FLASH - | LightEntityFeature.EFFECT, - }, - }, - ), + ({"lights": ["1", "2", "3"]}), + ({"lights": ["3", "1", "2"]}), + ({"lights": ["2", "3", "1"]}), ], ) async def test_groups( hass: HomeAssistant, + entity_registry: er.EntityRegistry, config_entry_factory: Callable[[], ConfigEntry], group_payload: dict[str, Any], input: dict[str, list[str]], - expected: dict[str, Any], + snapshot: SnapshotAssertion, ) -> None: """Test that different group entities are created with expected values.""" group_payload |= { @@ -948,14 +758,10 @@ async def test_groups( "lights": input["lights"], }, } - config_entry = await config_entry_factory() - assert len(hass.states.async_all()) == 4 - - group = hass.states.get(expected["entity_id"]) - assert group.state == expected["state"] - for attribute, expected_value in expected["attributes"].items(): - assert group.attributes[attribute] == expected_value + with patch("homeassistant.components.deconz.PLATFORMS", [Platform.LIGHT]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) await hass.config_entries.async_unload(config_entry.entry_id) From 4876e35fd82ec21dd5eee580ce43781b737af3c8 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 24 Jul 2024 20:12:20 +0200 Subject: [PATCH 1545/2411] Matter event follow up (#122553) --- homeassistant/components/matter/event.py | 2 +- homeassistant/components/matter/strings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/matter/event.py b/homeassistant/components/matter/event.py index 885ba83ce07..3cb3fe385d4 100644 --- a/homeassistant/components/matter/event.py +++ b/homeassistant/components/matter/event.py @@ -64,7 +64,7 @@ class MatterEventEntity(MatterEntity, EventEntity): # Momentary switch with multi press support # NOTE: We ignore 'multi press ongoing' as it doesn't make a lot # of sense and many devices do not support it. - # Instead we we report on the 'multi press complete' event with the number + # Instead we report on the 'multi press complete' event with the number # of presses. max_presses_supported = self.get_matter_attribute_value( clusters.Switch.Attributes.MultiPressMax diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 3c50ccbaa21..c23a2d6fe94 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -74,7 +74,7 @@ "state": { "switch_latched": "Switch latched", "initial_press": "Pressed", - "long_press": "Hold down", + "long_press": "Held down", "short_release": "Released after being pressed", "long_release": "Released after being held down", "multi_press_1": "Pressed once", From d7c713d18d93506e496971237890cd97e6c8b952 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 24 Jul 2024 20:12:51 +0200 Subject: [PATCH 1546/2411] Fix typo in Matter lock platform (#122536) --- homeassistant/components/matter/lock.py | 4 ++-- .../matter/{test_door_lock.py => test_lock.py} | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) rename tests/components/matter/{test_door_lock.py => test_lock.py} (95%) diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index ae01faa3bc7..31ae5e496ce 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -168,10 +168,10 @@ class MatterLock(MatterEntity, LockEntity): LOGGER.debug("Lock state: %s for %s", lock_state, self.entity_id) - if lock_state is clusters.DoorLock.Enums.DlLockState.kUnlatched: + if lock_state == clusters.DoorLock.Enums.DlLockState.kUnlatched: self._attr_is_locked = False self._attr_is_open = True - if lock_state is clusters.DoorLock.Enums.DlLockState.kLocked: + elif lock_state == clusters.DoorLock.Enums.DlLockState.kLocked: self._attr_is_locked = True self._attr_is_open = False elif lock_state in ( diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_lock.py similarity index 95% rename from tests/components/matter/test_door_lock.py rename to tests/components/matter/test_lock.py index 461cc1b7f3d..1180e6ee469 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_lock.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components.lock import ( STATE_LOCKED, + STATE_OPEN, STATE_UNLOCKED, LockEntityFeature, ) @@ -82,12 +83,12 @@ async def test_lock( assert state assert state.state == STATE_UNLOCKED - set_node_attribute(door_lock, 1, 257, 0, 0) + set_node_attribute(door_lock, 1, 257, 0, 1) await trigger_subscription_callback(hass, matter_client) state = hass.states.get("lock.mock_door_lock_lock") assert state - assert state.state == STATE_UNLOCKED + assert state.state == STATE_LOCKED set_node_attribute(door_lock, 1, 257, 0, None) await trigger_subscription_callback(hass, matter_client) @@ -213,9 +214,16 @@ async def test_lock_with_unbolt( assert state assert state.state == STATE_OPENING - set_node_attribute(door_lock_with_unbolt, 1, 257, 3, 0) + set_node_attribute(door_lock_with_unbolt, 1, 257, 0, 0) await trigger_subscription_callback(hass, matter_client) state = hass.states.get("lock.mock_door_lock_lock") assert state - assert state.state == STATE_LOCKED + assert state.state == STATE_UNLOCKED + + set_node_attribute(door_lock_with_unbolt, 1, 257, 0, 3) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("lock.mock_door_lock_lock") + assert state + assert state.state == STATE_OPEN From 34b32ced253854a5f66100c6d7c6be0d0f0b6b66 Mon Sep 17 00:00:00 2001 From: Stefano Semeraro Date: Wed, 24 Jul 2024 20:37:38 +0200 Subject: [PATCH 1547/2411] Add CCT support to WLED (#122488) --- homeassistant/components/wled/const.py | 7 +- homeassistant/components/wled/helpers.py | 10 + homeassistant/components/wled/light.py | 19 +- tests/components/wled/fixtures/cct.json | 383 +++++++++++++++++++++++ tests/components/wled/test_light.py | 34 ++ 5 files changed, 451 insertions(+), 2 deletions(-) create mode 100644 tests/components/wled/fixtures/cct.json diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 635b78dcf13..69ff6ccb1fa 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -19,6 +19,7 @@ CONF_KEEP_MAIN_LIGHT = "keep_master_light" DEFAULT_KEEP_MAIN_LIGHT = False # Attributes +ATTR_CCT = "cct" ATTR_COLOR_PRIMARY = "color_primary" ATTR_DURATION = "duration" ATTR_FADE = "fade" @@ -30,6 +31,10 @@ ATTR_SPEED = "speed" ATTR_TARGET_BRIGHTNESS = "target_brightness" ATTR_UDP_PORT = "udp_port" +# Static values +COLOR_TEMP_K_MIN = 2000 +COLOR_TEMP_K_MAX = 6535 + LIGHT_CAPABILITIES_COLOR_MODE_MAPPING: dict[LightCapability, list[ColorMode]] = { LightCapability.NONE: [ @@ -56,8 +61,8 @@ LIGHT_CAPABILITIES_COLOR_MODE_MAPPING: dict[LightCapability, list[ColorMode]] = LightCapability.RGB_COLOR | LightCapability.WHITE_CHANNEL | LightCapability.COLOR_TEMPERATURE: [ - ColorMode.RGB, ColorMode.COLOR_TEMP, + ColorMode.RGBW, ], LightCapability.MANUAL_WHITE: [ ColorMode.BRIGHTNESS, diff --git a/homeassistant/components/wled/helpers.py b/homeassistant/components/wled/helpers.py index 0dd29fdc2a3..216dba67c94 100644 --- a/homeassistant/components/wled/helpers.py +++ b/homeassistant/components/wled/helpers.py @@ -35,3 +35,13 @@ def wled_exception_handler[_WLEDEntityT: WLEDEntity, **_P]( raise HomeAssistantError("Invalid response from WLED API") from error return handler + + +def kelvin_to_255(k: int, min_k: int, max_k: int) -> int: + """Map color temperature in K from minK-maxK to 0-255.""" + return int((k - min_k) / (max_k - min_k) * 255) + + +def kelvin_to_255_reverse(v: int, min_k: int, max_k: int) -> int: + """Map color temperature from 0-255 to minK-maxK K.""" + return int(v / 255 * (max_k - min_k) + min_k) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 5423df84686..b4edf10dc58 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -7,6 +7,7 @@ from typing import Any, cast from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, @@ -20,14 +21,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import WLEDConfigEntry from .const import ( + ATTR_CCT, ATTR_COLOR_PRIMARY, ATTR_ON, ATTR_SEGMENT_ID, + COLOR_TEMP_K_MAX, + COLOR_TEMP_K_MIN, LIGHT_CAPABILITIES_COLOR_MODE_MAPPING, ) from .coordinator import WLEDDataUpdateCoordinator from .entity import WLEDEntity -from .helpers import wled_exception_handler +from .helpers import kelvin_to_255, kelvin_to_255_reverse, wled_exception_handler PARALLEL_UPDATES = 1 @@ -109,6 +113,8 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): _attr_supported_features = LightEntityFeature.EFFECT | LightEntityFeature.TRANSITION _attr_translation_key = "segment" + _attr_min_color_temp_kelvin = COLOR_TEMP_K_MIN + _attr_max_color_temp_kelvin = COLOR_TEMP_K_MAX def __init__( self, @@ -166,6 +172,12 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): return None return cast(tuple[int, int, int, int], color.primary) + @property + def color_temp_kelvin(self) -> int | None: + """Return the CT color value in K.""" + cct = self.coordinator.data.state.segments[self._segment].cct + return kelvin_to_255_reverse(cct, COLOR_TEMP_K_MIN, COLOR_TEMP_K_MAX) + @property def effect(self) -> str | None: """Return the current effect of the light.""" @@ -235,6 +247,11 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): if ATTR_RGBW_COLOR in kwargs: data[ATTR_COLOR_PRIMARY] = kwargs[ATTR_RGBW_COLOR] + if ATTR_COLOR_TEMP_KELVIN in kwargs: + data[ATTR_CCT] = kelvin_to_255( + kwargs[ATTR_COLOR_TEMP_KELVIN], COLOR_TEMP_K_MIN, COLOR_TEMP_K_MAX + ) + if ATTR_TRANSITION in kwargs: # WLED uses 100ms per unit, so 10 = 1 second. data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10) diff --git a/tests/components/wled/fixtures/cct.json b/tests/components/wled/fixtures/cct.json new file mode 100644 index 00000000000..da36f8a5f69 --- /dev/null +++ b/tests/components/wled/fixtures/cct.json @@ -0,0 +1,383 @@ +{ + "state": { + "on": true, + "bri": 255, + "transition": 7, + "ps": 2, + "pl": -1, + "nl": { + "on": false, + "dur": 60, + "mode": 1, + "tbri": 0, + "rem": -1 + }, + "udpn": { + "send": false, + "recv": true, + "sgrp": 1, + "rgrp": 1 + }, + "lor": 0, + "mainseg": 0, + "seg": [ + { + "id": 0, + "start": 0, + "stop": 178, + "len": 178, + "grp": 1, + "spc": 0, + "of": 0, + "on": true, + "frz": false, + "bri": 255, + "cct": 53, + "set": 0, + "col": [ + [0, 0, 0, 255], + [0, 0, 0, 0], + [0, 0, 0, 0] + ], + "fx": 0, + "sx": 128, + "ix": 128, + "pal": 0, + "c1": 128, + "c2": 128, + "c3": 16, + "sel": true, + "rev": false, + "mi": false, + "o1": false, + "o2": false, + "o3": false, + "si": 0, + "m12": 0 + } + ] + }, + "info": { + "ver": "0.15.0-b3", + "vid": 2405180, + "cn": "Kōsen", + "release": "ESP32", + "leds": { + "count": 178, + "pwr": 0, + "fps": 0, + "maxpwr": 0, + "maxseg": 32, + "bootps": 1, + "seglc": [7], + "lc": 7, + "rgbw": true, + "wv": 2, + "cct": 4 + }, + "str": false, + "name": "WLED CCT light", + "udpport": 21324, + "simplifiedui": false, + "live": false, + "liveseg": -1, + "lm": "", + "lip": "", + "ws": 1, + "fxcount": 187, + "palcount": 75, + "cpalcount": 4, + "maps": [ + { + "id": 0 + } + ], + "wifi": { + "bssid": "AA:AA:AA:AA:AA:BB", + "rssi": -44, + "signal": 100, + "channel": 11 + }, + "fs": { + "u": 20, + "t": 983, + "pmt": 1721752272 + }, + "ndc": 1, + "arch": "esp32", + "core": "v3.3.6-16-gcc5440f6a2", + "clock": 240, + "flash": 4, + "lwip": 0, + "freeheap": 164804, + "uptime": 79769, + "time": "2024-7-24, 14:34:00", + "opt": 79, + "brand": "WLED", + "product": "FOSS", + "mac": "aabbccddeeff", + "ip": "127.0.0.1" + }, + "effects": [ + "Solid", + "Blink", + "Breathe", + "Wipe", + "Wipe Random", + "Random Colors", + "Sweep", + "Dynamic", + "Colorloop", + "Rainbow", + "Scan", + "Scan Dual", + "Fade", + "Theater", + "Theater Rainbow", + "Running", + "Saw", + "Twinkle", + "Dissolve", + "Dissolve Rnd", + "Sparkle", + "Sparkle Dark", + "Sparkle+", + "Strobe", + "Strobe Rainbow", + "Strobe Mega", + "Blink Rainbow", + "Android", + "Chase", + "Chase Random", + "Chase Rainbow", + "Chase Flash", + "Chase Flash Rnd", + "Rainbow Runner", + "Colorful", + "Traffic Light", + "Sweep Random", + "Chase 2", + "Aurora", + "Stream", + "Scanner", + "Lighthouse", + "Fireworks", + "Rain", + "Tetrix", + "Fire Flicker", + "Gradient", + "Loading", + "Rolling Balls", + "Fairy", + "Two Dots", + "Fairytwinkle", + "Running Dual", + "RSVD", + "Chase 3", + "Tri Wipe", + "Tri Fade", + "Lightning", + "ICU", + "Multi Comet", + "Scanner Dual", + "Stream 2", + "Oscillate", + "Pride 2015", + "Juggle", + "Palette", + "Fire 2012", + "Colorwaves", + "Bpm", + "Fill Noise", + "Noise 1", + "Noise 2", + "Noise 3", + "Noise 4", + "Colortwinkles", + "Lake", + "Meteor", + "Meteor Smooth", + "Railway", + "Ripple", + "Twinklefox", + "Twinklecat", + "Halloween Eyes", + "Solid Pattern", + "Solid Pattern Tri", + "Spots", + "Spots Fade", + "Glitter", + "Candle", + "Fireworks Starburst", + "Fireworks 1D", + "Bouncing Balls", + "Sinelon", + "Sinelon Dual", + "Sinelon Rainbow", + "Popcorn", + "Drip", + "Plasma", + "Percent", + "Ripple Rainbow", + "Heartbeat", + "Pacifica", + "Candle Multi", + "Solid Glitter", + "Sunrise", + "Phased", + "Twinkleup", + "Noise Pal", + "Sine", + "Phased Noise", + "Flow", + "Chunchun", + "Dancing Shadows", + "Washing Machine", + "Rotozoomer", + "Blends", + "TV Simulator", + "Dynamic Smooth", + "Spaceships", + "Crazy Bees", + "Ghost Rider", + "Blobs", + "Scrolling Text", + "Drift Rose", + "Distortion Waves", + "Soap", + "Octopus", + "Waving Cell", + "Pixels", + "Pixelwave", + "Juggles", + "Matripix", + "Gravimeter", + "Plasmoid", + "Puddles", + "Midnoise", + "Noisemeter", + "Freqwave", + "Freqmatrix", + "GEQ", + "Waterfall", + "Freqpixels", + "RSVD", + "Noisefire", + "Puddlepeak", + "Noisemove", + "Noise2D", + "Perlin Move", + "Ripple Peak", + "Firenoise", + "Squared Swirl", + "RSVD", + "DNA", + "Matrix", + "Metaballs", + "Freqmap", + "Gravcenter", + "Gravcentric", + "Gravfreq", + "DJ Light", + "Funky Plank", + "RSVD", + "Pulser", + "Blurz", + "Drift", + "Waverly", + "Sun Radiation", + "Colored Bursts", + "Julia", + "RSVD", + "RSVD", + "RSVD", + "Game Of Life", + "Tartan", + "Polar Lights", + "Swirl", + "Lissajous", + "Frizzles", + "Plasma Ball", + "Flow Stripe", + "Hiphotic", + "Sindots", + "DNA Spiral", + "Black Hole", + "Wavesins", + "Rocktaves", + "Akemi" + ], + "palettes": [ + "Default", + "* Random Cycle", + "* Color 1", + "* Colors 1&2", + "* Color Gradient", + "* Colors Only", + "Party", + "Cloud", + "Lava", + "Ocean", + "Forest", + "Rainbow", + "Rainbow Bands", + "Sunset", + "Rivendell", + "Breeze", + "Red & Blue", + "Yellowout", + "Analogous", + "Splash", + "Pastel", + "Sunset 2", + "Beach", + "Vintage", + "Departure", + "Landscape", + "Beech", + "Sherbet", + "Hult", + "Hult 64", + "Drywet", + "Jul", + "Grintage", + "Rewhi", + "Tertiary", + "Fire", + "Icefire", + "Cyane", + "Light Pink", + "Autumn", + "Magenta", + "Magred", + "Yelmag", + "Yelblu", + "Orange & Teal", + "Tiamat", + "April Night", + "Orangery", + "C9", + "Sakura", + "Aurora", + "Atlantica", + "C9 2", + "C9 New", + "Temperature", + "Aurora 2", + "Retro Clown", + "Candy", + "Toxy Reaf", + "Fairy Reaf", + "Semi Blue", + "Pink Candy", + "Red Reaf", + "Aqua Flash", + "Yelblu Hot", + "Lite Light", + "Red Flash", + "Blink Red", + "Red Shift", + "Red Tide", + "Candy2" + ] +} diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 032035f0141..58c4aa4e8c6 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -9,8 +9,11 @@ from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, + ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_MAX_COLOR_TEMP_KELVIN, + ATTR_MIN_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_SUPPORTED_COLOR_MODES, @@ -374,3 +377,34 @@ async def test_single_segment_with_keep_main_light( assert (state := hass.states.get("light.wled_rgb_light_main")) assert state.state == STATE_ON + + +@pytest.mark.parametrize("device_fixture", ["cct"]) +async def test_cct_light(hass: HomeAssistant, mock_wled: MagicMock) -> None: + """Test CCT support for WLED.""" + assert (state := hass.states.get("light.wled_cct_light")) + assert state.state == STATE_ON + assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [ + ColorMode.COLOR_TEMP, + ColorMode.RGBW, + ] + assert state.attributes.get(ATTR_COLOR_MODE) == ColorMode.COLOR_TEMP + assert state.attributes.get(ATTR_MIN_COLOR_TEMP_KELVIN) == 2000 + assert state.attributes.get(ATTR_MAX_COLOR_TEMP_KELVIN) == 6535 + assert state.attributes.get(ATTR_COLOR_TEMP_KELVIN) == 2942 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.wled_cct_light", + ATTR_COLOR_TEMP_KELVIN: 4321, + }, + blocking=True, + ) + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with( + cct=130, + on=True, + segment_id=0, + ) From fcccd85ac4dafb85ed7e2f5390b58463c70ac88a Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Wed, 24 Jul 2024 21:40:05 +0200 Subject: [PATCH 1548/2411] Add tests to emoncms (#122547) * Add tests to emoncms * Reduce snapshot size * Reduce snapshot size * run hassfest to update CODEOWNERS file * Update requirements_test_all.txt * Update tests/components/emoncms/test_sensor.py Co-authored-by: Joost Lekkerkerker * Dont use snapshot when testing state change --------- Co-authored-by: Joost Lekkerkerker --- CODEOWNERS | 1 + homeassistant/components/emoncms/const.py | 1 + requirements_test_all.txt | 3 + tests/components/emoncms/__init__.py | 1 + tests/components/emoncms/conftest.py | 47 ++++++++++ .../emoncms/snapshots/test_sensor.ambr | 24 +++++ tests/components/emoncms/test_sensor.py | 90 +++++++++++++++++++ 7 files changed, 167 insertions(+) create mode 100644 tests/components/emoncms/__init__.py create mode 100644 tests/components/emoncms/conftest.py create mode 100644 tests/components/emoncms/snapshots/test_sensor.ambr create mode 100644 tests/components/emoncms/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index b382d63cf44..c059e84f677 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -384,6 +384,7 @@ build.json @home-assistant/supervisor /tests/components/elvia/ @ludeeus /homeassistant/components/emby/ @mezz64 /homeassistant/components/emoncms/ @borpin @alexandrecuer +/tests/components/emoncms/ @borpin @alexandrecuer /homeassistant/components/emonitor/ @bdraco /tests/components/emonitor/ @bdraco /homeassistant/components/emulated_hue/ @bdraco @Tho85 diff --git a/homeassistant/components/emoncms/const.py b/homeassistant/components/emoncms/const.py index dc43e7a07dc..96269218316 100644 --- a/homeassistant/components/emoncms/const.py +++ b/homeassistant/components/emoncms/const.py @@ -6,6 +6,7 @@ CONF_EXCLUDE_FEEDID = "exclude_feed_id" CONF_ONLY_INCLUDE_FEEDID = "include_only_feed_id" CONF_MESSAGE = "message" CONF_SUCCESS = "success" +DOMAIN = "emoncms" LOGGER = logging.getLogger(__package__) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b7344dd3eb..35ae05a00d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1453,6 +1453,9 @@ pyefergy==22.5.0 # homeassistant.components.energenie_power_sockets pyegps==0.2.5 +# homeassistant.components.emoncms +pyemoncms==0.0.7 + # homeassistant.components.enphase_envoy pyenphase==1.20.6 diff --git a/tests/components/emoncms/__init__.py b/tests/components/emoncms/__init__.py new file mode 100644 index 00000000000..ecf3c54e9ed --- /dev/null +++ b/tests/components/emoncms/__init__.py @@ -0,0 +1 @@ +"""Tests for the emoncms component.""" diff --git a/tests/components/emoncms/conftest.py b/tests/components/emoncms/conftest.py new file mode 100644 index 00000000000..500fff228e9 --- /dev/null +++ b/tests/components/emoncms/conftest.py @@ -0,0 +1,47 @@ +"""Fixtures for emoncms integration tests.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, patch + +import pytest + +UNITS = ["kWh", "Wh", "W", "V", "A", "VA", "°C", "°F", "K", "Hz", "hPa", ""] + + +def get_feed( + number: int, unit: str = "W", value: int = 18.04, timestamp: int = 1665509570 +): + """Generate feed details.""" + return { + "id": str(number), + "userid": "1", + "name": f"parameter {number}", + "tag": "tag", + "size": "35809224", + "unit": unit, + "time": timestamp, + "value": value, + } + + +FEEDS = [get_feed(i + 1, unit=unit) for i, unit in enumerate(UNITS)] + + +EMONCMS_FAILURE = {"success": False, "message": "failure"} + + +@pytest.fixture +async def emoncms_client() -> AsyncGenerator[AsyncMock]: + """Mock pyemoncms success response.""" + with ( + patch( + "homeassistant.components.emoncms.sensor.EmoncmsClient", autospec=True + ) as mock_client, + patch( + "homeassistant.components.emoncms.coordinator.EmoncmsClient", + new=mock_client, + ), + ): + client = mock_client.return_value + client.async_request.return_value = {"success": True, "message": FEEDS} + yield client diff --git a/tests/components/emoncms/snapshots/test_sensor.ambr b/tests/components/emoncms/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..62c85aaba01 --- /dev/null +++ b/tests/components/emoncms/snapshots/test_sensor.ambr @@ -0,0 +1,24 @@ +# serializer version: 1 +# name: test_coordinator_update[sensor.emoncms_parameter_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'FeedId': '1', + 'FeedName': 'parameter 1', + 'LastUpdated': 1665509570, + 'LastUpdatedStr': '2022-10-11T10:32:50-07:00', + 'Size': '35809224', + 'Tag': 'tag', + 'UserId': '1', + 'device_class': 'temperature', + 'friendly_name': 'EmonCMS parameter 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.emoncms_parameter_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.04', + }) +# --- diff --git a/tests/components/emoncms/test_sensor.py b/tests/components/emoncms/test_sensor.py new file mode 100644 index 00000000000..a039239077e --- /dev/null +++ b/tests/components/emoncms/test_sensor.py @@ -0,0 +1,90 @@ +"""Test emoncms sensor.""" + +from typing import Any +from unittest.mock import AsyncMock + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.emoncms.const import CONF_ONLY_INCLUDE_FEEDID, DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_PLATFORM, CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType +from homeassistant.setup import async_setup_component + +from .conftest import EMONCMS_FAILURE, FEEDS, get_feed + +from tests.common import async_fire_time_changed + +YAML = { + CONF_PLATFORM: "emoncms", + CONF_API_KEY: "my_api_key", + CONF_ID: 1, + CONF_URL: "http://1.1.1.1", + CONF_ONLY_INCLUDE_FEEDID: [1, 2], + "scan_interval": 30, +} + + +@pytest.fixture +def emoncms_yaml_config() -> ConfigType: + """Mock emoncms configuration from yaml.""" + return {"sensor": YAML} + + +def get_entity_ids(feeds: list[dict[str, Any]]) -> list[str]: + """Get emoncms entity ids.""" + return [ + f"{SENSOR_DOMAIN}.{DOMAIN}_{feed["name"].replace(' ', '_')}" for feed in feeds + ] + + +def get_feeds(nbs: list[int]) -> list[dict[str, Any]]: + """Get feeds.""" + return [feed for feed in FEEDS if feed["id"] in str(nbs)] + + +async def test_coordinator_update( + hass: HomeAssistant, + emoncms_yaml_config: ConfigType, + snapshot: SnapshotAssertion, + emoncms_client: AsyncMock, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, +) -> None: + """Test coordinator update.""" + emoncms_client.async_request.return_value = { + "success": True, + "message": [get_feed(1, unit="°C")], + } + await async_setup_component(hass, SENSOR_DOMAIN, emoncms_yaml_config) + await hass.async_block_till_done() + feeds = get_feeds([1]) + for entity_id in get_entity_ids(feeds): + state = hass.states.get(entity_id) + assert state == snapshot(name=entity_id) + + async def skip_time() -> None: + freezer.tick(60) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + emoncms_client.async_request.return_value = { + "success": True, + "message": [get_feed(1, unit="°C", value=24.04, timestamp=1665509670)], + } + + await skip_time() + + for entity_id in get_entity_ids(feeds): + state = hass.states.get(entity_id) + assert state.attributes["LastUpdated"] == 1665509670 + assert state.state == "24.04" + + emoncms_client.async_request.return_value = EMONCMS_FAILURE + + await skip_time() + + assert f"Error fetching {DOMAIN}_coordinator data" in caplog.text From 59637d23913b85ed005befcc15204c0bec202323 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 25 Jul 2024 00:26:18 +0200 Subject: [PATCH 1549/2411] Address Wake on Lan post-merge feedback (#122549) Address Wake on Late post-merge feedback --- homeassistant/components/wake_on_lan/button.py | 6 +++--- homeassistant/components/wake_on_lan/config_flow.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/wake_on_lan/button.py b/homeassistant/components/wake_on_lan/button.py index 0818fd11f08..39c4511868d 100644 --- a/homeassistant/components/wake_on_lan/button.py +++ b/homeassistant/components/wake_on_lan/button.py @@ -25,7 +25,7 @@ async def async_setup_entry( entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the Wake on LAN sensor entry.""" + """Set up the Wake on LAN button entry.""" broadcast_address: str | None = entry.options.get(CONF_BROADCAST_ADDRESS) broadcast_port: int | None = entry.options.get(CONF_BROADCAST_PORT) mac_address: str = entry.options[CONF_MAC] @@ -33,7 +33,7 @@ async def async_setup_entry( async_add_entities( [ - WolSwitch( + WolButton( name, mac_address, broadcast_address, @@ -43,7 +43,7 @@ async def async_setup_entry( ) -class WolSwitch(ButtonEntity): +class WolButton(ButtonEntity): """Representation of a wake on lan button.""" _attr_name = None diff --git a/homeassistant/components/wake_on_lan/config_flow.py b/homeassistant/components/wake_on_lan/config_flow.py index a7c406cefb7..fb54dd146e5 100644 --- a/homeassistant/components/wake_on_lan/config_flow.py +++ b/homeassistant/components/wake_on_lan/config_flow.py @@ -68,8 +68,8 @@ OPTIONS_FLOW = { } -class StatisticsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): - """Handle a config flow for Statistics.""" +class WakeonLanConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config flow for Wake on Lan.""" config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW From 4901ecba7f33efd9ea452a1189b89d0c10dba5cb Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Thu, 25 Jul 2024 09:44:56 +1000 Subject: [PATCH 1550/2411] Bump aiolifx to 1.0.6 (#122569) --- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index da4eb3296f2..54cff7d6e1f 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -48,7 +48,7 @@ "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"], "requirements": [ - "aiolifx==1.0.5", + "aiolifx==1.0.6", "aiolifx-effects==0.3.2", "aiolifx-themes==0.5.0" ] diff --git a/requirements_all.txt b/requirements_all.txt index 05010ccacae..cca613a479a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,7 +276,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.5.0 # homeassistant.components.lifx -aiolifx==1.0.5 +aiolifx==1.0.6 # homeassistant.components.livisi aiolivisi==0.0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 35ae05a00d0..776fc815e34 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -255,7 +255,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.5.0 # homeassistant.components.lifx -aiolifx==1.0.5 +aiolifx==1.0.6 # homeassistant.components.livisi aiolivisi==0.0.19 From 8687b438f1e46fd7d95f246b76ddf41d6e22306c Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 25 Jul 2024 02:05:31 -0700 Subject: [PATCH 1551/2411] Use appropriate selector for homeassistant.update_entity (#122497) --- homeassistant/components/homeassistant/services.yaml | 8 ++++++-- homeassistant/components/homeassistant/strings.json | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml index 892e577490d..897b7d50e31 100644 --- a/homeassistant/components/homeassistant/services.yaml +++ b/homeassistant/components/homeassistant/services.yaml @@ -43,8 +43,12 @@ turn_off: entity: {} update_entity: - target: - entity: {} + fields: + entity_id: + required: true + selector: + entity: + multiple: true reload_custom_templates: reload_config_entry: diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 7cf05527b6b..e3e1464077a 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -145,7 +145,13 @@ }, "update_entity": { "name": "Update entity", - "description": "Forces one or more entities to update its data." + "description": "Forces one or more entities to update its data.", + "fields": { + "entity_id": { + "name": "Entities to update", + "description": "List of entities to force update." + } + } }, "reload_custom_templates": { "name": "Reload custom Jinja2 templates", From 7348a1fd0c9c11c3ca1a75f0c1f2e186609ec2de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Jul 2024 04:06:55 -0500 Subject: [PATCH 1552/2411] Convert homekit to use entry.runtime_data (#122533) --- homeassistant/components/homekit/__init__.py | 34 +++++++++++-------- .../components/homekit/diagnostics.py | 9 ++--- homeassistant/components/homekit/models.py | 4 +++ homeassistant/components/homekit/util.py | 5 +-- tests/components/homekit/test_homekit.py | 6 +++- tests/components/homekit/test_util.py | 7 +++- 6 files changed, 40 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 36372a5d16f..3f633c2ec59 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -127,7 +127,7 @@ from .const import ( SIGNAL_RELOAD_ENTITIES, ) from .iidmanager import AccessoryIIDStorage -from .models import HomeKitEntryData +from .models import HomeKitConfigEntry, HomeKitEntryData from .type_triggers import DeviceTriggerAccessory from .util import ( accessory_friendly_name, @@ -223,8 +223,12 @@ UNPAIR_SERVICE_SCHEMA = vol.All( def _async_all_homekit_instances(hass: HomeAssistant) -> list[HomeKit]: """All active HomeKit instances.""" - domain_data: dict[str, HomeKitEntryData] = hass.data[DOMAIN] - return [data.homekit for data in domain_data.values()] + hk_data: HomeKitEntryData | None + return [ + hk_data.homekit + for entry in hass.config_entries.async_entries(DOMAIN) + if (hk_data := getattr(entry, "runtime_data", None)) + ] def _async_get_imported_entries_indices( @@ -246,7 +250,6 @@ def _async_get_imported_entries_indices( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HomeKit from yaml.""" - hass.data[DOMAIN] = {} hass.data[PERSIST_LOCK_DATA] = asyncio.Lock() # Initialize the loader before loading entries to ensure @@ -316,7 +319,7 @@ def _async_update_config_entry_from_yaml( return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: HomeKitConfigEntry) -> bool: """Set up HomeKit from a config entry.""" _async_import_options_from_data_if_missing(hass, entry) @@ -372,7 +375,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_data = HomeKitEntryData( homekit=homekit, pairing_qr=None, pairing_qr_secret=None ) - hass.data[DOMAIN][entry.entry_id] = entry_data + entry.runtime_data = entry_data async def _async_start_homekit(hass: HomeAssistant) -> None: await homekit.async_start() @@ -382,17 +385,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def _async_update_listener( + hass: HomeAssistant, entry: HomeKitConfigEntry +) -> None: """Handle options update.""" if entry.source == SOURCE_IMPORT: return await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: HomeKitConfigEntry) -> bool: """Unload a config entry.""" async_dismiss_setup_message(hass, entry.entry_id) - entry_data: HomeKitEntryData = hass.data[DOMAIN][entry.entry_id] + entry_data = entry.runtime_data homekit = entry_data.homekit if homekit.status == STATUS_RUNNING: @@ -409,12 +414,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await asyncio.sleep(PORT_CLEANUP_CHECK_INTERVAL_SECS) - hass.data[DOMAIN].pop(entry.entry_id) - return True -async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_remove_entry(hass: HomeAssistant, entry: HomeKitConfigEntry) -> None: """Remove a config entry.""" await hass.async_add_executor_job( remove_state_files_for_entry_id, hass, entry.entry_id @@ -423,7 +426,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: @callback def _async_import_options_from_data_if_missing( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: HomeKitConfigEntry ) -> None: options = deepcopy(dict(entry.options)) data = deepcopy(dict(entry.data)) @@ -1198,9 +1201,10 @@ class HomeKitPairingQRView(HomeAssistantView): raise Unauthorized entry_id, secret = request.query_string.split("-") hass = request.app[KEY_HASS] - domain_data: dict[str, HomeKitEntryData] = hass.data[DOMAIN] + entry_data: HomeKitEntryData | None if ( - not (entry_data := domain_data.get(entry_id)) + not (entry := hass.config_entries.async_get_entry(entry_id)) + or not (entry_data := getattr(entry, "runtime_data", None)) or not secret or not entry_data.pairing_qr_secret or secret != entry_data.pairing_qr_secret diff --git a/homeassistant/components/homekit/diagnostics.py b/homeassistant/components/homekit/diagnostics.py index f31dd268b26..eb062735ad0 100644 --- a/homeassistant/components/homekit/diagnostics.py +++ b/homeassistant/components/homekit/diagnostics.py @@ -8,22 +8,19 @@ from pyhap.accessory_driver import AccessoryDriver from pyhap.state import State from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .accessories import HomeAccessory, HomeBridge -from .const import DOMAIN -from .models import HomeKitEntryData +from .models import HomeKitConfigEntry TO_REDACT = {"access_token", "entity_picture"} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: HomeKitConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - entry_data: HomeKitEntryData = hass.data[DOMAIN][entry.entry_id] - homekit = entry_data.homekit + homekit = entry.runtime_data.homekit data: dict[str, Any] = { "status": homekit.status, "config-entry": { diff --git a/homeassistant/components/homekit/models.py b/homeassistant/components/homekit/models.py index f3fa8b7504c..9b647928fdd 100644 --- a/homeassistant/components/homekit/models.py +++ b/homeassistant/components/homekit/models.py @@ -5,9 +5,13 @@ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING +from homeassistant.config_entries import ConfigEntry + if TYPE_CHECKING: from . import HomeKit +type HomeKitConfigEntry = ConfigEntry[HomeKitEntryData] + @dataclass class HomeKitEntryData: diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index d521fd6db0c..a4566efaa35 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -106,7 +106,7 @@ from .const import ( VIDEO_CODEC_H264_V4L2M2M, VIDEO_CODEC_LIBX264, ) -from .models import HomeKitEntryData +from .models import HomeKitConfigEntry _LOGGER = logging.getLogger(__name__) @@ -366,7 +366,8 @@ def async_show_setup_message( url.svg(buffer, scale=5, module_color="#000", background="#FFF") pairing_secret = secrets.token_hex(32) - entry_data: HomeKitEntryData = hass.data[DOMAIN][entry_id] + entry = cast(HomeKitConfigEntry, hass.config_entries.async_get_entry(entry_id)) + entry_data = entry.runtime_data entry_data.pairing_qr = buffer.getvalue() entry_data.pairing_qr_secret = pairing_secret diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 9653acdfabb..93458724c5e 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1843,7 +1843,11 @@ async def test_homekit_uses_system_zeroconf(hass: HomeAssistant, hk_driver) -> N entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - entry_data: HomeKitEntryData = hass.data[DOMAIN][entry.entry_id] + # New tests should not access runtime data. + # Do not use this pattern for new tests. + entry_data: HomeKitEntryData = hass.config_entries.async_get_entry( + entry.entry_id + ).runtime_data assert entry_data.homekit.driver.advertiser == system_async_zc assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index ff6ee0c6aa8..4939511166f 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -257,7 +257,12 @@ async def test_async_show_setup_msg(hass: HomeAssistant, hk_driver) -> None: hass, entry.entry_id, "bridge_name", pincode, "X-HM://0" ) await hass.async_block_till_done() - entry_data: HomeKitEntryData = hass.data[DOMAIN][entry.entry_id] + + # New tests should not access runtime data. + # Do not use this pattern for new tests. + entry_data: HomeKitEntryData = hass.config_entries.async_get_entry( + entry.entry_id + ).runtime_data assert entry_data.pairing_qr_secret assert entry_data.pairing_qr From 6223fe93c893c5f342bf93d4eed30067bf9d9537 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Jul 2024 12:08:52 +0200 Subject: [PATCH 1553/2411] Fix typo in conftest.py (#122583) --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 935ceffa108..bc139255e66 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,7 +39,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant import block_async_io from homeassistant.exceptions import ServiceNotFound -# Setup patching if dt_util time functions before any other Home Assistant imports +# Setup patching of dt_util time functions before any other Home Assistant imports from . import patch_time # noqa: F401, isort:skip from homeassistant import core as ha, loader, runner From 256a2276ef0725e81338f2bf0d302ed043b5980a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Jul 2024 05:15:40 -0500 Subject: [PATCH 1554/2411] Bump govee-ble to 0.40.0 (#122564) --- 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 a959af60ae8..c4119275dce 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -114,5 +114,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/govee_ble", "iot_class": "local_push", - "requirements": ["govee-ble==0.38.0"] + "requirements": ["govee-ble==0.40.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index cca613a479a..eed972cad71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -989,7 +989,7 @@ goslide-api==0.5.1 gotailwind==0.2.3 # homeassistant.components.govee_ble -govee-ble==0.38.0 +govee-ble==0.40.0 # homeassistant.components.govee_light_local govee-local-api==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 776fc815e34..dfad2a3f894 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -830,7 +830,7 @@ googlemaps==2.5.1 gotailwind==0.2.3 # homeassistant.components.govee_ble -govee-ble==0.38.0 +govee-ble==0.40.0 # homeassistant.components.govee_light_local govee-local-api==1.5.1 From 3b01a57de32c8274b6e75a5cce31498ee1fd6472 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Jul 2024 05:16:16 -0500 Subject: [PATCH 1555/2411] Bump aioesphomeapi to 24.6.2 (#122566) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 6e30febd7db..ff7569bbc5f 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==24.6.1", + "aioesphomeapi==24.6.2", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index eed972cad71..792e355d361 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -237,7 +237,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==24.6.1 +aioesphomeapi==24.6.2 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dfad2a3f894..94e1039eb02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==24.6.1 +aioesphomeapi==24.6.2 # homeassistant.components.flo aioflo==2021.11.0 From a89853da9df9b2716aeef3b26520981fe6decbbd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Jul 2024 05:18:24 -0500 Subject: [PATCH 1556/2411] Migrate switchbot to use entry.runtime_data (#122530) --- homeassistant/components/switchbot/__init__.py | 17 ++++------------- .../components/switchbot/binary_sensor.py | 10 +++++----- .../components/switchbot/coordinator.py | 3 +++ homeassistant/components/switchbot/cover.py | 10 +++++----- .../components/switchbot/humidifier.py | 14 +++++--------- homeassistant/components/switchbot/light.py | 9 +++------ homeassistant/components/switchbot/lock.py | 11 +++++------ homeassistant/components/switchbot/sensor.py | 10 +++++----- homeassistant/components/switchbot/switch.py | 14 +++++--------- 9 files changed, 40 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 7bf02ed37b6..75845d3f3ce 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -24,11 +24,10 @@ from .const import ( CONF_RETRY_COUNT, CONNECTABLE_SUPPORTED_MODEL_TYPES, DEFAULT_RETRY_COUNT, - DOMAIN, HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL, SupportedModels, ) -from .coordinator import SwitchbotDataUpdateCoordinator +from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { SupportedModels.BULB.value: [Platform.SENSOR, Platform.LIGHT], @@ -79,10 +78,9 @@ CLASS_BY_DEVICE = { _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: SwitchbotConfigEntry) -> bool: """Set up Switchbot from a config entry.""" assert entry.unique_id is not None - hass.data.setdefault(DOMAIN, {}) if CONF_ADDRESS not in entry.data and CONF_MAC in entry.data: # Bleak uses addresses not mac addresses which are actually # UUIDs on some platforms (MacOS). @@ -137,7 +135,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( + coordinator = entry.runtime_data = SwitchbotDataUpdateCoordinator( hass, _LOGGER, ble_device, @@ -167,13 +165,6 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> Non async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" sensor_type = entry.data[CONF_SENSOR_TYPE] - unload_ok = await hass.config_entries.async_unload_platforms( + return await hass.config_entries.async_unload_platforms( entry, PLATFORMS_BY_TYPE[sensor_type] ) - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - if not hass.config_entries.async_entries(DOMAIN): - hass.data.pop(DOMAIN) - - return unload_ok diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index 92e00a65d8a..a545ffd01ce 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -7,13 +7,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN -from .coordinator import SwitchbotDataUpdateCoordinator +from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity PARALLEL_UPDATES = 0 @@ -70,10 +68,12 @@ BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: SwitchbotConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Switchbot curtain based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( SwitchBotBinarySensor(coordinator, binary_sensor) for binary_sensor in coordinator.device.parsed_data diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 2c68b126fa5..807132d13e8 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -14,6 +14,7 @@ from homeassistant.components import bluetooth from homeassistant.components.bluetooth.active_update_coordinator import ( ActiveBluetoothDataUpdateCoordinator, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import CoreState, HomeAssistant, callback if TYPE_CHECKING: @@ -24,6 +25,8 @@ _LOGGER = logging.getLogger(__name__) DEVICE_STARTUP_TIMEOUT = 30 +type SwitchbotConfigEntry = ConfigEntry[SwitchbotDataUpdateCoordinator] + class SwitchbotDataUpdateCoordinator(ActiveBluetoothDataUpdateCoordinator[None]): """Class to manage fetching switchbot data.""" diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 8039ff8ec15..d2fd073cdcb 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -16,13 +16,11 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import DOMAIN -from .coordinator import SwitchbotDataUpdateCoordinator +from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity # Initialize the logger @@ -31,10 +29,12 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: SwitchbotConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Switchbot curtain based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data if isinstance(coordinator.device, switchbot.SwitchbotBlindTilt): async_add_entities([SwitchBotBlindTiltEntity(coordinator)]) else: diff --git a/homeassistant/components/switchbot/humidifier.py b/homeassistant/components/switchbot/humidifier.py index 3871fcb7265..40f96577842 100644 --- a/homeassistant/components/switchbot/humidifier.py +++ b/homeassistant/components/switchbot/humidifier.py @@ -2,8 +2,6 @@ from __future__ import annotations -import logging - import switchbot from homeassistant.components.humidifier import ( @@ -13,24 +11,22 @@ from homeassistant.components.humidifier import ( HumidifierEntity, HumidifierEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN -from .coordinator import SwitchbotDataUpdateCoordinator +from .coordinator import SwitchbotConfigEntry from .entity import SwitchbotSwitchedEntity PARALLEL_UPDATES = 0 -_LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: SwitchbotConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Switchbot based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([SwitchBotHumidifier(coordinator)]) + async_add_entities([SwitchBotHumidifier(entry.runtime_data)]) class SwitchBotHumidifier(SwitchbotSwitchedEntity, HumidifierEntity): diff --git a/homeassistant/components/switchbot/light.py b/homeassistant/components/switchbot/light.py index 649a8b34c75..836ba1bd4f3 100644 --- a/homeassistant/components/switchbot/light.py +++ b/homeassistant/components/switchbot/light.py @@ -13,7 +13,6 @@ from homeassistant.components.light import ( ColorMode, LightEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.color import ( @@ -21,8 +20,7 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin, ) -from .const import DOMAIN -from .coordinator import SwitchbotDataUpdateCoordinator +from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity SWITCHBOT_COLOR_MODE_TO_HASS = { @@ -35,12 +33,11 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: SwitchbotConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the switchbot light.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([SwitchbotLightEntity(coordinator)]) + async_add_entities([SwitchbotLightEntity(entry.runtime_data)]) class SwitchbotLightEntity(SwitchbotEntity, LightEntity): diff --git a/homeassistant/components/switchbot/lock.py b/homeassistant/components/switchbot/lock.py index 7b58a2f5ac3..cb41d14cf66 100644 --- a/homeassistant/components/switchbot/lock.py +++ b/homeassistant/components/switchbot/lock.py @@ -6,21 +6,20 @@ import switchbot from switchbot.const import LockStatus from homeassistant.components.lock import LockEntity, LockEntityFeature -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN -from .coordinator import SwitchbotDataUpdateCoordinator +from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: SwitchbotConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Switchbot lock based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([(SwitchBotLock(coordinator))]) + async_add_entities([(SwitchBotLock(entry.runtime_data))]) # noinspection PyAbstractClass diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 2a25d84aa8d..e696f21e082 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -9,7 +9,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -20,8 +19,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN -from .coordinator import SwitchbotDataUpdateCoordinator +from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity PARALLEL_UPDATES = 0 @@ -81,10 +79,12 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: SwitchbotConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Switchbot sensor based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data entities = [ SwitchBotSensor(coordinator, sensor) for sensor in coordinator.device.parsed_data diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 26ceee203aa..427496ef20c 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -2,33 +2,29 @@ from __future__ import annotations -import logging from typing import Any import switchbot from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import DOMAIN -from .coordinator import SwitchbotDataUpdateCoordinator +from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator from .entity import SwitchbotSwitchedEntity -# Initialize the logger -_LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: SwitchbotConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Switchbot based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([SwitchBotSwitch(coordinator)]) + async_add_entities([SwitchBotSwitch(entry.runtime_data)]) class SwitchBotSwitch(SwitchbotSwitchedEntity, SwitchEntity, RestoreEntity): From 78e24be1e707245891d7954571e0aae096e5ff9f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Jul 2024 05:19:55 -0500 Subject: [PATCH 1557/2411] Convert qingping to use entry.runtime_data (#122528) --- homeassistant/components/qingping/__init__.py | 32 ++++++++----------- .../components/qingping/binary_sensor.py | 10 ++---- homeassistant/components/qingping/sensor.py | 10 ++---- 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/qingping/__init__.py b/homeassistant/components/qingping/__init__.py index ac91f314a28..d0dcb7bfee7 100644 --- a/homeassistant/components/qingping/__init__.py +++ b/homeassistant/components/qingping/__init__.py @@ -14,37 +14,31 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN - PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) +type QingpingConfigEntry = ConfigEntry[PassiveBluetoothProcessorCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: QingpingConfigEntry) -> bool: """Set up Qingping BLE device from a config entry.""" address = entry.unique_id assert address is not None data = QingpingBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( - PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=data.update, - ) + coordinator = entry.runtime_data = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload( - coordinator.async_start() - ) # only start after all platforms have had a chance to subscribe + # only start after all platforms have had a chance to subscribe + entry.async_on_unload(coordinator.async_start()) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: QingpingConfigEntry) -> 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 + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/qingping/binary_sensor.py b/homeassistant/components/qingping/binary_sensor.py index 4c8c2b43425..5f1367fbce8 100644 --- a/homeassistant/components/qingping/binary_sensor.py +++ b/homeassistant/components/qingping/binary_sensor.py @@ -7,7 +7,6 @@ from qingping_ble import ( SensorUpdate, ) -from homeassistant import config_entries from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -16,14 +15,13 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info -from .const import DOMAIN +from . import QingpingConfigEntry from .device import device_key_to_bluetooth_entity_key BINARY_SENSOR_DESCRIPTIONS = { @@ -75,13 +73,11 @@ def sensor_update_to_bluetooth_data_update( async def async_setup_entry( hass: HomeAssistant, - entry: config_entries.ConfigEntry, + entry: QingpingConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Qingping BLE sensors.""" - coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ - entry.entry_id - ] + coordinator = entry.runtime_data processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( diff --git a/homeassistant/components/qingping/sensor.py b/homeassistant/components/qingping/sensor.py index 015df41f7bf..3d5f30c61fc 100644 --- a/homeassistant/components/qingping/sensor.py +++ b/homeassistant/components/qingping/sensor.py @@ -8,11 +8,9 @@ from qingping_ble import ( Units, ) -from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( @@ -35,7 +33,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info -from .const import DOMAIN +from . import QingpingConfigEntry from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS = { @@ -143,13 +141,11 @@ def sensor_update_to_bluetooth_data_update( async def async_setup_entry( hass: HomeAssistant, - entry: config_entries.ConfigEntry, + entry: QingpingConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Qingping BLE sensors.""" - coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ - entry.entry_id - ] + coordinator = entry.runtime_data processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( From 33d5ed52e6fc9f138dcc6369f8d468fac09f2d54 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Jul 2024 12:26:44 +0200 Subject: [PATCH 1558/2411] Avoid nesting sessions in recorder statistics tests (#122582) --- tests/components/recorder/test_statistics.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index cd8cd1a51df..993a4a5bcf8 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -24,6 +24,7 @@ from homeassistant.components.recorder.statistics import ( get_last_statistics, get_latest_short_term_statistics_with_session, get_metadata, + get_metadata_with_session, get_short_term_statistics_run_cache, list_statistic_ids, ) @@ -293,14 +294,17 @@ def mock_sensor_statistics(): } def get_fake_stats(_hass, session, start, _end): + instance = recorder.get_instance(_hass) return statistics.PlatformCompiledStatistics( [ sensor_stats("sensor.test1", start), sensor_stats("sensor.test2", start), sensor_stats("sensor.test3", start), ], - get_metadata( - _hass, statistic_ids={"sensor.test1", "sensor.test2", "sensor.test3"} + get_metadata_with_session( + instance, + session, + statistic_ids={"sensor.test1", "sensor.test2", "sensor.test3"}, ), ) From cde22a44dbd5ae2ac1165a82588418d3e5c1f961 Mon Sep 17 00:00:00 2001 From: Philip Vanloo <26272906+dukeofphilberg@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:27:10 +0200 Subject: [PATCH 1559/2411] Add LinkPlay integration (#113940) * Intial commit * Add artsound as virtual integration * Add config_flow test Add linkplay to .coveragerc Add linkplay to .strict-typing * Remove artsound component * Bump package version * Address mypy and coveragerc * Address comments * Address more feedback, add zeroconf and user flow * Catch broken bridge in async_setup_entry * Raise ConfigEntryNotReady, add __all__ * Implement new tests for the config_flow * Fix async warning * Fix test * Address feedback * Address comments * Address comment --------- Co-authored-by: Philip Vanloo <26272906+pvanloo@users.noreply.github.com> --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/linkplay/__init__.py | 44 +++ .../components/linkplay/config_flow.py | 91 +++++++ homeassistant/components/linkplay/const.py | 6 + .../components/linkplay/manifest.json | 11 + .../components/linkplay/media_player.py | 257 ++++++++++++++++++ .../components/linkplay/strings.json | 26 ++ homeassistant/components/linkplay/utils.py | 20 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + homeassistant/generated/zeroconf.py | 5 + mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/linkplay/__init__.py | 1 + tests/components/linkplay/conftest.py | 40 +++ tests/components/linkplay/test_config_flow.py | 204 ++++++++++++++ 18 files changed, 731 insertions(+) create mode 100644 homeassistant/components/linkplay/__init__.py create mode 100644 homeassistant/components/linkplay/config_flow.py create mode 100644 homeassistant/components/linkplay/const.py create mode 100644 homeassistant/components/linkplay/manifest.json create mode 100644 homeassistant/components/linkplay/media_player.py create mode 100644 homeassistant/components/linkplay/strings.json create mode 100644 homeassistant/components/linkplay/utils.py create mode 100644 tests/components/linkplay/__init__.py create mode 100644 tests/components/linkplay/conftest.py create mode 100644 tests/components/linkplay/test_config_flow.py diff --git a/.strict-typing b/.strict-typing index 8dabc9c6f27..84cdbe02424 100644 --- a/.strict-typing +++ b/.strict-typing @@ -280,6 +280,7 @@ homeassistant.components.lidarr.* homeassistant.components.lifx.* homeassistant.components.light.* homeassistant.components.linear_garage_door.* +homeassistant.components.linkplay.* homeassistant.components.litejet.* homeassistant.components.litterrobot.* homeassistant.components.local_ip.* diff --git a/CODEOWNERS b/CODEOWNERS index c059e84f677..273607234e5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -795,6 +795,8 @@ build.json @home-assistant/supervisor /tests/components/light/ @home-assistant/core /homeassistant/components/linear_garage_door/ @IceBotYT /tests/components/linear_garage_door/ @IceBotYT +/homeassistant/components/linkplay/ @Velleman +/tests/components/linkplay/ @Velleman /homeassistant/components/linux_battery/ @fabaff /homeassistant/components/litejet/ @joncar /tests/components/litejet/ @joncar diff --git a/homeassistant/components/linkplay/__init__.py b/homeassistant/components/linkplay/__init__.py new file mode 100644 index 00000000000..c0fe711a61b --- /dev/null +++ b/homeassistant/components/linkplay/__init__.py @@ -0,0 +1,44 @@ +"""Support for LinkPlay devices.""" + +from linkplay.bridge import LinkPlayBridge +from linkplay.discovery import linkplay_factory_bridge + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import PLATFORMS + + +class LinkPlayData: + """Data for LinkPlay.""" + + bridge: LinkPlayBridge + + +type LinkPlayConfigEntry = ConfigEntry[LinkPlayData] + + +async def async_setup_entry(hass: HomeAssistant, entry: LinkPlayConfigEntry) -> bool: + """Async setup hass config entry. Called when an entry has been setup.""" + + session = async_get_clientsession(hass) + if ( + bridge := await linkplay_factory_bridge(entry.data[CONF_HOST], session) + ) is None: + raise ConfigEntryNotReady( + f"Failed to connect to LinkPlay device at {entry.data[CONF_HOST]}" + ) + + entry.runtime_data = LinkPlayData() + entry.runtime_data.bridge = bridge + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: LinkPlayConfigEntry) -> bool: + """Unload a config entry.""" + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/linkplay/config_flow.py b/homeassistant/components/linkplay/config_flow.py new file mode 100644 index 00000000000..0f9c40d0fd4 --- /dev/null +++ b/homeassistant/components/linkplay/config_flow.py @@ -0,0 +1,91 @@ +"""Config flow to configure LinkPlay component.""" + +from typing import Any + +from linkplay.discovery import linkplay_factory_bridge +import voluptuous as vol + +from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_MODEL +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + + +class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN): + """LinkPlay config flow.""" + + def __init__(self) -> None: + """Initialize the LinkPlay config flow.""" + self.data: dict[str, Any] = {} + + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> ConfigFlowResult: + """Handle Zeroconf discovery.""" + + session = async_get_clientsession(self.hass) + bridge = await linkplay_factory_bridge(discovery_info.host, session) + + if bridge is None: + return self.async_abort(reason="cannot_connect") + + self.data[CONF_HOST] = discovery_info.host + self.data[CONF_MODEL] = bridge.device.name + + await self.async_set_unique_id(bridge.device.uuid) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host}) + + self.context["title_placeholders"] = { + "name": self.data[CONF_MODEL], + } + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm discovery.""" + if user_input is not None: + return self.async_create_entry( + title=self.data[CONF_MODEL], + data={CONF_HOST: self.data[CONF_HOST]}, + ) + + self._set_confirm_only() + return self.async_show_form( + step_id="discovery_confirm", + description_placeholders={ + "name": self.data[CONF_MODEL], + }, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow initialized by the user.""" + errors: dict[str, str] = {} + if user_input: + session = async_get_clientsession(self.hass) + bridge = await linkplay_factory_bridge(user_input[CONF_HOST], session) + + if bridge is not None: + self.data[CONF_HOST] = user_input[CONF_HOST] + self.data[CONF_MODEL] = bridge.device.name + + await self.async_set_unique_id(bridge.device.uuid) + self._abort_if_unique_id_configured( + updates={CONF_HOST: self.data[CONF_HOST]} + ) + + return self.async_create_entry( + title=self.data[CONF_MODEL], + data={CONF_HOST: self.data[CONF_HOST]}, + ) + + errors["base"] = "cannot_connect" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors=errors, + ) diff --git a/homeassistant/components/linkplay/const.py b/homeassistant/components/linkplay/const.py new file mode 100644 index 00000000000..48ae225dd98 --- /dev/null +++ b/homeassistant/components/linkplay/const.py @@ -0,0 +1,6 @@ +"""LinkPlay constants.""" + +from homeassistant.const import Platform + +DOMAIN = "linkplay" +PLATFORMS = [Platform.MEDIA_PLAYER] diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json new file mode 100644 index 00000000000..0345d4ad727 --- /dev/null +++ b/homeassistant/components/linkplay/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "linkplay", + "name": "LinkPlay", + "codeowners": ["@Velleman"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/linkplay", + "integration_type": "hub", + "iot_class": "local_polling", + "requirements": ["python-linkplay==0.0.5"], + "zeroconf": ["_linkplay._tcp.local."] +} diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py new file mode 100644 index 00000000000..103b09f46da --- /dev/null +++ b/homeassistant/components/linkplay/media_player.py @@ -0,0 +1,257 @@ +"""Support for LinkPlay media players.""" + +from __future__ import annotations + +from collections.abc import Callable, Coroutine +import logging +from typing import Any, Concatenate + +from linkplay.bridge import LinkPlayBridge +from linkplay.consts import EqualizerMode, LoopMode, PlayingMode, PlayingStatus +from linkplay.exceptions import LinkPlayException, LinkPlayRequestException + +from homeassistant.components import media_source +from homeassistant.components.media_player import ( + BrowseMedia, + MediaPlayerDeviceClass, + MediaPlayerEntity, + MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, + RepeatMode, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.dt import utcnow + +from . import LinkPlayConfigEntry +from .const import DOMAIN +from .utils import get_info_from_project + +_LOGGER = logging.getLogger(__name__) +STATE_MAP: dict[PlayingStatus, MediaPlayerState] = { + PlayingStatus.STOPPED: MediaPlayerState.IDLE, + PlayingStatus.PAUSED: MediaPlayerState.PAUSED, + PlayingStatus.PLAYING: MediaPlayerState.PLAYING, + PlayingStatus.LOADING: MediaPlayerState.BUFFERING, +} + +SOURCE_MAP: dict[PlayingMode, str] = { + PlayingMode.LINE_IN: "Line In", + PlayingMode.BLUETOOTH: "Bluetooth", + PlayingMode.OPTICAL: "Optical", + PlayingMode.LINE_IN_2: "Line In 2", + PlayingMode.USB_DAC: "USB DAC", + PlayingMode.COAXIAL: "Coaxial", + PlayingMode.XLR: "XLR", + PlayingMode.HDMI: "HDMI", + PlayingMode.OPTICAL_2: "Optical 2", +} + +SOURCE_MAP_INV: dict[str, PlayingMode] = {v: k for k, v in SOURCE_MAP.items()} + +REPEAT_MAP: dict[LoopMode, RepeatMode] = { + LoopMode.CONTINOUS_PLAY_ONE_SONG: RepeatMode.ONE, + LoopMode.PLAY_IN_ORDER: RepeatMode.OFF, + LoopMode.CONTINUOUS_PLAYBACK: RepeatMode.ALL, + LoopMode.RANDOM_PLAYBACK: RepeatMode.ALL, + LoopMode.LIST_CYCLE: RepeatMode.ALL, +} + +REPEAT_MAP_INV: dict[RepeatMode, LoopMode] = {v: k for k, v in REPEAT_MAP.items()} + +EQUALIZER_MAP: dict[EqualizerMode, str] = { + EqualizerMode.NONE: "None", + EqualizerMode.CLASSIC: "Classic", + EqualizerMode.POP: "Pop", + EqualizerMode.JAZZ: "Jazz", + EqualizerMode.VOCAL: "Vocal", +} + +EQUALIZER_MAP_INV: dict[str, EqualizerMode] = {v: k for k, v in EQUALIZER_MAP.items()} + +DEFAULT_FEATURES: MediaPlayerEntityFeature = ( + MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.SELECT_SOURCE + | MediaPlayerEntityFeature.SELECT_SOUND_MODE + | MediaPlayerEntityFeature.GROUPING +) + +SEEKABLE_FEATURES: MediaPlayerEntityFeature = ( + MediaPlayerEntityFeature.PREVIOUS_TRACK + | MediaPlayerEntityFeature.NEXT_TRACK + | MediaPlayerEntityFeature.REPEAT_SET + | MediaPlayerEntityFeature.SEEK +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: LinkPlayConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a media player from a config entry.""" + + async_add_entities([LinkPlayMediaPlayerEntity(entry.runtime_data.bridge)]) + + +def exception_wrap[_LinkPlayEntityT: LinkPlayMediaPlayerEntity, **_P, _R]( + func: Callable[Concatenate[_LinkPlayEntityT, _P], Coroutine[Any, Any, _R]], +) -> Callable[Concatenate[_LinkPlayEntityT, _P], Coroutine[Any, Any, _R]]: + """Define a wrapper to catch exceptions and raise HomeAssistant errors.""" + + async def _wrap(self: _LinkPlayEntityT, *args: _P.args, **kwargs: _P.kwargs) -> _R: + try: + return await func(self, *args, **kwargs) + except LinkPlayRequestException as err: + raise HomeAssistantError( + f"Exception occurred when communicating with API {func}: {err}" + ) from err + + return _wrap + + +class LinkPlayMediaPlayerEntity(MediaPlayerEntity): + """Representation of a LinkPlay media player.""" + + _attr_sound_mode_list = list(EQUALIZER_MAP.values()) + _attr_device_class = MediaPlayerDeviceClass.RECEIVER + _attr_media_content_type = MediaType.MUSIC + _attr_has_entity_name = True + _attr_name = None + + def __init__(self, bridge: LinkPlayBridge) -> None: + """Initialize the LinkPlay media player.""" + + self._bridge = bridge + self._attr_unique_id = bridge.device.uuid + + self._attr_source_list = [ + SOURCE_MAP[playing_mode] for playing_mode in bridge.device.playmode_support + ] + + manufacturer, model = get_info_from_project(bridge.device.properties["project"]) + self._attr_device_info = dr.DeviceInfo( + configuration_url=bridge.endpoint, + connections={(dr.CONNECTION_NETWORK_MAC, bridge.device.properties["MAC"])}, + hw_version=bridge.device.properties["hardware"], + identifiers={(DOMAIN, bridge.device.uuid)}, + manufacturer=manufacturer, + model=model, + name=bridge.device.name, + sw_version=bridge.device.properties["firmware"], + ) + + @exception_wrap + async def async_update(self) -> None: + """Update the state of the media player.""" + try: + await self._bridge.player.update_status() + self._update_properties() + except LinkPlayException: + self._attr_available = False + raise + + @exception_wrap + async def async_select_source(self, source: str) -> None: + """Select input source.""" + await self._bridge.player.set_play_mode(SOURCE_MAP_INV[source]) + + @exception_wrap + async def async_select_sound_mode(self, sound_mode: str) -> None: + """Select sound mode.""" + await self._bridge.player.set_equalizer_mode(EQUALIZER_MAP_INV[sound_mode]) + + @exception_wrap + async def async_mute_volume(self, mute: bool) -> None: + """Mute the volume.""" + if mute: + await self._bridge.player.mute() + else: + await self._bridge.player.unmute() + + @exception_wrap + async def async_set_volume_level(self, volume: float) -> None: + """Set volume level, range 0..1.""" + await self._bridge.player.set_volume(int(volume * 100)) + + @exception_wrap + async def async_media_pause(self) -> None: + """Send pause command.""" + await self._bridge.player.pause() + + @exception_wrap + async def async_media_play(self) -> None: + """Send play command.""" + await self._bridge.player.resume() + + @exception_wrap + async def async_set_repeat(self, repeat: RepeatMode) -> None: + """Set repeat mode.""" + await self._bridge.player.set_loop_mode(REPEAT_MAP_INV[repeat]) + + async def async_browse_media( + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: + """Return a BrowseMedia instance. + + The BrowseMedia instance will be used by the + "media_player/browse_media" websocket command. + """ + return await media_source.async_browse_media( + self.hass, + media_content_id, + # This allows filtering content. In this case it will only show audio sources. + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) + + @exception_wrap + async def async_play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: + """Play a piece of media.""" + media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) + await self._bridge.player.play(media.url) + + def _update_properties(self) -> None: + """Update the properties of the media player.""" + self._attr_available = True + self._attr_state = STATE_MAP[self._bridge.player.status] + self._attr_volume_level = self._bridge.player.volume / 100 + self._attr_is_volume_muted = self._bridge.player.muted + self._attr_repeat = REPEAT_MAP[self._bridge.player.loop_mode] + self._attr_shuffle = self._bridge.player.loop_mode == LoopMode.RANDOM_PLAYBACK + self._attr_sound_mode = EQUALIZER_MAP[self._bridge.player.equalizer_mode] + self._attr_supported_features = DEFAULT_FEATURES + + if self._bridge.player.status == PlayingStatus.PLAYING: + if self._bridge.player.total_length != 0: + self._attr_supported_features = ( + self._attr_supported_features | SEEKABLE_FEATURES + ) + + self._attr_source = SOURCE_MAP.get(self._bridge.player.play_mode, "other") + self._attr_media_position = self._bridge.player.current_position / 1000 + self._attr_media_position_updated_at = utcnow() + self._attr_media_duration = self._bridge.player.total_length / 1000 + self._attr_media_artist = self._bridge.player.artist + self._attr_media_title = self._bridge.player.title + self._attr_media_album_name = self._bridge.player.album + elif self._bridge.player.status == PlayingStatus.STOPPED: + self._attr_media_position = None + self._attr_media_position_updated_at = None + self._attr_media_artist = None + self._attr_media_title = None + self._attr_media_album_name = None diff --git a/homeassistant/components/linkplay/strings.json b/homeassistant/components/linkplay/strings.json new file mode 100644 index 00000000000..46f5b29059f --- /dev/null +++ b/homeassistant/components/linkplay/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "The hostname or IP address of the LinkPlay device." + } + }, + "discovery_confirm": { + "description": "Do you want to setup {name}?" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/linkplay/utils.py b/homeassistant/components/linkplay/utils.py new file mode 100644 index 00000000000..9ca76b3933d --- /dev/null +++ b/homeassistant/components/linkplay/utils.py @@ -0,0 +1,20 @@ +"""Utilities for the LinkPlay component.""" + +from typing import Final + +MANUFACTURER_ARTSOUND: Final[str] = "ArtSound" +MANUFACTURER_GENERIC: Final[str] = "Generic" +MODELS_ARTSOUND_SMART_ZONE4: Final[str] = "Smart Zone 4 AMP" +MODELS_ARTSOUND_SMART_HYDE: Final[str] = "Smart Hyde" +MODELS_GENERIC: Final[str] = "Generic" + + +def get_info_from_project(project: str) -> tuple[str, str]: + """Get manufacturer and model info based on given project.""" + match project: + case "SMART_ZONE4_AMP": + return MANUFACTURER_ARTSOUND, MODELS_ARTSOUND_SMART_ZONE4 + case "SMART_HYDE": + return MANUFACTURER_ARTSOUND, MODELS_ARTSOUND_SMART_HYDE + case _: + return MANUFACTURER_GENERIC, MODELS_GENERIC diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 96875e247f1..14036dcb1b5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -312,6 +312,7 @@ FLOWS = { "lidarr", "lifx", "linear_garage_door", + "linkplay", "litejet", "litterrobot", "livisi", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index f60028240fb..14d4bdc5660 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3268,6 +3268,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "linkplay": { + "name": "LinkPlay", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "linksys_smart": { "name": "Linksys Smart Wi-Fi", "integration_type": "hub", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 8efe49b7892..c53add1814d 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -589,6 +589,11 @@ ZEROCONF = { "name": "gateway*", }, ], + "_linkplay._tcp.local.": [ + { + "domain": "linkplay", + }, + ], "_lookin._tcp.local.": [ { "domain": "lookin", diff --git a/mypy.ini b/mypy.ini index bcfc55273a5..9a35b74e6d5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2556,6 +2556,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.linkplay.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.litejet.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 792e355d361..731518c7121 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2285,6 +2285,9 @@ python-juicenet==1.1.0 # homeassistant.components.tplink python-kasa[speedups]==0.7.0.5 +# homeassistant.components.linkplay +python-linkplay==0.0.5 + # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94e1039eb02..4338e86fee5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1797,6 +1797,9 @@ python-juicenet==1.1.0 # homeassistant.components.tplink python-kasa[speedups]==0.7.0.5 +# homeassistant.components.linkplay +python-linkplay==0.0.5 + # homeassistant.components.matter python-matter-server==6.3.0 diff --git a/tests/components/linkplay/__init__.py b/tests/components/linkplay/__init__.py new file mode 100644 index 00000000000..5962f7fdaba --- /dev/null +++ b/tests/components/linkplay/__init__.py @@ -0,0 +1 @@ +"""Tests for the LinkPlay integration.""" diff --git a/tests/components/linkplay/conftest.py b/tests/components/linkplay/conftest.py new file mode 100644 index 00000000000..b3d65422e08 --- /dev/null +++ b/tests/components/linkplay/conftest.py @@ -0,0 +1,40 @@ +"""Test configuration and mocks for LinkPlay component.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from linkplay.bridge import LinkPlayBridge, LinkPlayDevice +import pytest + +HOST = "10.0.0.150" +HOST_REENTRY = "10.0.0.66" +UUID = "FF31F09E-5001-FBDE-0546-2DBFFF31F09E" +NAME = "Smart Zone 1_54B9" + + +@pytest.fixture +def mock_linkplay_factory_bridge() -> Generator[AsyncMock]: + """Mock for linkplay_factory_bridge.""" + + with ( + patch( + "homeassistant.components.linkplay.config_flow.linkplay_factory_bridge" + ) as factory, + ): + bridge = AsyncMock(spec=LinkPlayBridge) + bridge.endpoint = HOST + bridge.device = AsyncMock(spec=LinkPlayDevice) + bridge.device.uuid = UUID + bridge.device.name = NAME + factory.return_value = bridge + yield factory + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.linkplay.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/linkplay/test_config_flow.py b/tests/components/linkplay/test_config_flow.py new file mode 100644 index 00000000000..641f09893c2 --- /dev/null +++ b/tests/components/linkplay/test_config_flow.py @@ -0,0 +1,204 @@ +"""Tests for the LinkPlay config flow.""" + +from ipaddress import ip_address +from unittest.mock import AsyncMock + +from homeassistant.components.linkplay.const import DOMAIN +from homeassistant.components.zeroconf import ZeroconfServiceInfo +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .conftest import HOST, HOST_REENTRY, NAME, UUID + +from tests.common import MockConfigEntry + +ZEROCONF_DISCOVERY = ZeroconfServiceInfo( + ip_address=ip_address(HOST), + ip_addresses=[ip_address(HOST)], + hostname=f"{NAME}.local.", + name=f"{NAME}._linkplay._tcp.local.", + port=59152, + type="_linkplay._tcp.local.", + properties={ + "uuid": f"uuid:{UUID}", + "mac": "00:2F:69:01:84:3A", + "security": "https 2.0", + "upnp": "1.0.0", + "bootid": "1f347886-1dd2-11b2-86ab-aa0cd2803583", + }, +) + +ZEROCONF_DISCOVERY_RE_ENTRY = ZeroconfServiceInfo( + ip_address=ip_address(HOST_REENTRY), + ip_addresses=[ip_address(HOST_REENTRY)], + hostname=f"{NAME}.local.", + name=f"{NAME}._linkplay._tcp.local.", + port=59152, + type="_linkplay._tcp.local.", + properties={ + "uuid": f"uuid:{UUID}", + "mac": "00:2F:69:01:84:3A", + "security": "https 2.0", + "upnp": "1.0.0", + "bootid": "1f347886-1dd2-11b2-86ab-aa0cd2803583", + }, +) + + +async def test_user_flow( + hass: HomeAssistant, + mock_linkplay_factory_bridge: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test user setup config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: HOST}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == { + CONF_HOST: HOST, + } + assert result["result"].unique_id == UUID + + +async def test_user_flow_re_entry( + hass: HomeAssistant, + mock_linkplay_factory_bridge: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test user setup config flow when an entry with the same unique id already exists.""" + + # Create mock entry which already has the same UUID + entry = MockConfigEntry( + data={CONF_HOST: HOST}, + domain=DOMAIN, + title=NAME, + unique_id=UUID, + ) + entry.add_to_hass(hass) + + # Re-create entry with different host + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: HOST_REENTRY}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_zeroconf_flow( + hass: HomeAssistant, + mock_linkplay_factory_bridge: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test Zeroconf flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == { + CONF_HOST: HOST, + } + assert result["result"].unique_id == UUID + + +async def test_zeroconf_flow_re_entry( + hass: HomeAssistant, + mock_linkplay_factory_bridge: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test Zeroconf flow when an entry with the same unique id already exists.""" + + # Create mock entry which already has the same UUID + entry = MockConfigEntry( + data={CONF_HOST: HOST}, + domain=DOMAIN, + title=NAME, + unique_id=UUID, + ) + entry.add_to_hass(hass) + + # Re-create entry with different host + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY_RE_ENTRY, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_errors( + hass: HomeAssistant, + mock_linkplay_factory_bridge: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test flow when the device cannot be reached.""" + + # Temporarily store bridge in a separate variable and set factory to return None + bridge = mock_linkplay_factory_bridge.return_value + mock_linkplay_factory_bridge.return_value = None + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: HOST}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + # Make linkplay_factory_bridge return a mock bridge again + mock_linkplay_factory_bridge.return_value = bridge + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: HOST}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == { + CONF_HOST: HOST, + } + assert result["result"].unique_id == UUID From a94e9d472bb990abba58a841d50b73bf817777c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Jul 2024 05:29:52 -0500 Subject: [PATCH 1560/2411] Add support for govee H5124 vibration sensors (#122562) --- homeassistant/components/govee_ble/event.py | 7 ++++ homeassistant/components/govee_ble/icons.json | 9 +++++ .../components/govee_ble/manifest.json | 4 +++ .../components/govee_ble/strings.json | 10 ++++++ homeassistant/generated/bluetooth.py | 5 +++ tests/components/govee_ble/__init__.py | 25 ++++++++++++++ tests/components/govee_ble/test_event.py | 33 +++++++++++++++++++ 7 files changed, 93 insertions(+) create mode 100644 homeassistant/components/govee_ble/icons.json diff --git a/homeassistant/components/govee_ble/event.py b/homeassistant/components/govee_ble/event.py index 67e0b0b86fb..55275477164 100644 --- a/homeassistant/components/govee_ble/event.py +++ b/homeassistant/components/govee_ble/event.py @@ -35,6 +35,11 @@ MOTION_DESCRIPTION = EventEntityDescription( event_types=["motion"], device_class=EventDeviceClass.MOTION, ) +VIBRATION_DESCRIPTION = EventEntityDescription( + key="vibration", + event_types=["vibration"], + translation_key="vibration", +) class GoveeBluetoothEventEntity(EventEntity): @@ -95,6 +100,8 @@ async def async_setup_entry( sensor_type = model_info.sensor_type if sensor_type is SensorType.MOTION: descriptions = [MOTION_DESCRIPTION] + elif sensor_type is SensorType.VIBRATION: + descriptions = [VIBRATION_DESCRIPTION] elif sensor_type is SensorType.BUTTON: button_count = model_info.button_count descriptions = BUTTON_DESCRIPTIONS[0:button_count] diff --git a/homeassistant/components/govee_ble/icons.json b/homeassistant/components/govee_ble/icons.json new file mode 100644 index 00000000000..4a5d2a2d78c --- /dev/null +++ b/homeassistant/components/govee_ble/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "event": { + "vibration": { + "default": "mdi:vibrate" + } + } + } +} diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index c4119275dce..0e425977211 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -26,6 +26,10 @@ "local_name": "GV5123*", "connectable": false }, + { + "local_name": "GV5124*", + "connectable": false + }, { "local_name": "GV5125*", "connectable": false diff --git a/homeassistant/components/govee_ble/strings.json b/homeassistant/components/govee_ble/strings.json index 7608e6c5c82..c3f4729873c 100644 --- a/homeassistant/components/govee_ble/strings.json +++ b/homeassistant/components/govee_ble/strings.json @@ -29,6 +29,16 @@ } } }, + "vibration": { + "name": "Vibration", + "state_attributes": { + "event_type": { + "state": { + "vibration": "Vibration" + } + } + } + }, "button_0": { "name": "Button 1", "state_attributes": { diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index b370c161cc0..222cf44d989 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -152,6 +152,11 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [ "domain": "govee_ble", "local_name": "GV5123*", }, + { + "connectable": False, + "domain": "govee_ble", + "local_name": "GV5124*", + }, { "connectable": False, "domain": "govee_ble", diff --git a/tests/components/govee_ble/__init__.py b/tests/components/govee_ble/__init__.py index 11f4065b506..838abb3d19c 100644 --- a/tests/components/govee_ble/__init__.py +++ b/tests/components/govee_ble/__init__.py @@ -162,3 +162,28 @@ GV5123_CLOSED_SERVICE_INFO = BluetoothServiceInfo( service_uuids=[], source="24:4C:AB:03:E6:B8", ) + + +GVH5124_SERVICE_INFO = BluetoothServiceInfo( + name="GV51242F68", + address="D3:32:39:37:2F:68", + rssi=-67, + manufacturer_data={ + 61320: b"\x08\xa2\x00\x01%\xc2YW\xfdzu\x0e\xf24\xa2\x18\xbb\x15F|[s{\x04" + }, + service_data={}, + service_uuids=[], + source="local", +) + +GVH5124_2_SERVICE_INFO = BluetoothServiceInfo( + name="GV51242F68", + address="D3:32:39:37:2F:68", + rssi=-67, + manufacturer_data={ + 61320: b"\x08\xa2\x00\x13^Sso\xaeC\x9aU\xcf\xd8\x02\x1b\xdf\xd5\xded;+\xd6\x13" + }, + service_data={}, + service_uuids=[], + source="local", +) diff --git a/tests/components/govee_ble/test_event.py b/tests/components/govee_ble/test_event.py index c2e215188ff..c41cdad3c89 100644 --- a/tests/components/govee_ble/test_event.py +++ b/tests/components/govee_ble/test_event.py @@ -9,6 +9,8 @@ from . import ( GV5121_MOTION_SERVICE_INFO_2, GV5125_BUTTON_0_SERVICE_INFO, GV5125_BUTTON_1_SERVICE_INFO, + GVH5124_2_SERVICE_INFO, + GVH5124_SERVICE_INFO, ) from tests.common import MockConfigEntry @@ -73,3 +75,34 @@ async def test_button(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_vibration_sensor(hass: HomeAssistant) -> None: + """Test setting up creates the vibration sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=GVH5124_SERVICE_INFO.address, + data={CONF_DEVICE_TYPE: "H5124"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + inject_bluetooth_service_info(hass, GVH5124_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + motion_sensor = hass.states.get("event.h5124_vibration") + first_time = motion_sensor.state + assert motion_sensor.state != STATE_UNKNOWN + + inject_bluetooth_service_info(hass, GVH5124_2_SERVICE_INFO) + await hass.async_block_till_done() + + motion_sensor = hass.states.get("event.h5124_vibration") + assert motion_sensor.state != first_time + assert motion_sensor.state != STATE_UNKNOWN + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From c12a79ecbadf32beaf57dbaa2d52077f76def94b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Jul 2024 12:31:11 +0200 Subject: [PATCH 1561/2411] Deduplicate sensor recorder tests (#122516) --- tests/components/sensor/test_recorder.py | 244 ++++------------------- 1 file changed, 39 insertions(+), 205 deletions(-) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index afa543ac12d..a9fd7fbde9c 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta import math from statistics import mean -from typing import Literal +from typing import Any, Literal from unittest.mock import patch from freezegun import freeze_time @@ -52,7 +52,11 @@ from tests.components.recorder.common import ( do_adhoc_statistics, statistics_during_period, ) -from tests.typing import RecorderInstanceGenerator, WebSocketGenerator +from tests.typing import ( + MockHAClientWebSocket, + RecorderInstanceGenerator, + WebSocketGenerator, +) BATTERY_SENSOR_ATTRIBUTES = { "device_class": "battery", @@ -116,6 +120,33 @@ async def async_list_statistic_ids( ) +async def assert_statistic_ids( + hass: HomeAssistant, + expected_result: list[dict[str, Any]], +) -> None: + """Assert statistic ids.""" + with session_scope(hass=hass, read_only=True) as session: + db_states = list(session.query(StatisticsMeta)) + assert len(db_states) == len(expected_result) + for i, db_state in enumerate(db_states): + assert db_state.statistic_id == expected_result[i]["statistic_id"] + assert ( + db_state.unit_of_measurement + == expected_result[i]["unit_of_measurement"] + ) + + +async def assert_validation_result( + client: MockHAClientWebSocket, + expected_result: dict[str, list[dict[str, Any]]], +) -> None: + """Assert statistics validation result.""" + await client.send_json_auto_id({"type": "recorder/validate_statistics"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + @pytest.mark.parametrize( ( "device_class", @@ -4178,20 +4209,6 @@ async def test_validate_unit_change_convertible( The test also asserts that the sensor's device class is ignored. """ - msg_id = 1 - - def next_id(): - nonlocal msg_id - msg_id += 1 - return msg_id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result now = dt_util.utcnow() @@ -4292,21 +4309,6 @@ async def test_validate_statistics_unit_ignore_device_class( The test asserts that the sensor's device class is ignored. """ - msg_id = 1 - - def next_id(): - nonlocal msg_id - msg_id += 1 - return msg_id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - now = dt_util.utcnow() hass.config.units = units @@ -4384,23 +4386,9 @@ async def test_validate_statistics_unit_change_no_device_class( conversion, and the unit is then changed to a unit which can and cannot be converted to the original unit. """ - msg_id = 1 attributes = dict(attributes) attributes.pop("device_class") - def next_id(): - nonlocal msg_id - msg_id += 1 - return msg_id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - now = dt_util.utcnow() hass.config.units = units @@ -4498,21 +4486,6 @@ async def test_validate_statistics_unsupported_state_class( unit, ) -> None: """Test validate_statistics.""" - msg_id = 1 - - def next_id(): - nonlocal msg_id - msg_id += 1 - return msg_id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - now = dt_util.utcnow() hass.config.units = units @@ -4566,21 +4539,6 @@ async def test_validate_statistics_sensor_no_longer_recorded( unit, ) -> None: """Test validate_statistics.""" - msg_id = 1 - - def next_id(): - nonlocal msg_id - msg_id += 1 - return msg_id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - now = dt_util.utcnow() hass.config.units = units @@ -4633,21 +4591,6 @@ async def test_validate_statistics_sensor_not_recorded( unit, ) -> None: """Test validate_statistics.""" - msg_id = 1 - - def next_id(): - nonlocal msg_id - msg_id += 1 - return msg_id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - now = dt_util.utcnow() hass.config.units = units @@ -4697,21 +4640,6 @@ async def test_validate_statistics_sensor_removed( unit, ) -> None: """Test validate_statistics.""" - msg_id = 1 - - def next_id(): - nonlocal msg_id - msg_id += 1 - return msg_id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - now = dt_util.utcnow() hass.config.units = units @@ -4760,32 +4688,6 @@ async def test_validate_statistics_unit_change_no_conversion( unit2, ) -> None: """Test validate_statistics.""" - msg_id = 1 - - def next_id(): - nonlocal msg_id - msg_id += 1 - return msg_id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - - async def assert_statistic_ids(expected_result): - with session_scope(hass=hass, read_only=True) as session: - db_states = list(session.query(StatisticsMeta)) - assert len(db_states) == len(expected_result) - for i, db_state in enumerate(db_states): - assert db_state.statistic_id == expected_result[i]["statistic_id"] - assert ( - db_state.unit_of_measurement - == expected_result[i]["unit_of_measurement"] - ) - now = dt_util.utcnow() await async_setup_component(hass, "sensor", {}) @@ -4811,7 +4713,7 @@ async def test_validate_statistics_unit_change_no_conversion( await async_recorder_block_till_done(hass) do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) - await assert_statistic_ids([]) + await assert_statistic_ids(hass, []) # No statistics, original unit - empty response hass.states.async_set( @@ -4824,7 +4726,7 @@ async def test_validate_statistics_unit_change_no_conversion( do_adhoc_statistics(hass, start=now + timedelta(hours=1)) await async_recorder_block_till_done(hass) await assert_statistic_ids( - [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}] + hass, [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}] ) await assert_validation_result(client, {}) @@ -4894,32 +4796,6 @@ async def test_validate_statistics_unit_change_equivalent_units( This tests no validation issue is created when a sensor's unit changes to an equivalent unit. """ - msg_id = 1 - - def next_id(): - nonlocal msg_id - msg_id += 1 - return msg_id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - - async def assert_statistic_ids(expected_result): - with session_scope(hass=hass, read_only=True) as session: - db_states = list(session.query(StatisticsMeta)) - assert len(db_states) == len(expected_result) - for i, db_state in enumerate(db_states): - assert db_state.statistic_id == expected_result[i]["statistic_id"] - assert ( - db_state.unit_of_measurement - == expected_result[i]["unit_of_measurement"] - ) - now = dt_util.utcnow() await async_setup_component(hass, "sensor", {}) @@ -4940,7 +4816,7 @@ async def test_validate_statistics_unit_change_equivalent_units( do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) await assert_statistic_ids( - [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}] + hass, [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}] ) # Units changed to an equivalent unit - empty response @@ -4954,7 +4830,7 @@ async def test_validate_statistics_unit_change_equivalent_units( do_adhoc_statistics(hass, start=now + timedelta(hours=1)) await async_recorder_block_till_done(hass) await assert_statistic_ids( - [{"statistic_id": "sensor.test", "unit_of_measurement": unit2}] + hass, [{"statistic_id": "sensor.test", "unit_of_measurement": unit2}] ) await assert_validation_result(client, {}) @@ -4978,33 +4854,6 @@ async def test_validate_statistics_unit_change_equivalent_units_2( This tests a validation issue is created when a sensor's unit changes to an equivalent unit which is not known to the unit converters. """ - - msg_id = 1 - - def next_id(): - nonlocal msg_id - msg_id += 1 - return msg_id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - - async def assert_statistic_ids(expected_result): - with session_scope(hass=hass, read_only=True) as session: - db_states = list(session.query(StatisticsMeta)) - assert len(db_states) == len(expected_result) - for i, db_state in enumerate(db_states): - assert db_state.statistic_id == expected_result[i]["statistic_id"] - assert ( - db_state.unit_of_measurement - == expected_result[i]["unit_of_measurement"] - ) - now = dt_util.utcnow() await async_setup_component(hass, "sensor", {}) @@ -5025,7 +4874,7 @@ async def test_validate_statistics_unit_change_equivalent_units_2( do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) await assert_statistic_ids( - [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}] + hass, [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}] ) # Units changed to an equivalent unit which is not known by the unit converters @@ -5052,7 +4901,7 @@ async def test_validate_statistics_unit_change_equivalent_units_2( do_adhoc_statistics(hass, start=now + timedelta(hours=1)) await async_recorder_block_till_done(hass) await assert_statistic_ids( - [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}] + hass, [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}] ) await assert_validation_result(client, expected) @@ -5061,21 +4910,6 @@ async def test_validate_statistics_other_domain( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test sensor does not raise issues for statistics for other domains.""" - msg_id = 1 - - def next_id(): - nonlocal msg_id - msg_id += 1 - return msg_id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) client = await hass_ws_client() From 1f2c54f112ec7f02ed7f141cd3dd33a5351d70ab Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Jul 2024 13:12:10 +0200 Subject: [PATCH 1562/2411] Avoid nesting sessions in recorder purge tests (#122581) --- tests/components/recorder/test_purge.py | 367 ++++++++++------ .../recorder/test_purge_v32_schema.py | 411 +++++++++++------- 2 files changed, 485 insertions(+), 293 deletions(-) diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index b206fefc392..60ee913cb66 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -85,12 +85,12 @@ async def test_purge_big_database(hass: HomeAssistant, recorder_mock: Recorder) with ( patch.object(recorder_mock, "max_bind_vars", 72), patch.object(recorder_mock.database_engine, "max_bind_vars", 72), - session_scope(hass=hass) as session, ): - states = session.query(States) - state_attributes = session.query(StateAttributes) - assert states.count() == 72 - assert state_attributes.count() == 3 + with session_scope(hass=hass) as session: + states = session.query(States) + state_attributes = session.query(StateAttributes) + assert states.count() == 72 + assert state_attributes.count() == 3 purge_before = dt_util.utcnow() - timedelta(days=4) @@ -102,8 +102,12 @@ async def test_purge_big_database(hass: HomeAssistant, recorder_mock: Recorder) repack=False, ) assert not finished - assert states.count() == 24 - assert state_attributes.count() == 1 + + with session_scope(hass=hass) as session: + states = session.query(States) + state_attributes = session.query(StateAttributes) + assert states.count() == 24 + assert state_attributes.count() == 1 async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> None: @@ -122,24 +126,30 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> events = session.query(Events).filter(Events.event_type == "state_changed") assert events.count() == 0 - assert "test.recorder2" in recorder_mock.states_manager._last_committed_id - purge_before = dt_util.utcnow() - timedelta(days=4) + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id - # run purge_old_data() - finished = purge_old_data( - recorder_mock, - purge_before, - states_batch_size=1, - events_batch_size=1, - repack=False, - ) - assert not finished + purge_before = dt_util.utcnow() - timedelta(days=4) + + # run purge_old_data() + finished = purge_old_data( + recorder_mock, + purge_before, + states_batch_size=1, + events_batch_size=1, + repack=False, + ) + assert not finished + + with session_scope(hass=hass) as session: + states = session.query(States) + state_attributes = session.query(StateAttributes) assert states.count() == 2 assert state_attributes.count() == 1 - assert "test.recorder2" in recorder_mock.states_manager._last_committed_id + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id + with session_scope(hass=hass) as session: states_after_purge = list(session.query(States)) # Since these states are deleted in batches, we can't guarantee the order # but we can look them up by state @@ -150,27 +160,33 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> assert dontpurgeme_5.old_state_id == dontpurgeme_4.state_id assert dontpurgeme_4.old_state_id is None - finished = purge_old_data(recorder_mock, purge_before, repack=False) - assert finished + finished = purge_old_data(recorder_mock, purge_before, repack=False) + assert finished + + with session_scope(hass=hass) as session: + states = session.query(States) + state_attributes = session.query(StateAttributes) assert states.count() == 2 assert state_attributes.count() == 1 - assert "test.recorder2" in recorder_mock.states_manager._last_committed_id + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id - # run purge_old_data again - purge_before = dt_util.utcnow() - finished = purge_old_data( - recorder_mock, - purge_before, - states_batch_size=1, - events_batch_size=1, - repack=False, - ) - assert not finished + # run purge_old_data again + purge_before = dt_util.utcnow() + finished = purge_old_data( + recorder_mock, + purge_before, + states_batch_size=1, + events_batch_size=1, + repack=False, + ) + assert not finished + + with session_scope(hass=hass) as session: assert states.count() == 0 assert state_attributes.count() == 0 - assert "test.recorder2" not in recorder_mock.states_manager._last_committed_id + assert "test.recorder2" not in recorder_mock.states_manager._last_committed_id # Add some more states await _add_test_states(hass) @@ -290,29 +306,39 @@ async def test_purge_old_events(hass: HomeAssistant, recorder_mock: Recorder) -> ) assert events.count() == 6 - purge_before = dt_util.utcnow() - timedelta(days=4) + purge_before = dt_util.utcnow() - timedelta(days=4) - # run purge_old_data() - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - events_batch_size=1, - states_batch_size=1, + # run purge_old_data() + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) + assert not finished + + with session_scope(hass=hass) as session: + events = session.query(Events).filter( + Events.event_type_id.in_(select_event_type_ids(TEST_EVENT_TYPES)) ) - assert not finished all_events = events.all() assert events.count() == 2, f"Should have 2 events left: {all_events}" - # we should only have 2 events left - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - events_batch_size=1, - states_batch_size=1, + # we should only have 2 events left + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) + assert finished + + with session_scope(hass=hass) as session: + events = session.query(Events).filter( + Events.event_type_id.in_(select_event_type_ids(TEST_EVENT_TYPES)) ) - assert finished assert events.count() == 2 @@ -327,26 +353,29 @@ async def test_purge_old_recorder_runs( recorder_runs = session.query(RecorderRuns) assert recorder_runs.count() == 7 - purge_before = dt_util.utcnow() + purge_before = dt_util.utcnow() - # run purge_old_data() - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - events_batch_size=1, - states_batch_size=1, - ) - assert not finished + # run purge_old_data() + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) + assert not finished - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - events_batch_size=1, - states_batch_size=1, - ) - assert finished + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) + assert finished + + with session_scope(hass=hass) as session: + recorder_runs = session.query(RecorderRuns) assert recorder_runs.count() == 1 @@ -361,14 +390,17 @@ async def test_purge_old_statistics_runs( statistics_runs = session.query(StatisticsRuns) assert statistics_runs.count() == 7 - purge_before = dt_util.utcnow() + purge_before = dt_util.utcnow() - # run purge_old_data() - finished = purge_old_data(recorder_mock, purge_before, repack=False) - assert not finished + # run purge_old_data() + finished = purge_old_data(recorder_mock, purge_before, repack=False) + assert not finished - finished = purge_old_data(recorder_mock, purge_before, repack=False) - assert finished + finished = purge_old_data(recorder_mock, purge_before, repack=False) + assert finished + + with session_scope(hass=hass) as session: + statistics_runs = session.query(StatisticsRuns) assert statistics_runs.count() == 1 @@ -1655,39 +1687,54 @@ async def test_purge_many_old_events( ) assert events.count() == old_events_count * 6 - purge_before = dt_util.utcnow() - timedelta(days=4) + purge_before = dt_util.utcnow() - timedelta(days=4) - # run purge_old_data() - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - states_batch_size=3, - events_batch_size=3, + # run purge_old_data() + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + states_batch_size=3, + events_batch_size=3, + ) + assert not finished + + with session_scope(hass=hass) as session: + events = session.query(Events).filter( + Events.event_type_id.in_(select_event_type_ids(TEST_EVENT_TYPES)) ) - assert not finished assert events.count() == old_events_count * 3 - # we should only have 2 groups of events left - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - states_batch_size=3, - events_batch_size=3, + # we should only have 2 groups of events left + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + states_batch_size=3, + events_batch_size=3, + ) + assert finished + + with session_scope(hass=hass) as session: + events = session.query(Events).filter( + Events.event_type_id.in_(select_event_type_ids(TEST_EVENT_TYPES)) ) - assert finished assert events.count() == old_events_count * 2 - # we should now purge everything - finished = purge_old_data( - recorder_mock, - dt_util.utcnow(), - repack=False, - states_batch_size=20, - events_batch_size=20, + # we should now purge everything + finished = purge_old_data( + recorder_mock, + dt_util.utcnow(), + repack=False, + states_batch_size=20, + events_batch_size=20, + ) + assert finished + + with session_scope(hass=hass) as session: + events = session.query(Events).filter( + Events.event_type_id.in_(select_event_type_ids(TEST_EVENT_TYPES)) ) - assert finished assert events.count() == 0 @@ -1762,37 +1809,61 @@ async def test_purge_old_events_purges_the_event_type_ids( assert events.count() == 30 assert event_types.count() == 4 - # run purge_old_data() - finished = purge_old_data( - recorder_mock, - far_past, - repack=False, + # run purge_old_data() + finished = purge_old_data( + recorder_mock, + far_past, + repack=False, + ) + assert finished + + with session_scope(hass=hass) as session: + events = session.query(Events).where( + Events.event_type_id.in_(test_event_type_ids) + ) + event_types = session.query(EventTypes).where( + EventTypes.event_type_id.in_(test_event_type_ids) ) - assert finished assert events.count() == 30 # We should remove the unused event type assert event_types.count() == 3 - assert "EVENT_TEST_UNUSED" not in recorder_mock.event_type_manager._id_map + assert "EVENT_TEST_UNUSED" not in recorder_mock.event_type_manager._id_map - # we should only have 10 events left since - # only one event type was recorded now - finished = purge_old_data( - recorder_mock, - utcnow, - repack=False, + # we should only have 10 events left since + # only one event type was recorded now + finished = purge_old_data( + recorder_mock, + utcnow, + repack=False, + ) + assert finished + + with session_scope(hass=hass) as session: + events = session.query(Events).where( + Events.event_type_id.in_(test_event_type_ids) + ) + event_types = session.query(EventTypes).where( + EventTypes.event_type_id.in_(test_event_type_ids) ) - assert finished assert events.count() == 10 assert event_types.count() == 1 - # Purge everything - finished = purge_old_data( - recorder_mock, - utcnow + timedelta(seconds=1), - repack=False, + # Purge everything + finished = purge_old_data( + recorder_mock, + utcnow + timedelta(seconds=1), + repack=False, + ) + assert finished + + with session_scope(hass=hass) as session: + events = session.query(Events).where( + Events.event_type_id.in_(test_event_type_ids) + ) + event_types = session.query(EventTypes).where( + EventTypes.event_type_id.in_(test_event_type_ids) ) - assert finished assert events.count() == 0 assert event_types.count() == 0 @@ -1864,37 +1935,55 @@ async def test_purge_old_states_purges_the_state_metadata_ids( assert states.count() == 30 assert states_meta.count() == 4 - # run purge_old_data() - finished = purge_old_data( - recorder_mock, - far_past, - repack=False, + # run purge_old_data() + finished = purge_old_data( + recorder_mock, + far_past, + repack=False, + ) + assert finished + + with session_scope(hass=hass) as session: + states = session.query(States).where(States.metadata_id.in_(test_metadata_ids)) + states_meta = session.query(StatesMeta).where( + StatesMeta.metadata_id.in_(test_metadata_ids) ) - assert finished assert states.count() == 30 # We should remove the unused entity_id assert states_meta.count() == 3 - assert "sensor.unused" not in recorder_mock.event_type_manager._id_map + assert "sensor.unused" not in recorder_mock.event_type_manager._id_map - # we should only have 10 states left since - # only one event type was recorded now - finished = purge_old_data( - recorder_mock, - utcnow, - repack=False, + # we should only have 10 states left since + # only one event type was recorded now + finished = purge_old_data( + recorder_mock, + utcnow, + repack=False, + ) + assert finished + + with session_scope(hass=hass) as session: + states = session.query(States).where(States.metadata_id.in_(test_metadata_ids)) + states_meta = session.query(StatesMeta).where( + StatesMeta.metadata_id.in_(test_metadata_ids) ) - assert finished assert states.count() == 10 assert states_meta.count() == 1 - # Purge everything - finished = purge_old_data( - recorder_mock, - utcnow + timedelta(seconds=1), - repack=False, + # Purge everything + finished = purge_old_data( + recorder_mock, + utcnow + timedelta(seconds=1), + repack=False, + ) + assert finished + + with session_scope(hass=hass) as session: + states = session.query(States).where(States.metadata_id.in_(test_metadata_ids)) + states_meta = session.query(StatesMeta).where( + StatesMeta.metadata_id.in_(test_metadata_ids) ) - assert finished assert states.count() == 0 assert states_meta.count() == 0 diff --git a/tests/components/recorder/test_purge_v32_schema.py b/tests/components/recorder/test_purge_v32_schema.py index 9f3a124629a..0754b2e911c 100644 --- a/tests/components/recorder/test_purge_v32_schema.py +++ b/tests/components/recorder/test_purge_v32_schema.py @@ -96,17 +96,21 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> assert events.count() == 0 assert "test.recorder2" in recorder_mock.states_manager._last_committed_id - purge_before = dt_util.utcnow() - timedelta(days=4) + purge_before = dt_util.utcnow() - timedelta(days=4) - # run purge_old_data() - finished = purge_old_data( - recorder_mock, - purge_before, - states_batch_size=1, - events_batch_size=1, - repack=False, - ) - assert not finished + # run purge_old_data() + finished = purge_old_data( + recorder_mock, + purge_before, + states_batch_size=1, + events_batch_size=1, + repack=False, + ) + assert not finished + + with session_scope(hass=hass) as session: + states = session.query(States) + state_attributes = session.query(StateAttributes) assert states.count() == 2 assert state_attributes.count() == 1 @@ -122,23 +126,31 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> assert dontpurgeme_5.old_state_id == dontpurgeme_4.state_id assert dontpurgeme_4.old_state_id is None - finished = purge_old_data(recorder_mock, purge_before, repack=False) - assert finished + finished = purge_old_data(recorder_mock, purge_before, repack=False) + assert finished + + with session_scope(hass=hass) as session: + states = session.query(States) + state_attributes = session.query(StateAttributes) assert states.count() == 2 assert state_attributes.count() == 1 assert "test.recorder2" in recorder_mock.states_manager._last_committed_id - # run purge_old_data again - purge_before = dt_util.utcnow() - finished = purge_old_data( - recorder_mock, - purge_before, - states_batch_size=1, - events_batch_size=1, - repack=False, - ) - assert not finished + # run purge_old_data again + purge_before = dt_util.utcnow() + finished = purge_old_data( + recorder_mock, + purge_before, + states_batch_size=1, + events_batch_size=1, + repack=False, + ) + assert not finished + + with session_scope(hass=hass) as session: + states = session.query(States) + state_attributes = session.query(StateAttributes) assert states.count() == 0 assert state_attributes.count() == 0 @@ -270,26 +282,32 @@ async def test_purge_old_events(hass: HomeAssistant, recorder_mock: Recorder) -> purge_before = dt_util.utcnow() - timedelta(days=4) - # run purge_old_data() - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - events_batch_size=1, - states_batch_size=1, - ) - assert not finished + # run purge_old_data() + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) + assert not finished + + with session_scope(hass=hass) as session: + events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%")) assert events.count() == 2 - # we should only have 2 events left - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - events_batch_size=1, - states_batch_size=1, - ) - assert finished + # we should only have 2 events left + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) + assert finished + + with session_scope(hass=hass) as session: + events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%")) assert events.count() == 2 @@ -306,26 +324,29 @@ async def test_purge_old_recorder_runs( recorder_runs = session.query(RecorderRuns) assert recorder_runs.count() == 7 - purge_before = dt_util.utcnow() + purge_before = dt_util.utcnow() - # run purge_old_data() - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - events_batch_size=1, - states_batch_size=1, - ) - assert not finished + # run purge_old_data() + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) + assert not finished - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - events_batch_size=1, - states_batch_size=1, - ) - assert finished + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) + assert finished + + with session_scope(hass=hass) as session: + recorder_runs = session.query(RecorderRuns) assert recorder_runs.count() == 1 @@ -342,14 +363,17 @@ async def test_purge_old_statistics_runs( statistics_runs = session.query(StatisticsRuns) assert statistics_runs.count() == 7 - purge_before = dt_util.utcnow() + purge_before = dt_util.utcnow() - # run purge_old_data() - finished = purge_old_data(recorder_mock, purge_before, repack=False) - assert not finished + # run purge_old_data() + finished = purge_old_data(recorder_mock, purge_before, repack=False) + assert not finished - finished = purge_old_data(recorder_mock, purge_before, repack=False) - assert finished + finished = purge_old_data(recorder_mock, purge_before, repack=False) + assert finished + + with session_scope(hass=hass) as session: + statistics_runs = session.query(StatisticsRuns) assert statistics_runs.count() == 1 @@ -945,39 +969,48 @@ async def test_purge_many_old_events( events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%")) assert events.count() == old_events_count * 6 - purge_before = dt_util.utcnow() - timedelta(days=4) + purge_before = dt_util.utcnow() - timedelta(days=4) - # run purge_old_data() - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - states_batch_size=3, - events_batch_size=3, - ) - assert not finished + # run purge_old_data() + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + states_batch_size=3, + events_batch_size=3, + ) + assert not finished + + with session_scope(hass=hass) as session: + events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%")) assert events.count() == old_events_count * 3 - # we should only have 2 groups of events left - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - states_batch_size=3, - events_batch_size=3, - ) - assert finished + # we should only have 2 groups of events left + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + states_batch_size=3, + events_batch_size=3, + ) + assert finished + + with session_scope(hass=hass) as session: + events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%")) assert events.count() == old_events_count * 2 - # we should now purge everything - finished = purge_old_data( - recorder_mock, - dt_util.utcnow(), - repack=False, - states_batch_size=20, - events_batch_size=20, - ) - assert finished + # we should now purge everything + finished = purge_old_data( + recorder_mock, + dt_util.utcnow(), + repack=False, + states_batch_size=20, + events_batch_size=20, + ) + assert finished + + with session_scope(hass=hass) as session: + events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%")) assert events.count() == 0 @@ -1038,39 +1071,65 @@ async def test_purge_can_mix_legacy_and_new_format( assert states_with_event_id.count() == 50 assert states_without_event_id.count() == 51 - purge_before = dt_util.utcnow() - timedelta(days=4) - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, + purge_before = dt_util.utcnow() - timedelta(days=4) + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + ) + assert not finished + + with session_scope(hass=hass) as session: + states_with_event_id = session.query(States).filter( + States.event_id.is_not(None) + ) + states_without_event_id = session.query(States).filter( + States.event_id.is_(None) ) - assert not finished assert states_with_event_id.count() == 0 assert states_without_event_id.count() == 51 - # At this point all the legacy states are gone - # and we switch methods - purge_before = dt_util.utcnow() - timedelta(days=4) - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - events_batch_size=1, - states_batch_size=1, + + # At this point all the legacy states are gone + # and we switch methods + purge_before = dt_util.utcnow() - timedelta(days=4) + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) + # Since we only allow one iteration, we won't + # check if we are finished this loop similar + # to the legacy method + assert not finished + + with session_scope(hass=hass) as session: + states_with_event_id = session.query(States).filter( + States.event_id.is_not(None) + ) + states_without_event_id = session.query(States).filter( + States.event_id.is_(None) ) - # Since we only allow one iteration, we won't - # check if we are finished this loop similar - # to the legacy method - assert not finished assert states_with_event_id.count() == 0 assert states_without_event_id.count() == 1 - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - events_batch_size=100, - states_batch_size=100, + + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + events_batch_size=100, + states_batch_size=100, + ) + assert finished + + with session_scope(hass=hass) as session: + states_with_event_id = session.query(States).filter( + States.event_id.is_not(None) + ) + states_without_event_id = session.query(States).filter( + States.event_id.is_(None) ) - assert finished assert states_with_event_id.count() == 0 assert states_without_event_id.count() == 1 _add_state_without_event_linkage( @@ -1078,12 +1137,21 @@ async def test_purge_can_mix_legacy_and_new_format( ) assert states_with_event_id.count() == 0 assert states_without_event_id.count() == 2 - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, + + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + ) + assert finished + + with session_scope(hass=hass) as session: + states_with_event_id = session.query(States).filter( + States.event_id.is_not(None) + ) + states_without_event_id = session.query(States).filter( + States.event_id.is_(None) ) - assert finished # The broken state without a timestamp # does not prevent future purges. Its ignored. assert states_with_event_id.count() == 0 @@ -1185,39 +1253,65 @@ async def test_purge_can_mix_legacy_and_new_format_with_detached_state( assert states_with_event_id.count() == 52 assert states_without_event_id.count() == 51 - purge_before = dt_util.utcnow() - timedelta(days=4) - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, + purge_before = dt_util.utcnow() - timedelta(days=4) + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + ) + assert not finished + + with session_scope(hass=hass) as session: + states_with_event_id = session.query(States).filter( + States.event_id.is_not(None) + ) + states_without_event_id = session.query(States).filter( + States.event_id.is_(None) ) - assert not finished assert states_with_event_id.count() == 0 assert states_without_event_id.count() == 51 - # At this point all the legacy states are gone - # and we switch methods - purge_before = dt_util.utcnow() - timedelta(days=4) - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - events_batch_size=1, - states_batch_size=1, + + # At this point all the legacy states are gone + # and we switch methods + purge_before = dt_util.utcnow() - timedelta(days=4) + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) + # Since we only allow one iteration, we won't + # check if we are finished this loop similar + # to the legacy method + assert not finished + + with session_scope(hass=hass) as session: + states_with_event_id = session.query(States).filter( + States.event_id.is_not(None) + ) + states_without_event_id = session.query(States).filter( + States.event_id.is_(None) ) - # Since we only allow one iteration, we won't - # check if we are finished this loop similar - # to the legacy method - assert not finished assert states_with_event_id.count() == 0 assert states_without_event_id.count() == 1 - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, - events_batch_size=100, - states_batch_size=100, + + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + events_batch_size=100, + states_batch_size=100, + ) + assert finished + + with session_scope(hass=hass) as session: + states_with_event_id = session.query(States).filter( + States.event_id.is_not(None) + ) + states_without_event_id = session.query(States).filter( + States.event_id.is_(None) ) - assert finished assert states_with_event_id.count() == 0 assert states_without_event_id.count() == 1 _add_state_without_event_linkage( @@ -1225,12 +1319,21 @@ async def test_purge_can_mix_legacy_and_new_format_with_detached_state( ) assert states_with_event_id.count() == 0 assert states_without_event_id.count() == 2 - finished = purge_old_data( - recorder_mock, - purge_before, - repack=False, + + finished = purge_old_data( + recorder_mock, + purge_before, + repack=False, + ) + assert finished + + with session_scope(hass=hass) as session: + states_with_event_id = session.query(States).filter( + States.event_id.is_not(None) + ) + states_without_event_id = session.query(States).filter( + States.event_id.is_(None) ) - assert finished # The broken state without a timestamp # does not prevent future purges. Its ignored. assert states_with_event_id.count() == 0 From 3caffa4dad2aac4402e9b5ed8f258cbcc320fd47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 25 Jul 2024 13:34:02 +0200 Subject: [PATCH 1563/2411] Update aioqsw to v0.4.0 (#122586) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update aioqsw to v0.4.0 Signed-off-by: Álvaro Fernández Rojas * trigger CI --------- Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/manifest.json | 2 +- homeassistant/components/qnap_qsw/sensor.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/qnap_qsw/test_diagnostics.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index 76949b95cbd..b8c62133193 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", "iot_class": "local_polling", "loggers": ["aioqsw"], - "requirements": ["aioqsw==0.3.5"] + "requirements": ["aioqsw==0.4.0"] } diff --git a/homeassistant/components/qnap_qsw/sensor.py b/homeassistant/components/qnap_qsw/sensor.py index b64c0aaad82..009bc63b2c6 100644 --- a/homeassistant/components/qnap_qsw/sensor.py +++ b/homeassistant/components/qnap_qsw/sensor.py @@ -25,7 +25,7 @@ from aioqsw.const import ( QSD_TEMP_MAX, QSD_TX_OCTETS, QSD_TX_SPEED, - QSD_UPTIME, + QSD_UPTIME_SECONDS, ) from homeassistant.components.sensor import ( @@ -145,7 +145,7 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfTime.SECONDS, state_class=SensorStateClass.TOTAL_INCREASING, - subkey=QSD_UPTIME, + subkey=QSD_UPTIME_SECONDS, ), ) diff --git a/requirements_all.txt b/requirements_all.txt index 731518c7121..4d48afe2f2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -335,7 +335,7 @@ aiopvpc==4.2.2 aiopyarr==23.4.0 # homeassistant.components.qnap_qsw -aioqsw==0.3.5 +aioqsw==0.4.0 # homeassistant.components.rainforest_raven aioraven==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4338e86fee5..2663194667f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -314,7 +314,7 @@ aiopvpc==4.2.2 aiopyarr==23.4.0 # homeassistant.components.qnap_qsw -aioqsw==0.3.5 +aioqsw==0.4.0 # homeassistant.components.rainforest_raven aioraven==0.7.0 diff --git a/tests/components/qnap_qsw/test_diagnostics.py b/tests/components/qnap_qsw/test_diagnostics.py index 8bca9d8d989..ccaac458b12 100644 --- a/tests/components/qnap_qsw/test_diagnostics.py +++ b/tests/components/qnap_qsw/test_diagnostics.py @@ -25,7 +25,7 @@ from aioqsw.const import ( QSD_SYSTEM_TIME, QSD_TEMP, QSD_TEMP_MAX, - QSD_UPTIME, + QSD_UPTIME_SECONDS, QSD_VERSION, ) @@ -118,6 +118,6 @@ async def test_config_entry_diagnostics( assert ( sys_time_diag.items() >= { - QSD_UPTIME: sys_time_mock[API_UPTIME], + QSD_UPTIME_SECONDS: sys_time_mock[API_UPTIME], }.items() ) From e795f81f730b44abd744f361ad02331662625060 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Jul 2024 06:35:00 -0500 Subject: [PATCH 1564/2411] Add support for govee presence sensor h5127 (#122568) --- .../components/govee_ble/binary_sensor.py | 8 ++++ .../components/govee_ble/manifest.json | 9 ++++ homeassistant/generated/bluetooth.py | 16 +++++++ tests/components/govee_ble/__init__.py | 29 ++++++++++++ .../govee_ble/test_binary_sensor.py | 47 ++++++++++++++++++- 5 files changed, 108 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/govee_ble/binary_sensor.py b/homeassistant/components/govee_ble/binary_sensor.py index 82033300797..e5966124216 100644 --- a/homeassistant/components/govee_ble/binary_sensor.py +++ b/homeassistant/components/govee_ble/binary_sensor.py @@ -31,6 +31,14 @@ BINARY_SENSOR_DESCRIPTIONS = { key=GoveeBLEBinarySensorDeviceClass.WINDOW, device_class=BinarySensorDeviceClass.WINDOW, ), + GoveeBLEBinarySensorDeviceClass.MOTION: BinarySensorEntityDescription( + key=GoveeBLEBinarySensorDeviceClass.MOTION, + device_class=BinarySensorDeviceClass.MOTION, + ), + GoveeBLEBinarySensorDeviceClass.OCCUPANCY: BinarySensorEntityDescription( + key=GoveeBLEBinarySensorDeviceClass.OCCUPANCY, + device_class=BinarySensorDeviceClass.OCCUPANCY, + ), } diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 0e425977211..d9827e9155c 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -38,6 +38,10 @@ "local_name": "GV5126*", "connectable": false }, + { + "local_name": "GVH5127*", + "connectable": false + }, { "manufacturer_id": 1, "service_uuid": "0000ec88-0000-1000-8000-00805f9b34fb", @@ -111,6 +115,11 @@ { "manufacturer_id": 61320, "connectable": false + }, + { + "manufacturer_id": 34819, + "manufacturer_data_start": [236, 0, 0, 1], + "connectable": false } ], "codeowners": ["@bdraco", "@PierreAronnax"], diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 222cf44d989..cda011d1bef 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -167,6 +167,11 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [ "domain": "govee_ble", "local_name": "GV5126*", }, + { + "connectable": False, + "domain": "govee_ble", + "local_name": "GVH5127*", + }, { "connectable": False, "domain": "govee_ble", @@ -256,6 +261,17 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [ "domain": "govee_ble", "manufacturer_id": 61320, }, + { + "connectable": False, + "domain": "govee_ble", + "manufacturer_data_start": [ + 236, + 0, + 0, + 1, + ], + "manufacturer_id": 34819, + }, { "domain": "homekit_controller", "manufacturer_data_start": [ diff --git a/tests/components/govee_ble/__init__.py b/tests/components/govee_ble/__init__.py index 838abb3d19c..66c5b0b832c 100644 --- a/tests/components/govee_ble/__init__.py +++ b/tests/components/govee_ble/__init__.py @@ -187,3 +187,32 @@ GVH5124_2_SERVICE_INFO = BluetoothServiceInfo( service_uuids=[], source="local", ) + + +GVH5127_MOTION_SERVICE_INFO = BluetoothServiceInfo( + name="GVH51275E3F", + address="D0:C9:07:1B:5E:3F", + rssi=-61, + manufacturer_data={34819: b"\xec\x00\x01\x01\x01\x11"}, + service_data={}, + service_uuids=[], + source="Core Bluetooth", +) +GVH5127_PRESENT_SERVICE_INFO = BluetoothServiceInfo( + name="GVH51275E3F", + address="D0:C9:07:1B:5E:3F", + rssi=-60, + manufacturer_data={34819: b"\xec\x00\x01\x01\x01\x01"}, + service_data={}, + service_uuids=[], + source="Core Bluetooth", +) +GVH5127_ABSENT_SERVICE_INFO = BluetoothServiceInfo( + name="GVH51275E3F", + address="D0:C9:07:1B:5E:3F", + rssi=-53, + manufacturer_data={34819: b"\xec\x00\x01\x01\x00\x00"}, + service_data={}, + service_uuids=[], + source="Core Bluetooth", +) diff --git a/tests/components/govee_ble/test_binary_sensor.py b/tests/components/govee_ble/test_binary_sensor.py index a0acf4c461e..cf8b54ef54f 100644 --- a/tests/components/govee_ble/test_binary_sensor.py +++ b/tests/components/govee_ble/test_binary_sensor.py @@ -4,7 +4,13 @@ from homeassistant.components.govee_ble.const import CONF_DEVICE_TYPE, DOMAIN from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from . import GV5123_CLOSED_SERVICE_INFO, GV5123_OPEN_SERVICE_INFO +from . import ( + GV5123_CLOSED_SERVICE_INFO, + GV5123_OPEN_SERVICE_INFO, + GVH5127_ABSENT_SERVICE_INFO, + GVH5127_MOTION_SERVICE_INFO, + GVH5127_PRESENT_SERVICE_INFO, +) from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info @@ -37,3 +43,42 @@ async def test_window_sensor(hass: HomeAssistant) -> None: assert motion_sensor.state == STATE_OFF assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_presence_sensor(hass: HomeAssistant) -> None: + """Test the presence sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=GVH5127_ABSENT_SERVICE_INFO.address, + data={CONF_DEVICE_TYPE: "H5127"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info(hass, GVH5127_ABSENT_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + motion_sensor = hass.states.get("binary_sensor.h51275e3f_motion") + assert motion_sensor.state == STATE_OFF + occupancy_sensor = hass.states.get("binary_sensor.h51275e3f_occupancy") + assert occupancy_sensor.state == STATE_OFF + + inject_bluetooth_service_info(hass, GVH5127_PRESENT_SERVICE_INFO) + await hass.async_block_till_done() + + motion_sensor = hass.states.get("binary_sensor.h51275e3f_motion") + assert motion_sensor.state == STATE_OFF + occupancy_sensor = hass.states.get("binary_sensor.h51275e3f_occupancy") + assert occupancy_sensor.state == STATE_ON + + inject_bluetooth_service_info(hass, GVH5127_MOTION_SERVICE_INFO) + await hass.async_block_till_done() + + motion_sensor = hass.states.get("binary_sensor.h51275e3f_motion") + assert motion_sensor.state == STATE_ON + occupancy_sensor = hass.states.get("binary_sensor.h51275e3f_occupancy") + assert occupancy_sensor.state == STATE_ON From e6d0bc7d5dbdaada8d50e8bc6dcfa09b0590d01d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 25 Jul 2024 14:16:21 +0200 Subject: [PATCH 1565/2411] Add device to Worldclock (#122557) * Add device to Worldclock * has_entity_name --- homeassistant/components/worldclock/sensor.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py index 7ca2b252beb..f4879ca08c4 100644 --- a/homeassistant/components/worldclock/sensor.py +++ b/homeassistant/components/worldclock/sensor.py @@ -14,6 +14,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_TIME_ZONE from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -85,15 +86,22 @@ class WorldClockSensor(SensorEntity): """Representation of a World clock sensor.""" _attr_icon = "mdi:clock" + _attr_has_entity_name = True + _attr_name = None def __init__( self, time_zone: tzinfo | None, name: str, time_format: str, unique_id: str ) -> None: """Initialize the sensor.""" - self._attr_name = name self._time_zone = time_zone self._time_format = time_format self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=name, + entry_type=DeviceEntryType.SERVICE, + manufacturer="Worldclock", + ) async def async_update(self) -> None: """Get the time and updates the states.""" From f1b933ae0cd4556982ff5aaf61018124f9962e9e Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:42:10 +0200 Subject: [PATCH 1566/2411] Add uncalibrated sensor for tedee (#122594) * add uncalibrated sensor * change off icon --- .../components/tedee/binary_sensor.py | 8 ++++ homeassistant/components/tedee/icons.json | 8 ++++ homeassistant/components/tedee/strings.json | 3 ++ .../tedee/snapshots/test_binary_sensor.ambr | 47 +++++++++++++++++++ tests/components/tedee/test_binary_sensor.py | 10 ++-- 5 files changed, 70 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tedee/binary_sensor.py b/homeassistant/components/tedee/binary_sensor.py index 98c70f32450..3a7d1a12f2e 100644 --- a/homeassistant/components/tedee/binary_sensor.py +++ b/homeassistant/components/tedee/binary_sensor.py @@ -47,6 +47,14 @@ ENTITIES: tuple[TedeeBinarySensorEntityDescription, ...] = ( is_on_fn=lambda lock: lock.is_enabled_pullspring, entity_category=EntityCategory.DIAGNOSTIC, ), + TedeeBinarySensorEntityDescription( + key="uncalibrated", + translation_key="uncalibrated", + is_on_fn=lambda lock: lock.state == TedeeLockState.UNCALIBRATED, + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), ) diff --git a/homeassistant/components/tedee/icons.json b/homeassistant/components/tedee/icons.json index 3f98462b22f..4fae6e0fcd2 100644 --- a/homeassistant/components/tedee/icons.json +++ b/homeassistant/components/tedee/icons.json @@ -1,5 +1,13 @@ { "entity": { + "binary_sensor": { + "uncalibrated": { + "state": { + "on": "mdi:sync-alert", + "off": "mdi:sync" + } + } + }, "sensor": { "pullspring_duration": { "default": "mdi:timer-lock-open" diff --git a/homeassistant/components/tedee/strings.json b/homeassistant/components/tedee/strings.json index e16cdbdd330..0668d1370b4 100644 --- a/homeassistant/components/tedee/strings.json +++ b/homeassistant/components/tedee/strings.json @@ -53,6 +53,9 @@ }, "semi_locked": { "name": "Semi locked" + }, + "uncalibrated": { + "name": "Lock uncalibrated" } }, "sensor": { diff --git a/tests/components/tedee/snapshots/test_binary_sensor.ambr b/tests/components/tedee/snapshots/test_binary_sensor.ambr index 8c9dca1bd12..385e4ac9bc1 100644 --- a/tests/components/tedee/snapshots/test_binary_sensor.ambr +++ b/tests/components/tedee/snapshots/test_binary_sensor.ambr @@ -32,6 +32,39 @@ 'unit_of_measurement': None, }) # --- +# name: test_binary_sensors[entry-lock_uncalibrated] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.lock_1a2b_lock_uncalibrated', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lock uncalibrated', + 'platform': 'tedee', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uncalibrated', + 'unique_id': '12345-uncalibrated', + 'unit_of_measurement': None, + }) +# --- # name: test_binary_sensors[entry-pullspring_enabled] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -112,6 +145,20 @@ 'state': 'off', }) # --- +# name: test_binary_sensors[state-lock_uncalibrated] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Lock-1A2B Lock uncalibrated', + }), + 'context': , + 'entity_id': 'binary_sensor.lock_1a2b_lock_uncalibrated', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_binary_sensors[state-pullspring_enabled] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/tedee/test_binary_sensor.py b/tests/components/tedee/test_binary_sensor.py index ee8c318d2dd..788d31c84d2 100644 --- a/tests/components/tedee/test_binary_sensor.py +++ b/tests/components/tedee/test_binary_sensor.py @@ -15,20 +15,17 @@ from tests.common import async_fire_time_changed pytestmark = pytest.mark.usefixtures("init_integration") -BINARY_SENSORS = ( - "charging", - "semi_locked", - "pullspring_enabled", -) +BINARY_SENSORS = ("charging", "semi_locked", "pullspring_enabled", "lock_uncalibrated") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_binary_sensors( hass: HomeAssistant, mock_tedee: MagicMock, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: - """Test tedee battery charging sensor.""" + """Test tedee binary sensor.""" for key in BINARY_SENSORS: state = hass.states.get(f"binary_sensor.lock_1a2b_{key}") assert state @@ -39,6 +36,7 @@ async def test_binary_sensors( assert entry == snapshot(name=f"entry-{key}") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_new_binary_sensors( hass: HomeAssistant, mock_tedee: MagicMock, From 0c7ab2062f28db1c789d8e6303ed2ddace04118a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Jul 2024 15:44:48 +0200 Subject: [PATCH 1567/2411] Avoid creating nested sessions in recorder migration (#122580) --- homeassistant/components/recorder/migration.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 3ef9b65e259..6f438106ab6 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -2010,7 +2010,7 @@ class MigrationTask(RecorderTask): # Schedule a new migration task if this one didn't finish instance.queue_task(MigrationTask(self.migrator)) else: - self.migrator.migration_done(instance) + self.migrator.migration_done(instance, None) @dataclass(slots=True) @@ -2046,14 +2046,14 @@ class BaseRunTimeMigration(ABC): if self.needs_migrate(instance, session): instance.queue_task(self.task(self)) else: - self.migration_done(instance) + self.migration_done(instance, session) @staticmethod @abstractmethod def migrate_data(instance: Recorder) -> bool: """Migrate some data, returns True if migration is completed.""" - def migration_done(self, instance: Recorder) -> None: + def migration_done(self, instance: Recorder, session: Session | None) -> None: """Will be called after migrate returns True or if migration is not needed.""" @abstractmethod @@ -2274,7 +2274,7 @@ class EventTypeIDMigration(BaseRunTimeMigrationWithQuery): _LOGGER.debug("Migrating event_types done=%s", is_done) return is_done - def migration_done(self, instance: Recorder) -> None: + def migration_done(self, instance: Recorder, session: Session | None) -> None: """Will be called after migrate returns True.""" _LOGGER.debug("Activating event_types manager as all data is migrated") instance.event_type_manager.active = True @@ -2367,7 +2367,7 @@ class EntityIDMigration(BaseRunTimeMigrationWithQuery): _LOGGER.debug("Migrating entity_ids done=%s", is_done) return is_done - def migration_done(self, instance: Recorder) -> None: + def migration_done(self, instance: Recorder, _session: Session | None) -> None: """Will be called after migrate returns True.""" # The migration has finished, now we start the post migration # to remove the old entity_id data from the states table @@ -2375,9 +2375,14 @@ class EntityIDMigration(BaseRunTimeMigrationWithQuery): # so we set active to True _LOGGER.debug("Activating states_meta manager as all data is migrated") instance.states_meta_manager.active = True + session_generator = ( + contextlib.nullcontext(_session) + if _session + else session_scope(session=instance.get_session()) + ) with ( contextlib.suppress(SQLAlchemyError), - session_scope(session=instance.get_session()) as session, + session_generator as session, ): # If ix_states_entity_id_last_updated_ts still exists # on the states table it means the entity id migration From e8eb1ed35c2cfed83f2e98cf9b924dab649859b4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 25 Jul 2024 07:46:09 -0700 Subject: [PATCH 1568/2411] Bump airgradient to 0.7.1 removing mashumaro direct dependency (#122534) --- homeassistant/components/airgradient/config_flow.py | 12 ++++++++---- homeassistant/components/airgradient/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airgradient/test_config_flow.py | 11 ++++++----- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/airgradient/config_flow.py b/homeassistant/components/airgradient/config_flow.py index 6fc12cf7397..93cd0be61c4 100644 --- a/homeassistant/components/airgradient/config_flow.py +++ b/homeassistant/components/airgradient/config_flow.py @@ -2,9 +2,13 @@ from typing import Any -from airgradient import AirGradientClient, AirGradientError, ConfigurationControl +from airgradient import ( + AirGradientClient, + AirGradientError, + AirGradientParseError, + ConfigurationControl, +) from awesomeversion import AwesomeVersion -from mashumaro import MissingField import voluptuous as vol from homeassistant.components import zeroconf @@ -83,10 +87,10 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN): self.client = AirGradientClient(user_input[CONF_HOST], session=session) try: current_measures = await self.client.get_current_measures() + except AirGradientParseError: + return self.async_abort(reason="invalid_version") except AirGradientError: errors["base"] = "cannot_connect" - except MissingField: - return self.async_abort(reason="invalid_version") else: await self.async_set_unique_id(current_measures.serial_number) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index af345bc25ed..efb18ae5752 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/airgradient", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["airgradient==0.7.0"], + "requirements": ["airgradient==0.7.1"], "zeroconf": ["_airgradient._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 4d48afe2f2e..ac2db12aae5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -410,7 +410,7 @@ aiowithings==3.0.2 aioymaps==1.2.4 # homeassistant.components.airgradient -airgradient==0.7.0 +airgradient==0.7.1 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2663194667f..c2bcb11fa83 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -389,7 +389,7 @@ aiowithings==3.0.2 aioymaps==1.2.4 # homeassistant.components.airgradient -airgradient==0.7.0 +airgradient==0.7.1 # homeassistant.components.airly airly==1.1.0 diff --git a/tests/components/airgradient/test_config_flow.py b/tests/components/airgradient/test_config_flow.py index 217d2ac0e8c..222ac5d04af 100644 --- a/tests/components/airgradient/test_config_flow.py +++ b/tests/components/airgradient/test_config_flow.py @@ -3,8 +3,11 @@ from ipaddress import ip_address from unittest.mock import AsyncMock -from airgradient import AirGradientConnectionError, ConfigurationControl -from mashumaro import MissingField +from airgradient import ( + AirGradientConnectionError, + AirGradientParseError, + ConfigurationControl, +) from homeassistant.components.airgradient import DOMAIN from homeassistant.components.zeroconf import ZeroconfServiceInfo @@ -141,9 +144,7 @@ async def test_flow_old_firmware_version( mock_setup_entry: AsyncMock, ) -> None: """Test flow with old firmware version.""" - mock_airgradient_client.get_current_measures.side_effect = MissingField( - "", object, object - ) + mock_airgradient_client.get_current_measures.side_effect = AirGradientParseError result = await hass.config_entries.flow.async_init( DOMAIN, From 131ce094903894a6194bfc69af03a0445e524f7a Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Thu, 25 Jul 2024 16:27:08 +0100 Subject: [PATCH 1569/2411] Allow nightly Mealie versions to pass (#121761) * Allow invalid versions to pass * Add log warning * Change log message * Add assert for log --- homeassistant/components/mealie/__init__.py | 8 ++++++-- .../components/mealie/config_flow.py | 2 +- tests/components/mealie/test_config_flow.py | 1 - tests/components/mealie/test_init.py | 19 ++++++++++++++++++- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mealie/__init__.py b/homeassistant/components/mealie/__init__.py index 393ef1e5ecd..df9faf6e540 100644 --- a/homeassistant/components/mealie/__init__.py +++ b/homeassistant/components/mealie/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, MIN_REQUIRED_MEALIE_VERSION +from .const import DOMAIN, LOGGER, MIN_REQUIRED_MEALIE_VERSION from .coordinator import ( MealieConfigEntry, MealieData, @@ -55,7 +55,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo except MealieConnectionError as error: raise ConfigEntryNotReady(error) from error - if not version.valid or version < MIN_REQUIRED_MEALIE_VERSION: + if not version.valid: + LOGGER.warning( + "It seems like you are using the nightly version of Mealie, nightly versions could have changes that stop this integration working" + ) + if version.valid and version < MIN_REQUIRED_MEALIE_VERSION: raise ConfigEntryError( translation_domain=DOMAIN, translation_key="version_error", diff --git a/homeassistant/components/mealie/config_flow.py b/homeassistant/components/mealie/config_flow.py index 6b75f57313c..ccbedff04fc 100644 --- a/homeassistant/components/mealie/config_flow.py +++ b/homeassistant/components/mealie/config_flow.py @@ -55,7 +55,7 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN): except Exception: # noqa: BLE001 LOGGER.exception("Unexpected error") return {"base": "unknown"}, None - if not version.valid or version < MIN_REQUIRED_MEALIE_VERSION: + if version.valid and version < MIN_REQUIRED_MEALIE_VERSION: return {"base": "mealie_version"}, None return {}, info.user_id diff --git a/tests/components/mealie/test_config_flow.py b/tests/components/mealie/test_config_flow.py index 8edc89c3213..f2886578744 100644 --- a/tests/components/mealie/test_config_flow.py +++ b/tests/components/mealie/test_config_flow.py @@ -91,7 +91,6 @@ async def test_flow_errors( ("v1.0.0beta-5"), ("v1.0.0-RC2"), ("v0.1.0"), - ("something"), ], ) async def test_flow_version_error( diff --git a/tests/components/mealie/test_init.py b/tests/components/mealie/test_init.py index ed5b1290a9b..5c25af5a0a0 100644 --- a/tests/components/mealie/test_init.py +++ b/tests/components/mealie/test_init.py @@ -70,7 +70,6 @@ async def test_setup_failure( ("v1.0.0beta-5"), ("v1.0.0-RC2"), ("v0.1.0"), - ("something"), ], ) async def test_setup_too_old( @@ -87,6 +86,24 @@ async def test_setup_too_old( assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR +async def test_setup_invalid( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test setup of Mealie entry with too old version of Mealie.""" + mock_mealie_client.get_about.return_value = About(version="nightly") + + await setup_integration(hass, mock_config_entry) + + assert ( + "It seems like you are using the nightly version of Mealie, nightly versions could have changes that stop this integration working" + in caplog.text + ) + assert mock_config_entry.state is ConfigEntryState.LOADED + + async def test_load_unload_entry( hass: HomeAssistant, mock_mealie_client: AsyncMock, From 08d7beb8033b24305b2568b9cf69bac1f9278c15 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 25 Jul 2024 17:32:31 +0200 Subject: [PATCH 1570/2411] Use snapshots in UniFi update tests (#122599) --- .../unifi/snapshots/test_update.ambr | 229 ++++++++++++++++++ tests/components/unifi/test_update.py | 75 +++--- 2 files changed, 256 insertions(+), 48 deletions(-) create mode 100644 tests/components/unifi/snapshots/test_update.ambr diff --git a/tests/components/unifi/snapshots/test_update.ambr b/tests/components/unifi/snapshots/test_update.ambr new file mode 100644 index 00000000000..99a403a8f21 --- /dev/null +++ b/tests/components/unifi/snapshots/test_update.ambr @@ -0,0 +1,229 @@ +# serializer version: 1 +# name: test_entity_and_device_data[site_payload0-device_payload0][update.device_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'update', + 'entity_category': , + 'entity_id': 'update.device_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'device_update-00:00:00:00:01:01', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][update.device_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'device_class': 'firmware', + 'entity_picture': 'https://brands.home-assistant.io/_/unifi/icon.png', + 'friendly_name': 'Device 1', + 'in_progress': False, + 'installed_version': '4.0.42.10433', + 'latest_version': '4.3.17.11279', + 'release_summary': None, + 'release_url': None, + 'skipped_version': None, + 'supported_features': , + 'title': None, + }), + 'context': , + 'entity_id': 'update.device_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][update.device_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'update', + 'entity_category': , + 'entity_id': 'update.device_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'device_update-00:00:00:00:01:02', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][update.device_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'device_class': 'firmware', + 'entity_picture': 'https://brands.home-assistant.io/_/unifi/icon.png', + 'friendly_name': 'Device 2', + 'in_progress': False, + 'installed_version': '4.0.42.10433', + 'latest_version': '4.0.42.10433', + 'release_summary': None, + 'release_url': None, + 'skipped_version': None, + 'supported_features': , + 'title': None, + }), + 'context': , + 'entity_id': 'update.device_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_and_device_data[site_payload1-device_payload0][update.device_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'update', + 'entity_category': , + 'entity_id': 'update.device_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'device_update-00:00:00:00:01:01', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload1-device_payload0][update.device_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'device_class': 'firmware', + 'entity_picture': 'https://brands.home-assistant.io/_/unifi/icon.png', + 'friendly_name': 'Device 1', + 'in_progress': False, + 'installed_version': '4.0.42.10433', + 'latest_version': '4.3.17.11279', + 'release_summary': None, + 'release_url': None, + 'skipped_version': None, + 'supported_features': , + 'title': None, + }), + 'context': , + 'entity_id': 'update.device_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload1-device_payload0][update.device_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'update', + 'entity_category': , + 'entity_id': 'update.device_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'device_update-00:00:00:00:01:02', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload1-device_payload0][update.device_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'device_class': 'firmware', + 'entity_picture': 'https://brands.home-assistant.io/_/unifi/icon.png', + 'friendly_name': 'Device 2', + 'in_progress': False, + 'installed_version': '4.0.42.10433', + 'latest_version': '4.0.42.10433', + 'release_summary': None, + 'release_url': None, + 'skipped_version': None, + 'supported_features': , + 'title': None, + }), + 'context': , + 'entity_id': 'update.device_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py index 3b1de6c4456..a8fe9231159 100644 --- a/tests/components/unifi/test_update.py +++ b/tests/components/unifi/test_update.py @@ -1,9 +1,11 @@ """The tests for the UniFi Network update platform.""" from copy import deepcopy +from unittest.mock import patch from aiounifi.models.message import MessageKey import pytest +from syrupy import SnapshotAssertion from yarl import URL from homeassistant.components.unifi.const import CONF_SITE_ID @@ -13,23 +15,23 @@ from homeassistant.components.update import ( ATTR_LATEST_VERSION, DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL, - UpdateDeviceClass, - UpdateEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, CONF_HOST, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from tests.common import snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker +# Device with new firmware available DEVICE_1 = { "board_rev": 3, "device_id": "mock-id", @@ -46,6 +48,7 @@ DEVICE_1 = { "upgrade_to_firmware": "4.3.17.11279", } +# Device without new firmware available DEVICE_2 = { "board_rev": 3, "device_id": "mock-id", @@ -61,43 +64,38 @@ DEVICE_2 = { @pytest.mark.parametrize("device_payload", [[DEVICE_1, DEVICE_2]]) +@pytest.mark.parametrize( + "site_payload", + [ + [{"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}], + [{"desc": "Site name", "name": "site_id", "role": "not admin", "_id": "1"}], + ], +) +async def test_entity_and_device_data( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory, + snapshot: SnapshotAssertion, +) -> None: + """Validate entity and device data with and without admin rights.""" + with patch("homeassistant.components.unifi.PLATFORMS", [Platform.UPDATE]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize("device_payload", [[DEVICE_1]]) @pytest.mark.usefixtures("config_entry_setup") async def test_device_updates(hass: HomeAssistant, mock_websocket_message) -> None: """Test the update_items function with some devices.""" - assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 2 - - # Device with new firmware available - device_1_state = hass.states.get("update.device_1") assert device_1_state.state == STATE_ON - assert device_1_state.attributes[ATTR_INSTALLED_VERSION] == "4.0.42.10433" - assert device_1_state.attributes[ATTR_LATEST_VERSION] == "4.3.17.11279" assert device_1_state.attributes[ATTR_IN_PROGRESS] is False - assert device_1_state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE - assert ( - device_1_state.attributes[ATTR_SUPPORTED_FEATURES] - == UpdateEntityFeature.PROGRESS | UpdateEntityFeature.INSTALL - ) - - # Device without new firmware available - - device_2_state = hass.states.get("update.device_2") - assert device_2_state.state == STATE_OFF - assert device_2_state.attributes[ATTR_INSTALLED_VERSION] == "4.0.42.10433" - assert device_2_state.attributes[ATTR_LATEST_VERSION] == "4.0.42.10433" - assert device_2_state.attributes[ATTR_IN_PROGRESS] is False - assert device_2_state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE - assert ( - device_2_state.attributes[ATTR_SUPPORTED_FEATURES] - == UpdateEntityFeature.PROGRESS | UpdateEntityFeature.INSTALL - ) # Simulate start of update device_1 = deepcopy(DEVICE_1) device_1["state"] = 4 mock_websocket_message(message=MessageKey.DEVICE, data=device_1) - await hass.async_block_till_done() device_1_state = hass.states.get("update.device_1") assert device_1_state.state == STATE_ON @@ -112,7 +110,6 @@ async def test_device_updates(hass: HomeAssistant, mock_websocket_message) -> No device_1["upgradable"] = False del device_1["upgrade_to_firmware"] mock_websocket_message(message=MessageKey.DEVICE, data=device_1) - await hass.async_block_till_done() device_1_state = hass.states.get("update.device_1") assert device_1_state.state == STATE_OFF @@ -121,22 +118,6 @@ async def test_device_updates(hass: HomeAssistant, mock_websocket_message) -> No assert device_1_state.attributes[ATTR_IN_PROGRESS] is False -@pytest.mark.parametrize("device_payload", [[DEVICE_1]]) -@pytest.mark.parametrize( - "site_payload", - [[{"desc": "Site name", "name": "site_id", "role": "not admin", "_id": "1"}]], -) -@pytest.mark.usefixtures("config_entry_setup") -async def test_not_admin(hass: HomeAssistant) -> None: - """Test that the INSTALL feature is not available on a non-admin account.""" - assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 - device_state = hass.states.get("update.device_1") - assert device_state.state == STATE_ON - assert ( - device_state.attributes[ATTR_SUPPORTED_FEATURES] == UpdateEntityFeature.PROGRESS - ) - - @pytest.mark.parametrize("device_payload", [[DEVICE_1]]) async def test_install( hass: HomeAssistant, @@ -144,7 +125,6 @@ async def test_install( config_entry_setup: ConfigEntry, ) -> None: """Test the device update install call.""" - assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 device_state = hass.states.get("update.device_1") assert device_state.state == STATE_ON @@ -176,7 +156,6 @@ async def test_install( @pytest.mark.usefixtures("config_entry_setup") async def test_hub_state_change(hass: HomeAssistant, mock_websocket_state) -> None: """Verify entities state reflect on hub becoming unavailable.""" - assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 assert hass.states.get("update.device_1").state == STATE_ON # Controller unavailable From ec957e4a94277b898e6d89948c311efbffe43855 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Jul 2024 17:32:49 +0200 Subject: [PATCH 1571/2411] Run statistics on 5-minute intervals in tests (#122592) * Run statistics on 5-minute intervals in tests * Fix test failing when mysql does not return rows in insert order --- tests/components/recorder/common.py | 12 +- tests/components/recorder/test_statistics.py | 3 +- .../components/recorder/test_websocket_api.py | 93 +++++--- tests/components/sensor/test_recorder.py | 222 ++++++++++++------ 4 files changed, 226 insertions(+), 104 deletions(-) diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 003b07ab80f..aee35fceb80 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -79,10 +79,18 @@ async def async_block_recorder(hass: HomeAssistant, seconds: float) -> None: await event.wait() +def get_start_time(start: datetime) -> datetime: + """Calculate a valid start time for statistics.""" + start_minutes = start.minute - start.minute % 5 + return start.replace(minute=start_minutes, second=0, microsecond=0) + + def do_adhoc_statistics(hass: HomeAssistant, **kwargs: Any) -> None: """Trigger an adhoc statistics run.""" if not (start := kwargs.get("start")): start = statistics.get_start_time() + elif (start.minute % 5) != 0 or start.second != 0 or start.microsecond != 0: + raise ValueError(f"Statistics must start on 5 minute boundary got {start}") get_instance(hass).queue_task(StatisticsTask(start, False)) @@ -291,11 +299,11 @@ def record_states(hass): wait_recording_done(hass) return hass.states.get(entity_id) - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) one = zero + timedelta(seconds=1 * 5) two = one + timedelta(seconds=15 * 5) three = two + timedelta(seconds=30 * 5) - four = three + timedelta(seconds=15 * 5) + four = three + timedelta(seconds=14 * 5) states = {mp: [], sns1: [], sns2: [], sns3: [], sns4: []} with freeze_time(one) as freezer: diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 993a4a5bcf8..074a98e5230 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -44,6 +44,7 @@ from .common import ( async_record_states, async_wait_recording_done, do_adhoc_statistics, + get_start_time, statistics_during_period, ) @@ -342,7 +343,7 @@ async def test_compile_periodic_statistics_exception( """Test exception handling when compiling periodic statistics.""" await async_setup_component(hass, "sensor", {}) - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) do_adhoc_statistics(hass, start=now) do_adhoc_statistics(hass, start=now + timedelta(minutes=5)) await async_wait_recording_done(hass) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index bcdf07502b0..ed36f4dacbf 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -35,6 +35,7 @@ from .common import ( async_wait_recording_done, create_engine_test, do_adhoc_statistics, + get_start_time, statistics_during_period, ) from .conftest import InstrumentedMigration @@ -155,12 +156,17 @@ async def test_statistics_during_period( recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test statistics_during_period.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) hass.config.units = US_CUSTOMARY_SYSTEM await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", 10, attributes=POWER_SENSOR_KW_ATTRIBUTES) + hass.states.async_set( + "sensor.test", + 10, + attributes=POWER_SENSOR_KW_ATTRIBUTES, + timestamp=now.timestamp(), + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=now) @@ -608,7 +614,12 @@ async def test_statistic_during_period( } # Test we can automatically convert units - hass.states.async_set("sensor.test", None, attributes=ENERGY_SENSOR_WH_ATTRIBUTES) + hass.states.async_set( + "sensor.test", + None, + attributes=ENERGY_SENSOR_WH_ATTRIBUTES, + timestamp=now.timestamp(), + ) await client.send_json_auto_id( { "type": "recorder/statistic_during_period", @@ -1265,11 +1276,13 @@ async def test_statistics_during_period_unit_conversion( converted_value, ) -> None: """Test statistics_during_period.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", state, attributes=attributes) + hass.states.async_set( + "sensor.test", state, attributes=attributes, timestamp=now.timestamp() + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=now) @@ -1350,12 +1363,16 @@ async def test_sum_statistics_during_period_unit_conversion( converted_value, ) -> None: """Test statistics_during_period.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", 0, attributes=attributes) - hass.states.async_set("sensor.test", state, attributes=attributes) + hass.states.async_set( + "sensor.test", 0, attributes=attributes, timestamp=now.timestamp() + ) + hass.states.async_set( + "sensor.test", state, attributes=attributes, timestamp=now.timestamp() + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=now) @@ -1471,7 +1488,7 @@ async def test_statistics_during_period_in_the_past( ) -> None: """Test statistics_during_period in the past.""" await hass.config.async_set_time_zone("UTC") - now = dt_util.utcnow().replace() + now = get_start_time(dt_util.utcnow()) hass.config.units = US_CUSTOMARY_SYSTEM await async_setup_component(hass, "sensor", {}) @@ -1726,7 +1743,7 @@ async def test_list_statistic_ids( unit_class, ) -> None: """Test list_statistic_ids.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) has_mean = attributes["state_class"] == "measurement" has_sum = not has_mean @@ -1740,7 +1757,9 @@ async def test_list_statistic_ids( assert response["success"] assert response["result"] == [] - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", 10, attributes=attributes, timestamp=now.timestamp() + ) await async_wait_recording_done(hass) await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) @@ -1890,7 +1909,7 @@ async def test_list_statistic_ids_unit_change( unit_class, ) -> None: """Test list_statistic_ids.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) has_mean = attributes["state_class"] == "measurement" has_sum = not has_mean @@ -1903,7 +1922,9 @@ async def test_list_statistic_ids_unit_change( assert response["success"] assert response["result"] == [] - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", 10, attributes=attributes, timestamp=now.timestamp() + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=now) @@ -1926,7 +1947,9 @@ async def test_list_statistic_ids_unit_change( ] # Change the state unit - hass.states.async_set("sensor.test", 10, attributes=attributes2) + hass.states.async_set( + "sensor.test", 10, attributes=attributes2, timestamp=now.timestamp() + ) await client.send_json_auto_id({"type": "recorder/list_statistic_ids"}) response = await client.receive_json() @@ -1965,7 +1988,7 @@ async def test_clear_statistics( recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test removing statistics.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) units = METRIC_SYSTEM attributes = POWER_SENSOR_KW_ATTRIBUTES @@ -1975,9 +1998,15 @@ async def test_clear_statistics( hass.config.units = units await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test1", state, attributes=attributes) - hass.states.async_set("sensor.test2", state * 2, attributes=attributes) - hass.states.async_set("sensor.test3", state * 3, attributes=attributes) + hass.states.async_set( + "sensor.test1", state, attributes=attributes, timestamp=now.timestamp() + ) + hass.states.async_set( + "sensor.test2", state * 2, attributes=attributes, timestamp=now.timestamp() + ) + hass.states.async_set( + "sensor.test3", state * 3, attributes=attributes, timestamp=now.timestamp() + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=now) @@ -2088,7 +2117,7 @@ async def test_update_statistics_metadata( new_display_unit, ) -> None: """Test removing statistics.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) units = METRIC_SYSTEM attributes = POWER_SENSOR_KW_ATTRIBUTES | {"device_class": None} @@ -2097,7 +2126,9 @@ async def test_update_statistics_metadata( hass.config.units = units await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", state, attributes=attributes) + hass.states.async_set( + "sensor.test", state, attributes=attributes, timestamp=now.timestamp() + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, period="hourly", start=now) @@ -2177,7 +2208,7 @@ async def test_change_statistics_unit( recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test change unit of recorded statistics.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) units = METRIC_SYSTEM attributes = POWER_SENSOR_KW_ATTRIBUTES | {"device_class": None} @@ -2186,7 +2217,9 @@ async def test_change_statistics_unit( hass.config.units = units await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", state, attributes=attributes) + hass.states.async_set( + "sensor.test", state, attributes=attributes, timestamp=now.timestamp() + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, period="hourly", start=now) @@ -2322,7 +2355,7 @@ async def test_change_statistics_unit_errors( caplog: pytest.LogCaptureFixture, ) -> None: """Test change unit of recorded statistics.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) units = METRIC_SYSTEM attributes = POWER_SENSOR_KW_ATTRIBUTES | {"device_class": None} @@ -2376,7 +2409,9 @@ async def test_change_statistics_unit_errors( hass.config.units = units await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", state, attributes=attributes) + hass.states.async_set( + "sensor.test", state, attributes=attributes, timestamp=now.timestamp() + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, period="hourly", start=now) @@ -2599,7 +2634,7 @@ async def test_get_statistics_metadata( unit_class, ) -> None: """Test get_statistics_metadata.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) has_mean = attributes["state_class"] == "measurement" has_sum = not has_mean @@ -2678,10 +2713,14 @@ async def test_get_statistics_metadata( } ] - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", 10, attributes=attributes, timestamp=now.timestamp() + ) await async_wait_recording_done(hass) - hass.states.async_set("sensor.test2", 10, attributes=attributes) + hass.states.async_set( + "sensor.test2", 10, attributes=attributes, timestamp=now.timestamp() + ) await async_wait_recording_done(hass) await client.send_json_auto_id( diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index a9fd7fbde9c..2bd751a553c 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -50,6 +50,7 @@ from tests.components.recorder.common import ( async_recorder_block_till_done, async_wait_recording_done, do_adhoc_statistics, + get_start_time, statistics_during_period, ) from tests.typing import ( @@ -194,7 +195,7 @@ async def test_compile_hourly_statistics( max, ) -> None: """Test compiling hourly statistics.""" - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -278,7 +279,7 @@ async def test_compile_hourly_statistics_with_some_same_last_updated( If the last updated value is the same we will have a zero duration. """ - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -392,7 +393,7 @@ async def test_compile_hourly_statistics_with_all_same_last_updated( If the last updated value is the same we will have a zero duration. """ - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -498,7 +499,7 @@ async def test_compile_hourly_statistics_only_state_is_and_end_of_period( max, ) -> None: """Test compiling hourly statistics when the only state at end of period.""" - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -592,7 +593,7 @@ async def test_compile_hourly_statistics_purged_state_changes( unit_class, ) -> None: """Test compiling hourly statistics.""" - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -663,7 +664,7 @@ async def test_compile_hourly_statistics_wrong_unit( attributes, ) -> None: """Test compiling hourly statistics for sensor with unit not matching device class.""" - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -887,7 +888,7 @@ async def test_compile_hourly_sum_statistics_amount( factor, ) -> None: """Test compiling hourly statistics.""" - period0 = dt_util.utcnow() + period0 = get_start_time(dt_util.utcnow()) period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) @@ -1071,7 +1072,7 @@ async def test_compile_hourly_sum_statistics_amount_reset_every_state_change( factor, ) -> None: """Test compiling hourly statistics.""" - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -1194,7 +1195,7 @@ async def test_compile_hourly_sum_statistics_amount_invalid_last_reset( factor, ) -> None: """Test compiling hourly statistics.""" - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -1294,7 +1295,7 @@ async def test_compile_hourly_sum_statistics_nan_inf_state( factor, ) -> None: """Test compiling hourly statistics with nan and inf states.""" - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -1429,7 +1430,7 @@ async def test_compile_hourly_sum_statistics_negative_state( offset, ) -> None: """Test compiling hourly statistics with negative states.""" - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) hass.data.pop(loader.DATA_CUSTOM_COMPONENTS) mocksensor = MockSensor(name="custom_sensor") @@ -1437,10 +1438,11 @@ async def test_compile_hourly_sum_statistics_negative_state( setup_test_component_platform(hass, DOMAIN, [mocksensor], built_in=False) await async_setup_component(hass, "homeassistant", {}) - await async_setup_component( - hass, "sensor", {"sensor": [{"platform": "demo"}, {"platform": "test"}]} - ) - await hass.async_block_till_done() + with freeze_time(zero) as freezer: + await async_setup_component( + hass, "sensor", {"sensor": [{"platform": "demo"}, {"platform": "test"}]} + ) + await hass.async_block_till_done() attributes = { "device_class": device_class, "state_class": state_class, @@ -1541,7 +1543,7 @@ async def test_compile_hourly_sum_statistics_total_no_reset( factor, ) -> None: """Test compiling hourly statistics.""" - period0 = dt_util.utcnow() + period0 = get_start_time(dt_util.utcnow()) period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) @@ -1654,7 +1656,7 @@ async def test_compile_hourly_sum_statistics_total_increasing( factor, ) -> None: """Test compiling hourly statistics.""" - period0 = dt_util.utcnow() + period0 = get_start_time(dt_util.utcnow()) period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) @@ -1767,7 +1769,7 @@ async def test_compile_hourly_sum_statistics_total_increasing_small_dip( factor, ) -> None: """Test small dips in sensor readings do not trigger a reset.""" - period0 = dt_util.utcnow() + period0 = get_start_time(dt_util.utcnow()) period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) @@ -1869,7 +1871,7 @@ async def test_compile_hourly_energy_statistics_unsupported( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test compiling hourly statistics.""" - period0 = dt_util.utcnow() + period0 = get_start_time(dt_util.utcnow()) period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) @@ -1973,7 +1975,7 @@ async def test_compile_hourly_energy_statistics_multiple( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test compiling multiple hourly statistics.""" - period0 = dt_util.utcnow() + period0 = get_start_time(dt_util.utcnow()) period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) @@ -2187,7 +2189,7 @@ async def test_compile_hourly_statistics_unchanged( value, ) -> None: """Test compiling hourly statistics, with no changes during the hour.""" - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -2230,7 +2232,7 @@ async def test_compile_hourly_statistics_partially_unavailable( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test compiling hourly statistics, with the sensor being partially unavailable.""" - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -2299,7 +2301,7 @@ async def test_compile_hourly_statistics_unavailable( sensor.test1 is unavailable and should not have statistics generated sensor.test2 should have statistics generated """ - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -2346,7 +2348,7 @@ async def test_compile_hourly_statistics_fails( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test compiling hourly statistics throws.""" - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -2523,7 +2525,7 @@ async def test_compile_hourly_statistics_changing_units_1( This tests the case where the recorder cannot convert between the units. """ - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -2652,7 +2654,7 @@ async def test_compile_hourly_statistics_changing_units_2( This tests the behaviour when the sensor units are note supported by any unit converter. """ - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) - timedelta(seconds=30 * 5) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -2731,7 +2733,7 @@ async def test_compile_hourly_statistics_changing_units_3( This tests the behaviour when the sensor units are note supported by any unit converter. """ - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -2852,7 +2854,7 @@ async def test_compile_hourly_statistics_convert_units_1( This tests the case where the recorder can convert between the units. """ - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -3011,7 +3013,7 @@ async def test_compile_hourly_statistics_equivalent_units_1( max, ) -> None: """Test compiling hourly statistics where units change from one hour to the next.""" - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -3136,7 +3138,7 @@ async def test_compile_hourly_statistics_equivalent_units_2( max, ) -> None: """Test compiling hourly statistics where units change during an hour.""" - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -3160,7 +3162,7 @@ async def test_compile_hourly_statistics_equivalent_units_2( ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - do_adhoc_statistics(hass, start=zero + timedelta(seconds=30 * 5)) + do_adhoc_statistics(hass, start=zero + timedelta(seconds=30 * 10)) await async_wait_recording_done(hass) assert "The unit of sensor.test1 is changing" not in caplog.text assert "and matches the unit of already compiled statistics" not in caplog.text @@ -3182,9 +3184,9 @@ async def test_compile_hourly_statistics_equivalent_units_2( "sensor.test1": [ { "start": process_timestamp( - zero + timedelta(seconds=30 * 5) + zero + timedelta(seconds=30 * 10) ).timestamp(), - "end": process_timestamp(zero + timedelta(seconds=30 * 15)).timestamp(), + "end": process_timestamp(zero + timedelta(seconds=30 * 20)).timestamp(), "mean": pytest.approx(mean), "min": pytest.approx(min), "max": pytest.approx(max), @@ -3229,7 +3231,7 @@ async def test_compile_hourly_statistics_changing_device_class_1( Device class is ignored, meaning changing device class should not influence the statistics. """ - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -3440,7 +3442,7 @@ async def test_compile_hourly_statistics_changing_device_class_2( Device class is ignored, meaning changing device class should not influence the statistics. """ - zero = dt_util.utcnow() + zero = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -3578,7 +3580,7 @@ async def test_compile_hourly_statistics_changing_state_class( max, ) -> None: """Test compiling hourly statistics where state class changes.""" - period0 = dt_util.utcnow() + period0 = get_start_time(dt_util.utcnow()) period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period0 + timedelta(minutes=10) await async_setup_component(hass, "sensor", {}) @@ -4148,7 +4150,7 @@ async def async_record_states( one = zero + timedelta(seconds=1 * 5) two = one + timedelta(seconds=10 * 5) three = two + timedelta(seconds=40 * 5) - four = three + timedelta(seconds=10 * 5) + four = three + timedelta(seconds=9 * 5) states = {entity_id: []} freezer.move_to(one) @@ -4210,7 +4212,7 @@ async def test_validate_unit_change_convertible( The test also asserts that the sensor's device class is ignored. """ - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) hass.config.units = units await async_setup_component(hass, "sensor", {}) @@ -4222,14 +4224,20 @@ async def test_validate_unit_change_convertible( # No statistics, unit in state matching device class - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, "unit_of_measurement": unit} + "sensor.test", + 10, + attributes={**attributes, "unit_of_measurement": unit}, + timestamp=now.timestamp(), ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) # No statistics, unit in state not matching device class - empty response hass.states.async_set( - "sensor.test", 11, attributes={**attributes, "unit_of_measurement": "dogs"} + "sensor.test", + 11, + attributes={**attributes, "unit_of_measurement": "dogs"}, + timestamp=now.timestamp(), ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -4238,7 +4246,10 @@ async def test_validate_unit_change_convertible( await async_recorder_block_till_done(hass) do_adhoc_statistics(hass, start=now) hass.states.async_set( - "sensor.test", 12, attributes={**attributes, "unit_of_measurement": "dogs"} + "sensor.test", + 12, + attributes={**attributes, "unit_of_measurement": "dogs"}, + timestamp=now.timestamp(), ) await async_recorder_block_till_done(hass) expected = { @@ -4258,7 +4269,10 @@ async def test_validate_unit_change_convertible( # Valid state - empty response hass.states.async_set( - "sensor.test", 13, attributes={**attributes, "unit_of_measurement": unit} + "sensor.test", + 13, + attributes={**attributes, "unit_of_measurement": unit}, + timestamp=now.timestamp(), ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -4270,7 +4284,10 @@ async def test_validate_unit_change_convertible( # Valid state in compatible unit - empty response hass.states.async_set( - "sensor.test", 13, attributes={**attributes, "unit_of_measurement": unit2} + "sensor.test", + 13, + attributes={**attributes, "unit_of_measurement": unit2}, + timestamp=now.timestamp(), ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -4309,7 +4326,7 @@ async def test_validate_statistics_unit_ignore_device_class( The test asserts that the sensor's device class is ignored. """ - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) hass.config.units = units await async_setup_component(hass, "sensor", {}) @@ -4321,7 +4338,9 @@ async def test_validate_statistics_unit_ignore_device_class( # No statistics, no device class - empty response initial_attributes = {"state_class": "measurement", "unit_of_measurement": "dogs"} - hass.states.async_set("sensor.test", 10, attributes=initial_attributes) + hass.states.async_set( + "sensor.test", 10, attributes=initial_attributes, timestamp=now.timestamp() + ) await hass.async_block_till_done() await assert_validation_result(client, {}) @@ -4329,7 +4348,10 @@ async def test_validate_statistics_unit_ignore_device_class( do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) hass.states.async_set( - "sensor.test", 12, attributes={**attributes, "unit_of_measurement": "dogs"} + "sensor.test", + 12, + attributes={**attributes, "unit_of_measurement": "dogs"}, + timestamp=now.timestamp(), ) await hass.async_block_till_done() await assert_validation_result(client, {}) @@ -4389,7 +4411,7 @@ async def test_validate_statistics_unit_change_no_device_class( attributes = dict(attributes) attributes.pop("device_class") - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) hass.config.units = units await async_setup_component(hass, "sensor", {}) @@ -4401,14 +4423,20 @@ async def test_validate_statistics_unit_change_no_device_class( # No statistics, sensor state set - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, "unit_of_measurement": unit} + "sensor.test", + 10, + attributes={**attributes, "unit_of_measurement": unit}, + timestamp=now.timestamp(), ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) # No statistics, sensor state set to an incompatible unit - empty response hass.states.async_set( - "sensor.test", 11, attributes={**attributes, "unit_of_measurement": "dogs"} + "sensor.test", + 11, + attributes={**attributes, "unit_of_measurement": "dogs"}, + timestamp=now.timestamp(), ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -4417,7 +4445,10 @@ async def test_validate_statistics_unit_change_no_device_class( await async_recorder_block_till_done(hass) do_adhoc_statistics(hass, start=now) hass.states.async_set( - "sensor.test", 12, attributes={**attributes, "unit_of_measurement": "dogs"} + "sensor.test", + 12, + attributes={**attributes, "unit_of_measurement": "dogs"}, + timestamp=now.timestamp(), ) await async_recorder_block_till_done(hass) expected = { @@ -4437,7 +4468,10 @@ async def test_validate_statistics_unit_change_no_device_class( # Valid state - empty response hass.states.async_set( - "sensor.test", 13, attributes={**attributes, "unit_of_measurement": unit} + "sensor.test", + 13, + attributes={**attributes, "unit_of_measurement": unit}, + timestamp=now.timestamp(), ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -4449,7 +4483,10 @@ async def test_validate_statistics_unit_change_no_device_class( # Valid state in compatible unit - empty response hass.states.async_set( - "sensor.test", 13, attributes={**attributes, "unit_of_measurement": unit2} + "sensor.test", + 13, + attributes={**attributes, "unit_of_measurement": unit2}, + timestamp=now.timestamp(), ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -4486,7 +4523,7 @@ async def test_validate_statistics_unsupported_state_class( unit, ) -> None: """Test validate_statistics.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) hass.config.units = units await async_setup_component(hass, "sensor", {}) @@ -4497,7 +4534,9 @@ async def test_validate_statistics_unsupported_state_class( await assert_validation_result(client, {}) # No statistics, valid state - empty response - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", 10, attributes=attributes, timestamp=now.timestamp() + ) await hass.async_block_till_done() await assert_validation_result(client, {}) @@ -4509,7 +4548,9 @@ async def test_validate_statistics_unsupported_state_class( # State update with invalid state class, expect error _attributes = dict(attributes) _attributes.pop("state_class") - hass.states.async_set("sensor.test", 12, attributes=_attributes) + hass.states.async_set( + "sensor.test", 12, attributes=_attributes, timestamp=now.timestamp() + ) await hass.async_block_till_done() expected = { "sensor.test": [ @@ -4539,7 +4580,7 @@ async def test_validate_statistics_sensor_no_longer_recorded( unit, ) -> None: """Test validate_statistics.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) hass.config.units = units await async_setup_component(hass, "sensor", {}) @@ -4550,7 +4591,9 @@ async def test_validate_statistics_sensor_no_longer_recorded( await assert_validation_result(client, {}) # No statistics, valid state - empty response - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", 10, attributes=attributes, timestamp=now.timestamp() + ) await hass.async_block_till_done() await assert_validation_result(client, {}) @@ -4591,7 +4634,7 @@ async def test_validate_statistics_sensor_not_recorded( unit, ) -> None: """Test validate_statistics.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) hass.config.units = units await async_setup_component(hass, "sensor", {}) @@ -4616,7 +4659,9 @@ async def test_validate_statistics_sensor_not_recorded( "entity_filter", return_value=False, ): - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", 10, attributes=attributes, timestamp=now.timestamp() + ) await hass.async_block_till_done() await assert_validation_result(client, expected) @@ -4640,7 +4685,7 @@ async def test_validate_statistics_sensor_removed( unit, ) -> None: """Test validate_statistics.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) hass.config.units = units await async_setup_component(hass, "sensor", {}) @@ -4651,7 +4696,9 @@ async def test_validate_statistics_sensor_removed( await assert_validation_result(client, {}) # No statistics, valid state - empty response - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", 10, attributes=attributes, timestamp=now.timestamp() + ) await hass.async_block_till_done() await assert_validation_result(client, {}) @@ -4688,7 +4735,7 @@ async def test_validate_statistics_unit_change_no_conversion( unit2, ) -> None: """Test validate_statistics.""" - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) @@ -4699,13 +4746,19 @@ async def test_validate_statistics_unit_change_no_conversion( # No statistics, original unit - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, "unit_of_measurement": unit1} + "sensor.test", + 10, + attributes={**attributes, "unit_of_measurement": unit1}, + timestamp=now.timestamp(), ) await assert_validation_result(client, {}) # No statistics, changed unit - empty response hass.states.async_set( - "sensor.test", 11, attributes={**attributes, "unit_of_measurement": unit2} + "sensor.test", + 11, + attributes={**attributes, "unit_of_measurement": unit2}, + timestamp=now.timestamp(), ) await assert_validation_result(client, {}) @@ -4717,7 +4770,10 @@ async def test_validate_statistics_unit_change_no_conversion( # No statistics, original unit - empty response hass.states.async_set( - "sensor.test", 12, attributes={**attributes, "unit_of_measurement": unit1} + "sensor.test", + 12, + attributes={**attributes, "unit_of_measurement": unit1}, + timestamp=now.timestamp(), ) await assert_validation_result(client, {}) @@ -4732,7 +4788,10 @@ async def test_validate_statistics_unit_change_no_conversion( # Change unit - expect error hass.states.async_set( - "sensor.test", 13, attributes={**attributes, "unit_of_measurement": unit2} + "sensor.test", + 13, + attributes={**attributes, "unit_of_measurement": unit2}, + timestamp=now.timestamp(), ) await async_recorder_block_till_done(hass) expected = { @@ -4752,7 +4811,10 @@ async def test_validate_statistics_unit_change_no_conversion( # Original unit - empty response hass.states.async_set( - "sensor.test", 14, attributes={**attributes, "unit_of_measurement": unit1} + "sensor.test", + 14, + attributes={**attributes, "unit_of_measurement": unit1}, + timestamp=now.timestamp(), ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -4796,7 +4858,7 @@ async def test_validate_statistics_unit_change_equivalent_units( This tests no validation issue is created when a sensor's unit changes to an equivalent unit. """ - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) @@ -4807,7 +4869,10 @@ async def test_validate_statistics_unit_change_equivalent_units( # No statistics, original unit - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, "unit_of_measurement": unit1} + "sensor.test", + 10, + attributes={**attributes, "unit_of_measurement": unit1}, + timestamp=now.timestamp(), ) await assert_validation_result(client, {}) @@ -4821,7 +4886,10 @@ async def test_validate_statistics_unit_change_equivalent_units( # Units changed to an equivalent unit - empty response hass.states.async_set( - "sensor.test", 12, attributes={**attributes, "unit_of_measurement": unit2} + "sensor.test", + 12, + attributes={**attributes, "unit_of_measurement": unit2}, + timestamp=now.timestamp() + 1, ) await assert_validation_result(client, {}) @@ -4854,7 +4922,7 @@ async def test_validate_statistics_unit_change_equivalent_units_2( This tests a validation issue is created when a sensor's unit changes to an equivalent unit which is not known to the unit converters. """ - now = dt_util.utcnow() + now = get_start_time(dt_util.utcnow()) await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) @@ -4865,7 +4933,10 @@ async def test_validate_statistics_unit_change_equivalent_units_2( # No statistics, original unit - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, "unit_of_measurement": unit1} + "sensor.test", + 10, + attributes={**attributes, "unit_of_measurement": unit1}, + timestamp=now.timestamp(), ) await assert_validation_result(client, {}) @@ -4879,7 +4950,10 @@ async def test_validate_statistics_unit_change_equivalent_units_2( # Units changed to an equivalent unit which is not known by the unit converters hass.states.async_set( - "sensor.test", 12, attributes={**attributes, "unit_of_measurement": unit2} + "sensor.test", + 12, + attributes={**attributes, "unit_of_measurement": unit2}, + timestamp=now.timestamp(), ) expected = { "sensor.test": [ @@ -5045,7 +5119,7 @@ async def async_record_states_partially_unavailable(hass, zero, entity_id, attri one = zero + timedelta(seconds=1 * 5) two = one + timedelta(seconds=15 * 5) three = two + timedelta(seconds=30 * 5) - four = three + timedelta(seconds=15 * 5) + four = three + timedelta(seconds=14 * 5) states = {entity_id: []} with freeze_time(one) as freezer: From 81c8ba87ab41c800aa05c2b76a39bc565b2e3c33 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 25 Jul 2024 18:16:25 +0200 Subject: [PATCH 1572/2411] Use snapshot in UniFi button tests (#122602) --- .../unifi/snapshots/test_button.ambr | 236 ++++++++++++++++++ tests/components/unifi/test_button.py | 79 +++--- 2 files changed, 266 insertions(+), 49 deletions(-) create mode 100644 tests/components/unifi/snapshots/test_button.ambr diff --git a/tests/components/unifi/snapshots/test_button.ambr b/tests/components/unifi/snapshots/test_button.ambr new file mode 100644 index 00000000000..51a37620268 --- /dev/null +++ b/tests/components/unifi/snapshots/test_button.ambr @@ -0,0 +1,236 @@ +# serializer version: 1 +# name: test_entity_and_device_data[site_payload0-device_payload0][button.switch_port_1_power_cycle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.switch_port_1_power_cycle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Port 1 Power Cycle', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power_cycle-00:00:00:00:01:01_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][button.switch_port_1_power_cycle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'switch Port 1 Power Cycle', + }), + 'context': , + 'entity_id': 'button.switch_port_1_power_cycle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][button.switch_restart-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.switch_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'device_restart-00:00:00:00:01:01', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][button.switch_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'switch Restart', + }), + 'context': , + 'entity_id': 'button.switch_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-device_payload0][button.ssid_1_regenerate_password-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.ssid_1_regenerate_password', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Regenerate Password', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'regenerate_password-012345678910111213141516', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-device_payload0][button.ssid_1_regenerate_password-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'update', + 'friendly_name': 'SSID 1 Regenerate Password', + }), + 'context': , + 'entity_id': 'button.ssid_1_regenerate_password', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-device_payload0][button.switch_port_1_power_cycle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.switch_port_1_power_cycle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Port 1 Power Cycle', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power_cycle-00:00:00:00:01:01_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-device_payload0][button.switch_port_1_power_cycle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'switch Port 1 Power Cycle', + }), + 'context': , + 'entity_id': 'button.switch_port_1_power_cycle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-device_payload0][button.switch_restart-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.switch_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'device_restart-00:00:00:00:01:01', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-device_payload0][button.switch_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'switch Restart', + }), + 'context': , + 'entity_id': 'button.switch_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/unifi/test_button.py b/tests/components/unifi/test_button.py index b7bf19aedc2..9af96b64a50 100644 --- a/tests/components/unifi/test_button.py +++ b/tests/components/unifi/test_button.py @@ -7,23 +7,23 @@ from unittest.mock import patch from aiounifi.models.message import MessageKey import pytest +from syrupy import SnapshotAssertion -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonDeviceClass +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.unifi.const import CONF_SITE_ID from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, ConfigEntry from homeassistant.const import ( - ATTR_DEVICE_CLASS, CONF_HOST, CONTENT_TYPE_JSON, STATE_UNAVAILABLE, - EntityCategory, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryDisabler import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker RANDOM_TOKEN = "random_token" @@ -121,33 +121,44 @@ WLAN_REGENERATE_PASSWORD = [ ] -async def _test_button_entity( +@pytest.mark.parametrize("device_payload", [DEVICE_RESTART + DEVICE_POWER_CYCLE_POE]) +@pytest.mark.parametrize("wlan_payload", [WLAN_REGENERATE_PASSWORD]) +@pytest.mark.parametrize( + "site_payload", + [ + [{"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}], + [{"desc": "Site name", "name": "site_id", "role": "not admin", "_id": "1"}], + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_entity_and_device_data( hass: HomeAssistant, entity_registry: er.EntityRegistry, + config_entry_factory, + site_payload: dict[str, Any], + snapshot: SnapshotAssertion, +) -> None: + """Validate entity and device data with and without admin rights.""" + with patch("homeassistant.components.unifi.PLATFORMS", [Platform.BUTTON]): + config_entry = await config_entry_factory() + if site_payload[0]["role"] == "admin": + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + else: + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 0 + + +async def _test_button_entity( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_websocket_state, config_entry: ConfigEntry, - entity_count: int, entity_id: str, - unique_id: str, - device_class: ButtonDeviceClass, request_method: str, request_path: str, request_data: dict[str, Any], call: dict[str, str], ) -> None: """Test button entity.""" - assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == entity_count - - ent_reg_entry = entity_registry.async_get(entity_id) - assert ent_reg_entry.unique_id == unique_id - assert ent_reg_entry.entity_category is EntityCategory.CONFIG - - # Validate state object - button = hass.states.get(entity_id) - assert button is not None - assert button.attributes.get(ATTR_DEVICE_CLASS) == device_class - # Send and validate device command aioclient_mock.clear_requests() aioclient_mock.request( @@ -177,10 +188,7 @@ async def _test_button_entity( @pytest.mark.parametrize( ( "device_payload", - "entity_count", "entity_id", - "unique_id", - "device_class", "request_method", "request_path", "call", @@ -188,10 +196,7 @@ async def _test_button_entity( [ ( DEVICE_RESTART, - 1, "button.switch_restart", - "device_restart-00:00:00:00:01:01", - ButtonDeviceClass.RESTART, "post", "/cmd/devmgr", { @@ -202,10 +207,7 @@ async def _test_button_entity( ), ( DEVICE_POWER_CYCLE_POE, - 2, "button.switch_port_1_power_cycle", - "power_cycle-00:00:00:00:01:01_1", - ButtonDeviceClass.RESTART, "post", "/cmd/devmgr", { @@ -218,14 +220,10 @@ async def _test_button_entity( ) async def test_device_button_entities( hass: HomeAssistant, - entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, config_entry_setup: ConfigEntry, mock_websocket_state, - entity_count: int, entity_id: str, - unique_id: str, - device_class: ButtonDeviceClass, request_method: str, request_path: str, call: dict[str, str], @@ -233,14 +231,10 @@ async def test_device_button_entities( """Test button entities based on device sources.""" await _test_button_entity( hass, - entity_registry, aioclient_mock, mock_websocket_state, config_entry_setup, - entity_count, entity_id, - unique_id, - device_class, request_method, request_path, {}, @@ -251,10 +245,7 @@ async def test_device_button_entities( @pytest.mark.parametrize( ( "wlan_payload", - "entity_count", "entity_id", - "unique_id", - "device_class", "request_method", "request_path", "request_data", @@ -263,10 +254,7 @@ async def test_device_button_entities( [ ( WLAN_REGENERATE_PASSWORD, - 1, "button.ssid_1_regenerate_password", - "regenerate_password-012345678910111213141516", - ButtonDeviceClass.UPDATE, "put", f"/rest/wlanconf/{WLAN_REGENERATE_PASSWORD[0]["_id"]}", { @@ -283,10 +271,7 @@ async def test_wlan_button_entities( aioclient_mock: AiohttpClientMocker, config_entry_setup: ConfigEntry, mock_websocket_state, - entity_count: int, entity_id: str, - unique_id: str, - device_class: ButtonDeviceClass, request_method: str, request_path: str, request_data: dict[str, Any], @@ -308,14 +293,10 @@ async def test_wlan_button_entities( await _test_button_entity( hass, - entity_registry, aioclient_mock, mock_websocket_state, config_entry_setup, - entity_count, entity_id, - unique_id, - device_class, request_method, request_path, request_data, From e015c0a6ae41b6c89c8348496b98a8ad4c13c42b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 25 Jul 2024 18:16:58 +0200 Subject: [PATCH 1573/2411] Use snapshot in UniFi device tracker tests (#122603) --- .../unifi/snapshots/test_device_tracker.ambr | 149 ++++++++++++++++++ tests/components/unifi/test_device_tracker.py | 25 ++- 2 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 tests/components/unifi/snapshots/test_device_tracker.ambr diff --git a/tests/components/unifi/snapshots/test_device_tracker.ambr b/tests/components/unifi/snapshots/test_device_tracker.ambr new file mode 100644 index 00000000000..3debd512050 --- /dev/null +++ b/tests/components/unifi/snapshots/test_device_tracker.ambr @@ -0,0 +1,149 @@ +# serializer version: 1 +# name: test_entity_and_device_data[site_payload0-device_payload0-client_payload0][device_tracker.switch_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'device_tracker', + 'entity_category': , + 'entity_id': 'device_tracker.switch_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch 1', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:00:00:00:01:01', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0-client_payload0][device_tracker.switch_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Switch 1', + 'ip': '10.0.1.1', + 'mac': '00:00:00:00:01:01', + 'source_type': , + }), + 'context': , + 'entity_id': 'device_tracker.switch_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'home', + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0-client_payload0][device_tracker.wd_client_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'device_tracker', + 'entity_category': , + 'entity_id': 'device_tracker.wd_client_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'wd_client_1', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'site_id-00:00:00:00:00:02', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0-client_payload0][device_tracker.wd_client_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'wd_client_1', + 'host_name': 'wd_client_1', + 'mac': '00:00:00:00:00:02', + 'source_type': , + }), + 'context': , + 'entity_id': 'device_tracker.wd_client_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'not_home', + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0-client_payload0][device_tracker.ws_client_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'device_tracker', + 'entity_category': , + 'entity_id': 'device_tracker.ws_client_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'ws_client_1', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'site_id-00:00:00:00:00:01', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0-client_payload0][device_tracker.ws_client_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ws_client_1', + 'host_name': 'ws_client_1', + 'ip': '10.0.0.1', + 'mac': '00:00:00:00:00:01', + 'source_type': , + }), + 'context': , + 'entity_id': 'device_tracker.ws_client_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'not_home', + }) +# --- diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 615afb61bb6..f2480a4f050 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -4,11 +4,13 @@ from collections.abc import Callable from datetime import timedelta from types import MappingProxyType from typing import Any +from unittest.mock import patch from aiounifi.models.event import EventKey from aiounifi.models.message import MessageKey from freezegun.api import FrozenDateTimeFactory, freeze_time import pytest +from syrupy import SnapshotAssertion from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.unifi.const import ( @@ -23,12 +25,12 @@ from homeassistant.components.unifi.const import ( DOMAIN as UNIFI_DOMAIN, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE +from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, snapshot_platform WIRED_CLIENT_1 = { "hostname": "wd_client_1", @@ -84,6 +86,25 @@ SWITCH_1 = { } +@pytest.mark.parametrize("client_payload", [[WIRED_CLIENT_1, WIRELESS_CLIENT_1]]) +@pytest.mark.parametrize("device_payload", [[SWITCH_1]]) +@pytest.mark.parametrize( + "site_payload", + [[{"desc": "Site name", "name": "site_id", "role": "not admin", "_id": "1"}]], +) +@pytest.mark.usefixtures("mock_device_registry") +async def test_entity_and_device_data( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory, + snapshot: SnapshotAssertion, +) -> None: + """Validate entity and device data with and without admin rights.""" + with patch("homeassistant.components.unifi.PLATFORMS", [Platform.DEVICE_TRACKER]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + @pytest.mark.parametrize( "client_payload", [[WIRELESS_CLIENT_1, WIRED_BUG_CLIENT, UNSEEN_CLIENT]] ) From 81983d66f4cf34256b01183b5f930b0714560fdc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Jul 2024 19:52:13 +0200 Subject: [PATCH 1574/2411] Avoid nesting sessions in recorder auto repairs tests (#122596) --- .../recorder/auto_repairs/test_schema.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/components/recorder/auto_repairs/test_schema.py b/tests/components/recorder/auto_repairs/test_schema.py index 3d623b6bf8a..857c0f6572f 100644 --- a/tests/components/recorder/auto_repairs/test_schema.py +++ b/tests/components/recorder/auto_repairs/test_schema.py @@ -176,14 +176,14 @@ async def test_validate_db_schema_fix_utf8_issue_with_broken_schema_unrepairable "LOCK=EXCLUSIVE;" ) ) - _modify_columns( - session_maker, - recorder_mock.engine, - "states", - [ - "entity_id VARCHAR(255) NOT NULL", - ], - ) + _modify_columns( + session_maker, + recorder_mock.engine, + "states", + [ + "entity_id VARCHAR(255) NOT NULL", + ], + ) await recorder_mock.async_add_executor_job(_break_states_schema) schema_errors = await recorder_mock.async_add_executor_job( From eb3686af06c2708c526f0838b9bfaa88e2b21698 Mon Sep 17 00:00:00 2001 From: huettner94 Date: Thu, 25 Jul 2024 20:22:18 +0200 Subject: [PATCH 1575/2411] Add shelly overcurrent sensor for switches (#122494) shelly: add overcurrent sensor for switches just like overvoltage shelly switches can react to overcurrent and diable the switch. Unfortunately this is is not mentioned anywhere in the documentation. It can be triggered by a device using more amps than set in "Output protections" under the name "Overcurrent in amperes". --- homeassistant/components/shelly/binary_sensor.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index bc2ba3326a7..c2127828b07 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -204,6 +204,15 @@ RPC_SENSORS: Final = { entity_category=EntityCategory.DIAGNOSTIC, supported=lambda status: status.get("apower") is not None, ), + "overcurrent": RpcBinarySensorDescription( + key="switch", + sub_key="errors", + name="Overcurrent", + device_class=BinarySensorDeviceClass.PROBLEM, + value=lambda status, _: False if status is None else "overcurrent" in status, + entity_category=EntityCategory.DIAGNOSTIC, + supported=lambda status: status.get("apower") is not None, + ), "smoke": RpcBinarySensorDescription( key="smoke", sub_key="alarm", From 62a3902de77c2881ea8ce7e57faf1e96a73ee33d Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 25 Jul 2024 21:18:28 +0200 Subject: [PATCH 1576/2411] Set mode for Ecovacs clean count entity (#122611) --- homeassistant/components/ecovacs/number.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecovacs/number.py b/homeassistant/components/ecovacs/number.py index bfe840dad42..3b24091ca34 100644 --- a/homeassistant/components/ecovacs/number.py +++ b/homeassistant/components/ecovacs/number.py @@ -9,7 +9,11 @@ from typing import Generic from deebot_client.capabilities import CapabilitySet from deebot_client.events import CleanCountEvent, VolumeEvent -from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, +) from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -59,6 +63,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsNumberEntityDescription, ...] = ( native_min_value=1, native_max_value=4, native_step=1.0, + mode=NumberMode.BOX, ), ) From 32a0463f47fdba5ab89249425bc0e8b27c5e53a2 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 25 Jul 2024 21:18:42 +0200 Subject: [PATCH 1577/2411] Update Ecovacs translations (#122610) * Update Ecovacs translations * Update tests --- homeassistant/components/ecovacs/strings.json | 4 ++-- tests/components/ecovacs/snapshots/test_button.ambr | 12 ++++++------ tests/components/ecovacs/snapshots/test_sensor.ambr | 12 ++++++------ tests/components/ecovacs/test_button.py | 4 ++-- tests/components/ecovacs/test_sensor.py | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index 68218e63d4e..d501c333a03 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -65,7 +65,7 @@ "name": "Reset unit care lifespan" }, "reset_lifespan_side_brush": { - "name": "Reset side brushes lifespan" + "name": "Reset side brush lifespan" } }, "event": { @@ -117,7 +117,7 @@ "name": "Lens brush lifespan" }, "lifespan_side_brush": { - "name": "Side brushes lifespan" + "name": "Side brush lifespan" }, "lifespan_unit_care": { "name": "Unit care lifespan" diff --git a/tests/components/ecovacs/snapshots/test_button.ambr b/tests/components/ecovacs/snapshots/test_button.ambr index d250a60a35f..efae8896962 100644 --- a/tests/components/ecovacs/snapshots/test_button.ambr +++ b/tests/components/ecovacs/snapshots/test_button.ambr @@ -229,7 +229,7 @@ 'state': '2024-01-01T00:00:00+00:00', }) # --- -# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brushes_lifespan:entity-registry] +# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brush_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -241,7 +241,7 @@ 'disabled_by': None, 'domain': 'button', 'entity_category': , - 'entity_id': 'button.ozmo_950_reset_side_brushes_lifespan', + 'entity_id': 'button.ozmo_950_reset_side_brush_lifespan', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -253,7 +253,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Reset side brushes lifespan', + 'original_name': 'Reset side brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': 0, @@ -262,13 +262,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brushes_lifespan:state] +# name: test_buttons[yna5x1][button.ozmo_950_reset_side_brush_lifespan:state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ozmo 950 Reset side brushes lifespan', + 'friendly_name': 'Ozmo 950 Reset side brush lifespan', }), 'context': , - 'entity_id': 'button.ozmo_950_reset_side_brushes_lifespan', + 'entity_id': 'button.ozmo_950_reset_side_brush_lifespan', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index e2cee3d410f..07ebd400870 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -910,7 +910,7 @@ 'state': '80', }) # --- -# name: test_sensors[yna5x1][sensor.ozmo_950_side_brushes_lifespan:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_side_brush_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -922,7 +922,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.ozmo_950_side_brushes_lifespan', + 'entity_id': 'sensor.ozmo_950_side_brush_lifespan', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -934,7 +934,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Side brushes lifespan', + 'original_name': 'Side brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': 0, @@ -943,14 +943,14 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[yna5x1][sensor.ozmo_950_side_brushes_lifespan:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_side_brush_lifespan:state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ozmo 950 Side brushes lifespan', + 'friendly_name': 'Ozmo 950 Side brush lifespan', 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.ozmo_950_side_brushes_lifespan', + 'entity_id': 'sensor.ozmo_950_side_brush_lifespan', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/ecovacs/test_button.py b/tests/components/ecovacs/test_button.py index 08d53f3e93d..4b3068f6cda 100644 --- a/tests/components/ecovacs/test_button.py +++ b/tests/components/ecovacs/test_button.py @@ -42,7 +42,7 @@ def platforms() -> Platform | list[Platform]: ResetLifeSpan(LifeSpan.FILTER), ), ( - "button.ozmo_950_reset_side_brushes_lifespan", + "button.ozmo_950_reset_side_brush_lifespan", ResetLifeSpan(LifeSpan.SIDE_BRUSH), ), ], @@ -107,7 +107,7 @@ async def test_buttons( [ "button.ozmo_950_reset_main_brush_lifespan", "button.ozmo_950_reset_filter_lifespan", - "button.ozmo_950_reset_side_brushes_lifespan", + "button.ozmo_950_reset_side_brush_lifespan", ], ), ( diff --git a/tests/components/ecovacs/test_sensor.py b/tests/components/ecovacs/test_sensor.py index 005d10bffbd..19b4c8ce09b 100644 --- a/tests/components/ecovacs/test_sensor.py +++ b/tests/components/ecovacs/test_sensor.py @@ -64,7 +64,7 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus): "sensor.ozmo_950_wi_fi_ssid", "sensor.ozmo_950_main_brush_lifespan", "sensor.ozmo_950_filter_lifespan", - "sensor.ozmo_950_side_brushes_lifespan", + "sensor.ozmo_950_side_brush_lifespan", "sensor.ozmo_950_error", ], ), From 5dbd7684ce7433f063445254d0f3485c4b6045af Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Jul 2024 21:18:55 +0200 Subject: [PATCH 1578/2411] Fail tests if recorder creates nested sessions (#122579) * Fail tests if recorder creates nested sessions * Adjust import order * Move get_instance --- homeassistant/components/recorder/__init__.py | 2 +- homeassistant/components/recorder/const.py | 4 -- homeassistant/components/recorder/util.py | 48 ++-------------- homeassistant/helpers/recorder.py | 56 ++++++++++++++++++- tests/conftest.py | 51 +++++++++++++++++ tests/patch_recorder.py | 27 +++++++++ 6 files changed, 139 insertions(+), 49 deletions(-) create mode 100644 tests/patch_recorder.py diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index b9ba90caf3f..41fa8db5814 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -23,6 +23,7 @@ from homeassistant.helpers.entityfilter import ( from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) +from homeassistant.helpers.recorder import DATA_INSTANCE from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.event_type import EventType @@ -30,7 +31,6 @@ from homeassistant.util.event_type import EventType from . import entity_registry, websocket_api from .const import ( # noqa: F401 CONF_DB_INTEGRITY_CHECK, - DATA_INSTANCE, DOMAIN, INTEGRATION_PLATFORM_COMPILE_STATISTICS, INTEGRATION_PLATFORMS_RUN_IN_RECORDER_THREAD, diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 31870a5db2d..c7dba18cad9 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -13,15 +13,11 @@ from homeassistant.const import ( EVENT_RECORDER_HOURLY_STATISTICS_GENERATED, # noqa: F401 ) from homeassistant.helpers.json import JSON_DUMP # noqa: F401 -from homeassistant.util.hass_dict import HassKey if TYPE_CHECKING: from .core import Recorder # noqa: F401 -DATA_INSTANCE: HassKey[Recorder] = HassKey("recorder_instance") - - SQLITE_URL_PREFIX = "sqlite://" MARIADB_URL_PREFIX = "mariadb://" MARIADB_PYMYSQL_URL_PREFIX = "mariadb+pymysql://" diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 89621821ff8..1ef85b28f8d 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -29,10 +29,14 @@ import voluptuous as vol from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, issue_registry as ir +from homeassistant.helpers.recorder import ( # noqa: F401 + DATA_INSTANCE, + get_instance, + session_scope, +) import homeassistant.util.dt as dt_util from .const import ( - DATA_INSTANCE, DEFAULT_MAX_BIND_VARS, DOMAIN, SQLITE_MAX_BIND_VARS, @@ -111,42 +115,6 @@ SUNDAY_WEEKDAY = 6 DAYS_IN_WEEK = 7 -@contextmanager -def session_scope( - *, - hass: HomeAssistant | None = None, - session: Session | None = None, - exception_filter: Callable[[Exception], bool] | None = None, - read_only: bool = False, -) -> Generator[Session]: - """Provide a transactional scope around a series of operations. - - read_only is used to indicate that the session is only used for reading - data and that no commit is required. It does not prevent the session - from writing and is not a security measure. - """ - if session is None and hass is not None: - session = get_instance(hass).get_session() - - if session is None: - raise RuntimeError("Session required") - - need_rollback = False - try: - yield session - if not read_only and session.get_transaction(): - need_rollback = True - session.commit() - except Exception as err: - _LOGGER.exception("Error executing query") - if need_rollback: - session.rollback() - if not exception_filter or not exception_filter(err): - raise - finally: - session.close() - - def execute( qry: Query, to_native: bool = False, validate_entity_ids: bool = True ) -> list[Row]: @@ -769,12 +737,6 @@ def is_second_sunday(date_time: datetime) -> bool: return bool(second_sunday(date_time.year, date_time.month).day == date_time.day) -@functools.lru_cache(maxsize=1) -def get_instance(hass: HomeAssistant) -> Recorder: - """Get the recorder instance.""" - return hass.data[DATA_INSTANCE] - - PERIOD_SCHEMA = vol.Schema( { vol.Exclusive("calendar", "period"): vol.Schema( diff --git a/homeassistant/helpers/recorder.py b/homeassistant/helpers/recorder.py index f6657efc6d7..59604944eeb 100644 --- a/homeassistant/helpers/recorder.py +++ b/homeassistant/helpers/recorder.py @@ -3,13 +3,25 @@ from __future__ import annotations import asyncio +from collections.abc import Callable, Generator +from contextlib import contextmanager from dataclasses import dataclass, field -from typing import Any +import functools +import logging +from typing import TYPE_CHECKING, Any from homeassistant.core import HomeAssistant, callback from homeassistant.util.hass_dict import HassKey +if TYPE_CHECKING: + from sqlalchemy.orm.session import Session + + from homeassistant.components.recorder import Recorder + +_LOGGER = logging.getLogger(__name__) + DOMAIN: HassKey[RecorderData] = HassKey("recorder") +DATA_INSTANCE: HassKey[Recorder] = HassKey("recorder_instance") @dataclass(slots=True) @@ -56,3 +68,45 @@ async def async_wait_recorder(hass: HomeAssistant) -> bool: if DOMAIN not in hass.data: return False return await hass.data[DOMAIN].db_connected + + +@functools.lru_cache(maxsize=1) +def get_instance(hass: HomeAssistant) -> Recorder: + """Get the recorder instance.""" + return hass.data[DATA_INSTANCE] + + +@contextmanager +def session_scope( + *, + hass: HomeAssistant | None = None, + session: Session | None = None, + exception_filter: Callable[[Exception], bool] | None = None, + read_only: bool = False, +) -> Generator[Session]: + """Provide a transactional scope around a series of operations. + + read_only is used to indicate that the session is only used for reading + data and that no commit is required. It does not prevent the session + from writing and is not a security measure. + """ + if session is None and hass is not None: + session = get_instance(hass).get_session() + + if session is None: + raise RuntimeError("Session required") + + need_rollback = False + try: + yield session + if not read_only and session.get_transaction(): + need_rollback = True + session.commit() + except Exception as err: + _LOGGER.exception("Error executing query") + if need_rollback: + session.rollback() + if not exception_filter or not exception_filter(err): + raise + finally: + session.close() diff --git a/tests/conftest.py b/tests/conftest.py index bc139255e66..de0dbc2e0d2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,6 +39,9 @@ from syrupy.assertion import SnapshotAssertion from homeassistant import block_async_io from homeassistant.exceptions import ServiceNotFound +# Setup patching of recorder functions before any other Home Assistant imports +from . import patch_recorder # noqa: F401, isort:skip + # Setup patching of dt_util time functions before any other Home Assistant imports from . import patch_time # noqa: F401, isort:skip @@ -1423,6 +1426,15 @@ async def _async_init_recorder_component( ) +class ThreadSession(threading.local): + """Keep track of session per thread.""" + + has_session = False + + +thread_session = ThreadSession() + + @pytest.fixture async def async_test_recorder( recorder_db_url: str, @@ -1444,6 +1456,39 @@ async def async_test_recorder( # pylint: disable-next=import-outside-toplevel from .components.recorder.common import async_recorder_block_till_done + # pylint: disable-next=import-outside-toplevel + from .patch_recorder import real_session_scope + + if TYPE_CHECKING: + # pylint: disable-next=import-outside-toplevel + from sqlalchemy.orm.session import Session + + @contextmanager + def debug_session_scope( + *, + hass: HomeAssistant | None = None, + session: Session | None = None, + exception_filter: Callable[[Exception], bool] | None = None, + read_only: bool = False, + ) -> Generator[Session]: + """Wrap session_scope to bark if we create nested sessions.""" + if thread_session.has_session: + raise RuntimeError( + f"Thread '{threading.current_thread().name}' already has an " + "active session" + ) + thread_session.has_session = True + try: + with real_session_scope( + hass=hass, + session=session, + exception_filter=exception_filter, + read_only=read_only, + ) as ses: + yield ses + finally: + thread_session.has_session = False + nightly = recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None stats = recorder.Recorder.async_periodic_statistics if enable_statistics else None schema_validate = ( @@ -1525,6 +1570,12 @@ async def async_test_recorder( side_effect=compile_missing, autospec=True, ), + patch.object( + patch_recorder, + "real_session_scope", + side_effect=debug_session_scope, + autospec=True, + ), ): @asynccontextmanager diff --git a/tests/patch_recorder.py b/tests/patch_recorder.py new file mode 100644 index 00000000000..4993e84fc30 --- /dev/null +++ b/tests/patch_recorder.py @@ -0,0 +1,27 @@ +"""Patch recorder related functions.""" + +from __future__ import annotations + +from contextlib import contextmanager +import sys + +# Patch recorder util session scope +from homeassistant.helpers import recorder as recorder_helper # noqa: E402 + +# Make sure homeassistant.components.recorder.util is not already imported +assert "homeassistant.components.recorder.util" not in sys.modules + +real_session_scope = recorder_helper.session_scope + + +@contextmanager +def _session_scope_wrapper(*args, **kwargs): + """Make session_scope patchable. + + This function will be imported by recorder modules. + """ + with real_session_scope(*args, **kwargs) as ses: + yield ses + + +recorder_helper.session_scope = _session_scope_wrapper From d77b5cbbbf0be01320b399d801aefeec880551c9 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 25 Jul 2024 21:23:14 +0200 Subject: [PATCH 1579/2411] Bump deebot-client to 8.2.0 (#122612) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 0dadcba7beb..8838eb4f50a 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==8.1.1"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ac2db12aae5..0481c9e2a84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -706,7 +706,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==8.1.1 +deebot-client==8.2.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2bcb11fa83..37677cb05e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -596,7 +596,7 @@ dbus-fast==2.22.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==8.1.1 +deebot-client==8.2.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From e5f2046b1935f38a8db3bb7087dcffb27a55bda8 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 25 Jul 2024 21:48:10 +0200 Subject: [PATCH 1580/2411] Update mypy-dev to 1.12.0a2 (#122613) --- homeassistant/components/cloud/prefs.py | 6 +++--- homeassistant/components/overkiz/climate.py | 5 +---- homeassistant/components/transmission/__init__.py | 5 ++--- requirements_test.txt | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 9b7f863c368..9f76c16a113 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Coroutine from typing import Any import uuid -from hass_nabucasa.voice import MAP_VOICE +from hass_nabucasa.voice import MAP_VOICE, Gender from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.auth.models import User @@ -91,8 +91,8 @@ class CloudPreferencesStore(Store): # The new second item is the voice name. default_tts_voice = old_data.get(PREF_TTS_DEFAULT_VOICE) if default_tts_voice and (voice_item_two := default_tts_voice[1]) in ( - "female", - "male", + Gender.FEMALE, + Gender.MALE, ): language: str = default_tts_voice[0] if voice := MAP_VOICE.get((language, voice_item_two)): diff --git a/homeassistant/components/overkiz/climate.py b/homeassistant/components/overkiz/climate.py index b569d05d2d7..1663834abee 100644 --- a/homeassistant/components/overkiz/climate.py +++ b/homeassistant/components/overkiz/climate.py @@ -2,8 +2,6 @@ from __future__ import annotations -from typing import cast - from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -15,7 +13,6 @@ from .climate_entities import ( WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY, WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY, WIDGET_TO_CLIMATE_ENTITY, - Controllable, ) from .const import DOMAIN @@ -39,7 +36,7 @@ async def async_setup_entry( # ie Atlantic APC entities_based_on_widget_and_controllable: list[Entity] = [ WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget][ - cast(Controllable, device.controllable_name) + device.controllable_name ](device.device_url, data.coordinator) for device in data.platforms[Platform.CLIMATE] if device.widget in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 37771430199..1c108831acf 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -5,7 +5,7 @@ from __future__ import annotations from functools import partial import logging import re -from typing import Any, Literal +from typing import Any, Final import transmission_rpc from transmission_rpc.error import ( @@ -278,8 +278,7 @@ async def get_api( hass: HomeAssistant, entry: dict[str, Any] ) -> transmission_rpc.Client: """Get Transmission client.""" - protocol: Literal["http", "https"] - protocol = "https" if entry[CONF_SSL] else "http" + protocol: Final = "https" if entry[CONF_SSL] else "http" host = entry[CONF_HOST] port = entry[CONF_PORT] path = entry[CONF_PATH] diff --git a/requirements_test.txt b/requirements_test.txt index 0088fa8e17e..19a60b6aa28 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ astroid==3.2.4 coverage==7.6.0 freezegun==1.5.1 mock-open==1.4.0 -mypy-dev==1.12.0a1 +mypy-dev==1.12.0a2 pre-commit==3.7.1 pydantic==1.10.17 pylint==3.2.6 From 78a98afb8d7f3999cdce7872ffc56d8fc6ff2a18 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Fri, 26 Jul 2024 04:48:26 +0300 Subject: [PATCH 1581/2411] Remove obsolete string from openai_conversation strings.json (#122623) --- homeassistant/components/openai_conversation/strings.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json index 4af333d42b4..2477155e3cb 100644 --- a/homeassistant/components/openai_conversation/strings.json +++ b/homeassistant/components/openai_conversation/strings.json @@ -64,11 +64,5 @@ "invalid_config_entry": { "message": "Invalid config entry provided. Got {config_entry}" } - }, - "issues": { - "image_size_deprecated_format": { - "title": "Deprecated size format for image generation action", - "description": "OpenAI is now using Dall-E 3 to generate images using `openai_conversation.generate_image`, which supports different sizes. Valid values are now \"1024x1024\", \"1024x1792\", \"1792x1024\". The old values of \"256\", \"512\", \"1024\" are currently interpreted as \"1024x1024\".\nPlease update your scripts or automations with the new parameters." - } } } From e262f759af4097b8b58620b3f943cca7774d1316 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Jul 2024 02:22:56 -0500 Subject: [PATCH 1582/2411] Speed up bluetooth matching (#122626) - use a defaultdict to avoid lots of setdefault - move the intersection outside of the genexpr to avoid entering the genexpr if there is no intersection --- homeassistant/components/bluetooth/match.py | 61 +++++++++++---------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 06caf18c9f1..ee62420b692 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections import defaultdict from dataclasses import dataclass from fnmatch import translate from functools import lru_cache @@ -173,10 +174,10 @@ class BluetoothMatcherIndexBase[ def __init__(self) -> None: """Initialize the matcher index.""" - self.local_name: dict[str, list[_T]] = {} - self.service_uuid: dict[str, list[_T]] = {} - self.service_data_uuid: dict[str, list[_T]] = {} - self.manufacturer_id: dict[int, list[_T]] = {} + self.local_name: defaultdict[str, list[_T]] = defaultdict(list) + self.service_uuid: defaultdict[str, list[_T]] = defaultdict(list) + self.service_data_uuid: defaultdict[str, list[_T]] = defaultdict(list) + self.manufacturer_id: defaultdict[int, list[_T]] = defaultdict(list) self.service_uuid_set: set[str] = set() self.service_data_uuid_set: set[str] = set() self.manufacturer_id_set: set[int] = set() @@ -190,26 +191,22 @@ class BluetoothMatcherIndexBase[ """ # Local name is the cheapest to match since its just a dict lookup if LOCAL_NAME in matcher: - self.local_name.setdefault( - _local_name_to_index_key(matcher[LOCAL_NAME]), [] - ).append(matcher) + self.local_name[_local_name_to_index_key(matcher[LOCAL_NAME])].append( + matcher + ) return True # Manufacturer data is 2nd cheapest since its all ints if MANUFACTURER_ID in matcher: - self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append( - matcher - ) + self.manufacturer_id[matcher[MANUFACTURER_ID]].append(matcher) return True if SERVICE_UUID in matcher: - self.service_uuid.setdefault(matcher[SERVICE_UUID], []).append(matcher) + self.service_uuid[matcher[SERVICE_UUID]].append(matcher) return True if SERVICE_DATA_UUID in matcher: - self.service_data_uuid.setdefault(matcher[SERVICE_DATA_UUID], []).append( - matcher - ) + self.service_data_uuid[matcher[SERVICE_DATA_UUID]].append(matcher) return True return False @@ -260,32 +257,38 @@ class BluetoothMatcherIndexBase[ if ble_device_matches(matcher, service_info) ) - if self.service_data_uuid_set and service_info.service_data: + if ( + (service_data_uuid_set := self.service_data_uuid_set) + and (service_data := service_info.service_data) + and (matched_uuids := service_data_uuid_set.intersection(service_data)) + ): matches.extend( matcher - for service_data_uuid in self.service_data_uuid_set.intersection( - service_info.service_data - ) + for service_data_uuid in matched_uuids for matcher in self.service_data_uuid[service_data_uuid] if ble_device_matches(matcher, service_info) ) - if self.manufacturer_id_set and service_info.manufacturer_data: + if ( + (manufacturer_id_set := self.manufacturer_id_set) + and (manufacturer_data := service_info.manufacturer_data) + and (matched_ids := manufacturer_id_set.intersection(manufacturer_data)) + ): matches.extend( matcher - for manufacturer_id in self.manufacturer_id_set.intersection( - service_info.manufacturer_data - ) + for manufacturer_id in matched_ids for matcher in self.manufacturer_id[manufacturer_id] if ble_device_matches(matcher, service_info) ) - if self.service_uuid_set and service_info.service_uuids: + if ( + (service_uuid_set := self.service_uuid_set) + and (service_uuids := service_info.service_uuids) + and (matched_uuids := service_uuid_set.intersection(service_uuids)) + ): matches.extend( matcher - for service_uuid in self.service_uuid_set.intersection( - service_info.service_uuids - ) + for service_uuid in matched_uuids for matcher in self.service_uuid[service_uuid] if ble_device_matches(matcher, service_info) ) @@ -310,7 +313,9 @@ class BluetoothCallbackMatcherIndex( def __init__(self) -> None: """Initialize the matcher index.""" super().__init__() - self.address: dict[str, list[BluetoothCallbackMatcherWithCallback]] = {} + self.address: defaultdict[str, list[BluetoothCallbackMatcherWithCallback]] = ( + defaultdict(list) + ) self.connectable: list[BluetoothCallbackMatcherWithCallback] = [] def add_callback_matcher( @@ -323,7 +328,7 @@ class BluetoothCallbackMatcherIndex( We put them in the bucket that they are most likely to match. """ if ADDRESS in matcher: - self.address.setdefault(matcher[ADDRESS], []).append(matcher) + self.address[matcher[ADDRESS]].append(matcher) return if super().add(matcher): From 9b4cf873c170b7cf2d45942e80a4ba7bda300f3d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 26 Jul 2024 09:36:41 +0200 Subject: [PATCH 1583/2411] Replace ConfigEntry with MockConfigEntry in deCONZ tests (#122631) --- tests/components/deconz/conftest.py | 2 +- tests/components/deconz/test_binary_sensor.py | 11 ++++++----- tests/components/deconz/test_config_flow.py | 16 ++++++++-------- tests/components/deconz/test_deconz_event.py | 13 ++++++------- tests/components/deconz/test_device_trigger.py | 9 ++++----- tests/components/deconz/test_diagnostics.py | 4 ++-- tests/components/deconz/test_fan.py | 5 ++--- tests/components/deconz/test_hub.py | 16 ++++++++++------ tests/components/deconz/test_init.py | 7 +++---- tests/components/deconz/test_light.py | 9 ++++----- tests/components/deconz/test_lock.py | 6 +++--- tests/components/deconz/test_services.py | 5 ++--- tests/components/deconz/test_siren.py | 4 ++-- tests/components/deconz/test_switch.py | 4 ++-- 14 files changed, 55 insertions(+), 56 deletions(-) diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py index b468e402c34..fd3003b96ef 100644 --- a/tests/components/deconz/conftest.py +++ b/tests/components/deconz/conftest.py @@ -219,7 +219,7 @@ async def fixture_config_entry_factory( @pytest.fixture(name="config_entry_setup") async def fixture_config_entry_setup( - config_entry_factory: Callable[[], Coroutine[Any, Any, MockConfigEntry]], + config_entry_factory: ConfigEntryFactoryType, ) -> MockConfigEntry: """Fixture providing a set up instance of deCONZ integration.""" return await config_entry_factory() diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index b3e80942981..78f6a5f4b0e 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -14,14 +14,13 @@ from homeassistant.components.deconz.const import ( DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH -from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from .conftest import ConfigEntryFactoryType, WebsocketDataType -from tests.common import snapshot_platform +from tests.common import MockConfigEntry, snapshot_platform TEST_DATA = [ ( # Alarm binary sensor @@ -409,7 +408,9 @@ async def test_not_allow_clip_sensor(hass: HomeAssistant) -> None: ], ) @pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_CLIP_SENSOR: True}]) -async def test_allow_clip_sensor(hass: HomeAssistant, config_entry_setup) -> None: +async def test_allow_clip_sensor( + hass: HomeAssistant, config_entry_setup: MockConfigEntry +) -> None: """Test that CLIP sensors can be allowed.""" assert len(hass.states.async_all()) == 3 @@ -470,7 +471,7 @@ async def test_add_new_binary_sensor( async def test_add_new_binary_sensor_ignored_load_entities_on_service_call( hass: HomeAssistant, entity_registry: er.EntityRegistry, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, deconz_payload: dict[str, Any], mock_requests: Callable[[str], None], sensor_ws_data: WebsocketDataType, @@ -515,7 +516,7 @@ async def test_add_new_binary_sensor_ignored_load_entities_on_service_call( async def test_add_new_binary_sensor_ignored_load_entities_on_options_change( hass: HomeAssistant, entity_registry: er.EntityRegistry, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, deconz_payload: dict[str, Any], mock_requests: Callable[[str], None], sensor_ws_data: WebsocketDataType, diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 434856549c6..49711962407 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -27,7 +27,6 @@ from homeassistant.config_entries import ( SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER, - ConfigEntry, ) from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant @@ -35,6 +34,7 @@ from homeassistant.data_entry_flow import FlowResultType from .conftest import API_KEY, BRIDGE_ID +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker BAD_BRIDGEID = "0000000000000000" @@ -225,7 +225,7 @@ async def test_manual_configuration_after_discovery_ResponseError( async def test_manual_configuration_update_configuration( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, ) -> None: """Test that manual configuration can update existing config entry.""" aioclient_mock.get( @@ -404,7 +404,7 @@ async def test_link_step_fails( async def test_reauth_flow_update_configuration( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, ) -> None: """Verify reauth flow can update gateway API key.""" result = await hass.config_entries.flow.async_init( @@ -484,7 +484,7 @@ async def test_flow_ssdp_discovery( async def test_ssdp_discovery_update_configuration( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Test if a discovered bridge is configured but updates with new attributes.""" with patch( @@ -513,7 +513,7 @@ async def test_ssdp_discovery_update_configuration( async def test_ssdp_discovery_dont_update_configuration( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Test if a discovered bridge has already been configured.""" @@ -538,7 +538,7 @@ async def test_ssdp_discovery_dont_update_configuration( @pytest.mark.parametrize("config_entry_source", [SOURCE_HASSIO]) async def test_ssdp_discovery_dont_update_existing_hassio_configuration( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Test to ensure the SSDP discovery does not update an Hass.io entry.""" result = await hass.config_entries.flow.async_init( @@ -608,7 +608,7 @@ async def test_flow_hassio_discovery(hass: HomeAssistant) -> None: async def test_hassio_discovery_update_configuration( hass: HomeAssistant, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, ) -> None: """Test we can update an existing config entry.""" with patch( @@ -664,7 +664,7 @@ async def test_hassio_discovery_dont_update_configuration(hass: HomeAssistant) - async def test_option_flow( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Test config flow options.""" result = await hass.config_entries.options.async_init(config_entry_setup.entry_id) diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 8057605f1c5..77cc50ebed6 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -17,7 +17,6 @@ from homeassistant.components.deconz.deconz_event import ( CONF_DECONZ_RELATIVE_ROTARY_EVENT, RELATIVE_ROTARY_DECONZ_TO_EVENT, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_ID, CONF_EVENT, @@ -30,7 +29,7 @@ from homeassistant.helpers import device_registry as dr from .conftest import WebsocketDataType -from tests.common import async_capture_events +from tests.common import MockConfigEntry, async_capture_events @pytest.mark.parametrize( @@ -78,7 +77,7 @@ from tests.common import async_capture_events async def test_deconz_events( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, sensor_ws_data: WebsocketDataType, ) -> None: """Test successful creation of deconz events.""" @@ -246,7 +245,7 @@ async def test_deconz_events( async def test_deconz_alarm_events( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, sensor_ws_data: WebsocketDataType, ) -> None: """Test successful creation of deconz alarm events.""" @@ -380,7 +379,7 @@ async def test_deconz_alarm_events( async def test_deconz_presence_events( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, sensor_ws_data: WebsocketDataType, ) -> None: """Test successful creation of deconz presence events.""" @@ -468,7 +467,7 @@ async def test_deconz_presence_events( async def test_deconz_relative_rotary_events( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, sensor_ws_data: WebsocketDataType, ) -> None: """Test successful creation of deconz relative rotary events.""" @@ -549,7 +548,7 @@ async def test_deconz_relative_rotary_events( async def test_deconz_events_bad_unique_id( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, ) -> None: """Verify no devices are created if unique id is bad or missing.""" assert len(hass.states.async_all()) == 1 diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 46d36229488..211ce14b8dc 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -18,7 +18,6 @@ from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.device_trigger import CONF_SUBTYPE from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, @@ -35,7 +34,7 @@ from homeassistant.setup import async_setup_component from .conftest import WebsocketDataType -from tests.common import async_get_device_automations +from tests.common import MockConfigEntry, async_get_device_automations @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -381,7 +380,7 @@ async def test_validate_trigger_unknown_device(hass: HomeAssistant) -> None: async def test_validate_trigger_unsupported_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, ) -> None: """Test unsupported device doesn't return a trigger config.""" device = device_registry.async_get_or_create( @@ -421,7 +420,7 @@ async def test_validate_trigger_unsupported_device( async def test_validate_trigger_unsupported_trigger( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, ) -> None: """Test unsupported trigger does not return a trigger config.""" device = device_registry.async_get_or_create( @@ -463,7 +462,7 @@ async def test_validate_trigger_unsupported_trigger( async def test_attach_trigger_no_matching_event( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, ) -> None: """Test no matching event for device doesn't return a trigger config.""" device = device_registry.async_get_or_create( diff --git a/tests/components/deconz/test_diagnostics.py b/tests/components/deconz/test_diagnostics.py index 615cce03ec2..a490c95d5e6 100644 --- a/tests/components/deconz/test_diagnostics.py +++ b/tests/components/deconz/test_diagnostics.py @@ -3,11 +3,11 @@ from pydeconz.websocket import State from syrupy import SnapshotAssertion -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .conftest import WebsocketStateType +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -15,7 +15,7 @@ from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, mock_websocket_state: WebsocketStateType, snapshot: SnapshotAssertion, ) -> None: diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index cccf894c249..9fd022e65a5 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -13,7 +13,6 @@ from homeassistant.components.fan import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, STATE_OFF, @@ -24,7 +23,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import WebsocketDataType +from .conftest import ConfigEntryFactoryType, WebsocketDataType from tests.common import snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker @@ -57,7 +56,7 @@ async def test_fans( entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, aioclient_mock: AiohttpClientMocker, - config_entry_factory: ConfigEntry, + config_entry_factory: ConfigEntryFactoryType, mock_put_request: Callable[[str, str], AiohttpClientMocker], light_ws_data: WebsocketDataType, ) -> None: diff --git a/tests/components/deconz/test_hub.py b/tests/components/deconz/test_hub.py index 3a334a47838..9f6c5a8b90f 100644 --- a/tests/components/deconz/test_hub.py +++ b/tests/components/deconz/test_hub.py @@ -17,16 +17,18 @@ from homeassistant.components.ssdp import ( ATTR_UPNP_SERIAL, ATTR_UPNP_UDN, ) -from homeassistant.config_entries import SOURCE_SSDP, ConfigEntry +from homeassistant.config_entries import SOURCE_SSDP from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from .conftest import BRIDGE_ID +from tests.common import MockConfigEntry + async def test_device_registry_entry( - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, ) -> None: @@ -68,7 +70,7 @@ async def test_connection_status_signalling( async def test_update_address( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Make sure that connection status triggers a dispatcher send.""" gateway = DeconzHub.get_hub(hass, config_entry_setup) @@ -99,7 +101,7 @@ async def test_update_address( async def test_reset_after_successful_setup( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Make sure that connection status triggers a dispatcher send.""" gateway = DeconzHub.get_hub(hass, config_entry_setup) @@ -110,7 +112,9 @@ async def test_reset_after_successful_setup( assert result is True -async def test_get_deconz_api(hass: HomeAssistant, config_entry: ConfigEntry) -> None: +async def test_get_deconz_api( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: """Successful call.""" with patch("pydeconz.DeconzSession.refresh_state", return_value=True): assert await get_deconz_api(hass, config_entry) @@ -127,7 +131,7 @@ async def test_get_deconz_api(hass: HomeAssistant, config_entry: ConfigEntry) -> ) async def test_get_deconz_api_fails( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MockConfigEntry, side_effect: Exception, raised_exception: Exception, ) -> None: diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 0a4e63de6ab..e13135850ae 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -10,7 +10,6 @@ from homeassistant.components.deconz import ( ) from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.errors import AuthenticationRequired, CannotConnect -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .conftest import ConfigEntryFactoryType @@ -18,7 +17,7 @@ from .conftest import ConfigEntryFactoryType from tests.common import MockConfigEntry -async def setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def setup_entry(hass: HomeAssistant, entry: MockConfigEntry) -> None: """Test that setup entry works.""" with ( patch.object(DeconzHub, "async_setup", return_value=True), @@ -28,7 +27,7 @@ async def setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def test_setup_entry_successful( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Test setup entry is successful.""" assert hass.data[DECONZ_DOMAIN] @@ -86,7 +85,7 @@ async def test_setup_entry_multiple_gateways( async def test_unload_entry( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Test being able to unload an entry.""" assert hass.data[DECONZ_DOMAIN] diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 7bc2d961d13..c74005d96d4 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -27,7 +27,6 @@ from homeassistant.components.light import ( ColorMode, LightEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -41,7 +40,7 @@ from homeassistant.helpers import entity_registry as er from .conftest import ConfigEntryFactoryType, WebsocketDataType -from tests.common import snapshot_platform +from tests.common import MockConfigEntry, snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker @@ -491,7 +490,7 @@ async def test_light_state_change( async def test_light_service_calls( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - config_entry_factory: Callable[[], ConfigEntry], + config_entry_factory: ConfigEntryFactoryType, light_payload: dict[str, Any], mock_put_request: Callable[[str, str], AiohttpClientMocker], input: dict[str, Any], @@ -730,7 +729,7 @@ async def test_configuration_tool(hass: HomeAssistant) -> None: async def test_groups( hass: HomeAssistant, entity_registry: er.EntityRegistry, - config_entry_factory: Callable[[], ConfigEntry], + config_entry_factory: ConfigEntryFactoryType, group_payload: dict[str, Any], input: dict[str, list[str]], snapshot: SnapshotAssertion, @@ -972,7 +971,7 @@ async def test_empty_group(hass: HomeAssistant) -> None: @pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_DECONZ_GROUPS: False}]) async def test_disable_light_groups( hass: HomeAssistant, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, ) -> None: """Test disallowing light groups work.""" assert len(hass.states.async_all()) == 1 diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index a370261616b..452a4685150 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -9,7 +9,6 @@ from homeassistant.components.lock import ( SERVICE_LOCK, SERVICE_UNLOCK, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, STATE_LOCKED, @@ -20,6 +19,7 @@ from homeassistant.core import HomeAssistant from .conftest import WebsocketDataType +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -43,7 +43,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker ) async def test_lock_from_light( hass: HomeAssistant, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], light_ws_data: WebsocketDataType, ) -> None: @@ -118,7 +118,7 @@ async def test_lock_from_light( ) async def test_lock_from_sensor( hass: HomeAssistant, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], sensor_ws_data: WebsocketDataType, ) -> None: diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 4b0d8ab1405..9a30564385c 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -21,13 +21,12 @@ from homeassistant.components.deconz.services import ( SERVICE_REMOVE_ORPHANED_ENTRIES, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from .test_hub import BRIDGE_ID -from tests.common import async_capture_events +from tests.common import MockConfigEntry, async_capture_events from tests.test_util.aiohttp import AiohttpClientMocker @@ -329,7 +328,7 @@ async def test_remove_orphaned_entries_service( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, ) -> None: """Test service works and also don't remove more than expected.""" device = device_registry.async_get_or_create( diff --git a/tests/components/deconz/test_siren.py b/tests/components/deconz/test_siren.py index 2d11468bfad..488f12cd65d 100644 --- a/tests/components/deconz/test_siren.py +++ b/tests/components/deconz/test_siren.py @@ -5,7 +5,6 @@ from collections.abc import Callable import pytest from homeassistant.components.siren import ATTR_DURATION, DOMAIN as SIREN_DOMAIN -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -18,6 +17,7 @@ from homeassistant.core import HomeAssistant from .conftest import WebsocketDataType +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -34,7 +34,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker ) async def test_sirens( hass: HomeAssistant, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, light_ws_data: WebsocketDataType, mock_put_request: Callable[[str, str], AiohttpClientMocker], ) -> None: diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 1b28c8d3939..60731162e35 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -11,13 +11,13 @@ from homeassistant.components.switch import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from .conftest import ConfigEntryFactoryType, WebsocketDataType +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -54,7 +54,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker ) async def test_power_plugs( hass: HomeAssistant, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], light_ws_data: WebsocketDataType, ) -> None: From 621bd5f0c3d3bedb8bc9cb95d8788c8a95c13286 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 26 Jul 2024 17:40:49 +1000 Subject: [PATCH 1584/2411] Add dynamic coordinator interval to Tesla Fleet (#122234) * Add dynamic rate limiter * tweaks * Revert min polling back to 2min * Set max 1 hour * Remove redundant update_interval * Tuning and fixes * Reduce double API calls * Type test * Remove RateCalculator --- .../components/tesla_fleet/coordinator.py | 25 +++++++++++++------ .../components/tesla_fleet/manifest.json | 2 +- .../components/teslemetry/manifest.json | 2 +- homeassistant/components/tessie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tesla_fleet/conftest.py | 15 +++++------ tests/components/tesla_fleet/test_init.py | 15 +++++------ 8 files changed, 38 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/tesla_fleet/coordinator.py b/homeassistant/components/tesla_fleet/coordinator.py index dad592d3033..42b93352a6f 100644 --- a/homeassistant/components/tesla_fleet/coordinator.py +++ b/homeassistant/components/tesla_fleet/coordinator.py @@ -13,6 +13,7 @@ from tesla_fleet_api.exceptions import ( TeslaFleetError, VehicleOffline, ) +from tesla_fleet_api.ratecalculator import RateCalculator from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -20,7 +21,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import LOGGER, TeslaFleetState -VEHICLE_INTERVAL_SECONDS = 120 +VEHICLE_INTERVAL_SECONDS = 90 VEHICLE_INTERVAL = timedelta(seconds=VEHICLE_INTERVAL_SECONDS) VEHICLE_WAIT = timedelta(minutes=15) @@ -56,6 +57,7 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): updated_once: bool pre2021: bool last_active: datetime + rate: RateCalculator def __init__( self, hass: HomeAssistant, api: VehicleSpecific, product: dict @@ -65,27 +67,31 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): hass, LOGGER, name="Tesla Fleet Vehicle", - update_interval=timedelta(seconds=5), + update_interval=VEHICLE_INTERVAL, ) self.api = api self.data = flatten(product) self.updated_once = False self.last_active = datetime.now() + self.rate = RateCalculator(200, 86400, VEHICLE_INTERVAL_SECONDS, 3600, 5) async def _async_update_data(self) -> dict[str, Any]: """Update vehicle data using TeslaFleet API.""" - self.update_interval = VEHICLE_INTERVAL - try: # Check if the vehicle is awake using a non-rate limited API call - state = (await self.api.vehicle())["response"] - if state and state["state"] != TeslaFleetState.ONLINE: - self.data["state"] = state["state"] + if self.data["state"] != TeslaFleetState.ONLINE: + response = await self.api.vehicle() + self.data["state"] = response["response"]["state"] + + if self.data["state"] != TeslaFleetState.ONLINE: return self.data # This is a rated limited API call - data = (await self.api.vehicle_data(endpoints=ENDPOINTS))["response"] + self.rate.consume() + response = await self.api.vehicle_data(endpoints=ENDPOINTS) + data = response["response"] + except VehicleOffline: self.data["state"] = TeslaFleetState.ASLEEP return self.data @@ -103,6 +109,9 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): except TeslaFleetError as e: raise UpdateFailed(e.message) from e + # Calculate ideal refresh interval + self.update_interval = timedelta(seconds=self.rate.calculate()) + self.updated_once = True if self.api.pre2021 and data["state"] == TeslaFleetState.ONLINE: diff --git a/homeassistant/components/tesla_fleet/manifest.json b/homeassistant/components/tesla_fleet/manifest.json index 310d8940432..2acacab5065 100644 --- a/homeassistant/components/tesla_fleet/manifest.json +++ b/homeassistant/components/tesla_fleet/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/tesla_fleet", "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], - "requirements": ["tesla-fleet-api==0.7.2"] + "requirements": ["tesla-fleet-api==0.7.3"] } diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index e241c7c4ae4..1780d9f0a10 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], "quality_scale": "platinum", - "requirements": ["tesla-fleet-api==0.7.2"] + "requirements": ["tesla-fleet-api==0.7.3"] } diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json index e1908350e91..6059072c239 100644 --- a/homeassistant/components/tessie/manifest.json +++ b/homeassistant/components/tessie/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["tessie"], "quality_scale": "platinum", - "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.7.2"] + "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.7.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0481c9e2a84..80c3834db5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2723,7 +2723,7 @@ temperusb==1.6.1 # homeassistant.components.tesla_fleet # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==0.7.2 +tesla-fleet-api==0.7.3 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37677cb05e4..79aa747b117 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2136,7 +2136,7 @@ temperusb==1.6.1 # homeassistant.components.tesla_fleet # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==0.7.2 +tesla-fleet-api==0.7.3 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/tests/components/tesla_fleet/conftest.py b/tests/components/tesla_fleet/conftest.py index 4e4d7b406d5..ade2f6eb0a9 100644 --- a/tests/components/tesla_fleet/conftest.py +++ b/tests/components/tesla_fleet/conftest.py @@ -2,9 +2,10 @@ from __future__ import annotations +from collections.abc import Generator from copy import deepcopy import time -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import jwt import pytest @@ -83,7 +84,7 @@ async def setup_credentials(hass: HomeAssistant) -> None: @pytest.fixture(autouse=True) -def mock_products(): +def mock_products() -> Generator[AsyncMock]: """Mock Tesla Fleet Api products method.""" with patch( "homeassistant.components.tesla_fleet.TeslaFleetApi.products", @@ -93,7 +94,7 @@ def mock_products(): @pytest.fixture(autouse=True) -def mock_vehicle_state(): +def mock_vehicle_state() -> Generator[AsyncMock]: """Mock Tesla Fleet API Vehicle Specific vehicle method.""" with patch( "homeassistant.components.tesla_fleet.VehicleSpecific.vehicle", @@ -103,7 +104,7 @@ def mock_vehicle_state(): @pytest.fixture(autouse=True) -def mock_vehicle_data(): +def mock_vehicle_data() -> Generator[AsyncMock]: """Mock Tesla Fleet API Vehicle Specific vehicle_data method.""" with patch( "homeassistant.components.tesla_fleet.VehicleSpecific.vehicle_data", @@ -113,7 +114,7 @@ def mock_vehicle_data(): @pytest.fixture(autouse=True) -def mock_wake_up(): +def mock_wake_up() -> Generator[AsyncMock]: """Mock Tesla Fleet API Vehicle Specific wake_up method.""" with patch( "homeassistant.components.tesla_fleet.VehicleSpecific.wake_up", @@ -123,7 +124,7 @@ def mock_wake_up(): @pytest.fixture(autouse=True) -def mock_live_status(): +def mock_live_status() -> Generator[AsyncMock]: """Mock Teslemetry Energy Specific live_status method.""" with patch( "homeassistant.components.tesla_fleet.EnergySpecific.live_status", @@ -133,7 +134,7 @@ def mock_live_status(): @pytest.fixture(autouse=True) -def mock_site_info(): +def mock_site_info() -> Generator[AsyncMock]: """Mock Teslemetry Energy Specific site_info method.""" with patch( "homeassistant.components.tesla_fleet.EnergySpecific.site_info", diff --git a/tests/components/tesla_fleet/test_init.py b/tests/components/tesla_fleet/test_init.py index 4e6352efc6b..20bb6c66906 100644 --- a/tests/components/tesla_fleet/test_init.py +++ b/tests/components/tesla_fleet/test_init.py @@ -1,5 +1,7 @@ """Test the Tesla Fleet init.""" +from unittest.mock import AsyncMock + from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion @@ -102,18 +104,17 @@ async def test_vehicle_refresh_offline( mock_vehicle_state.reset_mock() mock_vehicle_data.reset_mock() - # Test the unlikely condition that a vehicle state is online but actually offline + # Then the vehicle goes offline mock_vehicle_data.side_effect = VehicleOffline freezer.tick(VEHICLE_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() - mock_vehicle_state.assert_called_once() + mock_vehicle_state.assert_not_called() mock_vehicle_data.assert_called_once() - mock_vehicle_state.reset_mock() mock_vehicle_data.reset_mock() - # Test the normal condition that a vehcile state is offline + # And stays offline mock_vehicle_state.return_value = VEHICLE_ASLEEP freezer.tick(VEHICLE_INTERVAL) async_fire_time_changed(hass) @@ -127,15 +128,15 @@ async def test_vehicle_refresh_offline( async def test_vehicle_refresh_error( hass: HomeAssistant, normal_config_entry: MockConfigEntry, - mock_vehicle_state, - side_effect, + mock_vehicle_data: AsyncMock, + side_effect: TeslaFleetError, freezer: FrozenDateTimeFactory, ) -> None: """Test coordinator refresh makes entity unavailable.""" await setup_platform(hass, normal_config_entry) - mock_vehicle_state.side_effect = side_effect + mock_vehicle_data.side_effect = side_effect freezer.tick(VEHICLE_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() From 51d5e21203b1a3e591c254a56815bf5c92829c45 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 26 Jul 2024 09:48:12 +0200 Subject: [PATCH 1585/2411] Remove unused fixtures in UniFi tests (#122628) --- tests/components/unifi/conftest.py | 2 +- tests/components/unifi/test_config_flow.py | 15 +++++---------- tests/components/unifi/test_init.py | 5 +---- tests/components/unifi/test_services.py | 2 +- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index f1ebe84c350..c20b8766bfc 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -295,7 +295,7 @@ async def fixture_config_entry_factory( @pytest.fixture(name="config_entry_setup") async def fixture_config_entry_setup( - hass: HomeAssistant, config_entry_factory: Callable[[], ConfigEntry] + config_entry_factory: Callable[[], ConfigEntry], ) -> ConfigEntry: """Fixture providing a set up instance of UniFi network integration.""" return await config_entry_factory() diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 9ae3af19b46..fc0d2626eb6 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -37,7 +37,6 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -from tests.test_util.aiohttp import AiohttpClientMocker CLIENTS = [{"mac": "00:00:00:00:00:01"}] @@ -137,9 +136,7 @@ async def test_flow_works(hass: HomeAssistant, mock_discovery) -> None: } -async def test_flow_works_negative_discovery( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +async def test_flow_works_negative_discovery(hass: HomeAssistant) -> None: """Test config flow with a negative outcome of async_discovery_unifi.""" result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -535,9 +532,8 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: } -async def test_form_ssdp_aborts_if_host_already_exists( - hass: HomeAssistant, config_entry: ConfigEntry -) -> None: +@pytest.mark.usefixtures("config_entry") +async def test_form_ssdp_aborts_if_host_already_exists(hass: HomeAssistant) -> None: """Test we abort if the host is already configured.""" result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, @@ -557,9 +553,8 @@ async def test_form_ssdp_aborts_if_host_already_exists( assert result["reason"] == "already_configured" -async def test_form_ssdp_aborts_if_serial_already_exists( - hass: HomeAssistant, config_entry: ConfigEntry -) -> None: +@pytest.mark.usefixtures("config_entry") +async def test_form_ssdp_aborts_if_serial_already_exists(hass: HomeAssistant) -> None: """Test we abort if the serial is already configured.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 7cd203ab8fd..fe464989af3 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -24,7 +24,6 @@ from homeassistant.setup import async_setup_component from .conftest import DEFAULT_CONFIG_ENTRY_ID from tests.common import flush_store -from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import WebSocketGenerator @@ -35,7 +34,7 @@ async def test_setup_with_no_config(hass: HomeAssistant) -> None: async def test_setup_entry_fails_config_entry_not_ready( - hass: HomeAssistant, config_entry_factory: Callable[[], ConfigEntry] + config_entry_factory: Callable[[], ConfigEntry], ) -> None: """Failed authentication trigger a reauthentication flow.""" with patch( @@ -170,8 +169,6 @@ async def test_wireless_clients( ) async def test_remove_config_entry_device( hass: HomeAssistant, - hass_storage: dict[str, Any], - aioclient_mock: AiohttpClientMocker, device_registry: dr.DeviceRegistry, config_entry_factory: Callable[[], ConfigEntry], client_payload: list[dict[str, Any]], diff --git a/tests/components/unifi/test_services.py b/tests/components/unifi/test_services.py index e3b03bc868d..bf7058e28ff 100644 --- a/tests/components/unifi/test_services.py +++ b/tests/components/unifi/test_services.py @@ -289,7 +289,7 @@ async def test_services_handle_unloaded_config_entry( aioclient_mock: AiohttpClientMocker, device_registry: dr.DeviceRegistry, config_entry_setup: ConfigEntry, - clients_all_payload, + clients_all_payload: dict[str, Any], ) -> None: """Verify no call is made if config entry is unloaded.""" await hass.config_entries.async_unload(config_entry_setup.entry_id) From c9b81a5c044c3737f1a15915084fcac622a2cc64 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 26 Jul 2024 09:48:37 +0200 Subject: [PATCH 1586/2411] Replace ConfigEntry with MockConfigEntry in Axis tests (#122629) * Remove unused fixtures in Axis tests * Replace ConfigEntry with MockConfigEntry --- tests/components/axis/conftest.py | 1 - tests/components/axis/test_diagnostics.py | 4 ++-- tests/components/axis/test_hub.py | 14 +++++++------- tests/components/axis/test_init.py | 14 +++++++++----- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index 0cbfdc007c0..30e1b7335b9 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -85,7 +85,6 @@ def fixture_setup_entry() -> Generator[AsyncMock]: @pytest.fixture(name="config_entry") def fixture_config_entry( - hass: HomeAssistant, config_entry_data: MappingProxyType[str, Any], config_entry_options: MappingProxyType[str, Any], config_entry_version: int, diff --git a/tests/components/axis/test_diagnostics.py b/tests/components/axis/test_diagnostics.py index b949c23236b..07caf5b39de 100644 --- a/tests/components/axis/test_diagnostics.py +++ b/tests/components/axis/test_diagnostics.py @@ -3,11 +3,11 @@ import pytest from syrupy import SnapshotAssertion -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import API_DISCOVERY_BASIC_DEVICE_INFO +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -16,7 +16,7 @@ from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index a28f6f4dabc..d7f303539e4 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -14,7 +14,7 @@ from syrupy import SnapshotAssertion from homeassistant.components import axis, zeroconf from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigEntry +from homeassistant.config_entries import SOURCE_ZEROCONF from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -28,7 +28,7 @@ from .const import ( NAME, ) -from tests.common import async_fire_mqtt_message +from tests.common import MockConfigEntry, async_fire_mqtt_message from tests.typing import MqttMockHAClient @@ -36,7 +36,7 @@ from tests.typing import MqttMockHAClient "api_discovery_items", [({}), (API_DISCOVERY_BASIC_DEVICE_INFO)] ) async def test_device_registry_entry( - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, ) -> None: @@ -83,7 +83,7 @@ async def test_device_support_mqtt_low_privilege(mqtt_mock: MqttMockHAClient) -> async def test_update_address( hass: HomeAssistant, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, mock_requests: Callable[[str], None], ) -> None: """Test update address works.""" @@ -148,7 +148,7 @@ async def test_device_unavailable( @pytest.mark.usefixtures("mock_default_requests") async def test_device_not_accessible( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Failed setup schedules a retry of setup.""" config_entry.add_to_hass(hass) @@ -160,7 +160,7 @@ async def test_device_not_accessible( @pytest.mark.usefixtures("mock_default_requests") async def test_device_trigger_reauth_flow( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Failed authentication trigger a reauthentication flow.""" config_entry.add_to_hass(hass) @@ -178,7 +178,7 @@ async def test_device_trigger_reauth_flow( @pytest.mark.usefixtures("mock_default_requests") async def test_device_unknown_error( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Unknown errors are handled.""" config_entry.add_to_hass(hass) diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index acfcb8d48ec..89737325440 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -5,17 +5,19 @@ from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant.components import axis -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry -async def test_setup_entry(config_entry_setup: ConfigEntry) -> None: + +async def test_setup_entry(config_entry_setup: MockConfigEntry) -> None: """Test successful setup of entry.""" assert config_entry_setup.state is ConfigEntryState.LOADED async def test_setup_entry_fails( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test successful setup of entry.""" config_entry.add_to_hass(hass) @@ -32,7 +34,7 @@ async def test_setup_entry_fails( async def test_unload_entry( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Test successful unload of entry.""" assert config_entry_setup.state is ConfigEntryState.LOADED @@ -42,7 +44,9 @@ async def test_unload_entry( @pytest.mark.parametrize("config_entry_version", [1]) -async def test_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: +async def test_migrate_entry( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: """Test successful migration of entry data.""" config_entry.add_to_hass(hass) assert config_entry.version == 1 From b41b7aeb5bc7866faafc52b418f0a54304a13aed Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 26 Jul 2024 10:06:58 +0200 Subject: [PATCH 1587/2411] Remove validation of state==UNAVAILABLE on config entry unload in deCONZ test (#122558) Only test remove entry marks entities unavailable in one place --- homeassistant/components/deconz/fan.py | 1 - .../deconz/test_alarm_control_panel.py | 8 --- tests/components/deconz/test_binary_sensor.py | 13 +---- tests/components/deconz/test_button.py | 13 +---- tests/components/deconz/test_climate.py | 19 +------ tests/components/deconz/test_cover.py | 13 +---- tests/components/deconz/test_deconz_event.py | 52 +------------------ tests/components/deconz/test_fan.py | 19 +------ tests/components/deconz/test_light.py | 21 -------- tests/components/deconz/test_lock.py | 29 +---------- tests/components/deconz/test_number.py | 13 +---- tests/components/deconz/test_scene.py | 13 +---- tests/components/deconz/test_select.py | 13 +---- tests/components/deconz/test_sensor.py | 13 +---- tests/components/deconz/test_siren.py | 12 ----- tests/components/deconz/test_switch.py | 13 +---- 16 files changed, 12 insertions(+), 253 deletions(-) diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index 6192421ecdc..dc65756eeeb 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -66,7 +66,6 @@ class DeconzFan(DeconzDevice[Light], FanEntity): def __init__(self, device: Light, hub: DeconzHub) -> None: """Set up fan.""" super().__init__(device, hub) - _attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS) if device.fan_speed in ORDERED_NAMED_FAN_SPEEDS: self._default_on_speed = device.fan_speed diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py index 712dddc7225..6c47146f9b0 100644 --- a/tests/components/deconz/test_alarm_control_panel.py +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -24,7 +24,6 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, - STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant @@ -158,10 +157,3 @@ async def test_alarm_control_panel( blocking=True, ) assert aioclient_mock.mock_calls[0][2] == {"code0": code} - - await hass.config_entries.async_unload(config_entry.entry_id) - assert hass.states.get("alarm_control_panel.keypad").state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 78f6a5f4b0e..59d31afb9fc 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.components.deconz.const import ( DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform +from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -344,17 +344,6 @@ async def test_binary_sensors( await sensor_ws_data({"state": expected["websocket_event"]}) assert hass.states.get(expected["entity_id"]).state == expected["next_state"] - # Unload entry - - await hass.config_entries.async_unload(config_entry.entry_id) - assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE - - # Remove entry - - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - @pytest.mark.parametrize( "sensor_payload", diff --git a/tests/components/deconz/test_button.py b/tests/components/deconz/test_button.py index 76fcd784634..c649dba5b00 100644 --- a/tests/components/deconz/test_button.py +++ b/tests/components/deconz/test_button.py @@ -8,7 +8,7 @@ import pytest from syrupy import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -101,14 +101,3 @@ async def test_button( blocking=True, ) assert aioclient_mock.mock_calls[1][2] == expected["request_data"] - - # Unload entry - - await hass.config_entries.async_unload(config_entry.entry_id) - assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE - - # Remove entry - - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index c9104a5583a..7f456e81976 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -30,13 +30,7 @@ from homeassistant.components.deconz.climate import ( DECONZ_PRESET_MANUAL, ) from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_TEMPERATURE, - STATE_OFF, - STATE_UNAVAILABLE, - Platform, -) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_OFF, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er @@ -277,17 +271,6 @@ async def test_climate_device_without_cooling_support( blocking=True, ) - await hass.config_entries.async_unload(config_entry.entry_id) - - states = hass.states.async_all() - assert len(states) == 1 - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - @pytest.mark.parametrize( "sensor_payload", diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index a93d40b4d1e..f1573394fae 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -20,7 +20,7 @@ from homeassistant.components.cover import ( SERVICE_STOP_COVER, SERVICE_STOP_COVER_TILT, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_OPEN, STATE_UNAVAILABLE, Platform +from homeassistant.const import ATTR_ENTITY_ID, STATE_OPEN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -114,17 +114,6 @@ async def test_cover( ) assert aioclient_mock.mock_calls[4][2] == {"stop": True} - await hass.config_entries.async_unload(config_entry.entry_id) - - states = hass.states.async_all() - assert len(states) == 1 - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - @pytest.mark.parametrize( "light_payload", diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 77cc50ebed6..8bf7bb146d1 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -17,13 +17,7 @@ from homeassistant.components.deconz.deconz_event import ( CONF_DECONZ_RELATIVE_ROTARY_EVENT, RELATIVE_ROTARY_DECONZ_TO_EVENT, ) -from homeassistant.const import ( - CONF_DEVICE_ID, - CONF_EVENT, - CONF_ID, - CONF_UNIQUE_ID, - STATE_UNAVAILABLE, -) +from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_UNIQUE_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -166,17 +160,6 @@ async def test_deconz_events( await sensor_ws_data({"id": "1", "name": "other name"}) assert len(captured_events) == 4 - await hass.config_entries.async_unload(config_entry_setup.entry_id) - - states = hass.states.async_all() - assert len(hass.states.async_all()) == 3 - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry_setup.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - @pytest.mark.parametrize( "alarm_system_payload", @@ -336,17 +319,6 @@ async def test_deconz_alarm_events( await sensor_ws_data({"state": {"panel": AncillaryControlPanel.ARMED_AWAY}}) assert len(captured_events) == 4 - await hass.config_entries.async_unload(config_entry_setup.entry_id) - - states = hass.states.async_all() - assert len(hass.states.async_all()) == 4 - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry_setup.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - @pytest.mark.parametrize( "sensor_payload", @@ -425,17 +397,6 @@ async def test_deconz_presence_events( await sensor_ws_data({"state": {"presenceevent": PresenceStatePresenceEvent.NINE}}) assert len(captured_events) == 0 - await hass.config_entries.async_unload(config_entry_setup.entry_id) - - states = hass.states.async_all() - assert len(hass.states.async_all()) == 5 - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry_setup.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - @pytest.mark.parametrize( "sensor_payload", @@ -513,17 +474,6 @@ async def test_deconz_relative_rotary_events( await sensor_ws_data({"name": "123"}) assert len(captured_events) == 0 - await hass.config_entries.async_unload(config_entry_setup.entry_id) - - states = hass.states.async_all() - assert len(hass.states.async_all()) == 1 - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry_setup.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - @pytest.mark.parametrize( "sensor_payload", diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index 9fd022e65a5..21809a138c6 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -13,13 +13,7 @@ from homeassistant.components.fan import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, - Platform, -) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -128,14 +122,3 @@ async def test_fans( await light_ws_data({"state": {"speed": 5}}) assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert not hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] - - await hass.config_entries.async_unload(config_entry.entry_id) - - states = hass.states.async_all() - assert len(states) == 1 - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index c74005d96d4..441cb01be63 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -32,7 +32,6 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, - STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant @@ -295,16 +294,6 @@ async def test_lights( config_entry = await config_entry_factory() await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) - await hass.config_entries.async_unload(config_entry.entry_id) - - states = hass.states.async_all() - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - @pytest.mark.parametrize( "light_payload", @@ -762,16 +751,6 @@ async def test_groups( config_entry = await config_entry_factory() await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) - await hass.config_entries.async_unload(config_entry.entry_id) - - states = hass.states.async_all() - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - @pytest.mark.parametrize( "light_payload", diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index 452a4685150..467b96f200e 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -9,12 +9,7 @@ from homeassistant.components.lock import ( SERVICE_LOCK, SERVICE_UNLOCK, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - STATE_LOCKED, - STATE_UNAVAILABLE, - STATE_UNLOCKED, -) +from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant from .conftest import WebsocketDataType @@ -78,17 +73,6 @@ async def test_lock_from_light( ) assert aioclient_mock.mock_calls[2][2] == {"on": False} - await hass.config_entries.async_unload(config_entry_setup.entry_id) - - states = hass.states.async_all() - assert len(states) == 1 - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry_setup.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - @pytest.mark.parametrize( "sensor_payload", @@ -152,14 +136,3 @@ async def test_lock_from_sensor( blocking=True, ) assert aioclient_mock.mock_calls[2][2] == {"lock": False} - - await hass.config_entries.async_unload(config_entry_setup.entry_id) - - states = hass.states.async_all() - assert len(states) == 2 - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry_setup.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py index 66eccdc6b4c..962c2c0a89b 100644 --- a/tests/components/deconz/test_number.py +++ b/tests/components/deconz/test_number.py @@ -12,7 +12,7 @@ from homeassistant.components.number import ( DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er @@ -136,14 +136,3 @@ async def test_number_entities( }, blocking=True, ) - - # Unload entry - - await hass.config_entries.async_unload(config_entry.entry_id) - assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE - - # Remove entry - - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index d3a5725f7b1..c1240b6881c 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -8,7 +8,7 @@ import pytest from syrupy import SnapshotAssertion from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, SERVICE_TURN_ON -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -64,17 +64,6 @@ async def test_scenes( ) assert aioclient_mock.mock_calls[1][2] == {} - # Unload entry - - await hass.config_entries.async_unload(config_entry.entry_id) - assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE - - # Remove entry - - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - @pytest.mark.parametrize( "group_payload", diff --git a/tests/components/deconz/test_select.py b/tests/components/deconz/test_select.py index cee133f9999..900283d88bb 100644 --- a/tests/components/deconz/test_select.py +++ b/tests/components/deconz/test_select.py @@ -16,7 +16,7 @@ from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -149,14 +149,3 @@ async def test_select( blocking=True, ) assert aioclient_mock.mock_calls[1][2] == expected["request_data"] - - # Unload entry - - await hass.config_entries.async_unload(config_entry.entry_id) - assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE - - # Remove entry - - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index b50032d9c9f..e6ae85df615 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -10,7 +10,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY -from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util @@ -639,17 +639,6 @@ async def test_sensors( await sensor_ws_data(expected["websocket_event"]) assert hass.states.get(expected["entity_id"]).state == expected["next_state"] - # Unload entry - - await hass.config_entries.async_unload(config_entry.entry_id) - assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE - - # Remove entry - - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - @pytest.mark.parametrize( "sensor_payload", diff --git a/tests/components/deconz/test_siren.py b/tests/components/deconz/test_siren.py index 488f12cd65d..17d91a97d28 100644 --- a/tests/components/deconz/test_siren.py +++ b/tests/components/deconz/test_siren.py @@ -11,7 +11,6 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_OFF, STATE_ON, - STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant @@ -78,14 +77,3 @@ async def test_sirens( blocking=True, ) assert aioclient_mock.mock_calls[3][2] == {"alert": "lselect", "ontime": 100} - - await hass.config_entries.async_unload(config_entry_setup.entry_id) - - states = hass.states.async_all() - assert len(states) == 1 - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry_setup.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 60731162e35..c1a047c73c4 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -11,7 +11,7 @@ from homeassistant.components.switch import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -92,17 +92,6 @@ async def test_power_plugs( ) assert aioclient_mock.mock_calls[2][2] == {"on": False} - await hass.config_entries.async_unload(config_entry_setup.entry_id) - - states = hass.states.async_all() - assert len(states) == 4 - for state in states: - assert state.state == STATE_UNAVAILABLE - - await hass.config_entries.async_remove(config_entry_setup.entry_id) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - @pytest.mark.parametrize( "light_payload", From ecadf6a330a43243821032248c00fa728ea54c46 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Fri, 26 Jul 2024 09:21:39 +0100 Subject: [PATCH 1588/2411] Log line wrap in Mealie integration (#122635) Log line wrap --- homeassistant/components/mealie/__init__.py | 3 ++- tests/components/mealie/test_init.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mealie/__init__.py b/homeassistant/components/mealie/__init__.py index df9faf6e540..5c9c91729c0 100644 --- a/homeassistant/components/mealie/__init__.py +++ b/homeassistant/components/mealie/__init__.py @@ -57,7 +57,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo if not version.valid: LOGGER.warning( - "It seems like you are using the nightly version of Mealie, nightly versions could have changes that stop this integration working" + "It seems like you are using the nightly version of Mealie, nightly" + " versions could have changes that stop this integration working" ) if version.valid and version < MIN_REQUIRED_MEALIE_VERSION: raise ConfigEntryError( diff --git a/tests/components/mealie/test_init.py b/tests/components/mealie/test_init.py index 5c25af5a0a0..a45a67801df 100644 --- a/tests/components/mealie/test_init.py +++ b/tests/components/mealie/test_init.py @@ -98,8 +98,8 @@ async def test_setup_invalid( await setup_integration(hass, mock_config_entry) assert ( - "It seems like you are using the nightly version of Mealie, nightly versions could have changes that stop this integration working" - in caplog.text + "It seems like you are using the nightly version of Mealie, nightly" + " versions could have changes that stop this integration working" in caplog.text ) assert mock_config_entry.state is ConfigEntryState.LOADED From 047100069b8fa25277fd398b98a21ff44cfc8216 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 26 Jul 2024 11:21:48 +0200 Subject: [PATCH 1589/2411] Clean up some fixtures not referenced within deCONZ tests (#122637) --- tests/components/deconz/test_lock.py | 5 ++--- tests/components/deconz/test_siren.py | 3 +-- tests/components/deconz/test_switch.py | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index 467b96f200e..28d60e403ef 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -14,7 +14,6 @@ from homeassistant.core import HomeAssistant from .conftest import WebsocketDataType -from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -36,9 +35,9 @@ from tests.test_util.aiohttp import AiohttpClientMocker } ], ) +@pytest.mark.usefixtures("config_entry_setup") async def test_lock_from_light( hass: HomeAssistant, - config_entry_setup: MockConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], light_ws_data: WebsocketDataType, ) -> None: @@ -100,9 +99,9 @@ async def test_lock_from_light( } ], ) +@pytest.mark.usefixtures("config_entry_setup") async def test_lock_from_sensor( hass: HomeAssistant, - config_entry_setup: MockConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], sensor_ws_data: WebsocketDataType, ) -> None: diff --git a/tests/components/deconz/test_siren.py b/tests/components/deconz/test_siren.py index 17d91a97d28..5c80feef38c 100644 --- a/tests/components/deconz/test_siren.py +++ b/tests/components/deconz/test_siren.py @@ -16,7 +16,6 @@ from homeassistant.core import HomeAssistant from .conftest import WebsocketDataType -from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -31,9 +30,9 @@ from tests.test_util.aiohttp import AiohttpClientMocker } ], ) +@pytest.mark.usefixtures("config_entry_setup") async def test_sirens( hass: HomeAssistant, - config_entry_setup: MockConfigEntry, light_ws_data: WebsocketDataType, mock_put_request: Callable[[str, str], AiohttpClientMocker], ) -> None: diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index c1a047c73c4..ed82b0c2ac3 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -17,7 +17,6 @@ from homeassistant.helpers import entity_registry as er from .conftest import ConfigEntryFactoryType, WebsocketDataType -from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -52,9 +51,9 @@ from tests.test_util.aiohttp import AiohttpClientMocker } ], ) +@pytest.mark.usefixtures("config_entry_setup") async def test_power_plugs( hass: HomeAssistant, - config_entry_setup: MockConfigEntry, mock_put_request: Callable[[str, str], AiohttpClientMocker], light_ws_data: WebsocketDataType, ) -> None: From 33ea67e1d0d65d5a1a4b97dbdb8972dbc2cb7def Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 26 Jul 2024 12:29:21 +0200 Subject: [PATCH 1590/2411] Remove last references to hass.data[UNIFI_DOMAIN] (#122642) --- tests/components/unifi/test_init.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index fe464989af3..de08ba2c6d7 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -13,7 +13,6 @@ from homeassistant.components.unifi.const import ( CONF_ALLOW_UPTIME_SENSORS, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, - DOMAIN as UNIFI_DOMAIN, ) from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -27,12 +26,6 @@ from tests.common import flush_store from tests.typing import WebSocketGenerator -async def test_setup_with_no_config(hass: HomeAssistant) -> None: - """Test that we do not discover anything or try to set up a hub.""" - assert await async_setup_component(hass, UNIFI_DOMAIN, {}) is True - assert UNIFI_DOMAIN not in hass.data - - async def test_setup_entry_fails_config_entry_not_ready( config_entry_factory: Callable[[], ConfigEntry], ) -> None: From 72fdcd1cb137ce44333ef5a242b659d56024c6cd Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 26 Jul 2024 12:29:47 +0200 Subject: [PATCH 1591/2411] Final steps to runtime_data in Axis integration (#122641) * Rework connection error test to check config entry status * Remove final dependencies to hass.data[AXIS_DOMAIN] --- homeassistant/components/axis/__init__.py | 4 +- tests/components/axis/test_camera.py | 18 +---- tests/components/axis/test_hub.py | 84 +++++++---------------- 3 files changed, 28 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 94752182d10..f1d8d1d4b63 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -7,7 +7,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from .const import DOMAIN as AXIS_DOMAIN, PLATFORMS +from .const import PLATFORMS from .errors import AuthenticationRequired, CannotConnect from .hub import AxisHub, get_axis_api @@ -18,8 +18,6 @@ type AxisConfigEntry = ConfigEntry[AxisHub] async def async_setup_entry(hass: HomeAssistant, config_entry: AxisConfigEntry) -> bool: """Set up the Axis integration.""" - hass.data.setdefault(AXIS_DOMAIN, {}) - try: api = await get_axis_api(hass, config_entry.data) except CannotConnect as err: diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index c1590717983..00fe4391b0c 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -3,30 +3,14 @@ import pytest from homeassistant.components import camera -from homeassistant.components.axis.const import ( - CONF_STREAM_PROFILE, - DOMAIN as AXIS_DOMAIN, -) +from homeassistant.components.axis.const import CONF_STREAM_PROFILE from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.const import STATE_IDLE from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .const import MAC, NAME -async def test_platform_manually_configured(hass: HomeAssistant) -> None: - """Test that nothing happens when platform is manually configured.""" - assert ( - await async_setup_component( - hass, CAMERA_DOMAIN, {CAMERA_DOMAIN: {"platform": AXIS_DOMAIN}} - ) - is True - ) - - assert AXIS_DOMAIN not in hass.data - - @pytest.mark.usefixtures("config_entry_setup") async def test_camera(hass: HomeAssistant) -> None: """Test that Axis camera platform is loaded properly.""" diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index d7f303539e4..d0911ed6adb 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -14,7 +14,7 @@ from syrupy import SnapshotAssertion from homeassistant.components import axis, zeroconf from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.config_entries import SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigEntryState from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -146,18 +146,6 @@ async def test_device_unavailable( assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state == STATE_OFF -@pytest.mark.usefixtures("mock_default_requests") -async def test_device_not_accessible( - hass: HomeAssistant, config_entry: MockConfigEntry -) -> None: - """Failed setup schedules a retry of setup.""" - config_entry.add_to_hass(hass) - with patch.object(axis, "get_axis_api", side_effect=axis.errors.CannotConnect): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert hass.data[AXIS_DOMAIN] == {} - - @pytest.mark.usefixtures("mock_default_requests") async def test_device_trigger_reauth_flow( hass: HomeAssistant, config_entry: MockConfigEntry @@ -173,19 +161,7 @@ async def test_device_trigger_reauth_flow( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() mock_flow_init.assert_called_once() - assert hass.data[AXIS_DOMAIN] == {} - - -@pytest.mark.usefixtures("mock_default_requests") -async def test_device_unknown_error( - hass: HomeAssistant, config_entry: MockConfigEntry -) -> None: - """Unknown errors are handled.""" - config_entry.add_to_hass(hass) - with patch.object(axis, "get_axis_api", side_effect=Exception): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert hass.data[AXIS_DOMAIN] == {} + assert config_entry.state == ConfigEntryState.SETUP_ERROR async def test_shutdown(config_entry_data: MappingProxyType[str, Any]) -> None: @@ -203,36 +179,28 @@ async def test_shutdown(config_entry_data: MappingProxyType[str, Any]) -> None: assert len(axis_device.api.stream.stop.mock_calls) == 1 -async def test_get_device_fails( - hass: HomeAssistant, config_entry_data: MappingProxyType[str, Any] +@pytest.mark.parametrize( + ("side_effect", "state"), + [ + # Device unauthorized yields authentication required error + (axislib.Unauthorized, ConfigEntryState.SETUP_ERROR), + # Device unavailable yields cannot connect error + (TimeoutError, ConfigEntryState.SETUP_RETRY), + (axislib.RequestError, ConfigEntryState.SETUP_RETRY), + # Device yield unknown error + (axislib.AxisException, ConfigEntryState.SETUP_ERROR), + ], +) +@pytest.mark.usefixtures("mock_default_requests") +async def test_get_axis_api_errors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + side_effect: Exception, + state: ConfigEntryState, ) -> None: - """Device unauthorized yields authentication required error.""" - with ( - patch( - "axis.interfaces.vapix.Vapix.initialize", side_effect=axislib.Unauthorized - ), - pytest.raises(axis.errors.AuthenticationRequired), - ): - await axis.hub.get_axis_api(hass, config_entry_data) - - -async def test_get_device_device_unavailable( - hass: HomeAssistant, config_entry_data: MappingProxyType[str, Any] -) -> None: - """Device unavailable yields cannot connect error.""" - with ( - patch("axis.interfaces.vapix.Vapix.request", side_effect=axislib.RequestError), - pytest.raises(axis.errors.CannotConnect), - ): - await axis.hub.get_axis_api(hass, config_entry_data) - - -async def test_get_device_unknown_error( - hass: HomeAssistant, config_entry_data: MappingProxyType[str, Any] -) -> None: - """Device yield unknown error.""" - with ( - patch("axis.interfaces.vapix.Vapix.request", side_effect=axislib.AxisException), - pytest.raises(axis.errors.AuthenticationRequired), - ): - await axis.hub.get_axis_api(hass, config_entry_data) + """Failed setup schedules a retry of setup.""" + config_entry.add_to_hass(hass) + with patch("axis.interfaces.vapix.Vapix.initialize", side_effect=side_effect): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == state From 850703824b8bf8a74abb89e29e0e58465775b6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 26 Jul 2024 14:15:48 +0200 Subject: [PATCH 1592/2411] Update aioairzone-cloud to v0.6.0 (#122647) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- .../components/airzone_cloud/entity.py | 12 ++++++ .../components/airzone_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../snapshots/test_diagnostics.ambr | 11 ++++++ .../airzone_cloud/test_config_flow.py | 9 +++++ .../airzone_cloud/test_coordinator.py | 7 ++++ tests/components/airzone_cloud/util.py | 38 ++++++++++++++++++- 8 files changed, 79 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airzone_cloud/entity.py b/homeassistant/components/airzone_cloud/entity.py index 8e8a7aff1bc..b8ab464d20c 100644 --- a/homeassistant/components/airzone_cloud/entity.py +++ b/homeassistant/components/airzone_cloud/entity.py @@ -13,9 +13,12 @@ from aioairzone_cloud.const import ( AZD_GROUPS, AZD_HOT_WATERS, AZD_INSTALLATIONS, + AZD_MODEL, AZD_NAME, AZD_SYSTEM_ID, AZD_SYSTEMS, + AZD_THERMOSTAT_FW, + AZD_THERMOSTAT_MODEL, AZD_WEBSERVER, AZD_WEBSERVERS, AZD_ZONES, @@ -69,6 +72,7 @@ class AirzoneAidooEntity(AirzoneEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, aidoo_id)}, manufacturer=MANUFACTURER, + model=aidoo_data[AZD_MODEL], name=aidoo_data[AZD_NAME], via_device=(DOMAIN, aidoo_data[AZD_WEBSERVER]), ) @@ -111,6 +115,7 @@ class AirzoneGroupEntity(AirzoneEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, group_id)}, + model="Group", manufacturer=MANUFACTURER, name=group_data[AZD_NAME], ) @@ -154,6 +159,7 @@ class AirzoneHotWaterEntity(AirzoneEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, dhw_id)}, manufacturer=MANUFACTURER, + model="Hot Water", name=dhw_data[AZD_NAME], via_device=(DOMAIN, dhw_data[AZD_WEBSERVER]), ) @@ -195,6 +201,7 @@ class AirzoneInstallationEntity(AirzoneEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, inst_id)}, manufacturer=MANUFACTURER, + model="Installation", name=inst_data[AZD_NAME], ) @@ -240,9 +247,11 @@ class AirzoneSystemEntity(AirzoneEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, system_id)}, + model=system_data.get(AZD_MODEL), manufacturer=MANUFACTURER, name=system_data[AZD_NAME], via_device=(DOMAIN, system_data[AZD_WEBSERVER]), + sw_version=system_data.get(AZD_FIRMWARE), ) def get_airzone_value(self, key: str) -> Any: @@ -270,6 +279,7 @@ class AirzoneWebServerEntity(AirzoneEntity): self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, ws_id)}, identifiers={(DOMAIN, ws_id)}, + model="WebServer", manufacturer=MANUFACTURER, name=ws_data[AZD_NAME], sw_version=ws_data[AZD_FIRMWARE], @@ -300,9 +310,11 @@ class AirzoneZoneEntity(AirzoneEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, zone_id)}, + model=zone_data.get(AZD_THERMOSTAT_MODEL), manufacturer=MANUFACTURER, name=zone_data[AZD_NAME], via_device=(DOMAIN, self.system_id), + sw_version=zone_data.get(AZD_THERMOSTAT_FW), ) def get_airzone_value(self, key: str) -> Any: diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index 38d50cb02c8..a47aeb6c886 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_push", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.5.5"] + "requirements": ["aioairzone-cloud==0.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 80c3834db5a..d2e62bb9dc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -176,7 +176,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.5.5 +aioairzone-cloud==0.6.0 # homeassistant.components.airzone aioairzone==0.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 79aa747b117..762353676a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.5.5 +aioairzone-cloud==0.6.0 # homeassistant.components.airzone aioairzone==0.8.1 diff --git a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr index 31065d68a47..004769a55cb 100644 --- a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr @@ -114,6 +114,7 @@ 'installation': 'installation1', 'is-connected': True, 'mode': 3, + 'model': 'Aidoo', 'modes': list([ 1, 2, @@ -156,6 +157,7 @@ 'installation': 'installation1', 'is-connected': True, 'mode': 2, + 'model': 'Aidoo Pro', 'modes': list([ 1, 2, @@ -345,6 +347,7 @@ 'temperature-setpoint-max': 30.0, 'temperature-setpoint-min': 15.0, 'temperature-step': 0.5, + 'user-access': 'admin', 'web-servers': list([ 'webserver1', 'webserver2', @@ -370,10 +373,12 @@ '_id': 'error-id', }), ]), + 'firmware': '3.35', 'id': 'system1', 'installation': 'installation1', 'is-connected': True, 'mode': 2, + 'model': 'c6', 'modes': list([ 2, 3, @@ -494,6 +499,8 @@ 'temperature-setpoint-stop-air': 24.0, 'temperature-setpoint-vent-air': 24.0, 'temperature-step': 0.5, + 'thermostat-fw': '3.52', + 'thermostat-model': 'blueface', 'web-server': 'webserver1', 'ws-connected': True, 'zone': 1, @@ -557,6 +564,10 @@ 'temperature-setpoint-stop-air': 24.0, 'temperature-setpoint-vent-air': 24.0, 'temperature-step': 0.5, + 'thermostat-battery': 54, + 'thermostat-coverage': 76, + 'thermostat-fw': '3.33', + 'thermostat-model': 'thinkradio', 'web-server': 'webserver1', 'ws-connected': True, 'zone': 2, diff --git a/tests/components/airzone_cloud/test_config_flow.py b/tests/components/airzone_cloud/test_config_flow.py index 86a70ced51a..04e253eb494 100644 --- a/tests/components/airzone_cloud/test_config_flow.py +++ b/tests/components/airzone_cloud/test_config_flow.py @@ -15,6 +15,7 @@ from .util import ( GET_INSTALLATION_MOCK, GET_INSTALLATIONS_MOCK, WS_ID, + mock_get_device_config, mock_get_device_status, mock_get_webserver, ) @@ -28,6 +29,10 @@ async def test_form(hass: HomeAssistant) -> None: "homeassistant.components.airzone_cloud.async_setup_entry", return_value=True, ) as mock_setup_entry, + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_config", + side_effect=mock_get_device_config, + ), patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status", side_effect=mock_get_device_status, @@ -99,6 +104,10 @@ async def test_installations_list_error(hass: HomeAssistant) -> None: "homeassistant.components.airzone_cloud.async_setup_entry", return_value=True, ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_config", + side_effect=mock_get_device_config, + ), patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status", side_effect=mock_get_device_status, diff --git a/tests/components/airzone_cloud/test_coordinator.py b/tests/components/airzone_cloud/test_coordinator.py index b4b7afd6086..e2b80e66672 100644 --- a/tests/components/airzone_cloud/test_coordinator.py +++ b/tests/components/airzone_cloud/test_coordinator.py @@ -14,6 +14,7 @@ from .util import ( CONFIG, GET_INSTALLATION_MOCK, GET_INSTALLATIONS_MOCK, + mock_get_device_config, mock_get_device_status, mock_get_webserver, ) @@ -32,6 +33,10 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) with ( + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_config", + side_effect=mock_get_device_config, + ) as mock_device_config, patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status", side_effect=mock_get_device_status, @@ -56,11 +61,13 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + mock_device_config.assert_called() mock_device_status.assert_called() mock_installation.assert_awaited_once() mock_installations.assert_called_once() mock_webserver.assert_called() + mock_device_config.reset_mock() mock_device_status.reset_mock() mock_installation.reset_mock() mock_installations.reset_mock() diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index 6e7dad707f1..3bc10537907 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -3,8 +3,9 @@ from typing import Any from unittest.mock import patch -from aioairzone_cloud.common import OperationMode +from aioairzone_cloud.common import OperationMode, UserAccessType from aioairzone_cloud.const import ( + API_ACCESS_TYPE, API_ACTIVE, API_AIR_ACTIVE, API_AQ_ACTIVE, @@ -44,6 +45,8 @@ from aioairzone_cloud.const import ( API_POWER, API_POWERFUL_MODE, API_RAD_ACTIVE, + API_RADIO_BATTERY_PERCENT, + API_RADIO_COVERAGE_PERCENT, API_RANGE_MAX_AIR, API_RANGE_MIN_AIR, API_RANGE_SP_MAX_ACS, @@ -79,8 +82,12 @@ from aioairzone_cloud.const import ( API_STAT_SSID, API_STATUS, API_STEP, + API_SYSTEM_FW, API_SYSTEM_NUMBER, + API_SYSTEM_TYPE, API_TANK_TEMP, + API_THERMOSTAT_FW, + API_THERMOSTAT_TYPE, API_TYPE, API_WARNINGS, API_WS_CONNECTED, @@ -184,6 +191,7 @@ GET_INSTALLATIONS_MOCK = { { API_INSTALLATION_ID: CONFIG[CONF_ID], API_NAME: "House", + API_ACCESS_TYPE: UserAccessType.ADMIN, API_WS_IDS: [ WS_ID, WS_ID_AIDOO, @@ -245,6 +253,30 @@ GET_WEBSERVER_MOCK_AIDOO_PRO = { } +def mock_get_device_config(device: Device) -> dict[str, Any]: + """Mock API device config.""" + + if device.get_id() == "system1": + return { + API_SYSTEM_FW: "3.35", + API_SYSTEM_TYPE: "c6", + } + if device.get_id() == "zone1": + return { + API_THERMOSTAT_FW: "3.52", + API_THERMOSTAT_TYPE: "blueface", + } + if device.get_id() == "zone2": + return { + API_THERMOSTAT_FW: "3.33", + API_THERMOSTAT_TYPE: "thinkradio", + API_RADIO_BATTERY_PERCENT: 54, + API_RADIO_COVERAGE_PERCENT: 76, + } + + return {} + + def mock_get_device_status(device: Device) -> dict[str, Any]: """Mock API device status.""" @@ -470,6 +502,10 @@ async def async_init_integration( config_entry.add_to_hass(hass) with ( + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_config", + side_effect=mock_get_device_config, + ), patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status", side_effect=mock_get_device_status, From 5bb6272dfa98c118465a0ea6aa02992d4e8f67ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Jul 2024 09:55:14 -0500 Subject: [PATCH 1593/2411] Add test coverage for doorbird events (#122617) --- homeassistant/components/doorbird/view.py | 11 +----- tests/components/doorbird/__init__.py | 25 ++++++++++++- tests/components/doorbird/conftest.py | 18 ++++++++- .../doorbird/fixtures/favorites.json | 12 ++++++ tests/components/doorbird/test_button.py | 2 +- tests/components/doorbird/test_event.py | 37 +++++++++++++++++++ 6 files changed, 91 insertions(+), 14 deletions(-) create mode 100644 tests/components/doorbird/fixtures/favorites.json create mode 100644 tests/components/doorbird/test_event.py diff --git a/homeassistant/components/doorbird/view.py b/homeassistant/components/doorbird/view.py index 77b84bf4f3b..71e9d33b681 100644 --- a/homeassistant/components/doorbird/view.py +++ b/homeassistant/components/doorbird/view.py @@ -25,19 +25,12 @@ class DoorBirdRequestView(HomeAssistantView): """Respond to requests from the device.""" hass = request.app[KEY_HASS] token: str | None = request.query.get("token") - if ( - token is None - or (door_station := get_door_station_by_token(hass, token)) is None - ): + if not token or not (door_station := get_door_station_by_token(hass, token)): return web.Response( status=HTTPStatus.UNAUTHORIZED, text="Invalid token provided." ) - if door_station: - event_data = door_station.get_event_data(event) - else: - event_data = {} - + event_data = door_station.get_event_data(event) # # This integration uses a multiple different events. # It would be a major breaking change to change this to diff --git a/tests/components/doorbird/__init__.py b/tests/components/doorbird/__init__.py index c342fac20e9..515b9441c1d 100644 --- a/tests/components/doorbird/__init__.py +++ b/tests/components/doorbird/__init__.py @@ -6,7 +6,15 @@ from unittest.mock import AsyncMock, MagicMock, Mock import aiohttp from doorbirdpy import DoorBird, DoorBirdScheduleEntry -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant import config_entries +from homeassistant.components.doorbird.const import API_URL +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_TOKEN, + CONF_USERNAME, +) VALID_CONFIG = { CONF_HOST: "1.2.3.4", @@ -39,6 +47,7 @@ def get_mock_doorbird_api( info: dict[str, Any] | None = None, info_side_effect: Exception | None = None, schedule: list[DoorBirdScheduleEntry] | None = None, + favorites: dict[str, dict[str, Any]] | None = None, favorites_side_effect: Exception | None = None, ) -> DoorBird: """Return a mock DoorBirdAPI object with return values.""" @@ -48,9 +57,10 @@ def get_mock_doorbird_api( ) type(doorbirdapi_mock).favorites = AsyncMock( side_effect=favorites_side_effect, - return_value={"http": {"x": {"value": "http://webhook"}}}, + return_value=favorites, ) type(doorbirdapi_mock).change_favorite = AsyncMock(return_value=True) + type(doorbirdapi_mock).change_schedule = AsyncMock(return_value=(True, 200)) type(doorbirdapi_mock).schedule = AsyncMock(return_value=schedule) type(doorbirdapi_mock).energize_relay = AsyncMock(return_value=True) type(doorbirdapi_mock).turn_light_on = AsyncMock(return_value=True) @@ -59,3 +69,14 @@ def get_mock_doorbird_api( side_effect=mock_unauthorized_exception() ) return doorbirdapi_mock + + +async def mock_webhook_call( + config_entry: config_entries.ConfigEntry, + aiohttp_client: aiohttp.ClientSession, + event: str, +) -> None: + """Mock the webhook call.""" + token = config_entry.data.get(CONF_TOKEN, config_entry.entry_id) + response = await aiohttp_client.get(f"{API_URL}/{event}?token={token}") + response.raise_for_status() diff --git a/tests/components/doorbird/conftest.py b/tests/components/doorbird/conftest.py index f98fcf0eac8..cd3e410624d 100644 --- a/tests/components/doorbird/conftest.py +++ b/tests/components/doorbird/conftest.py @@ -9,7 +9,12 @@ from unittest.mock import MagicMock, patch from doorbirdpy import DoorBird, DoorBirdScheduleEntry import pytest -from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN +from homeassistant.components.doorbird.const import ( + CONF_EVENTS, + DEFAULT_DOORBELL_EVENT, + DEFAULT_MOTION_EVENT, + DOMAIN, +) from homeassistant.core import HomeAssistant from . import VALID_CONFIG, get_mock_doorbird_api @@ -41,6 +46,12 @@ def doorbird_schedule() -> list[DoorBirdScheduleEntry]: ) +@pytest.fixture(scope="session") +def doorbird_favorites() -> dict[str, dict[str, Any]]: + """Return a loaded DoorBird favorites fixture.""" + return load_json_value_fixture("favorites.json", "doorbird") + + @pytest.fixture def doorbird_api( doorbird_info: dict[str, Any], doorbird_schedule: dict[str, Any] @@ -72,6 +83,7 @@ async def doorbird_mocker( hass: HomeAssistant, doorbird_info: dict[str, Any], doorbird_schedule: dict[str, Any], + doorbird_favorites: dict[str, dict[str, Any]], ) -> DoorbirdMockerType: """Create a MockDoorbirdEntry.""" @@ -81,6 +93,7 @@ async def doorbird_mocker( info: dict[str, Any] | None = None, info_side_effect: Exception | None = None, schedule: list[DoorBirdScheduleEntry] | None = None, + favorites: dict[str, dict[str, Any]] | None = None, favorites_side_effect: Exception | None = None, ) -> MockDoorbirdEntry: """Create a MockDoorbirdEntry from defaults or specific values.""" @@ -88,12 +101,13 @@ async def doorbird_mocker( domain=DOMAIN, unique_id="1CCAE3AAAAAA", data=VALID_CONFIG, - options={CONF_EVENTS: ["event1", "event2", "event3"]}, + options={CONF_EVENTS: [DEFAULT_DOORBELL_EVENT, DEFAULT_MOTION_EVENT]}, ) api = api or get_mock_doorbird_api( info=info or doorbird_info, info_side_effect=info_side_effect, schedule=schedule or doorbird_schedule, + favorites=favorites or doorbird_favorites, favorites_side_effect=favorites_side_effect, ) entry.add_to_hass(hass) diff --git a/tests/components/doorbird/fixtures/favorites.json b/tests/components/doorbird/fixtures/favorites.json new file mode 100644 index 00000000000..c56f79c0300 --- /dev/null +++ b/tests/components/doorbird/fixtures/favorites.json @@ -0,0 +1,12 @@ +{ + "http": { + "0": { + "title": "Home Assistant (mydoorbird_doorbell)", + "value": "http://127.0.0.1:8123/api/doorbird/mydoorbird_doorbell?token=01J2F4B97Y7P1SARXEJ6W07EKD" + }, + "1": { + "title": "Home Assistant (mydoorbird_motion)", + "value": "http://127.0.0.1:8123/api/doorbird/mydoorbird_motion?token=01J2F4B97Y7P1SARXEJ6W07EKD" + } + } +} diff --git a/tests/components/doorbird/test_button.py b/tests/components/doorbird/test_button.py index fc10362a077..2131e3d6133 100644 --- a/tests/components/doorbird/test_button.py +++ b/tests/components/doorbird/test_button.py @@ -49,4 +49,4 @@ async def test_reset_favorites_button( DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: reset_entity_id}, blocking=True ) assert hass.states.get(reset_entity_id).state != STATE_UNKNOWN - assert doorbird_entry.api.delete_favorite.call_count == 1 + assert doorbird_entry.api.delete_favorite.call_count == 2 diff --git a/tests/components/doorbird/test_event.py b/tests/components/doorbird/test_event.py new file mode 100644 index 00000000000..11e0f3a306d --- /dev/null +++ b/tests/components/doorbird/test_event.py @@ -0,0 +1,37 @@ +"""Test DoorBird events.""" + +from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from . import mock_webhook_call +from .conftest import DoorbirdMockerType + +from tests.typing import ClientSessionGenerator + + +async def test_doorbell_ring_event( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + doorbird_mocker: DoorbirdMockerType, +) -> None: + """Test a doorbell ring event.""" + doorbird_entry = await doorbird_mocker() + relay_1_entity_id = "event.mydoorbird_doorbell" + assert hass.states.get(relay_1_entity_id).state == STATE_UNKNOWN + client = await hass_client() + await mock_webhook_call(doorbird_entry.entry, client, "mydoorbird_doorbell") + assert hass.states.get(relay_1_entity_id).state != STATE_UNKNOWN + + +async def test_motion_event( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + doorbird_mocker: DoorbirdMockerType, +) -> None: + """Test a doorbell motion event.""" + doorbird_entry = await doorbird_mocker() + relay_1_entity_id = "event.mydoorbird_motion" + assert hass.states.get(relay_1_entity_id).state == STATE_UNKNOWN + client = await hass_client() + await mock_webhook_call(doorbird_entry.entry, client, "mydoorbird_motion") + assert hass.states.get(relay_1_entity_id).state != STATE_UNKNOWN From 55a10828664cf422b5c07cdc260f196b40775402 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 26 Jul 2024 16:59:12 +0200 Subject: [PATCH 1594/2411] Return unknown when data is missing in Trafikverket Weather (#122652) Return unknown when data is missing --- .../trafikverket_weatherstation/sensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 36c6350280e..22661426f00 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -61,7 +61,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="air_temp", translation_key="air_temperature", - value_fn=lambda data: data.air_temp or 0, + value_fn=lambda data: data.air_temp, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -69,7 +69,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="road_temp", translation_key="road_temperature", - value_fn=lambda data: data.road_temp or 0, + value_fn=lambda data: data.road_temp, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -91,7 +91,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="wind_speed", - value_fn=lambda data: data.windforce or 0, + value_fn=lambda data: data.windforce, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, @@ -99,7 +99,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="wind_speed_max", translation_key="wind_speed_max", - value_fn=lambda data: data.windforcemax or 0, + value_fn=lambda data: data.windforcemax, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, device_class=SensorDeviceClass.WIND_SPEED, entity_registry_enabled_default=False, @@ -107,7 +107,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="humidity", - value_fn=lambda data: data.humidity or 0, + value_fn=lambda data: data.humidity, native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, entity_registry_enabled_default=False, @@ -115,7 +115,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="precipitation_amount", - value_fn=lambda data: data.precipitation_amount or 0, + value_fn=lambda data: data.precipitation_amount, native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, state_class=SensorStateClass.MEASUREMENT, @@ -130,7 +130,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="dew_point", translation_key="dew_point", - value_fn=lambda data: data.dew_point or 0, + value_fn=lambda data: data.dew_point, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, From 49e2bfae313d149ddab6f79116b107802e1a2ce9 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 26 Jul 2024 16:59:28 +0200 Subject: [PATCH 1595/2411] Bump bring-api to v0.8.1 (#122653) * Bump bring-api to v0.8.1 * update imports --- homeassistant/components/bring/__init__.py | 4 ++-- homeassistant/components/bring/config_flow.py | 9 ++++++--- homeassistant/components/bring/coordinator.py | 4 ++-- homeassistant/components/bring/manifest.json | 2 +- homeassistant/components/bring/todo.py | 8 ++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 19 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/bring/__init__.py b/homeassistant/components/bring/__init__.py index 30cbbbbbfa0..f55e75c70bf 100644 --- a/homeassistant/components/bring/__init__.py +++ b/homeassistant/components/bring/__init__.py @@ -4,8 +4,8 @@ from __future__ import annotations import logging -from bring_api.bring import Bring -from bring_api.exceptions import ( +from bring_api import ( + Bring, BringAuthException, BringParseException, BringRequestException, diff --git a/homeassistant/components/bring/config_flow.py b/homeassistant/components/bring/config_flow.py index 997342033e4..c675eda3cd2 100644 --- a/homeassistant/components/bring/config_flow.py +++ b/homeassistant/components/bring/config_flow.py @@ -6,9 +6,12 @@ from collections.abc import Mapping import logging from typing import Any -from bring_api.bring import Bring -from bring_api.exceptions import BringAuthException, BringRequestException -from bring_api.types import BringAuthResponse +from bring_api import ( + Bring, + BringAuthException, + BringAuthResponse, + BringRequestException, +) import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult diff --git a/homeassistant/components/bring/coordinator.py b/homeassistant/components/bring/coordinator.py index 222c650e614..439eb552de4 100644 --- a/homeassistant/components/bring/coordinator.py +++ b/homeassistant/components/bring/coordinator.py @@ -5,8 +5,8 @@ from __future__ import annotations from datetime import timedelta import logging -from bring_api.bring import Bring -from bring_api.exceptions import ( +from bring_api import ( + Bring, BringAuthException, BringParseException, BringRequestException, diff --git a/homeassistant/components/bring/manifest.json b/homeassistant/components/bring/manifest.json index 1b781813203..17c742415ff 100644 --- a/homeassistant/components/bring/manifest.json +++ b/homeassistant/components/bring/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bring", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["bring-api==0.7.1"] + "requirements": ["bring-api==0.8.1"] } diff --git a/homeassistant/components/bring/todo.py b/homeassistant/components/bring/todo.py index f3ba70f6cc5..001466bc1fe 100644 --- a/homeassistant/components/bring/todo.py +++ b/homeassistant/components/bring/todo.py @@ -5,8 +5,12 @@ from __future__ import annotations from typing import TYPE_CHECKING import uuid -from bring_api.exceptions import BringRequestException -from bring_api.types import BringItem, BringItemOperation, BringNotificationType +from bring_api import ( + BringItem, + BringItemOperation, + BringNotificationType, + BringRequestException, +) import voluptuous as vol from homeassistant.components.todo import ( diff --git a/requirements_all.txt b/requirements_all.txt index d2e62bb9dc5..3cb7d583e64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -616,7 +616,7 @@ boto3==1.34.131 botocore==1.34.131 # homeassistant.components.bring -bring-api==0.7.1 +bring-api==0.8.1 # homeassistant.components.broadlink broadlink==0.19.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 762353676a1..975f953aa33 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -533,7 +533,7 @@ boschshcpy==0.2.91 botocore==1.34.131 # homeassistant.components.bring -bring-api==0.7.1 +bring-api==0.8.1 # homeassistant.components.broadlink broadlink==0.19.0 From 7820bcf2186c9e9d815c91c724e5187809b64d4b Mon Sep 17 00:00:00 2001 From: David Knowles Date: Fri, 26 Jul 2024 11:25:56 -0400 Subject: [PATCH 1596/2411] Add entity services to the Hydrawise integration (#120883) * Add services to the Hydrawise integration * Add validation of duration ranges * Remove clamping test * Fix duration type in test * Changes requested during review * Add back the HydrawiseZoneBinarySensor class --- .../components/hydrawise/binary_sensor.py | 49 +++++++++- homeassistant/components/hydrawise/const.py | 10 +- homeassistant/components/hydrawise/icons.json | 5 + .../components/hydrawise/services.yaml | 32 +++++++ .../components/hydrawise/strings.json | 26 ++++++ tests/components/hydrawise/test_services.py | 93 +++++++++++++++++++ 6 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/hydrawise/services.yaml create mode 100644 tests/components/hydrawise/test_services.py diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index 52b4c28d718..0e00d237fae 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -4,6 +4,10 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime + +from pydrawise import Zone +import voluptuous as vol from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -12,9 +16,11 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import VolDictType -from .const import DOMAIN +from .const import DOMAIN, SERVICE_RESUME, SERVICE_START_WATERING, SERVICE_SUSPEND from .coordinator import HydrawiseDataUpdateCoordinator from .entity import HydrawiseEntity @@ -61,6 +67,13 @@ ZONE_BINARY_SENSORS: tuple[HydrawiseBinarySensorEntityDescription, ...] = ( ), ) +SCHEMA_START_WATERING: VolDictType = { + vol.Optional("duration"): vol.All(vol.Coerce(int), vol.Range(min=0, max=90)), +} +SCHEMA_SUSPEND: VolDictType = { + vol.Required("until"): cv.datetime, +} + async def async_setup_entry( hass: HomeAssistant, @@ -89,11 +102,19 @@ async def async_setup_entry( if "rain sensor" in sensor.model.name.lower() ) entities.extend( - HydrawiseBinarySensor(coordinator, description, controller, zone_id=zone.id) + HydrawiseZoneBinarySensor( + coordinator, description, controller, zone_id=zone.id + ) for zone in controller.zones for description in ZONE_BINARY_SENSORS ) async_add_entities(entities) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service(SERVICE_RESUME, {}, "resume") + platform.async_register_entity_service( + SERVICE_START_WATERING, SCHEMA_START_WATERING, "start_watering" + ) + platform.async_register_entity_service(SERVICE_SUSPEND, SCHEMA_SUSPEND, "suspend") class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): @@ -111,3 +132,27 @@ class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): if self.entity_description.always_available: return True return super().available + + +class HydrawiseZoneBinarySensor(HydrawiseBinarySensor): + """A binary sensor for a Hydrawise irrigation zone. + + This is only used for irrigation zones, as they have special methods for + service actions that don't apply to other binary sensors. + """ + + zone: Zone + + async def start_watering(self, duration: int | None = None) -> None: + """Start watering in the irrigation zone.""" + await self.coordinator.api.start_zone( + self.zone, custom_run_duration=int((duration or 0) * 60) + ) + + async def suspend(self, until: datetime) -> None: + """Suspend automatic watering in the irrigation zone.""" + await self.coordinator.api.suspend_zone(self.zone, until=until) + + async def resume(self) -> None: + """Resume automatic watering in the irrigation zone.""" + await self.coordinator.api.resume_zone(self.zone) diff --git a/homeassistant/components/hydrawise/const.py b/homeassistant/components/hydrawise/const.py index 08862246613..f731ecf278c 100644 --- a/homeassistant/components/hydrawise/const.py +++ b/homeassistant/components/hydrawise/const.py @@ -5,9 +5,6 @@ import logging LOGGER = logging.getLogger(__package__) -ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60] -CONF_WATERING_TIME = "watering_minutes" - DOMAIN = "hydrawise" DEFAULT_WATERING_TIME = timedelta(minutes=15) @@ -16,3 +13,10 @@ MANUFACTURER = "Hydrawise" SCAN_INTERVAL = timedelta(seconds=30) SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update" + +SERVICE_RESUME = "resume" +SERVICE_START_WATERING = "start_watering" +SERVICE_SUSPEND = "suspend" + +ATTR_DURATION = "duration" +ATTR_UNTIL = "until" diff --git a/homeassistant/components/hydrawise/icons.json b/homeassistant/components/hydrawise/icons.json index 4af4fe75fcc..1d1d349dbf9 100644 --- a/homeassistant/components/hydrawise/icons.json +++ b/homeassistant/components/hydrawise/icons.json @@ -29,5 +29,10 @@ } } } + }, + "services": { + "start_watering": "mdi:sprinkler-variant", + "suspend": "mdi:pause-circle-outline", + "resume": "mdi:play" } } diff --git a/homeassistant/components/hydrawise/services.yaml b/homeassistant/components/hydrawise/services.yaml new file mode 100644 index 00000000000..64c04901816 --- /dev/null +++ b/homeassistant/components/hydrawise/services.yaml @@ -0,0 +1,32 @@ +start_watering: + target: + entity: + integration: hydrawise + domain: binary_sensor + device_class: running + fields: + duration: + required: false + selector: + number: + min: 0 + max: 90 + unit_of_measurement: min + mode: box +suspend: + target: + entity: + integration: hydrawise + domain: binary_sensor + device_class: running + fields: + until: + required: true + selector: + datetime: +resume: + target: + entity: + integration: hydrawise + domain: binary_sensor + device_class: running diff --git a/homeassistant/components/hydrawise/strings.json b/homeassistant/components/hydrawise/strings.json index c455412d1a4..b6df36ad4ff 100644 --- a/homeassistant/components/hydrawise/strings.json +++ b/homeassistant/components/hydrawise/strings.json @@ -57,5 +57,31 @@ "name": "Manual watering" } } + }, + "services": { + "start_watering": { + "name": "Start watering", + "description": "Starts a watering cycle in the selected irrigation zone.", + "fields": { + "duration": { + "name": "Duration", + "description": "Length of time to run the watering cycle. If not specified (or zero), the default watering duration set in the Hydrawise mobile or web app for the irrigation zone will be used." + } + } + }, + "suspend": { + "name": "Suspend automatic watering", + "description": "Suspends an irrigation zone's automatic watering schedule until the given date and time.", + "fields": { + "until": { + "name": "Until", + "description": "Date and time to resume the automated watering schedule." + } + } + }, + "resume": { + "name": "Resume automatic watering", + "description": "Resumes an irrigation zone's automatic watering schedule." + } } } diff --git a/tests/components/hydrawise/test_services.py b/tests/components/hydrawise/test_services.py new file mode 100644 index 00000000000..f61a6786270 --- /dev/null +++ b/tests/components/hydrawise/test_services.py @@ -0,0 +1,93 @@ +"""Test Hydrawise services.""" + +from datetime import datetime +from unittest.mock import AsyncMock + +from pydrawise.schema import Zone + +from homeassistant.components.hydrawise.const import ( + ATTR_DURATION, + ATTR_UNTIL, + DOMAIN, + SERVICE_RESUME, + SERVICE_START_WATERING, + SERVICE_SUSPEND, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_start_watering( + hass: HomeAssistant, + mock_added_config_entry: MockConfigEntry, + mock_pydrawise: AsyncMock, + zones: list[Zone], +) -> None: + """Test that the start_watering service works as intended.""" + await hass.services.async_call( + DOMAIN, + SERVICE_START_WATERING, + { + ATTR_ENTITY_ID: "binary_sensor.zone_one_watering", + ATTR_DURATION: 20, + }, + blocking=True, + ) + mock_pydrawise.start_zone.assert_called_once_with( + zones[0], custom_run_duration=20 * 60 + ) + + +async def test_start_watering_no_duration( + hass: HomeAssistant, + mock_added_config_entry: MockConfigEntry, + mock_pydrawise: AsyncMock, + zones: list[Zone], +) -> None: + """Test that the start_watering service works with no duration specified.""" + await hass.services.async_call( + DOMAIN, + SERVICE_START_WATERING, + {ATTR_ENTITY_ID: "binary_sensor.zone_one_watering"}, + blocking=True, + ) + mock_pydrawise.start_zone.assert_called_once_with(zones[0], custom_run_duration=0) + + +async def test_resume( + hass: HomeAssistant, + mock_added_config_entry: MockConfigEntry, + mock_pydrawise: AsyncMock, + zones: list[Zone], +) -> None: + """Test that the resume service works as intended.""" + await hass.services.async_call( + DOMAIN, + SERVICE_RESUME, + {ATTR_ENTITY_ID: "binary_sensor.zone_one_watering"}, + blocking=True, + ) + mock_pydrawise.resume_zone.assert_called_once_with(zones[0]) + + +async def test_suspend( + hass: HomeAssistant, + mock_added_config_entry: MockConfigEntry, + mock_pydrawise: AsyncMock, + zones: list[Zone], +) -> None: + """Test that the suspend service works as intended.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SUSPEND, + { + ATTR_ENTITY_ID: "binary_sensor.zone_one_watering", + ATTR_UNTIL: datetime(2026, 1, 1, 0, 0, 0), + }, + blocking=True, + ) + mock_pydrawise.suspend_zone.assert_called_once_with( + zones[0], until=datetime(2026, 1, 1, 0, 0, 0) + ) From 53131390ac7640a83c1bea0e9b8acc21b5e90eb8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 26 Jul 2024 19:22:09 +0200 Subject: [PATCH 1597/2411] Use snapshot in UniFi image tests (#122608) * Use snapshot in UniFi image tests * Make Image access_token deterministic --- .../unifi/snapshots/test_image.ambr | 48 ++++++++++++ tests/components/unifi/test_image.py | 77 +++++++++++++++---- 2 files changed, 108 insertions(+), 17 deletions(-) diff --git a/tests/components/unifi/snapshots/test_image.ambr b/tests/components/unifi/snapshots/test_image.ambr index 83d76688ea3..e33ec678217 100644 --- a/tests/components/unifi/snapshots/test_image.ambr +++ b/tests/components/unifi/snapshots/test_image.ambr @@ -1,4 +1,52 @@ # serializer version: 1 +# name: test_entity_and_device_data[site_payload0-wlan_payload0][image.ssid_1_qr_code-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'image', + 'entity_category': , + 'entity_id': 'image.ssid_1_qr_code', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'QR Code', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'qr_code-012345678910111213141516', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0][image.ssid_1_qr_code-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'access_token': '1', + 'entity_picture': '/api/image_proxy/image.ssid_1_qr_code?token=1', + 'friendly_name': 'SSID 1 QR Code', + }), + 'context': , + 'entity_id': 'image.ssid_1_qr_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-01T01:01:00+00:00', + }) +# --- # name: test_wlan_qr_code b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x84\x00\x00\x00\x84\x01\x00\x00\x00\x00y?\xbe\n\x00\x00\x00\xcaIDATx\xda\xedV[\n\xc30\x0c\x13\xbb\x80\xef\x7fK\xdd\xc0\x93\x94\xfd\xac\x1fcL\xfbl(\xc4\x04*\xacG\xdcb/\x8b\xb8O\xdeO\x00\xccP\x95\x8b\xe5\x03\xd7\xf5\xcd\x89pF\xcf\x8c \\48\x08\nS\x948\x03p\xfe\x80C\xa8\x9d\x16\xc7P\xabvJ}\xe2\xd7\x84[\xe5W\xfc7\xbbS\xfd\xde\xcfB\xf115\xa2\xe3%\x99\xad\x93\xa0:\xbf6\xbeS\xec\x1a^\xb4\xed\xfb\xb2\xab\xd1\x99\xc9\xcdAjx\x89\x0e\xc5\xea\xf4T\xf9\xee\xe40m58\xb6<\x1b\xab~\xf4\xban\xd7:\xceu\x9e\x05\xc4I\xa6\xbb\xfb%q<7:\xbf\xa2\x90wo\xf5 None: + """Validate entity and device data with and without admin rights.""" + with patch("homeassistant.components.unifi.PLATFORMS", [Platform.IMAGE]): + config_entry = await config_entry_factory() + if site_payload[0]["role"] == "admin": + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + else: + assert len(hass.states.async_entity_ids(IMAGE_DOMAIN)) == 0 + + @pytest.mark.parametrize("wlan_payload", [[WLAN]]) @pytest.mark.usefixtures("config_entry_setup") async def test_wlan_qr_code( @@ -64,15 +103,12 @@ async def test_wlan_qr_code( hass_client: ClientSessionGenerator, snapshot: SnapshotAssertion, mock_websocket_message, - mock_websocket_state, ) -> None: """Test the update_clients function when no clients are found.""" assert len(hass.states.async_entity_ids(IMAGE_DOMAIN)) == 0 ent_reg_entry = entity_registry.async_get("image.ssid_1_qr_code") - assert ent_reg_entry.unique_id == "qr_code-012345678910111213141516" assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION - assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC # Enable entity entity_registry.async_update_entity( @@ -84,10 +120,6 @@ async def test_wlan_qr_code( ) await hass.async_block_till_done() - # Validate state object - image_state_1 = hass.states.get("image.ssid_1_qr_code") - assert image_state_1.name == "SSID 1 QR Code" - # Validate image client = await hass_client() resp = await client.get("/api/image_proxy/image.ssid_1_qr_code") @@ -96,8 +128,8 @@ async def test_wlan_qr_code( assert body == snapshot # Update state object - same password - no change to state + image_state_1 = hass.states.get("image.ssid_1_qr_code") mock_websocket_message(message=MessageKey.WLAN_CONF_UPDATED, data=WLAN) - await hass.async_block_till_done() image_state_2 = hass.states.get("image.ssid_1_qr_code") assert image_state_1.state == image_state_2.state @@ -105,7 +137,6 @@ async def test_wlan_qr_code( data = deepcopy(WLAN) data["x_passphrase"] = "new password" mock_websocket_message(message=MessageKey.WLAN_CONF_UPDATED, data=data) - await hass.async_block_till_done() image_state_3 = hass.states.get("image.ssid_1_qr_code") assert image_state_1.state != image_state_3.state @@ -116,25 +147,37 @@ async def test_wlan_qr_code( body = await resp.read() assert body == snapshot - # Availability signalling - # Controller disconnects +@pytest.mark.parametrize("wlan_payload", [[WLAN]]) +@pytest.mark.usefixtures("config_entry_setup") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_hub_state_change(hass: HomeAssistant, mock_websocket_state) -> None: + """Verify entities state reflect on hub becoming unavailable.""" + assert hass.states.get("image.ssid_1_qr_code").state != STATE_UNAVAILABLE + + # Controller unavailable await mock_websocket_state.disconnect() assert hass.states.get("image.ssid_1_qr_code").state == STATE_UNAVAILABLE - # Controller reconnects + # Controller available await mock_websocket_state.reconnect() assert hass.states.get("image.ssid_1_qr_code").state != STATE_UNAVAILABLE + +@pytest.mark.parametrize("wlan_payload", [[WLAN]]) +@pytest.mark.usefixtures("config_entry_setup") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_source_availability(hass: HomeAssistant, mock_websocket_message) -> None: + """Verify entities state reflect on source becoming unavailable.""" + assert hass.states.get("image.ssid_1_qr_code").state != STATE_UNAVAILABLE + # WLAN gets disabled wlan_1 = deepcopy(WLAN) wlan_1["enabled"] = False mock_websocket_message(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) - await hass.async_block_till_done() assert hass.states.get("image.ssid_1_qr_code").state == STATE_UNAVAILABLE # WLAN gets re-enabled wlan_1["enabled"] = True mock_websocket_message(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) - await hass.async_block_till_done() assert hass.states.get("image.ssid_1_qr_code").state != STATE_UNAVAILABLE From d3d522c463c479b21e4b95c04c22e4527cd7d4dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 26 Jul 2024 19:28:39 +0200 Subject: [PATCH 1598/2411] Add Airzone Cloud zone thermostat sensors (#122648) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * airzone_cloud: sensor: add zone thermostat sensors Signed-off-by: Álvaro Fernández Rojas * airzone_cloud: sensor: add missing signal percentage icon Signed-off-by: Álvaro Fernández Rojas * airzone_cloud: sensor: add signal percentage translation Signed-off-by: Álvaro Fernández Rojas * airzone_cloud: sensor: disable thermostat_coverage Also add to diagnostics category. Signed-off-by: Álvaro Fernández Rojas * Update homeassistant/components/airzone_cloud/strings.json Co-authored-by: Joost Lekkerkerker --------- Signed-off-by: Álvaro Fernández Rojas Co-authored-by: Joost Lekkerkerker --- .../components/airzone_cloud/icons.json | 9 +++++++++ homeassistant/components/airzone_cloud/sensor.py | 16 ++++++++++++++++ .../components/airzone_cloud/strings.json | 5 +++++ tests/components/airzone_cloud/test_sensor.py | 6 ++++++ 4 files changed, 36 insertions(+) create mode 100644 homeassistant/components/airzone_cloud/icons.json diff --git a/homeassistant/components/airzone_cloud/icons.json b/homeassistant/components/airzone_cloud/icons.json new file mode 100644 index 00000000000..27dbd03349b --- /dev/null +++ b/homeassistant/components/airzone_cloud/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "thermostat_coverage": { + "default": "mdi:signal" + } + } + } +} diff --git a/homeassistant/components/airzone_cloud/sensor.py b/homeassistant/components/airzone_cloud/sensor.py index f5dc2d7f9eb..7eb62fe5d2c 100644 --- a/homeassistant/components/airzone_cloud/sensor.py +++ b/homeassistant/components/airzone_cloud/sensor.py @@ -12,6 +12,8 @@ from aioairzone_cloud.const import ( AZD_AQ_PM_10, AZD_HUMIDITY, AZD_TEMP, + AZD_THERMOSTAT_BATTERY, + AZD_THERMOSTAT_COVERAGE, AZD_WEBSERVERS, AZD_WIFI_RSSI, AZD_ZONES, @@ -98,6 +100,20 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + device_class=SensorDeviceClass.BATTERY, + key=AZD_THERMOSTAT_BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + key=AZD_THERMOSTAT_COVERAGE, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + translation_key="thermostat_coverage", + ), ) diff --git a/homeassistant/components/airzone_cloud/strings.json b/homeassistant/components/airzone_cloud/strings.json index daeb360719b..68f3d0080db 100644 --- a/homeassistant/components/airzone_cloud/strings.json +++ b/homeassistant/components/airzone_cloud/strings.json @@ -37,6 +37,11 @@ "auto": "Auto" } } + }, + "sensor": { + "thermostat_coverage": { + "name": "Signal percentage" + } } } } diff --git a/tests/components/airzone_cloud/test_sensor.py b/tests/components/airzone_cloud/test_sensor.py index 31fe52f3302..d5addfed4a1 100644 --- a/tests/components/airzone_cloud/test_sensor.py +++ b/tests/components/airzone_cloud/test_sensor.py @@ -31,6 +31,9 @@ async def test_airzone_create_sensors(hass: HomeAssistant) -> None: state = hass.states.get("sensor.dormitorio_air_quality_index") assert state.state == "1" + state = hass.states.get("sensor.dormitorio_battery") + assert state.state == "54" + state = hass.states.get("sensor.dormitorio_pm1") assert state.state == "3" @@ -40,6 +43,9 @@ async def test_airzone_create_sensors(hass: HomeAssistant) -> None: state = hass.states.get("sensor.dormitorio_pm10") assert state.state == "3" + state = hass.states.get("sensor.dormitorio_signal_percentage") + assert state.state == "76" + state = hass.states.get("sensor.dormitorio_temperature") assert state.state == "25.0" From 8e578227c34bc7b10bcd18a8ee6ec12f9b2bb83f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Jul 2024 13:04:23 -0500 Subject: [PATCH 1599/2411] Add test coverage for doorbird cameras (#122660) --- tests/components/doorbird/__init__.py | 1 + tests/components/doorbird/test_camera.py | 46 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/components/doorbird/test_camera.py diff --git a/tests/components/doorbird/__init__.py b/tests/components/doorbird/__init__.py index 515b9441c1d..59ce6ecd958 100644 --- a/tests/components/doorbird/__init__.py +++ b/tests/components/doorbird/__init__.py @@ -65,6 +65,7 @@ def get_mock_doorbird_api( type(doorbirdapi_mock).energize_relay = AsyncMock(return_value=True) type(doorbirdapi_mock).turn_light_on = AsyncMock(return_value=True) type(doorbirdapi_mock).delete_favorite = AsyncMock(return_value=True) + type(doorbirdapi_mock).get_image = AsyncMock(return_value=b"image") type(doorbirdapi_mock).doorbell_state = AsyncMock( side_effect=mock_unauthorized_exception() ) diff --git a/tests/components/doorbird/test_camera.py b/tests/components/doorbird/test_camera.py new file mode 100644 index 00000000000..228a6c81daa --- /dev/null +++ b/tests/components/doorbird/test_camera.py @@ -0,0 +1,46 @@ +"""Test DoorBird cameras.""" + +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components.camera import ( + STATE_IDLE, + async_get_image, + async_get_stream_source, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import mock_not_found_exception +from .conftest import DoorbirdMockerType + + +async def test_doorbird_cameras( + hass: HomeAssistant, + doorbird_mocker: DoorbirdMockerType, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the doorbird cameras.""" + doorbird_entry = await doorbird_mocker() + live_camera_entity_id = "camera.mydoorbird_live" + assert hass.states.get(live_camera_entity_id).state == STATE_IDLE + last_motion_camera_entity_id = "camera.mydoorbird_last_motion" + assert hass.states.get(last_motion_camera_entity_id).state == STATE_IDLE + last_ring_camera_entity_id = "camera.mydoorbird_last_ring" + assert hass.states.get(last_ring_camera_entity_id).state == STATE_IDLE + assert await async_get_stream_source(hass, live_camera_entity_id) is not None + api = doorbird_entry.api + api.get_image.side_effect = mock_not_found_exception() + with pytest.raises(HomeAssistantError): + await async_get_image(hass, live_camera_entity_id) + api.get_image.side_effect = TimeoutError() + with pytest.raises(HomeAssistantError): + await async_get_image(hass, live_camera_entity_id) + api.get_image.side_effect = None + assert (await async_get_image(hass, live_camera_entity_id)).content == b"image" + api.get_image.return_value = b"notyet" + # Ensure rate limit works + assert (await async_get_image(hass, live_camera_entity_id)).content == b"image" + + freezer.tick(60) + assert (await async_get_image(hass, live_camera_entity_id)).content == b"notyet" From b2b40d9ed62c8a3c1526524ba0194772d2cb9f51 Mon Sep 17 00:00:00 2001 From: SplicedNZ <5253213+SplicedNZ@users.noreply.github.com> Date: Sat, 27 Jul 2024 06:19:58 +1200 Subject: [PATCH 1600/2411] Bump opower to 6.0.0 (#122658) * Bump opower to 0.6.0 * Bump opower to 0.6.0 --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 28c2e8ba2a8..b869356cdf9 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.5.2"] + "requirements": ["opower==0.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3cb7d583e64..a809da36aad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1505,7 +1505,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.5.2 +opower==0.6.0 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 975f953aa33..ba299504da6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1229,7 +1229,7 @@ openhomedevice==2.2.0 openwebifpy==4.2.5 # homeassistant.components.opower -opower==0.5.2 +opower==0.6.0 # homeassistant.components.oralb oralb-ble==0.17.6 From 57a5c7c8b60334de7da2cd1ff9a71d5b734fda75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 26 Jul 2024 20:41:31 +0200 Subject: [PATCH 1601/2411] Update aioairzone-cloud to v0.6.1 (#122661) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../airzone_cloud/snapshots/test_diagnostics.ambr | 3 +++ tests/components/airzone_cloud/util.py | 10 ++++++++++ 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index a47aeb6c886..362973ae833 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_push", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.6.0"] + "requirements": ["aioairzone-cloud==0.6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a809da36aad..1e5350d5988 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -176,7 +176,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.6.0 +aioairzone-cloud==0.6.1 # homeassistant.components.airzone aioairzone==0.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba299504da6..c3b94163ea8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.6.0 +aioairzone-cloud==0.6.1 # homeassistant.components.airzone aioairzone==0.8.1 diff --git a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr index 004769a55cb..26a606bde42 100644 --- a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr @@ -396,10 +396,12 @@ 'webserver1': dict({ 'available': True, 'connection-date': '2023-05-07T12:55:51.000Z', + 'cpu-usage': 32, 'disconnection-date': '2023-01-01T22:26:55.376Z', 'firmware': '3.44', 'id': 'webserver1', 'installation': 'installation1', + 'memory-free': 42616, 'name': 'WebServer 11:22:33:44:55:66', 'type': 'ws_az', 'wifi-channel': 36, @@ -565,6 +567,7 @@ 'temperature-setpoint-vent-air': 24.0, 'temperature-step': 0.5, 'thermostat-battery': 54, + 'thermostat-battery-low': False, 'thermostat-coverage': 76, 'thermostat-fw': '3.33', 'thermostat-model': 'thinkradio', diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index 3bc10537907..fb538ea7c8e 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -24,12 +24,16 @@ from aioairzone_cloud.const import ( API_CELSIUS, API_CONFIG, API_CONNECTION_DATE, + API_CPU_WS, API_DEVICE_ID, API_DEVICES, API_DISCONNECTION_DATE, API_DOUBLE_SET_POINT, API_ERRORS, API_FAH, + API_FREE, + API_FREE_MEM, + API_GENERAL, API_GROUP_ID, API_GROUPS, API_HUMIDITY, @@ -210,6 +214,12 @@ GET_WEBSERVER_MOCK = { API_STAT_AP_MAC: "00:00:00:00:00:00", }, API_STATUS: { + API_CPU_WS: { + API_GENERAL: 32, + }, + API_FREE_MEM: { + API_FREE: 42616, + }, API_IS_CONNECTED: True, API_STAT_QUALITY: 4, API_STAT_RSSI: -56, From 58419f14e88289da0f98b3ff012b32956d372b88 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 26 Jul 2024 20:58:00 +0200 Subject: [PATCH 1602/2411] Less use of hass.data[DECONZ_DOMAIN] in deCONZ tests (#122657) * Less use of hass.data[DECONZ_DOMAIN] in deCONZ tests * Fix review comment * Change patch path --- tests/components/deconz/test_hub.py | 38 +--------- tests/components/deconz/test_init.py | 100 ++++++++++++++------------- 2 files changed, 54 insertions(+), 84 deletions(-) diff --git a/tests/components/deconz/test_hub.py b/tests/components/deconz/test_hub.py index 9f6c5a8b90f..43c2dccae93 100644 --- a/tests/components/deconz/test_hub.py +++ b/tests/components/deconz/test_hub.py @@ -2,7 +2,6 @@ from unittest.mock import patch -import pydeconz from pydeconz.websocket import State import pytest from syrupy import SnapshotAssertion @@ -10,8 +9,7 @@ from syrupy import SnapshotAssertion from homeassistant.components import ssdp from homeassistant.components.deconz.config_flow import DECONZ_MANUFACTURERURL from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN -from homeassistant.components.deconz.errors import AuthenticationRequired, CannotConnect -from homeassistant.components.deconz.hub import DeconzHub, get_deconz_api +from homeassistant.components.deconz.hub import DeconzHub from homeassistant.components.ssdp import ( ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL, @@ -110,37 +108,3 @@ async def test_reset_after_successful_setup( await hass.async_block_till_done() assert result is True - - -async def test_get_deconz_api( - hass: HomeAssistant, config_entry: MockConfigEntry -) -> None: - """Successful call.""" - with patch("pydeconz.DeconzSession.refresh_state", return_value=True): - assert await get_deconz_api(hass, config_entry) - - -@pytest.mark.parametrize( - ("side_effect", "raised_exception"), - [ - (TimeoutError, CannotConnect), - (pydeconz.RequestError, CannotConnect), - (pydeconz.ResponseError, CannotConnect), - (pydeconz.Unauthorized, AuthenticationRequired), - ], -) -async def test_get_deconz_api_fails( - hass: HomeAssistant, - config_entry: MockConfigEntry, - side_effect: Exception, - raised_exception: Exception, -) -> None: - """Failed call.""" - with ( - patch( - "pydeconz.DeconzSession.refresh_state", - side_effect=side_effect, - ), - pytest.raises(raised_exception), - ): - assert await get_deconz_api(hass, config_entry) diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index e13135850ae..390d8b9b353 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -3,13 +3,15 @@ import asyncio from unittest.mock import patch -from homeassistant.components.deconz import ( - DeconzHub, - async_setup_entry, - async_unload_entry, +import pydeconz +import pytest + +from homeassistant.components.deconz.const import ( + CONF_MASTER_GATEWAY, + DOMAIN as DECONZ_DOMAIN, ) -from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN -from homeassistant.components.deconz.errors import AuthenticationRequired, CannotConnect +from homeassistant.components.deconz.errors import AuthenticationRequired +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from .conftest import ConfigEntryFactoryType @@ -17,35 +19,38 @@ from .conftest import ConfigEntryFactoryType from tests.common import MockConfigEntry -async def setup_entry(hass: HomeAssistant, entry: MockConfigEntry) -> None: - """Test that setup entry works.""" - with ( - patch.object(DeconzHub, "async_setup", return_value=True), - patch.object(DeconzHub, "async_update_device_registry", return_value=True), - ): - assert await async_setup_entry(hass, entry) is True +async def test_setup_entry(config_entry_setup: MockConfigEntry) -> None: + """Test successful setup of entry.""" + assert config_entry_setup.state is ConfigEntryState.LOADED + assert config_entry_setup.options[CONF_MASTER_GATEWAY] is True -async def test_setup_entry_successful( - hass: HomeAssistant, config_entry_setup: MockConfigEntry +@pytest.mark.parametrize( + ("side_effect", "state"), + [ + # Failed authentication trigger a reauthentication flow + (pydeconz.Unauthorized, ConfigEntryState.SETUP_ERROR), + # Connection fails + (TimeoutError, ConfigEntryState.SETUP_RETRY), + (pydeconz.RequestError, ConfigEntryState.SETUP_RETRY), + (pydeconz.ResponseError, ConfigEntryState.SETUP_RETRY), + ], +) +async def test_get_deconz_api_fails( + hass: HomeAssistant, + config_entry: MockConfigEntry, + side_effect: Exception, + state: ConfigEntryState, ) -> None: - """Test setup entry is successful.""" - assert hass.data[DECONZ_DOMAIN] - assert config_entry_setup.entry_id in hass.data[DECONZ_DOMAIN] - assert hass.data[DECONZ_DOMAIN][config_entry_setup.entry_id].master - - -async def test_setup_entry_fails_config_entry_not_ready( - hass: HomeAssistant, config_entry_factory: ConfigEntryFactoryType -) -> None: - """Failed authentication trigger a reauthentication flow.""" + """Failed setup.""" + config_entry.add_to_hass(hass) with patch( - "homeassistant.components.deconz.get_deconz_api", - side_effect=CannotConnect, + "homeassistant.components.deconz.hub.api.DeconzSession.refresh_state", + side_effect=side_effect, ): - await config_entry_factory() - - assert hass.data[DECONZ_DOMAIN] == {} + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is state async def test_setup_entry_fails_trigger_reauth_flow( @@ -59,10 +64,9 @@ async def test_setup_entry_fails_trigger_reauth_flow( ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init, ): - await config_entry_factory() + config_entry = await config_entry_factory() mock_flow_init.assert_called_once() - - assert hass.data[DECONZ_DOMAIN] == {} + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_setup_entry_multiple_gateways( @@ -79,19 +83,19 @@ async def test_setup_entry_multiple_gateways( ) config_entry2 = await config_entry_factory(entry2) - assert len(hass.data[DECONZ_DOMAIN]) == 2 - assert hass.data[DECONZ_DOMAIN][config_entry.entry_id].master - assert not hass.data[DECONZ_DOMAIN][config_entry2.entry_id].master + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry2.state is ConfigEntryState.LOADED + assert config_entry.options[CONF_MASTER_GATEWAY] is True + assert config_entry2.options[CONF_MASTER_GATEWAY] is False async def test_unload_entry( hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Test being able to unload an entry.""" - assert hass.data[DECONZ_DOMAIN] - - assert await async_unload_entry(hass, config_entry_setup) - assert not hass.data[DECONZ_DOMAIN] + assert config_entry_setup.state is ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(config_entry_setup.entry_id) + assert config_entry_setup.state is ConfigEntryState.NOT_LOADED async def test_unload_entry_multiple_gateways( @@ -108,12 +112,12 @@ async def test_unload_entry_multiple_gateways( ) config_entry2 = await config_entry_factory(entry2) - assert len(hass.data[DECONZ_DOMAIN]) == 2 + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry2.state is ConfigEntryState.LOADED - assert await async_unload_entry(hass, config_entry) - - assert len(hass.data[DECONZ_DOMAIN]) == 1 - assert hass.data[DECONZ_DOMAIN][config_entry2.entry_id].master + assert await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.NOT_LOADED + assert config_entry2.options[CONF_MASTER_GATEWAY] is True async def test_unload_entry_multiple_gateways_parallel( @@ -130,11 +134,13 @@ async def test_unload_entry_multiple_gateways_parallel( ) config_entry2 = await config_entry_factory(entry2) - assert len(hass.data[DECONZ_DOMAIN]) == 2 + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry2.state is ConfigEntryState.LOADED await asyncio.gather( hass.config_entries.async_unload(config_entry.entry_id), hass.config_entries.async_unload(config_entry2.entry_id), ) - assert len(hass.data[DECONZ_DOMAIN]) == 0 + assert config_entry.state is ConfigEntryState.NOT_LOADED + assert config_entry2.state is ConfigEntryState.NOT_LOADED From 888ffc002f9ce954b87f6a31314cbfb59c6575df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 26 Jul 2024 21:36:21 +0200 Subject: [PATCH 1603/2411] Add Airzone Cloud WebServer CPU/Memory sensors (#122667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit airzone_cloud: sensor: add WebServer CPU/Memory Signed-off-by: Álvaro Fernández Rojas --- .../components/airzone_cloud/icons.json | 6 ++++++ .../components/airzone_cloud/sensor.py | 18 ++++++++++++++++++ .../components/airzone_cloud/strings.json | 6 ++++++ tests/components/airzone_cloud/test_sensor.py | 7 +++++-- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airzone_cloud/icons.json b/homeassistant/components/airzone_cloud/icons.json index 27dbd03349b..31a0a43a4d2 100644 --- a/homeassistant/components/airzone_cloud/icons.json +++ b/homeassistant/components/airzone_cloud/icons.json @@ -1,6 +1,12 @@ { "entity": { "sensor": { + "cpu_usage": { + "default": "mdi:cpu-32-bit" + }, + "free_memory": { + "default": "mdi:memory" + }, "thermostat_coverage": { "default": "mdi:signal" } diff --git a/homeassistant/components/airzone_cloud/sensor.py b/homeassistant/components/airzone_cloud/sensor.py index 7eb62fe5d2c..7946e0d35d0 100644 --- a/homeassistant/components/airzone_cloud/sensor.py +++ b/homeassistant/components/airzone_cloud/sensor.py @@ -10,7 +10,9 @@ from aioairzone_cloud.const import ( AZD_AQ_PM_1, AZD_AQ_PM_2P5, AZD_AQ_PM_10, + AZD_CPU_USAGE, AZD_HUMIDITY, + AZD_MEMORY_FREE, AZD_TEMP, AZD_THERMOSTAT_BATTERY, AZD_THERMOSTAT_COVERAGE, @@ -54,6 +56,22 @@ AIDOO_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( ) WEBSERVER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( + SensorEntityDescription( + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + key=AZD_CPU_USAGE, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + translation_key="cpu_usage", + ), + SensorEntityDescription( + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + key=AZD_MEMORY_FREE, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + translation_key="free_memory", + ), SensorEntityDescription( device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/airzone_cloud/strings.json b/homeassistant/components/airzone_cloud/strings.json index 68f3d0080db..eb9529c7ca5 100644 --- a/homeassistant/components/airzone_cloud/strings.json +++ b/homeassistant/components/airzone_cloud/strings.json @@ -39,6 +39,12 @@ } }, "sensor": { + "cpu_usage": { + "name": "CPU usage" + }, + "free_memory": { + "name": "Free memory" + }, "thermostat_coverage": { "name": "Signal percentage" } diff --git a/tests/components/airzone_cloud/test_sensor.py b/tests/components/airzone_cloud/test_sensor.py index d5addfed4a1..cf291ec23a6 100644 --- a/tests/components/airzone_cloud/test_sensor.py +++ b/tests/components/airzone_cloud/test_sensor.py @@ -21,8 +21,11 @@ async def test_airzone_create_sensors(hass: HomeAssistant) -> None: assert state.state == "20.0" # WebServers - state = hass.states.get("sensor.webserver_11_22_33_44_55_66_signal_strength") - assert state.state == "-56" + state = hass.states.get("sensor.webserver_11_22_33_44_55_66_cpu_usage") + assert state.state == "32" + + state = hass.states.get("sensor.webserver_11_22_33_44_55_66_free_memory") + assert state.state == "42616" state = hass.states.get("sensor.webserver_11_22_33_44_55_67_signal_strength") assert state.state == "-77" From 1a64489121ce55554eaaad51a3166b1031fe536c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 26 Jul 2024 21:36:39 +0200 Subject: [PATCH 1604/2411] Add Airzone Cloud low thermostat battery binary sensor (#122665) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit airzone_cloud: binary_sensor: add low thermostat battery Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone_cloud/binary_sensor.py | 5 +++++ tests/components/airzone_cloud/test_binary_sensor.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/airzone_cloud/binary_sensor.py b/homeassistant/components/airzone_cloud/binary_sensor.py index 3013a2eeadc..f22515155f1 100644 --- a/homeassistant/components/airzone_cloud/binary_sensor.py +++ b/homeassistant/components/airzone_cloud/binary_sensor.py @@ -14,6 +14,7 @@ from aioairzone_cloud.const import ( AZD_FLOOR_DEMAND, AZD_PROBLEMS, AZD_SYSTEMS, + AZD_THERMOSTAT_BATTERY_LOW, AZD_WARNINGS, AZD_ZONES, ) @@ -88,6 +89,10 @@ ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...] key=AZD_AQ_ACTIVE, translation_key="air_quality_active", ), + AirzoneBinarySensorEntityDescription( + device_class=BinarySensorDeviceClass.BATTERY, + key=AZD_THERMOSTAT_BATTERY_LOW, + ), AirzoneBinarySensorEntityDescription( device_class=BinarySensorDeviceClass.RUNNING, key=AZD_FLOOR_DEMAND, diff --git a/tests/components/airzone_cloud/test_binary_sensor.py b/tests/components/airzone_cloud/test_binary_sensor.py index 8e065821057..bb2d0f78060 100644 --- a/tests/components/airzone_cloud/test_binary_sensor.py +++ b/tests/components/airzone_cloud/test_binary_sensor.py @@ -47,6 +47,9 @@ async def test_airzone_create_binary_sensors(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.dormitorio_air_quality_active") assert state.state == STATE_OFF + state = hass.states.get("binary_sensor.dormitorio_battery") + assert state.state == STATE_OFF + state = hass.states.get("binary_sensor.dormitorio_floor_demand") assert state.state == STATE_OFF From c9eb1a2e9c3c2b065614c152df2fa4d32ca40eaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 26 Jul 2024 21:59:16 +0200 Subject: [PATCH 1605/2411] Fix Airzone Cloud WebServer memory usage unit (#122670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit airzone_cloud: sensor: fix webserver memory usage unit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone_cloud/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/airzone_cloud/sensor.py b/homeassistant/components/airzone_cloud/sensor.py index 7946e0d35d0..a3a456edd03 100644 --- a/homeassistant/components/airzone_cloud/sensor.py +++ b/homeassistant/components/airzone_cloud/sensor.py @@ -32,6 +32,7 @@ from homeassistant.const import ( PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, EntityCategory, + UnitOfInformation, UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback @@ -68,7 +69,7 @@ WEBSERVER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, key=AZD_MEMORY_FREE, - native_unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=UnitOfInformation.BYTES, state_class=SensorStateClass.MEASUREMENT, translation_key="free_memory", ), From 57554aba571acaed5907560c6c621dc59a033ccd Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 26 Jul 2024 21:28:58 +0100 Subject: [PATCH 1606/2411] Fix broken token caching for evohome (#122664) * bugfix token caching --- homeassistant/components/evohome/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 2a9a44de717..2df4ae1be6b 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -141,9 +141,9 @@ class EvoSession: client_v2._user_account = None # noqa: SLF001 await client_v2.login() - await self.save_auth_tokens() + self.client_v2 = client_v2 # only set attr if authentication succeeded - self.client_v2 = client_v2 + await self.save_auth_tokens() self.client_v1 = ev1.EvohomeClient( username, From c486baccaa043ef0bba5fb62651fd173f82791be Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 26 Jul 2024 23:33:37 +0200 Subject: [PATCH 1607/2411] Patch import where its used in Axis hub test (#122674) --- tests/components/axis/test_hub.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index d0911ed6adb..74cdb0164cd 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -200,7 +200,10 @@ async def test_get_axis_api_errors( ) -> None: """Failed setup schedules a retry of setup.""" config_entry.add_to_hass(hass) - with patch("axis.interfaces.vapix.Vapix.initialize", side_effect=side_effect): + with patch( + "homeassistant.components.axis.hub.api.axis.interfaces.vapix.Vapix.initialize", + side_effect=side_effect, + ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state == state From 84486bad788e26aad507b1992f76cae5a1084af0 Mon Sep 17 00:00:00 2001 From: "Phill (pssc)" Date: Fri, 26 Jul 2024 22:36:34 +0100 Subject: [PATCH 1608/2411] Yamaha device setup enhancement with unique id based on serial (#120764) * fix server unavailale at HA startup Fixes #111108 Remove receiver zone confusion for mediaplayer instances fix uniq id based on serial where avaialble get serial suppiled by discovery for config entries. * Fix linter errors * ruff format * Enhance debug to find setup code path for tests * Enhance debug to find setup code path for tests * Fix formatting * Revered uid chanages as not needed yet and cuases other issues * Revert "Fix formatting" This reverts commit f3324868d25261a1466233eeb804f526a0023ca1. * Fix formatting * Refector tests to cope with changes to plaform init to get serial numbers * Update test patch * Update test formatting * remove all fixes revert code to only make clear we deal with zones and improve debuging --- homeassistant/components/yamaha/const.py | 1 + .../components/yamaha/media_player.py | 133 +++++++++++------- tests/components/yamaha/test_media_player.py | 5 +- 3 files changed, 86 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/yamaha/const.py b/homeassistant/components/yamaha/const.py index c0f4e34dd50..492babe9657 100644 --- a/homeassistant/components/yamaha/const.py +++ b/homeassistant/components/yamaha/const.py @@ -1,6 +1,7 @@ """Constants for the Yamaha component.""" DOMAIN = "yamaha" +KNOWN_ZONES = "known_zones" CURSOR_TYPE_DOWN = "down" CURSOR_TYPE_LEFT = "left" CURSOR_TYPE_RETURN = "return" diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 1be7cb03e17..48dbcfffc97 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -29,6 +29,8 @@ from .const import ( CURSOR_TYPE_RIGHT, CURSOR_TYPE_SELECT, CURSOR_TYPE_UP, + DOMAIN, + KNOWN_ZONES, SERVICE_ENABLE_OUTPUT, SERVICE_MENU_CURSOR, SERVICE_SELECT_SCENE, @@ -55,7 +57,6 @@ CURSOR_TYPE_MAP = { CURSOR_TYPE_SELECT: rxv.RXV.menu_sel.__name__, CURSOR_TYPE_UP: rxv.RXV.menu_up.__name__, } -DATA_YAMAHA = "yamaha_known_receivers" DEFAULT_NAME = "Yamaha Receiver" SUPPORT_YAMAHA = ( @@ -99,6 +100,7 @@ class YamahaConfigInfo: self.zone_ignore = config.get(CONF_ZONE_IGNORE) self.zone_names = config.get(CONF_ZONE_NAMES) self.from_discovery = False + _LOGGER.debug("Discovery Info: %s", discovery_info) if discovery_info is not None: self.name = discovery_info.get("name") self.model = discovery_info.get("model_name") @@ -109,23 +111,26 @@ class YamahaConfigInfo: def _discovery(config_info): - """Discover receivers from configuration in the network.""" + """Discover list of zone controllers from configuration in the network.""" if config_info.from_discovery: - receivers = rxv.RXV( + _LOGGER.debug("Discovery Zones") + zones = rxv.RXV( config_info.ctrl_url, model_name=config_info.model, friendly_name=config_info.name, unit_desc_url=config_info.desc_url, ).zone_controllers() - _LOGGER.debug("Receivers: %s", receivers) elif config_info.host is None: - receivers = [] + _LOGGER.debug("Config No Host Supplied Zones") + zones = [] for recv in rxv.find(): - receivers.extend(recv.zone_controllers()) + zones.extend(recv.zone_controllers()) else: - receivers = rxv.RXV(config_info.ctrl_url, config_info.name).zone_controllers() + _LOGGER.debug("Config Zones Fallback") + zones = rxv.RXV(config_info.ctrl_url, config_info.name).zone_controllers() - return receivers + _LOGGER.debug("Returned _discover zones: %s", zones) + return zones async def async_setup_platform( @@ -138,21 +143,24 @@ async def async_setup_platform( # Keep track of configured receivers so that we don't end up # discovering a receiver dynamically that we have static config # for. Map each device from its zone_id . - known_zones = hass.data.setdefault(DATA_YAMAHA, set()) + known_zones = hass.data.setdefault(DOMAIN, {KNOWN_ZONES: set()})[KNOWN_ZONES] + _LOGGER.debug("Known receiver zones: %s", known_zones) # Get the Infos for configuration from config (YAML) or Discovery config_info = YamahaConfigInfo(config=config, discovery_info=discovery_info) # Async check if the Receivers are there in the network - receivers = await hass.async_add_executor_job(_discovery, config_info) + zone_ctrls = await hass.async_add_executor_job(_discovery, config_info) entities = [] - for receiver in receivers: - if config_info.zone_ignore and receiver.zone in config_info.zone_ignore: + for zctrl in zone_ctrls: + _LOGGER.info("Receiver zone: %s", zctrl.zone) + if config_info.zone_ignore and zctrl.zone in config_info.zone_ignore: + _LOGGER.debug("Ignore receiver zone: %s %s", config_info.name, zctrl.zone) continue - entity = YamahaDevice( + entity = YamahaDeviceZone( config_info.name, - receiver, + zctrl, config_info.source_ignore, config_info.source_names, config_info.zone_names, @@ -163,7 +171,9 @@ async def async_setup_platform( known_zones.add(entity.zone_id) entities.append(entity) else: - _LOGGER.debug("Ignoring duplicate receiver: %s", config_info.name) + _LOGGER.debug( + "Ignoring duplicate zone: %s %s", config_info.name, zctrl.zone + ) async_add_entities(entities) @@ -184,16 +194,16 @@ async def async_setup_platform( platform.async_register_entity_service( SERVICE_MENU_CURSOR, {vol.Required(ATTR_CURSOR): vol.In(CURSOR_TYPE_MAP)}, - YamahaDevice.menu_cursor.__name__, + YamahaDeviceZone.menu_cursor.__name__, ) -class YamahaDevice(MediaPlayerEntity): - """Representation of a Yamaha device.""" +class YamahaDeviceZone(MediaPlayerEntity): + """Representation of a Yamaha device zone.""" - def __init__(self, name, receiver, source_ignore, source_names, zone_names): + def __init__(self, name, zctrl, source_ignore, source_names, zone_names): """Initialize the Yamaha Receiver.""" - self.receiver = receiver + self.zctrl = zctrl self._attr_is_volume_muted = False self._attr_volume_level = 0 self._attr_state = MediaPlayerState.OFF @@ -205,24 +215,38 @@ class YamahaDevice(MediaPlayerEntity): self._is_playback_supported = False self._play_status = None self._name = name - self._zone = receiver.zone - if self.receiver.serial_number is not None: + self._zone = zctrl.zone + if self.zctrl.serial_number is not None: # Since not all receivers will have a serial number and set a unique id # the default name of the integration may not be changed # to avoid a breaking change. - self._attr_unique_id = f"{self.receiver.serial_number}_{self._zone}" + # Prefix as MusicCast could have used this + self._attr_unique_id = f"{self.zctrl.serial_number}_{self._zone}" + _LOGGER.debug( + "Receiver zone: %s zone %s uid %s", + self._name, + self._zone, + self._attr_unique_id, + ) + else: + _LOGGER.info( + "Receiver zone: %s zone %s no uid %s", + self._name, + self._zone, + self._attr_unique_id, + ) def update(self) -> None: """Get the latest details from the device.""" try: - self._play_status = self.receiver.play_status() + self._play_status = self.zctrl.play_status() except requests.exceptions.ConnectionError: _LOGGER.info("Receiver is offline: %s", self._name) self._attr_available = False return self._attr_available = True - if self.receiver.on: + if self.zctrl.on: if self._play_status is None: self._attr_state = MediaPlayerState.ON elif self._play_status.playing: @@ -232,21 +256,21 @@ class YamahaDevice(MediaPlayerEntity): else: self._attr_state = MediaPlayerState.OFF - self._attr_is_volume_muted = self.receiver.mute - self._attr_volume_level = (self.receiver.volume / 100) + 1 + self._attr_is_volume_muted = self.zctrl.mute + self._attr_volume_level = (self.zctrl.volume / 100) + 1 if self.source_list is None: self.build_source_list() - current_source = self.receiver.input + current_source = self.zctrl.input self._attr_source = self._source_names.get(current_source, current_source) - self._playback_support = self.receiver.get_playback_support() - self._is_playback_supported = self.receiver.is_playback_supported( + self._playback_support = self.zctrl.get_playback_support() + self._is_playback_supported = self.zctrl.is_playback_supported( self._attr_source ) - surround_programs = self.receiver.surround_programs() + surround_programs = self.zctrl.surround_programs() if surround_programs: - self._attr_sound_mode = self.receiver.surround_program + self._attr_sound_mode = self.zctrl.surround_program self._attr_sound_mode_list = surround_programs else: self._attr_sound_mode = None @@ -260,10 +284,15 @@ class YamahaDevice(MediaPlayerEntity): self._attr_source_list = sorted( self._source_names.get(source, source) - for source in self.receiver.inputs() + for source in self.zctrl.inputs() if source not in self._source_ignore ) + @property + def unique_id(self) -> str: + """Return the unique ID for this media_player.""" + return self._attr_unique_id or "" + @property def name(self): """Return the name of the device.""" @@ -277,7 +306,7 @@ class YamahaDevice(MediaPlayerEntity): @property def zone_id(self): """Return a zone_id to ensure 1 media player per zone.""" - return f"{self.receiver.ctrl_url}:{self._zone}" + return f"{self.zctrl.ctrl_url}:{self._zone}" @property def supported_features(self) -> MediaPlayerEntityFeature: @@ -301,42 +330,42 @@ class YamahaDevice(MediaPlayerEntity): def turn_off(self) -> None: """Turn off media player.""" - self.receiver.on = False + self.zctrl.on = False def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" - receiver_vol = 100 - (volume * 100) - negative_receiver_vol = -receiver_vol - self.receiver.volume = negative_receiver_vol + zone_vol = 100 - (volume * 100) + negative_zone_vol = -zone_vol + self.zctrl.volume = negative_zone_vol def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" - self.receiver.mute = mute + self.zctrl.mute = mute def turn_on(self) -> None: """Turn the media player on.""" - self.receiver.on = True - self._attr_volume_level = (self.receiver.volume / 100) + 1 + self.zctrl.on = True + self._attr_volume_level = (self.zctrl.volume / 100) + 1 def media_play(self) -> None: """Send play command.""" - self._call_playback_function(self.receiver.play, "play") + self._call_playback_function(self.zctrl.play, "play") def media_pause(self) -> None: """Send pause command.""" - self._call_playback_function(self.receiver.pause, "pause") + self._call_playback_function(self.zctrl.pause, "pause") def media_stop(self) -> None: """Send stop command.""" - self._call_playback_function(self.receiver.stop, "stop") + self._call_playback_function(self.zctrl.stop, "stop") def media_previous_track(self) -> None: """Send previous track command.""" - self._call_playback_function(self.receiver.previous, "previous track") + self._call_playback_function(self.zctrl.previous, "previous track") def media_next_track(self) -> None: """Send next track command.""" - self._call_playback_function(self.receiver.next, "next track") + self._call_playback_function(self.zctrl.next, "next track") def _call_playback_function(self, function, function_text): try: @@ -346,7 +375,7 @@ class YamahaDevice(MediaPlayerEntity): def select_source(self, source: str) -> None: """Select input source.""" - self.receiver.input = self._reverse_mapping.get(source, source) + self.zctrl.input = self._reverse_mapping.get(source, source) def play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any @@ -370,26 +399,26 @@ class YamahaDevice(MediaPlayerEntity): menu must be fetched by the receiver from the vtuner service. """ if media_type == "NET RADIO": - self.receiver.net_radio(media_id) + self.zctrl.net_radio(media_id) def enable_output(self, port, enabled): """Enable or disable an output port..""" - self.receiver.enable_output(port, enabled) + self.zctrl.enable_output(port, enabled) def menu_cursor(self, cursor): """Press a menu cursor button.""" - getattr(self.receiver, CURSOR_TYPE_MAP[cursor])() + getattr(self.zctrl, CURSOR_TYPE_MAP[cursor])() def set_scene(self, scene): """Set the current scene.""" try: - self.receiver.scene = scene + self.zctrl.scene = scene except AssertionError: _LOGGER.warning("Scene '%s' does not exist!", scene) def select_sound_mode(self, sound_mode: str) -> None: """Set Sound Mode for Receiver..""" - self.receiver.surround_program = sound_mode + self.zctrl.surround_program = sound_mode @property def media_artist(self): diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 02246e69269..66d0a42f256 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -46,7 +46,10 @@ def main_zone_fixture(): def device_fixture(main_zone): """Mock the yamaha device.""" device = FakeYamahaDevice("http://receiver", "Receiver", zones=[main_zone]) - with patch("rxv.RXV", return_value=device): + with ( + patch("rxv.RXV", return_value=device), + patch("rxv.find", return_value=[device]), + ): yield device From 09622e180e55bbfc0c189ae09a0623940076a84a Mon Sep 17 00:00:00 2001 From: SplicedNZ <5253213+SplicedNZ@users.noreply.github.com> Date: Sat, 27 Jul 2024 15:00:01 +1200 Subject: [PATCH 1609/2411] Add virtual integraion for "Mercury NZ Limited" (opower) (#122650) * Add virtual integraion for "Mercury NZ Limited" and bump opower version requirement * revert opower version bump, fix newlines * Update name --- homeassistant/components/mercury_nz/__init__.py | 1 + homeassistant/components/mercury_nz/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 homeassistant/components/mercury_nz/__init__.py create mode 100644 homeassistant/components/mercury_nz/manifest.json diff --git a/homeassistant/components/mercury_nz/__init__.py b/homeassistant/components/mercury_nz/__init__.py new file mode 100644 index 00000000000..ff22fc5ce4a --- /dev/null +++ b/homeassistant/components/mercury_nz/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Mercury NZ Limited.""" diff --git a/homeassistant/components/mercury_nz/manifest.json b/homeassistant/components/mercury_nz/manifest.json new file mode 100644 index 00000000000..d9d30787067 --- /dev/null +++ b/homeassistant/components/mercury_nz/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "mercury_nz", + "name": "Mercury NZ Limited", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 14d4bdc5660..c1b8c5a7cf6 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3577,6 +3577,11 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "mercury_nz": { + "name": "Mercury NZ Limited", + "integration_type": "virtual", + "supported_by": "opower" + }, "message_bird": { "name": "MessageBird", "integration_type": "hub", From bfbd01a4e5e0a8a691e76b7d5148ac0cd153cced Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 27 Jul 2024 08:07:36 +0200 Subject: [PATCH 1610/2411] Add typing to Comfoconnect (#122669) --- .../components/comfoconnect/__init__.py | 20 +++++++++++++------ .../components/comfoconnect/sensor.py | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index 118b59d6cae..8a54c863083 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -13,7 +13,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send @@ -76,7 +76,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: ccb.connect() # Schedule disconnect on shutdown - def _shutdown(_event): + def _shutdown(_event: Event) -> None: ccb.disconnect() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) @@ -90,7 +90,15 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: class ComfoConnectBridge: """Representation of a ComfoConnect bridge.""" - def __init__(self, hass, bridge, name, token, friendly_name, pin): + def __init__( + self, + hass: HomeAssistant, + bridge: Bridge, + name: str, + token: str, + friendly_name: str, + pin: int, + ) -> None: """Initialize the ComfoConnect bridge.""" self.name = name self.hass = hass @@ -104,17 +112,17 @@ class ComfoConnectBridge: ) self.comfoconnect.callback_sensor = self.sensor_callback - def connect(self): + def connect(self) -> None: """Connect with the bridge.""" _LOGGER.debug("Connecting with bridge") self.comfoconnect.connect(True) - def disconnect(self): + def disconnect(self) -> None: """Disconnect from the bridge.""" _LOGGER.debug("Disconnecting from bridge") self.comfoconnect.disconnect() - def sensor_callback(self, var, value): + def sensor_callback(self, var: str, value: str) -> None: """Notify listeners that we have received an update.""" _LOGGER.debug("Received update for %s: %s", var, value) dispatcher_send( diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 25726b3789b..6a15e37f3f1 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -327,7 +327,7 @@ class ComfoConnectSensor(SensorEntity): self._ccb.comfoconnect.register_sensor, self.entity_description.sensor_id ) - def _handle_update(self, value): + def _handle_update(self, value: float) -> None: """Handle update callbacks.""" _LOGGER.debug( "Handle update for sensor %s (%d): %s", From 13c320902e9b2e26bb9593ccf0bd08652aad47c6 Mon Sep 17 00:00:00 2001 From: "Phill (pssc)" Date: Sat, 27 Jul 2024 07:23:41 +0100 Subject: [PATCH 1611/2411] Fix yamaha uid where host in config is defined (#122676) Fix for uid where host in config is defined --- .../components/yamaha/media_player.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 48dbcfffc97..61077d648d2 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -126,8 +126,16 @@ def _discovery(config_info): for recv in rxv.find(): zones.extend(recv.zone_controllers()) else: - _LOGGER.debug("Config Zones Fallback") - zones = rxv.RXV(config_info.ctrl_url, config_info.name).zone_controllers() + _LOGGER.debug("Config Zones") + zones = None + for recv in rxv.find(): + if recv.ctrl_url == config_info.ctrl_url: + _LOGGER.debug("Config Zones Matched %s", config_info.ctrl_url) + zones = recv.zone_controllers() + break + if not zones: + _LOGGER.debug("Config Zones Fallback") + zones = rxv.RXV(config_info.ctrl_url, config_info.name).zone_controllers() _LOGGER.debug("Returned _discover zones: %s", zones) return zones @@ -153,7 +161,7 @@ async def async_setup_platform( entities = [] for zctrl in zone_ctrls: - _LOGGER.info("Receiver zone: %s", zctrl.zone) + _LOGGER.debug("Receiver zone: %s", zctrl.zone) if config_info.zone_ignore and zctrl.zone in config_info.zone_ignore: _LOGGER.debug("Ignore receiver zone: %s %s", config_info.name, zctrl.zone) continue @@ -220,7 +228,6 @@ class YamahaDeviceZone(MediaPlayerEntity): # Since not all receivers will have a serial number and set a unique id # the default name of the integration may not be changed # to avoid a breaking change. - # Prefix as MusicCast could have used this self._attr_unique_id = f"{self.zctrl.serial_number}_{self._zone}" _LOGGER.debug( "Receiver zone: %s zone %s uid %s", @@ -241,7 +248,7 @@ class YamahaDeviceZone(MediaPlayerEntity): try: self._play_status = self.zctrl.play_status() except requests.exceptions.ConnectionError: - _LOGGER.info("Receiver is offline: %s", self._name) + _LOGGER.debug("Receiver is offline: %s", self._name) self._attr_available = False return @@ -288,11 +295,6 @@ class YamahaDeviceZone(MediaPlayerEntity): if source not in self._source_ignore ) - @property - def unique_id(self) -> str: - """Return the unique ID for this media_player.""" - return self._attr_unique_id or "" - @property def name(self): """Return the name of the device.""" From 1a5706a693786f70d4a2a0b2d0c84aa642066e31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Jul 2024 03:14:40 -0500 Subject: [PATCH 1612/2411] Cache unifi device_tracker properties that never change (#122683) --- homeassistant/components/unifi/device_tracker.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index a1014bfd184..800730f3b49 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -5,6 +5,7 @@ from __future__ import annotations from collections.abc import Callable, Mapping from dataclasses import dataclass from datetime import timedelta +from functools import cached_property import logging from typing import Any @@ -261,17 +262,17 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): """Return the primary ip address of the device.""" return self.entity_description.ip_address_fn(self.hub.api, self._obj_id) - @property + @cached_property def mac_address(self) -> str: """Return the mac address of the device.""" return self._obj_id - @property + @cached_property def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" return SourceType.ROUTER - @property + @cached_property def unique_id(self) -> str: """Return a unique ID.""" return self._attr_unique_id From 482cf261c0d7cf356dd0ffcb6c2e2eb0a8dd8bab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Jul 2024 03:19:53 -0500 Subject: [PATCH 1613/2411] Small speedups to unifi (#122684) - Use a set for event_is_on to avoid linear search - Avoid many duplicate property lookups --- homeassistant/components/unifi/button.py | 2 +- .../components/unifi/device_tracker.py | 35 ++++++++++--------- homeassistant/components/unifi/entity.py | 13 +++---- homeassistant/components/unifi/image.py | 4 +-- homeassistant/components/unifi/sensor.py | 2 +- homeassistant/components/unifi/switch.py | 13 +++---- homeassistant/components/unifi/update.py | 6 ++-- 7 files changed, 40 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/unifi/button.py b/homeassistant/components/unifi/button.py index 716d3734953..c53f8be147f 100644 --- a/homeassistant/components/unifi/button.py +++ b/homeassistant/components/unifi/button.py @@ -149,7 +149,7 @@ class UnifiButtonEntity(UnifiEntity[HandlerT, ApiItemT], ButtonEntity): async def async_press(self) -> None: """Press the button.""" - await self.entity_description.control_fn(self.hub.api, self._obj_id) + await self.entity_description.control_fn(self.api, self._obj_id) @callback def async_update_state(self, event: ItemEvent, obj_id: str) -> None: diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 800730f3b49..aae1194b70d 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -153,7 +153,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiTrackerEntityDescription, ...] = ( allowed_fn=async_client_allowed_fn, api_handler_fn=lambda api: api.clients, device_info_fn=lambda api, obj_id: None, - event_is_on=(WIRED_CONNECTION + WIRELESS_CONNECTION), + event_is_on=set(WIRED_CONNECTION + WIRELESS_CONNECTION), event_to_subscribe=( WIRED_CONNECTION + WIRED_DISCONNECTION @@ -226,7 +226,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): entity_description: UnifiTrackerEntityDescription - _event_is_on: tuple[EventKey, ...] + _event_is_on: set[EventKey] _ignore_events: bool _is_connected: bool @@ -237,7 +237,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): Initiate is_connected. """ description = self.entity_description - self._event_is_on = description.event_is_on or () + self._event_is_on = description.event_is_on or set() self._ignore_events = False self._is_connected = description.is_connected_fn(self.hub, self._obj_id) if self.is_connected: @@ -255,12 +255,12 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): @property def hostname(self) -> str | None: """Return hostname of the device.""" - return self.entity_description.hostname_fn(self.hub.api, self._obj_id) + return self.entity_description.hostname_fn(self.api, self._obj_id) @property def ip_address(self) -> str | None: """Return the primary ip address of the device.""" - return self.entity_description.ip_address_fn(self.hub.api, self._obj_id) + return self.entity_description.ip_address_fn(self.api, self._obj_id) @cached_property def mac_address(self) -> str: @@ -293,42 +293,45 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): Schedule new heartbeat check if connected. """ description = self.entity_description + hub = self.hub - if event == ItemEvent.CHANGED: + if event is ItemEvent.CHANGED: # Prioritize normal data updates over events self._ignore_events = True - elif event == ItemEvent.ADDED and not self.available: + elif event is ItemEvent.ADDED and not self.available: # From unifi.entity.async_signal_reachable_callback # Controller connection state has changed and entity is unavailable # Cancel heartbeat - self.hub.remove_heartbeat(self.unique_id) + hub.remove_heartbeat(self.unique_id) return - if is_connected := description.is_connected_fn(self.hub, self._obj_id): + obj_id = self._obj_id + if is_connected := description.is_connected_fn(hub, obj_id): self._is_connected = is_connected self.hub.update_heartbeat( self.unique_id, - dt_util.utcnow() - + description.heartbeat_timedelta_fn(self.hub, self._obj_id), + dt_util.utcnow() + description.heartbeat_timedelta_fn(hub, obj_id), ) @callback def async_event_callback(self, event: Event) -> None: """Event subscription callback.""" - if event.mac != self._obj_id or self._ignore_events: + obj_id = self._obj_id + if event.mac != obj_id or self._ignore_events: return + hub = self.hub if event.key in self._event_is_on: - self.hub.remove_heartbeat(self.unique_id) + hub.remove_heartbeat(self.unique_id) self._is_connected = True self.async_write_ha_state() return - self.hub.update_heartbeat( + hub.update_heartbeat( self.unique_id, dt_util.utcnow() - + self.entity_description.heartbeat_timedelta_fn(self.hub, self._obj_id), + + self.entity_description.heartbeat_timedelta_fn(hub, obj_id), ) async def async_added_to_hass(self) -> None: @@ -353,7 +356,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): if self.entity_description.key != "Client device scanner": return None - client = self.entity_description.object_fn(self.hub.api, self._obj_id) + client = self.entity_description.object_fn(self.api, self._obj_id) raw = client.raw attributes_to_check = CLIENT_STATIC_ATTRIBUTES diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index e162b32ba42..1f9d5b304bc 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -120,7 +120,7 @@ class UnifiEntityDescription(EntityDescription, Generic[HandlerT, ApiItemT]): # Optional constants has_entity_name = True # Part of EntityDescription """Has entity name defaults to true.""" - event_is_on: tuple[EventKey, ...] | None = None + event_is_on: set[EventKey] | None = None """Which UniFi events should be used to consider state 'on'.""" event_to_subscribe: tuple[EventKey, ...] | None = None """Which UniFi events to listen on.""" @@ -143,6 +143,7 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): """Set up UniFi switch entity.""" self._obj_id = obj_id self.hub = hub + self.api = hub.api self.entity_description = description hub.entity_loader.known_objects.add((description.key, obj_id)) @@ -154,14 +155,14 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): self._attr_should_poll = description.should_poll self._attr_unique_id = description.unique_id_fn(hub, obj_id) - obj = description.object_fn(self.hub.api, obj_id) + obj = description.object_fn(self.api, obj_id) self._attr_name = description.name_fn(obj) self.async_initiate_state() async def async_added_to_hass(self) -> None: """Register callbacks.""" description = self.entity_description - handler = description.api_handler_fn(self.hub.api) + handler = description.api_handler_fn(self.api) @callback def unregister_object() -> None: @@ -201,7 +202,7 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): # Subscribe to events if defined if description.event_to_subscribe is not None: self.async_on_remove( - self.hub.api.events.subscribe( + self.api.events.subscribe( self.async_event_callback, description.event_to_subscribe, ) @@ -210,8 +211,8 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): @callback def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: """Update the entity state.""" - if event == ItemEvent.DELETED and obj_id == self._obj_id: - self.hass.async_create_task(self.remove_item({self._obj_id})) + if event is ItemEvent.DELETED and obj_id == self._obj_id: + self.hass.async_create_task(self.remove_item({obj_id})) return description = self.entity_description diff --git a/homeassistant/components/unifi/image.py b/homeassistant/components/unifi/image.py index bbc20e2b06b..426f2ce2884 100644 --- a/homeassistant/components/unifi/image.py +++ b/homeassistant/components/unifi/image.py @@ -97,7 +97,7 @@ class UnifiImageEntity(UnifiEntity[HandlerT, ApiItemT], ImageEntity): """Return bytes of image.""" if self.current_image is None: description = self.entity_description - obj = description.object_fn(self.hub.api, self._obj_id) + obj = description.object_fn(self.api, self._obj_id) self.current_image = description.image_fn(self.hub, obj) return self.current_image @@ -105,7 +105,7 @@ class UnifiImageEntity(UnifiEntity[HandlerT, ApiItemT], ImageEntity): def async_update_state(self, event: ItemEvent, obj_id: str) -> None: """Update entity state.""" description = self.entity_description - obj = description.object_fn(self.hub.api, self._obj_id) + obj = description.object_fn(self.api, self._obj_id) if (value := description.value_fn(obj)) != self.previous_value: self.previous_value = value self.current_image = None diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 071230a9652..bb974864f60 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -490,7 +490,7 @@ class UnifiSensorEntity(UnifiEntity[HandlerT, ApiItemT], SensorEntity): Update native_value. """ description = self.entity_description - obj = description.object_fn(self.hub.api, self._obj_id) + obj = description.object_fn(self.api, self._obj_id) # Update the value only if value is considered to have changed relative to its previous state if description.value_changed_fn( self.native_value, (value := description.value_fn(self.hub, obj)) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index be475803f7e..ef30abb9349 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -11,7 +11,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Any +from typing import TYPE_CHECKING, Any import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent @@ -189,7 +189,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSwitchEntityDescription, ...] = ( api_handler_fn=lambda api: api.clients, control_fn=async_block_client_control_fn, device_info_fn=async_client_device_info_fn, - event_is_on=CLIENT_UNBLOCKED, + event_is_on=set(CLIENT_UNBLOCKED), event_to_subscribe=CLIENT_BLOCKED + CLIENT_UNBLOCKED, is_on_fn=lambda hub, client: not client.blocked, object_fn=lambda api, obj_id: api.clients[obj_id], @@ -342,7 +342,7 @@ class UnifiSwitchEntity(UnifiEntity[HandlerT, ApiItemT], SwitchEntity): return description = self.entity_description - obj = description.object_fn(self.hub.api, self._obj_id) + obj = description.object_fn(self.api, self._obj_id) if (is_on := description.is_on_fn(self.hub, obj)) != self.is_on: self._attr_is_on = is_on @@ -353,8 +353,9 @@ class UnifiSwitchEntity(UnifiEntity[HandlerT, ApiItemT], SwitchEntity): return description = self.entity_description - assert isinstance(description.event_to_subscribe, tuple) - assert isinstance(description.event_is_on, tuple) + if TYPE_CHECKING: + assert description.event_to_subscribe is not None + assert description.event_is_on is not None if event.key in description.event_to_subscribe: self._attr_is_on = event.key in description.event_is_on @@ -367,7 +368,7 @@ class UnifiSwitchEntity(UnifiEntity[HandlerT, ApiItemT], SwitchEntity): if self.entity_description.custom_subscribe is not None: self.async_on_remove( - self.entity_description.custom_subscribe(self.hub.api)( + self.entity_description.custom_subscribe(self.api)( self.async_signalling_callback, ItemEvent.CHANGED ), ) diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index b3cfc6f1c66..65202045a05 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -96,7 +96,7 @@ class UnifiDeviceUpdateEntity(UnifiEntity[_HandlerT, _DataT], UpdateEntity): self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update.""" - await self.entity_description.control_fn(self.hub.api, self._obj_id) + await self.entity_description.control_fn(self.api, self._obj_id) @callback def async_update_state(self, event: ItemEvent, obj_id: str) -> None: @@ -106,7 +106,7 @@ class UnifiDeviceUpdateEntity(UnifiEntity[_HandlerT, _DataT], UpdateEntity): """ description = self.entity_description - obj = description.object_fn(self.hub.api, self._obj_id) - self._attr_in_progress = description.state_fn(self.hub.api, obj) + obj = description.object_fn(self.api, self._obj_id) + self._attr_in_progress = description.state_fn(self.api, obj) self._attr_installed_version = obj.version self._attr_latest_version = obj.upgrade_to_firmware or obj.version From 64f997718a44e270c013f209957ba5af62b2273e Mon Sep 17 00:00:00 2001 From: Luke Wale Date: Sat, 27 Jul 2024 18:36:48 +0800 Subject: [PATCH 1614/2411] Add AirTouch5 cover (#122462) * AirTouch5 - add cover Each zone has a damper that can be controlled as a cover. * remove unused assignment * remove opinionated feature support * Revert "remove unused assignment" This reverts commit b4205a60a22ae3869436229b4a45547348496d39. * ruff formatting changes * git push translation and refactor --- .../components/airtouch5/__init__.py | 2 +- homeassistant/components/airtouch5/climate.py | 1 + homeassistant/components/airtouch5/cover.py | 134 ++++++++++++++++++ homeassistant/components/airtouch5/entity.py | 3 - .../components/airtouch5/strings.json | 5 + 5 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/airtouch5/cover.py diff --git a/homeassistant/components/airtouch5/__init__.py b/homeassistant/components/airtouch5/__init__.py index 1931098282d..8aab41d72cb 100644 --- a/homeassistant/components/airtouch5/__init__.py +++ b/homeassistant/components/airtouch5/__init__.py @@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN -PLATFORMS: list[Platform] = [Platform.CLIMATE] +PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER] type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient] diff --git a/homeassistant/components/airtouch5/climate.py b/homeassistant/components/airtouch5/climate.py index 1f97c254efe..2d5740b1837 100644 --- a/homeassistant/components/airtouch5/climate.py +++ b/homeassistant/components/airtouch5/climate.py @@ -121,6 +121,7 @@ class Airtouch5ClimateEntity(ClimateEntity, Airtouch5Entity): """Base class for Airtouch5 Climate Entities.""" _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_translation_key = DOMAIN _attr_target_temperature_step = 1 _attr_name = None _enable_turn_on_off_backwards_compatibility = False diff --git a/homeassistant/components/airtouch5/cover.py b/homeassistant/components/airtouch5/cover.py new file mode 100644 index 00000000000..62cf7938fc2 --- /dev/null +++ b/homeassistant/components/airtouch5/cover.py @@ -0,0 +1,134 @@ +"""Representation of the Damper for AirTouch 5 Devices.""" + +import logging +from typing import Any + +from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient +from airtouch5py.packets.zone_control import ( + ZoneControlZone, + ZoneSettingPower, + ZoneSettingValue, +) +from airtouch5py.packets.zone_name import ZoneName +from airtouch5py.packets.zone_status import ZoneStatusZone + +from homeassistant.components.cover import ( + ATTR_POSITION, + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import Airtouch5ConfigEntry +from .const import DOMAIN +from .entity import Airtouch5Entity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: Airtouch5ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Airtouch 5 Cover entities.""" + client = config_entry.runtime_data + + # Each zone has a cover for its open percentage + async_add_entities( + Airtouch5ZoneOpenPercentage( + client, zone, client.latest_zone_status[zone.zone_number].has_sensor + ) + for zone in client.zones + ) + + +class Airtouch5ZoneOpenPercentage(CoverEntity, Airtouch5Entity): + """How open the damper is in each zone.""" + + _attr_device_class = CoverDeviceClass.DAMPER + _attr_translation_key = "damper" + + # Zones with temperature sensors shouldn't be manually controlled. + # We allow it but warn the user in the integration documentation. + _attr_supported_features = ( + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + ) + + def __init__( + self, client: Airtouch5SimpleClient, zone_name: ZoneName, has_sensor: bool + ) -> None: + """Initialise the Cover Entity.""" + super().__init__(client) + self._zone_name = zone_name + + self._attr_unique_id = f"zone_{zone_name.zone_number}_open_percentage" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"zone_{zone_name.zone_number}")}, + name=zone_name.zone_name, + manufacturer="Polyaire", + model="AirTouch 5", + ) + + @callback + def _async_update_attrs(self, data: dict[int, ZoneStatusZone]) -> None: + if self._zone_name.zone_number not in data: + return + status = data[self._zone_name.zone_number] + + self._attr_current_cover_position = int(status.open_percentage * 100) + if status.open_percentage == 0: + self._attr_is_closed = True + else: + self._attr_is_closed = False + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Add data updated listener after this object has been initialized.""" + await super().async_added_to_hass() + self._client.zone_status_callbacks.append(self._async_update_attrs) + self._async_update_attrs(self._client.latest_zone_status) + + async def async_will_remove_from_hass(self) -> None: + """Remove data updated listener after this object has been initialized.""" + await super().async_will_remove_from_hass() + self._client.zone_status_callbacks.remove(self._async_update_attrs) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the damper.""" + await self._set_cover_position(100) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close damper.""" + await self._set_cover_position(0) + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Update the damper to a specific position.""" + + if (position := kwargs.get(ATTR_POSITION)) is None: + _LOGGER.debug("Argument `position` is missing in set_cover_position") + return + await self._set_cover_position(position) + + async def _set_cover_position(self, position_percent: float) -> None: + power: ZoneSettingPower + + if position_percent == 0: + power = ZoneSettingPower.SET_TO_OFF + else: + power = ZoneSettingPower.SET_TO_ON + + zcz = ZoneControlZone( + self._zone_name.zone_number, + ZoneSettingValue.SET_OPEN_PERCENTAGE, + power, + position_percent / 100.0, + ) + + packet = self._client.data_packet_factory.zone_control([zcz]) + await self._client.send_packet(packet) diff --git a/homeassistant/components/airtouch5/entity.py b/homeassistant/components/airtouch5/entity.py index e5899850e0f..d0a3cc8fea3 100644 --- a/homeassistant/components/airtouch5/entity.py +++ b/homeassistant/components/airtouch5/entity.py @@ -6,15 +6,12 @@ from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from .const import DOMAIN - class Airtouch5Entity(Entity): """Base class for Airtouch5 entities.""" _attr_should_poll = False _attr_has_entity_name = True - _attr_translation_key = DOMAIN def __init__(self, client: Airtouch5SimpleClient) -> None: """Initialise the Entity.""" diff --git a/homeassistant/components/airtouch5/strings.json b/homeassistant/components/airtouch5/strings.json index 6a91fa85fa5..effeb0c72e0 100644 --- a/homeassistant/components/airtouch5/strings.json +++ b/homeassistant/components/airtouch5/strings.json @@ -27,6 +27,11 @@ } } } + }, + "cover": { + "damper": { + "name": "[%key:component::cover::entity_component::damper::name%]" + } } } } From cb4a48ca02aaf2780dc939fd3c531c6d294e1873 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Sat, 27 Jul 2024 12:07:02 +0100 Subject: [PATCH 1615/2411] Migrate Mastodon integration to config flow (#122376) * Migrate to config flow * Fixes & add code owner * Add codeowners * Import within notify module * Fixes from review * Fixes * Remove config schema --- CODEOWNERS | 3 +- homeassistant/components/mastodon/__init__.py | 59 ++++++ .../components/mastodon/config_flow.py | 168 ++++++++++++++++ homeassistant/components/mastodon/const.py | 9 + .../components/mastodon/manifest.json | 4 +- homeassistant/components/mastodon/notify.py | 89 ++++++--- .../components/mastodon/strings.json | 39 ++++ homeassistant/components/mastodon/utils.py | 32 ++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 4 +- requirements_test_all.txt | 3 + tests/components/mastodon/__init__.py | 13 ++ tests/components/mastodon/conftest.py | 57 ++++++ .../fixtures/account_verify_credentials.json | 78 ++++++++ .../mastodon/fixtures/instance.json | 147 ++++++++++++++ .../mastodon/snapshots/test_init.ambr | 33 ++++ tests/components/mastodon/test_config_flow.py | 179 ++++++++++++++++++ tests/components/mastodon/test_init.py | 25 +++ tests/components/mastodon/test_notify.py | 38 ++++ 19 files changed, 953 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/mastodon/config_flow.py create mode 100644 homeassistant/components/mastodon/strings.json create mode 100644 homeassistant/components/mastodon/utils.py create mode 100644 tests/components/mastodon/__init__.py create mode 100644 tests/components/mastodon/conftest.py create mode 100644 tests/components/mastodon/fixtures/account_verify_credentials.json create mode 100644 tests/components/mastodon/fixtures/instance.json create mode 100644 tests/components/mastodon/snapshots/test_init.ambr create mode 100644 tests/components/mastodon/test_config_flow.py create mode 100644 tests/components/mastodon/test_init.py create mode 100644 tests/components/mastodon/test_notify.py diff --git a/CODEOWNERS b/CODEOWNERS index 273607234e5..1b9808a418a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -839,7 +839,8 @@ build.json @home-assistant/supervisor /tests/components/lyric/ @timmo001 /homeassistant/components/madvr/ @iloveicedgreentea /tests/components/madvr/ @iloveicedgreentea -/homeassistant/components/mastodon/ @fabaff +/homeassistant/components/mastodon/ @fabaff @andrew-codechimp +/tests/components/mastodon/ @fabaff @andrew-codechimp /homeassistant/components/matrix/ @PaarthShah /tests/components/matrix/ @PaarthShah /homeassistant/components/matter/ @home-assistant/matter diff --git a/homeassistant/components/mastodon/__init__.py b/homeassistant/components/mastodon/__init__.py index 6a9f074a9ba..2fe379702ee 100644 --- a/homeassistant/components/mastodon/__init__.py +++ b/homeassistant/components/mastodon/__init__.py @@ -1 +1,60 @@ """The Mastodon integration.""" + +from __future__ import annotations + +from mastodon.Mastodon import Mastodon, MastodonError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_NAME, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import discovery + +from .const import CONF_BASE_URL, DOMAIN +from .utils import create_mastodon_client + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Mastodon from a config entry.""" + + try: + client, _, _ = await hass.async_add_executor_job( + setup_mastodon, + entry, + ) + + except MastodonError as ex: + raise ConfigEntryNotReady("Failed to connect") from ex + + assert entry.unique_id + + await discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + {CONF_NAME: entry.title, "client": client}, + {}, + ) + + return True + + +def setup_mastodon(entry: ConfigEntry) -> tuple[Mastodon, dict, dict]: + """Get mastodon details.""" + client = create_mastodon_client( + entry.data[CONF_BASE_URL], + entry.data[CONF_CLIENT_ID], + entry.data[CONF_CLIENT_SECRET], + entry.data[CONF_ACCESS_TOKEN], + ) + + instance = client.instance() + account = client.account_verify_credentials() + + return client, instance, account diff --git a/homeassistant/components/mastodon/config_flow.py b/homeassistant/components/mastodon/config_flow.py new file mode 100644 index 00000000000..7d1c9396cbb --- /dev/null +++ b/homeassistant/components/mastodon/config_flow.py @@ -0,0 +1,168 @@ +"""Config flow for Mastodon.""" + +from __future__ import annotations + +from typing import Any + +from mastodon.Mastodon import MastodonNetworkError, MastodonUnauthorizedError +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_NAME, +) +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) +from homeassistant.helpers.typing import ConfigType + +from .const import CONF_BASE_URL, DEFAULT_URL, DOMAIN, LOGGER +from .utils import construct_mastodon_username, create_mastodon_client + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required( + CONF_BASE_URL, + default=DEFAULT_URL, + ): TextSelector(TextSelectorConfig(type=TextSelectorType.URL)), + vol.Required( + CONF_CLIENT_ID, + ): TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWORD)), + vol.Required( + CONF_CLIENT_SECRET, + ): TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWORD)), + vol.Required( + CONF_ACCESS_TOKEN, + ): TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWORD)), + } +) + + +class MastodonConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + config_entry: ConfigEntry + + def check_connection( + self, + base_url: str, + client_id: str, + client_secret: str, + access_token: str, + ) -> tuple[ + dict[str, str] | None, + dict[str, str] | None, + dict[str, str], + ]: + """Check connection to the Mastodon instance.""" + try: + client = create_mastodon_client( + base_url, + client_id, + client_secret, + access_token, + ) + instance = client.instance() + account = client.account_verify_credentials() + + except MastodonNetworkError: + return None, None, {"base": "network_error"} + except MastodonUnauthorizedError: + return None, None, {"base": "unauthorized_error"} + except Exception: # noqa: BLE001 + LOGGER.exception("Unexpected error") + return None, None, {"base": "unknown"} + return instance, account, {} + + def show_user_form( + self, + user_input: dict[str, Any] | None = None, + errors: dict[str, str] | None = None, + description_placeholders: dict[str, str] | None = None, + step_id: str = "user", + ) -> ConfigFlowResult: + """Show the user form.""" + if user_input is None: + user_input = {} + return self.async_show_form( + step_id=step_id, + data_schema=self.add_suggested_values_to_schema( + STEP_USER_DATA_SCHEMA, user_input + ), + description_placeholders=description_placeholders, + errors=errors, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow initialized by the user.""" + errors: dict[str, str] | None = None + if user_input: + self._async_abort_entries_match( + {CONF_CLIENT_ID: user_input[CONF_CLIENT_ID]} + ) + + instance, account, errors = await self.hass.async_add_executor_job( + self.check_connection, + user_input[CONF_BASE_URL], + user_input[CONF_CLIENT_ID], + user_input[CONF_CLIENT_SECRET], + user_input[CONF_ACCESS_TOKEN], + ) + + if not errors: + name = construct_mastodon_username(instance, account) + await self.async_set_unique_id(user_input[CONF_CLIENT_ID]) + return self.async_create_entry( + title=name, + data=user_input, + ) + + return self.show_user_form(user_input, errors) + + async def async_step_import(self, import_config: ConfigType) -> ConfigFlowResult: + """Import a config entry from configuration.yaml.""" + errors: dict[str, str] | None = None + + LOGGER.debug("Importing Mastodon from configuration.yaml") + + base_url = str(import_config.get(CONF_BASE_URL, DEFAULT_URL)) + client_id = str(import_config.get(CONF_CLIENT_ID)) + client_secret = str(import_config.get(CONF_CLIENT_SECRET)) + access_token = str(import_config.get(CONF_ACCESS_TOKEN)) + name = import_config.get(CONF_NAME, None) + + instance, account, errors = await self.hass.async_add_executor_job( + self.check_connection, + base_url, + client_id, + client_secret, + access_token, + ) + + if not errors: + await self.async_set_unique_id(client_id) + self._abort_if_unique_id_configured() + + if not name: + name = construct_mastodon_username(instance, account) + + return self.async_create_entry( + title=name, + data={ + CONF_BASE_URL: base_url, + CONF_CLIENT_ID: client_id, + CONF_CLIENT_SECRET: client_secret, + CONF_ACCESS_TOKEN: access_token, + }, + ) + + reason = next(iter(errors.items()))[1] + return self.async_abort(reason=reason) diff --git a/homeassistant/components/mastodon/const.py b/homeassistant/components/mastodon/const.py index 6fe9552f991..3a9cf7462e6 100644 --- a/homeassistant/components/mastodon/const.py +++ b/homeassistant/components/mastodon/const.py @@ -5,5 +5,14 @@ from typing import Final LOGGER = logging.getLogger(__name__) +DOMAIN: Final = "mastodon" + CONF_BASE_URL: Final = "base_url" +DATA_HASS_CONFIG = "mastodon_hass_config" DEFAULT_URL: Final = "https://mastodon.social" +DEFAULT_NAME: Final = "Mastodon" + +INSTANCE_VERSION: Final = "version" +INSTANCE_URI: Final = "uri" +INSTANCE_DOMAIN: Final = "domain" +ACCOUNT_USERNAME: Final = "username" diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index 673a60166c0..40fd9d2f7b3 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -1,8 +1,10 @@ { "domain": "mastodon", "name": "Mastodon", - "codeowners": ["@fabaff"], + "codeowners": ["@fabaff", "@andrew-codechimp"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mastodon", + "integration_type": "service", "iot_class": "cloud_push", "loggers": ["mastodon"], "requirements": ["Mastodon.py==1.8.1"] diff --git a/homeassistant/components/mastodon/notify.py b/homeassistant/components/mastodon/notify.py index 99999275aeb..7878fc665a1 100644 --- a/homeassistant/components/mastodon/notify.py +++ b/homeassistant/components/mastodon/notify.py @@ -6,7 +6,7 @@ import mimetypes from typing import Any, cast from mastodon import Mastodon -from mastodon.Mastodon import MastodonAPIError, MastodonUnauthorizedError +from mastodon.Mastodon import MastodonAPIError import voluptuous as vol from homeassistant.components.notify import ( @@ -14,12 +14,14 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_BASE_URL, DEFAULT_URL, LOGGER +from .const import CONF_BASE_URL, DEFAULT_URL, DOMAIN, LOGGER ATTR_MEDIA = "media" ATTR_TARGET = "target" @@ -35,39 +37,78 @@ PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( } ) +INTEGRATION_TITLE = "Mastodon" -def get_service( + +async def async_get_service( hass: HomeAssistant, config: ConfigType, discovery_info: DiscoveryInfoType | None = None, ) -> MastodonNotificationService | None: """Get the Mastodon notification service.""" - client_id = config.get(CONF_CLIENT_ID) - client_secret = config.get(CONF_CLIENT_SECRET) - access_token = config.get(CONF_ACCESS_TOKEN) - base_url = config.get(CONF_BASE_URL) - try: - mastodon = Mastodon( - client_id=client_id, - client_secret=client_secret, - access_token=access_token, - api_base_url=base_url, + if not discovery_info: + # Import config entry + + import_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, ) - mastodon.account_verify_credentials() - except MastodonUnauthorizedError: - LOGGER.warning("Authentication failed") + + if ( + import_result["type"] == FlowResultType.ABORT + and import_result["reason"] != "already_configured" + ): + ir.async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_import_issue_{import_result["reason"]}", + breaks_in_ha_version="2025.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=ir.IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{import_result["reason"]}", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": INTEGRATION_TITLE, + }, + ) + return None + + ir.async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2025.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": INTEGRATION_TITLE, + }, + ) + return None - return MastodonNotificationService(mastodon) + client: Mastodon = discovery_info.get("client") + + return MastodonNotificationService(hass, client) class MastodonNotificationService(BaseNotificationService): """Implement the notification service for Mastodon.""" - def __init__(self, api: Mastodon) -> None: + def __init__( + self, + hass: HomeAssistant, + client: Mastodon, + ) -> None: """Initialize the service.""" - self._api = api + + self.client = client def send_message(self, message: str = "", **kwargs: Any) -> None: """Toot a message, with media perhaps.""" @@ -96,7 +137,7 @@ class MastodonNotificationService(BaseNotificationService): if mediadata: try: - self._api.status_post( + self.client.status_post( message, media_ids=mediadata["id"], sensitive=sensitive, @@ -107,7 +148,7 @@ class MastodonNotificationService(BaseNotificationService): LOGGER.error("Unable to send message") else: try: - self._api.status_post( + self.client.status_post( message, visibility=target, spoiler_text=content_warning ) except MastodonAPIError: @@ -118,7 +159,7 @@ class MastodonNotificationService(BaseNotificationService): with open(media_path, "rb"): media_type = self._media_type(media_path) try: - mediadata = self._api.media_post(media_path, mime_type=media_type) + mediadata = self.client.media_post(media_path, mime_type=media_type) except MastodonAPIError: LOGGER.error(f"Unable to upload image {media_path}") diff --git a/homeassistant/components/mastodon/strings.json b/homeassistant/components/mastodon/strings.json new file mode 100644 index 00000000000..e1124aad1a9 --- /dev/null +++ b/homeassistant/components/mastodon/strings.json @@ -0,0 +1,39 @@ +{ + "config": { + "step": { + "user": { + "data": { + "base_url": "[%key:common::config_flow::data::url%]", + "client_id": "Client Key", + "client_secret": "Client Secret", + "access_token": "[%key:common::config_flow::data::access_token%]" + }, + "data_description": { + "base_url": "The URL of your Mastodon instance." + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, + "error": { + "unauthorized_error": "The credentials are incorrect.", + "network_error": "The Mastodon instance was not found.", + "unknown": "Unknown error occured when connecting to the Mastodon instance." + } + }, + "issues": { + "deprecated_yaml_import_issue_unauthorized_error": { + "title": "YAML import failed due to an authentication error", + "description": "Configuring {integration_title} using YAML is being removed but there was an authentication error while importing your existing configuration.\nPlease use the UI to configure Mastodon. Don't forget to delete the YAML configuration." + }, + "deprecated_yaml_import_issue_network_error": { + "title": "YAML import failed because the instance was not found", + "description": "Configuring {integration_title} using YAML is being removed but no instance was found while importing your existing configuration.\nPlease use the UI to configure Mastodon. Don't forget to delete the YAML configuration." + }, + "deprecated_yaml_import_issue_unknown": { + "title": "YAML import failed with unknown error", + "description": "Configuring {integration_title} using YAML is being removed but there was an unknown error while importing your existing configuration.\nPlease use the UI to configure Mastodon. Don't forget to delete the YAML configuration." + } + } +} diff --git a/homeassistant/components/mastodon/utils.py b/homeassistant/components/mastodon/utils.py new file mode 100644 index 00000000000..8e1bd697027 --- /dev/null +++ b/homeassistant/components/mastodon/utils.py @@ -0,0 +1,32 @@ +"""Mastodon util functions.""" + +from __future__ import annotations + +from mastodon import Mastodon + +from .const import ACCOUNT_USERNAME, DEFAULT_NAME, INSTANCE_DOMAIN, INSTANCE_URI + + +def create_mastodon_client( + base_url: str, client_id: str, client_secret: str, access_token: str +) -> Mastodon: + """Create a Mastodon client with the api base url.""" + return Mastodon( + api_base_url=base_url, + client_id=client_id, + client_secret=client_secret, + access_token=access_token, + ) + + +def construct_mastodon_username( + instance: dict[str, str] | None, account: dict[str, str] | None +) -> str: + """Construct a mastodon username from the account and instance.""" + if instance and account: + return ( + f"@{account[ACCOUNT_USERNAME]}@" + f"{instance.get(INSTANCE_URI, instance.get(INSTANCE_DOMAIN))}" + ) + + return DEFAULT_NAME diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 14036dcb1b5..90f9675339b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -330,6 +330,7 @@ FLOWS = { "lyric", "madvr", "mailgun", + "mastodon", "matter", "mealie", "meater", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index c1b8c5a7cf6..dc1d203856c 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3495,8 +3495,8 @@ }, "mastodon": { "name": "Mastodon", - "integration_type": "hub", - "config_flow": false, + "integration_type": "service", + "config_flow": true, "iot_class": "cloud_push" }, "matrix": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3b94163ea8..6ba2e3fd2c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -21,6 +21,9 @@ HAP-python==4.9.1 # homeassistant.components.tasmota HATasmota==0.9.2 +# homeassistant.components.mastodon +Mastodon.py==1.8.1 + # homeassistant.components.doods # homeassistant.components.generic # homeassistant.components.image_upload diff --git a/tests/components/mastodon/__init__.py b/tests/components/mastodon/__init__.py new file mode 100644 index 00000000000..a4c730db07a --- /dev/null +++ b/tests/components/mastodon/__init__.py @@ -0,0 +1,13 @@ +"""Tests for the Mastodon integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/mastodon/conftest.py b/tests/components/mastodon/conftest.py new file mode 100644 index 00000000000..03c3e754c11 --- /dev/null +++ b/tests/components/mastodon/conftest.py @@ -0,0 +1,57 @@ +"""Mastodon tests configuration.""" + +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from homeassistant.components.mastodon.const import CONF_BASE_URL, DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET + +from tests.common import MockConfigEntry, load_json_object_fixture +from tests.components.smhi.common import AsyncMock + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.mastodon.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_mastodon_client() -> Generator[AsyncMock]: + """Mock a Mastodon client.""" + with ( + patch( + "homeassistant.components.mastodon.utils.Mastodon", + autospec=True, + ) as mock_client, + ): + client = mock_client.return_value + client.instance.return_value = load_json_object_fixture("instance.json", DOMAIN) + client.account_verify_credentials.return_value = load_json_object_fixture( + "account_verify_credentials.json", DOMAIN + ) + client.status_post.return_value = None + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="@trwnh@mastodon.social", + data={ + CONF_BASE_URL: "https://mastodon.social", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + CONF_ACCESS_TOKEN: "access_token", + }, + entry_id="01J35M4AH9HYRC2V0G6RNVNWJH", + unique_id="client_id", + ) diff --git a/tests/components/mastodon/fixtures/account_verify_credentials.json b/tests/components/mastodon/fixtures/account_verify_credentials.json new file mode 100644 index 00000000000..401caa121ae --- /dev/null +++ b/tests/components/mastodon/fixtures/account_verify_credentials.json @@ -0,0 +1,78 @@ +{ + "id": "14715", + "username": "trwnh", + "acct": "trwnh", + "display_name": "infinite love ⴳ", + "locked": false, + "bot": false, + "created_at": "2016-11-24T10:02:12.085Z", + "note": "

i have approximate knowledge of many things. perpetual student. (nb/ace/they)

xmpp/email: a@trwnh.com
https://trwnh.com
help me live: https://liberapay.com/at or https://paypal.me/trwnh

- my triggers are moths and glitter
- i have all notifs except mentions turned off, so please interact if you wanna be friends! i literally will not notice otherwise
- dm me if i did something wrong, so i can improve
- purest person on fedi, do not lewd in my presence
- #1 ami cole fan account

:fatyoshi:

", + "url": "https://mastodon.social/@trwnh", + "avatar": "https://files.mastodon.social/accounts/avatars/000/014/715/original/34aa222f4ae2e0a9.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/014/715/original/34aa222f4ae2e0a9.png", + "header": "https://files.mastodon.social/accounts/headers/000/014/715/original/5c6fc24edb3bb873.jpg", + "header_static": "https://files.mastodon.social/accounts/headers/000/014/715/original/5c6fc24edb3bb873.jpg", + "followers_count": 821, + "following_count": 178, + "statuses_count": 33120, + "last_status_at": "2019-11-24T15:49:42.251Z", + "source": { + "privacy": "public", + "sensitive": false, + "language": "", + "note": "i have approximate knowledge of many things. perpetual student. (nb/ace/they)\r\n\r\nxmpp/email: a@trwnh.com\r\nhttps://trwnh.com\r\nhelp me live: https://liberapay.com/at or https://paypal.me/trwnh\r\n\r\n- my triggers are moths and glitter\r\n- i have all notifs except mentions turned off, so please interact if you wanna be friends! i literally will not notice otherwise\r\n- dm me if i did something wrong, so i can improve\r\n- purest person on fedi, do not lewd in my presence\r\n- #1 ami cole fan account\r\n\r\n:fatyoshi:", + "fields": [ + { + "name": "Website", + "value": "https://trwnh.com", + "verified_at": "2019-08-29T04:14:55.571+00:00" + }, + { + "name": "Sponsor", + "value": "https://liberapay.com/at", + "verified_at": "2019-11-15T10:06:15.557+00:00" + }, + { + "name": "Fan of:", + "value": "Punk-rock and post-hardcore (Circa Survive, letlive., La Dispute, THE FEVER 333)Manga (Yu-Gi-Oh!, One Piece, JoJo's Bizarre Adventure, Death Note, Shaman King)Platformers and RPGs (Banjo-Kazooie, Boktai, Final Fantasy Crystal Chronicles)", + "verified_at": null + }, + { + "name": "Main topics:", + "value": "systemic analysis, design patterns, anticapitalism, info/tech freedom, theory and philosophy, and otherwise being a genuine and decent wholesome poster. i'm just here to hang out and talk to cool people!", + "verified_at": null + } + ], + "follow_requests_count": 0 + }, + "emojis": [ + { + "shortcode": "fatyoshi", + "url": "https://files.mastodon.social/custom_emojis/images/000/023/920/original/e57ecb623faa0dc9.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/023/920/static/e57ecb623faa0dc9.png", + "visible_in_picker": true + } + ], + "fields": [ + { + "name": "Website", + "value": "https://trwnh.com", + "verified_at": "2019-08-29T04:14:55.571+00:00" + }, + { + "name": "Sponsor", + "value": "https://liberapay.com/at", + "verified_at": "2019-11-15T10:06:15.557+00:00" + }, + { + "name": "Fan of:", + "value": "Punk-rock and post-hardcore (Circa Survive, letlive., La Dispute, THE FEVER 333)Manga (Yu-Gi-Oh!, One Piece, JoJo's Bizarre Adventure, Death Note, Shaman King)Platformers and RPGs (Banjo-Kazooie, Boktai, Final Fantasy Crystal Chronicles)", + "verified_at": null + }, + { + "name": "Main topics:", + "value": "systemic analysis, design patterns, anticapitalism, info/tech freedom, theory and philosophy, and otherwise being a genuine and decent wholesome poster. i'm just here to hang out and talk to cool people!", + "verified_at": null + } + ] +} diff --git a/tests/components/mastodon/fixtures/instance.json b/tests/components/mastodon/fixtures/instance.json new file mode 100644 index 00000000000..b0e904e80ef --- /dev/null +++ b/tests/components/mastodon/fixtures/instance.json @@ -0,0 +1,147 @@ +{ + "domain": "mastodon.social", + "title": "Mastodon", + "version": "4.0.0rc1", + "source_url": "https://github.com/mastodon/mastodon", + "description": "The original server operated by the Mastodon gGmbH non-profit", + "usage": { + "users": { + "active_month": 123122 + } + }, + "thumbnail": { + "url": "https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png", + "blurhash": "UeKUpFxuo~R%0nW;WCnhF6RjaJt757oJodS$", + "versions": { + "@1x": "https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png", + "@2x": "https://files.mastodon.social/site_uploads/files/000/000/001/@2x/57c12f441d083cde.png" + } + }, + "languages": ["en"], + "configuration": { + "urls": { + "streaming": "wss://mastodon.social" + }, + "vapid": { + "public_key": "BCkMmVdKDnKYwzVCDC99Iuc9GvId-x7-kKtuHnLgfF98ENiZp_aj-UNthbCdI70DqN1zUVis-x0Wrot2sBagkMc=" + }, + "accounts": { + "max_featured_tags": 10, + "max_pinned_statuses": 4 + }, + "statuses": { + "max_characters": 500, + "max_media_attachments": 4, + "characters_reserved_per_url": 23 + }, + "media_attachments": { + "supported_mime_types": [ + "image/jpeg", + "image/png", + "image/gif", + "image/heic", + "image/heif", + "image/webp", + "video/webm", + "video/mp4", + "video/quicktime", + "video/ogg", + "audio/wave", + "audio/wav", + "audio/x-wav", + "audio/x-pn-wave", + "audio/vnd.wave", + "audio/ogg", + "audio/vorbis", + "audio/mpeg", + "audio/mp3", + "audio/webm", + "audio/flac", + "audio/aac", + "audio/m4a", + "audio/x-m4a", + "audio/mp4", + "audio/3gpp", + "video/x-ms-asf" + ], + "image_size_limit": 10485760, + "image_matrix_limit": 16777216, + "video_size_limit": 41943040, + "video_frame_rate_limit": 60, + "video_matrix_limit": 2304000 + }, + "polls": { + "max_options": 4, + "max_characters_per_option": 50, + "min_expiration": 300, + "max_expiration": 2629746 + }, + "translation": { + "enabled": true + } + }, + "registrations": { + "enabled": false, + "approval_required": false, + "message": null + }, + "contact": { + "email": "staff@mastodon.social", + "account": { + "id": "1", + "username": "Gargron", + "acct": "Gargron", + "display_name": "Eugen 💀", + "locked": false, + "bot": false, + "discoverable": true, + "group": false, + "created_at": "2016-03-16T00:00:00.000Z", + "note": "

Founder, CEO and lead developer @Mastodon, Germany.

", + "url": "https://mastodon.social/@Gargron", + "avatar": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg", + "header": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", + "header_static": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", + "followers_count": 133026, + "following_count": 311, + "statuses_count": 72605, + "last_status_at": "2022-10-31", + "noindex": false, + "emojis": [], + "fields": [ + { + "name": "Patreon", + "value": "https://www.patreon.com/mastodon", + "verified_at": null + } + ] + } + }, + "rules": [ + { + "id": "1", + "text": "Sexually explicit or violent media must be marked as sensitive when posting" + }, + { + "id": "2", + "text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism" + }, + { + "id": "3", + "text": "No incitement of violence or promotion of violent ideologies" + }, + { + "id": "4", + "text": "No harassment, dogpiling or doxxing of other users" + }, + { + "id": "5", + "text": "No content illegal in Germany" + }, + { + "id": "7", + "text": "Do not share intentionally false or misleading information" + } + ] +} diff --git a/tests/components/mastodon/snapshots/test_init.ambr b/tests/components/mastodon/snapshots/test_init.ambr new file mode 100644 index 00000000000..f0b650076be --- /dev/null +++ b/tests/components/mastodon/snapshots/test_init.ambr @@ -0,0 +1,33 @@ +# serializer version: 1 +# name: test_device_info + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'mastodon', + 'client_id', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Mastodon gGmbH', + 'model': '@trwnh@mastodon.social', + 'model_id': None, + 'name': 'Mastodon', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '4.0.0rc1', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/mastodon/test_config_flow.py b/tests/components/mastodon/test_config_flow.py new file mode 100644 index 00000000000..01cdc061d3e --- /dev/null +++ b/tests/components/mastodon/test_config_flow.py @@ -0,0 +1,179 @@ +"""Tests for the Mastodon config flow.""" + +from unittest.mock import AsyncMock + +from mastodon.Mastodon import MastodonNetworkError, MastodonUnauthorizedError +import pytest + +from homeassistant.components.mastodon.const import CONF_BASE_URL, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_flow( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test full flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_BASE_URL: "https://mastodon.social", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + CONF_ACCESS_TOKEN: "access_token", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "@trwnh@mastodon.social" + assert result["data"] == { + CONF_BASE_URL: "https://mastodon.social", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + CONF_ACCESS_TOKEN: "access_token", + } + assert result["result"].unique_id == "client_id" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (MastodonNetworkError, "network_error"), + (MastodonUnauthorizedError, "unauthorized_error"), + (Exception, "unknown"), + ], +) +async def test_flow_errors( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_setup_entry: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test flow errors.""" + mock_mastodon_client.account_verify_credentials.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_BASE_URL: "https://mastodon.social", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + CONF_ACCESS_TOKEN: "access_token", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_mastodon_client.account_verify_credentials.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_BASE_URL: "https://mastodon.social", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + CONF_ACCESS_TOKEN: "access_token", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_duplicate( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test duplicate flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_BASE_URL: "https://mastodon.social", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + CONF_ACCESS_TOKEN: "access_token", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_import_flow( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test importing yaml config.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_BASE_URL: "https://mastodon.social", + CONF_CLIENT_ID: "import_client_id", + CONF_CLIENT_SECRET: "import_client_secret", + CONF_ACCESS_TOKEN: "import_access_token", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (MastodonNetworkError, "network_error"), + (MastodonUnauthorizedError, "unauthorized_error"), + (Exception, "unknown"), + ], +) +async def test_import_flow_abort( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_setup_entry: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test importing yaml config abort.""" + mock_mastodon_client.account_verify_credentials.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_BASE_URL: "https://mastodon.social", + CONF_CLIENT_ID: "import_client_id", + CONF_CLIENT_SECRET: "import_client_secret", + CONF_ACCESS_TOKEN: "import_access_token", + }, + ) + assert result["type"] is FlowResultType.ABORT diff --git a/tests/components/mastodon/test_init.py b/tests/components/mastodon/test_init.py new file mode 100644 index 00000000000..53796e39782 --- /dev/null +++ b/tests/components/mastodon/test_init.py @@ -0,0 +1,25 @@ +"""Tests for the Mastodon integration.""" + +from unittest.mock import AsyncMock + +from mastodon.Mastodon import MastodonError + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_initialization_failure( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test initialization failure.""" + mock_mastodon_client.instance.side_effect = MastodonError + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/mastodon/test_notify.py b/tests/components/mastodon/test_notify.py new file mode 100644 index 00000000000..ab2d7456baf --- /dev/null +++ b/tests/components/mastodon/test_notify.py @@ -0,0 +1,38 @@ +"""Tests for the Mastodon notify platform.""" + +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_notify( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test sending a message.""" + await setup_integration(hass, mock_config_entry) + + assert hass.services.has_service(NOTIFY_DOMAIN, "trwnh_mastodon_social") + + await hass.services.async_call( + NOTIFY_DOMAIN, + "trwnh_mastodon_social", + { + "message": "test toot", + }, + blocking=True, + return_response=False, + ) + + assert mock_mastodon_client.status_post.assert_called_once From 02a5df0aeee61579e3bfd39b9804306f3b71df42 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 27 Jul 2024 14:01:58 +0200 Subject: [PATCH 1616/2411] Update nibe library to 2.11.0 (#122697) Update nibe library to 2.11.0 with changes Addition of BT71 for F1155/F1255 Addition of climate zones for S1155/S1255 Include log information on incomplete reads Correct fan speeds from being a percentage to a mapping on F series pumps Corrections for airflow units Let denied alarm resets writes be considered as a valid connection on setup --- homeassistant/components/nibe_heatpump/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/manifest.json b/homeassistant/components/nibe_heatpump/manifest.json index 970f53837ea..b3e5597da73 100644 --- a/homeassistant/components/nibe_heatpump/manifest.json +++ b/homeassistant/components/nibe_heatpump/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nibe_heatpump", "iot_class": "local_polling", - "requirements": ["nibe==2.8.0"] + "requirements": ["nibe==2.11.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1e5350d5988..bcb1a52d3ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1411,7 +1411,7 @@ nextcord==2.6.0 nextdns==3.1.0 # homeassistant.components.nibe_heatpump -nibe==2.8.0 +nibe==2.11.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 6ba2e3fd2c4..e8ad506f418 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1168,7 +1168,7 @@ nextcord==2.6.0 nextdns==3.1.0 # homeassistant.components.nibe_heatpump -nibe==2.8.0 +nibe==2.11.0 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 From b0780e1db5c67a3cbb9be42aa5e7aa21a8a09f24 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sat, 27 Jul 2024 14:32:37 +0200 Subject: [PATCH 1617/2411] Remove conditions from enphase_envoy test_switch (#122693) --- tests/components/enphase_envoy/test_switch.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/components/enphase_envoy/test_switch.py b/tests/components/enphase_envoy/test_switch.py index 5a549257685..15f59cc3ea6 100644 --- a/tests/components/enphase_envoy/test_switch.py +++ b/tests/components/enphase_envoy/test_switch.py @@ -12,7 +12,6 @@ from homeassistant.const import ( SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_CLOSED, STATE_OFF, STATE_ON, ) @@ -146,12 +145,24 @@ async def test_switch_grid_operation( @pytest.mark.parametrize( - ("mock_envoy"), ["envoy_metered_batt_relay"], indirect=["mock_envoy"] + ("mock_envoy", "entity_states"), + [ + ( + "envoy_metered_batt_relay", + { + "NC1": (STATE_OFF, 0, 1), + "NC2": (STATE_ON, 1, 0), + "NC3": (STATE_OFF, 0, 1), + }, + ) + ], + indirect=["mock_envoy"], ) async def test_switch_relay_operation( hass: HomeAssistant, mock_envoy: AsyncMock, config_entry: MockConfigEntry, + entity_states: dict[str, tuple[str, int, int]], ) -> None: """Test enphase_envoy switch relay entities operation.""" with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SWITCH]): @@ -162,13 +173,10 @@ async def test_switch_relay_operation( for contact_id, dry_contact in mock_envoy.data.dry_contact_settings.items(): name = dry_contact.load_name.lower().replace(" ", "_") test_entity = f"{entity_base}{name}" - target_value = mock_envoy.data.dry_contact_status[contact_id].status assert (entity_state := hass.states.get(test_entity)) - assert ( - entity_state.state == STATE_ON - if target_value == STATE_CLOSED - else STATE_OFF - ) + assert entity_state.state == entity_states[contact_id][0] + open_count = entity_states[contact_id][1] + close_count = entity_states[contact_id][2] await hass.services.async_call( SWITCH_DOMAIN, @@ -199,15 +207,7 @@ async def test_switch_relay_operation( blocking=True, ) - assert ( - mock_envoy.open_dry_contact.await_count - if target_value == STATE_CLOSED - else mock_envoy.close_dry_contact.await_count - ) == 1 - assert ( - mock_envoy.close_dry_contact.await_count - if target_value == STATE_CLOSED - else mock_envoy.open_dry_contact.await_count - ) == 0 + assert mock_envoy.open_dry_contact.await_count == open_count + assert mock_envoy.close_dry_contact.await_count == close_count mock_envoy.open_dry_contact.reset_mock() mock_envoy.close_dry_contact.reset_mock() From 6752bd450b6a7759b740b3f05692a90dc48c76c5 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 27 Jul 2024 17:41:42 +0200 Subject: [PATCH 1618/2411] Use snapshot in Axis light tests (#122703) --- .../components/axis/snapshots/test_light.ambr | 57 +++++++++++++++++++ tests/components/axis/test_light.py | 27 +++++---- 2 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 tests/components/axis/snapshots/test_light.ambr diff --git a/tests/components/axis/snapshots/test_light.ambr b/tests/components/axis/snapshots/test_light.ambr new file mode 100644 index 00000000000..b37da39fe27 --- /dev/null +++ b/tests/components/axis/snapshots/test_light.ambr @@ -0,0 +1,57 @@ +# serializer version: 1 +# name: test_lights[api_discovery_items0][light.home_ir_light_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.home_ir_light_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'IR Light 0', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tns1:Device/tnsaxis:Light/Status-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_lights[api_discovery_items0][light.home_ir_light_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 170, + 'color_mode': , + 'friendly_name': 'home IR Light 0', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.home_ir_light_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index 9f68aa6fdd3..c33af5ec3a4 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -6,6 +6,7 @@ from unittest.mock import patch from axis.models.api import CONTEXT import pytest import respx +from syrupy import SnapshotAssertion from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN from homeassistant.const import ( @@ -13,13 +14,16 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, - STATE_ON, + Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er -from .conftest import RtspEventMock +from .conftest import ConfigEntryFactoryType, RtspEventMock from .const import DEFAULT_HOST, NAME +from tests.common import snapshot_platform + API_DISCOVERY_LIGHT_CONTROL = { "id": "light-control", "version": "1.1", @@ -88,8 +92,13 @@ async def test_no_light_entity_without_light_control_representation( @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_LIGHT_CONTROL]) -@pytest.mark.usefixtures("config_entry_setup") -async def test_lights(hass: HomeAssistant, mock_rtsp_event: RtspEventMock) -> None: +async def test_lights( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory: ConfigEntryFactoryType, + mock_rtsp_event: RtspEventMock, + snapshot: SnapshotAssertion, +) -> None: """Test that lights are loaded properly.""" # Add light respx.post( @@ -125,6 +134,9 @@ async def test_lights(hass: HomeAssistant, mock_rtsp_event: RtspEventMock) -> No }, ) + with patch("homeassistant.components.axis.PLATFORMS", [Platform.LIGHT]): + config_entry = await config_entry_factory() + mock_rtsp_event( topic="tns1:Device/tnsaxis:Light/Status", data_type="state", @@ -133,15 +145,10 @@ async def test_lights(hass: HomeAssistant, mock_rtsp_event: RtspEventMock) -> No source_idx="0", ) await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1 + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) entity_id = f"{LIGHT_DOMAIN}.{NAME}_ir_light_0" - light_0 = hass.states.get(entity_id) - assert light_0.state == STATE_ON - assert light_0.name == f"{NAME} IR Light 0" - # Turn on, set brightness, light already on with ( patch("axis.interfaces.vapix.LightHandler.activate_light") as mock_activate, From 383dd80919a120a9030d7f9efb659cf8d5821655 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Jul 2024 12:13:11 -0500 Subject: [PATCH 1619/2411] Bump aiohomekit to 3.2.1 (#122704) --- 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 6b30439aa77..476d17d3515 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.2.0"], + "requirements": ["aiohomekit==3.2.1"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index bcb1a52d3ab..3e75a65cc32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -255,7 +255,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.2.0 +aiohomekit==3.2.1 # homeassistant.components.hue aiohue==4.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8ad506f418..84bdbc00d6c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -240,7 +240,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.2.0 +aiohomekit==3.2.1 # homeassistant.components.hue aiohue==4.7.1 From e708e30c330f1611aaafb2292eff525fbeaf7f22 Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Sun, 28 Jul 2024 00:11:42 +0300 Subject: [PATCH 1620/2411] Bump pyswitchbee to 1.8.3 (#122713) * Bump pyswitchbee to 1.8.3 * fix license --- homeassistant/components/switchbee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/licenses.py | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switchbee/manifest.json b/homeassistant/components/switchbee/manifest.json index 2175f28eede..2e7b15e0561 100644 --- a/homeassistant/components/switchbee/manifest.json +++ b/homeassistant/components/switchbee/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/switchbee", "iot_class": "local_push", - "requirements": ["pyswitchbee==1.8.0"] + "requirements": ["pyswitchbee==1.8.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3e75a65cc32..9b23b8d1580 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2211,7 +2211,7 @@ pystiebeleltron==0.0.1.dev2 pysuez==0.2.0 # homeassistant.components.switchbee -pyswitchbee==1.8.0 +pyswitchbee==1.8.3 # homeassistant.components.tautulli pytautulli==23.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 84bdbc00d6c..ca9fec8ea6e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1759,7 +1759,7 @@ pysqueezebox==0.7.1 pysuez==0.2.0 # homeassistant.components.switchbee -pyswitchbee==1.8.0 +pyswitchbee==1.8.3 # homeassistant.components.tautulli pytautulli==23.1.1 diff --git a/script/licenses.py b/script/licenses.py index 358e0e03791..fc6ba8b2b7d 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -162,7 +162,6 @@ EXCEPTIONS = { "pyeconet", # https://github.com/w1ll1am23/pyeconet/pull/41 "pylutron-caseta", # https://github.com/gurumitts/pylutron-caseta/pull/168 "pysabnzbd", # https://github.com/jeradM/pysabnzbd/pull/6 - "pyswitchbee", # https://github.com/jafar-atili/pySwitchbee/pull/5 "pyvera", # https://github.com/maximvelichko/pyvera/pull/164 "pyxeoma", # https://github.com/jeradM/pyxeoma/pull/11 "repoze.lru", From ec15a66a68f94b4cbf72907ba3d45c642c1cb6da Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sun, 28 Jul 2024 09:37:38 +0200 Subject: [PATCH 1621/2411] Bump ruff to 0.5.5 (#122722) --- .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 c711f98f5d6..22e10d420d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.4 + rev: v0.5.5 hooks: - id: ruff args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index f7c7a18f3f3..d57a005bb5d 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.3.0 -ruff==0.5.4 +ruff==0.5.5 yamllint==1.35.1 From f563817b98edb659dabe78d1cf5ee0d58e427fca Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Sun, 28 Jul 2024 11:18:21 +0300 Subject: [PATCH 1622/2411] Bump pyElectra to 1.2.4 (#122724) * Bump PyElectra to 1.2.3 * one more thing * Bump PyElectra to 1.2.4 * fixed pyElectra license --- homeassistant/components/electrasmart/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/licenses.py | 3 --- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/electrasmart/manifest.json b/homeassistant/components/electrasmart/manifest.json index f19aeb3d947..f21f02b8cfe 100644 --- a/homeassistant/components/electrasmart/manifest.json +++ b/homeassistant/components/electrasmart/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/electrasmart", "iot_class": "cloud_polling", - "requirements": ["pyElectra==1.2.3"] + "requirements": ["pyElectra==1.2.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9b23b8d1580..70db08e36aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1674,7 +1674,7 @@ pyControl4==1.1.0 pyDuotecno==2024.5.1 # homeassistant.components.electrasmart -pyElectra==1.2.3 +pyElectra==1.2.4 # homeassistant.components.emby pyEmby==1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca9fec8ea6e..bf52db6d9a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1357,7 +1357,7 @@ pyControl4==1.1.0 pyDuotecno==2024.5.1 # homeassistant.components.electrasmart -pyElectra==1.2.3 +pyElectra==1.2.4 # homeassistant.components.rfxtrx pyRFXtrx==0.31.1 diff --git a/script/licenses.py b/script/licenses.py index fc6ba8b2b7d..f2298e473a2 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -187,9 +187,6 @@ TODO = { "mficlient": AwesomeVersion( "0.3.0" ), # No license https://github.com/kk7ds/mficlient/issues/4 - "pyElectra": AwesomeVersion( - "1.2.3" - ), # No License https://github.com/jafar-atili/pyElectra/issues/3 "pyflic": AwesomeVersion("2.0.3"), # No OSI approved license CC0-1.0 Universal) "uvcclient": AwesomeVersion( "0.11.0" From 146ec4e760e12b0e9aecb8cea735cfd8af7b6306 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 28 Jul 2024 18:28:42 +1000 Subject: [PATCH 1623/2411] Create theme select entities on matrix devices (#122695) Signed-off-by: Avi Miller --- homeassistant/components/lifx/select.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lifx/select.py b/homeassistant/components/lifx/select.py index ef2967d1776..de3a5b431a9 100644 --- a/homeassistant/components/lifx/select.py +++ b/homeassistant/components/lifx/select.py @@ -50,7 +50,10 @@ async def async_setup_entry( LIFXInfraredBrightnessSelectEntity(coordinator, INFRARED_BRIGHTNESS_ENTITY) ) - if lifx_features(coordinator.device)["multizone"] is True: + if ( + lifx_features(coordinator.device)["multizone"] is True + or lifx_features(coordinator.device)["matrix"] is True + ): entities.append(LIFXThemeSelectEntity(coordinator, THEME_ENTITY)) async_add_entities(entities) From 3ad2456dd92833e8b62726e39da8e8deb63c5f5b Mon Sep 17 00:00:00 2001 From: "Phill (pssc)" Date: Sun, 28 Jul 2024 09:39:41 +0100 Subject: [PATCH 1624/2411] Add yamaha platform retry if receiver unavailable at setup (#122679) * Add platform retry if recieiver unavailable at setup * Fix Excpetion handling after testing --- homeassistant/components/yamaha/media_player.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 61077d648d2..bf217f12ca4 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -18,6 +18,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -157,7 +158,10 @@ async def async_setup_platform( # Get the Infos for configuration from config (YAML) or Discovery config_info = YamahaConfigInfo(config=config, discovery_info=discovery_info) # Async check if the Receivers are there in the network - zone_ctrls = await hass.async_add_executor_job(_discovery, config_info) + try: + zone_ctrls = await hass.async_add_executor_job(_discovery, config_info) + except requests.exceptions.ConnectionError as ex: + raise PlatformNotReady(f"Issue while connecting to {config_info.name}") from ex entities = [] for zctrl in zone_ctrls: From 092ab823d1087f9f1a755c9a77f4e7824cda4f1f Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 28 Jul 2024 11:06:32 +0200 Subject: [PATCH 1625/2411] Add device info for legacy Ecovacs bots (#122671) * add device info * add tests --- homeassistant/components/ecovacs/vacuum.py | 13 ++++++++++- tests/components/ecovacs/conftest.py | 21 +++++++++++++++++ .../ecovacs/fixtures/devices/123/device.json | 23 +++++++++++++++++++ tests/components/ecovacs/test_init.py | 5 +++- 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 tests/components/ecovacs/fixtures/devices/123/device.json diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index 401274609d8..16cf5687720 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -26,6 +26,7 @@ from homeassistant.components.vacuum import ( from homeassistant.core import HomeAssistant, SupportsResponse from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_platform +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify @@ -75,6 +76,7 @@ class EcovacsLegacyVacuum(StateVacuumEntity): """Legacy Ecovacs vacuums.""" _attr_fan_speed_list = [sucks.FAN_SPEED_NORMAL, sucks.FAN_SPEED_HIGH] + _attr_has_entity_name = True _attr_should_poll = False _attr_supported_features = ( VacuumEntityFeature.BATTERY @@ -95,7 +97,16 @@ class EcovacsLegacyVacuum(StateVacuumEntity): self.error: str | None = None self._attr_unique_id = vacuum["did"] - self._attr_name = vacuum.get("nick", vacuum["did"]) + + if (name := vacuum.get("nick")) is None: + name = vacuum.get("name", vacuum["did"]) + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, vacuum["did"])}, + model=vacuum.get("deviceName"), + name=name, + serial_number=vacuum["did"], + ) async def async_added_to_hass(self) -> None: """Set up the event listeners now that hass is ready.""" diff --git a/tests/components/ecovacs/conftest.py b/tests/components/ecovacs/conftest.py index 59721b65563..e53cfcc8a3d 100644 --- a/tests/components/ecovacs/conftest.py +++ b/tests/components/ecovacs/conftest.py @@ -117,6 +117,27 @@ def mock_mqtt_client(mock_authenticator: Mock) -> Generator[Mock]: yield client +@pytest.fixture +def mock_vacbot(device_fixture: str) -> Generator[Mock]: + """Mock the legacy VacBot.""" + with patch( + "homeassistant.components.ecovacs.controller.VacBot", + autospec=True, + ) as mock: + vacbot = mock.return_value + vacbot.vacuum = load_json_object_fixture( + f"devices/{device_fixture}/device.json", DOMAIN + ) + vacbot.statusEvents = Mock() + vacbot.batteryEvents = Mock() + vacbot.lifespanEvents = Mock() + vacbot.errorEvents = Mock() + vacbot.battery_status = None + vacbot.fan_speed = None + vacbot.components = {} + yield vacbot + + @pytest.fixture def mock_device_execute() -> Generator[AsyncMock]: """Mock the device execute function.""" diff --git a/tests/components/ecovacs/fixtures/devices/123/device.json b/tests/components/ecovacs/fixtures/devices/123/device.json new file mode 100644 index 00000000000..07bdf01b156 --- /dev/null +++ b/tests/components/ecovacs/fixtures/devices/123/device.json @@ -0,0 +1,23 @@ +{ + "did": "E1234567890000000003", + "name": "E1234567890000000003", + "class": "123", + "resource": "atom", + "company": "eco-legacy", + "deviceName": "DEEBOT Slim2 Series", + "icon": "https://portal-ww.ecouser.net/api/pim/file/get/5d2c150dba13eb00013feaae", + "ota": false, + "UILogicId": "ECO_INTL_123", + "materialNo": "110-1639-0102", + "pid": "5cae9b201285190001685977", + "product_category": "DEEBOT", + "model": "Slim2", + "updateInfo": { + "needUpdate": false, + "changeLog": "" + }, + "nick": null, + "homeSort": 9999, + "status": 2, + "otaUpgrade": {} +} diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py index 27d00a2d023..c0c475217c1 100644 --- a/tests/components/ecovacs/test_init.py +++ b/tests/components/ecovacs/test_init.py @@ -129,12 +129,15 @@ async def test_devices_in_dr( assert device_entry == snapshot(name=device.device_info["did"]) -@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration") +@pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "mock_vacbot", "init_integration" +) @pytest.mark.parametrize( ("device_fixture", "entities"), [ ("yna5x1", 26), ("5xu9h3", 24), + ("123", 1), ], ) async def test_all_entities_loaded( From ac0d0b21e20534e89590868284f690f6990c9011 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 28 Jul 2024 11:50:00 +0200 Subject: [PATCH 1626/2411] Bump github/codeql-action from 3.25.13 to 3.25.14 (#122632) --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fd37005a59f..b199ea09bb3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.1.7 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.25.13 + uses: github/codeql-action/init@v3.25.14 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.25.13 + uses: github/codeql-action/analyze@v3.25.14 with: category: "/language:python" From d4aa981fd79293f8321c829282349f539be3be01 Mon Sep 17 00:00:00 2001 From: Bill Flood Date: Sun, 28 Jul 2024 05:07:00 -0700 Subject: [PATCH 1627/2411] Bump mopeka-iot-ble to version 0.8.0 (#122717) --- homeassistant/components/mopeka/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mopeka/manifest.json b/homeassistant/components/mopeka/manifest.json index 82afd4d2057..ee644c16c15 100644 --- a/homeassistant/components/mopeka/manifest.json +++ b/homeassistant/components/mopeka/manifest.json @@ -63,5 +63,5 @@ "documentation": "https://www.home-assistant.io/integrations/mopeka", "integration_type": "device", "iot_class": "local_push", - "requirements": ["mopeka-iot-ble==0.7.0"] + "requirements": ["mopeka-iot-ble==0.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 70db08e36aa..9c9a99ef8af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1348,7 +1348,7 @@ moehlenhoff-alpha2==1.3.1 monzopy==1.3.0 # homeassistant.components.mopeka -mopeka-iot-ble==0.7.0 +mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds motionblinds==0.6.23 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf52db6d9a2..c70f53ca781 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1114,7 +1114,7 @@ moehlenhoff-alpha2==1.3.1 monzopy==1.3.0 # homeassistant.components.mopeka -mopeka-iot-ble==0.7.0 +mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds motionblinds==0.6.23 From ba266ab13c0baa23096476815f07e2a60ec12779 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Jul 2024 07:11:56 -0500 Subject: [PATCH 1628/2411] Add coverage for calling doorbird webhook with the wrong token (#122700) --- tests/components/doorbird/test_view.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/components/doorbird/test_view.py diff --git a/tests/components/doorbird/test_view.py b/tests/components/doorbird/test_view.py new file mode 100644 index 00000000000..9d2b53714b6 --- /dev/null +++ b/tests/components/doorbird/test_view.py @@ -0,0 +1,21 @@ +"""Test DoorBird view.""" + +from http import HTTPStatus + +from homeassistant.components.doorbird.const import API_URL + +from .conftest import DoorbirdMockerType + +from tests.typing import ClientSessionGenerator + + +async def test_non_webhook_with_wrong_token( + hass_client: ClientSessionGenerator, + doorbird_mocker: DoorbirdMockerType, +) -> None: + """Test calling the webhook with the wrong token.""" + await doorbird_mocker() + client = await hass_client() + + response = await client.get(f"{API_URL}/doorbell?token=wrong") + assert response.status == HTTPStatus.UNAUTHORIZED From d765b92cca447c293e7ca50083cb8f66d24ead07 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 28 Jul 2024 15:01:34 +0200 Subject: [PATCH 1629/2411] Unsubscribe event listeners on remove of Ecovacs legacy bot entities (#122731) * unsubscribe on entity remove, create base EcovacsLegacyEntity * fix name and model in device info * apply suggestion * add manufacturer to device info * fix device info --- homeassistant/components/ecovacs/entity.py | 34 ++++++++++++++++ homeassistant/components/ecovacs/vacuum.py | 45 +++++++++------------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/ecovacs/entity.py b/homeassistant/components/ecovacs/entity.py index 5b586eaf9ef..9d3092f37b4 100644 --- a/homeassistant/components/ecovacs/entity.py +++ b/homeassistant/components/ecovacs/entity.py @@ -10,6 +10,7 @@ from deebot_client.capabilities import Capabilities from deebot_client.device import Device from deebot_client.events import AvailabilityEvent from deebot_client.events.base import Event +from sucks import EventListener, VacBot from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo @@ -120,3 +121,36 @@ class EcovacsCapabilityEntityDescription( """Ecovacs entity description.""" capability_fn: Callable[[Capabilities], CapabilityEntity | None] + + +class EcovacsLegacyEntity(Entity): + """Ecovacs legacy bot entity.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__(self, device: VacBot) -> None: + """Initialize the legacy Ecovacs entity.""" + self.device = device + vacuum = device.vacuum + + self.error: str | None = None + self._attr_unique_id = vacuum["did"] + + if (name := vacuum.get("nick")) is None: + name = vacuum["did"] + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, vacuum["did"])}, + manufacturer="Ecovacs", + model=vacuum.get("deviceName"), + name=name, + serial_number=vacuum["did"], + ) + + self._event_listeners: list[EventListener] = [] + + async def async_will_remove_from_hass(self) -> None: + """Remove event listeners on entity remove.""" + for listener in self._event_listeners: + listener.unsubscribe() diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index 16cf5687720..ba94df0cb5f 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -26,14 +26,13 @@ from homeassistant.components.vacuum import ( from homeassistant.core import HomeAssistant, SupportsResponse from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_platform -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify from . import EcovacsConfigEntry from .const import DOMAIN -from .entity import EcovacsEntity +from .entity import EcovacsEntity, EcovacsLegacyEntity from .util import get_name_key _LOGGER = logging.getLogger(__name__) @@ -72,12 +71,10 @@ async def async_setup_entry( ) -class EcovacsLegacyVacuum(StateVacuumEntity): +class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity): """Legacy Ecovacs vacuums.""" _attr_fan_speed_list = [sucks.FAN_SPEED_NORMAL, sucks.FAN_SPEED_HIGH] - _attr_has_entity_name = True - _attr_should_poll = False _attr_supported_features = ( VacuumEntityFeature.BATTERY | VacuumEntityFeature.RETURN_HOME @@ -90,30 +87,24 @@ class EcovacsLegacyVacuum(StateVacuumEntity): | VacuumEntityFeature.FAN_SPEED ) - def __init__(self, device: sucks.VacBot) -> None: - """Initialize the Ecovacs Vacuum.""" - self.device = device - vacuum = self.device.vacuum - - self.error: str | None = None - self._attr_unique_id = vacuum["did"] - - if (name := vacuum.get("nick")) is None: - name = vacuum.get("name", vacuum["did"]) - - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, vacuum["did"])}, - model=vacuum.get("deviceName"), - name=name, - serial_number=vacuum["did"], - ) - async def async_added_to_hass(self) -> None: """Set up the event listeners now that hass is ready.""" - self.device.statusEvents.subscribe(lambda _: self.schedule_update_ha_state()) - self.device.batteryEvents.subscribe(lambda _: self.schedule_update_ha_state()) - self.device.lifespanEvents.subscribe(lambda _: self.schedule_update_ha_state()) - self.device.errorEvents.subscribe(self.on_error) + self._event_listeners.append( + self.device.statusEvents.subscribe( + lambda _: self.schedule_update_ha_state() + ) + ) + self._event_listeners.append( + self.device.batteryEvents.subscribe( + lambda _: self.schedule_update_ha_state() + ) + ) + self._event_listeners.append( + self.device.lifespanEvents.subscribe( + lambda _: self.schedule_update_ha_state() + ) + ) + self._event_listeners.append(self.device.errorEvents.subscribe(self.on_error)) def on_error(self, error: str) -> None: """Handle an error event from the robot. From e5c36c8d564e6b09363aa148af9e1e97ff812c3e Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:36:36 +0200 Subject: [PATCH 1630/2411] Refactor asserts in enphase_envoy test_sensor (#122726) refactor asserts in enphase_envoy test_sensor --- tests/components/enphase_envoy/test_sensor.py | 86 ++++++++++--------- 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/tests/components/enphase_envoy/test_sensor.py b/tests/components/enphase_envoy/test_sensor.py index 1b066ca9f59..273f81173ff 100644 --- a/tests/components/enphase_envoy/test_sensor.py +++ b/tests/components/enphase_envoy/test_sensor.py @@ -88,7 +88,7 @@ async def test_sensor_production_data( for name, target in list(zip(PRODUCTION_NAMES, PRODUCTION_TARGETS, strict=False)): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == float(entity_state.state) + assert float(entity_state.state) == target PRODUCTION_PHASE_NAMES: list[str] = [ @@ -133,7 +133,7 @@ async def test_sensor_production_phase_data( zip(PRODUCTION_PHASE_NAMES, PRODUCTION_PHASE_TARGET, strict=False) ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == float(entity_state.state) + assert float(entity_state.state) == target CONSUMPTION_NAMES: tuple[str, ...] = ( @@ -176,7 +176,7 @@ async def test_sensor_consumption_data( for name, target in list(zip(CONSUMPTION_NAMES, CONSUMPTION_TARGETS, strict=False)): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == float(entity_state.state) + assert float(entity_state.state) == target CONSUMPTION_PHASE_NAMES: list[str] = [ @@ -221,7 +221,7 @@ async def test_sensor_consumption_phase_data( zip(CONSUMPTION_PHASE_NAMES, CONSUMPTION_PHASE_TARGET, strict=False) ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == float(entity_state.state) + assert float(entity_state.state) == target CT_PRODUCTION_NAMES_INT = ("meter_status_flags_active_production_ct",) @@ -256,14 +256,14 @@ async def test_sensor_production_ct_data( zip(CT_PRODUCTION_NAMES_INT, CT_PRODUCTION_TARGETS_INT, strict=False) ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == float(entity_state.state) + assert float(entity_state.state) == target CT_PRODUCTION_TARGETS_STR = (data.metering_status,) for name, target in list( zip(CT_PRODUCTION_NAMES_STR, CT_PRODUCTION_TARGETS_STR, strict=False) ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == entity_state.state + assert entity_state.state == target CT_PRODUCTION_NAMES_FLOAT_PHASE = [ @@ -313,7 +313,7 @@ async def test_sensor_production_ct_phase_data( ) ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == float(entity_state.state) + assert float(entity_state.state) == target CT_PRODUCTION_NAMES_STR_TARGET = [ phase_data.metering_status @@ -328,7 +328,7 @@ async def test_sensor_production_ct_phase_data( ) ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == entity_state.state + assert entity_state.state == target CT_CONSUMPTION_NAMES_FLOAT: tuple[str, ...] = ( @@ -378,14 +378,14 @@ async def test_sensor_consumption_ct_data( zip(CT_CONSUMPTION_NAMES_FLOAT, CT_CONSUMPTION_TARGETS_FLOAT, strict=False) ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == float(entity_state.state) + assert float(entity_state.state) == target CT_CONSUMPTION_TARGETS_STR = (data.metering_status,) for name, target in list( zip(CT_CONSUMPTION_NAMES_STR, CT_CONSUMPTION_TARGETS_STR, strict=False) ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == entity_state.state + assert entity_state.state == target CT_CONSUMPTION_NAMES_FLOAT_PHASE = [ @@ -444,7 +444,7 @@ async def test_sensor_consumption_ct_phase_data( ) ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == float(entity_state.state) + assert float(entity_state.state) == target CT_CONSUMPTION_NAMES_STR_PHASE_TARGET = [ phase_data.metering_status @@ -459,7 +459,7 @@ async def test_sensor_consumption_ct_phase_data( ) ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == entity_state.state + assert entity_state.state == target CT_STORAGE_NAMES_FLOAT = ( @@ -505,14 +505,14 @@ async def test_sensor_storage_ct_data( zip(CT_STORAGE_NAMES_FLOAT, CT_STORAGE_TARGETS_FLOAT, strict=False) ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == float(entity_state.state) + assert float(entity_state.state) == target CT_STORAGE_TARGETS_STR = (data.metering_status,) for name, target in list( zip(CT_STORAGE_NAMES_STR, CT_STORAGE_TARGETS_STR, strict=False) ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == entity_state.state + assert entity_state.state == target CT_STORAGE_NAMES_FLOAT_PHASE = [ @@ -567,7 +567,7 @@ async def test_sensor_storage_ct_phase_data( ) ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == float(entity_state.state) + assert float(entity_state.state) == target CT_STORAGE_NAMES_STR_PHASE_TARGET = [ phase_data.metering_status @@ -582,7 +582,7 @@ async def test_sensor_storage_ct_phase_data( ) ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{name}")) - assert target == entity_state.state + assert entity_state.state == target @pytest.mark.parametrize( @@ -672,11 +672,11 @@ async def test_sensor_inverter_data( for sn, inverter in mock_envoy.data.inverters.items(): assert (entity_state := hass.states.get(f"{entity_base}_{sn}")) - assert (inverter.last_report_watts) == float(entity_state.state) + assert float(entity_state.state) == (inverter.last_report_watts) assert (last_reported := hass.states.get(f"{entity_base}_{sn}_last_reported")) - assert dt_util.utc_from_timestamp( - inverter.last_report_date - ) == dt_util.parse_datetime(last_reported.state) + assert dt_util.parse_datetime( + last_reported.state + ) == dt_util.utc_from_timestamp(inverter.last_report_date) @pytest.mark.parametrize( @@ -738,7 +738,7 @@ async def test_sensor_encharge_aggregate_data( ("battery_capacity", data.max_available_capacity), ): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{target[0]}")) - assert target[1] == float(entity_state.state) + assert float(entity_state.state) == target[1] @pytest.mark.parametrize( @@ -761,19 +761,22 @@ async def test_sensor_encharge_enpower_data( ENTITY_BASE = f"{Platform.SENSOR}.enpower" assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{sn}_temperature")) - assert mock_envoy.data.enpower.temperature == round( - TemperatureConverter.convert( - float(entity_state.state), - hass.config.units.temperature_unit, - UnitOfTemperature.FAHRENHEIT - if mock_envoy.data.enpower.temperature_unit == "F" - else UnitOfTemperature.CELSIUS, + assert ( + round( + TemperatureConverter.convert( + float(entity_state.state), + hass.config.units.temperature_unit, + UnitOfTemperature.FAHRENHEIT + if mock_envoy.data.enpower.temperature_unit == "F" + else UnitOfTemperature.CELSIUS, + ) ) + == mock_envoy.data.enpower.temperature ) assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{sn}_last_reported")) - assert dt_util.utc_from_timestamp( + assert dt_util.parse_datetime(entity_state.state) == dt_util.utc_from_timestamp( mock_envoy.data.enpower.last_report_date - ) == dt_util.parse_datetime(entity_state.state) + ) @pytest.mark.parametrize( @@ -815,23 +818,26 @@ async def test_sensor_encharge_power_data( for sn, sn_target in ENCHARGE_POWER_TARGETS: for name, target in list(zip(ENCHARGE_POWER_NAMES, sn_target, strict=False)): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{sn}_{name}")) - assert target == float(entity_state.state) + assert float(entity_state.state) == target for sn, encharge_inventory in mock_envoy.data.encharge_inventory.items(): assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{sn}_temperature")) - assert encharge_inventory.temperature == round( - TemperatureConverter.convert( - float(entity_state.state), - hass.config.units.temperature_unit, - UnitOfTemperature.FAHRENHEIT - if encharge_inventory.temperature_unit == "F" - else UnitOfTemperature.CELSIUS, + assert ( + round( + TemperatureConverter.convert( + float(entity_state.state), + hass.config.units.temperature_unit, + UnitOfTemperature.FAHRENHEIT + if encharge_inventory.temperature_unit == "F" + else UnitOfTemperature.CELSIUS, + ) ) + == encharge_inventory.temperature ) assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{sn}_last_reported")) - assert dt_util.utc_from_timestamp( + assert dt_util.parse_datetime(entity_state.state) == dt_util.utc_from_timestamp( encharge_inventory.last_report_date - ) == dt_util.parse_datetime(entity_state.state) + ) def integration_disabled_entities( From dff964582bb52882805b50a5f5bd235aab35e20b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 28 Jul 2024 19:47:37 +0200 Subject: [PATCH 1631/2411] Bump reolink-aio to 0.9.6 (#122738) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index c329289790b..7289dac682c 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.9.5"] + "requirements": ["reolink-aio==0.9.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9c9a99ef8af..b4df02e398f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2471,7 +2471,7 @@ renault-api==0.2.5 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.5 +reolink-aio==0.9.6 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c70f53ca781..5b161b828dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1950,7 +1950,7 @@ renault-api==0.2.5 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.5 +reolink-aio==0.9.6 # homeassistant.components.rflink rflink==0.0.66 From f98487ef18978f70ebe5c898efec0916816688c9 Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Sun, 28 Jul 2024 20:48:20 +0200 Subject: [PATCH 1632/2411] Add config_flow to bluesound integration (#115207) * Add config flow to bluesound * update init * abort flow if connection is not possible * add to codeowners * update unique id * add async_unload_entry * add import flow * add device_info * add zeroconf * fix errors * formatting * use bluos specific zeroconf service type * implement requested changes * implement requested changes * fix test; add more tests * use AsyncMock assert functions * fix potential naming collision * move setup_services back to media_player.py * implement requested changes * add port to zeroconf flow * Fix comments --------- Co-authored-by: Joostlek --- CODEOWNERS | 3 +- .../components/bluesound/__init__.py | 80 ++++++ .../components/bluesound/config_flow.py | 150 +++++++++++ homeassistant/components/bluesound/const.py | 3 + .../components/bluesound/manifest.json | 11 +- .../components/bluesound/media_player.py | 207 ++++++++++----- .../components/bluesound/strings.json | 26 ++ homeassistant/components/bluesound/utils.py | 8 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- homeassistant/generated/zeroconf.py | 5 + requirements_test_all.txt | 3 + tests/components/bluesound/__init__.py | 1 + tests/components/bluesound/conftest.py | 103 ++++++++ .../components/bluesound/test_config_flow.py | 247 ++++++++++++++++++ 15 files changed, 778 insertions(+), 72 deletions(-) create mode 100644 homeassistant/components/bluesound/config_flow.py create mode 100644 homeassistant/components/bluesound/utils.py create mode 100644 tests/components/bluesound/__init__.py create mode 100644 tests/components/bluesound/conftest.py create mode 100644 tests/components/bluesound/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 1b9808a418a..7db252a9117 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -197,7 +197,8 @@ build.json @home-assistant/supervisor /tests/components/bluemaestro/ @bdraco /homeassistant/components/blueprint/ @home-assistant/core /tests/components/blueprint/ @home-assistant/core -/homeassistant/components/bluesound/ @thrawnarn +/homeassistant/components/bluesound/ @thrawnarn @LouisChrist +/tests/components/bluesound/ @thrawnarn @LouisChrist /homeassistant/components/bluetooth/ @bdraco /tests/components/bluetooth/ @bdraco /homeassistant/components/bluetooth_adapters/ @bdraco diff --git a/homeassistant/components/bluesound/__init__.py b/homeassistant/components/bluesound/__init__.py index 9dbe0f754fb..0912a584fce 100644 --- a/homeassistant/components/bluesound/__init__.py +++ b/homeassistant/components/bluesound/__init__.py @@ -1 +1,81 @@ """The bluesound component.""" + +from dataclasses import dataclass + +import aiohttp +from pyblu import Player, SyncStatus + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN +from .media_player import setup_services + +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + +PLATFORMS = [Platform.MEDIA_PLAYER] + + +@dataclass +class BluesoundData: + """Bluesound data class.""" + + player: Player + sync_status: SyncStatus + + +type BluesoundConfigEntry = ConfigEntry[BluesoundData] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Bluesound.""" + if DOMAIN not in hass.data: + hass.data[DOMAIN] = [] + setup_services(hass) + + return True + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: BluesoundConfigEntry +) -> bool: + """Set up the Bluesound entry.""" + host = config_entry.data[CONF_HOST] + port = config_entry.data[CONF_PORT] + session = async_get_clientsession(hass) + async with Player(host, port, session=session, default_timeout=10) as player: + try: + sync_status = await player.sync_status(timeout=1) + except TimeoutError as ex: + raise ConfigEntryNotReady( + f"Timeout while connecting to {host}:{port}" + ) from ex + except aiohttp.ClientError as ex: + raise ConfigEntryNotReady(f"Error connecting to {host}:{port}") from ex + + config_entry.runtime_data = BluesoundData(player, sync_status) + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + player = None + for player in hass.data[DOMAIN]: + if player.unique_id == config_entry.unique_id: + break + + if player is None: + return False + + player.stop_polling() + hass.data[DOMAIN].remove(player) + + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) diff --git a/homeassistant/components/bluesound/config_flow.py b/homeassistant/components/bluesound/config_flow.py new file mode 100644 index 00000000000..aae527187d2 --- /dev/null +++ b/homeassistant/components/bluesound/config_flow.py @@ -0,0 +1,150 @@ +"""Config flow for bluesound.""" + +import logging +from typing import Any + +import aiohttp +from pyblu import Player, SyncStatus +import voluptuous as vol + +from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN +from .media_player import DEFAULT_PORT +from .utils import format_unique_id + +_LOGGER = logging.getLogger(__name__) + + +class BluesoundConfigFlow(ConfigFlow, domain=DOMAIN): + """Bluesound config flow.""" + + VERSION = 1 + MINOR_VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._host: str | None = None + self._port = DEFAULT_PORT + self._sync_status: SyncStatus | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow initiated by the user.""" + errors: dict[str, str] = {} + if user_input is not None: + session = async_get_clientsession(self.hass) + async with Player( + user_input[CONF_HOST], user_input[CONF_PORT], session=session + ) as player: + try: + sync_status = await player.sync_status(timeout=1) + except (TimeoutError, aiohttp.ClientError): + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id( + format_unique_id(sync_status.mac, user_input[CONF_PORT]) + ) + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: user_input[CONF_HOST], + } + ) + + return self.async_create_entry( + title=sync_status.name, + data=user_input, + ) + + return self.async_show_form( + step_id="user", + errors=errors, + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=11000): int, + } + ), + ) + + async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: + """Import bluesound config entry from configuration.yaml.""" + session = async_get_clientsession(self.hass) + async with Player( + import_data[CONF_HOST], import_data[CONF_PORT], session=session + ) as player: + try: + sync_status = await player.sync_status(timeout=1) + except (TimeoutError, aiohttp.ClientError): + return self.async_abort(reason="cannot_connect") + + await self.async_set_unique_id( + format_unique_id(sync_status.mac, import_data[CONF_PORT]) + ) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=sync_status.name, + data=import_data, + ) + + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> ConfigFlowResult: + """Handle a flow initialized by zeroconf discovery.""" + if discovery_info.port is not None: + self._port = discovery_info.port + + session = async_get_clientsession(self.hass) + try: + async with Player( + discovery_info.host, self._port, session=session + ) as player: + sync_status = await player.sync_status(timeout=1) + except (TimeoutError, aiohttp.ClientError): + return self.async_abort(reason="cannot_connect") + + await self.async_set_unique_id(format_unique_id(sync_status.mac, self._port)) + + self._host = discovery_info.host + self._sync_status = sync_status + + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: self._host, + } + ) + + self.context.update( + { + "title_placeholders": {"name": sync_status.name}, + "configuration_url": f"http://{discovery_info.host}", + } + ) + return await self.async_step_confirm() + + async def async_step_confirm(self, user_input=None) -> ConfigFlowResult: + """Confirm the zeroconf setup.""" + assert self._sync_status is not None + assert self._host is not None + + if user_input is not None: + return self.async_create_entry( + title=self._sync_status.name, + data={ + CONF_HOST: self._host, + CONF_PORT: self._port, + }, + ) + + return self.async_show_form( + step_id="confirm", + description_placeholders={ + "name": self._sync_status.name, + "host": self._host, + }, + ) diff --git a/homeassistant/components/bluesound/const.py b/homeassistant/components/bluesound/const.py index ae5291c6513..b7da4e31702 100644 --- a/homeassistant/components/bluesound/const.py +++ b/homeassistant/components/bluesound/const.py @@ -1,7 +1,10 @@ """Constants for the Bluesound HiFi wireless speakers and audio integrations component.""" DOMAIN = "bluesound" +INTEGRATION_TITLE = "Bluesound" SERVICE_CLEAR_TIMER = "clear_sleep_timer" SERVICE_JOIN = "join" SERVICE_SET_TIMER = "set_sleep_timer" SERVICE_UNJOIN = "unjoin" +ATTR_BLUESOUND_GROUP = "bluesound_group" +ATTR_MASTER = "master" diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index e41a2ac21b9..64b8e8abffc 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -1,8 +1,15 @@ { "domain": "bluesound", "name": "Bluesound", - "codeowners": ["@thrawnarn"], + "after_dependencies": ["zeroconf"], + "codeowners": ["@thrawnarn", "@LouisChrist"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bluesound", "iot_class": "local_polling", - "requirements": ["pyblu==0.4.0"] + "requirements": ["pyblu==0.4.0"], + "zeroconf": [ + { + "type": "_musc._tcp.local." + } + ] } diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 52bbf813dcc..e40a20f888a 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -4,10 +4,11 @@ from __future__ import annotations import asyncio from asyncio import CancelledError +from collections.abc import Callable from contextlib import suppress from datetime import datetime, timedelta import logging -from typing import Any, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from aiohttp.client_exceptions import ClientError from pyblu import Input, Player, Preset, Status, SyncStatus @@ -23,6 +24,7 @@ from homeassistant.components.media_player import ( MediaType, async_process_play_media_url, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -32,11 +34,16 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + Event, + HomeAssistant, + ServiceCall, + callback, +) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -44,19 +51,23 @@ from homeassistant.util import Throttle import homeassistant.util.dt as dt_util from .const import ( + ATTR_BLUESOUND_GROUP, + ATTR_MASTER, DOMAIN, + INTEGRATION_TITLE, SERVICE_CLEAR_TIMER, SERVICE_JOIN, SERVICE_SET_TIMER, SERVICE_UNJOIN, ) +from .utils import format_unique_id + +if TYPE_CHECKING: + from . import BluesoundConfigEntry _LOGGER = logging.getLogger(__name__) -ATTR_BLUESOUND_GROUP = "bluesound_group" -ATTR_MASTER = "master" - -DATA_BLUESOUND = "bluesound" +DATA_BLUESOUND = DOMAIN DEFAULT_PORT = 11000 NODE_OFFLINE_CHECK_TIMEOUT = 180 @@ -83,6 +94,10 @@ PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( } ) +BS_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) + +BS_JOIN_SCHEMA = BS_SCHEMA.extend({vol.Required(ATTR_MASTER): cv.entity_id}) + class ServiceMethodDetails(NamedTuple): """Details for SERVICE_TO_METHOD mapping.""" @@ -91,10 +106,6 @@ class ServiceMethodDetails(NamedTuple): schema: vol.Schema -BS_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) - -BS_JOIN_SCHEMA = BS_SCHEMA.extend({vol.Required(ATTR_MASTER): cv.entity_id}) - SERVICE_TO_METHOD = { SERVICE_JOIN: ServiceMethodDetails(method="async_join", schema=BS_JOIN_SCHEMA), SERVICE_UNJOIN: ServiceMethodDetails(method="async_unjoin", schema=BS_SCHEMA), @@ -107,34 +118,41 @@ SERVICE_TO_METHOD = { } -def _add_player(hass: HomeAssistant, async_add_entities, host, port=None, name=None): +def _add_player( + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + host: str, + port: int, + player: Player, + sync_status: SyncStatus, +): """Add Bluesound players.""" @callback - def _init_player(event=None): + def _init_bluesound_player(event: Event | None = None): """Start polling.""" - hass.async_create_task(player.async_init()) + hass.async_create_task(bluesound_player.async_init()) @callback - def _start_polling(event=None): + def _start_polling(event: Event | None = None): """Start polling.""" - player.start_polling() + bluesound_player.start_polling() @callback - def _stop_polling(event=None): + def _stop_polling(event: Event | None = None): """Stop polling.""" - player.stop_polling() + bluesound_player.stop_polling() @callback - def _add_player_cb(): + def _add_bluesound_player_cb(): """Add player after first sync fetch.""" - if player.id in [x.id for x in hass.data[DATA_BLUESOUND]]: - _LOGGER.warning("Player already added %s", player.id) + if bluesound_player.id in [x.id for x in hass.data[DATA_BLUESOUND]]: + _LOGGER.warning("Player already added %s", bluesound_player.id) return - hass.data[DATA_BLUESOUND].append(player) - async_add_entities([player]) - _LOGGER.info("Added device with name: %s", player.name) + hass.data[DATA_BLUESOUND].append(bluesound_player) + async_add_entities([bluesound_player]) + _LOGGER.debug("Added device with name: %s", bluesound_player.name) if hass.is_running: _start_polling() @@ -143,42 +161,61 @@ def _add_player(hass: HomeAssistant, async_add_entities, host, port=None, name=N hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_polling) - player = BluesoundPlayer(hass, host, port, name, _add_player_cb) + bluesound_player = BluesoundPlayer( + hass, host, port, player, sync_status, _add_bluesound_player_cb + ) if hass.is_running: - _init_player() + _init_bluesound_player() else: - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_player) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_bluesound_player) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Bluesound platforms.""" - if DATA_BLUESOUND not in hass.data: - hass.data[DATA_BLUESOUND] = [] - - if discovery_info: - _add_player( - hass, - async_add_entities, - discovery_info.get(CONF_HOST), - discovery_info.get(CONF_PORT), +async def _async_import(hass: HomeAssistant, config: ConfigType) -> None: + """Import config entry from configuration.yaml.""" + if not hass.config_entries.async_entries(DOMAIN): + # Start import flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) - return - - if hosts := config.get(CONF_HOSTS): - for host in hosts: - _add_player( + if ( + result["type"] == FlowResultType.ABORT + and result["reason"] == "cannot_connect" + ): + ir.async_create_issue( hass, - async_add_entities, - host.get(CONF_HOST), - host.get(CONF_PORT), - host.get(CONF_NAME), + DOMAIN, + f"deprecated_yaml_import_issue_{result['reason']}", + breaks_in_ha_version="2025.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=ir.IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": INTEGRATION_TITLE, + }, ) + return + + ir.async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2025.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": INTEGRATION_TITLE, + }, + ) + + +def setup_services(hass: HomeAssistant) -> None: + """Set up services for Bluesound component.""" async def async_service_handler(service: ServiceCall) -> None: """Map services to method of Bluesound devices.""" @@ -190,12 +227,10 @@ async def async_setup_platform( } if entity_ids := service.data.get(ATTR_ENTITY_ID): target_players = [ - player - for player in hass.data[DATA_BLUESOUND] - if player.entity_id in entity_ids + player for player in hass.data[DOMAIN] if player.entity_id in entity_ids ] else: - target_players = hass.data[DATA_BLUESOUND] + target_players = hass.data[DOMAIN] for player in target_players: await getattr(player, method.method)(**params) @@ -206,20 +241,61 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: BluesoundConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Bluesound entry.""" + host = config_entry.data[CONF_HOST] + port = config_entry.data[CONF_PORT] + + _add_player( + hass, + async_add_entities, + host, + port, + config_entry.runtime_data.player, + config_entry.runtime_data.sync_status, + ) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None, +) -> None: + """Trigger import flows.""" + hosts = config.get(CONF_HOSTS, []) + for host in hosts: + import_data = { + CONF_HOST: host[CONF_HOST], + CONF_PORT: host.get(CONF_PORT, 11000), + } + hass.async_create_task(_async_import(hass, import_data)) + + class BluesoundPlayer(MediaPlayerEntity): """Representation of a Bluesound Player.""" _attr_media_content_type = MediaType.MUSIC def __init__( - self, hass: HomeAssistant, host, port=None, name=None, init_callback=None + self, + hass: HomeAssistant, + host: str, + port: int, + player: Player, + sync_status: SyncStatus, + init_callback: Callable[[], None], ) -> None: """Initialize the media player.""" self.host = host self._hass = hass self.port = port self._polling_task = None # The actual polling task. - self._name = name + self._name = sync_status.name self._id = None self._last_status_update = None self._sync_status: SyncStatus | None = None @@ -234,15 +310,10 @@ class BluesoundPlayer(MediaPlayerEntity): self._group_name = None self._group_list: list[str] = [] self._bluesound_device_name = None - self._player = Player( - host, port, async_get_clientsession(hass), default_timeout=10 - ) + self._player = player self._init_callback = init_callback - if self.port is None: - self.port = DEFAULT_PORT - @staticmethod def _try_get_index(string, search_string): """Get the index.""" @@ -388,10 +459,10 @@ class BluesoundPlayer(MediaPlayerEntity): raise @property - def unique_id(self) -> str | None: + def unique_id(self) -> str: """Return an unique ID.""" assert self._sync_status is not None - return f"{format_mac(self._sync_status.mac)}-{self.port}" + return format_unique_id(self._sync_status.mac, self.port) async def async_trigger_sync_on_all(self): """Trigger sync status update on all devices.""" diff --git a/homeassistant/components/bluesound/strings.json b/homeassistant/components/bluesound/strings.json index f41c34a7449..c85014fedc3 100644 --- a/homeassistant/components/bluesound/strings.json +++ b/homeassistant/components/bluesound/strings.json @@ -1,4 +1,30 @@ { + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "host": "Hostname or IP address of your Bluesound player", + "port": "Port of your Bluesound player. This is usually 11000." + } + }, + "confirm": { + "title": "Discover Bluesound player", + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + }, "services": { "join": { "name": "Join", diff --git a/homeassistant/components/bluesound/utils.py b/homeassistant/components/bluesound/utils.py new file mode 100644 index 00000000000..89a6fd1e787 --- /dev/null +++ b/homeassistant/components/bluesound/utils.py @@ -0,0 +1,8 @@ +"""Utility functions for the Bluesound component.""" + +from homeassistant.helpers.device_registry import format_mac + + +def format_unique_id(mac: str, port: int) -> str: + """Generate a unique ID based on the MAC address and port number.""" + return f"{format_mac(mac)}-{port}" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 90f9675339b..1cea31202bc 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -82,6 +82,7 @@ FLOWS = { "blink", "blue_current", "bluemaestro", + "bluesound", "bluetooth", "bmw_connected_drive", "bond", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index dc1d203856c..74efe96dd2d 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -725,7 +725,7 @@ "bluesound": { "name": "Bluesound", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "bluetooth": { diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index c53add1814d..7cd60da2d0e 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -651,6 +651,11 @@ ZEROCONF = { "name": "yeelink-*", }, ], + "_musc._tcp.local.": [ + { + "domain": "bluesound", + }, + ], "_nanoleafapi._tcp.local.": [ { "domain": "nanoleaf", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b161b828dd..a949a12623f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1405,6 +1405,9 @@ pybalboa==1.0.2 # homeassistant.components.blackbird pyblackbird==0.6 +# homeassistant.components.bluesound +pyblu==0.4.0 + # homeassistant.components.neato pybotvac==0.0.25 diff --git a/tests/components/bluesound/__init__.py b/tests/components/bluesound/__init__.py new file mode 100644 index 00000000000..f8a3701422e --- /dev/null +++ b/tests/components/bluesound/__init__.py @@ -0,0 +1 @@ +"""Tests for the Bluesound integration.""" diff --git a/tests/components/bluesound/conftest.py b/tests/components/bluesound/conftest.py new file mode 100644 index 00000000000..02c73bcd62f --- /dev/null +++ b/tests/components/bluesound/conftest.py @@ -0,0 +1,103 @@ +"""Common fixtures for the Bluesound tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from pyblu import SyncStatus +import pytest + +from homeassistant.components.bluesound.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture +def sync_status() -> SyncStatus: + """Return a sync status object.""" + return SyncStatus( + etag="etag", + id="1.1.1.1:11000", + mac="00:11:22:33:44:55", + name="player-name", + image="invalid_url", + initialized=True, + brand="brand", + model="model", + model_name="model-name", + volume_db=0.5, + volume=50, + group=None, + master=None, + slaves=None, + zone=None, + zone_master=None, + zone_slave=None, + mute_volume_db=None, + mute_volume=None, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.bluesound.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Return a mocked config entry.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.2", + CONF_PORT: 11000, + }, + unique_id="00:11:22:33:44:55-11000", + ) + mock_entry.add_to_hass(hass) + + return mock_entry + + +@pytest.fixture +def mock_player() -> Generator[AsyncMock]: + """Mock the player.""" + with ( + patch( + "homeassistant.components.bluesound.Player", autospec=True + ) as mock_player, + patch( + "homeassistant.components.bluesound.config_flow.Player", + new=mock_player, + ), + ): + player = mock_player.return_value + player.__aenter__.return_value = player + player.status.return_value = None + player.sync_status.return_value = SyncStatus( + etag="etag", + id="1.1.1.1:11000", + mac="00:11:22:33:44:55", + name="player-name", + image="invalid_url", + initialized=True, + brand="brand", + model="model", + model_name="model-name", + volume_db=0.5, + volume=50, + group=None, + master=None, + slaves=None, + zone=None, + zone_master=None, + zone_slave=None, + mute_volume_db=None, + mute_volume=None, + ) + yield player diff --git a/tests/components/bluesound/test_config_flow.py b/tests/components/bluesound/test_config_flow.py new file mode 100644 index 00000000000..32f36fcea58 --- /dev/null +++ b/tests/components/bluesound/test_config_flow.py @@ -0,0 +1,247 @@ +"""Test the Bluesound config flow.""" + +from unittest.mock import AsyncMock + +from aiohttp import ClientConnectionError + +from homeassistant.components.bluesound.const import DOMAIN +from homeassistant.components.zeroconf import ZeroconfServiceInfo +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_user_flow_success( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_player: AsyncMock +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "player-name" + assert result["data"] == {CONF_HOST: "1.1.1.1", CONF_PORT: 11000} + assert result["result"].unique_id == "00:11:22:33:44:55-11000" + + mock_setup_entry.assert_called_once() + + +async def test_user_flow_cannot_connect( + hass: HomeAssistant, mock_player: AsyncMock +) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + mock_player.sync_status.side_effect = ClientConnectionError + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + assert result["step_id"] == "user" + + mock_player.sync_status.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "player-name" + assert result["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: 11000, + } + + +async def test_user_flow_aleady_configured( + hass: HomeAssistant, + mock_player: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we handle already configured.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PORT: 11000, + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert mock_config_entry.data[CONF_HOST] == "1.1.1.1" + + mock_player.sync_status.assert_called_once() + + +async def test_import_flow_success( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_player: AsyncMock +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_HOST: "1.1.1.1", CONF_PORT: 11000}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "player-name" + assert result["data"] == {CONF_HOST: "1.1.1.1", CONF_PORT: 11000} + assert result["result"].unique_id == "00:11:22:33:44:55-11000" + + mock_setup_entry.assert_called_once() + mock_player.sync_status.assert_called_once() + + +async def test_import_flow_cannot_connect( + hass: HomeAssistant, mock_player: AsyncMock +) -> None: + """Test we handle cannot connect error.""" + mock_player.sync_status.side_effect = ClientConnectionError + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_HOST: "1.1.1.1", CONF_PORT: 11000}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + mock_player.sync_status.assert_called_once() + + +async def test_import_flow_already_configured( + hass: HomeAssistant, + mock_player: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we handle already configured.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_HOST: "1.1.1.1", CONF_PORT: 11000}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + mock_player.sync_status.assert_called_once() + + +async def test_zeroconf_flow_success( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_player: AsyncMock +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address="1.1.1.1", + ip_addresses=["1.1.1.1"], + port=11000, + hostname="player-name", + type="_musc._tcp.local.", + name="player-name._musc._tcp.local.", + properties={}, + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm" + + mock_setup_entry.assert_not_called() + mock_player.sync_status.assert_called_once() + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "player-name" + assert result["data"] == {CONF_HOST: "1.1.1.1", CONF_PORT: 11000} + assert result["result"].unique_id == "00:11:22:33:44:55-11000" + + mock_setup_entry.assert_called_once() + + +async def test_zeroconf_flow_cannot_connect( + hass: HomeAssistant, mock_player: AsyncMock +) -> None: + """Test we handle cannot connect error.""" + mock_player.sync_status.side_effect = ClientConnectionError + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address="1.1.1.1", + ip_addresses=["1.1.1.1"], + port=11000, + hostname="player-name", + type="_musc._tcp.local.", + name="player-name._musc._tcp.local.", + properties={}, + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + mock_player.sync_status.assert_called_once() + + +async def test_zeroconf_flow_already_configured( + hass: HomeAssistant, + mock_player: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we handle already configured and update the host.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address="1.1.1.1", + ip_addresses=["1.1.1.1"], + port=11000, + hostname="player-name", + type="_musc._tcp.local.", + name="player-name._musc._tcp.local.", + properties={}, + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert mock_config_entry.data[CONF_HOST] == "1.1.1.1" + + mock_player.sync_status.assert_called_once() From 4b2073ca592962d818e642e15e6acf8286f0efd6 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Mon, 29 Jul 2024 04:19:53 +0300 Subject: [PATCH 1633/2411] Add LLM tools support for Ollama (#120454) * Add LLM tools support for Ollama * fix tests * coverage * Separate call for tool parameters * Fix example * hint on parameters schema if LLM forgot to request it * Switch to native tool call functionality * Fix tests * Fix tools list * update strings and default model * Ignore mypy error until fixed upstream * Ignore mypy error until fixed upstream * Add missing prompt part * Update default model --- .../components/ollama/config_flow.py | 38 ++- homeassistant/components/ollama/const.py | 69 +---- .../components/ollama/conversation.py | 245 +++++++++------ homeassistant/components/ollama/manifest.json | 2 +- homeassistant/components/ollama/models.py | 20 +- homeassistant/components/ollama/strings.json | 4 +- tests/components/ollama/__init__.py | 4 +- tests/components/ollama/conftest.py | 13 + .../ollama/snapshots/test_conversation.ambr | 34 ++ tests/components/ollama/test_conversation.py | 291 ++++++++++++++---- 10 files changed, 465 insertions(+), 255 deletions(-) create mode 100644 tests/components/ollama/snapshots/test_conversation.ambr diff --git a/homeassistant/components/ollama/config_flow.py b/homeassistant/components/ollama/config_flow.py index 475d5339dea..bcdd6e06f48 100644 --- a/homeassistant/components/ollama/config_flow.py +++ b/homeassistant/components/ollama/config_flow.py @@ -18,7 +18,9 @@ from homeassistant.config_entries import ( ConfigFlowResult, OptionsFlow, ) -from homeassistant.const import CONF_URL +from homeassistant.const import CONF_LLM_HASS_API, CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.helpers import llm from homeassistant.helpers.selector import ( NumberSelector, NumberSelectorConfig, @@ -40,7 +42,6 @@ from .const import ( DEFAULT_KEEP_ALIVE, DEFAULT_MAX_HISTORY, DEFAULT_MODEL, - DEFAULT_PROMPT, DEFAULT_TIMEOUT, DOMAIN, MODEL_NAMES, @@ -208,25 +209,52 @@ class OllamaOptionsFlow(OptionsFlow): ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: + if user_input[CONF_LLM_HASS_API] == "none": + user_input.pop(CONF_LLM_HASS_API) return self.async_create_entry( title=_get_title(self.model), data=user_input ) options = self.config_entry.options or MappingProxyType({}) - schema = ollama_config_option_schema(options) + schema = ollama_config_option_schema(self.hass, options) return self.async_show_form( step_id="init", data_schema=vol.Schema(schema), ) -def ollama_config_option_schema(options: MappingProxyType[str, Any]) -> dict: +def ollama_config_option_schema( + hass: HomeAssistant, options: MappingProxyType[str, Any] +) -> dict: """Ollama options schema.""" + hass_apis: list[SelectOptionDict] = [ + SelectOptionDict( + label="No control", + value="none", + ) + ] + hass_apis.extend( + SelectOptionDict( + label=api.name, + value=api.id, + ) + for api in llm.async_get_apis(hass) + ) + return { vol.Optional( CONF_PROMPT, - description={"suggested_value": options.get(CONF_PROMPT, DEFAULT_PROMPT)}, + description={ + "suggested_value": options.get( + CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT + ) + }, ): TemplateSelector(), + vol.Optional( + CONF_LLM_HASS_API, + description={"suggested_value": options.get(CONF_LLM_HASS_API)}, + default="none", + ): SelectSelector(SelectSelectorConfig(options=hass_apis)), vol.Optional( CONF_MAX_HISTORY, description={ diff --git a/homeassistant/components/ollama/const.py b/homeassistant/components/ollama/const.py index b3bce3624c2..97c4f1186fc 100644 --- a/homeassistant/components/ollama/const.py +++ b/homeassistant/components/ollama/const.py @@ -4,73 +4,6 @@ DOMAIN = "ollama" CONF_MODEL = "model" CONF_PROMPT = "prompt" -DEFAULT_PROMPT = """{%- set used_domains = set([ - "binary_sensor", - "climate", - "cover", - "fan", - "light", - "lock", - "sensor", - "switch", - "weather", -]) %} -{%- set used_attributes = set([ - "temperature", - "current_temperature", - "temperature_unit", - "brightness", - "humidity", - "unit_of_measurement", - "device_class", - "current_position", - "percentage", -]) %} - -This smart home is controlled by Home Assistant. -The current time is {{ now().strftime("%X") }}. -Today's date is {{ now().strftime("%x") }}. - -An overview of the areas and the devices in this smart home: -```yaml -{%- for entity in exposed_entities: %} -{%- if entity.domain not in used_domains: %} - {%- continue %} -{%- endif %} - -- domain: {{ entity.domain }} -{%- if entity.names | length == 1: %} - name: {{ entity.names[0] }} -{%- else: %} - names: -{%- for name in entity.names: %} - - {{ name }} -{%- endfor %} -{%- endif %} -{%- if entity.area_names | length == 1: %} - area: {{ entity.area_names[0] }} -{%- elif entity.area_names: %} - areas: -{%- for area_name in entity.area_names: %} - - {{ area_name }} -{%- endfor %} -{%- endif %} - state: {{ entity.state.state }} - {%- set attributes_key_printed = False %} -{%- for attr_name, attr_value in entity.state.attributes.items(): %} - {%- if attr_name in used_attributes: %} - {%- if not attributes_key_printed: %} - attributes: - {%- set attributes_key_printed = True %} - {%- endif %} - {{ attr_name }}: {{ attr_value }} - {%- endif %} -{%- endfor %} -{%- endfor %} -``` - -Answer the user's questions using the information about this smart home. -Keep your answers brief and do not apologize.""" CONF_KEEP_ALIVE = "keep_alive" DEFAULT_KEEP_ALIVE = -1 # seconds. -1 = indefinite, 0 = never @@ -187,4 +120,4 @@ MODEL_NAMES = [ # https://ollama.com/library "yi", "zephyr", ] -DEFAULT_MODEL = "llama2:latest" +DEFAULT_MODEL = "llama3.1:latest" diff --git a/homeassistant/components/ollama/conversation.py b/homeassistant/components/ollama/conversation.py index ccc7b9bdecc..ae0acef1077 100644 --- a/homeassistant/components/ollama/conversation.py +++ b/homeassistant/components/ollama/conversation.py @@ -2,26 +2,23 @@ from __future__ import annotations +from collections.abc import Callable +import json import logging import time -from typing import Literal +from typing import Any, Literal import ollama +import voluptuous as vol +from voluptuous_openapi import convert from homeassistant.components import assist_pipeline, conversation from homeassistant.components.conversation import trace -from homeassistant.components.homeassistant.exposed_entities import async_should_expose from homeassistant.config_entries import ConfigEntry -from homeassistant.const import MATCH_ALL +from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL from homeassistant.core import HomeAssistant -from homeassistant.exceptions import TemplateError -from homeassistant.helpers import ( - area_registry as ar, - device_registry as dr, - entity_registry as er, - intent, - template, -) +from homeassistant.exceptions import HomeAssistantError, TemplateError +from homeassistant.helpers import intent, llm, template from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import ulid @@ -32,11 +29,13 @@ from .const import ( CONF_PROMPT, DEFAULT_KEEP_ALIVE, DEFAULT_MAX_HISTORY, - DEFAULT_PROMPT, DOMAIN, MAX_HISTORY_SECONDS, ) -from .models import ExposedEntity, MessageHistory, MessageRole +from .models import MessageHistory, MessageRole + +# Max number of back and forth with the LLM to generate a response +MAX_TOOL_ITERATIONS = 10 _LOGGER = logging.getLogger(__name__) @@ -51,6 +50,19 @@ async def async_setup_entry( async_add_entities([agent]) +def _format_tool( + tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None +) -> dict[str, Any]: + """Format tool specification.""" + tool_spec = { + "name": tool.name, + "parameters": convert(tool.parameters, custom_serializer=custom_serializer), + } + if tool.description: + tool_spec["description"] = tool.description + return {"type": "function", "function": tool_spec} + + class OllamaConversationEntity( conversation.ConversationEntity, conversation.AbstractConversationAgent ): @@ -94,6 +106,47 @@ class OllamaConversationEntity( client = self.hass.data[DOMAIN][self.entry.entry_id] conversation_id = user_input.conversation_id or ulid.ulid_now() model = settings[CONF_MODEL] + intent_response = intent.IntentResponse(language=user_input.language) + llm_api: llm.APIInstance | None = None + tools: list[dict[str, Any]] | None = None + user_name: str | None = None + llm_context = llm.LLMContext( + platform=DOMAIN, + context=user_input.context, + user_prompt=user_input.text, + language=user_input.language, + assistant=conversation.DOMAIN, + device_id=user_input.device_id, + ) + + if settings.get(CONF_LLM_HASS_API): + try: + llm_api = await llm.async_get_api( + self.hass, + settings[CONF_LLM_HASS_API], + llm_context, + ) + except HomeAssistantError as err: + _LOGGER.error("Error getting LLM API: %s", err) + intent_response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Error preparing LLM API: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=user_input.conversation_id + ) + tools = [ + _format_tool(tool, llm_api.custom_serializer) for tool in llm_api.tools + ] + + if ( + user_input.context + and user_input.context.user_id + and ( + user := await self.hass.auth.async_get_user(user_input.context.user_id) + ) + ): + user_name = user.name # Look up message history message_history: MessageHistory | None = None @@ -102,13 +155,24 @@ class OllamaConversationEntity( # New history # # Render prompt and error out early if there's a problem - raw_prompt = settings.get(CONF_PROMPT, DEFAULT_PROMPT) try: - prompt = self._generate_prompt(raw_prompt) - _LOGGER.debug("Prompt: %s", prompt) + prompt_parts = [ + template.Template( + llm.BASE_PROMPT + + settings.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT), + self.hass, + ).async_render( + { + "ha_name": self.hass.config.location_name, + "user_name": user_name, + "llm_context": llm_context, + }, + parse_result=False, + ) + ] + except TemplateError as err: _LOGGER.error("Error rendering prompt: %s", err) - intent_response = intent.IntentResponse(language=user_input.language) intent_response.async_set_error( intent.IntentResponseErrorCode.UNKNOWN, f"Sorry, I had a problem generating my prompt: {err}", @@ -117,6 +181,13 @@ class OllamaConversationEntity( response=intent_response, conversation_id=conversation_id ) + if llm_api: + prompt_parts.append(llm_api.api_prompt) + + prompt = "\n".join(prompt_parts) + _LOGGER.debug("Prompt: %s", prompt) + _LOGGER.debug("Tools: %s", tools) + message_history = MessageHistory( timestamp=time.monotonic(), messages=[ @@ -146,35 +217,66 @@ class OllamaConversationEntity( ) # Get response - try: - response = await client.chat( - model=model, - # Make a copy of the messages because we mutate the list later - messages=list(message_history.messages), - stream=False, - # keep_alive requires specifying unit. In this case, seconds - keep_alive=f"{settings.get(CONF_KEEP_ALIVE, DEFAULT_KEEP_ALIVE)}s", - ) - except (ollama.RequestError, ollama.ResponseError) as err: - _LOGGER.error("Unexpected error talking to Ollama server: %s", err) - intent_response = intent.IntentResponse(language=user_input.language) - intent_response.async_set_error( - intent.IntentResponseErrorCode.UNKNOWN, - f"Sorry, I had a problem talking to the Ollama server: {err}", - ) - return conversation.ConversationResult( - response=intent_response, conversation_id=conversation_id + # To prevent infinite loops, we limit the number of iterations + for _iteration in range(MAX_TOOL_ITERATIONS): + try: + response = await client.chat( + model=model, + # Make a copy of the messages because we mutate the list later + messages=list(message_history.messages), + tools=tools, + stream=False, + # keep_alive requires specifying unit. In this case, seconds + keep_alive=f"{settings.get(CONF_KEEP_ALIVE, DEFAULT_KEEP_ALIVE)}s", + ) + except (ollama.RequestError, ollama.ResponseError) as err: + _LOGGER.error("Unexpected error talking to Ollama server: %s", err) + intent_response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Sorry, I had a problem talking to the Ollama server: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + response_message = response["message"] + message_history.messages.append( + ollama.Message( + role=response_message["role"], + content=response_message.get("content"), + tool_calls=response_message.get("tool_calls"), + ) ) - response_message = response["message"] - message_history.messages.append( - ollama.Message( - role=response_message["role"], content=response_message["content"] - ) - ) + tool_calls = response_message.get("tool_calls") + if not tool_calls or not llm_api: + break + + for tool_call in tool_calls: + tool_input = llm.ToolInput( + tool_name=tool_call["function"]["name"], + tool_args=tool_call["function"]["arguments"], + ) + _LOGGER.debug( + "Tool call: %s(%s)", tool_input.tool_name, tool_input.tool_args + ) + + try: + tool_response = await llm_api.async_call_tool(tool_input) + except (HomeAssistantError, vol.Invalid) as e: + tool_response = {"error": type(e).__name__} + if str(e): + tool_response["error_text"] = str(e) + + _LOGGER.debug("Tool response: %s", tool_response) + message_history.messages.append( + ollama.Message( + role=MessageRole.TOOL.value, # type: ignore[typeddict-item] + content=json.dumps(tool_response), + ) + ) # Create intent response - intent_response = intent.IntentResponse(language=user_input.language) intent_response.async_set_speech(response_message["content"]) return conversation.ConversationResult( response=intent_response, conversation_id=conversation_id @@ -204,62 +306,3 @@ class OllamaConversationEntity( message_history.messages = [ message_history.messages[0] ] + message_history.messages[drop_index:] - - def _generate_prompt(self, raw_prompt: str) -> str: - """Generate a prompt for the user.""" - return template.Template(raw_prompt, self.hass).async_render( - { - "ha_name": self.hass.config.location_name, - "ha_language": self.hass.config.language, - "exposed_entities": self._get_exposed_entities(), - }, - parse_result=False, - ) - - def _get_exposed_entities(self) -> list[ExposedEntity]: - """Get state list of exposed entities.""" - area_registry = ar.async_get(self.hass) - entity_registry = er.async_get(self.hass) - device_registry = dr.async_get(self.hass) - - exposed_entities = [] - exposed_states = [ - state - for state in self.hass.states.async_all() - if async_should_expose(self.hass, conversation.DOMAIN, state.entity_id) - ] - - for state in exposed_states: - entity_entry = entity_registry.async_get(state.entity_id) - names = [state.name] - area_names = [] - - if entity_entry is not None: - # Add aliases - names.extend(entity_entry.aliases) - if entity_entry.area_id and ( - area := area_registry.async_get_area(entity_entry.area_id) - ): - # Entity is in area - area_names.append(area.name) - area_names.extend(area.aliases) - elif entity_entry.device_id and ( - device := device_registry.async_get(entity_entry.device_id) - ): - # Check device area - if device.area_id and ( - area := area_registry.async_get_area(device.area_id) - ): - area_names.append(area.name) - area_names.extend(area.aliases) - - exposed_entities.append( - ExposedEntity( - entity_id=state.entity_id, - state=state, - names=names, - area_names=area_names, - ) - ) - - return exposed_entities diff --git a/homeassistant/components/ollama/manifest.json b/homeassistant/components/ollama/manifest.json index f7265d87aab..4d4321b8e3d 100644 --- a/homeassistant/components/ollama/manifest.json +++ b/homeassistant/components/ollama/manifest.json @@ -1,7 +1,7 @@ { "domain": "ollama", "name": "Ollama", - "after_dependencies": ["assist_pipeline"], + "after_dependencies": ["assist_pipeline", "intent"], "codeowners": ["@synesthesiam"], "config_flow": true, "dependencies": ["conversation"], diff --git a/homeassistant/components/ollama/models.py b/homeassistant/components/ollama/models.py index 56cc552fad1..3b6fc958587 100644 --- a/homeassistant/components/ollama/models.py +++ b/homeassistant/components/ollama/models.py @@ -2,18 +2,17 @@ from dataclasses import dataclass from enum import StrEnum -from functools import cached_property import ollama -from homeassistant.core import State - class MessageRole(StrEnum): """Role of a chat message.""" SYSTEM = "system" # prompt USER = "user" + ASSISTANT = "assistant" + TOOL = "tool" @dataclass @@ -30,18 +29,3 @@ class MessageHistory: def num_user_messages(self) -> int: """Return a count of user messages.""" return sum(m["role"] == MessageRole.USER.value for m in self.messages) - - -@dataclass(frozen=True) -class ExposedEntity: - """Relevant information about an exposed entity.""" - - entity_id: str - state: State - names: list[str] - area_names: list[str] - - @cached_property - def domain(self) -> str: - """Get domain from entity id.""" - return self.entity_id.split(".", maxsplit=1)[0] diff --git a/homeassistant/components/ollama/strings.json b/homeassistant/components/ollama/strings.json index cc0f05d3068..2366ecd0848 100644 --- a/homeassistant/components/ollama/strings.json +++ b/homeassistant/components/ollama/strings.json @@ -24,11 +24,13 @@ "step": { "init": { "data": { - "prompt": "Prompt template", + "prompt": "Instructions", + "llm_hass_api": "[%key:common::config_flow::data::llm_hass_api%]", "max_history": "Max history messages", "keep_alive": "Keep alive" }, "data_description": { + "prompt": "Instruct how the LLM should respond. This can be a template.", "keep_alive": "Duration in seconds for Ollama to keep model in memory. -1 = indefinite, 0 = never." } } diff --git a/tests/components/ollama/__init__.py b/tests/components/ollama/__init__.py index 22a576e94a4..6ad77bb2217 100644 --- a/tests/components/ollama/__init__.py +++ b/tests/components/ollama/__init__.py @@ -1,7 +1,7 @@ """Tests for the Ollama integration.""" from homeassistant.components import ollama -from homeassistant.components.ollama.const import DEFAULT_PROMPT +from homeassistant.helpers import llm TEST_USER_DATA = { ollama.CONF_URL: "http://localhost:11434", @@ -9,6 +9,6 @@ TEST_USER_DATA = { } TEST_OPTIONS = { - ollama.CONF_PROMPT: DEFAULT_PROMPT, + ollama.CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT, ollama.CONF_MAX_HISTORY: 2, } diff --git a/tests/components/ollama/conftest.py b/tests/components/ollama/conftest.py index db1689bd416..0355a13eba7 100644 --- a/tests/components/ollama/conftest.py +++ b/tests/components/ollama/conftest.py @@ -5,7 +5,9 @@ from unittest.mock import patch import pytest from homeassistant.components import ollama +from homeassistant.const import CONF_LLM_HASS_API from homeassistant.core import HomeAssistant +from homeassistant.helpers import llm from homeassistant.setup import async_setup_component from . import TEST_OPTIONS, TEST_USER_DATA @@ -25,6 +27,17 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: return entry +@pytest.fixture +def mock_config_entry_with_assist( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> MockConfigEntry: + """Mock a config entry with assist.""" + hass.config_entries.async_update_entry( + mock_config_entry, options={CONF_LLM_HASS_API: llm.LLM_API_ASSIST} + ) + return mock_config_entry + + @pytest.fixture async def mock_init_component(hass: HomeAssistant, mock_config_entry: MockConfigEntry): """Initialize integration.""" diff --git a/tests/components/ollama/snapshots/test_conversation.ambr b/tests/components/ollama/snapshots/test_conversation.ambr new file mode 100644 index 00000000000..e4dd7cd00bb --- /dev/null +++ b/tests/components/ollama/snapshots/test_conversation.ambr @@ -0,0 +1,34 @@ +# serializer version: 1 +# name: test_unknown_hass_api + dict({ + 'conversation_id': None, + 'response': IntentResponse( + card=dict({ + }), + error_code=, + failed_results=list([ + ]), + intent=None, + intent_targets=list([ + ]), + language='en', + matched_states=list([ + ]), + reprompt=dict({ + }), + response_type=, + speech=dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': 'Error preparing LLM API: API non-existing not found', + }), + }), + speech_slots=dict({ + }), + success_results=list([ + ]), + unmatched_states=list([ + ]), + ), + }) +# --- diff --git a/tests/components/ollama/test_conversation.py b/tests/components/ollama/test_conversation.py index b6f0be3c414..9be6f3b33a3 100644 --- a/tests/components/ollama/test_conversation.py +++ b/tests/components/ollama/test_conversation.py @@ -1,21 +1,18 @@ """Tests for the Ollama integration.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch from ollama import Message, ResponseError import pytest +from syrupy.assertion import SnapshotAssertion +import voluptuous as vol from homeassistant.components import conversation, ollama from homeassistant.components.conversation import trace -from homeassistant.components.homeassistant.exposed_entities import async_expose_entity -from homeassistant.const import ATTR_FRIENDLY_NAME, MATCH_ALL +from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import ( - area_registry as ar, - device_registry as dr, - entity_registry as er, - intent, -) +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import intent, llm from tests.common import MockConfigEntry @@ -25,9 +22,6 @@ async def test_chat( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component, - area_registry: ar.AreaRegistry, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, agent_id: str, ) -> None: """Test that the chat function is called with the appropriate arguments.""" @@ -35,48 +29,8 @@ async def test_chat( if agent_id is None: agent_id = mock_config_entry.entry_id - # Create some areas, devices, and entities - area_kitchen = area_registry.async_get_or_create("kitchen_id") - area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") - area_bedroom = area_registry.async_get_or_create("bedroom_id") - area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom") - area_office = area_registry.async_get_or_create("office_id") - area_office = area_registry.async_update(area_office.id, name="office") - entry = MockConfigEntry() entry.add_to_hass(hass) - kitchen_device = device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections=set(), - identifiers={("demo", "id-1234")}, - ) - device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id) - - kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234") - kitchen_light = entity_registry.async_update_entity( - kitchen_light.entity_id, device_id=kitchen_device.id - ) - hass.states.async_set( - kitchen_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} - ) - - bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678") - bedroom_light = entity_registry.async_update_entity( - bedroom_light.entity_id, area_id=area_bedroom.id - ) - hass.states.async_set( - bedroom_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "bedroom light"} - ) - - # Hide the office light - office_light = entity_registry.async_get_or_create("light", "demo", "ABCD") - office_light = entity_registry.async_update_entity( - office_light.entity_id, area_id=area_office.id - ) - hass.states.async_set( - office_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "office light"} - ) - async_expose_entity(hass, conversation.DOMAIN, office_light.entity_id, False) with patch( "ollama.AsyncClient.chat", @@ -100,12 +54,6 @@ async def test_chat( Message({"role": "user", "content": "test message"}), ] - # Verify only exposed devices/areas are in prompt - assert "kitchen light" in prompt - assert "bedroom light" in prompt - assert "office light" not in prompt - assert "office" not in prompt - assert ( result.response.response_type == intent.IntentResponseType.ACTION_DONE ), result @@ -122,7 +70,232 @@ async def test_chat( ] # AGENT_DETAIL event contains the raw prompt passed to the model detail_event = trace_events[1] - assert "The current time is" in detail_event["data"]["messages"][0]["content"] + assert "Current time is" in detail_event["data"]["messages"][0]["content"] + + +async def test_template_variables( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test that template variables work.""" + context = Context(user_id="12345") + mock_user = Mock() + mock_user.id = "12345" + mock_user.name = "Test User" + + hass.config_entries.async_update_entry( + mock_config_entry, + options={ + "prompt": ( + "The user name is {{ user_name }}. " + "The user id is {{ llm_context.context.user_id }}." + ), + }, + ) + with ( + patch("ollama.AsyncClient.list"), + patch( + "ollama.AsyncClient.chat", + return_value={"message": {"role": "assistant", "content": "test response"}}, + ) as mock_chat, + patch("homeassistant.auth.AuthManager.async_get_user", return_value=mock_user), + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + result = await conversation.async_converse( + hass, "hello", None, context, agent_id=mock_config_entry.entry_id + ) + + assert ( + result.response.response_type == intent.IntentResponseType.ACTION_DONE + ), result + + args = mock_chat.call_args.kwargs + prompt = args["messages"][0]["content"] + + assert "The user name is Test User." in prompt + assert "The user id is 12345." in prompt + + +@patch("homeassistant.components.ollama.conversation.llm.AssistAPI._async_get_tools") +async def test_function_call( + mock_get_tools, + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_init_component, +) -> None: + """Test function call from the assistant.""" + agent_id = mock_config_entry_with_assist.entry_id + context = Context() + + mock_tool = AsyncMock() + mock_tool.name = "test_tool" + mock_tool.description = "Test function" + mock_tool.parameters = vol.Schema( + {vol.Optional("param1", description="Test parameters"): str} + ) + mock_tool.async_call.return_value = "Test response" + + mock_get_tools.return_value = [mock_tool] + + def completion_result(*args, messages, **kwargs): + for message in messages: + if message["role"] == "tool": + return { + "message": { + "role": "assistant", + "content": "I have successfully called the function", + } + } + + return { + "message": { + "role": "assistant", + "tool_calls": [ + { + "function": { + "name": "test_tool", + "arguments": {"param1": "test_value"}, + } + } + ], + } + } + + with patch( + "ollama.AsyncClient.chat", + side_effect=completion_result, + ) as mock_chat: + result = await conversation.async_converse( + hass, + "Please call the test function", + None, + context, + agent_id=agent_id, + ) + + assert mock_chat.call_count == 2 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert ( + result.response.speech["plain"]["speech"] + == "I have successfully called the function" + ) + mock_tool.async_call.assert_awaited_once_with( + hass, + llm.ToolInput( + tool_name="test_tool", + tool_args={"param1": "test_value"}, + ), + llm.LLMContext( + platform="ollama", + context=context, + user_prompt="Please call the test function", + language="en", + assistant="conversation", + device_id=None, + ), + ) + + +@patch("homeassistant.components.ollama.conversation.llm.AssistAPI._async_get_tools") +async def test_function_exception( + mock_get_tools, + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_init_component, +) -> None: + """Test function call with exception.""" + agent_id = mock_config_entry_with_assist.entry_id + context = Context() + + mock_tool = AsyncMock() + mock_tool.name = "test_tool" + mock_tool.description = "Test function" + mock_tool.parameters = vol.Schema( + {vol.Optional("param1", description="Test parameters"): str} + ) + mock_tool.async_call.side_effect = HomeAssistantError("Test tool exception") + + mock_get_tools.return_value = [mock_tool] + + def completion_result(*args, messages, **kwargs): + for message in messages: + if message["role"] == "tool": + return { + "message": { + "role": "assistant", + "content": "There was an error calling the function", + } + } + + return { + "message": { + "role": "assistant", + "tool_calls": [ + { + "function": { + "name": "test_tool", + "arguments": {"param1": "test_value"}, + } + } + ], + } + } + + with patch( + "ollama.AsyncClient.chat", + side_effect=completion_result, + ) as mock_chat: + result = await conversation.async_converse( + hass, + "Please call the test function", + None, + context, + agent_id=agent_id, + ) + + assert mock_chat.call_count == 2 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert ( + result.response.speech["plain"]["speech"] + == "There was an error calling the function" + ) + mock_tool.async_call.assert_awaited_once_with( + hass, + llm.ToolInput( + tool_name="test_tool", + tool_args={"param1": "test_value"}, + ), + llm.LLMContext( + platform="ollama", + context=context, + user_prompt="Please call the test function", + language="en", + assistant="conversation", + device_id=None, + ), + ) + + +async def test_unknown_hass_api( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + mock_init_component, +) -> None: + """Test when we reference an API that no longer exists.""" + hass.config_entries.async_update_entry( + mock_config_entry, + options={ + **mock_config_entry.options, + CONF_LLM_HASS_API: "non-existing", + }, + ) + + result = await conversation.async_converse( + hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id + ) + + assert result == snapshot async def test_message_history_trimming( From 5f088832270e242763aa337b1b4cf9a3bfc8654d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 09:58:15 +0200 Subject: [PATCH 1634/2411] Bump github/codeql-action from 3.25.14 to 3.25.15 (#122753) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b199ea09bb3..7fe545e469c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.1.7 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.25.14 + uses: github/codeql-action/init@v3.25.15 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.25.14 + uses: github/codeql-action/analyze@v3.25.15 with: category: "/language:python" From 9b497aebb4fbcf2e11b5a8e3aa70a2c761c8dcc3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 29 Jul 2024 10:12:18 +0200 Subject: [PATCH 1635/2411] Fix bug in timeout util related to multiple global freezes (#122466) --- homeassistant/util/timeout.py | 20 +++++++++----------- tests/util/test_timeout.py | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py index 72cabffeed6..821f502694b 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -61,18 +61,16 @@ class _GlobalFreezeContext: def _enter(self) -> None: """Run freeze.""" - if not self._manager.freezes_done: - return + if self._manager.freezes_done: + # Global reset + for task in self._manager.global_tasks: + task.pause() - # Global reset - for task in self._manager.global_tasks: - task.pause() - - # Zones reset - for zone in self._manager.zones.values(): - if not zone.freezes_done: - continue - zone.pause() + # Zones reset + for zone in self._manager.zones.values(): + if not zone.freezes_done: + continue + zone.pause() self._manager.global_freezes.append(self) diff --git a/tests/util/test_timeout.py b/tests/util/test_timeout.py index 496096bd740..1c4b06d99b4 100644 --- a/tests/util/test_timeout.py +++ b/tests/util/test_timeout.py @@ -338,3 +338,24 @@ async def test_simple_zone_timeout_zone_with_timeout_exeption() -> None: raise RuntimeError await asyncio.sleep(0.3) + + +async def test_multiple_global_freezes(hass: HomeAssistant) -> None: + """Test multiple global freezes.""" + timeout = TimeoutManager() + + async def background(delay: float) -> None: + async with timeout.async_freeze(): + await asyncio.sleep(delay) + + async with timeout.async_timeout(0.1): + task = hass.async_create_task(background(0.2)) + async with timeout.async_freeze(): + await asyncio.sleep(0.1) + await task + + async with timeout.async_timeout(0.1): + task = hass.async_create_task(background(0.2)) + async with timeout.async_freeze(): + await asyncio.sleep(0.3) + await task From 2a5cb8da327c66fe6fc060a166f176f1ea2cc5bd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 29 Jul 2024 10:57:34 +0200 Subject: [PATCH 1636/2411] Fix copy-paste errors in alarm_control_panel tests (#122755) --- tests/components/alarm_control_panel/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/alarm_control_panel/conftest.py b/tests/components/alarm_control_panel/conftest.py index 9c59c9e39c3..3e82b935493 100644 --- a/tests/components/alarm_control_panel/conftest.py +++ b/tests/components/alarm_control_panel/conftest.py @@ -129,7 +129,7 @@ async def code_arm_required() -> bool: @pytest.fixture(name="supported_features") -async def lock_supported_features() -> AlarmControlPanelEntityFeature: +async def alarm_control_panel_supported_features() -> AlarmControlPanelEntityFeature: """Return the supported features for the test alarm control panel entity.""" return ( AlarmControlPanelEntityFeature.ARM_AWAY @@ -142,7 +142,7 @@ async def lock_supported_features() -> AlarmControlPanelEntityFeature: @pytest.fixture(name="mock_alarm_control_panel_entity") -async def setup_lock_platform_test_entity( +async def setup_alarm_control_panel_platform_test_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, code_format: CodeFormat | None, From 5f5dcec0b91e938f8316402a238f9d4a4b1982bb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 29 Jul 2024 10:57:49 +0200 Subject: [PATCH 1637/2411] Revert unneeded type annotation in the api integration (#122757) --- homeassistant/components/api/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 9572ed3fbd1..b794b60b33d 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -118,7 +118,7 @@ class APICoreStateView(HomeAssistantView): Home Assistant core is running. Its primary use case is for supervisor to check if Home Assistant is running. """ - hass: HomeAssistant = request.app[KEY_HASS] + hass = request.app[KEY_HASS] migration = recorder.async_migration_in_progress(hass) live = recorder.async_migration_is_live(hass) recorder_state = {"migration_in_progress": migration, "migration_is_live": live} From 686598b6b308d0af41c446630d4ca85cd72aa849 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 29 Jul 2024 11:24:14 +0200 Subject: [PATCH 1638/2411] Don't block HA startup while set up legacy Ecovacs bot (#122732) wait for connection in background --- homeassistant/components/ecovacs/__init__.py | 11 +++++++++++ homeassistant/components/ecovacs/vacuum.py | 11 ++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/__init__.py b/homeassistant/components/ecovacs/__init__.py index b2f40acc2f8..d13a337057d 100644 --- a/homeassistant/components/ecovacs/__init__.py +++ b/homeassistant/components/ecovacs/__init__.py @@ -1,5 +1,6 @@ """Support for Ecovacs Deebot vacuums.""" +from sucks import VacBot import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -61,6 +62,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: EcovacsConfigEntry) -> b entry.async_on_unload(on_unload) entry.runtime_data = controller + + async def _async_wait_connect(device: VacBot) -> None: + await hass.async_add_executor_job(device.connect_and_wait_until_ready) + + for device in controller.legacy_devices: + entry.async_create_background_task( + hass=hass, + target=_async_wait_connect(device), + name=f"{entry.title}_wait_connect_{device.vacuum['did']}", + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index ba94df0cb5f..d28e632580f 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -56,9 +56,9 @@ async def async_setup_entry( for device in controller.devices if device.capabilities.device_type is DeviceType.VACUUM ] - for device in controller.legacy_devices: - await hass.async_add_executor_job(device.connect_and_wait_until_ready) - vacuums.append(EcovacsLegacyVacuum(device)) + vacuums.extend( + [EcovacsLegacyVacuum(device) for device in controller.legacy_devices] + ) _LOGGER.debug("Adding Ecovacs Vacuums to Home Assistant: %s", vacuums) async_add_entities(vacuums) @@ -142,6 +142,11 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity): return None + @property + def available(self) -> bool: + """Return True if the vacuum is available.""" + return super().available and self.state is not None + @property def battery_level(self) -> int | None: """Return the battery level of the vacuum cleaner.""" From fa61ad072d6f30d3b1e5ad9e8c280edec4aee1a6 Mon Sep 17 00:00:00 2001 From: danielsmyers Date: Mon, 29 Jul 2024 02:25:04 -0700 Subject: [PATCH 1639/2411] Add Bryant Evolution Integration (#119788) * Add an integration for Bryant Evolution HVAC systems. * Update newly created tests so that they pass. * Improve compliance with home assistant guidelines. * Added tests * remove xxx * Minor test cleanups * Add a test for reading HVAC actions. * Update homeassistant/components/bryant_evolution/__init__.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/bryant_evolution/climate.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/bryant_evolution/climate.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/bryant_evolution/climate.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/bryant_evolution/climate.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/bryant_evolution/climate.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/bryant_evolution/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Address reviewer comments. * Address additional reviewer comments. * Use translation for exception error messages. * Simplify config flow. * Continue addressing comments * Use mocking rather than DI to provide a for-test client in tests. * Fix a failure in test_config_flow.py * Track host->filename in strings.json. * Use config entry ID for climate entity unique id * Guard against fan mode returning None in async_update. * Move unavailable-client check from climate.py to init.py. * Improve test coverage * Bump evolutionhttp version * Address comments * update comment * only have one _can_reach_device fn * Auto-detect which systems and zones are attached. * Add support for reconfiguration * Fix a few review comments * Introduce multiple devices * Track evolutionhttp library change that returns additional per-zone information during enumeration * Move construction of devices to init * Avoid triplicate writing * rework tests to use mocks * Correct attribute name to unbreak test * Pull magic tuple of system-zone into a constant * Address some test comments * Create test_init.py * simplify test_reconfigure * Replace disable_auto_entity_update with mocks. * Update tests/components/bryant_evolution/test_climate.py Co-authored-by: Joost Lekkerkerker * Update tests/components/bryant_evolution/test_climate.py Co-authored-by: Joost Lekkerkerker * Update tests/components/bryant_evolution/test_config_flow.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/bryant_evolution/config_flow.py Co-authored-by: Joost Lekkerkerker * Update tests/components/bryant_evolution/test_config_flow.py Co-authored-by: Joost Lekkerkerker * Update tests/components/bryant_evolution/test_config_flow.py Co-authored-by: Joost Lekkerkerker * fix test errors * do not access runtime_data in tests * use snapshot_platform and type fixtures --------- Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> Co-authored-by: Joost Lekkerkerker --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/bryant_evolution/__init__.py | 84 ++++++ .../components/bryant_evolution/climate.py | 252 +++++++++++++++++ .../bryant_evolution/config_flow.py | 87 ++++++ .../components/bryant_evolution/const.py | 4 + .../components/bryant_evolution/manifest.json | 10 + .../components/bryant_evolution/names.py | 18 ++ .../components/bryant_evolution/strings.json | 48 ++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/bryant_evolution/__init__.py | 1 + tests/components/bryant_evolution/conftest.py | 70 +++++ .../snapshots/test_climate.ambr | 83 ++++++ .../bryant_evolution/test_climate.py | 259 ++++++++++++++++++ .../bryant_evolution/test_config_flow.py | 170 ++++++++++++ .../components/bryant_evolution/test_init.py | 112 ++++++++ 20 files changed, 1224 insertions(+) create mode 100644 homeassistant/components/bryant_evolution/__init__.py create mode 100644 homeassistant/components/bryant_evolution/climate.py create mode 100644 homeassistant/components/bryant_evolution/config_flow.py create mode 100644 homeassistant/components/bryant_evolution/const.py create mode 100644 homeassistant/components/bryant_evolution/manifest.json create mode 100644 homeassistant/components/bryant_evolution/names.py create mode 100644 homeassistant/components/bryant_evolution/strings.json create mode 100644 tests/components/bryant_evolution/__init__.py create mode 100644 tests/components/bryant_evolution/conftest.py create mode 100644 tests/components/bryant_evolution/snapshots/test_climate.ambr create mode 100644 tests/components/bryant_evolution/test_climate.py create mode 100644 tests/components/bryant_evolution/test_config_flow.py create mode 100644 tests/components/bryant_evolution/test_init.py diff --git a/.strict-typing b/.strict-typing index 84cdbe02424..a4f6d198d97 100644 --- a/.strict-typing +++ b/.strict-typing @@ -120,6 +120,7 @@ homeassistant.components.bond.* homeassistant.components.braviatv.* homeassistant.components.brother.* homeassistant.components.browser.* +homeassistant.components.bryant_evolution.* homeassistant.components.bthome.* homeassistant.components.button.* homeassistant.components.calendar.* diff --git a/CODEOWNERS b/CODEOWNERS index 7db252a9117..fc18be91239 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -221,6 +221,8 @@ build.json @home-assistant/supervisor /tests/components/brottsplatskartan/ @gjohansson-ST /homeassistant/components/brunt/ @eavanvalkenburg /tests/components/brunt/ @eavanvalkenburg +/homeassistant/components/bryant_evolution/ @danielsmyers +/tests/components/bryant_evolution/ @danielsmyers /homeassistant/components/bsblan/ @liudger /tests/components/bsblan/ @liudger /homeassistant/components/bt_smarthub/ @typhoon2099 diff --git a/homeassistant/components/bryant_evolution/__init__.py b/homeassistant/components/bryant_evolution/__init__.py new file mode 100644 index 00000000000..6ff58ad5df5 --- /dev/null +++ b/homeassistant/components/bryant_evolution/__init__.py @@ -0,0 +1,84 @@ +"""The Bryant Evolution integration.""" + +from __future__ import annotations + +import logging + +from evolutionhttp import BryantEvolutionLocalClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_FILENAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr + +from . import names +from .const import CONF_SYSTEM_ZONE, DOMAIN + +PLATFORMS: list[Platform] = [Platform.CLIMATE] + +type BryantEvolutionLocalClients = dict[tuple[int, int], BryantEvolutionLocalClient] +type BryantEvolutionConfigEntry = ConfigEntry[BryantEvolutionLocalClients] +_LOGGER = logging.getLogger(__name__) + + +async def _can_reach_device(client: BryantEvolutionLocalClient) -> bool: + """Return whether we can reach the device at the given filename.""" + # Verify that we can read current temperature to check that the + # (filename, system, zone) is valid. + return await client.read_current_temperature() is not None + + +async def async_setup_entry( + hass: HomeAssistant, entry: BryantEvolutionConfigEntry +) -> bool: + """Set up Bryant Evolution from a config entry.""" + + # Add a device for the SAM itself. + sam_uid = names.sam_device_uid(entry) + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, sam_uid)}, + manufacturer="Bryant", + name="System Access Module", + ) + + # Add a device for each system. + for sys_id in (1, 2): + if not any(sz[0] == sys_id for sz in entry.data[CONF_SYSTEM_ZONE]): + _LOGGER.debug( + "Skipping system %s because it is not configured for this integration: %s", + sys_id, + entry.data[CONF_SYSTEM_ZONE], + ) + continue + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, names.system_device_uid(sam_uid, sys_id))}, + via_device=(DOMAIN, names.sam_device_uid(entry)), + manufacturer="Bryant", + name=f"System {sys_id}", + ) + + # Create a client for every zone. + entry.runtime_data = {} + for sz in entry.data[CONF_SYSTEM_ZONE]: + try: + client = await BryantEvolutionLocalClient.get_client( + sz[0], sz[1], entry.data[CONF_FILENAME] + ) + if not await _can_reach_device(client): + raise ConfigEntryNotReady + entry.runtime_data[tuple(sz)] = client + except FileNotFoundError as f: + raise ConfigEntryNotReady from f + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry( + hass: HomeAssistant, entry: BryantEvolutionConfigEntry +) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/bryant_evolution/climate.py b/homeassistant/components/bryant_evolution/climate.py new file mode 100644 index 00000000000..dd31097a1ee --- /dev/null +++ b/homeassistant/components/bryant_evolution/climate.py @@ -0,0 +1,252 @@ +"""Support for Bryant Evolution HVAC systems.""" + +from datetime import timedelta +import logging +from typing import Any + +from evolutionhttp import BryantEvolutionLocalClient + +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.const import UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import BryantEvolutionConfigEntry, names +from .const import CONF_SYSTEM_ZONE, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +SCAN_INTERVAL = timedelta(seconds=60) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: BryantEvolutionConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a config entry.""" + + # Add a climate entity for each system/zone. + sam_uid = names.sam_device_uid(config_entry) + entities: list[Entity] = [] + for sz in config_entry.data[CONF_SYSTEM_ZONE]: + system_id = sz[0] + zone_id = sz[1] + client = config_entry.runtime_data.get(tuple(sz)) + climate = BryantEvolutionClimate( + client, + system_id, + zone_id, + sam_uid, + ) + entities.append(climate) + async_add_entities(entities, update_before_add=True) + + +class BryantEvolutionClimate(ClimateEntity): + """ClimateEntity for Bryant Evolution HVAC systems. + + Design note: this class updates using polling. However, polling + is very slow (~1500 ms / parameter). To improve the user + experience on updates, we also locally update this instance and + call async_write_ha_state as well. + """ + + _attr_has_entity_name = True + _attr_temperature_unit = UnitOfTemperature.FAHRENHEIT + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.TURN_OFF + ) + _attr_hvac_modes = [ + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.OFF, + ] + _attr_fan_modes = ["auto", "low", "med", "high"] + _enable_turn_on_off_backwards_compatibility = False + + def __init__( + self, + client: BryantEvolutionLocalClient, + system_id: int, + zone_id: int, + sam_uid: str, + ) -> None: + """Initialize an entity from parts.""" + self._client = client + self._attr_name = None + self._attr_unique_id = names.zone_entity_uid(sam_uid, system_id, zone_id) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._attr_unique_id)}, + manufacturer="Bryant", + via_device=(DOMAIN, names.system_device_uid(sam_uid, system_id)), + name=f"System {system_id} Zone {zone_id}", + ) + + async def async_update(self) -> None: + """Update the entity state.""" + self._attr_current_temperature = await self._client.read_current_temperature() + if (fan_mode := await self._client.read_fan_mode()) is not None: + self._attr_fan_mode = fan_mode.lower() + else: + self._attr_fan_mode = None + self._attr_target_temperature = None + self._attr_target_temperature_high = None + self._attr_target_temperature_low = None + self._attr_hvac_mode = await self._read_hvac_mode() + + # Set target_temperature or target_temperature_{high, low} based on mode. + match self._attr_hvac_mode: + case HVACMode.HEAT: + self._attr_target_temperature = ( + await self._client.read_heating_setpoint() + ) + case HVACMode.COOL: + self._attr_target_temperature = ( + await self._client.read_cooling_setpoint() + ) + case HVACMode.HEAT_COOL: + self._attr_target_temperature_high = ( + await self._client.read_cooling_setpoint() + ) + self._attr_target_temperature_low = ( + await self._client.read_heating_setpoint() + ) + case HVACMode.OFF: + pass + case _: + _LOGGER.error("Unknown HVAC mode %s", self._attr_hvac_mode) + + # Note: depends on current temperature and target temperature low read + # above. + self._attr_hvac_action = await self._read_hvac_action() + + async def _read_hvac_mode(self) -> HVACMode: + mode_and_active = await self._client.read_hvac_mode() + if not mode_and_active: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="failed_to_read_hvac_mode" + ) + mode = mode_and_active[0] + mode_enum = { + "HEAT": HVACMode.HEAT, + "COOL": HVACMode.COOL, + "AUTO": HVACMode.HEAT_COOL, + "OFF": HVACMode.OFF, + }.get(mode.upper()) + if mode_enum is None: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="failed_to_parse_hvac_mode", + translation_placeholders={"mode": mode}, + ) + return mode_enum + + async def _read_hvac_action(self) -> HVACAction: + """Return the current running hvac operation.""" + mode_and_active = await self._client.read_hvac_mode() + if not mode_and_active: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="failed_to_read_hvac_action" + ) + mode, is_active = mode_and_active + if not is_active: + return HVACAction.OFF + match mode.upper(): + case "HEAT": + return HVACAction.HEATING + case "COOL": + return HVACAction.COOLING + case "OFF": + return HVACAction.OFF + case "AUTO": + # In AUTO, we need to figure out what the actual action is + # based on the setpoints. + if ( + self.current_temperature is not None + and self.target_temperature_low is not None + ): + if self.current_temperature > self.target_temperature_low: + # If the system is on and the current temperature is + # higher than the point at which heating would activate, + # then we must be cooling. + return HVACAction.COOLING + return HVACAction.HEATING + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="failed_to_parse_hvac_mode", + translation_placeholders={ + "mode_and_active": mode_and_active, + "current_temperature": str(self.current_temperature), + "target_temperature_low": str(self.target_temperature_low), + }, + ) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + if hvac_mode == HVACMode.HEAT_COOL: + hvac_mode = HVACMode.AUTO + if not await self._client.set_hvac_mode(hvac_mode): + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="failed_to_set_hvac_mode" + ) + self._attr_hvac_mode = hvac_mode + self._async_write_ha_state() + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + if kwargs.get("target_temp_high"): + temp = int(kwargs["target_temp_high"]) + if not await self._client.set_cooling_setpoint(temp): + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="failed_to_set_clsp" + ) + self._attr_target_temperature_high = temp + + if kwargs.get("target_temp_low"): + temp = int(kwargs["target_temp_low"]) + if not await self._client.set_heating_setpoint(temp): + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="failed_to_set_htsp" + ) + self._attr_target_temperature_low = temp + + if kwargs.get("temperature"): + temp = int(kwargs["temperature"]) + fn = ( + self._client.set_heating_setpoint + if self.hvac_mode == HVACMode.HEAT + else self._client.set_cooling_setpoint + ) + if not await fn(temp): + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="failed_to_set_temp" + ) + self._attr_target_temperature = temp + + # If we get here, we must have changed something unless HA allowed an + # invalid service call (without any recognized kwarg). + self._async_write_ha_state() + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + if not await self._client.set_fan_mode(fan_mode): + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="failed_to_set_fan_mode" + ) + self._attr_fan_mode = fan_mode.lower() + self.async_write_ha_state() diff --git a/homeassistant/components/bryant_evolution/config_flow.py b/homeassistant/components/bryant_evolution/config_flow.py new file mode 100644 index 00000000000..a6b07daf96b --- /dev/null +++ b/homeassistant/components/bryant_evolution/config_flow.py @@ -0,0 +1,87 @@ +"""Config flow for Bryant Evolution integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from evolutionhttp import BryantEvolutionLocalClient +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_FILENAME +from homeassistant.helpers.typing import UNDEFINED + +from .const import CONF_SYSTEM_ZONE, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_FILENAME, default="/dev/ttyUSB0"): str, + } +) + + +async def _enumerate_sz(tty: str) -> list[tuple[int, int]]: + """Return (system, zone) tuples for each system+zone accessible through tty.""" + return [ + (system_id, zone.zone_id) + for system_id in (1, 2) + for zone in await BryantEvolutionLocalClient.enumerate_zones(system_id, tty) + ] + + +class BryantConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Bryant Evolution.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + try: + system_zone = await _enumerate_sz(user_input[CONF_FILENAME]) + except FileNotFoundError: + _LOGGER.error("Could not open %s: not found", user_input[CONF_FILENAME]) + errors["base"] = "cannot_connect" + else: + if len(system_zone) != 0: + return self.async_create_entry( + title=f"SAM at {user_input[CONF_FILENAME]}", + data={ + CONF_FILENAME: user_input[CONF_FILENAME], + CONF_SYSTEM_ZONE: system_zone, + }, + ) + errors["base"] = "cannot_connect" + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle integration reconfiguration.""" + errors: dict[str, str] = {} + if user_input is not None: + system_zone = await _enumerate_sz(user_input[CONF_FILENAME]) + if len(system_zone) != 0: + our_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + assert our_entry is not None, "Could not find own entry" + return self.async_update_reload_and_abort( + entry=our_entry, + data={ + CONF_FILENAME: user_input[CONF_FILENAME], + CONF_SYSTEM_ZONE: system_zone, + }, + unique_id=UNDEFINED, + reason="reconfigured", + ) + errors["base"] = "cannot_connect" + return self.async_show_form( + step_id="reconfigure", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/bryant_evolution/const.py b/homeassistant/components/bryant_evolution/const.py new file mode 100644 index 00000000000..82637b34eb9 --- /dev/null +++ b/homeassistant/components/bryant_evolution/const.py @@ -0,0 +1,4 @@ +"""Constants for the Bryant Evolution integration.""" + +DOMAIN = "bryant_evolution" +CONF_SYSTEM_ZONE = "system_zone" diff --git a/homeassistant/components/bryant_evolution/manifest.json b/homeassistant/components/bryant_evolution/manifest.json new file mode 100644 index 00000000000..27fd8860e76 --- /dev/null +++ b/homeassistant/components/bryant_evolution/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "bryant_evolution", + "name": "Bryant Evolution", + "codeowners": ["@danielsmyers"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/bryant_evolution", + "integration_type": "device", + "iot_class": "local_polling", + "requirements": ["evolutionhttp==0.0.18"] +} diff --git a/homeassistant/components/bryant_evolution/names.py b/homeassistant/components/bryant_evolution/names.py new file mode 100644 index 00000000000..dbe0eb65b60 --- /dev/null +++ b/homeassistant/components/bryant_evolution/names.py @@ -0,0 +1,18 @@ +"""Functions to generate names for devices and entities.""" + +from homeassistant.config_entries import ConfigEntry + + +def sam_device_uid(entry: ConfigEntry) -> str: + """Return the UID for the SAM device.""" + return entry.entry_id + + +def system_device_uid(sam_uid: str, system_id: int) -> str: + """Return the UID for a given system (e.g., 1) under a SAM.""" + return f"{sam_uid}-S{system_id}" + + +def zone_entity_uid(sam_uid: str, system_id: int, zone_id: int) -> str: + """Return the UID for a given system and zone (e.g., 1 and 2) under a SAM.""" + return f"{sam_uid}-S{system_id}-Z{zone_id}" diff --git a/homeassistant/components/bryant_evolution/strings.json b/homeassistant/components/bryant_evolution/strings.json new file mode 100644 index 00000000000..1ce9d58bb10 --- /dev/null +++ b/homeassistant/components/bryant_evolution/strings.json @@ -0,0 +1,48 @@ +{ + "config": { + "step": { + "user": { + "data": { + "filename": "Serial port filename" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "exceptions": { + "failed_to_read_hvac_mode": { + "message": "Failed to read current HVAC mode" + }, + "failed_to_parse_hvac_mode": { + "message": "Cannot parse response to HVACMode: {mode}" + }, + "failed_to_read_hvac_action": { + "message": "Failed to read current HVAC action" + }, + "failed_to_parse_hvac_action": { + "message": "Could not determine HVAC action: {mode_and_active}, {self.current_temperature}, {self.target_temperature_low}" + }, + "failed_to_set_hvac_mode": { + "message": "Failed to set HVAC mode" + }, + "failed_to_set_clsp": { + "message": "Failed to set cooling setpoint" + }, + "failed_to_set_htsp": { + "message": "Failed to set heating setpoint" + }, + "failed_to_set_temp": { + "message": "Failed to set temperature" + }, + "failed_to_set_fan_mode": { + "message": "Failed to set fan mode" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1cea31202bc..e7d5278dd89 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -93,6 +93,7 @@ FLOWS = { "brother", "brottsplatskartan", "brunt", + "bryant_evolution", "bsblan", "bthome", "buienradar", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 74efe96dd2d..8bfef6a9887 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -810,6 +810,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "bryant_evolution": { + "name": "Bryant Evolution", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_polling" + }, "bsblan": { "name": "BSB-Lan", "integration_type": "device", diff --git a/mypy.ini b/mypy.ini index 9a35b74e6d5..dd7904d798b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -955,6 +955,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.bryant_evolution.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.bthome.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index b4df02e398f..3b599b00ce8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -845,6 +845,9 @@ eufylife-ble-client==0.1.8 # homeassistant.components.evohome evohome-async==0.4.20 +# homeassistant.components.bryant_evolution +evolutionhttp==0.0.18 + # homeassistant.components.faa_delays faadelays==2023.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a949a12623f..27d112fb4f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -711,6 +711,9 @@ eternalegypt==0.0.16 # homeassistant.components.eufylife_ble eufylife-ble-client==0.1.8 +# homeassistant.components.bryant_evolution +evolutionhttp==0.0.18 + # homeassistant.components.faa_delays faadelays==2023.9.1 diff --git a/tests/components/bryant_evolution/__init__.py b/tests/components/bryant_evolution/__init__.py new file mode 100644 index 00000000000..22fa2950253 --- /dev/null +++ b/tests/components/bryant_evolution/__init__.py @@ -0,0 +1 @@ +"""Tests for the Bryant Evolution integration.""" diff --git a/tests/components/bryant_evolution/conftest.py b/tests/components/bryant_evolution/conftest.py new file mode 100644 index 00000000000..cc9dfbec1e1 --- /dev/null +++ b/tests/components/bryant_evolution/conftest.py @@ -0,0 +1,70 @@ +"""Common fixtures for the Bryant Evolution tests.""" + +from collections.abc import Generator, Mapping +from unittest.mock import AsyncMock, patch + +from evolutionhttp import BryantEvolutionLocalClient +import pytest + +from homeassistant.components.bryant_evolution.const import CONF_SYSTEM_ZONE, DOMAIN +from homeassistant.const import CONF_FILENAME +from homeassistant.core import HomeAssistant +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.bryant_evolution.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +DEFAULT_SYSTEM_ZONES = ((1, 1), (1, 2), (2, 3)) +""" +A tuple of (system, zone) pairs representing the default system and zone configurations +for the Bryant Evolution integration. +""" + + +@pytest.fixture(autouse=True) +def mock_evolution_client_factory() -> Generator[AsyncMock, None, None]: + """Mock an Evolution client.""" + with patch( + "evolutionhttp.BryantEvolutionLocalClient.get_client", + austospec=True, + ) as mock_get_client: + clients: Mapping[tuple[int, int], AsyncMock] = {} + for system, zone in DEFAULT_SYSTEM_ZONES: + clients[(system, zone)] = AsyncMock(spec=BryantEvolutionLocalClient) + client = clients[system, zone] + client.read_zone_name.return_value = f"System {system} Zone {zone}" + client.read_current_temperature.return_value = 75 + client.read_hvac_mode.return_value = ("COOL", False) + client.read_fan_mode.return_value = "AUTO" + client.read_cooling_setpoint.return_value = 72 + mock_get_client.side_effect = lambda system, zone, tty: clients[ + (system, zone) + ] + yield mock_get_client + + +@pytest.fixture +async def mock_evolution_entry( + hass: HomeAssistant, + mock_evolution_client_factory: AsyncMock, +) -> MockConfigEntry: + """Configure and return a Bryant evolution integration.""" + hass.config.units = US_CUSTOMARY_SYSTEM + entry = MockConfigEntry( + entry_id="01J3XJZSTEF6G5V0QJX6HBC94T", # For determinism in snapshot tests + domain=DOMAIN, + data={CONF_FILENAME: "/dev/ttyUSB0", CONF_SYSTEM_ZONE: [(1, 1)]}, + ) + 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/bryant_evolution/snapshots/test_climate.ambr b/tests/components/bryant_evolution/snapshots/test_climate.ambr new file mode 100644 index 00000000000..4f6c1f2bbc4 --- /dev/null +++ b/tests/components/bryant_evolution/snapshots/test_climate.ambr @@ -0,0 +1,83 @@ +# serializer version: 1 +# name: test_setup_integration_success[climate.system_1_zone_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'auto', + 'low', + 'med', + 'high', + ]), + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 95, + 'min_temp': 45, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.system_1_zone_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'bryant_evolution', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '01J3XJZSTEF6G5V0QJX6HBC94T-S1-Z1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_integration_success[climate.system_1_zone_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 75, + 'fan_mode': 'auto', + 'fan_modes': list([ + 'auto', + 'low', + 'med', + 'high', + ]), + 'friendly_name': 'System 1 Zone 1', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 95, + 'min_temp': 45, + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'temperature': 72, + }), + 'context': , + 'entity_id': 'climate.system_1_zone_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- diff --git a/tests/components/bryant_evolution/test_climate.py b/tests/components/bryant_evolution/test_climate.py new file mode 100644 index 00000000000..42944c32bc2 --- /dev/null +++ b/tests/components/bryant_evolution/test_climate.py @@ -0,0 +1,259 @@ +"""Test the BryantEvolutionClient type.""" + +from collections.abc import Generator +from datetime import timedelta +import logging +from unittest.mock import AsyncMock + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.bryant_evolution.climate import SCAN_INTERVAL +from homeassistant.components.climate import ( + ATTR_FAN_MODE, + ATTR_HVAC_ACTION, + ATTR_HVAC_MODE, + ATTR_TEMPERATURE, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, + HVACAction, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + +_LOGGER = logging.getLogger(__name__) + + +async def trigger_polling(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: + """Trigger a polling event.""" + freezer.tick(SCAN_INTERVAL + timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + +async def test_setup_integration_success( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_evolution_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test that an instance can be constructed.""" + await snapshot_platform( + hass, entity_registry, snapshot, mock_evolution_entry.entry_id + ) + + +async def test_set_temperature_mode_cool( + hass: HomeAssistant, + mock_evolution_entry: MockConfigEntry, + mock_evolution_client_factory: Generator[AsyncMock, None, None], + freezer: FrozenDateTimeFactory, +) -> None: + """Test setting the temperature in cool mode.""" + # Start with known initial conditions + client = await mock_evolution_client_factory(1, 1, "/dev/unused") + client.read_hvac_mode.return_value = ("COOL", False) + client.read_cooling_setpoint.return_value = 75 + await trigger_polling(hass, freezer) + state = hass.states.get("climate.system_1_zone_1") + assert state.attributes["temperature"] == 75, state.attributes + + # Make the call, modifting the mock client to throw an exception on + # read to ensure that the update is visible iff we call + # async_update_ha_state. + data = {ATTR_TEMPERATURE: 70} + data[ATTR_ENTITY_ID] = "climate.system_1_zone_1" + client.read_cooling_setpoint.side_effect = Exception("fake failure") + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, data, blocking=True + ) + + # Verify effect. + client.set_cooling_setpoint.assert_called_once_with(70) + state = hass.states.get("climate.system_1_zone_1") + assert state.attributes["temperature"] == 70 + + +async def test_set_temperature_mode_heat( + hass: HomeAssistant, + mock_evolution_entry: MockConfigEntry, + mock_evolution_client_factory: Generator[AsyncMock, None, None], + freezer: FrozenDateTimeFactory, +) -> None: + """Test setting the temperature in heat mode.""" + + # Start with known initial conditions + client = await mock_evolution_client_factory(1, 1, "/dev/unused") + client.read_hvac_mode.return_value = ("HEAT", False) + client.read_heating_setpoint.return_value = 60 + await trigger_polling(hass, freezer) + + # Make the call, modifting the mock client to throw an exception on + # read to ensure that the update is visible iff we call + # async_update_ha_state. + data = {"temperature": 65} + data[ATTR_ENTITY_ID] = "climate.system_1_zone_1" + client.read_heating_setpoint.side_effect = Exception("fake failure") + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, data, blocking=True + ) + # Verify effect. + state = hass.states.get("climate.system_1_zone_1") + assert state.attributes["temperature"] == 65, state.attributes + + +async def test_set_temperature_mode_heat_cool( + hass: HomeAssistant, + mock_evolution_entry: MockConfigEntry, + mock_evolution_client_factory: Generator[AsyncMock, None, None], + freezer: FrozenDateTimeFactory, +) -> None: + """Test setting the temperature in heat_cool mode.""" + + # Enter heat_cool with known setpoints + mock_client = await mock_evolution_client_factory(1, 1, "/dev/unused") + mock_client.read_hvac_mode.return_value = ("AUTO", False) + mock_client.read_cooling_setpoint.return_value = 90 + mock_client.read_heating_setpoint.return_value = 40 + await trigger_polling(hass, freezer) + state = hass.states.get("climate.system_1_zone_1") + assert state.state == "heat_cool" + assert state.attributes["target_temp_low"] == 40 + assert state.attributes["target_temp_high"] == 90 + + # Make the call, modifting the mock client to throw an exception on + # read to ensure that the update is visible iff we call + # async_update_ha_state. + mock_client.read_heating_setpoint.side_effect = Exception("fake failure") + mock_client.read_cooling_setpoint.side_effect = Exception("fake failure") + data = {"target_temp_low": 70, "target_temp_high": 80} + data[ATTR_ENTITY_ID] = "climate.system_1_zone_1" + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, data, blocking=True + ) + state = hass.states.get("climate.system_1_zone_1") + assert state.attributes["target_temp_low"] == 70, state.attributes + assert state.attributes["target_temp_high"] == 80, state.attributes + mock_client.set_cooling_setpoint.assert_called_once_with(80) + mock_client.set_heating_setpoint.assert_called_once_with(70) + + +async def test_set_fan_mode( + hass: HomeAssistant, + mock_evolution_entry: MockConfigEntry, + mock_evolution_client_factory: Generator[AsyncMock, None, None], +) -> None: + """Test that setting fan mode works.""" + mock_client = await mock_evolution_client_factory(1, 1, "/dev/unused") + fan_modes = ["auto", "low", "med", "high"] + for mode in fan_modes: + # Make the call, modifting the mock client to throw an exception on + # read to ensure that the update is visible iff we call + # async_update_ha_state. + mock_client.read_fan_mode.side_effect = Exception("fake failure") + data = {ATTR_FAN_MODE: mode} + data[ATTR_ENTITY_ID] = "climate.system_1_zone_1" + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, data, blocking=True + ) + assert ( + hass.states.get("climate.system_1_zone_1").attributes[ATTR_FAN_MODE] == mode + ) + mock_client.set_fan_mode.assert_called_with(mode) + + +@pytest.mark.parametrize( + ("hvac_mode", "evolution_mode"), + [("heat_cool", "auto"), ("heat", "heat"), ("cool", "cool"), ("off", "off")], +) +async def test_set_hvac_mode( + hass: HomeAssistant, + mock_evolution_entry: MockConfigEntry, + mock_evolution_client_factory: Generator[AsyncMock, None, None], + hvac_mode, + evolution_mode, +) -> None: + """Test that setting HVAC mode works.""" + mock_client = await mock_evolution_client_factory(1, 1, "/dev/unused") + + # Make the call, modifting the mock client to throw an exception on + # read to ensure that the update is visible iff we call + # async_update_ha_state. + data = {ATTR_HVAC_MODE: hvac_mode} + data[ATTR_ENTITY_ID] = "climate.system_1_zone_1" + mock_client.read_hvac_mode.side_effect = Exception("fake failure") + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, data, blocking=True + ) + await hass.async_block_till_done() + assert hass.states.get("climate.system_1_zone_1").state == evolution_mode + mock_client.set_hvac_mode.assert_called_with(evolution_mode) + + +@pytest.mark.parametrize( + ("curr_temp", "expected_action"), + [(62, HVACAction.HEATING), (70, HVACAction.OFF), (80, HVACAction.COOLING)], +) +async def test_read_hvac_action_heat_cool( + hass: HomeAssistant, + mock_evolution_entry: MockConfigEntry, + mock_evolution_client_factory: Generator[AsyncMock, None, None], + freezer: FrozenDateTimeFactory, + curr_temp: int, + expected_action: HVACAction, +) -> None: + """Test that we can read the current HVAC action in heat_cool mode.""" + htsp = 68 + clsp = 72 + + mock_client = await mock_evolution_client_factory(1, 1, "/dev/unused") + mock_client.read_heating_setpoint.return_value = htsp + mock_client.read_cooling_setpoint.return_value = clsp + is_active = curr_temp < htsp or curr_temp > clsp + mock_client.read_hvac_mode.return_value = ("auto", is_active) + mock_client.read_current_temperature.return_value = curr_temp + await trigger_polling(hass, freezer) + state = hass.states.get("climate.system_1_zone_1") + assert state.attributes[ATTR_HVAC_ACTION] == expected_action + + +@pytest.mark.parametrize( + ("mode", "active", "expected_action"), + [ + ("heat", True, "heating"), + ("heat", False, "off"), + ("cool", True, "cooling"), + ("cool", False, "off"), + ("off", False, "off"), + ], +) +async def test_read_hvac_action( + hass: HomeAssistant, + mock_evolution_entry: MockConfigEntry, + mock_evolution_client_factory: Generator[AsyncMock, None, None], + freezer: FrozenDateTimeFactory, + mode: str, + active: bool, + expected_action: str, +) -> None: + """Test that we can read the current HVAC action.""" + # Initial state should be no action. + assert ( + hass.states.get("climate.system_1_zone_1").attributes[ATTR_HVAC_ACTION] + == HVACAction.OFF + ) + # Perturb the system and verify we see an action. + mock_client = await mock_evolution_client_factory(1, 1, "/dev/unused") + mock_client.read_heating_setpoint.return_value = 75 # Needed if mode == heat + mock_client.read_hvac_mode.return_value = (mode, active) + await trigger_polling(hass, freezer) + assert ( + hass.states.get("climate.system_1_zone_1").attributes[ATTR_HVAC_ACTION] + == expected_action + ) diff --git a/tests/components/bryant_evolution/test_config_flow.py b/tests/components/bryant_evolution/test_config_flow.py new file mode 100644 index 00000000000..39d203201eb --- /dev/null +++ b/tests/components/bryant_evolution/test_config_flow.py @@ -0,0 +1,170 @@ +"""Test the Bryant Evolution config flow.""" + +from unittest.mock import DEFAULT, AsyncMock, patch + +from evolutionhttp import BryantEvolutionLocalClient, ZoneInfo + +from homeassistant import config_entries +from homeassistant.components.bryant_evolution.const import CONF_SYSTEM_ZONE, DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_FILENAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_form_success(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + with ( + patch.object( + BryantEvolutionLocalClient, + "enumerate_zones", + return_value=DEFAULT, + ) as mock_call, + ): + mock_call.side_effect = lambda system_id, filename: { + 1: [ZoneInfo(1, 1, "S1Z1"), ZoneInfo(1, 2, "S1Z2")], + 2: [ZoneInfo(2, 3, "S2Z2"), ZoneInfo(2, 4, "S2Z3")], + }.get(system_id, []) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_FILENAME: "test_form_success", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY, result + assert result["title"] == "SAM at test_form_success" + assert result["data"] == { + CONF_FILENAME: "test_form_success", + CONF_SYSTEM_ZONE: [(1, 1), (1, 2), (2, 3), (2, 4)], + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect( + hass: HomeAssistant, + mock_evolution_client_factory: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with ( + patch.object( + BryantEvolutionLocalClient, + "enumerate_zones", + return_value=DEFAULT, + ) as mock_call, + ): + mock_call.return_value = [] + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_FILENAME: "test_form_cannot_connect", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + with ( + patch.object( + BryantEvolutionLocalClient, + "enumerate_zones", + return_value=DEFAULT, + ) as mock_call, + ): + mock_call.side_effect = lambda system_id, filename: { + 1: [ZoneInfo(1, 1, "S1Z1"), ZoneInfo(1, 2, "S1Z2")], + 2: [ZoneInfo(2, 3, "S2Z3"), ZoneInfo(2, 4, "S2Z4")], + }.get(system_id, []) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_FILENAME: "some-serial", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "SAM at some-serial" + assert result["data"] == { + CONF_FILENAME: "some-serial", + CONF_SYSTEM_ZONE: [(1, 1), (1, 2), (2, 3), (2, 4)], + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect_bad_file( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_evolution_client_factory: AsyncMock, +) -> None: + """Test we handle cannot connect error from a missing file.""" + mock_evolution_client_factory.side_effect = FileNotFoundError("test error") + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + # This file does not exist. + CONF_FILENAME: "test_form_cannot_connect_bad_file", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_reconfigure( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_evolution_entry: MockConfigEntry, +) -> None: + """Test that reconfigure discovers additional systems and zones.""" + + # Reconfigure with additional systems and zones. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": mock_evolution_entry.entry_id, + }, + ) + with ( + patch.object( + BryantEvolutionLocalClient, + "enumerate_zones", + return_value=DEFAULT, + ) as mock_call, + ): + mock_call.side_effect = lambda system_id, filename: { + 1: [ZoneInfo(1, 1, "S1Z1")], + 2: [ZoneInfo(2, 3, "S2Z3"), ZoneInfo(2, 4, "S2Z4"), ZoneInfo(2, 5, "S2Z5")], + }.get(system_id, []) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_FILENAME: "test_reconfigure", + }, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT, result + assert result["reason"] == "reconfigured" + config_entry = hass.config_entries.async_entries()[0] + assert config_entry.data[CONF_SYSTEM_ZONE] == [ + (1, 1), + (2, 3), + (2, 4), + (2, 5), + ] diff --git a/tests/components/bryant_evolution/test_init.py b/tests/components/bryant_evolution/test_init.py new file mode 100644 index 00000000000..72734f7e117 --- /dev/null +++ b/tests/components/bryant_evolution/test_init.py @@ -0,0 +1,112 @@ +"""Test setup for the bryant_evolution integration.""" + +import logging +from unittest.mock import AsyncMock + +from evolutionhttp import BryantEvolutionLocalClient +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.components.bryant_evolution.const import CONF_SYSTEM_ZONE, DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_FILENAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM + +from .conftest import DEFAULT_SYSTEM_ZONES +from .test_climate import trigger_polling + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + + +async def test_setup_integration_prevented_by_unavailable_client( + hass: HomeAssistant, mock_evolution_client_factory: AsyncMock +) -> None: + """Test that setup throws ConfigEntryNotReady when the client is unavailable.""" + mock_evolution_client_factory.side_effect = FileNotFoundError("test error") + mock_evolution_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_FILENAME: "test_setup_integration_prevented_by_unavailable_client", + CONF_SYSTEM_ZONE: [(1, 1)], + }, + ) + mock_evolution_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_evolution_entry.entry_id) + await hass.async_block_till_done() + assert mock_evolution_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_integration_client_returns_none( + hass: HomeAssistant, mock_evolution_client_factory: AsyncMock +) -> None: + """Test that an unavailable client causes ConfigEntryNotReady.""" + mock_client = AsyncMock(spec=BryantEvolutionLocalClient) + mock_evolution_client_factory.side_effect = None + mock_evolution_client_factory.return_value = mock_client + mock_client.read_fan_mode.return_value = None + mock_client.read_current_temperature.return_value = None + mock_client.read_hvac_mode.return_value = None + mock_client.read_cooling_setpoint.return_value = None + mock_client.read_zone_name.return_value = None + mock_evolution_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_FILENAME: "/dev/ttyUSB0", CONF_SYSTEM_ZONE: [(1, 1)]}, + ) + mock_evolution_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_evolution_entry.entry_id) + await hass.async_block_till_done() + assert mock_evolution_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_multiple_systems_zones( + hass: HomeAssistant, + mock_evolution_client_factory: AsyncMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that a device with multiple systems and zones works.""" + hass.config.units = US_CUSTOMARY_SYSTEM + mock_evolution_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_FILENAME: "/dev/ttyUSB0", CONF_SYSTEM_ZONE: DEFAULT_SYSTEM_ZONES}, + ) + mock_evolution_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_evolution_entry.entry_id) + await hass.async_block_till_done() + + # Set the temperature of each zone to its zone number so that we can + # ensure we've created the right client for each zone. + for sz, client in mock_evolution_entry.runtime_data.items(): + client.read_current_temperature.return_value = sz[1] + await trigger_polling(hass, freezer) + + # Check that each system and zone has the expected temperature value to + # verify that the initial setup flow worked as expected. + for sz in DEFAULT_SYSTEM_ZONES: + system = sz[0] + zone = sz[1] + state = hass.states.get(f"climate.system_{system}_zone_{zone}") + assert state, hass.states.async_all() + assert state.attributes["current_temperature"] == zone + + # Check that the created devices are wired to each other as expected. + device_registry = dr.async_get(hass) + + def find_device(name): + return next(filter(lambda x: x.name == name, device_registry.devices.values())) + + sam = find_device("System Access Module") + s1 = find_device("System 1") + s2 = find_device("System 2") + s1z1 = find_device("System 1 Zone 1") + s1z2 = find_device("System 1 Zone 2") + s2z3 = find_device("System 2 Zone 3") + + assert sam.via_device_id is None + assert s1.via_device_id == sam.id + assert s2.via_device_id == sam.id + assert s1z1.via_device_id == s1.id + assert s1z2.via_device_id == s1.id + assert s2z3.via_device_id == s2.id From ca430f0e7b31dfbde9a36596d432c24303e6f435 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 04:36:44 -0500 Subject: [PATCH 1640/2411] Add coverage for fixing missing params in the doorbird schedule (#122745) --- homeassistant/components/doorbird/device.py | 3 +- tests/components/doorbird/__init__.py | 5 +- tests/components/doorbird/conftest.py | 14 +++- .../fixtures/schedule_wrong_param.json | 67 +++++++++++++++++++ tests/components/doorbird/test_device.py | 59 ++++++++++++++++ 5 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 tests/components/doorbird/fixtures/schedule_wrong_param.json create mode 100644 tests/components/doorbird/test_device.py diff --git a/homeassistant/components/doorbird/device.py b/homeassistant/components/doorbird/device.py index 9bb3397d0ff..866251f3d28 100644 --- a/homeassistant/components/doorbird/device.py +++ b/homeassistant/components/doorbird/device.py @@ -103,9 +103,8 @@ class ConfiguredDoorBird: async def async_register_events(self) -> None: """Register events on device.""" if not self.door_station_events: - # User may not have permission to get the favorites + # The config entry might not have any events configured yet return - http_fav = await self._async_register_events() event_config = await self._async_get_event_config(http_fav) _LOGGER.debug("%s: Event config: %s", self.name, event_config) diff --git a/tests/components/doorbird/__init__.py b/tests/components/doorbird/__init__.py index 59ce6ecd958..41def92f121 100644 --- a/tests/components/doorbird/__init__.py +++ b/tests/components/doorbird/__init__.py @@ -49,6 +49,7 @@ def get_mock_doorbird_api( schedule: list[DoorBirdScheduleEntry] | None = None, favorites: dict[str, dict[str, Any]] | None = None, favorites_side_effect: Exception | None = None, + change_schedule: tuple[bool, int] | None = None, ) -> DoorBird: """Return a mock DoorBirdAPI object with return values.""" doorbirdapi_mock = MagicMock(spec_set=DoorBird) @@ -60,7 +61,9 @@ def get_mock_doorbird_api( return_value=favorites, ) type(doorbirdapi_mock).change_favorite = AsyncMock(return_value=True) - type(doorbirdapi_mock).change_schedule = AsyncMock(return_value=(True, 200)) + type(doorbirdapi_mock).change_schedule = AsyncMock( + return_value=change_schedule or (True, 200) + ) type(doorbirdapi_mock).schedule = AsyncMock(return_value=schedule) type(doorbirdapi_mock).energize_relay = AsyncMock(return_value=True) type(doorbirdapi_mock).turn_light_on = AsyncMock(return_value=True) diff --git a/tests/components/doorbird/conftest.py b/tests/components/doorbird/conftest.py index cd3e410624d..59ead250293 100644 --- a/tests/components/doorbird/conftest.py +++ b/tests/components/doorbird/conftest.py @@ -46,6 +46,14 @@ def doorbird_schedule() -> list[DoorBirdScheduleEntry]: ) +@pytest.fixture(scope="session") +def doorbird_schedule_wrong_param() -> list[DoorBirdScheduleEntry]: + """Return a loaded DoorBird schedule fixture with an incorrect param.""" + return DoorBirdScheduleEntry.parse_all( + load_json_value_fixture("schedule_wrong_param.json", "doorbird") + ) + + @pytest.fixture(scope="session") def doorbird_favorites() -> dict[str, dict[str, Any]]: """Return a loaded DoorBird favorites fixture.""" @@ -90,18 +98,21 @@ async def doorbird_mocker( async def _async_mock( entry: MockConfigEntry | None = None, api: DoorBird | None = None, + change_schedule: tuple[bool, int] | None = None, info: dict[str, Any] | None = None, info_side_effect: Exception | None = None, schedule: list[DoorBirdScheduleEntry] | None = None, favorites: dict[str, dict[str, Any]] | None = None, favorites_side_effect: Exception | None = None, + options: dict[str, Any] | None = None, ) -> MockDoorbirdEntry: """Create a MockDoorbirdEntry from defaults or specific values.""" entry = entry or MockConfigEntry( domain=DOMAIN, unique_id="1CCAE3AAAAAA", data=VALID_CONFIG, - options={CONF_EVENTS: [DEFAULT_DOORBELL_EVENT, DEFAULT_MOTION_EVENT]}, + options=options + or {CONF_EVENTS: [DEFAULT_DOORBELL_EVENT, DEFAULT_MOTION_EVENT]}, ) api = api or get_mock_doorbird_api( info=info or doorbird_info, @@ -109,6 +120,7 @@ async def doorbird_mocker( schedule=schedule or doorbird_schedule, favorites=favorites or doorbird_favorites, favorites_side_effect=favorites_side_effect, + change_schedule=change_schedule, ) entry.add_to_hass(hass) with patch_doorbird_api_entry_points(api): diff --git a/tests/components/doorbird/fixtures/schedule_wrong_param.json b/tests/components/doorbird/fixtures/schedule_wrong_param.json new file mode 100644 index 00000000000..724f19b1774 --- /dev/null +++ b/tests/components/doorbird/fixtures/schedule_wrong_param.json @@ -0,0 +1,67 @@ +[ + { + "input": "doorbell", + "param": "99", + "output": [ + { + "event": "notify", + "param": "", + "schedule": { + "weekdays": [ + { + "to": "107999", + "from": "108000" + } + ] + } + }, + { + "event": "http", + "param": "0", + "schedule": { + "weekdays": [ + { + "to": "107999", + "from": "108000" + } + ] + } + } + ] + }, + { + "input": "motion", + "param": "", + "output": [ + { + "event": "notify", + "param": "", + "schedule": { + "weekdays": [ + { + "to": "107999", + "from": "108000" + } + ] + } + }, + { + "event": "http", + "param": "5", + "schedule": { + "weekdays": [ + { + "to": "107999", + "from": "108000" + } + ] + } + } + ] + }, + { + "input": "relay", + "param": "1", + "output": [] + } +] diff --git a/tests/components/doorbird/test_device.py b/tests/components/doorbird/test_device.py new file mode 100644 index 00000000000..cf3beae5e68 --- /dev/null +++ b/tests/components/doorbird/test_device.py @@ -0,0 +1,59 @@ +"""Test DoorBird device.""" + +from copy import deepcopy +from http import HTTPStatus + +from doorbirdpy import DoorBirdScheduleEntry +import pytest + +from homeassistant.components.doorbird.const import CONF_EVENTS +from homeassistant.core import HomeAssistant + +from .conftest import DoorbirdMockerType + + +async def test_no_configured_events( + hass: HomeAssistant, + doorbird_mocker: DoorbirdMockerType, +) -> None: + """Test a doorbird with no events configured.""" + await doorbird_mocker(options={CONF_EVENTS: []}) + assert not hass.states.async_all("event") + + +async def test_change_schedule_success( + doorbird_mocker: DoorbirdMockerType, + doorbird_schedule_wrong_param: list[DoorBirdScheduleEntry], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test a doorbird when change_schedule fails.""" + schedule_copy = deepcopy(doorbird_schedule_wrong_param) + mock_doorbird = await doorbird_mocker(schedule=schedule_copy) + assert "Unable to update schedule entry mydoorbird" not in caplog.text + assert mock_doorbird.api.change_schedule.call_count == 1 + new_schedule: list[DoorBirdScheduleEntry] = ( + mock_doorbird.api.change_schedule.call_args[0] + ) + # Ensure the attempt to update the schedule to fix the incorrect + # param is made + assert new_schedule[-1].output[-1].param == "1" + + +async def test_change_schedule_fails( + doorbird_mocker: DoorbirdMockerType, + doorbird_schedule_wrong_param: list[DoorBirdScheduleEntry], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test a doorbird when change_schedule fails.""" + schedule_copy = deepcopy(doorbird_schedule_wrong_param) + mock_doorbird = await doorbird_mocker( + schedule=schedule_copy, change_schedule=(False, HTTPStatus.UNAUTHORIZED) + ) + assert "Unable to update schedule entry mydoorbird" in caplog.text + assert mock_doorbird.api.change_schedule.call_count == 1 + new_schedule: list[DoorBirdScheduleEntry] = ( + mock_doorbird.api.change_schedule.call_args[0] + ) + # Ensure the attempt to update the schedule to fix the incorrect + # param is made + assert new_schedule[-1].output[-1].param == "1" From efaf75f2e6c81563c36cfa4334095a371e6c3028 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 29 Jul 2024 11:38:21 +0200 Subject: [PATCH 1641/2411] Rename recorder INTEGRATION_PLATFORMS_RUN_IN_RECORDER_THREAD (#122758) --- homeassistant/components/recorder/__init__.py | 7 ++----- homeassistant/components/recorder/const.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 41fa8db5814..8564827d839 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -33,7 +33,7 @@ from .const import ( # noqa: F401 CONF_DB_INTEGRITY_CHECK, DOMAIN, INTEGRATION_PLATFORM_COMPILE_STATISTICS, - INTEGRATION_PLATFORMS_RUN_IN_RECORDER_THREAD, + INTEGRATION_PLATFORM_METHODS, SQLITE_URL_PREFIX, SupportedDialect, ) @@ -189,10 +189,7 @@ async def _async_setup_integration_platform( """Process a recorder platform.""" # If the platform has a compile_statistics method, we need to # add it to the recorder queue to be processed. - if any( - hasattr(platform, _attr) - for _attr in INTEGRATION_PLATFORMS_RUN_IN_RECORDER_THREAD - ): + if any(hasattr(platform, _attr) for _attr in INTEGRATION_PLATFORM_METHODS): instance.queue_task(AddRecorderPlatformTask(domain, platform)) await async_process_integration_platforms(hass, DOMAIN, _process_recorder_platform) diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index c7dba18cad9..066ae938971 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -66,7 +66,7 @@ INTEGRATION_PLATFORM_COMPILE_STATISTICS = "compile_statistics" INTEGRATION_PLATFORM_VALIDATE_STATISTICS = "validate_statistics" INTEGRATION_PLATFORM_LIST_STATISTIC_IDS = "list_statistic_ids" -INTEGRATION_PLATFORMS_RUN_IN_RECORDER_THREAD = { +INTEGRATION_PLATFORM_METHODS = { INTEGRATION_PLATFORM_COMPILE_STATISTICS, INTEGRATION_PLATFORM_VALIDATE_STATISTICS, INTEGRATION_PLATFORM_LIST_STATISTIC_IDS, From 5467685bd8d8390f465bee122234f5e9c6f5a1bd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 29 Jul 2024 11:38:36 +0200 Subject: [PATCH 1642/2411] Adjust warning message when recorder is doing offline migration (#122509) * Adjust warning message when recorder is doing offline migration * Address review comments --- .../components/recorder/migration.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 6f438106ab6..d7c5e7f0ea0 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -120,6 +120,13 @@ if TYPE_CHECKING: from . import Recorder LIVE_MIGRATION_MIN_SCHEMA_VERSION = 0 + +MIGRATION_NOTE_OFFLINE = ( + "Note: this may take several hours on large databases and slow machines. " + "Home Assistant will not start until the upgrade is completed. Please be patient " + "and do not turn off or restart Home Assistant while the upgrade is in progress!" +) + _EMPTY_ENTITY_ID = "missing.entity_id" _EMPTY_EVENT_TYPE = "missing_event_type" @@ -276,9 +283,12 @@ def _migrate_schema( if current_version < end_version: _LOGGER.warning( - "Database is about to upgrade from schema version: %s to: %s", + "The database is about to upgrade from schema version %s to %s%s", current_version, end_version, + f". {MIGRATION_NOTE_OFFLINE}" + if current_version < LIVE_MIGRATION_MIN_SCHEMA_VERSION + else "", ) schema_status = dataclass_replace(schema_status, current_version=end_version) @@ -362,7 +372,7 @@ def _create_index( _LOGGER.debug("Creating %s index", index_name) _LOGGER.warning( "Adding index `%s` to table `%s`. Note: this can take several " - "minutes on large databases and slow computers. Please " + "minutes on large databases and slow machines. Please " "be patient!", index_name, table_name, @@ -411,7 +421,7 @@ def _drop_index( """ _LOGGER.warning( "Dropping index `%s` from table `%s`. Note: this can take several " - "minutes on large databases and slow computers. Please " + "minutes on large databases and slow machines. Please " "be patient!", index_name, table_name, @@ -462,7 +472,7 @@ def _add_columns( _LOGGER.warning( ( "Adding columns %s to table %s. Note: this can take several " - "minutes on large databases and slow computers. Please " + "minutes on large databases and slow machines. Please " "be patient!" ), ", ".join(column.split(" ")[0] for column in columns_def), @@ -524,7 +534,7 @@ def _modify_columns( _LOGGER.warning( ( "Modifying columns %s in table %s. Note: this can take several " - "minutes on large databases and slow computers. Please " + "minutes on large databases and slow machines. Please " "be patient!" ), ", ".join(column.split(" ")[0] for column in columns_def), @@ -1554,7 +1564,7 @@ def _correct_table_character_set_and_collation( _LOGGER.warning( "Updating character set and collation of table %s to utf8mb4. " "Note: this can take several minutes on large databases and slow " - "computers. Please be patient!", + "machines. Please be patient!", table, ) with ( From 06ee8fdd47b5a36f58c6e4bce622dd2e79655cbd Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 29 Jul 2024 11:43:04 +0200 Subject: [PATCH 1643/2411] Do not use get_hub in deCONZ tests (#122706) --- tests/components/deconz/test_hub.py | 30 ++++++++++------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/tests/components/deconz/test_hub.py b/tests/components/deconz/test_hub.py index 43c2dccae93..43c51179337 100644 --- a/tests/components/deconz/test_hub.py +++ b/tests/components/deconz/test_hub.py @@ -9,7 +9,6 @@ from syrupy import SnapshotAssertion from homeassistant.components import ssdp from homeassistant.components.deconz.config_flow import DECONZ_MANUFACTURERURL from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN -from homeassistant.components.deconz.hub import DeconzHub from homeassistant.components.ssdp import ( ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL, @@ -71,13 +70,15 @@ async def test_update_address( hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Make sure that connection status triggers a dispatcher send.""" - gateway = DeconzHub.get_hub(hass, config_entry_setup) - assert gateway.api.host == "1.2.3.4" + assert config_entry_setup.data["host"] == "1.2.3.4" - with patch( - "homeassistant.components.deconz.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.deconz.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch("pydeconz.gateway.WSClient") as ws_mock, + ): await hass.config_entries.flow.async_init( DECONZ_DOMAIN, data=ssdp.SsdpServiceInfo( @@ -94,17 +95,6 @@ async def test_update_address( ) await hass.async_block_till_done() - assert gateway.api.host == "2.3.4.5" + assert ws_mock.call_args[0][1] == "2.3.4.5" + assert config_entry_setup.data["host"] == "2.3.4.5" assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_reset_after_successful_setup( - hass: HomeAssistant, config_entry_setup: MockConfigEntry -) -> None: - """Make sure that connection status triggers a dispatcher send.""" - gateway = DeconzHub.get_hub(hass, config_entry_setup) - - result = await gateway.async_reset() - await hass.async_block_till_done() - - assert result is True From 70df4ca461610afeadbb5a4c9d18cd9a8de5bb2a Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Mon, 29 Jul 2024 11:44:01 +0200 Subject: [PATCH 1644/2411] Integration for IronOS (Pinecil V2) soldering irons (#120802) * Add Pinecil integration * Refactor with new library * Add tests for config flow, remove unused code * requested changes * update requirements * Move some sensor values to diagnostics, add tests for sensors * User service uuid in discovery * fix manufacturer name * Bump pynecil to version 0.2.0 * Rename integration to IronOS * Recreate snapshot * Update strings * type checking * Update snapshot * Add async_setup to coordinator * Show device id with serial number * Added missing boost to operation mode states * remove super call * Refactor * tests --- CODEOWNERS | 2 + homeassistant/components/iron_os/__init__.py | 53 ++ .../components/iron_os/config_flow.py | 83 +++ homeassistant/components/iron_os/const.py | 10 + .../components/iron_os/coordinator.py | 49 ++ homeassistant/components/iron_os/entity.py | 41 ++ homeassistant/components/iron_os/icons.json | 61 ++ .../components/iron_os/manifest.json | 17 + homeassistant/components/iron_os/sensor.py | 199 +++++ homeassistant/components/iron_os/strings.json | 84 +++ homeassistant/generated/bluetooth.py | 5 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/iron_os/__init__.py | 1 + tests/components/iron_os/conftest.py | 141 ++++ .../iron_os/snapshots/test_sensor.ambr | 683 ++++++++++++++++++ tests/components/iron_os/test_config_flow.py | 66 ++ tests/components/iron_os/test_sensor.py | 73 ++ 20 files changed, 1581 insertions(+) create mode 100644 homeassistant/components/iron_os/__init__.py create mode 100644 homeassistant/components/iron_os/config_flow.py create mode 100644 homeassistant/components/iron_os/const.py create mode 100644 homeassistant/components/iron_os/coordinator.py create mode 100644 homeassistant/components/iron_os/entity.py create mode 100644 homeassistant/components/iron_os/icons.json create mode 100644 homeassistant/components/iron_os/manifest.json create mode 100644 homeassistant/components/iron_os/sensor.py create mode 100644 homeassistant/components/iron_os/strings.json create mode 100644 tests/components/iron_os/__init__.py create mode 100644 tests/components/iron_os/conftest.py create mode 100644 tests/components/iron_os/snapshots/test_sensor.ambr create mode 100644 tests/components/iron_os/test_config_flow.py create mode 100644 tests/components/iron_os/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index fc18be91239..bf93676f962 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -710,6 +710,8 @@ build.json @home-assistant/supervisor /homeassistant/components/iqvia/ @bachya /tests/components/iqvia/ @bachya /homeassistant/components/irish_rail_transport/ @ttroy50 +/homeassistant/components/iron_os/ @tr4nt0r +/tests/components/iron_os/ @tr4nt0r /homeassistant/components/isal/ @bdraco /tests/components/isal/ @bdraco /homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair diff --git a/homeassistant/components/iron_os/__init__.py b/homeassistant/components/iron_os/__init__.py new file mode 100644 index 00000000000..bf3c6c34c83 --- /dev/null +++ b/homeassistant/components/iron_os/__init__.py @@ -0,0 +1,53 @@ +"""The IronOS integration.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from pynecil import Pynecil + +from homeassistant.components import bluetooth +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN +from .coordinator import IronOSCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +type IronOSConfigEntry = ConfigEntry[IronOSCoordinator] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: IronOSConfigEntry) -> bool: + """Set up IronOS from a config entry.""" + if TYPE_CHECKING: + assert entry.unique_id + ble_device = bluetooth.async_ble_device_from_address( + hass, entry.unique_id, connectable=True + ) + if not ble_device: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="setup_device_unavailable_exception", + translation_placeholders={CONF_NAME: entry.title}, + ) + + device = Pynecil(ble_device) + + coordinator = IronOSCoordinator(hass, device) + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: IronOSConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/iron_os/config_flow.py b/homeassistant/components/iron_os/config_flow.py new file mode 100644 index 00000000000..444db79c926 --- /dev/null +++ b/homeassistant/components/iron_os/config_flow.py @@ -0,0 +1,83 @@ +"""Config flow for IronOS integration.""" + +from __future__ import annotations + +from typing import Any + +from habluetooth import BluetoothServiceInfoBleak +import voluptuous as vol + +from homeassistant.components.bluetooth.api import async_discovered_service_info +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_ADDRESS + +from .const import DISCOVERY_SVC_UUID, DOMAIN + + +class IronOSConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for IronOS.""" + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> ConfigFlowResult: + """Handle bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + self._discovery_info = discovery_info + + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm discovery.""" + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = 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 + ) -> ConfigFlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + title = self._discovered_devices[address] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=title, data={}) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, True): + address = discovery_info.address + if ( + DISCOVERY_SVC_UUID not in discovery_info.service_uuids + or address in current_addresses + or address in self._discovered_devices + ): + continue + self._discovered_devices[address] = discovery_info.name + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/iron_os/const.py b/homeassistant/components/iron_os/const.py new file mode 100644 index 00000000000..86b7d401f4f --- /dev/null +++ b/homeassistant/components/iron_os/const.py @@ -0,0 +1,10 @@ +"""Constants for the IronOS integration.""" + +DOMAIN = "iron_os" + +MANUFACTURER = "PINE64" +MODEL = "Pinecil V2" + +OHM = "Ω" + +DISCOVERY_SVC_UUID = "9eae1000-9d0d-48c5-aa55-33e27f9bc533" diff --git a/homeassistant/components/iron_os/coordinator.py b/homeassistant/components/iron_os/coordinator.py new file mode 100644 index 00000000000..e8424478d86 --- /dev/null +++ b/homeassistant/components/iron_os/coordinator.py @@ -0,0 +1,49 @@ +"""Update coordinator for IronOS Integration.""" + +from __future__ import annotations + +from datetime import timedelta +import logging + +from pynecil import CommunicationError, DeviceInfoResponse, LiveDataResponse, Pynecil + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=5) + + +class IronOSCoordinator(DataUpdateCoordinator[LiveDataResponse]): + """IronOS coordinator.""" + + device_info: DeviceInfoResponse + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, device: Pynecil) -> None: + """Initialize IronOS coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + self.device = device + + async def _async_update_data(self) -> LiveDataResponse: + """Fetch data from Device.""" + + try: + return await self.device.get_live_data() + + except CommunicationError as e: + raise UpdateFailed("Cannot connect to device") from e + + async def _async_setup(self) -> None: + """Set up the coordinator.""" + + self.device_info = await self.device.get_device_info() diff --git a/homeassistant/components/iron_os/entity.py b/homeassistant/components/iron_os/entity.py new file mode 100644 index 00000000000..5a24b0a5567 --- /dev/null +++ b/homeassistant/components/iron_os/entity.py @@ -0,0 +1,41 @@ +"""Base entity for IronOS integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import MANUFACTURER, MODEL +from .coordinator import IronOSCoordinator + + +class IronOSBaseEntity(CoordinatorEntity[IronOSCoordinator]): + """Base IronOS entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: IronOSCoordinator, + entity_description: EntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + + self.entity_description = entity_description + self._attr_unique_id = ( + f"{coordinator.config_entry.unique_id}_{entity_description.key}" + ) + if TYPE_CHECKING: + assert coordinator.config_entry.unique_id + self.device_info = DeviceInfo( + connections={(CONNECTION_BLUETOOTH, coordinator.config_entry.unique_id)}, + manufacturer=MANUFACTURER, + model=MODEL, + name="Pinecil", + sw_version=coordinator.device_info.build, + serial_number=f"{coordinator.device_info.device_sn} (ID:{coordinator.device_info.device_id})", + ) diff --git a/homeassistant/components/iron_os/icons.json b/homeassistant/components/iron_os/icons.json new file mode 100644 index 00000000000..0d207607a4f --- /dev/null +++ b/homeassistant/components/iron_os/icons.json @@ -0,0 +1,61 @@ +{ + "entity": { + "sensor": { + "live_temperature": { + "default": "mdi:soldering-iron" + }, + "setpoint_temperature": { + "default": "mdi:thermostat" + }, + "voltage": { + "default": "mdi:current-dc" + }, + "handle_temperature": { + "default": "mdi:grease-pencil" + }, + "power_pwm_level": { + "default": "mdi:square-wave" + }, + "power_source": { + "default": "mdi:power-plug", + "state": { + "dc": "mdi:record-circle-outline", + "qc": "mdi:usb-port", + "pd_vbus": "mdi:usb-c-port", + "pd": "mdi:usb-c-port" + } + }, + "tip_resistance": { + "default": "mdi:omega" + }, + "hall_sensor": { + "default": "mdi:leak" + }, + "movement_time": { + "default": "mdi:clock-fast" + }, + "max_tip_temp_ability": { + "default": "mdi:thermometer-chevron-up" + }, + "uptime": { + "default": "mdi:progress-clock" + }, + "tip_voltage": { + "default": "mdi:sine-wave" + }, + "operating_mode": { + "default": "mdi:format-list-bulleted", + "state": { + "boost": "mdi:rocket-launch", + "soldering": "mdi:soldering-iron", + "sleeping": "mdi:sleep", + "settings": "mdi:menu-open", + "debug": "mdi:bug-play" + } + }, + "estimated_power": { + "default": "mdi:flash" + } + } + } +} diff --git a/homeassistant/components/iron_os/manifest.json b/homeassistant/components/iron_os/manifest.json new file mode 100644 index 00000000000..cfaf36880f2 --- /dev/null +++ b/homeassistant/components/iron_os/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "iron_os", + "name": "IronOS", + "bluetooth": [ + { + "service_uuid": "9eae1000-9d0d-48c5-aa55-33e27f9bc533", + "connectable": true + } + ], + "codeowners": ["@tr4nt0r"], + "config_flow": true, + "dependencies": ["bluetooth_adapters"], + "documentation": "https://www.home-assistant.io/integrations/iron_os", + "iot_class": "local_polling", + "loggers": ["pynecil"], + "requirements": ["pynecil==0.2.0"] +} diff --git a/homeassistant/components/iron_os/sensor.py b/homeassistant/components/iron_os/sensor.py new file mode 100644 index 00000000000..095ffd254df --- /dev/null +++ b/homeassistant/components/iron_os/sensor.py @@ -0,0 +1,199 @@ +"""Sensor platform for IronOS integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from enum import StrEnum + +from pynecil import LiveDataResponse, OperatingMode, PowerSource + +from homeassistant.components.sensor import ( + EntityCategory, + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + PERCENTAGE, + UnitOfElectricPotential, + UnitOfPower, + UnitOfTemperature, + UnitOfTime, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import IronOSConfigEntry +from .const import OHM +from .entity import IronOSBaseEntity + + +class PinecilSensor(StrEnum): + """Pinecil Sensors.""" + + LIVE_TEMP = "live_temperature" + SETPOINT_TEMP = "setpoint_temperature" + DC_VOLTAGE = "voltage" + HANDLETEMP = "handle_temperature" + PWMLEVEL = "power_pwm_level" + POWER_SRC = "power_source" + TIP_RESISTANCE = "tip_resistance" + UPTIME = "uptime" + MOVEMENT_TIME = "movement_time" + MAX_TIP_TEMP_ABILITY = "max_tip_temp_ability" + TIP_VOLTAGE = "tip_voltage" + HALL_SENSOR = "hall_sensor" + OPERATING_MODE = "operating_mode" + ESTIMATED_POWER = "estimated_power" + + +@dataclass(frozen=True, kw_only=True) +class IronOSSensorEntityDescription(SensorEntityDescription): + """IronOS sensor entity descriptions.""" + + value_fn: Callable[[LiveDataResponse], StateType] + + +PINECIL_SENSOR_DESCRIPTIONS: tuple[IronOSSensorEntityDescription, ...] = ( + IronOSSensorEntityDescription( + key=PinecilSensor.LIVE_TEMP, + translation_key=PinecilSensor.LIVE_TEMP, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.live_temp, + ), + IronOSSensorEntityDescription( + key=PinecilSensor.DC_VOLTAGE, + translation_key=PinecilSensor.DC_VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.dc_voltage, + entity_category=EntityCategory.DIAGNOSTIC, + ), + IronOSSensorEntityDescription( + key=PinecilSensor.HANDLETEMP, + translation_key=PinecilSensor.HANDLETEMP, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.handle_temp, + ), + IronOSSensorEntityDescription( + key=PinecilSensor.PWMLEVEL, + translation_key=PinecilSensor.PWMLEVEL, + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=0, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.pwm_level, + entity_category=EntityCategory.DIAGNOSTIC, + ), + IronOSSensorEntityDescription( + key=PinecilSensor.POWER_SRC, + translation_key=PinecilSensor.POWER_SRC, + device_class=SensorDeviceClass.ENUM, + options=[item.name.lower() for item in PowerSource], + value_fn=lambda data: data.power_src.name.lower() if data.power_src else None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + IronOSSensorEntityDescription( + key=PinecilSensor.TIP_RESISTANCE, + translation_key=PinecilSensor.TIP_RESISTANCE, + native_unit_of_measurement=OHM, + value_fn=lambda data: data.tip_resistance, + entity_category=EntityCategory.DIAGNOSTIC, + ), + IronOSSensorEntityDescription( + key=PinecilSensor.UPTIME, + translation_key=PinecilSensor.UPTIME, + native_unit_of_measurement=UnitOfTime.SECONDS, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.uptime, + entity_category=EntityCategory.DIAGNOSTIC, + ), + IronOSSensorEntityDescription( + key=PinecilSensor.MOVEMENT_TIME, + translation_key=PinecilSensor.MOVEMENT_TIME, + native_unit_of_measurement=UnitOfTime.SECONDS, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.movement_time, + entity_category=EntityCategory.DIAGNOSTIC, + ), + IronOSSensorEntityDescription( + key=PinecilSensor.MAX_TIP_TEMP_ABILITY, + translation_key=PinecilSensor.MAX_TIP_TEMP_ABILITY, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + value_fn=lambda data: data.max_tip_temp_ability, + entity_category=EntityCategory.DIAGNOSTIC, + ), + IronOSSensorEntityDescription( + key=PinecilSensor.TIP_VOLTAGE, + translation_key=PinecilSensor.TIP_VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=3, + value_fn=lambda data: data.tip_voltage, + entity_category=EntityCategory.DIAGNOSTIC, + ), + IronOSSensorEntityDescription( + key=PinecilSensor.HALL_SENSOR, + translation_key=PinecilSensor.HALL_SENSOR, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + value_fn=lambda data: data.hall_sensor, + entity_category=EntityCategory.DIAGNOSTIC, + ), + IronOSSensorEntityDescription( + key=PinecilSensor.OPERATING_MODE, + translation_key=PinecilSensor.OPERATING_MODE, + device_class=SensorDeviceClass.ENUM, + options=[item.name.lower() for item in OperatingMode], + value_fn=( + lambda data: data.operating_mode.name.lower() + if data.operating_mode + else None + ), + ), + IronOSSensorEntityDescription( + key=PinecilSensor.ESTIMATED_POWER, + translation_key=PinecilSensor.ESTIMATED_POWER, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.estimated_power, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: IronOSConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors from a config entry.""" + coordinator = entry.runtime_data + + async_add_entities( + IronOSSensorEntity(coordinator, description) + for description in PINECIL_SENSOR_DESCRIPTIONS + ) + + +class IronOSSensorEntity(IronOSBaseEntity, SensorEntity): + """Representation of a IronOS sensor entity.""" + + entity_description: IronOSSensorEntityDescription + + @property + def native_value(self) -> StateType: + """Return sensor state.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/iron_os/strings.json b/homeassistant/components/iron_os/strings.json new file mode 100644 index 00000000000..cb95330b768 --- /dev/null +++ b/homeassistant/components/iron_os/strings.json @@ -0,0 +1,84 @@ +{ + "config": { + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:common::config_flow::data::device%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "sensor": { + "live_temperature": { + "name": "Tip temperature" + }, + "voltage": { + "name": "DC input voltage" + }, + "handle_temperature": { + "name": "Handle temperature" + }, + "power_pwm_level": { + "name": "Power level" + }, + "power_source": { + "name": "Power source", + "state": { + "dc": "DC input", + "qc": "USB Quick Charge", + "pd_vbus": "USB PD VBUS", + "pd": "USB Power Delivery" + } + }, + "tip_resistance": { + "name": "Tip resistance" + }, + "uptime": { + "name": "Uptime" + }, + "movement_time": { + "name": "Last movement time" + }, + "max_tip_temp_ability": { + "name": "Max tip temperature" + }, + "tip_voltage": { + "name": "Raw tip voltage" + }, + "hall_sensor": { + "name": "Hall effect strength" + }, + "operating_mode": { + "name": "Operating mode", + "state": { + "idle": "[%key:common::state::idle%]", + "soldering": "Soldering", + "sleeping": "Sleeping", + "settings": "Settings", + "debug": "Debug", + "boost": "Boost" + } + }, + "estimated_power": { + "name": "Estimated power" + } + } + }, + "exceptions": { + "setup_device_unavailable_exception": { + "message": "Device {name} is not reachable" + }, + "setup_device_connection_error_exception": { + "message": "Connection to device {name} failed, try again later" + } + } +} diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index cda011d1bef..2ea604a91a2 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -321,6 +321,11 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [ "domain": "inkbird", "local_name": "tps", }, + { + "connectable": True, + "domain": "iron_os", + "service_uuid": "9eae1000-9d0d-48c5-aa55-33e27f9bc533", + }, { "connectable": False, "domain": "kegtron", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e7d5278dd89..d350a58f3c6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -276,6 +276,7 @@ FLOWS = { "ipma", "ipp", "iqvia", + "iron_os", "islamic_prayer_times", "israel_rail", "iss", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 8bfef6a9887..8b0225ed063 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2899,6 +2899,12 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "iron_os": { + "name": "IronOS", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "islamic_prayer_times": { "integration_type": "hub", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 3b599b00ce8..90ca4049d85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2025,6 +2025,9 @@ pymsteams==0.1.12 # homeassistant.components.mysensors pymysensors==0.24.0 +# homeassistant.components.iron_os +pynecil==0.2.0 + # homeassistant.components.netgear pynetgear==0.10.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27d112fb4f4..7bdd1a910ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1615,6 +1615,9 @@ pymonoprice==0.4 # homeassistant.components.mysensors pymysensors==0.24.0 +# homeassistant.components.iron_os +pynecil==0.2.0 + # homeassistant.components.netgear pynetgear==0.10.10 diff --git a/tests/components/iron_os/__init__.py b/tests/components/iron_os/__init__.py new file mode 100644 index 00000000000..4e27f2c741c --- /dev/null +++ b/tests/components/iron_os/__init__.py @@ -0,0 +1 @@ +"""Tests for the Pinecil integration.""" diff --git a/tests/components/iron_os/conftest.py b/tests/components/iron_os/conftest.py new file mode 100644 index 00000000000..b6983074441 --- /dev/null +++ b/tests/components/iron_os/conftest.py @@ -0,0 +1,141 @@ +"""Fixtures for Pinecil tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +from bleak.backends.device import BLEDevice +from habluetooth import BluetoothServiceInfoBleak +from pynecil import DeviceInfoResponse, LiveDataResponse, OperatingMode, PowerSource +import pytest + +from homeassistant.components.iron_os import DOMAIN +from homeassistant.const import CONF_ADDRESS + +from tests.common import MockConfigEntry +from tests.components.bluetooth import generate_advertisement_data, generate_ble_device + +USER_INPUT = {CONF_ADDRESS: "c0:ff:ee:c0:ff:ee"} +DEFAULT_NAME = "Pinecil-C0FFEEE" +PINECIL_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Pinecil-C0FFEEE", + address="c0:ff:ee:c0:ff:ee", + device=generate_ble_device( + address="c0:ff:ee:c0:ff:ee", + name="Pinecil-C0FFEEE", + ), + rssi=-61, + manufacturer_data={}, + service_data={}, + service_uuids=["9eae1000-9d0d-48c5-aa55-33e27f9bc533"], + source="local", + advertisement=generate_advertisement_data( + manufacturer_data={}, + service_uuids=["9eae1000-9d0d-48c5-aa55-33e27f9bc533"], + ), + connectable=True, + time=0, + tx_power=None, +) + +UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak( + name="", + address="c0:ff:ee:c0:ff:ee", + device=generate_ble_device( + address="c0:ff:ee:c0:ff:ee", + name="", + ), + rssi=-61, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + advertisement=generate_advertisement_data( + manufacturer_data={}, + service_uuids=[], + ), + connectable=True, + time=0, + tx_power=None, +) + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth: None) -> None: + """Auto mock bluetooth.""" + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.iron_os.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(name="discovery") +def mock_async_discovered_service_info() -> Generator[MagicMock]: + """Mock service discovery.""" + with patch( + "homeassistant.components.iron_os.config_flow.async_discovered_service_info", + return_value=[PINECIL_SERVICE_INFO, UNKNOWN_SERVICE_INFO], + ) as discovery: + yield discovery + + +@pytest.fixture(name="config_entry") +def mock_config_entry() -> MockConfigEntry: + """Mock Pinecil configuration entry.""" + return MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_NAME, + data={}, + unique_id="c0:ff:ee:c0:ff:ee", + entry_id="1234567890", + ) + + +@pytest.fixture(name="ble_device") +def mock_ble_device() -> Generator[MagicMock]: + """Mock BLEDevice.""" + with patch( + "homeassistant.components.bluetooth.async_ble_device_from_address", + return_value=BLEDevice( + address="c0:ff:ee:c0:ff:ee", name=DEFAULT_NAME, rssi=-50, details={} + ), + ) as ble_device: + yield ble_device + + +@pytest.fixture +def mock_pynecil() -> Generator[AsyncMock, None, None]: + """Mock Pynecil library.""" + with patch( + "homeassistant.components.iron_os.Pynecil", autospec=True + ) as mock_client: + client = mock_client.return_value + + client.get_device_info.return_value = DeviceInfoResponse( + build="v2.22", + device_id="c0ffeeC0", + address="c0:ff:ee:c0:ff:ee", + device_sn="0000c0ffeec0ffee", + name=DEFAULT_NAME, + ) + client.get_live_data.return_value = LiveDataResponse( + live_temp=298, + setpoint_temp=300, + dc_voltage=20.6, + handle_temp=36.3, + pwm_level=41, + power_src=PowerSource.PD, + tip_resistance=6.2, + uptime=1671, + movement_time=10000, + max_tip_temp_ability=460, + tip_voltage=2212, + hall_sensor=0, + operating_mode=OperatingMode.SOLDERING, + estimated_power=24.8, + ) + yield client diff --git a/tests/components/iron_os/snapshots/test_sensor.ambr b/tests/components/iron_os/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..64cb951dacc --- /dev/null +++ b/tests/components/iron_os/snapshots/test_sensor.ambr @@ -0,0 +1,683 @@ +# serializer version: 1 +# name: test_sensors[sensor.pinecil_dc_input_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_dc_input_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input voltage', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_dc_input_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Pinecil DC input voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_dc_input_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.6', + }) +# --- +# name: test_sensors[sensor.pinecil_estimated_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pinecil_estimated_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Estimated power', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_estimated_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_estimated_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Pinecil Estimated power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_estimated_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.8', + }) +# --- +# name: test_sensors[sensor.pinecil_hall_effect_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_hall_effect_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hall effect strength', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_hall_sensor', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.pinecil_hall_effect_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Hall effect strength', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_hall_effect_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.pinecil_handle_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pinecil_handle_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Handle temperature', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_handle_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_handle_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pinecil Handle temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_handle_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '36.3', + }) +# --- +# name: test_sensors[sensor.pinecil_last_movement_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_last_movement_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last movement time', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_movement_time', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_last_movement_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Pinecil Last movement time', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_last_movement_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10000', + }) +# --- +# name: test_sensors[sensor.pinecil_max_tip_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_max_tip_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Max tip temperature', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_max_tip_temp_ability', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_max_tip_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pinecil Max tip temperature', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_max_tip_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '460', + }) +# --- +# name: test_sensors[sensor.pinecil_operating_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'idle', + 'soldering', + 'boost', + 'sleeping', + 'settings', + 'debug', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pinecil_operating_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Operating mode', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_operating_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.pinecil_operating_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Pinecil Operating mode', + 'options': list([ + 'idle', + 'soldering', + 'boost', + 'sleeping', + 'settings', + 'debug', + ]), + }), + 'context': , + 'entity_id': 'sensor.pinecil_operating_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'soldering', + }) +# --- +# name: test_sensors[sensor.pinecil_power_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_power_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power level', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_power_pwm_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.pinecil_power_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'Pinecil Power level', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.pinecil_power_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '41', + }) +# --- +# name: test_sensors[sensor.pinecil_power_source-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'dc', + 'qc', + 'pd_vbus', + 'pd', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_power_source', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power source', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_power_source', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.pinecil_power_source-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Pinecil Power source', + 'options': list([ + 'dc', + 'qc', + 'pd_vbus', + 'pd', + ]), + }), + 'context': , + 'entity_id': 'sensor.pinecil_power_source', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'pd', + }) +# --- +# name: test_sensors[sensor.pinecil_raw_tip_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_raw_tip_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Raw tip voltage', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_tip_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_raw_tip_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Pinecil Raw tip voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_raw_tip_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2212', + }) +# --- +# name: test_sensors[sensor.pinecil_tip_resistance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_tip_resistance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tip resistance', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_tip_resistance', + 'unit_of_measurement': 'Ω', + }) +# --- +# name: test_sensors[sensor.pinecil_tip_resistance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Tip resistance', + 'unit_of_measurement': 'Ω', + }), + 'context': , + 'entity_id': 'sensor.pinecil_tip_resistance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.2', + }) +# --- +# name: test_sensors[sensor.pinecil_tip_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pinecil_tip_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tip temperature', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_live_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_tip_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pinecil Tip temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_tip_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '298', + }) +# --- +# name: test_sensors[sensor.pinecil_uptime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_uptime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Uptime', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_uptime', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_uptime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Pinecil Uptime', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_uptime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1671', + }) +# --- diff --git a/tests/components/iron_os/test_config_flow.py b/tests/components/iron_os/test_config_flow.py new file mode 100644 index 00000000000..231ec6cc3d6 --- /dev/null +++ b/tests/components/iron_os/test_config_flow.py @@ -0,0 +1,66 @@ +"""Tests for the Pinecil config flow.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock + +from homeassistant.components.iron_os import DOMAIN +from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .conftest import DEFAULT_NAME, PINECIL_SERVICE_INFO, USER_INPUT + + +async def test_form( + hass: HomeAssistant, mock_setup_entry: AsyncMock, discovery: MagicMock +) -> None: + """Test the user config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_no_device_discovered( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + discovery: MagicMock, +) -> None: + """Test setup with no device discoveries.""" + discovery.return_value = [] + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth(hass: HomeAssistant) -> None: + """Test discovery via bluetooth..""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=PINECIL_SERVICE_INFO, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == {} + assert result["result"].unique_id == "c0:ff:ee:c0:ff:ee" diff --git a/tests/components/iron_os/test_sensor.py b/tests/components/iron_os/test_sensor.py new file mode 100644 index 00000000000..0c35193e400 --- /dev/null +++ b/tests/components/iron_os/test_sensor.py @@ -0,0 +1,73 @@ +"""Tests for the Pinecil Sensors.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, MagicMock, patch + +from freezegun.api import FrozenDateTimeFactory +from pynecil import CommunicationError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.iron_os.coordinator import SCAN_INTERVAL +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.fixture(autouse=True) +async def sensor_only() -> AsyncGenerator[None, None]: + """Enable only the sensor platform.""" + with patch( + "homeassistant.components.iron_os.PLATFORMS", + [Platform.SENSOR], + ): + yield + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_pynecil: AsyncMock, + ble_device: MagicMock, +) -> None: + """Test the Pinecil sensor platform.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors_unavailable( + hass: HomeAssistant, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + mock_pynecil: AsyncMock, + ble_device: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the sensors when device disconnects.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + mock_pynecil.get_live_data.side_effect = CommunicationError + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + + entity_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + for entity_entry in entity_entries: + assert hass.states.get(entity_entry.entity_id).state == STATE_UNAVAILABLE From 869ec3f670fbad42594020d03fa5c461828b3922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Mon, 29 Jul 2024 12:44:28 +0300 Subject: [PATCH 1645/2411] Bump pyOverkiz to 1.13.14 (#122691) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 12dfe89c7d3..8825c09e0ff 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -19,7 +19,7 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], - "requirements": ["pyoverkiz==1.13.12"], + "requirements": ["pyoverkiz==1.13.14"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 90ca4049d85..7fbd1e182c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2082,7 +2082,7 @@ pyotgw==2.2.0 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.12 +pyoverkiz==1.13.14 # homeassistant.components.onewire pyownet==0.10.0.post1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7bdd1a910ef..b58b560c7d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1663,7 +1663,7 @@ pyotgw==2.2.0 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.12 +pyoverkiz==1.13.14 # homeassistant.components.onewire pyownet==0.10.0.post1 From 1879db9f8f77137d4a46f9c9580a66633eb67cc8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 04:45:39 -0500 Subject: [PATCH 1646/2411] Revert to using call_soon for event triggers and state changed event trackers (#122735) --- .../homeassistant/triggers/event.py | 3 +- homeassistant/helpers/event.py | 12 ++- tests/components/automation/test_init.py | 1 + .../components/deconz/test_device_trigger.py | 1 + .../components/history/test_websocket_api.py | 88 ++++++++++++++++++- .../components/logbook/test_websocket_api.py | 80 ++++++++++++++++- .../components/universal/test_media_player.py | 1 + .../components/websocket_api/test_commands.py | 52 +++++++++++ tests/components/zha/test_device_trigger.py | 1 + 9 files changed, 233 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 0a15585586e..98363de1f8d 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -154,7 +154,8 @@ async def async_attach_trigger( # If event doesn't match, skip event return - hass.async_run_hass_job( + hass.loop.call_soon( + hass.async_run_hass_job, job, { "trigger": { diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 0c77809079e..207dd024b6a 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -328,6 +328,16 @@ def async_track_state_change_event( return _async_track_state_change_event(hass, entity_ids, action, job_type) +@callback +def _async_dispatch_entity_id_event_soon( + hass: HomeAssistant, + callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]], + event: Event[_StateEventDataT], +) -> None: + """Dispatch to listeners soon to ensure one event loop runs before dispatch.""" + hass.loop.call_soon(_async_dispatch_entity_id_event, hass, callbacks, event) + + @callback def _async_dispatch_entity_id_event( hass: HomeAssistant, @@ -361,7 +371,7 @@ def _async_state_filter( _KEYED_TRACK_STATE_CHANGE = _KeyedEventTracker( key=_TRACK_STATE_CHANGE_DATA, event_type=EVENT_STATE_CHANGED, - dispatcher_callable=_async_dispatch_entity_id_event, + dispatcher_callable=_async_dispatch_entity_id_event_soon, filter_callable=_async_state_filter, ) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 8bac0c15db9..d8078984630 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -3229,6 +3229,7 @@ async def test_two_automations_call_restart_script_same_time( hass.states.async_set("binary_sensor.presence", "on") await hass.async_block_till_done() + await hass.async_block_till_done() assert len(events) == 2 cancel() diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 211ce14b8dc..6f74db0b82c 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -343,6 +343,7 @@ async def test_functional_device_trigger( assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 1 await sensor_ws_data({"state": {"buttonevent": 1002}}) + await hass.async_block_till_done() assert len(service_calls) == 1 assert service_calls[0].data["some"] == "test_trigger_button_press" diff --git a/tests/components/history/test_websocket_api.py b/tests/components/history/test_websocket_api.py index e5c33d0e7af..717840c6b05 100644 --- a/tests/components/history/test_websocket_api.py +++ b/tests/components/history/test_websocket_api.py @@ -2,7 +2,7 @@ import asyncio from datetime import timedelta -from unittest.mock import patch +from unittest.mock import ANY, patch from freezegun import freeze_time import pytest @@ -10,8 +10,9 @@ import pytest from homeassistant.components import history from homeassistant.components.history import websocket_api from homeassistant.components.recorder import Recorder -from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE -from homeassistant.core import HomeAssistant +from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -2072,3 +2073,84 @@ async def test_history_stream_historical_only_with_start_time_state_past( "id": 1, "type": "event", } + + +async def test_history_stream_live_chained_events( + hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator +) -> None: + """Test history stream with history with a chained event.""" + now = dt_util.utcnow() + await async_setup_component(hass, "history", {}) + + await async_wait_recording_done(hass) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": ["binary_sensor.is_light"], + "start_time": now.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + + assert response == { + "event": { + "end_time": ANY, + "start_time": ANY, + "states": { + "binary_sensor.is_light": [ + { + "a": {}, + "lu": ANY, + "s": STATE_OFF, + }, + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + + @callback + def auto_off_listener(event): + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + + async_track_state_change_event(hass, ["binary_sensor.is_light"], auto_off_listener) + + hass.states.async_set("binary_sensor.is_light", STATE_ON) + + response = await client.receive_json() + assert response == { + "event": { + "states": { + "binary_sensor.is_light": [ + { + "lu": ANY, + "s": STATE_ON, + "a": {}, + }, + { + "lu": ANY, + "s": STATE_OFF, + "a": {}, + }, + ], + }, + }, + "id": 1, + "type": "event", + } diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index ac653737614..9b1a6bb44cc 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -3,6 +3,7 @@ import asyncio from collections.abc import Callable from datetime import timedelta +from typing import Any from unittest.mock import ANY, patch from freezegun import freeze_time @@ -31,9 +32,10 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, State +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -2965,3 +2967,79 @@ async def test_subscribe_all_entities_are_continuous_with_device( assert listeners_without_writes( hass.bus.async_listeners() ) == listeners_without_writes(init_listeners) + + +@pytest.mark.parametrize("params", [{"entity_ids": ["binary_sensor.is_light"]}, {}]) +async def test_live_stream_with_changed_state_change( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + params: dict[str, Any], +) -> None: + """Test the live logbook stream with chained events.""" + config = {recorder.CONF_COMMIT_INTERVAL: 0.5} + await async_setup_recorder_instance(hass, config) + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook") + ] + ) + + hass.states.async_set("binary_sensor.is_light", "ignored") + hass.states.async_set("binary_sensor.is_light", "init") + await async_wait_recording_done(hass) + + @callback + def auto_off_listener(event): + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + + async_track_state_change_event(hass, ["binary_sensor.is_light"], auto_off_listener) + + websocket_client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + **params, + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + await hass.async_block_till_done() + hass.states.async_set("binary_sensor.is_light", STATE_ON) + + recieved_rows = [] + while len(recieved_rows) < 3: + msg = await asyncio.wait_for(websocket_client.receive_json(), 2.5) + assert msg["id"] == 7 + assert msg["type"] == "event" + recieved_rows.extend(msg["event"]["events"]) + + # Make sure we get rows back in order + assert recieved_rows == [ + {"entity_id": "binary_sensor.is_light", "state": "init", "when": ANY}, + {"entity_id": "binary_sensor.is_light", "state": "on", "when": ANY}, + {"entity_id": "binary_sensor.is_light", "state": "off", "when": ANY}, + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 527675a2208..187b62a93a1 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -1280,6 +1280,7 @@ async def test_master_state_with_template(hass: HomeAssistant) -> None: context = Context() hass.states.async_set("input_boolean.test", STATE_ON, context=context) await hass.async_block_till_done() + await hass.async_block_till_done() assert hass.states.get("media_player.tv").state == STATE_OFF assert events[0].context == context diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 276a383d9e9..10a9c4876b9 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -24,6 +24,7 @@ from homeassistant.core import Context, HomeAssistant, State, SupportsResponse, from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component from homeassistant.util.json import json_loads @@ -2814,3 +2815,54 @@ async def test_integration_descriptions( assert response["success"] assert response["result"] + + +async def test_subscribe_entities_chained_state_change( + hass: HomeAssistant, + websocket_client: MockHAClientWebSocket, + hass_admin_user: MockUser, +) -> None: + """Test chaining state changed events. + + Ensure the websocket sends the off state after + the on state. + """ + + @callback + def auto_off_listener(event): + hass.states.async_set("light.permitted", "off") + + async_track_state_change_event(hass, ["light.permitted"], auto_off_listener) + + await websocket_client.send_json({"id": 7, "type": "subscribe_entities"}) + + data = await websocket_client.receive_str() + msg = json_loads(data) + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + data = await websocket_client.receive_str() + msg = json_loads(data) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == {"a": {}} + + hass.states.async_set("light.permitted", "on") + data = await websocket_client.receive_str() + msg = json_loads(data) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "a": {"light.permitted": {"a": {}, "c": ANY, "lc": ANY, "s": "on"}} + } + data = await websocket_client.receive_str() + msg = json_loads(data) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "c": {"light.permitted": {"+": {"c": ANY, "lc": ANY, "s": "off"}}} + } + + await websocket_client.close() + await hass.async_block_till_done() diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 24883dfc336..09b2d155547 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -306,6 +306,7 @@ async def test_device_offline_fires( assert zha_device.available is True zha_device.available = False zha_device.emit_zha_event({"device_event_type": "device_offline"}) + await hass.async_block_till_done() assert len(service_calls) == 1 assert service_calls[0].data["message"] == "service called" From 8a84addc549004ed87d0f2ece6883c53633446ca Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 29 Jul 2024 11:57:53 +0200 Subject: [PATCH 1647/2411] Add test of recorder platform with statistics support (#122754) * Add test of recorder platform with statistics support * Remove excessive line breaks --- tests/components/recorder/test_statistics.py | 165 ++++++++++++++++++- 1 file changed, 164 insertions(+), 1 deletion(-) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 074a98e5230..5cbb29afc91 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -1,7 +1,8 @@ """The tests for sensor recorder platform.""" from datetime import timedelta -from unittest.mock import patch +from typing import Any +from unittest.mock import ANY, Mock, patch import pytest from sqlalchemy import select @@ -15,11 +16,13 @@ from homeassistant.components.recorder.models import ( ) from homeassistant.components.recorder.statistics import ( STATISTIC_UNIT_TO_UNIT_CONVERTER, + PlatformCompiledStatistics, _generate_max_mean_min_statistic_in_sub_period_stmt, _generate_statistics_at_time_stmt, _generate_statistics_during_period_stmt, async_add_external_statistics, async_import_statistics, + async_list_statistic_ids, get_last_short_term_statistics, get_last_statistics, get_latest_short_term_statistics_with_session, @@ -27,6 +30,7 @@ from homeassistant.components.recorder.statistics import ( get_metadata_with_session, get_short_term_statistics_run_cache, list_statistic_ids, + validate_statistics, ) from homeassistant.components.recorder.table_managers.statistics_meta import ( _generate_get_metadata_stmt, @@ -42,12 +46,14 @@ import homeassistant.util.dt as dt_util from .common import ( assert_dict_of_states_equal_without_context_and_last_changed, async_record_states, + async_recorder_block_till_done, async_wait_recording_done, do_adhoc_statistics, get_start_time, statistics_during_period, ) +from tests.common import MockPlatform, mock_platform from tests.typing import RecorderInstanceGenerator, WebSocketGenerator @@ -63,6 +69,15 @@ def setup_recorder(recorder_mock: Recorder) -> None: """Set up recorder.""" +async def _setup_mock_domain( + hass: HomeAssistant, + platform: Any | None = None, # There's no RecorderPlatform class yet +) -> None: + """Set up a mock domain.""" + mock_platform(hass, "some_domain.recorder", platform or MockPlatform()) + assert await async_setup_component(hass, "some_domain", {}) + + def test_converters_align_with_sensor() -> None: """Ensure STATISTIC_UNIT_TO_UNIT_CONVERTER is aligned with UNIT_CONVERTERS.""" for converter in UNIT_CONVERTERS.values(): @@ -2473,3 +2488,151 @@ async def test_change_with_none( types={"change"}, ) assert stats == {} + + +async def test_recorder_platform_with_statistics( + hass: HomeAssistant, + setup_recorder: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test recorder platform.""" + instance = recorder.get_instance(hass) + recorder_data = hass.data["recorder"] + assert not recorder_data.recorder_platforms + + def _mock_compile_statistics(*args: Any) -> PlatformCompiledStatistics: + return PlatformCompiledStatistics([], {}) + + def _mock_list_statistic_ids(*args: Any, **kwargs: Any) -> dict: + return {} + + def _mock_validate_statistics(*args: Any) -> dict: + return {} + + recorder_platform = Mock( + compile_statistics=Mock(wraps=_mock_compile_statistics), + list_statistic_ids=Mock(wraps=_mock_list_statistic_ids), + validate_statistics=Mock(wraps=_mock_validate_statistics), + ) + + await _setup_mock_domain(hass, recorder_platform) + + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) + assert recorder_data.recorder_platforms == {"some_domain": recorder_platform} + + recorder_platform.compile_statistics.assert_not_called() + recorder_platform.list_statistic_ids.assert_not_called() + recorder_platform.validate_statistics.assert_not_called() + + # Test compile statistics + zero = get_start_time(dt_util.utcnow()) + do_adhoc_statistics(hass, start=zero) + await async_wait_recording_done(hass) + + recorder_platform.compile_statistics.assert_called_once_with( + hass, ANY, zero, zero + timedelta(minutes=5) + ) + recorder_platform.list_statistic_ids.assert_not_called() + recorder_platform.validate_statistics.assert_not_called() + + # Test list statistic IDs + await async_list_statistic_ids(hass) + recorder_platform.compile_statistics.assert_called_once() + recorder_platform.list_statistic_ids.assert_called_once_with( + hass, statistic_ids=None, statistic_type=None + ) + recorder_platform.validate_statistics.assert_not_called() + + # Test validate statistics + await instance.async_add_executor_job( + validate_statistics, + hass, + ) + recorder_platform.compile_statistics.assert_called_once() + recorder_platform.list_statistic_ids.assert_called_once() + recorder_platform.validate_statistics.assert_called_once_with(hass) + + +async def test_recorder_platform_without_statistics( + hass: HomeAssistant, + setup_recorder: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test recorder platform.""" + recorder_data = hass.data["recorder"] + assert recorder_data.recorder_platforms == {} + + await _setup_mock_domain(hass) + + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) + assert recorder_data.recorder_platforms == {} + + +@pytest.mark.parametrize( + "supported_methods", + [ + ("compile_statistics",), + ("list_statistic_ids",), + ("validate_statistics",), + ], +) +async def test_recorder_platform_with_partial_statistics_support( + hass: HomeAssistant, + setup_recorder: None, + caplog: pytest.LogCaptureFixture, + supported_methods: tuple[str, ...], +) -> None: + """Test recorder platform.""" + instance = recorder.get_instance(hass) + recorder_data = hass.data["recorder"] + assert not recorder_data.recorder_platforms + + def _mock_compile_statistics(*args: Any) -> PlatformCompiledStatistics: + return PlatformCompiledStatistics([], {}) + + def _mock_list_statistic_ids(*args: Any, **kwargs: Any) -> dict: + return {} + + def _mock_validate_statistics(*args: Any) -> dict: + return {} + + mock_impl = { + "compile_statistics": _mock_compile_statistics, + "list_statistic_ids": _mock_list_statistic_ids, + "validate_statistics": _mock_validate_statistics, + } + + kwargs = {meth: Mock(wraps=mock_impl[meth]) for meth in supported_methods} + + recorder_platform = Mock( + spec=supported_methods, + **kwargs, + ) + + await _setup_mock_domain(hass, recorder_platform) + + # Wait for the sensor recorder platform to be added + await async_recorder_block_till_done(hass) + assert recorder_data.recorder_platforms == {"some_domain": recorder_platform} + + for meth in supported_methods: + getattr(recorder_platform, meth).assert_not_called() + + # Test compile statistics + zero = get_start_time(dt_util.utcnow()) + do_adhoc_statistics(hass, start=zero) + await async_wait_recording_done(hass) + + # Test list statistic IDs + await async_list_statistic_ids(hass) + + # Test validate statistics + await instance.async_add_executor_job( + validate_statistics, + hass, + ) + + for meth in supported_methods: + getattr(recorder_platform, meth).assert_called_once() From 6d4711ce436b138105b56915ccc651c1ba5b039d Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 29 Jul 2024 11:59:13 +0200 Subject: [PATCH 1648/2411] Bump aiohue to version 4.7.2 (#122651) --- homeassistant/components/hue/manifest.json | 2 +- homeassistant/components/hue/v2/hue_event.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/fixtures/v2_resources.json | 12 ++++++++++++ tests/components/hue/test_device_trigger_v2.py | 9 +++++++-- 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index e8d214da3c8..71aabd4c204 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -11,6 +11,6 @@ "iot_class": "local_push", "loggers": ["aiohue"], "quality_scale": "platinum", - "requirements": ["aiohue==4.7.1"], + "requirements": ["aiohue==4.7.2"], "zeroconf": ["_hue._tcp.local."] } diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index b0e0de234f1..b286a11aade 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -55,7 +55,7 @@ async def async_setup_hue_events(bridge: HueBridge): CONF_ID: slugify(f"{hue_device.metadata.name} Button"), CONF_DEVICE_ID: device.id, # type: ignore[union-attr] CONF_UNIQUE_ID: hue_resource.id, - CONF_TYPE: hue_resource.button.last_event.value, + CONF_TYPE: hue_resource.button.button_report.event.value, CONF_SUBTYPE: hue_resource.metadata.control_id, } hass.bus.async_fire(ATTR_HUE_EVENT, data) @@ -79,7 +79,7 @@ async def async_setup_hue_events(bridge: HueBridge): data = { CONF_DEVICE_ID: device.id, # type: ignore[union-attr] CONF_UNIQUE_ID: hue_resource.id, - CONF_TYPE: hue_resource.relative_rotary.last_event.action.value, + CONF_TYPE: hue_resource.relative_rotary.rotary_report.action.value, CONF_SUBTYPE: hue_resource.relative_rotary.last_event.rotation.direction.value, CONF_DURATION: hue_resource.relative_rotary.last_event.rotation.duration, CONF_STEPS: hue_resource.relative_rotary.last_event.rotation.steps, diff --git a/requirements_all.txt b/requirements_all.txt index 7fbd1e182c5..1f81d87a167 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -258,7 +258,7 @@ aioharmony==0.2.10 aiohomekit==3.2.1 # homeassistant.components.hue -aiohue==4.7.1 +aiohue==4.7.2 # homeassistant.components.imap aioimaplib==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b58b560c7d6..a39a9c29822 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -243,7 +243,7 @@ aioharmony==0.2.10 aiohomekit==3.2.1 # homeassistant.components.hue -aiohue==4.7.1 +aiohue==4.7.2 # homeassistant.components.imap aioimaplib==1.1.0 diff --git a/tests/components/hue/fixtures/v2_resources.json b/tests/components/hue/fixtures/v2_resources.json index 662e1107ca9..980086d0988 100644 --- a/tests/components/hue/fixtures/v2_resources.json +++ b/tests/components/hue/fixtures/v2_resources.json @@ -1487,6 +1487,10 @@ "on": { "on": true }, + "owner": { + "rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "rtype": "zone" + }, "type": "grouped_light" }, { @@ -1498,6 +1502,10 @@ "on": { "on": true }, + "owner": { + "rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "rtype": "zone" + }, "type": "grouped_light" }, { @@ -1509,6 +1517,10 @@ "on": { "on": false }, + "owner": { + "rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "rtype": "zone" + }, "type": "grouped_light" }, { diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index 96d24835e3c..1115e63fd92 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -31,7 +31,12 @@ async def test_hue_event( # Emit button update event btn_event = { - "button": {"last_event": "initial_press"}, + "button": { + "button_report": { + "event": "initial_press", + "updated": "2021-10-01T12:00:00Z", + } + }, "id": "c658d3d8-a013-4b81-8ac6-78b248537e70", "metadata": {"control_id": 1}, "type": "button", @@ -44,7 +49,7 @@ async def test_hue_event( assert len(events) == 1 assert events[0].data["id"] == "wall_switch_with_2_controls_button" assert events[0].data["unique_id"] == btn_event["id"] - assert events[0].data["type"] == btn_event["button"]["last_event"] + assert events[0].data["type"] == btn_event["button"]["button_report"]["event"] assert events[0].data["subtype"] == btn_event["metadata"]["control_id"] From d586e7df33e6387c4e384c1fe1f3358678994eb0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 04:59:31 -0500 Subject: [PATCH 1649/2411] Retry later on OSError during apple_tv entry setup (#122747) --- homeassistant/components/apple_tv/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 4e5c8791acd..08372aa79ae 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -60,6 +60,7 @@ AUTH_EXCEPTIONS = ( exceptions.NoCredentialsError, ) CONNECTION_TIMEOUT_EXCEPTIONS = ( + OSError, asyncio.CancelledError, TimeoutError, exceptions.ConnectionLostError, From 745eea9a29c317018a5862516156b80e24bad420 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Mon, 29 Jul 2024 12:02:47 +0200 Subject: [PATCH 1650/2411] Bump bimmer_connected to 0.16.1 (#122699) Co-authored-by: Richard --- .../bmw_connected_drive/manifest.json | 2 +- .../bmw_connected_drive/strings.json | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../bmw_connected_drive/__init__.py | 6 +- .../snapshots/test_diagnostics.ambr | 1492 +++++++++-------- .../snapshots/test_select.ambr | 6 + 7 files changed, 802 insertions(+), 711 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 9dfe2672b66..304973b816f 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], "quality_scale": "platinum", - "requirements": ["bimmer-connected[china]==0.15.3"] + "requirements": ["bimmer-connected[china]==0.16.1"] } diff --git a/homeassistant/components/bmw_connected_drive/strings.json b/homeassistant/components/bmw_connected_drive/strings.json index 125b622105c..8121ab6f65f 100644 --- a/homeassistant/components/bmw_connected_drive/strings.json +++ b/homeassistant/components/bmw_connected_drive/strings.json @@ -86,7 +86,8 @@ "name": "Charging Mode", "state": { "immediate_charging": "Immediate charging", - "delayed_charging": "Delayed charging" + "delayed_charging": "Delayed charging", + "no_action": "No action" } } }, diff --git a/requirements_all.txt b/requirements_all.txt index 1f81d87a167..59f1ced067f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -556,7 +556,7 @@ beautifulsoup4==4.12.3 # beewi-smartclim==0.0.10 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.15.3 +bimmer-connected[china]==0.16.1 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a39a9c29822..5021d7f07fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -490,7 +490,7 @@ base36==0.1.1 beautifulsoup4==4.12.3 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.15.3 +bimmer-connected[china]==0.16.1 # homeassistant.components.eq3btsmart # homeassistant.components.esphome diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index 3632bfc1332..655955ff9aa 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -1,7 +1,7 @@ """Tests for the for the BMW Connected Drive integration.""" from bimmer_connected.const import ( - REMOTE_SERVICE_BASE_URL, + REMOTE_SERVICE_V4_BASE_URL, VEHICLE_CHARGING_BASE_URL, VEHICLE_POI_URL, ) @@ -71,11 +71,11 @@ def check_remote_service_call( first_remote_service_call: respx.models.Call = next( c for c in router.calls - if c.request.url.path.startswith(REMOTE_SERVICE_BASE_URL) + if c.request.url.path.startswith(REMOTE_SERVICE_V4_BASE_URL) or c.request.url.path.startswith( VEHICLE_CHARGING_BASE_URL.replace("/{vin}", "") ) - or c.request.url.path == VEHICLE_POI_URL + or c.request.url.path.endswith(VEHICLE_POI_URL.rsplit("/", maxsplit=1)[-1]) ) assert ( first_remote_service_call.request.url.path.endswith(remote_service) is True diff --git a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr index 477cd24376d..81ef1220069 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr @@ -232,16 +232,19 @@ }), 'capabilities': dict({ 'a4aType': 'BLUETOOTH', - 'checkSustainabilityDPP': False, + 'alarmSystem': True, 'climateFunction': 'AIR_CONDITIONING', 'climateNow': True, 'digitalKey': dict({ 'bookedServicePackage': 'SMACC_2_UWB', + 'isDigitalKeyFirstSupported': False, 'readerGraphics': 'readerGraphics', 'state': 'ACTIVATED', + 'vehicleSoftwareUpgradeRequired': True, }), 'horn': True, 'inCarCamera': True, + 'inCarCameraDwa': True, 'isBmwChargingSupported': True, 'isCarSharingSupported': False, 'isChargeNowForBusinessSupported': True, @@ -252,27 +255,38 @@ 'isChargingPowerLimitEnabled': True, 'isChargingSettingsEnabled': True, 'isChargingTargetSocEnabled': True, + 'isClimateTimerSupported': False, 'isClimateTimerWeeklyActive': False, 'isCustomerEsimSupported': True, 'isDCSContractManagementSupported': True, 'isDataPrivacyEnabled': False, 'isEasyChargeEnabled': True, 'isEvGoChargingSupported': False, + 'isLocationBasedChargingSettingsSupported': False, 'isMiniChargingSupported': False, 'isNonLscFeatureEnabled': False, 'isPersonalPictureUploadSupported': False, - 'isRemoteEngineStartSupported': False, + 'isPlugAndChargeSupported': False, + 'isRemoteEngineStartEnabled': False, + 'isRemoteEngineStartSupported': True, 'isRemoteHistoryDeletionSupported': False, 'isRemoteHistorySupported': True, + 'isRemoteParkingEes25Active': False, 'isRemoteParkingSupported': False, 'isRemoteServicesActivationRequired': False, 'isRemoteServicesBookingRequired': False, 'isScanAndChargeSupported': True, 'isSustainabilityAccumulatedViewEnabled': False, 'isSustainabilitySupported': False, + 'isThirdPartyAppStoreSupported': False, 'isWifiHotspotServiceSupported': False, 'lastStateCallState': 'ACTIVATED', 'lights': True, + 'locationBasedCommerceFeatures': dict({ + 'fueling': False, + 'parking': False, + 'reservations': False, + }), 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ @@ -287,11 +301,45 @@ 'NOT_SUPPORTED', ]), }), + 'remoteServices': dict({ + 'doorLock': dict({ + 'id': 'doorLock', + 'state': 'ACTIVATED', + }), + 'doorUnlock': dict({ + 'id': 'doorUnlock', + 'state': 'ACTIVATED', + }), + 'hornBlow': dict({ + 'id': 'hornBlow', + 'state': 'ACTIVATED', + }), + 'inCarCamera': dict({ + 'id': 'inCarCamera', + 'state': 'ACTIVATED', + }), + 'inCarCameraDwa': dict({ + 'id': 'inCarCameraDwa', + 'state': 'ACTIVATED', + }), + 'lightFlash': dict({ + 'id': 'lightFlash', + 'state': 'ACTIVATED', + }), + 'remote360': dict({ + 'id': 'remote360', + 'state': 'ACTIVATED', + }), + 'surroundViewRecorder': dict({ + 'id': 'surroundViewRecorder', + 'state': 'ACTIVATED', + }), + }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, 'specialThemeSupport': list([ ]), - 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexa': True, 'speechThirdPartyAlexaSDK': False, 'surroundViewRecorder': True, 'unlock': True, @@ -570,6 +618,7 @@ 'roofState': 'CLOSED', 'roofStateType': 'SUN_ROOF', }), + 'securityOverviewMode': 'ARMED', 'tireState': dict({ 'frontLeft': dict({ 'details': dict({ @@ -660,6 +709,18 @@ }), }), }), + 'vehicleSoftwareVersion': dict({ + 'iStep': dict({ + 'iStep': 0, + 'month': 0, + 'seriesCluster': '', + 'year': 0, + }), + 'puStep': dict({ + 'month': 0, + 'year': 0, + }), + }), 'windowsState': dict({ 'combinedState': 'CLOSED', 'leftFront': 'CLOSED', @@ -1086,15 +1147,19 @@ }), 'capabilities': dict({ 'a4aType': 'NOT_SUPPORTED', - 'checkSustainabilityDPP': False, + 'alarmSystem': False, 'climateFunction': 'AIR_CONDITIONING', 'climateNow': True, 'digitalKey': dict({ 'bookedServicePackage': 'SMACC_1_5', + 'isDigitalKeyFirstSupported': False, 'readerGraphics': 'readerGraphics', 'state': 'ACTIVATED', + 'vehicleSoftwareUpgradeRequired': False, }), 'horn': True, + 'inCarCamera': False, + 'inCarCameraDwa': False, 'isBmwChargingSupported': True, 'isCarSharingSupported': False, 'isChargeNowForBusinessSupported': True, @@ -1105,37 +1170,80 @@ 'isChargingPowerLimitEnabled': True, 'isChargingSettingsEnabled': True, 'isChargingTargetSocEnabled': True, + 'isClimateTimerSupported': False, 'isClimateTimerWeeklyActive': False, - 'isCustomerEsimSupported': True, + 'isCustomerEsimSupported': False, 'isDCSContractManagementSupported': True, 'isDataPrivacyEnabled': False, 'isEasyChargeEnabled': True, 'isEvGoChargingSupported': False, + 'isLocationBasedChargingSettingsSupported': False, 'isMiniChargingSupported': False, 'isNonLscFeatureEnabled': False, 'isPersonalPictureUploadSupported': False, - 'isRemoteEngineStartSupported': False, + 'isPlugAndChargeSupported': False, + 'isRemoteEngineStartEnabled': False, + 'isRemoteEngineStartSupported': True, 'isRemoteHistoryDeletionSupported': False, 'isRemoteHistorySupported': True, + 'isRemoteParkingEes25Active': False, 'isRemoteParkingSupported': False, 'isRemoteServicesActivationRequired': False, 'isRemoteServicesBookingRequired': False, 'isScanAndChargeSupported': True, 'isSustainabilityAccumulatedViewEnabled': False, 'isSustainabilitySupported': False, + 'isThirdPartyAppStoreSupported': False, 'isWifiHotspotServiceSupported': False, 'lastStateCallState': 'ACTIVATED', 'lights': True, + 'locationBasedCommerceFeatures': dict({ + 'fueling': False, + 'parking': False, + 'reservations': False, + }), 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ }), + 'remoteServices': dict({ + 'doorLock': dict({ + 'id': 'doorLock', + 'state': 'ACTIVATED', + }), + 'doorUnlock': dict({ + 'id': 'doorUnlock', + 'state': 'ACTIVATED', + }), + 'hornBlow': dict({ + 'id': 'hornBlow', + 'state': 'ACTIVATED', + }), + 'inCarCamera': dict({ + 'id': 'inCarCamera', + }), + 'inCarCameraDwa': dict({ + 'id': 'inCarCameraDwa', + }), + 'lightFlash': dict({ + 'id': 'lightFlash', + 'state': 'ACTIVATED', + }), + 'remote360': dict({ + 'id': 'remote360', + 'state': 'ACTIVATED', + }), + 'surroundViewRecorder': dict({ + 'id': 'surroundViewRecorder', + }), + }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, 'specialThemeSupport': list([ ]), - 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexa': True, 'speechThirdPartyAlexaSDK': False, + 'surroundViewRecorder': False, 'unlock': True, 'vehicleFinder': True, 'vehicleStateSource': 'LAST_STATE_CALL', @@ -1408,6 +1516,7 @@ 'type': 'TIRE_WEAR_FRONT', }), ]), + 'securityOverviewMode': 'NOT_ARMED', 'tireState': dict({ 'frontLeft': dict({ 'details': dict({ @@ -1498,6 +1607,18 @@ }), }), }), + 'vehicleSoftwareVersion': dict({ + 'iStep': dict({ + 'iStep': 0, + 'month': 0, + 'seriesCluster': '', + 'year': 0, + }), + 'puStep': dict({ + 'month': 0, + 'year': 0, + }), + }), 'windowsState': dict({ 'combinedState': 'CLOSED', 'leftFront': 'CLOSED', @@ -1840,16 +1961,20 @@ }), 'capabilities': dict({ 'a4aType': 'NOT_SUPPORTED', - 'checkSustainabilityDPP': False, + 'alarmSystem': False, 'climateFunction': 'VENTILATION', 'climateNow': True, 'climateTimerTrigger': 'DEPARTURE_TIMER', 'digitalKey': dict({ 'bookedServicePackage': 'SMACC_1_5', + 'isDigitalKeyFirstSupported': False, 'readerGraphics': 'readerGraphics', 'state': 'ACTIVATED', + 'vehicleSoftwareUpgradeRequired': False, }), 'horn': True, + 'inCarCamera': False, + 'inCarCameraDwa': False, 'isBmwChargingSupported': False, 'isCarSharingSupported': False, 'isChargeNowForBusinessSupported': False, @@ -1867,31 +1992,73 @@ 'isDataPrivacyEnabled': False, 'isEasyChargeEnabled': False, 'isEvGoChargingSupported': False, + 'isLocationBasedChargingSettingsSupported': False, 'isMiniChargingSupported': False, 'isNonLscFeatureEnabled': False, 'isPersonalPictureUploadSupported': False, - 'isRemoteEngineStartSupported': False, + 'isPlugAndChargeSupported': False, + 'isRemoteEngineStartEnabled': False, + 'isRemoteEngineStartSupported': True, 'isRemoteHistoryDeletionSupported': False, 'isRemoteHistorySupported': True, + 'isRemoteParkingEes25Active': False, 'isRemoteParkingSupported': False, 'isRemoteServicesActivationRequired': False, 'isRemoteServicesBookingRequired': False, 'isScanAndChargeSupported': False, 'isSustainabilityAccumulatedViewEnabled': False, 'isSustainabilitySupported': False, - 'isWifiHotspotServiceSupported': True, + 'isThirdPartyAppStoreSupported': False, + 'isWifiHotspotServiceSupported': False, 'lastStateCallState': 'ACTIVATED', 'lights': True, + 'locationBasedCommerceFeatures': dict({ + 'fueling': False, + 'parking': False, + 'reservations': False, + }), 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ }), + 'remoteServices': dict({ + 'doorLock': dict({ + 'id': 'doorLock', + 'state': 'ACTIVATED', + }), + 'doorUnlock': dict({ + 'id': 'doorUnlock', + 'state': 'ACTIVATED', + }), + 'hornBlow': dict({ + 'id': 'hornBlow', + 'state': 'ACTIVATED', + }), + 'inCarCamera': dict({ + 'id': 'inCarCamera', + }), + 'inCarCameraDwa': dict({ + 'id': 'inCarCameraDwa', + }), + 'lightFlash': dict({ + 'id': 'lightFlash', + 'state': 'ACTIVATED', + }), + 'remote360': dict({ + 'id': 'remote360', + 'state': 'ACTIVATED', + }), + 'surroundViewRecorder': dict({ + 'id': 'surroundViewRecorder', + }), + }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, 'specialThemeSupport': list([ ]), - 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexa': True, 'speechThirdPartyAlexaSDK': False, + 'surroundViewRecorder': False, 'unlock': True, 'vehicleFinder': True, 'vehicleStateSource': 'LAST_STATE_CALL', @@ -2027,6 +2194,7 @@ 'type': 'TIRE_WEAR_FRONT', }), ]), + 'securityOverviewMode': None, 'tireState': dict({ 'frontLeft': dict({ 'details': dict({ @@ -2113,6 +2281,18 @@ }), }), }), + 'vehicleSoftwareVersion': dict({ + 'iStep': dict({ + 'iStep': 0, + 'month': 0, + 'seriesCluster': '', + 'year': 0, + }), + 'puStep': dict({ + 'month': 0, + 'year': 0, + }), + }), 'windowsState': dict({ 'combinedState': 'CLOSED', 'leftFront': 'CLOSED', @@ -2942,226 +3122,6 @@ }), ]), 'fingerprint': list([ - dict({ - 'content': dict({ - 'capabilities': dict({ - 'climateFunction': 'AIR_CONDITIONING', - 'climateNow': True, - 'climateTimerTrigger': 'DEPARTURE_TIMER', - 'horn': True, - 'isBmwChargingSupported': True, - 'isCarSharingSupported': False, - 'isChargeNowForBusinessSupported': False, - 'isChargingHistorySupported': True, - 'isChargingHospitalityEnabled': False, - 'isChargingLoudnessEnabled': False, - 'isChargingPlanSupported': True, - 'isChargingPowerLimitEnabled': False, - 'isChargingSettingsEnabled': False, - 'isChargingTargetSocEnabled': False, - 'isClimateTimerSupported': True, - 'isCustomerEsimSupported': False, - 'isDCSContractManagementSupported': True, - 'isDataPrivacyEnabled': False, - 'isEasyChargeEnabled': False, - 'isEvGoChargingSupported': False, - 'isMiniChargingSupported': False, - 'isNonLscFeatureEnabled': False, - 'isRemoteEngineStartSupported': False, - 'isRemoteHistoryDeletionSupported': False, - 'isRemoteHistorySupported': True, - 'isRemoteParkingSupported': False, - 'isRemoteServicesActivationRequired': False, - 'isRemoteServicesBookingRequired': False, - 'isScanAndChargeSupported': False, - 'isSustainabilitySupported': False, - 'isWifiHotspotServiceSupported': False, - 'lastStateCallState': 'ACTIVATED', - 'lights': True, - 'lock': True, - 'remoteChargingCommands': dict({ - }), - 'sendPoi': True, - 'specialThemeSupport': list([ - ]), - 'unlock': True, - 'vehicleFinder': False, - 'vehicleStateSource': 'LAST_STATE_CALL', - }), - 'state': dict({ - 'chargingProfile': dict({ - 'chargingControlType': 'WEEKLY_PLANNER', - 'chargingMode': 'DELAYED_CHARGING', - 'chargingPreference': 'CHARGING_WINDOW', - 'chargingSettings': dict({ - 'hospitality': 'NO_ACTION', - 'idcc': 'NO_ACTION', - 'targetSoc': 100, - }), - 'climatisationOn': False, - 'departureTimes': list([ - dict({ - 'action': 'DEACTIVATE', - 'id': 1, - 'timeStamp': dict({ - 'hour': 7, - 'minute': 35, - }), - 'timerWeekDays': list([ - 'MONDAY', - 'TUESDAY', - 'WEDNESDAY', - 'THURSDAY', - 'FRIDAY', - ]), - }), - dict({ - 'action': 'DEACTIVATE', - 'id': 2, - 'timeStamp': dict({ - 'hour': 18, - 'minute': 0, - }), - 'timerWeekDays': list([ - 'MONDAY', - 'TUESDAY', - 'WEDNESDAY', - 'THURSDAY', - 'FRIDAY', - 'SATURDAY', - 'SUNDAY', - ]), - }), - dict({ - 'action': 'DEACTIVATE', - 'id': 3, - 'timeStamp': dict({ - 'hour': 7, - 'minute': 0, - }), - 'timerWeekDays': list([ - ]), - }), - dict({ - 'action': 'DEACTIVATE', - 'id': 4, - 'timerWeekDays': list([ - ]), - }), - ]), - 'reductionOfChargeCurrent': dict({ - 'end': dict({ - 'hour': 1, - 'minute': 30, - }), - 'start': dict({ - 'hour': 18, - 'minute': 1, - }), - }), - }), - 'checkControlMessages': list([ - ]), - 'climateTimers': list([ - dict({ - 'departureTime': dict({ - 'hour': 6, - 'minute': 40, - }), - 'isWeeklyTimer': True, - 'timerAction': 'ACTIVATE', - 'timerWeekDays': list([ - 'THURSDAY', - 'SUNDAY', - ]), - }), - dict({ - 'departureTime': dict({ - 'hour': 12, - 'minute': 50, - }), - 'isWeeklyTimer': False, - 'timerAction': 'ACTIVATE', - 'timerWeekDays': list([ - 'MONDAY', - ]), - }), - dict({ - 'departureTime': dict({ - 'hour': 18, - 'minute': 59, - }), - 'isWeeklyTimer': True, - 'timerAction': 'DEACTIVATE', - 'timerWeekDays': list([ - 'WEDNESDAY', - ]), - }), - ]), - 'combustionFuelLevel': dict({ - 'range': 105, - 'remainingFuelLiters': 6, - }), - 'currentMileage': 137009, - 'doorsState': dict({ - 'combinedSecurityState': 'UNLOCKED', - 'combinedState': 'CLOSED', - 'hood': 'CLOSED', - 'leftFront': 'CLOSED', - 'leftRear': 'CLOSED', - 'rightFront': 'CLOSED', - 'rightRear': 'CLOSED', - 'trunk': 'CLOSED', - }), - 'driverPreferences': dict({ - 'lscPrivacyMode': 'OFF', - }), - 'electricChargingState': dict({ - 'chargingConnectionType': 'CONDUCTIVE', - 'chargingLevelPercent': 82, - 'chargingStatus': 'WAITING_FOR_CHARGING', - 'chargingTarget': 100, - 'isChargerConnected': True, - 'range': 174, - }), - 'isLeftSteering': True, - 'isLscSupported': True, - 'lastFetched': '2022-06-22T14:24:23.982Z', - 'lastUpdatedAt': '2022-06-22T13:58:52Z', - 'range': 174, - 'requiredServices': list([ - dict({ - 'dateTime': '2022-10-01T00:00:00.000Z', - 'description': 'Next service due by the specified date.', - 'status': 'OK', - 'type': 'BRAKE_FLUID', - }), - dict({ - 'dateTime': '2023-05-01T00:00:00.000Z', - 'description': 'Next vehicle check due after the specified distance or date.', - 'status': 'OK', - 'type': 'VEHICLE_CHECK', - }), - dict({ - 'dateTime': '2023-05-01T00:00:00.000Z', - 'description': 'Next state inspection due by the specified date.', - 'status': 'OK', - 'type': 'VEHICLE_TUV', - }), - ]), - 'roofState': dict({ - 'roofState': 'CLOSED', - 'roofStateType': 'SUN_ROOF', - }), - 'windowsState': dict({ - 'combinedState': 'CLOSED', - 'leftFront': 'CLOSED', - 'rightFront': 'CLOSED', - }), - }), - }), - 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', - }), dict({ 'content': dict({ 'chargeAndClimateSettings': dict({ @@ -3235,20 +3195,31 @@ }), 'filename': 'mini-eadrax-vcs_v5_vehicle-list.json', }), + dict({ + 'content': dict({ + 'gcid': 'ceb64158-d2ca-47e9-9ee6-cbffb881434e', + 'mappingInfos': list([ + ]), + }), + 'filename': 'toyota-eadrax-vcs_v5_vehicle-list.json', + }), dict({ 'content': dict({ 'capabilities': dict({ 'a4aType': 'BLUETOOTH', - 'checkSustainabilityDPP': False, + 'alarmSystem': True, 'climateFunction': 'AIR_CONDITIONING', 'climateNow': True, 'digitalKey': dict({ 'bookedServicePackage': 'SMACC_2_UWB', + 'isDigitalKeyFirstSupported': False, 'readerGraphics': 'readerGraphics', 'state': 'ACTIVATED', + 'vehicleSoftwareUpgradeRequired': True, }), 'horn': True, 'inCarCamera': True, + 'inCarCameraDwa': True, 'isBmwChargingSupported': True, 'isCarSharingSupported': False, 'isChargeNowForBusinessSupported': True, @@ -3259,27 +3230,38 @@ 'isChargingPowerLimitEnabled': True, 'isChargingSettingsEnabled': True, 'isChargingTargetSocEnabled': True, + 'isClimateTimerSupported': False, 'isClimateTimerWeeklyActive': False, 'isCustomerEsimSupported': True, 'isDCSContractManagementSupported': True, 'isDataPrivacyEnabled': False, 'isEasyChargeEnabled': True, 'isEvGoChargingSupported': False, + 'isLocationBasedChargingSettingsSupported': False, 'isMiniChargingSupported': False, 'isNonLscFeatureEnabled': False, 'isPersonalPictureUploadSupported': False, - 'isRemoteEngineStartSupported': False, + 'isPlugAndChargeSupported': False, + 'isRemoteEngineStartEnabled': False, + 'isRemoteEngineStartSupported': True, 'isRemoteHistoryDeletionSupported': False, 'isRemoteHistorySupported': True, + 'isRemoteParkingEes25Active': False, 'isRemoteParkingSupported': False, 'isRemoteServicesActivationRequired': False, 'isRemoteServicesBookingRequired': False, 'isScanAndChargeSupported': True, 'isSustainabilityAccumulatedViewEnabled': False, 'isSustainabilitySupported': False, + 'isThirdPartyAppStoreSupported': False, 'isWifiHotspotServiceSupported': False, 'lastStateCallState': 'ACTIVATED', 'lights': True, + 'locationBasedCommerceFeatures': dict({ + 'fueling': False, + 'parking': False, + 'reservations': False, + }), 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ @@ -3294,11 +3276,45 @@ 'NOT_SUPPORTED', ]), }), + 'remoteServices': dict({ + 'doorLock': dict({ + 'id': 'doorLock', + 'state': 'ACTIVATED', + }), + 'doorUnlock': dict({ + 'id': 'doorUnlock', + 'state': 'ACTIVATED', + }), + 'hornBlow': dict({ + 'id': 'hornBlow', + 'state': 'ACTIVATED', + }), + 'inCarCamera': dict({ + 'id': 'inCarCamera', + 'state': 'ACTIVATED', + }), + 'inCarCameraDwa': dict({ + 'id': 'inCarCameraDwa', + 'state': 'ACTIVATED', + }), + 'lightFlash': dict({ + 'id': 'lightFlash', + 'state': 'ACTIVATED', + }), + 'remote360': dict({ + 'id': 'remote360', + 'state': 'ACTIVATED', + }), + 'surroundViewRecorder': dict({ + 'id': 'surroundViewRecorder', + 'state': 'ACTIVATED', + }), + }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, 'specialThemeSupport': list([ ]), - 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexa': True, 'speechThirdPartyAlexaSDK': False, 'surroundViewRecorder': True, 'unlock': True, @@ -3476,6 +3492,7 @@ 'roofState': 'CLOSED', 'roofStateType': 'SUN_ROOF', }), + 'securityOverviewMode': 'ARMED', 'tireState': dict({ 'frontLeft': dict({ 'details': dict({ @@ -3566,6 +3583,18 @@ }), }), }), + 'vehicleSoftwareVersion': dict({ + 'iStep': dict({ + 'iStep': 0, + 'month': 0, + 'seriesCluster': '', + 'year': 0, + }), + 'puStep': dict({ + 'month': 0, + 'year': 0, + }), + }), 'windowsState': dict({ 'combinedState': 'CLOSED', 'leftFront': 'CLOSED', @@ -3685,15 +3714,19 @@ 'content': dict({ 'capabilities': dict({ 'a4aType': 'NOT_SUPPORTED', - 'checkSustainabilityDPP': False, + 'alarmSystem': False, 'climateFunction': 'AIR_CONDITIONING', 'climateNow': True, 'digitalKey': dict({ 'bookedServicePackage': 'SMACC_1_5', + 'isDigitalKeyFirstSupported': False, 'readerGraphics': 'readerGraphics', 'state': 'ACTIVATED', + 'vehicleSoftwareUpgradeRequired': False, }), 'horn': True, + 'inCarCamera': False, + 'inCarCameraDwa': False, 'isBmwChargingSupported': True, 'isCarSharingSupported': False, 'isChargeNowForBusinessSupported': True, @@ -3704,37 +3737,80 @@ 'isChargingPowerLimitEnabled': True, 'isChargingSettingsEnabled': True, 'isChargingTargetSocEnabled': True, + 'isClimateTimerSupported': False, 'isClimateTimerWeeklyActive': False, - 'isCustomerEsimSupported': True, + 'isCustomerEsimSupported': False, 'isDCSContractManagementSupported': True, 'isDataPrivacyEnabled': False, 'isEasyChargeEnabled': True, 'isEvGoChargingSupported': False, + 'isLocationBasedChargingSettingsSupported': False, 'isMiniChargingSupported': False, 'isNonLscFeatureEnabled': False, 'isPersonalPictureUploadSupported': False, - 'isRemoteEngineStartSupported': False, + 'isPlugAndChargeSupported': False, + 'isRemoteEngineStartEnabled': False, + 'isRemoteEngineStartSupported': True, 'isRemoteHistoryDeletionSupported': False, 'isRemoteHistorySupported': True, + 'isRemoteParkingEes25Active': False, 'isRemoteParkingSupported': False, 'isRemoteServicesActivationRequired': False, 'isRemoteServicesBookingRequired': False, 'isScanAndChargeSupported': True, 'isSustainabilityAccumulatedViewEnabled': False, 'isSustainabilitySupported': False, + 'isThirdPartyAppStoreSupported': False, 'isWifiHotspotServiceSupported': False, 'lastStateCallState': 'ACTIVATED', 'lights': True, + 'locationBasedCommerceFeatures': dict({ + 'fueling': False, + 'parking': False, + 'reservations': False, + }), 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ }), + 'remoteServices': dict({ + 'doorLock': dict({ + 'id': 'doorLock', + 'state': 'ACTIVATED', + }), + 'doorUnlock': dict({ + 'id': 'doorUnlock', + 'state': 'ACTIVATED', + }), + 'hornBlow': dict({ + 'id': 'hornBlow', + 'state': 'ACTIVATED', + }), + 'inCarCamera': dict({ + 'id': 'inCarCamera', + }), + 'inCarCameraDwa': dict({ + 'id': 'inCarCameraDwa', + }), + 'lightFlash': dict({ + 'id': 'lightFlash', + 'state': 'ACTIVATED', + }), + 'remote360': dict({ + 'id': 'remote360', + 'state': 'ACTIVATED', + }), + 'surroundViewRecorder': dict({ + 'id': 'surroundViewRecorder', + }), + }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, 'specialThemeSupport': list([ ]), - 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexa': True, 'speechThirdPartyAlexaSDK': False, + 'surroundViewRecorder': False, 'unlock': True, 'vehicleFinder': True, 'vehicleStateSource': 'LAST_STATE_CALL', @@ -3906,6 +3982,7 @@ 'type': 'TIRE_WEAR_FRONT', }), ]), + 'securityOverviewMode': 'NOT_ARMED', 'tireState': dict({ 'frontLeft': dict({ 'details': dict({ @@ -3996,6 +4073,18 @@ }), }), }), + 'vehicleSoftwareVersion': dict({ + 'iStep': dict({ + 'iStep': 0, + 'month': 0, + 'seriesCluster': '', + 'year': 0, + }), + 'puStep': dict({ + 'month': 0, + 'year': 0, + }), + }), 'windowsState': dict({ 'combinedState': 'CLOSED', 'leftFront': 'CLOSED', @@ -4115,16 +4204,20 @@ 'content': dict({ 'capabilities': dict({ 'a4aType': 'NOT_SUPPORTED', - 'checkSustainabilityDPP': False, + 'alarmSystem': False, 'climateFunction': 'VENTILATION', 'climateNow': True, 'climateTimerTrigger': 'DEPARTURE_TIMER', 'digitalKey': dict({ 'bookedServicePackage': 'SMACC_1_5', + 'isDigitalKeyFirstSupported': False, 'readerGraphics': 'readerGraphics', 'state': 'ACTIVATED', + 'vehicleSoftwareUpgradeRequired': False, }), 'horn': True, + 'inCarCamera': False, + 'inCarCameraDwa': False, 'isBmwChargingSupported': False, 'isCarSharingSupported': False, 'isChargeNowForBusinessSupported': False, @@ -4142,31 +4235,73 @@ 'isDataPrivacyEnabled': False, 'isEasyChargeEnabled': False, 'isEvGoChargingSupported': False, + 'isLocationBasedChargingSettingsSupported': False, 'isMiniChargingSupported': False, 'isNonLscFeatureEnabled': False, 'isPersonalPictureUploadSupported': False, - 'isRemoteEngineStartSupported': False, + 'isPlugAndChargeSupported': False, + 'isRemoteEngineStartEnabled': False, + 'isRemoteEngineStartSupported': True, 'isRemoteHistoryDeletionSupported': False, 'isRemoteHistorySupported': True, + 'isRemoteParkingEes25Active': False, 'isRemoteParkingSupported': False, 'isRemoteServicesActivationRequired': False, 'isRemoteServicesBookingRequired': False, 'isScanAndChargeSupported': False, 'isSustainabilityAccumulatedViewEnabled': False, 'isSustainabilitySupported': False, - 'isWifiHotspotServiceSupported': True, + 'isThirdPartyAppStoreSupported': False, + 'isWifiHotspotServiceSupported': False, 'lastStateCallState': 'ACTIVATED', 'lights': True, + 'locationBasedCommerceFeatures': dict({ + 'fueling': False, + 'parking': False, + 'reservations': False, + }), 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ }), + 'remoteServices': dict({ + 'doorLock': dict({ + 'id': 'doorLock', + 'state': 'ACTIVATED', + }), + 'doorUnlock': dict({ + 'id': 'doorUnlock', + 'state': 'ACTIVATED', + }), + 'hornBlow': dict({ + 'id': 'hornBlow', + 'state': 'ACTIVATED', + }), + 'inCarCamera': dict({ + 'id': 'inCarCamera', + }), + 'inCarCameraDwa': dict({ + 'id': 'inCarCameraDwa', + }), + 'lightFlash': dict({ + 'id': 'lightFlash', + 'state': 'ACTIVATED', + }), + 'remote360': dict({ + 'id': 'remote360', + 'state': 'ACTIVATED', + }), + 'surroundViewRecorder': dict({ + 'id': 'surroundViewRecorder', + }), + }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, 'specialThemeSupport': list([ ]), - 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexa': True, 'speechThirdPartyAlexaSDK': False, + 'surroundViewRecorder': False, 'unlock': True, 'vehicleFinder': True, 'vehicleStateSource': 'LAST_STATE_CALL', @@ -4300,6 +4435,7 @@ 'type': 'TIRE_WEAR_FRONT', }), ]), + 'securityOverviewMode': None, 'tireState': dict({ 'frontLeft': dict({ 'details': dict({ @@ -4386,6 +4522,18 @@ }), }), }), + 'vehicleSoftwareVersion': dict({ + 'iStep': dict({ + 'iStep': 0, + 'month': 0, + 'seriesCluster': '', + 'year': 0, + }), + 'puStep': dict({ + 'month': 0, + 'year': 0, + }), + }), 'windowsState': dict({ 'combinedState': 'CLOSED', 'leftFront': 'CLOSED', @@ -5343,226 +5491,6 @@ 'vin': '**REDACTED**', }), 'fingerprint': list([ - dict({ - 'content': dict({ - 'capabilities': dict({ - 'climateFunction': 'AIR_CONDITIONING', - 'climateNow': True, - 'climateTimerTrigger': 'DEPARTURE_TIMER', - 'horn': True, - 'isBmwChargingSupported': True, - 'isCarSharingSupported': False, - 'isChargeNowForBusinessSupported': False, - 'isChargingHistorySupported': True, - 'isChargingHospitalityEnabled': False, - 'isChargingLoudnessEnabled': False, - 'isChargingPlanSupported': True, - 'isChargingPowerLimitEnabled': False, - 'isChargingSettingsEnabled': False, - 'isChargingTargetSocEnabled': False, - 'isClimateTimerSupported': True, - 'isCustomerEsimSupported': False, - 'isDCSContractManagementSupported': True, - 'isDataPrivacyEnabled': False, - 'isEasyChargeEnabled': False, - 'isEvGoChargingSupported': False, - 'isMiniChargingSupported': False, - 'isNonLscFeatureEnabled': False, - 'isRemoteEngineStartSupported': False, - 'isRemoteHistoryDeletionSupported': False, - 'isRemoteHistorySupported': True, - 'isRemoteParkingSupported': False, - 'isRemoteServicesActivationRequired': False, - 'isRemoteServicesBookingRequired': False, - 'isScanAndChargeSupported': False, - 'isSustainabilitySupported': False, - 'isWifiHotspotServiceSupported': False, - 'lastStateCallState': 'ACTIVATED', - 'lights': True, - 'lock': True, - 'remoteChargingCommands': dict({ - }), - 'sendPoi': True, - 'specialThemeSupport': list([ - ]), - 'unlock': True, - 'vehicleFinder': False, - 'vehicleStateSource': 'LAST_STATE_CALL', - }), - 'state': dict({ - 'chargingProfile': dict({ - 'chargingControlType': 'WEEKLY_PLANNER', - 'chargingMode': 'DELAYED_CHARGING', - 'chargingPreference': 'CHARGING_WINDOW', - 'chargingSettings': dict({ - 'hospitality': 'NO_ACTION', - 'idcc': 'NO_ACTION', - 'targetSoc': 100, - }), - 'climatisationOn': False, - 'departureTimes': list([ - dict({ - 'action': 'DEACTIVATE', - 'id': 1, - 'timeStamp': dict({ - 'hour': 7, - 'minute': 35, - }), - 'timerWeekDays': list([ - 'MONDAY', - 'TUESDAY', - 'WEDNESDAY', - 'THURSDAY', - 'FRIDAY', - ]), - }), - dict({ - 'action': 'DEACTIVATE', - 'id': 2, - 'timeStamp': dict({ - 'hour': 18, - 'minute': 0, - }), - 'timerWeekDays': list([ - 'MONDAY', - 'TUESDAY', - 'WEDNESDAY', - 'THURSDAY', - 'FRIDAY', - 'SATURDAY', - 'SUNDAY', - ]), - }), - dict({ - 'action': 'DEACTIVATE', - 'id': 3, - 'timeStamp': dict({ - 'hour': 7, - 'minute': 0, - }), - 'timerWeekDays': list([ - ]), - }), - dict({ - 'action': 'DEACTIVATE', - 'id': 4, - 'timerWeekDays': list([ - ]), - }), - ]), - 'reductionOfChargeCurrent': dict({ - 'end': dict({ - 'hour': 1, - 'minute': 30, - }), - 'start': dict({ - 'hour': 18, - 'minute': 1, - }), - }), - }), - 'checkControlMessages': list([ - ]), - 'climateTimers': list([ - dict({ - 'departureTime': dict({ - 'hour': 6, - 'minute': 40, - }), - 'isWeeklyTimer': True, - 'timerAction': 'ACTIVATE', - 'timerWeekDays': list([ - 'THURSDAY', - 'SUNDAY', - ]), - }), - dict({ - 'departureTime': dict({ - 'hour': 12, - 'minute': 50, - }), - 'isWeeklyTimer': False, - 'timerAction': 'ACTIVATE', - 'timerWeekDays': list([ - 'MONDAY', - ]), - }), - dict({ - 'departureTime': dict({ - 'hour': 18, - 'minute': 59, - }), - 'isWeeklyTimer': True, - 'timerAction': 'DEACTIVATE', - 'timerWeekDays': list([ - 'WEDNESDAY', - ]), - }), - ]), - 'combustionFuelLevel': dict({ - 'range': 105, - 'remainingFuelLiters': 6, - }), - 'currentMileage': 137009, - 'doorsState': dict({ - 'combinedSecurityState': 'UNLOCKED', - 'combinedState': 'CLOSED', - 'hood': 'CLOSED', - 'leftFront': 'CLOSED', - 'leftRear': 'CLOSED', - 'rightFront': 'CLOSED', - 'rightRear': 'CLOSED', - 'trunk': 'CLOSED', - }), - 'driverPreferences': dict({ - 'lscPrivacyMode': 'OFF', - }), - 'electricChargingState': dict({ - 'chargingConnectionType': 'CONDUCTIVE', - 'chargingLevelPercent': 82, - 'chargingStatus': 'WAITING_FOR_CHARGING', - 'chargingTarget': 100, - 'isChargerConnected': True, - 'range': 174, - }), - 'isLeftSteering': True, - 'isLscSupported': True, - 'lastFetched': '2022-06-22T14:24:23.982Z', - 'lastUpdatedAt': '2022-06-22T13:58:52Z', - 'range': 174, - 'requiredServices': list([ - dict({ - 'dateTime': '2022-10-01T00:00:00.000Z', - 'description': 'Next service due by the specified date.', - 'status': 'OK', - 'type': 'BRAKE_FLUID', - }), - dict({ - 'dateTime': '2023-05-01T00:00:00.000Z', - 'description': 'Next vehicle check due after the specified distance or date.', - 'status': 'OK', - 'type': 'VEHICLE_CHECK', - }), - dict({ - 'dateTime': '2023-05-01T00:00:00.000Z', - 'description': 'Next state inspection due by the specified date.', - 'status': 'OK', - 'type': 'VEHICLE_TUV', - }), - ]), - 'roofState': dict({ - 'roofState': 'CLOSED', - 'roofStateType': 'SUN_ROOF', - }), - 'windowsState': dict({ - 'combinedState': 'CLOSED', - 'leftFront': 'CLOSED', - 'rightFront': 'CLOSED', - }), - }), - }), - 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', - }), dict({ 'content': dict({ 'chargeAndClimateSettings': dict({ @@ -5636,20 +5564,31 @@ }), 'filename': 'mini-eadrax-vcs_v5_vehicle-list.json', }), + dict({ + 'content': dict({ + 'gcid': 'ceb64158-d2ca-47e9-9ee6-cbffb881434e', + 'mappingInfos': list([ + ]), + }), + 'filename': 'toyota-eadrax-vcs_v5_vehicle-list.json', + }), dict({ 'content': dict({ 'capabilities': dict({ 'a4aType': 'BLUETOOTH', - 'checkSustainabilityDPP': False, + 'alarmSystem': True, 'climateFunction': 'AIR_CONDITIONING', 'climateNow': True, 'digitalKey': dict({ 'bookedServicePackage': 'SMACC_2_UWB', + 'isDigitalKeyFirstSupported': False, 'readerGraphics': 'readerGraphics', 'state': 'ACTIVATED', + 'vehicleSoftwareUpgradeRequired': True, }), 'horn': True, 'inCarCamera': True, + 'inCarCameraDwa': True, 'isBmwChargingSupported': True, 'isCarSharingSupported': False, 'isChargeNowForBusinessSupported': True, @@ -5660,27 +5599,38 @@ 'isChargingPowerLimitEnabled': True, 'isChargingSettingsEnabled': True, 'isChargingTargetSocEnabled': True, + 'isClimateTimerSupported': False, 'isClimateTimerWeeklyActive': False, 'isCustomerEsimSupported': True, 'isDCSContractManagementSupported': True, 'isDataPrivacyEnabled': False, 'isEasyChargeEnabled': True, 'isEvGoChargingSupported': False, + 'isLocationBasedChargingSettingsSupported': False, 'isMiniChargingSupported': False, 'isNonLscFeatureEnabled': False, 'isPersonalPictureUploadSupported': False, - 'isRemoteEngineStartSupported': False, + 'isPlugAndChargeSupported': False, + 'isRemoteEngineStartEnabled': False, + 'isRemoteEngineStartSupported': True, 'isRemoteHistoryDeletionSupported': False, 'isRemoteHistorySupported': True, + 'isRemoteParkingEes25Active': False, 'isRemoteParkingSupported': False, 'isRemoteServicesActivationRequired': False, 'isRemoteServicesBookingRequired': False, 'isScanAndChargeSupported': True, 'isSustainabilityAccumulatedViewEnabled': False, 'isSustainabilitySupported': False, + 'isThirdPartyAppStoreSupported': False, 'isWifiHotspotServiceSupported': False, 'lastStateCallState': 'ACTIVATED', 'lights': True, + 'locationBasedCommerceFeatures': dict({ + 'fueling': False, + 'parking': False, + 'reservations': False, + }), 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ @@ -5695,11 +5645,45 @@ 'NOT_SUPPORTED', ]), }), + 'remoteServices': dict({ + 'doorLock': dict({ + 'id': 'doorLock', + 'state': 'ACTIVATED', + }), + 'doorUnlock': dict({ + 'id': 'doorUnlock', + 'state': 'ACTIVATED', + }), + 'hornBlow': dict({ + 'id': 'hornBlow', + 'state': 'ACTIVATED', + }), + 'inCarCamera': dict({ + 'id': 'inCarCamera', + 'state': 'ACTIVATED', + }), + 'inCarCameraDwa': dict({ + 'id': 'inCarCameraDwa', + 'state': 'ACTIVATED', + }), + 'lightFlash': dict({ + 'id': 'lightFlash', + 'state': 'ACTIVATED', + }), + 'remote360': dict({ + 'id': 'remote360', + 'state': 'ACTIVATED', + }), + 'surroundViewRecorder': dict({ + 'id': 'surroundViewRecorder', + 'state': 'ACTIVATED', + }), + }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, 'specialThemeSupport': list([ ]), - 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexa': True, 'speechThirdPartyAlexaSDK': False, 'surroundViewRecorder': True, 'unlock': True, @@ -5877,6 +5861,7 @@ 'roofState': 'CLOSED', 'roofStateType': 'SUN_ROOF', }), + 'securityOverviewMode': 'ARMED', 'tireState': dict({ 'frontLeft': dict({ 'details': dict({ @@ -5967,6 +5952,18 @@ }), }), }), + 'vehicleSoftwareVersion': dict({ + 'iStep': dict({ + 'iStep': 0, + 'month': 0, + 'seriesCluster': '', + 'year': 0, + }), + 'puStep': dict({ + 'month': 0, + 'year': 0, + }), + }), 'windowsState': dict({ 'combinedState': 'CLOSED', 'leftFront': 'CLOSED', @@ -6086,15 +6083,19 @@ 'content': dict({ 'capabilities': dict({ 'a4aType': 'NOT_SUPPORTED', - 'checkSustainabilityDPP': False, + 'alarmSystem': False, 'climateFunction': 'AIR_CONDITIONING', 'climateNow': True, 'digitalKey': dict({ 'bookedServicePackage': 'SMACC_1_5', + 'isDigitalKeyFirstSupported': False, 'readerGraphics': 'readerGraphics', 'state': 'ACTIVATED', + 'vehicleSoftwareUpgradeRequired': False, }), 'horn': True, + 'inCarCamera': False, + 'inCarCameraDwa': False, 'isBmwChargingSupported': True, 'isCarSharingSupported': False, 'isChargeNowForBusinessSupported': True, @@ -6105,37 +6106,80 @@ 'isChargingPowerLimitEnabled': True, 'isChargingSettingsEnabled': True, 'isChargingTargetSocEnabled': True, + 'isClimateTimerSupported': False, 'isClimateTimerWeeklyActive': False, - 'isCustomerEsimSupported': True, + 'isCustomerEsimSupported': False, 'isDCSContractManagementSupported': True, 'isDataPrivacyEnabled': False, 'isEasyChargeEnabled': True, 'isEvGoChargingSupported': False, + 'isLocationBasedChargingSettingsSupported': False, 'isMiniChargingSupported': False, 'isNonLscFeatureEnabled': False, 'isPersonalPictureUploadSupported': False, - 'isRemoteEngineStartSupported': False, + 'isPlugAndChargeSupported': False, + 'isRemoteEngineStartEnabled': False, + 'isRemoteEngineStartSupported': True, 'isRemoteHistoryDeletionSupported': False, 'isRemoteHistorySupported': True, + 'isRemoteParkingEes25Active': False, 'isRemoteParkingSupported': False, 'isRemoteServicesActivationRequired': False, 'isRemoteServicesBookingRequired': False, 'isScanAndChargeSupported': True, 'isSustainabilityAccumulatedViewEnabled': False, 'isSustainabilitySupported': False, + 'isThirdPartyAppStoreSupported': False, 'isWifiHotspotServiceSupported': False, 'lastStateCallState': 'ACTIVATED', 'lights': True, + 'locationBasedCommerceFeatures': dict({ + 'fueling': False, + 'parking': False, + 'reservations': False, + }), 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ }), + 'remoteServices': dict({ + 'doorLock': dict({ + 'id': 'doorLock', + 'state': 'ACTIVATED', + }), + 'doorUnlock': dict({ + 'id': 'doorUnlock', + 'state': 'ACTIVATED', + }), + 'hornBlow': dict({ + 'id': 'hornBlow', + 'state': 'ACTIVATED', + }), + 'inCarCamera': dict({ + 'id': 'inCarCamera', + }), + 'inCarCameraDwa': dict({ + 'id': 'inCarCameraDwa', + }), + 'lightFlash': dict({ + 'id': 'lightFlash', + 'state': 'ACTIVATED', + }), + 'remote360': dict({ + 'id': 'remote360', + 'state': 'ACTIVATED', + }), + 'surroundViewRecorder': dict({ + 'id': 'surroundViewRecorder', + }), + }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, 'specialThemeSupport': list([ ]), - 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexa': True, 'speechThirdPartyAlexaSDK': False, + 'surroundViewRecorder': False, 'unlock': True, 'vehicleFinder': True, 'vehicleStateSource': 'LAST_STATE_CALL', @@ -6307,6 +6351,7 @@ 'type': 'TIRE_WEAR_FRONT', }), ]), + 'securityOverviewMode': 'NOT_ARMED', 'tireState': dict({ 'frontLeft': dict({ 'details': dict({ @@ -6397,6 +6442,18 @@ }), }), }), + 'vehicleSoftwareVersion': dict({ + 'iStep': dict({ + 'iStep': 0, + 'month': 0, + 'seriesCluster': '', + 'year': 0, + }), + 'puStep': dict({ + 'month': 0, + 'year': 0, + }), + }), 'windowsState': dict({ 'combinedState': 'CLOSED', 'leftFront': 'CLOSED', @@ -6516,16 +6573,20 @@ 'content': dict({ 'capabilities': dict({ 'a4aType': 'NOT_SUPPORTED', - 'checkSustainabilityDPP': False, + 'alarmSystem': False, 'climateFunction': 'VENTILATION', 'climateNow': True, 'climateTimerTrigger': 'DEPARTURE_TIMER', 'digitalKey': dict({ 'bookedServicePackage': 'SMACC_1_5', + 'isDigitalKeyFirstSupported': False, 'readerGraphics': 'readerGraphics', 'state': 'ACTIVATED', + 'vehicleSoftwareUpgradeRequired': False, }), 'horn': True, + 'inCarCamera': False, + 'inCarCameraDwa': False, 'isBmwChargingSupported': False, 'isCarSharingSupported': False, 'isChargeNowForBusinessSupported': False, @@ -6543,31 +6604,73 @@ 'isDataPrivacyEnabled': False, 'isEasyChargeEnabled': False, 'isEvGoChargingSupported': False, + 'isLocationBasedChargingSettingsSupported': False, 'isMiniChargingSupported': False, 'isNonLscFeatureEnabled': False, 'isPersonalPictureUploadSupported': False, - 'isRemoteEngineStartSupported': False, + 'isPlugAndChargeSupported': False, + 'isRemoteEngineStartEnabled': False, + 'isRemoteEngineStartSupported': True, 'isRemoteHistoryDeletionSupported': False, 'isRemoteHistorySupported': True, + 'isRemoteParkingEes25Active': False, 'isRemoteParkingSupported': False, 'isRemoteServicesActivationRequired': False, 'isRemoteServicesBookingRequired': False, 'isScanAndChargeSupported': False, 'isSustainabilityAccumulatedViewEnabled': False, 'isSustainabilitySupported': False, - 'isWifiHotspotServiceSupported': True, + 'isThirdPartyAppStoreSupported': False, + 'isWifiHotspotServiceSupported': False, 'lastStateCallState': 'ACTIVATED', 'lights': True, + 'locationBasedCommerceFeatures': dict({ + 'fueling': False, + 'parking': False, + 'reservations': False, + }), 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ }), + 'remoteServices': dict({ + 'doorLock': dict({ + 'id': 'doorLock', + 'state': 'ACTIVATED', + }), + 'doorUnlock': dict({ + 'id': 'doorUnlock', + 'state': 'ACTIVATED', + }), + 'hornBlow': dict({ + 'id': 'hornBlow', + 'state': 'ACTIVATED', + }), + 'inCarCamera': dict({ + 'id': 'inCarCamera', + }), + 'inCarCameraDwa': dict({ + 'id': 'inCarCameraDwa', + }), + 'lightFlash': dict({ + 'id': 'lightFlash', + 'state': 'ACTIVATED', + }), + 'remote360': dict({ + 'id': 'remote360', + 'state': 'ACTIVATED', + }), + 'surroundViewRecorder': dict({ + 'id': 'surroundViewRecorder', + }), + }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, 'specialThemeSupport': list([ ]), - 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexa': True, 'speechThirdPartyAlexaSDK': False, + 'surroundViewRecorder': False, 'unlock': True, 'vehicleFinder': True, 'vehicleStateSource': 'LAST_STATE_CALL', @@ -6701,6 +6804,7 @@ 'type': 'TIRE_WEAR_FRONT', }), ]), + 'securityOverviewMode': None, 'tireState': dict({ 'frontLeft': dict({ 'details': dict({ @@ -6787,6 +6891,18 @@ }), }), }), + 'vehicleSoftwareVersion': dict({ + 'iStep': dict({ + 'iStep': 0, + 'month': 0, + 'seriesCluster': '', + 'year': 0, + }), + 'puStep': dict({ + 'month': 0, + 'year': 0, + }), + }), 'windowsState': dict({ 'combinedState': 'CLOSED', 'leftFront': 'CLOSED', @@ -7098,226 +7214,6 @@ dict({ 'data': None, 'fingerprint': list([ - dict({ - 'content': dict({ - 'capabilities': dict({ - 'climateFunction': 'AIR_CONDITIONING', - 'climateNow': True, - 'climateTimerTrigger': 'DEPARTURE_TIMER', - 'horn': True, - 'isBmwChargingSupported': True, - 'isCarSharingSupported': False, - 'isChargeNowForBusinessSupported': False, - 'isChargingHistorySupported': True, - 'isChargingHospitalityEnabled': False, - 'isChargingLoudnessEnabled': False, - 'isChargingPlanSupported': True, - 'isChargingPowerLimitEnabled': False, - 'isChargingSettingsEnabled': False, - 'isChargingTargetSocEnabled': False, - 'isClimateTimerSupported': True, - 'isCustomerEsimSupported': False, - 'isDCSContractManagementSupported': True, - 'isDataPrivacyEnabled': False, - 'isEasyChargeEnabled': False, - 'isEvGoChargingSupported': False, - 'isMiniChargingSupported': False, - 'isNonLscFeatureEnabled': False, - 'isRemoteEngineStartSupported': False, - 'isRemoteHistoryDeletionSupported': False, - 'isRemoteHistorySupported': True, - 'isRemoteParkingSupported': False, - 'isRemoteServicesActivationRequired': False, - 'isRemoteServicesBookingRequired': False, - 'isScanAndChargeSupported': False, - 'isSustainabilitySupported': False, - 'isWifiHotspotServiceSupported': False, - 'lastStateCallState': 'ACTIVATED', - 'lights': True, - 'lock': True, - 'remoteChargingCommands': dict({ - }), - 'sendPoi': True, - 'specialThemeSupport': list([ - ]), - 'unlock': True, - 'vehicleFinder': False, - 'vehicleStateSource': 'LAST_STATE_CALL', - }), - 'state': dict({ - 'chargingProfile': dict({ - 'chargingControlType': 'WEEKLY_PLANNER', - 'chargingMode': 'DELAYED_CHARGING', - 'chargingPreference': 'CHARGING_WINDOW', - 'chargingSettings': dict({ - 'hospitality': 'NO_ACTION', - 'idcc': 'NO_ACTION', - 'targetSoc': 100, - }), - 'climatisationOn': False, - 'departureTimes': list([ - dict({ - 'action': 'DEACTIVATE', - 'id': 1, - 'timeStamp': dict({ - 'hour': 7, - 'minute': 35, - }), - 'timerWeekDays': list([ - 'MONDAY', - 'TUESDAY', - 'WEDNESDAY', - 'THURSDAY', - 'FRIDAY', - ]), - }), - dict({ - 'action': 'DEACTIVATE', - 'id': 2, - 'timeStamp': dict({ - 'hour': 18, - 'minute': 0, - }), - 'timerWeekDays': list([ - 'MONDAY', - 'TUESDAY', - 'WEDNESDAY', - 'THURSDAY', - 'FRIDAY', - 'SATURDAY', - 'SUNDAY', - ]), - }), - dict({ - 'action': 'DEACTIVATE', - 'id': 3, - 'timeStamp': dict({ - 'hour': 7, - 'minute': 0, - }), - 'timerWeekDays': list([ - ]), - }), - dict({ - 'action': 'DEACTIVATE', - 'id': 4, - 'timerWeekDays': list([ - ]), - }), - ]), - 'reductionOfChargeCurrent': dict({ - 'end': dict({ - 'hour': 1, - 'minute': 30, - }), - 'start': dict({ - 'hour': 18, - 'minute': 1, - }), - }), - }), - 'checkControlMessages': list([ - ]), - 'climateTimers': list([ - dict({ - 'departureTime': dict({ - 'hour': 6, - 'minute': 40, - }), - 'isWeeklyTimer': True, - 'timerAction': 'ACTIVATE', - 'timerWeekDays': list([ - 'THURSDAY', - 'SUNDAY', - ]), - }), - dict({ - 'departureTime': dict({ - 'hour': 12, - 'minute': 50, - }), - 'isWeeklyTimer': False, - 'timerAction': 'ACTIVATE', - 'timerWeekDays': list([ - 'MONDAY', - ]), - }), - dict({ - 'departureTime': dict({ - 'hour': 18, - 'minute': 59, - }), - 'isWeeklyTimer': True, - 'timerAction': 'DEACTIVATE', - 'timerWeekDays': list([ - 'WEDNESDAY', - ]), - }), - ]), - 'combustionFuelLevel': dict({ - 'range': 105, - 'remainingFuelLiters': 6, - }), - 'currentMileage': 137009, - 'doorsState': dict({ - 'combinedSecurityState': 'UNLOCKED', - 'combinedState': 'CLOSED', - 'hood': 'CLOSED', - 'leftFront': 'CLOSED', - 'leftRear': 'CLOSED', - 'rightFront': 'CLOSED', - 'rightRear': 'CLOSED', - 'trunk': 'CLOSED', - }), - 'driverPreferences': dict({ - 'lscPrivacyMode': 'OFF', - }), - 'electricChargingState': dict({ - 'chargingConnectionType': 'CONDUCTIVE', - 'chargingLevelPercent': 82, - 'chargingStatus': 'WAITING_FOR_CHARGING', - 'chargingTarget': 100, - 'isChargerConnected': True, - 'range': 174, - }), - 'isLeftSteering': True, - 'isLscSupported': True, - 'lastFetched': '2022-06-22T14:24:23.982Z', - 'lastUpdatedAt': '2022-06-22T13:58:52Z', - 'range': 174, - 'requiredServices': list([ - dict({ - 'dateTime': '2022-10-01T00:00:00.000Z', - 'description': 'Next service due by the specified date.', - 'status': 'OK', - 'type': 'BRAKE_FLUID', - }), - dict({ - 'dateTime': '2023-05-01T00:00:00.000Z', - 'description': 'Next vehicle check due after the specified distance or date.', - 'status': 'OK', - 'type': 'VEHICLE_CHECK', - }), - dict({ - 'dateTime': '2023-05-01T00:00:00.000Z', - 'description': 'Next state inspection due by the specified date.', - 'status': 'OK', - 'type': 'VEHICLE_TUV', - }), - ]), - 'roofState': dict({ - 'roofState': 'CLOSED', - 'roofStateType': 'SUN_ROOF', - }), - 'windowsState': dict({ - 'combinedState': 'CLOSED', - 'leftFront': 'CLOSED', - 'rightFront': 'CLOSED', - }), - }), - }), - 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', - }), dict({ 'content': dict({ 'chargeAndClimateSettings': dict({ @@ -7391,20 +7287,31 @@ }), 'filename': 'mini-eadrax-vcs_v5_vehicle-list.json', }), + dict({ + 'content': dict({ + 'gcid': 'ceb64158-d2ca-47e9-9ee6-cbffb881434e', + 'mappingInfos': list([ + ]), + }), + 'filename': 'toyota-eadrax-vcs_v5_vehicle-list.json', + }), dict({ 'content': dict({ 'capabilities': dict({ 'a4aType': 'BLUETOOTH', - 'checkSustainabilityDPP': False, + 'alarmSystem': True, 'climateFunction': 'AIR_CONDITIONING', 'climateNow': True, 'digitalKey': dict({ 'bookedServicePackage': 'SMACC_2_UWB', + 'isDigitalKeyFirstSupported': False, 'readerGraphics': 'readerGraphics', 'state': 'ACTIVATED', + 'vehicleSoftwareUpgradeRequired': True, }), 'horn': True, 'inCarCamera': True, + 'inCarCameraDwa': True, 'isBmwChargingSupported': True, 'isCarSharingSupported': False, 'isChargeNowForBusinessSupported': True, @@ -7415,27 +7322,38 @@ 'isChargingPowerLimitEnabled': True, 'isChargingSettingsEnabled': True, 'isChargingTargetSocEnabled': True, + 'isClimateTimerSupported': False, 'isClimateTimerWeeklyActive': False, 'isCustomerEsimSupported': True, 'isDCSContractManagementSupported': True, 'isDataPrivacyEnabled': False, 'isEasyChargeEnabled': True, 'isEvGoChargingSupported': False, + 'isLocationBasedChargingSettingsSupported': False, 'isMiniChargingSupported': False, 'isNonLscFeatureEnabled': False, 'isPersonalPictureUploadSupported': False, - 'isRemoteEngineStartSupported': False, + 'isPlugAndChargeSupported': False, + 'isRemoteEngineStartEnabled': False, + 'isRemoteEngineStartSupported': True, 'isRemoteHistoryDeletionSupported': False, 'isRemoteHistorySupported': True, + 'isRemoteParkingEes25Active': False, 'isRemoteParkingSupported': False, 'isRemoteServicesActivationRequired': False, 'isRemoteServicesBookingRequired': False, 'isScanAndChargeSupported': True, 'isSustainabilityAccumulatedViewEnabled': False, 'isSustainabilitySupported': False, + 'isThirdPartyAppStoreSupported': False, 'isWifiHotspotServiceSupported': False, 'lastStateCallState': 'ACTIVATED', 'lights': True, + 'locationBasedCommerceFeatures': dict({ + 'fueling': False, + 'parking': False, + 'reservations': False, + }), 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ @@ -7450,11 +7368,45 @@ 'NOT_SUPPORTED', ]), }), + 'remoteServices': dict({ + 'doorLock': dict({ + 'id': 'doorLock', + 'state': 'ACTIVATED', + }), + 'doorUnlock': dict({ + 'id': 'doorUnlock', + 'state': 'ACTIVATED', + }), + 'hornBlow': dict({ + 'id': 'hornBlow', + 'state': 'ACTIVATED', + }), + 'inCarCamera': dict({ + 'id': 'inCarCamera', + 'state': 'ACTIVATED', + }), + 'inCarCameraDwa': dict({ + 'id': 'inCarCameraDwa', + 'state': 'ACTIVATED', + }), + 'lightFlash': dict({ + 'id': 'lightFlash', + 'state': 'ACTIVATED', + }), + 'remote360': dict({ + 'id': 'remote360', + 'state': 'ACTIVATED', + }), + 'surroundViewRecorder': dict({ + 'id': 'surroundViewRecorder', + 'state': 'ACTIVATED', + }), + }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, 'specialThemeSupport': list([ ]), - 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexa': True, 'speechThirdPartyAlexaSDK': False, 'surroundViewRecorder': True, 'unlock': True, @@ -7632,6 +7584,7 @@ 'roofState': 'CLOSED', 'roofStateType': 'SUN_ROOF', }), + 'securityOverviewMode': 'ARMED', 'tireState': dict({ 'frontLeft': dict({ 'details': dict({ @@ -7722,6 +7675,18 @@ }), }), }), + 'vehicleSoftwareVersion': dict({ + 'iStep': dict({ + 'iStep': 0, + 'month': 0, + 'seriesCluster': '', + 'year': 0, + }), + 'puStep': dict({ + 'month': 0, + 'year': 0, + }), + }), 'windowsState': dict({ 'combinedState': 'CLOSED', 'leftFront': 'CLOSED', @@ -7841,15 +7806,19 @@ 'content': dict({ 'capabilities': dict({ 'a4aType': 'NOT_SUPPORTED', - 'checkSustainabilityDPP': False, + 'alarmSystem': False, 'climateFunction': 'AIR_CONDITIONING', 'climateNow': True, 'digitalKey': dict({ 'bookedServicePackage': 'SMACC_1_5', + 'isDigitalKeyFirstSupported': False, 'readerGraphics': 'readerGraphics', 'state': 'ACTIVATED', + 'vehicleSoftwareUpgradeRequired': False, }), 'horn': True, + 'inCarCamera': False, + 'inCarCameraDwa': False, 'isBmwChargingSupported': True, 'isCarSharingSupported': False, 'isChargeNowForBusinessSupported': True, @@ -7860,37 +7829,80 @@ 'isChargingPowerLimitEnabled': True, 'isChargingSettingsEnabled': True, 'isChargingTargetSocEnabled': True, + 'isClimateTimerSupported': False, 'isClimateTimerWeeklyActive': False, - 'isCustomerEsimSupported': True, + 'isCustomerEsimSupported': False, 'isDCSContractManagementSupported': True, 'isDataPrivacyEnabled': False, 'isEasyChargeEnabled': True, 'isEvGoChargingSupported': False, + 'isLocationBasedChargingSettingsSupported': False, 'isMiniChargingSupported': False, 'isNonLscFeatureEnabled': False, 'isPersonalPictureUploadSupported': False, - 'isRemoteEngineStartSupported': False, + 'isPlugAndChargeSupported': False, + 'isRemoteEngineStartEnabled': False, + 'isRemoteEngineStartSupported': True, 'isRemoteHistoryDeletionSupported': False, 'isRemoteHistorySupported': True, + 'isRemoteParkingEes25Active': False, 'isRemoteParkingSupported': False, 'isRemoteServicesActivationRequired': False, 'isRemoteServicesBookingRequired': False, 'isScanAndChargeSupported': True, 'isSustainabilityAccumulatedViewEnabled': False, 'isSustainabilitySupported': False, + 'isThirdPartyAppStoreSupported': False, 'isWifiHotspotServiceSupported': False, 'lastStateCallState': 'ACTIVATED', 'lights': True, + 'locationBasedCommerceFeatures': dict({ + 'fueling': False, + 'parking': False, + 'reservations': False, + }), 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ }), + 'remoteServices': dict({ + 'doorLock': dict({ + 'id': 'doorLock', + 'state': 'ACTIVATED', + }), + 'doorUnlock': dict({ + 'id': 'doorUnlock', + 'state': 'ACTIVATED', + }), + 'hornBlow': dict({ + 'id': 'hornBlow', + 'state': 'ACTIVATED', + }), + 'inCarCamera': dict({ + 'id': 'inCarCamera', + }), + 'inCarCameraDwa': dict({ + 'id': 'inCarCameraDwa', + }), + 'lightFlash': dict({ + 'id': 'lightFlash', + 'state': 'ACTIVATED', + }), + 'remote360': dict({ + 'id': 'remote360', + 'state': 'ACTIVATED', + }), + 'surroundViewRecorder': dict({ + 'id': 'surroundViewRecorder', + }), + }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, 'specialThemeSupport': list([ ]), - 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexa': True, 'speechThirdPartyAlexaSDK': False, + 'surroundViewRecorder': False, 'unlock': True, 'vehicleFinder': True, 'vehicleStateSource': 'LAST_STATE_CALL', @@ -8062,6 +8074,7 @@ 'type': 'TIRE_WEAR_FRONT', }), ]), + 'securityOverviewMode': 'NOT_ARMED', 'tireState': dict({ 'frontLeft': dict({ 'details': dict({ @@ -8152,6 +8165,18 @@ }), }), }), + 'vehicleSoftwareVersion': dict({ + 'iStep': dict({ + 'iStep': 0, + 'month': 0, + 'seriesCluster': '', + 'year': 0, + }), + 'puStep': dict({ + 'month': 0, + 'year': 0, + }), + }), 'windowsState': dict({ 'combinedState': 'CLOSED', 'leftFront': 'CLOSED', @@ -8271,16 +8296,20 @@ 'content': dict({ 'capabilities': dict({ 'a4aType': 'NOT_SUPPORTED', - 'checkSustainabilityDPP': False, + 'alarmSystem': False, 'climateFunction': 'VENTILATION', 'climateNow': True, 'climateTimerTrigger': 'DEPARTURE_TIMER', 'digitalKey': dict({ 'bookedServicePackage': 'SMACC_1_5', + 'isDigitalKeyFirstSupported': False, 'readerGraphics': 'readerGraphics', 'state': 'ACTIVATED', + 'vehicleSoftwareUpgradeRequired': False, }), 'horn': True, + 'inCarCamera': False, + 'inCarCameraDwa': False, 'isBmwChargingSupported': False, 'isCarSharingSupported': False, 'isChargeNowForBusinessSupported': False, @@ -8298,31 +8327,73 @@ 'isDataPrivacyEnabled': False, 'isEasyChargeEnabled': False, 'isEvGoChargingSupported': False, + 'isLocationBasedChargingSettingsSupported': False, 'isMiniChargingSupported': False, 'isNonLscFeatureEnabled': False, 'isPersonalPictureUploadSupported': False, - 'isRemoteEngineStartSupported': False, + 'isPlugAndChargeSupported': False, + 'isRemoteEngineStartEnabled': False, + 'isRemoteEngineStartSupported': True, 'isRemoteHistoryDeletionSupported': False, 'isRemoteHistorySupported': True, + 'isRemoteParkingEes25Active': False, 'isRemoteParkingSupported': False, 'isRemoteServicesActivationRequired': False, 'isRemoteServicesBookingRequired': False, 'isScanAndChargeSupported': False, 'isSustainabilityAccumulatedViewEnabled': False, 'isSustainabilitySupported': False, - 'isWifiHotspotServiceSupported': True, + 'isThirdPartyAppStoreSupported': False, + 'isWifiHotspotServiceSupported': False, 'lastStateCallState': 'ACTIVATED', 'lights': True, + 'locationBasedCommerceFeatures': dict({ + 'fueling': False, + 'parking': False, + 'reservations': False, + }), 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ }), + 'remoteServices': dict({ + 'doorLock': dict({ + 'id': 'doorLock', + 'state': 'ACTIVATED', + }), + 'doorUnlock': dict({ + 'id': 'doorUnlock', + 'state': 'ACTIVATED', + }), + 'hornBlow': dict({ + 'id': 'hornBlow', + 'state': 'ACTIVATED', + }), + 'inCarCamera': dict({ + 'id': 'inCarCamera', + }), + 'inCarCameraDwa': dict({ + 'id': 'inCarCameraDwa', + }), + 'lightFlash': dict({ + 'id': 'lightFlash', + 'state': 'ACTIVATED', + }), + 'remote360': dict({ + 'id': 'remote360', + 'state': 'ACTIVATED', + }), + 'surroundViewRecorder': dict({ + 'id': 'surroundViewRecorder', + }), + }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, 'specialThemeSupport': list([ ]), - 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexa': True, 'speechThirdPartyAlexaSDK': False, + 'surroundViewRecorder': False, 'unlock': True, 'vehicleFinder': True, 'vehicleStateSource': 'LAST_STATE_CALL', @@ -8456,6 +8527,7 @@ 'type': 'TIRE_WEAR_FRONT', }), ]), + 'securityOverviewMode': None, 'tireState': dict({ 'frontLeft': dict({ 'details': dict({ @@ -8542,6 +8614,18 @@ }), }), }), + 'vehicleSoftwareVersion': dict({ + 'iStep': dict({ + 'iStep': 0, + 'month': 0, + 'seriesCluster': '', + 'year': 0, + }), + 'puStep': dict({ + 'month': 0, + 'year': 0, + }), + }), 'windowsState': dict({ 'combinedState': 'CLOSED', 'leftFront': 'CLOSED', diff --git a/tests/components/bmw_connected_drive/snapshots/test_select.ambr b/tests/components/bmw_connected_drive/snapshots/test_select.ambr index dac776aa49b..b827dfe478a 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_select.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_select.ambr @@ -8,6 +8,7 @@ 'options': list([ 'immediate_charging', 'delayed_charging', + 'no_action', ]), }), 'config_entry_id': , @@ -44,6 +45,7 @@ 'options': list([ 'immediate_charging', 'delayed_charging', + 'no_action', ]), }), 'context': , @@ -141,6 +143,7 @@ 'options': list([ 'immediate_charging', 'delayed_charging', + 'no_action', ]), }), 'config_entry_id': , @@ -177,6 +180,7 @@ 'options': list([ 'immediate_charging', 'delayed_charging', + 'no_action', ]), }), 'context': , @@ -274,6 +278,7 @@ 'options': list([ 'immediate_charging', 'delayed_charging', + 'no_action', ]), }), 'config_entry_id': , @@ -310,6 +315,7 @@ 'options': list([ 'immediate_charging', 'delayed_charging', + 'no_action', ]), }), 'context': , From 85aca4f09566269fb5fcc05ae6fc56384e352437 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 29 Jul 2024 12:03:40 +0200 Subject: [PATCH 1651/2411] Fix default turn_on without explicit preset or percentage in Matter Fan platform (#122591) --- homeassistant/components/matter/fan.py | 32 +- homeassistant/components/matter/switch.py | 1 + tests/components/matter/common.py | 3 + .../components/matter/fixtures/nodes/fan.json | 340 ++++++++++++++++++ tests/components/matter/test_fan.py | 203 +++++++++-- 5 files changed, 550 insertions(+), 29 deletions(-) create mode 100644 tests/components/matter/fixtures/nodes/fan.json diff --git a/homeassistant/components/matter/fan.py b/homeassistant/components/matter/fan.py index a88c297d31a..8e5ef617304 100644 --- a/homeassistant/components/matter/fan.py +++ b/homeassistant/components/matter/fan.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any from chip.clusters import Objects as clusters from matter_server.common.helpers.util import create_attribute_path_from_attribute @@ -57,6 +57,7 @@ class MatterFan(MatterEntity, FanEntity): """Representation of a Matter fan.""" _last_known_preset_mode: str | None = None + _last_known_percentage: int = 0 _enable_turn_on_off_backwards_compatibility = False async def async_turn_on( @@ -66,14 +67,27 @@ class MatterFan(MatterEntity, FanEntity): **kwargs: Any, ) -> None: """Turn on the fan.""" + if percentage is None and preset_mode is None: + # turn_on without explicit percentage or preset_mode given + # try to handle this with the last known value + if self._last_known_percentage != 0: + percentage = self._last_known_percentage + elif self._last_known_preset_mode is not None: + preset_mode = self._last_known_preset_mode + elif self._attr_preset_modes: + # fallback: default to first supported preset + preset_mode = self._attr_preset_modes[0] + else: + # this really should not be possible but handle it anyways + percentage = 50 + + # prefer setting fan speed by percentage if percentage is not None: - # handle setting fan speed by percentage await self.async_set_percentage(percentage) return # handle setting fan mode by preset - if preset_mode is None: - # no preset given, try to handle this with the last known value - preset_mode = self._last_known_preset_mode or PRESET_AUTO + if TYPE_CHECKING: + assert preset_mode is not None await self.async_set_preset_mode(preset_mode) async def async_turn_off(self, **kwargs: Any) -> None: @@ -236,6 +250,8 @@ class MatterFan(MatterEntity, FanEntity): # keep track of the last known mode for turn_on commands without preset if self._attr_preset_mode is not None: self._last_known_preset_mode = self._attr_preset_mode + if current_percent: + self._last_known_percentage = current_percent @callback def _calculate_features( @@ -276,8 +292,10 @@ class MatterFan(MatterEntity, FanEntity): preset_modes = [PRESET_LOW, PRESET_MEDIUM, PRESET_HIGH] elif fan_mode_seq == FanModeSequenceEnum.kOffLowMedHighAuto: preset_modes = [PRESET_LOW, PRESET_MEDIUM, PRESET_HIGH, PRESET_AUTO] - elif fan_mode_seq == FanModeSequenceEnum.kOffOnAuto: - preset_modes = [PRESET_AUTO] + elif fan_mode_seq == FanModeSequenceEnum.kOffHighAuto: + preset_modes = [PRESET_HIGH, PRESET_AUTO] + elif fan_mode_seq == FanModeSequenceEnum.kOffHigh: + preset_modes = [PRESET_HIGH] # treat Matter Wind feature as additional preset(s) if feature_map & FanControlFeature.kWind: wind_support = int( diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 2fb325b8808..953897fdaa6 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -124,6 +124,7 @@ DISCOVERY_SCHEMAS = [ device_types.Cooktop, device_types.Dishwasher, device_types.ExtractorHood, + device_types.Fan, device_types.HeatingCoolingUnit, device_types.LaundryDryer, device_types.LaundryWasher, diff --git a/tests/components/matter/common.py b/tests/components/matter/common.py index 7878ac564fd..541f7383f1d 100644 --- a/tests/components/matter/common.py +++ b/tests/components/matter/common.py @@ -31,9 +31,12 @@ async def setup_integration_with_node_fixture( hass: HomeAssistant, node_fixture: str, client: MagicMock, + override_attributes: dict[str, Any] | None = None, ) -> MatterNode: """Set up Matter integration with fixture as node.""" node_data = load_and_parse_node_fixture(node_fixture) + if override_attributes: + node_data["attributes"].update(override_attributes) node = MatterNode( dataclass_from_dict( MatterNodeData, diff --git a/tests/components/matter/fixtures/nodes/fan.json b/tests/components/matter/fixtures/nodes/fan.json new file mode 100644 index 00000000000..e33c29ce66d --- /dev/null +++ b/tests/components/matter/fixtures/nodes/fan.json @@ -0,0 +1,340 @@ +{ + "node_id": 29, + "date_commissioned": "2024-07-25T08:34:23.014310", + "last_interview": "2024-07-25T08:34:23.014315", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 18, + "1": 1 + }, + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 42, 48, 49, 51, 53, 60, 62, 63, 64], + "0/29/2": [41], + "0/29/3": [1, 2, 3, 4, 5, 6], + "0/29/65532": 0, + "0/29/65533": 2, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65530": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 5 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65530": [0, 1], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65530, 65531, 65532, 65533], + "0/40/0": 17, + "0/40/1": "Mock", + "0/40/2": 4961, + "0/40/3": "Fan", + "0/40/4": 2, + "0/40/5": "Mocked Fan Switch", + "0/40/6": "**REDACTED**", + "0/40/7": 1, + "0/40/8": "1.0", + "0/40/9": 4, + "0/40/10": "0.0.1", + "0/40/11": "", + "0/40/12": "", + "0/40/13": "", + "0/40/14": "", + "0/40/15": "", + "0/40/16": false, + "0/40/17": true, + "0/40/18": "", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/65532": 0, + "0/40/65533": 2, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65530": [0], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 65528, 65529, 65530, 65531, 65532, 65533 + ], + "0/42/0": [], + "0/42/1": true, + "0/42/2": 1, + "0/42/3": null, + "0/42/65532": 0, + "0/42/65533": 1, + "0/42/65528": [], + "0/42/65529": [0], + "0/42/65530": [0, 1, 2], + "0/42/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65530": [], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65530, 65531, 65532, 65533], + "0/49/0": 1, + "0/49/1": [ + { + "0": "J/YquJb4Ao4=", + "1": true + } + ], + "0/49/2": 10, + "0/49/3": 20, + "0/49/4": true, + "0/49/5": 0, + "0/49/6": "J/YquJb4Ao4=", + "0/49/7": null, + "0/49/65532": 2, + "0/49/65533": 1, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 3, 4, 6, 8], + "0/49/65530": [], + "0/49/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "0/51/0": [], + "0/51/1": 15, + "0/51/2": 5688, + "0/51/3": 1, + "0/51/4": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 1, + "0/51/65528": [], + "0/51/65529": [0], + "0/51/65530": [3], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "0/53/0": 25, + "0/53/1": 5, + "0/53/2": "ha-thread", + "0/53/3": 12768, + "0/53/4": 5924944741529093989, + "0/53/5": "", + "0/53/6": 0, + "0/53/7": [], + "0/53/8": [], + "0/53/9": 933034070, + "0/53/10": 68, + "0/53/11": 16, + "0/53/12": 151, + "0/53/13": 31, + "0/53/14": 1, + "0/53/15": 0, + "0/53/16": 1, + "0/53/17": 0, + "0/53/18": 0, + "0/53/19": 1, + "0/53/20": 0, + "0/53/21": 0, + "0/53/22": 3533, + "0/53/23": 3105, + "0/53/24": 428, + "0/53/25": 1889, + "0/53/26": 1879, + "0/53/27": 1644, + "0/53/28": 2317, + "0/53/29": 0, + "0/53/30": 1216, + "0/53/31": 0, + "0/53/32": 0, + "0/53/33": 534, + "0/53/34": 10, + "0/53/35": 0, + "0/53/36": 42, + "0/53/37": 0, + "0/53/38": 0, + "0/53/39": 18130, + "0/53/40": 12178, + "0/53/41": 5863, + "0/53/42": 5103, + "0/53/43": 0, + "0/53/44": 11639, + "0/53/45": 1216, + "0/53/46": 0, + "0/53/47": 0, + "0/53/48": 0, + "0/53/49": 14, + "0/53/50": 0, + "0/53/51": 89, + "0/53/52": 0, + "0/53/53": 69, + "0/53/54": 0, + "0/53/55": 0, + "0/53/56": 131072, + "0/53/57": 0, + "0/53/58": 0, + "0/53/59": { + "0": 672, + "1": 8335 + }, + "0/53/60": "AB//4A==", + "0/53/61": { + "0": true, + "1": false, + "2": true, + "3": true, + "4": true, + "5": true, + "6": false, + "7": true, + "8": true, + "9": true, + "10": true, + "11": true + }, + "0/53/62": [0, 0, 0, 0], + "0/53/65532": 15, + "0/53/65533": 1, + "0/53/65528": [], + "0/53/65529": [0], + "0/53/65530": [], + "0/53/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 0, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65530": [], + "0/60/65531": [0, 1, 2, 65528, 65529, 65530, 65531, 65532, 65533], + "0/62/0": [], + "0/62/1": [], + "0/62/2": 5, + "0/62/3": 4, + "0/62/4": [], + "0/62/5": 5, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65530": [], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65530, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 4, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 2, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65530": [], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "0/64/0": [ + { + "0": "Vendor", + "1": "Mocked" + }, + { + "0": "Product", + "1": "Fan" + } + ], + "0/64/65532": 0, + "0/64/65533": 1, + "0/64/65528": [], + "0/64/65529": [], + "0/64/65530": [], + "0/64/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 2, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65530": [], + "1/3/65531": [0, 1, 65528, 65529, 65530, 65531, 65532, 65533], + "1/4/0": 128, + "1/4/65532": 1, + "1/4/65533": 4, + "1/4/65528": [0, 1, 2, 3], + "1/4/65529": [0, 1, 2, 3, 4, 5], + "1/4/65530": [], + "1/4/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "1/29/0": [ + { + "0": 43, + "1": 1 + } + ], + "1/29/1": [3, 4, 6, 8, 29, 64, 80, 514, 305134641], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 2, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65530": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "1/64/0": [ + { + "0": "DeviceType", + "1": "Fan" + } + ], + "1/64/65532": 0, + "1/64/65533": 1, + "1/64/65528": [], + "1/64/65529": [], + "1/64/65530": [], + "1/64/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + + "1/514/0": 8, + "1/514/1": 2, + "1/514/2": 0, + "1/514/3": 0, + "1/514/4": 3, + "1/514/5": 0, + "1/514/6": 0, + "1/514/9": 3, + "1/514/10": 0, + "1/514/65532": 25, + "1/514/65533": 4, + "1/514/65528": [], + "1/514/65529": [0], + "1/514/65530": [], + "1/514/65531": [ + 0, 1, 2, 3, 4, 5, 6, 9, 10, 65528, 65529, 65530, 65531, 65532, 65533 + ] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/test_fan.py b/tests/components/matter/test_fan.py index 7e964d672ca..18c2c2ed255 100644 --- a/tests/components/matter/test_fan.py +++ b/tests/components/matter/test_fan.py @@ -1,5 +1,6 @@ """Test Matter Fan platform.""" +from typing import Any from unittest.mock import MagicMock, call from matter_server.client.models.node import MatterNode @@ -27,6 +28,14 @@ from .common import ( ) +@pytest.fixture(name="fan_node") +async def simple_fan_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a Fan node.""" + return await setup_integration_with_node_fixture(hass, "fan", matter_client) + + @pytest.fixture(name="air_purifier") async def air_purifier_fixture( hass: HomeAssistant, matter_client: MagicMock @@ -100,6 +109,7 @@ async def test_fan_base( assert state.attributes["percentage"] == 0 +@pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_fan_turn_on_with_percentage( hass: HomeAssistant, matter_client: MagicMock, @@ -119,15 +129,31 @@ async def test_fan_turn_on_with_percentage( attribute_path="1/514/2", value=50, ) + # test again where preset_mode is omitted in the service call + # which should select the last active percentage + matter_client.write_attribute.reset_mock() + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert matter_client.write_attribute.call_count == 1 + assert matter_client.write_attribute.call_args == call( + node_id=air_purifier.node_id, + attribute_path="1/514/2", + value=255, + ) +@pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_fan_turn_on_with_preset_mode( hass: HomeAssistant, matter_client: MagicMock, - air_purifier: MatterNode, + fan_node: MatterNode, ) -> None: """Test turning on the fan with a specific preset mode.""" - entity_id = "fan.air_purifier_fan" + entity_id = "fan.mocked_fan_switch_fan" await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, @@ -136,7 +162,7 @@ async def test_fan_turn_on_with_preset_mode( ) assert matter_client.write_attribute.call_count == 1 assert matter_client.write_attribute.call_args == call( - node_id=air_purifier.node_id, + node_id=fan_node.node_id, attribute_path="1/514/0", value=2, ) @@ -151,28 +177,13 @@ async def test_fan_turn_on_with_preset_mode( ) assert matter_client.write_attribute.call_count == 1 assert matter_client.write_attribute.call_args == call( - node_id=air_purifier.node_id, + node_id=fan_node.node_id, attribute_path="1/514/10", value=value, ) - # test again where preset_mode is omitted in the service call - # which should select a default preset mode - matter_client.write_attribute.reset_mock() - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - assert matter_client.write_attribute.call_count == 1 - assert matter_client.write_attribute.call_args == call( - node_id=air_purifier.node_id, - attribute_path="1/514/0", - value=5, - ) # test again if wind mode is explicitly turned off when we set a new preset mode matter_client.write_attribute.reset_mock() - set_node_attribute(air_purifier, 1, 514, 10, 2) + set_node_attribute(fan_node, 1, 514, 10, 2) await trigger_subscription_callback(hass, matter_client) await hass.services.async_call( FAN_DOMAIN, @@ -182,15 +193,33 @@ async def test_fan_turn_on_with_preset_mode( ) assert matter_client.write_attribute.call_count == 2 assert matter_client.write_attribute.call_args_list[0] == call( - node_id=air_purifier.node_id, + node_id=fan_node.node_id, attribute_path="1/514/10", value=0, ) assert matter_client.write_attribute.call_args == call( - node_id=air_purifier.node_id, + node_id=fan_node.node_id, attribute_path="1/514/0", value=2, ) + # test again where preset_mode is omitted in the service call + # which should select the last active preset + matter_client.write_attribute.reset_mock() + set_node_attribute(fan_node, 1, 514, 0, 1) + set_node_attribute(fan_node, 1, 514, 10, 0) + await trigger_subscription_callback(hass, matter_client) + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert matter_client.write_attribute.call_count == 1 + assert matter_client.write_attribute.call_args == call( + node_id=fan_node.node_id, + attribute_path="1/514/0", + value=1, + ) async def test_fan_turn_off( @@ -279,3 +308,133 @@ async def test_fan_set_direction( value=value, ) matter_client.write_attribute.reset_mock() + + +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize( + ("fixture", "entity_id", "attributes", "features"), + [ + ( + "fan", + "fan.mocked_fan_switch_fan", + { + "1/514/65532": 0, + }, + (FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF), + ), + ( + "fan", + "fan.mocked_fan_switch_fan", + { + "1/514/65532": 1, + }, + ( + FanEntityFeature.TURN_ON + | FanEntityFeature.TURN_OFF + | FanEntityFeature.SET_SPEED + ), + ), + ( + "fan", + "fan.mocked_fan_switch_fan", + { + "1/514/65532": 4, + }, + ( + FanEntityFeature.TURN_ON + | FanEntityFeature.TURN_OFF + | FanEntityFeature.OSCILLATE + ), + ), + ( + "fan", + "fan.mocked_fan_switch_fan", + { + "1/514/65532": 36, + }, + ( + FanEntityFeature.TURN_ON + | FanEntityFeature.TURN_OFF + | FanEntityFeature.OSCILLATE + | FanEntityFeature.DIRECTION + ), + ), + ], +) +async def test_fan_supported_features( + hass: HomeAssistant, + matter_client: MagicMock, + fixture: str, + entity_id: str, + attributes: dict[str, Any], + features: int, +) -> None: + """Test if the correct features get discovered from featuremap.""" + await setup_integration_with_node_fixture(hass, fixture, matter_client, attributes) + state = hass.states.get(entity_id) + assert state + assert state.attributes["supported_features"] & features == features + + +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize( + ("fixture", "entity_id", "attributes", "preset_modes"), + [ + ( + "fan", + "fan.mocked_fan_switch_fan", + {"1/514/1": 0, "1/514/65532": 0}, + [ + "low", + "medium", + "high", + ], + ), + ( + "fan", + "fan.mocked_fan_switch_fan", + {"1/514/1": 1, "1/514/65532": 0}, + [ + "low", + "high", + ], + ), + ( + "fan", + "fan.mocked_fan_switch_fan", + {"1/514/1": 2, "1/514/65532": 0}, + ["low", "medium", "high", "auto"], + ), + ( + "fan", + "fan.mocked_fan_switch_fan", + {"1/514/1": 4, "1/514/65532": 0}, + ["high", "auto"], + ), + ( + "fan", + "fan.mocked_fan_switch_fan", + {"1/514/1": 5, "1/514/65532": 0}, + ["high"], + ), + ( + "fan", + "fan.mocked_fan_switch_fan", + {"1/514/1": 5, "1/514/65532": 8, "1/514/9": 3}, + ["high", "natural_wind", "sleep_wind"], + ), + ], +) +async def test_fan_features( + hass: HomeAssistant, + matter_client: MagicMock, + fixture: str, + entity_id: str, + attributes: dict[str, Any], + preset_modes: list[str], +) -> None: + """Test if the correct presets get discovered from fanmodesequence.""" + await setup_integration_with_node_fixture(hass, fixture, matter_client, attributes) + state = hass.states.get(entity_id) + assert state + assert state.attributes["preset_modes"] == preset_modes From e5bb1b2cc6f9ec601cbc84a6212d1f5044be8ef7 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 29 Jul 2024 03:04:23 -0700 Subject: [PATCH 1652/2411] Update LLM prompt to improve quality for local LLMs (#122746) --- homeassistant/helpers/llm.py | 6 +- tests/helpers/test_llm.py | 115 +++++++++++++++-------------------- 2 files changed, 52 insertions(+), 69 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 177e3735bc0..4c8e2df06a4 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -324,8 +324,7 @@ class AssistAPI(API): ( "When controlling Home Assistant always call the intent tools. " "Use HassTurnOn to lock and HassTurnOff to unlock a lock. " - "When controlling a device, prefer passing just its name and its domain " - "(what comes before the dot in its entity id). " + "When controlling a device, prefer passing just name and domain. " "When controlling an area, prefer passing just area name and domain." ) ] @@ -363,7 +362,7 @@ class AssistAPI(API): prompt.append( "An overview of the areas and the devices in this smart home:" ) - prompt.append(yaml.dump(exposed_entities)) + prompt.append(yaml.dump(list(exposed_entities.values()))) return "\n".join(prompt) @@ -477,6 +476,7 @@ def _get_exposed_entities( info: dict[str, Any] = { "names": ", ".join(names), + "domain": state.domain, "state": state.state, } diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index e1f55942d10..3ad5b23b731 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -22,7 +22,6 @@ from homeassistant.helpers import ( selector, ) from homeassistant.setup import async_setup_component -from homeassistant.util import yaml from tests.common import MockConfigEntry @@ -506,74 +505,58 @@ async def test_assist_api_prompt( suggested_area="Test Area 2", ) ) - - exposed_entities = llm._get_exposed_entities(hass, llm_context.assistant) - assert exposed_entities == { - "light.1": { - "areas": "Test Area 2", - "names": "1", - "state": "unavailable", - }, - entry1.entity_id: { - "names": "Kitchen", - "state": "on", - "attributes": {"temperature": "0.9", "humidity": "65"}, - }, - entry2.entity_id: { - "areas": "Test Area, Alternative name", - "names": "Living Room", - "state": "on", - }, - "light.test_device": { - "areas": "Test Area, Alternative name", - "names": "Test Device", - "state": "unavailable", - }, - "light.test_device_2": { - "areas": "Test Area 2", - "names": "Test Device 2", - "state": "unavailable", - }, - "light.test_device_3": { - "areas": "Test Area 2", - "names": "Test Device 3", - "state": "unavailable", - }, - "light.test_device_4": { - "areas": "Test Area 2", - "names": "Test Device 4", - "state": "unavailable", - }, - "light.test_service": { - "areas": "Test Area, Alternative name", - "names": "Test Service", - "state": "unavailable", - }, - "light.test_service_2": { - "areas": "Test Area, Alternative name", - "names": "Test Service", - "state": "unavailable", - }, - "light.test_service_3": { - "areas": "Test Area, Alternative name", - "names": "Test Service", - "state": "unavailable", - }, - "light.unnamed_device": { - "areas": "Test Area 2", - "names": "Unnamed Device", - "state": "unavailable", - }, - } - exposed_entities_prompt = ( - "An overview of the areas and the devices in this smart home:\n" - + yaml.dump(exposed_entities) - ) + exposed_entities_prompt = """An overview of the areas and the devices in this smart home: +- names: Kitchen + domain: light + state: 'on' + attributes: + temperature: '0.9' + humidity: '65' +- names: Living Room + domain: light + state: 'on' + areas: Test Area, Alternative name +- names: Test Device + domain: light + state: unavailable + areas: Test Area, Alternative name +- names: Test Service + domain: light + state: unavailable + areas: Test Area, Alternative name +- names: Test Service + domain: light + state: unavailable + areas: Test Area, Alternative name +- names: Test Service + domain: light + state: unavailable + areas: Test Area, Alternative name +- names: Test Device 2 + domain: light + state: unavailable + areas: Test Area 2 +- names: Test Device 3 + domain: light + state: unavailable + areas: Test Area 2 +- names: Test Device 4 + domain: light + state: unavailable + areas: Test Area 2 +- names: Unnamed Device + domain: light + state: unavailable + areas: Test Area 2 +- names: '1' + domain: light + state: unavailable + areas: Test Area 2 +""" first_part_prompt = ( "When controlling Home Assistant always call the intent tools. " "Use HassTurnOn to lock and HassTurnOff to unlock a lock. " - "When controlling a device, prefer passing just its name and its domain " - "(what comes before the dot in its entity id). " + "When controlling a device, prefer passing just name and domain. " "When controlling an area, prefer passing just area name and domain." ) no_timer_prompt = "This device is not able to start timers." From 075550b7bafb32dde739ee07543df5a2c87d5d63 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 12:51:12 +0200 Subject: [PATCH 1653/2411] Use HOMEASSISTANT_DOMAIN alias for core DOMAIN in tests (#122762) --- .../bmw_connected_drive/test_coordinator.py | 5 +-- .../conversation/test_default_agent.py | 9 +++-- .../components/feedreader/test_config_flow.py | 6 ++-- .../components/google_assistant/test_trait.py | 6 ++-- .../components/homeassistant/test_repairs.py | 14 +++++--- tests/components/reolink/test_init.py | 6 ++-- tests/test_config.py | 36 +++++++++++-------- tests/test_config_entries.py | 19 ++++++---- tests/test_setup.py | 11 ++++-- 9 files changed, 71 insertions(+), 41 deletions(-) diff --git a/tests/components/bmw_connected_drive/test_coordinator.py b/tests/components/bmw_connected_drive/test_coordinator.py index ca629084f6c..b0f507bbfc2 100644 --- a/tests/components/bmw_connected_drive/test_coordinator.py +++ b/tests/components/bmw_connected_drive/test_coordinator.py @@ -8,7 +8,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN -from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.update_coordinator import UpdateFailed @@ -118,6 +118,7 @@ async def test_init_reauth( await hass.async_block_till_done() reauth_issue = issue_registry.async_get_issue( - HA_DOMAIN, f"config_entry_reauth_{BMW_DOMAIN}_{config_entry.entry_id}" + HOMEASSISTANT_DOMAIN, + f"config_entry_reauth_{BMW_DOMAIN}_{config_entry.entry_id}", ) assert reauth_issue.active is True diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 10a81a024ca..315b73bacfd 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -32,7 +32,12 @@ from homeassistant.const import ( STATE_UNKNOWN, EntityCategory, ) -from homeassistant.core import DOMAIN as HASS_DOMAIN, Context, HomeAssistant, callback +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + Context, + HomeAssistant, + callback, +) from homeassistant.helpers import ( area_registry as ar, device_registry as dr, @@ -93,7 +98,7 @@ async def test_hidden_entities_skipped( "light", "demo", "1234", suggested_object_id="Test light", **er_kwargs ) hass.states.async_set("light.test_light", "off") - calls = async_mock_service(hass, HASS_DOMAIN, "turn_on") + calls = async_mock_service(hass, HOMEASSISTANT_DOMAIN, "turn_on") result = await conversation.async_converse( hass, "turn on test light", None, Context(), None ) diff --git a/tests/components/feedreader/test_config_flow.py b/tests/components/feedreader/test_config_flow.py index 669ca665f6b..47bccce902f 100644 --- a/tests/components/feedreader/test_config_flow.py +++ b/tests/components/feedreader/test_config_flow.py @@ -13,7 +13,7 @@ from homeassistant.components.feedreader.const import ( ) from homeassistant.config_entries import SOURCE_RECONFIGURE, SOURCE_USER from homeassistant.const import CONF_URL -from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component @@ -128,7 +128,9 @@ async def test_import( assert config_entries[0].data == expected_data assert config_entries[0].options == expected_options - assert issue_registry.async_get_issue(HA_DOMAIN, "deprecated_yaml_feedreader") + assert issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, "deprecated_yaml_feedreader" + ) async def test_import_errors( diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 5308b5608ea..54aa4035670 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -76,7 +76,7 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, State +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, State from homeassistant.util import color, dt as dt_util from homeassistant.util.unit_conversion import TemperatureConverter @@ -186,12 +186,12 @@ async def test_onoff_group(hass: HomeAssistant) -> None: assert trt_off.query_attributes() == {"on": False} - on_calls = async_mock_service(hass, HA_DOMAIN, SERVICE_TURN_ON) + on_calls = async_mock_service(hass, HOMEASSISTANT_DOMAIN, SERVICE_TURN_ON) await trt_on.execute(trait.COMMAND_ONOFF, BASIC_DATA, {"on": True}, {}) assert len(on_calls) == 1 assert on_calls[0].data == {ATTR_ENTITY_ID: "group.bla"} - off_calls = async_mock_service(hass, HA_DOMAIN, SERVICE_TURN_OFF) + off_calls = async_mock_service(hass, HOMEASSISTANT_DOMAIN, SERVICE_TURN_OFF) await trt_on.execute(trait.COMMAND_ONOFF, BASIC_DATA, {"on": False}, {}) assert len(off_calls) == 1 assert off_calls[0].data == {ATTR_ENTITY_ID: "group.bla"} diff --git a/tests/components/homeassistant/test_repairs.py b/tests/components/homeassistant/test_repairs.py index 968330de0fc..c7a1b3e762e 100644 --- a/tests/components/homeassistant/test_repairs.py +++ b/tests/components/homeassistant/test_repairs.py @@ -10,7 +10,7 @@ from homeassistant.components.repairs.websocket_api import ( RepairsFlowIndexView, RepairsFlowResourceView, ) -from homeassistant.core import DOMAIN, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -23,7 +23,7 @@ async def test_integration_not_found_confirm_step( hass_ws_client: WebSocketGenerator, ) -> None: """Test the integration_not_found issue confirm step.""" - assert await async_setup_component(hass, DOMAIN, {}) + assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) await hass.async_block_till_done() assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) await hass.async_block_till_done() @@ -49,7 +49,9 @@ async def test_integration_not_found_confirm_step( assert issue["translation_placeholders"] == {"domain": "test1"} url = RepairsFlowIndexView.url - resp = await http_client.post(url, json={"handler": DOMAIN, "issue_id": issue_id}) + resp = await http_client.post( + url, json={"handler": HOMEASSISTANT_DOMAIN, "issue_id": issue_id} + ) assert resp.status == HTTPStatus.OK data = await resp.json() @@ -93,7 +95,7 @@ async def test_integration_not_found_ignore_step( hass_ws_client: WebSocketGenerator, ) -> None: """Test the integration_not_found issue ignore step.""" - assert await async_setup_component(hass, DOMAIN, {}) + assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) await hass.async_block_till_done() assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) await hass.async_block_till_done() @@ -117,7 +119,9 @@ async def test_integration_not_found_ignore_step( assert issue["translation_placeholders"] == {"domain": "test1"} url = RepairsFlowIndexView.url - resp = await http_client.post(url, json={"handler": DOMAIN, "issue_id": issue_id}) + resp = await http_client.post( + url, json={"handler": HOMEASSISTANT_DOMAIN, "issue_id": issue_id} + ) assert resp.status == HTTPStatus.OK data = await resp.json() diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index f70fd312051..85ce5d94657 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -17,7 +17,7 @@ from homeassistant.components.reolink import ( from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE, Platform -from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import ( device_registry as dr, entity_registry as er, @@ -143,13 +143,13 @@ async def test_credential_error_three( issue_id = f"config_entry_reauth_{const.DOMAIN}_{config_entry.entry_id}" for _ in range(NUM_CRED_ERRORS): - assert (HA_DOMAIN, issue_id) not in issue_registry.issues + assert (HOMEASSISTANT_DOMAIN, issue_id) not in issue_registry.issues async_fire_time_changed( hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30) ) await hass.async_block_till_done() - assert (HA_DOMAIN, issue_id) in issue_registry.issues + assert (HOMEASSISTANT_DOMAIN, issue_id) in issue_registry.issues async def test_entry_reloading( diff --git a/tests/test_config.py b/tests/test_config.py index 9ea227767db..c7039cabe8b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -32,7 +32,11 @@ from homeassistant.const import ( CONF_PACKAGES, __version__, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, ConfigSource, HomeAssistant +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + ConfigSource, + HomeAssistant, +) from homeassistant.exceptions import ConfigValidationError, HomeAssistantError from homeassistant.helpers import ( check_config, @@ -1066,7 +1070,9 @@ async def test_check_ha_config_file_wrong(mock_check, hass: HomeAssistant) -> No "hass_config", [ { - HA_DOMAIN: {CONF_PACKAGES: {"pack_dict": {"input_boolean": {"ib1": None}}}}, + HOMEASSISTANT_DOMAIN: { + CONF_PACKAGES: {"pack_dict": {"input_boolean": {"ib1": None}}} + }, "input_boolean": {"ib2": None}, "light": {"platform": "test"}, } @@ -1080,7 +1086,7 @@ async def test_async_hass_config_yaml_merge( conf = await config_util.async_hass_config_yaml(hass) assert merge_log_err.call_count == 0 - assert conf[HA_DOMAIN].get(CONF_PACKAGES) is not None + assert conf[HOMEASSISTANT_DOMAIN].get(CONF_PACKAGES) is not None assert len(conf) == 3 assert len(conf["input_boolean"]) == 2 assert len(conf["light"]) == 1 @@ -1108,7 +1114,7 @@ async def test_merge(merge_log_err: MagicMock, hass: HomeAssistant) -> None: }, } config = { - HA_DOMAIN: {CONF_PACKAGES: packages}, + HOMEASSISTANT_DOMAIN: {CONF_PACKAGES: packages}, "input_boolean": {"ib2": None}, "light": {"platform": "test"}, "automation": [], @@ -1135,7 +1141,7 @@ async def test_merge_try_falsy(merge_log_err: MagicMock, hass: HomeAssistant) -> "pack_list2": {"light": OrderedDict()}, } config = { - HA_DOMAIN: {CONF_PACKAGES: packages}, + HOMEASSISTANT_DOMAIN: {CONF_PACKAGES: packages}, "automation": {"do": "something"}, "light": {"some": "light"}, } @@ -1158,7 +1164,7 @@ async def test_merge_new(merge_log_err: MagicMock, hass: HomeAssistant) -> None: "api": {}, }, } - config = {HA_DOMAIN: {CONF_PACKAGES: packages}} + config = {HOMEASSISTANT_DOMAIN: {CONF_PACKAGES: packages}} await config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 0 @@ -1178,7 +1184,7 @@ async def test_merge_type_mismatch( "pack_2": {"light": {"ib1": None}}, # light gets merged - ensure_list } config = { - HA_DOMAIN: {CONF_PACKAGES: packages}, + HOMEASSISTANT_DOMAIN: {CONF_PACKAGES: packages}, "input_boolean": {"ib2": None}, "input_select": [{"ib2": None}], "light": [{"platform": "two"}], @@ -1196,13 +1202,13 @@ async def test_merge_once_only_keys( ) -> None: """Test if we have a merge for a comp that may occur only once. Keys.""" packages = {"pack_2": {"api": None}} - config = {HA_DOMAIN: {CONF_PACKAGES: packages}, "api": None} + config = {HOMEASSISTANT_DOMAIN: {CONF_PACKAGES: packages}, "api": None} await config_util.merge_packages_config(hass, config, packages) assert config["api"] == OrderedDict() packages = {"pack_2": {"api": {"key_3": 3}}} config = { - HA_DOMAIN: {CONF_PACKAGES: packages}, + HOMEASSISTANT_DOMAIN: {CONF_PACKAGES: packages}, "api": {"key_1": 1, "key_2": 2}, } await config_util.merge_packages_config(hass, config, packages) @@ -1211,7 +1217,7 @@ async def test_merge_once_only_keys( # Duplicate keys error packages = {"pack_2": {"api": {"key": 2}}} config = { - HA_DOMAIN: {CONF_PACKAGES: packages}, + HOMEASSISTANT_DOMAIN: {CONF_PACKAGES: packages}, "api": {"key": 1}, } await config_util.merge_packages_config(hass, config, packages) @@ -1226,7 +1232,7 @@ async def test_merge_once_only_lists(hass: HomeAssistant) -> None: } } config = { - HA_DOMAIN: {CONF_PACKAGES: packages}, + HOMEASSISTANT_DOMAIN: {CONF_PACKAGES: packages}, "api": {"list_1": ["item_1"]}, } await config_util.merge_packages_config(hass, config, packages) @@ -1249,7 +1255,7 @@ async def test_merge_once_only_dictionaries(hass: HomeAssistant) -> None: } } config = { - HA_DOMAIN: {CONF_PACKAGES: packages}, + HOMEASSISTANT_DOMAIN: {CONF_PACKAGES: packages}, "api": {"dict_1": {"key_1": 1, "dict_1.1": {"key_1.1": 1.1}}}, } await config_util.merge_packages_config(hass, config, packages) @@ -1285,7 +1291,7 @@ async def test_merge_duplicate_keys( """Test if keys in dicts are duplicates.""" packages = {"pack_1": {"input_select": {"ib1": None}}} config = { - HA_DOMAIN: {CONF_PACKAGES: packages}, + HOMEASSISTANT_DOMAIN: {CONF_PACKAGES: packages}, "input_select": {"ib1": 1}, } await config_util.merge_packages_config(hass, config, packages) @@ -1443,7 +1449,7 @@ async def test_merge_split_component_definition(hass: HomeAssistant) -> None: "pack_1": {"light one": {"l1": None}}, "pack_2": {"light two": {"l2": None}, "light three": {"l3": None}}, } - config = {HA_DOMAIN: {CONF_PACKAGES: packages}} + config = {HOMEASSISTANT_DOMAIN: {CONF_PACKAGES: packages}} await config_util.merge_packages_config(hass, config, packages) assert len(config) == 4 @@ -2332,7 +2338,7 @@ async def test_packages_schema_validation_error( ] assert error_records == snapshot - assert len(config[HA_DOMAIN][CONF_PACKAGES]) == 0 + assert len(config[HOMEASSISTANT_DOMAIN][CONF_PACKAGES]) == 0 def test_extract_domain_configs() -> None: diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b1c3915f983..10cdaa8add9 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -22,7 +22,12 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, CoreState, HomeAssistant, callback +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + CoreState, + HomeAssistant, + callback, +) from homeassistant.data_entry_flow import BaseServiceInfo, FlowResult, FlowResultType from homeassistant.exceptions import ( ConfigEntryAuthFailed, @@ -526,13 +531,13 @@ async def test_remove_entry_cancels_reauth( assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR issue_id = f"config_entry_reauth_test_{entry.entry_id}" - assert issue_registry.async_get_issue(HA_DOMAIN, issue_id) + assert issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, issue_id) await manager.async_remove(entry.entry_id) flows = hass.config_entries.flow.async_progress_by_handler("test") assert len(flows) == 0 - assert not issue_registry.async_get_issue(HA_DOMAIN, issue_id) + assert not issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, issue_id) async def test_remove_entry_handles_callback_error( @@ -1189,14 +1194,14 @@ async def test_reauth_issue( assert len(issue_registry.issues) == 1 issue_id = f"config_entry_reauth_test_{entry.entry_id}" - issue = issue_registry.async_get_issue(HA_DOMAIN, issue_id) + issue = issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, issue_id) assert issue == ir.IssueEntry( active=True, breaks_in_ha_version=None, created=ANY, data={"flow_id": flows[0]["flow_id"]}, dismissed_version=None, - domain=HA_DOMAIN, + domain=HOMEASSISTANT_DOMAIN, is_fixable=False, is_persistent=False, issue_domain="test", @@ -5098,7 +5103,7 @@ async def test_hashable_non_string_unique_id( { "type": data_entry_flow.FlowResultType.ABORT, "reason": "single_instance_allowed", - "translation_domain": HA_DOMAIN, + "translation_domain": HOMEASSISTANT_DOMAIN, }, ), ], @@ -5296,7 +5301,7 @@ async def test_avoid_adding_second_config_entry_on_single_config_entry( ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" - assert result["translation_domain"] == HA_DOMAIN + assert result["translation_domain"] == HOMEASSISTANT_DOMAIN async def test_in_progress_get_canceled_when_entry_is_created( diff --git a/tests/test_setup.py b/tests/test_setup.py index e28506adc59..3430c17960c 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -10,7 +10,12 @@ import voluptuous as vol from homeassistant import config_entries, loader, setup from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START -from homeassistant.core import DOMAIN, CoreState, HomeAssistant, callback +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + CoreState, + HomeAssistant, + callback, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, discovery, translation from homeassistant.helpers.dispatcher import ( @@ -243,7 +248,9 @@ async def test_component_not_found( """setup_component should not crash if component doesn't exist.""" assert await setup.async_setup_component(hass, "non_existing", {}) is False assert len(issue_registry.issues) == 1 - issue = issue_registry.async_get_issue(DOMAIN, "integration_not_found.non_existing") + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, "integration_not_found.non_existing" + ) assert issue assert issue.translation_key == "integration_not_found" From 0de75aeee1fa02ea56516bcffbd9524df3d4aa7d Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Mon, 29 Jul 2024 13:52:01 +0300 Subject: [PATCH 1654/2411] Wait for initial scan to finish before setting up platforms (#122360) --- homeassistant/components/gree/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index 0a2e2852e34..c385ce45262 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -29,8 +29,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: gree_discovery = DiscoveryService(hass) hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - async def _async_scan_update(_=None): bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass)) await gree_discovery.discovery.scan(0, bcast_ifaces=bcast_addr) @@ -44,6 +42,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True From 86bfc7ada86f8501fee0650bfcc133199f913883 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Mon, 29 Jul 2024 11:52:37 +0100 Subject: [PATCH 1655/2411] Remove UE Smart Radio integration (#122578) --- homeassistant/brands/logitech.json | 2 +- .../components/ue_smart_radio/__init__.py | 1 - .../components/ue_smart_radio/manifest.json | 7 - .../components/ue_smart_radio/media_player.py | 190 ------------------ homeassistant/generated/integrations.json | 6 - 5 files changed, 1 insertion(+), 205 deletions(-) delete mode 100644 homeassistant/components/ue_smart_radio/__init__.py delete mode 100644 homeassistant/components/ue_smart_radio/manifest.json delete mode 100644 homeassistant/components/ue_smart_radio/media_player.py diff --git a/homeassistant/brands/logitech.json b/homeassistant/brands/logitech.json index d4a0dd1bb87..2fd61ca0e2b 100644 --- a/homeassistant/brands/logitech.json +++ b/homeassistant/brands/logitech.json @@ -1,5 +1,5 @@ { "domain": "logitech", "name": "Logitech", - "integrations": ["harmony", "ue_smart_radio", "squeezebox"] + "integrations": ["harmony", "squeezebox"] } diff --git a/homeassistant/components/ue_smart_radio/__init__.py b/homeassistant/components/ue_smart_radio/__init__.py deleted file mode 100644 index 2d686b7c5ea..00000000000 --- a/homeassistant/components/ue_smart_radio/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The ue_smart_radio component.""" diff --git a/homeassistant/components/ue_smart_radio/manifest.json b/homeassistant/components/ue_smart_radio/manifest.json deleted file mode 100644 index 2d3568a115a..00000000000 --- a/homeassistant/components/ue_smart_radio/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "ue_smart_radio", - "name": "Logitech UE Smart Radio", - "codeowners": [], - "documentation": "https://www.home-assistant.io/integrations/ue_smart_radio", - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/ue_smart_radio/media_player.py b/homeassistant/components/ue_smart_radio/media_player.py deleted file mode 100644 index 62675c62c0e..00000000000 --- a/homeassistant/components/ue_smart_radio/media_player.py +++ /dev/null @@ -1,190 +0,0 @@ -"""Support for Logitech UE Smart Radios.""" - -from __future__ import annotations - -import logging - -import requests -import voluptuous as vol - -from homeassistant.components.media_player import ( - PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, - MediaPlayerEntity, - MediaPlayerEntityFeature, - MediaPlayerState, - MediaType, -) -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -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 - -_LOGGER = logging.getLogger(__name__) - -ICON = "mdi:radio" -URL = "http://decibel.logitechmusic.com/jsonrpc.js" - -PLAYBACK_DICT = { - "play": MediaPlayerState.PLAYING, - "pause": MediaPlayerState.PAUSED, - "stop": MediaPlayerState.IDLE, -} - -PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( - {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} -) - - -def send_request(payload, session): - """Send request to radio.""" - try: - request = requests.post( - URL, - cookies={"sdi_squeezenetwork_session": session}, - json=payload, - timeout=5, - ) - except requests.exceptions.Timeout: - _LOGGER.error("Timed out when sending request") - except requests.exceptions.ConnectionError: - _LOGGER.error("An error occurred while connecting") - else: - return request.json() - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Logitech UE Smart Radio platform.""" - email = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - - session_request = requests.post( - "https://www.uesmartradio.com/user/login", - data={"email": email, "password": password}, - timeout=5, - ) - session = session_request.cookies["sdi_squeezenetwork_session"] - - player_request = send_request({"params": ["", ["serverstatus"]]}, session) - - players = [ - UERadioDevice(session, player["playerid"], player["name"]) - for player in player_request["result"]["players_loop"] - ] - - add_entities(players) - - -class UERadioDevice(MediaPlayerEntity): - """Representation of a Logitech UE Smart Radio device.""" - - _attr_icon = ICON - _attr_media_content_type = MediaType.MUSIC - _attr_supported_features = ( - MediaPlayerEntityFeature.PLAY - | MediaPlayerEntityFeature.PAUSE - | MediaPlayerEntityFeature.STOP - | MediaPlayerEntityFeature.PREVIOUS_TRACK - | MediaPlayerEntityFeature.NEXT_TRACK - | MediaPlayerEntityFeature.TURN_ON - | MediaPlayerEntityFeature.TURN_OFF - | MediaPlayerEntityFeature.VOLUME_SET - | MediaPlayerEntityFeature.VOLUME_MUTE - ) - - def __init__(self, session, player_id, player_name): - """Initialize the Logitech UE Smart Radio device.""" - self._session = session - self._player_id = player_id - self._attr_name = player_name - self._attr_volume_level = 0 - self._last_volume = 0 - - def send_command(self, command): - """Send command to radio.""" - send_request( - {"method": "slim.request", "params": [self._player_id, command]}, - self._session, - ) - - def update(self) -> None: - """Get the latest details from the device.""" - request = send_request( - { - "method": "slim.request", - "params": [ - self._player_id, - ["status", "-", 1, "tags:cgABbehldiqtyrSuoKLN"], - ], - }, - self._session, - ) - - if request["error"] is not None: - self._attr_state = None - return - - if request["result"]["power"] == 0: - self._attr_state = MediaPlayerState.OFF - else: - self._attr_state = PLAYBACK_DICT[request["result"]["mode"]] - - media_info = request["result"]["playlist_loop"][0] - - self._attr_volume_level = request["result"]["mixer volume"] / 100 - self._attr_media_image_url = media_info["artwork_url"] - self._attr_media_title = media_info["title"] - if "artist" in media_info: - self._attr_media_artist = media_info["artist"] - else: - self._attr_media_artist = media_info.get("remote_title") - - @property - def is_volume_muted(self) -> bool: - """Boolean if volume is currently muted.""" - return self.volume_level is not None and self.volume_level <= 0 - - def turn_on(self) -> None: - """Turn on specified media player or all.""" - self.send_command(["power", 1]) - - def turn_off(self) -> None: - """Turn off specified media player or all.""" - self.send_command(["power", 0]) - - def media_play(self) -> None: - """Send the media player the command for play/pause.""" - self.send_command(["play"]) - - def media_pause(self) -> None: - """Send the media player the command for pause.""" - self.send_command(["pause"]) - - def media_stop(self) -> None: - """Send the media player the stop command.""" - self.send_command(["stop"]) - - def media_previous_track(self) -> None: - """Send the media player the command for prev track.""" - self.send_command(["button", "rew"]) - - def media_next_track(self) -> None: - """Send the media player the command for next track.""" - self.send_command(["button", "fwd"]) - - def mute_volume(self, mute: bool) -> None: - """Send mute command.""" - if mute: - self._last_volume = self.volume_level - self.send_command(["mixer", "volume", 0]) - else: - self.send_command(["mixer", "volume", self._last_volume * 100]) - - def set_volume_level(self, volume: float) -> None: - """Set volume level, range 0..1.""" - self.send_command(["mixer", "volume", volume * 100]) diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 8b0225ed063..4de325a0c6e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3382,12 +3382,6 @@ "iot_class": "local_push", "name": "Logitech Harmony Hub" }, - "ue_smart_radio": { - "integration_type": "hub", - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Logitech UE Smart Radio" - }, "squeezebox": { "integration_type": "hub", "config_flow": true, From cfef72ae5789e2eb840b139de733ba0587470fe8 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Mon, 29 Jul 2024 06:56:26 -0400 Subject: [PATCH 1656/2411] Add Sonos tests for media_player volume (#122283) --- .../components/sonos/media_player.py | 2 +- tests/components/sonos/test_media_player.py | 46 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index e9fbb152b7a..4125466bd99 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -388,7 +388,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): @soco_error() def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" - self.soco.volume = str(int(volume * 100)) + self.soco.volume = int(volume * 100) @soco_error(UPNP_ERRORS_TO_IGNORE) def set_shuffle(self, shuffle: bool) -> None: diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 0a9b1960910..c765ed82ac6 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -22,8 +22,14 @@ from homeassistant.components.sonos.media_player import ( LONG_SERVICE_TIMEOUT, SERVICE_RESTORE, SERVICE_SNAPSHOT, + VOLUME_INCREMENT, +) +from homeassistant.const import ( + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP, + STATE_IDLE, ) -from homeassistant.const import STATE_IDLE from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.device_registry import ( @@ -768,3 +774,41 @@ async def test_service_snapshot_restore( blocking=True, ) assert mock_restore.call_count == 2 + + +async def test_volume( + hass: HomeAssistant, + soco: MockSoCo, + async_autosetup_sonos, +) -> None: + """Test the media player volume services.""" + initial_volume = soco.volume + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_UP, + { + "entity_id": "media_player.zone_a", + }, + blocking=True, + ) + assert soco.volume == initial_volume + VOLUME_INCREMENT + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_DOWN, + { + "entity_id": "media_player.zone_a", + }, + blocking=True, + ) + assert soco.volume == initial_volume + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_SET, + {"entity_id": "media_player.zone_a", "volume_level": 0.30}, + blocking=True, + ) + # SoCo uses 0..100 for its range. + assert soco.volume == 30 From 9e10126505b24f1c3719442e53e23e5fe8e292c6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 12:59:25 +0200 Subject: [PATCH 1657/2411] Revert "Small refactor to cleanup unnecessary returns (#121653)" (#122756) --- homeassistant/components/homematic/climate.py | 6 ++++-- homeassistant/components/nest/media_source.py | 7 ++++--- homeassistant/components/telnet/switch.py | 3 ++- homeassistant/components/tradfri/switch.py | 10 ++++++---- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 2b0306809b0..16c345c5635 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -140,8 +140,10 @@ class HMThermostat(HMDevice, ClimateEntity): def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None: - self._hmdevice.writeNodeData(self._state, float(temperature)) + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: + return None + + self._hmdevice.writeNodeData(self._state, float(temperature)) def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 1260474ad88..6c481806e4f 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -227,9 +227,10 @@ class NestEventMediaStore(EventMediaStore): filename = self.get_media_filename(media_key) def remove_media(filename: str) -> None: - if os.path.exists(filename): - _LOGGER.debug("Removing event media from disk store: %s", filename) - os.remove(filename) + if not os.path.exists(filename): + return None + _LOGGER.debug("Removing event media from disk store: %s", filename) + os.remove(filename) try: await self._hass.async_add_executor_job(remove_media, filename) diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index 8aae49f8730..805f037dbae 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -142,9 +142,10 @@ class TelnetSwitch(SwitchEntity): response = self._telnet_command(self._command_state) if response and self._value_template: rendered = self._value_template.render_with_possible_json_value(response) - self._attr_is_on = rendered == "True" else: _LOGGER.warning("Empty response for command: %s", self._command_state) + return None + self._attr_is_on = rendered == "True" def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index 88126f1ffce..4ad1424aa9a 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -72,10 +72,12 @@ class TradfriSwitch(TradfriBaseEntity, SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the switch to turn off.""" - if self._device_control: - await self._api(self._device_control.set_state(False)) + if not self._device_control: + return None + await self._api(self._device_control.set_state(False)) async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the switch to turn on.""" - if self._device_control: - await self._api(self._device_control.set_state(True)) + if not self._device_control: + return None + await self._api(self._device_control.set_state(True)) From 9ce7779bde1151840971f7e90086c87d85dca35b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 12:59:57 +0200 Subject: [PATCH 1658/2411] Use correct constant in rest tests (#122765) --- tests/components/rest/test_init.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 0fda89cc329..02dfe6364ff 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -16,7 +16,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, UnitOfInformation, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -468,7 +468,7 @@ async def test_config_schema_via_packages(hass: HomeAssistant) -> None: "pack_11": {"rest": {"resource": "http://url1"}}, "pack_list": {"rest": [{"resource": "http://url2"}]}, } - config = {hass_config.HA_DOMAIN: {hass_config.CONF_PACKAGES: packages}} + config = {HOMEASSISTANT_DOMAIN: {hass_config.CONF_PACKAGES: packages}} await hass_config.merge_packages_config(hass, config, packages) assert len(config) == 2 From 07c7bb8b2a4a1c1a6f5e89799000b29a0ca07d56 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:35:36 +0200 Subject: [PATCH 1659/2411] Use HOMEASSISTANT_DOMAIN alias for core DOMAIN (#122760) --- homeassistant/components/blueprint/models.py | 4 +-- homeassistant/components/config/scene.py | 6 ++-- .../generic_hygrostat/humidifier.py | 8 +++-- .../components/generic_thermostat/climate.py | 6 ++-- .../components/google_assistant/trait.py | 4 +-- .../components/homeassistant/scene.py | 6 ++-- homeassistant/components/incomfort/errors.py | 10 +++--- homeassistant/components/intent/__init__.py | 8 ++--- homeassistant/components/ping/entity.py | 4 +-- homeassistant/components/scene/__init__.py | 8 +++-- .../components/streamlabswater/entity.py | 5 +-- homeassistant/config.py | 34 +++++++++++-------- homeassistant/config_entries.py | 12 +++---- homeassistant/helpers/check_config.py | 10 +++--- script/hassfest/config_schema.py | 4 +-- 15 files changed, 70 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 01d26de618d..02a215ca103 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -21,7 +21,7 @@ from homeassistant.const import ( CONF_PATH, __version__, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.util import yaml @@ -372,7 +372,7 @@ class DomainBlueprints: shutil.copytree( integration.file_path / BLUEPRINT_FOLDER, - self.blueprint_folder / HA_DOMAIN, + self.blueprint_folder / HOMEASSISTANT_DOMAIN, ) await self.hass.async_add_executor_job(populate) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 8192c0051b0..d44c2bb87b4 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -11,7 +11,7 @@ from homeassistant.components.scene import ( ) from homeassistant.config import SCENE_CONFIG_PATH from homeassistant.const import CONF_ID, SERVICE_RELOAD -from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from .const import ACTION_DELETE @@ -32,7 +32,9 @@ def async_setup(hass: HomeAssistant) -> bool: ent_reg = er.async_get(hass) - entity_id = ent_reg.async_get_entity_id(DOMAIN, HA_DOMAIN, config_key) + entity_id = ent_reg.async_get_entity_id( + DOMAIN, HOMEASSISTANT_DOMAIN, config_key + ) if entity_id is None: return diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index cc04dbf13c3..ab29e587232 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -33,7 +33,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import ( - DOMAIN as HA_DOMAIN, + DOMAIN as HOMEASSISTANT_DOMAIN, Event, EventStateChangedData, HomeAssistant, @@ -554,12 +554,14 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): async def _async_device_turn_on(self) -> None: """Turn humidifier toggleable device on.""" data = {ATTR_ENTITY_ID: self._switch_entity_id} - await self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_ON, data) + await self.hass.services.async_call(HOMEASSISTANT_DOMAIN, SERVICE_TURN_ON, data) async def _async_device_turn_off(self) -> None: """Turn humidifier toggleable device off.""" data = {ATTR_ENTITY_ID: self._switch_entity_id} - await self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data) + await self.hass.services.async_call( + HOMEASSISTANT_DOMAIN, SERVICE_TURN_OFF, data + ) async def async_set_mode(self, mode: str) -> None: """Set new mode. diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 22001b2acc4..c142d15f9e5 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -38,7 +38,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import ( - DOMAIN as HA_DOMAIN, + DOMAIN as HOMEASSISTANT_DOMAIN, CoreState, Event, EventStateChangedData, @@ -570,14 +570,14 @@ class GenericThermostat(ClimateEntity, RestoreEntity): """Turn heater toggleable device on.""" data = {ATTR_ENTITY_ID: self.heater_entity_id} await self.hass.services.async_call( - HA_DOMAIN, SERVICE_TURN_ON, data, context=self._context + HOMEASSISTANT_DOMAIN, SERVICE_TURN_ON, data, context=self._context ) async def _async_heater_turn_off(self) -> None: """Turn heater toggleable device off.""" data = {ATTR_ENTITY_ID: self.heater_entity_id} await self.hass.services.async_call( - HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context + HOMEASSISTANT_DOMAIN, SERVICE_TURN_OFF, data, context=self._context ) async def async_set_preset_mode(self, preset_mode: str) -> None: diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 05d18f1e45b..e54684fbc64 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -81,7 +81,7 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers.network import get_url from homeassistant.util import color as color_util, dt as dt_util from homeassistant.util.dt import utcnow @@ -511,7 +511,7 @@ class OnOffTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute an OnOff command.""" if (domain := self.state.domain) == group.DOMAIN: - service_domain = HA_DOMAIN + service_domain = HOMEASSISTANT_DOMAIN service = SERVICE_TURN_ON if params["on"] else SERVICE_TURN_OFF else: diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 1c4fee23198..0d12c1537ff 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -24,7 +24,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import ( - DOMAIN as HA_DOMAIN, + DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, ServiceCall, State, @@ -92,7 +92,7 @@ STATES_SCHEMA = vol.All(dict, _convert_states) PLATFORM_SCHEMA = vol.Schema( { - vol.Required(CONF_PLATFORM): HA_DOMAIN, + vol.Required(CONF_PLATFORM): HOMEASSISTANT_DOMAIN, vol.Required(STATES): vol.All( cv.ensure_list, [ @@ -206,7 +206,7 @@ async def async_setup_platform( # Extract only the config for the Home Assistant platform, ignore the rest. for p_type, p_config in conf_util.config_per_platform(conf, SCENE_DOMAIN): - if p_type != HA_DOMAIN: + if p_type != HOMEASSISTANT_DOMAIN: continue _process_scenes_config(hass, async_add_entities, p_config) diff --git a/homeassistant/components/incomfort/errors.py b/homeassistant/components/incomfort/errors.py index 1023ce70eec..93a29d05bb8 100644 --- a/homeassistant/components/incomfort/errors.py +++ b/homeassistant/components/incomfort/errors.py @@ -1,32 +1,32 @@ """Exceptions raised by Intergas InComfort integration.""" -from homeassistant.core import DOMAIN +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError class NotFound(HomeAssistantError): """Raise exception if no Lan2RF Gateway was found.""" - translation_domain = DOMAIN + translation_domain = HOMEASSISTANT_DOMAIN translation_key = "not_found" class NoHeaters(ConfigEntryNotReady): """Raise exception if no heaters are found.""" - translation_domain = DOMAIN + translation_domain = HOMEASSISTANT_DOMAIN translation_key = "no_heaters" class InConfortTimeout(ConfigEntryNotReady): """Raise exception if no heaters are found.""" - translation_domain = DOMAIN + translation_domain = HOMEASSISTANT_DOMAIN translation_key = "timeout_error" class InConfortUnknownError(ConfigEntryNotReady): """Raise exception if no heaters are found.""" - translation_domain = DOMAIN + translation_domain = HOMEASSISTANT_DOMAIN translation_key = "unknown" diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index c933b94fdd4..b1716a8d2d2 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -35,7 +35,7 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, State +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, State from homeassistant.helpers import config_validation as cv, integration_platform, intent from homeassistant.helpers.typing import ConfigType @@ -82,7 +82,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, OnOffIntentHandler( intent.INTENT_TURN_ON, - HA_DOMAIN, + HOMEASSISTANT_DOMAIN, SERVICE_TURN_ON, description="Turns on/opens a device or entity", ), @@ -91,7 +91,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, OnOffIntentHandler( intent.INTENT_TURN_OFF, - HA_DOMAIN, + HOMEASSISTANT_DOMAIN, SERVICE_TURN_OFF, description="Turns off/closes a device or entity", ), @@ -100,7 +100,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, intent.ServiceIntentHandler( intent.INTENT_TOGGLE, - HA_DOMAIN, + HOMEASSISTANT_DOMAIN, SERVICE_TOGGLE, description="Toggles a device or entity", ), diff --git a/homeassistant/components/ping/entity.py b/homeassistant/components/ping/entity.py index 34207b284bb..a1f84f6ef32 100644 --- a/homeassistant/components/ping/entity.py +++ b/homeassistant/components/ping/entity.py @@ -1,7 +1,7 @@ """Base entity for the Ping component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.core import DOMAIN +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -24,6 +24,6 @@ class PingEntity(CoordinatorEntity[PingUpdateCoordinator]): self._attr_unique_id = unique_id self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, config_entry.entry_id)}, + identifiers={(HOMEASSISTANT_DOMAIN, config_entry.entry_id)}, manufacturer="Ping", ) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 5a7df164e1f..596d256ffb7 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.light import ATTR_TRANSITION from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON, STATE_UNAVAILABLE -from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType @@ -25,7 +25,7 @@ STATES: Final = "states" def _hass_domain_validator(config: dict[str, Any]) -> dict[str, Any]: """Validate platform in config for homeassistant domain.""" if CONF_PLATFORM not in config: - config = {CONF_PLATFORM: HA_DOMAIN, STATES: config} + config = {CONF_PLATFORM: HOMEASSISTANT_DOMAIN, STATES: config} return config @@ -67,7 +67,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_setup(config) # Ensure Home Assistant platform always loaded. hass.async_create_task( - component.async_setup_platform(HA_DOMAIN, {"platform": HA_DOMAIN, STATES: []}), + component.async_setup_platform( + HOMEASSISTANT_DOMAIN, {"platform": HOMEASSISTANT_DOMAIN, STATES: []} + ), eager_start=True, ) component.async_register_entity_service( diff --git a/homeassistant/components/streamlabswater/entity.py b/homeassistant/components/streamlabswater/entity.py index fb7031a9e76..3110a56cd99 100644 --- a/homeassistant/components/streamlabswater/entity.py +++ b/homeassistant/components/streamlabswater/entity.py @@ -1,6 +1,6 @@ """Base entity for Streamlabs integration.""" -from homeassistant.core import DOMAIN +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -23,7 +23,8 @@ class StreamlabsWaterEntity(CoordinatorEntity[StreamlabsCoordinator]): self._location_id = location_id self._attr_unique_id = f"{location_id}-{key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, location_id)}, name=self.location_data.name + identifiers={(HOMEASSISTANT_DOMAIN, location_id)}, + name=self.location_data.name, ) @property diff --git a/homeassistant/config.py b/homeassistant/config.py index a61fcbdbb0c..18c833d4c75 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -60,7 +60,7 @@ from .const import ( LEGACY_CONF_WHITELIST_EXTERNAL_DIRS, __version__, ) -from .core import DOMAIN as HA_DOMAIN, ConfigSource, HomeAssistant, callback +from .core import DOMAIN as HOMEASSISTANT_DOMAIN, ConfigSource, HomeAssistant, callback from .exceptions import ConfigValidationError, HomeAssistantError from .generated.currencies import HISTORIC_CURRENCIES from .helpers import config_validation as cv, issue_registry as ir @@ -261,12 +261,12 @@ CUSTOMIZE_CONFIG_SCHEMA = vol.Schema( def _raise_issue_if_historic_currency(hass: HomeAssistant, currency: str) -> None: if currency not in HISTORIC_CURRENCIES: - ir.async_delete_issue(hass, HA_DOMAIN, "historic_currency") + ir.async_delete_issue(hass, HOMEASSISTANT_DOMAIN, "historic_currency") return ir.async_create_issue( hass, - HA_DOMAIN, + HOMEASSISTANT_DOMAIN, "historic_currency", is_fixable=False, learn_more_url="homeassistant://config/general", @@ -278,12 +278,12 @@ def _raise_issue_if_historic_currency(hass: HomeAssistant, currency: str) -> Non def _raise_issue_if_no_country(hass: HomeAssistant, country: str | None) -> None: if country is not None: - ir.async_delete_issue(hass, HA_DOMAIN, "country_not_configured") + ir.async_delete_issue(hass, HOMEASSISTANT_DOMAIN, "country_not_configured") return ir.async_create_issue( hass, - HA_DOMAIN, + HOMEASSISTANT_DOMAIN, "country_not_configured", is_fixable=False, learn_more_url="homeassistant://config/general", @@ -481,12 +481,14 @@ async def async_hass_config_yaml(hass: HomeAssistant) -> dict: for invalid_domain in invalid_domains: config.pop(invalid_domain) - core_config = config.get(HA_DOMAIN, {}) + core_config = config.get(HOMEASSISTANT_DOMAIN, {}) try: await merge_packages_config(hass, config, core_config.get(CONF_PACKAGES, {})) except vol.Invalid as exc: suffix = "" - if annotation := find_annotation(config, [HA_DOMAIN, CONF_PACKAGES, *exc.path]): + if annotation := find_annotation( + config, [HOMEASSISTANT_DOMAIN, CONF_PACKAGES, *exc.path] + ): suffix = f" at {_relpath(hass, annotation[0])}, line {annotation[1]}" _LOGGER.error( "Invalid package configuration '%s'%s: %s", CONF_PACKAGES, suffix, exc @@ -709,7 +711,7 @@ def stringify_invalid( ) else: message_prefix = f"Invalid config for '{domain}'" - if domain != HA_DOMAIN and link: + if domain != HOMEASSISTANT_DOMAIN and link: message_suffix = f", please check the docs at {link}" else: message_suffix = "" @@ -792,7 +794,7 @@ def format_homeassistant_error( if annotation := find_annotation(config, [domain]): message_prefix += f" at {_relpath(hass, annotation[0])}, line {annotation[1]}" message = f"{message_prefix}: {str(exc) or repr(exc)}" - if domain != HA_DOMAIN and link: + if domain != HOMEASSISTANT_DOMAIN and link: message += f", please check the docs at {link}" return message @@ -916,7 +918,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non cust_glob = OrderedDict(config[CONF_CUSTOMIZE_GLOB]) for name, pkg in config[CONF_PACKAGES].items(): - if (pkg_cust := pkg.get(HA_DOMAIN)) is None: + if (pkg_cust := pkg.get(HOMEASSISTANT_DOMAIN)) is None: continue try: @@ -940,7 +942,9 @@ def _log_pkg_error( ) -> None: """Log an error while merging packages.""" message_prefix = f"Setup of package '{package}'" - if annotation := find_annotation(config, [HA_DOMAIN, CONF_PACKAGES, package]): + if annotation := find_annotation( + config, [HOMEASSISTANT_DOMAIN, CONF_PACKAGES, package] + ): message_prefix += f" at {_relpath(hass, annotation[0])}, line {annotation[1]}" _LOGGER.error("%s failed: %s", message_prefix, message) @@ -1055,7 +1059,7 @@ async def merge_packages_config( continue for comp_name, comp_conf in pack_conf.items(): - if comp_name == HA_DOMAIN: + if comp_name == HOMEASSISTANT_DOMAIN: continue try: domain = cv.domain_key(comp_name) @@ -1200,7 +1204,7 @@ def _get_log_message_and_stack_print_pref( # Generate the log message from the English translations log_message = async_get_exception_message( - HA_DOMAIN, + HOMEASSISTANT_DOMAIN, platform_exception.translation_key, translation_placeholders=placeholders, ) @@ -1261,7 +1265,7 @@ def async_drop_config_annotations( # Don't drop annotations from the homeassistant integration because it may # have configuration for other integrations as packages. - if integration.domain in config and integration.domain != HA_DOMAIN: + if integration.domain in config and integration.domain != HOMEASSISTANT_DOMAIN: drop_config_annotations_rec(config[integration.domain]) return config @@ -1313,7 +1317,7 @@ def async_handle_component_errors( raise ConfigValidationError( translation_key, [platform_exception.exception for platform_exception in config_exception_info], - translation_domain=HA_DOMAIN, + translation_domain=HOMEASSISTANT_DOMAIN, translation_placeholders=placeholders, ) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index bf3d8fa8f03..70c87392d0b 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -31,7 +31,7 @@ from .components import persistent_notification from .const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, Platform from .core import ( CALLBACK_TYPE, - DOMAIN as HA_DOMAIN, + DOMAIN as HOMEASSISTANT_DOMAIN, CoreState, Event, HassJob, @@ -1049,7 +1049,7 @@ class ConfigEntry(Generic[_DataT]): issue_id = f"config_entry_reauth_{self.domain}_{self.entry_id}" ir.async_create_issue( hass, - HA_DOMAIN, + HOMEASSISTANT_DOMAIN, issue_id, data={"flow_id": result["flow_id"]}, is_fixable=False, @@ -1254,7 +1254,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): flow_id=flow_id, handler=handler, reason="single_instance_allowed", - translation_domain=HA_DOMAIN, + translation_domain=HOMEASSISTANT_DOMAIN, ) loop = self.hass.loop @@ -1343,7 +1343,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): entry := self.config_entries.async_get_entry(entry_id) ) is not None: issue_id = f"config_entry_reauth_{entry.domain}_{entry.entry_id}" - ir.async_delete_issue(self.hass, HA_DOMAIN, issue_id) + ir.async_delete_issue(self.hass, HOMEASSISTANT_DOMAIN, issue_id) if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY: return result @@ -1360,7 +1360,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): flow_id=flow.flow_id, handler=flow.handler, reason="single_instance_allowed", - translation_domain=HA_DOMAIN, + translation_domain=HOMEASSISTANT_DOMAIN, ) # Check if config entry exists with unique ID. Unload it. @@ -1752,7 +1752,7 @@ class ConfigEntries: if "flow_id" in progress_flow: self.hass.config_entries.flow.async_abort(progress_flow["flow_id"]) issue_id = f"config_entry_reauth_{entry.domain}_{entry.entry_id}" - ir.async_delete_issue(self.hass, HA_DOMAIN, issue_id) + ir.async_delete_issue(self.hass, HOMEASSISTANT_DOMAIN, issue_id) # After we have fully removed an "ignore" config entry we can try and rediscover # it so that a user is able to immediately start configuring it. We do this by diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 0626e0033c4..06d836e8c20 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -22,7 +22,7 @@ from homeassistant.config import ( # type: ignore[attr-defined] load_yaml_config_file, merge_packages_config, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.requirements import ( RequirementsNotFound, @@ -157,10 +157,10 @@ async def async_check_ha_config_file( # noqa: C901 return result.add_error(f"Error loading {config_path}: {err}") # Extract and validate core [homeassistant] config - core_config = config.pop(HA_DOMAIN, {}) + core_config = config.pop(HOMEASSISTANT_DOMAIN, {}) try: core_config = CORE_CONFIG_SCHEMA(core_config) - result[HA_DOMAIN] = core_config + result[HOMEASSISTANT_DOMAIN] = core_config # Merge packages await merge_packages_config( @@ -168,8 +168,8 @@ async def async_check_ha_config_file( # noqa: C901 ) except vol.Invalid as err: result.add_error( - format_schema_error(hass, err, HA_DOMAIN, core_config), - HA_DOMAIN, + format_schema_error(hass, err, HOMEASSISTANT_DOMAIN, core_config), + HOMEASSISTANT_DOMAIN, core_config, ) core_config = {} diff --git a/script/hassfest/config_schema.py b/script/hassfest/config_schema.py index 4d3f0cde482..06ef2065127 100644 --- a/script/hassfest/config_schema.py +++ b/script/hassfest/config_schema.py @@ -4,7 +4,7 @@ from __future__ import annotations import ast -from homeassistant.core import DOMAIN as HA_DOMAIN +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from .model import Config, Integration @@ -12,7 +12,7 @@ CONFIG_SCHEMA_IGNORE = { # Configuration under the homeassistant key is a special case, it's handled by # conf_util.async_process_ha_core_config already during bootstrapping, not by # a schema in the homeassistant integration. - HA_DOMAIN, + HOMEASSISTANT_DOMAIN, } From 61d4bc1430915486b2d472b4fce1869f770c2a02 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 29 Jul 2024 13:38:58 +0200 Subject: [PATCH 1660/2411] Fix device class of water consumption sensor in Overkiz (#122766) Fixes #118959 --- homeassistant/components/overkiz/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index bf9608358eb..5c54a1bd383 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -110,7 +110,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ name="Water consumption", icon="mdi:water", native_unit_of_measurement=UnitOfVolume.LITERS, - device_class=SensorDeviceClass.VOLUME, + device_class=SensorDeviceClass.WATER, state_class=SensorStateClass.TOTAL_INCREASING, ), OverkizSensorDescription( From 74d10b9824444350a4007085200ea6810a678ee2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 22 Jul 2024 01:11:05 +0200 Subject: [PATCH 1661/2411] Bump `aiotractive` to 0.6.0 (#121155) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> Co-authored-by: J. Nick Koston --- homeassistant/components/tractive/__init__.py | 2 +- homeassistant/components/tractive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index fd5abe24c06..4f0de7b14cd 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -132,7 +132,7 @@ async def _generate_trackables( trackable = await trackable.details() # Check that the pet has tracker linked. - if not trackable["device_id"]: + if not trackable.get("device_id"): return None if "details" not in trackable: diff --git a/homeassistant/components/tractive/manifest.json b/homeassistant/components/tractive/manifest.json index 75ddf065bd7..903c5347d52 100644 --- a/homeassistant/components/tractive/manifest.json +++ b/homeassistant/components/tractive/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "cloud_push", "loggers": ["aiotractive"], - "requirements": ["aiotractive==0.5.6"] + "requirements": ["aiotractive==0.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1512acef5e8..6c87b9bd19a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ aiosyncthing==0.5.1 aiotankerkoenig==0.4.1 # homeassistant.components.tractive -aiotractive==0.5.6 +aiotractive==0.6.0 # homeassistant.components.unifi aiounifi==79 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7cbf46c54b..a0b1497a185 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -359,7 +359,7 @@ aiosyncthing==0.5.1 aiotankerkoenig==0.4.1 # homeassistant.components.tractive -aiotractive==0.5.6 +aiotractive==0.6.0 # homeassistant.components.unifi aiounifi==79 From cf20e67f1f541a8d28703b4dceb218403c2ba1e4 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 21 Jul 2024 12:36:06 +0200 Subject: [PATCH 1662/2411] Ensure mqtt subscriptions are in a set (#122201) --- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mqtt/client.py | 8 ++++---- homeassistant/components/mqtt/models.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index f057dab8bc4..2b3aa21aa22 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -251,7 +251,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: mqtt_data.client.async_restore_tracked_subscriptions( mqtt_data.subscriptions_to_restore ) - mqtt_data.subscriptions_to_restore = [] + mqtt_data.subscriptions_to_restore = set() mqtt_data.reload_dispatchers.append( entry.add_update_listener(_async_config_entry_updated) ) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index f65769badfa..d242ec019c6 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -428,12 +428,12 @@ class MQTT: await self.async_init_client() @property - def subscriptions(self) -> list[Subscription]: + def subscriptions(self) -> set[Subscription]: """Return the tracked subscriptions.""" - return [ + return { *chain.from_iterable(self._simple_subscriptions.values()), *self._wildcard_subscriptions, - ] + } def cleanup(self) -> None: """Clean up listeners.""" @@ -736,7 +736,7 @@ class MQTT: @callback def async_restore_tracked_subscriptions( - self, subscriptions: list[Subscription] + self, subscriptions: set[Subscription] ) -> None: """Restore tracked subscriptions after reload.""" for subscription in subscriptions: diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index e5a9a9c44da..c355510a5c2 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -423,7 +423,7 @@ class MqttData: reload_handlers: dict[str, CALLBACK_TYPE] = field(default_factory=dict) reload_schema: dict[str, VolSchemaType] = field(default_factory=dict) state_write_requests: EntityTopicState = field(default_factory=EntityTopicState) - subscriptions_to_restore: list[Subscription] = field(default_factory=list) + subscriptions_to_restore: set[Subscription] = field(default_factory=set) tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict) From 683069cb9850e724594306ddd73fee92c713bbbf Mon Sep 17 00:00:00 2001 From: Alexander Schneider Date: Sun, 21 Jul 2024 14:51:10 -0700 Subject: [PATCH 1663/2411] Add Z-Wave discovery schema for ZVIDAR roller shades (#122332) Add discovery schema for ZVIDAR roller shades --- .../components/zwave_js/discovery.py | 9 + tests/components/zwave_js/conftest.py | 14 + .../zwave_js/fixtures/cover_zvidar_state.json | 1120 +++++++++++++++++ tests/components/zwave_js/test_discovery.py | 12 + 4 files changed, 1155 insertions(+) create mode 100644 tests/components/zwave_js/fixtures/cover_zvidar_state.json diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 0b66567c036..6798e644a02 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -579,6 +579,15 @@ DISCOVERY_SCHEMAS = [ ), entity_registry_enabled_default=False, ), + # ZVIDAR Z-CM-V01 (SmartWings/Deyi WM25L/V Z-Wave Motor for Roller Shade) + ZWaveDiscoverySchema( + platform=Platform.COVER, + hint="shade", + manufacturer_id={0x045A}, + product_id={0x0507}, + product_type={0x0904}, + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, + ), # Vision Security ZL7432 In Wall Dual Relay Switch ZWaveDiscoverySchema( platform=Platform.SWITCH, diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index a2a4c217b8b..31c9c5affa5 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -472,6 +472,12 @@ def iblinds_v3_state_fixture(): return json.loads(load_fixture("zwave_js/cover_iblinds_v3_state.json")) +@pytest.fixture(name="zvidar_state", scope="package") +def zvidar_state_fixture(): + """Load the ZVIDAR node state fixture data.""" + return json.loads(load_fixture("zwave_js/cover_zvidar_state.json")) + + @pytest.fixture(name="qubino_shutter_state", scope="package") def qubino_shutter_state_fixture(): """Load the Qubino Shutter node state fixture data.""" @@ -1081,6 +1087,14 @@ def iblinds_v3_cover_fixture(client, iblinds_v3_state): return node +@pytest.fixture(name="zvidar") +def zvidar_cover_fixture(client, zvidar_state): + """Mock a ZVIDAR window cover node.""" + node = Node(client, copy.deepcopy(zvidar_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="qubino_shutter") def qubino_shutter_cover_fixture(client, qubino_shutter_state): """Mock a Qubino flush shutter node.""" diff --git a/tests/components/zwave_js/fixtures/cover_zvidar_state.json b/tests/components/zwave_js/fixtures/cover_zvidar_state.json new file mode 100644 index 00000000000..05118931026 --- /dev/null +++ b/tests/components/zwave_js/fixtures/cover_zvidar_state.json @@ -0,0 +1,1120 @@ +{ + "nodeId": 270, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": false, + "isSecure": true, + "manufacturerId": 1114, + "productId": 1287, + "productType": 2308, + "firmwareVersion": "1.10.0", + "zwavePlusVersion": 2, + "name": "Window Blind Controller", + "location": "**REDACTED**", + "deviceConfig": { + "filename": "/snapshot/build/node_modules/@zwave-js/config/config/devices/0x045a/Z-CM-V01.json", + "isEmbedded": true, + "manufacturer": "ZVIDAR", + "manufacturerId": 1114, + "label": "Z-CM-V01", + "description": "Smart Curtain Motor", + "devices": [ + { + "productType": 2308, + "productId": 1287 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "paramInformation": { + "_map": {} + }, + "compat": { + "removeCCs": {} + } + }, + "label": "Z-CM-V01", + "interviewAttempts": 0, + "isFrequentListening": "1000ms", + "maxDataRate": 100000, + "supportedDataRates": [100000], + "protocolVersion": 3, + "supportsBeaming": false, + "supportsSecurity": true, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 7, + "deviceClass": { + "basic": { + "key": 3, + "label": "End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x045a:0x0904:0x0507:1.10.0", + "statistics": { + "commandsTX": 2, + "commandsRX": 1, + "commandsDroppedRX": 1, + "commandsDroppedTX": 0, + "timeoutResponse": 0, + "rtt": 357.6, + "lastSeen": "2024-07-21T16:42:38.086Z", + "rssi": -89, + "lwr": { + "protocolDataRate": 4, + "repeaters": [], + "rssi": -91, + "repeaterRSSI": [] + } + }, + "highestSecurityClass": 1, + "isControllerNode": false, + "keepAwake": false, + "lastSeen": "2024-07-21T16:42:38.086Z", + "protocol": 1, + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": "unknown" + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "states": { + "true": "Restore" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Hand Button Action", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Hand Button Action", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Close", + "1": "Open" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Motor Direction", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Motor Direction", + "default": 1, + "min": 1, + "max": 3, + "states": { + "1": "Forward", + "2": "Opposite", + "3": "Reverse" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Manually Set Open Boundary", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Manually Set Open Boundary", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Cancel", + "1": "Start" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Manually Set Closed Boundary", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Manually Set Closed Boundary", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Cancel", + "1": "Start" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Control Motor", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Control Motor", + "default": 3, + "min": 1, + "max": 3, + "states": { + "1": "Open (Up)", + "2": "Close (Down)", + "3": "Stop" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Calibrate Limit Position", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Calibrate Limit Position", + "default": 1, + "min": 1, + "max": 3, + "states": { + "1": "Upper limit", + "2": "Lower limit", + "3": "Third limit" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Delete Limit Position", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Delete Limit Position", + "default": 0, + "min": 0, + "max": 3, + "states": { + "0": "All limits", + "1": "Only upper limit", + "2": "Only lower limit", + "3": "Only third limit" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Low Battery Level Alarm Threshold", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Low Battery Level Alarm Threshold", + "default": 10, + "min": 0, + "max": 50, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Battery Report Interval", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Battery Report Interval", + "default": 3600, + "min": 0, + "max": 2678400, + "unit": "seconds", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3600 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Battery Change Report Threshold", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Battery Change Report Threshold", + "default": 5, + "min": 0, + "max": 50, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Mains status", + "propertyName": "Power Management", + "propertyKeyName": "Mains status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Mains status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "2": "AC mains disconnected", + "3": "AC mains re-connected" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 1114 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 2308 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 1287 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%", + "stateful": true, + "secret": false + }, + "value": 86 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.16" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["1.10"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version", + "stateful": true, + "secret": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version", + "stateful": true, + "secret": false + }, + "value": "7.16.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version", + "stateful": true, + "secret": false + }, + "value": "10.16.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number", + "stateful": true, + "secret": false + }, + "value": 297 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version", + "stateful": true, + "secret": false + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number", + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.16.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number", + "stateful": true, + "secret": false + }, + "value": 297 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version", + "stateful": true, + "secret": false + }, + "value": "1.10.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number", + "stateful": true, + "secret": false + }, + "value": 43707 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 3, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "Node Identify - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 4, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "Node Identify - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 5, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "Node Identify - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "value", + "propertyName": "value", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Indicator value", + "ccSpecific": { + "indicatorId": 0 + }, + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "identify", + "propertyName": "identify", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Identify", + "states": { + "true": "Identify" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "timeout", + "propertyName": "timeout", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "Timeout", + "stateful": true, + "secret": false + } + } + ], + "endpoints": [ + { + "nodeId": 261, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "deviceClass": { + "basic": { + "key": 3, + "label": "End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": true + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 135, + "name": "Indicator", + "version": 3, + "isSecure": true + } + ] + } + ] +} diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 1179d8e843c..57841ef2a83 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -49,6 +49,18 @@ async def test_iblinds_v2(hass: HomeAssistant, client, iblinds_v2, integration) assert state +async def test_zvidar_state(hass: HomeAssistant, client, zvidar, integration) -> None: + """Test that an ZVIDAR Z-CM-V01 multilevel switch value is discovered as a cover.""" + node = zvidar + assert node.device_class.specific.label == "Unused" + + state = hass.states.get("light.window_blind_controller") + assert not state + + state = hass.states.get("cover.window_blind_controller") + assert state + + async def test_ge_12730(hass: HomeAssistant, client, ge_12730, integration) -> None: """Test GE 12730 Fan Controller v2.0 multilevel switch is discovered as a fan.""" node = ge_12730 From b63bc72450631c7d04ea6dd5430265bbb50c7e2a Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Tue, 23 Jul 2024 21:56:46 +0200 Subject: [PATCH 1664/2411] Fix device class on sensor in ViCare (#122334) update device class on init --- homeassistant/components/vicare/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 0e98729e40f..0271ffc9798 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -950,6 +950,8 @@ class ViCareSensor(ViCareEntity, SensorEntity): """Initialize the sensor.""" super().__init__(device_config, api, description.key) self.entity_description = description + # run update to have device_class set depending on unit_of_measurement + self.update() @property def available(self) -> bool: From f739644735525d693e1d6a66a0a39a6256b6ef5b Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Mon, 22 Jul 2024 07:54:31 +0300 Subject: [PATCH 1665/2411] Goofle Generative AI: Fix string format (#122348) * Ignore format for string tool args * Add tests --- .../google_generative_ai_conversation/conversation.py | 2 ++ .../snapshots/test_conversation.ambr | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 8052ee66f40..c80581a1f57 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -81,6 +81,8 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: key = "type_" val = val.upper() elif key == "format": + if schema.get("type") == "string" and val != "enum": + continue key = "format_" elif key == "items": val = _format_schema(val) diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index 7f28c172970..66caf4c7218 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -449,7 +449,6 @@ description: "Test parameters" items { type_: STRING - format_: "lower" } } } From 9d6bd359c497de39b61dc8578fe180796698da5a Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Mon, 22 Jul 2024 12:11:09 +0300 Subject: [PATCH 1666/2411] Ensure script llm tool name does not start with a digit (#122349) * Ensure script tool name does not start with a digit * Fix test name --- homeassistant/helpers/llm.py | 5 ++++- tests/helpers/test_llm.py | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 506cadbf168..f386fb3ddec 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -615,6 +615,9 @@ class ScriptTool(Tool): entity_registry = er.async_get(hass) self.name = split_entity_id(script_entity_id)[1] + if self.name[0].isdigit(): + self.name = "_" + self.name + self._entity_id = script_entity_id self.parameters = vol.Schema({}) entity_entry = entity_registry.async_get(script_entity_id) if entity_entry and entity_entry.unique_id: @@ -715,7 +718,7 @@ class ScriptTool(Tool): SCRIPT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: SCRIPT_DOMAIN + "." + self.name, + ATTR_ENTITY_ID: self._entity_id, ATTR_VARIABLES: tool_input.tool_args, }, context=llm_context.context, diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 81fa573852e..e1f55942d10 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -780,6 +780,46 @@ async def test_script_tool( } +async def test_script_tool_name(hass: HomeAssistant) -> None: + """Test that script tool name is not started with a digit.""" + assert await async_setup_component(hass, "homeassistant", {}) + context = Context() + llm_context = llm.LLMContext( + platform="test_platform", + context=context, + user_prompt="test_text", + language="*", + assistant="conversation", + device_id=None, + ) + + # Create a script with a unique ID + assert await async_setup_component( + hass, + "script", + { + "script": { + "123456": { + "description": "This is a test script", + "sequence": [], + "fields": { + "beer": {"description": "Number of beers", "required": True}, + }, + }, + } + }, + ) + async_expose_entity(hass, "conversation", "script.123456", True) + + api = await llm.async_get_api(hass, "assist", llm_context) + + tools = [tool for tool in api.tools if isinstance(tool, llm.ScriptTool)] + assert len(tools) == 1 + + tool = tools[0] + assert tool.name == "_123456" + + async def test_selector_serializer( hass: HomeAssistant, llm_context: llm.LLMContext ) -> None: From 7135a919e3338741f4a8a816bec9f1eee02de4d4 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 22 Jul 2024 11:09:03 +0200 Subject: [PATCH 1667/2411] Bump reolink-aio to 0.9.5 (#122366) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index ee3ebe8a13a..c329289790b 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.9.4"] + "requirements": ["reolink-aio==0.9.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6c87b9bd19a..e8d846ef099 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2460,7 +2460,7 @@ renault-api==0.2.4 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.4 +reolink-aio==0.9.5 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0b1497a185..787b2af38fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1924,7 +1924,7 @@ renault-api==0.2.4 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.4 +reolink-aio==0.9.5 # homeassistant.components.rflink rflink==0.0.66 From 56f51d3e3511752c3ff8f746c64d57a5bd325606 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Tue, 23 Jul 2024 03:56:13 +0300 Subject: [PATCH 1668/2411] Fix gemini api format conversion (#122403) * Fix gemini api format conversion * add tests * fix tests * fix tests * fix coverage --- .../conversation.py | 18 +++++++++++++++++- .../snapshots/test_conversation.ambr | 18 ++++++++++++++++++ .../test_conversation.py | 4 +++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index c80581a1f57..69a68121c7b 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -73,6 +73,14 @@ SUPPORTED_SCHEMA_KEYS = { def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: """Format the schema to protobuf.""" + if (subschemas := schema.get("anyOf")) or (subschemas := schema.get("allOf")): + for subschema in subschemas: # Gemini API does not support anyOf and allOf keys + if "type" in subschema: # Fallback to first subschema with 'type' field + return _format_schema(subschema) + return _format_schema( + subschemas[0] + ) # Or, if not found, to any of the subschemas + result = {} for key, val in schema.items(): if key not in SUPPORTED_SCHEMA_KEYS: @@ -81,7 +89,9 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: key = "type_" val = val.upper() elif key == "format": - if schema.get("type") == "string" and val != "enum": + if (schema.get("type") == "string" and val != "enum") or ( + schema.get("type") not in ("number", "integer", "string") + ): continue key = "format_" elif key == "items": @@ -89,6 +99,12 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: elif key == "properties": val = {k: _format_schema(v) for k, v in val.items()} result[key] = val + + if result.get("type_") == "OBJECT" and not result.get("properties"): + # An object with undefined properties is not supported by Gemini API. + # Fallback to JSON string. This will probably fail for most tools that want it, + # but we don't have a better fallback strategy so far. + result["properties"] = {"json": {"type_": "STRING"}} return result diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index 66caf4c7218..abd3658e869 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -442,6 +442,24 @@ description: "Test function" parameters { type_: OBJECT + properties { + key: "param3" + value { + type_: OBJECT + properties { + key: "json" + value { + type_: STRING + } + } + } + } + properties { + key: "param2" + value { + type_: NUMBER + } + } properties { key: "param1" value { diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 30016335f3b..a7ab2c1b337 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -185,7 +185,9 @@ async def test_function_call( { vol.Optional("param1", description="Test parameters"): [ vol.All(str, vol.Lower) - ] + ], + vol.Optional("param2"): vol.Any(float, int), + vol.Optional("param3"): dict, } ) From 75f0384a15d8ed2a61f8186911d8a2915f1e4cff Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 24 Jul 2024 20:12:51 +0200 Subject: [PATCH 1669/2411] Fix typo in Matter lock platform (#122536) --- homeassistant/components/matter/lock.py | 4 ++-- .../matter/{test_door_lock.py => test_lock.py} | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) rename tests/components/matter/{test_door_lock.py => test_lock.py} (95%) diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index ae01faa3bc7..31ae5e496ce 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -168,10 +168,10 @@ class MatterLock(MatterEntity, LockEntity): LOGGER.debug("Lock state: %s for %s", lock_state, self.entity_id) - if lock_state is clusters.DoorLock.Enums.DlLockState.kUnlatched: + if lock_state == clusters.DoorLock.Enums.DlLockState.kUnlatched: self._attr_is_locked = False self._attr_is_open = True - if lock_state is clusters.DoorLock.Enums.DlLockState.kLocked: + elif lock_state == clusters.DoorLock.Enums.DlLockState.kLocked: self._attr_is_locked = True self._attr_is_open = False elif lock_state in ( diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_lock.py similarity index 95% rename from tests/components/matter/test_door_lock.py rename to tests/components/matter/test_lock.py index 461cc1b7f3d..1180e6ee469 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_lock.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components.lock import ( STATE_LOCKED, + STATE_OPEN, STATE_UNLOCKED, LockEntityFeature, ) @@ -82,12 +83,12 @@ async def test_lock( assert state assert state.state == STATE_UNLOCKED - set_node_attribute(door_lock, 1, 257, 0, 0) + set_node_attribute(door_lock, 1, 257, 0, 1) await trigger_subscription_callback(hass, matter_client) state = hass.states.get("lock.mock_door_lock_lock") assert state - assert state.state == STATE_UNLOCKED + assert state.state == STATE_LOCKED set_node_attribute(door_lock, 1, 257, 0, None) await trigger_subscription_callback(hass, matter_client) @@ -213,9 +214,16 @@ async def test_lock_with_unbolt( assert state assert state.state == STATE_OPENING - set_node_attribute(door_lock_with_unbolt, 1, 257, 3, 0) + set_node_attribute(door_lock_with_unbolt, 1, 257, 0, 0) await trigger_subscription_callback(hass, matter_client) state = hass.states.get("lock.mock_door_lock_lock") assert state - assert state.state == STATE_LOCKED + assert state.state == STATE_UNLOCKED + + set_node_attribute(door_lock_with_unbolt, 1, 257, 0, 3) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("lock.mock_door_lock_lock") + assert state + assert state.state == STATE_OPEN From 586a0b12ab36043c1fa3d7b6ce6c5e85a4204025 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Wed, 24 Jul 2024 17:19:12 +0100 Subject: [PATCH 1670/2411] Fix target service attribute on Mastodon integration (#122546) * Fix target * Fix --- homeassistant/components/mastodon/notify.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mastodon/notify.py b/homeassistant/components/mastodon/notify.py index f15b8c6f0ab..99999275aeb 100644 --- a/homeassistant/components/mastodon/notify.py +++ b/homeassistant/components/mastodon/notify.py @@ -3,7 +3,7 @@ from __future__ import annotations import mimetypes -from typing import Any +from typing import Any, cast from mastodon import Mastodon from mastodon.Mastodon import MastodonAPIError, MastodonUnauthorizedError @@ -71,11 +71,15 @@ class MastodonNotificationService(BaseNotificationService): def send_message(self, message: str = "", **kwargs: Any) -> None: """Toot a message, with media perhaps.""" + + target = None + if (target_list := kwargs.get(ATTR_TARGET)) is not None: + target = cast(list[str], target_list)[0] + data = kwargs.get(ATTR_DATA) media = None mediadata = None - target = None sensitive = False content_warning = None @@ -87,7 +91,6 @@ class MastodonNotificationService(BaseNotificationService): return mediadata = self._upload_media(media) - target = data.get(ATTR_TARGET) sensitive = data.get(ATTR_MEDIA_WARNING) content_warning = data.get(ATTR_CONTENT_WARNING) From 9940d0281bc527e7b85fc7ff9cd1792807c5287a Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Thu, 25 Jul 2024 09:44:56 +1000 Subject: [PATCH 1671/2411] Bump aiolifx to 1.0.6 (#122569) --- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 3d0bd1d73d1..59b336373c2 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -48,7 +48,7 @@ "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"], "requirements": [ - "aiolifx==1.0.5", + "aiolifx==1.0.6", "aiolifx-effects==0.3.2", "aiolifx-themes==0.4.15" ] diff --git a/requirements_all.txt b/requirements_all.txt index e8d846ef099..1195f7b50eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -282,7 +282,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.4.15 # homeassistant.components.lifx -aiolifx==1.0.5 +aiolifx==1.0.6 # homeassistant.components.livisi aiolivisi==0.0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 787b2af38fc..9bfd0311b25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -255,7 +255,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.4.15 # homeassistant.components.lifx -aiolifx==1.0.5 +aiolifx==1.0.6 # homeassistant.components.livisi aiolivisi==0.0.19 From aa44c54a1912ec948adffb19fe295e7e5d56ac22 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 25 Jul 2024 21:23:14 +0200 Subject: [PATCH 1672/2411] Bump deebot-client to 8.2.0 (#122612) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 6ca9b9e3edc..5a21facab71 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==8.1.1"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1195f7b50eb..f8c62aa75b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -709,7 +709,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==8.1.1 +deebot-client==8.2.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bfd0311b25..579f24fb4bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -590,7 +590,7 @@ dbus-fast==2.22.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==8.1.1 +deebot-client==8.2.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 00c3b0d888e5d5427588a83e7b05036dfba83b02 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 29 Jul 2024 11:59:13 +0200 Subject: [PATCH 1673/2411] Bump aiohue to version 4.7.2 (#122651) --- homeassistant/components/hue/manifest.json | 2 +- homeassistant/components/hue/v2/hue_event.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/fixtures/v2_resources.json | 12 ++++++++++++ tests/components/hue/test_device_trigger_v2.py | 9 +++++++-- 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index e8d214da3c8..71aabd4c204 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -11,6 +11,6 @@ "iot_class": "local_push", "loggers": ["aiohue"], "quality_scale": "platinum", - "requirements": ["aiohue==4.7.1"], + "requirements": ["aiohue==4.7.2"], "zeroconf": ["_hue._tcp.local."] } diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index b0e0de234f1..b286a11aade 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -55,7 +55,7 @@ async def async_setup_hue_events(bridge: HueBridge): CONF_ID: slugify(f"{hue_device.metadata.name} Button"), CONF_DEVICE_ID: device.id, # type: ignore[union-attr] CONF_UNIQUE_ID: hue_resource.id, - CONF_TYPE: hue_resource.button.last_event.value, + CONF_TYPE: hue_resource.button.button_report.event.value, CONF_SUBTYPE: hue_resource.metadata.control_id, } hass.bus.async_fire(ATTR_HUE_EVENT, data) @@ -79,7 +79,7 @@ async def async_setup_hue_events(bridge: HueBridge): data = { CONF_DEVICE_ID: device.id, # type: ignore[union-attr] CONF_UNIQUE_ID: hue_resource.id, - CONF_TYPE: hue_resource.relative_rotary.last_event.action.value, + CONF_TYPE: hue_resource.relative_rotary.rotary_report.action.value, CONF_SUBTYPE: hue_resource.relative_rotary.last_event.rotation.direction.value, CONF_DURATION: hue_resource.relative_rotary.last_event.rotation.duration, CONF_STEPS: hue_resource.relative_rotary.last_event.rotation.steps, diff --git a/requirements_all.txt b/requirements_all.txt index f8c62aa75b0..6c7f7bbc000 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -264,7 +264,7 @@ aioharmony==0.2.10 aiohomekit==3.1.5 # homeassistant.components.hue -aiohue==4.7.1 +aiohue==4.7.2 # homeassistant.components.imap aioimaplib==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 579f24fb4bb..fc830f0fe7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -240,7 +240,7 @@ aioharmony==0.2.10 aiohomekit==3.1.5 # homeassistant.components.hue -aiohue==4.7.1 +aiohue==4.7.2 # homeassistant.components.imap aioimaplib==1.1.0 diff --git a/tests/components/hue/fixtures/v2_resources.json b/tests/components/hue/fixtures/v2_resources.json index 662e1107ca9..980086d0988 100644 --- a/tests/components/hue/fixtures/v2_resources.json +++ b/tests/components/hue/fixtures/v2_resources.json @@ -1487,6 +1487,10 @@ "on": { "on": true }, + "owner": { + "rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "rtype": "zone" + }, "type": "grouped_light" }, { @@ -1498,6 +1502,10 @@ "on": { "on": true }, + "owner": { + "rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "rtype": "zone" + }, "type": "grouped_light" }, { @@ -1509,6 +1517,10 @@ "on": { "on": false }, + "owner": { + "rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "rtype": "zone" + }, "type": "grouped_light" }, { diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index 0a89b3263c7..efdc33375a6 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -28,7 +28,12 @@ async def test_hue_event( # Emit button update event btn_event = { - "button": {"last_event": "initial_press"}, + "button": { + "button_report": { + "event": "initial_press", + "updated": "2021-10-01T12:00:00Z", + } + }, "id": "c658d3d8-a013-4b81-8ac6-78b248537e70", "metadata": {"control_id": 1}, "type": "button", @@ -41,7 +46,7 @@ async def test_hue_event( assert len(events) == 1 assert events[0].data["id"] == "wall_switch_with_2_controls_button" assert events[0].data["unique_id"] == btn_event["id"] - assert events[0].data["type"] == btn_event["button"]["last_event"] + assert events[0].data["type"] == btn_event["button"]["button_report"]["event"] assert events[0].data["subtype"] == btn_event["metadata"]["control_id"] From e5fd9819da291fef90726c1a88270586fe05bce2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 26 Jul 2024 16:59:12 +0200 Subject: [PATCH 1674/2411] Return unknown when data is missing in Trafikverket Weather (#122652) Return unknown when data is missing --- .../trafikverket_weatherstation/sensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 4bd14448546..8856482d885 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -61,7 +61,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="air_temp", translation_key="air_temperature", - value_fn=lambda data: data.air_temp or 0, + value_fn=lambda data: data.air_temp, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -69,7 +69,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="road_temp", translation_key="road_temperature", - value_fn=lambda data: data.road_temp or 0, + value_fn=lambda data: data.road_temp, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -91,7 +91,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="wind_speed", - value_fn=lambda data: data.windforce or 0, + value_fn=lambda data: data.windforce, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, @@ -99,7 +99,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="wind_speed_max", translation_key="wind_speed_max", - value_fn=lambda data: data.windforcemax or 0, + value_fn=lambda data: data.windforcemax, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, device_class=SensorDeviceClass.WIND_SPEED, entity_registry_enabled_default=False, @@ -107,7 +107,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="humidity", - value_fn=lambda data: data.humidity or 0, + value_fn=lambda data: data.humidity, native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, entity_registry_enabled_default=False, @@ -115,7 +115,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="precipitation_amount", - value_fn=lambda data: data.precipitation_amount or 0, + value_fn=lambda data: data.precipitation_amount, native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, state_class=SensorStateClass.MEASUREMENT, @@ -130,7 +130,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="dew_point", translation_key="dew_point", - value_fn=lambda data: data.dew_point or 0, + value_fn=lambda data: data.dew_point, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, From d51d584aed3d2adfbde15a0afe133b51a7a45d1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 04:59:31 -0500 Subject: [PATCH 1675/2411] Retry later on OSError during apple_tv entry setup (#122747) --- homeassistant/components/apple_tv/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 4e5c8791acd..08372aa79ae 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -60,6 +60,7 @@ AUTH_EXCEPTIONS = ( exceptions.NoCredentialsError, ) CONNECTION_TIMEOUT_EXCEPTIONS = ( + OSError, asyncio.CancelledError, TimeoutError, exceptions.ConnectionLostError, From 02c592d6afe64ea1f19f0cc1ee7a86a29537b095 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 29 Jul 2024 14:40:02 +0200 Subject: [PATCH 1676/2411] Bump version to 2024.7.4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f706b2d1243..71b7d79cb01 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index f044551ce1e..55f96c3e0b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.7.3" +version = "2024.7.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From d94e79d57a5c57e993590b16638594ba6b5e11bb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 29 Jul 2024 14:49:34 +0200 Subject: [PATCH 1677/2411] Add Macedonian language (#122768) --- homeassistant/generated/languages.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/generated/languages.py b/homeassistant/generated/languages.py index feedd373fd9..78105c76f4c 100644 --- a/homeassistant/generated/languages.py +++ b/homeassistant/generated/languages.py @@ -44,6 +44,7 @@ LANGUAGES = { "lb", "lt", "lv", + "mk", "ml", "nb", "nl", From 9514a38320224d1e7e75b3f98d6f545462f1dd6f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:22:08 +0200 Subject: [PATCH 1678/2411] Fix implicit-return rule in zha tests (#122772) --- tests/components/zha/common.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 2958c92c81f..1dd1e5f81aa 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, Mock import zigpy.zcl import zigpy.zcl.foundation as zcl_f +from homeassistant.components.zha.helpers import ZHADeviceProxy from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util @@ -123,7 +124,9 @@ async def send_attributes_report( await hass.async_block_till_done() -def find_entity_id(domain, zha_device, hass: HomeAssistant, qualifier=None): +def find_entity_id( + domain: str, zha_device: ZHADeviceProxy, hass: HomeAssistant, qualifier=None +) -> str | None: """Find the entity id under the testing. This is used to get the entity id in order to get the state from the state @@ -136,11 +139,13 @@ def find_entity_id(domain, zha_device, hass: HomeAssistant, qualifier=None): for entity_id in entities: if qualifier in entity_id: return entity_id - else: - return entities[0] + return None + return entities[0] -def find_entity_ids(domain, zha_device, hass: HomeAssistant): +def find_entity_ids( + domain: str, zha_device: ZHADeviceProxy, hass: HomeAssistant +) -> list[str]: """Find the entity ids under the testing. This is used to get the entity id in order to get the state from the state From ea75c8864fdd1f4dc3093da6cb7e39edff5e48e7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 29 Jul 2024 15:52:18 +0200 Subject: [PATCH 1679/2411] Remove support for live schema migration of old recorder databases (#122399) * Remove support for live schema migration of old recorder databases * Update test --- .../components/recorder/migration.py | 5 +- tests/components/recorder/test_migrate.py | 70 +++++++++++++++---- .../components/recorder/test_websocket_api.py | 4 +- tests/conftest.py | 17 +++-- 4 files changed, 77 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index d7c5e7f0ea0..2932ea484c9 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -119,7 +119,10 @@ from .util import ( if TYPE_CHECKING: from . import Recorder -LIVE_MIGRATION_MIN_SCHEMA_VERSION = 0 +# Live schema migration supported starting from schema version 42 or newer +# Schema version 41 was introduced in HA Core 2023.4 +# Schema version 42 was introduced in HA Core 2023.11 +LIVE_MIGRATION_MIN_SCHEMA_VERSION = 42 MIGRATION_NOTE_OFFLINE = ( "Note: this may take several hours on large databases and slow machines. " diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 3eea231a659..dc99ddefa3b 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -87,10 +87,23 @@ async def test_schema_update_calls( call(instance, hass, engine, session_maker, version + 1, 0) for version in range(db_schema.SCHEMA_VERSION) ] - status = migration.SchemaValidationStatus(0, True, set(), 0) assert migrate_schema.mock_calls == [ - call(instance, hass, engine, session_maker, status, 0), - call(instance, hass, engine, session_maker, status, db_schema.SCHEMA_VERSION), + call( + instance, + hass, + engine, + session_maker, + migration.SchemaValidationStatus(0, True, set(), 0), + 42, + ), + call( + instance, + hass, + engine, + session_maker, + migration.SchemaValidationStatus(42, True, set(), 0), + db_schema.SCHEMA_VERSION, + ), ] @@ -117,7 +130,9 @@ async def test_migration_in_progress( new=create_engine_test, ), ): - await async_setup_recorder_instance(hass, wait_recorder=False) + await async_setup_recorder_instance( + hass, wait_recorder=False, wait_recorder_setup=False + ) await hass.async_add_executor_job(instrument_migration.migration_started.wait) assert recorder.util.async_migration_in_progress(hass) is True @@ -129,8 +144,25 @@ async def test_migration_in_progress( assert recorder.get_instance(hass).schema_version == SCHEMA_VERSION +@pytest.mark.parametrize( + ( + "func_to_patch", + "expected_setup_result", + "expected_pn_create", + "expected_pn_dismiss", + ), + [ + ("migrate_schema_non_live", False, 1, 0), + ("migrate_schema_live", True, 2, 1), + ], +) async def test_database_migration_failed( - hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator + hass: HomeAssistant, + async_setup_recorder_instance: RecorderInstanceGenerator, + func_to_patch: str, + expected_setup_result: bool, + expected_pn_create: int, + expected_pn_dismiss: int, ) -> None: """Test we notify if the migration fails.""" assert recorder.util.async_migration_in_progress(hass) is False @@ -141,7 +173,7 @@ async def test_database_migration_failed( new=create_engine_test, ), patch( - "homeassistant.components.recorder.migration._apply_update", + f"homeassistant.components.recorder.migration.{func_to_patch}", side_effect=ValueError, ), patch( @@ -153,7 +185,9 @@ async def test_database_migration_failed( side_effect=pn.dismiss, ) as mock_dismiss, ): - await async_setup_recorder_instance(hass, wait_recorder=False) + await async_setup_recorder_instance( + hass, wait_recorder=False, expected_setup_result=expected_setup_result + ) hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() @@ -161,8 +195,8 @@ async def test_database_migration_failed( await hass.async_block_till_done() assert recorder.util.async_migration_in_progress(hass) is False - assert len(mock_create.mock_calls) == 2 - assert len(mock_dismiss.mock_calls) == 1 + assert len(mock_create.mock_calls) == expected_pn_create + assert len(mock_dismiss.mock_calls) == expected_pn_dismiss @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) @@ -346,7 +380,7 @@ async def test_events_during_migration_are_queued( ), ): await async_setup_recorder_instance( - hass, {"commit_interval": 0}, wait_recorder=False + hass, {"commit_interval": 0}, wait_recorder=False, wait_recorder_setup=False ) await hass.async_add_executor_job(instrument_migration.migration_started.wait) assert recorder.util.async_migration_in_progress(hass) is True @@ -389,7 +423,7 @@ async def test_events_during_migration_queue_exhausted( ), ): await async_setup_recorder_instance( - hass, {"commit_interval": 0}, wait_recorder=False + hass, {"commit_interval": 0}, wait_recorder=False, wait_recorder_setup=False ) await hass.async_add_executor_job(instrument_migration.migration_started.wait) assert recorder.util.async_migration_in_progress(hass) is True @@ -421,7 +455,15 @@ async def test_events_during_migration_queue_exhausted( @pytest.mark.parametrize( ("start_version", "live"), - [(0, True), (9, True), (16, True), (18, True), (22, True), (25, True), (43, True)], + [ + (0, False), + (9, False), + (16, False), + (18, False), + (22, False), + (25, False), + (43, True), + ], ) async def test_schema_migrate( hass: HomeAssistant, @@ -500,7 +542,9 @@ async def test_schema_migrate( "homeassistant.components.recorder.Recorder._pre_process_startup_events", ), ): - await async_setup_recorder_instance(hass, wait_recorder=False) + await async_setup_recorder_instance( + hass, wait_recorder=False, wait_recorder_setup=live + ) await hass.async_add_executor_job(instrument_migration.migration_started.wait) assert recorder.util.async_migration_in_progress(hass) is True await recorder_helper.async_wait_recorder(hass) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index ed36f4dacbf..8efbf226bc1 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -2555,7 +2555,9 @@ async def test_recorder_info_migration_queue_exhausted( recorder.core, "MIN_AVAILABLE_MEMORY_FOR_QUEUE_BACKLOG", sys.maxsize ), ): - async with async_test_recorder(hass, wait_recorder=False): + async with async_test_recorder( + hass, wait_recorder=False, wait_recorder_setup=False + ): await hass.async_add_executor_job( instrument_migration.migration_started.wait ) diff --git a/tests/conftest.py b/tests/conftest.py index de0dbc2e0d2..0d0fd826b44 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1399,6 +1399,7 @@ async def _async_init_recorder_component( db_url: str | None = None, *, expected_setup_result: bool, + wait_setup: bool, ) -> None: """Initialize the recorder asynchronously.""" # pylint: disable-next=import-outside-toplevel @@ -1416,10 +1417,14 @@ async def _async_init_recorder_component( setup_task = asyncio.ensure_future( async_setup_component(hass, recorder.DOMAIN, {recorder.DOMAIN: config}) ) - # Wait for recorder integration to setup - setup_result = await setup_task - assert setup_result == expected_setup_result - assert (recorder.DOMAIN in hass.config.components) == expected_setup_result + if wait_setup: + # Wait for recorder integration to setup + setup_result = await setup_task + assert setup_result == expected_setup_result + assert (recorder.DOMAIN in hass.config.components) == expected_setup_result + else: + # Wait for recorder to connect to the database + await recorder_helper.async_wait_recorder(hass) _LOGGER.info( "Test recorder successfully started, database location: %s", config[recorder.CONF_DB_URL], @@ -1585,6 +1590,7 @@ async def async_test_recorder( *, expected_setup_result: bool = True, wait_recorder: bool = True, + wait_recorder_setup: bool = True, ) -> AsyncGenerator[recorder.Recorder]: """Setup and return recorder instance.""" # noqa: D401 await _async_init_recorder_component( @@ -1592,6 +1598,7 @@ async def async_test_recorder( config, recorder_db_url, expected_setup_result=expected_setup_result, + wait_setup=wait_recorder_setup, ) await hass.async_block_till_done() instance = hass.data[recorder.DATA_INSTANCE] @@ -1621,6 +1628,7 @@ async def async_setup_recorder_instance( *, expected_setup_result: bool = True, wait_recorder: bool = True, + wait_recorder_setup: bool = True, ) -> AsyncGenerator[recorder.Recorder]: """Set up and return recorder instance.""" @@ -1630,6 +1638,7 @@ async def async_setup_recorder_instance( config, expected_setup_result=expected_setup_result, wait_recorder=wait_recorder, + wait_recorder_setup=wait_recorder_setup, ) ) From 732b9e47c8fd8a1b460c5e23f776e684c124768b Mon Sep 17 00:00:00 2001 From: Christian Neumeier <47736781+NECH2004@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:48:58 +0200 Subject: [PATCH 1680/2411] Add missing variable 'energy_today' to Zeversolar diagnostics. (#122786) added var 'energy_today' to zeversolar diagnostics. --- homeassistant/components/zeversolar/diagnostics.py | 1 + tests/components/zeversolar/snapshots/test_diagnostics.ambr | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/zeversolar/diagnostics.py b/homeassistant/components/zeversolar/diagnostics.py index b8901a7e793..6e6ed262f51 100644 --- a/homeassistant/components/zeversolar/diagnostics.py +++ b/homeassistant/components/zeversolar/diagnostics.py @@ -31,6 +31,7 @@ async def async_get_config_entry_diagnostics( "num_inverters": data.num_inverters, "serial_number": data.serial_number, "pac": data.pac, + "energy_today": data.energy_today, "status": data.status.value, "meter_status": data.meter_status.value, } diff --git a/tests/components/zeversolar/snapshots/test_diagnostics.ambr b/tests/components/zeversolar/snapshots/test_diagnostics.ambr index eebc8468076..4090a3262ba 100644 --- a/tests/components/zeversolar/snapshots/test_diagnostics.ambr +++ b/tests/components/zeversolar/snapshots/test_diagnostics.ambr @@ -10,6 +10,7 @@ # name: test_entry_diagnostics dict({ 'communication_status': 'OK', + 'energy_today': 123.4, 'hardware_version': 'M10', 'meter_status': 'OK', 'num_inverters': 1, From 570725293ca455083677f14c170c60f32a609f7c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:13:31 +0200 Subject: [PATCH 1681/2411] Fix implicit-return in arcam_fmj tests (#122792) --- tests/components/arcam_fmj/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py index 6c73b5c763a..ca4af1b00a3 100644 --- a/tests/components/arcam_fmj/conftest.py +++ b/tests/components/arcam_fmj/conftest.py @@ -99,6 +99,7 @@ async def player_setup_fixture( return state_1 if zone == 2: return state_2 + raise ValueError(f"Unknown player zone: {zone}") await async_setup_component(hass, "homeassistant", {}) From 1f488b00f85003a17752b5c15f86495c029bc382 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 29 Jul 2024 12:39:25 -0400 Subject: [PATCH 1682/2411] Abstract SkyConnect firmware config flow to the hardware platform (#122140) * Move the SkyConnect config flow to hardware; * Clean up * Get SkyConnect unit tests passing * Split apart `test_util.py` * Migrate `test_config_flow` * Remove unnecessary constants * Re-apply `contextmanager` typing from #122250 * Move the SkyConnect translation strings into hardware --- .../homeassistant_hardware/const.py | 10 + .../firmware_config_flow.py | 557 +++++++++++++++ .../homeassistant_hardware/strings.json | 61 ++ .../components/homeassistant_hardware/util.py | 138 ++++ .../homeassistant_sky_connect/__init__.py | 3 +- .../homeassistant_sky_connect/config_flow.py | 558 +-------------- .../homeassistant_sky_connect/const.py | 10 - .../homeassistant_sky_connect/strings.json | 108 +-- .../homeassistant_sky_connect/util.py | 132 +--- .../test_config_flow.py | 674 ++++++++++++++++++ .../test_config_flow_failures.py | 227 ++---- .../homeassistant_hardware/test_util.py | 158 ++++ .../test_config_flow.py | 641 ++--------------- .../homeassistant_sky_connect/test_init.py | 2 +- .../homeassistant_sky_connect/test_util.py | 152 ---- 15 files changed, 1803 insertions(+), 1628 deletions(-) create mode 100644 homeassistant/components/homeassistant_hardware/firmware_config_flow.py create mode 100644 homeassistant/components/homeassistant_hardware/util.py create mode 100644 tests/components/homeassistant_hardware/test_config_flow.py rename tests/components/{homeassistant_sky_connect => homeassistant_hardware}/test_config_flow_failures.py (73%) create mode 100644 tests/components/homeassistant_hardware/test_util.py diff --git a/homeassistant/components/homeassistant_hardware/const.py b/homeassistant/components/homeassistant_hardware/const.py index e4aa7c80f8d..8fddbe41b7d 100644 --- a/homeassistant/components/homeassistant_hardware/const.py +++ b/homeassistant/components/homeassistant_hardware/const.py @@ -4,5 +4,15 @@ import logging LOGGER = logging.getLogger(__package__) +ZHA_DOMAIN = "zha" + +OTBR_ADDON_NAME = "OpenThread Border Router" +OTBR_ADDON_MANAGER_DATA = "openthread_border_router" +OTBR_ADDON_SLUG = "core_openthread_border_router" + +ZIGBEE_FLASHER_ADDON_NAME = "Silicon Labs Flasher" +ZIGBEE_FLASHER_ADDON_MANAGER_DATA = "silabs_flasher" +ZIGBEE_FLASHER_ADDON_SLUG = "core_silabs_flasher" + SILABS_MULTIPROTOCOL_ADDON_SLUG = "core_silabs_multiprotocol" SILABS_FLASHER_ADDON_SLUG = "core_silabs_flasher" diff --git a/homeassistant/components/homeassistant_hardware/firmware_config_flow.py b/homeassistant/components/homeassistant_hardware/firmware_config_flow.py new file mode 100644 index 00000000000..b8dc4227ece --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/firmware_config_flow.py @@ -0,0 +1,557 @@ +"""Config flow for the Home Assistant SkyConnect integration.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +import asyncio +import logging +from typing import Any + +from universal_silabs_flasher.const import ApplicationType + +from homeassistant.components.hassio import ( + AddonError, + AddonInfo, + AddonManager, + AddonState, + is_hassio, +) +from homeassistant.components.zha.repairs.wrong_silabs_firmware import ( + probe_silabs_firmware_type, +) +from homeassistant.config_entries import ( + ConfigEntry, + ConfigEntryBaseFlow, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, + OptionsFlowWithConfigEntry, +) +from homeassistant.core import callback +from homeassistant.data_entry_flow import AbortFlow + +from . import silabs_multiprotocol_addon +from .const import ZHA_DOMAIN +from .util import ( + get_otbr_addon_manager, + get_zha_device_path, + get_zigbee_flasher_addon_manager, +) + +_LOGGER = logging.getLogger(__name__) + +STEP_PICK_FIRMWARE_THREAD = "pick_firmware_thread" +STEP_PICK_FIRMWARE_ZIGBEE = "pick_firmware_zigbee" + + +class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC): + """Base flow to install firmware.""" + + _failed_addon_name: str + _failed_addon_reason: str + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Instantiate base flow.""" + super().__init__(*args, **kwargs) + + self._probed_firmware_type: ApplicationType | None = None + self._device: str | None = None # To be set in a subclass + self._hardware_name: str = "unknown" # To be set in a subclass + + self.addon_install_task: asyncio.Task | None = None + self.addon_start_task: asyncio.Task | None = None + self.addon_uninstall_task: asyncio.Task | None = None + + def _get_translation_placeholders(self) -> dict[str, str]: + """Shared translation placeholders.""" + placeholders = { + "firmware_type": ( + self._probed_firmware_type.value + if self._probed_firmware_type is not None + else "unknown" + ), + "model": self._hardware_name, + } + + self.context["title_placeholders"] = placeholders + + return placeholders + + async def _async_set_addon_config( + self, config: dict, addon_manager: AddonManager + ) -> None: + """Set add-on config.""" + try: + await addon_manager.async_set_addon_options(config) + except AddonError as err: + _LOGGER.error(err) + raise AbortFlow( + "addon_set_config_failed", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": addon_manager.addon_name, + }, + ) from err + + async def _async_get_addon_info(self, addon_manager: AddonManager) -> AddonInfo: + """Return add-on info.""" + try: + addon_info = await addon_manager.async_get_addon_info() + except AddonError as err: + _LOGGER.error(err) + raise AbortFlow( + "addon_info_failed", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": addon_manager.addon_name, + }, + ) from err + + return addon_info + + async def async_step_pick_firmware( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Thread or Zigbee firmware.""" + return self.async_show_menu( + step_id="pick_firmware", + menu_options=[ + STEP_PICK_FIRMWARE_ZIGBEE, + STEP_PICK_FIRMWARE_THREAD, + ], + description_placeholders=self._get_translation_placeholders(), + ) + + async def _probe_firmware_type(self) -> bool: + """Probe the firmware currently on the device.""" + assert self._device is not None + + self._probed_firmware_type = await probe_silabs_firmware_type( + self._device, + probe_methods=( + # We probe in order of frequency: Zigbee, Thread, then multi-PAN + ApplicationType.GECKO_BOOTLOADER, + ApplicationType.EZSP, + ApplicationType.SPINEL, + ApplicationType.CPC, + ), + ) + + return self._probed_firmware_type in ( + ApplicationType.EZSP, + ApplicationType.SPINEL, + ApplicationType.CPC, + ) + + async def async_step_pick_firmware_zigbee( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Zigbee firmware.""" + if not await self._probe_firmware_type(): + return self.async_abort( + reason="unsupported_firmware", + description_placeholders=self._get_translation_placeholders(), + ) + + # Allow the stick to be used with ZHA without flashing + if self._probed_firmware_type == ApplicationType.EZSP: + return await self.async_step_confirm_zigbee() + + if not is_hassio(self.hass): + return self.async_abort( + reason="not_hassio", + description_placeholders=self._get_translation_placeholders(), + ) + + # Only flash new firmware if we need to + fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(fw_flasher_manager) + + if addon_info.state == AddonState.NOT_INSTALLED: + return await self.async_step_install_zigbee_flasher_addon() + + if addon_info.state == AddonState.NOT_RUNNING: + return await self.async_step_run_zigbee_flasher_addon() + + # If the addon is already installed and running, fail + return self.async_abort( + reason="addon_already_running", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": fw_flasher_manager.addon_name, + }, + ) + + async def async_step_install_zigbee_flasher_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Show progress dialog for installing the Zigbee flasher addon.""" + return await self._install_addon( + get_zigbee_flasher_addon_manager(self.hass), + "install_zigbee_flasher_addon", + "run_zigbee_flasher_addon", + ) + + async def _install_addon( + self, + addon_manager: silabs_multiprotocol_addon.WaitingAddonManager, + step_id: str, + next_step_id: str, + ) -> ConfigFlowResult: + """Show progress dialog for installing an addon.""" + addon_info = await self._async_get_addon_info(addon_manager) + + _LOGGER.debug("Flasher addon state: %s", addon_info) + + if not self.addon_install_task: + self.addon_install_task = self.hass.async_create_task( + addon_manager.async_install_addon_waiting(), + "Addon install", + ) + + if not self.addon_install_task.done(): + return self.async_show_progress( + step_id=step_id, + progress_action="install_addon", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": addon_manager.addon_name, + }, + progress_task=self.addon_install_task, + ) + + try: + await self.addon_install_task + except AddonError as err: + _LOGGER.error(err) + self._failed_addon_name = addon_manager.addon_name + self._failed_addon_reason = "addon_install_failed" + return self.async_show_progress_done(next_step_id="addon_operation_failed") + finally: + self.addon_install_task = None + + return self.async_show_progress_done(next_step_id=next_step_id) + + async def async_step_addon_operation_failed( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Abort when add-on installation or start failed.""" + return self.async_abort( + reason=self._failed_addon_reason, + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": self._failed_addon_name, + }, + ) + + async def async_step_run_zigbee_flasher_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Configure the flasher addon to point to the SkyConnect and run it.""" + fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(fw_flasher_manager) + + assert self._device is not None + new_addon_config = { + **addon_info.options, + "device": self._device, + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + } + + _LOGGER.debug("Reconfiguring flasher addon with %s", new_addon_config) + await self._async_set_addon_config(new_addon_config, fw_flasher_manager) + + if not self.addon_start_task: + + async def start_and_wait_until_done() -> None: + await fw_flasher_manager.async_start_addon_waiting() + # Now that the addon is running, wait for it to finish + await fw_flasher_manager.async_wait_until_addon_state( + AddonState.NOT_RUNNING + ) + + self.addon_start_task = self.hass.async_create_task( + start_and_wait_until_done() + ) + + if not self.addon_start_task.done(): + return self.async_show_progress( + step_id="run_zigbee_flasher_addon", + progress_action="run_zigbee_flasher_addon", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": fw_flasher_manager.addon_name, + }, + progress_task=self.addon_start_task, + ) + + try: + await self.addon_start_task + except (AddonError, AbortFlow) as err: + _LOGGER.error(err) + self._failed_addon_name = fw_flasher_manager.addon_name + self._failed_addon_reason = "addon_start_failed" + return self.async_show_progress_done(next_step_id="addon_operation_failed") + finally: + self.addon_start_task = None + + return self.async_show_progress_done( + next_step_id="uninstall_zigbee_flasher_addon" + ) + + async def async_step_uninstall_zigbee_flasher_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Uninstall the flasher addon.""" + fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass) + + if not self.addon_uninstall_task: + _LOGGER.debug("Uninstalling flasher addon") + self.addon_uninstall_task = self.hass.async_create_task( + fw_flasher_manager.async_uninstall_addon_waiting() + ) + + if not self.addon_uninstall_task.done(): + return self.async_show_progress( + step_id="uninstall_zigbee_flasher_addon", + progress_action="uninstall_zigbee_flasher_addon", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": fw_flasher_manager.addon_name, + }, + progress_task=self.addon_uninstall_task, + ) + + try: + await self.addon_uninstall_task + except (AddonError, AbortFlow) as err: + _LOGGER.error(err) + # The uninstall failing isn't critical so we can just continue + finally: + self.addon_uninstall_task = None + + return self.async_show_progress_done(next_step_id="confirm_zigbee") + + async def async_step_confirm_zigbee( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm Zigbee setup.""" + assert self._device is not None + assert self._hardware_name is not None + self._probed_firmware_type = ApplicationType.EZSP + + if user_input is not None: + await self.hass.config_entries.flow.async_init( + ZHA_DOMAIN, + context={"source": "hardware"}, + data={ + "name": self._hardware_name, + "port": { + "path": self._device, + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "ezsp", + }, + ) + + return self._async_flow_finished() + + return self.async_show_form( + step_id="confirm_zigbee", + description_placeholders=self._get_translation_placeholders(), + ) + + async def async_step_pick_firmware_thread( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Thread firmware.""" + if not await self._probe_firmware_type(): + return self.async_abort( + reason="unsupported_firmware", + description_placeholders=self._get_translation_placeholders(), + ) + + # We install the OTBR addon no matter what, since it is required to use Thread + if not is_hassio(self.hass): + return self.async_abort( + reason="not_hassio_thread", + description_placeholders=self._get_translation_placeholders(), + ) + + otbr_manager = get_otbr_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(otbr_manager) + + if addon_info.state == AddonState.NOT_INSTALLED: + return await self.async_step_install_otbr_addon() + + if addon_info.state == AddonState.NOT_RUNNING: + return await self.async_step_start_otbr_addon() + + # If the addon is already installed and running, fail + return self.async_abort( + reason="otbr_addon_already_running", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": otbr_manager.addon_name, + }, + ) + + async def async_step_install_otbr_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Show progress dialog for installing the OTBR addon.""" + return await self._install_addon( + get_otbr_addon_manager(self.hass), "install_otbr_addon", "start_otbr_addon" + ) + + async def async_step_start_otbr_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Configure OTBR to point to the SkyConnect and run the addon.""" + otbr_manager = get_otbr_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(otbr_manager) + + assert self._device is not None + new_addon_config = { + **addon_info.options, + "device": self._device, + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + } + + _LOGGER.debug("Reconfiguring OTBR addon with %s", new_addon_config) + await self._async_set_addon_config(new_addon_config, otbr_manager) + + if not self.addon_start_task: + self.addon_start_task = self.hass.async_create_task( + otbr_manager.async_start_addon_waiting() + ) + + if not self.addon_start_task.done(): + return self.async_show_progress( + step_id="start_otbr_addon", + progress_action="start_otbr_addon", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": otbr_manager.addon_name, + }, + progress_task=self.addon_start_task, + ) + + try: + await self.addon_start_task + except (AddonError, AbortFlow) as err: + _LOGGER.error(err) + self._failed_addon_name = otbr_manager.addon_name + self._failed_addon_reason = "addon_start_failed" + return self.async_show_progress_done(next_step_id="addon_operation_failed") + finally: + self.addon_start_task = None + + return self.async_show_progress_done(next_step_id="confirm_otbr") + + async def async_step_confirm_otbr( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm OTBR setup.""" + assert self._device is not None + + self._probed_firmware_type = ApplicationType.SPINEL + + if user_input is not None: + # OTBR discovery is done automatically via hassio + return self._async_flow_finished() + + return self.async_show_form( + step_id="confirm_otbr", + description_placeholders=self._get_translation_placeholders(), + ) + + @abstractmethod + def _async_flow_finished(self) -> ConfigFlowResult: + """Finish the flow.""" + raise NotImplementedError + + +class BaseFirmwareConfigFlow(BaseFirmwareInstallFlow, ConfigFlow): + """Base config flow for installing firmware.""" + + @staticmethod + @callback + @abstractmethod + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlow: + """Return the options flow.""" + raise NotImplementedError + + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm a discovery.""" + return await self.async_step_pick_firmware() + + +class BaseFirmwareOptionsFlow(BaseFirmwareInstallFlow, OptionsFlowWithConfigEntry): + """Zigbee and Thread options flow handlers.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Instantiate options flow.""" + super().__init__(*args, **kwargs) + + self._probed_firmware_type = ApplicationType(self.config_entry.data["firmware"]) + + # Make `context` a regular dictionary + self.context = {} + + # Subclasses are expected to override `_device` and `_hardware_name` + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options flow.""" + return await self.async_step_pick_firmware() + + async def async_step_pick_firmware_zigbee( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Zigbee firmware.""" + assert self._device is not None + + if is_hassio(self.hass): + otbr_manager = get_otbr_addon_manager(self.hass) + otbr_addon_info = await self._async_get_addon_info(otbr_manager) + + if ( + otbr_addon_info.state != AddonState.NOT_INSTALLED + and otbr_addon_info.options.get("device") == self._device + ): + raise AbortFlow( + "otbr_still_using_stick", + description_placeholders=self._get_translation_placeholders(), + ) + + return await super().async_step_pick_firmware_zigbee(user_input) + + async def async_step_pick_firmware_thread( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Thread firmware.""" + assert self._device is not None + + for zha_entry in self.hass.config_entries.async_entries( + ZHA_DOMAIN, + include_ignore=False, + include_disabled=True, + ): + if get_zha_device_path(zha_entry) == self._device: + raise AbortFlow( + "zha_still_using_stick", + description_placeholders=self._get_translation_placeholders(), + ) + + return await super().async_step_pick_firmware_thread(user_input) diff --git a/homeassistant/components/homeassistant_hardware/strings.json b/homeassistant/components/homeassistant_hardware/strings.json index a66e4879f68..dbbb2057323 100644 --- a/homeassistant/components/homeassistant_hardware/strings.json +++ b/homeassistant/components/homeassistant_hardware/strings.json @@ -1,4 +1,65 @@ { + "firmware_picker": { + "options": { + "step": { + "pick_firmware": { + "title": "Pick your firmware", + "description": "Let's get started with setting up your {model}. Do you want to use it to set up a Zigbee or Thread network?", + "menu_options": { + "pick_firmware_zigbee": "Zigbee", + "pick_firmware_thread": "Thread" + } + }, + "install_zigbee_flasher_addon": { + "title": "Installing flasher", + "description": "Installing the Silicon Labs Flasher add-on." + }, + "run_zigbee_flasher_addon": { + "title": "Installing Zigbee firmware", + "description": "Installing Zigbee firmware. This will take about a minute." + }, + "uninstall_zigbee_flasher_addon": { + "title": "Removing flasher", + "description": "Removing the Silicon Labs Flasher add-on." + }, + "zigbee_flasher_failed": { + "title": "Zigbee installation failed", + "description": "The Zigbee firmware installation process was unsuccessful. Ensure no other software is trying to communicate with the {model} and try again." + }, + "confirm_zigbee": { + "title": "Zigbee setup complete", + "description": "Your {model} is now a Zigbee coordinator and will be shown as discovered by the Zigbee Home Automation integration once you exit." + }, + "install_otbr_addon": { + "title": "Installing OpenThread Border Router add-on", + "description": "The OpenThread Border Router (OTBR) add-on is being installed." + }, + "start_otbr_addon": { + "title": "Starting OpenThread Border Router add-on", + "description": "The OpenThread Border Router (OTBR) add-on is now starting." + }, + "otbr_failed": { + "title": "Failed to setup OpenThread Border Router", + "description": "The OpenThread Border Router add-on installation was unsuccessful. Ensure no other software is trying to communicate with the {model}, you have access to the internet and can install other add-ons, and try again. Check the Supervisor logs if the problem persists." + }, + "confirm_otbr": { + "title": "OpenThread Border Router setup complete", + "description": "Your {model} is now an OpenThread Border Router and will show up in the Thread integration once you exit." + } + }, + "abort": { + "not_hassio_thread": "The OpenThread Border Router addon can only be installed with Home Assistant OS. If you would like to use the {model} as an Thread border router, please flash the firmware manually using the [web flasher]({docs_web_flasher_url}) and set up OpenThread Border Router to communicate with it.", + "otbr_addon_already_running": "The OpenThread Border Router add-on is already running, it cannot be installed again.", + "zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again.", + "otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again." + }, + "progress": { + "install_zigbee_flasher_addon": "The Silicon Labs Flasher addon is installed, this may take a few minutes.", + "run_zigbee_flasher_addon": "Please wait while Zigbee firmware is installed to your {model}, this will take a few minutes. Do not make any changes to your hardware or software until this finishes.", + "uninstall_zigbee_flasher_addon": "The Silicon Labs Flasher addon is being removed." + } + } + }, "silabs_multiprotocol_hardware": { "options": { "step": { diff --git a/homeassistant/components/homeassistant_hardware/util.py b/homeassistant/components/homeassistant_hardware/util.py new file mode 100644 index 00000000000..90cfee076e3 --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/util.py @@ -0,0 +1,138 @@ +"""Utility functions for Home Assistant SkyConnect integration.""" + +from __future__ import annotations + +from collections import defaultdict +from dataclasses import dataclass +import logging +from typing import cast + +from universal_silabs_flasher.const import ApplicationType + +from homeassistant.components.hassio import AddonError, AddonState, is_hassio +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.singleton import singleton + +from .const import ( + OTBR_ADDON_MANAGER_DATA, + OTBR_ADDON_NAME, + OTBR_ADDON_SLUG, + ZHA_DOMAIN, + ZIGBEE_FLASHER_ADDON_MANAGER_DATA, + ZIGBEE_FLASHER_ADDON_NAME, + ZIGBEE_FLASHER_ADDON_SLUG, +) +from .silabs_multiprotocol_addon import ( + WaitingAddonManager, + get_multiprotocol_addon_manager, +) + +_LOGGER = logging.getLogger(__name__) + + +def get_zha_device_path(config_entry: ConfigEntry) -> str | None: + """Get the device path from a ZHA config entry.""" + return cast(str | None, config_entry.data.get("device", {}).get("path", None)) + + +@singleton(OTBR_ADDON_MANAGER_DATA) +@callback +def get_otbr_addon_manager(hass: HomeAssistant) -> WaitingAddonManager: + """Get the OTBR add-on manager.""" + return WaitingAddonManager( + hass, + _LOGGER, + OTBR_ADDON_NAME, + OTBR_ADDON_SLUG, + ) + + +@singleton(ZIGBEE_FLASHER_ADDON_MANAGER_DATA) +@callback +def get_zigbee_flasher_addon_manager(hass: HomeAssistant) -> WaitingAddonManager: + """Get the flasher add-on manager.""" + return WaitingAddonManager( + hass, + _LOGGER, + ZIGBEE_FLASHER_ADDON_NAME, + ZIGBEE_FLASHER_ADDON_SLUG, + ) + + +@dataclass(slots=True, kw_only=True) +class FirmwareGuess: + """Firmware guess.""" + + is_running: bool + firmware_type: ApplicationType + source: str + + +async def guess_firmware_type(hass: HomeAssistant, device_path: str) -> FirmwareGuess: + """Guess the firmware type based on installed addons and other integrations.""" + device_guesses: defaultdict[str | None, list[FirmwareGuess]] = defaultdict(list) + + for zha_config_entry in hass.config_entries.async_entries(ZHA_DOMAIN): + zha_path = get_zha_device_path(zha_config_entry) + + if zha_path is not None: + device_guesses[zha_path].append( + FirmwareGuess( + is_running=(zha_config_entry.state == ConfigEntryState.LOADED), + firmware_type=ApplicationType.EZSP, + source="zha", + ) + ) + + if is_hassio(hass): + otbr_addon_manager = get_otbr_addon_manager(hass) + + try: + otbr_addon_info = await otbr_addon_manager.async_get_addon_info() + except AddonError: + pass + else: + if otbr_addon_info.state != AddonState.NOT_INSTALLED: + otbr_path = otbr_addon_info.options.get("device") + device_guesses[otbr_path].append( + FirmwareGuess( + is_running=(otbr_addon_info.state == AddonState.RUNNING), + firmware_type=ApplicationType.SPINEL, + source="otbr", + ) + ) + + multipan_addon_manager = await get_multiprotocol_addon_manager(hass) + + try: + multipan_addon_info = await multipan_addon_manager.async_get_addon_info() + except AddonError: + pass + else: + if multipan_addon_info.state != AddonState.NOT_INSTALLED: + multipan_path = multipan_addon_info.options.get("device") + device_guesses[multipan_path].append( + FirmwareGuess( + is_running=(multipan_addon_info.state == AddonState.RUNNING), + firmware_type=ApplicationType.CPC, + source="multiprotocol", + ) + ) + + # Fall back to EZSP if we can't guess the firmware type + if device_path not in device_guesses: + return FirmwareGuess( + is_running=False, firmware_type=ApplicationType.EZSP, source="unknown" + ) + + # Prioritizes guesses that were pulled from a running addon or integration but keep + # the sort order we defined above + guesses = sorted( + device_guesses[device_path], + key=lambda guess: guess.is_running, + ) + + assert guesses + + return guesses[-1] diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index fc02f31f263..43d42e4fa59 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -4,11 +4,10 @@ from __future__ import annotations import logging +from homeassistant.components.homeassistant_hardware.util import guess_firmware_type from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .util import guess_firmware_type - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index 8eeb703248a..b1776624736 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -2,82 +2,46 @@ from __future__ import annotations -from abc import ABC, abstractmethod -import asyncio import logging -from typing import Any +from typing import TYPE_CHECKING, Any, Protocol from universal_silabs_flasher.const import ApplicationType from homeassistant.components import usb -from homeassistant.components.hassio import ( - AddonError, - AddonInfo, - AddonManager, - AddonState, - is_hassio, -) -from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon -from homeassistant.components.zha.repairs.wrong_silabs_firmware import ( - probe_silabs_firmware_type, -) -from homeassistant.config_entries import ( - ConfigEntry, - ConfigEntryBaseFlow, - ConfigFlow, - ConfigFlowResult, - OptionsFlow, - OptionsFlowWithConfigEntry, +from homeassistant.components.homeassistant_hardware import ( + firmware_config_flow, + silabs_multiprotocol_addon, ) +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult, OptionsFlow from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow -from .const import DOCS_WEB_FLASHER_URL, DOMAIN, ZHA_DOMAIN, HardwareVariant -from .util import ( - get_hardware_variant, - get_otbr_addon_manager, - get_usb_service_info, - get_zha_device_path, - get_zigbee_flasher_addon_manager, -) +from .const import DOCS_WEB_FLASHER_URL, DOMAIN, HardwareVariant +from .util import get_hardware_variant, get_usb_service_info _LOGGER = logging.getLogger(__name__) -STEP_PICK_FIRMWARE_THREAD = "pick_firmware_thread" -STEP_PICK_FIRMWARE_ZIGBEE = "pick_firmware_zigbee" + +if TYPE_CHECKING: + + class TranslationPlaceholderProtocol(Protocol): + """Protocol describing `BaseFirmwareInstallFlow`'s translation placeholders.""" + + def _get_translation_placeholders(self) -> dict[str, str]: + return {} +else: + # Multiple inheritance with `Protocol` seems to break + TranslationPlaceholderProtocol = object -class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC): - """Base flow to install firmware.""" +class SkyConnectTranslationMixin(TranslationPlaceholderProtocol): + """Translation placeholder mixin for Home Assistant SkyConnect.""" - _failed_addon_name: str - _failed_addon_reason: str - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Instantiate base flow.""" - super().__init__(*args, **kwargs) - - self._usb_info: usb.UsbServiceInfo | None = None - self._hw_variant: HardwareVariant | None = None - self._probed_firmware_type: ApplicationType | None = None - - self.addon_install_task: asyncio.Task | None = None - self.addon_start_task: asyncio.Task | None = None - self.addon_uninstall_task: asyncio.Task | None = None + context: dict[str, Any] def _get_translation_placeholders(self) -> dict[str, str]: """Shared translation placeholders.""" placeholders = { - "model": ( - self._hw_variant.full_name - if self._hw_variant is not None - else "unknown" - ), - "firmware_type": ( - self._probed_firmware_type.value - if self._probed_firmware_type is not None - else "unknown" - ), + **super()._get_translation_placeholders(), "docs_web_flasher_url": DOCS_WEB_FLASHER_URL, } @@ -85,416 +49,24 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC): return placeholders - async def _async_set_addon_config( - self, config: dict, addon_manager: AddonManager - ) -> None: - """Set add-on config.""" - try: - await addon_manager.async_set_addon_options(config) - except AddonError as err: - _LOGGER.error(err) - raise AbortFlow( - "addon_set_config_failed", - description_placeholders={ - **self._get_translation_placeholders(), - "addon_name": addon_manager.addon_name, - }, - ) from err - - async def _async_get_addon_info(self, addon_manager: AddonManager) -> AddonInfo: - """Return add-on info.""" - try: - addon_info = await addon_manager.async_get_addon_info() - except AddonError as err: - _LOGGER.error(err) - raise AbortFlow( - "addon_info_failed", - description_placeholders={ - **self._get_translation_placeholders(), - "addon_name": addon_manager.addon_name, - }, - ) from err - - return addon_info - - async def async_step_pick_firmware( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Pick Thread or Zigbee firmware.""" - return self.async_show_menu( - step_id="pick_firmware", - menu_options=[ - STEP_PICK_FIRMWARE_ZIGBEE, - STEP_PICK_FIRMWARE_THREAD, - ], - description_placeholders=self._get_translation_placeholders(), - ) - - async def _probe_firmware_type(self) -> bool: - """Probe the firmware currently on the device.""" - assert self._usb_info is not None - - self._probed_firmware_type = await probe_silabs_firmware_type( - self._usb_info.device, - probe_methods=( - # We probe in order of frequency: Zigbee, Thread, then multi-PAN - ApplicationType.GECKO_BOOTLOADER, - ApplicationType.EZSP, - ApplicationType.SPINEL, - ApplicationType.CPC, - ), - ) - - return self._probed_firmware_type in ( - ApplicationType.EZSP, - ApplicationType.SPINEL, - ApplicationType.CPC, - ) - - async def async_step_pick_firmware_zigbee( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Pick Zigbee firmware.""" - if not await self._probe_firmware_type(): - return self.async_abort( - reason="unsupported_firmware", - description_placeholders=self._get_translation_placeholders(), - ) - - # Allow the stick to be used with ZHA without flashing - if self._probed_firmware_type == ApplicationType.EZSP: - return await self.async_step_confirm_zigbee() - - if not is_hassio(self.hass): - return self.async_abort( - reason="not_hassio", - description_placeholders=self._get_translation_placeholders(), - ) - - # Only flash new firmware if we need to - fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass) - addon_info = await self._async_get_addon_info(fw_flasher_manager) - - if addon_info.state == AddonState.NOT_INSTALLED: - return await self.async_step_install_zigbee_flasher_addon() - - if addon_info.state == AddonState.NOT_RUNNING: - return await self.async_step_run_zigbee_flasher_addon() - - # If the addon is already installed and running, fail - return self.async_abort( - reason="addon_already_running", - description_placeholders={ - **self._get_translation_placeholders(), - "addon_name": fw_flasher_manager.addon_name, - }, - ) - - async def async_step_install_zigbee_flasher_addon( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Show progress dialog for installing the Zigbee flasher addon.""" - return await self._install_addon( - get_zigbee_flasher_addon_manager(self.hass), - "install_zigbee_flasher_addon", - "run_zigbee_flasher_addon", - ) - - async def _install_addon( - self, - addon_manager: silabs_multiprotocol_addon.WaitingAddonManager, - step_id: str, - next_step_id: str, - ) -> ConfigFlowResult: - """Show progress dialog for installing an addon.""" - addon_info = await self._async_get_addon_info(addon_manager) - - _LOGGER.debug("Flasher addon state: %s", addon_info) - - if not self.addon_install_task: - self.addon_install_task = self.hass.async_create_task( - addon_manager.async_install_addon_waiting(), - "Addon install", - ) - - if not self.addon_install_task.done(): - return self.async_show_progress( - step_id=step_id, - progress_action="install_addon", - description_placeholders={ - **self._get_translation_placeholders(), - "addon_name": addon_manager.addon_name, - }, - progress_task=self.addon_install_task, - ) - - try: - await self.addon_install_task - except AddonError as err: - _LOGGER.error(err) - self._failed_addon_name = addon_manager.addon_name - self._failed_addon_reason = "addon_install_failed" - return self.async_show_progress_done(next_step_id="addon_operation_failed") - finally: - self.addon_install_task = None - - return self.async_show_progress_done(next_step_id=next_step_id) - - async def async_step_addon_operation_failed( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Abort when add-on installation or start failed.""" - return self.async_abort( - reason=self._failed_addon_reason, - description_placeholders={ - **self._get_translation_placeholders(), - "addon_name": self._failed_addon_name, - }, - ) - - async def async_step_run_zigbee_flasher_addon( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Configure the flasher addon to point to the SkyConnect and run it.""" - fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass) - addon_info = await self._async_get_addon_info(fw_flasher_manager) - - assert self._usb_info is not None - new_addon_config = { - **addon_info.options, - "device": self._usb_info.device, - "baudrate": 115200, - "bootloader_baudrate": 115200, - "flow_control": True, - } - - _LOGGER.debug("Reconfiguring flasher addon with %s", new_addon_config) - await self._async_set_addon_config(new_addon_config, fw_flasher_manager) - - if not self.addon_start_task: - - async def start_and_wait_until_done() -> None: - await fw_flasher_manager.async_start_addon_waiting() - # Now that the addon is running, wait for it to finish - await fw_flasher_manager.async_wait_until_addon_state( - AddonState.NOT_RUNNING - ) - - self.addon_start_task = self.hass.async_create_task( - start_and_wait_until_done() - ) - - if not self.addon_start_task.done(): - return self.async_show_progress( - step_id="run_zigbee_flasher_addon", - progress_action="run_zigbee_flasher_addon", - description_placeholders={ - **self._get_translation_placeholders(), - "addon_name": fw_flasher_manager.addon_name, - }, - progress_task=self.addon_start_task, - ) - - try: - await self.addon_start_task - except (AddonError, AbortFlow) as err: - _LOGGER.error(err) - self._failed_addon_name = fw_flasher_manager.addon_name - self._failed_addon_reason = "addon_start_failed" - return self.async_show_progress_done(next_step_id="addon_operation_failed") - finally: - self.addon_start_task = None - - return self.async_show_progress_done( - next_step_id="uninstall_zigbee_flasher_addon" - ) - - async def async_step_uninstall_zigbee_flasher_addon( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Uninstall the flasher addon.""" - fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass) - - if not self.addon_uninstall_task: - _LOGGER.debug("Uninstalling flasher addon") - self.addon_uninstall_task = self.hass.async_create_task( - fw_flasher_manager.async_uninstall_addon_waiting() - ) - - if not self.addon_uninstall_task.done(): - return self.async_show_progress( - step_id="uninstall_zigbee_flasher_addon", - progress_action="uninstall_zigbee_flasher_addon", - description_placeholders={ - **self._get_translation_placeholders(), - "addon_name": fw_flasher_manager.addon_name, - }, - progress_task=self.addon_uninstall_task, - ) - - try: - await self.addon_uninstall_task - except (AddonError, AbortFlow) as err: - _LOGGER.error(err) - # The uninstall failing isn't critical so we can just continue - finally: - self.addon_uninstall_task = None - - return self.async_show_progress_done(next_step_id="confirm_zigbee") - - async def async_step_confirm_zigbee( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Confirm Zigbee setup.""" - assert self._usb_info is not None - assert self._hw_variant is not None - self._probed_firmware_type = ApplicationType.EZSP - - if user_input is not None: - await self.hass.config_entries.flow.async_init( - ZHA_DOMAIN, - context={"source": "hardware"}, - data={ - "name": self._hw_variant.full_name, - "port": { - "path": self._usb_info.device, - "baudrate": 115200, - "flow_control": "hardware", - }, - "radio_type": "ezsp", - }, - ) - - return self._async_flow_finished() - - return self.async_show_form( - step_id="confirm_zigbee", - description_placeholders=self._get_translation_placeholders(), - ) - - async def async_step_pick_firmware_thread( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Pick Thread firmware.""" - if not await self._probe_firmware_type(): - return self.async_abort( - reason="unsupported_firmware", - description_placeholders=self._get_translation_placeholders(), - ) - - # We install the OTBR addon no matter what, since it is required to use Thread - if not is_hassio(self.hass): - return self.async_abort( - reason="not_hassio_thread", - description_placeholders=self._get_translation_placeholders(), - ) - - otbr_manager = get_otbr_addon_manager(self.hass) - addon_info = await self._async_get_addon_info(otbr_manager) - - if addon_info.state == AddonState.NOT_INSTALLED: - return await self.async_step_install_otbr_addon() - - if addon_info.state == AddonState.NOT_RUNNING: - return await self.async_step_start_otbr_addon() - - # If the addon is already installed and running, fail - return self.async_abort( - reason="otbr_addon_already_running", - description_placeholders={ - **self._get_translation_placeholders(), - "addon_name": otbr_manager.addon_name, - }, - ) - - async def async_step_install_otbr_addon( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Show progress dialog for installing the OTBR addon.""" - return await self._install_addon( - get_otbr_addon_manager(self.hass), "install_otbr_addon", "start_otbr_addon" - ) - - async def async_step_start_otbr_addon( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Configure OTBR to point to the SkyConnect and run the addon.""" - otbr_manager = get_otbr_addon_manager(self.hass) - addon_info = await self._async_get_addon_info(otbr_manager) - - assert self._usb_info is not None - new_addon_config = { - **addon_info.options, - "device": self._usb_info.device, - "baudrate": 460800, - "flow_control": True, - "autoflash_firmware": True, - } - - _LOGGER.debug("Reconfiguring OTBR addon with %s", new_addon_config) - await self._async_set_addon_config(new_addon_config, otbr_manager) - - if not self.addon_start_task: - self.addon_start_task = self.hass.async_create_task( - otbr_manager.async_start_addon_waiting() - ) - - if not self.addon_start_task.done(): - return self.async_show_progress( - step_id="start_otbr_addon", - progress_action="start_otbr_addon", - description_placeholders={ - **self._get_translation_placeholders(), - "addon_name": otbr_manager.addon_name, - }, - progress_task=self.addon_start_task, - ) - - try: - await self.addon_start_task - except (AddonError, AbortFlow) as err: - _LOGGER.error(err) - self._failed_addon_name = otbr_manager.addon_name - self._failed_addon_reason = "addon_start_failed" - return self.async_show_progress_done(next_step_id="addon_operation_failed") - finally: - self.addon_start_task = None - - return self.async_show_progress_done(next_step_id="confirm_otbr") - - async def async_step_confirm_otbr( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Confirm OTBR setup.""" - assert self._usb_info is not None - assert self._hw_variant is not None - - self._probed_firmware_type = ApplicationType.SPINEL - - if user_input is not None: - # OTBR discovery is done automatically via hassio - return self._async_flow_finished() - - return self.async_show_form( - step_id="confirm_otbr", - description_placeholders=self._get_translation_placeholders(), - ) - - @abstractmethod - def _async_flow_finished(self) -> ConfigFlowResult: - """Finish the flow.""" - # This should be implemented by a subclass - raise NotImplementedError - class HomeAssistantSkyConnectConfigFlow( - BaseFirmwareInstallFlow, ConfigFlow, domain=DOMAIN + SkyConnectTranslationMixin, + firmware_config_flow.BaseFirmwareConfigFlow, + domain=DOMAIN, ): """Handle a config flow for Home Assistant SkyConnect.""" VERSION = 1 MINOR_VERSION = 2 + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the config flow.""" + super().__init__(*args, **kwargs) + + self._usb_info: usb.UsbServiceInfo | None = None + self._hw_variant: HardwareVariant | None = None + @staticmethod @callback def async_get_options_flow( @@ -532,13 +104,11 @@ class HomeAssistantSkyConnectConfigFlow( assert description is not None self._hw_variant = HardwareVariant.from_usb_product_name(description) - return await self.async_step_confirm() + # Set parent class attributes + self._device = self._usb_info.device + self._hardware_name = self._hw_variant.full_name - async def async_step_confirm( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Confirm a discovery.""" - return await self.async_step_pick_firmware() + return await self.async_step_confirm() def _async_flow_finished(self) -> ConfigFlowResult: """Create the config entry.""" @@ -617,7 +187,7 @@ class HomeAssistantSkyConnectMultiPanOptionsFlowHandler( class HomeAssistantSkyConnectOptionsFlowHandler( - BaseFirmwareInstallFlow, OptionsFlowWithConfigEntry + SkyConnectTranslationMixin, firmware_config_flow.BaseFirmwareOptionsFlow ): """Zigbee and Thread options flow handlers.""" @@ -626,67 +196,17 @@ class HomeAssistantSkyConnectOptionsFlowHandler( super().__init__(*args, **kwargs) self._usb_info = get_usb_service_info(self.config_entry) - self._probed_firmware_type = ApplicationType(self.config_entry.data["firmware"]) self._hw_variant = HardwareVariant.from_usb_product_name( self.config_entry.data["product"] ) - - # Make `context` a regular dictionary - self.context = {} + self._hardware_name = self._hw_variant.full_name + self._device = self._usb_info.device # Regenerate the translation placeholders self._get_translation_placeholders() - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Manage the options flow.""" - return await self.async_step_pick_firmware() - - async def async_step_pick_firmware_zigbee( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Pick Zigbee firmware.""" - assert self._usb_info is not None - - if is_hassio(self.hass): - otbr_manager = get_otbr_addon_manager(self.hass) - otbr_addon_info = await self._async_get_addon_info(otbr_manager) - - if ( - otbr_addon_info.state != AddonState.NOT_INSTALLED - and otbr_addon_info.options.get("device") == self._usb_info.device - ): - raise AbortFlow( - "otbr_still_using_stick", - description_placeholders=self._get_translation_placeholders(), - ) - - return await super().async_step_pick_firmware_zigbee(user_input) - - async def async_step_pick_firmware_thread( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Pick Thread firmware.""" - assert self._usb_info is not None - - for zha_entry in self.hass.config_entries.async_entries( - ZHA_DOMAIN, - include_ignore=False, - include_disabled=True, - ): - if get_zha_device_path(zha_entry) == self._usb_info.device: - raise AbortFlow( - "zha_still_using_stick", - description_placeholders=self._get_translation_placeholders(), - ) - - return await super().async_step_pick_firmware_thread(user_input) - def _async_flow_finished(self) -> ConfigFlowResult: """Create the config entry.""" - assert self._usb_info is not None - assert self._hw_variant is not None assert self._probed_firmware_type is not None self.hass.config_entries.async_update_entry( diff --git a/homeassistant/components/homeassistant_sky_connect/const.py b/homeassistant/components/homeassistant_sky_connect/const.py index 1d6c16dc528..cae0b98a25b 100644 --- a/homeassistant/components/homeassistant_sky_connect/const.py +++ b/homeassistant/components/homeassistant_sky_connect/const.py @@ -5,18 +5,8 @@ import enum from typing import Self DOMAIN = "homeassistant_sky_connect" -ZHA_DOMAIN = "zha" - DOCS_WEB_FLASHER_URL = "https://skyconnect.home-assistant.io/firmware-update/" -OTBR_ADDON_NAME = "OpenThread Border Router" -OTBR_ADDON_MANAGER_DATA = "openthread_border_router" -OTBR_ADDON_SLUG = "core_openthread_border_router" - -ZIGBEE_FLASHER_ADDON_NAME = "Silicon Labs Flasher" -ZIGBEE_FLASHER_ADDON_MANAGER_DATA = "silabs_flasher" -ZIGBEE_FLASHER_ADDON_SLUG = "core_silabs_flasher" - @dataclasses.dataclass(frozen=True) class VariantInfo: diff --git a/homeassistant/components/homeassistant_sky_connect/strings.json b/homeassistant/components/homeassistant_sky_connect/strings.json index 59bcb6e606a..20f587c2dbb 100644 --- a/homeassistant/components/homeassistant_sky_connect/strings.json +++ b/homeassistant/components/homeassistant_sky_connect/strings.json @@ -59,44 +59,44 @@ "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::description%]" }, "pick_firmware": { - "title": "[%key:component::homeassistant_sky_connect::config::step::pick_firmware::title%]", - "description": "[%key:component::homeassistant_sky_connect::config::step::pick_firmware::description%]", + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]", "menu_options": { - "pick_firmware_thread": "[%key:component::homeassistant_sky_connect::config::step::pick_firmware::menu_options::pick_firmware_thread%]", - "pick_firmware_zigbee": "[%key:component::homeassistant_sky_connect::config::step::pick_firmware::menu_options::pick_firmware_zigbee%]" + "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]", + "pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]" } }, "install_zigbee_flasher_addon": { - "title": "[%key:component::homeassistant_sky_connect::config::step::install_zigbee_flasher_addon::title%]", - "description": "[%key:component::homeassistant_sky_connect::config::step::install_zigbee_flasher_addon::description%]" + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_flasher_addon::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_flasher_addon::description%]" }, "run_zigbee_flasher_addon": { - "title": "[%key:component::homeassistant_sky_connect::config::step::run_zigbee_flasher_addon::title%]", - "description": "[%key:component::homeassistant_sky_connect::config::step::run_zigbee_flasher_addon::description%]" + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::run_zigbee_flasher_addon::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::run_zigbee_flasher_addon::description%]" }, "zigbee_flasher_failed": { - "title": "[%key:component::homeassistant_sky_connect::config::step::zigbee_flasher_failed::title%]", - "description": "[%key:component::homeassistant_sky_connect::config::step::zigbee_flasher_failed::description%]" + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_flasher_failed::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_flasher_failed::description%]" }, "confirm_zigbee": { - "title": "[%key:component::homeassistant_sky_connect::config::step::confirm_zigbee::title%]", - "description": "[%key:component::homeassistant_sky_connect::config::step::confirm_zigbee::description%]" + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]" }, "install_otbr_addon": { - "title": "[%key:component::homeassistant_sky_connect::config::step::install_otbr_addon::title%]", - "description": "[%key:component::homeassistant_sky_connect::config::step::install_otbr_addon::description%]" + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::description%]" }, "start_otbr_addon": { - "title": "[%key:component::homeassistant_sky_connect::config::step::start_otbr_addon::title%]", - "description": "[%key:component::homeassistant_sky_connect::config::step::start_otbr_addon::description%]" + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::description%]" }, "otbr_failed": { - "title": "[%key:component::homeassistant_sky_connect::config::step::otbr_failed::title%]", - "description": "[%key:component::homeassistant_sky_connect::config::step::otbr_failed::description%]" + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::description%]" }, "confirm_otbr": { - "title": "[%key:component::homeassistant_sky_connect::config::step::confirm_otbr::title%]", - "description": "[%key:component::homeassistant_sky_connect::config::step::confirm_otbr::description%]" + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::description%]" } }, "error": { @@ -110,66 +110,66 @@ "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]", "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", - "not_hassio_thread": "[%key:component::homeassistant_sky_connect::config::abort::not_hassio_thread%]", - "otbr_addon_already_running": "[%key:component::homeassistant_sky_connect::config::abort::otbr_addon_already_running%]", - "zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again.", - "otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again." + "not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]", + "otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]", + "zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]", + "otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]" }, "progress": { "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", "start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]", "start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]", - "install_zigbee_flasher_addon": "[%key:component::homeassistant_sky_connect::config::progress::install_zigbee_flasher_addon%]", - "run_zigbee_flasher_addon": "[%key:component::homeassistant_sky_connect::config::progress::run_zigbee_flasher_addon%]", - "uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_sky_connect::config::progress::uninstall_zigbee_flasher_addon%]" + "install_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_zigbee_flasher_addon%]", + "run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]", + "uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]" } }, "config": { "flow_title": "{model}", "step": { "pick_firmware": { - "title": "Pick your firmware", - "description": "Let's get started with setting up your {model}. Do you want to use it to set up a Zigbee or Thread network?", + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]", "menu_options": { - "pick_firmware_zigbee": "Zigbee", - "pick_firmware_thread": "Thread" + "pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]", + "pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]" } }, "install_zigbee_flasher_addon": { - "title": "Installing flasher", - "description": "Installing the Silicon Labs Flasher add-on." + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_flasher_addon::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_flasher_addon::description%]" }, "run_zigbee_flasher_addon": { - "title": "Installing Zigbee firmware", - "description": "Installing Zigbee firmware. This will take about a minute." + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::run_zigbee_flasher_addon::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::run_zigbee_flasher_addon::description%]" }, "uninstall_zigbee_flasher_addon": { - "title": "Removing flasher", - "description": "Removing the Silicon Labs Flasher add-on." + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::uninstall_zigbee_flasher_addon::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::uninstall_zigbee_flasher_addon::description%]" }, "zigbee_flasher_failed": { - "title": "Zigbee installation failed", - "description": "The Zigbee firmware installation process was unsuccessful. Ensure no other software is trying to communicate with the {model} and try again." + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_flasher_failed::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_flasher_failed::description%]" }, "confirm_zigbee": { - "title": "Zigbee setup complete", - "description": "Your {model} is now a Zigbee coordinator and will be shown as discovered by the Zigbee Home Automation integration once you exit." + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]" }, "install_otbr_addon": { - "title": "Installing OpenThread Border Router add-on", - "description": "The OpenThread Border Router (OTBR) add-on is being installed." + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::description%]" }, "start_otbr_addon": { - "title": "Starting OpenThread Border Router add-on", - "description": "The OpenThread Border Router (OTBR) add-on is now starting." + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::description%]" }, "otbr_failed": { - "title": "Failed to setup OpenThread Border Router", - "description": "The OpenThread Border Router add-on installation was unsuccessful. Ensure no other software is trying to communicate with the {model}, you have access to the internet and can install other add-ons, and try again. Check the Supervisor logs if the problem persists." + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::description%]" }, "confirm_otbr": { - "title": "OpenThread Border Router setup complete", - "description": "Your {model} is now an OpenThread Border Router and will show up in the Thread integration once you exit." + "title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::title%]", + "description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::description%]" } }, "abort": { @@ -180,16 +180,16 @@ "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]", "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", - "not_hassio_thread": "The OpenThread Border Router addon can only be installed with Home Assistant OS. If you would like to use the {model} as an Thread border router, please flash the firmware manually using the [web flasher]({docs_web_flasher_url}) and set up OpenThread Border Router to communicate with it.", - "otbr_addon_already_running": "The OpenThread Border Router add-on is already running, it cannot be installed again." + "not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]", + "otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]" }, "progress": { "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", "start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]", "start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]", - "install_zigbee_flasher_addon": "The Silicon Labs Flasher addon is installed, this may take a few minutes.", - "run_zigbee_flasher_addon": "Please wait while Zigbee firmware is installed to your {model}, this will take a few minutes. Do not make any changes to your hardware or software until this finishes.", - "uninstall_zigbee_flasher_addon": "The Silicon Labs Flasher addon is being removed." + "install_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_zigbee_flasher_addon%]", + "run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]", + "uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]" } } } diff --git a/homeassistant/components/homeassistant_sky_connect/util.py b/homeassistant/components/homeassistant_sky_connect/util.py index 864d6bfd9dc..f8c5d004d0e 100644 --- a/homeassistant/components/homeassistant_sky_connect/util.py +++ b/homeassistant/components/homeassistant_sky_connect/util.py @@ -2,33 +2,12 @@ from __future__ import annotations -from collections import defaultdict -from dataclasses import dataclass import logging -from typing import cast - -from universal_silabs_flasher.const import ApplicationType from homeassistant.components import usb -from homeassistant.components.hassio import AddonError, AddonState, is_hassio -from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( - WaitingAddonManager, - get_multiprotocol_addon_manager, -) -from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.singleton import singleton +from homeassistant.config_entries import ConfigEntry -from .const import ( - OTBR_ADDON_MANAGER_DATA, - OTBR_ADDON_NAME, - OTBR_ADDON_SLUG, - ZHA_DOMAIN, - ZIGBEE_FLASHER_ADDON_MANAGER_DATA, - ZIGBEE_FLASHER_ADDON_NAME, - ZIGBEE_FLASHER_ADDON_SLUG, - HardwareVariant, -) +from .const import HardwareVariant _LOGGER = logging.getLogger(__name__) @@ -48,110 +27,3 @@ def get_usb_service_info(config_entry: ConfigEntry) -> usb.UsbServiceInfo: def get_hardware_variant(config_entry: ConfigEntry) -> HardwareVariant: """Get the hardware variant from the config entry.""" return HardwareVariant.from_usb_product_name(config_entry.data["product"]) - - -def get_zha_device_path(config_entry: ConfigEntry) -> str | None: - """Get the device path from a ZHA config entry.""" - return cast(str | None, config_entry.data.get("device", {}).get("path", None)) - - -@singleton(OTBR_ADDON_MANAGER_DATA) -@callback -def get_otbr_addon_manager(hass: HomeAssistant) -> WaitingAddonManager: - """Get the OTBR add-on manager.""" - return WaitingAddonManager( - hass, - _LOGGER, - OTBR_ADDON_NAME, - OTBR_ADDON_SLUG, - ) - - -@singleton(ZIGBEE_FLASHER_ADDON_MANAGER_DATA) -@callback -def get_zigbee_flasher_addon_manager(hass: HomeAssistant) -> WaitingAddonManager: - """Get the flasher add-on manager.""" - return WaitingAddonManager( - hass, - _LOGGER, - ZIGBEE_FLASHER_ADDON_NAME, - ZIGBEE_FLASHER_ADDON_SLUG, - ) - - -@dataclass(slots=True, kw_only=True) -class FirmwareGuess: - """Firmware guess.""" - - is_running: bool - firmware_type: ApplicationType - source: str - - -async def guess_firmware_type(hass: HomeAssistant, device_path: str) -> FirmwareGuess: - """Guess the firmware type based on installed addons and other integrations.""" - device_guesses: defaultdict[str | None, list[FirmwareGuess]] = defaultdict(list) - - for zha_config_entry in hass.config_entries.async_entries(ZHA_DOMAIN): - zha_path = get_zha_device_path(zha_config_entry) - - if zha_path is not None: - device_guesses[zha_path].append( - FirmwareGuess( - is_running=(zha_config_entry.state == ConfigEntryState.LOADED), - firmware_type=ApplicationType.EZSP, - source="zha", - ) - ) - - if is_hassio(hass): - otbr_addon_manager = get_otbr_addon_manager(hass) - - try: - otbr_addon_info = await otbr_addon_manager.async_get_addon_info() - except AddonError: - pass - else: - if otbr_addon_info.state != AddonState.NOT_INSTALLED: - otbr_path = otbr_addon_info.options.get("device") - device_guesses[otbr_path].append( - FirmwareGuess( - is_running=(otbr_addon_info.state == AddonState.RUNNING), - firmware_type=ApplicationType.SPINEL, - source="otbr", - ) - ) - - multipan_addon_manager = await get_multiprotocol_addon_manager(hass) - - try: - multipan_addon_info = await multipan_addon_manager.async_get_addon_info() - except AddonError: - pass - else: - if multipan_addon_info.state != AddonState.NOT_INSTALLED: - multipan_path = multipan_addon_info.options.get("device") - device_guesses[multipan_path].append( - FirmwareGuess( - is_running=(multipan_addon_info.state == AddonState.RUNNING), - firmware_type=ApplicationType.CPC, - source="multiprotocol", - ) - ) - - # Fall back to EZSP if we can't guess the firmware type - if device_path not in device_guesses: - return FirmwareGuess( - is_running=False, firmware_type=ApplicationType.EZSP, source="unknown" - ) - - # Prioritizes guesses that were pulled from a running addon or integration but keep - # the sort order we defined above - guesses = sorted( - device_guesses[device_path], - key=lambda guess: guess.is_running, - ) - - assert guesses - - return guesses[-1] diff --git a/tests/components/homeassistant_hardware/test_config_flow.py b/tests/components/homeassistant_hardware/test_config_flow.py new file mode 100644 index 00000000000..a1842f4c4e6 --- /dev/null +++ b/tests/components/homeassistant_hardware/test_config_flow.py @@ -0,0 +1,674 @@ +"""Test the Home Assistant hardware firmware config flow.""" + +import asyncio +from collections.abc import Awaitable, Callable, Generator, Iterator +import contextlib +from typing import Any +from unittest.mock import AsyncMock, Mock, call, patch + +import pytest +from universal_silabs_flasher.const import ApplicationType + +from homeassistant.components.hassio.addon_manager import AddonInfo, AddonState +from homeassistant.components.homeassistant_hardware.firmware_config_flow import ( + STEP_PICK_FIRMWARE_THREAD, + STEP_PICK_FIRMWARE_ZIGBEE, + BaseFirmwareConfigFlow, + BaseFirmwareOptionsFlow, +) +from homeassistant.components.homeassistant_hardware.util import ( + get_otbr_addon_manager, + get_zigbee_flasher_addon_manager, +) +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult, OptionsFlow +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import ( + MockConfigEntry, + MockModule, + mock_config_flow, + mock_integration, + mock_platform, +) + +TEST_DOMAIN = "test_firmware_domain" +TEST_DEVICE = "/dev/SomeDevice123" +TEST_HARDWARE_NAME = "Some Hardware Name" + + +class FakeFirmwareConfigFlow(BaseFirmwareConfigFlow, domain=TEST_DOMAIN): + """Config flow for `test_firmware_domain`.""" + + VERSION = 1 + MINOR_VERSION = 2 + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlow: + """Return the options flow.""" + return FakeFirmwareOptionsFlowHandler(config_entry) + + async def async_step_hardware( + self, data: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle hardware flow.""" + self._device = TEST_DEVICE + self._hardware_name = TEST_HARDWARE_NAME + + return await self.async_step_confirm() + + def _async_flow_finished(self) -> ConfigFlowResult: + """Create the config entry.""" + assert self._device is not None + assert self._hardware_name is not None + assert self._probed_firmware_type is not None + + return self.async_create_entry( + title=self._hardware_name, + data={ + "device": self._device, + "firmware": self._probed_firmware_type.value, + "hardware": self._hardware_name, + }, + ) + + +class FakeFirmwareOptionsFlowHandler(BaseFirmwareOptionsFlow): + """Options flow for `test_firmware_domain`.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Instantiate options flow.""" + super().__init__(*args, **kwargs) + + self._device = self.config_entry.data["device"] + self._hardware_name = self.config_entry.data["hardware"] + + # Regenerate the translation placeholders + self._get_translation_placeholders() + + def _async_flow_finished(self) -> ConfigFlowResult: + """Create the config entry.""" + assert self._probed_firmware_type is not None + + self.hass.config_entries.async_update_entry( + entry=self.config_entry, + data={ + **self.config_entry.data, + "firmware": self._probed_firmware_type.value, + }, + options=self.config_entry.options, + ) + + return self.async_create_entry(title="", data={}) + + +@pytest.fixture(autouse=True) +def mock_test_firmware_platform( + hass: HomeAssistant, +) -> Generator[None]: + """Fixture for a test config flow.""" + mock_module = MockModule( + TEST_DOMAIN, async_setup_entry=AsyncMock(return_value=True) + ) + mock_integration(hass, mock_module) + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + with mock_config_flow(TEST_DOMAIN, FakeFirmwareConfigFlow): + yield + + +def delayed_side_effect() -> Callable[..., Awaitable[None]]: + """Slows down eager tasks by delaying for an event loop tick.""" + + async def side_effect(*args: Any, **kwargs: Any) -> None: + await asyncio.sleep(0) + + return side_effect + + +@contextlib.contextmanager +def mock_addon_info( + hass: HomeAssistant, + *, + is_hassio: bool = True, + app_type: ApplicationType = ApplicationType.EZSP, + otbr_addon_info: AddonInfo = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ), + flasher_addon_info: AddonInfo = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ), +) -> Iterator[tuple[Mock, Mock]]: + """Mock the main addon states for the config flow.""" + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_uninstall_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_get_addon_info.return_value = flasher_addon_info + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_uninstall_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_get_addon_info.return_value = otbr_addon_info + + with ( + patch( + "homeassistant.components.homeassistant_hardware.firmware_config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_hardware.firmware_config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_hardware.firmware_config_flow.is_hassio", + return_value=is_hassio, + ), + patch( + "homeassistant.components.homeassistant_hardware.firmware_config_flow.probe_silabs_firmware_type", + return_value=app_type, + ), + ): + yield mock_otbr_manager, mock_flasher_manager + + +async def test_config_flow_zigbee(hass: HomeAssistant) -> None: + """Test the config flow.""" + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + + with mock_addon_info( + hass, + app_type=ApplicationType.SPINEL, + ) as (mock_otbr_manager, mock_flasher_manager): + # Pick the menu option: we are now installing the addon + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "install_addon" + assert result["step_id"] == "install_zigbee_flasher_addon" + assert result["description_placeholders"]["firmware_type"] == "spinel" + + await hass.async_block_till_done(wait_background_tasks=True) + + # Progress the flow, we are now configuring the addon and running it + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "run_zigbee_flasher_addon" + assert result["progress_action"] == "run_zigbee_flasher_addon" + assert mock_flasher_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": TEST_DEVICE, + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + } + ) + ] + + await hass.async_block_till_done(wait_background_tasks=True) + + # Progress the flow, we are now uninstalling the addon + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "uninstall_zigbee_flasher_addon" + assert result["progress_action"] == "uninstall_zigbee_flasher_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + # We are finally done with the addon + assert mock_flasher_manager.async_uninstall_addon_waiting.mock_calls == [call()] + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + config_entry = result["result"] + assert config_entry.data == { + "firmware": "ezsp", + "device": TEST_DEVICE, + "hardware": TEST_HARDWARE_NAME, + } + + # Ensure a ZHA discovery flow has been created + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + zha_flow = flows[0] + assert zha_flow["handler"] == "zha" + assert zha_flow["context"]["source"] == "hardware" + assert zha_flow["step_id"] == "confirm" + + +async def test_config_flow_zigbee_skip_step_if_installed(hass: HomeAssistant) -> None: + """Test the config flow, skip installing the addon if necessary.""" + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + + with mock_addon_info( + hass, + app_type=ApplicationType.SPINEL, + flasher_addon_info=AddonInfo( + available=True, + hostname=None, + options={ + "device": "", + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + }, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.2.3", + ), + ) as (mock_otbr_manager, mock_flasher_manager): + # Pick the menu option: we skip installation, instead we directly run it + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "run_zigbee_flasher_addon" + assert result["progress_action"] == "run_zigbee_flasher_addon" + assert result["description_placeholders"]["firmware_type"] == "spinel" + assert mock_flasher_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": TEST_DEVICE, + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + } + ) + ] + + # Uninstall the addon + await hass.async_block_till_done(wait_background_tasks=True) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + # Done + await hass.async_block_till_done(wait_background_tasks=True) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" + + +async def test_config_flow_thread(hass: HomeAssistant) -> None: + """Test the config flow.""" + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + + with mock_addon_info( + hass, + app_type=ApplicationType.EZSP, + ) as (mock_otbr_manager, mock_flasher_manager): + # Pick the menu option + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "install_addon" + assert result["step_id"] == "install_otbr_addon" + assert result["description_placeholders"]["firmware_type"] == "ezsp" + assert result["description_placeholders"]["model"] == TEST_HARDWARE_NAME + + await hass.async_block_till_done(wait_background_tasks=True) + + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={ + "device": "", + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + }, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.2.3", + ) + + # Progress the flow, it is now configuring the addon and running it + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_otbr_addon" + assert result["progress_action"] == "start_otbr_addon" + + assert mock_otbr_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": TEST_DEVICE, + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + } + ) + ] + + await hass.async_block_till_done(wait_background_tasks=True) + + # The addon is now running + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_otbr" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + config_entry = result["result"] + assert config_entry.data == { + "firmware": "spinel", + "device": TEST_DEVICE, + "hardware": TEST_HARDWARE_NAME, + } + + +async def test_config_flow_thread_addon_already_installed(hass: HomeAssistant) -> None: + """Test the Thread config flow, addon is already installed.""" + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + with mock_addon_info( + hass, + app_type=ApplicationType.EZSP, + otbr_addon_info=AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_RUNNING, + update_available=False, + version=None, + ), + ) as (mock_otbr_manager, mock_flasher_manager): + # Pick the menu option + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_otbr_addon" + assert result["progress_action"] == "start_otbr_addon" + + assert mock_otbr_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": TEST_DEVICE, + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + } + ) + ] + + await hass.async_block_till_done(wait_background_tasks=True) + + # The addon is now running + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_otbr" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_config_flow_zigbee_not_hassio(hass: HomeAssistant) -> None: + """Test when the stick is used with a non-hassio setup.""" + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "hardware"} + ) + + with mock_addon_info( + hass, + is_hassio=False, + app_type=ApplicationType.EZSP, + ) as (mock_otbr_manager, mock_flasher_manager): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + config_entry = result["result"] + assert config_entry.data == { + "firmware": "ezsp", + "device": TEST_DEVICE, + "hardware": TEST_HARDWARE_NAME, + } + + # Ensure a ZHA discovery flow has been created + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + zha_flow = flows[0] + assert zha_flow["handler"] == "zha" + assert zha_flow["context"]["source"] == "hardware" + assert zha_flow["step_id"] == "confirm" + + +async def test_options_flow_zigbee_to_thread(hass: HomeAssistant) -> None: + """Test the options flow, migrating Zigbee to Thread.""" + config_entry = MockConfigEntry( + domain=TEST_DOMAIN, + data={ + "firmware": "ezsp", + "device": TEST_DEVICE, + "hardware": TEST_HARDWARE_NAME, + }, + version=1, + minor_version=2, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + + # First step is confirmation + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + assert result["description_placeholders"]["firmware_type"] == "ezsp" + assert result["description_placeholders"]["model"] == TEST_HARDWARE_NAME + + with mock_addon_info( + hass, + app_type=ApplicationType.EZSP, + ) as (mock_otbr_manager, mock_flasher_manager): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "install_addon" + assert result["step_id"] == "install_otbr_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={ + "device": "", + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + }, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.2.3", + ) + + # Progress the flow, it is now configuring the addon and running it + result = await hass.config_entries.options.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_otbr_addon" + assert result["progress_action"] == "start_otbr_addon" + + assert mock_otbr_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": TEST_DEVICE, + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + } + ) + ] + + await hass.async_block_till_done(wait_background_tasks=True) + + # The addon is now running + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_otbr" + + # We are now done + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + # The firmware type has been updated + assert config_entry.data["firmware"] == "spinel" + + +async def test_options_flow_thread_to_zigbee(hass: HomeAssistant) -> None: + """Test the options flow, migrating Thread to Zigbee.""" + config_entry = MockConfigEntry( + domain=TEST_DOMAIN, + data={ + "firmware": "spinel", + "device": TEST_DEVICE, + "hardware": TEST_HARDWARE_NAME, + }, + version=1, + minor_version=2, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + + # First step is confirmation + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + assert result["description_placeholders"]["firmware_type"] == "spinel" + assert result["description_placeholders"]["model"] == TEST_HARDWARE_NAME + + with mock_addon_info( + hass, + app_type=ApplicationType.SPINEL, + ) as (mock_otbr_manager, mock_flasher_manager): + # Pick the menu option: we are now installing the addon + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "install_addon" + assert result["step_id"] == "install_zigbee_flasher_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + # Progress the flow, we are now configuring the addon and running it + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "run_zigbee_flasher_addon" + assert result["progress_action"] == "run_zigbee_flasher_addon" + assert mock_flasher_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": TEST_DEVICE, + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + } + ) + ] + + await hass.async_block_till_done(wait_background_tasks=True) + + # Progress the flow, we are now uninstalling the addon + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "uninstall_zigbee_flasher_addon" + assert result["progress_action"] == "uninstall_zigbee_flasher_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + # We are finally done with the addon + assert mock_flasher_manager.async_uninstall_addon_waiting.mock_calls == [call()] + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" + + # We are now done + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + # The firmware type has been updated + assert config_entry.data["firmware"] == "ezsp" diff --git a/tests/components/homeassistant_sky_connect/test_config_flow_failures.py b/tests/components/homeassistant_hardware/test_config_flow_failures.py similarity index 73% rename from tests/components/homeassistant_sky_connect/test_config_flow_failures.py rename to tests/components/homeassistant_hardware/test_config_flow_failures.py index b29f8d808ae..4c3ea7d28fa 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow_failures.py +++ b/tests/components/homeassistant_hardware/test_config_flow_failures.py @@ -1,38 +1,43 @@ -"""Test the Home Assistant SkyConnect config flow failure cases.""" +"""Test the Home Assistant hardware firmware config flow failure cases.""" from unittest.mock import AsyncMock import pytest from universal_silabs_flasher.const import ApplicationType -from homeassistant.components import usb from homeassistant.components.hassio.addon_manager import ( AddonError, AddonInfo, AddonState, ) -from homeassistant.components.homeassistant_sky_connect.config_flow import ( +from homeassistant.components.homeassistant_hardware.firmware_config_flow import ( STEP_PICK_FIRMWARE_THREAD, STEP_PICK_FIRMWARE_ZIGBEE, ) -from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .test_config_flow import USB_DATA_ZBT1, delayed_side_effect, mock_addon_info +from .test_config_flow import ( + TEST_DEVICE, + TEST_DOMAIN, + TEST_HARDWARE_NAME, + delayed_side_effect, + mock_addon_info, + mock_test_firmware_platform, # noqa: F401 +) from tests.common import MockConfigEntry @pytest.mark.parametrize( - ("usb_data", "model", "next_step"), + "next_step", [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1", STEP_PICK_FIRMWARE_ZIGBEE), - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1", STEP_PICK_FIRMWARE_THREAD), + STEP_PICK_FIRMWARE_ZIGBEE, + STEP_PICK_FIRMWARE_THREAD, ], ) async def test_config_flow_cannot_probe_firmware( - usb_data: usb.UsbServiceInfo, model: str, next_step: str, hass: HomeAssistant + next_step: str, hass: HomeAssistant ) -> None: """Test failure case when firmware cannot be probed.""" @@ -42,7 +47,7 @@ async def test_config_flow_cannot_probe_firmware( ) as (mock_otbr_manager, mock_flasher_manager): # Start the flow result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) result = await hass.config_entries.flow.async_configure( @@ -54,18 +59,12 @@ async def test_config_flow_cannot_probe_firmware( assert result["reason"] == "unsupported_firmware" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) async def test_config_flow_zigbee_not_hassio_wrong_firmware( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant + hass: HomeAssistant, ) -> None: """Test when the stick is used with a non-hassio setup but the firmware is bad.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) with mock_addon_info( @@ -85,18 +84,12 @@ async def test_config_flow_zigbee_not_hassio_wrong_firmware( assert result["reason"] == "not_hassio" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) async def test_config_flow_zigbee_flasher_addon_already_running( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant + hass: HomeAssistant, ) -> None: """Test failure case when flasher addon is already running.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) with mock_addon_info( @@ -125,18 +118,10 @@ async def test_config_flow_zigbee_flasher_addon_already_running( assert result["reason"] == "addon_already_running" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_zigbee_flasher_addon_info_fails( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: +async def test_config_flow_zigbee_flasher_addon_info_fails(hass: HomeAssistant) -> None: """Test failure case when flasher addon cannot be installed.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) with mock_addon_info( @@ -166,18 +151,12 @@ async def test_config_flow_zigbee_flasher_addon_info_fails( assert result["reason"] == "addon_info_failed" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) async def test_config_flow_zigbee_flasher_addon_install_fails( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant + hass: HomeAssistant, ) -> None: """Test failure case when flasher addon cannot be installed.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) with mock_addon_info( @@ -202,18 +181,12 @@ async def test_config_flow_zigbee_flasher_addon_install_fails( assert result["reason"] == "addon_install_failed" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) async def test_config_flow_zigbee_flasher_addon_set_config_fails( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant + hass: HomeAssistant, ) -> None: """Test failure case when flasher addon cannot be configured.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) with mock_addon_info( @@ -242,18 +215,10 @@ async def test_config_flow_zigbee_flasher_addon_set_config_fails( assert result["reason"] == "addon_set_config_failed" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_zigbee_flasher_run_fails( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: +async def test_config_flow_zigbee_flasher_run_fails(hass: HomeAssistant) -> None: """Test failure case when flasher addon fails to run.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) with mock_addon_info( @@ -279,18 +244,10 @@ async def test_config_flow_zigbee_flasher_run_fails( assert result["reason"] == "addon_start_failed" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_zigbee_flasher_uninstall_fails( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: +async def test_config_flow_zigbee_flasher_uninstall_fails(hass: HomeAssistant) -> None: """Test failure case when flasher addon uninstall fails.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) with mock_addon_info( @@ -319,18 +276,10 @@ async def test_config_flow_zigbee_flasher_uninstall_fails( assert result["step_id"] == "confirm_zigbee" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_thread_not_hassio( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: +async def test_config_flow_thread_not_hassio(hass: HomeAssistant) -> None: """Test when the stick is used with a non-hassio setup and Thread is selected.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) with mock_addon_info( @@ -350,18 +299,10 @@ async def test_config_flow_thread_not_hassio( assert result["reason"] == "not_hassio_thread" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_thread_addon_info_fails( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: +async def test_config_flow_thread_addon_info_fails(hass: HomeAssistant) -> None: """Test failure case when flasher addon cannot be installed.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) with mock_addon_info( @@ -382,18 +323,10 @@ async def test_config_flow_thread_addon_info_fails( assert result["reason"] == "addon_info_failed" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_thread_addon_already_running( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: +async def test_config_flow_thread_addon_already_running(hass: HomeAssistant) -> None: """Test failure case when the Thread addon is already running.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) with mock_addon_info( @@ -425,18 +358,10 @@ async def test_config_flow_thread_addon_already_running( assert result["reason"] == "otbr_addon_already_running" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_thread_addon_install_fails( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: +async def test_config_flow_thread_addon_install_fails(hass: HomeAssistant) -> None: """Test failure case when flasher addon cannot be installed.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) with mock_addon_info( @@ -460,18 +385,10 @@ async def test_config_flow_thread_addon_install_fails( assert result["reason"] == "addon_install_failed" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_thread_addon_set_config_fails( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: +async def test_config_flow_thread_addon_set_config_fails(hass: HomeAssistant) -> None: """Test failure case when flasher addon cannot be configured.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) with mock_addon_info( @@ -495,18 +412,10 @@ async def test_config_flow_thread_addon_set_config_fails( assert result["reason"] == "addon_set_config_failed" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_thread_flasher_run_fails( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: +async def test_config_flow_thread_flasher_run_fails(hass: HomeAssistant) -> None: """Test failure case when flasher addon fails to run.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) with mock_addon_info( @@ -531,18 +440,10 @@ async def test_config_flow_thread_flasher_run_fails( assert result["reason"] == "addon_start_failed" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_thread_flasher_uninstall_fails( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: +async def test_config_flow_thread_flasher_uninstall_fails(hass: HomeAssistant) -> None: """Test failure case when flasher addon uninstall fails.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + TEST_DOMAIN, context={"source": "hardware"} ) with mock_addon_info( @@ -572,27 +473,16 @@ async def test_config_flow_thread_flasher_uninstall_fails( assert result["step_id"] == "confirm_otbr" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) async def test_options_flow_zigbee_to_thread_zha_configured( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant + hass: HomeAssistant, ) -> None: """Test the options flow migration failure, ZHA using the stick.""" config_entry = MockConfigEntry( - domain="homeassistant_sky_connect", + domain=TEST_DOMAIN, data={ "firmware": "ezsp", - "device": usb_data.device, - "manufacturer": usb_data.manufacturer, - "pid": usb_data.pid, - "description": usb_data.description, - "product": usb_data.description, - "serial_number": usb_data.serial_number, - "vid": usb_data.vid, + "device": TEST_DEVICE, + "hardware": TEST_HARDWARE_NAME, }, version=1, minor_version=2, @@ -604,7 +494,7 @@ async def test_options_flow_zigbee_to_thread_zha_configured( # Set up ZHA as well zha_config_entry = MockConfigEntry( domain="zha", - data={"device": {"path": usb_data.device}}, + data={"device": {"path": TEST_DEVICE}}, ) zha_config_entry.add_to_hass(hass) @@ -620,27 +510,16 @@ async def test_options_flow_zigbee_to_thread_zha_configured( assert result["reason"] == "zha_still_using_stick" -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) async def test_options_flow_thread_to_zigbee_otbr_configured( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant + hass: HomeAssistant, ) -> None: """Test the options flow migration failure, OTBR still using the stick.""" config_entry = MockConfigEntry( - domain="homeassistant_sky_connect", + domain=TEST_DOMAIN, data={ "firmware": "spinel", - "device": usb_data.device, - "manufacturer": usb_data.manufacturer, - "pid": usb_data.pid, - "description": usb_data.description, - "product": usb_data.description, - "serial_number": usb_data.serial_number, - "vid": usb_data.vid, + "device": TEST_DEVICE, + "hardware": TEST_HARDWARE_NAME, }, version=1, minor_version=2, @@ -658,7 +537,7 @@ async def test_options_flow_thread_to_zigbee_otbr_configured( otbr_addon_info=AddonInfo( available=True, hostname=None, - options={"device": usb_data.device}, + options={"device": TEST_DEVICE}, state=AddonState.RUNNING, update_available=False, version="1.0.0", diff --git a/tests/components/homeassistant_hardware/test_util.py b/tests/components/homeassistant_hardware/test_util.py new file mode 100644 index 00000000000..4a30a39686f --- /dev/null +++ b/tests/components/homeassistant_hardware/test_util.py @@ -0,0 +1,158 @@ +"""Test hardware utilities.""" + +from unittest.mock import AsyncMock, patch + +from universal_silabs_flasher.const import ApplicationType + +from homeassistant.components.hassio import AddonError, AddonInfo, AddonState +from homeassistant.components.homeassistant_hardware.util import ( + FirmwareGuess, + get_zha_device_path, + guess_firmware_type, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +ZHA_CONFIG_ENTRY = MockConfigEntry( + domain="zha", + unique_id="some_unique_id", + data={ + "device": { + "path": "socket://1.2.3.4:5678", + "baudrate": 115200, + "flow_control": None, + }, + "radio_type": "ezsp", + }, + version=4, +) + + +def test_get_zha_device_path() -> None: + """Test extracting the ZHA device path from its config entry.""" + assert ( + get_zha_device_path(ZHA_CONFIG_ENTRY) == ZHA_CONFIG_ENTRY.data["device"]["path"] + ) + + +def test_get_zha_device_path_ignored_discovery() -> None: + """Test extracting the ZHA device path from an ignored ZHA discovery.""" + config_entry = MockConfigEntry( + domain="zha", + unique_id="some_unique_id", + data={}, + version=4, + ) + + assert get_zha_device_path(config_entry) is None + + +async def test_guess_firmware_type_unknown(hass: HomeAssistant) -> None: + """Test guessing the firmware type.""" + + assert (await guess_firmware_type(hass, "/dev/missing")) == FirmwareGuess( + is_running=False, firmware_type=ApplicationType.EZSP, source="unknown" + ) + + +async def test_guess_firmware_type(hass: HomeAssistant) -> None: + """Test guessing the firmware.""" + path = ZHA_CONFIG_ENTRY.data["device"]["path"] + + ZHA_CONFIG_ENTRY.add_to_hass(hass) + + ZHA_CONFIG_ENTRY.mock_state(hass, ConfigEntryState.NOT_LOADED) + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=False, firmware_type=ApplicationType.EZSP, source="zha" + ) + + # When ZHA is running, we indicate as such when guessing + ZHA_CONFIG_ENTRY.mock_state(hass, ConfigEntryState.LOADED) + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.EZSP, source="zha" + ) + + mock_otbr_addon_manager = AsyncMock() + mock_multipan_addon_manager = AsyncMock() + + with ( + patch( + "homeassistant.components.homeassistant_hardware.util.is_hassio", + return_value=True, + ), + patch( + "homeassistant.components.homeassistant_hardware.util.get_otbr_addon_manager", + return_value=mock_otbr_addon_manager, + ), + patch( + "homeassistant.components.homeassistant_hardware.util.get_multiprotocol_addon_manager", + return_value=mock_multipan_addon_manager, + ), + ): + mock_otbr_addon_manager.async_get_addon_info.side_effect = AddonError() + mock_multipan_addon_manager.async_get_addon_info.side_effect = AddonError() + + # Hassio errors are ignored and we still go with ZHA + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.EZSP, source="zha" + ) + + mock_otbr_addon_manager.async_get_addon_info.side_effect = None + mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": "/some/other/device"}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + # We will prefer ZHA, as it is running (and actually pointing to the device) + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.EZSP, source="zha" + ) + + mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": path}, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.0.0", + ) + + # We will still prefer ZHA, as it is the one actually running + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.EZSP, source="zha" + ) + + mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": path}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + # Finally, ZHA loses out to OTBR + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.SPINEL, source="otbr" + ) + + mock_multipan_addon_manager.async_get_addon_info.side_effect = None + mock_multipan_addon_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": path}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + # Which will lose out to multi-PAN + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.CPC, source="multiprotocol" + ) diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index 48b774d5aeb..0d4c517b07f 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -1,30 +1,20 @@ """Test the Home Assistant SkyConnect config flow.""" -import asyncio -from collections.abc import Awaitable, Callable, Iterator -import contextlib -from typing import Any -from unittest.mock import AsyncMock, Mock, call, patch +from unittest.mock import Mock, patch import pytest -from universal_silabs_flasher.const import ApplicationType from homeassistant.components import usb from homeassistant.components.hassio.addon_manager import AddonInfo, AddonState +from homeassistant.components.homeassistant_hardware.firmware_config_flow import ( + STEP_PICK_FIRMWARE_ZIGBEE, +) from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( CONF_DISABLE_MULTI_PAN, get_flasher_addon_manager, get_multiprotocol_addon_manager, ) -from homeassistant.components.homeassistant_sky_connect.config_flow import ( - STEP_PICK_FIRMWARE_THREAD, - STEP_PICK_FIRMWARE_ZIGBEE, -) from homeassistant.components.homeassistant_sky_connect.const import DOMAIN -from homeassistant.components.homeassistant_sky_connect.util import ( - get_otbr_addon_manager, - get_zigbee_flasher_addon_manager, -) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -49,86 +39,6 @@ USB_DATA_ZBT1 = usb.UsbServiceInfo( ) -def delayed_side_effect() -> Callable[..., Awaitable[None]]: - """Slows down eager tasks by delaying for an event loop tick.""" - - async def side_effect(*args: Any, **kwargs: Any) -> None: - await asyncio.sleep(0) - - return side_effect - - -@contextlib.contextmanager -def mock_addon_info( - hass: HomeAssistant, - *, - is_hassio: bool = True, - app_type: ApplicationType = ApplicationType.EZSP, - otbr_addon_info: AddonInfo = AddonInfo( - available=True, - hostname=None, - options={}, - state=AddonState.NOT_INSTALLED, - update_available=False, - version=None, - ), - flasher_addon_info: AddonInfo = AddonInfo( - available=True, - hostname=None, - options={}, - state=AddonState.NOT_INSTALLED, - update_available=False, - version=None, - ), -) -> Iterator[tuple[Mock, Mock]]: - """Mock the main addon states for the config flow.""" - mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) - mock_flasher_manager.addon_name = "Silicon Labs Flasher" - mock_flasher_manager.async_start_addon_waiting = AsyncMock( - side_effect=delayed_side_effect() - ) - mock_flasher_manager.async_install_addon_waiting = AsyncMock( - side_effect=delayed_side_effect() - ) - mock_flasher_manager.async_uninstall_addon_waiting = AsyncMock( - side_effect=delayed_side_effect() - ) - mock_flasher_manager.async_get_addon_info.return_value = flasher_addon_info - - mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) - mock_otbr_manager.addon_name = "OpenThread Border Router" - mock_otbr_manager.async_install_addon_waiting = AsyncMock( - side_effect=delayed_side_effect() - ) - mock_otbr_manager.async_uninstall_addon_waiting = AsyncMock( - side_effect=delayed_side_effect() - ) - mock_otbr_manager.async_start_addon_waiting = AsyncMock( - side_effect=delayed_side_effect() - ) - mock_otbr_manager.async_get_addon_info.return_value = otbr_addon_info - - with ( - patch( - "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", - return_value=mock_otbr_manager, - ), - patch( - "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", - return_value=mock_flasher_manager, - ), - patch( - "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", - return_value=is_hassio, - ), - patch( - "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", - return_value=app_type, - ), - ): - yield mock_otbr_manager, mock_flasher_manager - - @pytest.mark.parametrize( ("usb_data", "model"), [ @@ -136,7 +46,7 @@ def mock_addon_info( (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), ], ) -async def test_config_flow_zigbee( +async def test_config_flow( usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant ) -> None: """Test the config flow for SkyConnect.""" @@ -146,453 +56,42 @@ async def test_config_flow_zigbee( assert result["type"] is FlowResultType.MENU assert result["step_id"] == "pick_firmware" - - with mock_addon_info( - hass, - app_type=ApplicationType.SPINEL, - ) as (mock_otbr_manager, mock_flasher_manager): - # Pick the menu option: we are now installing the addon - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, - ) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["progress_action"] == "install_addon" - assert result["step_id"] == "install_zigbee_flasher_addon" - assert result["description_placeholders"]["firmware_type"] == "spinel" - - await hass.async_block_till_done(wait_background_tasks=True) - - # Progress the flow, we are now configuring the addon and running it - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "run_zigbee_flasher_addon" - assert result["progress_action"] == "run_zigbee_flasher_addon" - assert mock_flasher_manager.async_set_addon_options.mock_calls == [ - call( - { - "device": usb_data.device, - "baudrate": 115200, - "bootloader_baudrate": 115200, - "flow_control": True, - } - ) - ] - - await hass.async_block_till_done(wait_background_tasks=True) - - # Progress the flow, we are now uninstalling the addon - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "uninstall_zigbee_flasher_addon" - assert result["progress_action"] == "uninstall_zigbee_flasher_addon" - - await hass.async_block_till_done(wait_background_tasks=True) - - # We are finally done with the addon - assert mock_flasher_manager.async_uninstall_addon_waiting.mock_calls == [call()] - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "confirm_zigbee" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - - config_entry = result["result"] - assert config_entry.data == { - "firmware": "ezsp", - "device": usb_data.device, - "manufacturer": usb_data.manufacturer, - "pid": usb_data.pid, - "description": usb_data.description, - "product": usb_data.description, - "serial_number": usb_data.serial_number, - "vid": usb_data.vid, - } - - # Ensure a ZHA discovery flow has been created - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - zha_flow = flows[0] - assert zha_flow["handler"] == "zha" - assert zha_flow["context"]["source"] == "hardware" - assert zha_flow["step_id"] == "confirm" - - -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_SKY, "Home Assistant SkyConnect"), - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_zigbee_skip_step_if_installed( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: - """Test the config flow for SkyConnect, skip installing the addon if necessary.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data - ) - - assert result["type"] is FlowResultType.MENU - assert result["step_id"] == "pick_firmware" - - with mock_addon_info( - hass, - app_type=ApplicationType.SPINEL, - flasher_addon_info=AddonInfo( - available=True, - hostname=None, - options={ - "device": "", - "baudrate": 115200, - "bootloader_baudrate": 115200, - "flow_control": True, - }, - state=AddonState.NOT_RUNNING, - update_available=False, - version="1.2.3", - ), - ) as (mock_otbr_manager, mock_flasher_manager): - # Pick the menu option: we skip installation, instead we directly run it - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, - ) - - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "run_zigbee_flasher_addon" - assert result["progress_action"] == "run_zigbee_flasher_addon" - assert result["description_placeholders"]["firmware_type"] == "spinel" - assert mock_flasher_manager.async_set_addon_options.mock_calls == [ - call( - { - "device": usb_data.device, - "baudrate": 115200, - "bootloader_baudrate": 115200, - "flow_control": True, - } - ) - ] - - # Uninstall the addon - await hass.async_block_till_done(wait_background_tasks=True) - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - - # Done - await hass.async_block_till_done(wait_background_tasks=True) - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "confirm_zigbee" - - -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_SKY, "Home Assistant SkyConnect"), - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_thread( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: - """Test the config flow for SkyConnect.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data - ) - - assert result["type"] is FlowResultType.MENU - assert result["step_id"] == "pick_firmware" - - with mock_addon_info( - hass, - app_type=ApplicationType.EZSP, - ) as (mock_otbr_manager, mock_flasher_manager): - # Pick the menu option - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, - ) - - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["progress_action"] == "install_addon" - assert result["step_id"] == "install_otbr_addon" - assert result["description_placeholders"]["firmware_type"] == "ezsp" - assert result["description_placeholders"]["model"] == model - - await hass.async_block_till_done(wait_background_tasks=True) - - mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( - available=True, - hostname=None, - options={ - "device": "", - "baudrate": 460800, - "flow_control": True, - "autoflash_firmware": True, - }, - state=AddonState.NOT_RUNNING, - update_available=False, - version="1.2.3", - ) - - # Progress the flow, it is now configuring the addon and running it - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "start_otbr_addon" - assert result["progress_action"] == "start_otbr_addon" - - assert mock_otbr_manager.async_set_addon_options.mock_calls == [ - call( - { - "device": usb_data.device, - "baudrate": 460800, - "flow_control": True, - "autoflash_firmware": True, - } - ) - ] - - await hass.async_block_till_done(wait_background_tasks=True) - - # The addon is now running - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "confirm_otbr" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - - config_entry = result["result"] - assert config_entry.data == { - "firmware": "spinel", - "device": usb_data.device, - "manufacturer": usb_data.manufacturer, - "pid": usb_data.pid, - "description": usb_data.description, - "product": usb_data.description, - "serial_number": usb_data.serial_number, - "vid": usb_data.vid, - } - - -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_SKY, "Home Assistant SkyConnect"), - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_thread_addon_already_installed( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: - """Test the Thread config flow for SkyConnect, addon is already installed.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data - ) - - with mock_addon_info( - hass, - app_type=ApplicationType.EZSP, - otbr_addon_info=AddonInfo( - available=True, - hostname=None, - options={}, - state=AddonState.NOT_RUNNING, - update_available=False, - version=None, - ), - ) as (mock_otbr_manager, mock_flasher_manager): - # Pick the menu option - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, - ) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "start_otbr_addon" - assert result["progress_action"] == "start_otbr_addon" - - assert mock_otbr_manager.async_set_addon_options.mock_calls == [ - call( - { - "device": usb_data.device, - "baudrate": 460800, - "flow_control": True, - "autoflash_firmware": True, - } - ) - ] - - await hass.async_block_till_done(wait_background_tasks=True) - - # The addon is now running - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "confirm_otbr" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - - -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_zigbee_not_hassio( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: - """Test when the stick is used with a non-hassio setup.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data - ) - - with mock_addon_info( - hass, - is_hassio=False, - app_type=ApplicationType.EZSP, - ) as (mock_otbr_manager, mock_flasher_manager): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "confirm_zigbee" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - - config_entry = result["result"] - assert config_entry.data == { - "firmware": "ezsp", - "device": usb_data.device, - "manufacturer": usb_data.manufacturer, - "pid": usb_data.pid, - "description": usb_data.description, - "product": usb_data.description, - "serial_number": usb_data.serial_number, - "vid": usb_data.vid, - } - - # Ensure a ZHA discovery flow has been created - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - zha_flow = flows[0] - assert zha_flow["handler"] == "zha" - assert zha_flow["context"]["source"] == "hardware" - assert zha_flow["step_id"] == "confirm" - - -@pytest.mark.parametrize( - ("usb_data", "model"), - [ - (USB_DATA_SKY, "Home Assistant SkyConnect"), - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_options_flow_zigbee_to_thread( - usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant -) -> None: - """Test the options flow for SkyConnect, migrating Zigbee to Thread.""" - config_entry = MockConfigEntry( - domain="homeassistant_sky_connect", - data={ - "firmware": "ezsp", - "device": usb_data.device, - "manufacturer": usb_data.manufacturer, - "pid": usb_data.pid, - "description": usb_data.description, - "product": usb_data.description, - "serial_number": usb_data.serial_number, - "vid": usb_data.vid, - }, - version=1, - minor_version=2, - ) - config_entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(config_entry.entry_id) - - # First step is confirmation - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] is FlowResultType.MENU - assert result["step_id"] == "pick_firmware" - assert result["description_placeholders"]["firmware_type"] == "ezsp" assert result["description_placeholders"]["model"] == model - with mock_addon_info( - hass, - app_type=ApplicationType.EZSP, - ) as (mock_otbr_manager, mock_flasher_manager): - result = await hass.config_entries.options.async_configure( + async def mock_async_step_pick_firmware_zigbee(self, data): + return await self.async_step_confirm_zigbee(user_input={}) + + with patch( + "homeassistant.components.homeassistant_hardware.firmware_config_flow.BaseFirmwareConfigFlow.async_step_pick_firmware_zigbee", + autospec=True, + side_effect=mock_async_step_pick_firmware_zigbee, + ): + result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, ) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["progress_action"] == "install_addon" - assert result["step_id"] == "install_otbr_addon" - - await hass.async_block_till_done(wait_background_tasks=True) - - mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( - available=True, - hostname=None, - options={ - "device": "", - "baudrate": 460800, - "flow_control": True, - "autoflash_firmware": True, - }, - state=AddonState.NOT_RUNNING, - update_available=False, - version="1.2.3", - ) - - # Progress the flow, it is now configuring the addon and running it - result = await hass.config_entries.options.async_configure(result["flow_id"]) - - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "start_otbr_addon" - assert result["progress_action"] == "start_otbr_addon" - - assert mock_otbr_manager.async_set_addon_options.mock_calls == [ - call( - { - "device": usb_data.device, - "baudrate": 460800, - "flow_control": True, - "autoflash_firmware": True, - } - ) - ] - - await hass.async_block_till_done(wait_background_tasks=True) - - # The addon is now running - result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "confirm_otbr" - - # We are now done - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={} - ) assert result["type"] is FlowResultType.CREATE_ENTRY - # The firmware type has been updated - assert config_entry.data["firmware"] == "spinel" + config_entry = result["result"] + assert config_entry.data == { + "firmware": "ezsp", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + } + + # Ensure a ZHA discovery flow has been created + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + zha_flow = flows[0] + assert zha_flow["handler"] == "zha" + assert zha_flow["context"]["source"] == "hardware" + assert zha_flow["step_id"] == "confirm" @pytest.mark.parametrize( @@ -602,10 +101,10 @@ async def test_options_flow_zigbee_to_thread( (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), ], ) -async def test_options_flow_thread_to_zigbee( +async def test_options_flow( usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant ) -> None: - """Test the options flow for SkyConnect, migrating Thread to Zigbee.""" + """Test the options flow for SkyConnect.""" config_entry = MockConfigEntry( domain="homeassistant_sky_connect", data={ @@ -632,62 +131,32 @@ async def test_options_flow_thread_to_zigbee( assert result["description_placeholders"]["firmware_type"] == "spinel" assert result["description_placeholders"]["model"] == model - with mock_addon_info( - hass, - app_type=ApplicationType.SPINEL, - ) as (mock_otbr_manager, mock_flasher_manager): - # Pick the menu option: we are now installing the addon + async def mock_async_step_pick_firmware_zigbee(self, data): + return await self.async_step_confirm_zigbee(user_input={}) + + with patch( + "homeassistant.components.homeassistant_hardware.firmware_config_flow.BaseFirmwareOptionsFlow.async_step_pick_firmware_zigbee", + autospec=True, + side_effect=mock_async_step_pick_firmware_zigbee, + ): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, ) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["progress_action"] == "install_addon" - assert result["step_id"] == "install_zigbee_flasher_addon" - await hass.async_block_till_done(wait_background_tasks=True) - - # Progress the flow, we are now configuring the addon and running it - result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "run_zigbee_flasher_addon" - assert result["progress_action"] == "run_zigbee_flasher_addon" - assert mock_flasher_manager.async_set_addon_options.mock_calls == [ - call( - { - "device": usb_data.device, - "baudrate": 115200, - "bootloader_baudrate": 115200, - "flow_control": True, - } - ) - ] - - await hass.async_block_till_done(wait_background_tasks=True) - - # Progress the flow, we are now uninstalling the addon - result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "uninstall_zigbee_flasher_addon" - assert result["progress_action"] == "uninstall_zigbee_flasher_addon" - - await hass.async_block_till_done(wait_background_tasks=True) - - # We are finally done with the addon - assert mock_flasher_manager.async_uninstall_addon_waiting.mock_calls == [call()] - - result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "confirm_zigbee" - - # We are now done - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={} - ) assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["result"] is True - # The firmware type has been updated - assert config_entry.data["firmware"] == "ezsp" + assert config_entry.data == { + "firmware": "ezsp", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + } @pytest.mark.parametrize( diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index 88b57f2dd64..e1c13771fdc 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -4,8 +4,8 @@ from unittest.mock import patch from universal_silabs_flasher.const import ApplicationType +from homeassistant.components.homeassistant_hardware.util import FirmwareGuess from homeassistant.components.homeassistant_sky_connect.const import DOMAIN -from homeassistant.components.homeassistant_sky_connect.util import FirmwareGuess from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry diff --git a/tests/components/homeassistant_sky_connect/test_util.py b/tests/components/homeassistant_sky_connect/test_util.py index b560acc65b7..1d1d70c1b4c 100644 --- a/tests/components/homeassistant_sky_connect/test_util.py +++ b/tests/components/homeassistant_sky_connect/test_util.py @@ -1,24 +1,14 @@ """Test SkyConnect utilities.""" -from unittest.mock import AsyncMock, patch - -from universal_silabs_flasher.const import ApplicationType - -from homeassistant.components.hassio import AddonError, AddonInfo, AddonState from homeassistant.components.homeassistant_sky_connect.const import ( DOMAIN, HardwareVariant, ) from homeassistant.components.homeassistant_sky_connect.util import ( - FirmwareGuess, get_hardware_variant, get_usb_service_info, - get_zha_device_path, - guess_firmware_type, ) from homeassistant.components.usb import UsbServiceInfo -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -52,20 +42,6 @@ CONNECT_ZBT1_CONFIG_ENTRY = MockConfigEntry( version=2, ) -ZHA_CONFIG_ENTRY = MockConfigEntry( - domain="zha", - unique_id="some_unique_id", - data={ - "device": { - "path": "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_3c0ed67c628beb11b1cd64a0f320645d-if00-port0", - "baudrate": 115200, - "flow_control": None, - }, - "radio_type": "ezsp", - }, - version=4, -) - def test_get_usb_service_info() -> None: """Test `get_usb_service_info` conversion.""" @@ -85,131 +61,3 @@ def test_get_hardware_variant() -> None: assert ( get_hardware_variant(CONNECT_ZBT1_CONFIG_ENTRY) == HardwareVariant.CONNECT_ZBT1 ) - - -def test_get_zha_device_path() -> None: - """Test extracting the ZHA device path from its config entry.""" - assert ( - get_zha_device_path(ZHA_CONFIG_ENTRY) == ZHA_CONFIG_ENTRY.data["device"]["path"] - ) - - -def test_get_zha_device_path_ignored_discovery() -> None: - """Test extracting the ZHA device path from an ignored ZHA discovery.""" - config_entry = MockConfigEntry( - domain="zha", - unique_id="some_unique_id", - data={}, - version=4, - ) - - assert get_zha_device_path(config_entry) is None - - -async def test_guess_firmware_type_unknown(hass: HomeAssistant) -> None: - """Test guessing the firmware type.""" - - assert (await guess_firmware_type(hass, "/dev/missing")) == FirmwareGuess( - is_running=False, firmware_type=ApplicationType.EZSP, source="unknown" - ) - - -async def test_guess_firmware_type(hass: HomeAssistant) -> None: - """Test guessing the firmware.""" - path = ZHA_CONFIG_ENTRY.data["device"]["path"] - - ZHA_CONFIG_ENTRY.add_to_hass(hass) - - ZHA_CONFIG_ENTRY.mock_state(hass, ConfigEntryState.NOT_LOADED) - assert (await guess_firmware_type(hass, path)) == FirmwareGuess( - is_running=False, firmware_type=ApplicationType.EZSP, source="zha" - ) - - # When ZHA is running, we indicate as such when guessing - ZHA_CONFIG_ENTRY.mock_state(hass, ConfigEntryState.LOADED) - assert (await guess_firmware_type(hass, path)) == FirmwareGuess( - is_running=True, firmware_type=ApplicationType.EZSP, source="zha" - ) - - mock_otbr_addon_manager = AsyncMock() - mock_multipan_addon_manager = AsyncMock() - - with ( - patch( - "homeassistant.components.homeassistant_sky_connect.util.is_hassio", - return_value=True, - ), - patch( - "homeassistant.components.homeassistant_sky_connect.util.get_otbr_addon_manager", - return_value=mock_otbr_addon_manager, - ), - patch( - "homeassistant.components.homeassistant_sky_connect.util.get_multiprotocol_addon_manager", - return_value=mock_multipan_addon_manager, - ), - ): - mock_otbr_addon_manager.async_get_addon_info.side_effect = AddonError() - mock_multipan_addon_manager.async_get_addon_info.side_effect = AddonError() - - # Hassio errors are ignored and we still go with ZHA - assert (await guess_firmware_type(hass, path)) == FirmwareGuess( - is_running=True, firmware_type=ApplicationType.EZSP, source="zha" - ) - - mock_otbr_addon_manager.async_get_addon_info.side_effect = None - mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( - available=True, - hostname=None, - options={"device": "/some/other/device"}, - state=AddonState.RUNNING, - update_available=False, - version="1.0.0", - ) - - # We will prefer ZHA, as it is running (and actually pointing to the device) - assert (await guess_firmware_type(hass, path)) == FirmwareGuess( - is_running=True, firmware_type=ApplicationType.EZSP, source="zha" - ) - - mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( - available=True, - hostname=None, - options={"device": path}, - state=AddonState.NOT_RUNNING, - update_available=False, - version="1.0.0", - ) - - # We will still prefer ZHA, as it is the one actually running - assert (await guess_firmware_type(hass, path)) == FirmwareGuess( - is_running=True, firmware_type=ApplicationType.EZSP, source="zha" - ) - - mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( - available=True, - hostname=None, - options={"device": path}, - state=AddonState.RUNNING, - update_available=False, - version="1.0.0", - ) - - # Finally, ZHA loses out to OTBR - assert (await guess_firmware_type(hass, path)) == FirmwareGuess( - is_running=True, firmware_type=ApplicationType.SPINEL, source="otbr" - ) - - mock_multipan_addon_manager.async_get_addon_info.side_effect = None - mock_multipan_addon_manager.async_get_addon_info.return_value = AddonInfo( - available=True, - hostname=None, - options={"device": path}, - state=AddonState.RUNNING, - update_available=False, - version="1.0.0", - ) - - # Which will lose out to multi-PAN - assert (await guess_firmware_type(hass, path)) == FirmwareGuess( - is_running=True, firmware_type=ApplicationType.CPC, source="multiprotocol" - ) From 5d87a74c3c0ba435e377dadea778a5c8cd1c4462 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:50:45 +0200 Subject: [PATCH 1683/2411] Fix implicit-return in unifiprotect tests (#122781) --- tests/components/unifiprotect/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 21c01f77c5f..25a9ddcbb92 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Callable, Sequence from dataclasses import dataclass from datetime import timedelta -from typing import Any from unittest.mock import Mock from uiprotect import ProtectApiClient @@ -41,11 +40,11 @@ class MockUFPFixture: ws_subscription: Callable[[WSSubscriptionMessage], None] | None = None ws_state_subscription: Callable[[WebsocketState], None] | None = None - def ws_msg(self, msg: WSSubscriptionMessage) -> Any: + def ws_msg(self, msg: WSSubscriptionMessage) -> None: """Emit WS message for testing.""" if self.ws_subscription is not None: - return self.ws_subscription(msg) + self.ws_subscription(msg) def reset_objects(bootstrap: Bootstrap): From 7bbbda8d2b2c22c8b3cea183047d64ab8ca6f662 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:52:15 +0200 Subject: [PATCH 1684/2411] Fix implicit-return in sonos tests (#122780) --- tests/components/sonos/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 26666d98ced..4e5f704d322 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -453,6 +453,7 @@ def mock_get_music_library_information( "object.container.album.musicAlbum", ) ] + return [] @pytest.fixture(name="music_library_browse_categories") From 1958a149c3ad343c46eabd64490678288b5128de Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:52:47 +0200 Subject: [PATCH 1685/2411] Fix implicit-return in ipma tests (#122791) --- tests/components/ipma/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/ipma/__init__.py b/tests/components/ipma/__init__.py index 799120e3966..ab5998c922f 100644 --- a/tests/components/ipma/__init__.py +++ b/tests/components/ipma/__init__.py @@ -108,6 +108,7 @@ class MockLocation: location=Forecast_Location(0, "", 0, 0, 0, "", (0, 0)), ), ] + raise ValueError(f"Unknown forecast period: {period}") name = "HomeTown" station = "HomeTown Station" From 197ac8b950a75626648ab7f8d4705cc6e88bcddf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:53:22 +0200 Subject: [PATCH 1686/2411] Fix implicit-return in netatmo tests (#122789) --- tests/components/netatmo/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py index d9fe5e5b277..730cb0cb117 100644 --- a/tests/components/netatmo/common.py +++ b/tests/components/netatmo/common.py @@ -87,7 +87,7 @@ async def fake_post_request(*args: Any, **kwargs: Any): ) -async def fake_get_image(*args: Any, **kwargs: Any) -> bytes | str: +async def fake_get_image(*args: Any, **kwargs: Any) -> bytes | str | None: """Return fake data.""" if "endpoint" not in kwargs: return "{}" @@ -96,6 +96,7 @@ async def fake_get_image(*args: Any, **kwargs: Any) -> bytes | str: if endpoint in "snapshot_720.jpg": return b"test stream image bytes" + return None async def simulate_webhook(hass: HomeAssistant, webhook_id: str, response) -> None: From 8de7a2e3c79bea13eb6a945ca00d8330aae1ebc6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 14:55:22 -0500 Subject: [PATCH 1687/2411] Bump aiohttp to 3.10.0rc0 (#122793) --- 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 80eaa3bc31d..f0c72a91501 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-zlib==0.1.1 -aiohttp==3.10.0b1 +aiohttp==3.10.0rc0 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index 172bb0139d1..940a11753a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.10.0b1", + "aiohttp==3.10.0rc0", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index 6f6a11b03a1..6e5ef50c187 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.10.0b1 +aiohttp==3.10.0rc0 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 From 6ba633451245781b1c1542768edbb7b5094b1d88 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:57:35 +0200 Subject: [PATCH 1688/2411] Fix implicit-return in enigma2 tests (#122790) --- tests/components/enigma2/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/enigma2/conftest.py b/tests/components/enigma2/conftest.py index f5436183559..6c024ebf66a 100644 --- a/tests/components/enigma2/conftest.py +++ b/tests/components/enigma2/conftest.py @@ -72,7 +72,7 @@ class MockDevice: """Initialize the mock Enigma2 device.""" self.status = OpenWebIfStatus(currservice=OpenWebIfServiceEvent()) - async def _call_api(self, url: str) -> dict: + async def _call_api(self, url: str) -> dict | None: if url.endswith("/api/about"): return { "info": { @@ -85,6 +85,7 @@ class MockDevice: "brand": "Enigma2", } } + return None def get_version(self) -> str | None: """Return the version.""" From 5b434ee3365f5ad9a6353388737995c1914ef5fa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:58:03 +0200 Subject: [PATCH 1689/2411] Fix implicit-return in xiaomi tests (#122778) --- tests/components/xiaomi/test_device_tracker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py index 975e666af68..0f1c36d1fba 100644 --- a/tests/components/xiaomi/test_device_tracker.py +++ b/tests/components/xiaomi/test_device_tracker.py @@ -144,6 +144,7 @@ def mocked_requests(*args, **kwargs): 200, ) _LOGGER.debug("UNKNOWN ROUTE") + return None @patch( From 9393dcddb74fa0dea5abcc155157725e50f86faf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:59:59 +0200 Subject: [PATCH 1690/2411] Fix implicit-return in nx584 tests (#122788) --- tests/components/nx584/test_binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/nx584/test_binary_sensor.py b/tests/components/nx584/test_binary_sensor.py index 5c57feb471b..9261521f850 100644 --- a/tests/components/nx584/test_binary_sensor.py +++ b/tests/components/nx584/test_binary_sensor.py @@ -216,8 +216,8 @@ def test_nx584_watcher_run_with_zone_events() -> None: """Return nothing twice, then some events.""" if empty_me: empty_me.pop() - else: - return fake_events + return None + return fake_events client = mock.MagicMock() fake_events = [ From b8c363a82cd4a3684f50567465a8381145adefd4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:03:14 +0200 Subject: [PATCH 1691/2411] Fix implicit-return in tplink_omada tests (#122776) --- tests/components/tplink_omada/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/tplink_omada/conftest.py b/tests/components/tplink_omada/conftest.py index aef51bce87c..510a2e7a87c 100644 --- a/tests/components/tplink_omada/conftest.py +++ b/tests/components/tplink_omada/conftest.py @@ -129,6 +129,7 @@ def _get_mock_client(mac: str) -> OmadaNetworkClient: if c["wireless"]: return OmadaWirelessClient(c) return OmadaWiredClient(c) + raise ValueError(f"Client with MAC {mac} not found in mock data") @pytest.fixture From bf38db003582fdd45bd0b64df82f557a8894e69c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:03:44 +0200 Subject: [PATCH 1692/2411] Fix implicit-return in surepetcare tests (#122785) --- tests/components/surepetcare/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/surepetcare/conftest.py b/tests/components/surepetcare/conftest.py index 9ae1bfe310a..5dcc5dfdadc 100644 --- a/tests/components/surepetcare/conftest.py +++ b/tests/components/surepetcare/conftest.py @@ -17,6 +17,7 @@ from tests.common import MockConfigEntry async def _mock_call(method, resource): if method == "GET" and resource == MESTART_RESOURCE: return {"data": MOCK_API_DATA} + return None @pytest.fixture From 20c4f84a4e04f04ba81db79ef2813d61d987dda9 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Mon, 29 Jul 2024 22:04:54 +0200 Subject: [PATCH 1693/2411] Fix incorrect Bang & Olufsen MDNS announcements (#122782) --- .../components/bang_olufsen/config_flow.py | 9 +++++++++ .../bang_olufsen/test_config_flow.py | 20 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bang_olufsen/config_flow.py b/homeassistant/components/bang_olufsen/config_flow.py index e3b8f9979d1..76e4656129e 100644 --- a/homeassistant/components/bang_olufsen/config_flow.py +++ b/homeassistant/components/bang_olufsen/config_flow.py @@ -135,6 +135,15 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN): except AddressValueError: return self.async_abort(reason="ipv6_address") + # Check connection to ensure valid address is received + self._client = MozartClient(self._host) + + async with self._client: + try: + await self._client.get_beolink_self(_request_timeout=3) + except (ClientConnectorError, TimeoutError): + return self.async_abort(reason="invalid_address") + self._model = discovery_info.hostname[:-16].replace("-", " ") self._serial_number = discovery_info.properties[ATTR_SERIAL_NUMBER] self._beolink_jid = f"{discovery_info.properties[ATTR_TYPE_NUMBER]}.{discovery_info.properties[ATTR_ITEM_NUMBER]}.{self._serial_number}@products.bang-olufsen.com" diff --git a/tests/components/bang_olufsen/test_config_flow.py b/tests/components/bang_olufsen/test_config_flow.py index ad513905f16..e637120a6ae 100644 --- a/tests/components/bang_olufsen/test_config_flow.py +++ b/tests/components/bang_olufsen/test_config_flow.py @@ -132,7 +132,7 @@ async def test_config_flow_zeroconf(hass: HomeAssistant, mock_mozart_client) -> assert result_confirm["type"] is FlowResultType.CREATE_ENTRY assert result_confirm["data"] == TEST_DATA_CREATE_ENTRY - assert mock_mozart_client.get_beolink_self.call_count == 0 + assert mock_mozart_client.get_beolink_self.call_count == 1 async def test_config_flow_zeroconf_not_mozart_device(hass: HomeAssistant) -> None: @@ -159,3 +159,21 @@ async def test_config_flow_zeroconf_ipv6(hass: HomeAssistant) -> None: assert result_user["type"] is FlowResultType.ABORT assert result_user["reason"] == "ipv6_address" + + +async def test_config_flow_zeroconf_invalid_ip( + hass: HomeAssistant, mock_mozart_client +) -> None: + """Test zeroconf discovery with invalid IP address.""" + mock_mozart_client.get_beolink_self.side_effect = ClientConnectorError( + Mock(), Mock() + ) + + result_user = await hass.config_entries.flow.async_init( + handler=DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DATA_ZEROCONF, + ) + + assert result_user["type"] is FlowResultType.ABORT + assert result_user["reason"] == "invalid_address" From ad50136dbd23bd074a5ddd84085d927538df7252 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 29 Jul 2024 22:08:46 +0200 Subject: [PATCH 1694/2411] Add created_at/modified_at to config entries (#122456) --- homeassistant/config_entries.py | 65 +++-- tests/components/aemet/test_diagnostics.py | 3 +- tests/components/airly/test_diagnostics.py | 3 +- tests/components/airnow/test_diagnostics.py | 8 +- .../components/airvisual/test_diagnostics.py | 8 +- .../airvisual_pro/test_diagnostics.py | 8 +- tests/components/airzone/test_diagnostics.py | 3 +- .../airzone_cloud/test_diagnostics.py | 3 +- .../ambient_station/test_diagnostics.py | 8 +- tests/components/axis/test_diagnostics.py | 8 +- tests/components/blink/test_diagnostics.py | 2 +- tests/components/braviatv/test_diagnostics.py | 3 +- .../components/co2signal/test_diagnostics.py | 3 +- tests/components/coinbase/test_diagnostics.py | 3 +- .../components/config/test_config_entries.py | 115 ++++++++- tests/components/deconz/test_diagnostics.py | 8 +- .../devolo_home_control/test_diagnostics.py | 3 +- .../devolo_home_network/test_diagnostics.py | 3 +- .../dsmr_reader/test_diagnostics.py | 3 +- tests/components/ecovacs/test_diagnostics.py | 2 +- tests/components/esphome/test_diagnostics.py | 5 +- tests/components/fronius/test_diagnostics.py | 14 +- tests/components/fyta/test_diagnostics.py | 3 +- tests/components/gios/test_diagnostics.py | 5 +- tests/components/goodwe/test_diagnostics.py | 3 +- .../google_assistant/test_diagnostics.py | 2 +- tests/components/guardian/test_diagnostics.py | 4 +- .../husqvarna_automower/test_diagnostics.py | 3 +- tests/components/imgw_pib/test_diagnostics.py | 2 +- tests/components/iqvia/test_diagnostics.py | 8 +- .../kostal_plenticore/test_diagnostics.py | 4 +- .../lacrosse_view/test_diagnostics.py | 8 +- .../linear_garage_door/test_diagnostics.py | 3 +- tests/components/melcloud/test_diagnostics.py | 3 +- tests/components/netatmo/test_diagnostics.py | 9 +- tests/components/nextdns/test_diagnostics.py | 5 +- tests/components/notion/test_diagnostics.py | 3 + tests/components/onvif/test_diagnostics.py | 5 +- tests/components/openuv/test_diagnostics.py | 3 + .../components/philips_js/test_diagnostics.py | 2 +- tests/components/pi_hole/test_diagnostics.py | 5 +- .../components/proximity/test_diagnostics.py | 9 +- .../components/purpleair/test_diagnostics.py | 3 + .../rainmachine/test_diagnostics.py | 15 +- .../recollect_waste/test_diagnostics.py | 3 + tests/components/ridwell/test_diagnostics.py | 8 +- .../components/samsungtv/test_diagnostics.py | 7 + .../screenlogic/test_diagnostics.py | 3 +- .../sensibo/snapshots/test_diagnostics.ambr | 241 ------------------ tests/components/sensibo/test_diagnostics.py | 19 +- .../components/simplisafe/test_diagnostics.py | 3 + .../switcher_kis/test_diagnostics.py | 3 + .../systemmonitor/test_diagnostics.py | 2 +- .../tankerkoenig/test_diagnostics.py | 3 +- tests/components/tractive/test_diagnostics.py | 3 +- tests/components/twinkly/test_diagnostics.py | 5 +- tests/components/unifi/test_diagnostics.py | 8 +- .../utility_meter/test_diagnostics.py | 8 +- tests/components/v2c/test_diagnostics.py | 8 +- tests/components/vicare/test_diagnostics.py | 3 +- tests/components/watttime/test_diagnostics.py | 2 +- tests/components/webmin/test_diagnostics.py | 10 +- tests/components/webostv/test_diagnostics.py | 2 + .../components/whirlpool/test_diagnostics.py | 2 +- tests/snapshots/test_config_entries.ambr | 2 + tests/syrupy.py | 3 +- tests/test_config_entries.py | 96 ++++++- 67 files changed, 440 insertions(+), 392 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 70c87392d0b..aa0113cd7ce 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -15,6 +15,7 @@ from collections.abc import ( ) from contextvars import ContextVar from copy import deepcopy +from datetime import datetime from enum import Enum, StrEnum import functools from functools import cached_property @@ -69,6 +70,7 @@ from .setup import ( from .util import ulid as ulid_util from .util.async_ import create_eager_task from .util.decorator import Registry +from .util.dt import utc_from_timestamp, utcnow from .util.enum import try_parse_enum if TYPE_CHECKING: @@ -118,7 +120,7 @@ HANDLERS: Registry[str, type[ConfigFlow]] = Registry() STORAGE_KEY = "core.config_entries" STORAGE_VERSION = 1 -STORAGE_VERSION_MINOR = 2 +STORAGE_VERSION_MINOR = 3 SAVE_DELAY = 1 @@ -303,15 +305,19 @@ class ConfigEntry(Generic[_DataT]): _background_tasks: set[asyncio.Future[Any]] _integration_for_domain: loader.Integration | None _tries: int + created_at: datetime + modified_at: datetime def __init__( self, *, + created_at: datetime | None = None, data: Mapping[str, Any], disabled_by: ConfigEntryDisabler | None = None, domain: str, entry_id: str | None = None, minor_version: int, + modified_at: datetime | None = None, options: Mapping[str, Any] | None, pref_disable_new_entities: bool | None = None, pref_disable_polling: bool | None = None, @@ -415,6 +421,8 @@ class ConfigEntry(Generic[_DataT]): _setter(self, "_integration_for_domain", None) _setter(self, "_tries", 0) + _setter(self, "created_at", created_at or utcnow()) + _setter(self, "modified_at", modified_at or utcnow()) def __repr__(self) -> str: """Representation of ConfigEntry.""" @@ -483,8 +491,10 @@ class ConfigEntry(Generic[_DataT]): def as_json_fragment(self) -> json_fragment: """Return JSON fragment of a config entry.""" json_repr = { + "created_at": self.created_at.timestamp(), "entry_id": self.entry_id, "domain": self.domain, + "modified_at": self.modified_at.timestamp(), "title": self.title, "source": self.source, "state": self.state.value, @@ -831,6 +841,10 @@ class ConfigEntry(Generic[_DataT]): async def async_remove(self, hass: HomeAssistant) -> None: """Invoke remove callback on component.""" + old_modified_at = self.modified_at + object.__setattr__(self, "modified_at", utcnow()) + self.clear_cache() + if self.source == SOURCE_IGNORE: return @@ -862,6 +876,8 @@ class ConfigEntry(Generic[_DataT]): self.title, integration.domain, ) + # Restore modified_at + object.__setattr__(self, "modified_at", old_modified_at) @callback def _async_set_state( @@ -950,11 +966,13 @@ class ConfigEntry(Generic[_DataT]): def as_dict(self) -> dict[str, Any]: """Return dictionary version of this entry.""" return { + "created_at": self.created_at.isoformat(), "data": dict(self.data), "disabled_by": self.disabled_by, "domain": self.domain, "entry_id": self.entry_id, "minor_version": self.minor_version, + "modified_at": self.modified_at.isoformat(), "options": dict(self.options), "pref_disable_new_entities": self.pref_disable_new_entities, "pref_disable_polling": self.pref_disable_polling, @@ -1599,25 +1617,34 @@ class ConfigEntryStore(storage.Store[dict[str, list[dict[str, Any]]]]): ) -> dict[str, Any]: """Migrate to the new version.""" data = old_data - if old_major_version == 1 and old_minor_version < 2: - # Version 1.2 implements migration and freezes the available keys - for entry in data["entries"]: - # Populate keys which were introduced before version 1.2 + if old_major_version == 1: + if old_minor_version < 2: + # Version 1.2 implements migration and freezes the available keys + for entry in data["entries"]: + # Populate keys which were introduced before version 1.2 - pref_disable_new_entities = entry.get("pref_disable_new_entities") - if pref_disable_new_entities is None and "system_options" in entry: - pref_disable_new_entities = entry.get("system_options", {}).get( - "disable_new_entities" + pref_disable_new_entities = entry.get("pref_disable_new_entities") + if pref_disable_new_entities is None and "system_options" in entry: + pref_disable_new_entities = entry.get("system_options", {}).get( + "disable_new_entities" + ) + + entry.setdefault("disabled_by", entry.get("disabled_by")) + entry.setdefault("minor_version", entry.get("minor_version", 1)) + entry.setdefault("options", entry.get("options", {})) + entry.setdefault( + "pref_disable_new_entities", pref_disable_new_entities ) + entry.setdefault( + "pref_disable_polling", entry.get("pref_disable_polling") + ) + entry.setdefault("unique_id", entry.get("unique_id")) - entry.setdefault("disabled_by", entry.get("disabled_by")) - entry.setdefault("minor_version", entry.get("minor_version", 1)) - entry.setdefault("options", entry.get("options", {})) - entry.setdefault("pref_disable_new_entities", pref_disable_new_entities) - entry.setdefault( - "pref_disable_polling", entry.get("pref_disable_polling") - ) - entry.setdefault("unique_id", entry.get("unique_id")) + if old_minor_version < 3: + # Version 1.3 adds the created_at and modified_at fields + created_at = utc_from_timestamp(0).isoformat() + for entry in data["entries"]: + entry["created_at"] = entry["modified_at"] = created_at if old_major_version > 1: raise NotImplementedError @@ -1793,11 +1820,13 @@ class ConfigEntries: entry_id = entry["entry_id"] config_entry = ConfigEntry( + created_at=datetime.fromisoformat(entry["created_at"]), data=entry["data"], disabled_by=try_parse_enum(ConfigEntryDisabler, entry["disabled_by"]), domain=entry["domain"], entry_id=entry_id, minor_version=entry["minor_version"], + modified_at=datetime.fromisoformat(entry["modified_at"]), options=entry["options"], pref_disable_new_entities=entry["pref_disable_new_entities"], pref_disable_polling=entry["pref_disable_polling"], @@ -2014,6 +2043,8 @@ class ConfigEntries: if not changed: return False + _setter(entry, "modified_at", utcnow()) + for listener in entry.update_listeners: self.hass.async_create_task( listener(self.hass, entry), diff --git a/tests/components/aemet/test_diagnostics.py b/tests/components/aemet/test_diagnostics.py index 0d94995a85b..6d007dd0465 100644 --- a/tests/components/aemet/test_diagnostics.py +++ b/tests/components/aemet/test_diagnostics.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.components.aemet.const import DOMAIN from homeassistant.core import HomeAssistant @@ -30,4 +31,4 @@ async def test_config_entry_diagnostics( return_value={}, ): result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/airly/test_diagnostics.py b/tests/components/airly/test_diagnostics.py index 7364824e594..9a61bf5abee 100644 --- a/tests/components/airly/test_diagnostics.py +++ b/tests/components/airly/test_diagnostics.py @@ -1,6 +1,7 @@ """Test Airly diagnostics.""" from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -22,4 +23,4 @@ async def test_entry_diagnostics( result = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/airnow/test_diagnostics.py b/tests/components/airnow/test_diagnostics.py index 7329398e789..eb79dabe51a 100644 --- a/tests/components/airnow/test_diagnostics.py +++ b/tests/components/airnow/test_diagnostics.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -27,7 +28,6 @@ async def test_entry_diagnostics( return_value="PST", ): assert await hass.config_entries.async_setup(config_entry.entry_id) - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/airvisual/test_diagnostics.py b/tests/components/airvisual/test_diagnostics.py index 072e4559705..0253f102c59 100644 --- a/tests/components/airvisual/test_diagnostics.py +++ b/tests/components/airvisual/test_diagnostics.py @@ -1,6 +1,7 @@ """Test AirVisual diagnostics.""" from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -16,7 +17,6 @@ async def test_entry_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/airvisual_pro/test_diagnostics.py b/tests/components/airvisual_pro/test_diagnostics.py index dd87d00be30..372b62eaf38 100644 --- a/tests/components/airvisual_pro/test_diagnostics.py +++ b/tests/components/airvisual_pro/test_diagnostics.py @@ -1,6 +1,7 @@ """Test AirVisual Pro diagnostics.""" from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -16,7 +17,6 @@ async def test_entry_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/airzone/test_diagnostics.py b/tests/components/airzone/test_diagnostics.py index 6a03b9f1985..bca75bca778 100644 --- a/tests/components/airzone/test_diagnostics.py +++ b/tests/components/airzone/test_diagnostics.py @@ -4,6 +4,7 @@ from unittest.mock import patch from aioairzone.const import RAW_HVAC, RAW_VERSION, RAW_WEBSERVER from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.components.airzone.const import DOMAIN from homeassistant.core import HomeAssistant @@ -37,4 +38,4 @@ async def test_config_entry_diagnostics( }, ): result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/airzone_cloud/test_diagnostics.py b/tests/components/airzone_cloud/test_diagnostics.py index 254dba16b09..d3e23fc7f4b 100644 --- a/tests/components/airzone_cloud/test_diagnostics.py +++ b/tests/components/airzone_cloud/test_diagnostics.py @@ -15,6 +15,7 @@ from aioairzone_cloud.const import ( RAW_WEBSERVERS, ) from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.components.airzone_cloud.const import DOMAIN from homeassistant.const import CONF_ID @@ -111,4 +112,4 @@ async def test_config_entry_diagnostics( return_value=RAW_DATA_MOCK, ): result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/ambient_station/test_diagnostics.py b/tests/components/ambient_station/test_diagnostics.py index 05161ba32cd..82db72eb9ca 100644 --- a/tests/components/ambient_station/test_diagnostics.py +++ b/tests/components/ambient_station/test_diagnostics.py @@ -1,6 +1,7 @@ """Test Ambient PWS diagnostics.""" from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.components.ambient_station import AmbientStationConfigEntry from homeassistant.core import HomeAssistant @@ -20,7 +21,6 @@ async def test_entry_diagnostics( """Test config entry diagnostics.""" ambient = config_entry.runtime_data ambient.stations = data_station - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/axis/test_diagnostics.py b/tests/components/axis/test_diagnostics.py index 07caf5b39de..e96ba88c2cd 100644 --- a/tests/components/axis/test_diagnostics.py +++ b/tests/components/axis/test_diagnostics.py @@ -2,6 +2,7 @@ import pytest from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -20,7 +21,6 @@ async def test_entry_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry_setup) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry_setup + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/blink/test_diagnostics.py b/tests/components/blink/test_diagnostics.py index 3b120d23038..d527633d4c9 100644 --- a/tests/components/blink/test_diagnostics.py +++ b/tests/components/blink/test_diagnostics.py @@ -31,4 +31,4 @@ async def test_entry_diagnostics( hass, hass_client, mock_config_entry ) - assert result == snapshot(exclude=props("entry_id")) + assert result == snapshot(exclude=props("entry_id", "created_at", "modified_at")) diff --git a/tests/components/braviatv/test_diagnostics.py b/tests/components/braviatv/test_diagnostics.py index 13f6c92fb76..a7bd1631788 100644 --- a/tests/components/braviatv/test_diagnostics.py +++ b/tests/components/braviatv/test_diagnostics.py @@ -3,6 +3,7 @@ from unittest.mock import patch from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.components.braviatv.const import CONF_USE_PSK, DOMAIN from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN @@ -71,4 +72,4 @@ async def test_entry_diagnostics( assert await async_setup_component(hass, DOMAIN, {}) result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/co2signal/test_diagnostics.py b/tests/components/co2signal/test_diagnostics.py index edc0007952b..3d5e1a0580b 100644 --- a/tests/components/co2signal/test_diagnostics.py +++ b/tests/components/co2signal/test_diagnostics.py @@ -2,6 +2,7 @@ import pytest from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -20,4 +21,4 @@ async def test_entry_diagnostics( """Test config entry diagnostics.""" result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/coinbase/test_diagnostics.py b/tests/components/coinbase/test_diagnostics.py index e30bdef30b8..0e06c172c37 100644 --- a/tests/components/coinbase/test_diagnostics.py +++ b/tests/components/coinbase/test_diagnostics.py @@ -3,6 +3,7 @@ from unittest.mock import patch from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -40,4 +41,4 @@ async def test_entry_diagnostics( result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index b184fedf928..a4dc91d5355 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -6,6 +6,7 @@ from http import HTTPStatus from unittest.mock import ANY, AsyncMock, patch from aiohttp.test_utils import TestClient +from freezegun.api import FrozenDateTimeFactory import pytest import voluptuous as vol @@ -18,6 +19,7 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_flow, config_validation as cv from homeassistant.loader import IntegrationNotFound from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow from tests.common import ( MockConfigEntry, @@ -69,6 +71,7 @@ def mock_flow() -> Generator[None]: yield +@pytest.mark.usefixtures("freezer") @pytest.mark.usefixtures("clear_handlers", "mock_flow") async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None: """Test get entries.""" @@ -124,12 +127,15 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None: data = await resp.json() for entry in data: entry.pop("entry_id") + timestamp = utcnow().timestamp() assert data == [ { + "created_at": timestamp, "disabled_by": None, "domain": "comp1", "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -142,10 +148,12 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None: "title": "Test 1", }, { + "created_at": timestamp, "disabled_by": None, "domain": "comp2", "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": "Unsupported API", @@ -158,10 +166,12 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None: "title": "Test 2", }, { + "created_at": timestamp, "disabled_by": core_ce.ConfigEntryDisabler.USER, "domain": "comp3", "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -174,10 +184,12 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None: "title": "Test 3", }, { + "created_at": timestamp, "disabled_by": None, "domain": "comp4", "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -190,10 +202,12 @@ async def test_get_entries(hass: HomeAssistant, client: TestClient) -> None: "title": "Test 4", }, { + "created_at": timestamp, "disabled_by": None, "domain": "comp5", "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -509,7 +523,7 @@ async def test_abort(hass: HomeAssistant, client: TestClient) -> None: } -@pytest.mark.usefixtures("enable_custom_integrations") +@pytest.mark.usefixtures("enable_custom_integrations", "freezer") async def test_create_account(hass: HomeAssistant, client: TestClient) -> None: """Test a flow that creates an account.""" mock_platform(hass, "test.config_flow", None) @@ -536,6 +550,7 @@ async def test_create_account(hass: HomeAssistant, client: TestClient) -> None: entries = hass.config_entries.async_entries("test") assert len(entries) == 1 + timestamp = utcnow().timestamp() data = await resp.json() data.pop("flow_id") assert data == { @@ -544,11 +559,13 @@ async def test_create_account(hass: HomeAssistant, client: TestClient) -> None: "type": "create_entry", "version": 1, "result": { + "created_at": timestamp, "disabled_by": None, "domain": "test", "entry_id": entries[0].entry_id, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -567,7 +584,7 @@ async def test_create_account(hass: HomeAssistant, client: TestClient) -> None: } -@pytest.mark.usefixtures("enable_custom_integrations") +@pytest.mark.usefixtures("enable_custom_integrations", "freezer") async def test_two_step_flow(hass: HomeAssistant, client: TestClient) -> None: """Test we can finish a two step flow.""" mock_integration( @@ -616,6 +633,7 @@ async def test_two_step_flow(hass: HomeAssistant, client: TestClient) -> None: entries = hass.config_entries.async_entries("test") assert len(entries) == 1 + timestamp = utcnow().timestamp() data = await resp.json() data.pop("flow_id") assert data == { @@ -624,11 +642,13 @@ async def test_two_step_flow(hass: HomeAssistant, client: TestClient) -> None: "title": "user-title", "version": 1, "result": { + "created_at": timestamp, "disabled_by": None, "domain": "test", "entry_id": entries[0].entry_id, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1059,6 +1079,7 @@ async def test_options_flow_with_invalid_data( assert data == {"errors": {"choices": "invalid is not a valid option"}} +@pytest.mark.usefixtures("freezer") async def test_get_single( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -1080,13 +1101,16 @@ async def test_get_single( ) response = await ws_client.receive_json() + timestamp = utcnow().timestamp() assert response["success"] assert response["result"]["config_entry"] == { + "created_at": timestamp, "disabled_by": None, "domain": "test", "entry_id": entry.entry_id, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1366,7 +1390,7 @@ async def test_ignore_flow_nonexisting( assert response["error"]["code"] == "not_found" -@pytest.mark.usefixtures("clear_handlers") +@pytest.mark.usefixtures("clear_handlers", "freezer") async def test_get_matching_entries_ws( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -1420,13 +1444,16 @@ async def test_get_matching_entries_ws( await ws_client.send_json_auto_id({"type": "config_entries/get"}) response = await ws_client.receive_json() + timestamp = utcnow().timestamp() assert response["result"] == [ { + "created_at": timestamp, "disabled_by": None, "domain": "comp1", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1439,11 +1466,13 @@ async def test_get_matching_entries_ws( "title": "Test 1", }, { + "created_at": timestamp, "disabled_by": None, "domain": "comp2", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": "Unsupported API", @@ -1456,11 +1485,13 @@ async def test_get_matching_entries_ws( "title": "Test 2", }, { + "created_at": timestamp, "disabled_by": "user", "domain": "comp3", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1473,11 +1504,13 @@ async def test_get_matching_entries_ws( "title": "Test 3", }, { + "created_at": timestamp, "disabled_by": None, "domain": "comp4", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1490,11 +1523,13 @@ async def test_get_matching_entries_ws( "title": "Test 4", }, { + "created_at": timestamp, "disabled_by": None, "domain": "comp5", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1518,11 +1553,13 @@ async def test_get_matching_entries_ws( response = await ws_client.receive_json() assert response["result"] == [ { + "created_at": timestamp, "disabled_by": None, "domain": "comp1", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1545,11 +1582,13 @@ async def test_get_matching_entries_ws( response = await ws_client.receive_json() assert response["result"] == [ { + "created_at": timestamp, "disabled_by": None, "domain": "comp4", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1562,11 +1601,13 @@ async def test_get_matching_entries_ws( "title": "Test 4", }, { + "created_at": timestamp, "disabled_by": None, "domain": "comp5", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1589,11 +1630,13 @@ async def test_get_matching_entries_ws( response = await ws_client.receive_json() assert response["result"] == [ { + "created_at": timestamp, "disabled_by": None, "domain": "comp1", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1606,11 +1649,13 @@ async def test_get_matching_entries_ws( "title": "Test 1", }, { + "created_at": timestamp, "disabled_by": "user", "domain": "comp3", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1639,11 +1684,13 @@ async def test_get_matching_entries_ws( assert response["result"] == [ { + "created_at": timestamp, "disabled_by": None, "domain": "comp1", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1656,11 +1703,13 @@ async def test_get_matching_entries_ws( "title": "Test 1", }, { + "created_at": timestamp, "disabled_by": None, "domain": "comp2", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": "Unsupported API", @@ -1673,11 +1722,13 @@ async def test_get_matching_entries_ws( "title": "Test 2", }, { + "created_at": timestamp, "disabled_by": "user", "domain": "comp3", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1690,11 +1741,13 @@ async def test_get_matching_entries_ws( "title": "Test 3", }, { + "created_at": timestamp, "disabled_by": None, "domain": "comp4", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1707,11 +1760,13 @@ async def test_get_matching_entries_ws( "title": "Test 4", }, { + "created_at": timestamp, "disabled_by": None, "domain": "comp5", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1759,7 +1814,9 @@ async def test_get_matching_entries_ws( @pytest.mark.usefixtures("clear_handlers") async def test_subscribe_entries_ws( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, ) -> None: """Test subscribe entries with the websocket api.""" assert await async_setup_component(hass, "config", {}) @@ -1805,15 +1862,18 @@ async def test_subscribe_entries_ws( assert response["type"] == "result" response = await ws_client.receive_json() assert response["id"] == 5 + created = utcnow().timestamp() assert response["event"] == [ { "type": None, "entry": { + "created_at": created, "disabled_by": None, "domain": "comp1", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": created, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1829,11 +1889,13 @@ async def test_subscribe_entries_ws( { "type": None, "entry": { + "created_at": created, "disabled_by": None, "domain": "comp2", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": created, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": "Unsupported API", @@ -1849,11 +1911,13 @@ async def test_subscribe_entries_ws( { "type": None, "entry": { + "created_at": created, "disabled_by": "user", "domain": "comp3", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": created, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1867,17 +1931,21 @@ async def test_subscribe_entries_ws( }, }, ] + freezer.tick() + modified = utcnow().timestamp() assert hass.config_entries.async_update_entry(entry, title="changed") response = await ws_client.receive_json() assert response["id"] == 5 assert response["event"] == [ { "entry": { + "created_at": created, "disabled_by": None, "domain": "comp1", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": modified, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1892,17 +1960,21 @@ async def test_subscribe_entries_ws( "type": "updated", } ] + freezer.tick() + modified = utcnow().timestamp() await hass.config_entries.async_remove(entry.entry_id) response = await ws_client.receive_json() assert response["id"] == 5 assert response["event"] == [ { "entry": { + "created_at": created, "disabled_by": None, "domain": "comp1", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": modified, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1917,17 +1989,20 @@ async def test_subscribe_entries_ws( "type": "removed", } ] + freezer.tick() await hass.config_entries.async_add(entry) response = await ws_client.receive_json() assert response["id"] == 5 assert response["event"] == [ { "entry": { + "created_at": entry.created_at.timestamp(), "disabled_by": None, "domain": "comp1", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": entry.modified_at.timestamp(), "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -1946,9 +2021,12 @@ async def test_subscribe_entries_ws( @pytest.mark.usefixtures("clear_handlers") async def test_subscribe_entries_ws_filtered( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, ) -> None: """Test subscribe entries with the websocket api with a type filter.""" + created = utcnow().timestamp() assert await async_setup_component(hass, "config", {}) mock_integration(hass, MockModule("comp1")) mock_integration( @@ -2008,11 +2086,13 @@ async def test_subscribe_entries_ws_filtered( { "type": None, "entry": { + "created_at": created, "disabled_by": None, "domain": "comp1", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": created, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -2028,11 +2108,13 @@ async def test_subscribe_entries_ws_filtered( { "type": None, "entry": { + "created_at": created, "disabled_by": "user", "domain": "comp3", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": created, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -2046,6 +2128,8 @@ async def test_subscribe_entries_ws_filtered( }, }, ] + freezer.tick() + modified = utcnow().timestamp() assert hass.config_entries.async_update_entry(entry, title="changed") assert hass.config_entries.async_update_entry(entry3, title="changed too") assert hass.config_entries.async_update_entry(entry4, title="changed but ignored") @@ -2054,11 +2138,13 @@ async def test_subscribe_entries_ws_filtered( assert response["event"] == [ { "entry": { + "created_at": created, "disabled_by": None, "domain": "comp1", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": modified, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -2078,11 +2164,13 @@ async def test_subscribe_entries_ws_filtered( assert response["event"] == [ { "entry": { + "created_at": created, "disabled_by": "user", "domain": "comp3", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": modified, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -2097,6 +2185,8 @@ async def test_subscribe_entries_ws_filtered( "type": "updated", } ] + freezer.tick() + modified = utcnow().timestamp() await hass.config_entries.async_remove(entry.entry_id) await hass.config_entries.async_remove(entry2.entry_id) response = await ws_client.receive_json() @@ -2104,11 +2194,13 @@ async def test_subscribe_entries_ws_filtered( assert response["event"] == [ { "entry": { + "created_at": created, "disabled_by": None, "domain": "comp1", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": modified, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -2123,17 +2215,20 @@ async def test_subscribe_entries_ws_filtered( "type": "removed", } ] + freezer.tick() await hass.config_entries.async_add(entry) response = await ws_client.receive_json() assert response["id"] == 5 assert response["event"] == [ { "entry": { + "created_at": entry.created_at.timestamp(), "disabled_by": None, "domain": "comp1", "entry_id": ANY, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": entry.modified_at.timestamp(), "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, @@ -2238,8 +2333,11 @@ async def test_flow_with_multiple_schema_errors_base( } -@pytest.mark.usefixtures("enable_custom_integrations") -async def test_supports_reconfigure(hass: HomeAssistant, client: TestClient) -> None: +@pytest.mark.usefixtures("enable_custom_integrations", "freezer") +async def test_supports_reconfigure( + hass: HomeAssistant, + client: TestClient, +) -> None: """Test a flow that support reconfigure step.""" mock_platform(hass, "test.config_flow", None) @@ -2297,6 +2395,7 @@ async def test_supports_reconfigure(hass: HomeAssistant, client: TestClient) -> assert len(entries) == 1 data = await resp.json() + timestamp = utcnow().timestamp() data.pop("flow_id") assert data == { "handler": "test", @@ -2304,11 +2403,13 @@ async def test_supports_reconfigure(hass: HomeAssistant, client: TestClient) -> "type": "create_entry", "version": 1, "result": { + "created_at": timestamp, "disabled_by": None, "domain": "test", "entry_id": entries[0].entry_id, "error_reason_translation_key": None, "error_reason_translation_placeholders": None, + "modified_at": timestamp, "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, diff --git a/tests/components/deconz/test_diagnostics.py b/tests/components/deconz/test_diagnostics.py index a490c95d5e6..2abc6d83995 100644 --- a/tests/components/deconz/test_diagnostics.py +++ b/tests/components/deconz/test_diagnostics.py @@ -2,6 +2,7 @@ from pydeconz.websocket import State from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -23,7 +24,6 @@ async def test_entry_diagnostics( await mock_websocket_state(State.RUNNING) await hass.async_block_till_done() - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry_setup) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry_setup + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/devolo_home_control/test_diagnostics.py b/tests/components/devolo_home_control/test_diagnostics.py index f52a9d49017..dfadc4d1c4b 100644 --- a/tests/components/devolo_home_control/test_diagnostics.py +++ b/tests/components/devolo_home_control/test_diagnostics.py @@ -5,6 +5,7 @@ from __future__ import annotations from unittest.mock import patch from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -35,4 +36,4 @@ async def test_entry_diagnostics( assert entry.state is ConfigEntryState.LOADED result = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/devolo_home_network/test_diagnostics.py b/tests/components/devolo_home_network/test_diagnostics.py index a3580cac954..05d3c594677 100644 --- a/tests/components/devolo_home_network/test_diagnostics.py +++ b/tests/components/devolo_home_network/test_diagnostics.py @@ -4,6 +4,7 @@ from __future__ import annotations import pytest from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -28,4 +29,4 @@ async def test_entry_diagnostics( assert entry.state is ConfigEntryState.LOADED result = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/dsmr_reader/test_diagnostics.py b/tests/components/dsmr_reader/test_diagnostics.py index 553efd0b38b..793fe1362b0 100644 --- a/tests/components/dsmr_reader/test_diagnostics.py +++ b/tests/components/dsmr_reader/test_diagnostics.py @@ -3,6 +3,7 @@ from unittest.mock import patch from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.components.dsmr_reader.const import DOMAIN from homeassistant.core import HomeAssistant @@ -36,4 +37,4 @@ async def test_get_config_entry_diagnostics( diagnostics = await get_diagnostics_for_config_entry( hass, hass_client, config_entry ) - assert diagnostics == snapshot + assert diagnostics == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/ecovacs/test_diagnostics.py b/tests/components/ecovacs/test_diagnostics.py index b025db43cc0..6e4dcd5f677 100644 --- a/tests/components/ecovacs/test_diagnostics.py +++ b/tests/components/ecovacs/test_diagnostics.py @@ -28,4 +28,4 @@ async def test_diagnostics( """Test diagnostics.""" assert await get_diagnostics_for_config_entry( hass, hass_client, init_integration - ) == snapshot(exclude=props("entry_id")) + ) == snapshot(exclude=props("entry_id", "created_at", "modified_at")) diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index 03689a5699e..b66b6d72fce 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -5,6 +5,7 @@ from unittest.mock import ANY import pytest from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.components import bluetooth from homeassistant.core import HomeAssistant @@ -27,7 +28,7 @@ async def test_diagnostics( """Test diagnostics for config entry.""" result = await get_diagnostics_for_config_entry(hass, hass_client, init_integration) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) async def test_diagnostics_with_bluetooth( @@ -61,6 +62,7 @@ async def test_diagnostics_with_bluetooth( }, }, "config": { + "created_at": ANY, "data": { "device_name": "test", "host": "test.local", @@ -71,6 +73,7 @@ async def test_diagnostics_with_bluetooth( "domain": "esphome", "entry_id": ANY, "minor_version": 1, + "modified_at": ANY, "options": {"allow_service_calls": False}, "pref_disable_new_entities": False, "pref_disable_polling": False, diff --git a/tests/components/fronius/test_diagnostics.py b/tests/components/fronius/test_diagnostics.py index 7b1f384e405..ddef5b4a18c 100644 --- a/tests/components/fronius/test_diagnostics.py +++ b/tests/components/fronius/test_diagnostics.py @@ -1,6 +1,7 @@ """Tests for the diagnostics data provided by the Fronius integration.""" from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -21,11 +22,8 @@ async def test_diagnostics( mock_responses(aioclient_mock) entry = await setup_fronius_integration(hass) - assert ( - await get_diagnostics_for_config_entry( - hass, - hass_client, - entry, - ) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, + hass_client, + entry, + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/fyta/test_diagnostics.py b/tests/components/fyta/test_diagnostics.py index 3a95b533489..cfaa5484b82 100644 --- a/tests/components/fyta/test_diagnostics.py +++ b/tests/components/fyta/test_diagnostics.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -28,4 +29,4 @@ async def test_entry_diagnostics( hass, hass_client, mock_config_entry ) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/gios/test_diagnostics.py b/tests/components/gios/test_diagnostics.py index 903de4872a2..a965e5550df 100644 --- a/tests/components/gios/test_diagnostics.py +++ b/tests/components/gios/test_diagnostics.py @@ -1,6 +1,7 @@ """Test GIOS diagnostics.""" from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -18,4 +19,6 @@ async def test_entry_diagnostics( """Test config entry diagnostics.""" entry = await init_integration(hass) - assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot( + exclude=props("created_at", "modified_at") + ) diff --git a/tests/components/goodwe/test_diagnostics.py b/tests/components/goodwe/test_diagnostics.py index 21917265811..0a997edc594 100644 --- a/tests/components/goodwe/test_diagnostics.py +++ b/tests/components/goodwe/test_diagnostics.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.components.goodwe import CONF_MODEL_FAMILY, DOMAIN from homeassistant.const import CONF_HOST @@ -32,4 +33,4 @@ async def test_entry_diagnostics( assert await async_setup_component(hass, DOMAIN, {}) result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/google_assistant/test_diagnostics.py b/tests/components/google_assistant/test_diagnostics.py index 26d91ce7920..1d68079563c 100644 --- a/tests/components/google_assistant/test_diagnostics.py +++ b/tests/components/google_assistant/test_diagnostics.py @@ -50,4 +50,4 @@ async def test_diagnostics( config_entry = hass.config_entries.async_entries("google_assistant")[0] assert await get_diagnostics_for_config_entry( hass, hass_client, config_entry - ) == snapshot(exclude=props("entry_id")) + ) == snapshot(exclude=props("entry_id", "created_at", "modified_at")) diff --git a/tests/components/guardian/test_diagnostics.py b/tests/components/guardian/test_diagnostics.py index 6ec7376f3ef..3b3ed21bc65 100644 --- a/tests/components/guardian/test_diagnostics.py +++ b/tests/components/guardian/test_diagnostics.py @@ -4,7 +4,7 @@ from homeassistant.components.diagnostics import REDACTED from homeassistant.components.guardian import DOMAIN, GuardianData from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import ANY, MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -39,6 +39,8 @@ async def test_entry_diagnostics( "source": "user", "unique_id": REDACTED, "disabled_by": None, + "created_at": ANY, + "modified_at": ANY, }, "data": { "valve_controller": { diff --git a/tests/components/husqvarna_automower/test_diagnostics.py b/tests/components/husqvarna_automower/test_diagnostics.py index eeb6b46e6c4..3166b09f1ee 100644 --- a/tests/components/husqvarna_automower/test_diagnostics.py +++ b/tests/components/husqvarna_automower/test_diagnostics.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock import pytest from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props from homeassistant.components.husqvarna_automower.const import DOMAIN from homeassistant.core import HomeAssistant @@ -36,7 +37,7 @@ async def test_entry_diagnostics( result = await get_diagnostics_for_config_entry( hass, hass_client, mock_config_entry ) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) @pytest.mark.freeze_time(datetime.datetime(2024, 2, 29, 11, tzinfo=datetime.UTC)) diff --git a/tests/components/imgw_pib/test_diagnostics.py b/tests/components/imgw_pib/test_diagnostics.py index 62dabc982c4..14d4e7a5224 100644 --- a/tests/components/imgw_pib/test_diagnostics.py +++ b/tests/components/imgw_pib/test_diagnostics.py @@ -28,4 +28,4 @@ async def test_entry_diagnostics( hass, hass_client, mock_config_entry ) - assert result == snapshot(exclude=props("entry_id")) + assert result == snapshot(exclude=props("entry_id", "created_at", "modified_at")) diff --git a/tests/components/iqvia/test_diagnostics.py b/tests/components/iqvia/test_diagnostics.py index 21935a81e86..9d5639c311c 100644 --- a/tests/components/iqvia/test_diagnostics.py +++ b/tests/components/iqvia/test_diagnostics.py @@ -1,6 +1,7 @@ """Test IQVIA diagnostics.""" from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -18,7 +19,6 @@ async def test_entry_diagnostics( ) -> None: """Test config entry diagnostics.""" - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/kostal_plenticore/test_diagnostics.py b/tests/components/kostal_plenticore/test_diagnostics.py index 1c3a9efe2e5..0f358260be7 100644 --- a/tests/components/kostal_plenticore/test_diagnostics.py +++ b/tests/components/kostal_plenticore/test_diagnostics.py @@ -6,7 +6,7 @@ from homeassistant.components.diagnostics import REDACTED from homeassistant.components.kostal_plenticore.coordinator import Plenticore from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import ANY, MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -54,6 +54,8 @@ async def test_entry_diagnostics( "source": "user", "unique_id": None, "disabled_by": None, + "created_at": ANY, + "modified_at": ANY, }, "client": { "version": "api_version='0.2.0' hostname='scb' name='PUCK RESTful API' sw_version='01.16.05025'", diff --git a/tests/components/lacrosse_view/test_diagnostics.py b/tests/components/lacrosse_view/test_diagnostics.py index 08cef64a935..dc48f160113 100644 --- a/tests/components/lacrosse_view/test_diagnostics.py +++ b/tests/components/lacrosse_view/test_diagnostics.py @@ -3,6 +3,7 @@ from unittest.mock import patch from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props from homeassistant.components.lacrosse_view import DOMAIN from homeassistant.core import HomeAssistant @@ -32,7 +33,6 @@ async def test_entry_diagnostics( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/linear_garage_door/test_diagnostics.py b/tests/components/linear_garage_door/test_diagnostics.py index 6bf7415bde5..a00feed43ff 100644 --- a/tests/components/linear_garage_door/test_diagnostics.py +++ b/tests/components/linear_garage_door/test_diagnostics.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -25,4 +26,4 @@ async def test_entry_diagnostics( result = await get_diagnostics_for_config_entry( hass, hass_client, mock_config_entry ) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/melcloud/test_diagnostics.py b/tests/components/melcloud/test_diagnostics.py index cbb35eadfd4..32ec94a54d1 100644 --- a/tests/components/melcloud/test_diagnostics.py +++ b/tests/components/melcloud/test_diagnostics.py @@ -3,6 +3,7 @@ from unittest.mock import patch from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.components.melcloud.const import DOMAIN from homeassistant.core import HomeAssistant @@ -36,4 +37,4 @@ async def test_get_config_entry_diagnostics( diagnostics = await get_diagnostics_for_config_entry( hass, hass_client, config_entry ) - assert diagnostics == snapshot + assert diagnostics == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/netatmo/test_diagnostics.py b/tests/components/netatmo/test_diagnostics.py index 48f021295e1..7a0bf11c652 100644 --- a/tests/components/netatmo/test_diagnostics.py +++ b/tests/components/netatmo/test_diagnostics.py @@ -42,4 +42,11 @@ async def test_entry_diagnostics( assert await get_diagnostics_for_config_entry( hass, hass_client, config_entry - ) == snapshot(exclude=paths("info.data.token.expires_at", "info.entry_id")) + ) == snapshot( + exclude=paths( + "info.data.token.expires_at", + "info.entry_id", + "info.created_at", + "info.modified_at", + ) + ) diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index 7652bc4f03e..3bb1fc3ee67 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -1,6 +1,7 @@ """Test NextDNS diagnostics.""" from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -18,4 +19,6 @@ async def test_entry_diagnostics( """Test config entry diagnostics.""" entry = await init_integration(hass) - assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot( + exclude=props("created_at", "modified_at") + ) diff --git a/tests/components/notion/test_diagnostics.py b/tests/components/notion/test_diagnostics.py index 023b9369f03..4d87b6292e4 100644 --- a/tests/components/notion/test_diagnostics.py +++ b/tests/components/notion/test_diagnostics.py @@ -4,6 +4,7 @@ from homeassistant.components.diagnostics import REDACTED from homeassistant.components.notion import DOMAIN from homeassistant.core import HomeAssistant +from tests.common import ANY from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -33,6 +34,8 @@ async def test_entry_diagnostics( "source": "user", "unique_id": REDACTED, "disabled_by": None, + "created_at": ANY, + "modified_at": ANY, }, "data": { "bridges": [ diff --git a/tests/components/onvif/test_diagnostics.py b/tests/components/onvif/test_diagnostics.py index d58c8008ea6..ce8febe2341 100644 --- a/tests/components/onvif/test_diagnostics.py +++ b/tests/components/onvif/test_diagnostics.py @@ -1,6 +1,7 @@ """Test ONVIF diagnostics.""" from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -19,4 +20,6 @@ async def test_diagnostics( entry, _, _ = await setup_onvif_integration(hass) - assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot( + exclude=props("created_at", "modified_at") + ) diff --git a/tests/components/openuv/test_diagnostics.py b/tests/components/openuv/test_diagnostics.py index 4b5114bccd1..4fe851eea53 100644 --- a/tests/components/openuv/test_diagnostics.py +++ b/tests/components/openuv/test_diagnostics.py @@ -4,6 +4,7 @@ from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import ANY from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -35,6 +36,8 @@ async def test_entry_diagnostics( "source": "user", "unique_id": REDACTED, "disabled_by": None, + "created_at": ANY, + "modified_at": ANY, }, "data": { "protection_window": { diff --git a/tests/components/philips_js/test_diagnostics.py b/tests/components/philips_js/test_diagnostics.py index cb3235b9780..d61546e52c3 100644 --- a/tests/components/philips_js/test_diagnostics.py +++ b/tests/components/philips_js/test_diagnostics.py @@ -63,4 +63,4 @@ async def test_entry_diagnostics( hass, hass_client, mock_config_entry ) - assert result == snapshot(exclude=props("entry_id")) + assert result == snapshot(exclude=props("entry_id", "created_at", "modified_at")) diff --git a/tests/components/pi_hole/test_diagnostics.py b/tests/components/pi_hole/test_diagnostics.py index c9fc9a0a9b8..8d5a83e4622 100644 --- a/tests/components/pi_hole/test_diagnostics.py +++ b/tests/components/pi_hole/test_diagnostics.py @@ -1,6 +1,7 @@ """Test pi_hole component.""" from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props from homeassistant.components import pi_hole from homeassistant.core import HomeAssistant @@ -28,4 +29,6 @@ async def test_diagnostics( await hass.async_block_till_done() - assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot( + exclude=props("created_at", "modified_at") + ) diff --git a/tests/components/proximity/test_diagnostics.py b/tests/components/proximity/test_diagnostics.py index a60c592fcab..e4f22236808 100644 --- a/tests/components/proximity/test_diagnostics.py +++ b/tests/components/proximity/test_diagnostics.py @@ -72,5 +72,12 @@ async def test_entry_diagnostics( assert await get_diagnostics_for_config_entry( hass, hass_client, mock_entry ) == snapshot( - exclude=props("entry_id", "last_changed", "last_reported", "last_updated") + exclude=props( + "entry_id", + "last_changed", + "last_reported", + "last_updated", + "created_at", + "modified_at", + ) ) diff --git a/tests/components/purpleair/test_diagnostics.py b/tests/components/purpleair/test_diagnostics.py index 13dcd1338e0..599549bb723 100644 --- a/tests/components/purpleair/test_diagnostics.py +++ b/tests/components/purpleair/test_diagnostics.py @@ -3,6 +3,7 @@ from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant +from tests.common import ANY from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -34,6 +35,8 @@ async def test_entry_diagnostics( "source": "user", "unique_id": REDACTED, "disabled_by": None, + "created_at": ANY, + "modified_at": ANY, }, "data": { "fields": [ diff --git a/tests/components/rainmachine/test_diagnostics.py b/tests/components/rainmachine/test_diagnostics.py index 1fc03ab357a..ad5743957dd 100644 --- a/tests/components/rainmachine/test_diagnostics.py +++ b/tests/components/rainmachine/test_diagnostics.py @@ -2,6 +2,7 @@ from regenmaschine.errors import RainMachineError from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -17,10 +18,9 @@ async def test_entry_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) async def test_entry_diagnostics_failed_controller_diagnostics( @@ -33,7 +33,6 @@ async def test_entry_diagnostics_failed_controller_diagnostics( ) -> None: """Test config entry diagnostics when the controller diagnostics API call fails.""" controller.diagnostics.current.side_effect = RainMachineError - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/recollect_waste/test_diagnostics.py b/tests/components/recollect_waste/test_diagnostics.py index 6c8549786e8..2b92892b1d1 100644 --- a/tests/components/recollect_waste/test_diagnostics.py +++ b/tests/components/recollect_waste/test_diagnostics.py @@ -5,6 +5,7 @@ from homeassistant.core import HomeAssistant from .conftest import TEST_SERVICE_ID +from tests.common import ANY from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -30,6 +31,8 @@ async def test_entry_diagnostics( "source": "user", "unique_id": REDACTED, "disabled_by": None, + "created_at": ANY, + "modified_at": ANY, }, "data": [ { diff --git a/tests/components/ridwell/test_diagnostics.py b/tests/components/ridwell/test_diagnostics.py index adfbb525283..45683bba903 100644 --- a/tests/components/ridwell/test_diagnostics.py +++ b/tests/components/ridwell/test_diagnostics.py @@ -1,6 +1,7 @@ """Test Ridwell diagnostics.""" from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -16,7 +17,6 @@ async def test_entry_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/samsungtv/test_diagnostics.py b/tests/components/samsungtv/test_diagnostics.py index 7b20002ae5b..b1bdf034bc1 100644 --- a/tests/components/samsungtv/test_diagnostics.py +++ b/tests/components/samsungtv/test_diagnostics.py @@ -16,6 +16,7 @@ from .const import ( SAMPLE_DEVICE_INFO_WIFI, ) +from tests.common import ANY from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -29,6 +30,7 @@ async def test_entry_diagnostics( assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { + "created_at": ANY, "data": { "host": "fake_host", "ip_address": "test", @@ -43,6 +45,7 @@ async def test_entry_diagnostics( "domain": "samsungtv", "entry_id": "123456", "minor_version": 2, + "modified_at": ANY, "options": {}, "pref_disable_new_entities": False, "pref_disable_polling": False, @@ -65,6 +68,7 @@ async def test_entry_diagnostics_encrypted( assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { + "created_at": ANY, "data": { "host": "fake_host", "ip_address": "test", @@ -80,6 +84,7 @@ async def test_entry_diagnostics_encrypted( "domain": "samsungtv", "entry_id": "123456", "minor_version": 2, + "modified_at": ANY, "options": {}, "pref_disable_new_entities": False, "pref_disable_polling": False, @@ -102,6 +107,7 @@ async def test_entry_diagnostics_encrypte_offline( assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { + "created_at": ANY, "data": { "host": "fake_host", "ip_address": "test", @@ -116,6 +122,7 @@ async def test_entry_diagnostics_encrypte_offline( "domain": "samsungtv", "entry_id": "123456", "minor_version": 2, + "modified_at": ANY, "options": {}, "pref_disable_new_entities": False, "pref_disable_polling": False, diff --git a/tests/components/screenlogic/test_diagnostics.py b/tests/components/screenlogic/test_diagnostics.py index c6d6ea60e87..77e1ce58dad 100644 --- a/tests/components/screenlogic/test_diagnostics.py +++ b/tests/components/screenlogic/test_diagnostics.py @@ -4,6 +4,7 @@ from unittest.mock import DEFAULT, patch from screenlogicpy import ScreenLogicGateway from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -56,4 +57,4 @@ async def test_diagnostics( hass, hass_client, mock_config_entry ) - assert diag == snapshot + assert diag == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/sensibo/snapshots/test_diagnostics.ambr b/tests/components/sensibo/snapshots/test_diagnostics.ambr index c911a7629be..a33209f7c88 100644 --- a/tests/components/sensibo/snapshots/test_diagnostics.ambr +++ b/tests/components/sensibo/snapshots/test_diagnostics.ambr @@ -1,246 +1,5 @@ # serializer version: 1 # name: test_diagnostics - dict({ - 'modes': dict({ - 'auto': dict({ - 'fanLevels': list([ - 'quiet', - 'low', - 'medium', - ]), - 'horizontalSwing': list([ - 'stopped', - 'fixedLeft', - 'fixedCenterLeft', - ]), - 'light': list([ - 'on', - 'off', - ]), - 'swing': list([ - 'stopped', - 'fixedTop', - 'fixedMiddleTop', - ]), - 'temperatures': dict({ - 'C': dict({ - 'isNative': True, - 'values': list([ - 10, - 16, - 17, - 18, - 19, - 20, - ]), - }), - 'F': dict({ - 'isNative': False, - 'values': list([ - 64, - 66, - 68, - ]), - }), - }), - }), - 'cool': dict({ - 'fanLevels': list([ - 'quiet', - 'low', - 'medium', - ]), - 'horizontalSwing': list([ - 'stopped', - 'fixedLeft', - 'fixedCenterLeft', - ]), - 'light': list([ - 'on', - 'off', - ]), - 'swing': list([ - 'stopped', - 'fixedTop', - 'fixedMiddleTop', - ]), - 'temperatures': dict({ - 'C': dict({ - 'isNative': True, - 'values': list([ - 10, - 16, - 17, - 18, - 19, - 20, - ]), - }), - 'F': dict({ - 'isNative': False, - 'values': list([ - 64, - 66, - 68, - ]), - }), - }), - }), - 'dry': dict({ - 'horizontalSwing': list([ - 'stopped', - 'fixedLeft', - 'fixedCenterLeft', - ]), - 'light': list([ - 'on', - 'off', - ]), - 'swing': list([ - 'stopped', - 'fixedTop', - 'fixedMiddleTop', - ]), - 'temperatures': dict({ - 'C': dict({ - 'isNative': True, - 'values': list([ - 10, - 16, - 17, - 18, - 19, - 20, - ]), - }), - 'F': dict({ - 'isNative': False, - 'values': list([ - 64, - 66, - 68, - ]), - }), - }), - }), - 'fan': dict({ - 'fanLevels': list([ - 'quiet', - 'low', - 'medium', - ]), - 'horizontalSwing': list([ - 'stopped', - 'fixedLeft', - 'fixedCenterLeft', - ]), - 'light': list([ - 'on', - 'off', - ]), - 'swing': list([ - 'stopped', - 'fixedTop', - 'fixedMiddleTop', - ]), - 'temperatures': dict({ - }), - }), - 'heat': dict({ - 'fanLevels': list([ - 'quiet', - 'low', - 'medium', - ]), - 'horizontalSwing': list([ - 'stopped', - 'fixedLeft', - 'fixedCenterLeft', - ]), - 'light': list([ - 'on', - 'off', - ]), - 'swing': list([ - 'stopped', - 'fixedTop', - 'fixedMiddleTop', - ]), - 'temperatures': dict({ - 'C': dict({ - 'isNative': True, - 'values': list([ - 10, - 16, - 17, - 18, - 19, - 20, - ]), - }), - 'F': dict({ - 'isNative': False, - 'values': list([ - 63, - 64, - 66, - ]), - }), - }), - }), - }), - }) -# --- -# name: test_diagnostics.1 - dict({ - 'low': 'low', - 'medium': 'medium', - 'quiet': 'quiet', - }) -# --- -# name: test_diagnostics.2 - dict({ - 'fixedmiddletop': 'fixedMiddleTop', - 'fixedtop': 'fixedTop', - 'stopped': 'stopped', - }) -# --- -# name: test_diagnostics.3 - dict({ - 'fixedcenterleft': 'fixedCenterLeft', - 'fixedleft': 'fixedLeft', - 'stopped': 'stopped', - }) -# --- -# name: test_diagnostics.4 - dict({ - 'fanlevel': 'low', - 'horizontalswing': 'stopped', - 'light': 'on', - 'mode': 'heat', - 'on': True, - 'swing': 'stopped', - 'targettemperature': 21, - 'temperatureunit': 'c', - }) -# --- -# name: test_diagnostics.5 - dict({ - 'fanlevel': 'high', - 'horizontalswing': 'stopped', - 'light': 'on', - 'mode': 'cool', - 'on': True, - 'swing': 'stopped', - 'targettemperature': 21, - 'temperatureunit': 'c', - }) -# --- -# name: test_diagnostics.6 - dict({ - }) -# --- -# name: test_diagnostics[full_snapshot] dict({ 'AAZZAAZZ': dict({ 'ac_states': dict({ diff --git a/tests/components/sensibo/test_diagnostics.py b/tests/components/sensibo/test_diagnostics.py index 1fe72cca0f3..0dc1f2c25e9 100644 --- a/tests/components/sensibo/test_diagnostics.py +++ b/tests/components/sensibo/test_diagnostics.py @@ -3,6 +3,7 @@ from __future__ import annotations from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -10,8 +11,6 @@ from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator -EXCLUDE_ATTRIBUTES = {"full_features"} - async def test_diagnostics( hass: HomeAssistant, @@ -24,16 +23,6 @@ async def test_diagnostics( diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert diag["ABC999111"]["full_capabilities"] == snapshot - assert diag["ABC999111"]["fan_modes_translated"] == snapshot - assert diag["ABC999111"]["swing_modes_translated"] == snapshot - assert diag["ABC999111"]["horizontal_swing_modes_translated"] == snapshot - assert diag["ABC999111"]["smart_low_state"] == snapshot - assert diag["ABC999111"]["smart_high_state"] == snapshot - assert diag["ABC999111"]["pure_conf"] == snapshot - - def limit_attrs(prop, path): - exclude_attrs = EXCLUDE_ATTRIBUTES - return prop in exclude_attrs - - assert diag == snapshot(name="full_snapshot", exclude=limit_attrs) + assert diag == snapshot( + exclude=props("full_features", "created_at", "modified_at"), + ) diff --git a/tests/components/simplisafe/test_diagnostics.py b/tests/components/simplisafe/test_diagnostics.py index 6948f98b159..31bd44c6146 100644 --- a/tests/components/simplisafe/test_diagnostics.py +++ b/tests/components/simplisafe/test_diagnostics.py @@ -3,6 +3,7 @@ from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant +from tests.common import ANY from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -28,6 +29,8 @@ async def test_entry_diagnostics( "source": "user", "unique_id": REDACTED, "disabled_by": None, + "created_at": ANY, + "modified_at": ANY, }, "subscription_data": { "12345": { diff --git a/tests/components/switcher_kis/test_diagnostics.py b/tests/components/switcher_kis/test_diagnostics.py index 107a48a1062..c8df4dd0b83 100644 --- a/tests/components/switcher_kis/test_diagnostics.py +++ b/tests/components/switcher_kis/test_diagnostics.py @@ -8,6 +8,7 @@ from homeassistant.core import HomeAssistant from . import init_integration from .consts import DUMMY_WATER_HEATER_DEVICE +from tests.common import ANY from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -64,5 +65,7 @@ async def test_diagnostics( "source": "user", "unique_id": "switcher_kis", "disabled_by": None, + "created_at": ANY, + "modified_at": ANY, }, } diff --git a/tests/components/systemmonitor/test_diagnostics.py b/tests/components/systemmonitor/test_diagnostics.py index 78128aad5f4..b0f4fca3d0c 100644 --- a/tests/components/systemmonitor/test_diagnostics.py +++ b/tests/components/systemmonitor/test_diagnostics.py @@ -23,4 +23,4 @@ async def test_diagnostics( """Test diagnostics.""" assert await get_diagnostics_for_config_entry( hass, hass_client, mock_added_config_entry - ) == snapshot(exclude=props("last_update", "entry_id")) + ) == snapshot(exclude=props("last_update", "entry_id", "created_at", "modified_at")) diff --git a/tests/components/tankerkoenig/test_diagnostics.py b/tests/components/tankerkoenig/test_diagnostics.py index 441268659f3..e7b479a0c32 100644 --- a/tests/components/tankerkoenig/test_diagnostics.py +++ b/tests/components/tankerkoenig/test_diagnostics.py @@ -4,6 +4,7 @@ from __future__ import annotations import pytest from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -21,4 +22,4 @@ async def test_entry_diagnostics( ) -> None: """Test config entry diagnostics.""" result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/tractive/test_diagnostics.py b/tests/components/tractive/test_diagnostics.py index cc4fcdeba15..ce07b4d6e2a 100644 --- a/tests/components/tractive/test_diagnostics.py +++ b/tests/components/tractive/test_diagnostics.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -27,4 +28,4 @@ async def test_entry_diagnostics( hass, hass_client, mock_config_entry ) - assert result == snapshot + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/twinkly/test_diagnostics.py b/tests/components/twinkly/test_diagnostics.py index 5cb9fc1fe9e..f9cf0bc562c 100644 --- a/tests/components/twinkly/test_diagnostics.py +++ b/tests/components/twinkly/test_diagnostics.py @@ -3,6 +3,7 @@ from collections.abc import Awaitable, Callable from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -26,4 +27,6 @@ async def test_diagnostics( await setup_integration() entry = hass.config_entries.async_entries(DOMAIN)[0] - assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot( + exclude=props("created_at", "modified_at") + ) diff --git a/tests/components/unifi/test_diagnostics.py b/tests/components/unifi/test_diagnostics.py index fcaba59cbad..3963de2deb3 100644 --- a/tests/components/unifi/test_diagnostics.py +++ b/tests/components/unifi/test_diagnostics.py @@ -2,6 +2,7 @@ import pytest from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, @@ -125,7 +126,6 @@ async def test_entry_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry_setup) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry_setup + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/utility_meter/test_diagnostics.py b/tests/components/utility_meter/test_diagnostics.py index cefd17fc7e4..9ecabe813b1 100644 --- a/tests/components/utility_meter/test_diagnostics.py +++ b/tests/components/utility_meter/test_diagnostics.py @@ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient from freezegun import freeze_time import pytest from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.auth.models import Credentials from homeassistant.components.utility_meter.const import DOMAIN @@ -45,11 +46,6 @@ def _get_test_client_generator( return auth_client -def limit_diagnostic_attrs(prop, path) -> bool: - """Mark attributes to exclude from diagnostic snapshot.""" - return prop in {"entry_id"} - - @freeze_time("2024-04-06 00:00:00+00:00") @pytest.mark.usefixtures("socket_enabled") async def test_diagnostics( @@ -125,4 +121,4 @@ async def test_diagnostics( hass, _get_test_client_generator(hass, aiohttp_client, new_token), config_entry ) - assert diag == snapshot(exclude=limit_diagnostic_attrs) + assert diag == snapshot(exclude=props("entry_id", "created_at", "modified_at")) diff --git a/tests/components/v2c/test_diagnostics.py b/tests/components/v2c/test_diagnostics.py index 770b00e988b..eafbd68e6fc 100644 --- a/tests/components/v2c/test_diagnostics.py +++ b/tests/components/v2c/test_diagnostics.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -24,7 +25,6 @@ async def test_entry_diagnostics( await init_integration(hass, mock_config_entry) - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) - == snapshot() - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/vicare/test_diagnostics.py b/tests/components/vicare/test_diagnostics.py index 815b39545a9..6adf4fe0edc 100644 --- a/tests/components/vicare/test_diagnostics.py +++ b/tests/components/vicare/test_diagnostics.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -21,4 +22,4 @@ async def test_diagnostics( hass, hass_client, mock_vicare_gas_boiler ) - assert diag == snapshot + assert diag == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/watttime/test_diagnostics.py b/tests/components/watttime/test_diagnostics.py index 0526a64aedc..f4465a44d26 100644 --- a/tests/components/watttime/test_diagnostics.py +++ b/tests/components/watttime/test_diagnostics.py @@ -19,4 +19,4 @@ async def test_entry_diagnostics( """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry( hass, hass_client, config_entry - ) == snapshot(exclude=props("entry_id")) + ) == snapshot(exclude=props("entry_id", "created_at", "modified_at")) diff --git a/tests/components/webmin/test_diagnostics.py b/tests/components/webmin/test_diagnostics.py index 5f1df44f4a8..98d6544bc76 100644 --- a/tests/components/webmin/test_diagnostics.py +++ b/tests/components/webmin/test_diagnostics.py @@ -1,6 +1,7 @@ """Tests for the diagnostics data provided by the Webmin integration.""" from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props from homeassistant.core import HomeAssistant @@ -16,9 +17,6 @@ async def test_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - assert ( - await get_diagnostics_for_config_entry( - hass, hass_client, await async_init_integration(hass) - ) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, await async_init_integration(hass) + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/webostv/test_diagnostics.py b/tests/components/webostv/test_diagnostics.py index 934b59a7b83..e2fbc43e187 100644 --- a/tests/components/webostv/test_diagnostics.py +++ b/tests/components/webostv/test_diagnostics.py @@ -58,5 +58,7 @@ async def test_diagnostics( "source": "user", "unique_id": REDACTED, "disabled_by": None, + "created_at": entry.created_at.isoformat(), + "modified_at": entry.modified_at.isoformat(), }, } diff --git a/tests/components/whirlpool/test_diagnostics.py b/tests/components/whirlpool/test_diagnostics.py index 6cfc1b76e38..2a0b2e6fd18 100644 --- a/tests/components/whirlpool/test_diagnostics.py +++ b/tests/components/whirlpool/test_diagnostics.py @@ -29,4 +29,4 @@ async def test_entry_diagnostics( result = await get_diagnostics_for_config_entry(hass, hass_client, mock_entry) - assert result == snapshot(exclude=props("entry_id")) + assert result == snapshot(exclude=props("entry_id", "created_at", "modified_at")) diff --git a/tests/snapshots/test_config_entries.ambr b/tests/snapshots/test_config_entries.ambr index bfb583ba8db..136749dfb14 100644 --- a/tests/snapshots/test_config_entries.ambr +++ b/tests/snapshots/test_config_entries.ambr @@ -1,12 +1,14 @@ # serializer version: 1 # name: test_as_dict dict({ + 'created_at': '2024-02-14T12:00:00+00:00', 'data': dict({ }), 'disabled_by': None, 'domain': 'test', 'entry_id': 'mock-entry', 'minor_version': 1, + 'modified_at': '2024-02-14T12:00:00+00:00', 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/syrupy.py b/tests/syrupy.py index 09e18428015..0bdbcf99e2b 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -137,7 +137,8 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): @classmethod def _serializable_config_entry(cls, data: ConfigEntry) -> SerializableData: """Prepare a Home Assistant config entry for serialization.""" - return ConfigEntrySnapshot(data.as_dict() | {"entry_id": ANY}) + entry = ConfigEntrySnapshot(data.as_dict() | {"entry_id": ANY}) + return cls._remove_created_and_modified_at(entry) @classmethod def _serializable_device_registry_entry( diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 10cdaa8add9..2a5dff5c14a 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -10,6 +10,7 @@ import logging from typing import Any from unittest.mock import ANY, AsyncMock, Mock, patch +from freezegun import freeze_time from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion @@ -51,6 +52,7 @@ from .common import ( async_capture_events, async_fire_time_changed, async_get_persistent_notifications, + flush_store, mock_config_flow, mock_integration, mock_platform, @@ -912,6 +914,7 @@ async def test_saving_and_loading( assert orig.as_dict() == loaded.as_dict() +@freeze_time("2024-02-14 12:00:00") async def test_as_dict(snapshot: SnapshotAssertion) -> None: """Test ConfigEntry.as_dict.""" @@ -1251,8 +1254,11 @@ async def test_loading_default_config(hass: HomeAssistant) -> None: assert len(manager.async_entries()) == 0 -async def test_updating_entry_data(manager: config_entries.ConfigEntries) -> None: +async def test_updating_entry_data( + manager: config_entries.ConfigEntries, freezer: FrozenDateTimeFactory +) -> None: """Test that we can update an entry data.""" + created = dt_util.utcnow() entry = MockConfigEntry( domain="test", data={"first": True}, @@ -1260,17 +1266,32 @@ async def test_updating_entry_data(manager: config_entries.ConfigEntries) -> Non ) entry.add_to_manager(manager) + assert len(manager.async_entries()) == 1 + assert manager.async_entries()[0] == entry + assert entry.created_at == created + assert entry.modified_at == created + + freezer.tick() + assert manager.async_update_entry(entry) is False assert entry.data == {"first": True} + assert entry.modified_at == created + assert manager.async_entries()[0].modified_at == created + + freezer.tick() + modified = dt_util.utcnow() assert manager.async_update_entry(entry, data={"second": True}) is True assert entry.data == {"second": True} + assert entry.modified_at == modified + assert manager.async_entries()[0].modified_at == modified async def test_updating_entry_system_options( - manager: config_entries.ConfigEntries, + manager: config_entries.ConfigEntries, freezer: FrozenDateTimeFactory ) -> None: """Test that we can update an entry data.""" + created = dt_util.utcnow() entry = MockConfigEntry( domain="test", data={"first": True}, @@ -1281,6 +1302,11 @@ async def test_updating_entry_system_options( assert entry.pref_disable_new_entities is True assert entry.pref_disable_polling is False + assert entry.created_at == created + assert entry.modified_at == created + + freezer.tick() + modified = dt_util.utcnow() manager.async_update_entry( entry, pref_disable_new_entities=False, pref_disable_polling=True @@ -1288,6 +1314,8 @@ async def test_updating_entry_system_options( assert entry.pref_disable_new_entities is False assert entry.pref_disable_polling is True + assert entry.created_at == created + assert entry.modified_at == modified async def test_update_entry_options_and_trigger_listener( @@ -5908,3 +5936,67 @@ async def test_config_entry_late_platform_setup( "entry_id test2 cannot forward setup for light because it is " "not loaded in the ConfigEntryState.NOT_LOADED state" ) not in caplog.text + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_migration_from_1_2( + hass: HomeAssistant, hass_storage: dict[str, Any] +) -> None: + """Test migration from version 1.2.""" + hass_storage[config_entries.STORAGE_KEY] = { + "version": 1, + "minor_version": 2, + "data": { + "entries": [ + { + "data": {}, + "disabled_by": None, + "domain": "sun", + "entry_id": "0a8bd02d0d58c7debf5daf7941c9afe2", + "minor_version": 1, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "import", + "title": "Sun", + "unique_id": None, + "version": 1, + }, + ] + }, + } + + manager = config_entries.ConfigEntries(hass, {}) + await manager.async_initialize() + + # Test data was loaded + entries = manager.async_entries() + assert len(entries) == 1 + + # Check we store migrated data + await flush_store(manager._store) + assert hass_storage[config_entries.STORAGE_KEY] == { + "version": config_entries.STORAGE_VERSION, + "minor_version": config_entries.STORAGE_VERSION_MINOR, + "key": config_entries.STORAGE_KEY, + "data": { + "entries": [ + { + "created_at": "1970-01-01T00:00:00+00:00", + "data": {}, + "disabled_by": None, + "domain": "sun", + "entry_id": "0a8bd02d0d58c7debf5daf7941c9afe2", + "minor_version": 1, + "modified_at": "1970-01-01T00:00:00+00:00", + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "import", + "title": "Sun", + "unique_id": None, + "version": 1, + }, + ] + }, + } From 4ac85829c80cdccac5978af20ca957bb87058793 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:09:40 +0200 Subject: [PATCH 1695/2411] Fix implicit-return in season tests (#122784) --- tests/components/season/test_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/season/test_sensor.py b/tests/components/season/test_sensor.py index ffc8e9f1a07..881192c95f0 100644 --- a/tests/components/season/test_sensor.py +++ b/tests/components/season/test_sensor.py @@ -70,6 +70,7 @@ def idfn(val): """Provide IDs for pytest parametrize.""" if isinstance(val, (datetime)): return val.strftime("%Y%m%d") + return None @pytest.mark.parametrize(("type", "day", "expected"), NORTHERN_PARAMETERS, ids=idfn) From 02581bbf028355677c75de94b4d8d1101ca9b2da Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:10:44 +0200 Subject: [PATCH 1696/2411] Enforce HOMEASSISTANT_DOMAIN alias for core DOMAIN (#122763) --- .../components/homeassistant/repairs.py | 4 +- pyproject.toml | 1 + .../generic_hygrostat/test_humidifier.py | 44 +++++++++---------- .../generic_thermostat/test_climate.py | 40 ++++++++--------- 4 files changed, 46 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/homeassistant/repairs.py b/homeassistant/components/homeassistant/repairs.py index af8f8f05a35..9dd732e3ce8 100644 --- a/homeassistant/components/homeassistant/repairs.py +++ b/homeassistant/components/homeassistant/repairs.py @@ -3,10 +3,12 @@ from __future__ import annotations from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow -from homeassistant.core import DOMAIN, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import issue_registry as ir +from .const import DOMAIN + class IntegrationNotFoundFlow(RepairsFlow): """Handler for an issue fixing flow.""" diff --git a/pyproject.toml b/pyproject.toml index 940a11753a6..70bfa1f18d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -872,6 +872,7 @@ voluptuous = "vol" "homeassistant.components.wake_word.PLATFORM_SCHEMA" = "WAKE_WORD_PLATFORM_SCHEMA" "homeassistant.components.water_heater.PLATFORM_SCHEMA" = "WATER_HEATER_PLATFORM_SCHEMA" "homeassistant.components.weather.PLATFORM_SCHEMA" = "WEATHER_PLATFORM_SCHEMA" +"homeassistant.core.DOMAIN" = "HOMEASSISTANT_DOMAIN" "homeassistant.helpers.area_registry" = "ar" "homeassistant.helpers.category_registry" = "cr" "homeassistant.helpers.config_validation" = "cv" diff --git a/tests/components/generic_hygrostat/test_humidifier.py b/tests/components/generic_hygrostat/test_humidifier.py index a97d5a7c1a6..fc46db48664 100644 --- a/tests/components/generic_hygrostat/test_humidifier.py +++ b/tests/components/generic_hygrostat/test_humidifier.py @@ -29,7 +29,7 @@ from homeassistant.const import ( ) import homeassistant.core as ha from homeassistant.core import ( - DOMAIN as HASS_DOMAIN, + DOMAIN as HOMEASSISTANT_DOMAIN, CoreState, HomeAssistant, State, @@ -527,7 +527,7 @@ async def test_set_target_humidity_humidifier_on(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -547,7 +547,7 @@ async def test_set_target_humidity_humidifier_off(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -587,7 +587,7 @@ async def test_humidity_change_humidifier_on_outside_tolerance( await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -627,7 +627,7 @@ async def test_humidity_change_humidifier_off_outside_tolerance( await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -664,7 +664,7 @@ async def test_operation_mode_humidify(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -725,7 +725,7 @@ async def test_set_target_humidity_dry_off(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH assert hass.states.get(ENTITY).attributes.get("action") == "drying" @@ -784,7 +784,7 @@ async def test_operation_mode_dry(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -797,7 +797,7 @@ async def test_set_target_humidity_dry_on(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -810,7 +810,7 @@ async def test_init_ignores_tolerance(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -835,7 +835,7 @@ async def test_set_humidity_change_dry_off_outside_tolerance( await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -858,7 +858,7 @@ async def test_humidity_change_dry_on_outside_tolerance(hass: HomeAssistant) -> await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -878,7 +878,7 @@ async def test_running_when_operating_mode_is_off_2(hass: HomeAssistant) -> None await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH assert hass.states.get(ENTITY).attributes.get("action") == "off" @@ -956,7 +956,7 @@ async def test_humidity_change_dry_trigger_on_long_enough(hass: HomeAssistant) - await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -990,7 +990,7 @@ async def test_humidity_change_dry_trigger_off_long_enough(hass: HomeAssistant) await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -1115,7 +1115,7 @@ async def test_humidity_change_humidifier_trigger_on_long_enough( await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -1136,7 +1136,7 @@ async def test_humidity_change_humidifier_trigger_off_long_enough( await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -1243,7 +1243,7 @@ async def test_humidity_change_dry_trigger_on_long_enough_3( await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -1264,7 +1264,7 @@ async def test_humidity_change_dry_trigger_off_long_enough_3( await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -1309,7 +1309,7 @@ async def test_humidity_change_humidifier_trigger_on_long_enough_2( await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -1330,7 +1330,7 @@ async def test_humidity_change_humidifier_trigger_off_long_enough_2( await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -1385,7 +1385,7 @@ async def test_float_tolerance_values_2(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index dcf1cd695e2..18e31b9591f 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -37,7 +37,7 @@ from homeassistant.const import ( ) import homeassistant.core as ha from homeassistant.core import ( - DOMAIN as HASS_DOMAIN, + DOMAIN as HOMEASSISTANT_DOMAIN, CoreState, HomeAssistant, State, @@ -484,7 +484,7 @@ async def test_set_target_temp_heater_on(hass: HomeAssistant) -> None: await common.async_set_temperature(hass, 30) assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -498,7 +498,7 @@ async def test_set_target_temp_heater_off(hass: HomeAssistant) -> None: await common.async_set_temperature(hass, 25) assert len(calls) == 2 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -522,7 +522,7 @@ async def test_temp_change_heater_on_outside_tolerance(hass: HomeAssistant) -> N await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -546,7 +546,7 @@ async def test_temp_change_heater_off_outside_tolerance(hass: HomeAssistant) -> await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -559,7 +559,7 @@ async def test_running_when_hvac_mode_is_off(hass: HomeAssistant) -> None: await common.async_set_hvac_mode(hass, HVACMode.OFF) assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -589,7 +589,7 @@ async def test_hvac_mode_heat(hass: HomeAssistant) -> None: await common.async_set_hvac_mode(hass, HVACMode.HEAT) assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -643,7 +643,7 @@ async def test_set_target_temp_ac_off(hass: HomeAssistant) -> None: await common.async_set_temperature(hass, 30) assert len(calls) == 2 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -674,7 +674,7 @@ async def test_hvac_mode_cool(hass: HomeAssistant) -> None: await common.async_set_hvac_mode(hass, HVACMode.COOL) assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -688,7 +688,7 @@ async def test_set_target_temp_ac_on(hass: HomeAssistant) -> None: await common.async_set_temperature(hass, 25) assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -712,7 +712,7 @@ async def test_set_temp_change_ac_off_outside_tolerance(hass: HomeAssistant) -> await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -736,7 +736,7 @@ async def test_temp_change_ac_on_outside_tolerance(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -749,7 +749,7 @@ async def test_running_when_operating_mode_is_off_2(hass: HomeAssistant) -> None await common.async_set_hvac_mode(hass, HVACMode.OFF) assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -867,7 +867,7 @@ async def test_heating_cooling_switch_toggles_when_outside_min_cycle_duration( # Then assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == expected_triggered_service_call assert call.data["entity_id"] == ENT_SWITCH @@ -965,7 +965,7 @@ async def test_temp_change_ac_trigger_on_long_enough_3(hass: HomeAssistant) -> N await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -989,7 +989,7 @@ async def test_temp_change_ac_trigger_off_long_enough_3(hass: HomeAssistant) -> await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -1038,7 +1038,7 @@ async def test_temp_change_heater_trigger_on_long_enough_2(hass: HomeAssistant) await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_ON assert call.data["entity_id"] == ENT_SWITCH @@ -1064,7 +1064,7 @@ async def test_temp_change_heater_trigger_off_long_enough_2( await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -1237,7 +1237,7 @@ async def test_initial_hvac_off_force_heater_off(hass: HomeAssistant) -> None: # heater must be switched off assert len(calls) == 1 call = calls[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH @@ -1345,7 +1345,7 @@ async def test_restore_will_turn_off_when_loaded_second(hass: HomeAssistant) -> assert len(calls_on) == 0 assert len(calls_off) == 1 call = calls_off[0] - assert call.domain == HASS_DOMAIN + assert call.domain == HOMEASSISTANT_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == "input_boolean.test" From b5b01d97f17061177afb18d2803080afbc29831a Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Mon, 29 Jul 2024 22:12:34 +0200 Subject: [PATCH 1697/2411] Add support for ASIN Pool devices to ASEKO (#122773) --- homeassistant/components/aseko_pool_live/entity.py | 5 ++++- homeassistant/components/aseko_pool_live/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/aseko_pool_live/entity.py b/homeassistant/components/aseko_pool_live/entity.py index cd96b8f59a7..6f0979da2e7 100644 --- a/homeassistant/components/aseko_pool_live/entity.py +++ b/homeassistant/components/aseko_pool_live/entity.py @@ -19,7 +19,10 @@ class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]): super().__init__(coordinator) self._unit = unit - self._device_model = f"ASIN AQUA {self._unit.type}" + if self._unit.type == "Remote": + self._device_model = "ASIN Pool" + else: + self._device_model = f"ASIN AQUA {self._unit.type}" self._device_name = self._unit.name if self._unit.name else self._device_model self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/aseko_pool_live/manifest.json b/homeassistant/components/aseko_pool_live/manifest.json index f7c29277977..a340408ad71 100644 --- a/homeassistant/components/aseko_pool_live/manifest.json +++ b/homeassistant/components/aseko_pool_live/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aseko_pool_live", "iot_class": "cloud_polling", "loggers": ["aioaseko"], - "requirements": ["aioaseko==0.1.1"] + "requirements": ["aioaseko==0.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 59f1ced067f..21b677b06b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -192,7 +192,7 @@ aioapcaccess==0.4.2 aioaquacell==0.2.0 # homeassistant.components.aseko_pool_live -aioaseko==0.1.1 +aioaseko==0.2.0 # homeassistant.components.asuswrt aioasuswrt==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5021d7f07fc..9dbfb034389 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -180,7 +180,7 @@ aioapcaccess==0.4.2 aioaquacell==0.2.0 # homeassistant.components.aseko_pool_live -aioaseko==0.1.1 +aioaseko==0.2.0 # homeassistant.components.asuswrt aioasuswrt==1.4.0 From 3e1aee4cbc2a941beb48a4610b71c0789f42e9e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 15:13:39 -0500 Subject: [PATCH 1698/2411] Remove unused constant in august (#122804) --- homeassistant/components/august/const.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index fcb64231e93..661b291edb1 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -16,8 +16,6 @@ NOTIFICATION_TITLE = "August" MANUFACTURER = "August Home Inc." -DEFAULT_AUGUST_CONFIG_FILE = ".august.conf" - DEFAULT_NAME = "August" DOMAIN = "august" From 2102a104d24c1951d287d885a882324dc62e3fb0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:14:05 +0200 Subject: [PATCH 1699/2411] Adjust DOMAIN imports in homeassistant integration (#122774) --- .../components/homeassistant/__init__.py | 76 ++++++++++--------- .../components/homeassistant/logbook.py | 2 +- .../components/homeassistant/scene.py | 14 ++-- 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index cc948fcc663..f771923ab2d 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -23,7 +23,13 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -import homeassistant.core as ha +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + callback, + split_entity_id, +) from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser from homeassistant.helpers import config_validation as cv, recorder, restore_state from homeassistant.helpers.entity_component import async_update_entity @@ -76,14 +82,14 @@ SCHEMA_RESTART = vol.Schema({vol.Optional(ATTR_SAFE_MODE, default=False): bool}) SHUTDOWN_SERVICES = (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART) -async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # noqa: C901 +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901 """Set up general services related to Home Assistant.""" - async def async_save_persistent_states(service: ha.ServiceCall) -> None: + async def async_save_persistent_states(service: ServiceCall) -> None: """Handle calls to homeassistant.save_persistent_states.""" await restore_state.RestoreStateData.async_save_persistent_states(hass) - async def async_handle_turn_service(service: ha.ServiceCall) -> None: + async def async_handle_turn_service(service: ServiceCall) -> None: """Handle calls to homeassistant.turn_on/off.""" referenced = async_extract_referenced_entity_ids(hass, service) all_referenced = referenced.referenced | referenced.indirectly_referenced @@ -98,10 +104,10 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no # Group entity_ids by domain. groupby requires sorted data. by_domain = it.groupby( - sorted(all_referenced), lambda item: ha.split_entity_id(item)[0] + sorted(all_referenced), lambda item: split_entity_id(item)[0] ) - tasks: list[Coroutine[Any, Any, ha.ServiceResponse]] = [] + tasks: list[Coroutine[Any, Any, ServiceResponse]] = [] unsupported_entities: set[str] = set() for domain, ent_ids in by_domain: @@ -145,24 +151,24 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no await asyncio.gather(*tasks) hass.services.async_register( - ha.DOMAIN, SERVICE_SAVE_PERSISTENT_STATES, async_save_persistent_states + DOMAIN, SERVICE_SAVE_PERSISTENT_STATES, async_save_persistent_states ) service_schema = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}, extra=vol.ALLOW_EXTRA) hass.services.async_register( - ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service, schema=service_schema + DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service, schema=service_schema ) hass.services.async_register( - ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service, schema=service_schema + DOMAIN, SERVICE_TURN_ON, async_handle_turn_service, schema=service_schema ) hass.services.async_register( - ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service, schema=service_schema + DOMAIN, SERVICE_TOGGLE, async_handle_turn_service, schema=service_schema ) - async def async_handle_core_service(call: ha.ServiceCall) -> None: + async def async_handle_core_service(call: ServiceCall) -> None: """Service handler for handling core services.""" - stop_handler: Callable[[ha.HomeAssistant, bool], Coroutine[Any, Any, None]] + stop_handler: Callable[[HomeAssistant, bool], Coroutine[Any, Any, None]] if call.service in SHUTDOWN_SERVICES and recorder.async_migration_in_progress( hass @@ -193,7 +199,7 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no hass, "Config error. See [the logs](/config/logs) for details.", "Config validating", - f"{ha.DOMAIN}.check_config", + f"{DOMAIN}.check_config", ) raise HomeAssistantError( f"The system cannot {call.service} " @@ -206,7 +212,7 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no stop_handler = hass.data[DATA_STOP_HANDLER] await stop_handler(hass, True) - async def async_handle_update_service(call: ha.ServiceCall) -> None: + async def async_handle_update_service(call: ServiceCall) -> None: """Service handler for updating an entity.""" if call.context.user_id: user = await hass.auth.async_get_user(call.context.user_id) @@ -235,26 +241,26 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no await asyncio.gather(*tasks) async_register_admin_service( - hass, ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service + hass, DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service ) async_register_admin_service( hass, - ha.DOMAIN, + DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service, SCHEMA_RESTART, ) async_register_admin_service( - hass, ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service + hass, DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service ) hass.services.async_register( - ha.DOMAIN, + DOMAIN, SERVICE_UPDATE_ENTITY, async_handle_update_service, schema=SCHEMA_UPDATE_ENTITY, ) - async def async_handle_reload_config(call: ha.ServiceCall) -> None: + async def async_handle_reload_config(call: ServiceCall) -> None: """Service handler for reloading core config.""" try: conf = await conf_util.async_hass_config_yaml(hass) @@ -263,13 +269,13 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no return # auth only processed during startup - await conf_util.async_process_ha_core_config(hass, conf.get(ha.DOMAIN) or {}) + await conf_util.async_process_ha_core_config(hass, conf.get(DOMAIN) or {}) async_register_admin_service( - hass, ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config + hass, DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config ) - async def async_set_location(call: ha.ServiceCall) -> None: + async def async_set_location(call: ServiceCall) -> None: """Service handler to set location.""" service_data = { "latitude": call.data[ATTR_LATITUDE], @@ -283,7 +289,7 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no async_register_admin_service( hass, - ha.DOMAIN, + DOMAIN, SERVICE_SET_LOCATION, async_set_location, vol.Schema( @@ -295,15 +301,15 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no ), ) - async def async_handle_reload_templates(call: ha.ServiceCall) -> None: + async def async_handle_reload_templates(call: ServiceCall) -> None: """Service handler to reload custom Jinja.""" await async_load_custom_templates(hass) async_register_admin_service( - hass, ha.DOMAIN, SERVICE_RELOAD_CUSTOM_TEMPLATES, async_handle_reload_templates + hass, DOMAIN, SERVICE_RELOAD_CUSTOM_TEMPLATES, async_handle_reload_templates ) - async def async_handle_reload_config_entry(call: ha.ServiceCall) -> None: + async def async_handle_reload_config_entry(call: ServiceCall) -> None: """Service handler for reloading a config entry.""" reload_entries: set[str] = set() if ATTR_ENTRY_ID in call.data: @@ -320,13 +326,13 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no async_register_admin_service( hass, - ha.DOMAIN, + DOMAIN, SERVICE_RELOAD_CONFIG_ENTRY, async_handle_reload_config_entry, schema=SCHEMA_RELOAD_CONFIG_ENTRY, ) - async def async_handle_reload_all(call: ha.ServiceCall) -> None: + async def async_handle_reload_all(call: ServiceCall) -> None: """Service handler for calling all integration reload services. Calls all reload services on all active domains, which triggers the @@ -363,16 +369,16 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no domain, service, context=call.context, blocking=True ) for domain, service in ( - (ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG), + (DOMAIN, SERVICE_RELOAD_CORE_CONFIG), ("frontend", "reload_themes"), - (ha.DOMAIN, SERVICE_RELOAD_CUSTOM_TEMPLATES), + (DOMAIN, SERVICE_RELOAD_CUSTOM_TEMPLATES), ) ] await asyncio.gather(*tasks) async_register_admin_service( - hass, ha.DOMAIN, SERVICE_RELOAD_ALL, async_handle_reload_all + hass, DOMAIN, SERVICE_RELOAD_ALL, async_handle_reload_all ) exposed_entities = ExposedEntities(hass) @@ -383,17 +389,17 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no return True -async def _async_stop(hass: ha.HomeAssistant, restart: bool) -> None: +async def _async_stop(hass: HomeAssistant, restart: bool) -> None: """Stop home assistant.""" exit_code = RESTART_EXIT_CODE if restart else 0 # Track trask in hass.data. No need to cleanup, we're stopping. hass.data[KEY_HA_STOP] = asyncio.create_task(hass.async_stop(exit_code)) -@ha.callback +@callback def async_set_stop_handler( - hass: ha.HomeAssistant, - stop_handler: Callable[[ha.HomeAssistant, bool], Coroutine[Any, Any, None]], + hass: HomeAssistant, + stop_handler: Callable[[HomeAssistant, bool], Coroutine[Any, Any, None]], ) -> None: """Set function which is called by the stop and restart services.""" hass.data[DATA_STOP_HANDLER] = stop_handler diff --git a/homeassistant/components/homeassistant/logbook.py b/homeassistant/components/homeassistant/logbook.py index 92a91dbd5cb..2e7c17485e1 100644 --- a/homeassistant/components/homeassistant/logbook.py +++ b/homeassistant/components/homeassistant/logbook.py @@ -15,7 +15,7 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.typing import NoEventData from homeassistant.util.event_type import EventType -from . import DOMAIN +from .const import DOMAIN EVENT_TO_NAME: dict[EventType[Any] | str, str] = { EVENT_HOMEASSISTANT_STOP: "stopped", diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 0d12c1537ff..aec9b9cd06b 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -23,13 +23,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import ( - DOMAIN as HOMEASSISTANT_DOMAIN, - HomeAssistant, - ServiceCall, - State, - callback, -) +from homeassistant.core import HomeAssistant, ServiceCall, State, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback, EntityPlatform @@ -41,6 +35,8 @@ from homeassistant.helpers.state import async_reproduce_state from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.loader import async_get_integration +from .const import DOMAIN + def _convert_states(states: dict[str, Any]) -> dict[str, State]: """Convert state definitions to State objects.""" @@ -92,7 +88,7 @@ STATES_SCHEMA = vol.All(dict, _convert_states) PLATFORM_SCHEMA = vol.Schema( { - vol.Required(CONF_PLATFORM): HOMEASSISTANT_DOMAIN, + vol.Required(CONF_PLATFORM): DOMAIN, vol.Required(STATES): vol.All( cv.ensure_list, [ @@ -206,7 +202,7 @@ async def async_setup_platform( # Extract only the config for the Home Assistant platform, ignore the rest. for p_type, p_config in conf_util.config_per_platform(conf, SCENE_DOMAIN): - if p_type != HOMEASSISTANT_DOMAIN: + if p_type != DOMAIN: continue _process_scenes_config(hass, async_add_entities, p_config) From 7b08e625b42e06560f69b77023043f140315b142 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:14:41 +0200 Subject: [PATCH 1700/2411] Fix implicit-return in websocket_api tests (#122779) --- tests/components/websocket_api/test_connection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index 5740bb48019..343575e5b4a 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -77,13 +77,15 @@ async def test_exception_handling( refresh_token = Mock() hass.data[DOMAIN] = {} - def get_extra_info(key: str) -> Any: + def get_extra_info(key: str) -> Any | None: if key == "sslcontext": return True if key == "peername": return ("127.0.0.42", 8123) + return None + mocked_transport = Mock() mocked_transport.get_extra_info = get_extra_info mocked_request = make_mocked_request( From fdab23c3f95791cb39939a473216f455a0ef57ca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:16:00 +0200 Subject: [PATCH 1701/2411] Fix implicit-return in test schema extractions (#122787) --- tests/components/cast/test_config_flow.py | 1 + tests/components/mqtt/test_config_flow.py | 6 ++++-- tests/components/obihai/__init__.py | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index 2c0c36d6632..7dce3f768e2 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -148,6 +148,7 @@ def get_suggested(schema, key): if k.description is None or "suggested_value" not in k.description: return None return k.description["suggested_value"] + return None @pytest.mark.parametrize( diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 38dfdefcf97..2b4cb20ccf9 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -850,16 +850,17 @@ async def test_invalid_discovery_prefix( assert mock_reload_after_entry_update.call_count == 0 -def get_default(schema: vol.Schema, key: str) -> Any: +def get_default(schema: vol.Schema, key: str) -> Any | None: """Get default value for key in voluptuous schema.""" for schema_key in schema: if schema_key == key: if schema_key.default == vol.UNDEFINED: return None return schema_key.default() + return None -def get_suggested(schema: vol.Schema, key: str) -> Any: +def get_suggested(schema: vol.Schema, key: str) -> Any | None: """Get suggested value for key in voluptuous schema.""" for schema_key in schema: if schema_key == key: @@ -869,6 +870,7 @@ def get_suggested(schema: vol.Schema, key: str) -> Any: ): return None return schema_key.description["suggested_value"] + return None @pytest.mark.usefixtures("mock_reload_after_entry_update") diff --git a/tests/components/obihai/__init__.py b/tests/components/obihai/__init__.py index d43aa6a9bb8..b88f0a5c874 100644 --- a/tests/components/obihai/__init__.py +++ b/tests/components/obihai/__init__.py @@ -32,3 +32,4 @@ def get_schema_suggestion(schema, key): if k.description is None or "suggested_value" not in k.description: return None return k.description["suggested_value"] + return None From 1c03c83c0a8c2c9cee4153873ef4af56c1c716e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 15:38:58 -0500 Subject: [PATCH 1702/2411] Fix blocking stat() via is_file in image_upload (#122808) --- .../components/image_upload/__init__.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/image_upload/__init__.py b/homeassistant/components/image_upload/__init__.py index 8bb3aca3708..5e9cf8c4e0e 100644 --- a/homeassistant/components/image_upload/__init__.py +++ b/homeassistant/components/image_upload/__init__.py @@ -220,18 +220,18 @@ class ImageServeView(HomeAssistantView): hass = request.app[KEY_HASS] target_file = self.image_folder / image_id / f"{width}x{height}" - if not target_file.is_file(): + if not await hass.async_add_executor_job(target_file.is_file): async with self.transform_lock: # Another check in case another request already # finished it while waiting - if not target_file.is_file(): - await hass.async_add_executor_job( - _generate_thumbnail, - self.image_folder / image_id / "original", - image_info["content_type"], - target_file, - (width, height), - ) + await hass.async_add_executor_job( + _generate_thumbnail_if_file_does_not_exist, + target_file, + self.image_folder / image_id / "original", + image_info["content_type"], + target_file, + (width, height), + ) return web.FileResponse( target_file, @@ -239,16 +239,18 @@ class ImageServeView(HomeAssistantView): ) -def _generate_thumbnail( +def _generate_thumbnail_if_file_does_not_exist( + target_file: pathlib.Path, original_path: pathlib.Path, content_type: str, target_path: pathlib.Path, target_size: tuple[int, int], ) -> None: """Generate a size.""" - image = ImageOps.exif_transpose(Image.open(original_path)) - image.thumbnail(target_size) - image.save(target_path, format=content_type.partition("/")[-1]) + if not target_file.is_file(): + image = ImageOps.exif_transpose(Image.open(original_path)) + image.thumbnail(target_size) + image.save(target_path, format=content_type.partition("/")[-1]) def _validate_size_from_filename(filename: str) -> tuple[int, int]: From 9450744b3be6572a7d5fb3cee78efd9434a08018 Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Mon, 29 Jul 2024 23:11:51 +0200 Subject: [PATCH 1703/2411] Add device _info to bluesound integration (#122795) * Add device_info * Use _attr_unique_id instead of custom methode * Use different DeviceInfo if port is not DEFAULT_PORT * Remove name method; Add has_entity_name=True * Remove self._name completely * move _attr_has_entity_name and _attr_name out of __init__ * log error if status update fails * use error for remaining info logs --- .../components/bluesound/media_player.py | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index e40a20f888a..b320566c74a 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -44,6 +44,11 @@ from homeassistant.core import ( from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv, issue_registry as ir +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + DeviceInfo, + format_mac, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -280,6 +285,8 @@ class BluesoundPlayer(MediaPlayerEntity): """Representation of a Bluesound Player.""" _attr_media_content_type = MediaType.MUSIC + _attr_has_entity_name = True + _attr_name = None def __init__( self, @@ -295,7 +302,6 @@ class BluesoundPlayer(MediaPlayerEntity): self._hass = hass self.port = port self._polling_task = None # The actual polling task. - self._name = sync_status.name self._id = None self._last_status_update = None self._sync_status: SyncStatus | None = None @@ -314,6 +320,27 @@ class BluesoundPlayer(MediaPlayerEntity): self._init_callback = init_callback + self._attr_unique_id = format_unique_id(sync_status.mac, port) + # there should always be one player with the default port per mac + if port is DEFAULT_PORT: + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, format_mac(sync_status.mac))}, + connections={(CONNECTION_NETWORK_MAC, format_mac(sync_status.mac))}, + name=sync_status.name, + manufacturer=sync_status.brand, + model=sync_status.model_name, + model_id=sync_status.model, + ) + else: + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, format_unique_id(sync_status.mac, port))}, + name=sync_status.name, + manufacturer=sync_status.brand, + model=sync_status.model_name, + model_id=sync_status.model, + via_device=(DOMAIN, format_mac(sync_status.mac)), + ) + @staticmethod def _try_get_index(string, search_string): """Get the index.""" @@ -328,12 +355,10 @@ class BluesoundPlayer(MediaPlayerEntity): self._sync_status = sync_status - if not self._name: - self._name = sync_status.name if sync_status.name else self.host if not self._id: self._id = sync_status.id if not self._bluesound_device_name: - self._bluesound_device_name = self._name + self._bluesound_device_name = sync_status.name if sync_status.master is not None: self._is_master = False @@ -366,7 +391,7 @@ class BluesoundPlayer(MediaPlayerEntity): await self.async_update_status() except (TimeoutError, ClientError): - _LOGGER.info("Node %s:%s is offline, retrying later", self.name, self.port) + _LOGGER.error("Node %s:%s is offline, retrying later", self.name, self.port) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) self.start_polling() @@ -393,7 +418,7 @@ class BluesoundPlayer(MediaPlayerEntity): await self.force_update_sync_status(self._init_callback) except (TimeoutError, ClientError): - _LOGGER.info("Node %s:%s is offline, retrying later", self.host, self.port) + _LOGGER.error("Node %s:%s is offline, retrying later", self.host, self.port) self._retry_remove = async_track_time_interval( self._hass, self.async_init, NODE_RETRY_INITIATION ) @@ -455,15 +480,12 @@ class BluesoundPlayer(MediaPlayerEntity): self._last_status_update = None self._status = None self.async_write_ha_state() - _LOGGER.info("Client connection error, marking %s as offline", self._name) + _LOGGER.error( + "Client connection error, marking %s as offline", + self._bluesound_device_name, + ) raise - @property - def unique_id(self) -> str: - """Return an unique ID.""" - assert self._sync_status is not None - return format_unique_id(self._sync_status.mac, self.port) - async def async_trigger_sync_on_all(self): """Trigger sync status update on all devices.""" _LOGGER.debug("Trigger sync status on all devices") @@ -618,11 +640,6 @@ class BluesoundPlayer(MediaPlayerEntity): """Get id of device.""" return self._id - @property - def name(self) -> str | None: - """Return the name of the device.""" - return self._name - @property def bluesound_device_name(self) -> str | None: """Return the device name as returned by the device.""" From bd3f0da3851a947c4c8b85195418525285e1d27e Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 29 Jul 2024 18:16:54 -0400 Subject: [PATCH 1704/2411] Bump ZHA lib to 0.0.24 and universal-silabs-flasher to 0.0.22 (#122812) * Bump ZHA lib to 0.0.24 * update for node state change for coordinator data * bump flasher --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/zha/conftest.py | 2 ++ 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 6067fa897f5..d2d328cc84b 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.21", "zha==0.0.23"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.24"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 21b677b06b3..70565d41175 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2822,7 +2822,7 @@ unifi_ap==0.0.1 unifiled==0.11 # homeassistant.components.zha -universal-silabs-flasher==0.0.21 +universal-silabs-flasher==0.0.22 # homeassistant.components.upb upb-lib==0.5.8 @@ -2983,7 +2983,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.23 +zha==0.0.24 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9dbfb034389..fe01d45de76 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2217,7 +2217,7 @@ ultraheat-api==0.5.7 unifi-discovery==1.2.0 # homeassistant.components.zha -universal-silabs-flasher==0.0.21 +universal-silabs-flasher==0.0.22 # homeassistant.components.upb upb-lib==0.5.8 @@ -2357,7 +2357,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.23 +zha==0.0.24 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 9b1ec7b33bf..a9f4c51d75d 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -154,6 +154,8 @@ async def zigpy_app_controller(): app.state.node_info.nwk = 0x0000 app.state.node_info.ieee = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32") + app.state.node_info.manufacturer = "Coordinator Manufacturer" + app.state.node_info.model = "Coordinator Model" app.state.network_info.pan_id = 0x1234 app.state.network_info.extended_pan_id = app.state.node_info.ieee app.state.network_info.channel = 15 From 36c01042c1e67132f0398a45f584645dc47fd172 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 29 Jul 2024 20:08:21 -0400 Subject: [PATCH 1705/2411] Enhance ZHA device removal (#122815) --- homeassistant/components/zha/websocket_api.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/zha/websocket_api.py b/homeassistant/components/zha/websocket_api.py index 97c625a27ed..0d4296e4b22 100644 --- a/homeassistant/components/zha/websocket_api.py +++ b/homeassistant/components/zha/websocket_api.py @@ -1311,12 +1311,8 @@ def async_load_api(hass: HomeAssistant) -> None: """Remove a node from the network.""" zha_gateway = get_zha_gateway(hass) ieee: EUI64 = service.data[ATTR_IEEE] - zha_device: Device | None = zha_gateway.get_device(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) - await application_controller.remove(ieee) + await zha_gateway.async_remove_device(ieee) async_register_admin_service( hass, DOMAIN, SERVICE_REMOVE, remove, schema=SERVICE_SCHEMAS[IEEE_SERVICE] From 004eccec893b4ceae0b6a50e97801dff34edb25c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 29 Jul 2024 20:10:02 -0400 Subject: [PATCH 1706/2411] Fix supported_features for ZHA fans (#122813) --- homeassistant/components/zha/fan.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index e5c100f1dc6..767c0d4cfb7 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -5,6 +5,8 @@ from __future__ import annotations import functools from typing import Any +from zha.application.platforms.fan.const import FanEntityFeature as ZHAFanEntityFeature + from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -15,6 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ZHAEntity from .helpers import ( SIGNAL_ADD_ENTITIES, + EntityData, async_add_entities as zha_async_add_entities, convert_zha_error_to_ha_error, get_zha_data, @@ -43,14 +46,30 @@ async def async_setup_entry( class ZhaFan(FanEntity, ZHAEntity): """Representation of a ZHA fan.""" - _attr_supported_features = ( - FanEntityFeature.SET_SPEED - | FanEntityFeature.TURN_OFF - | FanEntityFeature.TURN_ON - ) _attr_translation_key: str = "fan" _enable_turn_on_off_backwards_compatibility = False + def __init__(self, entity_data: EntityData) -> None: + """Initialize the ZHA fan.""" + super().__init__(entity_data) + features = FanEntityFeature(0) + zha_features: ZHAFanEntityFeature = self.entity_data.entity.supported_features + + if ZHAFanEntityFeature.DIRECTION in zha_features: + features |= FanEntityFeature.DIRECTION + if ZHAFanEntityFeature.OSCILLATE in zha_features: + features |= FanEntityFeature.OSCILLATE + if ZHAFanEntityFeature.PRESET_MODE in zha_features: + features |= FanEntityFeature.PRESET_MODE + if ZHAFanEntityFeature.SET_SPEED in zha_features: + features |= FanEntityFeature.SET_SPEED + if ZHAFanEntityFeature.TURN_ON in zha_features: + features |= FanEntityFeature.TURN_ON + if ZHAFanEntityFeature.TURN_OFF in zha_features: + features |= FanEntityFeature.TURN_OFF + + self._attr_supported_features = features + @property def preset_mode(self) -> str | None: """Return the current preset mode.""" From 70e368a57e5c3a09dd2b9546c1adac362663e19c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 30 Jul 2024 07:14:56 +0200 Subject: [PATCH 1707/2411] Use snapshot in Axis switch tests (#122680) --- .../axis/snapshots/test_switch.ambr | 189 ++++++++++++++++++ tests/components/axis/test_switch.py | 143 +++++++------ 2 files changed, 259 insertions(+), 73 deletions(-) create mode 100644 tests/components/axis/snapshots/test_switch.ambr diff --git a/tests/components/axis/snapshots/test_switch.ambr b/tests/components/axis/snapshots/test_switch.ambr new file mode 100644 index 00000000000..dc4c75371cf --- /dev/null +++ b/tests/components/axis/snapshots/test_switch.ambr @@ -0,0 +1,189 @@ +# serializer version: 1 +# name: test_switches_with_port_cgi[root.IOPort.I0.Configurable=yes\nroot.IOPort.I0.Direction=output\nroot.IOPort.I0.Output.Name=Doorbell\nroot.IOPort.I0.Output.Active=closed\nroot.IOPort.I1.Configurable=yes\nroot.IOPort.I1.Direction=output\nroot.IOPort.I1.Output.Name=\nroot.IOPort.I1.Output.Active=open\n][switch.home_doorbell-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.home_doorbell', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Doorbell', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tns1:Device/Trigger/Relay-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches_with_port_cgi[root.IOPort.I0.Configurable=yes\nroot.IOPort.I0.Direction=output\nroot.IOPort.I0.Output.Name=Doorbell\nroot.IOPort.I0.Output.Active=closed\nroot.IOPort.I1.Configurable=yes\nroot.IOPort.I1.Direction=output\nroot.IOPort.I1.Output.Name=\nroot.IOPort.I1.Output.Active=open\n][switch.home_doorbell-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'home Doorbell', + }), + 'context': , + 'entity_id': 'switch.home_doorbell', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches_with_port_cgi[root.IOPort.I0.Configurable=yes\nroot.IOPort.I0.Direction=output\nroot.IOPort.I0.Output.Name=Doorbell\nroot.IOPort.I0.Output.Active=closed\nroot.IOPort.I1.Configurable=yes\nroot.IOPort.I1.Direction=output\nroot.IOPort.I1.Output.Name=\nroot.IOPort.I1.Output.Active=open\n][switch.home_relay_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.home_relay_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tns1:Device/Trigger/Relay-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches_with_port_cgi[root.IOPort.I0.Configurable=yes\nroot.IOPort.I0.Direction=output\nroot.IOPort.I0.Output.Name=Doorbell\nroot.IOPort.I0.Output.Active=closed\nroot.IOPort.I1.Configurable=yes\nroot.IOPort.I1.Direction=output\nroot.IOPort.I1.Output.Name=\nroot.IOPort.I1.Output.Active=open\n][switch.home_relay_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'home Relay 1', + }), + 'context': , + 'entity_id': 'switch.home_relay_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches_with_port_management[port_management_payload0-api_discovery_items0][switch.home_doorbell-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.home_doorbell', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Doorbell', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tns1:Device/Trigger/Relay-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches_with_port_management[port_management_payload0-api_discovery_items0][switch.home_doorbell-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'home Doorbell', + }), + 'context': , + 'entity_id': 'switch.home_doorbell', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches_with_port_management[port_management_payload0-api_discovery_items0][switch.home_relay_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.home_relay_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay 1', + 'platform': 'axis', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:40:8c:12:34:56-tns1:Device/Trigger/Relay-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches_with_port_management[port_management_payload0-api_discovery_items0][switch.home_relay_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'home Relay 1', + }), + 'context': , + 'entity_id': 'switch.home_relay_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 8a93c844042..964cfdae64c 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -4,20 +4,24 @@ from unittest.mock import patch from axis.models.api import CONTEXT import pytest +from syrupy import SnapshotAssertion 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, + Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er -from .conftest import RtspEventMock +from .conftest import ConfigEntryFactoryType, RtspEventMock from .const import API_DISCOVERY_PORT_MANAGEMENT, NAME +from tests.common import snapshot_platform + PORT_DATA = """root.IOPort.I0.Configurable=yes root.IOPort.I0.Direction=output root.IOPort.I0.Output.Name=Doorbell @@ -28,61 +32,6 @@ root.IOPort.I1.Output.Name= root.IOPort.I1.Output.Active=open """ - -@pytest.mark.parametrize("param_ports_payload", [PORT_DATA]) -@pytest.mark.usefixtures("config_entry_setup") -async def test_switches_with_port_cgi( - hass: HomeAssistant, - mock_rtsp_event: RtspEventMock, -) -> None: - """Test that switches are loaded properly using port.cgi.""" - mock_rtsp_event( - topic="tns1:Device/Trigger/Relay", - data_type="LogicalState", - data_value="inactive", - source_name="RelayToken", - source_idx="0", - ) - mock_rtsp_event( - topic="tns1:Device/Trigger/Relay", - data_type="LogicalState", - data_value="active", - source_name="RelayToken", - source_idx="1", - ) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 - - relay_1 = hass.states.get(f"{SWITCH_DOMAIN}.{NAME}_relay_1") - assert relay_1.state == STATE_ON - assert relay_1.name == f"{NAME} Relay 1" - - entity_id = f"{SWITCH_DOMAIN}.{NAME}_doorbell" - - relay_0 = hass.states.get(entity_id) - assert relay_0.state == STATE_OFF - assert relay_0.name == f"{NAME} Doorbell" - - with patch("axis.interfaces.vapix.Ports.close") as mock_turn_on: - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - mock_turn_on.assert_called_once_with("0") - - with patch("axis.interfaces.vapix.Ports.open") as mock_turn_off: - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - mock_turn_off.assert_called_once_with("0") - - PORT_MANAGEMENT_RESPONSE = { "apiVersion": "1.0", "method": "getPorts", @@ -113,14 +62,18 @@ PORT_MANAGEMENT_RESPONSE = { } -@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_PORT_MANAGEMENT]) -@pytest.mark.parametrize("port_management_payload", [PORT_MANAGEMENT_RESPONSE]) -@pytest.mark.usefixtures("config_entry_setup") -async def test_switches_with_port_management( +@pytest.mark.parametrize("param_ports_payload", [PORT_DATA]) +async def test_switches_with_port_cgi( hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory: ConfigEntryFactoryType, mock_rtsp_event: RtspEventMock, + snapshot: SnapshotAssertion, ) -> None: - """Test that switches are loaded properly using port management.""" + """Test that switches are loaded properly using port.cgi.""" + with patch("homeassistant.components.axis.PLATFORMS", [Platform.SWITCH]): + config_entry = await config_entry_factory() + mock_rtsp_event( topic="tns1:Device/Trigger/Relay", data_type="LogicalState", @@ -137,30 +90,61 @@ async def test_switches_with_port_management( ) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 - - relay_1 = hass.states.get(f"{SWITCH_DOMAIN}.{NAME}_relay_1") - assert relay_1.state == STATE_ON - assert relay_1.name == f"{NAME} Relay 1" + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) entity_id = f"{SWITCH_DOMAIN}.{NAME}_doorbell" - relay_0 = hass.states.get(entity_id) - assert relay_0.state == STATE_OFF - assert relay_0.name == f"{NAME} Doorbell" + with patch("axis.interfaces.vapix.Ports.close") as mock_turn_on: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_turn_on.assert_called_once_with("0") - # State update + with patch("axis.interfaces.vapix.Ports.open") as mock_turn_off: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_turn_off.assert_called_once_with("0") + +@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_PORT_MANAGEMENT]) +@pytest.mark.parametrize("port_management_payload", [PORT_MANAGEMENT_RESPONSE]) +async def test_switches_with_port_management( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory: ConfigEntryFactoryType, + mock_rtsp_event: RtspEventMock, + snapshot: SnapshotAssertion, +) -> None: + """Test that switches are loaded properly using port management.""" + with patch("homeassistant.components.axis.PLATFORMS", [Platform.SWITCH]): + config_entry = await config_entry_factory() + + mock_rtsp_event( + topic="tns1:Device/Trigger/Relay", + data_type="LogicalState", + data_value="inactive", + source_name="RelayToken", + source_idx="0", + ) mock_rtsp_event( topic="tns1:Device/Trigger/Relay", data_type="LogicalState", data_value="active", source_name="RelayToken", - source_idx="0", + source_idx="1", ) await hass.async_block_till_done() - assert hass.states.get(f"{SWITCH_DOMAIN}.{NAME}_relay_1").state == STATE_ON + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + entity_id = f"{SWITCH_DOMAIN}.{NAME}_doorbell" with patch("axis.interfaces.vapix.IoPortManagement.close") as mock_turn_on: await hass.services.async_call( @@ -179,3 +163,16 @@ async def test_switches_with_port_management( blocking=True, ) mock_turn_off.assert_called_once_with("0") + + # State update + + mock_rtsp_event( + topic="tns1:Device/Trigger/Relay", + data_type="LogicalState", + data_value="active", + source_name="RelayToken", + source_idx="0", + ) + await hass.async_block_till_done() + + assert hass.states.get(f"{SWITCH_DOMAIN}.{NAME}_relay_1").state == STATE_ON From b4cba018709562892ef8f8b0bca74a2fb7cdb868 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:17:01 +0200 Subject: [PATCH 1708/2411] Fix implicit-return in command_line (#122838) --- homeassistant/components/command_line/cover.py | 5 ++--- homeassistant/components/command_line/switch.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 6400be7d92f..2c6ec78b689 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any, cast +from typing import Any, cast from homeassistant.components.cover import CoverEntity from homeassistant.const import ( @@ -145,8 +145,7 @@ class CommandCover(ManualTriggerEntity, CoverEntity): if self._command_state: LOGGER.info("Running state value command: %s", self._command_state) return await async_check_output_or_log(self._command_state, self._timeout) - if TYPE_CHECKING: - return None + return None async def _update_entity_state(self, now: datetime | None = None) -> None: """Update the state of the entity.""" diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 8a75276c8b4..f8e9d21cf23 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any, cast +from typing import Any, cast from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity from homeassistant.const import ( @@ -147,8 +147,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity): if self._value_template: return await self._async_query_state_value(self._command_state) return await self._async_query_state_code(self._command_state) - if TYPE_CHECKING: - return None + return None async def _update_entity_state(self, now: datetime | None = None) -> None: """Update the state of the entity.""" From fa53055485ca6d338b3abc554163e55ac969f8cb Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Tue, 30 Jul 2024 11:57:56 +0300 Subject: [PATCH 1709/2411] Bump voluptuous-openapi (#122828) --- 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 f0c72a91501..b0e17bc2826 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -57,7 +57,7 @@ SQLAlchemy==2.0.31 typing-extensions>=4.12.2,<5.0 ulid-transform==0.10.1 urllib3>=1.26.5,<2 -voluptuous-openapi==0.0.4 +voluptuous-openapi==0.0.5 voluptuous-serialize==2.6.0 voluptuous==0.15.2 webrtc-noise-gain==1.2.3 diff --git a/pyproject.toml b/pyproject.toml index 70bfa1f18d8..eac18012ae3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ dependencies = [ "urllib3>=1.26.5,<2", "voluptuous==0.15.2", "voluptuous-serialize==2.6.0", - "voluptuous-openapi==0.0.4", + "voluptuous-openapi==0.0.5", "yarl==1.9.4", ] diff --git a/requirements.txt b/requirements.txt index 6e5ef50c187..5122cb99c41 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,5 +40,5 @@ ulid-transform==0.10.1 urllib3>=1.26.5,<2 voluptuous==0.15.2 voluptuous-serialize==2.6.0 -voluptuous-openapi==0.0.4 +voluptuous-openapi==0.0.5 yarl==1.9.4 From d825ac346ece81888d0231a51a028956e6917fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o=20Gon=C3=A7alves?= <5808343+bgoncal@users.noreply.github.com> Date: Tue, 30 Jul 2024 11:00:08 +0200 Subject: [PATCH 1710/2411] Add 'use_custom_colors' to iOS Action configuration (#122767) --- homeassistant/components/ios/__init__.py | 2 ++ homeassistant/components/ios/const.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index 4b2b92a482d..2a821166d8a 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -29,6 +29,7 @@ from .const import ( CONF_ACTION_NAME, CONF_ACTION_SHOW_IN_CARPLAY, CONF_ACTION_SHOW_IN_WATCH, + CONF_ACTION_USE_CUSTOM_COLORS, CONF_ACTIONS, DOMAIN, ) @@ -152,6 +153,7 @@ ACTION_SCHEMA = vol.Schema( }, vol.Optional(CONF_ACTION_SHOW_IN_CARPLAY): cv.boolean, vol.Optional(CONF_ACTION_SHOW_IN_WATCH): cv.boolean, + vol.Optional(CONF_ACTION_USE_CUSTOM_COLORS): cv.boolean, }, ) diff --git a/homeassistant/components/ios/const.py b/homeassistant/components/ios/const.py index 41da1954b44..181bbebd9a6 100644 --- a/homeassistant/components/ios/const.py +++ b/homeassistant/components/ios/const.py @@ -13,3 +13,4 @@ CONF_ACTION_ICON_ICON = "icon" CONF_ACTIONS = "actions" CONF_ACTION_SHOW_IN_CARPLAY = "show_in_carplay" CONF_ACTION_SHOW_IN_WATCH = "show_in_watch" +CONF_ACTION_USE_CUSTOM_COLORS = "use_custom_colors" From d78acd480a44dfd03eb88fce92c5c0d3eba6d22a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Cl=C3=A9ment?= Date: Tue, 30 Jul 2024 11:23:55 +0200 Subject: [PATCH 1711/2411] Add QBittorent switch to control alternative speed (#107637) * Fix key in strings.json for current_status in QBittorrent * Add switch on QBittorent to control alternative speed * Add switch file to .coveragerc * Fix some typo * Use coordinator for switch * Update to mach new lib * Import annotation Co-authored-by: Erik Montnemery * Remove quoted coordinator * Revert "Fix key in strings.json for current_status in QBittorrent" This reverts commit 962fd0474f0c9d6053bcf34898f68e48cf2bb715. --------- Co-authored-by: Erik Montnemery --- .../components/qbittorrent/__init__.py | 2 +- .../components/qbittorrent/coordinator.py | 22 +++- .../components/qbittorrent/helpers.py | 1 - .../components/qbittorrent/strings.json | 5 + .../components/qbittorrent/switch.py | 104 ++++++++++++++++++ 5 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/qbittorrent/switch.py diff --git a/homeassistant/components/qbittorrent/__init__.py b/homeassistant/components/qbittorrent/__init__.py index fb781dd1a0c..d95136965f8 100644 --- a/homeassistant/components/qbittorrent/__init__.py +++ b/homeassistant/components/qbittorrent/__init__.py @@ -34,7 +34,7 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.SENSOR, Platform.SWITCH] CONF_ENTRY = "entry" diff --git a/homeassistant/components/qbittorrent/coordinator.py b/homeassistant/components/qbittorrent/coordinator.py index 0ef36d2a954..c590bb9d81a 100644 --- a/homeassistant/components/qbittorrent/coordinator.py +++ b/homeassistant/components/qbittorrent/coordinator.py @@ -30,6 +30,7 @@ class QBittorrentDataCoordinator(DataUpdateCoordinator[SyncMainDataDictionary]): def __init__(self, hass: HomeAssistant, client: Client) -> None: """Initialize coordinator.""" self.client = client + self._is_alternative_mode_enabled = False # self.main_data: dict[str, int] = {} self.total_torrents: dict[str, int] = {} self.active_torrents: dict[str, int] = {} @@ -47,7 +48,13 @@ class QBittorrentDataCoordinator(DataUpdateCoordinator[SyncMainDataDictionary]): async def _async_update_data(self) -> SyncMainDataDictionary: try: - return await self.hass.async_add_executor_job(self.client.sync_maindata) + data = await self.hass.async_add_executor_job(self.client.sync_maindata) + self._is_alternative_mode_enabled = ( + await self.hass.async_add_executor_job( + self.client.transfer_speed_limits_mode + ) + == "1" + ) except (LoginFailed, Forbidden403Error) as exc: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="login_error" @@ -56,6 +63,19 @@ class QBittorrentDataCoordinator(DataUpdateCoordinator[SyncMainDataDictionary]): raise HomeAssistantError( translation_domain=DOMAIN, translation_key="cannot_connect" ) from exc + return data + + def set_alt_speed_enabled(self, is_enabled: bool) -> None: + """Set the alternative speed mode.""" + self.client.transfer_toggle_speed_limits_mode(is_enabled) + + def toggle_alt_speed_enabled(self) -> None: + """Toggle the alternative speed mode.""" + self.client.transfer_toggle_speed_limits_mode() + + def get_alt_speed_enabled(self) -> bool: + """Get the alternative speed mode.""" + return self._is_alternative_mode_enabled async def get_torrents(self, torrent_filter: TorrentStatusesT) -> TorrentInfoList: """Async method to get QBittorrent torrents.""" diff --git a/homeassistant/components/qbittorrent/helpers.py b/homeassistant/components/qbittorrent/helpers.py index fac0a6033fa..6b459e99741 100644 --- a/homeassistant/components/qbittorrent/helpers.py +++ b/homeassistant/components/qbittorrent/helpers.py @@ -8,7 +8,6 @@ from qbittorrentapi import Client, TorrentDictionary, TorrentInfoList def setup_client(url: str, username: str, password: str, verify_ssl: bool) -> Client: """Create a qBittorrent client.""" - client = Client( url, username=username, password=password, VERIFY_WEBUI_CERTIFICATE=verify_ssl ) diff --git a/homeassistant/components/qbittorrent/strings.json b/homeassistant/components/qbittorrent/strings.json index 948e9dca8e9..fe27beb2a2d 100644 --- a/homeassistant/components/qbittorrent/strings.json +++ b/homeassistant/components/qbittorrent/strings.json @@ -47,6 +47,11 @@ "all_torrents": { "name": "All torrents" } + }, + "switch": { + "alternative_speed": { + "name": "Alternative speed" + } } }, "services": { diff --git a/homeassistant/components/qbittorrent/switch.py b/homeassistant/components/qbittorrent/switch.py new file mode 100644 index 00000000000..f12118e5233 --- /dev/null +++ b/homeassistant/components/qbittorrent/switch.py @@ -0,0 +1,104 @@ +"""Support for monitoring the qBittorrent API.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import QBittorrentDataCoordinator + + +@dataclass(frozen=True, kw_only=True) +class QBittorrentSwitchEntityDescription(SwitchEntityDescription): + """Describes qBittorren switch.""" + + is_on_func: Callable[[QBittorrentDataCoordinator], bool] + turn_on_fn: Callable[[QBittorrentDataCoordinator], None] + turn_off_fn: Callable[[QBittorrentDataCoordinator], None] + toggle_func: Callable[[QBittorrentDataCoordinator], None] + + +SWITCH_TYPES: tuple[QBittorrentSwitchEntityDescription, ...] = ( + QBittorrentSwitchEntityDescription( + key="alternative_speed", + translation_key="alternative_speed", + icon="mdi:speedometer-slow", + is_on_func=lambda coordinator: coordinator.get_alt_speed_enabled(), + turn_on_fn=lambda coordinator: coordinator.set_alt_speed_enabled(True), + turn_off_fn=lambda coordinator: coordinator.set_alt_speed_enabled(False), + toggle_func=lambda coordinator: coordinator.toggle_alt_speed_enabled(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up qBittorrent switch entries.""" + + coordinator: QBittorrentDataCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + QBittorrentSwitch(coordinator, config_entry, description) + for description in SWITCH_TYPES + ) + + +class QBittorrentSwitch(CoordinatorEntity[QBittorrentDataCoordinator], SwitchEntity): + """Representation of a qBittorrent switch.""" + + _attr_has_entity_name = True + entity_description: QBittorrentSwitchEntityDescription + + def __init__( + self, + coordinator: QBittorrentDataCoordinator, + config_entry: ConfigEntry, + entity_description: QBittorrentSwitchEntityDescription, + ) -> None: + """Initialize qBittorrent switch.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_unique_id = f"{config_entry.entry_id}-{entity_description.key}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, config_entry.entry_id)}, + manufacturer="QBittorrent", + ) + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self.entity_description.is_on_func(self.coordinator) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on this switch.""" + await self.hass.async_add_executor_job( + self.entity_description.turn_on_fn, self.coordinator + ) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off this switch.""" + await self.hass.async_add_executor_job( + self.entity_description.turn_off_fn, self.coordinator + ) + await self.coordinator.async_request_refresh() + + async def async_toggle(self, **kwargs: Any) -> None: + """Toggle the device.""" + await self.hass.async_add_executor_job( + self.entity_description.toggle_func, self.coordinator + ) + await self.coordinator.async_request_refresh() From 53a59412bb12ff48dae11dd219bec593929cd4ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristof=20Mari=C3=ABn?= Date: Tue, 30 Jul 2024 02:34:30 -0700 Subject: [PATCH 1712/2411] Add Foscam sleep switch (#109491) * Add sleep switch * Replace awake with sleep switch --- homeassistant/components/foscam/__init__.py | 2 +- .../components/foscam/coordinator.py | 5 ++ homeassistant/components/foscam/strings.json | 7 ++ homeassistant/components/foscam/switch.py | 85 +++++++++++++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/foscam/switch.py diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index aed3ed637ae..f8708a589ce 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -17,7 +17,7 @@ from .config_flow import DEFAULT_RTSP_PORT from .const import CONF_RTSP_PORT, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET from .coordinator import FoscamCoordinator -PLATFORMS = [Platform.CAMERA] +PLATFORMS = [Platform.CAMERA, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/foscam/coordinator.py b/homeassistant/components/foscam/coordinator.py index 063d5235c04..e7a8abf7d30 100644 --- a/homeassistant/components/foscam/coordinator.py +++ b/homeassistant/components/foscam/coordinator.py @@ -44,4 +44,9 @@ class FoscamCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.session.get_product_all_info ) data["product_info"] = all_info[1] + + ret, is_asleep = await self.hass.async_add_executor_job( + self.session.is_asleep + ) + data["is_asleep"] = {"supported": ret == 0, "status": is_asleep} return data diff --git a/homeassistant/components/foscam/strings.json b/homeassistant/components/foscam/strings.json index 285f0f5a780..2784e541809 100644 --- a/homeassistant/components/foscam/strings.json +++ b/homeassistant/components/foscam/strings.json @@ -25,6 +25,13 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, + "entity": { + "switch": { + "sleep_switch": { + "name": "Sleep" + } + } + }, "services": { "ptz": { "name": "PTZ", diff --git a/homeassistant/components/foscam/switch.py b/homeassistant/components/foscam/switch.py new file mode 100644 index 00000000000..9eae211881f --- /dev/null +++ b/homeassistant/components/foscam/switch.py @@ -0,0 +1,85 @@ +"""Component provides support for the Foscam Switch.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import FoscamCoordinator +from .const import DOMAIN, LOGGER +from .entity import FoscamEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up foscam switch from a config entry.""" + + coordinator: FoscamCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + await coordinator.async_config_entry_first_refresh() + + if coordinator.data["is_asleep"]["supported"]: + async_add_entities([FoscamSleepSwitch(coordinator, config_entry)]) + + +class FoscamSleepSwitch(FoscamEntity, SwitchEntity): + """An implementation for Sleep Switch.""" + + def __init__( + self, + coordinator: FoscamCoordinator, + config_entry: ConfigEntry, + ) -> None: + """Initialize a Foscam Sleep Switch.""" + super().__init__(coordinator, config_entry.entry_id) + + self._attr_unique_id = "sleep_switch" + self._attr_translation_key = "sleep_switch" + self._attr_has_entity_name = True + + self.is_asleep = self.coordinator.data["is_asleep"]["status"] + + @property + def is_on(self): + """Return true if camera is asleep.""" + return self.is_asleep + + async def async_turn_off(self, **kwargs: Any) -> None: + """Wake camera.""" + LOGGER.debug("Wake camera") + + ret, _ = await self.hass.async_add_executor_job( + self.coordinator.session.wake_up + ) + + if ret != 0: + raise HomeAssistantError(f"Error waking up: {ret}") + + await self.coordinator.async_request_refresh() + + async def async_turn_on(self, **kwargs: Any) -> None: + """But camera is sleep.""" + LOGGER.debug("Sleep camera") + + ret, _ = await self.hass.async_add_executor_job(self.coordinator.session.sleep) + + if ret != 0: + raise HomeAssistantError(f"Error sleeping: {ret}") + + await self.coordinator.async_request_refresh() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + + self.is_asleep = self.coordinator.data["is_asleep"]["status"] + + self.async_write_ha_state() From 7c92287f972554826f4dd0bb99652ad88c7c64b6 Mon Sep 17 00:00:00 2001 From: Luke Wale Date: Tue, 30 Jul 2024 18:34:49 +0800 Subject: [PATCH 1713/2411] Add Airtouch5 cover tests (#122769) add airtouch5 cover tests --- tests/components/airtouch5/__init__.py | 12 ++ tests/components/airtouch5/conftest.py | 118 +++++++++++++++ .../airtouch5/snapshots/test_cover.ambr | 99 ++++++++++++ tests/components/airtouch5/test_cover.py | 143 ++++++++++++++++++ 4 files changed, 372 insertions(+) create mode 100644 tests/components/airtouch5/snapshots/test_cover.ambr create mode 100644 tests/components/airtouch5/test_cover.py diff --git a/tests/components/airtouch5/__init__.py b/tests/components/airtouch5/__init__.py index 2b76786e7e5..567be6af774 100644 --- a/tests/components/airtouch5/__init__.py +++ b/tests/components/airtouch5/__init__.py @@ -1 +1,13 @@ """Tests for the Airtouch 5 integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/airtouch5/conftest.py b/tests/components/airtouch5/conftest.py index ca678258c77..fab26e3f6cc 100644 --- a/tests/components/airtouch5/conftest.py +++ b/tests/components/airtouch5/conftest.py @@ -3,8 +3,22 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch +from airtouch5py.data_packet_factory import DataPacketFactory +from airtouch5py.packets.ac_ability import AcAbility +from airtouch5py.packets.ac_status import AcFanSpeed, AcMode, AcPowerState, AcStatus +from airtouch5py.packets.zone_name import ZoneName +from airtouch5py.packets.zone_status import ( + ControlMethod, + ZonePowerState, + ZoneStatusZone, +) import pytest +from homeassistant.components.airtouch5.const import DOMAIN +from homeassistant.const import CONF_HOST + +from tests.common import MockConfigEntry + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: @@ -13,3 +27,107 @@ def mock_setup_entry() -> Generator[AsyncMock]: "homeassistant.components.airtouch5.async_setup_entry", return_value=True ) as mock_setup_entry: yield mock_setup_entry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock the config entry.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id="1.1.1.1", + data={ + CONF_HOST: "1.1.1.1", + }, + ) + + +@pytest.fixture +def mock_airtouch5_client() -> Generator[AsyncMock]: + """Mock an Airtouch5 client.""" + + with ( + patch( + "homeassistant.components.airtouch5.Airtouch5SimpleClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.airtouch5.config_flow.Airtouch5SimpleClient", + new=mock_client, + ), + ): + client = mock_client.return_value + + # Default values for the tests using this mock : + client.data_packet_factory = DataPacketFactory() + client.ac = [ + AcAbility( + ac_number=1, + ac_name="AC 1", + start_zone_number=1, + zone_count=2, + supports_mode_cool=True, + supports_mode_fan=True, + supports_mode_dry=True, + supports_mode_heat=True, + supports_mode_auto=True, + supports_fan_speed_intelligent_auto=True, + supports_fan_speed_turbo=True, + supports_fan_speed_powerful=True, + supports_fan_speed_high=True, + supports_fan_speed_medium=True, + supports_fan_speed_low=True, + supports_fan_speed_quiet=True, + supports_fan_speed_auto=True, + min_cool_set_point=15, + max_cool_set_point=25, + min_heat_set_point=20, + max_heat_set_point=30, + ) + ] + client.latest_ac_status = { + 1: AcStatus( + ac_power_state=AcPowerState.ON, + ac_number=1, + ac_mode=AcMode.AUTO, + ac_fan_speed=AcFanSpeed.AUTO, + ac_setpoint=24, + turbo_active=False, + bypass_active=False, + spill_active=False, + timer_set=False, + temperature=24, + error_code=0, + ) + } + + client.zones = [ZoneName(1, "Zone 1"), ZoneName(2, "Zone 2")] + client.latest_zone_status = { + 1: ZoneStatusZone( + zone_power_state=ZonePowerState.ON, + zone_number=1, + control_method=ControlMethod.PERCENTAGE_CONTROL, + open_percentage=0.9, + set_point=24, + has_sensor=False, + temperature=24, + spill_active=False, + is_low_battery=False, + ), + 2: ZoneStatusZone( + zone_power_state=ZonePowerState.ON, + zone_number=1, + control_method=ControlMethod.TEMPERATURE_CONTROL, + open_percentage=1, + set_point=24, + has_sensor=True, + temperature=24, + spill_active=False, + is_low_battery=False, + ), + } + + client.connection_state_callbacks = [] + client.zone_status_callbacks = [] + client.ac_status_callbacks = [] + + yield client diff --git a/tests/components/airtouch5/snapshots/test_cover.ambr b/tests/components/airtouch5/snapshots/test_cover.ambr new file mode 100644 index 00000000000..a8e57f69527 --- /dev/null +++ b/tests/components/airtouch5/snapshots/test_cover.ambr @@ -0,0 +1,99 @@ +# serializer version: 1 +# name: test_all_entities[cover.zone_1_damper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.zone_1_damper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Damper', + 'platform': 'airtouch5', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'damper', + 'unique_id': 'zone_1_open_percentage', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[cover.zone_1_damper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 90, + 'device_class': 'damper', + 'friendly_name': 'Zone 1 Damper', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.zone_1_damper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- +# name: test_all_entities[cover.zone_2_damper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.zone_2_damper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Damper', + 'platform': 'airtouch5', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'damper', + 'unique_id': 'zone_2_open_percentage', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[cover.zone_2_damper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 100, + 'device_class': 'damper', + 'friendly_name': 'Zone 2 Damper', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.zone_2_damper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- diff --git a/tests/components/airtouch5/test_cover.py b/tests/components/airtouch5/test_cover.py new file mode 100644 index 00000000000..295535cd95d --- /dev/null +++ b/tests/components/airtouch5/test_cover.py @@ -0,0 +1,143 @@ +"""Tests for the Airtouch5 cover platform.""" + +from collections.abc import Callable +from unittest.mock import AsyncMock, patch + +from airtouch5py.packets.zone_status import ( + ControlMethod, + ZonePowerState, + ZoneStatusZone, +) +from syrupy import SnapshotAssertion + +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + STATE_OPEN, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_CLOSED, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + +COVER_ENTITY_ID = "cover.zone_1_damper" + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_airtouch5_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + + with patch("homeassistant.components.airtouch5.PLATFORMS", [Platform.COVER]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_cover_actions( + hass: HomeAssistant, + mock_airtouch5_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the actions of the Airtouch5 covers.""" + + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: COVER_ENTITY_ID}, + blocking=True, + ) + mock_airtouch5_client.send_packet.assert_called_once() + mock_airtouch5_client.reset_mock() + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: COVER_ENTITY_ID}, + blocking=True, + ) + mock_airtouch5_client.send_packet.assert_called_once() + mock_airtouch5_client.reset_mock() + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: COVER_ENTITY_ID, ATTR_POSITION: 50}, + blocking=True, + ) + mock_airtouch5_client.send_packet.assert_called_once() + mock_airtouch5_client.reset_mock() + + +async def test_cover_callbacks( + hass: HomeAssistant, + mock_airtouch5_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the callbacks of the Airtouch5 covers.""" + + await setup_integration(hass, mock_config_entry) + + # We find the callback method on the mock client + zone_status_callback: Callable[[dict[int, ZoneStatusZone]], None] = ( + mock_airtouch5_client.zone_status_callbacks[2] + ) + + # Define a method to simply call it + async def _call_zone_status_callback(open_percentage: int) -> None: + zsz = ZoneStatusZone( + zone_power_state=ZonePowerState.ON, + zone_number=1, + control_method=ControlMethod.PERCENTAGE_CONTROL, + open_percentage=open_percentage, + set_point=None, + has_sensor=False, + temperature=None, + spill_active=False, + is_low_battery=False, + ) + zone_status_callback({1: zsz}) + await hass.async_block_till_done() + + # And call it to effectively launch the callback as the server would do + + # Partly open + await _call_zone_status_callback(0.7) + state = hass.states.get(COVER_ENTITY_ID) + assert state + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_POSITION) == 70 + + # Fully open + await _call_zone_status_callback(1) + state = hass.states.get(COVER_ENTITY_ID) + assert state + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_POSITION) == 100 + + # Fully closed + await _call_zone_status_callback(0.0) + state = hass.states.get(COVER_ENTITY_ID) + assert state + assert state.state == STATE_CLOSED + assert state.attributes.get(ATTR_CURRENT_POSITION) == 0 + + # Partly reopened + await _call_zone_status_callback(0.3) + state = hass.states.get(COVER_ENTITY_ID) + assert state + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_POSITION) == 30 From b6f0893c336a575b9658b530eb89972b05abb8ff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:05:38 +0200 Subject: [PATCH 1714/2411] Fix implicit-return in denon (#122835) --- homeassistant/components/denon/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index b3b3ba97baa..0a6fe18d986 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -253,11 +253,12 @@ class DenonDevice(MediaPlayerEntity): return SUPPORT_DENON @property - def source(self): + def source(self) -> str | None: """Return the current input source.""" for pretty_name, name in self._source_list.items(): if self._mediasource == name: return pretty_name + return None def turn_off(self) -> None: """Turn off media player.""" From 015c50bbdb13788828b2e15f234dae4a5fbddf69 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:44:11 +0200 Subject: [PATCH 1715/2411] Fix implicit-return in ddwrt (#122837) --- homeassistant/components/ddwrt/device_tracker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index 30ab3af53fb..5d31d16a530 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -162,6 +162,7 @@ class DdWrtDeviceScanner(DeviceScanner): ) return None _LOGGER.error("Invalid response from DD-WRT: %s", response) + return None def _parse_ddwrt_response(data_str): From 956cc6a85cd13be520c2410989acde35a5538b88 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 30 Jul 2024 13:54:44 +0200 Subject: [PATCH 1716/2411] Add UI to create KNX switch and light entities (#122630) Update KNX frontend to 2024.7.25.204106 --- 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 3e8986641e7..5035239d1fb 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==2.12.2", "xknxproject==3.7.1", - "knx-frontend==2024.1.20.105944" + "knx-frontend==2024.7.25.204106" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 70565d41175..7a63bcb5602 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1213,7 +1213,7 @@ kiwiki-client==0.1.1 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.1.20.105944 +knx-frontend==2024.7.25.204106 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe01d45de76..9e8664f4821 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1009,7 +1009,7 @@ kegtron-ble==0.4.0 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.1.20.105944 +knx-frontend==2024.7.25.204106 # homeassistant.components.konnected konnected==1.2.0 From 72f9d85bbebb9c45b157974391728b12c6efb3ad Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:57:43 +0200 Subject: [PATCH 1717/2411] Fix implicit-return in whirlpool tests (#122775) --- tests/components/whirlpool/conftest.py | 2 ++ tests/components/whirlpool/test_sensor.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index a5926f55a94..50620b20b8b 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -145,6 +145,8 @@ def side_effect_function(*args, **kwargs): if args[0] == "WashCavity_OpStatusBulkDispense1Level": return "3" + return None + def get_sensor_mock(said): """Get a mock of a sensor.""" diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index 6af88c8a9f3..548025e29bd 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -42,6 +42,8 @@ def side_effect_function_open_door(*args, **kwargs): if args[0] == "WashCavity_OpStatusBulkDispense1Level": return "3" + return None + async def test_dryer_sensor_values( hass: HomeAssistant, From e7971f5a679b245e0aec0fdcb8afc8ff5d7d5f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Cl=C3=A9ment?= Date: Tue, 30 Jul 2024 15:03:36 +0200 Subject: [PATCH 1718/2411] Fix qbittorent current_status key in strings.json (#122848) --- homeassistant/components/qbittorrent/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/qbittorrent/strings.json b/homeassistant/components/qbittorrent/strings.json index fe27beb2a2d..88015dad5c3 100644 --- a/homeassistant/components/qbittorrent/strings.json +++ b/homeassistant/components/qbittorrent/strings.json @@ -26,7 +26,7 @@ "upload_speed": { "name": "Upload speed" }, - "transmission_status": { + "current_status": { "name": "Status", "state": { "idle": "[%key:common::state::idle%]", From fd7c92879cccfa456088fa03f3063a36a707f791 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:23:04 +0200 Subject: [PATCH 1719/2411] Fix implicit-return in foursquare (#122843) --- homeassistant/components/foursquare/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/foursquare/__init__.py b/homeassistant/components/foursquare/__init__.py index c0eac33a6a8..12a29fd632e 100644 --- a/homeassistant/components/foursquare/__init__.py +++ b/homeassistant/components/foursquare/__init__.py @@ -3,6 +3,7 @@ from http import HTTPStatus import logging +from aiohttp import web import requests import voluptuous as vol @@ -85,11 +86,11 @@ class FoursquarePushReceiver(HomeAssistantView): url = "/api/foursquare" name = "foursquare" - def __init__(self, push_secret): + def __init__(self, push_secret: str) -> None: """Initialize the OAuth callback view.""" self.push_secret = push_secret - async def post(self, request): + async def post(self, request: web.Request) -> web.Response | None: """Accept the POST from Foursquare.""" try: data = await request.json() @@ -107,3 +108,4 @@ class FoursquarePushReceiver(HomeAssistantView): return self.json_message("Incorrect secret", HTTPStatus.BAD_REQUEST) request.app[KEY_HASS].bus.async_fire(EVENT_PUSH, data) + return None From 41c7414d9784d5401bce706d109f83e4258b6323 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:23:53 +0200 Subject: [PATCH 1720/2411] Fix implicit-return in forked_daapd (#122842) --- homeassistant/components/forked_daapd/media_player.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 98ad2f28caf..b8b544c1a2c 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -827,12 +827,13 @@ class ForkedDaapdMaster(MediaPlayerEntity): return self._source[:-7] return "" - async def _pipe_call(self, pipe_name, base_function_name): - if self._pipe_control_api.get(pipe_name): - return await getattr( - self._pipe_control_api[pipe_name], + async def _pipe_call(self, pipe_name, base_function_name) -> None: + if pipe := self._pipe_control_api.get(pipe_name): + await getattr( + pipe, PIPE_FUNCTION_MAP[pipe_name][base_function_name], )() + return _LOGGER.warning("No pipe control available for %s", pipe_name) async def async_browse_media( From 27eba3cd4631a4efe3580e231532e0235bb4c367 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:24:35 +0200 Subject: [PATCH 1721/2411] Fix implicit-return in fixer (#122841) --- homeassistant/components/fixer/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index 4a03de5d6de..f8b4546d4c7 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from fixerio import Fixerio from fixerio.exceptions import FixerioException @@ -89,13 +90,14 @@ class ExchangeRateSensor(SensorEntity): return self._state @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" if self.data.rate is not None: return { ATTR_EXCHANGE_RATE: self.data.rate["rates"][self._target], ATTR_TARGET: self._target, } + return None def update(self) -> None: """Get the latest data and updates the states.""" From 7b5db6521c8a047570c968e95010d7279d486080 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:29:23 +0200 Subject: [PATCH 1722/2411] Fix implicit-return in advantage_air (#122840) --- homeassistant/components/advantage_air/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index 7f9d3f2dc65..8da46cc7463 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -206,7 +206,8 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC Mode and State.""" if hvac_mode == HVACMode.OFF: - return await self.async_turn_off() + await self.async_turn_off() + return if hvac_mode == HVACMode.HEAT_COOL and self.preset_mode != ADVANTAGE_AIR_MYAUTO: raise ServiceValidationError("Heat/Cool is not supported in this mode") await self.async_update_ac( From 09cd79772f8e1a07397ad6ea4dda48b1a47ccdb8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:29:53 +0200 Subject: [PATCH 1723/2411] Fix implicit-return in airtouch4 (#122839) --- homeassistant/components/airtouch4/climate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index 29fd2bc4bed..dbb6f02859b 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -156,7 +156,8 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): raise ValueError(f"Unsupported HVAC mode: {hvac_mode}") if hvac_mode == HVACMode.OFF: - return await self.async_turn_off() + await self.async_turn_off() + return await self._airtouch.SetCoolingModeForAc( self._ac_number, HA_STATE_TO_AT[hvac_mode] ) @@ -262,7 +263,8 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): raise ValueError(f"Unsupported HVAC mode: {hvac_mode}") if hvac_mode == HVACMode.OFF: - return await self.async_turn_off() + await self.async_turn_off() + return if self.hvac_mode == HVACMode.OFF: await self.async_turn_on() self._unit = self._airtouch.GetGroups()[self._group_number] From ea508b26298aed78676c93d28b520e4e1f6a4356 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:32:29 +0200 Subject: [PATCH 1724/2411] Fix implicit-return in dialogflow (#122834) --- homeassistant/components/dialogflow/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index 1c0da6b26eb..da6fbaf9969 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -103,6 +103,8 @@ def get_api_version(message): if message.get("responseId") is not None: return V2 + raise ValueError(f"Unable to extract API version from message: {message}") + async def async_handle_message(hass, message): """Handle a DialogFlow message.""" @@ -173,3 +175,5 @@ class DialogflowResponse: if self.api_version is V2: return {"fulfillmentText": self.speech, "source": SOURCE} + + raise ValueError(f"Invalid API version: {self.api_version}") From 2135691b90f4e7069c6981f373d21293d6420273 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:33:05 +0200 Subject: [PATCH 1725/2411] Fix implicit-return in dublin bus transport (#122833) --- homeassistant/components/dublin_bus_transport/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index 91773d08142..5fc3453fca6 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -9,6 +9,7 @@ from __future__ import annotations from contextlib import suppress from datetime import datetime, timedelta from http import HTTPStatus +from typing import Any import requests import voluptuous as vol @@ -102,7 +103,7 @@ class DublinPublicTransportSensor(SensorEntity): return self._state @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" if self._times is not None: next_up = "None" @@ -117,6 +118,7 @@ class DublinPublicTransportSensor(SensorEntity): ATTR_ROUTE: self._times[0][ATTR_ROUTE], ATTR_NEXT_UP: next_up, } + return None @property def native_unit_of_measurement(self): From c8372a3aa5094a1a96c3a64bed704f93763e43f4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:33:57 +0200 Subject: [PATCH 1726/2411] Fix implicit-return in ecobee (#122832) --- homeassistant/components/ecobee/binary_sensor.py | 3 ++- homeassistant/components/ecobee/sensor.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 4286f2cf757..2a021442a63 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -46,7 +46,7 @@ class EcobeeBinarySensor(BinarySensorEntity): self.index = sensor_index @property - def unique_id(self): + def unique_id(self) -> str | None: """Return a unique identifier for this sensor.""" for sensor in self.data.ecobee.get_remote_sensors(self.index): if sensor["name"] == self.sensor_name: @@ -54,6 +54,7 @@ class EcobeeBinarySensor(BinarySensorEntity): return f"{sensor['code']}-{self.device_class}" thermostat = self.data.ecobee.get_thermostat(self.index) return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" + return None @property def device_info(self) -> DeviceInfo | None: diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 3e2e984cccb..fe0442fb885 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -112,7 +112,7 @@ class EcobeeSensor(SensorEntity): self._state = None @property - def unique_id(self): + def unique_id(self) -> str | None: """Return a unique identifier for this sensor.""" for sensor in self.data.ecobee.get_remote_sensors(self.index): if sensor["name"] == self.sensor_name: @@ -120,6 +120,7 @@ class EcobeeSensor(SensorEntity): return f"{sensor['code']}-{self.device_class}" thermostat = self.data.ecobee.get_thermostat(self.index) return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" + return None @property def device_info(self) -> DeviceInfo | None: From 224228e4480b4f71c5722ed6ac33221c5b1b4710 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Jul 2024 16:16:33 +0200 Subject: [PATCH 1727/2411] Fix Axis tests affecting other tests (#122857) --- tests/components/axis/conftest.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index 30e1b7335b9..c3377c15955 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -128,6 +128,13 @@ def fixture_config_entry_options() -> MappingProxyType[str, Any]: # Axis API fixtures +@pytest.fixture(autouse=True) +def reset_mock_requests() -> Generator[None]: + """Reset respx mock routes after the test.""" + yield + respx.mock.clear() + + @pytest.fixture(name="mock_requests") def fixture_request( respx_mock: respx.MockRouter, From d9e996def5ad3824504e5062640b5393b2ec0132 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Jul 2024 16:18:47 +0200 Subject: [PATCH 1728/2411] Fix template binary sensor test (#122855) --- .../components/template/test_binary_sensor.py | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 50cad5be9e1..eb51b3f53b4 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -1,5 +1,6 @@ """The tests for the Template Binary sensor platform.""" +from copy import deepcopy from datetime import UTC, datetime, timedelta import logging from unittest.mock import patch @@ -995,20 +996,32 @@ async def test_availability_icon_picture( ], ) @pytest.mark.parametrize( - ("extra_config", "restored_state", "initial_state"), + ("extra_config", "source_state", "restored_state", "initial_state"), [ - ({}, ON, OFF), - ({}, OFF, OFF), - ({}, STATE_UNAVAILABLE, OFF), - ({}, STATE_UNKNOWN, OFF), - ({"delay_off": 5}, ON, ON), - ({"delay_off": 5}, OFF, OFF), - ({"delay_off": 5}, STATE_UNAVAILABLE, STATE_UNKNOWN), - ({"delay_off": 5}, STATE_UNKNOWN, STATE_UNKNOWN), - ({"delay_on": 5}, ON, ON), - ({"delay_on": 5}, OFF, OFF), - ({"delay_on": 5}, STATE_UNAVAILABLE, STATE_UNKNOWN), - ({"delay_on": 5}, STATE_UNKNOWN, STATE_UNKNOWN), + ({}, OFF, ON, OFF), + ({}, OFF, OFF, OFF), + ({}, OFF, STATE_UNAVAILABLE, OFF), + ({}, OFF, STATE_UNKNOWN, OFF), + ({"delay_off": 5}, OFF, ON, ON), + ({"delay_off": 5}, OFF, OFF, OFF), + ({"delay_off": 5}, OFF, STATE_UNAVAILABLE, STATE_UNKNOWN), + ({"delay_off": 5}, OFF, STATE_UNKNOWN, STATE_UNKNOWN), + ({"delay_on": 5}, OFF, ON, OFF), + ({"delay_on": 5}, OFF, OFF, OFF), + ({"delay_on": 5}, OFF, STATE_UNAVAILABLE, OFF), + ({"delay_on": 5}, OFF, STATE_UNKNOWN, OFF), + ({}, ON, ON, ON), + ({}, ON, OFF, ON), + ({}, ON, STATE_UNAVAILABLE, ON), + ({}, ON, STATE_UNKNOWN, ON), + ({"delay_off": 5}, ON, ON, ON), + ({"delay_off": 5}, ON, OFF, ON), + ({"delay_off": 5}, ON, STATE_UNAVAILABLE, ON), + ({"delay_off": 5}, ON, STATE_UNKNOWN, ON), + ({"delay_on": 5}, ON, ON, ON), + ({"delay_on": 5}, ON, OFF, OFF), + ({"delay_on": 5}, ON, STATE_UNAVAILABLE, STATE_UNKNOWN), + ({"delay_on": 5}, ON, STATE_UNKNOWN, STATE_UNKNOWN), ], ) async def test_restore_state( @@ -1017,18 +1030,20 @@ async def test_restore_state( domain, config, extra_config, + source_state, restored_state, initial_state, ) -> None: """Test restoring template binary sensor.""" + hass.states.async_set("sensor.test_state", source_state) fake_state = State( "binary_sensor.test", restored_state, {}, ) mock_restore_cache(hass, (fake_state,)) - config = dict(config) + config = deepcopy(config) config["template"]["binary_sensor"].update(**extra_config) with assert_setup_component(count, domain): assert await async_setup_component( From a5136a10217f3e643952548402338312e5c3dd95 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:27:58 +0200 Subject: [PATCH 1729/2411] Speed up slow tests in Husqvarna Automower (#122854) --- .../husqvarna_automower/test_number.py | 20 ++++++++++++++++--- .../husqvarna_automower/test_switch.py | 12 +++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/tests/components/husqvarna_automower/test_number.py b/tests/components/husqvarna_automower/test_number.py index ac7353386ac..9f2f8793bba 100644 --- a/tests/components/husqvarna_automower/test_number.py +++ b/tests/components/husqvarna_automower/test_number.py @@ -1,13 +1,18 @@ """Tests for number platform.""" +from datetime import timedelta from unittest.mock import AsyncMock, patch from aioautomower.exceptions import ApiException from aioautomower.utils import mower_list_to_dictionary_dataclass +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion -from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.components.husqvarna_automower.const import ( + DOMAIN, + EXECUTION_TIME_DELAY, +) from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -16,7 +21,12 @@ from homeassistant.helpers import entity_registry as er from . import setup_integration from .const import TEST_MOWER_ID -from tests.common import MockConfigEntry, load_json_value_fixture, snapshot_platform +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_value_fixture, + snapshot_platform, +) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -57,6 +67,7 @@ async def test_number_workarea_commands( hass: HomeAssistant, mock_automower_client: AsyncMock, mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, ) -> None: """Test number commands.""" entity_id = "number.test_mower_1_front_lawn_cutting_height" @@ -75,8 +86,11 @@ async def test_number_workarea_commands( service="set_value", target={"entity_id": entity_id}, service_data={"value": "75"}, - blocking=True, + blocking=False, ) + freezer.tick(timedelta(seconds=EXECUTION_TIME_DELAY)) + async_fire_time_changed(hass) + await hass.async_block_till_done() mocked_method.assert_called_once_with(TEST_MOWER_ID, 75, 123456) state = hass.states.get(entity_id) assert state.state is not None diff --git a/tests/components/husqvarna_automower/test_switch.py b/tests/components/husqvarna_automower/test_switch.py index 24fd63be749..5b4e465e253 100644 --- a/tests/components/husqvarna_automower/test_switch.py +++ b/tests/components/husqvarna_automower/test_switch.py @@ -1,5 +1,6 @@ """Tests for switch platform.""" +from datetime import timedelta from unittest.mock import AsyncMock, patch from aioautomower.exceptions import ApiException @@ -9,7 +10,10 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion -from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.components.husqvarna_automower.const import ( + DOMAIN, + EXECUTION_TIME_DELAY, +) from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -109,6 +113,7 @@ async def test_stay_out_zone_switch_commands( excepted_state: str, mock_automower_client: AsyncMock, mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, ) -> None: """Test switch commands.""" entity_id = "switch.test_mower_1_avoid_danger_zone" @@ -124,8 +129,11 @@ async def test_stay_out_zone_switch_commands( domain="switch", service=service, service_data={"entity_id": entity_id}, - blocking=True, + blocking=False, ) + freezer.tick(timedelta(seconds=EXECUTION_TIME_DELAY)) + async_fire_time_changed(hass) + await hass.async_block_till_done() mocked_method.assert_called_once_with(TEST_MOWER_ID, TEST_ZONE_ID, boolean) state = hass.states.get(entity_id) assert state is not None From b973455037a163b9753df3b493c82b545dac74f4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Jul 2024 16:28:55 +0200 Subject: [PATCH 1730/2411] Fix template image test affecting other tests (#122849) --- tests/components/template/test_image.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/components/template/test_image.py b/tests/components/template/test_image.py index d4e98d7a3ca..101b475956a 100644 --- a/tests/components/template/test_image.py +++ b/tests/components/template/test_image.py @@ -76,10 +76,12 @@ async def _assert_state( assert body == expected_image +@respx.mock @pytest.mark.freeze_time("2024-07-09 00:00:00+00:00") async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, + imgbytes_jpg, ) -> None: """Test the config flow.""" @@ -538,6 +540,7 @@ async def test_trigger_image_custom_entity_picture( ) +@respx.mock async def test_device_id( hass: HomeAssistant, device_registry: dr.DeviceRegistry, From 1382f7a3dc12423ffb2f9a38bd76a72a799ae222 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Jul 2024 16:29:59 +0200 Subject: [PATCH 1731/2411] Fix generic IP camera tests affecting other tests (#122858) --- tests/components/generic/conftest.py | 17 +++++++++++++---- tests/components/generic/test_config_flow.py | 1 + 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/components/generic/conftest.py b/tests/components/generic/conftest.py index 34062aab954..69e6cc6b696 100644 --- a/tests/components/generic/conftest.py +++ b/tests/components/generic/conftest.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator from io import BytesIO from unittest.mock import AsyncMock, MagicMock, Mock, _patch, patch @@ -51,15 +52,23 @@ def fakeimgbytes_gif() -> bytes: @pytest.fixture -def fakeimg_png(fakeimgbytes_png: bytes) -> None: +def fakeimg_png(fakeimgbytes_png: bytes) -> Generator[None]: """Set up respx to respond to test url with fake image bytes.""" - respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) + respx.get("http://127.0.0.1/testurl/1", name="fake_img").respond( + stream=fakeimgbytes_png + ) + yield + respx.pop("fake_img") @pytest.fixture -def fakeimg_gif(fakeimgbytes_gif: bytes) -> None: +def fakeimg_gif(fakeimgbytes_gif: bytes) -> Generator[None]: """Set up respx to respond to test url with fake image bytes.""" - respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_gif) + respx.get("http://127.0.0.1/testurl/1", name="fake_img").respond( + stream=fakeimgbytes_gif + ) + yield + respx.pop("fake_img") @pytest.fixture(scope="package") diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 456e41a8d60..e7af9383791 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -638,6 +638,7 @@ async def test_form_stream_other_error(hass: HomeAssistant, user_flow) -> None: @respx.mock +@pytest.mark.usefixtures("fakeimg_png") async def test_form_stream_worker_error( hass: HomeAssistant, user_flow: ConfigFlowResult ) -> None: From 4994e46ad04fec84f7e2aed0f167796ab158d192 Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:44:04 +0200 Subject: [PATCH 1732/2411] Add mdi:alert-circle-outline to degrade status (#122859) --- homeassistant/components/synology_dsm/icons.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/icons.json b/homeassistant/components/synology_dsm/icons.json index 8b4fad457d5..8e6d2b17f02 100644 --- a/homeassistant/components/synology_dsm/icons.json +++ b/homeassistant/components/synology_dsm/icons.json @@ -50,7 +50,10 @@ "default": "mdi:download" }, "volume_status": { - "default": "mdi:checkbox-marked-circle-outline" + "default": "mdi:checkbox-marked-circle-outline", + "state": { + "degrade": "mdi:alert-circle-outline" + } }, "volume_size_total": { "default": "mdi:chart-pie" From b3f7f379df235bcd7ebdd2b039932c98e2dff937 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 30 Jul 2024 16:51:02 +0200 Subject: [PATCH 1733/2411] Upgrade dsmr-parser to 1.4.2 (#121929) --- homeassistant/components/dsmr/manifest.json | 2 +- homeassistant/components/dsmr/sensor.py | 141 ++++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dsmr/test_mbus_migration.py | 30 +-- tests/components/dsmr/test_sensor.py | 183 ++++++++++--------- 6 files changed, 195 insertions(+), 165 deletions(-) diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index c8f0a78f4dc..5490b2a6503 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["dsmr_parser"], - "requirements": ["dsmr-parser==1.3.1"] + "requirements": ["dsmr-parser==1.4.2"] } diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index ae7b08b7f62..f794d1d05e9 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 collections.abc import Callable +from collections.abc import Callable, Generator from contextlib import suppress from dataclasses import dataclass from datetime import timedelta +from enum import IntEnum from functools import partial from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader @@ -15,7 +16,7 @@ from dsmr_parser.clients.rfxtrx_protocol import ( create_rfxtrx_dsmr_reader, create_rfxtrx_tcp_dsmr_reader, ) -from dsmr_parser.objects import DSMRObject, Telegram +from dsmr_parser.objects import DSMRObject, MbusDevice, Telegram import serial from homeassistant.components.sensor import ( @@ -77,6 +78,13 @@ class DSMRSensorEntityDescription(SensorEntityDescription): obis_reference: str +class MbusDeviceType(IntEnum): + """Types of mbus devices (13757-3:2013).""" + + GAS = 3 + WATER = 7 + + SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="timestamp", @@ -318,7 +326,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="belgium_max_current_per_phase", translation_key="max_current_per_phase", - obis_reference="BELGIUM_MAX_CURRENT_PER_PHASE", + obis_reference="FUSE_THRESHOLD_L1", dsmr_versions={"5B"}, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, @@ -377,38 +385,36 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), ) - -def create_mbus_entity( - mbus: int, mtype: int, telegram: Telegram -) -> DSMRSensorEntityDescription | None: - """Create a new MBUS Entity.""" - if mtype == 3 and hasattr(telegram, f"BELGIUM_MBUS{mbus}_METER_READING2"): - return DSMRSensorEntityDescription( - key=f"mbus{mbus}_gas_reading", +SENSORS_MBUS_DEVICE_TYPE: dict[int, tuple[DSMRSensorEntityDescription, ...]] = { + MbusDeviceType.GAS: ( + DSMRSensorEntityDescription( + key="gas_reading", translation_key="gas_meter_reading", - obis_reference=f"BELGIUM_MBUS{mbus}_METER_READING2", + obis_reference="MBUS_METER_READING", is_gas=True, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, - ) - if mtype == 7 and (hasattr(telegram, f"BELGIUM_MBUS{mbus}_METER_READING1")): - return DSMRSensorEntityDescription( - key=f"mbus{mbus}_water_reading", + ), + ), + MbusDeviceType.WATER: ( + DSMRSensorEntityDescription( + key="water_reading", translation_key="water_meter_reading", - obis_reference=f"BELGIUM_MBUS{mbus}_METER_READING1", + obis_reference="MBUS_METER_READING", is_water=True, device_class=SensorDeviceClass.WATER, state_class=SensorStateClass.TOTAL_INCREASING, - ) - return None + ), + ), +} def device_class_and_uom( - telegram: dict[str, DSMRObject], + data: Telegram | MbusDevice, entity_description: DSMRSensorEntityDescription, ) -> tuple[SensorDeviceClass | None, str | None]: """Get native unit of measurement from telegram,.""" - dsmr_object = getattr(telegram, entity_description.obis_reference) + dsmr_object = getattr(data, entity_description.obis_reference) uom: str | None = getattr(dsmr_object, "unit") or None with suppress(ValueError): if entity_description.device_class == SensorDeviceClass.GAS and ( @@ -460,37 +466,60 @@ def rename_old_gas_to_mbus( dev_reg.async_remove_device(device_id) +def is_supported_description( + data: Telegram | MbusDevice, + description: DSMRSensorEntityDescription, + dsmr_version: str, +) -> bool: + """Check if this is a supported description for this telegram.""" + return hasattr(data, description.obis_reference) and ( + description.dsmr_versions is None or dsmr_version in description.dsmr_versions + ) + + def create_mbus_entities( - hass: HomeAssistant, telegram: Telegram, entry: ConfigEntry -) -> list[DSMREntity]: + hass: HomeAssistant, telegram: Telegram, entry: ConfigEntry, dsmr_version: str +) -> Generator[DSMREntity]: """Create MBUS Entities.""" - entities = [] - for idx in range(1, 5): - if ( - device_type := getattr(telegram, f"BELGIUM_MBUS{idx}_DEVICE_TYPE", None) - ) is None: + mbus_devices: list[MbusDevice] = getattr(telegram, "MBUS_DEVICES", []) + for device in mbus_devices: + if (device_type := getattr(device, "MBUS_DEVICE_TYPE", None)) is None: continue - if (type_ := int(device_type.value)) not in (3, 7): - continue - if identifier := getattr( - telegram, f"BELGIUM_MBUS{idx}_EQUIPMENT_IDENTIFIER", None - ): + type_ = int(device_type.value) + + if identifier := getattr(device, "MBUS_EQUIPMENT_IDENTIFIER", None): serial_ = identifier.value rename_old_gas_to_mbus(hass, entry, serial_) else: serial_ = "" - if description := create_mbus_entity(idx, type_, telegram): - entities.append( - DSMREntity( - description, - entry, - telegram, - *device_class_and_uom(telegram, description), # type: ignore[arg-type] - serial_, - idx, - ) + + for description in SENSORS_MBUS_DEVICE_TYPE.get(type_, ()): + if not is_supported_description(device, description, dsmr_version): + continue + yield DSMREntity( + description, + entry, + telegram, + *device_class_and_uom(device, description), # type: ignore[arg-type] + serial_, + device.channel_id, ) - return entities + + +def get_dsmr_object( + telegram: Telegram | None, mbus_id: int, obis_reference: str +) -> DSMRObject | None: + """Extract DSMR object from telegram.""" + if not telegram: + return None + + telegram_or_device: Telegram | MbusDevice | None = telegram + if mbus_id: + telegram_or_device = telegram.get_mbus_device_by_channel(mbus_id) + if telegram_or_device is None: + return None + + return getattr(telegram_or_device, obis_reference, None) async def async_setup_entry( @@ -510,8 +539,7 @@ async def async_setup_entry( add_entities_handler() add_entities_handler = None - if dsmr_version == "5B": - entities.extend(create_mbus_entities(hass, telegram, entry)) + entities.extend(create_mbus_entities(hass, telegram, entry, dsmr_version)) entities.extend( [ @@ -522,12 +550,8 @@ async def async_setup_entry( *device_class_and_uom(telegram, description), # type: ignore[arg-type] ) for description in SENSORS - if ( - description.dsmr_versions is None - or dsmr_version in description.dsmr_versions - ) + if is_supported_description(telegram, description, dsmr_version) and (not description.is_gas or CONF_SERIAL_ID_GAS in entry.data) - and hasattr(telegram, description.obis_reference) ] ) async_add_entities(entities) @@ -723,6 +747,7 @@ class DSMREntity(SensorEntity): identifiers={(DOMAIN, device_serial)}, name=device_name, ) + self._mbus_id = mbus_id if mbus_id != 0: if serial_id: self._attr_unique_id = f"{device_serial}" @@ -737,20 +762,22 @@ class DSMREntity(SensorEntity): self.telegram = telegram if self.hass and ( telegram is None - or hasattr(telegram, self.entity_description.obis_reference) + or get_dsmr_object( + telegram, self._mbus_id, self.entity_description.obis_reference + ) ): 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.telegram is None or not hasattr( - self.telegram, self.entity_description.obis_reference - ): + # Get the object + dsmr_object = get_dsmr_object( + self.telegram, self._mbus_id, self.entity_description.obis_reference + ) + if dsmr_object is None: return None # Get the attribute value if the object has it - dsmr_object = getattr(self.telegram, self.entity_description.obis_reference) attr: str | None = getattr(dsmr_object, attribute) return attr diff --git a/requirements_all.txt b/requirements_all.txt index 7a63bcb5602..4d74e059a59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -750,7 +750,7 @@ dremel3dpy==2.1.1 dropmqttapi==1.0.3 # homeassistant.components.dsmr -dsmr-parser==1.3.1 +dsmr-parser==1.4.2 # homeassistant.components.dwd_weather_warnings dwdwfsapi==1.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e8664f4821..86fdf2da7f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -640,7 +640,7 @@ dremel3dpy==2.1.1 dropmqttapi==1.0.3 # homeassistant.components.dsmr -dsmr-parser==1.3.1 +dsmr-parser==1.4.2 # homeassistant.components.dwd_weather_warnings dwdwfsapi==1.0.7 diff --git a/tests/components/dsmr/test_mbus_migration.py b/tests/components/dsmr/test_mbus_migration.py index cd3db27be8c..a28bc2c3a33 100644 --- a/tests/components/dsmr/test_mbus_migration.py +++ b/tests/components/dsmr/test_mbus_migration.py @@ -5,9 +5,9 @@ from decimal import Decimal from unittest.mock import MagicMock from dsmr_parser.obis_references import ( - BELGIUM_MBUS1_DEVICE_TYPE, - BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS1_METER_READING2, + MBUS_DEVICE_TYPE, + MBUS_EQUIPMENT_IDENTIFIER, + MBUS_METER_READING, ) from dsmr_parser.objects import CosemObject, MBusObject, Telegram @@ -67,20 +67,20 @@ async def test_migrate_gas_to_mbus( telegram = Telegram() telegram.add( - BELGIUM_MBUS1_DEVICE_TYPE, + MBUS_DEVICE_TYPE, CosemObject((0, 1), [{"value": "003", "unit": ""}]), - "BELGIUM_MBUS1_DEVICE_TYPE", + "MBUS_DEVICE_TYPE", ) telegram.add( - BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + MBUS_EQUIPMENT_IDENTIFIER, CosemObject( (0, 1), [{"value": "37464C4F32313139303333373331", "unit": ""}], ), - "BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER", + "MBUS_EQUIPMENT_IDENTIFIER", ) telegram.add( - BELGIUM_MBUS1_METER_READING2, + MBUS_METER_READING, MBusObject( (0, 1), [ @@ -88,7 +88,7 @@ async def test_migrate_gas_to_mbus( {"value": Decimal(745.695), "unit": "m3"}, ], ), - "BELGIUM_MBUS1_METER_READING2", + "MBUS_METER_READING", ) assert await hass.config_entries.async_setup(mock_entry.entry_id) @@ -184,20 +184,20 @@ async def test_migrate_gas_to_mbus_exists( telegram = Telegram() telegram.add( - BELGIUM_MBUS1_DEVICE_TYPE, + MBUS_DEVICE_TYPE, CosemObject((0, 0), [{"value": "003", "unit": ""}]), - "BELGIUM_MBUS1_DEVICE_TYPE", + "MBUS_DEVICE_TYPE", ) telegram.add( - BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + MBUS_EQUIPMENT_IDENTIFIER, CosemObject( (0, 1), [{"value": "37464C4F32313139303333373331", "unit": ""}], ), - "BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER", + "MBUS_EQUIPMENT_IDENTIFIER", ) telegram.add( - BELGIUM_MBUS1_METER_READING2, + MBUS_METER_READING, MBusObject( (0, 1), [ @@ -205,7 +205,7 @@ async def test_migrate_gas_to_mbus_exists( {"value": Decimal(745.695), "unit": "m3"}, ], ), - "BELGIUM_MBUS1_METER_READING2", + "MBUS_METER_READING", ) assert await hass.config_entries.async_setup(mock_entry.entry_id) diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 5b0cf6d7a15..b93dd8d18d2 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -15,33 +15,20 @@ from dsmr_parser import obis_references from dsmr_parser.obis_references import ( BELGIUM_CURRENT_AVERAGE_DEMAND, BELGIUM_MAXIMUM_DEMAND_MONTH, - BELGIUM_MBUS1_DEVICE_TYPE, - BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS1_METER_READING1, - BELGIUM_MBUS1_METER_READING2, - BELGIUM_MBUS2_DEVICE_TYPE, - BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS2_METER_READING1, - BELGIUM_MBUS2_METER_READING2, - BELGIUM_MBUS3_DEVICE_TYPE, - BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS3_METER_READING1, - BELGIUM_MBUS3_METER_READING2, - BELGIUM_MBUS4_DEVICE_TYPE, - BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER, - BELGIUM_MBUS4_METER_READING1, - BELGIUM_MBUS4_METER_READING2, CURRENT_ELECTRICITY_USAGE, ELECTRICITY_ACTIVE_TARIFF, ELECTRICITY_EXPORTED_TOTAL, ELECTRICITY_IMPORTED_TOTAL, GAS_METER_READING, HOURLY_GAS_METER_READING, + MBUS_DEVICE_TYPE, + MBUS_EQUIPMENT_IDENTIFIER, + MBUS_METER_READING, ) from dsmr_parser.objects import CosemObject, MBusObject, Telegram import pytest -from homeassistant.components.dsmr.sensor import SENSORS +from homeassistant.components.dsmr.sensor import SENSORS, SENSORS_MBUS_DEVICE_TYPE from homeassistant.components.sensor import ( ATTR_OPTIONS, ATTR_STATE_CLASS, @@ -562,20 +549,20 @@ async def test_belgian_meter( "BELGIUM_MAXIMUM_DEMAND_MONTH", ) telegram.add( - BELGIUM_MBUS1_DEVICE_TYPE, + MBUS_DEVICE_TYPE, CosemObject((0, 1), [{"value": "003", "unit": ""}]), - "BELGIUM_MBUS1_DEVICE_TYPE", + "MBUS_DEVICE_TYPE", ) telegram.add( - BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + MBUS_EQUIPMENT_IDENTIFIER, CosemObject( (0, 1), [{"value": "37464C4F32313139303333373331", "unit": ""}], ), - "BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER", + "MBUS_EQUIPMENT_IDENTIFIER", ) telegram.add( - BELGIUM_MBUS1_METER_READING2, + MBUS_METER_READING, MBusObject( (0, 1), [ @@ -583,23 +570,23 @@ async def test_belgian_meter( {"value": Decimal(745.695), "unit": "m3"}, ], ), - "BELGIUM_MBUS1_METER_READING2", + "MBUS_METER_READING", ) telegram.add( - BELGIUM_MBUS2_DEVICE_TYPE, + MBUS_DEVICE_TYPE, CosemObject((0, 2), [{"value": "007", "unit": ""}]), - "BELGIUM_MBUS2_DEVICE_TYPE", + "MBUS_DEVICE_TYPE", ) telegram.add( - BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, + MBUS_EQUIPMENT_IDENTIFIER, CosemObject( (0, 2), [{"value": "37464C4F32313139303333373332", "unit": ""}], ), - "BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER", + "MBUS_EQUIPMENT_IDENTIFIER", ) telegram.add( - BELGIUM_MBUS2_METER_READING1, + MBUS_METER_READING, MBusObject( (0, 2), [ @@ -607,23 +594,23 @@ async def test_belgian_meter( {"value": Decimal(678.695), "unit": "m3"}, ], ), - "BELGIUM_MBUS2_METER_READING1", + "MBUS_METER_READING", ) telegram.add( - BELGIUM_MBUS3_DEVICE_TYPE, + MBUS_DEVICE_TYPE, CosemObject((0, 3), [{"value": "003", "unit": ""}]), - "BELGIUM_MBUS3_DEVICE_TYPE", + "MBUS_DEVICE_TYPE", ) telegram.add( - BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, + MBUS_EQUIPMENT_IDENTIFIER, CosemObject( (0, 3), [{"value": "37464C4F32313139303333373333", "unit": ""}], ), - "BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER", + "MBUS_EQUIPMENT_IDENTIFIER", ) telegram.add( - BELGIUM_MBUS3_METER_READING2, + MBUS_METER_READING, MBusObject( (0, 3), [ @@ -631,23 +618,23 @@ async def test_belgian_meter( {"value": Decimal(12.12), "unit": "m3"}, ], ), - "BELGIUM_MBUS3_METER_READING2", + "MBUS_METER_READING", ) telegram.add( - BELGIUM_MBUS4_DEVICE_TYPE, + MBUS_DEVICE_TYPE, CosemObject((0, 4), [{"value": "007", "unit": ""}]), - "BELGIUM_MBUS4_DEVICE_TYPE", + "MBUS_DEVICE_TYPE", ) telegram.add( - BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER, + MBUS_EQUIPMENT_IDENTIFIER, CosemObject( (0, 4), [{"value": "37464C4F32313139303333373334", "unit": ""}], ), - "BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER", + "MBUS_EQUIPMENT_IDENTIFIER", ) telegram.add( - BELGIUM_MBUS4_METER_READING1, + MBUS_METER_READING, MBusObject( (0, 4), [ @@ -655,7 +642,7 @@ async def test_belgian_meter( {"value": Decimal(13.13), "unit": "m3"}, ], ), - "BELGIUM_MBUS4_METER_READING1", + "MBUS_METER_READING", ) telegram.add( ELECTRICITY_ACTIVE_TARIFF, @@ -777,20 +764,20 @@ async def test_belgian_meter_alt( telegram = Telegram() telegram.add( - BELGIUM_MBUS1_DEVICE_TYPE, + MBUS_DEVICE_TYPE, CosemObject((0, 1), [{"value": "007", "unit": ""}]), - "BELGIUM_MBUS1_DEVICE_TYPE", + "MBUS_DEVICE_TYPE", ) telegram.add( - BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + MBUS_EQUIPMENT_IDENTIFIER, CosemObject( (0, 1), [{"value": "37464C4F32313139303333373331", "unit": ""}], ), - "BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER", + "MBUS_EQUIPMENT_IDENTIFIER", ) telegram.add( - BELGIUM_MBUS1_METER_READING1, + MBUS_METER_READING, MBusObject( (0, 1), [ @@ -798,23 +785,23 @@ async def test_belgian_meter_alt( {"value": Decimal(123.456), "unit": "m3"}, ], ), - "BELGIUM_MBUS1_METER_READING1", + "MBUS_METER_READING", ) telegram.add( - BELGIUM_MBUS2_DEVICE_TYPE, + MBUS_DEVICE_TYPE, CosemObject((0, 2), [{"value": "003", "unit": ""}]), - "BELGIUM_MBUS2_DEVICE_TYPE", + "MBUS_DEVICE_TYPE", ) telegram.add( - BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, + MBUS_EQUIPMENT_IDENTIFIER, CosemObject( (0, 2), [{"value": "37464C4F32313139303333373332", "unit": ""}], ), - "BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER", + "MBUS_EQUIPMENT_IDENTIFIER", ) telegram.add( - BELGIUM_MBUS2_METER_READING2, + MBUS_METER_READING, MBusObject( (0, 2), [ @@ -822,23 +809,23 @@ async def test_belgian_meter_alt( {"value": Decimal(678.901), "unit": "m3"}, ], ), - "BELGIUM_MBUS2_METER_READING2", + "MBUS_METER_READING", ) telegram.add( - BELGIUM_MBUS3_DEVICE_TYPE, + MBUS_DEVICE_TYPE, CosemObject((0, 3), [{"value": "007", "unit": ""}]), - "BELGIUM_MBUS3_DEVICE_TYPE", + "MBUS_DEVICE_TYPE", ) telegram.add( - BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, + MBUS_EQUIPMENT_IDENTIFIER, CosemObject( (0, 3), [{"value": "37464C4F32313139303333373333", "unit": ""}], ), - "BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER", + "MBUS_EQUIPMENT_IDENTIFIER", ) telegram.add( - BELGIUM_MBUS3_METER_READING1, + MBUS_METER_READING, MBusObject( (0, 3), [ @@ -846,23 +833,23 @@ async def test_belgian_meter_alt( {"value": Decimal(12.12), "unit": "m3"}, ], ), - "BELGIUM_MBUS3_METER_READING1", + "MBUS_METER_READING", ) telegram.add( - BELGIUM_MBUS4_DEVICE_TYPE, + MBUS_DEVICE_TYPE, CosemObject((0, 4), [{"value": "003", "unit": ""}]), - "BELGIUM_MBUS4_DEVICE_TYPE", + "MBUS_DEVICE_TYPE", ) telegram.add( - BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER, + MBUS_EQUIPMENT_IDENTIFIER, CosemObject( (0, 4), [{"value": "37464C4F32313139303333373334", "unit": ""}], ), - "BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER", + "MBUS_EQUIPMENT_IDENTIFIER", ) telegram.add( - BELGIUM_MBUS4_METER_READING2, + MBUS_METER_READING, MBusObject( (0, 4), [ @@ -870,7 +857,7 @@ async def test_belgian_meter_alt( {"value": Decimal(13.13), "unit": "m3"}, ], ), - "BELGIUM_MBUS4_METER_READING2", + "MBUS_METER_READING", ) mock_entry = MockConfigEntry( @@ -970,46 +957,46 @@ async def test_belgian_meter_mbus( "ELECTRICITY_ACTIVE_TARIFF", ) telegram.add( - BELGIUM_MBUS1_DEVICE_TYPE, + MBUS_DEVICE_TYPE, CosemObject((0, 1), [{"value": "006", "unit": ""}]), - "BELGIUM_MBUS1_DEVICE_TYPE", + "MBUS_DEVICE_TYPE", ) telegram.add( - BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + MBUS_EQUIPMENT_IDENTIFIER, CosemObject( (0, 1), [{"value": "37464C4F32313139303333373331", "unit": ""}], ), - "BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER", + "MBUS_EQUIPMENT_IDENTIFIER", ) telegram.add( - BELGIUM_MBUS2_DEVICE_TYPE, + MBUS_DEVICE_TYPE, CosemObject((0, 2), [{"value": "003", "unit": ""}]), - "BELGIUM_MBUS2_DEVICE_TYPE", + "MBUS_DEVICE_TYPE", ) telegram.add( - BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, + MBUS_EQUIPMENT_IDENTIFIER, CosemObject( (0, 2), [{"value": "37464C4F32313139303333373332", "unit": ""}], ), - "BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER", + "MBUS_EQUIPMENT_IDENTIFIER", ) telegram.add( - BELGIUM_MBUS3_DEVICE_TYPE, + MBUS_DEVICE_TYPE, CosemObject((0, 3), [{"value": "007", "unit": ""}]), - "BELGIUM_MBUS3_DEVICE_TYPE", + "MBUS_DEVICE_TYPE", ) telegram.add( - BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, + MBUS_EQUIPMENT_IDENTIFIER, CosemObject( (0, 3), [{"value": "37464C4F32313139303333373333", "unit": ""}], ), - "BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER", + "MBUS_EQUIPMENT_IDENTIFIER", ) telegram.add( - BELGIUM_MBUS3_METER_READING2, + MBUS_METER_READING, MBusObject( (0, 3), [ @@ -1017,15 +1004,15 @@ async def test_belgian_meter_mbus( {"value": Decimal(12.12), "unit": "m3"}, ], ), - "BELGIUM_MBUS3_METER_READING2", + "MBUS_METER_READING", ) telegram.add( - BELGIUM_MBUS4_DEVICE_TYPE, + MBUS_DEVICE_TYPE, CosemObject((0, 4), [{"value": "007", "unit": ""}]), - "BELGIUM_MBUS4_DEVICE_TYPE", + "MBUS_DEVICE_TYPE", ) telegram.add( - BELGIUM_MBUS4_METER_READING1, + MBUS_METER_READING, MBusObject( (0, 4), [ @@ -1033,7 +1020,7 @@ async def test_belgian_meter_mbus( {"value": Decimal(13.13), "unit": "m3"}, ], ), - "BELGIUM_MBUS4_METER_READING1", + "MBUS_METER_READING", ) mock_entry = MockConfigEntry( @@ -1057,20 +1044,32 @@ async def test_belgian_meter_mbus( active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") assert active_tariff.state == "unknown" - # check if gas consumption mbus2 is parsed correctly + # check if gas consumption mbus1 is parsed correctly gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption is None - # check if water usage mbus3 is parsed correctly - water_consumption = hass.states.get("sensor.water_meter_water_consumption_2") - assert water_consumption is None - - # check if gas consumption mbus4 is parsed correctly + # check if gas consumption mbus2 is parsed correctly gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption_2") assert gas_consumption is None - # check if gas consumption mbus4 is parsed correctly + # check if water usage mbus3 is parsed correctly water_consumption = hass.states.get("sensor.water_meter_water_consumption") + assert water_consumption + assert water_consumption.state == "12.12" + assert ( + water_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER + ) + assert ( + water_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) + assert ( + water_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS + ) + + # check if gas consumption mbus4 is parsed correctly + water_consumption = hass.states.get("sensor.water_meter_water_consumption_2") assert water_consumption.state == "13.13" assert ( water_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER @@ -1526,3 +1525,7 @@ def test_all_obis_references_exists(): """Verify that all attributes exist by name in database.""" for sensor in SENSORS: assert hasattr(obis_references, sensor.obis_reference) + + for sensors in SENSORS_MBUS_DEVICE_TYPE.values(): + for sensor in sensors: + assert hasattr(obis_references, sensor.obis_reference) From 4a34855a921fd6d6d7519ff2ef721e3cb72f31e6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:57:42 +0200 Subject: [PATCH 1734/2411] Fix implicit-return in scripts (#122831) --- script/install_integration_requirements.py | 1 + 1 file changed, 1 insertion(+) diff --git a/script/install_integration_requirements.py b/script/install_integration_requirements.py index ab91ea71557..91c9f6a8ed0 100644 --- a/script/install_integration_requirements.py +++ b/script/install_integration_requirements.py @@ -45,6 +45,7 @@ def main() -> int | None: cmd, check=True, ) + return None if __name__ == "__main__": From 6840f27bc6217ea57c1617261a320c624427acbf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Jul 2024 17:12:58 +0200 Subject: [PATCH 1735/2411] Verify respx mock routes are cleaned up when tests finish (#122852) --- tests/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 0d0fd826b44..0667edf4be2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -34,6 +34,7 @@ import multidict import pytest import pytest_socket import requests_mock +import respx from syrupy.assertion import SnapshotAssertion from homeassistant import block_async_io @@ -398,6 +399,13 @@ def verify_cleanup( # Restore the default time zone to not break subsequent tests dt_util.DEFAULT_TIME_ZONE = datetime.UTC + try: + # Verify respx.mock has been cleaned up + assert not respx.mock.routes, "respx.mock routes not cleaned up, maybe the test needs to be decorated with @respx.mock" + finally: + # Clear mock routes not break subsequent tests + respx.mock.clear() + @pytest.fixture(autouse=True) def reset_hass_threading_local_object() -> Generator[None]: From b69b927795e4ede255b3f323e3594ce0a06acf6f Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Tue, 30 Jul 2024 17:17:20 +0200 Subject: [PATCH 1736/2411] Set parallel updates in devolo_home_network (#122847) --- homeassistant/components/devolo_home_network/binary_sensor.py | 2 ++ homeassistant/components/devolo_home_network/button.py | 2 ++ homeassistant/components/devolo_home_network/device_tracker.py | 2 ++ homeassistant/components/devolo_home_network/image.py | 2 ++ homeassistant/components/devolo_home_network/sensor.py | 2 ++ homeassistant/components/devolo_home_network/switch.py | 2 ++ homeassistant/components/devolo_home_network/update.py | 2 ++ 7 files changed, 14 insertions(+) diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py index 38d79951149..c96d0273a50 100644 --- a/homeassistant/components/devolo_home_network/binary_sensor.py +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -21,6 +21,8 @@ from . import DevoloHomeNetworkConfigEntry from .const import CONNECTED_PLC_DEVICES, CONNECTED_TO_ROUTER from .entity import DevoloCoordinatorEntity +PARALLEL_UPDATES = 1 + def _is_connected_to_router(entity: DevoloBinarySensorEntity) -> bool: """Check, if device is attached to the router.""" diff --git a/homeassistant/components/devolo_home_network/button.py b/homeassistant/components/devolo_home_network/button.py index 1f67912f020..ca17b572522 100644 --- a/homeassistant/components/devolo_home_network/button.py +++ b/homeassistant/components/devolo_home_network/button.py @@ -22,6 +22,8 @@ from . import DevoloHomeNetworkConfigEntry from .const import DOMAIN, IDENTIFY, PAIRING, RESTART, START_WPS from .entity import DevoloEntity +PARALLEL_UPDATES = 1 + @dataclass(frozen=True, kw_only=True) class DevoloButtonEntityDescription(ButtonEntityDescription): diff --git a/homeassistant/components/devolo_home_network/device_tracker.py b/homeassistant/components/devolo_home_network/device_tracker.py index 0a221779622..960069191ee 100644 --- a/homeassistant/components/devolo_home_network/device_tracker.py +++ b/homeassistant/components/devolo_home_network/device_tracker.py @@ -22,6 +22,8 @@ from homeassistant.helpers.update_coordinator import ( from . import DevoloHomeNetworkConfigEntry from .const import CONNECTED_WIFI_CLIENTS, DOMAIN, WIFI_APTYPE, WIFI_BANDS +PARALLEL_UPDATES = 1 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/devolo_home_network/image.py b/homeassistant/components/devolo_home_network/image.py index ee3b079da02..58052d3021e 100644 --- a/homeassistant/components/devolo_home_network/image.py +++ b/homeassistant/components/devolo_home_network/image.py @@ -20,6 +20,8 @@ from . import DevoloHomeNetworkConfigEntry from .const import IMAGE_GUEST_WIFI, SWITCH_GUEST_WIFI from .entity import DevoloCoordinatorEntity +PARALLEL_UPDATES = 1 + @dataclass(frozen=True, kw_only=True) class DevoloImageEntityDescription(ImageEntityDescription): diff --git a/homeassistant/components/devolo_home_network/sensor.py b/homeassistant/components/devolo_home_network/sensor.py index ffd40acf42a..2fd8ab9220c 100644 --- a/homeassistant/components/devolo_home_network/sensor.py +++ b/homeassistant/components/devolo_home_network/sensor.py @@ -31,6 +31,8 @@ from .const import ( ) from .entity import DevoloCoordinatorEntity +PARALLEL_UPDATES = 1 + _CoordinatorDataT = TypeVar( "_CoordinatorDataT", bound=LogicalNetwork | DataRate | list[ConnectedStationInfo] | list[NeighborAPInfo], diff --git a/homeassistant/components/devolo_home_network/switch.py b/homeassistant/components/devolo_home_network/switch.py index 3df67287f3b..c3400916d78 100644 --- a/homeassistant/components/devolo_home_network/switch.py +++ b/homeassistant/components/devolo_home_network/switch.py @@ -21,6 +21,8 @@ from . import DevoloHomeNetworkConfigEntry from .const import DOMAIN, SWITCH_GUEST_WIFI, SWITCH_LEDS from .entity import DevoloCoordinatorEntity +PARALLEL_UPDATES = 1 + _DataT = TypeVar("_DataT", bound=WifiGuestAccessGet | bool) diff --git a/homeassistant/components/devolo_home_network/update.py b/homeassistant/components/devolo_home_network/update.py index 92f5cb0f094..29c0c8762b9 100644 --- a/homeassistant/components/devolo_home_network/update.py +++ b/homeassistant/components/devolo_home_network/update.py @@ -26,6 +26,8 @@ from . import DevoloHomeNetworkConfigEntry from .const import DOMAIN, REGULAR_FIRMWARE from .entity import DevoloCoordinatorEntity +PARALLEL_UPDATES = 1 + @dataclass(frozen=True, kw_only=True) class DevoloUpdateEntityDescription(UpdateEntityDescription): From 1ffde403f020e3aafe21e1fe09e9ffa9bff3d5a2 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 30 Jul 2024 16:18:33 +0100 Subject: [PATCH 1737/2411] Ensure evohome leaves no lingering timers (#122860) --- homeassistant/components/evohome/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 2df4ae1be6b..5a5d9d09521 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -243,6 +243,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: update_interval=config[DOMAIN][CONF_SCAN_INTERVAL], update_method=broker.async_update, ) + await coordinator.async_register_shutdown() hass.data[DOMAIN] = {"broker": broker, "coordinator": coordinator} From 896cd27bea14fdb11d3bde57d7a2902567b85342 Mon Sep 17 00:00:00 2001 From: Kim de Vos Date: Tue, 30 Jul 2024 17:20:56 +0200 Subject: [PATCH 1738/2411] Add sensors for Unifi latency (#116737) * Add sensors for Unifi latency * Add needed guard and casting * Use new types * Add WAN2 support * Add literals * Make ids for WAN and WAN2 unique * Make methods general * Update sensor.py * add more typing * Simplify usage make_wan_latency_sensors * Simplify further * Move latency entity creation to method * Make method internal * simplify tests * Apply feedback * Reduce boiler copied code and add support function * Add external method for share logic between * Remove none * Add more tests * Apply feedback and change code to improve code coverage --- homeassistant/components/unifi/sensor.py | 93 +++++++++- tests/components/unifi/test_sensor.py | 224 +++++++++++++++++++++++ 2 files changed, 315 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index bb974864f60..d86b72d1b2f 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -11,6 +11,7 @@ from dataclasses import dataclass from datetime import date, datetime, timedelta from decimal import Decimal from functools import partial +from typing import TYPE_CHECKING, Literal from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients @@ -20,7 +21,7 @@ from aiounifi.interfaces.ports import Ports from aiounifi.interfaces.wlans import Wlans from aiounifi.models.api import ApiItemT from aiounifi.models.client import Client -from aiounifi.models.device import Device +from aiounifi.models.device import Device, TypedDeviceUptimeStatsWanMonitor from aiounifi.models.outlet import Outlet from aiounifi.models.port import Port from aiounifi.models.wlan import Wlan @@ -32,7 +33,13 @@ from homeassistant.components.sensor import ( SensorStateClass, UnitOfTemperature, ) -from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfDataRate, UnitOfPower +from homeassistant.const import ( + PERCENTAGE, + EntityCategory, + UnitOfDataRate, + UnitOfPower, + UnitOfTime, +) from homeassistant.core import Event as core_Event, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -192,6 +199,86 @@ def async_device_state_value_fn(hub: UnifiHub, device: Device) -> str: return DEVICE_STATES[device.state] +@callback +def async_device_wan_latency_supported_fn( + wan: Literal["WAN", "WAN2"], + monitor_target: str, + hub: UnifiHub, + obj_id: str, +) -> bool: + """Determine if an device have a latency monitor.""" + if (device := hub.api.devices[obj_id]) and device.uptime_stats: + return _device_wan_latency_monitor(wan, monitor_target, device) is not None + return False + + +@callback +def async_device_wan_latency_value_fn( + wan: Literal["WAN", "WAN2"], + monitor_target: str, + hub: UnifiHub, + device: Device, +) -> int | None: + """Retrieve the monitor target from WAN monitors.""" + target = _device_wan_latency_monitor(wan, monitor_target, device) + + if TYPE_CHECKING: + # Checked by async_device_wan_latency_supported_fn + assert target + + return target.get("latency_average", 0) + + +@callback +def _device_wan_latency_monitor( + wan: Literal["WAN", "WAN2"], monitor_target: str, device: Device +) -> TypedDeviceUptimeStatsWanMonitor | None: + """Return the target of the WAN latency monitor.""" + if device.uptime_stats and (uptime_stats_wan := device.uptime_stats.get(wan)): + for monitor in uptime_stats_wan["monitors"]: + if monitor_target in monitor["target"]: + return monitor + return None + + +def make_wan_latency_sensors() -> tuple[UnifiSensorEntityDescription, ...]: + """Create WAN latency sensors from WAN monitor data.""" + + def make_wan_latency_entity_description( + wan: Literal["WAN", "WAN2"], name: str, monitor_target: str + ) -> UnifiSensorEntityDescription: + return UnifiSensorEntityDescription[Devices, Device]( + key=f"{name} {wan} latency", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfTime.MILLISECONDS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DURATION, + entity_registry_enabled_default=False, + api_handler_fn=lambda api: api.devices, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + name_fn=lambda _: f"{name} {wan} latency", + object_fn=lambda api, obj_id: api.devices[obj_id], + supported_fn=partial( + async_device_wan_latency_supported_fn, wan, monitor_target + ), + unique_id_fn=lambda hub, + obj_id: f"{name.lower}_{wan.lower}_latency-{obj_id}", + value_fn=partial(async_device_wan_latency_value_fn, wan, monitor_target), + ) + + wans: tuple[Literal["WAN"], Literal["WAN2"]] = ("WAN", "WAN2") + return tuple( + make_wan_latency_entity_description(wan, name, target) + for wan in wans + for name, target in ( + ("Microsoft", "microsoft"), + ("Google", "google"), + ("Cloudflare", "1.1.1.1"), + ) + ) + + @dataclass(frozen=True, kw_only=True) class UnifiSensorEntityDescription( SensorEntityDescription, UnifiEntityDescription[HandlerT, ApiItemT] @@ -456,6 +543,8 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( ), ) +ENTITY_DESCRIPTIONS += make_wan_latency_sensors() + async def async_setup_entry( hass: HomeAssistant, diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index e1893922f60..779df6660f0 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1424,3 +1424,227 @@ async def test_device_uptime( entity_registry.async_get("sensor.device_uptime").entity_category is EntityCategory.DIAGNOSTIC ) + + +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 2, + "device_id": "mock-id", + "ip": "10.0.1.1", + "mac": "10:00:00:00:01:01", + "last_seen": 1562600145, + "model": "US16P150", + "name": "mock-name", + "port_overrides": [], + "uptime_stats": { + "WAN": { + "availability": 100.0, + "latency_average": 39, + "monitors": [ + { + "availability": 100.0, + "latency_average": 56, + "target": "www.microsoft.com", + "type": "icmp", + }, + { + "availability": 100.0, + "latency_average": 53, + "target": "google.com", + "type": "icmp", + }, + { + "availability": 100.0, + "latency_average": 30, + "target": "1.1.1.1", + "type": "icmp", + }, + ], + }, + "WAN2": { + "monitors": [ + { + "availability": 0.0, + "target": "www.microsoft.com", + "type": "icmp", + }, + { + "availability": 0.0, + "target": "google.com", + "type": "icmp", + }, + {"availability": 0.0, "target": "1.1.1.1", "type": "icmp"}, + ], + }, + }, + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + } + ] + ], +) +@pytest.mark.parametrize( + ("entity_id", "state", "updated_state", "index_to_update"), + [ + # Microsoft + ("microsoft_wan", "56", "20", 0), + # Google + ("google_wan", "53", "90", 1), + # Cloudflare + ("cloudflare_wan", "30", "80", 2), + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_wan_monitor_latency( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_websocket_message, + device_payload: list[dict[str, Any]], + entity_id: str, + state: str, + updated_state: str, + index_to_update: int, +) -> None: + """Verify that wan latency sensors are working as expected.""" + + assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + + latency_entry = entity_registry.async_get(f"sensor.mock_name_{entity_id}_latency") + assert latency_entry.disabled_by == RegistryEntryDisabler.INTEGRATION + assert latency_entry.entity_category is EntityCategory.DIAGNOSTIC + + # Enable entity + entity_registry.async_update_entity( + entity_id=f"sensor.mock_name_{entity_id}_latency", disabled_by=None + ) + + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 7 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 + + # Verify sensor attributes and state + latency_entry = hass.states.get(f"sensor.mock_name_{entity_id}_latency") + assert latency_entry.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DURATION + assert ( + latency_entry.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + ) + assert latency_entry.state == state + + # Verify state update + device = device_payload[0] + device["uptime_stats"]["WAN"]["monitors"][index_to_update]["latency_average"] = ( + updated_state + ) + + mock_websocket_message(message=MessageKey.DEVICE, data=device) + + assert ( + hass.states.get(f"sensor.mock_name_{entity_id}_latency").state == updated_state + ) + + +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 2, + "device_id": "mock-id", + "ip": "10.0.1.1", + "mac": "10:00:00:00:01:01", + "last_seen": 1562600145, + "model": "US16P150", + "name": "mock-name", + "port_overrides": [], + "uptime_stats": { + "WAN": { + "monitors": [ + { + "availability": 100.0, + "latency_average": 30, + "target": "1.2.3.4", + "type": "icmp", + }, + ], + }, + "WAN2": { + "monitors": [ + { + "availability": 0.0, + "target": "www.microsoft.com", + "type": "icmp", + }, + { + "availability": 0.0, + "target": "google.com", + "type": "icmp", + }, + {"availability": 0.0, "target": "1.1.1.1", "type": "icmp"}, + ], + }, + }, + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + } + ] + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_wan_monitor_latency_with_no_entries( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Verify that wan latency sensors is not created if there is no data.""" + + assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + + latency_entry = entity_registry.async_get("sensor.mock_name_google_wan_latency") + assert latency_entry is None + + +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 2, + "device_id": "mock-id", + "ip": "10.0.1.1", + "mac": "10:00:00:00:01:01", + "last_seen": 1562600145, + "model": "US16P150", + "name": "mock-name", + "port_overrides": [], + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + } + ] + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_wan_monitor_latency_with_no_uptime( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Verify that wan latency sensors is not created if there is no data.""" + + assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + + latency_entry = entity_registry.async_get("sensor.mock_name_google_wan_latency") + assert latency_entry is None From 8066c7dec60e7164001bfff952d68517d1477c65 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:21:45 +0200 Subject: [PATCH 1739/2411] Fix implicit-return in deconz (#122836) --- homeassistant/components/deconz/fan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index dc65756eeeb..67c759afeda 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -95,7 +95,8 @@ class DeconzFan(DeconzDevice[Light], FanEntity): 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.async_turn_off() + return await self.hub.api.lights.lights.set_state( id=self._device.resource_id, fan_speed=percentage_to_ordered_list_item( From be24475cee71d3369d3975da12afd3709d47056b Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Tue, 30 Jul 2024 18:24:03 +0300 Subject: [PATCH 1740/2411] Update selector converters for llm script tools (#122830) --- homeassistant/helpers/llm.py | 4 +- tests/helpers/test_llm.py | 73 ++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 4c8e2df06a4..8ad576b7ea5 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -521,7 +521,7 @@ def _selector_serializer(schema: Any) -> Any: # noqa: C901 return convert(cv.CONDITIONS_SCHEMA) if isinstance(schema, selector.ConstantSelector): - return {"enum": [schema.config["value"]]} + return convert(vol.Schema(schema.config["value"])) result: dict[str, Any] if isinstance(schema, selector.ColorTempSelector): @@ -573,7 +573,7 @@ def _selector_serializer(schema: Any) -> Any: # noqa: C901 return result if isinstance(schema, selector.ObjectSelector): - return {"type": "object"} + return {"type": "object", "additionalProperties": True} if isinstance(schema, selector.SelectSelector): options = [ diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 3ad5b23b731..ea6e628d1d4 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -842,13 +842,22 @@ async def test_selector_serializer( assert selector_serializer( selector.ColorTempSelector({"min_mireds": 100, "max_mireds": 1000}) ) == {"type": "number", "minimum": 100, "maximum": 1000} + assert selector_serializer(selector.ConditionSelector()) == { + "type": "array", + "items": {"nullable": True, "type": "string"}, + } assert selector_serializer(selector.ConfigEntrySelector()) == {"type": "string"} assert selector_serializer(selector.ConstantSelector({"value": "test"})) == { - "enum": ["test"] + "type": "string", + "enum": ["test"], + } + assert selector_serializer(selector.ConstantSelector({"value": 1})) == { + "type": "integer", + "enum": [1], } - assert selector_serializer(selector.ConstantSelector({"value": 1})) == {"enum": [1]} assert selector_serializer(selector.ConstantSelector({"value": True})) == { - "enum": [True] + "type": "boolean", + "enum": [True], } assert selector_serializer(selector.QrCodeSelector({"data": "test"})) == { "type": "string" @@ -876,6 +885,17 @@ async def test_selector_serializer( "type": "array", "items": {"type": "string"}, } + assert selector_serializer(selector.DurationSelector()) == { + "type": "object", + "properties": { + "days": {"type": "number"}, + "hours": {"type": "number"}, + "minutes": {"type": "number"}, + "seconds": {"type": "number"}, + "milliseconds": {"type": "number"}, + }, + "required": [], + } assert selector_serializer(selector.EntitySelector()) == { "type": "string", "format": "entity_id", @@ -929,7 +949,10 @@ async def test_selector_serializer( "minimum": 30, "maximum": 100, } - assert selector_serializer(selector.ObjectSelector()) == {"type": "object"} + assert selector_serializer(selector.ObjectSelector()) == { + "type": "object", + "additionalProperties": True, + } assert selector_serializer( selector.SelectSelector( { @@ -951,6 +974,48 @@ async def test_selector_serializer( assert selector_serializer( selector.StateSelector({"entity_id": "sensor.test"}) ) == {"type": "string"} + target_schema = selector_serializer(selector.TargetSelector()) + target_schema["properties"]["entity_id"]["anyOf"][0][ + "enum" + ].sort() # Order is not deterministic + assert target_schema == { + "type": "object", + "properties": { + "area_id": { + "anyOf": [ + {"type": "string", "enum": ["none"]}, + {"type": "array", "items": {"type": "string", "nullable": True}}, + ] + }, + "device_id": { + "anyOf": [ + {"type": "string", "enum": ["none"]}, + {"type": "array", "items": {"type": "string", "nullable": True}}, + ] + }, + "entity_id": { + "anyOf": [ + {"type": "string", "enum": ["all", "none"], "format": "lower"}, + {"type": "string", "nullable": True}, + {"type": "array", "items": {"type": "string"}}, + ] + }, + "floor_id": { + "anyOf": [ + {"type": "string", "enum": ["none"]}, + {"type": "array", "items": {"type": "string", "nullable": True}}, + ] + }, + "label_id": { + "anyOf": [ + {"type": "string", "enum": ["none"]}, + {"type": "array", "items": {"type": "string", "nullable": True}}, + ] + }, + }, + "required": [], + } + assert selector_serializer(selector.TemplateSelector()) == { "type": "string", "format": "jinja2", From 18a7d15d14bea31f340611a86c096f143cce0fd9 Mon Sep 17 00:00:00 2001 From: bdowden Date: Tue, 30 Jul 2024 11:26:08 -0400 Subject: [PATCH 1741/2411] Add Traffic Rule switches to UniFi Network (#118821) * Add Traffic Rule switches to UniFi Network * Retrieve Fix unifi traffic rule switches Poll for traffic rule updates; have immediate feedback in the UI for modifying traffic rules * Remove default values for unifi entity; Remove unnecessary code * Begin updating traffic rule unit tests * For the mock get request, allow for meta and data properties to not be appended to support v2 api requests Fix traffic rule unit tests; * inspect path to determine json response instead of passing an argument * Remove entity id parameter from tests; remove unused code; rename traffic rule unique ID prefix * Remove parameter with default. * More code removal; * Rename copy/paste variable; remove commented code; remove duplicate default code --------- Co-authored-by: ViViDboarder --- .../components/unifi/hub/entity_loader.py | 34 ++++++- homeassistant/components/unifi/switch.py | 31 ++++++- tests/components/unifi/conftest.py | 18 +++- tests/components/unifi/test_switch.py | 89 +++++++++++++++++++ 4 files changed, 164 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/unifi/hub/entity_loader.py b/homeassistant/components/unifi/hub/entity_loader.py index 29448a4114a..f11ddefec98 100644 --- a/homeassistant/components/unifi/hub/entity_loader.py +++ b/homeassistant/components/unifi/hub/entity_loader.py @@ -7,9 +7,10 @@ Make sure expected clients are available for platforms. from __future__ import annotations import asyncio +from collections.abc import Callable, Coroutine, Sequence from datetime import timedelta from functools import partial -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from aiounifi.interfaces.api_handlers import ItemEvent @@ -18,6 +19,7 @@ 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_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from ..const import LOGGER, UNIFI_WIRELESS_CLIENTS from ..entity import UnifiEntity, UnifiEntityDescription @@ -26,6 +28,7 @@ if TYPE_CHECKING: from .hub import UnifiHub CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) +POLL_INTERVAL = timedelta(seconds=10) class UnifiEntityLoader: @@ -43,10 +46,24 @@ class UnifiEntityLoader: hub.api.port_forwarding.update, hub.api.sites.update, hub.api.system_information.update, + hub.api.traffic_rules.update, hub.api.wlans.update, ) + self.polling_api_updaters = (hub.api.traffic_rules.update,) self.wireless_clients = hub.hass.data[UNIFI_WIRELESS_CLIENTS] + self._dataUpdateCoordinator = DataUpdateCoordinator( + hub.hass, + LOGGER, + name="Unifi entity poller", + update_method=self._update_pollable_api_data, + update_interval=POLL_INTERVAL, + ) + + self._update_listener = self._dataUpdateCoordinator.async_add_listener( + update_callback=lambda: None + ) + self.platforms: list[ tuple[ AddEntitiesCallback, @@ -65,16 +82,25 @@ class UnifiEntityLoader: self._restore_inactive_clients() self.wireless_clients.update_clients(set(self.hub.api.clients.values())) - async def _refresh_api_data(self) -> None: - """Refresh API data from network application.""" + async def _refresh_data( + self, updaters: Sequence[Callable[[], Coroutine[Any, Any, None]]] + ) -> None: results = await asyncio.gather( - *[update() for update in self.api_updaters], + *[update() for update in updaters], return_exceptions=True, ) for result in results: if result is not None: LOGGER.warning("Exception on update %s", result) + async def _update_pollable_api_data(self) -> None: + """Refresh API data for pollable updaters.""" + await self._refresh_data(self.polling_api_updaters) + + async def _refresh_api_data(self) -> None: + """Refresh API data from network application.""" + await self._refresh_data(self.api_updaters) + @callback def _restore_inactive_clients(self) -> None: """Restore inactive clients. diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index ef30abb9349..93a0c81a24e 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -20,6 +20,7 @@ from aiounifi.interfaces.dpi_restriction_groups import DPIRestrictionGroups from aiounifi.interfaces.outlets import Outlets from aiounifi.interfaces.port_forwarding import PortForwarding from aiounifi.interfaces.ports import Ports +from aiounifi.interfaces.traffic_rules import TrafficRules from aiounifi.interfaces.wlans import Wlans from aiounifi.models.api import ApiItemT from aiounifi.models.client import Client, ClientBlockRequest @@ -30,6 +31,7 @@ from aiounifi.models.event import Event, EventKey from aiounifi.models.outlet import Outlet from aiounifi.models.port import Port from aiounifi.models.port_forward import PortForward, PortForwardEnableRequest +from aiounifi.models.traffic_rule import TrafficRule, TrafficRuleEnableRequest from aiounifi.models.wlan import Wlan, WlanEnableRequest from homeassistant.components.switch import ( @@ -94,8 +96,8 @@ def async_dpi_group_device_info_fn(hub: UnifiHub, obj_id: str) -> DeviceInfo: @callback -def async_port_forward_device_info_fn(hub: UnifiHub, obj_id: str) -> DeviceInfo: - """Create device registry entry for port forward.""" +def async_unifi_network_device_info_fn(hub: UnifiHub, obj_id: str) -> DeviceInfo: + """Create device registry entry for the UniFi Network application.""" unique_id = hub.config.entry.unique_id assert unique_id is not None return DeviceInfo( @@ -158,6 +160,16 @@ async def async_port_forward_control_fn( await hub.api.request(PortForwardEnableRequest.create(port_forward, target)) +async def async_traffic_rule_control_fn( + hub: UnifiHub, obj_id: str, target: bool +) -> None: + """Control traffic rule state.""" + traffic_rule = hub.api.traffic_rules[obj_id].raw + await hub.api.request(TrafficRuleEnableRequest.create(traffic_rule, target)) + # Update the traffic rules so the UI is updated appropriately + await hub.api.traffic_rules.update() + + async def async_wlan_control_fn(hub: UnifiHub, obj_id: str, target: bool) -> None: """Control outlet relay.""" await hub.api.request(WlanEnableRequest.create(obj_id, target)) @@ -232,12 +244,25 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSwitchEntityDescription, ...] = ( icon="mdi:upload-network", api_handler_fn=lambda api: api.port_forwarding, control_fn=async_port_forward_control_fn, - device_info_fn=async_port_forward_device_info_fn, + device_info_fn=async_unifi_network_device_info_fn, is_on_fn=lambda hub, port_forward: port_forward.enabled, name_fn=lambda port_forward: f"{port_forward.name}", object_fn=lambda api, obj_id: api.port_forwarding[obj_id], unique_id_fn=lambda hub, obj_id: f"port_forward-{obj_id}", ), + UnifiSwitchEntityDescription[TrafficRules, TrafficRule]( + key="Traffic rule control", + device_class=SwitchDeviceClass.SWITCH, + entity_category=EntityCategory.CONFIG, + icon="mdi:security-network", + api_handler_fn=lambda api: api.traffic_rules, + control_fn=async_traffic_rule_control_fn, + device_info_fn=async_unifi_network_device_info_fn, + is_on_fn=lambda hub, traffic_rule: traffic_rule.enabled, + name_fn=lambda traffic_rule: traffic_rule.description, + object_fn=lambda api, obj_id: api.traffic_rules[obj_id], + unique_id_fn=lambda hub, obj_id: f"traffic_rule-{obj_id}", + ), UnifiSwitchEntityDescription[Ports, Port]( key="PoE port control", device_class=SwitchDeviceClass.OUTLET, diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index c20b8766bfc..4e460bab8f8 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -160,6 +160,7 @@ def fixture_request( dpi_app_payload: list[dict[str, Any]], dpi_group_payload: list[dict[str, Any]], port_forward_payload: list[dict[str, Any]], + traffic_rule_payload: list[dict[str, Any]], site_payload: list[dict[str, Any]], system_information_payload: list[dict[str, Any]], wlan_payload: list[dict[str, Any]], @@ -170,9 +171,16 @@ def fixture_request( url = f"https://{host}:{DEFAULT_PORT}" def mock_get_request(path: str, payload: list[dict[str, Any]]) -> None: + # APIV2 request respoonses have `meta` and `data` automatically appended + json = {} + if path.startswith("/v2"): + json = payload + else: + json = {"meta": {"rc": "OK"}, "data": payload} + aioclient_mock.get( f"{url}{path}", - json={"meta": {"rc": "OK"}, "data": payload}, + json=json, headers={"content-type": CONTENT_TYPE_JSON}, ) @@ -182,6 +190,7 @@ def fixture_request( json={"data": "login successful", "meta": {"rc": "ok"}}, headers={"content-type": CONTENT_TYPE_JSON}, ) + mock_get_request("/api/self/sites", site_payload) mock_get_request(f"/api/s/{site_id}/stat/sta", client_payload) mock_get_request(f"/api/s/{site_id}/rest/user", clients_all_payload) @@ -191,6 +200,7 @@ def fixture_request( mock_get_request(f"/api/s/{site_id}/rest/portforward", port_forward_payload) mock_get_request(f"/api/s/{site_id}/stat/sysinfo", system_information_payload) mock_get_request(f"/api/s/{site_id}/rest/wlanconf", wlan_payload) + mock_get_request(f"/v2/api/site/{site_id}/trafficrules", traffic_rule_payload) return __mock_requests @@ -262,6 +272,12 @@ def fixture_system_information_data() -> list[dict[str, Any]]: ] +@pytest.fixture(name="traffic_rule_payload") +def traffic_rule_payload_data() -> list[dict[str, Any]]: + """Traffic rule data.""" + return [] + + @pytest.fixture(name="wlan_payload") def fixture_wlan_data() -> list[dict[str, Any]]: """WLAN data.""" diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index b0ae8bde445..daf64301c8e 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -774,6 +774,37 @@ PORT_FORWARD_PLEX = { "src": "any", } +TRAFFIC_RULE = { + "_id": "6452cd9b859d5b11aa002ea1", + "action": "BLOCK", + "app_category_ids": [], + "app_ids": [], + "bandwidth_limit": { + "download_limit_kbps": 1024, + "enabled": False, + "upload_limit_kbps": 1024, + }, + "description": "Test Traffic Rule", + "name": "Test Traffic Rule", + "domains": [], + "enabled": True, + "ip_addresses": [], + "ip_ranges": [], + "matching_target": "INTERNET", + "network_ids": [], + "regions": [], + "schedule": { + "date_end": "2023-05-10", + "date_start": "2023-05-03", + "mode": "ALWAYS", + "repeat_on_days": [], + "time_all_day": False, + "time_range_end": "12:00", + "time_range_start": "09:00", + }, + "target_devices": [{"client_mac": CLIENT_1["mac"], "type": "CLIENT"}], +} + @pytest.mark.parametrize("client_payload", [[CONTROLLER_HOST]]) @pytest.mark.parametrize("device_payload", [[DEVICE_1]]) @@ -1072,6 +1103,64 @@ async def test_dpi_switches_add_second_app( assert hass.states.get("switch.block_media_streaming").state == STATE_ON +@pytest.mark.parametrize(("traffic_rule_payload"), [([TRAFFIC_RULE])]) +@pytest.mark.usefixtures("config_entry_setup") +async def test_traffic_rules( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_websocket_message, + config_entry_setup: ConfigEntry, + traffic_rule_payload: list[dict[str, Any]], +) -> None: + """Test control of UniFi traffic rules.""" + + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 + + # Validate state object + switch_1 = hass.states.get("switch.unifi_network_test_traffic_rule") + assert switch_1.state == STATE_ON + assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.SWITCH + + traffic_rule = deepcopy(traffic_rule_payload[0]) + + # Disable traffic rule + aioclient_mock.put( + f"https://{config_entry_setup.data[CONF_HOST]}:1234" + f"/v2/api/site/{config_entry_setup.data[CONF_SITE_ID]}/trafficrules/{traffic_rule['_id']}", + ) + + call_count = aioclient_mock.call_count + + await hass.services.async_call( + SWITCH_DOMAIN, + "turn_off", + {"entity_id": "switch.unifi_network_test_traffic_rule"}, + blocking=True, + ) + # Updating the value for traffic rules will make another call to retrieve the values + assert aioclient_mock.call_count == call_count + 2 + expected_disable_call = deepcopy(traffic_rule) + expected_disable_call["enabled"] = False + + assert aioclient_mock.mock_calls[call_count][2] == expected_disable_call + + call_count = aioclient_mock.call_count + + # Enable traffic rule + await hass.services.async_call( + SWITCH_DOMAIN, + "turn_on", + {"entity_id": "switch.unifi_network_test_traffic_rule"}, + blocking=True, + ) + + expected_enable_call = deepcopy(traffic_rule) + expected_enable_call["enabled"] = True + + assert aioclient_mock.call_count == call_count + 2 + assert aioclient_mock.mock_calls[call_count][2] == expected_enable_call + + @pytest.mark.parametrize( ("device_payload", "entity_id", "outlet_index", "expected_switches"), [ From ea727546d645fd55b4f722c53c643fba1f1730bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B6rrle?= <7945681+CM000n@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:11:08 +0200 Subject: [PATCH 1742/2411] Add apsystems power switch (#122447) * bring back power switch * fix pylint issues * add SWITCH to platform list * improve run_on and turn_off functions * ruff formatting * replace _state with _attr_is_on * Update homeassistant/components/apsystems/switch.py Co-authored-by: Joost Lekkerkerker * remove unused dependencies * Update homeassistant/components/apsystems/switch.py Co-authored-by: Joost Lekkerkerker * use async functions from api * convert Api IntEnum Status Information to bool * add translation key * implement async_update again * replace finally with else * better handling of bool value * Update homeassistant/components/apsystems/switch.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/apsystems/switch.py Co-authored-by: Joost Lekkerkerker * rename power switch to inverter switch * add test_number and test_switch module * remove test_number * Add mock entry for get_device_power_status * Add mock entry for get_device_power_status * Update test snapshots --------- Co-authored-by: Joost Lekkerkerker --- .../components/apsystems/__init__.py | 2 +- .../components/apsystems/strings.json | 45 +++++++++++---- homeassistant/components/apsystems/switch.py | 56 +++++++++++++++++++ tests/components/apsystems/conftest.py | 3 +- .../apsystems/snapshots/test_switch.ambr | 48 ++++++++++++++++ tests/components/apsystems/test_switch.py | 31 ++++++++++ 6 files changed, 173 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/apsystems/switch.py create mode 100644 tests/components/apsystems/snapshots/test_switch.ambr create mode 100644 tests/components/apsystems/test_switch.py diff --git a/homeassistant/components/apsystems/__init__.py b/homeassistant/components/apsystems/__init__.py index 40e62a32475..91650201a87 100644 --- a/homeassistant/components/apsystems/__init__.py +++ b/homeassistant/components/apsystems/__init__.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant from .const import DEFAULT_PORT from .coordinator import ApSystemsDataCoordinator -PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] @dataclass diff --git a/homeassistant/components/apsystems/strings.json b/homeassistant/components/apsystems/strings.json index 95499e96b4d..18200f7b49d 100644 --- a/homeassistant/components/apsystems/strings.json +++ b/homeassistant/components/apsystems/strings.json @@ -20,18 +20,43 @@ }, "entity": { "sensor": { - "total_power": { "name": "Total power" }, - "total_power_p1": { "name": "Power of P1" }, - "total_power_p2": { "name": "Power of P2" }, - "lifetime_production": { "name": "Total lifetime production" }, - "lifetime_production_p1": { "name": "Lifetime production of P1" }, - "lifetime_production_p2": { "name": "Lifetime production of P2" }, - "today_production": { "name": "Production of today" }, - "today_production_p1": { "name": "Production of today from P1" }, - "today_production_p2": { "name": "Production of today from P2" } + "total_power": { + "name": "Total power" + }, + "total_power_p1": { + "name": "Power of P1" + }, + "total_power_p2": { + "name": "Power of P2" + }, + "lifetime_production": { + "name": "Total lifetime production" + }, + "lifetime_production_p1": { + "name": "Lifetime production of P1" + }, + "lifetime_production_p2": { + "name": "Lifetime production of P2" + }, + "today_production": { + "name": "Production of today" + }, + "today_production_p1": { + "name": "Production of today from P1" + }, + "today_production_p2": { + "name": "Production of today from P2" + } }, "number": { - "max_output": { "name": "Max output" } + "max_output": { + "name": "Max output" + } + }, + "switch": { + "inverter_status": { + "name": "Inverter status" + } } } } diff --git a/homeassistant/components/apsystems/switch.py b/homeassistant/components/apsystems/switch.py new file mode 100644 index 00000000000..405adc94b27 --- /dev/null +++ b/homeassistant/components/apsystems/switch.py @@ -0,0 +1,56 @@ +"""The power switch which can be toggled via the APsystems local API integration.""" + +from __future__ import annotations + +from typing import Any + +from aiohttp.client_exceptions import ClientConnectionError +from APsystemsEZ1 import Status + +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ApSystemsConfigEntry, ApSystemsData +from .entity import ApSystemsEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ApSystemsConfigEntry, + add_entities: AddEntitiesCallback, +) -> None: + """Set up the switch platform.""" + + add_entities([ApSystemsInverterSwitch(config_entry.runtime_data)], True) + + +class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity): + """The switch class for APSystems switches.""" + + _attr_device_class = SwitchDeviceClass.SWITCH + _attr_translation_key = "inverter_status" + + def __init__(self, data: ApSystemsData) -> None: + """Initialize the switch.""" + super().__init__(data) + self._api = data.coordinator.api + self._attr_unique_id = f"{data.device_id}_inverter_status" + + async def async_update(self) -> None: + """Update switch status and availability.""" + try: + status = await self._api.get_device_power_status() + except (TimeoutError, ClientConnectionError): + self._attr_available = False + else: + self._attr_available = True + self._attr_is_on = status == Status.normal + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self._api.set_device_power_status(0) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self._api.set_device_power_status(1) diff --git a/tests/components/apsystems/conftest.py b/tests/components/apsystems/conftest.py index 682086be380..c191c7ca2dc 100644 --- a/tests/components/apsystems/conftest.py +++ b/tests/components/apsystems/conftest.py @@ -3,7 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch -from APsystemsEZ1 import ReturnDeviceInfo, ReturnOutputData +from APsystemsEZ1 import ReturnDeviceInfo, ReturnOutputData, Status import pytest from homeassistant.components.apsystems.const import DOMAIN @@ -52,6 +52,7 @@ def mock_apsystems() -> Generator[MagicMock]: e2=6.0, te2=7.0, ) + mock_api.get_device_power_status.return_value = Status.normal yield mock_api diff --git a/tests/components/apsystems/snapshots/test_switch.ambr b/tests/components/apsystems/snapshots/test_switch.ambr new file mode 100644 index 00000000000..6daa9fd6e14 --- /dev/null +++ b/tests/components/apsystems/snapshots/test_switch.ambr @@ -0,0 +1,48 @@ +# serializer version: 1 +# name: test_all_entities[switch.mock_title_inverter_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_inverter_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Inverter status', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'inverter_status', + 'unique_id': 'MY_SERIAL_NUMBER_inverter_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.mock_title_inverter_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Mock Title Inverter status', + }), + 'context': , + 'entity_id': 'switch.mock_title_inverter_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/apsystems/test_switch.py b/tests/components/apsystems/test_switch.py new file mode 100644 index 00000000000..afd889fe958 --- /dev/null +++ b/tests/components/apsystems/test_switch.py @@ -0,0 +1,31 @@ +"""Test the APSystem switch module.""" + +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_apsystems: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.apsystems.PLATFORMS", + [Platform.SWITCH], + ): + await setup_integration(hass, mock_config_entry) + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) From 50b35ac4bce293d55525adaae9b5df01b9157762 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Tue, 30 Jul 2024 18:14:01 +0200 Subject: [PATCH 1743/2411] Add number platform to IronOS integration (#122801) * Add setpoint temperature number entity to IronOS integration * Add tests for number platform * Initialize settings in coordinator * Remove unused code --- homeassistant/components/iron_os/__init__.py | 2 +- homeassistant/components/iron_os/const.py | 3 + homeassistant/components/iron_os/icons.json | 8 +- homeassistant/components/iron_os/number.py | 96 ++++++++++++++++ homeassistant/components/iron_os/strings.json | 8 ++ .../iron_os/snapshots/test_number.ambr | 58 ++++++++++ tests/components/iron_os/test_number.py | 104 ++++++++++++++++++ 7 files changed, 275 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/iron_os/number.py create mode 100644 tests/components/iron_os/snapshots/test_number.ambr create mode 100644 tests/components/iron_os/test_number.py diff --git a/homeassistant/components/iron_os/__init__.py b/homeassistant/components/iron_os/__init__.py index bf3c6c34c83..11d99a1558a 100644 --- a/homeassistant/components/iron_os/__init__.py +++ b/homeassistant/components/iron_os/__init__.py @@ -16,7 +16,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN from .coordinator import IronOSCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR] type IronOSConfigEntry = ConfigEntry[IronOSCoordinator] diff --git a/homeassistant/components/iron_os/const.py b/homeassistant/components/iron_os/const.py index 86b7d401f4f..34889636808 100644 --- a/homeassistant/components/iron_os/const.py +++ b/homeassistant/components/iron_os/const.py @@ -8,3 +8,6 @@ MODEL = "Pinecil V2" OHM = "Ω" DISCOVERY_SVC_UUID = "9eae1000-9d0d-48c5-aa55-33e27f9bc533" + +MAX_TEMP: int = 450 +MIN_TEMP: int = 10 diff --git a/homeassistant/components/iron_os/icons.json b/homeassistant/components/iron_os/icons.json index 0d207607a4f..fa14b8134d0 100644 --- a/homeassistant/components/iron_os/icons.json +++ b/homeassistant/components/iron_os/icons.json @@ -1,12 +1,14 @@ { "entity": { + "number": { + "setpoint_temperature": { + "default": "mdi:thermometer" + } + }, "sensor": { "live_temperature": { "default": "mdi:soldering-iron" }, - "setpoint_temperature": { - "default": "mdi:thermostat" - }, "voltage": { "default": "mdi:current-dc" }, diff --git a/homeassistant/components/iron_os/number.py b/homeassistant/components/iron_os/number.py new file mode 100644 index 00000000000..9230faec1f1 --- /dev/null +++ b/homeassistant/components/iron_os/number.py @@ -0,0 +1,96 @@ +"""Number platform for IronOS integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from enum import StrEnum + +from pynecil import CharSetting, CommunicationError, LiveDataResponse + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.const import UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import IronOSConfigEntry +from .const import DOMAIN, MAX_TEMP, MIN_TEMP +from .entity import IronOSBaseEntity + + +@dataclass(frozen=True, kw_only=True) +class IronOSNumberEntityDescription(NumberEntityDescription): + """Describes IronOS number entity.""" + + value_fn: Callable[[LiveDataResponse], float | int | None] + max_value_fn: Callable[[LiveDataResponse], float | int] + set_key: CharSetting + + +class PinecilNumber(StrEnum): + """Number controls for Pinecil device.""" + + SETPOINT_TEMP = "setpoint_temperature" + + +PINECIL_NUMBER_DESCRIPTIONS: tuple[IronOSNumberEntityDescription, ...] = ( + IronOSNumberEntityDescription( + key=PinecilNumber.SETPOINT_TEMP, + translation_key=PinecilNumber.SETPOINT_TEMP, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=NumberDeviceClass.TEMPERATURE, + value_fn=lambda data: data.setpoint_temp, + set_key=CharSetting.SETPOINT_TEMP, + mode=NumberMode.BOX, + native_min_value=MIN_TEMP, + native_step=5, + max_value_fn=lambda data: min(data.max_tip_temp_ability or MAX_TEMP, MAX_TEMP), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: IronOSConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up number entities from a config entry.""" + coordinator = entry.runtime_data + + async_add_entities( + IronOSNumberEntity(coordinator, description) + for description in PINECIL_NUMBER_DESCRIPTIONS + ) + + +class IronOSNumberEntity(IronOSBaseEntity, NumberEntity): + """Implementation of a IronOS number entity.""" + + entity_description: IronOSNumberEntityDescription + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + try: + await self.coordinator.device.write(self.entity_description.set_key, value) + except CommunicationError as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="submit_setting_failed", + ) from e + self.async_write_ha_state() + + @property + def native_value(self) -> float | int | None: + """Return sensor state.""" + return self.entity_description.value_fn(self.coordinator.data) + + @property + def native_max_value(self) -> float: + """Return sensor state.""" + return self.entity_description.max_value_fn(self.coordinator.data) diff --git a/homeassistant/components/iron_os/strings.json b/homeassistant/components/iron_os/strings.json index cb95330b768..75584fe191c 100644 --- a/homeassistant/components/iron_os/strings.json +++ b/homeassistant/components/iron_os/strings.json @@ -17,6 +17,11 @@ } }, "entity": { + "number": { + "setpoint_temperature": { + "name": "Setpoint temperature" + } + }, "sensor": { "live_temperature": { "name": "Tip temperature" @@ -79,6 +84,9 @@ }, "setup_device_connection_error_exception": { "message": "Connection to device {name} failed, try again later" + }, + "submit_setting_failed": { + "message": "Failed to submit setting to device, try again later" } } } diff --git a/tests/components/iron_os/snapshots/test_number.ambr b/tests/components/iron_os/snapshots/test_number.ambr new file mode 100644 index 00000000000..2f5ee62e37e --- /dev/null +++ b/tests/components/iron_os/snapshots/test_number.ambr @@ -0,0 +1,58 @@ +# serializer version: 1 +# name: test_state[number.pinecil_setpoint_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 450, + 'min': 10, + 'mode': , + 'step': 5, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.pinecil_setpoint_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Setpoint temperature', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_setpoint_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_setpoint_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pinecil Setpoint temperature', + 'max': 450, + 'min': 10, + 'mode': , + 'step': 5, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_setpoint_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '300', + }) +# --- diff --git a/tests/components/iron_os/test_number.py b/tests/components/iron_os/test_number.py new file mode 100644 index 00000000000..c091040668c --- /dev/null +++ b/tests/components/iron_os/test_number.py @@ -0,0 +1,104 @@ +"""Tests for the IronOS number platform.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, patch + +from pynecil import CharSetting, CommunicationError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.fixture(autouse=True) +async def sensor_only() -> AsyncGenerator[None, None]: + """Enable only the number platform.""" + with patch( + "homeassistant.components.iron_os.PLATFORMS", + [Platform.NUMBER], + ): + yield + + +@pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "mock_pynecil", "ble_device" +) +async def test_state( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test the IronOS number platform states.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") +async def test_set_value( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, +) -> None: + """Test the IronOS number platform set value service.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ATTR_VALUE: 300}, + target={ATTR_ENTITY_ID: "number.pinecil_setpoint_temperature"}, + blocking=True, + ) + assert len(mock_pynecil.write.mock_calls) == 1 + mock_pynecil.write.assert_called_once_with(CharSetting.SETPOINT_TEMP, 300) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") +async def test_set_value_exception( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, +) -> None: + """Test the IronOS number platform set value service with exception.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + mock_pynecil.write.side_effect = CommunicationError + + with pytest.raises( + ServiceValidationError, + match="Failed to submit setting to device, try again later", + ): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ATTR_VALUE: 300}, + target={ATTR_ENTITY_ID: "number.pinecil_setpoint_temperature"}, + blocking=True, + ) From fb229fcae85ff6719da7e03f6b8857de83a0110d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Jul 2024 18:40:36 +0200 Subject: [PATCH 1744/2411] Improve test coverage of the homeworks integration (#122865) * Improve test coverage of the homeworks integration * Revert changes from the future * Revert changes from the future --- .../components/homeworks/__init__.py | 14 +++++------ tests/components/homeworks/test_init.py | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index e30778f7f15..f1a95102c3b 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -171,16 +171,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - return False + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + data: HomeworksData = hass.data[DOMAIN].pop(entry.entry_id) + for keypad in data.keypads.values(): + keypad.unsubscribe() - data: HomeworksData = hass.data[DOMAIN].pop(entry.entry_id) - for keypad in data.keypads.values(): - keypad.unsubscribe() + await hass.async_add_executor_job(data.controller.close) - await hass.async_add_executor_job(data.controller.close) - - return True + return unload_ok async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/tests/components/homeworks/test_init.py b/tests/components/homeworks/test_init.py index 87aabb6258f..af43fcfba10 100644 --- a/tests/components/homeworks/test_init.py +++ b/tests/components/homeworks/test_init.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components.homeworks import EVENT_BUTTON_PRESS, EVENT_BUTTON_RELEASE from homeassistant.components.homeworks.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError @@ -165,3 +166,25 @@ async def test_send_command( blocking=True, ) assert len(mock_controller._send.mock_calls) == 0 + + +async def test_cleanup_on_ha_shutdown( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, +) -> None: + """Test cleanup when HA shuts down.""" + mock_controller = MagicMock() + mock_homeworks.return_value = mock_controller + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) + mock_controller.stop.assert_not_called() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + mock_controller.close.assert_called_once_with() From 5eff4f981641779972999fb040a3f5854d50ec16 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 30 Jul 2024 19:33:25 +0200 Subject: [PATCH 1745/2411] Unifi improve fixture typing (#122864) * Improve typing of UniFi fixtures * Improve fixture typing, excluding image, sensor, switch * Improve fixture typing in image tests * Improve fixtures typing in sensor tests * Improve fixture typing in switch tests * Fix review comment --- tests/components/unifi/conftest.py | 37 +++++++--- tests/components/unifi/test_button.py | 26 ++++--- tests/components/unifi/test_config_flow.py | 13 ++-- tests/components/unifi/test_device_tracker.py | 43 +++++++----- tests/components/unifi/test_diagnostics.py | 4 +- tests/components/unifi/test_hub.py | 29 ++++---- tests/components/unifi/test_image.py | 18 +++-- tests/components/unifi/test_init.py | 19 +++--- tests/components/unifi/test_sensor.py | 61 +++++++++-------- tests/components/unifi/test_services.py | 16 ++--- tests/components/unifi/test_switch.py | 67 +++++++++++-------- tests/components/unifi/test_update.py | 21 ++++-- 12 files changed, 214 insertions(+), 140 deletions(-) diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index 4e460bab8f8..798b613b18d 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -3,10 +3,10 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Generator +from collections.abc import Callable, Coroutine, Generator from datetime import timedelta from types import MappingProxyType -from typing import Any +from typing import Any, Protocol from unittest.mock import AsyncMock, patch from aiounifi.models.message import MessageKey @@ -16,7 +16,6 @@ import pytest from homeassistant.components.unifi import STORAGE_KEY, STORAGE_VERSION from homeassistant.components.unifi.const import CONF_SITE_ID, DOMAIN as UNIFI_DOMAIN from homeassistant.components.unifi.hub.websocket import RETRY_TIMER -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -52,6 +51,20 @@ CONTROLLER_HOST = { "uptime": 1562600160, } +type ConfigEntryFactoryType = Callable[[], Coroutine[Any, Any, MockConfigEntry]] + + +class WebsocketMessageMock(Protocol): + """Fixture to mock websocket message.""" + + def __call__( + self, + *, + message: MessageKey | None = None, + data: list[dict[str, Any]] | dict[str, Any] | None = None, + ) -> None: + """Send websocket message.""" + @pytest.fixture(autouse=True, name="mock_discovery") def fixture_discovery(): @@ -96,7 +109,7 @@ def fixture_config_entry( hass: HomeAssistant, config_entry_data: MappingProxyType[str, Any], config_entry_options: MappingProxyType[str, Any], -) -> ConfigEntry: +) -> MockConfigEntry: """Define a config entry fixture.""" config_entry = MockConfigEntry( domain=UNIFI_DOMAIN, @@ -295,12 +308,12 @@ def fixture_default_requests( @pytest.fixture(name="config_entry_factory") async def fixture_config_entry_factory( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MockConfigEntry, mock_requests: Callable[[str, str], None], -) -> Callable[[], ConfigEntry]: +) -> ConfigEntryFactoryType: """Fixture factory that can set up UniFi network integration.""" - async def __mock_setup_config_entry() -> ConfigEntry: + async def __mock_setup_config_entry() -> MockConfigEntry: mock_requests(config_entry.data[CONF_HOST], config_entry.data[CONF_SITE_ID]) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -311,8 +324,8 @@ async def fixture_config_entry_factory( @pytest.fixture(name="config_entry_setup") async def fixture_config_entry_setup( - config_entry_factory: Callable[[], ConfigEntry], -) -> ConfigEntry: + config_entry_factory: ConfigEntryFactoryType, +) -> MockConfigEntry: """Fixture providing a set up instance of UniFi network integration.""" return await config_entry_factory() @@ -382,13 +395,15 @@ def fixture_aiounifi_websocket_state( @pytest.fixture(name="mock_websocket_message") -def fixture_aiounifi_websocket_message(_mock_websocket: AsyncMock): +def fixture_aiounifi_websocket_message( + _mock_websocket: AsyncMock, +) -> WebsocketMessageMock: """No real websocket allowed.""" def make_websocket_call( *, message: MessageKey | None = None, - data: list[dict] | dict | None = None, + data: list[dict[str, Any]] | dict[str, Any] | None = None, ) -> None: """Generate a websocket call.""" message_handler = _mock_websocket.call_args[0][0] diff --git a/tests/components/unifi/test_button.py b/tests/components/unifi/test_button.py index 9af96b64a50..fc3aeccea9f 100644 --- a/tests/components/unifi/test_button.py +++ b/tests/components/unifi/test_button.py @@ -11,7 +11,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.unifi.const import CONF_SITE_ID -from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, ConfigEntry +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( CONF_HOST, CONTENT_TYPE_JSON, @@ -23,7 +23,13 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryDisabler import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, snapshot_platform +from .conftest import ( + ConfigEntryFactoryType, + WebsocketMessageMock, + WebsocketStateManager, +) + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker RANDOM_TOKEN = "random_token" @@ -134,7 +140,7 @@ WLAN_REGENERATE_PASSWORD = [ async def test_entity_and_device_data( hass: HomeAssistant, entity_registry: er.EntityRegistry, - config_entry_factory, + config_entry_factory: ConfigEntryFactoryType, site_payload: dict[str, Any], snapshot: SnapshotAssertion, ) -> None: @@ -150,8 +156,8 @@ async def test_entity_and_device_data( async def _test_button_entity( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - mock_websocket_state, - config_entry: ConfigEntry, + mock_websocket_state: WebsocketStateManager, + config_entry: MockConfigEntry, entity_id: str, request_method: str, request_path: str, @@ -221,8 +227,8 @@ async def _test_button_entity( async def test_device_button_entities( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, - mock_websocket_state, + config_entry_setup: MockConfigEntry, + mock_websocket_state: WebsocketStateManager, entity_id: str, request_method: str, request_path: str, @@ -269,8 +275,8 @@ async def test_wlan_button_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, - mock_websocket_state, + config_entry_setup: MockConfigEntry, + mock_websocket_state: WebsocketStateManager, entity_id: str, request_method: str, request_path: str, @@ -308,7 +314,7 @@ async def test_wlan_button_entities( @pytest.mark.usefixtures("config_entry_setup") async def test_power_cycle_availability( hass: HomeAssistant, - mock_websocket_message, + mock_websocket_message: WebsocketMessageMock, device_payload: dict[str, Any], ) -> None: """Verify that disabling PoE marks entity as unavailable.""" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index fc0d2626eb6..1d745511dc5 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -1,6 +1,5 @@ """Test UniFi Network config flow.""" -from collections.abc import Callable import socket from unittest.mock import PropertyMock, patch @@ -25,7 +24,7 @@ from homeassistant.components.unifi.const import ( CONF_TRACK_WIRED_CLIENTS, DOMAIN as UNIFI_DOMAIN, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -36,6 +35,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from .conftest import ConfigEntryFactoryType + from tests.common import MockConfigEntry CLIENTS = [{"mac": "00:00:00:00:00:01"}] @@ -296,7 +297,7 @@ async def test_flow_fails_hub_unavailable(hass: HomeAssistant) -> None: async def test_reauth_flow_update_configuration( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Verify reauth flow can update hub configuration.""" config_entry = config_entry_setup @@ -337,7 +338,7 @@ async def test_reauth_flow_update_configuration( async def test_reauth_flow_update_configuration_on_not_loaded_entry( - hass: HomeAssistant, config_entry_factory: Callable[[], ConfigEntry] + hass: HomeAssistant, config_entry_factory: ConfigEntryFactoryType ) -> None: """Verify reauth flow can update hub configuration on a not loaded entry.""" with patch("aiounifi.Controller.login", side_effect=aiounifi.errors.RequestError): @@ -379,7 +380,7 @@ async def test_reauth_flow_update_configuration_on_not_loaded_entry( @pytest.mark.parametrize("wlan_payload", [WLANS]) @pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) async def test_advanced_option_flow( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Test advanced config flow options.""" config_entry = config_entry_setup @@ -463,7 +464,7 @@ async def test_advanced_option_flow( @pytest.mark.parametrize("client_payload", [CLIENTS]) async def test_simple_option_flow( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Test simple config flow options.""" config_entry = config_entry_setup diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index f2480a4f050..c653370656d 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1,6 +1,5 @@ """The tests for the UniFi Network device tracker platform.""" -from collections.abc import Callable from datetime import timedelta from types import MappingProxyType from typing import Any @@ -24,13 +23,18 @@ from homeassistant.components.unifi.const import ( DEFAULT_DETECTION_TIME, DOMAIN as UNIFI_DOMAIN, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, snapshot_platform +from .conftest import ( + ConfigEntryFactoryType, + WebsocketMessageMock, + WebsocketStateManager, +) + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform WIRED_CLIENT_1 = { "hostname": "wd_client_1", @@ -96,7 +100,7 @@ SWITCH_1 = { async def test_entity_and_device_data( hass: HomeAssistant, entity_registry: er.EntityRegistry, - config_entry_factory, + config_entry_factory: ConfigEntryFactoryType, snapshot: SnapshotAssertion, ) -> None: """Validate entity and device data with and without admin rights.""" @@ -112,8 +116,8 @@ async def test_entity_and_device_data( @pytest.mark.usefixtures("mock_device_registry") async def test_client_state_update( hass: HomeAssistant, - mock_websocket_message, - config_entry_factory: Callable[[], ConfigEntry], + mock_websocket_message: WebsocketMessageMock, + config_entry_factory: ConfigEntryFactoryType, client_payload: list[dict[str, Any]], ) -> None: """Verify tracking of wireless clients.""" @@ -165,7 +169,7 @@ async def test_client_state_update( async def test_client_state_from_event_source( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - mock_websocket_message, + mock_websocket_message: WebsocketMessageMock, client_payload: list[dict[str, Any]], ) -> None: """Verify update state of client based on event source.""" @@ -247,8 +251,8 @@ async def test_client_state_from_event_source( async def test_tracked_device_state_change( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - config_entry_factory: Callable[[], ConfigEntry], - mock_websocket_message, + config_entry_factory: ConfigEntryFactoryType, + mock_websocket_message: WebsocketMessageMock, device_payload: list[dict[str, Any]], state: int, interval: int, @@ -289,7 +293,9 @@ async def test_tracked_device_state_change( @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("mock_device_registry") async def test_remove_clients( - hass: HomeAssistant, mock_websocket_message, client_payload: list[dict[str, Any]] + hass: HomeAssistant, + mock_websocket_message: WebsocketMessageMock, + client_payload: list[dict[str, Any]], ) -> None: """Test the remove_items function with some clients.""" assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 @@ -309,7 +315,10 @@ async def test_remove_clients( @pytest.mark.parametrize("device_payload", [[SWITCH_1]]) @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("mock_device_registry") -async def test_hub_state_change(hass: HomeAssistant, mock_websocket_state) -> None: +async def test_hub_state_change( + hass: HomeAssistant, + mock_websocket_state: WebsocketStateManager, +) -> None: """Verify entities state reflect on hub connection becoming unavailable.""" assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME @@ -330,7 +339,7 @@ async def test_hub_state_change(hass: HomeAssistant, mock_websocket_state) -> No async def test_option_ssid_filter( hass: HomeAssistant, mock_websocket_message, - config_entry_factory: Callable[[], ConfigEntry], + config_entry_factory: ConfigEntryFactoryType, client_payload: list[dict[str, Any]], ) -> None: """Test the SSID filter works. @@ -434,7 +443,7 @@ async def test_option_ssid_filter( async def test_wireless_client_go_wired_issue( hass: HomeAssistant, mock_websocket_message, - config_entry_factory: Callable[[], ConfigEntry], + config_entry_factory: ConfigEntryFactoryType, client_payload: list[dict[str, Any]], ) -> None: """Test the solution to catch wireless device go wired UniFi issue. @@ -494,7 +503,7 @@ async def test_wireless_client_go_wired_issue( async def test_option_ignore_wired_bug( hass: HomeAssistant, mock_websocket_message, - config_entry_factory: Callable[[], ConfigEntry], + config_entry_factory: ConfigEntryFactoryType, client_payload: list[dict[str, Any]], ) -> None: """Test option to ignore wired bug.""" @@ -571,8 +580,8 @@ async def test_option_ignore_wired_bug( async def test_restoring_client( hass: HomeAssistant, entity_registry: er.EntityRegistry, - config_entry: ConfigEntry, - config_entry_factory: Callable[[], ConfigEntry], + config_entry: MockConfigEntry, + config_entry_factory: ConfigEntryFactoryType, client_payload: list[dict[str, Any]], clients_all_payload: list[dict[str, Any]], ) -> None: @@ -645,7 +654,7 @@ async def test_restoring_client( @pytest.mark.usefixtures("mock_device_registry") async def test_config_entry_options_track( hass: HomeAssistant, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, config_entry_options: MappingProxyType[str, Any], counts: tuple[int], expected: tuple[tuple[bool | None, ...], ...], diff --git a/tests/components/unifi/test_diagnostics.py b/tests/components/unifi/test_diagnostics.py index 3963de2deb3..80359a9c75c 100644 --- a/tests/components/unifi/test_diagnostics.py +++ b/tests/components/unifi/test_diagnostics.py @@ -9,9 +9,9 @@ from homeassistant.components.unifi.const import ( CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -122,7 +122,7 @@ DPI_GROUP_DATA = [ async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" diff --git a/tests/components/unifi/test_hub.py b/tests/components/unifi/test_hub.py index 0d75a83c5f5..af134c7449b 100644 --- a/tests/components/unifi/test_hub.py +++ b/tests/components/unifi/test_hub.py @@ -1,6 +1,5 @@ """Test UniFi Network.""" -from collections.abc import Callable from http import HTTPStatus from types import MappingProxyType from typing import Any @@ -12,18 +11,21 @@ import pytest from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.components.unifi.hub import get_unifi_api -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.util.dt as dt_util +from .conftest import ConfigEntryFactoryType, WebsocketStateManager + +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker async def test_hub_setup( device_registry: dr.DeviceRegistry, - config_entry_factory: Callable[[], ConfigEntry], + config_entry_factory: ConfigEntryFactoryType, ) -> None: """Successful setup.""" with patch( @@ -54,7 +56,7 @@ async def test_hub_setup( async def test_reset_after_successful_setup( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Calling reset when the entry has been setup.""" assert config_entry_setup.state is ConfigEntryState.LOADED @@ -64,7 +66,7 @@ async def test_reset_after_successful_setup( async def test_reset_fails( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Calling reset when the entry has been setup can return false.""" assert config_entry_setup.state is ConfigEntryState.LOADED @@ -80,8 +82,8 @@ async def test_reset_fails( @pytest.mark.usefixtures("mock_device_registry") async def test_connection_state_signalling( hass: HomeAssistant, - mock_websocket_state, - config_entry_factory: Callable[[], ConfigEntry], + config_entry_factory: ConfigEntryFactoryType, + mock_websocket_state: WebsocketStateManager, client_payload: list[dict[str, Any]], ) -> None: """Verify connection statesignalling and connection state are working.""" @@ -110,8 +112,8 @@ async def test_connection_state_signalling( async def test_reconnect_mechanism( aioclient_mock: AiohttpClientMocker, - mock_websocket_state, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, + mock_websocket_state: WebsocketStateManager, ) -> None: """Verify reconnect prints only on first reconnection try.""" aioclient_mock.clear_requests() @@ -140,7 +142,10 @@ async def test_reconnect_mechanism( ], ) @pytest.mark.usefixtures("config_entry_setup") -async def test_reconnect_mechanism_exceptions(mock_websocket_state, exception) -> None: +async def test_reconnect_mechanism_exceptions( + mock_websocket_state: WebsocketStateManager, + exception: Exception, +) -> None: """Verify async_reconnect calls expected methods.""" with ( patch("aiounifi.Controller.login", side_effect=exception), @@ -170,8 +175,8 @@ async def test_reconnect_mechanism_exceptions(mock_websocket_state, exception) - ) async def test_get_unifi_api_fails_to_connect( hass: HomeAssistant, - side_effect, - raised_exception, + side_effect: Exception, + raised_exception: Exception, config_entry_data: MappingProxyType[str, Any], ) -> None: """Check that get_unifi_api can handle UniFi Network being unavailable.""" diff --git a/tests/components/unifi/test_image.py b/tests/components/unifi/test_image.py index 6733845c52f..dc37d7cb8b7 100644 --- a/tests/components/unifi/test_image.py +++ b/tests/components/unifi/test_image.py @@ -18,6 +18,12 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryDisabler from homeassistant.util import dt as dt_util +from .conftest import ( + ConfigEntryFactoryType, + WebsocketMessageMock, + WebsocketStateManager, +) + from tests.common import async_fire_time_changed, snapshot_platform from tests.typing import ClientSessionGenerator @@ -82,7 +88,7 @@ WLAN = { async def test_entity_and_device_data( hass: HomeAssistant, entity_registry: er.EntityRegistry, - config_entry_factory, + config_entry_factory: ConfigEntryFactoryType, site_payload: dict[str, Any], snapshot: SnapshotAssertion, ) -> None: @@ -102,7 +108,7 @@ async def test_wlan_qr_code( entity_registry: er.EntityRegistry, hass_client: ClientSessionGenerator, snapshot: SnapshotAssertion, - mock_websocket_message, + mock_websocket_message: WebsocketMessageMock, ) -> None: """Test the update_clients function when no clients are found.""" assert len(hass.states.async_entity_ids(IMAGE_DOMAIN)) == 0 @@ -151,7 +157,9 @@ async def test_wlan_qr_code( @pytest.mark.parametrize("wlan_payload", [[WLAN]]) @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_hub_state_change(hass: HomeAssistant, mock_websocket_state) -> None: +async def test_hub_state_change( + hass: HomeAssistant, mock_websocket_state: WebsocketStateManager +) -> None: """Verify entities state reflect on hub becoming unavailable.""" assert hass.states.get("image.ssid_1_qr_code").state != STATE_UNAVAILABLE @@ -167,7 +175,9 @@ async def test_hub_state_change(hass: HomeAssistant, mock_websocket_state) -> No @pytest.mark.parametrize("wlan_payload", [[WLAN]]) @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_source_availability(hass: HomeAssistant, mock_websocket_message) -> None: +async def test_source_availability( + hass: HomeAssistant, mock_websocket_message: WebsocketMessageMock +) -> None: """Verify entities state reflect on source becoming unavailable.""" assert hass.states.get("image.ssid_1_qr_code").state != STATE_UNAVAILABLE diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index de08ba2c6d7..68f80555cd6 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -1,6 +1,5 @@ """Test UniFi Network integration setup process.""" -from collections.abc import Callable from typing import Any from unittest.mock import patch @@ -15,19 +14,23 @@ from homeassistant.components.unifi.const import ( CONF_TRACK_DEVICES, ) from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from .conftest import DEFAULT_CONFIG_ENTRY_ID +from .conftest import ( + DEFAULT_CONFIG_ENTRY_ID, + ConfigEntryFactoryType, + WebsocketMessageMock, +) from tests.common import flush_store from tests.typing import WebSocketGenerator async def test_setup_entry_fails_config_entry_not_ready( - config_entry_factory: Callable[[], ConfigEntry], + hass: HomeAssistant, config_entry_factory: ConfigEntryFactoryType ) -> None: """Failed authentication trigger a reauthentication flow.""" with patch( @@ -40,7 +43,7 @@ async def test_setup_entry_fails_config_entry_not_ready( async def test_setup_entry_fails_trigger_reauth_flow( - hass: HomeAssistant, config_entry_factory: Callable[[], ConfigEntry] + hass: HomeAssistant, config_entry_factory: ConfigEntryFactoryType ) -> None: """Failed authentication trigger a reauthentication flow.""" with ( @@ -78,7 +81,7 @@ async def test_setup_entry_fails_trigger_reauth_flow( async def test_wireless_clients( hass: HomeAssistant, hass_storage: dict[str, Any], - config_entry_factory: Callable[[], ConfigEntry], + config_entry_factory: ConfigEntryFactoryType, ) -> None: """Verify wireless clients class.""" hass_storage[unifi.STORAGE_KEY] = { @@ -163,10 +166,10 @@ async def test_wireless_clients( async def test_remove_config_entry_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - config_entry_factory: Callable[[], ConfigEntry], + config_entry_factory: ConfigEntryFactoryType, client_payload: list[dict[str, Any]], device_payload: list[dict[str, Any]], - mock_websocket_message, + mock_websocket_message: WebsocketMessageMock, hass_ws_client: WebSocketGenerator, ) -> None: """Verify removing a device manually.""" diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 779df6660f0..5af4b297847 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1,6 +1,5 @@ """UniFi Network sensor platform tests.""" -from collections.abc import Callable from copy import deepcopy from datetime import datetime, timedelta from types import MappingProxyType @@ -29,7 +28,7 @@ from homeassistant.components.unifi.const import ( DEFAULT_DETECTION_TIME, DEVICE_STATES, ) -from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, ConfigEntry +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, @@ -42,7 +41,13 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryDisabler import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed +from .conftest import ( + ConfigEntryFactoryType, + WebsocketMessageMock, + WebsocketStateManager, +) + +from tests.common import MockConfigEntry, async_fire_time_changed DEVICE_1 = { "board_rev": 2, @@ -362,9 +367,9 @@ async def test_no_clients(hass: HomeAssistant) -> None: ) async def test_bandwidth_sensors( hass: HomeAssistant, - mock_websocket_message, + mock_websocket_message: WebsocketMessageMock, config_entry_options: MappingProxyType[str, Any], - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, client_payload: list[dict[str, Any]], ) -> None: """Verify that bandwidth sensors are working as expected.""" @@ -491,7 +496,9 @@ async def test_bandwidth_sensors( @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_remove_sensors( - hass: HomeAssistant, mock_websocket_message, client_payload: list[dict[str, Any]] + hass: HomeAssistant, + mock_websocket_message: WebsocketMessageMock, + client_payload: list[dict[str, Any]], ) -> None: """Verify removing of clients work as expected.""" assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 @@ -520,8 +527,8 @@ async def test_remove_sensors( async def test_poe_port_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_websocket_message, - mock_websocket_state, + mock_websocket_message: WebsocketMessageMock, + mock_websocket_state: WebsocketStateManager, ) -> None: """Test the update_items function with some clients.""" assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 @@ -594,9 +601,9 @@ async def test_poe_port_switches( async def test_wlan_client_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_websocket_message, - mock_websocket_state, - config_entry_factory: Callable[[], ConfigEntry], + config_entry_factory: ConfigEntryFactoryType, + mock_websocket_message: WebsocketMessageMock, + mock_websocket_state: WebsocketStateManager, client_payload: list[dict[str, Any]], ) -> None: """Verify that WLAN client sensors are working as expected.""" @@ -736,7 +743,7 @@ async def test_wlan_client_sensors( async def test_outlet_power_readings( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_websocket_message, + mock_websocket_message: WebsocketMessageMock, device_payload: list[dict[str, Any]], entity_id: str, expected_unique_id: str, @@ -798,7 +805,7 @@ async def test_outlet_power_readings( async def test_device_temperature( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_websocket_message, + mock_websocket_message: WebsocketMessageMock, device_payload: list[dict[str, Any]], ) -> None: """Verify that temperature sensors are working as expected.""" @@ -847,7 +854,7 @@ async def test_device_temperature( async def test_device_state( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_websocket_message, + mock_websocket_message: WebsocketMessageMock, device_payload: list[dict[str, Any]], ) -> None: """Verify that state sensors are working as expected.""" @@ -884,7 +891,7 @@ async def test_device_state( async def test_device_system_stats( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_websocket_message, + mock_websocket_message: WebsocketMessageMock, device_payload: list[dict[str, Any]], ) -> None: """Verify that device stats sensors are working as expected.""" @@ -979,9 +986,9 @@ async def test_device_system_stats( async def test_bandwidth_port_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_websocket_message, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, config_entry_options: MappingProxyType[str, Any], + mock_websocket_message: WebsocketMessageMock, device_payload: list[dict[str, Any]], ) -> None: """Verify that port bandwidth sensors are working as expected.""" @@ -1096,9 +1103,9 @@ async def test_bandwidth_port_sensors( async def test_device_client_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, - config_entry_factory, - mock_websocket_message, - client_payload, + config_entry_factory: ConfigEntryFactoryType, + mock_websocket_message: WebsocketMessageMock, + client_payload: dict[str, Any], ) -> None: """Verify that WLAN client sensors are working as expected.""" client_payload += [ @@ -1246,8 +1253,8 @@ async def test_sensor_sources( async def _test_uptime_entity( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - mock_websocket_message, - config_entry_factory: Callable[[], ConfigEntry], + mock_websocket_message: WebsocketMessageMock, + config_entry_factory: ConfigEntryFactoryType, payload: dict[str, Any], entity_id: str, message_key: MessageKey, @@ -1326,9 +1333,9 @@ async def test_client_uptime( hass: HomeAssistant, entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, - mock_websocket_message, config_entry_options: MappingProxyType[str, Any], - config_entry_factory: Callable[[], ConfigEntry], + config_entry_factory: ConfigEntryFactoryType, + mock_websocket_message: WebsocketMessageMock, client_payload: list[dict[str, Any]], initial_uptime, event_uptime, @@ -1401,8 +1408,8 @@ async def test_device_uptime( hass: HomeAssistant, entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, - mock_websocket_message, - config_entry_factory: Callable[[], ConfigEntry], + config_entry_factory: ConfigEntryFactoryType, + mock_websocket_message: WebsocketMessageMock, device_payload: list[dict[str, Any]], ) -> None: """Verify that device uptime sensors are working as expected.""" @@ -1502,7 +1509,7 @@ async def test_device_uptime( async def test_wan_monitor_latency( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_websocket_message, + mock_websocket_message: WebsocketMessageMock, device_payload: list[dict[str, Any]], entity_id: str, state: str, diff --git a/tests/components/unifi/test_services.py b/tests/components/unifi/test_services.py index bf7058e28ff..a7968a92e22 100644 --- a/tests/components/unifi/test_services.py +++ b/tests/components/unifi/test_services.py @@ -10,11 +10,11 @@ from homeassistant.components.unifi.services import ( SERVICE_RECONNECT_CLIENT, SERVICE_REMOVE_CLIENTS, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -25,7 +25,7 @@ async def test_reconnect_client( hass: HomeAssistant, device_registry: dr.DeviceRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, client_payload: list[dict[str, Any]], ) -> None: """Verify call to reconnect client is performed as expected.""" @@ -69,7 +69,7 @@ async def test_reconnect_device_without_mac( hass: HomeAssistant, device_registry: dr.DeviceRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, ) -> None: """Verify no call is made if device does not have a known mac.""" aioclient_mock.clear_requests() @@ -95,7 +95,7 @@ async def test_reconnect_client_hub_unavailable( hass: HomeAssistant, device_registry: dr.DeviceRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, client_payload: list[dict[str, Any]], ) -> None: """Verify no call is made if hub is unavailable.""" @@ -127,7 +127,7 @@ async def test_reconnect_client_unknown_mac( hass: HomeAssistant, device_registry: dr.DeviceRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, ) -> None: """Verify no call is made if trying to reconnect a mac unknown to hub.""" aioclient_mock.clear_requests() @@ -152,7 +152,7 @@ async def test_reconnect_wired_client( hass: HomeAssistant, device_registry: dr.DeviceRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, client_payload: list[dict[str, Any]], ) -> None: """Verify no call is made if client is wired.""" @@ -204,7 +204,7 @@ async def test_reconnect_wired_client( async def test_remove_clients( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, ) -> None: """Verify removing different variations of clients work.""" aioclient_mock.clear_requests() @@ -288,7 +288,7 @@ async def test_services_handle_unloaded_config_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, device_registry: dr.DeviceRegistry, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, clients_all_payload: dict[str, Any], ) -> None: """Verify no call is made if config entry is unloaded.""" diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index daf64301c8e..6d85437a244 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -1,6 +1,5 @@ """UniFi Network switch platform tests.""" -from collections.abc import Callable from copy import deepcopy from datetime import timedelta from typing import Any @@ -22,7 +21,7 @@ from homeassistant.components.unifi.const import ( CONF_TRACK_DEVICES, DOMAIN as UNIFI_DOMAIN, ) -from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, ConfigEntry +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -37,9 +36,14 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryDisabler from homeassistant.util import dt as dt_util -from .conftest import CONTROLLER_HOST +from .conftest import ( + CONTROLLER_HOST, + ConfigEntryFactoryType, + WebsocketMessageMock, + WebsocketStateManager, +) -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker CLIENT_1 = { @@ -841,12 +845,11 @@ async def test_not_admin(hass: HomeAssistant) -> None: @pytest.mark.parametrize("clients_all_payload", [[BLOCKED, UNBLOCKED, CLIENT_1]]) @pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) @pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) -@pytest.mark.usefixtures("config_entry_setup") async def test_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, ) -> None: """Test the update_items function with some clients.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 3 @@ -930,7 +933,9 @@ async def test_switches( @pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) @pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) @pytest.mark.usefixtures("config_entry_setup") -async def test_remove_switches(hass: HomeAssistant, mock_websocket_message) -> None: +async def test_remove_switches( + hass: HomeAssistant, mock_websocket_message: WebsocketMessageMock +) -> None: """Test the update_items function with some clients.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 @@ -967,8 +972,8 @@ async def test_remove_switches(hass: HomeAssistant, mock_websocket_message) -> N async def test_block_switches( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - mock_websocket_message, - config_entry_setup: ConfigEntry, + mock_websocket_message: WebsocketMessageMock, + config_entry_setup: MockConfigEntry, ) -> None: """Test the update_items function with some clients.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 @@ -1027,7 +1032,9 @@ async def test_block_switches( @pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) @pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) @pytest.mark.usefixtures("config_entry_setup") -async def test_dpi_switches(hass: HomeAssistant, mock_websocket_message) -> None: +async def test_dpi_switches( + hass: HomeAssistant, mock_websocket_message: WebsocketMessageMock +) -> None: """Test the update_items function with some clients.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 @@ -1053,7 +1060,7 @@ async def test_dpi_switches(hass: HomeAssistant, mock_websocket_message) -> None @pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) @pytest.mark.usefixtures("config_entry_setup") async def test_dpi_switches_add_second_app( - hass: HomeAssistant, mock_websocket_message + hass: HomeAssistant, mock_websocket_message: WebsocketMessageMock ) -> None: """Test the update_items function with some clients.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 @@ -1104,12 +1111,10 @@ async def test_dpi_switches_add_second_app( @pytest.mark.parametrize(("traffic_rule_payload"), [([TRAFFIC_RULE])]) -@pytest.mark.usefixtures("config_entry_setup") async def test_traffic_rules( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - mock_websocket_message, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, traffic_rule_payload: list[dict[str, Any]], ) -> None: """Test control of UniFi traffic rules.""" @@ -1172,8 +1177,8 @@ async def test_traffic_rules( async def test_outlet_switches( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - mock_websocket_message, - config_entry_setup: ConfigEntry, + mock_websocket_message: WebsocketMessageMock, + config_entry_setup: MockConfigEntry, device_payload: list[dict[str, Any]], entity_id: str, outlet_index: int, @@ -1268,7 +1273,7 @@ async def test_outlet_switches( ) @pytest.mark.usefixtures("config_entry_setup") async def test_new_client_discovered_on_block_control( - hass: HomeAssistant, mock_websocket_message + hass: HomeAssistant, mock_websocket_message: WebsocketMessageMock ) -> None: """Test if 2nd update has a new client.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 @@ -1286,7 +1291,9 @@ async def test_new_client_discovered_on_block_control( ) @pytest.mark.parametrize("clients_all_payload", [[BLOCKED, UNBLOCKED]]) async def test_option_block_clients( - hass: HomeAssistant, config_entry_setup: ConfigEntry, clients_all_payload + hass: HomeAssistant, + config_entry_setup: MockConfigEntry, + clients_all_payload: list[dict[str, Any]], ) -> None: """Test the changes to option reflects accordingly.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 @@ -1334,7 +1341,7 @@ async def test_option_block_clients( @pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) @pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) async def test_option_remove_switches( - hass: HomeAssistant, config_entry_setup: ConfigEntry + hass: HomeAssistant, config_entry_setup: MockConfigEntry ) -> None: """Test removal of DPI switch when options updated.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 @@ -1352,8 +1359,8 @@ async def test_poe_port_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - mock_websocket_message, - config_entry_setup: ConfigEntry, + mock_websocket_message: WebsocketMessageMock, + config_entry_setup: MockConfigEntry, device_payload: list[dict[str, Any]], ) -> None: """Test PoE port entities work.""" @@ -1451,8 +1458,8 @@ async def test_wlan_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - mock_websocket_message, - config_entry_setup: ConfigEntry, + mock_websocket_message: WebsocketMessageMock, + config_entry_setup: MockConfigEntry, wlan_payload: list[dict[str, Any]], ) -> None: """Test control of UniFi WLAN availability.""" @@ -1507,8 +1514,8 @@ async def test_port_forwarding_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - mock_websocket_message, - config_entry_setup: ConfigEntry, + mock_websocket_message: WebsocketMessageMock, + config_entry_setup: MockConfigEntry, port_forward_payload: list[dict[str, Any]], ) -> None: """Test control of UniFi port forwarding.""" @@ -1606,9 +1613,9 @@ async def test_port_forwarding_switches( async def test_updating_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, - config_entry_factory: Callable[[], ConfigEntry], - config_entry: ConfigEntry, - device_payload, + config_entry_factory: ConfigEntryFactoryType, + config_entry: MockConfigEntry, + device_payload: list[dict[str, Any]], ) -> None: """Verify outlet control and poe control unique ID update works.""" entity_registry.async_get_or_create( @@ -1644,7 +1651,9 @@ async def test_updating_unique_id( @pytest.mark.parametrize("wlan_payload", [[WLAN]]) @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_hub_state_change(hass: HomeAssistant, mock_websocket_state) -> None: +async def test_hub_state_change( + hass: HomeAssistant, mock_websocket_state: WebsocketStateManager +) -> None: """Verify entities state reflect on hub connection becoming unavailable.""" entity_ids = ( "switch.block_client_2", diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py index a8fe9231159..7bf4b9aec9d 100644 --- a/tests/components/unifi/test_update.py +++ b/tests/components/unifi/test_update.py @@ -16,7 +16,6 @@ from homeassistant.components.update import ( DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -28,7 +27,13 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.common import snapshot_platform +from .conftest import ( + ConfigEntryFactoryType, + WebsocketMessageMock, + WebsocketStateManager, +) + +from tests.common import MockConfigEntry, snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker # Device with new firmware available @@ -74,7 +79,7 @@ DEVICE_2 = { async def test_entity_and_device_data( hass: HomeAssistant, entity_registry: er.EntityRegistry, - config_entry_factory, + config_entry_factory: ConfigEntryFactoryType, snapshot: SnapshotAssertion, ) -> None: """Validate entity and device data with and without admin rights.""" @@ -85,7 +90,9 @@ async def test_entity_and_device_data( @pytest.mark.parametrize("device_payload", [[DEVICE_1]]) @pytest.mark.usefixtures("config_entry_setup") -async def test_device_updates(hass: HomeAssistant, mock_websocket_message) -> None: +async def test_device_updates( + hass: HomeAssistant, mock_websocket_message: WebsocketMessageMock +) -> None: """Test the update_items function with some devices.""" device_1_state = hass.states.get("update.device_1") assert device_1_state.state == STATE_ON @@ -122,7 +129,7 @@ async def test_device_updates(hass: HomeAssistant, mock_websocket_message) -> No async def test_install( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - config_entry_setup: ConfigEntry, + config_entry_setup: MockConfigEntry, ) -> None: """Test the device update install call.""" device_state = hass.states.get("update.device_1") @@ -154,7 +161,9 @@ async def test_install( @pytest.mark.parametrize("device_payload", [[DEVICE_1]]) @pytest.mark.usefixtures("config_entry_setup") -async def test_hub_state_change(hass: HomeAssistant, mock_websocket_state) -> None: +async def test_hub_state_change( + hass: HomeAssistant, mock_websocket_state: WebsocketStateManager +) -> None: """Verify entities state reflect on hub becoming unavailable.""" assert hass.states.get("update.device_1").state == STATE_ON From 94c0b9fc069b02bab31dff04e71ae0457e03f336 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Jul 2024 19:39:53 +0200 Subject: [PATCH 1746/2411] Bump pyhomeworks to 1.0.0 (#122867) --- homeassistant/components/homeworks/__init__.py | 14 ++++++++------ homeassistant/components/homeworks/config_flow.py | 7 ++++--- homeassistant/components/homeworks/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homeworks/test_config_flow.py | 8 ++++++-- tests/components/homeworks/test_init.py | 7 +++++-- 7 files changed, 26 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index f1a95102c3b..cf39bc72ec6 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -8,6 +8,7 @@ from dataclasses import dataclass import logging from typing import Any +from pyhomeworks import exceptions as hw_exceptions from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED, Homeworks import voluptuous as vol @@ -141,15 +142,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: dispatcher_send(hass, signal, msg_type, values) config = entry.options + controller = Homeworks(config[CONF_HOST], config[CONF_PORT], hw_callback) try: - controller = await hass.async_add_executor_job( - Homeworks, config[CONF_HOST], config[CONF_PORT], hw_callback - ) - except (ConnectionError, OSError) as err: + await hass.async_add_executor_job(controller.connect) + except hw_exceptions.HomeworksException as err: + _LOGGER.debug("Failed to connect: %s", err, exc_info=True) raise ConfigEntryNotReady from err + controller.start() def cleanup(event: Event) -> None: - controller.close() + controller.stop() entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)) @@ -176,7 +178,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for keypad in data.keypads.values(): keypad.unsubscribe() - await hass.async_add_executor_job(data.controller.close) + await hass.async_add_executor_job(data.controller.stop) return unload_ok diff --git a/homeassistant/components/homeworks/config_flow.py b/homeassistant/components/homeworks/config_flow.py index 81b31e4644e..ec381c3331f 100644 --- a/homeassistant/components/homeworks/config_flow.py +++ b/homeassistant/components/homeworks/config_flow.py @@ -6,6 +6,7 @@ from functools import partial import logging from typing import Any +from pyhomeworks import exceptions as hw_exceptions from pyhomeworks.pyhomeworks import Homeworks import voluptuous as vol @@ -128,18 +129,18 @@ async def _try_connection(user_input: dict[str, Any]) -> None: "Trying to connect to %s:%s", user_input[CONF_HOST], user_input[CONF_PORT] ) controller = Homeworks(host, port, lambda msg_types, values: None) + controller.connect() controller.close() - controller.join() hass = async_get_hass() try: await hass.async_add_executor_job( _try_connect, user_input[CONF_HOST], user_input[CONF_PORT] ) - except ConnectionError as err: + except hw_exceptions.HomeworksConnectionFailed as err: raise SchemaFlowError("connection_error") from err except Exception as err: - _LOGGER.exception("Caught unexpected exception") + _LOGGER.exception("Caught unexpected exception %s") raise SchemaFlowError("unknown_error") from err diff --git a/homeassistant/components/homeworks/manifest.json b/homeassistant/components/homeworks/manifest.json index c2520b910d9..9b447ef4aea 100644 --- a/homeassistant/components/homeworks/manifest.json +++ b/homeassistant/components/homeworks/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/homeworks", "iot_class": "local_push", "loggers": ["pyhomeworks"], - "requirements": ["pyhomeworks==0.0.6"] + "requirements": ["pyhomeworks==1.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4d74e059a59..e0d502f3000 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1903,7 +1903,7 @@ pyhiveapi==0.5.16 pyhomematic==0.1.77 # homeassistant.components.homeworks -pyhomeworks==0.0.6 +pyhomeworks==1.0.0 # homeassistant.components.ialarm pyialarm==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 86fdf2da7f9..a1037d5109f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1517,7 +1517,7 @@ pyhiveapi==0.5.16 pyhomematic==0.1.77 # homeassistant.components.homeworks -pyhomeworks==0.0.6 +pyhomeworks==1.0.0 # homeassistant.components.ialarm pyialarm==2.2.0 diff --git a/tests/components/homeworks/test_config_flow.py b/tests/components/homeworks/test_config_flow.py index 8f5334b21f9..3e359caf7f2 100644 --- a/tests/components/homeworks/test_config_flow.py +++ b/tests/components/homeworks/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import ANY, MagicMock +from pyhomeworks import exceptions as hw_exceptions import pytest from pytest_unordered import unordered @@ -55,7 +56,7 @@ async def test_user_flow( } mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) mock_controller.close.assert_called_once_with() - mock_controller.join.assert_called_once_with() + mock_controller.join.assert_not_called() async def test_user_flow_already_exists( @@ -96,7 +97,10 @@ async def test_user_flow_already_exists( @pytest.mark.parametrize( ("side_effect", "error"), - [(ConnectionError, "connection_error"), (Exception, "unknown_error")], + [ + (hw_exceptions.HomeworksConnectionFailed, "connection_error"), + (Exception, "unknown_error"), + ], ) async def test_user_flow_cannot_connect( hass: HomeAssistant, diff --git a/tests/components/homeworks/test_init.py b/tests/components/homeworks/test_init.py index af43fcfba10..2363e0f157d 100644 --- a/tests/components/homeworks/test_init.py +++ b/tests/components/homeworks/test_init.py @@ -2,6 +2,7 @@ from unittest.mock import ANY, MagicMock +from pyhomeworks import exceptions as hw_exceptions from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED import pytest @@ -41,7 +42,9 @@ async def test_config_entry_not_ready( mock_homeworks: MagicMock, ) -> None: """Test the Homeworks configuration entry not ready.""" - mock_homeworks.side_effect = ConnectionError + mock_homeworks.return_value.connect.side_effect = ( + hw_exceptions.HomeworksConnectionFailed + ) mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -187,4 +190,4 @@ async def test_cleanup_on_ha_shutdown( hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - mock_controller.close.assert_called_once_with() + mock_controller.stop.assert_called_once_with() From 022e1b0c02a60c90628f2d4ad11edb0fad445fb4 Mon Sep 17 00:00:00 2001 From: Bill Flood Date: Tue, 30 Jul 2024 12:07:12 -0700 Subject: [PATCH 1747/2411] Add other medium types to Mopeka sensor (#122705) Co-authored-by: J. Nick Koston --- homeassistant/components/mopeka/__init__.py | 15 +++- .../components/mopeka/config_flow.py | 86 +++++++++++++++++-- homeassistant/components/mopeka/const.py | 8 ++ homeassistant/components/mopeka/strings.json | 18 +++- tests/components/mopeka/test_config_flow.py | 52 +++++++++-- 5 files changed, 163 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/mopeka/__init__.py b/homeassistant/components/mopeka/__init__.py index 2538ec3d810..17a87efd6e6 100644 --- a/homeassistant/components/mopeka/__init__.py +++ b/homeassistant/components/mopeka/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging -from mopeka_iot_ble import MopekaIOTBluetoothDeviceData +from mopeka_iot_ble import MediumType, MopekaIOTBluetoothDeviceData from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( @@ -14,6 +14,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from .const import CONF_MEDIUM_TYPE + PLATFORMS: list[Platform] = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) @@ -26,7 +28,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: MopekaConfigEntry) -> bo """Set up Mopeka BLE device from a config entry.""" address = entry.unique_id assert address is not None - data = MopekaIOTBluetoothDeviceData() + + # Default sensors configured prior to the intorudction of MediumType + medium_type_str = entry.data.get(CONF_MEDIUM_TYPE, MediumType.PROPANE.value) + data = MopekaIOTBluetoothDeviceData(MediumType(medium_type_str)) coordinator = entry.runtime_data = PassiveBluetoothProcessorCoordinator( hass, _LOGGER, @@ -37,9 +42,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: MopekaConfigEntry) -> bo await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # only start after all platforms have had a chance to subscribe entry.async_on_unload(coordinator.async_start()) + entry.async_on_unload(entry.add_update_listener(update_listener)) return True +async def update_listener(hass: HomeAssistant, entry: MopekaConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry(hass: HomeAssistant, entry: MopekaConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/mopeka/config_flow.py b/homeassistant/components/mopeka/config_flow.py index 1732157ce49..72e9386a47f 100644 --- a/homeassistant/components/mopeka/config_flow.py +++ b/homeassistant/components/mopeka/config_flow.py @@ -2,19 +2,43 @@ from __future__ import annotations +from enum import Enum from typing import Any from mopeka_iot_ble import MopekaIOTBluetoothDeviceData as DeviceData import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS +from homeassistant.core import callback -from .const import DOMAIN +from .const import CONF_MEDIUM_TYPE, DEFAULT_MEDIUM_TYPE, DOMAIN, MediumType + + +def format_medium_type(medium_type: Enum) -> str: + """Format the medium type for human reading.""" + return medium_type.name.replace("_", " ").title() + + +MEDIUM_TYPES_BY_NAME = { + medium.value: format_medium_type(medium) for medium in MediumType +} + + +def async_generate_schema(medium_type: str | None = None) -> vol.Schema: + """Return the base schema with formatted medium types.""" + return vol.Schema( + { + vol.Required( + CONF_MEDIUM_TYPE, default=medium_type or DEFAULT_MEDIUM_TYPE + ): vol.In(MEDIUM_TYPES_BY_NAME) + } + ) class MopekaConfigFlow(ConfigFlow, domain=DOMAIN): @@ -28,6 +52,14 @@ class MopekaConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} + @callback + @staticmethod + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> MopekaOptionsFlow: + """Return the options flow for this handler.""" + return MopekaOptionsFlow(config_entry) + async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak ) -> ConfigFlowResult: @@ -44,32 +76,39 @@ class MopekaConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Confirm discovery.""" + """Confirm discovery and select medium type.""" 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._discovered_devices[discovery_info.address] = title + return self.async_create_entry( + title=self._discovered_devices[discovery_info.address], + data={CONF_MEDIUM_TYPE: user_input[CONF_MEDIUM_TYPE]}, + ) 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=placeholders, + data_schema=async_generate_schema(), ) async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Handle the user step to pick discovered device.""" + """Handle the user step to pick discovered device and select medium type.""" if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) self._abort_if_unique_id_configured() return self.async_create_entry( - title=self._discovered_devices[address], data={} + title=self._discovered_devices[address], + data={CONF_MEDIUM_TYPE: user_input[CONF_MEDIUM_TYPE]}, ) current_addresses = self._async_current_ids() @@ -89,6 +128,39 @@ class MopekaConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=vol.Schema( - {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + { + vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices), + **async_generate_schema().schema, + } + ), + ) + + +class MopekaOptionsFlow(config_entries.OptionsFlow): + """Handle options for the Mopeka component.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle options flow.""" + if user_input is not None: + new_data = { + **self.config_entry.data, + CONF_MEDIUM_TYPE: user_input[CONF_MEDIUM_TYPE], + } + self.hass.config_entries.async_update_entry( + self.config_entry, data=new_data + ) + await self.hass.config_entries.async_reload(self.config_entry.entry_id) + return self.async_create_entry(title="", data={}) + + return self.async_show_form( + step_id="init", + data_schema=async_generate_schema( + self.config_entry.data.get(CONF_MEDIUM_TYPE) ), ) diff --git a/homeassistant/components/mopeka/const.py b/homeassistant/components/mopeka/const.py index 0d78146f5a8..e18828f2364 100644 --- a/homeassistant/components/mopeka/const.py +++ b/homeassistant/components/mopeka/const.py @@ -1,3 +1,11 @@ """Constants for the Mopeka integration.""" +from typing import Final + +from mopeka_iot_ble import MediumType + DOMAIN = "mopeka" + +CONF_MEDIUM_TYPE: Final = "medium_type" + +DEFAULT_MEDIUM_TYPE = MediumType.PROPANE.value diff --git a/homeassistant/components/mopeka/strings.json b/homeassistant/components/mopeka/strings.json index 16a80220a20..2455eea2f76 100644 --- a/homeassistant/components/mopeka/strings.json +++ b/homeassistant/components/mopeka/strings.json @@ -5,11 +5,15 @@ "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", "data": { - "address": "[%key:common::config_flow::data::device%]" + "address": "[%key:common::config_flow::data::device%]", + "medium_type": "Medium Type" } }, "bluetooth_confirm": { - "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]", + "data": { + "medium_type": "[%key:component::mopeka::config::step::user::data::medium_type%]" + } } }, "abort": { @@ -18,5 +22,15 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "options": { + "step": { + "init": { + "title": "Configure Mopeka", + "data": { + "medium_type": "[%key:component::mopeka::config::step::user::data::medium_type%]" + } + } + } } } diff --git a/tests/components/mopeka/test_config_flow.py b/tests/components/mopeka/test_config_flow.py index 826fe8db2aa..7a341052f22 100644 --- a/tests/components/mopeka/test_config_flow.py +++ b/tests/components/mopeka/test_config_flow.py @@ -2,8 +2,10 @@ from unittest.mock import patch +import voluptuous as vol + from homeassistant import config_entries -from homeassistant.components.mopeka.const import DOMAIN +from homeassistant.components.mopeka.const import CONF_MEDIUM_TYPE, DOMAIN, MediumType from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -21,13 +23,14 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.mopeka.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} + result["flow_id"], user_input={CONF_MEDIUM_TYPE: MediumType.PROPANE.value} ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Pro Plus EEFF" - assert result2["data"] == {} + assert result2["data"] == {CONF_MEDIUM_TYPE: MediumType.PROPANE.value} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -71,7 +74,10 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Pro Plus EEFF" - assert result2["data"] == {} + assert CONF_MEDIUM_TYPE in result2["data"] + assert result2["data"][CONF_MEDIUM_TYPE] in [ + medium_type.value for medium_type in MediumType + ] assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -190,8 +196,44 @@ async def test_async_step_user_takes_precedence_over_discovery( ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Pro Plus EEFF" - assert result2["data"] == {} + assert CONF_MEDIUM_TYPE in result2["data"] + assert result2["data"][CONF_MEDIUM_TYPE] in [ + medium_type.value for medium_type in MediumType + ] 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) + + +async def test_async_step_reconfigure_options(hass: HomeAssistant) -> None: + """Test reconfig options: change MediumType from air to fresh water.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:75:10", + title="TD40/TD200 7510", + data={CONF_MEDIUM_TYPE: MediumType.AIR.value}, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.data[CONF_MEDIUM_TYPE] == MediumType.AIR.value + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + schema: vol.Schema = result["data_schema"] + medium_type_key = next( + iter(key for key in schema.schema if key == CONF_MEDIUM_TYPE) + ) + assert medium_type_key.default() == MediumType.AIR.value + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_MEDIUM_TYPE: MediumType.FRESH_WATER.value}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + + # Verify the new configuration + assert entry.data[CONF_MEDIUM_TYPE] == MediumType.FRESH_WATER.value From 6362ca1052f4042ccf2759dea0293e32a1445b79 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Jul 2024 21:52:25 +0200 Subject: [PATCH 1748/2411] Bump pyhomeworks to 1.1.0 (#122870) --- homeassistant/components/homeworks/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homeworks/manifest.json b/homeassistant/components/homeworks/manifest.json index 9b447ef4aea..1ba0672c9f1 100644 --- a/homeassistant/components/homeworks/manifest.json +++ b/homeassistant/components/homeworks/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/homeworks", "iot_class": "local_push", "loggers": ["pyhomeworks"], - "requirements": ["pyhomeworks==1.0.0"] + "requirements": ["pyhomeworks==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index e0d502f3000..80f4ab6bc4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1903,7 +1903,7 @@ pyhiveapi==0.5.16 pyhomematic==0.1.77 # homeassistant.components.homeworks -pyhomeworks==1.0.0 +pyhomeworks==1.1.0 # homeassistant.components.ialarm pyialarm==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1037d5109f..fa24095e5e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1517,7 +1517,7 @@ pyhiveapi==0.5.16 pyhomematic==0.1.77 # homeassistant.components.homeworks -pyhomeworks==1.0.0 +pyhomeworks==1.1.0 # homeassistant.components.ialarm pyialarm==2.2.0 From da18aae2d89f146e49c5cbba288df250d8782e3f Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 30 Jul 2024 15:27:16 -0500 Subject: [PATCH 1749/2411] Bump intents to 2024.7.29 (#122811) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index f308ae57647..65c79cef187 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.7.10"] + "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.7.29"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b0e17bc2826..70c05f35c33 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240719.0 -home-assistant-intents==2024.7.10 +home-assistant-intents==2024.7.29 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index 80f4ab6bc4a..c9e5ad7c264 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ holidays==0.53 home-assistant-frontend==20240719.0 # homeassistant.components.conversation -home-assistant-intents==2024.7.10 +home-assistant-intents==2024.7.29 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fa24095e5e2..33c1ebcdb09 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -916,7 +916,7 @@ holidays==0.53 home-assistant-frontend==20240719.0 # homeassistant.components.conversation -home-assistant-intents==2024.7.10 +home-assistant-intents==2024.7.29 # homeassistant.components.home_connect homeconnect==0.8.0 From 6999c6b0cf9f6f37a493ff06e93f5f81bad8365e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Jul 2024 16:40:38 -0500 Subject: [PATCH 1750/2411] Bump aiohttp to 3.10.0 (#122880) --- 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 70c05f35c33..b259bef2c8b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-zlib==0.1.1 -aiohttp==3.10.0rc0 +aiohttp==3.10.0 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index eac18012ae3..e9ee54b069e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.10.0rc0", + "aiohttp==3.10.0", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index 5122cb99c41..a8edc66cb9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.10.0rc0 +aiohttp==3.10.0 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 From 067acce4deeb397717b6bb7f9ca3e5f0dab79c41 Mon Sep 17 00:00:00 2001 From: Jeef Date: Tue, 30 Jul 2024 15:42:10 -0600 Subject: [PATCH 1751/2411] Add SimpleFin sensor to show age of data (#122550) --- .../components/simplefin/manifest.json | 2 +- homeassistant/components/simplefin/sensor.py | 13 +- .../components/simplefin/strings.json | 3 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../simplefin/snapshots/test_sensor.ambr | 384 ++++++++++++++++++ 6 files changed, 401 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/simplefin/manifest.json b/homeassistant/components/simplefin/manifest.json index f3e312d9de5..a790e64c578 100644 --- a/homeassistant/components/simplefin/manifest.json +++ b/homeassistant/components/simplefin/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["simplefin"], - "requirements": ["simplefin4py==0.0.16"] + "requirements": ["simplefin4py==0.0.18"] } diff --git a/homeassistant/components/simplefin/sensor.py b/homeassistant/components/simplefin/sensor.py index 2fac42cbac5..b2167a2c014 100644 --- a/homeassistant/components/simplefin/sensor.py +++ b/homeassistant/components/simplefin/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime from simplefin4py import Account @@ -13,6 +14,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -25,7 +27,7 @@ from .entity import SimpleFinEntity class SimpleFinSensorEntityDescription(SensorEntityDescription): """Describes a sensor entity.""" - value_fn: Callable[[Account], StateType] + value_fn: Callable[[Account], StateType | datetime] icon_fn: Callable[[Account], str] | None = None unit_fn: Callable[[Account], str] | None = None @@ -40,6 +42,13 @@ SIMPLEFIN_SENSORS: tuple[SimpleFinSensorEntityDescription, ...] = ( unit_fn=lambda account: account.currency, icon_fn=lambda account: account.inferred_account_type, ), + SimpleFinSensorEntityDescription( + key="age", + translation_key="age", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda account: account.balance_date, + ), ) @@ -70,7 +79,7 @@ class SimpleFinSensor(SimpleFinEntity, SensorEntity): entity_description: SimpleFinSensorEntityDescription @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime | None: """Return the state.""" return self.entity_description.value_fn(self.account_data) diff --git a/homeassistant/components/simplefin/strings.json b/homeassistant/components/simplefin/strings.json index c54520a0451..d6690e604c5 100644 --- a/homeassistant/components/simplefin/strings.json +++ b/homeassistant/components/simplefin/strings.json @@ -24,6 +24,9 @@ "sensor": { "balance": { "name": "Balance" + }, + "age": { + "name": "Data age" } } } diff --git a/requirements_all.txt b/requirements_all.txt index c9e5ad7c264..8389d5ed2cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2580,7 +2580,7 @@ sharp_aquos_rc==0.3.2 shodan==1.28.0 # homeassistant.components.simplefin -simplefin4py==0.0.16 +simplefin4py==0.0.18 # homeassistant.components.sighthound simplehound==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33c1ebcdb09..c5ad7e9da05 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2032,7 +2032,7 @@ sfrbox-api==0.0.8 sharkiq==1.0.2 # homeassistant.components.simplefin -simplefin4py==0.0.16 +simplefin4py==0.0.18 # homeassistant.components.sighthound simplehound==0.3 diff --git a/tests/components/simplefin/snapshots/test_sensor.ambr b/tests/components/simplefin/snapshots/test_sensor.ambr index 2660bbd74ca..c7dced9300e 100644 --- a/tests/components/simplefin/snapshots/test_sensor.ambr +++ b/tests/components/simplefin/snapshots/test_sensor.ambr @@ -52,6 +52,54 @@ 'state': '1000000.00', }) # --- +# name: test_all_entities[sensor.investments_dr_evil_data_age-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.investments_dr_evil_data_age', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Data age', + 'platform': 'simplefin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'age', + 'unique_id': 'account_ACT-4k5l6m7n-8o9p-1q2r-3s4t_age', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.investments_dr_evil_data_age-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by SimpleFIN API', + 'device_class': 'timestamp', + 'friendly_name': 'Investments Dr Evil Data age', + }), + 'context': , + 'entity_id': 'sensor.investments_dr_evil_data_age', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-16T13:55:19+00:00', + }) +# --- # name: test_all_entities[sensor.investments_my_checking_balance-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -105,6 +153,54 @@ 'state': '12345.67', }) # --- +# name: test_all_entities[sensor.investments_my_checking_data_age-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.investments_my_checking_data_age', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Data age', + 'platform': 'simplefin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'age', + 'unique_id': 'account_ACT-1k2l3m4n-5o6p-7q8r-9s0t_age', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.investments_my_checking_data_age-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by SimpleFIN API', + 'device_class': 'timestamp', + 'friendly_name': 'Investments My Checking Data age', + }), + 'context': , + 'entity_id': 'sensor.investments_my_checking_data_age', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-16T13:55:19+00:00', + }) +# --- # name: test_all_entities[sensor.investments_nerdcorp_series_b_balance-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -158,6 +254,54 @@ 'state': '13579.24', }) # --- +# name: test_all_entities[sensor.investments_nerdcorp_series_b_data_age-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.investments_nerdcorp_series_b_data_age', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Data age', + 'platform': 'simplefin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'age', + 'unique_id': 'account_ACT-5k6l7m8n-9o0p-1q2r-3s4t_age', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.investments_nerdcorp_series_b_data_age-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by SimpleFIN API', + 'device_class': 'timestamp', + 'friendly_name': 'Investments NerdCorp Series B Data age', + }), + 'context': , + 'entity_id': 'sensor.investments_nerdcorp_series_b_data_age', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-16T13:55:19+00:00', + }) +# --- # name: test_all_entities[sensor.mythical_randomsavings_castle_mortgage_balance-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -211,6 +355,54 @@ 'state': '7500.00', }) # --- +# name: test_all_entities[sensor.mythical_randomsavings_castle_mortgage_data_age-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mythical_randomsavings_castle_mortgage_data_age', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Data age', + 'platform': 'simplefin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'age', + 'unique_id': 'account_ACT-7a8b9c0d-1e2f-3g4h-5i6j_age', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mythical_randomsavings_castle_mortgage_data_age-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by SimpleFIN API', + 'device_class': 'timestamp', + 'friendly_name': 'Mythical RandomSavings Castle Mortgage Data age', + }), + 'context': , + 'entity_id': 'sensor.mythical_randomsavings_castle_mortgage_data_age', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-16T18:16:42+00:00', + }) +# --- # name: test_all_entities[sensor.mythical_randomsavings_unicorn_pot_balance-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -264,6 +456,54 @@ 'state': '10000.00', }) # --- +# name: test_all_entities[sensor.mythical_randomsavings_unicorn_pot_data_age-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mythical_randomsavings_unicorn_pot_data_age', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Data age', + 'platform': 'simplefin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'age', + 'unique_id': 'account_ACT-6a7b8c9d-0e1f-2g3h-4i5j_age', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.mythical_randomsavings_unicorn_pot_data_age-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by SimpleFIN API', + 'device_class': 'timestamp', + 'friendly_name': 'Mythical RandomSavings Unicorn Pot Data age', + }), + 'context': , + 'entity_id': 'sensor.mythical_randomsavings_unicorn_pot_data_age', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-16T18:16:42+00:00', + }) +# --- # name: test_all_entities[sensor.random_bank_costco_anywhere_visa_r_card_balance-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -317,6 +557,54 @@ 'state': '-532.69', }) # --- +# name: test_all_entities[sensor.random_bank_costco_anywhere_visa_r_card_data_age-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.random_bank_costco_anywhere_visa_r_card_data_age', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Data age', + 'platform': 'simplefin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'age', + 'unique_id': 'account_ACT-3a4b5c6d-7e8f-9g0h-1i2j_age', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.random_bank_costco_anywhere_visa_r_card_data_age-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by SimpleFIN API', + 'device_class': 'timestamp', + 'friendly_name': 'Random Bank Costco Anywhere Visa® Card Data age', + }), + 'context': , + 'entity_id': 'sensor.random_bank_costco_anywhere_visa_r_card_data_age', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-16T18:16:42+00:00', + }) +# --- # name: test_all_entities[sensor.the_bank_of_go_prime_savings_balance-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -370,6 +658,54 @@ 'state': '9876.54', }) # --- +# name: test_all_entities[sensor.the_bank_of_go_prime_savings_data_age-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.the_bank_of_go_prime_savings_data_age', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Data age', + 'platform': 'simplefin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'age', + 'unique_id': 'account_ACT-2a3b4c5d-6e7f-8g9h-0i1j_age', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.the_bank_of_go_prime_savings_data_age-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by SimpleFIN API', + 'device_class': 'timestamp', + 'friendly_name': 'The Bank of Go PRIME SAVINGS Data age', + }), + 'context': , + 'entity_id': 'sensor.the_bank_of_go_prime_savings_data_age', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-16T18:14:21+00:00', + }) +# --- # name: test_all_entities[sensor.the_bank_of_go_the_bank_balance-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -423,3 +759,51 @@ 'state': '7777.77', }) # --- +# name: test_all_entities[sensor.the_bank_of_go_the_bank_data_age-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.the_bank_of_go_the_bank_data_age', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Data age', + 'platform': 'simplefin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'age', + 'unique_id': 'account_ACT-1a2b3c4d-5e6f-7g8h-9i0j_age', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.the_bank_of_go_the_bank_data_age-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by SimpleFIN API', + 'device_class': 'timestamp', + 'friendly_name': 'The Bank of Go The Bank Data age', + }), + 'context': , + 'entity_id': 'sensor.the_bank_of_go_the_bank_data_age', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-16T14:04:03+00:00', + }) +# --- From aa801d9cc6b09050c3b4e5e4cf41d1607b21ae83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Jul 2024 18:20:55 -0500 Subject: [PATCH 1752/2411] Bump bluetooth-data-tools to 1.19.4 (#122886) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/components/ld2410_ble/manifest.json | 2 +- homeassistant/components/led_ble/manifest.json | 2 +- homeassistant/components/private_ble_device/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 12bb37ac570..95d2b171c9f 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak-retry-connector==3.5.0", "bluetooth-adapters==0.19.3", "bluetooth-auto-recovery==1.4.2", - "bluetooth-data-tools==1.19.3", + "bluetooth-data-tools==1.19.4", "dbus-fast==2.22.1", "habluetooth==3.1.3" ] diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index 2389e3199e2..a1b0e9a1398 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/ld2410_ble", "integration_type": "device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.19.3", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.19.4", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index bf15ab1cc66..e22d23fb971 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -35,5 +35,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/led_ble", "iot_class": "local_polling", - "requirements": ["bluetooth-data-tools==1.19.3", "led-ble==1.0.2"] + "requirements": ["bluetooth-data-tools==1.19.4", "led-ble==1.0.2"] } diff --git a/homeassistant/components/private_ble_device/manifest.json b/homeassistant/components/private_ble_device/manifest.json index bb29e2cf105..8b072361d34 100644 --- a/homeassistant/components/private_ble_device/manifest.json +++ b/homeassistant/components/private_ble_device/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/private_ble_device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.19.3"] + "requirements": ["bluetooth-data-tools==1.19.4"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b259bef2c8b..9fca07b6202 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ bleak-retry-connector==3.5.0 bleak==0.22.2 bluetooth-adapters==0.19.3 bluetooth-auto-recovery==1.4.2 -bluetooth-data-tools==1.19.3 +bluetooth-data-tools==1.19.4 cached_ipaddress==0.3.0 certifi>=2021.5.30 ciso8601==2.3.1 diff --git a/requirements_all.txt b/requirements_all.txt index 8389d5ed2cd..f46ed9dd53a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -600,7 +600,7 @@ bluetooth-auto-recovery==1.4.2 # homeassistant.components.ld2410_ble # homeassistant.components.led_ble # homeassistant.components.private_ble_device -bluetooth-data-tools==1.19.3 +bluetooth-data-tools==1.19.4 # homeassistant.components.bond bond-async==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5ad7e9da05..2750dd4cc96 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -524,7 +524,7 @@ bluetooth-auto-recovery==1.4.2 # homeassistant.components.ld2410_ble # homeassistant.components.led_ble # homeassistant.components.private_ble_device -bluetooth-data-tools==1.19.3 +bluetooth-data-tools==1.19.4 # homeassistant.components.bond bond-async==0.2.1 From 823910b69ed9f8aab788279e4304643ee54f541c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 31 Jul 2024 00:20:09 -0500 Subject: [PATCH 1753/2411] Bump ulid-transform to 0.13.1 (#122884) * Bump ulid-transform to 0.13.0 changelog: https://github.com/bdraco/ulid-transform/compare/v0.10.1...v0.13.0 * Bump ulid-transform to 0.13.1 --- 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 9fca07b6202..43e737a002d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -55,7 +55,7 @@ PyYAML==6.0.1 requests==2.32.3 SQLAlchemy==2.0.31 typing-extensions>=4.12.2,<5.0 -ulid-transform==0.10.1 +ulid-transform==0.13.1 urllib3>=1.26.5,<2 voluptuous-openapi==0.0.5 voluptuous-serialize==2.6.0 diff --git a/pyproject.toml b/pyproject.toml index e9ee54b069e..f71e9bd6013 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ dependencies = [ "requests==2.32.3", "SQLAlchemy==2.0.31", "typing-extensions>=4.12.2,<5.0", - "ulid-transform==0.10.1", + "ulid-transform==0.13.1", # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 # Temporary setting an upper bound, to prevent compat issues with urllib3>=2 # https://github.com/home-assistant/core/issues/97248 diff --git a/requirements.txt b/requirements.txt index a8edc66cb9a..c851927f9c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ PyYAML==6.0.1 requests==2.32.3 SQLAlchemy==2.0.31 typing-extensions>=4.12.2,<5.0 -ulid-transform==0.10.1 +ulid-transform==0.13.1 urllib3>=1.26.5,<2 voluptuous==0.15.2 voluptuous-serialize==2.6.0 From 6d8bc84db3ae6374f6ac4085de25972c979ab9af Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 31 Jul 2024 08:02:15 +0200 Subject: [PATCH 1754/2411] Allow [##:##:##:##:##] type device address in homeworks (#122872) * Allow [##:##:##:##:##] type device address in homeworks * Simplify regex --- homeassistant/components/homeworks/config_flow.py | 2 +- tests/components/homeworks/test_config_flow.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeworks/config_flow.py b/homeassistant/components/homeworks/config_flow.py index ec381c3331f..4508f3bd21d 100644 --- a/homeassistant/components/homeworks/config_flow.py +++ b/homeassistant/components/homeworks/config_flow.py @@ -89,7 +89,7 @@ BUTTON_EDIT: VolDictType = { } -validate_addr = cv.matches_regex(r"\[(?:\d\d:)?\d\d:\d\d:\d\d\]") +validate_addr = cv.matches_regex(r"\[(?:\d\d:){2,4}\d\d\]") async def validate_add_controller( diff --git a/tests/components/homeworks/test_config_flow.py b/tests/components/homeworks/test_config_flow.py index 3e359caf7f2..c4738e68ecc 100644 --- a/tests/components/homeworks/test_config_flow.py +++ b/tests/components/homeworks/test_config_flow.py @@ -436,7 +436,14 @@ async def test_options_add_remove_light_flow( ) -@pytest.mark.parametrize("keypad_address", ["[02:08:03:01]", "[02:08:03]"]) +@pytest.mark.parametrize( + "keypad_address", + [ + "[02:08:03]", + "[02:08:03:01]", + "[02:08:03:01:00]", + ], +) async def test_options_add_remove_keypad_flow( hass: HomeAssistant, mock_config_entry: MockConfigEntry, From 5766ea9541b998586fcf742340fdba0ed25f671b Mon Sep 17 00:00:00 2001 From: Lukas Kolletzki Date: Wed, 31 Jul 2024 08:26:57 +0200 Subject: [PATCH 1755/2411] Add generic URL handler to blueprint importer (#110576) * Add generic url handler to blueprint importer * Update tests/components/blueprint/test_importer.py * Update tests/components/blueprint/test_importer.py * Update test_importer.py --------- Co-authored-by: Erik Montnemery --- .../components/blueprint/importer.py | 34 +++++++++++++++---- tests/components/blueprint/test_importer.py | 29 +++++++++++++--- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index 4517d134e69..c231a33991a 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -245,14 +245,36 @@ async def fetch_blueprint_from_website_url( return ImportedBlueprint(suggested_filename, raw_yaml, blueprint) +async def fetch_blueprint_from_generic_url( + hass: HomeAssistant, url: str +) -> ImportedBlueprint: + """Get a blueprint from a generic website.""" + session = aiohttp_client.async_get_clientsession(hass) + + resp = await session.get(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(url) + suggested_filename = f"{parsed_import_url.host}/{parsed_import_url.parts[-1][:-5]}" + return ImportedBlueprint(suggested_filename, raw_yaml, blueprint) + + +FETCH_FUNCTIONS = ( + fetch_blueprint_from_community_post, + fetch_blueprint_from_github_url, + fetch_blueprint_from_github_gist_url, + fetch_blueprint_from_website_url, + fetch_blueprint_from_generic_url, +) + + async def fetch_blueprint_from_url(hass: HomeAssistant, url: str) -> ImportedBlueprint: """Get a blueprint from a url.""" - for func in ( - fetch_blueprint_from_community_post, - fetch_blueprint_from_github_url, - fetch_blueprint_from_github_gist_url, - fetch_blueprint_from_website_url, - ): + for func in FETCH_FUNCTIONS: with suppress(UnsupportedUrl): imported_bp = await func(hass, url) imported_bp.blueprint.update_metadata(source_url=url) diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index f135bbf23b8..94036d208ab 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -192,9 +192,28 @@ async def test_fetch_blueprint_from_website_url( assert imported_blueprint.blueprint.metadata["source_url"] == url -async def test_fetch_blueprint_from_unsupported_url(hass: HomeAssistant) -> None: - """Test fetching blueprint from an unsupported URL.""" - url = "https://example.com/unsupported.yaml" +async def test_fetch_blueprint_from_generic_url( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test fetching blueprint from url.""" + aioclient_mock.get( + "https://example.org/path/someblueprint.yaml", + text=Path( + hass.config.path("blueprints/automation/test_event_service.yaml") + ).read_text(encoding="utf8"), + ) - with pytest.raises(HomeAssistantError, match=r"^Unsupported URL$"): - await importer.fetch_blueprint_from_url(hass, url) + url = "https://example.org/path/someblueprint.yaml" + imported_blueprint = await importer.fetch_blueprint_from_url(hass, url) + assert isinstance(imported_blueprint, importer.ImportedBlueprint) + assert imported_blueprint.blueprint.domain == "automation" + assert imported_blueprint.suggested_filename == "example.org/someblueprint" + assert imported_blueprint.blueprint.metadata["source_url"] == url + + +def test_generic_importer_last() -> None: + """Test that generic importer is always the last one.""" + assert ( + importer.FETCH_FUNCTIONS.count(importer.fetch_blueprint_from_generic_url) == 1 + ) + assert importer.FETCH_FUNCTIONS[-1] == importer.fetch_blueprint_from_generic_url From 0d678120e42471710da358adacbb24452bac58e8 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Wed, 31 Jul 2024 08:28:39 +0200 Subject: [PATCH 1756/2411] Bump aioautomower to 2024.7.3 (#121983) * Bump aioautomower to 2024.7.0 * tests * Bump to 2024.7.1 * bump to 2024.7.2 * use timezone Europe/Berlin * bump to 2024.7.3 --------- Co-authored-by: Joost Lekkerkerker Co-authored-by: Erik Montnemery --- .../husqvarna_automower/manifest.json | 2 +- .../components/husqvarna_automower/sensor.py | 8 +++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../husqvarna_automower/__init__.py | 6 +++- .../snapshots/test_diagnostics.ambr | 28 +++++++++++++++---- .../snapshots/test_sensor.ambr | 2 +- .../husqvarna_automower/test_sensor.py | 4 +-- 8 files changed, 40 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index f27b04ef0c0..bb03806e417 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.6.4"] + "requirements": ["aioautomower==2024.7.3"] } diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index 2c8d369ea3a..bd0b8561223 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -5,8 +5,10 @@ from dataclasses import dataclass from datetime import datetime import logging from typing import TYPE_CHECKING +from zoneinfo import ZoneInfo from aioautomower.model import MowerAttributes, MowerModes, RestrictedReasons +from aioautomower.utils import naive_to_aware from homeassistant.components.sensor import ( SensorDeviceClass, @@ -18,6 +20,7 @@ from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfLength, UnitOf from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.util import dt as dt_util from . import AutomowerConfigEntry from .coordinator import AutomowerDataUpdateCoordinator @@ -324,7 +327,10 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( key="next_start_timestamp", translation_key="next_start_timestamp", device_class=SensorDeviceClass.TIMESTAMP, - value_fn=lambda data: data.planner.next_start_datetime, + value_fn=lambda data: naive_to_aware( + data.planner.next_start_datetime_naive, + ZoneInfo(str(dt_util.DEFAULT_TIME_ZONE)), + ), ), AutomowerSensorEntityDescription( key="error", diff --git a/requirements_all.txt b/requirements_all.txt index f46ed9dd53a..8aebf250fd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -198,7 +198,7 @@ aioaseko==0.2.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.4 +aioautomower==2024.7.3 # homeassistant.components.azure_devops aioazuredevops==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2750dd4cc96..859154b2837 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -186,7 +186,7 @@ aioaseko==0.2.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.6.4 +aioautomower==2024.7.3 # homeassistant.components.azure_devops aioazuredevops==2.1.1 diff --git a/tests/components/husqvarna_automower/__init__.py b/tests/components/husqvarna_automower/__init__.py index 8c51d69ba3d..9473b68a5ed 100644 --- a/tests/components/husqvarna_automower/__init__.py +++ b/tests/components/husqvarna_automower/__init__.py @@ -7,6 +7,10 @@ from tests.common import MockConfigEntry async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: """Fixture for setting up the component.""" + # We lock the timezone, because the timezone is passed to the library to generate + # some values like the next start sensor. This is needed, as the device is not aware + # of its own timezone. So we assume the device is in the timezone which is selected in + # the Home Assistant config. + await hass.config.async_set_time_zone("Europe/Berlin") config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr b/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr index d8cd748c793..212be85ce51 100644 --- a/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr @@ -7,18 +7,20 @@ 'calendar': dict({ 'events': list([ dict({ - 'end': '2024-03-02T00:00:00+00:00', + 'end': '2024-03-02T00:00:00', 'rrule': 'FREQ=WEEKLY;BYDAY=MO,WE,FR', - 'start': '2024-03-01T19:00:00+00:00', + 'start': '2024-03-01T19:00:00', 'uid': '1140_300_MO,WE,FR', 'work_area_id': None, + 'work_area_name': None, }), dict({ - 'end': '2024-03-02T08:00:00+00:00', + 'end': '2024-03-02T08:00:00', 'rrule': 'FREQ=WEEKLY;BYDAY=TU,TH,SA', - 'start': '2024-03-02T00:00:00+00:00', + 'start': '2024-03-02T00:00:00', 'uid': '0_480_TU,TH,SA', 'work_area_id': None, + 'work_area_name': None, }), ]), 'tasks': list([ @@ -33,6 +35,7 @@ 'tuesday': False, 'wednesday': True, 'work_area_id': None, + 'work_area_name': None, }), dict({ 'duration': 480, @@ -45,6 +48,7 @@ 'tuesday': True, 'wednesday': False, 'work_area_id': None, + 'work_area_name': None, }), ]), }), @@ -61,17 +65,18 @@ 'mower': dict({ 'activity': 'PARKED_IN_CS', 'error_code': 0, - 'error_datetime': None, 'error_datetime_naive': None, 'error_key': None, + 'error_timestamp': 0, 'inactive_reason': 'NONE', 'is_error_confirmable': False, 'mode': 'MAIN_AREA', 'state': 'RESTRICTED', 'work_area_id': 123456, + 'work_area_name': 'Front lawn', }), 'planner': dict({ - 'next_start_datetime': '2023-06-05T19:00:00+00:00', + 'next_start': 1685991600000, 'next_start_datetime_naive': '2023-06-05T19:00:00', 'override': dict({ 'action': 'NOT_ACTIVE', @@ -113,6 +118,17 @@ 'name': 'Test Mower 1', 'serial_number': 123, }), + 'work_area_dict': dict({ + '0': 'my_lawn', + '123456': 'Front lawn', + '654321': 'Back lawn', + }), + 'work_area_names': list([ + 'Front lawn', + 'Back lawn', + 'my_lawn', + 'no_work_area_active', + ]), 'work_areas': dict({ '0': dict({ 'cutting_height': 50, diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index 935303e48fb..730971a47dd 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -548,7 +548,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2023-06-05T19:00:00+00:00', + 'state': '2023-06-05T17:00:00+00:00', }) # --- # name: test_sensor_snapshot[sensor.test_mower_1_none-entry] diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index 314bcaaa00c..1a4f545ac96 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -73,12 +73,12 @@ async def test_next_start_sensor( await setup_integration(hass, mock_config_entry) state = hass.states.get("sensor.test_mower_1_next_start") assert state is not None - assert state.state == "2023-06-05T19:00:00+00:00" + assert state.state == "2023-06-05T17:00:00+00:00" values = mower_list_to_dictionary_dataclass( load_json_value_fixture("mower.json", DOMAIN) ) - values[TEST_MOWER_ID].planner.next_start_datetime = None + values[TEST_MOWER_ID].planner.next_start_datetime_naive = None mock_automower_client.get_status.return_value = values freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) From 9351f300b037c680e606bb4886751010993139ac Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 31 Jul 2024 09:10:36 +0200 Subject: [PATCH 1757/2411] Update xknx to 3.0.0 - more DPT definitions (#122891) * Support DPTComplex objects and validate sensor types * Gracefully start and stop xknx device objects * Use non-awaitable XknxDevice callbacks * Use non-awaitable xknx.TelegramQueue callbacks * Use non-awaitable xknx.ConnectionManager callbacks * Remove unnecessary `hass.async_block_till_done()` calls * Wait for StateUpdater logic to proceed when receiving responses * Update import module paths for specific DPTs * Support Enum data types * New HVAC mode names * HVAC Enums instead of Enum member value strings * New date and time devices * Update xknx to 3.0.0 * Fix expose tests and DPTEnumData check * ruff and mypy fixes --- homeassistant/components/knx/__init__.py | 16 +++--- homeassistant/components/knx/binary_sensor.py | 2 +- homeassistant/components/knx/climate.py | 43 ++++++++++------ homeassistant/components/knx/const.py | 23 +++++---- homeassistant/components/knx/date.py | 32 ++++-------- homeassistant/components/knx/datetime.py | 40 ++++++--------- homeassistant/components/knx/device.py | 5 +- homeassistant/components/knx/expose.py | 50 +++++++++++-------- homeassistant/components/knx/knx_entity.py | 12 +++-- homeassistant/components/knx/light.py | 6 +-- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/schema.py | 19 ++++--- homeassistant/components/knx/select.py | 13 ++--- homeassistant/components/knx/sensor.py | 2 +- homeassistant/components/knx/services.py | 27 ++++++---- homeassistant/components/knx/telegrams.py | 8 ++- homeassistant/components/knx/time.py | 33 ++++-------- homeassistant/components/knx/trigger.py | 6 +-- homeassistant/components/knx/validation.py | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/knx/conftest.py | 6 +-- tests/components/knx/test_binary_sensor.py | 13 ----- tests/components/knx/test_climate.py | 49 +++++++----------- tests/components/knx/test_config_flow.py | 16 ------ tests/components/knx/test_datetime.py | 5 +- tests/components/knx/test_device_trigger.py | 1 - tests/components/knx/test_events.py | 3 -- tests/components/knx/test_expose.py | 22 +++++--- tests/components/knx/test_init.py | 1 - tests/components/knx/test_interface_device.py | 11 +--- tests/components/knx/test_light.py | 4 +- tests/components/knx/test_notify.py | 9 ---- tests/components/knx/test_sensor.py | 4 -- tests/components/knx/test_services.py | 4 -- tests/components/knx/test_trigger.py | 1 - tests/components/knx/test_weather.py | 8 +-- 37 files changed, 217 insertions(+), 286 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index f7e9b161962..99b461dda1b 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio import contextlib import logging from pathlib import Path @@ -225,7 +224,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: knx_module: KNXModule = hass.data[DOMAIN] for exposure in knx_module.exposures: - exposure.shutdown() + exposure.async_remove() unload_ok = await hass.config_entries.async_unload_platforms( entry, @@ -439,13 +438,13 @@ class KNXModule: threaded=True, ) - async def connection_state_changed_cb(self, state: XknxConnectionState) -> None: + def connection_state_changed_cb(self, state: XknxConnectionState) -> None: """Call invoked after a KNX connection state change was received.""" self.connected = state == XknxConnectionState.CONNECTED - if tasks := [device.after_update() for device in self.xknx.devices]: - await asyncio.gather(*tasks) + for device in self.xknx.devices: + device.after_update() - async def telegram_received_cb(self, telegram: Telegram) -> None: + def telegram_received_cb(self, telegram: Telegram) -> None: """Call invoked after a KNX telegram was received.""" # Not all telegrams have serializable data. data: int | tuple[int, ...] | None = None @@ -504,10 +503,7 @@ class KNXModule: transcoder := DPTBase.parse_transcoder(dpt) ): self._address_filter_transcoder.update( - { - _filter: transcoder # type: ignore[type-abstract] - for _filter in _filters - } + {_filter: transcoder for _filter in _filters} ) return self.xknx.telegram_queue.register_telegram_received_cb( diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index dee56608421..0423c1d7b32 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -75,7 +75,7 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): if ( last_state := await self.async_get_last_state() ) and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - await self._device.remote_value.update_value(last_state.state == STATE_ON) + self._device.remote_value.update_value(last_state.state == STATE_ON) @property def is_on(self) -> bool: diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index e1179641cdc..26be6a03a79 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -6,7 +6,7 @@ from typing import Any from xknx import XKNX from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode -from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode +from xknx.dpt.dpt_20 import HVACControllerMode from homeassistant import config_entries from homeassistant.components.climate import ( @@ -80,7 +80,7 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate: group_address_operation_mode_protection=config.get( ClimateSchema.CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS ), - group_address_operation_mode_night=config.get( + group_address_operation_mode_economy=config.get( ClimateSchema.CONF_OPERATION_MODE_NIGHT_ADDRESS ), group_address_operation_mode_comfort=config.get( @@ -199,10 +199,12 @@ class KNXClimate(KnxEntity, ClimateEntity): self.async_write_ha_state() return - if self._device.mode is not None and self._device.mode.supports_controller_mode: - knx_controller_mode = HVACControllerMode( - CONTROLLER_MODES_INV.get(self._last_hvac_mode) - ) + if ( + self._device.mode is not None + and self._device.mode.supports_controller_mode + and (knx_controller_mode := CONTROLLER_MODES_INV.get(self._last_hvac_mode)) + is not None + ): await self._device.mode.set_controller_mode(knx_controller_mode) self.async_write_ha_state() @@ -234,7 +236,7 @@ class KNXClimate(KnxEntity, ClimateEntity): return HVACMode.OFF if self._device.mode is not None and self._device.mode.supports_controller_mode: hvac_mode = CONTROLLER_MODES.get( - self._device.mode.controller_mode.value, self.default_hvac_mode + self._device.mode.controller_mode, self.default_hvac_mode ) if hvac_mode is not HVACMode.OFF: self._last_hvac_mode = hvac_mode @@ -247,7 +249,7 @@ class KNXClimate(KnxEntity, ClimateEntity): ha_controller_modes: list[HVACMode | None] = [] if self._device.mode is not None: ha_controller_modes.extend( - CONTROLLER_MODES.get(knx_controller_mode.value) + CONTROLLER_MODES.get(knx_controller_mode) for knx_controller_mode in self._device.mode.controller_modes ) @@ -278,9 +280,7 @@ class KNXClimate(KnxEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set controller mode.""" if self._device.mode is not None and self._device.mode.supports_controller_mode: - knx_controller_mode = HVACControllerMode( - CONTROLLER_MODES_INV.get(hvac_mode) - ) + knx_controller_mode = CONTROLLER_MODES_INV.get(hvac_mode) if knx_controller_mode in self._device.mode.controller_modes: await self._device.mode.set_controller_mode(knx_controller_mode) @@ -298,7 +298,7 @@ class KNXClimate(KnxEntity, ClimateEntity): Requires ClimateEntityFeature.PRESET_MODE. """ if self._device.mode is not None and self._device.mode.supports_operation_mode: - return PRESET_MODES.get(self._device.mode.operation_mode.value, PRESET_AWAY) + return PRESET_MODES.get(self._device.mode.operation_mode, PRESET_AWAY) return None @property @@ -311,15 +311,18 @@ class KNXClimate(KnxEntity, ClimateEntity): return None presets = [ - PRESET_MODES.get(operation_mode.value) + PRESET_MODES.get(operation_mode) for operation_mode in self._device.mode.operation_modes ] return list(filter(None, presets)) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if self._device.mode is not None and self._device.mode.supports_operation_mode: - knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode)) + if ( + self._device.mode is not None + and self._device.mode.supports_operation_mode + and (knx_operation_mode := PRESET_MODES_INV.get(preset_mode)) is not None + ): await self._device.mode.set_operation_mode(knx_operation_mode) self.async_write_ha_state() @@ -333,7 +336,15 @@ class KNXClimate(KnxEntity, ClimateEntity): return attr async def async_added_to_hass(self) -> None: - """Store register state change callback.""" + """Store register state change callback and start device object.""" await super().async_added_to_hass() if self._device.mode is not None: self._device.mode.register_device_updated_cb(self.after_update_callback) + self._device.mode.xknx.devices.async_add(self._device.mode) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect device object when removed.""" + if self._device.mode is not None: + self._device.mode.unregister_device_updated_cb(self.after_update_callback) + self._device.mode.xknx.devices.async_remove(self._device.mode) + await super().async_will_remove_from_hass() diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 0b7b517dca5..6400f0fe466 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -6,6 +6,7 @@ from collections.abc import Awaitable, Callable from enum import Enum from typing import Final, TypedDict +from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode from xknx.telegram import Telegram from homeassistant.components.climate import ( @@ -158,12 +159,12 @@ SUPPORTED_PLATFORMS_UI: Final = {Platform.SWITCH, Platform.LIGHT} # Map KNX controller modes to HA modes. This list might not be complete. CONTROLLER_MODES: Final = { # Map DPT 20.105 HVAC control modes - "Auto": HVACMode.AUTO, - "Heat": HVACMode.HEAT, - "Cool": HVACMode.COOL, - "Off": HVACMode.OFF, - "Fan only": HVACMode.FAN_ONLY, - "Dry": HVACMode.DRY, + HVACControllerMode.AUTO: HVACMode.AUTO, + HVACControllerMode.HEAT: HVACMode.HEAT, + HVACControllerMode.COOL: HVACMode.COOL, + HVACControllerMode.OFF: HVACMode.OFF, + HVACControllerMode.FAN_ONLY: HVACMode.FAN_ONLY, + HVACControllerMode.DEHUMIDIFICATION: HVACMode.DRY, } CURRENT_HVAC_ACTIONS: Final = { @@ -176,9 +177,9 @@ CURRENT_HVAC_ACTIONS: Final = { PRESET_MODES: Final = { # Map DPT 20.102 HVAC operating modes to HA presets - "Auto": PRESET_NONE, - "Frost Protection": PRESET_ECO, - "Night": PRESET_SLEEP, - "Standby": PRESET_AWAY, - "Comfort": PRESET_COMFORT, + HVACOperationMode.AUTO: PRESET_NONE, + HVACOperationMode.BUILDING_PROTECTION: PRESET_ECO, + HVACOperationMode.ECONOMY: PRESET_SLEEP, + HVACOperationMode.STANDBY: PRESET_AWAY, + HVACOperationMode.COMFORT: PRESET_COMFORT, } diff --git a/homeassistant/components/knx/date.py b/homeassistant/components/knx/date.py index fa20a8d04c5..98cd22e0751 100644 --- a/homeassistant/components/knx/date.py +++ b/homeassistant/components/knx/date.py @@ -3,11 +3,10 @@ from __future__ import annotations from datetime import date as dt_date -import time -from typing import Final from xknx import XKNX -from xknx.devices import DateTime as XknxDateTime +from xknx.devices import DateDevice as XknxDateDevice +from xknx.dpt.dpt_11 import KNXDate as XKNXDate from homeassistant import config_entries from homeassistant.components.date import DateEntity @@ -33,8 +32,6 @@ from .const import ( ) from .knx_entity import KnxEntity -_DATE_TRANSLATION_FORMAT: Final = "%Y-%m-%d" - async def async_setup_entry( hass: HomeAssistant, @@ -45,15 +42,14 @@ async def async_setup_entry( xknx: XKNX = hass.data[DOMAIN].xknx config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATE] - async_add_entities(KNXDate(xknx, entity_config) for entity_config in config) + async_add_entities(KNXDateEntity(xknx, entity_config) for entity_config in config) -def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTime: +def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateDevice: """Return a XKNX DateTime object to be used within XKNX.""" - return XknxDateTime( + return XknxDateDevice( xknx, name=config[CONF_NAME], - broadcast_type="DATE", localtime=False, group_address=config[KNX_ADDRESS], group_address_state=config.get(CONF_STATE_ADDRESS), @@ -62,10 +58,10 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTime: ) -class KNXDate(KnxEntity, DateEntity, RestoreEntity): +class KNXDateEntity(KnxEntity, DateEntity, RestoreEntity): """Representation of a KNX date.""" - _device: XknxDateTime + _device: XknxDateDevice def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize a KNX time.""" @@ -81,21 +77,15 @@ class KNXDate(KnxEntity, DateEntity, RestoreEntity): and (last_state := await self.async_get_last_state()) is not None and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) ): - self._device.remote_value.value = time.strptime( - last_state.state, _DATE_TRANSLATION_FORMAT + self._device.remote_value.value = XKNXDate.from_date( + dt_date.fromisoformat(last_state.state) ) @property def native_value(self) -> dt_date | None: """Return the latest value.""" - if (time_struct := self._device.remote_value.value) is None: - return None - return dt_date( - year=time_struct.tm_year, - month=time_struct.tm_mon, - day=time_struct.tm_mday, - ) + return self._device.value async def async_set_value(self, value: dt_date) -> None: """Change the value.""" - await self._device.set(value.timetuple()) + await self._device.set(value) diff --git a/homeassistant/components/knx/datetime.py b/homeassistant/components/knx/datetime.py index 2a1a9e2f9c9..d4a25b522eb 100644 --- a/homeassistant/components/knx/datetime.py +++ b/homeassistant/components/knx/datetime.py @@ -5,7 +5,8 @@ from __future__ import annotations from datetime import datetime from xknx import XKNX -from xknx.devices import DateTime as XknxDateTime +from xknx.devices import DateTimeDevice as XknxDateTimeDevice +from xknx.dpt.dpt_19 import KNXDateTime as XKNXDateTime from homeassistant import config_entries from homeassistant.components.datetime import DateTimeEntity @@ -42,15 +43,16 @@ async def async_setup_entry( xknx: XKNX = hass.data[DOMAIN].xknx config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATETIME] - async_add_entities(KNXDateTime(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXDateTimeEntity(xknx, entity_config) for entity_config in config + ) -def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTime: +def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTimeDevice: """Return a XKNX DateTime object to be used within XKNX.""" - return XknxDateTime( + return XknxDateTimeDevice( xknx, name=config[CONF_NAME], - broadcast_type="DATETIME", localtime=False, group_address=config[KNX_ADDRESS], group_address_state=config.get(CONF_STATE_ADDRESS), @@ -59,10 +61,10 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTime: ) -class KNXDateTime(KnxEntity, DateTimeEntity, RestoreEntity): +class KNXDateTimeEntity(KnxEntity, DateTimeEntity, RestoreEntity): """Representation of a KNX datetime.""" - _device: XknxDateTime + _device: XknxDateTimeDevice def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize a KNX time.""" @@ -78,29 +80,19 @@ class KNXDateTime(KnxEntity, DateTimeEntity, RestoreEntity): and (last_state := await self.async_get_last_state()) is not None and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) ): - self._device.remote_value.value = ( - datetime.fromisoformat(last_state.state) - .astimezone(dt_util.get_default_time_zone()) - .timetuple() + self._device.remote_value.value = XKNXDateTime.from_datetime( + datetime.fromisoformat(last_state.state).astimezone( + dt_util.get_default_time_zone() + ) ) @property def native_value(self) -> datetime | None: """Return the latest value.""" - if (time_struct := self._device.remote_value.value) is None: + if (naive_dt := self._device.value) is None: return None - return datetime( - year=time_struct.tm_year, - month=time_struct.tm_mon, - day=time_struct.tm_mday, - hour=time_struct.tm_hour, - minute=time_struct.tm_min, - second=min(time_struct.tm_sec, 59), # account for leap seconds - tzinfo=dt_util.get_default_time_zone(), - ) + return naive_dt.replace(tzinfo=dt_util.get_default_time_zone()) async def async_set_value(self, value: datetime) -> None: """Change the value.""" - await self._device.set( - value.astimezone(dt_util.get_default_time_zone()).timetuple() - ) + await self._device.set(value.astimezone(dt_util.get_default_time_zone())) diff --git a/homeassistant/components/knx/device.py b/homeassistant/components/knx/device.py index fd5abc6a072..b43b5926d86 100644 --- a/homeassistant/components/knx/device.py +++ b/homeassistant/components/knx/device.py @@ -19,6 +19,7 @@ class KNXInterfaceDevice: def __init__(self, hass: HomeAssistant, entry: ConfigEntry, xknx: XKNX) -> None: """Initialize interface device class.""" + self.hass = hass self.device_registry = dr.async_get(hass) self.gateway_descriptor: GatewayDescriptor | None = None self.xknx = xknx @@ -46,7 +47,7 @@ class KNXInterfaceDevice: else None, ) - async def connection_state_changed_cb(self, state: XknxConnectionState) -> None: + def connection_state_changed_cb(self, state: XknxConnectionState) -> None: """Call invoked after a KNX connection state change was received.""" if state is XknxConnectionState.CONNECTED: - await self.update() + self.hass.async_create_task(self.update()) diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 695fe3b3851..29d0be998b6 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -6,7 +6,7 @@ from collections.abc import Callable import logging from xknx import XKNX -from xknx.devices import DateTime, ExposeSensor +from xknx.devices import DateDevice, DateTimeDevice, ExposeSensor, TimeDevice from xknx.dpt import DPTNumeric, DPTString from xknx.exceptions import ConversionError from xknx.remote_value import RemoteValueSensor @@ -60,6 +60,7 @@ def create_knx_exposure( xknx=xknx, config=config, ) + exposure.async_register() return exposure @@ -87,25 +88,23 @@ class KNXExposeSensor: self.value_template.hass = hass self._remove_listener: Callable[[], None] | None = None - self.device: ExposeSensor = self.async_register(config) - self._init_expose_state() - - @callback - def async_register(self, config: ConfigType) -> ExposeSensor: - """Register listener.""" - name = f"{self.entity_id}__{self.expose_attribute or "state"}" - device = ExposeSensor( + self.device: ExposeSensor = ExposeSensor( xknx=self.xknx, - name=name, + name=f"{self.entity_id}__{self.expose_attribute or "state"}", group_address=config[KNX_ADDRESS], respond_to_read=config[CONF_RESPOND_TO_READ], value_type=self.expose_type, cooldown=config[ExposeSchema.CONF_KNX_EXPOSE_COOLDOWN], ) + + @callback + def async_register(self) -> None: + """Register listener.""" self._remove_listener = async_track_state_change_event( self.hass, [self.entity_id], self._async_entity_changed ) - return device + self.xknx.devices.async_add(self.device) + self._init_expose_state() @callback def _init_expose_state(self) -> None: @@ -118,12 +117,12 @@ class KNXExposeSensor: _LOGGER.exception("Error during sending of expose sensor value") @callback - def shutdown(self) -> None: + def async_remove(self) -> None: """Prepare for deletion.""" if self._remove_listener is not None: self._remove_listener() self._remove_listener = None - self.device.shutdown() + self.xknx.devices.async_remove(self.device) def _get_expose_value(self, state: State | None) -> bool | int | float | str | None: """Extract value from state.""" @@ -196,21 +195,28 @@ class KNXExposeTime: def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of Expose class.""" self.xknx = xknx - self.device: DateTime = self.async_register(config) - - @callback - def async_register(self, config: ConfigType) -> DateTime: - """Register listener.""" expose_type = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE] - return DateTime( + xknx_device_cls: type[DateDevice | DateTimeDevice | TimeDevice] + match expose_type: + case ExposeSchema.CONF_DATE: + xknx_device_cls = DateDevice + case ExposeSchema.CONF_DATETIME: + xknx_device_cls = DateTimeDevice + case ExposeSchema.CONF_TIME: + xknx_device_cls = TimeDevice + self.device = xknx_device_cls( self.xknx, name=expose_type.capitalize(), - broadcast_type=expose_type.upper(), localtime=True, group_address=config[KNX_ADDRESS], ) @callback - def shutdown(self) -> None: + def async_register(self) -> None: + """Register listener.""" + self.xknx.devices.async_add(self.device) + + @callback + def async_remove(self) -> None: """Prepare for deletion.""" - self.device.shutdown() + self.xknx.devices.async_remove(self.device) diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index b03c59486e5..eebddbb0623 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -36,12 +36,16 @@ class KnxEntity(Entity): """Request a state update from KNX bus.""" await self._device.sync() - async def after_update_callback(self, device: XknxDevice) -> None: + def after_update_callback(self, _device: XknxDevice) -> None: """Call after device was updated.""" self.async_write_ha_state() async def async_added_to_hass(self) -> None: - """Store register state change callback.""" + """Store register state change callback and start device object.""" self._device.register_device_updated_cb(self.after_update_callback) - # will remove all callbacks and xknx tasks - self.async_on_remove(self._device.shutdown) + self._device.xknx.devices.async_add(self._device) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect device object when removed.""" + self._device.unregister_device_updated_cb(self.after_update_callback) + self._device.xknx.devices.async_remove(self._device) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 425640a9915..8ec42f3ee56 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -312,8 +312,7 @@ class _KnxLight(KnxEntity, LightEntity): if self._device.supports_brightness: return self._device.current_brightness if self._device.current_xyy_color is not None: - _, brightness = self._device.current_xyy_color - return brightness + return self._device.current_xyy_color.brightness if self._device.supports_color or self._device.supports_rgbw: rgb, white = self._device.current_color if rgb is None: @@ -363,8 +362,7 @@ class _KnxLight(KnxEntity, LightEntity): def xy_color(self) -> tuple[float, float] | None: """Return the xy color value [float, float].""" if self._device.current_xyy_color is not None: - xy_color, _ = self._device.current_xyy_color - return xy_color + return self._device.current_xyy_color.color return None @property diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 5035239d1fb..0f96970f3ae 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -11,7 +11,7 @@ "loggers": ["xknx", "xknxproject"], "quality_scale": "platinum", "requirements": [ - "xknx==2.12.2", + "xknx==3.0.0", "xknxproject==3.7.1", "knx-frontend==2024.7.25.204106" ], diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 34a145eadb3..43037ad8188 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -9,6 +9,7 @@ from typing import ClassVar, Final import voluptuous as vol from xknx.devices.climate import SetpointShiftMode from xknx.dpt import DPTBase, DPTNumeric +from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode from xknx.exceptions import ConversionError, CouldNotParseTelegram from homeassistant.components.binary_sensor import ( @@ -51,12 +52,11 @@ from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, CONF_SYNC_STATE, - CONTROLLER_MODES, KNX_ADDRESS, - PRESET_MODES, ColorTempModes, ) from .validation import ( + dpt_base_type_validator, ga_list_validator, ga_validator, numeric_type_validator, @@ -173,7 +173,7 @@ class EventSchema: KNX_EVENT_FILTER_SCHEMA = vol.Schema( { vol.Required(KNX_ADDRESS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_TYPE): sensor_type_validator, + vol.Optional(CONF_TYPE): dpt_base_type_validator, } ) @@ -409,10 +409,10 @@ class ClimateSchema(KNXPlatformSchema): CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT ): cv.boolean, vol.Optional(CONF_OPERATION_MODES): vol.All( - cv.ensure_list, [vol.In(PRESET_MODES)] + cv.ensure_list, [vol.All(vol.Upper, cv.enum(HVACOperationMode))] ), vol.Optional(CONF_CONTROLLER_MODES): vol.All( - cv.ensure_list, [vol.In(CONTROLLER_MODES)] + cv.ensure_list, [vol.All(vol.Upper, cv.enum(HVACControllerMode))] ), vol.Optional( CONF_DEFAULT_CONTROLLER_MODE, default=HVACMode.HEAT @@ -535,11 +535,10 @@ class ExposeSchema(KNXPlatformSchema): CONF_KNX_EXPOSE_BINARY = "binary" CONF_KNX_EXPOSE_COOLDOWN = "cooldown" CONF_KNX_EXPOSE_DEFAULT = "default" - EXPOSE_TIME_TYPES: Final = [ - "time", - "date", - "datetime", - ] + CONF_TIME = "time" + CONF_DATE = "date" + CONF_DATETIME = "datetime" + EXPOSE_TIME_TYPES: Final = [CONF_TIME, CONF_DATE, CONF_DATETIME] EXPOSE_TIME_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index 5d7532e0e5d..f338bf9feaf 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -81,17 +81,18 @@ class KNXSelect(KnxEntity, SelectEntity, RestoreEntity): if not self._device.remote_value.readable and ( last_state := await self.async_get_last_state() ): - if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - await self._device.remote_value.update_value( - self._option_payloads.get(last_state.state) - ) + if ( + last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) + and (option := self._option_payloads.get(last_state.state)) is not None + ): + self._device.remote_value.update_value(option) - async def after_update_callback(self, device: XknxDevice) -> None: + def after_update_callback(self, device: XknxDevice) -> None: """Call after device was updated.""" self._attr_current_option = self.option_from_payload( self._device.remote_value.value ) - await super().after_update_callback(device) + super().after_update_callback(device) def option_from_payload(self, payload: int | None) -> str | None: """Return the option a given payload is assigned to.""" diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 173979f78dc..5a09a921901 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -208,7 +208,7 @@ class KNXSystemSensor(SensorEntity): return True return self.knx.xknx.connection_manager.state is XknxConnectionState.CONNECTED - async def after_update_callback(self, _: XknxConnectionState) -> None: + def after_update_callback(self, _: XknxConnectionState) -> None: """Call after device was updated.""" self.async_write_ha_state() diff --git a/homeassistant/components/knx/services.py b/homeassistant/components/knx/services.py index 24b9452cf60..8b82671deaa 100644 --- a/homeassistant/components/knx/services.py +++ b/homeassistant/components/knx/services.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING import voluptuous as vol from xknx.dpt import DPTArray, DPTBase, DPTBinary +from xknx.exceptions import ConversionError from xknx.telegram import Telegram from xknx.telegram.address import parse_device_group_address from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite @@ -31,7 +32,7 @@ from .const import ( SERVICE_KNX_SEND, ) from .expose import create_knx_exposure -from .schema import ExposeSchema, ga_validator, sensor_type_validator +from .schema import ExposeSchema, dpt_base_type_validator, ga_validator if TYPE_CHECKING: from . import KNXModule @@ -95,7 +96,7 @@ SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema( cv.ensure_list, [ga_validator], ), - vol.Optional(CONF_TYPE): sensor_type_validator, + vol.Optional(CONF_TYPE): dpt_base_type_validator, vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean, } ) @@ -125,10 +126,7 @@ async def service_event_register_modify(hass: HomeAssistant, call: ServiceCall) transcoder := DPTBase.parse_transcoder(dpt) ): knx_module.group_address_transcoder.update( - { - _address: transcoder # type: ignore[type-abstract] - for _address in group_addresses - } + {_address: transcoder for _address in group_addresses} ) for group_address in group_addresses: if group_address in knx_module.knx_event_callback.group_addresses: @@ -173,7 +171,7 @@ async def service_exposure_register_modify( f"Could not find exposure for '{group_address}' to remove." ) from err - removed_exposure.shutdown() + removed_exposure.async_remove() return if group_address in knx_module.service_exposures: @@ -186,7 +184,7 @@ async def service_exposure_register_modify( group_address, replaced_exposure.device.name, ) - replaced_exposure.shutdown() + replaced_exposure.async_remove() exposure = create_knx_exposure(knx_module.hass, knx_module.xknx, call.data) knx_module.service_exposures[group_address] = exposure _LOGGER.debug( @@ -204,7 +202,7 @@ SERVICE_KNX_SEND_SCHEMA = vol.Any( [ga_validator], ), vol.Required(SERVICE_KNX_ATTR_PAYLOAD): cv.match_all, - vol.Required(SERVICE_KNX_ATTR_TYPE): sensor_type_validator, + vol.Required(SERVICE_KNX_ATTR_TYPE): dpt_base_type_validator, vol.Optional(SERVICE_KNX_ATTR_RESPONSE, default=False): cv.boolean, } ), @@ -237,8 +235,15 @@ async def service_send_to_knx_bus(hass: HomeAssistant, call: ServiceCall) -> Non if attr_type is not None: transcoder = DPTBase.parse_transcoder(attr_type) if transcoder is None: - raise ValueError(f"Invalid type for knx.send service: {attr_type}") - payload = transcoder.to_knx(attr_payload) + raise ServiceValidationError( + f"Invalid type for knx.send service: {attr_type}" + ) + try: + payload = transcoder.to_knx(attr_payload) + except ConversionError as err: + raise ServiceValidationError( + f"Invalid payload for knx.send service: {err}" + ) from err elif isinstance(attr_payload, int): payload = DPTBinary(attr_payload) else: diff --git a/homeassistant/components/knx/telegrams.py b/homeassistant/components/knx/telegrams.py index 82df78e748e..2ad46326b8e 100644 --- a/homeassistant/components/knx/telegrams.py +++ b/homeassistant/components/knx/telegrams.py @@ -7,6 +7,7 @@ from typing import Final, TypedDict from xknx import XKNX from xknx.dpt import DPTArray, DPTBase, DPTBinary +from xknx.dpt.dpt import DPTComplexData, DPTEnumData from xknx.exceptions import XKNXException from xknx.telegram import Telegram from xknx.telegram.apci import GroupValueResponse, GroupValueWrite @@ -93,7 +94,7 @@ class Telegrams: if self.recent_telegrams: await self._history_store.async_save(list(self.recent_telegrams)) - async def _xknx_telegram_cb(self, telegram: Telegram) -> None: + def _xknx_telegram_cb(self, telegram: Telegram) -> None: """Handle incoming and outgoing telegrams from xknx.""" telegram_dict = self.telegram_to_dict(telegram) self.recent_telegrams.append(telegram_dict) @@ -157,6 +158,11 @@ def decode_telegram_payload( except XKNXException: value = "Error decoding value" + if isinstance(value, DPTComplexData): + value = value.as_dict() + elif isinstance(value, DPTEnumData): + value = value.name.lower() + return DecodedTelegramPayload( dpt_main=transcoder.dpt_main_number, dpt_sub=transcoder.dpt_sub_number, diff --git a/homeassistant/components/knx/time.py b/homeassistant/components/knx/time.py index c11b40d13dc..28e1419233c 100644 --- a/homeassistant/components/knx/time.py +++ b/homeassistant/components/knx/time.py @@ -3,11 +3,11 @@ from __future__ import annotations from datetime import time as dt_time -import time as time_time from typing import Final from xknx import XKNX -from xknx.devices import DateTime as XknxDateTime +from xknx.devices import TimeDevice as XknxTimeDevice +from xknx.dpt.dpt_10 import KNXTime as XknxTime from homeassistant import config_entries from homeassistant.components.time import TimeEntity @@ -45,15 +45,14 @@ async def async_setup_entry( xknx: XKNX = hass.data[DOMAIN].xknx config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.TIME] - async_add_entities(KNXTime(xknx, entity_config) for entity_config in config) + async_add_entities(KNXTimeEntity(xknx, entity_config) for entity_config in config) -def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTime: +def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxTimeDevice: """Return a XKNX DateTime object to be used within XKNX.""" - return XknxDateTime( + return XknxTimeDevice( xknx, name=config[CONF_NAME], - broadcast_type="TIME", localtime=False, group_address=config[KNX_ADDRESS], group_address_state=config.get(CONF_STATE_ADDRESS), @@ -62,10 +61,10 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTime: ) -class KNXTime(KnxEntity, TimeEntity, RestoreEntity): +class KNXTimeEntity(KnxEntity, TimeEntity, RestoreEntity): """Representation of a KNX time.""" - _device: XknxDateTime + _device: XknxTimeDevice def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize a KNX time.""" @@ -81,25 +80,15 @@ class KNXTime(KnxEntity, TimeEntity, RestoreEntity): and (last_state := await self.async_get_last_state()) is not None and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) ): - self._device.remote_value.value = time_time.strptime( - last_state.state, _TIME_TRANSLATION_FORMAT + self._device.remote_value.value = XknxTime.from_time( + dt_time.fromisoformat(last_state.state) ) @property def native_value(self) -> dt_time | None: """Return the latest value.""" - if (time_struct := self._device.remote_value.value) is None: - return None - return dt_time( - hour=time_struct.tm_hour, - minute=time_struct.tm_min, - second=min(time_struct.tm_sec, 59), # account for leap seconds - ) + return self._device.value async def async_set_value(self, value: dt_time) -> None: """Change the value.""" - time_struct = time_time.strptime( - value.strftime(_TIME_TRANSLATION_FORMAT), - _TIME_TRANSLATION_FORMAT, - ) - await self._device.set(time_struct) + await self._device.set(value) diff --git a/homeassistant/components/knx/trigger.py b/homeassistant/components/knx/trigger.py index 82149b21561..ae3ba088357 100644 --- a/homeassistant/components/knx/trigger.py +++ b/homeassistant/components/knx/trigger.py @@ -18,7 +18,7 @@ from homeassistant.helpers.typing import ConfigType, VolDictType from .const import DOMAIN from .schema import ga_validator from .telegrams import SIGNAL_KNX_TELEGRAM, TelegramDict, decode_telegram_payload -from .validation import sensor_type_validator +from .validation import dpt_base_type_validator TRIGGER_TELEGRAM: Final = "telegram" @@ -44,7 +44,7 @@ TELEGRAM_TRIGGER_SCHEMA: VolDictType = { TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): PLATFORM_TYPE_TRIGGER_TELEGRAM, - vol.Optional(CONF_TYPE, default=None): vol.Any(sensor_type_validator, None), + vol.Optional(CONF_TYPE, default=None): vol.Any(dpt_base_type_validator, None), **TELEGRAM_TRIGGER_SCHEMA, } ) @@ -99,7 +99,7 @@ async def async_attach_trigger( ): decoded_payload = decode_telegram_payload( payload=telegram.payload.value, # type: ignore[union-attr] # checked via payload_apci - transcoder=trigger_transcoder, # type: ignore[type-abstract] # parse_transcoder don't return abstract classes + transcoder=trigger_transcoder, ) # overwrite decoded payload values in telegram_dict telegram_trigger_data = {**trigger_data, **telegram_dict, **decoded_payload} diff --git a/homeassistant/components/knx/validation.py b/homeassistant/components/knx/validation.py index 9ed4f32c920..422b8474fd9 100644 --- a/homeassistant/components/knx/validation.py +++ b/homeassistant/components/knx/validation.py @@ -30,9 +30,10 @@ def dpt_subclass_validator(dpt_base_class: type[DPTBase]) -> Callable[[Any], str return dpt_value_validator +dpt_base_type_validator = dpt_subclass_validator(DPTBase) # type: ignore[type-abstract] numeric_type_validator = dpt_subclass_validator(DPTNumeric) # type: ignore[type-abstract] -sensor_type_validator = dpt_subclass_validator(DPTBase) # type: ignore[type-abstract] string_type_validator = dpt_subclass_validator(DPTString) +sensor_type_validator = vol.Any(numeric_type_validator, string_type_validator) def ga_validator(value: Any) -> str | int: diff --git a/requirements_all.txt b/requirements_all.txt index 8aebf250fd4..2473bdf14b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2927,7 +2927,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.30.2 # homeassistant.components.knx -xknx==2.12.2 +xknx==3.0.0 # homeassistant.components.knx xknxproject==3.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 859154b2837..e5f8ec1bc43 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2310,7 +2310,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.30.2 # homeassistant.components.knx -xknx==2.12.2 +xknx==3.0.0 # homeassistant.components.knx xknxproject==3.7.1 diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 76f1b6f3ebc..19f2bc4d845 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -83,7 +83,7 @@ class KNXTestKit: self.xknx.rate_limit = 0 # set XknxConnectionState.CONNECTED to avoid `unavailable` entities at startup # and start StateUpdater. This would be awaited on normal startup too. - await self.xknx.connection_manager.connection_state_changed( + self.xknx.connection_manager.connection_state_changed( state=XknxConnectionState.CONNECTED, connection_type=XknxConnectionType.TUNNEL_TCP, ) @@ -93,6 +93,7 @@ class KNXTestKit: mock = Mock() mock.start = AsyncMock(side_effect=patch_xknx_start) mock.stop = AsyncMock() + mock.gateway_info = AsyncMock() return mock def fish_xknx(*args, **kwargs): @@ -151,8 +152,6 @@ class KNXTestKit: ) -> None: """Assert outgoing telegram. One by one in timely order.""" await self.xknx.telegrams.join() - await self.hass.async_block_till_done() - await self.hass.async_block_till_done() try: telegram = self._outgoing_telegrams.get_nowait() except asyncio.QueueEmpty as err: @@ -247,6 +246,7 @@ class KNXTestKit: GroupValueResponse(payload_value), source=source, ) + await asyncio.sleep(0) # advance loop to allow StateUpdater to process async def receive_write( self, diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index b9216aa149a..1b304293a86 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -123,25 +123,21 @@ async def test_binary_sensor_ignore_internal_state( # receive initial ON telegram await knx.receive_write("1/1/1", True) await knx.receive_write("2/2/2", True) - await hass.async_block_till_done() assert len(events) == 2 # receive second ON telegram - ignore_internal_state shall force state_changed event await knx.receive_write("1/1/1", True) await knx.receive_write("2/2/2", True) - await hass.async_block_till_done() assert len(events) == 3 # receive first OFF telegram await knx.receive_write("1/1/1", False) await knx.receive_write("2/2/2", False) - await hass.async_block_till_done() assert len(events) == 5 # receive second OFF telegram - ignore_internal_state shall force state_changed event await knx.receive_write("1/1/1", False) await knx.receive_write("2/2/2", False) - await hass.async_block_till_done() assert len(events) == 6 @@ -166,21 +162,17 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> No # receive initial ON telegram await knx.receive_write("2/2/2", True) - await hass.async_block_till_done() # no change yet - still in 1 sec context (additional async_block_till_done needed for time change) assert len(events) == 0 state = hass.states.get("binary_sensor.test") assert state.state is STATE_OFF assert state.attributes.get("counter") == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=context_timeout)) - await hass.async_block_till_done() await knx.xknx.task_registry.block_till_done() # state changed twice after context timeout - once to ON with counter 1 and once to counter 0 state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON assert state.attributes.get("counter") == 0 - # additional async_block_till_done needed event capture - await hass.async_block_till_done() assert len(events) == 2 event = events.pop(0).data assert event.get("new_state").attributes.get("counter") == 1 @@ -198,7 +190,6 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> No assert state.attributes.get("counter") == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=context_timeout)) await knx.xknx.task_registry.block_till_done() - await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON assert state.attributes.get("counter") == 0 @@ -230,12 +221,10 @@ async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit) -> None # receive ON telegram await knx.receive_write("2/2/2", True) - await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) await hass.async_block_till_done() - await hass.async_block_till_done() # state reset after after timeout state = hass.states.get("binary_sensor.test") assert state.state is STATE_OFF @@ -265,7 +254,6 @@ async def test_binary_sensor_restore_and_respond(hass: HomeAssistant, knx) -> No await knx.assert_telegram_count(0) await knx.receive_write(_ADDRESS, False) - await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") assert state.state is STATE_OFF @@ -296,6 +284,5 @@ async def test_binary_sensor_restore_invert(hass: HomeAssistant, knx) -> None: # inverted is on, make sure the state is off after it await knx.receive_write(_ADDRESS, True) - await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") assert state.state is STATE_OFF diff --git a/tests/components/knx/test_climate.py b/tests/components/knx/test_climate.py index 9c431386b43..77eeeef3559 100644 --- a/tests/components/knx/test_climate.py +++ b/tests/components/knx/test_climate.py @@ -80,12 +80,6 @@ async def test_climate_on_off( ) } ) - - await hass.async_block_till_done() - # read heat/cool state - if heat_cool_ga: - await knx.assert_read("1/2/11") - await knx.receive_response("1/2/11", 0) # cool # read temperature state await knx.assert_read("1/2/3") await knx.receive_response("1/2/3", RAW_FLOAT_20_0) @@ -95,6 +89,10 @@ async def test_climate_on_off( # read on/off state await knx.assert_read("1/2/9") await knx.receive_response("1/2/9", 1) + # read heat/cool state + if heat_cool_ga: + await knx.assert_read("1/2/11") + await knx.receive_response("1/2/11", 0) # cool # turn off await hass.services.async_call( @@ -171,18 +169,15 @@ async def test_climate_hvac_mode( ) } ) - - await hass.async_block_till_done() # read states state updater - await knx.assert_read("1/2/7") - await knx.assert_read("1/2/3") - # StateUpdater initialize state - await knx.receive_response("1/2/7", (0x01,)) - await knx.receive_response("1/2/3", RAW_FLOAT_20_0) # StateUpdater semaphore allows 2 concurrent requests - # read target temperature state + await knx.assert_read("1/2/3") await knx.assert_read("1/2/5") + # StateUpdater initialize state + await knx.receive_response("1/2/3", RAW_FLOAT_20_0) await knx.receive_response("1/2/5", RAW_FLOAT_22_0) + await knx.assert_read("1/2/7") + await knx.receive_response("1/2/7", (0x01,)) # turn hvac mode to off - set_hvac_mode() doesn't send to on_off if dedicated hvac mode is available await hass.services.async_call( @@ -254,17 +249,14 @@ async def test_climate_preset_mode( ) events = async_capture_events(hass, "state_changed") - await hass.async_block_till_done() - # read states state updater - await knx.assert_read("1/2/7") - await knx.assert_read("1/2/3") # StateUpdater initialize state - await knx.receive_response("1/2/7", (0x01,)) - await knx.receive_response("1/2/3", RAW_FLOAT_21_0) # StateUpdater semaphore allows 2 concurrent requests - # read target temperature state + await knx.assert_read("1/2/3") await knx.assert_read("1/2/5") + await knx.receive_response("1/2/3", RAW_FLOAT_21_0) await knx.receive_response("1/2/5", RAW_FLOAT_22_0) + await knx.assert_read("1/2/7") + await knx.receive_response("1/2/7", (0x01,)) events.clear() # set preset mode @@ -294,8 +286,6 @@ async def test_climate_preset_mode( assert len(knx.xknx.devices[1].device_updated_cbs) == 2 # test removing also removes hooks entity_registry.async_remove("climate.test") - await hass.async_block_till_done() - # If we remove the entity the underlying devices should disappear too assert len(knx.xknx.devices) == 0 @@ -315,18 +305,15 @@ async def test_update_entity(hass: HomeAssistant, knx: KNXTestKit) -> None: } ) assert await async_setup_component(hass, "homeassistant", {}) - await hass.async_block_till_done() - await hass.async_block_till_done() # read states state updater - await knx.assert_read("1/2/7") await knx.assert_read("1/2/3") - # StateUpdater initialize state - await knx.receive_response("1/2/7", (0x01,)) - await knx.receive_response("1/2/3", RAW_FLOAT_21_0) - # StateUpdater semaphore allows 2 concurrent requests await knx.assert_read("1/2/5") + # StateUpdater initialize state + await knx.receive_response("1/2/3", RAW_FLOAT_21_0) await knx.receive_response("1/2/5", RAW_FLOAT_22_0) + await knx.assert_read("1/2/7") + await knx.receive_response("1/2/7", (0x01,)) # verify update entity retriggers group value reads to the bus await hass.services.async_call( @@ -354,8 +341,6 @@ async def test_command_value_idle_mode(hass: HomeAssistant, knx: KNXTestKit) -> } } ) - - await hass.async_block_till_done() # read states state updater await knx.assert_read("1/2/3") await knx.assert_read("1/2/5") diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 3dad9320e21..a7da2d26600 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -184,7 +184,6 @@ async def test_routing_setup( CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", }, ) - await hass.async_block_till_done() assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Routing as 1.1.110" assert result3["data"] == { @@ -259,7 +258,6 @@ async def test_routing_setup_advanced( CONF_KNX_LOCAL_IP: "192.168.1.112", }, ) - await hass.async_block_till_done() assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Routing as 1.1.110" assert result3["data"] == { @@ -350,7 +348,6 @@ async def test_routing_secure_manual_setup( CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000, }, ) - await hass.async_block_till_done() assert secure_routing_manual["type"] is FlowResultType.CREATE_ENTRY assert secure_routing_manual["title"] == "Secure Routing as 0.0.123" assert secure_routing_manual["data"] == { @@ -419,7 +416,6 @@ async def test_routing_secure_keyfile( CONF_KNX_KNXKEY_PASSWORD: "password", }, ) - await hass.async_block_till_done() assert routing_secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY assert routing_secure_knxkeys["title"] == "Secure Routing as 0.0.123" assert routing_secure_knxkeys["data"] == { @@ -552,7 +548,6 @@ async def test_tunneling_setup_manual( result2["flow_id"], user_input, ) - await hass.async_block_till_done() assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == title assert result3["data"] == config_entry_data @@ -681,7 +676,6 @@ async def test_tunneling_setup_manual_request_description_error( CONF_PORT: 3671, }, ) - await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Tunneling TCP @ 192.168.0.1" assert result["data"] == { @@ -772,7 +766,6 @@ async def test_tunneling_setup_for_local_ip( CONF_KNX_LOCAL_IP: "192.168.1.112", }, ) - await hass.async_block_till_done() assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Tunneling UDP @ 192.168.0.2" assert result3["data"] == { @@ -821,7 +814,6 @@ async def test_tunneling_setup_for_multiple_found_gateways( tunnel_flow["flow_id"], {CONF_KNX_GATEWAY: str(gateway)}, ) - await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { **DEFAULT_ENTRY_DATA, @@ -905,7 +897,6 @@ async def test_form_with_automatic_connection_handling( CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, }, ) - await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONF_KNX_AUTOMATIC.capitalize() assert result2["data"] == { @@ -1040,7 +1031,6 @@ async def test_configure_secure_tunnel_manual(hass: HomeAssistant, knx_setup) -> CONF_KNX_SECURE_DEVICE_AUTHENTICATION: "device_auth", }, ) - await hass.async_block_till_done() assert secure_tunnel_manual["type"] is FlowResultType.CREATE_ENTRY assert secure_tunnel_manual["data"] == { **DEFAULT_ENTRY_DATA, @@ -1086,7 +1076,6 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant, knx_setup) -> None: {CONF_KNX_TUNNEL_ENDPOINT_IA: CONF_KNX_AUTOMATIC}, ) - await hass.async_block_till_done() assert secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY assert secure_knxkeys["data"] == { **DEFAULT_ENTRY_DATA, @@ -1201,7 +1190,6 @@ async def test_options_flow_connection_type( CONF_KNX_GATEWAY: str(gateway), }, ) - await hass.async_block_till_done() assert result3["type"] is FlowResultType.CREATE_ENTRY assert not result3["data"] assert mock_config_entry.data == { @@ -1307,7 +1295,6 @@ async def test_options_flow_secure_manual_to_keyfile( {CONF_KNX_TUNNEL_ENDPOINT_IA: "1.0.1"}, ) - await hass.async_block_till_done() assert secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY assert mock_config_entry.data == { **DEFAULT_ENTRY_DATA, @@ -1352,7 +1339,6 @@ async def test_options_communication_settings( CONF_KNX_TELEGRAM_LOG_SIZE: 3000, }, ) - await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY assert not result2.get("data") assert mock_config_entry.data == { @@ -1405,7 +1391,6 @@ async def test_options_update_keyfile(hass: HomeAssistant, knx_setup) -> None: CONF_KNX_KNXKEY_PASSWORD: "password", }, ) - await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY assert not result2.get("data") assert mock_config_entry.data == { @@ -1463,7 +1448,6 @@ async def test_options_keyfile_upload(hass: HomeAssistant, knx_setup) -> None: CONF_KNX_TUNNEL_ENDPOINT_IA: "1.0.1", }, ) - await hass.async_block_till_done() assert result3["type"] is FlowResultType.CREATE_ENTRY assert not result3.get("data") assert mock_config_entry.data == { diff --git a/tests/components/knx/test_datetime.py b/tests/components/knx/test_datetime.py index c8c6bd4f346..4b66769a8a3 100644 --- a/tests/components/knx/test_datetime.py +++ b/tests/components/knx/test_datetime.py @@ -34,7 +34,8 @@ async def test_datetime(hass: HomeAssistant, knx: KNXTestKit) -> None: ) await knx.assert_write( test_address, - (0x78, 0x01, 0x01, 0x73, 0x04, 0x05, 0x20, 0x80), + # service call in UTC, telegram in local time + (0x78, 0x01, 0x01, 0x13, 0x04, 0x05, 0x24, 0x00), ) state = hass.states.get("datetime.test") assert state.state == "2020-01-02T03:04:05+00:00" @@ -74,7 +75,7 @@ async def test_date_restore_and_respond(hass: HomeAssistant, knx: KNXTestKit) -> await knx.receive_read(test_address) await knx.assert_response( test_address, - (0x7A, 0x03, 0x03, 0x84, 0x04, 0x05, 0x20, 0x80), + (0x7A, 0x03, 0x03, 0x04, 0x04, 0x05, 0x24, 0x00), ) # don't respond to passive address diff --git a/tests/components/knx/test_device_trigger.py b/tests/components/knx/test_device_trigger.py index 9b49df080f5..e5f776a9404 100644 --- a/tests/components/knx/test_device_trigger.py +++ b/tests/components/knx/test_device_trigger.py @@ -391,7 +391,6 @@ async def test_invalid_device_trigger( ] }, ) - await hass.async_block_till_done() assert ( "Unnamed automation failed to setup triggers and has been disabled: " "extra keys not allowed @ data['invalid']. Got None" diff --git a/tests/components/knx/test_events.py b/tests/components/knx/test_events.py index ddb9d50240c..2228781ba89 100644 --- a/tests/components/knx/test_events.py +++ b/tests/components/knx/test_events.py @@ -31,7 +31,6 @@ async def test_knx_event( events = async_capture_events(hass, "knx_event") async def test_event_data(address, payload, value=None): - await hass.async_block_till_done() assert len(events) == 1 event = events.pop() assert event.data["data"] == payload @@ -69,7 +68,6 @@ async def test_knx_event( ) # no event received - await hass.async_block_till_done() assert len(events) == 0 # receive telegrams for group addresses matching the filter @@ -101,7 +99,6 @@ async def test_knx_event( await knx.receive_write("0/5/0", True) await knx.receive_write("1/7/0", True) await knx.receive_write("2/6/6", True) - await hass.async_block_till_done() assert len(events) == 0 # receive telegrams with wrong payload length diff --git a/tests/components/knx/test_expose.py b/tests/components/knx/test_expose.py index e0b4c78e322..96b00241ab6 100644 --- a/tests/components/knx/test_expose.py +++ b/tests/components/knx/test_expose.py @@ -1,9 +1,8 @@ """Test KNX expose.""" from datetime import timedelta -import time -from unittest.mock import patch +from freezegun import freeze_time import pytest from homeassistant.components.knx import CONF_KNX_EXPOSE, DOMAIN, KNX_ADDRESS @@ -327,25 +326,32 @@ async def test_expose_conversion_exception( ) -@patch("time.localtime") +@freeze_time("2022-1-7 9:13:14") +@pytest.mark.parametrize( + ("time_type", "raw"), + [ + ("time", (0xA9, 0x0D, 0x0E)), # localtime includes day of week + ("date", (0x07, 0x01, 0x16)), + ("datetime", (0x7A, 0x1, 0x7, 0xA9, 0xD, 0xE, 0x20, 0xC0)), + ], +) async def test_expose_with_date( - localtime, hass: HomeAssistant, knx: KNXTestKit + hass: HomeAssistant, knx: KNXTestKit, time_type: str, raw: tuple[int, ...] ) -> None: """Test an expose with a date.""" - localtime.return_value = time.struct_time([2022, 1, 7, 9, 13, 14, 6, 0, 0]) await knx.setup_integration( { CONF_KNX_EXPOSE: { - CONF_TYPE: "datetime", + CONF_TYPE: time_type, KNX_ADDRESS: "1/1/8", } } ) - await knx.assert_write("1/1/8", (0x7A, 0x1, 0x7, 0xE9, 0xD, 0xE, 0x20, 0x80)) + await knx.assert_write("1/1/8", raw) await knx.receive_read("1/1/8") - await knx.assert_response("1/1/8", (0x7A, 0x1, 0x7, 0xE9, 0xD, 0xE, 0x20, 0x80)) + await knx.assert_response("1/1/8", raw) entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 diff --git a/tests/components/knx/test_init.py b/tests/components/knx/test_init.py index a317a6a298c..48cc46ef1ee 100644 --- a/tests/components/knx/test_init.py +++ b/tests/components/knx/test_init.py @@ -284,7 +284,6 @@ async def test_async_remove_entry( assert await hass.config_entries.async_remove(config_entry.entry_id) assert unlink_mock.call_count == 3 rmdir_mock.assert_called_once() - await hass.async_block_till_done() assert hass.config_entries.async_entries() == [] assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/knx/test_interface_device.py b/tests/components/knx/test_interface_device.py index c21c25b6fad..8010496ef0d 100644 --- a/tests/components/knx/test_interface_device.py +++ b/tests/components/knx/test_interface_device.py @@ -66,25 +66,19 @@ async def test_diagnostic_entities( ): assert hass.states.get(entity_id).state == test_state - await knx.xknx.connection_manager.connection_state_changed( + knx.xknx.connection_manager.connection_state_changed( state=XknxConnectionState.DISCONNECTED ) await hass.async_block_till_done() - await hass.async_block_till_done() - await hass.async_block_till_done() - await hass.async_block_till_done() assert len(events) == 4 # 3 not always_available + 3 force_update - 2 disabled events.clear() knx.xknx.current_address = IndividualAddress("1.1.1") - await knx.xknx.connection_manager.connection_state_changed( + knx.xknx.connection_manager.connection_state_changed( state=XknxConnectionState.CONNECTED, connection_type=XknxConnectionType.TUNNEL_UDP, ) await hass.async_block_till_done() - await hass.async_block_till_done() - await hass.async_block_till_done() - await hass.async_block_till_done() assert len(events) == 6 # all diagnostic sensors - counters are reset on connect for entity_id, test_state in ( @@ -111,7 +105,6 @@ async def test_removed_entity( "sensor.knx_interface_connection_established", disabled_by=er.RegistryEntryDisabler.USER, ) - await hass.async_block_till_done() unregister_mock.assert_called_once() diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index 0c7a37979a8..8c966a77a0b 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -92,9 +92,7 @@ async def test_light_brightness(hass: HomeAssistant, knx: KNXTestKit) -> None: ) # StateUpdater initialize state await knx.assert_read(test_brightness_state) - await knx.xknx.connection_manager.connection_state_changed( - XknxConnectionState.CONNECTED - ) + knx.xknx.connection_manager.connection_state_changed(XknxConnectionState.CONNECTED) # turn on light via brightness await hass.services.async_call( "light", diff --git a/tests/components/knx/test_notify.py b/tests/components/knx/test_notify.py index 94f2d579fc8..b481675140b 100644 --- a/tests/components/knx/test_notify.py +++ b/tests/components/knx/test_notify.py @@ -21,17 +21,13 @@ async def test_legacy_notify_service_simple( } } ) - await hass.async_block_till_done() - await hass.services.async_call( "notify", "notify", {"target": "test", "message": "I love KNX"}, blocking=True ) - await knx.assert_write( "1/0/0", (73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 0, 0, 0, 0), ) - await hass.services.async_call( "notify", "notify", @@ -41,7 +37,6 @@ async def test_legacy_notify_service_simple( }, blocking=True, ) - await knx.assert_write( "1/0/0", (73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 44, 32, 98, 117), @@ -68,12 +63,9 @@ async def test_legacy_notify_service_multiple_sends_to_all_with_different_encodi ] } ) - await hass.async_block_till_done() - await hass.services.async_call( "notify", "notify", {"message": "Gänsefüßchen"}, blocking=True ) - await knx.assert_write( "1/0/0", # "G?nsef??chen" @@ -95,7 +87,6 @@ async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: } } ) - await hass.services.async_call( notify.DOMAIN, notify.SERVICE_SEND_MESSAGE, diff --git a/tests/components/knx/test_sensor.py b/tests/components/knx/test_sensor.py index 22d9993b58f..41ffcfcb5c7 100644 --- a/tests/components/knx/test_sensor.py +++ b/tests/components/knx/test_sensor.py @@ -68,25 +68,21 @@ async def test_always_callback(hass: HomeAssistant, knx: KNXTestKit) -> None: # receive initial telegram await knx.receive_write("1/1/1", (0x42,)) await knx.receive_write("2/2/2", (0x42,)) - await hass.async_block_till_done() assert len(events) == 2 # receive second telegram with identical payload # always_callback shall force state_changed event await knx.receive_write("1/1/1", (0x42,)) await knx.receive_write("2/2/2", (0x42,)) - await hass.async_block_till_done() assert len(events) == 3 # receive telegram with different payload await knx.receive_write("1/1/1", (0xFA,)) await knx.receive_write("2/2/2", (0xFA,)) - await hass.async_block_till_done() assert len(events) == 5 # receive telegram with second payload again # always_callback shall force state_changed event await knx.receive_write("1/1/1", (0xFA,)) await knx.receive_write("2/2/2", (0xFA,)) - await hass.async_block_till_done() assert len(events) == 6 diff --git a/tests/components/knx/test_services.py b/tests/components/knx/test_services.py index 7f748af5ceb..f70389dbc92 100644 --- a/tests/components/knx/test_services.py +++ b/tests/components/knx/test_services.py @@ -154,7 +154,6 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None: # no event registered await knx.receive_write(test_address, True) - await hass.async_block_till_done() assert len(events) == 0 # register event with `type` @@ -165,7 +164,6 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None: blocking=True, ) await knx.receive_write(test_address, (0x04, 0xD2)) - await hass.async_block_till_done() assert len(events) == 1 typed_event = events.pop() assert typed_event.data["data"] == (0x04, 0xD2) @@ -179,7 +177,6 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None: blocking=True, ) await knx.receive_write(test_address, True) - await hass.async_block_till_done() assert len(events) == 0 # register event without `type` @@ -188,7 +185,6 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None: ) await knx.receive_write(test_address, True) await knx.receive_write(test_address, False) - await hass.async_block_till_done() assert len(events) == 2 untyped_event_2 = events.pop() assert untyped_event_2.data["data"] is False diff --git a/tests/components/knx/test_trigger.py b/tests/components/knx/test_trigger.py index 4565122aba6..73e8b10840e 100644 --- a/tests/components/knx/test_trigger.py +++ b/tests/components/knx/test_trigger.py @@ -334,7 +334,6 @@ async def test_invalid_trigger( ] }, ) - await hass.async_block_till_done() assert ( "Unnamed automation failed to setup triggers and has been disabled: " "extra keys not allowed @ data['invalid']. Got None" diff --git a/tests/components/knx/test_weather.py b/tests/components/knx/test_weather.py index 0adcc309252..5ba38d6cdf8 100644 --- a/tests/components/knx/test_weather.py +++ b/tests/components/knx/test_weather.py @@ -45,12 +45,12 @@ async def test_weather(hass: HomeAssistant, knx: KNXTestKit) -> None: # brightness await knx.assert_read("1/1/6") - await knx.receive_response("1/1/6", (0x7C, 0x5E)) await knx.assert_read("1/1/8") + await knx.receive_response("1/1/6", (0x7C, 0x5E)) await knx.receive_response("1/1/8", (0x7C, 0x5E)) + await knx.assert_read("1/1/5") await knx.assert_read("1/1/7") await knx.receive_response("1/1/7", (0x7C, 0x5E)) - await knx.assert_read("1/1/5") await knx.receive_response("1/1/5", (0x7C, 0x5E)) # wind speed @@ -64,10 +64,10 @@ async def test_weather(hass: HomeAssistant, knx: KNXTestKit) -> None: # alarms await knx.assert_read("1/1/2") await knx.receive_response("1/1/2", False) - await knx.assert_read("1/1/3") - await knx.receive_response("1/1/3", False) await knx.assert_read("1/1/1") + await knx.assert_read("1/1/3") await knx.receive_response("1/1/1", False) + await knx.receive_response("1/1/3", False) # day night await knx.assert_read("1/1/12") From 35bfd0b88fe3c17b5810dd96fb3f50b6237df869 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 31 Jul 2024 08:35:21 +0100 Subject: [PATCH 1758/2411] Evohome drops use of async_call_later to avoid lingering task (#122879) initial commit --- homeassistant/components/evohome/coordinator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/evohome/coordinator.py b/homeassistant/components/evohome/coordinator.py index b83d2d20c6a..943bd6605b4 100644 --- a/homeassistant/components/evohome/coordinator.py +++ b/homeassistant/components/evohome/coordinator.py @@ -19,7 +19,6 @@ from evohomeasync2.schema.const import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_call_later from .const import CONF_LOCATION_IDX, DOMAIN, GWS, TCS, UTC_OFFSET from .helpers import handle_evo_exception @@ -107,7 +106,7 @@ class EvoBroker: return None if update_state: # wait a moment for system to quiesce before updating state - async_call_later(self.hass, 1, self._update_v2_api_state) + await self.hass.data[DOMAIN]["coordinator"].async_request_refresh() return result From 5a04d982d92b9dfba4c786dc5bf6dc07d5f4394b Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Wed, 31 Jul 2024 10:35:45 +0300 Subject: [PATCH 1759/2411] Bump ollama to 0.3.1 (#122866) --- homeassistant/components/ollama/conversation.py | 2 +- homeassistant/components/ollama/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ollama/conversation.py b/homeassistant/components/ollama/conversation.py index ae0acef1077..ac367a5cf6a 100644 --- a/homeassistant/components/ollama/conversation.py +++ b/homeassistant/components/ollama/conversation.py @@ -271,7 +271,7 @@ class OllamaConversationEntity( _LOGGER.debug("Tool response: %s", tool_response) message_history.messages.append( ollama.Message( - role=MessageRole.TOOL.value, # type: ignore[typeddict-item] + role=MessageRole.TOOL.value, content=json.dumps(tool_response), ) ) diff --git a/homeassistant/components/ollama/manifest.json b/homeassistant/components/ollama/manifest.json index 4d4321b8e3d..64224eb06fb 100644 --- a/homeassistant/components/ollama/manifest.json +++ b/homeassistant/components/ollama/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/ollama", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["ollama==0.3.0"] + "requirements": ["ollama==0.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2473bdf14b0..1abd6ec5be2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1466,7 +1466,7 @@ odp-amsterdam==6.0.2 oemthermostat==1.1.1 # homeassistant.components.ollama -ollama==0.3.0 +ollama==0.3.1 # homeassistant.components.omnilogic omnilogic==0.4.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5f8ec1bc43..463cbae4cdf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1205,7 +1205,7 @@ objgraph==3.5.0 odp-amsterdam==6.0.2 # homeassistant.components.ollama -ollama==0.3.0 +ollama==0.3.1 # homeassistant.components.omnilogic omnilogic==0.4.5 From beb2ef121ef3d682fa04c5a0750a5897ce410196 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Wed, 31 Jul 2024 10:37:55 +0300 Subject: [PATCH 1760/2411] Update todo intent slot schema (#122335) * Update todo intent slot schema * Update intent.py * ruff --- homeassistant/components/todo/intent.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/todo/intent.py b/homeassistant/components/todo/intent.py index 50afe916b27..cd8ad7f02ab 100644 --- a/homeassistant/components/todo/intent.py +++ b/homeassistant/components/todo/intent.py @@ -2,6 +2,8 @@ from __future__ import annotations +import voluptuous as vol + from homeassistant.core import HomeAssistant from homeassistant.helpers import intent from homeassistant.helpers.entity_component import EntityComponent @@ -21,7 +23,10 @@ class ListAddItemIntent(intent.IntentHandler): intent_type = INTENT_LIST_ADD_ITEM description = "Add item to a todo list" - slot_schema = {"item": intent.non_empty_string, "name": intent.non_empty_string} + slot_schema = { + vol.Required("item"): intent.non_empty_string, + vol.Required("name"): intent.non_empty_string, + } platforms = {DOMAIN} async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: From 7f4dabf546f93efa3271152c0f300a824ea06fa0 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 31 Jul 2024 02:42:45 -0500 Subject: [PATCH 1761/2411] Switch from WebRTC to microVAD (#122861) * Switch WebRTC to microVAD * Remove webrtc-noise-gain from licenses --- .../assist_pipeline/audio_enhancer.py | 82 +++++++++ .../components/assist_pipeline/const.py | 5 + .../components/assist_pipeline/manifest.json | 2 +- .../components/assist_pipeline/pipeline.py | 164 +++++++++--------- .../components/assist_pipeline/vad.py | 66 ++----- .../assist_pipeline/websocket_api.py | 20 ++- homeassistant/components/voip/voip.py | 38 ++-- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- script/licenses.py | 1 - tests/components/assist_pipeline/test_init.py | 43 ++--- tests/components/assist_pipeline/test_vad.py | 116 +++---------- .../assist_pipeline/test_websocket.py | 61 ++----- tests/components/voip/test_voip.py | 55 +++--- 15 files changed, 320 insertions(+), 347 deletions(-) create mode 100644 homeassistant/components/assist_pipeline/audio_enhancer.py diff --git a/homeassistant/components/assist_pipeline/audio_enhancer.py b/homeassistant/components/assist_pipeline/audio_enhancer.py new file mode 100644 index 00000000000..e7a149bd00e --- /dev/null +++ b/homeassistant/components/assist_pipeline/audio_enhancer.py @@ -0,0 +1,82 @@ +"""Audio enhancement for Assist.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +import logging + +from pymicro_vad import MicroVad + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True, slots=True) +class EnhancedAudioChunk: + """Enhanced audio chunk and metadata.""" + + audio: bytes + """Raw PCM audio @ 16Khz with 16-bit mono samples""" + + timestamp_ms: int + """Timestamp relative to start of audio stream (milliseconds)""" + + is_speech: bool | None + """True if audio chunk likely contains speech, False if not, None if unknown""" + + +class AudioEnhancer(ABC): + """Base class for audio enhancement.""" + + def __init__( + self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool + ) -> None: + """Initialize audio enhancer.""" + self.auto_gain = auto_gain + self.noise_suppression = noise_suppression + self.is_vad_enabled = is_vad_enabled + + @abstractmethod + def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk: + """Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples.""" + + @property + @abstractmethod + def samples_per_chunk(self) -> int | None: + """Return number of samples per chunk or None if chunking isn't required.""" + + +class MicroVadEnhancer(AudioEnhancer): + """Audio enhancer that just runs microVAD.""" + + def __init__( + self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool + ) -> None: + """Initialize audio enhancer.""" + super().__init__(auto_gain, noise_suppression, is_vad_enabled) + + self.vad: MicroVad | None = None + self.threshold = 0.5 + + if self.is_vad_enabled: + self.vad = MicroVad() + _LOGGER.debug("Initialized microVAD with threshold=%s", self.threshold) + + def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk: + """Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples.""" + is_speech: bool | None = None + + if self.vad is not None: + # Run VAD + speech_prob = self.vad.Process10ms(audio) + is_speech = speech_prob > self.threshold + + return EnhancedAudioChunk( + audio=audio, timestamp_ms=timestamp_ms, is_speech=is_speech + ) + + @property + def samples_per_chunk(self) -> int | None: + """Return number of samples per chunk or None if chunking isn't required.""" + if self.is_vad_enabled: + return 160 # 10ms + + return None diff --git a/homeassistant/components/assist_pipeline/const.py b/homeassistant/components/assist_pipeline/const.py index 36b72dad69c..14b93a90372 100644 --- a/homeassistant/components/assist_pipeline/const.py +++ b/homeassistant/components/assist_pipeline/const.py @@ -15,3 +15,8 @@ DATA_LAST_WAKE_UP = f"{DOMAIN}.last_wake_up" WAKE_WORD_COOLDOWN = 2 # seconds EVENT_RECORDING = f"{DOMAIN}_recording" + +SAMPLE_RATE = 16000 # hertz +SAMPLE_WIDTH = 2 # bytes +SAMPLE_CHANNELS = 1 # mono +SAMPLES_PER_CHUNK = 240 # 20 ms @ 16Khz diff --git a/homeassistant/components/assist_pipeline/manifest.json b/homeassistant/components/assist_pipeline/manifest.json index 31b3b0d4e32..b22ce72b1eb 100644 --- a/homeassistant/components/assist_pipeline/manifest.json +++ b/homeassistant/components/assist_pipeline/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/assist_pipeline", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["webrtc-noise-gain==1.2.3"] + "requirements": ["pymicro-vad==1.0.0"] } diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index ecf361cb67c..845950caf8d 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -13,14 +13,11 @@ from pathlib import Path from queue import Empty, Queue from threading import Thread import time -from typing import TYPE_CHECKING, Any, Final, Literal, cast +from typing import Any, Literal, cast import wave import voluptuous as vol -if TYPE_CHECKING: - from webrtc_noise_gain import AudioProcessor - from homeassistant.components import ( conversation, media_source, @@ -52,12 +49,17 @@ from homeassistant.util import ( ) from homeassistant.util.limited_size_dict import LimitedSizeDict +from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, MicroVadEnhancer from .const import ( CONF_DEBUG_RECORDING_DIR, DATA_CONFIG, DATA_LAST_WAKE_UP, DATA_MIGRATIONS, DOMAIN, + SAMPLE_CHANNELS, + SAMPLE_RATE, + SAMPLE_WIDTH, + SAMPLES_PER_CHUNK, WAKE_WORD_COOLDOWN, ) from .error import ( @@ -111,9 +113,6 @@ STORED_PIPELINE_RUNS = 10 SAVE_DELAY = 10 -AUDIO_PROCESSOR_SAMPLES: Final = 160 # 10 ms @ 16 Khz -AUDIO_PROCESSOR_BYTES: Final = AUDIO_PROCESSOR_SAMPLES * 2 # 16-bit samples - @callback def _async_resolve_default_pipeline_settings( @@ -503,8 +502,8 @@ class AudioSettings: is_vad_enabled: bool = True """True if VAD is used to determine the end of the voice command.""" - is_chunking_enabled: bool = True - """True if audio is automatically split into 10 ms chunks (required for VAD, etc.)""" + samples_per_chunk: int | None = None + """Number of samples that will be in each audio chunk (None for no chunking).""" def __post_init__(self) -> None: """Verify settings post-initialization.""" @@ -514,9 +513,6 @@ class AudioSettings: if (self.auto_gain_dbfs < 0) or (self.auto_gain_dbfs > 31): raise ValueError("auto_gain_dbfs must be in [0, 31]") - if self.needs_processor and (not self.is_chunking_enabled): - raise ValueError("Chunking must be enabled for audio processing") - @property def needs_processor(self) -> bool: """True if an audio processor is needed.""" @@ -526,19 +522,10 @@ class AudioSettings: or (self.auto_gain_dbfs > 0) ) - -@dataclass(frozen=True, slots=True) -class ProcessedAudioChunk: - """Processed audio chunk and metadata.""" - - audio: bytes - """Raw PCM audio @ 16Khz with 16-bit mono samples""" - - timestamp_ms: int - """Timestamp relative to start of audio stream (milliseconds)""" - - is_speech: bool | None - """True if audio chunk likely contains speech, False if not, None if unknown""" + @property + def is_chunking_enabled(self) -> bool: + """True if chunk size is set.""" + return self.samples_per_chunk is not None @dataclass @@ -573,10 +560,10 @@ class PipelineRun: debug_recording_queue: Queue[str | bytes | None] | None = None """Queue to communicate with debug recording thread""" - audio_processor: AudioProcessor | None = None + audio_enhancer: AudioEnhancer | None = None """VAD/noise suppression/auto gain""" - audio_processor_buffer: AudioBuffer = field(init=False, repr=False) + audio_chunking_buffer: AudioBuffer | None = None """Buffer used when splitting audio into chunks for audio processing""" _device_id: str | None = None @@ -601,19 +588,16 @@ class PipelineRun: pipeline_data.pipeline_runs.add_run(self) # Initialize with audio settings - self.audio_processor_buffer = AudioBuffer(AUDIO_PROCESSOR_BYTES) - if self.audio_settings.needs_processor: - # Delay import of webrtc so HA start up is not crashing - # on older architectures (armhf). - # - # pylint: disable=import-outside-toplevel - from webrtc_noise_gain import AudioProcessor - - self.audio_processor = AudioProcessor( + if self.audio_settings.needs_processor and (self.audio_enhancer is None): + # Default audio enhancer + self.audio_enhancer = MicroVadEnhancer( self.audio_settings.auto_gain_dbfs, self.audio_settings.noise_suppression_level, + self.audio_settings.is_vad_enabled, ) + self.audio_chunking_buffer = AudioBuffer(self.samples_per_chunk * SAMPLE_WIDTH) + def __eq__(self, other: object) -> bool: """Compare pipeline runs by id.""" if isinstance(other, PipelineRun): @@ -621,6 +605,14 @@ class PipelineRun: return False + @property + def samples_per_chunk(self) -> int: + """Return number of samples expected in each audio chunk.""" + if self.audio_enhancer is not None: + return self.audio_enhancer.samples_per_chunk or SAMPLES_PER_CHUNK + + return self.audio_settings.samples_per_chunk or SAMPLES_PER_CHUNK + @callback def process_event(self, event: PipelineEvent) -> None: """Log an event and call listener.""" @@ -688,8 +680,8 @@ class PipelineRun: async def wake_word_detection( self, - stream: AsyncIterable[ProcessedAudioChunk], - audio_chunks_for_stt: list[ProcessedAudioChunk], + stream: AsyncIterable[EnhancedAudioChunk], + audio_chunks_for_stt: list[EnhancedAudioChunk], ) -> wake_word.DetectionResult | None: """Run wake-word-detection portion of pipeline. Returns detection result.""" metadata_dict = asdict( @@ -732,10 +724,11 @@ class PipelineRun: # Audio chunk buffer. This audio will be forwarded to speech-to-text # after wake-word-detection. num_audio_chunks_to_buffer = int( - (wake_word_settings.audio_seconds_to_buffer * 16000) - / AUDIO_PROCESSOR_SAMPLES + (wake_word_settings.audio_seconds_to_buffer * SAMPLE_RATE) + / self.samples_per_chunk ) - stt_audio_buffer: deque[ProcessedAudioChunk] | None = None + + stt_audio_buffer: deque[EnhancedAudioChunk] | None = None if num_audio_chunks_to_buffer > 0: stt_audio_buffer = deque(maxlen=num_audio_chunks_to_buffer) @@ -797,7 +790,7 @@ class PipelineRun: # speech-to-text so the user does not have to pause before # speaking the voice command. audio_chunks_for_stt.extend( - ProcessedAudioChunk( + EnhancedAudioChunk( audio=chunk_ts[0], timestamp_ms=chunk_ts[1], is_speech=False ) for chunk_ts in result.queued_audio @@ -819,18 +812,17 @@ class PipelineRun: async def _wake_word_audio_stream( self, - audio_stream: AsyncIterable[ProcessedAudioChunk], - stt_audio_buffer: deque[ProcessedAudioChunk] | None, + audio_stream: AsyncIterable[EnhancedAudioChunk], + stt_audio_buffer: deque[EnhancedAudioChunk] | None, wake_word_vad: VoiceActivityTimeout | None, - sample_rate: int = 16000, - sample_width: int = 2, + sample_rate: int = SAMPLE_RATE, + sample_width: int = SAMPLE_WIDTH, ) -> AsyncIterable[tuple[bytes, int]]: """Yield audio chunks with timestamps (milliseconds since start of stream). Adds audio to a ring buffer that will be forwarded to speech-to-text after detection. Times out if VAD detects enough silence. """ - chunk_seconds = AUDIO_PROCESSOR_SAMPLES / sample_rate async for chunk in audio_stream: if self.abort_wake_word_detection: raise WakeWordDetectionAborted @@ -845,6 +837,7 @@ class PipelineRun: stt_audio_buffer.append(chunk) if wake_word_vad is not None: + chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate if not wake_word_vad.process(chunk_seconds, chunk.is_speech): raise WakeWordTimeoutError( code="wake-word-timeout", message="Wake word was not detected" @@ -881,7 +874,7 @@ class PipelineRun: async def speech_to_text( self, metadata: stt.SpeechMetadata, - stream: AsyncIterable[ProcessedAudioChunk], + stream: AsyncIterable[EnhancedAudioChunk], ) -> str: """Run speech-to-text portion of pipeline. Returns the spoken text.""" # Create a background task to prepare the conversation agent @@ -957,18 +950,18 @@ class PipelineRun: async def _speech_to_text_stream( self, - audio_stream: AsyncIterable[ProcessedAudioChunk], + audio_stream: AsyncIterable[EnhancedAudioChunk], stt_vad: VoiceCommandSegmenter | None, - sample_rate: int = 16000, - sample_width: int = 2, + sample_rate: int = SAMPLE_RATE, + sample_width: int = SAMPLE_WIDTH, ) -> AsyncGenerator[bytes]: """Yield audio chunks until VAD detects silence or speech-to-text completes.""" - chunk_seconds = AUDIO_PROCESSOR_SAMPLES / sample_rate sent_vad_start = False async for chunk in audio_stream: self._capture_chunk(chunk.audio) if stt_vad is not None: + chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate if not stt_vad.process(chunk_seconds, chunk.is_speech): # Silence detected at the end of voice command self.process_event( @@ -1072,8 +1065,8 @@ class PipelineRun: tts_options[tts.ATTR_PREFERRED_FORMAT] = self.tts_audio_output if self.tts_audio_output == "wav": # 16 Khz, 16-bit mono - tts_options[tts.ATTR_PREFERRED_SAMPLE_RATE] = 16000 - tts_options[tts.ATTR_PREFERRED_SAMPLE_CHANNELS] = 1 + tts_options[tts.ATTR_PREFERRED_SAMPLE_RATE] = SAMPLE_RATE + tts_options[tts.ATTR_PREFERRED_SAMPLE_CHANNELS] = SAMPLE_CHANNELS try: options_supported = await tts.async_support_options( @@ -1220,12 +1213,15 @@ class PipelineRun: async def process_volume_only( self, audio_stream: AsyncIterable[bytes], - sample_rate: int = 16000, - sample_width: int = 2, - ) -> AsyncGenerator[ProcessedAudioChunk]: + sample_rate: int = SAMPLE_RATE, + sample_width: int = SAMPLE_WIDTH, + ) -> AsyncGenerator[EnhancedAudioChunk]: """Apply volume transformation only (no VAD/audio enhancements) with optional chunking.""" + assert self.audio_chunking_buffer is not None + + bytes_per_chunk = self.samples_per_chunk * sample_width ms_per_sample = sample_rate // 1000 - ms_per_chunk = (AUDIO_PROCESSOR_SAMPLES // sample_width) // ms_per_sample + ms_per_chunk = self.samples_per_chunk // ms_per_sample timestamp_ms = 0 async for chunk in audio_stream: @@ -1233,19 +1229,18 @@ class PipelineRun: chunk = _multiply_volume(chunk, self.audio_settings.volume_multiplier) if self.audio_settings.is_chunking_enabled: - # 10 ms chunking - for chunk_10ms in chunk_samples( - chunk, AUDIO_PROCESSOR_BYTES, self.audio_processor_buffer + for sub_chunk in chunk_samples( + chunk, bytes_per_chunk, self.audio_chunking_buffer ): - yield ProcessedAudioChunk( - audio=chunk_10ms, + yield EnhancedAudioChunk( + audio=sub_chunk, timestamp_ms=timestamp_ms, is_speech=None, # no VAD ) timestamp_ms += ms_per_chunk else: # No chunking - yield ProcessedAudioChunk( + yield EnhancedAudioChunk( audio=chunk, timestamp_ms=timestamp_ms, is_speech=None, # no VAD @@ -1255,14 +1250,19 @@ class PipelineRun: async def process_enhance_audio( self, audio_stream: AsyncIterable[bytes], - sample_rate: int = 16000, - sample_width: int = 2, - ) -> AsyncGenerator[ProcessedAudioChunk]: + sample_rate: int = SAMPLE_RATE, + sample_width: int = SAMPLE_WIDTH, + ) -> AsyncGenerator[EnhancedAudioChunk]: """Split audio into 10 ms chunks and apply VAD/noise suppression/auto gain/volume transformation.""" - assert self.audio_processor is not None + assert self.audio_enhancer is not None + assert self.audio_enhancer.samples_per_chunk is not None + assert self.audio_chunking_buffer is not None + bytes_per_chunk = self.audio_enhancer.samples_per_chunk * sample_width ms_per_sample = sample_rate // 1000 - ms_per_chunk = (AUDIO_PROCESSOR_SAMPLES // sample_width) // ms_per_sample + ms_per_chunk = ( + self.audio_enhancer.samples_per_chunk // sample_width + ) // ms_per_sample timestamp_ms = 0 async for dirty_samples in audio_stream: @@ -1272,17 +1272,11 @@ class PipelineRun: dirty_samples, self.audio_settings.volume_multiplier ) - # Split into 10ms chunks for audio enhancements/VAD - for dirty_10ms_chunk in chunk_samples( - dirty_samples, AUDIO_PROCESSOR_BYTES, self.audio_processor_buffer + # Split into chunks for audio enhancements/VAD + for dirty_chunk in chunk_samples( + dirty_samples, bytes_per_chunk, self.audio_chunking_buffer ): - ap_result = self.audio_processor.Process10ms(dirty_10ms_chunk) - yield ProcessedAudioChunk( - audio=ap_result.audio, - timestamp_ms=timestamp_ms, - is_speech=ap_result.is_speech, - ) - + yield self.audio_enhancer.enhance_chunk(dirty_chunk, timestamp_ms) timestamp_ms += ms_per_chunk @@ -1323,9 +1317,9 @@ def _pipeline_debug_recording_thread_proc( wav_path = run_recording_dir / f"{message}.wav" wav_writer = wave.open(str(wav_path), "wb") - wav_writer.setframerate(16000) - wav_writer.setsampwidth(2) - wav_writer.setnchannels(1) + wav_writer.setframerate(SAMPLE_RATE) + wav_writer.setsampwidth(SAMPLE_WIDTH) + wav_writer.setnchannels(SAMPLE_CHANNELS) elif isinstance(message, bytes): # Chunk of 16-bit mono audio at 16Khz if wav_writer is not None: @@ -1368,8 +1362,8 @@ class PipelineInput: """Run pipeline.""" self.run.start(device_id=self.device_id) current_stage: PipelineStage | None = self.run.start_stage - stt_audio_buffer: list[ProcessedAudioChunk] = [] - stt_processed_stream: AsyncIterable[ProcessedAudioChunk] | None = None + stt_audio_buffer: list[EnhancedAudioChunk] = [] + stt_processed_stream: AsyncIterable[EnhancedAudioChunk] | None = None if self.stt_stream is not None: if self.run.audio_settings.needs_processor: @@ -1423,7 +1417,7 @@ class PipelineInput: # Send audio in the buffer first to speech-to-text, then move on to stt_stream. # This is basically an async itertools.chain. async def buffer_then_audio_stream() -> ( - AsyncGenerator[ProcessedAudioChunk] + AsyncGenerator[EnhancedAudioChunk] ): # Buffered audio for chunk in stt_audio_buffer: diff --git a/homeassistant/components/assist_pipeline/vad.py b/homeassistant/components/assist_pipeline/vad.py index 5b3d1408f58..e3b425a2a7b 100644 --- a/homeassistant/components/assist_pipeline/vad.py +++ b/homeassistant/components/assist_pipeline/vad.py @@ -2,12 +2,11 @@ from __future__ import annotations -from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Callable, Iterable from dataclasses import dataclass from enum import StrEnum import logging -from typing import Final, cast +from typing import Final _LOGGER = logging.getLogger(__name__) @@ -35,44 +34,6 @@ class VadSensitivity(StrEnum): return 1.0 -class VoiceActivityDetector(ABC): - """Base class for voice activity detectors (VAD).""" - - @abstractmethod - def is_speech(self, chunk: bytes) -> bool: - """Return True if audio chunk contains speech.""" - - @property - @abstractmethod - def samples_per_chunk(self) -> int | None: - """Return number of samples per chunk or None if chunking is not required.""" - - -class WebRtcVad(VoiceActivityDetector): - """Voice activity detector based on webrtc.""" - - def __init__(self) -> None: - """Initialize webrtcvad.""" - # Delay import of webrtc so HA start up is not crashing - # on older architectures (armhf). - # - # pylint: disable=import-outside-toplevel - from webrtc_noise_gain import AudioProcessor - - # Just VAD: no noise suppression or auto gain - self._audio_processor = AudioProcessor(0, 0) - - def is_speech(self, chunk: bytes) -> bool: - """Return True if audio chunk contains speech.""" - result = self._audio_processor.Process10ms(chunk) - return cast(bool, result.is_speech) - - @property - def samples_per_chunk(self) -> int | None: - """Return 10 ms.""" - return int(0.01 * _SAMPLE_RATE) # 10 ms - - class AudioBuffer: """Fixed-sized audio buffer with variable internal length.""" @@ -176,29 +137,38 @@ class VoiceCommandSegmenter: if self._speech_seconds_left <= 0: # Inside voice command self.in_command = True + self._silence_seconds_left = self.silence_seconds + _LOGGER.debug("Voice command started") else: # Reset if enough silence self._reset_seconds_left -= chunk_seconds if self._reset_seconds_left <= 0: self._speech_seconds_left = self.speech_seconds + self._reset_seconds_left = self.reset_seconds elif not is_speech: + # Silence in command self._reset_seconds_left = self.reset_seconds self._silence_seconds_left -= chunk_seconds if self._silence_seconds_left <= 0: + # Command finished successfully self.reset() + _LOGGER.debug("Voice command finished") return False else: - # Reset if enough speech + # Speech in command. + # Reset silence counter if enough speech. self._reset_seconds_left -= chunk_seconds if self._reset_seconds_left <= 0: self._silence_seconds_left = self.silence_seconds + self._reset_seconds_left = self.reset_seconds return True def process_with_vad( self, chunk: bytes, - vad: VoiceActivityDetector, + vad_samples_per_chunk: int | None, + vad_is_speech: Callable[[bytes], bool], leftover_chunk_buffer: AudioBuffer | None, ) -> bool: """Process an audio chunk using an external VAD. @@ -207,20 +177,20 @@ class VoiceCommandSegmenter: Returns False when voice command is finished. """ - if vad.samples_per_chunk is None: + if vad_samples_per_chunk is None: # No chunking chunk_seconds = (len(chunk) // _SAMPLE_WIDTH) / _SAMPLE_RATE - is_speech = vad.is_speech(chunk) + is_speech = vad_is_speech(chunk) return self.process(chunk_seconds, is_speech) if leftover_chunk_buffer is None: raise ValueError("leftover_chunk_buffer is required when vad uses chunking") # With chunking - seconds_per_chunk = vad.samples_per_chunk / _SAMPLE_RATE - bytes_per_chunk = vad.samples_per_chunk * _SAMPLE_WIDTH + seconds_per_chunk = vad_samples_per_chunk / _SAMPLE_RATE + bytes_per_chunk = vad_samples_per_chunk * _SAMPLE_WIDTH for vad_chunk in chunk_samples(chunk, bytes_per_chunk, leftover_chunk_buffer): - is_speech = vad.is_speech(vad_chunk) + is_speech = vad_is_speech(vad_chunk) if not self.process(seconds_per_chunk, is_speech): return False diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index 3855bd7afc5..c96af655589 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -24,6 +24,9 @@ from .const import ( DEFAULT_WAKE_WORD_TIMEOUT, DOMAIN, EVENT_RECORDING, + SAMPLE_CHANNELS, + SAMPLE_RATE, + SAMPLE_WIDTH, ) from .error import PipelineNotFound from .pipeline import ( @@ -92,7 +95,6 @@ def async_register_websocket_api(hass: HomeAssistant) -> None: vol.Optional("volume_multiplier"): float, # Advanced use cases/testing vol.Optional("no_vad"): bool, - vol.Optional("no_chunking"): bool, } }, extra=vol.ALLOW_EXTRA, @@ -170,9 +172,14 @@ async def websocket_run( # Yield until we receive an empty chunk while chunk := await audio_queue.get(): - if incoming_sample_rate != 16000: + if incoming_sample_rate != SAMPLE_RATE: chunk, state = audioop.ratecv( - chunk, 2, 1, incoming_sample_rate, 16000, state + chunk, + SAMPLE_WIDTH, + SAMPLE_CHANNELS, + incoming_sample_rate, + SAMPLE_RATE, + state, ) yield chunk @@ -206,7 +213,6 @@ async def websocket_run( auto_gain_dbfs=msg_input.get("auto_gain_dbfs", 0), volume_multiplier=msg_input.get("volume_multiplier", 1.0), is_vad_enabled=not msg_input.get("no_vad", False), - is_chunking_enabled=not msg_input.get("no_chunking", False), ) elif start_stage == PipelineStage.INTENT: # Input to conversation agent @@ -424,9 +430,9 @@ def websocket_list_languages( connection.send_result( msg["id"], { - "languages": sorted(pipeline_languages) - if pipeline_languages - else pipeline_languages + "languages": ( + sorted(pipeline_languages) if pipeline_languages else pipeline_languages + ) }, ) diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 5770d9d2b4a..243909629cf 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -31,12 +31,14 @@ from homeassistant.components.assist_pipeline import ( async_pipeline_from_audio_stream, select as pipeline_select, ) +from homeassistant.components.assist_pipeline.audio_enhancer import ( + AudioEnhancer, + MicroVadEnhancer, +) from homeassistant.components.assist_pipeline.vad import ( AudioBuffer, VadSensitivity, - VoiceActivityDetector, VoiceCommandSegmenter, - WebRtcVad, ) from homeassistant.const import __version__ from homeassistant.core import Context, HomeAssistant @@ -233,13 +235,13 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): try: # Wait for speech before starting pipeline segmenter = VoiceCommandSegmenter(silence_seconds=self.silence_seconds) - vad = WebRtcVad() + audio_enhancer = MicroVadEnhancer(0, 0, True) chunk_buffer: deque[bytes] = deque( maxlen=self.buffered_chunks_before_speech, ) speech_detected = await self._wait_for_speech( segmenter, - vad, + audio_enhancer, chunk_buffer, ) if not speech_detected: @@ -253,7 +255,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): try: async for chunk in self._segment_audio( segmenter, - vad, + audio_enhancer, chunk_buffer, ): yield chunk @@ -317,7 +319,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): async def _wait_for_speech( self, segmenter: VoiceCommandSegmenter, - vad: VoiceActivityDetector, + audio_enhancer: AudioEnhancer, chunk_buffer: MutableSequence[bytes], ): """Buffer audio chunks until speech is detected. @@ -329,13 +331,18 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): async with asyncio.timeout(self.audio_timeout): chunk = await self._audio_queue.get() - assert vad.samples_per_chunk is not None - vad_buffer = AudioBuffer(vad.samples_per_chunk * WIDTH) + assert audio_enhancer.samples_per_chunk is not None + vad_buffer = AudioBuffer(audio_enhancer.samples_per_chunk * WIDTH) while chunk: chunk_buffer.append(chunk) - segmenter.process_with_vad(chunk, vad, vad_buffer) + segmenter.process_with_vad( + chunk, + audio_enhancer.samples_per_chunk, + lambda x: audio_enhancer.enhance_chunk(x, 0).is_speech is True, + vad_buffer, + ) if segmenter.in_command: # Buffer until command starts if len(vad_buffer) > 0: @@ -351,7 +358,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): async def _segment_audio( self, segmenter: VoiceCommandSegmenter, - vad: VoiceActivityDetector, + audio_enhancer: AudioEnhancer, chunk_buffer: Sequence[bytes], ) -> AsyncIterable[bytes]: """Yield audio chunks until voice command has finished.""" @@ -364,11 +371,16 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): async with asyncio.timeout(self.audio_timeout): chunk = await self._audio_queue.get() - assert vad.samples_per_chunk is not None - vad_buffer = AudioBuffer(vad.samples_per_chunk * WIDTH) + assert audio_enhancer.samples_per_chunk is not None + vad_buffer = AudioBuffer(audio_enhancer.samples_per_chunk * WIDTH) while chunk: - if not segmenter.process_with_vad(chunk, vad, vad_buffer): + if not segmenter.process_with_vad( + chunk, + audio_enhancer.samples_per_chunk, + lambda x: audio_enhancer.enhance_chunk(x, 0).is_speech is True, + vad_buffer, + ): # Voice command is finished break diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 43e737a002d..c52ccfa6a8c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -45,6 +45,7 @@ Pillow==10.4.0 pip>=21.3.1 psutil-home-assistant==0.0.1 PyJWT==2.8.0 +pymicro-vad==1.0.0 PyNaCl==1.5.0 pyOpenSSL==24.2.1 pyserial==3.5 @@ -60,7 +61,6 @@ urllib3>=1.26.5,<2 voluptuous-openapi==0.0.5 voluptuous-serialize==2.6.0 voluptuous==0.15.2 -webrtc-noise-gain==1.2.3 yarl==1.9.4 zeroconf==0.132.2 diff --git a/requirements_all.txt b/requirements_all.txt index 1abd6ec5be2..bc1a13f19ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2007,6 +2007,9 @@ pymelcloud==2.5.9 # homeassistant.components.meteoclimatic pymeteoclimatic==0.1.0 +# homeassistant.components.assist_pipeline +pymicro-vad==1.0.0 + # homeassistant.components.xiaomi_tv pymitv==1.4.3 @@ -2896,9 +2899,6 @@ weatherflow4py==0.2.21 # homeassistant.components.webmin webmin-xmlrpc==0.0.2 -# homeassistant.components.assist_pipeline -webrtc-noise-gain==1.2.3 - # homeassistant.components.whirlpool whirlpool-sixth-sense==0.18.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 463cbae4cdf..22de281aa61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1603,6 +1603,9 @@ pymelcloud==2.5.9 # homeassistant.components.meteoclimatic pymeteoclimatic==0.1.0 +# homeassistant.components.assist_pipeline +pymicro-vad==1.0.0 + # homeassistant.components.mochad pymochad==0.2.0 @@ -2282,9 +2285,6 @@ weatherflow4py==0.2.21 # homeassistant.components.webmin webmin-xmlrpc==0.0.2 -# homeassistant.components.assist_pipeline -webrtc-noise-gain==1.2.3 - # homeassistant.components.whirlpool whirlpool-sixth-sense==0.18.8 diff --git a/script/licenses.py b/script/licenses.py index f2298e473a2..ad5ae8476b3 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -172,7 +172,6 @@ EXCEPTIONS = { "tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5 "tellduslive", # https://github.com/molobrakos/tellduslive/pull/24 "tellsticknet", # https://github.com/molobrakos/tellsticknet/pull/33 - "webrtc_noise_gain", # https://github.com/rhasspy/webrtc-noise-gain/pull/24 "vincenty", # Public domain "zeversolar", # https://github.com/kvanzuijlen/zeversolar/pull/46 } diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index f9b91af3bf1..8fb7ce5b5a5 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -75,9 +75,7 @@ async def test_pipeline_from_audio_stream_auto( channel=stt.AudioChannels.CHANNEL_MONO, ), stt_stream=audio_data(), - audio_settings=assist_pipeline.AudioSettings( - is_vad_enabled=False, is_chunking_enabled=False - ), + audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), ) assert process_events(events) == snapshot @@ -140,9 +138,7 @@ async def test_pipeline_from_audio_stream_legacy( ), stt_stream=audio_data(), pipeline_id=pipeline_id, - audio_settings=assist_pipeline.AudioSettings( - is_vad_enabled=False, is_chunking_enabled=False - ), + audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), ) assert process_events(events) == snapshot @@ -205,9 +201,7 @@ async def test_pipeline_from_audio_stream_entity( ), stt_stream=audio_data(), pipeline_id=pipeline_id, - audio_settings=assist_pipeline.AudioSettings( - is_vad_enabled=False, is_chunking_enabled=False - ), + audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), ) assert process_events(events) == snapshot @@ -271,9 +265,7 @@ async def test_pipeline_from_audio_stream_no_stt( ), stt_stream=audio_data(), pipeline_id=pipeline_id, - audio_settings=assist_pipeline.AudioSettings( - is_vad_enabled=False, is_chunking_enabled=False - ), + audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), ) assert not events @@ -335,24 +327,25 @@ async def test_pipeline_from_audio_stream_wake_word( # [0, 2, ...] wake_chunk_2 = bytes(it.islice(it.cycle(range(0, 256, 2)), BYTES_ONE_SECOND)) - bytes_per_chunk = int(0.01 * BYTES_ONE_SECOND) + samples_per_chunk = 160 + bytes_per_chunk = samples_per_chunk * 2 # 16-bit async def audio_data(): - # 1 second in 10 ms chunks + # 1 second in chunks i = 0 while i < len(wake_chunk_1): yield wake_chunk_1[i : i + bytes_per_chunk] i += bytes_per_chunk - # 1 second in 30 ms chunks + # 1 second in chunks i = 0 while i < len(wake_chunk_2): yield wake_chunk_2[i : i + bytes_per_chunk] i += bytes_per_chunk - yield b"wake word!" - yield b"part1" - yield b"part2" + for chunk in (b"wake word!", b"part1", b"part2"): + yield chunk + bytes(bytes_per_chunk - len(chunk)) + yield b"" await assist_pipeline.async_pipeline_from_audio_stream( @@ -373,7 +366,7 @@ async def test_pipeline_from_audio_stream_wake_word( audio_seconds_to_buffer=1.5 ), audio_settings=assist_pipeline.AudioSettings( - is_vad_enabled=False, is_chunking_enabled=False + is_vad_enabled=False, samples_per_chunk=samples_per_chunk ), ) @@ -390,7 +383,9 @@ async def test_pipeline_from_audio_stream_wake_word( ) assert first_chunk == wake_chunk_1[len(wake_chunk_1) // 2 :] + wake_chunk_2 - assert mock_stt_provider.received[-3:] == [b"queued audio", b"part1", b"part2"] + assert mock_stt_provider.received[-3] == b"queued audio" + assert mock_stt_provider.received[-2].startswith(b"part1") + assert mock_stt_provider.received[-1].startswith(b"part2") async def test_pipeline_save_audio( @@ -438,9 +433,7 @@ async def test_pipeline_save_audio( pipeline_id=pipeline.id, start_stage=assist_pipeline.PipelineStage.WAKE_WORD, end_stage=assist_pipeline.PipelineStage.STT, - audio_settings=assist_pipeline.AudioSettings( - is_vad_enabled=False, is_chunking_enabled=False - ), + audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), ) pipeline_dirs = list(temp_dir.iterdir()) @@ -685,9 +678,7 @@ async def test_wake_word_detection_aborted( wake_word_settings=assist_pipeline.WakeWordSettings( audio_seconds_to_buffer=1.5 ), - audio_settings=assist_pipeline.AudioSettings( - is_vad_enabled=False, is_chunking_enabled=False - ), + audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), ), ) await pipeline_input.validate() diff --git a/tests/components/assist_pipeline/test_vad.py b/tests/components/assist_pipeline/test_vad.py index 139ae915263..17cb73a9139 100644 --- a/tests/components/assist_pipeline/test_vad.py +++ b/tests/components/assist_pipeline/test_vad.py @@ -1,11 +1,9 @@ """Tests for voice command segmenter.""" import itertools as it -from unittest.mock import patch from homeassistant.components.assist_pipeline.vad import ( AudioBuffer, - VoiceActivityDetector, VoiceCommandSegmenter, chunk_samples, ) @@ -44,59 +42,41 @@ def test_speech() -> None: def test_audio_buffer() -> None: """Test audio buffer wrapping.""" - class DisabledVad(VoiceActivityDetector): - def is_speech(self, chunk): - return False + samples_per_chunk = 160 # 10 ms + bytes_per_chunk = samples_per_chunk * 2 + leftover_buffer = AudioBuffer(bytes_per_chunk) - @property - def samples_per_chunk(self): - return 160 # 10 ms + # Partially fill audio buffer + half_chunk = bytes(it.islice(it.cycle(range(256)), bytes_per_chunk // 2)) + chunks = list(chunk_samples(half_chunk, bytes_per_chunk, leftover_buffer)) - vad = DisabledVad() - bytes_per_chunk = vad.samples_per_chunk * 2 - vad_buffer = AudioBuffer(bytes_per_chunk) - segmenter = VoiceCommandSegmenter() + assert not chunks + assert leftover_buffer.bytes() == half_chunk - with patch.object(vad, "is_speech", return_value=False) as mock_process: - # Partially fill audio buffer - half_chunk = bytes(it.islice(it.cycle(range(256)), bytes_per_chunk // 2)) - segmenter.process_with_vad(half_chunk, vad, vad_buffer) + # Fill and wrap with 1/4 chunk left over + three_quarters_chunk = bytes( + it.islice(it.cycle(range(256)), int(0.75 * bytes_per_chunk)) + ) + chunks = list(chunk_samples(three_quarters_chunk, bytes_per_chunk, leftover_buffer)) - assert not mock_process.called - assert vad_buffer is not None - assert vad_buffer.bytes() == half_chunk + assert len(chunks) == 1 + assert ( + leftover_buffer.bytes() + == three_quarters_chunk[len(three_quarters_chunk) - (bytes_per_chunk // 4) :] + ) + assert chunks[0] == half_chunk + three_quarters_chunk[: bytes_per_chunk // 2] - # Fill and wrap with 1/4 chunk left over - three_quarters_chunk = bytes( - it.islice(it.cycle(range(256)), int(0.75 * bytes_per_chunk)) - ) - segmenter.process_with_vad(three_quarters_chunk, vad, vad_buffer) + # Run 2 chunks through + leftover_buffer.clear() + assert len(leftover_buffer) == 0 - assert mock_process.call_count == 1 - assert ( - vad_buffer.bytes() - == three_quarters_chunk[ - len(three_quarters_chunk) - (bytes_per_chunk // 4) : - ] - ) - assert ( - mock_process.call_args[0][0] - == half_chunk + three_quarters_chunk[: bytes_per_chunk // 2] - ) + two_chunks = bytes(it.islice(it.cycle(range(256)), bytes_per_chunk * 2)) + chunks = list(chunk_samples(two_chunks, bytes_per_chunk, leftover_buffer)) - # Run 2 chunks through - segmenter.reset() - vad_buffer.clear() - assert len(vad_buffer) == 0 - - mock_process.reset_mock() - two_chunks = bytes(it.islice(it.cycle(range(256)), bytes_per_chunk * 2)) - segmenter.process_with_vad(two_chunks, vad, vad_buffer) - - assert mock_process.call_count == 2 - assert len(vad_buffer) == 0 - assert mock_process.call_args_list[0][0][0] == two_chunks[:bytes_per_chunk] - assert mock_process.call_args_list[1][0][0] == two_chunks[bytes_per_chunk:] + assert len(chunks) == 2 + assert len(leftover_buffer) == 0 + assert chunks[0] == two_chunks[:bytes_per_chunk] + assert chunks[1] == two_chunks[bytes_per_chunk:] def test_partial_chunk() -> None: @@ -125,43 +105,3 @@ def test_chunk_samples_leftover() -> None: assert len(chunks) == 1 assert leftover_chunk_buffer.bytes() == bytes([5, 6]) - - -def test_vad_no_chunking() -> None: - """Test VAD that doesn't require chunking.""" - - class VadNoChunk(VoiceActivityDetector): - def is_speech(self, chunk: bytes) -> bool: - return sum(chunk) > 0 - - @property - def samples_per_chunk(self) -> int | None: - return None - - vad = VadNoChunk() - segmenter = VoiceCommandSegmenter( - speech_seconds=1.0, silence_seconds=1.0, reset_seconds=0.5 - ) - silence = bytes([0] * 16000) - speech = bytes([255] * (16000 // 2)) - - # Test with differently-sized chunks - assert vad.is_speech(speech) - assert not vad.is_speech(silence) - - # Simulate voice command - assert segmenter.process_with_vad(silence, vad, None) - # begin - assert segmenter.process_with_vad(speech, vad, None) - assert segmenter.process_with_vad(speech, vad, None) - assert segmenter.process_with_vad(speech, vad, None) - # reset with silence - assert segmenter.process_with_vad(silence, vad, None) - # resume - assert segmenter.process_with_vad(speech, vad, None) - assert segmenter.process_with_vad(speech, vad, None) - assert segmenter.process_with_vad(speech, vad, None) - assert segmenter.process_with_vad(speech, vad, None) - # end - assert segmenter.process_with_vad(silence, vad, None) - assert not segmenter.process_with_vad(silence, vad, None) diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index de8ddc7ccc7..7d4a9b18c12 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -259,12 +259,7 @@ async def test_audio_pipeline_with_wake_word_no_timeout( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": { - "sample_rate": 16000, - "timeout": 0, - "no_vad": True, - "no_chunking": True, - }, + "input": {"sample_rate": 16000, "timeout": 0, "no_vad": True}, } ) @@ -1876,11 +1871,7 @@ async def test_wake_word_cooldown_same_id( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": { - "sample_rate": 16000, - "no_vad": True, - "no_chunking": True, - }, + "input": {"sample_rate": 16000, "no_vad": True}, } ) @@ -1889,11 +1880,7 @@ async def test_wake_word_cooldown_same_id( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": { - "sample_rate": 16000, - "no_vad": True, - "no_chunking": True, - }, + "input": {"sample_rate": 16000, "no_vad": True}, } ) @@ -1967,11 +1954,7 @@ async def test_wake_word_cooldown_different_ids( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": { - "sample_rate": 16000, - "no_vad": True, - "no_chunking": True, - }, + "input": {"sample_rate": 16000, "no_vad": True}, } ) @@ -1980,11 +1963,7 @@ async def test_wake_word_cooldown_different_ids( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": { - "sample_rate": 16000, - "no_vad": True, - "no_chunking": True, - }, + "input": {"sample_rate": 16000, "no_vad": True}, } ) @@ -2094,11 +2073,7 @@ async def test_wake_word_cooldown_different_entities( "pipeline": pipeline_id_1, "start_stage": "wake_word", "end_stage": "tts", - "input": { - "sample_rate": 16000, - "no_vad": True, - "no_chunking": True, - }, + "input": {"sample_rate": 16000, "no_vad": True}, } ) @@ -2109,11 +2084,7 @@ async def test_wake_word_cooldown_different_entities( "pipeline": pipeline_id_2, "start_stage": "wake_word", "end_stage": "tts", - "input": { - "sample_rate": 16000, - "no_vad": True, - "no_chunking": True, - }, + "input": {"sample_rate": 16000, "no_vad": True}, } ) @@ -2210,11 +2181,7 @@ async def test_device_capture( "type": "assist_pipeline/run", "start_stage": "stt", "end_stage": "stt", - "input": { - "sample_rate": 16000, - "no_vad": True, - "no_chunking": True, - }, + "input": {"sample_rate": 16000, "no_vad": True}, "device_id": satellite_device.id, } ) @@ -2315,11 +2282,7 @@ async def test_device_capture_override( "type": "assist_pipeline/run", "start_stage": "stt", "end_stage": "stt", - "input": { - "sample_rate": 16000, - "no_vad": True, - "no_chunking": True, - }, + "input": {"sample_rate": 16000, "no_vad": True}, "device_id": satellite_device.id, } ) @@ -2464,11 +2427,7 @@ async def test_device_capture_queue_full( "type": "assist_pipeline/run", "start_stage": "stt", "end_stage": "stt", - "input": { - "sample_rate": 16000, - "no_vad": True, - "no_chunking": True, - }, + "input": {"sample_rate": 16000, "no_vad": True}, "device_id": satellite_device.id, } ) diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index 6c292241237..c2978afc17f 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -43,9 +43,12 @@ async def test_pipeline( """Test that pipeline function is called from RTP protocol.""" assert await async_setup_component(hass, "voip", {}) - def is_speech(self, chunk): + def process_10ms(self, chunk): """Anything non-zero is speech.""" - return sum(chunk) > 0 + if sum(chunk) > 0: + return 1 + + return 0 done = asyncio.Event() @@ -98,8 +101,8 @@ async def test_pipeline( with ( patch( - "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", - new=is_speech, + "pymicro_vad.MicroVad.Process10ms", + new=process_10ms, ), patch( "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", @@ -238,9 +241,12 @@ async def test_tts_timeout( """Test that TTS will time out based on its length.""" assert await async_setup_component(hass, "voip", {}) - def is_speech(self, chunk): + def process_10ms(self, chunk): """Anything non-zero is speech.""" - return sum(chunk) > 0 + if sum(chunk) > 0: + return 1 + + return 0 done = asyncio.Event() @@ -298,8 +304,8 @@ async def test_tts_timeout( with ( patch( - "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", - new=is_speech, + "pymicro_vad.MicroVad.Process10ms", + new=process_10ms, ), patch( "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", @@ -361,9 +367,12 @@ async def test_tts_wrong_extension( """Test that TTS will only stream WAV audio.""" assert await async_setup_component(hass, "voip", {}) - def is_speech(self, chunk): + def process_10ms(self, chunk): """Anything non-zero is speech.""" - return sum(chunk) > 0 + if sum(chunk) > 0: + return 1 + + return 0 done = asyncio.Event() @@ -403,8 +412,8 @@ async def test_tts_wrong_extension( with ( patch( - "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", - new=is_speech, + "pymicro_vad.MicroVad.Process10ms", + new=process_10ms, ), patch( "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", @@ -456,9 +465,12 @@ async def test_tts_wrong_wav_format( """Test that TTS will only stream WAV audio with a specific format.""" assert await async_setup_component(hass, "voip", {}) - def is_speech(self, chunk): + def process_10ms(self, chunk): """Anything non-zero is speech.""" - return sum(chunk) > 0 + if sum(chunk) > 0: + return 1 + + return 0 done = asyncio.Event() @@ -505,8 +517,8 @@ async def test_tts_wrong_wav_format( with ( patch( - "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", - new=is_speech, + "pymicro_vad.MicroVad.Process10ms", + new=process_10ms, ), patch( "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", @@ -558,9 +570,12 @@ async def test_empty_tts_output( """Test that TTS will not stream when output is empty.""" assert await async_setup_component(hass, "voip", {}) - def is_speech(self, chunk): + def process_10ms(self, chunk): """Anything non-zero is speech.""" - return sum(chunk) > 0 + if sum(chunk) > 0: + return 1 + + return 0 async def async_pipeline_from_audio_stream(*args, **kwargs): stt_stream = kwargs["stt_stream"] @@ -591,8 +606,8 @@ async def test_empty_tts_output( with ( patch( - "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", - new=is_speech, + "pymicro_vad.MicroVad.Process10ms", + new=process_10ms, ), patch( "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", From e0a1aaa1b93f5302baf90b09548733aa622090d8 Mon Sep 17 00:00:00 2001 From: Paarth Shah Date: Wed, 31 Jul 2024 00:44:59 -0700 Subject: [PATCH 1762/2411] Fix matrix blocking call by running sync_forever in background_task (#122800) Fix blocking call by running sync_forever in background_task --- homeassistant/components/matrix/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 4c9af45e63f..77f13293519 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -209,15 +209,22 @@ class MatrixBot: await self._resolve_room_aliases(listening_rooms) self._load_commands(commands) await self._join_rooms() + # Sync once so that we don't respond to past events. + _LOGGER.debug("Starting initial sync for %s", self._mx_id) await self._client.sync(timeout=30_000) + _LOGGER.debug("Finished initial sync for %s", self._mx_id) self._client.add_event_callback(self._handle_room_message, RoomMessageText) - await self._client.sync_forever( - timeout=30_000, - loop_sleep_time=1_000, - ) # milliseconds. + _LOGGER.debug("Starting sync_forever for %s", self._mx_id) + self.hass.async_create_background_task( + self._client.sync_forever( + timeout=30_000, + loop_sleep_time=1_000, + ), # milliseconds. + name=f"{self.__class__.__name__}: sync_forever for '{self._mx_id}'", + ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, handle_startup) From 015a1a6ebc0e923f058cded62fdfe4ba1c9b97d8 Mon Sep 17 00:00:00 2001 From: Paarth Shah Date: Wed, 31 Jul 2024 00:45:30 -0700 Subject: [PATCH 1763/2411] Fix blocking event loop call in matrix (#122730) Wrap load_json_object in async_add_executor_job --- homeassistant/components/matrix/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 77f13293519..e1b488c0fce 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -349,7 +349,9 @@ class MatrixBot: async def _get_auth_tokens(self) -> JsonObjectType: """Read sorted authentication tokens from disk.""" try: - return load_json_object(self._session_filepath) + return await self.hass.async_add_executor_job( + load_json_object, self._session_filepath + ) except HomeAssistantError as ex: _LOGGER.warning( "Loading authentication tokens from file '%s' failed: %s", From f6f7459c364bd0684192cdd285f52a9703884871 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 31 Jul 2024 10:35:05 +0200 Subject: [PATCH 1764/2411] Add support for login credentials to homeworks (#122877) * Add support for login credentials to homeworks * Store credentials in config entry data --- .../components/homeworks/__init__.py | 20 ++- .../components/homeworks/config_flow.py | 50 ++++++- .../components/homeworks/strings.json | 19 ++- tests/components/homeworks/conftest.py | 99 ++++++++----- .../homeworks/test_binary_sensor.py | 2 +- .../components/homeworks/test_config_flow.py | 138 +++++++++++++++++- tests/components/homeworks/test_init.py | 57 +++++++- tests/components/homeworks/test_light.py | 4 +- 8 files changed, 331 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index cf39bc72ec6..448487cb8b0 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -9,7 +9,12 @@ import logging from typing import Any from pyhomeworks import exceptions as hw_exceptions -from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED, Homeworks +from pyhomeworks.pyhomeworks import ( + HW_BUTTON_PRESSED, + HW_BUTTON_RELEASED, + HW_LOGIN_INCORRECT, + Homeworks, +) import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -17,7 +22,9 @@ from homeassistant.const import ( CONF_HOST, CONF_ID, CONF_NAME, + CONF_PASSWORD, CONF_PORT, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, Platform, ) @@ -137,12 +144,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def hw_callback(msg_type: Any, values: Any) -> None: """Dispatch state changes.""" _LOGGER.debug("callback: %s, %s", msg_type, values) + if msg_type == HW_LOGIN_INCORRECT: + _LOGGER.debug("login incorrect") + return addr = values[0] signal = f"homeworks_entity_{controller_id}_{addr}" dispatcher_send(hass, signal, msg_type, values) config = entry.options - controller = Homeworks(config[CONF_HOST], config[CONF_PORT], hw_callback) + controller = Homeworks( + config[CONF_HOST], + config[CONF_PORT], + hw_callback, + entry.data.get(CONF_USERNAME), + entry.data.get(CONF_PASSWORD), + ) try: await hass.async_add_executor_job(controller.connect) except hw_exceptions.HomeworksException as err: diff --git a/homeassistant/components/homeworks/config_flow.py b/homeassistant/components/homeworks/config_flow.py index 4508f3bd21d..9247670b40b 100644 --- a/homeassistant/components/homeworks/config_flow.py +++ b/homeassistant/components/homeworks/config_flow.py @@ -14,7 +14,13 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) from homeassistant.core import async_get_hass, callback from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import ( @@ -62,6 +68,10 @@ CONTROLLER_EDIT = { mode=selector.NumberSelectorMode.BOX, ) ), + vol.Optional(CONF_USERNAME): selector.TextSelector(), + vol.Optional(CONF_PASSWORD): selector.TextSelector( + selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD) + ), } LIGHT_EDIT: VolDictType = { @@ -92,10 +102,17 @@ BUTTON_EDIT: VolDictType = { validate_addr = cv.matches_regex(r"\[(?:\d\d:){2,4}\d\d\]") +def _validate_credentials(user_input: dict[str, Any]) -> None: + """Validate credentials.""" + if CONF_PASSWORD in user_input and CONF_USERNAME not in user_input: + raise SchemaFlowError("need_username_with_password") + + async def validate_add_controller( handler: ConfigFlow | SchemaOptionsFlowHandler, user_input: dict[str, Any] ) -> dict[str, Any]: """Validate controller setup.""" + _validate_credentials(user_input) user_input[CONF_CONTROLLER_ID] = slugify(user_input[CONF_NAME]) user_input[CONF_PORT] = int(user_input[CONF_PORT]) try: @@ -128,7 +145,13 @@ async def _try_connection(user_input: dict[str, Any]) -> None: _LOGGER.debug( "Trying to connect to %s:%s", user_input[CONF_HOST], user_input[CONF_PORT] ) - controller = Homeworks(host, port, lambda msg_types, values: None) + controller = Homeworks( + host, + port, + lambda msg_types, values: None, + user_input.get(CONF_USERNAME), + user_input.get(CONF_PASSWORD), + ) controller.connect() controller.close() @@ -138,7 +161,14 @@ async def _try_connection(user_input: dict[str, Any]) -> None: _try_connect, user_input[CONF_HOST], user_input[CONF_PORT] ) except hw_exceptions.HomeworksConnectionFailed as err: + _LOGGER.debug("Caught HomeworksConnectionFailed") raise SchemaFlowError("connection_error") from err + except hw_exceptions.HomeworksInvalidCredentialsProvided as err: + _LOGGER.debug("Caught HomeworksInvalidCredentialsProvided") + raise SchemaFlowError("invalid_credentials") from err + except hw_exceptions.HomeworksNoCredentialsProvided as err: + _LOGGER.debug("Caught HomeworksNoCredentialsProvided") + raise SchemaFlowError("credentials_needed") from err except Exception as err: _LOGGER.exception("Caught unexpected exception %s") raise SchemaFlowError("unknown_error") from err @@ -529,6 +559,7 @@ class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] ) -> dict[str, Any]: """Validate controller setup.""" + _validate_credentials(user_input) user_input[CONF_PORT] = int(user_input[CONF_PORT]) our_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -569,12 +600,19 @@ class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN): except SchemaFlowError as err: errors["base"] = str(err) else: + password = user_input.pop(CONF_PASSWORD, None) + username = user_input.pop(CONF_USERNAME, None) + new_data = entry.data | { + CONF_PASSWORD: password, + CONF_USERNAME: username, + } new_options = entry.options | { CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT], } return self.async_update_reload_and_abort( entry, + data=new_data, options=new_options, reason="reconfigure_successful", reload_even_if_entry_is_unchanged=False, @@ -603,8 +641,14 @@ class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN): {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} ) name = user_input.pop(CONF_NAME) + password = user_input.pop(CONF_PASSWORD, None) + username = user_input.pop(CONF_USERNAME, None) user_input |= {CONF_DIMMERS: [], CONF_KEYPADS: []} - return self.async_create_entry(title=name, data={}, options=user_input) + return self.async_create_entry( + title=name, + data={CONF_PASSWORD: password, CONF_USERNAME: username}, + options=user_input, + ) return self.async_show_form( step_id="user", diff --git a/homeassistant/components/homeworks/strings.json b/homeassistant/components/homeworks/strings.json index b0d0f6e61e1..a9dcab2f1e0 100644 --- a/homeassistant/components/homeworks/strings.json +++ b/homeassistant/components/homeworks/strings.json @@ -2,8 +2,11 @@ "config": { "error": { "connection_error": "Could not connect to the controller.", + "credentials_needed": "The controller needs credentials.", "duplicated_controller_id": "The controller name is already in use.", "duplicated_host_port": "The specified host and port is already configured.", + "invalid_credentials": "The provided credentials are not valid.", + "need_username_with_password": "Credentials must be either a username and a password or only a username.", "unknown_error": "[%key:common::config_flow::error::unknown%]" }, "step": { @@ -22,7 +25,13 @@ "reconfigure": { "data": { "host": "[%key:common::config_flow::data::host%]", - "port": "[%key:common::config_flow::data::port%]" + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "password": "Optional password, leave blank if your system does not need credentials or only needs a single credential", + "username": "Optional username, leave blank if your system does not need login credentials" }, "description": "Modify a Lutron Homeworks controller connection settings" }, @@ -30,10 +39,14 @@ "data": { "host": "[%key:common::config_flow::data::host%]", "name": "Controller name", - "port": "[%key:common::config_flow::data::port%]" + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" }, "data_description": { - "name": "A unique name identifying the Lutron Homeworks controller" + "name": "A unique name identifying the Lutron Homeworks controller", + "password": "[%key:component::homeworks::config::step::reconfigure::data_description::password%]", + "username": "[%key:component::homeworks::config::step::reconfigure::data_description::username%]" }, "description": "Add a Lutron Homeworks controller" } diff --git a/tests/components/homeworks/conftest.py b/tests/components/homeworks/conftest.py index 86c3381b7a0..9562063ab97 100644 --- a/tests/components/homeworks/conftest.py +++ b/tests/components/homeworks/conftest.py @@ -17,10 +17,55 @@ from homeassistant.components.homeworks.const import ( CONF_RELEASE_DELAY, DOMAIN, ) -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) from tests.common import MockConfigEntry +CONFIG_ENTRY_OPTIONS = { + CONF_CONTROLLER_ID: "main_controller", + CONF_HOST: "192.168.0.1", + CONF_PORT: 1234, + CONF_DIMMERS: [ + { + CONF_ADDR: "[02:08:01:01]", + CONF_NAME: "Foyer Sconces", + CONF_RATE: 1.0, + } + ], + CONF_KEYPADS: [ + { + CONF_ADDR: "[02:08:02:01]", + CONF_NAME: "Foyer Keypad", + CONF_BUTTONS: [ + { + CONF_NAME: "Morning", + CONF_NUMBER: 1, + CONF_LED: True, + CONF_RELEASE_DELAY: None, + }, + { + CONF_NAME: "Relax", + CONF_NUMBER: 2, + CONF_LED: True, + CONF_RELEASE_DELAY: None, + }, + { + CONF_NAME: "Dim up", + CONF_NUMBER: 3, + CONF_LED: False, + CONF_RELEASE_DELAY: 0.2, + }, + ], + } + ], +} + @pytest.fixture def mock_config_entry() -> MockConfigEntry: @@ -28,45 +73,19 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry( title="Lutron Homeworks", domain=DOMAIN, - data={}, - options={ - CONF_CONTROLLER_ID: "main_controller", - CONF_HOST: "192.168.0.1", - CONF_PORT: 1234, - CONF_DIMMERS: [ - { - CONF_ADDR: "[02:08:01:01]", - CONF_NAME: "Foyer Sconces", - CONF_RATE: 1.0, - } - ], - CONF_KEYPADS: [ - { - CONF_ADDR: "[02:08:02:01]", - CONF_NAME: "Foyer Keypad", - CONF_BUTTONS: [ - { - CONF_NAME: "Morning", - CONF_NUMBER: 1, - CONF_LED: True, - CONF_RELEASE_DELAY: None, - }, - { - CONF_NAME: "Relax", - CONF_NUMBER: 2, - CONF_LED: True, - CONF_RELEASE_DELAY: None, - }, - { - CONF_NAME: "Dim up", - CONF_NUMBER: 3, - CONF_LED: False, - CONF_RELEASE_DELAY: 0.2, - }, - ], - } - ], - }, + data={CONF_PASSWORD: None, CONF_USERNAME: None}, + options=CONFIG_ENTRY_OPTIONS, + ) + + +@pytest.fixture +def mock_config_entry_username_password() -> MockConfigEntry: + """Return the default mocked config entry with credentials.""" + return MockConfigEntry( + title="Lutron Homeworks", + domain=DOMAIN, + data={CONF_PASSWORD: "hunter2", CONF_USERNAME: "username"}, + options=CONFIG_ENTRY_OPTIONS, ) diff --git a/tests/components/homeworks/test_binary_sensor.py b/tests/components/homeworks/test_binary_sensor.py index 0b21ae3b773..4bd42cc0a59 100644 --- a/tests/components/homeworks/test_binary_sensor.py +++ b/tests/components/homeworks/test_binary_sensor.py @@ -30,7 +30,7 @@ async def test_binary_sensor_attributes_state_update( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY, None, None) hw_callback = mock_homeworks.mock_calls[0][1][2] assert entity_id in hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN) diff --git a/tests/components/homeworks/test_config_flow.py b/tests/components/homeworks/test_config_flow.py index c4738e68ecc..d0693531006 100644 --- a/tests/components/homeworks/test_config_flow.py +++ b/tests/components/homeworks/test_config_flow.py @@ -18,7 +18,13 @@ from homeassistant.components.homeworks.const import ( DOMAIN, ) from homeassistant.config_entries import SOURCE_RECONFIGURE, SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -46,7 +52,7 @@ async def test_user_flow( ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Main controller" - assert result["data"] == {} + assert result["data"] == {"password": None, "username": None} assert result["options"] == { "controller_id": "main_controller", "dimmers": [], @@ -54,11 +60,109 @@ async def test_user_flow( "keypads": [], "port": 1234, } - mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY, None, None) mock_controller.close.assert_called_once_with() mock_controller.join.assert_not_called() +async def test_user_flow_credentials( + hass: HomeAssistant, mock_homeworks: MagicMock, mock_setup_entry +) -> None: + """Test the user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + mock_controller = MagicMock() + mock_homeworks.return_value = mock_controller + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + CONF_NAME: "Main controller", + CONF_PASSWORD: "hunter2", + CONF_PORT: 1234, + CONF_USERNAME: "username", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Main controller" + assert result["data"] == {"password": "hunter2", "username": "username"} + assert result["options"] == { + "controller_id": "main_controller", + "dimmers": [], + "host": "192.168.0.1", + "keypads": [], + "port": 1234, + } + mock_homeworks.assert_called_once_with( + "192.168.0.1", 1234, ANY, "username", "hunter2" + ) + mock_controller.close.assert_called_once_with() + mock_controller.join.assert_not_called() + + +async def test_user_flow_credentials_user_only( + hass: HomeAssistant, mock_homeworks: MagicMock, mock_setup_entry +) -> None: + """Test the user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + mock_controller = MagicMock() + mock_homeworks.return_value = mock_controller + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + CONF_NAME: "Main controller", + CONF_PORT: 1234, + CONF_USERNAME: "username", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Main controller" + assert result["data"] == {"password": None, "username": "username"} + assert result["options"] == { + "controller_id": "main_controller", + "dimmers": [], + "host": "192.168.0.1", + "keypads": [], + "port": 1234, + } + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY, "username", None) + mock_controller.close.assert_called_once_with() + mock_controller.join.assert_not_called() + + +async def test_user_flow_credentials_password_only( + hass: HomeAssistant, mock_homeworks: MagicMock, mock_setup_entry +) -> None: + """Test the user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + mock_controller = MagicMock() + mock_homeworks.return_value = mock_controller + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + CONF_NAME: "Main controller", + CONF_PASSWORD: "hunter2", + CONF_PORT: 1234, + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "need_username_with_password"} + + async def test_user_flow_already_exists( hass: HomeAssistant, mock_empty_config_entry: MockConfigEntry, mock_setup_entry ) -> None: @@ -99,6 +203,8 @@ async def test_user_flow_already_exists( ("side_effect", "error"), [ (hw_exceptions.HomeworksConnectionFailed, "connection_error"), + (hw_exceptions.HomeworksInvalidCredentialsProvided, "invalid_credentials"), + (hw_exceptions.HomeworksNoCredentialsProvided, "credentials_needed"), (Exception, "unknown_error"), ], ) @@ -270,6 +376,32 @@ async def test_reconfigure_flow_flow_no_change( } +async def test_reconfigure_flow_credentials_password_only( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test reconfigure flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.2", + CONF_PASSWORD: "hunter2", + CONF_PORT: 1234, + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + assert result["errors"] == {"base": "need_username_with_password"} + + async def test_options_add_light_flow( hass: HomeAssistant, mock_empty_config_entry: MockConfigEntry, diff --git a/tests/components/homeworks/test_init.py b/tests/components/homeworks/test_init.py index 2363e0f157d..2a4bd28138e 100644 --- a/tests/components/homeworks/test_init.py +++ b/tests/components/homeworks/test_init.py @@ -3,7 +3,11 @@ from unittest.mock import ANY, MagicMock from pyhomeworks import exceptions as hw_exceptions -from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED +from pyhomeworks.pyhomeworks import ( + HW_BUTTON_PRESSED, + HW_BUTTON_RELEASED, + HW_LOGIN_INCORRECT, +) import pytest from homeassistant.components.homeworks import EVENT_BUTTON_PRESS, EVENT_BUTTON_RELEASE @@ -27,7 +31,7 @@ async def test_load_unload_config_entry( await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.LOADED - mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY, None, None) await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -36,6 +40,51 @@ async def test_load_unload_config_entry( assert mock_config_entry.state is ConfigEntryState.NOT_LOADED +async def test_load_config_entry_with_credentials( + hass: HomeAssistant, + mock_config_entry_username_password: MockConfigEntry, + mock_homeworks: MagicMock, +) -> None: + """Test the Homeworks configuration entry loading/unloading.""" + mock_config_entry_username_password.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry_username_password.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry_username_password.state is ConfigEntryState.LOADED + mock_homeworks.assert_called_once_with( + "192.168.0.1", 1234, ANY, "username", "hunter2" + ) + + await hass.config_entries.async_unload(mock_config_entry_username_password.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry_username_password.state is ConfigEntryState.NOT_LOADED + + +async def test_controller_credentials_changed( + hass: HomeAssistant, + mock_config_entry_username_password: MockConfigEntry, + mock_homeworks: MagicMock, +) -> None: + """Test controller credentials changed. + + Note: This just ensures we don't blow up when credentials changed, in the future a + reauth flow should be added. + """ + mock_config_entry_username_password.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry_username_password.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry_username_password.state is ConfigEntryState.LOADED + mock_homeworks.assert_called_once_with( + "192.168.0.1", 1234, ANY, "username", "hunter2" + ) + hw_callback = mock_homeworks.mock_calls[0][1][2] + + hw_callback(HW_LOGIN_INCORRECT, []) + + async def test_config_entry_not_ready( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -66,7 +115,7 @@ async def test_keypad_events( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY, None, None) hw_callback = mock_homeworks.mock_calls[0][1][2] hw_callback(HW_BUTTON_PRESSED, ["[02:08:02:01]", 1]) @@ -184,7 +233,7 @@ async def test_cleanup_on_ha_shutdown( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY, None, None) mock_controller.stop.assert_not_called() hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) diff --git a/tests/components/homeworks/test_light.py b/tests/components/homeworks/test_light.py index a5d94f736d5..1cd2951128c 100644 --- a/tests/components/homeworks/test_light.py +++ b/tests/components/homeworks/test_light.py @@ -35,7 +35,7 @@ async def test_light_attributes_state_update( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY, None, None) hw_callback = mock_homeworks.mock_calls[0][1][2] assert len(mock_controller.request_dimmer_level.mock_calls) == 1 @@ -106,7 +106,7 @@ async def test_light_restore_brightness( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY, None, None) hw_callback = mock_homeworks.mock_calls[0][1][2] assert hass.states.async_entity_ids("light") == unordered([entity_id]) From 222011fc5cd38184337fb15ef75f78a7ce14d00e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 31 Jul 2024 10:36:46 +0200 Subject: [PATCH 1765/2411] Log tests in test group (#122892) * Log tests in test group * Simplify print --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 142839e77ff..b705064e078 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -904,6 +904,7 @@ jobs: cov_params+=(--cov-report=xml) fi + echo "Test group ${{ matrix.group }}: $(sed -n "${{ matrix.group }},1p" pytest_buckets.txt)" python3 -b -X dev -m pytest \ -qq \ --timeout=9 \ From 67ed8b207aa0dfb0fb6158ca4b1602bafbdc9ebd Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 31 Jul 2024 11:08:05 +0200 Subject: [PATCH 1766/2411] KNX: use xknx 3.0.0 eager telegram decoding (#122896) * Use KNX xknx 3.0.0 eager telegram decoding * review suggestion --- homeassistant/components/knx/__init__.py | 2 +- homeassistant/components/knx/project.py | 17 +++++++-- homeassistant/components/knx/telegrams.py | 45 ++++++++++++----------- homeassistant/components/knx/websocket.py | 1 + tests/components/knx/test_websocket.py | 2 +- 5 files changed, 41 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 99b461dda1b..709a82b31fd 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -333,7 +333,7 @@ class KNXModule: async def start(self) -> None: """Start XKNX object. Connect to tunneling or Routing device.""" - await self.project.load_project() + await self.project.load_project(self.xknx) await self.config_store.load_data() await self.telegrams.load_history() await self.xknx.start() diff --git a/homeassistant/components/knx/project.py b/homeassistant/components/knx/project.py index 13e71dbbe38..3b3309dfc7d 100644 --- a/homeassistant/components/knx/project.py +++ b/homeassistant/components/knx/project.py @@ -6,6 +6,7 @@ from dataclasses import dataclass import logging from typing import Final +from xknx import XKNX from xknx.dpt import DPTBase from xknxproject import XKNXProj from xknxproject.models import ( @@ -80,15 +81,23 @@ class KNXProject: self.group_addresses = {} self.info = None - async def load_project(self, data: KNXProjectModel | None = None) -> None: + async def load_project( + self, xknx: XKNX, data: KNXProjectModel | None = None + ) -> None: """Load project data from storage.""" if project := data or await self._store.async_load(): self.devices = project["devices"] self.info = project["info"] + xknx.group_address_dpt.clear() + xknx_ga_dict = {} for ga_model in project["group_addresses"].values(): ga_info = _create_group_address_info(ga_model) self.group_addresses[ga_info.address] = ga_info + if (dpt_model := ga_model.get("dpt")) is not None: + xknx_ga_dict[ga_model["address"]] = dpt_model + + xknx.group_address_dpt.set(xknx_ga_dict) # type: ignore[arg-type] _LOGGER.debug( "Loaded KNX project data with %s group addresses from storage", @@ -96,7 +105,9 @@ class KNXProject: ) self.loaded = True - async def process_project_file(self, file_id: str, password: str) -> None: + async def process_project_file( + self, xknx: XKNX, file_id: str, password: str + ) -> None: """Process an uploaded project file.""" def _parse_project() -> KNXProjectModel: @@ -110,7 +121,7 @@ class KNXProject: project = await self.hass.async_add_executor_job(_parse_project) await self._store.async_save(project) - await self.load_project(data=project) + await self.load_project(xknx, data=project) async def remove_project_file(self) -> None: """Remove project file from storage.""" diff --git a/homeassistant/components/knx/telegrams.py b/homeassistant/components/knx/telegrams.py index 2ad46326b8e..a96d841a07d 100644 --- a/homeassistant/components/knx/telegrams.py +++ b/homeassistant/components/knx/telegrams.py @@ -35,7 +35,7 @@ class DecodedTelegramPayload(TypedDict): dpt_sub: int | None dpt_name: str | None unit: str | None - value: str | int | float | bool | None + value: bool | str | int | float | dict[str, str | int | float | bool] | None class TelegramDict(DecodedTelegramPayload): @@ -106,7 +106,7 @@ class Telegrams: payload_data: int | tuple[int, ...] | None = None src_name = "" transcoder = None - decoded_payload: DecodedTelegramPayload | None = None + value = None if ( ga_info := self.project.group_addresses.get( @@ -114,7 +114,6 @@ class Telegrams: ) ) is not None: dst_name = ga_info.name - transcoder = ga_info.transcoder if ( device := self.project.devices.get(f"{telegram.source_address}") @@ -123,45 +122,49 @@ class Telegrams: if isinstance(telegram.payload, (GroupValueWrite, GroupValueResponse)): payload_data = telegram.payload.value.value - if transcoder is not None: - decoded_payload = decode_telegram_payload( - payload=telegram.payload.value, transcoder=transcoder - ) + + if telegram.decoded_data is not None: + transcoder = telegram.decoded_data.transcoder + value = _serializable_decoded_data(telegram.decoded_data.value) return TelegramDict( destination=f"{telegram.destination_address}", destination_name=dst_name, direction=telegram.direction.value, - dpt_main=decoded_payload["dpt_main"] - if decoded_payload is not None - else None, - dpt_sub=decoded_payload["dpt_sub"] if decoded_payload is not None else None, - dpt_name=decoded_payload["dpt_name"] - if decoded_payload is not None - else None, + dpt_main=transcoder.dpt_main_number if transcoder is not None else None, + dpt_sub=transcoder.dpt_sub_number if transcoder is not None else None, + dpt_name=transcoder.value_type if transcoder is not None else None, payload=payload_data, source=f"{telegram.source_address}", source_name=src_name, telegramtype=telegram.payload.__class__.__name__, timestamp=dt_util.now().isoformat(), - unit=decoded_payload["unit"] if decoded_payload is not None else None, - value=decoded_payload["value"] if decoded_payload is not None else None, + unit=transcoder.unit if transcoder is not None else None, + value=value, ) +def _serializable_decoded_data( + value: bool | float | str | DPTComplexData | DPTEnumData, +) -> bool | str | int | float | dict[str, str | int | float | bool]: + """Return a serializable representation of decoded data.""" + if isinstance(value, DPTComplexData): + return value.as_dict() + if isinstance(value, DPTEnumData): + return value.name.lower() + return value + + def decode_telegram_payload( payload: DPTArray | DPTBinary, transcoder: type[DPTBase] ) -> DecodedTelegramPayload: - """Decode the payload of a KNX telegram.""" + """Decode the payload of a KNX telegram with custom transcoder.""" try: value = transcoder.from_knx(payload) except XKNXException: value = "Error decoding value" - if isinstance(value, DPTComplexData): - value = value.as_dict() - elif isinstance(value, DPTEnumData): - value = value.name.lower() + value = _serializable_decoded_data(value) return DecodedTelegramPayload( dpt_main=transcoder.dpt_main_number, diff --git a/homeassistant/components/knx/websocket.py b/homeassistant/components/knx/websocket.py index 97758dc87c9..4af3012741a 100644 --- a/homeassistant/components/knx/websocket.py +++ b/homeassistant/components/knx/websocket.py @@ -154,6 +154,7 @@ async def ws_project_file_process( knx: KNXModule = hass.data[DOMAIN] try: await knx.project.process_project_file( + xknx=knx.xknx, file_id=msg["file_id"], password=msg["password"], ) diff --git a/tests/components/knx/test_websocket.py b/tests/components/knx/test_websocket.py index eb22bac85bc..309ea111709 100644 --- a/tests/components/knx/test_websocket.py +++ b/tests/components/knx/test_websocket.py @@ -346,7 +346,7 @@ async def test_knx_subscribe_telegrams_command_project( assert res["event"]["destination"] == "0/1/1" assert res["event"]["destination_name"] == "percent" assert res["event"]["payload"] == 1 - assert res["event"]["value"] == "Error decoding value" + assert res["event"]["value"] is None assert res["event"]["telegramtype"] == "GroupValueWrite" assert res["event"]["source"] == "1.1.6" assert ( From 68f06e63e29ba31996d097392e068a6fd27ddb35 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 31 Jul 2024 10:32:13 +0100 Subject: [PATCH 1767/2411] Bump pytrydan to 0.8.0 (#122898) bump pytrydan to 0.8.0 --- homeassistant/components/v2c/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/v2c/snapshots/test_diagnostics.ambr | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/v2c/manifest.json b/homeassistant/components/v2c/manifest.json index ffe4b52ee6e..3a6eab0f335 100644 --- a/homeassistant/components/v2c/manifest.json +++ b/homeassistant/components/v2c/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/v2c", "iot_class": "local_polling", - "requirements": ["pytrydan==0.7.0"] + "requirements": ["pytrydan==0.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index bc1a13f19ab..6ba6601597d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2378,7 +2378,7 @@ pytradfri[async]==9.0.1 pytrafikverket==1.0.0 # homeassistant.components.v2c -pytrydan==0.7.0 +pytrydan==0.8.0 # homeassistant.components.usb pyudev==0.24.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22de281aa61..da3ac4cd662 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1881,7 +1881,7 @@ pytradfri[async]==9.0.1 pytrafikverket==1.0.0 # homeassistant.components.v2c -pytrydan==0.7.0 +pytrydan==0.8.0 # homeassistant.components.usb pyudev==0.24.1 diff --git a/tests/components/v2c/snapshots/test_diagnostics.ambr b/tests/components/v2c/snapshots/test_diagnostics.ambr index a4f6cad4cc8..cc34cae87f8 100644 --- a/tests/components/v2c/snapshots/test_diagnostics.ambr +++ b/tests/components/v2c/snapshots/test_diagnostics.ambr @@ -18,7 +18,7 @@ 'unique_id': 'ABC123', 'version': 1, }), - 'data': "TrydanData(ID='ABC123', charge_state=, ready_state=, charge_power=1500.27, charge_energy=1.8, slave_error=, charge_time=4355, house_power=0.0, fv_power=0.0, battery_power=0.0, paused=, locked=, timer=, intensity=6, dynamic=, min_intensity=6, max_intensity=16, pause_dynamic=, dynamic_power_mode=, contracted_power=4600, firmware_version='2.1.7')", + 'data': "TrydanData(ID='ABC123', charge_state=, ready_state=, charge_power=1500.27, voltage_installation=None, charge_energy=1.8, slave_error=, charge_time=4355, house_power=0.0, fv_power=0.0, battery_power=0.0, paused=, locked=, timer=, intensity=6, dynamic=, min_intensity=6, max_intensity=16, pause_dynamic=, dynamic_power_mode=, contracted_power=4600, firmware_version='2.1.7', SSID=None, IP=None, signal_status=None)", 'host_status': 200, 'raw_data': '{"ID":"ABC123","ChargeState":2,"ReadyState":0,"ChargePower":1500.27,"ChargeEnergy":1.8,"SlaveError":4,"ChargeTime":4355,"HousePower":0.0,"FVPower":0.0,"BatteryPower":0.0,"Paused":0,"Locked":0,"Timer":0,"Intensity":6,"Dynamic":0,"MinIntensity":6,"MaxIntensity":16,"PauseDynamic":0,"FirmwareVersion":"2.1.7","DynamicPowerMode":2,"ContractedPower":4600}', }) From 8b4f607806e263eea3aa9e944328c5bd8633ce47 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 11:39:51 +0200 Subject: [PATCH 1768/2411] Fix implicit-return in plant (#122903) --- homeassistant/components/plant/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index b549dee2887..2a5253d3faa 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -268,6 +268,7 @@ class Plant(Entity): min_value = self._config[params["min"]] if value < min_value: return f"{sensor_name} low" + return None def _check_max(self, sensor_name, value, params): """If configured, check the value against the defined maximum value.""" From 233c04a469b283a66f21e4acec7594377d527442 Mon Sep 17 00:00:00 2001 From: Alex MF Date: Wed, 31 Jul 2024 11:22:07 +0100 Subject: [PATCH 1769/2411] Add number entity for Ecovacs mower cut direction (#122598) --- homeassistant/components/ecovacs/icons.json | 3 + homeassistant/components/ecovacs/number.py | 16 ++- homeassistant/components/ecovacs/strings.json | 3 + .../ecovacs/snapshots/test_number.ambr | 111 ++++++++++++++++++ tests/components/ecovacs/test_init.py | 2 +- tests/components/ecovacs/test_number.py | 28 ++++- 6 files changed, 156 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ecovacs/icons.json b/homeassistant/components/ecovacs/icons.json index d129273e891..0c7178ced84 100644 --- a/homeassistant/components/ecovacs/icons.json +++ b/homeassistant/components/ecovacs/icons.json @@ -43,6 +43,9 @@ "clean_count": { "default": "mdi:counter" }, + "cut_direction": { + "default": "mdi:angle-acute" + }, "volume": { "default": "mdi:volume-high", "state": { diff --git a/homeassistant/components/ecovacs/number.py b/homeassistant/components/ecovacs/number.py index 3b24091ca34..2b9bdc1a425 100644 --- a/homeassistant/components/ecovacs/number.py +++ b/homeassistant/components/ecovacs/number.py @@ -7,14 +7,14 @@ from dataclasses import dataclass from typing import Generic from deebot_client.capabilities import CapabilitySet -from deebot_client.events import CleanCountEvent, VolumeEvent +from deebot_client.events import CleanCountEvent, CutDirectionEvent, VolumeEvent from homeassistant.components.number import ( NumberEntity, NumberEntityDescription, NumberMode, ) -from homeassistant.const import EntityCategory +from homeassistant.const import DEGREE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -53,6 +53,18 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsNumberEntityDescription, ...] = ( native_max_value=10, native_step=1.0, ), + EcovacsNumberEntityDescription[CutDirectionEvent]( + capability_fn=lambda caps: caps.settings.cut_direction, + value_fn=lambda e: e.angle, + key="cut_direction", + translation_key="cut_direction", + entity_registry_enabled_default=False, + entity_category=EntityCategory.CONFIG, + native_min_value=0, + native_max_value=180, + native_step=1.0, + native_unit_of_measurement=DEGREE, + ), EcovacsNumberEntityDescription[CleanCountEvent]( capability_fn=lambda caps: caps.clean.count, value_fn=lambda e: e.count, diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index d501c333a03..d2e385c79c7 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -91,6 +91,9 @@ "clean_count": { "name": "Clean count" }, + "cut_direction": { + "name": "Cut direction" + }, "volume": { "name": "Volume" } diff --git a/tests/components/ecovacs/snapshots/test_number.ambr b/tests/components/ecovacs/snapshots/test_number.ambr index da8406491b4..c80132784e1 100644 --- a/tests/components/ecovacs/snapshots/test_number.ambr +++ b/tests/components/ecovacs/snapshots/test_number.ambr @@ -1,4 +1,115 @@ # serializer version: 1 +# name: test_number_entities[5xu9h3][number.goat_g1_cut_direction:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 180, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.goat_g1_cut_direction', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cut direction', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cut_direction', + 'unique_id': '8516fbb1-17f1-4194-0000000_cut_direction', + 'unit_of_measurement': '°', + }) +# --- +# name: test_number_entities[5xu9h3][number.goat_g1_cut_direction:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Cut direction', + 'max': 180, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'number.goat_g1_cut_direction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45', + }) +# --- +# name: test_number_entities[5xu9h3][number.goat_g1_volume:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 11, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.goat_g1_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Volume', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'volume', + 'unique_id': '8516fbb1-17f1-4194-0000000_volume', + 'unit_of_measurement': None, + }) +# --- +# name: test_number_entities[5xu9h3][number.goat_g1_volume:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Volume', + 'max': 11, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.goat_g1_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- # name: test_number_entities[yna5x1][number.ozmo_950_volume:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py index c0c475217c1..ac4d5661a83 100644 --- a/tests/components/ecovacs/test_init.py +++ b/tests/components/ecovacs/test_init.py @@ -136,7 +136,7 @@ async def test_devices_in_dr( ("device_fixture", "entities"), [ ("yna5x1", 26), - ("5xu9h3", 24), + ("5xu9h3", 25), ("123", 1), ], ) diff --git a/tests/components/ecovacs/test_number.py b/tests/components/ecovacs/test_number.py index d444d6510a8..a735863d40a 100644 --- a/tests/components/ecovacs/test_number.py +++ b/tests/components/ecovacs/test_number.py @@ -3,8 +3,8 @@ from dataclasses import dataclass from deebot_client.command import Command -from deebot_client.commands.json import SetVolume -from deebot_client.events import Event, VolumeEvent +from deebot_client.commands.json import SetCutDirection, SetVolume +from deebot_client.events import CutDirectionEvent, Event, VolumeEvent import pytest from syrupy import SnapshotAssertion @@ -53,8 +53,23 @@ class NumberTestCase: ), ], ), + ( + "5xu9h3", + [ + NumberTestCase( + "number.goat_g1_volume", VolumeEvent(3, 11), "3", 7, SetVolume(7) + ), + NumberTestCase( + "number.goat_g1_cut_direction", + CutDirectionEvent(45), + "45", + 97, + SetCutDirection(97), + ), + ], + ), ], - ids=["yna5x1"], + ids=["yna5x1", "5xu9h3"], ) async def test_number_entities( hass: HomeAssistant, @@ -107,8 +122,12 @@ async def test_number_entities( "yna5x1", ["number.ozmo_950_volume"], ), + ( + "5xu9h3", + ["number.goat_g1_cut_direction", "number.goat_g1_volume"], + ), ], - ids=["yna5x1"], + ids=["yna5x1", "5xu9h3"], ) async def test_disabled_by_default_number_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, entity_ids: list[str] @@ -125,6 +144,7 @@ async def test_disabled_by_default_number_entities( @pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize(("device_fixture"), ["yna5x1"]) async def test_volume_maximum( hass: HomeAssistant, controller: EcovacsController, From 02d4d1a75ba664bb76e8be9cd6d6faf1644017b9 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 31 Jul 2024 11:31:35 +0100 Subject: [PATCH 1770/2411] Adds new sensors and configuration entities to V2C Trydan (#122883) * Adds new controls and sensors * update snapshot * Update homeassistant/components/v2c/strings.json Co-authored-by: Charles Garwood * Add unit * move icons to icons.json * update snapshot * missing translation fix --------- Co-authored-by: Charles Garwood --- homeassistant/components/v2c/icons.json | 9 + homeassistant/components/v2c/number.py | 24 +++ homeassistant/components/v2c/sensor.py | 41 +++- homeassistant/components/v2c/strings.json | 18 ++ .../components/v2c/snapshots/test_sensor.ambr | 197 +++++++++++++++++- 5 files changed, 284 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/v2c/icons.json b/homeassistant/components/v2c/icons.json index 1b76b669956..6b0a41bf752 100644 --- a/homeassistant/components/v2c/icons.json +++ b/homeassistant/components/v2c/icons.json @@ -21,6 +21,15 @@ }, "battery_power": { "default": "mdi:home-battery" + }, + "ssid": { + "default": "mdi:wifi" + }, + "ip_address": { + "default": "mdi:ip" + }, + "signal_status": { + "default": "mdi:signal" } }, "switch": { diff --git a/homeassistant/components/v2c/number.py b/homeassistant/components/v2c/number.py index 2ff70226132..1540b098cf1 100644 --- a/homeassistant/components/v2c/number.py +++ b/homeassistant/components/v2c/number.py @@ -13,6 +13,7 @@ from homeassistant.components.number import ( NumberEntity, NumberEntityDescription, ) +from homeassistant.const import EntityCategory, UnitOfElectricCurrent from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -37,11 +38,34 @@ TRYDAN_NUMBER_SETTINGS = ( key="intensity", translation_key="intensity", device_class=NumberDeviceClass.CURRENT, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, native_min_value=MIN_INTENSITY, native_max_value=MAX_INTENSITY, value_fn=lambda evse_data: evse_data.intensity, update_fn=lambda evse, value: evse.intensity(value), ), + V2CSettingsNumberEntityDescription( + key="min_intensity", + translation_key="min_intensity", + device_class=NumberDeviceClass.CURRENT, + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + native_min_value=MIN_INTENSITY, + native_max_value=MAX_INTENSITY, + value_fn=lambda evse_data: evse_data.min_intensity, + update_fn=lambda evse, value: evse.min_intensity(value), + ), + V2CSettingsNumberEntityDescription( + key="max_intensity", + translation_key="max_intensity", + device_class=NumberDeviceClass.CURRENT, + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + native_min_value=MIN_INTENSITY, + native_max_value=MAX_INTENSITY, + value_fn=lambda evse_data: evse_data.max_intensity, + update_fn=lambda evse, value: evse.max_intensity(value), + ), ) diff --git a/homeassistant/components/v2c/sensor.py b/homeassistant/components/v2c/sensor.py index fc0cc0bfaa8..97853740e9d 100644 --- a/homeassistant/components/v2c/sensor.py +++ b/homeassistant/components/v2c/sensor.py @@ -15,7 +15,13 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import UnitOfEnergy, UnitOfPower, UnitOfTime +from homeassistant.const import ( + EntityCategory, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfTime, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -45,12 +51,20 @@ TRYDAN_SENSORS = ( V2CSensorEntityDescription( key="charge_power", translation_key="charge_power", - icon="mdi:ev-station", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, value_fn=lambda evse_data: evse_data.charge_power, ), + V2CSensorEntityDescription( + key="voltage_installation", + translation_key="voltage_installation", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + value_fn=lambda evse_data: evse_data.voltage_installation, + entity_registry_enabled_default=False, + ), V2CSensorEntityDescription( key="charge_energy", translation_key="charge_energy", @@ -86,6 +100,7 @@ TRYDAN_SENSORS = ( V2CSensorEntityDescription( key="meter_error", translation_key="meter_error", + entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda evse_data: get_meter_value(evse_data.slave_error), entity_registry_enabled_default=False, device_class=SensorDeviceClass.ENUM, @@ -100,6 +115,28 @@ TRYDAN_SENSORS = ( value_fn=lambda evse_data: evse_data.battery_power, entity_registry_enabled_default=False, ), + V2CSensorEntityDescription( + key="ssid", + translation_key="ssid", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda evse_data: evse_data.SSID, + entity_registry_enabled_default=False, + ), + V2CSensorEntityDescription( + key="ip_address", + translation_key="ip_address", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda evse_data: evse_data.IP, + entity_registry_enabled_default=False, + ), + V2CSensorEntityDescription( + key="signal_status", + translation_key="signal_status", + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda evse_data: evse_data.signal_status, + entity_registry_enabled_default=False, + ), ) diff --git a/homeassistant/components/v2c/strings.json b/homeassistant/components/v2c/strings.json index 3342652cfb4..d52b8f066f9 100644 --- a/homeassistant/components/v2c/strings.json +++ b/homeassistant/components/v2c/strings.json @@ -33,12 +33,21 @@ "number": { "intensity": { "name": "Intensity" + }, + "max_intensity": { + "name": "Max intensity" + }, + "min_intensity": { + "name": "Min intensity" } }, "sensor": { "charge_power": { "name": "Charge power" }, + "voltage_installation": { + "name": "Installation voltage" + }, "charge_energy": { "name": "Charge energy" }, @@ -93,6 +102,15 @@ "empty_message": "Empty message", "undefined_error": "Undefined error" } + }, + "ssid": { + "name": "SSID" + }, + "ip_address": { + "name": "IP address" + }, + "signal_status": { + "name": "Signal status" } }, "switch": { diff --git a/tests/components/v2c/snapshots/test_sensor.ambr b/tests/components/v2c/snapshots/test_sensor.ambr index cc8077333cb..7b9ae4a9ff3 100644 --- a/tests/components/v2c/snapshots/test_sensor.ambr +++ b/tests/components/v2c/snapshots/test_sensor.ambr @@ -126,7 +126,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Charge power', 'platform': 'v2c', 'previous_unique_id': None, @@ -141,7 +141,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'power', 'friendly_name': 'EVSE 1.1.1.1 Charge power', - 'icon': 'mdi:ev-station', 'state_class': , 'unit_of_measurement': , }), @@ -255,6 +254,103 @@ 'state': '0.0', }) # --- +# name: test_sensor[sensor.evse_1_1_1_1_installation_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.evse_1_1_1_1_installation_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Installation voltage', + 'platform': 'v2c', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_installation', + 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_voltage_installation', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.evse_1_1_1_1_installation_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'EVSE 1.1.1.1 Installation voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.evse_1_1_1_1_installation_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.evse_1_1_1_1_ip_address-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.evse_1_1_1_1_ip_address', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'IP address', + 'platform': 'v2c', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ip_address', + 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_ip_address', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.evse_1_1_1_1_ip_address-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'EVSE 1.1.1.1 IP address', + }), + 'context': , + 'entity_id': 'sensor.evse_1_1_1_1_ip_address', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_sensor[sensor.evse_1_1_1_1_meter_error-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -304,7 +400,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.evse_1_1_1_1_meter_error', 'has_entity_name': True, 'hidden_by': None, @@ -428,3 +524,98 @@ 'state': '0.0', }) # --- +# name: test_sensor[sensor.evse_1_1_1_1_signal_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.evse_1_1_1_1_signal_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Signal status', + 'platform': 'v2c', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'signal_status', + 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_signal_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.evse_1_1_1_1_signal_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'EVSE 1.1.1.1 Signal status', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.evse_1_1_1_1_signal_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.evse_1_1_1_1_ssid-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.evse_1_1_1_1_ssid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SSID', + 'platform': 'v2c', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ssid', + 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_ssid', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.evse_1_1_1_1_ssid-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'EVSE 1.1.1.1 SSID', + }), + 'context': , + 'entity_id': 'sensor.evse_1_1_1_1_ssid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- From 6a45124878f66adf1b52ac642322f41b2e4ff2fc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:38:15 +0200 Subject: [PATCH 1771/2411] Fix implicit-return in qnap (#122901) --- homeassistant/components/qnap/sensor.py | 34 +++++++++++++++++++++---- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index e1739a900ce..526516bfcdd 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta +from typing import Any from homeassistant import config_entries from homeassistant.components.sensor import ( @@ -348,6 +349,8 @@ class QNAPCPUSensor(QNAPSensor): if self.entity_description.key == "cpu_usage": return self.coordinator.data["system_stats"]["cpu"]["usage_percent"] + return None + class QNAPMemorySensor(QNAPSensor): """A QNAP sensor that monitors memory stats.""" @@ -370,20 +373,25 @@ class QNAPMemorySensor(QNAPSensor): if self.entity_description.key == "memory_percent_used": return used / total * 100 + return None + # Deprecated since Home Assistant 2024.6.0 # Can be removed completely in 2024.12.0 @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" if self.coordinator.data: data = self.coordinator.data["system_stats"]["memory"] size = round(float(data["total"]) / 1024, 2) return {ATTR_MEMORY_SIZE: f"{size} {UnitOfInformation.GIBIBYTES}"} + return None class QNAPNetworkSensor(QNAPSensor): """A QNAP sensor that monitors network stats.""" + monitor_device: str + @property def native_value(self): """Return the state of the sensor.""" @@ -404,10 +412,12 @@ class QNAPNetworkSensor(QNAPSensor): if self.entity_description.key == "network_rx": return data["rx"] + return None + # Deprecated since Home Assistant 2024.6.0 # Can be removed completely in 2024.12.0 @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" if self.coordinator.data: data = self.coordinator.data["system_stats"]["nics"][self.monitor_device] @@ -418,6 +428,7 @@ class QNAPNetworkSensor(QNAPSensor): ATTR_MAX_SPEED: data["max_speed"], ATTR_PACKETS_ERR: data["err_packets"], } + return None class QNAPSystemSensor(QNAPSensor): @@ -442,10 +453,12 @@ class QNAPSystemSensor(QNAPSensor): ) return dt_util.now() - uptime_duration + return None + # Deprecated since Home Assistant 2024.6.0 # Can be removed completely in 2024.12.0 @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" if self.coordinator.data: data = self.coordinator.data["system_stats"] @@ -459,11 +472,14 @@ class QNAPSystemSensor(QNAPSensor): ATTR_SERIAL: data["system"]["serial_number"], ATTR_UPTIME: f"{days:0>2d}d {hours:0>2d}h {minutes:0>2d}m", } + return None class QNAPDriveSensor(QNAPSensor): """A QNAP sensor that monitors HDD/SSD drive stats.""" + monitor_device: str + @property def native_value(self): """Return the state of the sensor.""" @@ -475,8 +491,10 @@ class QNAPDriveSensor(QNAPSensor): if self.entity_description.key == "drive_temp": return int(data["temp_c"]) if data["temp_c"] is not None else 0 + return None + @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" if self.coordinator.data: data = self.coordinator.data["smart_drive_health"][self.monitor_device] @@ -486,11 +504,14 @@ class QNAPDriveSensor(QNAPSensor): ATTR_SERIAL: data["serial"], ATTR_TYPE: data["type"], } + return None class QNAPVolumeSensor(QNAPSensor): """A QNAP sensor that monitors storage volume stats.""" + monitor_device: str + @property def native_value(self): """Return the state of the sensor.""" @@ -511,10 +532,12 @@ class QNAPVolumeSensor(QNAPSensor): if self.entity_description.key == "volume_percentage_used": return used_gb / total_gb * 100 + return None + # Deprecated since Home Assistant 2024.6.0 # Can be removed completely in 2024.12.0 @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" if self.coordinator.data: data = self.coordinator.data["volumes"][self.monitor_device] @@ -523,3 +546,4 @@ class QNAPVolumeSensor(QNAPSensor): return { ATTR_VOLUME_SIZE: f"{round(total_gb, 1)} {UnitOfInformation.GIBIBYTES}" } + return None From dbdb148e1225c7d79d6fc2fb122884566e1256ff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:38:36 +0200 Subject: [PATCH 1772/2411] Fix implicit-return in plaato (#122902) --- homeassistant/components/plaato/entity.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plaato/entity.py b/homeassistant/components/plaato/entity.py index d4c4622a998..7ab8367bd1d 100644 --- a/homeassistant/components/plaato/entity.py +++ b/homeassistant/components/plaato/entity.py @@ -1,5 +1,7 @@ """PlaatoEntity class.""" +from typing import Any + from pyplaato.models.device import PlaatoDevice from homeassistant.helpers import entity @@ -59,7 +61,7 @@ class PlaatoEntity(entity.Entity): return self._entry_data[SENSOR_DATA] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the monitored installation.""" if self._attributes: return { @@ -68,6 +70,7 @@ class PlaatoEntity(entity.Entity): if plaato_key in self._attributes and self._attributes[plaato_key] is not None } + return None @property def available(self): From 47c96c52b1395d0ee671b430851e9be3fb0abab1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:39:01 +0200 Subject: [PATCH 1773/2411] Fix implicit-return in niko_home_control (#122904) --- homeassistant/components/niko_home_control/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index 360b45cceed..b2d41f3a41e 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -120,3 +120,4 @@ class NikoHomeControlData: if state["id"] == aid: return state["value1"] _LOGGER.error("Failed to retrieve state off unknown light") + return None From c32f1efad0828b2a5fd0b53af40dc21ee851c74b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:39:21 +0200 Subject: [PATCH 1774/2411] Fix implicit-return in maxcube (#122907) --- homeassistant/components/maxcube/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py index 82cdc56e5d9..d4a3a45f441 100644 --- a/homeassistant/components/maxcube/__init__.py +++ b/homeassistant/components/maxcube/__init__.py @@ -98,7 +98,7 @@ class MaxCubeHandle: self.mutex = Lock() self._updatets = time.monotonic() - def update(self): + def update(self) -> None: """Pull the latest data from the MAX! Cube.""" # Acquire mutex to prevent simultaneous update from multiple threads with self.mutex: @@ -110,7 +110,7 @@ class MaxCubeHandle: self.cube.update() except TimeoutError: _LOGGER.error("Max!Cube connection failed") - return False + return self._updatets = time.monotonic() else: From 01f41a597e8d6709580f5809ccb4f32b084b8e2b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:39:39 +0200 Subject: [PATCH 1775/2411] Fix implicit-return in melissa (#122908) --- homeassistant/components/melissa/climate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index fcb0820a6f0..0ad663faa2a 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -86,18 +86,21 @@ class MelissaClimate(ClimateEntity): """Return the current fan mode.""" if self._cur_settings is not None: return self.melissa_fan_to_hass(self._cur_settings[self._api.FAN]) + return None @property def current_temperature(self): """Return the current temperature.""" if self._data: return self._data[self._api.TEMP] + return None @property def current_humidity(self): """Return the current humidity value.""" if self._data: return self._data[self._api.HUMIDITY] + return None @property def target_temperature_step(self): @@ -224,6 +227,7 @@ class MelissaClimate(ClimateEntity): if mode == HVACMode.FAN_ONLY: return self._api.MODE_FAN _LOGGER.warning("Melissa have no setting for %s mode", mode) + return None def hass_fan_to_melissa(self, fan): """Translate hass fan modes to melissa modes.""" @@ -236,3 +240,4 @@ class MelissaClimate(ClimateEntity): if fan == FAN_HIGH: return self._api.FAN_HIGH _LOGGER.warning("Melissa have no setting for %s fan mode", fan) + return None From 8b1a5276025106672bafd63868d822d0971bcbc9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:39:59 +0200 Subject: [PATCH 1776/2411] Fix implicit-return in meraki (#122909) --- homeassistant/components/meraki/device_tracker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py index 95ed2ba9089..0eb3742a878 100644 --- a/homeassistant/components/meraki/device_tracker.py +++ b/homeassistant/components/meraki/device_tracker.py @@ -88,6 +88,7 @@ class MerakiView(HomeAssistantView): _LOGGER.debug("No observations found") return None self._handle(request.app[KEY_HASS], data) + return None @callback def _handle(self, hass, data): From c4398efbbb90b2620186b27a06a0990773fc8086 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:40:30 +0200 Subject: [PATCH 1777/2411] Fix implicit-return in meteo_france (#122910) --- homeassistant/components/meteo_france/weather.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 943d30fccfd..8305547afd3 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -165,6 +165,7 @@ class MeteoFranceWeather( wind_bearing = self.coordinator.data.current_forecast["wind"]["direction"] if wind_bearing != -1: return wind_bearing + return None def _forecast(self, mode: str) -> list[Forecast]: """Return the forecast.""" From cd552ceb2b15d5e65c4a5495f297004f1ce3d789 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:40:48 +0200 Subject: [PATCH 1778/2411] Fix implicit-return in mystrom (#122911) --- homeassistant/components/mystrom/binary_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mystrom/binary_sensor.py b/homeassistant/components/mystrom/binary_sensor.py index 66ea2cc9679..17a1da75a96 100644 --- a/homeassistant/components/mystrom/binary_sensor.py +++ b/homeassistant/components/mystrom/binary_sensor.py @@ -67,6 +67,7 @@ class MyStromView(HomeAssistantView): else: new_state = self.buttons[entity_id].state == "off" self.buttons[entity_id].async_on_update(new_state) + return None class MyStromBinarySensor(BinarySensorEntity): From ed9c4e0c0dcb806aefb9426ada40cbdc5cea77e7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:41:10 +0200 Subject: [PATCH 1779/2411] Fix implicit-return in landisgyr_heat_meter (#122912) --- homeassistant/components/landisgyr_heat_meter/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index 4e52e246d81..a2fc1320c2b 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -3,13 +3,14 @@ from __future__ import annotations import logging +from typing import Any import ultraheat_api from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_registry import async_migrate_entries +from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from .const import DOMAIN from .coordinator import UltraheatCoordinator @@ -55,7 +56,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> device_number = config_entry.data["device_number"] @callback - def update_entity_unique_id(entity_entry): + def update_entity_unique_id( + entity_entry: RegistryEntry, + ) -> dict[str, Any] | None: """Update unique ID of entity entry.""" if entity_entry.platform in entity_entry.unique_id: return { @@ -64,6 +67,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> f"{device_number}", ) } + return None await async_migrate_entries( hass, config_entry.entry_id, update_entity_unique_id From c8dccec9562ab6f5a4f1cb85aa529e47871486c2 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Wed, 31 Jul 2024 12:48:08 +0200 Subject: [PATCH 1780/2411] Bump velbusaio to 2024.07.06 (#122905) bumpo velbusaio to 2024.07.06 --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 4e9478ae575..c1cf2951bbd 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -13,7 +13,7 @@ "velbus-packet", "velbus-protocol" ], - "requirements": ["velbus-aio==2024.7.5"], + "requirements": ["velbus-aio==2024.7.6"], "usb": [ { "vid": "10CF", diff --git a/requirements_all.txt b/requirements_all.txt index 6ba6601597d..3c8d5357568 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2851,7 +2851,7 @@ vallox-websocket-api==5.3.0 vehicle==2.2.2 # homeassistant.components.velbus -velbus-aio==2024.7.5 +velbus-aio==2024.7.6 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da3ac4cd662..b95d1b58922 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2246,7 +2246,7 @@ vallox-websocket-api==5.3.0 vehicle==2.2.2 # homeassistant.components.velbus -velbus-aio==2024.7.5 +velbus-aio==2024.7.6 # homeassistant.components.venstar venstarcolortouch==0.19 From bf3a2cf393ab6f97bcfe959babb22f917b84e2a5 Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Wed, 31 Jul 2024 07:01:48 -0400 Subject: [PATCH 1781/2411] Add graceful handling for LASTSTEST sensor in APCUPSD (#113125) * Add handling for LASTSTEST sensor * Set the state to unknown instead of unavailable * Use LASTSTEST constant and revise the logic to add it to the entity list * Use LASTSTEST constant --- homeassistant/components/apcupsd/const.py | 3 ++ homeassistant/components/apcupsd/sensor.py | 22 +++++++++++--- tests/components/apcupsd/test_sensor.py | 34 +++++++++++++++++++++- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/apcupsd/const.py b/homeassistant/components/apcupsd/const.py index e24a66fdca1..56bf229579d 100644 --- a/homeassistant/components/apcupsd/const.py +++ b/homeassistant/components/apcupsd/const.py @@ -4,3 +4,6 @@ from typing import Final DOMAIN: Final = "apcupsd" CONNECTION_TIMEOUT: int = 10 + +# Field name of last self test retrieved from apcupsd. +LASTSTEST: Final = "laststest" diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 8d2c1ee2af1..ff72208e9ce 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, + STATE_UNKNOWN, UnitOfApparentPower, UnitOfElectricCurrent, UnitOfElectricPotential, @@ -25,7 +26,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from .const import DOMAIN, LASTSTEST from .coordinator import APCUPSdCoordinator PARALLEL_UPDATES = 0 @@ -156,8 +157,8 @@ SENSORS: dict[str, SensorEntityDescription] = { device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - "laststest": SensorEntityDescription( - key="laststest", + LASTSTEST: SensorEntityDescription( + key=LASTSTEST, translation_key="last_self_test", ), "lastxfer": SensorEntityDescription( @@ -417,7 +418,12 @@ async def async_setup_entry( available_resources: set[str] = {k.lower() for k, _ in coordinator.data.items()} entities = [] - for resource in available_resources: + + # "laststest" is a special sensor that only appears when the APC UPS daemon has done a + # periodical (or manual) self test since last daemon restart. It might not be available + # when we set up the integration, and we do not know if it would ever be available. Here we + # add it anyway and mark it as unknown initially. + for resource in available_resources | {LASTSTEST}: if resource not in SENSORS: _LOGGER.warning("Invalid resource from APCUPSd: %s", resource.upper()) continue @@ -473,6 +479,14 @@ class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity): def _update_attrs(self) -> None: """Update sensor attributes based on coordinator data.""" key = self.entity_description.key.upper() + # For most sensors the key will always be available for each refresh. However, some sensors + # (e.g., "laststest") will only appear after certain event occurs (e.g., a self test is + # performed) and may disappear again after certain event. So we mark the state as "unknown" + # when it becomes unknown after such events. + if key not in self.coordinator.data: + self._attr_native_value = STATE_UNKNOWN + return + self._attr_native_value, inferred_unit = infer_unit(self.coordinator.data[key]) if not self.native_unit_of_measurement: self._attr_native_unit_of_measurement = inferred_unit diff --git a/tests/components/apcupsd/test_sensor.py b/tests/components/apcupsd/test_sensor.py index 0c7d174a5e8..0fe7f12ad27 100644 --- a/tests/components/apcupsd/test_sensor.py +++ b/tests/components/apcupsd/test_sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE, + STATE_UNKNOWN, UnitOfElectricPotential, UnitOfPower, UnitOfTime, @@ -25,7 +26,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util import slugify from homeassistant.util.dt import utcnow -from . import MOCK_STATUS, async_init_integration +from . import MOCK_MINIMAL_STATUS, MOCK_STATUS, async_init_integration from tests.common import async_fire_time_changed @@ -237,3 +238,34 @@ async def test_multiple_manual_update_entity(hass: HomeAssistant) -> None: blocking=True, ) assert mock_request_status.call_count == 1 + + +async def test_sensor_unknown(hass: HomeAssistant) -> None: + """Test if our integration can properly certain sensors as unknown when it becomes so.""" + await async_init_integration(hass, status=MOCK_MINIMAL_STATUS) + + assert hass.states.get("sensor.mode").state == MOCK_MINIMAL_STATUS["UPSMODE"] + # Last self test sensor should be added even if our status does not report it initially (it is + # a sensor that appears only after a periodical or manual self test is performed). + assert hass.states.get("sensor.last_self_test") is not None + assert hass.states.get("sensor.last_self_test").state == STATE_UNKNOWN + + # Simulate an event (a self test) such that "LASTSTEST" field is being reported, the state of + # the sensor should be properly updated with the corresponding value. + with patch("aioapcaccess.request_status") as mock_request_status: + mock_request_status.return_value = MOCK_MINIMAL_STATUS | { + "LASTSTEST": "1970-01-01 00:00:00 0000" + } + future = utcnow() + timedelta(minutes=2) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + assert hass.states.get("sensor.last_self_test").state == "1970-01-01 00:00:00 0000" + + # Simulate another event (e.g., daemon restart) such that "LASTSTEST" is no longer reported. + with patch("aioapcaccess.request_status") as mock_request_status: + mock_request_status.return_value = MOCK_MINIMAL_STATUS + future = utcnow() + timedelta(minutes=2) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + # The state should become unknown again. + assert hass.states.get("sensor.last_self_test").state == STATE_UNKNOWN From 3bf00822b0118d95fee626d42fe118ab488561df Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:42:07 +0200 Subject: [PATCH 1782/2411] Fix implicit-return in kodi (#122914) --- homeassistant/components/kodi/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 46dee891e3a..cdbe4e334cb 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -529,10 +529,11 @@ class KodiEntity(MediaPlayerEntity): return not self._connection.can_subscribe @property - def volume_level(self): + def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" if "volume" in self._app_properties: return int(self._app_properties["volume"]) / 100.0 + return None @property def is_volume_muted(self): From 7c7b408df115d8cdbbd7a21243adf9496554a924 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:21:58 +0200 Subject: [PATCH 1783/2411] Fix implicit-return in homekit_controller (#122920) --- homeassistant/components/homekit_controller/fan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index db01147494f..93ebbba62b1 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -144,7 +144,8 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan.""" if percentage == 0: - return await self.async_turn_off() + await self.async_turn_off() + return await self.async_put_characteristics( { From f14471112d0904a5480528c3ec1097d11b5d8ba4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 31 Jul 2024 05:36:02 -0700 Subject: [PATCH 1784/2411] Improve LLM tool quality by more clearly specifying device_class slots (#122723) * Limit intent / llm API device_class slots to only necessary services and limited set of values * Fix ruff errors * Run ruff format * Fix typing and improve output schema * Fix schema and improve flattening * Revert conftest * Revert recorder * Fix ruff format errors * Update using latest version of voluptuous --- homeassistant/components/cover/intent.py | 4 +- homeassistant/components/intent/__init__.py | 15 +++++ .../components/media_player/intent.py | 7 ++- homeassistant/helpers/intent.py | 42 ++++++++++--- .../test_default_agent_intents.py | 28 +++++++++ tests/components/cover/test_intent.py | 49 ++++++++++++--- tests/helpers/test_intent.py | 59 ++++++++++++++++++- 7 files changed, 183 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/cover/intent.py b/homeassistant/components/cover/intent.py index b38f698ac3d..7580cff063a 100644 --- a/homeassistant/components/cover/intent.py +++ b/homeassistant/components/cover/intent.py @@ -4,7 +4,7 @@ from homeassistant.const import SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER from homeassistant.core import HomeAssistant from homeassistant.helpers import intent -from . import DOMAIN +from . import DOMAIN, CoverDeviceClass INTENT_OPEN_COVER = "HassOpenCover" INTENT_CLOSE_COVER = "HassCloseCover" @@ -21,6 +21,7 @@ async def async_setup_intents(hass: HomeAssistant) -> None: "Opening {}", description="Opens a cover", platforms={DOMAIN}, + device_classes={CoverDeviceClass}, ), ) intent.async_register( @@ -32,5 +33,6 @@ async def async_setup_intents(hass: HomeAssistant) -> None: "Closing {}", description="Closes a cover", platforms={DOMAIN}, + device_classes={CoverDeviceClass}, ), ) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index b1716a8d2d2..001f2515ebf 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -16,6 +16,7 @@ from homeassistant.components.cover import ( SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, + CoverDeviceClass, ) from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.lock import ( @@ -23,11 +24,14 @@ from homeassistant.components.lock import ( SERVICE_LOCK, SERVICE_UNLOCK, ) +from homeassistant.components.media_player import MediaPlayerDeviceClass +from homeassistant.components.switch import SwitchDeviceClass from homeassistant.components.valve import ( DOMAIN as VALVE_DOMAIN, SERVICE_CLOSE_VALVE, SERVICE_OPEN_VALVE, SERVICE_SET_VALVE_POSITION, + ValveDeviceClass, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -67,6 +71,13 @@ __all__ = [ "DOMAIN", ] +ONOFF_DEVICE_CLASSES = { + CoverDeviceClass, + ValveDeviceClass, + SwitchDeviceClass, + MediaPlayerDeviceClass, +} + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Intent component.""" @@ -85,6 +96,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: HOMEASSISTANT_DOMAIN, SERVICE_TURN_ON, description="Turns on/opens a device or entity", + device_classes=ONOFF_DEVICE_CLASSES, ), ) intent.async_register( @@ -94,6 +106,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: HOMEASSISTANT_DOMAIN, SERVICE_TURN_OFF, description="Turns off/closes a device or entity", + device_classes=ONOFF_DEVICE_CLASSES, ), ) intent.async_register( @@ -103,6 +116,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: HOMEASSISTANT_DOMAIN, SERVICE_TOGGLE, description="Toggles a device or entity", + device_classes=ONOFF_DEVICE_CLASSES, ), ) intent.async_register( @@ -358,6 +372,7 @@ class SetPositionIntentHandler(intent.DynamicServiceIntentHandler): }, description="Sets the position of a device or entity", platforms={COVER_DOMAIN, VALVE_DOMAIN}, + device_classes={CoverDeviceClass, ValveDeviceClass}, ) def get_domain_and_service( diff --git a/homeassistant/components/media_player/intent.py b/homeassistant/components/media_player/intent.py index 8a5d824112a..edfab2a668f 100644 --- a/homeassistant/components/media_player/intent.py +++ b/homeassistant/components/media_player/intent.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.core import Context, HomeAssistant, State from homeassistant.helpers import intent -from . import ATTR_MEDIA_VOLUME_LEVEL, DOMAIN +from . import ATTR_MEDIA_VOLUME_LEVEL, DOMAIN, MediaPlayerDeviceClass from .const import MediaPlayerEntityFeature, MediaPlayerState INTENT_MEDIA_PAUSE = "HassMediaPause" @@ -69,6 +69,7 @@ async def async_setup_intents(hass: HomeAssistant) -> None: required_states={MediaPlayerState.PLAYING}, description="Skips a media player to the next item", platforms={DOMAIN}, + device_classes={MediaPlayerDeviceClass}, ), ) intent.async_register( @@ -82,6 +83,7 @@ async def async_setup_intents(hass: HomeAssistant) -> None: required_states={MediaPlayerState.PLAYING}, description="Replays the previous item for a media player", platforms={DOMAIN}, + device_classes={MediaPlayerDeviceClass}, ), ) intent.async_register( @@ -100,6 +102,7 @@ async def async_setup_intents(hass: HomeAssistant) -> None: }, description="Sets the volume of a media player", platforms={DOMAIN}, + device_classes={MediaPlayerDeviceClass}, ), ) @@ -118,6 +121,7 @@ class MediaPauseHandler(intent.ServiceIntentHandler): required_states={MediaPlayerState.PLAYING}, description="Pauses a media player", platforms={DOMAIN}, + device_classes={MediaPlayerDeviceClass}, ) self.last_paused = last_paused @@ -153,6 +157,7 @@ class MediaUnpauseHandler(intent.ServiceIntentHandler): required_states={MediaPlayerState.PAUSED}, description="Resumes a media player", platforms={DOMAIN}, + device_classes={MediaPlayerDeviceClass}, ) self.last_paused = last_paused diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index eeb160934ff..be9b57bf814 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -7,7 +7,7 @@ import asyncio from collections.abc import Callable, Collection, Coroutine, Iterable import dataclasses from dataclasses import dataclass, field -from enum import Enum, auto +from enum import Enum, StrEnum, auto from functools import cached_property from itertools import groupby import logging @@ -820,6 +820,7 @@ class DynamicServiceIntentHandler(IntentHandler): required_states: set[str] | None = None, description: str | None = None, platforms: set[str] | None = None, + device_classes: set[type[StrEnum]] | None = None, ) -> None: """Create Service Intent Handler.""" self.intent_type = intent_type @@ -829,6 +830,7 @@ class DynamicServiceIntentHandler(IntentHandler): self.required_states = required_states self.description = description self.platforms = platforms + self.device_classes = device_classes self.required_slots: _IntentSlotsType = {} if required_slots: @@ -851,13 +853,38 @@ class DynamicServiceIntentHandler(IntentHandler): @cached_property def slot_schema(self) -> dict: """Return a slot schema.""" + domain_validator = ( + vol.In(list(self.required_domains)) if self.required_domains else cv.string + ) slot_schema = { vol.Any("name", "area", "floor"): non_empty_string, - vol.Optional("domain"): vol.All(cv.ensure_list, [cv.string]), - vol.Optional("device_class"): vol.All(cv.ensure_list, [cv.string]), - vol.Optional("preferred_area_id"): cv.string, - vol.Optional("preferred_floor_id"): cv.string, + vol.Optional("domain"): vol.All(cv.ensure_list, [domain_validator]), } + if self.device_classes: + # The typical way to match enums is with vol.Coerce, but we build a + # flat list to make the API simpler to describe programmatically + flattened_device_classes = vol.In( + [ + device_class.value + for device_class_enum in self.device_classes + for device_class in device_class_enum + ] + ) + slot_schema.update( + { + vol.Optional("device_class"): vol.All( + cv.ensure_list, + [flattened_device_classes], + ) + } + ) + + slot_schema.update( + { + vol.Optional("preferred_area_id"): cv.string, + vol.Optional("preferred_floor_id"): cv.string, + } + ) if self.required_slots: slot_schema.update( @@ -910,9 +937,6 @@ class DynamicServiceIntentHandler(IntentHandler): if "domain" in slots: domains = set(slots["domain"]["value"]) - if self.required_domains: - # Must be a subset of intent's required domain(s) - domains.intersection_update(self.required_domains) if "device_class" in slots: device_classes = set(slots["device_class"]["value"]) @@ -1120,6 +1144,7 @@ class ServiceIntentHandler(DynamicServiceIntentHandler): required_states: set[str] | None = None, description: str | None = None, platforms: set[str] | None = None, + device_classes: set[type[StrEnum]] | None = None, ) -> None: """Create service handler.""" super().__init__( @@ -1132,6 +1157,7 @@ class ServiceIntentHandler(DynamicServiceIntentHandler): required_states=required_states, description=description, platforms=platforms, + device_classes=device_classes, ) self.domain = domain self.service = service diff --git a/tests/components/conversation/test_default_agent_intents.py b/tests/components/conversation/test_default_agent_intents.py index 8be25136df4..7bae9c43f70 100644 --- a/tests/components/conversation/test_default_agent_intents.py +++ b/tests/components/conversation/test_default_agent_intents.py @@ -123,6 +123,34 @@ async def test_cover_set_position( assert call.data == {"entity_id": entity_id, cover.ATTR_POSITION: 50} +async def test_cover_device_class( + hass: HomeAssistant, + init_components, +) -> None: + """Test the open position for covers by device class.""" + await cover_intent.async_setup_intents(hass) + + entity_id = f"{cover.DOMAIN}.front" + hass.states.async_set( + entity_id, STATE_CLOSED, attributes={"device_class": "garage"} + ) + async_expose_entity(hass, conversation.DOMAIN, entity_id, True) + + # Open service + calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_OPEN_COVER) + result = await conversation.async_converse( + hass, "open the garage door", None, Context(), None + ) + await hass.async_block_till_done() + + response = result.response + assert response.response_type == intent.IntentResponseType.ACTION_DONE + assert response.speech["plain"]["speech"] == "Opened the garage" + assert len(calls) == 1 + call = calls[0] + assert call.data == {"entity_id": entity_id} + + async def test_valve_intents( hass: HomeAssistant, init_components, diff --git a/tests/components/cover/test_intent.py b/tests/components/cover/test_intent.py index 8ee621596db..1cf23c4c3df 100644 --- a/tests/components/cover/test_intent.py +++ b/tests/components/cover/test_intent.py @@ -1,5 +1,9 @@ """The tests for the cover platform.""" +from typing import Any + +import pytest + from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, DOMAIN, @@ -16,15 +20,24 @@ from homeassistant.setup import async_setup_component from tests.common import async_mock_service -async def test_open_cover_intent(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("slots"), + [ + ({"name": {"value": "garage door"}}), + ({"device_class": {"value": "garage"}}), + ], +) +async def test_open_cover_intent(hass: HomeAssistant, slots: dict[str, Any]) -> None: """Test HassOpenCover intent.""" await cover_intent.async_setup_intents(hass) - hass.states.async_set(f"{DOMAIN}.garage_door", STATE_CLOSED) + hass.states.async_set( + f"{DOMAIN}.garage_door", STATE_CLOSED, attributes={"device_class": "garage"} + ) calls = async_mock_service(hass, DOMAIN, SERVICE_OPEN_COVER) response = await intent.async_handle( - hass, "test", cover_intent.INTENT_OPEN_COVER, {"name": {"value": "garage door"}} + hass, "test", cover_intent.INTENT_OPEN_COVER, slots ) await hass.async_block_till_done() @@ -36,18 +49,27 @@ async def test_open_cover_intent(hass: HomeAssistant) -> None: assert call.data == {"entity_id": f"{DOMAIN}.garage_door"} -async def test_close_cover_intent(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("slots"), + [ + ({"name": {"value": "garage door"}}), + ({"device_class": {"value": "garage"}}), + ], +) +async def test_close_cover_intent(hass: HomeAssistant, slots: dict[str, Any]) -> None: """Test HassCloseCover intent.""" await cover_intent.async_setup_intents(hass) - hass.states.async_set(f"{DOMAIN}.garage_door", STATE_OPEN) + hass.states.async_set( + f"{DOMAIN}.garage_door", STATE_OPEN, attributes={"device_class": "garage"} + ) calls = async_mock_service(hass, DOMAIN, SERVICE_CLOSE_COVER) response = await intent.async_handle( hass, "test", cover_intent.INTENT_CLOSE_COVER, - {"name": {"value": "garage door"}}, + slots, ) await hass.async_block_till_done() @@ -59,13 +81,22 @@ async def test_close_cover_intent(hass: HomeAssistant) -> None: assert call.data == {"entity_id": f"{DOMAIN}.garage_door"} -async def test_set_cover_position(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("slots"), + [ + ({"name": {"value": "test cover"}, "position": {"value": 50}}), + ({"device_class": {"value": "shade"}, "position": {"value": 50}}), + ], +) +async def test_set_cover_position(hass: HomeAssistant, slots: dict[str, Any]) -> None: """Test HassSetPosition intent for covers.""" assert await async_setup_component(hass, "intent", {}) entity_id = f"{DOMAIN}.test_cover" hass.states.async_set( - entity_id, STATE_CLOSED, attributes={ATTR_CURRENT_POSITION: 0} + entity_id, + STATE_CLOSED, + attributes={ATTR_CURRENT_POSITION: 0, "device_class": "shade"}, ) calls = async_mock_service(hass, DOMAIN, SERVICE_SET_COVER_POSITION) @@ -73,7 +104,7 @@ async def test_set_cover_position(hass: HomeAssistant) -> None: hass, "test", intent.INTENT_SET_POSITION, - {"name": {"value": "test cover"}, "position": {"value": 50}}, + slots, ) await hass.async_block_till_done() diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index c592fc50c0a..ae8c2ed65d0 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -765,7 +765,7 @@ async def test_service_intent_handler_required_domains(hass: HomeAssistant) -> N ) # Still fails even if we provide the domain - with pytest.raises(intent.MatchFailedError): + with pytest.raises(intent.InvalidSlotInfo): await intent.async_handle( hass, "test", @@ -777,7 +777,10 @@ async def test_service_intent_handler_required_domains(hass: HomeAssistant) -> N async def test_service_handler_empty_strings(hass: HomeAssistant) -> None: """Test that passing empty strings for filters fails in ServiceIntentHandler.""" handler = intent.ServiceIntentHandler( - "TestType", "light", "turn_on", "Turned {} on" + "TestType", + "light", + "turn_on", + "Turned {} on", ) intent.async_register(hass, handler) @@ -814,3 +817,55 @@ async def test_service_handler_no_filter(hass: HomeAssistant) -> None: "test", "TestType", ) + + +async def test_service_handler_device_classes( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test that passing empty strings for filters fails in ServiceIntentHandler.""" + + # Register a fake service and a switch intent handler + call_done = asyncio.Event() + calls = [] + + # Register a service that takes 0.1 seconds to execute + async def mock_service(call): + """Mock service.""" + call_done.set() + calls.append(call) + + hass.services.async_register("switch", "turn_on", mock_service) + + handler = intent.ServiceIntentHandler( + "TestType", + "switch", + "turn_on", + "Turned {} on", + device_classes={switch.SwitchDeviceClass}, + ) + intent.async_register(hass, handler) + + # Create a switch enttiy and match by device class + hass.states.async_set( + "switch.bedroom", "off", attributes={"device_class": "outlet"} + ) + hass.states.async_set("switch.living_room", "off") + + await intent.async_handle( + hass, + "test", + "TestType", + slots={"device_class": {"value": "outlet"}}, + ) + await call_done.wait() + assert [call.data.get("entity_id") for call in calls] == ["switch.bedroom"] + calls.clear() + + # Validate which device classes are allowed + with pytest.raises(intent.InvalidSlotInfo): + await intent.async_handle( + hass, + "test", + "TestType", + slots={"device_class": {"value": "light"}}, + ) From 8b96c7873f778f4873370ebd7ef9359aa16b48da Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 31 Jul 2024 14:36:53 +0200 Subject: [PATCH 1785/2411] Rename 'service' to 'action' in automations and scripts (#122845) --- homeassistant/const.py | 1 + homeassistant/helpers/config_validation.py | 25 ++- homeassistant/helpers/service.py | 6 +- tests/components/automation/test_init.py | 208 +++++++++++-------- tests/components/automation/test_recorder.py | 2 +- tests/components/script/test_blueprint.py | 2 +- tests/components/script/test_init.py | 124 ++++++----- tests/components/script/test_recorder.py | 2 +- tests/helpers/test_config_validation.py | 95 ++++++--- tests/helpers/test_script.py | 208 +++++++++++-------- tests/helpers/test_service.py | 16 +- 11 files changed, 414 insertions(+), 275 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d0f1d4555d4..7d58bdb1e94 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -113,6 +113,7 @@ SUN_EVENT_SUNRISE: Final = "sunrise" # #### CONFIG #### CONF_ABOVE: Final = "above" CONF_ACCESS_TOKEN: Final = "access_token" +CONF_ACTION: Final = "action" CONF_ADDRESS: Final = "address" CONF_AFTER: Final = "after" CONF_ALIAS: Final = "alias" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index a28c81e6da9..cd6670dc597 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -34,6 +34,7 @@ from homeassistant.const import ( ATTR_FLOOR_ID, ATTR_LABEL_ID, CONF_ABOVE, + CONF_ACTION, CONF_ALIAS, CONF_ATTRIBUTE, CONF_BELOW, @@ -1325,11 +1326,30 @@ EVENT_SCHEMA = vol.Schema( } ) + +def _backward_compat_service_schema(value: Any | None) -> Any: + """Backward compatibility for service schemas.""" + + if not isinstance(value, dict): + return value + + # `service` has been renamed to `action` + if CONF_SERVICE in value: + if CONF_ACTION in value: + raise vol.Invalid( + "Cannot specify both 'service' and 'action'. Please use 'action' only." + ) + value[CONF_ACTION] = value.pop(CONF_SERVICE) + + return value + + SERVICE_SCHEMA = vol.All( + _backward_compat_service_schema, vol.Schema( { **SCRIPT_ACTION_BASE_SCHEMA, - vol.Exclusive(CONF_SERVICE, "service name"): vol.Any( + vol.Exclusive(CONF_ACTION, "service name"): vol.Any( service, dynamic_template ), vol.Exclusive(CONF_SERVICE_TEMPLATE, "service name"): vol.Any( @@ -1348,7 +1368,7 @@ SERVICE_SCHEMA = vol.All( vol.Remove("metadata"): dict, } ), - has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE), + has_at_least_one_key(CONF_ACTION, CONF_SERVICE_TEMPLATE), ) NUMERIC_STATE_THRESHOLD_SCHEMA = vol.Any( @@ -1844,6 +1864,7 @@ ACTIONS_MAP = { CONF_WAIT_FOR_TRIGGER: SCRIPT_ACTION_WAIT_FOR_TRIGGER, CONF_VARIABLES: SCRIPT_ACTION_VARIABLES, CONF_IF: SCRIPT_ACTION_IF, + CONF_ACTION: SCRIPT_ACTION_CALL_SERVICE, CONF_SERVICE: SCRIPT_ACTION_CALL_SERVICE, CONF_SERVICE_TEMPLATE: SCRIPT_ACTION_CALL_SERVICE, CONF_STOP: SCRIPT_ACTION_STOP, diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 35c682437cb..58cd4657301 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -20,8 +20,8 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FLOOR_ID, ATTR_LABEL_ID, + CONF_ACTION, CONF_ENTITY_ID, - CONF_SERVICE, CONF_SERVICE_DATA, CONF_SERVICE_DATA_TEMPLATE, CONF_SERVICE_TEMPLATE, @@ -358,8 +358,8 @@ def async_prepare_call_from_config( f"Invalid config for calling service: {ex}" ) from ex - if CONF_SERVICE in config: - domain_service = config[CONF_SERVICE] + if CONF_ACTION in config: + domain_service = config[CONF_ACTION] else: domain_service = config[CONF_SERVICE_TEMPLATE] diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index d8078984630..d8f04f10458 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -88,7 +88,7 @@ async def test_service_data_not_a_dict( { automation.DOMAIN: { "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation", "data": 100}, + "action": {"action": "test.automation", "data": 100}, } }, ) @@ -111,7 +111,7 @@ async def test_service_data_single_template( automation.DOMAIN: { "trigger": {"platform": "event", "event_type": "test_event"}, "action": { - "service": "test.automation", + "action": "test.automation", "data": "{{ { 'foo': 'bar' } }}", }, } @@ -136,7 +136,7 @@ async def test_service_specify_data( "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, "action": { - "service": "test.automation", + "action": "test.automation", "data_template": { "some": ( "{{ trigger.platform }} - {{ trigger.event.event_type }}" @@ -170,7 +170,7 @@ async def test_service_specify_entity_id( { automation.DOMAIN: { "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation", "entity_id": "hello.world"}, + "action": {"action": "test.automation", "entity_id": "hello.world"}, } }, ) @@ -192,7 +192,7 @@ async def test_service_specify_entity_id_list( automation.DOMAIN: { "trigger": {"platform": "event", "event_type": "test_event"}, "action": { - "service": "test.automation", + "action": "test.automation", "entity_id": ["hello.world", "hello.world2"], }, } @@ -216,7 +216,7 @@ async def test_two_triggers(hass: HomeAssistant, calls: list[ServiceCall]) -> No {"platform": "event", "event_type": "test_event"}, {"platform": "state", "entity_id": "test.entity"}, ], - "action": {"service": "test.automation"}, + "action": {"action": "test.automation"}, } }, ) @@ -245,7 +245,7 @@ async def test_trigger_service_ignoring_condition( "entity_id": "non.existing", "above": "1", }, - "action": {"service": "test.automation"}, + "action": {"action": "test.automation"}, } }, ) @@ -301,7 +301,7 @@ async def test_two_conditions_with_and( "below": 150, }, ], - "action": {"service": "test.automation"}, + "action": {"action": "test.automation"}, } }, ) @@ -333,7 +333,7 @@ async def test_shorthand_conditions_template( automation.DOMAIN: { "trigger": [{"platform": "event", "event_type": "test_event"}], "condition": "{{ is_state('test.entity', 'hello') }}", - "action": {"service": "test.automation"}, + "action": {"action": "test.automation"}, } }, ) @@ -360,11 +360,11 @@ async def test_automation_list_setting( automation.DOMAIN: [ { "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation"}, + "action": {"action": "test.automation"}, }, { "trigger": {"platform": "event", "event_type": "test_event_2"}, - "action": {"service": "test.automation"}, + "action": {"action": "test.automation"}, }, ] }, @@ -390,8 +390,8 @@ async def test_automation_calling_two_actions( automation.DOMAIN: { "trigger": {"platform": "event", "event_type": "test_event"}, "action": [ - {"service": "test.automation", "data": {"position": 0}}, - {"service": "test.automation", "data": {"position": 1}}, + {"action": "test.automation", "data": {"position": 0}}, + {"action": "test.automation", "data": {"position": 1}}, ], } }, @@ -420,7 +420,7 @@ async def test_shared_context(hass: HomeAssistant, calls: list[ServiceCall]) -> { "alias": "bye", "trigger": {"platform": "event", "event_type": "test_event2"}, - "action": {"service": "test.automation"}, + "action": {"action": "test.automation"}, }, ] }, @@ -486,7 +486,7 @@ async def test_services(hass: HomeAssistant, calls: list[ServiceCall]) -> None: automation.DOMAIN: { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation"}, + "action": {"action": "test.automation"}, } }, ) @@ -569,7 +569,7 @@ async def test_reload_config_service( "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, "action": { - "service": "test.automation", + "action": "test.automation", "data_template": {"event": "{{ trigger.event.event_type }}"}, }, } @@ -597,7 +597,7 @@ async def test_reload_config_service( "alias": "bye", "trigger": {"platform": "event", "event_type": "test_event2"}, "action": { - "service": "test.automation", + "action": "test.automation", "data_template": {"event": "{{ trigger.event.event_type }}"}, }, } @@ -650,7 +650,7 @@ async def test_reload_config_when_invalid_config( "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, "action": { - "service": "test.automation", + "action": "test.automation", "data_template": {"event": "{{ trigger.event.event_type }}"}, }, } @@ -690,7 +690,7 @@ async def test_reload_config_handles_load_fails( "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, "action": { - "service": "test.automation", + "action": "test.automation", "data_template": {"event": "{{ trigger.event.event_type }}"}, }, } @@ -735,7 +735,7 @@ async def test_automation_stops( "action": [ {"event": "running"}, {"wait_template": "{{ is_state('test.entity', 'goodbye') }}"}, - {"service": "test.automation"}, + {"action": "test.automation"}, ], } } @@ -811,7 +811,7 @@ async def test_reload_unchanged_does_not_stop( "action": [ {"event": "running"}, {"wait_template": "{{ is_state('test.entity', 'goodbye') }}"}, - {"service": "test.automation"}, + {"action": "test.automation"}, ], } } @@ -858,7 +858,7 @@ async def test_reload_single_unchanged_does_not_stop( "action": [ {"event": "running"}, {"wait_template": "{{ is_state('test.entity', 'goodbye') }}"}, - {"service": "test.automation"}, + {"action": "test.automation"}, ], } } @@ -905,7 +905,7 @@ async def test_reload_single_add_automation( "id": "sun", "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": [{"service": "test.automation"}], + "action": [{"action": "test.automation"}], } } assert await async_setup_component(hass, automation.DOMAIN, config1) @@ -942,25 +942,25 @@ async def test_reload_single_parallel_calls( "id": "sun", "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event_sun"}, - "action": [{"service": "test.automation"}], + "action": [{"action": "test.automation"}], }, { "id": "moon", "alias": "goodbye", "trigger": {"platform": "event", "event_type": "test_event_moon"}, - "action": [{"service": "test.automation"}], + "action": [{"action": "test.automation"}], }, { "id": "mars", "alias": "goodbye", "trigger": {"platform": "event", "event_type": "test_event_mars"}, - "action": [{"service": "test.automation"}], + "action": [{"action": "test.automation"}], }, { "id": "venus", "alias": "goodbye", "trigger": {"platform": "event", "event_type": "test_event_venus"}, - "action": [{"service": "test.automation"}], + "action": [{"action": "test.automation"}], }, ] } @@ -1055,7 +1055,7 @@ async def test_reload_single_remove_automation( "id": "sun", "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": [{"service": "test.automation"}], + "action": [{"action": "test.automation"}], } } config2 = {automation.DOMAIN: {}} @@ -1093,12 +1093,12 @@ async def test_reload_moved_automation_without_alias( automation.DOMAIN: [ { "trigger": {"platform": "event", "event_type": "test_event"}, - "action": [{"service": "test.automation"}], + "action": [{"action": "test.automation"}], }, { "alias": "automation_with_alias", "trigger": {"platform": "event", "event_type": "test_event2"}, - "action": [{"service": "test.automation"}], + "action": [{"action": "test.automation"}], }, ] } @@ -1149,17 +1149,17 @@ async def test_reload_identical_automations_without_id( { "alias": "dolly", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": [{"service": "test.automation"}], + "action": [{"action": "test.automation"}], }, { "alias": "dolly", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": [{"service": "test.automation"}], + "action": [{"action": "test.automation"}], }, { "alias": "dolly", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": [{"service": "test.automation"}], + "action": [{"action": "test.automation"}], }, ] } @@ -1246,12 +1246,12 @@ async def test_reload_identical_automations_without_id( [ { "trigger": {"platform": "event", "event_type": "test_event"}, - "action": [{"service": "test.automation"}], + "action": [{"action": "test.automation"}], }, # An automation using templates { "trigger": {"platform": "event", "event_type": "test_event"}, - "action": [{"service": "{{ 'test.automation' }}"}], + "action": [{"action": "{{ 'test.automation' }}"}], }, # An automation using blueprint { @@ -1278,13 +1278,13 @@ async def test_reload_identical_automations_without_id( { "id": "sun", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": [{"service": "test.automation"}], + "action": [{"action": "test.automation"}], }, # An automation using templates { "id": "sun", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": [{"service": "{{ 'test.automation' }}"}], + "action": [{"action": "{{ 'test.automation' }}"}], }, # An automation using blueprint { @@ -1424,12 +1424,12 @@ async def test_automation_restore_state(hass: HomeAssistant) -> None: { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event_hello"}, - "action": {"service": "test.automation"}, + "action": {"action": "test.automation"}, }, { "alias": "bye", "trigger": {"platform": "event", "event_type": "test_event_bye"}, - "action": {"service": "test.automation"}, + "action": {"action": "test.automation"}, }, ] } @@ -1474,7 +1474,7 @@ async def test_initial_value_off(hass: HomeAssistant) -> None: "alias": "hello", "initial_state": "off", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation", "entity_id": "hello.world"}, + "action": {"action": "test.automation", "entity_id": "hello.world"}, } }, ) @@ -1499,7 +1499,7 @@ async def test_initial_value_on(hass: HomeAssistant) -> None: "initial_state": "on", "trigger": {"platform": "event", "event_type": "test_event"}, "action": { - "service": "test.automation", + "action": "test.automation", "entity_id": ["hello.world", "hello.world2"], }, } @@ -1528,7 +1528,7 @@ async def test_initial_value_off_but_restore_on(hass: HomeAssistant) -> None: "alias": "hello", "initial_state": "off", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation", "entity_id": "hello.world"}, + "action": {"action": "test.automation", "entity_id": "hello.world"}, } }, ) @@ -1553,7 +1553,7 @@ async def test_initial_value_on_but_restore_off(hass: HomeAssistant) -> None: "alias": "hello", "initial_state": "on", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation", "entity_id": "hello.world"}, + "action": {"action": "test.automation", "entity_id": "hello.world"}, } }, ) @@ -1576,7 +1576,7 @@ async def test_no_initial_value_and_restore_off(hass: HomeAssistant) -> None: automation.DOMAIN: { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation", "entity_id": "hello.world"}, + "action": {"action": "test.automation", "entity_id": "hello.world"}, } }, ) @@ -1600,7 +1600,7 @@ async def test_automation_is_on_if_no_initial_state_or_restore( automation.DOMAIN: { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation", "entity_id": "hello.world"}, + "action": {"action": "test.automation", "entity_id": "hello.world"}, } }, ) @@ -1623,7 +1623,7 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None: automation.DOMAIN: { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation", "entity_id": "hello.world"}, + "action": {"action": "test.automation", "entity_id": "hello.world"}, } }, ) @@ -1714,7 +1714,7 @@ async def test_automation_bad_config_validation( "alias": "good_automation", "trigger": {"platform": "event", "event_type": "test_event"}, "action": { - "service": "test.automation", + "action": "test.automation", "entity_id": "hello.world", }, }, @@ -1756,7 +1756,7 @@ async def test_automation_bad_config_validation( "alias": "bad_automation", "trigger": {"platform": "event", "event_type": "test_event2"}, "action": { - "service": "test.automation", + "action": "test.automation", "data_template": {"event": "{{ trigger.event.event_type }}"}, }, } @@ -1785,7 +1785,7 @@ async def test_automation_with_error_in_script( automation.DOMAIN: { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation", "entity_id": "hello.world"}, + "action": {"action": "test.automation", "entity_id": "hello.world"}, } }, ) @@ -1811,7 +1811,7 @@ async def test_automation_with_error_in_script_2( automation.DOMAIN: { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": None, "entity_id": "hello.world"}, + "action": {"action": None, "entity_id": "hello.world"}, } }, ) @@ -1842,19 +1842,19 @@ async def test_automation_restore_last_triggered_with_initial_state( "alias": "hello", "initial_state": "off", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation"}, + "action": {"action": "test.automation"}, }, { "alias": "bye", "initial_state": "off", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation"}, + "action": {"action": "test.automation"}, }, { "alias": "solong", "initial_state": "on", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation"}, + "action": {"action": "test.automation"}, }, ] } @@ -2013,11 +2013,11 @@ async def test_extraction_functions( }, "action": [ { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.in_both"}, }, { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.in_first"}, }, { @@ -2027,15 +2027,15 @@ async def test_extraction_functions( "type": "turn_on", }, { - "service": "test.test", + "action": "test.test", "target": {"area_id": "area-in-both"}, }, { - "service": "test.test", + "action": "test.test", "target": {"floor_id": "floor-in-both"}, }, { - "service": "test.test", + "action": "test.test", "target": {"label_id": "label-in-both"}, }, ], @@ -2087,7 +2087,7 @@ async def test_extraction_functions( }, "action": [ { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.in_both"}, }, { @@ -2140,7 +2140,7 @@ async def test_extraction_functions( }, "action": [ { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.in_both"}, }, { @@ -2150,27 +2150,27 @@ async def test_extraction_functions( }, {"scene": "scene.hello"}, { - "service": "test.test", + "action": "test.test", "target": {"area_id": "area-in-both"}, }, { - "service": "test.test", + "action": "test.test", "target": {"area_id": "area-in-last"}, }, { - "service": "test.test", + "action": "test.test", "target": {"floor_id": "floor-in-both"}, }, { - "service": "test.test", + "action": "test.test", "target": {"floor_id": "floor-in-last"}, }, { - "service": "test.test", + "action": "test.test", "target": {"label_id": "label-in-both"}, }, { - "service": "test.test", + "action": "test.test", "target": {"label_id": "label-in-last"}, }, ], @@ -2289,7 +2289,7 @@ async def test_automation_variables( }, "trigger": {"platform": "event", "event_type": "test_event"}, "action": { - "service": "test.automation", + "action": "test.automation", "data": { "value": "{{ test_var }}", "event_type": "{{ event_type }}", @@ -2308,7 +2308,7 @@ async def test_automation_variables( "value_template": "{{ trigger.event.data.pass_condition }}", }, "action": { - "service": "test.automation", + "action": "test.automation", }, }, { @@ -2317,7 +2317,7 @@ async def test_automation_variables( }, "trigger": {"platform": "event", "event_type": "test_event_3"}, "action": { - "service": "test.automation", + "action": "test.automation", }, }, ] @@ -2373,7 +2373,7 @@ async def test_automation_trigger_variables( }, "trigger": {"platform": "event", "event_type": "test_event"}, "action": { - "service": "test.automation", + "action": "test.automation", "data": { "value": "{{ test_var }}", "event_type": "{{ event_type }}", @@ -2391,7 +2391,7 @@ async def test_automation_trigger_variables( }, "trigger": {"platform": "event", "event_type": "test_event_2"}, "action": { - "service": "test.automation", + "action": "test.automation", "data": { "value": "{{ test_var }}", "event_type": "{{ event_type }}", @@ -2438,7 +2438,7 @@ async def test_automation_bad_trigger_variables( }, "trigger": {"platform": "event", "event_type": "test_event"}, "action": { - "service": "test.automation", + "action": "test.automation", }, }, ] @@ -2465,7 +2465,7 @@ async def test_automation_this_var_always( { "trigger": {"platform": "event", "event_type": "test_event"}, "action": { - "service": "test.automation", + "action": "test.automation", "data": { "this_template": "{{this.entity_id}}", }, @@ -2542,7 +2542,7 @@ async def test_blueprint_automation( "Blueprint 'Call service based on event' generated invalid automation", ( "value should be a string for dictionary value @" - " data['action'][0]['service']" + " data['action'][0]['action']" ), ), ], @@ -2640,7 +2640,7 @@ async def test_trigger_service(hass: HomeAssistant, calls: list[ServiceCall]) -> "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, "action": { - "service": "test.automation", + "action": "test.automation", "data_template": {"trigger": "{{ trigger }}"}, }, } @@ -2679,14 +2679,14 @@ async def test_trigger_condition_implicit_id( { "conditions": {"condition": "trigger", "id": [0, "2"]}, "sequence": { - "service": "test.automation", + "action": "test.automation", "data": {"param": "one"}, }, }, { "conditions": {"condition": "trigger", "id": "1"}, "sequence": { - "service": "test.automation", + "action": "test.automation", "data": {"param": "two"}, }, }, @@ -2730,14 +2730,14 @@ async def test_trigger_condition_explicit_id( { "conditions": {"condition": "trigger", "id": "one"}, "sequence": { - "service": "test.automation", + "action": "test.automation", "data": {"param": "one"}, }, }, { "conditions": {"condition": "trigger", "id": "two"}, "sequence": { - "service": "test.automation", + "action": "test.automation", "data": {"param": "two"}, }, }, @@ -2822,8 +2822,8 @@ async def test_recursive_automation_starting_script( f" {automation_runs} }}}}" ) }, - {"service": "script.script1"}, - {"service": "test.script_done"}, + {"action": "script.script1"}, + {"action": "test.script_done"}, ], }, } @@ -2840,9 +2840,9 @@ async def test_recursive_automation_starting_script( {"platform": "event", "event_type": "trigger_automation"}, ], "action": [ - {"service": "test.automation_started"}, + {"action": "test.automation_started"}, {"delay": 0.001}, - {"service": "script.script1"}, + {"action": "script.script1"}, ], } }, @@ -2923,7 +2923,7 @@ async def test_recursive_automation( ], "action": [ {"event": "trigger_automation"}, - {"service": "test.automation_done"}, + {"action": "test.automation_done"}, ], } }, @@ -2985,7 +2985,7 @@ async def test_recursive_automation_restart_mode( ], "action": [ {"event": "trigger_automation"}, - {"service": "test.automation_done"}, + {"action": "test.automation_done"}, ], } }, @@ -3021,7 +3021,7 @@ async def test_websocket_config( config = { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation", "data": 100}, + "action": {"action": "test.automation", "data": 100}, } assert await async_setup_component( hass, automation.DOMAIN, {automation.DOMAIN: config} @@ -3095,7 +3095,7 @@ async def test_automation_turns_off_other_automation(hass: HomeAssistant) -> Non "from": "on", }, "action": { - "service": "automation.turn_off", + "action": "automation.turn_off", "target": { "entity_id": "automation.automation_1", }, @@ -3118,7 +3118,7 @@ async def test_automation_turns_off_other_automation(hass: HomeAssistant) -> Non }, }, "action": { - "service": "persistent_notification.create", + "action": "persistent_notification.create", "metadata": {}, "data": { "message": "Test race", @@ -3185,7 +3185,7 @@ async def test_two_automations_call_restart_script_same_time( "fire_toggle": { "sequence": [ { - "service": "input_boolean.toggle", + "action": "input_boolean.toggle", "target": {"entity_id": "input_boolean.test_1"}, } ] @@ -3206,7 +3206,7 @@ async def test_two_automations_call_restart_script_same_time( "to": "on", }, "action": { - "service": "script.fire_toggle", + "action": "script.fire_toggle", }, "id": "automation_0", "mode": "single", @@ -3218,7 +3218,7 @@ async def test_two_automations_call_restart_script_same_time( "to": "on", }, "action": { - "service": "script.fire_toggle", + "action": "script.fire_toggle", }, "id": "automation_1", "mode": "single", @@ -3301,3 +3301,29 @@ async def test_two_automation_call_restart_script_right_after_each_other( hass.states.async_set("input_boolean.test_2", "on") await hass.async_block_till_done() assert len(events) == 1 + + +async def test_action_service_backward_compatibility( + hass: HomeAssistant, calls: list[ServiceCall] +) -> None: + """Test we can still use the service call method.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": { + "service": "test.automation", + "entity_id": "hello.world", + "data": {"event": "{{ trigger.event.event_type }}"}, + }, + } + }, + ) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data.get(ATTR_ENTITY_ID) == ["hello.world"] + assert calls[0].data.get("event") == "test_event" diff --git a/tests/components/automation/test_recorder.py b/tests/components/automation/test_recorder.py index af3d0c41151..be354abe9d2 100644 --- a/tests/components/automation/test_recorder.py +++ b/tests/components/automation/test_recorder.py @@ -40,7 +40,7 @@ async def test_exclude_attributes( { automation.DOMAIN: { "trigger": {"platform": "event", "event_type": "test_event"}, - "action": {"service": "test.automation", "entity_id": "hello.world"}, + "action": {"action": "test.automation", "entity_id": "hello.world"}, } }, ) diff --git a/tests/components/script/test_blueprint.py b/tests/components/script/test_blueprint.py index b956aa588cb..aef22b93bcf 100644 --- a/tests/components/script/test_blueprint.py +++ b/tests/components/script/test_blueprint.py @@ -74,7 +74,7 @@ async def test_confirmable_notification( "message": "Throw ring in mountain?", "confirm_action": [ { - "service": "homeassistant.turn_on", + "action": "homeassistant.turn_on", "target": {"entity_id": "mount.doom"}, } ], diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 8362dfbcfb2..a5eda3757a9 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -85,7 +85,7 @@ async def test_passing_variables(hass: HomeAssistant) -> None: "script": { "test": { "sequence": { - "service": "test.script", + "action": "test.script", "data_template": {"hello": "{{ greeting }}"}, } } @@ -115,8 +115,14 @@ async def test_passing_variables(hass: HomeAssistant) -> None: @pytest.mark.parametrize("toggle", [False, True]) -async def test_turn_on_off_toggle(hass: HomeAssistant, toggle) -> None: - """Verify turn_on, turn_off & toggle services.""" +@pytest.mark.parametrize("action_schema_variations", ["action", "service"]) +async def test_turn_on_off_toggle( + hass: HomeAssistant, toggle: bool, action_schema_variations: str +) -> None: + """Verify turn_on, turn_off & toggle services. + + Ensures backward compatibility with the old service action schema is maintained. + """ event = "test_event" event_mock = Mock() @@ -132,9 +138,15 @@ async def test_turn_on_off_toggle(hass: HomeAssistant, toggle) -> None: async_track_state_change(hass, ENTITY_ID, state_listener, to_state="on") if toggle: - turn_off_step = {"service": "script.toggle", "entity_id": ENTITY_ID} + turn_off_step = { + action_schema_variations: "script.toggle", + "entity_id": ENTITY_ID, + } else: - turn_off_step = {"service": "script.turn_off", "entity_id": ENTITY_ID} + turn_off_step = { + action_schema_variations: "script.turn_off", + "entity_id": ENTITY_ID, + } assert await async_setup_component( hass, "script", @@ -165,7 +177,7 @@ async def test_turn_on_off_toggle(hass: HomeAssistant, toggle) -> None: invalid_configs = [ {"test": {}}, {"test hello world": {"sequence": [{"event": "bla"}]}}, - {"test": {"sequence": {"event": "test_event", "service": "homeassistant.turn_on"}}}, + {"test": {"sequence": {"event": "test_event", "action": "homeassistant.turn_on"}}}, ] @@ -180,7 +192,7 @@ invalid_configs = [ "test": { "sequence": { "event": "test_event", - "service": "homeassistant.turn_on", + "action": "homeassistant.turn_on", } } }, @@ -235,7 +247,7 @@ async def test_bad_config_validation_critical( "good_script": { "alias": "good_script", "sequence": { - "service": "test.automation", + "action": "test.automation", "entity_id": "hello.world", }, }, @@ -300,7 +312,7 @@ async def test_bad_config_validation( "good_script": { "alias": "good_script", "sequence": { - "service": "test.automation", + "action": "test.automation", "entity_id": "hello.world", }, }, @@ -342,7 +354,7 @@ async def test_bad_config_validation( object_id: { "alias": "bad_script", "sequence": { - "service": "test.automation", + "action": "test.automation", "entity_id": "hello.world", }, }, @@ -430,7 +442,7 @@ async def test_reload_unchanged_does_not_stop( "sequence": [ {"event": "running"}, {"wait_template": "{{ is_state('test.entity', 'goodbye') }}"}, - {"service": "test.script"}, + {"action": "test.script"}, ], } } @@ -473,13 +485,13 @@ async def test_reload_unchanged_does_not_stop( [ { "test": { - "sequence": [{"service": "test.script"}], + "sequence": [{"action": "test.script"}], } }, # A script using templates { "test": { - "sequence": [{"service": "{{ 'test.script' }}"}], + "sequence": [{"action": "{{ 'test.script' }}"}], } }, # A script using blueprint @@ -666,7 +678,7 @@ async def test_logging_script_error( assert await async_setup_component( hass, "script", - {"script": {"hello": {"sequence": [{"service": "non.existing"}]}}}, + {"script": {"hello": {"sequence": [{"action": "non.existing"}]}}}, ) with pytest.raises(ServiceNotFound) as err: await hass.services.async_call("script", "hello", blocking=True) @@ -690,7 +702,7 @@ async def test_async_get_descriptions_script(hass: HomeAssistant) -> None: """Test async_set_service_schema for the script integration.""" script_config = { DOMAIN: { - "test1": {"sequence": [{"service": "homeassistant.restart"}]}, + "test1": {"sequence": [{"action": "homeassistant.restart"}]}, "test2": { "description": "test2", "fields": { @@ -699,7 +711,7 @@ async def test_async_get_descriptions_script(hass: HomeAssistant) -> None: "example": "param_example", } }, - "sequence": [{"service": "homeassistant.restart"}], + "sequence": [{"action": "homeassistant.restart"}], }, } } @@ -795,11 +807,11 @@ async def test_extraction_functions( "test1": { "sequence": [ { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.in_both"}, }, { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.in_first"}, }, { @@ -809,15 +821,15 @@ async def test_extraction_functions( "device_id": device_in_both.id, }, { - "service": "test.test", + "action": "test.test", "target": {"area_id": "area-in-both"}, }, { - "service": "test.test", + "action": "test.test", "target": {"floor_id": "floor-in-both"}, }, { - "service": "test.test", + "action": "test.test", "target": {"label_id": "label-in-both"}, }, ] @@ -825,7 +837,7 @@ async def test_extraction_functions( "test2": { "sequence": [ { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.in_both"}, }, { @@ -851,7 +863,7 @@ async def test_extraction_functions( "test3": { "sequence": [ { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.in_both"}, }, { @@ -861,27 +873,27 @@ async def test_extraction_functions( }, {"scene": "scene.hello"}, { - "service": "test.test", + "action": "test.test", "target": {"area_id": "area-in-both"}, }, { - "service": "test.test", + "action": "test.test", "target": {"area_id": "area-in-last"}, }, { - "service": "test.test", + "action": "test.test", "target": {"floor_id": "floor-in-both"}, }, { - "service": "test.test", + "action": "test.test", "target": {"floor_id": "floor-in-last"}, }, { - "service": "test.test", + "action": "test.test", "target": {"label_id": "label-in-both"}, }, { - "service": "test.test", + "action": "test.test", "target": {"label_id": "label-in-last"}, }, ], @@ -1028,11 +1040,11 @@ async def test_concurrent_script(hass: HomeAssistant, concurrently) -> None: """Test calling script concurrently or not.""" if concurrently: call_script_2 = { - "service": "script.turn_on", + "action": "script.turn_on", "data": {"entity_id": "script.script2"}, } else: - call_script_2 = {"service": "script.script2"} + call_script_2 = {"action": "script.script2"} assert await async_setup_component( hass, "script", @@ -1045,17 +1057,17 @@ async def test_concurrent_script(hass: HomeAssistant, concurrently) -> None: { "wait_template": "{{ is_state('input_boolean.test1', 'on') }}" }, - {"service": "test.script", "data": {"value": "script1"}}, + {"action": "test.script", "data": {"value": "script1"}}, ], }, "script2": { "mode": "parallel", "sequence": [ - {"service": "test.script", "data": {"value": "script2a"}}, + {"action": "test.script", "data": {"value": "script2a"}}, { "wait_template": "{{ is_state('input_boolean.test2', 'on') }}" }, - {"service": "test.script", "data": {"value": "script2b"}}, + {"action": "test.script", "data": {"value": "script2b"}}, ], }, } @@ -1126,7 +1138,7 @@ async def test_script_variables( }, "sequence": [ { - "service": "test.script", + "action": "test.script", "data": { "value": "{{ test_var }}", "templated_config_var": "{{ templated_config_var }}", @@ -1142,7 +1154,7 @@ async def test_script_variables( }, "sequence": [ { - "service": "test.script", + "action": "test.script", "data": { "value": "{{ test_var }}", }, @@ -1155,7 +1167,7 @@ async def test_script_variables( }, "sequence": [ { - "service": "test.script", + "action": "test.script", "data": { "value": "{{ test_var }}", }, @@ -1221,7 +1233,7 @@ async def test_script_this_var_always( "script1": { "sequence": [ { - "service": "test.script", + "action": "test.script", "data": { "this_template": "{{this.entity_id}}", }, @@ -1306,8 +1318,8 @@ async def test_recursive_script( "script1": { "mode": script_mode, "sequence": [ - {"service": "script.script1"}, - {"service": "test.script"}, + {"action": "script.script1"}, + {"action": "test.script"}, ], }, } @@ -1356,26 +1368,26 @@ async def test_recursive_script_indirect( "script1": { "mode": script_mode, "sequence": [ - {"service": "script.script2"}, + {"action": "script.script2"}, ], }, "script2": { "mode": script_mode, "sequence": [ - {"service": "script.script3"}, + {"action": "script.script3"}, ], }, "script3": { "mode": script_mode, "sequence": [ - {"service": "script.script4"}, + {"action": "script.script4"}, ], }, "script4": { "mode": script_mode, "sequence": [ - {"service": "script.script1"}, - {"service": "test.script"}, + {"action": "script.script1"}, + {"action": "test.script"}, ], }, } @@ -1440,10 +1452,10 @@ async def test_recursive_script_turn_on( "condition": "template", "value_template": "{{ request == 'step_2' }}", }, - "sequence": {"service": "test.script_done"}, + "sequence": {"action": "test.script_done"}, }, "default": { - "service": "script.turn_on", + "action": "script.turn_on", "data": { "entity_id": "script.script1", "variables": {"request": "step_2"}, @@ -1451,7 +1463,7 @@ async def test_recursive_script_turn_on( }, }, { - "service": "script.turn_on", + "action": "script.turn_on", "data": {"entity_id": "script.script1"}, }, ], @@ -1513,7 +1525,7 @@ async def test_websocket_config( """Test config command.""" config = { "alias": "hello", - "sequence": [{"service": "light.turn_on"}], + "sequence": [{"action": "light.turn_on"}], } assert await async_setup_component( hass, @@ -1577,7 +1589,7 @@ async def test_script_service_changed_entity_id( "script": { "test": { "sequence": { - "service": "test.script", + "action": "test.script", "data_template": {"entity_id": "{{ this.entity_id }}"}, } } @@ -1658,7 +1670,7 @@ async def test_blueprint_script(hass: HomeAssistant, calls: list[ServiceCall]) - "a_number": 5, }, "Blueprint 'Call service' generated invalid script", - "value should be a string for dictionary value @ data['sequence'][0]['service']", + "value should be a string for dictionary value @ data['sequence'][0]['action']", ), ], ) @@ -1839,10 +1851,10 @@ async def test_script_queued_mode(hass: HomeAssistant) -> None: "sequence": [ { "parallel": [ - {"service": "script.test_sub"}, - {"service": "script.test_sub"}, - {"service": "script.test_sub"}, - {"service": "script.test_sub"}, + {"action": "script.test_sub"}, + {"action": "script.test_sub"}, + {"action": "script.test_sub"}, + {"action": "script.test_sub"}, ] } ] @@ -1850,7 +1862,7 @@ async def test_script_queued_mode(hass: HomeAssistant) -> None: "test_sub": { "mode": "queued", "sequence": [ - {"service": "test.simulated_remote"}, + {"action": "test.simulated_remote"}, ], }, } diff --git a/tests/components/script/test_recorder.py b/tests/components/script/test_recorder.py index ca915cede6f..6358093014a 100644 --- a/tests/components/script/test_recorder.py +++ b/tests/components/script/test_recorder.py @@ -52,7 +52,7 @@ async def test_exclude_attributes( "script": { "test": { "sequence": { - "service": "test.script", + "action": "test.script", "data_template": {"hello": "{{ greeting }}"}, } } diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index cde319c0b87..cf72012a1f1 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -6,6 +6,7 @@ import enum import logging import os from socket import _GLOBAL_DEFAULT_TIMEOUT +from typing import Any from unittest.mock import Mock, patch import uuid @@ -416,27 +417,9 @@ def test_service() -> None: schema("homeassistant.turn_on") -def test_service_schema(hass: HomeAssistant) -> None: - """Test service_schema validation.""" - options = ( - {}, - None, - { - "service": "homeassistant.turn_on", - "service_template": "homeassistant.turn_on", - }, - {"data": {"entity_id": "light.kitchen"}}, - {"service": "homeassistant.turn_on", "data": None}, - { - "service": "homeassistant.turn_on", - "data_template": {"brightness": "{{ no_end"}, - }, - ) - for value in options: - with pytest.raises(vol.MultipleInvalid): - cv.SERVICE_SCHEMA(value) - - options = ( +@pytest.mark.parametrize( + "config", + [ {"service": "homeassistant.turn_on"}, {"service": "homeassistant.turn_on", "entity_id": "light.kitchen"}, {"service": "light.turn_on", "entity_id": "all"}, @@ -450,14 +433,70 @@ def test_service_schema(hass: HomeAssistant) -> None: "alias": "turn on kitchen lights", }, {"service": "scene.turn_on", "metadata": {}}, - ) - for value in options: - cv.SERVICE_SCHEMA(value) + {"action": "homeassistant.turn_on"}, + {"action": "homeassistant.turn_on", "entity_id": "light.kitchen"}, + {"action": "light.turn_on", "entity_id": "all"}, + { + "action": "homeassistant.turn_on", + "entity_id": ["light.kitchen", "light.ceiling"], + }, + { + "action": "light.turn_on", + "entity_id": "all", + "alias": "turn on kitchen lights", + }, + {"action": "scene.turn_on", "metadata": {}}, + ], +) +def test_service_schema(hass: HomeAssistant, config: dict[str, Any]) -> None: + """Test service_schema validation.""" + validated = cv.SERVICE_SCHEMA(config) - # Check metadata is removed from the validated output - assert cv.SERVICE_SCHEMA({"service": "scene.turn_on", "metadata": {}}) == { - "service": "scene.turn_on" - } + # Ensure metadata is removed from the validated output + assert "metadata" not in validated + + # Ensure service is migrated to action + assert "service" not in validated + assert "action" in validated + assert validated["action"] == config.get("service", config["action"]) + + +@pytest.mark.parametrize( + "config", + [ + {}, + None, + {"data": {"entity_id": "light.kitchen"}}, + { + "service": "homeassistant.turn_on", + "service_template": "homeassistant.turn_on", + }, + {"service": "homeassistant.turn_on", "data": None}, + { + "service": "homeassistant.turn_on", + "data_template": {"brightness": "{{ no_end"}, + }, + { + "service": "homeassistant.turn_on", + "action": "homeassistant.turn_on", + }, + { + "action": "homeassistant.turn_on", + "service_template": "homeassistant.turn_on", + }, + {"action": "homeassistant.turn_on", "data": None}, + { + "action": "homeassistant.turn_on", + "data_template": {"brightness": "{{ no_end"}, + }, + ], +) +def test_invalid_service_schema( + hass: HomeAssistant, config: dict[str, Any] | None +) -> None: + """Test service_schema validation fails.""" + with pytest.raises(vol.MultipleInvalid): + cv.SERVICE_SCHEMA(config) def test_entity_service_schema() -> None: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 52d9ff11059..1bc33140124 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -249,7 +249,7 @@ async def test_calling_service_basic( alias = "service step" sequence = cv.SCRIPT_SCHEMA( - {"alias": alias, "service": "test.script", "data": {"hello": "world"}} + {"alias": alias, "action": "test.script", "data": {"hello": "world"}} ) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") @@ -352,13 +352,13 @@ async def test_calling_service_response_data( [ { "alias": "service step1", - "service": "test.script", + "action": "test.script", # Store the result of the service call as a variable "response_variable": "my_response", }, { "alias": "service step2", - "service": "test.script", + "action": "test.script", "data_template": { # Result of previous service call "key": "{{ my_response.data }}" @@ -441,7 +441,7 @@ async def test_service_response_data_errors( [ { "alias": "service step1", - "service": "test.script", + "action": "test.script", **params, }, ] @@ -458,7 +458,7 @@ async def test_data_template_with_templated_key(hass: HomeAssistant) -> None: calls = async_mock_service(hass, "test", "script") sequence = cv.SCRIPT_SCHEMA( - {"service": "test.script", "data_template": {"{{ hello_var }}": "world"}} + {"action": "test.script", "data_template": {"{{ hello_var }}": "world"}} ) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") @@ -525,11 +525,11 @@ async def test_multiple_runs_no_wait(hass: HomeAssistant) -> None: sequence = cv.SCRIPT_SCHEMA( [ { - "service": "test.script", + "action": "test.script", "data_template": {"fire": "{{ fire1 }}", "listen": "{{ listen1 }}"}, }, { - "service": "test.script", + "action": "test.script", "data_template": {"fire": "{{ fire2 }}", "listen": "{{ listen2 }}"}, }, ] @@ -605,7 +605,7 @@ async def test_stop_no_wait(hass: HomeAssistant, count) -> None: hass.services.async_register("test", "script", async_simulate_long_service) - sequence = cv.SCRIPT_SCHEMA([{"service": "test.script"}, {"event": event}]) + sequence = cv.SCRIPT_SCHEMA([{"action": "test.script"}, {"event": event}]) script_obj = script.Script( hass, sequence, @@ -3894,7 +3894,7 @@ async def test_parallel_error( sequence = cv.SCRIPT_SCHEMA( { "parallel": [ - {"service": "epic.failure"}, + {"action": "epic.failure"}, ] } ) @@ -3946,7 +3946,7 @@ async def test_propagate_error_service_not_found(hass: HomeAssistant) -> None: await async_setup_component(hass, "homeassistant", {}) event = "test_event" events = async_capture_events(hass, event) - sequence = cv.SCRIPT_SCHEMA([{"service": "test.script"}, {"event": event}]) + sequence = cv.SCRIPT_SCHEMA([{"action": "test.script"}, {"event": event}]) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") with pytest.raises(exceptions.ServiceNotFound): @@ -3980,7 +3980,7 @@ async def test_propagate_error_invalid_service_data(hass: HomeAssistant) -> None events = async_capture_events(hass, event) calls = async_mock_service(hass, "test", "script", vol.Schema({"text": str})) sequence = cv.SCRIPT_SCHEMA( - [{"service": "test.script", "data": {"text": 1}}, {"event": event}] + [{"action": "test.script", "data": {"text": 1}}, {"event": event}] ) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") @@ -4022,7 +4022,7 @@ async def test_propagate_error_service_exception(hass: HomeAssistant) -> None: hass.services.async_register("test", "script", record_call) - sequence = cv.SCRIPT_SCHEMA([{"service": "test.script"}, {"event": event}]) + sequence = cv.SCRIPT_SCHEMA([{"action": "test.script"}, {"event": event}]) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") with pytest.raises(ValueError): @@ -4057,35 +4057,35 @@ async def test_referenced_labels(hass: HomeAssistant) -> None: cv.SCRIPT_SCHEMA( [ { - "service": "test.script", + "action": "test.script", "data": {"label_id": "label_service_not_list"}, }, { - "service": "test.script", + "action": "test.script", "data": { "label_id": ["label_service_list_1", "label_service_list_2"] }, }, { - "service": "test.script", + "action": "test.script", "data": {"label_id": "{{ 'label_service_template' }}"}, }, { - "service": "test.script", + "action": "test.script", "target": {"label_id": "label_in_target"}, }, { - "service": "test.script", + "action": "test.script", "data_template": {"label_id": "label_in_data_template"}, }, - {"service": "test.script", "data": {"without": "label_id"}}, + {"action": "test.script", "data": {"without": "label_id"}}, { "choose": [ { "conditions": "{{ true == false }}", "sequence": [ { - "service": "test.script", + "action": "test.script", "data": {"label_id": "label_choice_1_seq"}, } ], @@ -4094,7 +4094,7 @@ async def test_referenced_labels(hass: HomeAssistant) -> None: "conditions": "{{ true == false }}", "sequence": [ { - "service": "test.script", + "action": "test.script", "data": {"label_id": "label_choice_2_seq"}, } ], @@ -4102,7 +4102,7 @@ async def test_referenced_labels(hass: HomeAssistant) -> None: ], "default": [ { - "service": "test.script", + "action": "test.script", "data": {"label_id": "label_default_seq"}, } ], @@ -4113,13 +4113,13 @@ async def test_referenced_labels(hass: HomeAssistant) -> None: "if": [], "then": [ { - "service": "test.script", + "action": "test.script", "data": {"label_id": "label_if_then"}, } ], "else": [ { - "service": "test.script", + "action": "test.script", "data": {"label_id": "label_if_else"}, } ], @@ -4127,7 +4127,7 @@ async def test_referenced_labels(hass: HomeAssistant) -> None: { "parallel": [ { - "service": "test.script", + "action": "test.script", "data": {"label_id": "label_parallel"}, } ], @@ -4161,33 +4161,33 @@ async def test_referenced_floors(hass: HomeAssistant) -> None: cv.SCRIPT_SCHEMA( [ { - "service": "test.script", + "action": "test.script", "data": {"floor_id": "floor_service_not_list"}, }, { - "service": "test.script", + "action": "test.script", "data": {"floor_id": ["floor_service_list"]}, }, { - "service": "test.script", + "action": "test.script", "data": {"floor_id": "{{ 'floor_service_template' }}"}, }, { - "service": "test.script", + "action": "test.script", "target": {"floor_id": "floor_in_target"}, }, { - "service": "test.script", + "action": "test.script", "data_template": {"floor_id": "floor_in_data_template"}, }, - {"service": "test.script", "data": {"without": "floor_id"}}, + {"action": "test.script", "data": {"without": "floor_id"}}, { "choose": [ { "conditions": "{{ true == false }}", "sequence": [ { - "service": "test.script", + "action": "test.script", "data": {"floor_id": "floor_choice_1_seq"}, } ], @@ -4196,7 +4196,7 @@ async def test_referenced_floors(hass: HomeAssistant) -> None: "conditions": "{{ true == false }}", "sequence": [ { - "service": "test.script", + "action": "test.script", "data": {"floor_id": "floor_choice_2_seq"}, } ], @@ -4204,7 +4204,7 @@ async def test_referenced_floors(hass: HomeAssistant) -> None: ], "default": [ { - "service": "test.script", + "action": "test.script", "data": {"floor_id": "floor_default_seq"}, } ], @@ -4215,13 +4215,13 @@ async def test_referenced_floors(hass: HomeAssistant) -> None: "if": [], "then": [ { - "service": "test.script", + "action": "test.script", "data": {"floor_id": "floor_if_then"}, } ], "else": [ { - "service": "test.script", + "action": "test.script", "data": {"floor_id": "floor_if_else"}, } ], @@ -4229,7 +4229,7 @@ async def test_referenced_floors(hass: HomeAssistant) -> None: { "parallel": [ { - "service": "test.script", + "action": "test.script", "data": {"floor_id": "floor_parallel"}, } ], @@ -4262,33 +4262,33 @@ async def test_referenced_areas(hass: HomeAssistant) -> None: cv.SCRIPT_SCHEMA( [ { - "service": "test.script", + "action": "test.script", "data": {"area_id": "area_service_not_list"}, }, { - "service": "test.script", + "action": "test.script", "data": {"area_id": ["area_service_list"]}, }, { - "service": "test.script", + "action": "test.script", "data": {"area_id": "{{ 'area_service_template' }}"}, }, { - "service": "test.script", + "action": "test.script", "target": {"area_id": "area_in_target"}, }, { - "service": "test.script", + "action": "test.script", "data_template": {"area_id": "area_in_data_template"}, }, - {"service": "test.script", "data": {"without": "area_id"}}, + {"action": "test.script", "data": {"without": "area_id"}}, { "choose": [ { "conditions": "{{ true == false }}", "sequence": [ { - "service": "test.script", + "action": "test.script", "data": {"area_id": "area_choice_1_seq"}, } ], @@ -4297,7 +4297,7 @@ async def test_referenced_areas(hass: HomeAssistant) -> None: "conditions": "{{ true == false }}", "sequence": [ { - "service": "test.script", + "action": "test.script", "data": {"area_id": "area_choice_2_seq"}, } ], @@ -4305,7 +4305,7 @@ async def test_referenced_areas(hass: HomeAssistant) -> None: ], "default": [ { - "service": "test.script", + "action": "test.script", "data": {"area_id": "area_default_seq"}, } ], @@ -4316,13 +4316,13 @@ async def test_referenced_areas(hass: HomeAssistant) -> None: "if": [], "then": [ { - "service": "test.script", + "action": "test.script", "data": {"area_id": "area_if_then"}, } ], "else": [ { - "service": "test.script", + "action": "test.script", "data": {"area_id": "area_if_else"}, } ], @@ -4330,7 +4330,7 @@ async def test_referenced_areas(hass: HomeAssistant) -> None: { "parallel": [ { - "service": "test.script", + "action": "test.script", "data": {"area_id": "area_parallel"}, } ], @@ -4364,27 +4364,27 @@ async def test_referenced_entities(hass: HomeAssistant) -> None: cv.SCRIPT_SCHEMA( [ { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.service_not_list"}, }, { - "service": "test.script", + "action": "test.script", "data": {"entity_id": ["light.service_list"]}, }, { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "{{ 'light.service_template' }}"}, }, { - "service": "test.script", + "action": "test.script", "entity_id": "light.direct_entity_referenced", }, { - "service": "test.script", + "action": "test.script", "target": {"entity_id": "light.entity_in_target"}, }, { - "service": "test.script", + "action": "test.script", "data_template": {"entity_id": "light.entity_in_data_template"}, }, { @@ -4392,7 +4392,7 @@ async def test_referenced_entities(hass: HomeAssistant) -> None: "entity_id": "sensor.condition", "state": "100", }, - {"service": "test.script", "data": {"without": "entity_id"}}, + {"action": "test.script", "data": {"without": "entity_id"}}, {"scene": "scene.hello"}, { "choose": [ @@ -4400,7 +4400,7 @@ async def test_referenced_entities(hass: HomeAssistant) -> None: "conditions": "{{ states.light.choice_1_cond == 'on' }}", "sequence": [ { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.choice_1_seq"}, } ], @@ -4413,7 +4413,7 @@ async def test_referenced_entities(hass: HomeAssistant) -> None: }, "sequence": [ { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.choice_2_seq"}, } ], @@ -4421,7 +4421,7 @@ async def test_referenced_entities(hass: HomeAssistant) -> None: ], "default": [ { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.default_seq"}, } ], @@ -4432,13 +4432,13 @@ async def test_referenced_entities(hass: HomeAssistant) -> None: "if": [], "then": [ { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.if_then"}, } ], "else": [ { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.if_else"}, } ], @@ -4446,7 +4446,7 @@ async def test_referenced_entities(hass: HomeAssistant) -> None: { "parallel": [ { - "service": "test.script", + "action": "test.script", "data": {"entity_id": "light.parallel"}, } ], @@ -4491,19 +4491,19 @@ async def test_referenced_devices(hass: HomeAssistant) -> None: "domain": "switch", }, { - "service": "test.script", + "action": "test.script", "data": {"device_id": "data-string-id"}, }, { - "service": "test.script", + "action": "test.script", "data_template": {"device_id": "data-template-string-id"}, }, { - "service": "test.script", + "action": "test.script", "target": {"device_id": "target-string-id"}, }, { - "service": "test.script", + "action": "test.script", "target": {"device_id": ["target-list-id-1", "target-list-id-2"]}, }, { @@ -4515,7 +4515,7 @@ async def test_referenced_devices(hass: HomeAssistant) -> None: ), "sequence": [ { - "service": "test.script", + "action": "test.script", "target": { "device_id": "choice-1-seq-device-target" }, @@ -4530,7 +4530,7 @@ async def test_referenced_devices(hass: HomeAssistant) -> None: }, "sequence": [ { - "service": "test.script", + "action": "test.script", "target": { "device_id": "choice-2-seq-device-target" }, @@ -4540,7 +4540,7 @@ async def test_referenced_devices(hass: HomeAssistant) -> None: ], "default": [ { - "service": "test.script", + "action": "test.script", "target": {"device_id": "default-device-target"}, } ], @@ -4549,13 +4549,13 @@ async def test_referenced_devices(hass: HomeAssistant) -> None: "if": [], "then": [ { - "service": "test.script", + "action": "test.script", "data": {"device_id": "if-then"}, } ], "else": [ { - "service": "test.script", + "action": "test.script", "data": {"device_id": "if-else"}, } ], @@ -4563,7 +4563,7 @@ async def test_referenced_devices(hass: HomeAssistant) -> None: { "parallel": [ { - "service": "test.script", + "action": "test.script", "target": {"device_id": "parallel-device"}, } ], @@ -5104,7 +5104,7 @@ async def test_set_variable( sequence = cv.SCRIPT_SCHEMA( [ {"alias": alias, "variables": {"variable": "value"}}, - {"service": "test.script", "data": {"value": "{{ variable }}"}}, + {"action": "test.script", "data": {"value": "{{ variable }}"}}, ] ) script_obj = script.Script(hass, sequence, "test script", "test_domain") @@ -5143,9 +5143,9 @@ async def test_set_redefines_variable( sequence = cv.SCRIPT_SCHEMA( [ {"variables": {"variable": "1"}}, - {"service": "test.script", "data": {"value": "{{ variable }}"}}, + {"action": "test.script", "data": {"value": "{{ variable }}"}}, {"variables": {"variable": "{{ variable | int + 1 }}"}}, - {"service": "test.script", "data": {"value": "{{ variable }}"}}, + {"action": "test.script", "data": {"value": "{{ variable }}"}}, ] ) script_obj = script.Script(hass, sequence, "test script", "test_domain") @@ -5214,7 +5214,7 @@ async def test_validate_action_config( } configs = { - cv.SCRIPT_ACTION_CALL_SERVICE: {"service": "light.turn_on"}, + cv.SCRIPT_ACTION_CALL_SERVICE: {"action": "light.turn_on"}, cv.SCRIPT_ACTION_DELAY: {"delay": 5}, cv.SCRIPT_ACTION_WAIT_TEMPLATE: { "wait_template": "{{ states.light.kitchen.state == 'on' }}" @@ -5349,7 +5349,7 @@ async def test_embedded_wait_for_trigger_in_automation(hass: HomeAssistant) -> N } ] }, - {"service": "test.script"}, + {"action": "test.script"}, ], } }, @@ -5704,12 +5704,12 @@ async def test_continue_on_error(hass: HomeAssistant) -> None: {"event": "test_event"}, { "continue_on_error": True, - "service": "broken.service", + "action": "broken.service", }, {"event": "test_event"}, { "continue_on_error": False, - "service": "broken.service", + "action": "broken.service", }, {"event": "test_event"}, ] @@ -5786,7 +5786,7 @@ async def test_continue_on_error_automation_issue(hass: HomeAssistant) -> None: [ { "continue_on_error": True, - "service": "service.not_found", + "action": "service.not_found", }, ] ) @@ -5834,7 +5834,7 @@ async def test_continue_on_error_unknown_error(hass: HomeAssistant) -> None: [ { "continue_on_error": True, - "service": "some.service", + "action": "some.service", }, ] ) @@ -5884,7 +5884,7 @@ async def test_disabled_actions( { "alias": "Hello", "enabled": enabled_value, - "service": "broken.service", + "action": "broken.service", }, { "alias": "World", @@ -6255,7 +6255,7 @@ async def test_disallowed_recursion( context = Context() calls = 0 alias = "event step" - sequence1 = cv.SCRIPT_SCHEMA({"alias": alias, "service": "test.call_script_2"}) + sequence1 = cv.SCRIPT_SCHEMA({"alias": alias, "action": "test.call_script_2"}) script1_obj = script.Script( hass, sequence1, @@ -6265,7 +6265,7 @@ async def test_disallowed_recursion( running_description="test script1", ) - sequence2 = cv.SCRIPT_SCHEMA({"alias": alias, "service": "test.call_script_3"}) + sequence2 = cv.SCRIPT_SCHEMA({"alias": alias, "action": "test.call_script_3"}) script2_obj = script.Script( hass, sequence2, @@ -6275,7 +6275,7 @@ async def test_disallowed_recursion( running_description="test script2", ) - sequence3 = cv.SCRIPT_SCHEMA({"alias": alias, "service": "test.call_script_1"}) + sequence3 = cv.SCRIPT_SCHEMA({"alias": alias, "action": "test.call_script_1"}) script3_obj = script.Script( hass, sequence3, @@ -6315,3 +6315,43 @@ async def test_disallowed_recursion( "- test_domain2.Test Name2\n" "- test_domain3.Test Name3" ) in caplog.text + + +async def test_calling_service_backwards_compatible( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the calling of a service with the service instead of the action key.""" + context = Context() + calls = async_mock_service(hass, "test", "script") + + alias = "service step" + sequence = cv.SCRIPT_SCHEMA( + {"alias": alias, "service": "test.script", "data": {"hello": "{{ 'world' }}"}} + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + await script_obj.async_run(context=context) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].context is context + assert calls[0].data.get("hello") == "world" + assert f"Executing step {alias}" in caplog.text + + assert_action_trace( + { + "0": [ + { + "result": { + "params": { + "domain": "test", + "service": "script", + "service_data": {"hello": "world"}, + "target": {}, + }, + "running_script": False, + } + } + ], + } + ) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index b05cdf9c3ae..81cc189e1af 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -405,7 +405,7 @@ async def test_service_call(hass: HomeAssistant) -> None: """Test service call with templating.""" calls = async_mock_service(hass, "test_domain", "test_service") config = { - "service": "{{ 'test_domain.test_service' }}", + "action": "{{ 'test_domain.test_service' }}", "entity_id": "hello.world", "data": { "hello": "{{ 'goodbye' }}", @@ -435,7 +435,7 @@ async def test_service_call(hass: HomeAssistant) -> None: } config = { - "service": "{{ 'test_domain.test_service' }}", + "action": "{{ 'test_domain.test_service' }}", "target": { "area_id": ["area-42", "{{ 'area-51' }}"], "device_id": ["abcdef", "{{ 'fedcba' }}"], @@ -455,7 +455,7 @@ async def test_service_call(hass: HomeAssistant) -> None: } config = { - "service": "{{ 'test_domain.test_service' }}", + "action": "{{ 'test_domain.test_service' }}", "target": "{{ var_target }}", } @@ -542,7 +542,7 @@ async def test_split_entity_string(hass: HomeAssistant) -> None: await service.async_call_from_config( hass, { - "service": "test_domain.test_service", + "action": "test_domain.test_service", "entity_id": "hello.world, sensor.beer", }, ) @@ -554,7 +554,7 @@ async def test_not_mutate_input(hass: HomeAssistant) -> None: """Test for immutable input.""" async_mock_service(hass, "test_domain", "test_service") config = { - "service": "test_domain.test_service", + "action": "test_domain.test_service", "entity_id": "hello.world, sensor.beer", "data": {"hello": 1}, "data_template": {"nested": {"value": "{{ 1 + 1 }}"}}, @@ -581,7 +581,7 @@ async def test_fail_silently_if_no_service(mock_log, hass: HomeAssistant) -> Non await service.async_call_from_config(hass, {}) assert mock_log.call_count == 2 - await service.async_call_from_config(hass, {"service": "invalid"}) + await service.async_call_from_config(hass, {"action": "invalid"}) assert mock_log.call_count == 3 @@ -597,7 +597,7 @@ async def test_service_call_entry_id( assert entry.entity_id == "hello.world" config = { - "service": "test_domain.test_service", + "action": "test_domain.test_service", "target": {"entity_id": entry.id}, } @@ -613,7 +613,7 @@ async def test_service_call_all_none(hass: HomeAssistant, target) -> None: calls = async_mock_service(hass, "test_domain", "test_service") config = { - "service": "test_domain.test_service", + "action": "test_domain.test_service", "target": {"entity_id": target}, } From 4f5eab4646e0560188f2dca6a9daae0e48b11dac Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 31 Jul 2024 05:37:39 -0700 Subject: [PATCH 1786/2411] Improve quality of ollama tool calling by repairing arguments (#122749) * Improve quality of ollama function calling by repairing function call arguments * Fix formatting of the tests * Run ruff format on ollama conversation * Add test for non-string arguments --- .../components/ollama/conversation.py | 30 ++++++++++++++++++- tests/components/ollama/test_conversation.py | 26 ++++++++++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ollama/conversation.py b/homeassistant/components/ollama/conversation.py index ac367a5cf6a..f59e268394b 100644 --- a/homeassistant/components/ollama/conversation.py +++ b/homeassistant/components/ollama/conversation.py @@ -63,6 +63,34 @@ def _format_tool( return {"type": "function", "function": tool_spec} +def _fix_invalid_arguments(value: Any) -> Any: + """Attempt to repair incorrectly formatted json function arguments. + + Small models (for example llama3.1 8B) may produce invalid argument values + which we attempt to repair here. + """ + if not isinstance(value, str): + return value + if (value.startswith("[") and value.endswith("]")) or ( + value.startswith("{") and value.endswith("}") + ): + try: + return json.loads(value) + except json.decoder.JSONDecodeError: + pass + return value + + +def _parse_tool_args(arguments: dict[str, Any]) -> dict[str, Any]: + """Rewrite ollama tool arguments. + + This function improves tool use quality by fixing common mistakes made by + small local tool use models. This will repair invalid json arguments and + omit unnecessary arguments with empty values that will fail intent parsing. + """ + return {k: _fix_invalid_arguments(v) for k, v in arguments.items() if v} + + class OllamaConversationEntity( conversation.ConversationEntity, conversation.AbstractConversationAgent ): @@ -255,7 +283,7 @@ class OllamaConversationEntity( for tool_call in tool_calls: tool_input = llm.ToolInput( tool_name=tool_call["function"]["name"], - tool_args=tool_call["function"]["arguments"], + tool_args=_parse_tool_args(tool_call["function"]["arguments"]), ) _LOGGER.debug( "Tool call: %s(%s)", tool_input.tool_name, tool_input.tool_args diff --git a/tests/components/ollama/test_conversation.py b/tests/components/ollama/test_conversation.py index 9be6f3b33a3..b5a94cc6f57 100644 --- a/tests/components/ollama/test_conversation.py +++ b/tests/components/ollama/test_conversation.py @@ -1,5 +1,6 @@ """Tests for the Ollama integration.""" +from typing import Any from unittest.mock import AsyncMock, Mock, patch from ollama import Message, ResponseError @@ -116,12 +117,33 @@ async def test_template_variables( assert "The user id is 12345." in prompt +@pytest.mark.parametrize( + ("tool_args", "expected_tool_args"), + [ + ({"param1": "test_value"}, {"param1": "test_value"}), + ({"param1": 2}, {"param1": 2}), + ( + {"param1": "test_value", "floor": ""}, + {"param1": "test_value"}, # Omit empty arguments + ), + ( + {"domain": '["light"]'}, + {"domain": ["light"]}, # Repair invalid json arguments + ), + ( + {"domain": "['light']"}, + {"domain": "['light']"}, # Preserve invalid json that can't be parsed + ), + ], +) @patch("homeassistant.components.ollama.conversation.llm.AssistAPI._async_get_tools") async def test_function_call( mock_get_tools, hass: HomeAssistant, mock_config_entry_with_assist: MockConfigEntry, mock_init_component, + tool_args: dict[str, Any], + expected_tool_args: dict[str, Any], ) -> None: """Test function call from the assistant.""" agent_id = mock_config_entry_with_assist.entry_id @@ -154,7 +176,7 @@ async def test_function_call( { "function": { "name": "test_tool", - "arguments": {"param1": "test_value"}, + "arguments": tool_args, } } ], @@ -183,7 +205,7 @@ async def test_function_call( hass, llm.ToolInput( tool_name="test_tool", - tool_args={"param1": "test_value"}, + tool_args=expected_tool_args, ), llm.LLMContext( platform="ollama", From 8d0e998e549bc176134231b93fa0f6495a7d4591 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 31 Jul 2024 05:38:44 -0700 Subject: [PATCH 1787/2411] Improve conversation agent tracing to help with eval and data collection (#122542) --- .../components/conversation/default_agent.py | 11 +++++++++++ homeassistant/components/conversation/trace.py | 4 ++-- .../conversation.py | 1 + .../components/openai_conversation/conversation.py | 3 ++- homeassistant/helpers/llm.py | 2 +- tests/components/conversation/test_trace.py | 12 +++++++++++- .../test_conversation.py | 3 ++- .../openai_conversation/test_conversation.py | 3 ++- 8 files changed, 32 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 45393289ac8..1661d2ad30d 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -47,6 +47,7 @@ from homeassistant.util.json import JsonObjectType, json_loads_object from .const import DEFAULT_EXPOSED_ATTRIBUTES, DOMAIN, ConversationEntityFeature from .entity import ConversationEntity from .models import ConversationInput, ConversationResult +from .trace import ConversationTraceEventType, async_conversation_trace_append _LOGGER = logging.getLogger(__name__) _DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that" @@ -348,6 +349,16 @@ class DefaultAgent(ConversationEntity): } for entity in result.entities_list } + async_conversation_trace_append( + ConversationTraceEventType.TOOL_CALL, + { + "intent_name": result.intent.name, + "slots": { + entity.name: entity.value or entity.text + for entity in result.entities_list + }, + }, + ) try: intent_response = await intent.async_handle( diff --git a/homeassistant/components/conversation/trace.py b/homeassistant/components/conversation/trace.py index 08b271d9058..6f993aa326a 100644 --- a/homeassistant/components/conversation/trace.py +++ b/homeassistant/components/conversation/trace.py @@ -22,8 +22,8 @@ class ConversationTraceEventType(enum.StrEnum): AGENT_DETAIL = "agent_detail" """Event detail added by a conversation agent.""" - LLM_TOOL_CALL = "llm_tool_call" - """An LLM Tool call""" + TOOL_CALL = "tool_call" + """A conversation agent Tool call or default agent intent call.""" @dataclass(frozen=True) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 8dec62ad26b..a5c911bb757 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -286,6 +286,7 @@ class GoogleGenerativeAIConversationEntity( if supports_system_instruction else messages[2:], "prompt": prompt, + "tools": [*llm_api.tools] if llm_api else None, }, ) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index dd42049e3d0..483b37945d6 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -225,7 +225,8 @@ class OpenAIConversationEntity( LOGGER.debug("Prompt: %s", messages) LOGGER.debug("Tools: %s", tools) trace.async_conversation_trace_append( - trace.ConversationTraceEventType.AGENT_DETAIL, {"messages": messages} + trace.ConversationTraceEventType.AGENT_DETAIL, + {"messages": messages, "tools": llm_api.tools if llm_api else None}, ) client = self.entry.runtime_data diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 8ad576b7ea5..4ddb00166b6 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -167,7 +167,7 @@ class APIInstance: async def async_call_tool(self, tool_input: ToolInput) -> JsonObjectType: """Call a LLM tool, validate args and return the response.""" async_conversation_trace_append( - ConversationTraceEventType.LLM_TOOL_CALL, + ConversationTraceEventType.TOOL_CALL, {"tool_name": tool_input.tool_name, "tool_args": tool_input.tool_args}, ) diff --git a/tests/components/conversation/test_trace.py b/tests/components/conversation/test_trace.py index c586eb8865d..59cd10d2510 100644 --- a/tests/components/conversation/test_trace.py +++ b/tests/components/conversation/test_trace.py @@ -33,7 +33,7 @@ async def test_converation_trace( assert traces last_trace = traces[-1].as_dict() assert last_trace.get("events") - assert len(last_trace.get("events")) == 1 + assert len(last_trace.get("events")) == 2 trace_event = last_trace["events"][0] assert ( trace_event.get("event_type") == trace.ConversationTraceEventType.ASYNC_PROCESS @@ -50,6 +50,16 @@ async def test_converation_trace( == "Added apples" ) + trace_event = last_trace["events"][1] + assert trace_event.get("event_type") == trace.ConversationTraceEventType.TOOL_CALL + assert trace_event.get("data") == { + "intent_name": "HassListAddItem", + "slots": { + "name": "Shopping List", + "item": "apples ", + }, + } + async def test_converation_trace_error( hass: HomeAssistant, diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index afeb6d01faa..41f96c7b0ac 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -269,11 +269,12 @@ async def test_function_call( assert [event["event_type"] for event in trace_events] == [ trace.ConversationTraceEventType.ASYNC_PROCESS, trace.ConversationTraceEventType.AGENT_DETAIL, - trace.ConversationTraceEventType.LLM_TOOL_CALL, + trace.ConversationTraceEventType.TOOL_CALL, ] # AGENT_DETAIL event contains the raw prompt passed to the model detail_event = trace_events[1] assert "Answer in plain text" in detail_event["data"]["prompt"] + assert [t.name for t in detail_event["data"]["tools"]] == ["test_tool"] @patch( diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index fee1543a0d7..3364d822245 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -294,7 +294,7 @@ async def test_function_call( assert [event["event_type"] for event in trace_events] == [ trace.ConversationTraceEventType.ASYNC_PROCESS, trace.ConversationTraceEventType.AGENT_DETAIL, - trace.ConversationTraceEventType.LLM_TOOL_CALL, + trace.ConversationTraceEventType.TOOL_CALL, ] # AGENT_DETAIL event contains the raw prompt passed to the model detail_event = trace_events[1] @@ -303,6 +303,7 @@ async def test_function_call( "Today's date is 2024-06-03." in trace_events[1]["data"]["messages"][0]["content"] ) + assert [t.name for t in detail_event["data"]["tools"]] == ["test_tool"] # Call it again, make sure we have updated prompt with ( From e706ff0564d6b31304790b443e8c75ae290812b5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:44:14 +0200 Subject: [PATCH 1788/2411] Fix implicit-return in transport_nsw (#122930) --- homeassistant/components/transport_nsw/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 787f3298e59..5628274b967 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta +from typing import Any from TransportNSW import TransportNSW import voluptuous as vol @@ -98,7 +99,7 @@ class TransportNSWSensor(SensorEntity): return self._state @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" if self._times is not None: return { @@ -110,6 +111,7 @@ class TransportNSWSensor(SensorEntity): ATTR_DESTINATION: self._times[ATTR_DESTINATION], ATTR_MODE: self._times[ATTR_MODE], } + return None @property def native_unit_of_measurement(self): From 2f181cbe41064d2310c79d87bc66264340b856c0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:53:05 +0200 Subject: [PATCH 1789/2411] Fix implicit-return in vera (#122934) --- homeassistant/components/vera/cover.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index 542680925f2..25ffe987d5e 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -61,10 +61,11 @@ class VeraCover(VeraDevice[veraApi.VeraCurtain], CoverEntity): self.schedule_update_ha_state() @property - def is_closed(self) -> bool: + def is_closed(self) -> bool | None: """Return if the cover is closed.""" if self.current_cover_position is not None: return self.current_cover_position == 0 + return None def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" From 5e1cca1c58f77ee9c1a1b6d549d00b6a34d001fe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:54:52 +0200 Subject: [PATCH 1790/2411] Fix implicit-return in shelly (#122926) --- homeassistant/components/shelly/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index ab1e58583d9..b77f45afb3f 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -54,7 +54,8 @@ async def async_setup_entry( ) -> None: """Set up climate device.""" if get_device_entry_gen(config_entry) in RPC_GENERATIONS: - return async_setup_rpc_entry(hass, config_entry, async_add_entities) + async_setup_rpc_entry(hass, config_entry, async_add_entities) + return coordinator = config_entry.runtime_data.block assert coordinator From e64e3c27785a4c7d5c373e20270c870a711e4bde Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:00:53 +0200 Subject: [PATCH 1791/2411] Fix implicit-return in time_date (#122929) --- homeassistant/components/time_date/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index 442442f0e1d..245d10bebba 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -48,7 +48,7 @@ async def async_setup_platform( """Set up the Time and Date sensor.""" if hass.config.time_zone is None: _LOGGER.error("Timezone is not set in Home Assistant configuration") # type: ignore[unreachable] - return False + return async_add_entities( [TimeDateSensor(variable) for variable in config[CONF_DISPLAY_OPTIONS]] From cddb3bb668328b3ee0dac0dbf89d54539d33c7e9 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Wed, 31 Jul 2024 15:08:25 +0200 Subject: [PATCH 1792/2411] Add reconfigure step for here_travel_time (#114667) * Add reconfigure step for here_travel_time * Add comments, reuse step_user, TYPE_CHECKING, remove defaults --- .../here_travel_time/config_flow.py | 183 +++++++++++++----- .../components/here_travel_time/strings.json | 3 +- .../here_travel_time/test_config_flow.py | 102 ++++++++++ 3 files changed, 235 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index 36d5c1efe1e..b708fd9cd3d 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import TYPE_CHECKING, Any from here_routing import ( HERERoutingApi, @@ -104,6 +104,8 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Init Config Flow.""" self._config: dict[str, Any] = {} + self._entry: ConfigEntry | None = None + self._is_reconfigure_flow: bool = False @staticmethod @callback @@ -119,21 +121,36 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} user_input = user_input or {} - if user_input: - try: - await async_validate_api_key(user_input[CONF_API_KEY]) - except HERERoutingUnauthorizedError: - errors["base"] = "invalid_auth" - except (HERERoutingError, HERETransitError): - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - if not errors: - self._config = user_input - return await self.async_step_origin_menu() + if not self._is_reconfigure_flow: # Always show form first for reconfiguration + if user_input: + try: + await async_validate_api_key(user_input[CONF_API_KEY]) + except HERERoutingUnauthorizedError: + errors["base"] = "invalid_auth" + except (HERERoutingError, HERETransitError): + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + if not errors: + self._config[CONF_NAME] = user_input[CONF_NAME] + self._config[CONF_API_KEY] = user_input[CONF_API_KEY] + self._config[CONF_MODE] = user_input[CONF_MODE] + return await self.async_step_origin_menu() + self._is_reconfigure_flow = False return self.async_show_form( step_id="user", data_schema=get_user_step_schema(user_input), errors=errors ) + async def async_step_reconfigure( + self, _: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration.""" + self._is_reconfigure_flow = True + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + if TYPE_CHECKING: + assert self._entry + self._config = self._entry.data.copy() + return await self.async_step_user(self._config) + async def async_step_origin_menu(self, _: None = None) -> ConfigFlowResult: """Show the origin menu.""" return self.async_show_menu( @@ -150,37 +167,57 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): self._config[CONF_ORIGIN_LONGITUDE] = user_input[CONF_ORIGIN][ CONF_LONGITUDE ] + # Remove possible previous configuration using an entity_id + self._config.pop(CONF_ORIGIN_ENTITY_ID, None) return await self.async_step_destination_menu() - schema = vol.Schema( + schema = self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required( + CONF_ORIGIN, + ): LocationSelector() + } + ), { - vol.Required( - CONF_ORIGIN, - default={ - CONF_LATITUDE: self.hass.config.latitude, - CONF_LONGITUDE: self.hass.config.longitude, - }, - ): LocationSelector() - } + CONF_ORIGIN: { + CONF_LATITUDE: self._config.get(CONF_ORIGIN_LATITUDE) + or self.hass.config.latitude, + CONF_LONGITUDE: self._config.get(CONF_ORIGIN_LONGITUDE) + or self.hass.config.longitude, + } + }, ) return self.async_show_form(step_id="origin_coordinates", data_schema=schema) - async def async_step_destination_menu(self, _: None = None) -> ConfigFlowResult: - """Show the destination menu.""" - return self.async_show_menu( - step_id="destination_menu", - menu_options=["destination_coordinates", "destination_entity"], - ) - async def async_step_origin_entity( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Configure origin by using an entity.""" if user_input is not None: self._config[CONF_ORIGIN_ENTITY_ID] = user_input[CONF_ORIGIN_ENTITY_ID] + # Remove possible previous configuration using coordinates + self._config.pop(CONF_ORIGIN_LATITUDE, None) + self._config.pop(CONF_ORIGIN_LONGITUDE, None) return await self.async_step_destination_menu() - schema = vol.Schema({vol.Required(CONF_ORIGIN_ENTITY_ID): EntitySelector()}) + schema = self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required( + CONF_ORIGIN_ENTITY_ID, + ): EntitySelector() + } + ), + {CONF_ORIGIN_ENTITY_ID: self._config.get(CONF_ORIGIN_ENTITY_ID)}, + ) return self.async_show_form(step_id="origin_entity", data_schema=schema) + async def async_step_destination_menu(self, _: None = None) -> ConfigFlowResult: + """Show the destination menu.""" + return self.async_show_menu( + step_id="destination_menu", + menu_options=["destination_coordinates", "destination_entity"], + ) + async def async_step_destination_coordinates( self, user_input: dict[str, Any] | None = None, @@ -193,21 +230,36 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): self._config[CONF_DESTINATION_LONGITUDE] = user_input[CONF_DESTINATION][ CONF_LONGITUDE ] + # Remove possible previous configuration using an entity_id + self._config.pop(CONF_DESTINATION_ENTITY_ID, None) + if self._entry: + return self.async_update_reload_and_abort( + self._entry, + title=self._config[CONF_NAME], + data=self._config, + reason="reconfigure_successful", + ) return self.async_create_entry( title=self._config[CONF_NAME], data=self._config, options=DEFAULT_OPTIONS, ) - schema = vol.Schema( + schema = self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required( + CONF_DESTINATION, + ): LocationSelector() + } + ), { - vol.Required( - CONF_DESTINATION, - default={ - CONF_LATITUDE: self.hass.config.latitude, - CONF_LONGITUDE: self.hass.config.longitude, - }, - ): LocationSelector() - } + CONF_DESTINATION: { + CONF_LATITUDE: self._config.get(CONF_DESTINATION_LATITUDE) + or self.hass.config.latitude, + CONF_LONGITUDE: self._config.get(CONF_DESTINATION_LONGITUDE) + or self.hass.config.longitude, + }, + }, ) return self.async_show_form( step_id="destination_coordinates", data_schema=schema @@ -222,13 +274,27 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): self._config[CONF_DESTINATION_ENTITY_ID] = user_input[ CONF_DESTINATION_ENTITY_ID ] + # Remove possible previous configuration using coordinates + self._config.pop(CONF_DESTINATION_LATITUDE, None) + self._config.pop(CONF_DESTINATION_LONGITUDE, None) + if self._entry: + return self.async_update_reload_and_abort( + self._entry, data=self._config, reason="reconfigure_successful" + ) return self.async_create_entry( title=self._config[CONF_NAME], data=self._config, options=DEFAULT_OPTIONS, ) - schema = vol.Schema( - {vol.Required(CONF_DESTINATION_ENTITY_ID): EntitySelector()} + schema = self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required( + CONF_DESTINATION_ENTITY_ID, + ): EntitySelector() + } + ), + {CONF_DESTINATION_ENTITY_ID: self._config.get(CONF_DESTINATION_ENTITY_ID)}, ) return self.async_show_form(step_id="destination_entity", data_schema=schema) @@ -249,15 +315,22 @@ class HERETravelTimeOptionsFlow(OptionsFlow): self._config = user_input return await self.async_step_time_menu() - schema = vol.Schema( + schema = self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Optional( + CONF_ROUTE_MODE, + default=self.config_entry.options.get( + CONF_ROUTE_MODE, DEFAULT_OPTIONS[CONF_ROUTE_MODE] + ), + ): vol.In(ROUTE_MODES), + } + ), { - vol.Optional( - CONF_ROUTE_MODE, - default=self.config_entry.options.get( - CONF_ROUTE_MODE, DEFAULT_OPTIONS[CONF_ROUTE_MODE] - ), - ): vol.In(ROUTE_MODES), - } + CONF_ROUTE_MODE: self.config_entry.options.get( + CONF_ROUTE_MODE, DEFAULT_OPTIONS[CONF_ROUTE_MODE] + ), + }, ) return self.async_show_form(step_id="init", data_schema=schema) @@ -283,8 +356,11 @@ class HERETravelTimeOptionsFlow(OptionsFlow): self._config[CONF_ARRIVAL_TIME] = user_input[CONF_ARRIVAL_TIME] return self.async_create_entry(title="", data=self._config) - schema = vol.Schema( - {vol.Required(CONF_ARRIVAL_TIME, default="00:00:00"): TimeSelector()} + schema = self.add_suggested_values_to_schema( + vol.Schema( + {vol.Required(CONF_ARRIVAL_TIME, default="00:00:00"): TimeSelector()} + ), + {CONF_ARRIVAL_TIME: "00:00:00"}, ) return self.async_show_form(step_id="arrival_time", data_schema=schema) @@ -297,8 +373,11 @@ class HERETravelTimeOptionsFlow(OptionsFlow): self._config[CONF_DEPARTURE_TIME] = user_input[CONF_DEPARTURE_TIME] return self.async_create_entry(title="", data=self._config) - schema = vol.Schema( - {vol.Required(CONF_DEPARTURE_TIME, default="00:00:00"): TimeSelector()} + schema = self.add_suggested_values_to_schema( + vol.Schema( + {vol.Required(CONF_DEPARTURE_TIME, default="00:00:00"): TimeSelector()} + ), + {CONF_DEPARTURE_TIME: "00:00:00"}, ) return self.async_show_form(step_id="departure_time", data_schema=schema) diff --git a/homeassistant/components/here_travel_time/strings.json b/homeassistant/components/here_travel_time/strings.json index 124aa070595..cfa14a3e3ca 100644 --- a/homeassistant/components/here_travel_time/strings.json +++ b/homeassistant/components/here_travel_time/strings.json @@ -52,7 +52,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, "options": { diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index 9b15a42dd56..ea3de64ed0c 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -6,17 +6,20 @@ from here_routing import HERERoutingError, HERERoutingUnauthorizedError import pytest from homeassistant import config_entries +from homeassistant.components.here_travel_time.config_flow import DEFAULT_OPTIONS from homeassistant.components.here_travel_time.const import ( CONF_ARRIVAL_TIME, CONF_DEPARTURE_TIME, CONF_DESTINATION_ENTITY_ID, CONF_DESTINATION_LATITUDE, CONF_DESTINATION_LONGITUDE, + CONF_ORIGIN_ENTITY_ID, CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, DOMAIN, ROUTE_MODE_FASTEST, + TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PUBLIC, ) @@ -249,6 +252,105 @@ async def test_step_destination_entity( } +@pytest.mark.usefixtures("valid_response") +async def test_reconfigure_destination_entity(hass: HomeAssistant) -> None: + """Test reconfigure flow when choosing a destination entity.""" + origin_entity_selector_result = await do_common_reconfiguration_steps(hass) + menu_result = await hass.config_entries.flow.async_configure( + origin_entity_selector_result["flow_id"], {"next_step_id": "destination_entity"} + ) + assert menu_result["type"] is FlowResultType.FORM + + destination_entity_selector_result = await hass.config_entries.flow.async_configure( + menu_result["flow_id"], + {"destination_entity_id": "zone.home"}, + ) + assert destination_entity_selector_result["type"] is FlowResultType.ABORT + assert destination_entity_selector_result["reason"] == "reconfigure_successful" + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == { + CONF_NAME: "test", + CONF_API_KEY: API_KEY, + CONF_ORIGIN_ENTITY_ID: "zone.home", + CONF_DESTINATION_ENTITY_ID: "zone.home", + CONF_MODE: TRAVEL_MODE_BICYCLE, + } + + +@pytest.mark.usefixtures("valid_response") +async def test_reconfigure_destination_coordinates(hass: HomeAssistant) -> None: + """Test reconfigure flow when choosing destination coordinates.""" + origin_entity_selector_result = await do_common_reconfiguration_steps(hass) + menu_result = await hass.config_entries.flow.async_configure( + origin_entity_selector_result["flow_id"], + {"next_step_id": "destination_coordinates"}, + ) + assert menu_result["type"] is FlowResultType.FORM + + destination_entity_selector_result = await hass.config_entries.flow.async_configure( + menu_result["flow_id"], + { + "destination": { + "latitude": 43.0, + "longitude": -80.3, + "radius": 5.0, + } + }, + ) + assert destination_entity_selector_result["type"] is FlowResultType.ABORT + assert destination_entity_selector_result["reason"] == "reconfigure_successful" + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == { + CONF_NAME: "test", + CONF_API_KEY: API_KEY, + CONF_ORIGIN_ENTITY_ID: "zone.home", + CONF_DESTINATION_LATITUDE: 43.0, + CONF_DESTINATION_LONGITUDE: -80.3, + CONF_MODE: TRAVEL_MODE_BICYCLE, + } + + +async def do_common_reconfiguration_steps(hass: HomeAssistant) -> None: + """Walk through common flow steps for reconfiguring.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data=DEFAULT_CONFIG, + options=DEFAULT_OPTIONS, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + reconfigure_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + ) + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["step_id"] == "user" + + user_step_result = await hass.config_entries.flow.async_configure( + reconfigure_result["flow_id"], + { + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_BICYCLE, + CONF_NAME: "test", + }, + ) + await hass.async_block_till_done() + menu_result = await hass.config_entries.flow.async_configure( + user_step_result["flow_id"], {"next_step_id": "origin_entity"} + ) + return await hass.config_entries.flow.async_configure( + menu_result["flow_id"], + {"origin_entity_id": "zone.home"}, + ) + + async def test_form_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( From a35fa0e95a381582200d49d770ff89119ba9f98e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 31 Jul 2024 08:13:04 -0500 Subject: [PATCH 1793/2411] Warn that the minimum SQLite version will change to 3.40.1 as of 2025.2 (#104298) Co-authored-by: Robert Resch --- .../components/recorder/strings.json | 4 + homeassistant/components/recorder/util.py | 77 ++++++++++++++++--- tests/components/recorder/test_util.py | 71 +++++++++++++++-- 3 files changed, 132 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/recorder/strings.json b/homeassistant/components/recorder/strings.json index bf5d95ae1fc..f891b4d18d2 100644 --- a/homeassistant/components/recorder/strings.json +++ b/homeassistant/components/recorder/strings.json @@ -16,6 +16,10 @@ "backup_failed_out_of_resources": { "title": "Database backup failed due to lack of resources", "description": "The database backup stated at {start_time} failed due to lack of resources. The backup cannot be trusted and must be restarted. This can happen if the database is too large or if the system is under heavy load. Consider upgrading the system hardware or reducing the size of the database by decreasing the number of history days to keep or creating a filter." + }, + "sqlite_too_old": { + "title": "Update SQLite to {min_version} or later to continue using the recorder", + "description": "Support for version {server_version} of SQLite is ending; the minimum supported version is {min_version}. Please upgrade your database software." } }, "services": { diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 1ef85b28f8d..4d494aed7d5 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -96,6 +96,7 @@ MARIADB_WITH_FIXED_IN_QUERIES_108 = _simple_version("10.8.4") MIN_VERSION_MYSQL = _simple_version("8.0.0") MIN_VERSION_PGSQL = _simple_version("12.0") MIN_VERSION_SQLITE = _simple_version("3.31.0") +UPCOMING_MIN_VERSION_SQLITE = _simple_version("3.40.1") MIN_VERSION_SQLITE_MODERN_BIND_VARS = _simple_version("3.32.0") @@ -356,7 +357,7 @@ def _fail_unsupported_dialect(dialect_name: str) -> NoReturn: raise UnsupportedDialect -def _fail_unsupported_version( +def _raise_if_version_unsupported( server_version: str, dialect_name: str, minimum_version: str ) -> NoReturn: """Warn about unsupported database version.""" @@ -373,16 +374,54 @@ def _fail_unsupported_version( raise UnsupportedDialect +@callback +def _async_delete_issue_deprecated_version( + hass: HomeAssistant, dialect_name: str +) -> None: + """Delete the issue about upcoming unsupported database version.""" + ir.async_delete_issue(hass, DOMAIN, f"{dialect_name}_too_old") + + +@callback +def _async_create_issue_deprecated_version( + hass: HomeAssistant, + server_version: AwesomeVersion, + dialect_name: str, + min_version: AwesomeVersion, +) -> None: + """Warn about upcoming unsupported database version.""" + ir.async_create_issue( + hass, + DOMAIN, + f"{dialect_name}_too_old", + is_fixable=False, + severity=ir.IssueSeverity.CRITICAL, + translation_key=f"{dialect_name}_too_old", + translation_placeholders={ + "server_version": str(server_version), + "min_version": str(min_version), + }, + breaks_in_ha_version="2025.2.0", + ) + + +def _extract_version_from_server_response_or_raise( + server_response: str, +) -> AwesomeVersion: + """Extract version from server response.""" + return AwesomeVersion( + server_response, + ensure_strategy=AwesomeVersionStrategy.SIMPLEVER, + find_first_match=True, + ) + + def _extract_version_from_server_response( server_response: str, ) -> AwesomeVersion | None: """Attempt to extract version from server response.""" try: - return AwesomeVersion( - server_response, - ensure_strategy=AwesomeVersionStrategy.SIMPLEVER, - find_first_match=True, - ) + return _extract_version_from_server_response_or_raise(server_response) except AwesomeVersionException: return None @@ -475,13 +514,27 @@ def setup_connection_for_dialect( # as its persistent and isn't free to call every time. result = query_on_connection(dbapi_connection, "SELECT sqlite_version()") version_string = result[0][0] - version = _extract_version_from_server_response(version_string) + version = _extract_version_from_server_response_or_raise(version_string) - if not version or version < MIN_VERSION_SQLITE: - _fail_unsupported_version( + if version < MIN_VERSION_SQLITE: + _raise_if_version_unsupported( version or version_string, "SQLite", MIN_VERSION_SQLITE ) + # No elif here since _raise_if_version_unsupported raises + if version < UPCOMING_MIN_VERSION_SQLITE: + instance.hass.add_job( + _async_create_issue_deprecated_version, + instance.hass, + version or version_string, + dialect_name, + UPCOMING_MIN_VERSION_SQLITE, + ) + else: + instance.hass.add_job( + _async_delete_issue_deprecated_version, instance.hass, dialect_name + ) + if version and version > MIN_VERSION_SQLITE_MODERN_BIND_VARS: max_bind_vars = SQLITE_MODERN_MAX_BIND_VARS @@ -513,7 +566,7 @@ def setup_connection_for_dialect( if is_maria_db: if not version or version < MIN_VERSION_MARIA_DB: - _fail_unsupported_version( + _raise_if_version_unsupported( version or version_string, "MariaDB", MIN_VERSION_MARIA_DB ) if version and ( @@ -529,7 +582,7 @@ def setup_connection_for_dialect( ) elif not version or version < MIN_VERSION_MYSQL: - _fail_unsupported_version( + _raise_if_version_unsupported( version or version_string, "MySQL", MIN_VERSION_MYSQL ) @@ -551,7 +604,7 @@ def setup_connection_for_dialect( version_string = result[0][0] version = _extract_version_from_server_response(version_string) if not version or version < MIN_VERSION_PGSQL: - _fail_unsupported_version( + _raise_if_version_unsupported( version or version_string, "PostgreSQL", MIN_VERSION_PGSQL ) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 16bf06204e2..04fe762c780 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -26,6 +26,8 @@ from homeassistant.components.recorder.models import ( process_timestamp, ) from homeassistant.components.recorder.util import ( + MIN_VERSION_SQLITE, + UPCOMING_MIN_VERSION_SQLITE, end_incomplete_runs, is_second_sunday, resolve_period, @@ -223,9 +225,9 @@ def test_setup_connection_for_dialect_mysql(mysql_version) -> None: @pytest.mark.parametrize( "sqlite_version", - ["3.31.0"], + [str(UPCOMING_MIN_VERSION_SQLITE)], ) -def test_setup_connection_for_dialect_sqlite(sqlite_version) -> None: +def test_setup_connection_for_dialect_sqlite(sqlite_version: str) -> None: """Test setting up the connection for a sqlite dialect.""" instance_mock = MagicMock() execute_args = [] @@ -276,10 +278,10 @@ def test_setup_connection_for_dialect_sqlite(sqlite_version) -> None: @pytest.mark.parametrize( "sqlite_version", - ["3.31.0"], + [str(UPCOMING_MIN_VERSION_SQLITE)], ) def test_setup_connection_for_dialect_sqlite_zero_commit_interval( - sqlite_version, + sqlite_version: str, ) -> None: """Test setting up the connection for a sqlite dialect with a zero commit interval.""" instance_mock = MagicMock(commit_interval=0) @@ -503,10 +505,6 @@ def test_supported_pgsql(caplog: pytest.LogCaptureFixture, pgsql_version) -> Non "2.0.0", "Version 2.0.0 of SQLite is not supported; minimum supported version is 3.31.0.", ), - ( - "dogs", - "Version dogs of SQLite is not supported; minimum supported version is 3.31.0.", - ), ], ) def test_fail_outdated_sqlite( @@ -725,6 +723,63 @@ async def test_no_issue_for_mariadb_with_MDEV_25020( assert database_engine.optimizer.slow_range_in_select is False +async def test_issue_for_old_sqlite( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test we create and delete an issue for old sqlite versions.""" + instance_mock = MagicMock() + instance_mock.hass = hass + execute_args = [] + close_mock = MagicMock() + min_version = str(MIN_VERSION_SQLITE) + + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SELECT sqlite_version()": + return [[min_version]] + return None + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + database_engine = await hass.async_add_executor_job( + util.setup_connection_for_dialect, + instance_mock, + "sqlite", + dbapi_connection, + True, + ) + await hass.async_block_till_done() + + issue = issue_registry.async_get_issue(DOMAIN, "sqlite_too_old") + assert issue is not None + assert issue.translation_placeholders == { + "min_version": str(UPCOMING_MIN_VERSION_SQLITE), + "server_version": min_version, + } + + min_version = str(UPCOMING_MIN_VERSION_SQLITE) + database_engine = await hass.async_add_executor_job( + util.setup_connection_for_dialect, + instance_mock, + "sqlite", + dbapi_connection, + True, + ) + await hass.async_block_till_done() + + issue = issue_registry.async_get_issue(DOMAIN, "sqlite_too_old") + assert issue is None + assert database_engine is not None + + @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) @pytest.mark.usefixtures("skip_by_db_engine") async def test_basic_sanity_check( From f7f0f490154eebc7ce43779456e89e3f162df0bb Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:36:57 +0200 Subject: [PATCH 1794/2411] Move lifespan attributes into own sensors for legacy Ecovacs bots (#122740) * move available property to base entity class * add lifespan sensors * apply suggestion, simplify the method * don't touch internals in tests * apply suggestion * apply suggestions --- homeassistant/components/ecovacs/const.py | 6 + .../components/ecovacs/controller.py | 10 ++ homeassistant/components/ecovacs/entity.py | 5 + homeassistant/components/ecovacs/sensor.py | 84 +++++++++- homeassistant/components/ecovacs/strings.json | 3 + homeassistant/components/ecovacs/vacuum.py | 6 +- tests/components/ecovacs/conftest.py | 11 +- .../ecovacs/snapshots/test_sensor.ambr | 148 ++++++++++++++++++ tests/components/ecovacs/test_sensor.py | 33 ++++ 9 files changed, 294 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ecovacs/const.py b/homeassistant/components/ecovacs/const.py index 65044c016f9..ac7a268f1bd 100644 --- a/homeassistant/components/ecovacs/const.py +++ b/homeassistant/components/ecovacs/const.py @@ -21,6 +21,12 @@ SUPPORTED_LIFESPANS = ( LifeSpan.ROUND_MOP, ) +LEGACY_SUPPORTED_LIFESPANS = ( + "main_brush", + "side_brush", + "filter", +) + class InstanceMode(StrEnum): """Instance mode.""" diff --git a/homeassistant/components/ecovacs/controller.py b/homeassistant/components/ecovacs/controller.py index 0bef2e8fdd7..c22fb240536 100644 --- a/homeassistant/components/ecovacs/controller.py +++ b/homeassistant/components/ecovacs/controller.py @@ -74,6 +74,8 @@ class EcovacsController: self._authenticator, ) + self._added_legacy_entities: set[str] = set() + async def initialize(self) -> None: """Init controller.""" mqtt_config_verfied = False @@ -117,6 +119,14 @@ class EcovacsController: await self._mqtt.disconnect() await self._authenticator.teardown() + def add_legacy_entity(self, device: VacBot, component: str) -> None: + """Add legacy entity.""" + self._added_legacy_entities.add(f"{device.vacuum['did']}_{component}") + + def legacy_entity_is_added(self, device: VacBot, component: str) -> bool: + """Check if legacy entity is added.""" + return f"{device.vacuum['did']}_{component}" in self._added_legacy_entities + @property def devices(self) -> list[Device]: """Return devices.""" diff --git a/homeassistant/components/ecovacs/entity.py b/homeassistant/components/ecovacs/entity.py index 9d3092f37b4..36103be4d11 100644 --- a/homeassistant/components/ecovacs/entity.py +++ b/homeassistant/components/ecovacs/entity.py @@ -150,6 +150,11 @@ class EcovacsLegacyEntity(Entity): self._event_listeners: list[EventListener] = [] + @property + def available(self) -> bool: + """Return True if the entity is available.""" + return super().available and self.state is not None + async def async_will_remove_from_hass(self) -> None: """Remove event listeners on entity remove.""" for listener in self._event_listeners: diff --git a/homeassistant/components/ecovacs/sensor.py b/homeassistant/components/ecovacs/sensor.py index 256198693fb..28c4efbd0c6 100644 --- a/homeassistant/components/ecovacs/sensor.py +++ b/homeassistant/components/ecovacs/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Generic +from typing import Any, Generic from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan from deebot_client.events import ( @@ -17,6 +17,7 @@ from deebot_client.events import ( StatsEvent, TotalStatsEvent, ) +from sucks import VacBot from homeassistant.components.sensor import ( SensorDeviceClass, @@ -37,11 +38,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import EcovacsConfigEntry -from .const import SUPPORTED_LIFESPANS +from .const import LEGACY_SUPPORTED_LIFESPANS, SUPPORTED_LIFESPANS from .entity import ( EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EcovacsEntity, + EcovacsLegacyEntity, EventT, ) from .util import get_supported_entitites @@ -158,6 +160,25 @@ LIFESPAN_ENTITY_DESCRIPTIONS = tuple( ) +@dataclass(kw_only=True, frozen=True) +class EcovacsLegacyLifespanSensorEntityDescription(SensorEntityDescription): + """Ecovacs lifespan sensor entity description.""" + + component: str + + +LEGACY_LIFESPAN_SENSORS = tuple( + EcovacsLegacyLifespanSensorEntityDescription( + component=component, + key=f"lifespan_{component}", + translation_key=f"lifespan_{component}", + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + ) + for component in LEGACY_SUPPORTED_LIFESPANS +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: EcovacsConfigEntry, @@ -183,6 +204,32 @@ async def async_setup_entry( async_add_entities(entities) + async def _add_legacy_entities() -> None: + entities = [] + for device in controller.legacy_devices: + for description in LEGACY_LIFESPAN_SENSORS: + if ( + description.component in device.components + and not controller.legacy_entity_is_added( + device, description.component + ) + ): + controller.add_legacy_entity(device, description.component) + entities.append(EcovacsLegacyLifespanSensor(device, description)) + + if entities: + async_add_entities(entities) + + def _fire_ecovacs_legacy_lifespan_event(_: Any) -> None: + hass.create_task(_add_legacy_entities()) + + for device in controller.legacy_devices: + config_entry.async_on_unload( + device.lifespanEvents.subscribe( + _fire_ecovacs_legacy_lifespan_event + ).unsubscribe + ) + class EcovacsSensor( EcovacsDescriptionEntity[CapabilityEvent], @@ -253,3 +300,36 @@ class EcovacsErrorSensor( self.async_write_ha_state() self._subscribe(self._capability.event, on_event) + + +class EcovacsLegacyLifespanSensor(EcovacsLegacyEntity, SensorEntity): + """Legacy Lifespan sensor.""" + + entity_description: EcovacsLegacyLifespanSensorEntityDescription + + def __init__( + self, + device: VacBot, + description: EcovacsLegacyLifespanSensorEntityDescription, + ) -> None: + """Initialize the entity.""" + super().__init__(device) + self.entity_description = description + self._attr_unique_id = f"{device.vacuum['did']}_{description.key}" + + if (value := device.components.get(description.component)) is not None: + value = int(value * 100) + self._attr_native_value = value + + async def async_added_to_hass(self) -> None: + """Set up the event listeners now that hass is ready.""" + + def on_event(_: Any) -> None: + if ( + value := self.device.components.get(self.entity_description.component) + ) is not None: + value = int(value * 100) + self._attr_native_value = value + self.schedule_update_ha_state() + + self._event_listeners.append(self.device.lifespanEvents.subscribe(on_event)) diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index d2e385c79c7..ea216f11694 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -119,6 +119,9 @@ "lifespan_lens_brush": { "name": "Lens brush lifespan" }, + "lifespan_main_brush": { + "name": "[%key:component::ecovacs::entity::sensor::lifespan_brush::name%]" + }, "lifespan_side_brush": { "name": "Side brush lifespan" }, diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index d28e632580f..e690038ff71 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -142,11 +142,6 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity): return None - @property - def available(self) -> bool: - """Return True if the vacuum is available.""" - return super().available and self.state is not None - @property def battery_level(self) -> int | None: """Return the battery level of the vacuum cleaner.""" @@ -173,6 +168,7 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity): data: dict[str, Any] = {} data[ATTR_ERROR] = self.error + # these attributes are deprecated and can be removed in 2025.2 for key, val in self.device.components.items(): attr_name = ATTR_COMPONENT_PREFIX + key data[attr_name] = int(val * 100) diff --git a/tests/components/ecovacs/conftest.py b/tests/components/ecovacs/conftest.py index e53cfcc8a3d..22039d6c0bc 100644 --- a/tests/components/ecovacs/conftest.py +++ b/tests/components/ecovacs/conftest.py @@ -10,6 +10,7 @@ from deebot_client.device import Device from deebot_client.exceptions import ApiError from deebot_client.models import Credentials import pytest +from sucks import EventEmitter from homeassistant.components.ecovacs import PLATFORMS from homeassistant.components.ecovacs.const import DOMAIN @@ -128,10 +129,10 @@ def mock_vacbot(device_fixture: str) -> Generator[Mock]: vacbot.vacuum = load_json_object_fixture( f"devices/{device_fixture}/device.json", DOMAIN ) - vacbot.statusEvents = Mock() - vacbot.batteryEvents = Mock() - vacbot.lifespanEvents = Mock() - vacbot.errorEvents = Mock() + vacbot.statusEvents = EventEmitter() + vacbot.batteryEvents = EventEmitter() + vacbot.lifespanEvents = EventEmitter() + vacbot.errorEvents = EventEmitter() vacbot.battery_status = None vacbot.fan_speed = None vacbot.components = {} @@ -175,7 +176,7 @@ async def init_integration( mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) yield mock_config_entry diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index 07ebd400870..659edfde2cf 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -1,4 +1,152 @@ # serializer version: 1 +# name: test_legacy_sensors[123][sensor.e1234567890000000003_filter_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.e1234567890000000003_filter_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Filter lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifespan_filter', + 'unique_id': 'E1234567890000000003_lifespan_filter', + 'unit_of_measurement': '%', + }) +# --- +# name: test_legacy_sensors[123][sensor.e1234567890000000003_filter_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'E1234567890000000003 Filter lifespan', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.e1234567890000000003_filter_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_legacy_sensors[123][sensor.e1234567890000000003_main_brush_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.e1234567890000000003_main_brush_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Main brush lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifespan_main_brush', + 'unique_id': 'E1234567890000000003_lifespan_main_brush', + 'unit_of_measurement': '%', + }) +# --- +# name: test_legacy_sensors[123][sensor.e1234567890000000003_main_brush_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'E1234567890000000003 Main brush lifespan', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.e1234567890000000003_main_brush_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) +# --- +# name: test_legacy_sensors[123][sensor.e1234567890000000003_side_brush_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.e1234567890000000003_side_brush_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Side brush lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifespan_side_brush', + 'unique_id': 'E1234567890000000003_lifespan_side_brush', + 'unit_of_measurement': '%', + }) +# --- +# name: test_legacy_sensors[123][sensor.e1234567890000000003_side_brush_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'E1234567890000000003 Side brush lifespan', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.e1234567890000000003_side_brush_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_legacy_sensors[123][states] + list([ + 'sensor.e1234567890000000003_main_brush_lifespan', + 'sensor.e1234567890000000003_side_brush_lifespan', + 'sensor.e1234567890000000003_filter_lifespan', + ]) +# --- # name: test_sensors[5xu9h3][sensor.goat_g1_area_cleaned:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/ecovacs/test_sensor.py b/tests/components/ecovacs/test_sensor.py index 19b4c8ce09b..53c57999776 100644 --- a/tests/components/ecovacs/test_sensor.py +++ b/tests/components/ecovacs/test_sensor.py @@ -1,5 +1,7 @@ """Tests for Ecovacs sensors.""" +from unittest.mock import Mock + from deebot_client.event_bus import EventBus from deebot_client.events import ( BatteryEvent, @@ -152,3 +154,34 @@ async def test_disabled_by_default_sensors( ), f"Entity registry entry for {entity_id} is missing" assert entry.disabled assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + +@pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "mock_vacbot", "init_integration" +) +@pytest.mark.parametrize(("device_fixture"), ["123"]) +async def test_legacy_sensors( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_vacbot: Mock, +) -> None: + """Test that sensor entity snapshots match.""" + mock_vacbot.components = {"main_brush": 0.8, "side_brush": 0.6, "filter": 0.4} + mock_vacbot.lifespanEvents.notify("dummy_data") + await hass.async_block_till_done(wait_background_tasks=True) + + states = hass.states.async_entity_ids() + assert snapshot(name="states") == states + + for entity_id in hass.states.async_entity_ids(): + assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" + assert snapshot(name=f"{entity_id}:state") == state + + assert (entity_entry := entity_registry.async_get(state.entity_id)) + assert snapshot(name=f"{entity_id}:entity-registry") == entity_entry + + assert entity_entry.device_id + assert (device_entry := device_registry.async_get(entity_entry.device_id)) + assert device_entry.identifiers == {(DOMAIN, "E1234567890000000003")} From 97de1c2b665b48033211ec2d9b3da9cf90e38143 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:46:13 +0200 Subject: [PATCH 1795/2411] Fix implicit-return in recorder (#122924) --- homeassistant/components/recorder/pool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index dcb19ddf044..0fa0e82e98b 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -71,7 +71,8 @@ class RecorderPool(SingletonThreadPool, NullPool): def _do_return_conn(self, record: ConnectionPoolEntry) -> None: if threading.get_ident() in self.recorder_and_worker_thread_ids: - return super()._do_return_conn(record) + super()._do_return_conn(record) + return record.close() def shutdown(self) -> None: From 3df78043c05ca7268e7e9ade8f2ce988f6e6ae0c Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 31 Jul 2024 07:13:05 -0700 Subject: [PATCH 1796/2411] Add enable_millisecond to duration selector (#122821) * Add enable_milliseconds to duration selector. * One more test --- homeassistant/helpers/selector.py | 3 +++ tests/helpers/test_selector.py | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 5a542657d10..025b8de8896 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -725,6 +725,7 @@ class DurationSelectorConfig(TypedDict, total=False): """Class to represent a duration selector config.""" enable_day: bool + enable_millisecond: bool allow_negative: bool @@ -739,6 +740,8 @@ class DurationSelector(Selector[DurationSelectorConfig]): # Enable day field in frontend. A selection with `days` set is allowed # even if `enable_day` is not set vol.Optional("enable_day"): cv.boolean, + # Enable millisecond field in frontend. + vol.Optional("enable_millisecond"): cv.boolean, # Allow negative durations. Will default to False in HA Core 2025.6.0. vol.Optional("allow_negative"): cv.boolean, } diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index e93ec3b8c22..de8c3555831 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -739,12 +739,13 @@ def test_attribute_selector_schema( ( {"seconds": 10}, {"days": 10}, # Days is allowed also if `enable_day` is not set + {"milliseconds": 500}, ), (None, {}), ), ( - {"enable_day": True}, - ({"seconds": 10}, {"days": 10}), + {"enable_day": True, "enable_millisecond": True}, + ({"seconds": 10}, {"days": 10}, {"milliseconds": 500}), (None, {}), ), ( From f76470562936dc4ce650346e79f60a49a61214c6 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:23:27 +0200 Subject: [PATCH 1797/2411] Add support for ventilation device to ViCare (#114175) * add ventilation program & mode * add ventilation device * Update climate.py * Update climate.py * Update climate.py * Update climate.py * Update climate.py * Update const.py * Create fan.py * Update fan.py * Update types.py * add test case * add translation key * use translation key * update snapshot * fix ruff findings * fix ruff findings * add log messages to setter * adjust test case * reset climate entity * do not display speed if not in permanent mode * update snapshot * update test cases * add comment * mark fan as always on * prevent turning off device * allow to set permanent mode * make speed_count static * add debug outputs * add preset state translations * allow permanent mode * update snapshot * add test case * load programs only on init * comment on ventilation modes * adjust test cases * add exception message * ignore test coverage on fan.py * Update test_fan.py * simplify * Apply suggestions from code review * remove tests * remove extra state attributes * fix leftover * add missing labels * adjust label * change state keys * use _attr_preset_modes * fix ruff findings * fix attribute access * fix from_ha_mode * fix ruff findings * fix mypy findings * simplify * format * fix typo * fix ruff finding * Apply suggestions from code review * change fan mode handling * add test cases * remove turn_off * Apply suggestions from code review Co-authored-by: Erik Montnemery * Apply suggestions from code review * Update fan.py --------- Co-authored-by: Erik Montnemery --- homeassistant/components/vicare/const.py | 1 + homeassistant/components/vicare/fan.py | 124 +++++++++++++++++++ homeassistant/components/vicare/strings.json | 15 +++ homeassistant/components/vicare/types.py | 49 ++++++++ tests/components/vicare/test_types.py | 87 +++++++++++++ 5 files changed, 276 insertions(+) create mode 100644 homeassistant/components/vicare/fan.py create mode 100644 tests/components/vicare/test_types.py diff --git a/homeassistant/components/vicare/const.py b/homeassistant/components/vicare/const.py index 286dcbbdfa8..8f8ae3c94e3 100644 --- a/homeassistant/components/vicare/const.py +++ b/homeassistant/components/vicare/const.py @@ -10,6 +10,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, + Platform.FAN, Platform.NUMBER, Platform.SENSOR, Platform.WATER_HEATER, diff --git a/homeassistant/components/vicare/fan.py b/homeassistant/components/vicare/fan.py new file mode 100644 index 00000000000..088e54c7354 --- /dev/null +++ b/homeassistant/components/vicare/fan.py @@ -0,0 +1,124 @@ +"""Viessmann ViCare ventilation device.""" + +from __future__ import annotations + +from contextlib import suppress +import logging + +from PyViCare.PyViCareDevice import Device as PyViCareDevice +from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig +from PyViCare.PyViCareUtils import ( + PyViCareInvalidDataError, + PyViCareNotSupportedFeatureError, + PyViCareRateLimitError, +) +from PyViCare.PyViCareVentilationDevice import ( + VentilationDevice as PyViCareVentilationDevice, +) +from requests.exceptions import ConnectionError as RequestConnectionError + +from homeassistant.components.fan import FanEntity, FanEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, +) + +from .const import DEVICE_LIST, DOMAIN +from .entity import ViCareEntity +from .types import VentilationMode, VentilationProgram + +_LOGGER = logging.getLogger(__name__) + +ORDERED_NAMED_FAN_SPEEDS = [ + VentilationProgram.LEVEL_ONE, + VentilationProgram.LEVEL_TWO, + VentilationProgram.LEVEL_THREE, + VentilationProgram.LEVEL_FOUR, +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the ViCare fan platform.""" + + device_list = hass.data[DOMAIN][config_entry.entry_id][DEVICE_LIST] + + async_add_entities( + [ + ViCareFan(device.config, device.api) + for device in device_list + if isinstance(device.api, PyViCareVentilationDevice) + ] + ) + + +class ViCareFan(ViCareEntity, FanEntity): + """Representation of the ViCare ventilation device.""" + + _attr_preset_modes = list[str]( + [ + VentilationMode.PERMANENT, + VentilationMode.VENTILATION, + VentilationMode.SENSOR_DRIVEN, + VentilationMode.SENSOR_OVERRIDE, + ] + ) + _attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS) + _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE + _attr_translation_key = "ventilation" + _enable_turn_on_off_backwards_compatibility = False + + def __init__( + self, + device_config: PyViCareDeviceConfig, + device: PyViCareDevice, + ) -> None: + """Initialize the fan entity.""" + super().__init__(device_config, device, self._attr_translation_key) + + def update(self) -> None: + """Update state of fan.""" + try: + with suppress(PyViCareNotSupportedFeatureError): + self._attr_preset_mode = VentilationMode.from_vicare_mode( + self._api.getActiveMode() + ) + with suppress(PyViCareNotSupportedFeatureError): + self._attr_percentage = ordered_list_item_to_percentage( + ORDERED_NAMED_FAN_SPEEDS, self._api.getActiveProgram() + ) + except RequestConnectionError: + _LOGGER.error("Unable to retrieve data from ViCare server") + except ValueError: + _LOGGER.error("Unable to decode data from ViCare server") + except PyViCareRateLimitError as limit_exception: + _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) + except PyViCareInvalidDataError as invalid_data_exception: + _LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) + + @property + def is_on(self) -> bool | None: + """Return true if the entity is on.""" + # Viessmann ventilation unit cannot be turned off + return True + + def set_percentage(self, percentage: int) -> None: + """Set the speed of the fan, as a percentage.""" + if self._attr_preset_mode != str(VentilationMode.PERMANENT): + self.set_preset_mode(VentilationMode.PERMANENT) + + level = percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage) + _LOGGER.debug("changing ventilation level to %s", level) + self._api.setPermanentLevel(level) + + def set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + target_mode = VentilationMode.to_vicare_mode(preset_mode) + _LOGGER.debug("changing ventilation mode to %s", target_mode) + self._api.setActiveMode(target_mode) diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index de92d0ec271..7c0088d065f 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -65,6 +65,21 @@ "name": "Heating" } }, + "fan": { + "ventilation": { + "name": "Ventilation", + "state_attributes": { + "preset_mode": { + "state": { + "permanent": "permanent", + "ventilation": "schedule", + "sensor_driven": "sensor", + "sensor_override": "schedule with sensor-override" + } + } + } + } + }, "number": { "heating_curve_shift": { "name": "Heating curve shift" diff --git a/homeassistant/components/vicare/types.py b/homeassistant/components/vicare/types.py index 7e1ec7f8bee..596605fccdd 100644 --- a/homeassistant/components/vicare/types.py +++ b/homeassistant/components/vicare/types.py @@ -64,6 +64,55 @@ VICARE_TO_HA_PRESET_HEATING = { } +class VentilationMode(enum.StrEnum): + """ViCare ventilation modes.""" + + PERMANENT = "permanent" # on, speed controlled by program (levelOne-levelFour) + VENTILATION = "ventilation" # activated by schedule + SENSOR_DRIVEN = "sensor_driven" # activated by schedule, override by sensor + SENSOR_OVERRIDE = "sensor_override" # activated by sensor + + @staticmethod + def to_vicare_mode(mode: str | None) -> str | None: + """Return the mapped ViCare ventilation mode for the Home Assistant mode.""" + if mode: + try: + ventilation_mode = VentilationMode(mode) + except ValueError: + # ignore unsupported / unmapped modes + return None + return HA_TO_VICARE_MODE_VENTILATION.get(ventilation_mode) if mode else None + return None + + @staticmethod + def from_vicare_mode(vicare_mode: str | None) -> str | None: + """Return the mapped Home Assistant mode for the ViCare ventilation mode.""" + for mode in VentilationMode: + if HA_TO_VICARE_MODE_VENTILATION.get(VentilationMode(mode)) == vicare_mode: + return mode + return None + + +HA_TO_VICARE_MODE_VENTILATION = { + VentilationMode.PERMANENT: "permanent", + VentilationMode.VENTILATION: "ventilation", + VentilationMode.SENSOR_DRIVEN: "sensorDriven", + VentilationMode.SENSOR_OVERRIDE: "sensorOverride", +} + + +class VentilationProgram(enum.StrEnum): + """ViCare preset ventilation programs. + + As listed in https://github.com/somm15/PyViCare/blob/6c5b023ca6c8bb2d38141dd1746dc1705ec84ce8/PyViCare/PyViCareVentilationDevice.py#L37 + """ + + LEVEL_ONE = "levelOne" + LEVEL_TWO = "levelTwo" + LEVEL_THREE = "levelThree" + LEVEL_FOUR = "levelFour" + + @dataclass(frozen=True) class ViCareDevice: """Dataclass holding the device api and config.""" diff --git a/tests/components/vicare/test_types.py b/tests/components/vicare/test_types.py new file mode 100644 index 00000000000..575e549f0d9 --- /dev/null +++ b/tests/components/vicare/test_types.py @@ -0,0 +1,87 @@ +"""Test ViCare diagnostics.""" + +import pytest + +from homeassistant.components.climate import PRESET_COMFORT, PRESET_SLEEP +from homeassistant.components.vicare.types import HeatingProgram, VentilationMode + + +@pytest.mark.parametrize( + ("vicare_program", "expected_result"), + [ + ("", None), + (None, None), + ("anything", None), + (HeatingProgram.COMFORT, PRESET_COMFORT), + (HeatingProgram.COMFORT_HEATING, PRESET_COMFORT), + ], +) +async def test_heating_program_to_ha_preset( + vicare_program: str | None, + expected_result: str | None, +) -> None: + """Testing ViCare HeatingProgram to HA Preset.""" + + assert HeatingProgram.to_ha_preset(vicare_program) == expected_result + + +@pytest.mark.parametrize( + ("ha_preset", "expected_result"), + [ + ("", None), + (None, None), + ("anything", None), + (PRESET_SLEEP, HeatingProgram.REDUCED), + ], +) +async def test_ha_preset_to_heating_program( + ha_preset: str | None, + expected_result: str | None, +) -> None: + """Testing HA Preset tp ViCare HeatingProgram.""" + + supported_programs = [ + HeatingProgram.COMFORT, + HeatingProgram.ECO, + HeatingProgram.NORMAL, + HeatingProgram.REDUCED, + ] + assert ( + HeatingProgram.from_ha_preset(ha_preset, supported_programs) == expected_result + ) + + +@pytest.mark.parametrize( + ("vicare_mode", "expected_result"), + [ + ("", None), + (None, None), + ("anything", None), + ("sensorOverride", VentilationMode.SENSOR_OVERRIDE), + ], +) +async def test_ventilation_mode_to_ha_mode( + vicare_mode: str | None, + expected_result: str | None, +) -> None: + """Testing ViCare mode to VentilationMode.""" + + assert VentilationMode.from_vicare_mode(vicare_mode) == expected_result + + +@pytest.mark.parametrize( + ("ha_mode", "expected_result"), + [ + ("", None), + (None, None), + ("anything", None), + (VentilationMode.SENSOR_OVERRIDE, "sensorOverride"), + ], +) +async def test_ha_mode_to_ventilation_mode( + ha_mode: str | None, + expected_result: str | None, +) -> None: + """Testing VentilationMode to ViCare mode.""" + + assert VentilationMode.to_vicare_mode(ha_mode) == expected_result From 8c0d9a13203980d17027518427fe10dce2ee249c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 31 Jul 2024 17:04:09 +0200 Subject: [PATCH 1798/2411] Add Reolink chime support (#122752) --- homeassistant/components/reolink/__init__.py | 16 +- .../components/reolink/binary_sensor.py | 16 +- homeassistant/components/reolink/button.py | 8 +- homeassistant/components/reolink/entity.py | 44 ++++- homeassistant/components/reolink/icons.json | 12 ++ homeassistant/components/reolink/number.py | 75 +++++++- homeassistant/components/reolink/select.py | 100 ++++++++++- homeassistant/components/reolink/sensor.py | 18 +- homeassistant/components/reolink/strings.json | 51 ++++++ homeassistant/components/reolink/switch.py | 79 ++++++++- homeassistant/components/reolink/update.py | 8 +- tests/components/reolink/test_init.py | 9 + tests/components/reolink/test_select.py | 167 ++++++++++++++++++ 13 files changed, 549 insertions(+), 54 deletions(-) create mode 100644 tests/components/reolink/test_select.py diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 2077b4a5e29..cc293d970b2 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -186,7 +186,7 @@ async def async_remove_config_entry_device( ) -> bool: """Remove a device from a config entry.""" host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host - (device_uid, ch) = get_device_uid_and_ch(device, host) + (device_uid, ch, is_chime) = get_device_uid_and_ch(device, host) if not host.api.is_nvr or ch is None: _LOGGER.warning( @@ -227,20 +227,24 @@ async def async_remove_config_entry_device( def get_device_uid_and_ch( device: dr.DeviceEntry, host: ReolinkHost -) -> tuple[list[str], int | None]: +) -> tuple[list[str], int | None, bool]: """Get the channel and the split device_uid from a reolink DeviceEntry.""" device_uid = [ dev_id[1].split("_") for dev_id in device.identifiers if dev_id[0] == DOMAIN ][0] + is_chime = False if len(device_uid) < 2: # NVR itself ch = None elif device_uid[1].startswith("ch") and len(device_uid[1]) <= 5: ch = int(device_uid[1][2:]) + elif device_uid[1].startswith("chime"): + ch = int(device_uid[1][5:]) + is_chime = True else: ch = host.api.channel_for_uid(device_uid[1]) - return (device_uid, ch) + return (device_uid, ch, is_chime) def migrate_entity_ids( @@ -251,7 +255,7 @@ def migrate_entity_ids( devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) ch_device_ids = {} for device in devices: - (device_uid, ch) = get_device_uid_and_ch(device, host) + (device_uid, ch, is_chime) = get_device_uid_and_ch(device, host) if host.api.supported(None, "UID") and device_uid[0] != host.unique_id: if ch is None: @@ -261,8 +265,8 @@ def migrate_entity_ids( new_identifiers = {(DOMAIN, new_device_id)} device_reg.async_update_device(device.id, new_identifiers=new_identifiers) - if ch is None: - continue # Do not consider the NVR itself + if ch is None or is_chime: + continue # Do not consider the NVR itself or chimes ch_device_ids[device.id] = ch if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid(ch): diff --git a/homeassistant/components/reolink/binary_sensor.py b/homeassistant/components/reolink/binary_sensor.py index d19987c3bc6..70c21849bc2 100644 --- a/homeassistant/components/reolink/binary_sensor.py +++ b/homeassistant/components/reolink/binary_sensor.py @@ -117,18 +117,14 @@ async def async_setup_entry( entities: list[ReolinkBinarySensorEntity] = [] for channel in reolink_data.host.api.channels: entities.extend( - [ - ReolinkPushBinarySensorEntity(reolink_data, channel, entity_description) - for entity_description in BINARY_PUSH_SENSORS - if entity_description.supported(reolink_data.host.api, channel) - ] + ReolinkPushBinarySensorEntity(reolink_data, channel, entity_description) + for entity_description in BINARY_PUSH_SENSORS + if entity_description.supported(reolink_data.host.api, channel) ) entities.extend( - [ - ReolinkBinarySensorEntity(reolink_data, channel, entity_description) - for entity_description in BINARY_SENSORS - if entity_description.supported(reolink_data.host.api, channel) - ] + ReolinkBinarySensorEntity(reolink_data, channel, entity_description) + for entity_description in BINARY_SENSORS + if entity_description.supported(reolink_data.host.api, channel) ) async_add_entities(entities) diff --git a/homeassistant/components/reolink/button.py b/homeassistant/components/reolink/button.py index 528807920d3..eba0570a3fb 100644 --- a/homeassistant/components/reolink/button.py +++ b/homeassistant/components/reolink/button.py @@ -164,11 +164,9 @@ async def async_setup_entry( if entity_description.supported(reolink_data.host.api, channel) ] entities.extend( - [ - ReolinkHostButtonEntity(reolink_data, entity_description) - for entity_description in HOST_BUTTON_ENTITIES - if entity_description.supported(reolink_data.host.api) - ] + ReolinkHostButtonEntity(reolink_data, entity_description) + for entity_description in HOST_BUTTON_ENTITIES + if entity_description.supported(reolink_data.host.api) ) async_add_entities(entities) diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index c07983175ae..053792ad667 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from reolink_aio.api import DUAL_LENS_MODELS, Host +from reolink_aio.api import DUAL_LENS_MODELS, Chime, Host from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity import EntityDescription @@ -59,8 +59,9 @@ class ReolinkHostCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[None] http_s = "https" if self._host.api.use_https else "http" self._conf_url = f"{http_s}://{self._host.api.host}:{self._host.api.port}" + self._dev_id = self._host.unique_id self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._host.unique_id)}, + identifiers={(DOMAIN, self._dev_id)}, connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)}, name=self._host.api.nvr_name, model=self._host.api.model, @@ -126,12 +127,14 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity): if self._host.api.is_nvr: if self._host.api.supported(dev_ch, "UID"): - dev_id = f"{self._host.unique_id}_{self._host.api.camera_uid(dev_ch)}" + self._dev_id = ( + f"{self._host.unique_id}_{self._host.api.camera_uid(dev_ch)}" + ) else: - dev_id = f"{self._host.unique_id}_ch{dev_ch}" + self._dev_id = f"{self._host.unique_id}_ch{dev_ch}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, dev_id)}, + identifiers={(DOMAIN, self._dev_id)}, via_device=(DOMAIN, self._host.unique_id), name=self._host.api.camera_name(dev_ch), model=self._host.api.camera_model(dev_ch), @@ -156,3 +159,34 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity): self._host.async_unregister_update_cmd(cmd_key, self._channel) await super().async_will_remove_from_hass() + + +class ReolinkChimeCoordinatorEntity(ReolinkChannelCoordinatorEntity): + """Parent class for Reolink chime entities connected.""" + + def __init__( + self, + reolink_data: ReolinkData, + chime: Chime, + coordinator: DataUpdateCoordinator[None] | None = None, + ) -> None: + """Initialize ReolinkChimeCoordinatorEntity for a chime.""" + super().__init__(reolink_data, chime.channel, coordinator) + + self._chime = chime + + self._attr_unique_id = ( + f"{self._host.unique_id}_chime{chime.dev_id}_{self.entity_description.key}" + ) + cam_dev_id = self._dev_id + self._dev_id = f"{self._host.unique_id}_chime{chime.dev_id}" + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._dev_id)}, + via_device=(DOMAIN, cam_dev_id), + name=chime.name, + model="Reolink Chime", + manufacturer=self._host.api.manufacturer, + serial_number=str(chime.dev_id), + configuration_url=self._conf_url, + ) diff --git a/homeassistant/components/reolink/icons.json b/homeassistant/components/reolink/icons.json index 539c2461204..7ca4c2d7f2b 100644 --- a/homeassistant/components/reolink/icons.json +++ b/homeassistant/components/reolink/icons.json @@ -206,6 +206,15 @@ }, "hdr": { "default": "mdi:hdr" + }, + "motion_tone": { + "default": "mdi:music-note" + }, + "people_tone": { + "default": "mdi:music-note" + }, + "visitor_tone": { + "default": "mdi:music-note" } }, "sensor": { @@ -284,6 +293,9 @@ }, "pir_reduce_alarm": { "default": "mdi:motion-sensor" + }, + "led": { + "default": "mdi:lightning-bolt-circle" } } }, diff --git a/homeassistant/components/reolink/number.py b/homeassistant/components/reolink/number.py index a4ea89c5b26..1dc99c886e1 100644 --- a/homeassistant/components/reolink/number.py +++ b/homeassistant/components/reolink/number.py @@ -6,7 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Any -from reolink_aio.api import Host +from reolink_aio.api import Chime, Host from reolink_aio.exceptions import InvalidParameterError, ReolinkError from homeassistant.components.number import ( @@ -22,7 +22,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ReolinkData from .const import DOMAIN -from .entity import ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription +from .entity import ( + ReolinkChannelCoordinatorEntity, + ReolinkChannelEntityDescription, + ReolinkChimeCoordinatorEntity, +) @dataclass(frozen=True, kw_only=True) @@ -39,6 +43,18 @@ class ReolinkNumberEntityDescription( value: Callable[[Host, int], float | None] +@dataclass(frozen=True, kw_only=True) +class ReolinkChimeNumberEntityDescription( + NumberEntityDescription, + ReolinkChannelEntityDescription, +): + """A class that describes number entities for a chime.""" + + method: Callable[[Chime, float], Any] + mode: NumberMode = NumberMode.AUTO + value: Callable[[Chime], float | None] + + NUMBER_ENTITIES = ( ReolinkNumberEntityDescription( key="zoom", @@ -459,6 +475,20 @@ NUMBER_ENTITIES = ( ), ) +CHIME_NUMBER_ENTITIES = ( + ReolinkChimeNumberEntityDescription( + key="volume", + cmd_key="DingDongOpt", + translation_key="volume", + entity_category=EntityCategory.CONFIG, + native_step=1, + native_min_value=0, + native_max_value=4, + value=lambda chime: chime.volume, + method=lambda chime, value: chime.set_option(volume=int(value)), + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -468,12 +498,18 @@ async def async_setup_entry( """Set up a Reolink number entities.""" reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( + entities: list[ReolinkNumberEntity | ReolinkChimeNumberEntity] = [ ReolinkNumberEntity(reolink_data, channel, entity_description) for entity_description in NUMBER_ENTITIES for channel in reolink_data.host.api.channels if entity_description.supported(reolink_data.host.api, channel) + ] + entities.extend( + ReolinkChimeNumberEntity(reolink_data, chime, entity_description) + for entity_description in CHIME_NUMBER_ENTITIES + for chime in reolink_data.host.api.chime_list ) + async_add_entities(entities) class ReolinkNumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity): @@ -515,3 +551,36 @@ class ReolinkNumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity): except ReolinkError as err: raise HomeAssistantError(err) from err self.async_write_ha_state() + + +class ReolinkChimeNumberEntity(ReolinkChimeCoordinatorEntity, NumberEntity): + """Base number entity class for Reolink IP cameras.""" + + entity_description: ReolinkChimeNumberEntityDescription + + def __init__( + self, + reolink_data: ReolinkData, + chime: Chime, + entity_description: ReolinkChimeNumberEntityDescription, + ) -> None: + """Initialize Reolink chime number entity.""" + self.entity_description = entity_description + super().__init__(reolink_data, chime) + + self._attr_mode = entity_description.mode + + @property + def native_value(self) -> float | None: + """State of the number entity.""" + return self.entity_description.value(self._chime) + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + try: + await self.entity_description.method(self._chime, value) + except InvalidParameterError as err: + raise ServiceValidationError(err) from err + except ReolinkError as err: + raise HomeAssistantError(err) from err + self.async_write_ha_state() diff --git a/homeassistant/components/reolink/select.py b/homeassistant/components/reolink/select.py index cf32d7b45f9..94cfdf6751b 100644 --- a/homeassistant/components/reolink/select.py +++ b/homeassistant/components/reolink/select.py @@ -8,6 +8,8 @@ import logging from typing import Any from reolink_aio.api import ( + Chime, + ChimeToneEnum, DayNightEnum, HDREnum, Host, @@ -26,7 +28,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ReolinkData from .const import DOMAIN -from .entity import ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription +from .entity import ( + ReolinkChannelCoordinatorEntity, + ReolinkChannelEntityDescription, + ReolinkChimeCoordinatorEntity, +) _LOGGER = logging.getLogger(__name__) @@ -43,6 +49,18 @@ class ReolinkSelectEntityDescription( value: Callable[[Host, int], str] | None = None +@dataclass(frozen=True, kw_only=True) +class ReolinkChimeSelectEntityDescription( + SelectEntityDescription, + ReolinkChannelEntityDescription, +): + """A class that describes select entities for a chime.""" + + get_options: list[str] + method: Callable[[Chime, str], Any] + value: Callable[[Chime], str] + + def _get_quick_reply_id(api: Host, ch: int, mess: str) -> int: """Get the quick reply file id from the message string.""" return [k for k, v in api.quick_reply_dict(ch).items() if v == mess][0] @@ -132,6 +150,36 @@ SELECT_ENTITIES = ( ), ) +CHIME_SELECT_ENTITIES = ( + ReolinkChimeSelectEntityDescription( + key="motion_tone", + cmd_key="GetDingDongCfg", + translation_key="motion_tone", + entity_category=EntityCategory.CONFIG, + get_options=[method.name for method in ChimeToneEnum], + value=lambda chime: ChimeToneEnum(chime.tone("md")).name, + method=lambda chime, name: chime.set_tone("md", ChimeToneEnum[name].value), + ), + ReolinkChimeSelectEntityDescription( + key="people_tone", + cmd_key="GetDingDongCfg", + translation_key="people_tone", + entity_category=EntityCategory.CONFIG, + get_options=[method.name for method in ChimeToneEnum], + value=lambda chime: ChimeToneEnum(chime.tone("people")).name, + method=lambda chime, name: chime.set_tone("people", ChimeToneEnum[name].value), + ), + ReolinkChimeSelectEntityDescription( + key="visitor_tone", + cmd_key="GetDingDongCfg", + translation_key="visitor_tone", + entity_category=EntityCategory.CONFIG, + get_options=[method.name for method in ChimeToneEnum], + value=lambda chime: ChimeToneEnum(chime.tone("visitor")).name, + method=lambda chime, name: chime.set_tone("visitor", ChimeToneEnum[name].value), + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -141,12 +189,18 @@ async def async_setup_entry( """Set up a Reolink select entities.""" reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( + entities: list[ReolinkSelectEntity | ReolinkChimeSelectEntity] = [ ReolinkSelectEntity(reolink_data, channel, entity_description) for entity_description in SELECT_ENTITIES for channel in reolink_data.host.api.channels if entity_description.supported(reolink_data.host.api, channel) + ] + entities.extend( + ReolinkChimeSelectEntity(reolink_data, chime, entity_description) + for entity_description in CHIME_SELECT_ENTITIES + for chime in reolink_data.host.api.chime_list ) + async_add_entities(entities) class ReolinkSelectEntity(ReolinkChannelCoordinatorEntity, SelectEntity): @@ -196,3 +250,45 @@ class ReolinkSelectEntity(ReolinkChannelCoordinatorEntity, SelectEntity): except ReolinkError as err: raise HomeAssistantError(err) from err self.async_write_ha_state() + + +class ReolinkChimeSelectEntity(ReolinkChimeCoordinatorEntity, SelectEntity): + """Base select entity class for Reolink IP cameras.""" + + entity_description: ReolinkChimeSelectEntityDescription + + def __init__( + self, + reolink_data: ReolinkData, + chime: Chime, + entity_description: ReolinkChimeSelectEntityDescription, + ) -> None: + """Initialize Reolink select entity for a chime.""" + self.entity_description = entity_description + super().__init__(reolink_data, chime) + self._log_error = True + self._attr_options = entity_description.get_options + + @property + def current_option(self) -> str | None: + """Return the current option.""" + try: + option = self.entity_description.value(self._chime) + except ValueError: + if self._log_error: + _LOGGER.exception("Reolink '%s' has an unknown value", self.name) + self._log_error = False + return None + + self._log_error = True + return option + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + try: + await self.entity_description.method(self._chime, option) + except InvalidParameterError as err: + raise ServiceValidationError(err) from err + except ReolinkError as err: + raise HomeAssistantError(err) from err + self.async_write_ha_state() diff --git a/homeassistant/components/reolink/sensor.py b/homeassistant/components/reolink/sensor.py index 419270a7082..988b091735e 100644 --- a/homeassistant/components/reolink/sensor.py +++ b/homeassistant/components/reolink/sensor.py @@ -141,19 +141,15 @@ async def async_setup_entry( if entity_description.supported(reolink_data.host.api, channel) ] entities.extend( - [ - ReolinkHostSensorEntity(reolink_data, entity_description) - for entity_description in HOST_SENSORS - if entity_description.supported(reolink_data.host.api) - ] + ReolinkHostSensorEntity(reolink_data, entity_description) + for entity_description in HOST_SENSORS + if entity_description.supported(reolink_data.host.api) ) entities.extend( - [ - ReolinkHddSensorEntity(reolink_data, hdd_index, entity_description) - for entity_description in HDD_SENSORS - for hdd_index in reolink_data.host.api.hdd_list - if entity_description.supported(reolink_data.host.api, hdd_index) - ] + ReolinkHddSensorEntity(reolink_data, hdd_index, entity_description) + for entity_description in HDD_SENSORS + for hdd_index in reolink_data.host.api.hdd_list + if entity_description.supported(reolink_data.host.api, hdd_index) ) async_add_entities(entities) diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index bcf1c71934d..cad09f71562 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -491,6 +491,54 @@ "on": "[%key:common::state::on%]", "auto": "Auto" } + }, + "motion_tone": { + "name": "Motion ringtone", + "state": { + "off": "[%key:common::state::off%]", + "citybird": "City bird", + "originaltune": "Original tune", + "pianokey": "Piano key", + "loop": "Loop", + "attraction": "Attraction", + "hophop": "Hop hop", + "goodday": "Good day", + "operetta": "Operetta", + "moonlight": "Moonlight", + "waybackhome": "Way back home" + } + }, + "people_tone": { + "name": "Person ringtone", + "state": { + "off": "[%key:common::state::off%]", + "citybird": "[%key:component::reolink::entity::select::motion_tone::state::citybird%]", + "originaltune": "[%key:component::reolink::entity::select::motion_tone::state::originaltune%]", + "pianokey": "[%key:component::reolink::entity::select::motion_tone::state::pianokey%]", + "loop": "[%key:component::reolink::entity::select::motion_tone::state::loop%]", + "attraction": "[%key:component::reolink::entity::select::motion_tone::state::attraction%]", + "hophop": "[%key:component::reolink::entity::select::motion_tone::state::hophop%]", + "goodday": "[%key:component::reolink::entity::select::motion_tone::state::goodday%]", + "operetta": "[%key:component::reolink::entity::select::motion_tone::state::operetta%]", + "moonlight": "[%key:component::reolink::entity::select::motion_tone::state::moonlight%]", + "waybackhome": "[%key:component::reolink::entity::select::motion_tone::state::waybackhome%]" + } + }, + "visitor_tone": { + "name": "Visitor ringtone", + "state": { + "off": "[%key:common::state::off%]", + "citybird": "[%key:component::reolink::entity::select::motion_tone::state::citybird%]", + "originaltune": "[%key:component::reolink::entity::select::motion_tone::state::originaltune%]", + "pianokey": "[%key:component::reolink::entity::select::motion_tone::state::pianokey%]", + "loop": "[%key:component::reolink::entity::select::motion_tone::state::loop%]", + "attraction": "[%key:component::reolink::entity::select::motion_tone::state::attraction%]", + "hophop": "[%key:component::reolink::entity::select::motion_tone::state::hophop%]", + "goodday": "[%key:component::reolink::entity::select::motion_tone::state::goodday%]", + "operetta": "[%key:component::reolink::entity::select::motion_tone::state::operetta%]", + "moonlight": "[%key:component::reolink::entity::select::motion_tone::state::moonlight%]", + "waybackhome": "[%key:component::reolink::entity::select::motion_tone::state::waybackhome%]" + } } }, "sensor": { @@ -574,6 +622,9 @@ }, "pir_reduce_alarm": { "name": "PIR reduce false alarm" + }, + "led": { + "name": "LED" } } } diff --git a/homeassistant/components/reolink/switch.py b/homeassistant/components/reolink/switch.py index cd74d774bb1..2bf7689b32f 100644 --- a/homeassistant/components/reolink/switch.py +++ b/homeassistant/components/reolink/switch.py @@ -6,7 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Any -from reolink_aio.api import Host +from reolink_aio.api import Chime, Host from reolink_aio.exceptions import ReolinkError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription @@ -22,6 +22,7 @@ from .const import DOMAIN from .entity import ( ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription, + ReolinkChimeCoordinatorEntity, ReolinkHostCoordinatorEntity, ReolinkHostEntityDescription, ) @@ -49,6 +50,17 @@ class ReolinkNVRSwitchEntityDescription( value: Callable[[Host], bool] +@dataclass(frozen=True, kw_only=True) +class ReolinkChimeSwitchEntityDescription( + SwitchEntityDescription, + ReolinkChannelEntityDescription, +): + """A class that describes switch entities for a chime.""" + + method: Callable[[Chime, bool], Any] + value: Callable[[Chime], bool | None] + + SWITCH_ENTITIES = ( ReolinkSwitchEntityDescription( key="ir_lights", @@ -245,6 +257,17 @@ NVR_SWITCH_ENTITIES = ( ), ) +CHIME_SWITCH_ENTITIES = ( + ReolinkChimeSwitchEntityDescription( + key="chime_led", + cmd_key="DingDongOpt", + translation_key="led", + entity_category=EntityCategory.CONFIG, + value=lambda chime: chime.led_state, + method=lambda chime, value: chime.set_option(led=value), + ), +) + # Can be removed in HA 2025.2.0 DEPRECATED_HDR = ReolinkSwitchEntityDescription( key="hdr", @@ -266,18 +289,23 @@ async def async_setup_entry( """Set up a Reolink switch entities.""" reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] - entities: list[ReolinkSwitchEntity | ReolinkNVRSwitchEntity] = [ + entities: list[ + ReolinkSwitchEntity | ReolinkNVRSwitchEntity | ReolinkChimeSwitchEntity + ] = [ ReolinkSwitchEntity(reolink_data, channel, entity_description) for entity_description in SWITCH_ENTITIES for channel in reolink_data.host.api.channels if entity_description.supported(reolink_data.host.api, channel) ] entities.extend( - [ - ReolinkNVRSwitchEntity(reolink_data, entity_description) - for entity_description in NVR_SWITCH_ENTITIES - if entity_description.supported(reolink_data.host.api) - ] + ReolinkNVRSwitchEntity(reolink_data, entity_description) + for entity_description in NVR_SWITCH_ENTITIES + if entity_description.supported(reolink_data.host.api) + ) + entities.extend( + ReolinkChimeSwitchEntity(reolink_data, chime, entity_description) + for entity_description in CHIME_SWITCH_ENTITIES + for chime in reolink_data.host.api.chime_list ) # Can be removed in HA 2025.2.0 @@ -378,3 +406,40 @@ class ReolinkNVRSwitchEntity(ReolinkHostCoordinatorEntity, SwitchEntity): except ReolinkError as err: raise HomeAssistantError(err) from err self.async_write_ha_state() + + +class ReolinkChimeSwitchEntity(ReolinkChimeCoordinatorEntity, SwitchEntity): + """Base switch entity class for a chime.""" + + entity_description: ReolinkChimeSwitchEntityDescription + + def __init__( + self, + reolink_data: ReolinkData, + chime: Chime, + entity_description: ReolinkChimeSwitchEntityDescription, + ) -> None: + """Initialize Reolink switch entity.""" + self.entity_description = entity_description + super().__init__(reolink_data, chime) + + @property + def is_on(self) -> bool | None: + """Return true if switch is on.""" + return self.entity_description.value(self._chime) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + try: + await self.entity_description.method(self._chime, True) + except ReolinkError as err: + raise HomeAssistantError(err) from err + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + try: + await self.entity_description.method(self._chime, False) + except ReolinkError as err: + raise HomeAssistantError(err) from err + self.async_write_ha_state() diff --git a/homeassistant/components/reolink/update.py b/homeassistant/components/reolink/update.py index da3dafe0130..9b710c6576d 100644 --- a/homeassistant/components/reolink/update.py +++ b/homeassistant/components/reolink/update.py @@ -81,11 +81,9 @@ async def async_setup_entry( if entity_description.supported(reolink_data.host.api, channel) ] entities.extend( - [ - ReolinkHostUpdateEntity(reolink_data, entity_description) - for entity_description in HOST_UPDATE_ENTITIES - if entity_description.supported(reolink_data.host.api) - ] + ReolinkHostUpdateEntity(reolink_data, entity_description) + for entity_description in HOST_UPDATE_ENTITIES + if entity_description.supported(reolink_data.host.api) ) async_add_entities(entities) diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 85ce5d94657..4f745530b6b 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -282,6 +282,15 @@ async def test_removing_disconnected_cams( True, False, ), + ( + f"{TEST_MAC}_chime123456789_play_ringtone", + f"{TEST_UID}_chime123456789_play_ringtone", + f"{TEST_MAC}_chime123456789", + f"{TEST_UID}_chime123456789", + Platform.SELECT, + True, + False, + ), ( f"{TEST_MAC}_0_record_audio", f"{TEST_MAC}_{TEST_UID_CAM}_record_audio", diff --git a/tests/components/reolink/test_select.py b/tests/components/reolink/test_select.py new file mode 100644 index 00000000000..908c06dc16f --- /dev/null +++ b/tests/components/reolink/test_select.py @@ -0,0 +1,167 @@ +"""Test the Reolink select platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from reolink_aio.api import Chime +from reolink_aio.exceptions import InvalidParameterError, ReolinkError + +from homeassistant.components.reolink import DEVICE_UPDATE_INTERVAL +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_SELECT_OPTION, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from .conftest import TEST_NVR_NAME + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_floodlight_mode_select( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test select entity with floodlight_mode.""" + reolink_connect.whiteled_mode.return_value = 1 + reolink_connect.whiteled_mode_list.return_value = ["off", "auto"] + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]): + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + entity_id = f"{Platform.SELECT}.{TEST_NVR_NAME}_floodlight_mode" + assert hass.states.is_state(entity_id, "auto") + + reolink_connect.set_whiteled = AsyncMock() + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, "option": "off"}, + blocking=True, + ) + reolink_connect.set_whiteled.assert_called_once() + + reolink_connect.set_whiteled = AsyncMock(side_effect=ReolinkError("Test error")) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, "option": "off"}, + blocking=True, + ) + + reolink_connect.set_whiteled = AsyncMock( + side_effect=InvalidParameterError("Test error") + ) + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, "option": "off"}, + blocking=True, + ) + + +async def test_play_quick_reply_message( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test select play_quick_reply_message entity.""" + reolink_connect.quick_reply_dict.return_value = {0: "off", 1: "test message"} + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]): + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + entity_id = f"{Platform.SELECT}.{TEST_NVR_NAME}_play_quick_reply_message" + assert hass.states.is_state(entity_id, STATE_UNKNOWN) + + reolink_connect.play_quick_reply = AsyncMock() + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, "option": "test message"}, + blocking=True, + ) + reolink_connect.play_quick_reply.assert_called_once() + + +async def test_chime_select( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test chime select entity.""" + TEST_CHIME = Chime( + host=reolink_connect, + dev_id=12345678, + channel=0, + name="Test chime", + event_info={ + "md": {"switch": 0, "musicId": 0}, + "people": {"switch": 0, "musicId": 1}, + "visitor": {"switch": 1, "musicId": 2}, + }, + ) + TEST_CHIME.volume = 3 + TEST_CHIME.led_state = True + + reolink_connect.chime_list = [TEST_CHIME] + + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]): + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + entity_id = f"{Platform.SELECT}.test_chime_visitor_ringtone" + assert hass.states.is_state(entity_id, "pianokey") + + TEST_CHIME.set_tone = AsyncMock() + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, "option": "off"}, + blocking=True, + ) + TEST_CHIME.set_tone.assert_called_once() + + TEST_CHIME.set_tone = AsyncMock(side_effect=ReolinkError("Test error")) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, "option": "off"}, + blocking=True, + ) + + TEST_CHIME.set_tone = AsyncMock(side_effect=InvalidParameterError("Test error")) + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, "option": "off"}, + blocking=True, + ) + + TEST_CHIME.event_info = {} + async_fire_time_changed( + hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30) + ) + await hass.async_block_till_done() + + assert hass.states.is_state(entity_id, STATE_UNKNOWN) From a1b8545568ae54ade7a3e45d61644afdb25414d8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:13:53 +0200 Subject: [PATCH 1799/2411] Fix unnecessary-return-none in nest (#122951) --- homeassistant/components/nest/media_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 6c481806e4f..71501e72552 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -228,7 +228,7 @@ class NestEventMediaStore(EventMediaStore): def remove_media(filename: str) -> None: if not os.path.exists(filename): - return None + return _LOGGER.debug("Removing event media from disk store: %s", filename) os.remove(filename) From c359d4a419e7ee4133fff96732930aa8a598095b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 31 Jul 2024 17:53:52 +0200 Subject: [PATCH 1800/2411] Update frontend to 20240731.0 (#122956) --- 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 d7253b52b28..60cfa0a26ff 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240719.0"] + "requirements": ["home-assistant-frontend==20240731.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c52ccfa6a8c..c7e5528d675 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240719.0 +home-assistant-frontend==20240731.0 home-assistant-intents==2024.7.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3c8d5357568..472270c1dcf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240719.0 +home-assistant-frontend==20240731.0 # homeassistant.components.conversation home-assistant-intents==2024.7.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b95d1b58922..b2021a6bc7f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -913,7 +913,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240719.0 +home-assistant-frontend==20240731.0 # homeassistant.components.conversation home-assistant-intents==2024.7.29 From 69f54656c44b9de30777abdce925cd0bb88be76a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:58:11 +0200 Subject: [PATCH 1801/2411] Fix cleanup of orphan device entries in AVM Fritz!Box Tools (#122937) * fix cleanup of orphan device entries * add test for cleanup button --- homeassistant/components/fritz/coordinator.py | 4 +- tests/components/fritz/test_button.py | 74 +++++++++++++++++-- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/fritz/coordinator.py b/homeassistant/components/fritz/coordinator.py index 8a55084d7ef..592bf37084e 100644 --- a/homeassistant/components/fritz/coordinator.py +++ b/homeassistant/components/fritz/coordinator.py @@ -666,7 +666,9 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): entity_reg.async_remove(entity.entity_id) device_reg = dr.async_get(self.hass) - orphan_connections = {(CONNECTION_NETWORK_MAC, mac) for mac in orphan_macs} + orphan_connections = { + (CONNECTION_NETWORK_MAC, dr.format_mac(mac)) for mac in orphan_macs + } for device in dr.async_entries_for_config_entry( device_reg, config_entry.entry_id ): diff --git a/tests/components/fritz/test_button.py b/tests/components/fritz/test_button.py index 8666491eb7a..79639835003 100644 --- a/tests/components/fritz/test_button.py +++ b/tests/components/fritz/test_button.py @@ -1,6 +1,6 @@ """Tests for Fritz!Tools button platform.""" -import copy +from copy import deepcopy from datetime import timedelta from unittest.mock import patch @@ -11,9 +11,15 @@ from homeassistant.components.fritz.const import DOMAIN, MeshRoles from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow -from .const import MOCK_MESH_DATA, MOCK_NEW_DEVICE_NODE, MOCK_USER_DATA +from .const import ( + MOCK_HOST_ATTRIBUTES_DATA, + MOCK_MESH_DATA, + MOCK_NEW_DEVICE_NODE, + MOCK_USER_DATA, +) from tests.common import MockConfigEntry, async_fire_time_changed @@ -120,7 +126,7 @@ async def test_wol_button_new_device( entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) entry.add_to_hass(hass) - mesh_data = copy.deepcopy(MOCK_MESH_DATA) + mesh_data = deepcopy(MOCK_MESH_DATA) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED @@ -148,7 +154,7 @@ async def test_wol_button_absent_for_mesh_slave( entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) entry.add_to_hass(hass) - slave_mesh_data = copy.deepcopy(MOCK_MESH_DATA) + slave_mesh_data = deepcopy(MOCK_MESH_DATA) slave_mesh_data["nodes"][0]["mesh_role"] = MeshRoles.SLAVE fh_class_mock.get_mesh_topology.return_value = slave_mesh_data @@ -170,7 +176,7 @@ async def test_wol_button_absent_for_non_lan_device( entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) entry.add_to_hass(hass) - printer_wifi_data = copy.deepcopy(MOCK_MESH_DATA) + printer_wifi_data = deepcopy(MOCK_MESH_DATA) # initialization logic uses the connection type of the `node_interface_1_uid` pair of the printer # ni-230 is wifi interface of fritzbox printer_node_interface = printer_wifi_data["nodes"][1]["node_interfaces"][0] @@ -184,3 +190,61 @@ async def test_wol_button_absent_for_non_lan_device( button = hass.states.get("button.printer_wake_on_lan") assert button is None + + +async def test_cleanup_button( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + fc_class_mock, + fh_class_mock, +) -> None: + """Test cleanup of orphan devices.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED + + # check if tracked device is registered properly + device = device_registry.async_get_device( + connections={("mac", "aa:bb:cc:00:11:22")} + ) + assert device + + entities = [ + entity + for entity in er.async_entries_for_config_entry(entity_registry, entry.entry_id) + if entity.unique_id.startswith("AA:BB:CC:00:11:22") + ] + assert entities + assert len(entities) == 3 + + # removed tracked device and trigger cleanup + host_attributes = deepcopy(MOCK_HOST_ATTRIBUTES_DATA) + host_attributes.pop(0) + fh_class_mock.get_hosts_attributes.return_value = host_attributes + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.mock_title_cleanup"}, + blocking=True, + ) + + await hass.async_block_till_done(wait_background_tasks=True) + + # check if orphan tracked device is removed + device = device_registry.async_get_device( + connections={("mac", "aa:bb:cc:00:11:22")} + ) + assert not device + + entities = [ + entity + for entity in er.async_entries_for_config_entry(entity_registry, entry.entry_id) + if entity.unique_id.startswith("AA:BB:CC:00:11:22") + ] + assert not entities From 172e2125f6c228c5225318d743cfef946f35b048 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 31 Jul 2024 11:17:45 -0500 Subject: [PATCH 1802/2411] Switch to using update for headers middleware (#122952) --- homeassistant/components/http/headers.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/http/headers.py b/homeassistant/components/http/headers.py index 3c845601183..ebc0594e15a 100644 --- a/homeassistant/components/http/headers.py +++ b/homeassistant/components/http/headers.py @@ -31,13 +31,10 @@ def setup_headers(app: Application, use_x_frame_options: bool) -> None: try: response = await handler(request) except HTTPException as err: - for key, value in added_headers.items(): - err.headers[key] = value + err.headers.update(added_headers) raise - for key, value in added_headers.items(): - response.headers[key] = value - + response.headers.update(added_headers) return response app.middlewares.append(headers_middleware) From c888908cc86b04aa3b5aa82c06edc44c1d3af43d Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 31 Jul 2024 18:23:40 +0200 Subject: [PATCH 1803/2411] Add default warning for installing matter device updates (#122597) --- homeassistant/components/matter/update.py | 55 ++++++++++++++++++----- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/matter/update.py b/homeassistant/components/matter/update.py index 4e6733db045..736664e0101 100644 --- a/homeassistant/components/matter/update.py +++ b/homeassistant/components/matter/update.py @@ -8,7 +8,7 @@ from typing import Any from chip.clusters import Objects as clusters from matter_server.common.errors import UpdateCheckError, UpdateError -from matter_server.common.models import MatterSoftwareVersion +from matter_server.common.models import MatterSoftwareVersion, UpdateSource from homeassistant.components.update import ( ATTR_LATEST_VERSION, @@ -18,7 +18,7 @@ from homeassistant.components.update import ( UpdateEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import STATE_ON, Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -76,6 +76,12 @@ class MatterUpdate(MatterEntity, UpdateEntity): _attr_should_poll = True _software_update: MatterSoftwareVersion | None = None _cancel_update: CALLBACK_TYPE | None = None + _attr_supported_features = ( + UpdateEntityFeature.INSTALL + | UpdateEntityFeature.PROGRESS + | UpdateEntityFeature.SPECIFIC_VERSION + | UpdateEntityFeature.RELEASE_NOTES + ) @callback def _update_from_device(self) -> None: @@ -84,16 +90,6 @@ class MatterUpdate(MatterEntity, UpdateEntity): self._attr_installed_version = self.get_matter_attribute_value( clusters.BasicInformation.Attributes.SoftwareVersionString ) - - if self.get_matter_attribute_value( - clusters.OtaSoftwareUpdateRequestor.Attributes.UpdatePossible - ): - self._attr_supported_features = ( - UpdateEntityFeature.INSTALL - | UpdateEntityFeature.PROGRESS - | UpdateEntityFeature.SPECIFIC_VERSION - ) - update_state: clusters.OtaSoftwareUpdateRequestor.Enums.UpdateStateEnum = ( self.get_matter_attribute_value( clusters.OtaSoftwareUpdateRequestor.Attributes.UpdateState @@ -133,9 +129,39 @@ class MatterUpdate(MatterEntity, UpdateEntity): self._software_update = update_information self._attr_latest_version = update_information.software_version_string self._attr_release_url = update_information.release_notes_url + except UpdateCheckError as err: raise HomeAssistantError(f"Error finding applicable update: {err}") from err + async def async_release_notes(self) -> str | None: + """Return full release notes. + + This is suitable for a long changelog that does not fit in the release_summary + property. The returned string can contain markdown. + """ + if self._software_update is None: + return None + if self.state != STATE_ON: + return None + + release_notes = "" + + # insert extra heavy warning case the update is not from the main net + if self._software_update.update_source != UpdateSource.MAIN_NET_DCL: + release_notes += ( + "\n\n" + f"Update provided by {self._software_update.update_source.value}. " + "Installing this update is at your own risk and you may run into unexpected " + "problems such as the need to re-add and factory reset your device.\n\n" + ) + return release_notes + ( + "\n\nThe update process can take a while, " + "especially for battery powered devices. Please be patient and wait until the update " + "process is fully completed. Do not remove power from the device while it's updating. " + "The device may restart during the update process and be unavailable for several minutes." + "\n\n" + ) + async def async_added_to_hass(self) -> None: """Call when the entity is added to hass.""" await super().async_added_to_hass() @@ -172,6 +198,11 @@ class MatterUpdate(MatterEntity, UpdateEntity): ) -> None: """Install a new software version.""" + if not self.get_matter_attribute_value( + clusters.OtaSoftwareUpdateRequestor.Attributes.UpdatePossible + ): + raise HomeAssistantError("Device is not ready to install updates") + software_version: str | int | None = version if self._software_update is not None and ( version is None or version == self._software_update.software_version_string From 3f091470fd7ef580e3bd565ed87571962d3552b5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 18:28:35 +0200 Subject: [PATCH 1804/2411] Use pytest.mark.usefixtures in risco tests (#122955) --- tests/components/risco/test_sensor.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py index 72444bdc9f2..4c8f7bb4180 100644 --- a/tests/components/risco/test_sensor.py +++ b/tests/components/risco/test_sensor.py @@ -174,11 +174,10 @@ def save_mock(): @pytest.mark.parametrize("events", [TEST_EVENTS]) +@pytest.mark.usefixtures("two_zone_cloud", "_set_utc_time_zone") async def test_cloud_setup( hass: HomeAssistant, entity_registry: er.EntityRegistry, - two_zone_cloud, - _set_utc_time_zone, save_mock, setup_risco_cloud, ) -> None: @@ -207,11 +206,9 @@ async def test_cloud_setup( _check_state(hass, category, entity_id) +@pytest.mark.usefixtures("setup_risco_local", "_no_zones_and_partitions") async def test_local_setup( - hass: HomeAssistant, entity_registry: er.EntityRegistry, - setup_risco_local, - _no_zones_and_partitions, ) -> None: """Test entity setup.""" for entity_id in ENTITY_IDS.values(): From 69a8c5dc9fb18178eb3091d0451150cf85073be5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 18:44:36 +0200 Subject: [PATCH 1805/2411] Fix implicit-return in hddtemp (#122919) --- homeassistant/components/hddtemp/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index 836e68abe9f..fbb6a6b48f9 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -6,6 +6,7 @@ from datetime import timedelta import logging import socket from telnetlib import Telnet # pylint: disable=deprecated-module +from typing import Any import voluptuous as vol @@ -82,10 +83,11 @@ class HddTempSensor(SensorEntity): self._details = None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the sensor.""" if self._details is not None: return {ATTR_DEVICE: self._details[0], ATTR_MODEL: self._details[1]} + return None def update(self) -> None: """Get the latest data from HDDTemp daemon and updates the state.""" From ae9e8ca419413481f0e245e16cfbe304c1a2ac24 Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Wed, 31 Jul 2024 19:04:17 +0200 Subject: [PATCH 1806/2411] Simplify async_setup_entry in bluesound integration (#122874) * Use async_added_to_hass and async_will_remove_from_hass * Remove self._hass --- .../components/bluesound/__init__.py | 11 -- .../components/bluesound/media_player.py | 143 ++++-------------- 2 files changed, 29 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/bluesound/__init__.py b/homeassistant/components/bluesound/__init__.py index 0912a584fce..cbe95fc3abf 100644 --- a/homeassistant/components/bluesound/__init__.py +++ b/homeassistant/components/bluesound/__init__.py @@ -67,15 +67,4 @@ async def async_setup_entry( async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - player = None - for player in hass.data[DOMAIN]: - if player.unique_id == config_entry.unique_id: - break - - if player is None: - return False - - player.stop_polling() - hass.data[DOMAIN].remove(player) - return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index b320566c74a..809ba293f89 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -3,8 +3,7 @@ from __future__ import annotations import asyncio -from asyncio import CancelledError -from collections.abc import Callable +from asyncio import CancelledError, Task from contextlib import suppress from datetime import datetime, timedelta import logging @@ -31,15 +30,11 @@ from homeassistant.const import ( CONF_HOSTS, CONF_NAME, CONF_PORT, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import ( DOMAIN as HOMEASSISTANT_DOMAIN, - Event, HomeAssistant, ServiceCall, - callback, ) from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import ServiceValidationError @@ -50,7 +45,6 @@ from homeassistant.helpers.device_registry import ( format_mac, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -123,59 +117,6 @@ SERVICE_TO_METHOD = { } -def _add_player( - hass: HomeAssistant, - async_add_entities: AddEntitiesCallback, - host: str, - port: int, - player: Player, - sync_status: SyncStatus, -): - """Add Bluesound players.""" - - @callback - def _init_bluesound_player(event: Event | None = None): - """Start polling.""" - hass.async_create_task(bluesound_player.async_init()) - - @callback - def _start_polling(event: Event | None = None): - """Start polling.""" - bluesound_player.start_polling() - - @callback - def _stop_polling(event: Event | None = None): - """Stop polling.""" - bluesound_player.stop_polling() - - @callback - def _add_bluesound_player_cb(): - """Add player after first sync fetch.""" - if bluesound_player.id in [x.id for x in hass.data[DATA_BLUESOUND]]: - _LOGGER.warning("Player already added %s", bluesound_player.id) - return - - hass.data[DATA_BLUESOUND].append(bluesound_player) - async_add_entities([bluesound_player]) - _LOGGER.debug("Added device with name: %s", bluesound_player.name) - - if hass.is_running: - _start_polling() - else: - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _start_polling) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_polling) - - bluesound_player = BluesoundPlayer( - hass, host, port, player, sync_status, _add_bluesound_player_cb - ) - - if hass.is_running: - _init_bluesound_player() - else: - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_bluesound_player) - - async def _async_import(hass: HomeAssistant, config: ConfigType) -> None: """Import config entry from configuration.yaml.""" if not hass.config_entries.async_entries(DOMAIN): @@ -252,18 +193,16 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Bluesound entry.""" - host = config_entry.data[CONF_HOST] - port = config_entry.data[CONF_PORT] - - _add_player( - hass, - async_add_entities, - host, - port, + bluesound_player = BluesoundPlayer( + config_entry.data[CONF_HOST], + config_entry.data[CONF_PORT], config_entry.runtime_data.player, config_entry.runtime_data.sync_status, ) + hass.data[DATA_BLUESOUND].append(bluesound_player) + async_add_entities([bluesound_player]) + async def async_setup_platform( hass: HomeAssistant, @@ -290,36 +229,30 @@ class BluesoundPlayer(MediaPlayerEntity): def __init__( self, - hass: HomeAssistant, host: str, port: int, player: Player, sync_status: SyncStatus, - init_callback: Callable[[], None], ) -> None: """Initialize the media player.""" self.host = host - self._hass = hass self.port = port - self._polling_task = None # The actual polling task. - self._id = None + self._polling_task: Task[None] | None = None # The actual polling task. + self._id = sync_status.id self._last_status_update = None - self._sync_status: SyncStatus | None = None + self._sync_status = sync_status self._status: Status | None = None self._inputs: list[Input] = [] self._presets: list[Preset] = [] self._is_online = False - self._retry_remove = None self._muted = False self._master: BluesoundPlayer | None = None self._is_master = False self._group_name = None self._group_list: list[str] = [] - self._bluesound_device_name = None + self._bluesound_device_name = sync_status.name self._player = player - self._init_callback = init_callback - self._attr_unique_id = format_unique_id(sync_status.mac, port) # there should always be one player with the default port per mac if port is DEFAULT_PORT: @@ -349,23 +282,18 @@ class BluesoundPlayer(MediaPlayerEntity): except ValueError: return -1 - async def force_update_sync_status(self, on_updated_cb=None) -> bool: + async def force_update_sync_status(self) -> bool: """Update the internal status.""" sync_status = await self._player.sync_status() self._sync_status = sync_status - if not self._id: - self._id = sync_status.id - if not self._bluesound_device_name: - self._bluesound_device_name = sync_status.name - if sync_status.master is not None: self._is_master = False master_id = f"{sync_status.master.ip}:{sync_status.master.port}" master_device = [ device - for device in self._hass.data[DATA_BLUESOUND] + for device in self.hass.data[DATA_BLUESOUND] if device.id == master_id ] @@ -380,8 +308,6 @@ class BluesoundPlayer(MediaPlayerEntity): slaves = self._sync_status.slaves self._is_master = slaves is not None - if on_updated_cb: - on_updated_cb() return True async def _start_poll_command(self): @@ -401,32 +327,21 @@ class BluesoundPlayer(MediaPlayerEntity): _LOGGER.exception("Unexpected error in %s:%s", self.name, self.port) raise - def start_polling(self): + async def async_added_to_hass(self) -> None: """Start the polling task.""" - self._polling_task = self._hass.async_create_task(self._start_poll_command()) + await super().async_added_to_hass() - def stop_polling(self): + self._polling_task = self.hass.async_create_task(self._start_poll_command()) + + async def async_will_remove_from_hass(self) -> None: """Stop the polling task.""" - self._polling_task.cancel() + await super().async_will_remove_from_hass() - async def async_init(self, triggered=None): - """Initialize the player async.""" - try: - if self._retry_remove is not None: - self._retry_remove() - self._retry_remove = None + assert self._polling_task is not None + if self._polling_task.cancel(): + await self._polling_task - await self.force_update_sync_status(self._init_callback) - except (TimeoutError, ClientError): - _LOGGER.error("Node %s:%s is offline, retrying later", self.host, self.port) - self._retry_remove = async_track_time_interval( - self._hass, self.async_init, NODE_RETRY_INITIATION - ) - except Exception: - _LOGGER.exception( - "Unexpected when initiating error in %s:%s", self.host, self.port - ) - raise + self.hass.data[DATA_BLUESOUND].remove(self) async def async_update(self) -> None: """Update internal status of the entity.""" @@ -490,13 +405,13 @@ class BluesoundPlayer(MediaPlayerEntity): """Trigger sync status update on all devices.""" _LOGGER.debug("Trigger sync status on all devices") - for player in self._hass.data[DATA_BLUESOUND]: + for player in self.hass.data[DATA_BLUESOUND]: await player.force_update_sync_status() @Throttle(SYNC_STATUS_INTERVAL) - async def async_update_sync_status(self, on_updated_cb=None): + async def async_update_sync_status(self): """Update sync status.""" - await self.force_update_sync_status(on_updated_cb) + await self.force_update_sync_status() @Throttle(UPDATE_CAPTURE_INTERVAL) async def async_update_captures(self) -> list[Input] | None: @@ -615,7 +530,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._status is not None: volume = self._status.volume - if self.is_grouped and self._sync_status is not None: + if self.is_grouped: volume = self._sync_status.volume if volume is None: @@ -630,7 +545,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._status is not None: mute = self._status.mute - if self.is_grouped and self._sync_status is not None: + if self.is_grouped: mute = self._sync_status.mute_volume is not None return mute @@ -778,7 +693,7 @@ class BluesoundPlayer(MediaPlayerEntity): device_group = self._group_name.split("+") sorted_entities = sorted( - self._hass.data[DATA_BLUESOUND], + self.hass.data[DATA_BLUESOUND], key=lambda entity: entity.is_master, reverse=True, ) From a913587eb6dff0d5c9a5846d4067ef6974d3e405 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 31 Jul 2024 19:17:53 +0200 Subject: [PATCH 1807/2411] Climate validate temperature(s) out of range (#118649) * Climate temperature out of range * Fix test sensibo * use temp converting for min/max * Fix * Fix mqtt tests * Fix honeywell tests * Fix Balboa tests * Fix whirlpool test * Fix teslemetry test * Fix plugwise test * Fix tplink test * Fix generic thermostat test * Fix modbus test * Fix fritzbox tests * Honewell --- homeassistant/components/climate/__init__.py | 29 +++- homeassistant/components/climate/strings.json | 3 + homeassistant/components/honeywell/climate.py | 9 +- tests/components/balboa/test_climate.py | 2 + tests/components/climate/test_init.py | 138 +++++++++++++++++- tests/components/fritzbox/test_climate.py | 8 +- .../generic_thermostat/test_climate.py | 4 +- tests/components/honeywell/conftest.py | 1 + .../honeywell/snapshots/test_climate.ambr | 6 +- tests/components/honeywell/test_climate.py | 71 +++++---- tests/components/modbus/test_climate.py | 8 +- tests/components/mqtt/test_climate.py | 14 +- tests/components/plugwise/test_climate.py | 4 +- tests/components/sensibo/test_climate.py | 6 +- tests/components/teslemetry/test_climate.py | 2 +- tests/components/tplink/test_climate.py | 5 +- tests/components/whirlpool/test_climate.py | 4 +- 17 files changed, 245 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 94cba54b247..f546ae0e671 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -914,12 +914,37 @@ async def async_service_temperature_set( """Handle set temperature service.""" hass = entity.hass kwargs = {} + min_temp = entity.min_temp + max_temp = entity.max_temp + temp_unit = entity.temperature_unit for value, temp in service_call.data.items(): if value in CONVERTIBLE_ATTRIBUTE: - kwargs[value] = TemperatureConverter.convert( - temp, hass.config.units.temperature_unit, entity.temperature_unit + kwargs[value] = check_temp = TemperatureConverter.convert( + temp, hass.config.units.temperature_unit, temp_unit ) + + _LOGGER.debug( + "Check valid temperature %d %s (%d %s) in range %d %s - %d %s", + check_temp, + entity.temperature_unit, + temp, + hass.config.units.temperature_unit, + min_temp, + temp_unit, + max_temp, + temp_unit, + ) + if check_temp < min_temp or check_temp > max_temp: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="temp_out_of_range", + translation_placeholders={ + "check_temp": str(check_temp), + "min_temp": str(min_temp), + "max_temp": str(max_temp), + }, + ) else: kwargs[value] = temp diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index dc212441824..1af21815b9f 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -266,6 +266,9 @@ }, "not_valid_fan_mode": { "message": "Fan mode {mode} is not valid. Valid fan modes are: {modes}." + }, + "temp_out_of_range": { + "message": "Provided temperature {check_temp} is not valid. Accepted range is {min_temp} to {max_temp}." } } } diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index d9260fc3be5..141cb87f117 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -42,6 +42,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.unit_conversion import TemperatureConverter from . import HoneywellData from .const import ( @@ -259,7 +260,9 @@ class HoneywellUSThermostat(ClimateEntity): self._device.raw_ui_data["HeatLowerSetptLimit"], ] ) - return DEFAULT_MIN_TEMP + return TemperatureConverter.convert( + DEFAULT_MIN_TEMP, UnitOfTemperature.CELSIUS, self.temperature_unit + ) @property def max_temp(self) -> float: @@ -275,7 +278,9 @@ class HoneywellUSThermostat(ClimateEntity): self._device.raw_ui_data["HeatUpperSetptLimit"], ] ) - return DEFAULT_MAX_TEMP + return TemperatureConverter.convert( + DEFAULT_MAX_TEMP, UnitOfTemperature.CELSIUS, self.temperature_unit + ) @property def current_humidity(self) -> int | None: diff --git a/tests/components/balboa/test_climate.py b/tests/components/balboa/test_climate.py index c877f2858cd..850184a7d71 100644 --- a/tests/components/balboa/test_climate.py +++ b/tests/components/balboa/test_climate.py @@ -85,6 +85,8 @@ async def test_spa_temperature( hass: HomeAssistant, client: MagicMock, integration: MockConfigEntry ) -> None: """Test spa temperature settings.""" + client.temperature_minimum = 110 + client.temperature_maximum = 250 # flip the spa into F # set temp to a valid number state = await _patch_spa_settemp(hass, client, 0, 100) diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index ced75ff7ef7..f306551e540 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -4,6 +4,7 @@ from __future__ import annotations from enum import Enum from types import ModuleType +from typing import Any from unittest.mock import MagicMock, Mock, patch import pytest @@ -17,9 +18,14 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.components.climate.const import ( + ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, ATTR_PRESET_MODE, ATTR_SWING_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, SERVICE_SET_FAN_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE, @@ -27,7 +33,13 @@ from homeassistant.components.climate.const import ( ClimateEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, UnitOfTemperature +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_WHOLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import issue_registry as ir @@ -1152,3 +1164,127 @@ async def test_no_issue_no_aux_property( "the auxiliary heater methods in a subclass of ClimateEntity which is deprecated " "and will be unsupported from Home Assistant 2024.10." ) not in caplog.text + + +async def test_temperature_validation( + hass: HomeAssistant, config_flow_fixture: None +) -> None: + """Test validation for temperatures.""" + + class MockClimateEntityTemp(MockClimateEntity): + """Mock climate class with mocked aux heater.""" + + _attr_supported_features = ( + ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.SWING_MODE + | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) + _attr_target_temperature = 15 + _attr_target_temperature_high = 18 + _attr_target_temperature_low = 10 + _attr_target_temperature_step = PRECISION_WHOLE + + def set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + if ATTR_TEMPERATURE in kwargs: + self._attr_target_temperature = kwargs[ATTR_TEMPERATURE] + if ATTR_TARGET_TEMP_HIGH in kwargs: + self._attr_target_temperature_high = kwargs[ATTR_TARGET_TEMP_HIGH] + self._attr_target_temperature_low = kwargs[ATTR_TARGET_TEMP_LOW] + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) + return True + + async def async_setup_entry_climate_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test climate platform via config entry.""" + async_add_entities( + [MockClimateEntityTemp(name="test", entity_id="climate.test")] + ) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=async_setup_entry_init, + ), + built_in=False, + ) + mock_platform( + hass, + "test.climate", + MockPlatform(async_setup_entry=async_setup_entry_climate_platform), + ) + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("climate.test") + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) is None + assert state.attributes.get(ATTR_MIN_TEMP) == 7 + assert state.attributes.get(ATTR_MAX_TEMP) == 35 + + with pytest.raises( + ServiceValidationError, + match="Provided temperature 40.0 is not valid. Accepted range is 7 to 35", + ) as exc: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + "entity_id": "climate.test", + ATTR_TEMPERATURE: "40", + }, + blocking=True, + ) + assert ( + str(exc.value) + == "Provided temperature 40.0 is not valid. Accepted range is 7 to 35" + ) + assert exc.value.translation_key == "temp_out_of_range" + + with pytest.raises( + ServiceValidationError, + match="Provided temperature 0.0 is not valid. Accepted range is 7 to 35", + ) as exc: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + "entity_id": "climate.test", + ATTR_TARGET_TEMP_HIGH: "25", + ATTR_TARGET_TEMP_LOW: "0", + }, + blocking=True, + ) + assert ( + str(exc.value) + == "Provided temperature 0.0 is not valid. Accepted range is 7 to 35" + ) + assert exc.value.translation_key == "temp_out_of_range" + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + "entity_id": "climate.test", + ATTR_TARGET_TEMP_HIGH: "25", + ATTR_TARGET_TEMP_LOW: "10", + }, + blocking=True, + ) + + state = hass.states.get("climate.test") + assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 10 + assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25 diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 8d1da9d09d5..853c09c534b 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -263,10 +263,10 @@ async def test_set_temperature_temperature(hass: HomeAssistant, fritz: Mock) -> await hass.services.async_call( DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 123}, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 23}, True, ) - assert device.set_target_temperature.call_args_list == [call(123)] + assert device.set_target_temperature.call_args_list == [call(23)] async def test_set_temperature_mode_off(hass: HomeAssistant, fritz: Mock) -> None: @@ -282,7 +282,7 @@ async def test_set_temperature_mode_off(hass: HomeAssistant, fritz: Mock) -> Non { ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.OFF, - ATTR_TEMPERATURE: 123, + ATTR_TEMPERATURE: 23, }, True, ) @@ -303,7 +303,7 @@ async def test_set_temperature_mode_heat(hass: HomeAssistant, fritz: Mock) -> No { ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT, - ATTR_TEMPERATURE: 123, + ATTR_TEMPERATURE: 23, }, True, ) diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 18e31b9591f..0f438056fbd 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -1097,9 +1097,9 @@ async def setup_comp_9(hass: HomeAssistant) -> None: async def test_precision(hass: HomeAssistant) -> None: """Test that setting precision to tenths works as intended.""" hass.config.units = US_CUSTOMARY_SYSTEM - await common.async_set_temperature(hass, 23.27) + await common.async_set_temperature(hass, 55.27) state = hass.states.get(ENTITY) - assert state.attributes.get("temperature") == 23.3 + assert state.attributes.get("temperature") == 55.3 # check that target_temp_step defaults to precision assert state.attributes.get("target_temp_step") == 0.1 diff --git a/tests/components/honeywell/conftest.py b/tests/components/honeywell/conftest.py index 5c5b6c0a44a..e48664db9ae 100644 --- a/tests/components/honeywell/conftest.py +++ b/tests/components/honeywell/conftest.py @@ -86,6 +86,7 @@ def device(): mock_device.system_mode = "off" mock_device.name = "device1" mock_device.current_temperature = CURRENTTEMPERATURE + mock_device.temperature_unit = "C" mock_device.mac_address = "macaddress1" mock_device.outdoor_temperature = None mock_device.outdoor_humidity = None diff --git a/tests/components/honeywell/snapshots/test_climate.ambr b/tests/components/honeywell/snapshots/test_climate.ambr index d1faf9af9a0..25bb73851c6 100644 --- a/tests/components/honeywell/snapshots/test_climate.ambr +++ b/tests/components/honeywell/snapshots/test_climate.ambr @@ -3,7 +3,7 @@ ReadOnlyDict({ 'aux_heat': 'off', 'current_humidity': 50, - 'current_temperature': -6.7, + 'current_temperature': 20, 'fan_action': 'idle', 'fan_mode': 'auto', 'fan_modes': list([ @@ -20,9 +20,9 @@ , ]), 'max_humidity': 99, - 'max_temp': 1.7, + 'max_temp': 35, 'min_humidity': 30, - 'min_temp': -13.9, + 'min_temp': 7, 'permanent_hold': False, 'preset_mode': 'none', 'preset_modes': list([ diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index b57be5f1838..55a55f7d7e7 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -92,14 +92,13 @@ async def test_dynamic_attributes( hass: HomeAssistant, device: MagicMock, config_entry: MagicMock ) -> None: """Test dynamic attributes.""" - await init_integration(hass, config_entry) entity_id = f"climate.{device.name}" state = hass.states.get(entity_id) assert state.state == HVACMode.OFF attributes = state.attributes - assert attributes["current_temperature"] == -6.7 + assert attributes["current_temperature"] == 20 assert attributes["current_humidity"] == 50 device.system_mode = "cool" @@ -114,7 +113,7 @@ async def test_dynamic_attributes( state = hass.states.get(entity_id) assert state.state == HVACMode.COOL attributes = state.attributes - assert attributes["current_temperature"] == -6.1 + assert attributes["current_temperature"] == 21 assert attributes["current_humidity"] == 55 device.system_mode = "heat" @@ -129,7 +128,7 @@ async def test_dynamic_attributes( state = hass.states.get(entity_id) assert state.state == HVACMode.HEAT attributes = state.attributes - assert attributes["current_temperature"] == 16.1 + assert attributes["current_temperature"] == 61 assert attributes["current_humidity"] == 50 device.system_mode = "auto" @@ -142,7 +141,7 @@ async def test_dynamic_attributes( state = hass.states.get(entity_id) assert state.state == HVACMode.HEAT_COOL attributes = state.attributes - assert attributes["current_temperature"] == 16.1 + assert attributes["current_temperature"] == 61 assert attributes["current_humidity"] == 50 @@ -348,7 +347,7 @@ async def test_service_calls_off_mode( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 35}, blocking=True, ) @@ -362,8 +361,8 @@ async def test_service_calls_off_mode( }, blocking=True, ) - device.set_setpoint_cool.assert_called_with(95) - device.set_setpoint_heat.assert_called_with(77) + device.set_setpoint_cool.assert_called_with(35) + device.set_setpoint_heat.assert_called_with(25) device.set_setpoint_heat.reset_mock() device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError @@ -375,13 +374,13 @@ async def test_service_calls_off_mode( SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: entity_id, - ATTR_TARGET_TEMP_LOW: 25.0, - ATTR_TARGET_TEMP_HIGH: 35.0, + ATTR_TARGET_TEMP_LOW: 24.0, + ATTR_TARGET_TEMP_HIGH: 34.0, }, blocking=True, ) - device.set_setpoint_cool.assert_called_with(95) - device.set_setpoint_heat.assert_called_with(77) + device.set_setpoint_cool.assert_called_with(34) + device.set_setpoint_heat.assert_called_with(24) assert "Invalid temperature" in caplog.text device.set_setpoint_heat.reset_mock() @@ -399,14 +398,14 @@ async def test_service_calls_off_mode( }, blocking=True, ) - device.set_setpoint_cool.assert_called_with(95) - device.set_setpoint_heat.assert_called_with(77) + device.set_setpoint_cool.assert_called_with(35) + device.set_setpoint_heat.assert_called_with(25) reset_mock(device) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 35}, blocking=True, ) device.set_setpoint_heat.assert_not_called() @@ -517,7 +516,7 @@ async def test_service_calls_cool_mode( {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, blocking=True, ) - device.set_hold_cool.assert_called_once_with(datetime.time(2, 30), 59) + device.set_hold_cool.assert_called_once_with(datetime.time(2, 30), 15) device.set_hold_cool.reset_mock() await hass.services.async_call( @@ -525,13 +524,13 @@ async def test_service_calls_cool_mode( SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: entity_id, - ATTR_TARGET_TEMP_LOW: 25.0, - ATTR_TARGET_TEMP_HIGH: 35.0, + ATTR_TARGET_TEMP_LOW: 15.0, + ATTR_TARGET_TEMP_HIGH: 20.0, }, blocking=True, ) - device.set_setpoint_cool.assert_called_with(95) - device.set_setpoint_heat.assert_called_with(77) + device.set_setpoint_cool.assert_called_with(20) + device.set_setpoint_heat.assert_called_with(15) caplog.clear() device.set_setpoint_cool.reset_mock() @@ -543,13 +542,13 @@ async def test_service_calls_cool_mode( SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: entity_id, - ATTR_TARGET_TEMP_LOW: 25.0, - ATTR_TARGET_TEMP_HIGH: 35.0, + ATTR_TARGET_TEMP_LOW: 15.0, + ATTR_TARGET_TEMP_HIGH: 20.0, }, blocking=True, ) - device.set_setpoint_cool.assert_called_with(95) - device.set_setpoint_heat.assert_called_with(77) + device.set_setpoint_cool.assert_called_with(20) + device.set_setpoint_heat.assert_called_with(15) assert "Invalid temperature" in caplog.text reset_mock(device) @@ -733,10 +732,10 @@ async def test_service_calls_heat_mode( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 25}, blocking=True, ) - device.set_hold_heat.assert_called_once_with(datetime.time(2, 30), 59) + device.set_hold_heat.assert_called_once_with(datetime.time(2, 30), 25) device.set_hold_heat.reset_mock() device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError @@ -744,10 +743,10 @@ async def test_service_calls_heat_mode( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 25}, blocking=True, ) - device.set_hold_heat.assert_called_once_with(datetime.time(2, 30), 59) + device.set_hold_heat.assert_called_once_with(datetime.time(2, 30), 25) device.set_hold_heat.reset_mock() assert "Invalid temperature" in caplog.text @@ -756,10 +755,10 @@ async def test_service_calls_heat_mode( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 25}, blocking=True, ) - device.set_hold_heat.assert_called_once_with(datetime.time(2, 30), 59) + device.set_hold_heat.assert_called_once_with(datetime.time(2, 30), 25) device.set_hold_heat.reset_mock() caplog.clear() @@ -773,8 +772,8 @@ async def test_service_calls_heat_mode( }, blocking=True, ) - device.set_setpoint_cool.assert_called_with(95) - device.set_setpoint_heat.assert_called_with(77) + device.set_setpoint_cool.assert_called_with(35) + device.set_setpoint_heat.assert_called_with(25) device.set_setpoint_heat.reset_mock() device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError @@ -789,8 +788,8 @@ async def test_service_calls_heat_mode( }, blocking=True, ) - device.set_setpoint_cool.assert_called_with(95) - device.set_setpoint_heat.assert_called_with(77) + device.set_setpoint_cool.assert_called_with(35) + device.set_setpoint_heat.assert_called_with(25) assert "Invalid temperature" in caplog.text reset_mock(device) @@ -984,8 +983,8 @@ async def test_service_calls_auto_mode( }, blocking=True, ) - device.set_setpoint_cool.assert_called_once_with(95) - device.set_setpoint_heat.assert_called_once_with(77) + device.set_setpoint_cool.assert_called_once_with(35) + device.set_setpoint_heat.assert_called_once_with(25) reset_mock(device) caplog.clear() diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index a52285b22d7..5578234ee6e 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -766,7 +766,7 @@ async def test_service_climate_swing_update( ("temperature", "result", "do_config"), [ ( - 35, + 31, [0x00], { CONF_CLIMATES: [ @@ -781,7 +781,7 @@ async def test_service_climate_swing_update( }, ), ( - 36, + 32, [0x00, 0x00], { CONF_CLIMATES: [ @@ -796,7 +796,7 @@ async def test_service_climate_swing_update( }, ), ( - 37.5, + 33.5, [0x00, 0x00], { CONF_CLIMATES: [ @@ -811,7 +811,7 @@ async def test_service_climate_swing_update( }, ), ( - "39", + "34", [0x00, 0x00, 0x00, 0x00], { CONF_CLIMATES: [ diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index f29c16f19ea..13bd6b5feda 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -654,11 +654,11 @@ async def test_set_target_temperature( assert state.state == "heat" mqtt_mock.async_publish.assert_called_once_with("mode-topic", "heat", 0, False) mqtt_mock.async_publish.reset_mock() - await common.async_set_temperature(hass, temperature=47, entity_id=ENTITY_CLIMATE) + await common.async_set_temperature(hass, temperature=35, entity_id=ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("temperature") == 47 + assert state.attributes.get("temperature") == 35 mqtt_mock.async_publish.assert_called_once_with( - "temperature-topic", "47.0", 0, False + "temperature-topic", "35.0", 0, False ) # also test directly supplying the operation mode to set_temperature @@ -713,7 +713,7 @@ async def test_set_target_temperature_pessimistic( state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") is None await common.async_set_hvac_mode(hass, "heat", ENTITY_CLIMATE) - await common.async_set_temperature(hass, temperature=47, entity_id=ENTITY_CLIMATE) + await common.async_set_temperature(hass, temperature=35, entity_id=ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") is None @@ -1590,13 +1590,13 @@ async def test_set_and_templates( assert state.attributes.get("swing_mode") == "on" # Temperature - await common.async_set_temperature(hass, temperature=47, entity_id=ENTITY_CLIMATE) + await common.async_set_temperature(hass, temperature=35, entity_id=ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with( - "temperature-topic", "temp: 47.0", 0, False + "temperature-topic", "temp: 35.0", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("temperature") == 47 + assert state.attributes.get("temperature") == 35 # Temperature Low/High await common.async_set_temperature( diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index c91e4d37ba6..70cef16bcdc 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -14,7 +14,7 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, async_fire_time_changed @@ -196,7 +196,7 @@ async def test_adam_climate_entity_climate_changes( "c50f167537524366a5af7aa3942feb1e", {"setpoint": 25.0} ) - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 6b4aedab828..b5a7be7bde0 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -400,6 +400,10 @@ async def test_climate_temperatures( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Success"}}, ), + pytest.raises( + ServiceValidationError, + match="Provided temperature 24.0 is not valid. Accepted range is 10 to 20", + ), ): await hass.services.async_call( CLIMATE_DOMAIN, @@ -410,7 +414,7 @@ async def test_climate_temperatures( await hass.async_block_till_done() state2 = hass.states.get("climate.hallway") - assert state2.attributes["temperature"] == 20 + assert state2.attributes["temperature"] == 19 with ( patch( diff --git a/tests/components/teslemetry/test_climate.py b/tests/components/teslemetry/test_climate.py index 250413396c1..31a39f1f21a 100644 --- a/tests/components/teslemetry/test_climate.py +++ b/tests/components/teslemetry/test_climate.py @@ -199,7 +199,7 @@ async def test_climate( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: [entity_id], ATTR_TEMPERATURE: 25}, + {ATTR_ENTITY_ID: [entity_id], ATTR_TEMPERATURE: 34}, blocking=True, ) diff --git a/tests/components/tplink/test_climate.py b/tests/components/tplink/test_climate.py index a80a74a5697..2f24fa829f9 100644 --- a/tests/components/tplink/test_climate.py +++ b/tests/components/tplink/test_climate.py @@ -120,12 +120,13 @@ async def test_set_temperature( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mocked_hub: Device ) -> None: """Test that set_temperature service calls the setter.""" + mocked_thermostat = mocked_hub.children[0] + mocked_thermostat.features["target_temperature"].minimum_value = 0 + await setup_platform_for_device( hass, mock_config_entry, Platform.CLIMATE, mocked_hub ) - mocked_thermostat = mocked_hub.children[0] - await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, diff --git a/tests/components/whirlpool/test_climate.py b/tests/components/whirlpool/test_climate.py index 18016bd9c67..cdae28f4432 100644 --- a/tests/components/whirlpool/test_climate.py +++ b/tests/components/whirlpool/test_climate.py @@ -264,10 +264,10 @@ async def test_service_calls( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 16}, blocking=True, ) - mock_instance.set_temp.assert_called_once_with(15) + mock_instance.set_temp.assert_called_once_with(16) mock_instance.set_mode.reset_mock() await hass.services.async_call( From cc1a6d60c07d2298e45bf5dbd9e028e64a0d1be6 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Wed, 31 Jul 2024 19:28:46 +0200 Subject: [PATCH 1808/2411] Add override for work areas in Husqvarna Automower (#120427) Co-authored-by: Robert Resch --- .../components/husqvarna_automower/icons.json | 3 +- .../husqvarna_automower/lawn_mower.py | 41 ++++++- .../components/husqvarna_automower/sensor.py | 25 ++++- .../husqvarna_automower/services.yaml | 19 ++++ .../husqvarna_automower/strings.json | 25 +++++ .../snapshots/test_sensor.ambr | 5 + .../husqvarna_automower/test_lawn_mower.py | 103 ++++++++++++++++-- 7 files changed, 208 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/icons.json b/homeassistant/components/husqvarna_automower/icons.json index a9002c5b44a..9dc1cbeb667 100644 --- a/homeassistant/components/husqvarna_automower/icons.json +++ b/homeassistant/components/husqvarna_automower/icons.json @@ -34,6 +34,7 @@ } }, "services": { - "override_schedule": "mdi:debug-step-over" + "override_schedule": "mdi:debug-step-over", + "override_schedule_work_area": "mdi:land-fields" } } diff --git a/homeassistant/components/husqvarna_automower/lawn_mower.py b/homeassistant/components/husqvarna_automower/lawn_mower.py index dd2129599fb..ac0f1fd6af2 100644 --- a/homeassistant/components/husqvarna_automower/lawn_mower.py +++ b/homeassistant/components/husqvarna_automower/lawn_mower.py @@ -2,8 +2,9 @@ from datetime import timedelta import logging +from typing import TYPE_CHECKING -from aioautomower.model import MowerActivities, MowerStates +from aioautomower.model import MowerActivities, MowerStates, WorkArea import voluptuous as vol from homeassistant.components.lawn_mower import ( @@ -12,10 +13,12 @@ from homeassistant.components.lawn_mower import ( LawnMowerEntityFeature, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AutomowerConfigEntry +from .const import DOMAIN from .coordinator import AutomowerDataUpdateCoordinator from .entity import AutomowerAvailableEntity, handle_sending_exception @@ -67,6 +70,18 @@ async def async_setup_entry( }, "async_override_schedule", ) + platform.async_register_entity_service( + "override_schedule_work_area", + { + vol.Required("work_area_id"): vol.Coerce(int), + vol.Required("duration"): vol.All( + cv.time_period, + cv.positive_timedelta, + vol.Range(min=timedelta(minutes=1), max=timedelta(days=42)), + ), + }, + "async_override_schedule_work_area", + ) class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity): @@ -98,6 +113,11 @@ class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity): return LawnMowerActivity.DOCKED return LawnMowerActivity.ERROR + @property + def work_areas(self) -> dict[int, WorkArea] | None: + """Return the work areas of the mower.""" + return self.mower_attributes.work_areas + @handle_sending_exception() async def async_start_mowing(self) -> None: """Resume schedule.""" @@ -122,3 +142,22 @@ class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity): await self.coordinator.api.commands.start_for(self.mower_id, duration) if override_mode == PARK: await self.coordinator.api.commands.park_for(self.mower_id, duration) + + @handle_sending_exception() + async def async_override_schedule_work_area( + self, work_area_id: int, duration: timedelta + ) -> None: + """Override the schedule with a certain work area.""" + if not self.mower_attributes.capabilities.work_areas: + raise ServiceValidationError( + translation_domain=DOMAIN, translation_key="work_areas_not_supported" + ) + if TYPE_CHECKING: + assert self.work_areas is not None + if work_area_id not in self.work_areas: + raise ServiceValidationError( + translation_domain=DOMAIN, translation_key="work_area_not_existing" + ) + await self.coordinator.api.commands.start_in_workarea( + self.mower_id, work_area_id, duration + ) diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index bd0b8561223..0e3e6771cec 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -1,10 +1,10 @@ """Creates the sensor entities for the mower.""" -from collections.abc import Callable +from collections.abc import Callable, Mapping from dataclasses import dataclass from datetime import datetime import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from zoneinfo import ZoneInfo from aioautomower.model import MowerAttributes, MowerModes, RestrictedReasons @@ -28,6 +28,8 @@ from .entity import AutomowerBaseEntity _LOGGER = logging.getLogger(__name__) +ATTR_WORK_AREA_ID_ASSIGNMENT = "work_area_id_assignment" + ERROR_KEY_LIST = [ "no_error", "alarm_mower_in_motion", @@ -214,11 +216,23 @@ def _get_current_work_area_name(data: MowerAttributes) -> str: return data.work_areas[data.mower.work_area_id].name +@callback +def _get_current_work_area_dict(data: MowerAttributes) -> Mapping[str, Any]: + """Return the name of the current work area.""" + if TYPE_CHECKING: + # Sensor does not get created if it is None + assert data.work_areas is not None + return {ATTR_WORK_AREA_ID_ASSIGNMENT: data.work_area_dict} + + @dataclass(frozen=True, kw_only=True) class AutomowerSensorEntityDescription(SensorEntityDescription): """Describes Automower sensor entity.""" exists_fn: Callable[[MowerAttributes], bool] = lambda _: True + extra_state_attributes_fn: Callable[[MowerAttributes], Mapping[str, Any] | None] = ( + lambda _: None + ) option_fn: Callable[[MowerAttributes], list[str] | None] = lambda _: None value_fn: Callable[[MowerAttributes], StateType | datetime] @@ -353,6 +367,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( translation_key="work_area", device_class=SensorDeviceClass.ENUM, exists_fn=lambda data: data.capabilities.work_areas, + extra_state_attributes_fn=_get_current_work_area_dict, option_fn=_get_work_area_names, value_fn=_get_current_work_area_name, ), @@ -378,6 +393,7 @@ class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity): """Defining the Automower Sensors with AutomowerSensorEntityDescription.""" entity_description: AutomowerSensorEntityDescription + _unrecorded_attributes = frozenset({ATTR_WORK_AREA_ID_ASSIGNMENT}) def __init__( self, @@ -399,3 +415,8 @@ class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity): def options(self) -> list[str] | None: """Return the option of the sensor.""" return self.entity_description.option_fn(self.mower_attributes) + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return the state attributes.""" + return self.entity_description.extra_state_attributes_fn(self.mower_attributes) diff --git a/homeassistant/components/husqvarna_automower/services.yaml b/homeassistant/components/husqvarna_automower/services.yaml index 94687a2ebfa..29c89360d1e 100644 --- a/homeassistant/components/husqvarna_automower/services.yaml +++ b/homeassistant/components/husqvarna_automower/services.yaml @@ -19,3 +19,22 @@ override_schedule: options: - "mow" - "park" + +override_schedule_work_area: + target: + entity: + integration: "husqvarna_automower" + domain: "lawn_mower" + fields: + duration: + required: true + example: "{'days': 1, 'hours': 12, 'minutes': 30}" + selector: + duration: + enable_day: true + work_area_id: + required: true + example: "123" + selector: + text: + type: number diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index be17cc25e32..c34a5dd3340 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -254,6 +254,11 @@ "state": { "my_lawn": "My lawn", "no_work_area_active": "No work area active" + }, + "state_attributes": { + "work_area_id_assignment": { + "name": "Work area ID assignment" + } } } }, @@ -269,6 +274,12 @@ "exceptions": { "command_send_failed": { "message": "Failed to send command: {exception}" + }, + "work_areas_not_supported": { + "message": "This mower does not support work areas." + }, + "work_area_not_existing": { + "message": "The selected work area does not exist." } }, "selector": { @@ -293,6 +304,20 @@ "description": "With which action the schedule should be overridden." } } + }, + "override_schedule_work_area": { + "name": "Override schedule work area", + "description": "Override the schedule of the mower for a duration of time in the selected work area.", + "fields": { + "duration": { + "name": "[%key:component::husqvarna_automower::services::override_schedule::fields::duration::name%]", + "description": "[%key:component::husqvarna_automower::services::override_schedule::fields::duration::description%]" + }, + "work_area_id": { + "name": "Work area ID", + "description": "In which work area the mower should mow." + } + } } } } diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index 730971a47dd..c727a49b71a 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -1100,6 +1100,11 @@ 'my_lawn', 'no_work_area_active', ]), + 'work_area_id_assignment': dict({ + 0: 'my_lawn', + 123456: 'Front lawn', + 654321: 'Back lawn', + }), }), 'context': , 'entity_id': 'sensor.test_mower_1_work_area', diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index 5d5cacfc6bf..2ae427e0e1e 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -13,7 +13,7 @@ from homeassistant.components.husqvarna_automower.const import DOMAIN from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL from homeassistant.components.lawn_mower import LawnMowerActivity from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from . import setup_integration from .const import TEST_MOWER_ID @@ -122,7 +122,7 @@ async def test_lawn_mower_commands( async def test_lawn_mower_service_commands( hass: HomeAssistant, aioautomower_command: str, - extra_data: int | None, + extra_data: timedelta, service: str, service_data: dict[str, int] | None, mock_automower_client: AsyncMock, @@ -158,27 +158,112 @@ async def test_lawn_mower_service_commands( @pytest.mark.parametrize( - ("service", "service_data"), + ("aioautomower_command", "extra_data1", "extra_data2", "service", "service_data"), [ ( - "override_schedule", + "start_in_workarea", + 123456, + timedelta(days=40), + "override_schedule_work_area", { - "duration": {"days": 1, "hours": 12, "minutes": 30}, - "override_mode": "fly_to_moon", + "work_area_id": 123456, + "duration": {"days": 40}, }, ), ], ) -async def test_lawn_mower_wrong_service_commands( +async def test_lawn_mower_override_work_area_command( hass: HomeAssistant, + aioautomower_command: str, + extra_data1: int, + extra_data2: timedelta, service: str, service_data: dict[str, int] | None, mock_automower_client: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: - """Test lawn_mower commands.""" + """Test lawn_mower work area override commands.""" await setup_integration(hass, mock_config_entry) - with pytest.raises(MultipleInvalid): + mocked_method = AsyncMock() + setattr(mock_automower_client.commands, aioautomower_command, mocked_method) + await hass.services.async_call( + domain=DOMAIN, + service=service, + target={"entity_id": "lawn_mower.test_mower_1"}, + service_data=service_data, + blocking=True, + ) + mocked_method.assert_called_once_with(TEST_MOWER_ID, extra_data1, extra_data2) + + getattr( + mock_automower_client.commands, aioautomower_command + ).side_effect = ApiException("Test error") + with pytest.raises( + HomeAssistantError, + match="Failed to send command: Test error", + ): + await hass.services.async_call( + domain=DOMAIN, + service=service, + target={"entity_id": "lawn_mower.test_mower_1"}, + service_data=service_data, + blocking=True, + ) + + +@pytest.mark.parametrize( + ("service", "service_data", "mower_support_wa", "exception"), + [ + ( + "override_schedule", + { + "duration": {"days": 1, "hours": 12, "minutes": 30}, + "override_mode": "fly_to_moon", + }, + False, + MultipleInvalid, + ), + ( + "override_schedule_work_area", + { + "work_area_id": 123456, + "duration": {"days": 40}, + }, + False, + ServiceValidationError, + ), + ( + "override_schedule_work_area", + { + "work_area_id": 12345, + "duration": {"days": 40}, + }, + True, + ServiceValidationError, + ), + ], +) +async def test_lawn_mower_wrong_service_commands( + hass: HomeAssistant, + service: str, + service_data: dict[str, int] | None, + mower_support_wa: bool, + exception, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test lawn_mower commands.""" + await setup_integration(hass, mock_config_entry) + values = mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower.json", DOMAIN) + ) + values[TEST_MOWER_ID].capabilities.work_areas = mower_support_wa + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + with pytest.raises(exception): await hass.services.async_call( domain=DOMAIN, service=service, From 9db42beadef9e7f49b35473878a72f1dffea6e64 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Wed, 31 Jul 2024 18:57:12 +0100 Subject: [PATCH 1809/2411] Fix handling of tplink light effects for scenes (#122965) Co-authored-by: J. Nick Koston --- homeassistant/components/tplink/light.py | 30 ++++++++++++------- tests/components/tplink/test_light.py | 38 +++++++++++++++++++++++- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 9b7dd499c97..8d6ec27f81c 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -382,17 +382,25 @@ class TPLinkLightEffectEntity(TPLinkLightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" brightness, transition = self._async_extract_brightness_transition(**kwargs) - if ( - (effect := kwargs.get(ATTR_EFFECT)) - # Effect is unlikely to be LIGHT_EFFECTS_OFF but check for it anyway - and effect not in {LightEffect.LIGHT_EFFECTS_OFF, EFFECT_OFF} - and effect in self._effect_module.effect_list - ): - await self._effect_module.set_effect( - kwargs[ATTR_EFFECT], brightness=brightness, transition=transition - ) - elif ATTR_COLOR_TEMP_KELVIN in kwargs: - if self.effect and self.effect != EFFECT_OFF: + effect_off_called = False + if effect := kwargs.get(ATTR_EFFECT): + if effect in {LightEffect.LIGHT_EFFECTS_OFF, EFFECT_OFF}: + if self._effect_module.effect is not LightEffect.LIGHT_EFFECTS_OFF: + await self._effect_module.set_effect(LightEffect.LIGHT_EFFECTS_OFF) + effect_off_called = True + if len(kwargs) == 1: + return + elif effect in self._effect_module.effect_list: + await self._effect_module.set_effect( + kwargs[ATTR_EFFECT], brightness=brightness, transition=transition + ) + return + else: + _LOGGER.error("Invalid effect %s for %s", effect, self._device.host) + return + + if ATTR_COLOR_TEMP_KELVIN in kwargs: + if self.effect and self.effect != EFFECT_OFF and not effect_off_called: # If there is an effect in progress # we have to clear the effect # before we can set a color temp diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 590274b8405..6998d8fbcc7 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -505,7 +505,9 @@ async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None: light.set_state.reset_mock() -async def test_smart_strip_effects(hass: HomeAssistant) -> None: +async def test_smart_strip_effects( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test smart strip effects.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS @@ -555,6 +557,40 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None: "Effect2", brightness=None, transition=None ) light_effect.set_effect.reset_mock() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_EFFECT] == "Effect2" + + # Test setting light effect off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "off"}, + blocking=True, + ) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_EFFECT] == "off" + light.set_state.assert_not_called() + + # Test setting light effect to invalid value + caplog.clear() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "Effect3"}, + blocking=True, + ) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_EFFECT] == "off" + assert "Invalid effect Effect3 for" in caplog.text light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) From bc25657f0ad6fc26a401f291a59f49343b92c61e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:21:04 +0200 Subject: [PATCH 1810/2411] Fix unnecessary-return-none in telnet (#122949) --- homeassistant/components/telnet/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index 805f037dbae..3b4b9e137d1 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -144,7 +144,7 @@ class TelnetSwitch(SwitchEntity): rendered = self._value_template.render_with_possible_json_value(response) else: _LOGGER.warning("Empty response for command: %s", self._command_state) - return None + return self._attr_is_on = rendered == "True" def turn_on(self, **kwargs: Any) -> None: From 93bcd413a79546e5aab29ab083981cd9b2bbf5ec Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:21:26 +0200 Subject: [PATCH 1811/2411] Fix unnecessary-return-none in iotty (#122947) --- homeassistant/components/iotty/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/iotty/switch.py b/homeassistant/components/iotty/switch.py index 6609fb59400..ee489e88349 100644 --- a/homeassistant/components/iotty/switch.py +++ b/homeassistant/components/iotty/switch.py @@ -53,7 +53,7 @@ async def async_setup_entry( def async_update_data() -> None: """Handle updated data from the API endpoint.""" if not coordinator.last_update_success: - return None + return devices = coordinator.data.devices entities = [] From 7276b4b3ad064bdad943284a6a8640f643c624b5 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Wed, 31 Jul 2024 19:31:53 +0100 Subject: [PATCH 1812/2411] Bump python-kasa to 0.7.1 (#122967) --- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index f935d019541..10b0ef61153 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -301,5 +301,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.7.0.5"] + "requirements": ["python-kasa[speedups]==0.7.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 472270c1dcf..2cae4548956 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2292,7 +2292,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.5 +python-kasa[speedups]==0.7.1 # homeassistant.components.linkplay python-linkplay==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2021a6bc7f..1d2f345cf1a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1810,7 +1810,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.0.5 +python-kasa[speedups]==0.7.1 # homeassistant.components.linkplay python-linkplay==0.0.5 From 0189a05297b73c8e9437a6c8f484b5484d56460f Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 31 Jul 2024 20:36:43 +0200 Subject: [PATCH 1813/2411] Extend Matter select entity (#122513) --- homeassistant/components/matter/select.py | 86 +++++++++++++++++--- homeassistant/components/matter/strings.json | 3 + tests/components/matter/test_select.py | 22 +++++ 3 files changed, 99 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py index bf528077b32..350712061ba 100644 --- a/homeassistant/components/matter/select.py +++ b/homeassistant/components/matter/select.py @@ -2,7 +2,12 @@ from __future__ import annotations +from dataclasses import dataclass +from typing import TYPE_CHECKING + from chip.clusters import Objects as clusters +from chip.clusters.Types import Nullable +from matter_server.common.helpers.util import create_attribute_path_from_attribute from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry @@ -10,7 +15,7 @@ from homeassistant.const import EntityCategory, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .entity import MatterEntity +from .entity import MatterEntity, MatterEntityDescription from .helpers import get_matter from .models import MatterDiscoverySchema @@ -38,7 +43,41 @@ async def async_setup_entry( matter.register_platform_handler(Platform.SELECT, async_add_entities) -class MatterModeSelectEntity(MatterEntity, SelectEntity): +@dataclass(frozen=True) +class MatterSelectEntityDescription(SelectEntityDescription, MatterEntityDescription): + """Describe Matter select entities.""" + + +class MatterSelectEntity(MatterEntity, SelectEntity): + """Representation of a select entity from Matter Attribute read/write.""" + + entity_description: MatterSelectEntityDescription + + async def async_select_option(self, option: str) -> None: + """Change the selected mode.""" + value_convert = self.entity_description.ha_to_native_value + if TYPE_CHECKING: + assert value_convert is not None + await self.matter_client.write_attribute( + node_id=self._endpoint.node.node_id, + attribute_path=create_attribute_path_from_attribute( + self._endpoint.endpoint_id, self._entity_info.primary_attribute + ), + value=value_convert(option), + ) + + @callback + def _update_from_device(self) -> None: + """Update from device.""" + value: Nullable | int | None + value = self.get_matter_attribute_value(self._entity_info.primary_attribute) + value_convert = self.entity_description.measurement_to_ha + if TYPE_CHECKING: + assert value_convert is not None + self._attr_current_option = value_convert(value) + + +class MatterModeSelectEntity(MatterSelectEntity): """Representation of a select entity from Matter (Mode) Cluster attribute(s).""" async def async_select_option(self, option: str) -> None: @@ -77,7 +116,7 @@ class MatterModeSelectEntity(MatterEntity, SelectEntity): DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.SELECT, - entity_description=SelectEntityDescription( + entity_description=MatterSelectEntityDescription( key="MatterModeSelect", entity_category=EntityCategory.CONFIG, translation_key="mode", @@ -90,7 +129,7 @@ DISCOVERY_SCHEMAS = [ ), MatterDiscoverySchema( platform=Platform.SELECT, - entity_description=SelectEntityDescription( + entity_description=MatterSelectEntityDescription( key="MatterOvenMode", translation_key="mode", ), @@ -102,7 +141,7 @@ DISCOVERY_SCHEMAS = [ ), MatterDiscoverySchema( platform=Platform.SELECT, - entity_description=SelectEntityDescription( + entity_description=MatterSelectEntityDescription( key="MatterLaundryWasherMode", translation_key="mode", ), @@ -114,7 +153,7 @@ DISCOVERY_SCHEMAS = [ ), MatterDiscoverySchema( platform=Platform.SELECT, - entity_description=SelectEntityDescription( + entity_description=MatterSelectEntityDescription( key="MatterRefrigeratorAndTemperatureControlledCabinetMode", translation_key="mode", ), @@ -126,7 +165,7 @@ DISCOVERY_SCHEMAS = [ ), MatterDiscoverySchema( platform=Platform.SELECT, - entity_description=SelectEntityDescription( + entity_description=MatterSelectEntityDescription( key="MatterRvcRunMode", translation_key="mode", ), @@ -138,7 +177,7 @@ DISCOVERY_SCHEMAS = [ ), MatterDiscoverySchema( platform=Platform.SELECT, - entity_description=SelectEntityDescription( + entity_description=MatterSelectEntityDescription( key="MatterRvcCleanMode", translation_key="mode", ), @@ -150,7 +189,7 @@ DISCOVERY_SCHEMAS = [ ), MatterDiscoverySchema( platform=Platform.SELECT, - entity_description=SelectEntityDescription( + entity_description=MatterSelectEntityDescription( key="MatterDishwasherMode", translation_key="mode", ), @@ -162,7 +201,7 @@ DISCOVERY_SCHEMAS = [ ), MatterDiscoverySchema( platform=Platform.SELECT, - entity_description=SelectEntityDescription( + entity_description=MatterSelectEntityDescription( key="MatterMicrowaveOvenMode", translation_key="mode", ), @@ -174,7 +213,7 @@ DISCOVERY_SCHEMAS = [ ), MatterDiscoverySchema( platform=Platform.SELECT, - entity_description=SelectEntityDescription( + entity_description=MatterSelectEntityDescription( key="MatterEnergyEvseMode", translation_key="mode", ), @@ -186,7 +225,7 @@ DISCOVERY_SCHEMAS = [ ), MatterDiscoverySchema( platform=Platform.SELECT, - entity_description=SelectEntityDescription( + entity_description=MatterSelectEntityDescription( key="MatterDeviceEnergyManagementMode", translation_key="mode", ), @@ -196,4 +235,27 @@ DISCOVERY_SCHEMAS = [ clusters.DeviceEnergyManagementMode.Attributes.SupportedModes, ), ), + MatterDiscoverySchema( + platform=Platform.SELECT, + entity_description=MatterSelectEntityDescription( + key="MatterStartUpOnOff", + entity_category=EntityCategory.CONFIG, + translation_key="startup_on_off", + options=["On", "Off", "Toggle", "Previous"], + measurement_to_ha=lambda x: { + 0: "Off", + 1: "On", + 2: "Toggle", + None: "Previous", + }[x], + ha_to_native_value=lambda x: { + "Off": 0, + "On": 1, + "Toggle": 2, + "Previous": None, + }[x], + ), + entity_class=MatterSelectEntity, + required_attributes=(clusters.OnOff.Attributes.StartUpOnOff,), + ), ] diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index c23a2d6fe94..e69c7ae3090 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -134,6 +134,9 @@ "select": { "mode": { "name": "Mode" + }, + "startup_on_off": { + "name": "Power-on behavior on Startup" } }, "sensor": { diff --git a/tests/components/matter/test_select.py b/tests/components/matter/test_select.py index 0d4d5e71b81..9b774f0430b 100644 --- a/tests/components/matter/test_select.py +++ b/tests/components/matter/test_select.py @@ -107,3 +107,25 @@ async def test_microwave_select_entities( await trigger_subscription_callback(hass, matter_client) state = hass.states.get("select.microwave_oven_mode") assert state.state == "Defrost" + + +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_attribute_select_entities( + hass: HomeAssistant, + matter_client: MagicMock, + light_node: MatterNode, +) -> None: + """Test select entities are created for attribute based discovery schema(s).""" + entity_id = "select.mock_dimmable_light_power_on_behavior_on_startup" + state = hass.states.get(entity_id) + assert state + assert state.state == "Previous" + assert state.attributes["options"] == ["On", "Off", "Toggle", "Previous"] + assert ( + state.attributes["friendly_name"] + == "Mock Dimmable Light Power-on behavior on Startup" + ) + set_node_attribute(light_node, 1, 6, 16387, 1) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get(entity_id) + assert state.state == "On" From f1084a57df5f38c470667e24b38983d26de1b8fa Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:36:59 -0400 Subject: [PATCH 1814/2411] Fix Sonos media_player control may fail when grouping speakers (#121853) --- homeassistant/components/sonos/speaker.py | 21 ++- tests/components/sonos/conftest.py | 24 +++ .../sonos/fixtures/av_transport.json | 38 +++++ tests/components/sonos/fixtures/zgs_group.xml | 8 + .../sonos/fixtures/zgs_two_single.xml | 10 ++ tests/components/sonos/test_speaker.py | 146 +++++++++++++++++- 6 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 tests/components/sonos/fixtures/av_transport.json create mode 100644 tests/components/sonos/fixtures/zgs_group.xml create mode 100644 tests/components/sonos/fixtures/zgs_two_single.xml diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index d77100a2236..d339e861a13 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -826,9 +826,6 @@ class SonosSpeaker: f"{SONOS_VANISHED}-{uid}", reason, ) - - if "zone_player_uui_ds_in_group" not in event.variables: - return self.event_stats.process(event) self.hass.async_create_background_task( self.create_update_groups_coro(event), @@ -857,8 +854,7 @@ class SonosSpeaker: async def _async_extract_group(event: SonosEvent | None) -> list[str]: """Extract group layout from a topology event.""" - group = event and event.zone_player_uui_ds_in_group - if group: + if group := (event and getattr(event, "zone_player_uui_ds_in_group", None)): assert isinstance(group, str) return group.split(",") @@ -867,11 +863,21 @@ class SonosSpeaker: @callback def _async_regroup(group: list[str]) -> None: """Rebuild internal group layout.""" + _LOGGER.debug("async_regroup %s %s", self.zone_name, group) if ( group == [self.soco.uid] and self.sonos_group == [self] and self.sonos_group_entities ): + # Single speakers do not have a coodinator, check and clear + if self.coordinator is not None: + _LOGGER.debug( + "Zone %s Cleared coordinator [%s]", + self.zone_name, + self.coordinator.zone_name, + ) + self.coordinator = None + self.async_write_entity_states() # Skip updating existing single speakers in polling mode return @@ -912,6 +918,11 @@ class SonosSpeaker: joined_speaker.coordinator = self joined_speaker.sonos_group = sonos_group joined_speaker.sonos_group_entities = sonos_group_entities + _LOGGER.debug( + "Zone %s Set coordinator [%s]", + joined_speaker.zone_name, + self.zone_name, + ) joined_speaker.async_write_entity_states() _LOGGER.debug("Regrouped %s: %s", self.zone_name, self.sonos_group_entities) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 4e5f704d322..bbec7a2308c 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -17,6 +17,7 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture, load_json_value_fixture @@ -661,3 +662,26 @@ def zgs_event_fixture(hass: HomeAssistant, soco: SoCo, zgs_discovery: str): await hass.async_block_till_done(wait_background_tasks=True) return _wrapper + + +@pytest.fixture(name="sonos_setup_two_speakers") +async def sonos_setup_two_speakers( + hass: HomeAssistant, soco_factory: SoCoMockFactory +) -> list[MockSoCo]: + """Set up home assistant with two Sonos Speakers.""" + soco_lr = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room") + soco_br = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom") + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "media_player": { + "interface_addr": "127.0.0.1", + "hosts": ["10.10.10.1", "10.10.10.2"], + } + } + }, + ) + await hass.async_block_till_done() + return [soco_lr, soco_br] diff --git a/tests/components/sonos/fixtures/av_transport.json b/tests/components/sonos/fixtures/av_transport.json new file mode 100644 index 00000000000..743ac61e3ff --- /dev/null +++ b/tests/components/sonos/fixtures/av_transport.json @@ -0,0 +1,38 @@ +{ + "transport_state": "PLAYING", + "current_play_mode": "NORMAL", + "current_crossfade_mode": "0", + "number_of_tracks": "1", + "current_track": "1", + "current_section": "0", + "current_track_uri": "x-rincon:RINCON_test_10.10.10.2", + "current_track_duration": "", + "current_track_meta_data": "", + "next_track_uri": "", + "next_track_meta_data": "", + "enqueued_transport_uri": "", + "enqueued_transport_uri_meta_data": "", + "playback_storage_medium": "NETWORK", + "av_transport_uri": "x-rincon:RINCON_test_10.10.10.2", + "av_transport_uri_meta_data": "", + "next_av_transport_uri": "", + "next_av_transport_uri_meta_data": "", + "current_transport_actions": "Stop, Play", + "current_valid_play_modes": "CROSSFADE", + "direct_control_client_id": "", + "direct_control_is_suspended": "0", + "direct_control_account_id": "", + "transport_status": "OK", + "sleep_timer_generation": "0", + "alarm_running": "0", + "snooze_running": "0", + "restart_pending": "0", + "transport_play_speed": "NOT_IMPLEMENTED", + "current_media_duration": "NOT_IMPLEMENTED", + "record_storage_medium": "NOT_IMPLEMENTED", + "possible_playback_storage_media": "NONE, NETWORK", + "possible_record_storage_media": "NOT_IMPLEMENTED", + "record_medium_write_status": "NOT_IMPLEMENTED", + "current_record_quality_mode": "NOT_IMPLEMENTED", + "possible_record_quality_modes": "NOT_IMPLEMENTED" +} diff --git a/tests/components/sonos/fixtures/zgs_group.xml b/tests/components/sonos/fixtures/zgs_group.xml new file mode 100644 index 00000000000..58f40be0049 --- /dev/null +++ b/tests/components/sonos/fixtures/zgs_group.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/components/sonos/fixtures/zgs_two_single.xml b/tests/components/sonos/fixtures/zgs_two_single.xml new file mode 100644 index 00000000000..18c3c9231c6 --- /dev/null +++ b/tests/components/sonos/fixtures/zgs_two_single.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tests/components/sonos/test_speaker.py b/tests/components/sonos/test_speaker.py index 2c4357060be..40d126c64f2 100644 --- a/tests/components/sonos/test_speaker.py +++ b/tests/components/sonos/test_speaker.py @@ -4,11 +4,18 @@ from unittest.mock import patch import pytest +from homeassistant.components.media_player import ( + DOMAIN as MP_DOMAIN, + SERVICE_MEDIA_PLAY, +) +from homeassistant.components.sonos import DOMAIN from homeassistant.components.sonos.const import DATA_SONOS, SCAN_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from tests.common import async_fire_time_changed +from .conftest import MockSoCo, SonosMockEvent + +from tests.common import async_fire_time_changed, load_fixture, load_json_value_fixture async def test_fallback_to_polling( @@ -67,3 +74,140 @@ async def test_subscription_creation_fails( await hass.async_block_till_done() assert speaker._subscriptions + + +def _create_zgs_sonos_event( + fixture_file: str, soco_1: MockSoCo, soco_2: MockSoCo, create_uui_ds: bool = True +) -> SonosMockEvent: + """Create a Sonos Event for zone group state, with the option of creating the uui_ds_in_group.""" + zgs = load_fixture(fixture_file, DOMAIN) + variables = {} + variables["ZoneGroupState"] = zgs + # Sonos does not always send this variable with zgs events + if create_uui_ds: + variables["zone_player_uui_ds_in_group"] = f"{soco_1.uid},{soco_2.uid}" + event = SonosMockEvent(soco_1, soco_1.zoneGroupTopology, variables) + if create_uui_ds: + event.zone_player_uui_ds_in_group = f"{soco_1.uid},{soco_2.uid}" + return event + + +def _create_avtransport_sonos_event( + fixture_file: str, soco: MockSoCo +) -> SonosMockEvent: + """Create a Sonos Event for an AVTransport update.""" + variables = load_json_value_fixture(fixture_file, DOMAIN) + return SonosMockEvent(soco, soco.avTransport, variables) + + +async def _media_play(hass: HomeAssistant, entity: str) -> None: + """Call media play service.""" + await hass.services.async_call( + MP_DOMAIN, + SERVICE_MEDIA_PLAY, + { + "entity_id": entity, + }, + blocking=True, + ) + + +async def test_zgs_event_group_speakers( + hass: HomeAssistant, sonos_setup_two_speakers: list[MockSoCo] +) -> None: + """Tests grouping and ungrouping two speakers.""" + # When Sonos speakers are grouped; one of the speakers is the coordinator and is in charge + # of playback across both speakers. Hence, service calls to play or pause on media_players + # that are part of the group are routed to the coordinator. + soco_lr = sonos_setup_two_speakers[0] + soco_br = sonos_setup_two_speakers[1] + + # Test 1 - Initial state - speakers are not grouped + state = hass.states.get("media_player.living_room") + assert state.attributes["group_members"] == ["media_player.living_room"] + state = hass.states.get("media_player.bedroom") + assert state.attributes["group_members"] == ["media_player.bedroom"] + # Each speaker is its own coordinator and calls should route to their SoCos + await _media_play(hass, "media_player.living_room") + assert soco_lr.play.call_count == 1 + await _media_play(hass, "media_player.bedroom") + assert soco_br.play.call_count == 1 + + soco_lr.play.reset_mock() + soco_br.play.reset_mock() + + # Test 2 - Group the speakers, living room is the coordinator + event = _create_zgs_sonos_event( + "zgs_group.xml", soco_lr, soco_br, create_uui_ds=True + ) + soco_lr.zoneGroupTopology.subscribe.return_value._callback(event) + soco_br.zoneGroupTopology.subscribe.return_value._callback(event) + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get("media_player.living_room") + assert state.attributes["group_members"] == [ + "media_player.living_room", + "media_player.bedroom", + ] + state = hass.states.get("media_player.bedroom") + assert state.attributes["group_members"] == [ + "media_player.living_room", + "media_player.bedroom", + ] + # Play calls should route to the living room SoCo + await _media_play(hass, "media_player.living_room") + await _media_play(hass, "media_player.bedroom") + assert soco_lr.play.call_count == 2 + assert soco_br.play.call_count == 0 + + soco_lr.play.reset_mock() + soco_br.play.reset_mock() + + # Test 3 - Ungroup the speakers + event = _create_zgs_sonos_event( + "zgs_two_single.xml", soco_lr, soco_br, create_uui_ds=False + ) + soco_lr.zoneGroupTopology.subscribe.return_value._callback(event) + soco_br.zoneGroupTopology.subscribe.return_value._callback(event) + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get("media_player.living_room") + assert state.attributes["group_members"] == ["media_player.living_room"] + state = hass.states.get("media_player.bedroom") + assert state.attributes["group_members"] == ["media_player.bedroom"] + # Calls should route to each speakers Soco + await _media_play(hass, "media_player.living_room") + assert soco_lr.play.call_count == 1 + await _media_play(hass, "media_player.bedroom") + assert soco_br.play.call_count == 1 + + +async def test_zgs_avtransport_group_speakers( + hass: HomeAssistant, sonos_setup_two_speakers: list[MockSoCo] +) -> None: + """Test processing avtransport and zgs events to change group membership.""" + soco_lr = sonos_setup_two_speakers[0] + soco_br = sonos_setup_two_speakers[1] + + # Test 1 - Send a transport event changing the coordinator + # for the living room speaker to the bedroom speaker. + event = _create_avtransport_sonos_event("av_transport.json", soco_lr) + soco_lr.avTransport.subscribe.return_value._callback(event) + await hass.async_block_till_done(wait_background_tasks=True) + # Call should route to the new coodinator which is the bedroom + await _media_play(hass, "media_player.living_room") + assert soco_lr.play.call_count == 0 + assert soco_br.play.call_count == 1 + + soco_lr.play.reset_mock() + soco_br.play.reset_mock() + + # Test 2- Send a zgs event to return living room to its own coordinator + event = _create_zgs_sonos_event( + "zgs_two_single.xml", soco_lr, soco_br, create_uui_ds=False + ) + soco_lr.zoneGroupTopology.subscribe.return_value._callback(event) + soco_br.zoneGroupTopology.subscribe.return_value._callback(event) + await hass.async_block_till_done(wait_background_tasks=True) + # Call should route to the living room + await _media_play(hass, "media_player.living_room") + assert soco_lr.play.call_count == 1 + assert soco_br.play.call_count == 0 From 8a4206da9914cae6e195b41d34f3c78ba83c5e49 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 31 Jul 2024 20:37:57 +0200 Subject: [PATCH 1815/2411] Matter handle FeatureMap update (#122544) --- homeassistant/components/matter/climate.py | 88 +++++++++++----------- homeassistant/components/matter/fan.py | 14 +++- homeassistant/components/matter/lock.py | 48 ++++++------ tests/components/matter/test_climate.py | 6 ++ tests/components/matter/test_fan.py | 5 ++ tests/components/matter/test_lock.py | 6 ++ 6 files changed, 93 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index 713aadf5620..ff00e4ee495 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -3,7 +3,7 @@ from __future__ import annotations from enum import IntEnum -from typing import TYPE_CHECKING, Any +from typing import Any from chip.clusters import Objects as clusters from matter_server.client.models import device_types @@ -30,12 +30,6 @@ from .entity import MatterEntity from .helpers import get_matter from .models import MatterDiscoverySchema -if TYPE_CHECKING: - from matter_server.client import MatterClient - from matter_server.client.models.node import MatterEndpoint - - from .discovery import MatterEntityInfo - TEMPERATURE_SCALING_FACTOR = 100 HVAC_SYSTEM_MODE_MAP = { HVACMode.OFF: 0, @@ -105,46 +99,9 @@ class MatterClimate(MatterEntity, ClimateEntity): _attr_temperature_unit: str = UnitOfTemperature.CELSIUS _attr_hvac_mode: HVACMode = HVACMode.OFF + _feature_map: int | None = None _enable_turn_on_off_backwards_compatibility = False - def __init__( - self, - matter_client: MatterClient, - endpoint: MatterEndpoint, - entity_info: MatterEntityInfo, - ) -> None: - """Initialize the Matter climate entity.""" - super().__init__(matter_client, endpoint, entity_info) - product_id = self._endpoint.node.device_info.productID - vendor_id = self._endpoint.node.device_info.vendorID - - # set hvac_modes based on feature map - self._attr_hvac_modes: list[HVACMode] = [HVACMode.OFF] - feature_map = int( - self.get_matter_attribute_value(clusters.Thermostat.Attributes.FeatureMap) - ) - self._attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF - ) - if feature_map & ThermostatFeature.kHeating: - self._attr_hvac_modes.append(HVACMode.HEAT) - if feature_map & ThermostatFeature.kCooling: - self._attr_hvac_modes.append(HVACMode.COOL) - if (vendor_id, product_id) in SUPPORT_DRY_MODE_DEVICES: - self._attr_hvac_modes.append(HVACMode.DRY) - if (vendor_id, product_id) in SUPPORT_FAN_MODE_DEVICES: - self._attr_hvac_modes.append(HVACMode.FAN_ONLY) - if feature_map & ThermostatFeature.kAutoMode: - self._attr_hvac_modes.append(HVACMode.HEAT_COOL) - # only enable temperature_range feature if the device actually supports that - - if (vendor_id, product_id) not in SINGLE_SETPOINT_DEVICES: - self._attr_supported_features |= ( - ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - ) - if any(mode for mode in self.hvac_modes if mode != HVACMode.OFF): - self._attr_supported_features |= ClimateEntityFeature.TURN_ON - async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" target_hvac_mode: HVACMode | None = kwargs.get(ATTR_HVAC_MODE) @@ -224,6 +181,7 @@ class MatterClimate(MatterEntity, ClimateEntity): @callback def _update_from_device(self) -> None: """Update from device.""" + self._calculate_features() self._attr_current_temperature = self._get_temperature_in_degrees( clusters.Thermostat.Attributes.LocalTemperature ) @@ -319,6 +277,46 @@ class MatterClimate(MatterEntity, ClimateEntity): else: self._attr_max_temp = DEFAULT_MAX_TEMP + @callback + def _calculate_features( + self, + ) -> None: + """Calculate features for HA Thermostat platform from Matter FeatureMap.""" + feature_map = int( + self.get_matter_attribute_value(clusters.Thermostat.Attributes.FeatureMap) + ) + # NOTE: the featuremap can dynamically change, so we need to update the + # supported features if the featuremap changes. + # work out supported features and presets from matter featuremap + if self._feature_map == feature_map: + return + self._feature_map = feature_map + product_id = self._endpoint.node.device_info.productID + vendor_id = self._endpoint.node.device_info.vendorID + self._attr_hvac_modes: list[HVACMode] = [HVACMode.OFF] + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF + ) + if feature_map & ThermostatFeature.kHeating: + self._attr_hvac_modes.append(HVACMode.HEAT) + if feature_map & ThermostatFeature.kCooling: + self._attr_hvac_modes.append(HVACMode.COOL) + if (vendor_id, product_id) in SUPPORT_DRY_MODE_DEVICES: + self._attr_hvac_modes.append(HVACMode.DRY) + if (vendor_id, product_id) in SUPPORT_FAN_MODE_DEVICES: + self._attr_hvac_modes.append(HVACMode.FAN_ONLY) + if feature_map & ThermostatFeature.kAutoMode: + self._attr_hvac_modes.append(HVACMode.HEAT_COOL) + # only enable temperature_range feature if the device actually supports that + + if (vendor_id, product_id) not in SINGLE_SETPOINT_DEVICES: + self._attr_supported_features |= ( + ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) + if any(mode for mode in self.hvac_modes if mode != HVACMode.OFF): + self._attr_supported_features |= ClimateEntityFeature.TURN_ON + + @callback def _get_temperature_in_degrees( self, attribute: type[clusters.ClusterAttributeDescriptor] ) -> float | None: diff --git a/homeassistant/components/matter/fan.py b/homeassistant/components/matter/fan.py index 8e5ef617304..458a57538eb 100644 --- a/homeassistant/components/matter/fan.py +++ b/homeassistant/components/matter/fan.py @@ -59,6 +59,7 @@ class MatterFan(MatterEntity, FanEntity): _last_known_preset_mode: str | None = None _last_known_percentage: int = 0 _enable_turn_on_off_backwards_compatibility = False + _feature_map: int | None = None async def async_turn_on( self, @@ -183,8 +184,7 @@ class MatterFan(MatterEntity, FanEntity): @callback def _update_from_device(self) -> None: """Update from device.""" - if not hasattr(self, "_attr_preset_modes"): - self._calculate_features() + self._calculate_features() if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False: # special case: the appliance has a dedicated Power switch on the OnOff cluster @@ -257,11 +257,17 @@ class MatterFan(MatterEntity, FanEntity): def _calculate_features( self, ) -> None: - """Calculate features and preset modes for HA Fan platform from Matter attributes..""" - # work out supported features and presets from matter featuremap + """Calculate features for HA Fan platform from Matter FeatureMap.""" feature_map = int( self.get_matter_attribute_value(clusters.FanControl.Attributes.FeatureMap) ) + # NOTE: the featuremap can dynamically change, so we need to update the + # supported features if the featuremap changes. + # work out supported features and presets from matter featuremap + if self._feature_map == feature_map: + return + self._feature_map = feature_map + self._attr_supported_features = FanEntityFeature(0) if feature_map & FanControlFeature.kMultiSpeed: self._attr_supported_features |= FanEntityFeature.SET_SPEED self._attr_speed_count = int( diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index 31ae5e496ce..8adaecd67ad 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -38,7 +38,7 @@ async def async_setup_entry( class MatterLock(MatterEntity, LockEntity): """Representation of a Matter lock.""" - features: int | None = None + _feature_map: int | None = None _optimistic_timer: asyncio.TimerHandle | None = None @property @@ -61,22 +61,6 @@ class MatterLock(MatterEntity, LockEntity): return None - @property - def supports_door_position_sensor(self) -> bool: - """Return True if the lock supports door position sensor.""" - if self.features is None: - return False - - return bool(self.features & DoorLockFeature.kDoorPositionSensor) - - @property - def supports_unbolt(self) -> bool: - """Return True if the lock supports unbolt.""" - if self.features is None: - return False - - return bool(self.features & DoorLockFeature.kUnbolt) - async def send_device_command( self, command: clusters.ClusterCommand, @@ -120,7 +104,7 @@ class MatterLock(MatterEntity, LockEntity): ) code: str | None = kwargs.get(ATTR_CODE) code_bytes = code.encode() if code else None - if self.supports_unbolt: + if self._attr_supported_features & LockEntityFeature.OPEN: # if the lock reports it has separate unbolt support, # the unlock command should unbolt only on the unlock command # and unlatch on the HA 'open' command. @@ -151,13 +135,8 @@ class MatterLock(MatterEntity, LockEntity): @callback def _update_from_device(self) -> None: """Update the entity from the device.""" - - if self.features is None: - self.features = int( - self.get_matter_attribute_value(clusters.DoorLock.Attributes.FeatureMap) - ) - if self.supports_unbolt: - self._attr_supported_features = LockEntityFeature.OPEN + # always calculate the features as they can dynamically change + self._calculate_features() lock_state = self.get_matter_attribute_value( clusters.DoorLock.Attributes.LockState @@ -197,6 +176,25 @@ class MatterLock(MatterEntity, LockEntity): if write_state: self.async_write_ha_state() + @callback + def _calculate_features( + self, + ) -> None: + """Calculate features for HA Lock platform from Matter FeatureMap.""" + feature_map = int( + self.get_matter_attribute_value(clusters.DoorLock.Attributes.FeatureMap) + ) + # NOTE: the featuremap can dynamically change, so we need to update the + # supported features if the featuremap changes. + if self._feature_map == feature_map: + return + self._feature_map = feature_map + supported_features = LockEntityFeature(0) + # determine if lock supports optional open/unbolt feature + if bool(feature_map & DoorLockFeature.kUnbolt): + supported_features |= LockEntityFeature.OPEN + self._attr_supported_features = supported_features + DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( diff --git a/tests/components/matter/test_climate.py b/tests/components/matter/test_climate.py index e0015e8b445..4d6978edfde 100644 --- a/tests/components/matter/test_climate.py +++ b/tests/components/matter/test_climate.py @@ -350,3 +350,9 @@ async def test_room_airconditioner( state = hass.states.get("climate.room_airconditioner_thermostat") assert state assert state.state == HVACMode.DRY + + # test featuremap update + set_node_attribute(room_airconditioner, 1, 513, 65532, 1) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.room_airconditioner_thermostat") + assert state.attributes["supported_features"] & ClimateEntityFeature.TURN_ON diff --git a/tests/components/matter/test_fan.py b/tests/components/matter/test_fan.py index 18c2c2ed255..690209b1165 100644 --- a/tests/components/matter/test_fan.py +++ b/tests/components/matter/test_fan.py @@ -107,6 +107,11 @@ async def test_fan_base( state = hass.states.get(entity_id) assert state.attributes["preset_mode"] is None assert state.attributes["percentage"] == 0 + # test featuremap update + set_node_attribute(air_purifier, 1, 514, 65532, 1) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get(entity_id) + assert state.attributes["supported_features"] & FanEntityFeature.SET_SPEED @pytest.mark.parametrize("expected_lingering_tasks", [True]) diff --git a/tests/components/matter/test_lock.py b/tests/components/matter/test_lock.py index 1180e6ee469..f279430b393 100644 --- a/tests/components/matter/test_lock.py +++ b/tests/components/matter/test_lock.py @@ -97,6 +97,12 @@ async def test_lock( assert state assert state.state == STATE_UNKNOWN + # test featuremap update + set_node_attribute(door_lock, 1, 257, 65532, 4096) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("lock.mock_door_lock_lock") + assert state.attributes["supported_features"] & LockEntityFeature.OPEN + # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) From d5388452d4e4bc29c8d684697bd9a0880bc9010e Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 31 Jul 2024 13:39:03 -0500 Subject: [PATCH 1816/2411] Use finished speaking detection in ESPHome/Wyoming (#122962) --- .../components/assist_pipeline/pipeline.py | 7 ++- .../components/assist_pipeline/vad.py | 2 +- homeassistant/components/esphome/select.py | 2 +- .../components/esphome/voice_assistant.py | 6 +++ homeassistant/components/wyoming/devices.py | 17 +++++++ homeassistant/components/wyoming/satellite.py | 4 ++ homeassistant/components/wyoming/select.py | 25 +++++++++- homeassistant/components/wyoming/strings.json | 8 ++++ tests/components/wyoming/test_select.py | 48 +++++++++++++++++++ 9 files changed, 115 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 845950caf8d..af29888eb07 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -505,6 +505,9 @@ class AudioSettings: samples_per_chunk: int | None = None """Number of samples that will be in each audio chunk (None for no chunking).""" + silence_seconds: float = 0.5 + """Seconds of silence after voice command has ended.""" + def __post_init__(self) -> None: """Verify settings post-initialization.""" if (self.noise_suppression_level < 0) or (self.noise_suppression_level > 4): @@ -909,7 +912,9 @@ class PipelineRun: # Transcribe audio stream stt_vad: VoiceCommandSegmenter | None = None if self.audio_settings.is_vad_enabled: - stt_vad = VoiceCommandSegmenter() + stt_vad = VoiceCommandSegmenter( + silence_seconds=self.audio_settings.silence_seconds + ) result = await self.stt_provider.async_process_audio_stream( metadata, diff --git a/homeassistant/components/assist_pipeline/vad.py b/homeassistant/components/assist_pipeline/vad.py index e3b425a2a7b..49496e66159 100644 --- a/homeassistant/components/assist_pipeline/vad.py +++ b/homeassistant/components/assist_pipeline/vad.py @@ -80,7 +80,7 @@ class VoiceCommandSegmenter: speech_seconds: float = 0.3 """Seconds of speech before voice command has started.""" - silence_seconds: float = 0.5 + silence_seconds: float = 1.0 """Seconds of silence after voice command has ended.""" timeout_seconds: float = 15.0 diff --git a/homeassistant/components/esphome/select.py b/homeassistant/components/esphome/select.py index ed37a9a6ab8..623946503eb 100644 --- a/homeassistant/components/esphome/select.py +++ b/homeassistant/components/esphome/select.py @@ -83,7 +83,7 @@ class EsphomeAssistPipelineSelect(EsphomeAssistEntity, AssistPipelineSelect): class EsphomeVadSensitivitySelect(EsphomeAssistEntity, VadSensitivitySelect): - """VAD sensitivity selector for VoIP devices.""" + """VAD sensitivity selector for ESPHome devices.""" def __init__(self, hass: HomeAssistant, entry_data: RuntimeEntryData) -> None: """Initialize a VAD sensitivity selector.""" diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index a6cedee30ab..eb55be2ced6 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -34,6 +34,7 @@ from homeassistant.components.assist_pipeline.error import ( WakeWordDetectionAborted, WakeWordDetectionError, ) +from homeassistant.components.assist_pipeline.vad import VadSensitivity from homeassistant.components.intent.timers import TimerEventType, TimerInfo from homeassistant.components.media_player import async_process_play_media_url from homeassistant.core import Context, HomeAssistant, callback @@ -243,6 +244,11 @@ class VoiceAssistantPipeline: auto_gain_dbfs=audio_settings.auto_gain, volume_multiplier=audio_settings.volume_multiplier, is_vad_enabled=bool(flags & VoiceAssistantCommandFlag.USE_VAD), + silence_seconds=VadSensitivity.to_seconds( + pipeline_select.get_vad_sensitivity( + self.hass, DOMAIN, self.device_info.mac_address + ) + ), ), ) diff --git a/homeassistant/components/wyoming/devices.py b/homeassistant/components/wyoming/devices.py index 2ca66f3b21a..2e00b31fd34 100644 --- a/homeassistant/components/wyoming/devices.py +++ b/homeassistant/components/wyoming/devices.py @@ -5,6 +5,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from homeassistant.components.assist_pipeline.vad import VadSensitivity from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -23,6 +24,7 @@ class SatelliteDevice: noise_suppression_level: int = 0 auto_gain: int = 0 volume_multiplier: float = 1.0 + vad_sensitivity: VadSensitivity = VadSensitivity.DEFAULT _is_active_listener: Callable[[], None] | None = None _is_muted_listener: Callable[[], None] | None = None @@ -77,6 +79,14 @@ class SatelliteDevice: if self._audio_settings_listener is not None: self._audio_settings_listener() + @callback + def set_vad_sensitivity(self, vad_sensitivity: VadSensitivity) -> None: + """Set VAD sensitivity.""" + if vad_sensitivity != self.vad_sensitivity: + self.vad_sensitivity = vad_sensitivity + if self._audio_settings_listener is not None: + self._audio_settings_listener() + @callback def set_is_active_listener(self, is_active_listener: Callable[[], None]) -> None: """Listen for updates to is_active.""" @@ -140,3 +150,10 @@ class SatelliteDevice: return ent_reg.async_get_entity_id( "number", DOMAIN, f"{self.satellite_id}-volume_multiplier" ) + + def get_vad_sensitivity_entity_id(self, hass: HomeAssistant) -> str | None: + """Return entity id for VAD sensitivity.""" + ent_reg = er.async_get(hass) + return ent_reg.async_get_entity_id( + "select", DOMAIN, f"{self.satellite_id}-vad_sensitivity" + ) diff --git a/homeassistant/components/wyoming/satellite.py b/homeassistant/components/wyoming/satellite.py index 3ca86a42e5d..781f0706c68 100644 --- a/homeassistant/components/wyoming/satellite.py +++ b/homeassistant/components/wyoming/satellite.py @@ -25,6 +25,7 @@ from wyoming.wake import Detect, Detection from homeassistant.components import assist_pipeline, intent, stt, tts from homeassistant.components.assist_pipeline import select as pipeline_select +from homeassistant.components.assist_pipeline.vad import VadSensitivity from homeassistant.config_entries import ConfigEntry from homeassistant.core import Context, HomeAssistant, callback @@ -409,6 +410,9 @@ class WyomingSatellite: noise_suppression_level=self.device.noise_suppression_level, auto_gain_dbfs=self.device.auto_gain, volume_multiplier=self.device.volume_multiplier, + silence_seconds=VadSensitivity.to_seconds( + self.device.vad_sensitivity + ), ), device_id=self.device.device_id, wake_word_phrase=wake_word_phrase, diff --git a/homeassistant/components/wyoming/select.py b/homeassistant/components/wyoming/select.py index 99f26c3e440..f852b4d0434 100644 --- a/homeassistant/components/wyoming/select.py +++ b/homeassistant/components/wyoming/select.py @@ -4,7 +4,11 @@ from __future__ import annotations from typing import TYPE_CHECKING, Final -from homeassistant.components.assist_pipeline.select import AssistPipelineSelect +from homeassistant.components.assist_pipeline.select import ( + AssistPipelineSelect, + VadSensitivitySelect, +) +from homeassistant.components.assist_pipeline.vad import VadSensitivity from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory @@ -45,6 +49,7 @@ async def async_setup_entry( [ WyomingSatellitePipelineSelect(hass, device), WyomingSatelliteNoiseSuppressionLevelSelect(device), + WyomingSatelliteVadSensitivitySelect(hass, device), ] ) @@ -92,3 +97,21 @@ class WyomingSatelliteNoiseSuppressionLevelSelect( self._attr_current_option = option self.async_write_ha_state() self._device.set_noise_suppression_level(_NOISE_SUPPRESSION_LEVEL[option]) + + +class WyomingSatelliteVadSensitivitySelect( + WyomingSatelliteEntity, VadSensitivitySelect +): + """VAD sensitivity selector for Wyoming satellites.""" + + def __init__(self, hass: HomeAssistant, device: SatelliteDevice) -> None: + """Initialize a VAD sensitivity selector.""" + self.device = device + + WyomingSatelliteEntity.__init__(self, device) + VadSensitivitySelect.__init__(self, hass, device.satellite_id) + + async def async_select_option(self, option: str) -> None: + """Select an option.""" + await super().async_select_option(option) + self.device.set_vad_sensitivity(VadSensitivity(option)) diff --git a/homeassistant/components/wyoming/strings.json b/homeassistant/components/wyoming/strings.json index f2768e45eb8..4a1a4c3a246 100644 --- a/homeassistant/components/wyoming/strings.json +++ b/homeassistant/components/wyoming/strings.json @@ -46,6 +46,14 @@ "high": "High", "max": "Max" } + }, + "vad_sensitivity": { + "name": "[%key:component::assist_pipeline::entity::select::vad_sensitivity::name%]", + "state": { + "default": "[%key:component::assist_pipeline::entity::select::vad_sensitivity::state::default%]", + "aggressive": "[%key:component::assist_pipeline::entity::select::vad_sensitivity::state::aggressive%]", + "relaxed": "[%key:component::assist_pipeline::entity::select::vad_sensitivity::state::relaxed%]" + } } }, "switch": { diff --git a/tests/components/wyoming/test_select.py b/tests/components/wyoming/test_select.py index e6ec2c4d432..2438d25b838 100644 --- a/tests/components/wyoming/test_select.py +++ b/tests/components/wyoming/test_select.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch from homeassistant.components import assist_pipeline from homeassistant.components.assist_pipeline.pipeline import PipelineData from homeassistant.components.assist_pipeline.select import OPTION_PREFERRED +from homeassistant.components.assist_pipeline.vad import VadSensitivity from homeassistant.components.wyoming.devices import SatelliteDevice from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -140,3 +141,50 @@ async def test_noise_suppression_level_select( ) assert satellite_device.noise_suppression_level == 2 + + +async def test_vad_sensitivity_select( + hass: HomeAssistant, + satellite_config_entry: ConfigEntry, + satellite_device: SatelliteDevice, +) -> None: + """Test VAD sensitivity select.""" + vs_entity_id = satellite_device.get_vad_sensitivity_entity_id(hass) + assert vs_entity_id + + state = hass.states.get(vs_entity_id) + assert state is not None + assert state.state == VadSensitivity.DEFAULT + assert satellite_device.vad_sensitivity == VadSensitivity.DEFAULT + + # Change setting + with patch.object(satellite_device, "set_vad_sensitivity") as mock_vs_changed: + await hass.services.async_call( + "select", + "select_option", + {"entity_id": vs_entity_id, "option": VadSensitivity.AGGRESSIVE.value}, + blocking=True, + ) + + state = hass.states.get(vs_entity_id) + assert state is not None + assert state.state == VadSensitivity.AGGRESSIVE.value + + # set function should have been called + mock_vs_changed.assert_called_once_with(VadSensitivity.AGGRESSIVE) + + # test restore + satellite_device = await reload_satellite(hass, satellite_config_entry.entry_id) + + state = hass.states.get(vs_entity_id) + assert state is not None + assert state.state == VadSensitivity.AGGRESSIVE.value + + await hass.services.async_call( + "select", + "select_option", + {"entity_id": vs_entity_id, "option": VadSensitivity.RELAXED.value}, + blocking=True, + ) + + assert satellite_device.vad_sensitivity == VadSensitivity.RELAXED From a23b3f84f0d108842bb9d707ca4656968e937b45 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:41:44 +0200 Subject: [PATCH 1817/2411] Fix implicit-return in garadget (#122923) --- homeassistant/components/garadget/cover.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index cb4f402d7bb..988c66b679c 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -213,23 +213,20 @@ class GaradgetCover(CoverEntity): def close_cover(self, **kwargs: Any) -> None: """Close the cover.""" if self._state not in ["close", "closing"]: - ret = self._put_command("setState", "close") + self._put_command("setState", "close") self._start_watcher("close") - return ret.get("return_value") == 1 def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" if self._state not in ["open", "opening"]: - ret = self._put_command("setState", "open") + self._put_command("setState", "open") self._start_watcher("open") - return ret.get("return_value") == 1 def stop_cover(self, **kwargs: Any) -> None: """Stop the door where it is.""" if self._state not in ["stopped"]: - ret = self._put_command("setState", "stop") + self._put_command("setState", "stop") self._start_watcher("stop") - return ret["return_value"] == 1 def update(self) -> None: """Get updated status from API.""" From 177690bcb372816d340e1c0416ad112c055d2fe6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:42:05 +0200 Subject: [PATCH 1818/2411] Rename variable in sensor tests (#122954) --- tests/components/sensor/test_recorder.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 2bd751a553c..27fab9c0b3b 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -2466,30 +2466,29 @@ async def test_list_statistic_ids( @pytest.mark.parametrize( - "_attributes", + "energy_attributes", [{**ENERGY_SENSOR_ATTRIBUTES, "last_reset": 0}, TEMPERATURE_SENSOR_ATTRIBUTES], ) async def test_list_statistic_ids_unsupported( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - _attributes, + energy_attributes: dict[str, Any], ) -> None: """Test listing future statistic ids for unsupported sensor.""" await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) - attributes = dict(_attributes) + attributes = dict(energy_attributes) hass.states.async_set("sensor.test1", 0, attributes=attributes) if "last_reset" in attributes: attributes.pop("unit_of_measurement") hass.states.async_set("last_reset.test2", 0, attributes=attributes) - attributes = dict(_attributes) + attributes = dict(energy_attributes) if "unit_of_measurement" in attributes: attributes["unit_of_measurement"] = "invalid" hass.states.async_set("sensor.test3", 0, attributes=attributes) attributes.pop("unit_of_measurement") hass.states.async_set("sensor.test4", 0, attributes=attributes) - attributes = dict(_attributes) + attributes = dict(energy_attributes) attributes["state_class"] = "invalid" hass.states.async_set("sensor.test5", 0, attributes=attributes) attributes.pop("state_class") From 7c179c33b5499ff15154b41f5f61f335535d961b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:42:19 +0200 Subject: [PATCH 1819/2411] Fix unnecessary-return-none in tradfri (#122950) --- homeassistant/components/tradfri/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index 4ad1424aa9a..20695f26500 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -73,11 +73,11 @@ class TradfriSwitch(TradfriBaseEntity, SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the switch to turn off.""" if not self._device_control: - return None + return await self._api(self._device_control.set_state(False)) async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the switch to turn on.""" if not self._device_control: - return None + return await self._api(self._device_control.set_state(True)) From 8de0e4ca7cb63bac30972f98d2e2c8652170562e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 31 Jul 2024 13:42:33 -0500 Subject: [PATCH 1820/2411] Remove aiohappyeyeballs license exception (#122969) --- script/licenses.py | 1 - 1 file changed, 1 deletion(-) diff --git a/script/licenses.py b/script/licenses.py index ad5ae8476b3..3b9ec389b08 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -124,7 +124,6 @@ EXCEPTIONS = { "PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201 "aiocomelit", # https://github.com/chemelli74/aiocomelit/pull/138 "aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180 - "aiohappyeyeballs", # PSF-2.0 license "aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94 "aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8 "aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6 From c0fe65fa60110f3ca9e2772e90b39a99ddfcbb7c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:42:42 +0200 Subject: [PATCH 1821/2411] Fix unnecessary-return-none in homematic (#122948) --- homeassistant/components/homematic/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 16c345c5635..bf1295df6be 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -141,7 +141,7 @@ class HMThermostat(HMDevice, ClimateEntity): def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: - return None + return self._hmdevice.writeNodeData(self._state, float(temperature)) From 79a741486ce76f8e91e2a0819050ea05546d8ca9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:42:57 +0200 Subject: [PATCH 1822/2411] Fix implicit-return in wyoming (#122946) --- homeassistant/components/wyoming/wake_word.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/wyoming/wake_word.py b/homeassistant/components/wyoming/wake_word.py index 6eba0f7ca6d..64dfd60c068 100644 --- a/homeassistant/components/wyoming/wake_word.py +++ b/homeassistant/components/wyoming/wake_word.py @@ -89,6 +89,7 @@ class WyomingWakeWordProvider(wake_word.WakeWordDetectionEntity): """Get the next chunk from audio stream.""" async for chunk_bytes in stream: return chunk_bytes + return None try: async with AsyncTcpClient(self.service.host, self.service.port) as client: From 2f3f124aa10461e741a2557cc695e23d6f834b34 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 31 Jul 2024 13:44:47 -0500 Subject: [PATCH 1823/2411] Drop unnecessary lambdas in the entity filter (#122941) --- homeassistant/helpers/entityfilter.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index 24b65cba82a..1eaa0fb1404 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -4,7 +4,8 @@ from __future__ import annotations from collections.abc import Callable import fnmatch -from functools import lru_cache +from functools import lru_cache, partial +import operator import re import voluptuous as vol @@ -195,7 +196,7 @@ def _generate_filter_from_sets_and_pattern_lists( # Case 1 - No filter # - All entities included if not have_include and not have_exclude: - return lambda entity_id: True + return bool # Case 2 - Only includes # - Entity listed in entities include: include @@ -280,4 +281,4 @@ def _generate_filter_from_sets_and_pattern_lists( # Case 6 - No Domain and/or glob includes or excludes # - Entity listed in entities include: include # - Otherwise: exclude - return lambda entity_id: entity_id in include_e + return partial(operator.contains, include_e) From d393317eb202a20ca6570420791f0e564fbb5140 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:45:10 +0200 Subject: [PATCH 1824/2411] Fix implicit-return in yamaha (#122942) --- homeassistant/components/yamaha/media_player.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index bf217f12ca4..507f485fcc7 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -427,19 +427,21 @@ class YamahaDeviceZone(MediaPlayerEntity): self.zctrl.surround_program = sound_mode @property - def media_artist(self): + def media_artist(self) -> str | None: """Artist of current playing media.""" if self._play_status is not None: return self._play_status.artist + return None @property - def media_album_name(self): + def media_album_name(self) -> str | None: """Album of current playing media.""" if self._play_status is not None: return self._play_status.album + return None @property - def media_content_type(self): + def media_content_type(self) -> MediaType | None: """Content type of current playing media.""" # Loose assumption that if playback is supported, we are playing music if self._is_playback_supported: @@ -447,7 +449,7 @@ class YamahaDeviceZone(MediaPlayerEntity): return None @property - def media_title(self): + def media_title(self) -> str | None: """Artist of current playing media.""" if self._play_status is not None: song = self._play_status.song @@ -459,3 +461,4 @@ class YamahaDeviceZone(MediaPlayerEntity): return f"{station}: {song}" return song or station + return None From dde97a02f05b9df11af989eb58206de687cb9ebe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:45:29 +0200 Subject: [PATCH 1825/2411] Fix implicit-return in xiaomi_aqara (#122940) --- homeassistant/components/xiaomi_aqara/binary_sensor.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index cee2980fe07..75208b142dd 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -202,6 +202,8 @@ class XiaomiNatgasSensor(XiaomiBinarySensor): return True return False + return False + class XiaomiMotionSensor(XiaomiBinarySensor): """Representation of a XiaomiMotionSensor.""" @@ -298,6 +300,8 @@ class XiaomiMotionSensor(XiaomiBinarySensor): self._state = True return True + return False + class XiaomiDoorSensor(XiaomiBinarySensor, RestoreEntity): """Representation of a XiaomiDoorSensor.""" @@ -357,6 +361,8 @@ class XiaomiDoorSensor(XiaomiBinarySensor, RestoreEntity): return True return False + return False + class XiaomiWaterLeakSensor(XiaomiBinarySensor): """Representation of a XiaomiWaterLeakSensor.""" @@ -401,6 +407,8 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor): return True return False + return False + class XiaomiSmokeSensor(XiaomiBinarySensor): """Representation of a XiaomiSmokeSensor.""" @@ -443,6 +451,8 @@ class XiaomiSmokeSensor(XiaomiBinarySensor): return True return False + return False + class XiaomiVibration(XiaomiBinarySensor): """Representation of a Xiaomi Vibration Sensor.""" From a6aae4e8572381b77f43406b49503f87d113d3ae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:45:48 +0200 Subject: [PATCH 1826/2411] Fix implicit-return in xiaomi_miio (#122939) --- homeassistant/components/xiaomi_miio/binary_sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index 7729ce27d29..6d1a81007dc 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -190,7 +190,8 @@ async def async_setup_entry( elif model in MODELS_HUMIDIFIER_MJJSQ: sensors = HUMIDIFIER_MJJSQ_BINARY_SENSORS elif model in MODELS_VACUUM: - return _setup_vacuum_sensors(hass, config_entry, async_add_entities) + _setup_vacuum_sensors(hass, config_entry, async_add_entities) + return for description in BINARY_SENSOR_TYPES: if description.key not in sensors: From 4aacec2de74938ec6e9e82598189652da345ba6f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:46:30 +0200 Subject: [PATCH 1827/2411] Fix implicit-return in xiaomi (#122938) --- homeassistant/components/xiaomi/device_tracker.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index b3983e76aaa..b14ec073938 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -172,7 +172,6 @@ def _get_token(host, username, password): ) _LOGGER.exception(error_message, url, data, result) return None - else: - _LOGGER.error( - "Invalid response: [%s] at url: [%s] with data [%s]", res, url, data - ) + + _LOGGER.error("Invalid response: [%s] at url: [%s] with data [%s]", res, url, data) + return None From c7f863a14123069774388358337aaac894f71589 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 31 Jul 2024 13:47:19 -0500 Subject: [PATCH 1828/2411] Drop some unnecessary lambdas in powerwall (#122936) --- homeassistant/components/powerwall/sensor.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 7113ab6ba70..9423d65b0fc 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from operator import attrgetter, methodcaller from typing import TYPE_CHECKING, Generic, TypeVar from tesla_powerwall import GridState, MeterResponse, MeterType @@ -58,11 +59,6 @@ def _get_meter_frequency(meter: MeterResponse) -> float: return round(meter.frequency, 1) -def _get_meter_total_current(meter: MeterResponse) -> float: - """Get the current value in A.""" - return meter.get_instant_total_current() - - def _get_meter_average_voltage(meter: MeterResponse) -> float: """Get the current value in V.""" return round(meter.instant_average_voltage, 1) @@ -93,7 +89,7 @@ POWERWALL_INSTANT_SENSORS = ( device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, entity_registry_enabled_default=False, - value_fn=_get_meter_total_current, + value_fn=methodcaller("get_instant_total_current"), ), PowerwallSensorEntityDescription[MeterResponse, float]( key="instant_voltage", @@ -132,7 +128,7 @@ BATTERY_INSTANT_SENSORS: list[PowerwallSensorEntityDescription] = [ native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=1, - value_fn=lambda battery_data: battery_data.capacity, + value_fn=attrgetter("capacity"), ), PowerwallSensorEntityDescription[BatteryResponse, float | None]( key="battery_instant_voltage", @@ -170,7 +166,7 @@ BATTERY_INSTANT_SENSORS: list[PowerwallSensorEntityDescription] = [ state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, - value_fn=lambda battery_data: battery_data.p_out, + value_fn=attrgetter("p_out"), ), PowerwallSensorEntityDescription[BatteryResponse, float | None]( key="battery_export", @@ -181,7 +177,7 @@ BATTERY_INSTANT_SENSORS: list[PowerwallSensorEntityDescription] = [ native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=0, - value_fn=lambda battery_data: battery_data.energy_discharged, + value_fn=attrgetter("energy_discharged"), ), PowerwallSensorEntityDescription[BatteryResponse, float | None]( key="battery_import", @@ -192,7 +188,7 @@ BATTERY_INSTANT_SENSORS: list[PowerwallSensorEntityDescription] = [ native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=0, - value_fn=lambda battery_data: battery_data.energy_charged, + value_fn=attrgetter("energy_charged"), ), PowerwallSensorEntityDescription[BatteryResponse, int]( key="battery_remaining", @@ -203,7 +199,7 @@ BATTERY_INSTANT_SENSORS: list[PowerwallSensorEntityDescription] = [ native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=1, - value_fn=lambda battery_data: battery_data.energy_remaining, + value_fn=attrgetter("energy_remaining"), ), PowerwallSensorEntityDescription[BatteryResponse, str]( key="grid_state", From 4fda025106cdb111a02d34cc753879801f013e45 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:47:33 +0200 Subject: [PATCH 1829/2411] Fix implicit-return in wsdot (#122935) --- homeassistant/components/wsdot/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 3aae6746ea9..73714b75c95 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta, timezone from http import HTTPStatus import logging import re +from typing import Any import requests import voluptuous as vol @@ -125,7 +126,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): self._state = self._data.get(ATTR_CURRENT_TIME) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return other details about the sensor state.""" if self._data is not None: attrs = {} @@ -140,6 +141,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): self._data.get(ATTR_TIME_UPDATED) ) return attrs + return None def _parse_wsdot_timestamp(timestamp): From be8186126ef606d58e1770a78ce634e69dde0dc4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:47:48 +0200 Subject: [PATCH 1830/2411] Fix implicit-return in valve (#122933) --- homeassistant/components/valve/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/valve/__init__.py b/homeassistant/components/valve/__init__.py index e97a68c2e82..3814275b703 100644 --- a/homeassistant/components/valve/__init__.py +++ b/homeassistant/components/valve/__init__.py @@ -223,7 +223,8 @@ class ValveEntity(Entity): async def async_handle_open_valve(self) -> None: """Open the valve.""" if self.supported_features & ValveEntityFeature.SET_POSITION: - return await self.async_set_valve_position(100) + await self.async_set_valve_position(100) + return await self.async_open_valve() def close_valve(self) -> None: @@ -238,7 +239,8 @@ class ValveEntity(Entity): async def async_handle_close_valve(self) -> None: """Close the valve.""" if self.supported_features & ValveEntityFeature.SET_POSITION: - return await self.async_set_valve_position(0) + await self.async_set_valve_position(0) + return await self.async_close_valve() async def async_toggle(self) -> None: From c702ffa7dd3bf7e5a3edc44d6e1686310ef280e9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:48:30 +0200 Subject: [PATCH 1831/2411] Fix implicit-return in uk_transport (#122932) --- homeassistant/components/uk_transport/sensor.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index 8e874be0bca..a86f7a1cc83 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta from http import HTTPStatus import logging import re +from typing import Any import requests import voluptuous as vol @@ -196,10 +197,10 @@ class UkTransportLiveBusTimeSensor(UkTransportSensor): self._state = None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return other details about the sensor state.""" - attrs = {} if self._data is not None: + attrs = {ATTR_NEXT_BUSES: self._next_buses} for key in ( ATTR_ATCOCODE, ATTR_LOCALITY, @@ -207,8 +208,8 @@ class UkTransportLiveBusTimeSensor(UkTransportSensor): ATTR_REQUEST_TIME, ): attrs[key] = self._data.get(key) - attrs[ATTR_NEXT_BUSES] = self._next_buses return attrs + return None class UkTransportLiveTrainTimeSensor(UkTransportSensor): @@ -266,15 +267,17 @@ class UkTransportLiveTrainTimeSensor(UkTransportSensor): self._state = None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return other details about the sensor state.""" - attrs = {} if self._data is not None: - attrs[ATTR_STATION_CODE] = self._station_code - attrs[ATTR_CALLING_AT] = self._calling_at + attrs = { + ATTR_STATION_CODE: self._station_code, + ATTR_CALLING_AT: self._calling_at, + } if self._next_trains: attrs[ATTR_NEXT_TRAINS] = self._next_trains return attrs + return None def _delta_mins(hhmm_time_str): From 9023d80d1b2835f0b66d07675bd9a075bbf76c18 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:48:51 +0200 Subject: [PATCH 1832/2411] Fix implicit-return in twitter (#122931) --- homeassistant/components/twitter/notify.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index 66b076126b5..eef51ca9613 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -129,10 +129,11 @@ class TwitterNotificationService(BaseNotificationService): else: _LOGGER.debug("Message posted: %s", resp.json()) - def upload_media_then_callback(self, callback, media_path=None): + def upload_media_then_callback(self, callback, media_path=None) -> None: """Upload media.""" if not media_path: - return callback() + callback() + return with open(media_path, "rb") as file: total_bytes = os.path.getsize(media_path) @@ -141,7 +142,7 @@ class TwitterNotificationService(BaseNotificationService): if 199 > resp.status_code < 300: self.log_error_resp(resp) - return None + return media_id = resp.json()["media_id"] media_id = self.upload_media_chunked(file, total_bytes, media_id) @@ -149,10 +150,11 @@ class TwitterNotificationService(BaseNotificationService): resp = self.upload_media_finalize(media_id) if 199 > resp.status_code < 300: self.log_error_resp(resp) - return None + return if resp.json().get("processing_info") is None: - return callback(media_id) + callback(media_id) + return self.check_status_until_done(media_id, callback) @@ -209,7 +211,7 @@ class TwitterNotificationService(BaseNotificationService): "media/upload", {"command": "FINALIZE", "media_id": media_id} ) - def check_status_until_done(self, media_id, callback, *args): + def check_status_until_done(self, media_id, callback, *args) -> None: """Upload media, STATUS phase.""" resp = self.api.request( "media/upload", @@ -223,7 +225,8 @@ class TwitterNotificationService(BaseNotificationService): _LOGGER.debug("media processing %s status: %s", media_id, processing_info) if processing_info["state"] in {"succeeded", "failed"}: - return callback(media_id) + callback(media_id) + return check_after_secs = processing_info["check_after_secs"] _LOGGER.debug( From b8ac86939b0d4fd21d48c82c48b581d6f6fe97bc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:49:06 +0200 Subject: [PATCH 1833/2411] Fix implicit-return in smartthings (#122927) --- homeassistant/components/smartthings/smartapp.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index e2593dd7b10..6b0da00b132 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -16,6 +16,7 @@ from pysmartthings import ( CAPABILITIES, CLASSIFICATION_AUTOMATION, App, + AppEntity, AppOAuth, AppSettings, InstalledAppStatus, @@ -63,7 +64,7 @@ def format_unique_id(app_id: str, location_id: str) -> str: return f"{app_id}_{location_id}" -async def find_app(hass: HomeAssistant, api): +async def find_app(hass: HomeAssistant, api: SmartThings) -> AppEntity | None: """Find an existing SmartApp for this installation of hass.""" apps = await api.apps() for app in [app for app in apps if app.app_name.startswith(APP_NAME_PREFIX)]: @@ -74,6 +75,7 @@ async def find_app(hass: HomeAssistant, api): == hass.data[DOMAIN][CONF_INSTANCE_ID] ): return app + return None async def validate_installed_app(api, installed_app_id: str): From 9860109db91ce9de2b2294069325088691dec2ed Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:49:19 +0200 Subject: [PATCH 1834/2411] Fix implicit-return in satel_integra (#122925) --- homeassistant/components/satel_integra/binary_sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index 209b6c38cda..8ff54940635 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -109,10 +109,11 @@ class SatelIntegraBinarySensor(BinarySensorEntity): return self._name @property - def icon(self): + def icon(self) -> str | None: """Icon for device by its type.""" if self._zone_type is BinarySensorDeviceClass.SMOKE: return "mdi:fire" + return None @property def is_on(self): From 4a4209647e69a3c8d19c90d119cdf7c540f0058c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:49:40 +0200 Subject: [PATCH 1835/2411] Fix implicit-return in humidifier (#122921) --- homeassistant/components/humidifier/device_action.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py index de1d4c871e3..06440480277 100644 --- a/homeassistant/components/humidifier/device_action.py +++ b/homeassistant/components/humidifier/device_action.py @@ -99,9 +99,10 @@ async def async_call_action_from_config( service = const.SERVICE_SET_MODE service_data[ATTR_MODE] = config[ATTR_MODE] else: - return await toggle_entity.async_call_action_from_config( + await toggle_entity.async_call_action_from_config( hass, config, variables, context, DOMAIN ) + return await hass.services.async_call( DOMAIN, service, service_data, blocking=True, context=context From b31263b747860a27643f3f4373a6efd6980ce624 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:50:11 +0200 Subject: [PATCH 1836/2411] Fix implicit-return in itunes (#122917) --- homeassistant/components/itunes/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index c32ca287793..0f241041c0d 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -135,6 +135,8 @@ class Itunes: path = f"/playlists/{playlist['id']}/play" return self._request("PUT", path) + raise ValueError(f"Playlist {playlist_id_or_name} not found") + def artwork_url(self): """Return a URL of the current track's album art.""" return f"{self._base_url}/artwork" From d878d744e7b56de8465be1ffeaf22f79b245a0a2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:50:31 +0200 Subject: [PATCH 1837/2411] Fix implicit-return in irish_rail_transport (#122916) --- homeassistant/components/irish_rail_transport/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/irish_rail_transport/sensor.py b/homeassistant/components/irish_rail_transport/sensor.py index a96846558fa..39bf39bcbe0 100644 --- a/homeassistant/components/irish_rail_transport/sensor.py +++ b/homeassistant/components/irish_rail_transport/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta +from typing import Any from pyirishrail.pyirishrail import IrishRailRTPI import voluptuous as vol @@ -104,7 +105,7 @@ class IrishRailTransportSensor(SensorEntity): return self._state @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" if self._times: next_up = "None" @@ -127,6 +128,7 @@ class IrishRailTransportSensor(SensorEntity): ATTR_NEXT_UP: next_up, ATTR_TRAIN_TYPE: self._times[0][ATTR_TRAIN_TYPE], } + return None @property def native_unit_of_measurement(self): From 220f6860787fcbe869a5356137fed821b36f3dfa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:51:24 +0200 Subject: [PATCH 1838/2411] Remove invalid type hint and assignment in number (#122906) --- homeassistant/components/number/significant_change.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/number/significant_change.py b/homeassistant/components/number/significant_change.py index 14cb2246615..e8cdd78e321 100644 --- a/homeassistant/components/number/significant_change.py +++ b/homeassistant/components/number/significant_change.py @@ -44,7 +44,6 @@ def async_check_significant_change( if (device_class := new_attrs.get(ATTR_DEVICE_CLASS)) is None: return None - absolute_change: float | None = None percentage_change: float | None = None # special for temperature @@ -83,11 +82,8 @@ def async_check_significant_change( # Old state was invalid, we should report again return True - if absolute_change is not None and percentage_change is not None: + if percentage_change is not None: return _absolute_and_relative_change( float(old_state), float(new_state), 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(float(old_state), float(new_state), absolute_change) From 17f34b452ed4f3f3b8a2e98229fc19f7b51d3214 Mon Sep 17 00:00:00 2001 From: alexfp14 Date: Wed, 31 Jul 2024 20:52:32 +0200 Subject: [PATCH 1839/2411] =?UTF-8?q?Add=20HVAC=20mode=20support=20for=20A?= =?UTF-8?q?tlanticPassAPCHeatPumpMainComponent=20(heati=E2=80=A6=20(#12217?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mick Vleeshouwer --- CODEOWNERS | 4 +- .../overkiz/climate_entities/__init__.py | 4 ++ ...antic_pass_apc_heat_pump_main_component.py | 65 +++++++++++++++++++ homeassistant/components/overkiz/const.py | 1 + .../components/overkiz/manifest.json | 3 +- 5 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heat_pump_main_component.py diff --git a/CODEOWNERS b/CODEOWNERS index bf93676f962..4f8c03fe90f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1051,8 +1051,8 @@ build.json @home-assistant/supervisor /tests/components/otbr/ @home-assistant/core /homeassistant/components/ourgroceries/ @OnFreund /tests/components/ourgroceries/ @OnFreund -/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 -/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 +/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14 +/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14 /homeassistant/components/ovo_energy/ @timmo001 /tests/components/ovo_energy/ @timmo001 /homeassistant/components/p1_monitor/ @klaasnicolaas diff --git a/homeassistant/components/overkiz/climate_entities/__init__.py b/homeassistant/components/overkiz/climate_entities/__init__.py index df997f7a68e..ac864686432 100644 --- a/homeassistant/components/overkiz/climate_entities/__init__.py +++ b/homeassistant/components/overkiz/climate_entities/__init__.py @@ -11,6 +11,9 @@ from .atlantic_electrical_heater_with_adjustable_temperature_setpoint import ( ) from .atlantic_electrical_towel_dryer import AtlanticElectricalTowelDryer from .atlantic_heat_recovery_ventilation import AtlanticHeatRecoveryVentilation +from .atlantic_pass_apc_heat_pump_main_component import ( + AtlanticPassAPCHeatPumpMainComponent, +) from .atlantic_pass_apc_heating_zone import AtlanticPassAPCHeatingZone from .atlantic_pass_apc_zone_control import AtlanticPassAPCZoneControl from .atlantic_pass_apc_zone_control_zone import AtlanticPassAPCZoneControlZone @@ -43,6 +46,7 @@ WIDGET_TO_CLIMATE_ENTITY = { UIWidget.SOMFY_HEATING_TEMPERATURE_INTERFACE: SomfyHeatingTemperatureInterface, UIWidget.SOMFY_THERMOSTAT: SomfyThermostat, UIWidget.VALVE_HEATING_TEMPERATURE_INTERFACE: ValveHeatingTemperatureInterface, + UIWidget.ATLANTIC_PASS_APC_HEAT_PUMP: AtlanticPassAPCHeatPumpMainComponent, } # For Atlantic APC, some devices are standalone and control themselves, some others needs to be diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heat_pump_main_component.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heat_pump_main_component.py new file mode 100644 index 00000000000..1cd13205b13 --- /dev/null +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heat_pump_main_component.py @@ -0,0 +1,65 @@ +"""Support for Atlantic Pass APC Heat Pump Main Component.""" + +from __future__ import annotations + +from asyncio import sleep +from typing import cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.const import UnitOfTemperature + +from ..const import DOMAIN +from ..entity import OverkizEntity + +OVERKIZ_TO_HVAC_MODES: dict[str, HVACMode] = { + OverkizCommandParam.STOP: HVACMode.OFF, + OverkizCommandParam.HEATING: HVACMode.HEAT, + OverkizCommandParam.COOLING: HVACMode.COOL, +} + +HVAC_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODES.items()} + + +class AtlanticPassAPCHeatPumpMainComponent(OverkizEntity, ClimateEntity): + """Representation of Atlantic Pass APC Heat Pump Main Component. + + This component can only turn off the heating pump and select the working mode: heating or cooling. + To set new temperatures, they must be selected individually per Zones (ie: AtlanticPassAPCHeatingAndCoolingZone). + Once the Device is switched on into heating or cooling mode, the Heat Pump will be activated and will use + the default temperature configuration for each available zone. + """ + + _attr_hvac_modes = [*HVAC_MODES_TO_OVERKIZ] + _attr_supported_features = ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False + + @property + def hvac_mode(self) -> HVACMode: + """Return hvac current mode: stop, cooling, heating.""" + return OVERKIZ_TO_HVAC_MODES[ + cast( + str, self.executor.select_state(OverkizState.IO_PASS_APC_OPERATING_MODE) + ) + ] + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode: stop, cooling, heating.""" + # They are mainly managed by the Zone Control device + # However, we can turn off or put the heat pump in cooling/ heating mode. + await self.executor.async_execute_command( + OverkizCommand.SET_PASS_APC_OPERATING_MODE, + HVAC_MODES_TO_OVERKIZ[hvac_mode], + ) + + # Wait for 2 seconds to ensure the HVAC mode change is properly applied and system stabilizes. + await sleep(2) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 59acc4ac232..a90260e0f0f 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -95,6 +95,7 @@ OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = { UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_HEAT_RECOVERY_VENTILATION: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_PASS_APC_DHW: Platform.WATER_HEATER, # widgetName, uiClass is WaterHeatingSystem (not supported) + UIWidget.ATLANTIC_PASS_APC_HEAT_PUMP: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_PASS_APC_HEATING_ZONE: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 8825c09e0ff..19850f0b57e 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -6,7 +6,8 @@ "@vlebourl", "@tetienne", "@nyroDev", - "@tronix117" + "@tronix117", + "@alexfp14" ], "config_flow": true, "dhcp": [ From 291036964756c01204c80788bf355d9ee7458876 Mon Sep 17 00:00:00 2001 From: Jack Gaino Date: Wed, 31 Jul 2024 15:00:04 -0400 Subject: [PATCH 1840/2411] Optionally return response data when calling services through the API (#115046) Co-authored-by: J. Nick Koston Co-authored-by: Erik Montnemery --- homeassistant/components/api/__init__.py | 29 ++++++++++- tests/components/api/test_init.py | 62 ++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index b794b60b33d..ba71fb0def1 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -390,6 +390,27 @@ class APIDomainServicesView(HomeAssistantView): ) context = self.context(request) + if not hass.services.has_service(domain, service): + raise HTTPBadRequest from ServiceNotFound(domain, service) + + if response_requested := "return_response" in request.query: + if ( + hass.services.supports_response(domain, service) + is ha.SupportsResponse.NONE + ): + return self.json_message( + "Service does not support responses. Remove return_response from request.", + HTTPStatus.BAD_REQUEST, + ) + elif ( + hass.services.supports_response(domain, service) is ha.SupportsResponse.ONLY + ): + return self.json_message( + "Service call requires responses but caller did not ask for responses. " + "Add ?return_response to query parameters.", + HTTPStatus.BAD_REQUEST, + ) + changed_states: list[json_fragment] = [] @ha.callback @@ -406,13 +427,14 @@ class APIDomainServicesView(HomeAssistantView): try: # shield the service call from cancellation on connection drop - await shield( + response = await shield( hass.services.async_call( domain, service, data, # type: ignore[arg-type] blocking=True, context=context, + return_response=response_requested, ) ) except (vol.Invalid, ServiceNotFound) as ex: @@ -420,6 +442,11 @@ class APIDomainServicesView(HomeAssistantView): finally: cancel_listen() + if response_requested: + return self.json( + {"changed_states": changed_states, "service_response": response} + ) + return self.json(changed_states) diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index c283aeb718e..abce262fd12 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -3,6 +3,7 @@ import asyncio from http import HTTPStatus import json +from typing import Any from unittest.mock import patch from aiohttp import ServerDisconnectedError, web @@ -355,6 +356,67 @@ async def test_api_call_service_with_data( assert state["attributes"] == {"data": 1} +SERVICE_DICT = {"changed_states": [], "service_response": {"foo": "bar"}} +RESP_REQUIRED = { + "message": ( + "Service call requires responses but caller did not ask for " + "responses. Add ?return_response to query parameters." + ) +} +RESP_UNSUPPORTED = { + "message": "Service does not support responses. Remove return_response from request." +} + + +@pytest.mark.parametrize( + ( + "supports_response", + "requested_response", + "expected_number_of_service_calls", + "expected_status", + "expected_response", + ), + [ + (ha.SupportsResponse.ONLY, True, 1, HTTPStatus.OK, SERVICE_DICT), + (ha.SupportsResponse.ONLY, False, 0, HTTPStatus.BAD_REQUEST, RESP_REQUIRED), + (ha.SupportsResponse.OPTIONAL, True, 1, HTTPStatus.OK, SERVICE_DICT), + (ha.SupportsResponse.OPTIONAL, False, 1, HTTPStatus.OK, []), + (ha.SupportsResponse.NONE, True, 0, HTTPStatus.BAD_REQUEST, RESP_UNSUPPORTED), + (ha.SupportsResponse.NONE, False, 1, HTTPStatus.OK, []), + ], +) +async def test_api_call_service_returns_response_requested_response( + hass: HomeAssistant, + mock_api_client: TestClient, + supports_response: ha.SupportsResponse, + requested_response: bool, + expected_number_of_service_calls: int, + expected_status: int, + expected_response: Any, +) -> None: + """Test if the API allows us to call a service.""" + test_value = [] + + @ha.callback + def listener(service_call): + """Record that our service got called.""" + test_value.append(1) + return {"foo": "bar"} + + hass.services.async_register( + "test_domain", "test_service", listener, supports_response=supports_response + ) + + resp = await mock_api_client.post( + "/api/services/test_domain/test_service" + + ("?return_response" if requested_response else "") + ) + assert resp.status == expected_status + await hass.async_block_till_done() + assert len(test_value) == expected_number_of_service_calls + assert await resp.json() == expected_response + + async def test_api_call_service_client_closed( hass: HomeAssistant, mock_api_client: TestClient ) -> None: From 7bc2381a453dc88b7b456be2d1e6ef415c6fb230 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 31 Jul 2024 21:24:15 +0200 Subject: [PATCH 1841/2411] Add Pinecil virtual integration supported by IronOS (#122803) --- homeassistant/components/pinecil/__init__.py | 1 + homeassistant/components/pinecil/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 homeassistant/components/pinecil/__init__.py create mode 100644 homeassistant/components/pinecil/manifest.json diff --git a/homeassistant/components/pinecil/__init__.py b/homeassistant/components/pinecil/__init__.py new file mode 100644 index 00000000000..a0e84725435 --- /dev/null +++ b/homeassistant/components/pinecil/__init__.py @@ -0,0 +1 @@ +"""Pinecil integration.""" diff --git a/homeassistant/components/pinecil/manifest.json b/homeassistant/components/pinecil/manifest.json new file mode 100644 index 00000000000..4ec6e75cfcb --- /dev/null +++ b/homeassistant/components/pinecil/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "pinecil", + "name": "Pinecil", + "integration_type": "virtual", + "supported_by": "iron_os" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 4de325a0c6e..597cd8be936 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4599,6 +4599,11 @@ "config_flow": false, "iot_class": "local_push" }, + "pinecil": { + "name": "Pinecil", + "integration_type": "virtual", + "supported_by": "iron_os" + }, "ping": { "name": "Ping (ICMP)", "integration_type": "hub", From 5fefa606b6c3aaf38e7555e18037c18adb645a9b Mon Sep 17 00:00:00 2001 From: Simon <80467011+sorgfresser@users.noreply.github.com> Date: Wed, 31 Jul 2024 21:31:09 +0200 Subject: [PATCH 1842/2411] Add ElevenLabs text-to-speech integration (#115645) * Add ElevenLabs text-to-speech integration * Remove commented out code * Use model_id instead of model_name for elevenlabs api * Apply suggestions from code review Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com> * Use async client instead of sync * Add ElevenLabs code owner * Apply suggestions from code review Co-authored-by: Paulus Schoutsen * Set entity title to voice * Rename to elevenlabs * Apply suggestions from code review Co-authored-by: Paulus Schoutsen * Allow multiple voices and options flow * Sort default voice at beginning * Rework config flow to include default model and reloading on options flow * Add error to strings * Add ElevenLabsData and suggestions from code review * Shorten options and config flow * Fix comments * Fix comments * Add wip * Fix * Cleanup * Bump elevenlabs version * Add data description * Fix --------- Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com> Co-authored-by: Paulus Schoutsen Co-authored-by: Michael Hansen Co-authored-by: Joostlek Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/elevenlabs/__init__.py | 71 +++++ .../components/elevenlabs/config_flow.py | 145 ++++++++++ homeassistant/components/elevenlabs/const.py | 7 + .../components/elevenlabs/manifest.json | 11 + .../components/elevenlabs/strings.json | 31 ++ homeassistant/components/elevenlabs/tts.py | 116 ++++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/elevenlabs/__init__.py | 1 + tests/components/elevenlabs/conftest.py | 65 +++++ tests/components/elevenlabs/const.py | 52 ++++ .../components/elevenlabs/test_config_flow.py | 94 ++++++ tests/components/elevenlabs/test_tts.py | 270 ++++++++++++++++++ 18 files changed, 889 insertions(+) create mode 100644 homeassistant/components/elevenlabs/__init__.py create mode 100644 homeassistant/components/elevenlabs/config_flow.py create mode 100644 homeassistant/components/elevenlabs/const.py create mode 100644 homeassistant/components/elevenlabs/manifest.json create mode 100644 homeassistant/components/elevenlabs/strings.json create mode 100644 homeassistant/components/elevenlabs/tts.py create mode 100644 tests/components/elevenlabs/__init__.py create mode 100644 tests/components/elevenlabs/conftest.py create mode 100644 tests/components/elevenlabs/const.py create mode 100644 tests/components/elevenlabs/test_config_flow.py create mode 100644 tests/components/elevenlabs/test_tts.py diff --git a/.strict-typing b/.strict-typing index a4f6d198d97..02d9968d247 100644 --- a/.strict-typing +++ b/.strict-typing @@ -168,6 +168,7 @@ homeassistant.components.ecowitt.* homeassistant.components.efergy.* homeassistant.components.electrasmart.* homeassistant.components.electric_kiwi.* +homeassistant.components.elevenlabs.* homeassistant.components.elgato.* homeassistant.components.elkm1.* homeassistant.components.emulated_hue.* diff --git a/CODEOWNERS b/CODEOWNERS index 4f8c03fe90f..e90def993d2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -376,6 +376,8 @@ build.json @home-assistant/supervisor /tests/components/electrasmart/ @jafar-atili /homeassistant/components/electric_kiwi/ @mikey0000 /tests/components/electric_kiwi/ @mikey0000 +/homeassistant/components/elevenlabs/ @sorgfresser +/tests/components/elevenlabs/ @sorgfresser /homeassistant/components/elgato/ @frenck /tests/components/elgato/ @frenck /homeassistant/components/elkm1/ @gwww @bdraco diff --git a/homeassistant/components/elevenlabs/__init__.py b/homeassistant/components/elevenlabs/__init__.py new file mode 100644 index 00000000000..99cddd783e2 --- /dev/null +++ b/homeassistant/components/elevenlabs/__init__.py @@ -0,0 +1,71 @@ +"""The ElevenLabs text-to-speech integration.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from elevenlabs import Model +from elevenlabs.client import AsyncElevenLabs +from elevenlabs.core import ApiError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError + +from .const import CONF_MODEL + +PLATFORMS: list[Platform] = [Platform.TTS] + + +async def get_model_by_id(client: AsyncElevenLabs, model_id: str) -> Model | None: + """Get ElevenLabs model from their API by the model_id.""" + models = await client.models.get_all() + for maybe_model in models: + if maybe_model.model_id == model_id: + return maybe_model + return None + + +@dataclass(kw_only=True, slots=True) +class ElevenLabsData: + """ElevenLabs data type.""" + + client: AsyncElevenLabs + model: Model + + +type EleventLabsConfigEntry = ConfigEntry[ElevenLabsData] + + +async def async_setup_entry(hass: HomeAssistant, entry: EleventLabsConfigEntry) -> bool: + """Set up ElevenLabs text-to-speech from a config entry.""" + entry.add_update_listener(update_listener) + client = AsyncElevenLabs(api_key=entry.data[CONF_API_KEY]) + model_id = entry.options[CONF_MODEL] + try: + model = await get_model_by_id(client, model_id) + except ApiError as err: + raise ConfigEntryError("Auth failed") from err + + if model is None or (not model.languages): + raise ConfigEntryError("Model could not be resolved") + + entry.runtime_data = ElevenLabsData(client=client, model=model) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry( + hass: HomeAssistant, entry: EleventLabsConfigEntry +) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def update_listener( + hass: HomeAssistant, config_entry: EleventLabsConfigEntry +) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/elevenlabs/config_flow.py b/homeassistant/components/elevenlabs/config_flow.py new file mode 100644 index 00000000000..cf04304510a --- /dev/null +++ b/homeassistant/components/elevenlabs/config_flow.py @@ -0,0 +1,145 @@ +"""Config flow for ElevenLabs text-to-speech integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from elevenlabs.client import AsyncElevenLabs +from elevenlabs.core import ApiError +import voluptuous as vol + +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, + OptionsFlowWithConfigEntry, +) +from homeassistant.const import CONF_API_KEY +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, +) + +from .const import CONF_MODEL, CONF_VOICE, DEFAULT_MODEL, DOMAIN + +USER_STEP_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str}) + + +_LOGGER = logging.getLogger(__name__) + + +async def get_voices_models(api_key: str) -> tuple[dict[str, str], dict[str, str]]: + """Get available voices and models as dicts.""" + client = AsyncElevenLabs(api_key=api_key) + voices = (await client.voices.get_all()).voices + models = await client.models.get_all() + voices_dict = { + voice.voice_id: voice.name + for voice in sorted(voices, key=lambda v: v.name or "") + if voice.name + } + models_dict = { + model.model_id: model.name + for model in sorted(models, key=lambda m: m.name or "") + if model.name and model.can_do_text_to_speech + } + return voices_dict, models_dict + + +class ElevenLabsConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for ElevenLabs text-to-speech.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + try: + voices, _ = await get_voices_models(user_input[CONF_API_KEY]) + except ApiError: + errors["base"] = "invalid_api_key" + else: + return self.async_create_entry( + title="ElevenLabs", + data=user_input, + options={CONF_MODEL: DEFAULT_MODEL, CONF_VOICE: list(voices)[0]}, + ) + return self.async_show_form( + step_id="user", data_schema=USER_STEP_SCHEMA, errors=errors + ) + + @staticmethod + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlow: + """Create the options flow.""" + return ElevenLabsOptionsFlow(config_entry) + + +class ElevenLabsOptionsFlow(OptionsFlowWithConfigEntry): + """ElevenLabs options flow.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + super().__init__(config_entry) + self.api_key: str = self.config_entry.data[CONF_API_KEY] + # id -> name + self.voices: dict[str, str] = {} + self.models: dict[str, str] = {} + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options.""" + if not self.voices or not self.models: + self.voices, self.models = await get_voices_models(self.api_key) + + assert self.models and self.voices + + if user_input is not None: + return self.async_create_entry( + title="ElevenLabs", + data=user_input, + ) + + schema = self.elevenlabs_config_option_schema() + return self.async_show_form( + step_id="init", + data_schema=schema, + ) + + def elevenlabs_config_option_schema(self) -> vol.Schema: + """Elevenlabs options schema.""" + return self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required( + CONF_MODEL, + ): SelectSelector( + SelectSelectorConfig( + options=[ + SelectOptionDict(label=model_name, value=model_id) + for model_id, model_name in self.models.items() + ] + ) + ), + vol.Required( + CONF_VOICE, + ): SelectSelector( + SelectSelectorConfig( + options=[ + SelectOptionDict(label=voice_name, value=voice_id) + for voice_id, voice_name in self.voices.items() + ] + ) + ), + } + ), + self.options, + ) diff --git a/homeassistant/components/elevenlabs/const.py b/homeassistant/components/elevenlabs/const.py new file mode 100644 index 00000000000..c0fc3c7b1b0 --- /dev/null +++ b/homeassistant/components/elevenlabs/const.py @@ -0,0 +1,7 @@ +"""Constants for the ElevenLabs text-to-speech integration.""" + +CONF_VOICE = "voice" +CONF_MODEL = "model" +DOMAIN = "elevenlabs" + +DEFAULT_MODEL = "eleven_multilingual_v2" diff --git a/homeassistant/components/elevenlabs/manifest.json b/homeassistant/components/elevenlabs/manifest.json new file mode 100644 index 00000000000..968ea7b688a --- /dev/null +++ b/homeassistant/components/elevenlabs/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "elevenlabs", + "name": "ElevenLabs", + "codeowners": ["@sorgfresser"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/elevenlabs", + "integration_type": "service", + "iot_class": "cloud_polling", + "loggers": ["elevenlabs"], + "requirements": ["elevenlabs==1.6.1"] +} diff --git a/homeassistant/components/elevenlabs/strings.json b/homeassistant/components/elevenlabs/strings.json new file mode 100644 index 00000000000..16b40137090 --- /dev/null +++ b/homeassistant/components/elevenlabs/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "api_key": "Your Elevenlabs API key." + } + } + }, + "error": { + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "voice": "Voice", + "model": "Model" + }, + "data_description": { + "voice": "Voice to use for the TTS.", + "model": "ElevenLabs model to use. Please note that not all models support all languages equally well." + } + } + } + } +} diff --git a/homeassistant/components/elevenlabs/tts.py b/homeassistant/components/elevenlabs/tts.py new file mode 100644 index 00000000000..35ba6053cd8 --- /dev/null +++ b/homeassistant/components/elevenlabs/tts.py @@ -0,0 +1,116 @@ +"""Support for the ElevenLabs text-to-speech service.""" + +from __future__ import annotations + +import logging +from typing import Any + +from elevenlabs.client import AsyncElevenLabs +from elevenlabs.core import ApiError +from elevenlabs.types import Model, Voice as ElevenLabsVoice + +from homeassistant.components.tts import ( + ATTR_VOICE, + TextToSpeechEntity, + TtsAudioType, + Voice, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import EleventLabsConfigEntry +from .const import CONF_VOICE, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: EleventLabsConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ElevenLabs tts platform via config entry.""" + client = config_entry.runtime_data.client + voices = (await client.voices.get_all()).voices + default_voice_id = config_entry.options[CONF_VOICE] + async_add_entities( + [ + ElevenLabsTTSEntity( + client, + config_entry.runtime_data.model, + voices, + default_voice_id, + config_entry.entry_id, + config_entry.title, + ) + ] + ) + + +class ElevenLabsTTSEntity(TextToSpeechEntity): + """The ElevenLabs API entity.""" + + _attr_supported_options = [ATTR_VOICE] + + def __init__( + self, + client: AsyncElevenLabs, + model: Model, + voices: list[ElevenLabsVoice], + default_voice_id: str, + entry_id: str, + title: str, + ) -> None: + """Init ElevenLabs TTS service.""" + self._client = client + self._model = model + self._default_voice_id = default_voice_id + self._voices = sorted( + (Voice(v.voice_id, v.name) for v in voices if v.name), + key=lambda v: v.name, + ) + # Default voice first + voice_indices = [ + idx for idx, v in enumerate(self._voices) if v.voice_id == default_voice_id + ] + if voice_indices: + self._voices.insert(0, self._voices.pop(voice_indices[0])) + self._attr_unique_id = entry_id + self._attr_name = title + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry_id)}, + manufacturer="ElevenLabs", + model=model.name, + entry_type=DeviceEntryType.SERVICE, + ) + self._attr_supported_languages = [ + lang.language_id for lang in self._model.languages or [] + ] + self._attr_default_language = self.supported_languages[0] + + def async_get_supported_voices(self, language: str) -> list[Voice]: + """Return a list of supported voices for a language.""" + return self._voices + + async def async_get_tts_audio( + self, message: str, language: str, options: dict[str, Any] + ) -> TtsAudioType: + """Load tts audio file from the engine.""" + _LOGGER.debug("Getting TTS audio for %s", message) + _LOGGER.debug("Options: %s", options) + voice_id = options[ATTR_VOICE] + try: + audio = await self._client.generate( + text=message, + voice=voice_id, + model=self._model.model_id, + ) + bytes_combined = b"".join([byte_seg async for byte_seg in audio]) + except ApiError as exc: + _LOGGER.warning( + "Error during processing of TTS request %s", exc, exc_info=True + ) + raise HomeAssistantError(exc) from exc + return "mp3", bytes_combined diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d350a58f3c6..0c37cf9c412 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -150,6 +150,7 @@ FLOWS = { "efergy", "electrasmart", "electric_kiwi", + "elevenlabs", "elgato", "elkm1", "elmax", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 597cd8be936..3cc3ea71df9 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1516,6 +1516,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "elevenlabs": { + "name": "ElevenLabs", + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling" + }, "elgato": { "name": "Elgato", "integrations": { diff --git a/mypy.ini b/mypy.ini index dd7904d798b..0f4f8907612 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1436,6 +1436,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.elevenlabs.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.elgato.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 2cae4548956..d00605c9893 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -779,6 +779,9 @@ ecoaliface==0.4.0 # homeassistant.components.electric_kiwi electrickiwi-api==0.8.5 +# homeassistant.components.elevenlabs +elevenlabs==1.6.1 + # homeassistant.components.elgato elgato==5.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d2f345cf1a..fd200794990 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -660,6 +660,9 @@ easyenergy==2.1.2 # homeassistant.components.electric_kiwi electrickiwi-api==0.8.5 +# homeassistant.components.elevenlabs +elevenlabs==1.6.1 + # homeassistant.components.elgato elgato==5.1.2 diff --git a/tests/components/elevenlabs/__init__.py b/tests/components/elevenlabs/__init__.py new file mode 100644 index 00000000000..261286f04f7 --- /dev/null +++ b/tests/components/elevenlabs/__init__.py @@ -0,0 +1 @@ +"""Tests for the ElevenLabs integration.""" diff --git a/tests/components/elevenlabs/conftest.py b/tests/components/elevenlabs/conftest.py new file mode 100644 index 00000000000..13eb022243f --- /dev/null +++ b/tests/components/elevenlabs/conftest.py @@ -0,0 +1,65 @@ +"""Common fixtures for the ElevenLabs text-to-speech tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from elevenlabs.core import ApiError +from elevenlabs.types import GetVoicesResponse +import pytest + +from homeassistant.components.elevenlabs.const import CONF_MODEL, CONF_VOICE +from homeassistant.const import CONF_API_KEY + +from .const import MOCK_MODELS, MOCK_VOICES + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.elevenlabs.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_async_client() -> Generator[AsyncMock, None, None]: + """Override async ElevenLabs client.""" + client_mock = AsyncMock() + client_mock.voices.get_all.return_value = GetVoicesResponse(voices=MOCK_VOICES) + client_mock.models.get_all.return_value = MOCK_MODELS + with patch( + "elevenlabs.client.AsyncElevenLabs", return_value=client_mock + ) as mock_async_client: + yield mock_async_client + + +@pytest.fixture +def mock_async_client_fail() -> Generator[AsyncMock, None, None]: + """Override async ElevenLabs client.""" + with patch( + "homeassistant.components.elevenlabs.config_flow.AsyncElevenLabs", + return_value=AsyncMock(), + ) as mock_async_client: + mock_async_client.side_effect = ApiError + yield mock_async_client + + +@pytest.fixture +def mock_entry() -> MockConfigEntry: + """Mock a config entry.""" + entry = MockConfigEntry( + domain="elevenlabs", + data={ + CONF_API_KEY: "api_key", + }, + options={CONF_MODEL: "model1", CONF_VOICE: "voice1"}, + ) + entry.models = { + "model1": "model1", + } + + entry.voices = {"voice1": "voice1"} + return entry diff --git a/tests/components/elevenlabs/const.py b/tests/components/elevenlabs/const.py new file mode 100644 index 00000000000..e16e1fd1334 --- /dev/null +++ b/tests/components/elevenlabs/const.py @@ -0,0 +1,52 @@ +"""Constants for the Testing of the ElevenLabs text-to-speech integration.""" + +from elevenlabs.types import LanguageResponse, Model, Voice + +from homeassistant.components.elevenlabs.const import DEFAULT_MODEL + +MOCK_VOICES = [ + Voice( + voice_id="voice1", + name="Voice 1", + ), + Voice( + voice_id="voice2", + name="Voice 2", + ), +] + +MOCK_MODELS = [ + Model( + model_id="model1", + name="Model 1", + can_do_text_to_speech=True, + languages=[ + LanguageResponse(language_id="en", name="English"), + LanguageResponse(language_id="de", name="German"), + LanguageResponse(language_id="es", name="Spanish"), + LanguageResponse(language_id="ja", name="Japanese"), + ], + ), + Model( + model_id="model2", + name="Model 2", + can_do_text_to_speech=True, + languages=[ + LanguageResponse(language_id="en", name="English"), + LanguageResponse(language_id="de", name="German"), + LanguageResponse(language_id="es", name="Spanish"), + LanguageResponse(language_id="ja", name="Japanese"), + ], + ), + Model( + model_id=DEFAULT_MODEL, + name=DEFAULT_MODEL, + can_do_text_to_speech=True, + languages=[ + LanguageResponse(language_id="en", name="English"), + LanguageResponse(language_id="de", name="German"), + LanguageResponse(language_id="es", name="Spanish"), + LanguageResponse(language_id="ja", name="Japanese"), + ], + ), +] diff --git a/tests/components/elevenlabs/test_config_flow.py b/tests/components/elevenlabs/test_config_flow.py new file mode 100644 index 00000000000..853c49d48ff --- /dev/null +++ b/tests/components/elevenlabs/test_config_flow.py @@ -0,0 +1,94 @@ +"""Test the ElevenLabs text-to-speech config flow.""" + +from unittest.mock import AsyncMock + +from homeassistant.components.elevenlabs.const import ( + CONF_MODEL, + CONF_VOICE, + DEFAULT_MODEL, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_user_step( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_async_client: AsyncMock, +) -> None: + """Test user step create entry result.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "api_key", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "ElevenLabs" + assert result["data"] == { + "api_key": "api_key", + } + assert result["options"] == {CONF_MODEL: DEFAULT_MODEL, CONF_VOICE: "voice1"} + + mock_setup_entry.assert_called_once() + + +async def test_invalid_api_key( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_async_client_fail: AsyncMock +) -> None: + """Test user step with invalid api key.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "api_key", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] + + mock_setup_entry.assert_not_called() + + +async def test_options_flow_init( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_async_client: AsyncMock, + mock_entry: MockConfigEntry, +) -> None: + """Test options flow init.""" + mock_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(mock_entry.entry_id) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_MODEL: "model1", CONF_VOICE: "voice1"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert mock_entry.options == {CONF_MODEL: "model1", CONF_VOICE: "voice1"} + + mock_setup_entry.assert_called_once() diff --git a/tests/components/elevenlabs/test_tts.py b/tests/components/elevenlabs/test_tts.py new file mode 100644 index 00000000000..7fa289f24ed --- /dev/null +++ b/tests/components/elevenlabs/test_tts.py @@ -0,0 +1,270 @@ +"""Tests for the ElevenLabs TTS entity.""" + +from __future__ import annotations + +from http import HTTPStatus +from typing import Any +from unittest.mock import AsyncMock, patch + +from elevenlabs.core import ApiError +from elevenlabs.types import GetVoicesResponse +import pytest + +from homeassistant.components import tts +from homeassistant.components.elevenlabs.const import CONF_MODEL, CONF_VOICE, DOMAIN +from homeassistant.components.media_player import ( + ATTR_MEDIA_CONTENT_ID, + DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, +) +from homeassistant.config import async_process_ha_core_config +from homeassistant.const import ATTR_ENTITY_ID, CONF_API_KEY +from homeassistant.core import HomeAssistant, ServiceCall + +from .const import MOCK_MODELS, MOCK_VOICES + +from tests.common import MockConfigEntry, async_mock_service +from tests.components.tts.common import retrieve_media +from tests.typing import ClientSessionGenerator + + +@pytest.fixture(autouse=True) +def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock): + """Mock writing tags.""" + + +@pytest.fixture(autouse=True) +def mock_tts_cache_dir_autouse(mock_tts_cache_dir): + """Mock the TTS cache dir with empty dir.""" + return mock_tts_cache_dir + + +@pytest.fixture +async def calls(hass: HomeAssistant) -> list[ServiceCall]: + """Mock media player calls.""" + return async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + +@pytest.fixture(autouse=True) +async def setup_internal_url(hass: HomeAssistant) -> None: + """Set up internal url.""" + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"} + ) + + +@pytest.fixture(name="setup") +async def setup_fixture( + hass: HomeAssistant, + config_data: dict[str, Any], + config_options: dict[str, Any], + request: pytest.FixtureRequest, + mock_async_client: AsyncMock, +) -> AsyncMock: + """Set up the test environment.""" + if request.param == "mock_config_entry_setup": + await mock_config_entry_setup(hass, config_data, config_options) + else: + raise RuntimeError("Invalid setup fixture") + + await hass.async_block_till_done() + return mock_async_client + + +@pytest.fixture(name="config_data") +def config_data_fixture() -> dict[str, Any]: + """Return config data.""" + return {} + + +@pytest.fixture(name="config_options") +def config_options_fixture() -> dict[str, Any]: + """Return config options.""" + return {} + + +async def mock_config_entry_setup( + hass: HomeAssistant, config_data: dict[str, Any], config_options: dict[str, Any] +) -> None: + """Mock config entry setup.""" + default_config_data = { + CONF_API_KEY: "api_key", + } + default_config_options = { + CONF_VOICE: "voice1", + CONF_MODEL: "model1", + } + config_entry = MockConfigEntry( + domain=DOMAIN, + data=default_config_data | config_data, + options=default_config_options | config_options, + ) + config_entry.add_to_hass(hass) + client_mock = AsyncMock() + client_mock.voices.get_all.return_value = GetVoicesResponse(voices=MOCK_VOICES) + client_mock.models.get_all.return_value = MOCK_MODELS + with patch( + "homeassistant.components.elevenlabs.AsyncElevenLabs", return_value=client_mock + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + + +@pytest.mark.parametrize( + "config_data", + [ + {}, + {tts.CONF_LANG: "de"}, + {tts.CONF_LANG: "en"}, + {tts.CONF_LANG: "ja"}, + {tts.CONF_LANG: "es"}, + ], +) +@pytest.mark.parametrize( + ("setup", "tts_service", "service_data"), + [ + ( + "mock_config_entry_setup", + "speak", + { + ATTR_ENTITY_ID: "tts.mock_title", + tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + tts.ATTR_OPTIONS: {tts.ATTR_VOICE: "voice2"}, + }, + ), + ], + indirect=["setup"], +) +async def test_tts_service_speak( + setup: AsyncMock, + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + calls: list[ServiceCall], + tts_service: str, + service_data: dict[str, Any], +) -> None: + """Test tts service.""" + tts_entity = hass.data[tts.DOMAIN].get_entity(service_data[ATTR_ENTITY_ID]) + tts_entity._client.generate.reset_mock() + + await hass.services.async_call( + tts.DOMAIN, + tts_service, + service_data, + blocking=True, + ) + + assert len(calls) == 1 + assert ( + await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + == HTTPStatus.OK + ) + + tts_entity._client.generate.assert_called_once_with( + text="There is a person at the front door.", voice="voice2", model="model1" + ) + + +@pytest.mark.parametrize( + ("setup", "tts_service", "service_data"), + [ + ( + "mock_config_entry_setup", + "speak", + { + ATTR_ENTITY_ID: "tts.mock_title", + tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + tts.ATTR_LANGUAGE: "de", + tts.ATTR_OPTIONS: {tts.ATTR_VOICE: "voice1"}, + }, + ), + ( + "mock_config_entry_setup", + "speak", + { + ATTR_ENTITY_ID: "tts.mock_title", + tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + tts.ATTR_LANGUAGE: "es", + tts.ATTR_OPTIONS: {tts.ATTR_VOICE: "voice1"}, + }, + ), + ], + indirect=["setup"], +) +async def test_tts_service_speak_lang_config( + setup: AsyncMock, + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + calls: list[ServiceCall], + tts_service: str, + service_data: dict[str, Any], +) -> None: + """Test service call say with other langcodes in the config.""" + tts_entity = hass.data[tts.DOMAIN].get_entity(service_data[ATTR_ENTITY_ID]) + tts_entity._client.generate.reset_mock() + + await hass.services.async_call( + tts.DOMAIN, + tts_service, + service_data, + blocking=True, + ) + + assert len(calls) == 1 + assert ( + await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + == HTTPStatus.OK + ) + + tts_entity._client.generate.assert_called_once_with( + text="There is a person at the front door.", voice="voice1", model="model1" + ) + + +@pytest.mark.parametrize( + ("setup", "tts_service", "service_data"), + [ + ( + "mock_config_entry_setup", + "speak", + { + ATTR_ENTITY_ID: "tts.mock_title", + tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + tts.ATTR_OPTIONS: {tts.ATTR_VOICE: "voice1"}, + }, + ), + ], + indirect=["setup"], +) +async def test_tts_service_speak_error( + setup: AsyncMock, + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + calls: list[ServiceCall], + tts_service: str, + service_data: dict[str, Any], +) -> None: + """Test service call say with http response 400.""" + tts_entity = hass.data[tts.DOMAIN].get_entity(service_data[ATTR_ENTITY_ID]) + tts_entity._client.generate.reset_mock() + tts_entity._client.generate.side_effect = ApiError + + await hass.services.async_call( + tts.DOMAIN, + tts_service, + service_data, + blocking=True, + ) + + assert len(calls) == 1 + assert ( + await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + == HTTPStatus.NOT_FOUND + ) + + tts_entity._client.generate.assert_called_once_with( + text="There is a person at the front door.", voice="voice1", model="model1" + ) From 6baee603a5d1396e78bfc9a384ed5545be0d34e0 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 31 Jul 2024 15:10:50 -0500 Subject: [PATCH 1843/2411] Bump pymicro-vad to 1.0.1 (#122973) --- homeassistant/components/assist_pipeline/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/assist_pipeline/manifest.json b/homeassistant/components/assist_pipeline/manifest.json index b22ce72b1eb..dd3ec77f165 100644 --- a/homeassistant/components/assist_pipeline/manifest.json +++ b/homeassistant/components/assist_pipeline/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/assist_pipeline", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["pymicro-vad==1.0.0"] + "requirements": ["pymicro-vad==1.0.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c7e5528d675..58f39907269 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -45,7 +45,7 @@ Pillow==10.4.0 pip>=21.3.1 psutil-home-assistant==0.0.1 PyJWT==2.8.0 -pymicro-vad==1.0.0 +pymicro-vad==1.0.1 PyNaCl==1.5.0 pyOpenSSL==24.2.1 pyserial==3.5 diff --git a/requirements_all.txt b/requirements_all.txt index d00605c9893..ad08342230d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2011,7 +2011,7 @@ pymelcloud==2.5.9 pymeteoclimatic==0.1.0 # homeassistant.components.assist_pipeline -pymicro-vad==1.0.0 +pymicro-vad==1.0.1 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd200794990..5bdf58ff217 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1607,7 +1607,7 @@ pymelcloud==2.5.9 pymeteoclimatic==0.1.0 # homeassistant.components.assist_pipeline -pymicro-vad==1.0.0 +pymicro-vad==1.0.1 # homeassistant.components.mochad pymochad==0.2.0 From 18afe07c169e05fe818e8eb5f7af2ec3303c3aed Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 31 Jul 2024 22:38:50 +0200 Subject: [PATCH 1844/2411] Bump version to 2024.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 7d58bdb1e94..b315d8c2618 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index f71e9bd6013..e705101e4ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0.dev0" +version = "2024.8.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From bdd6f579753a75d32ab206d9ac940f157530c53d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 31 Jul 2024 23:24:30 +0200 Subject: [PATCH 1845/2411] Bump version to 2024.9.0dev0 (#122975) --- .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 b705064e078..96ba319fdb9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -36,7 +36,7 @@ env: CACHE_VERSION: 9 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 8 - HA_SHORT_VERSION: "2024.8" + HA_SHORT_VERSION: "2024.9" DEFAULT_PYTHON: "3.12" ALL_PYTHON_VERSIONS: "['3.12']" # 10.3 is the oldest supported version diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d58bdb1e94..389aaf8fef3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 -MINOR_VERSION: Final = 8 +MINOR_VERSION: Final = 9 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/pyproject.toml b/pyproject.toml index f71e9bd6013..a9559b9e367 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0.dev0" +version = "2024.9.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 352f0953f3c0433d4b541d0beeb061d298925c42 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Aug 2024 01:00:17 +0200 Subject: [PATCH 1846/2411] Skip binary wheels for pymicro-vad (#122982) --- .github/workflows/wheels.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index b74406b9c82..697535172d7 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -211,7 +211,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" - skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic + skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_old-cython.txt" @@ -226,7 +226,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" - skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic + skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtaa" @@ -240,7 +240,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" - skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic + skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtab" @@ -254,7 +254,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" - skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic + skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txtac" From 8375b58eac5ea05fabfdf6ac74f774794793beef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Aug 2024 01:31:22 -0500 Subject: [PATCH 1847/2411] Update doorbird error notification to be a repair flow (#122987) --- homeassistant/components/doorbird/__init__.py | 37 ++++++----- .../components/doorbird/manifest.json | 2 +- homeassistant/components/doorbird/repairs.py | 55 +++++++++++++++++ .../components/doorbird/strings.json | 13 ++++ tests/components/doorbird/test_repairs.py | 61 +++++++++++++++++++ 5 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/doorbird/repairs.py create mode 100644 tests/components/doorbird/test_repairs.py diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 8989e0ec0be..113b8031d9b 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -3,11 +3,11 @@ from __future__ import annotations from http import HTTPStatus +import logging from aiohttp import ClientResponseError from doorbirdpy import DoorBird -from homeassistant.components import persistent_notification from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -17,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -30,6 +31,8 @@ CONF_CUSTOM_URL = "hass_url_override" CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the DoorBird component.""" @@ -68,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> door_bird_data = DoorBirdData(door_station, info, event_entity_ids) door_station.update_events(events) # Subscribe to doorbell or motion events - if not await _async_register_events(hass, door_station): + if not await _async_register_events(hass, door_station, entry): raise ConfigEntryNotReady entry.async_on_unload(entry.add_update_listener(_update_listener)) @@ -84,24 +87,30 @@ async def async_unload_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> async def _async_register_events( - hass: HomeAssistant, door_station: ConfiguredDoorBird + hass: HomeAssistant, door_station: ConfiguredDoorBird, entry: DoorBirdConfigEntry ) -> bool: """Register events on device.""" + issue_id = f"doorbird_schedule_error_{entry.entry_id}" try: await door_station.async_register_events() - except ClientResponseError: - persistent_notification.async_create( + except ClientResponseError as ex: + ir.async_create_issue( hass, - ( - "Doorbird configuration failed. Please verify that API " - "Operator permission is enabled for the Doorbird user. " - "A restart will be required once permissions have been " - "verified." - ), - title="Doorbird Configuration Failure", - notification_id="doorbird_schedule_error", + DOMAIN, + issue_id, + severity=ir.IssueSeverity.ERROR, + translation_key="error_registering_events", + data={"entry_id": entry.entry_id}, + is_fixable=True, + translation_placeholders={ + "error": str(ex), + "name": door_station.name or entry.data[CONF_NAME], + }, ) + _LOGGER.debug("Error registering DoorBird events", exc_info=True) return False + else: + ir.async_delete_issue(hass, DOMAIN, issue_id) return True @@ -111,4 +120,4 @@ async def _update_listener(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> N door_station = entry.runtime_data.door_station door_station.update_events(entry.options[CONF_EVENTS]) # Subscribe to doorbell or motion events - await _async_register_events(hass, door_station) + await _async_register_events(hass, door_station, entry) diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index e77f9aaf0a4..0e9f03c8ef8 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -3,7 +3,7 @@ "name": "DoorBird", "codeowners": ["@oblogic7", "@bdraco", "@flacjacket"], "config_flow": true, - "dependencies": ["http"], + "dependencies": ["http", "repairs"], "documentation": "https://www.home-assistant.io/integrations/doorbird", "iot_class": "local_push", "loggers": ["doorbirdpy"], diff --git a/homeassistant/components/doorbird/repairs.py b/homeassistant/components/doorbird/repairs.py new file mode 100644 index 00000000000..c8f9b73ecbd --- /dev/null +++ b/homeassistant/components/doorbird/repairs.py @@ -0,0 +1,55 @@ +"""Repairs for DoorBird.""" + +from __future__ import annotations + +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import RepairsFlow +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + + +class DoorBirdReloadConfirmRepairFlow(RepairsFlow): + """Handler to show doorbird error and reload.""" + + def __init__(self, entry_id: str) -> None: + """Initialize the flow.""" + self.entry_id = entry_id + + 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: + self.hass.config_entries.async_schedule_reload(self.entry_id) + return self.async_create_entry(data={}) + + issue_registry = ir.async_get(self.hass) + description_placeholders = None + if issue := issue_registry.async_get_issue(self.handler, self.issue_id): + description_placeholders = issue.translation_placeholders + + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema({}), + description_placeholders=description_placeholders, + ) + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: + """Create flow.""" + assert data is not None + entry_id = data["entry_id"] + assert isinstance(entry_id, str) + return DoorBirdReloadConfirmRepairFlow(entry_id=entry_id) diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index 29c85ec7311..090ba4f161f 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -11,6 +11,19 @@ } } }, + "issues": { + "error_registering_events": { + "title": "DoorBird {name} configuration failure", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::doorbird::issues::error_registering_events::title%]", + "description": "Configuring DoorBird {name} failed with error: `{error}`. Please enable the API Operator permission for the DoorBird user and continue to reload the integration." + } + } + } + } + }, "config": { "step": { "user": { diff --git a/tests/components/doorbird/test_repairs.py b/tests/components/doorbird/test_repairs.py new file mode 100644 index 00000000000..7449250b718 --- /dev/null +++ b/tests/components/doorbird/test_repairs.py @@ -0,0 +1,61 @@ +"""Test repairs for doorbird.""" + +from __future__ import annotations + +from http import HTTPStatus + +from homeassistant.components.doorbird.const import DOMAIN +from homeassistant.components.repairs.issue_handler import ( + async_process_repairs_platforms, +) +from homeassistant.components.repairs.websocket_api import ( + RepairsFlowIndexView, + RepairsFlowResourceView, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + +from . import mock_not_found_exception +from .conftest import DoorbirdMockerType + +from tests.typing import ClientSessionGenerator + + +async def test_change_schedule_fails( + hass: HomeAssistant, + doorbird_mocker: DoorbirdMockerType, + hass_client: ClientSessionGenerator, +) -> None: + """Test a doorbird when change_schedule fails.""" + assert await async_setup_component(hass, "repairs", {}) + doorbird_entry = await doorbird_mocker( + favorites_side_effect=mock_not_found_exception() + ) + assert doorbird_entry.entry.state is ConfigEntryState.SETUP_RETRY + issue_reg = ir.async_get(hass) + assert len(issue_reg.issues) == 1 + issue = list(issue_reg.issues.values())[0] + issue_id = issue.issue_id + assert issue.domain == DOMAIN + + await async_process_repairs_platforms(hass) + client = await hass_client() + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": issue_id}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + placeholders = data["description_placeholders"] + assert "404" in placeholders["error"] + assert data["step_id"] == "confirm" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" From 55e542844317d5488be01b51878b241327d0d137 Mon Sep 17 00:00:00 2001 From: Matrix Date: Thu, 1 Aug 2024 14:32:16 +0800 Subject: [PATCH 1848/2411] Fix yolink protocol changed (#122989) --- homeassistant/components/yolink/valve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/yolink/valve.py b/homeassistant/components/yolink/valve.py index a24ad7d385d..d8c199697c3 100644 --- a/homeassistant/components/yolink/valve.py +++ b/homeassistant/components/yolink/valve.py @@ -37,7 +37,7 @@ DEVICE_TYPES: tuple[YoLinkValveEntityDescription, ...] = ( key="valve_state", translation_key="meter_valve_state", device_class=ValveDeviceClass.WATER, - value=lambda value: value == "closed" if value is not None else None, + value=lambda value: value != "open" if value is not None else None, exists_fn=lambda device: device.device_type == ATTR_DEVICE_WATER_METER_CONTROLLER and not device.device_model_name.startswith(DEV_MODEL_WATER_METER_YS5007), From 2fd3c42e633bfb267604229876bd4846e93e8a3b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 1 Aug 2024 09:19:16 +0200 Subject: [PATCH 1849/2411] Fix implicit-return in squeezebox (#122928) --- homeassistant/components/squeezebox/config_flow.py | 6 ++++-- homeassistant/components/squeezebox/media_player.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index 9ccac13223b..95af3e8032a 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -3,7 +3,7 @@ import asyncio from http import HTTPStatus import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from pysqueezebox import Server, async_discover import voluptuous as vol @@ -102,7 +102,7 @@ class SqueezeboxConfigFlow(ConfigFlow, domain=DOMAIN): # update with suggested values from discovery self.data_schema = _base_schema(self.discovery_info) - async def _validate_input(self, data): + async def _validate_input(self, data: dict[str, Any]) -> str | None: """Validate the user input allows us to connect. Retrieve unique id and abort if already configured. @@ -129,6 +129,8 @@ class SqueezeboxConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(status["uuid"]) self._abort_if_unique_id_configured() + return None + async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" errors = {} diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index aaf64c34ddf..552b8ed800c 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -281,10 +281,11 @@ class SqueezeBoxEntity(MediaPlayerEntity): self.hass.data[DOMAIN][KNOWN_PLAYERS].remove(self) @property - def volume_level(self): + def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" if self._player.volume: return int(float(self._player.volume)) / 100.0 + return None @property def is_volume_muted(self): From d16a2fac80b5e3e897442bef786d89e4aa393a94 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 1 Aug 2024 09:20:21 +0200 Subject: [PATCH 1850/2411] Rename variable in async tests (#122996) --- tests/util/test_async.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/util/test_async.py b/tests/util/test_async.py index ac927b1375a..373768788b7 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -14,7 +14,9 @@ from tests.common import extract_stack_to_frame @patch("concurrent.futures.Future") @patch("threading.get_ident") -def test_run_callback_threadsafe_from_inside_event_loop(mock_ident, _) -> None: +def test_run_callback_threadsafe_from_inside_event_loop( + mock_ident: MagicMock, mock_future: MagicMock +) -> None: """Testing calling run_callback_threadsafe from inside an event loop.""" callback = MagicMock() From 25d4dd82a05c6587134709c32c5bb46f16495ae8 Mon Sep 17 00:00:00 2001 From: Ivan Belokobylskiy Date: Thu, 1 Aug 2024 12:51:41 +0400 Subject: [PATCH 1851/2411] Bump aioymaps to 1.2.5 (#123005) Bump aiomaps, fix sessionId parsing --- homeassistant/components/yandex_transport/manifest.json | 2 +- homeassistant/components/yandex_transport/sensor.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index c29b4d3dc98..1d1219d5a95 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@rishatik92", "@devbis"], "documentation": "https://www.home-assistant.io/integrations/yandex_transport", "iot_class": "cloud_polling", - "requirements": ["aioymaps==1.2.4"] + "requirements": ["aioymaps==1.2.5"] } diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 30227e3261e..95c4785a341 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import timedelta import logging -from aioymaps import CaptchaError, YandexMapsRequester +from aioymaps import CaptchaError, NoSessionError, YandexMapsRequester import voluptuous as vol from homeassistant.components.sensor import ( @@ -88,7 +88,7 @@ class DiscoverYandexTransport(SensorEntity): closer_time = None try: yandex_reply = await self.requester.get_stop_info(self._stop_id) - except CaptchaError as ex: + except (CaptchaError, NoSessionError) as ex: _LOGGER.error( "%s. You may need to disable the integration for some time", ex, diff --git a/requirements_all.txt b/requirements_all.txt index ad08342230d..3bd6cfa8b2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -407,7 +407,7 @@ aiowebostv==0.4.2 aiowithings==3.0.2 # homeassistant.components.yandex_transport -aioymaps==1.2.4 +aioymaps==1.2.5 # homeassistant.components.airgradient airgradient==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bdf58ff217..21c8fd5f677 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -389,7 +389,7 @@ aiowebostv==0.4.2 aiowithings==3.0.2 # homeassistant.components.yandex_transport -aioymaps==1.2.4 +aioymaps==1.2.5 # homeassistant.components.airgradient airgradient==0.7.1 From cd80cd5caafbfb45b4ddc03bdfcc2f4fad732eb4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 1 Aug 2024 11:30:29 +0200 Subject: [PATCH 1852/2411] Update audit licenses run conditions [ci] (#123009) --- .core_files.yaml | 1 + .github/workflows/ci.yaml | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.core_files.yaml b/.core_files.yaml index 08cabb71164..3f92ed87a84 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -146,6 +146,7 @@ requirements: &requirements - homeassistant/package_constraints.txt - requirements*.txt - pyproject.toml + - script/licenses.py any: - *base_platforms diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 96ba319fdb9..23a8b10c0d3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,6 +31,10 @@ on: description: "Only run mypy" default: false type: boolean + audit-licenses-only: + description: "Only run audit licenses" + default: false + type: boolean env: CACHE_VERSION: 9 @@ -222,6 +226,7 @@ jobs: if: | github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' + && github.event.inputs.audit-licenses-only != 'true' needs: - info steps: @@ -343,6 +348,7 @@ jobs: pre-commit run --hook-stage manual ruff --all-files --show-diff-on-failure env: RUFF_OUTPUT_FORMAT: github + lint-other: name: Check other linters runs-on: ubuntu-24.04 @@ -518,6 +524,7 @@ jobs: if: | github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' + && github.event.inputs.audit-licenses-only != 'true' needs: - info - base @@ -556,6 +563,7 @@ jobs: if: | github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' + && github.event.inputs.audit-licenses-only != 'true' needs: - info - base @@ -589,7 +597,10 @@ jobs: - info - base if: | - needs.info.outputs.requirements == 'true' + (github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' + || github.event.inputs.audit-licenses-only == 'true') + && needs.info.outputs.requirements == 'true' steps: - name: Check out code from GitHub uses: actions/checkout@v4.1.7 @@ -628,6 +639,7 @@ jobs: timeout-minutes: 20 if: | github.event.inputs.mypy-only != 'true' + && github.event.inputs.audit-licenses-only != 'true' || github.event.inputs.pylint-only == 'true' needs: - info @@ -672,7 +684,9 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 20 if: | - (github.event.inputs.mypy-only != 'true' || github.event.inputs.pylint-only == 'true') + (github.event.inputs.mypy-only != 'true' + && github.event.inputs.audit-licenses-only != 'true' + || github.event.inputs.pylint-only == 'true') && (needs.info.outputs.tests_glob || needs.info.outputs.test_full_suite == 'true') needs: - info @@ -717,6 +731,7 @@ jobs: runs-on: ubuntu-24.04 if: | github.event.inputs.pylint-only != 'true' + && github.event.inputs.audit-licenses-only != 'true' || github.event.inputs.mypy-only == 'true' needs: - info @@ -781,6 +796,7 @@ jobs: && github.event.inputs.lint-only != 'true' && github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' + && github.event.inputs.audit-licenses-only != 'true' && needs.info.outputs.test_full_suite == 'true' needs: - info @@ -831,6 +847,7 @@ jobs: && github.event.inputs.lint-only != 'true' && github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' + && github.event.inputs.audit-licenses-only != 'true' && needs.info.outputs.test_full_suite == 'true' needs: - info @@ -951,6 +968,7 @@ jobs: && github.event.inputs.lint-only != 'true' && github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' + && github.event.inputs.audit-licenses-only != 'true' && needs.info.outputs.mariadb_groups != '[]' needs: - info @@ -1076,6 +1094,7 @@ jobs: && github.event.inputs.lint-only != 'true' && github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' + && github.event.inputs.audit-licenses-only != 'true' && needs.info.outputs.postgresql_groups != '[]' needs: - info @@ -1220,6 +1239,7 @@ jobs: && github.event.inputs.lint-only != 'true' && github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' + && github.event.inputs.audit-licenses-only != 'true' && needs.info.outputs.tests_glob && needs.info.outputs.test_full_suite == 'false' needs: From 6bf59a8dfcfe357ed94fca219b2d64563fe8dbdf Mon Sep 17 00:00:00 2001 From: Matrix Date: Thu, 1 Aug 2024 17:49:58 +0800 Subject: [PATCH 1853/2411] Bump yolink api to 0.4.6 (#123012) --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 5353d5d5b8c..ceb4e4ceff3 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.4.4"] + "requirements": ["yolink-api==0.4.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3bd6cfa8b2a..0bf4b77e9d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2962,7 +2962,7 @@ yeelight==0.7.14 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.4.4 +yolink-api==0.4.6 # homeassistant.components.youless youless-api==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21c8fd5f677..f9670987b70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2339,7 +2339,7 @@ yalexs==6.4.3 yeelight==0.7.14 # homeassistant.components.yolink -yolink-api==0.4.4 +yolink-api==0.4.6 # homeassistant.components.youless youless-api==2.1.2 From bc91bd3293a78133f5781b3422dfd1e1f86b97fa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Aug 2024 11:51:45 +0200 Subject: [PATCH 1854/2411] Make the Android timer notification high priority (#123006) --- homeassistant/components/mobile_app/timers.py | 2 ++ tests/components/mobile_app/test_timers.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/mobile_app/timers.py b/homeassistant/components/mobile_app/timers.py index 93b4ac53be5..e092298c5d7 100644 --- a/homeassistant/components/mobile_app/timers.py +++ b/homeassistant/components/mobile_app/timers.py @@ -39,6 +39,8 @@ def async_handle_timer_event( # Android "channel": "Timers", "importance": "high", + "ttl": 0, + "priority": "high", # iOS "push": { "interruption-level": "time-sensitive", diff --git a/tests/components/mobile_app/test_timers.py b/tests/components/mobile_app/test_timers.py index 0eba88f7328..9f7d4cebc58 100644 --- a/tests/components/mobile_app/test_timers.py +++ b/tests/components/mobile_app/test_timers.py @@ -61,6 +61,8 @@ async def test_timer_events( "channel": "Timers", "group": "timers", "importance": "high", + "ttl": 0, + "priority": "high", "push": { "interruption-level": "time-sensitive", }, From c2a23bce5015961f2935a5aa90170c39dccc654b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:20:05 +0200 Subject: [PATCH 1855/2411] Fix implicit-return in python_script (#123004) --- homeassistant/components/python_script/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 72e2f3a824b..ab8cf17daa0 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -108,13 +108,13 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -def discover_scripts(hass): +def discover_scripts(hass: HomeAssistant) -> None: """Discover python scripts in folder.""" path = hass.config.path(FOLDER) if not os.path.isdir(path): _LOGGER.warning("Folder %s not found in configuration folder", FOLDER) - return False + return def python_script_service_handler(call: ServiceCall) -> ServiceResponse: """Handle python script service calls.""" From ab522dab71a75e0851e7e6cac9101b73ecf0d10d Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 1 Aug 2024 03:59:19 -0700 Subject: [PATCH 1856/2411] Restrict nws.get_forecasts_extra selector to nws weather entities (#122986) --- homeassistant/components/nws/services.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nws/services.yaml b/homeassistant/components/nws/services.yaml index 0d439a9d278..a3d241c775d 100644 --- a/homeassistant/components/nws/services.yaml +++ b/homeassistant/components/nws/services.yaml @@ -2,6 +2,7 @@ get_forecasts_extra: target: entity: domain: weather + integration: nws fields: type: required: true From adf20b60dc31f2a9fb1667d6908904a26b1632f0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:43:09 +0200 Subject: [PATCH 1857/2411] Rename variable in landisgyr_heat_meter tests (#122995) --- tests/components/landisgyr_heat_meter/test_init.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/components/landisgyr_heat_meter/test_init.py b/tests/components/landisgyr_heat_meter/test_init.py index c9768ec681f..76a376e441c 100644 --- a/tests/components/landisgyr_heat_meter/test_init.py +++ b/tests/components/landisgyr_heat_meter/test_init.py @@ -1,6 +1,6 @@ """Test the Landis + Gyr Heat Meter init.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch from homeassistant.components.landisgyr_heat_meter.const import ( DOMAIN as LANDISGYR_HEAT_METER_DOMAIN, @@ -17,7 +17,7 @@ API_HEAT_METER_SERVICE = ( @patch(API_HEAT_METER_SERVICE) -async def test_unload_entry(_, hass: HomeAssistant) -> None: +async def test_unload_entry(mock_meter_service: MagicMock, hass: HomeAssistant) -> None: """Test removing config entry.""" mock_entry_data = { "device": "/dev/USB0", @@ -41,7 +41,9 @@ async def test_unload_entry(_, hass: HomeAssistant) -> None: @patch(API_HEAT_METER_SERVICE) async def test_migrate_entry( - _, hass: HomeAssistant, entity_registry: er.EntityRegistry + mock_meter_service: MagicMock, + hass: HomeAssistant, + entity_registry: er.EntityRegistry, ) -> None: """Test successful migration of entry data from version 1 to 2.""" From faedba04079d2c999a479118b5189ef4c0bff060 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:44:30 +0200 Subject: [PATCH 1858/2411] Rename variable in knx tests (#122994) * Rename variable in knx tests * Type hints * Type hints --- tests/components/knx/test_config_flow.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index a7da2d26600..d82e5ae33c7 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -1,7 +1,7 @@ """Test the KNX config flow.""" from contextlib import contextmanager -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch import pytest from xknx.exceptions.exception import CommunicationError, InvalidSecureConfiguration @@ -510,7 +510,7 @@ async def test_routing_secure_keyfile( return_value=GatewayScannerMock(), ) async def test_tunneling_setup_manual( - _gateway_scanner_mock, + gateway_scanner_mock: MagicMock, hass: HomeAssistant, knx_setup, user_input, @@ -559,7 +559,7 @@ async def test_tunneling_setup_manual( return_value=GatewayScannerMock(), ) async def test_tunneling_setup_manual_request_description_error( - _gateway_scanner_mock, + gateway_scanner_mock: MagicMock, hass: HomeAssistant, knx_setup, ) -> None: @@ -700,7 +700,10 @@ async def test_tunneling_setup_manual_request_description_error( return_value=_gateway_descriptor("192.168.0.2", 3675), ) async def test_tunneling_setup_for_local_ip( - _request_description_mock, _gateway_scanner_mock, hass: HomeAssistant, knx_setup + request_description_mock: MagicMock, + gateway_scanner_mock: MagicMock, + hass: HomeAssistant, + knx_setup, ) -> None: """Test tunneling if only one gateway is found.""" result = await hass.config_entries.flow.async_init( @@ -962,7 +965,7 @@ async def _get_menu_step_secure_tunnel(hass: HomeAssistant) -> FlowResult: ), ) async def test_get_secure_menu_step_manual_tunnelling( - _request_description_mock, + request_description_mock: MagicMock, hass: HomeAssistant, ) -> None: """Test flow reaches secure_tunnellinn menu step from manual tunnelling configuration.""" From f5e88b8293ebc02c950087cc811a8657c937760f Mon Sep 17 00:00:00 2001 From: DeerMaximum <43999966+DeerMaximum@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:32:37 +0200 Subject: [PATCH 1859/2411] Velux use node id as fallback for unique id (#117508) Co-authored-by: Robert Resch --- homeassistant/components/velux/__init__.py | 8 ++++++-- homeassistant/components/velux/cover.py | 6 +++--- homeassistant/components/velux/light.py | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 4b89fc66a84..1b7cbd1ff93 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -108,10 +108,14 @@ class VeluxEntity(Entity): _attr_should_poll = False - def __init__(self, node: Node) -> None: + def __init__(self, node: Node, config_entry_id: str) -> None: """Initialize the Velux device.""" self.node = node - self._attr_unique_id = node.serial_number + self._attr_unique_id = ( + node.serial_number + if node.serial_number + else f"{config_entry_id}_{node.node_id}" + ) self._attr_name = node.name if node.name else f"#{node.node_id}" @callback diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index c8688e4d186..cd7564eee81 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -29,7 +29,7 @@ async def async_setup_entry( """Set up cover(s) for Velux platform.""" module = hass.data[DOMAIN][config.entry_id] async_add_entities( - VeluxCover(node) + VeluxCover(node, config.entry_id) for node in module.pyvlx.nodes if isinstance(node, OpeningDevice) ) @@ -41,9 +41,9 @@ class VeluxCover(VeluxEntity, CoverEntity): _is_blind = False node: OpeningDevice - def __init__(self, node: OpeningDevice) -> None: + def __init__(self, node: OpeningDevice, config_entry_id: str) -> None: """Initialize VeluxCover.""" - super().__init__(node) + super().__init__(node, config_entry_id) self._attr_device_class = CoverDeviceClass.WINDOW if isinstance(node, Awning): self._attr_device_class = CoverDeviceClass.AWNING diff --git a/homeassistant/components/velux/light.py b/homeassistant/components/velux/light.py index bbe9822648e..e98632701f3 100644 --- a/homeassistant/components/velux/light.py +++ b/homeassistant/components/velux/light.py @@ -23,7 +23,7 @@ async def async_setup_entry( module = hass.data[DOMAIN][config.entry_id] async_add_entities( - VeluxLight(node) + VeluxLight(node, config.entry_id) for node in module.pyvlx.nodes if isinstance(node, LighteningDevice) ) From 4f3d6243533777525a6d12b2236722b7e816ffc7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:46:15 +0200 Subject: [PATCH 1860/2411] Enable pytest-fixture-param-without-value (PT019) rule in ruff (#122953) --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a9559b9e367..eadd529b3e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -820,7 +820,6 @@ ignore = [ "PLE0605", # temporarily disabled - "PT019", "PYI024", # Use typing.NamedTuple instead of collections.namedtuple "RET503", "RET501", From fef9c92eb76967332a780d24ce6a5d08e210bbe5 Mon Sep 17 00:00:00 2001 From: amccook <30292381+amccook@users.noreply.github.com> Date: Thu, 1 Aug 2024 07:24:09 -0700 Subject: [PATCH 1861/2411] Fix handling of directory type playlists in Plex (#122990) Ignore type directory --- homeassistant/components/plex/media_browser.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index e47e6145761..87e9f47af66 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -132,7 +132,11 @@ def browse_media( # noqa: C901 "children": [], } for playlist in plex_server.playlists(): - if playlist.playlistType != "audio" and platform == "sonos": + if ( + playlist.type != "directory" + and playlist.playlistType != "audio" + and platform == "sonos" + ): continue try: playlists_info["children"].append(item_payload(playlist)) From a3b5dcc21b7f01f094b457a1cc7fbb0c03ebff0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Aug 2024 15:37:26 -0500 Subject: [PATCH 1862/2411] Fix doorbird models are missing the schedule API (#123033) * Fix doorbird models are missing the schedule API fixes #122997 * cover --- homeassistant/components/doorbird/device.py | 14 +++++++--- tests/components/doorbird/__init__.py | 29 ++++++++++----------- tests/components/doorbird/conftest.py | 2 ++ tests/components/doorbird/test_init.py | 10 +++++++ 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/doorbird/device.py b/homeassistant/components/doorbird/device.py index 866251f3d28..7cd45487464 100644 --- a/homeassistant/components/doorbird/device.py +++ b/homeassistant/components/doorbird/device.py @@ -5,9 +5,11 @@ from __future__ import annotations from collections import defaultdict from dataclasses import dataclass from functools import cached_property +from http import HTTPStatus import logging from typing import Any +from aiohttp import ClientResponseError from doorbirdpy import ( DoorBird, DoorBirdScheduleEntry, @@ -170,15 +172,21 @@ class ConfiguredDoorBird: ) -> DoorbirdEventConfig: """Get events and unconfigured favorites from http favorites.""" device = self.device - schedule = await device.schedule() + events: list[DoorbirdEvent] = [] + unconfigured_favorites: defaultdict[str, list[str]] = defaultdict(list) + try: + schedule = await device.schedule() + except ClientResponseError as ex: + if ex.status == HTTPStatus.NOT_FOUND: + # D301 models do not support schedules + return DoorbirdEventConfig(events, [], unconfigured_favorites) + raise favorite_input_type = { output.param: entry.input for entry in schedule for output in entry.output if output.event == HTTP_EVENT_TYPE } - events: list[DoorbirdEvent] = [] - unconfigured_favorites: defaultdict[str, list[str]] = defaultdict(list) default_event_types = { self._get_event_name(event): event_type for event, event_type in DEFAULT_EVENT_TYPES diff --git a/tests/components/doorbird/__init__.py b/tests/components/doorbird/__init__.py index 41def92f121..2d517dfcefe 100644 --- a/tests/components/doorbird/__init__.py +++ b/tests/components/doorbird/__init__.py @@ -47,31 +47,30 @@ def get_mock_doorbird_api( info: dict[str, Any] | None = None, info_side_effect: Exception | None = None, schedule: list[DoorBirdScheduleEntry] | None = None, + schedule_side_effect: Exception | None = None, favorites: dict[str, dict[str, Any]] | None = None, favorites_side_effect: Exception | None = None, change_schedule: tuple[bool, int] | None = None, ) -> DoorBird: """Return a mock DoorBirdAPI object with return values.""" doorbirdapi_mock = MagicMock(spec_set=DoorBird) - type(doorbirdapi_mock).info = AsyncMock( - side_effect=info_side_effect, return_value=info + api_mock_type = type(doorbirdapi_mock) + api_mock_type.info = AsyncMock(side_effect=info_side_effect, return_value=info) + api_mock_type.favorites = AsyncMock( + side_effect=favorites_side_effect, return_value=favorites ) - type(doorbirdapi_mock).favorites = AsyncMock( - side_effect=favorites_side_effect, - return_value=favorites, - ) - type(doorbirdapi_mock).change_favorite = AsyncMock(return_value=True) - type(doorbirdapi_mock).change_schedule = AsyncMock( + api_mock_type.change_favorite = AsyncMock(return_value=True) + api_mock_type.change_schedule = AsyncMock( return_value=change_schedule or (True, 200) ) - type(doorbirdapi_mock).schedule = AsyncMock(return_value=schedule) - type(doorbirdapi_mock).energize_relay = AsyncMock(return_value=True) - type(doorbirdapi_mock).turn_light_on = AsyncMock(return_value=True) - type(doorbirdapi_mock).delete_favorite = AsyncMock(return_value=True) - type(doorbirdapi_mock).get_image = AsyncMock(return_value=b"image") - type(doorbirdapi_mock).doorbell_state = AsyncMock( - side_effect=mock_unauthorized_exception() + api_mock_type.schedule = AsyncMock( + return_value=schedule, side_effect=schedule_side_effect ) + api_mock_type.energize_relay = AsyncMock(return_value=True) + api_mock_type.turn_light_on = AsyncMock(return_value=True) + api_mock_type.delete_favorite = AsyncMock(return_value=True) + api_mock_type.get_image = AsyncMock(return_value=b"image") + api_mock_type.doorbell_state = AsyncMock(side_effect=mock_unauthorized_exception()) return doorbirdapi_mock diff --git a/tests/components/doorbird/conftest.py b/tests/components/doorbird/conftest.py index 59ead250293..2e367e4e1d8 100644 --- a/tests/components/doorbird/conftest.py +++ b/tests/components/doorbird/conftest.py @@ -102,6 +102,7 @@ async def doorbird_mocker( info: dict[str, Any] | None = None, info_side_effect: Exception | None = None, schedule: list[DoorBirdScheduleEntry] | None = None, + schedule_side_effect: Exception | None = None, favorites: dict[str, dict[str, Any]] | None = None, favorites_side_effect: Exception | None = None, options: dict[str, Any] | None = None, @@ -118,6 +119,7 @@ async def doorbird_mocker( info=info or doorbird_info, info_side_effect=info_side_effect, schedule=schedule or doorbird_schedule, + schedule_side_effect=schedule_side_effect, favorites=favorites or doorbird_favorites, favorites_side_effect=favorites_side_effect, change_schedule=change_schedule, diff --git a/tests/components/doorbird/test_init.py b/tests/components/doorbird/test_init.py index fb8bad2fb46..31266c4acf0 100644 --- a/tests/components/doorbird/test_init.py +++ b/tests/components/doorbird/test_init.py @@ -56,6 +56,16 @@ async def test_http_favorites_request_fails( assert doorbird_entry.entry.state is ConfigEntryState.SETUP_RETRY +async def test_http_schedule_api_missing( + doorbird_mocker: DoorbirdMockerType, +) -> None: + """Test missing the schedule API is non-fatal as not all models support it.""" + doorbird_entry = await doorbird_mocker( + schedule_side_effect=mock_not_found_exception() + ) + assert doorbird_entry.entry.state is ConfigEntryState.LOADED + + async def test_events_changed( hass: HomeAssistant, doorbird_mocker: DoorbirdMockerType, From 80aa2c269b4e99ef21a3c5f950b31560272ae0f7 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 1 Aug 2024 15:39:17 -0500 Subject: [PATCH 1863/2411] Standardize assist pipelines on 10ms chunk size (#123024) * Make chunk size always 10ms * Fix voip --- .../components/assist_pipeline/__init__.py | 8 ++ .../assist_pipeline/audio_enhancer.py | 18 +--- .../components/assist_pipeline/const.py | 4 +- .../components/assist_pipeline/pipeline.py | 75 +++---------- homeassistant/components/voip/voip.py | 22 ++-- tests/components/assist_pipeline/conftest.py | 13 +++ .../snapshots/test_websocket.ambr | 2 +- tests/components/assist_pipeline/test_init.py | 71 +++++++------ .../assist_pipeline/test_websocket.py | 100 +++++++++++------- 9 files changed, 154 insertions(+), 159 deletions(-) diff --git a/homeassistant/components/assist_pipeline/__init__.py b/homeassistant/components/assist_pipeline/__init__.py index f481411e551..8ee053162b0 100644 --- a/homeassistant/components/assist_pipeline/__init__.py +++ b/homeassistant/components/assist_pipeline/__init__.py @@ -16,6 +16,10 @@ from .const import ( DATA_LAST_WAKE_UP, DOMAIN, EVENT_RECORDING, + SAMPLE_CHANNELS, + SAMPLE_RATE, + SAMPLE_WIDTH, + SAMPLES_PER_CHUNK, ) from .error import PipelineNotFound from .pipeline import ( @@ -53,6 +57,10 @@ __all__ = ( "PipelineNotFound", "WakeWordSettings", "EVENT_RECORDING", + "SAMPLES_PER_CHUNK", + "SAMPLE_RATE", + "SAMPLE_WIDTH", + "SAMPLE_CHANNELS", ) CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/assist_pipeline/audio_enhancer.py b/homeassistant/components/assist_pipeline/audio_enhancer.py index e7a149bd00e..c9c60f421b1 100644 --- a/homeassistant/components/assist_pipeline/audio_enhancer.py +++ b/homeassistant/components/assist_pipeline/audio_enhancer.py @@ -6,6 +6,8 @@ import logging from pymicro_vad import MicroVad +from .const import BYTES_PER_CHUNK + _LOGGER = logging.getLogger(__name__) @@ -38,11 +40,6 @@ class AudioEnhancer(ABC): def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk: """Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples.""" - @property - @abstractmethod - def samples_per_chunk(self) -> int | None: - """Return number of samples per chunk or None if chunking isn't required.""" - class MicroVadEnhancer(AudioEnhancer): """Audio enhancer that just runs microVAD.""" @@ -61,22 +58,15 @@ class MicroVadEnhancer(AudioEnhancer): _LOGGER.debug("Initialized microVAD with threshold=%s", self.threshold) def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk: - """Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples.""" + """Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples.""" is_speech: bool | None = None if self.vad is not None: # Run VAD + assert len(audio) == BYTES_PER_CHUNK speech_prob = self.vad.Process10ms(audio) is_speech = speech_prob > self.threshold return EnhancedAudioChunk( audio=audio, timestamp_ms=timestamp_ms, is_speech=is_speech ) - - @property - def samples_per_chunk(self) -> int | None: - """Return number of samples per chunk or None if chunking isn't required.""" - if self.is_vad_enabled: - return 160 # 10ms - - return None diff --git a/homeassistant/components/assist_pipeline/const.py b/homeassistant/components/assist_pipeline/const.py index 14b93a90372..f7306b89a54 100644 --- a/homeassistant/components/assist_pipeline/const.py +++ b/homeassistant/components/assist_pipeline/const.py @@ -19,4 +19,6 @@ EVENT_RECORDING = f"{DOMAIN}_recording" SAMPLE_RATE = 16000 # hertz SAMPLE_WIDTH = 2 # bytes SAMPLE_CHANNELS = 1 # mono -SAMPLES_PER_CHUNK = 240 # 20 ms @ 16Khz +MS_PER_CHUNK = 10 +SAMPLES_PER_CHUNK = SAMPLE_RATE // (1000 // MS_PER_CHUNK) # 10 ms @ 16Khz +BYTES_PER_CHUNK = SAMPLES_PER_CHUNK * SAMPLE_WIDTH * SAMPLE_CHANNELS # 16-bit diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index af29888eb07..9fada934ca1 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -51,11 +51,13 @@ from homeassistant.util.limited_size_dict import LimitedSizeDict from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, MicroVadEnhancer from .const import ( + BYTES_PER_CHUNK, CONF_DEBUG_RECORDING_DIR, DATA_CONFIG, DATA_LAST_WAKE_UP, DATA_MIGRATIONS, DOMAIN, + MS_PER_CHUNK, SAMPLE_CHANNELS, SAMPLE_RATE, SAMPLE_WIDTH, @@ -502,9 +504,6 @@ class AudioSettings: is_vad_enabled: bool = True """True if VAD is used to determine the end of the voice command.""" - samples_per_chunk: int | None = None - """Number of samples that will be in each audio chunk (None for no chunking).""" - silence_seconds: float = 0.5 """Seconds of silence after voice command has ended.""" @@ -525,11 +524,6 @@ class AudioSettings: or (self.auto_gain_dbfs > 0) ) - @property - def is_chunking_enabled(self) -> bool: - """True if chunk size is set.""" - return self.samples_per_chunk is not None - @dataclass class PipelineRun: @@ -566,7 +560,9 @@ class PipelineRun: audio_enhancer: AudioEnhancer | None = None """VAD/noise suppression/auto gain""" - audio_chunking_buffer: AudioBuffer | None = None + audio_chunking_buffer: AudioBuffer = field( + default_factory=lambda: AudioBuffer(BYTES_PER_CHUNK) + ) """Buffer used when splitting audio into chunks for audio processing""" _device_id: str | None = None @@ -599,8 +595,6 @@ class PipelineRun: self.audio_settings.is_vad_enabled, ) - self.audio_chunking_buffer = AudioBuffer(self.samples_per_chunk * SAMPLE_WIDTH) - def __eq__(self, other: object) -> bool: """Compare pipeline runs by id.""" if isinstance(other, PipelineRun): @@ -608,14 +602,6 @@ class PipelineRun: return False - @property - def samples_per_chunk(self) -> int: - """Return number of samples expected in each audio chunk.""" - if self.audio_enhancer is not None: - return self.audio_enhancer.samples_per_chunk or SAMPLES_PER_CHUNK - - return self.audio_settings.samples_per_chunk or SAMPLES_PER_CHUNK - @callback def process_event(self, event: PipelineEvent) -> None: """Log an event and call listener.""" @@ -728,7 +714,7 @@ class PipelineRun: # after wake-word-detection. num_audio_chunks_to_buffer = int( (wake_word_settings.audio_seconds_to_buffer * SAMPLE_RATE) - / self.samples_per_chunk + / SAMPLES_PER_CHUNK ) stt_audio_buffer: deque[EnhancedAudioChunk] | None = None @@ -1216,60 +1202,31 @@ class PipelineRun: self.debug_recording_thread = None async def process_volume_only( - self, - audio_stream: AsyncIterable[bytes], - sample_rate: int = SAMPLE_RATE, - sample_width: int = SAMPLE_WIDTH, + self, audio_stream: AsyncIterable[bytes] ) -> AsyncGenerator[EnhancedAudioChunk]: """Apply volume transformation only (no VAD/audio enhancements) with optional chunking.""" - assert self.audio_chunking_buffer is not None - - bytes_per_chunk = self.samples_per_chunk * sample_width - ms_per_sample = sample_rate // 1000 - ms_per_chunk = self.samples_per_chunk // ms_per_sample timestamp_ms = 0 - async for chunk in audio_stream: if self.audio_settings.volume_multiplier != 1.0: chunk = _multiply_volume(chunk, self.audio_settings.volume_multiplier) - if self.audio_settings.is_chunking_enabled: - for sub_chunk in chunk_samples( - chunk, bytes_per_chunk, self.audio_chunking_buffer - ): - yield EnhancedAudioChunk( - audio=sub_chunk, - timestamp_ms=timestamp_ms, - is_speech=None, # no VAD - ) - timestamp_ms += ms_per_chunk - else: - # No chunking + for sub_chunk in chunk_samples( + chunk, BYTES_PER_CHUNK, self.audio_chunking_buffer + ): yield EnhancedAudioChunk( - audio=chunk, + audio=sub_chunk, timestamp_ms=timestamp_ms, is_speech=None, # no VAD ) - timestamp_ms += (len(chunk) // sample_width) // ms_per_sample + timestamp_ms += MS_PER_CHUNK async def process_enhance_audio( - self, - audio_stream: AsyncIterable[bytes], - sample_rate: int = SAMPLE_RATE, - sample_width: int = SAMPLE_WIDTH, + self, audio_stream: AsyncIterable[bytes] ) -> AsyncGenerator[EnhancedAudioChunk]: - """Split audio into 10 ms chunks and apply VAD/noise suppression/auto gain/volume transformation.""" + """Split audio into chunks and apply VAD/noise suppression/auto gain/volume transformation.""" assert self.audio_enhancer is not None - assert self.audio_enhancer.samples_per_chunk is not None - assert self.audio_chunking_buffer is not None - bytes_per_chunk = self.audio_enhancer.samples_per_chunk * sample_width - ms_per_sample = sample_rate // 1000 - ms_per_chunk = ( - self.audio_enhancer.samples_per_chunk // sample_width - ) // ms_per_sample timestamp_ms = 0 - async for dirty_samples in audio_stream: if self.audio_settings.volume_multiplier != 1.0: # Static gain @@ -1279,10 +1236,10 @@ class PipelineRun: # Split into chunks for audio enhancements/VAD for dirty_chunk in chunk_samples( - dirty_samples, bytes_per_chunk, self.audio_chunking_buffer + dirty_samples, BYTES_PER_CHUNK, self.audio_chunking_buffer ): yield self.audio_enhancer.enhance_chunk(dirty_chunk, timestamp_ms) - timestamp_ms += ms_per_chunk + timestamp_ms += MS_PER_CHUNK def _multiply_volume(chunk: bytes, volume_multiplier: float) -> bytes: diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 243909629cf..161e938a3b6 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -21,7 +21,7 @@ from voip_utils import ( VoipDatagramProtocol, ) -from homeassistant.components import stt, tts +from homeassistant.components import assist_pipeline, stt, tts from homeassistant.components.assist_pipeline import ( Pipeline, PipelineEvent, @@ -331,15 +331,14 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): async with asyncio.timeout(self.audio_timeout): chunk = await self._audio_queue.get() - assert audio_enhancer.samples_per_chunk is not None - vad_buffer = AudioBuffer(audio_enhancer.samples_per_chunk * WIDTH) + vad_buffer = AudioBuffer(assist_pipeline.SAMPLES_PER_CHUNK * WIDTH) while chunk: chunk_buffer.append(chunk) segmenter.process_with_vad( chunk, - audio_enhancer.samples_per_chunk, + assist_pipeline.SAMPLES_PER_CHUNK, lambda x: audio_enhancer.enhance_chunk(x, 0).is_speech is True, vad_buffer, ) @@ -371,13 +370,12 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): async with asyncio.timeout(self.audio_timeout): chunk = await self._audio_queue.get() - assert audio_enhancer.samples_per_chunk is not None - vad_buffer = AudioBuffer(audio_enhancer.samples_per_chunk * WIDTH) + vad_buffer = AudioBuffer(assist_pipeline.SAMPLES_PER_CHUNK * WIDTH) while chunk: if not segmenter.process_with_vad( chunk, - audio_enhancer.samples_per_chunk, + assist_pipeline.SAMPLES_PER_CHUNK, lambda x: audio_enhancer.enhance_chunk(x, 0).is_speech is True, vad_buffer, ): @@ -437,13 +435,13 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): sample_channels = wav_file.getnchannels() if ( - (sample_rate != 16000) - or (sample_width != 2) - or (sample_channels != 1) + (sample_rate != RATE) + or (sample_width != WIDTH) + or (sample_channels != CHANNELS) ): raise ValueError( - "Expected rate/width/channels as 16000/2/1," - " got {sample_rate}/{sample_width}/{sample_channels}}" + f"Expected rate/width/channels as {RATE}/{WIDTH}/{CHANNELS}," + f" got {sample_rate}/{sample_width}/{sample_channels}" ) audio_bytes = wav_file.readframes(wav_file.getnframes()) diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index c041a54d8fa..b2eca1e7ce1 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -11,6 +11,12 @@ import pytest from homeassistant.components import stt, tts, wake_word from homeassistant.components.assist_pipeline import DOMAIN, select as assist_select +from homeassistant.components.assist_pipeline.const import ( + BYTES_PER_CHUNK, + SAMPLE_CHANNELS, + SAMPLE_RATE, + SAMPLE_WIDTH, +) from homeassistant.components.assist_pipeline.pipeline import ( PipelineData, PipelineStorageCollection, @@ -33,6 +39,8 @@ from tests.common import ( _TRANSCRIPT = "test transcript" +BYTES_ONE_SECOND = SAMPLE_RATE * SAMPLE_WIDTH * SAMPLE_CHANNELS + @pytest.fixture(autouse=True) def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: @@ -462,3 +470,8 @@ def pipeline_data(hass: HomeAssistant, init_components) -> PipelineData: def pipeline_storage(pipeline_data) -> PipelineStorageCollection: """Return pipeline storage collection.""" return pipeline_data.pipeline_store + + +def make_10ms_chunk(header: bytes) -> bytes: + """Return 10ms of zeros with the given header.""" + return header + bytes(BYTES_PER_CHUNK - len(header)) diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 0b04b67bb22..e5ae18d28f2 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -440,7 +440,7 @@ # --- # name: test_device_capture_override.2 dict({ - 'audio': 'Y2h1bmsx', + 'audio': 'Y2h1bmsxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', 'channels': 1, 'rate': 16000, 'type': 'audio', diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index 8fb7ce5b5a5..4206a288331 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -13,6 +13,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components import assist_pipeline, media_source, stt, tts from homeassistant.components.assist_pipeline.const import ( + BYTES_PER_CHUNK, CONF_DEBUG_RECORDING_DIR, DOMAIN, ) @@ -20,16 +21,16 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component from .conftest import ( + BYTES_ONE_SECOND, MockSttProvider, MockSttProviderEntity, MockTTSProvider, MockWakeWordEntity, + make_10ms_chunk, ) from tests.typing import ClientSessionGenerator, WebSocketGenerator -BYTES_ONE_SECOND = 16000 * 2 - def process_events(events: list[assist_pipeline.PipelineEvent]) -> list[dict]: """Process events to remove dynamic values.""" @@ -58,8 +59,8 @@ async def test_pipeline_from_audio_stream_auto( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" await assist_pipeline.async_pipeline_from_audio_stream( @@ -79,7 +80,9 @@ async def test_pipeline_from_audio_stream_auto( ) assert process_events(events) == snapshot - assert mock_stt_provider.received == [b"part1", b"part2"] + assert len(mock_stt_provider.received) == 2 + assert mock_stt_provider.received[0].startswith(b"part1") + assert mock_stt_provider.received[1].startswith(b"part2") async def test_pipeline_from_audio_stream_legacy( @@ -98,8 +101,8 @@ async def test_pipeline_from_audio_stream_legacy( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" # Create a pipeline using an stt entity @@ -142,7 +145,9 @@ async def test_pipeline_from_audio_stream_legacy( ) assert process_events(events) == snapshot - assert mock_stt_provider.received == [b"part1", b"part2"] + assert len(mock_stt_provider.received) == 2 + assert mock_stt_provider.received[0].startswith(b"part1") + assert mock_stt_provider.received[1].startswith(b"part2") async def test_pipeline_from_audio_stream_entity( @@ -161,8 +166,8 @@ async def test_pipeline_from_audio_stream_entity( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" # Create a pipeline using an stt entity @@ -205,7 +210,9 @@ async def test_pipeline_from_audio_stream_entity( ) assert process_events(events) == snapshot - assert mock_stt_provider_entity.received == [b"part1", b"part2"] + assert len(mock_stt_provider_entity.received) == 2 + assert mock_stt_provider_entity.received[0].startswith(b"part1") + assert mock_stt_provider_entity.received[1].startswith(b"part2") async def test_pipeline_from_audio_stream_no_stt( @@ -224,8 +231,8 @@ async def test_pipeline_from_audio_stream_no_stt( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" # Create a pipeline without stt support @@ -285,8 +292,8 @@ async def test_pipeline_from_audio_stream_unknown_pipeline( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" # Try to use the created pipeline @@ -327,7 +334,7 @@ async def test_pipeline_from_audio_stream_wake_word( # [0, 2, ...] wake_chunk_2 = bytes(it.islice(it.cycle(range(0, 256, 2)), BYTES_ONE_SECOND)) - samples_per_chunk = 160 + samples_per_chunk = 160 # 10ms @ 16Khz bytes_per_chunk = samples_per_chunk * 2 # 16-bit async def audio_data(): @@ -343,8 +350,8 @@ async def test_pipeline_from_audio_stream_wake_word( yield wake_chunk_2[i : i + bytes_per_chunk] i += bytes_per_chunk - for chunk in (b"wake word!", b"part1", b"part2"): - yield chunk + bytes(bytes_per_chunk - len(chunk)) + for header in (b"wake word!", b"part1", b"part2"): + yield make_10ms_chunk(header) yield b"" @@ -365,9 +372,7 @@ async def test_pipeline_from_audio_stream_wake_word( wake_word_settings=assist_pipeline.WakeWordSettings( audio_seconds_to_buffer=1.5 ), - audio_settings=assist_pipeline.AudioSettings( - is_vad_enabled=False, samples_per_chunk=samples_per_chunk - ), + audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), ) assert process_events(events) == snapshot @@ -408,13 +413,11 @@ async def test_pipeline_save_audio( pipeline = assist_pipeline.async_get_pipeline(hass) events: list[assist_pipeline.PipelineEvent] = [] - # Pad out to an even number of bytes since these "samples" will be saved - # as 16-bit values. async def audio_data(): - yield b"wake word_" + yield make_10ms_chunk(b"wake word") # queued audio - yield b"part1_" - yield b"part2_" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" await assist_pipeline.async_pipeline_from_audio_stream( @@ -457,12 +460,16 @@ async def test_pipeline_save_audio( # Verify wake file with wave.open(str(wake_file), "rb") as wake_wav: wake_data = wake_wav.readframes(wake_wav.getnframes()) - assert wake_data == b"wake word_" + assert wake_data.startswith(b"wake word") # Verify stt file with wave.open(str(stt_file), "rb") as stt_wav: stt_data = stt_wav.readframes(stt_wav.getnframes()) - assert stt_data == b"queued audiopart1_part2_" + assert stt_data.startswith(b"queued audio") + stt_data = stt_data[len(b"queued audio") :] + assert stt_data.startswith(b"part1") + stt_data = stt_data[BYTES_PER_CHUNK:] + assert stt_data.startswith(b"part2") async def test_pipeline_saved_audio_with_device_id( @@ -645,10 +652,10 @@ async def test_wake_word_detection_aborted( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"silence!" - yield b"wake word!" - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"silence!") + yield make_10ms_chunk(b"wake word!") + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" pipeline_store = pipeline_data.pipeline_store diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index 7d4a9b18c12..2da914f4252 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -8,7 +8,12 @@ from unittest.mock import ANY, patch import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.assist_pipeline.const import DOMAIN +from homeassistant.components.assist_pipeline.const import ( + DOMAIN, + SAMPLE_CHANNELS, + SAMPLE_RATE, + SAMPLE_WIDTH, +) from homeassistant.components.assist_pipeline.pipeline import ( DeviceAudioQueue, Pipeline, @@ -18,7 +23,13 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr -from .conftest import MockWakeWordEntity, MockWakeWordEntity2 +from .conftest import ( + BYTES_ONE_SECOND, + BYTES_PER_CHUNK, + MockWakeWordEntity, + MockWakeWordEntity2, + make_10ms_chunk, +) from tests.common import MockConfigEntry from tests.typing import WebSocketGenerator @@ -205,7 +216,7 @@ async def test_audio_pipeline_with_wake_word_timeout( "start_stage": "wake_word", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "timeout": 1, }, } @@ -229,7 +240,7 @@ async def test_audio_pipeline_with_wake_word_timeout( events.append(msg["event"]) # 2 seconds of silence - await client.send_bytes(bytes([1]) + bytes(16000 * 2 * 2)) + await client.send_bytes(bytes([1]) + bytes(2 * BYTES_ONE_SECOND)) # Time out error msg = await client.receive_json() @@ -259,7 +270,7 @@ async def test_audio_pipeline_with_wake_word_no_timeout( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "timeout": 0, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "timeout": 0, "no_vad": True}, } ) @@ -282,9 +293,10 @@ async def test_audio_pipeline_with_wake_word_no_timeout( events.append(msg["event"]) # "audio" - await client.send_bytes(bytes([handler_id]) + b"wake word") + await client.send_bytes(bytes([handler_id]) + make_10ms_chunk(b"wake word")) - msg = await client.receive_json() + async with asyncio.timeout(1): + msg = await client.receive_json() assert msg["event"]["type"] == "wake_word-end" assert msg["event"]["data"] == snapshot events.append(msg["event"]) @@ -365,7 +377,7 @@ async def test_audio_pipeline_no_wake_word_engine( "start_stage": "wake_word", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, }, } ) @@ -402,7 +414,7 @@ async def test_audio_pipeline_no_wake_word_entity( "start_stage": "wake_word", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, }, } ) @@ -1771,7 +1783,7 @@ async def test_audio_pipeline_with_enhancements( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, # Enhancements "noise_suppression_level": 2, "auto_gain_dbfs": 15, @@ -1801,7 +1813,7 @@ async def test_audio_pipeline_with_enhancements( # One second of silence. # This will pass through the audio enhancement pipeline, but we don't test # the actual output. - await client.send_bytes(bytes([handler_id]) + bytes(16000 * 2)) + await client.send_bytes(bytes([handler_id]) + bytes(BYTES_ONE_SECOND)) # End of audio stream (handler id + empty payload) await client.send_bytes(bytes([handler_id])) @@ -1871,7 +1883,7 @@ async def test_wake_word_cooldown_same_id( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -1880,7 +1892,7 @@ async def test_wake_word_cooldown_same_id( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -1914,8 +1926,8 @@ async def test_wake_word_cooldown_same_id( assert msg["event"]["data"] == snapshot # Wake both up at the same time - await client_1.send_bytes(bytes([handler_id_1]) + b"wake word") - await client_2.send_bytes(bytes([handler_id_2]) + b"wake word") + await client_1.send_bytes(bytes([handler_id_1]) + make_10ms_chunk(b"wake word")) + await client_2.send_bytes(bytes([handler_id_2]) + make_10ms_chunk(b"wake word")) # Get response events error_data: dict[str, Any] | None = None @@ -1954,7 +1966,7 @@ async def test_wake_word_cooldown_different_ids( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -1963,7 +1975,7 @@ async def test_wake_word_cooldown_different_ids( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -1997,8 +2009,8 @@ async def test_wake_word_cooldown_different_ids( assert msg["event"]["data"] == snapshot # Wake both up at the same time, but they will have different wake word ids - await client_1.send_bytes(bytes([handler_id_1]) + b"wake word") - await client_2.send_bytes(bytes([handler_id_2]) + b"wake word") + await client_1.send_bytes(bytes([handler_id_1]) + make_10ms_chunk(b"wake word")) + await client_2.send_bytes(bytes([handler_id_2]) + make_10ms_chunk(b"wake word")) # Get response events msg = await client_1.receive_json() @@ -2073,7 +2085,7 @@ async def test_wake_word_cooldown_different_entities( "pipeline": pipeline_id_1, "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -2084,7 +2096,7 @@ async def test_wake_word_cooldown_different_entities( "pipeline": pipeline_id_2, "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -2119,8 +2131,8 @@ async def test_wake_word_cooldown_different_entities( # Wake both up at the same time. # They will have the same wake word id, but different entities. - await client_1.send_bytes(bytes([handler_id_1]) + b"wake word") - await client_2.send_bytes(bytes([handler_id_2]) + b"wake word") + await client_1.send_bytes(bytes([handler_id_1]) + make_10ms_chunk(b"wake word")) + await client_2.send_bytes(bytes([handler_id_2]) + make_10ms_chunk(b"wake word")) # Get response events error_data: dict[str, Any] | None = None @@ -2158,7 +2170,11 @@ async def test_device_capture( identifiers={("demo", "satellite-1234")}, ) - audio_chunks = [b"chunk1", b"chunk2", b"chunk3"] + audio_chunks = [ + make_10ms_chunk(b"chunk1"), + make_10ms_chunk(b"chunk2"), + make_10ms_chunk(b"chunk3"), + ] # Start capture client_capture = await hass_ws_client(hass) @@ -2181,7 +2197,7 @@ async def test_device_capture( "type": "assist_pipeline/run", "start_stage": "stt", "end_stage": "stt", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, "device_id": satellite_device.id, } ) @@ -2232,9 +2248,9 @@ async def test_device_capture( # Verify audio chunks for i, audio_chunk in enumerate(audio_chunks): assert events[i]["type"] == "audio" - assert events[i]["rate"] == 16000 - assert events[i]["width"] == 2 - assert events[i]["channels"] == 1 + assert events[i]["rate"] == SAMPLE_RATE + assert events[i]["width"] == SAMPLE_WIDTH + assert events[i]["channels"] == SAMPLE_CHANNELS # Audio is base64 encoded assert events[i]["audio"] == base64.b64encode(audio_chunk).decode("ascii") @@ -2259,7 +2275,11 @@ async def test_device_capture_override( identifiers={("demo", "satellite-1234")}, ) - audio_chunks = [b"chunk1", b"chunk2", b"chunk3"] + audio_chunks = [ + make_10ms_chunk(b"chunk1"), + make_10ms_chunk(b"chunk2"), + make_10ms_chunk(b"chunk3"), + ] # Start first capture client_capture_1 = await hass_ws_client(hass) @@ -2282,7 +2302,7 @@ async def test_device_capture_override( "type": "assist_pipeline/run", "start_stage": "stt", "end_stage": "stt", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, "device_id": satellite_device.id, } ) @@ -2365,9 +2385,9 @@ async def test_device_capture_override( # Verify all but first audio chunk for i, audio_chunk in enumerate(audio_chunks[1:]): assert events[i]["type"] == "audio" - assert events[i]["rate"] == 16000 - assert events[i]["width"] == 2 - assert events[i]["channels"] == 1 + assert events[i]["rate"] == SAMPLE_RATE + assert events[i]["width"] == SAMPLE_WIDTH + assert events[i]["channels"] == SAMPLE_CHANNELS # Audio is base64 encoded assert events[i]["audio"] == base64.b64encode(audio_chunk).decode("ascii") @@ -2427,7 +2447,7 @@ async def test_device_capture_queue_full( "type": "assist_pipeline/run", "start_stage": "stt", "end_stage": "stt", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, "device_id": satellite_device.id, } ) @@ -2448,8 +2468,8 @@ async def test_device_capture_queue_full( assert msg["event"]["type"] == "stt-start" assert msg["event"]["data"] == snapshot - # Single sample will "overflow" the queue - await client_pipeline.send_bytes(bytes([handler_id, 0, 0])) + # Single chunk will "overflow" the queue + await client_pipeline.send_bytes(bytes([handler_id]) + bytes(BYTES_PER_CHUNK)) # End of audio stream await client_pipeline.send_bytes(bytes([handler_id])) @@ -2557,7 +2577,7 @@ async def test_stt_cooldown_same_id( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "wake_word_phrase": "ok_nabu", }, } @@ -2569,7 +2589,7 @@ async def test_stt_cooldown_same_id( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "wake_word_phrase": "ok_nabu", }, } @@ -2628,7 +2648,7 @@ async def test_stt_cooldown_different_ids( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "wake_word_phrase": "ok_nabu", }, } @@ -2640,7 +2660,7 @@ async def test_stt_cooldown_different_ids( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "wake_word_phrase": "hey_jarvis", }, } From 262d778a383aa9b05523c57663d80189d62997f9 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Thu, 1 Aug 2024 23:50:10 +0300 Subject: [PATCH 1864/2411] Anthropic Claude conversation integration (#122526) * Initial commit * Use add_suggested_values * Update homeassistant/components/anthropic/conversation.py Co-authored-by: Joost Lekkerkerker * Update strings.json * Update config_flow.py * Update config_flow.py * Fix tests * Update homeassistant/components/anthropic/conversation.py Co-authored-by: Paulus Schoutsen * Removed agent registration * Moved message_convert inline function outside --------- Co-authored-by: Joost Lekkerkerker Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 2 + .../components/anthropic/__init__.py | 46 ++ .../components/anthropic/config_flow.py | 210 ++++++++ homeassistant/components/anthropic/const.py | 15 + .../components/anthropic/conversation.py | 301 +++++++++++ .../components/anthropic/manifest.json | 12 + .../components/anthropic/strings.json | 34 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/anthropic/__init__.py | 1 + tests/components/anthropic/conftest.py | 51 ++ .../snapshots/test_conversation.ambr | 34 ++ .../components/anthropic/test_config_flow.py | 239 +++++++++ .../components/anthropic/test_conversation.py | 487 ++++++++++++++++++ tests/components/anthropic/test_init.py | 64 +++ 17 files changed, 1509 insertions(+) create mode 100644 homeassistant/components/anthropic/__init__.py create mode 100644 homeassistant/components/anthropic/config_flow.py create mode 100644 homeassistant/components/anthropic/const.py create mode 100644 homeassistant/components/anthropic/conversation.py create mode 100644 homeassistant/components/anthropic/manifest.json create mode 100644 homeassistant/components/anthropic/strings.json create mode 100644 tests/components/anthropic/__init__.py create mode 100644 tests/components/anthropic/conftest.py create mode 100644 tests/components/anthropic/snapshots/test_conversation.ambr create mode 100644 tests/components/anthropic/test_config_flow.py create mode 100644 tests/components/anthropic/test_conversation.py create mode 100644 tests/components/anthropic/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index e90def993d2..b53e0a929bb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -108,6 +108,8 @@ build.json @home-assistant/supervisor /tests/components/anova/ @Lash-L /homeassistant/components/anthemav/ @hyralex /tests/components/anthemav/ @hyralex +/homeassistant/components/anthropic/ @Shulyaka +/tests/components/anthropic/ @Shulyaka /homeassistant/components/aosmith/ @bdr99 /tests/components/aosmith/ @bdr99 /homeassistant/components/apache_kafka/ @bachya diff --git a/homeassistant/components/anthropic/__init__.py b/homeassistant/components/anthropic/__init__.py new file mode 100644 index 00000000000..aa6cf509fa1 --- /dev/null +++ b/homeassistant/components/anthropic/__init__.py @@ -0,0 +1,46 @@ +"""The Anthropic integration.""" + +from __future__ import annotations + +import anthropic + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv + +from .const import DOMAIN, LOGGER + +PLATFORMS = (Platform.CONVERSATION,) +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + +type AnthropicConfigEntry = ConfigEntry[anthropic.AsyncClient] + + +async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) -> bool: + """Set up Anthropic from a config entry.""" + client = anthropic.AsyncAnthropic(api_key=entry.data[CONF_API_KEY]) + try: + await client.messages.create( + model="claude-3-haiku-20240307", + max_tokens=1, + messages=[{"role": "user", "content": "Hi"}], + timeout=10.0, + ) + except anthropic.AuthenticationError as err: + LOGGER.error("Invalid API key: %s", err) + return False + except anthropic.AnthropicError as err: + raise ConfigEntryNotReady(err) from err + + entry.runtime_data = client + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Anthropic.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/anthropic/config_flow.py b/homeassistant/components/anthropic/config_flow.py new file mode 100644 index 00000000000..01e16ec5350 --- /dev/null +++ b/homeassistant/components/anthropic/config_flow.py @@ -0,0 +1,210 @@ +"""Config flow for Anthropic integration.""" + +from __future__ import annotations + +import logging +from types import MappingProxyType +from typing import Any + +import anthropic +import voluptuous as vol + +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) +from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API +from homeassistant.core import HomeAssistant +from homeassistant.helpers import llm +from homeassistant.helpers.selector import ( + NumberSelector, + NumberSelectorConfig, + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, + TemplateSelector, +) + +from .const import ( + CONF_CHAT_MODEL, + CONF_MAX_TOKENS, + CONF_PROMPT, + CONF_RECOMMENDED, + CONF_TEMPERATURE, + DOMAIN, + RECOMMENDED_CHAT_MODEL, + RECOMMENDED_MAX_TOKENS, + RECOMMENDED_TEMPERATURE, +) + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + } +) + +RECOMMENDED_OPTIONS = { + CONF_RECOMMENDED: True, + CONF_LLM_HASS_API: llm.LLM_API_ASSIST, + CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT, +} + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + client = anthropic.AsyncAnthropic(api_key=data[CONF_API_KEY]) + await client.messages.create( + model="claude-3-haiku-20240307", + max_tokens=1, + messages=[{"role": "user", "content": "Hi"}], + timeout=10.0, + ) + + +class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Anthropic.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors = {} + + if user_input is not None: + try: + await validate_input(self.hass, user_input) + except anthropic.APITimeoutError: + errors["base"] = "timeout_connect" + except anthropic.APIConnectionError: + errors["base"] = "cannot_connect" + except anthropic.APIStatusError as e: + if isinstance(e.body, dict): + errors["base"] = e.body.get("error", {}).get("type", "unknown") + else: + errors["base"] = "unknown" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title="Claude", + data=user_input, + options=RECOMMENDED_OPTIONS, + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors or None + ) + + @staticmethod + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlow: + """Create the options flow.""" + return AnthropicOptionsFlow(config_entry) + + +class AnthropicOptionsFlow(OptionsFlow): + """Anthropic config flow options handler.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + self.last_rendered_recommended = config_entry.options.get( + CONF_RECOMMENDED, False + ) + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options.""" + options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options + + if user_input is not None: + if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended: + if user_input[CONF_LLM_HASS_API] == "none": + user_input.pop(CONF_LLM_HASS_API) + return self.async_create_entry(title="", data=user_input) + + # Re-render the options again, now with the recommended options shown/hidden + self.last_rendered_recommended = user_input[CONF_RECOMMENDED] + + options = { + CONF_RECOMMENDED: user_input[CONF_RECOMMENDED], + CONF_PROMPT: user_input[CONF_PROMPT], + CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API], + } + + suggested_values = options.copy() + if not suggested_values.get(CONF_PROMPT): + suggested_values[CONF_PROMPT] = llm.DEFAULT_INSTRUCTIONS_PROMPT + + schema = self.add_suggested_values_to_schema( + vol.Schema(anthropic_config_option_schema(self.hass, options)), + suggested_values, + ) + + return self.async_show_form( + step_id="init", + data_schema=schema, + ) + + +def anthropic_config_option_schema( + hass: HomeAssistant, + options: dict[str, Any] | MappingProxyType[str, Any], +) -> dict: + """Return a schema for Anthropic completion options.""" + hass_apis: list[SelectOptionDict] = [ + SelectOptionDict( + label="No control", + value="none", + ) + ] + hass_apis.extend( + SelectOptionDict( + label=api.name, + value=api.id, + ) + for api in llm.async_get_apis(hass) + ) + + schema = { + vol.Optional(CONF_PROMPT): TemplateSelector(), + vol.Optional(CONF_LLM_HASS_API, default="none"): SelectSelector( + SelectSelectorConfig(options=hass_apis) + ), + vol.Required( + CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False) + ): bool, + } + + if options.get(CONF_RECOMMENDED): + return schema + + schema.update( + { + vol.Optional( + CONF_CHAT_MODEL, + default=RECOMMENDED_CHAT_MODEL, + ): str, + vol.Optional( + CONF_MAX_TOKENS, + default=RECOMMENDED_MAX_TOKENS, + ): int, + vol.Optional( + CONF_TEMPERATURE, + default=RECOMMENDED_TEMPERATURE, + ): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)), + } + ) + return schema diff --git a/homeassistant/components/anthropic/const.py b/homeassistant/components/anthropic/const.py new file mode 100644 index 00000000000..4ccf2c88faa --- /dev/null +++ b/homeassistant/components/anthropic/const.py @@ -0,0 +1,15 @@ +"""Constants for the Anthropic integration.""" + +import logging + +DOMAIN = "anthropic" +LOGGER = logging.getLogger(__package__) + +CONF_RECOMMENDED = "recommended" +CONF_PROMPT = "prompt" +CONF_CHAT_MODEL = "chat_model" +RECOMMENDED_CHAT_MODEL = "claude-3-5-sonnet-20240620" +CONF_MAX_TOKENS = "max_tokens" +RECOMMENDED_MAX_TOKENS = 1024 +CONF_TEMPERATURE = "temperature" +RECOMMENDED_TEMPERATURE = 1.0 diff --git a/homeassistant/components/anthropic/conversation.py b/homeassistant/components/anthropic/conversation.py new file mode 100644 index 00000000000..92a09ad8a10 --- /dev/null +++ b/homeassistant/components/anthropic/conversation.py @@ -0,0 +1,301 @@ +"""Conversation support for Anthropic.""" + +from collections.abc import Callable +import json +from typing import Any, Literal, cast + +import anthropic +from anthropic._types import NOT_GIVEN +from anthropic.types import ( + Message, + MessageParam, + TextBlock, + TextBlockParam, + ToolParam, + ToolResultBlockParam, + ToolUseBlock, + ToolUseBlockParam, +) +import voluptuous as vol +from voluptuous_openapi import convert + +from homeassistant.components import conversation +from homeassistant.components.conversation import trace +from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, TemplateError +from homeassistant.helpers import device_registry as dr, intent, llm, template +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import ulid + +from . import AnthropicConfigEntry +from .const import ( + CONF_CHAT_MODEL, + CONF_MAX_TOKENS, + CONF_PROMPT, + CONF_TEMPERATURE, + DOMAIN, + LOGGER, + RECOMMENDED_CHAT_MODEL, + RECOMMENDED_MAX_TOKENS, + RECOMMENDED_TEMPERATURE, +) + +# Max number of back and forth with the LLM to generate a response +MAX_TOOL_ITERATIONS = 10 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: AnthropicConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up conversation entities.""" + agent = AnthropicConversationEntity(config_entry) + async_add_entities([agent]) + + +def _format_tool( + tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None +) -> ToolParam: + """Format tool specification.""" + return ToolParam( + name=tool.name, + description=tool.description or "", + input_schema=convert(tool.parameters, custom_serializer=custom_serializer), + ) + + +def _message_convert( + message: Message, +) -> MessageParam: + """Convert from class to TypedDict.""" + param_content: list[TextBlockParam | ToolUseBlockParam] = [] + + for message_content in message.content: + if isinstance(message_content, TextBlock): + param_content.append(TextBlockParam(type="text", text=message_content.text)) + elif isinstance(message_content, ToolUseBlock): + param_content.append( + ToolUseBlockParam( + type="tool_use", + id=message_content.id, + name=message_content.name, + input=message_content.input, + ) + ) + + return MessageParam(role=message.role, content=param_content) + + +class AnthropicConversationEntity( + conversation.ConversationEntity, conversation.AbstractConversationAgent +): + """Anthropic conversation agent.""" + + _attr_has_entity_name = True + _attr_name = None + + def __init__(self, entry: AnthropicConfigEntry) -> None: + """Initialize the agent.""" + self.entry = entry + self.history: dict[str, list[MessageParam]] = {} + self._attr_unique_id = entry.entry_id + self._attr_device_info = dr.DeviceInfo( + identifiers={(DOMAIN, entry.entry_id)}, + manufacturer="Anthropic", + model="Claude", + entry_type=dr.DeviceEntryType.SERVICE, + ) + if self.entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) + + @property + def supported_languages(self) -> list[str] | Literal["*"]: + """Return a list of supported languages.""" + return MATCH_ALL + + async def async_process( + self, user_input: conversation.ConversationInput + ) -> conversation.ConversationResult: + """Process a sentence.""" + options = self.entry.options + intent_response = intent.IntentResponse(language=user_input.language) + llm_api: llm.APIInstance | None = None + tools: list[ToolParam] | None = None + user_name: str | None = None + llm_context = llm.LLMContext( + platform=DOMAIN, + context=user_input.context, + user_prompt=user_input.text, + language=user_input.language, + assistant=conversation.DOMAIN, + device_id=user_input.device_id, + ) + + if options.get(CONF_LLM_HASS_API): + try: + llm_api = await llm.async_get_api( + self.hass, + options[CONF_LLM_HASS_API], + llm_context, + ) + except HomeAssistantError as err: + LOGGER.error("Error getting LLM API: %s", err) + intent_response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Error preparing LLM API: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=user_input.conversation_id + ) + tools = [ + _format_tool(tool, llm_api.custom_serializer) for tool in llm_api.tools + ] + + if user_input.conversation_id is None: + conversation_id = ulid.ulid_now() + messages = [] + + elif user_input.conversation_id in self.history: + conversation_id = user_input.conversation_id + messages = self.history[conversation_id] + + else: + # Conversation IDs are ULIDs. We generate a new one if not provided. + # If an old OLID is passed in, we will generate a new one to indicate + # a new conversation was started. If the user picks their own, they + # want to track a conversation and we respect it. + try: + ulid.ulid_to_bytes(user_input.conversation_id) + conversation_id = ulid.ulid_now() + except ValueError: + conversation_id = user_input.conversation_id + + messages = [] + + if ( + user_input.context + and user_input.context.user_id + and ( + user := await self.hass.auth.async_get_user(user_input.context.user_id) + ) + ): + user_name = user.name + + try: + prompt_parts = [ + template.Template( + llm.BASE_PROMPT + + options.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT), + self.hass, + ).async_render( + { + "ha_name": self.hass.config.location_name, + "user_name": user_name, + "llm_context": llm_context, + }, + parse_result=False, + ) + ] + + except TemplateError as err: + LOGGER.error("Error rendering prompt: %s", err) + intent_response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Sorry, I had a problem with my template: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + if llm_api: + prompt_parts.append(llm_api.api_prompt) + + prompt = "\n".join(prompt_parts) + + # Create a copy of the variable because we attach it to the trace + messages = [*messages, MessageParam(role="user", content=user_input.text)] + + LOGGER.debug("Prompt: %s", messages) + LOGGER.debug("Tools: %s", tools) + trace.async_conversation_trace_append( + trace.ConversationTraceEventType.AGENT_DETAIL, + {"system": prompt, "messages": messages}, + ) + + client = self.entry.runtime_data + + # To prevent infinite loops, we limit the number of iterations + for _iteration in range(MAX_TOOL_ITERATIONS): + try: + response = await client.messages.create( + model=options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL), + messages=messages, + tools=tools or NOT_GIVEN, + max_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS), + system=prompt, + temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE), + ) + except anthropic.AnthropicError as err: + intent_response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Sorry, I had a problem talking to Anthropic: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + LOGGER.debug("Response %s", response) + + messages.append(_message_convert(response)) + + if response.stop_reason != "tool_use" or not llm_api: + break + + tool_results: list[ToolResultBlockParam] = [] + for tool_call in response.content: + if isinstance(tool_call, TextBlock): + LOGGER.info(tool_call.text) + + if not isinstance(tool_call, ToolUseBlock): + continue + + tool_input = llm.ToolInput( + tool_name=tool_call.name, + tool_args=cast(dict[str, Any], tool_call.input), + ) + LOGGER.debug( + "Tool call: %s(%s)", tool_input.tool_name, tool_input.tool_args + ) + + try: + tool_response = await llm_api.async_call_tool(tool_input) + except (HomeAssistantError, vol.Invalid) as e: + tool_response = {"error": type(e).__name__} + if str(e): + tool_response["error_text"] = str(e) + + LOGGER.debug("Tool response: %s", tool_response) + tool_results.append( + ToolResultBlockParam( + type="tool_result", + tool_use_id=tool_call.id, + content=json.dumps(tool_response), + ) + ) + + messages.append(MessageParam(role="user", content=tool_results)) + + self.history[conversation_id] = messages + + for content in response.content: + if isinstance(content, TextBlock): + intent_response.async_set_speech(content.text) + break + + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) diff --git a/homeassistant/components/anthropic/manifest.json b/homeassistant/components/anthropic/manifest.json new file mode 100644 index 00000000000..7d51c458e4d --- /dev/null +++ b/homeassistant/components/anthropic/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "anthropic", + "name": "Anthropic Conversation", + "after_dependencies": ["assist_pipeline", "intent"], + "codeowners": ["@Shulyaka"], + "config_flow": true, + "dependencies": ["conversation"], + "documentation": "https://www.home-assistant.io/integrations/anthropic", + "integration_type": "service", + "iot_class": "cloud_polling", + "requirements": ["anthropic==0.31.2"] +} diff --git a/homeassistant/components/anthropic/strings.json b/homeassistant/components/anthropic/strings.json new file mode 100644 index 00000000000..9550a1a6672 --- /dev/null +++ b/homeassistant/components/anthropic/strings.json @@ -0,0 +1,34 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", + "authentication_error": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "prompt": "Instructions", + "chat_model": "[%key:common::generic::model%]", + "max_tokens": "Maximum tokens to return in response", + "temperature": "Temperature", + "llm_hass_api": "[%key:common::config_flow::data::llm_hass_api%]", + "recommended": "Recommended model settings" + }, + "data_description": { + "prompt": "Instruct how the LLM should respond. This can be a template." + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 0c37cf9c412..67cffd25f28 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -53,6 +53,7 @@ FLOWS = { "androidtv_remote", "anova", "anthemav", + "anthropic", "aosmith", "apcupsd", "apple_tv", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3cc3ea71df9..25a78e30017 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -315,6 +315,12 @@ "config_flow": true, "iot_class": "local_push" }, + "anthropic": { + "name": "Anthropic Conversation", + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling" + }, "anwb_energie": { "name": "ANWB Energie", "integration_type": "virtual", diff --git a/requirements_all.txt b/requirements_all.txt index 0bf4b77e9d2..1df7cc12072 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -451,6 +451,9 @@ anova-wifi==0.17.0 # homeassistant.components.anthemav anthemav==1.4.1 +# homeassistant.components.anthropic +anthropic==0.31.2 + # homeassistant.components.weatherkit apple_weatherkit==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9670987b70..5831f7c23cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -424,6 +424,9 @@ anova-wifi==0.17.0 # homeassistant.components.anthemav anthemav==1.4.1 +# homeassistant.components.anthropic +anthropic==0.31.2 + # homeassistant.components.weatherkit apple_weatherkit==1.1.2 diff --git a/tests/components/anthropic/__init__.py b/tests/components/anthropic/__init__.py new file mode 100644 index 00000000000..99d7a5785a8 --- /dev/null +++ b/tests/components/anthropic/__init__.py @@ -0,0 +1 @@ +"""Tests for the Anthropic integration.""" diff --git a/tests/components/anthropic/conftest.py b/tests/components/anthropic/conftest.py new file mode 100644 index 00000000000..0a5ad5e5ac6 --- /dev/null +++ b/tests/components/anthropic/conftest.py @@ -0,0 +1,51 @@ +"""Tests helpers.""" + +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.const import CONF_LLM_HASS_API +from homeassistant.core import HomeAssistant +from homeassistant.helpers import llm +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry(hass): + """Mock a config entry.""" + entry = MockConfigEntry( + title="Claude", + domain="anthropic", + data={ + "api_key": "bla", + }, + ) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture +def mock_config_entry_with_assist(hass, mock_config_entry): + """Mock a config entry with assist.""" + hass.config_entries.async_update_entry( + mock_config_entry, options={CONF_LLM_HASS_API: llm.LLM_API_ASSIST} + ) + return mock_config_entry + + +@pytest.fixture +async def mock_init_component(hass, mock_config_entry): + """Initialize integration.""" + with patch( + "anthropic.resources.messages.AsyncMessages.create", new_callable=AsyncMock + ): + assert await async_setup_component(hass, "anthropic", {}) + await hass.async_block_till_done() + + +@pytest.fixture(autouse=True) +async def setup_ha(hass: HomeAssistant) -> None: + """Set up Home Assistant.""" + assert await async_setup_component(hass, "homeassistant", {}) diff --git a/tests/components/anthropic/snapshots/test_conversation.ambr b/tests/components/anthropic/snapshots/test_conversation.ambr new file mode 100644 index 00000000000..e4dd7cd00bb --- /dev/null +++ b/tests/components/anthropic/snapshots/test_conversation.ambr @@ -0,0 +1,34 @@ +# serializer version: 1 +# name: test_unknown_hass_api + dict({ + 'conversation_id': None, + 'response': IntentResponse( + card=dict({ + }), + error_code=, + failed_results=list([ + ]), + intent=None, + intent_targets=list([ + ]), + language='en', + matched_states=list([ + ]), + reprompt=dict({ + }), + response_type=, + speech=dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': 'Error preparing LLM API: API non-existing not found', + }), + }), + speech_slots=dict({ + }), + success_results=list([ + ]), + unmatched_states=list([ + ]), + ), + }) +# --- diff --git a/tests/components/anthropic/test_config_flow.py b/tests/components/anthropic/test_config_flow.py new file mode 100644 index 00000000000..df27352b7b2 --- /dev/null +++ b/tests/components/anthropic/test_config_flow.py @@ -0,0 +1,239 @@ +"""Test the Anthropic config flow.""" + +from unittest.mock import AsyncMock, patch + +from anthropic import ( + APIConnectionError, + APIResponseValidationError, + APITimeoutError, + AuthenticationError, + BadRequestError, + InternalServerError, +) +from httpx import URL, Request, Response +import pytest + +from homeassistant import config_entries +from homeassistant.components.anthropic.config_flow import RECOMMENDED_OPTIONS +from homeassistant.components.anthropic.const import ( + CONF_CHAT_MODEL, + CONF_MAX_TOKENS, + CONF_PROMPT, + CONF_RECOMMENDED, + CONF_TEMPERATURE, + DOMAIN, + RECOMMENDED_CHAT_MODEL, + RECOMMENDED_MAX_TOKENS, +) +from homeassistant.const import CONF_LLM_HASS_API +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.""" + # Pretend we already set up a config entry. + hass.config.components.add("anthropic") + MockConfigEntry( + domain=DOMAIN, + state=config_entries.ConfigEntryState.LOADED, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] is None + + with ( + patch( + "homeassistant.components.anthropic.config_flow.anthropic.resources.messages.AsyncMessages.create", + new_callable=AsyncMock, + ), + patch( + "homeassistant.components.anthropic.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": "bla", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["data"] == { + "api_key": "bla", + } + assert result2["options"] == RECOMMENDED_OPTIONS + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_options( + hass: HomeAssistant, mock_config_entry, mock_init_component +) -> None: + """Test the options form.""" + options_flow = await hass.config_entries.options.async_init( + mock_config_entry.entry_id + ) + options = await hass.config_entries.options.async_configure( + options_flow["flow_id"], + { + "prompt": "Speak like a pirate", + "max_tokens": 200, + }, + ) + await hass.async_block_till_done() + assert options["type"] is FlowResultType.CREATE_ENTRY + assert options["data"]["prompt"] == "Speak like a pirate" + assert options["data"]["max_tokens"] == 200 + assert options["data"][CONF_CHAT_MODEL] == RECOMMENDED_CHAT_MODEL + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (APIConnectionError(request=None), "cannot_connect"), + (APITimeoutError(request=None), "timeout_connect"), + ( + BadRequestError( + message="Your credit balance is too low to access the Claude API. Please go to Plans & Billing to upgrade or purchase credits.", + response=Response( + status_code=400, + request=Request(method="POST", url=URL()), + ), + body={"type": "error", "error": {"type": "invalid_request_error"}}, + ), + "invalid_request_error", + ), + ( + AuthenticationError( + message="invalid x-api-key", + response=Response( + status_code=401, + request=Request(method="POST", url=URL()), + ), + body={"type": "error", "error": {"type": "authentication_error"}}, + ), + "authentication_error", + ), + ( + InternalServerError( + message=None, + response=Response( + status_code=500, + request=Request(method="POST", url=URL()), + ), + body=None, + ), + "unknown", + ), + ( + APIResponseValidationError( + response=Response( + status_code=200, + request=Request(method="POST", url=URL()), + ), + body=None, + ), + "unknown", + ), + ], +) +async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.anthropic.config_flow.anthropic.resources.messages.AsyncMessages.create", + new_callable=AsyncMock, + side_effect=side_effect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": "bla", + }, + ) + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"base": error} + + +@pytest.mark.parametrize( + ("current_options", "new_options", "expected_options"), + [ + ( + { + CONF_RECOMMENDED: True, + CONF_LLM_HASS_API: "none", + CONF_PROMPT: "bla", + }, + { + CONF_RECOMMENDED: False, + CONF_PROMPT: "Speak like a pirate", + CONF_TEMPERATURE: 0.3, + }, + { + CONF_RECOMMENDED: False, + CONF_PROMPT: "Speak like a pirate", + CONF_TEMPERATURE: 0.3, + CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL, + CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS, + }, + ), + ( + { + CONF_RECOMMENDED: False, + CONF_PROMPT: "Speak like a pirate", + CONF_TEMPERATURE: 0.3, + CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL, + CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS, + }, + { + CONF_RECOMMENDED: True, + CONF_LLM_HASS_API: "assist", + CONF_PROMPT: "", + }, + { + CONF_RECOMMENDED: True, + CONF_LLM_HASS_API: "assist", + CONF_PROMPT: "", + }, + ), + ], +) +async def test_options_switching( + hass: HomeAssistant, + mock_config_entry, + mock_init_component, + current_options, + new_options, + expected_options, +) -> None: + """Test the options form.""" + hass.config_entries.async_update_entry(mock_config_entry, options=current_options) + options_flow = await hass.config_entries.options.async_init( + mock_config_entry.entry_id + ) + if current_options.get(CONF_RECOMMENDED) != new_options.get(CONF_RECOMMENDED): + options_flow = await hass.config_entries.options.async_configure( + options_flow["flow_id"], + { + **current_options, + CONF_RECOMMENDED: new_options[CONF_RECOMMENDED], + }, + ) + options = await hass.config_entries.options.async_configure( + options_flow["flow_id"], + new_options, + ) + await hass.async_block_till_done() + assert options["type"] is FlowResultType.CREATE_ENTRY + assert options["data"] == expected_options diff --git a/tests/components/anthropic/test_conversation.py b/tests/components/anthropic/test_conversation.py new file mode 100644 index 00000000000..65ede877281 --- /dev/null +++ b/tests/components/anthropic/test_conversation.py @@ -0,0 +1,487 @@ +"""Tests for the Anthropic integration.""" + +from unittest.mock import AsyncMock, Mock, patch + +from anthropic import RateLimitError +from anthropic.types import Message, TextBlock, ToolUseBlock, Usage +from freezegun import freeze_time +from httpx import URL, Request, Response +from syrupy.assertion import SnapshotAssertion +import voluptuous as vol + +from homeassistant.components import conversation +from homeassistant.components.conversation import trace +from homeassistant.const import CONF_LLM_HASS_API +from homeassistant.core import Context, HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import intent, llm +from homeassistant.setup import async_setup_component +from homeassistant.util import ulid + +from tests.common import MockConfigEntry + + +async def test_entity( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, +) -> None: + """Test entity properties.""" + state = hass.states.get("conversation.claude") + assert state + assert state.attributes["supported_features"] == 0 + + hass.config_entries.async_update_entry( + mock_config_entry, + options={ + **mock_config_entry.options, + CONF_LLM_HASS_API: "assist", + }, + ) + with patch( + "anthropic.resources.messages.AsyncMessages.create", new_callable=AsyncMock + ): + await hass.config_entries.async_reload(mock_config_entry.entry_id) + + state = hass.states.get("conversation.claude") + assert state + assert ( + state.attributes["supported_features"] + == conversation.ConversationEntityFeature.CONTROL + ) + + +async def test_error_handling( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component +) -> None: + """Test that the default prompt works.""" + with patch( + "anthropic.resources.messages.AsyncMessages.create", + new_callable=AsyncMock, + side_effect=RateLimitError( + message=None, + response=Response( + status_code=429, request=Request(method="POST", url=URL()) + ), + body=None, + ), + ): + result = await conversation.async_converse( + hass, "hello", None, Context(), agent_id="conversation.claude" + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR, result + assert result.response.error_code == "unknown", result + + +async def test_template_error( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test that template error handling works.""" + hass.config_entries.async_update_entry( + mock_config_entry, + options={ + "prompt": "talk like a {% if True %}smarthome{% else %}pirate please.", + }, + ) + with patch( + "anthropic.resources.messages.AsyncMessages.create", new_callable=AsyncMock + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + result = await conversation.async_converse( + hass, "hello", None, Context(), agent_id="conversation.claude" + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR, result + assert result.response.error_code == "unknown", result + + +async def test_template_variables( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test that template variables work.""" + context = Context(user_id="12345") + mock_user = Mock() + mock_user.id = "12345" + mock_user.name = "Test User" + + hass.config_entries.async_update_entry( + mock_config_entry, + options={ + "prompt": ( + "The user name is {{ user_name }}. " + "The user id is {{ llm_context.context.user_id }}." + ), + }, + ) + with ( + patch( + "anthropic.resources.messages.AsyncMessages.create", new_callable=AsyncMock + ) as mock_create, + patch("homeassistant.auth.AuthManager.async_get_user", return_value=mock_user), + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + result = await conversation.async_converse( + hass, "hello", None, context, agent_id="conversation.claude" + ) + + assert ( + result.response.response_type == intent.IntentResponseType.ACTION_DONE + ), result + assert "The user name is Test User." in mock_create.mock_calls[1][2]["system"] + assert "The user id is 12345." in mock_create.mock_calls[1][2]["system"] + + +async def test_conversation_agent( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, +) -> None: + """Test Anthropic Agent.""" + agent = conversation.agent_manager.async_get_agent(hass, "conversation.claude") + assert agent.supported_languages == "*" + + +@patch("homeassistant.components.anthropic.conversation.llm.AssistAPI._async_get_tools") +async def test_function_call( + mock_get_tools, + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_init_component, +) -> None: + """Test function call from the assistant.""" + agent_id = "conversation.claude" + context = Context() + + mock_tool = AsyncMock() + mock_tool.name = "test_tool" + mock_tool.description = "Test function" + mock_tool.parameters = vol.Schema( + {vol.Optional("param1", description="Test parameters"): str} + ) + mock_tool.async_call.return_value = "Test response" + + mock_get_tools.return_value = [mock_tool] + + def completion_result(*args, messages, **kwargs): + for message in messages: + for content in message["content"]: + if not isinstance(content, str) and content["type"] == "tool_use": + return Message( + type="message", + id="msg_1234567890ABCDEFGHIJKLMN", + content=[ + TextBlock( + type="text", + text="I have successfully called the function", + ) + ], + model="claude-3-5-sonnet-20240620", + role="assistant", + stop_reason="end_turn", + stop_sequence=None, + usage=Usage(input_tokens=8, output_tokens=12), + ) + + return Message( + type="message", + id="msg_1234567890ABCDEFGHIJKLMN", + content=[ + TextBlock(type="text", text="Certainly, calling it now!"), + ToolUseBlock( + type="tool_use", + id="toolu_0123456789AbCdEfGhIjKlM", + name="test_tool", + input={"param1": "test_value"}, + ), + ], + model="claude-3-5-sonnet-20240620", + role="assistant", + stop_reason="tool_use", + stop_sequence=None, + usage=Usage(input_tokens=8, output_tokens=12), + ) + + with ( + patch( + "anthropic.resources.messages.AsyncMessages.create", + new_callable=AsyncMock, + side_effect=completion_result, + ) as mock_create, + freeze_time("2024-06-03 23:00:00"), + ): + result = await conversation.async_converse( + hass, + "Please call the test function", + None, + context, + agent_id=agent_id, + ) + + assert "Today's date is 2024-06-03." in mock_create.mock_calls[1][2]["system"] + + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert mock_create.mock_calls[1][2]["messages"][2] == { + "role": "user", + "content": [ + { + "content": '"Test response"', + "tool_use_id": "toolu_0123456789AbCdEfGhIjKlM", + "type": "tool_result", + } + ], + } + mock_tool.async_call.assert_awaited_once_with( + hass, + llm.ToolInput( + tool_name="test_tool", + tool_args={"param1": "test_value"}, + ), + llm.LLMContext( + platform="anthropic", + context=context, + user_prompt="Please call the test function", + language="en", + assistant="conversation", + device_id=None, + ), + ) + + # Test Conversation tracing + traces = trace.async_get_traces() + assert traces + last_trace = traces[-1].as_dict() + trace_events = last_trace.get("events", []) + assert [event["event_type"] for event in trace_events] == [ + trace.ConversationTraceEventType.ASYNC_PROCESS, + trace.ConversationTraceEventType.AGENT_DETAIL, + trace.ConversationTraceEventType.TOOL_CALL, + ] + # AGENT_DETAIL event contains the raw prompt passed to the model + detail_event = trace_events[1] + assert "Answer in plain text" in detail_event["data"]["system"] + assert "Today's date is 2024-06-03." in trace_events[1]["data"]["system"] + + # Call it again, make sure we have updated prompt + with ( + patch( + "anthropic.resources.messages.AsyncMessages.create", + new_callable=AsyncMock, + side_effect=completion_result, + ) as mock_create, + freeze_time("2024-06-04 23:00:00"), + ): + result = await conversation.async_converse( + hass, + "Please call the test function", + None, + context, + agent_id=agent_id, + ) + + assert "Today's date is 2024-06-04." in mock_create.mock_calls[1][2]["system"] + # Test old assert message not updated + assert "Today's date is 2024-06-03." in trace_events[1]["data"]["system"] + + +@patch("homeassistant.components.anthropic.conversation.llm.AssistAPI._async_get_tools") +async def test_function_exception( + mock_get_tools, + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_init_component, +) -> None: + """Test function call with exception.""" + agent_id = "conversation.claude" + context = Context() + + mock_tool = AsyncMock() + mock_tool.name = "test_tool" + mock_tool.description = "Test function" + mock_tool.parameters = vol.Schema( + {vol.Optional("param1", description="Test parameters"): str} + ) + mock_tool.async_call.side_effect = HomeAssistantError("Test tool exception") + + mock_get_tools.return_value = [mock_tool] + + def completion_result(*args, messages, **kwargs): + for message in messages: + for content in message["content"]: + if not isinstance(content, str) and content["type"] == "tool_use": + return Message( + type="message", + id="msg_1234567890ABCDEFGHIJKLMN", + content=[ + TextBlock( + type="text", + text="There was an error calling the function", + ) + ], + model="claude-3-5-sonnet-20240620", + role="assistant", + stop_reason="end_turn", + stop_sequence=None, + usage=Usage(input_tokens=8, output_tokens=12), + ) + + return Message( + type="message", + id="msg_1234567890ABCDEFGHIJKLMN", + content=[ + TextBlock(type="text", text="Certainly, calling it now!"), + ToolUseBlock( + type="tool_use", + id="toolu_0123456789AbCdEfGhIjKlM", + name="test_tool", + input={"param1": "test_value"}, + ), + ], + model="claude-3-5-sonnet-20240620", + role="assistant", + stop_reason="tool_use", + stop_sequence=None, + usage=Usage(input_tokens=8, output_tokens=12), + ) + + with patch( + "anthropic.resources.messages.AsyncMessages.create", + new_callable=AsyncMock, + side_effect=completion_result, + ) as mock_create: + result = await conversation.async_converse( + hass, + "Please call the test function", + None, + context, + agent_id=agent_id, + ) + + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert mock_create.mock_calls[1][2]["messages"][2] == { + "role": "user", + "content": [ + { + "content": '{"error": "HomeAssistantError", "error_text": "Test tool exception"}', + "tool_use_id": "toolu_0123456789AbCdEfGhIjKlM", + "type": "tool_result", + } + ], + } + mock_tool.async_call.assert_awaited_once_with( + hass, + llm.ToolInput( + tool_name="test_tool", + tool_args={"param1": "test_value"}, + ), + llm.LLMContext( + platform="anthropic", + context=context, + user_prompt="Please call the test function", + language="en", + assistant="conversation", + device_id=None, + ), + ) + + +async def test_assist_api_tools_conversion( + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_init_component, +) -> None: + """Test that we are able to convert actual tools from Assist API.""" + for component in ( + "intent", + "todo", + "light", + "shopping_list", + "humidifier", + "climate", + "media_player", + "vacuum", + "cover", + "weather", + ): + assert await async_setup_component(hass, component, {}) + + agent_id = "conversation.claude" + with patch( + "anthropic.resources.messages.AsyncMessages.create", + new_callable=AsyncMock, + return_value=Message( + type="message", + id="msg_1234567890ABCDEFGHIJKLMN", + content=[TextBlock(type="text", text="Hello, how can I help you?")], + model="claude-3-5-sonnet-20240620", + role="assistant", + stop_reason="end_turn", + stop_sequence=None, + usage=Usage(input_tokens=8, output_tokens=12), + ), + ) as mock_create: + await conversation.async_converse( + hass, "hello", None, Context(), agent_id=agent_id + ) + + tools = mock_create.mock_calls[0][2]["tools"] + assert tools + + +async def test_unknown_hass_api( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + mock_init_component, +) -> None: + """Test when we reference an API that no longer exists.""" + hass.config_entries.async_update_entry( + mock_config_entry, + options={ + **mock_config_entry.options, + CONF_LLM_HASS_API: "non-existing", + }, + ) + + result = await conversation.async_converse( + hass, "hello", None, Context(), agent_id="conversation.claude" + ) + + assert result == snapshot + + +@patch("anthropic.resources.messages.AsyncMessages.create", new_callable=AsyncMock) +async def test_conversation_id( + mock_create, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, +) -> None: + """Test conversation ID is honored.""" + result = await conversation.async_converse( + hass, "hello", None, None, agent_id="conversation.claude" + ) + + conversation_id = result.conversation_id + + result = await conversation.async_converse( + hass, "hello", conversation_id, None, agent_id="conversation.claude" + ) + + assert result.conversation_id == conversation_id + + unknown_id = ulid.ulid() + + result = await conversation.async_converse( + hass, "hello", unknown_id, None, agent_id="conversation.claude" + ) + + assert result.conversation_id != unknown_id + + result = await conversation.async_converse( + hass, "hello", "koala", None, agent_id="conversation.claude" + ) + + assert result.conversation_id == "koala" diff --git a/tests/components/anthropic/test_init.py b/tests/components/anthropic/test_init.py new file mode 100644 index 00000000000..ee87bb708d0 --- /dev/null +++ b/tests/components/anthropic/test_init.py @@ -0,0 +1,64 @@ +"""Tests for the Anthropic integration.""" + +from unittest.mock import AsyncMock, patch + +from anthropic import ( + APIConnectionError, + APITimeoutError, + AuthenticationError, + BadRequestError, +) +from httpx import URL, Request, Response +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (APIConnectionError(request=None), "Connection error"), + (APITimeoutError(request=None), "Request timed out"), + ( + BadRequestError( + message="Your credit balance is too low to access the Claude API. Please go to Plans & Billing to upgrade or purchase credits.", + response=Response( + status_code=400, + request=Request(method="POST", url=URL()), + ), + body={"type": "error", "error": {"type": "invalid_request_error"}}, + ), + "anthropic integration not ready yet: Your credit balance is too low to access the Claude API", + ), + ( + AuthenticationError( + message="invalid x-api-key", + response=Response( + status_code=401, + request=Request(method="POST", url=URL()), + ), + body={"type": "error", "error": {"type": "authentication_error"}}, + ), + "Invalid API key", + ), + ], +) +async def test_init_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, + side_effect, + error, +) -> None: + """Test initialization errors.""" + with patch( + "anthropic.resources.messages.AsyncMessages.create", + new_callable=AsyncMock, + side_effect=side_effect, + ): + assert await async_setup_component(hass, "anthropic", {}) + await hass.async_block_till_done() + assert error in caplog.text From ed6d6575d7c9d359bbf84a702f8f90fb466207bb Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Fri, 2 Aug 2024 09:05:06 +0300 Subject: [PATCH 1865/2411] Add aliases to script llm tool description (#122380) * Add aliases to script llm tool description * Also add name --- homeassistant/helpers/llm.py | 13 +++++++++++++ tests/helpers/test_llm.py | 36 +++++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 4ddb00166b6..e37aa0c532d 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -677,6 +677,19 @@ class ScriptTool(Tool): self.parameters = vol.Schema(schema) + aliases: list[str] = [] + if entity_entry.name: + aliases.append(entity_entry.name) + if entity_entry.aliases: + aliases.extend(entity_entry.aliases) + if aliases: + if self.description: + self.description = ( + self.description + ". Aliases: " + str(list(aliases)) + ) + else: + self.description = "Aliases: " + str(list(aliases)) + parameters_cache[entity_entry.unique_id] = ( self.description, self.parameters, diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index ea6e628d1d4..4d14abb9819 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -411,7 +411,9 @@ async def test_assist_api_prompt( ) hass.states.async_set(entry2.entity_id, "on", {"friendly_name": "Living Room"}) - def create_entity(device: dr.DeviceEntry, write_state=True) -> None: + def create_entity( + device: dr.DeviceEntry, write_state=True, aliases: set[str] | None = None + ) -> None: """Create an entity for a device and track entity_id.""" entity = entity_registry.async_get_or_create( "light", @@ -421,6 +423,8 @@ async def test_assist_api_prompt( original_name=str(device.name or "Unnamed Device"), suggested_object_id=str(device.name or "unnamed_device"), ) + if aliases: + entity_registry.async_update_entity(entity.entity_id, aliases=aliases) if write_state: entity.write_unavailable_state(hass) @@ -432,7 +436,8 @@ async def test_assist_api_prompt( manufacturer="Test Manufacturer", model="Test Model", suggested_area="Test Area", - ) + ), + aliases={"my test light"}, ) for i in range(3): create_entity( @@ -516,7 +521,7 @@ async def test_assist_api_prompt( domain: light state: 'on' areas: Test Area, Alternative name -- names: Test Device +- names: Test Device, my test light domain: light state: unavailable areas: Test Area, Alternative name @@ -616,6 +621,7 @@ async def test_assist_api_prompt( async def test_script_tool( hass: HomeAssistant, + entity_registry: er.EntityRegistry, area_registry: ar.AreaRegistry, floor_registry: fr.FloorRegistry, ) -> None: @@ -659,6 +665,10 @@ async def test_script_tool( ) async_expose_entity(hass, "conversation", "script.test_script", True) + entity_registry.async_update_entity( + "script.test_script", name="script name", aliases={"script alias"} + ) + area = area_registry.async_create("Living room") floor = floor_registry.async_create("2") @@ -671,7 +681,10 @@ async def test_script_tool( tool = tools[0] assert tool.name == "test_script" - assert tool.description == "This is a test script" + assert ( + tool.description + == "This is a test script. Aliases: ['script name', 'script alias']" + ) schema = { vol.Required("beer", description="Number of beers"): cv.string, vol.Optional("wine"): selector.NumberSelector({"min": 0, "max": 3}), @@ -684,7 +697,10 @@ async def test_script_tool( assert tool.parameters.schema == schema assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == { - "test_script": ("This is a test script", vol.Schema(schema)) + "test_script": ( + "This is a test script. Aliases: ['script name', 'script alias']", + vol.Schema(schema), + ) } tool_input = llm.ToolInput( @@ -754,12 +770,18 @@ async def test_script_tool( tool = tools[0] assert tool.name == "test_script" - assert tool.description == "This is a new test script" + assert ( + tool.description + == "This is a new test script. Aliases: ['script name', 'script alias']" + ) schema = {vol.Required("beer", description="Number of beers"): cv.string} assert tool.parameters.schema == schema assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == { - "test_script": ("This is a new test script", vol.Schema(schema)) + "test_script": ( + "This is a new test script. Aliases: ['script name', 'script alias']", + vol.Schema(schema), + ) } From 8ec8aef02ecb9b215009ff772560f68b54e6a88d Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 2 Aug 2024 08:48:41 +0200 Subject: [PATCH 1866/2411] Use freezer in KNX tests (#123044) use freezer in tests --- tests/components/knx/test_binary_sensor.py | 27 ++++++++++++------- tests/components/knx/test_button.py | 9 ++++--- tests/components/knx/test_expose.py | 13 ++++----- tests/components/knx/test_interface_device.py | 10 ++++--- tests/components/knx/test_light.py | 11 ++++---- 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index 1b304293a86..dbb8d2ee832 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta +from freezegun.api import FrozenDateTimeFactory + from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE from homeassistant.components.knx.schema import BinarySensorSchema from homeassistant.const import ( @@ -13,7 +15,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util from .conftest import KNXTestKit @@ -141,9 +142,12 @@ async def test_binary_sensor_ignore_internal_state( assert len(events) == 6 -async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_binary_sensor_counter( + hass: HomeAssistant, + knx: KNXTestKit, + freezer: FrozenDateTimeFactory, +) -> None: """Test KNX binary_sensor with context timeout.""" - async_fire_time_changed(hass, dt_util.utcnow()) context_timeout = 1 await knx.setup_integration( @@ -167,7 +171,8 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> No state = hass.states.get("binary_sensor.test") assert state.state is STATE_OFF assert state.attributes.get("counter") == 0 - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=context_timeout)) + freezer.tick(timedelta(seconds=context_timeout)) + async_fire_time_changed(hass) await knx.xknx.task_registry.block_till_done() # state changed twice after context timeout - once to ON with counter 1 and once to counter 0 state = hass.states.get("binary_sensor.test") @@ -188,7 +193,8 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> No state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON assert state.attributes.get("counter") == 0 - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=context_timeout)) + freezer.tick(timedelta(seconds=context_timeout)) + async_fire_time_changed(hass) await knx.xknx.task_registry.block_till_done() state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON @@ -202,10 +208,12 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> No assert event.get("old_state").attributes.get("counter") == 2 -async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_binary_sensor_reset( + hass: HomeAssistant, + knx: KNXTestKit, + freezer: FrozenDateTimeFactory, +) -> None: """Test KNX binary_sensor with reset_after function.""" - async_fire_time_changed(hass, dt_util.utcnow()) - await knx.setup_integration( { BinarySensorSchema.PLATFORM: [ @@ -223,7 +231,8 @@ async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit) -> None await knx.receive_write("2/2/2", True) state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) await hass.async_block_till_done() # state reset after after timeout state = hass.states.get("binary_sensor.test") diff --git a/tests/components/knx/test_button.py b/tests/components/knx/test_button.py index 613208d5595..a05752eced1 100644 --- a/tests/components/knx/test_button.py +++ b/tests/components/knx/test_button.py @@ -3,20 +3,22 @@ from datetime import timedelta import logging +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.knx.const import CONF_PAYLOAD_LENGTH, DOMAIN, KNX_ADDRESS from homeassistant.components.knx.schema import ButtonSchema from homeassistant.const import CONF_NAME, CONF_PAYLOAD, CONF_TYPE from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from .conftest import KNXTestKit from tests.common import async_capture_events, async_fire_time_changed -async def test_button_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_button_simple( + hass: HomeAssistant, knx: KNXTestKit, freezer: FrozenDateTimeFactory +) -> None: """Test KNX button with default payload.""" await knx.setup_integration( { @@ -38,7 +40,8 @@ async def test_button_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: # received telegrams on button GA are ignored by the entity old_state = hass.states.get("button.test") - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3)) + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) await knx.receive_write("1/2/3", False) await knx.receive_write("1/2/3", True) new_state = hass.states.get("button.test") diff --git a/tests/components/knx/test_expose.py b/tests/components/knx/test_expose.py index 96b00241ab6..c4d0acf0ce2 100644 --- a/tests/components/knx/test_expose.py +++ b/tests/components/knx/test_expose.py @@ -3,6 +3,7 @@ from datetime import timedelta from freezegun import freeze_time +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.knx import CONF_KNX_EXPOSE, DOMAIN, KNX_ADDRESS @@ -14,11 +15,10 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from .conftest import KNXTestKit -from tests.common import async_fire_time_changed_exact +from tests.common import async_fire_time_changed async def test_binary_expose(hass: HomeAssistant, knx: KNXTestKit) -> None: @@ -206,7 +206,9 @@ async def test_expose_string(hass: HomeAssistant, knx: KNXTestKit) -> None: ) -async def test_expose_cooldown(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_expose_cooldown( + hass: HomeAssistant, knx: KNXTestKit, freezer: FrozenDateTimeFactory +) -> None: """Test an expose with cooldown.""" cooldown_time = 2 entity_id = "fake.entity" @@ -234,9 +236,8 @@ async def test_expose_cooldown(hass: HomeAssistant, knx: KNXTestKit) -> None: await hass.async_block_till_done() await knx.assert_no_telegram() # Wait for cooldown to pass - async_fire_time_changed_exact( - hass, dt_util.utcnow() + timedelta(seconds=cooldown_time) - ) + freezer.tick(timedelta(seconds=cooldown_time)) + async_fire_time_changed(hass) await hass.async_block_till_done() await knx.assert_write("1/1/8", (3,)) diff --git a/tests/components/knx/test_interface_device.py b/tests/components/knx/test_interface_device.py index 8010496ef0d..79114d4ffd5 100644 --- a/tests/components/knx/test_interface_device.py +++ b/tests/components/knx/test_interface_device.py @@ -2,6 +2,7 @@ from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory from xknx.core import XknxConnectionState, XknxConnectionType from xknx.telegram import IndividualAddress @@ -10,7 +11,6 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from homeassistant.util import dt as dt_util from .conftest import KNXTestKit @@ -19,7 +19,10 @@ from tests.typing import WebSocketGenerator async def test_diagnostic_entities( - hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry + hass: HomeAssistant, + knx: KNXTestKit, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test diagnostic entities.""" await knx.setup_integration({}) @@ -50,7 +53,8 @@ async def test_diagnostic_entities( knx.xknx.connection_manager.cemi_count_outgoing_error = 2 events = async_capture_events(hass, "state_changed") - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(events) == 3 # 5 polled sensors - 2 disabled diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index 8c966a77a0b..04f849bb555 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta +from freezegun.api import FrozenDateTimeFactory from xknx.core import XknxConnectionState from xknx.devices.light import Light as XknxLight @@ -19,7 +20,6 @@ from homeassistant.components.light import ( ) from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from . import KnxEntityGenerator from .conftest import KNXTestKit @@ -643,7 +643,9 @@ async def test_light_rgb_individual(hass: HomeAssistant, knx: KNXTestKit) -> Non await knx.assert_write(test_blue, (45,)) -async def test_light_rgbw_individual(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_light_rgbw_individual( + hass: HomeAssistant, knx: KNXTestKit, freezer: FrozenDateTimeFactory +) -> None: """Test KNX light with rgbw color in individual GAs.""" test_red = "1/1/3" test_red_state = "1/1/4" @@ -763,9 +765,8 @@ async def test_light_rgbw_individual(hass: HomeAssistant, knx: KNXTestKit) -> No await knx.receive_write(test_green, (0,)) # # individual color debounce takes 0.2 seconds if not all 4 addresses received knx.assert_state("light.test", STATE_ON) - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=XknxLight.DEBOUNCE_TIMEOUT) - ) + freezer.tick(timedelta(seconds=XknxLight.DEBOUNCE_TIMEOUT)) + async_fire_time_changed(hass) await knx.xknx.task_registry.block_till_done() knx.assert_state("light.test", STATE_OFF) # turn ON from KNX From 4da385898bbc4c58814d3ce1f2f393abd502de2d Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 2 Aug 2024 08:50:19 +0200 Subject: [PATCH 1867/2411] Mitigate breaking change for KNX climate schema (#123043) --- homeassistant/components/knx/schema.py | 7 +++-- homeassistant/components/knx/validation.py | 34 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 43037ad8188..c31b3d30ad0 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -56,6 +56,7 @@ from .const import ( ColorTempModes, ) from .validation import ( + backwards_compatible_xknx_climate_enum_member, dpt_base_type_validator, ga_list_validator, ga_validator, @@ -409,10 +410,12 @@ class ClimateSchema(KNXPlatformSchema): CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT ): cv.boolean, vol.Optional(CONF_OPERATION_MODES): vol.All( - cv.ensure_list, [vol.All(vol.Upper, cv.enum(HVACOperationMode))] + cv.ensure_list, + [backwards_compatible_xknx_climate_enum_member(HVACOperationMode)], ), vol.Optional(CONF_CONTROLLER_MODES): vol.All( - cv.ensure_list, [vol.All(vol.Upper, cv.enum(HVACControllerMode))] + cv.ensure_list, + [backwards_compatible_xknx_climate_enum_member(HVACControllerMode)], ), vol.Optional( CONF_DEFAULT_CONTROLLER_MODE, default=HVACMode.HEAT diff --git a/homeassistant/components/knx/validation.py b/homeassistant/components/knx/validation.py index 422b8474fd9..0283b65f899 100644 --- a/homeassistant/components/knx/validation.py +++ b/homeassistant/components/knx/validation.py @@ -1,6 +1,7 @@ """Validation helpers for KNX config schemas.""" from collections.abc import Callable +from enum import Enum import ipaddress from typing import Any @@ -104,3 +105,36 @@ sync_state_validator = vol.Any( cv.boolean, cv.matches_regex(r"^(init|expire|every)( \d*)?$"), ) + + +def backwards_compatible_xknx_climate_enum_member(enumClass: type[Enum]) -> vol.All: + """Transform a string to an enum member. + + Backwards compatible with member names of xknx 2.x climate DPT Enums + due to unintentional breaking change in HA 2024.8. + """ + + def _string_transform(value: Any) -> str: + """Upper and slugify string and substitute old member names. + + Previously this was checked against Enum values instead of names. These + looked like `FAN_ONLY = "Fan only"`, therefore the upper & replace part. + """ + if not isinstance(value, str): + raise vol.Invalid("value should be a string") + name = value.upper().replace(" ", "_") + match name: + case "NIGHT": + return "ECONOMY" + case "FROST_PROTECTION": + return "BUILDING_PROTECTION" + case "DRY": + return "DEHUMIDIFICATION" + case _: + return name + + return vol.All( + _string_transform, + vol.In(enumClass.__members__), + enumClass.__getitem__, + ) From 0058d42ca274d2e07e581ce1ff8b221bb2d4f046 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Aug 2024 11:49:47 +0200 Subject: [PATCH 1868/2411] Correct squeezebox service (#123060) --- homeassistant/components/squeezebox/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 552b8ed800c..97d55c2f2ef 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -185,7 +185,7 @@ async def async_setup_entry( {vol.Required(ATTR_OTHER_PLAYER): cv.string}, "async_sync", ) - platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync") + platform.async_register_entity_service(SERVICE_UNSYNC, {}, "async_unsync") # Start server discovery task if not already running entry.async_on_unload(async_at_start(hass, start_server_discovery)) From 449afe9e6fadcd1d798675ae0f51a466a2981493 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Aug 2024 11:58:07 +0200 Subject: [PATCH 1869/2411] Correct type annotation for `EntityPlatform.async_register_entity_service` (#123054) Correct type annotation for EntityPlatform.async_register_entity_service Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/helpers/entity_platform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index d868e582f8f..6774780f00f 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -985,7 +985,7 @@ class EntityPlatform: def async_register_entity_service( self, name: str, - schema: VolDictType | VolSchemaType | None, + schema: VolDictType | VolSchemaType, func: str | Callable[..., Any], required_features: Iterable[int] | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, From adf85156984e14b17cb0107e0078ae7c8285f794 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Aug 2024 12:08:44 +0200 Subject: [PATCH 1870/2411] OpenAI make supported features reflect the config entry options (#123047) --- .../openai_conversation/conversation.py | 15 +++++++++++++++ .../openai_conversation/test_conversation.py | 2 ++ 2 files changed, 17 insertions(+) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 483b37945d6..b482126e27c 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -23,6 +23,7 @@ from voluptuous_openapi import convert from homeassistant.components import assist_pipeline, conversation from homeassistant.components.conversation import trace +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, TemplateError @@ -109,6 +110,9 @@ class OpenAIConversationEntity( self.hass, "conversation", self.entry.entry_id, self.entity_id ) conversation.async_set_agent(self.hass, self.entry, self) + self.entry.async_on_unload( + self.entry.add_update_listener(self._async_entry_update_listener) + ) async def async_will_remove_from_hass(self) -> None: """When entity will be removed from Home Assistant.""" @@ -319,3 +323,14 @@ class OpenAIConversationEntity( return conversation.ConversationResult( response=intent_response, conversation_id=conversation_id ) + + async def _async_entry_update_listener( + self, hass: HomeAssistant, entry: ConfigEntry + ) -> None: + """Handle options update.""" + if entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) + else: + self._attr_supported_features = conversation.ConversationEntityFeature(0) diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index 3364d822245..e0665bc449f 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -521,6 +521,8 @@ async def test_unknown_hass_api( }, ) + await hass.async_block_till_done() + result = await conversation.async_converse( hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id ) From b2d5f9c742ff1002a841d69728782afed80a1795 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:17:51 +0200 Subject: [PATCH 1871/2411] Update generator typing (#123052) --- tests/components/bluesound/conftest.py | 2 +- tests/components/bryant_evolution/conftest.py | 4 ++-- .../bryant_evolution/test_climate.py | 14 ++++++------- tests/components/elevenlabs/conftest.py | 6 +++--- tests/components/iotty/conftest.py | 20 +++++++++---------- tests/components/iron_os/conftest.py | 2 +- tests/components/iron_os/test_number.py | 2 +- tests/components/iron_os/test_sensor.py | 2 +- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/tests/components/bluesound/conftest.py b/tests/components/bluesound/conftest.py index 02c73bcd62f..5d81b6863c6 100644 --- a/tests/components/bluesound/conftest.py +++ b/tests/components/bluesound/conftest.py @@ -40,7 +40,7 @@ def sync_status() -> SyncStatus: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.bluesound.async_setup_entry", return_value=True diff --git a/tests/components/bryant_evolution/conftest.py b/tests/components/bryant_evolution/conftest.py index cc9dfbec1e1..fb12d7ebf29 100644 --- a/tests/components/bryant_evolution/conftest.py +++ b/tests/components/bryant_evolution/conftest.py @@ -15,7 +15,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.bryant_evolution.async_setup_entry", return_value=True @@ -31,7 +31,7 @@ for the Bryant Evolution integration. @pytest.fixture(autouse=True) -def mock_evolution_client_factory() -> Generator[AsyncMock, None, None]: +def mock_evolution_client_factory() -> Generator[AsyncMock]: """Mock an Evolution client.""" with patch( "evolutionhttp.BryantEvolutionLocalClient.get_client", diff --git a/tests/components/bryant_evolution/test_climate.py b/tests/components/bryant_evolution/test_climate.py index 42944c32bc2..0b527e02a10 100644 --- a/tests/components/bryant_evolution/test_climate.py +++ b/tests/components/bryant_evolution/test_climate.py @@ -52,7 +52,7 @@ async def test_setup_integration_success( async def test_set_temperature_mode_cool( hass: HomeAssistant, mock_evolution_entry: MockConfigEntry, - mock_evolution_client_factory: Generator[AsyncMock, None, None], + mock_evolution_client_factory: Generator[AsyncMock], freezer: FrozenDateTimeFactory, ) -> None: """Test setting the temperature in cool mode.""" @@ -83,7 +83,7 @@ async def test_set_temperature_mode_cool( async def test_set_temperature_mode_heat( hass: HomeAssistant, mock_evolution_entry: MockConfigEntry, - mock_evolution_client_factory: Generator[AsyncMock, None, None], + mock_evolution_client_factory: Generator[AsyncMock], freezer: FrozenDateTimeFactory, ) -> None: """Test setting the temperature in heat mode.""" @@ -111,7 +111,7 @@ async def test_set_temperature_mode_heat( async def test_set_temperature_mode_heat_cool( hass: HomeAssistant, mock_evolution_entry: MockConfigEntry, - mock_evolution_client_factory: Generator[AsyncMock, None, None], + mock_evolution_client_factory: Generator[AsyncMock], freezer: FrozenDateTimeFactory, ) -> None: """Test setting the temperature in heat_cool mode.""" @@ -147,7 +147,7 @@ async def test_set_temperature_mode_heat_cool( async def test_set_fan_mode( hass: HomeAssistant, mock_evolution_entry: MockConfigEntry, - mock_evolution_client_factory: Generator[AsyncMock, None, None], + mock_evolution_client_factory: Generator[AsyncMock], ) -> None: """Test that setting fan mode works.""" mock_client = await mock_evolution_client_factory(1, 1, "/dev/unused") @@ -175,7 +175,7 @@ async def test_set_fan_mode( async def test_set_hvac_mode( hass: HomeAssistant, mock_evolution_entry: MockConfigEntry, - mock_evolution_client_factory: Generator[AsyncMock, None, None], + mock_evolution_client_factory: Generator[AsyncMock], hvac_mode, evolution_mode, ) -> None: @@ -203,7 +203,7 @@ async def test_set_hvac_mode( async def test_read_hvac_action_heat_cool( hass: HomeAssistant, mock_evolution_entry: MockConfigEntry, - mock_evolution_client_factory: Generator[AsyncMock, None, None], + mock_evolution_client_factory: Generator[AsyncMock], freezer: FrozenDateTimeFactory, curr_temp: int, expected_action: HVACAction, @@ -236,7 +236,7 @@ async def test_read_hvac_action_heat_cool( async def test_read_hvac_action( hass: HomeAssistant, mock_evolution_entry: MockConfigEntry, - mock_evolution_client_factory: Generator[AsyncMock, None, None], + mock_evolution_client_factory: Generator[AsyncMock], freezer: FrozenDateTimeFactory, mode: str, active: bool, diff --git a/tests/components/elevenlabs/conftest.py b/tests/components/elevenlabs/conftest.py index 13eb022243f..c4d9a87b5ad 100644 --- a/tests/components/elevenlabs/conftest.py +++ b/tests/components/elevenlabs/conftest.py @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.elevenlabs.async_setup_entry", return_value=True @@ -25,7 +25,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_async_client() -> Generator[AsyncMock, None, None]: +def mock_async_client() -> Generator[AsyncMock]: """Override async ElevenLabs client.""" client_mock = AsyncMock() client_mock.voices.get_all.return_value = GetVoicesResponse(voices=MOCK_VOICES) @@ -37,7 +37,7 @@ def mock_async_client() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_async_client_fail() -> Generator[AsyncMock, None, None]: +def mock_async_client_fail() -> Generator[AsyncMock]: """Override async ElevenLabs client.""" with patch( "homeassistant.components.elevenlabs.config_flow.AsyncElevenLabs", diff --git a/tests/components/iotty/conftest.py b/tests/components/iotty/conftest.py index 7961a4ce3a1..9f858879cb9 100644 --- a/tests/components/iotty/conftest.py +++ b/tests/components/iotty/conftest.py @@ -65,7 +65,7 @@ def aiohttp_client_session() -> None: @pytest.fixture -def mock_aioclient() -> Generator[AiohttpClientMocker, None, None]: +def mock_aioclient() -> Generator[AiohttpClientMocker]: """Fixture to mock aioclient calls.""" with mock_aiohttp_client() as mock_session: yield mock_session @@ -96,7 +96,7 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: @pytest.fixture -def mock_config_entries_async_forward_entry_setup() -> Generator[AsyncMock, None, None]: +def mock_config_entries_async_forward_entry_setup() -> Generator[AsyncMock]: """Mock async_forward_entry_setup.""" with patch( "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups" @@ -105,7 +105,7 @@ def mock_config_entries_async_forward_entry_setup() -> Generator[AsyncMock, None @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: +def mock_setup_entry() -> Generator[AsyncMock]: """Mock setting up a config entry.""" with patch( "homeassistant.components.iotty.async_setup_entry", return_value=True @@ -114,7 +114,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_iotty() -> Generator[None, MagicMock, None]: +def mock_iotty() -> Generator[MagicMock]: """Mock IottyProxy.""" with patch( "homeassistant.components.iotty.api.IottyProxy", autospec=True @@ -123,7 +123,7 @@ def mock_iotty() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_coordinator() -> Generator[None, MagicMock, None]: +def mock_coordinator() -> Generator[MagicMock]: """Mock IottyDataUpdateCoordinator.""" with patch( "homeassistant.components.iotty.coordinator.IottyDataUpdateCoordinator", @@ -133,7 +133,7 @@ def mock_coordinator() -> Generator[None, MagicMock, None]: @pytest.fixture -def mock_get_devices_nodevices() -> Generator[AsyncMock, None, None]: +def mock_get_devices_nodevices() -> Generator[AsyncMock]: """Mock for get_devices, returning two objects.""" with patch("iottycloud.cloudapi.CloudApi.get_devices") as mock_fn: @@ -141,7 +141,7 @@ def mock_get_devices_nodevices() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_get_devices_twolightswitches() -> Generator[AsyncMock, None, None]: +def mock_get_devices_twolightswitches() -> Generator[AsyncMock]: """Mock for get_devices, returning two objects.""" with patch( @@ -151,7 +151,7 @@ def mock_get_devices_twolightswitches() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_command_fn() -> Generator[AsyncMock, None, None]: +def mock_command_fn() -> Generator[AsyncMock]: """Mock for command.""" with patch("iottycloud.cloudapi.CloudApi.command", return_value=None) as mock_fn: @@ -159,7 +159,7 @@ def mock_command_fn() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_get_status_filled_off() -> Generator[AsyncMock, None, None]: +def mock_get_status_filled_off() -> Generator[AsyncMock]: """Mock setting up a get_status.""" retval = {RESULT: {STATUS: STATUS_OFF}} @@ -170,7 +170,7 @@ def mock_get_status_filled_off() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_get_status_filled() -> Generator[AsyncMock, None, None]: +def mock_get_status_filled() -> Generator[AsyncMock]: """Mock setting up a get_status.""" retval = {RESULT: {STATUS: STATUS_ON}} diff --git a/tests/components/iron_os/conftest.py b/tests/components/iron_os/conftest.py index b6983074441..f489d7b7bb5 100644 --- a/tests/components/iron_os/conftest.py +++ b/tests/components/iron_os/conftest.py @@ -108,7 +108,7 @@ def mock_ble_device() -> Generator[MagicMock]: @pytest.fixture -def mock_pynecil() -> Generator[AsyncMock, None, None]: +def mock_pynecil() -> Generator[AsyncMock]: """Mock Pynecil library.""" with patch( "homeassistant.components.iron_os.Pynecil", autospec=True diff --git a/tests/components/iron_os/test_number.py b/tests/components/iron_os/test_number.py index c091040668c..781492987ee 100644 --- a/tests/components/iron_os/test_number.py +++ b/tests/components/iron_os/test_number.py @@ -22,7 +22,7 @@ from tests.common import MockConfigEntry, snapshot_platform @pytest.fixture(autouse=True) -async def sensor_only() -> AsyncGenerator[None, None]: +async def sensor_only() -> AsyncGenerator[None]: """Enable only the number platform.""" with patch( "homeassistant.components.iron_os.PLATFORMS", diff --git a/tests/components/iron_os/test_sensor.py b/tests/components/iron_os/test_sensor.py index 0c35193e400..2f79487a7fd 100644 --- a/tests/components/iron_os/test_sensor.py +++ b/tests/components/iron_os/test_sensor.py @@ -18,7 +18,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_plat @pytest.fixture(autouse=True) -async def sensor_only() -> AsyncGenerator[None, None]: +async def sensor_only() -> AsyncGenerator[None]: """Enable only the sensor platform.""" with patch( "homeassistant.components.iron_os.PLATFORMS", From 7670ad0a7217db5baf923af85e12b3fc07c1a630 Mon Sep 17 00:00:00 2001 From: Fabian <115155196+Fabiann2205@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:19:55 +0200 Subject: [PATCH 1872/2411] Add device class (#123059) --- homeassistant/components/google_travel_time/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 6c45033eeb7..618dda50bd4 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -8,7 +8,11 @@ import logging from googlemaps import Client from googlemaps.distance_matrix import distance_matrix -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, @@ -72,6 +76,8 @@ class GoogleTravelTimeSensor(SensorEntity): _attr_attribution = ATTRIBUTION _attr_native_unit_of_measurement = UnitOfTime.MINUTES + _attr_device_class = SensorDeviceClass.DURATION + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, config_entry, name, api_key, origin, destination, client): """Initialize the sensor.""" From 5446dd92a977615f3e83cdb9469c900f503a5824 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 2 Aug 2024 06:22:36 -0400 Subject: [PATCH 1873/2411] Make ZHA load quirks earlier (#123027) --- homeassistant/components/zha/__init__.py | 4 +++- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zha/test_repairs.py | 10 +++++----- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 216261e3011..fc573b19ab1 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -117,6 +117,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ha_zha_data.config_entry = config_entry zha_lib_data: ZHAData = create_zha_config(hass, ha_zha_data) + zha_gateway = await Gateway.async_from_config(zha_lib_data) + # Load and cache device trigger information early device_registry = dr.async_get(hass) radio_mgr = ZhaRadioManager.from_config_entry(hass, config_entry) @@ -140,7 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b _LOGGER.debug("Trigger cache: %s", zha_lib_data.device_trigger_cache) try: - zha_gateway = await Gateway.async_from_config(zha_lib_data) + await zha_gateway.async_initialize() except NetworkSettingsInconsistent as exc: await warn_on_inconsistent_network_settings( hass, diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index d2d328cc84b..6e35339c53f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.24"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.25"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 1df7cc12072..602c03c879d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2989,7 +2989,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.24 +zha==0.0.25 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5831f7c23cf..623f0953721 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2363,7 +2363,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.24 +zha==0.0.25 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index 7f9b2b4a016..c2925161748 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -148,7 +148,7 @@ async def test_multipan_firmware_repair( autospec=True, ), patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=RuntimeError(), ), patch( @@ -199,7 +199,7 @@ async def test_multipan_firmware_no_repair_on_probe_failure( autospec=True, ), patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=RuntimeError(), ), ): @@ -236,7 +236,7 @@ async def test_multipan_firmware_retry_on_probe_ezsp( autospec=True, ), patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=RuntimeError(), ), ): @@ -311,7 +311,7 @@ async def test_inconsistent_settings_keep_new( old_state = network_backup with patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=NetworkSettingsInconsistent( message="Network settings are inconsistent", new_state=new_state, @@ -390,7 +390,7 @@ async def test_inconsistent_settings_restore_old( old_state = network_backup with patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=NetworkSettingsInconsistent( message="Network settings are inconsistent", new_state=new_state, From ad26db7dc8d72203c79c5a0b64362be06eec3b99 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:24:03 +0200 Subject: [PATCH 1874/2411] Replace pylint broad-exception-raised rule with ruff (#123021) --- homeassistant/components/fritz/coordinator.py | 3 +-- homeassistant/components/starline/config_flow.py | 3 +-- pyproject.toml | 2 +- .../templates/config_flow_helper/tests/test_config_flow.py | 2 +- .../components/bluetooth/test_passive_update_processor.py | 6 ++---- tests/components/notify/test_legacy.py | 2 +- tests/components/profiler/test_init.py | 2 +- tests/components/roon/test_config_flow.py | 2 +- tests/components/stt/test_legacy.py | 2 +- tests/components/system_health/test_init.py | 2 +- tests/components/system_log/test_init.py | 2 +- tests/components/tts/test_init.py | 4 ++-- tests/components/tts/test_legacy.py | 2 +- tests/helpers/test_dispatcher.py | 6 ++---- tests/test_config_entries.py | 2 +- tests/test_core.py | 4 ++-- tests/test_runner.py | 5 ++--- tests/test_setup.py | 7 +++---- tests/util/test_logging.py | 3 +-- 19 files changed, 26 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/fritz/coordinator.py b/homeassistant/components/fritz/coordinator.py index 592bf37084e..a67f385f3e8 100644 --- a/homeassistant/components/fritz/coordinator.py +++ b/homeassistant/components/fritz/coordinator.py @@ -568,8 +568,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): self.fritz_hosts.get_mesh_topology ) ): - # pylint: disable-next=broad-exception-raised - raise Exception("Mesh supported but empty topology reported") + raise Exception("Mesh supported but empty topology reported") # noqa: TRY002 except FritzActionError: self.mesh_role = MeshRoles.SLAVE # Avoid duplicating device trackers diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py index c13586d0bc3..6c38603a843 100644 --- a/homeassistant/components/starline/config_flow.py +++ b/homeassistant/components/starline/config_flow.py @@ -214,8 +214,7 @@ class StarlineFlowHandler(ConfigFlow, domain=DOMAIN): self._captcha_image = data["captchaImg"] return self._async_form_auth_captcha(error) - # pylint: disable=broad-exception-raised - raise Exception(data) + raise Exception(data) # noqa: TRY002 except Exception as err: # noqa: BLE001 _LOGGER.error("Error auth user: %s", err) return self._async_form_auth_user(ERROR_AUTH_USER) diff --git a/pyproject.toml b/pyproject.toml index eadd529b3e6..eb4bfc4970d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -312,6 +312,7 @@ disable = [ "no-else-return", # RET505 "broad-except", # BLE001 "protected-access", # SLF001 + "broad-exception-raised", # TRY002 # "no-self-use", # PLR6301 # Optional plugin, not enabled # Handled by mypy @@ -823,7 +824,6 @@ ignore = [ "PYI024", # Use typing.NamedTuple instead of collections.namedtuple "RET503", "RET501", - "TRY002", "TRY301" ] 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 809902fa0dd..8e7854835d8 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 @@ -59,7 +59,7 @@ def get_suggested(schema, key): return None return k.description["suggested_value"] # Wanted key absent from schema - raise Exception + raise KeyError(f"Key `{key}` is missing from schema") @pytest.mark.parametrize("platform", ["sensor"]) diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 079ac2200fc..d7a7a8ba08c 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -583,8 +583,7 @@ async def test_exception_from_update_method( nonlocal run_count run_count += 1 if run_count == 2: - # pylint: disable-next=broad-exception-raised - raise Exception("Test exception") + raise Exception("Test exception") # noqa: TRY002 return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( @@ -1418,8 +1417,7 @@ async def test_exception_from_coordinator_update_method( nonlocal run_count run_count += 1 if run_count == 2: - # pylint: disable-next=broad-exception-raised - raise Exception("Test exception") + raise Exception("Test exception") # noqa: TRY002 return {"test": "data"} @callback diff --git a/tests/components/notify/test_legacy.py b/tests/components/notify/test_legacy.py index b499486b312..8be80650053 100644 --- a/tests/components/notify/test_legacy.py +++ b/tests/components/notify/test_legacy.py @@ -265,7 +265,7 @@ async def test_platform_setup_with_error( async def async_get_service(hass, config, discovery_info=None): """Return None for an invalid notify service.""" - raise Exception("Setup error") # pylint: disable=broad-exception-raised + raise Exception("Setup error") # noqa: TRY002 mock_notify_platform( hass, tmp_path, "testnotify", async_get_service=async_get_service diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index 2eca84b43fe..b172ae0e12d 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -181,7 +181,7 @@ async def test_dump_log_object( def __repr__(self): if self.fail: - raise Exception("failed") # pylint: disable=broad-exception-raised + raise Exception("failed") # noqa: TRY002 return "" obj1 = DumpLogDummy(False) diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index 9822c88fa48..9539a9c0f5b 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -48,7 +48,7 @@ class RoonApiMockException(RoonApiMock): @property def token(self): """Throw exception.""" - raise Exception # pylint: disable=broad-exception-raised + raise Exception # noqa: TRY002 class RoonDiscoveryMock: diff --git a/tests/components/stt/test_legacy.py b/tests/components/stt/test_legacy.py index 04068b012f1..20fa86b4d20 100644 --- a/tests/components/stt/test_legacy.py +++ b/tests/components/stt/test_legacy.py @@ -41,7 +41,7 @@ async def test_platform_setup_with_error( discovery_info: DiscoveryInfoType | None = None, ) -> Provider: """Raise exception during platform setup.""" - raise Exception("Setup error") # pylint: disable=broad-exception-raised + raise Exception("Setup error") # noqa: TRY002 mock_stt_platform(hass, tmp_path, "bad_stt", async_get_engine=async_get_engine) diff --git a/tests/components/system_health/test_init.py b/tests/components/system_health/test_init.py index e51ab8fab99..b93dccffb92 100644 --- a/tests/components/system_health/test_init.py +++ b/tests/components/system_health/test_init.py @@ -110,7 +110,7 @@ async def test_info_endpoint_register_callback_exc( """Test that the info endpoint requires auth.""" async def mock_info(hass): - raise Exception("TEST ERROR") # pylint: disable=broad-exception-raised + raise Exception("TEST ERROR") # noqa: TRY002 async_register_info(hass, "lovelace", mock_info) assert await async_setup_component(hass, "system_health", {}) diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index fb46d120acf..1f1c4464c71 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -36,7 +36,7 @@ async def get_error_log(hass_ws_client): def _generate_and_log_exception(exception, log): try: - raise Exception(exception) # pylint: disable=broad-exception-raised + raise Exception(exception) # noqa: TRY002 except Exception: _LOGGER.exception(log) diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index bf44f120134..7a54ecc26b0 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -1016,7 +1016,7 @@ class MockProviderBoom(MockProvider): ) -> tts.TtsAudioType: """Load TTS dat.""" # This should not be called, data should be fetched from cache - raise Exception("Boom!") # pylint: disable=broad-exception-raised + raise Exception("Boom!") # noqa: TRY002 class MockEntityBoom(MockTTSEntity): @@ -1027,7 +1027,7 @@ class MockEntityBoom(MockTTSEntity): ) -> tts.TtsAudioType: """Load TTS dat.""" # This should not be called, data should be fetched from cache - raise Exception("Boom!") # pylint: disable=broad-exception-raised + raise Exception("Boom!") # noqa: TRY002 @pytest.mark.parametrize("mock_provider", [MockProviderBoom(DEFAULT_LANG)]) diff --git a/tests/components/tts/test_legacy.py b/tests/components/tts/test_legacy.py index 05bb6dec10f..0d7f99e8cd1 100644 --- a/tests/components/tts/test_legacy.py +++ b/tests/components/tts/test_legacy.py @@ -123,7 +123,7 @@ async def test_platform_setup_with_error( discovery_info: DiscoveryInfoType | None = None, ) -> Provider: """Raise exception during platform setup.""" - raise Exception("Setup error") # pylint: disable=broad-exception-raised + raise Exception("Setup error") # noqa: TRY002 mock_integration(hass, MockModule(domain="bad_tts")) mock_platform(hass, "bad_tts.tts", BadPlatform(mock_provider)) diff --git a/tests/helpers/test_dispatcher.py b/tests/helpers/test_dispatcher.py index c2c8663f47c..0350b2e6e3a 100644 --- a/tests/helpers/test_dispatcher.py +++ b/tests/helpers/test_dispatcher.py @@ -188,8 +188,7 @@ async def test_callback_exception_gets_logged( @callback def bad_handler(*args): """Record calls.""" - # pylint: disable-next=broad-exception-raised - raise Exception("This is a bad message callback") + raise Exception("This is a bad message callback") # noqa: TRY002 # wrap in partial to test message logging. async_dispatcher_connect(hass, "test", partial(bad_handler)) @@ -209,8 +208,7 @@ async def test_coro_exception_gets_logged( async def bad_async_handler(*args): """Record calls.""" - # pylint: disable-next=broad-exception-raised - raise Exception("This is a bad message in a coro") + raise Exception("This is a bad message in a coro") # noqa: TRY002 # wrap in partial to test message logging. async_dispatcher_connect(hass, "test", bad_async_handler) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 2a5dff5c14a..9983886ce44 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -584,7 +584,7 @@ async def test_remove_entry_raises( async def mock_unload_entry(hass, entry): """Mock unload entry function.""" - raise Exception("BROKEN") # pylint: disable=broad-exception-raised + raise Exception("BROKEN") # noqa: TRY002 mock_integration(hass, MockModule("comp", async_unload_entry=mock_unload_entry)) diff --git a/tests/test_core.py b/tests/test_core.py index 8035236fd08..9ca57d1563f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -424,11 +424,11 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: try: if ha.async_get_hass() is hass: return True - raise Exception # pylint: disable=broad-exception-raised + raise Exception # noqa: TRY002 except HomeAssistantError: return False - raise Exception # pylint: disable=broad-exception-raised + raise Exception # noqa: TRY002 # Test scheduling a coroutine which calls async_get_hass via hass.async_create_task async def _async_create_task() -> None: diff --git a/tests/test_runner.py b/tests/test_runner.py index 141af4f4bc7..c61b8ed5628 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -105,7 +105,7 @@ def test_run_does_not_block_forever_with_shielded_task( try: await asyncio.sleep(2) except asyncio.CancelledError: - raise Exception # pylint: disable=broad-exception-raised + raise Exception # noqa: TRY002 async def async_shielded(*_): try: @@ -142,8 +142,7 @@ async def test_unhandled_exception_traceback( async def _unhandled_exception(): raised.set() - # pylint: disable-next=broad-exception-raised - raise Exception("This is unhandled") + raise Exception("This is unhandled") # noqa: TRY002 try: hass.loop.set_debug(True) diff --git a/tests/test_setup.py b/tests/test_setup.py index 3430c17960c..fae2b46f18d 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -338,7 +338,7 @@ async def test_component_exception_setup(hass: HomeAssistant) -> None: def exception_setup(hass, config): """Raise exception.""" - raise Exception("fail!") # pylint: disable=broad-exception-raised + raise Exception("fail!") # noqa: TRY002 mock_integration(hass, MockModule("comp", setup=exception_setup)) @@ -352,7 +352,7 @@ async def test_component_base_exception_setup(hass: HomeAssistant) -> None: def exception_setup(hass, config): """Raise exception.""" - raise BaseException("fail!") # pylint: disable=broad-exception-raised + raise BaseException("fail!") # noqa: TRY002 mock_integration(hass, MockModule("comp", setup=exception_setup)) @@ -372,8 +372,7 @@ async def test_component_setup_with_validation_and_dependency( """Test that config is passed in.""" if config.get("comp_a", {}).get("valid", False): return True - # pylint: disable-next=broad-exception-raised - raise Exception(f"Config not passed in: {config}") + raise Exception(f"Config not passed in: {config}") # noqa: TRY002 platform = MockPlatform() diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 4667dbcbec8..795444c89bd 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -80,8 +80,7 @@ async def test_async_create_catching_coro( """Test exception logging of wrapped coroutine.""" async def job(): - # pylint: disable-next=broad-exception-raised - raise Exception("This is a bad coroutine") + raise Exception("This is a bad coroutine") # noqa: TRY002 hass.async_create_task(logging_util.async_create_catching_coro(job())) await hass.async_block_till_done() From 4a06e20318ba50fddb4be1f3ea20a988075622f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Aug 2024 12:31:31 +0200 Subject: [PATCH 1875/2411] Ollama implement CONTROL supported feature (#123049) --- .../components/ollama/conversation.py | 18 +++++++++++++ tests/components/ollama/test_conversation.py | 25 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ollama/conversation.py b/homeassistant/components/ollama/conversation.py index f59e268394b..9f66083f506 100644 --- a/homeassistant/components/ollama/conversation.py +++ b/homeassistant/components/ollama/conversation.py @@ -106,6 +106,10 @@ class OllamaConversationEntity( self._history: dict[str, MessageHistory] = {} self._attr_name = entry.title self._attr_unique_id = entry.entry_id + if self.entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) async def async_added_to_hass(self) -> None: """When entity is added to Home Assistant.""" @@ -114,6 +118,9 @@ class OllamaConversationEntity( self.hass, "conversation", self.entry.entry_id, self.entity_id ) conversation.async_set_agent(self.hass, self.entry, self) + self.entry.async_on_unload( + self.entry.add_update_listener(self._async_entry_update_listener) + ) async def async_will_remove_from_hass(self) -> None: """When entity will be removed from Home Assistant.""" @@ -334,3 +341,14 @@ class OllamaConversationEntity( message_history.messages = [ message_history.messages[0] ] + message_history.messages[drop_index:] + + async def _async_entry_update_listener( + self, hass: HomeAssistant, entry: ConfigEntry + ) -> None: + """Handle options update.""" + if entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) + else: + self._attr_supported_features = conversation.ConversationEntityFeature(0) diff --git a/tests/components/ollama/test_conversation.py b/tests/components/ollama/test_conversation.py index b5a94cc6f57..c83dce3b565 100644 --- a/tests/components/ollama/test_conversation.py +++ b/tests/components/ollama/test_conversation.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import conversation, ollama from homeassistant.components.conversation import trace -from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL +from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_LLM_HASS_API, MATCH_ALL from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import intent, llm @@ -554,3 +554,26 @@ async def test_conversation_agent( mock_config_entry.entry_id ) assert agent.supported_languages == MATCH_ALL + + state = hass.states.get("conversation.mock_title") + assert state + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + +async def test_conversation_agent_with_assist( + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_init_component, +) -> None: + """Test OllamaConversationEntity.""" + agent = conversation.get_agent_manager(hass).async_get_agent( + mock_config_entry_with_assist.entry_id + ) + assert agent.supported_languages == MATCH_ALL + + state = hass.states.get("conversation.mock_title") + assert state + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == conversation.ConversationEntityFeature.CONTROL + ) From 42234e6a09b67bf95f46ceb2690040ed55ef5deb Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 2 Aug 2024 12:53:39 +0200 Subject: [PATCH 1876/2411] Address post-merge reviews for KNX integration (#123038) --- homeassistant/components/knx/__init__.py | 2 +- homeassistant/components/knx/binary_sensor.py | 13 +++---- homeassistant/components/knx/button.py | 14 ++++---- homeassistant/components/knx/climate.py | 14 +++++--- homeassistant/components/knx/cover.py | 13 +++---- homeassistant/components/knx/date.py | 14 +++++--- homeassistant/components/knx/datetime.py | 12 ++++--- homeassistant/components/knx/fan.py | 13 +++---- homeassistant/components/knx/knx_entity.py | 36 ++++++++++++++++--- homeassistant/components/knx/light.py | 30 ++++++++-------- homeassistant/components/knx/notify.py | 14 +++++--- homeassistant/components/knx/number.py | 12 ++++--- homeassistant/components/knx/project.py | 6 ++-- homeassistant/components/knx/scene.py | 13 +++---- homeassistant/components/knx/select.py | 12 ++++--- homeassistant/components/knx/sensor.py | 17 +++++---- .../components/knx/storage/config_store.py | 28 ++++++++------- homeassistant/components/knx/switch.py | 30 ++++++++-------- homeassistant/components/knx/text.py | 12 ++++--- homeassistant/components/knx/time.py | 17 +++++---- homeassistant/components/knx/weather.py | 14 +++++--- 21 files changed, 211 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 709a82b31fd..fd46cad8489 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -302,7 +302,7 @@ class KNXModule: self.entry = entry self.project = KNXProject(hass=hass, entry=entry) - self.config_store = KNXConfigStore(hass=hass, entry=entry) + self.config_store = KNXConfigStore(hass=hass, config_entry=entry) self.xknx = XKNX( connection_config=self.connection_config(), diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 0423c1d7b32..ff15f725fae 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any -from xknx import XKNX from xknx.devices import BinarySensor as XknxBinarySensor from homeassistant import config_entries @@ -23,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ATTR_COUNTER, ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import BinarySensorSchema @@ -34,11 +34,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the KNX binary sensor platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: ConfigType = hass.data[DATA_KNX_CONFIG] async_add_entities( - KNXBinarySensor(xknx, entity_config) + KNXBinarySensor(knx_module, entity_config) for entity_config in config[Platform.BINARY_SENSOR] ) @@ -48,11 +48,12 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): _device: XknxBinarySensor - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX binary sensor.""" super().__init__( + knx_module=knx_module, device=XknxBinarySensor( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address_state=config[BinarySensorSchema.CONF_STATE_ADDRESS], invert=config[BinarySensorSchema.CONF_INVERT], @@ -62,7 +63,7 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): ], context_timeout=config.get(BinarySensorSchema.CONF_CONTEXT_TIMEOUT), reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER), - ) + ), ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_device_class = config.get(CONF_DEVICE_CLASS) diff --git a/homeassistant/components/knx/button.py b/homeassistant/components/knx/button.py index a38d8ad1b6c..2eb68eebe43 100644 --- a/homeassistant/components/knx/button.py +++ b/homeassistant/components/knx/button.py @@ -2,7 +2,6 @@ from __future__ import annotations -from xknx import XKNX from xknx.devices import RawValue as XknxRawValue from homeassistant import config_entries @@ -12,6 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import CONF_PAYLOAD_LENGTH, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity @@ -22,11 +22,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the KNX binary sensor platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: ConfigType = hass.data[DATA_KNX_CONFIG] async_add_entities( - KNXButton(xknx, entity_config) for entity_config in config[Platform.BUTTON] + KNXButton(knx_module, entity_config) + for entity_config in config[Platform.BUTTON] ) @@ -35,15 +36,16 @@ class KNXButton(KnxEntity, ButtonEntity): _device: XknxRawValue - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX button.""" super().__init__( + knx_module=knx_module, device=XknxRawValue( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], payload_length=config[CONF_PAYLOAD_LENGTH], group_address=config[KNX_ADDRESS], - ) + ), ) self._payload = config[CONF_PAYLOAD] self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 26be6a03a79..7470d60ef4b 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -27,6 +27,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, @@ -48,10 +49,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up climate(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.CLIMATE] - async_add_entities(KNXClimate(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXClimate(knx_module, entity_config) for entity_config in config + ) def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate: @@ -137,9 +140,12 @@ class KNXClimate(KnxEntity, ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _enable_turn_on_off_backwards_compatibility = False - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of a KNX climate device.""" - super().__init__(_create_climate(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_climate(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE if self._device.supports_on_off: diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 9d86d6ac272..1962db0ad3f 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Callable from typing import Any -from xknx import XKNX from xknx.devices import Cover as XknxCover from homeassistant import config_entries @@ -26,6 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import CoverSchema @@ -37,10 +37,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up cover(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.COVER] - async_add_entities(KNXCover(xknx, entity_config) for entity_config in config) + async_add_entities(KNXCover(knx_module, entity_config) for entity_config in config) class KNXCover(KnxEntity, CoverEntity): @@ -48,11 +48,12 @@ class KNXCover(KnxEntity, CoverEntity): _device: XknxCover - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize the cover.""" super().__init__( + knx_module=knx_module, device=XknxCover( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS), group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS), @@ -70,7 +71,7 @@ class KNXCover(KnxEntity, CoverEntity): invert_updown=config[CoverSchema.CONF_INVERT_UPDOWN], invert_position=config[CoverSchema.CONF_INVERT_POSITION], invert_angle=config[CoverSchema.CONF_INVERT_ANGLE], - ) + ), ) self._unsubscribe_auto_updater: Callable[[], None] | None = None diff --git a/homeassistant/components/knx/date.py b/homeassistant/components/knx/date.py index 98cd22e0751..80fea63d0a6 100644 --- a/homeassistant/components/knx/date.py +++ b/homeassistant/components/knx/date.py @@ -22,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -39,10 +40,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATE] - async_add_entities(KNXDateEntity(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXDateEntity(knx_module, entity_config) for entity_config in config + ) def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateDevice: @@ -63,9 +66,12 @@ class KNXDateEntity(KnxEntity, DateEntity, RestoreEntity): _device: XknxDateDevice - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX time.""" - super().__init__(_create_xknx_device(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_xknx_device(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/datetime.py b/homeassistant/components/knx/datetime.py index d4a25b522eb..16ccb7474a7 100644 --- a/homeassistant/components/knx/datetime.py +++ b/homeassistant/components/knx/datetime.py @@ -23,6 +23,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -40,11 +41,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATETIME] async_add_entities( - KNXDateTimeEntity(xknx, entity_config) for entity_config in config + KNXDateTimeEntity(knx_module, entity_config) for entity_config in config ) @@ -66,9 +67,12 @@ class KNXDateTimeEntity(KnxEntity, DateTimeEntity, RestoreEntity): _device: XknxDateTimeDevice - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX time.""" - super().__init__(_create_xknx_device(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_xknx_device(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 426a750f766..940e241ccda 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -5,7 +5,6 @@ from __future__ import annotations import math from typing import Any, Final -from xknx import XKNX from xknx.devices import Fan as XknxFan from homeassistant import config_entries @@ -20,6 +19,7 @@ from homeassistant.util.percentage import ( ) from homeassistant.util.scaling import int_states_in_range +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import FanSchema @@ -33,10 +33,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up fan(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.FAN] - async_add_entities(KNXFan(xknx, entity_config) for entity_config in config) + async_add_entities(KNXFan(knx_module, entity_config) for entity_config in config) class KNXFan(KnxEntity, FanEntity): @@ -45,12 +45,13 @@ class KNXFan(KnxEntity, FanEntity): _device: XknxFan _enable_turn_on_off_backwards_compatibility = False - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX fan.""" max_step = config.get(FanSchema.CONF_MAX_STEP) super().__init__( + knx_module=knx_module, device=XknxFan( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address_speed=config.get(KNX_ADDRESS), group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS), @@ -61,7 +62,7 @@ class KNXFan(KnxEntity, FanEntity): FanSchema.CONF_OSCILLATION_STATE_ADDRESS ), max_step=max_step, - ) + ), ) # FanSpeedMode.STEP if max_step is set self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index eebddbb0623..2b8d2e71186 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -2,23 +2,29 @@ from __future__ import annotations -from typing import cast +from typing import TYPE_CHECKING from xknx.devices import Device as XknxDevice +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import KNXModule from .const import DOMAIN +if TYPE_CHECKING: + from . import KNXModule + +SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal.{{}}" + class KnxEntity(Entity): """Representation of a KNX entity.""" _attr_should_poll = False - def __init__(self, device: XknxDevice) -> None: + def __init__(self, knx_module: KNXModule, device: XknxDevice) -> None: """Set up device.""" + self._knx_module = knx_module self._device = device @property @@ -29,8 +35,7 @@ class KnxEntity(Entity): @property def available(self) -> bool: """Return True if entity is available.""" - knx_module = cast(KNXModule, self.hass.data[DOMAIN]) - return knx_module.connected + return self._knx_module.connected async def async_update(self) -> None: """Request a state update from KNX bus.""" @@ -44,8 +49,29 @@ class KnxEntity(Entity): """Store register state change callback and start device object.""" self._device.register_device_updated_cb(self.after_update_callback) self._device.xknx.devices.async_add(self._device) + # super call needed to have methods of mulit-inherited classes called + # eg. for restoring state (like _KNXSwitch) + await super().async_added_to_hass() async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" self._device.unregister_device_updated_cb(self.after_update_callback) self._device.xknx.devices.async_remove(self._device) + + +class KnxUIEntity(KnxEntity): + """Representation of a KNX UI entity.""" + + _attr_unique_id: str + + async def async_added_to_hass(self) -> None: + """Register callbacks when entity added to hass.""" + await super().async_added_to_hass() + self._knx_module.config_store.entities.add(self._attr_unique_id) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_ENTITY_REMOVE.format(self._attr_unique_id), + self.async_remove, + ) + ) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 8ec42f3ee56..1197f09354b 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -27,7 +27,7 @@ import homeassistant.util.color as color_util from . import KNXModule from .const import CONF_SYNC_STATE, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, ColorTempModes -from .knx_entity import KnxEntity +from .knx_entity import KnxEntity, KnxUIEntity from .schema import LightSchema from .storage.const import ( CONF_COLOR_TEMP_MAX, @@ -65,10 +65,10 @@ async def async_setup_entry( knx_module: KNXModule = hass.data[DOMAIN] entities: list[KnxEntity] = [] - if yaml_config := hass.data[DATA_KNX_CONFIG].get(Platform.LIGHT): + if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.LIGHT): entities.extend( - KnxYamlLight(knx_module.xknx, entity_config) - for entity_config in yaml_config + KnxYamlLight(knx_module, entity_config) + for entity_config in yaml_platform_config ) if ui_config := knx_module.config_store.data["entities"].get(Platform.LIGHT): entities.extend( @@ -294,7 +294,7 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight ) -class _KnxLight(KnxEntity, LightEntity): +class _KnxLight(LightEntity): """Representation of a KNX light.""" _attr_max_color_temp_kelvin: int @@ -519,14 +519,17 @@ class _KnxLight(KnxEntity, LightEntity): await self._device.set_off() -class KnxYamlLight(_KnxLight): +class KnxYamlLight(_KnxLight, KnxEntity): """Representation of a KNX light.""" _device: XknxLight - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX light.""" - super().__init__(_create_yaml_light(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_yaml_light(knx_module.xknx, config), + ) self._attr_max_color_temp_kelvin: int = config[LightSchema.CONF_MAX_KELVIN] self._attr_min_color_temp_kelvin: int = config[LightSchema.CONF_MIN_KELVIN] self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) @@ -543,20 +546,21 @@ class KnxYamlLight(_KnxLight): ) -class KnxUiLight(_KnxLight): +class KnxUiLight(_KnxLight, KnxUIEntity): """Representation of a KNX light.""" - _device: XknxLight _attr_has_entity_name = True + _device: XknxLight def __init__( self, knx_module: KNXModule, unique_id: str, config: ConfigType ) -> None: """Initialize of KNX light.""" super().__init__( - _create_ui_light( + knx_module=knx_module, + device=_create_ui_light( knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME] - ) + ), ) self._attr_max_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MAX] self._attr_min_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MIN] @@ -565,5 +569,3 @@ class KnxUiLight(_KnxLight): self._attr_unique_id = unique_id if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO): self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)}) - - knx_module.config_store.entities[unique_id] = self diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 997bdb81057..b349681990c 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity @@ -44,7 +45,7 @@ async def async_get_service( class KNXNotificationService(BaseNotificationService): - """Implement demo notification service.""" + """Implement notification service.""" def __init__(self, devices: list[XknxNotification]) -> None: """Initialize the service.""" @@ -86,10 +87,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up notify(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.NOTIFY] - async_add_entities(KNXNotify(xknx, entity_config) for entity_config in config) + async_add_entities(KNXNotify(knx_module, entity_config) for entity_config in config) def _create_notification_instance(xknx: XKNX, config: ConfigType) -> XknxNotification: @@ -107,9 +108,12 @@ class KNXNotify(KnxEntity, NotifyEntity): _device: XknxNotification - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX notification.""" - super().__init__(_create_notification_instance(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_notification_instance(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index 8a9f1dea87c..3d4af503dff 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -39,10 +40,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up number(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.NUMBER] - async_add_entities(KNXNumber(xknx, entity_config) for entity_config in config) + async_add_entities(KNXNumber(knx_module, entity_config) for entity_config in config) def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue: @@ -62,9 +63,12 @@ class KNXNumber(KnxEntity, RestoreNumber): _device: NumericValue - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX number.""" - super().__init__(_create_numeric_value(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_numeric_value(knx_module.xknx, config), + ) self._attr_native_max_value = config.get( NumberSchema.CONF_MAX, self._device.sensor_value.dpt_class.value_max, diff --git a/homeassistant/components/knx/project.py b/homeassistant/components/knx/project.py index 3b3309dfc7d..b5bafe00724 100644 --- a/homeassistant/components/knx/project.py +++ b/homeassistant/components/knx/project.py @@ -8,9 +8,11 @@ from typing import Final from xknx import XKNX from xknx.dpt import DPTBase +from xknx.telegram.address import DeviceAddressableType from xknxproject import XKNXProj from xknxproject.models import ( Device, + DPTType, GroupAddress as GroupAddressModel, KNXProject as KNXProjectModel, ProjectInfo, @@ -89,7 +91,7 @@ class KNXProject: self.devices = project["devices"] self.info = project["info"] xknx.group_address_dpt.clear() - xknx_ga_dict = {} + xknx_ga_dict: dict[DeviceAddressableType, DPTType] = {} for ga_model in project["group_addresses"].values(): ga_info = _create_group_address_info(ga_model) @@ -97,7 +99,7 @@ class KNXProject: if (dpt_model := ga_model.get("dpt")) is not None: xknx_ga_dict[ga_model["address"]] = dpt_model - xknx.group_address_dpt.set(xknx_ga_dict) # type: ignore[arg-type] + xknx.group_address_dpt.set(xknx_ga_dict) _LOGGER.debug( "Loaded KNX project data with %s group addresses from storage", diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index 342d0f9eb83..fc37f36dd01 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any -from xknx import XKNX from xknx.devices import Scene as XknxScene from homeassistant import config_entries @@ -14,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import SceneSchema @@ -25,10 +25,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up scene(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.SCENE] - async_add_entities(KNXScene(xknx, entity_config) for entity_config in config) + async_add_entities(KNXScene(knx_module, entity_config) for entity_config in config) class KNXScene(KnxEntity, Scene): @@ -36,15 +36,16 @@ class KNXScene(KnxEntity, Scene): _device: XknxScene - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Init KNX scene.""" super().__init__( + knx_module=knx_module, device=XknxScene( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address=config[KNX_ADDRESS], scene_number=config[SceneSchema.CONF_SCENE_NUMBER], - ) + ), ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = ( diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index f338bf9feaf..1b862010c2a 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -20,6 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_PAYLOAD_LENGTH, CONF_RESPOND_TO_READ, @@ -39,10 +40,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up select(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.SELECT] - async_add_entities(KNXSelect(xknx, entity_config) for entity_config in config) + async_add_entities(KNXSelect(knx_module, entity_config) for entity_config in config) def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue: @@ -63,9 +64,12 @@ class KNXSelect(KnxEntity, SelectEntity, RestoreEntity): _device: RawValue - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX select.""" - super().__init__(_create_raw_value(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_raw_value(knx_module.xknx, config), + ) self._option_payloads: dict[str, int] = { option[SelectSchema.CONF_OPTION]: option[CONF_PAYLOAD] for option in config[SelectSchema.CONF_OPTIONS] diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 5a09a921901..ab363e2a35f 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -116,17 +116,17 @@ async def async_setup_entry( ) -> None: """Set up sensor(s) for KNX platform.""" knx_module: KNXModule = hass.data[DOMAIN] - - async_add_entities( + entities: list[SensorEntity] = [] + entities.extend( KNXSystemSensor(knx_module, description) for description in SYSTEM_ENTITY_DESCRIPTIONS ) - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG].get(Platform.SENSOR) if config: - async_add_entities( - KNXSensor(knx_module.xknx, entity_config) for entity_config in config + entities.extend( + KNXSensor(knx_module, entity_config) for entity_config in config ) + async_add_entities(entities) def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor: @@ -146,9 +146,12 @@ class KNXSensor(KnxEntity, SensorEntity): _device: XknxSensor - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of a KNX sensor.""" - super().__init__(_create_sensor(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_sensor(knx_module.xknx, config), + ) if device_class := config.get(CONF_DEVICE_CLASS): self._attr_device_class = device_class else: diff --git a/homeassistant/components/knx/storage/config_store.py b/homeassistant/components/knx/storage/config_store.py index 7ea61e1dd3e..876fe19a4b9 100644 --- a/homeassistant/components/knx/storage/config_store.py +++ b/homeassistant/components/knx/storage/config_store.py @@ -2,21 +2,20 @@ from collections.abc import Callable import logging -from typing import TYPE_CHECKING, Any, Final, TypedDict +from typing import Any, Final, TypedDict from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PLATFORM, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store from homeassistant.util.ulid import ulid_now from ..const import DOMAIN +from ..knx_entity import SIGNAL_ENTITY_REMOVE from .const import CONF_DATA -if TYPE_CHECKING: - from ..knx_entity import KnxEntity - _LOGGER = logging.getLogger(__name__) STORAGE_VERSION: Final = 1 @@ -40,15 +39,16 @@ class KNXConfigStore: def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, + config_entry: ConfigEntry, ) -> None: """Initialize config store.""" self.hass = hass + self.config_entry = config_entry self._store = Store[KNXConfigStoreModel](hass, STORAGE_VERSION, STORAGE_KEY) self.data = KNXConfigStoreModel(entities={}) - # entities and async_add_entity are filled by platform setups - self.entities: dict[str, KnxEntity] = {} # unique_id as key + # entities and async_add_entity are filled by platform / entity setups + self.entities: set[str] = set() # unique_id as values self.async_add_entity: dict[ Platform, Callable[[str, dict[str, Any]], None] ] = {} @@ -108,7 +108,7 @@ class KNXConfigStore: raise ConfigStoreException( f"Entity not found in storage: {entity_id} - {unique_id}" ) - await self.entities.pop(unique_id).async_remove() + async_dispatcher_send(self.hass, SIGNAL_ENTITY_REMOVE.format(unique_id)) self.async_add_entity[platform](unique_id, data) # store data after entity is added to make sure config doesn't raise exceptions self.data["entities"][platform][unique_id] = data @@ -126,7 +126,7 @@ class KNXConfigStore: f"Entity not found in {entry.domain}: {entry.unique_id}" ) from err try: - del self.entities[entry.unique_id] + self.entities.remove(entry.unique_id) except KeyError: _LOGGER.warning("Entity not initialized when deleted: %s", entity_id) entity_registry.async_remove(entity_id) @@ -134,10 +134,14 @@ class KNXConfigStore: def get_entity_entries(self) -> list[er.RegistryEntry]: """Get entity_ids of all configured entities by platform.""" + entity_registry = er.async_get(self.hass) + return [ - entity.registry_entry - for entity in self.entities.values() - if entity.registry_entry is not None + registry_entry + for registry_entry in er.async_entries_for_config_entry( + entity_registry, self.config_entry.entry_id + ) + if registry_entry.unique_id in self.entities ] diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 0a8a1dff964..a5f430e6157 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any -from xknx import XKNX from xknx.devices import Switch as XknxSwitch from homeassistant import config_entries @@ -33,7 +32,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxEntity, KnxUIEntity from .schema import SwitchSchema from .storage.const import ( CONF_DEVICE_INFO, @@ -54,10 +53,10 @@ async def async_setup_entry( knx_module: KNXModule = hass.data[DOMAIN] entities: list[KnxEntity] = [] - if yaml_config := hass.data[DATA_KNX_CONFIG].get(Platform.SWITCH): + if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.SWITCH): entities.extend( - KnxYamlSwitch(knx_module.xknx, entity_config) - for entity_config in yaml_config + KnxYamlSwitch(knx_module, entity_config) + for entity_config in yaml_platform_config ) if ui_config := knx_module.config_store.data["entities"].get(Platform.SWITCH): entities.extend( @@ -75,7 +74,7 @@ async def async_setup_entry( knx_module.config_store.async_add_entity[Platform.SWITCH] = add_new_ui_switch -class _KnxSwitch(KnxEntity, SwitchEntity, RestoreEntity): +class _KnxSwitch(SwitchEntity, RestoreEntity): """Base class for a KNX switch.""" _device: XknxSwitch @@ -103,36 +102,41 @@ class _KnxSwitch(KnxEntity, SwitchEntity, RestoreEntity): await self._device.set_off() -class KnxYamlSwitch(_KnxSwitch): +class KnxYamlSwitch(_KnxSwitch, KnxEntity): """Representation of a KNX switch configured from YAML.""" - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + _device: XknxSwitch + + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX switch.""" super().__init__( + knx_module=knx_module, device=XknxSwitch( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address=config[KNX_ADDRESS], group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), respond_to_read=config[CONF_RESPOND_TO_READ], invert=config[SwitchSchema.CONF_INVERT], - ) + ), ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_unique_id = str(self._device.switch.group_address) -class KnxUiSwitch(_KnxSwitch): +class KnxUiSwitch(_KnxSwitch, KnxUIEntity): """Representation of a KNX switch configured from UI.""" _attr_has_entity_name = True + _device: XknxSwitch def __init__( self, knx_module: KNXModule, unique_id: str, config: dict[str, Any] ) -> None: """Initialize of KNX switch.""" super().__init__( + knx_module=knx_module, device=XknxSwitch( knx_module.xknx, name=config[CONF_ENTITY][CONF_NAME], @@ -144,11 +148,9 @@ class KnxUiSwitch(_KnxSwitch): respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ], sync_state=config[DOMAIN][CONF_SYNC_STATE], invert=config[DOMAIN][CONF_INVERT], - ) + ), ) self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY] self._attr_unique_id = unique_id if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO): self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)}) - - knx_module.config_store.entities[unique_id] = self diff --git a/homeassistant/components/knx/text.py b/homeassistant/components/knx/text.py index 22d008cd5ce..9bca37434ac 100644 --- a/homeassistant/components/knx/text.py +++ b/homeassistant/components/knx/text.py @@ -22,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -38,10 +39,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensor(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.TEXT] - async_add_entities(KNXText(xknx, entity_config) for entity_config in config) + async_add_entities(KNXText(knx_module, entity_config) for entity_config in config) def _create_notification(xknx: XKNX, config: ConfigType) -> XknxNotification: @@ -62,9 +63,12 @@ class KNXText(KnxEntity, TextEntity, RestoreEntity): _device: XknxNotification _attr_native_max = 14 - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX text.""" - super().__init__(_create_notification(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_notification(knx_module.xknx, config), + ) self._attr_mode = config[CONF_MODE] self._attr_pattern = ( r"[\u0000-\u00ff]*" # Latin-1 diff --git a/homeassistant/components/knx/time.py b/homeassistant/components/knx/time.py index 28e1419233c..5d9225a1e41 100644 --- a/homeassistant/components/knx/time.py +++ b/homeassistant/components/knx/time.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import time as dt_time -from typing import Final from xknx import XKNX from xknx.devices import TimeDevice as XknxTimeDevice @@ -23,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -33,8 +33,6 @@ from .const import ( ) from .knx_entity import KnxEntity -_TIME_TRANSLATION_FORMAT: Final = "%H:%M:%S" - async def async_setup_entry( hass: HomeAssistant, @@ -42,10 +40,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.TIME] - async_add_entities(KNXTimeEntity(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXTimeEntity(knx_module, entity_config) for entity_config in config + ) def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxTimeDevice: @@ -66,9 +66,12 @@ class KNXTimeEntity(KnxEntity, TimeEntity, RestoreEntity): _device: XknxTimeDevice - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX time.""" - super().__init__(_create_xknx_device(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_xknx_device(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 584c9fd3323..11dae452e2f 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -19,6 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import WeatherSchema @@ -30,10 +31,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up switch(es) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.WEATHER] - async_add_entities(KNXWeather(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXWeather(knx_module, entity_config) for entity_config in config + ) def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather: @@ -80,9 +83,12 @@ class KNXWeather(KnxEntity, WeatherEntity): _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of a KNX sensor.""" - super().__init__(_create_weather(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_weather(knx_module.xknx, config), + ) self._attr_unique_id = str(self._device._temperature.group_address_state) # noqa: SLF001 self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) From b610b29d28250777a2a58d33bf37dd4b87fe6818 Mon Sep 17 00:00:00 2001 From: Philip Vanloo <26272906+dukeofphilberg@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:23:45 +0200 Subject: [PATCH 1877/2411] LinkPlay: Bump python-linkplay to 0.0.6 (#123062) Bump python-linkplay to 0.0.6 --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index 0345d4ad727..9ac2a9e66e6 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/linkplay", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["python-linkplay==0.0.5"], + "requirements": ["python-linkplay==0.0.6"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 602c03c879d..c27e5831cf5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2301,7 +2301,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.1 # homeassistant.components.linkplay -python-linkplay==0.0.5 +python-linkplay==0.0.6 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 623f0953721..bce4d3a8487 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1819,7 +1819,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.1 # homeassistant.components.linkplay -python-linkplay==0.0.5 +python-linkplay==0.0.6 # homeassistant.components.matter python-matter-server==6.3.0 From 3de88283584b352f5d984bc660e624454fce155b Mon Sep 17 00:00:00 2001 From: Philip Vanloo <26272906+dukeofphilberg@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:38:05 +0200 Subject: [PATCH 1878/2411] Add additional items to REPEAT_MAP in LinkPlay (#123063) * Upgrade python-linkplay, add items to REPEAT_MAP * Undo dependency bump --- homeassistant/components/linkplay/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py index 103b09f46da..398add235bd 100644 --- a/homeassistant/components/linkplay/media_player.py +++ b/homeassistant/components/linkplay/media_player.py @@ -58,6 +58,8 @@ REPEAT_MAP: dict[LoopMode, RepeatMode] = { LoopMode.CONTINUOUS_PLAYBACK: RepeatMode.ALL, LoopMode.RANDOM_PLAYBACK: RepeatMode.ALL, LoopMode.LIST_CYCLE: RepeatMode.ALL, + LoopMode.SHUFF_DISABLED_REPEAT_DISABLED: RepeatMode.OFF, + LoopMode.SHUFF_ENABLED_REPEAT_ENABLED_LOOP_ONCE: RepeatMode.ALL, } REPEAT_MAP_INV: dict[RepeatMode, LoopMode] = {v: k for k, v in REPEAT_MAP.items()} From d2dd5ba0e6206c9d0bc9ad293235f44d95ab5071 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 2 Aug 2024 13:38:56 +0200 Subject: [PATCH 1879/2411] Do not raise repair issue about missing integration in safe mode (#123066) --- homeassistant/setup.py | 27 ++++++++++++++------------- tests/test_setup.py | 11 ++++++++++- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 12dd17b289c..102c48e1d07 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -281,19 +281,20 @@ async def _async_setup_component( integration = await loader.async_get_integration(hass, domain) except loader.IntegrationNotFound: _log_error_setup_error(hass, domain, None, "Integration not found.") - ir.async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"integration_not_found.{domain}", - is_fixable=True, - issue_domain=HOMEASSISTANT_DOMAIN, - severity=IssueSeverity.ERROR, - translation_key="integration_not_found", - translation_placeholders={ - "domain": domain, - }, - data={"domain": domain}, - ) + if not hass.config.safe_mode: + ir.async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"integration_not_found.{domain}", + is_fixable=True, + issue_domain=HOMEASSISTANT_DOMAIN, + severity=IssueSeverity.ERROR, + translation_key="integration_not_found", + translation_placeholders={ + "domain": domain, + }, + data={"domain": domain}, + ) return False log_error = partial(_log_error_setup_error, hass, domain, integration) diff --git a/tests/test_setup.py b/tests/test_setup.py index fae2b46f18d..4b7df9563ba 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -245,7 +245,7 @@ async def test_validate_platform_config_4(hass: HomeAssistant) -> None: async def test_component_not_found( hass: HomeAssistant, issue_registry: IssueRegistry ) -> None: - """setup_component should not crash if component doesn't exist.""" + """setup_component should raise a repair issue if component doesn't exist.""" assert await setup.async_setup_component(hass, "non_existing", {}) is False assert len(issue_registry.issues) == 1 issue = issue_registry.async_get_issue( @@ -255,6 +255,15 @@ async def test_component_not_found( assert issue.translation_key == "integration_not_found" +async def test_component_missing_not_raising_in_safe_mode( + hass: HomeAssistant, issue_registry: IssueRegistry +) -> None: + """setup_component should not raise an issue if component doesn't exist in safe.""" + hass.config.safe_mode = True + assert await setup.async_setup_component(hass, "non_existing", {}) is False + assert len(issue_registry.issues) == 0 + + async def test_component_not_double_initialized(hass: HomeAssistant) -> None: """Test we do not set up a component twice.""" mock_setup = Mock(return_value=True) From a40dce449f85f11aafd8ae104b0d726ca12af7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=2E=20=C3=81rkosi=20R=C3=B3bert?= Date: Fri, 2 Aug 2024 14:25:43 +0200 Subject: [PATCH 1880/2411] Add LinkPlay models (#123056) * Add some LinkPlay models * Update utils.py * Update utils.py * Update utils.py * Update homeassistant/components/linkplay/utils.py * Update homeassistant/components/linkplay/utils.py * Update utils.py --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/linkplay/utils.py | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/homeassistant/components/linkplay/utils.py b/homeassistant/components/linkplay/utils.py index 9ca76b3933d..7532c9b354a 100644 --- a/homeassistant/components/linkplay/utils.py +++ b/homeassistant/components/linkplay/utils.py @@ -3,9 +3,19 @@ from typing import Final MANUFACTURER_ARTSOUND: Final[str] = "ArtSound" +MANUFACTURER_ARYLIC: Final[str] = "Arylic" +MANUFACTURER_IEAST: Final[str] = "iEAST" MANUFACTURER_GENERIC: Final[str] = "Generic" MODELS_ARTSOUND_SMART_ZONE4: Final[str] = "Smart Zone 4 AMP" MODELS_ARTSOUND_SMART_HYDE: Final[str] = "Smart Hyde" +MODELS_ARYLIC_S50: Final[str] = "S50+" +MODELS_ARYLIC_S50_PRO: Final[str] = "S50 Pro" +MODELS_ARYLIC_A30: Final[str] = "A30" +MODELS_ARYLIC_A50S: Final[str] = "A50+" +MODELS_ARYLIC_UP2STREAM_AMP_V3: Final[str] = "Up2Stream Amp v3" +MODELS_ARYLIC_UP2STREAM_AMP_V4: Final[str] = "Up2Stream Amp v4" +MODELS_ARYLIC_UP2STREAM_PRO_V3: Final[str] = "Up2Stream Pro v3" +MODELS_IEAST_AUDIOCAST_M5: Final[str] = "AudioCast M5" MODELS_GENERIC: Final[str] = "Generic" @@ -16,5 +26,21 @@ def get_info_from_project(project: str) -> tuple[str, str]: return MANUFACTURER_ARTSOUND, MODELS_ARTSOUND_SMART_ZONE4 case "SMART_HYDE": return MANUFACTURER_ARTSOUND, MODELS_ARTSOUND_SMART_HYDE + case "ARYLIC_S50": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_S50 + case "RP0016_S50PRO_S": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_S50_PRO + case "RP0011_WB60_S": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_A30 + case "ARYLIC_A50S": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_A50S + case "UP2STREAM_AMP_V3": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_UP2STREAM_AMP_V3 + case "UP2STREAM_AMP_V4": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_UP2STREAM_AMP_V4 + case "UP2STREAM_PRO_V3": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_UP2STREAM_PRO_V3 + case "iEAST-02": + return MANUFACTURER_IEAST, MODELS_IEAST_AUDIOCAST_M5 case _: return MANUFACTURER_GENERIC, MODELS_GENERIC From fb76e70c3fdf061cff0839e3231c4d986b0dba59 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 2 Aug 2024 06:10:04 -0700 Subject: [PATCH 1881/2411] Use text/multiple selector for input_select.set_options (#122539) --- homeassistant/components/input_select/services.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml index 92279e58a54..04a09e5366a 100644 --- a/homeassistant/components/input_select/services.yaml +++ b/homeassistant/components/input_select/services.yaml @@ -48,6 +48,7 @@ set_options: required: true example: '["Item A", "Item B", "Item C"]' selector: - object: + text: + multiple: true reload: From db238a75e3079f23a35d7c0a104f8cf93327ac3c Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Fri, 2 Aug 2024 08:13:56 -0500 Subject: [PATCH 1882/2411] Lyric: Properly tie room accessories to the data coordinator (#115902) * properly tie lyric accessories to the data coordinator so sensors recieve updates * only check for accessories for LCC devices * revert: meant to give it its own branch and PR --- homeassistant/components/lyric/__init__.py | 22 ++++++++++++++++++---- homeassistant/components/lyric/sensor.py | 3 +-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 7c002229741..e1eaed6602c 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -192,8 +192,8 @@ class LyricAccessoryEntity(LyricDeviceEntity): ) -> None: """Initialize the Honeywell Lyric accessory entity.""" super().__init__(coordinator, location, device, key) - self._room = room - self._accessory = accessory + self._room_id = room.id + self._accessory_id = accessory.id @property def device_info(self) -> DeviceInfo: @@ -202,11 +202,25 @@ class LyricAccessoryEntity(LyricDeviceEntity): identifiers={ ( f"{dr.CONNECTION_NETWORK_MAC}_room_accessory", - f"{self._mac_id}_room{self._room.id}_accessory{self._accessory.id}", + f"{self._mac_id}_room{self._room_id}_accessory{self._accessory_id}", ) }, manufacturer="Honeywell", model="RCHTSENSOR", - name=f"{self._room.roomName} Sensor", + name=f"{self.room.roomName} Sensor", via_device=(dr.CONNECTION_NETWORK_MAC, self._mac_id), ) + + @property + def room(self) -> LyricRoom: + """Get the Lyric Device.""" + return self.coordinator.data.rooms_dict[self._mac_id][self._room_id] + + @property + def accessory(self) -> LyricAccessories: + """Get the Lyric Device.""" + return next( + accessory + for accessory in self.room.accessories + if accessory.id == self._accessory_id + ) diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 64f60fa6611..9f05354c399 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -244,7 +244,6 @@ class LyricAccessorySensor(LyricAccessoryEntity, SensorEntity): accessory, f"{parentDevice.macID}_room{room.id}_acc{accessory.id}_{description.key}", ) - self.room = room self.entity_description = description if description.device_class == SensorDeviceClass.TEMPERATURE: if parentDevice.units == "Fahrenheit": @@ -255,4 +254,4 @@ class LyricAccessorySensor(LyricAccessoryEntity, SensorEntity): @property def native_value(self) -> StateType | datetime: """Return the state.""" - return self.entity_description.value_fn(self._room, self._accessory) + return self.entity_description.value_fn(self.room, self.accessory) From b6c9fe86e1817f1bff307614b599a1d85f074c9f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Aug 2024 15:27:11 +0200 Subject: [PATCH 1883/2411] Ensure claude supported feature reflect latest config entry options (#123050) --- .../components/anthropic/conversation.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/homeassistant/components/anthropic/conversation.py b/homeassistant/components/anthropic/conversation.py index 92a09ad8a10..3d876bf3325 100644 --- a/homeassistant/components/anthropic/conversation.py +++ b/homeassistant/components/anthropic/conversation.py @@ -21,6 +21,7 @@ from voluptuous_openapi import convert from homeassistant.components import conversation from homeassistant.components.conversation import trace +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, TemplateError @@ -117,6 +118,13 @@ class AnthropicConversationEntity( """Return a list of supported languages.""" return MATCH_ALL + async def async_added_to_hass(self) -> None: + """When entity is added to Home Assistant.""" + await super().async_added_to_hass() + self.entry.async_on_unload( + self.entry.add_update_listener(self._async_entry_update_listener) + ) + async def async_process( self, user_input: conversation.ConversationInput ) -> conversation.ConversationResult: @@ -299,3 +307,14 @@ class AnthropicConversationEntity( return conversation.ConversationResult( response=intent_response, conversation_id=conversation_id ) + + async def _async_entry_update_listener( + self, hass: HomeAssistant, entry: ConfigEntry + ) -> None: + """Handle options update.""" + if entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) + else: + self._attr_supported_features = conversation.ConversationEntityFeature(0) From e734971d3382223bb23ac73833d900114e3f5dac Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:28:32 +0200 Subject: [PATCH 1884/2411] Enable collections-named-tuple (PYI024) rule in ruff (#123019) --- homeassistant/components/ads/__init__.py | 2 +- homeassistant/components/asuswrt/bridge.py | 2 +- homeassistant/components/bbox/device_tracker.py | 2 +- homeassistant/components/bt_smarthub/device_tracker.py | 2 +- homeassistant/components/epic_games_store/calendar.py | 2 +- homeassistant/components/fints/sensor.py | 2 +- homeassistant/components/hitron_coda/device_tracker.py | 2 +- homeassistant/components/modbus/modbus.py | 4 ++-- homeassistant/components/modbus/validators.py | 4 ++-- homeassistant/components/ness_alarm/__init__.py | 2 +- homeassistant/components/netio/switch.py | 2 +- homeassistant/components/tellstick/sensor.py | 2 +- homeassistant/scripts/benchmark/__init__.py | 2 +- pyproject.toml | 1 - script/lint_and_test.py | 2 +- tests/components/hardware/test_websocket_api.py | 2 +- tests/components/jewish_calendar/__init__.py | 2 +- tests/components/logbook/test_init.py | 2 +- tests/components/russound_rio/const.py | 2 +- tests/components/tplink/__init__.py | 2 +- 20 files changed, 21 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 7041a757a42..f5742718b12 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -136,7 +136,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: # Tuple to hold data needed for notification -NotificationItem = namedtuple( +NotificationItem = namedtuple( # noqa: PYI024 "NotificationItem", "hnotify huser name plc_datatype callback" ) diff --git a/homeassistant/components/asuswrt/bridge.py b/homeassistant/components/asuswrt/bridge.py index b193787f500..4e928d63666 100644 --- a/homeassistant/components/asuswrt/bridge.py +++ b/homeassistant/components/asuswrt/bridge.py @@ -52,7 +52,7 @@ SENSORS_TYPE_LOAD_AVG = "sensors_load_avg" SENSORS_TYPE_RATES = "sensors_rates" SENSORS_TYPE_TEMPERATURES = "sensors_temperatures" -WrtDevice = namedtuple("WrtDevice", ["ip", "name", "connected_to"]) +WrtDevice = namedtuple("WrtDevice", ["ip", "name", "connected_to"]) # noqa: PYI024 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index 6ced2c73c9a..7157c47830c 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -39,7 +39,7 @@ def get_scanner(hass: HomeAssistant, config: ConfigType) -> BboxDeviceScanner | return scanner if scanner.success_init else None -Device = namedtuple("Device", ["mac", "name", "ip", "last_update"]) +Device = namedtuple("Device", ["mac", "name", "ip", "last_update"]) # noqa: PYI024 class BboxDeviceScanner(DeviceScanner): diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index 10c8000fb93..4b52f38ff31 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -51,7 +51,7 @@ def _create_device(data): return _Device(ip_address, mac, host, status, name) -_Device = namedtuple("_Device", ["ip_address", "mac", "host", "status", "name"]) +_Device = namedtuple("_Device", ["ip_address", "mac", "host", "status", "name"]) # noqa: PYI024 class BTSmartHubScanner(DeviceScanner): diff --git a/homeassistant/components/epic_games_store/calendar.py b/homeassistant/components/epic_games_store/calendar.py index 75c448e8467..2ebb381341e 100644 --- a/homeassistant/components/epic_games_store/calendar.py +++ b/homeassistant/components/epic_games_store/calendar.py @@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, CalendarType from .coordinator import EGSCalendarUpdateCoordinator -DateRange = namedtuple("DateRange", ["start", "end"]) +DateRange = namedtuple("DateRange", ["start", "end"]) # noqa: PYI024 async def async_setup_entry( diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 2f47fdc09eb..8a92850ad47 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -28,7 +28,7 @@ SCAN_INTERVAL = timedelta(hours=4) ICON = "mdi:currency-eur" -BankCredentials = namedtuple("BankCredentials", "blz login pin url") +BankCredentials = namedtuple("BankCredentials", "blz login pin url") # noqa: PYI024 CONF_BIN = "bank_identification_number" CONF_ACCOUNTS = "accounts" diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index 68d93e9719d..61199e4b2f7 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -42,7 +42,7 @@ def get_scanner( return scanner if scanner.success_init else None -Device = namedtuple("Device", ["mac", "name"]) +Device = namedtuple("Device", ["mac", "name"]) # noqa: PYI024 class HitronCODADeviceScanner(DeviceScanner): diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 82caa772ac4..e70b9de50f0 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -76,8 +76,8 @@ from .validators import check_config _LOGGER = logging.getLogger(__name__) -ConfEntry = namedtuple("ConfEntry", "call_type attr func_name") -RunEntry = namedtuple("RunEntry", "attr func") +ConfEntry = namedtuple("ConfEntry", "call_type attr func_name") # noqa: PYI024 +RunEntry = namedtuple("RunEntry", "attr func") # noqa: PYI024 PB_CALL = [ ConfEntry( CALL_TYPE_COIL, diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 90ef0b5f083..e1120094d01 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -46,7 +46,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -ENTRY = namedtuple( +ENTRY = namedtuple( # noqa: PYI024 "ENTRY", [ "struct_id", @@ -60,7 +60,7 @@ ILLEGAL = "I" OPTIONAL = "O" DEMANDED = "D" -PARM_IS_LEGAL = namedtuple( +PARM_IS_LEGAL = namedtuple( # noqa: PYI024 "PARM_IS_LEGAL", [ "count", diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index a8202434ce5..730a9aff765 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -44,7 +44,7 @@ DEFAULT_INFER_ARMING_STATE = False SIGNAL_ZONE_CHANGED = "ness_alarm.zone_changed" SIGNAL_ARMING_STATE_CHANGED = "ness_alarm.arming_state_changed" -ZoneChangedData = namedtuple("ZoneChangedData", ["zone_id", "state"]) +ZoneChangedData = namedtuple("ZoneChangedData", ["zone_id", "state"]) # noqa: PYI024 DEFAULT_ZONE_TYPE = BinarySensorDeviceClass.MOTION ZONE_SCHEMA = vol.Schema( diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index f5627f5e56b..54bfef5e1da 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -38,7 +38,7 @@ CONF_OUTLETS = "outlets" DEFAULT_PORT = 1234 DEFAULT_USERNAME = "admin" -Device = namedtuple("Device", ["netio", "entities"]) +Device = namedtuple("Device", ["netio", "entities"]) # noqa: PYI024 DEVICES: dict[str, Device] = {} MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index 2c304f259da..1e27511bd84 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -29,7 +29,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -DatatypeDescription = namedtuple( +DatatypeDescription = namedtuple( # noqa: PYI024 "DatatypeDescription", ["name", "unit", "device_class"] ) diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 34bc536502f..d39b1b64861 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -322,7 +322,7 @@ def _create_state_changed_event_from_old_new( attributes_json = json.dumps(attributes, cls=JSONEncoder) if attributes_json == "null": attributes_json = "{}" - row = collections.namedtuple( + row = collections.namedtuple( # noqa: PYI024 "Row", [ "event_type" diff --git a/pyproject.toml b/pyproject.toml index eb4bfc4970d..93f8427ef7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -821,7 +821,6 @@ ignore = [ "PLE0605", # temporarily disabled - "PYI024", # Use typing.NamedTuple instead of collections.namedtuple "RET503", "RET501", "TRY301" diff --git a/script/lint_and_test.py b/script/lint_and_test.py index e23870364b6..ff3db8aa1ed 100755 --- a/script/lint_and_test.py +++ b/script/lint_and_test.py @@ -20,7 +20,7 @@ except ImportError: RE_ASCII = re.compile(r"\033\[[^m]*m") -Error = namedtuple("Error", ["file", "line", "col", "msg", "skip"]) +Error = namedtuple("Error", ["file", "line", "col", "msg", "skip"]) # noqa: PYI024 PASS = "green" FAIL = "bold_red" diff --git a/tests/components/hardware/test_websocket_api.py b/tests/components/hardware/test_websocket_api.py index e8099069a9c..1379bdba120 100644 --- a/tests/components/hardware/test_websocket_api.py +++ b/tests/components/hardware/test_websocket_api.py @@ -61,7 +61,7 @@ async def test_system_status_subscription( response = await client.receive_json() assert response["success"] - VirtualMem = namedtuple("VirtualMemory", ["available", "percent", "total"]) + VirtualMem = namedtuple("VirtualMemory", ["available", "percent", "total"]) # noqa: PYI024 vmem = VirtualMem(10 * 1024**2, 50, 30 * 1024**2) with ( diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index 60726fc3a3e..440bffc2256 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -8,7 +8,7 @@ from freezegun import freeze_time as alter_time # noqa: F401 from homeassistant.components import jewish_calendar import homeassistant.util.dt as dt_util -_LatLng = namedtuple("_LatLng", ["lat", "lng"]) +_LatLng = namedtuple("_LatLng", ["lat", "lng"]) # noqa: PYI024 HDATE_DEFAULT_ALTITUDE = 754 NYC_LATLNG = _LatLng(40.7128, -74.0060) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 3534192a43e..34052cd8024 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -328,7 +328,7 @@ def create_state_changed_event_from_old_new( if new_state is not None: attributes = new_state.get("attributes") attributes_json = json.dumps(attributes, cls=JSONEncoder) - row = collections.namedtuple( + row = collections.namedtuple( # noqa: PYI024 "Row", [ "event_type", diff --git a/tests/components/russound_rio/const.py b/tests/components/russound_rio/const.py index d1f6aa7eead..527f4fe3377 100644 --- a/tests/components/russound_rio/const.py +++ b/tests/components/russound_rio/const.py @@ -12,5 +12,5 @@ MOCK_CONFIG = { "port": PORT, } -_CONTROLLER = namedtuple("Controller", ["mac_address", "controller_type"]) +_CONTROLLER = namedtuple("Controller", ["mac_address", "controller_type"]) # noqa: PYI024 MOCK_CONTROLLERS = {1: _CONTROLLER(mac_address=HARDWARE_MAC, controller_type=MODEL)} diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index c51a451c847..c63ca9139f1 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -39,7 +39,7 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_json_value_fixture -ColorTempRange = namedtuple("ColorTempRange", ["min", "max"]) +ColorTempRange = namedtuple("ColorTempRange", ["min", "max"]) # noqa: PYI024 MODULE = "homeassistant.components.tplink" MODULE_CONFIG_FLOW = "homeassistant.components.tplink.config_flow" From b609f8e96240c607d037ddc4c62562daf322f6ca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:30:29 +0200 Subject: [PATCH 1885/2411] Fix implicit-return in macos script (#122945) --- homeassistant/scripts/macos/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/scripts/macos/__init__.py b/homeassistant/scripts/macos/__init__.py index f629492ec39..0bf88da81dc 100644 --- a/homeassistant/scripts/macos/__init__.py +++ b/homeassistant/scripts/macos/__init__.py @@ -44,7 +44,7 @@ def uninstall_osx(): print("Home Assistant has been uninstalled.") -def run(args): +def run(args: list[str]) -> int: """Handle OSX commandline script.""" commands = "install", "uninstall", "restart" if not args or args[0] not in commands: @@ -63,3 +63,5 @@ def run(args): time.sleep(0.5) install_osx() return 0 + + raise ValueError(f"Invalid command {args[0]}") From 1eadb00fce1bcf78a104ad58ad74e7261d680e3c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:31:09 +0200 Subject: [PATCH 1886/2411] Fix implicit-return in google_assistant (#123002) --- homeassistant/components/google_assistant/trait.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index e54684fbc64..2be20237a84 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -878,6 +878,8 @@ class StartStopTrait(_Trait): if domain in COVER_VALVE_DOMAINS: return {} + raise NotImplementedError(f"Unsupported domain {domain}") + def query_attributes(self): """Return StartStop query attributes.""" domain = self.state.domain @@ -898,13 +900,17 @@ class StartStopTrait(_Trait): ) } + raise NotImplementedError(f"Unsupported domain {domain}") + async def execute(self, command, data, params, challenge): """Execute a StartStop command.""" domain = self.state.domain if domain == vacuum.DOMAIN: - return await self._execute_vacuum(command, data, params, challenge) + await self._execute_vacuum(command, data, params, challenge) + return if domain in COVER_VALVE_DOMAINS: - return await self._execute_cover_or_valve(command, data, params, challenge) + await self._execute_cover_or_valve(command, data, params, challenge) + return async def _execute_vacuum(self, command, data, params, challenge): """Execute a StartStop command.""" From 115303faf591a61fd80023846da7a84fdc405d08 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:44:19 +0200 Subject: [PATCH 1887/2411] Fix translation key for power exchange sensor in ViCare (#122339) --- homeassistant/components/vicare/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index 7c0088d065f..0452a560cb8 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -319,8 +319,8 @@ "ess_discharge_total": { "name": "Battery discharge total" }, - "pcc_current_power_exchange": { - "name": "Grid power exchange" + "pcc_transfer_power_exchange": { + "name": "Power exchange with grid" }, "pcc_energy_consumption": { "name": "Energy import from grid" From a4aefe43dc77c4c5e4310d5f2de2a644aac4507e Mon Sep 17 00:00:00 2001 From: Matrix Date: Fri, 2 Aug 2024 21:57:15 +0800 Subject: [PATCH 1888/2411] Yolink device model adaptation (#122824) --- homeassistant/components/yolink/const.py | 4 ++++ homeassistant/components/yolink/sensor.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 894c85d3f1b..686160d9248 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -17,5 +17,9 @@ YOLINK_OFFLINE_TIME = 32400 DEV_MODEL_WATER_METER_YS5007 = "YS5007" DEV_MODEL_MULTI_OUTLET_YS6801 = "YS6801" +DEV_MODEL_TH_SENSOR_YS8004_UC = "YS8004-UC" +DEV_MODEL_TH_SENSOR_YS8004_EC = "YS8004-EC" +DEV_MODEL_TH_SENSOR_YS8014_UC = "YS8014-UC" +DEV_MODEL_TH_SENSOR_YS8014_EC = "YS8014-EC" DEV_MODEL_TH_SENSOR_YS8017_UC = "YS8017-UC" DEV_MODEL_TH_SENSOR_YS8017_EC = "YS8017-EC" diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 4426602f133..77bbccb2f6a 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -48,7 +48,15 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import percentage -from .const import DEV_MODEL_TH_SENSOR_YS8017_EC, DEV_MODEL_TH_SENSOR_YS8017_UC, DOMAIN +from .const import ( + DEV_MODEL_TH_SENSOR_YS8004_EC, + DEV_MODEL_TH_SENSOR_YS8004_UC, + DEV_MODEL_TH_SENSOR_YS8014_EC, + DEV_MODEL_TH_SENSOR_YS8014_UC, + DEV_MODEL_TH_SENSOR_YS8017_EC, + DEV_MODEL_TH_SENSOR_YS8017_UC, + DOMAIN, +) from .coordinator import YoLinkCoordinator from .entity import YoLinkEntity @@ -109,6 +117,10 @@ MCU_DEV_TEMPERATURE_SENSOR = [ ] NONE_HUMIDITY_SENSOR_MODELS = [ + DEV_MODEL_TH_SENSOR_YS8004_EC, + DEV_MODEL_TH_SENSOR_YS8004_UC, + DEV_MODEL_TH_SENSOR_YS8014_EC, + DEV_MODEL_TH_SENSOR_YS8014_UC, DEV_MODEL_TH_SENSOR_YS8017_UC, DEV_MODEL_TH_SENSOR_YS8017_EC, ] From b89a859f1484f15dbd53fec738ce1ba8e9fe62a2 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 2 Aug 2024 15:58:41 +0200 Subject: [PATCH 1889/2411] Fix and improve tedee lock states (#123022) Improve tedee lock states --- homeassistant/components/tedee/lock.py | 13 +++++++++++-- tests/components/tedee/test_lock.py | 22 ++++++++++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tedee/lock.py b/homeassistant/components/tedee/lock.py index d11c873a94a..8d5fa028e12 100644 --- a/homeassistant/components/tedee/lock.py +++ b/homeassistant/components/tedee/lock.py @@ -55,8 +55,13 @@ class TedeeLockEntity(TedeeEntity, LockEntity): super().__init__(lock, coordinator, "lock") @property - def is_locked(self) -> bool: + def is_locked(self) -> bool | None: """Return true if lock is locked.""" + if self._lock.state in ( + TedeeLockState.HALF_OPEN, + TedeeLockState.UNKNOWN, + ): + return None return self._lock.state == TedeeLockState.LOCKED @property @@ -87,7 +92,11 @@ class TedeeLockEntity(TedeeEntity, LockEntity): @property def available(self) -> bool: """Return True if entity is available.""" - return super().available and self._lock.is_connected + return ( + super().available + and self._lock.is_connected + and self._lock.state != TedeeLockState.UNCALIBRATED + ) async def async_unlock(self, **kwargs: Any) -> None: """Unlock the door.""" diff --git a/tests/components/tedee/test_lock.py b/tests/components/tedee/test_lock.py index ffc4a8c30d6..741bc3156cb 100644 --- a/tests/components/tedee/test_lock.py +++ b/tests/components/tedee/test_lock.py @@ -25,7 +25,7 @@ from homeassistant.components.lock import ( STATE_UNLOCKING, ) from homeassistant.components.webhook import async_generate_url -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -276,10 +276,21 @@ async def test_new_lock( assert state +@pytest.mark.parametrize( + ("lib_state", "expected_state"), + [ + (TedeeLockState.LOCKED, STATE_LOCKED), + (TedeeLockState.HALF_OPEN, STATE_UNKNOWN), + (TedeeLockState.UNKNOWN, STATE_UNKNOWN), + (TedeeLockState.UNCALIBRATED, STATE_UNAVAILABLE), + ], +) async def test_webhook_update( hass: HomeAssistant, mock_tedee: MagicMock, hass_client_no_auth: ClientSessionGenerator, + lib_state: TedeeLockState, + expected_state: str, ) -> None: """Test updated data set through webhook.""" @@ -287,10 +298,9 @@ async def test_webhook_update( assert state assert state.state == STATE_UNLOCKED - webhook_data = {"dummystate": 6} - mock_tedee.locks_dict[ - 12345 - ].state = TedeeLockState.LOCKED # is updated in the lib, so mock and assert in L296 + webhook_data = {"dummystate": lib_state.value} + # is updated in the lib, so mock and assert below + mock_tedee.locks_dict[12345].state = lib_state client = await hass_client_no_auth() webhook_url = async_generate_url(hass, WEBHOOK_ID) @@ -302,4 +312,4 @@ async def test_webhook_update( state = hass.states.get("lock.lock_1a2b") assert state - assert state.state == STATE_LOCKED + assert state.state == expected_state From a18166e3f879ff358b366424edc0a0573e0c0c24 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 Aug 2024 16:48:37 +0200 Subject: [PATCH 1890/2411] Update frontend to 20240802.0 (#123072) --- 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 60cfa0a26ff..95afe1221ec 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240731.0"] + "requirements": ["home-assistant-frontend==20240802.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 58f39907269..1cc6a0fa85d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240731.0 +home-assistant-frontend==20240802.0 home-assistant-intents==2024.7.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index c27e5831cf5..2805ad2c716 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1096,7 +1096,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240731.0 +home-assistant-frontend==20240802.0 # homeassistant.components.conversation home-assistant-intents==2024.7.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bce4d3a8487..c7934e7fc84 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -919,7 +919,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240731.0 +home-assistant-frontend==20240802.0 # homeassistant.components.conversation home-assistant-intents==2024.7.29 From b0ece4bbaa16efee2f75a3898f9be6bc0be7790a Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Fri, 2 Aug 2024 17:07:23 +0200 Subject: [PATCH 1891/2411] Improve Bang olufsen media_player dispatcher formatting (#123065) * Avoid repeating almost the same command 8 times * Remove debugging --- .../components/bang_olufsen/media_player.py | 75 +++++-------------- 1 file changed, 18 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 5f8b7638125..8bc97858d0d 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Callable import json import logging from typing import Any, cast @@ -137,65 +138,25 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): """Turn on the dispatchers.""" await self._initialize() - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{self._unique_id}_{CONNECTION_STATUS}", - self._async_update_connection_state, - ) - ) + signal_handlers: dict[str, Callable] = { + CONNECTION_STATUS: self._async_update_connection_state, + WebsocketNotification.PLAYBACK_ERROR: self._async_update_playback_error, + WebsocketNotification.PLAYBACK_METADATA: self._async_update_playback_metadata, + WebsocketNotification.PLAYBACK_PROGRESS: self._async_update_playback_progress, + WebsocketNotification.PLAYBACK_STATE: self._async_update_playback_state, + WebsocketNotification.REMOTE_MENU_CHANGED: self._async_update_sources, + WebsocketNotification.SOURCE_CHANGE: self._async_update_source_change, + WebsocketNotification.VOLUME: self._async_update_volume, + } - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{self._unique_id}_{WebsocketNotification.PLAYBACK_ERROR}", - self._async_update_playback_error, + for signal, signal_handler in signal_handlers.items(): + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{self._unique_id}_{signal}", + signal_handler, + ) ) - ) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{self._unique_id}_{WebsocketNotification.PLAYBACK_METADATA}", - self._async_update_playback_metadata, - ) - ) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{self._unique_id}_{WebsocketNotification.PLAYBACK_PROGRESS}", - self._async_update_playback_progress, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{self._unique_id}_{WebsocketNotification.PLAYBACK_STATE}", - self._async_update_playback_state, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{self._unique_id}_{WebsocketNotification.REMOTE_MENU_CHANGED}", - self._async_update_sources, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{self._unique_id}_{WebsocketNotification.SOURCE_CHANGE}", - self._async_update_source_change, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{self._unique_id}_{WebsocketNotification.VOLUME}", - self._async_update_volume, - ) - ) async def _initialize(self) -> None: """Initialize connection dependent variables.""" From 2520fcd284e94f0eebee1165ef393966d0e9ede1 Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Fri, 2 Aug 2024 08:13:56 -0500 Subject: [PATCH 1892/2411] Lyric: Properly tie room accessories to the data coordinator (#115902) * properly tie lyric accessories to the data coordinator so sensors recieve updates * only check for accessories for LCC devices * revert: meant to give it its own branch and PR --- homeassistant/components/lyric/__init__.py | 22 ++++++++++++++++++---- homeassistant/components/lyric/sensor.py | 3 +-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 7c002229741..e1eaed6602c 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -192,8 +192,8 @@ class LyricAccessoryEntity(LyricDeviceEntity): ) -> None: """Initialize the Honeywell Lyric accessory entity.""" super().__init__(coordinator, location, device, key) - self._room = room - self._accessory = accessory + self._room_id = room.id + self._accessory_id = accessory.id @property def device_info(self) -> DeviceInfo: @@ -202,11 +202,25 @@ class LyricAccessoryEntity(LyricDeviceEntity): identifiers={ ( f"{dr.CONNECTION_NETWORK_MAC}_room_accessory", - f"{self._mac_id}_room{self._room.id}_accessory{self._accessory.id}", + f"{self._mac_id}_room{self._room_id}_accessory{self._accessory_id}", ) }, manufacturer="Honeywell", model="RCHTSENSOR", - name=f"{self._room.roomName} Sensor", + name=f"{self.room.roomName} Sensor", via_device=(dr.CONNECTION_NETWORK_MAC, self._mac_id), ) + + @property + def room(self) -> LyricRoom: + """Get the Lyric Device.""" + return self.coordinator.data.rooms_dict[self._mac_id][self._room_id] + + @property + def accessory(self) -> LyricAccessories: + """Get the Lyric Device.""" + return next( + accessory + for accessory in self.room.accessories + if accessory.id == self._accessory_id + ) diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 64f60fa6611..9f05354c399 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -244,7 +244,6 @@ class LyricAccessorySensor(LyricAccessoryEntity, SensorEntity): accessory, f"{parentDevice.macID}_room{room.id}_acc{accessory.id}_{description.key}", ) - self.room = room self.entity_description = description if description.device_class == SensorDeviceClass.TEMPERATURE: if parentDevice.units == "Fahrenheit": @@ -255,4 +254,4 @@ class LyricAccessorySensor(LyricAccessoryEntity, SensorEntity): @property def native_value(self) -> StateType | datetime: """Return the state.""" - return self.entity_description.value_fn(self._room, self._accessory) + return self.entity_description.value_fn(self.room, self.accessory) From 1b1d86409cc4605485b129c930b07d6675a3e00c Mon Sep 17 00:00:00 2001 From: DeerMaximum <43999966+DeerMaximum@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:32:37 +0200 Subject: [PATCH 1893/2411] Velux use node id as fallback for unique id (#117508) Co-authored-by: Robert Resch --- homeassistant/components/velux/__init__.py | 8 ++++++-- homeassistant/components/velux/cover.py | 6 +++--- homeassistant/components/velux/light.py | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 4b89fc66a84..1b7cbd1ff93 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -108,10 +108,14 @@ class VeluxEntity(Entity): _attr_should_poll = False - def __init__(self, node: Node) -> None: + def __init__(self, node: Node, config_entry_id: str) -> None: """Initialize the Velux device.""" self.node = node - self._attr_unique_id = node.serial_number + self._attr_unique_id = ( + node.serial_number + if node.serial_number + else f"{config_entry_id}_{node.node_id}" + ) self._attr_name = node.name if node.name else f"#{node.node_id}" @callback diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index c8688e4d186..cd7564eee81 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -29,7 +29,7 @@ async def async_setup_entry( """Set up cover(s) for Velux platform.""" module = hass.data[DOMAIN][config.entry_id] async_add_entities( - VeluxCover(node) + VeluxCover(node, config.entry_id) for node in module.pyvlx.nodes if isinstance(node, OpeningDevice) ) @@ -41,9 +41,9 @@ class VeluxCover(VeluxEntity, CoverEntity): _is_blind = False node: OpeningDevice - def __init__(self, node: OpeningDevice) -> None: + def __init__(self, node: OpeningDevice, config_entry_id: str) -> None: """Initialize VeluxCover.""" - super().__init__(node) + super().__init__(node, config_entry_id) self._attr_device_class = CoverDeviceClass.WINDOW if isinstance(node, Awning): self._attr_device_class = CoverDeviceClass.AWNING diff --git a/homeassistant/components/velux/light.py b/homeassistant/components/velux/light.py index bbe9822648e..e98632701f3 100644 --- a/homeassistant/components/velux/light.py +++ b/homeassistant/components/velux/light.py @@ -23,7 +23,7 @@ async def async_setup_entry( module = hass.data[DOMAIN][config.entry_id] async_add_entities( - VeluxLight(node) + VeluxLight(node, config.entry_id) for node in module.pyvlx.nodes if isinstance(node, LighteningDevice) ) From 804d7aa4c040b3d4ca0f97811d380128a90daafa Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:44:19 +0200 Subject: [PATCH 1894/2411] Fix translation key for power exchange sensor in ViCare (#122339) --- homeassistant/components/vicare/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index 7c0088d065f..0452a560cb8 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -319,8 +319,8 @@ "ess_discharge_total": { "name": "Battery discharge total" }, - "pcc_current_power_exchange": { - "name": "Grid power exchange" + "pcc_transfer_power_exchange": { + "name": "Power exchange with grid" }, "pcc_energy_consumption": { "name": "Energy import from grid" From 1a7085b068a61f44802d129c2fc6573c0f2cacd6 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Fri, 2 Aug 2024 09:05:06 +0300 Subject: [PATCH 1895/2411] Add aliases to script llm tool description (#122380) * Add aliases to script llm tool description * Also add name --- homeassistant/helpers/llm.py | 13 +++++++++++++ tests/helpers/test_llm.py | 36 +++++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 4ddb00166b6..e37aa0c532d 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -677,6 +677,19 @@ class ScriptTool(Tool): self.parameters = vol.Schema(schema) + aliases: list[str] = [] + if entity_entry.name: + aliases.append(entity_entry.name) + if entity_entry.aliases: + aliases.extend(entity_entry.aliases) + if aliases: + if self.description: + self.description = ( + self.description + ". Aliases: " + str(list(aliases)) + ) + else: + self.description = "Aliases: " + str(list(aliases)) + parameters_cache[entity_entry.unique_id] = ( self.description, self.parameters, diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index ea6e628d1d4..4d14abb9819 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -411,7 +411,9 @@ async def test_assist_api_prompt( ) hass.states.async_set(entry2.entity_id, "on", {"friendly_name": "Living Room"}) - def create_entity(device: dr.DeviceEntry, write_state=True) -> None: + def create_entity( + device: dr.DeviceEntry, write_state=True, aliases: set[str] | None = None + ) -> None: """Create an entity for a device and track entity_id.""" entity = entity_registry.async_get_or_create( "light", @@ -421,6 +423,8 @@ async def test_assist_api_prompt( original_name=str(device.name or "Unnamed Device"), suggested_object_id=str(device.name or "unnamed_device"), ) + if aliases: + entity_registry.async_update_entity(entity.entity_id, aliases=aliases) if write_state: entity.write_unavailable_state(hass) @@ -432,7 +436,8 @@ async def test_assist_api_prompt( manufacturer="Test Manufacturer", model="Test Model", suggested_area="Test Area", - ) + ), + aliases={"my test light"}, ) for i in range(3): create_entity( @@ -516,7 +521,7 @@ async def test_assist_api_prompt( domain: light state: 'on' areas: Test Area, Alternative name -- names: Test Device +- names: Test Device, my test light domain: light state: unavailable areas: Test Area, Alternative name @@ -616,6 +621,7 @@ async def test_assist_api_prompt( async def test_script_tool( hass: HomeAssistant, + entity_registry: er.EntityRegistry, area_registry: ar.AreaRegistry, floor_registry: fr.FloorRegistry, ) -> None: @@ -659,6 +665,10 @@ async def test_script_tool( ) async_expose_entity(hass, "conversation", "script.test_script", True) + entity_registry.async_update_entity( + "script.test_script", name="script name", aliases={"script alias"} + ) + area = area_registry.async_create("Living room") floor = floor_registry.async_create("2") @@ -671,7 +681,10 @@ async def test_script_tool( tool = tools[0] assert tool.name == "test_script" - assert tool.description == "This is a test script" + assert ( + tool.description + == "This is a test script. Aliases: ['script name', 'script alias']" + ) schema = { vol.Required("beer", description="Number of beers"): cv.string, vol.Optional("wine"): selector.NumberSelector({"min": 0, "max": 3}), @@ -684,7 +697,10 @@ async def test_script_tool( assert tool.parameters.schema == schema assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == { - "test_script": ("This is a test script", vol.Schema(schema)) + "test_script": ( + "This is a test script. Aliases: ['script name', 'script alias']", + vol.Schema(schema), + ) } tool_input = llm.ToolInput( @@ -754,12 +770,18 @@ async def test_script_tool( tool = tools[0] assert tool.name == "test_script" - assert tool.description == "This is a new test script" + assert ( + tool.description + == "This is a new test script. Aliases: ['script name', 'script alias']" + ) schema = {vol.Required("beer", description="Number of beers"): cv.string} assert tool.parameters.schema == schema assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == { - "test_script": ("This is a new test script", vol.Schema(schema)) + "test_script": ( + "This is a new test script. Aliases: ['script name', 'script alias']", + vol.Schema(schema), + ) } From 6a6814af61216df481410f57ecf9d971037eccff Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 2 Aug 2024 06:10:04 -0700 Subject: [PATCH 1896/2411] Use text/multiple selector for input_select.set_options (#122539) --- homeassistant/components/input_select/services.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml index 92279e58a54..04a09e5366a 100644 --- a/homeassistant/components/input_select/services.yaml +++ b/homeassistant/components/input_select/services.yaml @@ -48,6 +48,7 @@ set_options: required: true example: '["Item A", "Item B", "Item C"]' selector: - object: + text: + multiple: true reload: From dfb4e9c159e57f9731d7fd3745676ece4a657de0 Mon Sep 17 00:00:00 2001 From: Matrix Date: Fri, 2 Aug 2024 21:57:15 +0800 Subject: [PATCH 1897/2411] Yolink device model adaptation (#122824) --- homeassistant/components/yolink/const.py | 4 ++++ homeassistant/components/yolink/sensor.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 894c85d3f1b..686160d9248 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -17,5 +17,9 @@ YOLINK_OFFLINE_TIME = 32400 DEV_MODEL_WATER_METER_YS5007 = "YS5007" DEV_MODEL_MULTI_OUTLET_YS6801 = "YS6801" +DEV_MODEL_TH_SENSOR_YS8004_UC = "YS8004-UC" +DEV_MODEL_TH_SENSOR_YS8004_EC = "YS8004-EC" +DEV_MODEL_TH_SENSOR_YS8014_UC = "YS8014-UC" +DEV_MODEL_TH_SENSOR_YS8014_EC = "YS8014-EC" DEV_MODEL_TH_SENSOR_YS8017_UC = "YS8017-UC" DEV_MODEL_TH_SENSOR_YS8017_EC = "YS8017-EC" diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 4426602f133..77bbccb2f6a 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -48,7 +48,15 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import percentage -from .const import DEV_MODEL_TH_SENSOR_YS8017_EC, DEV_MODEL_TH_SENSOR_YS8017_UC, DOMAIN +from .const import ( + DEV_MODEL_TH_SENSOR_YS8004_EC, + DEV_MODEL_TH_SENSOR_YS8004_UC, + DEV_MODEL_TH_SENSOR_YS8014_EC, + DEV_MODEL_TH_SENSOR_YS8014_UC, + DEV_MODEL_TH_SENSOR_YS8017_EC, + DEV_MODEL_TH_SENSOR_YS8017_UC, + DOMAIN, +) from .coordinator import YoLinkCoordinator from .entity import YoLinkEntity @@ -109,6 +117,10 @@ MCU_DEV_TEMPERATURE_SENSOR = [ ] NONE_HUMIDITY_SENSOR_MODELS = [ + DEV_MODEL_TH_SENSOR_YS8004_EC, + DEV_MODEL_TH_SENSOR_YS8004_UC, + DEV_MODEL_TH_SENSOR_YS8014_EC, + DEV_MODEL_TH_SENSOR_YS8014_UC, DEV_MODEL_TH_SENSOR_YS8017_UC, DEV_MODEL_TH_SENSOR_YS8017_EC, ] From 3b462906d9982999d971c99b7841b19ce88a2964 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 1 Aug 2024 03:59:19 -0700 Subject: [PATCH 1898/2411] Restrict nws.get_forecasts_extra selector to nws weather entities (#122986) --- homeassistant/components/nws/services.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nws/services.yaml b/homeassistant/components/nws/services.yaml index 0d439a9d278..a3d241c775d 100644 --- a/homeassistant/components/nws/services.yaml +++ b/homeassistant/components/nws/services.yaml @@ -2,6 +2,7 @@ get_forecasts_extra: target: entity: domain: weather + integration: nws fields: type: required: true From cb37ae660843142bc8b951c3b91ccd0c5739863f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Aug 2024 01:31:22 -0500 Subject: [PATCH 1899/2411] Update doorbird error notification to be a repair flow (#122987) --- homeassistant/components/doorbird/__init__.py | 37 ++++++----- .../components/doorbird/manifest.json | 2 +- homeassistant/components/doorbird/repairs.py | 55 +++++++++++++++++ .../components/doorbird/strings.json | 13 ++++ tests/components/doorbird/test_repairs.py | 61 +++++++++++++++++++ 5 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/doorbird/repairs.py create mode 100644 tests/components/doorbird/test_repairs.py diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 8989e0ec0be..113b8031d9b 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -3,11 +3,11 @@ from __future__ import annotations from http import HTTPStatus +import logging from aiohttp import ClientResponseError from doorbirdpy import DoorBird -from homeassistant.components import persistent_notification from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -17,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -30,6 +31,8 @@ CONF_CUSTOM_URL = "hass_url_override" CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the DoorBird component.""" @@ -68,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> door_bird_data = DoorBirdData(door_station, info, event_entity_ids) door_station.update_events(events) # Subscribe to doorbell or motion events - if not await _async_register_events(hass, door_station): + if not await _async_register_events(hass, door_station, entry): raise ConfigEntryNotReady entry.async_on_unload(entry.add_update_listener(_update_listener)) @@ -84,24 +87,30 @@ async def async_unload_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> async def _async_register_events( - hass: HomeAssistant, door_station: ConfiguredDoorBird + hass: HomeAssistant, door_station: ConfiguredDoorBird, entry: DoorBirdConfigEntry ) -> bool: """Register events on device.""" + issue_id = f"doorbird_schedule_error_{entry.entry_id}" try: await door_station.async_register_events() - except ClientResponseError: - persistent_notification.async_create( + except ClientResponseError as ex: + ir.async_create_issue( hass, - ( - "Doorbird configuration failed. Please verify that API " - "Operator permission is enabled for the Doorbird user. " - "A restart will be required once permissions have been " - "verified." - ), - title="Doorbird Configuration Failure", - notification_id="doorbird_schedule_error", + DOMAIN, + issue_id, + severity=ir.IssueSeverity.ERROR, + translation_key="error_registering_events", + data={"entry_id": entry.entry_id}, + is_fixable=True, + translation_placeholders={ + "error": str(ex), + "name": door_station.name or entry.data[CONF_NAME], + }, ) + _LOGGER.debug("Error registering DoorBird events", exc_info=True) return False + else: + ir.async_delete_issue(hass, DOMAIN, issue_id) return True @@ -111,4 +120,4 @@ async def _update_listener(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> N door_station = entry.runtime_data.door_station door_station.update_events(entry.options[CONF_EVENTS]) # Subscribe to doorbell or motion events - await _async_register_events(hass, door_station) + await _async_register_events(hass, door_station, entry) diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index e77f9aaf0a4..0e9f03c8ef8 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -3,7 +3,7 @@ "name": "DoorBird", "codeowners": ["@oblogic7", "@bdraco", "@flacjacket"], "config_flow": true, - "dependencies": ["http"], + "dependencies": ["http", "repairs"], "documentation": "https://www.home-assistant.io/integrations/doorbird", "iot_class": "local_push", "loggers": ["doorbirdpy"], diff --git a/homeassistant/components/doorbird/repairs.py b/homeassistant/components/doorbird/repairs.py new file mode 100644 index 00000000000..c8f9b73ecbd --- /dev/null +++ b/homeassistant/components/doorbird/repairs.py @@ -0,0 +1,55 @@ +"""Repairs for DoorBird.""" + +from __future__ import annotations + +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import RepairsFlow +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + + +class DoorBirdReloadConfirmRepairFlow(RepairsFlow): + """Handler to show doorbird error and reload.""" + + def __init__(self, entry_id: str) -> None: + """Initialize the flow.""" + self.entry_id = entry_id + + 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: + self.hass.config_entries.async_schedule_reload(self.entry_id) + return self.async_create_entry(data={}) + + issue_registry = ir.async_get(self.hass) + description_placeholders = None + if issue := issue_registry.async_get_issue(self.handler, self.issue_id): + description_placeholders = issue.translation_placeholders + + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema({}), + description_placeholders=description_placeholders, + ) + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: + """Create flow.""" + assert data is not None + entry_id = data["entry_id"] + assert isinstance(entry_id, str) + return DoorBirdReloadConfirmRepairFlow(entry_id=entry_id) diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index 29c85ec7311..090ba4f161f 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -11,6 +11,19 @@ } } }, + "issues": { + "error_registering_events": { + "title": "DoorBird {name} configuration failure", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::doorbird::issues::error_registering_events::title%]", + "description": "Configuring DoorBird {name} failed with error: `{error}`. Please enable the API Operator permission for the DoorBird user and continue to reload the integration." + } + } + } + } + }, "config": { "step": { "user": { diff --git a/tests/components/doorbird/test_repairs.py b/tests/components/doorbird/test_repairs.py new file mode 100644 index 00000000000..7449250b718 --- /dev/null +++ b/tests/components/doorbird/test_repairs.py @@ -0,0 +1,61 @@ +"""Test repairs for doorbird.""" + +from __future__ import annotations + +from http import HTTPStatus + +from homeassistant.components.doorbird.const import DOMAIN +from homeassistant.components.repairs.issue_handler import ( + async_process_repairs_platforms, +) +from homeassistant.components.repairs.websocket_api import ( + RepairsFlowIndexView, + RepairsFlowResourceView, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + +from . import mock_not_found_exception +from .conftest import DoorbirdMockerType + +from tests.typing import ClientSessionGenerator + + +async def test_change_schedule_fails( + hass: HomeAssistant, + doorbird_mocker: DoorbirdMockerType, + hass_client: ClientSessionGenerator, +) -> None: + """Test a doorbird when change_schedule fails.""" + assert await async_setup_component(hass, "repairs", {}) + doorbird_entry = await doorbird_mocker( + favorites_side_effect=mock_not_found_exception() + ) + assert doorbird_entry.entry.state is ConfigEntryState.SETUP_RETRY + issue_reg = ir.async_get(hass) + assert len(issue_reg.issues) == 1 + issue = list(issue_reg.issues.values())[0] + issue_id = issue.issue_id + assert issue.domain == DOMAIN + + await async_process_repairs_platforms(hass) + client = await hass_client() + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": issue_id}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + placeholders = data["description_placeholders"] + assert "404" in placeholders["error"] + assert data["step_id"] == "confirm" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" From 0216455137c86a28f8a91b57a81262cac125fad2 Mon Sep 17 00:00:00 2001 From: Matrix Date: Thu, 1 Aug 2024 14:32:16 +0800 Subject: [PATCH 1900/2411] Fix yolink protocol changed (#122989) --- homeassistant/components/yolink/valve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/yolink/valve.py b/homeassistant/components/yolink/valve.py index a24ad7d385d..d8c199697c3 100644 --- a/homeassistant/components/yolink/valve.py +++ b/homeassistant/components/yolink/valve.py @@ -37,7 +37,7 @@ DEVICE_TYPES: tuple[YoLinkValveEntityDescription, ...] = ( key="valve_state", translation_key="meter_valve_state", device_class=ValveDeviceClass.WATER, - value=lambda value: value == "closed" if value is not None else None, + value=lambda value: value != "open" if value is not None else None, exists_fn=lambda device: device.device_type == ATTR_DEVICE_WATER_METER_CONTROLLER and not device.device_model_name.startswith(DEV_MODEL_WATER_METER_YS5007), From acf523b5fb75cae738ef77c35c2331381bf99abe Mon Sep 17 00:00:00 2001 From: amccook <30292381+amccook@users.noreply.github.com> Date: Thu, 1 Aug 2024 07:24:09 -0700 Subject: [PATCH 1901/2411] Fix handling of directory type playlists in Plex (#122990) Ignore type directory --- homeassistant/components/plex/media_browser.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index e47e6145761..87e9f47af66 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -132,7 +132,11 @@ def browse_media( # noqa: C901 "children": [], } for playlist in plex_server.playlists(): - if playlist.playlistType != "audio" and platform == "sonos": + if ( + playlist.type != "directory" + and playlist.playlistType != "audio" + and platform == "sonos" + ): continue try: playlists_info["children"].append(item_payload(playlist)) From 55abe68a5fcb3e74726a0321f80b188a602d1125 Mon Sep 17 00:00:00 2001 From: Ivan Belokobylskiy Date: Thu, 1 Aug 2024 12:51:41 +0400 Subject: [PATCH 1902/2411] Bump aioymaps to 1.2.5 (#123005) Bump aiomaps, fix sessionId parsing --- homeassistant/components/yandex_transport/manifest.json | 2 +- homeassistant/components/yandex_transport/sensor.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index c29b4d3dc98..1d1219d5a95 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@rishatik92", "@devbis"], "documentation": "https://www.home-assistant.io/integrations/yandex_transport", "iot_class": "cloud_polling", - "requirements": ["aioymaps==1.2.4"] + "requirements": ["aioymaps==1.2.5"] } diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 30227e3261e..95c4785a341 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import timedelta import logging -from aioymaps import CaptchaError, YandexMapsRequester +from aioymaps import CaptchaError, NoSessionError, YandexMapsRequester import voluptuous as vol from homeassistant.components.sensor import ( @@ -88,7 +88,7 @@ class DiscoverYandexTransport(SensorEntity): closer_time = None try: yandex_reply = await self.requester.get_stop_info(self._stop_id) - except CaptchaError as ex: + except (CaptchaError, NoSessionError) as ex: _LOGGER.error( "%s. You may need to disable the integration for some time", ex, diff --git a/requirements_all.txt b/requirements_all.txt index ad08342230d..3bd6cfa8b2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -407,7 +407,7 @@ aiowebostv==0.4.2 aiowithings==3.0.2 # homeassistant.components.yandex_transport -aioymaps==1.2.4 +aioymaps==1.2.5 # homeassistant.components.airgradient airgradient==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bdf58ff217..21c8fd5f677 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -389,7 +389,7 @@ aiowebostv==0.4.2 aiowithings==3.0.2 # homeassistant.components.yandex_transport -aioymaps==1.2.4 +aioymaps==1.2.5 # homeassistant.components.airgradient airgradient==0.7.1 From e9bfe82582f94f92c3db5597eb869cf7c0fcf678 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Aug 2024 11:51:45 +0200 Subject: [PATCH 1903/2411] Make the Android timer notification high priority (#123006) --- homeassistant/components/mobile_app/timers.py | 2 ++ tests/components/mobile_app/test_timers.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/mobile_app/timers.py b/homeassistant/components/mobile_app/timers.py index 93b4ac53be5..e092298c5d7 100644 --- a/homeassistant/components/mobile_app/timers.py +++ b/homeassistant/components/mobile_app/timers.py @@ -39,6 +39,8 @@ def async_handle_timer_event( # Android "channel": "Timers", "importance": "high", + "ttl": 0, + "priority": "high", # iOS "push": { "interruption-level": "time-sensitive", diff --git a/tests/components/mobile_app/test_timers.py b/tests/components/mobile_app/test_timers.py index 0eba88f7328..9f7d4cebc58 100644 --- a/tests/components/mobile_app/test_timers.py +++ b/tests/components/mobile_app/test_timers.py @@ -61,6 +61,8 @@ async def test_timer_events( "channel": "Timers", "group": "timers", "importance": "high", + "ttl": 0, + "priority": "high", "push": { "interruption-level": "time-sensitive", }, From ecbff61332364059bf089b69d00248a63f4b4a2a Mon Sep 17 00:00:00 2001 From: Matrix Date: Thu, 1 Aug 2024 17:49:58 +0800 Subject: [PATCH 1904/2411] Bump yolink api to 0.4.6 (#123012) --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 5353d5d5b8c..ceb4e4ceff3 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.4.4"] + "requirements": ["yolink-api==0.4.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3bd6cfa8b2a..0bf4b77e9d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2962,7 +2962,7 @@ yeelight==0.7.14 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.4.4 +yolink-api==0.4.6 # homeassistant.components.youless youless-api==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21c8fd5f677..f9670987b70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2339,7 +2339,7 @@ yalexs==6.4.3 yeelight==0.7.14 # homeassistant.components.yolink -yolink-api==0.4.4 +yolink-api==0.4.6 # homeassistant.components.youless youless-api==2.1.2 From a42615add0692b9752f2b1bfa395f8eeedd4e4cf Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 2 Aug 2024 15:58:41 +0200 Subject: [PATCH 1905/2411] Fix and improve tedee lock states (#123022) Improve tedee lock states --- homeassistant/components/tedee/lock.py | 13 +++++++++++-- tests/components/tedee/test_lock.py | 22 ++++++++++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tedee/lock.py b/homeassistant/components/tedee/lock.py index d11c873a94a..8d5fa028e12 100644 --- a/homeassistant/components/tedee/lock.py +++ b/homeassistant/components/tedee/lock.py @@ -55,8 +55,13 @@ class TedeeLockEntity(TedeeEntity, LockEntity): super().__init__(lock, coordinator, "lock") @property - def is_locked(self) -> bool: + def is_locked(self) -> bool | None: """Return true if lock is locked.""" + if self._lock.state in ( + TedeeLockState.HALF_OPEN, + TedeeLockState.UNKNOWN, + ): + return None return self._lock.state == TedeeLockState.LOCKED @property @@ -87,7 +92,11 @@ class TedeeLockEntity(TedeeEntity, LockEntity): @property def available(self) -> bool: """Return True if entity is available.""" - return super().available and self._lock.is_connected + return ( + super().available + and self._lock.is_connected + and self._lock.state != TedeeLockState.UNCALIBRATED + ) async def async_unlock(self, **kwargs: Any) -> None: """Unlock the door.""" diff --git a/tests/components/tedee/test_lock.py b/tests/components/tedee/test_lock.py index ffc4a8c30d6..741bc3156cb 100644 --- a/tests/components/tedee/test_lock.py +++ b/tests/components/tedee/test_lock.py @@ -25,7 +25,7 @@ from homeassistant.components.lock import ( STATE_UNLOCKING, ) from homeassistant.components.webhook import async_generate_url -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -276,10 +276,21 @@ async def test_new_lock( assert state +@pytest.mark.parametrize( + ("lib_state", "expected_state"), + [ + (TedeeLockState.LOCKED, STATE_LOCKED), + (TedeeLockState.HALF_OPEN, STATE_UNKNOWN), + (TedeeLockState.UNKNOWN, STATE_UNKNOWN), + (TedeeLockState.UNCALIBRATED, STATE_UNAVAILABLE), + ], +) async def test_webhook_update( hass: HomeAssistant, mock_tedee: MagicMock, hass_client_no_auth: ClientSessionGenerator, + lib_state: TedeeLockState, + expected_state: str, ) -> None: """Test updated data set through webhook.""" @@ -287,10 +298,9 @@ async def test_webhook_update( assert state assert state.state == STATE_UNLOCKED - webhook_data = {"dummystate": 6} - mock_tedee.locks_dict[ - 12345 - ].state = TedeeLockState.LOCKED # is updated in the lib, so mock and assert in L296 + webhook_data = {"dummystate": lib_state.value} + # is updated in the lib, so mock and assert below + mock_tedee.locks_dict[12345].state = lib_state client = await hass_client_no_auth() webhook_url = async_generate_url(hass, WEBHOOK_ID) @@ -302,4 +312,4 @@ async def test_webhook_update( state = hass.states.get("lock.lock_1a2b") assert state - assert state.state == STATE_LOCKED + assert state.state == expected_state From 5ce8a2d974a7cc80ac8e9a28640c5bb568fad9fc Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 1 Aug 2024 15:39:17 -0500 Subject: [PATCH 1906/2411] Standardize assist pipelines on 10ms chunk size (#123024) * Make chunk size always 10ms * Fix voip --- .../components/assist_pipeline/__init__.py | 8 ++ .../assist_pipeline/audio_enhancer.py | 18 +--- .../components/assist_pipeline/const.py | 4 +- .../components/assist_pipeline/pipeline.py | 75 +++---------- homeassistant/components/voip/voip.py | 22 ++-- tests/components/assist_pipeline/conftest.py | 13 +++ .../snapshots/test_websocket.ambr | 2 +- tests/components/assist_pipeline/test_init.py | 71 +++++++------ .../assist_pipeline/test_websocket.py | 100 +++++++++++------- 9 files changed, 154 insertions(+), 159 deletions(-) diff --git a/homeassistant/components/assist_pipeline/__init__.py b/homeassistant/components/assist_pipeline/__init__.py index f481411e551..8ee053162b0 100644 --- a/homeassistant/components/assist_pipeline/__init__.py +++ b/homeassistant/components/assist_pipeline/__init__.py @@ -16,6 +16,10 @@ from .const import ( DATA_LAST_WAKE_UP, DOMAIN, EVENT_RECORDING, + SAMPLE_CHANNELS, + SAMPLE_RATE, + SAMPLE_WIDTH, + SAMPLES_PER_CHUNK, ) from .error import PipelineNotFound from .pipeline import ( @@ -53,6 +57,10 @@ __all__ = ( "PipelineNotFound", "WakeWordSettings", "EVENT_RECORDING", + "SAMPLES_PER_CHUNK", + "SAMPLE_RATE", + "SAMPLE_WIDTH", + "SAMPLE_CHANNELS", ) CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/assist_pipeline/audio_enhancer.py b/homeassistant/components/assist_pipeline/audio_enhancer.py index e7a149bd00e..c9c60f421b1 100644 --- a/homeassistant/components/assist_pipeline/audio_enhancer.py +++ b/homeassistant/components/assist_pipeline/audio_enhancer.py @@ -6,6 +6,8 @@ import logging from pymicro_vad import MicroVad +from .const import BYTES_PER_CHUNK + _LOGGER = logging.getLogger(__name__) @@ -38,11 +40,6 @@ class AudioEnhancer(ABC): def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk: """Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples.""" - @property - @abstractmethod - def samples_per_chunk(self) -> int | None: - """Return number of samples per chunk or None if chunking isn't required.""" - class MicroVadEnhancer(AudioEnhancer): """Audio enhancer that just runs microVAD.""" @@ -61,22 +58,15 @@ class MicroVadEnhancer(AudioEnhancer): _LOGGER.debug("Initialized microVAD with threshold=%s", self.threshold) def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk: - """Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples.""" + """Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples.""" is_speech: bool | None = None if self.vad is not None: # Run VAD + assert len(audio) == BYTES_PER_CHUNK speech_prob = self.vad.Process10ms(audio) is_speech = speech_prob > self.threshold return EnhancedAudioChunk( audio=audio, timestamp_ms=timestamp_ms, is_speech=is_speech ) - - @property - def samples_per_chunk(self) -> int | None: - """Return number of samples per chunk or None if chunking isn't required.""" - if self.is_vad_enabled: - return 160 # 10ms - - return None diff --git a/homeassistant/components/assist_pipeline/const.py b/homeassistant/components/assist_pipeline/const.py index 14b93a90372..f7306b89a54 100644 --- a/homeassistant/components/assist_pipeline/const.py +++ b/homeassistant/components/assist_pipeline/const.py @@ -19,4 +19,6 @@ EVENT_RECORDING = f"{DOMAIN}_recording" SAMPLE_RATE = 16000 # hertz SAMPLE_WIDTH = 2 # bytes SAMPLE_CHANNELS = 1 # mono -SAMPLES_PER_CHUNK = 240 # 20 ms @ 16Khz +MS_PER_CHUNK = 10 +SAMPLES_PER_CHUNK = SAMPLE_RATE // (1000 // MS_PER_CHUNK) # 10 ms @ 16Khz +BYTES_PER_CHUNK = SAMPLES_PER_CHUNK * SAMPLE_WIDTH * SAMPLE_CHANNELS # 16-bit diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index af29888eb07..9fada934ca1 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -51,11 +51,13 @@ from homeassistant.util.limited_size_dict import LimitedSizeDict from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, MicroVadEnhancer from .const import ( + BYTES_PER_CHUNK, CONF_DEBUG_RECORDING_DIR, DATA_CONFIG, DATA_LAST_WAKE_UP, DATA_MIGRATIONS, DOMAIN, + MS_PER_CHUNK, SAMPLE_CHANNELS, SAMPLE_RATE, SAMPLE_WIDTH, @@ -502,9 +504,6 @@ class AudioSettings: is_vad_enabled: bool = True """True if VAD is used to determine the end of the voice command.""" - samples_per_chunk: int | None = None - """Number of samples that will be in each audio chunk (None for no chunking).""" - silence_seconds: float = 0.5 """Seconds of silence after voice command has ended.""" @@ -525,11 +524,6 @@ class AudioSettings: or (self.auto_gain_dbfs > 0) ) - @property - def is_chunking_enabled(self) -> bool: - """True if chunk size is set.""" - return self.samples_per_chunk is not None - @dataclass class PipelineRun: @@ -566,7 +560,9 @@ class PipelineRun: audio_enhancer: AudioEnhancer | None = None """VAD/noise suppression/auto gain""" - audio_chunking_buffer: AudioBuffer | None = None + audio_chunking_buffer: AudioBuffer = field( + default_factory=lambda: AudioBuffer(BYTES_PER_CHUNK) + ) """Buffer used when splitting audio into chunks for audio processing""" _device_id: str | None = None @@ -599,8 +595,6 @@ class PipelineRun: self.audio_settings.is_vad_enabled, ) - self.audio_chunking_buffer = AudioBuffer(self.samples_per_chunk * SAMPLE_WIDTH) - def __eq__(self, other: object) -> bool: """Compare pipeline runs by id.""" if isinstance(other, PipelineRun): @@ -608,14 +602,6 @@ class PipelineRun: return False - @property - def samples_per_chunk(self) -> int: - """Return number of samples expected in each audio chunk.""" - if self.audio_enhancer is not None: - return self.audio_enhancer.samples_per_chunk or SAMPLES_PER_CHUNK - - return self.audio_settings.samples_per_chunk or SAMPLES_PER_CHUNK - @callback def process_event(self, event: PipelineEvent) -> None: """Log an event and call listener.""" @@ -728,7 +714,7 @@ class PipelineRun: # after wake-word-detection. num_audio_chunks_to_buffer = int( (wake_word_settings.audio_seconds_to_buffer * SAMPLE_RATE) - / self.samples_per_chunk + / SAMPLES_PER_CHUNK ) stt_audio_buffer: deque[EnhancedAudioChunk] | None = None @@ -1216,60 +1202,31 @@ class PipelineRun: self.debug_recording_thread = None async def process_volume_only( - self, - audio_stream: AsyncIterable[bytes], - sample_rate: int = SAMPLE_RATE, - sample_width: int = SAMPLE_WIDTH, + self, audio_stream: AsyncIterable[bytes] ) -> AsyncGenerator[EnhancedAudioChunk]: """Apply volume transformation only (no VAD/audio enhancements) with optional chunking.""" - assert self.audio_chunking_buffer is not None - - bytes_per_chunk = self.samples_per_chunk * sample_width - ms_per_sample = sample_rate // 1000 - ms_per_chunk = self.samples_per_chunk // ms_per_sample timestamp_ms = 0 - async for chunk in audio_stream: if self.audio_settings.volume_multiplier != 1.0: chunk = _multiply_volume(chunk, self.audio_settings.volume_multiplier) - if self.audio_settings.is_chunking_enabled: - for sub_chunk in chunk_samples( - chunk, bytes_per_chunk, self.audio_chunking_buffer - ): - yield EnhancedAudioChunk( - audio=sub_chunk, - timestamp_ms=timestamp_ms, - is_speech=None, # no VAD - ) - timestamp_ms += ms_per_chunk - else: - # No chunking + for sub_chunk in chunk_samples( + chunk, BYTES_PER_CHUNK, self.audio_chunking_buffer + ): yield EnhancedAudioChunk( - audio=chunk, + audio=sub_chunk, timestamp_ms=timestamp_ms, is_speech=None, # no VAD ) - timestamp_ms += (len(chunk) // sample_width) // ms_per_sample + timestamp_ms += MS_PER_CHUNK async def process_enhance_audio( - self, - audio_stream: AsyncIterable[bytes], - sample_rate: int = SAMPLE_RATE, - sample_width: int = SAMPLE_WIDTH, + self, audio_stream: AsyncIterable[bytes] ) -> AsyncGenerator[EnhancedAudioChunk]: - """Split audio into 10 ms chunks and apply VAD/noise suppression/auto gain/volume transformation.""" + """Split audio into chunks and apply VAD/noise suppression/auto gain/volume transformation.""" assert self.audio_enhancer is not None - assert self.audio_enhancer.samples_per_chunk is not None - assert self.audio_chunking_buffer is not None - bytes_per_chunk = self.audio_enhancer.samples_per_chunk * sample_width - ms_per_sample = sample_rate // 1000 - ms_per_chunk = ( - self.audio_enhancer.samples_per_chunk // sample_width - ) // ms_per_sample timestamp_ms = 0 - async for dirty_samples in audio_stream: if self.audio_settings.volume_multiplier != 1.0: # Static gain @@ -1279,10 +1236,10 @@ class PipelineRun: # Split into chunks for audio enhancements/VAD for dirty_chunk in chunk_samples( - dirty_samples, bytes_per_chunk, self.audio_chunking_buffer + dirty_samples, BYTES_PER_CHUNK, self.audio_chunking_buffer ): yield self.audio_enhancer.enhance_chunk(dirty_chunk, timestamp_ms) - timestamp_ms += ms_per_chunk + timestamp_ms += MS_PER_CHUNK def _multiply_volume(chunk: bytes, volume_multiplier: float) -> bytes: diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 243909629cf..161e938a3b6 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -21,7 +21,7 @@ from voip_utils import ( VoipDatagramProtocol, ) -from homeassistant.components import stt, tts +from homeassistant.components import assist_pipeline, stt, tts from homeassistant.components.assist_pipeline import ( Pipeline, PipelineEvent, @@ -331,15 +331,14 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): async with asyncio.timeout(self.audio_timeout): chunk = await self._audio_queue.get() - assert audio_enhancer.samples_per_chunk is not None - vad_buffer = AudioBuffer(audio_enhancer.samples_per_chunk * WIDTH) + vad_buffer = AudioBuffer(assist_pipeline.SAMPLES_PER_CHUNK * WIDTH) while chunk: chunk_buffer.append(chunk) segmenter.process_with_vad( chunk, - audio_enhancer.samples_per_chunk, + assist_pipeline.SAMPLES_PER_CHUNK, lambda x: audio_enhancer.enhance_chunk(x, 0).is_speech is True, vad_buffer, ) @@ -371,13 +370,12 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): async with asyncio.timeout(self.audio_timeout): chunk = await self._audio_queue.get() - assert audio_enhancer.samples_per_chunk is not None - vad_buffer = AudioBuffer(audio_enhancer.samples_per_chunk * WIDTH) + vad_buffer = AudioBuffer(assist_pipeline.SAMPLES_PER_CHUNK * WIDTH) while chunk: if not segmenter.process_with_vad( chunk, - audio_enhancer.samples_per_chunk, + assist_pipeline.SAMPLES_PER_CHUNK, lambda x: audio_enhancer.enhance_chunk(x, 0).is_speech is True, vad_buffer, ): @@ -437,13 +435,13 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): sample_channels = wav_file.getnchannels() if ( - (sample_rate != 16000) - or (sample_width != 2) - or (sample_channels != 1) + (sample_rate != RATE) + or (sample_width != WIDTH) + or (sample_channels != CHANNELS) ): raise ValueError( - "Expected rate/width/channels as 16000/2/1," - " got {sample_rate}/{sample_width}/{sample_channels}}" + f"Expected rate/width/channels as {RATE}/{WIDTH}/{CHANNELS}," + f" got {sample_rate}/{sample_width}/{sample_channels}" ) audio_bytes = wav_file.readframes(wav_file.getnframes()) diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index c041a54d8fa..b2eca1e7ce1 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -11,6 +11,12 @@ import pytest from homeassistant.components import stt, tts, wake_word from homeassistant.components.assist_pipeline import DOMAIN, select as assist_select +from homeassistant.components.assist_pipeline.const import ( + BYTES_PER_CHUNK, + SAMPLE_CHANNELS, + SAMPLE_RATE, + SAMPLE_WIDTH, +) from homeassistant.components.assist_pipeline.pipeline import ( PipelineData, PipelineStorageCollection, @@ -33,6 +39,8 @@ from tests.common import ( _TRANSCRIPT = "test transcript" +BYTES_ONE_SECOND = SAMPLE_RATE * SAMPLE_WIDTH * SAMPLE_CHANNELS + @pytest.fixture(autouse=True) def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: @@ -462,3 +470,8 @@ def pipeline_data(hass: HomeAssistant, init_components) -> PipelineData: def pipeline_storage(pipeline_data) -> PipelineStorageCollection: """Return pipeline storage collection.""" return pipeline_data.pipeline_store + + +def make_10ms_chunk(header: bytes) -> bytes: + """Return 10ms of zeros with the given header.""" + return header + bytes(BYTES_PER_CHUNK - len(header)) diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 0b04b67bb22..e5ae18d28f2 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -440,7 +440,7 @@ # --- # name: test_device_capture_override.2 dict({ - 'audio': 'Y2h1bmsx', + 'audio': 'Y2h1bmsxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', 'channels': 1, 'rate': 16000, 'type': 'audio', diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index 8fb7ce5b5a5..4206a288331 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -13,6 +13,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components import assist_pipeline, media_source, stt, tts from homeassistant.components.assist_pipeline.const import ( + BYTES_PER_CHUNK, CONF_DEBUG_RECORDING_DIR, DOMAIN, ) @@ -20,16 +21,16 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component from .conftest import ( + BYTES_ONE_SECOND, MockSttProvider, MockSttProviderEntity, MockTTSProvider, MockWakeWordEntity, + make_10ms_chunk, ) from tests.typing import ClientSessionGenerator, WebSocketGenerator -BYTES_ONE_SECOND = 16000 * 2 - def process_events(events: list[assist_pipeline.PipelineEvent]) -> list[dict]: """Process events to remove dynamic values.""" @@ -58,8 +59,8 @@ async def test_pipeline_from_audio_stream_auto( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" await assist_pipeline.async_pipeline_from_audio_stream( @@ -79,7 +80,9 @@ async def test_pipeline_from_audio_stream_auto( ) assert process_events(events) == snapshot - assert mock_stt_provider.received == [b"part1", b"part2"] + assert len(mock_stt_provider.received) == 2 + assert mock_stt_provider.received[0].startswith(b"part1") + assert mock_stt_provider.received[1].startswith(b"part2") async def test_pipeline_from_audio_stream_legacy( @@ -98,8 +101,8 @@ async def test_pipeline_from_audio_stream_legacy( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" # Create a pipeline using an stt entity @@ -142,7 +145,9 @@ async def test_pipeline_from_audio_stream_legacy( ) assert process_events(events) == snapshot - assert mock_stt_provider.received == [b"part1", b"part2"] + assert len(mock_stt_provider.received) == 2 + assert mock_stt_provider.received[0].startswith(b"part1") + assert mock_stt_provider.received[1].startswith(b"part2") async def test_pipeline_from_audio_stream_entity( @@ -161,8 +166,8 @@ async def test_pipeline_from_audio_stream_entity( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" # Create a pipeline using an stt entity @@ -205,7 +210,9 @@ async def test_pipeline_from_audio_stream_entity( ) assert process_events(events) == snapshot - assert mock_stt_provider_entity.received == [b"part1", b"part2"] + assert len(mock_stt_provider_entity.received) == 2 + assert mock_stt_provider_entity.received[0].startswith(b"part1") + assert mock_stt_provider_entity.received[1].startswith(b"part2") async def test_pipeline_from_audio_stream_no_stt( @@ -224,8 +231,8 @@ async def test_pipeline_from_audio_stream_no_stt( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" # Create a pipeline without stt support @@ -285,8 +292,8 @@ async def test_pipeline_from_audio_stream_unknown_pipeline( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" # Try to use the created pipeline @@ -327,7 +334,7 @@ async def test_pipeline_from_audio_stream_wake_word( # [0, 2, ...] wake_chunk_2 = bytes(it.islice(it.cycle(range(0, 256, 2)), BYTES_ONE_SECOND)) - samples_per_chunk = 160 + samples_per_chunk = 160 # 10ms @ 16Khz bytes_per_chunk = samples_per_chunk * 2 # 16-bit async def audio_data(): @@ -343,8 +350,8 @@ async def test_pipeline_from_audio_stream_wake_word( yield wake_chunk_2[i : i + bytes_per_chunk] i += bytes_per_chunk - for chunk in (b"wake word!", b"part1", b"part2"): - yield chunk + bytes(bytes_per_chunk - len(chunk)) + for header in (b"wake word!", b"part1", b"part2"): + yield make_10ms_chunk(header) yield b"" @@ -365,9 +372,7 @@ async def test_pipeline_from_audio_stream_wake_word( wake_word_settings=assist_pipeline.WakeWordSettings( audio_seconds_to_buffer=1.5 ), - audio_settings=assist_pipeline.AudioSettings( - is_vad_enabled=False, samples_per_chunk=samples_per_chunk - ), + audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), ) assert process_events(events) == snapshot @@ -408,13 +413,11 @@ async def test_pipeline_save_audio( pipeline = assist_pipeline.async_get_pipeline(hass) events: list[assist_pipeline.PipelineEvent] = [] - # Pad out to an even number of bytes since these "samples" will be saved - # as 16-bit values. async def audio_data(): - yield b"wake word_" + yield make_10ms_chunk(b"wake word") # queued audio - yield b"part1_" - yield b"part2_" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" await assist_pipeline.async_pipeline_from_audio_stream( @@ -457,12 +460,16 @@ async def test_pipeline_save_audio( # Verify wake file with wave.open(str(wake_file), "rb") as wake_wav: wake_data = wake_wav.readframes(wake_wav.getnframes()) - assert wake_data == b"wake word_" + assert wake_data.startswith(b"wake word") # Verify stt file with wave.open(str(stt_file), "rb") as stt_wav: stt_data = stt_wav.readframes(stt_wav.getnframes()) - assert stt_data == b"queued audiopart1_part2_" + assert stt_data.startswith(b"queued audio") + stt_data = stt_data[len(b"queued audio") :] + assert stt_data.startswith(b"part1") + stt_data = stt_data[BYTES_PER_CHUNK:] + assert stt_data.startswith(b"part2") async def test_pipeline_saved_audio_with_device_id( @@ -645,10 +652,10 @@ async def test_wake_word_detection_aborted( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"silence!" - yield b"wake word!" - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"silence!") + yield make_10ms_chunk(b"wake word!") + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" pipeline_store = pipeline_data.pipeline_store diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index 7d4a9b18c12..2da914f4252 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -8,7 +8,12 @@ from unittest.mock import ANY, patch import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.assist_pipeline.const import DOMAIN +from homeassistant.components.assist_pipeline.const import ( + DOMAIN, + SAMPLE_CHANNELS, + SAMPLE_RATE, + SAMPLE_WIDTH, +) from homeassistant.components.assist_pipeline.pipeline import ( DeviceAudioQueue, Pipeline, @@ -18,7 +23,13 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr -from .conftest import MockWakeWordEntity, MockWakeWordEntity2 +from .conftest import ( + BYTES_ONE_SECOND, + BYTES_PER_CHUNK, + MockWakeWordEntity, + MockWakeWordEntity2, + make_10ms_chunk, +) from tests.common import MockConfigEntry from tests.typing import WebSocketGenerator @@ -205,7 +216,7 @@ async def test_audio_pipeline_with_wake_word_timeout( "start_stage": "wake_word", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "timeout": 1, }, } @@ -229,7 +240,7 @@ async def test_audio_pipeline_with_wake_word_timeout( events.append(msg["event"]) # 2 seconds of silence - await client.send_bytes(bytes([1]) + bytes(16000 * 2 * 2)) + await client.send_bytes(bytes([1]) + bytes(2 * BYTES_ONE_SECOND)) # Time out error msg = await client.receive_json() @@ -259,7 +270,7 @@ async def test_audio_pipeline_with_wake_word_no_timeout( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "timeout": 0, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "timeout": 0, "no_vad": True}, } ) @@ -282,9 +293,10 @@ async def test_audio_pipeline_with_wake_word_no_timeout( events.append(msg["event"]) # "audio" - await client.send_bytes(bytes([handler_id]) + b"wake word") + await client.send_bytes(bytes([handler_id]) + make_10ms_chunk(b"wake word")) - msg = await client.receive_json() + async with asyncio.timeout(1): + msg = await client.receive_json() assert msg["event"]["type"] == "wake_word-end" assert msg["event"]["data"] == snapshot events.append(msg["event"]) @@ -365,7 +377,7 @@ async def test_audio_pipeline_no_wake_word_engine( "start_stage": "wake_word", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, }, } ) @@ -402,7 +414,7 @@ async def test_audio_pipeline_no_wake_word_entity( "start_stage": "wake_word", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, }, } ) @@ -1771,7 +1783,7 @@ async def test_audio_pipeline_with_enhancements( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, # Enhancements "noise_suppression_level": 2, "auto_gain_dbfs": 15, @@ -1801,7 +1813,7 @@ async def test_audio_pipeline_with_enhancements( # One second of silence. # This will pass through the audio enhancement pipeline, but we don't test # the actual output. - await client.send_bytes(bytes([handler_id]) + bytes(16000 * 2)) + await client.send_bytes(bytes([handler_id]) + bytes(BYTES_ONE_SECOND)) # End of audio stream (handler id + empty payload) await client.send_bytes(bytes([handler_id])) @@ -1871,7 +1883,7 @@ async def test_wake_word_cooldown_same_id( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -1880,7 +1892,7 @@ async def test_wake_word_cooldown_same_id( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -1914,8 +1926,8 @@ async def test_wake_word_cooldown_same_id( assert msg["event"]["data"] == snapshot # Wake both up at the same time - await client_1.send_bytes(bytes([handler_id_1]) + b"wake word") - await client_2.send_bytes(bytes([handler_id_2]) + b"wake word") + await client_1.send_bytes(bytes([handler_id_1]) + make_10ms_chunk(b"wake word")) + await client_2.send_bytes(bytes([handler_id_2]) + make_10ms_chunk(b"wake word")) # Get response events error_data: dict[str, Any] | None = None @@ -1954,7 +1966,7 @@ async def test_wake_word_cooldown_different_ids( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -1963,7 +1975,7 @@ async def test_wake_word_cooldown_different_ids( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -1997,8 +2009,8 @@ async def test_wake_word_cooldown_different_ids( assert msg["event"]["data"] == snapshot # Wake both up at the same time, but they will have different wake word ids - await client_1.send_bytes(bytes([handler_id_1]) + b"wake word") - await client_2.send_bytes(bytes([handler_id_2]) + b"wake word") + await client_1.send_bytes(bytes([handler_id_1]) + make_10ms_chunk(b"wake word")) + await client_2.send_bytes(bytes([handler_id_2]) + make_10ms_chunk(b"wake word")) # Get response events msg = await client_1.receive_json() @@ -2073,7 +2085,7 @@ async def test_wake_word_cooldown_different_entities( "pipeline": pipeline_id_1, "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -2084,7 +2096,7 @@ async def test_wake_word_cooldown_different_entities( "pipeline": pipeline_id_2, "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -2119,8 +2131,8 @@ async def test_wake_word_cooldown_different_entities( # Wake both up at the same time. # They will have the same wake word id, but different entities. - await client_1.send_bytes(bytes([handler_id_1]) + b"wake word") - await client_2.send_bytes(bytes([handler_id_2]) + b"wake word") + await client_1.send_bytes(bytes([handler_id_1]) + make_10ms_chunk(b"wake word")) + await client_2.send_bytes(bytes([handler_id_2]) + make_10ms_chunk(b"wake word")) # Get response events error_data: dict[str, Any] | None = None @@ -2158,7 +2170,11 @@ async def test_device_capture( identifiers={("demo", "satellite-1234")}, ) - audio_chunks = [b"chunk1", b"chunk2", b"chunk3"] + audio_chunks = [ + make_10ms_chunk(b"chunk1"), + make_10ms_chunk(b"chunk2"), + make_10ms_chunk(b"chunk3"), + ] # Start capture client_capture = await hass_ws_client(hass) @@ -2181,7 +2197,7 @@ async def test_device_capture( "type": "assist_pipeline/run", "start_stage": "stt", "end_stage": "stt", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, "device_id": satellite_device.id, } ) @@ -2232,9 +2248,9 @@ async def test_device_capture( # Verify audio chunks for i, audio_chunk in enumerate(audio_chunks): assert events[i]["type"] == "audio" - assert events[i]["rate"] == 16000 - assert events[i]["width"] == 2 - assert events[i]["channels"] == 1 + assert events[i]["rate"] == SAMPLE_RATE + assert events[i]["width"] == SAMPLE_WIDTH + assert events[i]["channels"] == SAMPLE_CHANNELS # Audio is base64 encoded assert events[i]["audio"] == base64.b64encode(audio_chunk).decode("ascii") @@ -2259,7 +2275,11 @@ async def test_device_capture_override( identifiers={("demo", "satellite-1234")}, ) - audio_chunks = [b"chunk1", b"chunk2", b"chunk3"] + audio_chunks = [ + make_10ms_chunk(b"chunk1"), + make_10ms_chunk(b"chunk2"), + make_10ms_chunk(b"chunk3"), + ] # Start first capture client_capture_1 = await hass_ws_client(hass) @@ -2282,7 +2302,7 @@ async def test_device_capture_override( "type": "assist_pipeline/run", "start_stage": "stt", "end_stage": "stt", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, "device_id": satellite_device.id, } ) @@ -2365,9 +2385,9 @@ async def test_device_capture_override( # Verify all but first audio chunk for i, audio_chunk in enumerate(audio_chunks[1:]): assert events[i]["type"] == "audio" - assert events[i]["rate"] == 16000 - assert events[i]["width"] == 2 - assert events[i]["channels"] == 1 + assert events[i]["rate"] == SAMPLE_RATE + assert events[i]["width"] == SAMPLE_WIDTH + assert events[i]["channels"] == SAMPLE_CHANNELS # Audio is base64 encoded assert events[i]["audio"] == base64.b64encode(audio_chunk).decode("ascii") @@ -2427,7 +2447,7 @@ async def test_device_capture_queue_full( "type": "assist_pipeline/run", "start_stage": "stt", "end_stage": "stt", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, "device_id": satellite_device.id, } ) @@ -2448,8 +2468,8 @@ async def test_device_capture_queue_full( assert msg["event"]["type"] == "stt-start" assert msg["event"]["data"] == snapshot - # Single sample will "overflow" the queue - await client_pipeline.send_bytes(bytes([handler_id, 0, 0])) + # Single chunk will "overflow" the queue + await client_pipeline.send_bytes(bytes([handler_id]) + bytes(BYTES_PER_CHUNK)) # End of audio stream await client_pipeline.send_bytes(bytes([handler_id])) @@ -2557,7 +2577,7 @@ async def test_stt_cooldown_same_id( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "wake_word_phrase": "ok_nabu", }, } @@ -2569,7 +2589,7 @@ async def test_stt_cooldown_same_id( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "wake_word_phrase": "ok_nabu", }, } @@ -2628,7 +2648,7 @@ async def test_stt_cooldown_different_ids( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "wake_word_phrase": "ok_nabu", }, } @@ -2640,7 +2660,7 @@ async def test_stt_cooldown_different_ids( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "wake_word_phrase": "hey_jarvis", }, } From d87366b1e7154637811091de4c2523ad730aff3a Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 2 Aug 2024 06:22:36 -0400 Subject: [PATCH 1907/2411] Make ZHA load quirks earlier (#123027) --- homeassistant/components/zha/__init__.py | 4 +++- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zha/test_repairs.py | 10 +++++----- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 216261e3011..fc573b19ab1 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -117,6 +117,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ha_zha_data.config_entry = config_entry zha_lib_data: ZHAData = create_zha_config(hass, ha_zha_data) + zha_gateway = await Gateway.async_from_config(zha_lib_data) + # Load and cache device trigger information early device_registry = dr.async_get(hass) radio_mgr = ZhaRadioManager.from_config_entry(hass, config_entry) @@ -140,7 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b _LOGGER.debug("Trigger cache: %s", zha_lib_data.device_trigger_cache) try: - zha_gateway = await Gateway.async_from_config(zha_lib_data) + await zha_gateway.async_initialize() except NetworkSettingsInconsistent as exc: await warn_on_inconsistent_network_settings( hass, diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index d2d328cc84b..6e35339c53f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.24"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.25"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 0bf4b77e9d2..99a237efe02 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2986,7 +2986,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.24 +zha==0.0.25 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9670987b70..4ee6914700f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2360,7 +2360,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.24 +zha==0.0.25 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index 7f9b2b4a016..c2925161748 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -148,7 +148,7 @@ async def test_multipan_firmware_repair( autospec=True, ), patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=RuntimeError(), ), patch( @@ -199,7 +199,7 @@ async def test_multipan_firmware_no_repair_on_probe_failure( autospec=True, ), patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=RuntimeError(), ), ): @@ -236,7 +236,7 @@ async def test_multipan_firmware_retry_on_probe_ezsp( autospec=True, ), patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=RuntimeError(), ), ): @@ -311,7 +311,7 @@ async def test_inconsistent_settings_keep_new( old_state = network_backup with patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=NetworkSettingsInconsistent( message="Network settings are inconsistent", new_state=new_state, @@ -390,7 +390,7 @@ async def test_inconsistent_settings_restore_old( old_state = network_backup with patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=NetworkSettingsInconsistent( message="Network settings are inconsistent", new_state=new_state, From a624ada8d66f15942a6995c4f1f7a9f9699f0f66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Aug 2024 15:37:26 -0500 Subject: [PATCH 1908/2411] Fix doorbird models are missing the schedule API (#123033) * Fix doorbird models are missing the schedule API fixes #122997 * cover --- homeassistant/components/doorbird/device.py | 14 +++++++--- tests/components/doorbird/__init__.py | 29 ++++++++++----------- tests/components/doorbird/conftest.py | 2 ++ tests/components/doorbird/test_init.py | 10 +++++++ 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/doorbird/device.py b/homeassistant/components/doorbird/device.py index 866251f3d28..7cd45487464 100644 --- a/homeassistant/components/doorbird/device.py +++ b/homeassistant/components/doorbird/device.py @@ -5,9 +5,11 @@ from __future__ import annotations from collections import defaultdict from dataclasses import dataclass from functools import cached_property +from http import HTTPStatus import logging from typing import Any +from aiohttp import ClientResponseError from doorbirdpy import ( DoorBird, DoorBirdScheduleEntry, @@ -170,15 +172,21 @@ class ConfiguredDoorBird: ) -> DoorbirdEventConfig: """Get events and unconfigured favorites from http favorites.""" device = self.device - schedule = await device.schedule() + events: list[DoorbirdEvent] = [] + unconfigured_favorites: defaultdict[str, list[str]] = defaultdict(list) + try: + schedule = await device.schedule() + except ClientResponseError as ex: + if ex.status == HTTPStatus.NOT_FOUND: + # D301 models do not support schedules + return DoorbirdEventConfig(events, [], unconfigured_favorites) + raise favorite_input_type = { output.param: entry.input for entry in schedule for output in entry.output if output.event == HTTP_EVENT_TYPE } - events: list[DoorbirdEvent] = [] - unconfigured_favorites: defaultdict[str, list[str]] = defaultdict(list) default_event_types = { self._get_event_name(event): event_type for event, event_type in DEFAULT_EVENT_TYPES diff --git a/tests/components/doorbird/__init__.py b/tests/components/doorbird/__init__.py index 41def92f121..2d517dfcefe 100644 --- a/tests/components/doorbird/__init__.py +++ b/tests/components/doorbird/__init__.py @@ -47,31 +47,30 @@ def get_mock_doorbird_api( info: dict[str, Any] | None = None, info_side_effect: Exception | None = None, schedule: list[DoorBirdScheduleEntry] | None = None, + schedule_side_effect: Exception | None = None, favorites: dict[str, dict[str, Any]] | None = None, favorites_side_effect: Exception | None = None, change_schedule: tuple[bool, int] | None = None, ) -> DoorBird: """Return a mock DoorBirdAPI object with return values.""" doorbirdapi_mock = MagicMock(spec_set=DoorBird) - type(doorbirdapi_mock).info = AsyncMock( - side_effect=info_side_effect, return_value=info + api_mock_type = type(doorbirdapi_mock) + api_mock_type.info = AsyncMock(side_effect=info_side_effect, return_value=info) + api_mock_type.favorites = AsyncMock( + side_effect=favorites_side_effect, return_value=favorites ) - type(doorbirdapi_mock).favorites = AsyncMock( - side_effect=favorites_side_effect, - return_value=favorites, - ) - type(doorbirdapi_mock).change_favorite = AsyncMock(return_value=True) - type(doorbirdapi_mock).change_schedule = AsyncMock( + api_mock_type.change_favorite = AsyncMock(return_value=True) + api_mock_type.change_schedule = AsyncMock( return_value=change_schedule or (True, 200) ) - type(doorbirdapi_mock).schedule = AsyncMock(return_value=schedule) - type(doorbirdapi_mock).energize_relay = AsyncMock(return_value=True) - type(doorbirdapi_mock).turn_light_on = AsyncMock(return_value=True) - type(doorbirdapi_mock).delete_favorite = AsyncMock(return_value=True) - type(doorbirdapi_mock).get_image = AsyncMock(return_value=b"image") - type(doorbirdapi_mock).doorbell_state = AsyncMock( - side_effect=mock_unauthorized_exception() + api_mock_type.schedule = AsyncMock( + return_value=schedule, side_effect=schedule_side_effect ) + api_mock_type.energize_relay = AsyncMock(return_value=True) + api_mock_type.turn_light_on = AsyncMock(return_value=True) + api_mock_type.delete_favorite = AsyncMock(return_value=True) + api_mock_type.get_image = AsyncMock(return_value=b"image") + api_mock_type.doorbell_state = AsyncMock(side_effect=mock_unauthorized_exception()) return doorbirdapi_mock diff --git a/tests/components/doorbird/conftest.py b/tests/components/doorbird/conftest.py index 59ead250293..2e367e4e1d8 100644 --- a/tests/components/doorbird/conftest.py +++ b/tests/components/doorbird/conftest.py @@ -102,6 +102,7 @@ async def doorbird_mocker( info: dict[str, Any] | None = None, info_side_effect: Exception | None = None, schedule: list[DoorBirdScheduleEntry] | None = None, + schedule_side_effect: Exception | None = None, favorites: dict[str, dict[str, Any]] | None = None, favorites_side_effect: Exception | None = None, options: dict[str, Any] | None = None, @@ -118,6 +119,7 @@ async def doorbird_mocker( info=info or doorbird_info, info_side_effect=info_side_effect, schedule=schedule or doorbird_schedule, + schedule_side_effect=schedule_side_effect, favorites=favorites or doorbird_favorites, favorites_side_effect=favorites_side_effect, change_schedule=change_schedule, diff --git a/tests/components/doorbird/test_init.py b/tests/components/doorbird/test_init.py index fb8bad2fb46..31266c4acf0 100644 --- a/tests/components/doorbird/test_init.py +++ b/tests/components/doorbird/test_init.py @@ -56,6 +56,16 @@ async def test_http_favorites_request_fails( assert doorbird_entry.entry.state is ConfigEntryState.SETUP_RETRY +async def test_http_schedule_api_missing( + doorbird_mocker: DoorbirdMockerType, +) -> None: + """Test missing the schedule API is non-fatal as not all models support it.""" + doorbird_entry = await doorbird_mocker( + schedule_side_effect=mock_not_found_exception() + ) + assert doorbird_entry.entry.state is ConfigEntryState.LOADED + + async def test_events_changed( hass: HomeAssistant, doorbird_mocker: DoorbirdMockerType, From b06a5af069cbf13e96e452db23c0966b3b49b193 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 2 Aug 2024 12:53:39 +0200 Subject: [PATCH 1909/2411] Address post-merge reviews for KNX integration (#123038) --- homeassistant/components/knx/__init__.py | 2 +- homeassistant/components/knx/binary_sensor.py | 13 +++---- homeassistant/components/knx/button.py | 14 ++++---- homeassistant/components/knx/climate.py | 14 +++++--- homeassistant/components/knx/cover.py | 13 +++---- homeassistant/components/knx/date.py | 14 +++++--- homeassistant/components/knx/datetime.py | 12 ++++--- homeassistant/components/knx/fan.py | 13 +++---- homeassistant/components/knx/knx_entity.py | 36 ++++++++++++++++--- homeassistant/components/knx/light.py | 30 ++++++++-------- homeassistant/components/knx/notify.py | 14 +++++--- homeassistant/components/knx/number.py | 12 ++++--- homeassistant/components/knx/project.py | 6 ++-- homeassistant/components/knx/scene.py | 13 +++---- homeassistant/components/knx/select.py | 12 ++++--- homeassistant/components/knx/sensor.py | 17 +++++---- .../components/knx/storage/config_store.py | 28 ++++++++------- homeassistant/components/knx/switch.py | 30 ++++++++-------- homeassistant/components/knx/text.py | 12 ++++--- homeassistant/components/knx/time.py | 17 +++++---- homeassistant/components/knx/weather.py | 14 +++++--- 21 files changed, 211 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 709a82b31fd..fd46cad8489 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -302,7 +302,7 @@ class KNXModule: self.entry = entry self.project = KNXProject(hass=hass, entry=entry) - self.config_store = KNXConfigStore(hass=hass, entry=entry) + self.config_store = KNXConfigStore(hass=hass, config_entry=entry) self.xknx = XKNX( connection_config=self.connection_config(), diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 0423c1d7b32..ff15f725fae 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any -from xknx import XKNX from xknx.devices import BinarySensor as XknxBinarySensor from homeassistant import config_entries @@ -23,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ATTR_COUNTER, ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import BinarySensorSchema @@ -34,11 +34,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the KNX binary sensor platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: ConfigType = hass.data[DATA_KNX_CONFIG] async_add_entities( - KNXBinarySensor(xknx, entity_config) + KNXBinarySensor(knx_module, entity_config) for entity_config in config[Platform.BINARY_SENSOR] ) @@ -48,11 +48,12 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): _device: XknxBinarySensor - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX binary sensor.""" super().__init__( + knx_module=knx_module, device=XknxBinarySensor( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address_state=config[BinarySensorSchema.CONF_STATE_ADDRESS], invert=config[BinarySensorSchema.CONF_INVERT], @@ -62,7 +63,7 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): ], context_timeout=config.get(BinarySensorSchema.CONF_CONTEXT_TIMEOUT), reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER), - ) + ), ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_device_class = config.get(CONF_DEVICE_CLASS) diff --git a/homeassistant/components/knx/button.py b/homeassistant/components/knx/button.py index a38d8ad1b6c..2eb68eebe43 100644 --- a/homeassistant/components/knx/button.py +++ b/homeassistant/components/knx/button.py @@ -2,7 +2,6 @@ from __future__ import annotations -from xknx import XKNX from xknx.devices import RawValue as XknxRawValue from homeassistant import config_entries @@ -12,6 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import CONF_PAYLOAD_LENGTH, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity @@ -22,11 +22,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the KNX binary sensor platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: ConfigType = hass.data[DATA_KNX_CONFIG] async_add_entities( - KNXButton(xknx, entity_config) for entity_config in config[Platform.BUTTON] + KNXButton(knx_module, entity_config) + for entity_config in config[Platform.BUTTON] ) @@ -35,15 +36,16 @@ class KNXButton(KnxEntity, ButtonEntity): _device: XknxRawValue - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX button.""" super().__init__( + knx_module=knx_module, device=XknxRawValue( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], payload_length=config[CONF_PAYLOAD_LENGTH], group_address=config[KNX_ADDRESS], - ) + ), ) self._payload = config[CONF_PAYLOAD] self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 26be6a03a79..7470d60ef4b 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -27,6 +27,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, @@ -48,10 +49,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up climate(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.CLIMATE] - async_add_entities(KNXClimate(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXClimate(knx_module, entity_config) for entity_config in config + ) def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate: @@ -137,9 +140,12 @@ class KNXClimate(KnxEntity, ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _enable_turn_on_off_backwards_compatibility = False - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of a KNX climate device.""" - super().__init__(_create_climate(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_climate(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE if self._device.supports_on_off: diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 9d86d6ac272..1962db0ad3f 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Callable from typing import Any -from xknx import XKNX from xknx.devices import Cover as XknxCover from homeassistant import config_entries @@ -26,6 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import CoverSchema @@ -37,10 +37,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up cover(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.COVER] - async_add_entities(KNXCover(xknx, entity_config) for entity_config in config) + async_add_entities(KNXCover(knx_module, entity_config) for entity_config in config) class KNXCover(KnxEntity, CoverEntity): @@ -48,11 +48,12 @@ class KNXCover(KnxEntity, CoverEntity): _device: XknxCover - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize the cover.""" super().__init__( + knx_module=knx_module, device=XknxCover( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS), group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS), @@ -70,7 +71,7 @@ class KNXCover(KnxEntity, CoverEntity): invert_updown=config[CoverSchema.CONF_INVERT_UPDOWN], invert_position=config[CoverSchema.CONF_INVERT_POSITION], invert_angle=config[CoverSchema.CONF_INVERT_ANGLE], - ) + ), ) self._unsubscribe_auto_updater: Callable[[], None] | None = None diff --git a/homeassistant/components/knx/date.py b/homeassistant/components/knx/date.py index 98cd22e0751..80fea63d0a6 100644 --- a/homeassistant/components/knx/date.py +++ b/homeassistant/components/knx/date.py @@ -22,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -39,10 +40,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATE] - async_add_entities(KNXDateEntity(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXDateEntity(knx_module, entity_config) for entity_config in config + ) def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateDevice: @@ -63,9 +66,12 @@ class KNXDateEntity(KnxEntity, DateEntity, RestoreEntity): _device: XknxDateDevice - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX time.""" - super().__init__(_create_xknx_device(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_xknx_device(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/datetime.py b/homeassistant/components/knx/datetime.py index d4a25b522eb..16ccb7474a7 100644 --- a/homeassistant/components/knx/datetime.py +++ b/homeassistant/components/knx/datetime.py @@ -23,6 +23,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -40,11 +41,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATETIME] async_add_entities( - KNXDateTimeEntity(xknx, entity_config) for entity_config in config + KNXDateTimeEntity(knx_module, entity_config) for entity_config in config ) @@ -66,9 +67,12 @@ class KNXDateTimeEntity(KnxEntity, DateTimeEntity, RestoreEntity): _device: XknxDateTimeDevice - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX time.""" - super().__init__(_create_xknx_device(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_xknx_device(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 426a750f766..940e241ccda 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -5,7 +5,6 @@ from __future__ import annotations import math from typing import Any, Final -from xknx import XKNX from xknx.devices import Fan as XknxFan from homeassistant import config_entries @@ -20,6 +19,7 @@ from homeassistant.util.percentage import ( ) from homeassistant.util.scaling import int_states_in_range +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import FanSchema @@ -33,10 +33,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up fan(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.FAN] - async_add_entities(KNXFan(xknx, entity_config) for entity_config in config) + async_add_entities(KNXFan(knx_module, entity_config) for entity_config in config) class KNXFan(KnxEntity, FanEntity): @@ -45,12 +45,13 @@ class KNXFan(KnxEntity, FanEntity): _device: XknxFan _enable_turn_on_off_backwards_compatibility = False - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX fan.""" max_step = config.get(FanSchema.CONF_MAX_STEP) super().__init__( + knx_module=knx_module, device=XknxFan( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address_speed=config.get(KNX_ADDRESS), group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS), @@ -61,7 +62,7 @@ class KNXFan(KnxEntity, FanEntity): FanSchema.CONF_OSCILLATION_STATE_ADDRESS ), max_step=max_step, - ) + ), ) # FanSpeedMode.STEP if max_step is set self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index eebddbb0623..2b8d2e71186 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -2,23 +2,29 @@ from __future__ import annotations -from typing import cast +from typing import TYPE_CHECKING from xknx.devices import Device as XknxDevice +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import KNXModule from .const import DOMAIN +if TYPE_CHECKING: + from . import KNXModule + +SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal.{{}}" + class KnxEntity(Entity): """Representation of a KNX entity.""" _attr_should_poll = False - def __init__(self, device: XknxDevice) -> None: + def __init__(self, knx_module: KNXModule, device: XknxDevice) -> None: """Set up device.""" + self._knx_module = knx_module self._device = device @property @@ -29,8 +35,7 @@ class KnxEntity(Entity): @property def available(self) -> bool: """Return True if entity is available.""" - knx_module = cast(KNXModule, self.hass.data[DOMAIN]) - return knx_module.connected + return self._knx_module.connected async def async_update(self) -> None: """Request a state update from KNX bus.""" @@ -44,8 +49,29 @@ class KnxEntity(Entity): """Store register state change callback and start device object.""" self._device.register_device_updated_cb(self.after_update_callback) self._device.xknx.devices.async_add(self._device) + # super call needed to have methods of mulit-inherited classes called + # eg. for restoring state (like _KNXSwitch) + await super().async_added_to_hass() async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" self._device.unregister_device_updated_cb(self.after_update_callback) self._device.xknx.devices.async_remove(self._device) + + +class KnxUIEntity(KnxEntity): + """Representation of a KNX UI entity.""" + + _attr_unique_id: str + + async def async_added_to_hass(self) -> None: + """Register callbacks when entity added to hass.""" + await super().async_added_to_hass() + self._knx_module.config_store.entities.add(self._attr_unique_id) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_ENTITY_REMOVE.format(self._attr_unique_id), + self.async_remove, + ) + ) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 8ec42f3ee56..1197f09354b 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -27,7 +27,7 @@ import homeassistant.util.color as color_util from . import KNXModule from .const import CONF_SYNC_STATE, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, ColorTempModes -from .knx_entity import KnxEntity +from .knx_entity import KnxEntity, KnxUIEntity from .schema import LightSchema from .storage.const import ( CONF_COLOR_TEMP_MAX, @@ -65,10 +65,10 @@ async def async_setup_entry( knx_module: KNXModule = hass.data[DOMAIN] entities: list[KnxEntity] = [] - if yaml_config := hass.data[DATA_KNX_CONFIG].get(Platform.LIGHT): + if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.LIGHT): entities.extend( - KnxYamlLight(knx_module.xknx, entity_config) - for entity_config in yaml_config + KnxYamlLight(knx_module, entity_config) + for entity_config in yaml_platform_config ) if ui_config := knx_module.config_store.data["entities"].get(Platform.LIGHT): entities.extend( @@ -294,7 +294,7 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight ) -class _KnxLight(KnxEntity, LightEntity): +class _KnxLight(LightEntity): """Representation of a KNX light.""" _attr_max_color_temp_kelvin: int @@ -519,14 +519,17 @@ class _KnxLight(KnxEntity, LightEntity): await self._device.set_off() -class KnxYamlLight(_KnxLight): +class KnxYamlLight(_KnxLight, KnxEntity): """Representation of a KNX light.""" _device: XknxLight - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX light.""" - super().__init__(_create_yaml_light(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_yaml_light(knx_module.xknx, config), + ) self._attr_max_color_temp_kelvin: int = config[LightSchema.CONF_MAX_KELVIN] self._attr_min_color_temp_kelvin: int = config[LightSchema.CONF_MIN_KELVIN] self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) @@ -543,20 +546,21 @@ class KnxYamlLight(_KnxLight): ) -class KnxUiLight(_KnxLight): +class KnxUiLight(_KnxLight, KnxUIEntity): """Representation of a KNX light.""" - _device: XknxLight _attr_has_entity_name = True + _device: XknxLight def __init__( self, knx_module: KNXModule, unique_id: str, config: ConfigType ) -> None: """Initialize of KNX light.""" super().__init__( - _create_ui_light( + knx_module=knx_module, + device=_create_ui_light( knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME] - ) + ), ) self._attr_max_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MAX] self._attr_min_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MIN] @@ -565,5 +569,3 @@ class KnxUiLight(_KnxLight): self._attr_unique_id = unique_id if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO): self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)}) - - knx_module.config_store.entities[unique_id] = self diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 997bdb81057..b349681990c 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity @@ -44,7 +45,7 @@ async def async_get_service( class KNXNotificationService(BaseNotificationService): - """Implement demo notification service.""" + """Implement notification service.""" def __init__(self, devices: list[XknxNotification]) -> None: """Initialize the service.""" @@ -86,10 +87,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up notify(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.NOTIFY] - async_add_entities(KNXNotify(xknx, entity_config) for entity_config in config) + async_add_entities(KNXNotify(knx_module, entity_config) for entity_config in config) def _create_notification_instance(xknx: XKNX, config: ConfigType) -> XknxNotification: @@ -107,9 +108,12 @@ class KNXNotify(KnxEntity, NotifyEntity): _device: XknxNotification - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX notification.""" - super().__init__(_create_notification_instance(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_notification_instance(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index 8a9f1dea87c..3d4af503dff 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -39,10 +40,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up number(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.NUMBER] - async_add_entities(KNXNumber(xknx, entity_config) for entity_config in config) + async_add_entities(KNXNumber(knx_module, entity_config) for entity_config in config) def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue: @@ -62,9 +63,12 @@ class KNXNumber(KnxEntity, RestoreNumber): _device: NumericValue - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX number.""" - super().__init__(_create_numeric_value(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_numeric_value(knx_module.xknx, config), + ) self._attr_native_max_value = config.get( NumberSchema.CONF_MAX, self._device.sensor_value.dpt_class.value_max, diff --git a/homeassistant/components/knx/project.py b/homeassistant/components/knx/project.py index 3b3309dfc7d..b5bafe00724 100644 --- a/homeassistant/components/knx/project.py +++ b/homeassistant/components/knx/project.py @@ -8,9 +8,11 @@ from typing import Final from xknx import XKNX from xknx.dpt import DPTBase +from xknx.telegram.address import DeviceAddressableType from xknxproject import XKNXProj from xknxproject.models import ( Device, + DPTType, GroupAddress as GroupAddressModel, KNXProject as KNXProjectModel, ProjectInfo, @@ -89,7 +91,7 @@ class KNXProject: self.devices = project["devices"] self.info = project["info"] xknx.group_address_dpt.clear() - xknx_ga_dict = {} + xknx_ga_dict: dict[DeviceAddressableType, DPTType] = {} for ga_model in project["group_addresses"].values(): ga_info = _create_group_address_info(ga_model) @@ -97,7 +99,7 @@ class KNXProject: if (dpt_model := ga_model.get("dpt")) is not None: xknx_ga_dict[ga_model["address"]] = dpt_model - xknx.group_address_dpt.set(xknx_ga_dict) # type: ignore[arg-type] + xknx.group_address_dpt.set(xknx_ga_dict) _LOGGER.debug( "Loaded KNX project data with %s group addresses from storage", diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index 342d0f9eb83..fc37f36dd01 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any -from xknx import XKNX from xknx.devices import Scene as XknxScene from homeassistant import config_entries @@ -14,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import SceneSchema @@ -25,10 +25,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up scene(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.SCENE] - async_add_entities(KNXScene(xknx, entity_config) for entity_config in config) + async_add_entities(KNXScene(knx_module, entity_config) for entity_config in config) class KNXScene(KnxEntity, Scene): @@ -36,15 +36,16 @@ class KNXScene(KnxEntity, Scene): _device: XknxScene - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Init KNX scene.""" super().__init__( + knx_module=knx_module, device=XknxScene( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address=config[KNX_ADDRESS], scene_number=config[SceneSchema.CONF_SCENE_NUMBER], - ) + ), ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = ( diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index f338bf9feaf..1b862010c2a 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -20,6 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_PAYLOAD_LENGTH, CONF_RESPOND_TO_READ, @@ -39,10 +40,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up select(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.SELECT] - async_add_entities(KNXSelect(xknx, entity_config) for entity_config in config) + async_add_entities(KNXSelect(knx_module, entity_config) for entity_config in config) def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue: @@ -63,9 +64,12 @@ class KNXSelect(KnxEntity, SelectEntity, RestoreEntity): _device: RawValue - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX select.""" - super().__init__(_create_raw_value(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_raw_value(knx_module.xknx, config), + ) self._option_payloads: dict[str, int] = { option[SelectSchema.CONF_OPTION]: option[CONF_PAYLOAD] for option in config[SelectSchema.CONF_OPTIONS] diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 5a09a921901..ab363e2a35f 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -116,17 +116,17 @@ async def async_setup_entry( ) -> None: """Set up sensor(s) for KNX platform.""" knx_module: KNXModule = hass.data[DOMAIN] - - async_add_entities( + entities: list[SensorEntity] = [] + entities.extend( KNXSystemSensor(knx_module, description) for description in SYSTEM_ENTITY_DESCRIPTIONS ) - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG].get(Platform.SENSOR) if config: - async_add_entities( - KNXSensor(knx_module.xknx, entity_config) for entity_config in config + entities.extend( + KNXSensor(knx_module, entity_config) for entity_config in config ) + async_add_entities(entities) def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor: @@ -146,9 +146,12 @@ class KNXSensor(KnxEntity, SensorEntity): _device: XknxSensor - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of a KNX sensor.""" - super().__init__(_create_sensor(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_sensor(knx_module.xknx, config), + ) if device_class := config.get(CONF_DEVICE_CLASS): self._attr_device_class = device_class else: diff --git a/homeassistant/components/knx/storage/config_store.py b/homeassistant/components/knx/storage/config_store.py index 7ea61e1dd3e..876fe19a4b9 100644 --- a/homeassistant/components/knx/storage/config_store.py +++ b/homeassistant/components/knx/storage/config_store.py @@ -2,21 +2,20 @@ from collections.abc import Callable import logging -from typing import TYPE_CHECKING, Any, Final, TypedDict +from typing import Any, Final, TypedDict from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PLATFORM, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store from homeassistant.util.ulid import ulid_now from ..const import DOMAIN +from ..knx_entity import SIGNAL_ENTITY_REMOVE from .const import CONF_DATA -if TYPE_CHECKING: - from ..knx_entity import KnxEntity - _LOGGER = logging.getLogger(__name__) STORAGE_VERSION: Final = 1 @@ -40,15 +39,16 @@ class KNXConfigStore: def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, + config_entry: ConfigEntry, ) -> None: """Initialize config store.""" self.hass = hass + self.config_entry = config_entry self._store = Store[KNXConfigStoreModel](hass, STORAGE_VERSION, STORAGE_KEY) self.data = KNXConfigStoreModel(entities={}) - # entities and async_add_entity are filled by platform setups - self.entities: dict[str, KnxEntity] = {} # unique_id as key + # entities and async_add_entity are filled by platform / entity setups + self.entities: set[str] = set() # unique_id as values self.async_add_entity: dict[ Platform, Callable[[str, dict[str, Any]], None] ] = {} @@ -108,7 +108,7 @@ class KNXConfigStore: raise ConfigStoreException( f"Entity not found in storage: {entity_id} - {unique_id}" ) - await self.entities.pop(unique_id).async_remove() + async_dispatcher_send(self.hass, SIGNAL_ENTITY_REMOVE.format(unique_id)) self.async_add_entity[platform](unique_id, data) # store data after entity is added to make sure config doesn't raise exceptions self.data["entities"][platform][unique_id] = data @@ -126,7 +126,7 @@ class KNXConfigStore: f"Entity not found in {entry.domain}: {entry.unique_id}" ) from err try: - del self.entities[entry.unique_id] + self.entities.remove(entry.unique_id) except KeyError: _LOGGER.warning("Entity not initialized when deleted: %s", entity_id) entity_registry.async_remove(entity_id) @@ -134,10 +134,14 @@ class KNXConfigStore: def get_entity_entries(self) -> list[er.RegistryEntry]: """Get entity_ids of all configured entities by platform.""" + entity_registry = er.async_get(self.hass) + return [ - entity.registry_entry - for entity in self.entities.values() - if entity.registry_entry is not None + registry_entry + for registry_entry in er.async_entries_for_config_entry( + entity_registry, self.config_entry.entry_id + ) + if registry_entry.unique_id in self.entities ] diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 0a8a1dff964..a5f430e6157 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any -from xknx import XKNX from xknx.devices import Switch as XknxSwitch from homeassistant import config_entries @@ -33,7 +32,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxEntity, KnxUIEntity from .schema import SwitchSchema from .storage.const import ( CONF_DEVICE_INFO, @@ -54,10 +53,10 @@ async def async_setup_entry( knx_module: KNXModule = hass.data[DOMAIN] entities: list[KnxEntity] = [] - if yaml_config := hass.data[DATA_KNX_CONFIG].get(Platform.SWITCH): + if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.SWITCH): entities.extend( - KnxYamlSwitch(knx_module.xknx, entity_config) - for entity_config in yaml_config + KnxYamlSwitch(knx_module, entity_config) + for entity_config in yaml_platform_config ) if ui_config := knx_module.config_store.data["entities"].get(Platform.SWITCH): entities.extend( @@ -75,7 +74,7 @@ async def async_setup_entry( knx_module.config_store.async_add_entity[Platform.SWITCH] = add_new_ui_switch -class _KnxSwitch(KnxEntity, SwitchEntity, RestoreEntity): +class _KnxSwitch(SwitchEntity, RestoreEntity): """Base class for a KNX switch.""" _device: XknxSwitch @@ -103,36 +102,41 @@ class _KnxSwitch(KnxEntity, SwitchEntity, RestoreEntity): await self._device.set_off() -class KnxYamlSwitch(_KnxSwitch): +class KnxYamlSwitch(_KnxSwitch, KnxEntity): """Representation of a KNX switch configured from YAML.""" - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + _device: XknxSwitch + + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX switch.""" super().__init__( + knx_module=knx_module, device=XknxSwitch( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address=config[KNX_ADDRESS], group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), respond_to_read=config[CONF_RESPOND_TO_READ], invert=config[SwitchSchema.CONF_INVERT], - ) + ), ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_unique_id = str(self._device.switch.group_address) -class KnxUiSwitch(_KnxSwitch): +class KnxUiSwitch(_KnxSwitch, KnxUIEntity): """Representation of a KNX switch configured from UI.""" _attr_has_entity_name = True + _device: XknxSwitch def __init__( self, knx_module: KNXModule, unique_id: str, config: dict[str, Any] ) -> None: """Initialize of KNX switch.""" super().__init__( + knx_module=knx_module, device=XknxSwitch( knx_module.xknx, name=config[CONF_ENTITY][CONF_NAME], @@ -144,11 +148,9 @@ class KnxUiSwitch(_KnxSwitch): respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ], sync_state=config[DOMAIN][CONF_SYNC_STATE], invert=config[DOMAIN][CONF_INVERT], - ) + ), ) self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY] self._attr_unique_id = unique_id if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO): self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)}) - - knx_module.config_store.entities[unique_id] = self diff --git a/homeassistant/components/knx/text.py b/homeassistant/components/knx/text.py index 22d008cd5ce..9bca37434ac 100644 --- a/homeassistant/components/knx/text.py +++ b/homeassistant/components/knx/text.py @@ -22,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -38,10 +39,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensor(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.TEXT] - async_add_entities(KNXText(xknx, entity_config) for entity_config in config) + async_add_entities(KNXText(knx_module, entity_config) for entity_config in config) def _create_notification(xknx: XKNX, config: ConfigType) -> XknxNotification: @@ -62,9 +63,12 @@ class KNXText(KnxEntity, TextEntity, RestoreEntity): _device: XknxNotification _attr_native_max = 14 - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX text.""" - super().__init__(_create_notification(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_notification(knx_module.xknx, config), + ) self._attr_mode = config[CONF_MODE] self._attr_pattern = ( r"[\u0000-\u00ff]*" # Latin-1 diff --git a/homeassistant/components/knx/time.py b/homeassistant/components/knx/time.py index 28e1419233c..5d9225a1e41 100644 --- a/homeassistant/components/knx/time.py +++ b/homeassistant/components/knx/time.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import time as dt_time -from typing import Final from xknx import XKNX from xknx.devices import TimeDevice as XknxTimeDevice @@ -23,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -33,8 +33,6 @@ from .const import ( ) from .knx_entity import KnxEntity -_TIME_TRANSLATION_FORMAT: Final = "%H:%M:%S" - async def async_setup_entry( hass: HomeAssistant, @@ -42,10 +40,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.TIME] - async_add_entities(KNXTimeEntity(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXTimeEntity(knx_module, entity_config) for entity_config in config + ) def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxTimeDevice: @@ -66,9 +66,12 @@ class KNXTimeEntity(KnxEntity, TimeEntity, RestoreEntity): _device: XknxTimeDevice - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX time.""" - super().__init__(_create_xknx_device(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_xknx_device(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 584c9fd3323..11dae452e2f 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -19,6 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import WeatherSchema @@ -30,10 +31,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up switch(es) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.WEATHER] - async_add_entities(KNXWeather(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXWeather(knx_module, entity_config) for entity_config in config + ) def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather: @@ -80,9 +83,12 @@ class KNXWeather(KnxEntity, WeatherEntity): _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of a KNX sensor.""" - super().__init__(_create_weather(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_weather(knx_module.xknx, config), + ) self._attr_unique_id = str(self._device._temperature.group_address_state) # noqa: SLF001 self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) From dcae2f35ce917516d0abb8c8d45bb6931a700c37 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 2 Aug 2024 08:50:19 +0200 Subject: [PATCH 1910/2411] Mitigate breaking change for KNX climate schema (#123043) --- homeassistant/components/knx/schema.py | 7 +++-- homeassistant/components/knx/validation.py | 34 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 43037ad8188..c31b3d30ad0 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -56,6 +56,7 @@ from .const import ( ColorTempModes, ) from .validation import ( + backwards_compatible_xknx_climate_enum_member, dpt_base_type_validator, ga_list_validator, ga_validator, @@ -409,10 +410,12 @@ class ClimateSchema(KNXPlatformSchema): CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT ): cv.boolean, vol.Optional(CONF_OPERATION_MODES): vol.All( - cv.ensure_list, [vol.All(vol.Upper, cv.enum(HVACOperationMode))] + cv.ensure_list, + [backwards_compatible_xknx_climate_enum_member(HVACOperationMode)], ), vol.Optional(CONF_CONTROLLER_MODES): vol.All( - cv.ensure_list, [vol.All(vol.Upper, cv.enum(HVACControllerMode))] + cv.ensure_list, + [backwards_compatible_xknx_climate_enum_member(HVACControllerMode)], ), vol.Optional( CONF_DEFAULT_CONTROLLER_MODE, default=HVACMode.HEAT diff --git a/homeassistant/components/knx/validation.py b/homeassistant/components/knx/validation.py index 422b8474fd9..0283b65f899 100644 --- a/homeassistant/components/knx/validation.py +++ b/homeassistant/components/knx/validation.py @@ -1,6 +1,7 @@ """Validation helpers for KNX config schemas.""" from collections.abc import Callable +from enum import Enum import ipaddress from typing import Any @@ -104,3 +105,36 @@ sync_state_validator = vol.Any( cv.boolean, cv.matches_regex(r"^(init|expire|every)( \d*)?$"), ) + + +def backwards_compatible_xknx_climate_enum_member(enumClass: type[Enum]) -> vol.All: + """Transform a string to an enum member. + + Backwards compatible with member names of xknx 2.x climate DPT Enums + due to unintentional breaking change in HA 2024.8. + """ + + def _string_transform(value: Any) -> str: + """Upper and slugify string and substitute old member names. + + Previously this was checked against Enum values instead of names. These + looked like `FAN_ONLY = "Fan only"`, therefore the upper & replace part. + """ + if not isinstance(value, str): + raise vol.Invalid("value should be a string") + name = value.upper().replace(" ", "_") + match name: + case "NIGHT": + return "ECONOMY" + case "FROST_PROTECTION": + return "BUILDING_PROTECTION" + case "DRY": + return "DEHUMIDIFICATION" + case _: + return name + + return vol.All( + _string_transform, + vol.In(enumClass.__members__), + enumClass.__getitem__, + ) From bb597a908d4b4962175fbf4b5051e677e1da2031 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 2 Aug 2024 08:48:41 +0200 Subject: [PATCH 1911/2411] Use freezer in KNX tests (#123044) use freezer in tests --- tests/components/knx/test_binary_sensor.py | 27 ++++++++++++------- tests/components/knx/test_button.py | 9 ++++--- tests/components/knx/test_expose.py | 13 ++++----- tests/components/knx/test_interface_device.py | 10 ++++--- tests/components/knx/test_light.py | 11 ++++---- 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index 1b304293a86..dbb8d2ee832 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta +from freezegun.api import FrozenDateTimeFactory + from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE from homeassistant.components.knx.schema import BinarySensorSchema from homeassistant.const import ( @@ -13,7 +15,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util from .conftest import KNXTestKit @@ -141,9 +142,12 @@ async def test_binary_sensor_ignore_internal_state( assert len(events) == 6 -async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_binary_sensor_counter( + hass: HomeAssistant, + knx: KNXTestKit, + freezer: FrozenDateTimeFactory, +) -> None: """Test KNX binary_sensor with context timeout.""" - async_fire_time_changed(hass, dt_util.utcnow()) context_timeout = 1 await knx.setup_integration( @@ -167,7 +171,8 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> No state = hass.states.get("binary_sensor.test") assert state.state is STATE_OFF assert state.attributes.get("counter") == 0 - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=context_timeout)) + freezer.tick(timedelta(seconds=context_timeout)) + async_fire_time_changed(hass) await knx.xknx.task_registry.block_till_done() # state changed twice after context timeout - once to ON with counter 1 and once to counter 0 state = hass.states.get("binary_sensor.test") @@ -188,7 +193,8 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> No state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON assert state.attributes.get("counter") == 0 - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=context_timeout)) + freezer.tick(timedelta(seconds=context_timeout)) + async_fire_time_changed(hass) await knx.xknx.task_registry.block_till_done() state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON @@ -202,10 +208,12 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> No assert event.get("old_state").attributes.get("counter") == 2 -async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_binary_sensor_reset( + hass: HomeAssistant, + knx: KNXTestKit, + freezer: FrozenDateTimeFactory, +) -> None: """Test KNX binary_sensor with reset_after function.""" - async_fire_time_changed(hass, dt_util.utcnow()) - await knx.setup_integration( { BinarySensorSchema.PLATFORM: [ @@ -223,7 +231,8 @@ async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit) -> None await knx.receive_write("2/2/2", True) state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) await hass.async_block_till_done() # state reset after after timeout state = hass.states.get("binary_sensor.test") diff --git a/tests/components/knx/test_button.py b/tests/components/knx/test_button.py index 613208d5595..a05752eced1 100644 --- a/tests/components/knx/test_button.py +++ b/tests/components/knx/test_button.py @@ -3,20 +3,22 @@ from datetime import timedelta import logging +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.knx.const import CONF_PAYLOAD_LENGTH, DOMAIN, KNX_ADDRESS from homeassistant.components.knx.schema import ButtonSchema from homeassistant.const import CONF_NAME, CONF_PAYLOAD, CONF_TYPE from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from .conftest import KNXTestKit from tests.common import async_capture_events, async_fire_time_changed -async def test_button_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_button_simple( + hass: HomeAssistant, knx: KNXTestKit, freezer: FrozenDateTimeFactory +) -> None: """Test KNX button with default payload.""" await knx.setup_integration( { @@ -38,7 +40,8 @@ async def test_button_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: # received telegrams on button GA are ignored by the entity old_state = hass.states.get("button.test") - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3)) + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) await knx.receive_write("1/2/3", False) await knx.receive_write("1/2/3", True) new_state = hass.states.get("button.test") diff --git a/tests/components/knx/test_expose.py b/tests/components/knx/test_expose.py index 96b00241ab6..c4d0acf0ce2 100644 --- a/tests/components/knx/test_expose.py +++ b/tests/components/knx/test_expose.py @@ -3,6 +3,7 @@ from datetime import timedelta from freezegun import freeze_time +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.knx import CONF_KNX_EXPOSE, DOMAIN, KNX_ADDRESS @@ -14,11 +15,10 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from .conftest import KNXTestKit -from tests.common import async_fire_time_changed_exact +from tests.common import async_fire_time_changed async def test_binary_expose(hass: HomeAssistant, knx: KNXTestKit) -> None: @@ -206,7 +206,9 @@ async def test_expose_string(hass: HomeAssistant, knx: KNXTestKit) -> None: ) -async def test_expose_cooldown(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_expose_cooldown( + hass: HomeAssistant, knx: KNXTestKit, freezer: FrozenDateTimeFactory +) -> None: """Test an expose with cooldown.""" cooldown_time = 2 entity_id = "fake.entity" @@ -234,9 +236,8 @@ async def test_expose_cooldown(hass: HomeAssistant, knx: KNXTestKit) -> None: await hass.async_block_till_done() await knx.assert_no_telegram() # Wait for cooldown to pass - async_fire_time_changed_exact( - hass, dt_util.utcnow() + timedelta(seconds=cooldown_time) - ) + freezer.tick(timedelta(seconds=cooldown_time)) + async_fire_time_changed(hass) await hass.async_block_till_done() await knx.assert_write("1/1/8", (3,)) diff --git a/tests/components/knx/test_interface_device.py b/tests/components/knx/test_interface_device.py index 8010496ef0d..79114d4ffd5 100644 --- a/tests/components/knx/test_interface_device.py +++ b/tests/components/knx/test_interface_device.py @@ -2,6 +2,7 @@ from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory from xknx.core import XknxConnectionState, XknxConnectionType from xknx.telegram import IndividualAddress @@ -10,7 +11,6 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from homeassistant.util import dt as dt_util from .conftest import KNXTestKit @@ -19,7 +19,10 @@ from tests.typing import WebSocketGenerator async def test_diagnostic_entities( - hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry + hass: HomeAssistant, + knx: KNXTestKit, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test diagnostic entities.""" await knx.setup_integration({}) @@ -50,7 +53,8 @@ async def test_diagnostic_entities( knx.xknx.connection_manager.cemi_count_outgoing_error = 2 events = async_capture_events(hass, "state_changed") - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(events) == 3 # 5 polled sensors - 2 disabled diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index 8c966a77a0b..04f849bb555 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta +from freezegun.api import FrozenDateTimeFactory from xknx.core import XknxConnectionState from xknx.devices.light import Light as XknxLight @@ -19,7 +20,6 @@ from homeassistant.components.light import ( ) from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from . import KnxEntityGenerator from .conftest import KNXTestKit @@ -643,7 +643,9 @@ async def test_light_rgb_individual(hass: HomeAssistant, knx: KNXTestKit) -> Non await knx.assert_write(test_blue, (45,)) -async def test_light_rgbw_individual(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_light_rgbw_individual( + hass: HomeAssistant, knx: KNXTestKit, freezer: FrozenDateTimeFactory +) -> None: """Test KNX light with rgbw color in individual GAs.""" test_red = "1/1/3" test_red_state = "1/1/4" @@ -763,9 +765,8 @@ async def test_light_rgbw_individual(hass: HomeAssistant, knx: KNXTestKit) -> No await knx.receive_write(test_green, (0,)) # # individual color debounce takes 0.2 seconds if not all 4 addresses received knx.assert_state("light.test", STATE_ON) - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=XknxLight.DEBOUNCE_TIMEOUT) - ) + freezer.tick(timedelta(seconds=XknxLight.DEBOUNCE_TIMEOUT)) + async_fire_time_changed(hass) await knx.xknx.task_registry.block_till_done() knx.assert_state("light.test", STATE_OFF) # turn ON from KNX From abeba39842d51bb5f66fafc1d11b9b008907ca0c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Aug 2024 12:08:44 +0200 Subject: [PATCH 1912/2411] OpenAI make supported features reflect the config entry options (#123047) --- .../openai_conversation/conversation.py | 15 +++++++++++++++ .../openai_conversation/test_conversation.py | 2 ++ 2 files changed, 17 insertions(+) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 483b37945d6..b482126e27c 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -23,6 +23,7 @@ from voluptuous_openapi import convert from homeassistant.components import assist_pipeline, conversation from homeassistant.components.conversation import trace +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, TemplateError @@ -109,6 +110,9 @@ class OpenAIConversationEntity( self.hass, "conversation", self.entry.entry_id, self.entity_id ) conversation.async_set_agent(self.hass, self.entry, self) + self.entry.async_on_unload( + self.entry.add_update_listener(self._async_entry_update_listener) + ) async def async_will_remove_from_hass(self) -> None: """When entity will be removed from Home Assistant.""" @@ -319,3 +323,14 @@ class OpenAIConversationEntity( return conversation.ConversationResult( response=intent_response, conversation_id=conversation_id ) + + async def _async_entry_update_listener( + self, hass: HomeAssistant, entry: ConfigEntry + ) -> None: + """Handle options update.""" + if entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) + else: + self._attr_supported_features = conversation.ConversationEntityFeature(0) diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index 3364d822245..e0665bc449f 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -521,6 +521,8 @@ async def test_unknown_hass_api( }, ) + await hass.async_block_till_done() + result = await conversation.async_converse( hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id ) From d1411220082fcd046423be8241bad7ad6fb7ad55 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Aug 2024 12:31:31 +0200 Subject: [PATCH 1913/2411] Ollama implement CONTROL supported feature (#123049) --- .../components/ollama/conversation.py | 18 +++++++++++++ tests/components/ollama/test_conversation.py | 25 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ollama/conversation.py b/homeassistant/components/ollama/conversation.py index f59e268394b..9f66083f506 100644 --- a/homeassistant/components/ollama/conversation.py +++ b/homeassistant/components/ollama/conversation.py @@ -106,6 +106,10 @@ class OllamaConversationEntity( self._history: dict[str, MessageHistory] = {} self._attr_name = entry.title self._attr_unique_id = entry.entry_id + if self.entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) async def async_added_to_hass(self) -> None: """When entity is added to Home Assistant.""" @@ -114,6 +118,9 @@ class OllamaConversationEntity( self.hass, "conversation", self.entry.entry_id, self.entity_id ) conversation.async_set_agent(self.hass, self.entry, self) + self.entry.async_on_unload( + self.entry.add_update_listener(self._async_entry_update_listener) + ) async def async_will_remove_from_hass(self) -> None: """When entity will be removed from Home Assistant.""" @@ -334,3 +341,14 @@ class OllamaConversationEntity( message_history.messages = [ message_history.messages[0] ] + message_history.messages[drop_index:] + + async def _async_entry_update_listener( + self, hass: HomeAssistant, entry: ConfigEntry + ) -> None: + """Handle options update.""" + if entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) + else: + self._attr_supported_features = conversation.ConversationEntityFeature(0) diff --git a/tests/components/ollama/test_conversation.py b/tests/components/ollama/test_conversation.py index b5a94cc6f57..c83dce3b565 100644 --- a/tests/components/ollama/test_conversation.py +++ b/tests/components/ollama/test_conversation.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import conversation, ollama from homeassistant.components.conversation import trace -from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL +from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_LLM_HASS_API, MATCH_ALL from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import intent, llm @@ -554,3 +554,26 @@ async def test_conversation_agent( mock_config_entry.entry_id ) assert agent.supported_languages == MATCH_ALL + + state = hass.states.get("conversation.mock_title") + assert state + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + +async def test_conversation_agent_with_assist( + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_init_component, +) -> None: + """Test OllamaConversationEntity.""" + agent = conversation.get_agent_manager(hass).async_get_agent( + mock_config_entry_with_assist.entry_id + ) + assert agent.supported_languages == MATCH_ALL + + state = hass.states.get("conversation.mock_title") + assert state + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == conversation.ConversationEntityFeature.CONTROL + ) From c1043ada22a51246ba6f6644b7cc74cdf32820a9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Aug 2024 11:58:07 +0200 Subject: [PATCH 1914/2411] Correct type annotation for `EntityPlatform.async_register_entity_service` (#123054) Correct type annotation for EntityPlatform.async_register_entity_service Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/helpers/entity_platform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index d868e582f8f..6774780f00f 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -985,7 +985,7 @@ class EntityPlatform: def async_register_entity_service( self, name: str, - schema: VolDictType | VolSchemaType | None, + schema: VolDictType | VolSchemaType, func: str | Callable[..., Any], required_features: Iterable[int] | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, From 15ad6db1a72455a94958e13eef0ccd7484565855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=2E=20=C3=81rkosi=20R=C3=B3bert?= Date: Fri, 2 Aug 2024 14:25:43 +0200 Subject: [PATCH 1915/2411] Add LinkPlay models (#123056) * Add some LinkPlay models * Update utils.py * Update utils.py * Update utils.py * Update homeassistant/components/linkplay/utils.py * Update homeassistant/components/linkplay/utils.py * Update utils.py --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/linkplay/utils.py | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/homeassistant/components/linkplay/utils.py b/homeassistant/components/linkplay/utils.py index 9ca76b3933d..7532c9b354a 100644 --- a/homeassistant/components/linkplay/utils.py +++ b/homeassistant/components/linkplay/utils.py @@ -3,9 +3,19 @@ from typing import Final MANUFACTURER_ARTSOUND: Final[str] = "ArtSound" +MANUFACTURER_ARYLIC: Final[str] = "Arylic" +MANUFACTURER_IEAST: Final[str] = "iEAST" MANUFACTURER_GENERIC: Final[str] = "Generic" MODELS_ARTSOUND_SMART_ZONE4: Final[str] = "Smart Zone 4 AMP" MODELS_ARTSOUND_SMART_HYDE: Final[str] = "Smart Hyde" +MODELS_ARYLIC_S50: Final[str] = "S50+" +MODELS_ARYLIC_S50_PRO: Final[str] = "S50 Pro" +MODELS_ARYLIC_A30: Final[str] = "A30" +MODELS_ARYLIC_A50S: Final[str] = "A50+" +MODELS_ARYLIC_UP2STREAM_AMP_V3: Final[str] = "Up2Stream Amp v3" +MODELS_ARYLIC_UP2STREAM_AMP_V4: Final[str] = "Up2Stream Amp v4" +MODELS_ARYLIC_UP2STREAM_PRO_V3: Final[str] = "Up2Stream Pro v3" +MODELS_IEAST_AUDIOCAST_M5: Final[str] = "AudioCast M5" MODELS_GENERIC: Final[str] = "Generic" @@ -16,5 +26,21 @@ def get_info_from_project(project: str) -> tuple[str, str]: return MANUFACTURER_ARTSOUND, MODELS_ARTSOUND_SMART_ZONE4 case "SMART_HYDE": return MANUFACTURER_ARTSOUND, MODELS_ARTSOUND_SMART_HYDE + case "ARYLIC_S50": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_S50 + case "RP0016_S50PRO_S": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_S50_PRO + case "RP0011_WB60_S": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_A30 + case "ARYLIC_A50S": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_A50S + case "UP2STREAM_AMP_V3": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_UP2STREAM_AMP_V3 + case "UP2STREAM_AMP_V4": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_UP2STREAM_AMP_V4 + case "UP2STREAM_PRO_V3": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_UP2STREAM_PRO_V3 + case "iEAST-02": + return MANUFACTURER_IEAST, MODELS_IEAST_AUDIOCAST_M5 case _: return MANUFACTURER_GENERIC, MODELS_GENERIC From f9276e28b03bed2e88d3653cec98cf2a26686792 Mon Sep 17 00:00:00 2001 From: Fabian <115155196+Fabiann2205@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:19:55 +0200 Subject: [PATCH 1916/2411] Add device class (#123059) --- homeassistant/components/google_travel_time/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 6c45033eeb7..618dda50bd4 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -8,7 +8,11 @@ import logging from googlemaps import Client from googlemaps.distance_matrix import distance_matrix -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, @@ -72,6 +76,8 @@ class GoogleTravelTimeSensor(SensorEntity): _attr_attribution = ATTRIBUTION _attr_native_unit_of_measurement = UnitOfTime.MINUTES + _attr_device_class = SensorDeviceClass.DURATION + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, config_entry, name, api_key, origin, destination, client): """Initialize the sensor.""" From d7cc2a7e9a3df922e64d78ef5487e41f74b9e508 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Aug 2024 11:49:47 +0200 Subject: [PATCH 1917/2411] Correct squeezebox service (#123060) --- homeassistant/components/squeezebox/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index aaf64c34ddf..c0a6dad7a47 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -185,7 +185,7 @@ async def async_setup_entry( {vol.Required(ATTR_OTHER_PLAYER): cv.string}, "async_sync", ) - platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync") + platform.async_register_entity_service(SERVICE_UNSYNC, {}, "async_unsync") # Start server discovery task if not already running entry.async_on_unload(async_at_start(hass, start_server_discovery)) From 9c7134a86513e2f17b46767e01dbeff596e40125 Mon Sep 17 00:00:00 2001 From: Philip Vanloo <26272906+dukeofphilberg@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:23:45 +0200 Subject: [PATCH 1918/2411] LinkPlay: Bump python-linkplay to 0.0.6 (#123062) Bump python-linkplay to 0.0.6 --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index 0345d4ad727..9ac2a9e66e6 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/linkplay", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["python-linkplay==0.0.5"], + "requirements": ["python-linkplay==0.0.6"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 99a237efe02..747f6509604 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2298,7 +2298,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.1 # homeassistant.components.linkplay -python-linkplay==0.0.5 +python-linkplay==0.0.6 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ee6914700f..e85a748d13e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1816,7 +1816,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.1 # homeassistant.components.linkplay -python-linkplay==0.0.5 +python-linkplay==0.0.6 # homeassistant.components.matter python-matter-server==6.3.0 From 13c9d69440813adce42ad1e5a0ac791e7e5c9f7c Mon Sep 17 00:00:00 2001 From: Philip Vanloo <26272906+dukeofphilberg@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:38:05 +0200 Subject: [PATCH 1919/2411] Add additional items to REPEAT_MAP in LinkPlay (#123063) * Upgrade python-linkplay, add items to REPEAT_MAP * Undo dependency bump --- homeassistant/components/linkplay/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py index 103b09f46da..398add235bd 100644 --- a/homeassistant/components/linkplay/media_player.py +++ b/homeassistant/components/linkplay/media_player.py @@ -58,6 +58,8 @@ REPEAT_MAP: dict[LoopMode, RepeatMode] = { LoopMode.CONTINUOUS_PLAYBACK: RepeatMode.ALL, LoopMode.RANDOM_PLAYBACK: RepeatMode.ALL, LoopMode.LIST_CYCLE: RepeatMode.ALL, + LoopMode.SHUFF_DISABLED_REPEAT_DISABLED: RepeatMode.OFF, + LoopMode.SHUFF_ENABLED_REPEAT_ENABLED_LOOP_ONCE: RepeatMode.ALL, } REPEAT_MAP_INV: dict[RepeatMode, LoopMode] = {v: k for k, v in REPEAT_MAP.items()} From b36059fc64c6fd27b33069df234fdea468467573 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 2 Aug 2024 13:38:56 +0200 Subject: [PATCH 1920/2411] Do not raise repair issue about missing integration in safe mode (#123066) --- homeassistant/setup.py | 27 ++++++++++++++------------- tests/test_setup.py | 11 ++++++++++- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 12dd17b289c..102c48e1d07 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -281,19 +281,20 @@ async def _async_setup_component( integration = await loader.async_get_integration(hass, domain) except loader.IntegrationNotFound: _log_error_setup_error(hass, domain, None, "Integration not found.") - ir.async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"integration_not_found.{domain}", - is_fixable=True, - issue_domain=HOMEASSISTANT_DOMAIN, - severity=IssueSeverity.ERROR, - translation_key="integration_not_found", - translation_placeholders={ - "domain": domain, - }, - data={"domain": domain}, - ) + if not hass.config.safe_mode: + ir.async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"integration_not_found.{domain}", + is_fixable=True, + issue_domain=HOMEASSISTANT_DOMAIN, + severity=IssueSeverity.ERROR, + translation_key="integration_not_found", + translation_placeholders={ + "domain": domain, + }, + data={"domain": domain}, + ) return False log_error = partial(_log_error_setup_error, hass, domain, integration) diff --git a/tests/test_setup.py b/tests/test_setup.py index 3430c17960c..4e7c23865da 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -245,7 +245,7 @@ async def test_validate_platform_config_4(hass: HomeAssistant) -> None: async def test_component_not_found( hass: HomeAssistant, issue_registry: IssueRegistry ) -> None: - """setup_component should not crash if component doesn't exist.""" + """setup_component should raise a repair issue if component doesn't exist.""" assert await setup.async_setup_component(hass, "non_existing", {}) is False assert len(issue_registry.issues) == 1 issue = issue_registry.async_get_issue( @@ -255,6 +255,15 @@ async def test_component_not_found( assert issue.translation_key == "integration_not_found" +async def test_component_missing_not_raising_in_safe_mode( + hass: HomeAssistant, issue_registry: IssueRegistry +) -> None: + """setup_component should not raise an issue if component doesn't exist in safe.""" + hass.config.safe_mode = True + assert await setup.async_setup_component(hass, "non_existing", {}) is False + assert len(issue_registry.issues) == 0 + + async def test_component_not_double_initialized(hass: HomeAssistant) -> None: """Test we do not set up a component twice.""" mock_setup = Mock(return_value=True) From 433c1a57e76b9206e4f201c4b9e42fbc9eccba25 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 Aug 2024 16:48:37 +0200 Subject: [PATCH 1921/2411] Update frontend to 20240802.0 (#123072) --- 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 60cfa0a26ff..95afe1221ec 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240731.0"] + "requirements": ["home-assistant-frontend==20240802.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 58f39907269..1cc6a0fa85d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240731.0 +home-assistant-frontend==20240802.0 home-assistant-intents==2024.7.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 747f6509604..479e22a3bfc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240731.0 +home-assistant-frontend==20240802.0 # homeassistant.components.conversation home-assistant-intents==2024.7.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e85a748d13e..9d92bde7aa8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -916,7 +916,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240731.0 +home-assistant-frontend==20240802.0 # homeassistant.components.conversation home-assistant-intents==2024.7.29 From fe82e7f24d1362a4a657c9561fd98880d0236ace Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Aug 2024 17:46:01 +0200 Subject: [PATCH 1922/2411] Bump version to 2024.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 b315d8c2618..08ee0bb77f9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index e705101e4ae..c60a01663da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b0" +version = "2024.8.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e32a48ac55bc8fb1b056e6d0a1f9e54bbd7853fa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 2 Aug 2024 17:55:46 +0200 Subject: [PATCH 1923/2411] Improve type hints in google_assistant (#122895) --- .../components/google_assistant/trait.py | 120 +++++++++--------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 2be20237a84..145eb4b2935 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -294,7 +294,7 @@ class _Trait(ABC): self.state = state self.config = config - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return attributes for a sync request.""" raise NotImplementedError @@ -302,7 +302,7 @@ class _Trait(ABC): """Add options for the sync request.""" return {} - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return the attributes of this trait for this entity.""" raise NotImplementedError @@ -337,11 +337,11 @@ class BrightnessTrait(_Trait): return False - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return brightness attributes for a sync request.""" return {} - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return brightness query attributes.""" domain = self.state.domain response = {} @@ -388,7 +388,7 @@ class CameraStreamTrait(_Trait): return False - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return stream attributes for a sync request.""" return { "cameraStreamSupportedProtocols": ["hls"], @@ -396,7 +396,7 @@ class CameraStreamTrait(_Trait): "cameraStreamNeedDrmEncryption": False, } - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return camera stream attributes.""" return self.stream_info or {} @@ -426,7 +426,7 @@ class ObjectDetection(_Trait): domain == event.DOMAIN and device_class == event.EventDeviceClass.DOORBELL ) - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return ObjectDetection attributes for a sync request.""" return {} @@ -434,7 +434,7 @@ class ObjectDetection(_Trait): """Add options for the sync request.""" return {"notificationSupportedByAgent": True} - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return ObjectDetection query attributes.""" return {} @@ -498,13 +498,13 @@ class OnOffTrait(_Trait): humidifier.DOMAIN, ) - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return OnOff attributes for a sync request.""" if self.state.attributes.get(ATTR_ASSUMED_STATE, False): return {"commandOnlyOnOff": True} return {} - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return OnOff query attributes.""" return {"on": self.state.state not in (STATE_OFF, STATE_UNKNOWN)} @@ -548,11 +548,11 @@ class ColorSettingTrait(_Trait): color_modes ) - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return color temperature attributes for a sync request.""" attrs = self.state.attributes color_modes = attrs.get(light.ATTR_SUPPORTED_COLOR_MODES) - response = {} + response: dict[str, Any] = {} if light.color_supported(color_modes): response["colorModel"] = "hsv" @@ -571,11 +571,11 @@ class ColorSettingTrait(_Trait): return response - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return color temperature query attributes.""" color_mode = self.state.attributes.get(light.ATTR_COLOR_MODE) - color = {} + color: dict[str, Any] = {} if light.color_supported([color_mode]): color_hs = self.state.attributes.get(light.ATTR_HS_COLOR) @@ -684,12 +684,12 @@ class SceneTrait(_Trait): script.DOMAIN, ) - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return scene attributes for a sync request.""" # None of the supported domains can support sceneReversible return {} - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return scene query attributes.""" return {} @@ -728,11 +728,11 @@ class DockTrait(_Trait): """Test if state is supported.""" return domain == vacuum.DOMAIN - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return dock attributes for a sync request.""" return {} - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return dock query attributes.""" return {"isDocked": self.state.state == vacuum.STATE_DOCKED} @@ -762,11 +762,11 @@ class LocatorTrait(_Trait): """Test if state is supported.""" return domain == vacuum.DOMAIN and features & VacuumEntityFeature.LOCATE - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return locator attributes for a sync request.""" return {} - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return locator query attributes.""" return {} @@ -802,14 +802,14 @@ class EnergyStorageTrait(_Trait): """Test if state is supported.""" return domain == vacuum.DOMAIN and features & VacuumEntityFeature.BATTERY - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return EnergyStorage attributes for a sync request.""" return { "isRechargeable": True, "queryOnlyEnergyStorage": True, } - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return EnergyStorage query attributes.""" battery_level = self.state.attributes.get(ATTR_BATTERY_LEVEL) if battery_level is None: @@ -866,7 +866,7 @@ class StartStopTrait(_Trait): return False - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return StartStop attributes for a sync request.""" domain = self.state.domain if domain == vacuum.DOMAIN: @@ -880,7 +880,7 @@ class StartStopTrait(_Trait): raise NotImplementedError(f"Unsupported domain {domain}") - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return StartStop query attributes.""" domain = self.state.domain state = self.state.state @@ -1012,7 +1012,7 @@ class TemperatureControlTrait(_Trait): and device_class == sensor.SensorDeviceClass.TEMPERATURE ) - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return temperature attributes for a sync request.""" response = {} domain = self.state.domain @@ -1048,7 +1048,7 @@ class TemperatureControlTrait(_Trait): return response - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return temperature states.""" response = {} domain = self.state.domain @@ -1174,7 +1174,7 @@ class TemperatureSettingTrait(_Trait): return modes - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return temperature point and modes attributes for a sync request.""" response = {} attrs = self.state.attributes @@ -1217,9 +1217,9 @@ class TemperatureSettingTrait(_Trait): return response - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return temperature point and modes query attributes.""" - response = {} + response: dict[str, Any] = {} attrs = self.state.attributes unit = self.hass.config.units.temperature_unit @@ -1432,9 +1432,9 @@ class HumiditySettingTrait(_Trait): and device_class == sensor.SensorDeviceClass.HUMIDITY ) - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return humidity attributes for a sync request.""" - response = {} + response: dict[str, Any] = {} attrs = self.state.attributes domain = self.state.domain @@ -1455,7 +1455,7 @@ class HumiditySettingTrait(_Trait): return response - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return humidity query attributes.""" response = {} attrs = self.state.attributes @@ -1464,9 +1464,9 @@ class HumiditySettingTrait(_Trait): if domain == sensor.DOMAIN: device_class = attrs.get(ATTR_DEVICE_CLASS) if device_class == sensor.SensorDeviceClass.HUMIDITY: - current_humidity = self.state.state - if current_humidity not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - response["humidityAmbientPercent"] = round(float(current_humidity)) + humidity_state = self.state.state + if humidity_state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + response["humidityAmbientPercent"] = round(float(humidity_state)) elif domain == humidifier.DOMAIN: target_humidity: int | None = attrs.get(humidifier.ATTR_HUMIDITY) @@ -1518,11 +1518,11 @@ class LockUnlockTrait(_Trait): """Return if the trait might ask for 2FA.""" return True - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return LockUnlock attributes for a sync request.""" return {} - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return LockUnlock query attributes.""" if self.state.state == STATE_JAMMED: return {"isJammed": True} @@ -1604,7 +1604,7 @@ class ArmDisArmTrait(_Trait): return states[0] - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return ArmDisarm attributes for a sync request.""" response = {} levels = [] @@ -1624,7 +1624,7 @@ class ArmDisArmTrait(_Trait): response["availableArmLevels"] = {"levels": levels, "ordered": True} return response - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return ArmDisarm query attributes.""" armed_state = self.state.attributes.get("next_state", self.state.state) @@ -1721,11 +1721,11 @@ class FanSpeedTrait(_Trait): return features & ClimateEntityFeature.FAN_MODE return False - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return speed point and modes attributes for a sync request.""" domain = self.state.domain speeds = [] - result = {} + result: dict[str, Any] = {} if domain == fan.DOMAIN: reversible = bool( @@ -1770,7 +1770,7 @@ class FanSpeedTrait(_Trait): return result - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return speed point and modes query attributes.""" attrs = self.state.attributes @@ -1916,7 +1916,7 @@ class ModesTrait(_Trait): ) return mode - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return mode attributes for a sync request.""" modes = [] @@ -1940,10 +1940,10 @@ class ModesTrait(_Trait): return {"availableModes": modes} - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return current modes.""" attrs = self.state.attributes - response = {} + response: dict[str, Any] = {} mode_settings = {} if self.state.domain == fan.DOMAIN: @@ -2104,7 +2104,7 @@ class InputSelectorTrait(_Trait): return False - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return mode attributes for a sync request.""" attrs = self.state.attributes sourcelist: list[str] = attrs.get(media_player.ATTR_INPUT_SOURCE_LIST) or [] @@ -2115,7 +2115,7 @@ class InputSelectorTrait(_Trait): return {"availableInputs": inputs, "orderedInputs": True} - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return current modes.""" attrs = self.state.attributes return {"currentInput": attrs.get(media_player.ATTR_INPUT_SOURCE, "")} @@ -2185,7 +2185,7 @@ class OpenCloseTrait(_Trait): """Return if the trait might ask for 2FA.""" return domain == cover.DOMAIN and device_class in OpenCloseTrait.COVER_2FA - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return opening direction.""" response = {} features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -2221,10 +2221,10 @@ class OpenCloseTrait(_Trait): return response - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return state query attributes.""" domain = self.state.domain - response = {} + response: dict[str, Any] = {} # When it's an assumed state, we will return empty state # This shouldn't happen because we set `commandOnlyOpenClose` @@ -2330,7 +2330,7 @@ class VolumeTrait(_Trait): return False - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return volume attributes for a sync request.""" features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) return { @@ -2347,7 +2347,7 @@ class VolumeTrait(_Trait): "levelStepSize": 10, } - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return volume query attributes.""" response = {} @@ -2510,7 +2510,7 @@ class TransportControlTrait(_Trait): return False - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return opening direction.""" response = {} @@ -2525,7 +2525,7 @@ class TransportControlTrait(_Trait): return response - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return the attributes of this trait for this entity.""" return {} @@ -2624,11 +2624,11 @@ class MediaStateTrait(_Trait): """Test if state is supported.""" return domain == media_player.DOMAIN - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return attributes for a sync request.""" return {"supportActivityState": True, "supportPlaybackState": True} - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return the attributes of this trait for this entity.""" return { "activityState": self.activity_lookup.get(self.state.state, "INACTIVE"), @@ -2658,11 +2658,11 @@ class ChannelTrait(_Trait): return False - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return attributes for a sync request.""" return {"availableChannels": [], "commandOnlyChannels": True} - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return channel query attributes.""" return {} @@ -2735,7 +2735,7 @@ class SensorStateTrait(_Trait): """Test if state is supported.""" return domain == sensor.DOMAIN and device_class in cls.sensor_types - def sync_attributes(self): + def sync_attributes(self) -> dict[str, Any]: """Return attributes for a sync request.""" device_class = self.state.attributes.get(ATTR_DEVICE_CLASS) data = self.sensor_types.get(device_class) @@ -2763,7 +2763,7 @@ class SensorStateTrait(_Trait): return {"sensorStatesSupported": [sensor_state]} - def query_attributes(self): + def query_attributes(self) -> dict[str, Any]: """Return the attributes of this trait for this entity.""" device_class = self.state.attributes.get(ATTR_DEVICE_CLASS) data = self.sensor_types.get(device_class) From 34b561b21131fd5dc8c2c0593795576e82ee7b32 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 2 Aug 2024 19:04:00 +0200 Subject: [PATCH 1924/2411] Bump ruff to 0.5.6 (#123073) --- .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 22e10d420d4..9c75edf780c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.5 + rev: v0.5.6 hooks: - id: ruff args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index d57a005bb5d..a3d38c11a8d 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.3.0 -ruff==0.5.5 +ruff==0.5.6 yamllint==1.35.1 From 8687c32c15fb3a577a993a2e32c0527ed36f838b Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 2 Aug 2024 21:56:49 +0300 Subject: [PATCH 1925/2411] Ignore Shelly IPv6 address in zeroconf (#123081) --- .../components/shelly/config_flow.py | 2 ++ homeassistant/components/shelly/strings.json | 3 ++- tests/components/shelly/test_config_flow.py | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index cb3bca6aa47..c80d1e84d6f 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -279,6 +279,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): self, discovery_info: ZeroconfServiceInfo ) -> ConfigFlowResult: """Handle zeroconf discovery.""" + if discovery_info.ip_address.version == 6: + return self.async_abort(reason="ipv6_not_supported") host = discovery_info.host # First try to get the mac address from the name # so we can avoid making another connection to the diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 8ae4ff1f3e4..f76319eb08c 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -52,7 +52,8 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again.", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", - "another_device": "Re-configuration was unsuccessful, the IP address/hostname of another Shelly device was used." + "another_device": "Re-configuration was unsuccessful, the IP address/hostname of another Shelly device was used.", + "ipv6_not_supported": "IPv6 is not supported." } }, "device_automation": { diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index a3040fc2eb8..0c574a33e0c 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1305,3 +1305,22 @@ async def test_reconfigure_with_exception( ) assert result["errors"] == {"base": base_error} + + +async def test_zeroconf_rejects_ipv6(hass: HomeAssistant) -> None: + """Test zeroconf discovery rejects ipv6.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("fd00::b27c:63bb:cc85:4ea0"), + ip_addresses=[ip_address("fd00::b27c:63bb:cc85:4ea0")], + hostname="mock_hostname", + name="shelly1pm-12345", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "shelly1pm-12345"}, + type="mock_type", + ), + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "ipv6_not_supported" From f6ad018f8fa3ccd4bf31df47388d3c26532ecf6d Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Sat, 3 Aug 2024 09:14:24 +0300 Subject: [PATCH 1926/2411] Change enum type to string for Google Generative AI Conversation (#123069) --- .../conversation.py | 14 ++++- .../test_conversation.py | 59 +++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index a5c911bb757..6b3e87f8181 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -89,9 +89,9 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: key = "type_" val = val.upper() elif key == "format": - if (schema.get("type") == "string" and val != "enum") or ( - schema.get("type") not in ("number", "integer", "string") - ): + if schema.get("type") == "string" and val != "enum": + continue + if schema.get("type") not in ("number", "integer", "string"): continue key = "format_" elif key == "items": @@ -100,11 +100,19 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: val = {k: _format_schema(v) for k, v in val.items()} result[key] = val + if result.get("enum") and result.get("type_") != "STRING": + # enum is only allowed for STRING type. This is safe as long as the schema + # contains vol.Coerce for the respective type, for example: + # vol.All(vol.Coerce(int), vol.In([1, 2, 3])) + result["type_"] = "STRING" + result["enum"] = [str(item) for item in result["enum"]] + if result.get("type_") == "OBJECT" and not result.get("properties"): # An object with undefined properties is not supported by Gemini API. # Fallback to JSON string. This will probably fail for most tools that want it, # but we don't have a better fallback strategy so far. result["properties"] = {"json": {"type_": "STRING"}} + result["required"] = [] return result diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 41f96c7b0ac..df8472995a7 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -17,6 +17,7 @@ from homeassistant.components.google_generative_ai_conversation.const import ( ) from homeassistant.components.google_generative_ai_conversation.conversation import ( _escape_decode, + _format_schema, ) from homeassistant.const import CONF_LLM_HASS_API from homeassistant.core import Context, HomeAssistant @@ -622,3 +623,61 @@ async def test_escape_decode() -> None: "param2": "param2's value", "param3": {"param31": "Cheminée", "param32": "Cheminée"}, } + + +@pytest.mark.parametrize( + ("openapi", "protobuf"), + [ + ( + {"type": "string", "enum": ["a", "b", "c"]}, + {"type_": "STRING", "enum": ["a", "b", "c"]}, + ), + ( + {"type": "integer", "enum": [1, 2, 3]}, + {"type_": "STRING", "enum": ["1", "2", "3"]}, + ), + ({"anyOf": [{"type": "integer"}, {"type": "number"}]}, {"type_": "INTEGER"}), + ( + { + "anyOf": [ + {"anyOf": [{"type": "integer"}, {"type": "number"}]}, + {"anyOf": [{"type": "integer"}, {"type": "number"}]}, + ] + }, + {"type_": "INTEGER"}, + ), + ({"type": "string", "format": "lower"}, {"type_": "STRING"}), + ({"type": "boolean", "format": "bool"}, {"type_": "BOOLEAN"}), + ( + {"type": "number", "format": "percent"}, + {"type_": "NUMBER", "format_": "percent"}, + ), + ( + { + "type": "object", + "properties": {"var": {"type": "string"}}, + "required": [], + }, + { + "type_": "OBJECT", + "properties": {"var": {"type_": "STRING"}}, + "required": [], + }, + ), + ( + {"type": "object", "additionalProperties": True}, + { + "type_": "OBJECT", + "properties": {"json": {"type_": "STRING"}}, + "required": [], + }, + ), + ( + {"type": "array", "items": {"type": "string"}}, + {"type_": "ARRAY", "items": {"type_": "STRING"}}, + ), + ], +) +async def test_format_schema(openapi, protobuf) -> None: + """Test _format_schema.""" + assert _format_schema(openapi) == protobuf From aa6f0cd55af1c7dc64e6e93fc0df3eee8323b5ac Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 3 Aug 2024 08:16:30 +0200 Subject: [PATCH 1927/2411] Add CONTROL supported feature to Google conversation when API access (#123046) * Add CONTROL supported feature to Google conversation when API access * Better function name * Handle entry update inline * Reload instead of update --- .../conversation.py | 14 ++++++++++++++ .../snapshots/test_conversation.ambr | 8 ++++---- .../test_conversation.py | 15 +++++++++++---- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 6b3e87f8181..0d24ddbf39f 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -172,6 +172,10 @@ class GoogleGenerativeAIConversationEntity( model="Generative AI", entry_type=dr.DeviceEntryType.SERVICE, ) + if self.entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) @property def supported_languages(self) -> list[str] | Literal["*"]: @@ -185,6 +189,9 @@ class GoogleGenerativeAIConversationEntity( self.hass, "conversation", self.entry.entry_id, self.entity_id ) conversation.async_set_agent(self.hass, self.entry, self) + self.entry.async_on_unload( + self.entry.add_update_listener(self._async_entry_update_listener) + ) async def async_will_remove_from_hass(self) -> None: """When entity will be removed from Home Assistant.""" @@ -405,3 +412,10 @@ class GoogleGenerativeAIConversationEntity( parts.append(llm_api.api_prompt) return "\n".join(parts) + + async def _async_entry_update_listener( + self, hass: HomeAssistant, entry: ConfigEntry + ) -> None: + """Handle options update.""" + # Reload as we update device info + entity name + supported features + await hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index abd3658e869..65238c5212a 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -215,7 +215,7 @@ ), ]) # --- -# name: test_default_prompt[config_entry_options0-None] +# name: test_default_prompt[config_entry_options0-0-None] list([ tuple( '', @@ -263,7 +263,7 @@ ), ]) # --- -# name: test_default_prompt[config_entry_options0-conversation.google_generative_ai_conversation] +# name: test_default_prompt[config_entry_options0-0-conversation.google_generative_ai_conversation] list([ tuple( '', @@ -311,7 +311,7 @@ ), ]) # --- -# name: test_default_prompt[config_entry_options1-None] +# name: test_default_prompt[config_entry_options1-1-None] list([ tuple( '', @@ -360,7 +360,7 @@ ), ]) # --- -# name: test_default_prompt[config_entry_options1-conversation.google_generative_ai_conversation] +# name: test_default_prompt[config_entry_options1-1-conversation.google_generative_ai_conversation] list([ tuple( '', diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index df8472995a7..a8eae34e08b 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -19,7 +19,7 @@ from homeassistant.components.google_generative_ai_conversation.conversation imp _escape_decode, _format_schema, ) -from homeassistant.const import CONF_LLM_HASS_API +from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_LLM_HASS_API from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import intent, llm @@ -39,10 +39,13 @@ def freeze_the_time(): "agent_id", [None, "conversation.google_generative_ai_conversation"] ) @pytest.mark.parametrize( - "config_entry_options", + ("config_entry_options", "expected_features"), [ - {}, - {CONF_LLM_HASS_API: llm.LLM_API_ASSIST}, + ({}, 0), + ( + {CONF_LLM_HASS_API: llm.LLM_API_ASSIST}, + conversation.ConversationEntityFeature.CONTROL, + ), ], ) @pytest.mark.usefixtures("mock_init_component") @@ -52,6 +55,7 @@ async def test_default_prompt( snapshot: SnapshotAssertion, agent_id: str | None, config_entry_options: {}, + expected_features: conversation.ConversationEntityFeature, hass_ws_client: WebSocketGenerator, ) -> None: """Test that the default prompt works.""" @@ -98,6 +102,9 @@ async def test_default_prompt( assert [tuple(mock_call) for mock_call in mock_model.mock_calls] == snapshot assert mock_get_tools.called == (CONF_LLM_HASS_API in config_entry_options) + state = hass.states.get("conversation.google_generative_ai_conversation") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == expected_features + @pytest.mark.parametrize( ("model_name", "supports_system_instruction"), From 6684f61a54a4fb11ec05451ae4485877a3672cd6 Mon Sep 17 00:00:00 2001 From: Chris Buckley Date: Sat, 3 Aug 2024 08:07:13 +0100 Subject: [PATCH 1928/2411] Add support for Todoist sections (#115671) * Add support for Todoist sections * ServiceValidationError & section name tweaks from PR comments * Remove whitespace Co-authored-by: Erik Montnemery * More natural error message Co-authored-by: Erik Montnemery --------- Co-authored-by: Erik Montnemery --- homeassistant/components/todoist/calendar.py | 31 +++++++++++++++++-- homeassistant/components/todoist/const.py | 2 ++ .../components/todoist/coordinator.py | 6 +++- .../components/todoist/services.yaml | 4 +++ homeassistant/components/todoist/strings.json | 9 ++++++ tests/components/todoist/conftest.py | 11 ++++++- tests/components/todoist/test_calendar.py | 29 ++++++++++++++++- 7 files changed, 86 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 1c6f40005c1..f89c09451b6 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -21,7 +21,7 @@ from homeassistant.components.calendar import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, CONF_NAME, CONF_TOKEN, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -54,6 +54,7 @@ from .const import ( REMINDER_DATE, REMINDER_DATE_LANG, REMINDER_DATE_STRING, + SECTION_NAME, SERVICE_NEW_TASK, START, SUMMARY, @@ -68,6 +69,7 @@ NEW_TASK_SERVICE_SCHEMA = vol.Schema( vol.Required(CONTENT): cv.string, vol.Optional(DESCRIPTION): cv.string, vol.Optional(PROJECT_NAME, default="inbox"): vol.All(cv.string, vol.Lower), + vol.Optional(SECTION_NAME): vol.All(cv.string, vol.Lower), vol.Optional(LABELS): cv.ensure_list_csv, vol.Optional(ASSIGNEE): cv.string, vol.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)), @@ -201,7 +203,7 @@ async def async_setup_platform( async_register_services(hass, coordinator) -def async_register_services( +def async_register_services( # noqa: C901 hass: HomeAssistant, coordinator: TodoistCoordinator ) -> None: """Register services.""" @@ -211,7 +213,7 @@ def async_register_services( session = async_get_clientsession(hass) - async def handle_new_task(call: ServiceCall) -> None: + async def handle_new_task(call: ServiceCall) -> None: # noqa: C901 """Call when a user creates a new Todoist Task from Home Assistant.""" project_name = call.data[PROJECT_NAME].lower() projects = await coordinator.async_get_projects() @@ -222,12 +224,35 @@ def async_register_services( if project_id is None: raise HomeAssistantError(f"Invalid project name '{project_name}'") + # Optional section within project + section_id: str | None = None + if SECTION_NAME in call.data: + section_name = call.data[SECTION_NAME] + sections = await coordinator.async_get_sections(project_id) + for section in sections: + if section_name == section.name.lower(): + section_id = section.id + break + if section_id is None: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="section_invalid", + translation_placeholders={ + "section": section_name, + "project": project_name, + }, + ) + # Create the task content = call.data[CONTENT] data: dict[str, Any] = {"project_id": project_id} if description := call.data.get(DESCRIPTION): data["description"] = description + + if section_id is not None: + data["section_id"] = section_id + if task_labels := call.data.get(LABELS): data["labels"] = task_labels diff --git a/homeassistant/components/todoist/const.py b/homeassistant/components/todoist/const.py index 1a66fc9764f..be95d57dd2c 100644 --- a/homeassistant/components/todoist/const.py +++ b/homeassistant/components/todoist/const.py @@ -78,6 +78,8 @@ PROJECT_ID: Final = "project_id" PROJECT_NAME: Final = "project" # Todoist API: Fetch all Projects PROJECTS: Final = "projects" +# Section Name: What Section of the Project do you want to add the Task to? +SECTION_NAME: Final = "section" # Calendar Platform: When does a calendar event start? START: Final = "start" # Calendar Platform: What is the next calendar event about? diff --git a/homeassistant/components/todoist/coordinator.py b/homeassistant/components/todoist/coordinator.py index e01b4ecb35a..b55680907ac 100644 --- a/homeassistant/components/todoist/coordinator.py +++ b/homeassistant/components/todoist/coordinator.py @@ -4,7 +4,7 @@ from datetime import timedelta import logging from todoist_api_python.api_async import TodoistAPIAsync -from todoist_api_python.models import Label, Project, Task +from todoist_api_python.models import Label, Project, Section, Task from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -41,6 +41,10 @@ class TodoistCoordinator(DataUpdateCoordinator[list[Task]]): self._projects = await self.api.get_projects() return self._projects + async def async_get_sections(self, project_id: str) -> list[Section]: + """Return todoist sections for a given project ID.""" + return await self.api.get_sections(project_id=project_id) + async def async_get_labels(self) -> list[Label]: """Return todoist labels fetched at most once.""" if self._labels is None: diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index 1bd6320ebe3..17d877ea786 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -13,6 +13,10 @@ new_task: default: Inbox selector: text: + section: + example: Deliveries + selector: + text: labels: example: Chores,Delivieries selector: diff --git a/homeassistant/components/todoist/strings.json b/homeassistant/components/todoist/strings.json index 0cc74c9c8c6..55b7ef62b58 100644 --- a/homeassistant/components/todoist/strings.json +++ b/homeassistant/components/todoist/strings.json @@ -20,6 +20,11 @@ "default": "[%key:common::config_flow::create_entry::authenticated%]" } }, + "exceptions": { + "section_invalid": { + "message": "Project \"{project}\" has no section \"{section}\"" + } + }, "services": { "new_task": { "name": "New task", @@ -37,6 +42,10 @@ "name": "Project", "description": "The name of the project this task should belong to." }, + "section": { + "name": "Section", + "description": "The name of a section within the project to add the task to." + }, "labels": { "name": "Labels", "description": "Any labels that you want to apply to this task, separated by a comma." diff --git a/tests/components/todoist/conftest.py b/tests/components/todoist/conftest.py index 45fda53ccc1..4b2bfea2e30 100644 --- a/tests/components/todoist/conftest.py +++ b/tests/components/todoist/conftest.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, patch import pytest from requests.exceptions import HTTPError from requests.models import Response -from todoist_api_python.models import Collaborator, Due, Label, Project, Task +from todoist_api_python.models import Collaborator, Due, Label, Project, Section, Task from homeassistant.components.todoist import DOMAIN from homeassistant.const import CONF_TOKEN, Platform @@ -18,6 +18,7 @@ from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry PROJECT_ID = "project-id-1" +SECTION_ID = "section-id-1" SUMMARY = "A task" TOKEN = "some-token" TODAY = dt_util.now().strftime("%Y-%m-%d") @@ -98,6 +99,14 @@ def mock_api(tasks: list[Task]) -> AsyncMock: view_style="list", ) ] + api.get_sections.return_value = [ + Section( + id=SECTION_ID, + project_id=PROJECT_ID, + name="Section Name", + order=1, + ) + ] api.get_labels.return_value = [ Label(id="1", name="Label1", color="1", order=1, is_favorite=False) ] diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py index d8123af3231..680406096cc 100644 --- a/tests/components/todoist/test_calendar.py +++ b/tests/components/todoist/test_calendar.py @@ -18,6 +18,7 @@ from homeassistant.components.todoist.const import ( DOMAIN, LABELS, PROJECT_NAME, + SECTION_NAME, SERVICE_NEW_TASK, ) from homeassistant.const import CONF_TOKEN, Platform @@ -26,7 +27,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util import dt as dt_util -from .conftest import PROJECT_ID, SUMMARY +from .conftest import PROJECT_ID, SECTION_ID, SUMMARY from tests.typing import ClientSessionGenerator @@ -269,6 +270,32 @@ async def test_create_task_service_call(hass: HomeAssistant, api: AsyncMock) -> ) +async def test_create_task_service_call_with_section( + hass: HomeAssistant, api: AsyncMock +) -> None: + """Test api is called correctly when section is included.""" + await hass.services.async_call( + DOMAIN, + SERVICE_NEW_TASK, + { + ASSIGNEE: "user", + CONTENT: "task", + LABELS: ["Label1"], + PROJECT_NAME: "Name", + SECTION_NAME: "Section Name", + }, + ) + await hass.async_block_till_done() + + api.add_task.assert_called_with( + "task", + project_id=PROJECT_ID, + section_id=SECTION_ID, + labels=["Label1"], + assignee_id="1", + ) + + @pytest.mark.parametrize( ("due"), [ From bb31fc1ec72e204ee153f400b9baaa1a71d564e2 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sat, 3 Aug 2024 10:41:30 +0200 Subject: [PATCH 1929/2411] Test storage save and load for evohome (#122510) * test storage save and load * fix bug exposed by test * refactor test * add JSON for test account/location * create helpers to load JSON * refactor test * baseline refactor * tweak * update requiremenst * rationalise code * remove conditional in test * refactor test * mypy fix * tweak tests * working test * working test 4 * working test 5 * add typed dicts * working dtms * lint * fix dtm asserts * doc strings * list * tweak conditional * tweak test data sets to extend coverage * leverage conftest.py for subsequent tests * revert test storage * revert part two * rename symbols * remove anachronism * stop unwanted DNS lookup * Clean up type ignores * Format --------- Co-authored-by: Martin Hjelmare --- CODEOWNERS | 1 + requirements_test_all.txt | 3 + tests/components/evohome/__init__.py | 1 + tests/components/evohome/conftest.py | 111 ++++++ tests/components/evohome/const.py | 10 + .../evohome/fixtures/schedule_dhw.json | 81 ++++ .../evohome/fixtures/schedule_zone.json | 67 ++++ .../evohome/fixtures/status_2738909.json | 125 +++++++ .../evohome/fixtures/user_account.json | 11 + .../evohome/fixtures/user_locations.json | 346 ++++++++++++++++++ tests/components/evohome/test_storage.py | 208 +++++++++++ 11 files changed, 964 insertions(+) create mode 100644 tests/components/evohome/__init__.py create mode 100644 tests/components/evohome/conftest.py create mode 100644 tests/components/evohome/const.py create mode 100644 tests/components/evohome/fixtures/schedule_dhw.json create mode 100644 tests/components/evohome/fixtures/schedule_zone.json create mode 100644 tests/components/evohome/fixtures/status_2738909.json create mode 100644 tests/components/evohome/fixtures/user_account.json create mode 100644 tests/components/evohome/fixtures/user_locations.json create mode 100644 tests/components/evohome/test_storage.py diff --git a/CODEOWNERS b/CODEOWNERS index b53e0a929bb..c9b3a53184f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -433,6 +433,7 @@ build.json @home-assistant/supervisor /homeassistant/components/evil_genius_labs/ @balloob /tests/components/evil_genius_labs/ @balloob /homeassistant/components/evohome/ @zxdavb +/tests/components/evohome/ @zxdavb /homeassistant/components/ezviz/ @RenierM26 @baqs /tests/components/ezviz/ @RenierM26 @baqs /homeassistant/components/faa_delays/ @ntilley905 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7934e7fc84..e55efb78abb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -717,6 +717,9 @@ eternalegypt==0.0.16 # homeassistant.components.eufylife_ble eufylife-ble-client==0.1.8 +# homeassistant.components.evohome +evohome-async==0.4.20 + # homeassistant.components.bryant_evolution evolutionhttp==0.0.18 diff --git a/tests/components/evohome/__init__.py b/tests/components/evohome/__init__.py new file mode 100644 index 00000000000..588e0f61746 --- /dev/null +++ b/tests/components/evohome/__init__.py @@ -0,0 +1 @@ +"""The tests for the evohome integration.""" diff --git a/tests/components/evohome/conftest.py b/tests/components/evohome/conftest.py new file mode 100644 index 00000000000..260330896b7 --- /dev/null +++ b/tests/components/evohome/conftest.py @@ -0,0 +1,111 @@ +"""Fixtures and helpers for the evohome tests.""" + +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import Any, Final +from unittest.mock import MagicMock, patch + +from aiohttp import ClientSession +from evohomeasync2 import EvohomeClient +from evohomeasync2.broker import Broker +import pytest + +from homeassistant.components.evohome import CONF_PASSWORD, CONF_USERNAME, DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util.json import JsonArrayType, JsonObjectType + +from .const import ACCESS_TOKEN, REFRESH_TOKEN + +from tests.common import load_json_array_fixture, load_json_object_fixture + +TEST_CONFIG: Final = { + CONF_USERNAME: "username", + CONF_PASSWORD: "password", +} + + +def user_account_config_fixture() -> JsonObjectType: + """Load JSON for the config of a user's account.""" + return load_json_object_fixture("user_account.json", DOMAIN) + + +def user_locations_config_fixture() -> JsonArrayType: + """Load JSON for the config of a user's installation (a list of locations).""" + return load_json_array_fixture("user_locations.json", DOMAIN) + + +def location_status_fixture(loc_id: str) -> JsonObjectType: + """Load JSON for the status of a specific location.""" + return load_json_object_fixture(f"status_{loc_id}.json", DOMAIN) + + +def dhw_schedule_fixture() -> JsonObjectType: + """Load JSON for the schedule of a domesticHotWater zone.""" + return load_json_object_fixture("schedule_dhw.json", DOMAIN) + + +def zone_schedule_fixture() -> JsonObjectType: + """Load JSON for the schedule of a temperatureZone zone.""" + return load_json_object_fixture("schedule_zone.json", DOMAIN) + + +async def mock_get( + self: Broker, url: str, **kwargs: Any +) -> JsonArrayType | JsonObjectType: + """Return the JSON for a HTTP get of a given URL.""" + + # a proxy for the behaviour of the real web API + if self.refresh_token is None: + self.refresh_token = f"new_{REFRESH_TOKEN}" + + if self.access_token_expires is None or self.access_token_expires < datetime.now(): + self.access_token = f"new_{ACCESS_TOKEN}" + self.access_token_expires = datetime.now() + timedelta(minutes=30) + + # assume a valid GET, and return the JSON for that web API + if url == "userAccount": # userAccount + return user_account_config_fixture() + + if url.startswith("location"): + if "installationInfo" in url: # location/installationInfo?userId={id} + return user_locations_config_fixture() + if "location" in url: # location/{id}/status + return location_status_fixture("2738909") + + elif "schedule" in url: + if url.startswith("domesticHotWater"): # domesticHotWater/{id}/schedule + return dhw_schedule_fixture() + if url.startswith("temperatureZone"): # temperatureZone/{id}/schedule + return zone_schedule_fixture() + + pytest.xfail(f"Unexpected URL: {url}") + + +@patch("evohomeasync2.broker.Broker.get", mock_get) +async def setup_evohome(hass: HomeAssistant, test_config: dict[str, str]) -> MagicMock: + """Set up the evohome integration and return its client. + + The class is mocked here to check the client was instantiated with the correct args. + """ + + with ( + patch("homeassistant.components.evohome.evo.EvohomeClient") as mock_client, + patch("homeassistant.components.evohome.ev1.EvohomeClient", return_value=None), + ): + mock_client.side_effect = EvohomeClient + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: test_config}) + await hass.async_block_till_done() + + mock_client.assert_called_once() + + assert mock_client.call_args.args[0] == test_config[CONF_USERNAME] + assert mock_client.call_args.args[1] == test_config[CONF_PASSWORD] + + assert isinstance(mock_client.call_args.kwargs["session"], ClientSession) + + assert mock_client.account_info is not None + + return mock_client diff --git a/tests/components/evohome/const.py b/tests/components/evohome/const.py new file mode 100644 index 00000000000..0b298db533a --- /dev/null +++ b/tests/components/evohome/const.py @@ -0,0 +1,10 @@ +"""Constants for the evohome tests.""" + +from __future__ import annotations + +from typing import Final + +ACCESS_TOKEN: Final = "at_1dc7z657UKzbhKA..." +REFRESH_TOKEN: Final = "rf_jg68ZCKYdxEI3fF..." +SESSION_ID: Final = "F7181186..." +USERNAME: Final = "test_user@gmail.com" diff --git a/tests/components/evohome/fixtures/schedule_dhw.json b/tests/components/evohome/fixtures/schedule_dhw.json new file mode 100644 index 00000000000..da9a225fb82 --- /dev/null +++ b/tests/components/evohome/fixtures/schedule_dhw.json @@ -0,0 +1,81 @@ +{ + "dailySchedules": [ + { + "dayOfWeek": "Monday", + "switchpoints": [ + { "dhwState": "On", "timeOfDay": "06:30:00" }, + { "dhwState": "Off", "timeOfDay": "08:30:00" }, + { "dhwState": "On", "timeOfDay": "12:00:00" }, + { "dhwState": "Off", "timeOfDay": "13:00:00" }, + { "dhwState": "On", "timeOfDay": "16:30:00" }, + { "dhwState": "Off", "timeOfDay": "22:30:00" } + ] + }, + { + "dayOfWeek": "Tuesday", + "switchpoints": [ + { "dhwState": "On", "timeOfDay": "06:30:00" }, + { "dhwState": "Off", "timeOfDay": "08:30:00" }, + { "dhwState": "On", "timeOfDay": "12:00:00" }, + { "dhwState": "Off", "timeOfDay": "13:00:00" }, + { "dhwState": "On", "timeOfDay": "16:30:00" }, + { "dhwState": "Off", "timeOfDay": "22:30:00" } + ] + }, + { + "dayOfWeek": "Wednesday", + "switchpoints": [ + { "dhwState": "On", "timeOfDay": "06:30:00" }, + { "dhwState": "Off", "timeOfDay": "08:30:00" }, + { "dhwState": "On", "timeOfDay": "12:00:00" }, + { "dhwState": "Off", "timeOfDay": "13:00:00" }, + { "dhwState": "On", "timeOfDay": "16:30:00" }, + { "dhwState": "Off", "timeOfDay": "22:30:00" } + ] + }, + { + "dayOfWeek": "Thursday", + "switchpoints": [ + { "dhwState": "On", "timeOfDay": "06:30:00" }, + { "dhwState": "Off", "timeOfDay": "08:30:00" }, + { "dhwState": "On", "timeOfDay": "12:00:00" }, + { "dhwState": "Off", "timeOfDay": "13:00:00" }, + { "dhwState": "On", "timeOfDay": "16:30:00" }, + { "dhwState": "Off", "timeOfDay": "22:30:00" } + ] + }, + { + "dayOfWeek": "Friday", + "switchpoints": [ + { "dhwState": "On", "timeOfDay": "06:30:00" }, + { "dhwState": "Off", "timeOfDay": "08:30:00" }, + { "dhwState": "On", "timeOfDay": "12:00:00" }, + { "dhwState": "Off", "timeOfDay": "13:00:00" }, + { "dhwState": "On", "timeOfDay": "16:30:00" }, + { "dhwState": "Off", "timeOfDay": "22:30:00" } + ] + }, + { + "dayOfWeek": "Saturday", + "switchpoints": [ + { "dhwState": "On", "timeOfDay": "06:30:00" }, + { "dhwState": "Off", "timeOfDay": "09:30:00" }, + { "dhwState": "On", "timeOfDay": "12:00:00" }, + { "dhwState": "Off", "timeOfDay": "13:00:00" }, + { "dhwState": "On", "timeOfDay": "16:30:00" }, + { "dhwState": "Off", "timeOfDay": "23:00:00" } + ] + }, + { + "dayOfWeek": "Sunday", + "switchpoints": [ + { "dhwState": "On", "timeOfDay": "06:30:00" }, + { "dhwState": "Off", "timeOfDay": "09:30:00" }, + { "dhwState": "On", "timeOfDay": "12:00:00" }, + { "dhwState": "Off", "timeOfDay": "13:00:00" }, + { "dhwState": "On", "timeOfDay": "16:30:00" }, + { "dhwState": "Off", "timeOfDay": "23:00:00" } + ] + } + ] +} diff --git a/tests/components/evohome/fixtures/schedule_zone.json b/tests/components/evohome/fixtures/schedule_zone.json new file mode 100644 index 00000000000..5030d92ff3d --- /dev/null +++ b/tests/components/evohome/fixtures/schedule_zone.json @@ -0,0 +1,67 @@ +{ + "dailySchedules": [ + { + "dayOfWeek": "Monday", + "switchpoints": [ + { "heatSetpoint": 18.1, "timeOfDay": "07:00:00" }, + { "heatSetpoint": 16.0, "timeOfDay": "08:00:00" }, + { "heatSetpoint": 18.6, "timeOfDay": "22:10:00" }, + { "heatSetpoint": 15.9, "timeOfDay": "23:00:00" } + ] + }, + { + "dayOfWeek": "Tuesday", + "switchpoints": [ + { "heatSetpoint": 18.1, "timeOfDay": "07:00:00" }, + { "heatSetpoint": 16.0, "timeOfDay": "08:00:00" }, + { "heatSetpoint": 18.6, "timeOfDay": "22:10:00" }, + { "heatSetpoint": 15.9, "timeOfDay": "23:00:00" } + ] + }, + { + "dayOfWeek": "Wednesday", + "switchpoints": [ + { "heatSetpoint": 18.1, "timeOfDay": "07:00:00" }, + { "heatSetpoint": 16.0, "timeOfDay": "08:00:00" }, + { "heatSetpoint": 18.6, "timeOfDay": "22:10:00" }, + { "heatSetpoint": 15.9, "timeOfDay": "23:00:00" } + ] + }, + { + "dayOfWeek": "Thursday", + "switchpoints": [ + { "heatSetpoint": 18.1, "timeOfDay": "07:00:00" }, + { "heatSetpoint": 16.0, "timeOfDay": "08:00:00" }, + { "heatSetpoint": 18.6, "timeOfDay": "22:10:00" }, + { "heatSetpoint": 15.9, "timeOfDay": "23:00:00" } + ] + }, + { + "dayOfWeek": "Friday", + "switchpoints": [ + { "heatSetpoint": 18.1, "timeOfDay": "07:00:00" }, + { "heatSetpoint": 16.0, "timeOfDay": "08:00:00" }, + { "heatSetpoint": 18.6, "timeOfDay": "22:10:00" }, + { "heatSetpoint": 15.9, "timeOfDay": "23:00:00" } + ] + }, + { + "dayOfWeek": "Saturday", + "switchpoints": [ + { "heatSetpoint": 18.5, "timeOfDay": "07:00:00" }, + { "heatSetpoint": 16.0, "timeOfDay": "08:30:00" }, + { "heatSetpoint": 18.6, "timeOfDay": "22:10:00" }, + { "heatSetpoint": 15.9, "timeOfDay": "23:00:00" } + ] + }, + { + "dayOfWeek": "Sunday", + "switchpoints": [ + { "heatSetpoint": 18.5, "timeOfDay": "07:00:00" }, + { "heatSetpoint": 16.0, "timeOfDay": "08:30:00" }, + { "heatSetpoint": 18.6, "timeOfDay": "22:10:00" }, + { "heatSetpoint": 15.9, "timeOfDay": "23:00:00" } + ] + } + ] +} diff --git a/tests/components/evohome/fixtures/status_2738909.json b/tests/components/evohome/fixtures/status_2738909.json new file mode 100644 index 00000000000..6d555ba4e3e --- /dev/null +++ b/tests/components/evohome/fixtures/status_2738909.json @@ -0,0 +1,125 @@ +{ + "locationId": "2738909", + "gateways": [ + { + "gatewayId": "2499896", + "temperatureControlSystems": [ + { + "systemId": "3432522", + "zones": [ + { + "zoneId": "3432521", + "name": "Dead Zone", + "temperatureStatus": { "isAvailable": false }, + "setpointStatus": { + "targetHeatTemperature": 17.0, + "setpointMode": "FollowSchedule" + }, + "activeFaults": [] + }, + { + "zoneId": "3432576", + "name": "Main Room", + "temperatureStatus": { "temperature": 19.0, "isAvailable": true }, + "setpointStatus": { + "targetHeatTemperature": 17.0, + "setpointMode": "PermanentOverride" + }, + "activeFaults": [ + { + "faultType": "TempZoneActuatorCommunicationLost", + "since": "2022-03-02T15:56:01" + } + ] + }, + { + "zoneId": "3432577", + "name": "Front Room", + "temperatureStatus": { "temperature": 19.0, "isAvailable": true }, + "setpointStatus": { + "targetHeatTemperature": 21.0, + "setpointMode": "TemporaryOverride", + "until": "2022-03-07T19:00:00Z" + }, + "activeFaults": [ + { + "faultType": "TempZoneActuatorLowBattery", + "since": "2022-03-02T04:50:20" + } + ] + }, + { + "zoneId": "3432578", + "temperatureStatus": { "temperature": 20.0, "isAvailable": true }, + "activeFaults": [], + "setpointStatus": { + "targetHeatTemperature": 17.0, + "setpointMode": "FollowSchedule" + }, + "name": "Kitchen" + }, + { + "zoneId": "3432579", + "temperatureStatus": { "temperature": 20.0, "isAvailable": true }, + "activeFaults": [], + "setpointStatus": { + "targetHeatTemperature": 16.0, + "setpointMode": "FollowSchedule" + }, + "name": "Bathroom Dn" + }, + { + "zoneId": "3432580", + "temperatureStatus": { "temperature": 21.0, "isAvailable": true }, + "activeFaults": [], + "setpointStatus": { + "targetHeatTemperature": 16.0, + "setpointMode": "FollowSchedule" + }, + "name": "Main Bedroom" + }, + { + "zoneId": "3449703", + "temperatureStatus": { "temperature": 19.5, "isAvailable": true }, + "activeFaults": [], + "setpointStatus": { + "targetHeatTemperature": 17.0, + "setpointMode": "FollowSchedule" + }, + "name": "Kids Room" + }, + { + "zoneId": "3449740", + "temperatureStatus": { "temperature": 21.5, "isAvailable": true }, + "activeFaults": [], + "setpointStatus": { + "targetHeatTemperature": 16.5, + "setpointMode": "FollowSchedule" + }, + "name": "" + }, + { + "zoneId": "3450733", + "temperatureStatus": { "temperature": 19.5, "isAvailable": true }, + "activeFaults": [], + "setpointStatus": { + "targetHeatTemperature": 14.0, + "setpointMode": "PermanentOverride" + }, + "name": "Spare Room" + } + ], + "dhw": { + "dhwId": "3933910", + "temperatureStatus": { "temperature": 23.0, "isAvailable": true }, + "stateStatus": { "state": "Off", "mode": "PermanentOverride" }, + "activeFaults": [] + }, + "activeFaults": [], + "systemModeStatus": { "mode": "AutoWithEco", "isPermanent": true } + } + ], + "activeFaults": [] + } + ] +} diff --git a/tests/components/evohome/fixtures/user_account.json b/tests/components/evohome/fixtures/user_account.json new file mode 100644 index 00000000000..99a96a7961e --- /dev/null +++ b/tests/components/evohome/fixtures/user_account.json @@ -0,0 +1,11 @@ +{ + "userId": "2263181", + "username": "user_2263181@gmail.com", + "firstname": "John", + "lastname": "Smith", + "streetAddress": "1 Main Street", + "city": "London", + "postcode": "E1 1AA", + "country": "UnitedKingdom", + "language": "enGB" +} diff --git a/tests/components/evohome/fixtures/user_locations.json b/tests/components/evohome/fixtures/user_locations.json new file mode 100644 index 00000000000..cf59aa9ae8a --- /dev/null +++ b/tests/components/evohome/fixtures/user_locations.json @@ -0,0 +1,346 @@ +[ + { + "locationInfo": { + "locationId": "2738909", + "name": "My Home", + "streetAddress": "1 Main Street", + "city": "London", + "country": "UnitedKingdom", + "postcode": "E1 1AA", + "locationType": "Residential", + "useDaylightSaveSwitching": true, + "timeZone": { + "timeZoneId": "GMTStandardTime", + "displayName": "(UTC+00:00) Dublin, Edinburgh, Lisbon, London", + "offsetMinutes": 0, + "currentOffsetMinutes": 60, + "supportsDaylightSaving": true + }, + "locationOwner": { + "userId": "2263181", + "username": "user_2263181@gmail.com", + "firstname": "John", + "lastname": "Smith" + } + }, + "gateways": [ + { + "gatewayInfo": { + "gatewayId": "2499896", + "mac": "00D02DEE0000", + "crc": "1234", + "isWiFi": false + }, + "temperatureControlSystems": [ + { + "systemId": "3432522", + "modelType": "EvoTouch", + "zones": [ + { + "zoneId": "3432521", + "modelType": "HeatingZone", + "setpointCapabilities": { + "maxHeatSetpoint": 35.0, + "minHeatSetpoint": 5.0, + "valueResolution": 0.5, + "canControlHeat": true, + "canControlCool": false, + "allowedSetpointModes": [ + "PermanentOverride", + "FollowSchedule", + "TemporaryOverride" + ], + "maxDuration": "1.00:00:00", + "timingResolution": "00:10:00" + }, + "scheduleCapabilities": { + "maxSwitchpointsPerDay": 6, + "minSwitchpointsPerDay": 1, + "timingResolution": "00:10:00", + "setpointValueResolution": 0.5 + }, + "name": "Dead Zone", + "zoneType": "RadiatorZone" + }, + { + "zoneId": "3432576", + "modelType": "HeatingZone", + "setpointCapabilities": { + "maxHeatSetpoint": 35.0, + "minHeatSetpoint": 5.0, + "valueResolution": 0.5, + "canControlHeat": true, + "canControlCool": false, + "allowedSetpointModes": [ + "PermanentOverride", + "FollowSchedule", + "TemporaryOverride" + ], + "maxDuration": "1.00:00:00", + "timingResolution": "00:10:00" + }, + "scheduleCapabilities": { + "maxSwitchpointsPerDay": 6, + "minSwitchpointsPerDay": 1, + "timingResolution": "00:10:00", + "setpointValueResolution": 0.5 + }, + "name": "Main Room", + "zoneType": "RadiatorZone" + }, + { + "zoneId": "3432577", + "modelType": "HeatingZone", + "setpointCapabilities": { + "maxHeatSetpoint": 35.0, + "minHeatSetpoint": 5.0, + "valueResolution": 0.5, + "canControlHeat": true, + "canControlCool": false, + "allowedSetpointModes": [ + "PermanentOverride", + "FollowSchedule", + "TemporaryOverride" + ], + "maxDuration": "1.00:00:00", + "timingResolution": "00:10:00" + }, + "scheduleCapabilities": { + "maxSwitchpointsPerDay": 6, + "minSwitchpointsPerDay": 1, + "timingResolution": "00:10:00", + "setpointValueResolution": 0.5 + }, + "name": "Front Room", + "zoneType": "RadiatorZone" + }, + { + "zoneId": "3432578", + "modelType": "HeatingZone", + "setpointCapabilities": { + "maxHeatSetpoint": 35.0, + "minHeatSetpoint": 5.0, + "valueResolution": 0.5, + "canControlHeat": true, + "canControlCool": false, + "allowedSetpointModes": [ + "PermanentOverride", + "FollowSchedule", + "TemporaryOverride" + ], + "maxDuration": "1.00:00:00", + "timingResolution": "00:10:00" + }, + "scheduleCapabilities": { + "maxSwitchpointsPerDay": 6, + "minSwitchpointsPerDay": 1, + "timingResolution": "00:10:00", + "setpointValueResolution": 0.5 + }, + "name": "Kitchen", + "zoneType": "RadiatorZone" + }, + { + "zoneId": "3432579", + "modelType": "HeatingZone", + "setpointCapabilities": { + "maxHeatSetpoint": 35.0, + "minHeatSetpoint": 5.0, + "valueResolution": 0.5, + "canControlHeat": true, + "canControlCool": false, + "allowedSetpointModes": [ + "PermanentOverride", + "FollowSchedule", + "TemporaryOverride" + ], + "maxDuration": "1.00:00:00", + "timingResolution": "00:10:00" + }, + "scheduleCapabilities": { + "maxSwitchpointsPerDay": 6, + "minSwitchpointsPerDay": 1, + "timingResolution": "00:10:00", + "setpointValueResolution": 0.5 + }, + "name": "Bathroom Dn", + "zoneType": "RadiatorZone" + }, + { + "zoneId": "3432580", + "modelType": "HeatingZone", + "setpointCapabilities": { + "maxHeatSetpoint": 35.0, + "minHeatSetpoint": 5.0, + "valueResolution": 0.5, + "canControlHeat": true, + "canControlCool": false, + "allowedSetpointModes": [ + "PermanentOverride", + "FollowSchedule", + "TemporaryOverride" + ], + "maxDuration": "1.00:00:00", + "timingResolution": "00:10:00" + }, + "scheduleCapabilities": { + "maxSwitchpointsPerDay": 6, + "minSwitchpointsPerDay": 1, + "timingResolution": "00:10:00", + "setpointValueResolution": 0.5 + }, + "name": "Main Bedroom", + "zoneType": "RadiatorZone" + }, + { + "zoneId": "3449703", + "modelType": "HeatingZone", + "setpointCapabilities": { + "maxHeatSetpoint": 35.0, + "minHeatSetpoint": 5.0, + "valueResolution": 0.5, + "canControlHeat": true, + "canControlCool": false, + "allowedSetpointModes": [ + "PermanentOverride", + "FollowSchedule", + "TemporaryOverride" + ], + "maxDuration": "1.00:00:00", + "timingResolution": "00:10:00" + }, + "scheduleCapabilities": { + "maxSwitchpointsPerDay": 6, + "minSwitchpointsPerDay": 1, + "timingResolution": "00:10:00", + "setpointValueResolution": 0.5 + }, + "name": "Kids Room", + "zoneType": "RadiatorZone" + }, + { + "zoneId": "3449740", + "modelType": "Unknown", + "setpointCapabilities": { + "maxHeatSetpoint": 35.0, + "minHeatSetpoint": 5.0, + "valueResolution": 0.5, + "canControlHeat": true, + "canControlCool": false, + "allowedSetpointModes": [ + "PermanentOverride", + "FollowSchedule", + "TemporaryOverride" + ], + "maxDuration": "1.00:00:00", + "timingResolution": "00:10:00" + }, + "scheduleCapabilities": { + "maxSwitchpointsPerDay": 6, + "minSwitchpointsPerDay": 1, + "timingResolution": "00:10:00", + "setpointValueResolution": 0.5 + }, + "name": "", + "zoneType": "Unknown" + }, + { + "zoneId": "3450733", + "modelType": "xx", + "setpointCapabilities": { + "maxHeatSetpoint": 35.0, + "minHeatSetpoint": 5.0, + "valueResolution": 0.5, + "canControlHeat": true, + "canControlCool": false, + "allowedSetpointModes": [ + "PermanentOverride", + "FollowSchedule", + "TemporaryOverride" + ], + "maxDuration": "1.00:00:00", + "timingResolution": "00:10:00" + }, + "scheduleCapabilities": { + "maxSwitchpointsPerDay": 6, + "minSwitchpointsPerDay": 1, + "timingResolution": "00:10:00", + "setpointValueResolution": 0.5 + }, + "name": "Spare Room", + "zoneType": "xx" + } + ], + "dhw": { + "dhwId": "3933910", + "dhwStateCapabilitiesResponse": { + "allowedStates": ["On", "Off"], + "allowedModes": [ + "FollowSchedule", + "PermanentOverride", + "TemporaryOverride" + ], + "maxDuration": "1.00:00:00", + "timingResolution": "00:10:00" + }, + "scheduleCapabilitiesResponse": { + "maxSwitchpointsPerDay": 6, + "minSwitchpointsPerDay": 1, + "timingResolution": "00:10:00" + } + }, + "allowedSystemModes": [ + { + "systemMode": "HeatingOff", + "canBePermanent": true, + "canBeTemporary": false + }, + { + "systemMode": "Auto", + "canBePermanent": true, + "canBeTemporary": false + }, + { + "systemMode": "AutoWithReset", + "canBePermanent": true, + "canBeTemporary": false + }, + { + "systemMode": "AutoWithEco", + "canBePermanent": true, + "canBeTemporary": true, + "maxDuration": "1.00:00:00", + "timingResolution": "01:00:00", + "timingMode": "Duration" + }, + { + "systemMode": "Away", + "canBePermanent": true, + "canBeTemporary": true, + "maxDuration": "99.00:00:00", + "timingResolution": "1.00:00:00", + "timingMode": "Period" + }, + { + "systemMode": "DayOff", + "canBePermanent": true, + "canBeTemporary": true, + "maxDuration": "99.00:00:00", + "timingResolution": "1.00:00:00", + "timingMode": "Period" + }, + { + "systemMode": "Custom", + "canBePermanent": true, + "canBeTemporary": true, + "maxDuration": "99.00:00:00", + "timingResolution": "1.00:00:00", + "timingMode": "Period" + } + ] + } + ] + } + ] + } +] diff --git a/tests/components/evohome/test_storage.py b/tests/components/evohome/test_storage.py new file mode 100644 index 00000000000..e87b847a9ff --- /dev/null +++ b/tests/components/evohome/test_storage.py @@ -0,0 +1,208 @@ +"""The tests for evohome storage load & save.""" + +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import Any, Final, NotRequired, TypedDict + +import pytest + +from homeassistant.components.evohome import ( + CONF_PASSWORD, + CONF_USERNAME, + DOMAIN, + STORAGE_KEY, + STORAGE_VER, + dt_aware_to_naive, +) +from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util + +from .conftest import setup_evohome +from .const import ACCESS_TOKEN, REFRESH_TOKEN, SESSION_ID, USERNAME + + +class _SessionDataT(TypedDict): + sessionId: str + + +class _TokenStoreT(TypedDict): + username: str + refresh_token: str + access_token: str + access_token_expires: str # 2024-07-27T23:57:30+01:00 + user_data: NotRequired[_SessionDataT] + + +class _EmptyStoreT(TypedDict): + pass + + +SZ_USERNAME: Final = "username" +SZ_REFRESH_TOKEN: Final = "refresh_token" +SZ_ACCESS_TOKEN: Final = "access_token" +SZ_ACCESS_TOKEN_EXPIRES: Final = "access_token_expires" +SZ_USER_DATA: Final = "user_data" + + +def dt_pair(dt_dtm: datetime) -> tuple[datetime, str]: + """Return a datetime without milliseconds and its string representation.""" + dt_str = dt_dtm.isoformat(timespec="seconds") # e.g. 2024-07-28T00:57:29+01:00 + return dt_util.parse_datetime(dt_str, raise_on_error=True), dt_str + + +ACCESS_TOKEN_EXP_DTM, ACCESS_TOKEN_EXP_STR = dt_pair(dt_util.now() + timedelta(hours=1)) + +USERNAME_DIFF: Final = f"not_{USERNAME}" +USERNAME_SAME: Final = USERNAME + +TEST_CONFIG: Final = { + CONF_USERNAME: USERNAME_SAME, + CONF_PASSWORD: "password", +} + +TEST_DATA: Final[dict[str, _TokenStoreT]] = { + "sans_session_id": { + SZ_USERNAME: USERNAME_SAME, + SZ_REFRESH_TOKEN: REFRESH_TOKEN, + SZ_ACCESS_TOKEN: ACCESS_TOKEN, + SZ_ACCESS_TOKEN_EXPIRES: ACCESS_TOKEN_EXP_STR, + }, + "with_session_id": { + SZ_USERNAME: USERNAME_SAME, + SZ_REFRESH_TOKEN: REFRESH_TOKEN, + SZ_ACCESS_TOKEN: ACCESS_TOKEN, + SZ_ACCESS_TOKEN_EXPIRES: ACCESS_TOKEN_EXP_STR, + SZ_USER_DATA: {"sessionId": SESSION_ID}, + }, +} + +TEST_DATA_NULL: Final[dict[str, _EmptyStoreT | None]] = { + "store_is_absent": None, + "store_was_reset": {}, +} + +DOMAIN_STORAGE_BASE: Final = { + "version": STORAGE_VER, + "minor_version": 1, + "key": STORAGE_KEY, +} + + +@pytest.mark.parametrize("idx", TEST_DATA_NULL) +async def test_auth_tokens_null( + hass: HomeAssistant, + hass_storage: dict[str, Any], + idx: str, +) -> None: + """Test loading/saving authentication tokens when no cached tokens in the store.""" + + hass_storage[DOMAIN] = DOMAIN_STORAGE_BASE | {"data": TEST_DATA_NULL[idx]} + + mock_client = await setup_evohome(hass, TEST_CONFIG) + + # Confirm client was instantiated without tokens, as cache was empty... + assert SZ_REFRESH_TOKEN not in mock_client.call_args.kwargs + assert SZ_ACCESS_TOKEN not in mock_client.call_args.kwargs + assert SZ_ACCESS_TOKEN_EXPIRES not in mock_client.call_args.kwarg + + # Confirm the expected tokens were cached to storage... + data: _TokenStoreT = hass_storage[DOMAIN]["data"] + + assert data[SZ_USERNAME] == USERNAME_SAME + assert data[SZ_REFRESH_TOKEN] == f"new_{REFRESH_TOKEN}" + assert data[SZ_ACCESS_TOKEN] == f"new_{ACCESS_TOKEN}" + assert ( + dt_util.parse_datetime(data[SZ_ACCESS_TOKEN_EXPIRES], raise_on_error=True) + > dt_util.now() + ) + + +@pytest.mark.parametrize("idx", TEST_DATA) +async def test_auth_tokens_same( + hass: HomeAssistant, hass_storage: dict[str, Any], idx: str +) -> None: + """Test loading/saving authentication tokens when matching username.""" + + hass_storage[DOMAIN] = DOMAIN_STORAGE_BASE | {"data": TEST_DATA[idx]} + + mock_client = await setup_evohome(hass, TEST_CONFIG) + + # Confirm client was instantiated with the cached tokens... + assert mock_client.call_args.kwargs[SZ_REFRESH_TOKEN] == REFRESH_TOKEN + assert mock_client.call_args.kwargs[SZ_ACCESS_TOKEN] == ACCESS_TOKEN + assert mock_client.call_args.kwargs[SZ_ACCESS_TOKEN_EXPIRES] == dt_aware_to_naive( + ACCESS_TOKEN_EXP_DTM + ) + + # Confirm the expected tokens were cached to storage... + data: _TokenStoreT = hass_storage[DOMAIN]["data"] + + assert data[SZ_USERNAME] == USERNAME_SAME + assert data[SZ_REFRESH_TOKEN] == REFRESH_TOKEN + assert data[SZ_ACCESS_TOKEN] == ACCESS_TOKEN + assert dt_util.parse_datetime(data[SZ_ACCESS_TOKEN_EXPIRES]) == ACCESS_TOKEN_EXP_DTM + + +@pytest.mark.parametrize("idx", TEST_DATA) +async def test_auth_tokens_past( + hass: HomeAssistant, hass_storage: dict[str, Any], idx: str +) -> None: + """Test loading/saving authentication tokens with matching username, but expired.""" + + dt_dtm, dt_str = dt_pair(dt_util.now() - timedelta(hours=1)) + + # make this access token have expired in the past... + test_data = TEST_DATA[idx].copy() # shallow copy is OK here + test_data[SZ_ACCESS_TOKEN_EXPIRES] = dt_str + + hass_storage[DOMAIN] = DOMAIN_STORAGE_BASE | {"data": test_data} + + mock_client = await setup_evohome(hass, TEST_CONFIG) + + # Confirm client was instantiated with the cached tokens... + assert mock_client.call_args.kwargs[SZ_REFRESH_TOKEN] == REFRESH_TOKEN + assert mock_client.call_args.kwargs[SZ_ACCESS_TOKEN] == ACCESS_TOKEN + assert mock_client.call_args.kwargs[SZ_ACCESS_TOKEN_EXPIRES] == dt_aware_to_naive( + dt_dtm + ) + + # Confirm the expected tokens were cached to storage... + data: _TokenStoreT = hass_storage[DOMAIN]["data"] + + assert data[SZ_USERNAME] == USERNAME_SAME + assert data[SZ_REFRESH_TOKEN] == REFRESH_TOKEN + assert data[SZ_ACCESS_TOKEN] == f"new_{ACCESS_TOKEN}" + assert ( + dt_util.parse_datetime(data[SZ_ACCESS_TOKEN_EXPIRES], raise_on_error=True) + > dt_util.now() + ) + + +@pytest.mark.parametrize("idx", TEST_DATA) +async def test_auth_tokens_diff( + hass: HomeAssistant, hass_storage: dict[str, Any], idx: str +) -> None: + """Test loading/saving authentication tokens when unmatched username.""" + + hass_storage[DOMAIN] = DOMAIN_STORAGE_BASE | {"data": TEST_DATA[idx]} + + mock_client = await setup_evohome( + hass, TEST_CONFIG | {CONF_USERNAME: USERNAME_DIFF} + ) + + # Confirm client was instantiated without tokens, as username was different... + assert SZ_REFRESH_TOKEN not in mock_client.call_args.kwargs + assert SZ_ACCESS_TOKEN not in mock_client.call_args.kwargs + assert SZ_ACCESS_TOKEN_EXPIRES not in mock_client.call_args.kwarg + + # Confirm the expected tokens were cached to storage... + data: _TokenStoreT = hass_storage[DOMAIN]["data"] + + assert data[SZ_USERNAME] == USERNAME_DIFF + assert data[SZ_REFRESH_TOKEN] == f"new_{REFRESH_TOKEN}" + assert data[SZ_ACCESS_TOKEN] == f"new_{ACCESS_TOKEN}" + assert ( + dt_util.parse_datetime(data[SZ_ACCESS_TOKEN_EXPIRES], raise_on_error=True) + > dt_util.now() + ) From 61cbb770424fa27869c5484c56045521e8c083aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Aug 2024 06:26:32 -0500 Subject: [PATCH 1930/2411] Remove unneeded cast in logbook rest api (#123098) --- homeassistant/components/logbook/rest_api.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/logbook/rest_api.py b/homeassistant/components/logbook/rest_api.py index bd9efe7aba3..c7ba196275b 100644 --- a/homeassistant/components/logbook/rest_api.py +++ b/homeassistant/components/logbook/rest_api.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Callable from datetime import timedelta from http import HTTPStatus -from typing import Any, cast +from typing import Any from aiohttp import web import voluptuous as vol @@ -109,13 +109,6 @@ class LogbookView(HomeAssistantView): def json_events() -> web.Response: """Fetch events and generate JSON.""" - return self.json( - event_processor.get_events( - start_day, - end_day, - ) - ) + return self.json(event_processor.get_events(start_day, end_day)) - return cast( - web.Response, await get_instance(hass).async_add_executor_job(json_events) - ) + return await get_instance(hass).async_add_executor_job(json_events) From 0fe23c82a4c8ef078095da221500f0e6844f6e2b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Aug 2024 06:26:54 -0500 Subject: [PATCH 1931/2411] Remove unused variables in logbook LazyEventPartialState (#123097) --- homeassistant/components/logbook/models.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/logbook/models.py b/homeassistant/components/logbook/models.py index 2f9b2c8e289..93fc8885f57 100644 --- a/homeassistant/components/logbook/models.py +++ b/homeassistant/components/logbook/models.py @@ -45,8 +45,6 @@ class LazyEventPartialState: ) -> None: """Init the lazy event.""" self.row = row - self._event_data: dict[str, Any] | None = None - self._event_data_cache = event_data_cache # We need to explicitly check for the row is EventAsRow as the unhappy path # to fetch row.data for Row is very expensive if type(row) is EventAsRow: @@ -60,10 +58,10 @@ class LazyEventPartialState: source = row.event_data if not source: self.data = {} - elif event_data := self._event_data_cache.get(source): + elif event_data := event_data_cache.get(source): self.data = event_data else: - self.data = self._event_data_cache[source] = cast( + self.data = event_data_cache[source] = cast( dict[str, Any], json_loads(source) ) From cdec43ec067868c1436fd18b117616607c807214 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Aug 2024 08:03:28 -0500 Subject: [PATCH 1932/2411] Remove unreachable suppress in logbook (#123096) --- homeassistant/components/logbook/processor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index ed9888f83d0..28f98bc2ce9 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Callable, Generator, Sequence -from contextlib import suppress from dataclasses import dataclass from datetime import datetime as dt import logging @@ -262,8 +261,7 @@ def _humanify( entry_domain = event_data.get(ATTR_DOMAIN) entry_entity_id = event_data.get(ATTR_ENTITY_ID) if entry_domain is None and entry_entity_id is not None: - with suppress(IndexError): - entry_domain = split_entity_id(str(entry_entity_id))[0] + entry_domain = split_entity_id(str(entry_entity_id))[0] data = { LOGBOOK_ENTRY_WHEN: format_time(row), LOGBOOK_ENTRY_NAME: event_data.get(ATTR_NAME), From eb5ee1ffd1d41dc3515ea0112584c52366743702 Mon Sep 17 00:00:00 2001 From: Kim de Vos Date: Sat, 3 Aug 2024 15:08:01 +0200 Subject: [PATCH 1933/2411] Use slugify to create id for UniFi WAN latency (#123108) Use slugify to create id for latency --- homeassistant/components/unifi/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index d86b72d1b2f..08bd0ddb869 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -44,6 +44,7 @@ from homeassistant.core import Event as core_Event, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.util import slugify import homeassistant.util.dt as dt_util from . import UnifiConfigEntry @@ -247,8 +248,9 @@ def make_wan_latency_sensors() -> tuple[UnifiSensorEntityDescription, ...]: def make_wan_latency_entity_description( wan: Literal["WAN", "WAN2"], name: str, monitor_target: str ) -> UnifiSensorEntityDescription: + name_wan = f"{name} {wan}" return UnifiSensorEntityDescription[Devices, Device]( - key=f"{name} {wan} latency", + key=f"{name_wan} latency", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfTime.MILLISECONDS, state_class=SensorStateClass.MEASUREMENT, @@ -257,13 +259,12 @@ def make_wan_latency_sensors() -> tuple[UnifiSensorEntityDescription, ...]: api_handler_fn=lambda api: api.devices, available_fn=async_device_available_fn, device_info_fn=async_device_device_info_fn, - name_fn=lambda _: f"{name} {wan} latency", + name_fn=lambda device: f"{name_wan} latency", object_fn=lambda api, obj_id: api.devices[obj_id], supported_fn=partial( async_device_wan_latency_supported_fn, wan, monitor_target ), - unique_id_fn=lambda hub, - obj_id: f"{name.lower}_{wan.lower}_latency-{obj_id}", + unique_id_fn=lambda hub, obj_id: f"{slugify(name_wan)}_latency-{obj_id}", value_fn=partial(async_device_wan_latency_value_fn, wan, monitor_target), ) From b7d56ad38a7dbb0a4b1d6fce7cd49995c026c04b Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sat, 3 Aug 2024 17:21:12 +0200 Subject: [PATCH 1934/2411] Bump pyenphase to 1.22.0 (#123103) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 09c55fb23ac..aa06a1ff79f 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.20.6"], + "requirements": ["pyenphase==1.22.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 2805ad2c716..559290fd069 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1843,7 +1843,7 @@ pyeiscp==0.0.7 pyemoncms==0.0.7 # homeassistant.components.enphase_envoy -pyenphase==1.20.6 +pyenphase==1.22.0 # homeassistant.components.envisalink pyenvisalink==4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e55efb78abb..4ff84200d77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1475,7 +1475,7 @@ pyegps==0.2.5 pyemoncms==0.0.7 # homeassistant.components.enphase_envoy -pyenphase==1.20.6 +pyenphase==1.22.0 # homeassistant.components.everlights pyeverlights==0.1.0 From 02f81ec481870254497a963233858a2642a40fd8 Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Sat, 3 Aug 2024 22:32:47 +0200 Subject: [PATCH 1935/2411] Fix wrong DeviceInfo in bluesound integration (#123101) Fix bluesound device info --- homeassistant/components/bluesound/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 809ba293f89..dc09feaed63 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -255,7 +255,7 @@ class BluesoundPlayer(MediaPlayerEntity): self._attr_unique_id = format_unique_id(sync_status.mac, port) # there should always be one player with the default port per mac - if port is DEFAULT_PORT: + if port == DEFAULT_PORT: self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, format_mac(sync_status.mac))}, connections={(CONNECTION_NETWORK_MAC, format_mac(sync_status.mac))}, From b6de2cd741133cff15e9978ef478c3bbbba1f0b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Aug 2024 17:33:46 -0500 Subject: [PATCH 1936/2411] Unpack non-performant any expressions in config flow discovery path (#123124) --- homeassistant/config_entries.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index aa0113cd7ce..75b0631339f 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1245,10 +1245,10 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): @callback def _async_has_other_discovery_flows(self, flow_id: str) -> bool: """Check if there are any other discovery flows in progress.""" - return any( - flow.context["source"] in DISCOVERY_SOURCES and flow.flow_id != flow_id - for flow in self._progress.values() - ) + for flow in self._progress.values(): + if flow.flow_id != flow_id and flow.context["source"] in DISCOVERY_SOURCES: + return True + return False async def async_init( self, handler: str, *, context: dict[str, Any] | None = None, data: Any = None @@ -1699,12 +1699,12 @@ class ConfigEntries: entries = self._entries.get_entries_for_domain(domain) if include_ignore and include_disabled: return bool(entries) - return any( - entry - for entry in entries - if (include_ignore or entry.source != SOURCE_IGNORE) - and (include_disabled or not entry.disabled_by) - ) + for entry in entries: + if (include_ignore or entry.source != SOURCE_IGNORE) and ( + include_disabled or not entry.disabled_by + ): + return True + return False @callback def async_entries( From 232f78e7b68e3e97713b998cd8fcab8d312253fe Mon Sep 17 00:00:00 2001 From: Steve Repsher Date: Sun, 4 Aug 2024 08:28:45 -0400 Subject: [PATCH 1937/2411] Restore old service worker URL (#123131) --- homeassistant/components/frontend/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 5b462842e4a..c5df84cf549 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -398,6 +398,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: static_paths_configs: list[StaticPathConfig] = [] for path, should_cache in ( + ("service_worker.js", False), ("sw-modern.js", False), ("sw-modern.js.map", False), ("sw-legacy.js", False), From 70704f67d39605132e66c62b2869108861da9aeb Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Sun, 4 Aug 2024 15:17:54 +0200 Subject: [PATCH 1938/2411] Recorder system info: fix capitalization (#123141) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/recorder/strings.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/strings.json b/homeassistant/components/recorder/strings.json index f891b4d18d2..2ded6be58d6 100644 --- a/homeassistant/components/recorder/strings.json +++ b/homeassistant/components/recorder/strings.json @@ -1,11 +1,11 @@ { "system_health": { "info": { - "oldest_recorder_run": "Oldest Run Start Time", - "current_recorder_run": "Current Run Start Time", - "estimated_db_size": "Estimated Database Size (MiB)", - "database_engine": "Database Engine", - "database_version": "Database Version" + "oldest_recorder_run": "Oldest run start time", + "current_recorder_run": "Current run start time", + "estimated_db_size": "Estimated database size (MiB)", + "database_engine": "Database engine", + "database_version": "Database version" } }, "issues": { From e682d8c6e2cb08074f167c8877f04da5602a2c30 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Sun, 4 Aug 2024 04:39:41 -1000 Subject: [PATCH 1939/2411] Handle command_line missing discovery_info (#116873) * command_line: Do not lead to erroring out code indexing None or empty discovery_info * Apply suggestions from code review Co-authored-by: Franck Nijhof --------- Co-authored-by: Erik Montnemery Co-authored-by: Franck Nijhof --- homeassistant/components/command_line/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index f8e9d21cf23..a3c3d0b3e9c 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -37,6 +37,8 @@ async def async_setup_platform( ) -> None: """Find and return switches controlled by shell commands.""" + if not discovery_info: + return switches = [] discovery_info = cast(DiscoveryInfoType, discovery_info) entities: dict[str, dict[str, Any]] = { From 30f4d1b9581827a26b480bec5b210093f5b93d62 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 4 Aug 2024 20:18:19 +0200 Subject: [PATCH 1940/2411] Fix implicit-return in overkiz (#123000) --- .../atlantic_pass_apc_zone_control_zone.py | 9 ++++++--- .../somfy_heating_temperature_interface.py | 3 ++- .../overkiz/water_heater_entities/hitachi_dhw.py | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py index f18edd0cfe6..9027dcf8d03 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py @@ -234,7 +234,8 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): """Set new target hvac mode.""" if self.is_using_derogated_temperature_fallback: - return await super().async_set_hvac_mode(hvac_mode) + await super().async_set_hvac_mode(hvac_mode) + return # They are mainly managed by the Zone Control device # However, it make sense to map the OFF Mode to the Overkiz STOP Preset @@ -287,7 +288,8 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): """Set new preset mode.""" if self.is_using_derogated_temperature_fallback: - return await super().async_set_preset_mode(preset_mode) + await super().async_set_preset_mode(preset_mode) + return mode = PRESET_MODES_TO_OVERKIZ[preset_mode] @@ -361,7 +363,8 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): """Set new temperature.""" if self.is_using_derogated_temperature_fallback: - return await super().async_set_temperature(**kwargs) + await super().async_set_temperature(**kwargs) + return target_temperature = kwargs.get(ATTR_TEMPERATURE) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) diff --git a/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py b/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py index 85ce7ae57e3..acc761664ec 100644 --- a/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py +++ b/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py @@ -181,6 +181,7 @@ class SomfyHeatingTemperatureInterface(OverkizEntity, ClimateEntity): OverkizState.OVP_HEATING_TEMPERATURE_INTERFACE_SETPOINT_MODE ] ) and mode.value_as_str: - return await self.executor.async_execute_command( + await self.executor.async_execute_command( SETPOINT_MODE_TO_OVERKIZ_COMMAND[mode.value_as_str], temperature ) + return diff --git a/homeassistant/components/overkiz/water_heater_entities/hitachi_dhw.py b/homeassistant/components/overkiz/water_heater_entities/hitachi_dhw.py index 9f0a8798233..dc2a93a8d2f 100644 --- a/homeassistant/components/overkiz/water_heater_entities/hitachi_dhw.py +++ b/homeassistant/components/overkiz/water_heater_entities/hitachi_dhw.py @@ -87,9 +87,10 @@ class HitachiDHW(OverkizEntity, WaterHeaterEntity): """Set new target operation mode.""" # Turn water heater off if operation_mode == OverkizCommandParam.OFF: - return await self.executor.async_execute_command( + await self.executor.async_execute_command( OverkizCommand.SET_CONTROL_DHW, OverkizCommandParam.STOP ) + return # Turn water heater on, when off if self.current_operation == OverkizCommandParam.OFF: From a9d8e47979583a2d21f1ba8afea8dc13c072c561 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 4 Aug 2024 23:02:41 +0200 Subject: [PATCH 1941/2411] Support `DeviceInfo.model_id` in MQTT integration (#123152) Add support for model_id --- homeassistant/components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/mixins.py | 5 +++++ homeassistant/components/mqtt/schemas.py | 2 ++ homeassistant/const.py | 2 ++ tests/components/mqtt/test_common.py | 4 ++++ 5 files changed, 14 insertions(+) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index c3efe5667ad..f4a32bbdf9d 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -267,6 +267,7 @@ DEVICE_ABBREVIATIONS = { "name": "name", "mf": "manufacturer", "mdl": "model", + "mdl_id": "model_id", "hw": "hw_version", "sw": "sw_version", "sa": "suggested_area", diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index aca88f2cb97..ce811e13a24 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -16,6 +16,7 @@ from homeassistant.const import ( ATTR_HW_VERSION, ATTR_MANUFACTURER, ATTR_MODEL, + ATTR_MODEL_ID, ATTR_NAME, ATTR_SERIAL_NUMBER, ATTR_SUGGESTED_AREA, @@ -25,6 +26,7 @@ from homeassistant.const import ( CONF_ENTITY_CATEGORY, CONF_ICON, CONF_MODEL, + CONF_MODEL_ID, CONF_NAME, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, @@ -992,6 +994,9 @@ def device_info_from_specifications( if CONF_MODEL in specifications: info[ATTR_MODEL] = specifications[CONF_MODEL] + if CONF_MODEL_ID in specifications: + info[ATTR_MODEL_ID] = specifications[CONF_MODEL_ID] + if CONF_NAME in specifications: info[ATTR_NAME] = specifications[CONF_NAME] diff --git a/homeassistant/components/mqtt/schemas.py b/homeassistant/components/mqtt/schemas.py index bbc0194a1a5..67c6b447709 100644 --- a/homeassistant/components/mqtt/schemas.py +++ b/homeassistant/components/mqtt/schemas.py @@ -9,6 +9,7 @@ from homeassistant.const import ( CONF_ENTITY_CATEGORY, CONF_ICON, CONF_MODEL, + CONF_MODEL_ID, CONF_NAME, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, @@ -112,6 +113,7 @@ MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All( ), vol.Optional(CONF_MANUFACTURER): cv.string, vol.Optional(CONF_MODEL): cv.string, + vol.Optional(CONF_MODEL_ID): cv.string, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_HW_VERSION): cv.string, vol.Optional(CONF_SERIAL_NUMBER): cv.string, diff --git a/homeassistant/const.py b/homeassistant/const.py index 389aaf8fef3..891cc0cc023 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -222,6 +222,7 @@ CONF_METHOD: Final = "method" CONF_MINIMUM: Final = "minimum" CONF_MODE: Final = "mode" CONF_MODEL: Final = "model" +CONF_MODEL_ID: Final = "model_id" CONF_MONITORED_CONDITIONS: Final = "monitored_conditions" CONF_MONITORED_VARIABLES: Final = "monitored_variables" CONF_NAME: Final = "name" @@ -565,6 +566,7 @@ ATTR_CONNECTIONS: Final = "connections" ATTR_DEFAULT_NAME: Final = "default_name" ATTR_MANUFACTURER: Final = "manufacturer" ATTR_MODEL: Final = "model" +ATTR_MODEL_ID: Final = "model_id" ATTR_SERIAL_NUMBER: Final = "serial_number" ATTR_SUGGESTED_AREA: Final = "suggested_area" ATTR_SW_VERSION: Final = "sw_version" diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 8d457d9da85..f7ebd039d1a 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -42,6 +42,7 @@ DEFAULT_CONFIG_DEVICE_INFO_ID = { "manufacturer": "Whatever", "name": "Beer", "model": "Glass", + "model_id": "XYZ001", "hw_version": "rev1", "serial_number": "1234deadbeef", "sw_version": "0.1-beta", @@ -54,6 +55,7 @@ DEFAULT_CONFIG_DEVICE_INFO_MAC = { "manufacturer": "Whatever", "name": "Beer", "model": "Glass", + "model_id": "XYZ001", "hw_version": "rev1", "serial_number": "1234deadbeef", "sw_version": "0.1-beta", @@ -999,6 +1001,7 @@ async def help_test_entity_device_info_with_identifier( assert device.manufacturer == "Whatever" assert device.name == "Beer" assert device.model == "Glass" + assert device.model_id == "XYZ001" assert device.hw_version == "rev1" assert device.sw_version == "0.1-beta" assert device.suggested_area == "default_area" @@ -1035,6 +1038,7 @@ async def help_test_entity_device_info_with_connection( assert device.manufacturer == "Whatever" assert device.name == "Beer" assert device.model == "Glass" + assert device.model_id == "XYZ001" assert device.hw_version == "rev1" assert device.sw_version == "0.1-beta" assert device.suggested_area == "default_area" From ccd157dc26cb3b514a6708bb162b1ca9c7b87783 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Aug 2024 23:03:40 +0200 Subject: [PATCH 1942/2411] Use coordinator setup method in filesize (#123139) --- homeassistant/components/filesize/coordinator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/filesize/coordinator.py b/homeassistant/components/filesize/coordinator.py index 37fba19fb4e..c0dbb14555e 100644 --- a/homeassistant/components/filesize/coordinator.py +++ b/homeassistant/components/filesize/coordinator.py @@ -46,14 +46,15 @@ class FileSizeCoordinator(DataUpdateCoordinator[dict[str, int | float | datetime def _update(self) -> os.stat_result: """Fetch file information.""" - if not hasattr(self, "path"): - self.path = self._get_full_path() - try: return self.path.stat() except OSError as error: raise UpdateFailed(f"Can not retrieve file statistics {error}") from error + async def _async_setup(self) -> None: + """Set up path.""" + self.path = await self.hass.async_add_executor_job(self._get_full_path) + async def _async_update_data(self) -> dict[str, float | int | datetime]: """Fetch file information.""" statinfo = await self.hass.async_add_executor_job(self._update) From 3353c3c20572113a1b756aebd2948c82a4009e2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Aug 2024 16:05:56 -0500 Subject: [PATCH 1943/2411] Remove unneeded formatter argument from logbook websocket_api (#123095) --- homeassistant/components/logbook/websocket_api.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index cac58971cde..b776ad6303d 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -81,7 +81,6 @@ async def _async_send_historical_events( msg_id: int, start_time: dt, end_time: dt, - formatter: Callable[[int, Any], dict[str, Any]], event_processor: EventProcessor, partial: bool, force_send: bool = False, @@ -109,7 +108,6 @@ async def _async_send_historical_events( msg_id, start_time, end_time, - formatter, event_processor, partial, ) @@ -131,7 +129,6 @@ async def _async_send_historical_events( msg_id, recent_query_start, end_time, - formatter, event_processor, partial=True, ) @@ -143,7 +140,6 @@ async def _async_send_historical_events( msg_id, start_time, recent_query_start, - formatter, event_processor, partial, ) @@ -164,7 +160,6 @@ async def _async_get_ws_stream_events( msg_id: int, start_time: dt, end_time: dt, - formatter: Callable[[int, Any], dict[str, Any]], event_processor: EventProcessor, partial: bool, ) -> tuple[bytes, dt | None]: @@ -174,7 +169,6 @@ async def _async_get_ws_stream_events( msg_id, start_time, end_time, - formatter, event_processor, partial, ) @@ -195,7 +189,6 @@ def _ws_stream_get_events( msg_id: int, start_day: dt, end_day: dt, - formatter: Callable[[int, Any], dict[str, Any]], event_processor: EventProcessor, partial: bool, ) -> tuple[bytes, dt | None]: @@ -211,7 +204,7 @@ def _ws_stream_get_events( # data in case the UI needs to show that historical # data is still loading in the future message["partial"] = True - return json_bytes(formatter(msg_id, message)), last_time + return json_bytes(messages.event_message(msg_id, message)), last_time async def _async_events_consumer( @@ -318,7 +311,6 @@ async def ws_event_stream( msg_id, start_time, end_time, - messages.event_message, event_processor, partial=False, ) @@ -385,7 +377,6 @@ async def ws_event_stream( msg_id, start_time, subscriptions_setup_complete_time, - messages.event_message, event_processor, partial=True, # Force a send since the wait for the sync task @@ -431,7 +422,6 @@ async def ws_event_stream( # we could fetch the same event twice (last_event_time or start_time) + timedelta(microseconds=1), subscriptions_setup_complete_time, - messages.event_message, event_processor, partial=False, ) From b09dd95dbdb0a04294cff4fcb5edfb13f61cd8f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Aug 2024 16:09:10 -0500 Subject: [PATCH 1944/2411] Improve alignment of live logbook and historical logbook models (#123070) * Improve alignment of live logbook and historical logbook models - Make EventAsRow as NamedType which is better aligned with sqlalchemy Row - Use getitem to fetch results for both Row and EventAsRow since its an order of magnitude faster fetching sqlalchemy Row object values. * final * fix * unused * fix more tests * cleanup * reduce * tweak --- homeassistant/components/logbook/models.py | 100 ++++++++++++------ homeassistant/components/logbook/processor.py | 58 ++++++---- homeassistant/scripts/benchmark/__init__.py | 49 +-------- tests/components/logbook/common.py | 8 +- tests/components/logbook/test_init.py | 62 +++-------- tests/components/logbook/test_models.py | 16 ++- 6 files changed, 138 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/logbook/models.py b/homeassistant/components/logbook/models.py index 93fc8885f57..8fd850b26fb 100644 --- a/homeassistant/components/logbook/models.py +++ b/homeassistant/components/logbook/models.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Callable, Mapping from dataclasses import dataclass from functools import cached_property -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Final, NamedTuple, cast from sqlalchemy.engine.row import Row @@ -46,16 +46,16 @@ class LazyEventPartialState: """Init the lazy event.""" self.row = row # We need to explicitly check for the row is EventAsRow as the unhappy path - # to fetch row.data for Row is very expensive + # to fetch row[DATA_POS] for Row is very expensive if type(row) is EventAsRow: # If its an EventAsRow we can avoid the whole # json decode process as we already have the data - self.data = row.data + self.data = row[DATA_POS] return if TYPE_CHECKING: - source = cast(str, row.event_data) + source = cast(str, row[EVENT_DATA_POS]) else: - source = row.event_data + source = row[EVENT_DATA_POS] if not source: self.data = {} elif event_data := event_data_cache.get(source): @@ -68,51 +68,73 @@ class LazyEventPartialState: @cached_property def event_type(self) -> EventType[Any] | str | None: """Return the event type.""" - return self.row.event_type + return self.row[EVENT_TYPE_POS] @cached_property def entity_id(self) -> str | None: """Return the entity id.""" - return self.row.entity_id + return self.row[ENTITY_ID_POS] @cached_property def state(self) -> str | None: """Return the state.""" - return self.row.state + return self.row[STATE_POS] @cached_property def context_id(self) -> str | None: """Return the context id.""" - return bytes_to_ulid_or_none(self.row.context_id_bin) + return bytes_to_ulid_or_none(self.row[CONTEXT_ID_BIN_POS]) @cached_property def context_user_id(self) -> str | None: """Return the context user id.""" - return bytes_to_uuid_hex_or_none(self.row.context_user_id_bin) + return bytes_to_uuid_hex_or_none(self.row[CONTEXT_USER_ID_BIN_POS]) @cached_property def context_parent_id(self) -> str | None: """Return the context parent id.""" - return bytes_to_ulid_or_none(self.row.context_parent_id_bin) + return bytes_to_ulid_or_none(self.row[CONTEXT_PARENT_ID_BIN_POS]) -@dataclass(slots=True, frozen=True) -class EventAsRow: - """Convert an event to a row.""" +# Row order must match the query order in queries/common.py +# --------------------------------------------------------- +ROW_ID_POS: Final = 0 +EVENT_TYPE_POS: Final = 1 +EVENT_DATA_POS: Final = 2 +TIME_FIRED_TS_POS: Final = 3 +CONTEXT_ID_BIN_POS: Final = 4 +CONTEXT_USER_ID_BIN_POS: Final = 5 +CONTEXT_PARENT_ID_BIN_POS: Final = 6 +STATE_POS: Final = 7 +ENTITY_ID_POS: Final = 8 +ICON_POS: Final = 9 +CONTEXT_ONLY_POS: Final = 10 +# - For EventAsRow, additional fields are: +DATA_POS: Final = 11 +CONTEXT_POS: Final = 12 + +class EventAsRow(NamedTuple): + """Convert an event to a row. + + This much always match the order of the columns in queries/common.py + """ + + row_id: int + event_type: EventType[Any] | str | None + event_data: str | None + time_fired_ts: float + context_id_bin: bytes + context_user_id_bin: bytes | None + context_parent_id_bin: bytes | None + state: str | None + entity_id: str | None + icon: str | None + context_only: bool | None + + # Additional fields for EventAsRow data: Mapping[str, Any] context: Context - context_id_bin: bytes - time_fired_ts: float - row_id: int - event_data: str | None = None - entity_id: str | None = None - icon: str | None = None - context_user_id_bin: bytes | None = None - context_parent_id_bin: bytes | None = None - event_type: EventType[Any] | str | None = None - state: str | None = None - context_only: None = None @callback @@ -121,14 +143,19 @@ def async_event_to_row(event: Event) -> EventAsRow: if event.event_type != EVENT_STATE_CHANGED: context = event.context return EventAsRow( - data=event.data, - context=event.context, + row_id=hash(event), event_type=event.event_type, + event_data=None, + time_fired_ts=event.time_fired_timestamp, context_id_bin=ulid_to_bytes(context.id), context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_id), context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id), - time_fired_ts=event.time_fired_timestamp, - row_id=hash(event), + state=None, + entity_id=None, + icon=None, + context_only=None, + data=event.data, + context=context, ) # States are prefiltered so we never get states # that are missing new_state or old_state @@ -136,14 +163,17 @@ def async_event_to_row(event: Event) -> EventAsRow: new_state: State = event.data["new_state"] context = new_state.context return EventAsRow( - data=event.data, - context=event.context, - entity_id=new_state.entity_id, - state=new_state.state, + row_id=hash(event), + event_type=None, + event_data=None, + time_fired_ts=new_state.last_updated_timestamp, context_id_bin=ulid_to_bytes(context.id), context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_id), context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id), - time_fired_ts=new_state.last_updated_timestamp, - row_id=hash(event), + state=new_state.state, + entity_id=new_state.entity_id, icon=new_state.attributes.get(ATTR_ICON), + context_only=None, + data=event.data, + context=context, ) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 28f98bc2ce9..8d577089ea4 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -6,6 +6,7 @@ from collections.abc import Callable, Generator, Sequence from dataclasses import dataclass from datetime import datetime as dt import logging +import time from typing import Any from sqlalchemy.engine import Result @@ -17,7 +18,6 @@ from homeassistant.components.recorder.models import ( bytes_to_uuid_hex_or_none, extract_event_type_ids, extract_metadata_ids, - process_datetime_to_timestamp, process_timestamp_to_utc_isoformat, ) from homeassistant.components.recorder.util import ( @@ -62,7 +62,23 @@ from .const import ( LOGBOOK_ENTRY_WHEN, ) from .helpers import is_sensor_continuous -from .models import EventAsRow, LazyEventPartialState, LogbookConfig, async_event_to_row +from .models import ( + CONTEXT_ID_BIN_POS, + CONTEXT_ONLY_POS, + CONTEXT_PARENT_ID_BIN_POS, + CONTEXT_POS, + CONTEXT_USER_ID_BIN_POS, + ENTITY_ID_POS, + EVENT_TYPE_POS, + ICON_POS, + ROW_ID_POS, + STATE_POS, + TIME_FIRED_TS_POS, + EventAsRow, + LazyEventPartialState, + LogbookConfig, + async_event_to_row, +) from .queries import statement_for_request from .queries.common import PSEUDO_EVENT_STATE_CHANGED @@ -206,17 +222,17 @@ def _humanify( # Process rows for row in rows: - context_id_bin: bytes = row.context_id_bin + context_id_bin: bytes = row[CONTEXT_ID_BIN_POS] if memoize_new_contexts and context_id_bin not in context_lookup: context_lookup[context_id_bin] = row - if row.context_only: + if row[CONTEXT_ONLY_POS]: continue - event_type = row.event_type + event_type = row[EVENT_TYPE_POS] if event_type == EVENT_CALL_SERVICE: continue if event_type is PSEUDO_EVENT_STATE_CHANGED: - entity_id = row.entity_id + entity_id = row[ENTITY_ID_POS] assert entity_id is not None # Skip continuous sensors if ( @@ -229,12 +245,12 @@ def _humanify( data = { LOGBOOK_ENTRY_WHEN: format_time(row), - LOGBOOK_ENTRY_STATE: row.state, + LOGBOOK_ENTRY_STATE: row[STATE_POS], LOGBOOK_ENTRY_ENTITY_ID: entity_id, } if include_entity_name: data[LOGBOOK_ENTRY_NAME] = entity_name_cache.get(entity_id) - if icon := row.icon: + if icon := row[ICON_POS]: data[LOGBOOK_ENTRY_ICON] = icon context_augmenter.augment(data, row, context_id_bin) @@ -292,9 +308,11 @@ class ContextAugmenter: context_row := self.context_lookup.get(context_id_bin) ): return context_row - if (context := getattr(row, "context", None)) is not None and ( - origin_event := context.origin_event - ) is not None: + if ( + type(row) is EventAsRow + and (context := row[CONTEXT_POS]) is not None + and (origin_event := context.origin_event) is not None + ): return async_event_to_row(origin_event) return None @@ -302,7 +320,7 @@ class ContextAugmenter: self, data: dict[str, Any], row: Row | EventAsRow, context_id_bin: bytes | None ) -> None: """Augment data from the row and cache.""" - if context_user_id_bin := row.context_user_id_bin: + if context_user_id_bin := row[CONTEXT_USER_ID_BIN_POS]: data[CONTEXT_USER_ID] = bytes_to_uuid_hex_or_none(context_user_id_bin) if not (context_row := self._get_context_row(context_id_bin, row)): @@ -311,7 +329,7 @@ class ContextAugmenter: if _rows_match(row, context_row): # This is the first event with the given ID. Was it directly caused by # a parent event? - context_parent_id_bin = row.context_parent_id_bin + context_parent_id_bin = row[CONTEXT_PARENT_ID_BIN_POS] if ( not context_parent_id_bin or ( @@ -326,10 +344,10 @@ class ContextAugmenter: # this log entry. if _rows_match(row, context_row): return - event_type = context_row.event_type + event_type = context_row[EVENT_TYPE_POS] # State change - if context_entity_id := context_row.entity_id: - data[CONTEXT_STATE] = context_row.state + if context_entity_id := context_row[ENTITY_ID_POS]: + data[CONTEXT_STATE] = context_row[STATE_POS] data[CONTEXT_ENTITY_ID] = context_entity_id if self.include_entity_name: data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get( @@ -375,20 +393,22 @@ class ContextAugmenter: def _rows_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool: """Check of rows match by using the same method as Events __hash__.""" return bool( - row is other_row or (row_id := row.row_id) and row_id == other_row.row_id + row is other_row + or (row_id := row[ROW_ID_POS]) + and row_id == other_row[ROW_ID_POS] ) def _row_time_fired_isoformat(row: Row | EventAsRow) -> str: """Convert the row timed_fired to isoformat.""" return process_timestamp_to_utc_isoformat( - dt_util.utc_from_timestamp(row.time_fired_ts) or dt_util.utcnow() + dt_util.utc_from_timestamp(row[TIME_FIRED_TS_POS]) or dt_util.utcnow() ) def _row_time_fired_timestamp(row: Row | EventAsRow) -> float: """Convert the row timed_fired to timestamp.""" - return row.time_fired_ts or process_datetime_to_timestamp(dt_util.utcnow()) + return row[TIME_FIRED_TS_POS] or time.time() class EntityNameCache: diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index d39b1b64861..b769d385a4f 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -4,10 +4,8 @@ from __future__ import annotations import argparse import asyncio -import collections from collections.abc import Callable from contextlib import suppress -import json import logging from timeit import default_timer as timer @@ -18,7 +16,7 @@ from homeassistant.helpers.event import ( async_track_state_change, async_track_state_change_event, ) -from homeassistant.helpers.json import JSON_DUMP, JSONEncoder +from homeassistant.helpers.json import JSON_DUMP # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # mypy: no-warn-return-any @@ -310,48 +308,3 @@ async def json_serialize_states(hass): start = timer() JSON_DUMP(states) return timer() - start - - -def _create_state_changed_event_from_old_new( - entity_id, event_time_fired, old_state, new_state -): - """Create a state changed event from a old and new state.""" - attributes = {} - if new_state is not None: - attributes = new_state.get("attributes") - attributes_json = json.dumps(attributes, cls=JSONEncoder) - if attributes_json == "null": - attributes_json = "{}" - row = collections.namedtuple( # noqa: PYI024 - "Row", - [ - "event_type" - "event_data" - "time_fired" - "context_id" - "context_user_id" - "state" - "entity_id" - "domain" - "attributes" - "state_id", - "old_state_id", - ], - ) - - row.event_type = EVENT_STATE_CHANGED - row.event_data = "{}" - row.attributes = attributes_json - row.time_fired = event_time_fired - row.state = new_state and new_state.get("state") - row.entity_id = entity_id - row.domain = entity_id and core.split_entity_id(entity_id)[0] - row.context_id = None - row.context_user_id = None - row.old_state_id = old_state and 1 - row.state_id = new_state and 1 - - # pylint: disable-next=import-outside-toplevel - from homeassistant.components import logbook - - return logbook.LazyEventPartialState(row, {}) diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py index 67f12955581..c55b6230418 100644 --- a/tests/components/logbook/common.py +++ b/tests/components/logbook/common.py @@ -7,7 +7,7 @@ from typing import Any from homeassistant.components import logbook from homeassistant.components.logbook import processor -from homeassistant.components.logbook.models import LogbookConfig +from homeassistant.components.logbook.models import EventAsRow, LogbookConfig from homeassistant.components.recorder.models import ( process_timestamp_to_utc_isoformat, ulid_to_bytes_or_none, @@ -18,6 +18,8 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util +IDX_TO_NAME = dict(enumerate(EventAsRow._fields)) + class MockRow: """Minimal row mock.""" @@ -48,6 +50,10 @@ class MockRow: self.attributes = None self.context_only = False + def __getitem__(self, idx: int) -> Any: + """Get item.""" + return getattr(self, IDX_TO_NAME[idx]) + @property def time_fired_minute(self): """Minute the event was fired.""" diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 34052cd8024..3a20aac2602 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -1,11 +1,9 @@ """The tests for the logbook component.""" import asyncio -import collections from collections.abc import Callable from datetime import datetime, timedelta from http import HTTPStatus -import json from unittest.mock import Mock from freezegun import freeze_time @@ -15,7 +13,7 @@ import voluptuous as vol from homeassistant.components import logbook, recorder from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED -from homeassistant.components.logbook.models import LazyEventPartialState +from homeassistant.components.logbook.models import EventAsRow, LazyEventPartialState from homeassistant.components.logbook.processor import EventProcessor from homeassistant.components.logbook.queries.common import PSEUDO_EVENT_STATE_CHANGED from homeassistant.components.recorder import Recorder @@ -44,7 +42,6 @@ import homeassistant.core as ha from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS -from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -324,50 +321,21 @@ def create_state_changed_event_from_old_new( entity_id, event_time_fired, old_state, new_state ): """Create a state changed event from a old and new state.""" - attributes = {} - if new_state is not None: - attributes = new_state.get("attributes") - attributes_json = json.dumps(attributes, cls=JSONEncoder) - row = collections.namedtuple( # noqa: PYI024 - "Row", - [ - "event_type", - "event_data", - "time_fired", - "time_fired_ts", - "context_id_bin", - "context_user_id_bin", - "context_parent_id_bin", - "state", - "entity_id", - "domain", - "attributes", - "state_id", - "old_state_id", - "shared_attrs", - "shared_data", - "context_only", - ], + row = EventAsRow( + row_id=1, + event_type=PSEUDO_EVENT_STATE_CHANGED, + event_data="{}", + time_fired_ts=dt_util.utc_to_timestamp(event_time_fired), + context_id_bin=None, + context_user_id_bin=None, + context_parent_id_bin=None, + state=new_state and new_state.get("state"), + entity_id=entity_id, + icon=None, + context_only=False, + data=None, + context=None, ) - - row.event_type = PSEUDO_EVENT_STATE_CHANGED - row.event_data = "{}" - row.shared_data = "{}" - row.attributes = attributes_json - row.shared_attrs = attributes_json - row.time_fired = event_time_fired - row.time_fired_ts = dt_util.utc_to_timestamp(event_time_fired) - row.state = new_state and new_state.get("state") - row.entity_id = entity_id - row.domain = entity_id and ha.split_entity_id(entity_id)[0] - row.context_only = False - row.context_id_bin = None - row.friendly_name = None - row.icon = None - row.context_user_id_bin = None - row.context_parent_id_bin = None - row.old_state_id = old_state and 1 - row.state_id = new_state and 1 return LazyEventPartialState(row, {}) diff --git a/tests/components/logbook/test_models.py b/tests/components/logbook/test_models.py index 7021711014f..cfdd7efc727 100644 --- a/tests/components/logbook/test_models.py +++ b/tests/components/logbook/test_models.py @@ -2,20 +2,26 @@ from unittest.mock import Mock -from homeassistant.components.logbook.models import LazyEventPartialState +from homeassistant.components.logbook.models import EventAsRow, LazyEventPartialState def test_lazy_event_partial_state_context() -> None: """Test we can extract context from a lazy event partial state.""" state = LazyEventPartialState( - Mock( + EventAsRow( + row_id=1, + event_type="event_type", + event_data={}, + time_fired_ts=1, context_id_bin=b"1234123412341234", context_user_id_bin=b"1234123412341234", context_parent_id_bin=b"4444444444444444", - event_data={}, - event_type="event_type", - entity_id="entity_id", state="state", + entity_id="entity_id", + icon="icon", + context_only=False, + data={}, + context=Mock(), ), {}, ) From 6b7307df81b6918a04c9167d57ae6a266552a7d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Aug 2024 16:43:43 -0500 Subject: [PATCH 1945/2411] Speed up logbook timestamp processing (#123126) --- homeassistant/components/logbook/processor.py | 36 ++++++++----------- tests/components/logbook/common.py | 2 +- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 8d577089ea4..05566e52509 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -97,7 +97,7 @@ class LogbookRun: event_cache: EventCache entity_name_cache: EntityNameCache include_entity_name: bool - format_time: Callable[[Row | EventAsRow], Any] + timestamp: bool memoize_new_contexts: bool = True @@ -126,16 +126,13 @@ class EventProcessor: self.context_id = context_id logbook_config: LogbookConfig = hass.data[DOMAIN] self.filters: Filters | None = logbook_config.sqlalchemy_filter - format_time = ( - _row_time_fired_timestamp if timestamp else _row_time_fired_isoformat - ) self.logbook_run = LogbookRun( context_lookup={None: None}, external_events=logbook_config.external_events, event_cache=EventCache({}), entity_name_cache=EntityNameCache(self.hass), include_entity_name=include_entity_name, - format_time=format_time, + timestamp=timestamp, ) self.context_augmenter = ContextAugmenter(self.logbook_run) @@ -217,7 +214,7 @@ def _humanify( event_cache = logbook_run.event_cache entity_name_cache = logbook_run.entity_name_cache include_entity_name = logbook_run.include_entity_name - format_time = logbook_run.format_time + timestamp = logbook_run.timestamp memoize_new_contexts = logbook_run.memoize_new_contexts # Process rows @@ -231,6 +228,15 @@ def _humanify( if event_type == EVENT_CALL_SERVICE: continue + + time_fired_ts = row[TIME_FIRED_TS_POS] + if timestamp: + when = time_fired_ts or time.time() + else: + when = process_timestamp_to_utc_isoformat( + dt_util.utc_from_timestamp(time_fired_ts) or dt_util.utcnow() + ) + if event_type is PSEUDO_EVENT_STATE_CHANGED: entity_id = row[ENTITY_ID_POS] assert entity_id is not None @@ -244,7 +250,7 @@ def _humanify( continue data = { - LOGBOOK_ENTRY_WHEN: format_time(row), + LOGBOOK_ENTRY_WHEN: when, LOGBOOK_ENTRY_STATE: row[STATE_POS], LOGBOOK_ENTRY_ENTITY_ID: entity_id, } @@ -265,7 +271,7 @@ def _humanify( "Error with %s describe event for %s", domain, event_type ) continue - data[LOGBOOK_ENTRY_WHEN] = format_time(row) + data[LOGBOOK_ENTRY_WHEN] = when data[LOGBOOK_ENTRY_DOMAIN] = domain context_augmenter.augment(data, row, context_id_bin) yield data @@ -279,7 +285,7 @@ def _humanify( if entry_domain is None and entry_entity_id is not None: entry_domain = split_entity_id(str(entry_entity_id))[0] data = { - LOGBOOK_ENTRY_WHEN: format_time(row), + LOGBOOK_ENTRY_WHEN: when, LOGBOOK_ENTRY_NAME: event_data.get(ATTR_NAME), LOGBOOK_ENTRY_MESSAGE: event_data.get(ATTR_MESSAGE), LOGBOOK_ENTRY_DOMAIN: entry_domain, @@ -399,18 +405,6 @@ def _rows_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool: ) -def _row_time_fired_isoformat(row: Row | EventAsRow) -> str: - """Convert the row timed_fired to isoformat.""" - return process_timestamp_to_utc_isoformat( - dt_util.utc_from_timestamp(row[TIME_FIRED_TS_POS]) or dt_util.utcnow() - ) - - -def _row_time_fired_timestamp(row: Row | EventAsRow) -> float: - """Convert the row timed_fired to timestamp.""" - return row[TIME_FIRED_TS_POS] or time.time() - - class EntityNameCache: """A cache to lookup the name for an entity. diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py index c55b6230418..afa8b7fcde5 100644 --- a/tests/components/logbook/common.py +++ b/tests/components/logbook/common.py @@ -79,7 +79,7 @@ def mock_humanify(hass_, rows): event_cache, entity_name_cache, include_entity_name=True, - format_time=processor._row_time_fired_isoformat, + timestamp=False, ) context_augmenter = processor.ContextAugmenter(logbook_run) return list( From 4fd92c17f077bf842d054a48c2af6ecc48722b74 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Aug 2024 17:06:32 -0500 Subject: [PATCH 1946/2411] Optimize logbook row matching (#123127) --- homeassistant/components/logbook/processor.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 05566e52509..426f33fadb2 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -332,7 +332,7 @@ class ContextAugmenter: if not (context_row := self._get_context_row(context_id_bin, row)): return - if _rows_match(row, context_row): + if row is context_row or _rows_ids_match(row, context_row): # This is the first event with the given ID. Was it directly caused by # a parent event? context_parent_id_bin = row[CONTEXT_PARENT_ID_BIN_POS] @@ -348,7 +348,7 @@ class ContextAugmenter: return # Ensure the (parent) context_event exists and is not the root cause of # this log entry. - if _rows_match(row, context_row): + if row is context_row or _rows_ids_match(row, context_row): return event_type = context_row[EVENT_TYPE_POS] # State change @@ -396,13 +396,9 @@ class ContextAugmenter: data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get(attr_entity_id) -def _rows_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool: +def _rows_ids_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool: """Check of rows match by using the same method as Events __hash__.""" - return bool( - row is other_row - or (row_id := row[ROW_ID_POS]) - and row_id == other_row[ROW_ID_POS] - ) + return bool((row_id := row[ROW_ID_POS]) and row_id == other_row[ROW_ID_POS]) class EntityNameCache: From 4d103c1fc24dc81011bf46cc3d0425686b47a67b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Aug 2024 01:39:04 -0500 Subject: [PATCH 1947/2411] Bump aiohttp to 3.10.1 (#123159) --- 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 1cc6a0fa85d..3b251e91179 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-zlib==0.1.1 -aiohttp==3.10.0 +aiohttp==3.10.1 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index 93f8427ef7f..49346f90448 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.10.0", + "aiohttp==3.10.1", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index c851927f9c6..1beefe73914 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.10.0 +aiohttp==3.10.1 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 From 7308912b391ad9feeea05bd97a7e782f3d1b32a1 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Mon, 5 Aug 2024 08:43:29 +0200 Subject: [PATCH 1948/2411] Catch exception in coordinator setup of IronOS integration (#123079) --- .../components/iron_os/coordinator.py | 6 ++++- tests/components/iron_os/test_init.py | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/components/iron_os/test_init.py diff --git a/homeassistant/components/iron_os/coordinator.py b/homeassistant/components/iron_os/coordinator.py index e8424478d86..aefb14b689b 100644 --- a/homeassistant/components/iron_os/coordinator.py +++ b/homeassistant/components/iron_os/coordinator.py @@ -46,4 +46,8 @@ class IronOSCoordinator(DataUpdateCoordinator[LiveDataResponse]): async def _async_setup(self) -> None: """Set up the coordinator.""" - self.device_info = await self.device.get_device_info() + try: + self.device_info = await self.device.get_device_info() + + except CommunicationError as e: + raise UpdateFailed("Cannot connect to device") from e diff --git a/tests/components/iron_os/test_init.py b/tests/components/iron_os/test_init.py new file mode 100644 index 00000000000..fb0a782ea36 --- /dev/null +++ b/tests/components/iron_os/test_init.py @@ -0,0 +1,26 @@ +"""Test init of IronOS integration.""" + +from unittest.mock import AsyncMock + +from pynecil import CommunicationError +import pytest + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("ble_device") +async def test_setup_config_entry_not_ready( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, +) -> None: + """Test config entry not ready.""" + mock_pynecil.get_device_info.side_effect = CommunicationError + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY From 31fd4efa368f5f05129c1493debcff1b5fb236c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 09:21:37 +0200 Subject: [PATCH 1949/2411] Bump actions/upload-artifact from 4.3.4 to 4.3.5 (#123170) --- .github/workflows/builder.yml | 2 +- .github/workflows/ci.yaml | 20 ++++++++++---------- .github/workflows/wheels.yml | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index d0edc631762..7e3597e7289 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -69,7 +69,7 @@ jobs: run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - - name: Upload translations - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: translations path: translations.tar.gz diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 23a8b10c0d3..8dfdb2fd7af 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -624,7 +624,7 @@ jobs: . venv/bin/activate pip-licenses --format=json --output-file=licenses.json - name: Upload licenses - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: licenses path: licenses.json @@ -834,7 +834,7 @@ jobs: . venv/bin/activate python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests - name: Upload pytest_buckets - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: pytest_buckets path: pytest_buckets.txt @@ -935,14 +935,14 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-full.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml @@ -1061,7 +1061,7 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -1069,7 +1069,7 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -1188,7 +1188,7 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1196,7 +1196,7 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1330,14 +1330,14 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 697535172d7..6c9cb07a180 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -82,14 +82,14 @@ jobs: ) > .env_file - name: Upload env_file - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: env_file path: ./.env_file overwrite: true - name: Upload requirements_diff - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: requirements_diff path: ./requirements_diff.txt @@ -101,7 +101,7 @@ jobs: python -m script.gen_requirements_all ci - name: Upload requirements_all_wheels - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: requirements_all_wheels path: ./requirements_all_wheels_*.txt From d7e3df1974221957d3cbe7a36b1599e031e89a40 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 5 Aug 2024 17:59:33 +1000 Subject: [PATCH 1950/2411] Fix class attribute condition in Tesla Fleet (#123162) --- homeassistant/components/tesla_fleet/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 2c5ee1b5c75..8257bf75cd0 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -86,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - vehicles: list[TeslaFleetVehicleData] = [] energysites: list[TeslaFleetEnergyData] = [] for product in products: - if "vin" in product and tesla.vehicle: + if "vin" in product and hasattr(tesla, "vehicle"): # Remove the protobuff 'cached_data' that we do not use to save memory product.pop("cached_data", None) vin = product["vin"] @@ -111,7 +111,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - device=device, ) ) - elif "energy_site_id" in product and tesla.energy: + elif "energy_site_id" in product and hasattr(tesla, "energy"): site_id = product["energy_site_id"] if not ( product["components"]["battery"] From b45fe0ec7351de8c463c699bcae1686cc4072360 Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Mon, 5 Aug 2024 04:02:15 -0400 Subject: [PATCH 1951/2411] Add Govee H612B to the Matter transition blocklist (#123163) --- homeassistant/components/matter/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 65c3a535216..d05a7c85f9d 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -54,6 +54,7 @@ TRANSITION_BLOCKLIST = ( (4488, 514, "1.0", "1.0.0"), (4488, 260, "1.0", "1.0.0"), (5010, 769, "3.0", "1.0.0"), + (4999, 24875, "1.0", "27.0"), (4999, 25057, "1.0", "27.0"), (4448, 36866, "V1", "V1.0.0.5"), (5009, 514, "1.0", "1.0.0"), From a7fbac51851a0481d7e5b4eb0e7390bde709909b Mon Sep 17 00:00:00 2001 From: dupondje Date: Mon, 5 Aug 2024 10:32:58 +0200 Subject: [PATCH 1952/2411] dsmr: migrate hourly_gas_meter_reading to mbus device (#123149) --- homeassistant/components/dsmr/sensor.py | 4 +- tests/components/dsmr/test_mbus_migration.py | 100 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index f794d1d05e9..b298ed5bfc0 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -439,7 +439,9 @@ def rename_old_gas_to_mbus( entries = er.async_entries_for_device(ent_reg, device_id) for entity in entries: - if entity.unique_id.endswith("belgium_5min_gas_meter_reading"): + if entity.unique_id.endswith( + "belgium_5min_gas_meter_reading" + ) or entity.unique_id.endswith("hourly_gas_meter_reading"): try: ent_reg.async_update_entity( entity.entity_id, diff --git a/tests/components/dsmr/test_mbus_migration.py b/tests/components/dsmr/test_mbus_migration.py index a28bc2c3a33..20b3d253f39 100644 --- a/tests/components/dsmr/test_mbus_migration.py +++ b/tests/components/dsmr/test_mbus_migration.py @@ -119,6 +119,106 @@ async def test_migrate_gas_to_mbus( ) +async def test_migrate_hourly_gas_to_mbus( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], +) -> None: + """Test migration of unique_id.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="/dev/ttyUSB0", + data={ + "port": "/dev/ttyUSB0", + "dsmr_version": "5", + "serial_id": "1234", + "serial_id_gas": "4730303738353635363037343639323231", + }, + options={ + "time_between_update": 0, + }, + ) + + mock_entry.add_to_hass(hass) + + old_unique_id = "4730303738353635363037343639323231_hourly_gas_meter_reading" + + device = device_registry.async_get_or_create( + config_entry_id=mock_entry.entry_id, + identifiers={(DOMAIN, mock_entry.entry_id)}, + name="Gas Meter", + ) + await hass.async_block_till_done() + + entity: er.RegistryEntry = entity_registry.async_get_or_create( + suggested_object_id="gas_meter_reading", + disabled_by=None, + domain=SENSOR_DOMAIN, + platform=DOMAIN, + device_id=device.id, + unique_id=old_unique_id, + config_entry=mock_entry, + ) + assert entity.unique_id == old_unique_id + await hass.async_block_till_done() + + telegram = Telegram() + telegram.add( + MBUS_DEVICE_TYPE, + CosemObject((0, 1), [{"value": "003", "unit": ""}]), + "MBUS_DEVICE_TYPE", + ) + telegram.add( + MBUS_EQUIPMENT_IDENTIFIER, + CosemObject( + (0, 1), + [{"value": "4730303738353635363037343639323231", "unit": ""}], + ), + "MBUS_EQUIPMENT_IDENTIFIER", + ) + telegram.add( + MBUS_METER_READING, + MBusObject( + (0, 1), + [ + {"value": datetime.datetime.fromtimestamp(1722749707)}, + {"value": Decimal(778.963), "unit": "m3"}, + ], + ), + "MBUS_METER_READING", + ) + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to be created + await hass.async_block_till_done() + + dev_entities = er.async_entries_for_device( + entity_registry, device.id, include_disabled_entities=True + ) + assert not dev_entities + + 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, "4730303738353635363037343639323231" + ) + == "sensor.gas_meter_reading" + ) + + async def test_migrate_gas_to_mbus_exists( hass: HomeAssistant, entity_registry: er.EntityRegistry, From d246d02ab8d028439ab56681657a6127c1589da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B6rrle?= <7945681+CM000n@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:08:27 +0200 Subject: [PATCH 1953/2411] Add apsystems diagnostic binary sensors (#123045) * add diagnostic sensors * select output_data from data * split sensor and binary_sensor configurations * adjust module description * convert values to bool * add strings * add tests * add tests * update translations * remove already available _attr_has_entity_name * use dataclass instead of TypedDict * Update tests/components/apsystems/test_binary_sensor.py --------- Co-authored-by: Joost Lekkerkerker --- .../components/apsystems/__init__.py | 7 +- .../components/apsystems/binary_sensor.py | 102 +++++ .../components/apsystems/coordinator.py | 19 +- homeassistant/components/apsystems/sensor.py | 2 +- .../components/apsystems/strings.json | 14 + tests/components/apsystems/conftest.py | 8 +- .../snapshots/test_binary_sensor.ambr | 377 ++++++++++++++++++ .../apsystems/test_binary_sensor.py | 31 ++ 8 files changed, 553 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/apsystems/binary_sensor.py create mode 100644 tests/components/apsystems/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/apsystems/test_binary_sensor.py diff --git a/homeassistant/components/apsystems/__init__.py b/homeassistant/components/apsystems/__init__.py index 91650201a87..372ce52e049 100644 --- a/homeassistant/components/apsystems/__init__.py +++ b/homeassistant/components/apsystems/__init__.py @@ -13,7 +13,12 @@ from homeassistant.core import HomeAssistant from .const import DEFAULT_PORT from .coordinator import ApSystemsDataCoordinator -PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.NUMBER, + Platform.SENSOR, + Platform.SWITCH, +] @dataclass diff --git a/homeassistant/components/apsystems/binary_sensor.py b/homeassistant/components/apsystems/binary_sensor.py new file mode 100644 index 00000000000..528203dc2d9 --- /dev/null +++ b/homeassistant/components/apsystems/binary_sensor.py @@ -0,0 +1,102 @@ +"""The read-only binary sensors for APsystems local API integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from APsystemsEZ1 import ReturnAlarmInfo + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import ApSystemsConfigEntry, ApSystemsData +from .coordinator import ApSystemsDataCoordinator +from .entity import ApSystemsEntity + + +@dataclass(frozen=True, kw_only=True) +class ApsystemsLocalApiBinarySensorDescription(BinarySensorEntityDescription): + """Describes Apsystens Inverter binary sensor entity.""" + + is_on: Callable[[ReturnAlarmInfo], bool | None] + + +BINARY_SENSORS: tuple[ApsystemsLocalApiBinarySensorDescription, ...] = ( + ApsystemsLocalApiBinarySensorDescription( + key="off_grid_status", + translation_key="off_grid_status", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + is_on=lambda c: bool(c.og), + ), + ApsystemsLocalApiBinarySensorDescription( + key="dc_1_short_circuit_error_status", + translation_key="dc_1_short_circuit_error_status", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + is_on=lambda c: bool(c.isce1), + ), + ApsystemsLocalApiBinarySensorDescription( + key="dc_2_short_circuit_error_status", + translation_key="dc_2_short_circuit_error_status", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + is_on=lambda c: bool(c.isce2), + ), + ApsystemsLocalApiBinarySensorDescription( + key="output_fault_status", + translation_key="output_fault_status", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + is_on=lambda c: bool(c.oe), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ApSystemsConfigEntry, + add_entities: AddEntitiesCallback, +) -> None: + """Set up the binary sensor platform.""" + config = config_entry.runtime_data + + add_entities( + ApSystemsBinarySensorWithDescription( + data=config, + entity_description=desc, + ) + for desc in BINARY_SENSORS + ) + + +class ApSystemsBinarySensorWithDescription( + CoordinatorEntity[ApSystemsDataCoordinator], ApSystemsEntity, BinarySensorEntity +): + """Base binary sensor to be used with description.""" + + entity_description: ApsystemsLocalApiBinarySensorDescription + + def __init__( + self, + data: ApSystemsData, + entity_description: ApsystemsLocalApiBinarySensorDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(data.coordinator) + ApSystemsEntity.__init__(self, data) + self.entity_description = entity_description + self._attr_unique_id = f"{data.device_id}_{entity_description.key}" + + @property + def is_on(self) -> bool | None: + """Return value of sensor.""" + return self.entity_description.is_on(self.coordinator.data.alarm_info) diff --git a/homeassistant/components/apsystems/coordinator.py b/homeassistant/components/apsystems/coordinator.py index f2d076ce3fd..96956bafc3e 100644 --- a/homeassistant/components/apsystems/coordinator.py +++ b/homeassistant/components/apsystems/coordinator.py @@ -2,9 +2,10 @@ from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta -from APsystemsEZ1 import APsystemsEZ1M, ReturnOutputData +from APsystemsEZ1 import APsystemsEZ1M, ReturnAlarmInfo, ReturnOutputData from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -12,7 +13,15 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import LOGGER -class ApSystemsDataCoordinator(DataUpdateCoordinator[ReturnOutputData]): +@dataclass +class ApSystemsSensorData: + """Representing different Apsystems sensor data.""" + + output_data: ReturnOutputData + alarm_info: ReturnAlarmInfo + + +class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]): """Coordinator used for all sensors.""" def __init__(self, hass: HomeAssistant, api: APsystemsEZ1M) -> None: @@ -25,5 +34,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ReturnOutputData]): ) self.api = api - async def _async_update_data(self) -> ReturnOutputData: - return await self.api.get_output_data() + async def _async_update_data(self) -> ApSystemsSensorData: + output_data = await self.api.get_output_data() + alarm_info = await self.api.get_alarm_info() + return ApSystemsSensorData(output_data=output_data, alarm_info=alarm_info) diff --git a/homeassistant/components/apsystems/sensor.py b/homeassistant/components/apsystems/sensor.py index 637def4e418..afeb9d071ab 100644 --- a/homeassistant/components/apsystems/sensor.py +++ b/homeassistant/components/apsystems/sensor.py @@ -148,4 +148,4 @@ class ApSystemsSensorWithDescription( @property def native_value(self) -> StateType: """Return value of sensor.""" - return self.entity_description.value_fn(self.coordinator.data) + return self.entity_description.value_fn(self.coordinator.data.output_data) diff --git a/homeassistant/components/apsystems/strings.json b/homeassistant/components/apsystems/strings.json index 18200f7b49d..e02f86c2730 100644 --- a/homeassistant/components/apsystems/strings.json +++ b/homeassistant/components/apsystems/strings.json @@ -19,6 +19,20 @@ } }, "entity": { + "binary_sensor": { + "off_grid_status": { + "name": "Off grid status" + }, + "dc_1_short_circuit_error_status": { + "name": "DC 1 short circuit error status" + }, + "dc_2_short_circuit_error_status": { + "name": "DC 2 short circuit error status" + }, + "output_fault_status": { + "name": "Output fault status" + } + }, "sensor": { "total_power": { "name": "Total power" diff --git a/tests/components/apsystems/conftest.py b/tests/components/apsystems/conftest.py index c191c7ca2dc..7e6140e8279 100644 --- a/tests/components/apsystems/conftest.py +++ b/tests/components/apsystems/conftest.py @@ -3,7 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch -from APsystemsEZ1 import ReturnDeviceInfo, ReturnOutputData, Status +from APsystemsEZ1 import ReturnAlarmInfo, ReturnDeviceInfo, ReturnOutputData, Status import pytest from homeassistant.components.apsystems.const import DOMAIN @@ -52,6 +52,12 @@ def mock_apsystems() -> Generator[MagicMock]: e2=6.0, te2=7.0, ) + mock_api.get_alarm_info.return_value = ReturnAlarmInfo( + og=Status.normal, + isce1=Status.alarm, + isce2=Status.normal, + oe=Status.alarm, + ) mock_api.get_device_power_status.return_value = Status.normal yield mock_api diff --git a/tests/components/apsystems/snapshots/test_binary_sensor.ambr b/tests/components/apsystems/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..bb06b019f31 --- /dev/null +++ b/tests/components/apsystems/snapshots/test_binary_sensor.ambr @@ -0,0 +1,377 @@ +# serializer version: 1 +# name: test_all_entities[binary_sensor.mock_title_dc_1_short_circuit_error_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mock_title_dc_1_short_circuit_error_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC 1 short circuit error status', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dc_1_short_circuit_error_status', + 'unique_id': 'MY_SERIAL_NUMBER_dc_1_short_circuit_error_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_dc_1_short_circuit_error_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title DC 1 short circuit error status', + }), + 'context': , + 'entity_id': 'binary_sensor.mock_title_dc_1_short_circuit_error_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_dc_2_short_circuit_error_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mock_title_dc_2_short_circuit_error_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC 2 short circuit error status', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dc_2_short_circuit_error_status', + 'unique_id': 'MY_SERIAL_NUMBER_dc_2_short_circuit_error_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_dc_2_short_circuit_error_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title DC 2 short circuit error status', + }), + 'context': , + 'entity_id': 'binary_sensor.mock_title_dc_2_short_circuit_error_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_off_grid_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mock_title_off_grid_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Off grid status', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'off_grid_status', + 'unique_id': 'MY_SERIAL_NUMBER_off_grid_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_off_grid_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title Off grid status', + }), + 'context': , + 'entity_id': 'binary_sensor.mock_title_off_grid_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_output_fault_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mock_title_output_fault_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Output fault status', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'output_fault_status', + 'unique_id': 'MY_SERIAL_NUMBER_output_fault_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_output_fault_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title Output fault status', + }), + 'context': , + 'entity_id': 'binary_sensor.mock_title_output_fault_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mock_title_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'off_grid_status', + 'unique_id': 'MY_SERIAL_NUMBER_off_grid_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.mock_title_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mock_title_problem_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dc_1_short_circuit_error_status', + 'unique_id': 'MY_SERIAL_NUMBER_dc_1_short_circuit_error_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.mock_title_problem_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mock_title_problem_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dc_2_short_circuit_error_status', + 'unique_id': 'MY_SERIAL_NUMBER_dc_2_short_circuit_error_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.mock_title_problem_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mock_title_problem_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'apsystems', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'output_fault_status', + 'unique_id': 'MY_SERIAL_NUMBER_output_fault_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.mock_title_problem_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Title Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.mock_title_problem_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/apsystems/test_binary_sensor.py b/tests/components/apsystems/test_binary_sensor.py new file mode 100644 index 00000000000..0c6fbffc93c --- /dev/null +++ b/tests/components/apsystems/test_binary_sensor.py @@ -0,0 +1,31 @@ +"""Test the APSystem binary sensor module.""" + +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_apsystems: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.apsystems.PLATFORMS", + [Platform.BINARY_SENSOR], + ): + await setup_integration(hass, mock_config_entry) + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) From e9e357b12efcccf37e7a6b0ed6fd791fb8cb38e6 Mon Sep 17 00:00:00 2001 From: "Barry vd. Heuvel" Date: Mon, 5 Aug 2024 11:18:04 +0200 Subject: [PATCH 1954/2411] Add spaces for readability in licenses.py (#123173) --- script/licenses.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/script/licenses.py b/script/licenses.py index 3b9ec389b08..f6578bf3f65 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -208,7 +208,7 @@ def main() -> int: if previous_unapproved_version < package.version: if approved: print( - "Approved license detected for" + "Approved license detected for " f"{package.name}@{package.version}: {package.license}" ) print("Please remove the package from the TODO list.") @@ -222,14 +222,14 @@ def main() -> int: exit_code = 1 elif not approved and package.name not in EXCEPTIONS: print( - "We could not detect an OSI-approved license for" + "We could not detect an OSI-approved license for " f"{package.name}@{package.version}: {package.license}" ) print() exit_code = 1 elif approved and package.name in EXCEPTIONS: print( - "Approved license detected for" + "Approved license detected for " f"{package.name}@{package.version}: {package.license}" ) print(f"Please remove the package from the EXCEPTIONS list: {package.name}") From 1163cc7caba5c15ba1aa4580d3f6544bcb794c4d Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Mon, 5 Aug 2024 05:18:34 -0400 Subject: [PATCH 1955/2411] Update greeclimate to 2.0.0 (#121030) Co-authored-by: Joostlek --- homeassistant/components/gree/const.py | 2 + homeassistant/components/gree/coordinator.py | 65 ++++++- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gree/test_bridge.py | 35 +++- tests/components/gree/test_climate.py | 187 ++++++++++--------- 7 files changed, 190 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py index 46479210921..f926eb1c53e 100644 --- a/homeassistant/components/gree/const.py +++ b/homeassistant/components/gree/const.py @@ -18,3 +18,5 @@ FAN_MEDIUM_HIGH = "medium high" MAX_ERRORS = 2 TARGET_TEMPERATURE_STEP = 1 + +UPDATE_INTERVAL = 60 diff --git a/homeassistant/components/gree/coordinator.py b/homeassistant/components/gree/coordinator.py index 1bccf3bbc48..ae8b22706ef 100644 --- a/homeassistant/components/gree/coordinator.py +++ b/homeassistant/components/gree/coordinator.py @@ -2,16 +2,20 @@ from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging +from typing import Any from greeclimate.device import Device, DeviceInfo from greeclimate.discovery import Discovery, Listener from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError +from greeclimate.network import Response from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.json import json_dumps from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.dt import utcnow from .const import ( COORDINATORS, @@ -19,12 +23,13 @@ from .const import ( DISPATCH_DEVICE_DISCOVERED, DOMAIN, MAX_ERRORS, + UPDATE_INTERVAL, ) _LOGGER = logging.getLogger(__name__) -class DeviceDataUpdateCoordinator(DataUpdateCoordinator): +class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Manages polling for state changes from the device.""" def __init__(self, hass: HomeAssistant, device: Device) -> None: @@ -34,28 +39,68 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): hass, _LOGGER, name=f"{DOMAIN}-{device.device_info.name}", - update_interval=timedelta(seconds=60), + update_interval=timedelta(seconds=UPDATE_INTERVAL), + always_update=False, ) self.device = device - self._error_count = 0 + self.device.add_handler(Response.DATA, self.device_state_updated) + self.device.add_handler(Response.RESULT, self.device_state_updated) - async def _async_update_data(self): + self._error_count: int = 0 + self._last_response_time: datetime = utcnow() + self._last_error_time: datetime | None = None + + def device_state_updated(self, *args: Any) -> None: + """Handle device state updates.""" + _LOGGER.debug("Device state updated: %s", json_dumps(args)) + self._error_count = 0 + self._last_response_time = utcnow() + self.async_set_updated_data(self.device.raw_properties) + + async def _async_update_data(self) -> dict[str, Any]: """Update the state of the device.""" + _LOGGER.debug( + "Updating device state: %s, error count: %d", self.name, self._error_count + ) try: await self.device.update_state() except DeviceNotBoundError as error: - raise UpdateFailed(f"Device {self.name} is unavailable") from error + raise UpdateFailed( + f"Device {self.name} is unavailable, device is not bound." + ) from error except DeviceTimeoutError as error: self._error_count += 1 # Under normal conditions GREE units timeout every once in a while if self.last_update_success and self._error_count >= MAX_ERRORS: _LOGGER.warning( - "Device is unavailable: %s (%s)", - self.name, - self.device.device_info, + "Device %s is unavailable: %s", self.name, self.device.device_info ) - raise UpdateFailed(f"Device {self.name} is unavailable") from error + raise UpdateFailed( + f"Device {self.name} is unavailable, could not send update request" + ) from error + else: + # raise update failed if time for more than MAX_ERRORS has passed since last update + now = utcnow() + elapsed_success = now - self._last_response_time + if self.update_interval and elapsed_success >= self.update_interval: + if not self._last_error_time or ( + (now - self.update_interval) >= self._last_error_time + ): + self._last_error_time = now + self._error_count += 1 + + _LOGGER.warning( + "Device %s is unresponsive for %s seconds", + self.name, + elapsed_success, + ) + if self.last_update_success and self._error_count >= MAX_ERRORS: + raise UpdateFailed( + f"Device {self.name} is unresponsive for too long and now unavailable" + ) + + return self.device.raw_properties async def push_state_update(self): """Send state updates to the physical device.""" diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index a7c884c4042..ca1c4b5b754 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/gree", "iot_class": "local_polling", "loggers": ["greeclimate"], - "requirements": ["greeclimate==1.4.6"] + "requirements": ["greeclimate==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 559290fd069..2c3889c4c0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1010,7 +1010,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.6 +greeclimate==2.0.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ff84200d77..38739c86a3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -854,7 +854,7 @@ govee-local-api==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.6 +greeclimate==2.0.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/tests/components/gree/test_bridge.py b/tests/components/gree/test_bridge.py index 37b0b0dc15e..32372bebf37 100644 --- a/tests/components/gree/test_bridge.py +++ b/tests/components/gree/test_bridge.py @@ -5,8 +5,12 @@ from datetime import timedelta from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.climate import DOMAIN -from homeassistant.components.gree.const import COORDINATORS, DOMAIN as GREE +from homeassistant.components.climate import DOMAIN, HVACMode +from homeassistant.components.gree.const import ( + COORDINATORS, + DOMAIN as GREE, + UPDATE_INTERVAL, +) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -69,3 +73,30 @@ async def test_discovery_after_setup( device_infos = [x.device.device_info for x in hass.data[GREE][COORDINATORS]] assert device_infos[0].ip == "1.1.1.2" assert device_infos[1].ip == "2.2.2.1" + + +async def test_coordinator_updates( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device +) -> None: + """Test gree devices update their state.""" + await async_setup_gree(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all(DOMAIN)) == 1 + + callback = device().add_handler.call_args_list[0][0][1] + + async def fake_update_state(*args) -> None: + """Fake update state.""" + device().power = True + callback() + + device().update_state.side_effect = fake_update_state + + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID_1) + assert state is not None + assert state.state != HVACMode.OFF diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index e6f24ade1aa..1bf49bbca26 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -48,7 +48,12 @@ from homeassistant.components.gree.climate import ( HVAC_MODES_REVERSE, GreeClimateEntity, ) -from homeassistant.components.gree.const import FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW +from homeassistant.components.gree.const import ( + DISCOVERY_SCAN_INTERVAL, + FAN_MEDIUM_HIGH, + FAN_MEDIUM_LOW, + UPDATE_INTERVAL, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, @@ -61,7 +66,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er -import homeassistant.util.dt as dt_util from .common import async_setup_gree, build_device_mock @@ -70,12 +74,6 @@ from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_device_1" -@pytest.fixture -def mock_now(): - """Fixture for dtutil.now.""" - return dt_util.utcnow() - - async def test_discovery_called_once(hass: HomeAssistant, discovery, device) -> None: """Test discovery is only ever called once.""" await async_setup_gree(hass) @@ -104,7 +102,7 @@ async def test_discovery_setup(hass: HomeAssistant, discovery, device) -> None: async def test_discovery_setup_connection_error( - hass: HomeAssistant, discovery, device, mock_now + hass: HomeAssistant, discovery, device ) -> None: """Test gree integration is setup.""" MockDevice1 = build_device_mock( @@ -126,7 +124,7 @@ async def test_discovery_setup_connection_error( async def test_discovery_after_setup( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test gree devices don't change after multiple discoveries.""" MockDevice1 = build_device_mock( @@ -142,8 +140,7 @@ async def test_discovery_after_setup( discovery.return_value.mock_devices = [MockDevice1, MockDevice2] device.side_effect = [MockDevice1, MockDevice2] - await async_setup_gree(hass) - await hass.async_block_till_done() + await async_setup_gree(hass) # Update 1 assert discovery.return_value.scan_count == 1 assert len(hass.states.async_all(DOMAIN)) == 2 @@ -152,9 +149,8 @@ async def test_discovery_after_setup( discovery.return_value.mock_devices = [MockDevice1, MockDevice2] device.side_effect = [MockDevice1, MockDevice2] - next_update = mock_now + timedelta(minutes=6) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert discovery.return_value.scan_count == 2 @@ -162,7 +158,7 @@ async def test_discovery_after_setup( async def test_discovery_add_device_after_setup( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test gree devices can be added after initial setup.""" MockDevice1 = build_device_mock( @@ -178,6 +174,8 @@ async def test_discovery_add_device_after_setup( discovery.return_value.mock_devices = [MockDevice1] device.side_effect = [MockDevice1] + await async_setup_gree(hass) # Update 1 + await async_setup_gree(hass) await hass.async_block_till_done() @@ -188,9 +186,8 @@ async def test_discovery_add_device_after_setup( discovery.return_value.mock_devices = [MockDevice2] device.side_effect = [MockDevice2] - next_update = mock_now + timedelta(minutes=6) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert discovery.return_value.scan_count == 2 @@ -198,7 +195,7 @@ async def test_discovery_add_device_after_setup( async def test_discovery_device_bind_after_setup( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test gree devices can be added after a late device bind.""" MockDevice1 = build_device_mock( @@ -210,8 +207,7 @@ async def test_discovery_device_bind_after_setup( discovery.return_value.mock_devices = [MockDevice1] device.return_value = MockDevice1 - await async_setup_gree(hass) - await hass.async_block_till_done() + await async_setup_gree(hass) # Update 1 assert len(hass.states.async_all(DOMAIN)) == 1 state = hass.states.get(ENTITY_ID) @@ -222,9 +218,8 @@ async def test_discovery_device_bind_after_setup( MockDevice1.bind.side_effect = None MockDevice1.update_state.side_effect = None - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -232,7 +227,7 @@ async def test_discovery_device_bind_after_setup( async def test_update_connection_failure( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Testing update hvac connection failure exception.""" device().update_state.side_effect = [ @@ -241,36 +236,32 @@ async def test_update_connection_failure( DeviceTimeoutError, ] - await async_setup_gree(hass) + await async_setup_gree(hass) # Update 1 + + async def run_update(): + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - # First update to make the device available + # Update 2 + await run_update() state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - next_update = mock_now + timedelta(minutes=10) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + # Update 3 + await run_update() - next_update = mock_now + timedelta(minutes=15) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - - # Then two more update failures to make the device unavailable + # Update 4 + await run_update() state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state == STATE_UNAVAILABLE -async def test_update_connection_failure_recovery( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now +async def test_update_connection_send_failure_recovery( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Testing update hvac connection failure recovery.""" device().update_state.side_effect = [ @@ -279,31 +270,27 @@ async def test_update_connection_failure_recovery( DEFAULT_MOCK, ] - await async_setup_gree(hass) + await async_setup_gree(hass) # Update 1 + + async def run_update(): + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) - # First update becomes unavailable - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + await run_update() # Update 2 state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state == STATE_UNAVAILABLE - # Second update restores the connection - next_update = mock_now + timedelta(minutes=10) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - + await run_update() # Update 3 state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE async def test_update_unhandled_exception( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Testing update hvac connection unhandled response exception.""" device().update_state.side_effect = [DEFAULT_MOCK, Exception] @@ -314,9 +301,8 @@ async def test_update_unhandled_exception( assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - next_update = mock_now + timedelta(minutes=10) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -325,15 +311,13 @@ async def test_update_unhandled_exception( async def test_send_command_device_timeout( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test for sending power on command to the device with a device timeout.""" await async_setup_gree(hass) - # First update to make the device available - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -355,7 +339,40 @@ async def test_send_command_device_timeout( assert state.state != STATE_UNAVAILABLE -async def test_send_power_on(hass: HomeAssistant, discovery, device, mock_now) -> None: +async def test_unresponsive_device( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device +) -> None: + """Test for unresponsive device.""" + await async_setup_gree(hass) + + async def run_update(): + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Update 2 + await run_update() + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state != STATE_UNAVAILABLE + + # Update 3, 4, 5 + await run_update() + await run_update() + await run_update() + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state == STATE_UNAVAILABLE + + # Receiving update from device will reset the state to available again + device().device_state_updated("test") + await run_update() + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state != STATE_UNAVAILABLE + + +async def test_send_power_on(hass: HomeAssistant, discovery, device) -> None: """Test for sending power on command to the device.""" await async_setup_gree(hass) @@ -372,7 +389,7 @@ async def test_send_power_on(hass: HomeAssistant, discovery, device, mock_now) - async def test_send_power_off_device_timeout( - hass: HomeAssistant, discovery, device, mock_now + hass: HomeAssistant, discovery, device ) -> None: """Test for sending power off command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -543,9 +560,7 @@ async def test_update_target_temperature( @pytest.mark.parametrize( "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) -async def test_send_preset_mode( - hass: HomeAssistant, discovery, device, mock_now, preset -) -> None: +async def test_send_preset_mode(hass: HomeAssistant, discovery, device, preset) -> None: """Test for sending preset mode command to the device.""" await async_setup_gree(hass) @@ -561,9 +576,7 @@ async def test_send_preset_mode( assert state.attributes.get(ATTR_PRESET_MODE) == preset -async def test_send_invalid_preset_mode( - hass: HomeAssistant, discovery, device, mock_now -) -> None: +async def test_send_invalid_preset_mode(hass: HomeAssistant, discovery, device) -> None: """Test for sending preset mode command to the device.""" await async_setup_gree(hass) @@ -584,7 +597,7 @@ async def test_send_invalid_preset_mode( "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) async def test_send_preset_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, preset + hass: HomeAssistant, discovery, device, preset ) -> None: """Test for sending preset mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -607,7 +620,7 @@ async def test_send_preset_mode_device_timeout( "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) async def test_update_preset_mode( - hass: HomeAssistant, discovery, device, mock_now, preset + hass: HomeAssistant, discovery, device, preset ) -> None: """Test for updating preset mode from the device.""" device().steady_heat = preset == PRESET_AWAY @@ -634,7 +647,7 @@ async def test_update_preset_mode( ], ) async def test_send_hvac_mode( - hass: HomeAssistant, discovery, device, mock_now, hvac_mode + hass: HomeAssistant, discovery, device, hvac_mode ) -> None: """Test for sending hvac mode command to the device.""" await async_setup_gree(hass) @@ -656,7 +669,7 @@ async def test_send_hvac_mode( [HVACMode.AUTO, HVACMode.COOL, HVACMode.DRY, HVACMode.FAN_ONLY, HVACMode.HEAT], ) async def test_send_hvac_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, hvac_mode + hass: HomeAssistant, discovery, device, hvac_mode ) -> None: """Test for sending hvac mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -687,7 +700,7 @@ async def test_send_hvac_mode_device_timeout( ], ) async def test_update_hvac_mode( - hass: HomeAssistant, discovery, device, mock_now, hvac_mode + hass: HomeAssistant, discovery, device, hvac_mode ) -> None: """Test for updating hvac mode from the device.""" device().power = hvac_mode != HVACMode.OFF @@ -704,9 +717,7 @@ async def test_update_hvac_mode( "fan_mode", [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) -async def test_send_fan_mode( - hass: HomeAssistant, discovery, device, mock_now, fan_mode -) -> None: +async def test_send_fan_mode(hass: HomeAssistant, discovery, device, fan_mode) -> None: """Test for sending fan mode command to the device.""" await async_setup_gree(hass) @@ -722,9 +733,7 @@ async def test_send_fan_mode( assert state.attributes.get(ATTR_FAN_MODE) == fan_mode -async def test_send_invalid_fan_mode( - hass: HomeAssistant, discovery, device, mock_now -) -> None: +async def test_send_invalid_fan_mode(hass: HomeAssistant, discovery, device) -> None: """Test for sending fan mode command to the device.""" await async_setup_gree(hass) @@ -746,7 +755,7 @@ async def test_send_invalid_fan_mode( [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) async def test_send_fan_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, fan_mode + hass: HomeAssistant, discovery, device, fan_mode ) -> None: """Test for sending fan mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -770,7 +779,7 @@ async def test_send_fan_mode_device_timeout( [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) async def test_update_fan_mode( - hass: HomeAssistant, discovery, device, mock_now, fan_mode + hass: HomeAssistant, discovery, device, fan_mode ) -> None: """Test for updating fan mode from the device.""" device().fan_speed = FAN_MODES_REVERSE.get(fan_mode) @@ -786,7 +795,7 @@ async def test_update_fan_mode( "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_send_swing_mode( - hass: HomeAssistant, discovery, device, mock_now, swing_mode + hass: HomeAssistant, discovery, device, swing_mode ) -> None: """Test for sending swing mode command to the device.""" await async_setup_gree(hass) @@ -803,9 +812,7 @@ async def test_send_swing_mode( assert state.attributes.get(ATTR_SWING_MODE) == swing_mode -async def test_send_invalid_swing_mode( - hass: HomeAssistant, discovery, device, mock_now -) -> None: +async def test_send_invalid_swing_mode(hass: HomeAssistant, discovery, device) -> None: """Test for sending swing mode command to the device.""" await async_setup_gree(hass) @@ -826,7 +833,7 @@ async def test_send_invalid_swing_mode( "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_send_swing_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, swing_mode + hass: HomeAssistant, discovery, device, swing_mode ) -> None: """Test for sending swing mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -849,7 +856,7 @@ async def test_send_swing_mode_device_timeout( "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_update_swing_mode( - hass: HomeAssistant, discovery, device, mock_now, swing_mode + hass: HomeAssistant, discovery, device, swing_mode ) -> None: """Test for updating swing mode from the device.""" device().horizontal_swing = ( From 3257bdeed2f4ab11a793fdc4237c17119e76ab25 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 5 Aug 2024 05:19:57 -0400 Subject: [PATCH 1956/2411] Bump ZHA lib to 0.0.27 (#123125) --- 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 6e35339c53f..d7dc53b5167 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.25"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.27"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 2c3889c4c0b..9a0164f6c35 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2989,7 +2989,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.25 +zha==0.0.27 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38739c86a3b..b419e6c6430 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2366,7 +2366,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.25 +zha==0.0.27 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 From 67e3139dcfb8b4a48e18e10b9036679658455363 Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:22:13 +0200 Subject: [PATCH 1957/2411] Clean up useless logging handler setting (#120974) Setting level of the handler does effectively nothing, because HomeAssistantQueueHandler ignores this setting. Also make the convention of getting the root logger uniform. --- homeassistant/bootstrap.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 43f4d451497..742a293e4c4 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -586,10 +586,10 @@ async def async_enable_logging( logging.getLogger("aiohttp.access").setLevel(logging.WARNING) logging.getLogger("httpx").setLevel(logging.WARNING) - sys.excepthook = lambda *args: logging.getLogger(None).exception( + sys.excepthook = lambda *args: logging.getLogger().exception( "Uncaught exception", exc_info=args ) - threading.excepthook = lambda args: logging.getLogger(None).exception( + threading.excepthook = lambda args: logging.getLogger().exception( "Uncaught thread exception", exc_info=( # type: ignore[arg-type] args.exc_type, @@ -616,10 +616,9 @@ async def async_enable_logging( _create_log_file, err_log_path, log_rotate_days ) - err_handler.setLevel(logging.INFO if verbose else logging.WARNING) err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME)) - logger = logging.getLogger("") + logger = logging.getLogger() logger.addHandler(err_handler) logger.setLevel(logging.INFO if verbose else logging.WARNING) From ab811f70b18d72a7a550b61b76e415a0551583e7 Mon Sep 17 00:00:00 2001 From: Chris Buckley Date: Mon, 5 Aug 2024 10:24:49 +0100 Subject: [PATCH 1958/2411] Todoist service validation error consistency (#123122) --- homeassistant/components/todoist/calendar.py | 13 +++++++++--- homeassistant/components/todoist/strings.json | 3 +++ tests/components/todoist/test_calendar.py | 20 +++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index f89c09451b6..2acd4ea6dc6 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -21,7 +21,7 @@ from homeassistant.components.calendar import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, CONF_NAME, CONF_TOKEN, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -215,14 +215,21 @@ def async_register_services( # noqa: C901 async def handle_new_task(call: ServiceCall) -> None: # noqa: C901 """Call when a user creates a new Todoist Task from Home Assistant.""" - project_name = call.data[PROJECT_NAME].lower() + project_name = call.data[PROJECT_NAME] projects = await coordinator.async_get_projects() project_id: str | None = None for project in projects: if project_name == project.name.lower(): project_id = project.id + break if project_id is None: - raise HomeAssistantError(f"Invalid project name '{project_name}'") + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="project_invalid", + translation_placeholders={ + "project": project_name, + }, + ) # Optional section within project section_id: str | None = None diff --git a/homeassistant/components/todoist/strings.json b/homeassistant/components/todoist/strings.json index 55b7ef62b58..5b083ac58bf 100644 --- a/homeassistant/components/todoist/strings.json +++ b/homeassistant/components/todoist/strings.json @@ -21,6 +21,9 @@ } }, "exceptions": { + "project_invalid": { + "message": "Invalid project name \"{project}\"" + }, "section_invalid": { "message": "Project \"{project}\" has no section \"{section}\"" } diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py index 680406096cc..071a14a70ae 100644 --- a/tests/components/todoist/test_calendar.py +++ b/tests/components/todoist/test_calendar.py @@ -23,6 +23,7 @@ from homeassistant.components.todoist.const import ( ) from homeassistant.const import CONF_TOKEN, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util import dt as dt_util @@ -270,6 +271,25 @@ async def test_create_task_service_call(hass: HomeAssistant, api: AsyncMock) -> ) +async def test_create_task_service_call_raises( + hass: HomeAssistant, api: AsyncMock +) -> None: + """Test adding an item to an invalid project raises an error.""" + + with pytest.raises(ServiceValidationError, match="project_invalid"): + await hass.services.async_call( + DOMAIN, + SERVICE_NEW_TASK, + { + ASSIGNEE: "user", + CONTENT: "task", + LABELS: ["Label1"], + PROJECT_NAME: "Missing Project", + }, + blocking=True, + ) + + async def test_create_task_service_call_with_section( hass: HomeAssistant, api: AsyncMock ) -> None: From 5b7fd29797881b56c08034e3edef30b5ffccc6c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Aug 2024 04:33:37 -0500 Subject: [PATCH 1959/2411] Improve performance of logbook processor humanify (#123157) --- homeassistant/components/logbook/processor.py | 101 ++++++++---------- 1 file changed, 45 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 426f33fadb2..77aa71740f1 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from datetime import datetime as dt import logging import time -from typing import Any +from typing import TYPE_CHECKING, Any from sqlalchemy.engine import Result from sqlalchemy.engine.row import Row @@ -211,35 +211,30 @@ def _humanify( continuous_sensors: dict[str, bool] = {} context_lookup = logbook_run.context_lookup external_events = logbook_run.external_events - event_cache = logbook_run.event_cache - entity_name_cache = logbook_run.entity_name_cache + event_cache_get = logbook_run.event_cache.get + entity_name_cache_get = logbook_run.entity_name_cache.get include_entity_name = logbook_run.include_entity_name timestamp = logbook_run.timestamp memoize_new_contexts = logbook_run.memoize_new_contexts + get_context = context_augmenter.get_context + context_id_bin: bytes + data: dict[str, Any] # Process rows for row in rows: - context_id_bin: bytes = row[CONTEXT_ID_BIN_POS] + context_id_bin = row[CONTEXT_ID_BIN_POS] if memoize_new_contexts and context_id_bin not in context_lookup: context_lookup[context_id_bin] = row if row[CONTEXT_ONLY_POS]: continue event_type = row[EVENT_TYPE_POS] - if event_type == EVENT_CALL_SERVICE: continue - time_fired_ts = row[TIME_FIRED_TS_POS] - if timestamp: - when = time_fired_ts or time.time() - else: - when = process_timestamp_to_utc_isoformat( - dt_util.utc_from_timestamp(time_fired_ts) or dt_util.utcnow() - ) - if event_type is PSEUDO_EVENT_STATE_CHANGED: entity_id = row[ENTITY_ID_POS] - assert entity_id is not None + if TYPE_CHECKING: + assert entity_id is not None # Skip continuous sensors if ( is_continuous := continuous_sensors.get(entity_id) @@ -250,34 +245,27 @@ def _humanify( continue data = { - LOGBOOK_ENTRY_WHEN: when, LOGBOOK_ENTRY_STATE: row[STATE_POS], LOGBOOK_ENTRY_ENTITY_ID: entity_id, } if include_entity_name: - data[LOGBOOK_ENTRY_NAME] = entity_name_cache.get(entity_id) + data[LOGBOOK_ENTRY_NAME] = entity_name_cache_get(entity_id) if icon := row[ICON_POS]: data[LOGBOOK_ENTRY_ICON] = icon - context_augmenter.augment(data, row, context_id_bin) - yield data - elif event_type in external_events: domain, describe_event = external_events[event_type] try: - data = describe_event(event_cache.get(row)) + data = describe_event(event_cache_get(row)) except Exception: _LOGGER.exception( "Error with %s describe event for %s", domain, event_type ) continue - data[LOGBOOK_ENTRY_WHEN] = when data[LOGBOOK_ENTRY_DOMAIN] = domain - context_augmenter.augment(data, row, context_id_bin) - yield data elif event_type == EVENT_LOGBOOK_ENTRY: - event = event_cache.get(row) + event = event_cache_get(row) if not (event_data := event.data): continue entry_domain = event_data.get(ATTR_DOMAIN) @@ -285,14 +273,41 @@ def _humanify( if entry_domain is None and entry_entity_id is not None: entry_domain = split_entity_id(str(entry_entity_id))[0] data = { - LOGBOOK_ENTRY_WHEN: when, LOGBOOK_ENTRY_NAME: event_data.get(ATTR_NAME), LOGBOOK_ENTRY_MESSAGE: event_data.get(ATTR_MESSAGE), LOGBOOK_ENTRY_DOMAIN: entry_domain, LOGBOOK_ENTRY_ENTITY_ID: entry_entity_id, } - context_augmenter.augment(data, row, context_id_bin) - yield data + + else: + continue + + time_fired_ts = row[TIME_FIRED_TS_POS] + if timestamp: + when = time_fired_ts or time.time() + else: + when = process_timestamp_to_utc_isoformat( + dt_util.utc_from_timestamp(time_fired_ts) or dt_util.utcnow() + ) + data[LOGBOOK_ENTRY_WHEN] = when + + if context_user_id_bin := row[CONTEXT_USER_ID_BIN_POS]: + data[CONTEXT_USER_ID] = bytes_to_uuid_hex_or_none(context_user_id_bin) + + # Augment context if its available but not if the context is the same as the row + # or if the context is the parent of the row + if (context_row := get_context(context_id_bin, row)) and not ( + (row is context_row or _rows_ids_match(row, context_row)) + and ( + not (context_parent := row[CONTEXT_PARENT_ID_BIN_POS]) + or not (context_row := get_context(context_parent, context_row)) + or row is context_row + or _rows_ids_match(row, context_row) + ) + ): + context_augmenter.augment(data, context_row) + + yield data class ContextAugmenter: @@ -306,8 +321,8 @@ class ContextAugmenter: self.event_cache = logbook_run.event_cache self.include_entity_name = logbook_run.include_entity_name - def _get_context_row( - self, context_id_bin: bytes | None, row: Row | EventAsRow + def get_context( + self, context_id_bin: bytes | None, row: Row | EventAsRow | None ) -> Row | EventAsRow | None: """Get the context row from the id or row context.""" if context_id_bin is not None and ( @@ -322,34 +337,8 @@ class ContextAugmenter: return async_event_to_row(origin_event) return None - def augment( - self, data: dict[str, Any], row: Row | EventAsRow, context_id_bin: bytes | None - ) -> None: + def augment(self, data: dict[str, Any], context_row: Row | EventAsRow) -> None: """Augment data from the row and cache.""" - if context_user_id_bin := row[CONTEXT_USER_ID_BIN_POS]: - data[CONTEXT_USER_ID] = bytes_to_uuid_hex_or_none(context_user_id_bin) - - if not (context_row := self._get_context_row(context_id_bin, row)): - return - - if row is context_row or _rows_ids_match(row, context_row): - # This is the first event with the given ID. Was it directly caused by - # a parent event? - context_parent_id_bin = row[CONTEXT_PARENT_ID_BIN_POS] - if ( - not context_parent_id_bin - or ( - context_row := self._get_context_row( - context_parent_id_bin, context_row - ) - ) - is None - ): - return - # Ensure the (parent) context_event exists and is not the root cause of - # this log entry. - if row is context_row or _rows_ids_match(row, context_row): - return event_type = context_row[EVENT_TYPE_POS] # State change if context_entity_id := context_row[ENTITY_ID_POS]: From 7b1bf82e3c0179637cb43d4fcc5cd3a704122632 Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Mon, 5 Aug 2024 05:18:34 -0400 Subject: [PATCH 1960/2411] Update greeclimate to 2.0.0 (#121030) Co-authored-by: Joostlek --- homeassistant/components/gree/const.py | 2 + homeassistant/components/gree/coordinator.py | 65 ++++++- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gree/test_bridge.py | 35 +++- tests/components/gree/test_climate.py | 187 ++++++++++--------- 7 files changed, 190 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py index 46479210921..f926eb1c53e 100644 --- a/homeassistant/components/gree/const.py +++ b/homeassistant/components/gree/const.py @@ -18,3 +18,5 @@ FAN_MEDIUM_HIGH = "medium high" MAX_ERRORS = 2 TARGET_TEMPERATURE_STEP = 1 + +UPDATE_INTERVAL = 60 diff --git a/homeassistant/components/gree/coordinator.py b/homeassistant/components/gree/coordinator.py index 1bccf3bbc48..ae8b22706ef 100644 --- a/homeassistant/components/gree/coordinator.py +++ b/homeassistant/components/gree/coordinator.py @@ -2,16 +2,20 @@ from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging +from typing import Any from greeclimate.device import Device, DeviceInfo from greeclimate.discovery import Discovery, Listener from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError +from greeclimate.network import Response from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.json import json_dumps from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.dt import utcnow from .const import ( COORDINATORS, @@ -19,12 +23,13 @@ from .const import ( DISPATCH_DEVICE_DISCOVERED, DOMAIN, MAX_ERRORS, + UPDATE_INTERVAL, ) _LOGGER = logging.getLogger(__name__) -class DeviceDataUpdateCoordinator(DataUpdateCoordinator): +class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Manages polling for state changes from the device.""" def __init__(self, hass: HomeAssistant, device: Device) -> None: @@ -34,28 +39,68 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): hass, _LOGGER, name=f"{DOMAIN}-{device.device_info.name}", - update_interval=timedelta(seconds=60), + update_interval=timedelta(seconds=UPDATE_INTERVAL), + always_update=False, ) self.device = device - self._error_count = 0 + self.device.add_handler(Response.DATA, self.device_state_updated) + self.device.add_handler(Response.RESULT, self.device_state_updated) - async def _async_update_data(self): + self._error_count: int = 0 + self._last_response_time: datetime = utcnow() + self._last_error_time: datetime | None = None + + def device_state_updated(self, *args: Any) -> None: + """Handle device state updates.""" + _LOGGER.debug("Device state updated: %s", json_dumps(args)) + self._error_count = 0 + self._last_response_time = utcnow() + self.async_set_updated_data(self.device.raw_properties) + + async def _async_update_data(self) -> dict[str, Any]: """Update the state of the device.""" + _LOGGER.debug( + "Updating device state: %s, error count: %d", self.name, self._error_count + ) try: await self.device.update_state() except DeviceNotBoundError as error: - raise UpdateFailed(f"Device {self.name} is unavailable") from error + raise UpdateFailed( + f"Device {self.name} is unavailable, device is not bound." + ) from error except DeviceTimeoutError as error: self._error_count += 1 # Under normal conditions GREE units timeout every once in a while if self.last_update_success and self._error_count >= MAX_ERRORS: _LOGGER.warning( - "Device is unavailable: %s (%s)", - self.name, - self.device.device_info, + "Device %s is unavailable: %s", self.name, self.device.device_info ) - raise UpdateFailed(f"Device {self.name} is unavailable") from error + raise UpdateFailed( + f"Device {self.name} is unavailable, could not send update request" + ) from error + else: + # raise update failed if time for more than MAX_ERRORS has passed since last update + now = utcnow() + elapsed_success = now - self._last_response_time + if self.update_interval and elapsed_success >= self.update_interval: + if not self._last_error_time or ( + (now - self.update_interval) >= self._last_error_time + ): + self._last_error_time = now + self._error_count += 1 + + _LOGGER.warning( + "Device %s is unresponsive for %s seconds", + self.name, + elapsed_success, + ) + if self.last_update_success and self._error_count >= MAX_ERRORS: + raise UpdateFailed( + f"Device {self.name} is unresponsive for too long and now unavailable" + ) + + return self.device.raw_properties async def push_state_update(self): """Send state updates to the physical device.""" diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index a7c884c4042..ca1c4b5b754 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/gree", "iot_class": "local_polling", "loggers": ["greeclimate"], - "requirements": ["greeclimate==1.4.6"] + "requirements": ["greeclimate==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 479e22a3bfc..b73248c5e16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1007,7 +1007,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.6 +greeclimate==2.0.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d92bde7aa8..a0b634d0e2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -848,7 +848,7 @@ govee-local-api==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.6 +greeclimate==2.0.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/tests/components/gree/test_bridge.py b/tests/components/gree/test_bridge.py index 37b0b0dc15e..32372bebf37 100644 --- a/tests/components/gree/test_bridge.py +++ b/tests/components/gree/test_bridge.py @@ -5,8 +5,12 @@ from datetime import timedelta from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.climate import DOMAIN -from homeassistant.components.gree.const import COORDINATORS, DOMAIN as GREE +from homeassistant.components.climate import DOMAIN, HVACMode +from homeassistant.components.gree.const import ( + COORDINATORS, + DOMAIN as GREE, + UPDATE_INTERVAL, +) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -69,3 +73,30 @@ async def test_discovery_after_setup( device_infos = [x.device.device_info for x in hass.data[GREE][COORDINATORS]] assert device_infos[0].ip == "1.1.1.2" assert device_infos[1].ip == "2.2.2.1" + + +async def test_coordinator_updates( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device +) -> None: + """Test gree devices update their state.""" + await async_setup_gree(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all(DOMAIN)) == 1 + + callback = device().add_handler.call_args_list[0][0][1] + + async def fake_update_state(*args) -> None: + """Fake update state.""" + device().power = True + callback() + + device().update_state.side_effect = fake_update_state + + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID_1) + assert state is not None + assert state.state != HVACMode.OFF diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index e6f24ade1aa..1bf49bbca26 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -48,7 +48,12 @@ from homeassistant.components.gree.climate import ( HVAC_MODES_REVERSE, GreeClimateEntity, ) -from homeassistant.components.gree.const import FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW +from homeassistant.components.gree.const import ( + DISCOVERY_SCAN_INTERVAL, + FAN_MEDIUM_HIGH, + FAN_MEDIUM_LOW, + UPDATE_INTERVAL, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, @@ -61,7 +66,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er -import homeassistant.util.dt as dt_util from .common import async_setup_gree, build_device_mock @@ -70,12 +74,6 @@ from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_device_1" -@pytest.fixture -def mock_now(): - """Fixture for dtutil.now.""" - return dt_util.utcnow() - - async def test_discovery_called_once(hass: HomeAssistant, discovery, device) -> None: """Test discovery is only ever called once.""" await async_setup_gree(hass) @@ -104,7 +102,7 @@ async def test_discovery_setup(hass: HomeAssistant, discovery, device) -> None: async def test_discovery_setup_connection_error( - hass: HomeAssistant, discovery, device, mock_now + hass: HomeAssistant, discovery, device ) -> None: """Test gree integration is setup.""" MockDevice1 = build_device_mock( @@ -126,7 +124,7 @@ async def test_discovery_setup_connection_error( async def test_discovery_after_setup( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test gree devices don't change after multiple discoveries.""" MockDevice1 = build_device_mock( @@ -142,8 +140,7 @@ async def test_discovery_after_setup( discovery.return_value.mock_devices = [MockDevice1, MockDevice2] device.side_effect = [MockDevice1, MockDevice2] - await async_setup_gree(hass) - await hass.async_block_till_done() + await async_setup_gree(hass) # Update 1 assert discovery.return_value.scan_count == 1 assert len(hass.states.async_all(DOMAIN)) == 2 @@ -152,9 +149,8 @@ async def test_discovery_after_setup( discovery.return_value.mock_devices = [MockDevice1, MockDevice2] device.side_effect = [MockDevice1, MockDevice2] - next_update = mock_now + timedelta(minutes=6) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert discovery.return_value.scan_count == 2 @@ -162,7 +158,7 @@ async def test_discovery_after_setup( async def test_discovery_add_device_after_setup( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test gree devices can be added after initial setup.""" MockDevice1 = build_device_mock( @@ -178,6 +174,8 @@ async def test_discovery_add_device_after_setup( discovery.return_value.mock_devices = [MockDevice1] device.side_effect = [MockDevice1] + await async_setup_gree(hass) # Update 1 + await async_setup_gree(hass) await hass.async_block_till_done() @@ -188,9 +186,8 @@ async def test_discovery_add_device_after_setup( discovery.return_value.mock_devices = [MockDevice2] device.side_effect = [MockDevice2] - next_update = mock_now + timedelta(minutes=6) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert discovery.return_value.scan_count == 2 @@ -198,7 +195,7 @@ async def test_discovery_add_device_after_setup( async def test_discovery_device_bind_after_setup( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test gree devices can be added after a late device bind.""" MockDevice1 = build_device_mock( @@ -210,8 +207,7 @@ async def test_discovery_device_bind_after_setup( discovery.return_value.mock_devices = [MockDevice1] device.return_value = MockDevice1 - await async_setup_gree(hass) - await hass.async_block_till_done() + await async_setup_gree(hass) # Update 1 assert len(hass.states.async_all(DOMAIN)) == 1 state = hass.states.get(ENTITY_ID) @@ -222,9 +218,8 @@ async def test_discovery_device_bind_after_setup( MockDevice1.bind.side_effect = None MockDevice1.update_state.side_effect = None - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -232,7 +227,7 @@ async def test_discovery_device_bind_after_setup( async def test_update_connection_failure( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Testing update hvac connection failure exception.""" device().update_state.side_effect = [ @@ -241,36 +236,32 @@ async def test_update_connection_failure( DeviceTimeoutError, ] - await async_setup_gree(hass) + await async_setup_gree(hass) # Update 1 + + async def run_update(): + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - # First update to make the device available + # Update 2 + await run_update() state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - next_update = mock_now + timedelta(minutes=10) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + # Update 3 + await run_update() - next_update = mock_now + timedelta(minutes=15) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - - # Then two more update failures to make the device unavailable + # Update 4 + await run_update() state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state == STATE_UNAVAILABLE -async def test_update_connection_failure_recovery( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now +async def test_update_connection_send_failure_recovery( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Testing update hvac connection failure recovery.""" device().update_state.side_effect = [ @@ -279,31 +270,27 @@ async def test_update_connection_failure_recovery( DEFAULT_MOCK, ] - await async_setup_gree(hass) + await async_setup_gree(hass) # Update 1 + + async def run_update(): + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) - # First update becomes unavailable - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + await run_update() # Update 2 state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state == STATE_UNAVAILABLE - # Second update restores the connection - next_update = mock_now + timedelta(minutes=10) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - + await run_update() # Update 3 state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE async def test_update_unhandled_exception( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Testing update hvac connection unhandled response exception.""" device().update_state.side_effect = [DEFAULT_MOCK, Exception] @@ -314,9 +301,8 @@ async def test_update_unhandled_exception( assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - next_update = mock_now + timedelta(minutes=10) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -325,15 +311,13 @@ async def test_update_unhandled_exception( async def test_send_command_device_timeout( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test for sending power on command to the device with a device timeout.""" await async_setup_gree(hass) - # First update to make the device available - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -355,7 +339,40 @@ async def test_send_command_device_timeout( assert state.state != STATE_UNAVAILABLE -async def test_send_power_on(hass: HomeAssistant, discovery, device, mock_now) -> None: +async def test_unresponsive_device( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device +) -> None: + """Test for unresponsive device.""" + await async_setup_gree(hass) + + async def run_update(): + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Update 2 + await run_update() + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state != STATE_UNAVAILABLE + + # Update 3, 4, 5 + await run_update() + await run_update() + await run_update() + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state == STATE_UNAVAILABLE + + # Receiving update from device will reset the state to available again + device().device_state_updated("test") + await run_update() + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state != STATE_UNAVAILABLE + + +async def test_send_power_on(hass: HomeAssistant, discovery, device) -> None: """Test for sending power on command to the device.""" await async_setup_gree(hass) @@ -372,7 +389,7 @@ async def test_send_power_on(hass: HomeAssistant, discovery, device, mock_now) - async def test_send_power_off_device_timeout( - hass: HomeAssistant, discovery, device, mock_now + hass: HomeAssistant, discovery, device ) -> None: """Test for sending power off command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -543,9 +560,7 @@ async def test_update_target_temperature( @pytest.mark.parametrize( "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) -async def test_send_preset_mode( - hass: HomeAssistant, discovery, device, mock_now, preset -) -> None: +async def test_send_preset_mode(hass: HomeAssistant, discovery, device, preset) -> None: """Test for sending preset mode command to the device.""" await async_setup_gree(hass) @@ -561,9 +576,7 @@ async def test_send_preset_mode( assert state.attributes.get(ATTR_PRESET_MODE) == preset -async def test_send_invalid_preset_mode( - hass: HomeAssistant, discovery, device, mock_now -) -> None: +async def test_send_invalid_preset_mode(hass: HomeAssistant, discovery, device) -> None: """Test for sending preset mode command to the device.""" await async_setup_gree(hass) @@ -584,7 +597,7 @@ async def test_send_invalid_preset_mode( "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) async def test_send_preset_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, preset + hass: HomeAssistant, discovery, device, preset ) -> None: """Test for sending preset mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -607,7 +620,7 @@ async def test_send_preset_mode_device_timeout( "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) async def test_update_preset_mode( - hass: HomeAssistant, discovery, device, mock_now, preset + hass: HomeAssistant, discovery, device, preset ) -> None: """Test for updating preset mode from the device.""" device().steady_heat = preset == PRESET_AWAY @@ -634,7 +647,7 @@ async def test_update_preset_mode( ], ) async def test_send_hvac_mode( - hass: HomeAssistant, discovery, device, mock_now, hvac_mode + hass: HomeAssistant, discovery, device, hvac_mode ) -> None: """Test for sending hvac mode command to the device.""" await async_setup_gree(hass) @@ -656,7 +669,7 @@ async def test_send_hvac_mode( [HVACMode.AUTO, HVACMode.COOL, HVACMode.DRY, HVACMode.FAN_ONLY, HVACMode.HEAT], ) async def test_send_hvac_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, hvac_mode + hass: HomeAssistant, discovery, device, hvac_mode ) -> None: """Test for sending hvac mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -687,7 +700,7 @@ async def test_send_hvac_mode_device_timeout( ], ) async def test_update_hvac_mode( - hass: HomeAssistant, discovery, device, mock_now, hvac_mode + hass: HomeAssistant, discovery, device, hvac_mode ) -> None: """Test for updating hvac mode from the device.""" device().power = hvac_mode != HVACMode.OFF @@ -704,9 +717,7 @@ async def test_update_hvac_mode( "fan_mode", [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) -async def test_send_fan_mode( - hass: HomeAssistant, discovery, device, mock_now, fan_mode -) -> None: +async def test_send_fan_mode(hass: HomeAssistant, discovery, device, fan_mode) -> None: """Test for sending fan mode command to the device.""" await async_setup_gree(hass) @@ -722,9 +733,7 @@ async def test_send_fan_mode( assert state.attributes.get(ATTR_FAN_MODE) == fan_mode -async def test_send_invalid_fan_mode( - hass: HomeAssistant, discovery, device, mock_now -) -> None: +async def test_send_invalid_fan_mode(hass: HomeAssistant, discovery, device) -> None: """Test for sending fan mode command to the device.""" await async_setup_gree(hass) @@ -746,7 +755,7 @@ async def test_send_invalid_fan_mode( [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) async def test_send_fan_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, fan_mode + hass: HomeAssistant, discovery, device, fan_mode ) -> None: """Test for sending fan mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -770,7 +779,7 @@ async def test_send_fan_mode_device_timeout( [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) async def test_update_fan_mode( - hass: HomeAssistant, discovery, device, mock_now, fan_mode + hass: HomeAssistant, discovery, device, fan_mode ) -> None: """Test for updating fan mode from the device.""" device().fan_speed = FAN_MODES_REVERSE.get(fan_mode) @@ -786,7 +795,7 @@ async def test_update_fan_mode( "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_send_swing_mode( - hass: HomeAssistant, discovery, device, mock_now, swing_mode + hass: HomeAssistant, discovery, device, swing_mode ) -> None: """Test for sending swing mode command to the device.""" await async_setup_gree(hass) @@ -803,9 +812,7 @@ async def test_send_swing_mode( assert state.attributes.get(ATTR_SWING_MODE) == swing_mode -async def test_send_invalid_swing_mode( - hass: HomeAssistant, discovery, device, mock_now -) -> None: +async def test_send_invalid_swing_mode(hass: HomeAssistant, discovery, device) -> None: """Test for sending swing mode command to the device.""" await async_setup_gree(hass) @@ -826,7 +833,7 @@ async def test_send_invalid_swing_mode( "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_send_swing_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, swing_mode + hass: HomeAssistant, discovery, device, swing_mode ) -> None: """Test for sending swing mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -849,7 +856,7 @@ async def test_send_swing_mode_device_timeout( "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_update_swing_mode( - hass: HomeAssistant, discovery, device, mock_now, swing_mode + hass: HomeAssistant, discovery, device, swing_mode ) -> None: """Test for updating swing mode from the device.""" device().horizontal_swing = ( From 50b7eb44d1706ad69ebc016e0fe6423932cbde93 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 3 Aug 2024 08:16:30 +0200 Subject: [PATCH 1961/2411] Add CONTROL supported feature to Google conversation when API access (#123046) * Add CONTROL supported feature to Google conversation when API access * Better function name * Handle entry update inline * Reload instead of update --- .../conversation.py | 14 ++++++++++++++ .../snapshots/test_conversation.ambr | 8 ++++---- .../test_conversation.py | 15 +++++++++++---- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index a5c911bb757..1d8b46cde2f 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -164,6 +164,10 @@ class GoogleGenerativeAIConversationEntity( model="Generative AI", entry_type=dr.DeviceEntryType.SERVICE, ) + if self.entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) @property def supported_languages(self) -> list[str] | Literal["*"]: @@ -177,6 +181,9 @@ class GoogleGenerativeAIConversationEntity( self.hass, "conversation", self.entry.entry_id, self.entity_id ) conversation.async_set_agent(self.hass, self.entry, self) + self.entry.async_on_unload( + self.entry.add_update_listener(self._async_entry_update_listener) + ) async def async_will_remove_from_hass(self) -> None: """When entity will be removed from Home Assistant.""" @@ -397,3 +404,10 @@ class GoogleGenerativeAIConversationEntity( parts.append(llm_api.api_prompt) return "\n".join(parts) + + async def _async_entry_update_listener( + self, hass: HomeAssistant, entry: ConfigEntry + ) -> None: + """Handle options update.""" + # Reload as we update device info + entity name + supported features + await hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index abd3658e869..65238c5212a 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -215,7 +215,7 @@ ), ]) # --- -# name: test_default_prompt[config_entry_options0-None] +# name: test_default_prompt[config_entry_options0-0-None] list([ tuple( '', @@ -263,7 +263,7 @@ ), ]) # --- -# name: test_default_prompt[config_entry_options0-conversation.google_generative_ai_conversation] +# name: test_default_prompt[config_entry_options0-0-conversation.google_generative_ai_conversation] list([ tuple( '', @@ -311,7 +311,7 @@ ), ]) # --- -# name: test_default_prompt[config_entry_options1-None] +# name: test_default_prompt[config_entry_options1-1-None] list([ tuple( '', @@ -360,7 +360,7 @@ ), ]) # --- -# name: test_default_prompt[config_entry_options1-conversation.google_generative_ai_conversation] +# name: test_default_prompt[config_entry_options1-1-conversation.google_generative_ai_conversation] list([ tuple( '', diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 41f96c7b0ac..98f469643af 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -18,7 +18,7 @@ from homeassistant.components.google_generative_ai_conversation.const import ( from homeassistant.components.google_generative_ai_conversation.conversation import ( _escape_decode, ) -from homeassistant.const import CONF_LLM_HASS_API +from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_LLM_HASS_API from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import intent, llm @@ -38,10 +38,13 @@ def freeze_the_time(): "agent_id", [None, "conversation.google_generative_ai_conversation"] ) @pytest.mark.parametrize( - "config_entry_options", + ("config_entry_options", "expected_features"), [ - {}, - {CONF_LLM_HASS_API: llm.LLM_API_ASSIST}, + ({}, 0), + ( + {CONF_LLM_HASS_API: llm.LLM_API_ASSIST}, + conversation.ConversationEntityFeature.CONTROL, + ), ], ) @pytest.mark.usefixtures("mock_init_component") @@ -51,6 +54,7 @@ async def test_default_prompt( snapshot: SnapshotAssertion, agent_id: str | None, config_entry_options: {}, + expected_features: conversation.ConversationEntityFeature, hass_ws_client: WebSocketGenerator, ) -> None: """Test that the default prompt works.""" @@ -97,6 +101,9 @@ async def test_default_prompt( assert [tuple(mock_call) for mock_call in mock_model.mock_calls] == snapshot assert mock_get_tools.called == (CONF_LLM_HASS_API in config_entry_options) + state = hass.states.get("conversation.google_generative_ai_conversation") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == expected_features + @pytest.mark.parametrize( ("model_name", "supports_system_instruction"), From bee77041e83227d6a1097d1a4f860a9c12514e47 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Sat, 3 Aug 2024 09:14:24 +0300 Subject: [PATCH 1962/2411] Change enum type to string for Google Generative AI Conversation (#123069) --- .../conversation.py | 14 ++++- .../test_conversation.py | 59 +++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 1d8b46cde2f..0d24ddbf39f 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -89,9 +89,9 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: key = "type_" val = val.upper() elif key == "format": - if (schema.get("type") == "string" and val != "enum") or ( - schema.get("type") not in ("number", "integer", "string") - ): + if schema.get("type") == "string" and val != "enum": + continue + if schema.get("type") not in ("number", "integer", "string"): continue key = "format_" elif key == "items": @@ -100,11 +100,19 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: val = {k: _format_schema(v) for k, v in val.items()} result[key] = val + if result.get("enum") and result.get("type_") != "STRING": + # enum is only allowed for STRING type. This is safe as long as the schema + # contains vol.Coerce for the respective type, for example: + # vol.All(vol.Coerce(int), vol.In([1, 2, 3])) + result["type_"] = "STRING" + result["enum"] = [str(item) for item in result["enum"]] + if result.get("type_") == "OBJECT" and not result.get("properties"): # An object with undefined properties is not supported by Gemini API. # Fallback to JSON string. This will probably fail for most tools that want it, # but we don't have a better fallback strategy so far. result["properties"] = {"json": {"type_": "STRING"}} + result["required"] = [] return result diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 98f469643af..a8eae34e08b 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -17,6 +17,7 @@ from homeassistant.components.google_generative_ai_conversation.const import ( ) from homeassistant.components.google_generative_ai_conversation.conversation import ( _escape_decode, + _format_schema, ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_LLM_HASS_API from homeassistant.core import Context, HomeAssistant @@ -629,3 +630,61 @@ async def test_escape_decode() -> None: "param2": "param2's value", "param3": {"param31": "Cheminée", "param32": "Cheminée"}, } + + +@pytest.mark.parametrize( + ("openapi", "protobuf"), + [ + ( + {"type": "string", "enum": ["a", "b", "c"]}, + {"type_": "STRING", "enum": ["a", "b", "c"]}, + ), + ( + {"type": "integer", "enum": [1, 2, 3]}, + {"type_": "STRING", "enum": ["1", "2", "3"]}, + ), + ({"anyOf": [{"type": "integer"}, {"type": "number"}]}, {"type_": "INTEGER"}), + ( + { + "anyOf": [ + {"anyOf": [{"type": "integer"}, {"type": "number"}]}, + {"anyOf": [{"type": "integer"}, {"type": "number"}]}, + ] + }, + {"type_": "INTEGER"}, + ), + ({"type": "string", "format": "lower"}, {"type_": "STRING"}), + ({"type": "boolean", "format": "bool"}, {"type_": "BOOLEAN"}), + ( + {"type": "number", "format": "percent"}, + {"type_": "NUMBER", "format_": "percent"}, + ), + ( + { + "type": "object", + "properties": {"var": {"type": "string"}}, + "required": [], + }, + { + "type_": "OBJECT", + "properties": {"var": {"type_": "STRING"}}, + "required": [], + }, + ), + ( + {"type": "object", "additionalProperties": True}, + { + "type_": "OBJECT", + "properties": {"json": {"type_": "STRING"}}, + "required": [], + }, + ), + ( + {"type": "array", "items": {"type": "string"}}, + {"type_": "ARRAY", "items": {"type_": "STRING"}}, + ), + ], +) +async def test_format_schema(openapi, protobuf) -> None: + """Test _format_schema.""" + assert _format_schema(openapi) == protobuf From fa241dcd0440336622413dd87216823a1238247c Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Mon, 5 Aug 2024 08:43:29 +0200 Subject: [PATCH 1963/2411] Catch exception in coordinator setup of IronOS integration (#123079) --- .../components/iron_os/coordinator.py | 6 ++++- tests/components/iron_os/test_init.py | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/components/iron_os/test_init.py diff --git a/homeassistant/components/iron_os/coordinator.py b/homeassistant/components/iron_os/coordinator.py index e8424478d86..aefb14b689b 100644 --- a/homeassistant/components/iron_os/coordinator.py +++ b/homeassistant/components/iron_os/coordinator.py @@ -46,4 +46,8 @@ class IronOSCoordinator(DataUpdateCoordinator[LiveDataResponse]): async def _async_setup(self) -> None: """Set up the coordinator.""" - self.device_info = await self.device.get_device_info() + try: + self.device_info = await self.device.get_device_info() + + except CommunicationError as e: + raise UpdateFailed("Cannot connect to device") from e diff --git a/tests/components/iron_os/test_init.py b/tests/components/iron_os/test_init.py new file mode 100644 index 00000000000..fb0a782ea36 --- /dev/null +++ b/tests/components/iron_os/test_init.py @@ -0,0 +1,26 @@ +"""Test init of IronOS integration.""" + +from unittest.mock import AsyncMock + +from pynecil import CommunicationError +import pytest + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("ble_device") +async def test_setup_config_entry_not_ready( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, +) -> None: + """Test config entry not ready.""" + mock_pynecil.get_device_info.side_effect = CommunicationError + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY From 7623ee49e49867c388e5a74debf97dc7f36b550a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 2 Aug 2024 21:56:49 +0300 Subject: [PATCH 1964/2411] Ignore Shelly IPv6 address in zeroconf (#123081) --- .../components/shelly/config_flow.py | 2 ++ homeassistant/components/shelly/strings.json | 3 ++- tests/components/shelly/test_config_flow.py | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index cb3bca6aa47..c80d1e84d6f 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -279,6 +279,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): self, discovery_info: ZeroconfServiceInfo ) -> ConfigFlowResult: """Handle zeroconf discovery.""" + if discovery_info.ip_address.version == 6: + return self.async_abort(reason="ipv6_not_supported") host = discovery_info.host # First try to get the mac address from the name # so we can avoid making another connection to the diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 8ae4ff1f3e4..f76319eb08c 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -52,7 +52,8 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again.", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", - "another_device": "Re-configuration was unsuccessful, the IP address/hostname of another Shelly device was used." + "another_device": "Re-configuration was unsuccessful, the IP address/hostname of another Shelly device was used.", + "ipv6_not_supported": "IPv6 is not supported." } }, "device_automation": { diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index a3040fc2eb8..0c574a33e0c 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1305,3 +1305,22 @@ async def test_reconfigure_with_exception( ) assert result["errors"] == {"base": base_error} + + +async def test_zeroconf_rejects_ipv6(hass: HomeAssistant) -> None: + """Test zeroconf discovery rejects ipv6.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("fd00::b27c:63bb:cc85:4ea0"), + ip_addresses=[ip_address("fd00::b27c:63bb:cc85:4ea0")], + hostname="mock_hostname", + name="shelly1pm-12345", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "shelly1pm-12345"}, + type="mock_type", + ), + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "ipv6_not_supported" From fdb1baadbeed6e7f135c0323f229e3a85f32de4e Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Sat, 3 Aug 2024 22:32:47 +0200 Subject: [PATCH 1965/2411] Fix wrong DeviceInfo in bluesound integration (#123101) Fix bluesound device info --- homeassistant/components/bluesound/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 809ba293f89..dc09feaed63 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -255,7 +255,7 @@ class BluesoundPlayer(MediaPlayerEntity): self._attr_unique_id = format_unique_id(sync_status.mac, port) # there should always be one player with the default port per mac - if port is DEFAULT_PORT: + if port == DEFAULT_PORT: self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, format_mac(sync_status.mac))}, connections={(CONNECTION_NETWORK_MAC, format_mac(sync_status.mac))}, From eccce7017f21269245bb84be8b9ca2cf7f92464b Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sat, 3 Aug 2024 17:21:12 +0200 Subject: [PATCH 1966/2411] Bump pyenphase to 1.22.0 (#123103) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 09c55fb23ac..aa06a1ff79f 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.20.6"], + "requirements": ["pyenphase==1.22.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index b73248c5e16..b48fdd28553 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1840,7 +1840,7 @@ pyeiscp==0.0.7 pyemoncms==0.0.7 # homeassistant.components.enphase_envoy -pyenphase==1.20.6 +pyenphase==1.22.0 # homeassistant.components.envisalink pyenvisalink==4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0b634d0e2f..c9f3878dbdf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1469,7 +1469,7 @@ pyegps==0.2.5 pyemoncms==0.0.7 # homeassistant.components.enphase_envoy -pyenphase==1.20.6 +pyenphase==1.22.0 # homeassistant.components.everlights pyeverlights==0.1.0 From 832bac8c63ccb063ed74e17c6ffca83c07cb57bb Mon Sep 17 00:00:00 2001 From: Kim de Vos Date: Sat, 3 Aug 2024 15:08:01 +0200 Subject: [PATCH 1967/2411] Use slugify to create id for UniFi WAN latency (#123108) Use slugify to create id for latency --- homeassistant/components/unifi/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index d86b72d1b2f..08bd0ddb869 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -44,6 +44,7 @@ from homeassistant.core import Event as core_Event, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.util import slugify import homeassistant.util.dt as dt_util from . import UnifiConfigEntry @@ -247,8 +248,9 @@ def make_wan_latency_sensors() -> tuple[UnifiSensorEntityDescription, ...]: def make_wan_latency_entity_description( wan: Literal["WAN", "WAN2"], name: str, monitor_target: str ) -> UnifiSensorEntityDescription: + name_wan = f"{name} {wan}" return UnifiSensorEntityDescription[Devices, Device]( - key=f"{name} {wan} latency", + key=f"{name_wan} latency", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfTime.MILLISECONDS, state_class=SensorStateClass.MEASUREMENT, @@ -257,13 +259,12 @@ def make_wan_latency_sensors() -> tuple[UnifiSensorEntityDescription, ...]: api_handler_fn=lambda api: api.devices, available_fn=async_device_available_fn, device_info_fn=async_device_device_info_fn, - name_fn=lambda _: f"{name} {wan} latency", + name_fn=lambda device: f"{name_wan} latency", object_fn=lambda api, obj_id: api.devices[obj_id], supported_fn=partial( async_device_wan_latency_supported_fn, wan, monitor_target ), - unique_id_fn=lambda hub, - obj_id: f"{name.lower}_{wan.lower}_latency-{obj_id}", + unique_id_fn=lambda hub, obj_id: f"{slugify(name_wan)}_latency-{obj_id}", value_fn=partial(async_device_wan_latency_value_fn, wan, monitor_target), ) From c8a0e5228da591e521502890e8b7e43c514e8f0a Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 5 Aug 2024 05:19:57 -0400 Subject: [PATCH 1968/2411] Bump ZHA lib to 0.0.27 (#123125) --- 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 6e35339c53f..d7dc53b5167 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.25"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.27"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index b48fdd28553..fb881f5a8c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2986,7 +2986,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.25 +zha==0.0.27 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9f3878dbdf..ac3d594a541 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2360,7 +2360,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.25 +zha==0.0.27 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 From 0b4d92176275865c2a627d09c65337862489d3e6 Mon Sep 17 00:00:00 2001 From: Steve Repsher Date: Sun, 4 Aug 2024 08:28:45 -0400 Subject: [PATCH 1969/2411] Restore old service worker URL (#123131) --- homeassistant/components/frontend/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 5b462842e4a..c5df84cf549 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -398,6 +398,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: static_paths_configs: list[StaticPathConfig] = [] for path, should_cache in ( + ("service_worker.js", False), ("sw-modern.js", False), ("sw-modern.js.map", False), ("sw-legacy.js", False), From f6c4b6b0456cf8dd4b2bd4dce808e5c8b06caec2 Mon Sep 17 00:00:00 2001 From: dupondje Date: Mon, 5 Aug 2024 10:32:58 +0200 Subject: [PATCH 1970/2411] dsmr: migrate hourly_gas_meter_reading to mbus device (#123149) --- homeassistant/components/dsmr/sensor.py | 4 +- tests/components/dsmr/test_mbus_migration.py | 100 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index f794d1d05e9..b298ed5bfc0 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -439,7 +439,9 @@ def rename_old_gas_to_mbus( entries = er.async_entries_for_device(ent_reg, device_id) for entity in entries: - if entity.unique_id.endswith("belgium_5min_gas_meter_reading"): + if entity.unique_id.endswith( + "belgium_5min_gas_meter_reading" + ) or entity.unique_id.endswith("hourly_gas_meter_reading"): try: ent_reg.async_update_entity( entity.entity_id, diff --git a/tests/components/dsmr/test_mbus_migration.py b/tests/components/dsmr/test_mbus_migration.py index a28bc2c3a33..20b3d253f39 100644 --- a/tests/components/dsmr/test_mbus_migration.py +++ b/tests/components/dsmr/test_mbus_migration.py @@ -119,6 +119,106 @@ async def test_migrate_gas_to_mbus( ) +async def test_migrate_hourly_gas_to_mbus( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], +) -> None: + """Test migration of unique_id.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="/dev/ttyUSB0", + data={ + "port": "/dev/ttyUSB0", + "dsmr_version": "5", + "serial_id": "1234", + "serial_id_gas": "4730303738353635363037343639323231", + }, + options={ + "time_between_update": 0, + }, + ) + + mock_entry.add_to_hass(hass) + + old_unique_id = "4730303738353635363037343639323231_hourly_gas_meter_reading" + + device = device_registry.async_get_or_create( + config_entry_id=mock_entry.entry_id, + identifiers={(DOMAIN, mock_entry.entry_id)}, + name="Gas Meter", + ) + await hass.async_block_till_done() + + entity: er.RegistryEntry = entity_registry.async_get_or_create( + suggested_object_id="gas_meter_reading", + disabled_by=None, + domain=SENSOR_DOMAIN, + platform=DOMAIN, + device_id=device.id, + unique_id=old_unique_id, + config_entry=mock_entry, + ) + assert entity.unique_id == old_unique_id + await hass.async_block_till_done() + + telegram = Telegram() + telegram.add( + MBUS_DEVICE_TYPE, + CosemObject((0, 1), [{"value": "003", "unit": ""}]), + "MBUS_DEVICE_TYPE", + ) + telegram.add( + MBUS_EQUIPMENT_IDENTIFIER, + CosemObject( + (0, 1), + [{"value": "4730303738353635363037343639323231", "unit": ""}], + ), + "MBUS_EQUIPMENT_IDENTIFIER", + ) + telegram.add( + MBUS_METER_READING, + MBusObject( + (0, 1), + [ + {"value": datetime.datetime.fromtimestamp(1722749707)}, + {"value": Decimal(778.963), "unit": "m3"}, + ], + ), + "MBUS_METER_READING", + ) + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to be created + await hass.async_block_till_done() + + dev_entities = er.async_entries_for_device( + entity_registry, device.id, include_disabled_entities=True + ) + assert not dev_entities + + 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, "4730303738353635363037343639323231" + ) + == "sensor.gas_meter_reading" + ) + + async def test_migrate_gas_to_mbus_exists( hass: HomeAssistant, entity_registry: er.EntityRegistry, From 73a2ad7304cd9d4b7d9b3b191dae64601e203bdc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Aug 2024 01:39:04 -0500 Subject: [PATCH 1971/2411] Bump aiohttp to 3.10.1 (#123159) --- 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 1cc6a0fa85d..3b251e91179 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-zlib==0.1.1 -aiohttp==3.10.0 +aiohttp==3.10.1 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index c60a01663da..b3da2cdb631 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.10.0", + "aiohttp==3.10.1", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index c851927f9c6..1beefe73914 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.10.0 +aiohttp==3.10.1 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 From 85700fd80fe4229845ae7102ee22e95293d5dafe Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 5 Aug 2024 17:59:33 +1000 Subject: [PATCH 1972/2411] Fix class attribute condition in Tesla Fleet (#123162) --- homeassistant/components/tesla_fleet/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 2c5ee1b5c75..8257bf75cd0 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -86,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - vehicles: list[TeslaFleetVehicleData] = [] energysites: list[TeslaFleetEnergyData] = [] for product in products: - if "vin" in product and tesla.vehicle: + if "vin" in product and hasattr(tesla, "vehicle"): # Remove the protobuff 'cached_data' that we do not use to save memory product.pop("cached_data", None) vin = product["vin"] @@ -111,7 +111,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - device=device, ) ) - elif "energy_site_id" in product and tesla.energy: + elif "energy_site_id" in product and hasattr(tesla, "energy"): site_id = product["energy_site_id"] if not ( product["components"]["battery"] From cdb378066c14e7cd554b42e07e3801cbddacfb90 Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Mon, 5 Aug 2024 04:02:15 -0400 Subject: [PATCH 1973/2411] Add Govee H612B to the Matter transition blocklist (#123163) --- homeassistant/components/matter/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 65c3a535216..d05a7c85f9d 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -54,6 +54,7 @@ TRANSITION_BLOCKLIST = ( (4488, 514, "1.0", "1.0.0"), (4488, 260, "1.0", "1.0.0"), (5010, 769, "3.0", "1.0.0"), + (4999, 24875, "1.0", "27.0"), (4999, 25057, "1.0", "27.0"), (4448, 36866, "V1", "V1.0.0.5"), (5009, 514, "1.0", "1.0.0"), From 35a3d2306c8b77d0cf13442cc6c2c8e5cf928297 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Aug 2024 12:22:03 +0200 Subject: [PATCH 1974/2411] Bump version to 2024.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 08ee0bb77f9..7f27548a68c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index b3da2cdb631..f23c571feb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b1" +version = "2024.8.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f2d99cb059187f06dba3b26e04f1254fc42e23e0 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 5 Aug 2024 12:34:48 +0200 Subject: [PATCH 1975/2411] Use KNX UI entity platform controller class (#123128) --- homeassistant/components/knx/binary_sensor.py | 4 +- homeassistant/components/knx/button.py | 4 +- homeassistant/components/knx/climate.py | 4 +- homeassistant/components/knx/cover.py | 4 +- homeassistant/components/knx/date.py | 4 +- homeassistant/components/knx/datetime.py | 4 +- homeassistant/components/knx/fan.py | 4 +- homeassistant/components/knx/knx_entity.py | 76 +++++++++++++------ homeassistant/components/knx/light.py | 39 +++++----- homeassistant/components/knx/notify.py | 4 +- homeassistant/components/knx/number.py | 4 +- homeassistant/components/knx/scene.py | 4 +- homeassistant/components/knx/select.py | 4 +- homeassistant/components/knx/sensor.py | 4 +- .../components/knx/storage/config_store.py | 54 +++++++------ homeassistant/components/knx/switch.py | 59 +++++++------- homeassistant/components/knx/text.py | 4 +- homeassistant/components/knx/time.py | 4 +- homeassistant/components/knx/weather.py | 4 +- 19 files changed, 165 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index ff15f725fae..7d80ca55bf6 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -24,7 +24,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import ATTR_COUNTER, ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import BinarySensorSchema @@ -43,7 +43,7 @@ async def async_setup_entry( ) -class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): +class KNXBinarySensor(KnxYamlEntity, BinarySensorEntity, RestoreEntity): """Representation of a KNX binary sensor.""" _device: XknxBinarySensor diff --git a/homeassistant/components/knx/button.py b/homeassistant/components/knx/button.py index 2eb68eebe43..f6627fc527b 100644 --- a/homeassistant/components/knx/button.py +++ b/homeassistant/components/knx/button.py @@ -13,7 +13,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import CONF_PAYLOAD_LENGTH, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -31,7 +31,7 @@ async def async_setup_entry( ) -class KNXButton(KnxEntity, ButtonEntity): +class KNXButton(KnxYamlEntity, ButtonEntity): """Representation of a KNX button.""" _device: XknxRawValue diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 7470d60ef4b..9abc9023617 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -35,7 +35,7 @@ from .const import ( DOMAIN, PRESET_MODES, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import ClimateSchema ATTR_COMMAND_VALUE = "command_value" @@ -133,7 +133,7 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate: ) -class KNXClimate(KnxEntity, ClimateEntity): +class KNXClimate(KnxYamlEntity, ClimateEntity): """Representation of a KNX climate device.""" _device: XknxClimate diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 1962db0ad3f..408f746e094 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -27,7 +27,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import CoverSchema @@ -43,7 +43,7 @@ async def async_setup_entry( async_add_entities(KNXCover(knx_module, entity_config) for entity_config in config) -class KNXCover(KnxEntity, CoverEntity): +class KNXCover(KnxYamlEntity, CoverEntity): """Representation of a KNX cover.""" _device: XknxCover diff --git a/homeassistant/components/knx/date.py b/homeassistant/components/knx/date.py index 80fea63d0a6..9f04a4acd7e 100644 --- a/homeassistant/components/knx/date.py +++ b/homeassistant/components/knx/date.py @@ -31,7 +31,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -61,7 +61,7 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateDevice: ) -class KNXDateEntity(KnxEntity, DateEntity, RestoreEntity): +class KNXDateEntity(KnxYamlEntity, DateEntity, RestoreEntity): """Representation of a KNX date.""" _device: XknxDateDevice diff --git a/homeassistant/components/knx/datetime.py b/homeassistant/components/knx/datetime.py index 16ccb7474a7..8f1a25e6e3c 100644 --- a/homeassistant/components/knx/datetime.py +++ b/homeassistant/components/knx/datetime.py @@ -32,7 +32,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -62,7 +62,7 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTimeDevice: ) -class KNXDateTimeEntity(KnxEntity, DateTimeEntity, RestoreEntity): +class KNXDateTimeEntity(KnxYamlEntity, DateTimeEntity, RestoreEntity): """Representation of a KNX datetime.""" _device: XknxDateTimeDevice diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 940e241ccda..6fd87be97d1 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -21,7 +21,7 @@ from homeassistant.util.scaling import int_states_in_range from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import FanSchema DEFAULT_PERCENTAGE: Final = 50 @@ -39,7 +39,7 @@ async def async_setup_entry( async_add_entities(KNXFan(knx_module, entity_config) for entity_config in config) -class KNXFan(KnxEntity, FanEntity): +class KNXFan(KnxYamlEntity, FanEntity): """Representation of a KNX fan.""" _device: XknxFan diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index 2b8d2e71186..c81a6ee06db 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -2,30 +2,55 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any from xknx.devices import Device as XknxDevice -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity - -from .const import DOMAIN +from homeassistant.helpers.entity_platform import EntityPlatform +from homeassistant.helpers.entity_registry import RegistryEntry if TYPE_CHECKING: from . import KNXModule -SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal.{{}}" +from .storage.config_store import PlatformControllerBase -class KnxEntity(Entity): +class KnxUiEntityPlatformController(PlatformControllerBase): + """Class to manage dynamic adding and reloading of UI entities.""" + + def __init__( + self, + knx_module: KNXModule, + entity_platform: EntityPlatform, + entity_class: type[KnxUiEntity], + ) -> None: + """Initialize the UI platform.""" + self._knx_module = knx_module + self._entity_platform = entity_platform + self._entity_class = entity_class + + async def create_entity(self, unique_id: str, config: dict[str, Any]) -> None: + """Add a new UI entity.""" + await self._entity_platform.async_add_entities( + [self._entity_class(self._knx_module, unique_id, config)] + ) + + async def update_entity( + self, entity_entry: RegistryEntry, config: dict[str, Any] + ) -> None: + """Update an existing UI entities configuration.""" + await self._entity_platform.async_remove_entity(entity_entry.entity_id) + await self.create_entity(unique_id=entity_entry.unique_id, config=config) + + +class _KnxEntityBase(Entity): """Representation of a KNX entity.""" _attr_should_poll = False - - def __init__(self, knx_module: KNXModule, device: XknxDevice) -> None: - """Set up device.""" - self._knx_module = knx_module - self._device = device + _knx_module: KNXModule + _device: XknxDevice @property def name(self) -> str: @@ -49,7 +74,7 @@ class KnxEntity(Entity): """Store register state change callback and start device object.""" self._device.register_device_updated_cb(self.after_update_callback) self._device.xknx.devices.async_add(self._device) - # super call needed to have methods of mulit-inherited classes called + # super call needed to have methods of multi-inherited classes called # eg. for restoring state (like _KNXSwitch) await super().async_added_to_hass() @@ -59,19 +84,22 @@ class KnxEntity(Entity): self._device.xknx.devices.async_remove(self._device) -class KnxUIEntity(KnxEntity): +class KnxYamlEntity(_KnxEntityBase): + """Representation of a KNX entity configured from YAML.""" + + def __init__(self, knx_module: KNXModule, device: XknxDevice) -> None: + """Initialize the YAML entity.""" + self._knx_module = knx_module + self._device = device + + +class KnxUiEntity(_KnxEntityBase, ABC): """Representation of a KNX UI entity.""" _attr_unique_id: str - async def async_added_to_hass(self) -> None: - """Register callbacks when entity added to hass.""" - await super().async_added_to_hass() - self._knx_module.config_store.entities.add(self._attr_unique_id) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIGNAL_ENTITY_REMOVE.format(self._attr_unique_id), - self.async_remove, - ) - ) + @abstractmethod + def __init__( + self, knx_module: KNXModule, unique_id: str, config: dict[str, Any] + ) -> None: + """Initialize the UI entity.""" diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 1197f09354b..a2ce8f8d2cb 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -19,15 +19,18 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util from . import KNXModule from .const import CONF_SYNC_STATE, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, ColorTempModes -from .knx_entity import KnxEntity, KnxUIEntity +from .knx_entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity from .schema import LightSchema from .storage.const import ( CONF_COLOR_TEMP_MAX, @@ -63,8 +66,17 @@ async def async_setup_entry( ) -> None: """Set up light(s) for KNX platform.""" knx_module: KNXModule = hass.data[DOMAIN] + platform = async_get_current_platform() + knx_module.config_store.add_platform( + platform=Platform.LIGHT, + controller=KnxUiEntityPlatformController( + knx_module=knx_module, + entity_platform=platform, + entity_class=KnxUiLight, + ), + ) - entities: list[KnxEntity] = [] + entities: list[KnxYamlEntity | KnxUiEntity] = [] if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.LIGHT): entities.extend( KnxYamlLight(knx_module, entity_config) @@ -78,13 +90,6 @@ async def async_setup_entry( if entities: async_add_entities(entities) - @callback - def add_new_ui_light(unique_id: str, config: dict[str, Any]) -> None: - """Add KNX entity at runtime.""" - async_add_entities([KnxUiLight(knx_module, unique_id, config)]) - - knx_module.config_store.async_add_entity[Platform.LIGHT] = add_new_ui_light - def _create_yaml_light(xknx: XKNX, config: ConfigType) -> XknxLight: """Return a KNX Light device to be used within XKNX.""" @@ -519,7 +524,7 @@ class _KnxLight(LightEntity): await self._device.set_off() -class KnxYamlLight(_KnxLight, KnxEntity): +class KnxYamlLight(_KnxLight, KnxYamlEntity): """Representation of a KNX light.""" _device: XknxLight @@ -546,7 +551,7 @@ class KnxYamlLight(_KnxLight, KnxEntity): ) -class KnxUiLight(_KnxLight, KnxUIEntity): +class KnxUiLight(_KnxLight, KnxUiEntity): """Representation of a KNX light.""" _attr_has_entity_name = True @@ -556,11 +561,9 @@ class KnxUiLight(_KnxLight, KnxUIEntity): self, knx_module: KNXModule, unique_id: str, config: ConfigType ) -> None: """Initialize of KNX light.""" - super().__init__( - knx_module=knx_module, - device=_create_ui_light( - knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME] - ), + self._knx_module = knx_module + self._device = _create_ui_light( + knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME] ) self._attr_max_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MAX] self._attr_min_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MIN] diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index b349681990c..173ab3119a0 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_get_service( @@ -103,7 +103,7 @@ def _create_notification_instance(xknx: XKNX, config: ConfigType) -> XknxNotific ) -class KNXNotify(KnxEntity, NotifyEntity): +class KNXNotify(KnxYamlEntity, NotifyEntity): """Representation of a KNX notification entity.""" _device: XknxNotification diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index 3d4af503dff..cbbe91aba54 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -30,7 +30,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import NumberSchema @@ -58,7 +58,7 @@ def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue: ) -class KNXNumber(KnxEntity, RestoreNumber): +class KNXNumber(KnxYamlEntity, RestoreNumber): """Representation of a KNX number.""" _device: NumericValue diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index fc37f36dd01..2de832ae54a 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -15,7 +15,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import SceneSchema @@ -31,7 +31,7 @@ async def async_setup_entry( async_add_entities(KNXScene(knx_module, entity_config) for entity_config in config) -class KNXScene(KnxEntity, Scene): +class KNXScene(KnxYamlEntity, Scene): """Representation of a KNX scene.""" _device: XknxScene diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index 1b862010c2a..6c73bf8d573 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -30,7 +30,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import SelectSchema @@ -59,7 +59,7 @@ def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue: ) -class KNXSelect(KnxEntity, SelectEntity, RestoreEntity): +class KNXSelect(KnxYamlEntity, SelectEntity, RestoreEntity): """Representation of a KNX select.""" _device: RawValue diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index ab363e2a35f..a28c1a339e6 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -35,7 +35,7 @@ from homeassistant.util.enum import try_parse_enum from . import KNXModule from .const import ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import SensorSchema SCAN_INTERVAL = timedelta(seconds=10) @@ -141,7 +141,7 @@ def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor: ) -class KNXSensor(KnxEntity, SensorEntity): +class KNXSensor(KnxYamlEntity, SensorEntity): """Representation of a KNX sensor.""" _device: XknxSensor diff --git a/homeassistant/components/knx/storage/config_store.py b/homeassistant/components/knx/storage/config_store.py index 876fe19a4b9..ce7a705e629 100644 --- a/homeassistant/components/knx/storage/config_store.py +++ b/homeassistant/components/knx/storage/config_store.py @@ -1,6 +1,6 @@ """KNX entity configuration store.""" -from collections.abc import Callable +from abc import ABC, abstractmethod import logging from typing import Any, Final, TypedDict @@ -8,12 +8,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PLATFORM, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store from homeassistant.util.ulid import ulid_now from ..const import DOMAIN -from ..knx_entity import SIGNAL_ENTITY_REMOVE from .const import CONF_DATA _LOGGER = logging.getLogger(__name__) @@ -33,6 +31,20 @@ class KNXConfigStoreModel(TypedDict): entities: KNXEntityStoreModel +class PlatformControllerBase(ABC): + """Entity platform controller base class.""" + + @abstractmethod + async def create_entity(self, unique_id: str, config: dict[str, Any]) -> None: + """Create a new entity.""" + + @abstractmethod + async def update_entity( + self, entity_entry: er.RegistryEntry, config: dict[str, Any] + ) -> None: + """Update an existing entities configuration.""" + + class KNXConfigStore: """Manage KNX config store data.""" @@ -46,12 +58,7 @@ class KNXConfigStore: self.config_entry = config_entry self._store = Store[KNXConfigStoreModel](hass, STORAGE_VERSION, STORAGE_KEY) self.data = KNXConfigStoreModel(entities={}) - - # entities and async_add_entity are filled by platform / entity setups - self.entities: set[str] = set() # unique_id as values - self.async_add_entity: dict[ - Platform, Callable[[str, dict[str, Any]], None] - ] = {} + self._platform_controllers: dict[Platform, PlatformControllerBase] = {} async def load_data(self) -> None: """Load config store data from storage.""" @@ -62,14 +69,19 @@ class KNXConfigStore: len(self.data["entities"]), ) + def add_platform( + self, platform: Platform, controller: PlatformControllerBase + ) -> None: + """Add platform controller.""" + self._platform_controllers[platform] = controller + async def create_entity( self, platform: Platform, data: dict[str, Any] ) -> str | None: """Create a new entity.""" - if platform not in self.async_add_entity: - raise ConfigStoreException(f"Entity platform not ready: {platform}") + platform_controller = self._platform_controllers[platform] unique_id = f"knx_es_{ulid_now()}" - self.async_add_entity[platform](unique_id, data) + await platform_controller.create_entity(unique_id, data) # store data after entity was added to be sure config didn't raise exceptions self.data["entities"].setdefault(platform, {})[unique_id] = data await self._store.async_save(self.data) @@ -95,8 +107,7 @@ class KNXConfigStore: self, platform: Platform, entity_id: str, data: dict[str, Any] ) -> None: """Update an existing entity.""" - if platform not in self.async_add_entity: - raise ConfigStoreException(f"Entity platform not ready: {platform}") + platform_controller = self._platform_controllers[platform] entity_registry = er.async_get(self.hass) if (entry := entity_registry.async_get(entity_id)) is None: raise ConfigStoreException(f"Entity not found: {entity_id}") @@ -108,8 +119,7 @@ class KNXConfigStore: raise ConfigStoreException( f"Entity not found in storage: {entity_id} - {unique_id}" ) - async_dispatcher_send(self.hass, SIGNAL_ENTITY_REMOVE.format(unique_id)) - self.async_add_entity[platform](unique_id, data) + await platform_controller.update_entity(entry, data) # store data after entity is added to make sure config doesn't raise exceptions self.data["entities"][platform][unique_id] = data await self._store.async_save(self.data) @@ -125,23 +135,21 @@ class KNXConfigStore: raise ConfigStoreException( f"Entity not found in {entry.domain}: {entry.unique_id}" ) from err - try: - self.entities.remove(entry.unique_id) - except KeyError: - _LOGGER.warning("Entity not initialized when deleted: %s", entity_id) entity_registry.async_remove(entity_id) await self._store.async_save(self.data) def get_entity_entries(self) -> list[er.RegistryEntry]: - """Get entity_ids of all configured entities by platform.""" + """Get entity_ids of all UI configured entities.""" entity_registry = er.async_get(self.hass) - + unique_ids = { + uid for platform in self.data["entities"].values() for uid in platform + } return [ registry_entry for registry_entry in er.async_entries_for_config_entry( entity_registry, self.config_entry.entry_id ) - if registry_entry.unique_id in self.entities + if registry_entry.unique_id in unique_ids ] diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index a5f430e6157..ebe930957d6 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -17,9 +17,12 @@ from homeassistant.const import ( STATE_UNKNOWN, Platform, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType @@ -32,7 +35,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity, KnxUIEntity +from .knx_entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity from .schema import SwitchSchema from .storage.const import ( CONF_DEVICE_INFO, @@ -51,8 +54,17 @@ async def async_setup_entry( ) -> None: """Set up switch(es) for KNX platform.""" knx_module: KNXModule = hass.data[DOMAIN] + platform = async_get_current_platform() + knx_module.config_store.add_platform( + platform=Platform.SWITCH, + controller=KnxUiEntityPlatformController( + knx_module=knx_module, + entity_platform=platform, + entity_class=KnxUiSwitch, + ), + ) - entities: list[KnxEntity] = [] + entities: list[KnxYamlEntity | KnxUiEntity] = [] if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.SWITCH): entities.extend( KnxYamlSwitch(knx_module, entity_config) @@ -66,13 +78,6 @@ async def async_setup_entry( if entities: async_add_entities(entities) - @callback - def add_new_ui_switch(unique_id: str, config: dict[str, Any]) -> None: - """Add KNX entity at runtime.""" - async_add_entities([KnxUiSwitch(knx_module, unique_id, config)]) - - knx_module.config_store.async_add_entity[Platform.SWITCH] = add_new_ui_switch - class _KnxSwitch(SwitchEntity, RestoreEntity): """Base class for a KNX switch.""" @@ -102,7 +107,7 @@ class _KnxSwitch(SwitchEntity, RestoreEntity): await self._device.set_off() -class KnxYamlSwitch(_KnxSwitch, KnxEntity): +class KnxYamlSwitch(_KnxSwitch, KnxYamlEntity): """Representation of a KNX switch configured from YAML.""" _device: XknxSwitch @@ -125,7 +130,7 @@ class KnxYamlSwitch(_KnxSwitch, KnxEntity): self._attr_unique_id = str(self._device.switch.group_address) -class KnxUiSwitch(_KnxSwitch, KnxUIEntity): +class KnxUiSwitch(_KnxSwitch, KnxUiEntity): """Representation of a KNX switch configured from UI.""" _attr_has_entity_name = True @@ -134,21 +139,19 @@ class KnxUiSwitch(_KnxSwitch, KnxUIEntity): def __init__( self, knx_module: KNXModule, unique_id: str, config: dict[str, Any] ) -> None: - """Initialize of KNX switch.""" - super().__init__( - knx_module=knx_module, - device=XknxSwitch( - knx_module.xknx, - name=config[CONF_ENTITY][CONF_NAME], - group_address=config[DOMAIN][CONF_GA_SWITCH][CONF_GA_WRITE], - group_address_state=[ - config[DOMAIN][CONF_GA_SWITCH][CONF_GA_STATE], - *config[DOMAIN][CONF_GA_SWITCH][CONF_GA_PASSIVE], - ], - respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ], - sync_state=config[DOMAIN][CONF_SYNC_STATE], - invert=config[DOMAIN][CONF_INVERT], - ), + """Initialize KNX switch.""" + self._knx_module = knx_module + self._device = XknxSwitch( + knx_module.xknx, + name=config[CONF_ENTITY][CONF_NAME], + group_address=config[DOMAIN][CONF_GA_SWITCH][CONF_GA_WRITE], + group_address_state=[ + config[DOMAIN][CONF_GA_SWITCH][CONF_GA_STATE], + *config[DOMAIN][CONF_GA_SWITCH][CONF_GA_PASSIVE], + ], + respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ], + sync_state=config[DOMAIN][CONF_SYNC_STATE], + invert=config[DOMAIN][CONF_INVERT], ) self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY] self._attr_unique_id = unique_id diff --git a/homeassistant/components/knx/text.py b/homeassistant/components/knx/text.py index 9bca37434ac..381cb95ad32 100644 --- a/homeassistant/components/knx/text.py +++ b/homeassistant/components/knx/text.py @@ -30,7 +30,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -57,7 +57,7 @@ def _create_notification(xknx: XKNX, config: ConfigType) -> XknxNotification: ) -class KNXText(KnxEntity, TextEntity, RestoreEntity): +class KNXText(KnxYamlEntity, TextEntity, RestoreEntity): """Representation of a KNX text.""" _device: XknxNotification diff --git a/homeassistant/components/knx/time.py b/homeassistant/components/knx/time.py index 5d9225a1e41..b4e562a8869 100644 --- a/homeassistant/components/knx/time.py +++ b/homeassistant/components/knx/time.py @@ -31,7 +31,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -61,7 +61,7 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxTimeDevice: ) -class KNXTimeEntity(KnxEntity, TimeEntity, RestoreEntity): +class KNXTimeEntity(KnxYamlEntity, TimeEntity, RestoreEntity): """Representation of a KNX time.""" _device: XknxTimeDevice diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 11dae452e2f..99f4be962fe 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import WeatherSchema @@ -75,7 +75,7 @@ def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather: ) -class KNXWeather(KnxEntity, WeatherEntity): +class KNXWeather(KnxYamlEntity, WeatherEntity): """Representation of a KNX weather device.""" _device: XknxWeather From a6dfa6d4e01945e86ea1ec9168f6333f127976ac Mon Sep 17 00:00:00 2001 From: musapinar Date: Mon, 5 Aug 2024 14:21:01 +0200 Subject: [PATCH 1976/2411] Add Matter Leedarson RGBTW Bulb to the transition blocklist (#123182) --- homeassistant/components/matter/light.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index d05a7c85f9d..6e9019c46fa 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -51,18 +51,19 @@ DEFAULT_TRANSITION = 0.2 # hw version (attributeKey 0/40/8) # sw version (attributeKey 0/40/10) TRANSITION_BLOCKLIST = ( - (4488, 514, "1.0", "1.0.0"), - (4488, 260, "1.0", "1.0.0"), - (5010, 769, "3.0", "1.0.0"), - (4999, 24875, "1.0", "27.0"), - (4999, 25057, "1.0", "27.0"), - (4448, 36866, "V1", "V1.0.0.5"), - (5009, 514, "1.0", "1.0.0"), (4107, 8475, "v1.0", "v1.0"), (4107, 8550, "v1.0", "v1.0"), (4107, 8551, "v1.0", "v1.0"), - (4107, 8656, "v1.0", "v1.0"), (4107, 8571, "v1.0", "v1.0"), + (4107, 8656, "v1.0", "v1.0"), + (4448, 36866, "V1", "V1.0.0.5"), + (4456, 1011, "1.0.0", "2.00.00"), + (4488, 260, "1.0", "1.0.0"), + (4488, 514, "1.0", "1.0.0"), + (4999, 24875, "1.0", "27.0"), + (4999, 25057, "1.0", "27.0"), + (5009, 514, "1.0", "1.0.0"), + (5010, 769, "3.0", "1.0.0"), ) From b73ca874bb24d40d94cefa946d478df2a90bafeb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Aug 2024 14:23:07 +0200 Subject: [PATCH 1977/2411] Bump uvcclient to 0.11.1 (#123185) --- homeassistant/components/uvc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/licenses.py | 3 --- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/uvc/manifest.json b/homeassistant/components/uvc/manifest.json index 57e798c3fa6..0553eba320a 100644 --- a/homeassistant/components/uvc/manifest.json +++ b/homeassistant/components/uvc/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/uvc", "iot_class": "local_polling", "loggers": ["uvcclient"], - "requirements": ["uvcclient==0.11.0"] + "requirements": ["uvcclient==0.11.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9a0164f6c35..2e49dfc914f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2845,7 +2845,7 @@ upcloud-api==2.5.1 url-normalize==1.4.3 # homeassistant.components.uvc -uvcclient==0.11.0 +uvcclient==0.11.1 # homeassistant.components.roborock vacuum-map-parser-roborock==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b419e6c6430..63bdf5d3cc5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2243,7 +2243,7 @@ upcloud-api==2.5.1 url-normalize==1.4.3 # homeassistant.components.uvc -uvcclient==0.11.0 +uvcclient==0.11.1 # homeassistant.components.roborock vacuum-map-parser-roborock==0.1.2 diff --git a/script/licenses.py b/script/licenses.py index f6578bf3f65..281f440a02e 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -186,9 +186,6 @@ TODO = { "0.3.0" ), # No license https://github.com/kk7ds/mficlient/issues/4 "pyflic": AwesomeVersion("2.0.3"), # No OSI approved license CC0-1.0 Universal) - "uvcclient": AwesomeVersion( - "0.11.0" - ), # No License https://github.com/kk7ds/uvcclient/issues/7 } From 96364f04527ef564f3d44b314f382967273d92d8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Aug 2024 14:43:39 +0200 Subject: [PATCH 1978/2411] Remove deprecated asterisk_cdr integration (#123180) --- .strict-typing | 1 - homeassistant/brands/asterisk.json | 5 -- .../components/asterisk_cdr/__init__.py | 1 - .../components/asterisk_cdr/mailbox.py | 70 ------------------- .../components/asterisk_cdr/manifest.json | 8 --- homeassistant/generated/integrations.json | 21 ++---- mypy.ini | 10 --- 7 files changed, 5 insertions(+), 111 deletions(-) delete mode 100644 homeassistant/brands/asterisk.json delete mode 100644 homeassistant/components/asterisk_cdr/__init__.py delete mode 100644 homeassistant/components/asterisk_cdr/mailbox.py delete mode 100644 homeassistant/components/asterisk_cdr/manifest.json diff --git a/.strict-typing b/.strict-typing index 02d9968d247..169a361262c 100644 --- a/.strict-typing +++ b/.strict-typing @@ -95,7 +95,6 @@ homeassistant.components.aruba.* homeassistant.components.arwn.* homeassistant.components.aseko_pool_live.* homeassistant.components.assist_pipeline.* -homeassistant.components.asterisk_cdr.* homeassistant.components.asterisk_mbox.* homeassistant.components.asuswrt.* homeassistant.components.autarco.* diff --git a/homeassistant/brands/asterisk.json b/homeassistant/brands/asterisk.json deleted file mode 100644 index 1df3e660afe..00000000000 --- a/homeassistant/brands/asterisk.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "domain": "asterisk", - "name": "Asterisk", - "integrations": ["asterisk_cdr", "asterisk_mbox"] -} diff --git a/homeassistant/components/asterisk_cdr/__init__.py b/homeassistant/components/asterisk_cdr/__init__.py deleted file mode 100644 index d681a392c56..00000000000 --- a/homeassistant/components/asterisk_cdr/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The asterisk_cdr component.""" diff --git a/homeassistant/components/asterisk_cdr/mailbox.py b/homeassistant/components/asterisk_cdr/mailbox.py deleted file mode 100644 index fde4826fcee..00000000000 --- a/homeassistant/components/asterisk_cdr/mailbox.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Support for the Asterisk CDR interface.""" - -from __future__ import annotations - -import datetime -import hashlib -from typing import Any - -from homeassistant.components.asterisk_mbox import ( - DOMAIN as ASTERISK_DOMAIN, - SIGNAL_CDR_UPDATE, -) -from homeassistant.components.mailbox import Mailbox -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -MAILBOX_NAME = "asterisk_cdr" - - -async def async_get_handler( - hass: HomeAssistant, - config: ConfigType, - discovery_info: DiscoveryInfoType | None = None, -) -> Mailbox: - """Set up the Asterix CDR platform.""" - return AsteriskCDR(hass, MAILBOX_NAME) - - -class AsteriskCDR(Mailbox): - """Asterisk VM Call Data Record mailbox.""" - - def __init__(self, hass: HomeAssistant, name: str) -> None: - """Initialize Asterisk CDR.""" - super().__init__(hass, name) - self.cdr: list[dict[str, Any]] = [] - async_dispatcher_connect(self.hass, SIGNAL_CDR_UPDATE, self._update_callback) - - @callback - def _update_callback(self, msg: list[dict[str, Any]]) -> Any: - """Update the message count in HA, if needed.""" - self._build_message() - self.async_update() - - def _build_message(self) -> None: - """Build message structure.""" - cdr: list[dict[str, Any]] = [] - for entry in self.hass.data[ASTERISK_DOMAIN].cdr: - timestamp = datetime.datetime.strptime( - entry["time"], "%Y-%m-%d %H:%M:%S" - ).timestamp() - info = { - "origtime": timestamp, - "callerid": entry["callerid"], - "duration": entry["duration"], - } - sha = hashlib.sha256(str(entry).encode("utf-8")).hexdigest() - msg = ( - f"Destination: {entry['dest']}\n" - f"Application: {entry['application']}\n " - f"Context: {entry['context']}" - ) - cdr.append({"info": info, "sha": sha, "text": msg}) - self.cdr = cdr - - async def async_get_messages(self) -> list[dict[str, Any]]: - """Return a list of the current messages.""" - if not self.cdr: - self._build_message() - return self.cdr diff --git a/homeassistant/components/asterisk_cdr/manifest.json b/homeassistant/components/asterisk_cdr/manifest.json deleted file mode 100644 index 581b9dfb9a5..00000000000 --- a/homeassistant/components/asterisk_cdr/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "asterisk_cdr", - "name": "Asterisk Call Detail Records", - "codeowners": [], - "dependencies": ["asterisk_mbox"], - "documentation": "https://www.home-assistant.io/integrations/asterisk_cdr", - "iot_class": "local_polling" -} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 25a78e30017..55e7a89c669 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -502,22 +502,11 @@ "config_flow": false, "iot_class": "local_push" }, - "asterisk": { - "name": "Asterisk", - "integrations": { - "asterisk_cdr": { - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling", - "name": "Asterisk Call Detail Records" - }, - "asterisk_mbox": { - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_push", - "name": "Asterisk Voicemail" - } - } + "asterisk_mbox": { + "name": "Asterisk Voicemail", + "integration_type": "hub", + "config_flow": false, + "iot_class": "local_push" }, "asuswrt": { "name": "ASUSWRT", diff --git a/mypy.ini b/mypy.ini index 0f4f8907612..67755595e69 100644 --- a/mypy.ini +++ b/mypy.ini @@ -705,16 +705,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.asterisk_cdr.*] -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true -warn_return_any = true -warn_unreachable = true - [mypy-homeassistant.components.asterisk_mbox.*] check_untyped_defs = true disallow_incomplete_defs = true From ef237a8431752655423c48b99f626c42ec60e212 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Aug 2024 15:07:01 +0200 Subject: [PATCH 1979/2411] Fix MPD issue creation (#123187) --- homeassistant/components/mpd/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 3538b1c7973..92f0f5cfcc4 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -86,7 +86,7 @@ async def async_setup_platform( ) if ( result["type"] is FlowResultType.CREATE_ENTRY - or result["reason"] == "single_instance_allowed" + or result["reason"] == "already_configured" ): async_create_issue( hass, From 42ab8d0445fc9d0c1e647916e86ac17437deffd9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Aug 2024 15:28:49 +0200 Subject: [PATCH 1980/2411] Remove deprecated asterisk_mbox integration (#123174) --- .strict-typing | 1 - .../components/asterisk_mbox/__init__.py | 153 ------------------ .../components/asterisk_mbox/mailbox.py | 86 ---------- .../components/asterisk_mbox/manifest.json | 9 -- .../components/asterisk_mbox/strings.json | 8 - homeassistant/generated/integrations.json | 6 - mypy.ini | 10 -- requirements_all.txt | 3 - requirements_test_all.txt | 3 - script/licenses.py | 3 - tests/components/asterisk_mbox/__init__.py | 1 - tests/components/asterisk_mbox/const.py | 12 -- tests/components/asterisk_mbox/test_init.py | 36 ----- 13 files changed, 331 deletions(-) delete mode 100644 homeassistant/components/asterisk_mbox/__init__.py delete mode 100644 homeassistant/components/asterisk_mbox/mailbox.py delete mode 100644 homeassistant/components/asterisk_mbox/manifest.json delete mode 100644 homeassistant/components/asterisk_mbox/strings.json delete mode 100644 tests/components/asterisk_mbox/__init__.py delete mode 100644 tests/components/asterisk_mbox/const.py delete mode 100644 tests/components/asterisk_mbox/test_init.py diff --git a/.strict-typing b/.strict-typing index 169a361262c..1eec42ad209 100644 --- a/.strict-typing +++ b/.strict-typing @@ -95,7 +95,6 @@ homeassistant.components.aruba.* homeassistant.components.arwn.* homeassistant.components.aseko_pool_live.* homeassistant.components.assist_pipeline.* -homeassistant.components.asterisk_mbox.* homeassistant.components.asuswrt.* homeassistant.components.autarco.* homeassistant.components.auth.* diff --git a/homeassistant/components/asterisk_mbox/__init__.py b/homeassistant/components/asterisk_mbox/__init__.py deleted file mode 100644 index 3e3913b7d42..00000000000 --- a/homeassistant/components/asterisk_mbox/__init__.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Support for Asterisk Voicemail interface.""" - -import logging -from typing import Any, cast - -from asterisk_mbox import Client as asteriskClient -from asterisk_mbox.commands import ( - CMD_MESSAGE_CDR, - CMD_MESSAGE_CDR_AVAILABLE, - CMD_MESSAGE_LIST, -) -import voluptuous as vol - -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_connect -from homeassistant.helpers.issue_registry import IssueSeverity, create_issue -from homeassistant.helpers.typing import ConfigType - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "asterisk_mbox" - -SIGNAL_DISCOVER_PLATFORM = "asterisk_mbox.discover_platform" -SIGNAL_MESSAGE_REQUEST = "asterisk_mbox.message_request" -SIGNAL_MESSAGE_UPDATE = "asterisk_mbox.message_updated" -SIGNAL_CDR_UPDATE = "asterisk_mbox.message_updated" -SIGNAL_CDR_REQUEST = "asterisk_mbox.message_request" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_PORT): cv.port, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up for the Asterisk Voicemail box.""" - conf: dict[str, Any] = config[DOMAIN] - - host: str = conf[CONF_HOST] - port: int = conf[CONF_PORT] - password: str = conf[CONF_PASSWORD] - - hass.data[DOMAIN] = AsteriskData(hass, host, port, password, config) - create_issue( - hass, - DOMAIN, - "deprecated_integration", - breaks_in_ha_version="2024.9.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_integration", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Asterisk Voicemail", - "mailbox": "mailbox", - }, - ) - - return True - - -class AsteriskData: - """Store Asterisk mailbox data.""" - - def __init__( - self, - hass: HomeAssistant, - host: str, - port: int, - password: str, - config: dict[str, Any], - ) -> None: - """Init the Asterisk data object.""" - - self.hass = hass - self.config = config - self.messages: list[dict[str, Any]] | None = None - self.cdr: list[dict[str, Any]] | None = None - - dispatcher_connect(self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages) - dispatcher_connect(self.hass, SIGNAL_CDR_REQUEST, self._request_cdr) - dispatcher_connect(self.hass, SIGNAL_DISCOVER_PLATFORM, self._discover_platform) - # Only connect after signal connection to ensure we don't miss any - self.client = asteriskClient(host, port, password, self.handle_data) - - @callback - def _discover_platform(self, component: str) -> None: - _LOGGER.debug("Adding mailbox %s", component) - self.hass.async_create_task( - discovery.async_load_platform( - self.hass, "mailbox", component, {}, self.config - ) - ) - - @callback - def handle_data( - self, command: int, msg: list[dict[str, Any]] | dict[str, Any] - ) -> None: - """Handle changes to the mailbox.""" - - if command == CMD_MESSAGE_LIST: - msg = cast(list[dict[str, Any]], msg) - _LOGGER.debug("AsteriskVM sent updated message list: Len %d", len(msg)) - old_messages = self.messages - self.messages = sorted( - msg, key=lambda item: item["info"]["origtime"], reverse=True - ) - if not isinstance(old_messages, list): - async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM, DOMAIN) - async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE, self.messages) - elif command == CMD_MESSAGE_CDR: - msg = cast(dict[str, Any], msg) - _LOGGER.debug( - "AsteriskVM sent updated CDR list: Len %d", len(msg.get("entries", [])) - ) - self.cdr = msg["entries"] - async_dispatcher_send(self.hass, SIGNAL_CDR_UPDATE, self.cdr) - elif command == CMD_MESSAGE_CDR_AVAILABLE: - if not isinstance(self.cdr, list): - _LOGGER.debug("AsteriskVM adding CDR platform") - self.cdr = [] - async_dispatcher_send( - self.hass, SIGNAL_DISCOVER_PLATFORM, "asterisk_cdr" - ) - async_dispatcher_send(self.hass, SIGNAL_CDR_REQUEST) - else: - _LOGGER.debug( - "AsteriskVM sent unknown message '%d' len: %d", command, len(msg) - ) - - @callback - def _request_messages(self) -> None: - """Handle changes to the mailbox.""" - _LOGGER.debug("Requesting message list") - self.client.messages() - - @callback - def _request_cdr(self) -> None: - """Handle changes to the CDR.""" - _LOGGER.debug("Requesting CDR list") - self.client.get_cdr() diff --git a/homeassistant/components/asterisk_mbox/mailbox.py b/homeassistant/components/asterisk_mbox/mailbox.py deleted file mode 100644 index 14d54596eea..00000000000 --- a/homeassistant/components/asterisk_mbox/mailbox.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Support for the Asterisk Voicemail interface.""" - -from __future__ import annotations - -from functools import partial -import logging -from typing import Any - -from asterisk_mbox import ServerError - -from homeassistant.components.mailbox import CONTENT_TYPE_MPEG, Mailbox, StreamError -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import DOMAIN as ASTERISK_DOMAIN, AsteriskData - -_LOGGER = logging.getLogger(__name__) - -SIGNAL_MESSAGE_REQUEST = "asterisk_mbox.message_request" -SIGNAL_MESSAGE_UPDATE = "asterisk_mbox.message_updated" - - -async def async_get_handler( - hass: HomeAssistant, - config: ConfigType, - discovery_info: DiscoveryInfoType | None = None, -) -> Mailbox: - """Set up the Asterix VM platform.""" - return AsteriskMailbox(hass, ASTERISK_DOMAIN) - - -class AsteriskMailbox(Mailbox): - """Asterisk VM Sensor.""" - - def __init__(self, hass: HomeAssistant, name: str) -> None: - """Initialize Asterisk mailbox.""" - super().__init__(hass, name) - async_dispatcher_connect( - self.hass, SIGNAL_MESSAGE_UPDATE, self._update_callback - ) - - @callback - def _update_callback(self, msg: str) -> None: - """Update the message count in HA, if needed.""" - self.async_update() - - @property - def media_type(self) -> str: - """Return the supported media type.""" - return CONTENT_TYPE_MPEG - - @property - def can_delete(self) -> bool: - """Return if messages can be deleted.""" - return True - - @property - def has_media(self) -> bool: - """Return if messages have attached media files.""" - return True - - async def async_get_media(self, msgid: str) -> bytes: - """Return the media blob for the msgid.""" - - data: AsteriskData = self.hass.data[ASTERISK_DOMAIN] - client = data.client - try: - return await self.hass.async_add_executor_job( - partial(client.mp3, msgid, sync=True) - ) - except ServerError as err: - raise StreamError(err) from err - - async def async_get_messages(self) -> list[dict[str, Any]]: - """Return a list of the current messages.""" - data: AsteriskData = self.hass.data[ASTERISK_DOMAIN] - return data.messages or [] - - async def async_delete(self, msgid: str) -> bool: - """Delete the specified messages.""" - data: AsteriskData = self.hass.data[ASTERISK_DOMAIN] - client = data.client - _LOGGER.info("Deleting: %s", msgid) - await self.hass.async_add_executor_job(client.delete, msgid) - return True diff --git a/homeassistant/components/asterisk_mbox/manifest.json b/homeassistant/components/asterisk_mbox/manifest.json deleted file mode 100644 index 8348e40ba6b..00000000000 --- a/homeassistant/components/asterisk_mbox/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "asterisk_mbox", - "name": "Asterisk Voicemail", - "codeowners": [], - "documentation": "https://www.home-assistant.io/integrations/asterisk_mbox", - "iot_class": "local_push", - "loggers": ["asterisk_mbox"], - "requirements": ["asterisk_mbox==0.5.0"] -} diff --git a/homeassistant/components/asterisk_mbox/strings.json b/homeassistant/components/asterisk_mbox/strings.json deleted file mode 100644 index fb6c0637a64..00000000000 --- a/homeassistant/components/asterisk_mbox/strings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "deprecated_integration": { - "title": "The {integration_title} is being removed", - "description": "{integration_title} is being removed as the `{mailbox}` platform is being removed and {integration_title} supports no other platforms. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } - } -} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 55e7a89c669..673e37b3c12 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -502,12 +502,6 @@ "config_flow": false, "iot_class": "local_push" }, - "asterisk_mbox": { - "name": "Asterisk Voicemail", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_push" - }, "asuswrt": { "name": "ASUSWRT", "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index 67755595e69..c5478689702 100644 --- a/mypy.ini +++ b/mypy.ini @@ -705,16 +705,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.asterisk_mbox.*] -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true -warn_return_any = true -warn_unreachable = true - [mypy-homeassistant.components.asuswrt.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 2e49dfc914f..f373c0fe7e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -481,9 +481,6 @@ arris-tg2492lg==2.2.0 # homeassistant.components.ampio asmog==0.0.6 -# homeassistant.components.asterisk_mbox -asterisk_mbox==0.5.0 - # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms # homeassistant.components.samsungtv diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63bdf5d3cc5..529232f7847 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -445,9 +445,6 @@ aranet4==2.3.4 # homeassistant.components.arcam_fmj arcam-fmj==1.5.2 -# homeassistant.components.asterisk_mbox -asterisk_mbox==0.5.0 - # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms # homeassistant.components.samsungtv diff --git a/script/licenses.py b/script/licenses.py index 281f440a02e..e612b96794c 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -179,9 +179,6 @@ TODO = { "aiocache": AwesomeVersion( "0.12.2" ), # https://github.com/aio-libs/aiocache/blob/master/LICENSE all rights reserved? - "asterisk_mbox": AwesomeVersion( - "0.5.0" - ), # No license, integration is deprecated and scheduled for removal in 2024.9.0 "mficlient": AwesomeVersion( "0.3.0" ), # No license https://github.com/kk7ds/mficlient/issues/4 diff --git a/tests/components/asterisk_mbox/__init__.py b/tests/components/asterisk_mbox/__init__.py deleted file mode 100644 index 79e3675ad07..00000000000 --- a/tests/components/asterisk_mbox/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the asterisk component.""" diff --git a/tests/components/asterisk_mbox/const.py b/tests/components/asterisk_mbox/const.py deleted file mode 100644 index 945c6b28d30..00000000000 --- a/tests/components/asterisk_mbox/const.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Asterisk tests constants.""" - -from homeassistant.components.asterisk_mbox import DOMAIN -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT - -CONFIG = { - DOMAIN: { - CONF_HOST: "localhost", - CONF_PASSWORD: "password", - CONF_PORT: 1234, - } -} diff --git a/tests/components/asterisk_mbox/test_init.py b/tests/components/asterisk_mbox/test_init.py deleted file mode 100644 index d7567ea3286..00000000000 --- a/tests/components/asterisk_mbox/test_init.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Test mailbox.""" - -from collections.abc import Generator -from unittest.mock import Mock, patch - -import pytest - -from homeassistant.components.asterisk_mbox import DOMAIN -from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir -from homeassistant.setup import async_setup_component - -from .const import CONFIG - - -@pytest.fixture -def client() -> Generator[Mock]: - """Mock client.""" - with patch( - "homeassistant.components.asterisk_mbox.asteriskClient", autospec=True - ) as client: - yield client - - -async def test_repair_issue_is_created( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - client: Mock, -) -> None: - """Test repair issue is created.""" - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() - assert ( - DOMAIN, - "deprecated_integration", - ) in issue_registry.issues From b223931ac0688d559a17cbd6e4d649d99dcbb709 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:44:00 +0200 Subject: [PATCH 1981/2411] Remove deprecated proximity entity (#123158) --- .../components/proximity/__init__.py | 170 +---- homeassistant/components/proximity/helpers.py | 12 - .../components/proximity/strings.json | 11 - tests/components/proximity/test_init.py | 581 ++++-------------- 4 files changed, 119 insertions(+), 655 deletions(-) delete mode 100644 homeassistant/components/proximity/helpers.py diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 813686789a2..763274243c5 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -3,137 +3,20 @@ from __future__ import annotations import logging -from typing import cast -import voluptuous as vol - -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_DEVICES, - CONF_NAME, - CONF_UNIT_OF_MEASUREMENT, - CONF_ZONE, - STATE_UNKNOWN, - Platform, -) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers.event import ( async_track_entity_registry_updated_event, async_track_state_change_event, ) -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - ATTR_DIR_OF_TRAVEL, - ATTR_DIST_TO, - ATTR_NEAREST, - CONF_IGNORED_ZONES, - CONF_TOLERANCE, - CONF_TRACKED_ENTITIES, - DEFAULT_PROXIMITY_ZONE, - DEFAULT_TOLERANCE, - DOMAIN, - UNITS, -) +from .const import CONF_TRACKED_ENTITIES from .coordinator import ProximityConfigEntry, ProximityDataUpdateCoordinator -from .helpers import entity_used_in _LOGGER = logging.getLogger(__name__) -ZONE_SCHEMA = vol.Schema( - { - vol.Optional(CONF_ZONE, default=DEFAULT_PROXIMITY_ZONE): cv.string, - vol.Optional(CONF_DEVICES, default=[]): vol.All(cv.ensure_list, [cv.entity_id]), - vol.Optional(CONF_IGNORED_ZONES, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): cv.positive_int, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): vol.All(cv.string, vol.In(UNITS)), - } -) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - {DOMAIN: cv.schema_with_slug_keys(ZONE_SCHEMA)}, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def _async_setup_legacy( - hass: HomeAssistant, - entry: ProximityConfigEntry, - coordinator: ProximityDataUpdateCoordinator, -) -> None: - """Legacy proximity entity handling, can be removed in 2024.8.""" - friendly_name = entry.data[CONF_NAME] - proximity = Proximity(hass, friendly_name, coordinator) - await proximity.async_added_to_hass() - proximity.async_write_ha_state() - - if used_in := entity_used_in(hass, f"{DOMAIN}.{friendly_name}"): - async_create_issue( - hass, - DOMAIN, - f"deprecated_proximity_entity_{friendly_name}", - breaks_in_ha_version="2024.8.0", - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_proximity_entity", - translation_placeholders={ - "entity": f"{DOMAIN}.{friendly_name}", - "used_in": "\n- ".join([f"`{x}`" for x in used_in]), - }, - ) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Get the zones and offsets from configuration.yaml.""" - if DOMAIN in config: - for friendly_name, proximity_config in config[DOMAIN].items(): - _LOGGER.debug("import %s with config:%s", friendly_name, proximity_config) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_NAME: friendly_name, - CONF_ZONE: f"zone.{proximity_config[CONF_ZONE]}", - CONF_TRACKED_ENTITIES: proximity_config[CONF_DEVICES], - CONF_IGNORED_ZONES: [ - f"zone.{zone}" - for zone in proximity_config[CONF_IGNORED_ZONES] - ], - CONF_TOLERANCE: proximity_config[CONF_TOLERANCE], - CONF_UNIT_OF_MEASUREMENT: proximity_config.get( - CONF_UNIT_OF_MEASUREMENT, hass.config.units.length_unit - ), - }, - ) - ) - - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.8.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Proximity", - }, - ) - - return True - async def async_setup_entry(hass: HomeAssistant, entry: ProximityConfigEntry) -> bool: """Set up Proximity from a config entry.""" @@ -160,9 +43,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ProximityConfigEntry) -> await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator - if entry.source == SOURCE_IMPORT: - await _async_setup_legacy(hass, entry, coordinator) - await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR]) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True @@ -176,45 +56,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) - - -class Proximity(CoordinatorEntity[ProximityDataUpdateCoordinator]): - """Representation of a Proximity.""" - - # This entity is legacy and does not have a platform. - # We can't fix this easily without breaking changes. - _no_platform_reported = True - - def __init__( - self, - hass: HomeAssistant, - friendly_name: str, - coordinator: ProximityDataUpdateCoordinator, - ) -> None: - """Initialize the proximity.""" - super().__init__(coordinator) - self.hass = hass - self.entity_id = f"{DOMAIN}.{friendly_name}" - - self._attr_name = friendly_name - self._attr_unit_of_measurement = self.coordinator.unit_of_measurement - - @property - def data(self) -> dict[str, str | int | None]: - """Get data from coordinator.""" - return self.coordinator.data.proximity - - @property - def state(self) -> str | float: - """Return the state.""" - if isinstance(distance := self.data[ATTR_DIST_TO], str): - return distance - return self.coordinator.convert_legacy(cast(int, distance)) - - @property - def extra_state_attributes(self) -> dict[str, str]: - """Return the state attributes.""" - return { - ATTR_DIR_OF_TRAVEL: str(self.data[ATTR_DIR_OF_TRAVEL] or STATE_UNKNOWN), - ATTR_NEAREST: str(self.data[ATTR_NEAREST]), - } diff --git a/homeassistant/components/proximity/helpers.py b/homeassistant/components/proximity/helpers.py deleted file mode 100644 index af3d6d2a3bb..00000000000 --- a/homeassistant/components/proximity/helpers.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Helper functions for proximity.""" - -from homeassistant.components.automation import automations_with_entity -from homeassistant.components.script import scripts_with_entity -from homeassistant.core import HomeAssistant - - -def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]: - """Get list of related automations and scripts.""" - used_in = automations_with_entity(hass, entity_id) - used_in += scripts_with_entity(hass, entity_id) - return used_in diff --git a/homeassistant/components/proximity/strings.json b/homeassistant/components/proximity/strings.json index 72c95eeeeae..118004e908e 100644 --- a/homeassistant/components/proximity/strings.json +++ b/homeassistant/components/proximity/strings.json @@ -55,17 +55,6 @@ } }, "issues": { - "deprecated_proximity_entity": { - "title": "The proximity entity is deprecated", - "fix_flow": { - "step": { - "confirm": { - "title": "[%key:component::proximity::issues::deprecated_proximity_entity::title%]", - "description": "The proximity entity `{entity}` is deprecated and will be removed in `2024.8`. However it is used within the following configurations:\n- {used_in}\n\nPlease adjust any automations or scripts that use this deprecated Proximity entity.\nFor each tracked person or device one sensor for the distance and the direction of travel to/from the monitored zone is created. Additionally for each Proximity configuration one sensor which shows the nearest device or person to the monitored zone is created. With this you can use the Min/Max integration to determine the nearest and furthest distance." - } - } - } - }, "tracked_entity_removed": { "title": "Tracked entity has been removed", "fix_flow": { diff --git a/tests/components/proximity/test_init.py b/tests/components/proximity/test_init.py index 6c2b54cae29..456d6577c04 100644 --- a/tests/components/proximity/test_init.py +++ b/tests/components/proximity/test_init.py @@ -2,15 +2,12 @@ import pytest -from homeassistant.components import automation, script -from homeassistant.components.automation import automations_with_entity from homeassistant.components.proximity.const import ( CONF_IGNORED_ZONES, CONF_TOLERANCE, CONF_TRACKED_ENTITIES, DOMAIN, ) -from homeassistant.components.script import scripts_with_entity from homeassistant.const import ( ATTR_FRIENDLY_NAME, CONF_ZONE, @@ -20,109 +17,81 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.helpers.issue_registry as ir -from homeassistant.setup import async_setup_component from homeassistant.util import slugify from tests.common import MockConfigEntry +async def async_setup_single_entry( + hass: HomeAssistant, + zone: str, + tracked_entites: list[str], + ignored_zones: list[str], + tolerance: int, +) -> MockConfigEntry: + """Set up the proximity component with a single entry.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + title="Home", + data={ + CONF_ZONE: zone, + CONF_TRACKED_ENTITIES: tracked_entites, + CONF_IGNORED_ZONES: ignored_zones, + CONF_TOLERANCE: tolerance, + }, + ) + mock_config.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config.entry_id) + await hass.async_block_till_done() + return mock_config + + @pytest.mark.parametrize( - ("friendly_name", "config"), + "config", [ - ( - "home", - { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1", "device_tracker.test2"], - "tolerance": "1", - }, - ), - ( - "work", - { - "devices": ["device_tracker.test1"], - "tolerance": "1", - "zone": "work", - }, - ), + { + CONF_IGNORED_ZONES: ["zone.work"], + CONF_TRACKED_ENTITIES: ["device_tracker.test1", "device_tracker.test2"], + CONF_TOLERANCE: 1, + CONF_ZONE: "zone.home", + }, + { + CONF_IGNORED_ZONES: [], + CONF_TRACKED_ENTITIES: ["device_tracker.test1"], + CONF_TOLERANCE: 1, + CONF_ZONE: "zone.work", + }, ], ) -async def test_proximities( - hass: HomeAssistant, friendly_name: str, config: dict -) -> None: +async def test_proximities(hass: HomeAssistant, config: dict) -> None: """Test a list of proximities.""" - assert await async_setup_component( - hass, DOMAIN, {"proximity": {friendly_name: config}} + title = hass.states.get(config[CONF_ZONE]).name + mock_config = MockConfigEntry( + domain=DOMAIN, + title=title, + data=config, ) + mock_config.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config.entry_id) await hass.async_block_till_done() - # proximity entity - state = hass.states.get(f"proximity.{friendly_name}") - assert state.state == "not set" - assert state.attributes.get("nearest") == "not set" - assert state.attributes.get("dir_of_travel") == "not set" - hass.states.async_set(f"proximity.{friendly_name}", "0") - await hass.async_block_till_done() - state = hass.states.get(f"proximity.{friendly_name}") - assert state.state == "0" + zone_name = slugify(title) # sensor entities - state = hass.states.get(f"sensor.{friendly_name}_nearest_device") + state = hass.states.get(f"sensor.{zone_name}_nearest_device") assert state.state == STATE_UNKNOWN - for device in config["devices"]: - entity_base_name = f"sensor.{friendly_name}_{slugify(device.split('.')[-1])}" + for device in config[CONF_TRACKED_ENTITIES]: + entity_base_name = f"sensor.{zone_name}_{slugify(device.split('.')[-1])}" state = hass.states.get(f"{entity_base_name}_distance") assert state.state == STATE_UNAVAILABLE state = hass.states.get(f"{entity_base_name}_direction_of_travel") assert state.state == STATE_UNAVAILABLE -async def test_legacy_setup(hass: HomeAssistant) -> None: - """Test legacy setup only on imported entries.""" - config = { - "proximity": { - "home": { - "devices": ["device_tracker.test1"], - "tolerance": "1", - }, - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - assert hass.states.get("proximity.home") - - mock_config = MockConfigEntry( - domain=DOMAIN, - title="work", - data={ - CONF_ZONE: "zone.work", - CONF_TRACKED_ENTITIES: ["device_tracker.test2"], - CONF_IGNORED_ZONES: [], - CONF_TOLERANCE: 1, - }, - unique_id=f"{DOMAIN}_work", - ) - mock_config.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config.entry_id) - await hass.async_block_till_done() - - assert not hass.states.get("proximity.work") - - async def test_device_tracker_test1_in_zone(hass: HomeAssistant) -> None: """Test for tracker in zone.""" - config = { - "proximity": { - "home": { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1"], - "tolerance": "1", - } - } - } - - assert await async_setup_component(hass, DOMAIN, config) + await async_setup_single_entry(hass, "zone.home", ["device_tracker.test1"], [], 1) hass.states.async_set( "device_tracker.test1", @@ -131,12 +100,6 @@ async def test_device_tracker_test1_in_zone(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.state == "0" - assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "arrived" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1" @@ -150,17 +113,7 @@ async def test_device_tracker_test1_in_zone(hass: HomeAssistant) -> None: async def test_device_tracker_test1_away(hass: HomeAssistant) -> None: """Test for tracker state away.""" - config = { - "proximity": { - "home": { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1"], - "tolerance": "1", - } - } - } - - assert await async_setup_component(hass, DOMAIN, config) + await async_setup_single_entry(hass, "zone.home", ["device_tracker.test1"], [], 1) hass.states.async_set( "device_tracker.test1", @@ -170,11 +123,6 @@ async def test_device_tracker_test1_away(hass: HomeAssistant) -> None: await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "unknown" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1" @@ -190,20 +138,7 @@ async def test_device_tracker_test1_awayfurther( hass: HomeAssistant, config_zones ) -> None: """Test for tracker state away further.""" - - await hass.async_block_till_done() - - config = { - "proximity": { - "home": { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1"], - "tolerance": "1", - } - } - } - - assert await async_setup_component(hass, DOMAIN, config) + await async_setup_single_entry(hass, "zone.home", ["device_tracker.test1"], [], 1) hass.states.async_set( "device_tracker.test1", @@ -212,11 +147,6 @@ async def test_device_tracker_test1_awayfurther( ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "unknown" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1" @@ -234,11 +164,6 @@ async def test_device_tracker_test1_awayfurther( ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "away_from" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1" @@ -254,19 +179,7 @@ async def test_device_tracker_test1_awaycloser( hass: HomeAssistant, config_zones ) -> None: """Test for tracker state away closer.""" - await hass.async_block_till_done() - - config = { - "proximity": { - "home": { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1"], - "tolerance": "1", - } - } - } - - assert await async_setup_component(hass, DOMAIN, config) + await async_setup_single_entry(hass, "zone.home", ["device_tracker.test1"], [], 1) hass.states.async_set( "device_tracker.test1", @@ -275,11 +188,6 @@ async def test_device_tracker_test1_awaycloser( ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "unknown" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1" @@ -297,11 +205,6 @@ async def test_device_tracker_test1_awaycloser( ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "towards" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1" @@ -315,27 +218,11 @@ async def test_device_tracker_test1_awaycloser( async def test_all_device_trackers_in_ignored_zone(hass: HomeAssistant) -> None: """Test for tracker in ignored zone.""" - config = { - "proximity": { - "home": { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1"], - "tolerance": "1", - } - } - } - - assert await async_setup_component(hass, DOMAIN, config) + await async_setup_single_entry(hass, "zone.home", ["device_tracker.test1"], [], 1) hass.states.async_set("device_tracker.test1", "work", {"friendly_name": "test1"}) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.state == "not set" - assert state.attributes.get("nearest") == "not set" - assert state.attributes.get("dir_of_travel") == "not set" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == STATE_UNKNOWN @@ -349,28 +236,13 @@ async def test_all_device_trackers_in_ignored_zone(hass: HomeAssistant) -> None: async def test_device_tracker_test1_no_coordinates(hass: HomeAssistant) -> None: """Test for tracker with no coordinates.""" - config = { - "proximity": { - "home": { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1"], - "tolerance": "1", - } - } - } - - assert await async_setup_component(hass, DOMAIN, config) + await async_setup_single_entry(hass, "zone.home", ["device_tracker.test1"], [], 1) hass.states.async_set( "device_tracker.test1", "not_home", {"friendly_name": "test1"} ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "not set" - assert state.attributes.get("dir_of_travel") == "not set" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == STATE_UNKNOWN @@ -384,19 +256,8 @@ async def test_device_tracker_test1_no_coordinates(hass: HomeAssistant) -> None: async def test_device_tracker_test1_awayfurther_a_bit(hass: HomeAssistant) -> None: """Test for tracker states.""" - assert await async_setup_component( - hass, - DOMAIN, - { - "proximity": { - "home": { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1"], - "tolerance": 1000, - "zone": "home", - } - } - }, + await async_setup_single_entry( + hass, "zone.home", ["device_tracker.test1"], ["zone.work"], 1000 ) hass.states.async_set( @@ -406,11 +267,6 @@ async def test_device_tracker_test1_awayfurther_a_bit(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "unknown" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1" @@ -428,11 +284,6 @@ async def test_device_tracker_test1_awayfurther_a_bit(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "stationary" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1" @@ -446,17 +297,13 @@ async def test_device_tracker_test1_awayfurther_a_bit(hass: HomeAssistant) -> No async def test_device_trackers_in_zone(hass: HomeAssistant) -> None: """Test for trackers in zone.""" - config = { - "proximity": { - "home": { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1", "device_tracker.test2"], - "tolerance": "1", - } - } - } - - assert await async_setup_component(hass, DOMAIN, config) + await async_setup_single_entry( + hass, + "zone.home", + ["device_tracker.test1", "device_tracker.test2"], + ["zone.work"], + 1, + ) hass.states.async_set( "device_tracker.test1", @@ -471,14 +318,6 @@ async def test_device_trackers_in_zone(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.state == "0" - assert (state.attributes.get("nearest") == "test1, test2") or ( - state.attributes.get("nearest") == "test2, test1" - ) - assert state.attributes.get("dir_of_travel") == "arrived" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1, test2" @@ -495,30 +334,18 @@ async def test_device_tracker_test1_awayfurther_than_test2_first_test1( hass: HomeAssistant, config_zones ) -> None: """Test for tracker ordering.""" - await hass.async_block_till_done() - hass.states.async_set( "device_tracker.test1", "not_home", {"friendly_name": "test1"} ) - await hass.async_block_till_done() hass.states.async_set( "device_tracker.test2", "not_home", {"friendly_name": "test2"} ) - await hass.async_block_till_done() - - assert await async_setup_component( + await async_setup_single_entry( hass, - DOMAIN, - { - "proximity": { - "home": { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1", "device_tracker.test2"], - "tolerance": "1", - "zone": "home", - } - } - }, + "zone.home", + ["device_tracker.test1", "device_tracker.test2"], + ["zone.work"], + 1, ) hass.states.async_set( @@ -528,11 +355,6 @@ async def test_device_tracker_test1_awayfurther_than_test2_first_test1( ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "unknown" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1" @@ -556,11 +378,6 @@ async def test_device_tracker_test1_awayfurther_than_test2_first_test1( ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "unknown" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1" @@ -582,28 +399,19 @@ async def test_device_tracker_test1_awayfurther_than_test2_first_test2( hass: HomeAssistant, config_zones ) -> None: """Test for tracker ordering.""" - await hass.async_block_till_done() - hass.states.async_set( "device_tracker.test1", "not_home", {"friendly_name": "test1"} ) - await hass.async_block_till_done() hass.states.async_set( "device_tracker.test2", "not_home", {"friendly_name": "test2"} ) - await hass.async_block_till_done() - assert await async_setup_component( + + await async_setup_single_entry( hass, - DOMAIN, - { - "proximity": { - "home": { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1", "device_tracker.test2"], - "zone": "home", - } - } - }, + "zone.home", + ["device_tracker.test1", "device_tracker.test2"], + ["zone.work"], + 1, ) hass.states.async_set( @@ -613,11 +421,6 @@ async def test_device_tracker_test1_awayfurther_than_test2_first_test2( ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test2" - assert state.attributes.get("dir_of_travel") == "unknown" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test2" @@ -641,11 +444,6 @@ async def test_device_tracker_test1_awayfurther_than_test2_first_test2( ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "unknown" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1" @@ -670,23 +468,15 @@ async def test_device_tracker_test1_awayfurther_test2_in_ignored_zone( hass.states.async_set( "device_tracker.test1", "not_home", {"friendly_name": "test1"} ) - await hass.async_block_till_done() hass.states.async_set("device_tracker.test2", "work", {"friendly_name": "test2"}) - await hass.async_block_till_done() - assert await async_setup_component( - hass, - DOMAIN, - { - "proximity": { - "home": { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1", "device_tracker.test2"], - "zone": "home", - } - } - }, - ) + await async_setup_single_entry( + hass, + "zone.home", + ["device_tracker.test1", "device_tracker.test2"], + ["zone.work"], + 1, + ) hass.states.async_set( "device_tracker.test1", "not_home", @@ -694,11 +484,6 @@ async def test_device_tracker_test1_awayfurther_test2_in_ignored_zone( ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "unknown" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1" @@ -720,29 +505,19 @@ async def test_device_tracker_test1_awayfurther_test2_first( hass: HomeAssistant, config_zones ) -> None: """Test for tracker state.""" - await hass.async_block_till_done() - hass.states.async_set( "device_tracker.test1", "not_home", {"friendly_name": "test1"} ) - await hass.async_block_till_done() hass.states.async_set( "device_tracker.test2", "not_home", {"friendly_name": "test2"} ) - await hass.async_block_till_done() - assert await async_setup_component( + await async_setup_single_entry( hass, - DOMAIN, - { - "proximity": { - "home": { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1", "device_tracker.test2"], - "zone": "home", - } - } - }, + "zone.home", + ["device_tracker.test1", "device_tracker.test2"], + ["zone.work"], + 1, ) hass.states.async_set( @@ -776,11 +551,6 @@ async def test_device_tracker_test1_awayfurther_test2_first( hass.states.async_set("device_tracker.test1", "work", {"friendly_name": "test1"}) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test2" - assert state.attributes.get("dir_of_travel") == "unknown" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test2" @@ -803,7 +573,6 @@ async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone( ) -> None: """Test for tracker states.""" await hass.async_block_till_done() - hass.states.async_set( "device_tracker.test1", "not_home", {"friendly_name": "test1"} ) @@ -813,20 +582,28 @@ async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone( ) await hass.async_block_till_done() - assert await async_setup_component( + await async_setup_single_entry( hass, - DOMAIN, - { - "proximity": { - "home": { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1", "device_tracker.test2"], - "zone": "home", - } - } - }, + "zone.home", + ["device_tracker.test1", "device_tracker.test2"], + ["zone.work"], + 1, ) + # assert await async_setup_component( + # hass, + # DOMAIN, + # { + # "proximity": { + # "home": { + # "ignored_zones": ["zone.work"], + # "devices": ["device_tracker.test1", "device_tracker.test2"], + # "zone": "home", + # } + # } + # }, + # ) + hass.states.async_set( "device_tracker.test1", "not_home", @@ -834,11 +611,6 @@ async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone( ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "unknown" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1" @@ -862,11 +634,6 @@ async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone( ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test2" - assert state.attributes.get("dir_of_travel") == "unknown" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test2" @@ -890,11 +657,6 @@ async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone( ) await hass.async_block_till_done() - # proximity entity - state = hass.states.get("proximity.home") - assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "unknown" - # sensor entities state = hass.states.get("sensor.home_nearest_device") assert state.state == "test1" @@ -914,22 +676,10 @@ async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone( async def test_nearest_sensors(hass: HomeAssistant, config_zones) -> None: """Test for nearest sensors.""" - mock_config = MockConfigEntry( - domain=DOMAIN, - title="home", - data={ - CONF_ZONE: "zone.home", - CONF_TRACKED_ENTITIES: ["device_tracker.test1", "device_tracker.test2"], - CONF_IGNORED_ZONES: [], - CONF_TOLERANCE: 1, - }, - unique_id=f"{DOMAIN}_home", + await async_setup_single_entry( + hass, "zone.home", ["device_tracker.test1", "device_tracker.test2"], [], 1 ) - mock_config.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config.entry_id) - await hass.async_block_till_done() - hass.states.async_set( "device_tracker.test1", "not_home", @@ -1038,71 +788,6 @@ async def test_nearest_sensors(hass: HomeAssistant, config_zones) -> None: assert state.state == STATE_UNKNOWN -async def test_create_deprecated_proximity_issue( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, -) -> None: - """Test we create an issue for deprecated proximity entities used in automations and scripts.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "alias": "test", - "trigger": {"platform": "state", "entity_id": "proximity.home"}, - "action": { - "service": "automation.turn_on", - "target": {"entity_id": "automation.test"}, - }, - } - }, - ) - assert await async_setup_component( - hass, - script.DOMAIN, - { - script.DOMAIN: { - "test": { - "sequence": [ - { - "condition": "state", - "entity_id": "proximity.home", - "state": "home", - }, - ], - } - } - }, - ) - config = { - "proximity": { - "home": { - "ignored_zones": ["work"], - "devices": ["device_tracker.test1", "device_tracker.test2"], - "tolerance": "1", - }, - "work": {"tolerance": "1", "zone": "work"}, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - automation_entities = automations_with_entity(hass, "proximity.home") - assert len(automation_entities) == 1 - assert automation_entities[0] == "automation.test" - - script_entites = scripts_with_entity(hass, "proximity.home") - - assert len(script_entites) == 1 - assert script_entites[0] == "script.test" - assert issue_registry.async_get_issue(DOMAIN, "deprecated_proximity_entity_home") - - assert not issue_registry.async_get_issue( - DOMAIN, "deprecated_proximity_entity_work" - ) - - async def test_create_removed_tracked_entity_issue( hass: HomeAssistant, issue_registry: ir.IssueRegistry, @@ -1119,22 +804,10 @@ async def test_create_removed_tracked_entity_issue( hass.states.async_set(t1.entity_id, "not_home") hass.states.async_set(t2.entity_id, "not_home") - mock_config = MockConfigEntry( - domain=DOMAIN, - title="home", - data={ - CONF_ZONE: "zone.home", - CONF_TRACKED_ENTITIES: [t1.entity_id, t2.entity_id], - CONF_IGNORED_ZONES: [], - CONF_TOLERANCE: 1, - }, - unique_id=f"{DOMAIN}_home", + await async_setup_single_entry( + hass, "zone.home", [t1.entity_id, t2.entity_id], [], 1 ) - mock_config.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config.entry_id) - await hass.async_block_till_done() - sensor_t1 = f"sensor.home_{t1.entity_id.split('.')[-1]}_distance" sensor_t2 = f"sensor.home_{t2.entity_id.split('.')[-1]}_distance" @@ -1168,22 +841,10 @@ async def test_track_renamed_tracked_entity( hass.states.async_set(t1.entity_id, "not_home") - mock_config = MockConfigEntry( - domain=DOMAIN, - title="home", - data={ - CONF_ZONE: "zone.home", - CONF_TRACKED_ENTITIES: [t1.entity_id], - CONF_IGNORED_ZONES: [], - CONF_TOLERANCE: 1, - }, - unique_id=f"{DOMAIN}_home", + mock_config = await async_setup_single_entry( + hass, "zone.home", [t1.entity_id], ["zone.work"], 1 ) - mock_config.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config.entry_id) - await hass.async_block_till_done() - sensor_t1 = f"sensor.home_{t1.entity_id.split('.')[-1]}_distance" entity = entity_registry.async_get(sensor_t1) @@ -1216,28 +877,16 @@ async def test_sensor_unique_ids( hass.states.async_set("device_tracker.test2", "not_home") - mock_config = MockConfigEntry( - domain=DOMAIN, - title="home", - data={ - CONF_ZONE: "zone.home", - CONF_TRACKED_ENTITIES: [t1.entity_id, "device_tracker.test2"], - CONF_IGNORED_ZONES: [], - CONF_TOLERANCE: 1, - }, - unique_id=f"{DOMAIN}_home", + mock_config = await async_setup_single_entry( + hass, "zone.home", [t1.entity_id, "device_tracker.test2"], ["zone.work"], 1 ) - mock_config.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config.entry_id) - await hass.async_block_till_done() - sensor_t1 = "sensor.home_test_tracker_1_distance" entity = entity_registry.async_get(sensor_t1) assert entity assert entity.unique_id == f"{mock_config.entry_id}_{t1.id}_dist_to_zone" state = hass.states.get(sensor_t1) - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home Test tracker 1 Distance" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Home Test tracker 1 Distance" entity = entity_registry.async_get("sensor.home_test2_distance") assert entity From 94542d42fae3c11f400cdb6367d9bb24bdf40637 Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:17:46 +0200 Subject: [PATCH 1982/2411] Fix state icon for closed valve entities (#123190) --- homeassistant/components/valve/icons.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/valve/icons.json b/homeassistant/components/valve/icons.json index 1261d1cc398..2c887ebf273 100644 --- a/homeassistant/components/valve/icons.json +++ b/homeassistant/components/valve/icons.json @@ -3,7 +3,7 @@ "_": { "default": "mdi:valve-open", "state": { - "off": "mdi:valve-closed" + "closed": "mdi:valve-closed" } }, "gas": { @@ -12,7 +12,7 @@ "water": { "default": "mdi:valve-open", "state": { - "off": "mdi:valve-closed" + "closed": "mdi:valve-closed" } } }, From 844ccf461f9585d5381cab07296f61815adb89a3 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:44:14 +0200 Subject: [PATCH 1983/2411] Remove unnecessary config schema definitions (#123197) --- homeassistant/components/airvisual/__init__.py | 3 --- .../components/ambient_station/__init__.py | 3 --- .../components/android_ip_webcam/__init__.py | 4 ---- homeassistant/components/apcupsd/__init__.py | 3 --- homeassistant/components/arcam_fmj/__init__.py | 3 --- .../components/bmw_connected_drive/__init__.py | 2 -- homeassistant/components/cast/__init__.py | 3 +-- homeassistant/components/cloudflare/__init__.py | 3 --- homeassistant/components/coinbase/__init__.py | 5 +---- homeassistant/components/daikin/__init__.py | 3 --- homeassistant/components/directv/__init__.py | 3 --- homeassistant/components/glances/__init__.py | 2 -- .../hunterdouglas_powerview/__init__.py | 2 -- homeassistant/components/icloud/__init__.py | 2 -- .../components/islamic_prayer_times/__init__.py | 4 +--- homeassistant/components/local_ip/__init__.py | 5 +---- homeassistant/components/luftdaten/__init__.py | 3 --- homeassistant/components/mikrotik/__init__.py | 4 +--- homeassistant/components/mysensors/__init__.py | 4 ---- homeassistant/components/nexia/__init__.py | 3 --- homeassistant/components/nextcloud/__init__.py | 4 +--- homeassistant/components/notion/__init__.py | 7 +------ homeassistant/components/nuheat/__init__.py | 3 --- homeassistant/components/nzbget/__init__.py | 1 - homeassistant/components/pi_hole/__init__.py | 3 +-- homeassistant/components/powerwall/__init__.py | 3 --- .../components/pvpc_hourly_pricing/__init__.py | 2 -- homeassistant/components/rachio/__init__.py | 3 --- .../components/rainmachine/__init__.py | 1 - homeassistant/components/roku/__init__.py | 3 --- homeassistant/components/samsungtv/__init__.py | 7 +------ homeassistant/components/sentry/__init__.py | 5 +---- homeassistant/components/simplisafe/__init__.py | 2 -- homeassistant/components/solaredge/__init__.py | 5 +---- .../components/somfy_mylink/__init__.py | 3 --- .../components/synology_dsm/__init__.py | 5 +---- .../components/tankerkoenig/__init__.py | 3 --- .../components/totalconnect/__init__.py | 3 --- homeassistant/components/tradfri/__init__.py | 2 -- homeassistant/components/upnp/__init__.py | 3 +-- homeassistant/components/verisure/__init__.py | 3 --- homeassistant/components/vesync/__init__.py | 3 --- .../ambient_station/test_config_flow.py | 2 +- .../islamic_prayer_times/test_config_flow.py | 5 ++--- .../islamic_prayer_times/test_init.py | 17 ++++++++--------- tests/components/local_ip/test_init.py | 2 +- tests/components/nextcloud/test_config_flow.py | 2 +- tests/components/sentry/conftest.py | 2 +- 48 files changed, 27 insertions(+), 141 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 4d0563ddce8..60fdbf12ca1 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -31,7 +31,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import ( aiohttp_client, - config_validation as cv, device_registry as dr, entity_registry as er, ) @@ -62,8 +61,6 @@ PLATFORMS = [Platform.SENSOR] DEFAULT_ATTRIBUTION = "Data provided by AirVisual" -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - @callback def async_get_cloud_api_update_interval( diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index d0b04e53e67..469ad7e6e06 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -17,7 +17,6 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.helpers.entity_registry as er @@ -25,7 +24,6 @@ import homeassistant.helpers.entity_registry as er from .const import ( ATTR_LAST_DATA, CONF_APP_KEY, - DOMAIN, LOGGER, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX, @@ -37,7 +35,6 @@ DATA_CONFIG = "config" DEFAULT_SOCKET_MIN_RETRY = 15 -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) type AmbientStationConfigEntry = ConfigEntry[AmbientStation] diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index db50d6d3e1a..3772fe4642b 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -14,7 +14,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from .const import DOMAIN from .coordinator import AndroidIPCamDataUpdateCoordinator @@ -27,9 +26,6 @@ PLATFORMS: list[Platform] = [ ] -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Android IP Webcam from a config entry.""" websession = async_get_clientsession(hass) diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index 73ed721158d..7293a42f7e7 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -8,7 +8,6 @@ from typing import Final from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from .const import DOMAIN from .coordinator import APCUPSdCoordinator @@ -17,8 +16,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS: Final = (Platform.BINARY_SENSOR, Platform.SENSOR) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Use config values to set up a function enabling status retrieval.""" diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index e1a2ee0a046..71639ed8388 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -11,12 +11,10 @@ from arcam.fmj.client import Client from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( DEFAULT_SCAN_INTERVAL, - DOMAIN, SIGNAL_CLIENT_DATA, SIGNAL_CLIENT_STARTED, SIGNAL_CLIENT_STOPPED, @@ -26,7 +24,6 @@ type ArcamFmjConfigEntry = ConfigEntry[Client] _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.MEDIA_PLAYER] diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 495359ca314..9e43cfc4187 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -23,8 +23,6 @@ from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - SERVICE_SCHEMA = vol.Schema( vol.Any( {vol.Required(ATTR_VIN): cv.string}, diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index b41dc9ddb41..e72eb196b61 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) @@ -19,7 +19,6 @@ from homeassistant.helpers.integration_platform import ( from . import home_assistant_cast from .const import DOMAIN -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.MEDIA_PLAYER] diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index 5934e43f8a2..bd27be71d18 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -18,7 +18,6 @@ from homeassistant.exceptions import ( HomeAssistantError, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.location import async_detect_location_info from homeassistant.util.network import is_ipv4_address @@ -27,8 +26,6 @@ from .const import CONF_RECORDS, DEFAULT_UPDATE_INTERVAL, DOMAIN, SERVICE_UPDATE _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Cloudflare from a config entry.""" diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 0a34168b4ee..a231bb5cda0 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -11,7 +11,7 @@ from coinbase.wallet.error import AuthenticationError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.util import Throttle from .const import ( @@ -29,9 +29,6 @@ PLATFORMS = [Platform.SENSOR] MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Coinbase from a config entry.""" diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 1bd833f354d..4da6bcee50b 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -23,7 +23,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.util import Throttle @@ -36,8 +35,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.SWITCH] -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Establish connection with Daikin.""" diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 50eb6bc7959..e59fa4e9d0d 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -10,13 +10,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE] SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index f83b39d1cf9..0ddd8a86979 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -27,7 +27,6 @@ from homeassistant.exceptions import ( ConfigEntryNotReady, HomeAssistantError, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -36,7 +35,6 @@ from .coordinator import GlancesDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 6f63641b722..f8c7ac43b94 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -13,7 +13,6 @@ from homeassistant.const import CONF_API_VERSION, CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from .const import DOMAIN, HUB_EXCEPTIONS from .coordinator import PowerviewShadeUpdateCoordinator @@ -22,7 +21,6 @@ from .shade_data import PowerviewShadeData PARALLEL_UPDATES = 1 -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [ Platform.BUTTON, diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 431a1abd2e1..5bdfd00dc60 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -69,8 +69,6 @@ SERVICE_SCHEMA_LOST_DEVICE = vol.Schema( } ) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an iCloud account from a config entry.""" diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 089afc88564..d61eba343ac 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -7,14 +7,12 @@ import logging from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers import entity_registry as er -from .const import DOMAIN from .coordinator import IslamicPrayerDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py index 45ddbed7150..72f5d4f7a43 100644 --- a/homeassistant/components/local_ip/__init__.py +++ b/homeassistant/components/local_ip/__init__.py @@ -2,11 +2,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, PLATFORMS - -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +from .const import PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 2ef7864566f..9079b056731 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -15,7 +15,6 @@ from luftdaten.exceptions import LuftdatenError from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN @@ -24,8 +23,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR] -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Sensor.Community as config entry.""" diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 9f2b40bf1c8..cecf96a6c3e 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -4,14 +4,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import device_registry as dr from .const import ATTR_MANUFACTURER, DOMAIN from .coordinator import MikrotikDataUpdateCoordinator, get_api from .errors import CannotConnect, LoginError -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - PLATFORMS = [Platform.DEVICE_TRACKER] type MikrotikConfigEntry = ConfigEntry[MikrotikDataUpdateCoordinator] diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index ed18b890a24..8ebcbe0e2fe 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -10,7 +10,6 @@ from mysensors import BaseAsyncGateway from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntry from .const import ( @@ -32,9 +31,6 @@ _LOGGER = logging.getLogger(__name__) DATA_HASS_CONFIG = "hass_config" -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an instance of the MySensors integration. diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 4d0993d3569..9bc76fdcfdc 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -12,7 +12,6 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from .const import CONF_BRAND, DOMAIN, PLATFORMS from .coordinator import NexiaDataUpdateCoordinator @@ -21,8 +20,6 @@ from .util import is_invalid_auth_code _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - async def async_setup_entry(hass: HomeAssistant, entry: NexiaConfigEntry) -> bool: """Configure the base Nexia device for Home Assistant.""" diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 9e328e8e58d..a487a3f1414 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -19,14 +19,12 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers import entity_registry as er -from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator PLATFORMS = (Platform.SENSOR, Platform.BINARY_SENSOR, Platform.UPDATE) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 1793a0cfd47..00bded5c3a0 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -14,11 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import ( - config_validation as cv, - device_registry as dr, - entity_registry as er, -) +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -49,7 +45,6 @@ ATTR_SYSTEM_NAME = "system_name" DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) # Define a map of old-API task types to new-API listener types: TASK_TYPE_TO_LISTENER_MAP: dict[str, ListenerKind] = { diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index 8eeee1f3f95..fdb49688eba 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -11,15 +11,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import CONF_SERIAL_NUMBER, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - def _get_thermostat(api, serial_number): """Authenticate and create the thermostat object.""" diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 61b3f98739c..d47ac78c9d0 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -23,7 +23,6 @@ from .coordinator import NZBGetDataUpdateCoordinator PLATFORMS = [Platform.SENSOR, Platform.SWITCH] -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) SPEED_LIMIT_SCHEMA = vol.Schema( {vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int} diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index ad36b664994..bf314e96dec 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -20,7 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( @@ -33,7 +33,6 @@ from .const import CONF_STATISTICS_ONLY, DOMAIN, MIN_TIME_BETWEEN_UPDATES _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [ Platform.BINARY_SENSOR, diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index be09c729237..0b6f889b90a 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -22,7 +22,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_create_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.network import is_ip_address @@ -34,8 +33,6 @@ from .models import ( PowerwallRuntimeData, ) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index a92f159d172..6327164e3c8 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -3,7 +3,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv import homeassistant.helpers.entity_registry as er from .const import ATTR_POWER, ATTR_POWER_P3, DOMAIN @@ -11,7 +10,6 @@ from .coordinator import ElecPricesDataUpdateCoordinator from .helpers import get_enabled_sensor_keys PLATFORMS: list[Platform] = [Platform.SENSOR] -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index a5922e0cb95..6976d3f5ba6 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -11,7 +11,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_WEBHOOK_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv from .const import CONF_CLOUDHOOK_URL, CONF_MANUAL_RUN_MINS, DOMAIN from .device import RachioPerson @@ -25,8 +24,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.BINARY_SENSOR, Platform.CALENDAR, Platform.SWITCH] -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index cfbc95cf009..b10d562ac67 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -58,7 +58,6 @@ from .model import RainMachineEntityDescription DEFAULT_SSL = True -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [ Platform.BINARY_SENSOR, diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 0620207a8ee..7515f375054 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -5,13 +5,10 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv from .const import DOMAIN from .coordinator import RokuDataUpdateCoordinator -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - PLATFORMS = [ Platform.BINARY_SENSOR, Platform.MEDIA_PLAYER, diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 992c86d5d7e..f3b967a485e 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -23,11 +23,7 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import ( - config_validation as cv, - device_registry as dr, - entity_registry as er, -) +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.debounce import Debouncer from .bridge import ( @@ -53,7 +49,6 @@ from .coordinator import SamsungTVDataUpdateCoordinator PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE] -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) SamsungTVConfigEntry = ConfigEntry[SamsungTVDataUpdateCoordinator] diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index 8c042621db6..904d493a863 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -17,7 +17,7 @@ from homeassistant.const import ( __version__ as current_version, ) from homeassistant.core import HomeAssistant, get_release_channel -from homeassistant.helpers import config_validation as cv, entity_platform, instance_id +from homeassistant.helpers import entity_platform, instance_id from homeassistant.helpers.event import async_call_later from homeassistant.helpers.system_info import async_get_system_info from homeassistant.loader import Integration, async_get_custom_components @@ -36,12 +36,9 @@ from .const import ( DEFAULT_LOGGING_EVENT_LEVEL, DEFAULT_LOGGING_LEVEL, DEFAULT_TRACING_SAMPLE_RATE, - DOMAIN, ENTITY_COMPONENTS, ) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - LOGGER_INFO_REGEX = re.compile(r"^(\w+)\.?(\w+)?\.?(\w+)?\.?(\w+)?(?:\..*)?$") diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 29f53eafffb..b23358c985f 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -212,8 +212,6 @@ WEBSOCKET_EVENTS_TO_FIRE_HASS_EVENT = [ EVENT_USER_INITIATED_TEST, ] -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - @callback def _async_get_system_for_service_call( diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index 41448bae98d..206a2499494 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -11,13 +11,10 @@ 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 -import homeassistant.helpers.config_validation as cv -from .const import CONF_SITE_ID, DOMAIN, LOGGER +from .const import CONF_SITE_ID, LOGGER from .types import SolarEdgeConfigEntry, SolarEdgeData -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - PLATFORMS = [Platform.SENSOR] diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index ed9652de55a..89796f5ce46 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -8,7 +8,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv from .const import CONF_SYSTEM_ID, DATA_SOMFY_MYLINK, DOMAIN, MYLINK_STATUS, PLATFORMS @@ -16,8 +15,6 @@ UNDO_UPDATE_LISTENER = "undo_update_listener" _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Somfy MyLink from a config entry.""" diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index d42dacca638..3619619782e 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import device_registry as dr from .common import SynoApi, raise_config_entry_auth_error from .const import ( @@ -33,9 +33,6 @@ from .coordinator import ( from .models import SynologyDSMData from .service import async_setup_services -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index 78bced05b36..a500549a648 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -4,15 +4,12 @@ from __future__ import annotations from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv from .const import DEFAULT_SCAN_INTERVAL, DOMAIN from .coordinator import TankerkoenigConfigEntry, TankerkoenigDataUpdateCoordinator PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - async def async_setup_entry( hass: HomeAssistant, entry: TankerkoenigConfigEntry diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index bb19697b1e7..0d8b915770a 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -7,15 +7,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -import homeassistant.helpers.config_validation as cv from .const import AUTO_BYPASS, CONF_USERCODES, DOMAIN from .coordinator import TotalConnectDataUpdateCoordinator PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.BUTTON] -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up upon config entry in user interface.""" diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 2e267ffaa14..0060310e6c2 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -14,7 +14,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -35,7 +34,6 @@ from .const import ( ) from .coordinator import TradfriDeviceDataUpdateCoordinator -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [ Platform.COVER, Platform.FAN, diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 9b51e548f80..214521ee9c0 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import device_registry as dr from .const import ( CONFIG_ENTRY_FORCE_POLL, @@ -35,7 +35,6 @@ NOTIFICATION_TITLE = "UPnP/IGD Setup" PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) type UpnpConfigEntry = ConfigEntry[UpnpDataUpdateCoordinator] diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 9e5f0ca2703..0f8c8d936ef 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -12,7 +12,6 @@ from homeassistant.const import CONF_EMAIL, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry as er -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR from .const import CONF_LOCK_DEFAULT_CODE, DOMAIN, LOGGER @@ -27,8 +26,6 @@ PLATFORMS = [ Platform.SWITCH, ] -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.""" diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 7dceb1b3f8f..04547d33dea 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -7,7 +7,6 @@ from pyvesync import VeSync from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from .common import async_process_devices @@ -26,8 +25,6 @@ PLATFORMS = [Platform.FAN, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Vesync as config entry.""" diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py index 19ae9828c22..e4c8efabc20 100644 --- a/tests/components/ambient_station/test_config_flow.py +++ b/tests/components/ambient_station/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch from aioambient.errors import AmbientError import pytest -from homeassistant.components.ambient_station import CONF_APP_KEY, DOMAIN +from homeassistant.components.ambient_station.const import CONF_APP_KEY, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index cb37a6b147d..695be636a84 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -3,7 +3,6 @@ import pytest from homeassistant import config_entries -from homeassistant.components import islamic_prayer_times from homeassistant.components.islamic_prayer_times.const import ( CONF_CALC_METHOD, CONF_LAT_ADJ_METHOD, @@ -24,7 +23,7 @@ pytestmark = pytest.mark.usefixtures("mock_setup_entry") async def test_flow_works(hass: HomeAssistant) -> None: """Test user config.""" result = await hass.config_entries.flow.async_init( - islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -76,7 +75,7 @@ async def test_integration_already_configured(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index 025a202e6da..7961b79676b 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -6,8 +6,7 @@ from unittest.mock import patch from freezegun import freeze_time import pytest -from homeassistant.components import islamic_prayer_times -from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD +from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD, DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE @@ -30,7 +29,7 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None: """Test that Islamic Prayer Times is configured successfully.""" entry = MockConfigEntry( - domain=islamic_prayer_times.DOMAIN, + domain=DOMAIN, data={}, ) entry.add_to_hass(hass) @@ -48,7 +47,7 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None: async def test_unload_entry(hass: HomeAssistant) -> None: """Test removing Islamic Prayer Times.""" entry = MockConfigEntry( - domain=islamic_prayer_times.DOMAIN, + domain=DOMAIN, data={}, ) entry.add_to_hass(hass) @@ -66,7 +65,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None: async def test_options_listener(hass: HomeAssistant) -> None: """Ensure updating options triggers a coordinator refresh.""" - entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) + entry = MockConfigEntry(domain=DOMAIN, data={}) entry.add_to_hass(hass) with ( @@ -110,13 +109,13 @@ async def test_migrate_unique_id( old_unique_id: str, ) -> None: """Test unique id migration.""" - entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) + entry = MockConfigEntry(domain=DOMAIN, data={}) entry.add_to_hass(hass) entity: er.RegistryEntry = entity_registry.async_get_or_create( suggested_object_id=object_id, domain=SENSOR_DOMAIN, - platform=islamic_prayer_times.DOMAIN, + platform=DOMAIN, unique_id=old_unique_id, config_entry=entry, ) @@ -140,7 +139,7 @@ async def test_migrate_unique_id( async def test_migration_from_1_1_to_1_2(hass: HomeAssistant) -> None: """Test migrating from version 1.1 to 1.2.""" entry = MockConfigEntry( - domain=islamic_prayer_times.DOMAIN, + domain=DOMAIN, data={}, ) entry.add_to_hass(hass) @@ -164,7 +163,7 @@ async def test_migration_from_1_1_to_1_2(hass: HomeAssistant) -> None: async def test_update_scheduling(hass: HomeAssistant) -> None: """Test that integration schedules update immediately after Islamic midnight.""" - entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) + entry = MockConfigEntry(domain=DOMAIN, data={}) entry.add_to_hass(hass) with ( diff --git a/tests/components/local_ip/test_init.py b/tests/components/local_ip/test_init.py index 51e0628a417..7f411ea9cd7 100644 --- a/tests/components/local_ip/test_init.py +++ b/tests/components/local_ip/test_init.py @@ -2,7 +2,7 @@ from __future__ import annotations -from homeassistant.components.local_ip import DOMAIN +from homeassistant.components.local_ip.const import DOMAIN from homeassistant.components.network import MDNS_TARGET_IP, async_get_source_ip from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/nextcloud/test_config_flow.py b/tests/components/nextcloud/test_config_flow.py index 9a881197cf9..c02516fdc99 100644 --- a/tests/components/nextcloud/test_config_flow.py +++ b/tests/components/nextcloud/test_config_flow.py @@ -10,7 +10,7 @@ from nextcloudmonitor import ( import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.nextcloud import DOMAIN +from homeassistant.components.nextcloud.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant diff --git a/tests/components/sentry/conftest.py b/tests/components/sentry/conftest.py index 781250b2753..663f8ee6aa6 100644 --- a/tests/components/sentry/conftest.py +++ b/tests/components/sentry/conftest.py @@ -6,7 +6,7 @@ from typing import Any import pytest -from homeassistant.components.sentry import DOMAIN +from homeassistant.components.sentry.const import DOMAIN from tests.common import MockConfigEntry From 537d7728a726816220be0ca7c7f68ecea7d835d1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 5 Aug 2024 18:23:44 +0200 Subject: [PATCH 1984/2411] Update frontend to 20240805.1 (#123196) --- 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 95afe1221ec..82dc9cdb83f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240802.0"] + "requirements": ["home-assistant-frontend==20240805.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3b251e91179..6fc0d6f535d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240802.0 +home-assistant-frontend==20240805.1 home-assistant-intents==2024.7.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index f373c0fe7e3..69c0f5b4c21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240802.0 +home-assistant-frontend==20240805.1 # homeassistant.components.conversation home-assistant-intents==2024.7.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 529232f7847..4148ceced1c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -919,7 +919,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240802.0 +home-assistant-frontend==20240805.1 # homeassistant.components.conversation home-assistant-intents==2024.7.29 From f51cc8fe12ea1bbc0987e0b0518700fbcc364b6d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 5 Aug 2024 19:02:07 +0200 Subject: [PATCH 1985/2411] Change zha diagnostic to snapshot (#123198) --- .../zha/snapshots/test_diagnostics.ambr | 266 ++++++++++++++++++ tests/components/zha/test_diagnostics.py | 51 +--- 2 files changed, 275 insertions(+), 42 deletions(-) create mode 100644 tests/components/zha/snapshots/test_diagnostics.ambr diff --git a/tests/components/zha/snapshots/test_diagnostics.ambr b/tests/components/zha/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..8899712b99d --- /dev/null +++ b/tests/components/zha/snapshots/test_diagnostics.ambr @@ -0,0 +1,266 @@ +# serializer version: 1 +# name: test_diagnostics_for_config_entry + dict({ + 'application_state': dict({ + 'broadcast_counters': dict({ + }), + 'counters': dict({ + 'ezsp_counters': dict({ + 'counter_1': dict({ + '__type': "", + 'repr': "Counter(name='counter_1', _raw_value=1, reset_count=0, _last_reset_value=0)", + }), + 'counter_2': dict({ + '__type': "", + 'repr': "Counter(name='counter_2', _raw_value=1, reset_count=0, _last_reset_value=0)", + }), + 'counter_3': dict({ + '__type': "", + 'repr': "Counter(name='counter_3', _raw_value=1, reset_count=0, _last_reset_value=0)", + }), + }), + }), + 'device_counters': dict({ + }), + 'group_counters': dict({ + }), + 'network_info': dict({ + 'channel': 15, + 'channel_mask': 0, + 'children': list([ + ]), + 'extended_pan_id': '**REDACTED**', + 'key_table': list([ + ]), + 'metadata': dict({ + }), + 'network_key': '**REDACTED**', + 'nwk_addresses': dict({ + }), + 'nwk_manager_id': 0, + 'nwk_update_id': 0, + 'pan_id': 4660, + 'security_level': 0, + 'source': None, + 'stack_specific': dict({ + }), + 'tc_link_key': dict({ + 'key': list([ + 90, + 105, + 103, + 66, + 101, + 101, + 65, + 108, + 108, + 105, + 97, + 110, + 99, + 101, + 48, + 57, + ]), + 'partner_ieee': '**REDACTED**', + 'rx_counter': 0, + 'seq': 0, + 'tx_counter': 0, + }), + }), + 'node_info': dict({ + 'ieee': '**REDACTED**', + 'logical_type': 2, + 'manufacturer': 'Coordinator Manufacturer', + 'model': 'Coordinator Model', + 'nwk': 0, + 'version': None, + }), + }), + 'config': dict({ + 'device_config': dict({ + }), + 'enable_quirks': False, + }), + 'config_entry': dict({ + 'data': dict({ + 'device': dict({ + 'baudrate': 115200, + 'flow_control': 'hardware', + 'path': '/dev/ttyUSB0', + }), + 'radio_type': 'ezsp', + }), + 'disabled_by': None, + 'domain': 'zha', + 'minor_version': 1, + 'options': dict({ + 'custom_configuration': dict({ + 'zha_alarm_options': dict({ + 'alarm_arm_requires_code': False, + 'alarm_failed_tries': 2, + 'alarm_master_code': '**REDACTED**', + }), + 'zha_options': dict({ + 'enhanced_light_transition': True, + 'group_members_assume_state': False, + }), + }), + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': None, + 'version': 4, + }), + 'devices': list([ + dict({ + 'logical_type': 'Coordinator', + 'manufacturer': 'Coordinator Manufacturer', + 'model': 'Coordinator Model', + }), + dict({ + 'logical_type': 'EndDevice', + 'manufacturer': 'FakeManufacturer', + 'model': 'FakeModel', + }), + ]), + 'energy_scan': dict({ + '11': 4.313725490196078, + '12': 4.705882352941177, + '13': 5.098039215686274, + '14': 5.490196078431373, + '15': 5.882352941176471, + '16': 6.2745098039215685, + '17': 6.666666666666667, + '18': 7.0588235294117645, + '19': 7.450980392156863, + '20': 7.8431372549019605, + '21': 8.235294117647058, + '22': 8.627450980392156, + '23': 9.019607843137255, + '24': 9.411764705882353, + '25': 9.803921568627452, + '26': 10.196078431372548, + }), + }) +# --- +# name: test_diagnostics_for_device + dict({ + 'active_coordinator': False, + 'area_id': None, + 'available': True, + 'cluster_details': dict({ + '1': dict({ + 'device_type': dict({ + 'id': 1025, + 'name': 'IAS_ANCILLARY_CONTROL', + }), + 'in_clusters': dict({ + '0x0500': dict({ + 'attributes': dict({ + '0x0010': dict({ + 'attribute_name': 'cie_addr', + 'value': list([ + 50, + 79, + 50, + 2, + 0, + 141, + 21, + 0, + ]), + }), + }), + 'endpoint_attribute': 'ias_zone', + 'unsupported_attributes': dict({ + '0x0012': dict({ + 'attribute_name': 'num_zone_sensitivity_levels_supported', + }), + '0x0013': dict({ + 'attribute_name': 'current_zone_sensitivity_level', + }), + }), + }), + '0x0501': dict({ + 'attributes': dict({ + }), + 'endpoint_attribute': 'ias_ace', + 'unsupported_attributes': dict({ + '0x1000': dict({ + }), + 'unknown_attribute_name': dict({ + }), + }), + }), + }), + 'out_clusters': dict({ + }), + 'profile_id': 260, + }), + }), + 'device_type': 'EndDevice', + 'endpoint_names': list([ + dict({ + 'name': 'IAS_ANCILLARY_CONTROL', + }), + ]), + 'entities': list([ + dict({ + 'entity_id': 'alarm_control_panel.fakemanufacturer_fakemodel_alarm_control_panel', + 'name': 'FakeManufacturer FakeModel', + }), + ]), + 'ieee': '**REDACTED**', + 'lqi': None, + 'manufacturer': 'FakeManufacturer', + 'manufacturer_code': 4098, + 'model': 'FakeModel', + 'name': 'FakeManufacturer FakeModel', + 'neighbors': list([ + ]), + 'nwk': 47004, + 'power_source': 'Mains', + 'quirk_applied': False, + 'quirk_class': 'zigpy.device.Device', + 'quirk_id': None, + 'routes': list([ + ]), + 'rssi': None, + 'signature': dict({ + 'endpoints': dict({ + '1': dict({ + 'device_type': '0x0401', + 'input_clusters': list([ + '0x0500', + '0x0501', + ]), + 'output_clusters': list([ + ]), + 'profile_id': '0x0104', + }), + }), + 'manufacturer': 'FakeManufacturer', + 'model': 'FakeModel', + 'node_descriptor': dict({ + 'aps_flags': 0, + 'complex_descriptor_available': 0, + 'descriptor_capability_field': 0, + 'frequency_band': 8, + 'logical_type': 2, + 'mac_capability_flags': 140, + 'manufacturer_code': 4098, + 'maximum_buffer_size': 82, + 'maximum_incoming_transfer_size': 82, + 'maximum_outgoing_transfer_size': 82, + 'reserved': 0, + 'server_mask': 0, + 'user_descriptor_available': 0, + }), + }), + 'user_given_name': None, + }) +# --- diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index bbdc6271207..ed3f83c0c36 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -3,11 +3,11 @@ from unittest.mock import patch import pytest +from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props from zigpy.profiles import zha from zigpy.zcl.clusters import security -from homeassistant.components.diagnostics import REDACTED -from homeassistant.components.zha.diagnostics import KEYS_TO_REDACT from homeassistant.components.zha.helpers import ( ZHADeviceProxy, ZHAGatewayProxy, @@ -27,14 +27,6 @@ from tests.components.diagnostics import ( ) from tests.typing import ClientSessionGenerator -CONFIG_ENTRY_DIAGNOSTICS_KEYS = [ - "config", - "config_entry", - "application_state", - "versions", - "devices", -] - @pytest.fixture(autouse=True) def required_platforms_only(): @@ -51,6 +43,7 @@ async def test_diagnostics_for_config_entry( config_entry: MockConfigEntry, setup_zha, zigpy_device_mock, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics for config entry.""" @@ -81,30 +74,9 @@ async def test_diagnostics_for_config_entry( hass, hass_client, config_entry ) - for key in CONFIG_ENTRY_DIAGNOSTICS_KEYS: - assert key in diagnostics_data - assert diagnostics_data[key] is not None - - # Energy scan results are presented as a percentage. JSON object keys also must be - # strings, not integers. - assert diagnostics_data["energy_scan"] == { - str(k): 100 * v / 255 for k, v in scan.items() - } - - assert isinstance(diagnostics_data["devices"], list) - assert len(diagnostics_data["devices"]) == 2 - assert diagnostics_data["devices"] == [ - { - "manufacturer": "Coordinator Manufacturer", - "model": "Coordinator Model", - "logical_type": "Coordinator", - }, - { - "manufacturer": "FakeManufacturer", - "model": "FakeModel", - "logical_type": "EndDevice", - }, - ] + assert diagnostics_data == snapshot( + exclude=props("created_at", "modified_at", "entry_id", "versions") + ) async def test_diagnostics_for_device( @@ -114,6 +86,7 @@ async def test_diagnostics_for_device( config_entry: MockConfigEntry, setup_zha, zigpy_device_mock, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics for device.""" await setup_zha() @@ -161,11 +134,5 @@ async def test_diagnostics_for_device( diagnostics_data = await get_diagnostics_for_device( hass, hass_client, config_entry, device ) - assert diagnostics_data - device_info: dict = zha_device_proxy.zha_device_info - for key in device_info: - assert key in diagnostics_data - if key not in KEYS_TO_REDACT: - assert key in diagnostics_data - else: - assert diagnostics_data[key] == REDACTED + + assert diagnostics_data == snapshot(exclude=props("device_reg_id", "last_seen")) From 9c2ba9b157e0635231b69af3808d0cb590016384 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Aug 2024 20:40:44 +0200 Subject: [PATCH 1986/2411] Mark tag to be an entity component (#123200) --- homeassistant/components/tag/__init__.py | 1 - homeassistant/components/tag/icons.json | 8 +++----- homeassistant/components/tag/manifest.json | 1 + homeassistant/components/tag/strings.json | 18 ++++++++---------- homeassistant/generated/integrations.json | 5 ----- 5 files changed, 12 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 97307112f22..0462c5bec34 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -364,7 +364,6 @@ class TagEntity(Entity): """Representation of a Tag entity.""" _unrecorded_attributes = frozenset({TAG_ID}) - _attr_translation_key = DOMAIN _attr_should_poll = False def __init__( diff --git a/homeassistant/components/tag/icons.json b/homeassistant/components/tag/icons.json index d9532aadf73..c931ae8614c 100644 --- a/homeassistant/components/tag/icons.json +++ b/homeassistant/components/tag/icons.json @@ -1,9 +1,7 @@ { - "entity": { - "tag": { - "tag": { - "default": "mdi:tag-outline" - } + "entity_component": { + "_": { + "default": "mdi:tag-outline" } } } diff --git a/homeassistant/components/tag/manifest.json b/homeassistant/components/tag/manifest.json index 14701763573..738e7f7e744 100644 --- a/homeassistant/components/tag/manifest.json +++ b/homeassistant/components/tag/manifest.json @@ -3,5 +3,6 @@ "name": "Tags", "codeowners": ["@balloob", "@dmulcahey"], "documentation": "https://www.home-assistant.io/integrations/tag", + "integration_type": "entity", "quality_scale": "internal" } diff --git a/homeassistant/components/tag/strings.json b/homeassistant/components/tag/strings.json index 75cec1f9ef4..4adbf1d48fc 100644 --- a/homeassistant/components/tag/strings.json +++ b/homeassistant/components/tag/strings.json @@ -1,15 +1,13 @@ { "title": "Tag", - "entity": { - "tag": { - "tag": { - "state_attributes": { - "tag_id": { - "name": "Tag ID" - }, - "last_scanned_by_device_id": { - "name": "Last scanned by device ID" - } + "entity_component": { + "_": { + "state_attributes": { + "tag_id": { + "name": "Tag ID" + }, + "last_scanned_by_device_id": { + "name": "Last scanned by device ID" } } } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 673e37b3c12..c941cce64d4 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6002,10 +6002,6 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "tag": { - "integration_type": "hub", - "config_flow": false - }, "tailscale": { "name": "Tailscale", "integration_type": "hub", @@ -7354,7 +7350,6 @@ "shopping_list", "sun", "switch_as_x", - "tag", "threshold", "time_date", "tod", From bb1efe56b686f6a474b95a4eeef31b77953df000 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Aug 2024 21:06:58 +0200 Subject: [PATCH 1987/2411] Mark webhook as a system integration type (#123204) --- homeassistant/components/webhook/manifest.json | 1 + homeassistant/generated/integrations.json | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/webhook/manifest.json b/homeassistant/components/webhook/manifest.json index c2795e8ac17..43f5321d9f6 100644 --- a/homeassistant/components/webhook/manifest.json +++ b/homeassistant/components/webhook/manifest.json @@ -4,5 +4,6 @@ "codeowners": ["@home-assistant/core"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/webhook", + "integration_type": "system", "quality_scale": "internal" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index c941cce64d4..e8bdcc0bac9 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6805,11 +6805,6 @@ } } }, - "webhook": { - "name": "Webhook", - "integration_type": "hub", - "config_flow": false - }, "webmin": { "name": "Webmin", "integration_type": "device", From 5142cb5e981a754f711b39fc4ece822745420b78 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Aug 2024 21:07:11 +0200 Subject: [PATCH 1988/2411] Mark assist_pipeline as a system integration type (#123202) --- homeassistant/components/assist_pipeline/manifest.json | 1 + homeassistant/generated/integrations.json | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/assist_pipeline/manifest.json b/homeassistant/components/assist_pipeline/manifest.json index dd3ec77f165..00950b138fd 100644 --- a/homeassistant/components/assist_pipeline/manifest.json +++ b/homeassistant/components/assist_pipeline/manifest.json @@ -4,6 +4,7 @@ "codeowners": ["@balloob", "@synesthesiam"], "dependencies": ["conversation", "stt", "tts", "wake_word"], "documentation": "https://www.home-assistant.io/integrations/assist_pipeline", + "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", "requirements": ["pymicro-vad==1.0.1"] diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index e8bdcc0bac9..2516f25e6f2 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -496,12 +496,6 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "assist_pipeline": { - "name": "Assist pipeline", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_push" - }, "asuswrt": { "name": "ASUSWRT", "integration_type": "hub", From 4898ba932d5bfa4e6088b36f69e79c6eb4b4a7e6 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 5 Aug 2024 12:34:48 +0200 Subject: [PATCH 1989/2411] Use KNX UI entity platform controller class (#123128) --- homeassistant/components/knx/binary_sensor.py | 4 +- homeassistant/components/knx/button.py | 4 +- homeassistant/components/knx/climate.py | 4 +- homeassistant/components/knx/cover.py | 4 +- homeassistant/components/knx/date.py | 4 +- homeassistant/components/knx/datetime.py | 4 +- homeassistant/components/knx/fan.py | 4 +- homeassistant/components/knx/knx_entity.py | 76 +++++++++++++------ homeassistant/components/knx/light.py | 39 +++++----- homeassistant/components/knx/notify.py | 4 +- homeassistant/components/knx/number.py | 4 +- homeassistant/components/knx/scene.py | 4 +- homeassistant/components/knx/select.py | 4 +- homeassistant/components/knx/sensor.py | 4 +- .../components/knx/storage/config_store.py | 54 +++++++------ homeassistant/components/knx/switch.py | 59 +++++++------- homeassistant/components/knx/text.py | 4 +- homeassistant/components/knx/time.py | 4 +- homeassistant/components/knx/weather.py | 4 +- 19 files changed, 165 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index ff15f725fae..7d80ca55bf6 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -24,7 +24,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import ATTR_COUNTER, ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import BinarySensorSchema @@ -43,7 +43,7 @@ async def async_setup_entry( ) -class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): +class KNXBinarySensor(KnxYamlEntity, BinarySensorEntity, RestoreEntity): """Representation of a KNX binary sensor.""" _device: XknxBinarySensor diff --git a/homeassistant/components/knx/button.py b/homeassistant/components/knx/button.py index 2eb68eebe43..f6627fc527b 100644 --- a/homeassistant/components/knx/button.py +++ b/homeassistant/components/knx/button.py @@ -13,7 +13,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import CONF_PAYLOAD_LENGTH, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -31,7 +31,7 @@ async def async_setup_entry( ) -class KNXButton(KnxEntity, ButtonEntity): +class KNXButton(KnxYamlEntity, ButtonEntity): """Representation of a KNX button.""" _device: XknxRawValue diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 7470d60ef4b..9abc9023617 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -35,7 +35,7 @@ from .const import ( DOMAIN, PRESET_MODES, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import ClimateSchema ATTR_COMMAND_VALUE = "command_value" @@ -133,7 +133,7 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate: ) -class KNXClimate(KnxEntity, ClimateEntity): +class KNXClimate(KnxYamlEntity, ClimateEntity): """Representation of a KNX climate device.""" _device: XknxClimate diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 1962db0ad3f..408f746e094 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -27,7 +27,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import CoverSchema @@ -43,7 +43,7 @@ async def async_setup_entry( async_add_entities(KNXCover(knx_module, entity_config) for entity_config in config) -class KNXCover(KnxEntity, CoverEntity): +class KNXCover(KnxYamlEntity, CoverEntity): """Representation of a KNX cover.""" _device: XknxCover diff --git a/homeassistant/components/knx/date.py b/homeassistant/components/knx/date.py index 80fea63d0a6..9f04a4acd7e 100644 --- a/homeassistant/components/knx/date.py +++ b/homeassistant/components/knx/date.py @@ -31,7 +31,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -61,7 +61,7 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateDevice: ) -class KNXDateEntity(KnxEntity, DateEntity, RestoreEntity): +class KNXDateEntity(KnxYamlEntity, DateEntity, RestoreEntity): """Representation of a KNX date.""" _device: XknxDateDevice diff --git a/homeassistant/components/knx/datetime.py b/homeassistant/components/knx/datetime.py index 16ccb7474a7..8f1a25e6e3c 100644 --- a/homeassistant/components/knx/datetime.py +++ b/homeassistant/components/knx/datetime.py @@ -32,7 +32,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -62,7 +62,7 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTimeDevice: ) -class KNXDateTimeEntity(KnxEntity, DateTimeEntity, RestoreEntity): +class KNXDateTimeEntity(KnxYamlEntity, DateTimeEntity, RestoreEntity): """Representation of a KNX datetime.""" _device: XknxDateTimeDevice diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 940e241ccda..6fd87be97d1 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -21,7 +21,7 @@ from homeassistant.util.scaling import int_states_in_range from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import FanSchema DEFAULT_PERCENTAGE: Final = 50 @@ -39,7 +39,7 @@ async def async_setup_entry( async_add_entities(KNXFan(knx_module, entity_config) for entity_config in config) -class KNXFan(KnxEntity, FanEntity): +class KNXFan(KnxYamlEntity, FanEntity): """Representation of a KNX fan.""" _device: XknxFan diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index 2b8d2e71186..c81a6ee06db 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -2,30 +2,55 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any from xknx.devices import Device as XknxDevice -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity - -from .const import DOMAIN +from homeassistant.helpers.entity_platform import EntityPlatform +from homeassistant.helpers.entity_registry import RegistryEntry if TYPE_CHECKING: from . import KNXModule -SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal.{{}}" +from .storage.config_store import PlatformControllerBase -class KnxEntity(Entity): +class KnxUiEntityPlatformController(PlatformControllerBase): + """Class to manage dynamic adding and reloading of UI entities.""" + + def __init__( + self, + knx_module: KNXModule, + entity_platform: EntityPlatform, + entity_class: type[KnxUiEntity], + ) -> None: + """Initialize the UI platform.""" + self._knx_module = knx_module + self._entity_platform = entity_platform + self._entity_class = entity_class + + async def create_entity(self, unique_id: str, config: dict[str, Any]) -> None: + """Add a new UI entity.""" + await self._entity_platform.async_add_entities( + [self._entity_class(self._knx_module, unique_id, config)] + ) + + async def update_entity( + self, entity_entry: RegistryEntry, config: dict[str, Any] + ) -> None: + """Update an existing UI entities configuration.""" + await self._entity_platform.async_remove_entity(entity_entry.entity_id) + await self.create_entity(unique_id=entity_entry.unique_id, config=config) + + +class _KnxEntityBase(Entity): """Representation of a KNX entity.""" _attr_should_poll = False - - def __init__(self, knx_module: KNXModule, device: XknxDevice) -> None: - """Set up device.""" - self._knx_module = knx_module - self._device = device + _knx_module: KNXModule + _device: XknxDevice @property def name(self) -> str: @@ -49,7 +74,7 @@ class KnxEntity(Entity): """Store register state change callback and start device object.""" self._device.register_device_updated_cb(self.after_update_callback) self._device.xknx.devices.async_add(self._device) - # super call needed to have methods of mulit-inherited classes called + # super call needed to have methods of multi-inherited classes called # eg. for restoring state (like _KNXSwitch) await super().async_added_to_hass() @@ -59,19 +84,22 @@ class KnxEntity(Entity): self._device.xknx.devices.async_remove(self._device) -class KnxUIEntity(KnxEntity): +class KnxYamlEntity(_KnxEntityBase): + """Representation of a KNX entity configured from YAML.""" + + def __init__(self, knx_module: KNXModule, device: XknxDevice) -> None: + """Initialize the YAML entity.""" + self._knx_module = knx_module + self._device = device + + +class KnxUiEntity(_KnxEntityBase, ABC): """Representation of a KNX UI entity.""" _attr_unique_id: str - async def async_added_to_hass(self) -> None: - """Register callbacks when entity added to hass.""" - await super().async_added_to_hass() - self._knx_module.config_store.entities.add(self._attr_unique_id) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIGNAL_ENTITY_REMOVE.format(self._attr_unique_id), - self.async_remove, - ) - ) + @abstractmethod + def __init__( + self, knx_module: KNXModule, unique_id: str, config: dict[str, Any] + ) -> None: + """Initialize the UI entity.""" diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 1197f09354b..a2ce8f8d2cb 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -19,15 +19,18 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util from . import KNXModule from .const import CONF_SYNC_STATE, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, ColorTempModes -from .knx_entity import KnxEntity, KnxUIEntity +from .knx_entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity from .schema import LightSchema from .storage.const import ( CONF_COLOR_TEMP_MAX, @@ -63,8 +66,17 @@ async def async_setup_entry( ) -> None: """Set up light(s) for KNX platform.""" knx_module: KNXModule = hass.data[DOMAIN] + platform = async_get_current_platform() + knx_module.config_store.add_platform( + platform=Platform.LIGHT, + controller=KnxUiEntityPlatformController( + knx_module=knx_module, + entity_platform=platform, + entity_class=KnxUiLight, + ), + ) - entities: list[KnxEntity] = [] + entities: list[KnxYamlEntity | KnxUiEntity] = [] if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.LIGHT): entities.extend( KnxYamlLight(knx_module, entity_config) @@ -78,13 +90,6 @@ async def async_setup_entry( if entities: async_add_entities(entities) - @callback - def add_new_ui_light(unique_id: str, config: dict[str, Any]) -> None: - """Add KNX entity at runtime.""" - async_add_entities([KnxUiLight(knx_module, unique_id, config)]) - - knx_module.config_store.async_add_entity[Platform.LIGHT] = add_new_ui_light - def _create_yaml_light(xknx: XKNX, config: ConfigType) -> XknxLight: """Return a KNX Light device to be used within XKNX.""" @@ -519,7 +524,7 @@ class _KnxLight(LightEntity): await self._device.set_off() -class KnxYamlLight(_KnxLight, KnxEntity): +class KnxYamlLight(_KnxLight, KnxYamlEntity): """Representation of a KNX light.""" _device: XknxLight @@ -546,7 +551,7 @@ class KnxYamlLight(_KnxLight, KnxEntity): ) -class KnxUiLight(_KnxLight, KnxUIEntity): +class KnxUiLight(_KnxLight, KnxUiEntity): """Representation of a KNX light.""" _attr_has_entity_name = True @@ -556,11 +561,9 @@ class KnxUiLight(_KnxLight, KnxUIEntity): self, knx_module: KNXModule, unique_id: str, config: ConfigType ) -> None: """Initialize of KNX light.""" - super().__init__( - knx_module=knx_module, - device=_create_ui_light( - knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME] - ), + self._knx_module = knx_module + self._device = _create_ui_light( + knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME] ) self._attr_max_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MAX] self._attr_min_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MIN] diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index b349681990c..173ab3119a0 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_get_service( @@ -103,7 +103,7 @@ def _create_notification_instance(xknx: XKNX, config: ConfigType) -> XknxNotific ) -class KNXNotify(KnxEntity, NotifyEntity): +class KNXNotify(KnxYamlEntity, NotifyEntity): """Representation of a KNX notification entity.""" _device: XknxNotification diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index 3d4af503dff..cbbe91aba54 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -30,7 +30,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import NumberSchema @@ -58,7 +58,7 @@ def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue: ) -class KNXNumber(KnxEntity, RestoreNumber): +class KNXNumber(KnxYamlEntity, RestoreNumber): """Representation of a KNX number.""" _device: NumericValue diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index fc37f36dd01..2de832ae54a 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -15,7 +15,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import SceneSchema @@ -31,7 +31,7 @@ async def async_setup_entry( async_add_entities(KNXScene(knx_module, entity_config) for entity_config in config) -class KNXScene(KnxEntity, Scene): +class KNXScene(KnxYamlEntity, Scene): """Representation of a KNX scene.""" _device: XknxScene diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index 1b862010c2a..6c73bf8d573 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -30,7 +30,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import SelectSchema @@ -59,7 +59,7 @@ def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue: ) -class KNXSelect(KnxEntity, SelectEntity, RestoreEntity): +class KNXSelect(KnxYamlEntity, SelectEntity, RestoreEntity): """Representation of a KNX select.""" _device: RawValue diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index ab363e2a35f..a28c1a339e6 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -35,7 +35,7 @@ from homeassistant.util.enum import try_parse_enum from . import KNXModule from .const import ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import SensorSchema SCAN_INTERVAL = timedelta(seconds=10) @@ -141,7 +141,7 @@ def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor: ) -class KNXSensor(KnxEntity, SensorEntity): +class KNXSensor(KnxYamlEntity, SensorEntity): """Representation of a KNX sensor.""" _device: XknxSensor diff --git a/homeassistant/components/knx/storage/config_store.py b/homeassistant/components/knx/storage/config_store.py index 876fe19a4b9..ce7a705e629 100644 --- a/homeassistant/components/knx/storage/config_store.py +++ b/homeassistant/components/knx/storage/config_store.py @@ -1,6 +1,6 @@ """KNX entity configuration store.""" -from collections.abc import Callable +from abc import ABC, abstractmethod import logging from typing import Any, Final, TypedDict @@ -8,12 +8,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PLATFORM, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store from homeassistant.util.ulid import ulid_now from ..const import DOMAIN -from ..knx_entity import SIGNAL_ENTITY_REMOVE from .const import CONF_DATA _LOGGER = logging.getLogger(__name__) @@ -33,6 +31,20 @@ class KNXConfigStoreModel(TypedDict): entities: KNXEntityStoreModel +class PlatformControllerBase(ABC): + """Entity platform controller base class.""" + + @abstractmethod + async def create_entity(self, unique_id: str, config: dict[str, Any]) -> None: + """Create a new entity.""" + + @abstractmethod + async def update_entity( + self, entity_entry: er.RegistryEntry, config: dict[str, Any] + ) -> None: + """Update an existing entities configuration.""" + + class KNXConfigStore: """Manage KNX config store data.""" @@ -46,12 +58,7 @@ class KNXConfigStore: self.config_entry = config_entry self._store = Store[KNXConfigStoreModel](hass, STORAGE_VERSION, STORAGE_KEY) self.data = KNXConfigStoreModel(entities={}) - - # entities and async_add_entity are filled by platform / entity setups - self.entities: set[str] = set() # unique_id as values - self.async_add_entity: dict[ - Platform, Callable[[str, dict[str, Any]], None] - ] = {} + self._platform_controllers: dict[Platform, PlatformControllerBase] = {} async def load_data(self) -> None: """Load config store data from storage.""" @@ -62,14 +69,19 @@ class KNXConfigStore: len(self.data["entities"]), ) + def add_platform( + self, platform: Platform, controller: PlatformControllerBase + ) -> None: + """Add platform controller.""" + self._platform_controllers[platform] = controller + async def create_entity( self, platform: Platform, data: dict[str, Any] ) -> str | None: """Create a new entity.""" - if platform not in self.async_add_entity: - raise ConfigStoreException(f"Entity platform not ready: {platform}") + platform_controller = self._platform_controllers[platform] unique_id = f"knx_es_{ulid_now()}" - self.async_add_entity[platform](unique_id, data) + await platform_controller.create_entity(unique_id, data) # store data after entity was added to be sure config didn't raise exceptions self.data["entities"].setdefault(platform, {})[unique_id] = data await self._store.async_save(self.data) @@ -95,8 +107,7 @@ class KNXConfigStore: self, platform: Platform, entity_id: str, data: dict[str, Any] ) -> None: """Update an existing entity.""" - if platform not in self.async_add_entity: - raise ConfigStoreException(f"Entity platform not ready: {platform}") + platform_controller = self._platform_controllers[platform] entity_registry = er.async_get(self.hass) if (entry := entity_registry.async_get(entity_id)) is None: raise ConfigStoreException(f"Entity not found: {entity_id}") @@ -108,8 +119,7 @@ class KNXConfigStore: raise ConfigStoreException( f"Entity not found in storage: {entity_id} - {unique_id}" ) - async_dispatcher_send(self.hass, SIGNAL_ENTITY_REMOVE.format(unique_id)) - self.async_add_entity[platform](unique_id, data) + await platform_controller.update_entity(entry, data) # store data after entity is added to make sure config doesn't raise exceptions self.data["entities"][platform][unique_id] = data await self._store.async_save(self.data) @@ -125,23 +135,21 @@ class KNXConfigStore: raise ConfigStoreException( f"Entity not found in {entry.domain}: {entry.unique_id}" ) from err - try: - self.entities.remove(entry.unique_id) - except KeyError: - _LOGGER.warning("Entity not initialized when deleted: %s", entity_id) entity_registry.async_remove(entity_id) await self._store.async_save(self.data) def get_entity_entries(self) -> list[er.RegistryEntry]: - """Get entity_ids of all configured entities by platform.""" + """Get entity_ids of all UI configured entities.""" entity_registry = er.async_get(self.hass) - + unique_ids = { + uid for platform in self.data["entities"].values() for uid in platform + } return [ registry_entry for registry_entry in er.async_entries_for_config_entry( entity_registry, self.config_entry.entry_id ) - if registry_entry.unique_id in self.entities + if registry_entry.unique_id in unique_ids ] diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index a5f430e6157..ebe930957d6 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -17,9 +17,12 @@ from homeassistant.const import ( STATE_UNKNOWN, Platform, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType @@ -32,7 +35,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity, KnxUIEntity +from .knx_entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity from .schema import SwitchSchema from .storage.const import ( CONF_DEVICE_INFO, @@ -51,8 +54,17 @@ async def async_setup_entry( ) -> None: """Set up switch(es) for KNX platform.""" knx_module: KNXModule = hass.data[DOMAIN] + platform = async_get_current_platform() + knx_module.config_store.add_platform( + platform=Platform.SWITCH, + controller=KnxUiEntityPlatformController( + knx_module=knx_module, + entity_platform=platform, + entity_class=KnxUiSwitch, + ), + ) - entities: list[KnxEntity] = [] + entities: list[KnxYamlEntity | KnxUiEntity] = [] if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.SWITCH): entities.extend( KnxYamlSwitch(knx_module, entity_config) @@ -66,13 +78,6 @@ async def async_setup_entry( if entities: async_add_entities(entities) - @callback - def add_new_ui_switch(unique_id: str, config: dict[str, Any]) -> None: - """Add KNX entity at runtime.""" - async_add_entities([KnxUiSwitch(knx_module, unique_id, config)]) - - knx_module.config_store.async_add_entity[Platform.SWITCH] = add_new_ui_switch - class _KnxSwitch(SwitchEntity, RestoreEntity): """Base class for a KNX switch.""" @@ -102,7 +107,7 @@ class _KnxSwitch(SwitchEntity, RestoreEntity): await self._device.set_off() -class KnxYamlSwitch(_KnxSwitch, KnxEntity): +class KnxYamlSwitch(_KnxSwitch, KnxYamlEntity): """Representation of a KNX switch configured from YAML.""" _device: XknxSwitch @@ -125,7 +130,7 @@ class KnxYamlSwitch(_KnxSwitch, KnxEntity): self._attr_unique_id = str(self._device.switch.group_address) -class KnxUiSwitch(_KnxSwitch, KnxUIEntity): +class KnxUiSwitch(_KnxSwitch, KnxUiEntity): """Representation of a KNX switch configured from UI.""" _attr_has_entity_name = True @@ -134,21 +139,19 @@ class KnxUiSwitch(_KnxSwitch, KnxUIEntity): def __init__( self, knx_module: KNXModule, unique_id: str, config: dict[str, Any] ) -> None: - """Initialize of KNX switch.""" - super().__init__( - knx_module=knx_module, - device=XknxSwitch( - knx_module.xknx, - name=config[CONF_ENTITY][CONF_NAME], - group_address=config[DOMAIN][CONF_GA_SWITCH][CONF_GA_WRITE], - group_address_state=[ - config[DOMAIN][CONF_GA_SWITCH][CONF_GA_STATE], - *config[DOMAIN][CONF_GA_SWITCH][CONF_GA_PASSIVE], - ], - respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ], - sync_state=config[DOMAIN][CONF_SYNC_STATE], - invert=config[DOMAIN][CONF_INVERT], - ), + """Initialize KNX switch.""" + self._knx_module = knx_module + self._device = XknxSwitch( + knx_module.xknx, + name=config[CONF_ENTITY][CONF_NAME], + group_address=config[DOMAIN][CONF_GA_SWITCH][CONF_GA_WRITE], + group_address_state=[ + config[DOMAIN][CONF_GA_SWITCH][CONF_GA_STATE], + *config[DOMAIN][CONF_GA_SWITCH][CONF_GA_PASSIVE], + ], + respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ], + sync_state=config[DOMAIN][CONF_SYNC_STATE], + invert=config[DOMAIN][CONF_INVERT], ) self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY] self._attr_unique_id = unique_id diff --git a/homeassistant/components/knx/text.py b/homeassistant/components/knx/text.py index 9bca37434ac..381cb95ad32 100644 --- a/homeassistant/components/knx/text.py +++ b/homeassistant/components/knx/text.py @@ -30,7 +30,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -57,7 +57,7 @@ def _create_notification(xknx: XKNX, config: ConfigType) -> XknxNotification: ) -class KNXText(KnxEntity, TextEntity, RestoreEntity): +class KNXText(KnxYamlEntity, TextEntity, RestoreEntity): """Representation of a KNX text.""" _device: XknxNotification diff --git a/homeassistant/components/knx/time.py b/homeassistant/components/knx/time.py index 5d9225a1e41..b4e562a8869 100644 --- a/homeassistant/components/knx/time.py +++ b/homeassistant/components/knx/time.py @@ -31,7 +31,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -61,7 +61,7 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxTimeDevice: ) -class KNXTimeEntity(KnxEntity, TimeEntity, RestoreEntity): +class KNXTimeEntity(KnxYamlEntity, TimeEntity, RestoreEntity): """Representation of a KNX time.""" _device: XknxTimeDevice diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 11dae452e2f..99f4be962fe 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import WeatherSchema @@ -75,7 +75,7 @@ def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather: ) -class KNXWeather(KnxEntity, WeatherEntity): +class KNXWeather(KnxYamlEntity, WeatherEntity): """Representation of a KNX weather device.""" _device: XknxWeather From 0427aeccb0cdeb6757116441ae5db2ebe949ab72 Mon Sep 17 00:00:00 2001 From: musapinar Date: Mon, 5 Aug 2024 14:21:01 +0200 Subject: [PATCH 1990/2411] Add Matter Leedarson RGBTW Bulb to the transition blocklist (#123182) --- homeassistant/components/matter/light.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index d05a7c85f9d..6e9019c46fa 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -51,18 +51,19 @@ DEFAULT_TRANSITION = 0.2 # hw version (attributeKey 0/40/8) # sw version (attributeKey 0/40/10) TRANSITION_BLOCKLIST = ( - (4488, 514, "1.0", "1.0.0"), - (4488, 260, "1.0", "1.0.0"), - (5010, 769, "3.0", "1.0.0"), - (4999, 24875, "1.0", "27.0"), - (4999, 25057, "1.0", "27.0"), - (4448, 36866, "V1", "V1.0.0.5"), - (5009, 514, "1.0", "1.0.0"), (4107, 8475, "v1.0", "v1.0"), (4107, 8550, "v1.0", "v1.0"), (4107, 8551, "v1.0", "v1.0"), - (4107, 8656, "v1.0", "v1.0"), (4107, 8571, "v1.0", "v1.0"), + (4107, 8656, "v1.0", "v1.0"), + (4448, 36866, "V1", "V1.0.0.5"), + (4456, 1011, "1.0.0", "2.00.00"), + (4488, 260, "1.0", "1.0.0"), + (4488, 514, "1.0", "1.0.0"), + (4999, 24875, "1.0", "27.0"), + (4999, 25057, "1.0", "27.0"), + (5009, 514, "1.0", "1.0.0"), + (5010, 769, "3.0", "1.0.0"), ) From ea20c4b375c1ada418b42b13e172027124ed5c98 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Aug 2024 15:07:01 +0200 Subject: [PATCH 1991/2411] Fix MPD issue creation (#123187) --- homeassistant/components/mpd/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 3538b1c7973..92f0f5cfcc4 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -86,7 +86,7 @@ async def async_setup_platform( ) if ( result["type"] is FlowResultType.CREATE_ENTRY - or result["reason"] == "single_instance_allowed" + or result["reason"] == "already_configured" ): async_create_issue( hass, From 6b10dbb38c61c6a50dd3074aaa8f182094910a0e Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:17:46 +0200 Subject: [PATCH 1992/2411] Fix state icon for closed valve entities (#123190) --- homeassistant/components/valve/icons.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/valve/icons.json b/homeassistant/components/valve/icons.json index 1261d1cc398..2c887ebf273 100644 --- a/homeassistant/components/valve/icons.json +++ b/homeassistant/components/valve/icons.json @@ -3,7 +3,7 @@ "_": { "default": "mdi:valve-open", "state": { - "off": "mdi:valve-closed" + "closed": "mdi:valve-closed" } }, "gas": { @@ -12,7 +12,7 @@ "water": { "default": "mdi:valve-open", "state": { - "off": "mdi:valve-closed" + "closed": "mdi:valve-closed" } } }, From b16bf2981907c68e1be67bf98d170c76922f9006 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 5 Aug 2024 18:23:44 +0200 Subject: [PATCH 1993/2411] Update frontend to 20240805.1 (#123196) --- 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 95afe1221ec..82dc9cdb83f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240802.0"] + "requirements": ["home-assistant-frontend==20240805.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3b251e91179..6fc0d6f535d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240802.0 +home-assistant-frontend==20240805.1 home-assistant-intents==2024.7.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index fb881f5a8c6..2fd2c93a687 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240802.0 +home-assistant-frontend==20240805.1 # homeassistant.components.conversation home-assistant-intents==2024.7.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac3d594a541..3a71321bc5b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -916,7 +916,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240802.0 +home-assistant-frontend==20240805.1 # homeassistant.components.conversation home-assistant-intents==2024.7.29 From 859874487eed3c6593e3b3a0d965d4a3ad3794de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Aug 2024 20:40:44 +0200 Subject: [PATCH 1994/2411] Mark tag to be an entity component (#123200) --- homeassistant/components/tag/__init__.py | 1 - homeassistant/components/tag/icons.json | 8 +++----- homeassistant/components/tag/manifest.json | 1 + homeassistant/components/tag/strings.json | 18 ++++++++---------- homeassistant/generated/integrations.json | 5 ----- 5 files changed, 12 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 97307112f22..0462c5bec34 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -364,7 +364,6 @@ class TagEntity(Entity): """Representation of a Tag entity.""" _unrecorded_attributes = frozenset({TAG_ID}) - _attr_translation_key = DOMAIN _attr_should_poll = False def __init__( diff --git a/homeassistant/components/tag/icons.json b/homeassistant/components/tag/icons.json index d9532aadf73..c931ae8614c 100644 --- a/homeassistant/components/tag/icons.json +++ b/homeassistant/components/tag/icons.json @@ -1,9 +1,7 @@ { - "entity": { - "tag": { - "tag": { - "default": "mdi:tag-outline" - } + "entity_component": { + "_": { + "default": "mdi:tag-outline" } } } diff --git a/homeassistant/components/tag/manifest.json b/homeassistant/components/tag/manifest.json index 14701763573..738e7f7e744 100644 --- a/homeassistant/components/tag/manifest.json +++ b/homeassistant/components/tag/manifest.json @@ -3,5 +3,6 @@ "name": "Tags", "codeowners": ["@balloob", "@dmulcahey"], "documentation": "https://www.home-assistant.io/integrations/tag", + "integration_type": "entity", "quality_scale": "internal" } diff --git a/homeassistant/components/tag/strings.json b/homeassistant/components/tag/strings.json index 75cec1f9ef4..4adbf1d48fc 100644 --- a/homeassistant/components/tag/strings.json +++ b/homeassistant/components/tag/strings.json @@ -1,15 +1,13 @@ { "title": "Tag", - "entity": { - "tag": { - "tag": { - "state_attributes": { - "tag_id": { - "name": "Tag ID" - }, - "last_scanned_by_device_id": { - "name": "Last scanned by device ID" - } + "entity_component": { + "_": { + "state_attributes": { + "tag_id": { + "name": "Tag ID" + }, + "last_scanned_by_device_id": { + "name": "Last scanned by device ID" } } } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3cc3ea71df9..816241035e6 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6013,10 +6013,6 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "tag": { - "integration_type": "hub", - "config_flow": false - }, "tailscale": { "name": "Tailscale", "integration_type": "hub", @@ -7365,7 +7361,6 @@ "shopping_list", "sun", "switch_as_x", - "tag", "threshold", "time_date", "tod", From 62d38e786d21e3e80246ac5ff765a34c8061c49e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Aug 2024 21:07:11 +0200 Subject: [PATCH 1995/2411] Mark assist_pipeline as a system integration type (#123202) --- homeassistant/components/assist_pipeline/manifest.json | 1 + homeassistant/generated/integrations.json | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/assist_pipeline/manifest.json b/homeassistant/components/assist_pipeline/manifest.json index dd3ec77f165..00950b138fd 100644 --- a/homeassistant/components/assist_pipeline/manifest.json +++ b/homeassistant/components/assist_pipeline/manifest.json @@ -4,6 +4,7 @@ "codeowners": ["@balloob", "@synesthesiam"], "dependencies": ["conversation", "stt", "tts", "wake_word"], "documentation": "https://www.home-assistant.io/integrations/assist_pipeline", + "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", "requirements": ["pymicro-vad==1.0.1"] diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 816241035e6..850c7d78bc0 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -490,12 +490,6 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "assist_pipeline": { - "name": "Assist pipeline", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_push" - }, "asterisk": { "name": "Asterisk", "integrations": { From 4f722e864c13f465dce8b46aadafc1add0646069 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Aug 2024 21:06:58 +0200 Subject: [PATCH 1996/2411] Mark webhook as a system integration type (#123204) --- homeassistant/components/webhook/manifest.json | 1 + homeassistant/generated/integrations.json | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/webhook/manifest.json b/homeassistant/components/webhook/manifest.json index c2795e8ac17..43f5321d9f6 100644 --- a/homeassistant/components/webhook/manifest.json +++ b/homeassistant/components/webhook/manifest.json @@ -4,5 +4,6 @@ "codeowners": ["@home-assistant/core"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/webhook", + "integration_type": "system", "quality_scale": "internal" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 850c7d78bc0..5107587ab89 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6810,11 +6810,6 @@ } } }, - "webhook": { - "name": "Webhook", - "integration_type": "hub", - "config_flow": false - }, "webmin": { "name": "Webmin", "integration_type": "device", From d530137bec3e23a194841e56d32246d8a6e741fa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Aug 2024 21:12:09 +0200 Subject: [PATCH 1997/2411] Bump version to 2024.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 7f27548a68c..981fc42fd36 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index f23c571feb5..c29252b1ced 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b2" +version = "2024.8.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 54f8f24c2ce782a191fb76701493e03df3ec78ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Aug 2024 15:51:15 -0500 Subject: [PATCH 1998/2411] Bump PyJWT to 2.9.0 (#123209) --- 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 6fc0d6f535d..9f327697a19 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -44,7 +44,7 @@ paho-mqtt==1.6.1 Pillow==10.4.0 pip>=21.3.1 psutil-home-assistant==0.0.1 -PyJWT==2.8.0 +PyJWT==2.9.0 pymicro-vad==1.0.1 PyNaCl==1.5.0 pyOpenSSL==24.2.1 diff --git a/pyproject.toml b/pyproject.toml index 49346f90448..a0a98bc72be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "ifaddr==0.2.0", "Jinja2==3.1.4", "lru-dict==1.3.0", - "PyJWT==2.8.0", + "PyJWT==2.9.0", # PyJWT has loose dependency. We want the latest one. "cryptography==43.0.0", "Pillow==10.4.0", diff --git a/requirements.txt b/requirements.txt index 1beefe73914..f35fffe680a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ home-assistant-bluetooth==1.12.2 ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 -PyJWT==2.8.0 +PyJWT==2.9.0 cryptography==43.0.0 Pillow==10.4.0 pyOpenSSL==24.2.1 From 1b73b2a12ab2abeb73c5505283c9c80347e41883 Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Tue, 6 Aug 2024 04:06:35 -0400 Subject: [PATCH 1999/2411] Update greeclimate to 2.1.0 (#123210) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index ca1c4b5b754..dba8cd6077c 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/gree", "iot_class": "local_polling", "loggers": ["greeclimate"], - "requirements": ["greeclimate==2.0.0"] + "requirements": ["greeclimate==2.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 69c0f5b4c21..d0d388e194a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1007,7 +1007,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==2.0.0 +greeclimate==2.1.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4148ceced1c..4953419fe6d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -851,7 +851,7 @@ govee-local-api==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==2.0.0 +greeclimate==2.1.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 From 21da79a249c0b1aa85610fa4e82a922f39cc628e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 6 Aug 2024 20:11:08 +1200 Subject: [PATCH 2000/2411] Show project version as `sw_version` in ESPHome (#123183) --- homeassistant/components/esphome/manager.py | 6 +++--- tests/components/esphome/test_manager.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index e8d002fba9d..ef2a6862f10 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -654,12 +654,13 @@ def _async_setup_device_registry( if device_info.manufacturer: manufacturer = device_info.manufacturer model = device_info.model - hw_version = None if device_info.project_name: project_name = device_info.project_name.split(".") manufacturer = project_name[0] model = project_name[1] - hw_version = device_info.project_version + sw_version = ( + f"{device_info.project_version} (ESPHome {device_info.esphome_version})" + ) suggested_area = None if device_info.suggested_area: @@ -674,7 +675,6 @@ def _async_setup_device_registry( manufacturer=manufacturer, model=model, sw_version=sw_version, - hw_version=hw_version, suggested_area=suggested_area, ) return device_entry.id diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 01f267581f4..651c52cd083 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -1024,7 +1024,7 @@ async def test_esphome_device_with_project( ) assert dev.manufacturer == "mfr" assert dev.model == "model" - assert dev.hw_version == "2.2.2" + assert dev.sw_version == "2.2.2 (ESPHome 1.0.0)" async def test_esphome_device_with_manufacturer( From 164cfa85da6e5c1a85e004533da689982c053230 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 6 Aug 2024 20:12:31 +1200 Subject: [PATCH 2001/2411] Add support for ESPHome update entities to be checked on demand (#123161) --- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/esphome/update.py | 11 +++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/esphome/test_update.py | 14 ++++++++++++++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index ff7569bbc5f..97724a12203 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==24.6.2", + "aioesphomeapi==25.0.0", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index e86c88ddf5b..b7905fb4fdb 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -8,6 +8,7 @@ from typing import Any from aioesphomeapi import ( DeviceInfo as ESPHomeDeviceInfo, EntityInfo, + UpdateCommand, UpdateInfo, UpdateState, ) @@ -259,9 +260,15 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity): """Return the title of the update.""" return self._state.title + @convert_api_error_ha_error + async def async_update(self) -> None: + """Command device to check for update.""" + if self.available: + self._client.update_command(key=self._key, command=UpdateCommand.CHECK) + @convert_api_error_ha_error async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: - """Update the current value.""" - self._client.update_command(key=self._key, install=True) + """Command device to install update.""" + self._client.update_command(key=self._key, command=UpdateCommand.INSTALL) diff --git a/requirements_all.txt b/requirements_all.txt index d0d388e194a..927b3d45310 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -237,7 +237,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==24.6.2 +aioesphomeapi==25.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4953419fe6d..a75a4b6cec8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -225,7 +225,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==24.6.2 +aioesphomeapi==25.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index c9826c3f347..83e89b1de00 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -8,6 +8,7 @@ from aioesphomeapi import ( APIClient, EntityInfo, EntityState, + UpdateCommand, UpdateInfo, UpdateState, UserService, @@ -15,6 +16,10 @@ from aioesphomeapi import ( import pytest from homeassistant.components.esphome.dashboard import async_get_dashboard +from homeassistant.components.homeassistant import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, +) from homeassistant.components.update import ( DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL, @@ -527,3 +532,12 @@ async def test_generic_device_update_entity_has_update( assert state is not None assert state.state == STATE_ON assert state.attributes["in_progress"] == 50 + + await hass.services.async_call( + HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: "update.test_myupdate"}, + blocking=True, + ) + + mock_client.update_command.assert_called_with(key=1, command=UpdateCommand.CHECK) From 0d4ca35784f6768b782f7bc9a3329015fa6e93b5 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:13:12 +0200 Subject: [PATCH 2002/2411] Remove unused async_setup method in insteon (#123201) --- homeassistant/components/insteon/__init__.py | 10 +------ tests/components/insteon/const.py | 1 - tests/components/insteon/test_config_flow.py | 30 ++++++++------------ 3 files changed, 13 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 0ec2434bc82..ff72f90a87e 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -10,8 +10,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers import device_registry as dr from . import api from .const import ( @@ -36,8 +35,6 @@ from .utils import ( _LOGGER = logging.getLogger(__name__) OPTIONS = "options" -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - async def async_get_device_config(hass, config_entry): """Initiate the connection and services.""" @@ -77,11 +74,6 @@ async def close_insteon_connection(*args): await async_close() -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Insteon platform.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an Insteon entry.""" diff --git a/tests/components/insteon/const.py b/tests/components/insteon/const.py index c35db3b7092..a4e4e8a390d 100644 --- a/tests/components/insteon/const.py +++ b/tests/components/insteon/const.py @@ -79,5 +79,4 @@ PATCH_CONNECTION = "homeassistant.components.insteon.config_flow.async_connect" PATCH_CONNECTION_CLOSE = "homeassistant.components.insteon.config_flow.async_close" PATCH_DEVICES = "homeassistant.components.insteon.config_flow.devices" PATCH_USB_LIST = "homeassistant.components.insteon.config_flow.async_get_usb_ports" -PATCH_ASYNC_SETUP = "homeassistant.components.insteon.async_setup" PATCH_ASYNC_SETUP_ENTRY = "homeassistant.components.insteon.async_setup_entry" diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 4d3fb815463..51fdd7a550d 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -25,7 +25,6 @@ from .const import ( MOCK_USER_INPUT_HUB_V2, MOCK_USER_INPUT_PLM, MOCK_USER_INPUT_PLM_MANUAL, - PATCH_ASYNC_SETUP, PATCH_ASYNC_SETUP_ENTRY, PATCH_CONNECTION, PATCH_USB_LIST, @@ -81,7 +80,6 @@ async def _device_form(hass, flow_id, connection, user_input): PATCH_CONNECTION, new=connection, ), - patch(PATCH_ASYNC_SETUP, return_value=True) as mock_setup, patch( PATCH_ASYNC_SETUP_ENTRY, return_value=True, @@ -89,7 +87,7 @@ async def _device_form(hass, flow_id, connection, user_input): ): result = await hass.config_entries.flow.async_configure(flow_id, user_input) await hass.async_block_till_done() - return result, mock_setup, mock_setup_entry + return result, mock_setup_entry async def test_form_select_modem(hass: HomeAssistant) -> None: @@ -125,13 +123,12 @@ async def test_form_select_plm(hass: HomeAssistant) -> None: result = await _init_form(hass, STEP_PLM) - result2, mock_setup, mock_setup_entry = await _device_form( + result2, mock_setup_entry = await _device_form( hass, result["flow_id"], mock_successful_connection, MOCK_USER_INPUT_PLM ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == MOCK_USER_INPUT_PLM - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -142,7 +139,7 @@ async def test_form_select_plm_no_usb(hass: HomeAssistant) -> None: USB_PORTS.clear() result = await _init_form(hass, STEP_PLM) - result2, _, _ = await _device_form( + result2, _ = await _device_form( hass, result["flow_id"], mock_successful_connection, None ) USB_PORTS.update(temp_usb_list) @@ -155,18 +152,17 @@ async def test_form_select_plm_manual(hass: HomeAssistant) -> None: result = await _init_form(hass, STEP_PLM) - result2, mock_setup, mock_setup_entry = await _device_form( + result2, mock_setup_entry = await _device_form( hass, result["flow_id"], mock_failed_connection, MOCK_USER_INPUT_PLM_MANUAL ) - result3, mock_setup, mock_setup_entry = await _device_form( + result3, mock_setup_entry = await _device_form( hass, result2["flow_id"], mock_successful_connection, MOCK_USER_INPUT_PLM ) assert result2["type"] is FlowResultType.FORM assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == MOCK_USER_INPUT_PLM - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -175,7 +171,7 @@ async def test_form_select_hub_v1(hass: HomeAssistant) -> None: result = await _init_form(hass, STEP_HUB_V1) - result2, mock_setup, mock_setup_entry = await _device_form( + result2, mock_setup_entry = await _device_form( hass, result["flow_id"], mock_successful_connection, MOCK_USER_INPUT_HUB_V1 ) assert result2["type"] is FlowResultType.CREATE_ENTRY @@ -184,7 +180,6 @@ async def test_form_select_hub_v1(hass: HomeAssistant) -> None: CONF_HUB_VERSION: 1, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -193,7 +188,7 @@ async def test_form_select_hub_v2(hass: HomeAssistant) -> None: result = await _init_form(hass, STEP_HUB_V2) - result2, mock_setup, mock_setup_entry = await _device_form( + result2, mock_setup_entry = await _device_form( hass, result["flow_id"], mock_successful_connection, MOCK_USER_INPUT_HUB_V2 ) assert result2["type"] is FlowResultType.CREATE_ENTRY @@ -202,7 +197,6 @@ async def test_form_select_hub_v2(hass: HomeAssistant) -> None: CONF_HUB_VERSION: 2, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -233,7 +227,7 @@ async def test_failed_connection_plm(hass: HomeAssistant) -> None: result = await _init_form(hass, STEP_PLM) - result2, _, _ = await _device_form( + result2, _ = await _device_form( hass, result["flow_id"], mock_failed_connection, MOCK_USER_INPUT_PLM ) assert result2["type"] is FlowResultType.FORM @@ -245,10 +239,10 @@ async def test_failed_connection_plm_manually(hass: HomeAssistant) -> None: result = await _init_form(hass, STEP_PLM) - result2, _, _ = await _device_form( + result2, _ = await _device_form( hass, result["flow_id"], mock_successful_connection, MOCK_USER_INPUT_PLM_MANUAL ) - result3, _, _ = await _device_form( + result3, _ = await _device_form( hass, result["flow_id"], mock_failed_connection, MOCK_USER_INPUT_PLM ) assert result3["type"] is FlowResultType.FORM @@ -260,7 +254,7 @@ async def test_failed_connection_hub(hass: HomeAssistant) -> None: result = await _init_form(hass, STEP_HUB_V2) - result2, _, _ = await _device_form( + result2, _ = await _device_form( hass, result["flow_id"], mock_failed_connection, MOCK_USER_INPUT_HUB_V2 ) assert result2["type"] is FlowResultType.FORM @@ -284,7 +278,7 @@ async def test_discovery_via_usb(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_usb" - with patch(PATCH_CONNECTION), patch(PATCH_ASYNC_SETUP, return_value=True): + with patch(PATCH_CONNECTION): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) From b9251e94a910f0229e21d613131a473f50c83550 Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:15:48 +0200 Subject: [PATCH 2003/2411] Bump solarlog_cli to v0.1.6 (#123218) bump solarlog_cli to v0.1.6 --- homeassistant/components/solarlog/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/solarlog/manifest.json b/homeassistant/components/solarlog/manifest.json index 0878d652f43..0c097b7146d 100644 --- a/homeassistant/components/solarlog/manifest.json +++ b/homeassistant/components/solarlog/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/solarlog", "iot_class": "local_polling", "loggers": ["solarlog_cli"], - "requirements": ["solarlog_cli==0.1.5"] + "requirements": ["solarlog_cli==0.1.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 927b3d45310..79c4b327796 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2622,7 +2622,7 @@ soco==0.30.4 solaredge-local==0.2.3 # homeassistant.components.solarlog -solarlog_cli==0.1.5 +solarlog_cli==0.1.6 # homeassistant.components.solax solax==3.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a75a4b6cec8..7220510facd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2068,7 +2068,7 @@ snapcast==2.3.6 soco==0.30.4 # homeassistant.components.solarlog -solarlog_cli==0.1.5 +solarlog_cli==0.1.6 # homeassistant.components.solax solax==3.1.1 From bc02925630217105ee6a627169918e0ce6714f73 Mon Sep 17 00:00:00 2001 From: flopp999 <21694965+flopp999@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:21:48 +0200 Subject: [PATCH 2004/2411] Fix growatt server tlx battery api key (#123191) --- .../components/growatt_server/sensor_types/tlx.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/growatt_server/sensor_types/tlx.py b/homeassistant/components/growatt_server/sensor_types/tlx.py index d8f158f2421..bf8746e08ac 100644 --- a/homeassistant/components/growatt_server/sensor_types/tlx.py +++ b/homeassistant/components/growatt_server/sensor_types/tlx.py @@ -327,14 +327,14 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( GrowattSensorEntityDescription( key="tlx_battery_2_discharge_w", translation_key="tlx_battery_2_discharge_w", - api_key="bdc1DischargePower", + api_key="bdc2DischargePower", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="tlx_battery_2_discharge_total", translation_key="tlx_battery_2_discharge_total", - api_key="bdc1DischargeTotal", + api_key="bdc2DischargeTotal", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -376,14 +376,14 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( GrowattSensorEntityDescription( key="tlx_battery_2_charge_w", translation_key="tlx_battery_2_charge_w", - api_key="bdc1ChargePower", + api_key="bdc2ChargePower", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="tlx_battery_2_charge_total", translation_key="tlx_battery_2_charge_total", - api_key="bdc1ChargeTotal", + api_key="bdc2ChargeTotal", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, From 86d8c3b31ac9d0c36ca77ba8b191999827c9e357 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 6 Aug 2024 11:51:04 +0200 Subject: [PATCH 2005/2411] Update knx-frontend to 2024.8.6.85349 (#123226) --- 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 0f96970f3ae..37206df4c83 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.0.0", "xknxproject==3.7.1", - "knx-frontend==2024.7.25.204106" + "knx-frontend==2024.8.6.85349" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 79c4b327796..aeee59b04ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1216,7 +1216,7 @@ kiwiki-client==0.1.1 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.7.25.204106 +knx-frontend==2024.8.6.85349 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7220510facd..a67210ffe30 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1015,7 +1015,7 @@ kegtron-ble==0.4.0 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.7.25.204106 +knx-frontend==2024.8.6.85349 # homeassistant.components.konnected konnected==1.2.0 From a2dd017229dd2ca0b5efb294c9199d35d47ca455 Mon Sep 17 00:00:00 2001 From: Guy Lowe Date: Tue, 6 Aug 2024 21:56:39 +1200 Subject: [PATCH 2006/2411] Add unit tests for SNMP integer Switches (#123094) * Add unit tests for SNMP Switches (integer only) * Add unit test for SNMP switches (integer unknown) * log a warning when SNMP response is not a recognised payload * Use a single configuration for all test_integer_switch tests * Tweak unknown SNMP response warning * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker * import STATE_ consts * rename tests/components/snmp/test_integer_switch.py to test_switch.py * check that a warning is logged if the SNMP response payload is unknown --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/snmp/switch.py | 5 ++ tests/components/snmp/test_switch.py | 67 +++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 tests/components/snmp/test_switch.py diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index e3ce09cbf48..92e27daed6c 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -277,6 +277,11 @@ class SnmpSwitch(SwitchEntity): ): self._state = False else: + _LOGGER.warning( + "Invalid payload '%s' received for entity %s, state is unknown", + resrow[-1], + self.entity_id, + ) self._state = None @property diff --git a/tests/components/snmp/test_switch.py b/tests/components/snmp/test_switch.py new file mode 100644 index 00000000000..adb9d1c59d0 --- /dev/null +++ b/tests/components/snmp/test_switch.py @@ -0,0 +1,67 @@ +"""SNMP switch tests.""" + +from unittest.mock import patch + +from pysnmp.hlapi import Integer32 +import pytest + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +config = { + SWITCH_DOMAIN: { + "platform": "snmp", + "host": "192.168.1.32", + # ippower-mib::ippoweroutlet1.0 + "baseoid": "1.3.6.1.4.1.38107.1.3.1.0", + "payload_on": 1, + "payload_off": 0, + }, +} + + +async def test_snmp_integer_switch_off(hass: HomeAssistant) -> None: + """Test snmp switch returning int 0 for off.""" + + mock_data = Integer32(0) + with patch( + "homeassistant.components.snmp.switch.getCmd", + return_value=(None, None, None, [[mock_data]]), + ): + assert await async_setup_component(hass, SWITCH_DOMAIN, config) + await hass.async_block_till_done() + state = hass.states.get("switch.snmp") + assert state.state == STATE_OFF + + +async def test_snmp_integer_switch_on(hass: HomeAssistant) -> None: + """Test snmp switch returning int 1 for on.""" + + mock_data = Integer32(1) + with patch( + "homeassistant.components.snmp.switch.getCmd", + return_value=(None, None, None, [[mock_data]]), + ): + assert await async_setup_component(hass, SWITCH_DOMAIN, config) + await hass.async_block_till_done() + state = hass.states.get("switch.snmp") + assert state.state == STATE_ON + + +async def test_snmp_integer_switch_unknown( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test snmp switch returning int 3 (not a configured payload) for unknown.""" + + mock_data = Integer32(3) + with patch( + "homeassistant.components.snmp.switch.getCmd", + return_value=(None, None, None, [[mock_data]]), + ): + assert await async_setup_component(hass, SWITCH_DOMAIN, config) + await hass.async_block_till_done() + state = hass.states.get("switch.snmp") + assert state.state == STATE_UNKNOWN + assert "Invalid payload '3' received for entity" in caplog.text From 78d1cd79af3aec7b7e649f53477fd8b97380be7b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 6 Aug 2024 12:22:14 +0200 Subject: [PATCH 2007/2411] Bump yt-dlp to 2023.08.06 (#123229) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index cd312413db3..2285d7bce7d 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2024.07.16"], + "requirements": ["yt-dlp==2024.08.06"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index aeee59b04ff..234bfe664c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2971,7 +2971,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.07.16 +yt-dlp==2024.08.06 # homeassistant.components.zamg zamg==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a67210ffe30..7c5b7b47261 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2351,7 +2351,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.07.16 +yt-dlp==2024.08.06 # homeassistant.components.zamg zamg==0.3.6 From 1fc6ce3acd29a93982798110f65cb8237670f309 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Tue, 6 Aug 2024 06:35:47 -0400 Subject: [PATCH 2008/2411] Fix yamaha legacy receivers (#122985) --- .../components/yamaha/media_player.py | 15 +++++--- tests/components/yamaha/test_media_player.py | 37 ++++++++++++++++++- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 507f485fcc7..a8200ea3373 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations +import contextlib import logging from typing import Any @@ -129,11 +130,15 @@ def _discovery(config_info): else: _LOGGER.debug("Config Zones") zones = None - for recv in rxv.find(): - if recv.ctrl_url == config_info.ctrl_url: - _LOGGER.debug("Config Zones Matched %s", config_info.ctrl_url) - zones = recv.zone_controllers() - break + + # Fix for upstream issues in rxv.find() with some hardware. + with contextlib.suppress(AttributeError): + for recv in rxv.find(): + if recv.ctrl_url == config_info.ctrl_url: + _LOGGER.debug("Config Zones Matched %s", config_info.ctrl_url) + zones = recv.zone_controllers() + break + if not zones: _LOGGER.debug("Config Zones Fallback") zones = rxv.RXV(config_info.ctrl_url, config_info.name).zone_controllers() diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 66d0a42f256..804b800aaef 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -53,7 +53,20 @@ def device_fixture(main_zone): yield device -async def test_setup_host(hass: HomeAssistant, device, main_zone) -> None: +@pytest.fixture(name="device2") +def device2_fixture(main_zone): + """Mock the yamaha device.""" + device = FakeYamahaDevice( + "http://127.0.0.1:80/YamahaRemoteControl/ctrl", "Receiver 2", zones=[main_zone] + ) + with ( + patch("rxv.RXV", return_value=device), + patch("rxv.find", return_value=[device]), + ): + yield device + + +async def test_setup_host(hass: HomeAssistant, device, device2, main_zone) -> None: """Test set up integration with host.""" assert await async_setup_component(hass, MP_DOMAIN, CONFIG) await hass.async_block_till_done() @@ -63,6 +76,28 @@ async def test_setup_host(hass: HomeAssistant, device, main_zone) -> None: assert state is not None assert state.state == "off" + with patch("rxv.find", return_value=[device2]): + assert await async_setup_component(hass, MP_DOMAIN, CONFIG) + await hass.async_block_till_done() + + state = hass.states.get("media_player.yamaha_receiver_main_zone") + + assert state is not None + assert state.state == "off" + + +async def test_setup_attribute_error(hass: HomeAssistant, device, main_zone) -> None: + """Test set up integration encountering an Attribute Error.""" + + with patch("rxv.find", side_effect=AttributeError): + assert await async_setup_component(hass, MP_DOMAIN, CONFIG) + await hass.async_block_till_done() + + state = hass.states.get("media_player.yamaha_receiver_main_zone") + + assert state is not None + assert state.state == "off" + async def test_setup_no_host(hass: HomeAssistant, device, main_zone) -> None: """Test set up integration without host.""" From bc380859e8b26141c02470f13140ed5052331069 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 12:48:15 +0200 Subject: [PATCH 2009/2411] Update frontend to 20240806.0 (#123230) --- 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 82dc9cdb83f..a91feb82461 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240805.1"] + "requirements": ["home-assistant-frontend==20240806.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9f327697a19..9622c1acac0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240805.1 +home-assistant-frontend==20240806.0 home-assistant-intents==2024.7.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 234bfe664c6..97419def421 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240805.1 +home-assistant-frontend==20240806.0 # homeassistant.components.conversation home-assistant-intents==2024.7.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c5b7b47261..257ea036d3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -919,7 +919,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240805.1 +home-assistant-frontend==20240806.0 # homeassistant.components.conversation home-assistant-intents==2024.7.29 From fd5533d719ee82177579df92b225214b14ff7d9a Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Tue, 6 Aug 2024 06:35:47 -0400 Subject: [PATCH 2010/2411] Fix yamaha legacy receivers (#122985) --- .../components/yamaha/media_player.py | 15 +++++--- tests/components/yamaha/test_media_player.py | 37 ++++++++++++++++++- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 507f485fcc7..a8200ea3373 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations +import contextlib import logging from typing import Any @@ -129,11 +130,15 @@ def _discovery(config_info): else: _LOGGER.debug("Config Zones") zones = None - for recv in rxv.find(): - if recv.ctrl_url == config_info.ctrl_url: - _LOGGER.debug("Config Zones Matched %s", config_info.ctrl_url) - zones = recv.zone_controllers() - break + + # Fix for upstream issues in rxv.find() with some hardware. + with contextlib.suppress(AttributeError): + for recv in rxv.find(): + if recv.ctrl_url == config_info.ctrl_url: + _LOGGER.debug("Config Zones Matched %s", config_info.ctrl_url) + zones = recv.zone_controllers() + break + if not zones: _LOGGER.debug("Config Zones Fallback") zones = rxv.RXV(config_info.ctrl_url, config_info.name).zone_controllers() diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 66d0a42f256..804b800aaef 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -53,7 +53,20 @@ def device_fixture(main_zone): yield device -async def test_setup_host(hass: HomeAssistant, device, main_zone) -> None: +@pytest.fixture(name="device2") +def device2_fixture(main_zone): + """Mock the yamaha device.""" + device = FakeYamahaDevice( + "http://127.0.0.1:80/YamahaRemoteControl/ctrl", "Receiver 2", zones=[main_zone] + ) + with ( + patch("rxv.RXV", return_value=device), + patch("rxv.find", return_value=[device]), + ): + yield device + + +async def test_setup_host(hass: HomeAssistant, device, device2, main_zone) -> None: """Test set up integration with host.""" assert await async_setup_component(hass, MP_DOMAIN, CONFIG) await hass.async_block_till_done() @@ -63,6 +76,28 @@ async def test_setup_host(hass: HomeAssistant, device, main_zone) -> None: assert state is not None assert state.state == "off" + with patch("rxv.find", return_value=[device2]): + assert await async_setup_component(hass, MP_DOMAIN, CONFIG) + await hass.async_block_till_done() + + state = hass.states.get("media_player.yamaha_receiver_main_zone") + + assert state is not None + assert state.state == "off" + + +async def test_setup_attribute_error(hass: HomeAssistant, device, main_zone) -> None: + """Test set up integration encountering an Attribute Error.""" + + with patch("rxv.find", side_effect=AttributeError): + assert await async_setup_component(hass, MP_DOMAIN, CONFIG) + await hass.async_block_till_done() + + state = hass.states.get("media_player.yamaha_receiver_main_zone") + + assert state is not None + assert state.state == "off" + async def test_setup_no_host(hass: HomeAssistant, device, main_zone) -> None: """Test set up integration without host.""" From 6d47a4d7e4d927f1d61448b32f3c6d7a6dc81ecf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 6 Aug 2024 20:12:31 +1200 Subject: [PATCH 2011/2411] Add support for ESPHome update entities to be checked on demand (#123161) --- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/esphome/update.py | 11 +++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/esphome/test_update.py | 14 ++++++++++++++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index ff7569bbc5f..97724a12203 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==24.6.2", + "aioesphomeapi==25.0.0", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index e86c88ddf5b..b7905fb4fdb 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -8,6 +8,7 @@ from typing import Any from aioesphomeapi import ( DeviceInfo as ESPHomeDeviceInfo, EntityInfo, + UpdateCommand, UpdateInfo, UpdateState, ) @@ -259,9 +260,15 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity): """Return the title of the update.""" return self._state.title + @convert_api_error_ha_error + async def async_update(self) -> None: + """Command device to check for update.""" + if self.available: + self._client.update_command(key=self._key, command=UpdateCommand.CHECK) + @convert_api_error_ha_error async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: - """Update the current value.""" - self._client.update_command(key=self._key, install=True) + """Command device to install update.""" + self._client.update_command(key=self._key, command=UpdateCommand.INSTALL) diff --git a/requirements_all.txt b/requirements_all.txt index 2fd2c93a687..f54ae205864 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -237,7 +237,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==24.6.2 +aioesphomeapi==25.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a71321bc5b..2974d065380 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -225,7 +225,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==24.6.2 +aioesphomeapi==25.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index c9826c3f347..83e89b1de00 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -8,6 +8,7 @@ from aioesphomeapi import ( APIClient, EntityInfo, EntityState, + UpdateCommand, UpdateInfo, UpdateState, UserService, @@ -15,6 +16,10 @@ from aioesphomeapi import ( import pytest from homeassistant.components.esphome.dashboard import async_get_dashboard +from homeassistant.components.homeassistant import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, +) from homeassistant.components.update import ( DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL, @@ -527,3 +532,12 @@ async def test_generic_device_update_entity_has_update( assert state is not None assert state.state == STATE_ON assert state.attributes["in_progress"] == 50 + + await hass.services.async_call( + HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: "update.test_myupdate"}, + blocking=True, + ) + + mock_client.update_command.assert_called_with(key=1, command=UpdateCommand.CHECK) From 6af1e25d7e2a83463b2ac33f9e61442dd7aeb6eb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 6 Aug 2024 20:11:08 +1200 Subject: [PATCH 2012/2411] Show project version as `sw_version` in ESPHome (#123183) --- homeassistant/components/esphome/manager.py | 6 +++--- tests/components/esphome/test_manager.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index e8d002fba9d..ef2a6862f10 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -654,12 +654,13 @@ def _async_setup_device_registry( if device_info.manufacturer: manufacturer = device_info.manufacturer model = device_info.model - hw_version = None if device_info.project_name: project_name = device_info.project_name.split(".") manufacturer = project_name[0] model = project_name[1] - hw_version = device_info.project_version + sw_version = ( + f"{device_info.project_version} (ESPHome {device_info.esphome_version})" + ) suggested_area = None if device_info.suggested_area: @@ -674,7 +675,6 @@ def _async_setup_device_registry( manufacturer=manufacturer, model=model, sw_version=sw_version, - hw_version=hw_version, suggested_area=suggested_area, ) return device_entry.id diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 01f267581f4..651c52cd083 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -1024,7 +1024,7 @@ async def test_esphome_device_with_project( ) assert dev.manufacturer == "mfr" assert dev.model == "model" - assert dev.hw_version == "2.2.2" + assert dev.sw_version == "2.2.2 (ESPHome 1.0.0)" async def test_esphome_device_with_manufacturer( From 495fd946bc044d251b07500336490c58e84f093d Mon Sep 17 00:00:00 2001 From: flopp999 <21694965+flopp999@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:21:48 +0200 Subject: [PATCH 2013/2411] Fix growatt server tlx battery api key (#123191) --- .../components/growatt_server/sensor_types/tlx.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/growatt_server/sensor_types/tlx.py b/homeassistant/components/growatt_server/sensor_types/tlx.py index d8f158f2421..bf8746e08ac 100644 --- a/homeassistant/components/growatt_server/sensor_types/tlx.py +++ b/homeassistant/components/growatt_server/sensor_types/tlx.py @@ -327,14 +327,14 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( GrowattSensorEntityDescription( key="tlx_battery_2_discharge_w", translation_key="tlx_battery_2_discharge_w", - api_key="bdc1DischargePower", + api_key="bdc2DischargePower", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="tlx_battery_2_discharge_total", translation_key="tlx_battery_2_discharge_total", - api_key="bdc1DischargeTotal", + api_key="bdc2DischargeTotal", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -376,14 +376,14 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( GrowattSensorEntityDescription( key="tlx_battery_2_charge_w", translation_key="tlx_battery_2_charge_w", - api_key="bdc1ChargePower", + api_key="bdc2ChargePower", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="tlx_battery_2_charge_total", translation_key="tlx_battery_2_charge_total", - api_key="bdc1ChargeTotal", + api_key="bdc2ChargeTotal", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, From f796950493eeedd9be486684b4f1fdd4661229ab Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Tue, 6 Aug 2024 04:06:35 -0400 Subject: [PATCH 2014/2411] Update greeclimate to 2.1.0 (#123210) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index ca1c4b5b754..dba8cd6077c 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/gree", "iot_class": "local_polling", "loggers": ["greeclimate"], - "requirements": ["greeclimate==2.0.0"] + "requirements": ["greeclimate==2.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index f54ae205864..04daff1f122 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1007,7 +1007,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==2.0.0 +greeclimate==2.1.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2974d065380..617a239e388 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -848,7 +848,7 @@ govee-local-api==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==2.0.0 +greeclimate==2.1.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 From 01b54fe1a97faa0100c76282feb3d843c43ff632 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 6 Aug 2024 11:51:04 +0200 Subject: [PATCH 2015/2411] Update knx-frontend to 2024.8.6.85349 (#123226) --- 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 0f96970f3ae..37206df4c83 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.0.0", "xknxproject==3.7.1", - "knx-frontend==2024.7.25.204106" + "knx-frontend==2024.8.6.85349" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 04daff1f122..f62e8ba616f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1216,7 +1216,7 @@ kiwiki-client==0.1.1 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.7.25.204106 +knx-frontend==2024.8.6.85349 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 617a239e388..3475a4f9f9a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1012,7 +1012,7 @@ kegtron-ble==0.4.0 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.7.25.204106 +knx-frontend==2024.8.6.85349 # homeassistant.components.konnected konnected==1.2.0 From 97587fae08ffb0c0a9463daa345b2b3507466c29 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 6 Aug 2024 12:22:14 +0200 Subject: [PATCH 2016/2411] Bump yt-dlp to 2023.08.06 (#123229) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index cd312413db3..2285d7bce7d 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2024.07.16"], + "requirements": ["yt-dlp==2024.08.06"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index f62e8ba616f..e275d37d0cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2971,7 +2971,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.07.16 +yt-dlp==2024.08.06 # homeassistant.components.zamg zamg==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3475a4f9f9a..39fff2f33c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2348,7 +2348,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.07.16 +yt-dlp==2024.08.06 # homeassistant.components.zamg zamg==0.3.6 From 77bcbbcf538abcd89aca33c142e20ad2cf778cfe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 12:48:15 +0200 Subject: [PATCH 2017/2411] Update frontend to 20240806.0 (#123230) --- 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 82dc9cdb83f..a91feb82461 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240805.1"] + "requirements": ["home-assistant-frontend==20240806.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6fc0d6f535d..96cf6d7624c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240805.1 +home-assistant-frontend==20240806.0 home-assistant-intents==2024.7.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index e275d37d0cd..4614d760a84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240805.1 +home-assistant-frontend==20240806.0 # homeassistant.components.conversation home-assistant-intents==2024.7.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39fff2f33c9..e9f7b37122c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -916,7 +916,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240805.1 +home-assistant-frontend==20240806.0 # homeassistant.components.conversation home-assistant-intents==2024.7.29 From d24a87145df931131c0fc3f4cf2ec46bebe82265 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 13:18:22 +0200 Subject: [PATCH 2018/2411] Mark Alexa integration as system type (#123232) --- homeassistant/components/alexa/manifest.json | 1 + homeassistant/generated/integrations.json | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index 84a4e152c1d..de59d28925f 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@home-assistant/cloud", "@ochlocracy", "@jbouwh"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/alexa", + "integration_type": "system", "iot_class": "cloud_push" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 2516f25e6f2..2e68bee3146 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -200,12 +200,6 @@ "amazon": { "name": "Amazon", "integrations": { - "alexa": { - "integration_type": "hub", - "config_flow": false, - "iot_class": "cloud_push", - "name": "Amazon Alexa" - }, "amazon_polly": { "integration_type": "hub", "config_flow": false, From 9d2c2d90c83a5cbf77a261c440caaee209853057 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 13:20:32 +0200 Subject: [PATCH 2019/2411] Mark Google Assistant integration as system type (#123233) --- homeassistant/components/google_assistant/manifest.json | 1 + homeassistant/generated/integrations.json | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index e36f6a1ca87..a38ea4f7cfb 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@home-assistant/cloud"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/google_assistant", + "integration_type": "system", "iot_class": "cloud_push" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 2e68bee3146..69d65fd4bd8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2225,12 +2225,6 @@ "google": { "name": "Google", "integrations": { - "google_assistant": { - "integration_type": "hub", - "config_flow": false, - "iot_class": "cloud_push", - "name": "Google Assistant" - }, "google_assistant_sdk": { "integration_type": "service", "config_flow": true, From c1953e938d530f7dd1056ede09cc0ee32942551d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 13:18:22 +0200 Subject: [PATCH 2020/2411] Mark Alexa integration as system type (#123232) --- homeassistant/components/alexa/manifest.json | 1 + homeassistant/generated/integrations.json | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index 84a4e152c1d..de59d28925f 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@home-assistant/cloud", "@ochlocracy", "@jbouwh"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/alexa", + "integration_type": "system", "iot_class": "cloud_push" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 5107587ab89..73c60c1373e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -200,12 +200,6 @@ "amazon": { "name": "Amazon", "integrations": { - "alexa": { - "integration_type": "hub", - "config_flow": false, - "iot_class": "cloud_push", - "name": "Amazon Alexa" - }, "amazon_polly": { "integration_type": "hub", "config_flow": false, From 5b2e188b528d2a3ae1bc3b8cb3a03badd028b4d3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 13:20:32 +0200 Subject: [PATCH 2021/2411] Mark Google Assistant integration as system type (#123233) --- homeassistant/components/google_assistant/manifest.json | 1 + homeassistant/generated/integrations.json | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index e36f6a1ca87..a38ea4f7cfb 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@home-assistant/cloud"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/google_assistant", + "integration_type": "system", "iot_class": "cloud_push" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 73c60c1373e..85369b238db 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2236,12 +2236,6 @@ "google": { "name": "Google", "integrations": { - "google_assistant": { - "integration_type": "hub", - "config_flow": false, - "iot_class": "cloud_push", - "name": "Google Assistant" - }, "google_assistant_sdk": { "integration_type": "service", "config_flow": true, From e9fe98f7f9bf2fd04d29c2145bd4719d06144db1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 13:22:46 +0200 Subject: [PATCH 2022/2411] Bump version to 2024.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 981fc42fd36..ed0a44bb7cc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index c29252b1ced..86fa1c8bbc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b3" +version = "2024.8.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 7cb94e6392326c495948b6874fbf637a31355d3b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 6 Aug 2024 14:01:48 +0200 Subject: [PATCH 2023/2411] Bump uvcclient to 0.12.1 (#123237) --- homeassistant/components/uvc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/uvc/manifest.json b/homeassistant/components/uvc/manifest.json index 0553eba320a..c72b865b5ef 100644 --- a/homeassistant/components/uvc/manifest.json +++ b/homeassistant/components/uvc/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/uvc", "iot_class": "local_polling", "loggers": ["uvcclient"], - "requirements": ["uvcclient==0.11.1"] + "requirements": ["uvcclient==0.12.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 97419def421..42d6f362e1f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2842,7 +2842,7 @@ upcloud-api==2.5.1 url-normalize==1.4.3 # homeassistant.components.uvc -uvcclient==0.11.1 +uvcclient==0.12.1 # homeassistant.components.roborock vacuum-map-parser-roborock==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 257ea036d3e..bb61d52b171 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2240,7 +2240,7 @@ upcloud-api==2.5.1 url-normalize==1.4.3 # homeassistant.components.uvc -uvcclient==0.11.1 +uvcclient==0.12.1 # homeassistant.components.roborock vacuum-map-parser-roborock==0.1.2 From 260642345d2a2b1f335846b14fc455bc2c2ed9eb Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 6 Aug 2024 14:55:14 +0200 Subject: [PATCH 2024/2411] Delete mobile_app cloudhook if not logged into the cloud (#123234) --- .../components/mobile_app/__init__.py | 18 ++++++--- tests/components/mobile_app/test_init.py | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index a8577cc596d..80893e0cbfa 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -124,12 +124,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ): await async_create_cloud_hook(hass, webhook_id, entry) - if ( - CONF_CLOUDHOOK_URL not in entry.data - and cloud.async_active_subscription(hass) - and cloud.async_is_connected(hass) - ): - await async_create_cloud_hook(hass, webhook_id, entry) + if cloud.async_is_logged_in(hass): + if ( + CONF_CLOUDHOOK_URL not in entry.data + and cloud.async_active_subscription(hass) + and cloud.async_is_connected(hass) + ): + await async_create_cloud_hook(hass, webhook_id, entry) + elif CONF_CLOUDHOOK_URL in entry.data: + # If we have a cloudhook but no longer logged in to the cloud, remove it from the entry + data = dict(entry.data) + data.pop(CONF_CLOUDHOOK_URL) + hass.config_entries.async_update_entry(entry, data=data) entry.async_on_unload(cloud.async_listen_connection_change(hass, manage_cloudhook)) diff --git a/tests/components/mobile_app/test_init.py b/tests/components/mobile_app/test_init.py index 15380a0d8d7..e1c7ed27cf9 100644 --- a/tests/components/mobile_app/test_init.py +++ b/tests/components/mobile_app/test_init.py @@ -89,6 +89,7 @@ async def _test_create_cloud_hook( "homeassistant.components.cloud.async_active_subscription", return_value=async_active_subscription_return_value, ), + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), patch("homeassistant.components.cloud.async_is_connected", return_value=True), patch( "homeassistant.components.cloud.async_get_or_create_cloudhook", @@ -187,3 +188,41 @@ async def test_create_cloud_hook_after_connection( ) await _test_create_cloud_hook(hass, hass_admin_user, {}, False, additional_steps) + + +@pytest.mark.parametrize( + ("cloud_logged_in", "should_cloudhook_exist"), + [(True, True), (False, False)], +) +async def test_delete_cloud_hook( + hass: HomeAssistant, + hass_admin_user: MockUser, + cloud_logged_in: bool, + should_cloudhook_exist: bool, +) -> None: + """Test deleting the cloud hook only when logged out of the cloud.""" + + config_entry = MockConfigEntry( + data={ + **REGISTER_CLEARTEXT, + CONF_WEBHOOK_ID: "test-webhook-id", + ATTR_DEVICE_NAME: "Test", + ATTR_DEVICE_ID: "Test", + CONF_USER_ID: hass_admin_user.id, + CONF_CLOUDHOOK_URL: "https://hook-url-already-exists", + }, + domain=DOMAIN, + title="Test", + ) + config_entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.cloud.async_is_logged_in", + return_value=cloud_logged_in, + ), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + assert (CONF_CLOUDHOOK_URL in config_entry.data) == should_cloudhook_exist From f94bf51bb53ff66fbb340c33c091067a56a97808 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 14:55:37 +0200 Subject: [PATCH 2025/2411] Remove myself from DSMR codeowners (#123243) --- CODEOWNERS | 4 ++-- homeassistant/components/dsmr/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c9b3a53184f..710846a7f42 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -349,8 +349,8 @@ build.json @home-assistant/supervisor /tests/components/dremel_3d_printer/ @tkdrob /homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer /tests/components/drop_connect/ @ChandlerSystems @pfrazer -/homeassistant/components/dsmr/ @Robbie1221 @frenck -/tests/components/dsmr/ @Robbie1221 @frenck +/homeassistant/components/dsmr/ @Robbie1221 +/tests/components/dsmr/ @Robbie1221 /homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna /tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna /homeassistant/components/duotecno/ @cereal2nd diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index 5490b2a6503..561f06d1bbe 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -1,7 +1,7 @@ { "domain": "dsmr", "name": "DSMR Smart Meter", - "codeowners": ["@Robbie1221", "@frenck"], + "codeowners": ["@Robbie1221"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dsmr", "integration_type": "hub", From fe4e6f24f547f398a03b4ce60670a4c63aae6403 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Aug 2024 08:32:58 -0500 Subject: [PATCH 2026/2411] Fix sense doing blocking I/O in the event loop (#123247) --- homeassistant/components/sense/__init__.py | 12 ++++++++++-- homeassistant/components/sense/config_flow.py | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 28408c0cb7d..58e993ad6e0 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from datetime import timedelta +from functools import partial import logging from typing import Any @@ -80,8 +81,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: SenseConfigEntry) -> boo client_session = async_get_clientsession(hass) - gateway = ASyncSenseable( - api_timeout=timeout, wss_timeout=timeout, client_session=client_session + # Creating the AsyncSenseable object loads + # ssl certificates which does blocking IO + gateway = await hass.async_add_executor_job( + partial( + ASyncSenseable, + api_timeout=timeout, + wss_timeout=timeout, + client_session=client_session, + ) ) gateway.rate_limit = ACTIVE_UPDATE_RATE diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index 25c6898aec8..dab80b99e1a 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Sense integration.""" from collections.abc import Mapping +from functools import partial import logging from typing import Any @@ -48,8 +49,15 @@ class SenseConfigFlow(ConfigFlow, domain=DOMAIN): timeout = self._auth_data[CONF_TIMEOUT] client_session = async_get_clientsession(self.hass) - self._gateway = ASyncSenseable( - api_timeout=timeout, wss_timeout=timeout, client_session=client_session + # Creating the AsyncSenseable object loads + # ssl certificates which does blocking IO + self._gateway = await self.hass.async_add_executor_job( + partial( + ASyncSenseable, + api_timeout=timeout, + wss_timeout=timeout, + client_session=client_session, + ) ) self._gateway.rate_limit = ACTIVE_UPDATE_RATE await self._gateway.authenticate( From 1eaaa00687e8d86c09c18d4b79f160856d991c2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Aug 2024 09:00:37 -0500 Subject: [PATCH 2027/2411] Detect blocking ssl context creation in the event loop (#123240) --- homeassistant/block_async_io.py | 19 +++++++++++++++++++ tests/test_block_async_io.py | 24 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index 5b8ba535b5a..6ea0925574e 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -8,6 +8,7 @@ import glob from http.client import HTTPConnection import importlib import os +from ssl import SSLContext import sys import threading import time @@ -143,6 +144,24 @@ _BLOCKING_CALLS: tuple[BlockingCall, ...] = ( strict_core=False, skip_for_tests=True, ), + BlockingCall( + original_func=SSLContext.load_default_certs, + object=SSLContext, + function="load_default_certs", + check_allowed=None, + strict=False, + strict_core=False, + skip_for_tests=True, + ), + BlockingCall( + original_func=SSLContext.load_verify_locations, + object=SSLContext, + function="load_verify_locations", + check_allowed=None, + strict=False, + strict_core=False, + skip_for_tests=True, + ), ) diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index ef4f9df60f6..78b8711310b 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -5,6 +5,7 @@ import glob import importlib import os from pathlib import Path, PurePosixPath +import ssl import time from typing import Any from unittest.mock import Mock, patch @@ -330,6 +331,29 @@ async def test_protect_loop_walk( assert "Detected blocking call to walk with args" not in caplog.text +async def test_protect_loop_load_default_certs( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test SSLContext.load_default_certs calls in the loop are logged.""" + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() + context = ssl.create_default_context() + assert "Detected blocking call to load_default_certs" in caplog.text + assert context + + +async def test_protect_loop_load_verify_locations( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test SSLContext.load_verify_locations calls in the loop are logged.""" + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() + context = ssl.create_default_context() + with pytest.raises(OSError): + context.load_verify_locations("/dev/null") + assert "Detected blocking call to load_verify_locations" in caplog.text + + async def test_open_calls_ignored_in_tests(caplog: pytest.LogCaptureFixture) -> None: """Test opening a file in tests is ignored.""" assert block_async_io._IN_TESTS From c612cf95a8ac15a43ada3d5cf267f38e4e514bb5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 16:04:29 +0200 Subject: [PATCH 2028/2411] Mark FFmpeg integration as system type (#123241) --- homeassistant/components/ffmpeg/manifest.json | 1 + homeassistant/generated/integrations.json | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/ffmpeg/manifest.json b/homeassistant/components/ffmpeg/manifest.json index 8cd7b1f504d..ab9f3ed65c1 100644 --- a/homeassistant/components/ffmpeg/manifest.json +++ b/homeassistant/components/ffmpeg/manifest.json @@ -3,5 +3,6 @@ "name": "FFmpeg", "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/ffmpeg", + "integration_type": "system", "requirements": ["ha-ffmpeg==3.2.0"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 69d65fd4bd8..f59e0883dd4 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1787,11 +1787,6 @@ "ffmpeg": { "name": "FFmpeg", "integrations": { - "ffmpeg": { - "integration_type": "hub", - "config_flow": false, - "name": "FFmpeg" - }, "ffmpeg_motion": { "integration_type": "hub", "config_flow": false, From 4627a565d32d0b20197923ea0c6fb200cc37e2fc Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 6 Aug 2024 16:16:22 +0200 Subject: [PATCH 2029/2411] Bump deebot-client to 8.3.0 (#123249) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 8838eb4f50a..560ee4d599c 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==8.2.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 42d6f362e1f..b683866f6d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -706,7 +706,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==8.2.0 +deebot-client==8.3.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb61d52b171..a55edb474ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -599,7 +599,7 @@ dbus-fast==2.22.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==8.2.0 +deebot-client==8.3.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 9414e6d47213eacf0391a631cab3fda4759afe64 Mon Sep 17 00:00:00 2001 From: Steve Repsher Date: Tue, 6 Aug 2024 10:17:54 -0400 Subject: [PATCH 2030/2411] Adapt static resource handler to aiohttp 3.10 (#123166) --- homeassistant/components/http/static.py | 79 +++++++------------------ tests/components/http/test_static.py | 35 +++-------- 2 files changed, 30 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index a7280fb9b2f..29c5840a4bf 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -3,81 +3,46 @@ from __future__ import annotations from collections.abc import Mapping -import mimetypes from pathlib import Path from typing import Final -from aiohttp import hdrs +from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE from aiohttp.web import FileResponse, Request, StreamResponse -from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound +from aiohttp.web_fileresponse import CONTENT_TYPES, FALLBACK_CONTENT_TYPE from aiohttp.web_urldispatcher import StaticResource from lru import LRU -from .const import KEY_HASS - CACHE_TIME: Final = 31 * 86400 # = 1 month CACHE_HEADER = f"public, max-age={CACHE_TIME}" -CACHE_HEADERS: Mapping[str, str] = {hdrs.CACHE_CONTROL: CACHE_HEADER} -PATH_CACHE: LRU[tuple[str, Path], tuple[Path | None, str | None]] = LRU(512) - - -def _get_file_path(rel_url: str, directory: Path) -> Path | None: - """Return the path to file on disk or None.""" - 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: Path = directory.joinpath(filename).resolve() - 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 FileNotFoundError +CACHE_HEADERS: Mapping[str, str] = {CACHE_CONTROL: CACHE_HEADER} +RESPONSE_CACHE: LRU[tuple[str, Path], tuple[Path, str]] = LRU(512) class CachingStaticResource(StaticResource): """Static Resource handler that will add cache headers.""" async def _handle(self, request: Request) -> StreamResponse: - """Return requested file from disk as a FileResponse.""" + """Wrap base handler to cache file path resolution and content type guess.""" rel_url = request.match_info["filename"] key = (rel_url, self._directory) - if (filepath_content_type := PATH_CACHE.get(key)) is None: - hass = request.app[KEY_HASS] - try: - filepath = await hass.async_add_executor_job(_get_file_path, *key) - except (ValueError, FileNotFoundError) as error: - # relatively safe - raise HTTPNotFound from error - except HTTPForbidden: - # forbidden - raise - except Exception as error: - # perm error or other kind! - request.app.logger.exception("Unexpected exception") - raise HTTPNotFound from error + response: StreamResponse - content_type: str | None = None - if filepath is not None: - content_type = (mimetypes.guess_type(rel_url))[ - 0 - ] or "application/octet-stream" - PATH_CACHE[key] = (filepath, content_type) + if key in RESPONSE_CACHE: + file_path, content_type = RESPONSE_CACHE[key] + response = FileResponse(file_path, chunk_size=self._chunk_size) + response.headers[CONTENT_TYPE] = content_type else: - filepath, content_type = filepath_content_type - - if filepath and content_type: - return FileResponse( - filepath, - chunk_size=self._chunk_size, - headers={ - hdrs.CACHE_CONTROL: CACHE_HEADER, - hdrs.CONTENT_TYPE: content_type, - }, + response = await super()._handle(request) + if not isinstance(response, FileResponse): + # Must be directory index; ignore caching + return response + file_path = response._path # noqa: SLF001 + response.content_type = ( + CONTENT_TYPES.guess_type(file_path)[0] or FALLBACK_CONTENT_TYPE ) + # Cache actual header after setter construction. + content_type = response.headers[CONTENT_TYPE] + RESPONSE_CACHE[key] = (file_path, content_type) - raise HTTPForbidden if filepath is None else HTTPNotFound + response.headers[CACHE_CONTROL] = CACHE_HEADER + return response diff --git a/tests/components/http/test_static.py b/tests/components/http/test_static.py index 52a5db5daa7..2ac7c6ded93 100644 --- a/tests/components/http/test_static.py +++ b/tests/components/http/test_static.py @@ -4,11 +4,10 @@ from http import HTTPStatus from pathlib import Path from aiohttp.test_utils import TestClient -from aiohttp.web_exceptions import HTTPForbidden import pytest from homeassistant.components.http import StaticPathConfig -from homeassistant.components.http.static import CachingStaticResource, _get_file_path +from homeassistant.components.http.static import CachingStaticResource from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant from homeassistant.helpers.http import KEY_ALLOW_CONFIGURED_CORS @@ -31,37 +30,19 @@ async def mock_http_client(hass: HomeAssistant, aiohttp_client: ClientSessionGen return await aiohttp_client(hass.http.app, server_kwargs={"skip_url_asserts": True}) -@pytest.mark.parametrize( - ("url", "canonical_url"), - [ - ("//a", "//a"), - ("///a", "///a"), - ("/c:\\a\\b", "/c:%5Ca%5Cb"), - ], -) -async def test_static_path_blocks_anchors( - hass: HomeAssistant, - mock_http_client: TestClient, - tmp_path: Path, - url: str, - canonical_url: str, +async def test_static_resource_show_index( + hass: HomeAssistant, mock_http_client: TestClient, tmp_path: Path ) -> None: - """Test static paths block anchors.""" + """Test static resource will return a directory index.""" app = hass.http.app - resource = CachingStaticResource(url, str(tmp_path)) - assert resource.canonical == canonical_url + resource = CachingStaticResource("/", tmp_path, show_index=True) app.router.register_resource(resource) app[KEY_ALLOW_CONFIGURED_CORS](resource) - resp = await mock_http_client.get(canonical_url, allow_redirects=False) - assert resp.status == 403 - - # Tested directly since aiohttp will block it before - # it gets here but we want to make sure if aiohttp ever - # changes we still block it. - with pytest.raises(HTTPForbidden): - _get_file_path(canonical_url, tmp_path) + resp = await mock_http_client.get("/") + assert resp.status == 200 + assert resp.content_type == "text/html" async def test_async_register_static_paths( From 2000db57c8470df49a53df41607cd218c0faf285 Mon Sep 17 00:00:00 2001 From: Yehazkel Date: Tue, 6 Aug 2024 17:21:34 +0300 Subject: [PATCH 2031/2411] Fix Tami4 device name is None (#123156) Co-authored-by: Robert Resch --- homeassistant/components/tami4/config_flow.py | 5 ++- tests/components/tami4/conftest.py | 25 ++++++++++++++ tests/components/tami4/test_config_flow.py | 33 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tami4/config_flow.py b/homeassistant/components/tami4/config_flow.py index 8c1edbfb60f..0fa05bbebe4 100644 --- a/homeassistant/components/tami4/config_flow.py +++ b/homeassistant/components/tami4/config_flow.py @@ -82,8 +82,11 @@ class Tami4ConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: + device_name = api.device_metadata.name + if device_name is None: + device_name = "Tami4" return self.async_create_entry( - title=api.device_metadata.name, + title=device_name, data={CONF_REFRESH_TOKEN: refresh_token}, ) diff --git a/tests/components/tami4/conftest.py b/tests/components/tami4/conftest.py index 2f4201d9a9e..2b4acac0b3f 100644 --- a/tests/components/tami4/conftest.py +++ b/tests/components/tami4/conftest.py @@ -60,6 +60,31 @@ def mock__get_devices_metadata(request: pytest.FixtureRequest) -> Generator[None yield +@pytest.fixture +def mock__get_devices_metadata_no_name( + request: pytest.FixtureRequest, +) -> Generator[None]: + """Fixture to mock _get_devices which makes a call to the API.""" + + side_effect = getattr(request, "param", None) + + device_metadata = DeviceMetadata( + id=1, + name=None, + connected=True, + psn="psn", + type="type", + device_firmware="v1.1", + ) + + with patch( + "Tami4EdgeAPI.Tami4EdgeAPI.Tami4EdgeAPI._get_devices_metadata", + return_value=[device_metadata], + side_effect=side_effect, + ): + yield + + @pytest.fixture def mock_get_device( request: pytest.FixtureRequest, diff --git a/tests/components/tami4/test_config_flow.py b/tests/components/tami4/test_config_flow.py index 4210c391d70..4dfc27bba94 100644 --- a/tests/components/tami4/test_config_flow.py +++ b/tests/components/tami4/test_config_flow.py @@ -120,6 +120,39 @@ async def test_step_otp_valid( assert "refresh_token" in result["data"] +@pytest.mark.usefixtures( + "mock_setup_entry", + "mock_request_otp", + "mock_submit_otp", + "mock__get_devices_metadata_no_name", +) +async def test_step_otp_valid_device_no_name(hass: HomeAssistant) -> None: + """Test user step with valid phone number.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PHONE: "+972555555555"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "otp" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"otp": "123456"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Tami4" + assert "refresh_token" in result["data"] + + @pytest.mark.parametrize( ("mock_submit_otp", "expected_error"), [ From f9f3c7fb51ad69340283fb8f9d8af4809c41abb9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 6 Aug 2024 16:28:37 +0200 Subject: [PATCH 2032/2411] Bump mficlient to 0.5.0 (#123250) --- homeassistant/components/mfi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/licenses.py | 3 --- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mfi/manifest.json b/homeassistant/components/mfi/manifest.json index db9cb547b28..b569009d400 100644 --- a/homeassistant/components/mfi/manifest.json +++ b/homeassistant/components/mfi/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/mfi", "iot_class": "local_polling", "loggers": ["mficlient"], - "requirements": ["mficlient==0.3.0"] + "requirements": ["mficlient==0.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index b683866f6d9..b8d8f7ec2a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1327,7 +1327,7 @@ meteoalertapi==0.3.0 meteofrance-api==1.3.0 # homeassistant.components.mfi -mficlient==0.3.0 +mficlient==0.5.0 # homeassistant.components.xiaomi_miio micloud==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a55edb474ce..721e90fd89d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1096,7 +1096,7 @@ melnor-bluetooth==0.0.25 meteofrance-api==1.3.0 # homeassistant.components.mfi -mficlient==0.3.0 +mficlient==0.5.0 # homeassistant.components.xiaomi_miio micloud==0.5 diff --git a/script/licenses.py b/script/licenses.py index e612b96794c..9bcc7b54540 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -179,9 +179,6 @@ TODO = { "aiocache": AwesomeVersion( "0.12.2" ), # https://github.com/aio-libs/aiocache/blob/master/LICENSE all rights reserved? - "mficlient": AwesomeVersion( - "0.3.0" - ), # No license https://github.com/kk7ds/mficlient/issues/4 "pyflic": AwesomeVersion("2.0.3"), # No OSI approved license CC0-1.0 Universal) } From abaac519c9ceb8a54bfd2bc9344841b6c0382673 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 17:54:40 +0200 Subject: [PATCH 2033/2411] Update frontend to 20240806.1 (#123252) --- 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 a91feb82461..de423ee9ac6 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240806.0"] + "requirements": ["home-assistant-frontend==20240806.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9622c1acac0..ca37ffbf4a8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240806.0 +home-assistant-frontend==20240806.1 home-assistant-intents==2024.7.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index b8d8f7ec2a6..e6509060764 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240806.0 +home-assistant-frontend==20240806.1 # homeassistant.components.conversation home-assistant-intents==2024.7.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 721e90fd89d..42219d16de2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -919,7 +919,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240806.0 +home-assistant-frontend==20240806.1 # homeassistant.components.conversation home-assistant-intents==2024.7.29 From a09d0117b1a225d20f08a8d7d45f1c6443103baa Mon Sep 17 00:00:00 2001 From: Yehazkel Date: Tue, 6 Aug 2024 17:21:34 +0300 Subject: [PATCH 2034/2411] Fix Tami4 device name is None (#123156) Co-authored-by: Robert Resch --- homeassistant/components/tami4/config_flow.py | 5 ++- tests/components/tami4/conftest.py | 25 ++++++++++++++ tests/components/tami4/test_config_flow.py | 33 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tami4/config_flow.py b/homeassistant/components/tami4/config_flow.py index 8c1edbfb60f..0fa05bbebe4 100644 --- a/homeassistant/components/tami4/config_flow.py +++ b/homeassistant/components/tami4/config_flow.py @@ -82,8 +82,11 @@ class Tami4ConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: + device_name = api.device_metadata.name + if device_name is None: + device_name = "Tami4" return self.async_create_entry( - title=api.device_metadata.name, + title=device_name, data={CONF_REFRESH_TOKEN: refresh_token}, ) diff --git a/tests/components/tami4/conftest.py b/tests/components/tami4/conftest.py index 2f4201d9a9e..2b4acac0b3f 100644 --- a/tests/components/tami4/conftest.py +++ b/tests/components/tami4/conftest.py @@ -60,6 +60,31 @@ def mock__get_devices_metadata(request: pytest.FixtureRequest) -> Generator[None yield +@pytest.fixture +def mock__get_devices_metadata_no_name( + request: pytest.FixtureRequest, +) -> Generator[None]: + """Fixture to mock _get_devices which makes a call to the API.""" + + side_effect = getattr(request, "param", None) + + device_metadata = DeviceMetadata( + id=1, + name=None, + connected=True, + psn="psn", + type="type", + device_firmware="v1.1", + ) + + with patch( + "Tami4EdgeAPI.Tami4EdgeAPI.Tami4EdgeAPI._get_devices_metadata", + return_value=[device_metadata], + side_effect=side_effect, + ): + yield + + @pytest.fixture def mock_get_device( request: pytest.FixtureRequest, diff --git a/tests/components/tami4/test_config_flow.py b/tests/components/tami4/test_config_flow.py index 4210c391d70..4dfc27bba94 100644 --- a/tests/components/tami4/test_config_flow.py +++ b/tests/components/tami4/test_config_flow.py @@ -120,6 +120,39 @@ async def test_step_otp_valid( assert "refresh_token" in result["data"] +@pytest.mark.usefixtures( + "mock_setup_entry", + "mock_request_otp", + "mock_submit_otp", + "mock__get_devices_metadata_no_name", +) +async def test_step_otp_valid_device_no_name(hass: HomeAssistant) -> None: + """Test user step with valid phone number.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PHONE: "+972555555555"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "otp" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"otp": "123456"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Tami4" + assert "refresh_token" in result["data"] + + @pytest.mark.parametrize( ("mock_submit_otp", "expected_error"), [ From 35a6679ae9350351da3497862b95f08d5c3bfec6 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 6 Aug 2024 14:55:14 +0200 Subject: [PATCH 2035/2411] Delete mobile_app cloudhook if not logged into the cloud (#123234) --- .../components/mobile_app/__init__.py | 18 ++++++--- tests/components/mobile_app/test_init.py | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index a8577cc596d..80893e0cbfa 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -124,12 +124,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ): await async_create_cloud_hook(hass, webhook_id, entry) - if ( - CONF_CLOUDHOOK_URL not in entry.data - and cloud.async_active_subscription(hass) - and cloud.async_is_connected(hass) - ): - await async_create_cloud_hook(hass, webhook_id, entry) + if cloud.async_is_logged_in(hass): + if ( + CONF_CLOUDHOOK_URL not in entry.data + and cloud.async_active_subscription(hass) + and cloud.async_is_connected(hass) + ): + await async_create_cloud_hook(hass, webhook_id, entry) + elif CONF_CLOUDHOOK_URL in entry.data: + # If we have a cloudhook but no longer logged in to the cloud, remove it from the entry + data = dict(entry.data) + data.pop(CONF_CLOUDHOOK_URL) + hass.config_entries.async_update_entry(entry, data=data) entry.async_on_unload(cloud.async_listen_connection_change(hass, manage_cloudhook)) diff --git a/tests/components/mobile_app/test_init.py b/tests/components/mobile_app/test_init.py index 15380a0d8d7..e1c7ed27cf9 100644 --- a/tests/components/mobile_app/test_init.py +++ b/tests/components/mobile_app/test_init.py @@ -89,6 +89,7 @@ async def _test_create_cloud_hook( "homeassistant.components.cloud.async_active_subscription", return_value=async_active_subscription_return_value, ), + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), patch("homeassistant.components.cloud.async_is_connected", return_value=True), patch( "homeassistant.components.cloud.async_get_or_create_cloudhook", @@ -187,3 +188,41 @@ async def test_create_cloud_hook_after_connection( ) await _test_create_cloud_hook(hass, hass_admin_user, {}, False, additional_steps) + + +@pytest.mark.parametrize( + ("cloud_logged_in", "should_cloudhook_exist"), + [(True, True), (False, False)], +) +async def test_delete_cloud_hook( + hass: HomeAssistant, + hass_admin_user: MockUser, + cloud_logged_in: bool, + should_cloudhook_exist: bool, +) -> None: + """Test deleting the cloud hook only when logged out of the cloud.""" + + config_entry = MockConfigEntry( + data={ + **REGISTER_CLEARTEXT, + CONF_WEBHOOK_ID: "test-webhook-id", + ATTR_DEVICE_NAME: "Test", + ATTR_DEVICE_ID: "Test", + CONF_USER_ID: hass_admin_user.id, + CONF_CLOUDHOOK_URL: "https://hook-url-already-exists", + }, + domain=DOMAIN, + title="Test", + ) + config_entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.cloud.async_is_logged_in", + return_value=cloud_logged_in, + ), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + assert (CONF_CLOUDHOOK_URL in config_entry.data) == should_cloudhook_exist From 870bb7efd4e52953ecd8d56184a152341ebd9cae Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 16:04:29 +0200 Subject: [PATCH 2036/2411] Mark FFmpeg integration as system type (#123241) --- homeassistant/components/ffmpeg/manifest.json | 1 + homeassistant/generated/integrations.json | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/ffmpeg/manifest.json b/homeassistant/components/ffmpeg/manifest.json index 8cd7b1f504d..ab9f3ed65c1 100644 --- a/homeassistant/components/ffmpeg/manifest.json +++ b/homeassistant/components/ffmpeg/manifest.json @@ -3,5 +3,6 @@ "name": "FFmpeg", "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/ffmpeg", + "integration_type": "system", "requirements": ["ha-ffmpeg==3.2.0"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 85369b238db..13009fb58be 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1798,11 +1798,6 @@ "ffmpeg": { "name": "FFmpeg", "integrations": { - "ffmpeg": { - "integration_type": "hub", - "config_flow": false, - "name": "FFmpeg" - }, "ffmpeg_motion": { "integration_type": "hub", "config_flow": false, From 7aae9d9ad3e405cf233fef401fc473ca94844bc8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Aug 2024 08:32:58 -0500 Subject: [PATCH 2037/2411] Fix sense doing blocking I/O in the event loop (#123247) --- homeassistant/components/sense/__init__.py | 12 ++++++++++-- homeassistant/components/sense/config_flow.py | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 28408c0cb7d..58e993ad6e0 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from datetime import timedelta +from functools import partial import logging from typing import Any @@ -80,8 +81,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: SenseConfigEntry) -> boo client_session = async_get_clientsession(hass) - gateway = ASyncSenseable( - api_timeout=timeout, wss_timeout=timeout, client_session=client_session + # Creating the AsyncSenseable object loads + # ssl certificates which does blocking IO + gateway = await hass.async_add_executor_job( + partial( + ASyncSenseable, + api_timeout=timeout, + wss_timeout=timeout, + client_session=client_session, + ) ) gateway.rate_limit = ACTIVE_UPDATE_RATE diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index 25c6898aec8..dab80b99e1a 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Sense integration.""" from collections.abc import Mapping +from functools import partial import logging from typing import Any @@ -48,8 +49,15 @@ class SenseConfigFlow(ConfigFlow, domain=DOMAIN): timeout = self._auth_data[CONF_TIMEOUT] client_session = async_get_clientsession(self.hass) - self._gateway = ASyncSenseable( - api_timeout=timeout, wss_timeout=timeout, client_session=client_session + # Creating the AsyncSenseable object loads + # ssl certificates which does blocking IO + self._gateway = await self.hass.async_add_executor_job( + partial( + ASyncSenseable, + api_timeout=timeout, + wss_timeout=timeout, + client_session=client_session, + ) ) self._gateway.rate_limit = ACTIVE_UPDATE_RATE await self._gateway.authenticate( From 3d0a0cf376e880528aba6532503c02b853b95738 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 6 Aug 2024 16:16:22 +0200 Subject: [PATCH 2038/2411] Bump deebot-client to 8.3.0 (#123249) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 8838eb4f50a..560ee4d599c 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==8.2.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4614d760a84..64a93d91eff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -706,7 +706,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==8.2.0 +deebot-client==8.3.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9f7b37122c..d6f0efa56ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -599,7 +599,7 @@ dbus-fast==2.22.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==8.2.0 +deebot-client==8.3.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 3cf3780587be1e8a287507375de07b421dff31df Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 6 Aug 2024 16:28:37 +0200 Subject: [PATCH 2039/2411] Bump mficlient to 0.5.0 (#123250) --- homeassistant/components/mfi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/licenses.py | 3 --- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mfi/manifest.json b/homeassistant/components/mfi/manifest.json index db9cb547b28..b569009d400 100644 --- a/homeassistant/components/mfi/manifest.json +++ b/homeassistant/components/mfi/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/mfi", "iot_class": "local_polling", "loggers": ["mficlient"], - "requirements": ["mficlient==0.3.0"] + "requirements": ["mficlient==0.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 64a93d91eff..a8670fad05e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1327,7 +1327,7 @@ meteoalertapi==0.3.0 meteofrance-api==1.3.0 # homeassistant.components.mfi -mficlient==0.3.0 +mficlient==0.5.0 # homeassistant.components.xiaomi_miio micloud==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6f0efa56ff..310e2cc9431 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1093,7 +1093,7 @@ melnor-bluetooth==0.0.25 meteofrance-api==1.3.0 # homeassistant.components.mfi -mficlient==0.3.0 +mficlient==0.5.0 # homeassistant.components.xiaomi_miio micloud==0.5 diff --git a/script/licenses.py b/script/licenses.py index 3b9ec389b08..dc89cdad9a9 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -182,9 +182,6 @@ TODO = { "asterisk_mbox": AwesomeVersion( "0.5.0" ), # No license, integration is deprecated and scheduled for removal in 2024.9.0 - "mficlient": AwesomeVersion( - "0.3.0" - ), # No license https://github.com/kk7ds/mficlient/issues/4 "pyflic": AwesomeVersion("2.0.3"), # No OSI approved license CC0-1.0 Universal) "uvcclient": AwesomeVersion( "0.11.0" From a243ed5b238deff82f84d918f591066e5f13f3d1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 17:54:40 +0200 Subject: [PATCH 2040/2411] Update frontend to 20240806.1 (#123252) --- 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 a91feb82461..de423ee9ac6 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240806.0"] + "requirements": ["home-assistant-frontend==20240806.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 96cf6d7624c..13fbcabf0d1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240806.0 +home-assistant-frontend==20240806.1 home-assistant-intents==2024.7.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index a8670fad05e..d5658b2903e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240806.0 +home-assistant-frontend==20240806.1 # homeassistant.components.conversation home-assistant-intents==2024.7.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 310e2cc9431..b37af69bfc5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -916,7 +916,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240806.0 +home-assistant-frontend==20240806.1 # homeassistant.components.conversation home-assistant-intents==2024.7.29 From b636096ac308d8739f8549af7d87dbd63a479959 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 18:08:19 +0200 Subject: [PATCH 2041/2411] Bump version to 2024.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 ed0a44bb7cc..dd181d74655 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 86fa1c8bbc1..1831a46a0af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b4" +version = "2024.8.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f4db9e09c86a1980cc67d4744181ba373dd61fe1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 7 Aug 2024 00:16:57 +0200 Subject: [PATCH 2042/2411] Bump reolink-aio to 0.9.7 (#123263) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/reolink/test_select.py | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 7289dac682c..9671a4b4fc1 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.9.6"] + "requirements": ["reolink-aio==0.9.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index e6509060764..7914459aa4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2483,7 +2483,7 @@ renault-api==0.2.5 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.6 +reolink-aio==0.9.7 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42219d16de2..5e5da74232b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1968,7 +1968,7 @@ renault-api==0.2.5 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.6 +reolink-aio==0.9.7 # homeassistant.components.rflink rflink==0.0.66 diff --git a/tests/components/reolink/test_select.py b/tests/components/reolink/test_select.py index 908c06dc16f..53c1e494b3d 100644 --- a/tests/components/reolink/test_select.py +++ b/tests/components/reolink/test_select.py @@ -110,15 +110,15 @@ async def test_chime_select( host=reolink_connect, dev_id=12345678, channel=0, - name="Test chime", - event_info={ - "md": {"switch": 0, "musicId": 0}, - "people": {"switch": 0, "musicId": 1}, - "visitor": {"switch": 1, "musicId": 2}, - }, ) + TEST_CHIME.name = "Test chime" TEST_CHIME.volume = 3 TEST_CHIME.led_state = True + TEST_CHIME.event_info = { + "md": {"switch": 0, "musicId": 0}, + "people": {"switch": 0, "musicId": 1}, + "visitor": {"switch": 1, "musicId": 2}, + } reolink_connect.chime_list = [TEST_CHIME] From 78154f5daf37f186538048585d79b529b85eb0e2 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:46:25 -0400 Subject: [PATCH 2043/2411] Bump ZHA to 0.0.28 (#123259) * Bump ZHA to 0.0.28 * Drop redundant radio schema conversion --- homeassistant/components/zha/manifest.json | 2 +- homeassistant/components/zha/radio_manager.py | 1 - requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index d7dc53b5167..4a597b0233c 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.27"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.28"], "usb": [ { "vid": "10C4", diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 2b7a65f4997..82c30b7678a 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -178,7 +178,6 @@ class ZhaRadioManager: app_config[CONF_DEVICE] = self.device_settings app_config[CONF_NWK_BACKUP_ENABLED] = False app_config[CONF_USE_THREAD] = False - app_config = self.radio_type.controller.SCHEMA(app_config) app = await self.radio_type.controller.new( app_config, auto_form=False, start_radio=False diff --git a/requirements_all.txt b/requirements_all.txt index 7914459aa4b..e054715fddc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2986,7 +2986,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.27 +zha==0.0.28 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e5da74232b..125d583c263 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2363,7 +2363,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.27 +zha==0.0.28 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 From 89337091b2e7b1d91902335de728fd6485ea1fc5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 09:04:48 +0200 Subject: [PATCH 2044/2411] Bump github/codeql-action from 3.25.15 to 3.26.0 (#123273) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7fe545e469c..68673455e1f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.1.7 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.25.15 + uses: github/codeql-action/init@v3.26.0 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.25.15 + uses: github/codeql-action/analyze@v3.26.0 with: category: "/language:python" From 95f92ababf1b63312f64ad00a1d3e52f7b412a7d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 09:05:23 +0200 Subject: [PATCH 2045/2411] Bump actions/upload-artifact from 4.3.5 to 4.3.6 (#123272) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 2 +- .github/workflows/ci.yaml | 20 ++++++++++---------- .github/workflows/wheels.yml | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 7e3597e7289..5cb860a98ef 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -69,7 +69,7 @@ jobs: run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - - name: Upload translations - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: translations path: translations.tar.gz diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8dfdb2fd7af..3df1bf5b6aa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -624,7 +624,7 @@ jobs: . venv/bin/activate pip-licenses --format=json --output-file=licenses.json - name: Upload licenses - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: licenses path: licenses.json @@ -834,7 +834,7 @@ jobs: . venv/bin/activate python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests - name: Upload pytest_buckets - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: pytest_buckets path: pytest_buckets.txt @@ -935,14 +935,14 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-full.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml @@ -1061,7 +1061,7 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -1069,7 +1069,7 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -1188,7 +1188,7 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1196,7 +1196,7 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1330,14 +1330,14 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6c9cb07a180..694208d30ac 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -82,14 +82,14 @@ jobs: ) > .env_file - name: Upload env_file - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: env_file path: ./.env_file overwrite: true - name: Upload requirements_diff - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: requirements_diff path: ./requirements_diff.txt @@ -101,7 +101,7 @@ jobs: python -m script.gen_requirements_all ci - name: Upload requirements_all_wheels - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: requirements_all_wheels path: ./requirements_all_wheels_*.txt From 7ec0b8b331ec8d55baac0ef5c84cd8a977829071 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 7 Aug 2024 09:12:20 +0200 Subject: [PATCH 2046/2411] Update knx-frontend to 2024.8.6.211307 (#123261) --- 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 37206df4c83..62364f641f4 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.0.0", "xknxproject==3.7.1", - "knx-frontend==2024.8.6.85349" + "knx-frontend==2024.8.6.211307" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index e054715fddc..daf614f2a3a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1216,7 +1216,7 @@ kiwiki-client==0.1.1 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.8.6.85349 +knx-frontend==2024.8.6.211307 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 125d583c263..78ca83b9de6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1015,7 +1015,7 @@ kegtron-ble==0.4.0 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.8.6.85349 +knx-frontend==2024.8.6.211307 # homeassistant.components.konnected konnected==1.2.0 From 185b6e5908f18f67b37ed000e1fd654ea2cffbfb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Aug 2024 02:13:23 -0500 Subject: [PATCH 2047/2411] Allow non-admins to subscribe to newer registry update events (#123267) --- homeassistant/auth/permissions/events.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/auth/permissions/events.py b/homeassistant/auth/permissions/events.py index 9f2fb45f9f0..cb0506769bf 100644 --- a/homeassistant/auth/permissions/events.py +++ b/homeassistant/auth/permissions/events.py @@ -18,9 +18,12 @@ from homeassistant.const import ( EVENT_THEMES_UPDATED, ) from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED +from homeassistant.helpers.category_registry import EVENT_CATEGORY_REGISTRY_UPDATED from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED +from homeassistant.helpers.floor_registry import EVENT_FLOOR_REGISTRY_UPDATED from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED +from homeassistant.helpers.label_registry import EVENT_LABEL_REGISTRY_UPDATED from homeassistant.util.event_type import EventType # These are events that do not contain any sensitive data @@ -41,4 +44,7 @@ SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = { EVENT_SHOPPING_LIST_UPDATED, EVENT_STATE_CHANGED, EVENT_THEMES_UPDATED, + EVENT_LABEL_REGISTRY_UPDATED, + EVENT_CATEGORY_REGISTRY_UPDATED, + EVENT_FLOOR_REGISTRY_UPDATED, } From be7f3ca439e5854234ad38a4bfd9e81d72020c7e Mon Sep 17 00:00:00 2001 From: Terence Honles Date: Wed, 7 Aug 2024 10:10:18 +0200 Subject: [PATCH 2048/2411] remove unneeded type attributes on WebsocketNotification (#123238) --- .../components/bang_olufsen/const.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/bang_olufsen/const.py b/homeassistant/components/bang_olufsen/const.py index 657bedcf4d7..748b4baf621 100644 --- a/homeassistant/components/bang_olufsen/const.py +++ b/homeassistant/components/bang_olufsen/const.py @@ -68,20 +68,20 @@ class BangOlufsenModel(StrEnum): class WebsocketNotification(StrEnum): """Enum for WebSocket notification types.""" - PLAYBACK_ERROR: Final[str] = "playback_error" - PLAYBACK_METADATA: Final[str] = "playback_metadata" - PLAYBACK_PROGRESS: Final[str] = "playback_progress" - PLAYBACK_SOURCE: Final[str] = "playback_source" - PLAYBACK_STATE: Final[str] = "playback_state" - SOFTWARE_UPDATE_STATE: Final[str] = "software_update_state" - SOURCE_CHANGE: Final[str] = "source_change" - VOLUME: Final[str] = "volume" + PLAYBACK_ERROR = "playback_error" + PLAYBACK_METADATA = "playback_metadata" + PLAYBACK_PROGRESS = "playback_progress" + PLAYBACK_SOURCE = "playback_source" + PLAYBACK_STATE = "playback_state" + SOFTWARE_UPDATE_STATE = "software_update_state" + SOURCE_CHANGE = "source_change" + VOLUME = "volume" # Sub-notifications - NOTIFICATION: Final[str] = "notification" - REMOTE_MENU_CHANGED: Final[str] = "remoteMenuChanged" + NOTIFICATION = "notification" + REMOTE_MENU_CHANGED = "remoteMenuChanged" - ALL: Final[str] = "all" + ALL = "all" DOMAIN: Final[str] = "bang_olufsen" From 27b9965b10093d2132cace438b44d8a256fa862d Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 7 Aug 2024 01:16:07 -0700 Subject: [PATCH 2049/2411] Fix Google Cloud TTS not respecting config values (#123275) --- .../components/google_cloud/helpers.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google_cloud/helpers.py b/homeassistant/components/google_cloud/helpers.py index 66dfbcf01eb..8ae6a456a4f 100644 --- a/homeassistant/components/google_cloud/helpers.py +++ b/homeassistant/components/google_cloud/helpers.py @@ -59,7 +59,10 @@ def tts_options_schema( vol.Optional( CONF_GENDER, description={"suggested_value": config_options.get(CONF_GENDER)}, - default=texttospeech.SsmlVoiceGender.NEUTRAL.name, # type: ignore[attr-defined] + default=config_options.get( + CONF_GENDER, + texttospeech.SsmlVoiceGender.NEUTRAL.name, # type: ignore[attr-defined] + ), ): vol.All( vol.Upper, SelectSelector( @@ -72,7 +75,7 @@ def tts_options_schema( vol.Optional( CONF_VOICE, description={"suggested_value": config_options.get(CONF_VOICE)}, - default=DEFAULT_VOICE, + default=config_options.get(CONF_VOICE, DEFAULT_VOICE), ): SelectSelector( SelectSelectorConfig( mode=SelectSelectorMode.DROPDOWN, @@ -82,7 +85,10 @@ def tts_options_schema( vol.Optional( CONF_ENCODING, description={"suggested_value": config_options.get(CONF_ENCODING)}, - default=texttospeech.AudioEncoding.MP3.name, # type: ignore[attr-defined] + default=config_options.get( + CONF_ENCODING, + texttospeech.AudioEncoding.MP3.name, # type: ignore[attr-defined] + ), ): vol.All( vol.Upper, SelectSelector( @@ -95,22 +101,22 @@ def tts_options_schema( vol.Optional( CONF_SPEED, description={"suggested_value": config_options.get(CONF_SPEED)}, - default=1.0, + default=config_options.get(CONF_SPEED, 1.0), ): NumberSelector(NumberSelectorConfig(min=0.25, max=4.0, step=0.01)), vol.Optional( CONF_PITCH, description={"suggested_value": config_options.get(CONF_PITCH)}, - default=0, + default=config_options.get(CONF_PITCH, 0), ): NumberSelector(NumberSelectorConfig(min=-20.0, max=20.0, step=0.1)), vol.Optional( CONF_GAIN, description={"suggested_value": config_options.get(CONF_GAIN)}, - default=0, + default=config_options.get(CONF_GAIN, 0), ): NumberSelector(NumberSelectorConfig(min=-96.0, max=16.0, step=0.1)), vol.Optional( CONF_PROFILES, description={"suggested_value": config_options.get(CONF_PROFILES)}, - default=[], + default=config_options.get(CONF_PROFILES, []), ): SelectSelector( SelectSelectorConfig( mode=SelectSelectorMode.DROPDOWN, @@ -132,7 +138,7 @@ def tts_options_schema( vol.Optional( CONF_TEXT_TYPE, description={"suggested_value": config_options.get(CONF_TEXT_TYPE)}, - default="text", + default=config_options.get(CONF_TEXT_TYPE, "text"), ): vol.All( vol.Lower, SelectSelector( From bf28419851bfd5e926721902b306d5caf779ac5a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 7 Aug 2024 20:17:01 +1200 Subject: [PATCH 2050/2411] Update ESPHome voice assistant pipeline log warning (#123269) --- homeassistant/components/esphome/manager.py | 2 +- tests/components/esphome/test_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index ef2a6862f10..4b4537d147f 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -346,7 +346,7 @@ class ESPHomeManager: ) -> int | None: """Start a voice assistant pipeline.""" if self.voice_assistant_pipeline is not None: - _LOGGER.warning("Voice assistant UDP server was not stopped") + _LOGGER.warning("Previous Voice assistant pipeline was not stopped") self.voice_assistant_pipeline.stop() self.voice_assistant_pipeline = None diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 651c52cd083..9d2a906466e 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -1229,7 +1229,7 @@ async def test_manager_voice_assistant_handlers_api( "", 0, None, None ) - assert "Voice assistant UDP server was not stopped" in caplog.text + assert "Previous Voice assistant pipeline was not stopped" in caplog.text await device.mock_voice_assistant_handle_audio(bytes(_ONE_SECOND)) From 0270026f7cb6c76841deba32afda55798ee9dc3a Mon Sep 17 00:00:00 2001 From: Steve Repsher Date: Tue, 6 Aug 2024 10:17:54 -0400 Subject: [PATCH 2051/2411] Adapt static resource handler to aiohttp 3.10 (#123166) --- homeassistant/components/http/static.py | 79 +++++++------------------ tests/components/http/test_static.py | 35 +++-------- 2 files changed, 30 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index a7280fb9b2f..29c5840a4bf 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -3,81 +3,46 @@ from __future__ import annotations from collections.abc import Mapping -import mimetypes from pathlib import Path from typing import Final -from aiohttp import hdrs +from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE from aiohttp.web import FileResponse, Request, StreamResponse -from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound +from aiohttp.web_fileresponse import CONTENT_TYPES, FALLBACK_CONTENT_TYPE from aiohttp.web_urldispatcher import StaticResource from lru import LRU -from .const import KEY_HASS - CACHE_TIME: Final = 31 * 86400 # = 1 month CACHE_HEADER = f"public, max-age={CACHE_TIME}" -CACHE_HEADERS: Mapping[str, str] = {hdrs.CACHE_CONTROL: CACHE_HEADER} -PATH_CACHE: LRU[tuple[str, Path], tuple[Path | None, str | None]] = LRU(512) - - -def _get_file_path(rel_url: str, directory: Path) -> Path | None: - """Return the path to file on disk or None.""" - 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: Path = directory.joinpath(filename).resolve() - 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 FileNotFoundError +CACHE_HEADERS: Mapping[str, str] = {CACHE_CONTROL: CACHE_HEADER} +RESPONSE_CACHE: LRU[tuple[str, Path], tuple[Path, str]] = LRU(512) class CachingStaticResource(StaticResource): """Static Resource handler that will add cache headers.""" async def _handle(self, request: Request) -> StreamResponse: - """Return requested file from disk as a FileResponse.""" + """Wrap base handler to cache file path resolution and content type guess.""" rel_url = request.match_info["filename"] key = (rel_url, self._directory) - if (filepath_content_type := PATH_CACHE.get(key)) is None: - hass = request.app[KEY_HASS] - try: - filepath = await hass.async_add_executor_job(_get_file_path, *key) - except (ValueError, FileNotFoundError) as error: - # relatively safe - raise HTTPNotFound from error - except HTTPForbidden: - # forbidden - raise - except Exception as error: - # perm error or other kind! - request.app.logger.exception("Unexpected exception") - raise HTTPNotFound from error + response: StreamResponse - content_type: str | None = None - if filepath is not None: - content_type = (mimetypes.guess_type(rel_url))[ - 0 - ] or "application/octet-stream" - PATH_CACHE[key] = (filepath, content_type) + if key in RESPONSE_CACHE: + file_path, content_type = RESPONSE_CACHE[key] + response = FileResponse(file_path, chunk_size=self._chunk_size) + response.headers[CONTENT_TYPE] = content_type else: - filepath, content_type = filepath_content_type - - if filepath and content_type: - return FileResponse( - filepath, - chunk_size=self._chunk_size, - headers={ - hdrs.CACHE_CONTROL: CACHE_HEADER, - hdrs.CONTENT_TYPE: content_type, - }, + response = await super()._handle(request) + if not isinstance(response, FileResponse): + # Must be directory index; ignore caching + return response + file_path = response._path # noqa: SLF001 + response.content_type = ( + CONTENT_TYPES.guess_type(file_path)[0] or FALLBACK_CONTENT_TYPE ) + # Cache actual header after setter construction. + content_type = response.headers[CONTENT_TYPE] + RESPONSE_CACHE[key] = (file_path, content_type) - raise HTTPForbidden if filepath is None else HTTPNotFound + response.headers[CACHE_CONTROL] = CACHE_HEADER + return response diff --git a/tests/components/http/test_static.py b/tests/components/http/test_static.py index 52a5db5daa7..2ac7c6ded93 100644 --- a/tests/components/http/test_static.py +++ b/tests/components/http/test_static.py @@ -4,11 +4,10 @@ from http import HTTPStatus from pathlib import Path from aiohttp.test_utils import TestClient -from aiohttp.web_exceptions import HTTPForbidden import pytest from homeassistant.components.http import StaticPathConfig -from homeassistant.components.http.static import CachingStaticResource, _get_file_path +from homeassistant.components.http.static import CachingStaticResource from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant from homeassistant.helpers.http import KEY_ALLOW_CONFIGURED_CORS @@ -31,37 +30,19 @@ async def mock_http_client(hass: HomeAssistant, aiohttp_client: ClientSessionGen return await aiohttp_client(hass.http.app, server_kwargs={"skip_url_asserts": True}) -@pytest.mark.parametrize( - ("url", "canonical_url"), - [ - ("//a", "//a"), - ("///a", "///a"), - ("/c:\\a\\b", "/c:%5Ca%5Cb"), - ], -) -async def test_static_path_blocks_anchors( - hass: HomeAssistant, - mock_http_client: TestClient, - tmp_path: Path, - url: str, - canonical_url: str, +async def test_static_resource_show_index( + hass: HomeAssistant, mock_http_client: TestClient, tmp_path: Path ) -> None: - """Test static paths block anchors.""" + """Test static resource will return a directory index.""" app = hass.http.app - resource = CachingStaticResource(url, str(tmp_path)) - assert resource.canonical == canonical_url + resource = CachingStaticResource("/", tmp_path, show_index=True) app.router.register_resource(resource) app[KEY_ALLOW_CONFIGURED_CORS](resource) - resp = await mock_http_client.get(canonical_url, allow_redirects=False) - assert resp.status == 403 - - # Tested directly since aiohttp will block it before - # it gets here but we want to make sure if aiohttp ever - # changes we still block it. - with pytest.raises(HTTPForbidden): - _get_file_path(canonical_url, tmp_path) + resp = await mock_http_client.get("/") + assert resp.status == 200 + assert resp.content_type == "text/html" async def test_async_register_static_paths( From 940327dccf8ad55a5bf368c5cd7ff09ab8b8a4f5 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:46:25 -0400 Subject: [PATCH 2052/2411] Bump ZHA to 0.0.28 (#123259) * Bump ZHA to 0.0.28 * Drop redundant radio schema conversion --- homeassistant/components/zha/manifest.json | 2 +- homeassistant/components/zha/radio_manager.py | 1 - requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index d7dc53b5167..4a597b0233c 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.27"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.28"], "usb": [ { "vid": "10C4", diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 2b7a65f4997..82c30b7678a 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -178,7 +178,6 @@ class ZhaRadioManager: app_config[CONF_DEVICE] = self.device_settings app_config[CONF_NWK_BACKUP_ENABLED] = False app_config[CONF_USE_THREAD] = False - app_config = self.radio_type.controller.SCHEMA(app_config) app = await self.radio_type.controller.new( app_config, auto_form=False, start_radio=False diff --git a/requirements_all.txt b/requirements_all.txt index d5658b2903e..42850c46083 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2986,7 +2986,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.27 +zha==0.0.28 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b37af69bfc5..bad3a851c88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2360,7 +2360,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.27 +zha==0.0.28 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 From 9e75b639251359a3125bbd53d9cf31a4524fd1c0 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 7 Aug 2024 09:12:20 +0200 Subject: [PATCH 2053/2411] Update knx-frontend to 2024.8.6.211307 (#123261) --- 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 37206df4c83..62364f641f4 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.0.0", "xknxproject==3.7.1", - "knx-frontend==2024.8.6.85349" + "knx-frontend==2024.8.6.211307" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 42850c46083..21270678ba6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1216,7 +1216,7 @@ kiwiki-client==0.1.1 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.8.6.85349 +knx-frontend==2024.8.6.211307 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bad3a851c88..283c84d3b01 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1012,7 +1012,7 @@ kegtron-ble==0.4.0 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.8.6.85349 +knx-frontend==2024.8.6.211307 # homeassistant.components.konnected konnected==1.2.0 From 1143efedc53ace8e28cef3875e31de1d93d39a38 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 7 Aug 2024 00:16:57 +0200 Subject: [PATCH 2054/2411] Bump reolink-aio to 0.9.7 (#123263) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/reolink/test_select.py | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 7289dac682c..9671a4b4fc1 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.9.6"] + "requirements": ["reolink-aio==0.9.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 21270678ba6..008253960d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2483,7 +2483,7 @@ renault-api==0.2.5 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.6 +reolink-aio==0.9.7 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 283c84d3b01..737d30f7b25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1965,7 +1965,7 @@ renault-api==0.2.5 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.6 +reolink-aio==0.9.7 # homeassistant.components.rflink rflink==0.0.66 diff --git a/tests/components/reolink/test_select.py b/tests/components/reolink/test_select.py index 908c06dc16f..53c1e494b3d 100644 --- a/tests/components/reolink/test_select.py +++ b/tests/components/reolink/test_select.py @@ -110,15 +110,15 @@ async def test_chime_select( host=reolink_connect, dev_id=12345678, channel=0, - name="Test chime", - event_info={ - "md": {"switch": 0, "musicId": 0}, - "people": {"switch": 0, "musicId": 1}, - "visitor": {"switch": 1, "musicId": 2}, - }, ) + TEST_CHIME.name = "Test chime" TEST_CHIME.volume = 3 TEST_CHIME.led_state = True + TEST_CHIME.event_info = { + "md": {"switch": 0, "musicId": 0}, + "people": {"switch": 0, "musicId": 1}, + "visitor": {"switch": 1, "musicId": 2}, + } reolink_connect.chime_list = [TEST_CHIME] From b0269faae4c40b69f40b33074019628068cad97b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Aug 2024 02:13:23 -0500 Subject: [PATCH 2055/2411] Allow non-admins to subscribe to newer registry update events (#123267) --- homeassistant/auth/permissions/events.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/auth/permissions/events.py b/homeassistant/auth/permissions/events.py index 9f2fb45f9f0..cb0506769bf 100644 --- a/homeassistant/auth/permissions/events.py +++ b/homeassistant/auth/permissions/events.py @@ -18,9 +18,12 @@ from homeassistant.const import ( EVENT_THEMES_UPDATED, ) from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED +from homeassistant.helpers.category_registry import EVENT_CATEGORY_REGISTRY_UPDATED from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED +from homeassistant.helpers.floor_registry import EVENT_FLOOR_REGISTRY_UPDATED from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED +from homeassistant.helpers.label_registry import EVENT_LABEL_REGISTRY_UPDATED from homeassistant.util.event_type import EventType # These are events that do not contain any sensitive data @@ -41,4 +44,7 @@ SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = { EVENT_SHOPPING_LIST_UPDATED, EVENT_STATE_CHANGED, EVENT_THEMES_UPDATED, + EVENT_LABEL_REGISTRY_UPDATED, + EVENT_CATEGORY_REGISTRY_UPDATED, + EVENT_FLOOR_REGISTRY_UPDATED, } From ad674a1c2bdb52360606c55f55a5ad44d5189e3c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 7 Aug 2024 20:17:01 +1200 Subject: [PATCH 2056/2411] Update ESPHome voice assistant pipeline log warning (#123269) --- homeassistant/components/esphome/manager.py | 2 +- tests/components/esphome/test_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index ef2a6862f10..4b4537d147f 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -346,7 +346,7 @@ class ESPHomeManager: ) -> int | None: """Start a voice assistant pipeline.""" if self.voice_assistant_pipeline is not None: - _LOGGER.warning("Voice assistant UDP server was not stopped") + _LOGGER.warning("Previous Voice assistant pipeline was not stopped") self.voice_assistant_pipeline.stop() self.voice_assistant_pipeline = None diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 651c52cd083..9d2a906466e 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -1229,7 +1229,7 @@ async def test_manager_voice_assistant_handlers_api( "", 0, None, None ) - assert "Voice assistant UDP server was not stopped" in caplog.text + assert "Previous Voice assistant pipeline was not stopped" in caplog.text await device.mock_voice_assistant_handle_audio(bytes(_ONE_SECOND)) From cc5699bf0894f107a94af74c8928db47c1ebcbd5 Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 7 Aug 2024 01:16:07 -0700 Subject: [PATCH 2057/2411] Fix Google Cloud TTS not respecting config values (#123275) --- .../components/google_cloud/helpers.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google_cloud/helpers.py b/homeassistant/components/google_cloud/helpers.py index 66dfbcf01eb..8ae6a456a4f 100644 --- a/homeassistant/components/google_cloud/helpers.py +++ b/homeassistant/components/google_cloud/helpers.py @@ -59,7 +59,10 @@ def tts_options_schema( vol.Optional( CONF_GENDER, description={"suggested_value": config_options.get(CONF_GENDER)}, - default=texttospeech.SsmlVoiceGender.NEUTRAL.name, # type: ignore[attr-defined] + default=config_options.get( + CONF_GENDER, + texttospeech.SsmlVoiceGender.NEUTRAL.name, # type: ignore[attr-defined] + ), ): vol.All( vol.Upper, SelectSelector( @@ -72,7 +75,7 @@ def tts_options_schema( vol.Optional( CONF_VOICE, description={"suggested_value": config_options.get(CONF_VOICE)}, - default=DEFAULT_VOICE, + default=config_options.get(CONF_VOICE, DEFAULT_VOICE), ): SelectSelector( SelectSelectorConfig( mode=SelectSelectorMode.DROPDOWN, @@ -82,7 +85,10 @@ def tts_options_schema( vol.Optional( CONF_ENCODING, description={"suggested_value": config_options.get(CONF_ENCODING)}, - default=texttospeech.AudioEncoding.MP3.name, # type: ignore[attr-defined] + default=config_options.get( + CONF_ENCODING, + texttospeech.AudioEncoding.MP3.name, # type: ignore[attr-defined] + ), ): vol.All( vol.Upper, SelectSelector( @@ -95,22 +101,22 @@ def tts_options_schema( vol.Optional( CONF_SPEED, description={"suggested_value": config_options.get(CONF_SPEED)}, - default=1.0, + default=config_options.get(CONF_SPEED, 1.0), ): NumberSelector(NumberSelectorConfig(min=0.25, max=4.0, step=0.01)), vol.Optional( CONF_PITCH, description={"suggested_value": config_options.get(CONF_PITCH)}, - default=0, + default=config_options.get(CONF_PITCH, 0), ): NumberSelector(NumberSelectorConfig(min=-20.0, max=20.0, step=0.1)), vol.Optional( CONF_GAIN, description={"suggested_value": config_options.get(CONF_GAIN)}, - default=0, + default=config_options.get(CONF_GAIN, 0), ): NumberSelector(NumberSelectorConfig(min=-96.0, max=16.0, step=0.1)), vol.Optional( CONF_PROFILES, description={"suggested_value": config_options.get(CONF_PROFILES)}, - default=[], + default=config_options.get(CONF_PROFILES, []), ): SelectSelector( SelectSelectorConfig( mode=SelectSelectorMode.DROPDOWN, @@ -132,7 +138,7 @@ def tts_options_schema( vol.Optional( CONF_TEXT_TYPE, description={"suggested_value": config_options.get(CONF_TEXT_TYPE)}, - default="text", + default=config_options.get(CONF_TEXT_TYPE, "text"), ): vol.All( vol.Lower, SelectSelector( From a10fed9d728392aeba80a5312dc6e5f4ae3521d8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2024 10:22:39 +0200 Subject: [PATCH 2058/2411] Bump version to 2024.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 dd181d74655..02dcc77d36a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 1831a46a0af..58922676286 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b5" +version = "2024.8.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From cba6273ac6f8ce9d3c5a13a5507b4e8e7dacd780 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 7 Aug 2024 11:18:09 +0200 Subject: [PATCH 2059/2411] Tado change repair issue (#123256) --- homeassistant/components/tado/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index ab903dafb5b..39453cb5fe1 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -152,7 +152,7 @@ "issues": { "water_heater_fallback": { "title": "Tado Water Heater entities now support fallback options", - "description": "Due to added support for water heaters entities, these entities may use different overlay. Please configure integration entity and tado app water heater zone overlay options." + "description": "Due to added support for water heaters entities, these entities may use different overlay. Please configure integration entity and tado app water heater zone overlay options. Otherwise, please configure the integration entity and Tado app water heater zone overlay options (under Settings -> Rooms & Devices -> Hot Water)." } } } From 933fba84a9a16bb75b8cebff0e8726daacc267e0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 7 Aug 2024 11:18:48 +0200 Subject: [PATCH 2060/2411] Reload conversation entries on update (#123279) --- homeassistant/components/ollama/conversation.py | 8 ++------ .../components/openai_conversation/conversation.py | 8 ++------ tests/components/ollama/conftest.py | 1 + tests/components/ollama/test_conversation.py | 1 + 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ollama/conversation.py b/homeassistant/components/ollama/conversation.py index 9f66083f506..c0423b258f0 100644 --- a/homeassistant/components/ollama/conversation.py +++ b/homeassistant/components/ollama/conversation.py @@ -346,9 +346,5 @@ class OllamaConversationEntity( self, hass: HomeAssistant, entry: ConfigEntry ) -> None: """Handle options update.""" - if entry.options.get(CONF_LLM_HASS_API): - self._attr_supported_features = ( - conversation.ConversationEntityFeature.CONTROL - ) - else: - self._attr_supported_features = conversation.ConversationEntityFeature(0) + # Reload as we update device info + entity name + supported features + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index b482126e27c..a7109a6d6ec 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -328,9 +328,5 @@ class OpenAIConversationEntity( self, hass: HomeAssistant, entry: ConfigEntry ) -> None: """Handle options update.""" - if entry.options.get(CONF_LLM_HASS_API): - self._attr_supported_features = ( - conversation.ConversationEntityFeature.CONTROL - ) - else: - self._attr_supported_features = conversation.ConversationEntityFeature(0) + # Reload as we update device info + entity name + supported features + await hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/ollama/conftest.py b/tests/components/ollama/conftest.py index 0355a13eba7..b28b8850cd5 100644 --- a/tests/components/ollama/conftest.py +++ b/tests/components/ollama/conftest.py @@ -48,6 +48,7 @@ async def mock_init_component(hass: HomeAssistant, mock_config_entry: MockConfig ): assert await async_setup_component(hass, ollama.DOMAIN, {}) await hass.async_block_till_done() + yield @pytest.fixture(autouse=True) diff --git a/tests/components/ollama/test_conversation.py b/tests/components/ollama/test_conversation.py index c83dce3b565..cb56b398342 100644 --- a/tests/components/ollama/test_conversation.py +++ b/tests/components/ollama/test_conversation.py @@ -312,6 +312,7 @@ async def test_unknown_hass_api( CONF_LLM_HASS_API: "non-existing", }, ) + await hass.async_block_till_done() result = await conversation.async_converse( hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id From 799888df2fc491dcb09c6a2ef111c88393451c5f Mon Sep 17 00:00:00 2001 From: lunmay <28674102+lunmay@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:20:36 +0200 Subject: [PATCH 2061/2411] Fix typo on one of islamic_prayer_times calculation_method option (#123281) --- homeassistant/components/islamic_prayer_times/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json index 359d4626bd4..a90031c088d 100644 --- a/homeassistant/components/islamic_prayer_times/strings.json +++ b/homeassistant/components/islamic_prayer_times/strings.json @@ -45,7 +45,7 @@ "jakim": "Jabatan Kemajuan Islam Malaysia (JAKIM)", "tunisia": "Tunisia", "algeria": "Algeria", - "kemenag": "ementerian Agama Republik Indonesia", + "kemenag": "Kementerian Agama Republik Indonesia", "morocco": "Morocco", "portugal": "Comunidade Islamica de Lisboa", "jordan": "Ministry of Awqaf, Islamic Affairs and Holy Places, Jordan", From 270990fe3907229d3aad0dd327e83d0f97a7ea2a Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 7 Aug 2024 11:18:09 +0200 Subject: [PATCH 2062/2411] Tado change repair issue (#123256) --- homeassistant/components/tado/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index ab903dafb5b..39453cb5fe1 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -152,7 +152,7 @@ "issues": { "water_heater_fallback": { "title": "Tado Water Heater entities now support fallback options", - "description": "Due to added support for water heaters entities, these entities may use different overlay. Please configure integration entity and tado app water heater zone overlay options." + "description": "Due to added support for water heaters entities, these entities may use different overlay. Please configure integration entity and tado app water heater zone overlay options. Otherwise, please configure the integration entity and Tado app water heater zone overlay options (under Settings -> Rooms & Devices -> Hot Water)." } } } From db32460f3baceb3bf914979715cabfe2999c28d3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 7 Aug 2024 11:18:48 +0200 Subject: [PATCH 2063/2411] Reload conversation entries on update (#123279) --- homeassistant/components/ollama/conversation.py | 8 ++------ .../components/openai_conversation/conversation.py | 8 ++------ tests/components/ollama/conftest.py | 1 + tests/components/ollama/test_conversation.py | 1 + 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ollama/conversation.py b/homeassistant/components/ollama/conversation.py index 9f66083f506..c0423b258f0 100644 --- a/homeassistant/components/ollama/conversation.py +++ b/homeassistant/components/ollama/conversation.py @@ -346,9 +346,5 @@ class OllamaConversationEntity( self, hass: HomeAssistant, entry: ConfigEntry ) -> None: """Handle options update.""" - if entry.options.get(CONF_LLM_HASS_API): - self._attr_supported_features = ( - conversation.ConversationEntityFeature.CONTROL - ) - else: - self._attr_supported_features = conversation.ConversationEntityFeature(0) + # Reload as we update device info + entity name + supported features + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index b482126e27c..a7109a6d6ec 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -328,9 +328,5 @@ class OpenAIConversationEntity( self, hass: HomeAssistant, entry: ConfigEntry ) -> None: """Handle options update.""" - if entry.options.get(CONF_LLM_HASS_API): - self._attr_supported_features = ( - conversation.ConversationEntityFeature.CONTROL - ) - else: - self._attr_supported_features = conversation.ConversationEntityFeature(0) + # Reload as we update device info + entity name + supported features + await hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/ollama/conftest.py b/tests/components/ollama/conftest.py index 0355a13eba7..b28b8850cd5 100644 --- a/tests/components/ollama/conftest.py +++ b/tests/components/ollama/conftest.py @@ -48,6 +48,7 @@ async def mock_init_component(hass: HomeAssistant, mock_config_entry: MockConfig ): assert await async_setup_component(hass, ollama.DOMAIN, {}) await hass.async_block_till_done() + yield @pytest.fixture(autouse=True) diff --git a/tests/components/ollama/test_conversation.py b/tests/components/ollama/test_conversation.py index c83dce3b565..cb56b398342 100644 --- a/tests/components/ollama/test_conversation.py +++ b/tests/components/ollama/test_conversation.py @@ -312,6 +312,7 @@ async def test_unknown_hass_api( CONF_LLM_HASS_API: "non-existing", }, ) + await hass.async_block_till_done() result = await conversation.async_converse( hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id From af6f78a784399e491209a8c03c09d005421bf81a Mon Sep 17 00:00:00 2001 From: lunmay <28674102+lunmay@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:20:36 +0200 Subject: [PATCH 2064/2411] Fix typo on one of islamic_prayer_times calculation_method option (#123281) --- homeassistant/components/islamic_prayer_times/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json index 359d4626bd4..a90031c088d 100644 --- a/homeassistant/components/islamic_prayer_times/strings.json +++ b/homeassistant/components/islamic_prayer_times/strings.json @@ -45,7 +45,7 @@ "jakim": "Jabatan Kemajuan Islam Malaysia (JAKIM)", "tunisia": "Tunisia", "algeria": "Algeria", - "kemenag": "ementerian Agama Republik Indonesia", + "kemenag": "Kementerian Agama Republik Indonesia", "morocco": "Morocco", "portugal": "Comunidade Islamica de Lisboa", "jordan": "Ministry of Awqaf, Islamic Affairs and Holy Places, Jordan", From 782ff12e6eadbfc10342869351565a1e9f0e09be Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2024 11:26:03 +0200 Subject: [PATCH 2065/2411] Bump version to 2024.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 02dcc77d36a..8e49ccc1b0b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 58922676286..4e2c2ebb72a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b6" +version = "2024.8.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 45ce0fed0acc913597f7f3095c11c04fced2b326 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 7 Aug 2024 12:22:50 +0200 Subject: [PATCH 2066/2411] Reload config entry for anthropic on update (#123280) * Reload config entry for anthropic on update * Fix tests --- homeassistant/components/anthropic/conversation.py | 8 ++------ tests/components/anthropic/conftest.py | 1 + 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/anthropic/conversation.py b/homeassistant/components/anthropic/conversation.py index 3d876bf3325..20e555e9592 100644 --- a/homeassistant/components/anthropic/conversation.py +++ b/homeassistant/components/anthropic/conversation.py @@ -312,9 +312,5 @@ class AnthropicConversationEntity( self, hass: HomeAssistant, entry: ConfigEntry ) -> None: """Handle options update.""" - if entry.options.get(CONF_LLM_HASS_API): - self._attr_supported_features = ( - conversation.ConversationEntityFeature.CONTROL - ) - else: - self._attr_supported_features = conversation.ConversationEntityFeature(0) + # Reload as we update device info + entity name + supported features + await hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/anthropic/conftest.py b/tests/components/anthropic/conftest.py index 0a5ad5e5ac6..fe3b20f15b8 100644 --- a/tests/components/anthropic/conftest.py +++ b/tests/components/anthropic/conftest.py @@ -43,6 +43,7 @@ async def mock_init_component(hass, mock_config_entry): ): assert await async_setup_component(hass, "anthropic", {}) await hass.async_block_till_done() + yield @pytest.fixture(autouse=True) From 764166342e97cda133e099cdb11148a09b6a0ee7 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 7 Aug 2024 21:11:03 +1000 Subject: [PATCH 2067/2411] Add missing application credential to Tesla Fleet (#123271) Co-authored-by: Franck Nijhof --- .../components/tesla_fleet/__init__.py | 10 ++++++- .../tesla_fleet/application_credentials.py | 27 ++++++++++--------- .../components/tesla_fleet/config_flow.py | 17 +++++++++++- homeassistant/components/tesla_fleet/const.py | 5 ++++ tests/components/tesla_fleet/__init__.py | 14 ++++++++++ tests/components/tesla_fleet/conftest.py | 19 ------------- .../tesla_fleet/test_config_flow.py | 5 ++-- 7 files changed, 61 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 8257bf75cd0..4eac1168674 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -13,6 +13,7 @@ from tesla_fleet_api.exceptions import ( TeslaFleetError, ) +from homeassistant.components.application_credentials import ClientCredential from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant @@ -26,7 +27,9 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo -from .const import DOMAIN, LOGGER, MODELS +from .application_credentials import TeslaOAuth2Implementation +from .config_flow import OAuth2FlowHandler +from .const import CLIENT_ID, DOMAIN, LOGGER, MODELS, NAME from .coordinator import ( TeslaFleetEnergySiteInfoCoordinator, TeslaFleetEnergySiteLiveCoordinator, @@ -51,6 +54,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - scopes = token["scp"] region = token["ou_code"].lower() + OAuth2FlowHandler.async_register_implementation( + hass, + TeslaOAuth2Implementation(hass, DOMAIN, ClientCredential(CLIENT_ID, "", NAME)), + ) + implementation = await async_get_config_entry_implementation(hass, entry) oauth_session = OAuth2Session(hass, entry, implementation) refresh_lock = asyncio.Lock() diff --git a/homeassistant/components/tesla_fleet/application_credentials.py b/homeassistant/components/tesla_fleet/application_credentials.py index fda9fce8cec..32e16cc9244 100644 --- a/homeassistant/components/tesla_fleet/application_credentials.py +++ b/homeassistant/components/tesla_fleet/application_credentials.py @@ -5,15 +5,17 @@ import hashlib import secrets from typing import Any -from homeassistant.components.application_credentials import ClientCredential +from homeassistant.components.application_credentials import ( + AuthImplementation, + AuthorizationServer, + ClientCredential, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow -from .const import DOMAIN, SCOPES +from .const import AUTHORIZE_URL, DOMAIN, SCOPES, TOKEN_URL -CLIENT_ID = "71b813eb-4a2e-483a-b831-4dec5cb9bf0d" -AUTHORIZE_URL = "https://auth.tesla.com/oauth2/v3/authorize" -TOKEN_URL = "https://auth.tesla.com/oauth2/v3/token" +AUTH_SERVER = AuthorizationServer(AUTHORIZE_URL, TOKEN_URL) async def async_get_auth_implementation( @@ -23,15 +25,16 @@ async def async_get_auth_implementation( return TeslaOAuth2Implementation( hass, DOMAIN, + credential, ) -class TeslaOAuth2Implementation(config_entry_oauth2_flow.LocalOAuth2Implementation): +class TeslaOAuth2Implementation(AuthImplementation): """Tesla Fleet API Open Source Oauth2 implementation.""" - _name = "Tesla Fleet API" - - def __init__(self, hass: HomeAssistant, domain: str) -> None: + def __init__( + self, hass: HomeAssistant, domain: str, credential: ClientCredential + ) -> None: """Initialize local auth implementation.""" self.hass = hass self._domain = domain @@ -45,10 +48,8 @@ class TeslaOAuth2Implementation(config_entry_oauth2_flow.LocalOAuth2Implementati super().__init__( hass, domain, - CLIENT_ID, - "", # Implementation has no client secret - AUTHORIZE_URL, - TOKEN_URL, + credential, + AUTH_SERVER, ) @property diff --git a/homeassistant/components/tesla_fleet/config_flow.py b/homeassistant/components/tesla_fleet/config_flow.py index ad6ba8817c9..c09ea78177f 100644 --- a/homeassistant/components/tesla_fleet/config_flow.py +++ b/homeassistant/components/tesla_fleet/config_flow.py @@ -8,10 +8,12 @@ from typing import Any import jwt +from homeassistant.components.application_credentials import ClientCredential from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.helpers import config_entry_oauth2_flow -from .const import DOMAIN, LOGGER +from .application_credentials import TeslaOAuth2Implementation +from .const import CLIENT_ID, DOMAIN, LOGGER, NAME class OAuth2FlowHandler( @@ -27,6 +29,19 @@ class OAuth2FlowHandler( """Return logger.""" return LOGGER + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow start.""" + self.async_register_implementation( + self.hass, + TeslaOAuth2Implementation( + self.hass, DOMAIN, ClientCredential(CLIENT_ID, "", NAME) + ), + ) + + return await super().async_step_user() + async def async_oauth_create_entry( self, data: dict[str, Any], diff --git a/homeassistant/components/tesla_fleet/const.py b/homeassistant/components/tesla_fleet/const.py index ae622d2266c..9d78716a13e 100644 --- a/homeassistant/components/tesla_fleet/const.py +++ b/homeassistant/components/tesla_fleet/const.py @@ -13,6 +13,11 @@ CONF_REFRESH_TOKEN = "refresh_token" LOGGER = logging.getLogger(__package__) +NAME = "Home Assistant" +CLIENT_ID = "71b813eb-4a2e-483a-b831-4dec5cb9bf0d" +AUTHORIZE_URL = "https://auth.tesla.com/oauth2/v3/authorize" +TOKEN_URL = "https://auth.tesla.com/oauth2/v3/token" + SCOPES = [ Scope.OPENID, Scope.OFFLINE_ACCESS, diff --git a/tests/components/tesla_fleet/__init__.py b/tests/components/tesla_fleet/__init__.py index d5df0d0a2ed..78159402bff 100644 --- a/tests/components/tesla_fleet/__init__.py +++ b/tests/components/tesla_fleet/__init__.py @@ -4,9 +4,15 @@ from unittest.mock import patch from syrupy import SnapshotAssertion +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.tesla_fleet.const import CLIENT_ID, DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -18,6 +24,14 @@ async def setup_platform( ) -> None: """Set up the Tesla Fleet platform.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, "", "Home Assistant"), + DOMAIN, + ) + config_entry.add_to_hass(hass) if platforms is None: diff --git a/tests/components/tesla_fleet/conftest.py b/tests/components/tesla_fleet/conftest.py index ade2f6eb0a9..7d60ae5e174 100644 --- a/tests/components/tesla_fleet/conftest.py +++ b/tests/components/tesla_fleet/conftest.py @@ -10,14 +10,7 @@ from unittest.mock import AsyncMock, patch import jwt import pytest -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) -from homeassistant.components.tesla_fleet.application_credentials import CLIENT_ID from homeassistant.components.tesla_fleet.const import DOMAIN, SCOPES -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .const import LIVE_STATUS, PRODUCTS, SITE_INFO, VEHICLE_DATA, VEHICLE_ONLINE @@ -71,18 +64,6 @@ def normal_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry: ) -@pytest.fixture(autouse=True) -async def setup_credentials(hass: HomeAssistant) -> None: - """Fixture to setup credentials.""" - assert await async_setup_component(hass, "application_credentials", {}) - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential(CLIENT_ID, ""), - DOMAIN, - ) - - @pytest.fixture(autouse=True) def mock_products() -> Generator[AsyncMock]: """Mock Tesla Fleet Api products method.""" diff --git a/tests/components/tesla_fleet/test_config_flow.py b/tests/components/tesla_fleet/test_config_flow.py index 334d8902fc7..bd1c7d7c2b8 100644 --- a/tests/components/tesla_fleet/test_config_flow.py +++ b/tests/components/tesla_fleet/test_config_flow.py @@ -5,12 +5,13 @@ from urllib.parse import parse_qs, urlparse import pytest -from homeassistant.components.tesla_fleet.application_credentials import ( +from homeassistant.components.tesla_fleet.const import ( AUTHORIZE_URL, CLIENT_ID, + DOMAIN, + SCOPES, TOKEN_URL, ) -from homeassistant.components.tesla_fleet.const import DOMAIN, SCOPES from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType From 9717a867a77c5416d8d5713664bebcae2f3386de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2024 13:12:29 +0200 Subject: [PATCH 2068/2411] Update wled to 0.20.1 (#123283) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index b19e5f16ccb..efeb414438d 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "platinum", - "requirements": ["wled==0.20.0"], + "requirements": ["wled==0.20.1"], "zeroconf": ["_wled._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index daf614f2a3a..e4c8a93b438 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2915,7 +2915,7 @@ wiffi==1.1.2 wirelesstagpy==0.8.1 # homeassistant.components.wled -wled==0.20.0 +wled==0.20.1 # homeassistant.components.wolflink wolf-comm==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 78ca83b9de6..92e91a7f9b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2301,7 +2301,7 @@ whois==0.9.27 wiffi==1.1.2 # homeassistant.components.wled -wled==0.20.0 +wled==0.20.1 # homeassistant.components.wolflink wolf-comm==0.0.9 From 6bb55ce79e2bf34edfc80ed1580e6decdb39e30c Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 7 Aug 2024 21:11:03 +1000 Subject: [PATCH 2069/2411] Add missing application credential to Tesla Fleet (#123271) Co-authored-by: Franck Nijhof --- .../components/tesla_fleet/__init__.py | 10 ++++++- .../tesla_fleet/application_credentials.py | 27 ++++++++++--------- .../components/tesla_fleet/config_flow.py | 17 +++++++++++- homeassistant/components/tesla_fleet/const.py | 5 ++++ tests/components/tesla_fleet/__init__.py | 14 ++++++++++ tests/components/tesla_fleet/conftest.py | 19 ------------- .../tesla_fleet/test_config_flow.py | 5 ++-- 7 files changed, 61 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 8257bf75cd0..4eac1168674 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -13,6 +13,7 @@ from tesla_fleet_api.exceptions import ( TeslaFleetError, ) +from homeassistant.components.application_credentials import ClientCredential from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant @@ -26,7 +27,9 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo -from .const import DOMAIN, LOGGER, MODELS +from .application_credentials import TeslaOAuth2Implementation +from .config_flow import OAuth2FlowHandler +from .const import CLIENT_ID, DOMAIN, LOGGER, MODELS, NAME from .coordinator import ( TeslaFleetEnergySiteInfoCoordinator, TeslaFleetEnergySiteLiveCoordinator, @@ -51,6 +54,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - scopes = token["scp"] region = token["ou_code"].lower() + OAuth2FlowHandler.async_register_implementation( + hass, + TeslaOAuth2Implementation(hass, DOMAIN, ClientCredential(CLIENT_ID, "", NAME)), + ) + implementation = await async_get_config_entry_implementation(hass, entry) oauth_session = OAuth2Session(hass, entry, implementation) refresh_lock = asyncio.Lock() diff --git a/homeassistant/components/tesla_fleet/application_credentials.py b/homeassistant/components/tesla_fleet/application_credentials.py index fda9fce8cec..32e16cc9244 100644 --- a/homeassistant/components/tesla_fleet/application_credentials.py +++ b/homeassistant/components/tesla_fleet/application_credentials.py @@ -5,15 +5,17 @@ import hashlib import secrets from typing import Any -from homeassistant.components.application_credentials import ClientCredential +from homeassistant.components.application_credentials import ( + AuthImplementation, + AuthorizationServer, + ClientCredential, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow -from .const import DOMAIN, SCOPES +from .const import AUTHORIZE_URL, DOMAIN, SCOPES, TOKEN_URL -CLIENT_ID = "71b813eb-4a2e-483a-b831-4dec5cb9bf0d" -AUTHORIZE_URL = "https://auth.tesla.com/oauth2/v3/authorize" -TOKEN_URL = "https://auth.tesla.com/oauth2/v3/token" +AUTH_SERVER = AuthorizationServer(AUTHORIZE_URL, TOKEN_URL) async def async_get_auth_implementation( @@ -23,15 +25,16 @@ async def async_get_auth_implementation( return TeslaOAuth2Implementation( hass, DOMAIN, + credential, ) -class TeslaOAuth2Implementation(config_entry_oauth2_flow.LocalOAuth2Implementation): +class TeslaOAuth2Implementation(AuthImplementation): """Tesla Fleet API Open Source Oauth2 implementation.""" - _name = "Tesla Fleet API" - - def __init__(self, hass: HomeAssistant, domain: str) -> None: + def __init__( + self, hass: HomeAssistant, domain: str, credential: ClientCredential + ) -> None: """Initialize local auth implementation.""" self.hass = hass self._domain = domain @@ -45,10 +48,8 @@ class TeslaOAuth2Implementation(config_entry_oauth2_flow.LocalOAuth2Implementati super().__init__( hass, domain, - CLIENT_ID, - "", # Implementation has no client secret - AUTHORIZE_URL, - TOKEN_URL, + credential, + AUTH_SERVER, ) @property diff --git a/homeassistant/components/tesla_fleet/config_flow.py b/homeassistant/components/tesla_fleet/config_flow.py index ad6ba8817c9..c09ea78177f 100644 --- a/homeassistant/components/tesla_fleet/config_flow.py +++ b/homeassistant/components/tesla_fleet/config_flow.py @@ -8,10 +8,12 @@ from typing import Any import jwt +from homeassistant.components.application_credentials import ClientCredential from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.helpers import config_entry_oauth2_flow -from .const import DOMAIN, LOGGER +from .application_credentials import TeslaOAuth2Implementation +from .const import CLIENT_ID, DOMAIN, LOGGER, NAME class OAuth2FlowHandler( @@ -27,6 +29,19 @@ class OAuth2FlowHandler( """Return logger.""" return LOGGER + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow start.""" + self.async_register_implementation( + self.hass, + TeslaOAuth2Implementation( + self.hass, DOMAIN, ClientCredential(CLIENT_ID, "", NAME) + ), + ) + + return await super().async_step_user() + async def async_oauth_create_entry( self, data: dict[str, Any], diff --git a/homeassistant/components/tesla_fleet/const.py b/homeassistant/components/tesla_fleet/const.py index ae622d2266c..9d78716a13e 100644 --- a/homeassistant/components/tesla_fleet/const.py +++ b/homeassistant/components/tesla_fleet/const.py @@ -13,6 +13,11 @@ CONF_REFRESH_TOKEN = "refresh_token" LOGGER = logging.getLogger(__package__) +NAME = "Home Assistant" +CLIENT_ID = "71b813eb-4a2e-483a-b831-4dec5cb9bf0d" +AUTHORIZE_URL = "https://auth.tesla.com/oauth2/v3/authorize" +TOKEN_URL = "https://auth.tesla.com/oauth2/v3/token" + SCOPES = [ Scope.OPENID, Scope.OFFLINE_ACCESS, diff --git a/tests/components/tesla_fleet/__init__.py b/tests/components/tesla_fleet/__init__.py index d5df0d0a2ed..78159402bff 100644 --- a/tests/components/tesla_fleet/__init__.py +++ b/tests/components/tesla_fleet/__init__.py @@ -4,9 +4,15 @@ from unittest.mock import patch from syrupy import SnapshotAssertion +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.tesla_fleet.const import CLIENT_ID, DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -18,6 +24,14 @@ async def setup_platform( ) -> None: """Set up the Tesla Fleet platform.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, "", "Home Assistant"), + DOMAIN, + ) + config_entry.add_to_hass(hass) if platforms is None: diff --git a/tests/components/tesla_fleet/conftest.py b/tests/components/tesla_fleet/conftest.py index ade2f6eb0a9..7d60ae5e174 100644 --- a/tests/components/tesla_fleet/conftest.py +++ b/tests/components/tesla_fleet/conftest.py @@ -10,14 +10,7 @@ from unittest.mock import AsyncMock, patch import jwt import pytest -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) -from homeassistant.components.tesla_fleet.application_credentials import CLIENT_ID from homeassistant.components.tesla_fleet.const import DOMAIN, SCOPES -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .const import LIVE_STATUS, PRODUCTS, SITE_INFO, VEHICLE_DATA, VEHICLE_ONLINE @@ -71,18 +64,6 @@ def normal_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry: ) -@pytest.fixture(autouse=True) -async def setup_credentials(hass: HomeAssistant) -> None: - """Fixture to setup credentials.""" - assert await async_setup_component(hass, "application_credentials", {}) - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential(CLIENT_ID, ""), - DOMAIN, - ) - - @pytest.fixture(autouse=True) def mock_products() -> Generator[AsyncMock]: """Mock Tesla Fleet Api products method.""" diff --git a/tests/components/tesla_fleet/test_config_flow.py b/tests/components/tesla_fleet/test_config_flow.py index 334d8902fc7..bd1c7d7c2b8 100644 --- a/tests/components/tesla_fleet/test_config_flow.py +++ b/tests/components/tesla_fleet/test_config_flow.py @@ -5,12 +5,13 @@ from urllib.parse import parse_qs, urlparse import pytest -from homeassistant.components.tesla_fleet.application_credentials import ( +from homeassistant.components.tesla_fleet.const import ( AUTHORIZE_URL, CLIENT_ID, + DOMAIN, + SCOPES, TOKEN_URL, ) -from homeassistant.components.tesla_fleet.const import DOMAIN, SCOPES from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType From 4a212791a26a8f0d5a38aff399a58fb7cad53fef Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2024 13:12:29 +0200 Subject: [PATCH 2070/2411] Update wled to 0.20.1 (#123283) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index b19e5f16ccb..efeb414438d 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "platinum", - "requirements": ["wled==0.20.0"], + "requirements": ["wled==0.20.1"], "zeroconf": ["_wled._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 008253960d5..b12da4becd3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2915,7 +2915,7 @@ wiffi==1.1.2 wirelesstagpy==0.8.1 # homeassistant.components.wled -wled==0.20.0 +wled==0.20.1 # homeassistant.components.wolflink wolf-comm==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 737d30f7b25..7ec92ee20e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2298,7 +2298,7 @@ whois==0.9.27 wiffi==1.1.2 # homeassistant.components.wled -wled==0.20.0 +wled==0.20.1 # homeassistant.components.wolflink wolf-comm==0.0.9 From 082290b092985dcb4680655090f071692b911751 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2024 13:15:23 +0200 Subject: [PATCH 2071/2411] Bump version to 2024.8.0b8 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8e49ccc1b0b..987ee6572b6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 4e2c2ebb72a..29c58954c5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b7" +version = "2024.8.0b8" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 4cd3fce55556f9491de313613ccbf26c672433ec Mon Sep 17 00:00:00 2001 From: ashalita Date: Wed, 7 Aug 2024 15:19:05 +0300 Subject: [PATCH 2072/2411] Revert "Upgrade pycoolmasternet-async to 0.2.0" (#123286) --- homeassistant/components/coolmaster/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index b405a82ad62..9488e068d44 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/coolmaster", "iot_class": "local_polling", "loggers": ["pycoolmasternet_async"], - "requirements": ["pycoolmasternet-async==0.2.0"] + "requirements": ["pycoolmasternet-async==0.1.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index e4c8a93b438..30dbe77f5ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1777,7 +1777,7 @@ pycmus==0.1.1 pycomfoconnect==0.5.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.2.0 +pycoolmasternet-async==0.1.5 # homeassistant.components.microsoft pycsspeechtts==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 92e91a7f9b2..3b92a57da5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1430,7 +1430,7 @@ pycfdns==3.0.0 pycomfoconnect==0.5.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.2.0 +pycoolmasternet-async==0.1.5 # homeassistant.components.microsoft pycsspeechtts==1.0.8 From f1029596d2ce66ad075aeefe74ebb4f2752cc0b3 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 7 Aug 2024 16:45:46 +0200 Subject: [PATCH 2073/2411] Drop Matter Microwave Oven Mode select entity (#123294) --- homeassistant/components/matter/select.py | 13 --------- tests/components/matter/test_select.py | 32 ----------------------- 2 files changed, 45 deletions(-) diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py index 350712061ba..4a9ef3780d1 100644 --- a/homeassistant/components/matter/select.py +++ b/homeassistant/components/matter/select.py @@ -27,7 +27,6 @@ type SelectCluster = ( | clusters.RvcRunMode | clusters.RvcCleanMode | clusters.DishwasherMode - | clusters.MicrowaveOvenMode | clusters.EnergyEvseMode | clusters.DeviceEnergyManagementMode ) @@ -199,18 +198,6 @@ DISCOVERY_SCHEMAS = [ clusters.DishwasherMode.Attributes.SupportedModes, ), ), - MatterDiscoverySchema( - platform=Platform.SELECT, - entity_description=MatterSelectEntityDescription( - key="MatterMicrowaveOvenMode", - translation_key="mode", - ), - entity_class=MatterModeSelectEntity, - required_attributes=( - clusters.MicrowaveOvenMode.Attributes.CurrentMode, - clusters.MicrowaveOvenMode.Attributes.SupportedModes, - ), - ), MatterDiscoverySchema( platform=Platform.SELECT, entity_description=MatterSelectEntityDescription( diff --git a/tests/components/matter/test_select.py b/tests/components/matter/test_select.py index 9b774f0430b..f84e5870392 100644 --- a/tests/components/matter/test_select.py +++ b/tests/components/matter/test_select.py @@ -25,16 +25,6 @@ async def dimmable_light_node_fixture( ) -@pytest.fixture(name="microwave_oven_node") -async def microwave_oven_node_fixture( - hass: HomeAssistant, matter_client: MagicMock -) -> MatterNode: - """Fixture for a microwave oven node.""" - return await setup_integration_with_node_fixture( - hass, "microwave-oven", matter_client - ) - - # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_mode_select_entities( @@ -87,28 +77,6 @@ async def test_mode_select_entities( # This tests needs to be adjusted to remove lingering tasks -@pytest.mark.parametrize("expected_lingering_tasks", [True]) -async def test_microwave_select_entities( - hass: HomeAssistant, - matter_client: MagicMock, - microwave_oven_node: MatterNode, -) -> None: - """Test select entities are created for the MicrowaveOvenMode cluster attributes.""" - state = hass.states.get("select.microwave_oven_mode") - assert state - assert state.state == "Normal" - assert state.attributes["options"] == [ - "Normal", - "Defrost", - ] - # name should just be Mode (from the translation key) - assert state.attributes["friendly_name"] == "Microwave Oven Mode" - set_node_attribute(microwave_oven_node, 1, 94, 1, 1) - await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("select.microwave_oven_mode") - assert state.state == "Defrost" - - @pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_attribute_select_entities( hass: HomeAssistant, From f8fa6e43094c7288fcf1cb9fe6a260d71c4f3e1b Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 7 Aug 2024 10:42:59 -0500 Subject: [PATCH 2074/2411] Bump intents to 2024.8.7 (#123295) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 65c79cef187..d7a308b8b2b 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.7.29"] + "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.8.7"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ca37ffbf4a8..10823557b94 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240806.1 -home-assistant-intents==2024.7.29 +home-assistant-intents==2024.8.7 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index 30dbe77f5ff..6a0c68b8e25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1096,7 +1096,7 @@ holidays==0.53 home-assistant-frontend==20240806.1 # homeassistant.components.conversation -home-assistant-intents==2024.7.29 +home-assistant-intents==2024.8.7 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b92a57da5d..21e9895187d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -922,7 +922,7 @@ holidays==0.53 home-assistant-frontend==20240806.1 # homeassistant.components.conversation -home-assistant-intents==2024.7.29 +home-assistant-intents==2024.8.7 # homeassistant.components.home_connect homeconnect==0.8.0 From ef564c537d2e3a23ee433ec8a96315038baf9a9f Mon Sep 17 00:00:00 2001 From: ashalita Date: Wed, 7 Aug 2024 15:19:05 +0300 Subject: [PATCH 2075/2411] Revert "Upgrade pycoolmasternet-async to 0.2.0" (#123286) --- homeassistant/components/coolmaster/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index b405a82ad62..9488e068d44 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/coolmaster", "iot_class": "local_polling", "loggers": ["pycoolmasternet_async"], - "requirements": ["pycoolmasternet-async==0.2.0"] + "requirements": ["pycoolmasternet-async==0.1.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index b12da4becd3..cf96a4363e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1777,7 +1777,7 @@ pycmus==0.1.1 pycomfoconnect==0.5.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.2.0 +pycoolmasternet-async==0.1.5 # homeassistant.components.microsoft pycsspeechtts==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ec92ee20e7..8c215eedcea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1427,7 +1427,7 @@ pycfdns==3.0.0 pycomfoconnect==0.5.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.2.0 +pycoolmasternet-async==0.1.5 # homeassistant.components.microsoft pycsspeechtts==1.0.8 From 7a51d4ff62eaf3408662e76fe62de52d3e966ad9 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 7 Aug 2024 16:45:46 +0200 Subject: [PATCH 2076/2411] Drop Matter Microwave Oven Mode select entity (#123294) --- homeassistant/components/matter/select.py | 13 --------- tests/components/matter/test_select.py | 32 ----------------------- 2 files changed, 45 deletions(-) diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py index 350712061ba..4a9ef3780d1 100644 --- a/homeassistant/components/matter/select.py +++ b/homeassistant/components/matter/select.py @@ -27,7 +27,6 @@ type SelectCluster = ( | clusters.RvcRunMode | clusters.RvcCleanMode | clusters.DishwasherMode - | clusters.MicrowaveOvenMode | clusters.EnergyEvseMode | clusters.DeviceEnergyManagementMode ) @@ -199,18 +198,6 @@ DISCOVERY_SCHEMAS = [ clusters.DishwasherMode.Attributes.SupportedModes, ), ), - MatterDiscoverySchema( - platform=Platform.SELECT, - entity_description=MatterSelectEntityDescription( - key="MatterMicrowaveOvenMode", - translation_key="mode", - ), - entity_class=MatterModeSelectEntity, - required_attributes=( - clusters.MicrowaveOvenMode.Attributes.CurrentMode, - clusters.MicrowaveOvenMode.Attributes.SupportedModes, - ), - ), MatterDiscoverySchema( platform=Platform.SELECT, entity_description=MatterSelectEntityDescription( diff --git a/tests/components/matter/test_select.py b/tests/components/matter/test_select.py index 9b774f0430b..f84e5870392 100644 --- a/tests/components/matter/test_select.py +++ b/tests/components/matter/test_select.py @@ -25,16 +25,6 @@ async def dimmable_light_node_fixture( ) -@pytest.fixture(name="microwave_oven_node") -async def microwave_oven_node_fixture( - hass: HomeAssistant, matter_client: MagicMock -) -> MatterNode: - """Fixture for a microwave oven node.""" - return await setup_integration_with_node_fixture( - hass, "microwave-oven", matter_client - ) - - # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_mode_select_entities( @@ -87,28 +77,6 @@ async def test_mode_select_entities( # This tests needs to be adjusted to remove lingering tasks -@pytest.mark.parametrize("expected_lingering_tasks", [True]) -async def test_microwave_select_entities( - hass: HomeAssistant, - matter_client: MagicMock, - microwave_oven_node: MatterNode, -) -> None: - """Test select entities are created for the MicrowaveOvenMode cluster attributes.""" - state = hass.states.get("select.microwave_oven_mode") - assert state - assert state.state == "Normal" - assert state.attributes["options"] == [ - "Normal", - "Defrost", - ] - # name should just be Mode (from the translation key) - assert state.attributes["friendly_name"] == "Microwave Oven Mode" - set_node_attribute(microwave_oven_node, 1, 94, 1, 1) - await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("select.microwave_oven_mode") - assert state.state == "Defrost" - - @pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_attribute_select_entities( hass: HomeAssistant, From 5367886732a5af1d5b07d4975480394612bc3f12 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 7 Aug 2024 10:42:59 -0500 Subject: [PATCH 2077/2411] Bump intents to 2024.8.7 (#123295) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 65c79cef187..d7a308b8b2b 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.7.29"] + "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.8.7"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 13fbcabf0d1..472134fea37 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240806.1 -home-assistant-intents==2024.7.29 +home-assistant-intents==2024.8.7 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index cf96a4363e5..b8f50d328f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1096,7 +1096,7 @@ holidays==0.53 home-assistant-frontend==20240806.1 # homeassistant.components.conversation -home-assistant-intents==2024.7.29 +home-assistant-intents==2024.8.7 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c215eedcea..f6602bf082b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -919,7 +919,7 @@ holidays==0.53 home-assistant-frontend==20240806.1 # homeassistant.components.conversation -home-assistant-intents==2024.7.29 +home-assistant-intents==2024.8.7 # homeassistant.components.home_connect homeconnect==0.8.0 From ac6abb363c98ce5aef70c7b7eed38e4bcf787432 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2024 18:24:15 +0200 Subject: [PATCH 2078/2411] Bump version to 2024.8.0b9 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 987ee6572b6..6c63a980c5f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 29c58954c5b..a4e95216896 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b8" +version = "2024.8.0b9" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From be4810731a76c17a2c8206eb2e1bc90ec13b05cc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2024 19:04:33 +0200 Subject: [PATCH 2079/2411] Bump version to 2024.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 6c63a980c5f..402f57a4f8b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b9" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index a4e95216896..dc943b0832a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b9" +version = "2024.8.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2144a9a7b22e8f495bd15eac2d84d1e0fb95b82c Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Thu, 8 Aug 2024 02:56:02 -0400 Subject: [PATCH 2080/2411] Bump aiorussound to 2.2.2 (#123319) --- homeassistant/components/russound_rio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index be5dd86793f..e7bb99010ee 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/russound_rio", "iot_class": "local_push", "loggers": ["aiorussound"], - "requirements": ["aiorussound==2.2.0"] + "requirements": ["aiorussound==2.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6a0c68b8e25..51d321b9a19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.2.0 +aiorussound==2.2.2 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21e9895187d..1d69137d94c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -332,7 +332,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.2.0 +aiorussound==2.2.2 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 From 135c80186fe5f9348eb64a43c16e4876aebdc568 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Aug 2024 01:56:40 -0500 Subject: [PATCH 2081/2411] Fix doorbird with externally added events (#123313) --- homeassistant/components/doorbird/device.py | 2 +- tests/components/doorbird/fixtures/favorites.json | 4 ++++ tests/components/doorbird/test_button.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird/device.py b/homeassistant/components/doorbird/device.py index 7cd45487464..adcb441f458 100644 --- a/homeassistant/components/doorbird/device.py +++ b/homeassistant/components/doorbird/device.py @@ -195,7 +195,7 @@ class ConfiguredDoorBird: title: str | None = data.get("title") if not title or not title.startswith("Home Assistant"): continue - event = title.split("(")[1].strip(")") + event = title.partition("(")[2].strip(")") if input_type := favorite_input_type.get(identifier): events.append(DoorbirdEvent(event, input_type)) elif input_type := default_event_types.get(event): diff --git a/tests/components/doorbird/fixtures/favorites.json b/tests/components/doorbird/fixtures/favorites.json index c56f79c0300..50dddb850a5 100644 --- a/tests/components/doorbird/fixtures/favorites.json +++ b/tests/components/doorbird/fixtures/favorites.json @@ -7,6 +7,10 @@ "1": { "title": "Home Assistant (mydoorbird_motion)", "value": "http://127.0.0.1:8123/api/doorbird/mydoorbird_motion?token=01J2F4B97Y7P1SARXEJ6W07EKD" + }, + "2": { + "title": "externally added event", + "value": "http://127.0.0.1/" } } } diff --git a/tests/components/doorbird/test_button.py b/tests/components/doorbird/test_button.py index 2131e3d6133..cb4bab656ee 100644 --- a/tests/components/doorbird/test_button.py +++ b/tests/components/doorbird/test_button.py @@ -49,4 +49,4 @@ async def test_reset_favorites_button( DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: reset_entity_id}, blocking=True ) assert hass.states.get(reset_entity_id).state != STATE_UNKNOWN - assert doorbird_entry.api.delete_favorite.call_count == 2 + assert doorbird_entry.api.delete_favorite.call_count == 3 From 7dea5d2fe6911c2ed516b1d752e9ef83aa11b1a4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 8 Aug 2024 08:59:30 +0200 Subject: [PATCH 2082/2411] Refactor spc tests (#123287) --- homeassistant/components/spc/__init__.py | 4 +- tests/components/spc/conftest.py | 26 ++++++++ .../spc/test_alarm_control_panel.py | 34 ++++++++++ tests/components/spc/test_init.py | 65 ++----------------- 4 files changed, 69 insertions(+), 60 deletions(-) create mode 100644 tests/components/spc/conftest.py create mode 100644 tests/components/spc/test_alarm_control_panel.py diff --git a/homeassistant/components/spc/__init__.py b/homeassistant/components/spc/__init__.py index bb025d699fc..3d9467f2041 100644 --- a/homeassistant/components/spc/__init__.py +++ b/homeassistant/components/spc/__init__.py @@ -41,7 +41,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the SPC component.""" - async def async_upate_callback(spc_object): + async def async_update_callback(spc_object): if isinstance(spc_object, Area): async_dispatcher_send(hass, SIGNAL_UPDATE_ALARM.format(spc_object.id)) elif isinstance(spc_object, Zone): @@ -54,7 +54,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: session=session, api_url=config[DOMAIN].get(CONF_API_URL), ws_url=config[DOMAIN].get(CONF_WS_URL), - async_callback=async_upate_callback, + async_callback=async_update_callback, ) hass.data[DATA_API] = spc diff --git a/tests/components/spc/conftest.py b/tests/components/spc/conftest.py new file mode 100644 index 00000000000..1ccda31e314 --- /dev/null +++ b/tests/components/spc/conftest.py @@ -0,0 +1,26 @@ +"""Tests for Vanderbilt SPC component.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pyspcwebgw +import pytest + + +@pytest.fixture +def mock_client() -> Generator[AsyncMock]: + """Mock the SPC client.""" + + with patch( + "homeassistant.components.spc.SpcWebGateway", autospec=True + ) as mock_client: + client = mock_client.return_value + client.async_load_parameters.return_value = True + mock_area = AsyncMock(spec=pyspcwebgw.area.Area) + mock_area.id = "1" + mock_area.mode = pyspcwebgw.const.AreaMode.FULL_SET + mock_area.last_changed_by = "Sven" + mock_area.name = "House" + mock_area.verified_alarm = False + client.areas = {"1": mock_area} + yield mock_client diff --git a/tests/components/spc/test_alarm_control_panel.py b/tests/components/spc/test_alarm_control_panel.py new file mode 100644 index 00000000000..7b1ab4ff947 --- /dev/null +++ b/tests/components/spc/test_alarm_control_panel.py @@ -0,0 +1,34 @@ +"""Tests for Vanderbilt SPC component.""" + +from unittest.mock import AsyncMock + +from pyspcwebgw.const import AreaMode + +from homeassistant.const import STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_update_alarm_device(hass: HomeAssistant, mock_client: AsyncMock) -> None: + """Test that alarm panel state changes on incoming websocket data.""" + + config = {"spc": {"api_url": "http://localhost/", "ws_url": "ws://localhost/"}} + assert await async_setup_component(hass, "spc", config) is True + + await hass.async_block_till_done() + + entity_id = "alarm_control_panel.house" + + assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY + assert hass.states.get(entity_id).attributes["changed_by"] == "Sven" + + mock_area = mock_client.return_value.areas["1"] + + mock_area.mode = AreaMode.UNSET + mock_area.last_changed_by = "Anna" + + await mock_client.call_args_list[0][1]["async_callback"](mock_area) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED + assert hass.states.get(entity_id).attributes["changed_by"] == "Anna" diff --git a/tests/components/spc/test_init.py b/tests/components/spc/test_init.py index 4f335e2f980..dc407dc2c5b 100644 --- a/tests/components/spc/test_init.py +++ b/tests/components/spc/test_init.py @@ -1,73 +1,22 @@ """Tests for Vanderbilt SPC component.""" -from unittest.mock import Mock, PropertyMock, patch +from unittest.mock import AsyncMock -import pyspcwebgw -from pyspcwebgw.const import AreaMode - -from homeassistant.components.spc import DATA_API -from homeassistant.const import STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -async def test_valid_device_config(hass: HomeAssistant) -> None: +async def test_valid_device_config(hass: HomeAssistant, mock_client: AsyncMock) -> None: """Test valid device config.""" config = {"spc": {"api_url": "http://localhost/", "ws_url": "ws://localhost/"}} - with patch( - "homeassistant.components.spc.SpcWebGateway.async_load_parameters", - return_value=True, - ): - assert await async_setup_component(hass, "spc", config) is True + assert await async_setup_component(hass, "spc", config) is True -async def test_invalid_device_config(hass: HomeAssistant) -> None: +async def test_invalid_device_config( + hass: HomeAssistant, mock_client: AsyncMock +) -> None: """Test valid device config.""" config = {"spc": {"api_url": "http://localhost/"}} - with patch( - "homeassistant.components.spc.SpcWebGateway.async_load_parameters", - return_value=True, - ): - assert await async_setup_component(hass, "spc", config) is False - - -async def test_update_alarm_device(hass: HomeAssistant) -> None: - """Test that alarm panel state changes on incoming websocket data.""" - - config = {"spc": {"api_url": "http://localhost/", "ws_url": "ws://localhost/"}} - - area_mock = Mock( - spec=pyspcwebgw.area.Area, - id="1", - mode=AreaMode.FULL_SET, - last_changed_by="Sven", - ) - area_mock.name = "House" - area_mock.verified_alarm = False - - with patch( - "homeassistant.components.spc.SpcWebGateway.areas", new_callable=PropertyMock - ) as mock_areas: - mock_areas.return_value = {"1": area_mock} - with patch( - "homeassistant.components.spc.SpcWebGateway.async_load_parameters", - return_value=True, - ): - assert await async_setup_component(hass, "spc", config) is True - - await hass.async_block_till_done() - - entity_id = "alarm_control_panel.house" - - assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY - assert hass.states.get(entity_id).attributes["changed_by"] == "Sven" - - area_mock.mode = AreaMode.UNSET - area_mock.last_changed_by = "Anna" - await hass.data[DATA_API]._async_callback(area_mock) - await hass.async_block_till_done() - - assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED - assert hass.states.get(entity_id).attributes["changed_by"] == "Anna" + assert await async_setup_component(hass, "spc", config) is False From b7f5236a0a630aea594f1b2f9558aff908a99555 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:09:36 +0200 Subject: [PATCH 2083/2411] Fix implicit-return in konnected (#122915) * Fix implicit-return in konnected * Adjust * Adjust * Adjust tests --- homeassistant/components/konnected/switch.py | 5 ++--- tests/components/konnected/test_panel.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py index 424a2d9164d..65b99d623f1 100644 --- a/homeassistant/components/konnected/switch.py +++ b/homeassistant/components/konnected/switch.py @@ -102,13 +102,12 @@ class KonnectedSwitch(SwitchEntity): if resp.get(ATTR_STATE) is not None: self._set_state(self._boolean_state(resp.get(ATTR_STATE))) - def _boolean_state(self, int_state): - if int_state is None: - return False + def _boolean_state(self, int_state: int | None) -> bool | None: if int_state == 0: return self._activation == STATE_LOW if int_state == 1: return self._activation == STATE_HIGH + return None def _set_state(self, state): self._attr_is_on = state diff --git a/tests/components/konnected/test_panel.py b/tests/components/konnected/test_panel.py index 64cc414cdd3..48ebea64161 100644 --- a/tests/components/konnected/test_panel.py +++ b/tests/components/konnected/test_panel.py @@ -700,4 +700,4 @@ async def test_connect_retry(hass: HomeAssistant, mock_panel) -> None: async_fire_time_changed(hass, utcnow() + timedelta(seconds=21)) await hass.async_block_till_done() await async_update_entity(hass, "switch.konnected_445566_actuator_6") - assert hass.states.get("switch.konnected_445566_actuator_6").state == "off" + assert hass.states.get("switch.konnected_445566_actuator_6").state == "unknown" From 984bbf60ef9c1b14832f3f5ac4e3fb946371c09a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:25:32 +0200 Subject: [PATCH 2084/2411] Bump sigstore/cosign-installer from 3.5.0 to 3.6.0 (#123335) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .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 5cb860a98ef..522095ad041 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -323,7 +323,7 @@ jobs: uses: actions/checkout@v4.1.7 - name: Install Cosign - uses: sigstore/cosign-installer@v3.5.0 + uses: sigstore/cosign-installer@v3.6.0 with: cosign-release: "v2.2.3" From 8a8fac46e0206d2f58496569cee873cfeab8b9dd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 8 Aug 2024 11:32:22 +0200 Subject: [PATCH 2085/2411] Remove unneeded logs from Yamaha (#123349) --- homeassistant/components/yamaha/media_player.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index a8200ea3373..f6434616cfa 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -238,19 +238,6 @@ class YamahaDeviceZone(MediaPlayerEntity): # the default name of the integration may not be changed # to avoid a breaking change. self._attr_unique_id = f"{self.zctrl.serial_number}_{self._zone}" - _LOGGER.debug( - "Receiver zone: %s zone %s uid %s", - self._name, - self._zone, - self._attr_unique_id, - ) - else: - _LOGGER.info( - "Receiver zone: %s zone %s no uid %s", - self._name, - self._zone, - self._attr_unique_id, - ) def update(self) -> None: """Get the latest details from the device.""" From 02a404081a2820302dd2333f89b5eec40a88b2f7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:33:49 +0200 Subject: [PATCH 2086/2411] Fix implicit-return in yeelight (#122943) --- homeassistant/components/yeelight/scanner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 6ca12e9bd01..ac482504880 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -67,7 +67,8 @@ class YeelightScanner: async def async_setup(self) -> None: """Set up the scanner.""" if self._setup_future is not None: - return await self._setup_future + await self._setup_future + return self._setup_future = self._hass.loop.create_future() connected_futures: list[asyncio.Future[None]] = [] From d08f4fbace01f528dbebb732d02fc8201c5fdbf0 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:56:18 +0200 Subject: [PATCH 2087/2411] Enable Ruff RET501 (#115031) * Enable Ruff RET501 * add noqa and type hints * use Any for fixtures * Review comments, typing fixes * Review comments * fix new occurrences, clean up * Fix typing * clean up rebase * more cleanup * Remove old occurrences --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a0a98bc72be..9e4fe36243b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -822,7 +822,6 @@ ignore = [ # temporarily disabled "RET503", - "RET501", "TRY301" ] From baceb2a92ac6e0cb70639195c3ea205a378551ac Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Thu, 8 Aug 2024 11:26:03 +0100 Subject: [PATCH 2088/2411] Add support for v3 Coinbase API (#116345) * Add support for v3 Coinbase API * Add deps * Move tests --- homeassistant/components/coinbase/__init__.py | 103 ++++++++++++++---- .../components/coinbase/config_flow.py | 45 +++++--- homeassistant/components/coinbase/const.py | 11 +- .../components/coinbase/manifest.json | 2 +- homeassistant/components/coinbase/sensor.py | 69 +++++++----- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/coinbase/common.py | 68 +++++++++++- tests/components/coinbase/const.py | 28 +++++ .../coinbase/snapshots/test_diagnostics.ambr | 33 ++---- tests/components/coinbase/test_config_flow.py | 90 ++++++++++++++- 11 files changed, 359 insertions(+), 96 deletions(-) diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index a231bb5cda0..f5fd8fa1dc3 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -5,7 +5,9 @@ from __future__ import annotations from datetime import timedelta import logging -from coinbase.wallet.client import Client +from coinbase.rest import RESTClient +from coinbase.rest.rest_base import HTTPError +from coinbase.wallet.client import Client as LegacyClient from coinbase.wallet.error import AuthenticationError from homeassistant.config_entries import ConfigEntry @@ -15,8 +17,23 @@ from homeassistant.helpers import entity_registry as er from homeassistant.util import Throttle from .const import ( + ACCOUNT_IS_VAULT, + API_ACCOUNT_AMOUNT, + API_ACCOUNT_AVALIABLE, + API_ACCOUNT_BALANCE, + API_ACCOUNT_CURRENCY, + API_ACCOUNT_CURRENCY_CODE, + API_ACCOUNT_HOLD, API_ACCOUNT_ID, - API_ACCOUNTS_DATA, + API_ACCOUNT_NAME, + API_ACCOUNT_VALUE, + API_ACCOUNTS, + API_DATA, + API_RATES_CURRENCY, + API_RESOURCE_TYPE, + API_TYPE_VAULT, + API_V3_ACCOUNT_ID, + API_V3_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_BASE, CONF_EXCHANGE_RATES, @@ -56,9 +73,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def create_and_update_instance(entry: ConfigEntry) -> CoinbaseData: """Create and update a Coinbase Data instance.""" - client = Client(entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN]) + if "organizations" not in entry.data[CONF_API_KEY]: + client = LegacyClient(entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN]) + version = "v2" + else: + client = RESTClient( + api_key=entry.data[CONF_API_KEY], api_secret=entry.data[CONF_API_TOKEN] + ) + version = "v3" base_rate = entry.options.get(CONF_EXCHANGE_BASE, "USD") - instance = CoinbaseData(client, base_rate) + instance = CoinbaseData(client, base_rate, version) instance.update() return instance @@ -83,42 +107,83 @@ async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> Non registry.async_remove(entity.entity_id) -def get_accounts(client): +def get_accounts(client, version): """Handle paginated accounts.""" response = client.get_accounts() - accounts = response[API_ACCOUNTS_DATA] - next_starting_after = response.pagination.next_starting_after - - while next_starting_after: - response = client.get_accounts(starting_after=next_starting_after) - accounts += response[API_ACCOUNTS_DATA] + if version == "v2": + accounts = response[API_DATA] next_starting_after = response.pagination.next_starting_after - return accounts + while next_starting_after: + response = client.get_accounts(starting_after=next_starting_after) + accounts += response[API_DATA] + next_starting_after = response.pagination.next_starting_after + + return [ + { + API_ACCOUNT_ID: account[API_ACCOUNT_ID], + API_ACCOUNT_NAME: account[API_ACCOUNT_NAME], + API_ACCOUNT_CURRENCY: account[API_ACCOUNT_CURRENCY][ + API_ACCOUNT_CURRENCY_CODE + ], + API_ACCOUNT_AMOUNT: account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT], + ACCOUNT_IS_VAULT: account[API_RESOURCE_TYPE] == API_TYPE_VAULT, + } + for account in accounts + ] + + accounts = response[API_ACCOUNTS] + while response["has_next"]: + response = client.get_accounts(cursor=response["cursor"]) + accounts += response["accounts"] + + return [ + { + API_ACCOUNT_ID: account[API_V3_ACCOUNT_ID], + API_ACCOUNT_NAME: account[API_ACCOUNT_NAME], + API_ACCOUNT_CURRENCY: account[API_ACCOUNT_CURRENCY], + API_ACCOUNT_AMOUNT: account[API_ACCOUNT_AVALIABLE][API_ACCOUNT_VALUE] + + account[API_ACCOUNT_HOLD][API_ACCOUNT_VALUE], + ACCOUNT_IS_VAULT: account[API_RESOURCE_TYPE] == API_V3_TYPE_VAULT, + } + for account in accounts + ] class CoinbaseData: """Get the latest data and update the states.""" - def __init__(self, client, exchange_base): + def __init__(self, client, exchange_base, version): """Init the coinbase data object.""" self.client = client self.accounts = None self.exchange_base = exchange_base self.exchange_rates = None - self.user_id = self.client.get_current_user()[API_ACCOUNT_ID] + if version == "v2": + self.user_id = self.client.get_current_user()[API_ACCOUNT_ID] + else: + self.user_id = ( + "v3_" + client.get_portfolios()["portfolios"][0][API_V3_ACCOUNT_ID] + ) + self.api_version = version @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from coinbase.""" try: - self.accounts = get_accounts(self.client) - self.exchange_rates = self.client.get_exchange_rates( - currency=self.exchange_base - ) - except AuthenticationError as coinbase_error: + self.accounts = get_accounts(self.client, self.api_version) + if self.api_version == "v2": + self.exchange_rates = self.client.get_exchange_rates( + currency=self.exchange_base + ) + else: + self.exchange_rates = self.client.get( + "/v2/exchange-rates", + params={API_RATES_CURRENCY: self.exchange_base}, + )[API_DATA] + except (AuthenticationError, HTTPError) as coinbase_error: _LOGGER.error( "Authentication error connecting to coinbase: %s", coinbase_error ) diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index 623d5cf6731..616fdaf8f7a 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -5,7 +5,9 @@ from __future__ import annotations import logging from typing import Any -from coinbase.wallet.client import Client +from coinbase.rest import RESTClient +from coinbase.rest.rest_base import HTTPError +from coinbase.wallet.client import Client as LegacyClient from coinbase.wallet.error import AuthenticationError import voluptuous as vol @@ -15,18 +17,17 @@ from homeassistant.config_entries import ( ConfigFlowResult, OptionsFlow, ) -from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from . import get_accounts from .const import ( + ACCOUNT_IS_VAULT, API_ACCOUNT_CURRENCY, - API_ACCOUNT_CURRENCY_CODE, + API_DATA, API_RATES, - API_RESOURCE_TYPE, - API_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_BASE, CONF_EXCHANGE_PRECISION, @@ -49,8 +50,11 @@ STEP_USER_DATA_SCHEMA = vol.Schema( def get_user_from_client(api_key, api_token): """Get the user name from Coinbase API credentials.""" - client = Client(api_key, api_token) - return client.get_current_user() + if "organizations" not in api_key: + client = LegacyClient(api_key, api_token) + return client.get_current_user()["name"] + client = RESTClient(api_key=api_key, api_secret=api_token) + return client.get_portfolios()["portfolios"][0]["name"] async def validate_api(hass: HomeAssistant, data): @@ -60,11 +64,13 @@ async def validate_api(hass: HomeAssistant, data): user = await hass.async_add_executor_job( get_user_from_client, data[CONF_API_KEY], data[CONF_API_TOKEN] ) - except AuthenticationError as error: - if "api key" in str(error): + except (AuthenticationError, HTTPError) as error: + if "api key" in str(error) or " 401 Client Error" in str(error): _LOGGER.debug("Coinbase rejected API credentials due to an invalid API key") raise InvalidKey from error - if "invalid signature" in str(error): + if "invalid signature" in str( + error + ) or "'Could not deserialize key data" in str(error): _LOGGER.debug( "Coinbase rejected API credentials due to an invalid API secret" ) @@ -73,8 +79,8 @@ async def validate_api(hass: HomeAssistant, data): raise InvalidAuth from error except ConnectionError as error: raise CannotConnect from error - - return {"title": user["name"]} + api_version = "v3" if "organizations" in data[CONF_API_KEY] else "v2" + return {"title": user, "api_version": api_version} async def validate_options(hass: HomeAssistant, config_entry: ConfigEntry, options): @@ -82,14 +88,20 @@ async def validate_options(hass: HomeAssistant, config_entry: ConfigEntry, optio client = hass.data[DOMAIN][config_entry.entry_id].client - accounts = await hass.async_add_executor_job(get_accounts, client) + accounts = await hass.async_add_executor_job( + get_accounts, client, config_entry.data.get("api_version", "v2") + ) accounts_currencies = [ - account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] + account[API_ACCOUNT_CURRENCY] for account in accounts - if account[API_RESOURCE_TYPE] != API_TYPE_VAULT + if not account[ACCOUNT_IS_VAULT] ] - available_rates = await hass.async_add_executor_job(client.get_exchange_rates) + if config_entry.data.get("api_version", "v2") == "v2": + available_rates = await hass.async_add_executor_job(client.get_exchange_rates) + else: + resp = await hass.async_add_executor_job(client.get, "/v2/exchange-rates") + available_rates = resp[API_DATA] if CONF_CURRENCIES in options: for currency in options[CONF_CURRENCIES]: if currency not in accounts_currencies: @@ -134,6 +146,7 @@ class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: + user_input[CONF_API_VERSION] = info["api_version"] return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index f5c75e3f926..0f47d4bc208 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -1,5 +1,7 @@ """Constants used for Coinbase.""" +ACCOUNT_IS_VAULT = "is_vault" + CONF_CURRENCIES = "account_balance_currencies" CONF_EXCHANGE_BASE = "exchange_base" CONF_EXCHANGE_RATES = "exchange_rate_currencies" @@ -10,18 +12,25 @@ DOMAIN = "coinbase" # Constants for data returned by Coinbase API API_ACCOUNT_AMOUNT = "amount" +API_ACCOUNT_AVALIABLE = "available_balance" API_ACCOUNT_BALANCE = "balance" API_ACCOUNT_CURRENCY = "currency" API_ACCOUNT_CURRENCY_CODE = "code" +API_ACCOUNT_HOLD = "hold" API_ACCOUNT_ID = "id" API_ACCOUNT_NATIVE_BALANCE = "balance" API_ACCOUNT_NAME = "name" -API_ACCOUNTS_DATA = "data" +API_ACCOUNT_VALUE = "value" +API_ACCOUNTS = "accounts" +API_DATA = "data" API_RATES = "rates" +API_RATES_CURRENCY = "currency" API_RESOURCE_PATH = "resource_path" API_RESOURCE_TYPE = "type" API_TYPE_VAULT = "vault" API_USD = "USD" +API_V3_ACCOUNT_ID = "uuid" +API_V3_TYPE_VAULT = "ACCOUNT_TYPE_VAULT" WALLETS = { "1INCH": "1INCH", diff --git a/homeassistant/components/coinbase/manifest.json b/homeassistant/components/coinbase/manifest.json index 515fe9f9abb..be632b5e856 100644 --- a/homeassistant/components/coinbase/manifest.json +++ b/homeassistant/components/coinbase/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/coinbase", "iot_class": "cloud_polling", "loggers": ["coinbase"], - "requirements": ["coinbase==2.1.0"] + "requirements": ["coinbase==2.1.0", "coinbase-advanced-py==1.2.2"] } diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index 83c63fa55fb..d3f3c81fb0c 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -12,15 +12,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import CoinbaseData from .const import ( + ACCOUNT_IS_VAULT, API_ACCOUNT_AMOUNT, - API_ACCOUNT_BALANCE, API_ACCOUNT_CURRENCY, - API_ACCOUNT_CURRENCY_CODE, API_ACCOUNT_ID, API_ACCOUNT_NAME, API_RATES, - API_RESOURCE_TYPE, - API_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_PRECISION, CONF_EXCHANGE_PRECISION_DEFAULT, @@ -31,6 +28,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) ATTR_NATIVE_BALANCE = "Balance in native currency" +ATTR_API_VERSION = "API Version" CURRENCY_ICONS = { "BTC": "mdi:currency-btc", @@ -56,9 +54,9 @@ async def async_setup_entry( entities: list[SensorEntity] = [] provided_currencies: list[str] = [ - account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] + account[API_ACCOUNT_CURRENCY] for account in instance.accounts - if account[API_RESOURCE_TYPE] != API_TYPE_VAULT + if not account[ACCOUNT_IS_VAULT] ] desired_currencies: list[str] = [] @@ -73,6 +71,11 @@ async def async_setup_entry( ) for currency in desired_currencies: + _LOGGER.debug( + "Attempting to set up %s account sensor with %s API", + currency, + instance.api_version, + ) if currency not in provided_currencies: _LOGGER.warning( ( @@ -85,12 +88,17 @@ async def async_setup_entry( entities.append(AccountSensor(instance, currency)) if CONF_EXCHANGE_RATES in config_entry.options: - entities.extend( - ExchangeRateSensor( - instance, rate, exchange_base_currency, exchange_precision + for rate in config_entry.options[CONF_EXCHANGE_RATES]: + _LOGGER.debug( + "Attempting to set up %s account sensor with %s API", + rate, + instance.api_version, + ) + entities.append( + ExchangeRateSensor( + instance, rate, exchange_base_currency, exchange_precision + ) ) - for rate in config_entry.options[CONF_EXCHANGE_RATES] - ) async_add_entities(entities) @@ -105,26 +113,21 @@ class AccountSensor(SensorEntity): self._coinbase_data = coinbase_data self._currency = currency for account in coinbase_data.accounts: - if ( - account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] != currency - or account[API_RESOURCE_TYPE] == API_TYPE_VAULT - ): + if account[API_ACCOUNT_CURRENCY] != currency or account[ACCOUNT_IS_VAULT]: continue self._attr_name = f"Coinbase {account[API_ACCOUNT_NAME]}" self._attr_unique_id = ( f"coinbase-{account[API_ACCOUNT_ID]}-wallet-" - f"{account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]}" + f"{account[API_ACCOUNT_CURRENCY]}" ) - self._attr_native_value = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT] - self._attr_native_unit_of_measurement = account[API_ACCOUNT_CURRENCY][ - API_ACCOUNT_CURRENCY_CODE - ] + self._attr_native_value = account[API_ACCOUNT_AMOUNT] + self._attr_native_unit_of_measurement = account[API_ACCOUNT_CURRENCY] self._attr_icon = CURRENCY_ICONS.get( - account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE], + account[API_ACCOUNT_CURRENCY], DEFAULT_COIN_ICON, ) self._native_balance = round( - float(account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]) + float(account[API_ACCOUNT_AMOUNT]) / float(coinbase_data.exchange_rates[API_RATES][currency]), 2, ) @@ -144,21 +147,26 @@ class AccountSensor(SensorEntity): """Return the state attributes of the sensor.""" return { ATTR_NATIVE_BALANCE: f"{self._native_balance} {self._coinbase_data.exchange_base}", + ATTR_API_VERSION: self._coinbase_data.api_version, } def update(self) -> None: """Get the latest state of the sensor.""" + _LOGGER.debug( + "Updating %s account sensor with %s API", + self._currency, + self._coinbase_data.api_version, + ) self._coinbase_data.update() for account in self._coinbase_data.accounts: if ( - account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] - != self._currency - or account[API_RESOURCE_TYPE] == API_TYPE_VAULT + account[API_ACCOUNT_CURRENCY] != self._currency + or account[ACCOUNT_IS_VAULT] ): continue - self._attr_native_value = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT] + self._attr_native_value = account[API_ACCOUNT_AMOUNT] self._native_balance = round( - float(account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]) + float(account[API_ACCOUNT_AMOUNT]) / float(self._coinbase_data.exchange_rates[API_RATES][self._currency]), 2, ) @@ -202,8 +210,13 @@ class ExchangeRateSensor(SensorEntity): def update(self) -> None: """Get the latest state of the sensor.""" + _LOGGER.debug( + "Updating %s rate sensor with %s API", + self._currency, + self._coinbase_data.api_version, + ) self._coinbase_data.update() self._attr_native_value = round( - 1 / float(self._coinbase_data.exchange_rates.rates[self._currency]), + 1 / float(self._coinbase_data.exchange_rates[API_RATES][self._currency]), self._precision, ) diff --git a/requirements_all.txt b/requirements_all.txt index 51d321b9a19..1960107d88e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -660,6 +660,9 @@ clearpasspy==1.0.2 # homeassistant.components.sinch clx-sdk-xms==1.0.0 +# homeassistant.components.coinbase +coinbase-advanced-py==1.2.2 + # homeassistant.components.coinbase coinbase==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d69137d94c..4559cd3dca0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,6 +562,9 @@ cached_ipaddress==0.3.0 # homeassistant.components.caldav caldav==1.3.9 +# homeassistant.components.coinbase +coinbase-advanced-py==1.2.2 + # homeassistant.components.coinbase coinbase==2.1.0 diff --git a/tests/components/coinbase/common.py b/tests/components/coinbase/common.py index 3421c4ce838..2768b6a2cd4 100644 --- a/tests/components/coinbase/common.py +++ b/tests/components/coinbase/common.py @@ -5,13 +5,14 @@ from homeassistant.components.coinbase.const import ( CONF_EXCHANGE_RATES, DOMAIN, ) -from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION from .const import ( GOOD_CURRENCY_2, GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2, MOCK_ACCOUNTS_RESPONSE, + MOCK_ACCOUNTS_RESPONSE_V3, ) from tests.common import MockConfigEntry @@ -54,6 +55,33 @@ def mocked_get_accounts(_, **kwargs): return MockGetAccounts(**kwargs) +class MockGetAccountsV3: + """Mock accounts with pagination.""" + + def __init__(self, cursor=""): + """Init mocked object, forced to return two at a time.""" + ids = [account["uuid"] for account in MOCK_ACCOUNTS_RESPONSE_V3] + start = ids.index(cursor) if cursor else 0 + + has_next = (target_end := start + 2) < len(MOCK_ACCOUNTS_RESPONSE_V3) + end = target_end if has_next else -1 + next_cursor = ids[end] if has_next else ids[-1] + self.accounts = { + "accounts": MOCK_ACCOUNTS_RESPONSE_V3[start:end], + "has_next": has_next, + "cursor": next_cursor, + } + + def __getitem__(self, item): + """Handle subscript request.""" + return self.accounts[item] + + +def mocked_get_accounts_v3(_, **kwargs): + """Return simplified accounts using mock.""" + return MockGetAccountsV3(**kwargs) + + def mock_get_current_user(): """Return a simplified mock user.""" return { @@ -74,6 +102,19 @@ def mock_get_exchange_rates(): } +def mock_get_portfolios(): + """Return a mocked list of Coinbase portfolios.""" + return { + "portfolios": [ + { + "name": "Default", + "uuid": "123456", + "type": "DEFAULT", + } + ] + } + + async def init_mock_coinbase(hass, currencies=None, rates=None): """Init Coinbase integration for testing.""" config_entry = MockConfigEntry( @@ -93,3 +134,28 @@ async def init_mock_coinbase(hass, currencies=None, rates=None): await hass.async_block_till_done() return config_entry + + +async def init_mock_coinbase_v3(hass, currencies=None, rates=None): + """Init Coinbase integration for testing.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="080272b77a4f80c41b94d7cdc86fd826", + unique_id=None, + title="Test User v3", + data={ + CONF_API_KEY: "organizations/123456", + CONF_API_TOKEN: "AbCDeF", + CONF_API_VERSION: "v3", + }, + options={ + CONF_CURRENCIES: currencies or [], + CONF_EXCHANGE_RATES: rates or [], + }, + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/coinbase/const.py b/tests/components/coinbase/const.py index dcd14555ca3..5fbba11eb2d 100644 --- a/tests/components/coinbase/const.py +++ b/tests/components/coinbase/const.py @@ -31,3 +31,31 @@ MOCK_ACCOUNTS_RESPONSE = [ "type": "fiat", }, ] + +MOCK_ACCOUNTS_RESPONSE_V3 = [ + { + "uuid": "123456789", + "name": "BTC Wallet", + "currency": GOOD_CURRENCY, + "available_balance": {"value": "0.00001", "currency": GOOD_CURRENCY}, + "type": "ACCOUNT_TYPE_CRYPTO", + "hold": {"value": "0", "currency": GOOD_CURRENCY}, + }, + { + "uuid": "abcdefg", + "name": "BTC Vault", + "currency": GOOD_CURRENCY, + "available_balance": {"value": "100.00", "currency": GOOD_CURRENCY}, + "type": "ACCOUNT_TYPE_VAULT", + "hold": {"value": "0", "currency": GOOD_CURRENCY}, + }, + { + "uuid": "987654321", + "name": "USD Wallet", + "currency": GOOD_CURRENCY_2, + "available_balance": {"value": "9.90", "currency": GOOD_CURRENCY_2}, + "type": "ACCOUNT_TYPE_FIAT", + "ready": True, + "hold": {"value": "0", "currency": GOOD_CURRENCY_2}, + }, +] diff --git a/tests/components/coinbase/snapshots/test_diagnostics.ambr b/tests/components/coinbase/snapshots/test_diagnostics.ambr index 9079a7682c8..4f9e75dc38b 100644 --- a/tests/components/coinbase/snapshots/test_diagnostics.ambr +++ b/tests/components/coinbase/snapshots/test_diagnostics.ambr @@ -3,40 +3,25 @@ dict({ 'accounts': list([ dict({ - 'balance': dict({ - 'amount': '**REDACTED**', - 'currency': 'BTC', - }), - 'currency': dict({ - 'code': 'BTC', - }), + 'amount': '**REDACTED**', + 'currency': 'BTC', 'id': '**REDACTED**', + 'is_vault': False, 'name': 'BTC Wallet', - 'type': 'wallet', }), dict({ - 'balance': dict({ - 'amount': '**REDACTED**', - 'currency': 'BTC', - }), - 'currency': dict({ - 'code': 'BTC', - }), + 'amount': '**REDACTED**', + 'currency': 'BTC', 'id': '**REDACTED**', + 'is_vault': True, 'name': 'BTC Vault', - 'type': 'vault', }), dict({ - 'balance': dict({ - 'amount': '**REDACTED**', - 'currency': 'USD', - }), - 'currency': dict({ - 'code': 'USD', - }), + 'amount': '**REDACTED**', + 'currency': 'USD', 'id': '**REDACTED**', + 'is_vault': False, 'name': 'USD Wallet', - 'type': 'fiat', }), ]), 'entry': dict({ diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index f213392bb1e..aa2c6208e0f 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -14,15 +14,18 @@ from homeassistant.components.coinbase.const import ( CONF_EXCHANGE_RATES, DOMAIN, ) -from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from .common import ( init_mock_coinbase, + init_mock_coinbase_v3, mock_get_current_user, mock_get_exchange_rates, + mock_get_portfolios, mocked_get_accounts, + mocked_get_accounts_v3, ) from .const import BAD_CURRENCY, BAD_EXCHANGE_RATE, GOOD_CURRENCY, GOOD_EXCHANGE_RATE @@ -53,16 +56,17 @@ async def test_form(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_API_KEY: "123456", - CONF_API_TOKEN: "AbCDeF", - }, + {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}, ) await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test User" - assert result2["data"] == {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"} + assert result2["data"] == { + CONF_API_KEY: "123456", + CONF_API_TOKEN: "AbCDeF", + CONF_API_VERSION: "v2", + } assert len(mock_setup_entry.mock_calls) == 1 @@ -314,3 +318,77 @@ async def test_option_catch_all_exception(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} + + +async def test_form_v3(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"] is FlowResultType.FORM + assert result["errors"] == {} + + with ( + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), + patch( + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), + ), + patch( + "coinbase.rest.RESTBase.get", + return_value={"data": mock_get_exchange_rates()}, + ), + patch( + "homeassistant.components.coinbase.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "organizations/123456", CONF_API_TOKEN: "AbCDeF"}, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "Default" + assert result2["data"] == { + CONF_API_KEY: "organizations/123456", + CONF_API_TOKEN: "AbCDeF", + CONF_API_VERSION: "v3", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_option_form_v3(hass: HomeAssistant) -> None: + """Test we handle a good wallet currency option.""" + + with ( + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), + patch( + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), + ), + patch( + "coinbase.rest.RESTBase.get", + return_value={"data": mock_get_exchange_rates()}, + ), + patch( + "homeassistant.components.coinbase.update_listener" + ) as mock_update_listener, + ): + config_entry = await init_mock_coinbase_v3(hass) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_CURRENCIES: [GOOD_CURRENCY], + CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE], + CONF_EXCHANGE_PRECISION: 5, + }, + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() + assert len(mock_update_listener.mock_calls) == 1 From a406068f13022c7279ccf7c68c4553df60896124 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:32:46 +0200 Subject: [PATCH 2089/2411] Fix implicit-return in homematic (#122922) --- homeassistant/components/homematic/__init__.py | 3 +++ homeassistant/components/homematic/climate.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 80345866b1f..f0fc2a40278 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -573,6 +573,8 @@ def _create_ha_id(name, channel, param, count): if count > 1 and param is not None: return f"{name} {channel} {param}" + raise ValueError(f"Unable to create unique id for count:{count} and param:{param}") + def _hm_event_handler(hass, interface, device, caller, attribute, value): """Handle all pyhomematic device events.""" @@ -621,3 +623,4 @@ def _device_from_servicecall(hass, service): for devices in hass.data[DATA_HOMEMATIC].devices.values(): if address in devices: return devices[address] + return None diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index bf1295df6be..2be28487cbb 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -125,6 +125,7 @@ class HMThermostat(HMDevice, ClimateEntity): for node in HM_HUMI_MAP: if node in self._data: return self._data[node] + return None @property def current_temperature(self): @@ -132,6 +133,7 @@ class HMThermostat(HMDevice, ClimateEntity): for node in HM_TEMP_MAP: if node in self._data: return self._data[node] + return None @property def target_temperature(self): From 60117ae1504efa973c238e174e7dc76001d0383f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Aug 2024 17:30:39 +0200 Subject: [PATCH 2090/2411] Revert "Fix blocking I/O while validating config schema" (#123377) --- homeassistant/config.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 18c833d4c75..948ab342e79 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -817,9 +817,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non This method is a coroutine. """ - # CORE_CONFIG_SCHEMA is not async safe since it uses vol.IsDir - # so we need to run it in an executor job. - config = await hass.async_add_executor_job(CORE_CONFIG_SCHEMA, config) + config = CORE_CONFIG_SCHEMA(config) # Only load auth during startup. if not hasattr(hass, "auth"): @@ -1535,15 +1533,9 @@ async def async_process_component_config( return IntegrationConfigInfo(None, config_exceptions) # No custom config validator, proceed with schema validation - if config_schema := getattr(component, "CONFIG_SCHEMA", None): + if hasattr(component, "CONFIG_SCHEMA"): try: - if domain in config: - # cv.isdir, cv.isfile, cv.isdevice are not async - # friendly so we need to run this in executor - schema = await hass.async_add_executor_job(config_schema, config) - else: - schema = config_schema(config) - return IntegrationConfigInfo(schema, []) + return IntegrationConfigInfo(component.CONFIG_SCHEMA(config), []) except vol.Invalid as exc: exc_info = ConfigExceptionInfo( exc, From c2f2a868c4973de3d2fac019f6b0468372b0e496 Mon Sep 17 00:00:00 2001 From: fustom Date: Thu, 8 Aug 2024 18:49:47 +0200 Subject: [PATCH 2091/2411] Fix limit and order property for transmission integration (#123305) --- homeassistant/components/transmission/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/transmission/coordinator.py b/homeassistant/components/transmission/coordinator.py index d6b5b695656..e0930bd9e9e 100644 --- a/homeassistant/components/transmission/coordinator.py +++ b/homeassistant/components/transmission/coordinator.py @@ -55,12 +55,12 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]): @property def limit(self) -> int: """Return limit.""" - return self.config_entry.data.get(CONF_LIMIT, DEFAULT_LIMIT) + return self.config_entry.options.get(CONF_LIMIT, DEFAULT_LIMIT) @property def order(self) -> str: """Return order.""" - return self.config_entry.data.get(CONF_ORDER, DEFAULT_ORDER) + return self.config_entry.options.get(CONF_ORDER, DEFAULT_ORDER) async def _async_update_data(self) -> SessionStats: """Update transmission data.""" From ddc94030a6a7d6b422991d02b0ed909c348f5cd5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Aug 2024 18:50:49 +0200 Subject: [PATCH 2092/2411] Fix raise-within-try in config validation helper (#123353) --- homeassistant/helpers/config_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index cd6670dc597..01960b6c0c3 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -770,9 +770,9 @@ def socket_timeout(value: Any | None) -> object: float_value = float(value) if float_value > 0.0: return float_value - raise vol.Invalid("Invalid socket timeout value. float > 0.0 required.") except Exception as err: raise vol.Invalid(f"Invalid socket timeout: {err}") from err + raise vol.Invalid("Invalid socket timeout value. float > 0.0 required.") def url( From b498c898606b550f7f585b5d56a794ae81f0d18e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 Aug 2024 18:51:19 +0200 Subject: [PATCH 2093/2411] Fix raise-within-try in language util (#123354) --- homeassistant/util/language.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/util/language.py b/homeassistant/util/language.py index 8644f8014b6..8a82de9065f 100644 --- a/homeassistant/util/language.py +++ b/homeassistant/util/language.py @@ -137,9 +137,6 @@ class Dialect: region_idx = pref_regions.index(self.region) elif dialect.region is not None: region_idx = pref_regions.index(dialect.region) - else: - # Can't happen, but mypy is not smart enough - raise ValueError # More preferred regions are at the front. # Add 1 to boost above a weak match where no regions are set. From 634a2b22dc09188a4ef02b2d93746efe71077e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 8 Aug 2024 19:23:52 +0200 Subject: [PATCH 2094/2411] Improve Airzone Cloud sensors availability (#123383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit airzone_cloud: improve sensors availability Make sensor entities unavailable instead of providing an unknown state. Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone_cloud/binary_sensor.py | 5 +++++ homeassistant/components/airzone_cloud/sensor.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/airzone_cloud/binary_sensor.py b/homeassistant/components/airzone_cloud/binary_sensor.py index f22515155f1..3d6f6b42901 100644 --- a/homeassistant/components/airzone_cloud/binary_sensor.py +++ b/homeassistant/components/airzone_cloud/binary_sensor.py @@ -161,6 +161,11 @@ class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity): entity_description: AirzoneBinarySensorEntityDescription + @property + def available(self) -> bool: + """Return Airzone Cloud binary sensor availability.""" + return super().available and self.is_on is not None + @callback def _handle_coordinator_update(self) -> None: """Update attributes when the coordinator updates.""" diff --git a/homeassistant/components/airzone_cloud/sensor.py b/homeassistant/components/airzone_cloud/sensor.py index a3a456edd03..9f0ee01aca2 100644 --- a/homeassistant/components/airzone_cloud/sensor.py +++ b/homeassistant/components/airzone_cloud/sensor.py @@ -189,6 +189,11 @@ async def async_setup_entry( class AirzoneSensor(AirzoneEntity, SensorEntity): """Define an Airzone Cloud sensor.""" + @property + def available(self) -> bool: + """Return Airzone Cloud sensor availability.""" + return super().available and self.native_value is not None + @callback def _handle_coordinator_update(self) -> None: """Update attributes when the coordinator updates.""" From 2343f5e40fcc5ad60844e92843e1caa2fc9ce16e Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 8 Aug 2024 19:28:46 +0200 Subject: [PATCH 2095/2411] Reolink Chime online status and ability to remove (#123301) * Add chime available * allow removing a Reolink chime * Allow removal if doorbell itself removed * fix tests * Add tests * fix styling --- homeassistant/components/reolink/__init__.py | 40 ++++++++++ homeassistant/components/reolink/entity.py | 5 ++ tests/components/reolink/conftest.py | 32 ++++++++ tests/components/reolink/test_init.py | 82 ++++++++++++++++++-- tests/components/reolink/test_select.py | 37 ++++----- 5 files changed, 168 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index cc293d970b2..a319024633c 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -188,6 +188,46 @@ async def async_remove_config_entry_device( host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host (device_uid, ch, is_chime) = get_device_uid_and_ch(device, host) + if is_chime: + await host.api.get_state(cmd="GetDingDongList") + chime = host.api.chime(ch) + if ( + chime is None + or chime.connect_state is None + or chime.connect_state < 0 + or chime.channel not in host.api.channels + ): + _LOGGER.debug( + "Removing Reolink chime %s with id %s, " + "since it is not coupled to %s anymore", + device.name, + ch, + host.api.nvr_name, + ) + return True + + # remove the chime from the host + await chime.remove() + await host.api.get_state(cmd="GetDingDongList") + if chime.connect_state < 0: + _LOGGER.debug( + "Removed Reolink chime %s with id %s from %s", + device.name, + ch, + host.api.nvr_name, + ) + return True + + _LOGGER.warning( + "Cannot remove Reolink chime %s with id %s, because it is still connected " + "to %s, please first remove the chime " + "in the reolink app", + device.name, + ch, + host.api.nvr_name, + ) + return False + if not host.api.is_nvr or ch is None: _LOGGER.warning( "Cannot remove Reolink device %s, because it is not a camera connected " diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index 053792ad667..c47822e125c 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -190,3 +190,8 @@ class ReolinkChimeCoordinatorEntity(ReolinkChannelCoordinatorEntity): serial_number=str(chime.dev_id), configuration_url=self._conf_url, ) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._chime.online and super().available diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index c74cac76192..981dcc30e60 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -4,6 +4,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest +from reolink_aio.api import Chime from homeassistant.components.reolink import const from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL @@ -107,6 +108,14 @@ def reolink_connect_class() -> Generator[MagicMock]: host_mock.capabilities = {"Host": ["RTSP"], "0": ["motion_detection"]} host_mock.checked_api_versions = {"GetEvents": 1} host_mock.abilities = {"abilityChn": [{"aiTrack": {"permit": 0, "ver": 0}}]} + + # enums + host_mock.whiteled_mode.return_value = 1 + host_mock.whiteled_mode_list.return_value = ["off", "auto"] + host_mock.doorbell_led.return_value = "Off" + host_mock.doorbell_led_list.return_value = ["stayoff", "auto"] + host_mock.auto_track_method.return_value = 3 + host_mock.daynight_state.return_value = "Black&White" yield host_mock_class @@ -145,3 +154,26 @@ def config_entry(hass: HomeAssistant) -> MockConfigEntry: ) config_entry.add_to_hass(hass) return config_entry + + +@pytest.fixture +def test_chime(reolink_connect: MagicMock) -> None: + """Mock a reolink chime.""" + TEST_CHIME = Chime( + host=reolink_connect, + dev_id=12345678, + channel=0, + ) + TEST_CHIME.name = "Test chime" + TEST_CHIME.volume = 3 + TEST_CHIME.connect_state = 2 + TEST_CHIME.led_state = True + TEST_CHIME.event_info = { + "md": {"switch": 0, "musicId": 0}, + "people": {"switch": 0, "musicId": 1}, + "visitor": {"switch": 1, "musicId": 2}, + } + + reolink_connect.chime_list = [TEST_CHIME] + reolink_connect.chime.return_value = TEST_CHIME + return TEST_CHIME diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 4f745530b6b..5334e171e5e 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -6,6 +6,7 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest +from reolink_aio.api import Chime from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError from homeassistant.components.reolink import ( @@ -40,6 +41,8 @@ from tests.typing import WebSocketGenerator pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") +CHIME_MODEL = "Reolink Chime" + async def test_wait(*args, **key_args): """Ensure a mocked function takes a bit of time to be able to timeout in test.""" @@ -224,13 +227,9 @@ async def test_removing_disconnected_cams( device_models = [device.model for device in device_entries] assert sorted(device_models) == sorted([TEST_HOST_MODEL, TEST_CAM_MODEL]) - # reload integration after 'disconnecting' a camera. + # Try to remove the device after 'disconnecting' a camera. if attr is not None: setattr(reolink_connect, attr, value) - with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): - assert await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() - expected_success = TEST_CAM_MODEL not in expected_models for device in device_entries: if device.model == TEST_CAM_MODEL: @@ -244,6 +243,79 @@ async def test_removing_disconnected_cams( assert sorted(device_models) == sorted(expected_models) +@pytest.mark.parametrize( + ("attr", "value", "expected_models"), + [ + ( + None, + None, + [TEST_HOST_MODEL, TEST_CAM_MODEL, CHIME_MODEL], + ), + ( + "connect_state", + -1, + [TEST_HOST_MODEL, TEST_CAM_MODEL], + ), + ( + "remove", + -1, + [TEST_HOST_MODEL, TEST_CAM_MODEL], + ), + ], +) +async def test_removing_chime( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + test_chime: Chime, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + attr: str | None, + value: Any, + expected_models: list[str], +) -> None: + """Test removing a chime.""" + reolink_connect.channels = [0] + assert await async_setup_component(hass, "config", {}) + client = await hass_ws_client(hass) + # setup CH 0 and NVR switch entities/device + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + device_models = [device.model for device in device_entries] + assert sorted(device_models) == sorted( + [TEST_HOST_MODEL, TEST_CAM_MODEL, CHIME_MODEL] + ) + + if attr == "remove": + + async def test_remove_chime(*args, **key_args): + """Remove chime.""" + test_chime.connect_state = -1 + + test_chime.remove = test_remove_chime + elif attr is not None: + setattr(test_chime, attr, value) + + # Try to remove the device after 'disconnecting' a chime. + expected_success = CHIME_MODEL not in expected_models + for device in device_entries: + if device.model == CHIME_MODEL: + response = await client.remove_device(device.id, config_entry.entry_id) + assert response["success"] == expected_success + + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + device_models = [device.model for device in device_entries] + assert sorted(device_models) == sorted(expected_models) + + @pytest.mark.parametrize( ( "original_id", diff --git a/tests/components/reolink/test_select.py b/tests/components/reolink/test_select.py index 53c1e494b3d..5536797d7d3 100644 --- a/tests/components/reolink/test_select.py +++ b/tests/components/reolink/test_select.py @@ -33,8 +33,6 @@ async def test_floodlight_mode_select( entity_registry: er.EntityRegistry, ) -> None: """Test select entity with floodlight_mode.""" - reolink_connect.whiteled_mode.return_value = 1 - reolink_connect.whiteled_mode_list.return_value = ["off", "auto"] with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]): assert await hass.config_entries.async_setup(config_entry.entry_id) is True await hass.async_block_till_done() @@ -72,6 +70,14 @@ async def test_floodlight_mode_select( blocking=True, ) + reolink_connect.whiteled_mode.return_value = -99 # invalid value + async_fire_time_changed( + hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30) + ) + await hass.async_block_till_done() + + assert hass.states.is_state(entity_id, STATE_UNKNOWN) + async def test_play_quick_reply_message( hass: HomeAssistant, @@ -103,25 +109,10 @@ async def test_chime_select( hass: HomeAssistant, config_entry: MockConfigEntry, reolink_connect: MagicMock, + test_chime: Chime, entity_registry: er.EntityRegistry, ) -> None: """Test chime select entity.""" - TEST_CHIME = Chime( - host=reolink_connect, - dev_id=12345678, - channel=0, - ) - TEST_CHIME.name = "Test chime" - TEST_CHIME.volume = 3 - TEST_CHIME.led_state = True - TEST_CHIME.event_info = { - "md": {"switch": 0, "musicId": 0}, - "people": {"switch": 0, "musicId": 1}, - "visitor": {"switch": 1, "musicId": 2}, - } - - reolink_connect.chime_list = [TEST_CHIME] - with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]): assert await hass.config_entries.async_setup(config_entry.entry_id) is True await hass.async_block_till_done() @@ -131,16 +122,16 @@ async def test_chime_select( entity_id = f"{Platform.SELECT}.test_chime_visitor_ringtone" assert hass.states.is_state(entity_id, "pianokey") - TEST_CHIME.set_tone = AsyncMock() + test_chime.set_tone = AsyncMock() await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, {ATTR_ENTITY_ID: entity_id, "option": "off"}, blocking=True, ) - TEST_CHIME.set_tone.assert_called_once() + test_chime.set_tone.assert_called_once() - TEST_CHIME.set_tone = AsyncMock(side_effect=ReolinkError("Test error")) + test_chime.set_tone = AsyncMock(side_effect=ReolinkError("Test error")) with pytest.raises(HomeAssistantError): await hass.services.async_call( SELECT_DOMAIN, @@ -149,7 +140,7 @@ async def test_chime_select( blocking=True, ) - TEST_CHIME.set_tone = AsyncMock(side_effect=InvalidParameterError("Test error")) + test_chime.set_tone = AsyncMock(side_effect=InvalidParameterError("Test error")) with pytest.raises(ServiceValidationError): await hass.services.async_call( SELECT_DOMAIN, @@ -158,7 +149,7 @@ async def test_chime_select( blocking=True, ) - TEST_CHIME.event_info = {} + test_chime.event_info = {} async_fire_time_changed( hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30) ) From 69740e865c82c82fc27b79e9732787dc677feccd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Aug 2024 15:52:09 -0500 Subject: [PATCH 2096/2411] Reduce number of aiohttp.TCPConnector cleanup_closed checks to one per minute (#123268) --- homeassistant/helpers/aiohttp_client.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 6f52569c38c..d61f889d4b5 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -285,6 +285,21 @@ def _make_key( return (verify_ssl, family) +class HomeAssistantTCPConnector(aiohttp.TCPConnector): + """Home Assistant TCP Connector. + + Same as aiohttp.TCPConnector but with a longer cleanup_closed timeout. + + By default the cleanup_closed timeout is 2 seconds. This is too short + for Home Assistant since we churn through a lot of connections. We set + it to 60 seconds to reduce the overhead of aborting TLS connections + that are likely already closed. + """ + + # abort transport after 60 seconds (cleanup broken connections) + _cleanup_closed_period = 60.0 + + @callback def _async_get_connector( hass: HomeAssistant, @@ -306,7 +321,7 @@ def _async_get_connector( else: ssl_context = ssl_util.get_default_no_verify_context() - connector = aiohttp.TCPConnector( + connector = HomeAssistantTCPConnector( family=family, enable_cleanup_closed=ENABLE_CLEANUP_CLOSED, ssl=ssl_context, From 03ba8f6173a708e2dd800e2ee1ee5b0ed5697ccf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Aug 2024 17:07:22 -0500 Subject: [PATCH 2097/2411] Bump aiohttp to 3.10.2 (#123394) --- 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 10823557b94..7c0255b9eae 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-zlib==0.1.1 -aiohttp==3.10.1 +aiohttp==3.10.2 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index 9e4fe36243b..07af385dce4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.10.1", + "aiohttp==3.10.2", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index f35fffe680a..40f2165a61a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.10.1 +aiohttp==3.10.2 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 From 00c1a3fd4e473d415d89447cb2b89326dd8eeac3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Aug 2024 23:19:12 -0500 Subject: [PATCH 2098/2411] Ensure legacy event foreign key is removed from the states table when a previous rebuild failed (#123388) * Ensure legacy event foreign key is removed from the states table If the system ran out of disk space removing the FK, it would fail. #121938 fixed that to try again, however that PR was made ineffective by #122069 since it will never reach the check. To solve this, the migration version is incremented to 2, and the migration is no longer marked as done unless the rebuild /fk removal is successful. * fix logic for mysql * fix test * asserts * coverage * coverage * narrow test * fixes * split tests * should have skipped * fixture must be used --- .../components/recorder/migration.py | 24 +- tests/components/recorder/test_migrate.py | 14 +- .../components/recorder/test_v32_migration.py | 346 ++++++++++++++++++ 3 files changed, 368 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 2932ea484c9..a41de07e243 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -632,7 +632,7 @@ def _update_states_table_with_foreign_key_options( def _drop_foreign_key_constraints( session_maker: Callable[[], Session], engine: Engine, table: str, column: str -) -> list[tuple[str, str, ReflectedForeignKeyConstraint]]: +) -> tuple[bool, list[tuple[str, str, ReflectedForeignKeyConstraint]]]: """Drop foreign key constraints for a table on specific columns.""" inspector = sqlalchemy.inspect(engine) dropped_constraints = [ @@ -649,6 +649,7 @@ def _drop_foreign_key_constraints( if foreign_key["name"] and foreign_key["constrained_columns"] == [column] ] + fk_remove_ok = True for drop in drops: with session_scope(session=session_maker()) as session: try: @@ -660,8 +661,9 @@ def _drop_foreign_key_constraints( TABLE_STATES, column, ) + fk_remove_ok = False - return dropped_constraints + return fk_remove_ok, dropped_constraints def _restore_foreign_key_constraints( @@ -1481,7 +1483,7 @@ class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44): for column in columns for dropped_constraint in _drop_foreign_key_constraints( self.session_maker, self.engine, table, column - ) + )[1] ] _LOGGER.debug("Dropped foreign key constraints: %s", dropped_constraints) @@ -1956,14 +1958,15 @@ def cleanup_legacy_states_event_ids(instance: Recorder) -> bool: if instance.dialect_name == SupportedDialect.SQLITE: # SQLite does not support dropping foreign key constraints # so we have to rebuild the table - rebuild_sqlite_table(session_maker, instance.engine, States) + fk_remove_ok = rebuild_sqlite_table(session_maker, instance.engine, States) else: - _drop_foreign_key_constraints( + fk_remove_ok, _ = _drop_foreign_key_constraints( session_maker, instance.engine, TABLE_STATES, "event_id" ) - _drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX) - instance.use_legacy_events_index = False - _mark_migration_done(session, EventIDPostMigration) + if fk_remove_ok: + _drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX) + instance.use_legacy_events_index = False + _mark_migration_done(session, EventIDPostMigration) return True @@ -2419,6 +2422,7 @@ class EventIDPostMigration(BaseRunTimeMigration): migration_id = "event_id_post_migration" task = MigrationTask + migration_version = 2 @staticmethod def migrate_data(instance: Recorder) -> bool: @@ -2469,7 +2473,7 @@ def _mark_migration_done( def rebuild_sqlite_table( session_maker: Callable[[], Session], engine: Engine, table: type[Base] -) -> None: +) -> bool: """Rebuild an SQLite table. This must only be called after all migrations are complete @@ -2524,8 +2528,10 @@ def rebuild_sqlite_table( # Swallow the exception since we do not want to ever raise # an integrity error as it would cause the database # to be discarded and recreated from scratch + return False else: _LOGGER.warning("Rebuilding SQLite table %s finished", orig_name) + return True finally: with session_scope(session=session_maker()) as session: # Step 12 - Re-enable foreign keys diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index dc99ddefa3b..e55793caad7 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -748,7 +748,7 @@ def test_rebuild_sqlite_states_table(recorder_db_url: str) -> None: session.add(States(state="on")) session.commit() - migration.rebuild_sqlite_table(session_maker, engine, States) + assert migration.rebuild_sqlite_table(session_maker, engine, States) is True with session_scope(session=session_maker()) as session: assert session.query(States).count() == 1 @@ -776,13 +776,13 @@ def test_rebuild_sqlite_states_table_missing_fails( session.connection().execute(text("DROP TABLE states")) session.commit() - migration.rebuild_sqlite_table(session_maker, engine, States) + assert migration.rebuild_sqlite_table(session_maker, engine, States) is False assert "Error recreating SQLite table states" in caplog.text caplog.clear() # Now rebuild the events table to make sure the database did not # get corrupted - migration.rebuild_sqlite_table(session_maker, engine, Events) + assert migration.rebuild_sqlite_table(session_maker, engine, Events) is True with session_scope(session=session_maker()) as session: assert session.query(Events).count() == 1 @@ -812,7 +812,7 @@ def test_rebuild_sqlite_states_table_extra_columns( text("ALTER TABLE states ADD COLUMN extra_column TEXT") ) - migration.rebuild_sqlite_table(session_maker, engine, States) + assert migration.rebuild_sqlite_table(session_maker, engine, States) is True assert "Error recreating SQLite table states" not in caplog.text with session_scope(session=session_maker()) as session: @@ -905,7 +905,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: for table, column in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column - ) + )[1] ] assert dropped_constraints_1 == expected_dropped_constraints[db_engine] @@ -917,7 +917,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: for table, column in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column - ) + )[1] ] assert dropped_constraints_2 == [] @@ -936,7 +936,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: for table, column in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column - ) + )[1] ] assert dropped_constraints_3 == expected_dropped_constraints[db_engine] diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index 9956fec8a09..5266e55851c 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -7,6 +7,7 @@ from unittest.mock import patch import pytest from sqlalchemy import create_engine, inspect +from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm import Session from homeassistant.components import recorder @@ -444,3 +445,348 @@ async def test_migrate_can_resume_ix_states_event_id_removed( assert await instance.async_add_executor_job(_get_event_id_foreign_keys) is None await hass.async_stop() + + +@pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.parametrize("enable_migrate_event_ids", [True]) +@pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage +async def test_out_of_disk_space_while_rebuild_states_table( + async_test_recorder: RecorderInstanceGenerator, + caplog: pytest.LogCaptureFixture, + recorder_db_url: str, +) -> None: + """Test that we can recover from out of disk space while rebuilding the states table. + + This case tests the migration still happens if + ix_states_event_id is removed from the states table. + """ + importlib.import_module(SCHEMA_MODULE) + old_db_schema = sys.modules[SCHEMA_MODULE] + now = dt_util.utcnow() + one_second_past = now - timedelta(seconds=1) + mock_state = State( + "sensor.test", + "old", + {"last_reset": now.isoformat()}, + last_changed=one_second_past, + last_updated=now, + ) + state_changed_event = Event( + EVENT_STATE_CHANGED, + { + "entity_id": "sensor.test", + "old_state": None, + "new_state": mock_state, + }, + EventOrigin.local, + time_fired_timestamp=now.timestamp(), + ) + custom_event = Event( + "custom_event", + {"entity_id": "sensor.custom"}, + EventOrigin.local, + time_fired_timestamp=now.timestamp(), + ) + number_of_migrations = 5 + + def _get_event_id_foreign_keys(): + assert instance.engine is not None + return next( + ( + fk # type: ignore[misc] + for fk in inspect(instance.engine).get_foreign_keys("states") + if fk["constrained_columns"] == ["event_id"] + ), + None, + ) + + def _get_states_index_names(): + with session_scope(hass=hass) as session: + return inspect(session.connection()).get_indexes("states") + + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), + patch.object(core, "StatesMeta", old_db_schema.StatesMeta), + patch.object(core, "EventTypes", old_db_schema.EventTypes), + patch.object(core, "EventData", old_db_schema.EventData), + patch.object(core, "States", old_db_schema.States), + patch.object(core, "Events", old_db_schema.Events), + patch(CREATE_ENGINE_TARGET, new=_create_engine_test), + patch("homeassistant.components.recorder.Recorder._post_migrate_entity_ids"), + patch( + "homeassistant.components.recorder.migration.cleanup_legacy_states_event_ids" + ), + ): + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): + await hass.async_block_till_done() + await async_wait_recording_done(hass) + await async_wait_recording_done(hass) + + def _add_data(): + with session_scope(hass=hass) as session: + session.add(old_db_schema.Events.from_event(custom_event)) + session.add(old_db_schema.States.from_event(state_changed_event)) + + await instance.async_add_executor_job(_add_data) + await hass.async_block_till_done() + await instance.async_block_till_done() + + await instance.async_add_executor_job( + migration._drop_index, + instance.get_session, + "states", + "ix_states_event_id", + ) + + states_indexes = await instance.async_add_executor_job( + _get_states_index_names + ) + states_index_names = {index["name"] for index in states_indexes} + assert instance.use_legacy_events_index is True + assert ( + await instance.async_add_executor_job(_get_event_id_foreign_keys) + is not None + ) + + await hass.async_stop() + await hass.async_block_till_done() + + assert "ix_states_entity_id_last_updated_ts" in states_index_names + + # Simulate out of disk space while rebuilding the states table by + # - patching CreateTable to raise SQLAlchemyError for SQLite + # - patching DropConstraint to raise InternalError for MySQL and PostgreSQL + with ( + patch( + "homeassistant.components.recorder.migration.CreateTable", + side_effect=SQLAlchemyError, + ), + patch( + "homeassistant.components.recorder.migration.DropConstraint", + side_effect=OperationalError( + None, None, OSError("No space left on device") + ), + ), + ): + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): + await hass.async_block_till_done() + + # We need to wait for all the migration tasks to complete + # before we can check the database. + for _ in range(number_of_migrations): + await instance.async_block_till_done() + await async_wait_recording_done(hass) + + states_indexes = await instance.async_add_executor_job( + _get_states_index_names + ) + states_index_names = {index["name"] for index in states_indexes} + assert instance.use_legacy_events_index is True + assert "Error recreating SQLite table states" in caplog.text + assert await instance.async_add_executor_job(_get_event_id_foreign_keys) + + await hass.async_stop() + + # Now run it again to verify the table rebuild tries again + caplog.clear() + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): + await hass.async_block_till_done() + + # We need to wait for all the migration tasks to complete + # before we can check the database. + for _ in range(number_of_migrations): + await instance.async_block_till_done() + await async_wait_recording_done(hass) + + states_indexes = await instance.async_add_executor_job(_get_states_index_names) + states_index_names = {index["name"] for index in states_indexes} + assert instance.use_legacy_events_index is False + assert "ix_states_entity_id_last_updated_ts" not in states_index_names + assert "ix_states_event_id" not in states_index_names + assert "Rebuilding SQLite table states finished" in caplog.text + assert await instance.async_add_executor_job(_get_event_id_foreign_keys) is None + + await hass.async_stop() + + +@pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.skip_on_db_engine(["sqlite"]) +@pytest.mark.parametrize("enable_migrate_event_ids", [True]) +@pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage +async def test_out_of_disk_space_while_removing_foreign_key( + async_test_recorder: RecorderInstanceGenerator, + caplog: pytest.LogCaptureFixture, + recorder_db_url: str, +) -> None: + """Test that we can recover from out of disk space while removing the foreign key. + + This case tests the migration still happens if + ix_states_event_id is removed from the states table. + """ + importlib.import_module(SCHEMA_MODULE) + old_db_schema = sys.modules[SCHEMA_MODULE] + now = dt_util.utcnow() + one_second_past = now - timedelta(seconds=1) + mock_state = State( + "sensor.test", + "old", + {"last_reset": now.isoformat()}, + last_changed=one_second_past, + last_updated=now, + ) + state_changed_event = Event( + EVENT_STATE_CHANGED, + { + "entity_id": "sensor.test", + "old_state": None, + "new_state": mock_state, + }, + EventOrigin.local, + time_fired_timestamp=now.timestamp(), + ) + custom_event = Event( + "custom_event", + {"entity_id": "sensor.custom"}, + EventOrigin.local, + time_fired_timestamp=now.timestamp(), + ) + number_of_migrations = 5 + + def _get_event_id_foreign_keys(): + assert instance.engine is not None + return next( + ( + fk # type: ignore[misc] + for fk in inspect(instance.engine).get_foreign_keys("states") + if fk["constrained_columns"] == ["event_id"] + ), + None, + ) + + def _get_states_index_names(): + with session_scope(hass=hass) as session: + return inspect(session.connection()).get_indexes("states") + + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), + patch.object(core, "StatesMeta", old_db_schema.StatesMeta), + patch.object(core, "EventTypes", old_db_schema.EventTypes), + patch.object(core, "EventData", old_db_schema.EventData), + patch.object(core, "States", old_db_schema.States), + patch.object(core, "Events", old_db_schema.Events), + patch(CREATE_ENGINE_TARGET, new=_create_engine_test), + patch("homeassistant.components.recorder.Recorder._post_migrate_entity_ids"), + patch( + "homeassistant.components.recorder.migration.cleanup_legacy_states_event_ids" + ), + ): + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): + await hass.async_block_till_done() + await async_wait_recording_done(hass) + await async_wait_recording_done(hass) + + def _add_data(): + with session_scope(hass=hass) as session: + session.add(old_db_schema.Events.from_event(custom_event)) + session.add(old_db_schema.States.from_event(state_changed_event)) + + await instance.async_add_executor_job(_add_data) + await hass.async_block_till_done() + await instance.async_block_till_done() + + await instance.async_add_executor_job( + migration._drop_index, + instance.get_session, + "states", + "ix_states_event_id", + ) + + states_indexes = await instance.async_add_executor_job( + _get_states_index_names + ) + states_index_names = {index["name"] for index in states_indexes} + assert instance.use_legacy_events_index is True + assert ( + await instance.async_add_executor_job(_get_event_id_foreign_keys) + is not None + ) + + await hass.async_stop() + await hass.async_block_till_done() + + assert "ix_states_entity_id_last_updated_ts" in states_index_names + + # Simulate out of disk space while removing the foreign key from the states table by + # - patching DropConstraint to raise InternalError for MySQL and PostgreSQL + with ( + patch( + "homeassistant.components.recorder.migration.DropConstraint", + side_effect=OperationalError( + None, None, OSError("No space left on device") + ), + ), + ): + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): + await hass.async_block_till_done() + + # We need to wait for all the migration tasks to complete + # before we can check the database. + for _ in range(number_of_migrations): + await instance.async_block_till_done() + await async_wait_recording_done(hass) + + states_indexes = await instance.async_add_executor_job( + _get_states_index_names + ) + states_index_names = {index["name"] for index in states_indexes} + assert instance.use_legacy_events_index is True + assert await instance.async_add_executor_job(_get_event_id_foreign_keys) + + await hass.async_stop() + + # Now run it again to verify the table rebuild tries again + caplog.clear() + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): + await hass.async_block_till_done() + + # We need to wait for all the migration tasks to complete + # before we can check the database. + for _ in range(number_of_migrations): + await instance.async_block_till_done() + await async_wait_recording_done(hass) + + states_indexes = await instance.async_add_executor_job(_get_states_index_names) + states_index_names = {index["name"] for index in states_indexes} + assert instance.use_legacy_events_index is False + assert "ix_states_entity_id_last_updated_ts" not in states_index_names + assert "ix_states_event_id" not in states_index_names + assert await instance.async_add_executor_job(_get_event_id_foreign_keys) is None + + await hass.async_stop() From f8e1c2cfd4bd393a376231e5273ac208bd6929c6 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 9 Aug 2024 16:38:12 +1000 Subject: [PATCH 2099/2411] Rework OAuth in Tesla Fleet (#123324) * Rework Oauth * Improve docstrings * Update homeassistant/components/tesla_fleet/oauth.py Co-authored-by: Martin Hjelmare * review feedback * Add tests for user creds --------- Co-authored-by: Martin Hjelmare --- .../components/tesla_fleet/__init__.py | 7 +- .../tesla_fleet/application_credentials.py | 62 +------------ .../components/tesla_fleet/config_flow.py | 9 +- homeassistant/components/tesla_fleet/const.py | 1 - homeassistant/components/tesla_fleet/oauth.py | 86 +++++++++++++++++++ .../tesla_fleet/test_config_flow.py | 86 ++++++++++++++++++- 6 files changed, 181 insertions(+), 70 deletions(-) create mode 100644 homeassistant/components/tesla_fleet/oauth.py diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 4eac1168674..45657b3d8fb 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -13,7 +13,6 @@ from tesla_fleet_api.exceptions import ( TeslaFleetError, ) -from homeassistant.components.application_credentials import ClientCredential from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant @@ -27,15 +26,15 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo -from .application_credentials import TeslaOAuth2Implementation from .config_flow import OAuth2FlowHandler -from .const import CLIENT_ID, DOMAIN, LOGGER, MODELS, NAME +from .const import DOMAIN, LOGGER, MODELS from .coordinator import ( TeslaFleetEnergySiteInfoCoordinator, TeslaFleetEnergySiteLiveCoordinator, TeslaFleetVehicleDataCoordinator, ) from .models import TeslaFleetData, TeslaFleetEnergyData, TeslaFleetVehicleData +from .oauth import TeslaSystemImplementation PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR] @@ -56,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - OAuth2FlowHandler.async_register_implementation( hass, - TeslaOAuth2Implementation(hass, DOMAIN, ClientCredential(CLIENT_ID, "", NAME)), + TeslaSystemImplementation(hass), ) implementation = await async_get_config_entry_implementation(hass, entry) diff --git a/homeassistant/components/tesla_fleet/application_credentials.py b/homeassistant/components/tesla_fleet/application_credentials.py index 32e16cc9244..0ef38567b65 100644 --- a/homeassistant/components/tesla_fleet/application_credentials.py +++ b/homeassistant/components/tesla_fleet/application_credentials.py @@ -1,72 +1,18 @@ """Application Credentials platform the Tesla Fleet integration.""" -import base64 -import hashlib -import secrets -from typing import Any - -from homeassistant.components.application_credentials import ( - AuthImplementation, - AuthorizationServer, - ClientCredential, -) +from homeassistant.components.application_credentials import ClientCredential from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow -from .const import AUTHORIZE_URL, DOMAIN, SCOPES, TOKEN_URL - -AUTH_SERVER = AuthorizationServer(AUTHORIZE_URL, TOKEN_URL) +from .oauth import TeslaUserImplementation async def async_get_auth_implementation( hass: HomeAssistant, auth_domain: str, credential: ClientCredential ) -> config_entry_oauth2_flow.AbstractOAuth2Implementation: """Return auth implementation.""" - return TeslaOAuth2Implementation( + return TeslaUserImplementation( hass, - DOMAIN, + auth_domain, credential, ) - - -class TeslaOAuth2Implementation(AuthImplementation): - """Tesla Fleet API Open Source Oauth2 implementation.""" - - def __init__( - self, hass: HomeAssistant, domain: str, credential: ClientCredential - ) -> None: - """Initialize local auth implementation.""" - self.hass = hass - self._domain = domain - - # Setup PKCE - self.code_verifier = secrets.token_urlsafe(32) - hashed_verifier = hashlib.sha256(self.code_verifier.encode()).digest() - self.code_challenge = ( - base64.urlsafe_b64encode(hashed_verifier).decode().replace("=", "") - ) - super().__init__( - hass, - domain, - credential, - AUTH_SERVER, - ) - - @property - def extra_authorize_data(self) -> dict[str, Any]: - """Extra data that needs to be appended to the authorize url.""" - return { - "scope": " ".join(SCOPES), - "code_challenge": self.code_challenge, # PKCE - } - - async def async_resolve_external_data(self, external_data: Any) -> dict: - """Resolve the authorization code to tokens.""" - return await self._token_request( - { - "grant_type": "authorization_code", - "code": external_data["code"], - "redirect_uri": external_data["state"]["redirect_uri"], - "code_verifier": self.code_verifier, # PKCE - } - ) diff --git a/homeassistant/components/tesla_fleet/config_flow.py b/homeassistant/components/tesla_fleet/config_flow.py index c09ea78177f..0ffdca1aec6 100644 --- a/homeassistant/components/tesla_fleet/config_flow.py +++ b/homeassistant/components/tesla_fleet/config_flow.py @@ -8,12 +8,11 @@ from typing import Any import jwt -from homeassistant.components.application_credentials import ClientCredential from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.helpers import config_entry_oauth2_flow -from .application_credentials import TeslaOAuth2Implementation -from .const import CLIENT_ID, DOMAIN, LOGGER, NAME +from .const import DOMAIN, LOGGER +from .oauth import TeslaSystemImplementation class OAuth2FlowHandler( @@ -35,9 +34,7 @@ class OAuth2FlowHandler( """Handle a flow start.""" self.async_register_implementation( self.hass, - TeslaOAuth2Implementation( - self.hass, DOMAIN, ClientCredential(CLIENT_ID, "", NAME) - ), + TeslaSystemImplementation(self.hass), ) return await super().async_step_user() diff --git a/homeassistant/components/tesla_fleet/const.py b/homeassistant/components/tesla_fleet/const.py index 9d78716a13e..081225c296c 100644 --- a/homeassistant/components/tesla_fleet/const.py +++ b/homeassistant/components/tesla_fleet/const.py @@ -13,7 +13,6 @@ CONF_REFRESH_TOKEN = "refresh_token" LOGGER = logging.getLogger(__package__) -NAME = "Home Assistant" CLIENT_ID = "71b813eb-4a2e-483a-b831-4dec5cb9bf0d" AUTHORIZE_URL = "https://auth.tesla.com/oauth2/v3/authorize" TOKEN_URL = "https://auth.tesla.com/oauth2/v3/token" diff --git a/homeassistant/components/tesla_fleet/oauth.py b/homeassistant/components/tesla_fleet/oauth.py new file mode 100644 index 00000000000..00976abf56f --- /dev/null +++ b/homeassistant/components/tesla_fleet/oauth.py @@ -0,0 +1,86 @@ +"""Provide oauth implementations for the Tesla Fleet integration.""" + +import base64 +import hashlib +import secrets +from typing import Any + +from homeassistant.components.application_credentials import ( + AuthImplementation, + AuthorizationServer, + ClientCredential, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import AUTHORIZE_URL, CLIENT_ID, DOMAIN, SCOPES, TOKEN_URL + + +class TeslaSystemImplementation(config_entry_oauth2_flow.LocalOAuth2Implementation): + """Tesla Fleet API open source Oauth2 implementation.""" + + code_verifier: str + code_challenge: str + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize open source Oauth2 implementation.""" + + # Setup PKCE + self.code_verifier = secrets.token_urlsafe(32) + hashed_verifier = hashlib.sha256(self.code_verifier.encode()).digest() + self.code_challenge = ( + base64.urlsafe_b64encode(hashed_verifier).decode().replace("=", "") + ) + super().__init__( + hass, + DOMAIN, + CLIENT_ID, + "", + AUTHORIZE_URL, + TOKEN_URL, + ) + + @property + def name(self) -> str: + """Name of the implementation.""" + return "Built-in open source client ID" + + @property + def extra_authorize_data(self) -> dict[str, Any]: + """Extra data that needs to be appended to the authorize url.""" + return { + "scope": " ".join(SCOPES), + "code_challenge": self.code_challenge, # PKCE + } + + async def async_resolve_external_data(self, external_data: Any) -> dict: + """Resolve the authorization code to tokens.""" + return await self._token_request( + { + "grant_type": "authorization_code", + "code": external_data["code"], + "redirect_uri": external_data["state"]["redirect_uri"], + "code_verifier": self.code_verifier, # PKCE + } + ) + + +class TeslaUserImplementation(AuthImplementation): + """Tesla Fleet API user Oauth2 implementation.""" + + def __init__( + self, hass: HomeAssistant, auth_domain: str, credential: ClientCredential + ) -> None: + """Initialize user Oauth2 implementation.""" + + super().__init__( + hass, + auth_domain, + credential, + AuthorizationServer(AUTHORIZE_URL, TOKEN_URL), + ) + + @property + def extra_authorize_data(self) -> dict[str, Any]: + """Extra data that needs to be appended to the authorize url.""" + return {"scope": " ".join(SCOPES)} diff --git a/tests/components/tesla_fleet/test_config_flow.py b/tests/components/tesla_fleet/test_config_flow.py index bd1c7d7c2b8..45dbe6ca598 100644 --- a/tests/components/tesla_fleet/test_config_flow.py +++ b/tests/components/tesla_fleet/test_config_flow.py @@ -5,6 +5,10 @@ from urllib.parse import parse_qs, urlparse import pytest +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.tesla_fleet.const import ( AUTHORIZE_URL, CLIENT_ID, @@ -16,6 +20,7 @@ from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -26,7 +31,7 @@ UNIQUE_ID = "uid" @pytest.fixture -async def access_token(hass: HomeAssistant) -> dict[str, str | list[str]]: +async def access_token(hass: HomeAssistant) -> str: """Return a valid access token.""" return config_entry_oauth2_flow._encode_jwt( hass, @@ -111,6 +116,85 @@ async def test_full_flow( assert result["result"].data["token"]["refresh_token"] == "mock-refresh-token" +@pytest.mark.usefixtures("current_request_with_host") +async def test_full_flow_user_cred( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + access_token, +) -> None: + """Check full flow.""" + + # Create user application credential + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential("user_client_id", "user_client_secret"), + "user_cred", + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"implementation": "user_cred"} + ) + assert result["type"] is FlowResultType.EXTERNAL_STEP + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": REDIRECT, + }, + ) + + assert result["url"].startswith(AUTHORIZE_URL) + parsed_url = urlparse(result["url"]) + parsed_query = parse_qs(parsed_url.query) + assert parsed_query["response_type"][0] == "code" + assert parsed_query["client_id"][0] == "user_client_id" + assert parsed_query["redirect_uri"][0] == REDIRECT + assert parsed_query["state"][0] == state + assert parsed_query["scope"][0] == " ".join(SCOPES) + assert "code_challenge" not in parsed_query # Ensure not a PKCE flow + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.clear_requests() + aioclient_mock.post( + TOKEN_URL, + json={ + "refresh_token": "mock-refresh-token", + "access_token": access_token, + "type": "Bearer", + "expires_in": 60, + }, + ) + with patch( + "homeassistant.components.tesla_fleet.async_setup_entry", return_value=True + ) as mock_setup: + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == UNIQUE_ID + assert "result" in result + assert result["result"].unique_id == UNIQUE_ID + assert "token" in result["result"].data + assert result["result"].data["token"]["access_token"] == access_token + assert result["result"].data["token"]["refresh_token"] == "mock-refresh-token" + + @pytest.mark.usefixtures("current_request_with_host") async def test_reauthentication( hass: HomeAssistant, From 84d6f5ed071b39df6af3dddce29f558b0ef0b5e7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 9 Aug 2024 21:43:02 +1200 Subject: [PATCH 2100/2411] Remove ESPHome legacy entity naming (#123436) * Remove ESPHome legacy entity naming * Update test entity_ids to use sanitized name instead of object_id --- homeassistant/components/esphome/entity.py | 20 +-- .../esphome/test_alarm_control_panel.py | 22 +-- .../components/esphome/test_binary_sensor.py | 10 +- tests/components/esphome/test_button.py | 8 +- tests/components/esphome/test_camera.py | 36 ++--- tests/components/esphome/test_climate.py | 36 ++--- tests/components/esphome/test_cover.py | 24 +-- tests/components/esphome/test_date.py | 6 +- tests/components/esphome/test_datetime.py | 6 +- tests/components/esphome/test_entity.py | 89 ++++------- tests/components/esphome/test_event.py | 2 +- tests/components/esphome/test_fan.py | 46 +++--- tests/components/esphome/test_light.py | 148 +++++++++--------- tests/components/esphome/test_lock.py | 14 +- tests/components/esphome/test_media_player.py | 24 +-- tests/components/esphome/test_number.py | 10 +- tests/components/esphome/test_select.py | 4 +- tests/components/esphome/test_sensor.py | 38 ++--- tests/components/esphome/test_switch.py | 6 +- tests/components/esphome/test_text.py | 8 +- tests/components/esphome/test_time.py | 6 +- tests/components/esphome/test_update.py | 10 +- tests/components/esphome/test_valve.py | 24 +-- 23 files changed, 275 insertions(+), 322 deletions(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 6e02f8de869..5f845f4665b 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -176,6 +176,7 @@ ENTITY_CATEGORIES: EsphomeEnumMapper[EsphomeEntityCategory, EntityCategory | Non class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): """Define a base esphome entity.""" + _attr_has_entity_name = True _attr_should_poll = False _static_info: _InfoT _state: _StateT @@ -200,25 +201,6 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)} ) - # - # If `friendly_name` is set, we use the Friendly naming rules, if - # `friendly_name` is not set we make an exception to the naming rules for - # backwards compatibility and use the Legacy naming rules. - # - # Friendly naming - # - Friendly name is prepended to entity names - # - Device Name is prepended to entity ids - # - Entity id is constructed from device name and object id - # - # Legacy naming - # - Device name is not prepended to entity names - # - Device name is not prepended to entity ids - # - Entity id is constructed from entity name - # - if not device_info.friendly_name: - return - self._attr_has_entity_name = True - self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}" async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/tests/components/esphome/test_alarm_control_panel.py b/tests/components/esphome/test_alarm_control_panel.py index af717ac1b49..33c7be94736 100644 --- a/tests/components/esphome/test_alarm_control_panel.py +++ b/tests/components/esphome/test_alarm_control_panel.py @@ -58,7 +58,7 @@ async def test_generic_alarm_control_panel_requires_code( user_service=user_service, states=states, ) - state = hass.states.get("alarm_control_panel.test_myalarm_control_panel") + state = hass.states.get("alarm_control_panel.test_my_alarm_control_panel") assert state is not None assert state.state == STATE_ALARM_ARMED_AWAY @@ -66,7 +66,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_AWAY, { - ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -80,7 +80,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, { - ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -94,7 +94,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_HOME, { - ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -108,7 +108,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_NIGHT, { - ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -122,7 +122,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_VACATION, { - ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -136,7 +136,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_TRIGGER, { - ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -150,7 +150,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_DISARM, { - ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -193,14 +193,14 @@ async def test_generic_alarm_control_panel_no_code( user_service=user_service, states=states, ) - state = hass.states.get("alarm_control_panel.test_myalarm_control_panel") + state = hass.states.get("alarm_control_panel.test_my_alarm_control_panel") assert state is not None assert state.state == STATE_ALARM_ARMED_AWAY await hass.services.async_call( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_DISARM, - {ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel"}, + {ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel"}, blocking=True, ) mock_client.alarm_control_panel_command.assert_has_calls( @@ -239,6 +239,6 @@ async def test_generic_alarm_control_panel_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("alarm_control_panel.test_myalarm_control_panel") + state = hass.states.get("alarm_control_panel.test_my_alarm_control_panel") assert state is not None assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/test_binary_sensor.py b/tests/components/esphome/test_binary_sensor.py index 3da8a54ff34..c07635eff3b 100644 --- a/tests/components/esphome/test_binary_sensor.py +++ b/tests/components/esphome/test_binary_sensor.py @@ -74,7 +74,7 @@ async def test_binary_sensor_generic_entity( user_service=user_service, states=states, ) - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == hass_state @@ -105,7 +105,7 @@ async def test_status_binary_sensor( user_service=user_service, states=states, ) - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_ON @@ -135,7 +135,7 @@ async def test_binary_sensor_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_UNKNOWN @@ -165,12 +165,12 @@ async def test_binary_sensor_has_state_false( user_service=user_service, states=states, ) - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_UNKNOWN mock_device.set_state(BinarySensorState(key=1, state=True, missing_state=False)) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_ON diff --git a/tests/components/esphome/test_button.py b/tests/components/esphome/test_button.py index 8c120949caa..d3fec2a56d2 100644 --- a/tests/components/esphome/test_button.py +++ b/tests/components/esphome/test_button.py @@ -29,22 +29,22 @@ async def test_button_generic_entity( user_service=user_service, states=states, ) - state = hass.states.get("button.test_mybutton") + state = hass.states.get("button.test_my_button") assert state is not None assert state.state == STATE_UNKNOWN await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.test_mybutton"}, + {ATTR_ENTITY_ID: "button.test_my_button"}, blocking=True, ) mock_client.button_command.assert_has_calls([call(1)]) - state = hass.states.get("button.test_mybutton") + state = hass.states.get("button.test_my_button") assert state is not None assert state.state != STATE_UNKNOWN await mock_device.mock_disconnect(False) - state = hass.states.get("button.test_mybutton") + state = hass.states.get("button.test_my_button") assert state is not None assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/esphome/test_camera.py b/tests/components/esphome/test_camera.py index c6a61cd18e8..680cda00944 100644 --- a/tests/components/esphome/test_camera.py +++ b/tests/components/esphome/test_camera.py @@ -53,7 +53,7 @@ async def test_camera_single_image( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_mycamera") + state = hass.states.get("camera.test_my_camera") assert state is not None assert state.state == STATE_IDLE @@ -63,9 +63,9 @@ async def test_camera_single_image( mock_client.request_single_image = _mock_camera_image client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.test_mycamera") + resp = await client.get("/api/camera_proxy/camera.test_my_camera") await hass.async_block_till_done() - state = hass.states.get("camera.test_mycamera") + state = hass.states.get("camera.test_my_camera") assert state is not None assert state.state == STATE_IDLE @@ -101,15 +101,15 @@ async def test_camera_single_image_unavailable_before_requested( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_mycamera") + state = hass.states.get("camera.test_my_camera") assert state is not None assert state.state == STATE_IDLE await mock_device.mock_disconnect(False) client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.test_mycamera") + resp = await client.get("/api/camera_proxy/camera.test_my_camera") await hass.async_block_till_done() - state = hass.states.get("camera.test_mycamera") + state = hass.states.get("camera.test_my_camera") assert state is not None assert state.state == STATE_UNAVAILABLE @@ -142,7 +142,7 @@ async def test_camera_single_image_unavailable_during_request( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_mycamera") + state = hass.states.get("camera.test_my_camera") assert state is not None assert state.state == STATE_IDLE @@ -152,9 +152,9 @@ async def test_camera_single_image_unavailable_during_request( mock_client.request_single_image = _mock_camera_image client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.test_mycamera") + resp = await client.get("/api/camera_proxy/camera.test_my_camera") await hass.async_block_till_done() - state = hass.states.get("camera.test_mycamera") + state = hass.states.get("camera.test_my_camera") assert state is not None assert state.state == STATE_UNAVAILABLE @@ -187,7 +187,7 @@ async def test_camera_stream( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_mycamera") + state = hass.states.get("camera.test_my_camera") assert state is not None assert state.state == STATE_IDLE remaining_responses = 3 @@ -203,9 +203,9 @@ async def test_camera_stream( mock_client.request_single_image = _mock_camera_image client = await hass_client() - resp = await client.get("/api/camera_proxy_stream/camera.test_mycamera") + resp = await client.get("/api/camera_proxy_stream/camera.test_my_camera") await hass.async_block_till_done() - state = hass.states.get("camera.test_mycamera") + state = hass.states.get("camera.test_my_camera") assert state is not None assert state.state == STATE_IDLE @@ -247,16 +247,16 @@ async def test_camera_stream_unavailable( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_mycamera") + state = hass.states.get("camera.test_my_camera") assert state is not None assert state.state == STATE_IDLE await mock_device.mock_disconnect(False) client = await hass_client() - await client.get("/api/camera_proxy_stream/camera.test_mycamera") + await client.get("/api/camera_proxy_stream/camera.test_my_camera") await hass.async_block_till_done() - state = hass.states.get("camera.test_mycamera") + state = hass.states.get("camera.test_my_camera") assert state is not None assert state.state == STATE_UNAVAILABLE @@ -287,7 +287,7 @@ async def test_camera_stream_with_disconnection( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_mycamera") + state = hass.states.get("camera.test_my_camera") assert state is not None assert state.state == STATE_IDLE remaining_responses = 3 @@ -305,8 +305,8 @@ async def test_camera_stream_with_disconnection( mock_client.request_single_image = _mock_camera_image client = await hass_client() - await client.get("/api/camera_proxy_stream/camera.test_mycamera") + await client.get("/api/camera_proxy_stream/camera.test_my_camera") await hass.async_block_till_done() - state = hass.states.get("camera.test_mycamera") + state = hass.states.get("camera.test_my_camera") assert state is not None assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/esphome/test_climate.py b/tests/components/esphome/test_climate.py index 4ec7fee6447..a573128bef1 100644 --- a/tests/components/esphome/test_climate.py +++ b/tests/components/esphome/test_climate.py @@ -78,14 +78,14 @@ async def test_climate_entity( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_myclimate") + state = hass.states.get("climate.test_my_climate") assert state is not None assert state.state == HVACMode.COOL await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_TEMPERATURE: 25}, + {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_TEMPERATURE: 25}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, target_temperature=25.0)]) @@ -130,14 +130,14 @@ async def test_climate_entity_with_step_and_two_point( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_myclimate") + state = hass.states.get("climate.test_my_climate") assert state is not None assert state.state == HVACMode.COOL await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_TEMPERATURE: 25}, + {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_TEMPERATURE: 25}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, target_temperature=25.0)]) @@ -147,7 +147,7 @@ async def test_climate_entity_with_step_and_two_point( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { - ATTR_ENTITY_ID: "climate.test_myclimate", + ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_HVAC_MODE: HVACMode.AUTO, ATTR_TARGET_TEMP_LOW: 20, ATTR_TARGET_TEMP_HIGH: 30, @@ -209,14 +209,14 @@ async def test_climate_entity_with_step_and_target_temp( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_myclimate") + state = hass.states.get("climate.test_my_climate") assert state is not None assert state.state == HVACMode.COOL await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_TEMPERATURE: 25}, + {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_TEMPERATURE: 25}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, target_temperature=25.0)]) @@ -226,7 +226,7 @@ async def test_climate_entity_with_step_and_target_temp( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { - ATTR_ENTITY_ID: "climate.test_myclimate", + ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_HVAC_MODE: HVACMode.AUTO, ATTR_TARGET_TEMP_LOW: 20, ATTR_TARGET_TEMP_HIGH: 30, @@ -249,7 +249,7 @@ async def test_climate_entity_with_step_and_target_temp( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { - ATTR_ENTITY_ID: "climate.test_myclimate", + ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_HVAC_MODE: HVACMode.HEAT, }, blocking=True, @@ -267,7 +267,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_PRESET_MODE: "away"}, + {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_PRESET_MODE: "away"}, blocking=True, ) mock_client.climate_command.assert_has_calls( @@ -283,7 +283,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_PRESET_MODE: "preset1"}, + {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_PRESET_MODE: "preset1"}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, custom_preset="preset1")]) @@ -292,7 +292,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_FAN_MODE: FAN_HIGH}, + {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_FAN_MODE: FAN_HIGH}, blocking=True, ) mock_client.climate_command.assert_has_calls( @@ -303,7 +303,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_FAN_MODE: "fan2"}, + {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_FAN_MODE: "fan2"}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, custom_fan_mode="fan2")]) @@ -312,7 +312,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, - {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_SWING_MODE: SWING_BOTH}, + {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_SWING_MODE: SWING_BOTH}, blocking=True, ) mock_client.climate_command.assert_has_calls( @@ -362,7 +362,7 @@ async def test_climate_entity_with_humidity( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_myclimate") + state = hass.states.get("climate.test_my_climate") assert state is not None assert state.state == HVACMode.AUTO attributes = state.attributes @@ -374,7 +374,7 @@ async def test_climate_entity_with_humidity( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HUMIDITY, - {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_HUMIDITY: 23}, + {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_HUMIDITY: 23}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, target_humidity=23)]) @@ -422,7 +422,7 @@ async def test_climate_entity_with_inf_value( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_myclimate") + state = hass.states.get("climate.test_my_climate") assert state is not None assert state.state == HVACMode.AUTO attributes = state.attributes @@ -484,7 +484,7 @@ async def test_climate_entity_attributes( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_myclimate") + state = hass.states.get("climate.test_my_climate") assert state is not None assert state.state == HVACMode.COOL assert state.attributes == snapshot(name="climate-entity-attributes") diff --git a/tests/components/esphome/test_cover.py b/tests/components/esphome/test_cover.py index b190d287198..59eadb3cfd9 100644 --- a/tests/components/esphome/test_cover.py +++ b/tests/components/esphome/test_cover.py @@ -72,7 +72,7 @@ async def test_cover_entity( user_service=user_service, states=states, ) - state = hass.states.get("cover.test_mycover") + state = hass.states.get("cover.test_my_cover") assert state is not None assert state.state == STATE_OPENING assert state.attributes[ATTR_CURRENT_POSITION] == 50 @@ -81,7 +81,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.test_mycover"}, + {ATTR_ENTITY_ID: "cover.test_my_cover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, position=0.0)]) @@ -90,7 +90,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.test_mycover"}, + {ATTR_ENTITY_ID: "cover.test_my_cover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, position=1.0)]) @@ -99,7 +99,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.test_mycover", ATTR_POSITION: 50}, + {ATTR_ENTITY_ID: "cover.test_my_cover", ATTR_POSITION: 50}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, position=0.5)]) @@ -108,7 +108,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.test_mycover"}, + {ATTR_ENTITY_ID: "cover.test_my_cover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, stop=True)]) @@ -117,7 +117,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER_TILT, - {ATTR_ENTITY_ID: "cover.test_mycover"}, + {ATTR_ENTITY_ID: "cover.test_my_cover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, tilt=1.0)]) @@ -126,7 +126,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER_TILT, - {ATTR_ENTITY_ID: "cover.test_mycover"}, + {ATTR_ENTITY_ID: "cover.test_my_cover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, tilt=0.0)]) @@ -135,7 +135,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_TILT_POSITION, - {ATTR_ENTITY_ID: "cover.test_mycover", ATTR_TILT_POSITION: 50}, + {ATTR_ENTITY_ID: "cover.test_my_cover", ATTR_TILT_POSITION: 50}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, tilt=0.5)]) @@ -145,7 +145,7 @@ async def test_cover_entity( CoverState(key=1, position=0.0, current_operation=CoverOperation.IDLE) ) await hass.async_block_till_done() - state = hass.states.get("cover.test_mycover") + state = hass.states.get("cover.test_my_cover") assert state is not None assert state.state == STATE_CLOSED @@ -153,7 +153,7 @@ async def test_cover_entity( CoverState(key=1, position=0.5, current_operation=CoverOperation.IS_CLOSING) ) await hass.async_block_till_done() - state = hass.states.get("cover.test_mycover") + state = hass.states.get("cover.test_my_cover") assert state is not None assert state.state == STATE_CLOSING @@ -161,7 +161,7 @@ async def test_cover_entity( CoverState(key=1, position=1.0, current_operation=CoverOperation.IDLE) ) await hass.async_block_till_done() - state = hass.states.get("cover.test_mycover") + state = hass.states.get("cover.test_my_cover") assert state is not None assert state.state == STATE_OPEN @@ -201,7 +201,7 @@ async def test_cover_entity_without_position( user_service=user_service, states=states, ) - state = hass.states.get("cover.test_mycover") + state = hass.states.get("cover.test_my_cover") assert state is not None assert state.state == STATE_OPENING assert ATTR_CURRENT_TILT_POSITION not in state.attributes diff --git a/tests/components/esphome/test_date.py b/tests/components/esphome/test_date.py index 2deb92775fb..3b620a30461 100644 --- a/tests/components/esphome/test_date.py +++ b/tests/components/esphome/test_date.py @@ -35,14 +35,14 @@ async def test_generic_date_entity( user_service=user_service, states=states, ) - state = hass.states.get("date.test_mydate") + state = hass.states.get("date.test_my_date") assert state is not None assert state.state == "2024-12-31" await hass.services.async_call( DATE_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "date.test_mydate", ATTR_DATE: "1999-01-01"}, + {ATTR_ENTITY_ID: "date.test_my_date", ATTR_DATE: "1999-01-01"}, blocking=True, ) mock_client.date_command.assert_has_calls([call(1, 1999, 1, 1)]) @@ -71,6 +71,6 @@ async def test_generic_date_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("date.test_mydate") + state = hass.states.get("date.test_my_date") assert state is not None assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/test_datetime.py b/tests/components/esphome/test_datetime.py index 3bdc196de95..95f0b7584ad 100644 --- a/tests/components/esphome/test_datetime.py +++ b/tests/components/esphome/test_datetime.py @@ -35,7 +35,7 @@ async def test_generic_datetime_entity( user_service=user_service, states=states, ) - state = hass.states.get("datetime.test_mydatetime") + state = hass.states.get("datetime.test_my_datetime") assert state is not None assert state.state == "2024-04-16T12:34:56+00:00" @@ -43,7 +43,7 @@ async def test_generic_datetime_entity( DATETIME_DOMAIN, SERVICE_SET_VALUE, { - ATTR_ENTITY_ID: "datetime.test_mydatetime", + ATTR_ENTITY_ID: "datetime.test_my_datetime", ATTR_DATETIME: "2000-01-01T01:23:45+00:00", }, blocking=True, @@ -74,6 +74,6 @@ async def test_generic_datetime_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("datetime.test_mydatetime") + state = hass.states.get("datetime.test_my_datetime") assert state is not None assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index 296d61b664d..64b8d6101ac 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -69,10 +69,10 @@ async def test_entities_removed( entry = mock_device.entry entry_id = entry.entry_id storage_key = f"esphome.{entry_id}" - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_ON - state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") assert state is not None assert state.state == STATE_ON @@ -81,13 +81,13 @@ async def test_entities_removed( assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 2 - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.attributes[ATTR_RESTORED] is True - state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") assert state is not None reg_entry = entity_registry.async_get( - "binary_sensor.test_mybinary_sensor_to_be_removed" + "binary_sensor.test_my_binary_sensor_to_be_removed" ) assert reg_entry is not None assert state.attributes[ATTR_RESTORED] is True @@ -111,13 +111,13 @@ async def test_entities_removed( entry=entry, ) assert mock_device.entry.entry_id == entry_id - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_ON - state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") assert state is None reg_entry = entity_registry.async_get( - "binary_sensor.test_mybinary_sensor_to_be_removed" + "binary_sensor.test_my_binary_sensor_to_be_removed" ) assert reg_entry is None await hass.config_entries.async_unload(entry.entry_id) @@ -164,15 +164,15 @@ async def test_entities_removed_after_reload( entry = mock_device.entry entry_id = entry.entry_id storage_key = f"esphome.{entry_id}" - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_ON - state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") assert state is not None assert state.state == STATE_ON reg_entry = entity_registry.async_get( - "binary_sensor.test_mybinary_sensor_to_be_removed" + "binary_sensor.test_my_binary_sensor_to_be_removed" ) assert reg_entry is not None @@ -181,15 +181,15 @@ async def test_entities_removed_after_reload( assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 2 - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.attributes[ATTR_RESTORED] is True - state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") assert state is not None assert state.attributes[ATTR_RESTORED] is True reg_entry = entity_registry.async_get( - "binary_sensor.test_mybinary_sensor_to_be_removed" + "binary_sensor.test_my_binary_sensor_to_be_removed" ) assert reg_entry is not None @@ -198,14 +198,14 @@ async def test_entities_removed_after_reload( assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 2 - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert ATTR_RESTORED not in state.attributes - state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") assert state is not None assert ATTR_RESTORED not in state.attributes reg_entry = entity_registry.async_get( - "binary_sensor.test_mybinary_sensor_to_be_removed" + "binary_sensor.test_my_binary_sensor_to_be_removed" ) assert reg_entry is not None @@ -236,23 +236,23 @@ async def test_entities_removed_after_reload( on_future.set_result(None) async_track_state_change_event( - hass, ["binary_sensor.test_mybinary_sensor"], _async_wait_for_on + hass, ["binary_sensor.test_my_binary_sensor"], _async_wait_for_on ) await hass.async_block_till_done() async with asyncio.timeout(2): await on_future assert mock_device.entry.entry_id == entry_id - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_ON - state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") assert state is None await hass.async_block_till_done() reg_entry = entity_registry.async_get( - "binary_sensor.test_mybinary_sensor_to_be_removed" + "binary_sensor.test_my_binary_sensor_to_be_removed" ) assert reg_entry is None assert await hass.config_entries.async_unload(entry.entry_id) @@ -260,35 +260,6 @@ async def test_entities_removed_after_reload( assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 1 -async def test_entity_info_object_ids( - hass: HomeAssistant, - mock_client: APIClient, - mock_esphome_device: Callable[ - [APIClient, list[EntityInfo], list[UserService], list[EntityState]], - Awaitable[MockESPHomeDevice], - ], -) -> None: - """Test how object ids affect entity id.""" - entity_info = [ - BinarySensorInfo( - object_id="object_id_is_used", - key=1, - name="my binary_sensor", - unique_id="my_binary_sensor", - ) - ] - states = [] - user_service = [] - await mock_esphome_device( - mock_client=mock_client, - entity_info=entity_info, - user_service=user_service, - states=states, - ) - state = hass.states.get("binary_sensor.test_object_id_is_used") - assert state is not None - - async def test_deep_sleep_device( hass: HomeAssistant, mock_client: APIClient, @@ -326,7 +297,7 @@ async def test_deep_sleep_device( states=states, device_info={"has_deep_sleep": True}, ) - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_ON state = hass.states.get("sensor.test_my_sensor") @@ -335,7 +306,7 @@ async def test_deep_sleep_device( await mock_device.mock_disconnect(False) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_UNAVAILABLE state = hass.states.get("sensor.test_my_sensor") @@ -345,7 +316,7 @@ async def test_deep_sleep_device( await mock_device.mock_connect() await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_ON state = hass.states.get("sensor.test_my_sensor") @@ -359,7 +330,7 @@ async def test_deep_sleep_device( mock_device.set_state(BinarySensorState(key=1, state=False, missing_state=False)) mock_device.set_state(SensorState(key=3, state=56, missing_state=False)) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_OFF state = hass.states.get("sensor.test_my_sensor") @@ -368,7 +339,7 @@ async def test_deep_sleep_device( await mock_device.mock_disconnect(True) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_OFF state = hass.states.get("sensor.test_my_sensor") @@ -379,7 +350,7 @@ async def test_deep_sleep_device( await hass.async_block_till_done() await mock_device.mock_disconnect(False) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_UNAVAILABLE state = hass.states.get("sensor.test_my_sensor") @@ -388,14 +359,14 @@ async def test_deep_sleep_device( await mock_device.mock_connect() await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_ON hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() # Verify we do not dispatch any more state updates or # availability updates after the stop event is fired - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_ON @@ -430,6 +401,6 @@ async def test_esphome_device_without_friendly_name( states=states, device_info={"friendly_name": None}, ) - state = hass.states.get("binary_sensor.my_binary_sensor") + state = hass.states.get("binary_sensor.test_my_binary_sensor") assert state is not None assert state.state == STATE_ON diff --git a/tests/components/esphome/test_event.py b/tests/components/esphome/test_event.py index c17dc4d98a9..2daba94a3ca 100644 --- a/tests/components/esphome/test_event.py +++ b/tests/components/esphome/test_event.py @@ -32,7 +32,7 @@ async def test_generic_event_entity( user_service=user_service, states=states, ) - state = hass.states.get("event.test_myevent") + state = hass.states.get("event.test_my_event") assert state is not None assert state.state == "2024-04-24T00:00:00.000+00:00" assert state.attributes["event_type"] == "type1" diff --git a/tests/components/esphome/test_fan.py b/tests/components/esphome/test_fan.py index 064b37b1ec1..9aca36d79a4 100644 --- a/tests/components/esphome/test_fan.py +++ b/tests/components/esphome/test_fan.py @@ -62,14 +62,14 @@ async def test_fan_entity_with_all_features_old_api( user_service=user_service, states=states, ) - state = hass.states.get("fan.test_myfan") + state = hass.states.get("fan.test_my_fan") assert state is not None assert state.state == STATE_ON await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 20}, + {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 20}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -80,7 +80,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 50}, + {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 50}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -91,7 +91,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_DECREASE_SPEED, - {ATTR_ENTITY_ID: "fan.test_myfan"}, + {ATTR_ENTITY_ID: "fan.test_my_fan"}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -102,7 +102,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_INCREASE_SPEED, - {ATTR_ENTITY_ID: "fan.test_myfan"}, + {ATTR_ENTITY_ID: "fan.test_my_fan"}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -113,7 +113,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "fan.test_myfan"}, + {ATTR_ENTITY_ID: "fan.test_my_fan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=False)]) @@ -122,7 +122,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 100}, + {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 100}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -166,14 +166,14 @@ async def test_fan_entity_with_all_features_new_api( user_service=user_service, states=states, ) - state = hass.states.get("fan.test_myfan") + state = hass.states.get("fan.test_my_fan") assert state is not None assert state.state == STATE_ON await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 20}, + {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 20}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=1, state=True)]) @@ -182,7 +182,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 50}, + {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 50}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=2, state=True)]) @@ -191,7 +191,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_DECREASE_SPEED, - {ATTR_ENTITY_ID: "fan.test_myfan"}, + {ATTR_ENTITY_ID: "fan.test_my_fan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=2, state=True)]) @@ -200,7 +200,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_INCREASE_SPEED, - {ATTR_ENTITY_ID: "fan.test_myfan"}, + {ATTR_ENTITY_ID: "fan.test_my_fan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=4, state=True)]) @@ -209,7 +209,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "fan.test_myfan"}, + {ATTR_ENTITY_ID: "fan.test_my_fan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=False)]) @@ -218,7 +218,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 100}, + {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 100}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=4, state=True)]) @@ -227,7 +227,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 0}, + {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 0}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=False)]) @@ -236,7 +236,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_OSCILLATE, - {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_OSCILLATING: True}, + {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_OSCILLATING: True}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, oscillating=True)]) @@ -245,7 +245,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_OSCILLATE, - {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_OSCILLATING: False}, + {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_OSCILLATING: False}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, oscillating=False)]) @@ -254,7 +254,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_DIRECTION, - {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_DIRECTION: "forward"}, + {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_DIRECTION: "forward"}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -265,7 +265,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_DIRECTION, - {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_DIRECTION: "reverse"}, + {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_DIRECTION: "reverse"}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -276,7 +276,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PRESET_MODE: "Preset1"}, + {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PRESET_MODE: "Preset1"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, preset_mode="Preset1")]) @@ -308,14 +308,14 @@ async def test_fan_entity_with_no_features_new_api( user_service=user_service, states=states, ) - state = hass.states.get("fan.test_myfan") + state = hass.states.get("fan.test_my_fan") assert state is not None assert state.state == STATE_ON await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_myfan"}, + {ATTR_ENTITY_ID: "fan.test_my_fan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=True)]) @@ -324,7 +324,7 @@ async def test_fan_entity_with_no_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "fan.test_myfan"}, + {ATTR_ENTITY_ID: "fan.test_my_fan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=False)]) diff --git a/tests/components/esphome/test_light.py b/tests/components/esphome/test_light.py index 2324c73b16f..5dc563d9991 100644 --- a/tests/components/esphome/test_light.py +++ b/tests/components/esphome/test_light.py @@ -65,14 +65,14 @@ async def test_light_on_off( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -105,14 +105,14 @@ async def test_light_brightness( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -123,7 +123,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -141,7 +141,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_TRANSITION: 2}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_TRANSITION: 2}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -152,7 +152,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_FLASH: FLASH_LONG}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_FLASH: FLASH_LONG}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -163,7 +163,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_TRANSITION: 2}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_TRANSITION: 2}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -181,7 +181,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_FLASH: FLASH_SHORT}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_FLASH: FLASH_SHORT}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -223,14 +223,14 @@ async def test_light_brightness_on_off( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -248,7 +248,7 @@ async def test_light_brightness_on_off( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -293,14 +293,14 @@ async def test_light_legacy_white_converted_to_brightness( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -351,7 +351,7 @@ async def test_light_legacy_white_with_rgb( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ @@ -362,7 +362,7 @@ async def test_light_legacy_white_with_rgb( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_WHITE: 60}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_WHITE: 60}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -405,14 +405,14 @@ async def test_light_brightness_on_off_with_unknown_color_mode( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -431,7 +431,7 @@ async def test_light_brightness_on_off_with_unknown_color_mode( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -476,14 +476,14 @@ async def test_light_on_and_brightness( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -526,14 +526,14 @@ async def test_rgb_color_temp_light( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -551,7 +551,7 @@ async def test_rgb_color_temp_light( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -570,7 +570,7 @@ async def test_rgb_color_temp_light( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 2500}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_COLOR_TEMP_KELVIN: 2500}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -614,14 +614,14 @@ async def test_light_rgb( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -640,7 +640,7 @@ async def test_light_rgb( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -661,7 +661,7 @@ async def test_light_rgb( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_mylight", + ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127, ATTR_HS_COLOR: (100, 100), }, @@ -686,7 +686,7 @@ async def test_light_rgb( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -746,7 +746,7 @@ async def test_light_rgbw( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGBW] @@ -755,7 +755,7 @@ async def test_light_rgbw( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -775,7 +775,7 @@ async def test_light_rgbw( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -797,7 +797,7 @@ async def test_light_rgbw( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_mylight", + ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127, ATTR_HS_COLOR: (100, 100), }, @@ -824,7 +824,7 @@ async def test_light_rgbw( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -847,7 +847,7 @@ async def test_light_rgbw( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -917,7 +917,7 @@ async def test_light_rgbww_with_cold_warm_white_support( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGBWW] @@ -927,7 +927,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -949,7 +949,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -973,7 +973,7 @@ async def test_light_rgbww_with_cold_warm_white_support( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_mylight", + ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127, ATTR_HS_COLOR: (100, 100), }, @@ -1003,7 +1003,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1029,7 +1029,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1056,7 +1056,7 @@ async def test_light_rgbww_with_cold_warm_white_support( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_mylight", + ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGBWW_COLOR: (255, 255, 255, 255, 255), }, blocking=True, @@ -1084,7 +1084,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 2500}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_COLOR_TEMP_KELVIN: 2500}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1154,7 +1154,7 @@ async def test_light_rgbww_without_cold_warm_white_support( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGBWW] @@ -1164,7 +1164,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1185,7 +1185,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1208,7 +1208,7 @@ async def test_light_rgbww_without_cold_warm_white_support( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_mylight", + ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127, ATTR_HS_COLOR: (100, 100), }, @@ -1237,7 +1237,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1262,7 +1262,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1288,7 +1288,7 @@ async def test_light_rgbww_without_cold_warm_white_support( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_mylight", + ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGBWW_COLOR: (255, 255, 255, 255, 255), }, blocking=True, @@ -1315,7 +1315,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 2500}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_COLOR_TEMP_KELVIN: 2500}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1374,7 +1374,7 @@ async def test_light_color_temp( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON attributes = state.attributes @@ -1387,7 +1387,7 @@ async def test_light_color_temp( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1406,7 +1406,7 @@ async def test_light_color_temp( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls([call(key=1, state=False)]) @@ -1449,7 +1449,7 @@ async def test_light_color_temp_no_mireds_set( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON attributes = state.attributes @@ -1462,7 +1462,7 @@ async def test_light_color_temp_no_mireds_set( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1481,7 +1481,7 @@ async def test_light_color_temp_no_mireds_set( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 6000}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_COLOR_TEMP_KELVIN: 6000}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1501,7 +1501,7 @@ async def test_light_color_temp_no_mireds_set( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls([call(key=1, state=False)]) @@ -1551,7 +1551,7 @@ async def test_light_color_temp_legacy( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON attributes = state.attributes @@ -1566,7 +1566,7 @@ async def test_light_color_temp_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1585,7 +1585,7 @@ async def test_light_color_temp_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls([call(key=1, state=False)]) @@ -1637,7 +1637,7 @@ async def test_light_rgb_legacy( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON attributes = state.attributes @@ -1647,7 +1647,7 @@ async def test_light_rgb_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1663,7 +1663,7 @@ async def test_light_rgb_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls([call(key=1, state=False)]) @@ -1672,7 +1672,7 @@ async def test_light_rgb_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1715,7 +1715,7 @@ async def test_light_effects( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_EFFECT_LIST] == ["effect1", "effect2"] @@ -1723,7 +1723,7 @@ async def test_light_effects( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_EFFECT: "effect1"}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_EFFECT: "effect1"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1782,7 +1782,7 @@ async def test_only_cold_warm_white_support( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.COLOR_TEMP] @@ -1791,7 +1791,7 @@ async def test_only_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1802,7 +1802,7 @@ async def test_only_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1820,7 +1820,7 @@ async def test_only_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 2500}, + {ATTR_ENTITY_ID: "light.test_my_light", ATTR_COLOR_TEMP_KELVIN: 2500}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1861,7 +1861,7 @@ async def test_light_no_color_modes( user_service=user_service, states=states, ) - state = hass.states.get("light.test_mylight") + state = hass.states.get("light.test_my_light") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.ONOFF] @@ -1869,7 +1869,7 @@ async def test_light_no_color_modes( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_mylight"}, + {ATTR_ENTITY_ID: "light.test_my_light"}, blocking=True, ) mock_client.light_command.assert_has_calls([call(key=1, state=True, color_mode=0)]) diff --git a/tests/components/esphome/test_lock.py b/tests/components/esphome/test_lock.py index 82c24b59a2c..259e990c5f7 100644 --- a/tests/components/esphome/test_lock.py +++ b/tests/components/esphome/test_lock.py @@ -39,14 +39,14 @@ async def test_lock_entity_no_open( user_service=user_service, states=states, ) - state = hass.states.get("lock.test_mylock") + state = hass.states.get("lock.test_my_lock") assert state is not None assert state.state == STATE_UNLOCKING await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, - {ATTR_ENTITY_ID: "lock.test_mylock"}, + {ATTR_ENTITY_ID: "lock.test_my_lock"}, blocking=True, ) mock_client.lock_command.assert_has_calls([call(1, LockCommand.LOCK)]) @@ -73,7 +73,7 @@ async def test_lock_entity_start_locked( user_service=user_service, states=states, ) - state = hass.states.get("lock.test_mylock") + state = hass.states.get("lock.test_my_lock") assert state is not None assert state.state == STATE_LOCKED @@ -100,14 +100,14 @@ async def test_lock_entity_supports_open( user_service=user_service, states=states, ) - state = hass.states.get("lock.test_mylock") + state = hass.states.get("lock.test_my_lock") assert state is not None assert state.state == STATE_LOCKING await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, - {ATTR_ENTITY_ID: "lock.test_mylock"}, + {ATTR_ENTITY_ID: "lock.test_my_lock"}, blocking=True, ) mock_client.lock_command.assert_has_calls([call(1, LockCommand.LOCK)]) @@ -116,7 +116,7 @@ async def test_lock_entity_supports_open( await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, - {ATTR_ENTITY_ID: "lock.test_mylock"}, + {ATTR_ENTITY_ID: "lock.test_my_lock"}, blocking=True, ) mock_client.lock_command.assert_has_calls([call(1, LockCommand.UNLOCK, None)]) @@ -125,7 +125,7 @@ async def test_lock_entity_supports_open( await hass.services.async_call( LOCK_DOMAIN, SERVICE_OPEN, - {ATTR_ENTITY_ID: "lock.test_mylock"}, + {ATTR_ENTITY_ID: "lock.test_my_lock"}, blocking=True, ) mock_client.lock_command.assert_has_calls([call(1, LockCommand.OPEN)]) diff --git a/tests/components/esphome/test_media_player.py b/tests/components/esphome/test_media_player.py index 3879129ccb6..c9fcecb5d55 100644 --- a/tests/components/esphome/test_media_player.py +++ b/tests/components/esphome/test_media_player.py @@ -62,7 +62,7 @@ async def test_media_player_entity( user_service=user_service, states=states, ) - state = hass.states.get("media_player.test_mymedia_player") + state = hass.states.get("media_player.test_my_media_player") assert state is not None assert state.state == "paused" @@ -70,7 +70,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_MUTE, { - ATTR_ENTITY_ID: "media_player.test_mymedia_player", + ATTR_ENTITY_ID: "media_player.test_my_media_player", ATTR_MEDIA_VOLUME_MUTED: True, }, blocking=True, @@ -84,7 +84,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_MUTE, { - ATTR_ENTITY_ID: "media_player.test_mymedia_player", + ATTR_ENTITY_ID: "media_player.test_my_media_player", ATTR_MEDIA_VOLUME_MUTED: True, }, blocking=True, @@ -98,7 +98,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_SET, { - ATTR_ENTITY_ID: "media_player.test_mymedia_player", + ATTR_ENTITY_ID: "media_player.test_my_media_player", ATTR_MEDIA_VOLUME_LEVEL: 0.5, }, blocking=True, @@ -110,7 +110,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PAUSE, { - ATTR_ENTITY_ID: "media_player.test_mymedia_player", + ATTR_ENTITY_ID: "media_player.test_my_media_player", }, blocking=True, ) @@ -123,7 +123,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PLAY, { - ATTR_ENTITY_ID: "media_player.test_mymedia_player", + ATTR_ENTITY_ID: "media_player.test_my_media_player", }, blocking=True, ) @@ -136,7 +136,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_STOP, { - ATTR_ENTITY_ID: "media_player.test_mymedia_player", + ATTR_ENTITY_ID: "media_player.test_my_media_player", }, blocking=True, ) @@ -207,7 +207,7 @@ async def test_media_player_entity_with_source( user_service=user_service, states=states, ) - state = hass.states.get("media_player.test_mymedia_player") + state = hass.states.get("media_player.test_my_media_player") assert state is not None assert state.state == "playing" @@ -216,7 +216,7 @@ async def test_media_player_entity_with_source( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { - ATTR_ENTITY_ID: "media_player.test_mymedia_player", + ATTR_ENTITY_ID: "media_player.test_my_media_player", ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: "media-source://local/xz", }, @@ -240,7 +240,7 @@ async def test_media_player_entity_with_source( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { - ATTR_ENTITY_ID: "media_player.test_mymedia_player", + ATTR_ENTITY_ID: "media_player.test_my_media_player", ATTR_MEDIA_CONTENT_TYPE: "audio/mp3", ATTR_MEDIA_CONTENT_ID: "media-source://local/xy", }, @@ -256,7 +256,7 @@ async def test_media_player_entity_with_source( { "id": 1, "type": "media_player/browse_media", - "entity_id": "media_player.test_mymedia_player", + "entity_id": "media_player.test_my_media_player", } ) response = await client.receive_json() @@ -266,7 +266,7 @@ async def test_media_player_entity_with_source( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { - ATTR_ENTITY_ID: "media_player.test_mymedia_player", + ATTR_ENTITY_ID: "media_player.test_my_media_player", ATTR_MEDIA_CONTENT_TYPE: MediaType.URL, ATTR_MEDIA_CONTENT_ID: "media-source://tts?message=hello", ATTR_MEDIA_ANNOUNCE: True, diff --git a/tests/components/esphome/test_number.py b/tests/components/esphome/test_number.py index 557425052f3..91a21e670f5 100644 --- a/tests/components/esphome/test_number.py +++ b/tests/components/esphome/test_number.py @@ -48,14 +48,14 @@ async def test_generic_number_entity( user_service=user_service, states=states, ) - state = hass.states.get("number.test_mynumber") + state = hass.states.get("number.test_my_number") assert state is not None assert state.state == "50" await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "number.test_mynumber", ATTR_VALUE: 50}, + {ATTR_ENTITY_ID: "number.test_my_number", ATTR_VALUE: 50}, blocking=True, ) mock_client.number_command.assert_has_calls([call(1, 50)]) @@ -89,7 +89,7 @@ async def test_generic_number_nan( user_service=user_service, states=states, ) - state = hass.states.get("number.test_mynumber") + state = hass.states.get("number.test_my_number") assert state is not None assert state.state == STATE_UNKNOWN @@ -121,7 +121,7 @@ async def test_generic_number_with_unit_of_measurement_as_empty_string( user_service=user_service, states=states, ) - state = hass.states.get("number.test_mynumber") + state = hass.states.get("number.test_my_number") assert state is not None assert state.state == "42" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -160,7 +160,7 @@ async def test_generic_number_entity_set_when_disconnected( await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "number.test_mynumber", ATTR_VALUE: 20}, + {ATTR_ENTITY_ID: "number.test_my_number", ATTR_VALUE: 20}, blocking=True, ) mock_client.number_command.reset_mock() diff --git a/tests/components/esphome/test_select.py b/tests/components/esphome/test_select.py index a433b1b0ab0..8df898ea3cf 100644 --- a/tests/components/esphome/test_select.py +++ b/tests/components/esphome/test_select.py @@ -59,14 +59,14 @@ async def test_select_generic_entity( user_service=user_service, states=states, ) - state = hass.states.get("select.test_myselect") + state = hass.states.get("select.test_my_select") assert state is not None assert state.state == "a" await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: "select.test_myselect", ATTR_OPTION: "b"}, + {ATTR_ENTITY_ID: "select.test_my_select", ATTR_OPTION: "b"}, blocking=True, ) mock_client.select_command.assert_has_calls([call(1, "b")]) diff --git a/tests/components/esphome/test_sensor.py b/tests/components/esphome/test_sensor.py index 76f71b53167..5f68dbb8660 100644 --- a/tests/components/esphome/test_sensor.py +++ b/tests/components/esphome/test_sensor.py @@ -62,35 +62,35 @@ async def test_generic_numeric_sensor( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "50" # Test updating state mock_device.set_state(SensorState(key=1, state=60)) await hass.async_block_till_done() - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "60" # Test sending the same state again mock_device.set_state(SensorState(key=1, state=60)) await hass.async_block_till_done() - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "60" # Test we can still update after the same state mock_device.set_state(SensorState(key=1, state=70)) await hass.async_block_till_done() - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "70" # Test invalid data from the underlying api does not crash us mock_device.set_state(SensorState(key=1, state=object())) await hass.async_block_till_done() - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "70" @@ -120,11 +120,11 @@ async def test_generic_numeric_sensor_with_entity_category_and_icon( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "50" assert state.attributes[ATTR_ICON] == "mdi:leaf" - entry = entity_registry.async_get("sensor.test_mysensor") + entry = entity_registry.async_get("sensor.test_my_sensor") assert entry is not None # Note that ESPHome includes the EntityInfo type in the unique id # as this is not a 1:1 mapping to the entity platform (ie. text_sensor) @@ -158,11 +158,11 @@ async def test_generic_numeric_sensor_state_class_measurement( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "50" assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT - entry = entity_registry.async_get("sensor.test_mysensor") + entry = entity_registry.async_get("sensor.test_my_sensor") assert entry is not None # Note that ESPHome includes the EntityInfo type in the unique id # as this is not a 1:1 mapping to the entity platform (ie. text_sensor) @@ -193,7 +193,7 @@ async def test_generic_numeric_sensor_device_class_timestamp( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "2023-06-22T18:43:52+00:00" @@ -222,7 +222,7 @@ async def test_generic_numeric_sensor_legacy_last_reset_convert( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "50" assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL_INCREASING @@ -248,7 +248,7 @@ async def test_generic_numeric_sensor_no_state( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == STATE_UNKNOWN @@ -273,7 +273,7 @@ async def test_generic_numeric_sensor_nan_state( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == STATE_UNKNOWN @@ -298,7 +298,7 @@ async def test_generic_numeric_sensor_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == STATE_UNKNOWN @@ -325,7 +325,7 @@ async def test_generic_text_sensor( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "i am a teapot" @@ -350,7 +350,7 @@ async def test_generic_text_sensor_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == STATE_UNKNOWN @@ -378,7 +378,7 @@ async def test_generic_text_sensor_device_class_timestamp( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "2023-06-22T18:43:52+00:00" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TIMESTAMP @@ -407,7 +407,7 @@ async def test_generic_text_sensor_device_class_date( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "2023-06-22" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.DATE @@ -434,7 +434,7 @@ async def test_generic_numeric_sensor_empty_string_uom( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "123" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes diff --git a/tests/components/esphome/test_switch.py b/tests/components/esphome/test_switch.py index 561ac0b369f..799290c931a 100644 --- a/tests/components/esphome/test_switch.py +++ b/tests/components/esphome/test_switch.py @@ -33,14 +33,14 @@ async def test_switch_generic_entity( user_service=user_service, states=states, ) - state = hass.states.get("switch.test_myswitch") + state = hass.states.get("switch.test_my_switch") assert state is not None assert state.state == STATE_ON await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.test_myswitch"}, + {ATTR_ENTITY_ID: "switch.test_my_switch"}, blocking=True, ) mock_client.switch_command.assert_has_calls([call(1, True)]) @@ -48,7 +48,7 @@ async def test_switch_generic_entity( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_myswitch"}, + {ATTR_ENTITY_ID: "switch.test_my_switch"}, blocking=True, ) mock_client.switch_command.assert_has_calls([call(1, False)]) diff --git a/tests/components/esphome/test_text.py b/tests/components/esphome/test_text.py index 07157d98ac6..e2b459be2d2 100644 --- a/tests/components/esphome/test_text.py +++ b/tests/components/esphome/test_text.py @@ -39,14 +39,14 @@ async def test_generic_text_entity( user_service=user_service, states=states, ) - state = hass.states.get("text.test_mytext") + state = hass.states.get("text.test_my_text") assert state is not None assert state.state == "hello world" await hass.services.async_call( TEXT_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "text.test_mytext", ATTR_VALUE: "goodbye"}, + {ATTR_ENTITY_ID: "text.test_my_text", ATTR_VALUE: "goodbye"}, blocking=True, ) mock_client.text_command.assert_has_calls([call(1, "goodbye")]) @@ -79,7 +79,7 @@ async def test_generic_text_entity_no_state( user_service=user_service, states=states, ) - state = hass.states.get("text.test_mytext") + state = hass.states.get("text.test_my_text") assert state is not None assert state.state == STATE_UNKNOWN @@ -110,6 +110,6 @@ async def test_generic_text_entity_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("text.test_mytext") + state = hass.states.get("text.test_my_text") assert state is not None assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/test_time.py b/tests/components/esphome/test_time.py index aaa18c77a47..5277ca82e34 100644 --- a/tests/components/esphome/test_time.py +++ b/tests/components/esphome/test_time.py @@ -35,14 +35,14 @@ async def test_generic_time_entity( user_service=user_service, states=states, ) - state = hass.states.get("time.test_mytime") + state = hass.states.get("time.test_my_time") assert state is not None assert state.state == "12:34:56" await hass.services.async_call( TIME_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "time.test_mytime", ATTR_TIME: "01:23:45"}, + {ATTR_ENTITY_ID: "time.test_my_time", ATTR_TIME: "01:23:45"}, blocking=True, ) mock_client.time_command.assert_has_calls([call(1, 1, 23, 45)]) @@ -71,6 +71,6 @@ async def test_generic_time_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("time.test_mytime") + state = hass.states.get("time.test_my_time") assert state is not None assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 83e89b1de00..ad638add0a0 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -464,7 +464,7 @@ async def test_generic_device_update_entity( user_service=user_service, states=states, ) - state = hass.states.get("update.test_myupdate") + state = hass.states.get("update.test_my_update") assert state is not None assert state.state == STATE_OFF @@ -503,14 +503,14 @@ async def test_generic_device_update_entity_has_update( user_service=user_service, states=states, ) - state = hass.states.get("update.test_myupdate") + state = hass.states.get("update.test_my_update") assert state is not None assert state.state == STATE_ON await hass.services.async_call( UPDATE_DOMAIN, SERVICE_INSTALL, - {ATTR_ENTITY_ID: "update.test_myupdate"}, + {ATTR_ENTITY_ID: "update.test_my_update"}, blocking=True, ) @@ -528,7 +528,7 @@ async def test_generic_device_update_entity_has_update( ) ) - state = hass.states.get("update.test_myupdate") + state = hass.states.get("update.test_my_update") assert state is not None assert state.state == STATE_ON assert state.attributes["in_progress"] == 50 @@ -536,7 +536,7 @@ async def test_generic_device_update_entity_has_update( await hass.services.async_call( HOMEASSISTANT_DOMAIN, SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: "update.test_myupdate"}, + {ATTR_ENTITY_ID: "update.test_my_update"}, blocking=True, ) diff --git a/tests/components/esphome/test_valve.py b/tests/components/esphome/test_valve.py index 5ba7bcbe187..413e4cfcb9f 100644 --- a/tests/components/esphome/test_valve.py +++ b/tests/components/esphome/test_valve.py @@ -65,7 +65,7 @@ async def test_valve_entity( user_service=user_service, states=states, ) - state = hass.states.get("valve.test_myvalve") + state = hass.states.get("valve.test_my_valve") assert state is not None assert state.state == STATE_OPENING assert state.attributes[ATTR_CURRENT_POSITION] == 50 @@ -73,7 +73,7 @@ async def test_valve_entity( await hass.services.async_call( VALVE_DOMAIN, SERVICE_CLOSE_VALVE, - {ATTR_ENTITY_ID: "valve.test_myvalve"}, + {ATTR_ENTITY_ID: "valve.test_my_valve"}, blocking=True, ) mock_client.valve_command.assert_has_calls([call(key=1, position=0.0)]) @@ -82,7 +82,7 @@ async def test_valve_entity( await hass.services.async_call( VALVE_DOMAIN, SERVICE_OPEN_VALVE, - {ATTR_ENTITY_ID: "valve.test_myvalve"}, + {ATTR_ENTITY_ID: "valve.test_my_valve"}, blocking=True, ) mock_client.valve_command.assert_has_calls([call(key=1, position=1.0)]) @@ -91,7 +91,7 @@ async def test_valve_entity( await hass.services.async_call( VALVE_DOMAIN, SERVICE_SET_VALVE_POSITION, - {ATTR_ENTITY_ID: "valve.test_myvalve", ATTR_POSITION: 50}, + {ATTR_ENTITY_ID: "valve.test_my_valve", ATTR_POSITION: 50}, blocking=True, ) mock_client.valve_command.assert_has_calls([call(key=1, position=0.5)]) @@ -100,7 +100,7 @@ async def test_valve_entity( await hass.services.async_call( VALVE_DOMAIN, SERVICE_STOP_VALVE, - {ATTR_ENTITY_ID: "valve.test_myvalve"}, + {ATTR_ENTITY_ID: "valve.test_my_valve"}, blocking=True, ) mock_client.valve_command.assert_has_calls([call(key=1, stop=True)]) @@ -110,7 +110,7 @@ async def test_valve_entity( ValveState(key=1, position=0.0, current_operation=ValveOperation.IDLE) ) await hass.async_block_till_done() - state = hass.states.get("valve.test_myvalve") + state = hass.states.get("valve.test_my_valve") assert state is not None assert state.state == STATE_CLOSED @@ -118,7 +118,7 @@ async def test_valve_entity( ValveState(key=1, position=0.5, current_operation=ValveOperation.IS_CLOSING) ) await hass.async_block_till_done() - state = hass.states.get("valve.test_myvalve") + state = hass.states.get("valve.test_my_valve") assert state is not None assert state.state == STATE_CLOSING @@ -126,7 +126,7 @@ async def test_valve_entity( ValveState(key=1, position=1.0, current_operation=ValveOperation.IDLE) ) await hass.async_block_till_done() - state = hass.states.get("valve.test_myvalve") + state = hass.states.get("valve.test_my_valve") assert state is not None assert state.state == STATE_OPEN @@ -164,7 +164,7 @@ async def test_valve_entity_without_position( user_service=user_service, states=states, ) - state = hass.states.get("valve.test_myvalve") + state = hass.states.get("valve.test_my_valve") assert state is not None assert state.state == STATE_OPENING assert ATTR_CURRENT_POSITION not in state.attributes @@ -172,7 +172,7 @@ async def test_valve_entity_without_position( await hass.services.async_call( VALVE_DOMAIN, SERVICE_CLOSE_VALVE, - {ATTR_ENTITY_ID: "valve.test_myvalve"}, + {ATTR_ENTITY_ID: "valve.test_my_valve"}, blocking=True, ) mock_client.valve_command.assert_has_calls([call(key=1, position=0.0)]) @@ -181,7 +181,7 @@ async def test_valve_entity_without_position( await hass.services.async_call( VALVE_DOMAIN, SERVICE_OPEN_VALVE, - {ATTR_ENTITY_ID: "valve.test_myvalve"}, + {ATTR_ENTITY_ID: "valve.test_my_valve"}, blocking=True, ) mock_client.valve_command.assert_has_calls([call(key=1, position=1.0)]) @@ -191,6 +191,6 @@ async def test_valve_entity_without_position( ValveState(key=1, position=0.0, current_operation=ValveOperation.IDLE) ) await hass.async_block_till_done() - state = hass.states.get("valve.test_myvalve") + state = hass.states.get("valve.test_my_valve") assert state is not None assert state.state == STATE_CLOSED From 55eb11055ca8a5fbb2109fda1f9319e7d8adbe99 Mon Sep 17 00:00:00 2001 From: Matrix Date: Fri, 9 Aug 2024 18:21:49 +0800 Subject: [PATCH 2101/2411] Bump YoLink API to 0.4.7 (#123441) --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index ceb4e4ceff3..78b553d7978 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.4.6"] + "requirements": ["yolink-api==0.4.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1960107d88e..a4eba7932fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2965,7 +2965,7 @@ yeelight==0.7.14 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.4.6 +yolink-api==0.4.7 # homeassistant.components.youless youless-api==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4559cd3dca0..5d8c6ffb9d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2345,7 +2345,7 @@ yalexs==6.4.3 yeelight==0.7.14 # homeassistant.components.yolink -yolink-api==0.4.6 +yolink-api==0.4.7 # homeassistant.components.youless youless-api==2.1.2 From aee5d5126f7cbfa68c33f30d8b49cb5e35edcfe8 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Fri, 9 Aug 2024 15:02:27 +0100 Subject: [PATCH 2102/2411] Add sensor platform for Mastodon (#123434) * Add account sensors * Sensor icons * Change sensors to use value_fn * Add native unit of measurement * Update native unit of measurement * Change toots to posts * Fix sensor icons * Add device entry type * Explain conditional naming * Fixes from review * Remove unnecessary constructor --- homeassistant/components/mastodon/__init__.py | 43 ++++- homeassistant/components/mastodon/const.py | 3 + .../components/mastodon/coordinator.py | 35 ++++ homeassistant/components/mastodon/entity.py | 48 ++++++ homeassistant/components/mastodon/icons.json | 15 ++ homeassistant/components/mastodon/sensor.py | 85 ++++++++++ .../components/mastodon/strings.json | 13 ++ .../mastodon/snapshots/test_sensor.ambr | 151 ++++++++++++++++++ tests/components/mastodon/test_sensor.py | 27 ++++ 9 files changed, 418 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/mastodon/coordinator.py create mode 100644 homeassistant/components/mastodon/entity.py create mode 100644 homeassistant/components/mastodon/icons.json create mode 100644 homeassistant/components/mastodon/sensor.py create mode 100644 tests/components/mastodon/snapshots/test_sensor.ambr create mode 100644 tests/components/mastodon/test_sensor.py diff --git a/homeassistant/components/mastodon/__init__.py b/homeassistant/components/mastodon/__init__.py index 2fe379702ee..0a7c93911b5 100644 --- a/homeassistant/components/mastodon/__init__.py +++ b/homeassistant/components/mastodon/__init__.py @@ -2,6 +2,9 @@ from __future__ import annotations +from dataclasses import dataclass +from typing import TYPE_CHECKING + from mastodon.Mastodon import Mastodon, MastodonError from homeassistant.config_entries import ConfigEntry @@ -17,14 +20,33 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery from .const import CONF_BASE_URL, DOMAIN +from .coordinator import MastodonCoordinator from .utils import create_mastodon_client +PLATFORMS: list[Platform] = [Platform.NOTIFY, Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +@dataclass +class MastodonData: + """Mastodon data type.""" + + client: Mastodon + instance: dict + account: dict + coordinator: MastodonCoordinator + + +type MastodonConfigEntry = ConfigEntry[MastodonData] + +if TYPE_CHECKING: + from . import MastodonConfigEntry + + +async def async_setup_entry(hass: HomeAssistant, entry: MastodonConfigEntry) -> bool: """Set up Mastodon from a config entry.""" try: - client, _, _ = await hass.async_add_executor_job( + client, instance, account = await hass.async_add_executor_job( setup_mastodon, entry, ) @@ -34,6 +56,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: assert entry.unique_id + coordinator = MastodonCoordinator(hass, client) + + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = MastodonData(client, instance, account, coordinator) + await discovery.async_load_platform( hass, Platform.NOTIFY, @@ -42,9 +70,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: {}, ) + await hass.config_entries.async_forward_entry_setups( + entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] + ) + return True +async def async_unload_entry(hass: HomeAssistant, entry: MastodonConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms( + entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] + ) + + def setup_mastodon(entry: ConfigEntry) -> tuple[Mastodon, dict, dict]: """Get mastodon details.""" client = create_mastodon_client( diff --git a/homeassistant/components/mastodon/const.py b/homeassistant/components/mastodon/const.py index 3a9cf7462e6..e0593d15d2c 100644 --- a/homeassistant/components/mastodon/const.py +++ b/homeassistant/components/mastodon/const.py @@ -16,3 +16,6 @@ INSTANCE_VERSION: Final = "version" INSTANCE_URI: Final = "uri" INSTANCE_DOMAIN: Final = "domain" ACCOUNT_USERNAME: Final = "username" +ACCOUNT_FOLLOWERS_COUNT: Final = "followers_count" +ACCOUNT_FOLLOWING_COUNT: Final = "following_count" +ACCOUNT_STATUSES_COUNT: Final = "statuses_count" diff --git a/homeassistant/components/mastodon/coordinator.py b/homeassistant/components/mastodon/coordinator.py new file mode 100644 index 00000000000..f1332a0ea43 --- /dev/null +++ b/homeassistant/components/mastodon/coordinator.py @@ -0,0 +1,35 @@ +"""Define an object to manage fetching Mastodon data.""" + +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from mastodon import Mastodon +from mastodon.Mastodon import MastodonError + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import LOGGER + + +class MastodonCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Class to manage fetching Mastodon data.""" + + def __init__(self, hass: HomeAssistant, client: Mastodon) -> None: + """Initialize coordinator.""" + super().__init__( + hass, logger=LOGGER, name="Mastodon", update_interval=timedelta(hours=1) + ) + self.client = client + + async def _async_update_data(self) -> dict[str, Any]: + try: + account: dict = await self.hass.async_add_executor_job( + self.client.account_verify_credentials + ) + except MastodonError as ex: + raise UpdateFailed(ex) from ex + + return account diff --git a/homeassistant/components/mastodon/entity.py b/homeassistant/components/mastodon/entity.py new file mode 100644 index 00000000000..93d630627d7 --- /dev/null +++ b/homeassistant/components/mastodon/entity.py @@ -0,0 +1,48 @@ +"""Base class for Mastodon entities.""" + +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import MastodonConfigEntry +from .const import DEFAULT_NAME, DOMAIN, INSTANCE_VERSION +from .coordinator import MastodonCoordinator +from .utils import construct_mastodon_username + + +class MastodonEntity(CoordinatorEntity[MastodonCoordinator]): + """Defines a base Mastodon entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: MastodonCoordinator, + entity_description: EntityDescription, + data: MastodonConfigEntry, + ) -> None: + """Initialize Mastodon entity.""" + super().__init__(coordinator) + unique_id = data.unique_id + assert unique_id is not None + self._attr_unique_id = f"{unique_id}_{entity_description.key}" + + # Legacy yaml config default title is Mastodon, don't make name Mastodon Mastodon + name = "Mastodon" + if data.title != DEFAULT_NAME: + name = f"Mastodon {data.title}" + + full_account_name = construct_mastodon_username( + data.runtime_data.instance, data.runtime_data.account + ) + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer="Mastodon gGmbH", + model=full_account_name, + entry_type=DeviceEntryType.SERVICE, + sw_version=data.runtime_data.instance[INSTANCE_VERSION], + name=name, + ) + + self.entity_description = entity_description diff --git a/homeassistant/components/mastodon/icons.json b/homeassistant/components/mastodon/icons.json new file mode 100644 index 00000000000..082e27a64c2 --- /dev/null +++ b/homeassistant/components/mastodon/icons.json @@ -0,0 +1,15 @@ +{ + "entity": { + "sensor": { + "followers": { + "default": "mdi:account-multiple" + }, + "following": { + "default": "mdi:account-multiple" + }, + "posts": { + "default": "mdi:message-text" + } + } + } +} diff --git a/homeassistant/components/mastodon/sensor.py b/homeassistant/components/mastodon/sensor.py new file mode 100644 index 00000000000..12acfc04743 --- /dev/null +++ b/homeassistant/components/mastodon/sensor.py @@ -0,0 +1,85 @@ +"""Mastodon platform for sensor components.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import MastodonConfigEntry +from .const import ( + ACCOUNT_FOLLOWERS_COUNT, + ACCOUNT_FOLLOWING_COUNT, + ACCOUNT_STATUSES_COUNT, +) +from .entity import MastodonEntity + + +@dataclass(frozen=True, kw_only=True) +class MastodonSensorEntityDescription(SensorEntityDescription): + """Describes Mastodon sensor entity.""" + + value_fn: Callable[[dict[str, Any]], StateType] + + +ENTITY_DESCRIPTIONS = ( + MastodonSensorEntityDescription( + key="followers", + translation_key="followers", + native_unit_of_measurement="accounts", + state_class=SensorStateClass.TOTAL, + value_fn=lambda data: data.get(ACCOUNT_FOLLOWERS_COUNT), + ), + MastodonSensorEntityDescription( + key="following", + translation_key="following", + native_unit_of_measurement="accounts", + state_class=SensorStateClass.TOTAL, + value_fn=lambda data: data.get(ACCOUNT_FOLLOWING_COUNT), + ), + MastodonSensorEntityDescription( + key="posts", + translation_key="posts", + native_unit_of_measurement="posts", + state_class=SensorStateClass.TOTAL, + value_fn=lambda data: data.get(ACCOUNT_STATUSES_COUNT), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: MastodonConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sensor platform for entity.""" + coordinator = entry.runtime_data.coordinator + + async_add_entities( + MastodonSensorEntity( + coordinator=coordinator, + entity_description=entity_description, + data=entry, + ) + for entity_description in ENTITY_DESCRIPTIONS + ) + + +class MastodonSensorEntity(MastodonEntity, SensorEntity): + """A Mastodon sensor entity.""" + + entity_description: MastodonSensorEntityDescription + + @property + def native_value(self) -> StateType: + """Return the native value of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/mastodon/strings.json b/homeassistant/components/mastodon/strings.json index e1124aad1a9..ed8162eb3df 100644 --- a/homeassistant/components/mastodon/strings.json +++ b/homeassistant/components/mastodon/strings.json @@ -35,5 +35,18 @@ "title": "YAML import failed with unknown error", "description": "Configuring {integration_title} using YAML is being removed but there was an unknown error while importing your existing configuration.\nPlease use the UI to configure Mastodon. Don't forget to delete the YAML configuration." } + }, + "entity": { + "sensor": { + "followers": { + "name": "Followers" + }, + "following": { + "name": "Following" + }, + "posts": { + "name": "Posts" + } + } } } diff --git a/tests/components/mastodon/snapshots/test_sensor.ambr b/tests/components/mastodon/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..f94e34c00ab --- /dev/null +++ b/tests/components/mastodon/snapshots/test_sensor.ambr @@ -0,0 +1,151 @@ +# serializer version: 1 +# name: test_sensors[sensor.mastodon_trwnh_mastodon_social_followers-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mastodon_trwnh_mastodon_social_followers', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Followers', + 'platform': 'mastodon', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'followers', + 'unique_id': 'client_id_followers', + 'unit_of_measurement': 'accounts', + }) +# --- +# name: test_sensors[sensor.mastodon_trwnh_mastodon_social_followers-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mastodon @trwnh@mastodon.social Followers', + 'state_class': , + 'unit_of_measurement': 'accounts', + }), + 'context': , + 'entity_id': 'sensor.mastodon_trwnh_mastodon_social_followers', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '821', + }) +# --- +# name: test_sensors[sensor.mastodon_trwnh_mastodon_social_following-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mastodon_trwnh_mastodon_social_following', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Following', + 'platform': 'mastodon', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'following', + 'unique_id': 'client_id_following', + 'unit_of_measurement': 'accounts', + }) +# --- +# name: test_sensors[sensor.mastodon_trwnh_mastodon_social_following-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mastodon @trwnh@mastodon.social Following', + 'state_class': , + 'unit_of_measurement': 'accounts', + }), + 'context': , + 'entity_id': 'sensor.mastodon_trwnh_mastodon_social_following', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '178', + }) +# --- +# name: test_sensors[sensor.mastodon_trwnh_mastodon_social_posts-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mastodon_trwnh_mastodon_social_posts', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Posts', + 'platform': 'mastodon', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'posts', + 'unique_id': 'client_id_posts', + 'unit_of_measurement': 'posts', + }) +# --- +# name: test_sensors[sensor.mastodon_trwnh_mastodon_social_posts-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mastodon @trwnh@mastodon.social Posts', + 'state_class': , + 'unit_of_measurement': 'posts', + }), + 'context': , + 'entity_id': 'sensor.mastodon_trwnh_mastodon_social_posts', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '33120', + }) +# --- diff --git a/tests/components/mastodon/test_sensor.py b/tests/components/mastodon/test_sensor.py new file mode 100644 index 00000000000..343505260e2 --- /dev/null +++ b/tests/components/mastodon/test_sensor.py @@ -0,0 +1,27 @@ +"""Tests for the Mastodon sensors.""" + +from unittest.mock import AsyncMock, patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_sensors( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the sensor entities.""" + with patch("homeassistant.components.mastodon.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From e6e985af24bda84bbcba548d32ad71c5044e1bcd Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Fri, 9 Aug 2024 15:28:55 +0100 Subject: [PATCH 2103/2411] Remove type checking of config entry in Mastodon (#123467) Remove type checking of configentry --- homeassistant/components/mastodon/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/mastodon/__init__.py b/homeassistant/components/mastodon/__init__.py index 0a7c93911b5..3c305ca655b 100644 --- a/homeassistant/components/mastodon/__init__.py +++ b/homeassistant/components/mastodon/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING from mastodon.Mastodon import Mastodon, MastodonError @@ -38,9 +37,6 @@ class MastodonData: type MastodonConfigEntry = ConfigEntry[MastodonData] -if TYPE_CHECKING: - from . import MastodonConfigEntry - async def async_setup_entry(hass: HomeAssistant, entry: MastodonConfigEntry) -> bool: """Set up Mastodon from a config entry.""" From 97410474f5514473cc07231dd287f758dc8fe044 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:31:55 -0400 Subject: [PATCH 2104/2411] Bump ZHA library to 0.0.29 (#123464) * Bump zha to 0.0.29 * Pass the Core timezone to ZHA * Add a unit test --- homeassistant/components/zha/__init__.py | 19 ++++++++++++++++-- homeassistant/components/zha/helpers.py | 2 ++ homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zha/test_init.py | 23 +++++++++++++++++++++- 6 files changed, 44 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index fc573b19ab1..1897b741d87 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -2,6 +2,7 @@ import contextlib import logging +from zoneinfo import ZoneInfo import voluptuous as vol from zha.application.const import BAUD_RATES, RadioType @@ -12,8 +13,13 @@ from zigpy.config import CONF_DATABASE, CONF_DEVICE, CONF_DEVICE_PATH from zigpy.exceptions import NetworkSettingsInconsistent, TransientConnectionError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_TYPE, EVENT_HOMEASSISTANT_STOP, Platform -from homeassistant.core import Event, HomeAssistant +from homeassistant.const import ( + CONF_TYPE, + EVENT_CORE_CONFIG_UPDATE, + EVENT_HOMEASSISTANT_STOP, + Platform, +) +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv @@ -204,6 +210,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown) ) + @callback + def update_config(event: Event) -> None: + """Handle Core config update.""" + zha_gateway.config.local_timezone = ZoneInfo(hass.config.time_zone) + + config_entry.async_on_unload( + hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, update_config) + ) + await ha_zha_data.gateway_proxy.async_initialize_devices_and_entities() await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index 0691e2429d1..35a794e8631 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -15,6 +15,7 @@ import re import time from types import MappingProxyType from typing import TYPE_CHECKING, Any, Concatenate, NamedTuple, ParamSpec, TypeVar, cast +from zoneinfo import ZoneInfo import voluptuous as vol from zha.application.const import ( @@ -1273,6 +1274,7 @@ def create_zha_config(hass: HomeAssistant, ha_zha_data: HAZHAData) -> ZHAData: quirks_configuration=quirks_config, device_overrides=overrides_config, ), + local_timezone=ZoneInfo(hass.config.time_zone), ) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 4a597b0233c..385b95c8058 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.28"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.29"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index a4eba7932fe..21a3ad0e3f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2989,7 +2989,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.28 +zha==0.0.29 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d8c6ffb9d9..c1254d775d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2366,7 +2366,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.28 +zha==0.0.29 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index aa68d688799..00fc3afd0ea 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -3,6 +3,7 @@ import asyncio import typing from unittest.mock import AsyncMock, Mock, patch +import zoneinfo import pytest from zigpy.application import ControllerApplication @@ -16,7 +17,7 @@ from homeassistant.components.zha.const import ( CONF_USB_PATH, DOMAIN, ) -from homeassistant.components.zha.helpers import get_zha_data +from homeassistant.components.zha.helpers import get_zha_data, get_zha_gateway from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, MAJOR_VERSION, @@ -288,3 +289,23 @@ async def test_shutdown_on_ha_stop( await hass.async_block_till_done() assert len(mock_shutdown.mock_calls) == 1 + + +async def test_timezone_update( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_zigpy_connect: ControllerApplication, +) -> None: + """Test that the ZHA gateway timezone is updated when HA timezone changes.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + gateway = get_zha_gateway(hass) + + assert hass.config.time_zone == "US/Pacific" + assert gateway.config.local_timezone == zoneinfo.ZoneInfo("US/Pacific") + + await hass.config.async_update(time_zone="America/New_York") + + assert hass.config.time_zone == "America/New_York" + assert gateway.config.local_timezone == zoneinfo.ZoneInfo("America/New_York") From 228db1c063c3c3e678df83a0e15953ece952f9db Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 9 Aug 2024 17:18:42 +0200 Subject: [PATCH 2105/2411] Support action YAML syntax in old-style notify groups (#123457) --- homeassistant/components/group/notify.py | 33 ++++++++++++++++++-- tests/components/group/test_notify.py | 39 ++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index 8294b55be5e..ecbfec0bdb8 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -22,8 +22,9 @@ from homeassistant.components.notify import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_SERVICE, + CONF_ACTION, CONF_ENTITIES, + CONF_SERVICE, STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant, callback @@ -36,11 +37,37 @@ from .entity import GroupEntity CONF_SERVICES = "services" + +def _backward_compat_schema(value: Any | None) -> Any: + """Backward compatibility for notify service schemas.""" + + if not isinstance(value, dict): + return value + + # `service` has been renamed to `action` + if CONF_SERVICE in value: + if CONF_ACTION in value: + raise vol.Invalid( + "Cannot specify both 'service' and 'action'. Please use 'action' only." + ) + value[CONF_ACTION] = value.pop(CONF_SERVICE) + + return value + + PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SERVICES): vol.All( cv.ensure_list, - [{vol.Required(ATTR_SERVICE): cv.slug, vol.Optional(ATTR_DATA): dict}], + [ + vol.All( + _backward_compat_schema, + { + vol.Required(CONF_ACTION): cv.slug, + vol.Optional(ATTR_DATA): dict, + }, + ) + ], ) } ) @@ -88,7 +115,7 @@ class GroupNotifyPlatform(BaseNotificationService): tasks.append( asyncio.create_task( self.hass.services.async_call( - DOMAIN, entity[ATTR_SERVICE], sending_payload, blocking=True + DOMAIN, entity[CONF_ACTION], sending_payload, blocking=True ) ) ) diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index 2595b211dae..bbf2d98b492 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -122,7 +122,7 @@ async def test_send_message_with_data(hass: HomeAssistant, tmp_path: Path) -> No "services": [ {"service": "test_service1"}, { - "service": "test_service2", + "action": "test_service2", "data": { "target": "unnamed device", "data": {"test": "message", "default": "default"}, @@ -202,6 +202,41 @@ async def test_send_message_with_data(hass: HomeAssistant, tmp_path: Path) -> No ) +async def test_invalid_configuration( + hass: HomeAssistant, tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """Test failing to set up group with an invalid configuration.""" + assert await async_setup_component( + hass, + "group", + {}, + ) + await hass.async_block_till_done() + + group_setup = [ + { + "platform": "group", + "name": "My invalid notification group", + "services": [ + { + "service": "test_service1", + "action": "test_service2", + "data": { + "target": "unnamed device", + "data": {"test": "message", "default": "default"}, + }, + }, + ], + } + ] + await help_setup_notify(hass, tmp_path, {"service1": 1, "service2": 2}, group_setup) + assert not hass.services.has_service("notify", "my_invalid_notification_group") + assert ( + "Invalid config for 'notify' from integration 'group':" + " Cannot specify both 'service' and 'action'." in caplog.text + ) + + async def test_reload_notify(hass: HomeAssistant, tmp_path: Path) -> None: """Verify we can reload the notify service.""" assert await async_setup_component( @@ -219,7 +254,7 @@ async def test_reload_notify(hass: HomeAssistant, tmp_path: Path) -> None: { "name": "group_notify", "platform": "group", - "services": [{"service": "test_service1"}], + "services": [{"action": "test_service1"}], } ], ) From 85cbc2437c78dce62f92c8d7bb1b6ea2fe01c79a Mon Sep 17 00:00:00 2001 From: David Knowles Date: Fri, 9 Aug 2024 11:25:25 -0400 Subject: [PATCH 2106/2411] Bump pydrawise to 2024.8.0 (#123461) --- homeassistant/components/hydrawise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index c6f4d7d8dcd..9b733cb73d0 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], - "requirements": ["pydrawise==2024.6.4"] + "requirements": ["pydrawise==2024.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 21a3ad0e3f6..25adc9fade9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1810,7 +1810,7 @@ pydiscovergy==3.0.1 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2024.6.4 +pydrawise==2024.8.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1254d775d1..a51ec2d936a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1451,7 +1451,7 @@ pydexcom==0.2.3 pydiscovergy==3.0.1 # homeassistant.components.hydrawise -pydrawise==2024.6.4 +pydrawise==2024.8.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 From 572293fb8b7ae229be1c6c64f057c2819cd93bc2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Aug 2024 10:27:16 -0500 Subject: [PATCH 2107/2411] Bump uiprotect to 6.0.0 (#123402) --- 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 afc4b9a06e6..2843faa52bc 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==5.4.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==6.0.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 25adc9fade9..96da7411d27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2816,7 +2816,7 @@ twitchAPI==4.2.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==5.4.0 +uiprotect==6.0.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a51ec2d936a..bb44cfdeb79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2220,7 +2220,7 @@ twitchAPI==4.2.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==5.4.0 +uiprotect==6.0.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 6e1978971a7d9a2765e2d474b5cadf810fc08e27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Aug 2024 10:33:48 -0500 Subject: [PATCH 2108/2411] Bump PyYAML to 6.0.2 (#123466) --- 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 7c0255b9eae..7a44cf27677 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -52,7 +52,7 @@ pyserial==3.5 python-slugify==8.0.4 PyTurboJPEG==1.7.1 pyudev==0.24.1 -PyYAML==6.0.1 +PyYAML==6.0.2 requests==2.32.3 SQLAlchemy==2.0.31 typing-extensions>=4.12.2,<5.0 diff --git a/pyproject.toml b/pyproject.toml index 07af385dce4..a0012520758 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ dependencies = [ "pip>=21.3.1", "psutil-home-assistant==0.0.1", "python-slugify==8.0.4", - "PyYAML==6.0.1", + "PyYAML==6.0.2", "requests==2.32.3", "SQLAlchemy==2.0.31", "typing-extensions>=4.12.2,<5.0", diff --git a/requirements.txt b/requirements.txt index 40f2165a61a..a98b174be1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ packaging>=23.1 pip>=21.3.1 psutil-home-assistant==0.0.1 python-slugify==8.0.4 -PyYAML==6.0.1 +PyYAML==6.0.2 requests==2.32.3 SQLAlchemy==2.0.31 typing-extensions>=4.12.2,<5.0 From b445517244f06c5589ea22f38c831cfbd9cc1fc6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Aug 2024 10:34:21 -0500 Subject: [PATCH 2109/2411] Bump orjson to 3.10.7 (#123465) --- 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 7a44cf27677..0eedc043527 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 mutagen==1.47.0 -orjson==3.10.6 +orjson==3.10.7 packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.4.0 diff --git a/pyproject.toml b/pyproject.toml index a0012520758..cb928c736a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "cryptography==43.0.0", "Pillow==10.4.0", "pyOpenSSL==24.2.1", - "orjson==3.10.6", + "orjson==3.10.7", "packaging>=23.1", "pip>=21.3.1", "psutil-home-assistant==0.0.1", diff --git a/requirements.txt b/requirements.txt index a98b174be1e..7efddf5c765 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ PyJWT==2.9.0 cryptography==43.0.0 Pillow==10.4.0 pyOpenSSL==24.2.1 -orjson==3.10.6 +orjson==3.10.7 packaging>=23.1 pip>=21.3.1 psutil-home-assistant==0.0.1 From 618efdb3267e5f221828e6bc4810edd9de7f49e7 Mon Sep 17 00:00:00 2001 From: yangqian <5144644+yangqian@users.noreply.github.com> Date: Sat, 10 Aug 2024 00:25:53 +0800 Subject: [PATCH 2110/2411] Bump chacha20poly1305-reuseable to 0.13.2 (#123471) Co-authored-by: J. Nick Koston --- homeassistant/components/homekit/manifest.json | 1 + requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ 3 files changed, 7 insertions(+) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 17d1237e579..8bbf6b677a9 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -10,6 +10,7 @@ "loggers": ["pyhap"], "requirements": [ "HAP-python==4.9.1", + "chacha20poly1305-reuseable==0.13.2", "fnv-hash-fast==0.5.0", "PyQRCode==1.2.1", "base36==0.1.1" diff --git a/requirements_all.txt b/requirements_all.txt index 96da7411d27..f064223e0a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -651,6 +651,9 @@ cached_ipaddress==0.3.0 # homeassistant.components.caldav caldav==1.3.9 +# homeassistant.components.homekit +chacha20poly1305-reuseable==0.13.2 + # homeassistant.components.cisco_mobility_express ciscomobilityexpress==0.3.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb44cfdeb79..bd916af36a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,6 +562,9 @@ cached_ipaddress==0.3.0 # homeassistant.components.caldav caldav==1.3.9 +# homeassistant.components.homekit +chacha20poly1305-reuseable==0.13.2 + # homeassistant.components.coinbase coinbase-advanced-py==1.2.2 From acda7bc5c49269c4e530808f75012e4c9e3861c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Aug 2024 11:50:05 -0500 Subject: [PATCH 2111/2411] Bump uiprotect to 6.0.1 (#123481) --- 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 2843faa52bc..93536d1ad1b 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==6.0.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==6.0.1", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index f064223e0a5..51710d80f8c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2819,7 +2819,7 @@ twitchAPI==4.2.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==6.0.0 +uiprotect==6.0.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd916af36a7..bf827fd9fad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2223,7 +2223,7 @@ twitchAPI==4.2.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==6.0.0 +uiprotect==6.0.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 1ad1a2d51e846c8d2509bce030f8f1b13b216f11 Mon Sep 17 00:00:00 2001 From: Steve Easley Date: Fri, 9 Aug 2024 12:51:50 -0400 Subject: [PATCH 2112/2411] Bump pyjvcprojector to 1.0.12 to fix blocking call (#123473) --- homeassistant/components/jvc_projector/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jvc_projector/manifest.json b/homeassistant/components/jvc_projector/manifest.json index d3e1bf3d940..5d83e937494 100644 --- a/homeassistant/components/jvc_projector/manifest.json +++ b/homeassistant/components/jvc_projector/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["jvcprojector"], - "requirements": ["pyjvcprojector==1.0.11"] + "requirements": ["pyjvcprojector==1.0.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index 51710d80f8c..e39f34e2c81 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1948,7 +1948,7 @@ pyisy==3.1.14 pyitachip2ir==0.0.7 # homeassistant.components.jvc_projector -pyjvcprojector==1.0.11 +pyjvcprojector==1.0.12 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf827fd9fad..f5d00968f1c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1556,7 +1556,7 @@ pyiss==1.0.1 pyisy==3.1.14 # homeassistant.components.jvc_projector -pyjvcprojector==1.0.11 +pyjvcprojector==1.0.12 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From 8e34a0d3c71c54db163913b4bad20b39889c30fe Mon Sep 17 00:00:00 2001 From: Jake Martin Date: Fri, 9 Aug 2024 17:52:07 +0100 Subject: [PATCH 2113/2411] Bump monzopy to 1.3.2 (#123480) --- homeassistant/components/monzo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/monzo/manifest.json b/homeassistant/components/monzo/manifest.json index 8b816457004..d9d17eb8abc 100644 --- a/homeassistant/components/monzo/manifest.json +++ b/homeassistant/components/monzo/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/monzo", "iot_class": "cloud_polling", - "requirements": ["monzopy==1.3.0"] + "requirements": ["monzopy==1.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index e39f34e2c81..ddae9993474 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1357,7 +1357,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.3.1 # homeassistant.components.monzo -monzopy==1.3.0 +monzopy==1.3.2 # homeassistant.components.mopeka mopeka-iot-ble==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5d00968f1c..e20809eab88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1126,7 +1126,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.3.1 # homeassistant.components.monzo -monzopy==1.3.0 +monzopy==1.3.2 # homeassistant.components.mopeka mopeka-iot-ble==0.8.0 From 57da71c537ee4d08b8089cca87e96a730dec2421 Mon Sep 17 00:00:00 2001 From: YogevBokobza Date: Fri, 9 Aug 2024 20:04:11 +0300 Subject: [PATCH 2114/2411] Bump aioswitcher to 4.0.0 (#123260) * Bump aioswitcher to 4.0.0 * switcher fix version * swithcer fix test * switcher fix tests --- homeassistant/components/switcher_kis/button.py | 1 + homeassistant/components/switcher_kis/climate.py | 1 + homeassistant/components/switcher_kis/cover.py | 3 ++- homeassistant/components/switcher_kis/manifest.json | 2 +- homeassistant/components/switcher_kis/switch.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switcher_kis/consts.py | 8 ++++++++ tests/components/switcher_kis/test_cover.py | 2 +- tests/components/switcher_kis/test_diagnostics.py | 3 ++- 10 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/switcher_kis/button.py b/homeassistant/components/switcher_kis/button.py index b770c48c11c..2e559ba9f3b 100644 --- a/homeassistant/components/switcher_kis/button.py +++ b/homeassistant/components/switcher_kis/button.py @@ -137,6 +137,7 @@ class SwitcherThermostatButtonEntity( try: async with SwitcherType2Api( + self.coordinator.data.device_type, self.coordinator.data.ip_address, self.coordinator.data.device_id, self.coordinator.data.device_key, diff --git a/homeassistant/components/switcher_kis/climate.py b/homeassistant/components/switcher_kis/climate.py index e6267e15305..511630251f2 100644 --- a/homeassistant/components/switcher_kis/climate.py +++ b/homeassistant/components/switcher_kis/climate.py @@ -169,6 +169,7 @@ class SwitcherClimateEntity( try: async with SwitcherType2Api( + self.coordinator.data.device_type, self.coordinator.data.ip_address, self.coordinator.data.device_id, self.coordinator.data.device_key, diff --git a/homeassistant/components/switcher_kis/cover.py b/homeassistant/components/switcher_kis/cover.py index 258af3e1d5e..19c40d05e63 100644 --- a/homeassistant/components/switcher_kis/cover.py +++ b/homeassistant/components/switcher_kis/cover.py @@ -29,7 +29,7 @@ from .coordinator import SwitcherDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) API_SET_POSITON = "set_position" -API_STOP = "stop" +API_STOP = "stop_shutter" async def async_setup_entry( @@ -98,6 +98,7 @@ class SwitcherCoverEntity( try: async with SwitcherType2Api( + self.coordinator.data.device_type, self.coordinator.data.ip_address, self.coordinator.data.device_id, self.coordinator.data.device_key, diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 52b218fce9c..6aedd2f5670 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_push", "loggers": ["aioswitcher"], "quality_scale": "platinum", - "requirements": ["aioswitcher==3.4.3"], + "requirements": ["aioswitcher==4.0.0"], "single_config_entry": true } diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index aac5da10ae1..c667a6dd473 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -117,6 +117,7 @@ class SwitcherBaseSwitchEntity( try: async with SwitcherType1Api( + self.coordinator.data.device_type, self.coordinator.data.ip_address, self.coordinator.data.device_id, self.coordinator.data.device_key, diff --git a/requirements_all.txt b/requirements_all.txt index ddae9993474..9258a46aa23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -374,7 +374,7 @@ aiosolaredge==0.2.0 aiosteamist==1.0.0 # homeassistant.components.switcher_kis -aioswitcher==3.4.3 +aioswitcher==4.0.0 # homeassistant.components.syncthing aiosyncthing==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e20809eab88..fabd949fff5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ aiosolaredge==0.2.0 aiosteamist==1.0.0 # homeassistant.components.switcher_kis -aioswitcher==3.4.3 +aioswitcher==4.0.0 # homeassistant.components.syncthing aiosyncthing==0.5.1 diff --git a/tests/components/switcher_kis/consts.py b/tests/components/switcher_kis/consts.py index 3c5f3ff241e..ffeef64b5d7 100644 --- a/tests/components/switcher_kis/consts.py +++ b/tests/components/switcher_kis/consts.py @@ -38,6 +38,10 @@ DUMMY_MAC_ADDRESS1 = "A1:B2:C3:45:67:D8" DUMMY_MAC_ADDRESS2 = "A1:B2:C3:45:67:D9" DUMMY_MAC_ADDRESS3 = "A1:B2:C3:45:67:DA" DUMMY_MAC_ADDRESS4 = "A1:B2:C3:45:67:DB" +DUMMY_TOKEN_NEEDED1 = False +DUMMY_TOKEN_NEEDED2 = False +DUMMY_TOKEN_NEEDED3 = False +DUMMY_TOKEN_NEEDED4 = False DUMMY_PHONE_ID = "1234" DUMMY_POWER_CONSUMPTION1 = 100 DUMMY_POWER_CONSUMPTION2 = 2780 @@ -60,6 +64,7 @@ DUMMY_PLUG_DEVICE = SwitcherPowerPlug( DUMMY_IP_ADDRESS1, DUMMY_MAC_ADDRESS1, DUMMY_DEVICE_NAME1, + DUMMY_TOKEN_NEEDED1, DUMMY_POWER_CONSUMPTION1, DUMMY_ELECTRIC_CURRENT1, ) @@ -72,6 +77,7 @@ DUMMY_WATER_HEATER_DEVICE = SwitcherWaterHeater( DUMMY_IP_ADDRESS2, DUMMY_MAC_ADDRESS2, DUMMY_DEVICE_NAME2, + DUMMY_TOKEN_NEEDED2, DUMMY_POWER_CONSUMPTION2, DUMMY_ELECTRIC_CURRENT2, DUMMY_REMAINING_TIME, @@ -86,6 +92,7 @@ DUMMY_SHUTTER_DEVICE = SwitcherShutter( DUMMY_IP_ADDRESS4, DUMMY_MAC_ADDRESS4, DUMMY_DEVICE_NAME4, + DUMMY_TOKEN_NEEDED4, DUMMY_POSITION, DUMMY_DIRECTION, ) @@ -98,6 +105,7 @@ DUMMY_THERMOSTAT_DEVICE = SwitcherThermostat( DUMMY_IP_ADDRESS3, DUMMY_MAC_ADDRESS3, DUMMY_DEVICE_NAME3, + DUMMY_TOKEN_NEEDED3, DUMMY_THERMOSTAT_MODE, DUMMY_TEMPERATURE, DUMMY_TARGET_TEMPERATURE, diff --git a/tests/components/switcher_kis/test_cover.py b/tests/components/switcher_kis/test_cover.py index 57e2f98915e..c228da6b556 100644 --- a/tests/components/switcher_kis/test_cover.py +++ b/tests/components/switcher_kis/test_cover.py @@ -105,7 +105,7 @@ async def test_cover( # Test stop with patch( - "homeassistant.components.switcher_kis.cover.SwitcherType2Api.stop" + "homeassistant.components.switcher_kis.cover.SwitcherType2Api.stop_shutter" ) as mock_control_device: await hass.services.async_call( COVER_DOMAIN, diff --git a/tests/components/switcher_kis/test_diagnostics.py b/tests/components/switcher_kis/test_diagnostics.py index c8df4dd0b83..89bcefa5138 100644 --- a/tests/components/switcher_kis/test_diagnostics.py +++ b/tests/components/switcher_kis/test_diagnostics.py @@ -40,7 +40,7 @@ async def test_diagnostics( "__type": "", "repr": ( ")>" + "1, , False)>" ), }, "electric_current": 12.8, @@ -50,6 +50,7 @@ async def test_diagnostics( "name": "Heater FE12", "power_consumption": 2780, "remaining_time": "01:29:32", + "token_needed": False, } ], "entry": { From 86c4ded4cde544b6345c060589c3184b6245f2d7 Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Fri, 9 Aug 2024 19:36:58 +0200 Subject: [PATCH 2115/2411] Fix startup blocked by bluesound integration (#123483) --- homeassistant/components/bluesound/media_player.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index dc09feaed63..c1b662fcddc 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -317,21 +317,24 @@ class BluesoundPlayer(MediaPlayerEntity): await self.async_update_status() except (TimeoutError, ClientError): - _LOGGER.error("Node %s:%s is offline, retrying later", self.name, self.port) + _LOGGER.error("Node %s:%s is offline, retrying later", self.host, self.port) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) self.start_polling() except CancelledError: - _LOGGER.debug("Stopping the polling of node %s:%s", self.name, self.port) + _LOGGER.debug("Stopping the polling of node %s:%s", self.host, self.port) except Exception: - _LOGGER.exception("Unexpected error in %s:%s", self.name, self.port) + _LOGGER.exception("Unexpected error in %s:%s", self.host, self.port) raise async def async_added_to_hass(self) -> None: """Start the polling task.""" await super().async_added_to_hass() - self._polling_task = self.hass.async_create_task(self._start_poll_command()) + self._polling_task = self.hass.async_create_background_task( + self._start_poll_command(), + name=f"bluesound.polling_{self.host}:{self.port}", + ) async def async_will_remove_from_hass(self) -> None: """Stop the polling task.""" From 65f33f58e9e88d04a82a879333128d85bfdd76c5 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 9 Aug 2024 20:22:16 +0200 Subject: [PATCH 2116/2411] Bump motionblinds to 0.6.24 (#123395) --- 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 0f9241db7b4..e1e12cf6729 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -21,5 +21,5 @@ "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "iot_class": "local_push", "loggers": ["motionblinds"], - "requirements": ["motionblinds==0.6.23"] + "requirements": ["motionblinds==0.6.24"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9258a46aa23..0b760d074e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1363,7 +1363,7 @@ monzopy==1.3.2 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.23 +motionblinds==0.6.24 # homeassistant.components.motionblinds_ble motionblindsble==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fabd949fff5..6ee73516726 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1132,7 +1132,7 @@ monzopy==1.3.2 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.23 +motionblinds==0.6.24 # homeassistant.components.motionblinds_ble motionblindsble==0.1.0 From ac28d34ad5a68e9a0c485c80f44c0319ee1b45ca Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 9 Aug 2024 20:23:00 +0200 Subject: [PATCH 2117/2411] Improve test coverage for AVM Fritz!Smarthome (#122974) --- homeassistant/components/fritzbox/light.py | 5 +--- tests/components/fritzbox/__init__.py | 13 +++++++++ tests/components/fritzbox/test_climate.py | 19 +++++++++++- tests/components/fritzbox/test_cover.py | 28 ++++++++++++++++-- tests/components/fritzbox/test_init.py | 30 +++++++++++++++++++ tests/components/fritzbox/test_light.py | 34 +++++++++++++++++----- 6 files changed, 115 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py index 689e64c709a..65446dc3e04 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -72,11 +72,8 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity): return self.data.level # type: ignore [no-any-return] @property - def hs_color(self) -> tuple[float, float] | None: + def hs_color(self) -> tuple[float, float]: """Return the hs color value.""" - if self.data.color_mode != COLOR_MODE: - return None - hue = self.data.hue saturation = self.data.saturation diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 61312805e91..09e0aeaee51 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -115,6 +115,13 @@ class FritzDeviceClimateMock(FritzEntityBaseMock): scheduled_preset = PRESET_ECO +class FritzDeviceClimateWithoutTempSensorMock(FritzDeviceClimateMock): + """Mock of a AVM Fritz!Box climate device without exposing temperature sensor.""" + + temperature = None + has_temperature_sensor = False + + class FritzDeviceSensorMock(FritzEntityBaseMock): """Mock of a AVM Fritz!Box sensor device.""" @@ -187,3 +194,9 @@ class FritzDeviceCoverMock(FritzEntityBaseMock): has_thermostat = False has_blind = True levelpercentage = 0 + + +class FritzDeviceCoverUnknownPositionMock(FritzDeviceCoverMock): + """Mock of a AVM Fritz!Box cover device with unknown position.""" + + levelpercentage = None diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 853c09c534b..358eeaa714e 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -46,7 +46,12 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util -from . import FritzDeviceClimateMock, set_devices, setup_config_entry +from . import ( + FritzDeviceClimateMock, + FritzDeviceClimateWithoutTempSensorMock, + set_devices, + setup_config_entry, +) from .const import CONF_FAKE_NAME, MOCK_CONFIG from tests.common import async_fire_time_changed @@ -162,6 +167,18 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: assert state.state == PRESET_COMFORT +async def test_hkr_wo_temperature_sensor(hass: HomeAssistant, fritz: Mock) -> None: + """Test hkr without exposing dedicated temperature sensor data block.""" + device = FritzDeviceClimateWithoutTempSensorMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 18.0 + + async def test_target_temperature_on(hass: HomeAssistant, fritz: Mock) -> None: """Test turn device on.""" device = FritzDeviceClimateMock() diff --git a/tests/components/fritzbox/test_cover.py b/tests/components/fritzbox/test_cover.py index 6c301fc8f46..6626db2bccf 100644 --- a/tests/components/fritzbox/test_cover.py +++ b/tests/components/fritzbox/test_cover.py @@ -3,7 +3,12 @@ from datetime import timedelta from unittest.mock import Mock, call -from homeassistant.components.cover import ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DOMAIN, + STATE_OPEN, +) from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, @@ -12,11 +17,17 @@ from homeassistant.const import ( SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from . import FritzDeviceCoverMock, set_devices, setup_config_entry +from . import ( + FritzDeviceCoverMock, + FritzDeviceCoverUnknownPositionMock, + set_devices, + setup_config_entry, +) from .const import CONF_FAKE_NAME, MOCK_CONFIG from tests.common import async_fire_time_changed @@ -33,9 +44,22 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: state = hass.states.get(ENTITY_ID) assert state + assert state.state == STATE_OPEN assert state.attributes[ATTR_CURRENT_POSITION] == 100 +async def test_unknown_position(hass: HomeAssistant, fritz: Mock) -> None: + """Test cover with unknown position.""" + device = FritzDeviceCoverUnknownPositionMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_UNKNOWN + + async def test_open_cover(hass: HomeAssistant, fritz: Mock) -> None: """Test opening the cover.""" device = FritzDeviceCoverMock() diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index c84498b1560..56e3e7a5738 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, STATE_UNAVAILABLE, UnitOfTemperature, ) @@ -199,6 +200,35 @@ async def test_unload_remove(hass: HomeAssistant, fritz: Mock) -> None: assert state is None +async def test_logout_on_stop(hass: HomeAssistant, fritz: Mock) -> None: + """Test we log out from fritzbox when Home Assistants stops.""" + fritz().get_devices.return_value = [FritzDeviceSwitchMock()] + entity_id = f"{SWITCH_DOMAIN}.{CONF_FAKE_NAME}" + + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id=entity_id, + ) + entry.add_to_hass(hass) + + config_entries = hass.config_entries.async_entries(FB_DOMAIN) + assert len(config_entries) == 1 + assert entry is config_entries[0] + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + state = hass.states.get(entity_id) + assert state + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert fritz().logout.call_count == 1 + + async def test_remove_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/components/fritzbox/test_light.py b/tests/components/fritzbox/test_light.py index 45920c7c3ee..3cafa933fa3 100644 --- a/tests/components/fritzbox/test_light.py +++ b/tests/components/fritzbox/test_light.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import Mock, call +import pytest from requests.exceptions import HTTPError from homeassistant.components.fritzbox.const import ( @@ -12,12 +13,14 @@ from homeassistant.components.fritzbox.const import ( ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_MAX_COLOR_TEMP_KELVIN, ATTR_MIN_COLOR_TEMP_KELVIN, ATTR_SUPPORTED_COLOR_MODES, DOMAIN, + ColorMode, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -56,9 +59,11 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: assert state assert state.state == STATE_ON assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" + assert state.attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 2700 assert state.attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 2700 assert state.attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 6500 + assert state.attributes[ATTR_HS_COLOR] == (28.395, 65.723) assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"] @@ -99,6 +104,9 @@ async def test_setup_non_color_non_level(hass: HomeAssistant, fritz: Mock) -> No assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" assert ATTR_BRIGHTNESS not in state.attributes assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["onoff"] + assert state.attributes[ATTR_COLOR_MODE] == ColorMode.ONOFF + assert state.attributes.get(ATTR_COLOR_TEMP_KELVIN) is None + assert state.attributes.get(ATTR_HS_COLOR) is None async def test_setup_color(hass: HomeAssistant, fritz: Mock) -> None: @@ -120,6 +128,8 @@ async def test_setup_color(hass: HomeAssistant, fritz: Mock) -> None: assert state assert state.state == STATE_ON assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" + assert state.attributes[ATTR_COLOR_MODE] == ColorMode.HS + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] is None assert state.attributes[ATTR_BRIGHTNESS] == 100 assert state.attributes[ATTR_HS_COLOR] == (100, 70) assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"] @@ -183,16 +193,16 @@ async def test_turn_on_color_unsupported_api_method( device.get_colors.return_value = { "Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")] } - mockresponse = Mock() - mockresponse.status_code = 400 - - error = HTTPError("Bad Request") - error.response = mockresponse - device.set_unmapped_color.side_effect = error - assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) + + # test fallback to `setcolor` + error = HTTPError("Bad Request") + error.response = Mock() + error.response.status_code = 400 + device.set_unmapped_color.side_effect = error + await hass.services.async_call( DOMAIN, SERVICE_TURN_ON, @@ -205,6 +215,16 @@ async def test_turn_on_color_unsupported_api_method( assert device.set_level.call_args_list == [call(100)] assert device.set_color.call_args_list == [call((100, 70))] + # test for unknown error + error.response.status_code = 500 + with pytest.raises(HTTPError, match="Bad Request"): + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_BRIGHTNESS: 100, ATTR_HS_COLOR: (100, 70)}, + True, + ) + async def test_turn_off(hass: HomeAssistant, fritz: Mock) -> None: """Test turn device off.""" From eb1c2f5d9fe8e1381904f7c45b68f8a146bb1b82 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 9 Aug 2024 20:30:39 +0200 Subject: [PATCH 2118/2411] Update frontend to 20240809.0 (#123485) --- 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 de423ee9ac6..035b087e481 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240806.1"] + "requirements": ["home-assistant-frontend==20240809.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0eedc043527..9a23a5e42e0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240806.1 +home-assistant-frontend==20240809.0 home-assistant-intents==2024.8.7 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0b760d074e0..a4a7b576215 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1099,7 +1099,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240806.1 +home-assistant-frontend==20240809.0 # homeassistant.components.conversation home-assistant-intents==2024.8.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ee73516726..a991cb4cc2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -925,7 +925,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240806.1 +home-assistant-frontend==20240809.0 # homeassistant.components.conversation home-assistant-intents==2024.8.7 From 2b95a642fc030157ec712044530acebd046d14d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Aug 2024 13:32:11 -0500 Subject: [PATCH 2119/2411] Remove august IPv6 workaround (#123408) --- homeassistant/components/august/util.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/homeassistant/components/august/util.py b/homeassistant/components/august/util.py index 47482100794..6972913ba22 100644 --- a/homeassistant/components/august/util.py +++ b/homeassistant/components/august/util.py @@ -4,7 +4,6 @@ from __future__ import annotations from datetime import datetime, timedelta from functools import partial -import socket import aiohttp from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, Activity, ActivityType @@ -26,14 +25,7 @@ def async_create_august_clientsession(hass: HomeAssistant) -> aiohttp.ClientSess # Create an aiohttp session instead of using the default one since the # default one is likely to trigger august's WAF if another integration # is also using Cloudflare - # - # The family is set to AF_INET because IPv6 keeps coming up as an issue - # see https://github.com/home-assistant/core/issues/97146 - # - # When https://github.com/aio-libs/aiohttp/issues/4451 is implemented - # we can allow IPv6 again - # - return aiohttp_client.async_create_clientsession(hass, family=socket.AF_INET) + return aiohttp_client.async_create_clientsession(hass) def retrieve_time_based_activity( From ec9944b92ae41c503932bde0d40c3e8b6d6b9ed6 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 10 Aug 2024 04:33:13 +1000 Subject: [PATCH 2120/2411] Add missing logger to Tessie (#123413) --- homeassistant/components/tessie/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json index 6059072c239..c921921a0ca 100644 --- a/homeassistant/components/tessie/manifest.json +++ b/homeassistant/components/tessie/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tessie", "iot_class": "cloud_polling", - "loggers": ["tessie"], + "loggers": ["tessie", "tesla-fleet-api"], "quality_scale": "platinum", "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.7.3"] } From 94af95c95b1e5c24edcc754deee129ce0d778e94 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 10 Aug 2024 01:25:38 +0200 Subject: [PATCH 2121/2411] Post merge review for Proximity (#123500) remove commented code --- tests/components/proximity/test_init.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/components/proximity/test_init.py b/tests/components/proximity/test_init.py index 456d6577c04..37573483b74 100644 --- a/tests/components/proximity/test_init.py +++ b/tests/components/proximity/test_init.py @@ -590,20 +590,6 @@ async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone( 1, ) - # assert await async_setup_component( - # hass, - # DOMAIN, - # { - # "proximity": { - # "home": { - # "ignored_zones": ["zone.work"], - # "devices": ["device_tracker.test1", "device_tracker.test2"], - # "zone": "home", - # } - # } - # }, - # ) - hass.states.async_set( "device_tracker.test1", "not_home", From 750bce2b8659c7824326884d4f91c022a4815e5f Mon Sep 17 00:00:00 2001 From: dupondje Date: Sat, 10 Aug 2024 10:40:11 +0200 Subject: [PATCH 2122/2411] Also migrate dsmr entries for devices with correct serial (#123407) dsmr: also migrate entries for devices with correct serial When the dsmr code could not find the serial_nr for the gas meter, it creates the gas meter device with the entry_id as identifier. But when there is a correct serial_nr, it will use that as identifier for the dsmr gas device. Now the migration code did not take this into account, so migration to the new name failed since it didn't look for the device with correct serial_nr. This commit fixes this and adds a test for this. --- homeassistant/components/dsmr/sensor.py | 67 +++++++------- tests/components/dsmr/test_mbus_migration.py | 95 ++++++++++++++++++++ 2 files changed, 129 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index b298ed5bfc0..77c40c5c292 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -431,41 +431,42 @@ def rename_old_gas_to_mbus( ) -> None: """Rename old gas sensor to mbus variant.""" dev_reg = dr.async_get(hass) - device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, entry.entry_id)}) - if device_entry_v1 is not None: - device_id = device_entry_v1.id + for dev_id in (mbus_device_id, entry.entry_id): + device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, dev_id)}) + if device_entry_v1 is not None: + device_id = device_entry_v1.id - ent_reg = er.async_get(hass) - entries = er.async_entries_for_device(ent_reg, device_id) + ent_reg = er.async_get(hass) + entries = er.async_entries_for_device(ent_reg, device_id) - for entity in entries: - if entity.unique_id.endswith( - "belgium_5min_gas_meter_reading" - ) or entity.unique_id.endswith("hourly_gas_meter_reading"): - try: - ent_reg.async_update_entity( - entity.entity_id, - new_unique_id=mbus_device_id, - device_id=mbus_device_id, - ) - except ValueError: - LOGGER.debug( - "Skip migration of %s because it already exists", - entity.entity_id, - ) - else: - LOGGER.debug( - "Migrated entity %s from unique id %s to %s", - entity.entity_id, - entity.unique_id, - mbus_device_id, - ) - # Cleanup old device - dev_entities = er.async_entries_for_device( - ent_reg, device_id, include_disabled_entities=True - ) - if not dev_entities: - dev_reg.async_remove_device(device_id) + for entity in entries: + if entity.unique_id.endswith( + "belgium_5min_gas_meter_reading" + ) or entity.unique_id.endswith("hourly_gas_meter_reading"): + try: + ent_reg.async_update_entity( + entity.entity_id, + new_unique_id=mbus_device_id, + device_id=mbus_device_id, + ) + except ValueError: + LOGGER.debug( + "Skip migration of %s because it already exists", + entity.entity_id, + ) + else: + LOGGER.debug( + "Migrated entity %s from unique id %s to %s", + entity.entity_id, + entity.unique_id, + mbus_device_id, + ) + # Cleanup old device + dev_entities = er.async_entries_for_device( + ent_reg, device_id, include_disabled_entities=True + ) + if not dev_entities: + dev_reg.async_remove_device(device_id) def is_supported_description( diff --git a/tests/components/dsmr/test_mbus_migration.py b/tests/components/dsmr/test_mbus_migration.py index 20b3d253f39..7c7d182aa97 100644 --- a/tests/components/dsmr/test_mbus_migration.py +++ b/tests/components/dsmr/test_mbus_migration.py @@ -219,6 +219,101 @@ async def test_migrate_hourly_gas_to_mbus( ) +async def test_migrate_gas_with_devid_to_mbus( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], +) -> None: + """Test migration of unique_id.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="/dev/ttyUSB0", + data={ + "port": "/dev/ttyUSB0", + "dsmr_version": "5B", + "serial_id": "1234", + "serial_id_gas": "37464C4F32313139303333373331", + }, + options={ + "time_between_update": 0, + }, + ) + + mock_entry.add_to_hass(hass) + + old_unique_id = "37464C4F32313139303333373331_belgium_5min_gas_meter_reading" + + device = device_registry.async_get_or_create( + config_entry_id=mock_entry.entry_id, + identifiers={(DOMAIN, "37464C4F32313139303333373331")}, + name="Gas Meter", + ) + await hass.async_block_till_done() + + entity: er.RegistryEntry = entity_registry.async_get_or_create( + suggested_object_id="gas_meter_reading", + disabled_by=None, + domain=SENSOR_DOMAIN, + platform=DOMAIN, + device_id=device.id, + unique_id=old_unique_id, + config_entry=mock_entry, + ) + assert entity.unique_id == old_unique_id + await hass.async_block_till_done() + + telegram = Telegram() + telegram.add( + MBUS_DEVICE_TYPE, + CosemObject((0, 1), [{"value": "003", "unit": ""}]), + "MBUS_DEVICE_TYPE", + ) + telegram.add( + MBUS_EQUIPMENT_IDENTIFIER, + CosemObject( + (0, 1), + [{"value": "37464C4F32313139303333373331", "unit": ""}], + ), + "MBUS_EQUIPMENT_IDENTIFIER", + ) + telegram.add( + MBUS_METER_READING, + MBusObject( + (0, 1), + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": "m3"}, + ], + ), + "MBUS_METER_READING", + ) + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to be created + 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, "37464C4F32313139303333373331" + ) + == "sensor.gas_meter_reading" + ) + + async def test_migrate_gas_to_mbus_exists( hass: HomeAssistant, entity_registry: er.EntityRegistry, From 089d855c47e8783ecbba1d1d9dd740a583d8b615 Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Sat, 10 Aug 2024 12:28:48 +0200 Subject: [PATCH 2123/2411] Bump bsblan to 0.5.19 (#123515) * bump bsblan lib version * chore: Update bsblan diagnostics to use to_dict() instead of dict() method --- homeassistant/components/bsblan/diagnostics.py | 6 +++--- homeassistant/components/bsblan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bsblan/conftest.py | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/bsblan/diagnostics.py b/homeassistant/components/bsblan/diagnostics.py index 0bceed0bf23..a24082fd698 100644 --- a/homeassistant/components/bsblan/diagnostics.py +++ b/homeassistant/components/bsblan/diagnostics.py @@ -17,7 +17,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" data: HomeAssistantBSBLANData = hass.data[DOMAIN][entry.entry_id] return { - "info": data.info.dict(), - "device": data.device.dict(), - "state": data.coordinator.data.dict(), + "info": data.info.to_dict(), + "device": data.device.to_dict(), + "state": data.coordinator.data.to_dict(), } diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index 3f58fbe364c..fb3c9b49e4c 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["bsblan"], - "requirements": ["python-bsblan==0.5.18"] + "requirements": ["python-bsblan==0.5.19"] } diff --git a/requirements_all.txt b/requirements_all.txt index a4a7b576215..c5daef2eda9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2253,7 +2253,7 @@ python-awair==0.2.4 python-blockchain-api==0.0.2 # homeassistant.components.bsblan -python-bsblan==0.5.18 +python-bsblan==0.5.19 # homeassistant.components.clementine python-clementine-remote==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a991cb4cc2a..9ee4e424f33 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1798,7 +1798,7 @@ python-MotionMount==2.0.0 python-awair==0.2.4 # homeassistant.components.bsblan -python-bsblan==0.5.18 +python-bsblan==0.5.19 # homeassistant.components.ecobee python-ecobee-api==0.2.18 diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py index 862f3ae1d0c..07ca8b648f3 100644 --- a/tests/components/bsblan/conftest.py +++ b/tests/components/bsblan/conftest.py @@ -48,11 +48,11 @@ def mock_bsblan() -> Generator[MagicMock]: patch("homeassistant.components.bsblan.config_flow.BSBLAN", new=bsblan_mock), ): bsblan = bsblan_mock.return_value - bsblan.info.return_value = Info.parse_raw(load_fixture("info.json", DOMAIN)) - bsblan.device.return_value = Device.parse_raw( + bsblan.info.return_value = Info.from_json(load_fixture("info.json", DOMAIN)) + bsblan.device.return_value = Device.from_json( load_fixture("device.json", DOMAIN) ) - bsblan.state.return_value = State.parse_raw(load_fixture("state.json", DOMAIN)) + bsblan.state.return_value = State.from_json(load_fixture("state.json", DOMAIN)) yield bsblan From 5f03589d3e8444785ef3f045c67a9bf9ca9f63ff Mon Sep 17 00:00:00 2001 From: Matt Way Date: Sat, 10 Aug 2024 21:06:29 +1000 Subject: [PATCH 2124/2411] Bump pydaikin to 2.13.2 (#123519) --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 827deb27add..c5cb6064d88 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/daikin", "iot_class": "local_polling", "loggers": ["pydaikin"], - "requirements": ["pydaikin==2.13.1"], + "requirements": ["pydaikin==2.13.2"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index c5daef2eda9..945e239a372 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1792,7 +1792,7 @@ pycsspeechtts==1.0.8 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.13.1 +pydaikin==2.13.2 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ee4e424f33..441c7aee91f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1442,7 +1442,7 @@ pycoolmasternet-async==0.1.5 pycsspeechtts==1.0.8 # homeassistant.components.daikin -pydaikin==2.13.1 +pydaikin==2.13.2 # homeassistant.components.deconz pydeconz==116 From 5f73c73a8856eabe992f4f0ae296d7ba395b53be Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 10 Aug 2024 13:21:01 +0200 Subject: [PATCH 2125/2411] Improve test coverage for Proximity (#123523) * remove unneccessary type checking * remove unused method after #123158 * test when tracked zone is removed --- .../components/proximity/coordinator.py | 14 ------- tests/components/proximity/test_init.py | 41 +++++++++++++++++++ 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/proximity/coordinator.py b/homeassistant/components/proximity/coordinator.py index 2d32926832a..a8dd85c1523 100644 --- a/homeassistant/components/proximity/coordinator.py +++ b/homeassistant/components/proximity/coordinator.py @@ -13,7 +13,6 @@ from homeassistant.const import ( ATTR_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_ZONE, - UnitOfLength, ) from homeassistant.core import ( Event, @@ -27,7 +26,6 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.location import distance -from homeassistant.util.unit_conversion import DistanceConverter from .const import ( ATTR_DIR_OF_TRAVEL, @@ -145,18 +143,6 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]): }, ) - def convert_legacy(self, value: float | str) -> float | str: - """Round and convert given distance value.""" - if isinstance(value, str): - return value - return round( - DistanceConverter.convert( - value, - UnitOfLength.METERS, - self.unit_of_measurement, - ) - ) - def _calc_distance_to_zone( self, zone: State, diff --git a/tests/components/proximity/test_init.py b/tests/components/proximity/test_init.py index 37573483b74..eeb181e0670 100644 --- a/tests/components/proximity/test_init.py +++ b/tests/components/proximity/test_init.py @@ -879,3 +879,44 @@ async def test_sensor_unique_ids( assert ( entity.unique_id == f"{mock_config.entry_id}_device_tracker.test2_dist_to_zone" ) + + +async def test_tracked_zone_is_removed(hass: HomeAssistant) -> None: + """Test that tracked zone is removed.""" + await async_setup_single_entry(hass, "zone.home", ["device_tracker.test1"], [], 1) + + hass.states.async_set( + "device_tracker.test1", + "home", + {"friendly_name": "test1", "latitude": 2.1, "longitude": 1.1}, + ) + await hass.async_block_till_done() + + # check sensor entities + state = hass.states.get("sensor.home_nearest_device") + assert state.state == "test1" + + entity_base_name = "sensor.home_test1" + state = hass.states.get(f"{entity_base_name}_distance") + assert state.state == "0" + state = hass.states.get(f"{entity_base_name}_direction_of_travel") + assert state.state == "arrived" + + # remove tracked zone and move tracked entity + assert hass.states.async_remove("zone.home") + hass.states.async_set( + "device_tracker.test1", + "home", + {"friendly_name": "test1", "latitude": 2.2, "longitude": 1.2}, + ) + await hass.async_block_till_done() + + # check sensor entities + state = hass.states.get("sensor.home_nearest_device") + assert state.state == STATE_UNKNOWN + + entity_base_name = "sensor.home_test1" + state = hass.states.get(f"{entity_base_name}_distance") + assert state.state == STATE_UNAVAILABLE + state = hass.states.get(f"{entity_base_name}_direction_of_travel") + assert state.state == STATE_UNAVAILABLE From 9b678e474bbc36acf6709c9e33c4c90017863135 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sat, 10 Aug 2024 14:04:27 +0200 Subject: [PATCH 2126/2411] Bump ruff to 0.5.7 (#123531) --- .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 9c75edf780c..ed28852fd81 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.6 + rev: v0.5.7 hooks: - id: ruff args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index a3d38c11a8d..ba54a19da3e 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.3.0 -ruff==0.5.6 +ruff==0.5.7 yamllint==1.35.1 From cfd2ca3abb597fc782f7b2be14f710051d87a095 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 10 Aug 2024 08:07:08 -0400 Subject: [PATCH 2127/2411] Bump zha lib to 0.0.30 (#123499) --- 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 385b95c8058..bb1480b43e1 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.29"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.30"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 945e239a372..222de4cd5d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2992,7 +2992,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.29 +zha==0.0.30 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 441c7aee91f..c9043045431 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2369,7 +2369,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.29 +zha==0.0.30 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 From f02fceed5b6ae53f86aa0f0c4afa6c0fa9b255ae Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Sat, 10 Aug 2024 17:01:17 +0200 Subject: [PATCH 2128/2411] Bumb python-homewizard-energy to 6.2.0 (#123514) --- homeassistant/components/homewizard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 474d63e943d..dbad91b1fb8 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "loggers": ["homewizard_energy"], "quality_scale": "platinum", - "requirements": ["python-homewizard-energy==v6.1.1"], + "requirements": ["python-homewizard-energy==v6.2.0"], "zeroconf": ["_hwenergy._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 222de4cd5d0..3f0d1f2d99f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2286,7 +2286,7 @@ python-gitlab==1.6.0 python-homeassistant-analytics==0.7.0 # homeassistant.components.homewizard -python-homewizard-energy==v6.1.1 +python-homewizard-energy==v6.2.0 # homeassistant.components.hp_ilo python-hpilo==4.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9043045431..d7d98d3e057 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1813,7 +1813,7 @@ python-fullykiosk==0.0.14 python-homeassistant-analytics==0.7.0 # homeassistant.components.homewizard -python-homewizard-energy==v6.1.1 +python-homewizard-energy==v6.2.0 # homeassistant.components.izone python-izone==1.2.9 From 4f8a6979d9916714cb2a47141cd1615d91c7dc26 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Sat, 10 Aug 2024 17:01:26 +0200 Subject: [PATCH 2129/2411] Bump OpenWeatherMap to 0.1.1 (#120178) * add owm modes * fix tests * fix modes * remove sensors * Update homeassistant/components/openweathermap/sensor.py Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- .../components/openweathermap/__init__.py | 7 +-- .../components/openweathermap/const.py | 13 +++-- .../components/openweathermap/coordinator.py | 16 +++++-- .../components/openweathermap/manifest.json | 2 +- .../components/openweathermap/sensor.py | 27 +++++++---- .../components/openweathermap/utils.py | 4 +- .../components/openweathermap/weather.py | 48 +++++++++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../openweathermap/test_config_flow.py | 26 +++++----- 10 files changed, 97 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 7aea6aafe20..747b93179bc 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import dataclass import logging -from pyopenweathermap import OWMClient +from pyopenweathermap import create_owm_client from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -33,6 +33,7 @@ class OpenweathermapData: """Runtime data definition.""" name: str + mode: str coordinator: WeatherUpdateCoordinator @@ -52,7 +53,7 @@ async def async_setup_entry( else: async_delete_issue(hass, entry.entry_id) - owm_client = OWMClient(api_key, mode, lang=language) + owm_client = create_owm_client(api_key, mode, lang=language) weather_coordinator = WeatherUpdateCoordinator( owm_client, latitude, longitude, hass ) @@ -61,7 +62,7 @@ async def async_setup_entry( entry.async_on_unload(entry.add_update_listener(async_update_options)) - entry.runtime_data = OpenweathermapData(name, weather_coordinator) + entry.runtime_data = OpenweathermapData(name, mode, weather_coordinator) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 6c9997fc061..d34125a2405 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -58,10 +58,17 @@ FORECAST_MODE_DAILY = "daily" FORECAST_MODE_FREE_DAILY = "freedaily" FORECAST_MODE_ONECALL_HOURLY = "onecall_hourly" FORECAST_MODE_ONECALL_DAILY = "onecall_daily" -OWM_MODE_V25 = "v2.5" +OWM_MODE_FREE_CURRENT = "current" +OWM_MODE_FREE_FORECAST = "forecast" OWM_MODE_V30 = "v3.0" -OWM_MODES = [OWM_MODE_V30, OWM_MODE_V25] -DEFAULT_OWM_MODE = OWM_MODE_V30 +OWM_MODE_V25 = "v2.5" +OWM_MODES = [ + OWM_MODE_FREE_CURRENT, + OWM_MODE_FREE_FORECAST, + OWM_MODE_V30, + OWM_MODE_V25, +] +DEFAULT_OWM_MODE = OWM_MODE_FREE_CURRENT LANGUAGES = [ "af", diff --git a/homeassistant/components/openweathermap/coordinator.py b/homeassistant/components/openweathermap/coordinator.py index 0f99af5ad64..f7672a1290b 100644 --- a/homeassistant/components/openweathermap/coordinator.py +++ b/homeassistant/components/openweathermap/coordinator.py @@ -86,8 +86,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): """Format the weather response correctly.""" _LOGGER.debug("OWM weather response: %s", weather_report) + current_weather = ( + self._get_current_weather_data(weather_report.current) + if weather_report.current is not None + else {} + ) + return { - ATTR_API_CURRENT: self._get_current_weather_data(weather_report.current), + ATTR_API_CURRENT: current_weather, ATTR_API_HOURLY_FORECAST: [ self._get_hourly_forecast_weather_data(item) for item in weather_report.hourly_forecast @@ -122,6 +128,8 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): } def _get_hourly_forecast_weather_data(self, forecast: HourlyWeatherForecast): + uv_index = float(forecast.uv_index) if forecast.uv_index is not None else None + return Forecast( datetime=forecast.date_time.isoformat(), condition=self._get_condition(forecast.condition.id), @@ -134,12 +142,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): wind_speed=forecast.wind_speed, native_wind_gust_speed=forecast.wind_gust, wind_bearing=forecast.wind_bearing, - uv_index=float(forecast.uv_index), + uv_index=uv_index, precipitation_probability=round(forecast.precipitation_probability * 100), precipitation=self._calc_precipitation(forecast.rain, forecast.snow), ) def _get_daily_forecast_weather_data(self, forecast: DailyWeatherForecast): + uv_index = float(forecast.uv_index) if forecast.uv_index is not None else None + return Forecast( datetime=forecast.date_time.isoformat(), condition=self._get_condition(forecast.condition.id), @@ -153,7 +163,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): wind_speed=forecast.wind_speed, native_wind_gust_speed=forecast.wind_gust, wind_bearing=forecast.wind_bearing, - uv_index=float(forecast.uv_index), + uv_index=uv_index, precipitation_probability=round(forecast.precipitation_probability * 100), precipitation=round(forecast.rain + forecast.snow, 2), ) diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index e2c809cf385..199e750ad4f 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/openweathermap", "iot_class": "cloud_polling", "loggers": ["pyopenweathermap"], - "requirements": ["pyopenweathermap==0.0.9"] + "requirements": ["pyopenweathermap==0.1.1"] } diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 89905e99ed9..46789f4b3d2 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -47,6 +48,7 @@ from .const import ( DEFAULT_NAME, DOMAIN, MANUFACTURER, + OWM_MODE_FREE_FORECAST, ) from .coordinator import WeatherUpdateCoordinator @@ -161,16 +163,23 @@ async def async_setup_entry( name = domain_data.name weather_coordinator = domain_data.coordinator - entities: list[AbstractOpenWeatherMapSensor] = [ - OpenWeatherMapSensor( - name, - f"{config_entry.unique_id}-{description.key}", - description, - weather_coordinator, + if domain_data.mode == OWM_MODE_FREE_FORECAST: + entity_registry = er.async_get(hass) + entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + for entry in entries: + entity_registry.async_remove(entry.entity_id) + else: + async_add_entities( + OpenWeatherMapSensor( + name, + f"{config_entry.unique_id}-{description.key}", + description, + weather_coordinator, + ) + for description in WEATHER_SENSOR_TYPES ) - for description in WEATHER_SENSOR_TYPES - ] - async_add_entities(entities) class AbstractOpenWeatherMapSensor(SensorEntity): diff --git a/homeassistant/components/openweathermap/utils.py b/homeassistant/components/openweathermap/utils.py index 7f2391b21a1..ba5378fb31c 100644 --- a/homeassistant/components/openweathermap/utils.py +++ b/homeassistant/components/openweathermap/utils.py @@ -2,7 +2,7 @@ from typing import Any -from pyopenweathermap import OWMClient, RequestError +from pyopenweathermap import RequestError, create_owm_client from homeassistant.const import CONF_LANGUAGE, CONF_MODE @@ -16,7 +16,7 @@ async def validate_api_key(api_key, mode): api_key_valid = None errors, description_placeholders = {}, {} try: - owm_client = OWMClient(api_key, mode) + owm_client = create_owm_client(api_key, mode) api_key_valid = await owm_client.validate_key() except RequestError as error: errors["base"] = "cannot_connect" diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 62b15218233..3a134a0ee26 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -8,6 +8,7 @@ from homeassistant.components.weather import ( WeatherEntityFeature, ) from homeassistant.const import ( + UnitOfLength, UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, @@ -29,6 +30,7 @@ from .const import ( ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_TEMPERATURE, + ATTR_API_VISIBILITY_DISTANCE, ATTR_API_WIND_BEARING, ATTR_API_WIND_GUST, ATTR_API_WIND_SPEED, @@ -36,6 +38,9 @@ from .const import ( DEFAULT_NAME, DOMAIN, MANUFACTURER, + OWM_MODE_FREE_FORECAST, + OWM_MODE_V25, + OWM_MODE_V30, ) from .coordinator import WeatherUpdateCoordinator @@ -48,10 +53,11 @@ async def async_setup_entry( """Set up OpenWeatherMap weather entity based on a config entry.""" domain_data = config_entry.runtime_data name = domain_data.name + mode = domain_data.mode weather_coordinator = domain_data.coordinator unique_id = f"{config_entry.unique_id}" - owm_weather = OpenWeatherMapWeather(name, unique_id, weather_coordinator) + owm_weather = OpenWeatherMapWeather(name, unique_id, mode, weather_coordinator) async_add_entities([owm_weather], False) @@ -66,11 +72,13 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina _attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND + _attr_native_visibility_unit = UnitOfLength.METERS def __init__( self, name: str, unique_id: str, + mode: str, weather_coordinator: WeatherUpdateCoordinator, ) -> None: """Initialize the sensor.""" @@ -83,59 +91,71 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina manufacturer=MANUFACTURER, name=DEFAULT_NAME, ) - self._attr_supported_features = ( - WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY - ) + + if mode in (OWM_MODE_V30, OWM_MODE_V25): + self._attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY + | WeatherEntityFeature.FORECAST_HOURLY + ) + elif mode == OWM_MODE_FREE_FORECAST: + self._attr_supported_features = WeatherEntityFeature.FORECAST_HOURLY @property def condition(self) -> str | None: """Return the current condition.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_CONDITION] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_CONDITION) @property def cloud_coverage(self) -> float | None: """Return the Cloud coverage in %.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_CLOUDS] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_CLOUDS) @property def native_apparent_temperature(self) -> float | None: """Return the apparent temperature.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_FEELS_LIKE_TEMPERATURE] + return self.coordinator.data[ATTR_API_CURRENT].get( + ATTR_API_FEELS_LIKE_TEMPERATURE + ) @property def native_temperature(self) -> float | None: """Return the temperature.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_TEMPERATURE] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_TEMPERATURE) @property def native_pressure(self) -> float | None: """Return the pressure.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_PRESSURE] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_PRESSURE) @property def humidity(self) -> float | None: """Return the humidity.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_HUMIDITY] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_HUMIDITY) @property def native_dew_point(self) -> float | None: """Return the dew point.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_DEW_POINT] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_DEW_POINT) @property def native_wind_gust_speed(self) -> float | None: """Return the wind gust speed.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_GUST] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_GUST) @property def native_wind_speed(self) -> float | None: """Return the wind speed.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_SPEED] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_SPEED) @property def wind_bearing(self) -> float | str | None: """Return the wind bearing.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_BEARING] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_BEARING) + + @property + def visibility(self) -> float | str | None: + """Return visibility.""" + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_VISIBILITY_DISTANCE) @callback def _async_forecast_daily(self) -> list[Forecast] | None: diff --git a/requirements_all.txt b/requirements_all.txt index 3f0d1f2d99f..a7f66c5c161 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2074,7 +2074,7 @@ pyombi==0.1.10 pyopenuv==2023.02.0 # homeassistant.components.openweathermap -pyopenweathermap==0.0.9 +pyopenweathermap==0.1.1 # homeassistant.components.opnsense pyopnsense==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7d98d3e057..2bb4ecf5998 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1661,7 +1661,7 @@ pyoctoprintapi==0.1.12 pyopenuv==2023.02.0 # homeassistant.components.openweathermap -pyopenweathermap==0.0.9 +pyopenweathermap==0.1.1 # homeassistant.components.opnsense pyopnsense==0.4.0 diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index be02a6b01a9..f18aa432e2f 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -45,7 +45,7 @@ CONFIG = { VALID_YAML_CONFIG = {CONF_API_KEY: "foo"} -def _create_mocked_owm_client(is_valid: bool): +def _create_mocked_owm_factory(is_valid: bool): current_weather = CurrentWeather( date_time=datetime.fromtimestamp(1714063536, tz=UTC), temperature=6.84, @@ -118,18 +118,18 @@ def _create_mocked_owm_client(is_valid: bool): def mock_owm_client(): """Mock config_flow OWMClient.""" with patch( - "homeassistant.components.openweathermap.OWMClient", - ) as owm_client_mock: - yield owm_client_mock + "homeassistant.components.openweathermap.create_owm_client", + ) as mock: + yield mock @pytest.fixture(name="config_flow_owm_client_mock") def mock_config_flow_owm_client(): """Mock config_flow OWMClient.""" with patch( - "homeassistant.components.openweathermap.utils.OWMClient", - ) as config_flow_owm_client_mock: - yield config_flow_owm_client_mock + "homeassistant.components.openweathermap.utils.create_owm_client", + ) as mock: + yield mock async def test_successful_config_flow( @@ -138,7 +138,7 @@ async def test_successful_config_flow( config_flow_owm_client_mock, ) -> None: """Test that the form is served with valid input.""" - mock = _create_mocked_owm_client(True) + mock = _create_mocked_owm_factory(True) owm_client_mock.return_value = mock config_flow_owm_client_mock.return_value = mock @@ -177,7 +177,7 @@ async def test_abort_config_flow( config_flow_owm_client_mock, ) -> None: """Test that the form is served with same data.""" - mock = _create_mocked_owm_client(True) + mock = _create_mocked_owm_factory(True) owm_client_mock.return_value = mock config_flow_owm_client_mock.return_value = mock @@ -200,7 +200,7 @@ async def test_config_flow_options_change( config_flow_owm_client_mock, ) -> None: """Test that the options form.""" - mock = _create_mocked_owm_client(True) + mock = _create_mocked_owm_factory(True) owm_client_mock.return_value = mock config_flow_owm_client_mock.return_value = mock @@ -261,7 +261,7 @@ async def test_form_invalid_api_key( config_flow_owm_client_mock, ) -> None: """Test that the form is served with no input.""" - config_flow_owm_client_mock.return_value = _create_mocked_owm_client(False) + config_flow_owm_client_mock.return_value = _create_mocked_owm_factory(False) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) @@ -269,7 +269,7 @@ async def test_form_invalid_api_key( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_api_key"} - config_flow_owm_client_mock.return_value = _create_mocked_owm_client(True) + config_flow_owm_client_mock.return_value = _create_mocked_owm_factory(True) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG ) @@ -282,7 +282,7 @@ async def test_form_api_call_error( config_flow_owm_client_mock, ) -> None: """Test setting up with api call error.""" - config_flow_owm_client_mock.return_value = _create_mocked_owm_client(True) + config_flow_owm_client_mock.return_value = _create_mocked_owm_factory(True) config_flow_owm_client_mock.side_effect = RequestError("oops") result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG From ace6385f5e96d00470fa21704345e2e64c9f0533 Mon Sep 17 00:00:00 2001 From: cnico Date: Sat, 10 Aug 2024 17:01:49 +0200 Subject: [PATCH 2130/2411] Upgrade chacon_dio_api to version 1.2.0 (#123528) Upgrade api version 1.2.0 with the first user feedback improvement --- homeassistant/components/chacon_dio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/chacon_dio/manifest.json b/homeassistant/components/chacon_dio/manifest.json index d077b130da9..c0f4059e798 100644 --- a/homeassistant/components/chacon_dio/manifest.json +++ b/homeassistant/components/chacon_dio/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/chacon_dio", "iot_class": "cloud_push", "loggers": ["dio_chacon_api"], - "requirements": ["dio-chacon-wifi-api==1.1.0"] + "requirements": ["dio-chacon-wifi-api==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index a7f66c5c161..cbfab8d9812 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ devolo-home-control-api==0.18.3 devolo-plc-api==1.4.1 # homeassistant.components.chacon_dio -dio-chacon-wifi-api==1.1.0 +dio-chacon-wifi-api==1.2.0 # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2bb4ecf5998..9a980966cf6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -631,7 +631,7 @@ devolo-home-control-api==0.18.3 devolo-plc-api==1.4.1 # homeassistant.components.chacon_dio -dio-chacon-wifi-api==1.1.0 +dio-chacon-wifi-api==1.2.0 # homeassistant.components.directv directv==0.4.0 From 257742de46d586e89ffeb6849e4d29126ec59bf4 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 10 Aug 2024 18:01:15 +0200 Subject: [PATCH 2131/2411] Fix cleanup of old orphan device entries in AVM Fritz!Tools (#123516) fix cleanup of old orphan device entries --- homeassistant/components/fritz/coordinator.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/fritz/coordinator.py b/homeassistant/components/fritz/coordinator.py index a67f385f3e8..13c442a1ace 100644 --- a/homeassistant/components/fritz/coordinator.py +++ b/homeassistant/components/fritz/coordinator.py @@ -652,8 +652,6 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): entities: list[er.RegistryEntry] = er.async_entries_for_config_entry( entity_reg, config_entry.entry_id ) - - orphan_macs: set[str] = set() for entity in entities: entry_mac = entity.unique_id.split("_")[0] if ( @@ -661,17 +659,16 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): or "_internet_access" in entity.unique_id ) and entry_mac not in device_hosts: _LOGGER.info("Removing orphan entity entry %s", entity.entity_id) - orphan_macs.add(entry_mac) entity_reg.async_remove(entity.entity_id) device_reg = dr.async_get(self.hass) - orphan_connections = { - (CONNECTION_NETWORK_MAC, dr.format_mac(mac)) for mac in orphan_macs + valid_connections = { + (CONNECTION_NETWORK_MAC, dr.format_mac(mac)) for mac in device_hosts } for device in dr.async_entries_for_config_entry( device_reg, config_entry.entry_id ): - if any(con in device.connections for con in orphan_connections): + if not any(con in device.connections for con in valid_connections): _LOGGER.debug("Removing obsolete device entry %s", device.name) device_reg.async_update_device( device.id, remove_config_entry_id=config_entry.entry_id From 13b12a7657f9976ecb723a02a4daef410c51dbc0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 10 Aug 2024 18:28:01 +0200 Subject: [PATCH 2132/2411] Clean up codespell words (#123541) --- .pre-commit-config.yaml | 2 +- homeassistant/components/acer_projector/switch.py | 12 ++++++------ homeassistant/components/keba/sensor.py | 2 +- homeassistant/components/mysensors/binary_sensor.py | 4 ++-- homeassistant/components/mysensors/helpers.py | 10 ++++------ homeassistant/components/mysensors/sensor.py | 4 ++-- tests/helpers/test_config_validation.py | 4 ++-- 7 files changed, 18 insertions(+), 20 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ed28852fd81..f057931e2a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: hooks: - id: codespell args: - - --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn,pres,ser,ue + - --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn - --skip="./.*,*.csv,*.json,*.ambr" - --quiet-level=2 exclude_types: [csv, json, html] diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 5c1c37df5d8..c1463cd9a08 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -81,7 +81,7 @@ class AcerSwitch(SwitchEntity): write_timeout: int, ) -> None: """Init of the Acer projector.""" - self.ser = serial.Serial( + self.serial = serial.Serial( port=serial_port, timeout=timeout, write_timeout=write_timeout ) self._serial_port = serial_port @@ -99,16 +99,16 @@ class AcerSwitch(SwitchEntity): # was disconnected during runtime. # This way the projector can be reconnected and will still work try: - if not self.ser.is_open: - self.ser.open() - self.ser.write(msg.encode("utf-8")) + if not self.serial.is_open: + self.serial.open() + self.serial.write(msg.encode("utf-8")) # Size is an experience value there is no real limit. # AFAIK there is no limit and no end character so we will usually # need to wait for timeout - ret = self.ser.read_until(size=20).decode("utf-8") + ret = self.serial.read_until(size=20).decode("utf-8") except serial.SerialException: _LOGGER.error("Problem communicating with %s", self._serial_port) - self.ser.close() + self.serial.close() return ret def _write_read_format(self, msg: str) -> str: diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index 74c08933cbe..1878a7f6e49 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -64,7 +64,7 @@ async def async_setup_platform( keba, "session_energy", SensorEntityDescription( - key="E pres", + key="E pres", # codespell:ignore pres name="Session Energy", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index a0a1c92c682..b8a3769308a 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -104,8 +104,8 @@ class MySensorsBinarySensor(mysensors.device.MySensorsChildEntity, BinarySensorE def __init__(self, *args: Any, **kwargs: Any) -> None: """Set up the instance.""" super().__init__(*args, **kwargs) - pres = self.gateway.const.Presentation - self.entity_description = SENSORS[pres(self.child_type).name] + presentation = self.gateway.const.Presentation + self.entity_description = SENSORS[presentation(self.child_type).name] @property def is_on(self) -> bool: diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index f060f3313dc..74dc99e76d3 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -168,11 +168,9 @@ def invalid_msg( gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType ) -> str: """Return a message for an invalid child during schema validation.""" - pres = gateway.const.Presentation + presentation = gateway.const.Presentation set_req = gateway.const.SetReq - return ( - f"{pres(child.type).name} requires value_type {set_req[value_type_name].name}" - ) + return f"{presentation(child.type).name} requires value_type {set_req[value_type_name].name}" def validate_set_msg( @@ -202,10 +200,10 @@ def validate_child( ) -> defaultdict[Platform, list[DevId]]: """Validate a child. Returns a dict mapping hass platform names to list of DevId.""" validated: defaultdict[Platform, list[DevId]] = defaultdict(list) - pres: type[IntEnum] = gateway.const.Presentation + presentation: type[IntEnum] = gateway.const.Presentation set_req: type[IntEnum] = gateway.const.SetReq child_type_name: SensorType | None = next( - (member.name for member in pres if member.value == child.type), None + (member.name for member in presentation if member.value == child.type), None ) if not child_type_name: _LOGGER.warning("Child type %s is not supported", child.type) diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index a6a91c12a81..9c6c0de89e6 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -318,9 +318,9 @@ class MySensorsSensor(mysensors.device.MySensorsChildEntity, SensorEntity): entity_description = SENSORS.get(set_req(self.value_type).name) if not entity_description: - pres = self.gateway.const.Presentation + presentation = self.gateway.const.Presentation entity_description = SENSORS.get( - f"{set_req(self.value_type).name}_{pres(self.child_type).name}" + f"{set_req(self.value_type).name}_{presentation(self.child_type).name}" ) return entity_description diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index cf72012a1f1..ac3af13949b 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -33,7 +33,7 @@ def test_boolean() -> None: "T", "negative", "lock", - "tr ue", + "tr ue", # codespell:ignore ue [], [1, 2], {"one": "two"}, @@ -1492,7 +1492,7 @@ def test_whitespace() -> None: "T", "negative", "lock", - "tr ue", + "tr ue", # codespell:ignore ue [], [1, 2], {"one": "two"}, From 778194f7a0a6a086cf92a7f56005521686bc5fcc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 10 Aug 2024 18:31:17 +0200 Subject: [PATCH 2133/2411] Bump AirGradient to 0.8.0 (#123527) --- homeassistant/components/airgradient/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index efb18ae5752..fed4fafdc74 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/airgradient", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["airgradient==0.7.1"], + "requirements": ["airgradient==0.8.0"], "zeroconf": ["_airgradient._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index cbfab8d9812..2e8089bbf9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -410,7 +410,7 @@ aiowithings==3.0.2 aioymaps==1.2.5 # homeassistant.components.airgradient -airgradient==0.7.1 +airgradient==0.8.0 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a980966cf6..aeffade42eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -392,7 +392,7 @@ aiowithings==3.0.2 aioymaps==1.2.5 # homeassistant.components.airgradient -airgradient==0.7.1 +airgradient==0.8.0 # homeassistant.components.airly airly==1.1.0 From ef2ddbf86d9ed63fbf712092209e2f956206c733 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Aug 2024 11:37:00 -0500 Subject: [PATCH 2134/2411] Revert "Bump chacha20poly1305-reuseable to 0.13.2" (#123505) --- homeassistant/components/homekit/manifest.json | 1 - requirements_all.txt | 3 --- requirements_test_all.txt | 3 --- 3 files changed, 7 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 8bbf6b677a9..17d1237e579 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -10,7 +10,6 @@ "loggers": ["pyhap"], "requirements": [ "HAP-python==4.9.1", - "chacha20poly1305-reuseable==0.13.2", "fnv-hash-fast==0.5.0", "PyQRCode==1.2.1", "base36==0.1.1" diff --git a/requirements_all.txt b/requirements_all.txt index 2e8089bbf9a..29146455616 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -651,9 +651,6 @@ cached_ipaddress==0.3.0 # homeassistant.components.caldav caldav==1.3.9 -# homeassistant.components.homekit -chacha20poly1305-reuseable==0.13.2 - # homeassistant.components.cisco_mobility_express ciscomobilityexpress==0.3.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aeffade42eb..46889f670ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,9 +562,6 @@ cached_ipaddress==0.3.0 # homeassistant.components.caldav caldav==1.3.9 -# homeassistant.components.homekit -chacha20poly1305-reuseable==0.13.2 - # homeassistant.components.coinbase coinbase-advanced-py==1.2.2 From 0558c85b5dc4523772b4c7ab7171e412012f428e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 10 Aug 2024 18:38:20 +0200 Subject: [PATCH 2135/2411] Revert "Remove ESPHome legacy entity naming" (#123453) --- homeassistant/components/esphome/entity.py | 20 ++- .../esphome/test_alarm_control_panel.py | 22 +-- .../components/esphome/test_binary_sensor.py | 10 +- tests/components/esphome/test_button.py | 8 +- tests/components/esphome/test_camera.py | 36 ++--- tests/components/esphome/test_climate.py | 36 ++--- tests/components/esphome/test_cover.py | 24 +-- tests/components/esphome/test_date.py | 6 +- tests/components/esphome/test_datetime.py | 6 +- tests/components/esphome/test_entity.py | 89 +++++++---- tests/components/esphome/test_event.py | 2 +- tests/components/esphome/test_fan.py | 46 +++--- tests/components/esphome/test_light.py | 148 +++++++++--------- tests/components/esphome/test_lock.py | 14 +- tests/components/esphome/test_media_player.py | 24 +-- tests/components/esphome/test_number.py | 10 +- tests/components/esphome/test_select.py | 4 +- tests/components/esphome/test_sensor.py | 38 ++--- tests/components/esphome/test_switch.py | 6 +- tests/components/esphome/test_text.py | 8 +- tests/components/esphome/test_time.py | 6 +- tests/components/esphome/test_update.py | 10 +- tests/components/esphome/test_valve.py | 24 +-- 23 files changed, 322 insertions(+), 275 deletions(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 5f845f4665b..6e02f8de869 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -176,7 +176,6 @@ ENTITY_CATEGORIES: EsphomeEnumMapper[EsphomeEntityCategory, EntityCategory | Non class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): """Define a base esphome entity.""" - _attr_has_entity_name = True _attr_should_poll = False _static_info: _InfoT _state: _StateT @@ -201,6 +200,25 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)} ) + # + # If `friendly_name` is set, we use the Friendly naming rules, if + # `friendly_name` is not set we make an exception to the naming rules for + # backwards compatibility and use the Legacy naming rules. + # + # Friendly naming + # - Friendly name is prepended to entity names + # - Device Name is prepended to entity ids + # - Entity id is constructed from device name and object id + # + # Legacy naming + # - Device name is not prepended to entity names + # - Device name is not prepended to entity ids + # - Entity id is constructed from entity name + # + if not device_info.friendly_name: + return + self._attr_has_entity_name = True + self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}" async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/tests/components/esphome/test_alarm_control_panel.py b/tests/components/esphome/test_alarm_control_panel.py index 33c7be94736..af717ac1b49 100644 --- a/tests/components/esphome/test_alarm_control_panel.py +++ b/tests/components/esphome/test_alarm_control_panel.py @@ -58,7 +58,7 @@ async def test_generic_alarm_control_panel_requires_code( user_service=user_service, states=states, ) - state = hass.states.get("alarm_control_panel.test_my_alarm_control_panel") + state = hass.states.get("alarm_control_panel.test_myalarm_control_panel") assert state is not None assert state.state == STATE_ALARM_ARMED_AWAY @@ -66,7 +66,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_AWAY, { - ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -80,7 +80,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, { - ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -94,7 +94,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_HOME, { - ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -108,7 +108,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_NIGHT, { - ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -122,7 +122,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_VACATION, { - ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -136,7 +136,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_TRIGGER, { - ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -150,7 +150,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_DISARM, { - ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -193,14 +193,14 @@ async def test_generic_alarm_control_panel_no_code( user_service=user_service, states=states, ) - state = hass.states.get("alarm_control_panel.test_my_alarm_control_panel") + state = hass.states.get("alarm_control_panel.test_myalarm_control_panel") assert state is not None assert state.state == STATE_ALARM_ARMED_AWAY await hass.services.async_call( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_DISARM, - {ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel"}, + {ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel"}, blocking=True, ) mock_client.alarm_control_panel_command.assert_has_calls( @@ -239,6 +239,6 @@ async def test_generic_alarm_control_panel_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("alarm_control_panel.test_my_alarm_control_panel") + state = hass.states.get("alarm_control_panel.test_myalarm_control_panel") assert state is not None assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/test_binary_sensor.py b/tests/components/esphome/test_binary_sensor.py index c07635eff3b..3da8a54ff34 100644 --- a/tests/components/esphome/test_binary_sensor.py +++ b/tests/components/esphome/test_binary_sensor.py @@ -74,7 +74,7 @@ async def test_binary_sensor_generic_entity( user_service=user_service, states=states, ) - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == hass_state @@ -105,7 +105,7 @@ async def test_status_binary_sensor( user_service=user_service, states=states, ) - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON @@ -135,7 +135,7 @@ async def test_binary_sensor_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_UNKNOWN @@ -165,12 +165,12 @@ async def test_binary_sensor_has_state_false( user_service=user_service, states=states, ) - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_UNKNOWN mock_device.set_state(BinarySensorState(key=1, state=True, missing_state=False)) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON diff --git a/tests/components/esphome/test_button.py b/tests/components/esphome/test_button.py index d3fec2a56d2..8c120949caa 100644 --- a/tests/components/esphome/test_button.py +++ b/tests/components/esphome/test_button.py @@ -29,22 +29,22 @@ async def test_button_generic_entity( user_service=user_service, states=states, ) - state = hass.states.get("button.test_my_button") + state = hass.states.get("button.test_mybutton") assert state is not None assert state.state == STATE_UNKNOWN await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.test_my_button"}, + {ATTR_ENTITY_ID: "button.test_mybutton"}, blocking=True, ) mock_client.button_command.assert_has_calls([call(1)]) - state = hass.states.get("button.test_my_button") + state = hass.states.get("button.test_mybutton") assert state is not None assert state.state != STATE_UNKNOWN await mock_device.mock_disconnect(False) - state = hass.states.get("button.test_my_button") + state = hass.states.get("button.test_mybutton") assert state is not None assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/esphome/test_camera.py b/tests/components/esphome/test_camera.py index 680cda00944..c6a61cd18e8 100644 --- a/tests/components/esphome/test_camera.py +++ b/tests/components/esphome/test_camera.py @@ -53,7 +53,7 @@ async def test_camera_single_image( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE @@ -63,9 +63,9 @@ async def test_camera_single_image( mock_client.request_single_image = _mock_camera_image client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.test_my_camera") + resp = await client.get("/api/camera_proxy/camera.test_mycamera") await hass.async_block_till_done() - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE @@ -101,15 +101,15 @@ async def test_camera_single_image_unavailable_before_requested( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE await mock_device.mock_disconnect(False) client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.test_my_camera") + resp = await client.get("/api/camera_proxy/camera.test_mycamera") await hass.async_block_till_done() - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_UNAVAILABLE @@ -142,7 +142,7 @@ async def test_camera_single_image_unavailable_during_request( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE @@ -152,9 +152,9 @@ async def test_camera_single_image_unavailable_during_request( mock_client.request_single_image = _mock_camera_image client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.test_my_camera") + resp = await client.get("/api/camera_proxy/camera.test_mycamera") await hass.async_block_till_done() - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_UNAVAILABLE @@ -187,7 +187,7 @@ async def test_camera_stream( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE remaining_responses = 3 @@ -203,9 +203,9 @@ async def test_camera_stream( mock_client.request_single_image = _mock_camera_image client = await hass_client() - resp = await client.get("/api/camera_proxy_stream/camera.test_my_camera") + resp = await client.get("/api/camera_proxy_stream/camera.test_mycamera") await hass.async_block_till_done() - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE @@ -247,16 +247,16 @@ async def test_camera_stream_unavailable( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE await mock_device.mock_disconnect(False) client = await hass_client() - await client.get("/api/camera_proxy_stream/camera.test_my_camera") + await client.get("/api/camera_proxy_stream/camera.test_mycamera") await hass.async_block_till_done() - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_UNAVAILABLE @@ -287,7 +287,7 @@ async def test_camera_stream_with_disconnection( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE remaining_responses = 3 @@ -305,8 +305,8 @@ async def test_camera_stream_with_disconnection( mock_client.request_single_image = _mock_camera_image client = await hass_client() - await client.get("/api/camera_proxy_stream/camera.test_my_camera") + await client.get("/api/camera_proxy_stream/camera.test_mycamera") await hass.async_block_till_done() - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/esphome/test_climate.py b/tests/components/esphome/test_climate.py index a573128bef1..4ec7fee6447 100644 --- a/tests/components/esphome/test_climate.py +++ b/tests/components/esphome/test_climate.py @@ -78,14 +78,14 @@ async def test_climate_entity( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_my_climate") + state = hass.states.get("climate.test_myclimate") assert state is not None assert state.state == HVACMode.COOL await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_TEMPERATURE: 25}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_TEMPERATURE: 25}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, target_temperature=25.0)]) @@ -130,14 +130,14 @@ async def test_climate_entity_with_step_and_two_point( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_my_climate") + state = hass.states.get("climate.test_myclimate") assert state is not None assert state.state == HVACMode.COOL await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_TEMPERATURE: 25}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_TEMPERATURE: 25}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, target_temperature=25.0)]) @@ -147,7 +147,7 @@ async def test_climate_entity_with_step_and_two_point( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { - ATTR_ENTITY_ID: "climate.test_my_climate", + ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_HVAC_MODE: HVACMode.AUTO, ATTR_TARGET_TEMP_LOW: 20, ATTR_TARGET_TEMP_HIGH: 30, @@ -209,14 +209,14 @@ async def test_climate_entity_with_step_and_target_temp( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_my_climate") + state = hass.states.get("climate.test_myclimate") assert state is not None assert state.state == HVACMode.COOL await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_TEMPERATURE: 25}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_TEMPERATURE: 25}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, target_temperature=25.0)]) @@ -226,7 +226,7 @@ async def test_climate_entity_with_step_and_target_temp( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { - ATTR_ENTITY_ID: "climate.test_my_climate", + ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_HVAC_MODE: HVACMode.AUTO, ATTR_TARGET_TEMP_LOW: 20, ATTR_TARGET_TEMP_HIGH: 30, @@ -249,7 +249,7 @@ async def test_climate_entity_with_step_and_target_temp( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { - ATTR_ENTITY_ID: "climate.test_my_climate", + ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_HVAC_MODE: HVACMode.HEAT, }, blocking=True, @@ -267,7 +267,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_PRESET_MODE: "away"}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_PRESET_MODE: "away"}, blocking=True, ) mock_client.climate_command.assert_has_calls( @@ -283,7 +283,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_PRESET_MODE: "preset1"}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_PRESET_MODE: "preset1"}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, custom_preset="preset1")]) @@ -292,7 +292,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_FAN_MODE: FAN_HIGH}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_FAN_MODE: FAN_HIGH}, blocking=True, ) mock_client.climate_command.assert_has_calls( @@ -303,7 +303,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_FAN_MODE: "fan2"}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_FAN_MODE: "fan2"}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, custom_fan_mode="fan2")]) @@ -312,7 +312,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_SWING_MODE: SWING_BOTH}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_SWING_MODE: SWING_BOTH}, blocking=True, ) mock_client.climate_command.assert_has_calls( @@ -362,7 +362,7 @@ async def test_climate_entity_with_humidity( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_my_climate") + state = hass.states.get("climate.test_myclimate") assert state is not None assert state.state == HVACMode.AUTO attributes = state.attributes @@ -374,7 +374,7 @@ async def test_climate_entity_with_humidity( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HUMIDITY, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_HUMIDITY: 23}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_HUMIDITY: 23}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, target_humidity=23)]) @@ -422,7 +422,7 @@ async def test_climate_entity_with_inf_value( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_my_climate") + state = hass.states.get("climate.test_myclimate") assert state is not None assert state.state == HVACMode.AUTO attributes = state.attributes @@ -484,7 +484,7 @@ async def test_climate_entity_attributes( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_my_climate") + state = hass.states.get("climate.test_myclimate") assert state is not None assert state.state == HVACMode.COOL assert state.attributes == snapshot(name="climate-entity-attributes") diff --git a/tests/components/esphome/test_cover.py b/tests/components/esphome/test_cover.py index 59eadb3cfd9..b190d287198 100644 --- a/tests/components/esphome/test_cover.py +++ b/tests/components/esphome/test_cover.py @@ -72,7 +72,7 @@ async def test_cover_entity( user_service=user_service, states=states, ) - state = hass.states.get("cover.test_my_cover") + state = hass.states.get("cover.test_mycover") assert state is not None assert state.state == STATE_OPENING assert state.attributes[ATTR_CURRENT_POSITION] == 50 @@ -81,7 +81,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.test_my_cover"}, + {ATTR_ENTITY_ID: "cover.test_mycover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, position=0.0)]) @@ -90,7 +90,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.test_my_cover"}, + {ATTR_ENTITY_ID: "cover.test_mycover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, position=1.0)]) @@ -99,7 +99,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.test_my_cover", ATTR_POSITION: 50}, + {ATTR_ENTITY_ID: "cover.test_mycover", ATTR_POSITION: 50}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, position=0.5)]) @@ -108,7 +108,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.test_my_cover"}, + {ATTR_ENTITY_ID: "cover.test_mycover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, stop=True)]) @@ -117,7 +117,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER_TILT, - {ATTR_ENTITY_ID: "cover.test_my_cover"}, + {ATTR_ENTITY_ID: "cover.test_mycover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, tilt=1.0)]) @@ -126,7 +126,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER_TILT, - {ATTR_ENTITY_ID: "cover.test_my_cover"}, + {ATTR_ENTITY_ID: "cover.test_mycover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, tilt=0.0)]) @@ -135,7 +135,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_TILT_POSITION, - {ATTR_ENTITY_ID: "cover.test_my_cover", ATTR_TILT_POSITION: 50}, + {ATTR_ENTITY_ID: "cover.test_mycover", ATTR_TILT_POSITION: 50}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, tilt=0.5)]) @@ -145,7 +145,7 @@ async def test_cover_entity( CoverState(key=1, position=0.0, current_operation=CoverOperation.IDLE) ) await hass.async_block_till_done() - state = hass.states.get("cover.test_my_cover") + state = hass.states.get("cover.test_mycover") assert state is not None assert state.state == STATE_CLOSED @@ -153,7 +153,7 @@ async def test_cover_entity( CoverState(key=1, position=0.5, current_operation=CoverOperation.IS_CLOSING) ) await hass.async_block_till_done() - state = hass.states.get("cover.test_my_cover") + state = hass.states.get("cover.test_mycover") assert state is not None assert state.state == STATE_CLOSING @@ -161,7 +161,7 @@ async def test_cover_entity( CoverState(key=1, position=1.0, current_operation=CoverOperation.IDLE) ) await hass.async_block_till_done() - state = hass.states.get("cover.test_my_cover") + state = hass.states.get("cover.test_mycover") assert state is not None assert state.state == STATE_OPEN @@ -201,7 +201,7 @@ async def test_cover_entity_without_position( user_service=user_service, states=states, ) - state = hass.states.get("cover.test_my_cover") + state = hass.states.get("cover.test_mycover") assert state is not None assert state.state == STATE_OPENING assert ATTR_CURRENT_TILT_POSITION not in state.attributes diff --git a/tests/components/esphome/test_date.py b/tests/components/esphome/test_date.py index 3b620a30461..2deb92775fb 100644 --- a/tests/components/esphome/test_date.py +++ b/tests/components/esphome/test_date.py @@ -35,14 +35,14 @@ async def test_generic_date_entity( user_service=user_service, states=states, ) - state = hass.states.get("date.test_my_date") + state = hass.states.get("date.test_mydate") assert state is not None assert state.state == "2024-12-31" await hass.services.async_call( DATE_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "date.test_my_date", ATTR_DATE: "1999-01-01"}, + {ATTR_ENTITY_ID: "date.test_mydate", ATTR_DATE: "1999-01-01"}, blocking=True, ) mock_client.date_command.assert_has_calls([call(1, 1999, 1, 1)]) @@ -71,6 +71,6 @@ async def test_generic_date_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("date.test_my_date") + state = hass.states.get("date.test_mydate") assert state is not None assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/test_datetime.py b/tests/components/esphome/test_datetime.py index 95f0b7584ad..3bdc196de95 100644 --- a/tests/components/esphome/test_datetime.py +++ b/tests/components/esphome/test_datetime.py @@ -35,7 +35,7 @@ async def test_generic_datetime_entity( user_service=user_service, states=states, ) - state = hass.states.get("datetime.test_my_datetime") + state = hass.states.get("datetime.test_mydatetime") assert state is not None assert state.state == "2024-04-16T12:34:56+00:00" @@ -43,7 +43,7 @@ async def test_generic_datetime_entity( DATETIME_DOMAIN, SERVICE_SET_VALUE, { - ATTR_ENTITY_ID: "datetime.test_my_datetime", + ATTR_ENTITY_ID: "datetime.test_mydatetime", ATTR_DATETIME: "2000-01-01T01:23:45+00:00", }, blocking=True, @@ -74,6 +74,6 @@ async def test_generic_datetime_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("datetime.test_my_datetime") + state = hass.states.get("datetime.test_mydatetime") assert state is not None assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index 64b8d6101ac..296d61b664d 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -69,10 +69,10 @@ async def test_entities_removed( entry = mock_device.entry entry_id = entry.entry_id storage_key = f"esphome.{entry_id}" - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON - state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") assert state is not None assert state.state == STATE_ON @@ -81,13 +81,13 @@ async def test_entities_removed( assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 2 - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.attributes[ATTR_RESTORED] is True - state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") assert state is not None reg_entry = entity_registry.async_get( - "binary_sensor.test_my_binary_sensor_to_be_removed" + "binary_sensor.test_mybinary_sensor_to_be_removed" ) assert reg_entry is not None assert state.attributes[ATTR_RESTORED] is True @@ -111,13 +111,13 @@ async def test_entities_removed( entry=entry, ) assert mock_device.entry.entry_id == entry_id - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON - state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") assert state is None reg_entry = entity_registry.async_get( - "binary_sensor.test_my_binary_sensor_to_be_removed" + "binary_sensor.test_mybinary_sensor_to_be_removed" ) assert reg_entry is None await hass.config_entries.async_unload(entry.entry_id) @@ -164,15 +164,15 @@ async def test_entities_removed_after_reload( entry = mock_device.entry entry_id = entry.entry_id storage_key = f"esphome.{entry_id}" - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON - state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") assert state is not None assert state.state == STATE_ON reg_entry = entity_registry.async_get( - "binary_sensor.test_my_binary_sensor_to_be_removed" + "binary_sensor.test_mybinary_sensor_to_be_removed" ) assert reg_entry is not None @@ -181,15 +181,15 @@ async def test_entities_removed_after_reload( assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 2 - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.attributes[ATTR_RESTORED] is True - state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") assert state is not None assert state.attributes[ATTR_RESTORED] is True reg_entry = entity_registry.async_get( - "binary_sensor.test_my_binary_sensor_to_be_removed" + "binary_sensor.test_mybinary_sensor_to_be_removed" ) assert reg_entry is not None @@ -198,14 +198,14 @@ async def test_entities_removed_after_reload( assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 2 - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert ATTR_RESTORED not in state.attributes - state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") assert state is not None assert ATTR_RESTORED not in state.attributes reg_entry = entity_registry.async_get( - "binary_sensor.test_my_binary_sensor_to_be_removed" + "binary_sensor.test_mybinary_sensor_to_be_removed" ) assert reg_entry is not None @@ -236,23 +236,23 @@ async def test_entities_removed_after_reload( on_future.set_result(None) async_track_state_change_event( - hass, ["binary_sensor.test_my_binary_sensor"], _async_wait_for_on + hass, ["binary_sensor.test_mybinary_sensor"], _async_wait_for_on ) await hass.async_block_till_done() async with asyncio.timeout(2): await on_future assert mock_device.entry.entry_id == entry_id - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON - state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") assert state is None await hass.async_block_till_done() reg_entry = entity_registry.async_get( - "binary_sensor.test_my_binary_sensor_to_be_removed" + "binary_sensor.test_mybinary_sensor_to_be_removed" ) assert reg_entry is None assert await hass.config_entries.async_unload(entry.entry_id) @@ -260,6 +260,35 @@ async def test_entities_removed_after_reload( assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 1 +async def test_entity_info_object_ids( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test how object ids affect entity id.""" + entity_info = [ + BinarySensorInfo( + object_id="object_id_is_used", + key=1, + name="my binary_sensor", + unique_id="my_binary_sensor", + ) + ] + states = [] + user_service = [] + await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("binary_sensor.test_object_id_is_used") + assert state is not None + + async def test_deep_sleep_device( hass: HomeAssistant, mock_client: APIClient, @@ -297,7 +326,7 @@ async def test_deep_sleep_device( states=states, device_info={"has_deep_sleep": True}, ) - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON state = hass.states.get("sensor.test_my_sensor") @@ -306,7 +335,7 @@ async def test_deep_sleep_device( await mock_device.mock_disconnect(False) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_UNAVAILABLE state = hass.states.get("sensor.test_my_sensor") @@ -316,7 +345,7 @@ async def test_deep_sleep_device( await mock_device.mock_connect() await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON state = hass.states.get("sensor.test_my_sensor") @@ -330,7 +359,7 @@ async def test_deep_sleep_device( mock_device.set_state(BinarySensorState(key=1, state=False, missing_state=False)) mock_device.set_state(SensorState(key=3, state=56, missing_state=False)) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_OFF state = hass.states.get("sensor.test_my_sensor") @@ -339,7 +368,7 @@ async def test_deep_sleep_device( await mock_device.mock_disconnect(True) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_OFF state = hass.states.get("sensor.test_my_sensor") @@ -350,7 +379,7 @@ async def test_deep_sleep_device( await hass.async_block_till_done() await mock_device.mock_disconnect(False) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_UNAVAILABLE state = hass.states.get("sensor.test_my_sensor") @@ -359,14 +388,14 @@ async def test_deep_sleep_device( await mock_device.mock_connect() await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() # Verify we do not dispatch any more state updates or # availability updates after the stop event is fired - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON @@ -401,6 +430,6 @@ async def test_esphome_device_without_friendly_name( states=states, device_info={"friendly_name": None}, ) - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.my_binary_sensor") assert state is not None assert state.state == STATE_ON diff --git a/tests/components/esphome/test_event.py b/tests/components/esphome/test_event.py index 2daba94a3ca..c17dc4d98a9 100644 --- a/tests/components/esphome/test_event.py +++ b/tests/components/esphome/test_event.py @@ -32,7 +32,7 @@ async def test_generic_event_entity( user_service=user_service, states=states, ) - state = hass.states.get("event.test_my_event") + state = hass.states.get("event.test_myevent") assert state is not None assert state.state == "2024-04-24T00:00:00.000+00:00" assert state.attributes["event_type"] == "type1" diff --git a/tests/components/esphome/test_fan.py b/tests/components/esphome/test_fan.py index 9aca36d79a4..064b37b1ec1 100644 --- a/tests/components/esphome/test_fan.py +++ b/tests/components/esphome/test_fan.py @@ -62,14 +62,14 @@ async def test_fan_entity_with_all_features_old_api( user_service=user_service, states=states, ) - state = hass.states.get("fan.test_my_fan") + state = hass.states.get("fan.test_myfan") assert state is not None assert state.state == STATE_ON await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 20}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 20}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -80,7 +80,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 50}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 50}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -91,7 +91,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_DECREASE_SPEED, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -102,7 +102,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_INCREASE_SPEED, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -113,7 +113,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=False)]) @@ -122,7 +122,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 100}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 100}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -166,14 +166,14 @@ async def test_fan_entity_with_all_features_new_api( user_service=user_service, states=states, ) - state = hass.states.get("fan.test_my_fan") + state = hass.states.get("fan.test_myfan") assert state is not None assert state.state == STATE_ON await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 20}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 20}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=1, state=True)]) @@ -182,7 +182,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 50}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 50}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=2, state=True)]) @@ -191,7 +191,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_DECREASE_SPEED, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=2, state=True)]) @@ -200,7 +200,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_INCREASE_SPEED, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=4, state=True)]) @@ -209,7 +209,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=False)]) @@ -218,7 +218,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 100}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 100}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=4, state=True)]) @@ -227,7 +227,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 0}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 0}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=False)]) @@ -236,7 +236,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_OSCILLATE, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_OSCILLATING: True}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_OSCILLATING: True}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, oscillating=True)]) @@ -245,7 +245,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_OSCILLATE, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_OSCILLATING: False}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_OSCILLATING: False}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, oscillating=False)]) @@ -254,7 +254,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_DIRECTION, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_DIRECTION: "forward"}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_DIRECTION: "forward"}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -265,7 +265,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_DIRECTION, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_DIRECTION: "reverse"}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_DIRECTION: "reverse"}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -276,7 +276,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PRESET_MODE: "Preset1"}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PRESET_MODE: "Preset1"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, preset_mode="Preset1")]) @@ -308,14 +308,14 @@ async def test_fan_entity_with_no_features_new_api( user_service=user_service, states=states, ) - state = hass.states.get("fan.test_my_fan") + state = hass.states.get("fan.test_myfan") assert state is not None assert state.state == STATE_ON await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=True)]) @@ -324,7 +324,7 @@ async def test_fan_entity_with_no_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=False)]) diff --git a/tests/components/esphome/test_light.py b/tests/components/esphome/test_light.py index 5dc563d9991..2324c73b16f 100644 --- a/tests/components/esphome/test_light.py +++ b/tests/components/esphome/test_light.py @@ -65,14 +65,14 @@ async def test_light_on_off( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -105,14 +105,14 @@ async def test_light_brightness( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -123,7 +123,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -141,7 +141,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_TRANSITION: 2}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_TRANSITION: 2}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -152,7 +152,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_FLASH: FLASH_LONG}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_FLASH: FLASH_LONG}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -163,7 +163,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_TRANSITION: 2}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_TRANSITION: 2}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -181,7 +181,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_FLASH: FLASH_SHORT}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_FLASH: FLASH_SHORT}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -223,14 +223,14 @@ async def test_light_brightness_on_off( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -248,7 +248,7 @@ async def test_light_brightness_on_off( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -293,14 +293,14 @@ async def test_light_legacy_white_converted_to_brightness( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -351,7 +351,7 @@ async def test_light_legacy_white_with_rgb( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ @@ -362,7 +362,7 @@ async def test_light_legacy_white_with_rgb( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_WHITE: 60}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_WHITE: 60}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -405,14 +405,14 @@ async def test_light_brightness_on_off_with_unknown_color_mode( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -431,7 +431,7 @@ async def test_light_brightness_on_off_with_unknown_color_mode( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -476,14 +476,14 @@ async def test_light_on_and_brightness( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -526,14 +526,14 @@ async def test_rgb_color_temp_light( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -551,7 +551,7 @@ async def test_rgb_color_temp_light( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -570,7 +570,7 @@ async def test_rgb_color_temp_light( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_COLOR_TEMP_KELVIN: 2500}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 2500}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -614,14 +614,14 @@ async def test_light_rgb( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -640,7 +640,7 @@ async def test_light_rgb( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -661,7 +661,7 @@ async def test_light_rgb( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_my_light", + ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127, ATTR_HS_COLOR: (100, 100), }, @@ -686,7 +686,7 @@ async def test_light_rgb( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -746,7 +746,7 @@ async def test_light_rgbw( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGBW] @@ -755,7 +755,7 @@ async def test_light_rgbw( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -775,7 +775,7 @@ async def test_light_rgbw( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -797,7 +797,7 @@ async def test_light_rgbw( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_my_light", + ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127, ATTR_HS_COLOR: (100, 100), }, @@ -824,7 +824,7 @@ async def test_light_rgbw( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -847,7 +847,7 @@ async def test_light_rgbw( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -917,7 +917,7 @@ async def test_light_rgbww_with_cold_warm_white_support( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGBWW] @@ -927,7 +927,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -949,7 +949,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -973,7 +973,7 @@ async def test_light_rgbww_with_cold_warm_white_support( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_my_light", + ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127, ATTR_HS_COLOR: (100, 100), }, @@ -1003,7 +1003,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1029,7 +1029,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1056,7 +1056,7 @@ async def test_light_rgbww_with_cold_warm_white_support( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_my_light", + ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGBWW_COLOR: (255, 255, 255, 255, 255), }, blocking=True, @@ -1084,7 +1084,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_COLOR_TEMP_KELVIN: 2500}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 2500}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1154,7 +1154,7 @@ async def test_light_rgbww_without_cold_warm_white_support( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGBWW] @@ -1164,7 +1164,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1185,7 +1185,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1208,7 +1208,7 @@ async def test_light_rgbww_without_cold_warm_white_support( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_my_light", + ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127, ATTR_HS_COLOR: (100, 100), }, @@ -1237,7 +1237,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1262,7 +1262,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1288,7 +1288,7 @@ async def test_light_rgbww_without_cold_warm_white_support( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_my_light", + ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGBWW_COLOR: (255, 255, 255, 255, 255), }, blocking=True, @@ -1315,7 +1315,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_COLOR_TEMP_KELVIN: 2500}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 2500}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1374,7 +1374,7 @@ async def test_light_color_temp( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON attributes = state.attributes @@ -1387,7 +1387,7 @@ async def test_light_color_temp( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1406,7 +1406,7 @@ async def test_light_color_temp( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls([call(key=1, state=False)]) @@ -1449,7 +1449,7 @@ async def test_light_color_temp_no_mireds_set( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON attributes = state.attributes @@ -1462,7 +1462,7 @@ async def test_light_color_temp_no_mireds_set( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1481,7 +1481,7 @@ async def test_light_color_temp_no_mireds_set( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_COLOR_TEMP_KELVIN: 6000}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 6000}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1501,7 +1501,7 @@ async def test_light_color_temp_no_mireds_set( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls([call(key=1, state=False)]) @@ -1551,7 +1551,7 @@ async def test_light_color_temp_legacy( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON attributes = state.attributes @@ -1566,7 +1566,7 @@ async def test_light_color_temp_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1585,7 +1585,7 @@ async def test_light_color_temp_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls([call(key=1, state=False)]) @@ -1637,7 +1637,7 @@ async def test_light_rgb_legacy( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON attributes = state.attributes @@ -1647,7 +1647,7 @@ async def test_light_rgb_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1663,7 +1663,7 @@ async def test_light_rgb_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls([call(key=1, state=False)]) @@ -1672,7 +1672,7 @@ async def test_light_rgb_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1715,7 +1715,7 @@ async def test_light_effects( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_EFFECT_LIST] == ["effect1", "effect2"] @@ -1723,7 +1723,7 @@ async def test_light_effects( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_EFFECT: "effect1"}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_EFFECT: "effect1"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1782,7 +1782,7 @@ async def test_only_cold_warm_white_support( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.COLOR_TEMP] @@ -1791,7 +1791,7 @@ async def test_only_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1802,7 +1802,7 @@ async def test_only_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1820,7 +1820,7 @@ async def test_only_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_COLOR_TEMP_KELVIN: 2500}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 2500}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1861,7 +1861,7 @@ async def test_light_no_color_modes( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.ONOFF] @@ -1869,7 +1869,7 @@ async def test_light_no_color_modes( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls([call(key=1, state=True, color_mode=0)]) diff --git a/tests/components/esphome/test_lock.py b/tests/components/esphome/test_lock.py index 259e990c5f7..82c24b59a2c 100644 --- a/tests/components/esphome/test_lock.py +++ b/tests/components/esphome/test_lock.py @@ -39,14 +39,14 @@ async def test_lock_entity_no_open( user_service=user_service, states=states, ) - state = hass.states.get("lock.test_my_lock") + state = hass.states.get("lock.test_mylock") assert state is not None assert state.state == STATE_UNLOCKING await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, - {ATTR_ENTITY_ID: "lock.test_my_lock"}, + {ATTR_ENTITY_ID: "lock.test_mylock"}, blocking=True, ) mock_client.lock_command.assert_has_calls([call(1, LockCommand.LOCK)]) @@ -73,7 +73,7 @@ async def test_lock_entity_start_locked( user_service=user_service, states=states, ) - state = hass.states.get("lock.test_my_lock") + state = hass.states.get("lock.test_mylock") assert state is not None assert state.state == STATE_LOCKED @@ -100,14 +100,14 @@ async def test_lock_entity_supports_open( user_service=user_service, states=states, ) - state = hass.states.get("lock.test_my_lock") + state = hass.states.get("lock.test_mylock") assert state is not None assert state.state == STATE_LOCKING await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, - {ATTR_ENTITY_ID: "lock.test_my_lock"}, + {ATTR_ENTITY_ID: "lock.test_mylock"}, blocking=True, ) mock_client.lock_command.assert_has_calls([call(1, LockCommand.LOCK)]) @@ -116,7 +116,7 @@ async def test_lock_entity_supports_open( await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, - {ATTR_ENTITY_ID: "lock.test_my_lock"}, + {ATTR_ENTITY_ID: "lock.test_mylock"}, blocking=True, ) mock_client.lock_command.assert_has_calls([call(1, LockCommand.UNLOCK, None)]) @@ -125,7 +125,7 @@ async def test_lock_entity_supports_open( await hass.services.async_call( LOCK_DOMAIN, SERVICE_OPEN, - {ATTR_ENTITY_ID: "lock.test_my_lock"}, + {ATTR_ENTITY_ID: "lock.test_mylock"}, blocking=True, ) mock_client.lock_command.assert_has_calls([call(1, LockCommand.OPEN)]) diff --git a/tests/components/esphome/test_media_player.py b/tests/components/esphome/test_media_player.py index c9fcecb5d55..3879129ccb6 100644 --- a/tests/components/esphome/test_media_player.py +++ b/tests/components/esphome/test_media_player.py @@ -62,7 +62,7 @@ async def test_media_player_entity( user_service=user_service, states=states, ) - state = hass.states.get("media_player.test_my_media_player") + state = hass.states.get("media_player.test_mymedia_player") assert state is not None assert state.state == "paused" @@ -70,7 +70,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_MUTE, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", ATTR_MEDIA_VOLUME_MUTED: True, }, blocking=True, @@ -84,7 +84,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_MUTE, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", ATTR_MEDIA_VOLUME_MUTED: True, }, blocking=True, @@ -98,7 +98,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_SET, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", ATTR_MEDIA_VOLUME_LEVEL: 0.5, }, blocking=True, @@ -110,7 +110,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PAUSE, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", }, blocking=True, ) @@ -123,7 +123,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PLAY, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", }, blocking=True, ) @@ -136,7 +136,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_STOP, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", }, blocking=True, ) @@ -207,7 +207,7 @@ async def test_media_player_entity_with_source( user_service=user_service, states=states, ) - state = hass.states.get("media_player.test_my_media_player") + state = hass.states.get("media_player.test_mymedia_player") assert state is not None assert state.state == "playing" @@ -216,7 +216,7 @@ async def test_media_player_entity_with_source( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: "media-source://local/xz", }, @@ -240,7 +240,7 @@ async def test_media_player_entity_with_source( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", ATTR_MEDIA_CONTENT_TYPE: "audio/mp3", ATTR_MEDIA_CONTENT_ID: "media-source://local/xy", }, @@ -256,7 +256,7 @@ async def test_media_player_entity_with_source( { "id": 1, "type": "media_player/browse_media", - "entity_id": "media_player.test_my_media_player", + "entity_id": "media_player.test_mymedia_player", } ) response = await client.receive_json() @@ -266,7 +266,7 @@ async def test_media_player_entity_with_source( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", ATTR_MEDIA_CONTENT_TYPE: MediaType.URL, ATTR_MEDIA_CONTENT_ID: "media-source://tts?message=hello", ATTR_MEDIA_ANNOUNCE: True, diff --git a/tests/components/esphome/test_number.py b/tests/components/esphome/test_number.py index 91a21e670f5..557425052f3 100644 --- a/tests/components/esphome/test_number.py +++ b/tests/components/esphome/test_number.py @@ -48,14 +48,14 @@ async def test_generic_number_entity( user_service=user_service, states=states, ) - state = hass.states.get("number.test_my_number") + state = hass.states.get("number.test_mynumber") assert state is not None assert state.state == "50" await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "number.test_my_number", ATTR_VALUE: 50}, + {ATTR_ENTITY_ID: "number.test_mynumber", ATTR_VALUE: 50}, blocking=True, ) mock_client.number_command.assert_has_calls([call(1, 50)]) @@ -89,7 +89,7 @@ async def test_generic_number_nan( user_service=user_service, states=states, ) - state = hass.states.get("number.test_my_number") + state = hass.states.get("number.test_mynumber") assert state is not None assert state.state == STATE_UNKNOWN @@ -121,7 +121,7 @@ async def test_generic_number_with_unit_of_measurement_as_empty_string( user_service=user_service, states=states, ) - state = hass.states.get("number.test_my_number") + state = hass.states.get("number.test_mynumber") assert state is not None assert state.state == "42" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -160,7 +160,7 @@ async def test_generic_number_entity_set_when_disconnected( await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "number.test_my_number", ATTR_VALUE: 20}, + {ATTR_ENTITY_ID: "number.test_mynumber", ATTR_VALUE: 20}, blocking=True, ) mock_client.number_command.reset_mock() diff --git a/tests/components/esphome/test_select.py b/tests/components/esphome/test_select.py index 8df898ea3cf..a433b1b0ab0 100644 --- a/tests/components/esphome/test_select.py +++ b/tests/components/esphome/test_select.py @@ -59,14 +59,14 @@ async def test_select_generic_entity( user_service=user_service, states=states, ) - state = hass.states.get("select.test_my_select") + state = hass.states.get("select.test_myselect") assert state is not None assert state.state == "a" await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: "select.test_my_select", ATTR_OPTION: "b"}, + {ATTR_ENTITY_ID: "select.test_myselect", ATTR_OPTION: "b"}, blocking=True, ) mock_client.select_command.assert_has_calls([call(1, "b")]) diff --git a/tests/components/esphome/test_sensor.py b/tests/components/esphome/test_sensor.py index 5f68dbb8660..76f71b53167 100644 --- a/tests/components/esphome/test_sensor.py +++ b/tests/components/esphome/test_sensor.py @@ -62,35 +62,35 @@ async def test_generic_numeric_sensor( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "50" # Test updating state mock_device.set_state(SensorState(key=1, state=60)) await hass.async_block_till_done() - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "60" # Test sending the same state again mock_device.set_state(SensorState(key=1, state=60)) await hass.async_block_till_done() - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "60" # Test we can still update after the same state mock_device.set_state(SensorState(key=1, state=70)) await hass.async_block_till_done() - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "70" # Test invalid data from the underlying api does not crash us mock_device.set_state(SensorState(key=1, state=object())) await hass.async_block_till_done() - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "70" @@ -120,11 +120,11 @@ async def test_generic_numeric_sensor_with_entity_category_and_icon( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "50" assert state.attributes[ATTR_ICON] == "mdi:leaf" - entry = entity_registry.async_get("sensor.test_my_sensor") + entry = entity_registry.async_get("sensor.test_mysensor") assert entry is not None # Note that ESPHome includes the EntityInfo type in the unique id # as this is not a 1:1 mapping to the entity platform (ie. text_sensor) @@ -158,11 +158,11 @@ async def test_generic_numeric_sensor_state_class_measurement( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "50" assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT - entry = entity_registry.async_get("sensor.test_my_sensor") + entry = entity_registry.async_get("sensor.test_mysensor") assert entry is not None # Note that ESPHome includes the EntityInfo type in the unique id # as this is not a 1:1 mapping to the entity platform (ie. text_sensor) @@ -193,7 +193,7 @@ async def test_generic_numeric_sensor_device_class_timestamp( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "2023-06-22T18:43:52+00:00" @@ -222,7 +222,7 @@ async def test_generic_numeric_sensor_legacy_last_reset_convert( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "50" assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL_INCREASING @@ -248,7 +248,7 @@ async def test_generic_numeric_sensor_no_state( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == STATE_UNKNOWN @@ -273,7 +273,7 @@ async def test_generic_numeric_sensor_nan_state( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == STATE_UNKNOWN @@ -298,7 +298,7 @@ async def test_generic_numeric_sensor_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == STATE_UNKNOWN @@ -325,7 +325,7 @@ async def test_generic_text_sensor( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "i am a teapot" @@ -350,7 +350,7 @@ async def test_generic_text_sensor_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == STATE_UNKNOWN @@ -378,7 +378,7 @@ async def test_generic_text_sensor_device_class_timestamp( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "2023-06-22T18:43:52+00:00" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TIMESTAMP @@ -407,7 +407,7 @@ async def test_generic_text_sensor_device_class_date( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "2023-06-22" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.DATE @@ -434,7 +434,7 @@ async def test_generic_numeric_sensor_empty_string_uom( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "123" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes diff --git a/tests/components/esphome/test_switch.py b/tests/components/esphome/test_switch.py index 799290c931a..561ac0b369f 100644 --- a/tests/components/esphome/test_switch.py +++ b/tests/components/esphome/test_switch.py @@ -33,14 +33,14 @@ async def test_switch_generic_entity( user_service=user_service, states=states, ) - state = hass.states.get("switch.test_my_switch") + state = hass.states.get("switch.test_myswitch") assert state is not None assert state.state == STATE_ON await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.test_my_switch"}, + {ATTR_ENTITY_ID: "switch.test_myswitch"}, blocking=True, ) mock_client.switch_command.assert_has_calls([call(1, True)]) @@ -48,7 +48,7 @@ async def test_switch_generic_entity( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_my_switch"}, + {ATTR_ENTITY_ID: "switch.test_myswitch"}, blocking=True, ) mock_client.switch_command.assert_has_calls([call(1, False)]) diff --git a/tests/components/esphome/test_text.py b/tests/components/esphome/test_text.py index e2b459be2d2..07157d98ac6 100644 --- a/tests/components/esphome/test_text.py +++ b/tests/components/esphome/test_text.py @@ -39,14 +39,14 @@ async def test_generic_text_entity( user_service=user_service, states=states, ) - state = hass.states.get("text.test_my_text") + state = hass.states.get("text.test_mytext") assert state is not None assert state.state == "hello world" await hass.services.async_call( TEXT_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "text.test_my_text", ATTR_VALUE: "goodbye"}, + {ATTR_ENTITY_ID: "text.test_mytext", ATTR_VALUE: "goodbye"}, blocking=True, ) mock_client.text_command.assert_has_calls([call(1, "goodbye")]) @@ -79,7 +79,7 @@ async def test_generic_text_entity_no_state( user_service=user_service, states=states, ) - state = hass.states.get("text.test_my_text") + state = hass.states.get("text.test_mytext") assert state is not None assert state.state == STATE_UNKNOWN @@ -110,6 +110,6 @@ async def test_generic_text_entity_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("text.test_my_text") + state = hass.states.get("text.test_mytext") assert state is not None assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/test_time.py b/tests/components/esphome/test_time.py index 5277ca82e34..aaa18c77a47 100644 --- a/tests/components/esphome/test_time.py +++ b/tests/components/esphome/test_time.py @@ -35,14 +35,14 @@ async def test_generic_time_entity( user_service=user_service, states=states, ) - state = hass.states.get("time.test_my_time") + state = hass.states.get("time.test_mytime") assert state is not None assert state.state == "12:34:56" await hass.services.async_call( TIME_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "time.test_my_time", ATTR_TIME: "01:23:45"}, + {ATTR_ENTITY_ID: "time.test_mytime", ATTR_TIME: "01:23:45"}, blocking=True, ) mock_client.time_command.assert_has_calls([call(1, 1, 23, 45)]) @@ -71,6 +71,6 @@ async def test_generic_time_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("time.test_my_time") + state = hass.states.get("time.test_mytime") assert state is not None assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index ad638add0a0..83e89b1de00 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -464,7 +464,7 @@ async def test_generic_device_update_entity( user_service=user_service, states=states, ) - state = hass.states.get("update.test_my_update") + state = hass.states.get("update.test_myupdate") assert state is not None assert state.state == STATE_OFF @@ -503,14 +503,14 @@ async def test_generic_device_update_entity_has_update( user_service=user_service, states=states, ) - state = hass.states.get("update.test_my_update") + state = hass.states.get("update.test_myupdate") assert state is not None assert state.state == STATE_ON await hass.services.async_call( UPDATE_DOMAIN, SERVICE_INSTALL, - {ATTR_ENTITY_ID: "update.test_my_update"}, + {ATTR_ENTITY_ID: "update.test_myupdate"}, blocking=True, ) @@ -528,7 +528,7 @@ async def test_generic_device_update_entity_has_update( ) ) - state = hass.states.get("update.test_my_update") + state = hass.states.get("update.test_myupdate") assert state is not None assert state.state == STATE_ON assert state.attributes["in_progress"] == 50 @@ -536,7 +536,7 @@ async def test_generic_device_update_entity_has_update( await hass.services.async_call( HOMEASSISTANT_DOMAIN, SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: "update.test_my_update"}, + {ATTR_ENTITY_ID: "update.test_myupdate"}, blocking=True, ) diff --git a/tests/components/esphome/test_valve.py b/tests/components/esphome/test_valve.py index 413e4cfcb9f..5ba7bcbe187 100644 --- a/tests/components/esphome/test_valve.py +++ b/tests/components/esphome/test_valve.py @@ -65,7 +65,7 @@ async def test_valve_entity( user_service=user_service, states=states, ) - state = hass.states.get("valve.test_my_valve") + state = hass.states.get("valve.test_myvalve") assert state is not None assert state.state == STATE_OPENING assert state.attributes[ATTR_CURRENT_POSITION] == 50 @@ -73,7 +73,7 @@ async def test_valve_entity( await hass.services.async_call( VALVE_DOMAIN, SERVICE_CLOSE_VALVE, - {ATTR_ENTITY_ID: "valve.test_my_valve"}, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, blocking=True, ) mock_client.valve_command.assert_has_calls([call(key=1, position=0.0)]) @@ -82,7 +82,7 @@ async def test_valve_entity( await hass.services.async_call( VALVE_DOMAIN, SERVICE_OPEN_VALVE, - {ATTR_ENTITY_ID: "valve.test_my_valve"}, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, blocking=True, ) mock_client.valve_command.assert_has_calls([call(key=1, position=1.0)]) @@ -91,7 +91,7 @@ async def test_valve_entity( await hass.services.async_call( VALVE_DOMAIN, SERVICE_SET_VALVE_POSITION, - {ATTR_ENTITY_ID: "valve.test_my_valve", ATTR_POSITION: 50}, + {ATTR_ENTITY_ID: "valve.test_myvalve", ATTR_POSITION: 50}, blocking=True, ) mock_client.valve_command.assert_has_calls([call(key=1, position=0.5)]) @@ -100,7 +100,7 @@ async def test_valve_entity( await hass.services.async_call( VALVE_DOMAIN, SERVICE_STOP_VALVE, - {ATTR_ENTITY_ID: "valve.test_my_valve"}, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, blocking=True, ) mock_client.valve_command.assert_has_calls([call(key=1, stop=True)]) @@ -110,7 +110,7 @@ async def test_valve_entity( ValveState(key=1, position=0.0, current_operation=ValveOperation.IDLE) ) await hass.async_block_till_done() - state = hass.states.get("valve.test_my_valve") + state = hass.states.get("valve.test_myvalve") assert state is not None assert state.state == STATE_CLOSED @@ -118,7 +118,7 @@ async def test_valve_entity( ValveState(key=1, position=0.5, current_operation=ValveOperation.IS_CLOSING) ) await hass.async_block_till_done() - state = hass.states.get("valve.test_my_valve") + state = hass.states.get("valve.test_myvalve") assert state is not None assert state.state == STATE_CLOSING @@ -126,7 +126,7 @@ async def test_valve_entity( ValveState(key=1, position=1.0, current_operation=ValveOperation.IDLE) ) await hass.async_block_till_done() - state = hass.states.get("valve.test_my_valve") + state = hass.states.get("valve.test_myvalve") assert state is not None assert state.state == STATE_OPEN @@ -164,7 +164,7 @@ async def test_valve_entity_without_position( user_service=user_service, states=states, ) - state = hass.states.get("valve.test_my_valve") + state = hass.states.get("valve.test_myvalve") assert state is not None assert state.state == STATE_OPENING assert ATTR_CURRENT_POSITION not in state.attributes @@ -172,7 +172,7 @@ async def test_valve_entity_without_position( await hass.services.async_call( VALVE_DOMAIN, SERVICE_CLOSE_VALVE, - {ATTR_ENTITY_ID: "valve.test_my_valve"}, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, blocking=True, ) mock_client.valve_command.assert_has_calls([call(key=1, position=0.0)]) @@ -181,7 +181,7 @@ async def test_valve_entity_without_position( await hass.services.async_call( VALVE_DOMAIN, SERVICE_OPEN_VALVE, - {ATTR_ENTITY_ID: "valve.test_my_valve"}, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, blocking=True, ) mock_client.valve_command.assert_has_calls([call(key=1, position=1.0)]) @@ -191,6 +191,6 @@ async def test_valve_entity_without_position( ValveState(key=1, position=0.0, current_operation=ValveOperation.IDLE) ) await hass.async_block_till_done() - state = hass.states.get("valve.test_my_valve") + state = hass.states.get("valve.test_myvalve") assert state is not None assert state.state == STATE_CLOSED From f53da62026cc508d86703f45788418dfd8225597 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 10 Aug 2024 19:25:21 +0200 Subject: [PATCH 2136/2411] Extend ZHA attribute diagnostic information (#123199) * Include full attribute representation in in data * Extend attribute diagnostics for zha --- homeassistant/components/zha/diagnostics.py | 22 ++------ .../zha/snapshots/test_diagnostics.ambr | 50 ++++++++++++++----- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/zha/diagnostics.py b/homeassistant/components/zha/diagnostics.py index bc4738d032a..3e598c4d5f8 100644 --- a/homeassistant/components/zha/diagnostics.py +++ b/homeassistant/components/zha/diagnostics.py @@ -7,7 +7,7 @@ from importlib.metadata import version from typing import Any from zha.application.const import ( - ATTR_ATTRIBUTE_NAME, + ATTR_ATTRIBUTE, ATTR_DEVICE_TYPE, ATTR_IEEE, ATTR_IN_CLUSTERS, @@ -158,27 +158,13 @@ def get_endpoint_cluster_attr_data(zha_device: Device) -> dict: def get_cluster_attr_data(cluster: Cluster) -> dict: """Return cluster attribute data.""" - unsupported_attributes = {} - for u_attr in cluster.unsupported_attributes: - try: - u_attr_def = cluster.find_attribute(u_attr) - unsupported_attributes[f"0x{u_attr_def.id:04x}"] = { - ATTR_ATTRIBUTE_NAME: u_attr_def.name - } - except KeyError: - if isinstance(u_attr, int): - unsupported_attributes[f"0x{u_attr:04x}"] = {} - else: - unsupported_attributes[u_attr] = {} - return { ATTRIBUTES: { f"0x{attr_id:04x}": { - ATTR_ATTRIBUTE_NAME: attr_def.name, - ATTR_VALUE: attr_value, + ATTR_ATTRIBUTE: repr(attr_def), + ATTR_VALUE: cluster.get(attr_def.name), } for attr_id, attr_def in cluster.attributes.items() - if (attr_value := cluster.get(attr_def.name)) is not None }, - UNSUPPORTED_ATTRIBUTES: unsupported_attributes, + UNSUPPORTED_ATTRIBUTES: cluster.unsupported_attributes, } diff --git a/tests/components/zha/snapshots/test_diagnostics.ambr b/tests/components/zha/snapshots/test_diagnostics.ambr index 8899712b99d..67655aebc8c 100644 --- a/tests/components/zha/snapshots/test_diagnostics.ambr +++ b/tests/components/zha/snapshots/test_diagnostics.ambr @@ -161,8 +161,20 @@ 'in_clusters': dict({ '0x0500': dict({ 'attributes': dict({ + '0x0000': dict({ + 'attribute': "ZCLAttributeDef(id=0x0000, name='zone_state', type=, access=, mandatory=True, is_manufacturer_specific=False)", + 'value': None, + }), + '0x0001': dict({ + 'attribute': "ZCLAttributeDef(id=0x0001, name='zone_type', type=, access=, mandatory=True, is_manufacturer_specific=False)", + 'value': None, + }), + '0x0002': dict({ + 'attribute': "ZCLAttributeDef(id=0x0002, name='zone_status', type=, access=, mandatory=True, is_manufacturer_specific=False)", + 'value': None, + }), '0x0010': dict({ - 'attribute_name': 'cie_addr', + 'attribute': "ZCLAttributeDef(id=0x0010, name='cie_addr', type=, access=, mandatory=True, is_manufacturer_specific=False)", 'value': list([ 50, 79, @@ -174,27 +186,41 @@ 0, ]), }), - }), - 'endpoint_attribute': 'ias_zone', - 'unsupported_attributes': dict({ + '0x0011': dict({ + 'attribute': "ZCLAttributeDef(id=0x0011, name='zone_id', type=, access=, mandatory=True, is_manufacturer_specific=False)", + 'value': None, + }), '0x0012': dict({ - 'attribute_name': 'num_zone_sensitivity_levels_supported', + 'attribute': "ZCLAttributeDef(id=0x0012, name='num_zone_sensitivity_levels_supported', type=, access=, mandatory=False, is_manufacturer_specific=False)", + 'value': None, }), '0x0013': dict({ - 'attribute_name': 'current_zone_sensitivity_level', + 'attribute': "ZCLAttributeDef(id=0x0013, name='current_zone_sensitivity_level', type=, access=, mandatory=False, is_manufacturer_specific=False)", + 'value': None, }), }), + 'endpoint_attribute': 'ias_zone', + 'unsupported_attributes': list([ + 18, + 'current_zone_sensitivity_level', + ]), }), '0x0501': dict({ 'attributes': dict({ + '0xfffd': dict({ + 'attribute': "ZCLAttributeDef(id=0xFFFD, name='cluster_revision', type=, access=, mandatory=True, is_manufacturer_specific=False)", + 'value': None, + }), + '0xfffe': dict({ + 'attribute': "ZCLAttributeDef(id=0xFFFE, name='reporting_status', type=, access=, mandatory=False, is_manufacturer_specific=False)", + 'value': None, + }), }), 'endpoint_attribute': 'ias_ace', - 'unsupported_attributes': dict({ - '0x1000': dict({ - }), - 'unknown_attribute_name': dict({ - }), - }), + 'unsupported_attributes': list([ + 4096, + 'unknown_attribute_name', + ]), }), }), 'out_clusters': dict({ From f69507527b75eccc01dd2e9ba448b5533db0b1f0 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 10 Aug 2024 20:55:31 +0200 Subject: [PATCH 2137/2411] Make sure diagnostic data is output in deterministic order ZHA (#123551) Make sure diagnostic data is output in deterministic order Sets are not ordered, so the tests for this failed sporadically since it is converted into a list when converted to json. --- homeassistant/components/zha/diagnostics.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/diagnostics.py b/homeassistant/components/zha/diagnostics.py index 3e598c4d5f8..f276630dfee 100644 --- a/homeassistant/components/zha/diagnostics.py +++ b/homeassistant/components/zha/diagnostics.py @@ -166,5 +166,7 @@ def get_cluster_attr_data(cluster: Cluster) -> dict: } for attr_id, attr_def in cluster.attributes.items() }, - UNSUPPORTED_ATTRIBUTES: cluster.unsupported_attributes, + UNSUPPORTED_ATTRIBUTES: sorted( + cluster.unsupported_attributes, key=lambda v: (isinstance(v, str), v) + ), } From 7aed35b3f0043bb802aadc9f258ac37d0a743e0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Aug 2024 15:09:18 -0500 Subject: [PATCH 2138/2411] Bump aiohttp to 3.10.3 (#123549) --- 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 9a23a5e42e0..1f7c6bd61cc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-zlib==0.1.1 -aiohttp==3.10.2 +aiohttp==3.10.3 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index cb928c736a2..e94c9e96225 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.10.2", + "aiohttp==3.10.3", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index 7efddf5c765..556f9013cee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.10.2 +aiohttp==3.10.3 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 From 9be8616cc072e34eb624cc289eb6d5f854202310 Mon Sep 17 00:00:00 2001 From: Pavel Skuratovich Date: Sun, 11 Aug 2024 13:54:31 +0300 Subject: [PATCH 2139/2411] Add state_class to starline sensors to generate long-term statistics (#123540) * starline: Add state_class to sensors to generate long-term statistics * starline: Add 'errors' unit to 'errors' sensor --- homeassistant/components/starline/sensor.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index a53751a3b23..f9bd304c1e1 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -6,6 +6,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -30,47 +31,57 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( translation_key="battery", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="balance", translation_key="balance", + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ctemp", translation_key="interior_temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="etemp", translation_key="engine_temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="gsm_lvl", translation_key="gsm_signal", native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="fuel", translation_key="fuel", + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="errors", translation_key="errors", + native_unit_of_measurement="errors", entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="mileage", translation_key="mileage", native_unit_of_measurement=UnitOfLength.KILOMETERS, device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="gps_count", translation_key="gps_count", native_unit_of_measurement="satellites", + state_class=SensorStateClass.MEASUREMENT, ), ) From e93d0dfdfc43df4240b1e2d6918e3377b73f2bd2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 11 Aug 2024 14:15:20 +0200 Subject: [PATCH 2140/2411] Use setup method in coordinator for Trafikverket Train (#123138) * Use setup method in coordinator for Trafikverket Train * Overwrite types --- .../components/trafikverket_train/__init__.py | 28 ++-------------- .../trafikverket_train/coordinator.py | 32 +++++++++++++------ .../components/trafikverket_train/conftest.py | 2 +- .../trafikverket_train/test_config_flow.py | 2 +- .../trafikverket_train/test_init.py | 8 ++--- 5 files changed, 31 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/trafikverket_train/__init__.py b/homeassistant/components/trafikverket_train/__init__.py index 4bf1f681807..3e807df9301 100644 --- a/homeassistant/components/trafikverket_train/__init__.py +++ b/homeassistant/components/trafikverket_train/__init__.py @@ -2,21 +2,11 @@ from __future__ import annotations -from pytrafikverket import TrafikverketTrain -from pytrafikverket.exceptions import ( - InvalidAuthentication, - MultipleTrainStationsFound, - NoTrainStationFound, -) - from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_FROM, CONF_TO, PLATFORMS +from .const import PLATFORMS from .coordinator import TVDataUpdateCoordinator TVTrainConfigEntry = ConfigEntry[TVDataUpdateCoordinator] @@ -25,21 +15,7 @@ TVTrainConfigEntry = ConfigEntry[TVDataUpdateCoordinator] async def async_setup_entry(hass: HomeAssistant, entry: TVTrainConfigEntry) -> bool: """Set up Trafikverket Train from a config entry.""" - http_session = async_get_clientsession(hass) - train_api = TrafikverketTrain(http_session, entry.data[CONF_API_KEY]) - - try: - to_station = await train_api.async_get_train_station(entry.data[CONF_TO]) - from_station = await train_api.async_get_train_station(entry.data[CONF_FROM]) - except InvalidAuthentication as error: - raise ConfigEntryAuthFailed from error - except (NoTrainStationFound, MultipleTrainStationsFound) as error: - raise ConfigEntryNotReady( - f"Problem when trying station {entry.data[CONF_FROM]} to" - f" {entry.data[CONF_TO]}. Error: {error} " - ) from error - - coordinator = TVDataUpdateCoordinator(hass, to_station, from_station) + coordinator = TVDataUpdateCoordinator(hass) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/trafikverket_train/coordinator.py b/homeassistant/components/trafikverket_train/coordinator.py index 66ef3e6a1d2..16a7a649b85 100644 --- a/homeassistant/components/trafikverket_train/coordinator.py +++ b/homeassistant/components/trafikverket_train/coordinator.py @@ -10,7 +10,9 @@ from typing import TYPE_CHECKING from pytrafikverket import TrafikverketTrain from pytrafikverket.exceptions import ( InvalidAuthentication, + MultipleTrainStationsFound, NoTrainAnnouncementFound, + NoTrainStationFound, UnknownError, ) from pytrafikverket.models import StationInfoModel, TrainStopModel @@ -22,7 +24,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util -from .const import CONF_FILTER_PRODUCT, CONF_TIME, DOMAIN +from .const import CONF_FILTER_PRODUCT, CONF_FROM, CONF_TIME, CONF_TO, DOMAIN from .util import next_departuredate if TYPE_CHECKING: @@ -69,13 +71,10 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[TrainData]): """A Trafikverket Data Update Coordinator.""" config_entry: TVTrainConfigEntry + from_station: StationInfoModel + to_station: StationInfoModel - def __init__( - self, - hass: HomeAssistant, - to_station: StationInfoModel, - from_station: StationInfoModel, - ) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize the Trafikverket coordinator.""" super().__init__( hass, @@ -86,14 +85,29 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[TrainData]): self._train_api = TrafikverketTrain( async_get_clientsession(hass), self.config_entry.data[CONF_API_KEY] ) - self.from_station: StationInfoModel = from_station - self.to_station: StationInfoModel = to_station self._time: time | None = dt_util.parse_time(self.config_entry.data[CONF_TIME]) self._weekdays: list[str] = self.config_entry.data[CONF_WEEKDAY] self._filter_product: str | None = self.config_entry.options.get( CONF_FILTER_PRODUCT ) + async def _async_setup(self) -> None: + """Initiate stations.""" + try: + self.to_station = await self._train_api.async_get_train_station( + self.config_entry.data[CONF_TO] + ) + self.from_station = await self._train_api.async_get_train_station( + self.config_entry.data[CONF_FROM] + ) + except InvalidAuthentication as error: + raise ConfigEntryAuthFailed from error + except (NoTrainStationFound, MultipleTrainStationsFound) as error: + raise UpdateFailed( + f"Problem when trying station {self.config_entry.data[CONF_FROM]} to" + f" {self.config_entry.data[CONF_TO]}. Error: {error} " + ) from error + async def _async_update_data(self) -> TrainData: """Fetch data from Trafikverket.""" diff --git a/tests/components/trafikverket_train/conftest.py b/tests/components/trafikverket_train/conftest.py index 4915635e316..14671d27252 100644 --- a/tests/components/trafikverket_train/conftest.py +++ b/tests/components/trafikverket_train/conftest.py @@ -38,7 +38,7 @@ async def load_integration_from_entry( return_value=get_train_stop, ), patch( - "homeassistant.components.trafikverket_train.TrafikverketTrain.async_get_train_station", + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_station", ), ): await hass.config_entries.async_setup(config_entry_id) diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py index 400f396d355..83cc5a89016 100644 --- a/tests/components/trafikverket_train/test_config_flow.py +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -499,7 +499,7 @@ async def test_options_flow( with ( patch( - "homeassistant.components.trafikverket_train.TrafikverketTrain.async_get_train_station", + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_station", ), patch( "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", diff --git a/tests/components/trafikverket_train/test_init.py b/tests/components/trafikverket_train/test_init.py index 06598297dd1..c8fea174e83 100644 --- a/tests/components/trafikverket_train/test_init.py +++ b/tests/components/trafikverket_train/test_init.py @@ -34,7 +34,7 @@ async def test_unload_entry( with ( patch( - "homeassistant.components.trafikverket_train.TrafikverketTrain.async_get_train_station", + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_station", ), patch( "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", @@ -69,7 +69,7 @@ async def test_auth_failed( entry.add_to_hass(hass) with patch( - "homeassistant.components.trafikverket_train.TrafikverketTrain.async_get_train_station", + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_station", side_effect=InvalidAuthentication, ): await hass.config_entries.async_setup(entry.entry_id) @@ -99,7 +99,7 @@ async def test_no_stations( entry.add_to_hass(hass) with patch( - "homeassistant.components.trafikverket_train.TrafikverketTrain.async_get_train_station", + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_station", side_effect=NoTrainStationFound, ): await hass.config_entries.async_setup(entry.entry_id) @@ -135,7 +135,7 @@ async def test_migrate_entity_unique_id( with ( patch( - "homeassistant.components.trafikverket_train.TrafikverketTrain.async_get_train_station", + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_station", ), patch( "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", From be3e720c574cf77146bff8b72a739c9eb225ad97 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Sun, 11 Aug 2024 13:53:44 +0100 Subject: [PATCH 2141/2411] Add diagnostics platform to Mastodon (#123592) Diagnostics --- .../components/mastodon/diagnostics.py | 35 +++ .../mastodon/snapshots/test_diagnostics.ambr | 247 ++++++++++++++++++ tests/components/mastodon/test_diagnostics.py | 28 ++ 3 files changed, 310 insertions(+) create mode 100644 homeassistant/components/mastodon/diagnostics.py create mode 100644 tests/components/mastodon/snapshots/test_diagnostics.ambr create mode 100644 tests/components/mastodon/test_diagnostics.py diff --git a/homeassistant/components/mastodon/diagnostics.py b/homeassistant/components/mastodon/diagnostics.py new file mode 100644 index 00000000000..7246ae9cf63 --- /dev/null +++ b/homeassistant/components/mastodon/diagnostics.py @@ -0,0 +1,35 @@ +"""Diagnostics support for the Mastodon integration.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.core import HomeAssistant + +from . import MastodonConfigEntry + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: MastodonConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + + instance, account = await hass.async_add_executor_job( + get_diagnostics, + config_entry, + ) + + return { + "instance": instance, + "account": account, + } + + +def get_diagnostics(config_entry: MastodonConfigEntry) -> tuple[dict, dict]: + """Get mastodon diagnostics.""" + client = config_entry.runtime_data.client + + instance = client.instance() + account = client.account_verify_credentials() + + return instance, account diff --git a/tests/components/mastodon/snapshots/test_diagnostics.ambr b/tests/components/mastodon/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..982ecee7ee2 --- /dev/null +++ b/tests/components/mastodon/snapshots/test_diagnostics.ambr @@ -0,0 +1,247 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'account': dict({ + 'acct': 'trwnh', + 'avatar': 'https://files.mastodon.social/accounts/avatars/000/014/715/original/34aa222f4ae2e0a9.png', + 'avatar_static': 'https://files.mastodon.social/accounts/avatars/000/014/715/original/34aa222f4ae2e0a9.png', + 'bot': False, + 'created_at': '2016-11-24T10:02:12.085Z', + 'display_name': 'infinite love ⴳ', + 'emojis': list([ + dict({ + 'shortcode': 'fatyoshi', + 'static_url': 'https://files.mastodon.social/custom_emojis/images/000/023/920/static/e57ecb623faa0dc9.png', + 'url': 'https://files.mastodon.social/custom_emojis/images/000/023/920/original/e57ecb623faa0dc9.png', + 'visible_in_picker': True, + }), + ]), + 'fields': list([ + dict({ + 'name': 'Website', + 'value': 'trwnh.com', + 'verified_at': '2019-08-29T04:14:55.571+00:00', + }), + dict({ + 'name': 'Sponsor', + 'value': 'liberapay.com/at', + 'verified_at': '2019-11-15T10:06:15.557+00:00', + }), + dict({ + 'name': 'Fan of:', + 'value': 'Punk-rock and post-hardcore (Circa Survive, letlive., La Dispute, THE FEVER 333)Manga (Yu-Gi-Oh!, One Piece, JoJo's Bizarre Adventure, Death Note, Shaman King)Platformers and RPGs (Banjo-Kazooie, Boktai, Final Fantasy Crystal Chronicles)', + 'verified_at': None, + }), + dict({ + 'name': 'Main topics:', + 'value': 'systemic analysis, design patterns, anticapitalism, info/tech freedom, theory and philosophy, and otherwise being a genuine and decent wholesome poster. i'm just here to hang out and talk to cool people!', + 'verified_at': None, + }), + ]), + 'followers_count': 821, + 'following_count': 178, + 'header': 'https://files.mastodon.social/accounts/headers/000/014/715/original/5c6fc24edb3bb873.jpg', + 'header_static': 'https://files.mastodon.social/accounts/headers/000/014/715/original/5c6fc24edb3bb873.jpg', + 'id': '14715', + 'last_status_at': '2019-11-24T15:49:42.251Z', + 'locked': False, + 'note': '

i have approximate knowledge of many things. perpetual student. (nb/ace/they)

xmpp/email: a@trwnh.com
trwnh.com
help me live: liberapay.com/at or paypal.me/trwnh

- my triggers are moths and glitter
- i have all notifs except mentions turned off, so please interact if you wanna be friends! i literally will not notice otherwise
- dm me if i did something wrong, so i can improve
- purest person on fedi, do not lewd in my presence
- #1 ami cole fan account

:fatyoshi:

', + 'source': dict({ + 'fields': list([ + dict({ + 'name': 'Website', + 'value': 'https://trwnh.com', + 'verified_at': '2019-08-29T04:14:55.571+00:00', + }), + dict({ + 'name': 'Sponsor', + 'value': 'https://liberapay.com/at', + 'verified_at': '2019-11-15T10:06:15.557+00:00', + }), + dict({ + 'name': 'Fan of:', + 'value': "Punk-rock and post-hardcore (Circa Survive, letlive., La Dispute, THE FEVER 333)Manga (Yu-Gi-Oh!, One Piece, JoJo's Bizarre Adventure, Death Note, Shaman King)Platformers and RPGs (Banjo-Kazooie, Boktai, Final Fantasy Crystal Chronicles)", + 'verified_at': None, + }), + dict({ + 'name': 'Main topics:', + 'value': "systemic analysis, design patterns, anticapitalism, info/tech freedom, theory and philosophy, and otherwise being a genuine and decent wholesome poster. i'm just here to hang out and talk to cool people!", + 'verified_at': None, + }), + ]), + 'follow_requests_count': 0, + 'language': '', + 'note': ''' + i have approximate knowledge of many things. perpetual student. (nb/ace/they) + + xmpp/email: a@trwnh.com + https://trwnh.com + help me live: https://liberapay.com/at or https://paypal.me/trwnh + + - my triggers are moths and glitter + - i have all notifs except mentions turned off, so please interact if you wanna be friends! i literally will not notice otherwise + - dm me if i did something wrong, so i can improve + - purest person on fedi, do not lewd in my presence + - #1 ami cole fan account + + :fatyoshi: + ''', + 'privacy': 'public', + 'sensitive': False, + }), + 'statuses_count': 33120, + 'url': 'https://mastodon.social/@trwnh', + 'username': 'trwnh', + }), + 'instance': dict({ + 'configuration': dict({ + 'accounts': dict({ + 'max_featured_tags': 10, + 'max_pinned_statuses': 4, + }), + 'media_attachments': dict({ + 'image_matrix_limit': 16777216, + 'image_size_limit': 10485760, + 'supported_mime_types': list([ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/heic', + 'image/heif', + 'image/webp', + 'video/webm', + 'video/mp4', + 'video/quicktime', + 'video/ogg', + 'audio/wave', + 'audio/wav', + 'audio/x-wav', + 'audio/x-pn-wave', + 'audio/vnd.wave', + 'audio/ogg', + 'audio/vorbis', + 'audio/mpeg', + 'audio/mp3', + 'audio/webm', + 'audio/flac', + 'audio/aac', + 'audio/m4a', + 'audio/x-m4a', + 'audio/mp4', + 'audio/3gpp', + 'video/x-ms-asf', + ]), + 'video_frame_rate_limit': 60, + 'video_matrix_limit': 2304000, + 'video_size_limit': 41943040, + }), + 'polls': dict({ + 'max_characters_per_option': 50, + 'max_expiration': 2629746, + 'max_options': 4, + 'min_expiration': 300, + }), + 'statuses': dict({ + 'characters_reserved_per_url': 23, + 'max_characters': 500, + 'max_media_attachments': 4, + }), + 'translation': dict({ + 'enabled': True, + }), + 'urls': dict({ + 'streaming': 'wss://mastodon.social', + }), + 'vapid': dict({ + 'public_key': 'BCkMmVdKDnKYwzVCDC99Iuc9GvId-x7-kKtuHnLgfF98ENiZp_aj-UNthbCdI70DqN1zUVis-x0Wrot2sBagkMc=', + }), + }), + 'contact': dict({ + 'account': dict({ + 'acct': 'Gargron', + 'avatar': 'https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg', + 'avatar_static': 'https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg', + 'bot': False, + 'created_at': '2016-03-16T00:00:00.000Z', + 'discoverable': True, + 'display_name': 'Eugen 💀', + 'emojis': list([ + ]), + 'fields': list([ + dict({ + 'name': 'Patreon', + 'value': 'patreon.com/mastodon', + 'verified_at': None, + }), + ]), + 'followers_count': 133026, + 'following_count': 311, + 'group': False, + 'header': 'https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg', + 'header_static': 'https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg', + 'id': '1', + 'last_status_at': '2022-10-31', + 'locked': False, + 'noindex': False, + 'note': '

Founder, CEO and lead developer @Mastodon, Germany.

', + 'statuses_count': 72605, + 'url': 'https://mastodon.social/@Gargron', + 'username': 'Gargron', + }), + 'email': 'staff@mastodon.social', + }), + 'description': 'The original server operated by the Mastodon gGmbH non-profit', + 'domain': 'mastodon.social', + 'languages': list([ + 'en', + ]), + 'registrations': dict({ + 'approval_required': False, + 'enabled': False, + 'message': None, + }), + 'rules': list([ + dict({ + 'id': '1', + 'text': 'Sexually explicit or violent media must be marked as sensitive when posting', + }), + dict({ + 'id': '2', + 'text': 'No racism, sexism, homophobia, transphobia, xenophobia, or casteism', + }), + dict({ + 'id': '3', + 'text': 'No incitement of violence or promotion of violent ideologies', + }), + dict({ + 'id': '4', + 'text': 'No harassment, dogpiling or doxxing of other users', + }), + dict({ + 'id': '5', + 'text': 'No content illegal in Germany', + }), + dict({ + 'id': '7', + 'text': 'Do not share intentionally false or misleading information', + }), + ]), + 'source_url': 'https://github.com/mastodon/mastodon', + 'thumbnail': dict({ + 'blurhash': 'UeKUpFxuo~R%0nW;WCnhF6RjaJt757oJodS$', + 'url': 'https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png', + 'versions': dict({ + '@1x': 'https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png', + '@2x': 'https://files.mastodon.social/site_uploads/files/000/000/001/@2x/57c12f441d083cde.png', + }), + }), + 'title': 'Mastodon', + 'usage': dict({ + 'users': dict({ + 'active_month': 123122, + }), + }), + 'version': '4.0.0rc1', + }), + }) +# --- diff --git a/tests/components/mastodon/test_diagnostics.py b/tests/components/mastodon/test_diagnostics.py new file mode 100644 index 00000000000..c2de15d1a51 --- /dev/null +++ b/tests/components/mastodon/test_diagnostics.py @@ -0,0 +1,28 @@ +"""Test Mastodon diagnostics.""" + +from unittest.mock import AsyncMock + +from syrupy import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + await setup_integration(hass, mock_config_entry) + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot + ) From b392d6139131aac2895ae1c8e43f9642474a1684 Mon Sep 17 00:00:00 2001 From: Carlos Gustavo Sarmiento Date: Sun, 11 Aug 2024 11:50:11 -0500 Subject: [PATCH 2142/2411] Update MPD Player to use HOST and PORT to detect duplicate configs (#123410) * Allow Monetary device_class to accept `Measurement` state_class * Update MPD Player to use HOST and PORT to detect duplicate configs --- homeassistant/components/mpd/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mpd/config_flow.py b/homeassistant/components/mpd/config_flow.py index 619fb8936e2..f37ebe5e5e8 100644 --- a/homeassistant/components/mpd/config_flow.py +++ b/homeassistant/components/mpd/config_flow.py @@ -32,7 +32,9 @@ class MPDConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a flow initiated by the user.""" errors = {} if user_input: - self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) client = MPDClient() client.timeout = 30 client.idletimeout = 10 From 766733b3b26104552700365ef2f51b32f60d4f4d Mon Sep 17 00:00:00 2001 From: wittypluck Date: Sun, 11 Aug 2024 19:14:43 +0200 Subject: [PATCH 2143/2411] Avoid Exception on Glances missing key (#114628) * Handle case of sensors removed server side * Update available state on value update * Set uptime to None if key is missing * Replace _attr_available by _data_valid --- .../components/glances/coordinator.py | 10 ++--- homeassistant/components/glances/sensor.py | 25 ++++++------ tests/components/glances/test_sensor.py | 38 +++++++++++++++++++ 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/glances/coordinator.py b/homeassistant/components/glances/coordinator.py index 4e5bdcc1543..8882b097ba9 100644 --- a/homeassistant/components/glances/coordinator.py +++ b/homeassistant/components/glances/coordinator.py @@ -45,15 +45,13 @@ class GlancesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): except exceptions.GlancesApiError as err: raise UpdateFailed from err # Update computed values - uptime: datetime | None = self.data["computed"]["uptime"] if self.data else None + uptime: datetime | None = None up_duration: timedelta | None = None - if up_duration := parse_duration(data.get("uptime")): + if "uptime" in data and (up_duration := parse_duration(data["uptime"])): + uptime = self.data["computed"]["uptime"] if self.data else None # Update uptime if previous value is None or previous uptime is bigger than # new uptime (i.e. server restarted) - if ( - self.data is None - or self.data["computed"]["uptime_duration"] > up_duration - ): + if uptime is None or self.data["computed"]["uptime_duration"] > up_duration: uptime = utcnow() - up_duration data["computed"] = {"uptime_duration": up_duration, "uptime": uptime} return data or {} diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index a1cb8e47b9d..59eba69d60a 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -325,6 +325,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit entity_description: GlancesSensorEntityDescription _attr_has_entity_name = True + _data_valid: bool = False def __init__( self, @@ -351,14 +352,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit @property def available(self) -> bool: """Set sensor unavailable when native value is invalid.""" - if super().available: - return ( - not self._numeric_state_expected - or isinstance(value := self.native_value, (int, float)) - or isinstance(value, str) - and value.isnumeric() - ) - return False + return super().available and self._data_valid @callback def _handle_coordinator_update(self) -> None: @@ -368,10 +362,19 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit def _update_native_value(self) -> None: """Update sensor native value from coordinator data.""" - data = self.coordinator.data[self.entity_description.type] - if dict_val := data.get(self._sensor_label): + data = self.coordinator.data.get(self.entity_description.type) + if data and (dict_val := data.get(self._sensor_label)): self._attr_native_value = dict_val.get(self.entity_description.key) - elif self.entity_description.key in data: + elif data and (self.entity_description.key in data): self._attr_native_value = data.get(self.entity_description.key) else: self._attr_native_value = None + self._update_data_valid() + + def _update_data_valid(self) -> None: + self._data_valid = self._attr_native_value is not None and ( + not self._numeric_state_expected + or isinstance(self._attr_native_value, (int, float)) + or isinstance(self._attr_native_value, str) + and self._attr_native_value.isnumeric() + ) diff --git a/tests/components/glances/test_sensor.py b/tests/components/glances/test_sensor.py index 7dee47680ed..8e0367a712c 100644 --- a/tests/components/glances/test_sensor.py +++ b/tests/components/glances/test_sensor.py @@ -7,6 +7,7 @@ from freezegun.api import FrozenDateTimeFactory from syrupy import SnapshotAssertion from homeassistant.components.glances.const import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -71,3 +72,40 @@ async def test_uptime_variation( async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("sensor.0_0_0_0_uptime").state == "2024-02-15T12:49:52+00:00" + + +async def test_sensor_removed( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_api: AsyncMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test sensor removed server side.""" + + # Init with reference time + freezer.move_to(MOCK_REFERENCE_DATE) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, entry_id="test") + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.0_0_0_0_ssl_disk_used").state != STATE_UNAVAILABLE + assert hass.states.get("sensor.0_0_0_0_memory_use").state != STATE_UNAVAILABLE + assert hass.states.get("sensor.0_0_0_0_uptime").state != STATE_UNAVAILABLE + + # Remove some sensors from Glances API data + mock_data = HA_SENSOR_DATA.copy() + mock_data.pop("fs") + mock_data.pop("mem") + mock_data.pop("uptime") + mock_api.return_value.get_ha_sensor_data = AsyncMock(return_value=mock_data) + + # Server stops providing some sensors, so state should switch to Unavailable + freezer.move_to(MOCK_REFERENCE_DATE + timedelta(minutes=2)) + freezer.tick(delta=timedelta(seconds=120)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("sensor.0_0_0_0_ssl_disk_used").state == STATE_UNAVAILABLE + assert hass.states.get("sensor.0_0_0_0_memory_use").state == STATE_UNAVAILABLE + assert hass.states.get("sensor.0_0_0_0_uptime").state == STATE_UNAVAILABLE From a040f1a9d15621c8ae6f13fddea35e1ad6ede95f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 11 Aug 2024 19:56:12 +0200 Subject: [PATCH 2144/2411] Bump `aioshelly` to version 11.2.0 (#123602) Bump aioshelly to version 11.2.0 --- 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 1e65a51733d..c742b45632c 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==11.1.0"], + "requirements": ["aioshelly==11.2.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 29146455616..f1900dea123 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -359,7 +359,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==11.1.0 +aioshelly==11.2.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 46889f670ee..63e63f48e56 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==11.1.0 +aioshelly==11.2.0 # homeassistant.components.skybell aioskybell==22.7.0 From 4daefe0b6e9b7f83e907da2adcaa7d165f5a9098 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 11 Aug 2024 22:37:33 +0200 Subject: [PATCH 2145/2411] Remove deprecated fan as light in lutron (#123607) * Remove deprecated fan as light in lutron * Remove more --- homeassistant/components/lutron/__init__.py | 2 - homeassistant/components/lutron/light.py | 84 +------------------- homeassistant/components/lutron/strings.json | 14 ---- 3 files changed, 4 insertions(+), 96 deletions(-) diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 1521a05df8e..45a51eb6df8 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -82,8 +82,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b elif output.type == "CEILING_FAN_TYPE": entry_data.fans.append((area.name, output)) platform = Platform.FAN - # Deprecated, should be removed in 2024.8 - entry_data.lights.append((area.name, output)) elif output.is_dimmable: entry_data.lights.append((area.name, output)) platform = Platform.LIGHT diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index eb003fd431a..7e8829b231c 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -3,12 +3,10 @@ from __future__ import annotations from collections.abc import Mapping -import logging from typing import Any from pylutron import Output -from homeassistant.components.automation import automations_with_entity from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_FLASH, @@ -17,23 +15,13 @@ from homeassistant.components.light import ( LightEntity, LightEntityFeature, ) -from homeassistant.components.script import scripts_with_entity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import ( - IssueSeverity, - async_create_issue, - create_issue, -) from . import DOMAIN, LutronData from .entity import LutronDevice -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, @@ -45,50 +33,13 @@ async def async_setup_entry( Adds dimmers from the Main Repeater associated with the config_entry as light entities. """ - ent_reg = er.async_get(hass) entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id] - lights = [] - - for area_name, device in entry_data.lights: - if device.type == "CEILING_FAN_TYPE": - # If this is a fan, check to see if this entity already exists. - # If not, do not create a new one. - entity_id = ent_reg.async_get_entity_id( - Platform.LIGHT, - DOMAIN, - f"{entry_data.client.guid}_{device.uuid}", - ) - if entity_id: - entity_entry = ent_reg.async_get(entity_id) - assert entity_entry - if entity_entry.disabled: - # If the entity exists and is disabled then we want to remove - # the entity so that the user is using the new fan entity instead. - ent_reg.async_remove(entity_id) - else: - lights.append(LutronLight(area_name, device, entry_data.client)) - entity_automations = automations_with_entity(hass, entity_id) - entity_scripts = scripts_with_entity(hass, entity_id) - for item in entity_automations + entity_scripts: - async_create_issue( - hass, - DOMAIN, - f"deprecated_light_fan_{entity_id}_{item}", - breaks_in_ha_version="2024.8.0", - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_light_fan_entity", - translation_placeholders={ - "entity": entity_id, - "info": item, - }, - ) - else: - lights.append(LutronLight(area_name, device, entry_data.client)) async_add_entities( - lights, + ( + LutronLight(area_name, device, entry_data.client) + for area_name, device in entry_data.lights + ), True, ) @@ -113,24 +64,8 @@ class LutronLight(LutronDevice, LightEntity): _prev_brightness: int | None = None _attr_name = None - def __init__(self, area_name, lutron_device, controller) -> None: - """Initialize the light.""" - super().__init__(area_name, lutron_device, controller) - self._is_fan = lutron_device.type == "CEILING_FAN_TYPE" - def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - if self._is_fan: - create_issue( - self.hass, - DOMAIN, - "deprecated_light_fan_on", - breaks_in_ha_version="2024.8.0", - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_light_fan_on", - ) if flash := kwargs.get(ATTR_FLASH): self._lutron_device.flash(0.5 if flash == "short" else 1.5) else: @@ -148,17 +83,6 @@ class LutronLight(LutronDevice, LightEntity): def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - if self._is_fan: - create_issue( - self.hass, - DOMAIN, - "deprecated_light_fan_off", - breaks_in_ha_version="2024.8.0", - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_light_fan_off", - ) args = {"new_level": 0} if ATTR_TRANSITION in kwargs: args["fade_time_seconds"] = kwargs[ATTR_TRANSITION] diff --git a/homeassistant/components/lutron/strings.json b/homeassistant/components/lutron/strings.json index d5197375dc1..770a453eb9e 100644 --- a/homeassistant/components/lutron/strings.json +++ b/homeassistant/components/lutron/strings.json @@ -36,19 +36,5 @@ } } } - }, - "issues": { - "deprecated_light_fan_entity": { - "title": "Detected Lutron fan entity created as a light", - "description": "Fan entities have been added to the Lutron integration.\nWe detected that entity `{entity}` is being used in `{info}`\n\nWe have created a new fan entity and you should migrate `{info}` to use this new entity.\n\nWhen you are done migrating `{info}` and are ready to have the deprecated light entity removed, disable the entity and restart Home Assistant." - }, - "deprecated_light_fan_on": { - "title": "The Lutron integration deprecated fan turned on", - "description": "Fan entities have been added to the Lutron integration.\nPreviously fans were created as lights; this behavior is now deprecated.\n\nYour configuration just turned on a fan created as a light. You should migrate your scenes and automations to use the new fan entity.\n\nWhen you are done migrating your automations and are ready to have the deprecated light entity removed, disable the entity and restart Home Assistant.\n\nAn issue will be created each time the incorrect entity is used to remind you to migrate." - }, - "deprecated_light_fan_off": { - "title": "The Lutron integration deprecated fan turned off", - "description": "Fan entities have been added to the Lutron integration.\nPreviously fans were created as lights; this behavior is now deprecated.\n\nYour configuration just turned off a fan created as a light. You should migrate your scenes and automations to use the new fan entity.\n\nWhen you are done migrating your automations and are ready to have the deprecated light entity removed, disable the entity and restart Home Assistant.\n\nAn issue will be created each time the incorrect entity is used to remind you to migrate." - } } } From 4a099ab9423f4bc8b445bf437df0ae8739d7f248 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 11 Aug 2024 22:38:20 +0200 Subject: [PATCH 2146/2411] Remove deprecated yaml import from lupusec (#123606) --- homeassistant/components/lupusec/__init__.py | 83 +----------------- .../components/lupusec/config_flow.py | 39 +-------- homeassistant/components/lupusec/const.py | 4 - homeassistant/components/lupusec/strings.json | 10 --- tests/components/lupusec/test_config_flow.py | 85 ------------------- 5 files changed, 4 insertions(+), 217 deletions(-) diff --git a/homeassistant/components/lupusec/__init__.py b/homeassistant/components/lupusec/__init__.py index 51bba44aef0..c0593674972 100644 --- a/homeassistant/components/lupusec/__init__.py +++ b/homeassistant/components/lupusec/__init__.py @@ -5,24 +5,10 @@ import logging import lupupy from lupupy.exceptions import LupusecException -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_IP_ADDRESS, - CONF_NAME, - CONF_PASSWORD, - CONF_USERNAME, - Platform, -) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType - -from .const import INTEGRATION_TITLE, ISSUE_PLACEHOLDER +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant _LOGGER = logging.getLogger(__name__) @@ -31,19 +17,6 @@ DOMAIN = "lupusec" NOTIFICATION_ID = "lupusec_notification" NOTIFICATION_TITLE = "Lupusec Security Setup" -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_IP_ADDRESS): cv.string, - vol.Optional(CONF_NAME): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) PLATFORMS: list[Platform] = [ Platform.ALARM_CONTROL_PANEL, @@ -52,56 +25,6 @@ PLATFORMS: list[Platform] = [ ] -async def handle_async_init_result(hass: HomeAssistant, domain: str, conf: dict): - """Handle the result of the async_init to issue deprecated warnings.""" - flow = hass.config_entries.flow - result = await flow.async_init(domain, context={"source": SOURCE_IMPORT}, data=conf) - - if ( - result["type"] == FlowResultType.CREATE_ENTRY - or result["reason"] == "already_configured" - ): - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.8.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": INTEGRATION_TITLE, - }, - ) - else: - async_create_issue( - hass, - DOMAIN, - f"deprecated_yaml_import_issue_{result['reason']}", - breaks_in_ha_version="2024.8.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_{result['reason']}", - translation_placeholders=ISSUE_PLACEHOLDER, - ) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the lupusec integration.""" - - if DOMAIN not in config: - return True - - conf = config[DOMAIN] - - hass.async_create_task(handle_async_init_result(hass, DOMAIN, conf)) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up this integration using UI.""" diff --git a/homeassistant/components/lupusec/config_flow.py b/homeassistant/components/lupusec/config_flow.py index 82162bccf80..45b2b2b0cd8 100644 --- a/homeassistant/components/lupusec/config_flow.py +++ b/homeassistant/components/lupusec/config_flow.py @@ -8,13 +8,7 @@ import lupupy import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import ( - CONF_HOST, - CONF_IP_ADDRESS, - CONF_NAME, - CONF_PASSWORD, - CONF_USERNAME, -) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -66,37 +60,6 @@ class LupusecConfigFlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: - """Import the yaml config.""" - self._async_abort_entries_match( - { - CONF_HOST: user_input[CONF_IP_ADDRESS], - CONF_USERNAME: user_input[CONF_USERNAME], - CONF_PASSWORD: user_input[CONF_PASSWORD], - } - ) - host = user_input[CONF_IP_ADDRESS] - username = user_input[CONF_USERNAME] - password = user_input[CONF_PASSWORD] - try: - await test_host_connection(self.hass, host, username, password) - except CannotConnect: - return self.async_abort(reason="cannot_connect") - except JSONDecodeError: - return self.async_abort(reason="cannot_connect") - except Exception: - _LOGGER.exception("Unexpected exception") - return self.async_abort(reason="unknown") - - return self.async_create_entry( - title=user_input.get(CONF_NAME, host), - data={ - CONF_HOST: host, - CONF_USERNAME: username, - CONF_PASSWORD: password, - }, - ) - async def test_host_connection( hass: HomeAssistant, host: str, username: str, password: str diff --git a/homeassistant/components/lupusec/const.py b/homeassistant/components/lupusec/const.py index 489d878306d..4904bc481a7 100644 --- a/homeassistant/components/lupusec/const.py +++ b/homeassistant/components/lupusec/const.py @@ -18,10 +18,6 @@ from lupupy.constants import ( DOMAIN = "lupusec" -INTEGRATION_TITLE = "Lupus Electronics LUPUSEC" -ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=lupusec"} - - TYPE_TRANSLATION = { TYPE_WINDOW: "Fensterkontakt", TYPE_DOOR: "Türkontakt", diff --git a/homeassistant/components/lupusec/strings.json b/homeassistant/components/lupusec/strings.json index 6fa59aaeb3d..907232e0665 100644 --- a/homeassistant/components/lupusec/strings.json +++ b/homeassistant/components/lupusec/strings.json @@ -17,15 +17,5 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } - }, - "issues": { - "deprecated_yaml_import_issue_cannot_connect": { - "title": "The Lupus Electronics LUPUSEC YAML configuration import failed", - "description": "Configuring Lupus Electronics LUPUSEC using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to Lupus Electronics LUPUSEC works and restart Home Assistant to try again or remove the Lupus Electronics LUPUSEC YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - }, - "deprecated_yaml_import_issue_unknown": { - "title": "The Lupus Electronics LUPUSEC YAML configuration import failed", - "description": "Configuring Lupus Electronics LUPUSEC using YAML is being removed but there was an unknown error when trying to import the YAML configuration.\n\nEnsure the imported configuration is correct and remove the Lupus Electronics LUPUSEC YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - } } } diff --git a/tests/components/lupusec/test_config_flow.py b/tests/components/lupusec/test_config_flow.py index e106bbd5001..f354eaf0644 100644 --- a/tests/components/lupusec/test_config_flow.py +++ b/tests/components/lupusec/test_config_flow.py @@ -153,88 +153,3 @@ async def test_flow_user_init_data_already_configured(hass: HomeAssistant) -> No assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" - - -@pytest.mark.parametrize( - ("mock_import_step", "mock_title"), - [ - (MOCK_IMPORT_STEP, MOCK_IMPORT_STEP[CONF_IP_ADDRESS]), - (MOCK_IMPORT_STEP_NAME, MOCK_IMPORT_STEP_NAME[CONF_NAME]), - ], -) -async def test_flow_source_import( - hass: HomeAssistant, mock_import_step, mock_title -) -> None: - """Test configuration import from YAML.""" - with ( - patch( - "homeassistant.components.lupusec.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - patch( - "homeassistant.components.lupusec.config_flow.lupupy.Lupusec", - ) as mock_initialize_lupusec, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=mock_import_step, - ) - - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == mock_title - assert result["data"] == MOCK_DATA_STEP - assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_initialize_lupusec.mock_calls) == 1 - - -@pytest.mark.parametrize( - ("raise_error", "text_error"), - [ - (LupusecException("Test lupusec exception"), "cannot_connect"), - (JSONDecodeError("Test JSONDecodeError", "test", 1), "cannot_connect"), - (Exception("Test unknown exception"), "unknown"), - ], -) -async def test_flow_source_import_error_and_recover( - hass: HomeAssistant, raise_error, text_error -) -> None: - """Test exceptions and recovery.""" - - with patch( - "homeassistant.components.lupusec.config_flow.lupupy.Lupusec", - side_effect=raise_error, - ) as mock_initialize_lupusec: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_IMPORT_STEP, - ) - - await hass.async_block_till_done() - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == text_error - assert len(mock_initialize_lupusec.mock_calls) == 1 - - -async def test_flow_source_import_already_configured(hass: HomeAssistant) -> None: - """Test duplicate config entry..""" - - entry = MockConfigEntry( - domain=DOMAIN, - title=MOCK_DATA_STEP[CONF_HOST], - data=MOCK_DATA_STEP, - ) - - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_IMPORT_STEP, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" From ca34bac479c08e678a5c3729d351147928b47689 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 11 Aug 2024 22:38:59 +0200 Subject: [PATCH 2147/2411] Remove deprecated horn switch in starline (#123608) --- homeassistant/components/starline/strings.json | 9 --------- homeassistant/components/starline/switch.py | 16 ---------------- 2 files changed, 25 deletions(-) diff --git a/homeassistant/components/starline/strings.json b/homeassistant/components/starline/strings.json index 6f0c42f0882..14a8ed5a035 100644 --- a/homeassistant/components/starline/strings.json +++ b/homeassistant/components/starline/strings.json @@ -114,9 +114,6 @@ "additional_channel": { "name": "Additional channel" }, - "horn": { - "name": "Horn" - }, "service_mode": { "name": "Service mode" } @@ -127,12 +124,6 @@ } } }, - "issues": { - "deprecated_horn_switch": { - "title": "The Starline Horn switch entity is being removed", - "description": "Using the Horn switch is now deprecated and will be removed in a future version of Home Assistant.\n\nPlease adjust any automations or scripts that use Horn switch entity to instead use the Horn button entity." - } - }, "services": { "update_state": { "name": "Update state", diff --git a/homeassistant/components/starline/switch.py b/homeassistant/components/starline/switch.py index 8ca736d2ac5..1b48a72c732 100644 --- a/homeassistant/components/starline/switch.py +++ b/homeassistant/components/starline/switch.py @@ -8,7 +8,6 @@ from homeassistant.components.switch import SwitchEntity, SwitchEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from .account import StarlineAccount, StarlineDevice from .const import DOMAIN @@ -27,11 +26,6 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( key="out", translation_key="additional_channel", ), - # Deprecated and should be removed in 2024.8 - SwitchEntityDescription( - key="poke", - translation_key="horn", - ), SwitchEntityDescription( key="valet", translation_key="service_mode", @@ -90,16 +84,6 @@ class StarlineSwitch(StarlineEntity, SwitchEntity): def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - if self._key == "poke": - create_issue( - self.hass, - DOMAIN, - "deprecated_horn_switch", - breaks_in_ha_version="2024.8.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_horn_switch", - ) self._account.api.set_car_state(self._device.device_id, self._key, True) def turn_off(self, **kwargs: Any) -> None: From e1336a197508ef8ec5e4908be266ec80b4a1eb74 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 12 Aug 2024 08:55:24 +0200 Subject: [PATCH 2148/2411] Update knx-frontend to 2024.8.9.225351 (#123557) --- 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 62364f641f4..6974ee300f5 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.0.0", "xknxproject==3.7.1", - "knx-frontend==2024.8.6.211307" + "knx-frontend==2024.8.9.225351" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index f1900dea123..f1bad6ec1b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1219,7 +1219,7 @@ kiwiki-client==0.1.1 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.8.6.211307 +knx-frontend==2024.8.9.225351 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63e63f48e56..c4d10f58d83 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1018,7 +1018,7 @@ kegtron-ble==0.4.0 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.8.6.211307 +knx-frontend==2024.8.9.225351 # homeassistant.components.konnected konnected==1.2.0 From 5b6bfa9ac8114e458c6ee6e829f6cffbd2804b95 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 12 Aug 2024 09:04:12 +0200 Subject: [PATCH 2149/2411] Remove Spotify scope check (#123545) --- homeassistant/components/spotify/media_player.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index bd1bcdfd43e..3653bdb149a 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -31,7 +31,7 @@ from homeassistant.util.dt import utcnow from . import SpotifyConfigEntry from .browse_media import async_browse_media_internal -from .const import DOMAIN, MEDIA_PLAYER_PREFIX, PLAYABLE_MEDIA_TYPES, SPOTIFY_SCOPES +from .const import DOMAIN, MEDIA_PLAYER_PREFIX, PLAYABLE_MEDIA_TYPES from .models import HomeAssistantSpotifyData from .util import fetch_image_url @@ -138,10 +138,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity): entry_type=DeviceEntryType.SERVICE, configuration_url="https://open.spotify.com", ) - - self._scope_ok = set(data.session.token["scope"].split(" ")).issuperset( - SPOTIFY_SCOPES - ) self._currently_playing: dict | None = {} self._playlist: dict | None = None self._restricted_device: bool = False @@ -459,13 +455,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity): ) -> BrowseMedia: """Implement the websocket media browsing helper.""" - if not self._scope_ok: - _LOGGER.debug( - "Spotify scopes are not set correctly, this can impact features such as" - " media browsing" - ) - raise NotImplementedError - return await async_browse_media_internal( self.hass, self.data.client, From 6343a086e497659bd0bdceeff9359b9bfc696eae Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Aug 2024 09:08:40 +0200 Subject: [PATCH 2150/2411] Remove deprecated process sensor from System monitor (#123616) --- .../components/systemmonitor/__init__.py | 14 +- .../components/systemmonitor/config_flow.py | 2 +- .../components/systemmonitor/repairs.py | 72 ------- .../components/systemmonitor/sensor.py | 63 +----- .../components/systemmonitor/strings.json | 13 -- .../snapshots/test_diagnostics.ambr | 2 +- .../systemmonitor/snapshots/test_sensor.ambr | 18 -- tests/components/systemmonitor/test_init.py | 46 +++- .../components/systemmonitor/test_repairs.py | 199 ------------------ tests/components/systemmonitor/test_sensor.py | 61 +----- 10 files changed, 60 insertions(+), 430 deletions(-) delete mode 100644 homeassistant/components/systemmonitor/repairs.py delete mode 100644 tests/components/systemmonitor/test_repairs.py diff --git a/homeassistant/components/systemmonitor/__init__.py b/homeassistant/components/systemmonitor/__init__.py index 3fbc9edec2a..4a794a00432 100644 --- a/homeassistant/components/systemmonitor/__init__.py +++ b/homeassistant/components/systemmonitor/__init__.py @@ -73,7 +73,11 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Migrate old entry.""" - if entry.version == 1: + if entry.version > 1: + # This means the user has downgraded from a future version + return False + + if entry.version == 1 and entry.minor_version < 3: new_options = {**entry.options} if entry.minor_version == 1: # Migration copies process sensors to binary sensors @@ -84,6 +88,14 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, options=new_options, version=1, minor_version=2 ) + if entry.minor_version == 2: + new_options = {**entry.options} + if SENSOR_DOMAIN in new_options: + new_options.pop(SENSOR_DOMAIN) + hass.config_entries.async_update_entry( + entry, options=new_options, version=1, minor_version=3 + ) + _LOGGER.debug( "Migration to version %s.%s successful", entry.version, entry.minor_version ) diff --git a/homeassistant/components/systemmonitor/config_flow.py b/homeassistant/components/systemmonitor/config_flow.py index 0ff882d89da..34b28a1d47a 100644 --- a/homeassistant/components/systemmonitor/config_flow.py +++ b/homeassistant/components/systemmonitor/config_flow.py @@ -95,7 +95,7 @@ class SystemMonitorConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW VERSION = 1 - MINOR_VERSION = 2 + MINOR_VERSION = 3 def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" diff --git a/homeassistant/components/systemmonitor/repairs.py b/homeassistant/components/systemmonitor/repairs.py deleted file mode 100644 index 10b5d18830d..00000000000 --- a/homeassistant/components/systemmonitor/repairs.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Repairs platform for the System Monitor integration.""" - -from __future__ import annotations - -from typing import Any, cast - -from homeassistant import data_entry_flow -from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - - -class ProcessFixFlow(RepairsFlow): - """Handler for an issue fixing flow.""" - - def __init__(self, entry: ConfigEntry, processes: list[str]) -> None: - """Create flow.""" - super().__init__() - self.entry = entry - self._processes = processes - - 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_migrate_process_sensor() - - async def async_step_migrate_process_sensor( - self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: - """Handle the options step of a fix flow.""" - if user_input is None: - return self.async_show_form( - step_id="migrate_process_sensor", - description_placeholders={"processes": ", ".join(self._processes)}, - ) - - # Migration has copied the sensors to binary sensors - # Pop the sensors to repair and remove entities - new_options: dict[str, Any] = self.entry.options.copy() - new_options.pop(SENSOR_DOMAIN) - - entity_reg = er.async_get(self.hass) - entries = er.async_entries_for_config_entry(entity_reg, self.entry.entry_id) - for entry in entries: - if entry.entity_id.startswith("sensor.") and entry.unique_id.startswith( - "process_" - ): - entity_reg.async_remove(entry.entity_id) - - self.hass.config_entries.async_update_entry(self.entry, options=new_options) - await self.hass.config_entries.async_reload(self.entry.entry_id) - return self.async_create_entry(data={}) - - -async def async_create_fix_flow( - hass: HomeAssistant, - issue_id: str, - data: dict[str, Any] | None, -) -> RepairsFlow: - """Create flow.""" - entry = None - if data and (entry_id := data.get("entry_id")): - entry_id = cast(str, entry_id) - processes: list[str] = data["processes"] - entry = hass.config_entries.async_get_entry(entry_id) - assert entry - return ProcessFixFlow(entry, processes) - - return ConfirmRepairFlow() diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index bad4c3be0b5..ef1153f09e8 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -14,8 +14,6 @@ import sys import time from typing import Any, Literal -from psutil import NoSuchProcess - from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, @@ -25,8 +23,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( PERCENTAGE, - STATE_OFF, - STATE_ON, EntityCategory, UnitOfDataRate, UnitOfInformation, @@ -36,13 +32,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify from . import SystemMonitorConfigEntry -from .const import CONF_PROCESS, DOMAIN, NET_IO_TYPES +from .const import DOMAIN, NET_IO_TYPES from .coordinator import SystemMonitorCoordinator from .util import get_all_disk_mounts, get_all_network_interfaces, read_cpu_temperature @@ -68,24 +63,6 @@ def get_cpu_icon() -> Literal["mdi:cpu-64-bit", "mdi:cpu-32-bit"]: return "mdi:cpu-32-bit" -def get_process(entity: SystemMonitorSensor) -> str: - """Return process.""" - state = STATE_OFF - for proc in entity.coordinator.data.processes: - try: - _LOGGER.debug("process %s for argument %s", proc.name(), entity.argument) - if entity.argument == proc.name(): - state = STATE_ON - break - except NoSuchProcess as err: - _LOGGER.warning( - "Failed to load process with ID: %s, old name: %s", - err.pid, - err.name, - ) - return state - - def get_network(entity: SystemMonitorSensor) -> float | None: """Return network in and out.""" counters = entity.coordinator.data.io_counters @@ -341,15 +318,6 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { value_fn=get_throughput, add_to_update=lambda entity: ("io_counters", ""), ), - "process": SysMonitorSensorEntityDescription( - key="process", - translation_key="process", - placeholder="process", - icon=get_cpu_icon(), - mandatory_arg=True, - value_fn=get_process, - add_to_update=lambda entity: ("processes", ""), - ), "processor_use": SysMonitorSensorEntityDescription( key="processor_use", translation_key="processor_use", @@ -551,35 +519,6 @@ async def async_setup_entry( ) continue - if _type == "process": - _entry = entry.options.get(SENSOR_DOMAIN, {}) - for argument in _entry.get(CONF_PROCESS, []): - loaded_resources.add(slugify(f"{_type}_{argument}")) - entities.append( - SystemMonitorSensor( - coordinator, - sensor_description, - entry.entry_id, - argument, - True, - ) - ) - async_create_issue( - hass, - DOMAIN, - "process_sensor", - breaks_in_ha_version="2024.9.0", - is_fixable=True, - is_persistent=False, - severity=IssueSeverity.WARNING, - translation_key="process_sensor", - data={ - "entry_id": entry.entry_id, - "processes": _entry[CONF_PROCESS], - }, - ) - continue - if _type == "processor_use": argument = "" is_enabled = check_legacy_resource(f"{_type}_{argument}", legacy_resources) diff --git a/homeassistant/components/systemmonitor/strings.json b/homeassistant/components/systemmonitor/strings.json index aae2463c9da..dde97918bc3 100644 --- a/homeassistant/components/systemmonitor/strings.json +++ b/homeassistant/components/systemmonitor/strings.json @@ -22,19 +22,6 @@ } } }, - "issues": { - "process_sensor": { - "title": "Process sensors are deprecated and will be removed", - "fix_flow": { - "step": { - "migrate_process_sensor": { - "title": "Process sensors have been setup as binary sensors", - "description": "Process sensors `{processes}` have been created as binary sensors and the sensors will be removed in 2024.9.0.\n\nPlease update all automations, scripts, dashboards or other things depending on these sensors to use the newly created binary sensors instead and press **Submit** to fix this issue." - } - } - } - } - }, "entity": { "binary_sensor": { "process": { diff --git a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr index b50e051c816..328065f6098 100644 --- a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr +++ b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr @@ -35,7 +35,7 @@ }), 'disabled_by': None, 'domain': 'systemmonitor', - 'minor_version': 2, + 'minor_version': 3, 'options': dict({ 'binary_sensor': dict({ 'process': list([ diff --git a/tests/components/systemmonitor/snapshots/test_sensor.ambr b/tests/components/systemmonitor/snapshots/test_sensor.ambr index 3fe9ae7e809..1ee9067a528 100644 --- a/tests/components/systemmonitor/snapshots/test_sensor.ambr +++ b/tests/components/systemmonitor/snapshots/test_sensor.ambr @@ -300,24 +300,6 @@ # name: test_sensor[System Monitor Packets out eth1 - state] '150' # --- -# name: test_sensor[System Monitor Process pip - attributes] - ReadOnlyDict({ - 'friendly_name': 'System Monitor Process pip', - 'icon': 'mdi:cpu-64-bit', - }) -# --- -# name: test_sensor[System Monitor Process pip - state] - 'on' -# --- -# name: test_sensor[System Monitor Process python3 - attributes] - ReadOnlyDict({ - 'friendly_name': 'System Monitor Process python3', - 'icon': 'mdi:cpu-64-bit', - }) -# --- -# name: test_sensor[System Monitor Process python3 - state] - 'on' -# --- # name: test_sensor[System Monitor Processor temperature - attributes] ReadOnlyDict({ 'device_class': 'temperature', diff --git a/tests/components/systemmonitor/test_init.py b/tests/components/systemmonitor/test_init.py index 97f4a41b96c..6c1e4e6316c 100644 --- a/tests/components/systemmonitor/test_init.py +++ b/tests/components/systemmonitor/test_init.py @@ -95,9 +95,49 @@ async def test_migrate_process_sensor_to_binary_sensors( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - process_sensor = hass.states.get("sensor.system_monitor_process_python3") - assert process_sensor is not None - assert process_sensor.state == STATE_ON process_sensor = hass.states.get("binary_sensor.system_monitor_process_python3") assert process_sensor is not None assert process_sensor.state == STATE_ON + + assert mock_config_entry.minor_version == 3 + assert mock_config_entry.options == { + "binary_sensor": {"process": ["python3", "pip"]}, + "resources": [ + "disk_use_percent_/", + "disk_use_percent_/home/notexist/", + "memory_free_", + "network_out_eth0", + "process_python3", + ], + } + + +async def test_migration_from_future_version( + hass: HomeAssistant, + mock_psutil: Mock, + mock_os: Mock, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test migration from future version.""" + mock_config_entry = MockConfigEntry( + title="System Monitor", + domain=DOMAIN, + version=2, + data={}, + options={ + "sensor": {"process": ["python3", "pip"]}, + "resources": [ + "disk_use_percent_/", + "disk_use_percent_/home/notexist/", + "memory_free_", + "network_out_eth0", + "process_python3", + ], + }, + ) + 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.MIGRATION_ERROR diff --git a/tests/components/systemmonitor/test_repairs.py b/tests/components/systemmonitor/test_repairs.py deleted file mode 100644 index 6c1ff9dfd16..00000000000 --- a/tests/components/systemmonitor/test_repairs.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Test repairs for System Monitor.""" - -from __future__ import annotations - -from http import HTTPStatus -from unittest.mock import Mock - -import pytest -from syrupy.assertion import SnapshotAssertion - -from homeassistant.components.repairs.websocket_api import ( - RepairsFlowIndexView, - RepairsFlowResourceView, -) -from homeassistant.components.systemmonitor.const import DOMAIN -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import entity_registry as er, issue_registry as ir -from homeassistant.setup import async_setup_component - -from tests.common import ANY, MockConfigEntry -from tests.typing import ClientSessionGenerator, WebSocketGenerator - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_migrate_process_sensor( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - mock_psutil: Mock, - mock_os: Mock, - hass_client: ClientSessionGenerator, - hass_ws_client: WebSocketGenerator, - snapshot: SnapshotAssertion, -) -> None: - """Test migrating process sensor to binary sensor.""" - mock_config_entry = MockConfigEntry( - title="System Monitor", - domain=DOMAIN, - data={}, - options={ - "binary_sensor": {"process": ["python3", "pip"]}, - "sensor": {"process": ["python3", "pip"]}, - "resources": [ - "disk_use_percent_/", - "disk_use_percent_/home/notexist/", - "memory_free_", - "network_out_eth0", - "process_python3", - ], - }, - ) - 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 hass.config_entries.async_entries(DOMAIN) == snapshot( - name="before_migration" - ) - - assert await async_setup_component(hass, "repairs", {}) - await hass.async_block_till_done() - - entity = "sensor.system_monitor_process_python3" - state = hass.states.get(entity) - assert state - - assert entity_registry.async_get(entity) - - 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 len(msg["result"]["issues"]) > 0 - issue = None - for i in msg["result"]["issues"]: - if i["issue_id"] == "process_sensor": - issue = i - assert issue is not None - - url = RepairsFlowIndexView.url - resp = await client.post( - url, json={"handler": DOMAIN, "issue_id": "process_sensor"} - ) - assert resp.status == HTTPStatus.OK - data = await resp.json() - - flow_id = data["flow_id"] - assert data["step_id"] == "migrate_process_sensor" - - url = RepairsFlowResourceView.url.format(flow_id=flow_id) - resp = await client.post(url, json={}) - assert resp.status == HTTPStatus.OK - data = await resp.json() - - # Cannot use identity `is` check here as the value is parsed from JSON - assert data["type"] == FlowResultType.CREATE_ENTRY.value - await hass.async_block_till_done() - - state = hass.states.get("binary_sensor.system_monitor_process_python3") - assert state - - await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) - msg = await ws_client.receive_json() - - assert msg["success"] - issue = None - for i in msg["result"]["issues"]: - if i["issue_id"] == "migrate_process_sensor": - issue = i - assert not issue - - entity = "sensor.system_monitor_process_python3" - state = hass.states.get(entity) - assert not state - - assert not entity_registry.async_get(entity) - - assert hass.config_entries.async_entries(DOMAIN) == snapshot(name="after_migration") - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_other_fixable_issues( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - hass_ws_client: WebSocketGenerator, - mock_added_config_entry: ConfigEntry, -) -> None: - """Test fixing other issues.""" - assert await async_setup_component(hass, "repairs", {}) - await hass.async_block_till_done() - - 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"] - - issue = { - "breaks_in_ha_version": "2022.9.0dev0", - "domain": DOMAIN, - "issue_id": "issue_1", - "is_fixable": True, - "learn_more_url": "", - "severity": "error", - "translation_key": "issue_1", - } - ir.async_create_issue( - hass, - issue["domain"], - issue["issue_id"], - breaks_in_ha_version=issue["breaks_in_ha_version"], - is_fixable=issue["is_fixable"], - is_persistent=False, - learn_more_url=None, - severity=issue["severity"], - translation_key=issue["translation_key"], - ) - - await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) - msg = await ws_client.receive_json() - - assert msg["success"] - results = msg["result"]["issues"] - assert { - "breaks_in_ha_version": "2022.9.0dev0", - "created": ANY, - "dismissed_version": None, - "domain": DOMAIN, - "is_fixable": True, - "issue_domain": None, - "issue_id": "issue_1", - "learn_more_url": None, - "severity": "error", - "translation_key": "issue_1", - "translation_placeholders": None, - "ignored": False, - } in results - - url = RepairsFlowIndexView.url - resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "issue_1"}) - assert resp.status == HTTPStatus.OK - data = await resp.json() - - flow_id = data["flow_id"] - assert data["step_id"] == "confirm" - - url = RepairsFlowResourceView.url.format(flow_id=flow_id) - resp = await client.post(url) - assert resp.status == HTTPStatus.OK - data = await resp.json() - - # Cannot use identity `is` check here as the value is parsed from JSON - assert data["type"] == FlowResultType.CREATE_ENTRY.value - await hass.async_block_till_done() diff --git a/tests/components/systemmonitor/test_sensor.py b/tests/components/systemmonitor/test_sensor.py index ce15083da67..6d22c5354a4 100644 --- a/tests/components/systemmonitor/test_sensor.py +++ b/tests/components/systemmonitor/test_sensor.py @@ -14,12 +14,10 @@ from homeassistant.components.systemmonitor.const import DOMAIN from homeassistant.components.systemmonitor.coordinator import VirtualMemory from homeassistant.components.systemmonitor.sensor import get_cpu_icon from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import MockProcess - from tests.common import MockConfigEntry, async_fire_time_changed @@ -38,7 +36,6 @@ async def test_sensor( data={}, options={ "binary_sensor": {"process": ["python3", "pip"]}, - "sensor": {"process": ["python3", "pip"]}, "resources": [ "disk_use_percent_/", "disk_use_percent_/home/notexist/", @@ -62,10 +59,6 @@ async def test_sensor( "friendly_name": "System Monitor Memory free", } - process_sensor = hass.states.get("sensor.system_monitor_process_python3") - assert process_sensor is not None - assert process_sensor.state == STATE_ON - for entity in er.async_entries_for_config_entry( entity_registry, mock_config_entry.entry_id ): @@ -154,7 +147,6 @@ async def test_sensor_updating( data={}, options={ "binary_sensor": {"process": ["python3", "pip"]}, - "sensor": {"process": ["python3", "pip"]}, "resources": [ "disk_use_percent_/", "disk_use_percent_/home/notexist/", @@ -172,10 +164,6 @@ async def test_sensor_updating( assert memory_sensor is not None assert memory_sensor.state == "40.0" - process_sensor = hass.states.get("sensor.system_monitor_process_python3") - assert process_sensor is not None - assert process_sensor.state == STATE_ON - mock_psutil.virtual_memory.side_effect = Exception("Failed to update") freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) @@ -202,53 +190,6 @@ async def test_sensor_updating( assert memory_sensor.state == "25.0" -async def test_sensor_process_fails( - hass: HomeAssistant, - mock_psutil: Mock, - mock_os: Mock, - freezer: FrozenDateTimeFactory, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test process not exist failure.""" - mock_config_entry = MockConfigEntry( - title="System Monitor", - domain=DOMAIN, - data={}, - options={ - "binary_sensor": {"process": ["python3", "pip"]}, - "sensor": {"process": ["python3", "pip"]}, - "resources": [ - "disk_use_percent_/", - "disk_use_percent_/home/notexist/", - "memory_free_", - "network_out_eth0", - "process_python3", - ], - }, - ) - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - process_sensor = hass.states.get("sensor.system_monitor_process_python3") - assert process_sensor is not None - assert process_sensor.state == STATE_ON - - _process = MockProcess("python3", True) - - mock_psutil.process_iter.return_value = [_process] - - freezer.tick(timedelta(minutes=1)) - async_fire_time_changed(hass) - await hass.async_block_till_done(wait_background_tasks=True) - - process_sensor = hass.states.get("sensor.system_monitor_process_python3") - assert process_sensor is not None - assert process_sensor.state == STATE_OFF - - assert "Failed to load process with ID: 1, old name: python3" in caplog.text - - @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_network_sensors( freezer: FrozenDateTimeFactory, From 401e36b8857c7f3370f057bab281752a7afaf70f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Aug 2024 09:09:51 +0200 Subject: [PATCH 2151/2411] Remove deprecated yaml import from Ecovacs (#123605) --- homeassistant/components/ecovacs/__init__.py | 33 +---- .../components/ecovacs/config_flow.py | 104 +-------------- homeassistant/components/ecovacs/strings.json | 26 ---- tests/components/ecovacs/test_config_flow.py | 124 +----------------- tests/components/ecovacs/test_init.py | 32 +---- 5 files changed, 8 insertions(+), 311 deletions(-) diff --git a/homeassistant/components/ecovacs/__init__.py b/homeassistant/components/ecovacs/__init__.py index d13a337057d..f8abf87ef27 100644 --- a/homeassistant/components/ecovacs/__init__.py +++ b/homeassistant/components/ecovacs/__init__.py @@ -1,31 +1,13 @@ """Support for Ecovacs Deebot vacuums.""" from sucks import VacBot -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType -from .const import CONF_CONTINENT, DOMAIN from .controller import EcovacsController -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_COUNTRY): vol.All(vol.Lower, cv.string), - vol.Required(CONF_CONTINENT): vol.All(vol.Lower, cv.string), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, @@ -41,17 +23,6 @@ PLATFORMS = [ type EcovacsConfigEntry = ConfigEntry[EcovacsController] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Ecovacs component.""" - if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] - ) - ) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: EcovacsConfigEntry) -> bool: """Set up this integration using UI.""" controller = EcovacsController(hass, entry.data) diff --git a/homeassistant/components/ecovacs/config_flow.py b/homeassistant/components/ecovacs/config_flow.py index a254731a946..e19d9994f9e 100644 --- a/homeassistant/components/ecovacs/config_flow.py +++ b/homeassistant/components/ecovacs/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging import ssl -from typing import Any, cast +from typing import Any from urllib.parse import urlparse from aiohttp import ClientError @@ -13,21 +13,16 @@ from deebot_client.const import UNDEFINED, UndefinedType from deebot_client.exceptions import InvalidAuthenticationError, MqttError from deebot_client.mqtt_client import MqttClient, create_mqtt_config from deebot_client.util import md5 -from deebot_client.util.continents import COUNTRIES_TO_CONTINENTS, get_continent import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_COUNTRY, CONF_MODE, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.data_entry_flow import AbortFlow +from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, selector -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import VolDictType -from homeassistant.loader import async_get_issue_tracker from homeassistant.util.ssl import get_default_no_verify_context from .const import ( - CONF_CONTINENT, CONF_OVERRIDE_MQTT_URL, CONF_OVERRIDE_REST_URL, CONF_VERIFY_MQTT_CERTIFICATE, @@ -218,98 +213,3 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, last_step=True, ) - - async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: - """Import configuration from yaml.""" - - def create_repair( - error: str | None = None, placeholders: dict[str, Any] | None = None - ) -> None: - if placeholders is None: - placeholders = {} - if error: - async_create_issue( - self.hass, - DOMAIN, - f"deprecated_yaml_import_issue_{error}", - breaks_in_ha_version="2024.8.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_{error}", - translation_placeholders=placeholders - | {"url": "/config/integrations/dashboard/add?domain=ecovacs"}, - ) - else: - async_create_issue( - self.hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.8.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders=placeholders - | { - "domain": DOMAIN, - "integration_title": "Ecovacs", - }, - ) - - # We need to validate the imported country and continent - # as the YAML configuration allows any string for them. - # The config flow allows only valid alpha-2 country codes - # through the CountrySelector. - # The continent will be calculated with the function get_continent - # from the country code and there is no need to specify the continent anymore. - # As the YAML configuration includes the continent, - # we check if both the entered continent and the calculated continent match. - # If not we will inform the user about the mismatch. - error = None - placeholders = None - - # Convert the country to upper case as ISO 3166-1 alpha-2 country codes are upper case - user_input[CONF_COUNTRY] = user_input[CONF_COUNTRY].upper() - - if len(user_input[CONF_COUNTRY]) != 2: - error = "invalid_country_length" - placeholders = {"countries_url": "https://www.iso.org/obp/ui/#search/code/"} - elif len(user_input[CONF_CONTINENT]) != 2: - error = "invalid_continent_length" - placeholders = { - "continent_list": ",".join( - sorted(set(COUNTRIES_TO_CONTINENTS.values())) - ) - } - elif user_input[CONF_CONTINENT].lower() != ( - continent := get_continent(user_input[CONF_COUNTRY]) - ): - error = "continent_not_match" - placeholders = { - "continent": continent, - "github_issue_url": cast( - str, async_get_issue_tracker(self.hass, integration_domain=DOMAIN) - ), - } - - if error: - create_repair(error, placeholders) - return self.async_abort(reason=error) - - # Remove the continent from the user input as it is not needed anymore - user_input.pop(CONF_CONTINENT) - try: - result = await self.async_step_auth(user_input) - except AbortFlow as ex: - if ex.reason == "already_configured": - create_repair() - raise - - if errors := result.get("errors"): - error = errors["base"] - create_repair(error) - return self.async_abort(reason=error) - - create_repair() - return result diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index ea216f11694..8222cabed07 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -237,32 +237,6 @@ "message": "Getting the positions of the chargers and the device itself is not supported" } }, - "issues": { - "deprecated_yaml_import_issue_cannot_connect": { - "title": "The Ecovacs YAML configuration import failed", - "description": "Configuring Ecovacs using YAML is being removed but there was a connection error when trying to import the YAML configuration.\n\nPlease verify that you have a stable internet connection and restart Home Assistant to try again or remove the Ecovacs YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - }, - "deprecated_yaml_import_issue_invalid_auth": { - "title": "The Ecovacs YAML configuration import failed", - "description": "Configuring Ecovacs using YAML is being removed but there was an authentication error when trying to import the YAML configuration.\n\nCorrect the YAML configuration and restart Home Assistant to try again or remove the Ecovacs YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - }, - "deprecated_yaml_import_issue_unknown": { - "title": "The Ecovacs YAML configuration import failed", - "description": "Configuring Ecovacs using YAML is being removed but there was an unknown error when trying to import the YAML configuration.\n\nEnsure the YAML configuration is correct and restart Home Assistant to try again or remove the Ecovacs YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - }, - "deprecated_yaml_import_issue_invalid_country_length": { - "title": "The Ecovacs YAML configuration import failed", - "description": "Configuring Ecovacs using YAML is being removed but there is an invalid country specified in the YAML configuration.\n\nPlease change the country to the [Alpha-2 code of your country]({countries_url}) and restart Home Assistant to try again or remove the Ecovacs YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - }, - "deprecated_yaml_import_issue_invalid_continent_length": { - "title": "The Ecovacs YAML configuration import failed", - "description": "Configuring Ecovacs using YAML is being removed but there is an invalid continent specified in the YAML configuration.\n\nPlease correct the continent to be one of {continent_list} and restart Home Assistant to try again or remove the Ecovacs YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - }, - "deprecated_yaml_import_issue_continent_not_match": { - "title": "The Ecovacs YAML configuration import failed", - "description": "Configuring Ecovacs using YAML is being removed but there is an unexpected continent specified in the YAML configuration.\n\nFrom the given country, the continent \"{continent}\" is expected. Change the continent and restart Home Assistant to try again or remove the Ecovacs YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually.\n\nIf the contintent \"{continent}\" is not applicable, please open an issue on [GitHub]({github_issue_url})." - } - }, "selector": { "installation_mode": { "options": { diff --git a/tests/components/ecovacs/test_config_flow.py b/tests/components/ecovacs/test_config_flow.py index 0a161f88baa..5bf1144db0b 100644 --- a/tests/components/ecovacs/test_config_flow.py +++ b/tests/components/ecovacs/test_config_flow.py @@ -11,28 +11,23 @@ from deebot_client.mqtt_client import create_mqtt_config import pytest from homeassistant.components.ecovacs.const import ( - CONF_CONTINENT, CONF_OVERRIDE_MQTT_URL, CONF_OVERRIDE_REST_URL, CONF_VERIFY_MQTT_CERTIFICATE, DOMAIN, InstanceMode, ) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_COUNTRY, CONF_MODE, CONF_USERNAME -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_MODE, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import issue_registry as ir from .const import ( - IMPORT_DATA, VALID_ENTRY_DATA_CLOUD, VALID_ENTRY_DATA_SELF_HOSTED, VALID_ENTRY_DATA_SELF_HOSTED_WITH_VALIDATE_CERT, ) -from tests.common import MockConfigEntry - _USER_STEP_SELF_HOSTED = {CONF_MODE: InstanceMode.SELF_HOSTED} _TEST_FN_AUTH_ARG = "user_input_auth" @@ -303,116 +298,3 @@ async def test_user_flow_self_hosted_error( mock_setup_entry.assert_called() mock_authenticator_authenticate.assert_called() mock_mqtt_client.verify_config.assert_called() - - -async def test_import_flow( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - mock_setup_entry: AsyncMock, - mock_authenticator_authenticate: AsyncMock, - mock_mqtt_client: Mock, -) -> None: - """Test importing yaml config.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=IMPORT_DATA.copy(), - ) - mock_authenticator_authenticate.assert_called() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == VALID_ENTRY_DATA_CLOUD[CONF_USERNAME] - assert result["data"] == VALID_ENTRY_DATA_CLOUD - assert (HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}") in issue_registry.issues - mock_setup_entry.assert_called() - mock_mqtt_client.verify_config.assert_called() - - -async def test_import_flow_already_configured( - hass: HomeAssistant, issue_registry: ir.IssueRegistry -) -> None: - """Test importing yaml config where entry already configured.""" - entry = MockConfigEntry(domain=DOMAIN, data=VALID_ENTRY_DATA_CLOUD) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=IMPORT_DATA.copy(), - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - assert (HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}") in issue_registry.issues - - -@pytest.mark.parametrize("show_advanced_options", [True, False]) -@pytest.mark.parametrize( - ("side_effect", "reason"), - [ - (ClientError, "cannot_connect"), - (InvalidAuthenticationError, "invalid_auth"), - (Exception, "unknown"), - ], -) -async def test_import_flow_error( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - mock_authenticator_authenticate: AsyncMock, - mock_mqtt_client: Mock, - side_effect: Exception, - reason: str, - show_advanced_options: bool, -) -> None: - """Test handling invalid connection.""" - mock_authenticator_authenticate.side_effect = side_effect - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_IMPORT, - "show_advanced_options": show_advanced_options, - }, - data=IMPORT_DATA.copy(), - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == reason - assert ( - DOMAIN, - f"deprecated_yaml_import_issue_{reason}", - ) in issue_registry.issues - mock_authenticator_authenticate.assert_called() - - -@pytest.mark.parametrize("show_advanced_options", [True, False]) -@pytest.mark.parametrize( - ("reason", "user_input"), - [ - ("invalid_country_length", IMPORT_DATA | {CONF_COUNTRY: "too_long"}), - ("invalid_country_length", IMPORT_DATA | {CONF_COUNTRY: "a"}), # too short - ("invalid_continent_length", IMPORT_DATA | {CONF_CONTINENT: "too_long"}), - ("invalid_continent_length", IMPORT_DATA | {CONF_CONTINENT: "a"}), # too short - ("continent_not_match", IMPORT_DATA | {CONF_CONTINENT: "AA"}), - ], -) -async def test_import_flow_invalid_data( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - reason: str, - user_input: dict[str, Any], - show_advanced_options: bool, -) -> None: - """Test handling invalid connection.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_IMPORT, - "show_advanced_options": show_advanced_options, - }, - data=user_input, - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == reason - assert ( - DOMAIN, - f"deprecated_yaml_import_issue_{reason}", - ) in issue_registry.issues diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py index ac4d5661a83..2185ae4c9eb 100644 --- a/tests/components/ecovacs/test_init.py +++ b/tests/components/ecovacs/test_init.py @@ -1,7 +1,6 @@ """Test init of ecovacs.""" -from typing import Any -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import Mock, patch from deebot_client.exceptions import DeebotError, InvalidAuthenticationError import pytest @@ -12,9 +11,6 @@ from homeassistant.components.ecovacs.controller import EcovacsController from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.setup import async_setup_component - -from .const import IMPORT_DATA from tests.common import MockConfigEntry @@ -88,32 +84,6 @@ async def test_invalid_auth( assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR -@pytest.mark.parametrize( - ("config", "config_entries_expected"), - [ - ({}, 0), - ({DOMAIN: IMPORT_DATA.copy()}, 1), - ], - ids=["no_config", "import_config"], -) -async def test_async_setup_import( - hass: HomeAssistant, - config: dict[str, Any], - config_entries_expected: int, - mock_setup_entry: AsyncMock, - mock_authenticator_authenticate: AsyncMock, - mock_mqtt_client: Mock, -) -> None: - """Test async_setup config import.""" - assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - assert len(hass.config_entries.async_entries(DOMAIN)) == config_entries_expected - assert mock_setup_entry.call_count == config_entries_expected - assert mock_authenticator_authenticate.call_count == config_entries_expected - assert mock_mqtt_client.verify_config.call_count == config_entries_expected - - async def test_devices_in_dr( device_registry: dr.DeviceRegistry, controller: EcovacsController, From b19758ff71297061f8942ff063ac38688e90b477 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Aug 2024 09:11:44 +0200 Subject: [PATCH 2152/2411] Change WoL to be secondary on device info (#123591) --- homeassistant/components/wake_on_lan/button.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/wake_on_lan/button.py b/homeassistant/components/wake_on_lan/button.py index 39c4511868d..87135a61380 100644 --- a/homeassistant/components/wake_on_lan/button.py +++ b/homeassistant/components/wake_on_lan/button.py @@ -15,8 +15,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN - _LOGGER = logging.getLogger(__name__) @@ -62,9 +60,8 @@ class WolButton(ButtonEntity): self._attr_unique_id = dr.format_mac(mac_address) self._attr_device_info = dr.DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, self._attr_unique_id)}, - identifiers={(DOMAIN, self._attr_unique_id)}, - manufacturer="Wake on LAN", - name=name, + default_manufacturer="Wake on LAN", + default_name=name, ) async def async_press(self) -> None: From bbefe47aeb6eb176d7a0c08f364dcfeed2736d5c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Aug 2024 09:12:26 +0200 Subject: [PATCH 2153/2411] Add unique id to Manual alarm (#123588) --- homeassistant/components/demo/alarm_control_panel.py | 9 ++------- homeassistant/components/manual/alarm_control_panel.py | 5 +++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/demo/alarm_control_panel.py b/homeassistant/components/demo/alarm_control_panel.py index f95042f2cc7..d1b558842b6 100644 --- a/homeassistant/components/demo/alarm_control_panel.py +++ b/homeassistant/components/demo/alarm_control_panel.py @@ -30,9 +30,10 @@ async def async_setup_entry( """Set up the Demo config entry.""" async_add_entities( [ - DemoAlarm( # type:ignore[no-untyped-call] + ManualAlarm( # type:ignore[no-untyped-call] hass, "Security", + "demo_alarm_control_panel", "1234", None, True, @@ -74,9 +75,3 @@ async def async_setup_entry( ) ] ) - - -class DemoAlarm(ManualAlarm): - """Demo Alarm Control Panel.""" - - _attr_unique_id = "demo_alarm_control_panel" diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index 5b344dd01ac..422a9726e81 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PLATFORM, CONF_TRIGGER_TIME, + CONF_UNIQUE_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, @@ -122,6 +123,7 @@ PLATFORM_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): "manual", vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Exclusive(CONF_CODE, "code validation"): cv.string, vol.Exclusive(CONF_CODE_TEMPLATE, "code validation"): cv.template, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, @@ -179,6 +181,7 @@ def setup_platform( ManualAlarm( hass, config[CONF_NAME], + config.get(CONF_UNIQUE_ID), config.get(CONF_CODE), config.get(CONF_CODE_TEMPLATE), config.get(CONF_CODE_ARM_REQUIRED), @@ -205,6 +208,7 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity): self, hass, name, + unique_id, code, code_template, code_arm_required, @@ -215,6 +219,7 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity): self._state = STATE_ALARM_DISARMED self._hass = hass self._attr_name = name + self._attr_unique_id = unique_id if code_template: self._code = code_template self._code.hass = hass From 4527de18d579d87c2336ce1c5d3983aa01184401 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Mon, 12 Aug 2024 10:13:52 +0300 Subject: [PATCH 2154/2411] Bump pycoolmasternet-async to 0.2.2 (#123634) --- homeassistant/components/coolmaster/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index 9488e068d44..8775d7f72b8 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/coolmaster", "iot_class": "local_polling", "loggers": ["pycoolmasternet_async"], - "requirements": ["pycoolmasternet-async==0.1.5"] + "requirements": ["pycoolmasternet-async==0.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index f1bad6ec1b4..8cca616dc16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1780,7 +1780,7 @@ pycmus==0.1.1 pycomfoconnect==0.5.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.1.5 +pycoolmasternet-async==0.2.2 # homeassistant.components.microsoft pycsspeechtts==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4d10f58d83..f63b962b489 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1433,7 +1433,7 @@ pycfdns==3.0.0 pycomfoconnect==0.5.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.1.5 +pycoolmasternet-async==0.2.2 # homeassistant.components.microsoft pycsspeechtts==1.0.8 From 86df43879c6e310c458085b38fc0f956a0af48fe Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Aug 2024 09:14:48 +0200 Subject: [PATCH 2155/2411] Define Manual alarm as a helper (#123587) --- homeassistant/components/manual/manifest.json | 1 + homeassistant/generated/integrations.json | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/manual/manifest.json b/homeassistant/components/manual/manifest.json index 7406ab26830..37ba45c2dda 100644 --- a/homeassistant/components/manual/manifest.json +++ b/homeassistant/components/manual/manifest.json @@ -3,6 +3,7 @@ "name": "Manual Alarm Control Panel", "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/manual", + "integration_type": "helper", "iot_class": "calculated", "quality_scale": "internal" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index f59e0883dd4..3d3344c1f60 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3449,12 +3449,6 @@ "config_flow": true, "iot_class": "cloud_push" }, - "manual": { - "name": "Manual Alarm Control Panel", - "integration_type": "hub", - "config_flow": false, - "iot_class": "calculated" - }, "marantz": { "name": "Marantz", "integration_type": "virtual", @@ -7218,6 +7212,12 @@ "config_flow": true, "iot_class": "local_push" }, + "manual": { + "name": "Manual Alarm Control Panel", + "integration_type": "helper", + "config_flow": false, + "iot_class": "calculated" + }, "min_max": { "integration_type": "helper", "config_flow": true, From b15ea588510601789af555b6beb3abf6639f1a25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Aug 2024 02:15:33 -0500 Subject: [PATCH 2156/2411] Relocate code to get scheduled TimerHandles (#123546) --- homeassistant/core.py | 4 ++-- homeassistant/util/async_.py | 16 +++++++++++++++- tests/common.py | 3 ++- tests/conftest.py | 4 ++-- tests/util/test_async.py | 14 ++++++++++++++ 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 5d223b9f19f..1050d25ee71 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -101,6 +101,7 @@ from .util import dt as dt_util, location from .util.async_ import ( cancelling, create_eager_task, + get_scheduled_timer_handles, run_callback_threadsafe, shutdown_run_callback_threadsafe, ) @@ -1227,8 +1228,7 @@ class HomeAssistant: def _cancel_cancellable_timers(self) -> None: """Cancel timer handles marked as cancellable.""" - handles: Iterable[asyncio.TimerHandle] = self.loop._scheduled # type: ignore[attr-defined] # noqa: SLF001 - for handle in handles: + for handle in get_scheduled_timer_handles(self.loop): if ( not handle.cancelled() and (args := handle._args) # noqa: SLF001 diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index f2dc1291324..dcb788f0685 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -2,7 +2,15 @@ from __future__ import annotations -from asyncio import AbstractEventLoop, Future, Semaphore, Task, gather, get_running_loop +from asyncio import ( + AbstractEventLoop, + Future, + Semaphore, + Task, + TimerHandle, + gather, + get_running_loop, +) from collections.abc import Awaitable, Callable, Coroutine import concurrent.futures import logging @@ -124,3 +132,9 @@ def shutdown_run_callback_threadsafe(loop: AbstractEventLoop) -> None: python is going to exit. """ setattr(loop, _SHUTDOWN_RUN_CALLBACK_THREADSAFE, True) + + +def get_scheduled_timer_handles(loop: AbstractEventLoop) -> list[TimerHandle]: + """Return a list of scheduled TimerHandles.""" + handles: list[TimerHandle] = loop._scheduled # type: ignore[attr-defined] # noqa: SLF001 + return handles diff --git a/tests/common.py b/tests/common.py index 64e11ee7b51..d36df509142 100644 --- a/tests/common.py +++ b/tests/common.py @@ -93,6 +93,7 @@ from homeassistant.helpers.json import JSONEncoder, _orjson_default_encoder, jso from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.async_ import ( _SHUTDOWN_RUN_CALLBACK_THREADSAFE, + get_scheduled_timer_handles, run_callback_threadsafe, ) import homeassistant.util.dt as dt_util @@ -531,7 +532,7 @@ def _async_fire_time_changed( hass: HomeAssistant, utc_datetime: datetime | None, fire_all: bool ) -> None: timestamp = dt_util.utc_to_timestamp(utc_datetime) - for task in list(hass.loop._scheduled): + for task in list(get_scheduled_timer_handles(hass.loop)): if not isinstance(task, asyncio.TimerHandle): continue if task.cancelled(): diff --git a/tests/conftest.py b/tests/conftest.py index 0667edf4be2..ea0453e7450 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -84,7 +84,7 @@ from homeassistant.helpers.translation import _TranslationsCacheData from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, location -from homeassistant.util.async_ import create_eager_task +from homeassistant.util.async_ import create_eager_task, get_scheduled_timer_handles from homeassistant.util.json import json_loads from .ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS @@ -372,7 +372,7 @@ def verify_cleanup( if tasks: event_loop.run_until_complete(asyncio.wait(tasks)) - for handle in event_loop._scheduled: # type: ignore[attr-defined] + for handle in get_scheduled_timer_handles(event_loop): if not handle.cancelled(): with long_repr_strings(): if expected_lingering_timers: diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 373768788b7..17349cf6ff9 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -199,3 +199,17 @@ async def test_create_eager_task_from_thread_in_integration( "from a thread at homeassistant/components/hue/light.py, line 23: " "self.light.is_on" ) in caplog.text + + +async def test_get_scheduled_timer_handles(hass: HomeAssistant) -> None: + """Test get_scheduled_timer_handles returns all scheduled timer handles.""" + loop = hass.loop + timer_handle = loop.call_later(10, lambda: None) + timer_handle2 = loop.call_later(5, lambda: None) + timer_handle3 = loop.call_later(15, lambda: None) + + handles = hasync.get_scheduled_timer_handles(loop) + assert set(handles).issuperset({timer_handle, timer_handle2, timer_handle3}) + timer_handle.cancel() + timer_handle2.cancel() + timer_handle3.cancel() From 0bb8c4832d8b8a3296cebbbdca54d0c7909a9d28 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Aug 2024 09:16:33 +0200 Subject: [PATCH 2157/2411] Enable raise-within-try (TRY301) rule in ruff (#123351) --- homeassistant/components/alexa/smart_home.py | 2 +- homeassistant/components/amcrest/camera.py | 2 +- homeassistant/components/camera/__init__.py | 2 +- homeassistant/components/control4/config_flow.py | 4 ++-- homeassistant/components/currencylayer/sensor.py | 2 +- homeassistant/components/device_tracker/legacy.py | 2 +- homeassistant/components/dunehd/config_flow.py | 2 +- homeassistant/components/egardia/__init__.py | 2 +- homeassistant/components/elmax/config_flow.py | 2 +- homeassistant/components/generic_thermostat/climate.py | 2 +- homeassistant/components/group/sensor.py | 2 +- homeassistant/components/icloud/account.py | 2 +- homeassistant/components/icloud/config_flow.py | 6 +++--- homeassistant/components/idasen_desk/__init__.py | 2 +- homeassistant/components/jvc_projector/config_flow.py | 2 +- homeassistant/components/kaleidescape/config_flow.py | 4 ++-- homeassistant/components/knx/config_flow.py | 2 +- homeassistant/components/mailbox/__init__.py | 2 +- homeassistant/components/mqtt/sensor.py | 4 ++-- homeassistant/components/notify/legacy.py | 2 +- homeassistant/components/python_script/__init__.py | 2 +- homeassistant/components/raincloud/__init__.py | 2 +- homeassistant/components/roborock/__init__.py | 2 +- homeassistant/components/signal_messenger/notify.py | 4 ++-- homeassistant/components/starline/config_flow.py | 2 +- homeassistant/components/tami4/config_flow.py | 2 +- homeassistant/components/template/vacuum.py | 2 +- homeassistant/components/websocket_api/connection.py | 2 +- homeassistant/components/websocket_api/http.py | 4 ++-- homeassistant/components/wyoming/data.py | 2 +- pyproject.toml | 3 +-- tests/components/system_log/test_init.py | 6 +++--- 32 files changed, 41 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 57c1ba791ba..d7bcfa5698e 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -194,7 +194,7 @@ async def async_handle_message( try: if not enabled: - raise AlexaBridgeUnreachableError( + raise AlexaBridgeUnreachableError( # noqa: TRY301 "Alexa API not enabled in Home Assistant configuration" ) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index b9b2701eac6..0bf02b604f1 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -499,7 +499,7 @@ class AmcrestCam(Camera): await getattr(self, f"_async_set_{func}")(value) new_value = await getattr(self, f"_async_get_{func}")() if new_value != value: - raise AmcrestCommandFailed + raise AmcrestCommandFailed # noqa: TRY301 except (AmcrestError, AmcrestCommandFailed) as error: if tries == 1: log_update_error(_LOGGER, action, self.name, description, error) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index d8fa4bfbc7a..cbcf08cb7c2 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -862,7 +862,7 @@ class CameraMjpegStream(CameraView): # Compose camera stream from stills interval = float(interval_str) if interval < MIN_STREAM_INTERVAL: - raise ValueError(f"Stream interval must be > {MIN_STREAM_INTERVAL}") + raise ValueError(f"Stream interval must be > {MIN_STREAM_INTERVAL}") # noqa: TRY301 return await camera.handle_async_still_stream(request, interval) except ValueError as err: raise web.HTTPBadRequest from err diff --git a/homeassistant/components/control4/config_flow.py b/homeassistant/components/control4/config_flow.py index f6d746c9cb4..f6eb410cbf2 100644 --- a/homeassistant/components/control4/config_flow.py +++ b/homeassistant/components/control4/config_flow.py @@ -105,9 +105,9 @@ class Control4ConfigFlow(ConfigFlow, domain=DOMAIN): ) try: if not await hub.authenticate(): - raise InvalidAuth + raise InvalidAuth # noqa: TRY301 if not await hub.connect_to_director(): - raise CannotConnect + raise CannotConnect # noqa: TRY301 except InvalidAuth: errors["base"] = "invalid_auth" except CannotConnect: diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index 2ad0f88a2ab..01dec10efe0 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -108,7 +108,7 @@ class CurrencylayerData: try: result = requests.get(self._resource, params=self._parameters, timeout=10) if "error" in result.json(): - raise ValueError(result.json()["error"]["info"]) + raise ValueError(result.json()["error"]["info"]) # noqa: TRY301 self.data = result.json()["quotes"] _LOGGER.debug("Currencylayer data updated: %s", result.json()["timestamp"]) except ValueError as err: diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index ac168c06fb1..15cb67f5ee8 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -350,7 +350,7 @@ class DeviceTrackerPlatform: discovery_info, ) else: - raise HomeAssistantError("Invalid legacy device_tracker platform.") + raise HomeAssistantError("Invalid legacy device_tracker platform.") # noqa: TRY301 if scanner is not None: async_setup_scanner_platform( diff --git a/homeassistant/components/dunehd/config_flow.py b/homeassistant/components/dunehd/config_flow.py index 8a0f3eec4a0..33ffd4a812a 100644 --- a/homeassistant/components/dunehd/config_flow.py +++ b/homeassistant/components/dunehd/config_flow.py @@ -39,7 +39,7 @@ class DuneHDConfigFlow(ConfigFlow, domain=DOMAIN): try: if self.host_already_configured(host): - raise AlreadyConfigured + raise AlreadyConfigured # noqa: TRY301 await self.init_device(host) except CannotConnect: errors[CONF_HOST] = "cannot_connect" diff --git a/homeassistant/components/egardia/__init__.py b/homeassistant/components/egardia/__init__.py index 9ff4b9af94f..89dae7d23c9 100644 --- a/homeassistant/components/egardia/__init__.py +++ b/homeassistant/components/egardia/__init__.py @@ -113,7 +113,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: server = egardiaserver.EgardiaServer("", rs_port) bound = server.bind() if not bound: - raise OSError( + raise OSError( # noqa: TRY301 "Binding error occurred while starting EgardiaServer." ) hass.data[EGARDIA_SERVER] = server diff --git a/homeassistant/components/elmax/config_flow.py b/homeassistant/components/elmax/config_flow.py index 2971a425663..69f69a5fd31 100644 --- a/homeassistant/components/elmax/config_flow.py +++ b/homeassistant/components/elmax/config_flow.py @@ -424,7 +424,7 @@ class ElmaxConfigFlow(ConfigFlow, domain=DOMAIN): if p.hash == self._entry.data[CONF_ELMAX_PANEL_ID] ] if len(panels) < 1: - raise NoOnlinePanelsError + raise NoOnlinePanelsError # noqa: TRY301 # Verify the pin is still valid. await client.get_panel_status( diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index c142d15f9e5..2a118b70879 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -485,7 +485,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): try: cur_temp = float(state.state) if not math.isfinite(cur_temp): - raise ValueError(f"Sensor has illegal state {state.state}") + raise ValueError(f"Sensor has illegal state {state.state}") # noqa: TRY301 self._cur_temp = cur_temp except ValueError as ex: _LOGGER.error("Unable to update from sensor: %s", ex) diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index eaaedcf0e46..a99ed9dad63 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -406,7 +406,7 @@ class SensorGroup(GroupEntity, SensorEntity): and (uom := state.attributes["unit_of_measurement"]) not in self._valid_units ): - raise HomeAssistantError("Not a valid unit") + raise HomeAssistantError("Not a valid unit") # noqa: TRY301 sensor_values.append((entity_id, numeric_state, state)) if entity_id in self._state_incorrect: diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 988073384f8..9536cd9ee5c 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -117,7 +117,7 @@ class IcloudAccount: if self.api.requires_2fa: # Trigger a new log in to ensure the user enters the 2FA code again. - raise PyiCloudFailedLoginException + raise PyiCloudFailedLoginException # noqa: TRY301 except PyiCloudFailedLoginException: self.api = None diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index 36fe880ec79..30942ce6727 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -141,7 +141,7 @@ class IcloudFlowHandler(ConfigFlow, domain=DOMAIN): getattr, self.api, "devices" ) if not devices: - raise PyiCloudNoDevicesException + raise PyiCloudNoDevicesException # noqa: TRY301 except (PyiCloudServiceNotActivatedException, PyiCloudNoDevicesException): _LOGGER.error("No device found in the iCloud account: %s", self._username) self.api = None @@ -264,13 +264,13 @@ class IcloudFlowHandler(ConfigFlow, domain=DOMAIN): if not await self.hass.async_add_executor_job( self.api.validate_2fa_code, self._verification_code ): - raise PyiCloudException("The code you entered is not valid.") + raise PyiCloudException("The code you entered is not valid.") # noqa: TRY301 elif not await self.hass.async_add_executor_job( self.api.validate_verification_code, self._trusted_device, self._verification_code, ): - raise PyiCloudException("The code you entered is not valid.") + raise PyiCloudException("The code you entered is not valid.") # noqa: TRY301 except PyiCloudException as error: # Reset to the initial 2FA state to allow the user to retry _LOGGER.error("Failed to verify verification code: %s", error) diff --git a/homeassistant/components/idasen_desk/__init__.py b/homeassistant/components/idasen_desk/__init__.py index f0d8013cb50..56a377ac2df 100644 --- a/homeassistant/components/idasen_desk/__init__.py +++ b/homeassistant/components/idasen_desk/__init__.py @@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: if not await coordinator.async_connect(): - raise ConfigEntryNotReady(f"Unable to connect to desk {address}") + raise ConfigEntryNotReady(f"Unable to connect to desk {address}") # noqa: TRY301 except (AuthFailedError, TimeoutError, BleakError, Exception) as ex: raise ConfigEntryNotReady(f"Unable to connect to desk {address}") from ex diff --git a/homeassistant/components/jvc_projector/config_flow.py b/homeassistant/components/jvc_projector/config_flow.py index 7564d571d3b..7fbfb17a976 100644 --- a/homeassistant/components/jvc_projector/config_flow.py +++ b/homeassistant/components/jvc_projector/config_flow.py @@ -37,7 +37,7 @@ class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN): try: if not is_host_valid(host): - raise InvalidHost + raise InvalidHost # noqa: TRY301 mac = await get_mac_address(host, port, password) except InvalidHost: diff --git a/homeassistant/components/kaleidescape/config_flow.py b/homeassistant/components/kaleidescape/config_flow.py index bb9f47ec1e8..e4a562dc00b 100644 --- a/homeassistant/components/kaleidescape/config_flow.py +++ b/homeassistant/components/kaleidescape/config_flow.py @@ -38,7 +38,7 @@ class KaleidescapeConfigFlow(ConfigFlow, domain=DOMAIN): try: info = await validate_host(host) if info.server_only: - raise UnsupportedError + raise UnsupportedError # noqa: TRY301 except ConnectionError: errors["base"] = ERROR_CANNOT_CONNECT except UnsupportedError: @@ -73,7 +73,7 @@ class KaleidescapeConfigFlow(ConfigFlow, domain=DOMAIN): try: self.discovered_device = await validate_host(host) if self.discovered_device.server_only: - raise UnsupportedError + raise UnsupportedError # noqa: TRY301 except ConnectionError: return self.async_abort(reason=ERROR_CANNOT_CONNECT) except UnsupportedError: diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 2fc1f49800c..7e4db1f889b 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -445,7 +445,7 @@ class KNXCommonFlow(ABC, ConfigEntryBaseFlow): try: key_bytes = bytes.fromhex(user_input[CONF_KNX_ROUTING_BACKBONE_KEY]) if len(key_bytes) != 16: - raise ValueError + raise ValueError # noqa: TRY301 except ValueError: errors[CONF_KNX_ROUTING_BACKBONE_KEY] = "invalid_backbone_key" if not errors: diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index b446ba3704e..e0438342a54 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -92,7 +92,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: platform.get_handler, hass, p_config, discovery_info ) else: - raise HomeAssistantError("Invalid mailbox platform.") + raise HomeAssistantError("Invalid mailbox platform.") # noqa: TRY301 if mailbox is None: _LOGGER.error("Failed to initialize mailbox platform %s", p_type) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 4a41f486831..e983f1b66f3 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -260,7 +260,7 @@ class MqttSensor(MqttEntity, RestoreSensor): return try: if (payload_datetime := dt_util.parse_datetime(payload)) is None: - raise ValueError + raise ValueError # noqa: TRY301 except ValueError: _LOGGER.warning("Invalid state message '%s' from '%s'", payload, msg.topic) self._attr_native_value = None @@ -280,7 +280,7 @@ class MqttSensor(MqttEntity, RestoreSensor): try: last_reset = dt_util.parse_datetime(str(payload)) if last_reset is None: - raise ValueError + raise ValueError # noqa: TRY301 self._attr_last_reset = last_reset except ValueError: _LOGGER.warning( diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index b3871d858e8..81444a36296 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -105,7 +105,7 @@ def async_setup_legacy( platform.get_service, hass, p_config, discovery_info ) else: - raise HomeAssistantError("Invalid notify platform.") + raise HomeAssistantError("Invalid notify platform.") # noqa: TRY301 if notify_service is None: # Platforms can decide not to create a service based diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index ab8cf17daa0..70e9c5b0d29 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -277,7 +277,7 @@ def execute(hass, filename, source, data=None, return_response=False): if not isinstance(restricted_globals["output"], dict): output_type = type(restricted_globals["output"]) restricted_globals["output"] = {} - raise ScriptError( + raise ScriptError( # noqa: TRY301 f"Expected `output` to be a dictionary, was {output_type}" ) except ScriptError as err: diff --git a/homeassistant/components/raincloud/__init__.py b/homeassistant/components/raincloud/__init__.py index e6f5d2ecf8d..a805024357c 100644 --- a/homeassistant/components/raincloud/__init__.py +++ b/homeassistant/components/raincloud/__init__.py @@ -102,7 +102,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: try: raincloud = RainCloudy(username=username, password=password) if not raincloud.is_connected: - raise HTTPError + raise HTTPError # noqa: TRY301 hass.data[DATA_RAINCLOUD] = RainCloudHub(raincloud) except (ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Rain Cloud service: %s", str(ex)) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index 3743faa32d8..d107a0bee8b 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -174,7 +174,7 @@ async def setup_device_v1( if networking is None: # If the api does not return an error but does return None for # get_networking - then we need to go through cache checking. - raise RoborockException("Networking request returned None.") + raise RoborockException("Networking request returned None.") # noqa: TRY301 except RoborockException as err: _LOGGER.warning( "Not setting up %s because we could not get the network information of the device. " diff --git a/homeassistant/components/signal_messenger/notify.py b/homeassistant/components/signal_messenger/notify.py index 21d42f8912f..9321bc3232f 100644 --- a/homeassistant/components/signal_messenger/notify.py +++ b/homeassistant/components/signal_messenger/notify.py @@ -166,7 +166,7 @@ class SignalNotificationService(BaseNotificationService): and int(str(resp.headers.get("Content-Length"))) > attachment_size_limit ): - raise ValueError( + raise ValueError( # noqa: TRY301 "Attachment too large (Content-Length reports {}). Max size: {}" " bytes".format( int(str(resp.headers.get("Content-Length"))), @@ -179,7 +179,7 @@ class SignalNotificationService(BaseNotificationService): for chunk in resp.iter_content(1024): size += len(chunk) if size > attachment_size_limit: - raise ValueError( + raise ValueError( # noqa: TRY301 f"Attachment too large (Stream reports {size}). " f"Max size: {CONF_MAX_ALLOWED_DOWNLOAD_SIZE_BYTES} bytes" ) diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py index 6c38603a843..fbb7fa9acdc 100644 --- a/homeassistant/components/starline/config_flow.py +++ b/homeassistant/components/starline/config_flow.py @@ -214,7 +214,7 @@ class StarlineFlowHandler(ConfigFlow, domain=DOMAIN): self._captcha_image = data["captchaImg"] return self._async_form_auth_captcha(error) - raise Exception(data) # noqa: TRY002 + raise Exception(data) # noqa: TRY002, TRY301 except Exception as err: # noqa: BLE001 _LOGGER.error("Error auth user: %s", err) return self._async_form_auth_user(ERROR_AUTH_USER) diff --git a/homeassistant/components/tami4/config_flow.py b/homeassistant/components/tami4/config_flow.py index 0fa05bbebe4..72b19470f45 100644 --- a/homeassistant/components/tami4/config_flow.py +++ b/homeassistant/components/tami4/config_flow.py @@ -42,7 +42,7 @@ class Tami4ConfigFlow(ConfigFlow, domain=DOMAIN): if m := _PHONE_MATCHER.match(phone): self.phone = f"+972{m.group('number')}" else: - raise InvalidPhoneNumber + raise InvalidPhoneNumber # noqa: TRY301 await self.hass.async_add_executor_job( Tami4EdgeAPI.request_otp, self.phone ) diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 9062f71d818..e512ce2eb04 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -318,7 +318,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): try: battery_level_int = int(battery_level) if not 0 <= battery_level_int <= 100: - raise ValueError + raise ValueError # noqa: TRY301 except ValueError: _LOGGER.error( "Received invalid battery level: %s for entity %s. Expected: 0-100", diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index ef70df4a123..6c0c6f0c587 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -223,7 +223,7 @@ class ActiveConnection: try: if schema is False: if len(msg) > 2: - raise vol.Invalid("extra keys not allowed") + raise vol.Invalid("extra keys not allowed") # noqa: TRY301 handler(self.hass, self, msg) else: handler(self.hass, self, schema(msg)) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index c65c4c65988..8ed3469d7ed 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -339,11 +339,11 @@ class WebSocketHandler: raise Disconnect from err if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING): - raise Disconnect + raise Disconnect # noqa: TRY301 if msg.type != WSMsgType.TEXT: disconnect_warn = "Received non-Text message." - raise Disconnect + raise Disconnect # noqa: TRY301 try: auth_msg_data = json_loads(msg.data) diff --git a/homeassistant/components/wyoming/data.py b/homeassistant/components/wyoming/data.py index e333a740741..1ee0f24f805 100644 --- a/homeassistant/components/wyoming/data.py +++ b/homeassistant/components/wyoming/data.py @@ -100,7 +100,7 @@ async def load_wyoming_info( while True: event = await client.read_event() if event is None: - raise WyomingError( + raise WyomingError( # noqa: TRY301 "Connection closed unexpectedly", ) diff --git a/pyproject.toml b/pyproject.toml index e94c9e96225..1cccc155d34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -821,8 +821,7 @@ ignore = [ "PLE0605", # temporarily disabled - "RET503", - "TRY301" + "RET503" ] [tool.ruff.lint.flake8-import-conventions.extend-aliases] diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 1f1c4464c71..83adab8200b 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -36,7 +36,7 @@ async def get_error_log(hass_ws_client): def _generate_and_log_exception(exception, log): try: - raise Exception(exception) # noqa: TRY002 + raise Exception(exception) # noqa: TRY002, TRY301 except Exception: _LOGGER.exception(log) @@ -461,7 +461,7 @@ async def test__figure_out_source(hass: HomeAssistant) -> None: in a test because the test is not a component. """ try: - raise ValueError("test") + raise ValueError("test") # noqa: TRY301 except ValueError as ex: exc_info = (type(ex), ex, ex.__traceback__) mock_record = MagicMock( @@ -486,7 +486,7 @@ async def test__figure_out_source(hass: HomeAssistant) -> None: async def test_formatting_exception(hass: HomeAssistant) -> None: """Test that exceptions are formatted correctly.""" try: - raise ValueError("test") + raise ValueError("test") # noqa: TRY301 except ValueError as ex: exc_info = (type(ex), ex, ex.__traceback__) mock_record = MagicMock( From e64ca7c274fd12208c0e0ecc4058cb01dad9a301 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Aug 2024 10:04:16 +0200 Subject: [PATCH 2158/2411] Enable implicit-return (RET503) rule in ruff (#122771) --- homeassistant/components/control4/__init__.py | 3 ++- homeassistant/components/recorder/pool.py | 2 +- homeassistant/components/stream/fmp4utils.py | 3 ++- pyproject.toml | 5 +---- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index c9a6eab5c62..a3d0cebd1fc 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -50,7 +50,8 @@ PLATFORMS = [Platform.LIGHT, Platform.MEDIA_PLAYER] async def call_c4_api_retry(func, *func_args): """Call C4 API function and retry on failure.""" - for i in range(API_RETRY_TIMES): + # Ruff doesn't understand this loop - the exception is always raised after the retries + for i in range(API_RETRY_TIMES): # noqa: RET503 try: return await func(*func_args) except client_exceptions.ClientError as exception: diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index 0fa0e82e98b..30f8fa8d07a 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -100,7 +100,7 @@ class RecorderPool(SingletonThreadPool, NullPool): # which is allowed but discouraged since its much slower return self._do_get_db_connection_protected() # In the event loop, raise an exception - raise_for_blocking_call( + raise_for_blocking_call( # noqa: RET503 self._do_get_db_connection_protected, strict=True, advise_msg=ADVISE_MSG, diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index 255d75e3b79..5080678e3ca 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -149,7 +149,8 @@ def get_codec_string(mp4_bytes: bytes) -> str: def find_moov(mp4_io: BufferedIOBase) -> int: """Find location of moov atom in a BufferedIOBase mp4.""" index = 0 - while 1: + # Ruff doesn't understand this loop - the exception is always raised at the end + while 1: # noqa: RET503 mp4_io.seek(index) box_header = mp4_io.read(8) if len(box_header) != 8 or box_header[0:4] == b"\x00\x00\x00\x00": diff --git a/pyproject.toml b/pyproject.toml index 1cccc155d34..5f6324bbac5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -818,10 +818,7 @@ ignore = [ "ISC001", # Disabled because ruff does not understand type of __all__ generated by a function - "PLE0605", - - # temporarily disabled - "RET503" + "PLE0605" ] [tool.ruff.lint.flake8-import-conventions.extend-aliases] From 8cfac68317288fcb88888e369d3bd4302037b77b Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Mon, 12 Aug 2024 10:57:51 +0200 Subject: [PATCH 2159/2411] Bump BSBLan to 0.6.2 (#123594) * chore: Update bsblan library to version 0.6.1 * add dataclass BSBLANConfig remove session as bsblan has it's own session * Update temperature unit handling in BSBLANClimate * chore: Remove unused constant in bsblan/const.py * chore: Update python-bsblan library to version 0.6.2 * feat: Add async_get_clientsession to BSBLAN initialization This commit adds the `async_get_clientsession` function to the initialization of the `BSBLAN` class in both `__init__.py` and `config_flow.py` files. This allows the `BSBLAN` instance to have its own session for making HTTP requests. This change improves the performance and reliability of the BSBLAN integration. --- homeassistant/components/bsblan/__init__.py | 16 +++++++++++----- homeassistant/components/bsblan/climate.py | 2 +- homeassistant/components/bsblan/config_flow.py | 12 ++++++------ homeassistant/components/bsblan/const.py | 2 -- homeassistant/components/bsblan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 21 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py index 9a471329ba9..113a582f403 100644 --- a/homeassistant/components/bsblan/__init__.py +++ b/homeassistant/components/bsblan/__init__.py @@ -2,7 +2,7 @@ import dataclasses -from bsblan import BSBLAN, Device, Info, StaticState +from bsblan import BSBLAN, BSBLANConfig, Device, Info, StaticState from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -35,22 +35,28 @@ class HomeAssistantBSBLANData: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up BSB-Lan from a config entry.""" - session = async_get_clientsession(hass) - bsblan = BSBLAN( - entry.data[CONF_HOST], + # create config using BSBLANConfig + config = BSBLANConfig( + host=entry.data[CONF_HOST], passkey=entry.data[CONF_PASSKEY], port=entry.data[CONF_PORT], username=entry.data.get(CONF_USERNAME), password=entry.data.get(CONF_PASSWORD), - session=session, ) + # create BSBLAN client + session = async_get_clientsession(hass) + bsblan = BSBLAN(config, session) + + # Create and perform first refresh of the coordinator coordinator = BSBLanUpdateCoordinator(hass, entry, bsblan) await coordinator.async_config_entry_first_refresh() + # Fetch all required data concurrently device = await bsblan.device() info = await bsblan.info() static = await bsblan.static_values() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantBSBLANData( client=bsblan, coordinator=coordinator, diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 1b300e1e738..4d6514251cb 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -103,7 +103,7 @@ class BSBLANClimate( self._attr_min_temp = float(static.min_temp.value) self._attr_max_temp = float(static.max_temp.value) # check if self.coordinator.data.current_temperature.unit is "°C" or "°C" - if self.coordinator.data.current_temperature.unit in ("°C", "°C"): + if static.min_temp.unit in ("°C", "°C"): self._attr_temperature_unit = UnitOfTemperature.CELSIUS else: self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py index 9732f0a77a9..a1d7d6d403a 100644 --- a/homeassistant/components/bsblan/config_flow.py +++ b/homeassistant/components/bsblan/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any -from bsblan import BSBLAN, BSBLANError +from bsblan import BSBLAN, BSBLANConfig, BSBLANError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -80,15 +80,15 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): async def _get_bsblan_info(self, raise_on_progress: bool = True) -> None: """Get device information from an BSBLAN device.""" - session = async_get_clientsession(self.hass) - bsblan = BSBLAN( + config = BSBLANConfig( host=self.host, - username=self.username, - password=self.password, passkey=self.passkey, port=self.port, - session=session, + username=self.username, + password=self.password, ) + session = async_get_clientsession(self.hass) + bsblan = BSBLAN(config, session) device = await bsblan.device() self.mac = device.MAC diff --git a/homeassistant/components/bsblan/const.py b/homeassistant/components/bsblan/const.py index 5bca20cb4d4..25d9dec865b 100644 --- a/homeassistant/components/bsblan/const.py +++ b/homeassistant/components/bsblan/const.py @@ -21,6 +21,4 @@ ATTR_OUTSIDE_TEMPERATURE: Final = "outside_temperature" CONF_PASSKEY: Final = "passkey" -CONF_DEVICE_IDENT: Final = "RVS21.831F/127" - DEFAULT_PORT: Final = 80 diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index fb3c9b49e4c..6cd8608c42d 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["bsblan"], - "requirements": ["python-bsblan==0.5.19"] + "requirements": ["python-bsblan==0.6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8cca616dc16..d0bd63bd826 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2250,7 +2250,7 @@ python-awair==0.2.4 python-blockchain-api==0.0.2 # homeassistant.components.bsblan -python-bsblan==0.5.19 +python-bsblan==0.6.2 # homeassistant.components.clementine python-clementine-remote==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f63b962b489..b6c15fc8942 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1795,7 +1795,7 @@ python-MotionMount==2.0.0 python-awair==0.2.4 # homeassistant.components.bsblan -python-bsblan==0.5.19 +python-bsblan==0.6.2 # homeassistant.components.ecobee python-ecobee-api==0.2.18 From 0803ac9b0ba7a3a75a5e3b473eef9eada342c700 Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Mon, 12 Aug 2024 11:26:42 +0200 Subject: [PATCH 2160/2411] Add Swiss public transport fetch connections service (#114671) * add service to fetch more connections * improve error messages * better errors * wip * fix service register * add working tests * improve tests * temp availability * test availability * remove availability test * change error type for coordinator update * fix missed coverage * convert from entity service to integration service * cleanup changes * add more tests for the service --- .../swiss_public_transport/__init__.py | 16 +- .../swiss_public_transport/const.py | 9 +- .../swiss_public_transport/coordinator.py | 20 +- .../swiss_public_transport/icons.json | 3 + .../swiss_public_transport/sensor.py | 4 +- .../swiss_public_transport/services.py | 89 +++++++ .../swiss_public_transport/services.yaml | 14 ++ .../swiss_public_transport/strings.json | 25 ++ script/hassfest/translations.py | 1 + .../swiss_public_transport/__init__.py | 12 + .../fixtures/connections.json | 130 ++++++++++ .../swiss_public_transport/test_init.py | 2 +- .../swiss_public_transport/test_service.py | 226 ++++++++++++++++++ 13 files changed, 541 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/swiss_public_transport/services.py create mode 100644 homeassistant/components/swiss_public_transport/services.yaml create mode 100644 tests/components/swiss_public_transport/fixtures/connections.json create mode 100644 tests/components/swiss_public_transport/test_service.py diff --git a/homeassistant/components/swiss_public_transport/__init__.py b/homeassistant/components/swiss_public_transport/__init__.py index 1242c95269e..3e29fb9c746 100644 --- a/homeassistant/components/swiss_public_transport/__init__.py +++ b/homeassistant/components/swiss_public_transport/__init__.py @@ -11,18 +11,32 @@ from opendata_transport.exceptions import ( from homeassistant import config_entries, core from homeassistant.const import Platform from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType from .const import CONF_DESTINATION, CONF_START, CONF_VIA, DOMAIN, PLACEHOLDERS from .coordinator import SwissPublicTransportDataUpdateCoordinator from .helper import unique_id_from_config +from .services import setup_services _LOGGER = logging.getLogger(__name__) PLATFORMS: list[Platform] = [Platform.SENSOR] +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup(hass: core.HomeAssistant, config: ConfigType) -> bool: + """Set up the Swiss public transport component.""" + setup_services(hass) + return True + async def async_setup_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry diff --git a/homeassistant/components/swiss_public_transport/const.py b/homeassistant/components/swiss_public_transport/const.py index 32b6427ced5..c02f36f2f25 100644 --- a/homeassistant/components/swiss_public_transport/const.py +++ b/homeassistant/components/swiss_public_transport/const.py @@ -9,12 +9,19 @@ CONF_START: Final = "from" CONF_VIA: Final = "via" DEFAULT_NAME = "Next Destination" +DEFAULT_UPDATE_TIME = 90 MAX_VIA = 5 -SENSOR_CONNECTIONS_COUNT = 3 +CONNECTIONS_COUNT = 3 +CONNECTIONS_MAX = 15 PLACEHOLDERS = { "stationboard_url": "http://transport.opendata.ch/examples/stationboard.html", "opendata_url": "http://transport.opendata.ch", } + +ATTR_CONFIG_ENTRY_ID: Final = "config_entry_id" +ATTR_LIMIT: Final = "limit" + +SERVICE_FETCH_CONNECTIONS = "fetch_connections" diff --git a/homeassistant/components/swiss_public_transport/coordinator.py b/homeassistant/components/swiss_public_transport/coordinator.py index ae7e1b2366d..114215520ac 100644 --- a/homeassistant/components/swiss_public_transport/coordinator.py +++ b/homeassistant/components/swiss_public_transport/coordinator.py @@ -7,14 +7,17 @@ import logging from typing import TypedDict from opendata_transport import OpendataTransport -from opendata_transport.exceptions import OpendataTransportError +from opendata_transport.exceptions import ( + OpendataTransportConnectionError, + OpendataTransportError, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed import homeassistant.util.dt as dt_util -from .const import DOMAIN, SENSOR_CONNECTIONS_COUNT +from .const import CONNECTIONS_COUNT, DEFAULT_UPDATE_TIME, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -54,7 +57,7 @@ class SwissPublicTransportDataUpdateCoordinator( hass, _LOGGER, name=DOMAIN, - update_interval=timedelta(seconds=90), + update_interval=timedelta(seconds=DEFAULT_UPDATE_TIME), ) self._opendata = opendata @@ -74,14 +77,21 @@ class SwissPublicTransportDataUpdateCoordinator( return None async def _async_update_data(self) -> list[DataConnection]: + return await self.fetch_connections(limit=CONNECTIONS_COUNT) + + async def fetch_connections(self, limit: int) -> list[DataConnection]: + """Fetch connections using the opendata api.""" + self._opendata.limit = limit try: await self._opendata.async_get_data() + except OpendataTransportConnectionError as e: + _LOGGER.warning("Connection to transport.opendata.ch cannot be established") + raise UpdateFailed from e except OpendataTransportError as e: _LOGGER.warning( "Unable to connect and retrieve data from transport.opendata.ch" ) raise UpdateFailed from e - connections = self._opendata.connections return [ DataConnection( @@ -95,6 +105,6 @@ class SwissPublicTransportDataUpdateCoordinator( remaining_time=str(self.remaining_time(connections[i]["departure"])), delay=connections[i]["delay"], ) - for i in range(SENSOR_CONNECTIONS_COUNT) + for i in range(limit) if len(connections) > i and connections[i] is not None ] diff --git a/homeassistant/components/swiss_public_transport/icons.json b/homeassistant/components/swiss_public_transport/icons.json index 10573b8f5c3..7c2e5436834 100644 --- a/homeassistant/components/swiss_public_transport/icons.json +++ b/homeassistant/components/swiss_public_transport/icons.json @@ -23,5 +23,8 @@ "default": "mdi:clock-plus" } } + }, + "services": { + "fetch_connections": "mdi:bus-clock" } } diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index 88a6dbecae4..c186b963705 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, SENSOR_CONNECTIONS_COUNT +from .const import CONNECTIONS_COUNT, DOMAIN from .coordinator import DataConnection, SwissPublicTransportDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -46,7 +46,7 @@ SENSORS: tuple[SwissPublicTransportSensorEntityDescription, ...] = ( value_fn=lambda data_connection: data_connection["departure"], index=i, ) - for i in range(SENSOR_CONNECTIONS_COUNT) + for i in range(CONNECTIONS_COUNT) ], SwissPublicTransportSensorEntityDescription( key="duration", diff --git a/homeassistant/components/swiss_public_transport/services.py b/homeassistant/components/swiss_public_transport/services.py new file mode 100644 index 00000000000..e8b7c6bd458 --- /dev/null +++ b/homeassistant/components/swiss_public_transport/services.py @@ -0,0 +1,89 @@ +"""Define services for the Swiss public transport integration.""" + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, +) +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers.selector import ( + NumberSelector, + NumberSelectorConfig, + NumberSelectorMode, +) +from homeassistant.helpers.update_coordinator import UpdateFailed + +from .const import ( + ATTR_CONFIG_ENTRY_ID, + ATTR_LIMIT, + CONNECTIONS_COUNT, + CONNECTIONS_MAX, + DOMAIN, + SERVICE_FETCH_CONNECTIONS, +) + +SERVICE_FETCH_CONNECTIONS_SCHEMA = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): str, + vol.Optional(ATTR_LIMIT, default=CONNECTIONS_COUNT): NumberSelector( + NumberSelectorConfig( + min=1, max=CONNECTIONS_MAX, mode=NumberSelectorMode.BOX + ) + ), + } +) + + +def async_get_entry( + hass: HomeAssistant, config_entry_id: str +) -> config_entries.ConfigEntry: + """Get the Swiss public transport config entry.""" + if not (entry := hass.config_entries.async_get_entry(config_entry_id)): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="config_entry_not_found", + translation_placeholders={"target": config_entry_id}, + ) + if entry.state is not ConfigEntryState.LOADED: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="not_loaded", + translation_placeholders={"target": entry.title}, + ) + return entry + + +def setup_services(hass: HomeAssistant) -> None: + """Set up the services for the Swiss public transport integration.""" + + async def async_fetch_connections( + call: ServiceCall, + ) -> ServiceResponse: + """Fetch a set of connections.""" + config_entry = async_get_entry(hass, call.data[ATTR_CONFIG_ENTRY_ID]) + limit = call.data.get(ATTR_LIMIT) or CONNECTIONS_COUNT + coordinator = hass.data[DOMAIN][config_entry.entry_id] + try: + connections = await coordinator.fetch_connections(limit=int(limit)) + except UpdateFailed as e: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="cannot_connect", + translation_placeholders={ + "error": str(e), + }, + ) from e + return {"connections": connections} + + hass.services.async_register( + DOMAIN, + SERVICE_FETCH_CONNECTIONS, + async_fetch_connections, + schema=SERVICE_FETCH_CONNECTIONS_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) diff --git a/homeassistant/components/swiss_public_transport/services.yaml b/homeassistant/components/swiss_public_transport/services.yaml new file mode 100644 index 00000000000..d88dad2ca1f --- /dev/null +++ b/homeassistant/components/swiss_public_transport/services.yaml @@ -0,0 +1,14 @@ +fetch_connections: + fields: + config_entry_id: + required: true + selector: + config_entry: + integration: swiss_public_transport + limit: + example: 3 + selector: + number: + min: 1 + max: 15 + step: 1 diff --git a/homeassistant/components/swiss_public_transport/strings.json b/homeassistant/components/swiss_public_transport/strings.json index 4f4bc0522fc..29e73978538 100644 --- a/homeassistant/components/swiss_public_transport/strings.json +++ b/homeassistant/components/swiss_public_transport/strings.json @@ -49,12 +49,37 @@ } } }, + "services": { + "fetch_connections": { + "name": "Fetch Connections", + "description": "Fetch a list of connections from the swiss public transport.", + "fields": { + "config_entry_id": { + "name": "Instance", + "description": "Swiss public transport instance to fetch connections for." + }, + "limit": { + "name": "Limit", + "description": "Number of connections to fetch from [1-15]" + } + } + } + }, "exceptions": { "invalid_data": { "message": "Setup failed for entry {config_title} with invalid data, check at the [stationboard]({stationboard_url}) if your station names are valid.\n{error}" }, "request_timeout": { "message": "Timeout while connecting for entry {config_title}.\n{error}" + }, + "cannot_connect": { + "message": "Cannot connect to server.\n{error}" + }, + "not_loaded": { + "message": "{target} is not loaded." + }, + "config_entry_not_found": { + "message": "Swiss public transport integration instance \"{target}\" not found." } } } diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index c39c070eba2..c5efd05948f 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -41,6 +41,7 @@ ALLOW_NAME_TRANSLATION = { "local_todo", "nmap_tracker", "rpi_power", + "swiss_public_transport", "waze_travel_time", "zodiac", } diff --git a/tests/components/swiss_public_transport/__init__.py b/tests/components/swiss_public_transport/__init__.py index 3859a630c31..98262324b11 100644 --- a/tests/components/swiss_public_transport/__init__.py +++ b/tests/components/swiss_public_transport/__init__.py @@ -1 +1,13 @@ """Tests for the swiss_public_transport integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/swiss_public_transport/fixtures/connections.json b/tests/components/swiss_public_transport/fixtures/connections.json new file mode 100644 index 00000000000..4edead56f14 --- /dev/null +++ b/tests/components/swiss_public_transport/fixtures/connections.json @@ -0,0 +1,130 @@ +[ + { + "departure": "2024-01-06T18:03:00+0100", + "number": 0, + "platform": 0, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:04:00+0100", + "number": 1, + "platform": 1, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:05:00+0100", + "number": 2, + "platform": 2, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:06:00+0100", + "number": 3, + "platform": 3, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:07:00+0100", + "number": 4, + "platform": 4, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:08:00+0100", + "number": 5, + "platform": 5, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:09:00+0100", + "number": 6, + "platform": 6, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:10:00+0100", + "number": 7, + "platform": 7, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:11:00+0100", + "number": 8, + "platform": 8, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:12:00+0100", + "number": 9, + "platform": 9, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:13:00+0100", + "number": 10, + "platform": 10, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:14:00+0100", + "number": 11, + "platform": 11, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:15:00+0100", + "number": 12, + "platform": 12, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:16:00+0100", + "number": 13, + "platform": 13, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:17:00+0100", + "number": 14, + "platform": 14, + "transfers": 0, + "duration": "10", + "delay": 0 + }, + { + "departure": "2024-01-06T18:18:00+0100", + "number": 15, + "platform": 15, + "transfers": 0, + "duration": "10", + "delay": 0 + } +] diff --git a/tests/components/swiss_public_transport/test_init.py b/tests/components/swiss_public_transport/test_init.py index 47360f93cf2..7ee8b696499 100644 --- a/tests/components/swiss_public_transport/test_init.py +++ b/tests/components/swiss_public_transport/test_init.py @@ -1,4 +1,4 @@ -"""Test the swiss_public_transport config flow.""" +"""Test the swiss_public_transport integration.""" from unittest.mock import AsyncMock, patch diff --git a/tests/components/swiss_public_transport/test_service.py b/tests/components/swiss_public_transport/test_service.py new file mode 100644 index 00000000000..34640de9f21 --- /dev/null +++ b/tests/components/swiss_public_transport/test_service.py @@ -0,0 +1,226 @@ +"""Test the swiss_public_transport service.""" + +import json +import logging +from unittest.mock import AsyncMock, patch + +from opendata_transport.exceptions import ( + OpendataTransportConnectionError, + OpendataTransportError, +) +import pytest +from voluptuous import error as vol_er + +from homeassistant.components.swiss_public_transport.const import ( + ATTR_CONFIG_ENTRY_ID, + ATTR_LIMIT, + CONF_DESTINATION, + CONF_START, + CONNECTIONS_COUNT, + CONNECTIONS_MAX, + DOMAIN, + SERVICE_FETCH_CONNECTIONS, +) +from homeassistant.components.swiss_public_transport.helper import unique_id_from_config +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError + +from . import setup_integration + +from tests.common import MockConfigEntry, load_fixture + +_LOGGER = logging.getLogger(__name__) + +MOCK_DATA_STEP_BASE = { + CONF_START: "test_start", + CONF_DESTINATION: "test_destination", +} + + +@pytest.mark.parametrize( + ("limit", "config_data"), + [ + (1, MOCK_DATA_STEP_BASE), + (2, MOCK_DATA_STEP_BASE), + (3, MOCK_DATA_STEP_BASE), + (CONNECTIONS_MAX, MOCK_DATA_STEP_BASE), + (None, MOCK_DATA_STEP_BASE), + ], +) +async def test_service_call_fetch_connections_success( + hass: HomeAssistant, + limit: int, + config_data, +) -> None: + """Test the fetch_connections service.""" + + unique_id = unique_id_from_config(config_data) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=config_data, + title=f"Service test call with limit={limit}", + unique_id=unique_id, + entry_id=f"entry_{unique_id}", + ) + + with patch( + "homeassistant.components.swiss_public_transport.OpendataTransport", + return_value=AsyncMock(), + ) as mock: + mock().connections = json.loads(load_fixture("connections.json", DOMAIN))[ + 0 : (limit or CONNECTIONS_COUNT) + 2 + ] + + await setup_integration(hass, config_entry) + + data = {ATTR_CONFIG_ENTRY_ID: config_entry.entry_id} + if limit is not None: + data[ATTR_LIMIT] = limit + assert hass.services.has_service(DOMAIN, SERVICE_FETCH_CONNECTIONS) + response = await hass.services.async_call( + domain=DOMAIN, + service=SERVICE_FETCH_CONNECTIONS, + service_data=data, + blocking=True, + return_response=True, + ) + await hass.async_block_till_done() + assert response["connections"] is not None + assert len(response["connections"]) == (limit or CONNECTIONS_COUNT) + + +@pytest.mark.parametrize( + ("limit", "config_data", "expected_result", "raise_error"), + [ + (-1, MOCK_DATA_STEP_BASE, pytest.raises(vol_er.MultipleInvalid), None), + (0, MOCK_DATA_STEP_BASE, pytest.raises(vol_er.MultipleInvalid), None), + ( + CONNECTIONS_MAX + 1, + MOCK_DATA_STEP_BASE, + pytest.raises(vol_er.MultipleInvalid), + None, + ), + ( + 1, + MOCK_DATA_STEP_BASE, + pytest.raises(HomeAssistantError), + OpendataTransportConnectionError(), + ), + ( + 2, + MOCK_DATA_STEP_BASE, + pytest.raises(HomeAssistantError), + OpendataTransportError(), + ), + ], +) +async def test_service_call_fetch_connections_error( + hass: HomeAssistant, + limit, + config_data, + expected_result, + raise_error, +) -> None: + """Test service call with standard error.""" + + unique_id = unique_id_from_config(config_data) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=config_data, + title=f"Service test call with limit={limit} and error={raise_error}", + unique_id=unique_id, + entry_id=f"entry_{unique_id}", + ) + + with patch( + "homeassistant.components.swiss_public_transport.OpendataTransport", + return_value=AsyncMock(), + ) as mock: + mock().connections = json.loads(load_fixture("connections.json", DOMAIN)) + + await setup_integration(hass, config_entry) + + assert hass.services.has_service(DOMAIN, SERVICE_FETCH_CONNECTIONS) + mock().async_get_data.side_effect = raise_error + with expected_result: + await hass.services.async_call( + domain=DOMAIN, + service=SERVICE_FETCH_CONNECTIONS, + service_data={ + ATTR_CONFIG_ENTRY_ID: config_entry.entry_id, + ATTR_LIMIT: limit, + }, + blocking=True, + return_response=True, + ) + + +async def test_service_call_load_unload( + hass: HomeAssistant, +) -> None: + """Test service call with integration error.""" + + unique_id = unique_id_from_config(MOCK_DATA_STEP_BASE) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_DATA_STEP_BASE, + title="Service test call for unloaded entry", + unique_id=unique_id, + entry_id=f"entry_{unique_id}", + ) + + bad_entry_id = "bad_entry_id" + + with patch( + "homeassistant.components.swiss_public_transport.OpendataTransport", + return_value=AsyncMock(), + ) as mock: + mock().connections = json.loads(load_fixture("connections.json", DOMAIN)) + + await setup_integration(hass, config_entry) + + assert hass.services.has_service(DOMAIN, SERVICE_FETCH_CONNECTIONS) + response = await hass.services.async_call( + domain=DOMAIN, + service=SERVICE_FETCH_CONNECTIONS, + service_data={ + ATTR_CONFIG_ENTRY_ID: config_entry.entry_id, + }, + blocking=True, + return_response=True, + ) + await hass.async_block_till_done() + assert response["connections"] is not None + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + with pytest.raises( + ServiceValidationError, match=f"{config_entry.title} is not loaded" + ): + await hass.services.async_call( + domain=DOMAIN, + service=SERVICE_FETCH_CONNECTIONS, + service_data={ + ATTR_CONFIG_ENTRY_ID: config_entry.entry_id, + }, + blocking=True, + return_response=True, + ) + + with pytest.raises( + ServiceValidationError, + match=f'Swiss public transport integration instance "{bad_entry_id}" not found', + ): + await hass.services.async_call( + domain=DOMAIN, + service=SERVICE_FETCH_CONNECTIONS, + service_data={ + ATTR_CONFIG_ENTRY_ID: bad_entry_id, + }, + blocking=True, + return_response=True, + ) From 8c5748dcc12cbc7101fa16ac9c0246a1f0cee153 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 12 Aug 2024 13:23:10 +0200 Subject: [PATCH 2161/2411] Remove regex constraint (#123650) --- homeassistant/package_constraints.txt | 5 ----- script/gen_requirements_all.py | 5 ----- 2 files changed, 10 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1f7c6bd61cc..e3f80f1b82d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -98,11 +98,6 @@ enum34==1000000000.0.0 typing==1000000000.0.0 uuid==1000000000.0.0 -# regex causes segfault with version 2021.8.27 -# https://bitbucket.org/mrabarnett/mrab-regex/issues/421/2021827-results-in-fatal-python-error -# This is fixed in 2021.8.28 -regex==2021.8.28 - # httpx requires httpcore, and httpcore requires anyio and h11, but the version constraints on # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index f887f8113a7..aa92cacb237 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -120,11 +120,6 @@ enum34==1000000000.0.0 typing==1000000000.0.0 uuid==1000000000.0.0 -# regex causes segfault with version 2021.8.27 -# https://bitbucket.org/mrabarnett/mrab-regex/issues/421/2021827-results-in-fatal-python-error -# This is fixed in 2021.8.28 -regex==2021.8.28 - # httpx requires httpcore, and httpcore requires anyio and h11, but the version constraints on # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these From e8d7eb05ae875ad5a02930335917613dac0f3e82 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 12 Aug 2024 13:28:09 +0200 Subject: [PATCH 2162/2411] Delete unused snapshots (#123656) * Delete unused snapshots * Delete unused snapshots --- .../accuweather/snapshots/test_weather.ambr | 81 - .../aemet/snapshots/test_weather.ambr | 490 - .../snapshots/test_binary_sensor.ambr | 188 - .../snapshots/test_websocket.ambr | 3 - .../autarco/snapshots/test_sensor.ambr | 402 - .../axis/snapshots/test_binary_sensor.ambr | 825 -- .../azure_devops/snapshots/test_sensor.ambr | 759 - .../calendar/snapshots/test_init.ambr | 30 - .../snapshots/test_default_agent.ambr | 190 - .../deconz/snapshots/test_light.ambr | 1485 -- .../deconz/snapshots/test_number.ambr | 100 - .../deconz/snapshots/test_sensor.ambr | 96 - .../google_tasks/snapshots/test_todo.ambr | 3 - .../snapshots/test_exposed_entities.ambr | 10 - .../snapshots/test_init.ambr | 318 - .../snapshots/test_sensor.ambr | 58 - .../snapshots/test_binary_sensor.ambr | 98 - .../imgw_pib/snapshots/test_sensor.ambr | 110 - .../ipma/snapshots/test_weather.ambr | 115 - .../israel_rail/snapshots/test_sensor.ambr | 285 - .../ista_ecotrend/snapshots/test_sensor.ambr | 60 - .../mastodon/snapshots/test_init.ambr | 33 - .../mealie/snapshots/test_todo.ambr | 14 - .../media_extractor/snapshots/test_init.ambr | 45 - .../met_eireann/snapshots/test_weather.ambr | 100 - .../metoffice/snapshots/test_weather.ambr | 654 - .../components/nam/snapshots/test_sensor.ambr | 47 - .../nextcloud/snapshots/test_config_flow.ambr | 8 - .../nextdns/snapshots/test_binary_sensor.ambr | 2182 --- .../nextdns/snapshots/test_sensor.ambr | 3498 ----- .../nextdns/snapshots/test_switch.ambr | 1390 -- .../nibe_heatpump/snapshots/test_climate.ambr | 6 - .../ping/snapshots/test_binary_sensor.ambr | 60 - .../pyload/snapshots/test_switch.ambr | 47 - .../samsungtv/snapshots/test_init.ambr | 8 + .../smhi/snapshots/test_weather.ambr | 136 - .../solarlog/snapshots/test_sensor.ambr | 1091 -- .../template/snapshots/test_select.ambr | 2 +- .../template/snapshots/test_weather.ambr | 254 - .../tesla_fleet/snapshots/test_sensor.ambr | 61 - .../tessie/snapshots/test_cover.ambr | 33 - .../tomorrowio/snapshots/test_weather.ambr | 1120 -- .../tplink_omada/snapshots/test_switch.ambr | 289 - .../snapshots/test_binary_sensor.ambr | 47 - .../snapshots/test_device_tracker.ambr | 51 - .../tractive/snapshots/test_switch.ambr | 138 - .../unifi/snapshots/test_button.ambr | 94 - .../unifi/snapshots/test_image.ambr | 6 - .../unifi/snapshots/test_sensor.ambr | 135 - .../uptime/snapshots/test_sensor.ambr | 22 - .../snapshots/test_sensor.ambr | 750 - .../weatherkit/snapshots/test_weather.ambr | 12262 ---------------- .../wyoming/snapshots/test_config_flow.ambr | 38 - .../wyoming/snapshots/test_tts.ambr | 22 - .../zeversolar/snapshots/test_sensor.ambr | 20 - 55 files changed, 9 insertions(+), 30360 deletions(-) delete mode 100644 tests/components/mastodon/snapshots/test_init.ambr diff --git a/tests/components/accuweather/snapshots/test_weather.ambr b/tests/components/accuweather/snapshots/test_weather.ambr index 49bf4008884..cbe1891d216 100644 --- a/tests/components/accuweather/snapshots/test_weather.ambr +++ b/tests/components/accuweather/snapshots/test_weather.ambr @@ -1,85 +1,4 @@ # serializer version: 1 -# name: test_forecast_service[get_forecast] - dict({ - 'forecast': list([ - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 58, - 'condition': 'lightning-rainy', - 'datetime': '2020-07-26T05:00:00+00:00', - 'humidity': 60, - 'precipitation': 2.5, - 'precipitation_probability': 60, - 'temperature': 29.5, - 'templow': 15.4, - 'uv_index': 5, - 'wind_bearing': 166, - 'wind_gust_speed': 29.6, - 'wind_speed': 13.0, - }), - dict({ - 'apparent_temperature': 28.9, - 'cloud_coverage': 52, - 'condition': 'partlycloudy', - 'datetime': '2020-07-27T05:00:00+00:00', - 'humidity': 58, - 'precipitation': 0.0, - 'precipitation_probability': 25, - 'temperature': 26.2, - 'templow': 15.9, - 'uv_index': 7, - 'wind_bearing': 297, - 'wind_gust_speed': 14.8, - 'wind_speed': 9.3, - }), - dict({ - 'apparent_temperature': 31.6, - 'cloud_coverage': 65, - 'condition': 'partlycloudy', - 'datetime': '2020-07-28T05:00:00+00:00', - 'humidity': 52, - 'precipitation': 0.0, - 'precipitation_probability': 10, - 'temperature': 31.7, - 'templow': 16.8, - 'uv_index': 7, - 'wind_bearing': 198, - 'wind_gust_speed': 24.1, - 'wind_speed': 16.7, - }), - dict({ - 'apparent_temperature': 26.5, - 'cloud_coverage': 45, - 'condition': 'partlycloudy', - 'datetime': '2020-07-29T05:00:00+00:00', - 'humidity': 65, - 'precipitation': 0.0, - 'precipitation_probability': 9, - 'temperature': 24.0, - 'templow': 11.7, - 'uv_index': 6, - 'wind_bearing': 293, - 'wind_gust_speed': 24.1, - 'wind_speed': 13.0, - }), - dict({ - 'apparent_temperature': 22.2, - 'cloud_coverage': 50, - 'condition': 'partlycloudy', - 'datetime': '2020-07-30T05:00:00+00:00', - 'humidity': 55, - 'precipitation': 0.0, - 'precipitation_probability': 1, - 'temperature': 21.4, - 'templow': 12.2, - 'uv_index': 7, - 'wind_bearing': 280, - 'wind_gust_speed': 27.8, - 'wind_speed': 18.5, - }), - ]), - }) -# --- # name: test_forecast_service[get_forecasts] dict({ 'weather.home': dict({ diff --git a/tests/components/aemet/snapshots/test_weather.ambr b/tests/components/aemet/snapshots/test_weather.ambr index f19f95a6e80..58c854dcda9 100644 --- a/tests/components/aemet/snapshots/test_weather.ambr +++ b/tests/components/aemet/snapshots/test_weather.ambr @@ -1,494 +1,4 @@ # serializer version: 1 -# name: test_forecast_service[get_forecast] - dict({ - 'forecast': list([ - dict({ - 'condition': 'snowy', - 'datetime': '2021-01-08T23:00:00+00:00', - 'precipitation_probability': 0, - 'temperature': 2.0, - 'templow': -1.0, - 'wind_bearing': 90.0, - 'wind_speed': 0.0, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-01-09T23:00:00+00:00', - 'precipitation_probability': 30, - 'temperature': 4.0, - 'templow': -4.0, - 'wind_bearing': 45.0, - 'wind_speed': 20.0, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-01-10T23:00:00+00:00', - 'precipitation_probability': 0, - 'temperature': 3.0, - 'templow': -7.0, - 'wind_bearing': 0.0, - 'wind_speed': 5.0, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-01-11T23:00:00+00:00', - 'precipitation_probability': 0, - 'temperature': -1.0, - 'templow': -13.0, - 'wind_bearing': None, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-01-12T23:00:00+00:00', - 'precipitation_probability': 0, - 'temperature': 6.0, - 'templow': -11.0, - 'wind_bearing': None, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-01-13T23:00:00+00:00', - 'precipitation_probability': 0, - 'temperature': 6.0, - 'templow': -7.0, - 'wind_bearing': None, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-14T23:00:00+00:00', - 'precipitation_probability': 0, - 'temperature': 5.0, - 'templow': -4.0, - 'wind_bearing': None, - }), - ]), - }) -# --- -# name: test_forecast_service[get_forecast].1 - dict({ - 'forecast': list([ - dict({ - 'condition': 'snowy', - 'datetime': '2021-01-09T12:00:00+00:00', - 'precipitation': 2.7, - 'precipitation_probability': 100, - 'temperature': 0.0, - 'wind_bearing': 135.0, - 'wind_gust_speed': 22.0, - 'wind_speed': 15.0, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2021-01-09T13:00:00+00:00', - 'precipitation': 0.6, - 'precipitation_probability': 100, - 'temperature': 0.0, - 'wind_bearing': 135.0, - 'wind_gust_speed': 24.0, - 'wind_speed': 14.0, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2021-01-09T14:00:00+00:00', - 'precipitation': 0.8, - 'precipitation_probability': 100, - 'temperature': 1.0, - 'wind_bearing': 135.0, - 'wind_gust_speed': 20.0, - 'wind_speed': 10.0, - }), - dict({ - 'condition': 'snowy', - 'datetime': '2021-01-09T15:00:00+00:00', - 'precipitation': 1.4, - 'precipitation_probability': 100, - 'temperature': 1.0, - 'wind_bearing': 135.0, - 'wind_gust_speed': 14.0, - 'wind_speed': 8.0, - }), - dict({ - 'condition': 'snowy', - 'datetime': '2021-01-09T16:00:00+00:00', - 'precipitation': 1.2, - 'precipitation_probability': 100, - 'temperature': 1.0, - 'wind_bearing': 135.0, - 'wind_gust_speed': 13.0, - 'wind_speed': 9.0, - }), - dict({ - 'condition': 'snowy', - 'datetime': '2021-01-09T17:00:00+00:00', - 'precipitation': 0.4, - 'precipitation_probability': 100, - 'temperature': 1.0, - 'wind_bearing': 90.0, - 'wind_gust_speed': 13.0, - 'wind_speed': 7.0, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2021-01-09T18:00:00+00:00', - 'precipitation': 0.3, - 'precipitation_probability': 100, - 'temperature': 1.0, - 'wind_bearing': 135.0, - 'wind_gust_speed': 12.0, - 'wind_speed': 8.0, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2021-01-09T19:00:00+00:00', - 'precipitation': 0.1, - 'precipitation_probability': 100, - 'temperature': 1.0, - 'wind_bearing': 135.0, - 'wind_gust_speed': 12.0, - 'wind_speed': 6.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-09T20:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 100, - 'temperature': 1.0, - 'wind_bearing': 90.0, - 'wind_gust_speed': 8.0, - 'wind_speed': 6.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-09T21:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 100, - 'temperature': 1.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 9.0, - 'wind_speed': 6.0, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-01-09T22:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 100, - 'temperature': 1.0, - 'wind_bearing': 90.0, - 'wind_gust_speed': 11.0, - 'wind_speed': 8.0, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-01-09T23:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 10, - 'temperature': 1.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 12.0, - 'wind_speed': 6.0, - }), - dict({ - 'condition': 'fog', - 'datetime': '2021-01-10T00:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 10, - 'temperature': 0.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 10.0, - 'wind_speed': 5.0, - }), - dict({ - 'condition': 'fog', - 'datetime': '2021-01-10T01:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 10, - 'temperature': 0.0, - 'wind_bearing': 0.0, - 'wind_gust_speed': 11.0, - 'wind_speed': 6.0, - }), - dict({ - 'condition': 'fog', - 'datetime': '2021-01-10T02:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 10, - 'temperature': 0.0, - 'wind_bearing': 0.0, - 'wind_gust_speed': 9.0, - 'wind_speed': 6.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T03:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 10, - 'temperature': -1.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 12.0, - 'wind_speed': 8.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T04:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 10, - 'temperature': -1.0, - 'wind_bearing': 0.0, - 'wind_gust_speed': 11.0, - 'wind_speed': 5.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 15, - 'temperature': -1.0, - 'wind_bearing': 0.0, - 'wind_gust_speed': 13.0, - 'wind_speed': 9.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T06:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 15, - 'temperature': -2.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 18.0, - 'wind_speed': 13.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T07:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 15, - 'temperature': -1.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 25.0, - 'wind_speed': 17.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T08:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 15, - 'temperature': -1.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 31.0, - 'wind_speed': 21.0, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-01-10T09:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 15, - 'temperature': 0.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 32.0, - 'wind_speed': 21.0, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-01-10T10:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 15, - 'temperature': 2.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 30.0, - 'wind_speed': 21.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 5, - 'temperature': 3.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 32.0, - 'wind_speed': 22.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T12:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 5, - 'temperature': 3.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 32.0, - 'wind_speed': 20.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T13:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 5, - 'temperature': 3.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 30.0, - 'wind_speed': 19.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T14:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 5, - 'temperature': 4.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 28.0, - 'wind_speed': 17.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T15:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 5, - 'temperature': 3.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 25.0, - 'wind_speed': 16.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T16:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 5, - 'temperature': 2.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 24.0, - 'wind_speed': 16.0, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-01-10T17:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 1.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 24.0, - 'wind_speed': 17.0, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-01-10T18:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 1.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 25.0, - 'wind_speed': 17.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T19:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 1.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 25.0, - 'wind_speed': 16.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T20:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 1.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 24.0, - 'wind_speed': 17.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T21:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 0.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 27.0, - 'wind_speed': 19.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T22:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 0.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 30.0, - 'wind_speed': 21.0, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-01-10T23:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': -1.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 30.0, - 'wind_speed': 19.0, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-01-11T00:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': -1.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 27.0, - 'wind_speed': 16.0, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-01-11T01:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': -2.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 22.0, - 'wind_speed': 12.0, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-01-11T02:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': -2.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 17.0, - 'wind_speed': 10.0, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-01-11T03:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': -3.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 15.0, - 'wind_speed': 11.0, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-01-11T04:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': -4.0, - 'wind_bearing': 45.0, - 'wind_gust_speed': 15.0, - 'wind_speed': 10.0, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-01-11T05:00:00+00:00', - 'precipitation_probability': None, - 'temperature': -4.0, - 'wind_bearing': 0.0, - 'wind_gust_speed': 15.0, - 'wind_speed': 10.0, - }), - ]), - }) -# --- # name: test_forecast_service[get_forecasts] dict({ 'weather.aemet': dict({ diff --git a/tests/components/apsystems/snapshots/test_binary_sensor.ambr b/tests/components/apsystems/snapshots/test_binary_sensor.ambr index bb06b019f31..0875c88976b 100644 --- a/tests/components/apsystems/snapshots/test_binary_sensor.ambr +++ b/tests/components/apsystems/snapshots/test_binary_sensor.ambr @@ -187,191 +187,3 @@ 'state': 'on', }) # --- -# name: test_all_entities[binary_sensor.mock_title_problem-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.mock_title_problem', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Problem', - 'platform': 'apsystems', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'off_grid_status', - 'unique_id': 'MY_SERIAL_NUMBER_off_grid_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[binary_sensor.mock_title_problem-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Mock Title Problem', - }), - 'context': , - 'entity_id': 'binary_sensor.mock_title_problem', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_all_entities[binary_sensor.mock_title_problem_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.mock_title_problem_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Problem', - 'platform': 'apsystems', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'dc_1_short_circuit_error_status', - 'unique_id': 'MY_SERIAL_NUMBER_dc_1_short_circuit_error_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[binary_sensor.mock_title_problem_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Mock Title Problem', - }), - 'context': , - 'entity_id': 'binary_sensor.mock_title_problem_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_all_entities[binary_sensor.mock_title_problem_3-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.mock_title_problem_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Problem', - 'platform': 'apsystems', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'dc_2_short_circuit_error_status', - 'unique_id': 'MY_SERIAL_NUMBER_dc_2_short_circuit_error_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[binary_sensor.mock_title_problem_3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Mock Title Problem', - }), - 'context': , - 'entity_id': 'binary_sensor.mock_title_problem_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_all_entities[binary_sensor.mock_title_problem_4-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.mock_title_problem_4', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Problem', - 'platform': 'apsystems', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'output_fault_status', - 'unique_id': 'MY_SERIAL_NUMBER_output_fault_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[binary_sensor.mock_title_problem_4-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Mock Title Problem', - }), - 'context': , - 'entity_id': 'binary_sensor.mock_title_problem_4', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index e5ae18d28f2..fb1ca6db121 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -663,9 +663,6 @@ # name: test_stt_stream_failed.2 None # --- -# name: test_text_only_pipeline.3 - None -# --- # name: test_text_only_pipeline[extra_msg0] dict({ 'language': 'en', diff --git a/tests/components/autarco/snapshots/test_sensor.ambr b/tests/components/autarco/snapshots/test_sensor.ambr index 2ff0236a59f..0aa093d6a6d 100644 --- a/tests/components/autarco/snapshots/test_sensor.ambr +++ b/tests/components/autarco/snapshots/test_sensor.ambr @@ -401,405 +401,3 @@ 'state': '200', }) # --- -# name: test_solar_sensors[sensor.inverter_test_serial_1_energy_ac_output_total-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.inverter_test_serial_1_energy_ac_output_total', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Energy AC output total', - 'platform': 'autarco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'out_ac_energy_total', - 'unique_id': 'test-serial-1_out_ac_energy_total', - 'unit_of_measurement': , - }) -# --- -# name: test_solar_sensors[sensor.inverter_test_serial_1_energy_ac_output_total-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Inverter test-serial-1 Energy AC output total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.inverter_test_serial_1_energy_ac_output_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10379', - }) -# --- -# name: test_solar_sensors[sensor.inverter_test_serial_1_power_ac_output-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.inverter_test_serial_1_power_ac_output', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power AC output', - 'platform': 'autarco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'out_ac_power', - 'unique_id': 'test-serial-1_out_ac_power', - 'unit_of_measurement': , - }) -# --- -# name: test_solar_sensors[sensor.inverter_test_serial_1_power_ac_output-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Inverter test-serial-1 Power AC output', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.inverter_test_serial_1_power_ac_output', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '200', - }) -# --- -# name: test_solar_sensors[sensor.inverter_test_serial_2_energy_ac_output_total-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.inverter_test_serial_2_energy_ac_output_total', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Energy AC output total', - 'platform': 'autarco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'out_ac_energy_total', - 'unique_id': 'test-serial-2_out_ac_energy_total', - 'unit_of_measurement': , - }) -# --- -# name: test_solar_sensors[sensor.inverter_test_serial_2_energy_ac_output_total-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Inverter test-serial-2 Energy AC output total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.inverter_test_serial_2_energy_ac_output_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10379', - }) -# --- -# name: test_solar_sensors[sensor.inverter_test_serial_2_power_ac_output-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.inverter_test_serial_2_power_ac_output', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power AC output', - 'platform': 'autarco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'out_ac_power', - 'unique_id': 'test-serial-2_out_ac_power', - 'unit_of_measurement': , - }) -# --- -# name: test_solar_sensors[sensor.inverter_test_serial_2_power_ac_output-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Inverter test-serial-2 Power AC output', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.inverter_test_serial_2_power_ac_output', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '500', - }) -# --- -# name: test_solar_sensors[sensor.solar_energy_production_month-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solar_energy_production_month', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Energy production month', - 'platform': 'autarco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'energy_production_month', - 'unique_id': '1_solar_energy_production_month', - 'unit_of_measurement': , - }) -# --- -# name: test_solar_sensors[sensor.solar_energy_production_month-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Solar Energy production month', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solar_energy_production_month', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '58', - }) -# --- -# name: test_solar_sensors[sensor.solar_energy_production_today-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solar_energy_production_today', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Energy production today', - 'platform': 'autarco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'energy_production_today', - 'unique_id': '1_solar_energy_production_today', - 'unit_of_measurement': , - }) -# --- -# name: test_solar_sensors[sensor.solar_energy_production_today-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Solar Energy production today', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solar_energy_production_today', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '4', - }) -# --- -# name: test_solar_sensors[sensor.solar_energy_production_total-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solar_energy_production_total', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Energy production total', - 'platform': 'autarco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'energy_production_total', - 'unique_id': '1_solar_energy_production_total', - 'unit_of_measurement': , - }) -# --- -# name: test_solar_sensors[sensor.solar_energy_production_total-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Solar Energy production total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solar_energy_production_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10379', - }) -# --- -# name: test_solar_sensors[sensor.solar_power_production-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solar_power_production', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power production', - 'platform': 'autarco', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'power_production', - 'unique_id': '1_solar_power_production', - 'unit_of_measurement': , - }) -# --- -# name: test_solar_sensors[sensor.solar_power_production-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Solar Power production', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solar_power_production', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '200', - }) -# --- diff --git a/tests/components/axis/snapshots/test_binary_sensor.ambr b/tests/components/axis/snapshots/test_binary_sensor.ambr index 94b1cc2fc2e..ab860489d55 100644 --- a/tests/components/axis/snapshots/test_binary_sensor.ambr +++ b/tests/components/axis/snapshots/test_binary_sensor.ambr @@ -1,79 +1,4 @@ # serializer version: 1 -# name: test_binary_sensors[event0-binary_sensor.name_daynight_1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'light', - 'friendly_name': 'name DayNight 1', - }), - 'context': , - 'entity_id': 'binary_sensor.name_daynight_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event0-daynight_1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'light', - 'friendly_name': 'name DayNight 1', - }), - 'context': , - 'entity_id': 'binary_sensor.name_daynight_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event0-daynight_1][binary_sensor.home_daynight_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.home_daynight_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'DayNight 1', - 'platform': 'axis', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:40:8c:12:34:56-tns1:VideoSource/tnsaxis:DayNightVision-1', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[event0-daynight_1][binary_sensor.home_daynight_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'light', - 'friendly_name': 'home DayNight 1', - }), - 'context': , - 'entity_id': 'binary_sensor.home_daynight_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_binary_sensors[event0][binary_sensor.home_daynight_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -121,156 +46,6 @@ 'state': 'on', }) # --- -# name: test_binary_sensors[event1-binary_sensor.name_sound_1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'sound', - 'friendly_name': 'name Sound 1', - }), - 'context': , - 'entity_id': 'binary_sensor.name_sound_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensors[event1-sound_1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'sound', - 'friendly_name': 'name Sound 1', - }), - 'context': , - 'entity_id': 'binary_sensor.name_sound_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensors[event1-sound_1][binary_sensor.home_sound_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.home_sound_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Sound 1', - 'platform': 'axis', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:40:8c:12:34:56-tns1:AudioSource/tnsaxis:TriggerLevel-1', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[event1-sound_1][binary_sensor.home_sound_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'sound', - 'friendly_name': 'home Sound 1', - }), - 'context': , - 'entity_id': 'binary_sensor.home_sound_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensors[event10-binary_sensor.name_object_analytics_device1scenario8] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name Object Analytics Device1Scenario8', - }), - 'context': , - 'entity_id': 'binary_sensor.name_object_analytics_device1scenario8', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event10-object_analytics_device1scenario8] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name Object Analytics Device1Scenario8', - }), - 'context': , - 'entity_id': 'binary_sensor.name_object_analytics_device1scenario8', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event10-object_analytics_device1scenario8][binary_sensor.home_object_analytics_device1scenario8-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.home_object_analytics_device1scenario8', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Object Analytics Device1Scenario8', - 'platform': 'axis', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario8-Device1Scenario8', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[event10-object_analytics_device1scenario8][binary_sensor.home_object_analytics_device1scenario8-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'home Object Analytics Device1Scenario8', - }), - 'context': , - 'entity_id': 'binary_sensor.home_object_analytics_device1scenario8', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_binary_sensors[event10][binary_sensor.home_object_analytics_device1scenario8-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -365,81 +140,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[event2-binary_sensor.name_pir_sensor] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'connectivity', - 'friendly_name': 'name PIR sensor', - }), - 'context': , - 'entity_id': 'binary_sensor.name_pir_sensor', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensors[event2-pir_sensor] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'connectivity', - 'friendly_name': 'name PIR sensor', - }), - 'context': , - 'entity_id': 'binary_sensor.name_pir_sensor', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensors[event2-pir_sensor][binary_sensor.home_pir_sensor-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.home_pir_sensor', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'PIR sensor', - 'platform': 'axis', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:40:8c:12:34:56-tns1:Device/tnsaxis:IO/Port-0', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[event2-pir_sensor][binary_sensor.home_pir_sensor-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'connectivity', - 'friendly_name': 'home PIR sensor', - }), - 'context': , - 'entity_id': 'binary_sensor.home_pir_sensor', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_binary_sensors[event2][binary_sensor.home_pir_sensor-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -487,81 +187,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[event3-binary_sensor.name_pir_0] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name PIR 0', - }), - 'context': , - 'entity_id': 'binary_sensor.name_pir_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensors[event3-pir_0] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name PIR 0', - }), - 'context': , - 'entity_id': 'binary_sensor.name_pir_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensors[event3-pir_0][binary_sensor.home_pir_0-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.home_pir_0', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'PIR 0', - 'platform': 'axis', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:40:8c:12:34:56-tns1:Device/tnsaxis:Sensor/PIR-0', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[event3-pir_0][binary_sensor.home_pir_0-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'home PIR 0', - }), - 'context': , - 'entity_id': 'binary_sensor.home_pir_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_binary_sensors[event3][binary_sensor.home_pir_0-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -609,81 +234,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[event4-binary_sensor.name_fence_guard_profile_1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name Fence Guard Profile 1', - }), - 'context': , - 'entity_id': 'binary_sensor.name_fence_guard_profile_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event4-fence_guard_profile_1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name Fence Guard Profile 1', - }), - 'context': , - 'entity_id': 'binary_sensor.name_fence_guard_profile_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event4-fence_guard_profile_1][binary_sensor.home_fence_guard_profile_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.home_fence_guard_profile_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Fence Guard Profile 1', - 'platform': 'axis', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/FenceGuard/Camera1Profile1-Camera1Profile1', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[event4-fence_guard_profile_1][binary_sensor.home_fence_guard_profile_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'home Fence Guard Profile 1', - }), - 'context': , - 'entity_id': 'binary_sensor.home_fence_guard_profile_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_binary_sensors[event4][binary_sensor.home_fence_guard_profile_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -731,81 +281,6 @@ 'state': 'on', }) # --- -# name: test_binary_sensors[event5-binary_sensor.name_motion_guard_profile_1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name Motion Guard Profile 1', - }), - 'context': , - 'entity_id': 'binary_sensor.name_motion_guard_profile_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event5-motion_guard_profile_1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name Motion Guard Profile 1', - }), - 'context': , - 'entity_id': 'binary_sensor.name_motion_guard_profile_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event5-motion_guard_profile_1][binary_sensor.home_motion_guard_profile_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.home_motion_guard_profile_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Motion Guard Profile 1', - 'platform': 'axis', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/MotionGuard/Camera1Profile1-Camera1Profile1', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[event5-motion_guard_profile_1][binary_sensor.home_motion_guard_profile_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'home Motion Guard Profile 1', - }), - 'context': , - 'entity_id': 'binary_sensor.home_motion_guard_profile_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_binary_sensors[event5][binary_sensor.home_motion_guard_profile_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -853,81 +328,6 @@ 'state': 'on', }) # --- -# name: test_binary_sensors[event6-binary_sensor.name_loitering_guard_profile_1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name Loitering Guard Profile 1', - }), - 'context': , - 'entity_id': 'binary_sensor.name_loitering_guard_profile_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event6-loitering_guard_profile_1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name Loitering Guard Profile 1', - }), - 'context': , - 'entity_id': 'binary_sensor.name_loitering_guard_profile_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event6-loitering_guard_profile_1][binary_sensor.home_loitering_guard_profile_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.home_loitering_guard_profile_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Loitering Guard Profile 1', - 'platform': 'axis', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/LoiteringGuard/Camera1Profile1-Camera1Profile1', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[event6-loitering_guard_profile_1][binary_sensor.home_loitering_guard_profile_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'home Loitering Guard Profile 1', - }), - 'context': , - 'entity_id': 'binary_sensor.home_loitering_guard_profile_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_binary_sensors[event6][binary_sensor.home_loitering_guard_profile_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -975,81 +375,6 @@ 'state': 'on', }) # --- -# name: test_binary_sensors[event7-binary_sensor.name_vmd4_profile_1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name VMD4 Profile 1', - }), - 'context': , - 'entity_id': 'binary_sensor.name_vmd4_profile_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event7-vmd4_profile_1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name VMD4 Profile 1', - }), - 'context': , - 'entity_id': 'binary_sensor.name_vmd4_profile_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event7-vmd4_profile_1][binary_sensor.home_vmd4_profile_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.home_vmd4_profile_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'VMD4 Profile 1', - 'platform': 'axis', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1-Camera1Profile1', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[event7-vmd4_profile_1][binary_sensor.home_vmd4_profile_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'home VMD4 Profile 1', - }), - 'context': , - 'entity_id': 'binary_sensor.home_vmd4_profile_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_binary_sensors[event7][binary_sensor.home_vmd4_profile_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1097,81 +422,6 @@ 'state': 'on', }) # --- -# name: test_binary_sensors[event8-binary_sensor.name_object_analytics_scenario_1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name Object Analytics Scenario 1', - }), - 'context': , - 'entity_id': 'binary_sensor.name_object_analytics_scenario_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event8-object_analytics_scenario_1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name Object Analytics Scenario 1', - }), - 'context': , - 'entity_id': 'binary_sensor.name_object_analytics_scenario_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event8-object_analytics_scenario_1][binary_sensor.home_object_analytics_scenario_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.home_object_analytics_scenario_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Object Analytics Scenario 1', - 'platform': 'axis', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1-Device1Scenario1', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[event8-object_analytics_scenario_1][binary_sensor.home_object_analytics_scenario_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'home Object Analytics Scenario 1', - }), - 'context': , - 'entity_id': 'binary_sensor.home_object_analytics_scenario_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_binary_sensors[event8][binary_sensor.home_object_analytics_scenario_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1219,81 +469,6 @@ 'state': 'on', }) # --- -# name: test_binary_sensors[event9-binary_sensor.name_vmd4_camera1profile9] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name VMD4 Camera1Profile9', - }), - 'context': , - 'entity_id': 'binary_sensor.name_vmd4_camera1profile9', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event9-vmd4_camera1profile9] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'name VMD4 Camera1Profile9', - }), - 'context': , - 'entity_id': 'binary_sensor.name_vmd4_camera1profile9', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensors[event9-vmd4_camera1profile9][binary_sensor.home_vmd4_camera1profile9-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.home_vmd4_camera1profile9', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'VMD4 Camera1Profile9', - 'platform': 'axis', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile9-Camera1Profile9', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[event9-vmd4_camera1profile9][binary_sensor.home_vmd4_camera1profile9-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'home VMD4 Camera1Profile9', - }), - 'context': , - 'entity_id': 'binary_sensor.home_vmd4_camera1profile9', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_binary_sensors[event9][binary_sensor.home_vmd4_camera1profile9-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/azure_devops/snapshots/test_sensor.ambr b/tests/components/azure_devops/snapshots/test_sensor.ambr index 0ce82cae1e8..aa8d1d9e7e0 100644 --- a/tests/components/azure_devops/snapshots/test_sensor.ambr +++ b/tests/components/azure_devops/snapshots/test_sensor.ambr @@ -1,467 +1,4 @@ # serializer version: 1 -# name: test_sensors[sensor.testproject_ci_build_finish_time-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.testproject_ci_build_finish_time', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'CI build finish time', - 'platform': 'azure_devops', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'finish_time', - 'unique_id': 'testorg_1234_9876_finish_time', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_finish_time-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'date', - 'friendly_name': 'testproject CI build finish time', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_finish_time', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2021-01-01T00:00:00+00:00', - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_id-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.testproject_ci_build_id', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'CI build id', - 'platform': 'azure_devops', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'build_id', - 'unique_id': 'testorg_1234_9876_build_id', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_id-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI build id', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_id', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '5678', - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_queue_time-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.testproject_ci_build_queue_time', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'CI build queue time', - 'platform': 'azure_devops', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'queue_time', - 'unique_id': 'testorg_1234_9876_queue_time', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_queue_time-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'date', - 'friendly_name': 'testproject CI build queue time', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_queue_time', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2021-01-01T00:00:00+00:00', - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_reason-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.testproject_ci_build_reason', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'CI build reason', - 'platform': 'azure_devops', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'reason', - 'unique_id': 'testorg_1234_9876_reason', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_reason-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI build reason', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_reason', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'manual', - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_result-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.testproject_ci_build_result', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'CI build result', - 'platform': 'azure_devops', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'result', - 'unique_id': 'testorg_1234_9876_result', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_result-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI build result', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_result', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'succeeded', - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_source_branch-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.testproject_ci_build_source_branch', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'CI build source branch', - 'platform': 'azure_devops', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'source_branch', - 'unique_id': 'testorg_1234_9876_source_branch', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_source_branch-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI build source branch', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_source_branch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'main', - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_source_version-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.testproject_ci_build_source_version', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'CI build source version', - 'platform': 'azure_devops', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'source_version', - 'unique_id': 'testorg_1234_9876_source_version', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_source_version-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI build source version', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_source_version', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '123', - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_start_time-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.testproject_ci_build_start_time', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'CI build start time', - 'platform': 'azure_devops', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'start_time', - 'unique_id': 'testorg_1234_9876_start_time', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_start_time-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'date', - 'friendly_name': 'testproject CI build start time', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_start_time', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2021-01-01T00:00:00+00:00', - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.testproject_ci_build_status', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'CI build status', - 'platform': 'azure_devops', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'status', - 'unique_id': 'testorg_1234_9876_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI build status', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'completed', - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_url-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.testproject_ci_build_url', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'CI build url', - 'platform': 'azure_devops', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'url', - 'unique_id': 'testorg_1234_9876_url', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.testproject_ci_build_url-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI build url', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_url', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_sensors[sensor.testproject_ci_latest_build-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -891,52 +428,6 @@ 'state': '2021-01-01T00:00:00+00:00', }) # --- -# name: test_sensors[sensor.testproject_ci_latest_build_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.testproject_ci_latest_build_status', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'CI latest build status', - 'platform': 'azure_devops', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'status', - 'unique_id': 'testorg_1234_9876_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.testproject_ci_latest_build_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI latest build status', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_latest_build_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'completed', - }) -# --- # name: test_sensors[sensor.testproject_ci_latest_build_url-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -983,243 +474,6 @@ 'state': 'unknown', }) # --- -# name: test_sensors[sensor.testproject_test_build_build_id-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.testproject_test_build_build_id', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Test Build build id', - 'platform': 'azure_devops', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'build_id', - 'unique_id': 'testorg_1234_9876_build_id', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.testproject_test_build_build_id-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject Test Build build id', - }), - 'context': , - 'entity_id': 'sensor.testproject_test_build_build_id', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '5678', - }) -# --- -# name: test_sensors[sensor.testproject_test_build_latest_build-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.testproject_test_build_latest_build', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Test Build latest build', - 'platform': 'azure_devops', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'latest_build', - 'unique_id': 'testorg_1234_9876_latest_build', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.testproject_test_build_latest_build-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'definition_id': 9876, - 'definition_name': 'Test Build', - 'finish_time': '2021-01-01T00:00:00Z', - 'friendly_name': 'testproject Test Build latest build', - 'id': 5678, - 'queue_time': '2021-01-01T00:00:00Z', - 'reason': 'manual', - 'result': 'succeeded', - 'source_branch': 'main', - 'source_version': '123', - 'start_time': '2021-01-01T00:00:00Z', - 'status': 'completed', - 'url': None, - }), - 'context': , - 'entity_id': 'sensor.testproject_test_build_latest_build', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1', - }) -# --- -# name: test_sensors_missing_data[sensor.testproject_ci_build_finish_time-state-missing-data] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'date', - 'friendly_name': 'testproject CI build finish time', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_finish_time', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_sensors_missing_data[sensor.testproject_ci_build_id-state-missing-data] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI build id', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_id', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '6789', - }) -# --- -# name: test_sensors_missing_data[sensor.testproject_ci_build_queue_time-state-missing-data] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'date', - 'friendly_name': 'testproject CI build queue time', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_queue_time', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_sensors_missing_data[sensor.testproject_ci_build_reason-state-missing-data] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI build reason', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_reason', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_sensors_missing_data[sensor.testproject_ci_build_result-state-missing-data] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI build result', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_result', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_sensors_missing_data[sensor.testproject_ci_build_source_branch-state-missing-data] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI build source branch', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_source_branch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_sensors_missing_data[sensor.testproject_ci_build_source_version-state-missing-data] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI build source version', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_source_version', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_sensors_missing_data[sensor.testproject_ci_build_start_time-state-missing-data] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'date', - 'friendly_name': 'testproject CI build start time', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_start_time', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_sensors_missing_data[sensor.testproject_ci_build_status-state-missing-data] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI build status', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_sensors_missing_data[sensor.testproject_ci_build_url-state-missing-data] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI build url', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_build_url', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_sensors_missing_data[sensor.testproject_ci_latest_build-state-missing-data] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -1352,19 +606,6 @@ 'state': 'unknown', }) # --- -# name: test_sensors_missing_data[sensor.testproject_ci_latest_build_status-state-missing-data] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'testproject CI latest build status', - }), - 'context': , - 'entity_id': 'sensor.testproject_ci_latest_build_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_sensors_missing_data[sensor.testproject_ci_latest_build_url-state-missing-data] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/calendar/snapshots/test_init.ambr b/tests/components/calendar/snapshots/test_init.ambr index fe23c5dbac9..1b2bb9f0196 100644 --- a/tests/components/calendar/snapshots/test_init.ambr +++ b/tests/components/calendar/snapshots/test_init.ambr @@ -7,12 +7,6 @@ }), }) # --- -# name: test_list_events_service_duration[frozen_time-calendar.calendar_1-00:15:00-list_events] - dict({ - 'events': list([ - ]), - }) -# --- # name: test_list_events_service_duration[frozen_time-calendar.calendar_1-01:00:00-get_events] dict({ 'calendar.calendar_1': dict({ @@ -28,19 +22,6 @@ }), }) # --- -# name: test_list_events_service_duration[frozen_time-calendar.calendar_1-01:00:00-list_events] - dict({ - 'events': list([ - dict({ - 'description': 'Future Description', - 'end': '2023-10-19T09:20:05-06:00', - 'location': 'Future Location', - 'start': '2023-10-19T08:20:05-06:00', - 'summary': 'Future Event', - }), - ]), - }) -# --- # name: test_list_events_service_duration[frozen_time-calendar.calendar_2-00:15:00-get_events] dict({ 'calendar.calendar_2': dict({ @@ -54,14 +35,3 @@ }), }) # --- -# name: test_list_events_service_duration[frozen_time-calendar.calendar_2-00:15:00-list_events] - dict({ - 'events': list([ - dict({ - 'end': '2023-10-19T08:20:05-06:00', - 'start': '2023-10-19T07:20:05-06:00', - 'summary': 'Current Event', - }), - ]), - }) -# --- diff --git a/tests/components/conversation/snapshots/test_default_agent.ambr b/tests/components/conversation/snapshots/test_default_agent.ambr index d015b19ddc1..051613f0300 100644 --- a/tests/components/conversation/snapshots/test_default_agent.ambr +++ b/tests/components/conversation/snapshots/test_default_agent.ambr @@ -344,126 +344,6 @@ }), }) # --- -# name: test_intent_entity_exposed.1 - dict({ - 'conversation_id': None, - 'response': dict({ - 'card': dict({ - }), - 'data': dict({ - 'code': 'no_valid_targets', - }), - 'language': 'en', - 'response_type': 'error', - 'speech': dict({ - 'plain': dict({ - 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called my cool light', - }), - }), - }), - }) -# --- -# name: test_intent_entity_exposed.2 - dict({ - 'conversation_id': None, - 'response': dict({ - 'card': dict({ - }), - 'data': dict({ - 'code': 'no_valid_targets', - }), - 'language': 'en', - 'response_type': 'error', - 'speech': dict({ - 'plain': dict({ - 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen light', - }), - }), - }), - }) -# --- -# name: test_intent_entity_exposed.3 - dict({ - 'conversation_id': None, - 'response': dict({ - 'card': dict({ - }), - 'data': dict({ - 'code': 'no_valid_targets', - }), - 'language': 'en', - 'response_type': 'error', - 'speech': dict({ - 'plain': dict({ - 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called my cool light', - }), - }), - }), - }) -# --- -# name: test_intent_entity_exposed.4 - dict({ - 'conversation_id': None, - 'response': dict({ - 'card': dict({ - }), - 'data': dict({ - 'failed': list([ - ]), - 'success': list([ - dict({ - 'id': 'light.kitchen', - 'name': 'kitchen light', - 'type': , - }), - ]), - 'targets': list([ - ]), - }), - 'language': 'en', - 'response_type': 'action_done', - 'speech': dict({ - 'plain': dict({ - 'extra_data': None, - 'speech': 'Turned on the light', - }), - }), - }), - }) -# --- -# name: test_intent_entity_exposed.5 - dict({ - 'conversation_id': None, - 'response': dict({ - 'card': dict({ - }), - 'data': dict({ - 'failed': list([ - ]), - 'success': list([ - dict({ - 'id': 'light.kitchen', - 'name': 'kitchen light', - 'type': , - }), - ]), - 'targets': list([ - ]), - }), - 'language': 'en', - 'response_type': 'action_done', - 'speech': dict({ - 'plain': dict({ - 'extra_data': None, - 'speech': 'Turned on the light', - }), - }), - }), - }) -# --- # name: test_intent_entity_fail_if_unexposed dict({ 'conversation_id': None, @@ -614,73 +494,3 @@ }), }) # --- -# name: test_intent_entity_renamed.2 - dict({ - 'conversation_id': None, - 'response': dict({ - 'card': dict({ - }), - 'data': dict({ - 'code': 'no_valid_targets', - }), - 'language': 'en', - 'response_type': 'error', - 'speech': dict({ - 'plain': dict({ - 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen light', - }), - }), - }), - }) -# --- -# name: test_intent_entity_renamed.3 - dict({ - 'conversation_id': None, - 'response': dict({ - 'card': dict({ - }), - 'data': dict({ - 'failed': list([ - ]), - 'success': list([ - dict({ - 'id': 'light.kitchen', - 'name': 'kitchen light', - 'type': , - }), - ]), - 'targets': list([ - ]), - }), - 'language': 'en', - 'response_type': 'action_done', - 'speech': dict({ - 'plain': dict({ - 'extra_data': None, - 'speech': 'Turned on the light', - }), - }), - }), - }) -# --- -# name: test_intent_entity_renamed.4 - dict({ - 'conversation_id': None, - 'response': dict({ - 'card': dict({ - }), - 'data': dict({ - 'code': 'no_valid_targets', - }), - 'language': 'en', - 'response_type': 'error', - 'speech': dict({ - 'plain': dict({ - 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called renamed light', - }), - }), - }), - }) -# --- diff --git a/tests/components/deconz/snapshots/test_light.ambr b/tests/components/deconz/snapshots/test_light.ambr index 46b6611dcbe..b5a9f7b5543 100644 --- a/tests/components/deconz/snapshots/test_light.ambr +++ b/tests/components/deconz/snapshots/test_light.ambr @@ -1,308 +1,4 @@ # serializer version: 1 -# name: test_groups[input0-expected0-light_payload0][light.dimmable_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.dimmable_light', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Dimmable light', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00:00:02-00', - 'unit_of_measurement': None, - }) -# --- -# name: test_groups[input0-expected0-light_payload0][light.dimmable_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 255, - 'color_mode': , - 'friendly_name': 'Dimmable light', - 'is_deconz_group': False, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.dimmable_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_groups[input0-expected0-light_payload0][light.group-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'effect_list': list([ - 'colorloop', - ]), - 'max_color_temp_kelvin': 6535, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.group', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '01234E56789A-/groups/0', - 'unit_of_measurement': None, - }) -# --- -# name: test_groups[input0-expected0-light_payload0][light.group-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'all_on': False, - 'brightness': 255, - 'color_mode': , - 'color_temp': 2500, - 'color_temp_kelvin': 400, - 'effect': None, - 'effect_list': list([ - 'colorloop', - ]), - 'friendly_name': 'Group', - 'hs_color': tuple( - 15.981, - 100.0, - ), - 'is_deconz_group': True, - 'max_color_temp_kelvin': 6535, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': tuple( - 255, - 67, - 0, - ), - 'supported_color_modes': list([ - , - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.674, - 0.322, - ), - }), - 'context': , - 'entity_id': 'light.group', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_groups[input0-expected0-light_payload0][light.rgb_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'effect_list': list([ - 'colorloop', - ]), - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.rgb_light', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'RGB light', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00:00:00-00', - 'unit_of_measurement': None, - }) -# --- -# name: test_groups[input0-expected0-light_payload0][light.rgb_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 50, - 'color_mode': , - 'effect': None, - 'effect_list': list([ - 'colorloop', - ]), - 'friendly_name': 'RGB light', - 'hs_color': tuple( - 52.0, - 100.0, - ), - 'is_deconz_group': False, - 'rgb_color': tuple( - 255, - 221, - 0, - ), - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.5, - 0.5, - ), - }), - 'context': , - 'entity_id': 'light.rgb_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_groups[input0-expected0-light_payload0][light.tunable_white_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6451, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 155, - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.tunable_white_light', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Tunable white light', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00:00:01-00', - 'unit_of_measurement': None, - }) -# --- -# name: test_groups[input0-expected0-light_payload0][light.tunable_white_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': , - 'color_temp': 2500, - 'color_temp_kelvin': 400, - 'friendly_name': 'Tunable white light', - 'hs_color': tuple( - 15.981, - 100.0, - ), - 'is_deconz_group': False, - 'max_color_temp_kelvin': 6451, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 155, - 'rgb_color': tuple( - 255, - 67, - 0, - ), - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.674, - 0.322, - ), - }), - 'context': , - 'entity_id': 'light.tunable_white_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_groups[input0-light_payload0][light.dimmable_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -607,310 +303,6 @@ 'state': 'on', }) # --- -# name: test_groups[input1-expected1-light_payload0][light.dimmable_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.dimmable_light', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Dimmable light', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00:00:02-00', - 'unit_of_measurement': None, - }) -# --- -# name: test_groups[input1-expected1-light_payload0][light.dimmable_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 255, - 'color_mode': , - 'friendly_name': 'Dimmable light', - 'is_deconz_group': False, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.dimmable_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_groups[input1-expected1-light_payload0][light.group-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'effect_list': list([ - 'colorloop', - ]), - 'max_color_temp_kelvin': 6535, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.group', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '01234E56789A-/groups/0', - 'unit_of_measurement': None, - }) -# --- -# name: test_groups[input1-expected1-light_payload0][light.group-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'all_on': False, - 'brightness': 50, - 'color_mode': , - 'color_temp': 2500, - 'color_temp_kelvin': 400, - 'effect': None, - 'effect_list': list([ - 'colorloop', - ]), - 'friendly_name': 'Group', - 'hs_color': tuple( - 15.981, - 100.0, - ), - 'is_deconz_group': True, - 'max_color_temp_kelvin': 6535, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': tuple( - 255, - 67, - 0, - ), - 'supported_color_modes': list([ - , - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.674, - 0.322, - ), - }), - 'context': , - 'entity_id': 'light.group', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_groups[input1-expected1-light_payload0][light.rgb_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'effect_list': list([ - 'colorloop', - ]), - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.rgb_light', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'RGB light', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00:00:00-00', - 'unit_of_measurement': None, - }) -# --- -# name: test_groups[input1-expected1-light_payload0][light.rgb_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 50, - 'color_mode': , - 'effect': None, - 'effect_list': list([ - 'colorloop', - ]), - 'friendly_name': 'RGB light', - 'hs_color': tuple( - 52.0, - 100.0, - ), - 'is_deconz_group': False, - 'rgb_color': tuple( - 255, - 221, - 0, - ), - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.5, - 0.5, - ), - }), - 'context': , - 'entity_id': 'light.rgb_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_groups[input1-expected1-light_payload0][light.tunable_white_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6451, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 155, - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.tunable_white_light', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Tunable white light', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00:00:01-00', - 'unit_of_measurement': None, - }) -# --- -# name: test_groups[input1-expected1-light_payload0][light.tunable_white_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': , - 'color_temp': 2500, - 'color_temp_kelvin': 400, - 'friendly_name': 'Tunable white light', - 'hs_color': tuple( - 15.981, - 100.0, - ), - 'is_deconz_group': False, - 'max_color_temp_kelvin': 6451, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 155, - 'rgb_color': tuple( - 255, - 67, - 0, - ), - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.674, - 0.322, - ), - }), - 'context': , - 'entity_id': 'light.tunable_white_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_groups[input1-light_payload0][light.dimmable_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1215,310 +607,6 @@ 'state': 'on', }) # --- -# name: test_groups[input2-expected2-light_payload0][light.dimmable_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.dimmable_light', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Dimmable light', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00:00:02-00', - 'unit_of_measurement': None, - }) -# --- -# name: test_groups[input2-expected2-light_payload0][light.dimmable_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 255, - 'color_mode': , - 'friendly_name': 'Dimmable light', - 'is_deconz_group': False, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.dimmable_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_groups[input2-expected2-light_payload0][light.group-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'effect_list': list([ - 'colorloop', - ]), - 'max_color_temp_kelvin': 6535, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.group', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '01234E56789A-/groups/0', - 'unit_of_measurement': None, - }) -# --- -# name: test_groups[input2-expected2-light_payload0][light.group-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'all_on': False, - 'brightness': 50, - 'color_mode': , - 'color_temp': None, - 'color_temp_kelvin': None, - 'effect': None, - 'effect_list': list([ - 'colorloop', - ]), - 'friendly_name': 'Group', - 'hs_color': tuple( - 52.0, - 100.0, - ), - 'is_deconz_group': True, - 'max_color_temp_kelvin': 6535, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': tuple( - 255, - 221, - 0, - ), - 'supported_color_modes': list([ - , - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.5, - 0.5, - ), - }), - 'context': , - 'entity_id': 'light.group', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_groups[input2-expected2-light_payload0][light.rgb_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'effect_list': list([ - 'colorloop', - ]), - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.rgb_light', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'RGB light', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00:00:00-00', - 'unit_of_measurement': None, - }) -# --- -# name: test_groups[input2-expected2-light_payload0][light.rgb_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 50, - 'color_mode': , - 'effect': None, - 'effect_list': list([ - 'colorloop', - ]), - 'friendly_name': 'RGB light', - 'hs_color': tuple( - 52.0, - 100.0, - ), - 'is_deconz_group': False, - 'rgb_color': tuple( - 255, - 221, - 0, - ), - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.5, - 0.5, - ), - }), - 'context': , - 'entity_id': 'light.rgb_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_groups[input2-expected2-light_payload0][light.tunable_white_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6451, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 155, - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.tunable_white_light', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Tunable white light', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00:00:01-00', - 'unit_of_measurement': None, - }) -# --- -# name: test_groups[input2-expected2-light_payload0][light.tunable_white_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': , - 'color_temp': 2500, - 'color_temp_kelvin': 400, - 'friendly_name': 'Tunable white light', - 'hs_color': tuple( - 15.981, - 100.0, - ), - 'is_deconz_group': False, - 'max_color_temp_kelvin': 6451, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 155, - 'rgb_color': tuple( - 255, - 67, - 0, - ), - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.674, - 0.322, - ), - }), - 'context': , - 'entity_id': 'light.tunable_white_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_groups[input2-light_payload0][light.dimmable_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1823,97 +911,6 @@ 'state': 'on', }) # --- -# name: test_lights[light_payload0-expected0][light.hue_go-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'effect_list': list([ - 'colorloop', - ]), - 'max_color_temp_kelvin': 6535, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hue_go', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hue Go', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:17:88:01:01:23:45:67-00', - 'unit_of_measurement': None, - }) -# --- -# name: test_lights[light_payload0-expected0][light.hue_go-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 254, - 'color_mode': , - 'color_temp': 375, - 'color_temp_kelvin': 2666, - 'effect': None, - 'effect_list': list([ - 'colorloop', - ]), - 'friendly_name': 'Hue Go', - 'hs_color': tuple( - 28.47, - 66.821, - ), - 'is_deconz_group': False, - 'max_color_temp_kelvin': 6535, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': tuple( - 255, - 165, - 84, - ), - 'supported_color_modes': list([ - , - , - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.53, - 0.388, - ), - }), - 'context': , - 'entity_id': 'light.hue_go', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_lights[light_payload0][light.hue_go-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2005,97 +1002,6 @@ 'state': 'on', }) # --- -# name: test_lights[light_payload1-expected1][light.hue_ensis-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'effect_list': list([ - 'colorloop', - ]), - 'max_color_temp_kelvin': 7142, - 'max_mireds': 650, - 'min_color_temp_kelvin': 1538, - 'min_mireds': 140, - 'supported_color_modes': list([ - , - , - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hue_ensis', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hue Ensis', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:17:88:01:01:23:45:67-01', - 'unit_of_measurement': None, - }) -# --- -# name: test_lights[light_payload1-expected1][light.hue_ensis-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 254, - 'color_mode': , - 'color_temp': None, - 'color_temp_kelvin': None, - 'effect': None, - 'effect_list': list([ - 'colorloop', - ]), - 'friendly_name': 'Hue Ensis', - 'hs_color': tuple( - 29.691, - 38.039, - ), - 'is_deconz_group': False, - 'max_color_temp_kelvin': 7142, - 'max_mireds': 650, - 'min_color_temp_kelvin': 1538, - 'min_mireds': 140, - 'rgb_color': tuple( - 255, - 206, - 158, - ), - 'supported_color_modes': list([ - , - , - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.427, - 0.373, - ), - }), - 'context': , - 'entity_id': 'light.hue_ensis', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_lights[light_payload1][light.hue_ensis-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2187,113 +1093,6 @@ 'state': 'on', }) # --- -# name: test_lights[light_payload2-expected2][light.lidl_xmas_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'effect_list': list([ - 'carnival', - 'collide', - 'fading', - 'fireworks', - 'flag', - 'glow', - 'rainbow', - 'snake', - 'snow', - 'sparkles', - 'steady', - 'strobe', - 'twinkle', - 'updown', - 'vintage', - 'waves', - ]), - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.lidl_xmas_light', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'LIDL xmas light', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '58:8e:81:ff:fe:db:7b:be-01', - 'unit_of_measurement': None, - }) -# --- -# name: test_lights[light_payload2-expected2][light.lidl_xmas_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 25, - 'color_mode': , - 'effect': None, - 'effect_list': list([ - 'carnival', - 'collide', - 'fading', - 'fireworks', - 'flag', - 'glow', - 'rainbow', - 'snake', - 'snow', - 'sparkles', - 'steady', - 'strobe', - 'twinkle', - 'updown', - 'vintage', - 'waves', - ]), - 'friendly_name': 'LIDL xmas light', - 'hs_color': tuple( - 294.938, - 55.294, - ), - 'is_deconz_group': False, - 'rgb_color': tuple( - 243, - 113, - 255, - ), - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.357, - 0.188, - ), - }), - 'context': , - 'entity_id': 'light.lidl_xmas_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_lights[light_payload2][light.lidl_xmas_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2401,86 +1200,6 @@ 'state': 'on', }) # --- -# name: test_lights[light_payload3-expected3][light.hue_white_ambiance-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6535, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hue_white_ambiance', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hue White Ambiance', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:17:88:01:01:23:45:67-02', - 'unit_of_measurement': None, - }) -# --- -# name: test_lights[light_payload3-expected3][light.hue_white_ambiance-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 254, - 'color_mode': , - 'color_temp': 396, - 'color_temp_kelvin': 2525, - 'friendly_name': 'Hue White Ambiance', - 'hs_color': tuple( - 28.809, - 71.624, - ), - 'is_deconz_group': False, - 'max_color_temp_kelvin': 6535, - 'max_mireds': 454, - 'min_color_temp_kelvin': 2202, - 'min_mireds': 153, - 'rgb_color': tuple( - 255, - 160, - 72, - ), - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.544, - 0.389, - ), - }), - 'context': , - 'entity_id': 'light.hue_white_ambiance', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_lights[light_payload3][light.hue_white_ambiance-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2561,63 +1280,6 @@ 'state': 'on', }) # --- -# name: test_lights[light_payload4-expected4][light.hue_filament-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hue_filament', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hue Filament', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:17:88:01:01:23:45:67-03', - 'unit_of_measurement': None, - }) -# --- -# name: test_lights[light_payload4-expected4][light.hue_filament-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 254, - 'color_mode': , - 'friendly_name': 'Hue Filament', - 'is_deconz_group': False, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.hue_filament', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_lights[light_payload4][light.hue_filament-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2675,62 +1337,6 @@ 'state': 'on', }) # --- -# name: test_lights[light_payload5-expected5][light.simple_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.simple_light', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Simple Light', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:15:8d:00:01:23:45:67-01', - 'unit_of_measurement': None, - }) -# --- -# name: test_lights[light_payload5-expected5][light.simple_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'color_mode': , - 'friendly_name': 'Simple Light', - 'is_deconz_group': False, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.simple_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_lights[light_payload5][light.simple_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2787,97 +1393,6 @@ 'state': 'on', }) # --- -# name: test_lights[light_payload6-expected6][light.gradient_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'effect_list': list([ - 'colorloop', - ]), - 'max_color_temp_kelvin': 6535, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - , - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.gradient_light', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Gradient light', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:17:88:01:0b:0c:0d:0e-0f', - 'unit_of_measurement': None, - }) -# --- -# name: test_lights[light_payload6-expected6][light.gradient_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 184, - 'color_mode': , - 'color_temp': None, - 'color_temp_kelvin': None, - 'effect': None, - 'effect_list': list([ - 'colorloop', - ]), - 'friendly_name': 'Gradient light', - 'hs_color': tuple( - 98.095, - 74.118, - ), - 'is_deconz_group': False, - 'max_color_temp_kelvin': 6535, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': tuple( - 135, - 255, - 66, - ), - 'supported_color_modes': list([ - , - , - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.2727, - 0.6226, - ), - }), - 'context': , - 'entity_id': 'light.gradient_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_lights[light_payload6][light.gradient_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/deconz/snapshots/test_number.ambr b/tests/components/deconz/snapshots/test_number.ambr index 5311addc7a1..26e044e1d31 100644 --- a/tests/components/deconz/snapshots/test_number.ambr +++ b/tests/components/deconz/snapshots/test_number.ambr @@ -1,54 +1,4 @@ # serializer version: 1 -# name: test_number_entities[sensor_payload0-expected0][binary_sensor.presence_sensor-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.presence_sensor', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Presence sensor', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00:00:00-00-presence', - 'unit_of_measurement': None, - }) -# --- -# name: test_number_entities[sensor_payload0-expected0][binary_sensor.presence_sensor-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'dark': False, - 'device_class': 'motion', - 'friendly_name': 'Presence sensor', - 'on': True, - 'temperature': 0.1, - }), - 'context': , - 'entity_id': 'binary_sensor.presence_sensor', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_number_entities[sensor_payload0-expected0][number.presence_sensor_delay-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -104,56 +54,6 @@ 'state': '0', }) # --- -# name: test_number_entities[sensor_payload1-expected1][binary_sensor.presence_sensor-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.presence_sensor', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Presence sensor', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00:00:00-00-presence', - 'unit_of_measurement': None, - }) -# --- -# name: test_number_entities[sensor_payload1-expected1][binary_sensor.presence_sensor-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'dark': False, - 'device_class': 'motion', - 'friendly_name': 'Presence sensor', - 'on': True, - 'temperature': 0.1, - }), - 'context': , - 'entity_id': 'binary_sensor.presence_sensor', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_number_entities[sensor_payload1-expected1][number.presence_sensor_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/deconz/snapshots/test_sensor.ambr b/tests/components/deconz/snapshots/test_sensor.ambr index 7f12292abbd..dd097ea1c9a 100644 --- a/tests/components/deconz/snapshots/test_sensor.ambr +++ b/tests/components/deconz/snapshots/test_sensor.ambr @@ -548,53 +548,6 @@ 'state': '100', }) # --- -# name: test_sensors[config_entry_options0-sensor_payload12-expected12][binary_sensor.soil_sensor_low_battery-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.soil_sensor_low_battery', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Soil Sensor Low Battery', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'a4:c1:38:fe:86:8f:07:a3-01-0408-low_battery', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[config_entry_options0-sensor_payload12-expected12][binary_sensor.soil_sensor_low_battery-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'battery', - 'friendly_name': 'Soil Sensor Low Battery', - }), - 'context': , - 'entity_id': 'binary_sensor.soil_sensor_low_battery', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_sensors[config_entry_options0-sensor_payload12-expected12][sensor.soil_sensor-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1224,55 +1177,6 @@ 'state': '40', }) # --- -# name: test_sensors[config_entry_options0-sensor_payload19-expected19][binary_sensor.alarm_10-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.alarm_10', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Alarm 10', - 'platform': 'deconz', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:15:8d:00:02:b5:d1:80-01-0500-alarm', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[config_entry_options0-sensor_payload19-expected19][binary_sensor.alarm_10-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'safety', - 'friendly_name': 'Alarm 10', - 'on': True, - 'temperature': 26.0, - }), - 'context': , - 'entity_id': 'binary_sensor.alarm_10', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_sensors[config_entry_options0-sensor_payload19-expected19][sensor.alarm_10_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/google_tasks/snapshots/test_todo.ambr b/tests/components/google_tasks/snapshots/test_todo.ambr index af8dec6a182..76611ba4a31 100644 --- a/tests/components/google_tasks/snapshots/test_todo.ambr +++ b/tests/components/google_tasks/snapshots/test_todo.ambr @@ -79,9 +79,6 @@ }), ]) # --- -# name: test_move_todo_item[api_responses0].4 - None -# --- # name: test_parent_child_ordering[api_responses0] list([ dict({ diff --git a/tests/components/homeassistant/snapshots/test_exposed_entities.ambr b/tests/components/homeassistant/snapshots/test_exposed_entities.ambr index 55b95186b49..9c93655cd4e 100644 --- a/tests/components/homeassistant/snapshots/test_exposed_entities.ambr +++ b/tests/components/homeassistant/snapshots/test_exposed_entities.ambr @@ -13,13 +13,3 @@ dict({ }) # --- -# name: test_listeners - dict({ - 'light.kitchen': dict({ - 'should_expose': True, - }), - 'switch.test_unique1': mappingproxy({ - 'should_expose': True, - }), - }) -# --- diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 2e96295a0ab..078ef792a55 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -7010,324 +7010,6 @@ }), ]) # --- -# name: test_snapshots[haa_fan] - list([ - dict({ - 'device': dict({ - 'area_id': None, - 'config_entries': list([ - 'TestData', - ]), - 'configuration_url': None, - 'connections': list([ - ]), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': '', - 'identifiers': list([ - list([ - 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:1', - ]), - ]), - 'is_new': False, - 'labels': list([ - ]), - 'manufacturer': 'José A. Jiménez Campos', - 'model': 'RavenSystem HAA', - 'name': 'HAA-C718B3', - 'name_by_user': None, - 'serial_number': 'C718B3-1', - 'suggested_area': None, - 'sw_version': '5.0.18', - }), - 'entities': list([ - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.haa_c718b3_identify', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'HAA-C718B3 Identify', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_1_1_7', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'identify', - 'friendly_name': 'HAA-C718B3 Identify', - }), - 'entity_id': 'button.haa_c718b3_identify', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.haa_c718b3_setup', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'HAA-C718B3 Setup', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'setup', - 'unique_id': '00:00:00:00:00:00_1_1010_1012', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'friendly_name': 'HAA-C718B3 Setup', - }), - 'entity_id': 'button.haa_c718b3_setup', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.haa_c718b3_update', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'HAA-C718B3 Update', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_1_1010_1011', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'update', - 'friendly_name': 'HAA-C718B3 Update', - }), - 'entity_id': 'button.haa_c718b3_update', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': dict({ - 'preset_modes': None, - }), - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'fan', - 'entity_category': None, - 'entity_id': 'fan.haa_c718b3', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'HAA-C718B3', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_1_8', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'friendly_name': 'HAA-C718B3', - 'percentage': 66, - 'percentage_step': 33.333333333333336, - 'preset_mode': None, - 'preset_modes': None, - 'supported_features': , - }), - 'entity_id': 'fan.haa_c718b3', - 'state': 'on', - }), - }), - ]), - }), - dict({ - 'device': dict({ - 'area_id': None, - 'config_entries': list([ - 'TestData', - ]), - 'configuration_url': None, - 'connections': list([ - ]), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': '', - 'identifiers': list([ - list([ - 'homekit_controller:accessory-id', - '00:00:00:00:00:00:aid:2', - ]), - ]), - 'is_new': False, - 'labels': list([ - ]), - 'manufacturer': 'José A. Jiménez Campos', - 'model': 'RavenSystem HAA', - 'name': 'HAA-C718B3', - 'name_by_user': None, - 'serial_number': 'C718B3-2', - 'suggested_area': None, - 'sw_version': '5.0.18', - }), - 'entities': list([ - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.haa_c718b3_identify_2', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'HAA-C718B3 Identify', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_2_1_7', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'identify', - 'friendly_name': 'HAA-C718B3 Identify', - }), - 'entity_id': 'button.haa_c718b3_identify_2', - 'state': 'unknown', - }), - }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'categories': dict({ - }), - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.haa_c718b3', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'HAA-C718B3', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_2_8', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'friendly_name': 'HAA-C718B3', - }), - 'entity_id': 'switch.haa_c718b3', - 'state': 'off', - }), - }), - ]), - }), - ]) -# --- # name: test_snapshots[home_assistant_bridge_basic_cover] list([ dict({ diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index c727a49b71a..c260e6beba6 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -551,64 +551,6 @@ 'state': '2023-06-05T17:00:00+00:00', }) # --- -# name: test_sensor_snapshot[sensor.test_mower_1_none-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'Front lawn', - 'Back lawn', - 'my_lawn', - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_mower_1_none', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'husqvarna_automower', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'work_area', - 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_work_area', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor_snapshot[sensor.test_mower_1_none-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Test Mower 1 None', - 'options': list([ - 'Front lawn', - 'Back lawn', - 'my_lawn', - ]), - }), - 'context': , - 'entity_id': 'sensor.test_mower_1_none', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'Front lawn', - }) -# --- # name: test_sensor_snapshot[sensor.test_mower_1_number_of_charging_cycles-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/imgw_pib/snapshots/test_binary_sensor.ambr b/tests/components/imgw_pib/snapshots/test_binary_sensor.ambr index f314a4be590..c5ae6880022 100644 --- a/tests/components/imgw_pib/snapshots/test_binary_sensor.ambr +++ b/tests/components/imgw_pib/snapshots/test_binary_sensor.ambr @@ -95,101 +95,3 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[binary_sensor.station_name_flood_alarm-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.station_name_flood_alarm', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Flood alarm', - 'platform': 'imgw_pib', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'flood_alarm', - 'unique_id': '123_flood_alarm', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[binary_sensor.station_name_flood_alarm-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'alarm_level': 630.0, - 'attribution': 'Data provided by IMGW-PIB', - 'device_class': 'safety', - 'friendly_name': 'Station Name Flood alarm', - }), - 'context': , - 'entity_id': 'binary_sensor.station_name_flood_alarm', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[binary_sensor.station_name_flood_warning-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.station_name_flood_warning', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Flood warning', - 'platform': 'imgw_pib', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'flood_warning', - 'unique_id': '123_flood_warning', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[binary_sensor.station_name_flood_warning-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by IMGW-PIB', - 'device_class': 'safety', - 'friendly_name': 'Station Name Flood warning', - 'warning_level': 590.0, - }), - 'context': , - 'entity_id': 'binary_sensor.station_name_flood_warning', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- diff --git a/tests/components/imgw_pib/snapshots/test_sensor.ambr b/tests/components/imgw_pib/snapshots/test_sensor.ambr index 2638e468d92..6c69b890842 100644 --- a/tests/components/imgw_pib/snapshots/test_sensor.ambr +++ b/tests/components/imgw_pib/snapshots/test_sensor.ambr @@ -213,113 +213,3 @@ 'state': '10.8', }) # --- -# name: test_sensor[sensor.station_name_water_level-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.station_name_water_level', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Water level', - 'platform': 'imgw_pib', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'water_level', - 'unique_id': '123_water_level', - 'unit_of_measurement': , - }) -# --- -# name: test_sensor[sensor.station_name_water_level-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by IMGW-PIB', - 'device_class': 'distance', - 'friendly_name': 'Station Name Water level', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.station_name_water_level', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '526.0', - }) -# --- -# name: test_sensor[sensor.station_name_water_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.station_name_water_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Water temperature', - 'platform': 'imgw_pib', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'water_temperature', - 'unique_id': '123_water_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_sensor[sensor.station_name_water_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by IMGW-PIB', - 'device_class': 'temperature', - 'friendly_name': 'Station Name Water temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.station_name_water_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10.8', - }) -# --- diff --git a/tests/components/ipma/snapshots/test_weather.ambr b/tests/components/ipma/snapshots/test_weather.ambr index 1142cb7cfe5..80f385546d1 100644 --- a/tests/components/ipma/snapshots/test_weather.ambr +++ b/tests/components/ipma/snapshots/test_weather.ambr @@ -1,119 +1,4 @@ # serializer version: 1 -# name: test_forecast_service - dict({ - 'forecast': list([ - dict({ - 'condition': 'rainy', - 'datetime': datetime.datetime(2020, 1, 16, 0, 0), - 'precipitation_probability': '100.0', - 'temperature': 16.2, - 'templow': 10.6, - 'wind_bearing': 'S', - 'wind_speed': 10.0, - }), - ]), - }) -# --- -# name: test_forecast_service.1 - dict({ - 'forecast': list([ - dict({ - 'condition': 'rainy', - 'datetime': datetime.datetime(2020, 1, 15, 1, 0, tzinfo=datetime.timezone.utc), - 'precipitation_probability': 80.0, - 'temperature': 12.0, - 'wind_bearing': 'S', - 'wind_speed': 32.7, - }), - dict({ - 'condition': 'clear-night', - 'datetime': datetime.datetime(2020, 1, 15, 2, 0, tzinfo=datetime.timezone.utc), - 'precipitation_probability': 80.0, - 'temperature': 12.0, - 'wind_bearing': 'S', - 'wind_speed': 32.7, - }), - ]), - }) -# --- -# name: test_forecast_service[forecast] - dict({ - 'weather.hometown': dict({ - 'forecast': list([ - dict({ - 'condition': 'rainy', - 'datetime': datetime.datetime(2020, 1, 16, 0, 0), - 'precipitation_probability': '100.0', - 'temperature': 16.2, - 'templow': 10.6, - 'wind_bearing': 'S', - 'wind_speed': 10.0, - }), - ]), - }), - }) -# --- -# name: test_forecast_service[forecast].1 - dict({ - 'weather.hometown': dict({ - 'forecast': list([ - dict({ - 'condition': 'rainy', - 'datetime': datetime.datetime(2020, 1, 15, 1, 0, tzinfo=datetime.timezone.utc), - 'precipitation_probability': 80.0, - 'temperature': 12.0, - 'wind_bearing': 'S', - 'wind_speed': 32.7, - }), - dict({ - 'condition': 'clear-night', - 'datetime': datetime.datetime(2020, 1, 15, 2, 0, tzinfo=datetime.timezone.utc), - 'precipitation_probability': 80.0, - 'temperature': 12.0, - 'wind_bearing': 'S', - 'wind_speed': 32.7, - }), - ]), - }), - }) -# --- -# name: test_forecast_service[get_forecast] - dict({ - 'forecast': list([ - dict({ - 'condition': 'rainy', - 'datetime': datetime.datetime(2020, 1, 16, 0, 0), - 'precipitation_probability': 100.0, - 'temperature': 16.2, - 'templow': 10.6, - 'wind_bearing': 'S', - 'wind_speed': 10.0, - }), - ]), - }) -# --- -# name: test_forecast_service[get_forecast].1 - dict({ - 'forecast': list([ - dict({ - 'condition': 'rainy', - 'datetime': datetime.datetime(2020, 1, 15, 1, 0, tzinfo=datetime.timezone.utc), - 'precipitation_probability': 80.0, - 'temperature': 12.0, - 'wind_bearing': 'S', - 'wind_speed': 32.7, - }), - dict({ - 'condition': 'clear-night', - 'datetime': datetime.datetime(2020, 1, 15, 2, 0, tzinfo=datetime.timezone.utc), - 'precipitation_probability': 80.0, - 'temperature': 12.0, - 'wind_bearing': 'S', - 'wind_speed': 32.7, - }), - ]), - }) -# --- # name: test_forecast_service[get_forecasts] dict({ 'weather.hometown': dict({ diff --git a/tests/components/israel_rail/snapshots/test_sensor.ambr b/tests/components/israel_rail/snapshots/test_sensor.ambr index 9806ecb1fae..f851f1cd726 100644 --- a/tests/components/israel_rail/snapshots/test_sensor.ambr +++ b/tests/components/israel_rail/snapshots/test_sensor.ambr @@ -143,147 +143,6 @@ 'state': '2021-10-10T10:30:10+00:00', }) # --- -# name: test_valid_config[sensor.mock_title_none-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.mock_title_none', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'israel_rail', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'platform', - 'unique_id': 'באר יעקב אשקלון_platform', - 'unit_of_measurement': None, - }) -# --- -# name: test_valid_config[sensor.mock_title_none-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Israel rail.', - 'friendly_name': 'Mock Title None', - }), - 'context': , - 'entity_id': 'sensor.mock_title_none', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1', - }) -# --- -# name: test_valid_config[sensor.mock_title_none_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.mock_title_none_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'israel_rail', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'trains', - 'unique_id': 'באר יעקב אשקלון_trains', - 'unit_of_measurement': None, - }) -# --- -# name: test_valid_config[sensor.mock_title_none_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Israel rail.', - 'friendly_name': 'Mock Title None', - }), - 'context': , - 'entity_id': 'sensor.mock_title_none_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1', - }) -# --- -# name: test_valid_config[sensor.mock_title_none_3-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.mock_title_none_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'israel_rail', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'train_number', - 'unique_id': 'באר יעקב אשקלון_train_number', - 'unit_of_measurement': None, - }) -# --- -# name: test_valid_config[sensor.mock_title_none_3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Israel rail.', - 'friendly_name': 'Mock Title None', - }), - 'context': , - 'entity_id': 'sensor.mock_title_none_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1234', - }) -# --- # name: test_valid_config[sensor.mock_title_platform-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -331,150 +190,6 @@ 'state': '1', }) # --- -# name: test_valid_config[sensor.mock_title_timestamp-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.mock_title_timestamp', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Timestamp', - 'platform': 'israel_rail', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'departure0', - 'unique_id': 'באר יעקב אשקלון_departure', - 'unit_of_measurement': None, - }) -# --- -# name: test_valid_config[sensor.mock_title_timestamp-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Israel rail.', - 'device_class': 'timestamp', - 'friendly_name': 'Mock Title Timestamp', - }), - 'context': , - 'entity_id': 'sensor.mock_title_timestamp', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2021-10-10T10:10:10+00:00', - }) -# --- -# name: test_valid_config[sensor.mock_title_timestamp_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.mock_title_timestamp_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Timestamp', - 'platform': 'israel_rail', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'departure1', - 'unique_id': 'באר יעקב אשקלון_departure1', - 'unit_of_measurement': None, - }) -# --- -# name: test_valid_config[sensor.mock_title_timestamp_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Israel rail.', - 'device_class': 'timestamp', - 'friendly_name': 'Mock Title Timestamp', - }), - 'context': , - 'entity_id': 'sensor.mock_title_timestamp_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2021-10-10T10:20:10+00:00', - }) -# --- -# name: test_valid_config[sensor.mock_title_timestamp_3-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.mock_title_timestamp_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Timestamp', - 'platform': 'israel_rail', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'departure2', - 'unique_id': 'באר יעקב אשקלון_departure2', - 'unit_of_measurement': None, - }) -# --- -# name: test_valid_config[sensor.mock_title_timestamp_3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Israel rail.', - 'device_class': 'timestamp', - 'friendly_name': 'Mock Title Timestamp', - }), - 'context': , - 'entity_id': 'sensor.mock_title_timestamp_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2021-10-10T10:30:10+00:00', - }) -# --- # name: test_valid_config[sensor.mock_title_train_number-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/ista_ecotrend/snapshots/test_sensor.ambr b/tests/components/ista_ecotrend/snapshots/test_sensor.ambr index f9ab7a54b63..b5056019c74 100644 --- a/tests/components/ista_ecotrend/snapshots/test_sensor.ambr +++ b/tests/components/ista_ecotrend/snapshots/test_sensor.ambr @@ -1,64 +1,4 @@ # serializer version: 1 -# name: test_setup.32 - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': 'https://ecotrend.ista.de/', - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': , - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'ista_ecotrend', - '26e93f1a-c828-11ea-87d0-0242ac130003', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'ista SE', - 'model': 'ista EcoTrend', - 'name': 'Luxemburger Str. 1', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }) -# --- -# name: test_setup.33 - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': 'https://ecotrend.ista.de/', - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': , - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'ista_ecotrend', - 'eaf5c5c8-889f-4a3c-b68c-e9a676505762', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'ista SE', - 'model': 'ista EcoTrend', - 'name': 'Bahnhofsstr. 1A', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }) -# --- # name: test_setup[sensor.bahnhofsstr_1a_heating-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/mastodon/snapshots/test_init.ambr b/tests/components/mastodon/snapshots/test_init.ambr deleted file mode 100644 index f0b650076be..00000000000 --- a/tests/components/mastodon/snapshots/test_init.ambr +++ /dev/null @@ -1,33 +0,0 @@ -# serializer version: 1 -# name: test_device_info - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': , - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'mastodon', - 'client_id', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Mastodon gGmbH', - 'model': '@trwnh@mastodon.social', - 'model_id': None, - 'name': 'Mastodon', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.0.0rc1', - 'via_device_id': None, - }) -# --- diff --git a/tests/components/mealie/snapshots/test_todo.ambr b/tests/components/mealie/snapshots/test_todo.ambr index a580862535e..4c58a839f57 100644 --- a/tests/components/mealie/snapshots/test_todo.ambr +++ b/tests/components/mealie/snapshots/test_todo.ambr @@ -140,17 +140,3 @@ 'state': '3', }) # --- -# name: test_get_todo_list_items - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mealie Supermarket', - 'supported_features': , - }), - 'context': , - 'entity_id': 'todo.mealie_supermarket', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3', - }) -# --- diff --git a/tests/components/media_extractor/snapshots/test_init.ambr b/tests/components/media_extractor/snapshots/test_init.ambr index ed56f40af73..9731a415c00 100644 --- a/tests/components/media_extractor/snapshots/test_init.ambr +++ b/tests/components/media_extractor/snapshots/test_init.ambr @@ -30,15 +30,6 @@ 'media_content_type': 'VIDEO', }) # --- -# name: test_play_media_service - ReadOnlyDict({ - 'entity_id': 'media_player.bedroom', - 'extra': dict({ - }), - 'media_content_id': 'https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1694794256/ei/sC0EZYCPHbuZx_AP3bGz0Ac/ip/84.31.234.146/id/750c38c3d5a05dc4/itag/616/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D99471214%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D356%3Blmt%3D1694043438471036/hls_chunk_host/rr2---sn-5hnekn7k.googlevideo.com/mh/7c/mm/31,29/mn/sn-5hnekn7k,sn-5hne6nzy/ms/au,rdu/mv/m/mvi/2/pl/14/initcwndbps/2267500/vprv/1/playlist_type/DVR/dover/13/txp/4532434/mt/1694772337/fvip/3/short_key/1/keepalive/yes/fexp/24007246,24362685/beids/24350018/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRgIhAIC0iobMnRschmQ3QaYsytXg9eg7l9B_-UNvMciis4bmAiEAg-3jr6SwOfAGCCU-JyTyxcXmraug-hPcjjJzm__43ug%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIhAOlqbgmuueNhIuGENYKCsdwiNAUPheXw-RMUqsiaB7YuAiANN43FxJl14Ve_H_c9K-aDoXG4sI7PDCqKDhov6Qro_g%3D%3D/playlist/index.m3u8', - 'media_content_type': 'VIDEO', - }) -# --- # name: test_play_media_service[https://soundcloud.com/bruttoband/brutto-11-AUDIO-audio_media_extractor_config] ReadOnlyDict({ 'entity_id': 'media_player.bedroom', @@ -57,24 +48,6 @@ 'media_content_type': 'AUDIO', }) # --- -# name: test_play_media_service[https://soundcloud.com/bruttoband/brutto-11-VIDEO-audio_media_extractor_config] - ReadOnlyDict({ - 'entity_id': 'media_player.bedroom', - 'extra': dict({ - }), - 'media_content_id': 'https://cf-media.sndcdn.com/50remGX1OqRY.128.mp3?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLW1lZGlhLnNuZGNkbi5jb20vNTByZW1HWDFPcVJZLjEyOC5tcDMqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNjk0Nzk5MTc5fX19XX0_&Signature=JtF8BXxTCElhjCrhnSAq3W6z960VmdVXx7BPhQvI0MCxr~J43JFGO8CVw9-VBM2oEf14mqWo63-C0FO29DvUuBZnmLD3dhDfryVfWJsrix7voimoRDaNFE~3zntDbg7O2S8uWYyZK8OZC9anzwokvjH7jbmviWqK4~2IM9dwgejGgzrQU1aadV2Yro7NJZnF7SD~7tVjkM-hBg~X5zDYVxmGrdzN3tFoLwRmUch6RNDL~1DcWBk0AveBKQFAdBrFBjDDUeIyDz9Idhw2aG9~fjfckcf95KwqrVQxz1N5XEzfNDDo8xkUgDt0eb9dtXdwxLJ0swC6e5VLS8bsH91GMg__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ', - 'media_content_type': 'VIDEO', - }) -# --- -# name: test_play_media_service[https://soundcloud.com/bruttoband/brutto-11-VIDEO-empty_media_extractor_config] - ReadOnlyDict({ - 'entity_id': 'media_player.bedroom', - 'extra': dict({ - }), - 'media_content_id': 'https://cf-media.sndcdn.com/50remGX1OqRY.128.mp3?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLW1lZGlhLnNuZGNkbi5jb20vNTByZW1HWDFPcVJZLjEyOC5tcDMqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNjk0Nzk4NTkzfX19XX0_&Signature=flALJvEBnzS0ZOOhf0-07Ap~NURw2Gn2OqkeKKTTMX5HRGJw9eXFay79tcC4GsMMXWUgWoCx-n3yelpyilE2MOEIufBNUbjqRfMSJaX5YhYxjQdoDYuiU~gqBzJyPw9pKzr6P8~5HNKL3Idr0CNhUzdV6FQLaUPKMMibq9ghV833mUmdyvdk1~GZBc8MOg9GrTdcigGgpPzd-vrIMICMvFzFnwBOeOotxX2Vfqf9~wVekBKGlvB9A~7TlZ71lv9Fl9u4m8rse9E-mByweVc1M784ehJV3~tRPjuF~FXXWKP8x0nGJmoq7RAnG7iFIt~fQFmsfOq2o~PG7dHMRPh7hw__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ', - 'media_content_type': 'VIDEO', - }) -# --- # name: test_play_media_service[https://test.com/abc-AUDIO-audio_media_extractor_config] ReadOnlyDict({ 'entity_id': 'media_player.bedroom', @@ -93,15 +66,6 @@ 'media_content_type': 'AUDIO', }) # --- -# name: test_play_media_service[https://www.youtube.com/watch?v=dQw4w9WgXcQ-VIDEO-audio_media_extractor_config-] - ReadOnlyDict({ - 'entity_id': 'media_player.bedroom', - 'extra': dict({ - }), - 'media_content_id': 'https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1694805268/ei/tFgEZcu0DoOD-gaqg47wBA/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/616/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D99471214%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D356%3Blmt%3D1694043438471036/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,29/mn/sn-5hne6nzy,sn-5hnekn7k/ms/au,rdu/mv/m/mvi/3/pl/22/initcwndbps/1957500/vprv/1/playlist_type/DVR/dover/13/txp/4532434/mt/1694783146/fvip/2/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIhALAASH0_ZDQQoMA82qWNCXSHPZ0bb9TQldIs7AAxktiiAiASA5bQy7IAa6NwdGIOpfye5OgcY_BNuo0WgSdh84tosw%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRgIhAIsDcLGH8KJpQpBgyJ5VWlDxfr75HyO8hMSVS9v7nRu4AiEA2xjtLZOzeNFoJlxwCsH3YqsUQt-BF_4gikhi_P4FbBc%3D/playlist/index.m3u8', - 'media_content_type': 'VIDEO', - }) -# --- # name: test_play_media_service[https://www.youtube.com/watch?v=dQw4w9WgXcQ-VIDEO-audio_media_extractor_config] ReadOnlyDict({ 'entity_id': 'media_player.bedroom', @@ -111,15 +75,6 @@ 'media_content_type': 'VIDEO', }) # --- -# name: test_play_media_service[https://www.youtube.com/watch?v=dQw4w9WgXcQ-VIDEO-empty_media_extractor_config-] - ReadOnlyDict({ - 'entity_id': 'media_player.bedroom', - 'extra': dict({ - }), - 'media_content_id': 'https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1694805294/ei/zlgEZcCPFpqOx_APj42f2Ao/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/616/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D99471214%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D356%3Blmt%3D1694043438471036/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-aigzrnld/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/2095000/vprv/1/playlist_type/DVR/dover/13/txp/4532434/mt/1694783390/fvip/1/short_key/1/keepalive/yes/fexp/24007246,24362685/beids/24350017/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRgIhANCPwWNfq6wBp1Xo1L8bRJpDrzOyv7kfH_J65cZ_PRZLAiEAwo-0wQgeIjPe7OgyAAvMCx_A9wd1h8Qyh7VntKwGJUs%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIgIqS9Ub_6L9ScKXr0T9bkeu6TZsEsyNApYfF_MqeukqECIQCMSeJ1sSEw5QGMgHAW8Fhsir4TYHEK5KVg-PzJbrT6hw%3D%3D/playlist/index.m3u8', - 'media_content_type': 'VIDEO', - }) -# --- # name: test_play_media_service[https://www.youtube.com/watch?v=dQw4w9WgXcQ-VIDEO-empty_media_extractor_config] ReadOnlyDict({ 'entity_id': 'media_player.bedroom', diff --git a/tests/components/met_eireann/snapshots/test_weather.ambr b/tests/components/met_eireann/snapshots/test_weather.ambr index 90f36d09d25..de8b69de18a 100644 --- a/tests/components/met_eireann/snapshots/test_weather.ambr +++ b/tests/components/met_eireann/snapshots/test_weather.ambr @@ -1,104 +1,4 @@ # serializer version: 1 -# name: test_forecast_service - dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2023-08-08T12:00:00+00:00', - 'temperature': 10.0, - }), - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2023-08-09T12:00:00+00:00', - 'temperature': 20.0, - }), - ]), - }) -# --- -# name: test_forecast_service.1 - dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2023-08-08T12:00:00+00:00', - 'temperature': 10.0, - }), - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2023-08-09T12:00:00+00:00', - 'temperature': 20.0, - }), - ]), - }) -# --- -# name: test_forecast_service[forecast] - dict({ - 'weather.somewhere': dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2023-08-08T12:00:00+00:00', - 'temperature': 10.0, - }), - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2023-08-09T12:00:00+00:00', - 'temperature': 20.0, - }), - ]), - }), - }) -# --- -# name: test_forecast_service[forecast].1 - dict({ - 'weather.somewhere': dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2023-08-08T12:00:00+00:00', - 'temperature': 10.0, - }), - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2023-08-09T12:00:00+00:00', - 'temperature': 20.0, - }), - ]), - }), - }) -# --- -# name: test_forecast_service[get_forecast] - dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2023-08-08T12:00:00+00:00', - 'temperature': 10.0, - }), - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2023-08-09T12:00:00+00:00', - 'temperature': 20.0, - }), - ]), - }) -# --- -# name: test_forecast_service[get_forecast].1 - dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2023-08-08T12:00:00+00:00', - 'temperature': 10.0, - }), - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2023-08-09T12:00:00+00:00', - 'temperature': 20.0, - }), - ]), - }) -# --- # name: test_forecast_service[get_forecasts] dict({ 'weather.somewhere': dict({ diff --git a/tests/components/metoffice/snapshots/test_weather.ambr b/tests/components/metoffice/snapshots/test_weather.ambr index a6991a8631b..0bbc0e06a0a 100644 --- a/tests/components/metoffice/snapshots/test_weather.ambr +++ b/tests/components/metoffice/snapshots/test_weather.ambr @@ -1,658 +1,4 @@ # serializer version: 1 -# name: test_forecast_service[get_forecast] - dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 13.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - ]), - }) -# --- -# name: test_forecast_service[get_forecast].1 - dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-25T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 19.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T18:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 17.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 14.0, - 'wind_bearing': 'NW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T00:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 13.0, - 'wind_bearing': 'WSW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T03:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T09:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T15:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T18:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T00:00:00+00:00', - 'precipitation_probability': 11, - 'temperature': 9.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T03:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 8.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T06:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 8.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 4, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T18:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-27T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T00:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 8.0, - 'wind_bearing': 'NNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 7.0, - 'wind_bearing': 'W', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-28T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 6.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-28T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T15:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T18:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NNE', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T00:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'E', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-29T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 8.0, - 'wind_bearing': 'SSE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T06:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 8.0, - 'wind_bearing': 'SE', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T09:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 10.0, - 'wind_bearing': 'SE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 47, - 'temperature': 12.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'pouring', - 'datetime': '2020-04-29T15:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T18:00:00+00:00', - 'precipitation_probability': 39, - 'temperature': 12.0, - 'wind_bearing': 'SSE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T21:00:00+00:00', - 'precipitation_probability': 19, - 'temperature': 11.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - ]), - }) -# --- -# name: test_forecast_service[get_forecast].2 - dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 13.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - ]), - }) -# --- -# name: test_forecast_service[get_forecast].3 - dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-25T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 19.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T18:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 17.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 14.0, - 'wind_bearing': 'NW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T00:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 13.0, - 'wind_bearing': 'WSW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T03:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T09:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T15:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T18:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T00:00:00+00:00', - 'precipitation_probability': 11, - 'temperature': 9.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T03:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 8.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T06:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 8.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 4, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T18:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-27T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T00:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 8.0, - 'wind_bearing': 'NNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 7.0, - 'wind_bearing': 'W', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-28T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 6.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-28T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T15:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T18:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NNE', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T00:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'E', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-29T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 8.0, - 'wind_bearing': 'SSE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T06:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 8.0, - 'wind_bearing': 'SE', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T09:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 10.0, - 'wind_bearing': 'SE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 47, - 'temperature': 12.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'pouring', - 'datetime': '2020-04-29T15:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T18:00:00+00:00', - 'precipitation_probability': 39, - 'temperature': 12.0, - 'wind_bearing': 'SSE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T21:00:00+00:00', - 'precipitation_probability': 19, - 'temperature': 11.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - ]), - }) -# --- -# name: test_forecast_service[get_forecast].4 - dict({ - 'forecast': list([ - ]), - }) -# --- # name: test_forecast_service[get_forecasts] dict({ 'weather.met_office_wavertree_daily': dict({ diff --git a/tests/components/nam/snapshots/test_sensor.ambr b/tests/components/nam/snapshots/test_sensor.ambr index 426b2ff2e03..16129c5d7ce 100644 --- a/tests/components/nam/snapshots/test_sensor.ambr +++ b/tests/components/nam/snapshots/test_sensor.ambr @@ -1,51 +1,4 @@ # serializer version: 1 -# name: test_sensor[button.nettigo_air_monitor_restart-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.nettigo_air_monitor_restart', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Restart', - 'platform': 'nam', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'aa:bb:cc:dd:ee:ff-restart', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[button.nettigo_air_monitor_restart-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'restart', - 'friendly_name': 'Nettigo Air Monitor Restart', - }), - 'context': , - 'entity_id': 'button.nettigo_air_monitor_restart', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_sensor[sensor.nettigo_air_monitor_bme280_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/nextcloud/snapshots/test_config_flow.ambr b/tests/components/nextcloud/snapshots/test_config_flow.ambr index 3334478ba24..06c4ce216db 100644 --- a/tests/components/nextcloud/snapshots/test_config_flow.ambr +++ b/tests/components/nextcloud/snapshots/test_config_flow.ambr @@ -1,12 +1,4 @@ # serializer version: 1 -# name: test_import - dict({ - 'password': 'nc_pass', - 'url': 'nc_url', - 'username': 'nc_user', - 'verify_ssl': True, - }) -# --- # name: test_reauth dict({ 'password': 'other_password', diff --git a/tests/components/nextdns/snapshots/test_binary_sensor.ambr b/tests/components/nextdns/snapshots/test_binary_sensor.ambr index bd4ecbba084..814b4c1ac16 100644 --- a/tests/components/nextdns/snapshots/test_binary_sensor.ambr +++ b/tests/components/nextdns/snapshots/test_binary_sensor.ambr @@ -1,1095 +1,4 @@ # serializer version: 1 -# name: test_binary_Sensor[switch.fake_profile_ai_driven_threat_detection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_ai_driven_threat_detection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'AI-Driven threat detection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'ai_threat_detection', - 'unique_id': 'xyz12_ai_threat_detection', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_ai_driven_threat_detection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile AI-Driven threat detection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_ai_driven_threat_detection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_allow_affiliate_tracking_links-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_allow_affiliate_tracking_links', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Allow affiliate & tracking links', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'allow_affiliate', - 'unique_id': 'xyz12_allow_affiliate', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_allow_affiliate_tracking_links-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Allow affiliate & tracking links', - }), - 'context': , - 'entity_id': 'switch.fake_profile_allow_affiliate_tracking_links', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_anonymized_edns_client_subnet-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_anonymized_edns_client_subnet', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Anonymized EDNS client subnet', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'anonymized_ecs', - 'unique_id': 'xyz12_anonymized_ecs', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_anonymized_edns_client_subnet-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Anonymized EDNS client subnet', - }), - 'context': , - 'entity_id': 'switch.fake_profile_anonymized_edns_client_subnet', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_9gag-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_9gag', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block 9GAG', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_9gag', - 'unique_id': 'xyz12_block_9gag', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_bypass_methods-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_bypass_methods', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block bypass methods', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_bypass_methods', - 'unique_id': 'xyz12_block_bypass_methods', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_bypass_methods-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block bypass methods', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_bypass_methods', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_child_sexual_abuse_material-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_child_sexual_abuse_material', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block child sexual abuse material', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_csam', - 'unique_id': 'xyz12_block_csam', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_child_sexual_abuse_material-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block child sexual abuse material', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_child_sexual_abuse_material', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_disguised_third_party_trackers-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_disguised_third_party_trackers', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block disguised third-party trackers', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_disguised_trackers', - 'unique_id': 'xyz12_block_disguised_trackers', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_disguised_third_party_trackers-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block disguised third-party trackers', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_disguised_third_party_trackers', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_dynamic_dns_hostnames-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_dynamic_dns_hostnames', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block dynamic DNS hostnames', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_ddns', - 'unique_id': 'xyz12_block_ddns', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_dynamic_dns_hostnames-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block dynamic DNS hostnames', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_dynamic_dns_hostnames', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_newly_registered_domains-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_newly_registered_domains', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block newly registered domains', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_nrd', - 'unique_id': 'xyz12_block_nrd', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_newly_registered_domains-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block newly registered domains', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_newly_registered_domains', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_page-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_page', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block page', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_page', - 'unique_id': 'xyz12_block_page', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_page-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block page', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_page', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_parked_domains-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_parked_domains', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block parked domains', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_parked_domains', - 'unique_id': 'xyz12_block_parked_domains', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_block_parked_domains-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block parked domains', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_parked_domains', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_cache_boost-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_cache_boost', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Cache boost', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cache_boost', - 'unique_id': 'xyz12_cache_boost', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_cache_boost-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Cache boost', - }), - 'context': , - 'entity_id': 'switch.fake_profile_cache_boost', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_cname_flattening-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_cname_flattening', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'CNAME flattening', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cname_flattening', - 'unique_id': 'xyz12_cname_flattening', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_cname_flattening-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile CNAME flattening', - }), - 'context': , - 'entity_id': 'switch.fake_profile_cname_flattening', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_cryptojacking_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_cryptojacking_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Cryptojacking protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cryptojacking_protection', - 'unique_id': 'xyz12_cryptojacking_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_cryptojacking_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Cryptojacking protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_cryptojacking_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_dns_rebinding_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_dns_rebinding_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS rebinding protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'dns_rebinding_protection', - 'unique_id': 'xyz12_dns_rebinding_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_dns_rebinding_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS rebinding protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_dns_rebinding_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_domain_generation_algorithms_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_domain_generation_algorithms_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Domain generation algorithms protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'dga_protection', - 'unique_id': 'xyz12_dga_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_domain_generation_algorithms_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Domain generation algorithms protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_domain_generation_algorithms_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_force_safesearch-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_force_safesearch', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Force SafeSearch', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'safesearch', - 'unique_id': 'xyz12_safesearch', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_force_safesearch-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Force SafeSearch', - }), - 'context': , - 'entity_id': 'switch.fake_profile_force_safesearch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_force_youtube_restricted_mode-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_force_youtube_restricted_mode', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Force YouTube restricted mode', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'youtube_restricted_mode', - 'unique_id': 'xyz12_youtube_restricted_mode', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_force_youtube_restricted_mode-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Force YouTube restricted mode', - }), - 'context': , - 'entity_id': 'switch.fake_profile_force_youtube_restricted_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_google_safe_browsing-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_google_safe_browsing', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Google safe browsing', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'google_safe_browsing', - 'unique_id': 'xyz12_google_safe_browsing', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_google_safe_browsing-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Google safe browsing', - }), - 'context': , - 'entity_id': 'switch.fake_profile_google_safe_browsing', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_idn_homograph_attacks_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_idn_homograph_attacks_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'IDN homograph attacks protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'idn_homograph_attacks_protection', - 'unique_id': 'xyz12_idn_homograph_attacks_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_idn_homograph_attacks_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile IDN homograph attacks protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_idn_homograph_attacks_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_logs-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_logs', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Logs', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'logs', - 'unique_id': 'xyz12_logs', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_logs-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Logs', - }), - 'context': , - 'entity_id': 'switch.fake_profile_logs', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_threat_intelligence_feeds-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_threat_intelligence_feeds', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Threat intelligence feeds', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'threat_intelligence_feeds', - 'unique_id': 'xyz12_threat_intelligence_feeds', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_threat_intelligence_feeds-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Threat intelligence feeds', - }), - 'context': , - 'entity_id': 'switch.fake_profile_threat_intelligence_feeds', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_typosquatting_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_typosquatting_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Typosquatting protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'typosquatting_protection', - 'unique_id': 'xyz12_typosquatting_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_typosquatting_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Typosquatting protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_typosquatting_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_web3-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_web3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Web3', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'web3', - 'unique_id': 'xyz12_web3', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_Sensor[switch.fake_profile_web3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Web3', - }), - 'context': , - 'entity_id': 'switch.fake_profile_web3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_binary_sensor[binary_sensor.fake_profile_device_connection_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1184,1094 +93,3 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[switch.fake_profile_ai_driven_threat_detection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_ai_driven_threat_detection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'AI-Driven threat detection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'ai_threat_detection', - 'unique_id': 'xyz12_ai_threat_detection', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_ai_driven_threat_detection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile AI-Driven threat detection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_ai_driven_threat_detection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_allow_affiliate_tracking_links-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_allow_affiliate_tracking_links', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Allow affiliate & tracking links', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'allow_affiliate', - 'unique_id': 'xyz12_allow_affiliate', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_allow_affiliate_tracking_links-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Allow affiliate & tracking links', - }), - 'context': , - 'entity_id': 'switch.fake_profile_allow_affiliate_tracking_links', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_anonymized_edns_client_subnet-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_anonymized_edns_client_subnet', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Anonymized EDNS client subnet', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'anonymized_ecs', - 'unique_id': 'xyz12_anonymized_ecs', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_anonymized_edns_client_subnet-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Anonymized EDNS client subnet', - }), - 'context': , - 'entity_id': 'switch.fake_profile_anonymized_edns_client_subnet', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_9gag-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_9gag', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block 9GAG', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_9gag', - 'unique_id': 'xyz12_block_9gag', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_bypass_methods-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_bypass_methods', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block bypass methods', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_bypass_methods', - 'unique_id': 'xyz12_block_bypass_methods', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_bypass_methods-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block bypass methods', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_bypass_methods', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_child_sexual_abuse_material-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_child_sexual_abuse_material', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block child sexual abuse material', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_csam', - 'unique_id': 'xyz12_block_csam', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_child_sexual_abuse_material-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block child sexual abuse material', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_child_sexual_abuse_material', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_disguised_third_party_trackers-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_disguised_third_party_trackers', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block disguised third-party trackers', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_disguised_trackers', - 'unique_id': 'xyz12_block_disguised_trackers', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_disguised_third_party_trackers-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block disguised third-party trackers', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_disguised_third_party_trackers', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_dynamic_dns_hostnames-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_dynamic_dns_hostnames', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block dynamic DNS hostnames', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_ddns', - 'unique_id': 'xyz12_block_ddns', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_dynamic_dns_hostnames-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block dynamic DNS hostnames', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_dynamic_dns_hostnames', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_newly_registered_domains-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_newly_registered_domains', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block newly registered domains', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_nrd', - 'unique_id': 'xyz12_block_nrd', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_newly_registered_domains-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block newly registered domains', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_newly_registered_domains', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_page-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_page', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block page', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_page', - 'unique_id': 'xyz12_block_page', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_page-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block page', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_page', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_parked_domains-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_parked_domains', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block parked domains', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_parked_domains', - 'unique_id': 'xyz12_block_parked_domains', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_block_parked_domains-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block parked domains', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_parked_domains', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_cache_boost-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_cache_boost', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Cache boost', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cache_boost', - 'unique_id': 'xyz12_cache_boost', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_cache_boost-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Cache boost', - }), - 'context': , - 'entity_id': 'switch.fake_profile_cache_boost', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_cname_flattening-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_cname_flattening', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'CNAME flattening', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cname_flattening', - 'unique_id': 'xyz12_cname_flattening', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_cname_flattening-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile CNAME flattening', - }), - 'context': , - 'entity_id': 'switch.fake_profile_cname_flattening', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_cryptojacking_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_cryptojacking_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Cryptojacking protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cryptojacking_protection', - 'unique_id': 'xyz12_cryptojacking_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_cryptojacking_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Cryptojacking protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_cryptojacking_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_dns_rebinding_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_dns_rebinding_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS rebinding protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'dns_rebinding_protection', - 'unique_id': 'xyz12_dns_rebinding_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_dns_rebinding_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS rebinding protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_dns_rebinding_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_domain_generation_algorithms_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_domain_generation_algorithms_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Domain generation algorithms protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'dga_protection', - 'unique_id': 'xyz12_dga_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_domain_generation_algorithms_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Domain generation algorithms protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_domain_generation_algorithms_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_force_safesearch-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_force_safesearch', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Force SafeSearch', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'safesearch', - 'unique_id': 'xyz12_safesearch', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_force_safesearch-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Force SafeSearch', - }), - 'context': , - 'entity_id': 'switch.fake_profile_force_safesearch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_force_youtube_restricted_mode-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_force_youtube_restricted_mode', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Force YouTube restricted mode', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'youtube_restricted_mode', - 'unique_id': 'xyz12_youtube_restricted_mode', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_force_youtube_restricted_mode-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Force YouTube restricted mode', - }), - 'context': , - 'entity_id': 'switch.fake_profile_force_youtube_restricted_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_google_safe_browsing-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_google_safe_browsing', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Google safe browsing', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'google_safe_browsing', - 'unique_id': 'xyz12_google_safe_browsing', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_google_safe_browsing-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Google safe browsing', - }), - 'context': , - 'entity_id': 'switch.fake_profile_google_safe_browsing', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_idn_homograph_attacks_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_idn_homograph_attacks_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'IDN homograph attacks protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'idn_homograph_attacks_protection', - 'unique_id': 'xyz12_idn_homograph_attacks_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_idn_homograph_attacks_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile IDN homograph attacks protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_idn_homograph_attacks_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_logs-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_logs', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Logs', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'logs', - 'unique_id': 'xyz12_logs', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_logs-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Logs', - }), - 'context': , - 'entity_id': 'switch.fake_profile_logs', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_threat_intelligence_feeds-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_threat_intelligence_feeds', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Threat intelligence feeds', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'threat_intelligence_feeds', - 'unique_id': 'xyz12_threat_intelligence_feeds', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_threat_intelligence_feeds-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Threat intelligence feeds', - }), - 'context': , - 'entity_id': 'switch.fake_profile_threat_intelligence_feeds', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_typosquatting_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_typosquatting_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Typosquatting protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'typosquatting_protection', - 'unique_id': 'xyz12_typosquatting_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_typosquatting_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Typosquatting protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_typosquatting_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[switch.fake_profile_web3-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_web3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Web3', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'web3', - 'unique_id': 'xyz12_web3', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[switch.fake_profile_web3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Web3', - }), - 'context': , - 'entity_id': 'switch.fake_profile_web3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- diff --git a/tests/components/nextdns/snapshots/test_sensor.ambr b/tests/components/nextdns/snapshots/test_sensor.ambr index 34b40433e3b..14bebea53f8 100644 --- a/tests/components/nextdns/snapshots/test_sensor.ambr +++ b/tests/components/nextdns/snapshots/test_sensor.ambr @@ -1,144 +1,4 @@ # serializer version: 1 -# name: test_sensor[binary_sensor.fake_profile_device_connection_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.fake_profile_device_connection_status', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Device connection status', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'device_connection_status', - 'unique_id': 'xyz12_this_device_nextdns_connection_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[binary_sensor.fake_profile_device_connection_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'connectivity', - 'friendly_name': 'Fake Profile Device connection status', - }), - 'context': , - 'entity_id': 'binary_sensor.fake_profile_device_connection_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[binary_sensor.fake_profile_device_profile_connection_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.fake_profile_device_profile_connection_status', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Device profile connection status', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'device_profile_connection_status', - 'unique_id': 'xyz12_this_device_profile_connection_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[binary_sensor.fake_profile_device_profile_connection_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'connectivity', - 'friendly_name': 'Fake Profile Device profile connection status', - }), - 'context': , - 'entity_id': 'binary_sensor.fake_profile_device_profile_connection_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_sensor[button.fake_profile_clear_logs-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.fake_profile_clear_logs', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Clear logs', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'clear_logs', - 'unique_id': 'xyz12_clear_logs', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[button.fake_profile_clear_logs-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Clear logs', - }), - 'context': , - 'entity_id': 'button.fake_profile_clear_logs', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_sensor[sensor.fake_profile_dns_over_http_3_queries-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1389,3361 +1249,3 @@ 'state': '40', }) # --- -# name: test_sensor[switch.fake_profile_ai_driven_threat_detection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_ai_driven_threat_detection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'AI-Driven threat detection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'ai_threat_detection', - 'unique_id': 'xyz12_ai_threat_detection', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_ai_driven_threat_detection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile AI-Driven threat detection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_ai_driven_threat_detection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_allow_affiliate_tracking_links-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_allow_affiliate_tracking_links', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Allow affiliate & tracking links', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'allow_affiliate', - 'unique_id': 'xyz12_allow_affiliate', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_allow_affiliate_tracking_links-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Allow affiliate & tracking links', - }), - 'context': , - 'entity_id': 'switch.fake_profile_allow_affiliate_tracking_links', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_anonymized_edns_client_subnet-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_anonymized_edns_client_subnet', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Anonymized EDNS client subnet', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'anonymized_ecs', - 'unique_id': 'xyz12_anonymized_ecs', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_anonymized_edns_client_subnet-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Anonymized EDNS client subnet', - }), - 'context': , - 'entity_id': 'switch.fake_profile_anonymized_edns_client_subnet', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_9gag-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_9gag', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block 9GAG', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_9gag', - 'unique_id': 'xyz12_block_9gag', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_9gag-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block 9GAG', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_9gag', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_amazon-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_amazon', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Amazon', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_amazon', - 'unique_id': 'xyz12_block_amazon', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_amazon-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Amazon', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_amazon', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_bereal-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_bereal', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block BeReal', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_bereal', - 'unique_id': 'xyz12_block_bereal', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_bereal-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block BeReal', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_bereal', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_blizzard-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_blizzard', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Blizzard', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_blizzard', - 'unique_id': 'xyz12_block_blizzard', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_blizzard-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Blizzard', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_blizzard', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_bypass_methods-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_bypass_methods', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block bypass methods', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_bypass_methods', - 'unique_id': 'xyz12_block_bypass_methods', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_bypass_methods-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block bypass methods', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_bypass_methods', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_chatgpt-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_chatgpt', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block ChatGPT', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_chatgpt', - 'unique_id': 'xyz12_block_chatgpt', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_chatgpt-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block ChatGPT', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_chatgpt', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_child_sexual_abuse_material-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_child_sexual_abuse_material', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block child sexual abuse material', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_csam', - 'unique_id': 'xyz12_block_csam', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_child_sexual_abuse_material-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block child sexual abuse material', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_child_sexual_abuse_material', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_dailymotion-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_dailymotion', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Dailymotion', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_dailymotion', - 'unique_id': 'xyz12_block_dailymotion', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_dailymotion-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Dailymotion', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_dailymotion', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_dating-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_dating', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block dating', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_dating', - 'unique_id': 'xyz12_block_dating', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_dating-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block dating', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_dating', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_discord-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_discord', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Discord', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_discord', - 'unique_id': 'xyz12_block_discord', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_discord-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Discord', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_discord', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_disguised_third_party_trackers-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_disguised_third_party_trackers', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block disguised third-party trackers', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_disguised_trackers', - 'unique_id': 'xyz12_block_disguised_trackers', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_disguised_third_party_trackers-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block disguised third-party trackers', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_disguised_third_party_trackers', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_disney_plus-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_disney_plus', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Disney Plus', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_disneyplus', - 'unique_id': 'xyz12_block_disneyplus', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_disney_plus-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Disney Plus', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_disney_plus', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_dynamic_dns_hostnames-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_dynamic_dns_hostnames', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block dynamic DNS hostnames', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_ddns', - 'unique_id': 'xyz12_block_ddns', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_dynamic_dns_hostnames-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block dynamic DNS hostnames', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_dynamic_dns_hostnames', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_ebay-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_ebay', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block eBay', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_ebay', - 'unique_id': 'xyz12_block_ebay', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_ebay-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block eBay', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_ebay', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_facebook-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_facebook', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Facebook', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_facebook', - 'unique_id': 'xyz12_block_facebook', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_facebook-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Facebook', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_facebook', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_fortnite-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_fortnite', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Fortnite', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_fortnite', - 'unique_id': 'xyz12_block_fortnite', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_fortnite-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Fortnite', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_fortnite', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_gambling-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_gambling', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block gambling', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_gambling', - 'unique_id': 'xyz12_block_gambling', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_gambling-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block gambling', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_gambling', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_google_chat-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_google_chat', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Google Chat', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_google_chat', - 'unique_id': 'xyz12_block_google_chat', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_google_chat-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Google Chat', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_google_chat', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_hbo_max-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_hbo_max', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block HBO Max', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_hbomax', - 'unique_id': 'xyz12_block_hbomax', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_hbo_max-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block HBO Max', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_hbo_max', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_hulu-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_hulu', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Hulu', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'xyz12_block_hulu', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_hulu-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Hulu', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_hulu', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_imgur-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_imgur', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Imgur', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_imgur', - 'unique_id': 'xyz12_block_imgur', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_imgur-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Imgur', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_imgur', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_instagram-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_instagram', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Instagram', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_instagram', - 'unique_id': 'xyz12_block_instagram', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_instagram-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Instagram', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_instagram', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_league_of_legends-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_league_of_legends', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block League of Legends', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_leagueoflegends', - 'unique_id': 'xyz12_block_leagueoflegends', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_league_of_legends-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block League of Legends', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_league_of_legends', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_mastodon-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_mastodon', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Mastodon', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_mastodon', - 'unique_id': 'xyz12_block_mastodon', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_mastodon-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Mastodon', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_mastodon', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_messenger-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_messenger', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Messenger', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_messenger', - 'unique_id': 'xyz12_block_messenger', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_messenger-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Messenger', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_messenger', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_minecraft-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_minecraft', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Minecraft', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_minecraft', - 'unique_id': 'xyz12_block_minecraft', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_minecraft-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Minecraft', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_minecraft', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_netflix-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_netflix', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Netflix', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_netflix', - 'unique_id': 'xyz12_block_netflix', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_netflix-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Netflix', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_netflix', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_newly_registered_domains-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_newly_registered_domains', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block newly registered domains', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_nrd', - 'unique_id': 'xyz12_block_nrd', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_newly_registered_domains-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block newly registered domains', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_newly_registered_domains', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_online_gaming-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_online_gaming', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block online gaming', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_online_gaming', - 'unique_id': 'xyz12_block_online_gaming', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_online_gaming-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block online gaming', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_online_gaming', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_page-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_page', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block page', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_page', - 'unique_id': 'xyz12_block_page', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_page-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block page', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_page', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_sensor[switch.fake_profile_block_parked_domains-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_parked_domains', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block parked domains', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_parked_domains', - 'unique_id': 'xyz12_block_parked_domains', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_parked_domains-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block parked domains', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_parked_domains', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_pinterest-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_pinterest', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Pinterest', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_pinterest', - 'unique_id': 'xyz12_block_pinterest', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_pinterest-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Pinterest', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_pinterest', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_piracy-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_piracy', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block piracy', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_piracy', - 'unique_id': 'xyz12_block_piracy', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_piracy-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block piracy', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_piracy', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_playstation_network-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_playstation_network', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block PlayStation Network', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_playstation_network', - 'unique_id': 'xyz12_block_playstation_network', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_playstation_network-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block PlayStation Network', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_playstation_network', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_porn-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_porn', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block porn', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_porn', - 'unique_id': 'xyz12_block_porn', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_porn-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block porn', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_porn', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_prime_video-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_prime_video', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Prime Video', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_primevideo', - 'unique_id': 'xyz12_block_primevideo', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_prime_video-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Prime Video', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_prime_video', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_reddit-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_reddit', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Reddit', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_reddit', - 'unique_id': 'xyz12_block_reddit', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_reddit-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Reddit', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_reddit', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_roblox-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_roblox', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Roblox', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_roblox', - 'unique_id': 'xyz12_block_roblox', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_roblox-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Roblox', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_roblox', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_signal-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_signal', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Signal', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_signal', - 'unique_id': 'xyz12_block_signal', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_signal-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Signal', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_signal', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_skype-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_skype', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Skype', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_skype', - 'unique_id': 'xyz12_block_skype', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_skype-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Skype', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_skype', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_snapchat-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_snapchat', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Snapchat', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_snapchat', - 'unique_id': 'xyz12_block_snapchat', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_snapchat-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Snapchat', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_snapchat', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_social_networks-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_social_networks', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block social networks', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_social_networks', - 'unique_id': 'xyz12_block_social_networks', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_social_networks-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block social networks', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_social_networks', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_spotify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_spotify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Spotify', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_spotify', - 'unique_id': 'xyz12_block_spotify', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_spotify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Spotify', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_spotify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_steam-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_steam', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Steam', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_steam', - 'unique_id': 'xyz12_block_steam', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_steam-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Steam', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_steam', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_telegram-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_telegram', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Telegram', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_telegram', - 'unique_id': 'xyz12_block_telegram', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_telegram-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Telegram', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_telegram', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_tiktok-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_tiktok', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block TikTok', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_tiktok', - 'unique_id': 'xyz12_block_tiktok', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_tiktok-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block TikTok', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_tiktok', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_tinder-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_tinder', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Tinder', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_tinder', - 'unique_id': 'xyz12_block_tinder', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_tinder-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Tinder', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_tinder', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_tumblr-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_tumblr', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Tumblr', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_tumblr', - 'unique_id': 'xyz12_block_tumblr', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_tumblr-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Tumblr', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_tumblr', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_twitch-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_twitch', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Twitch', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_twitch', - 'unique_id': 'xyz12_block_twitch', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_twitch-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Twitch', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_twitch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_video_streaming-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_video_streaming', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block video streaming', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_video_streaming', - 'unique_id': 'xyz12_block_video_streaming', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_video_streaming-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block video streaming', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_video_streaming', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_vimeo-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_vimeo', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Vimeo', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_vimeo', - 'unique_id': 'xyz12_block_vimeo', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_vimeo-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Vimeo', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_vimeo', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_vk-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_vk', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block VK', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_vk', - 'unique_id': 'xyz12_block_vk', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_vk-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block VK', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_vk', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_whatsapp-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_whatsapp', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block WhatsApp', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_whatsapp', - 'unique_id': 'xyz12_block_whatsapp', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_whatsapp-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block WhatsApp', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_whatsapp', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_x_formerly_twitter-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_x_formerly_twitter', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block X (formerly Twitter)', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_twitter', - 'unique_id': 'xyz12_block_twitter', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_x_formerly_twitter-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block X (formerly Twitter)', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_x_formerly_twitter', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_xbox_live-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_xbox_live', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Xbox Live', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_xboxlive', - 'unique_id': 'xyz12_block_xboxlive', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_xbox_live-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Xbox Live', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_xbox_live', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_youtube-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_youtube', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block YouTube', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_youtube', - 'unique_id': 'xyz12_block_youtube', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_youtube-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block YouTube', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_youtube', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_block_zoom-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_block_zoom', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Block Zoom', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'block_zoom', - 'unique_id': 'xyz12_block_zoom', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_block_zoom-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Block Zoom', - }), - 'context': , - 'entity_id': 'switch.fake_profile_block_zoom', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_cache_boost-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_cache_boost', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Cache boost', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cache_boost', - 'unique_id': 'xyz12_cache_boost', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_cache_boost-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Cache boost', - }), - 'context': , - 'entity_id': 'switch.fake_profile_cache_boost', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_cname_flattening-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_cname_flattening', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'CNAME flattening', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cname_flattening', - 'unique_id': 'xyz12_cname_flattening', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_cname_flattening-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile CNAME flattening', - }), - 'context': , - 'entity_id': 'switch.fake_profile_cname_flattening', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_cryptojacking_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_cryptojacking_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Cryptojacking protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cryptojacking_protection', - 'unique_id': 'xyz12_cryptojacking_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_cryptojacking_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Cryptojacking protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_cryptojacking_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_dns_rebinding_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_dns_rebinding_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS rebinding protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'dns_rebinding_protection', - 'unique_id': 'xyz12_dns_rebinding_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_dns_rebinding_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS rebinding protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_dns_rebinding_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_domain_generation_algorithms_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_domain_generation_algorithms_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Domain generation algorithms protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'dga_protection', - 'unique_id': 'xyz12_dga_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_domain_generation_algorithms_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Domain generation algorithms protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_domain_generation_algorithms_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_force_safesearch-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_force_safesearch', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Force SafeSearch', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'safesearch', - 'unique_id': 'xyz12_safesearch', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_force_safesearch-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Force SafeSearch', - }), - 'context': , - 'entity_id': 'switch.fake_profile_force_safesearch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_sensor[switch.fake_profile_force_youtube_restricted_mode-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_force_youtube_restricted_mode', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Force YouTube restricted mode', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'youtube_restricted_mode', - 'unique_id': 'xyz12_youtube_restricted_mode', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_force_youtube_restricted_mode-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Force YouTube restricted mode', - }), - 'context': , - 'entity_id': 'switch.fake_profile_force_youtube_restricted_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_sensor[switch.fake_profile_google_safe_browsing-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_google_safe_browsing', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Google safe browsing', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'google_safe_browsing', - 'unique_id': 'xyz12_google_safe_browsing', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_google_safe_browsing-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Google safe browsing', - }), - 'context': , - 'entity_id': 'switch.fake_profile_google_safe_browsing', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_sensor[switch.fake_profile_idn_homograph_attacks_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_idn_homograph_attacks_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'IDN homograph attacks protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'idn_homograph_attacks_protection', - 'unique_id': 'xyz12_idn_homograph_attacks_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_idn_homograph_attacks_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile IDN homograph attacks protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_idn_homograph_attacks_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_logs-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_logs', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Logs', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'logs', - 'unique_id': 'xyz12_logs', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_logs-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Logs', - }), - 'context': , - 'entity_id': 'switch.fake_profile_logs', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_threat_intelligence_feeds-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_threat_intelligence_feeds', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Threat intelligence feeds', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'threat_intelligence_feeds', - 'unique_id': 'xyz12_threat_intelligence_feeds', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_threat_intelligence_feeds-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Threat intelligence feeds', - }), - 'context': , - 'entity_id': 'switch.fake_profile_threat_intelligence_feeds', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_typosquatting_protection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_typosquatting_protection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Typosquatting protection', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'typosquatting_protection', - 'unique_id': 'xyz12_typosquatting_protection', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_typosquatting_protection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Typosquatting protection', - }), - 'context': , - 'entity_id': 'switch.fake_profile_typosquatting_protection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.fake_profile_web3-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.fake_profile_web3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Web3', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'web3', - 'unique_id': 'xyz12_web3', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.fake_profile_web3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Web3', - }), - 'context': , - 'entity_id': 'switch.fake_profile_web3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- diff --git a/tests/components/nextdns/snapshots/test_switch.ambr b/tests/components/nextdns/snapshots/test_switch.ambr index 8472f02e8c5..3328e341a2e 100644 --- a/tests/components/nextdns/snapshots/test_switch.ambr +++ b/tests/components/nextdns/snapshots/test_switch.ambr @@ -1,1394 +1,4 @@ # serializer version: 1 -# name: test_switch[binary_sensor.fake_profile_device_connection_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.fake_profile_device_connection_status', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Device connection status', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'device_connection_status', - 'unique_id': 'xyz12_this_device_nextdns_connection_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_switch[binary_sensor.fake_profile_device_connection_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'connectivity', - 'friendly_name': 'Fake Profile Device connection status', - }), - 'context': , - 'entity_id': 'binary_sensor.fake_profile_device_connection_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_switch[binary_sensor.fake_profile_device_profile_connection_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.fake_profile_device_profile_connection_status', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Device profile connection status', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'device_profile_connection_status', - 'unique_id': 'xyz12_this_device_profile_connection_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_switch[binary_sensor.fake_profile_device_profile_connection_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'connectivity', - 'friendly_name': 'Fake Profile Device profile connection status', - }), - 'context': , - 'entity_id': 'binary_sensor.fake_profile_device_profile_connection_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_switch[button.fake_profile_clear_logs-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.fake_profile_clear_logs', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Clear logs', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'clear_logs', - 'unique_id': 'xyz12_clear_logs', - 'unit_of_measurement': None, - }) -# --- -# name: test_switch[button.fake_profile_clear_logs-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Clear logs', - }), - 'context': , - 'entity_id': 'button.fake_profile_clear_logs', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_http_3_queries-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dns_over_http_3_queries', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS-over-HTTP/3 queries', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'doh3_queries', - 'unique_id': 'xyz12_doh3_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_http_3_queries-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS-over-HTTP/3 queries', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dns_over_http_3_queries', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '15', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_http_3_queries_ratio-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dns_over_http_3_queries_ratio', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS-over-HTTP/3 queries ratio', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'doh3_queries_ratio', - 'unique_id': 'xyz12_doh3_queries_ratio', - 'unit_of_measurement': '%', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_http_3_queries_ratio-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS-over-HTTP/3 queries ratio', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dns_over_http_3_queries_ratio', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '13.0', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_https_queries-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dns_over_https_queries', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS-over-HTTPS queries', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'doh_queries', - 'unique_id': 'xyz12_doh_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_https_queries-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS-over-HTTPS queries', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dns_over_https_queries', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '20', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_https_queries_ratio-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dns_over_https_queries_ratio', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS-over-HTTPS queries ratio', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'doh_queries_ratio', - 'unique_id': 'xyz12_doh_queries_ratio', - 'unit_of_measurement': '%', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_https_queries_ratio-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS-over-HTTPS queries ratio', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dns_over_https_queries_ratio', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '17.4', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_quic_queries-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dns_over_quic_queries', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS-over-QUIC queries', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'doq_queries', - 'unique_id': 'xyz12_doq_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_quic_queries-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS-over-QUIC queries', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dns_over_quic_queries', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_quic_queries_ratio-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dns_over_quic_queries_ratio', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS-over-QUIC queries ratio', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'doq_queries_ratio', - 'unique_id': 'xyz12_doq_queries_ratio', - 'unit_of_measurement': '%', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_quic_queries_ratio-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS-over-QUIC queries ratio', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dns_over_quic_queries_ratio', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '8.7', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_tls_queries-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dns_over_tls_queries', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS-over-TLS queries', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'dot_queries', - 'unique_id': 'xyz12_dot_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_tls_queries-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS-over-TLS queries', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dns_over_tls_queries', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '30', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_tls_queries_ratio-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dns_over_tls_queries_ratio', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS-over-TLS queries ratio', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'dot_queries_ratio', - 'unique_id': 'xyz12_dot_queries_ratio', - 'unit_of_measurement': '%', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_over_tls_queries_ratio-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS-over-TLS queries ratio', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dns_over_tls_queries_ratio', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '26.1', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_queries-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dns_queries', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS queries', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'all_queries', - 'unique_id': 'xyz12_all_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_queries-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS queries', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dns_queries', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '100', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_queries_blocked-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dns_queries_blocked', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS queries blocked', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'blocked_queries', - 'unique_id': 'xyz12_blocked_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_queries_blocked-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS queries blocked', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dns_queries_blocked', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '20', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_queries_blocked_ratio-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dns_queries_blocked_ratio', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS queries blocked ratio', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'blocked_queries_ratio', - 'unique_id': 'xyz12_blocked_queries_ratio', - 'unit_of_measurement': '%', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_queries_blocked_ratio-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS queries blocked ratio', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dns_queries_blocked_ratio', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '20.0', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_queries_relayed-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dns_queries_relayed', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNS queries relayed', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'relayed_queries', - 'unique_id': 'xyz12_relayed_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_dns_queries_relayed-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNS queries relayed', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dns_queries_relayed', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10', - }) -# --- -# name: test_switch[sensor.fake_profile_dnssec_not_validated_queries-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dnssec_not_validated_queries', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNSSEC not validated queries', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'not_validated_queries', - 'unique_id': 'xyz12_not_validated_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_dnssec_not_validated_queries-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNSSEC not validated queries', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dnssec_not_validated_queries', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '25', - }) -# --- -# name: test_switch[sensor.fake_profile_dnssec_validated_queries-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dnssec_validated_queries', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNSSEC validated queries', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'validated_queries', - 'unique_id': 'xyz12_validated_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_dnssec_validated_queries-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNSSEC validated queries', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dnssec_validated_queries', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '75', - }) -# --- -# name: test_switch[sensor.fake_profile_dnssec_validated_queries_ratio-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_dnssec_validated_queries_ratio', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'DNSSEC validated queries ratio', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'validated_queries_ratio', - 'unique_id': 'xyz12_validated_queries_ratio', - 'unit_of_measurement': '%', - }) -# --- -# name: test_switch[sensor.fake_profile_dnssec_validated_queries_ratio-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile DNSSEC validated queries ratio', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_dnssec_validated_queries_ratio', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '75.0', - }) -# --- -# name: test_switch[sensor.fake_profile_encrypted_queries-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_encrypted_queries', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Encrypted queries', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'encrypted_queries', - 'unique_id': 'xyz12_encrypted_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_encrypted_queries-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Encrypted queries', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_encrypted_queries', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '60', - }) -# --- -# name: test_switch[sensor.fake_profile_encrypted_queries_ratio-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_encrypted_queries_ratio', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Encrypted queries ratio', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'encrypted_queries_ratio', - 'unique_id': 'xyz12_encrypted_queries_ratio', - 'unit_of_measurement': '%', - }) -# --- -# name: test_switch[sensor.fake_profile_encrypted_queries_ratio-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Encrypted queries ratio', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_encrypted_queries_ratio', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '60.0', - }) -# --- -# name: test_switch[sensor.fake_profile_ipv4_queries-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_ipv4_queries', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'IPv4 queries', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'ipv4_queries', - 'unique_id': 'xyz12_ipv4_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_ipv4_queries-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile IPv4 queries', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_ipv4_queries', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '90', - }) -# --- -# name: test_switch[sensor.fake_profile_ipv6_queries-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_ipv6_queries', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'IPv6 queries', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'ipv6_queries', - 'unique_id': 'xyz12_ipv6_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_ipv6_queries-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile IPv6 queries', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_ipv6_queries', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10', - }) -# --- -# name: test_switch[sensor.fake_profile_ipv6_queries_ratio-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_ipv6_queries_ratio', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'IPv6 queries ratio', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'ipv6_queries_ratio', - 'unique_id': 'xyz12_ipv6_queries_ratio', - 'unit_of_measurement': '%', - }) -# --- -# name: test_switch[sensor.fake_profile_ipv6_queries_ratio-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile IPv6 queries ratio', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_ipv6_queries_ratio', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10.0', - }) -# --- -# name: test_switch[sensor.fake_profile_tcp_queries-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_tcp_queries', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'TCP queries', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'tcp_queries', - 'unique_id': 'xyz12_tcp_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_tcp_queries-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile TCP queries', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_tcp_queries', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_switch[sensor.fake_profile_tcp_queries_ratio-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_tcp_queries_ratio', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'TCP queries ratio', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'tcp_queries_ratio', - 'unique_id': 'xyz12_tcp_queries_ratio', - 'unit_of_measurement': '%', - }) -# --- -# name: test_switch[sensor.fake_profile_tcp_queries_ratio-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile TCP queries ratio', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_tcp_queries_ratio', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_switch[sensor.fake_profile_udp_queries-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_udp_queries', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'UDP queries', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'udp_queries', - 'unique_id': 'xyz12_udp_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_udp_queries-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile UDP queries', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_udp_queries', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '40', - }) -# --- -# name: test_switch[sensor.fake_profile_udp_queries_ratio-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_udp_queries_ratio', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'UDP queries ratio', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'udp_queries_ratio', - 'unique_id': 'xyz12_udp_queries_ratio', - 'unit_of_measurement': '%', - }) -# --- -# name: test_switch[sensor.fake_profile_udp_queries_ratio-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile UDP queries ratio', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_udp_queries_ratio', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '34.8', - }) -# --- -# name: test_switch[sensor.fake_profile_unencrypted_queries-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.fake_profile_unencrypted_queries', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Unencrypted queries', - 'platform': 'nextdns', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'unencrypted_queries', - 'unique_id': 'xyz12_unencrypted_queries', - 'unit_of_measurement': 'queries', - }) -# --- -# name: test_switch[sensor.fake_profile_unencrypted_queries-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Fake Profile Unencrypted queries', - 'state_class': , - 'unit_of_measurement': 'queries', - }), - 'context': , - 'entity_id': 'sensor.fake_profile_unencrypted_queries', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '40', - }) -# --- # name: test_switch[switch.fake_profile_ai_driven_threat_detection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/nibe_heatpump/snapshots/test_climate.ambr b/tests/components/nibe_heatpump/snapshots/test_climate.ambr index fb3e2d1003b..2db9a813bff 100644 --- a/tests/components/nibe_heatpump/snapshots/test_climate.ambr +++ b/tests/components/nibe_heatpump/snapshots/test_climate.ambr @@ -97,12 +97,6 @@ 'state': 'unavailable', }) # --- -# name: test_active_accessory[Model.S320-s2-climate.climate_system_21][initial] - None -# --- -# name: test_active_accessory[Model.S320-s2-climate.climate_system_s1][initial] - None -# --- # name: test_basic[Model.F1155-s2-climate.climate_system_s2][cooling] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/ping/snapshots/test_binary_sensor.ambr b/tests/components/ping/snapshots/test_binary_sensor.ambr index 98ea9a8a847..24717938874 100644 --- a/tests/components/ping/snapshots/test_binary_sensor.ambr +++ b/tests/components/ping/snapshots/test_binary_sensor.ambr @@ -1,64 +1,4 @@ # serializer version: 1 -# name: test_sensor - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.10_10_10_10', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': '10.10.10.10', - 'platform': 'ping', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor.1 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'connectivity', - 'friendly_name': '10.10.10.10', - 'round_trip_time_avg': 4.333, - 'round_trip_time_max': 10, - 'round_trip_time_mdev': '', - 'round_trip_time_min': 1, - }), - 'context': , - 'entity_id': 'binary_sensor.10_10_10_10', - 'last_changed': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor.2 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'connectivity', - 'friendly_name': '10.10.10.10', - }), - 'context': , - 'entity_id': 'binary_sensor.10_10_10_10', - 'last_changed': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_setup_and_update EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/pyload/snapshots/test_switch.ambr b/tests/components/pyload/snapshots/test_switch.ambr index b6465341b0a..0fcc45f8586 100644 --- a/tests/components/pyload/snapshots/test_switch.ambr +++ b/tests/components/pyload/snapshots/test_switch.ambr @@ -93,50 +93,3 @@ 'state': 'on', }) # --- -# name: test_state[switch.pyload_reconnect-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.pyload_reconnect', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Reconnect', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'XXXXXXXXXXXXXX_reconnect', - 'unit_of_measurement': None, - }) -# --- -# name: test_state[switch.pyload_reconnect-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'switch', - 'friendly_name': 'pyLoad Reconnect', - }), - 'context': , - 'entity_id': 'switch.pyload_reconnect', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- diff --git a/tests/components/samsungtv/snapshots/test_init.ambr b/tests/components/samsungtv/snapshots/test_init.ambr index 42a3f4fb396..061b5bc1836 100644 --- a/tests/components/samsungtv/snapshots/test_init.ambr +++ b/tests/components/samsungtv/snapshots/test_init.ambr @@ -30,8 +30,10 @@ }), 'manufacturer': None, 'model': '82GXARRS', + 'model_id': None, 'name': 'fake', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, @@ -50,6 +52,10 @@ 'mac', 'aa:bb:cc:dd:ee:ff', ), + tuple( + 'mac', + 'none', + ), }), 'disabled_by': None, 'entry_type': None, @@ -66,8 +72,10 @@ }), 'manufacturer': None, 'model': '82GXARRS', + 'model_id': None, 'name': 'fake', 'name_by_user': None, + 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, 'sw_version': None, diff --git a/tests/components/smhi/snapshots/test_weather.ambr b/tests/components/smhi/snapshots/test_weather.ambr index d825e22d470..9ab0375df83 100644 --- a/tests/components/smhi/snapshots/test_weather.ambr +++ b/tests/components/smhi/snapshots/test_weather.ambr @@ -80,142 +80,6 @@ 'wind_speed_unit': , }) # --- -# name: test_forecast_service[get_forecast] - dict({ - 'forecast': list([ - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-07T12:00:00', - 'humidity': 96, - 'precipitation': 0.0, - 'pressure': 991.0, - 'temperature': 18.0, - 'templow': 15.0, - 'wind_bearing': 114, - 'wind_gust_speed': 32.76, - 'wind_speed': 10.08, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'rainy', - 'datetime': '2023-08-08T12:00:00', - 'humidity': 97, - 'precipitation': 10.6, - 'pressure': 984.0, - 'temperature': 15.0, - 'templow': 11.0, - 'wind_bearing': 183, - 'wind_gust_speed': 27.36, - 'wind_speed': 11.16, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'rainy', - 'datetime': '2023-08-09T12:00:00', - 'humidity': 95, - 'precipitation': 6.3, - 'pressure': 1001.0, - 'temperature': 12.0, - 'templow': 11.0, - 'wind_bearing': 166, - 'wind_gust_speed': 48.24, - 'wind_speed': 18.0, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-10T12:00:00', - 'humidity': 75, - 'precipitation': 4.8, - 'pressure': 1011.0, - 'temperature': 14.0, - 'templow': 10.0, - 'wind_bearing': 174, - 'wind_gust_speed': 29.16, - 'wind_speed': 11.16, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-11T12:00:00', - 'humidity': 69, - 'precipitation': 0.6, - 'pressure': 1015.0, - 'temperature': 18.0, - 'templow': 12.0, - 'wind_bearing': 197, - 'wind_gust_speed': 27.36, - 'wind_speed': 10.08, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-12T12:00:00', - 'humidity': 82, - 'precipitation': 0.0, - 'pressure': 1014.0, - 'temperature': 17.0, - 'templow': 12.0, - 'wind_bearing': 225, - 'wind_gust_speed': 28.08, - 'wind_speed': 8.64, - }), - dict({ - 'cloud_coverage': 75, - 'condition': 'partlycloudy', - 'datetime': '2023-08-13T12:00:00', - 'humidity': 59, - 'precipitation': 0.0, - 'pressure': 1013.0, - 'temperature': 20.0, - 'templow': 14.0, - 'wind_bearing': 234, - 'wind_gust_speed': 35.64, - 'wind_speed': 14.76, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'partlycloudy', - 'datetime': '2023-08-14T12:00:00', - 'humidity': 56, - 'precipitation': 0.0, - 'pressure': 1015.0, - 'temperature': 21.0, - 'templow': 14.0, - 'wind_bearing': 216, - 'wind_gust_speed': 33.12, - 'wind_speed': 13.68, - }), - dict({ - 'cloud_coverage': 88, - 'condition': 'partlycloudy', - 'datetime': '2023-08-15T12:00:00', - 'humidity': 64, - 'precipitation': 3.6, - 'pressure': 1014.0, - 'temperature': 20.0, - 'templow': 14.0, - 'wind_bearing': 226, - 'wind_gust_speed': 33.12, - 'wind_speed': 13.68, - }), - dict({ - 'cloud_coverage': 75, - 'condition': 'partlycloudy', - 'datetime': '2023-08-16T12:00:00', - 'humidity': 61, - 'precipitation': 2.4, - 'pressure': 1014.0, - 'temperature': 20.0, - 'templow': 14.0, - 'wind_bearing': 233, - 'wind_gust_speed': 33.48, - 'wind_speed': 14.04, - }), - ]), - }) -# --- # name: test_forecast_service[get_forecasts] dict({ 'weather.smhi_test': dict({ diff --git a/tests/components/solarlog/snapshots/test_sensor.ambr b/tests/components/solarlog/snapshots/test_sensor.ambr index 5fb369bc3b6..df154a5eb9b 100644 --- a/tests/components/solarlog/snapshots/test_sensor.ambr +++ b/tests/components/solarlog/snapshots/test_sensor.ambr @@ -745,1097 +745,6 @@ 'state': '545', }) # --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_alternator_loss-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_alternator_loss', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Alternator loss', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'alternator_loss', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_alternator_loss', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_alternator_loss-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'solarlog_test_1_2_3 Alternator loss', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_alternator_loss', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_capacity-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_capacity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Capacity', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'capacity', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_capacity', - 'unit_of_measurement': '%', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_capacity-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power_factor', - 'friendly_name': 'solarlog_test_1_2_3 Capacity', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_capacity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '85.0', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_ac-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_ac', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Consumption AC', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'consumption_ac', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_ac', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_ac-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'solarlog_test_1_2_3 Consumption AC', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_ac', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '54.87', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_day-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_day', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Consumption day', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'consumption_day', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_day', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_day-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'solarlog_test_1_2_3 Consumption day', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_day', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.005', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_month-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_month', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Consumption month', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'consumption_month', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_month', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_month-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'solarlog_test_1_2_3 Consumption month', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_month', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.758', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_total-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_total', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Consumption total', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'consumption_total', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_total', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_total-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'solarlog_test_1_2_3 Consumption total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '354.687', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_year-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_year', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Consumption year', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'consumption_year', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_year', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_year-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'solarlog_test_1_2_3 Consumption year', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_year', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '4.587', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_yesterday-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_yesterday', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Consumption yesterday', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'consumption_yesterday', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_yesterday', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_consumption_yesterday-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'solarlog_test_1_2_3 Consumption yesterday', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_consumption_yesterday', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.007', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_efficiency-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_efficiency', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Efficiency', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'efficiency', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_efficiency', - 'unit_of_measurement': '%', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_efficiency-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power_factor', - 'friendly_name': 'solarlog_test_1_2_3 Efficiency', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_efficiency', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '98.0', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_installed_peak_power-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_installed_peak_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Installed peak power', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_power', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_total_power', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_installed_peak_power-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'solarlog_test_1_2_3 Installed peak power', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_installed_peak_power', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '120', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_last_update-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_last_update', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Last update', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'last_update', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_last_updated', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_last_update-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'timestamp', - 'friendly_name': 'solarlog_test_1_2_3 Last update', - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_last_update', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_power_ac-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_power_ac', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power AC', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'power_ac', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_ac', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_power_ac-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'solarlog_test_1_2_3 Power AC', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_power_ac', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '100', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_power_available-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_power_available', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power available', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'power_available', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_available', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_power_available-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'solarlog_test_1_2_3 Power available', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_power_available', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '45.13', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_power_dc-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_power_dc', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power DC', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'power_dc', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_dc', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_power_dc-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'solarlog_test_1_2_3 Power DC', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_power_dc', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '102', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_usage-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_usage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Usage', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'usage', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_usage', - 'unit_of_measurement': '%', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_usage-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power_factor', - 'friendly_name': 'solarlog_test_1_2_3 Usage', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_usage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '54.9', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_voltage_ac-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_voltage_ac', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Voltage AC', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'voltage_ac', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_voltage_ac', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_voltage_ac-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'solarlog_test_1_2_3 Voltage AC', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_voltage_ac', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '100', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_voltage_dc-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_voltage_dc', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Voltage DC', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'voltage_dc', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_voltage_dc', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_voltage_dc-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'solarlog_test_1_2_3 Voltage DC', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_voltage_dc', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '100', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_day-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_yield_day', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Yield day', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'yield_day', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_day', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_day-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'solarlog_test_1_2_3 Yield day', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_yield_day', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.004', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_month-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_yield_month', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Yield month', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'yield_month', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_month', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_month-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'solarlog_test_1_2_3 Yield month', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_yield_month', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.515', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_total-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_yield_total', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Yield total', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'yield_total', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_total', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_total-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'solarlog_test_1_2_3 Yield total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_yield_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '56.513', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_year-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_yield_year', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Yield year', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'yield_year', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_year', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_year-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'solarlog_test_1_2_3 Yield year', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_yield_year', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.023', - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_yesterday-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.solarlog_test_1_2_3_yield_yesterday', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Yield yesterday', - 'platform': 'solarlog', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'yield_yesterday', - 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_yesterday', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.solarlog_test_1_2_3_yield_yesterday-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'solarlog_test_1_2_3 Yield yesterday', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.solarlog_test_1_2_3_yield_yesterday', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.005', - }) -# --- # name: test_all_entities[sensor.solarlog_usage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/template/snapshots/test_select.ambr b/tests/components/template/snapshots/test_select.ambr index d4cabb2900f..e2142394cba 100644 --- a/tests/components/template/snapshots/test_select.ambr +++ b/tests/components/template/snapshots/test_select.ambr @@ -16,4 +16,4 @@ 'last_updated': , 'state': 'on', }) -# --- \ No newline at end of file +# --- diff --git a/tests/components/template/snapshots/test_weather.ambr b/tests/components/template/snapshots/test_weather.ambr index 9b0cf2b9471..bdda5b44e94 100644 --- a/tests/components/template/snapshots/test_weather.ambr +++ b/tests/components/template/snapshots/test_weather.ambr @@ -1,87 +1,4 @@ # serializer version: 1 -# name: test_forecasts[config0-1-weather-forecast] - dict({ - 'weather.forecast': dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-02-17T14:00:00+00:00', - 'temperature': 14.2, - }), - ]), - }), - }) -# --- -# name: test_forecasts[config0-1-weather-forecast].1 - dict({ - 'weather.forecast': dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-02-17T14:00:00+00:00', - 'temperature': 14.2, - }), - ]), - }), - }) -# --- -# name: test_forecasts[config0-1-weather-forecast].2 - dict({ - 'weather.forecast': dict({ - 'forecast': list([ - dict({ - 'condition': 'fog', - 'datetime': '2023-02-17T14:00:00+00:00', - 'is_daytime': True, - 'temperature': 14.2, - }), - ]), - }), - }) -# --- -# name: test_forecasts[config0-1-weather-forecast].3 - dict({ - 'weather.forecast': dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-02-17T14:00:00+00:00', - 'temperature': 16.9, - }), - ]), - }), - }) -# --- -# name: test_forecasts[config0-1-weather-get_forecast] - dict({ - 'forecast': list([ - ]), - }) -# --- -# name: test_forecasts[config0-1-weather-get_forecast].1 - dict({ - 'forecast': list([ - ]), - }) -# --- -# name: test_forecasts[config0-1-weather-get_forecast].2 - dict({ - 'forecast': list([ - dict({ - 'condition': 'fog', - 'datetime': '2023-02-17T14:00:00+00:00', - 'is_daytime': True, - 'temperature': 14.2, - }), - ]), - }) -# --- -# name: test_forecasts[config0-1-weather-get_forecast].3 - dict({ - 'forecast': list([ - ]), - }) -# --- # name: test_forecasts[config0-1-weather-get_forecasts] dict({ 'weather.forecast': dict({ @@ -120,51 +37,6 @@ }), }) # --- -# name: test_forecasts[config0-1-weather] - dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-02-17T14:00:00+00:00', - 'temperature': 14.2, - }), - ]), - }) -# --- -# name: test_forecasts[config0-1-weather].1 - dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-02-17T14:00:00+00:00', - 'temperature': 14.2, - }), - ]), - }) -# --- -# name: test_forecasts[config0-1-weather].2 - dict({ - 'forecast': list([ - dict({ - 'condition': 'fog', - 'datetime': '2023-02-17T14:00:00+00:00', - 'is_daytime': True, - 'temperature': 14.2, - }), - ]), - }) -# --- -# name: test_forecasts[config0-1-weather].3 - dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-02-17T14:00:00+00:00', - 'temperature': 16.9, - }), - ]), - }) -# --- # name: test_restore_weather_save_state dict({ 'last_apparent_temperature': None, @@ -180,92 +52,6 @@ 'last_wind_speed': None, }) # --- -# name: test_trigger_weather_services[config0-1-template-forecast] - dict({ - 'weather.test': dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2023-10-19T06:50:05-07:00', - 'precipitation': 20.0, - 'temperature': 20.0, - 'templow': 15.0, - }), - ]), - }), - }) -# --- -# name: test_trigger_weather_services[config0-1-template-forecast].1 - dict({ - 'weather.test': dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2023-10-19T06:50:05-07:00', - 'precipitation': 20.0, - 'temperature': 20.0, - 'templow': 15.0, - }), - ]), - }), - }) -# --- -# name: test_trigger_weather_services[config0-1-template-forecast].2 - dict({ - 'weather.test': dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2023-10-19T06:50:05-07:00', - 'is_daytime': True, - 'precipitation': 20.0, - 'temperature': 20.0, - 'templow': 15.0, - }), - ]), - }), - }) -# --- -# name: test_trigger_weather_services[config0-1-template-get_forecast] - dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2023-10-19T06:50:05-07:00', - 'precipitation': 20.0, - 'temperature': 20.0, - 'templow': 15.0, - }), - ]), - }) -# --- -# name: test_trigger_weather_services[config0-1-template-get_forecast].1 - dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2023-10-19T06:50:05-07:00', - 'precipitation': 20.0, - 'temperature': 20.0, - 'templow': 15.0, - }), - ]), - }) -# --- -# name: test_trigger_weather_services[config0-1-template-get_forecast].2 - dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2023-10-19T06:50:05-07:00', - 'is_daytime': True, - 'precipitation': 20.0, - 'temperature': 20.0, - 'templow': 15.0, - }), - ]), - }) -# --- # name: test_trigger_weather_services[config0-1-template-get_forecasts] dict({ 'weather.test': dict({ @@ -312,43 +98,3 @@ }), }) # --- -# name: test_trigger_weather_services[config0-1-template] - dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2023-10-19T06:50:05-07:00', - 'precipitation': 20.0, - 'temperature': 20.0, - 'templow': 15.0, - }), - ]), - }) -# --- -# name: test_trigger_weather_services[config0-1-template].1 - dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2023-10-19T06:50:05-07:00', - 'precipitation': 20.0, - 'temperature': 20.0, - 'templow': 15.0, - }), - ]), - }) -# --- -# name: test_trigger_weather_services[config0-1-template].2 - dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2023-10-19T06:50:05-07:00', - 'is_daytime': True, - 'precipitation': 20.0, - 'temperature': 20.0, - 'templow': 15.0, - }), - ]), - }) -# --- diff --git a/tests/components/tesla_fleet/snapshots/test_sensor.ambr b/tests/components/tesla_fleet/snapshots/test_sensor.ambr index e4c4c3d96c2..c6a4860056a 100644 --- a/tests/components/tesla_fleet/snapshots/test_sensor.ambr +++ b/tests/components/tesla_fleet/snapshots/test_sensor.ambr @@ -437,67 +437,6 @@ 'state': '6.245', }) # --- -# name: test_sensors[sensor.energy_site_none-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.energy_site_none', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'tesla_fleet', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'island_status', - 'unique_id': '123456-island_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[sensor.energy_site_none-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Energy Site None', - }), - 'context': , - 'entity_id': 'sensor.energy_site_none', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on_grid', - }) -# --- -# name: test_sensors[sensor.energy_site_none-statealt] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Energy Site None', - }), - 'context': , - 'entity_id': 'sensor.energy_site_none', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on_grid', - }) -# --- # name: test_sensors[sensor.energy_site_percentage_charged-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tessie/snapshots/test_cover.ambr b/tests/components/tessie/snapshots/test_cover.ambr index 8c8c9a48c11..6338758afb7 100644 --- a/tests/components/tessie/snapshots/test_cover.ambr +++ b/tests/components/tessie/snapshots/test_cover.ambr @@ -95,39 +95,6 @@ 'state': 'closed', }) # --- -# name: test_covers[cover.test_none-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'cover', - 'entity_category': None, - 'entity_id': 'cover.test_none', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'tessie', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': 'vehicle_state_sun_roof_state', - 'unique_id': 'VINVINVIN-vehicle_state_sun_roof_state', - 'unit_of_measurement': None, - }) -# --- # name: test_covers[cover.test_sunroof-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tomorrowio/snapshots/test_weather.ambr b/tests/components/tomorrowio/snapshots/test_weather.ambr index fe65925e4c7..6278b50b7f7 100644 --- a/tests/components/tomorrowio/snapshots/test_weather.ambr +++ b/tests/components/tomorrowio/snapshots/test_weather.ambr @@ -735,1126 +735,6 @@ }), ]) # --- -# name: test_v4_forecast_service - dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T11:00:00+00:00', - 'dew_point': 12.8, - 'humidity': 58, - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 45.9, - 'templow': 26.1, - 'wind_bearing': 239.6, - 'wind_speed': 34.16, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-08T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 49.4, - 'templow': 26.3, - 'wind_bearing': 262.82, - 'wind_speed': 26.06, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-09T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 67.0, - 'templow': 31.5, - 'wind_bearing': 229.3, - 'wind_speed': 25.38, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-10T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 65.3, - 'templow': 37.3, - 'wind_bearing': 149.91, - 'wind_speed': 38.3, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-11T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 66.2, - 'templow': 48.3, - 'wind_bearing': 210.45, - 'wind_speed': 56.48, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2021-03-12T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 25, - 'temperature': 67.9, - 'templow': 53.8, - 'wind_bearing': 217.98, - 'wind_speed': 44.28, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-13T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 25, - 'temperature': 54.5, - 'templow': 42.9, - 'wind_bearing': 58.79, - 'wind_speed': 34.99, - }), - dict({ - 'condition': 'snowy', - 'datetime': '2021-03-14T10:00:00+00:00', - 'precipitation': 0.94, - 'precipitation_probability': 95, - 'temperature': 42.9, - 'templow': 33.4, - 'wind_bearing': 70.25, - 'wind_speed': 58.5, - }), - dict({ - 'condition': 'snowy', - 'datetime': '2021-03-15T10:00:00+00:00', - 'precipitation': 0.06, - 'precipitation_probability': 55, - 'temperature': 43.7, - 'templow': 29.4, - 'wind_bearing': 84.47, - 'wind_speed': 57.2, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-16T10:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 43.0, - 'templow': 29.1, - 'wind_bearing': 103.85, - 'wind_speed': 24.16, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-17T10:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 52.4, - 'templow': 34.3, - 'wind_bearing': 145.41, - 'wind_speed': 26.17, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-18T10:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 10, - 'temperature': 54.1, - 'templow': 41.3, - 'wind_bearing': 62.99, - 'wind_speed': 23.69, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2021-03-19T10:00:00+00:00', - 'precipitation': 0.12, - 'precipitation_probability': 55, - 'temperature': 48.9, - 'templow': 39.4, - 'wind_bearing': 68.54, - 'wind_speed': 50.08, - }), - dict({ - 'condition': 'snowy', - 'datetime': '2021-03-20T10:00:00+00:00', - 'precipitation': 0.05, - 'precipitation_probability': 33, - 'temperature': 40.1, - 'templow': 35.1, - 'wind_bearing': 56.98, - 'wind_speed': 62.46, - }), - ]), - }) -# --- -# name: test_v4_forecast_service.1 - dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T17:48:00+00:00', - 'dew_point': 12.8, - 'humidity': 58, - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 44.1, - 'wind_bearing': 315.14, - 'wind_speed': 33.59, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T18:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 44.8, - 'wind_bearing': 321.71, - 'wind_speed': 31.82, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T19:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 45.8, - 'wind_bearing': 323.38, - 'wind_speed': 32.04, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T20:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 45.3, - 'wind_bearing': 318.43, - 'wind_speed': 33.73, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T21:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 44.6, - 'wind_bearing': 320.9, - 'wind_speed': 28.98, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T22:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 41.9, - 'wind_bearing': 322.11, - 'wind_speed': 15.7, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T23:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 38.9, - 'wind_bearing': 295.94, - 'wind_speed': 17.78, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-08T00:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 36.2, - 'wind_bearing': 11.94, - 'wind_speed': 20.12, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-08T01:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 34.3, - 'wind_bearing': 13.68, - 'wind_speed': 20.05, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T02:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 32.9, - 'wind_bearing': 14.93, - 'wind_speed': 19.48, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T03:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 31.9, - 'wind_bearing': 26.07, - 'wind_speed': 16.6, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T04:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 29.2, - 'wind_bearing': 51.27, - 'wind_speed': 9.32, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T05:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 27.4, - 'wind_bearing': 343.25, - 'wind_speed': 11.92, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T06:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 26.7, - 'wind_bearing': 341.46, - 'wind_speed': 15.37, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T07:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 26.4, - 'wind_bearing': 322.34, - 'wind_speed': 12.71, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T08:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 26.1, - 'wind_bearing': 294.69, - 'wind_speed': 13.14, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T09:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 30.1, - 'wind_bearing': 325.32, - 'wind_speed': 11.52, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T10:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 31.0, - 'wind_bearing': 322.27, - 'wind_speed': 10.22, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T11:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 27.2, - 'wind_bearing': 310.14, - 'wind_speed': 20.12, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T12:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 29.2, - 'wind_bearing': 324.8, - 'wind_speed': 25.38, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-03-08T13:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 33.2, - 'wind_bearing': 335.16, - 'wind_speed': 23.26, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-08T14:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 37.0, - 'wind_bearing': 324.49, - 'wind_speed': 21.17, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-08T15:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 40.0, - 'wind_bearing': 310.68, - 'wind_speed': 19.98, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-03-08T16:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 42.4, - 'wind_bearing': 304.18, - 'wind_speed': 19.66, - }), - ]), - }) -# --- -# name: test_v4_forecast_service[forecast] - dict({ - 'weather.tomorrow_io_daily': dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T11:00:00+00:00', - 'dew_point': 12.8, - 'humidity': 58, - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 45.9, - 'templow': 26.1, - 'wind_bearing': 239.6, - 'wind_speed': 34.16, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-08T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 49.4, - 'templow': 26.3, - 'wind_bearing': 262.82, - 'wind_speed': 26.06, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-09T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 67.0, - 'templow': 31.5, - 'wind_bearing': 229.3, - 'wind_speed': 25.38, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-10T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 65.3, - 'templow': 37.3, - 'wind_bearing': 149.91, - 'wind_speed': 38.3, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-11T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 66.2, - 'templow': 48.3, - 'wind_bearing': 210.45, - 'wind_speed': 56.48, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2021-03-12T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 25, - 'temperature': 67.9, - 'templow': 53.8, - 'wind_bearing': 217.98, - 'wind_speed': 44.28, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-13T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 25, - 'temperature': 54.5, - 'templow': 42.9, - 'wind_bearing': 58.79, - 'wind_speed': 34.99, - }), - dict({ - 'condition': 'snowy', - 'datetime': '2021-03-14T10:00:00+00:00', - 'precipitation': 0.94, - 'precipitation_probability': 95, - 'temperature': 42.9, - 'templow': 33.4, - 'wind_bearing': 70.25, - 'wind_speed': 58.5, - }), - dict({ - 'condition': 'snowy', - 'datetime': '2021-03-15T10:00:00+00:00', - 'precipitation': 0.06, - 'precipitation_probability': 55, - 'temperature': 43.7, - 'templow': 29.4, - 'wind_bearing': 84.47, - 'wind_speed': 57.2, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-16T10:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 43.0, - 'templow': 29.1, - 'wind_bearing': 103.85, - 'wind_speed': 24.16, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-17T10:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 52.4, - 'templow': 34.3, - 'wind_bearing': 145.41, - 'wind_speed': 26.17, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-18T10:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 10, - 'temperature': 54.1, - 'templow': 41.3, - 'wind_bearing': 62.99, - 'wind_speed': 23.69, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2021-03-19T10:00:00+00:00', - 'precipitation': 0.12, - 'precipitation_probability': 55, - 'temperature': 48.9, - 'templow': 39.4, - 'wind_bearing': 68.54, - 'wind_speed': 50.08, - }), - dict({ - 'condition': 'snowy', - 'datetime': '2021-03-20T10:00:00+00:00', - 'precipitation': 0.05, - 'precipitation_probability': 33, - 'temperature': 40.1, - 'templow': 35.1, - 'wind_bearing': 56.98, - 'wind_speed': 62.46, - }), - ]), - }), - }) -# --- -# name: test_v4_forecast_service[forecast].1 - dict({ - 'weather.tomorrow_io_daily': dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T17:48:00+00:00', - 'dew_point': 12.8, - 'humidity': 58, - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 44.1, - 'wind_bearing': 315.14, - 'wind_speed': 33.59, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T18:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 44.8, - 'wind_bearing': 321.71, - 'wind_speed': 31.82, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T19:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 45.8, - 'wind_bearing': 323.38, - 'wind_speed': 32.04, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T20:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 45.3, - 'wind_bearing': 318.43, - 'wind_speed': 33.73, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T21:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 44.6, - 'wind_bearing': 320.9, - 'wind_speed': 28.98, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T22:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 41.9, - 'wind_bearing': 322.11, - 'wind_speed': 15.7, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T23:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 38.9, - 'wind_bearing': 295.94, - 'wind_speed': 17.78, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-08T00:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 36.2, - 'wind_bearing': 11.94, - 'wind_speed': 20.12, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-08T01:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 34.3, - 'wind_bearing': 13.68, - 'wind_speed': 20.05, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T02:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 32.9, - 'wind_bearing': 14.93, - 'wind_speed': 19.48, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T03:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 31.9, - 'wind_bearing': 26.07, - 'wind_speed': 16.6, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T04:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 29.2, - 'wind_bearing': 51.27, - 'wind_speed': 9.32, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T05:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 27.4, - 'wind_bearing': 343.25, - 'wind_speed': 11.92, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T06:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 26.7, - 'wind_bearing': 341.46, - 'wind_speed': 15.37, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T07:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 26.4, - 'wind_bearing': 322.34, - 'wind_speed': 12.71, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T08:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 26.1, - 'wind_bearing': 294.69, - 'wind_speed': 13.14, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T09:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 30.1, - 'wind_bearing': 325.32, - 'wind_speed': 11.52, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T10:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 31.0, - 'wind_bearing': 322.27, - 'wind_speed': 10.22, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T11:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 27.2, - 'wind_bearing': 310.14, - 'wind_speed': 20.12, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T12:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 29.2, - 'wind_bearing': 324.8, - 'wind_speed': 25.38, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-03-08T13:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 33.2, - 'wind_bearing': 335.16, - 'wind_speed': 23.26, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-08T14:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 37.0, - 'wind_bearing': 324.49, - 'wind_speed': 21.17, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-08T15:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 40.0, - 'wind_bearing': 310.68, - 'wind_speed': 19.98, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-03-08T16:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 42.4, - 'wind_bearing': 304.18, - 'wind_speed': 19.66, - }), - ]), - }), - }) -# --- -# name: test_v4_forecast_service[get_forecast] - dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T11:00:00+00:00', - 'dew_point': 12.8, - 'humidity': 58, - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 45.9, - 'templow': 26.1, - 'wind_bearing': 239.6, - 'wind_speed': 34.16, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-08T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 49.4, - 'templow': 26.3, - 'wind_bearing': 262.82, - 'wind_speed': 26.06, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-09T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 67.0, - 'templow': 31.5, - 'wind_bearing': 229.3, - 'wind_speed': 25.38, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-10T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 65.3, - 'templow': 37.3, - 'wind_bearing': 149.91, - 'wind_speed': 38.3, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-11T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 66.2, - 'templow': 48.3, - 'wind_bearing': 210.45, - 'wind_speed': 56.48, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2021-03-12T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 25, - 'temperature': 67.9, - 'templow': 53.8, - 'wind_bearing': 217.98, - 'wind_speed': 44.28, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-13T11:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 25, - 'temperature': 54.5, - 'templow': 42.9, - 'wind_bearing': 58.79, - 'wind_speed': 34.99, - }), - dict({ - 'condition': 'snowy', - 'datetime': '2021-03-14T10:00:00+00:00', - 'precipitation': 0.94, - 'precipitation_probability': 95, - 'temperature': 42.9, - 'templow': 33.4, - 'wind_bearing': 70.25, - 'wind_speed': 58.5, - }), - dict({ - 'condition': 'snowy', - 'datetime': '2021-03-15T10:00:00+00:00', - 'precipitation': 0.06, - 'precipitation_probability': 55, - 'temperature': 43.7, - 'templow': 29.4, - 'wind_bearing': 84.47, - 'wind_speed': 57.2, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-16T10:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 43.0, - 'templow': 29.1, - 'wind_bearing': 103.85, - 'wind_speed': 24.16, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-17T10:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 52.4, - 'templow': 34.3, - 'wind_bearing': 145.41, - 'wind_speed': 26.17, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-18T10:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 10, - 'temperature': 54.1, - 'templow': 41.3, - 'wind_bearing': 62.99, - 'wind_speed': 23.69, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2021-03-19T10:00:00+00:00', - 'precipitation': 0.12, - 'precipitation_probability': 55, - 'temperature': 48.9, - 'templow': 39.4, - 'wind_bearing': 68.54, - 'wind_speed': 50.08, - }), - dict({ - 'condition': 'snowy', - 'datetime': '2021-03-20T10:00:00+00:00', - 'precipitation': 0.05, - 'precipitation_probability': 33, - 'temperature': 40.1, - 'templow': 35.1, - 'wind_bearing': 56.98, - 'wind_speed': 62.46, - }), - ]), - }) -# --- -# name: test_v4_forecast_service[get_forecast].1 - dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T17:48:00+00:00', - 'dew_point': 12.8, - 'humidity': 58, - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 44.1, - 'wind_bearing': 315.14, - 'wind_speed': 33.59, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T18:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 44.8, - 'wind_bearing': 321.71, - 'wind_speed': 31.82, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T19:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 45.8, - 'wind_bearing': 323.38, - 'wind_speed': 32.04, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T20:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 45.3, - 'wind_bearing': 318.43, - 'wind_speed': 33.73, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T21:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 44.6, - 'wind_bearing': 320.9, - 'wind_speed': 28.98, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T22:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 41.9, - 'wind_bearing': 322.11, - 'wind_speed': 15.7, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-07T23:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 38.9, - 'wind_bearing': 295.94, - 'wind_speed': 17.78, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-08T00:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 36.2, - 'wind_bearing': 11.94, - 'wind_speed': 20.12, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2021-03-08T01:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 34.3, - 'wind_bearing': 13.68, - 'wind_speed': 20.05, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T02:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 32.9, - 'wind_bearing': 14.93, - 'wind_speed': 19.48, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T03:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 31.9, - 'wind_bearing': 26.07, - 'wind_speed': 16.6, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T04:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 29.2, - 'wind_bearing': 51.27, - 'wind_speed': 9.32, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T05:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 27.4, - 'wind_bearing': 343.25, - 'wind_speed': 11.92, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T06:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 26.7, - 'wind_bearing': 341.46, - 'wind_speed': 15.37, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T07:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 26.4, - 'wind_bearing': 322.34, - 'wind_speed': 12.71, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T08:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 26.1, - 'wind_bearing': 294.69, - 'wind_speed': 13.14, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T09:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 30.1, - 'wind_bearing': 325.32, - 'wind_speed': 11.52, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T10:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 31.0, - 'wind_bearing': 322.27, - 'wind_speed': 10.22, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T11:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 27.2, - 'wind_bearing': 310.14, - 'wind_speed': 20.12, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2021-03-08T12:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 29.2, - 'wind_bearing': 324.8, - 'wind_speed': 25.38, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-03-08T13:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 33.2, - 'wind_bearing': 335.16, - 'wind_speed': 23.26, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-08T14:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 37.0, - 'wind_bearing': 324.49, - 'wind_speed': 21.17, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2021-03-08T15:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 40.0, - 'wind_bearing': 310.68, - 'wind_speed': 19.98, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2021-03-08T16:48:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 0, - 'temperature': 42.4, - 'wind_bearing': 304.18, - 'wind_speed': 19.66, - }), - ]), - }) -# --- # name: test_v4_forecast_service[get_forecasts] dict({ 'weather.tomorrow_io_daily': dict({ diff --git a/tests/components/tplink_omada/snapshots/test_switch.ambr b/tests/components/tplink_omada/snapshots/test_switch.ambr index 282d2a4a6a5..a13d386e721 100644 --- a/tests/components/tplink_omada/snapshots/test_switch.ambr +++ b/tests/components/tplink_omada/snapshots/test_switch.ambr @@ -25,19 +25,6 @@ 'state': 'on', }) # --- -# name: test_gateway_disappear_disables_switches - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Router Port 4 Internet Connected', - 'icon': 'mdi:ethernet', - }), - 'context': , - 'entity_id': 'switch.test_router_port_4_internet_connected', - 'last_changed': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_gateway_port_change_disables_switch_entities StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -110,144 +97,6 @@ 'unit_of_measurement': None, }) # --- -# name: test_poe_switches.10 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test PoE Switch Port 6 PoE', - }), - 'context': , - 'entity_id': 'switch.test_poe_switch_port_6_poe', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_poe_switches.11 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.test_poe_switch_port_6_poe', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Port 6 PoE', - 'platform': 'tplink_omada', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'poe_control', - 'unique_id': '54-AF-97-00-00-01_000000000000000000000006_poe', - 'unit_of_measurement': None, - }) -# --- -# name: test_poe_switches.12 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test PoE Switch Port 7 PoE', - }), - 'context': , - 'entity_id': 'switch.test_poe_switch_port_7_poe', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_poe_switches.13 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.test_poe_switch_port_7_poe', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Port 7 PoE', - 'platform': 'tplink_omada', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'poe_control', - 'unique_id': '54-AF-97-00-00-01_000000000000000000000007_poe', - 'unit_of_measurement': None, - }) -# --- -# name: test_poe_switches.14 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test PoE Switch Port 8 PoE', - }), - 'context': , - 'entity_id': 'switch.test_poe_switch_port_8_poe', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_poe_switches.15 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.test_poe_switch_port_8_poe', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Port 8 PoE', - 'platform': 'tplink_omada', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'poe_control', - 'unique_id': '54-AF-97-00-00-01_000000000000000000000008_poe', - 'unit_of_measurement': None, - }) -# --- # name: test_poe_switches.2 StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -294,141 +143,3 @@ 'unit_of_measurement': None, }) # --- -# name: test_poe_switches.4 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test PoE Switch Port 3 PoE', - }), - 'context': , - 'entity_id': 'switch.test_poe_switch_port_3_poe', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_poe_switches.5 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.test_poe_switch_port_3_poe', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Port 3 PoE', - 'platform': 'tplink_omada', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'poe_control', - 'unique_id': '54-AF-97-00-00-01_000000000000000000000003_poe', - 'unit_of_measurement': None, - }) -# --- -# name: test_poe_switches.6 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test PoE Switch Port 4 PoE', - }), - 'context': , - 'entity_id': 'switch.test_poe_switch_port_4_poe', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_poe_switches.7 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.test_poe_switch_port_4_poe', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Port 4 PoE', - 'platform': 'tplink_omada', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'poe_control', - 'unique_id': '54-AF-97-00-00-01_000000000000000000000004_poe', - 'unit_of_measurement': None, - }) -# --- -# name: test_poe_switches.8 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test PoE Switch Port 5 PoE', - }), - 'context': , - 'entity_id': 'switch.test_poe_switch_port_5_poe', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_poe_switches.9 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.test_poe_switch_port_5_poe', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Port 5 PoE', - 'platform': 'tplink_omada', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'poe_control', - 'unique_id': '54-AF-97-00-00-01_000000000000000000000005_poe', - 'unit_of_measurement': None, - }) -# --- diff --git a/tests/components/tractive/snapshots/test_binary_sensor.ambr b/tests/components/tractive/snapshots/test_binary_sensor.ambr index c6d50fb0fbb..4b610e927d5 100644 --- a/tests/components/tractive/snapshots/test_binary_sensor.ambr +++ b/tests/components/tractive/snapshots/test_binary_sensor.ambr @@ -46,50 +46,3 @@ 'state': 'on', }) # --- -# name: test_sensor[binary_sensor.test_pet_tracker_battery_charging-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.test_pet_tracker_battery_charging', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Tracker battery charging', - 'platform': 'tractive', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'tracker_battery_charging', - 'unique_id': 'pet_id_123_battery_charging', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[binary_sensor.test_pet_tracker_battery_charging-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'battery_charging', - 'friendly_name': 'Test Pet Tracker battery charging', - }), - 'context': , - 'entity_id': 'binary_sensor.test_pet_tracker_battery_charging', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- diff --git a/tests/components/tractive/snapshots/test_device_tracker.ambr b/tests/components/tractive/snapshots/test_device_tracker.ambr index 3a145a48b5a..4e7c5bfe173 100644 --- a/tests/components/tractive/snapshots/test_device_tracker.ambr +++ b/tests/components/tractive/snapshots/test_device_tracker.ambr @@ -50,54 +50,3 @@ 'state': 'not_home', }) # --- -# name: test_sensor[device_tracker.test_pet_tracker-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'device_tracker', - 'entity_category': , - 'entity_id': 'device_tracker.test_pet_tracker', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Tracker', - 'platform': 'tractive', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'tracker', - 'unique_id': 'pet_id_123', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[device_tracker.test_pet_tracker-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'battery_level': 88, - 'friendly_name': 'Test Pet Tracker', - 'gps_accuracy': 99, - 'latitude': 22.333, - 'longitude': 44.555, - 'source_type': , - }), - 'context': , - 'entity_id': 'device_tracker.test_pet_tracker', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'not_home', - }) -# --- diff --git a/tests/components/tractive/snapshots/test_switch.ambr b/tests/components/tractive/snapshots/test_switch.ambr index ea9ea9d9e48..08e0c984d0c 100644 --- a/tests/components/tractive/snapshots/test_switch.ambr +++ b/tests/components/tractive/snapshots/test_switch.ambr @@ -1,142 +1,4 @@ # serializer version: 1 -# name: test_sensor[switch.test_pet_live_tracking-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.test_pet_live_tracking', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Live tracking', - 'platform': 'tractive', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'live_tracking', - 'unique_id': 'pet_id_123_live_tracking', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.test_pet_live_tracking-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Pet Live tracking', - }), - 'context': , - 'entity_id': 'switch.test_pet_live_tracking', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.test_pet_tracker_buzzer-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.test_pet_tracker_buzzer', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Tracker buzzer', - 'platform': 'tractive', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'tracker_buzzer', - 'unique_id': 'pet_id_123_buzzer', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.test_pet_tracker_buzzer-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Pet Tracker buzzer', - }), - 'context': , - 'entity_id': 'switch.test_pet_tracker_buzzer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_sensor[switch.test_pet_tracker_led-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.test_pet_tracker_led', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Tracker LED', - 'platform': 'tractive', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'tracker_led', - 'unique_id': 'pet_id_123_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[switch.test_pet_tracker_led-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Pet Tracker LED', - }), - 'context': , - 'entity_id': 'switch.test_pet_tracker_led', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_switch[switch.test_pet_live_tracking-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/unifi/snapshots/test_button.ambr b/tests/components/unifi/snapshots/test_button.ambr index 51a37620268..de305aee7eb 100644 --- a/tests/components/unifi/snapshots/test_button.ambr +++ b/tests/components/unifi/snapshots/test_button.ambr @@ -1,98 +1,4 @@ # serializer version: 1 -# name: test_entity_and_device_data[site_payload0-device_payload0][button.switch_port_1_power_cycle-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.switch_port_1_power_cycle', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Port 1 Power Cycle', - 'platform': 'unifi', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'power_cycle-00:00:00:00:01:01_1', - 'unit_of_measurement': None, - }) -# --- -# name: test_entity_and_device_data[site_payload0-device_payload0][button.switch_port_1_power_cycle-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'restart', - 'friendly_name': 'switch Port 1 Power Cycle', - }), - 'context': , - 'entity_id': 'button.switch_port_1_power_cycle', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_entity_and_device_data[site_payload0-device_payload0][button.switch_restart-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.switch_restart', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Restart', - 'platform': 'unifi', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'device_restart-00:00:00:00:01:01', - 'unit_of_measurement': None, - }) -# --- -# name: test_entity_and_device_data[site_payload0-device_payload0][button.switch_restart-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'restart', - 'friendly_name': 'switch Restart', - }), - 'context': , - 'entity_id': 'button.switch_restart', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_entity_and_device_data[site_payload0-wlan_payload0-device_payload0][button.ssid_1_regenerate_password-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/unifi/snapshots/test_image.ambr b/tests/components/unifi/snapshots/test_image.ambr index e33ec678217..0922320ed4d 100644 --- a/tests/components/unifi/snapshots/test_image.ambr +++ b/tests/components/unifi/snapshots/test_image.ambr @@ -47,12 +47,6 @@ 'state': '2021-01-01T01:01:00+00:00', }) # --- -# name: test_wlan_qr_code - b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x84\x00\x00\x00\x84\x01\x00\x00\x00\x00y?\xbe\n\x00\x00\x00\xcaIDATx\xda\xedV[\n\xc30\x0c\x13\xbb\x80\xef\x7fK\xdd\xc0\x93\x94\xfd\xac\x1fcL\xfbl(\xc4\x04*\xacG\xdcb/\x8b\xb8O\xdeO\x00\xccP\x95\x8b\xe5\x03\xd7\xf5\xcd\x89pF\xcf\x8c \\48\x08\nS\x948\x03p\xfe\x80C\xa8\x9d\x16\xc7P\xabvJ}\xe2\xd7\x84[\xe5W\xfc7\xbbS\xfd\xde\xcfB\xf115\xa2\xe3%\x99\xad\x93\xa0:\xbf6\xbeS\xec\x1a^\xb4\xed\xfb\xb2\xab\xd1\x99\xc9\xcdAjx\x89\x0e\xc5\xea\xf4T\xf9\xee\xe40m58\xb6<\x1b\xab~\xf4\xban\xd7:\xceu\x9e\x05\xc4I\xa6\xbb\xfb%q<7:\xbf\xa2\x90wo\xf5 -# --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-config_entry_options0].3 - -# --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-config_entry_options0].4 - '1234.0' -# --- # name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0] 'rx-00:00:00:00:00:01' # --- @@ -35,63 +20,6 @@ # name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0].6 '1234.0' # --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_uptime-uptime--config_entry_options0] - 'uptime-00:00:00:00:00:01' -# --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_uptime-uptime--config_entry_options0].1 - -# --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_uptime-uptime--config_entry_options0].2 - 'timestamp' -# --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_uptime-uptime--config_entry_options0].3 - 'Wired client Uptime' -# --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_uptime-uptime--config_entry_options0].4 - None -# --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_uptime-uptime--config_entry_options0].5 - None -# --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_uptime-uptime--config_entry_options0].6 - '2020-09-14T14:41:45+00:00' -# --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_rx-rx--config_entry_options0] - 'rx-00:00:00:00:00:01' -# --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_rx-rx--config_entry_options0].1 - -# --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_rx-rx--config_entry_options0].2 - 'data_rate' -# --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_rx-rx--config_entry_options0].3 - 'Wired client RX' -# --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_rx-rx--config_entry_options0].4 - -# --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_rx-rx--config_entry_options0].5 - -# --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_rx-rx--config_entry_options0].6 - '1234.0' -# --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-config_entry_options0] - 'data_rate' -# --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-config_entry_options0].1 - 'data_rate' -# --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-config_entry_options0].2 - -# --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-config_entry_options0].3 - -# --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-config_entry_options0].4 - '5678.0' -# --- # name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0] 'tx-00:00:00:00:00:01' # --- @@ -113,27 +41,6 @@ # name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0].6 '5678.0' # --- -# name: test_sensor_sources[client_payload2-sensor.wired_client_tx-tx--config_entry_options0] - 'tx-00:00:00:00:00:01' -# --- -# name: test_sensor_sources[client_payload2-sensor.wired_client_tx-tx--config_entry_options0].1 - -# --- -# name: test_sensor_sources[client_payload2-sensor.wired_client_tx-tx--config_entry_options0].2 - 'data_rate' -# --- -# name: test_sensor_sources[client_payload2-sensor.wired_client_tx-tx--config_entry_options0].3 - 'Wired client TX' -# --- -# name: test_sensor_sources[client_payload2-sensor.wired_client_tx-tx--config_entry_options0].4 - -# --- -# name: test_sensor_sources[client_payload2-sensor.wired_client_tx-tx--config_entry_options0].5 - -# --- -# name: test_sensor_sources[client_payload2-sensor.wired_client_tx-tx--config_entry_options0].6 - '5678.0' -# --- # name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0] 'uptime-00:00:00:00:00:01' # --- @@ -155,27 +62,6 @@ # name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0].6 '2020-09-14T14:41:45+00:00' # --- -# name: test_sensor_sources[client_payload2-sensor.wireless_client_rx-rx--config_entry_options0] - 'rx-00:00:00:00:00:02' -# --- -# name: test_sensor_sources[client_payload2-sensor.wireless_client_rx-rx--config_entry_options0].1 - -# --- -# name: test_sensor_sources[client_payload2-sensor.wireless_client_rx-rx--config_entry_options0].2 - 'data_rate' -# --- -# name: test_sensor_sources[client_payload2-sensor.wireless_client_rx-rx--config_entry_options0].3 - 'Wireless client RX' -# --- -# name: test_sensor_sources[client_payload2-sensor.wireless_client_rx-rx--config_entry_options0].4 - -# --- -# name: test_sensor_sources[client_payload2-sensor.wireless_client_rx-rx--config_entry_options0].5 - -# --- -# name: test_sensor_sources[client_payload2-sensor.wireless_client_rx-rx--config_entry_options0].6 - '2345.0' -# --- # name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0] 'rx-00:00:00:00:00:01' # --- @@ -197,27 +83,6 @@ # name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0].6 '2345.0' # --- -# name: test_sensor_sources[client_payload3-sensor.wireless_client_tx-tx--config_entry_options0] - 'tx-00:00:00:00:00:02' -# --- -# name: test_sensor_sources[client_payload3-sensor.wireless_client_tx-tx--config_entry_options0].1 - -# --- -# name: test_sensor_sources[client_payload3-sensor.wireless_client_tx-tx--config_entry_options0].2 - 'data_rate' -# --- -# name: test_sensor_sources[client_payload3-sensor.wireless_client_tx-tx--config_entry_options0].3 - 'Wireless client TX' -# --- -# name: test_sensor_sources[client_payload3-sensor.wireless_client_tx-tx--config_entry_options0].4 - -# --- -# name: test_sensor_sources[client_payload3-sensor.wireless_client_tx-tx--config_entry_options0].5 - -# --- -# name: test_sensor_sources[client_payload3-sensor.wireless_client_tx-tx--config_entry_options0].6 - '6789.0' -# --- # name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0] 'tx-00:00:00:00:00:01' # --- diff --git a/tests/components/uptime/snapshots/test_sensor.ambr b/tests/components/uptime/snapshots/test_sensor.ambr index fa0cb6bf8a9..561e4b83320 100644 --- a/tests/components/uptime/snapshots/test_sensor.ambr +++ b/tests/components/uptime/snapshots/test_sensor.ambr @@ -71,25 +71,3 @@ 'via_device_id': None, }) # --- -# name: test_uptime_sensor.3 - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': , - 'hw_version': None, - 'id': , - 'is_new': False, - 'manufacturer': None, - 'model': None, - 'name': 'Uptime', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }) -# --- diff --git a/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr b/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr index f7b635eb4fa..95be86664a2 100644 --- a/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr +++ b/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr @@ -53,122 +53,6 @@ 'state': '0.96139', }) # --- -# name: test_all_entities[sensor.my_home_station_atmospheric_pressure-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.my_home_station_atmospheric_pressure', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Atmospheric pressure', - 'platform': 'weatherflow_cloud', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'barometric_pressure', - 'unique_id': '24432_barometric_pressure', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.my_home_station_atmospheric_pressure-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'My Home Station Atmospheric pressure', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.my_home_station_atmospheric_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '782.8', - }) -# --- -# name: test_all_entities[sensor.my_home_station_atmospheric_pressure_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.my_home_station_atmospheric_pressure_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Atmospheric pressure', - 'platform': 'weatherflow_cloud', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'sea_level_pressure', - 'unique_id': '24432_sea_level_pressure', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.my_home_station_atmospheric_pressure_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'My Home Station Atmospheric pressure', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.my_home_station_atmospheric_pressure_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1006.2', - }) -# --- # name: test_all_entities[sensor.my_home_station_dew_point-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -224,58 +108,6 @@ 'state': '-10.4', }) # --- -# name: test_all_entities[sensor.my_home_station_distance-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.my_home_station_distance', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Distance', - 'platform': 'weatherflow_cloud', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'lightning_strike_last_distance', - 'unique_id': '24432_lightning_strike_last_distance', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.my_home_station_distance-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api', - 'device_class': 'distance', - 'friendly_name': 'My Home Station Distance', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.my_home_station_distance', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '26', - }) -# --- # name: test_all_entities[sensor.my_home_station_feels_like-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -636,210 +468,6 @@ 'state': '2024-02-07T23:01:15+00:00', }) # --- -# name: test_all_entities[sensor.my_home_station_none-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.my_home_station_none', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 5, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'weatherflow_cloud', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'air_density', - 'unique_id': '24432_air_density', - 'unit_of_measurement': 'kg/m³', - }) -# --- -# name: test_all_entities[sensor.my_home_station_none-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api', - 'friendly_name': 'My Home Station None', - 'state_class': , - 'unit_of_measurement': 'kg/m³', - }), - 'context': , - 'entity_id': 'sensor.my_home_station_none', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.96139', - }) -# --- -# name: test_all_entities[sensor.my_home_station_none_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.my_home_station_none_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'weatherflow_cloud', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'lightning_strike_count', - 'unique_id': '24432_lightning_strike_count', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.my_home_station_none_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api', - 'friendly_name': 'My Home Station None', - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.my_home_station_none_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_all_entities[sensor.my_home_station_none_3-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.my_home_station_none_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'weatherflow_cloud', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'lightning_strike_count_last_1hr', - 'unique_id': '24432_lightning_strike_count_last_1hr', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.my_home_station_none_3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api', - 'friendly_name': 'My Home Station None', - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.my_home_station_none_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_all_entities[sensor.my_home_station_none_4-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.my_home_station_none_4', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'weatherflow_cloud', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'lightning_strike_count_last_3hr', - 'unique_id': '24432_lightning_strike_count_last_3hr', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.my_home_station_none_4-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api', - 'friendly_name': 'My Home Station None', - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.my_home_station_none_4', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- # name: test_all_entities[sensor.my_home_station_pressure_barometric-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1011,384 +639,6 @@ 'state': '10.5', }) # --- -# name: test_all_entities[sensor.my_home_station_temperature_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.my_home_station_temperature_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'weatherflow_cloud', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'dew_point', - 'unique_id': '24432_dew_point', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.my_home_station_temperature_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api', - 'device_class': 'temperature', - 'friendly_name': 'My Home Station Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.my_home_station_temperature_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '-10.4', - }) -# --- -# name: test_all_entities[sensor.my_home_station_temperature_3-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.my_home_station_temperature_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'weatherflow_cloud', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'feels_like', - 'unique_id': '24432_feels_like', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.my_home_station_temperature_3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api', - 'device_class': 'temperature', - 'friendly_name': 'My Home Station Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.my_home_station_temperature_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10.5', - }) -# --- -# name: test_all_entities[sensor.my_home_station_temperature_4-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.my_home_station_temperature_4', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'weatherflow_cloud', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'heat_index', - 'unique_id': '24432_heat_index', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.my_home_station_temperature_4-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api', - 'device_class': 'temperature', - 'friendly_name': 'My Home Station Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.my_home_station_temperature_4', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10.5', - }) -# --- -# name: test_all_entities[sensor.my_home_station_temperature_5-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.my_home_station_temperature_5', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'weatherflow_cloud', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'wind_chill', - 'unique_id': '24432_wind_chill', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.my_home_station_temperature_5-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api', - 'device_class': 'temperature', - 'friendly_name': 'My Home Station Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.my_home_station_temperature_5', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10.5', - }) -# --- -# name: test_all_entities[sensor.my_home_station_temperature_6-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.my_home_station_temperature_6', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'weatherflow_cloud', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'wet_bulb_temperature', - 'unique_id': '24432_wet_bulb_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.my_home_station_temperature_6-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api', - 'device_class': 'temperature', - 'friendly_name': 'My Home Station Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.my_home_station_temperature_6', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.1', - }) -# --- -# name: test_all_entities[sensor.my_home_station_temperature_7-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.my_home_station_temperature_7', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'weatherflow_cloud', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'wet_bulb_globe_temperature', - 'unique_id': '24432_wet_bulb_globe_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.my_home_station_temperature_7-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api', - 'device_class': 'temperature', - 'friendly_name': 'My Home Station Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.my_home_station_temperature_7', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '4.6', - }) -# --- -# name: test_all_entities[sensor.my_home_station_timestamp-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.my_home_station_timestamp', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Timestamp', - 'platform': 'weatherflow_cloud', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'lightning_strike_last_epoch', - 'unique_id': '24432_lightning_strike_last_epoch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.my_home_station_timestamp-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Weather data delivered by WeatherFlow/Tempest REST Api', - 'device_class': 'timestamp', - 'friendly_name': 'My Home Station Timestamp', - }), - 'context': , - 'entity_id': 'sensor.my_home_station_timestamp', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2024-02-07T23:01:15+00:00', - }) -# --- # name: test_all_entities[sensor.my_home_station_wet_bulb_globe_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/weatherkit/snapshots/test_weather.ambr b/tests/components/weatherkit/snapshots/test_weather.ambr index 1fbe5389e98..f6fa2f1514b 100644 --- a/tests/components/weatherkit/snapshots/test_weather.ambr +++ b/tests/components/weatherkit/snapshots/test_weather.ambr @@ -1,294 +1,4 @@ # serializer version: 1 -# name: test_daily_forecast - dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-09-08T15:00:00Z', - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'temperature': 28.6, - 'templow': 21.2, - 'uv_index': 6, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2023-09-09T15:00:00Z', - 'precipitation': 3.6, - 'precipitation_probability': 45.0, - 'temperature': 30.6, - 'templow': 21.0, - 'uv_index': 6, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2023-09-10T15:00:00Z', - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'temperature': 30.4, - 'templow': 23.1, - 'uv_index': 6, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2023-09-11T15:00:00Z', - 'precipitation': 0.7, - 'precipitation_probability': 47.0, - 'temperature': 30.4, - 'templow': 23.1, - 'uv_index': 5, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2023-09-12T15:00:00Z', - 'precipitation': 7.7, - 'precipitation_probability': 37.0, - 'temperature': 30.4, - 'templow': 22.1, - 'uv_index': 6, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2023-09-13T15:00:00Z', - 'precipitation': 0.6, - 'precipitation_probability': 45.0, - 'temperature': 31.0, - 'templow': 22.6, - 'uv_index': 6, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T15:00:00Z', - 'precipitation': 0.0, - 'precipitation_probability': 52.0, - 'temperature': 31.5, - 'templow': 22.4, - 'uv_index': 7, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2023-09-15T15:00:00Z', - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'temperature': 31.8, - 'templow': 23.3, - 'uv_index': 8, - }), - dict({ - 'condition': 'lightning', - 'datetime': '2023-09-16T15:00:00Z', - 'precipitation': 5.3, - 'precipitation_probability': 35.0, - 'temperature': 30.7, - 'templow': 23.2, - 'uv_index': 8, - }), - dict({ - 'condition': 'lightning', - 'datetime': '2023-09-17T15:00:00Z', - 'precipitation': 2.1, - 'precipitation_probability': 49.0, - 'temperature': 28.1, - 'templow': 22.5, - 'uv_index': 6, - }), - ]), - }) -# --- -# name: test_daily_forecast[forecast] - dict({ - 'weather.home': dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-09-08T15:00:00Z', - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'temperature': 28.6, - 'templow': 21.2, - 'uv_index': 6, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2023-09-09T15:00:00Z', - 'precipitation': 3.6, - 'precipitation_probability': 45.0, - 'temperature': 30.6, - 'templow': 21.0, - 'uv_index': 6, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2023-09-10T15:00:00Z', - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'temperature': 30.4, - 'templow': 23.1, - 'uv_index': 6, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2023-09-11T15:00:00Z', - 'precipitation': 0.7, - 'precipitation_probability': 47.0, - 'temperature': 30.4, - 'templow': 23.1, - 'uv_index': 5, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2023-09-12T15:00:00Z', - 'precipitation': 7.7, - 'precipitation_probability': 37.0, - 'temperature': 30.4, - 'templow': 22.1, - 'uv_index': 6, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2023-09-13T15:00:00Z', - 'precipitation': 0.6, - 'precipitation_probability': 45.0, - 'temperature': 31.0, - 'templow': 22.6, - 'uv_index': 6, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T15:00:00Z', - 'precipitation': 0.0, - 'precipitation_probability': 52.0, - 'temperature': 31.5, - 'templow': 22.4, - 'uv_index': 7, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2023-09-15T15:00:00Z', - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'temperature': 31.8, - 'templow': 23.3, - 'uv_index': 8, - }), - dict({ - 'condition': 'lightning', - 'datetime': '2023-09-16T15:00:00Z', - 'precipitation': 5.3, - 'precipitation_probability': 35.0, - 'temperature': 30.7, - 'templow': 23.2, - 'uv_index': 8, - }), - dict({ - 'condition': 'lightning', - 'datetime': '2023-09-17T15:00:00Z', - 'precipitation': 2.1, - 'precipitation_probability': 49.0, - 'temperature': 28.1, - 'templow': 22.5, - 'uv_index': 6, - }), - ]), - }), - }) -# --- -# name: test_daily_forecast[get_forecast] - dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-09-08T15:00:00Z', - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'temperature': 28.6, - 'templow': 21.2, - 'uv_index': 6, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2023-09-09T15:00:00Z', - 'precipitation': 3.6, - 'precipitation_probability': 45.0, - 'temperature': 30.6, - 'templow': 21.0, - 'uv_index': 6, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2023-09-10T15:00:00Z', - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'temperature': 30.4, - 'templow': 23.1, - 'uv_index': 6, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2023-09-11T15:00:00Z', - 'precipitation': 0.7, - 'precipitation_probability': 47.0, - 'temperature': 30.4, - 'templow': 23.1, - 'uv_index': 5, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2023-09-12T15:00:00Z', - 'precipitation': 7.7, - 'precipitation_probability': 37.0, - 'temperature': 30.4, - 'templow': 22.1, - 'uv_index': 6, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2023-09-13T15:00:00Z', - 'precipitation': 0.6, - 'precipitation_probability': 45.0, - 'temperature': 31.0, - 'templow': 22.6, - 'uv_index': 6, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T15:00:00Z', - 'precipitation': 0.0, - 'precipitation_probability': 52.0, - 'temperature': 31.5, - 'templow': 22.4, - 'uv_index': 7, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2023-09-15T15:00:00Z', - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'temperature': 31.8, - 'templow': 23.3, - 'uv_index': 8, - }), - dict({ - 'condition': 'lightning', - 'datetime': '2023-09-16T15:00:00Z', - 'precipitation': 5.3, - 'precipitation_probability': 35.0, - 'temperature': 30.7, - 'templow': 23.2, - 'uv_index': 8, - }), - dict({ - 'condition': 'lightning', - 'datetime': '2023-09-17T15:00:00Z', - 'precipitation': 2.1, - 'precipitation_probability': 49.0, - 'temperature': 28.1, - 'templow': 22.5, - 'uv_index': 6, - }), - ]), - }) -# --- # name: test_daily_forecast[get_forecasts] dict({ 'weather.home': dict({ @@ -387,11978 +97,6 @@ }), }) # --- -# name: test_hourly_forecast - dict({ - 'forecast': list([ - dict({ - 'apparent_temperature': 24.6, - 'cloud_coverage': 79.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T14:00:00Z', - 'dew_point': 21.5, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.24, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 264, - 'wind_gust_speed': 13.44, - 'wind_speed': 6.62, - }), - dict({ - 'apparent_temperature': 24.4, - 'cloud_coverage': 80.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T15:00:00Z', - 'dew_point': 21.4, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.24, - 'temperature': 22.4, - 'uv_index': 0, - 'wind_bearing': 261, - 'wind_gust_speed': 11.91, - 'wind_speed': 6.64, - }), - dict({ - 'apparent_temperature': 23.8, - 'cloud_coverage': 89.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T16:00:00Z', - 'dew_point': 21.1, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.12, - 'temperature': 22.0, - 'uv_index': 0, - 'wind_bearing': 252, - 'wind_gust_speed': 11.15, - 'wind_speed': 6.14, - }), - dict({ - 'apparent_temperature': 23.5, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T17:00:00Z', - 'dew_point': 20.9, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.03, - 'temperature': 21.7, - 'uv_index': 0, - 'wind_bearing': 248, - 'wind_gust_speed': 11.57, - 'wind_speed': 5.95, - }), - dict({ - 'apparent_temperature': 23.3, - 'cloud_coverage': 85.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T18:00:00Z', - 'dew_point': 20.8, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.05, - 'temperature': 21.6, - 'uv_index': 0, - 'wind_bearing': 237, - 'wind_gust_speed': 12.42, - 'wind_speed': 5.86, - }), - dict({ - 'apparent_temperature': 23.0, - 'cloud_coverage': 75.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T19:00:00Z', - 'dew_point': 20.6, - 'humidity': 96, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.03, - 'temperature': 21.3, - 'uv_index': 0, - 'wind_bearing': 224, - 'wind_gust_speed': 11.3, - 'wind_speed': 5.34, - }), - dict({ - 'apparent_temperature': 22.8, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T20:00:00Z', - 'dew_point': 20.4, - 'humidity': 96, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.31, - 'temperature': 21.2, - 'uv_index': 0, - 'wind_bearing': 221, - 'wind_gust_speed': 10.57, - 'wind_speed': 5.13, - }), - dict({ - 'apparent_temperature': 23.1, - 'cloud_coverage': 56.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-08T21:00:00Z', - 'dew_point': 20.5, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.55, - 'temperature': 21.4, - 'uv_index': 0, - 'wind_bearing': 237, - 'wind_gust_speed': 10.63, - 'wind_speed': 5.7, - }), - dict({ - 'apparent_temperature': 24.9, - 'cloud_coverage': 61.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-08T22:00:00Z', - 'dew_point': 21.3, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.79, - 'temperature': 22.8, - 'uv_index': 1, - 'wind_bearing': 258, - 'wind_gust_speed': 10.47, - 'wind_speed': 5.22, - }), - dict({ - 'apparent_temperature': 26.1, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T23:00:00Z', - 'dew_point': 21.3, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.95, - 'temperature': 24.0, - 'uv_index': 2, - 'wind_bearing': 282, - 'wind_gust_speed': 12.74, - 'wind_speed': 5.71, - }), - dict({ - 'apparent_temperature': 27.4, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T00:00:00Z', - 'dew_point': 21.5, - 'humidity': 80, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.35, - 'temperature': 25.1, - 'uv_index': 3, - 'wind_bearing': 294, - 'wind_gust_speed': 13.87, - 'wind_speed': 6.53, - }), - dict({ - 'apparent_temperature': 29.0, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T01:00:00Z', - 'dew_point': 21.8, - 'humidity': 75, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.48, - 'temperature': 26.5, - 'uv_index': 5, - 'wind_bearing': 308, - 'wind_gust_speed': 16.04, - 'wind_speed': 6.54, - }), - dict({ - 'apparent_temperature': 30.3, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T02:00:00Z', - 'dew_point': 22.0, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.23, - 'temperature': 27.6, - 'uv_index': 6, - 'wind_bearing': 314, - 'wind_gust_speed': 18.1, - 'wind_speed': 7.32, - }), - dict({ - 'apparent_temperature': 31.1, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T03:00:00Z', - 'dew_point': 22.1, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.86, - 'temperature': 28.3, - 'uv_index': 6, - 'wind_bearing': 317, - 'wind_gust_speed': 20.77, - 'wind_speed': 9.1, - }), - dict({ - 'apparent_temperature': 31.5, - 'cloud_coverage': 69.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T04:00:00Z', - 'dew_point': 22.1, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.65, - 'temperature': 28.6, - 'uv_index': 6, - 'wind_bearing': 311, - 'wind_gust_speed': 21.27, - 'wind_speed': 10.21, - }), - dict({ - 'apparent_temperature': 31.3, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T05:00:00Z', - 'dew_point': 22.1, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.48, - 'temperature': 28.4, - 'uv_index': 5, - 'wind_bearing': 317, - 'wind_gust_speed': 19.62, - 'wind_speed': 10.53, - }), - dict({ - 'apparent_temperature': 30.8, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T06:00:00Z', - 'dew_point': 22.2, - 'humidity': 71, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.54, - 'temperature': 27.9, - 'uv_index': 3, - 'wind_bearing': 335, - 'wind_gust_speed': 18.98, - 'wind_speed': 8.63, - }), - dict({ - 'apparent_temperature': 29.9, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T07:00:00Z', - 'dew_point': 22.2, - 'humidity': 74, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.76, - 'temperature': 27.1, - 'uv_index': 2, - 'wind_bearing': 338, - 'wind_gust_speed': 17.04, - 'wind_speed': 7.75, - }), - dict({ - 'apparent_temperature': 29.1, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T08:00:00Z', - 'dew_point': 22.1, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.05, - 'temperature': 26.4, - 'uv_index': 0, - 'wind_bearing': 342, - 'wind_gust_speed': 14.75, - 'wind_speed': 6.26, - }), - dict({ - 'apparent_temperature': 27.9, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T09:00:00Z', - 'dew_point': 22.0, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.38, - 'temperature': 25.4, - 'uv_index': 0, - 'wind_bearing': 344, - 'wind_gust_speed': 10.43, - 'wind_speed': 5.2, - }), - dict({ - 'apparent_temperature': 26.9, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T10:00:00Z', - 'dew_point': 21.9, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.73, - 'temperature': 24.5, - 'uv_index': 0, - 'wind_bearing': 339, - 'wind_gust_speed': 6.95, - 'wind_speed': 3.59, - }), - dict({ - 'apparent_temperature': 26.4, - 'cloud_coverage': 51.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T11:00:00Z', - 'dew_point': 21.8, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.3, - 'temperature': 24.1, - 'uv_index': 0, - 'wind_bearing': 326, - 'wind_gust_speed': 5.27, - 'wind_speed': 2.1, - }), - dict({ - 'apparent_temperature': 26.1, - 'cloud_coverage': 53.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T12:00:00Z', - 'dew_point': 21.8, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.52, - 'temperature': 23.9, - 'uv_index': 0, - 'wind_bearing': 257, - 'wind_gust_speed': 5.48, - 'wind_speed': 0.93, - }), - dict({ - 'apparent_temperature': 25.8, - 'cloud_coverage': 56.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T13:00:00Z', - 'dew_point': 21.8, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.53, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 188, - 'wind_gust_speed': 4.44, - 'wind_speed': 1.79, - }), - dict({ - 'apparent_temperature': 25.3, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T14:00:00Z', - 'dew_point': 21.7, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.46, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 183, - 'wind_gust_speed': 4.49, - 'wind_speed': 2.19, - }), - dict({ - 'apparent_temperature': 24.6, - 'cloud_coverage': 45.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T15:00:00Z', - 'dew_point': 21.4, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.21, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 179, - 'wind_gust_speed': 5.32, - 'wind_speed': 2.65, - }), - dict({ - 'apparent_temperature': 24.0, - 'cloud_coverage': 42.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T16:00:00Z', - 'dew_point': 21.1, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.09, - 'temperature': 22.1, - 'uv_index': 0, - 'wind_bearing': 173, - 'wind_gust_speed': 5.81, - 'wind_speed': 3.2, - }), - dict({ - 'apparent_temperature': 23.7, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T17:00:00Z', - 'dew_point': 20.9, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.88, - 'temperature': 21.9, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 5.53, - 'wind_speed': 3.16, - }), - dict({ - 'apparent_temperature': 23.3, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T18:00:00Z', - 'dew_point': 20.7, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.94, - 'temperature': 21.6, - 'uv_index': 0, - 'wind_bearing': 153, - 'wind_gust_speed': 6.09, - 'wind_speed': 3.36, - }), - dict({ - 'apparent_temperature': 23.1, - 'cloud_coverage': 51.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T19:00:00Z', - 'dew_point': 20.5, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.96, - 'temperature': 21.4, - 'uv_index': 0, - 'wind_bearing': 150, - 'wind_gust_speed': 6.83, - 'wind_speed': 3.71, - }), - dict({ - 'apparent_temperature': 22.5, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T20:00:00Z', - 'dew_point': 20.0, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.29, - 'temperature': 21.0, - 'uv_index': 0, - 'wind_bearing': 156, - 'wind_gust_speed': 7.98, - 'wind_speed': 4.27, - }), - dict({ - 'apparent_temperature': 22.8, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T21:00:00Z', - 'dew_point': 20.2, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.61, - 'temperature': 21.2, - 'uv_index': 0, - 'wind_bearing': 156, - 'wind_gust_speed': 8.4, - 'wind_speed': 4.69, - }), - dict({ - 'apparent_temperature': 25.1, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T22:00:00Z', - 'dew_point': 21.3, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.87, - 'temperature': 23.1, - 'uv_index': 1, - 'wind_bearing': 150, - 'wind_gust_speed': 7.66, - 'wind_speed': 4.33, - }), - dict({ - 'apparent_temperature': 28.3, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T23:00:00Z', - 'dew_point': 22.3, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.93, - 'temperature': 25.6, - 'uv_index': 2, - 'wind_bearing': 123, - 'wind_gust_speed': 9.63, - 'wind_speed': 3.91, - }), - dict({ - 'apparent_temperature': 30.4, - 'cloud_coverage': 63.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T00:00:00Z', - 'dew_point': 22.6, - 'humidity': 75, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.93, - 'temperature': 27.4, - 'uv_index': 4, - 'wind_bearing': 105, - 'wind_gust_speed': 12.59, - 'wind_speed': 3.96, - }), - dict({ - 'apparent_temperature': 32.2, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T01:00:00Z', - 'dew_point': 22.9, - 'humidity': 70, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.79, - 'temperature': 28.9, - 'uv_index': 5, - 'wind_bearing': 99, - 'wind_gust_speed': 14.17, - 'wind_speed': 4.06, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 62.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-10T02:00:00Z', - 'dew_point': 22.9, - 'humidity': 66, - 'precipitation': 0.3, - 'precipitation_probability': 7.000000000000001, - 'pressure': 1011.29, - 'temperature': 29.9, - 'uv_index': 6, - 'wind_bearing': 93, - 'wind_gust_speed': 17.75, - 'wind_speed': 4.87, - }), - dict({ - 'apparent_temperature': 34.3, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T03:00:00Z', - 'dew_point': 23.1, - 'humidity': 64, - 'precipitation': 0.3, - 'precipitation_probability': 11.0, - 'pressure': 1010.78, - 'temperature': 30.6, - 'uv_index': 6, - 'wind_bearing': 78, - 'wind_gust_speed': 17.43, - 'wind_speed': 4.54, - }), - dict({ - 'apparent_temperature': 34.0, - 'cloud_coverage': 74.0, - 'condition': 'rainy', - 'datetime': '2023-09-10T04:00:00Z', - 'dew_point': 23.2, - 'humidity': 66, - 'precipitation': 0.4, - 'precipitation_probability': 15.0, - 'pressure': 1010.37, - 'temperature': 30.3, - 'uv_index': 5, - 'wind_bearing': 60, - 'wind_gust_speed': 15.24, - 'wind_speed': 4.9, - }), - dict({ - 'apparent_temperature': 33.7, - 'cloud_coverage': 79.0, - 'condition': 'rainy', - 'datetime': '2023-09-10T05:00:00Z', - 'dew_point': 23.3, - 'humidity': 67, - 'precipitation': 0.7, - 'precipitation_probability': 17.0, - 'pressure': 1010.09, - 'temperature': 30.0, - 'uv_index': 4, - 'wind_bearing': 80, - 'wind_gust_speed': 13.53, - 'wind_speed': 5.98, - }), - dict({ - 'apparent_temperature': 33.2, - 'cloud_coverage': 80.0, - 'condition': 'rainy', - 'datetime': '2023-09-10T06:00:00Z', - 'dew_point': 23.4, - 'humidity': 70, - 'precipitation': 1.0, - 'precipitation_probability': 17.0, - 'pressure': 1010.0, - 'temperature': 29.5, - 'uv_index': 3, - 'wind_bearing': 83, - 'wind_gust_speed': 12.55, - 'wind_speed': 6.84, - }), - dict({ - 'apparent_temperature': 32.3, - 'cloud_coverage': 88.0, - 'condition': 'rainy', - 'datetime': '2023-09-10T07:00:00Z', - 'dew_point': 23.4, - 'humidity': 73, - 'precipitation': 0.4, - 'precipitation_probability': 16.0, - 'pressure': 1010.27, - 'temperature': 28.7, - 'uv_index': 2, - 'wind_bearing': 90, - 'wind_gust_speed': 10.16, - 'wind_speed': 6.07, - }), - dict({ - 'apparent_temperature': 30.9, - 'cloud_coverage': 92.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T08:00:00Z', - 'dew_point': 23.2, - 'humidity': 77, - 'precipitation': 0.5, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1010.71, - 'temperature': 27.6, - 'uv_index': 0, - 'wind_bearing': 101, - 'wind_gust_speed': 8.18, - 'wind_speed': 4.82, - }), - dict({ - 'apparent_temperature': 29.7, - 'cloud_coverage': 93.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T09:00:00Z', - 'dew_point': 23.2, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.9, - 'temperature': 26.5, - 'uv_index': 0, - 'wind_bearing': 128, - 'wind_gust_speed': 8.89, - 'wind_speed': 4.95, - }), - dict({ - 'apparent_temperature': 28.6, - 'cloud_coverage': 88.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T10:00:00Z', - 'dew_point': 23.0, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.12, - 'temperature': 25.6, - 'uv_index': 0, - 'wind_bearing': 134, - 'wind_gust_speed': 10.03, - 'wind_speed': 4.52, - }), - dict({ - 'apparent_temperature': 27.9, - 'cloud_coverage': 87.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T11:00:00Z', - 'dew_point': 22.8, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.43, - 'temperature': 25.1, - 'uv_index': 0, - 'wind_bearing': 137, - 'wind_gust_speed': 12.4, - 'wind_speed': 5.41, - }), - dict({ - 'apparent_temperature': 27.4, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T12:00:00Z', - 'dew_point': 22.5, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.58, - 'temperature': 24.8, - 'uv_index': 0, - 'wind_bearing': 143, - 'wind_gust_speed': 16.36, - 'wind_speed': 6.31, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T13:00:00Z', - 'dew_point': 22.4, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.55, - 'temperature': 24.5, - 'uv_index': 0, - 'wind_bearing': 144, - 'wind_gust_speed': 19.66, - 'wind_speed': 7.23, - }), - dict({ - 'apparent_temperature': 26.8, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T14:00:00Z', - 'dew_point': 22.2, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.4, - 'temperature': 24.3, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 21.15, - 'wind_speed': 7.46, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T15:00:00Z', - 'dew_point': 22.0, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.23, - 'temperature': 23.9, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 22.26, - 'wind_speed': 7.84, - }), - dict({ - 'apparent_temperature': 26.1, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T16:00:00Z', - 'dew_point': 21.8, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.01, - 'temperature': 23.8, - 'uv_index': 0, - 'wind_bearing': 144, - 'wind_gust_speed': 23.53, - 'wind_speed': 8.63, - }), - dict({ - 'apparent_temperature': 25.6, - 'cloud_coverage': 61.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-10T17:00:00Z', - 'dew_point': 21.6, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.78, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 144, - 'wind_gust_speed': 22.83, - 'wind_speed': 8.61, - }), - dict({ - 'apparent_temperature': 25.4, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T18:00:00Z', - 'dew_point': 21.5, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.69, - 'temperature': 23.3, - 'uv_index': 0, - 'wind_bearing': 143, - 'wind_gust_speed': 23.7, - 'wind_speed': 8.7, - }), - dict({ - 'apparent_temperature': 25.2, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T19:00:00Z', - 'dew_point': 21.4, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.77, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 140, - 'wind_gust_speed': 24.24, - 'wind_speed': 8.74, - }), - dict({ - 'apparent_temperature': 25.5, - 'cloud_coverage': 89.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T20:00:00Z', - 'dew_point': 21.6, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.89, - 'temperature': 23.3, - 'uv_index': 0, - 'wind_bearing': 138, - 'wind_gust_speed': 23.99, - 'wind_speed': 8.81, - }), - dict({ - 'apparent_temperature': 25.9, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T21:00:00Z', - 'dew_point': 21.6, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.1, - 'temperature': 23.7, - 'uv_index': 0, - 'wind_bearing': 138, - 'wind_gust_speed': 25.55, - 'wind_speed': 9.05, - }), - dict({ - 'apparent_temperature': 27.0, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T22:00:00Z', - 'dew_point': 21.8, - 'humidity': 84, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.29, - 'temperature': 24.6, - 'uv_index': 1, - 'wind_bearing': 140, - 'wind_gust_speed': 29.08, - 'wind_speed': 10.37, - }), - dict({ - 'apparent_temperature': 28.4, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T23:00:00Z', - 'dew_point': 21.9, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.36, - 'temperature': 25.9, - 'uv_index': 2, - 'wind_bearing': 140, - 'wind_gust_speed': 34.13, - 'wind_speed': 12.56, - }), - dict({ - 'apparent_temperature': 30.1, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T00:00:00Z', - 'dew_point': 22.3, - 'humidity': 74, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.39, - 'temperature': 27.2, - 'uv_index': 3, - 'wind_bearing': 140, - 'wind_gust_speed': 38.2, - 'wind_speed': 15.65, - }), - dict({ - 'apparent_temperature': 31.4, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-11T01:00:00Z', - 'dew_point': 22.3, - 'humidity': 70, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.31, - 'temperature': 28.4, - 'uv_index': 5, - 'wind_bearing': 141, - 'wind_gust_speed': 37.55, - 'wind_speed': 15.78, - }), - dict({ - 'apparent_temperature': 32.7, - 'cloud_coverage': 63.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T02:00:00Z', - 'dew_point': 22.4, - 'humidity': 66, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.98, - 'temperature': 29.6, - 'uv_index': 6, - 'wind_bearing': 143, - 'wind_gust_speed': 35.86, - 'wind_speed': 15.41, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T03:00:00Z', - 'dew_point': 22.5, - 'humidity': 63, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.61, - 'temperature': 30.3, - 'uv_index': 6, - 'wind_bearing': 141, - 'wind_gust_speed': 35.88, - 'wind_speed': 15.51, - }), - dict({ - 'apparent_temperature': 33.8, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T04:00:00Z', - 'dew_point': 22.6, - 'humidity': 63, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.36, - 'temperature': 30.4, - 'uv_index': 5, - 'wind_bearing': 140, - 'wind_gust_speed': 35.99, - 'wind_speed': 15.75, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T05:00:00Z', - 'dew_point': 22.6, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.11, - 'temperature': 30.1, - 'uv_index': 4, - 'wind_bearing': 137, - 'wind_gust_speed': 33.61, - 'wind_speed': 15.36, - }), - dict({ - 'apparent_temperature': 33.2, - 'cloud_coverage': 77.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T06:00:00Z', - 'dew_point': 22.5, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.98, - 'temperature': 30.0, - 'uv_index': 3, - 'wind_bearing': 138, - 'wind_gust_speed': 32.61, - 'wind_speed': 14.98, - }), - dict({ - 'apparent_temperature': 32.3, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T07:00:00Z', - 'dew_point': 22.2, - 'humidity': 66, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.13, - 'temperature': 29.2, - 'uv_index': 2, - 'wind_bearing': 138, - 'wind_gust_speed': 28.1, - 'wind_speed': 13.88, - }), - dict({ - 'apparent_temperature': 31.2, - 'cloud_coverage': 56.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-11T08:00:00Z', - 'dew_point': 22.1, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.48, - 'temperature': 28.3, - 'uv_index': 0, - 'wind_bearing': 137, - 'wind_gust_speed': 24.22, - 'wind_speed': 13.02, - }), - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 55.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-11T09:00:00Z', - 'dew_point': 21.9, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.81, - 'temperature': 27.1, - 'uv_index': 0, - 'wind_bearing': 138, - 'wind_gust_speed': 22.5, - 'wind_speed': 11.94, - }), - dict({ - 'apparent_temperature': 28.8, - 'cloud_coverage': 63.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T10:00:00Z', - 'dew_point': 21.7, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.29, - 'temperature': 26.3, - 'uv_index': 0, - 'wind_bearing': 137, - 'wind_gust_speed': 21.47, - 'wind_speed': 11.25, - }), - dict({ - 'apparent_temperature': 28.1, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T11:00:00Z', - 'dew_point': 21.8, - 'humidity': 80, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.77, - 'temperature': 25.6, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 22.71, - 'wind_speed': 12.39, - }), - dict({ - 'apparent_temperature': 27.6, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T12:00:00Z', - 'dew_point': 21.8, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.97, - 'temperature': 25.2, - 'uv_index': 0, - 'wind_bearing': 143, - 'wind_gust_speed': 23.67, - 'wind_speed': 12.83, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 89.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T13:00:00Z', - 'dew_point': 21.7, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.97, - 'temperature': 24.7, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 23.34, - 'wind_speed': 12.62, - }), - dict({ - 'apparent_temperature': 26.7, - 'cloud_coverage': 88.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T14:00:00Z', - 'dew_point': 21.7, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.83, - 'temperature': 24.4, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 22.9, - 'wind_speed': 12.07, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 90.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T15:00:00Z', - 'dew_point': 21.6, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.74, - 'temperature': 24.1, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 22.01, - 'wind_speed': 11.19, - }), - dict({ - 'apparent_temperature': 25.9, - 'cloud_coverage': 88.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T16:00:00Z', - 'dew_point': 21.6, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.56, - 'temperature': 23.7, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 21.29, - 'wind_speed': 10.97, - }), - dict({ - 'apparent_temperature': 25.8, - 'cloud_coverage': 85.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T17:00:00Z', - 'dew_point': 21.5, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.35, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 150, - 'wind_gust_speed': 20.52, - 'wind_speed': 10.5, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T18:00:00Z', - 'dew_point': 21.4, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.3, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 20.04, - 'wind_speed': 10.51, - }), - dict({ - 'apparent_temperature': 25.4, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T19:00:00Z', - 'dew_point': 21.3, - 'humidity': 88, - 'precipitation': 0.3, - 'precipitation_probability': 12.0, - 'pressure': 1011.37, - 'temperature': 23.4, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 18.07, - 'wind_speed': 10.13, - }), - dict({ - 'apparent_temperature': 25.2, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T20:00:00Z', - 'dew_point': 21.2, - 'humidity': 89, - 'precipitation': 0.2, - 'precipitation_probability': 13.0, - 'pressure': 1011.53, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 16.86, - 'wind_speed': 10.34, - }), - dict({ - 'apparent_temperature': 25.5, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T21:00:00Z', - 'dew_point': 21.4, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.71, - 'temperature': 23.4, - 'uv_index': 0, - 'wind_bearing': 138, - 'wind_gust_speed': 16.66, - 'wind_speed': 10.68, - }), - dict({ - 'apparent_temperature': 26.8, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T22:00:00Z', - 'dew_point': 21.9, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.94, - 'temperature': 24.4, - 'uv_index': 1, - 'wind_bearing': 137, - 'wind_gust_speed': 17.21, - 'wind_speed': 10.61, - }), - dict({ - 'apparent_temperature': 28.2, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T23:00:00Z', - 'dew_point': 22.3, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.05, - 'temperature': 25.6, - 'uv_index': 2, - 'wind_bearing': 138, - 'wind_gust_speed': 19.23, - 'wind_speed': 11.13, - }), - dict({ - 'apparent_temperature': 29.5, - 'cloud_coverage': 79.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T00:00:00Z', - 'dew_point': 22.6, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.07, - 'temperature': 26.6, - 'uv_index': 3, - 'wind_bearing': 140, - 'wind_gust_speed': 20.61, - 'wind_speed': 11.13, - }), - dict({ - 'apparent_temperature': 31.2, - 'cloud_coverage': 82.0, - 'condition': 'rainy', - 'datetime': '2023-09-12T01:00:00Z', - 'dew_point': 23.1, - 'humidity': 75, - 'precipitation': 0.2, - 'precipitation_probability': 16.0, - 'pressure': 1011.89, - 'temperature': 27.9, - 'uv_index': 4, - 'wind_bearing': 141, - 'wind_gust_speed': 23.35, - 'wind_speed': 11.98, - }), - dict({ - 'apparent_temperature': 32.6, - 'cloud_coverage': 85.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T02:00:00Z', - 'dew_point': 23.5, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.53, - 'temperature': 29.0, - 'uv_index': 5, - 'wind_bearing': 143, - 'wind_gust_speed': 26.45, - 'wind_speed': 13.01, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T03:00:00Z', - 'dew_point': 23.5, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.15, - 'temperature': 29.8, - 'uv_index': 5, - 'wind_bearing': 141, - 'wind_gust_speed': 28.95, - 'wind_speed': 13.9, - }), - dict({ - 'apparent_temperature': 34.0, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T04:00:00Z', - 'dew_point': 23.4, - 'humidity': 67, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.79, - 'temperature': 30.2, - 'uv_index': 5, - 'wind_bearing': 141, - 'wind_gust_speed': 27.9, - 'wind_speed': 13.95, - }), - dict({ - 'apparent_temperature': 34.0, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T05:00:00Z', - 'dew_point': 23.1, - 'humidity': 65, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.43, - 'temperature': 30.4, - 'uv_index': 4, - 'wind_bearing': 140, - 'wind_gust_speed': 26.53, - 'wind_speed': 13.78, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 56.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T06:00:00Z', - 'dew_point': 22.6, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.21, - 'temperature': 30.1, - 'uv_index': 3, - 'wind_bearing': 138, - 'wind_gust_speed': 24.56, - 'wind_speed': 13.74, - }), - dict({ - 'apparent_temperature': 32.0, - 'cloud_coverage': 53.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T07:00:00Z', - 'dew_point': 22.1, - 'humidity': 66, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.26, - 'temperature': 29.1, - 'uv_index': 2, - 'wind_bearing': 138, - 'wind_gust_speed': 22.78, - 'wind_speed': 13.21, - }), - dict({ - 'apparent_temperature': 30.9, - 'cloud_coverage': 48.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T08:00:00Z', - 'dew_point': 21.9, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.51, - 'temperature': 28.1, - 'uv_index': 0, - 'wind_bearing': 140, - 'wind_gust_speed': 19.92, - 'wind_speed': 12.0, - }), - dict({ - 'apparent_temperature': 29.7, - 'cloud_coverage': 50.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T09:00:00Z', - 'dew_point': 21.7, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.8, - 'temperature': 27.2, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 17.65, - 'wind_speed': 10.97, - }), - dict({ - 'apparent_temperature': 28.6, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T10:00:00Z', - 'dew_point': 21.4, - 'humidity': 75, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.23, - 'temperature': 26.2, - 'uv_index': 0, - 'wind_bearing': 143, - 'wind_gust_speed': 15.87, - 'wind_speed': 10.23, - }), - dict({ - 'apparent_temperature': 27.6, - 'cloud_coverage': 56.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T11:00:00Z', - 'dew_point': 21.3, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1011.79, - 'temperature': 25.4, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 13.9, - 'wind_speed': 9.39, - }), - dict({ - 'apparent_temperature': 26.8, - 'cloud_coverage': 60.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T12:00:00Z', - 'dew_point': 21.2, - 'humidity': 81, - 'precipitation': 0.0, - 'precipitation_probability': 47.0, - 'pressure': 1012.12, - 'temperature': 24.7, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 13.32, - 'wind_speed': 8.9, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T13:00:00Z', - 'dew_point': 21.2, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1012.18, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 13.18, - 'wind_speed': 8.59, - }), - dict({ - 'apparent_temperature': 26.0, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T14:00:00Z', - 'dew_point': 21.3, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.09, - 'temperature': 23.9, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 13.84, - 'wind_speed': 8.87, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T15:00:00Z', - 'dew_point': 21.3, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.99, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 15.08, - 'wind_speed': 8.93, - }), - dict({ - 'apparent_temperature': 25.1, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T16:00:00Z', - 'dew_point': 21.0, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.93, - 'temperature': 23.2, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 16.74, - 'wind_speed': 9.49, - }), - dict({ - 'apparent_temperature': 24.7, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T17:00:00Z', - 'dew_point': 20.8, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.75, - 'temperature': 22.9, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 17.45, - 'wind_speed': 9.12, - }), - dict({ - 'apparent_temperature': 24.4, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T18:00:00Z', - 'dew_point': 20.7, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.77, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 17.04, - 'wind_speed': 8.68, - }), - dict({ - 'apparent_temperature': 24.1, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T19:00:00Z', - 'dew_point': 20.6, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.93, - 'temperature': 22.4, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 16.8, - 'wind_speed': 8.61, - }), - dict({ - 'apparent_temperature': 23.9, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T20:00:00Z', - 'dew_point': 20.5, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.23, - 'temperature': 22.1, - 'uv_index': 0, - 'wind_bearing': 150, - 'wind_gust_speed': 15.35, - 'wind_speed': 8.36, - }), - dict({ - 'apparent_temperature': 24.4, - 'cloud_coverage': 75.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T21:00:00Z', - 'dew_point': 20.6, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.49, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 155, - 'wind_gust_speed': 14.09, - 'wind_speed': 7.77, - }), - dict({ - 'apparent_temperature': 25.8, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T22:00:00Z', - 'dew_point': 21.0, - 'humidity': 84, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.72, - 'temperature': 23.8, - 'uv_index': 1, - 'wind_bearing': 152, - 'wind_gust_speed': 14.04, - 'wind_speed': 7.25, - }), - dict({ - 'apparent_temperature': 27.8, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T23:00:00Z', - 'dew_point': 21.4, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.85, - 'temperature': 25.5, - 'uv_index': 2, - 'wind_bearing': 149, - 'wind_gust_speed': 15.31, - 'wind_speed': 7.14, - }), - dict({ - 'apparent_temperature': 29.7, - 'cloud_coverage': 60.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-13T00:00:00Z', - 'dew_point': 21.8, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.89, - 'temperature': 27.1, - 'uv_index': 4, - 'wind_bearing': 141, - 'wind_gust_speed': 16.42, - 'wind_speed': 6.89, - }), - dict({ - 'apparent_temperature': 31.2, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T01:00:00Z', - 'dew_point': 22.0, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.65, - 'temperature': 28.4, - 'uv_index': 5, - 'wind_bearing': 137, - 'wind_gust_speed': 18.64, - 'wind_speed': 6.65, - }), - dict({ - 'apparent_temperature': 32.3, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T02:00:00Z', - 'dew_point': 21.9, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.26, - 'temperature': 29.4, - 'uv_index': 5, - 'wind_bearing': 128, - 'wind_gust_speed': 21.69, - 'wind_speed': 7.12, - }), - dict({ - 'apparent_temperature': 33.0, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T03:00:00Z', - 'dew_point': 21.9, - 'humidity': 62, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.88, - 'temperature': 30.1, - 'uv_index': 6, - 'wind_bearing': 111, - 'wind_gust_speed': 23.41, - 'wind_speed': 7.33, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 72.0, - 'condition': 'rainy', - 'datetime': '2023-09-13T04:00:00Z', - 'dew_point': 22.0, - 'humidity': 61, - 'precipitation': 0.9, - 'precipitation_probability': 12.0, - 'pressure': 1011.55, - 'temperature': 30.4, - 'uv_index': 5, - 'wind_bearing': 56, - 'wind_gust_speed': 23.1, - 'wind_speed': 8.09, - }), - dict({ - 'apparent_temperature': 33.2, - 'cloud_coverage': 72.0, - 'condition': 'rainy', - 'datetime': '2023-09-13T05:00:00Z', - 'dew_point': 21.9, - 'humidity': 61, - 'precipitation': 1.9, - 'precipitation_probability': 12.0, - 'pressure': 1011.29, - 'temperature': 30.2, - 'uv_index': 4, - 'wind_bearing': 20, - 'wind_gust_speed': 21.81, - 'wind_speed': 9.46, - }), - dict({ - 'apparent_temperature': 32.6, - 'cloud_coverage': 74.0, - 'condition': 'rainy', - 'datetime': '2023-09-13T06:00:00Z', - 'dew_point': 21.9, - 'humidity': 63, - 'precipitation': 2.3, - 'precipitation_probability': 11.0, - 'pressure': 1011.17, - 'temperature': 29.7, - 'uv_index': 3, - 'wind_bearing': 20, - 'wind_gust_speed': 19.72, - 'wind_speed': 9.8, - }), - dict({ - 'apparent_temperature': 31.8, - 'cloud_coverage': 69.0, - 'condition': 'rainy', - 'datetime': '2023-09-13T07:00:00Z', - 'dew_point': 22.4, - 'humidity': 68, - 'precipitation': 1.8, - 'precipitation_probability': 10.0, - 'pressure': 1011.32, - 'temperature': 28.8, - 'uv_index': 1, - 'wind_bearing': 18, - 'wind_gust_speed': 17.55, - 'wind_speed': 9.23, - }), - dict({ - 'apparent_temperature': 30.8, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T08:00:00Z', - 'dew_point': 22.9, - 'humidity': 76, - 'precipitation': 0.8, - 'precipitation_probability': 10.0, - 'pressure': 1011.6, - 'temperature': 27.6, - 'uv_index': 0, - 'wind_bearing': 27, - 'wind_gust_speed': 15.08, - 'wind_speed': 8.05, - }), - dict({ - 'apparent_temperature': 29.4, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T09:00:00Z', - 'dew_point': 23.0, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.94, - 'temperature': 26.3, - 'uv_index': 0, - 'wind_bearing': 32, - 'wind_gust_speed': 12.17, - 'wind_speed': 6.68, - }), - dict({ - 'apparent_temperature': 28.5, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T10:00:00Z', - 'dew_point': 22.9, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.3, - 'temperature': 25.5, - 'uv_index': 0, - 'wind_bearing': 69, - 'wind_gust_speed': 11.64, - 'wind_speed': 6.69, - }), - dict({ - 'apparent_temperature': 27.7, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T11:00:00Z', - 'dew_point': 22.6, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.71, - 'temperature': 25.0, - 'uv_index': 0, - 'wind_bearing': 155, - 'wind_gust_speed': 11.91, - 'wind_speed': 6.23, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T12:00:00Z', - 'dew_point': 22.3, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.96, - 'temperature': 24.5, - 'uv_index': 0, - 'wind_bearing': 161, - 'wind_gust_speed': 12.47, - 'wind_speed': 5.73, - }), - dict({ - 'apparent_temperature': 26.7, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T13:00:00Z', - 'dew_point': 22.3, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.03, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 161, - 'wind_gust_speed': 13.57, - 'wind_speed': 5.66, - }), - dict({ - 'apparent_temperature': 26.4, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T14:00:00Z', - 'dew_point': 22.2, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.99, - 'temperature': 23.9, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 15.07, - 'wind_speed': 5.83, - }), - dict({ - 'apparent_temperature': 26.1, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T15:00:00Z', - 'dew_point': 22.2, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.95, - 'temperature': 23.7, - 'uv_index': 0, - 'wind_bearing': 158, - 'wind_gust_speed': 16.06, - 'wind_speed': 5.93, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 88.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T16:00:00Z', - 'dew_point': 22.0, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.9, - 'temperature': 23.4, - 'uv_index': 0, - 'wind_bearing': 153, - 'wind_gust_speed': 16.05, - 'wind_speed': 5.75, - }), - dict({ - 'apparent_temperature': 25.4, - 'cloud_coverage': 90.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T17:00:00Z', - 'dew_point': 21.8, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.85, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 150, - 'wind_gust_speed': 15.52, - 'wind_speed': 5.49, - }), - dict({ - 'apparent_temperature': 25.2, - 'cloud_coverage': 92.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T18:00:00Z', - 'dew_point': 21.8, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.87, - 'temperature': 23.0, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 15.01, - 'wind_speed': 5.32, - }), - dict({ - 'apparent_temperature': 25.0, - 'cloud_coverage': 90.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T19:00:00Z', - 'dew_point': 21.7, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.01, - 'temperature': 22.8, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 14.39, - 'wind_speed': 5.33, - }), - dict({ - 'apparent_temperature': 24.8, - 'cloud_coverage': 89.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T20:00:00Z', - 'dew_point': 21.6, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.22, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 13.79, - 'wind_speed': 5.43, - }), - dict({ - 'apparent_temperature': 25.3, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T21:00:00Z', - 'dew_point': 21.8, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.41, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 14.12, - 'wind_speed': 5.52, - }), - dict({ - 'apparent_temperature': 26.7, - 'cloud_coverage': 77.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T22:00:00Z', - 'dew_point': 22.1, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.59, - 'temperature': 24.3, - 'uv_index': 1, - 'wind_bearing': 147, - 'wind_gust_speed': 16.14, - 'wind_speed': 5.58, - }), - dict({ - 'apparent_temperature': 28.4, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T23:00:00Z', - 'dew_point': 22.4, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.74, - 'temperature': 25.7, - 'uv_index': 2, - 'wind_bearing': 146, - 'wind_gust_speed': 19.09, - 'wind_speed': 5.62, - }), - dict({ - 'apparent_temperature': 30.5, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T00:00:00Z', - 'dew_point': 22.9, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.78, - 'temperature': 27.4, - 'uv_index': 4, - 'wind_bearing': 143, - 'wind_gust_speed': 21.6, - 'wind_speed': 5.58, - }), - dict({ - 'apparent_temperature': 32.2, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T01:00:00Z', - 'dew_point': 23.2, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.61, - 'temperature': 28.7, - 'uv_index': 5, - 'wind_bearing': 138, - 'wind_gust_speed': 23.36, - 'wind_speed': 5.34, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T02:00:00Z', - 'dew_point': 23.2, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.32, - 'temperature': 29.9, - 'uv_index': 6, - 'wind_bearing': 111, - 'wind_gust_speed': 24.72, - 'wind_speed': 4.99, - }), - dict({ - 'apparent_temperature': 34.4, - 'cloud_coverage': 56.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T03:00:00Z', - 'dew_point': 23.3, - 'humidity': 65, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.04, - 'temperature': 30.7, - 'uv_index': 6, - 'wind_bearing': 354, - 'wind_gust_speed': 25.23, - 'wind_speed': 4.74, - }), - dict({ - 'apparent_temperature': 34.9, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T04:00:00Z', - 'dew_point': 23.4, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.77, - 'temperature': 31.0, - 'uv_index': 6, - 'wind_bearing': 341, - 'wind_gust_speed': 24.6, - 'wind_speed': 4.79, - }), - dict({ - 'apparent_temperature': 34.5, - 'cloud_coverage': 60.0, - 'condition': 'rainy', - 'datetime': '2023-09-14T05:00:00Z', - 'dew_point': 23.2, - 'humidity': 64, - 'precipitation': 0.2, - 'precipitation_probability': 15.0, - 'pressure': 1012.53, - 'temperature': 30.7, - 'uv_index': 5, - 'wind_bearing': 336, - 'wind_gust_speed': 23.28, - 'wind_speed': 5.07, - }), - dict({ - 'apparent_temperature': 33.8, - 'cloud_coverage': 59.0, - 'condition': 'rainy', - 'datetime': '2023-09-14T06:00:00Z', - 'dew_point': 23.1, - 'humidity': 66, - 'precipitation': 0.2, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1012.49, - 'temperature': 30.2, - 'uv_index': 3, - 'wind_bearing': 336, - 'wind_gust_speed': 22.05, - 'wind_speed': 5.34, - }), - dict({ - 'apparent_temperature': 32.9, - 'cloud_coverage': 53.0, - 'condition': 'rainy', - 'datetime': '2023-09-14T07:00:00Z', - 'dew_point': 23.0, - 'humidity': 68, - 'precipitation': 0.2, - 'precipitation_probability': 40.0, - 'pressure': 1012.73, - 'temperature': 29.5, - 'uv_index': 2, - 'wind_bearing': 339, - 'wind_gust_speed': 21.18, - 'wind_speed': 5.63, - }), - dict({ - 'apparent_temperature': 31.6, - 'cloud_coverage': 43.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T08:00:00Z', - 'dew_point': 22.8, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 45.0, - 'pressure': 1013.16, - 'temperature': 28.4, - 'uv_index': 0, - 'wind_bearing': 342, - 'wind_gust_speed': 20.35, - 'wind_speed': 5.93, - }), - dict({ - 'apparent_temperature': 30.0, - 'cloud_coverage': 35.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T09:00:00Z', - 'dew_point': 22.5, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1013.62, - 'temperature': 27.1, - 'uv_index': 0, - 'wind_bearing': 347, - 'wind_gust_speed': 19.42, - 'wind_speed': 5.95, - }), - dict({ - 'apparent_temperature': 29.0, - 'cloud_coverage': 32.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T10:00:00Z', - 'dew_point': 22.4, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.09, - 'temperature': 26.3, - 'uv_index': 0, - 'wind_bearing': 348, - 'wind_gust_speed': 18.19, - 'wind_speed': 5.31, - }), - dict({ - 'apparent_temperature': 28.2, - 'cloud_coverage': 31.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T11:00:00Z', - 'dew_point': 22.4, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.56, - 'temperature': 25.5, - 'uv_index': 0, - 'wind_bearing': 177, - 'wind_gust_speed': 16.79, - 'wind_speed': 4.28, - }), - dict({ - 'apparent_temperature': 27.5, - 'cloud_coverage': 31.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T12:00:00Z', - 'dew_point': 22.3, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.87, - 'temperature': 24.9, - 'uv_index': 0, - 'wind_bearing': 171, - 'wind_gust_speed': 15.61, - 'wind_speed': 3.72, - }), - dict({ - 'apparent_temperature': 26.6, - 'cloud_coverage': 31.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T13:00:00Z', - 'dew_point': 22.1, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.91, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 171, - 'wind_gust_speed': 14.7, - 'wind_speed': 4.11, - }), - dict({ - 'apparent_temperature': 25.9, - 'cloud_coverage': 32.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T14:00:00Z', - 'dew_point': 21.9, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.8, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 171, - 'wind_gust_speed': 13.81, - 'wind_speed': 4.97, - }), - dict({ - 'apparent_temperature': 25.3, - 'cloud_coverage': 34.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T15:00:00Z', - 'dew_point': 21.7, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.66, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 170, - 'wind_gust_speed': 12.88, - 'wind_speed': 5.57, - }), - dict({ - 'apparent_temperature': 24.8, - 'cloud_coverage': 37.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T16:00:00Z', - 'dew_point': 21.5, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.54, - 'temperature': 22.7, - 'uv_index': 0, - 'wind_bearing': 168, - 'wind_gust_speed': 12.0, - 'wind_speed': 5.62, - }), - dict({ - 'apparent_temperature': 24.4, - 'cloud_coverage': 39.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T17:00:00Z', - 'dew_point': 21.3, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.45, - 'temperature': 22.4, - 'uv_index': 0, - 'wind_bearing': 165, - 'wind_gust_speed': 11.43, - 'wind_speed': 5.48, - }), - dict({ - 'apparent_temperature': 24.6, - 'cloud_coverage': 40.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T18:00:00Z', - 'dew_point': 21.4, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 44.0, - 'pressure': 1014.45, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 11.42, - 'wind_speed': 5.38, - }), - dict({ - 'apparent_temperature': 25.0, - 'cloud_coverage': 40.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T19:00:00Z', - 'dew_point': 21.6, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 52.0, - 'pressure': 1014.63, - 'temperature': 22.9, - 'uv_index': 0, - 'wind_bearing': 161, - 'wind_gust_speed': 12.15, - 'wind_speed': 5.39, - }), - dict({ - 'apparent_temperature': 25.6, - 'cloud_coverage': 38.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T20:00:00Z', - 'dew_point': 21.8, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 51.0, - 'pressure': 1014.91, - 'temperature': 23.4, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 13.54, - 'wind_speed': 5.45, - }), - dict({ - 'apparent_temperature': 26.6, - 'cloud_coverage': 36.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T21:00:00Z', - 'dew_point': 22.0, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 42.0, - 'pressure': 1015.18, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 158, - 'wind_gust_speed': 15.48, - 'wind_speed': 5.62, - }), - dict({ - 'apparent_temperature': 28.5, - 'cloud_coverage': 32.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T22:00:00Z', - 'dew_point': 22.5, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 28.999999999999996, - 'pressure': 1015.4, - 'temperature': 25.7, - 'uv_index': 1, - 'wind_bearing': 158, - 'wind_gust_speed': 17.86, - 'wind_speed': 5.84, - }), - dict({ - 'apparent_temperature': 30.3, - 'cloud_coverage': 30.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T23:00:00Z', - 'dew_point': 22.9, - 'humidity': 77, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.54, - 'temperature': 27.2, - 'uv_index': 2, - 'wind_bearing': 155, - 'wind_gust_speed': 20.19, - 'wind_speed': 6.09, - }), - dict({ - 'apparent_temperature': 32.1, - 'cloud_coverage': 30.0, - 'condition': 'sunny', - 'datetime': '2023-09-15T00:00:00Z', - 'dew_point': 23.3, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.55, - 'temperature': 28.6, - 'uv_index': 4, - 'wind_bearing': 152, - 'wind_gust_speed': 21.83, - 'wind_speed': 6.42, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 34.0, - 'condition': 'sunny', - 'datetime': '2023-09-15T01:00:00Z', - 'dew_point': 23.5, - 'humidity': 70, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.35, - 'temperature': 29.6, - 'uv_index': 6, - 'wind_bearing': 144, - 'wind_gust_speed': 22.56, - 'wind_speed': 6.91, - }), - dict({ - 'apparent_temperature': 34.2, - 'cloud_coverage': 41.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T02:00:00Z', - 'dew_point': 23.5, - 'humidity': 67, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.0, - 'temperature': 30.4, - 'uv_index': 7, - 'wind_bearing': 336, - 'wind_gust_speed': 22.83, - 'wind_speed': 7.47, - }), - dict({ - 'apparent_temperature': 34.9, - 'cloud_coverage': 46.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T03:00:00Z', - 'dew_point': 23.5, - 'humidity': 65, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.62, - 'temperature': 30.9, - 'uv_index': 7, - 'wind_bearing': 336, - 'wind_gust_speed': 22.98, - 'wind_speed': 7.95, - }), - dict({ - 'apparent_temperature': 35.4, - 'cloud_coverage': 46.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T04:00:00Z', - 'dew_point': 23.6, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.25, - 'temperature': 31.3, - 'uv_index': 6, - 'wind_bearing': 341, - 'wind_gust_speed': 23.21, - 'wind_speed': 8.44, - }), - dict({ - 'apparent_temperature': 35.6, - 'cloud_coverage': 44.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T05:00:00Z', - 'dew_point': 23.7, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.95, - 'temperature': 31.5, - 'uv_index': 5, - 'wind_bearing': 344, - 'wind_gust_speed': 23.46, - 'wind_speed': 8.95, - }), - dict({ - 'apparent_temperature': 35.1, - 'cloud_coverage': 42.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T06:00:00Z', - 'dew_point': 23.6, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.83, - 'temperature': 31.1, - 'uv_index': 3, - 'wind_bearing': 347, - 'wind_gust_speed': 23.64, - 'wind_speed': 9.13, - }), - dict({ - 'apparent_temperature': 34.1, - 'cloud_coverage': 41.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T07:00:00Z', - 'dew_point': 23.4, - 'humidity': 66, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.96, - 'temperature': 30.3, - 'uv_index': 2, - 'wind_bearing': 350, - 'wind_gust_speed': 23.66, - 'wind_speed': 8.78, - }), - dict({ - 'apparent_temperature': 32.4, - 'cloud_coverage': 40.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T08:00:00Z', - 'dew_point': 23.1, - 'humidity': 70, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.25, - 'temperature': 29.0, - 'uv_index': 0, - 'wind_bearing': 356, - 'wind_gust_speed': 23.51, - 'wind_speed': 8.13, - }), - dict({ - 'apparent_temperature': 31.1, - 'cloud_coverage': 41.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T09:00:00Z', - 'dew_point': 22.9, - 'humidity': 74, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.61, - 'temperature': 27.9, - 'uv_index': 0, - 'wind_bearing': 3, - 'wind_gust_speed': 23.21, - 'wind_speed': 7.48, - }), - dict({ - 'apparent_temperature': 30.0, - 'cloud_coverage': 43.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T10:00:00Z', - 'dew_point': 22.8, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.02, - 'temperature': 26.9, - 'uv_index': 0, - 'wind_bearing': 20, - 'wind_gust_speed': 22.68, - 'wind_speed': 6.83, - }), - dict({ - 'apparent_temperature': 29.2, - 'cloud_coverage': 46.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T11:00:00Z', - 'dew_point': 22.8, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.43, - 'temperature': 26.2, - 'uv_index': 0, - 'wind_bearing': 129, - 'wind_gust_speed': 22.04, - 'wind_speed': 6.1, - }), - dict({ - 'apparent_temperature': 28.4, - 'cloud_coverage': 48.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T12:00:00Z', - 'dew_point': 22.7, - 'humidity': 84, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.71, - 'temperature': 25.6, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 21.64, - 'wind_speed': 5.6, - }), - dict({ - 'apparent_temperature': 28.2, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T13:00:00Z', - 'dew_point': 23.2, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.52, - 'temperature': 25.2, - 'uv_index': 0, - 'wind_bearing': 164, - 'wind_gust_speed': 16.35, - 'wind_speed': 5.58, - }), - dict({ - 'apparent_temperature': 27.4, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T14:00:00Z', - 'dew_point': 22.9, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.37, - 'temperature': 24.6, - 'uv_index': 0, - 'wind_bearing': 168, - 'wind_gust_speed': 17.11, - 'wind_speed': 5.79, - }), - dict({ - 'apparent_temperature': 26.9, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T15:00:00Z', - 'dew_point': 22.7, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.21, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 182, - 'wind_gust_speed': 17.32, - 'wind_speed': 5.77, - }), - dict({ - 'apparent_temperature': 26.4, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T16:00:00Z', - 'dew_point': 22.6, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.07, - 'temperature': 23.8, - 'uv_index': 0, - 'wind_bearing': 201, - 'wind_gust_speed': 16.6, - 'wind_speed': 5.27, - }), - dict({ - 'apparent_temperature': 26.0, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T17:00:00Z', - 'dew_point': 22.5, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.95, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 219, - 'wind_gust_speed': 15.52, - 'wind_speed': 4.62, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T18:00:00Z', - 'dew_point': 22.3, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.88, - 'temperature': 23.3, - 'uv_index': 0, - 'wind_bearing': 216, - 'wind_gust_speed': 14.64, - 'wind_speed': 4.32, - }), - dict({ - 'apparent_temperature': 26.0, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T19:00:00Z', - 'dew_point': 22.4, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.91, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 198, - 'wind_gust_speed': 14.06, - 'wind_speed': 4.73, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T20:00:00Z', - 'dew_point': 22.4, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.99, - 'temperature': 23.8, - 'uv_index': 0, - 'wind_bearing': 189, - 'wind_gust_speed': 13.7, - 'wind_speed': 5.49, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T21:00:00Z', - 'dew_point': 22.5, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.07, - 'temperature': 24.4, - 'uv_index': 0, - 'wind_bearing': 183, - 'wind_gust_speed': 13.77, - 'wind_speed': 5.95, - }), - dict({ - 'apparent_temperature': 28.3, - 'cloud_coverage': 59.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T22:00:00Z', - 'dew_point': 22.6, - 'humidity': 84, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.12, - 'temperature': 25.5, - 'uv_index': 1, - 'wind_bearing': 179, - 'wind_gust_speed': 14.38, - 'wind_speed': 5.77, - }), - dict({ - 'apparent_temperature': 29.9, - 'cloud_coverage': 52.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T23:00:00Z', - 'dew_point': 22.9, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.13, - 'temperature': 26.9, - 'uv_index': 2, - 'wind_bearing': 170, - 'wind_gust_speed': 15.2, - 'wind_speed': 5.27, - }), - dict({ - 'apparent_temperature': 31.2, - 'cloud_coverage': 44.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T00:00:00Z', - 'dew_point': 22.9, - 'humidity': 74, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.04, - 'temperature': 28.0, - 'uv_index': 4, - 'wind_bearing': 155, - 'wind_gust_speed': 15.85, - 'wind_speed': 4.76, - }), - dict({ - 'apparent_temperature': 32.5, - 'cloud_coverage': 24.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T01:00:00Z', - 'dew_point': 22.6, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.52, - 'temperature': 29.2, - 'uv_index': 6, - 'wind_bearing': 110, - 'wind_gust_speed': 16.27, - 'wind_speed': 6.81, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 16.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T02:00:00Z', - 'dew_point': 22.4, - 'humidity': 63, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.01, - 'temperature': 30.2, - 'uv_index': 8, - 'wind_bearing': 30, - 'wind_gust_speed': 16.55, - 'wind_speed': 6.86, - }), - dict({ - 'apparent_temperature': 34.2, - 'cloud_coverage': 10.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T03:00:00Z', - 'dew_point': 22.0, - 'humidity': 59, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.45, - 'temperature': 31.1, - 'uv_index': 8, - 'wind_bearing': 17, - 'wind_gust_speed': 16.52, - 'wind_speed': 6.8, - }), - dict({ - 'apparent_temperature': 34.7, - 'cloud_coverage': 10.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T04:00:00Z', - 'dew_point': 21.9, - 'humidity': 57, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.89, - 'temperature': 31.5, - 'uv_index': 8, - 'wind_bearing': 17, - 'wind_gust_speed': 16.08, - 'wind_speed': 6.62, - }), - dict({ - 'apparent_temperature': 34.9, - 'cloud_coverage': 10.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T05:00:00Z', - 'dew_point': 21.9, - 'humidity': 56, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.39, - 'temperature': 31.8, - 'uv_index': 6, - 'wind_bearing': 20, - 'wind_gust_speed': 15.48, - 'wind_speed': 6.45, - }), - dict({ - 'apparent_temperature': 34.5, - 'cloud_coverage': 10.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T06:00:00Z', - 'dew_point': 21.7, - 'humidity': 56, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.11, - 'temperature': 31.4, - 'uv_index': 4, - 'wind_bearing': 26, - 'wind_gust_speed': 15.08, - 'wind_speed': 6.43, - }), - dict({ - 'apparent_temperature': 33.6, - 'cloud_coverage': 7.000000000000001, - 'condition': 'sunny', - 'datetime': '2023-09-16T07:00:00Z', - 'dew_point': 21.7, - 'humidity': 59, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.15, - 'temperature': 30.7, - 'uv_index': 2, - 'wind_bearing': 39, - 'wind_gust_speed': 14.88, - 'wind_speed': 6.61, - }), - dict({ - 'apparent_temperature': 32.5, - 'cloud_coverage': 2.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T08:00:00Z', - 'dew_point': 21.9, - 'humidity': 63, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.41, - 'temperature': 29.6, - 'uv_index': 0, - 'wind_bearing': 72, - 'wind_gust_speed': 14.82, - 'wind_speed': 6.95, - }), - dict({ - 'apparent_temperature': 31.4, - 'cloud_coverage': 2.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T09:00:00Z', - 'dew_point': 22.1, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.75, - 'temperature': 28.5, - 'uv_index': 0, - 'wind_bearing': 116, - 'wind_gust_speed': 15.13, - 'wind_speed': 7.45, - }), - dict({ - 'apparent_temperature': 30.5, - 'cloud_coverage': 13.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T10:00:00Z', - 'dew_point': 22.3, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.13, - 'temperature': 27.6, - 'uv_index': 0, - 'wind_bearing': 140, - 'wind_gust_speed': 16.09, - 'wind_speed': 8.15, - }), - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 31.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T11:00:00Z', - 'dew_point': 22.6, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.47, - 'temperature': 26.9, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 17.37, - 'wind_speed': 8.87, - }), - dict({ - 'apparent_temperature': 29.3, - 'cloud_coverage': 45.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T12:00:00Z', - 'dew_point': 22.9, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.6, - 'temperature': 26.3, - 'uv_index': 0, - 'wind_bearing': 155, - 'wind_gust_speed': 18.29, - 'wind_speed': 9.21, - }), - dict({ - 'apparent_temperature': 28.7, - 'cloud_coverage': 51.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T13:00:00Z', - 'dew_point': 23.0, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.41, - 'temperature': 25.7, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 18.49, - 'wind_speed': 8.96, - }), - dict({ - 'apparent_temperature': 27.9, - 'cloud_coverage': 55.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T14:00:00Z', - 'dew_point': 22.8, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.01, - 'temperature': 25.0, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 18.47, - 'wind_speed': 8.45, - }), - dict({ - 'apparent_temperature': 27.2, - 'cloud_coverage': 59.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T15:00:00Z', - 'dew_point': 22.7, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.55, - 'temperature': 24.5, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 18.79, - 'wind_speed': 8.1, - }), - dict({ - 'apparent_temperature': 26.7, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T16:00:00Z', - 'dew_point': 22.6, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.1, - 'temperature': 24.0, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 19.81, - 'wind_speed': 8.15, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T17:00:00Z', - 'dew_point': 22.6, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.68, - 'temperature': 23.7, - 'uv_index': 0, - 'wind_bearing': 161, - 'wind_gust_speed': 20.96, - 'wind_speed': 8.3, - }), - dict({ - 'apparent_temperature': 26.0, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T18:00:00Z', - 'dew_point': 22.4, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.39, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 21.41, - 'wind_speed': 8.24, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T19:00:00Z', - 'dew_point': 22.5, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.29, - 'temperature': 23.8, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 20.42, - 'wind_speed': 7.62, - }), - dict({ - 'apparent_temperature': 26.8, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T20:00:00Z', - 'dew_point': 22.6, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.31, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 158, - 'wind_gust_speed': 18.61, - 'wind_speed': 6.66, - }), - dict({ - 'apparent_temperature': 27.7, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T21:00:00Z', - 'dew_point': 22.6, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.37, - 'temperature': 24.9, - 'uv_index': 0, - 'wind_bearing': 158, - 'wind_gust_speed': 17.14, - 'wind_speed': 5.86, - }), - dict({ - 'apparent_temperature': 28.9, - 'cloud_coverage': 48.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T22:00:00Z', - 'dew_point': 22.6, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.46, - 'temperature': 26.0, - 'uv_index': 1, - 'wind_bearing': 161, - 'wind_gust_speed': 16.78, - 'wind_speed': 5.5, - }), - dict({ - 'apparent_temperature': 30.6, - 'cloud_coverage': 39.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T23:00:00Z', - 'dew_point': 22.9, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.51, - 'temperature': 27.5, - 'uv_index': 2, - 'wind_bearing': 165, - 'wind_gust_speed': 17.21, - 'wind_speed': 5.56, - }), - dict({ - 'apparent_temperature': 31.7, - 'cloud_coverage': 33.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T00:00:00Z', - 'dew_point': 22.8, - 'humidity': 71, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.39, - 'temperature': 28.5, - 'uv_index': 4, - 'wind_bearing': 174, - 'wind_gust_speed': 17.96, - 'wind_speed': 6.04, - }), - dict({ - 'apparent_temperature': 32.6, - 'cloud_coverage': 30.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T01:00:00Z', - 'dew_point': 22.7, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.98, - 'temperature': 29.4, - 'uv_index': 6, - 'wind_bearing': 192, - 'wind_gust_speed': 19.15, - 'wind_speed': 7.23, - }), - dict({ - 'apparent_temperature': 33.6, - 'cloud_coverage': 28.999999999999996, - 'condition': 'sunny', - 'datetime': '2023-09-17T02:00:00Z', - 'dew_point': 22.8, - 'humidity': 65, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.38, - 'temperature': 30.1, - 'uv_index': 7, - 'wind_bearing': 225, - 'wind_gust_speed': 20.89, - 'wind_speed': 8.9, - }), - dict({ - 'apparent_temperature': 34.1, - 'cloud_coverage': 30.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T03:00:00Z', - 'dew_point': 22.8, - 'humidity': 63, - 'precipitation': 0.3, - 'precipitation_probability': 9.0, - 'pressure': 1009.75, - 'temperature': 30.7, - 'uv_index': 8, - 'wind_bearing': 264, - 'wind_gust_speed': 22.67, - 'wind_speed': 10.27, - }), - dict({ - 'apparent_temperature': 33.9, - 'cloud_coverage': 37.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T04:00:00Z', - 'dew_point': 22.5, - 'humidity': 62, - 'precipitation': 0.4, - 'precipitation_probability': 10.0, - 'pressure': 1009.18, - 'temperature': 30.5, - 'uv_index': 7, - 'wind_bearing': 293, - 'wind_gust_speed': 23.93, - 'wind_speed': 10.82, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 45.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T05:00:00Z', - 'dew_point': 22.4, - 'humidity': 63, - 'precipitation': 0.6, - 'precipitation_probability': 12.0, - 'pressure': 1008.71, - 'temperature': 30.1, - 'uv_index': 5, - 'wind_bearing': 308, - 'wind_gust_speed': 24.39, - 'wind_speed': 10.72, - }), - dict({ - 'apparent_temperature': 32.7, - 'cloud_coverage': 50.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T06:00:00Z', - 'dew_point': 22.2, - 'humidity': 64, - 'precipitation': 0.7, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1008.46, - 'temperature': 29.6, - 'uv_index': 3, - 'wind_bearing': 312, - 'wind_gust_speed': 23.9, - 'wind_speed': 10.28, - }), - dict({ - 'apparent_temperature': 31.8, - 'cloud_coverage': 47.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T07:00:00Z', - 'dew_point': 22.1, - 'humidity': 67, - 'precipitation': 0.7, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1008.53, - 'temperature': 28.9, - 'uv_index': 1, - 'wind_bearing': 312, - 'wind_gust_speed': 22.3, - 'wind_speed': 9.59, - }), - dict({ - 'apparent_temperature': 30.6, - 'cloud_coverage': 41.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T08:00:00Z', - 'dew_point': 21.9, - 'humidity': 70, - 'precipitation': 0.6, - 'precipitation_probability': 15.0, - 'pressure': 1008.82, - 'temperature': 27.9, - 'uv_index': 0, - 'wind_bearing': 305, - 'wind_gust_speed': 19.73, - 'wind_speed': 8.58, - }), - dict({ - 'apparent_temperature': 29.6, - 'cloud_coverage': 35.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T09:00:00Z', - 'dew_point': 22.0, - 'humidity': 74, - 'precipitation': 0.5, - 'precipitation_probability': 15.0, - 'pressure': 1009.21, - 'temperature': 27.0, - 'uv_index': 0, - 'wind_bearing': 291, - 'wind_gust_speed': 16.49, - 'wind_speed': 7.34, - }), - dict({ - 'apparent_temperature': 28.6, - 'cloud_coverage': 33.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T10:00:00Z', - 'dew_point': 21.9, - 'humidity': 78, - 'precipitation': 0.4, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1009.65, - 'temperature': 26.1, - 'uv_index': 0, - 'wind_bearing': 257, - 'wind_gust_speed': 12.71, - 'wind_speed': 5.91, - }), - dict({ - 'apparent_temperature': 27.8, - 'cloud_coverage': 34.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T11:00:00Z', - 'dew_point': 21.9, - 'humidity': 82, - 'precipitation': 0.3, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1010.04, - 'temperature': 25.3, - 'uv_index': 0, - 'wind_bearing': 212, - 'wind_gust_speed': 9.16, - 'wind_speed': 4.54, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 36.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T12:00:00Z', - 'dew_point': 21.9, - 'humidity': 85, - 'precipitation': 0.3, - 'precipitation_probability': 28.000000000000004, - 'pressure': 1010.24, - 'temperature': 24.6, - 'uv_index': 0, - 'wind_bearing': 192, - 'wind_gust_speed': 7.09, - 'wind_speed': 3.62, - }), - dict({ - 'apparent_temperature': 26.5, - 'cloud_coverage': 40.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T13:00:00Z', - 'dew_point': 22.0, - 'humidity': 88, - 'precipitation': 0.3, - 'precipitation_probability': 30.0, - 'pressure': 1010.15, - 'temperature': 24.1, - 'uv_index': 0, - 'wind_bearing': 185, - 'wind_gust_speed': 7.2, - 'wind_speed': 3.27, - }), - dict({ - 'apparent_temperature': 25.9, - 'cloud_coverage': 44.0, - 'condition': 'rainy', - 'datetime': '2023-09-17T14:00:00Z', - 'dew_point': 21.8, - 'humidity': 90, - 'precipitation': 0.3, - 'precipitation_probability': 30.0, - 'pressure': 1009.87, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 182, - 'wind_gust_speed': 8.37, - 'wind_speed': 3.22, - }), - dict({ - 'apparent_temperature': 25.5, - 'cloud_coverage': 49.0, - 'condition': 'rainy', - 'datetime': '2023-09-17T15:00:00Z', - 'dew_point': 21.8, - 'humidity': 92, - 'precipitation': 0.2, - 'precipitation_probability': 31.0, - 'pressure': 1009.56, - 'temperature': 23.2, - 'uv_index': 0, - 'wind_bearing': 180, - 'wind_gust_speed': 9.21, - 'wind_speed': 3.3, - }), - dict({ - 'apparent_temperature': 25.1, - 'cloud_coverage': 53.0, - 'condition': 'rainy', - 'datetime': '2023-09-17T16:00:00Z', - 'dew_point': 21.8, - 'humidity': 94, - 'precipitation': 0.2, - 'precipitation_probability': 33.0, - 'pressure': 1009.29, - 'temperature': 22.9, - 'uv_index': 0, - 'wind_bearing': 182, - 'wind_gust_speed': 9.0, - 'wind_speed': 3.46, - }), - dict({ - 'apparent_temperature': 24.8, - 'cloud_coverage': 56.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T17:00:00Z', - 'dew_point': 21.7, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 35.0, - 'pressure': 1009.09, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 186, - 'wind_gust_speed': 8.37, - 'wind_speed': 3.72, - }), - dict({ - 'apparent_temperature': 24.6, - 'cloud_coverage': 59.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T18:00:00Z', - 'dew_point': 21.6, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 37.0, - 'pressure': 1009.01, - 'temperature': 22.5, - 'uv_index': 0, - 'wind_bearing': 201, - 'wind_gust_speed': 7.99, - 'wind_speed': 4.07, - }), - dict({ - 'apparent_temperature': 24.9, - 'cloud_coverage': 62.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T19:00:00Z', - 'dew_point': 21.7, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 39.0, - 'pressure': 1009.07, - 'temperature': 22.7, - 'uv_index': 0, - 'wind_bearing': 258, - 'wind_gust_speed': 8.18, - 'wind_speed': 4.55, - }), - dict({ - 'apparent_temperature': 25.2, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-17T20:00:00Z', - 'dew_point': 21.7, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 39.0, - 'pressure': 1009.23, - 'temperature': 23.0, - 'uv_index': 0, - 'wind_bearing': 305, - 'wind_gust_speed': 8.77, - 'wind_speed': 5.17, - }), - dict({ - 'apparent_temperature': 25.8, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-17T21:00:00Z', - 'dew_point': 21.8, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 38.0, - 'pressure': 1009.47, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 318, - 'wind_gust_speed': 9.69, - 'wind_speed': 5.77, - }), - dict({ - 'apparent_temperature': 26.5, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-17T22:00:00Z', - 'dew_point': 21.8, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 30.0, - 'pressure': 1009.77, - 'temperature': 24.2, - 'uv_index': 1, - 'wind_bearing': 324, - 'wind_gust_speed': 10.88, - 'wind_speed': 6.26, - }), - dict({ - 'apparent_temperature': 27.6, - 'cloud_coverage': 80.0, - 'condition': 'rainy', - 'datetime': '2023-09-17T23:00:00Z', - 'dew_point': 21.9, - 'humidity': 83, - 'precipitation': 0.2, - 'precipitation_probability': 15.0, - 'pressure': 1010.09, - 'temperature': 25.1, - 'uv_index': 2, - 'wind_bearing': 329, - 'wind_gust_speed': 12.21, - 'wind_speed': 6.68, - }), - dict({ - 'apparent_temperature': 28.2, - 'cloud_coverage': 87.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T00:00:00Z', - 'dew_point': 21.9, - 'humidity': 80, - 'precipitation': 0.2, - 'precipitation_probability': 15.0, - 'pressure': 1010.33, - 'temperature': 25.7, - 'uv_index': 3, - 'wind_bearing': 332, - 'wind_gust_speed': 13.52, - 'wind_speed': 7.12, - }), - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 67.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T01:00:00Z', - 'dew_point': 21.7, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1007.43, - 'temperature': 27.2, - 'uv_index': 5, - 'wind_bearing': 330, - 'wind_gust_speed': 11.36, - 'wind_speed': 11.36, - }), - dict({ - 'apparent_temperature': 30.1, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T02:00:00Z', - 'dew_point': 21.6, - 'humidity': 70, - 'precipitation': 0.3, - 'precipitation_probability': 9.0, - 'pressure': 1007.05, - 'temperature': 27.5, - 'uv_index': 6, - 'wind_bearing': 332, - 'wind_gust_speed': 12.06, - 'wind_speed': 12.06, - }), - dict({ - 'apparent_temperature': 30.3, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T03:00:00Z', - 'dew_point': 21.6, - 'humidity': 69, - 'precipitation': 0.5, - 'precipitation_probability': 10.0, - 'pressure': 1006.67, - 'temperature': 27.8, - 'uv_index': 6, - 'wind_bearing': 333, - 'wind_gust_speed': 12.81, - 'wind_speed': 12.81, - }), - dict({ - 'apparent_temperature': 30.6, - 'cloud_coverage': 67.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T04:00:00Z', - 'dew_point': 21.5, - 'humidity': 68, - 'precipitation': 0.4, - 'precipitation_probability': 10.0, - 'pressure': 1006.28, - 'temperature': 28.0, - 'uv_index': 5, - 'wind_bearing': 335, - 'wind_gust_speed': 13.68, - 'wind_speed': 13.68, - }), - dict({ - 'apparent_temperature': 30.7, - 'cloud_coverage': 60.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T05:00:00Z', - 'dew_point': 21.4, - 'humidity': 67, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1005.89, - 'temperature': 28.1, - 'uv_index': 4, - 'wind_bearing': 336, - 'wind_gust_speed': 14.61, - 'wind_speed': 14.61, - }), - dict({ - 'apparent_temperature': 30.3, - 'cloud_coverage': 56.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T06:00:00Z', - 'dew_point': 21.2, - 'humidity': 67, - 'precipitation': 0.0, - 'precipitation_probability': 27.0, - 'pressure': 1005.67, - 'temperature': 27.9, - 'uv_index': 3, - 'wind_bearing': 338, - 'wind_gust_speed': 15.25, - 'wind_speed': 15.25, - }), - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 60.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T07:00:00Z', - 'dew_point': 21.3, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 28.000000000000004, - 'pressure': 1005.74, - 'temperature': 27.4, - 'uv_index': 1, - 'wind_bearing': 339, - 'wind_gust_speed': 15.45, - 'wind_speed': 15.45, - }), - dict({ - 'apparent_temperature': 29.1, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T08:00:00Z', - 'dew_point': 21.4, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 26.0, - 'pressure': 1005.98, - 'temperature': 26.7, - 'uv_index': 0, - 'wind_bearing': 341, - 'wind_gust_speed': 15.38, - 'wind_speed': 15.38, - }), - dict({ - 'apparent_temperature': 28.6, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T09:00:00Z', - 'dew_point': 21.6, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1006.22, - 'temperature': 26.1, - 'uv_index': 0, - 'wind_bearing': 341, - 'wind_gust_speed': 15.27, - 'wind_speed': 15.27, - }), - dict({ - 'apparent_temperature': 27.9, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T10:00:00Z', - 'dew_point': 21.6, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1006.44, - 'temperature': 25.6, - 'uv_index': 0, - 'wind_bearing': 339, - 'wind_gust_speed': 15.09, - 'wind_speed': 15.09, - }), - dict({ - 'apparent_temperature': 27.6, - 'cloud_coverage': 61.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T11:00:00Z', - 'dew_point': 21.7, - 'humidity': 81, - 'precipitation': 0.0, - 'precipitation_probability': 26.0, - 'pressure': 1006.66, - 'temperature': 25.2, - 'uv_index': 0, - 'wind_bearing': 336, - 'wind_gust_speed': 14.88, - 'wind_speed': 14.88, - }), - dict({ - 'apparent_temperature': 27.2, - 'cloud_coverage': 61.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T12:00:00Z', - 'dew_point': 21.8, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 26.0, - 'pressure': 1006.79, - 'temperature': 24.8, - 'uv_index': 0, - 'wind_bearing': 333, - 'wind_gust_speed': 14.91, - 'wind_speed': 14.91, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 38.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T13:00:00Z', - 'dew_point': 21.2, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.36, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 83, - 'wind_gust_speed': 4.58, - 'wind_speed': 3.16, - }), - dict({ - 'apparent_temperature': 25.1, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T14:00:00Z', - 'dew_point': 21.2, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.96, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 144, - 'wind_gust_speed': 4.74, - 'wind_speed': 4.52, - }), - dict({ - 'apparent_temperature': 24.5, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T15:00:00Z', - 'dew_point': 20.9, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.6, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 152, - 'wind_gust_speed': 5.63, - 'wind_speed': 5.63, - }), - dict({ - 'apparent_temperature': 24.0, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T16:00:00Z', - 'dew_point': 20.7, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.37, - 'temperature': 22.3, - 'uv_index': 0, - 'wind_bearing': 156, - 'wind_gust_speed': 6.02, - 'wind_speed': 6.02, - }), - dict({ - 'apparent_temperature': 23.7, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T17:00:00Z', - 'dew_point': 20.4, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.2, - 'temperature': 22.0, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 6.15, - 'wind_speed': 6.15, - }), - dict({ - 'apparent_temperature': 23.4, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T18:00:00Z', - 'dew_point': 20.2, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.08, - 'temperature': 21.9, - 'uv_index': 0, - 'wind_bearing': 167, - 'wind_gust_speed': 6.48, - 'wind_speed': 6.48, - }), - dict({ - 'apparent_temperature': 23.2, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T19:00:00Z', - 'dew_point': 19.8, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.04, - 'temperature': 21.8, - 'uv_index': 0, - 'wind_bearing': 165, - 'wind_gust_speed': 7.51, - 'wind_speed': 7.51, - }), - dict({ - 'apparent_temperature': 23.4, - 'cloud_coverage': 99.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T20:00:00Z', - 'dew_point': 19.6, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.05, - 'temperature': 22.0, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 8.73, - 'wind_speed': 8.73, - }), - dict({ - 'apparent_temperature': 23.9, - 'cloud_coverage': 98.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T21:00:00Z', - 'dew_point': 19.5, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.06, - 'temperature': 22.5, - 'uv_index': 0, - 'wind_bearing': 164, - 'wind_gust_speed': 9.21, - 'wind_speed': 9.11, - }), - dict({ - 'apparent_temperature': 25.3, - 'cloud_coverage': 96.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T22:00:00Z', - 'dew_point': 19.7, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.09, - 'temperature': 23.8, - 'uv_index': 1, - 'wind_bearing': 171, - 'wind_gust_speed': 9.03, - 'wind_speed': 7.91, - }), - ]), - }) -# --- -# name: test_hourly_forecast[forecast] - dict({ - 'weather.home': dict({ - 'forecast': list([ - dict({ - 'apparent_temperature': 24.6, - 'cloud_coverage': 79.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T14:00:00Z', - 'dew_point': 21.5, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.24, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 264, - 'wind_gust_speed': 13.44, - 'wind_speed': 6.62, - }), - dict({ - 'apparent_temperature': 24.4, - 'cloud_coverage': 80.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T15:00:00Z', - 'dew_point': 21.4, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.24, - 'temperature': 22.4, - 'uv_index': 0, - 'wind_bearing': 261, - 'wind_gust_speed': 11.91, - 'wind_speed': 6.64, - }), - dict({ - 'apparent_temperature': 23.8, - 'cloud_coverage': 89.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T16:00:00Z', - 'dew_point': 21.1, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.12, - 'temperature': 22.0, - 'uv_index': 0, - 'wind_bearing': 252, - 'wind_gust_speed': 11.15, - 'wind_speed': 6.14, - }), - dict({ - 'apparent_temperature': 23.5, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T17:00:00Z', - 'dew_point': 20.9, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.03, - 'temperature': 21.7, - 'uv_index': 0, - 'wind_bearing': 248, - 'wind_gust_speed': 11.57, - 'wind_speed': 5.95, - }), - dict({ - 'apparent_temperature': 23.3, - 'cloud_coverage': 85.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T18:00:00Z', - 'dew_point': 20.8, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.05, - 'temperature': 21.6, - 'uv_index': 0, - 'wind_bearing': 237, - 'wind_gust_speed': 12.42, - 'wind_speed': 5.86, - }), - dict({ - 'apparent_temperature': 23.0, - 'cloud_coverage': 75.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T19:00:00Z', - 'dew_point': 20.6, - 'humidity': 96, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.03, - 'temperature': 21.3, - 'uv_index': 0, - 'wind_bearing': 224, - 'wind_gust_speed': 11.3, - 'wind_speed': 5.34, - }), - dict({ - 'apparent_temperature': 22.8, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T20:00:00Z', - 'dew_point': 20.4, - 'humidity': 96, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.31, - 'temperature': 21.2, - 'uv_index': 0, - 'wind_bearing': 221, - 'wind_gust_speed': 10.57, - 'wind_speed': 5.13, - }), - dict({ - 'apparent_temperature': 23.1, - 'cloud_coverage': 56.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-08T21:00:00Z', - 'dew_point': 20.5, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.55, - 'temperature': 21.4, - 'uv_index': 0, - 'wind_bearing': 237, - 'wind_gust_speed': 10.63, - 'wind_speed': 5.7, - }), - dict({ - 'apparent_temperature': 24.9, - 'cloud_coverage': 61.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-08T22:00:00Z', - 'dew_point': 21.3, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.79, - 'temperature': 22.8, - 'uv_index': 1, - 'wind_bearing': 258, - 'wind_gust_speed': 10.47, - 'wind_speed': 5.22, - }), - dict({ - 'apparent_temperature': 26.1, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T23:00:00Z', - 'dew_point': 21.3, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.95, - 'temperature': 24.0, - 'uv_index': 2, - 'wind_bearing': 282, - 'wind_gust_speed': 12.74, - 'wind_speed': 5.71, - }), - dict({ - 'apparent_temperature': 27.4, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T00:00:00Z', - 'dew_point': 21.5, - 'humidity': 80, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.35, - 'temperature': 25.1, - 'uv_index': 3, - 'wind_bearing': 294, - 'wind_gust_speed': 13.87, - 'wind_speed': 6.53, - }), - dict({ - 'apparent_temperature': 29.0, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T01:00:00Z', - 'dew_point': 21.8, - 'humidity': 75, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.48, - 'temperature': 26.5, - 'uv_index': 5, - 'wind_bearing': 308, - 'wind_gust_speed': 16.04, - 'wind_speed': 6.54, - }), - dict({ - 'apparent_temperature': 30.3, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T02:00:00Z', - 'dew_point': 22.0, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.23, - 'temperature': 27.6, - 'uv_index': 6, - 'wind_bearing': 314, - 'wind_gust_speed': 18.1, - 'wind_speed': 7.32, - }), - dict({ - 'apparent_temperature': 31.1, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T03:00:00Z', - 'dew_point': 22.1, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.86, - 'temperature': 28.3, - 'uv_index': 6, - 'wind_bearing': 317, - 'wind_gust_speed': 20.77, - 'wind_speed': 9.1, - }), - dict({ - 'apparent_temperature': 31.5, - 'cloud_coverage': 69.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T04:00:00Z', - 'dew_point': 22.1, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.65, - 'temperature': 28.6, - 'uv_index': 6, - 'wind_bearing': 311, - 'wind_gust_speed': 21.27, - 'wind_speed': 10.21, - }), - dict({ - 'apparent_temperature': 31.3, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T05:00:00Z', - 'dew_point': 22.1, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.48, - 'temperature': 28.4, - 'uv_index': 5, - 'wind_bearing': 317, - 'wind_gust_speed': 19.62, - 'wind_speed': 10.53, - }), - dict({ - 'apparent_temperature': 30.8, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T06:00:00Z', - 'dew_point': 22.2, - 'humidity': 71, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.54, - 'temperature': 27.9, - 'uv_index': 3, - 'wind_bearing': 335, - 'wind_gust_speed': 18.98, - 'wind_speed': 8.63, - }), - dict({ - 'apparent_temperature': 29.9, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T07:00:00Z', - 'dew_point': 22.2, - 'humidity': 74, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.76, - 'temperature': 27.1, - 'uv_index': 2, - 'wind_bearing': 338, - 'wind_gust_speed': 17.04, - 'wind_speed': 7.75, - }), - dict({ - 'apparent_temperature': 29.1, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T08:00:00Z', - 'dew_point': 22.1, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.05, - 'temperature': 26.4, - 'uv_index': 0, - 'wind_bearing': 342, - 'wind_gust_speed': 14.75, - 'wind_speed': 6.26, - }), - dict({ - 'apparent_temperature': 27.9, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T09:00:00Z', - 'dew_point': 22.0, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.38, - 'temperature': 25.4, - 'uv_index': 0, - 'wind_bearing': 344, - 'wind_gust_speed': 10.43, - 'wind_speed': 5.2, - }), - dict({ - 'apparent_temperature': 26.9, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T10:00:00Z', - 'dew_point': 21.9, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.73, - 'temperature': 24.5, - 'uv_index': 0, - 'wind_bearing': 339, - 'wind_gust_speed': 6.95, - 'wind_speed': 3.59, - }), - dict({ - 'apparent_temperature': 26.4, - 'cloud_coverage': 51.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T11:00:00Z', - 'dew_point': 21.8, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.3, - 'temperature': 24.1, - 'uv_index': 0, - 'wind_bearing': 326, - 'wind_gust_speed': 5.27, - 'wind_speed': 2.1, - }), - dict({ - 'apparent_temperature': 26.1, - 'cloud_coverage': 53.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T12:00:00Z', - 'dew_point': 21.8, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.52, - 'temperature': 23.9, - 'uv_index': 0, - 'wind_bearing': 257, - 'wind_gust_speed': 5.48, - 'wind_speed': 0.93, - }), - dict({ - 'apparent_temperature': 25.8, - 'cloud_coverage': 56.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T13:00:00Z', - 'dew_point': 21.8, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.53, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 188, - 'wind_gust_speed': 4.44, - 'wind_speed': 1.79, - }), - dict({ - 'apparent_temperature': 25.3, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T14:00:00Z', - 'dew_point': 21.7, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.46, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 183, - 'wind_gust_speed': 4.49, - 'wind_speed': 2.19, - }), - dict({ - 'apparent_temperature': 24.6, - 'cloud_coverage': 45.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T15:00:00Z', - 'dew_point': 21.4, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.21, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 179, - 'wind_gust_speed': 5.32, - 'wind_speed': 2.65, - }), - dict({ - 'apparent_temperature': 24.0, - 'cloud_coverage': 42.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T16:00:00Z', - 'dew_point': 21.1, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.09, - 'temperature': 22.1, - 'uv_index': 0, - 'wind_bearing': 173, - 'wind_gust_speed': 5.81, - 'wind_speed': 3.2, - }), - dict({ - 'apparent_temperature': 23.7, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T17:00:00Z', - 'dew_point': 20.9, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.88, - 'temperature': 21.9, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 5.53, - 'wind_speed': 3.16, - }), - dict({ - 'apparent_temperature': 23.3, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T18:00:00Z', - 'dew_point': 20.7, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.94, - 'temperature': 21.6, - 'uv_index': 0, - 'wind_bearing': 153, - 'wind_gust_speed': 6.09, - 'wind_speed': 3.36, - }), - dict({ - 'apparent_temperature': 23.1, - 'cloud_coverage': 51.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T19:00:00Z', - 'dew_point': 20.5, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.96, - 'temperature': 21.4, - 'uv_index': 0, - 'wind_bearing': 150, - 'wind_gust_speed': 6.83, - 'wind_speed': 3.71, - }), - dict({ - 'apparent_temperature': 22.5, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T20:00:00Z', - 'dew_point': 20.0, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.29, - 'temperature': 21.0, - 'uv_index': 0, - 'wind_bearing': 156, - 'wind_gust_speed': 7.98, - 'wind_speed': 4.27, - }), - dict({ - 'apparent_temperature': 22.8, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T21:00:00Z', - 'dew_point': 20.2, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.61, - 'temperature': 21.2, - 'uv_index': 0, - 'wind_bearing': 156, - 'wind_gust_speed': 8.4, - 'wind_speed': 4.69, - }), - dict({ - 'apparent_temperature': 25.1, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T22:00:00Z', - 'dew_point': 21.3, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.87, - 'temperature': 23.1, - 'uv_index': 1, - 'wind_bearing': 150, - 'wind_gust_speed': 7.66, - 'wind_speed': 4.33, - }), - dict({ - 'apparent_temperature': 28.3, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T23:00:00Z', - 'dew_point': 22.3, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.93, - 'temperature': 25.6, - 'uv_index': 2, - 'wind_bearing': 123, - 'wind_gust_speed': 9.63, - 'wind_speed': 3.91, - }), - dict({ - 'apparent_temperature': 30.4, - 'cloud_coverage': 63.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T00:00:00Z', - 'dew_point': 22.6, - 'humidity': 75, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.93, - 'temperature': 27.4, - 'uv_index': 4, - 'wind_bearing': 105, - 'wind_gust_speed': 12.59, - 'wind_speed': 3.96, - }), - dict({ - 'apparent_temperature': 32.2, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T01:00:00Z', - 'dew_point': 22.9, - 'humidity': 70, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.79, - 'temperature': 28.9, - 'uv_index': 5, - 'wind_bearing': 99, - 'wind_gust_speed': 14.17, - 'wind_speed': 4.06, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 62.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-10T02:00:00Z', - 'dew_point': 22.9, - 'humidity': 66, - 'precipitation': 0.3, - 'precipitation_probability': 7.000000000000001, - 'pressure': 1011.29, - 'temperature': 29.9, - 'uv_index': 6, - 'wind_bearing': 93, - 'wind_gust_speed': 17.75, - 'wind_speed': 4.87, - }), - dict({ - 'apparent_temperature': 34.3, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T03:00:00Z', - 'dew_point': 23.1, - 'humidity': 64, - 'precipitation': 0.3, - 'precipitation_probability': 11.0, - 'pressure': 1010.78, - 'temperature': 30.6, - 'uv_index': 6, - 'wind_bearing': 78, - 'wind_gust_speed': 17.43, - 'wind_speed': 4.54, - }), - dict({ - 'apparent_temperature': 34.0, - 'cloud_coverage': 74.0, - 'condition': 'rainy', - 'datetime': '2023-09-10T04:00:00Z', - 'dew_point': 23.2, - 'humidity': 66, - 'precipitation': 0.4, - 'precipitation_probability': 15.0, - 'pressure': 1010.37, - 'temperature': 30.3, - 'uv_index': 5, - 'wind_bearing': 60, - 'wind_gust_speed': 15.24, - 'wind_speed': 4.9, - }), - dict({ - 'apparent_temperature': 33.7, - 'cloud_coverage': 79.0, - 'condition': 'rainy', - 'datetime': '2023-09-10T05:00:00Z', - 'dew_point': 23.3, - 'humidity': 67, - 'precipitation': 0.7, - 'precipitation_probability': 17.0, - 'pressure': 1010.09, - 'temperature': 30.0, - 'uv_index': 4, - 'wind_bearing': 80, - 'wind_gust_speed': 13.53, - 'wind_speed': 5.98, - }), - dict({ - 'apparent_temperature': 33.2, - 'cloud_coverage': 80.0, - 'condition': 'rainy', - 'datetime': '2023-09-10T06:00:00Z', - 'dew_point': 23.4, - 'humidity': 70, - 'precipitation': 1.0, - 'precipitation_probability': 17.0, - 'pressure': 1010.0, - 'temperature': 29.5, - 'uv_index': 3, - 'wind_bearing': 83, - 'wind_gust_speed': 12.55, - 'wind_speed': 6.84, - }), - dict({ - 'apparent_temperature': 32.3, - 'cloud_coverage': 88.0, - 'condition': 'rainy', - 'datetime': '2023-09-10T07:00:00Z', - 'dew_point': 23.4, - 'humidity': 73, - 'precipitation': 0.4, - 'precipitation_probability': 16.0, - 'pressure': 1010.27, - 'temperature': 28.7, - 'uv_index': 2, - 'wind_bearing': 90, - 'wind_gust_speed': 10.16, - 'wind_speed': 6.07, - }), - dict({ - 'apparent_temperature': 30.9, - 'cloud_coverage': 92.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T08:00:00Z', - 'dew_point': 23.2, - 'humidity': 77, - 'precipitation': 0.5, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1010.71, - 'temperature': 27.6, - 'uv_index': 0, - 'wind_bearing': 101, - 'wind_gust_speed': 8.18, - 'wind_speed': 4.82, - }), - dict({ - 'apparent_temperature': 29.7, - 'cloud_coverage': 93.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T09:00:00Z', - 'dew_point': 23.2, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.9, - 'temperature': 26.5, - 'uv_index': 0, - 'wind_bearing': 128, - 'wind_gust_speed': 8.89, - 'wind_speed': 4.95, - }), - dict({ - 'apparent_temperature': 28.6, - 'cloud_coverage': 88.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T10:00:00Z', - 'dew_point': 23.0, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.12, - 'temperature': 25.6, - 'uv_index': 0, - 'wind_bearing': 134, - 'wind_gust_speed': 10.03, - 'wind_speed': 4.52, - }), - dict({ - 'apparent_temperature': 27.9, - 'cloud_coverage': 87.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T11:00:00Z', - 'dew_point': 22.8, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.43, - 'temperature': 25.1, - 'uv_index': 0, - 'wind_bearing': 137, - 'wind_gust_speed': 12.4, - 'wind_speed': 5.41, - }), - dict({ - 'apparent_temperature': 27.4, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T12:00:00Z', - 'dew_point': 22.5, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.58, - 'temperature': 24.8, - 'uv_index': 0, - 'wind_bearing': 143, - 'wind_gust_speed': 16.36, - 'wind_speed': 6.31, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T13:00:00Z', - 'dew_point': 22.4, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.55, - 'temperature': 24.5, - 'uv_index': 0, - 'wind_bearing': 144, - 'wind_gust_speed': 19.66, - 'wind_speed': 7.23, - }), - dict({ - 'apparent_temperature': 26.8, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T14:00:00Z', - 'dew_point': 22.2, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.4, - 'temperature': 24.3, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 21.15, - 'wind_speed': 7.46, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T15:00:00Z', - 'dew_point': 22.0, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.23, - 'temperature': 23.9, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 22.26, - 'wind_speed': 7.84, - }), - dict({ - 'apparent_temperature': 26.1, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T16:00:00Z', - 'dew_point': 21.8, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.01, - 'temperature': 23.8, - 'uv_index': 0, - 'wind_bearing': 144, - 'wind_gust_speed': 23.53, - 'wind_speed': 8.63, - }), - dict({ - 'apparent_temperature': 25.6, - 'cloud_coverage': 61.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-10T17:00:00Z', - 'dew_point': 21.6, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.78, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 144, - 'wind_gust_speed': 22.83, - 'wind_speed': 8.61, - }), - dict({ - 'apparent_temperature': 25.4, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T18:00:00Z', - 'dew_point': 21.5, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.69, - 'temperature': 23.3, - 'uv_index': 0, - 'wind_bearing': 143, - 'wind_gust_speed': 23.7, - 'wind_speed': 8.7, - }), - dict({ - 'apparent_temperature': 25.2, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T19:00:00Z', - 'dew_point': 21.4, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.77, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 140, - 'wind_gust_speed': 24.24, - 'wind_speed': 8.74, - }), - dict({ - 'apparent_temperature': 25.5, - 'cloud_coverage': 89.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T20:00:00Z', - 'dew_point': 21.6, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.89, - 'temperature': 23.3, - 'uv_index': 0, - 'wind_bearing': 138, - 'wind_gust_speed': 23.99, - 'wind_speed': 8.81, - }), - dict({ - 'apparent_temperature': 25.9, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T21:00:00Z', - 'dew_point': 21.6, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.1, - 'temperature': 23.7, - 'uv_index': 0, - 'wind_bearing': 138, - 'wind_gust_speed': 25.55, - 'wind_speed': 9.05, - }), - dict({ - 'apparent_temperature': 27.0, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T22:00:00Z', - 'dew_point': 21.8, - 'humidity': 84, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.29, - 'temperature': 24.6, - 'uv_index': 1, - 'wind_bearing': 140, - 'wind_gust_speed': 29.08, - 'wind_speed': 10.37, - }), - dict({ - 'apparent_temperature': 28.4, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T23:00:00Z', - 'dew_point': 21.9, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.36, - 'temperature': 25.9, - 'uv_index': 2, - 'wind_bearing': 140, - 'wind_gust_speed': 34.13, - 'wind_speed': 12.56, - }), - dict({ - 'apparent_temperature': 30.1, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T00:00:00Z', - 'dew_point': 22.3, - 'humidity': 74, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.39, - 'temperature': 27.2, - 'uv_index': 3, - 'wind_bearing': 140, - 'wind_gust_speed': 38.2, - 'wind_speed': 15.65, - }), - dict({ - 'apparent_temperature': 31.4, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-11T01:00:00Z', - 'dew_point': 22.3, - 'humidity': 70, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.31, - 'temperature': 28.4, - 'uv_index': 5, - 'wind_bearing': 141, - 'wind_gust_speed': 37.55, - 'wind_speed': 15.78, - }), - dict({ - 'apparent_temperature': 32.7, - 'cloud_coverage': 63.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T02:00:00Z', - 'dew_point': 22.4, - 'humidity': 66, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.98, - 'temperature': 29.6, - 'uv_index': 6, - 'wind_bearing': 143, - 'wind_gust_speed': 35.86, - 'wind_speed': 15.41, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T03:00:00Z', - 'dew_point': 22.5, - 'humidity': 63, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.61, - 'temperature': 30.3, - 'uv_index': 6, - 'wind_bearing': 141, - 'wind_gust_speed': 35.88, - 'wind_speed': 15.51, - }), - dict({ - 'apparent_temperature': 33.8, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T04:00:00Z', - 'dew_point': 22.6, - 'humidity': 63, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.36, - 'temperature': 30.4, - 'uv_index': 5, - 'wind_bearing': 140, - 'wind_gust_speed': 35.99, - 'wind_speed': 15.75, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T05:00:00Z', - 'dew_point': 22.6, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.11, - 'temperature': 30.1, - 'uv_index': 4, - 'wind_bearing': 137, - 'wind_gust_speed': 33.61, - 'wind_speed': 15.36, - }), - dict({ - 'apparent_temperature': 33.2, - 'cloud_coverage': 77.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T06:00:00Z', - 'dew_point': 22.5, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.98, - 'temperature': 30.0, - 'uv_index': 3, - 'wind_bearing': 138, - 'wind_gust_speed': 32.61, - 'wind_speed': 14.98, - }), - dict({ - 'apparent_temperature': 32.3, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T07:00:00Z', - 'dew_point': 22.2, - 'humidity': 66, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.13, - 'temperature': 29.2, - 'uv_index': 2, - 'wind_bearing': 138, - 'wind_gust_speed': 28.1, - 'wind_speed': 13.88, - }), - dict({ - 'apparent_temperature': 31.2, - 'cloud_coverage': 56.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-11T08:00:00Z', - 'dew_point': 22.1, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.48, - 'temperature': 28.3, - 'uv_index': 0, - 'wind_bearing': 137, - 'wind_gust_speed': 24.22, - 'wind_speed': 13.02, - }), - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 55.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-11T09:00:00Z', - 'dew_point': 21.9, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.81, - 'temperature': 27.1, - 'uv_index': 0, - 'wind_bearing': 138, - 'wind_gust_speed': 22.5, - 'wind_speed': 11.94, - }), - dict({ - 'apparent_temperature': 28.8, - 'cloud_coverage': 63.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T10:00:00Z', - 'dew_point': 21.7, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.29, - 'temperature': 26.3, - 'uv_index': 0, - 'wind_bearing': 137, - 'wind_gust_speed': 21.47, - 'wind_speed': 11.25, - }), - dict({ - 'apparent_temperature': 28.1, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T11:00:00Z', - 'dew_point': 21.8, - 'humidity': 80, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.77, - 'temperature': 25.6, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 22.71, - 'wind_speed': 12.39, - }), - dict({ - 'apparent_temperature': 27.6, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T12:00:00Z', - 'dew_point': 21.8, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.97, - 'temperature': 25.2, - 'uv_index': 0, - 'wind_bearing': 143, - 'wind_gust_speed': 23.67, - 'wind_speed': 12.83, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 89.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T13:00:00Z', - 'dew_point': 21.7, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.97, - 'temperature': 24.7, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 23.34, - 'wind_speed': 12.62, - }), - dict({ - 'apparent_temperature': 26.7, - 'cloud_coverage': 88.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T14:00:00Z', - 'dew_point': 21.7, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.83, - 'temperature': 24.4, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 22.9, - 'wind_speed': 12.07, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 90.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T15:00:00Z', - 'dew_point': 21.6, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.74, - 'temperature': 24.1, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 22.01, - 'wind_speed': 11.19, - }), - dict({ - 'apparent_temperature': 25.9, - 'cloud_coverage': 88.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T16:00:00Z', - 'dew_point': 21.6, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.56, - 'temperature': 23.7, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 21.29, - 'wind_speed': 10.97, - }), - dict({ - 'apparent_temperature': 25.8, - 'cloud_coverage': 85.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T17:00:00Z', - 'dew_point': 21.5, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.35, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 150, - 'wind_gust_speed': 20.52, - 'wind_speed': 10.5, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T18:00:00Z', - 'dew_point': 21.4, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.3, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 20.04, - 'wind_speed': 10.51, - }), - dict({ - 'apparent_temperature': 25.4, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T19:00:00Z', - 'dew_point': 21.3, - 'humidity': 88, - 'precipitation': 0.3, - 'precipitation_probability': 12.0, - 'pressure': 1011.37, - 'temperature': 23.4, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 18.07, - 'wind_speed': 10.13, - }), - dict({ - 'apparent_temperature': 25.2, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T20:00:00Z', - 'dew_point': 21.2, - 'humidity': 89, - 'precipitation': 0.2, - 'precipitation_probability': 13.0, - 'pressure': 1011.53, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 16.86, - 'wind_speed': 10.34, - }), - dict({ - 'apparent_temperature': 25.5, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T21:00:00Z', - 'dew_point': 21.4, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.71, - 'temperature': 23.4, - 'uv_index': 0, - 'wind_bearing': 138, - 'wind_gust_speed': 16.66, - 'wind_speed': 10.68, - }), - dict({ - 'apparent_temperature': 26.8, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T22:00:00Z', - 'dew_point': 21.9, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.94, - 'temperature': 24.4, - 'uv_index': 1, - 'wind_bearing': 137, - 'wind_gust_speed': 17.21, - 'wind_speed': 10.61, - }), - dict({ - 'apparent_temperature': 28.2, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T23:00:00Z', - 'dew_point': 22.3, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.05, - 'temperature': 25.6, - 'uv_index': 2, - 'wind_bearing': 138, - 'wind_gust_speed': 19.23, - 'wind_speed': 11.13, - }), - dict({ - 'apparent_temperature': 29.5, - 'cloud_coverage': 79.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T00:00:00Z', - 'dew_point': 22.6, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.07, - 'temperature': 26.6, - 'uv_index': 3, - 'wind_bearing': 140, - 'wind_gust_speed': 20.61, - 'wind_speed': 11.13, - }), - dict({ - 'apparent_temperature': 31.2, - 'cloud_coverage': 82.0, - 'condition': 'rainy', - 'datetime': '2023-09-12T01:00:00Z', - 'dew_point': 23.1, - 'humidity': 75, - 'precipitation': 0.2, - 'precipitation_probability': 16.0, - 'pressure': 1011.89, - 'temperature': 27.9, - 'uv_index': 4, - 'wind_bearing': 141, - 'wind_gust_speed': 23.35, - 'wind_speed': 11.98, - }), - dict({ - 'apparent_temperature': 32.6, - 'cloud_coverage': 85.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T02:00:00Z', - 'dew_point': 23.5, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.53, - 'temperature': 29.0, - 'uv_index': 5, - 'wind_bearing': 143, - 'wind_gust_speed': 26.45, - 'wind_speed': 13.01, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T03:00:00Z', - 'dew_point': 23.5, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.15, - 'temperature': 29.8, - 'uv_index': 5, - 'wind_bearing': 141, - 'wind_gust_speed': 28.95, - 'wind_speed': 13.9, - }), - dict({ - 'apparent_temperature': 34.0, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T04:00:00Z', - 'dew_point': 23.4, - 'humidity': 67, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.79, - 'temperature': 30.2, - 'uv_index': 5, - 'wind_bearing': 141, - 'wind_gust_speed': 27.9, - 'wind_speed': 13.95, - }), - dict({ - 'apparent_temperature': 34.0, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T05:00:00Z', - 'dew_point': 23.1, - 'humidity': 65, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.43, - 'temperature': 30.4, - 'uv_index': 4, - 'wind_bearing': 140, - 'wind_gust_speed': 26.53, - 'wind_speed': 13.78, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 56.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T06:00:00Z', - 'dew_point': 22.6, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.21, - 'temperature': 30.1, - 'uv_index': 3, - 'wind_bearing': 138, - 'wind_gust_speed': 24.56, - 'wind_speed': 13.74, - }), - dict({ - 'apparent_temperature': 32.0, - 'cloud_coverage': 53.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T07:00:00Z', - 'dew_point': 22.1, - 'humidity': 66, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.26, - 'temperature': 29.1, - 'uv_index': 2, - 'wind_bearing': 138, - 'wind_gust_speed': 22.78, - 'wind_speed': 13.21, - }), - dict({ - 'apparent_temperature': 30.9, - 'cloud_coverage': 48.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T08:00:00Z', - 'dew_point': 21.9, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.51, - 'temperature': 28.1, - 'uv_index': 0, - 'wind_bearing': 140, - 'wind_gust_speed': 19.92, - 'wind_speed': 12.0, - }), - dict({ - 'apparent_temperature': 29.7, - 'cloud_coverage': 50.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T09:00:00Z', - 'dew_point': 21.7, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.8, - 'temperature': 27.2, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 17.65, - 'wind_speed': 10.97, - }), - dict({ - 'apparent_temperature': 28.6, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T10:00:00Z', - 'dew_point': 21.4, - 'humidity': 75, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.23, - 'temperature': 26.2, - 'uv_index': 0, - 'wind_bearing': 143, - 'wind_gust_speed': 15.87, - 'wind_speed': 10.23, - }), - dict({ - 'apparent_temperature': 27.6, - 'cloud_coverage': 56.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T11:00:00Z', - 'dew_point': 21.3, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1011.79, - 'temperature': 25.4, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 13.9, - 'wind_speed': 9.39, - }), - dict({ - 'apparent_temperature': 26.8, - 'cloud_coverage': 60.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T12:00:00Z', - 'dew_point': 21.2, - 'humidity': 81, - 'precipitation': 0.0, - 'precipitation_probability': 47.0, - 'pressure': 1012.12, - 'temperature': 24.7, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 13.32, - 'wind_speed': 8.9, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T13:00:00Z', - 'dew_point': 21.2, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1012.18, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 13.18, - 'wind_speed': 8.59, - }), - dict({ - 'apparent_temperature': 26.0, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T14:00:00Z', - 'dew_point': 21.3, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.09, - 'temperature': 23.9, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 13.84, - 'wind_speed': 8.87, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T15:00:00Z', - 'dew_point': 21.3, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.99, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 15.08, - 'wind_speed': 8.93, - }), - dict({ - 'apparent_temperature': 25.1, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T16:00:00Z', - 'dew_point': 21.0, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.93, - 'temperature': 23.2, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 16.74, - 'wind_speed': 9.49, - }), - dict({ - 'apparent_temperature': 24.7, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T17:00:00Z', - 'dew_point': 20.8, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.75, - 'temperature': 22.9, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 17.45, - 'wind_speed': 9.12, - }), - dict({ - 'apparent_temperature': 24.4, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T18:00:00Z', - 'dew_point': 20.7, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.77, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 17.04, - 'wind_speed': 8.68, - }), - dict({ - 'apparent_temperature': 24.1, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T19:00:00Z', - 'dew_point': 20.6, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.93, - 'temperature': 22.4, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 16.8, - 'wind_speed': 8.61, - }), - dict({ - 'apparent_temperature': 23.9, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T20:00:00Z', - 'dew_point': 20.5, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.23, - 'temperature': 22.1, - 'uv_index': 0, - 'wind_bearing': 150, - 'wind_gust_speed': 15.35, - 'wind_speed': 8.36, - }), - dict({ - 'apparent_temperature': 24.4, - 'cloud_coverage': 75.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T21:00:00Z', - 'dew_point': 20.6, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.49, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 155, - 'wind_gust_speed': 14.09, - 'wind_speed': 7.77, - }), - dict({ - 'apparent_temperature': 25.8, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T22:00:00Z', - 'dew_point': 21.0, - 'humidity': 84, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.72, - 'temperature': 23.8, - 'uv_index': 1, - 'wind_bearing': 152, - 'wind_gust_speed': 14.04, - 'wind_speed': 7.25, - }), - dict({ - 'apparent_temperature': 27.8, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T23:00:00Z', - 'dew_point': 21.4, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.85, - 'temperature': 25.5, - 'uv_index': 2, - 'wind_bearing': 149, - 'wind_gust_speed': 15.31, - 'wind_speed': 7.14, - }), - dict({ - 'apparent_temperature': 29.7, - 'cloud_coverage': 60.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-13T00:00:00Z', - 'dew_point': 21.8, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.89, - 'temperature': 27.1, - 'uv_index': 4, - 'wind_bearing': 141, - 'wind_gust_speed': 16.42, - 'wind_speed': 6.89, - }), - dict({ - 'apparent_temperature': 31.2, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T01:00:00Z', - 'dew_point': 22.0, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.65, - 'temperature': 28.4, - 'uv_index': 5, - 'wind_bearing': 137, - 'wind_gust_speed': 18.64, - 'wind_speed': 6.65, - }), - dict({ - 'apparent_temperature': 32.3, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T02:00:00Z', - 'dew_point': 21.9, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.26, - 'temperature': 29.4, - 'uv_index': 5, - 'wind_bearing': 128, - 'wind_gust_speed': 21.69, - 'wind_speed': 7.12, - }), - dict({ - 'apparent_temperature': 33.0, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T03:00:00Z', - 'dew_point': 21.9, - 'humidity': 62, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.88, - 'temperature': 30.1, - 'uv_index': 6, - 'wind_bearing': 111, - 'wind_gust_speed': 23.41, - 'wind_speed': 7.33, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 72.0, - 'condition': 'rainy', - 'datetime': '2023-09-13T04:00:00Z', - 'dew_point': 22.0, - 'humidity': 61, - 'precipitation': 0.9, - 'precipitation_probability': 12.0, - 'pressure': 1011.55, - 'temperature': 30.4, - 'uv_index': 5, - 'wind_bearing': 56, - 'wind_gust_speed': 23.1, - 'wind_speed': 8.09, - }), - dict({ - 'apparent_temperature': 33.2, - 'cloud_coverage': 72.0, - 'condition': 'rainy', - 'datetime': '2023-09-13T05:00:00Z', - 'dew_point': 21.9, - 'humidity': 61, - 'precipitation': 1.9, - 'precipitation_probability': 12.0, - 'pressure': 1011.29, - 'temperature': 30.2, - 'uv_index': 4, - 'wind_bearing': 20, - 'wind_gust_speed': 21.81, - 'wind_speed': 9.46, - }), - dict({ - 'apparent_temperature': 32.6, - 'cloud_coverage': 74.0, - 'condition': 'rainy', - 'datetime': '2023-09-13T06:00:00Z', - 'dew_point': 21.9, - 'humidity': 63, - 'precipitation': 2.3, - 'precipitation_probability': 11.0, - 'pressure': 1011.17, - 'temperature': 29.7, - 'uv_index': 3, - 'wind_bearing': 20, - 'wind_gust_speed': 19.72, - 'wind_speed': 9.8, - }), - dict({ - 'apparent_temperature': 31.8, - 'cloud_coverage': 69.0, - 'condition': 'rainy', - 'datetime': '2023-09-13T07:00:00Z', - 'dew_point': 22.4, - 'humidity': 68, - 'precipitation': 1.8, - 'precipitation_probability': 10.0, - 'pressure': 1011.32, - 'temperature': 28.8, - 'uv_index': 1, - 'wind_bearing': 18, - 'wind_gust_speed': 17.55, - 'wind_speed': 9.23, - }), - dict({ - 'apparent_temperature': 30.8, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T08:00:00Z', - 'dew_point': 22.9, - 'humidity': 76, - 'precipitation': 0.8, - 'precipitation_probability': 10.0, - 'pressure': 1011.6, - 'temperature': 27.6, - 'uv_index': 0, - 'wind_bearing': 27, - 'wind_gust_speed': 15.08, - 'wind_speed': 8.05, - }), - dict({ - 'apparent_temperature': 29.4, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T09:00:00Z', - 'dew_point': 23.0, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.94, - 'temperature': 26.3, - 'uv_index': 0, - 'wind_bearing': 32, - 'wind_gust_speed': 12.17, - 'wind_speed': 6.68, - }), - dict({ - 'apparent_temperature': 28.5, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T10:00:00Z', - 'dew_point': 22.9, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.3, - 'temperature': 25.5, - 'uv_index': 0, - 'wind_bearing': 69, - 'wind_gust_speed': 11.64, - 'wind_speed': 6.69, - }), - dict({ - 'apparent_temperature': 27.7, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T11:00:00Z', - 'dew_point': 22.6, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.71, - 'temperature': 25.0, - 'uv_index': 0, - 'wind_bearing': 155, - 'wind_gust_speed': 11.91, - 'wind_speed': 6.23, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T12:00:00Z', - 'dew_point': 22.3, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.96, - 'temperature': 24.5, - 'uv_index': 0, - 'wind_bearing': 161, - 'wind_gust_speed': 12.47, - 'wind_speed': 5.73, - }), - dict({ - 'apparent_temperature': 26.7, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T13:00:00Z', - 'dew_point': 22.3, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.03, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 161, - 'wind_gust_speed': 13.57, - 'wind_speed': 5.66, - }), - dict({ - 'apparent_temperature': 26.4, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T14:00:00Z', - 'dew_point': 22.2, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.99, - 'temperature': 23.9, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 15.07, - 'wind_speed': 5.83, - }), - dict({ - 'apparent_temperature': 26.1, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T15:00:00Z', - 'dew_point': 22.2, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.95, - 'temperature': 23.7, - 'uv_index': 0, - 'wind_bearing': 158, - 'wind_gust_speed': 16.06, - 'wind_speed': 5.93, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 88.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T16:00:00Z', - 'dew_point': 22.0, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.9, - 'temperature': 23.4, - 'uv_index': 0, - 'wind_bearing': 153, - 'wind_gust_speed': 16.05, - 'wind_speed': 5.75, - }), - dict({ - 'apparent_temperature': 25.4, - 'cloud_coverage': 90.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T17:00:00Z', - 'dew_point': 21.8, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.85, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 150, - 'wind_gust_speed': 15.52, - 'wind_speed': 5.49, - }), - dict({ - 'apparent_temperature': 25.2, - 'cloud_coverage': 92.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T18:00:00Z', - 'dew_point': 21.8, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.87, - 'temperature': 23.0, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 15.01, - 'wind_speed': 5.32, - }), - dict({ - 'apparent_temperature': 25.0, - 'cloud_coverage': 90.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T19:00:00Z', - 'dew_point': 21.7, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.01, - 'temperature': 22.8, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 14.39, - 'wind_speed': 5.33, - }), - dict({ - 'apparent_temperature': 24.8, - 'cloud_coverage': 89.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T20:00:00Z', - 'dew_point': 21.6, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.22, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 13.79, - 'wind_speed': 5.43, - }), - dict({ - 'apparent_temperature': 25.3, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T21:00:00Z', - 'dew_point': 21.8, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.41, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 14.12, - 'wind_speed': 5.52, - }), - dict({ - 'apparent_temperature': 26.7, - 'cloud_coverage': 77.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T22:00:00Z', - 'dew_point': 22.1, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.59, - 'temperature': 24.3, - 'uv_index': 1, - 'wind_bearing': 147, - 'wind_gust_speed': 16.14, - 'wind_speed': 5.58, - }), - dict({ - 'apparent_temperature': 28.4, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T23:00:00Z', - 'dew_point': 22.4, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.74, - 'temperature': 25.7, - 'uv_index': 2, - 'wind_bearing': 146, - 'wind_gust_speed': 19.09, - 'wind_speed': 5.62, - }), - dict({ - 'apparent_temperature': 30.5, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T00:00:00Z', - 'dew_point': 22.9, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.78, - 'temperature': 27.4, - 'uv_index': 4, - 'wind_bearing': 143, - 'wind_gust_speed': 21.6, - 'wind_speed': 5.58, - }), - dict({ - 'apparent_temperature': 32.2, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T01:00:00Z', - 'dew_point': 23.2, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.61, - 'temperature': 28.7, - 'uv_index': 5, - 'wind_bearing': 138, - 'wind_gust_speed': 23.36, - 'wind_speed': 5.34, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T02:00:00Z', - 'dew_point': 23.2, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.32, - 'temperature': 29.9, - 'uv_index': 6, - 'wind_bearing': 111, - 'wind_gust_speed': 24.72, - 'wind_speed': 4.99, - }), - dict({ - 'apparent_temperature': 34.4, - 'cloud_coverage': 56.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T03:00:00Z', - 'dew_point': 23.3, - 'humidity': 65, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.04, - 'temperature': 30.7, - 'uv_index': 6, - 'wind_bearing': 354, - 'wind_gust_speed': 25.23, - 'wind_speed': 4.74, - }), - dict({ - 'apparent_temperature': 34.9, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T04:00:00Z', - 'dew_point': 23.4, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.77, - 'temperature': 31.0, - 'uv_index': 6, - 'wind_bearing': 341, - 'wind_gust_speed': 24.6, - 'wind_speed': 4.79, - }), - dict({ - 'apparent_temperature': 34.5, - 'cloud_coverage': 60.0, - 'condition': 'rainy', - 'datetime': '2023-09-14T05:00:00Z', - 'dew_point': 23.2, - 'humidity': 64, - 'precipitation': 0.2, - 'precipitation_probability': 15.0, - 'pressure': 1012.53, - 'temperature': 30.7, - 'uv_index': 5, - 'wind_bearing': 336, - 'wind_gust_speed': 23.28, - 'wind_speed': 5.07, - }), - dict({ - 'apparent_temperature': 33.8, - 'cloud_coverage': 59.0, - 'condition': 'rainy', - 'datetime': '2023-09-14T06:00:00Z', - 'dew_point': 23.1, - 'humidity': 66, - 'precipitation': 0.2, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1012.49, - 'temperature': 30.2, - 'uv_index': 3, - 'wind_bearing': 336, - 'wind_gust_speed': 22.05, - 'wind_speed': 5.34, - }), - dict({ - 'apparent_temperature': 32.9, - 'cloud_coverage': 53.0, - 'condition': 'rainy', - 'datetime': '2023-09-14T07:00:00Z', - 'dew_point': 23.0, - 'humidity': 68, - 'precipitation': 0.2, - 'precipitation_probability': 40.0, - 'pressure': 1012.73, - 'temperature': 29.5, - 'uv_index': 2, - 'wind_bearing': 339, - 'wind_gust_speed': 21.18, - 'wind_speed': 5.63, - }), - dict({ - 'apparent_temperature': 31.6, - 'cloud_coverage': 43.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T08:00:00Z', - 'dew_point': 22.8, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 45.0, - 'pressure': 1013.16, - 'temperature': 28.4, - 'uv_index': 0, - 'wind_bearing': 342, - 'wind_gust_speed': 20.35, - 'wind_speed': 5.93, - }), - dict({ - 'apparent_temperature': 30.0, - 'cloud_coverage': 35.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T09:00:00Z', - 'dew_point': 22.5, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1013.62, - 'temperature': 27.1, - 'uv_index': 0, - 'wind_bearing': 347, - 'wind_gust_speed': 19.42, - 'wind_speed': 5.95, - }), - dict({ - 'apparent_temperature': 29.0, - 'cloud_coverage': 32.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T10:00:00Z', - 'dew_point': 22.4, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.09, - 'temperature': 26.3, - 'uv_index': 0, - 'wind_bearing': 348, - 'wind_gust_speed': 18.19, - 'wind_speed': 5.31, - }), - dict({ - 'apparent_temperature': 28.2, - 'cloud_coverage': 31.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T11:00:00Z', - 'dew_point': 22.4, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.56, - 'temperature': 25.5, - 'uv_index': 0, - 'wind_bearing': 177, - 'wind_gust_speed': 16.79, - 'wind_speed': 4.28, - }), - dict({ - 'apparent_temperature': 27.5, - 'cloud_coverage': 31.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T12:00:00Z', - 'dew_point': 22.3, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.87, - 'temperature': 24.9, - 'uv_index': 0, - 'wind_bearing': 171, - 'wind_gust_speed': 15.61, - 'wind_speed': 3.72, - }), - dict({ - 'apparent_temperature': 26.6, - 'cloud_coverage': 31.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T13:00:00Z', - 'dew_point': 22.1, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.91, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 171, - 'wind_gust_speed': 14.7, - 'wind_speed': 4.11, - }), - dict({ - 'apparent_temperature': 25.9, - 'cloud_coverage': 32.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T14:00:00Z', - 'dew_point': 21.9, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.8, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 171, - 'wind_gust_speed': 13.81, - 'wind_speed': 4.97, - }), - dict({ - 'apparent_temperature': 25.3, - 'cloud_coverage': 34.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T15:00:00Z', - 'dew_point': 21.7, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.66, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 170, - 'wind_gust_speed': 12.88, - 'wind_speed': 5.57, - }), - dict({ - 'apparent_temperature': 24.8, - 'cloud_coverage': 37.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T16:00:00Z', - 'dew_point': 21.5, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.54, - 'temperature': 22.7, - 'uv_index': 0, - 'wind_bearing': 168, - 'wind_gust_speed': 12.0, - 'wind_speed': 5.62, - }), - dict({ - 'apparent_temperature': 24.4, - 'cloud_coverage': 39.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T17:00:00Z', - 'dew_point': 21.3, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.45, - 'temperature': 22.4, - 'uv_index': 0, - 'wind_bearing': 165, - 'wind_gust_speed': 11.43, - 'wind_speed': 5.48, - }), - dict({ - 'apparent_temperature': 24.6, - 'cloud_coverage': 40.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T18:00:00Z', - 'dew_point': 21.4, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 44.0, - 'pressure': 1014.45, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 11.42, - 'wind_speed': 5.38, - }), - dict({ - 'apparent_temperature': 25.0, - 'cloud_coverage': 40.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T19:00:00Z', - 'dew_point': 21.6, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 52.0, - 'pressure': 1014.63, - 'temperature': 22.9, - 'uv_index': 0, - 'wind_bearing': 161, - 'wind_gust_speed': 12.15, - 'wind_speed': 5.39, - }), - dict({ - 'apparent_temperature': 25.6, - 'cloud_coverage': 38.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T20:00:00Z', - 'dew_point': 21.8, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 51.0, - 'pressure': 1014.91, - 'temperature': 23.4, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 13.54, - 'wind_speed': 5.45, - }), - dict({ - 'apparent_temperature': 26.6, - 'cloud_coverage': 36.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T21:00:00Z', - 'dew_point': 22.0, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 42.0, - 'pressure': 1015.18, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 158, - 'wind_gust_speed': 15.48, - 'wind_speed': 5.62, - }), - dict({ - 'apparent_temperature': 28.5, - 'cloud_coverage': 32.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T22:00:00Z', - 'dew_point': 22.5, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 28.999999999999996, - 'pressure': 1015.4, - 'temperature': 25.7, - 'uv_index': 1, - 'wind_bearing': 158, - 'wind_gust_speed': 17.86, - 'wind_speed': 5.84, - }), - dict({ - 'apparent_temperature': 30.3, - 'cloud_coverage': 30.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T23:00:00Z', - 'dew_point': 22.9, - 'humidity': 77, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.54, - 'temperature': 27.2, - 'uv_index': 2, - 'wind_bearing': 155, - 'wind_gust_speed': 20.19, - 'wind_speed': 6.09, - }), - dict({ - 'apparent_temperature': 32.1, - 'cloud_coverage': 30.0, - 'condition': 'sunny', - 'datetime': '2023-09-15T00:00:00Z', - 'dew_point': 23.3, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.55, - 'temperature': 28.6, - 'uv_index': 4, - 'wind_bearing': 152, - 'wind_gust_speed': 21.83, - 'wind_speed': 6.42, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 34.0, - 'condition': 'sunny', - 'datetime': '2023-09-15T01:00:00Z', - 'dew_point': 23.5, - 'humidity': 70, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.35, - 'temperature': 29.6, - 'uv_index': 6, - 'wind_bearing': 144, - 'wind_gust_speed': 22.56, - 'wind_speed': 6.91, - }), - dict({ - 'apparent_temperature': 34.2, - 'cloud_coverage': 41.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T02:00:00Z', - 'dew_point': 23.5, - 'humidity': 67, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.0, - 'temperature': 30.4, - 'uv_index': 7, - 'wind_bearing': 336, - 'wind_gust_speed': 22.83, - 'wind_speed': 7.47, - }), - dict({ - 'apparent_temperature': 34.9, - 'cloud_coverage': 46.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T03:00:00Z', - 'dew_point': 23.5, - 'humidity': 65, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.62, - 'temperature': 30.9, - 'uv_index': 7, - 'wind_bearing': 336, - 'wind_gust_speed': 22.98, - 'wind_speed': 7.95, - }), - dict({ - 'apparent_temperature': 35.4, - 'cloud_coverage': 46.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T04:00:00Z', - 'dew_point': 23.6, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.25, - 'temperature': 31.3, - 'uv_index': 6, - 'wind_bearing': 341, - 'wind_gust_speed': 23.21, - 'wind_speed': 8.44, - }), - dict({ - 'apparent_temperature': 35.6, - 'cloud_coverage': 44.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T05:00:00Z', - 'dew_point': 23.7, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.95, - 'temperature': 31.5, - 'uv_index': 5, - 'wind_bearing': 344, - 'wind_gust_speed': 23.46, - 'wind_speed': 8.95, - }), - dict({ - 'apparent_temperature': 35.1, - 'cloud_coverage': 42.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T06:00:00Z', - 'dew_point': 23.6, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.83, - 'temperature': 31.1, - 'uv_index': 3, - 'wind_bearing': 347, - 'wind_gust_speed': 23.64, - 'wind_speed': 9.13, - }), - dict({ - 'apparent_temperature': 34.1, - 'cloud_coverage': 41.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T07:00:00Z', - 'dew_point': 23.4, - 'humidity': 66, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.96, - 'temperature': 30.3, - 'uv_index': 2, - 'wind_bearing': 350, - 'wind_gust_speed': 23.66, - 'wind_speed': 8.78, - }), - dict({ - 'apparent_temperature': 32.4, - 'cloud_coverage': 40.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T08:00:00Z', - 'dew_point': 23.1, - 'humidity': 70, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.25, - 'temperature': 29.0, - 'uv_index': 0, - 'wind_bearing': 356, - 'wind_gust_speed': 23.51, - 'wind_speed': 8.13, - }), - dict({ - 'apparent_temperature': 31.1, - 'cloud_coverage': 41.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T09:00:00Z', - 'dew_point': 22.9, - 'humidity': 74, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.61, - 'temperature': 27.9, - 'uv_index': 0, - 'wind_bearing': 3, - 'wind_gust_speed': 23.21, - 'wind_speed': 7.48, - }), - dict({ - 'apparent_temperature': 30.0, - 'cloud_coverage': 43.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T10:00:00Z', - 'dew_point': 22.8, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.02, - 'temperature': 26.9, - 'uv_index': 0, - 'wind_bearing': 20, - 'wind_gust_speed': 22.68, - 'wind_speed': 6.83, - }), - dict({ - 'apparent_temperature': 29.2, - 'cloud_coverage': 46.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T11:00:00Z', - 'dew_point': 22.8, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.43, - 'temperature': 26.2, - 'uv_index': 0, - 'wind_bearing': 129, - 'wind_gust_speed': 22.04, - 'wind_speed': 6.1, - }), - dict({ - 'apparent_temperature': 28.4, - 'cloud_coverage': 48.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T12:00:00Z', - 'dew_point': 22.7, - 'humidity': 84, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.71, - 'temperature': 25.6, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 21.64, - 'wind_speed': 5.6, - }), - dict({ - 'apparent_temperature': 28.2, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T13:00:00Z', - 'dew_point': 23.2, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.52, - 'temperature': 25.2, - 'uv_index': 0, - 'wind_bearing': 164, - 'wind_gust_speed': 16.35, - 'wind_speed': 5.58, - }), - dict({ - 'apparent_temperature': 27.4, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T14:00:00Z', - 'dew_point': 22.9, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.37, - 'temperature': 24.6, - 'uv_index': 0, - 'wind_bearing': 168, - 'wind_gust_speed': 17.11, - 'wind_speed': 5.79, - }), - dict({ - 'apparent_temperature': 26.9, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T15:00:00Z', - 'dew_point': 22.7, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.21, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 182, - 'wind_gust_speed': 17.32, - 'wind_speed': 5.77, - }), - dict({ - 'apparent_temperature': 26.4, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T16:00:00Z', - 'dew_point': 22.6, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.07, - 'temperature': 23.8, - 'uv_index': 0, - 'wind_bearing': 201, - 'wind_gust_speed': 16.6, - 'wind_speed': 5.27, - }), - dict({ - 'apparent_temperature': 26.0, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T17:00:00Z', - 'dew_point': 22.5, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.95, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 219, - 'wind_gust_speed': 15.52, - 'wind_speed': 4.62, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T18:00:00Z', - 'dew_point': 22.3, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.88, - 'temperature': 23.3, - 'uv_index': 0, - 'wind_bearing': 216, - 'wind_gust_speed': 14.64, - 'wind_speed': 4.32, - }), - dict({ - 'apparent_temperature': 26.0, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T19:00:00Z', - 'dew_point': 22.4, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.91, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 198, - 'wind_gust_speed': 14.06, - 'wind_speed': 4.73, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T20:00:00Z', - 'dew_point': 22.4, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.99, - 'temperature': 23.8, - 'uv_index': 0, - 'wind_bearing': 189, - 'wind_gust_speed': 13.7, - 'wind_speed': 5.49, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T21:00:00Z', - 'dew_point': 22.5, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.07, - 'temperature': 24.4, - 'uv_index': 0, - 'wind_bearing': 183, - 'wind_gust_speed': 13.77, - 'wind_speed': 5.95, - }), - dict({ - 'apparent_temperature': 28.3, - 'cloud_coverage': 59.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T22:00:00Z', - 'dew_point': 22.6, - 'humidity': 84, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.12, - 'temperature': 25.5, - 'uv_index': 1, - 'wind_bearing': 179, - 'wind_gust_speed': 14.38, - 'wind_speed': 5.77, - }), - dict({ - 'apparent_temperature': 29.9, - 'cloud_coverage': 52.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T23:00:00Z', - 'dew_point': 22.9, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.13, - 'temperature': 26.9, - 'uv_index': 2, - 'wind_bearing': 170, - 'wind_gust_speed': 15.2, - 'wind_speed': 5.27, - }), - dict({ - 'apparent_temperature': 31.2, - 'cloud_coverage': 44.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T00:00:00Z', - 'dew_point': 22.9, - 'humidity': 74, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.04, - 'temperature': 28.0, - 'uv_index': 4, - 'wind_bearing': 155, - 'wind_gust_speed': 15.85, - 'wind_speed': 4.76, - }), - dict({ - 'apparent_temperature': 32.5, - 'cloud_coverage': 24.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T01:00:00Z', - 'dew_point': 22.6, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.52, - 'temperature': 29.2, - 'uv_index': 6, - 'wind_bearing': 110, - 'wind_gust_speed': 16.27, - 'wind_speed': 6.81, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 16.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T02:00:00Z', - 'dew_point': 22.4, - 'humidity': 63, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.01, - 'temperature': 30.2, - 'uv_index': 8, - 'wind_bearing': 30, - 'wind_gust_speed': 16.55, - 'wind_speed': 6.86, - }), - dict({ - 'apparent_temperature': 34.2, - 'cloud_coverage': 10.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T03:00:00Z', - 'dew_point': 22.0, - 'humidity': 59, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.45, - 'temperature': 31.1, - 'uv_index': 8, - 'wind_bearing': 17, - 'wind_gust_speed': 16.52, - 'wind_speed': 6.8, - }), - dict({ - 'apparent_temperature': 34.7, - 'cloud_coverage': 10.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T04:00:00Z', - 'dew_point': 21.9, - 'humidity': 57, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.89, - 'temperature': 31.5, - 'uv_index': 8, - 'wind_bearing': 17, - 'wind_gust_speed': 16.08, - 'wind_speed': 6.62, - }), - dict({ - 'apparent_temperature': 34.9, - 'cloud_coverage': 10.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T05:00:00Z', - 'dew_point': 21.9, - 'humidity': 56, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.39, - 'temperature': 31.8, - 'uv_index': 6, - 'wind_bearing': 20, - 'wind_gust_speed': 15.48, - 'wind_speed': 6.45, - }), - dict({ - 'apparent_temperature': 34.5, - 'cloud_coverage': 10.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T06:00:00Z', - 'dew_point': 21.7, - 'humidity': 56, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.11, - 'temperature': 31.4, - 'uv_index': 4, - 'wind_bearing': 26, - 'wind_gust_speed': 15.08, - 'wind_speed': 6.43, - }), - dict({ - 'apparent_temperature': 33.6, - 'cloud_coverage': 7.000000000000001, - 'condition': 'sunny', - 'datetime': '2023-09-16T07:00:00Z', - 'dew_point': 21.7, - 'humidity': 59, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.15, - 'temperature': 30.7, - 'uv_index': 2, - 'wind_bearing': 39, - 'wind_gust_speed': 14.88, - 'wind_speed': 6.61, - }), - dict({ - 'apparent_temperature': 32.5, - 'cloud_coverage': 2.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T08:00:00Z', - 'dew_point': 21.9, - 'humidity': 63, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.41, - 'temperature': 29.6, - 'uv_index': 0, - 'wind_bearing': 72, - 'wind_gust_speed': 14.82, - 'wind_speed': 6.95, - }), - dict({ - 'apparent_temperature': 31.4, - 'cloud_coverage': 2.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T09:00:00Z', - 'dew_point': 22.1, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.75, - 'temperature': 28.5, - 'uv_index': 0, - 'wind_bearing': 116, - 'wind_gust_speed': 15.13, - 'wind_speed': 7.45, - }), - dict({ - 'apparent_temperature': 30.5, - 'cloud_coverage': 13.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T10:00:00Z', - 'dew_point': 22.3, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.13, - 'temperature': 27.6, - 'uv_index': 0, - 'wind_bearing': 140, - 'wind_gust_speed': 16.09, - 'wind_speed': 8.15, - }), - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 31.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T11:00:00Z', - 'dew_point': 22.6, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.47, - 'temperature': 26.9, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 17.37, - 'wind_speed': 8.87, - }), - dict({ - 'apparent_temperature': 29.3, - 'cloud_coverage': 45.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T12:00:00Z', - 'dew_point': 22.9, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.6, - 'temperature': 26.3, - 'uv_index': 0, - 'wind_bearing': 155, - 'wind_gust_speed': 18.29, - 'wind_speed': 9.21, - }), - dict({ - 'apparent_temperature': 28.7, - 'cloud_coverage': 51.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T13:00:00Z', - 'dew_point': 23.0, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.41, - 'temperature': 25.7, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 18.49, - 'wind_speed': 8.96, - }), - dict({ - 'apparent_temperature': 27.9, - 'cloud_coverage': 55.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T14:00:00Z', - 'dew_point': 22.8, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.01, - 'temperature': 25.0, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 18.47, - 'wind_speed': 8.45, - }), - dict({ - 'apparent_temperature': 27.2, - 'cloud_coverage': 59.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T15:00:00Z', - 'dew_point': 22.7, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.55, - 'temperature': 24.5, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 18.79, - 'wind_speed': 8.1, - }), - dict({ - 'apparent_temperature': 26.7, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T16:00:00Z', - 'dew_point': 22.6, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.1, - 'temperature': 24.0, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 19.81, - 'wind_speed': 8.15, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T17:00:00Z', - 'dew_point': 22.6, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.68, - 'temperature': 23.7, - 'uv_index': 0, - 'wind_bearing': 161, - 'wind_gust_speed': 20.96, - 'wind_speed': 8.3, - }), - dict({ - 'apparent_temperature': 26.0, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T18:00:00Z', - 'dew_point': 22.4, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.39, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 21.41, - 'wind_speed': 8.24, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T19:00:00Z', - 'dew_point': 22.5, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.29, - 'temperature': 23.8, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 20.42, - 'wind_speed': 7.62, - }), - dict({ - 'apparent_temperature': 26.8, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T20:00:00Z', - 'dew_point': 22.6, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.31, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 158, - 'wind_gust_speed': 18.61, - 'wind_speed': 6.66, - }), - dict({ - 'apparent_temperature': 27.7, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T21:00:00Z', - 'dew_point': 22.6, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.37, - 'temperature': 24.9, - 'uv_index': 0, - 'wind_bearing': 158, - 'wind_gust_speed': 17.14, - 'wind_speed': 5.86, - }), - dict({ - 'apparent_temperature': 28.9, - 'cloud_coverage': 48.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T22:00:00Z', - 'dew_point': 22.6, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.46, - 'temperature': 26.0, - 'uv_index': 1, - 'wind_bearing': 161, - 'wind_gust_speed': 16.78, - 'wind_speed': 5.5, - }), - dict({ - 'apparent_temperature': 30.6, - 'cloud_coverage': 39.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T23:00:00Z', - 'dew_point': 22.9, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.51, - 'temperature': 27.5, - 'uv_index': 2, - 'wind_bearing': 165, - 'wind_gust_speed': 17.21, - 'wind_speed': 5.56, - }), - dict({ - 'apparent_temperature': 31.7, - 'cloud_coverage': 33.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T00:00:00Z', - 'dew_point': 22.8, - 'humidity': 71, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.39, - 'temperature': 28.5, - 'uv_index': 4, - 'wind_bearing': 174, - 'wind_gust_speed': 17.96, - 'wind_speed': 6.04, - }), - dict({ - 'apparent_temperature': 32.6, - 'cloud_coverage': 30.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T01:00:00Z', - 'dew_point': 22.7, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.98, - 'temperature': 29.4, - 'uv_index': 6, - 'wind_bearing': 192, - 'wind_gust_speed': 19.15, - 'wind_speed': 7.23, - }), - dict({ - 'apparent_temperature': 33.6, - 'cloud_coverage': 28.999999999999996, - 'condition': 'sunny', - 'datetime': '2023-09-17T02:00:00Z', - 'dew_point': 22.8, - 'humidity': 65, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.38, - 'temperature': 30.1, - 'uv_index': 7, - 'wind_bearing': 225, - 'wind_gust_speed': 20.89, - 'wind_speed': 8.9, - }), - dict({ - 'apparent_temperature': 34.1, - 'cloud_coverage': 30.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T03:00:00Z', - 'dew_point': 22.8, - 'humidity': 63, - 'precipitation': 0.3, - 'precipitation_probability': 9.0, - 'pressure': 1009.75, - 'temperature': 30.7, - 'uv_index': 8, - 'wind_bearing': 264, - 'wind_gust_speed': 22.67, - 'wind_speed': 10.27, - }), - dict({ - 'apparent_temperature': 33.9, - 'cloud_coverage': 37.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T04:00:00Z', - 'dew_point': 22.5, - 'humidity': 62, - 'precipitation': 0.4, - 'precipitation_probability': 10.0, - 'pressure': 1009.18, - 'temperature': 30.5, - 'uv_index': 7, - 'wind_bearing': 293, - 'wind_gust_speed': 23.93, - 'wind_speed': 10.82, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 45.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T05:00:00Z', - 'dew_point': 22.4, - 'humidity': 63, - 'precipitation': 0.6, - 'precipitation_probability': 12.0, - 'pressure': 1008.71, - 'temperature': 30.1, - 'uv_index': 5, - 'wind_bearing': 308, - 'wind_gust_speed': 24.39, - 'wind_speed': 10.72, - }), - dict({ - 'apparent_temperature': 32.7, - 'cloud_coverage': 50.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T06:00:00Z', - 'dew_point': 22.2, - 'humidity': 64, - 'precipitation': 0.7, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1008.46, - 'temperature': 29.6, - 'uv_index': 3, - 'wind_bearing': 312, - 'wind_gust_speed': 23.9, - 'wind_speed': 10.28, - }), - dict({ - 'apparent_temperature': 31.8, - 'cloud_coverage': 47.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T07:00:00Z', - 'dew_point': 22.1, - 'humidity': 67, - 'precipitation': 0.7, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1008.53, - 'temperature': 28.9, - 'uv_index': 1, - 'wind_bearing': 312, - 'wind_gust_speed': 22.3, - 'wind_speed': 9.59, - }), - dict({ - 'apparent_temperature': 30.6, - 'cloud_coverage': 41.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T08:00:00Z', - 'dew_point': 21.9, - 'humidity': 70, - 'precipitation': 0.6, - 'precipitation_probability': 15.0, - 'pressure': 1008.82, - 'temperature': 27.9, - 'uv_index': 0, - 'wind_bearing': 305, - 'wind_gust_speed': 19.73, - 'wind_speed': 8.58, - }), - dict({ - 'apparent_temperature': 29.6, - 'cloud_coverage': 35.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T09:00:00Z', - 'dew_point': 22.0, - 'humidity': 74, - 'precipitation': 0.5, - 'precipitation_probability': 15.0, - 'pressure': 1009.21, - 'temperature': 27.0, - 'uv_index': 0, - 'wind_bearing': 291, - 'wind_gust_speed': 16.49, - 'wind_speed': 7.34, - }), - dict({ - 'apparent_temperature': 28.6, - 'cloud_coverage': 33.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T10:00:00Z', - 'dew_point': 21.9, - 'humidity': 78, - 'precipitation': 0.4, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1009.65, - 'temperature': 26.1, - 'uv_index': 0, - 'wind_bearing': 257, - 'wind_gust_speed': 12.71, - 'wind_speed': 5.91, - }), - dict({ - 'apparent_temperature': 27.8, - 'cloud_coverage': 34.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T11:00:00Z', - 'dew_point': 21.9, - 'humidity': 82, - 'precipitation': 0.3, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1010.04, - 'temperature': 25.3, - 'uv_index': 0, - 'wind_bearing': 212, - 'wind_gust_speed': 9.16, - 'wind_speed': 4.54, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 36.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T12:00:00Z', - 'dew_point': 21.9, - 'humidity': 85, - 'precipitation': 0.3, - 'precipitation_probability': 28.000000000000004, - 'pressure': 1010.24, - 'temperature': 24.6, - 'uv_index': 0, - 'wind_bearing': 192, - 'wind_gust_speed': 7.09, - 'wind_speed': 3.62, - }), - dict({ - 'apparent_temperature': 26.5, - 'cloud_coverage': 40.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T13:00:00Z', - 'dew_point': 22.0, - 'humidity': 88, - 'precipitation': 0.3, - 'precipitation_probability': 30.0, - 'pressure': 1010.15, - 'temperature': 24.1, - 'uv_index': 0, - 'wind_bearing': 185, - 'wind_gust_speed': 7.2, - 'wind_speed': 3.27, - }), - dict({ - 'apparent_temperature': 25.9, - 'cloud_coverage': 44.0, - 'condition': 'rainy', - 'datetime': '2023-09-17T14:00:00Z', - 'dew_point': 21.8, - 'humidity': 90, - 'precipitation': 0.3, - 'precipitation_probability': 30.0, - 'pressure': 1009.87, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 182, - 'wind_gust_speed': 8.37, - 'wind_speed': 3.22, - }), - dict({ - 'apparent_temperature': 25.5, - 'cloud_coverage': 49.0, - 'condition': 'rainy', - 'datetime': '2023-09-17T15:00:00Z', - 'dew_point': 21.8, - 'humidity': 92, - 'precipitation': 0.2, - 'precipitation_probability': 31.0, - 'pressure': 1009.56, - 'temperature': 23.2, - 'uv_index': 0, - 'wind_bearing': 180, - 'wind_gust_speed': 9.21, - 'wind_speed': 3.3, - }), - dict({ - 'apparent_temperature': 25.1, - 'cloud_coverage': 53.0, - 'condition': 'rainy', - 'datetime': '2023-09-17T16:00:00Z', - 'dew_point': 21.8, - 'humidity': 94, - 'precipitation': 0.2, - 'precipitation_probability': 33.0, - 'pressure': 1009.29, - 'temperature': 22.9, - 'uv_index': 0, - 'wind_bearing': 182, - 'wind_gust_speed': 9.0, - 'wind_speed': 3.46, - }), - dict({ - 'apparent_temperature': 24.8, - 'cloud_coverage': 56.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T17:00:00Z', - 'dew_point': 21.7, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 35.0, - 'pressure': 1009.09, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 186, - 'wind_gust_speed': 8.37, - 'wind_speed': 3.72, - }), - dict({ - 'apparent_temperature': 24.6, - 'cloud_coverage': 59.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T18:00:00Z', - 'dew_point': 21.6, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 37.0, - 'pressure': 1009.01, - 'temperature': 22.5, - 'uv_index': 0, - 'wind_bearing': 201, - 'wind_gust_speed': 7.99, - 'wind_speed': 4.07, - }), - dict({ - 'apparent_temperature': 24.9, - 'cloud_coverage': 62.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T19:00:00Z', - 'dew_point': 21.7, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 39.0, - 'pressure': 1009.07, - 'temperature': 22.7, - 'uv_index': 0, - 'wind_bearing': 258, - 'wind_gust_speed': 8.18, - 'wind_speed': 4.55, - }), - dict({ - 'apparent_temperature': 25.2, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-17T20:00:00Z', - 'dew_point': 21.7, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 39.0, - 'pressure': 1009.23, - 'temperature': 23.0, - 'uv_index': 0, - 'wind_bearing': 305, - 'wind_gust_speed': 8.77, - 'wind_speed': 5.17, - }), - dict({ - 'apparent_temperature': 25.8, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-17T21:00:00Z', - 'dew_point': 21.8, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 38.0, - 'pressure': 1009.47, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 318, - 'wind_gust_speed': 9.69, - 'wind_speed': 5.77, - }), - dict({ - 'apparent_temperature': 26.5, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-17T22:00:00Z', - 'dew_point': 21.8, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 30.0, - 'pressure': 1009.77, - 'temperature': 24.2, - 'uv_index': 1, - 'wind_bearing': 324, - 'wind_gust_speed': 10.88, - 'wind_speed': 6.26, - }), - dict({ - 'apparent_temperature': 27.6, - 'cloud_coverage': 80.0, - 'condition': 'rainy', - 'datetime': '2023-09-17T23:00:00Z', - 'dew_point': 21.9, - 'humidity': 83, - 'precipitation': 0.2, - 'precipitation_probability': 15.0, - 'pressure': 1010.09, - 'temperature': 25.1, - 'uv_index': 2, - 'wind_bearing': 329, - 'wind_gust_speed': 12.21, - 'wind_speed': 6.68, - }), - dict({ - 'apparent_temperature': 28.2, - 'cloud_coverage': 87.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T00:00:00Z', - 'dew_point': 21.9, - 'humidity': 80, - 'precipitation': 0.2, - 'precipitation_probability': 15.0, - 'pressure': 1010.33, - 'temperature': 25.7, - 'uv_index': 3, - 'wind_bearing': 332, - 'wind_gust_speed': 13.52, - 'wind_speed': 7.12, - }), - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 67.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T01:00:00Z', - 'dew_point': 21.7, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1007.43, - 'temperature': 27.2, - 'uv_index': 5, - 'wind_bearing': 330, - 'wind_gust_speed': 11.36, - 'wind_speed': 11.36, - }), - dict({ - 'apparent_temperature': 30.1, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T02:00:00Z', - 'dew_point': 21.6, - 'humidity': 70, - 'precipitation': 0.3, - 'precipitation_probability': 9.0, - 'pressure': 1007.05, - 'temperature': 27.5, - 'uv_index': 6, - 'wind_bearing': 332, - 'wind_gust_speed': 12.06, - 'wind_speed': 12.06, - }), - dict({ - 'apparent_temperature': 30.3, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T03:00:00Z', - 'dew_point': 21.6, - 'humidity': 69, - 'precipitation': 0.5, - 'precipitation_probability': 10.0, - 'pressure': 1006.67, - 'temperature': 27.8, - 'uv_index': 6, - 'wind_bearing': 333, - 'wind_gust_speed': 12.81, - 'wind_speed': 12.81, - }), - dict({ - 'apparent_temperature': 30.6, - 'cloud_coverage': 67.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T04:00:00Z', - 'dew_point': 21.5, - 'humidity': 68, - 'precipitation': 0.4, - 'precipitation_probability': 10.0, - 'pressure': 1006.28, - 'temperature': 28.0, - 'uv_index': 5, - 'wind_bearing': 335, - 'wind_gust_speed': 13.68, - 'wind_speed': 13.68, - }), - dict({ - 'apparent_temperature': 30.7, - 'cloud_coverage': 60.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T05:00:00Z', - 'dew_point': 21.4, - 'humidity': 67, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1005.89, - 'temperature': 28.1, - 'uv_index': 4, - 'wind_bearing': 336, - 'wind_gust_speed': 14.61, - 'wind_speed': 14.61, - }), - dict({ - 'apparent_temperature': 30.3, - 'cloud_coverage': 56.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T06:00:00Z', - 'dew_point': 21.2, - 'humidity': 67, - 'precipitation': 0.0, - 'precipitation_probability': 27.0, - 'pressure': 1005.67, - 'temperature': 27.9, - 'uv_index': 3, - 'wind_bearing': 338, - 'wind_gust_speed': 15.25, - 'wind_speed': 15.25, - }), - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 60.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T07:00:00Z', - 'dew_point': 21.3, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 28.000000000000004, - 'pressure': 1005.74, - 'temperature': 27.4, - 'uv_index': 1, - 'wind_bearing': 339, - 'wind_gust_speed': 15.45, - 'wind_speed': 15.45, - }), - dict({ - 'apparent_temperature': 29.1, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T08:00:00Z', - 'dew_point': 21.4, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 26.0, - 'pressure': 1005.98, - 'temperature': 26.7, - 'uv_index': 0, - 'wind_bearing': 341, - 'wind_gust_speed': 15.38, - 'wind_speed': 15.38, - }), - dict({ - 'apparent_temperature': 28.6, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T09:00:00Z', - 'dew_point': 21.6, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1006.22, - 'temperature': 26.1, - 'uv_index': 0, - 'wind_bearing': 341, - 'wind_gust_speed': 15.27, - 'wind_speed': 15.27, - }), - dict({ - 'apparent_temperature': 27.9, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T10:00:00Z', - 'dew_point': 21.6, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1006.44, - 'temperature': 25.6, - 'uv_index': 0, - 'wind_bearing': 339, - 'wind_gust_speed': 15.09, - 'wind_speed': 15.09, - }), - dict({ - 'apparent_temperature': 27.6, - 'cloud_coverage': 61.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T11:00:00Z', - 'dew_point': 21.7, - 'humidity': 81, - 'precipitation': 0.0, - 'precipitation_probability': 26.0, - 'pressure': 1006.66, - 'temperature': 25.2, - 'uv_index': 0, - 'wind_bearing': 336, - 'wind_gust_speed': 14.88, - 'wind_speed': 14.88, - }), - dict({ - 'apparent_temperature': 27.2, - 'cloud_coverage': 61.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T12:00:00Z', - 'dew_point': 21.8, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 26.0, - 'pressure': 1006.79, - 'temperature': 24.8, - 'uv_index': 0, - 'wind_bearing': 333, - 'wind_gust_speed': 14.91, - 'wind_speed': 14.91, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 38.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T13:00:00Z', - 'dew_point': 21.2, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.36, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 83, - 'wind_gust_speed': 4.58, - 'wind_speed': 3.16, - }), - dict({ - 'apparent_temperature': 25.1, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T14:00:00Z', - 'dew_point': 21.2, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.96, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 144, - 'wind_gust_speed': 4.74, - 'wind_speed': 4.52, - }), - dict({ - 'apparent_temperature': 24.5, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T15:00:00Z', - 'dew_point': 20.9, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.6, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 152, - 'wind_gust_speed': 5.63, - 'wind_speed': 5.63, - }), - dict({ - 'apparent_temperature': 24.0, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T16:00:00Z', - 'dew_point': 20.7, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.37, - 'temperature': 22.3, - 'uv_index': 0, - 'wind_bearing': 156, - 'wind_gust_speed': 6.02, - 'wind_speed': 6.02, - }), - dict({ - 'apparent_temperature': 23.7, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T17:00:00Z', - 'dew_point': 20.4, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.2, - 'temperature': 22.0, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 6.15, - 'wind_speed': 6.15, - }), - dict({ - 'apparent_temperature': 23.4, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T18:00:00Z', - 'dew_point': 20.2, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.08, - 'temperature': 21.9, - 'uv_index': 0, - 'wind_bearing': 167, - 'wind_gust_speed': 6.48, - 'wind_speed': 6.48, - }), - dict({ - 'apparent_temperature': 23.2, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T19:00:00Z', - 'dew_point': 19.8, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.04, - 'temperature': 21.8, - 'uv_index': 0, - 'wind_bearing': 165, - 'wind_gust_speed': 7.51, - 'wind_speed': 7.51, - }), - dict({ - 'apparent_temperature': 23.4, - 'cloud_coverage': 99.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T20:00:00Z', - 'dew_point': 19.6, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.05, - 'temperature': 22.0, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 8.73, - 'wind_speed': 8.73, - }), - dict({ - 'apparent_temperature': 23.9, - 'cloud_coverage': 98.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T21:00:00Z', - 'dew_point': 19.5, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.06, - 'temperature': 22.5, - 'uv_index': 0, - 'wind_bearing': 164, - 'wind_gust_speed': 9.21, - 'wind_speed': 9.11, - }), - dict({ - 'apparent_temperature': 25.3, - 'cloud_coverage': 96.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T22:00:00Z', - 'dew_point': 19.7, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.09, - 'temperature': 23.8, - 'uv_index': 1, - 'wind_bearing': 171, - 'wind_gust_speed': 9.03, - 'wind_speed': 7.91, - }), - ]), - }), - }) -# --- -# name: test_hourly_forecast[get_forecast] - dict({ - 'forecast': list([ - dict({ - 'apparent_temperature': 24.6, - 'cloud_coverage': 79.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T14:00:00Z', - 'dew_point': 21.5, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.24, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 264, - 'wind_gust_speed': 13.44, - 'wind_speed': 6.62, - }), - dict({ - 'apparent_temperature': 24.4, - 'cloud_coverage': 80.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T15:00:00Z', - 'dew_point': 21.4, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.24, - 'temperature': 22.4, - 'uv_index': 0, - 'wind_bearing': 261, - 'wind_gust_speed': 11.91, - 'wind_speed': 6.64, - }), - dict({ - 'apparent_temperature': 23.8, - 'cloud_coverage': 89.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T16:00:00Z', - 'dew_point': 21.1, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.12, - 'temperature': 22.0, - 'uv_index': 0, - 'wind_bearing': 252, - 'wind_gust_speed': 11.15, - 'wind_speed': 6.14, - }), - dict({ - 'apparent_temperature': 23.5, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T17:00:00Z', - 'dew_point': 20.9, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.03, - 'temperature': 21.7, - 'uv_index': 0, - 'wind_bearing': 248, - 'wind_gust_speed': 11.57, - 'wind_speed': 5.95, - }), - dict({ - 'apparent_temperature': 23.3, - 'cloud_coverage': 85.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T18:00:00Z', - 'dew_point': 20.8, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.05, - 'temperature': 21.6, - 'uv_index': 0, - 'wind_bearing': 237, - 'wind_gust_speed': 12.42, - 'wind_speed': 5.86, - }), - dict({ - 'apparent_temperature': 23.0, - 'cloud_coverage': 75.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T19:00:00Z', - 'dew_point': 20.6, - 'humidity': 96, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.03, - 'temperature': 21.3, - 'uv_index': 0, - 'wind_bearing': 224, - 'wind_gust_speed': 11.3, - 'wind_speed': 5.34, - }), - dict({ - 'apparent_temperature': 22.8, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T20:00:00Z', - 'dew_point': 20.4, - 'humidity': 96, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.31, - 'temperature': 21.2, - 'uv_index': 0, - 'wind_bearing': 221, - 'wind_gust_speed': 10.57, - 'wind_speed': 5.13, - }), - dict({ - 'apparent_temperature': 23.1, - 'cloud_coverage': 56.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-08T21:00:00Z', - 'dew_point': 20.5, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.55, - 'temperature': 21.4, - 'uv_index': 0, - 'wind_bearing': 237, - 'wind_gust_speed': 10.63, - 'wind_speed': 5.7, - }), - dict({ - 'apparent_temperature': 24.9, - 'cloud_coverage': 61.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-08T22:00:00Z', - 'dew_point': 21.3, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.79, - 'temperature': 22.8, - 'uv_index': 1, - 'wind_bearing': 258, - 'wind_gust_speed': 10.47, - 'wind_speed': 5.22, - }), - dict({ - 'apparent_temperature': 26.1, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-08T23:00:00Z', - 'dew_point': 21.3, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.95, - 'temperature': 24.0, - 'uv_index': 2, - 'wind_bearing': 282, - 'wind_gust_speed': 12.74, - 'wind_speed': 5.71, - }), - dict({ - 'apparent_temperature': 27.4, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T00:00:00Z', - 'dew_point': 21.5, - 'humidity': 80, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.35, - 'temperature': 25.1, - 'uv_index': 3, - 'wind_bearing': 294, - 'wind_gust_speed': 13.87, - 'wind_speed': 6.53, - }), - dict({ - 'apparent_temperature': 29.0, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T01:00:00Z', - 'dew_point': 21.8, - 'humidity': 75, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.48, - 'temperature': 26.5, - 'uv_index': 5, - 'wind_bearing': 308, - 'wind_gust_speed': 16.04, - 'wind_speed': 6.54, - }), - dict({ - 'apparent_temperature': 30.3, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T02:00:00Z', - 'dew_point': 22.0, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.23, - 'temperature': 27.6, - 'uv_index': 6, - 'wind_bearing': 314, - 'wind_gust_speed': 18.1, - 'wind_speed': 7.32, - }), - dict({ - 'apparent_temperature': 31.1, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T03:00:00Z', - 'dew_point': 22.1, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.86, - 'temperature': 28.3, - 'uv_index': 6, - 'wind_bearing': 317, - 'wind_gust_speed': 20.77, - 'wind_speed': 9.1, - }), - dict({ - 'apparent_temperature': 31.5, - 'cloud_coverage': 69.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T04:00:00Z', - 'dew_point': 22.1, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.65, - 'temperature': 28.6, - 'uv_index': 6, - 'wind_bearing': 311, - 'wind_gust_speed': 21.27, - 'wind_speed': 10.21, - }), - dict({ - 'apparent_temperature': 31.3, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T05:00:00Z', - 'dew_point': 22.1, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.48, - 'temperature': 28.4, - 'uv_index': 5, - 'wind_bearing': 317, - 'wind_gust_speed': 19.62, - 'wind_speed': 10.53, - }), - dict({ - 'apparent_temperature': 30.8, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T06:00:00Z', - 'dew_point': 22.2, - 'humidity': 71, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.54, - 'temperature': 27.9, - 'uv_index': 3, - 'wind_bearing': 335, - 'wind_gust_speed': 18.98, - 'wind_speed': 8.63, - }), - dict({ - 'apparent_temperature': 29.9, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T07:00:00Z', - 'dew_point': 22.2, - 'humidity': 74, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.76, - 'temperature': 27.1, - 'uv_index': 2, - 'wind_bearing': 338, - 'wind_gust_speed': 17.04, - 'wind_speed': 7.75, - }), - dict({ - 'apparent_temperature': 29.1, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T08:00:00Z', - 'dew_point': 22.1, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.05, - 'temperature': 26.4, - 'uv_index': 0, - 'wind_bearing': 342, - 'wind_gust_speed': 14.75, - 'wind_speed': 6.26, - }), - dict({ - 'apparent_temperature': 27.9, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T09:00:00Z', - 'dew_point': 22.0, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.38, - 'temperature': 25.4, - 'uv_index': 0, - 'wind_bearing': 344, - 'wind_gust_speed': 10.43, - 'wind_speed': 5.2, - }), - dict({ - 'apparent_temperature': 26.9, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T10:00:00Z', - 'dew_point': 21.9, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.73, - 'temperature': 24.5, - 'uv_index': 0, - 'wind_bearing': 339, - 'wind_gust_speed': 6.95, - 'wind_speed': 3.59, - }), - dict({ - 'apparent_temperature': 26.4, - 'cloud_coverage': 51.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T11:00:00Z', - 'dew_point': 21.8, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.3, - 'temperature': 24.1, - 'uv_index': 0, - 'wind_bearing': 326, - 'wind_gust_speed': 5.27, - 'wind_speed': 2.1, - }), - dict({ - 'apparent_temperature': 26.1, - 'cloud_coverage': 53.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T12:00:00Z', - 'dew_point': 21.8, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.52, - 'temperature': 23.9, - 'uv_index': 0, - 'wind_bearing': 257, - 'wind_gust_speed': 5.48, - 'wind_speed': 0.93, - }), - dict({ - 'apparent_temperature': 25.8, - 'cloud_coverage': 56.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T13:00:00Z', - 'dew_point': 21.8, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.53, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 188, - 'wind_gust_speed': 4.44, - 'wind_speed': 1.79, - }), - dict({ - 'apparent_temperature': 25.3, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T14:00:00Z', - 'dew_point': 21.7, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.46, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 183, - 'wind_gust_speed': 4.49, - 'wind_speed': 2.19, - }), - dict({ - 'apparent_temperature': 24.6, - 'cloud_coverage': 45.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T15:00:00Z', - 'dew_point': 21.4, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.21, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 179, - 'wind_gust_speed': 5.32, - 'wind_speed': 2.65, - }), - dict({ - 'apparent_temperature': 24.0, - 'cloud_coverage': 42.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T16:00:00Z', - 'dew_point': 21.1, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.09, - 'temperature': 22.1, - 'uv_index': 0, - 'wind_bearing': 173, - 'wind_gust_speed': 5.81, - 'wind_speed': 3.2, - }), - dict({ - 'apparent_temperature': 23.7, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T17:00:00Z', - 'dew_point': 20.9, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.88, - 'temperature': 21.9, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 5.53, - 'wind_speed': 3.16, - }), - dict({ - 'apparent_temperature': 23.3, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T18:00:00Z', - 'dew_point': 20.7, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.94, - 'temperature': 21.6, - 'uv_index': 0, - 'wind_bearing': 153, - 'wind_gust_speed': 6.09, - 'wind_speed': 3.36, - }), - dict({ - 'apparent_temperature': 23.1, - 'cloud_coverage': 51.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T19:00:00Z', - 'dew_point': 20.5, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.96, - 'temperature': 21.4, - 'uv_index': 0, - 'wind_bearing': 150, - 'wind_gust_speed': 6.83, - 'wind_speed': 3.71, - }), - dict({ - 'apparent_temperature': 22.5, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T20:00:00Z', - 'dew_point': 20.0, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.29, - 'temperature': 21.0, - 'uv_index': 0, - 'wind_bearing': 156, - 'wind_gust_speed': 7.98, - 'wind_speed': 4.27, - }), - dict({ - 'apparent_temperature': 22.8, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T21:00:00Z', - 'dew_point': 20.2, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.61, - 'temperature': 21.2, - 'uv_index': 0, - 'wind_bearing': 156, - 'wind_gust_speed': 8.4, - 'wind_speed': 4.69, - }), - dict({ - 'apparent_temperature': 25.1, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-09T22:00:00Z', - 'dew_point': 21.3, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.87, - 'temperature': 23.1, - 'uv_index': 1, - 'wind_bearing': 150, - 'wind_gust_speed': 7.66, - 'wind_speed': 4.33, - }), - dict({ - 'apparent_temperature': 28.3, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-09T23:00:00Z', - 'dew_point': 22.3, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.93, - 'temperature': 25.6, - 'uv_index': 2, - 'wind_bearing': 123, - 'wind_gust_speed': 9.63, - 'wind_speed': 3.91, - }), - dict({ - 'apparent_temperature': 30.4, - 'cloud_coverage': 63.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T00:00:00Z', - 'dew_point': 22.6, - 'humidity': 75, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.93, - 'temperature': 27.4, - 'uv_index': 4, - 'wind_bearing': 105, - 'wind_gust_speed': 12.59, - 'wind_speed': 3.96, - }), - dict({ - 'apparent_temperature': 32.2, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T01:00:00Z', - 'dew_point': 22.9, - 'humidity': 70, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.79, - 'temperature': 28.9, - 'uv_index': 5, - 'wind_bearing': 99, - 'wind_gust_speed': 14.17, - 'wind_speed': 4.06, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 62.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-10T02:00:00Z', - 'dew_point': 22.9, - 'humidity': 66, - 'precipitation': 0.3, - 'precipitation_probability': 7.000000000000001, - 'pressure': 1011.29, - 'temperature': 29.9, - 'uv_index': 6, - 'wind_bearing': 93, - 'wind_gust_speed': 17.75, - 'wind_speed': 4.87, - }), - dict({ - 'apparent_temperature': 34.3, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T03:00:00Z', - 'dew_point': 23.1, - 'humidity': 64, - 'precipitation': 0.3, - 'precipitation_probability': 11.0, - 'pressure': 1010.78, - 'temperature': 30.6, - 'uv_index': 6, - 'wind_bearing': 78, - 'wind_gust_speed': 17.43, - 'wind_speed': 4.54, - }), - dict({ - 'apparent_temperature': 34.0, - 'cloud_coverage': 74.0, - 'condition': 'rainy', - 'datetime': '2023-09-10T04:00:00Z', - 'dew_point': 23.2, - 'humidity': 66, - 'precipitation': 0.4, - 'precipitation_probability': 15.0, - 'pressure': 1010.37, - 'temperature': 30.3, - 'uv_index': 5, - 'wind_bearing': 60, - 'wind_gust_speed': 15.24, - 'wind_speed': 4.9, - }), - dict({ - 'apparent_temperature': 33.7, - 'cloud_coverage': 79.0, - 'condition': 'rainy', - 'datetime': '2023-09-10T05:00:00Z', - 'dew_point': 23.3, - 'humidity': 67, - 'precipitation': 0.7, - 'precipitation_probability': 17.0, - 'pressure': 1010.09, - 'temperature': 30.0, - 'uv_index': 4, - 'wind_bearing': 80, - 'wind_gust_speed': 13.53, - 'wind_speed': 5.98, - }), - dict({ - 'apparent_temperature': 33.2, - 'cloud_coverage': 80.0, - 'condition': 'rainy', - 'datetime': '2023-09-10T06:00:00Z', - 'dew_point': 23.4, - 'humidity': 70, - 'precipitation': 1.0, - 'precipitation_probability': 17.0, - 'pressure': 1010.0, - 'temperature': 29.5, - 'uv_index': 3, - 'wind_bearing': 83, - 'wind_gust_speed': 12.55, - 'wind_speed': 6.84, - }), - dict({ - 'apparent_temperature': 32.3, - 'cloud_coverage': 88.0, - 'condition': 'rainy', - 'datetime': '2023-09-10T07:00:00Z', - 'dew_point': 23.4, - 'humidity': 73, - 'precipitation': 0.4, - 'precipitation_probability': 16.0, - 'pressure': 1010.27, - 'temperature': 28.7, - 'uv_index': 2, - 'wind_bearing': 90, - 'wind_gust_speed': 10.16, - 'wind_speed': 6.07, - }), - dict({ - 'apparent_temperature': 30.9, - 'cloud_coverage': 92.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T08:00:00Z', - 'dew_point': 23.2, - 'humidity': 77, - 'precipitation': 0.5, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1010.71, - 'temperature': 27.6, - 'uv_index': 0, - 'wind_bearing': 101, - 'wind_gust_speed': 8.18, - 'wind_speed': 4.82, - }), - dict({ - 'apparent_temperature': 29.7, - 'cloud_coverage': 93.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T09:00:00Z', - 'dew_point': 23.2, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.9, - 'temperature': 26.5, - 'uv_index': 0, - 'wind_bearing': 128, - 'wind_gust_speed': 8.89, - 'wind_speed': 4.95, - }), - dict({ - 'apparent_temperature': 28.6, - 'cloud_coverage': 88.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T10:00:00Z', - 'dew_point': 23.0, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.12, - 'temperature': 25.6, - 'uv_index': 0, - 'wind_bearing': 134, - 'wind_gust_speed': 10.03, - 'wind_speed': 4.52, - }), - dict({ - 'apparent_temperature': 27.9, - 'cloud_coverage': 87.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T11:00:00Z', - 'dew_point': 22.8, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.43, - 'temperature': 25.1, - 'uv_index': 0, - 'wind_bearing': 137, - 'wind_gust_speed': 12.4, - 'wind_speed': 5.41, - }), - dict({ - 'apparent_temperature': 27.4, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T12:00:00Z', - 'dew_point': 22.5, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.58, - 'temperature': 24.8, - 'uv_index': 0, - 'wind_bearing': 143, - 'wind_gust_speed': 16.36, - 'wind_speed': 6.31, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T13:00:00Z', - 'dew_point': 22.4, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.55, - 'temperature': 24.5, - 'uv_index': 0, - 'wind_bearing': 144, - 'wind_gust_speed': 19.66, - 'wind_speed': 7.23, - }), - dict({ - 'apparent_temperature': 26.8, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T14:00:00Z', - 'dew_point': 22.2, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.4, - 'temperature': 24.3, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 21.15, - 'wind_speed': 7.46, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T15:00:00Z', - 'dew_point': 22.0, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.23, - 'temperature': 23.9, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 22.26, - 'wind_speed': 7.84, - }), - dict({ - 'apparent_temperature': 26.1, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T16:00:00Z', - 'dew_point': 21.8, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.01, - 'temperature': 23.8, - 'uv_index': 0, - 'wind_bearing': 144, - 'wind_gust_speed': 23.53, - 'wind_speed': 8.63, - }), - dict({ - 'apparent_temperature': 25.6, - 'cloud_coverage': 61.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-10T17:00:00Z', - 'dew_point': 21.6, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.78, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 144, - 'wind_gust_speed': 22.83, - 'wind_speed': 8.61, - }), - dict({ - 'apparent_temperature': 25.4, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T18:00:00Z', - 'dew_point': 21.5, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.69, - 'temperature': 23.3, - 'uv_index': 0, - 'wind_bearing': 143, - 'wind_gust_speed': 23.7, - 'wind_speed': 8.7, - }), - dict({ - 'apparent_temperature': 25.2, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T19:00:00Z', - 'dew_point': 21.4, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.77, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 140, - 'wind_gust_speed': 24.24, - 'wind_speed': 8.74, - }), - dict({ - 'apparent_temperature': 25.5, - 'cloud_coverage': 89.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T20:00:00Z', - 'dew_point': 21.6, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.89, - 'temperature': 23.3, - 'uv_index': 0, - 'wind_bearing': 138, - 'wind_gust_speed': 23.99, - 'wind_speed': 8.81, - }), - dict({ - 'apparent_temperature': 25.9, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T21:00:00Z', - 'dew_point': 21.6, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.1, - 'temperature': 23.7, - 'uv_index': 0, - 'wind_bearing': 138, - 'wind_gust_speed': 25.55, - 'wind_speed': 9.05, - }), - dict({ - 'apparent_temperature': 27.0, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T22:00:00Z', - 'dew_point': 21.8, - 'humidity': 84, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.29, - 'temperature': 24.6, - 'uv_index': 1, - 'wind_bearing': 140, - 'wind_gust_speed': 29.08, - 'wind_speed': 10.37, - }), - dict({ - 'apparent_temperature': 28.4, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-10T23:00:00Z', - 'dew_point': 21.9, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.36, - 'temperature': 25.9, - 'uv_index': 2, - 'wind_bearing': 140, - 'wind_gust_speed': 34.13, - 'wind_speed': 12.56, - }), - dict({ - 'apparent_temperature': 30.1, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T00:00:00Z', - 'dew_point': 22.3, - 'humidity': 74, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.39, - 'temperature': 27.2, - 'uv_index': 3, - 'wind_bearing': 140, - 'wind_gust_speed': 38.2, - 'wind_speed': 15.65, - }), - dict({ - 'apparent_temperature': 31.4, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-11T01:00:00Z', - 'dew_point': 22.3, - 'humidity': 70, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.31, - 'temperature': 28.4, - 'uv_index': 5, - 'wind_bearing': 141, - 'wind_gust_speed': 37.55, - 'wind_speed': 15.78, - }), - dict({ - 'apparent_temperature': 32.7, - 'cloud_coverage': 63.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T02:00:00Z', - 'dew_point': 22.4, - 'humidity': 66, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.98, - 'temperature': 29.6, - 'uv_index': 6, - 'wind_bearing': 143, - 'wind_gust_speed': 35.86, - 'wind_speed': 15.41, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T03:00:00Z', - 'dew_point': 22.5, - 'humidity': 63, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.61, - 'temperature': 30.3, - 'uv_index': 6, - 'wind_bearing': 141, - 'wind_gust_speed': 35.88, - 'wind_speed': 15.51, - }), - dict({ - 'apparent_temperature': 33.8, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T04:00:00Z', - 'dew_point': 22.6, - 'humidity': 63, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.36, - 'temperature': 30.4, - 'uv_index': 5, - 'wind_bearing': 140, - 'wind_gust_speed': 35.99, - 'wind_speed': 15.75, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T05:00:00Z', - 'dew_point': 22.6, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.11, - 'temperature': 30.1, - 'uv_index': 4, - 'wind_bearing': 137, - 'wind_gust_speed': 33.61, - 'wind_speed': 15.36, - }), - dict({ - 'apparent_temperature': 33.2, - 'cloud_coverage': 77.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T06:00:00Z', - 'dew_point': 22.5, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1009.98, - 'temperature': 30.0, - 'uv_index': 3, - 'wind_bearing': 138, - 'wind_gust_speed': 32.61, - 'wind_speed': 14.98, - }), - dict({ - 'apparent_temperature': 32.3, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T07:00:00Z', - 'dew_point': 22.2, - 'humidity': 66, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.13, - 'temperature': 29.2, - 'uv_index': 2, - 'wind_bearing': 138, - 'wind_gust_speed': 28.1, - 'wind_speed': 13.88, - }), - dict({ - 'apparent_temperature': 31.2, - 'cloud_coverage': 56.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-11T08:00:00Z', - 'dew_point': 22.1, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.48, - 'temperature': 28.3, - 'uv_index': 0, - 'wind_bearing': 137, - 'wind_gust_speed': 24.22, - 'wind_speed': 13.02, - }), - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 55.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-11T09:00:00Z', - 'dew_point': 21.9, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.81, - 'temperature': 27.1, - 'uv_index': 0, - 'wind_bearing': 138, - 'wind_gust_speed': 22.5, - 'wind_speed': 11.94, - }), - dict({ - 'apparent_temperature': 28.8, - 'cloud_coverage': 63.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T10:00:00Z', - 'dew_point': 21.7, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.29, - 'temperature': 26.3, - 'uv_index': 0, - 'wind_bearing': 137, - 'wind_gust_speed': 21.47, - 'wind_speed': 11.25, - }), - dict({ - 'apparent_temperature': 28.1, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T11:00:00Z', - 'dew_point': 21.8, - 'humidity': 80, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.77, - 'temperature': 25.6, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 22.71, - 'wind_speed': 12.39, - }), - dict({ - 'apparent_temperature': 27.6, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T12:00:00Z', - 'dew_point': 21.8, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.97, - 'temperature': 25.2, - 'uv_index': 0, - 'wind_bearing': 143, - 'wind_gust_speed': 23.67, - 'wind_speed': 12.83, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 89.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T13:00:00Z', - 'dew_point': 21.7, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.97, - 'temperature': 24.7, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 23.34, - 'wind_speed': 12.62, - }), - dict({ - 'apparent_temperature': 26.7, - 'cloud_coverage': 88.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T14:00:00Z', - 'dew_point': 21.7, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.83, - 'temperature': 24.4, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 22.9, - 'wind_speed': 12.07, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 90.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T15:00:00Z', - 'dew_point': 21.6, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.74, - 'temperature': 24.1, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 22.01, - 'wind_speed': 11.19, - }), - dict({ - 'apparent_temperature': 25.9, - 'cloud_coverage': 88.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T16:00:00Z', - 'dew_point': 21.6, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.56, - 'temperature': 23.7, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 21.29, - 'wind_speed': 10.97, - }), - dict({ - 'apparent_temperature': 25.8, - 'cloud_coverage': 85.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T17:00:00Z', - 'dew_point': 21.5, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.35, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 150, - 'wind_gust_speed': 20.52, - 'wind_speed': 10.5, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T18:00:00Z', - 'dew_point': 21.4, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.3, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 20.04, - 'wind_speed': 10.51, - }), - dict({ - 'apparent_temperature': 25.4, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T19:00:00Z', - 'dew_point': 21.3, - 'humidity': 88, - 'precipitation': 0.3, - 'precipitation_probability': 12.0, - 'pressure': 1011.37, - 'temperature': 23.4, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 18.07, - 'wind_speed': 10.13, - }), - dict({ - 'apparent_temperature': 25.2, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T20:00:00Z', - 'dew_point': 21.2, - 'humidity': 89, - 'precipitation': 0.2, - 'precipitation_probability': 13.0, - 'pressure': 1011.53, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 16.86, - 'wind_speed': 10.34, - }), - dict({ - 'apparent_temperature': 25.5, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T21:00:00Z', - 'dew_point': 21.4, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.71, - 'temperature': 23.4, - 'uv_index': 0, - 'wind_bearing': 138, - 'wind_gust_speed': 16.66, - 'wind_speed': 10.68, - }), - dict({ - 'apparent_temperature': 26.8, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T22:00:00Z', - 'dew_point': 21.9, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.94, - 'temperature': 24.4, - 'uv_index': 1, - 'wind_bearing': 137, - 'wind_gust_speed': 17.21, - 'wind_speed': 10.61, - }), - dict({ - 'apparent_temperature': 28.2, - 'cloud_coverage': 78.0, - 'condition': 'cloudy', - 'datetime': '2023-09-11T23:00:00Z', - 'dew_point': 22.3, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.05, - 'temperature': 25.6, - 'uv_index': 2, - 'wind_bearing': 138, - 'wind_gust_speed': 19.23, - 'wind_speed': 11.13, - }), - dict({ - 'apparent_temperature': 29.5, - 'cloud_coverage': 79.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T00:00:00Z', - 'dew_point': 22.6, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.07, - 'temperature': 26.6, - 'uv_index': 3, - 'wind_bearing': 140, - 'wind_gust_speed': 20.61, - 'wind_speed': 11.13, - }), - dict({ - 'apparent_temperature': 31.2, - 'cloud_coverage': 82.0, - 'condition': 'rainy', - 'datetime': '2023-09-12T01:00:00Z', - 'dew_point': 23.1, - 'humidity': 75, - 'precipitation': 0.2, - 'precipitation_probability': 16.0, - 'pressure': 1011.89, - 'temperature': 27.9, - 'uv_index': 4, - 'wind_bearing': 141, - 'wind_gust_speed': 23.35, - 'wind_speed': 11.98, - }), - dict({ - 'apparent_temperature': 32.6, - 'cloud_coverage': 85.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T02:00:00Z', - 'dew_point': 23.5, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.53, - 'temperature': 29.0, - 'uv_index': 5, - 'wind_bearing': 143, - 'wind_gust_speed': 26.45, - 'wind_speed': 13.01, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T03:00:00Z', - 'dew_point': 23.5, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.15, - 'temperature': 29.8, - 'uv_index': 5, - 'wind_bearing': 141, - 'wind_gust_speed': 28.95, - 'wind_speed': 13.9, - }), - dict({ - 'apparent_temperature': 34.0, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T04:00:00Z', - 'dew_point': 23.4, - 'humidity': 67, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.79, - 'temperature': 30.2, - 'uv_index': 5, - 'wind_bearing': 141, - 'wind_gust_speed': 27.9, - 'wind_speed': 13.95, - }), - dict({ - 'apparent_temperature': 34.0, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T05:00:00Z', - 'dew_point': 23.1, - 'humidity': 65, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.43, - 'temperature': 30.4, - 'uv_index': 4, - 'wind_bearing': 140, - 'wind_gust_speed': 26.53, - 'wind_speed': 13.78, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 56.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T06:00:00Z', - 'dew_point': 22.6, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.21, - 'temperature': 30.1, - 'uv_index': 3, - 'wind_bearing': 138, - 'wind_gust_speed': 24.56, - 'wind_speed': 13.74, - }), - dict({ - 'apparent_temperature': 32.0, - 'cloud_coverage': 53.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T07:00:00Z', - 'dew_point': 22.1, - 'humidity': 66, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.26, - 'temperature': 29.1, - 'uv_index': 2, - 'wind_bearing': 138, - 'wind_gust_speed': 22.78, - 'wind_speed': 13.21, - }), - dict({ - 'apparent_temperature': 30.9, - 'cloud_coverage': 48.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T08:00:00Z', - 'dew_point': 21.9, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.51, - 'temperature': 28.1, - 'uv_index': 0, - 'wind_bearing': 140, - 'wind_gust_speed': 19.92, - 'wind_speed': 12.0, - }), - dict({ - 'apparent_temperature': 29.7, - 'cloud_coverage': 50.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T09:00:00Z', - 'dew_point': 21.7, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.8, - 'temperature': 27.2, - 'uv_index': 0, - 'wind_bearing': 141, - 'wind_gust_speed': 17.65, - 'wind_speed': 10.97, - }), - dict({ - 'apparent_temperature': 28.6, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T10:00:00Z', - 'dew_point': 21.4, - 'humidity': 75, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.23, - 'temperature': 26.2, - 'uv_index': 0, - 'wind_bearing': 143, - 'wind_gust_speed': 15.87, - 'wind_speed': 10.23, - }), - dict({ - 'apparent_temperature': 27.6, - 'cloud_coverage': 56.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T11:00:00Z', - 'dew_point': 21.3, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1011.79, - 'temperature': 25.4, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 13.9, - 'wind_speed': 9.39, - }), - dict({ - 'apparent_temperature': 26.8, - 'cloud_coverage': 60.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-12T12:00:00Z', - 'dew_point': 21.2, - 'humidity': 81, - 'precipitation': 0.0, - 'precipitation_probability': 47.0, - 'pressure': 1012.12, - 'temperature': 24.7, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 13.32, - 'wind_speed': 8.9, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T13:00:00Z', - 'dew_point': 21.2, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1012.18, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 13.18, - 'wind_speed': 8.59, - }), - dict({ - 'apparent_temperature': 26.0, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T14:00:00Z', - 'dew_point': 21.3, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.09, - 'temperature': 23.9, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 13.84, - 'wind_speed': 8.87, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T15:00:00Z', - 'dew_point': 21.3, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.99, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 15.08, - 'wind_speed': 8.93, - }), - dict({ - 'apparent_temperature': 25.1, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T16:00:00Z', - 'dew_point': 21.0, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.93, - 'temperature': 23.2, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 16.74, - 'wind_speed': 9.49, - }), - dict({ - 'apparent_temperature': 24.7, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T17:00:00Z', - 'dew_point': 20.8, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.75, - 'temperature': 22.9, - 'uv_index': 0, - 'wind_bearing': 146, - 'wind_gust_speed': 17.45, - 'wind_speed': 9.12, - }), - dict({ - 'apparent_temperature': 24.4, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T18:00:00Z', - 'dew_point': 20.7, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.77, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 17.04, - 'wind_speed': 8.68, - }), - dict({ - 'apparent_temperature': 24.1, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T19:00:00Z', - 'dew_point': 20.6, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.93, - 'temperature': 22.4, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 16.8, - 'wind_speed': 8.61, - }), - dict({ - 'apparent_temperature': 23.9, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T20:00:00Z', - 'dew_point': 20.5, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.23, - 'temperature': 22.1, - 'uv_index': 0, - 'wind_bearing': 150, - 'wind_gust_speed': 15.35, - 'wind_speed': 8.36, - }), - dict({ - 'apparent_temperature': 24.4, - 'cloud_coverage': 75.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T21:00:00Z', - 'dew_point': 20.6, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.49, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 155, - 'wind_gust_speed': 14.09, - 'wind_speed': 7.77, - }), - dict({ - 'apparent_temperature': 25.8, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T22:00:00Z', - 'dew_point': 21.0, - 'humidity': 84, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.72, - 'temperature': 23.8, - 'uv_index': 1, - 'wind_bearing': 152, - 'wind_gust_speed': 14.04, - 'wind_speed': 7.25, - }), - dict({ - 'apparent_temperature': 27.8, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-12T23:00:00Z', - 'dew_point': 21.4, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.85, - 'temperature': 25.5, - 'uv_index': 2, - 'wind_bearing': 149, - 'wind_gust_speed': 15.31, - 'wind_speed': 7.14, - }), - dict({ - 'apparent_temperature': 29.7, - 'cloud_coverage': 60.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-13T00:00:00Z', - 'dew_point': 21.8, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.89, - 'temperature': 27.1, - 'uv_index': 4, - 'wind_bearing': 141, - 'wind_gust_speed': 16.42, - 'wind_speed': 6.89, - }), - dict({ - 'apparent_temperature': 31.2, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T01:00:00Z', - 'dew_point': 22.0, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.65, - 'temperature': 28.4, - 'uv_index': 5, - 'wind_bearing': 137, - 'wind_gust_speed': 18.64, - 'wind_speed': 6.65, - }), - dict({ - 'apparent_temperature': 32.3, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T02:00:00Z', - 'dew_point': 21.9, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.26, - 'temperature': 29.4, - 'uv_index': 5, - 'wind_bearing': 128, - 'wind_gust_speed': 21.69, - 'wind_speed': 7.12, - }), - dict({ - 'apparent_temperature': 33.0, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T03:00:00Z', - 'dew_point': 21.9, - 'humidity': 62, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.88, - 'temperature': 30.1, - 'uv_index': 6, - 'wind_bearing': 111, - 'wind_gust_speed': 23.41, - 'wind_speed': 7.33, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 72.0, - 'condition': 'rainy', - 'datetime': '2023-09-13T04:00:00Z', - 'dew_point': 22.0, - 'humidity': 61, - 'precipitation': 0.9, - 'precipitation_probability': 12.0, - 'pressure': 1011.55, - 'temperature': 30.4, - 'uv_index': 5, - 'wind_bearing': 56, - 'wind_gust_speed': 23.1, - 'wind_speed': 8.09, - }), - dict({ - 'apparent_temperature': 33.2, - 'cloud_coverage': 72.0, - 'condition': 'rainy', - 'datetime': '2023-09-13T05:00:00Z', - 'dew_point': 21.9, - 'humidity': 61, - 'precipitation': 1.9, - 'precipitation_probability': 12.0, - 'pressure': 1011.29, - 'temperature': 30.2, - 'uv_index': 4, - 'wind_bearing': 20, - 'wind_gust_speed': 21.81, - 'wind_speed': 9.46, - }), - dict({ - 'apparent_temperature': 32.6, - 'cloud_coverage': 74.0, - 'condition': 'rainy', - 'datetime': '2023-09-13T06:00:00Z', - 'dew_point': 21.9, - 'humidity': 63, - 'precipitation': 2.3, - 'precipitation_probability': 11.0, - 'pressure': 1011.17, - 'temperature': 29.7, - 'uv_index': 3, - 'wind_bearing': 20, - 'wind_gust_speed': 19.72, - 'wind_speed': 9.8, - }), - dict({ - 'apparent_temperature': 31.8, - 'cloud_coverage': 69.0, - 'condition': 'rainy', - 'datetime': '2023-09-13T07:00:00Z', - 'dew_point': 22.4, - 'humidity': 68, - 'precipitation': 1.8, - 'precipitation_probability': 10.0, - 'pressure': 1011.32, - 'temperature': 28.8, - 'uv_index': 1, - 'wind_bearing': 18, - 'wind_gust_speed': 17.55, - 'wind_speed': 9.23, - }), - dict({ - 'apparent_temperature': 30.8, - 'cloud_coverage': 73.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T08:00:00Z', - 'dew_point': 22.9, - 'humidity': 76, - 'precipitation': 0.8, - 'precipitation_probability': 10.0, - 'pressure': 1011.6, - 'temperature': 27.6, - 'uv_index': 0, - 'wind_bearing': 27, - 'wind_gust_speed': 15.08, - 'wind_speed': 8.05, - }), - dict({ - 'apparent_temperature': 29.4, - 'cloud_coverage': 76.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T09:00:00Z', - 'dew_point': 23.0, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.94, - 'temperature': 26.3, - 'uv_index': 0, - 'wind_bearing': 32, - 'wind_gust_speed': 12.17, - 'wind_speed': 6.68, - }), - dict({ - 'apparent_temperature': 28.5, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T10:00:00Z', - 'dew_point': 22.9, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.3, - 'temperature': 25.5, - 'uv_index': 0, - 'wind_bearing': 69, - 'wind_gust_speed': 11.64, - 'wind_speed': 6.69, - }), - dict({ - 'apparent_temperature': 27.7, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T11:00:00Z', - 'dew_point': 22.6, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.71, - 'temperature': 25.0, - 'uv_index': 0, - 'wind_bearing': 155, - 'wind_gust_speed': 11.91, - 'wind_speed': 6.23, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T12:00:00Z', - 'dew_point': 22.3, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.96, - 'temperature': 24.5, - 'uv_index': 0, - 'wind_bearing': 161, - 'wind_gust_speed': 12.47, - 'wind_speed': 5.73, - }), - dict({ - 'apparent_temperature': 26.7, - 'cloud_coverage': 82.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T13:00:00Z', - 'dew_point': 22.3, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.03, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 161, - 'wind_gust_speed': 13.57, - 'wind_speed': 5.66, - }), - dict({ - 'apparent_temperature': 26.4, - 'cloud_coverage': 84.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T14:00:00Z', - 'dew_point': 22.2, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.99, - 'temperature': 23.9, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 15.07, - 'wind_speed': 5.83, - }), - dict({ - 'apparent_temperature': 26.1, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T15:00:00Z', - 'dew_point': 22.2, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.95, - 'temperature': 23.7, - 'uv_index': 0, - 'wind_bearing': 158, - 'wind_gust_speed': 16.06, - 'wind_speed': 5.93, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 88.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T16:00:00Z', - 'dew_point': 22.0, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.9, - 'temperature': 23.4, - 'uv_index': 0, - 'wind_bearing': 153, - 'wind_gust_speed': 16.05, - 'wind_speed': 5.75, - }), - dict({ - 'apparent_temperature': 25.4, - 'cloud_coverage': 90.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T17:00:00Z', - 'dew_point': 21.8, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.85, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 150, - 'wind_gust_speed': 15.52, - 'wind_speed': 5.49, - }), - dict({ - 'apparent_temperature': 25.2, - 'cloud_coverage': 92.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T18:00:00Z', - 'dew_point': 21.8, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.87, - 'temperature': 23.0, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 15.01, - 'wind_speed': 5.32, - }), - dict({ - 'apparent_temperature': 25.0, - 'cloud_coverage': 90.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T19:00:00Z', - 'dew_point': 21.7, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.01, - 'temperature': 22.8, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 14.39, - 'wind_speed': 5.33, - }), - dict({ - 'apparent_temperature': 24.8, - 'cloud_coverage': 89.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T20:00:00Z', - 'dew_point': 21.6, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.22, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 13.79, - 'wind_speed': 5.43, - }), - dict({ - 'apparent_temperature': 25.3, - 'cloud_coverage': 86.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T21:00:00Z', - 'dew_point': 21.8, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.41, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 147, - 'wind_gust_speed': 14.12, - 'wind_speed': 5.52, - }), - dict({ - 'apparent_temperature': 26.7, - 'cloud_coverage': 77.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T22:00:00Z', - 'dew_point': 22.1, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.59, - 'temperature': 24.3, - 'uv_index': 1, - 'wind_bearing': 147, - 'wind_gust_speed': 16.14, - 'wind_speed': 5.58, - }), - dict({ - 'apparent_temperature': 28.4, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-13T23:00:00Z', - 'dew_point': 22.4, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.74, - 'temperature': 25.7, - 'uv_index': 2, - 'wind_bearing': 146, - 'wind_gust_speed': 19.09, - 'wind_speed': 5.62, - }), - dict({ - 'apparent_temperature': 30.5, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T00:00:00Z', - 'dew_point': 22.9, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.78, - 'temperature': 27.4, - 'uv_index': 4, - 'wind_bearing': 143, - 'wind_gust_speed': 21.6, - 'wind_speed': 5.58, - }), - dict({ - 'apparent_temperature': 32.2, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T01:00:00Z', - 'dew_point': 23.2, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.61, - 'temperature': 28.7, - 'uv_index': 5, - 'wind_bearing': 138, - 'wind_gust_speed': 23.36, - 'wind_speed': 5.34, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 54.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T02:00:00Z', - 'dew_point': 23.2, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.32, - 'temperature': 29.9, - 'uv_index': 6, - 'wind_bearing': 111, - 'wind_gust_speed': 24.72, - 'wind_speed': 4.99, - }), - dict({ - 'apparent_temperature': 34.4, - 'cloud_coverage': 56.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T03:00:00Z', - 'dew_point': 23.3, - 'humidity': 65, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.04, - 'temperature': 30.7, - 'uv_index': 6, - 'wind_bearing': 354, - 'wind_gust_speed': 25.23, - 'wind_speed': 4.74, - }), - dict({ - 'apparent_temperature': 34.9, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T04:00:00Z', - 'dew_point': 23.4, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.77, - 'temperature': 31.0, - 'uv_index': 6, - 'wind_bearing': 341, - 'wind_gust_speed': 24.6, - 'wind_speed': 4.79, - }), - dict({ - 'apparent_temperature': 34.5, - 'cloud_coverage': 60.0, - 'condition': 'rainy', - 'datetime': '2023-09-14T05:00:00Z', - 'dew_point': 23.2, - 'humidity': 64, - 'precipitation': 0.2, - 'precipitation_probability': 15.0, - 'pressure': 1012.53, - 'temperature': 30.7, - 'uv_index': 5, - 'wind_bearing': 336, - 'wind_gust_speed': 23.28, - 'wind_speed': 5.07, - }), - dict({ - 'apparent_temperature': 33.8, - 'cloud_coverage': 59.0, - 'condition': 'rainy', - 'datetime': '2023-09-14T06:00:00Z', - 'dew_point': 23.1, - 'humidity': 66, - 'precipitation': 0.2, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1012.49, - 'temperature': 30.2, - 'uv_index': 3, - 'wind_bearing': 336, - 'wind_gust_speed': 22.05, - 'wind_speed': 5.34, - }), - dict({ - 'apparent_temperature': 32.9, - 'cloud_coverage': 53.0, - 'condition': 'rainy', - 'datetime': '2023-09-14T07:00:00Z', - 'dew_point': 23.0, - 'humidity': 68, - 'precipitation': 0.2, - 'precipitation_probability': 40.0, - 'pressure': 1012.73, - 'temperature': 29.5, - 'uv_index': 2, - 'wind_bearing': 339, - 'wind_gust_speed': 21.18, - 'wind_speed': 5.63, - }), - dict({ - 'apparent_temperature': 31.6, - 'cloud_coverage': 43.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T08:00:00Z', - 'dew_point': 22.8, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 45.0, - 'pressure': 1013.16, - 'temperature': 28.4, - 'uv_index': 0, - 'wind_bearing': 342, - 'wind_gust_speed': 20.35, - 'wind_speed': 5.93, - }), - dict({ - 'apparent_temperature': 30.0, - 'cloud_coverage': 35.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T09:00:00Z', - 'dew_point': 22.5, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1013.62, - 'temperature': 27.1, - 'uv_index': 0, - 'wind_bearing': 347, - 'wind_gust_speed': 19.42, - 'wind_speed': 5.95, - }), - dict({ - 'apparent_temperature': 29.0, - 'cloud_coverage': 32.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T10:00:00Z', - 'dew_point': 22.4, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.09, - 'temperature': 26.3, - 'uv_index': 0, - 'wind_bearing': 348, - 'wind_gust_speed': 18.19, - 'wind_speed': 5.31, - }), - dict({ - 'apparent_temperature': 28.2, - 'cloud_coverage': 31.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T11:00:00Z', - 'dew_point': 22.4, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.56, - 'temperature': 25.5, - 'uv_index': 0, - 'wind_bearing': 177, - 'wind_gust_speed': 16.79, - 'wind_speed': 4.28, - }), - dict({ - 'apparent_temperature': 27.5, - 'cloud_coverage': 31.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T12:00:00Z', - 'dew_point': 22.3, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.87, - 'temperature': 24.9, - 'uv_index': 0, - 'wind_bearing': 171, - 'wind_gust_speed': 15.61, - 'wind_speed': 3.72, - }), - dict({ - 'apparent_temperature': 26.6, - 'cloud_coverage': 31.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T13:00:00Z', - 'dew_point': 22.1, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.91, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 171, - 'wind_gust_speed': 14.7, - 'wind_speed': 4.11, - }), - dict({ - 'apparent_temperature': 25.9, - 'cloud_coverage': 32.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T14:00:00Z', - 'dew_point': 21.9, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.8, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 171, - 'wind_gust_speed': 13.81, - 'wind_speed': 4.97, - }), - dict({ - 'apparent_temperature': 25.3, - 'cloud_coverage': 34.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T15:00:00Z', - 'dew_point': 21.7, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.66, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 170, - 'wind_gust_speed': 12.88, - 'wind_speed': 5.57, - }), - dict({ - 'apparent_temperature': 24.8, - 'cloud_coverage': 37.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T16:00:00Z', - 'dew_point': 21.5, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.54, - 'temperature': 22.7, - 'uv_index': 0, - 'wind_bearing': 168, - 'wind_gust_speed': 12.0, - 'wind_speed': 5.62, - }), - dict({ - 'apparent_temperature': 24.4, - 'cloud_coverage': 39.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T17:00:00Z', - 'dew_point': 21.3, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 40.0, - 'pressure': 1014.45, - 'temperature': 22.4, - 'uv_index': 0, - 'wind_bearing': 165, - 'wind_gust_speed': 11.43, - 'wind_speed': 5.48, - }), - dict({ - 'apparent_temperature': 24.6, - 'cloud_coverage': 40.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T18:00:00Z', - 'dew_point': 21.4, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 44.0, - 'pressure': 1014.45, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 11.42, - 'wind_speed': 5.38, - }), - dict({ - 'apparent_temperature': 25.0, - 'cloud_coverage': 40.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T19:00:00Z', - 'dew_point': 21.6, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 52.0, - 'pressure': 1014.63, - 'temperature': 22.9, - 'uv_index': 0, - 'wind_bearing': 161, - 'wind_gust_speed': 12.15, - 'wind_speed': 5.39, - }), - dict({ - 'apparent_temperature': 25.6, - 'cloud_coverage': 38.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-14T20:00:00Z', - 'dew_point': 21.8, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 51.0, - 'pressure': 1014.91, - 'temperature': 23.4, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 13.54, - 'wind_speed': 5.45, - }), - dict({ - 'apparent_temperature': 26.6, - 'cloud_coverage': 36.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T21:00:00Z', - 'dew_point': 22.0, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 42.0, - 'pressure': 1015.18, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 158, - 'wind_gust_speed': 15.48, - 'wind_speed': 5.62, - }), - dict({ - 'apparent_temperature': 28.5, - 'cloud_coverage': 32.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T22:00:00Z', - 'dew_point': 22.5, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 28.999999999999996, - 'pressure': 1015.4, - 'temperature': 25.7, - 'uv_index': 1, - 'wind_bearing': 158, - 'wind_gust_speed': 17.86, - 'wind_speed': 5.84, - }), - dict({ - 'apparent_temperature': 30.3, - 'cloud_coverage': 30.0, - 'condition': 'sunny', - 'datetime': '2023-09-14T23:00:00Z', - 'dew_point': 22.9, - 'humidity': 77, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.54, - 'temperature': 27.2, - 'uv_index': 2, - 'wind_bearing': 155, - 'wind_gust_speed': 20.19, - 'wind_speed': 6.09, - }), - dict({ - 'apparent_temperature': 32.1, - 'cloud_coverage': 30.0, - 'condition': 'sunny', - 'datetime': '2023-09-15T00:00:00Z', - 'dew_point': 23.3, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.55, - 'temperature': 28.6, - 'uv_index': 4, - 'wind_bearing': 152, - 'wind_gust_speed': 21.83, - 'wind_speed': 6.42, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 34.0, - 'condition': 'sunny', - 'datetime': '2023-09-15T01:00:00Z', - 'dew_point': 23.5, - 'humidity': 70, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.35, - 'temperature': 29.6, - 'uv_index': 6, - 'wind_bearing': 144, - 'wind_gust_speed': 22.56, - 'wind_speed': 6.91, - }), - dict({ - 'apparent_temperature': 34.2, - 'cloud_coverage': 41.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T02:00:00Z', - 'dew_point': 23.5, - 'humidity': 67, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.0, - 'temperature': 30.4, - 'uv_index': 7, - 'wind_bearing': 336, - 'wind_gust_speed': 22.83, - 'wind_speed': 7.47, - }), - dict({ - 'apparent_temperature': 34.9, - 'cloud_coverage': 46.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T03:00:00Z', - 'dew_point': 23.5, - 'humidity': 65, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.62, - 'temperature': 30.9, - 'uv_index': 7, - 'wind_bearing': 336, - 'wind_gust_speed': 22.98, - 'wind_speed': 7.95, - }), - dict({ - 'apparent_temperature': 35.4, - 'cloud_coverage': 46.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T04:00:00Z', - 'dew_point': 23.6, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.25, - 'temperature': 31.3, - 'uv_index': 6, - 'wind_bearing': 341, - 'wind_gust_speed': 23.21, - 'wind_speed': 8.44, - }), - dict({ - 'apparent_temperature': 35.6, - 'cloud_coverage': 44.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T05:00:00Z', - 'dew_point': 23.7, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.95, - 'temperature': 31.5, - 'uv_index': 5, - 'wind_bearing': 344, - 'wind_gust_speed': 23.46, - 'wind_speed': 8.95, - }), - dict({ - 'apparent_temperature': 35.1, - 'cloud_coverage': 42.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T06:00:00Z', - 'dew_point': 23.6, - 'humidity': 64, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.83, - 'temperature': 31.1, - 'uv_index': 3, - 'wind_bearing': 347, - 'wind_gust_speed': 23.64, - 'wind_speed': 9.13, - }), - dict({ - 'apparent_temperature': 34.1, - 'cloud_coverage': 41.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T07:00:00Z', - 'dew_point': 23.4, - 'humidity': 66, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.96, - 'temperature': 30.3, - 'uv_index': 2, - 'wind_bearing': 350, - 'wind_gust_speed': 23.66, - 'wind_speed': 8.78, - }), - dict({ - 'apparent_temperature': 32.4, - 'cloud_coverage': 40.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T08:00:00Z', - 'dew_point': 23.1, - 'humidity': 70, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.25, - 'temperature': 29.0, - 'uv_index': 0, - 'wind_bearing': 356, - 'wind_gust_speed': 23.51, - 'wind_speed': 8.13, - }), - dict({ - 'apparent_temperature': 31.1, - 'cloud_coverage': 41.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T09:00:00Z', - 'dew_point': 22.9, - 'humidity': 74, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.61, - 'temperature': 27.9, - 'uv_index': 0, - 'wind_bearing': 3, - 'wind_gust_speed': 23.21, - 'wind_speed': 7.48, - }), - dict({ - 'apparent_temperature': 30.0, - 'cloud_coverage': 43.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T10:00:00Z', - 'dew_point': 22.8, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.02, - 'temperature': 26.9, - 'uv_index': 0, - 'wind_bearing': 20, - 'wind_gust_speed': 22.68, - 'wind_speed': 6.83, - }), - dict({ - 'apparent_temperature': 29.2, - 'cloud_coverage': 46.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T11:00:00Z', - 'dew_point': 22.8, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.43, - 'temperature': 26.2, - 'uv_index': 0, - 'wind_bearing': 129, - 'wind_gust_speed': 22.04, - 'wind_speed': 6.1, - }), - dict({ - 'apparent_temperature': 28.4, - 'cloud_coverage': 48.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T12:00:00Z', - 'dew_point': 22.7, - 'humidity': 84, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.71, - 'temperature': 25.6, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 21.64, - 'wind_speed': 5.6, - }), - dict({ - 'apparent_temperature': 28.2, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T13:00:00Z', - 'dew_point': 23.2, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.52, - 'temperature': 25.2, - 'uv_index': 0, - 'wind_bearing': 164, - 'wind_gust_speed': 16.35, - 'wind_speed': 5.58, - }), - dict({ - 'apparent_temperature': 27.4, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T14:00:00Z', - 'dew_point': 22.9, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.37, - 'temperature': 24.6, - 'uv_index': 0, - 'wind_bearing': 168, - 'wind_gust_speed': 17.11, - 'wind_speed': 5.79, - }), - dict({ - 'apparent_temperature': 26.9, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T15:00:00Z', - 'dew_point': 22.7, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.21, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 182, - 'wind_gust_speed': 17.32, - 'wind_speed': 5.77, - }), - dict({ - 'apparent_temperature': 26.4, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T16:00:00Z', - 'dew_point': 22.6, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.07, - 'temperature': 23.8, - 'uv_index': 0, - 'wind_bearing': 201, - 'wind_gust_speed': 16.6, - 'wind_speed': 5.27, - }), - dict({ - 'apparent_temperature': 26.0, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T17:00:00Z', - 'dew_point': 22.5, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.95, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 219, - 'wind_gust_speed': 15.52, - 'wind_speed': 4.62, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T18:00:00Z', - 'dew_point': 22.3, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.88, - 'temperature': 23.3, - 'uv_index': 0, - 'wind_bearing': 216, - 'wind_gust_speed': 14.64, - 'wind_speed': 4.32, - }), - dict({ - 'apparent_temperature': 26.0, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T19:00:00Z', - 'dew_point': 22.4, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.91, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 198, - 'wind_gust_speed': 14.06, - 'wind_speed': 4.73, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T20:00:00Z', - 'dew_point': 22.4, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.99, - 'temperature': 23.8, - 'uv_index': 0, - 'wind_bearing': 189, - 'wind_gust_speed': 13.7, - 'wind_speed': 5.49, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-15T21:00:00Z', - 'dew_point': 22.5, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.07, - 'temperature': 24.4, - 'uv_index': 0, - 'wind_bearing': 183, - 'wind_gust_speed': 13.77, - 'wind_speed': 5.95, - }), - dict({ - 'apparent_temperature': 28.3, - 'cloud_coverage': 59.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T22:00:00Z', - 'dew_point': 22.6, - 'humidity': 84, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.12, - 'temperature': 25.5, - 'uv_index': 1, - 'wind_bearing': 179, - 'wind_gust_speed': 14.38, - 'wind_speed': 5.77, - }), - dict({ - 'apparent_temperature': 29.9, - 'cloud_coverage': 52.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-15T23:00:00Z', - 'dew_point': 22.9, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.13, - 'temperature': 26.9, - 'uv_index': 2, - 'wind_bearing': 170, - 'wind_gust_speed': 15.2, - 'wind_speed': 5.27, - }), - dict({ - 'apparent_temperature': 31.2, - 'cloud_coverage': 44.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T00:00:00Z', - 'dew_point': 22.9, - 'humidity': 74, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1015.04, - 'temperature': 28.0, - 'uv_index': 4, - 'wind_bearing': 155, - 'wind_gust_speed': 15.85, - 'wind_speed': 4.76, - }), - dict({ - 'apparent_temperature': 32.5, - 'cloud_coverage': 24.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T01:00:00Z', - 'dew_point': 22.6, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.52, - 'temperature': 29.2, - 'uv_index': 6, - 'wind_bearing': 110, - 'wind_gust_speed': 16.27, - 'wind_speed': 6.81, - }), - dict({ - 'apparent_temperature': 33.5, - 'cloud_coverage': 16.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T02:00:00Z', - 'dew_point': 22.4, - 'humidity': 63, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1014.01, - 'temperature': 30.2, - 'uv_index': 8, - 'wind_bearing': 30, - 'wind_gust_speed': 16.55, - 'wind_speed': 6.86, - }), - dict({ - 'apparent_temperature': 34.2, - 'cloud_coverage': 10.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T03:00:00Z', - 'dew_point': 22.0, - 'humidity': 59, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.45, - 'temperature': 31.1, - 'uv_index': 8, - 'wind_bearing': 17, - 'wind_gust_speed': 16.52, - 'wind_speed': 6.8, - }), - dict({ - 'apparent_temperature': 34.7, - 'cloud_coverage': 10.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T04:00:00Z', - 'dew_point': 21.9, - 'humidity': 57, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.89, - 'temperature': 31.5, - 'uv_index': 8, - 'wind_bearing': 17, - 'wind_gust_speed': 16.08, - 'wind_speed': 6.62, - }), - dict({ - 'apparent_temperature': 34.9, - 'cloud_coverage': 10.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T05:00:00Z', - 'dew_point': 21.9, - 'humidity': 56, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.39, - 'temperature': 31.8, - 'uv_index': 6, - 'wind_bearing': 20, - 'wind_gust_speed': 15.48, - 'wind_speed': 6.45, - }), - dict({ - 'apparent_temperature': 34.5, - 'cloud_coverage': 10.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T06:00:00Z', - 'dew_point': 21.7, - 'humidity': 56, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.11, - 'temperature': 31.4, - 'uv_index': 4, - 'wind_bearing': 26, - 'wind_gust_speed': 15.08, - 'wind_speed': 6.43, - }), - dict({ - 'apparent_temperature': 33.6, - 'cloud_coverage': 7.000000000000001, - 'condition': 'sunny', - 'datetime': '2023-09-16T07:00:00Z', - 'dew_point': 21.7, - 'humidity': 59, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.15, - 'temperature': 30.7, - 'uv_index': 2, - 'wind_bearing': 39, - 'wind_gust_speed': 14.88, - 'wind_speed': 6.61, - }), - dict({ - 'apparent_temperature': 32.5, - 'cloud_coverage': 2.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T08:00:00Z', - 'dew_point': 21.9, - 'humidity': 63, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.41, - 'temperature': 29.6, - 'uv_index': 0, - 'wind_bearing': 72, - 'wind_gust_speed': 14.82, - 'wind_speed': 6.95, - }), - dict({ - 'apparent_temperature': 31.4, - 'cloud_coverage': 2.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T09:00:00Z', - 'dew_point': 22.1, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.75, - 'temperature': 28.5, - 'uv_index': 0, - 'wind_bearing': 116, - 'wind_gust_speed': 15.13, - 'wind_speed': 7.45, - }), - dict({ - 'apparent_temperature': 30.5, - 'cloud_coverage': 13.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T10:00:00Z', - 'dew_point': 22.3, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.13, - 'temperature': 27.6, - 'uv_index': 0, - 'wind_bearing': 140, - 'wind_gust_speed': 16.09, - 'wind_speed': 8.15, - }), - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 31.0, - 'condition': 'sunny', - 'datetime': '2023-09-16T11:00:00Z', - 'dew_point': 22.6, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.47, - 'temperature': 26.9, - 'uv_index': 0, - 'wind_bearing': 149, - 'wind_gust_speed': 17.37, - 'wind_speed': 8.87, - }), - dict({ - 'apparent_temperature': 29.3, - 'cloud_coverage': 45.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T12:00:00Z', - 'dew_point': 22.9, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.6, - 'temperature': 26.3, - 'uv_index': 0, - 'wind_bearing': 155, - 'wind_gust_speed': 18.29, - 'wind_speed': 9.21, - }), - dict({ - 'apparent_temperature': 28.7, - 'cloud_coverage': 51.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T13:00:00Z', - 'dew_point': 23.0, - 'humidity': 85, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.41, - 'temperature': 25.7, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 18.49, - 'wind_speed': 8.96, - }), - dict({ - 'apparent_temperature': 27.9, - 'cloud_coverage': 55.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T14:00:00Z', - 'dew_point': 22.8, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1013.01, - 'temperature': 25.0, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 18.47, - 'wind_speed': 8.45, - }), - dict({ - 'apparent_temperature': 27.2, - 'cloud_coverage': 59.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T15:00:00Z', - 'dew_point': 22.7, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.55, - 'temperature': 24.5, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 18.79, - 'wind_speed': 8.1, - }), - dict({ - 'apparent_temperature': 26.7, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T16:00:00Z', - 'dew_point': 22.6, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.1, - 'temperature': 24.0, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 19.81, - 'wind_speed': 8.15, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T17:00:00Z', - 'dew_point': 22.6, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.68, - 'temperature': 23.7, - 'uv_index': 0, - 'wind_bearing': 161, - 'wind_gust_speed': 20.96, - 'wind_speed': 8.3, - }), - dict({ - 'apparent_temperature': 26.0, - 'cloud_coverage': 72.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T18:00:00Z', - 'dew_point': 22.4, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.39, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 21.41, - 'wind_speed': 8.24, - }), - dict({ - 'apparent_temperature': 26.3, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T19:00:00Z', - 'dew_point': 22.5, - 'humidity': 93, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.29, - 'temperature': 23.8, - 'uv_index': 0, - 'wind_bearing': 159, - 'wind_gust_speed': 20.42, - 'wind_speed': 7.62, - }), - dict({ - 'apparent_temperature': 26.8, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-16T20:00:00Z', - 'dew_point': 22.6, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.31, - 'temperature': 24.2, - 'uv_index': 0, - 'wind_bearing': 158, - 'wind_gust_speed': 18.61, - 'wind_speed': 6.66, - }), - dict({ - 'apparent_temperature': 27.7, - 'cloud_coverage': 57.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T21:00:00Z', - 'dew_point': 22.6, - 'humidity': 87, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.37, - 'temperature': 24.9, - 'uv_index': 0, - 'wind_bearing': 158, - 'wind_gust_speed': 17.14, - 'wind_speed': 5.86, - }), - dict({ - 'apparent_temperature': 28.9, - 'cloud_coverage': 48.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T22:00:00Z', - 'dew_point': 22.6, - 'humidity': 82, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.46, - 'temperature': 26.0, - 'uv_index': 1, - 'wind_bearing': 161, - 'wind_gust_speed': 16.78, - 'wind_speed': 5.5, - }), - dict({ - 'apparent_temperature': 30.6, - 'cloud_coverage': 39.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-16T23:00:00Z', - 'dew_point': 22.9, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.51, - 'temperature': 27.5, - 'uv_index': 2, - 'wind_bearing': 165, - 'wind_gust_speed': 17.21, - 'wind_speed': 5.56, - }), - dict({ - 'apparent_temperature': 31.7, - 'cloud_coverage': 33.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T00:00:00Z', - 'dew_point': 22.8, - 'humidity': 71, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.39, - 'temperature': 28.5, - 'uv_index': 4, - 'wind_bearing': 174, - 'wind_gust_speed': 17.96, - 'wind_speed': 6.04, - }), - dict({ - 'apparent_temperature': 32.6, - 'cloud_coverage': 30.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T01:00:00Z', - 'dew_point': 22.7, - 'humidity': 68, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.98, - 'temperature': 29.4, - 'uv_index': 6, - 'wind_bearing': 192, - 'wind_gust_speed': 19.15, - 'wind_speed': 7.23, - }), - dict({ - 'apparent_temperature': 33.6, - 'cloud_coverage': 28.999999999999996, - 'condition': 'sunny', - 'datetime': '2023-09-17T02:00:00Z', - 'dew_point': 22.8, - 'humidity': 65, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1010.38, - 'temperature': 30.1, - 'uv_index': 7, - 'wind_bearing': 225, - 'wind_gust_speed': 20.89, - 'wind_speed': 8.9, - }), - dict({ - 'apparent_temperature': 34.1, - 'cloud_coverage': 30.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T03:00:00Z', - 'dew_point': 22.8, - 'humidity': 63, - 'precipitation': 0.3, - 'precipitation_probability': 9.0, - 'pressure': 1009.75, - 'temperature': 30.7, - 'uv_index': 8, - 'wind_bearing': 264, - 'wind_gust_speed': 22.67, - 'wind_speed': 10.27, - }), - dict({ - 'apparent_temperature': 33.9, - 'cloud_coverage': 37.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T04:00:00Z', - 'dew_point': 22.5, - 'humidity': 62, - 'precipitation': 0.4, - 'precipitation_probability': 10.0, - 'pressure': 1009.18, - 'temperature': 30.5, - 'uv_index': 7, - 'wind_bearing': 293, - 'wind_gust_speed': 23.93, - 'wind_speed': 10.82, - }), - dict({ - 'apparent_temperature': 33.4, - 'cloud_coverage': 45.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T05:00:00Z', - 'dew_point': 22.4, - 'humidity': 63, - 'precipitation': 0.6, - 'precipitation_probability': 12.0, - 'pressure': 1008.71, - 'temperature': 30.1, - 'uv_index': 5, - 'wind_bearing': 308, - 'wind_gust_speed': 24.39, - 'wind_speed': 10.72, - }), - dict({ - 'apparent_temperature': 32.7, - 'cloud_coverage': 50.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T06:00:00Z', - 'dew_point': 22.2, - 'humidity': 64, - 'precipitation': 0.7, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1008.46, - 'temperature': 29.6, - 'uv_index': 3, - 'wind_bearing': 312, - 'wind_gust_speed': 23.9, - 'wind_speed': 10.28, - }), - dict({ - 'apparent_temperature': 31.8, - 'cloud_coverage': 47.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T07:00:00Z', - 'dew_point': 22.1, - 'humidity': 67, - 'precipitation': 0.7, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1008.53, - 'temperature': 28.9, - 'uv_index': 1, - 'wind_bearing': 312, - 'wind_gust_speed': 22.3, - 'wind_speed': 9.59, - }), - dict({ - 'apparent_temperature': 30.6, - 'cloud_coverage': 41.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T08:00:00Z', - 'dew_point': 21.9, - 'humidity': 70, - 'precipitation': 0.6, - 'precipitation_probability': 15.0, - 'pressure': 1008.82, - 'temperature': 27.9, - 'uv_index': 0, - 'wind_bearing': 305, - 'wind_gust_speed': 19.73, - 'wind_speed': 8.58, - }), - dict({ - 'apparent_temperature': 29.6, - 'cloud_coverage': 35.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T09:00:00Z', - 'dew_point': 22.0, - 'humidity': 74, - 'precipitation': 0.5, - 'precipitation_probability': 15.0, - 'pressure': 1009.21, - 'temperature': 27.0, - 'uv_index': 0, - 'wind_bearing': 291, - 'wind_gust_speed': 16.49, - 'wind_speed': 7.34, - }), - dict({ - 'apparent_temperature': 28.6, - 'cloud_coverage': 33.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T10:00:00Z', - 'dew_point': 21.9, - 'humidity': 78, - 'precipitation': 0.4, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1009.65, - 'temperature': 26.1, - 'uv_index': 0, - 'wind_bearing': 257, - 'wind_gust_speed': 12.71, - 'wind_speed': 5.91, - }), - dict({ - 'apparent_temperature': 27.8, - 'cloud_coverage': 34.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T11:00:00Z', - 'dew_point': 21.9, - 'humidity': 82, - 'precipitation': 0.3, - 'precipitation_probability': 14.000000000000002, - 'pressure': 1010.04, - 'temperature': 25.3, - 'uv_index': 0, - 'wind_bearing': 212, - 'wind_gust_speed': 9.16, - 'wind_speed': 4.54, - }), - dict({ - 'apparent_temperature': 27.1, - 'cloud_coverage': 36.0, - 'condition': 'sunny', - 'datetime': '2023-09-17T12:00:00Z', - 'dew_point': 21.9, - 'humidity': 85, - 'precipitation': 0.3, - 'precipitation_probability': 28.000000000000004, - 'pressure': 1010.24, - 'temperature': 24.6, - 'uv_index': 0, - 'wind_bearing': 192, - 'wind_gust_speed': 7.09, - 'wind_speed': 3.62, - }), - dict({ - 'apparent_temperature': 26.5, - 'cloud_coverage': 40.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T13:00:00Z', - 'dew_point': 22.0, - 'humidity': 88, - 'precipitation': 0.3, - 'precipitation_probability': 30.0, - 'pressure': 1010.15, - 'temperature': 24.1, - 'uv_index': 0, - 'wind_bearing': 185, - 'wind_gust_speed': 7.2, - 'wind_speed': 3.27, - }), - dict({ - 'apparent_temperature': 25.9, - 'cloud_coverage': 44.0, - 'condition': 'rainy', - 'datetime': '2023-09-17T14:00:00Z', - 'dew_point': 21.8, - 'humidity': 90, - 'precipitation': 0.3, - 'precipitation_probability': 30.0, - 'pressure': 1009.87, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 182, - 'wind_gust_speed': 8.37, - 'wind_speed': 3.22, - }), - dict({ - 'apparent_temperature': 25.5, - 'cloud_coverage': 49.0, - 'condition': 'rainy', - 'datetime': '2023-09-17T15:00:00Z', - 'dew_point': 21.8, - 'humidity': 92, - 'precipitation': 0.2, - 'precipitation_probability': 31.0, - 'pressure': 1009.56, - 'temperature': 23.2, - 'uv_index': 0, - 'wind_bearing': 180, - 'wind_gust_speed': 9.21, - 'wind_speed': 3.3, - }), - dict({ - 'apparent_temperature': 25.1, - 'cloud_coverage': 53.0, - 'condition': 'rainy', - 'datetime': '2023-09-17T16:00:00Z', - 'dew_point': 21.8, - 'humidity': 94, - 'precipitation': 0.2, - 'precipitation_probability': 33.0, - 'pressure': 1009.29, - 'temperature': 22.9, - 'uv_index': 0, - 'wind_bearing': 182, - 'wind_gust_speed': 9.0, - 'wind_speed': 3.46, - }), - dict({ - 'apparent_temperature': 24.8, - 'cloud_coverage': 56.00000000000001, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T17:00:00Z', - 'dew_point': 21.7, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 35.0, - 'pressure': 1009.09, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 186, - 'wind_gust_speed': 8.37, - 'wind_speed': 3.72, - }), - dict({ - 'apparent_temperature': 24.6, - 'cloud_coverage': 59.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T18:00:00Z', - 'dew_point': 21.6, - 'humidity': 95, - 'precipitation': 0.0, - 'precipitation_probability': 37.0, - 'pressure': 1009.01, - 'temperature': 22.5, - 'uv_index': 0, - 'wind_bearing': 201, - 'wind_gust_speed': 7.99, - 'wind_speed': 4.07, - }), - dict({ - 'apparent_temperature': 24.9, - 'cloud_coverage': 62.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-17T19:00:00Z', - 'dew_point': 21.7, - 'humidity': 94, - 'precipitation': 0.0, - 'precipitation_probability': 39.0, - 'pressure': 1009.07, - 'temperature': 22.7, - 'uv_index': 0, - 'wind_bearing': 258, - 'wind_gust_speed': 8.18, - 'wind_speed': 4.55, - }), - dict({ - 'apparent_temperature': 25.2, - 'cloud_coverage': 64.0, - 'condition': 'cloudy', - 'datetime': '2023-09-17T20:00:00Z', - 'dew_point': 21.7, - 'humidity': 92, - 'precipitation': 0.0, - 'precipitation_probability': 39.0, - 'pressure': 1009.23, - 'temperature': 23.0, - 'uv_index': 0, - 'wind_bearing': 305, - 'wind_gust_speed': 8.77, - 'wind_speed': 5.17, - }), - dict({ - 'apparent_temperature': 25.8, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-17T21:00:00Z', - 'dew_point': 21.8, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 38.0, - 'pressure': 1009.47, - 'temperature': 23.5, - 'uv_index': 0, - 'wind_bearing': 318, - 'wind_gust_speed': 9.69, - 'wind_speed': 5.77, - }), - dict({ - 'apparent_temperature': 26.5, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-17T22:00:00Z', - 'dew_point': 21.8, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 30.0, - 'pressure': 1009.77, - 'temperature': 24.2, - 'uv_index': 1, - 'wind_bearing': 324, - 'wind_gust_speed': 10.88, - 'wind_speed': 6.26, - }), - dict({ - 'apparent_temperature': 27.6, - 'cloud_coverage': 80.0, - 'condition': 'rainy', - 'datetime': '2023-09-17T23:00:00Z', - 'dew_point': 21.9, - 'humidity': 83, - 'precipitation': 0.2, - 'precipitation_probability': 15.0, - 'pressure': 1010.09, - 'temperature': 25.1, - 'uv_index': 2, - 'wind_bearing': 329, - 'wind_gust_speed': 12.21, - 'wind_speed': 6.68, - }), - dict({ - 'apparent_temperature': 28.2, - 'cloud_coverage': 87.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T00:00:00Z', - 'dew_point': 21.9, - 'humidity': 80, - 'precipitation': 0.2, - 'precipitation_probability': 15.0, - 'pressure': 1010.33, - 'temperature': 25.7, - 'uv_index': 3, - 'wind_bearing': 332, - 'wind_gust_speed': 13.52, - 'wind_speed': 7.12, - }), - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 67.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T01:00:00Z', - 'dew_point': 21.7, - 'humidity': 72, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1007.43, - 'temperature': 27.2, - 'uv_index': 5, - 'wind_bearing': 330, - 'wind_gust_speed': 11.36, - 'wind_speed': 11.36, - }), - dict({ - 'apparent_temperature': 30.1, - 'cloud_coverage': 70.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T02:00:00Z', - 'dew_point': 21.6, - 'humidity': 70, - 'precipitation': 0.3, - 'precipitation_probability': 9.0, - 'pressure': 1007.05, - 'temperature': 27.5, - 'uv_index': 6, - 'wind_bearing': 332, - 'wind_gust_speed': 12.06, - 'wind_speed': 12.06, - }), - dict({ - 'apparent_temperature': 30.3, - 'cloud_coverage': 71.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T03:00:00Z', - 'dew_point': 21.6, - 'humidity': 69, - 'precipitation': 0.5, - 'precipitation_probability': 10.0, - 'pressure': 1006.67, - 'temperature': 27.8, - 'uv_index': 6, - 'wind_bearing': 333, - 'wind_gust_speed': 12.81, - 'wind_speed': 12.81, - }), - dict({ - 'apparent_temperature': 30.6, - 'cloud_coverage': 67.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T04:00:00Z', - 'dew_point': 21.5, - 'humidity': 68, - 'precipitation': 0.4, - 'precipitation_probability': 10.0, - 'pressure': 1006.28, - 'temperature': 28.0, - 'uv_index': 5, - 'wind_bearing': 335, - 'wind_gust_speed': 13.68, - 'wind_speed': 13.68, - }), - dict({ - 'apparent_temperature': 30.7, - 'cloud_coverage': 60.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T05:00:00Z', - 'dew_point': 21.4, - 'humidity': 67, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1005.89, - 'temperature': 28.1, - 'uv_index': 4, - 'wind_bearing': 336, - 'wind_gust_speed': 14.61, - 'wind_speed': 14.61, - }), - dict({ - 'apparent_temperature': 30.3, - 'cloud_coverage': 56.99999999999999, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T06:00:00Z', - 'dew_point': 21.2, - 'humidity': 67, - 'precipitation': 0.0, - 'precipitation_probability': 27.0, - 'pressure': 1005.67, - 'temperature': 27.9, - 'uv_index': 3, - 'wind_bearing': 338, - 'wind_gust_speed': 15.25, - 'wind_speed': 15.25, - }), - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 60.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T07:00:00Z', - 'dew_point': 21.3, - 'humidity': 69, - 'precipitation': 0.0, - 'precipitation_probability': 28.000000000000004, - 'pressure': 1005.74, - 'temperature': 27.4, - 'uv_index': 1, - 'wind_bearing': 339, - 'wind_gust_speed': 15.45, - 'wind_speed': 15.45, - }), - dict({ - 'apparent_temperature': 29.1, - 'cloud_coverage': 65.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T08:00:00Z', - 'dew_point': 21.4, - 'humidity': 73, - 'precipitation': 0.0, - 'precipitation_probability': 26.0, - 'pressure': 1005.98, - 'temperature': 26.7, - 'uv_index': 0, - 'wind_bearing': 341, - 'wind_gust_speed': 15.38, - 'wind_speed': 15.38, - }), - dict({ - 'apparent_temperature': 28.6, - 'cloud_coverage': 68.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T09:00:00Z', - 'dew_point': 21.6, - 'humidity': 76, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1006.22, - 'temperature': 26.1, - 'uv_index': 0, - 'wind_bearing': 341, - 'wind_gust_speed': 15.27, - 'wind_speed': 15.27, - }), - dict({ - 'apparent_temperature': 27.9, - 'cloud_coverage': 66.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T10:00:00Z', - 'dew_point': 21.6, - 'humidity': 79, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1006.44, - 'temperature': 25.6, - 'uv_index': 0, - 'wind_bearing': 339, - 'wind_gust_speed': 15.09, - 'wind_speed': 15.09, - }), - dict({ - 'apparent_temperature': 27.6, - 'cloud_coverage': 61.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T11:00:00Z', - 'dew_point': 21.7, - 'humidity': 81, - 'precipitation': 0.0, - 'precipitation_probability': 26.0, - 'pressure': 1006.66, - 'temperature': 25.2, - 'uv_index': 0, - 'wind_bearing': 336, - 'wind_gust_speed': 14.88, - 'wind_speed': 14.88, - }), - dict({ - 'apparent_temperature': 27.2, - 'cloud_coverage': 61.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T12:00:00Z', - 'dew_point': 21.8, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 26.0, - 'pressure': 1006.79, - 'temperature': 24.8, - 'uv_index': 0, - 'wind_bearing': 333, - 'wind_gust_speed': 14.91, - 'wind_speed': 14.91, - }), - dict({ - 'apparent_temperature': 25.7, - 'cloud_coverage': 38.0, - 'condition': 'partlycloudy', - 'datetime': '2023-09-18T13:00:00Z', - 'dew_point': 21.2, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1012.36, - 'temperature': 23.6, - 'uv_index': 0, - 'wind_bearing': 83, - 'wind_gust_speed': 4.58, - 'wind_speed': 3.16, - }), - dict({ - 'apparent_temperature': 25.1, - 'cloud_coverage': 74.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T14:00:00Z', - 'dew_point': 21.2, - 'humidity': 89, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.96, - 'temperature': 23.1, - 'uv_index': 0, - 'wind_bearing': 144, - 'wind_gust_speed': 4.74, - 'wind_speed': 4.52, - }), - dict({ - 'apparent_temperature': 24.5, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T15:00:00Z', - 'dew_point': 20.9, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.6, - 'temperature': 22.6, - 'uv_index': 0, - 'wind_bearing': 152, - 'wind_gust_speed': 5.63, - 'wind_speed': 5.63, - }), - dict({ - 'apparent_temperature': 24.0, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T16:00:00Z', - 'dew_point': 20.7, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.37, - 'temperature': 22.3, - 'uv_index': 0, - 'wind_bearing': 156, - 'wind_gust_speed': 6.02, - 'wind_speed': 6.02, - }), - dict({ - 'apparent_temperature': 23.7, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T17:00:00Z', - 'dew_point': 20.4, - 'humidity': 91, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.2, - 'temperature': 22.0, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 6.15, - 'wind_speed': 6.15, - }), - dict({ - 'apparent_temperature': 23.4, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T18:00:00Z', - 'dew_point': 20.2, - 'humidity': 90, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.08, - 'temperature': 21.9, - 'uv_index': 0, - 'wind_bearing': 167, - 'wind_gust_speed': 6.48, - 'wind_speed': 6.48, - }), - dict({ - 'apparent_temperature': 23.2, - 'cloud_coverage': 100.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T19:00:00Z', - 'dew_point': 19.8, - 'humidity': 88, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.04, - 'temperature': 21.8, - 'uv_index': 0, - 'wind_bearing': 165, - 'wind_gust_speed': 7.51, - 'wind_speed': 7.51, - }), - dict({ - 'apparent_temperature': 23.4, - 'cloud_coverage': 99.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T20:00:00Z', - 'dew_point': 19.6, - 'humidity': 86, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.05, - 'temperature': 22.0, - 'uv_index': 0, - 'wind_bearing': 162, - 'wind_gust_speed': 8.73, - 'wind_speed': 8.73, - }), - dict({ - 'apparent_temperature': 23.9, - 'cloud_coverage': 98.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T21:00:00Z', - 'dew_point': 19.5, - 'humidity': 83, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.06, - 'temperature': 22.5, - 'uv_index': 0, - 'wind_bearing': 164, - 'wind_gust_speed': 9.21, - 'wind_speed': 9.11, - }), - dict({ - 'apparent_temperature': 25.3, - 'cloud_coverage': 96.0, - 'condition': 'cloudy', - 'datetime': '2023-09-18T22:00:00Z', - 'dew_point': 19.7, - 'humidity': 78, - 'precipitation': 0.0, - 'precipitation_probability': 0.0, - 'pressure': 1011.09, - 'temperature': 23.8, - 'uv_index': 1, - 'wind_bearing': 171, - 'wind_gust_speed': 9.03, - 'wind_speed': 7.91, - }), - ]), - }) -# --- # name: test_hourly_forecast[get_forecasts] dict({ 'weather.home': dict({ diff --git a/tests/components/wyoming/snapshots/test_config_flow.ambr b/tests/components/wyoming/snapshots/test_config_flow.ambr index a0e0c7c5011..ee4c5533254 100644 --- a/tests/components/wyoming/snapshots/test_config_flow.ambr +++ b/tests/components/wyoming/snapshots/test_config_flow.ambr @@ -1,42 +1,4 @@ # serializer version: 1 -# name: test_hassio_addon_discovery - FlowResultSnapshot({ - 'context': dict({ - 'source': 'hassio', - 'unique_id': '1234', - }), - 'data': dict({ - 'host': 'mock-piper', - 'port': 10200, - }), - 'description': None, - 'description_placeholders': None, - 'flow_id': , - 'handler': 'wyoming', - 'options': dict({ - }), - 'result': ConfigEntrySnapshot({ - 'data': dict({ - 'host': 'mock-piper', - 'port': 10200, - }), - 'disabled_by': None, - 'domain': 'wyoming', - 'entry_id': , - 'options': dict({ - }), - 'pref_disable_new_entities': False, - 'pref_disable_polling': False, - 'source': 'hassio', - 'title': 'Piper', - 'unique_id': '1234', - 'version': 1, - }), - 'title': 'Piper', - 'type': , - 'version': 1, - }) -# --- # name: test_hassio_addon_discovery[info0] FlowResultSnapshot({ 'context': dict({ diff --git a/tests/components/wyoming/snapshots/test_tts.ambr b/tests/components/wyoming/snapshots/test_tts.ambr index 299bddb07e5..7ca5204e66c 100644 --- a/tests/components/wyoming/snapshots/test_tts.ambr +++ b/tests/components/wyoming/snapshots/test_tts.ambr @@ -32,28 +32,6 @@ }), ]) # --- -# name: test_get_tts_audio_mp3 - list([ - dict({ - 'data': dict({ - 'text': 'Hello world', - }), - 'payload': None, - 'type': 'synthesize', - }), - ]) -# --- -# name: test_get_tts_audio_raw - list([ - dict({ - 'data': dict({ - 'text': 'Hello world', - }), - 'payload': None, - 'type': 'synthesize', - }), - ]) -# --- # name: test_voice_speaker list([ dict({ diff --git a/tests/components/zeversolar/snapshots/test_sensor.ambr b/tests/components/zeversolar/snapshots/test_sensor.ambr index bee522133a5..aaef2c43d79 100644 --- a/tests/components/zeversolar/snapshots/test_sensor.ambr +++ b/tests/components/zeversolar/snapshots/test_sensor.ambr @@ -1,24 +1,4 @@ # serializer version: 1 -# name: test_sensors - ConfigEntrySnapshot({ - 'data': dict({ - 'host': 'zeversolar-fake-host', - 'port': 10200, - }), - 'disabled_by': None, - 'domain': 'zeversolar', - 'entry_id': , - 'minor_version': 1, - 'options': dict({ - }), - 'pref_disable_new_entities': False, - 'pref_disable_polling': False, - 'source': 'user', - 'title': 'Mock Title', - 'unique_id': None, - 'version': 1, - }) -# --- # name: test_sensors[sensor.zeversolar_sensor_energy_today-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From ecf22e4c4f8d28d1198a1f38c40d320b9086e858 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Aug 2024 13:29:38 +0200 Subject: [PATCH 2163/2411] Improve type hints in logbook tests (#123652) --- tests/components/logbook/test_init.py | 154 +++++++++++------- .../components/logbook/test_websocket_api.py | 6 - 2 files changed, 93 insertions(+), 67 deletions(-) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 3a20aac2602..9dc96410166 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -65,12 +65,12 @@ async def hass_(recorder_mock: Recorder, hass: HomeAssistant) -> HomeAssistant: @pytest.fixture -async def set_utc(hass): +async def set_utc(hass: HomeAssistant) -> None: """Set timezone to UTC.""" await hass.config.async_set_time_zone("UTC") -async def test_service_call_create_logbook_entry(hass_) -> None: +async def test_service_call_create_logbook_entry(hass_: HomeAssistant) -> None: """Test if service call create log book entry.""" calls = async_capture_events(hass_, logbook.EVENT_LOGBOOK_ENTRY) @@ -123,8 +123,9 @@ async def test_service_call_create_logbook_entry(hass_) -> None: assert last_call.data.get(logbook.ATTR_DOMAIN) == "logbook" +@pytest.mark.usefixtures("recorder_mock") async def test_service_call_create_logbook_entry_invalid_entity_id( - recorder_mock: Recorder, hass: HomeAssistant + hass: HomeAssistant, ) -> None: """Test if service call create log book entry with an invalid entity id.""" await async_setup_component(hass, "logbook", {}) @@ -153,7 +154,9 @@ async def test_service_call_create_logbook_entry_invalid_entity_id( assert events[0][logbook.ATTR_MESSAGE] == "is triggered" -async def test_service_call_create_log_book_entry_no_message(hass_) -> None: +async def test_service_call_create_log_book_entry_no_message( + hass_: HomeAssistant, +) -> None: """Test if service call create log book entry without message.""" calls = async_capture_events(hass_, logbook.EVENT_LOGBOOK_ENTRY) @@ -169,7 +172,7 @@ async def test_service_call_create_log_book_entry_no_message(hass_) -> None: async def test_filter_sensor( - hass_: ha.HomeAssistant, hass_client: ClientSessionGenerator + hass_: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test numeric sensors are filtered.""" @@ -217,7 +220,7 @@ async def test_filter_sensor( _assert_entry(entries[2], name="ble", entity_id=entity_id4, state="10") -async def test_home_assistant_start_stop_not_grouped(hass_) -> None: +async def test_home_assistant_start_stop_not_grouped(hass_: HomeAssistant) -> None: """Test if HA start and stop events are no longer grouped.""" await async_setup_component(hass_, "homeassistant", {}) await hass_.async_block_till_done() @@ -234,7 +237,7 @@ async def test_home_assistant_start_stop_not_grouped(hass_) -> None: assert_entry(entries[1], name="Home Assistant", message="started", domain=ha.DOMAIN) -async def test_home_assistant_start(hass_) -> None: +async def test_home_assistant_start(hass_: HomeAssistant) -> None: """Test if HA start is not filtered or converted into a restart.""" await async_setup_component(hass_, "homeassistant", {}) await hass_.async_block_till_done() @@ -254,7 +257,7 @@ async def test_home_assistant_start(hass_) -> None: assert_entry(entries[1], pointA, "bla", entity_id=entity_id) -def test_process_custom_logbook_entries(hass_) -> None: +def test_process_custom_logbook_entries(hass_: HomeAssistant) -> None: """Test if custom log book entries get added as an entry.""" name = "Nice name" message = "has a custom entry" @@ -339,8 +342,9 @@ def create_state_changed_event_from_old_new( return LazyEventPartialState(row, {}) +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_view( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook view.""" await async_setup_component(hass, "logbook", {}) @@ -350,8 +354,9 @@ async def test_logbook_view( assert response.status == HTTPStatus.OK +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_view_invalid_start_date_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook view with an invalid date time.""" await async_setup_component(hass, "logbook", {}) @@ -361,8 +366,9 @@ async def test_logbook_view_invalid_start_date_time( assert response.status == HTTPStatus.BAD_REQUEST +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_view_invalid_end_date_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook view.""" await async_setup_component(hass, "logbook", {}) @@ -374,11 +380,10 @@ async def test_logbook_view_invalid_end_date_time( assert response.status == HTTPStatus.BAD_REQUEST +@pytest.mark.usefixtures("recorder_mock", "set_utc") async def test_logbook_view_period_entity( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator, - set_utc, ) -> None: """Test the logbook view with period and entity.""" await async_setup_component(hass, "logbook", {}) @@ -460,8 +465,9 @@ async def test_logbook_view_period_entity( assert response_json[0]["entity_id"] == entity_id_test +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_describe_event( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test teaching logbook about a new event.""" @@ -508,8 +514,9 @@ async def test_logbook_describe_event( assert event["domain"] == "test_domain" +@pytest.mark.usefixtures("recorder_mock") async def test_exclude_described_event( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test exclusions of events that are described by another integration.""" name = "My Automation Rule" @@ -579,8 +586,9 @@ async def test_exclude_described_event( assert event["entity_id"] == "automation.included_rule" +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_view_end_time_entity( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook view with end_time and entity.""" await async_setup_component(hass, "logbook", {}) @@ -639,8 +647,9 @@ async def test_logbook_view_end_time_entity( assert response_json[0]["entity_id"] == entity_id_test +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_entity_filter_with_automations( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook view with end_time and entity with automations and scripts.""" await asyncio.gather( @@ -725,8 +734,9 @@ async def test_logbook_entity_filter_with_automations( assert json_dict[0]["entity_id"] == entity_id_second +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_entity_no_longer_in_state_machine( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook view with an entity that hass been removed from the state machine.""" await async_setup_component(hass, "logbook", {}) @@ -764,11 +774,10 @@ async def test_logbook_entity_no_longer_in_state_machine( assert json_dict[0]["name"] == "area 001" +@pytest.mark.usefixtures("recorder_mock", "set_utc") async def test_filter_continuous_sensor_values( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator, - set_utc, ) -> None: """Test remove continuous sensor events from logbook.""" await async_setup_component(hass, "logbook", {}) @@ -808,11 +817,10 @@ async def test_filter_continuous_sensor_values( assert response_json[1]["entity_id"] == entity_id_third +@pytest.mark.usefixtures("recorder_mock", "set_utc") async def test_exclude_new_entities( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator, - set_utc, ) -> None: """Test if events are excluded on first update.""" await asyncio.gather( @@ -850,11 +858,10 @@ async def test_exclude_new_entities( assert response_json[1]["message"] == "started" +@pytest.mark.usefixtures("recorder_mock", "set_utc") async def test_exclude_removed_entities( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator, - set_utc, ) -> None: """Test if events are excluded on last update.""" await asyncio.gather( @@ -899,11 +906,10 @@ async def test_exclude_removed_entities( assert response_json[2]["entity_id"] == entity_id2 +@pytest.mark.usefixtures("recorder_mock", "set_utc") async def test_exclude_attribute_changes( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator, - set_utc, ) -> None: """Test if events of attribute changes are filtered.""" await asyncio.gather( @@ -944,8 +950,9 @@ async def test_exclude_attribute_changes( assert response_json[2]["entity_id"] == "light.kitchen" +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_entity_context_id( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook view with end_time and entity with automations and scripts.""" await asyncio.gather( @@ -1097,8 +1104,9 @@ async def test_logbook_entity_context_id( assert json_dict[7]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_context_id_automation_script_started_manually( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook populates context_ids for scripts and automations started manually.""" await asyncio.gather( @@ -1189,8 +1197,9 @@ async def test_logbook_context_id_automation_script_started_manually( assert json_dict[4]["context_domain"] == "script" +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_entity_context_parent_id( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook view links events via context parent_id.""" await asyncio.gather( @@ -1371,8 +1380,9 @@ async def test_logbook_entity_context_parent_id( assert json_dict[8]["context_user_id"] == "485cacf93ef84d25a99ced3126b921d2" +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_context_from_template( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook view with end_time and entity with automations and scripts.""" await asyncio.gather( @@ -1461,8 +1471,9 @@ async def test_logbook_context_from_template( assert json_dict[5]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook view with a single entity and .""" await async_setup_component(hass, "logbook", {}) @@ -1532,8 +1543,9 @@ async def test_logbook_( assert json_dict[1]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_many_entities_multiple_calls( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook view with a many entities called multiple times.""" await async_setup_component(hass, "logbook", {}) @@ -1604,8 +1616,9 @@ async def test_logbook_many_entities_multiple_calls( assert len(json_dict) == 0 +@pytest.mark.usefixtures("recorder_mock") async def test_custom_log_entry_discoverable_via_( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test if a custom log entry is later discoverable via .""" await async_setup_component(hass, "logbook", {}) @@ -1641,8 +1654,9 @@ async def test_custom_log_entry_discoverable_via_( assert json_dict[0]["entity_id"] == "switch.test_switch" +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_multiple_entities( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook view with a multiple entities.""" await async_setup_component(hass, "logbook", {}) @@ -1767,8 +1781,9 @@ async def test_logbook_multiple_entities( assert json_dict[3]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_invalid_entity( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook view with requesting an invalid entity.""" await async_setup_component(hass, "logbook", {}) @@ -1787,8 +1802,9 @@ async def test_logbook_invalid_entity( assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR +@pytest.mark.usefixtures("recorder_mock") async def test_icon_and_state( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test to ensure state and custom icons are returned.""" await asyncio.gather( @@ -1832,8 +1848,9 @@ async def test_icon_and_state( assert response_json[2]["state"] == STATE_OFF +@pytest.mark.usefixtures("recorder_mock") async def test_fire_logbook_entries( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test many logbook entry calls.""" await async_setup_component(hass, "logbook", {}) @@ -1870,8 +1887,9 @@ async def test_fire_logbook_entries( assert len(response_json) == 11 +@pytest.mark.usefixtures("recorder_mock") async def test_exclude_events_domain( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test if events are filtered if domain is excluded in config.""" entity_id = "switch.bla" @@ -1906,8 +1924,9 @@ async def test_exclude_events_domain( _assert_entry(entries[1], name="blu", entity_id=entity_id2) +@pytest.mark.usefixtures("recorder_mock") async def test_exclude_events_domain_glob( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test if events are filtered if domain or glob is excluded in config.""" entity_id = "switch.bla" @@ -1951,8 +1970,9 @@ async def test_exclude_events_domain_glob( _assert_entry(entries[1], name="blu", entity_id=entity_id2) +@pytest.mark.usefixtures("recorder_mock") async def test_include_events_entity( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test if events are filtered if entity is included in config.""" entity_id = "sensor.bla" @@ -1993,8 +2013,9 @@ async def test_include_events_entity( _assert_entry(entries[1], name="blu", entity_id=entity_id2) +@pytest.mark.usefixtures("recorder_mock") async def test_exclude_events_entity( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test if events are filtered if entity is excluded in config.""" entity_id = "sensor.bla" @@ -2029,8 +2050,9 @@ async def test_exclude_events_entity( _assert_entry(entries[1], name="blu", entity_id=entity_id2) +@pytest.mark.usefixtures("recorder_mock") async def test_include_events_domain( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test if events are filtered if domain is included in config.""" assert await async_setup_component(hass, "alexa", {}) @@ -2073,8 +2095,9 @@ async def test_include_events_domain( _assert_entry(entries[2], name="blu", entity_id=entity_id2) +@pytest.mark.usefixtures("recorder_mock") async def test_include_events_domain_glob( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test if events are filtered if domain or glob is included in config.""" assert await async_setup_component(hass, "alexa", {}) @@ -2132,8 +2155,9 @@ async def test_include_events_domain_glob( _assert_entry(entries[3], name="included", entity_id=entity_id3) +@pytest.mark.usefixtures("recorder_mock") async def test_include_exclude_events_no_globs( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test if events are filtered if include and exclude is configured.""" entity_id = "switch.bla" @@ -2190,8 +2214,9 @@ async def test_include_exclude_events_no_globs( _assert_entry(entries[5], name="keep", entity_id=entity_id4, state="10") +@pytest.mark.usefixtures("recorder_mock") async def test_include_exclude_events_with_glob_filters( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test if events are filtered if include and exclude is configured.""" entity_id = "switch.bla" @@ -2256,8 +2281,9 @@ async def test_include_exclude_events_with_glob_filters( _assert_entry(entries[6], name="included", entity_id=entity_id5, state="30") +@pytest.mark.usefixtures("recorder_mock") async def test_empty_config( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test we can handle an empty entity filter.""" entity_id = "sensor.blu" @@ -2290,8 +2316,9 @@ async def test_empty_config( _assert_entry(entries[1], name="blu", entity_id=entity_id) +@pytest.mark.usefixtures("recorder_mock") async def test_context_filter( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test we can filter by context.""" assert await async_setup_component(hass, "logbook", {}) @@ -2367,8 +2394,9 @@ def _assert_entry( assert state == entry["state"] +@pytest.mark.usefixtures("recorder_mock") async def test_get_events( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test logbook get_events.""" now = dt_util.utcnow() @@ -2487,8 +2515,9 @@ async def test_get_events( assert isinstance(results[0]["when"], float) +@pytest.mark.usefixtures("recorder_mock") async def test_get_events_future_start_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test get_events with a future start time.""" await async_setup_component(hass, "logbook", {}) @@ -2512,8 +2541,9 @@ async def test_get_events_future_start_time( assert len(results) == 0 +@pytest.mark.usefixtures("recorder_mock") async def test_get_events_bad_start_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test get_events bad start time.""" await async_setup_component(hass, "logbook", {}) @@ -2532,8 +2562,9 @@ async def test_get_events_bad_start_time( assert response["error"]["code"] == "invalid_start_time" +@pytest.mark.usefixtures("recorder_mock") async def test_get_events_bad_end_time( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test get_events bad end time.""" now = dt_util.utcnow() @@ -2554,8 +2585,9 @@ async def test_get_events_bad_end_time( assert response["error"]["code"] == "invalid_end_time" +@pytest.mark.usefixtures("recorder_mock") async def test_get_events_invalid_filters( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test get_events invalid filters.""" await async_setup_component(hass, "logbook", {}) @@ -2584,8 +2616,8 @@ async def test_get_events_invalid_filters( assert response["error"]["code"] == "invalid_format" +@pytest.mark.usefixtures("recorder_mock") async def test_get_events_with_device_ids( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator, device_registry: dr.DeviceRegistry, @@ -2725,8 +2757,9 @@ async def test_get_events_with_device_ids( assert isinstance(results[3]["when"], float) +@pytest.mark.usefixtures("recorder_mock") async def test_logbook_select_entities_context_id( - recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test the logbook view with end_time and entity with automations and scripts.""" await asyncio.gather( @@ -2860,8 +2893,9 @@ async def test_logbook_select_entities_context_id( assert json_dict[3]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" +@pytest.mark.usefixtures("recorder_mock") async def test_get_events_with_context_state( - recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test logbook get_events with a context state.""" now = dt_util.utcnow() @@ -2925,9 +2959,8 @@ async def test_get_events_with_context_state( assert "context_event_type" not in results[3] -async def test_logbook_with_empty_config( - recorder_mock: Recorder, hass: HomeAssistant -) -> None: +@pytest.mark.usefixtures("recorder_mock") +async def test_logbook_with_empty_config(hass: HomeAssistant) -> None: """Test we handle a empty configuration.""" assert await async_setup_component( hass, @@ -2940,9 +2973,8 @@ async def test_logbook_with_empty_config( await hass.async_block_till_done() -async def test_logbook_with_non_iterable_entity_filter( - recorder_mock: Recorder, hass: HomeAssistant -) -> None: +@pytest.mark.usefixtures("recorder_mock") +async def test_logbook_with_non_iterable_entity_filter(hass: HomeAssistant) -> None: """Test we handle a non-iterable entity filter.""" assert await async_setup_component( hass, diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 9b1a6bb44cc..e5649564f94 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -48,12 +48,6 @@ from tests.components.recorder.common import ( from tests.typing import RecorderInstanceGenerator, WebSocketGenerator -@pytest.fixture -async def set_utc(hass): - """Set timezone to UTC.""" - await hass.config.async_set_time_zone("UTC") - - def listeners_without_writes(listeners: dict[str, int]) -> dict[str, int]: """Return listeners without final write listeners since we are not testing for these.""" return { From 81faf1b5820f23b8c11a6eab40a0dae54ca42ef2 Mon Sep 17 00:00:00 2001 From: "Barry vd. Heuvel" Date: Mon, 12 Aug 2024 14:01:12 +0200 Subject: [PATCH 2164/2411] Add homematicip_cloud service set cooling home (#121943) * [homematicip_cloud] Add service to set cooling mode * Create seperate test for cooling * Rename service to set_home_cooling_mode * Raise exception when accesspoint not found --- .../components/homematicip_cloud/icons.json | 3 +- .../homematicip_cloud/manifest.json | 2 +- .../components/homematicip_cloud/services.py | 40 +++++++++++- .../homematicip_cloud/services.yaml | 11 ++++ .../components/homematicip_cloud/strings.json | 19 ++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homematicip_cloud/test_climate.py | 65 ++++++++++++++++--- .../components/homematicip_cloud/test_init.py | 6 +- 9 files changed, 133 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/icons.json b/homeassistant/components/homematicip_cloud/icons.json index 2e9f6158c35..73c60ea8cdd 100644 --- a/homeassistant/components/homematicip_cloud/icons.json +++ b/homeassistant/components/homematicip_cloud/icons.json @@ -7,6 +7,7 @@ "deactivate_vacation": "mdi:compass-off", "set_active_climate_profile": "mdi:home-thermometer", "dump_hap_config": "mdi:database-export", - "reset_energy_counter": "mdi:reload" + "reset_energy_counter": "mdi:reload", + "set_home_cooling_mode": "mdi:snowflake" } } diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 024cb2d9f21..b3e7eb9a72a 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_push", "loggers": ["homematicip"], "quality_scale": "silver", - "requirements": ["homematicip==1.1.1"] + "requirements": ["homematicip==1.1.2"] } diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py index 37cda9e7683..4c04e4a858b 100644 --- a/homeassistant/components/homematicip_cloud/services.py +++ b/homeassistant/components/homematicip_cloud/services.py @@ -13,6 +13,7 @@ import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ServiceValidationError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import comp_entity_ids from homeassistant.helpers.service import ( @@ -31,6 +32,7 @@ ATTR_CONFIG_OUTPUT_FILE_PREFIX = "config_output_file_prefix" ATTR_CONFIG_OUTPUT_PATH = "config_output_path" ATTR_DURATION = "duration" ATTR_ENDTIME = "endtime" +ATTR_COOLING = "cooling" DEFAULT_CONFIG_FILE_PREFIX = "hmip-config" @@ -42,6 +44,7 @@ SERVICE_DEACTIVATE_VACATION = "deactivate_vacation" SERVICE_DUMP_HAP_CONFIG = "dump_hap_config" SERVICE_RESET_ENERGY_COUNTER = "reset_energy_counter" SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile" +SERVICE_SET_HOME_COOLING_MODE = "set_home_cooling_mode" HMIPC_SERVICES = [ SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION, @@ -52,6 +55,7 @@ HMIPC_SERVICES = [ SERVICE_DUMP_HAP_CONFIG, SERVICE_RESET_ENERGY_COUNTER, SERVICE_SET_ACTIVE_CLIMATE_PROFILE, + SERVICE_SET_HOME_COOLING_MODE, ] SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION = vol.Schema( @@ -107,6 +111,13 @@ SCHEMA_RESET_ENERGY_COUNTER = vol.Schema( {vol.Required(ATTR_ENTITY_ID): comp_entity_ids} ) +SCHEMA_SET_HOME_COOLING_MODE = vol.Schema( + { + vol.Optional(ATTR_COOLING, default=True): cv.boolean, + vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), + } +) + async def async_setup_services(hass: HomeAssistant) -> None: """Set up the HomematicIP Cloud services.""" @@ -135,6 +146,8 @@ async def async_setup_services(hass: HomeAssistant) -> None: await _async_reset_energy_counter(hass, service) elif service_name == SERVICE_SET_ACTIVE_CLIMATE_PROFILE: await _set_active_climate_profile(hass, service) + elif service_name == SERVICE_SET_HOME_COOLING_MODE: + await _async_set_home_cooling_mode(hass, service) hass.services.async_register( domain=HMIPC_DOMAIN, @@ -194,6 +207,14 @@ async def async_setup_services(hass: HomeAssistant) -> None: schema=SCHEMA_RESET_ENERGY_COUNTER, ) + async_register_admin_service( + hass=hass, + domain=HMIPC_DOMAIN, + service=SERVICE_SET_HOME_COOLING_MODE, + service_func=async_call_hmipc_service, + schema=SCHEMA_SET_HOME_COOLING_MODE, + ) + async def async_unload_services(hass: HomeAssistant): """Unload HomematicIP Cloud services.""" @@ -324,10 +345,25 @@ async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCall) await device.reset_energy_counter() +async def _async_set_home_cooling_mode(hass: HomeAssistant, service: ServiceCall): + """Service to set the cooling mode.""" + cooling = service.data[ATTR_COOLING] + + if hapid := service.data.get(ATTR_ACCESSPOINT_ID): + if home := _get_home(hass, hapid): + await home.set_cooling(cooling) + else: + for hap in hass.data[HMIPC_DOMAIN].values(): + await hap.home.set_cooling(cooling) + + def _get_home(hass: HomeAssistant, hapid: str) -> AsyncHome | None: """Return a HmIP home.""" if hap := hass.data[HMIPC_DOMAIN].get(hapid): return hap.home - _LOGGER.info("No matching access point found for access point id %s", hapid) - return None + raise ServiceValidationError( + translation_domain=HMIPC_DOMAIN, + translation_key="access_point_not_found", + translation_placeholders={"id": hapid}, + ) diff --git a/homeassistant/components/homematicip_cloud/services.yaml b/homeassistant/components/homematicip_cloud/services.yaml index 9e831339787..aced5c838a6 100644 --- a/homeassistant/components/homematicip_cloud/services.yaml +++ b/homeassistant/components/homematicip_cloud/services.yaml @@ -98,3 +98,14 @@ reset_energy_counter: example: switch.livingroom selector: text: + +set_home_cooling_mode: + fields: + cooling: + default: true + selector: + boolean: + accesspoint_id: + example: 3014xxxxxxxxxxxxxxxxxxxx + selector: + text: diff --git a/homeassistant/components/homematicip_cloud/strings.json b/homeassistant/components/homematicip_cloud/strings.json index 3795508d75d..a7c795c81f6 100644 --- a/homeassistant/components/homematicip_cloud/strings.json +++ b/homeassistant/components/homematicip_cloud/strings.json @@ -26,6 +26,11 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, + "exceptions": { + "access_point_not_found": { + "message": "No matching access point found for access point id {id}" + } + }, "services": { "activate_eco_mode_with_duration": { "name": "Activate eco mode with duration", @@ -134,6 +139,20 @@ "description": "The ID of the measuring entity. Use 'all' keyword to reset all energy counters." } } + }, + "set_home_cooling_mode": { + "name": "Set home cooling mode", + "description": "Set the heating/cooling mode for the entire home", + "fields": { + "accesspoint_id": { + "name": "[%key:component::homematicip_cloud::services::activate_eco_mode_with_duration::fields::accesspoint_id::name%]", + "description": "[%key:component::homematicip_cloud::services::activate_eco_mode_with_duration::fields::accesspoint_id::description%]" + }, + "cooling": { + "name": "Cooling", + "description": "Enable for cooling mode, disable for heating mode" + } + } } } } diff --git a/requirements_all.txt b/requirements_all.txt index d0bd63bd826..9898da0e19e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1105,7 +1105,7 @@ home-assistant-intents==2024.8.7 homeconnect==0.8.0 # homeassistant.components.homematicip_cloud -homematicip==1.1.1 +homematicip==1.1.2 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6c15fc8942..572e70bc2b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -931,7 +931,7 @@ home-assistant-intents==2024.8.7 homeconnect==0.8.0 # homeassistant.components.homematicip_cloud -homematicip==1.1.1 +homematicip==1.1.2 # homeassistant.components.remember_the_milk httplib2==0.20.4 diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index f175e2060df..2b4d023baf8 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -622,18 +622,67 @@ async def test_hmip_climate_services( assert len(home._connection.mock_calls) == 10 not_existing_hap_id = "5555F7110000000000000001" - await hass.services.async_call( - "homematicip_cloud", - "deactivate_vacation", - {"accesspoint_id": not_existing_hap_id}, - blocking=True, - ) - assert home.mock_calls[-1][0] == "deactivate_vacation" - assert home.mock_calls[-1][1] == () + with pytest.raises(ServiceValidationError) as excinfo: + await hass.services.async_call( + "homematicip_cloud", + "deactivate_vacation", + {"accesspoint_id": not_existing_hap_id}, + blocking=True, + ) + assert excinfo.value.translation_domain == HMIPC_DOMAIN + assert excinfo.value.translation_key == "access_point_not_found" # There is no further call on connection. assert len(home._connection.mock_calls) == 10 +async def test_hmip_set_home_cooling_mode( + hass: HomeAssistant, mock_hap_with_service +) -> None: + """Test HomematicipSetHomeCoolingMode.""" + + home = mock_hap_with_service.home + + await hass.services.async_call( + "homematicip_cloud", + "set_home_cooling_mode", + {"accesspoint_id": HAPID, "cooling": False}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "set_cooling" + assert home.mock_calls[-1][1] == (False,) + assert len(home._connection.mock_calls) == 1 + + await hass.services.async_call( + "homematicip_cloud", + "set_home_cooling_mode", + {"accesspoint_id": HAPID, "cooling": True}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "set_cooling" + assert home.mock_calls[-1][1] + assert len(home._connection.mock_calls) == 2 + + await hass.services.async_call( + "homematicip_cloud", "set_home_cooling_mode", blocking=True + ) + assert home.mock_calls[-1][0] == "set_cooling" + assert home.mock_calls[-1][1] + assert len(home._connection.mock_calls) == 3 + + not_existing_hap_id = "5555F7110000000000000001" + with pytest.raises(ServiceValidationError) as excinfo: + await hass.services.async_call( + "homematicip_cloud", + "set_home_cooling_mode", + {"accesspoint_id": not_existing_hap_id, "cooling": True}, + blocking=True, + ) + assert excinfo.value.translation_domain == HMIPC_DOMAIN + assert excinfo.value.translation_key == "access_point_not_found" + # There is no further call on connection. + assert len(home._connection.mock_calls) == 3 + + async def test_hmip_heating_group_services( hass: HomeAssistant, default_mock_hap_factory ) -> None: diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 9303a755e89..ad1c8140aea 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -199,7 +199,7 @@ async def test_setup_services_and_unload_services(hass: HomeAssistant) -> None: # Check services are created hmipc_services = hass.services.async_services()[HMIPC_DOMAIN] - assert len(hmipc_services) == 8 + assert len(hmipc_services) == 9 config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) assert len(config_entries) == 1 @@ -232,7 +232,7 @@ async def test_setup_two_haps_unload_one_by_one(hass: HomeAssistant) -> None: assert await async_setup_component(hass, HMIPC_DOMAIN, {}) hmipc_services = hass.services.async_services()[HMIPC_DOMAIN] - assert len(hmipc_services) == 8 + assert len(hmipc_services) == 9 config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) assert len(config_entries) == 2 @@ -241,7 +241,7 @@ async def test_setup_two_haps_unload_one_by_one(hass: HomeAssistant) -> None: # services still exists hmipc_services = hass.services.async_services()[HMIPC_DOMAIN] - assert len(hmipc_services) == 8 + assert len(hmipc_services) == 9 # unload the second AP await hass.config_entries.async_unload(config_entries[1].entry_id) From 840d9a0923c086bc704c8af654761a8a030656bb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 14:30:35 +0200 Subject: [PATCH 2165/2411] Remove unnecessary assignment of Template.hass from arest (#123662) --- homeassistant/components/arest/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index ab502fa275a..8c68c13018b 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -87,8 +87,6 @@ def setup_platform( if value_template is None: return lambda value: value - value_template.hass = hass - def _render(value): try: return value_template.async_render({"value": value}, parse_result=False) From ecc308c32622098d35ef420b9e35c001c277f67e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 14:30:48 +0200 Subject: [PATCH 2166/2411] Remove unnecessary assignment of Template.hass from command_line (#123664) --- homeassistant/components/command_line/binary_sensor.py | 4 +--- homeassistant/components/command_line/cover.py | 5 +---- homeassistant/components/command_line/sensor.py | 4 +--- homeassistant/components/command_line/switch.py | 5 +---- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index 2ff17e86efd..20deddcf14e 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -51,9 +51,7 @@ async def async_setup_platform( scan_interval: timedelta = binary_sensor_config.get( CONF_SCAN_INTERVAL, SCAN_INTERVAL ) - - if value_template := binary_sensor_config.get(CONF_VALUE_TEMPLATE): - value_template.hass = hass + value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE) data = CommandSensorData(hass, command, command_timeout) diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 2c6ec78b689..344fddabc3b 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -45,9 +45,6 @@ async def async_setup_platform( } for device_name, cover_config in entities.items(): - if value_template := cover_config.get(CONF_VALUE_TEMPLATE): - value_template.hass = hass - trigger_entity_config = { CONF_NAME: Template(cover_config.get(CONF_NAME, device_name), hass), **{k: v for k, v in cover_config.items() if k in TRIGGER_ENTITY_OPTIONS}, @@ -60,7 +57,7 @@ async def async_setup_platform( cover_config[CONF_COMMAND_CLOSE], cover_config[CONF_COMMAND_STOP], cover_config.get(CONF_COMMAND_STATE), - value_template, + cover_config.get(CONF_VALUE_TEMPLATE), cover_config[CONF_COMMAND_TIMEOUT], cover_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), ) diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 14edbb55ed0..786afc8f3a7 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -57,11 +57,9 @@ async def async_setup_platform( json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES) json_attributes_path: str | None = sensor_config.get(CONF_JSON_ATTRIBUTES_PATH) scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE) data = CommandSensorData(hass, command, command_timeout) - if value_template := sensor_config.get(CONF_VALUE_TEMPLATE): - value_template.hass = hass - trigger_entity_config = { CONF_NAME: Template(sensor_config[CONF_NAME], hass), **{k: v for k, v in sensor_config.items() if k in TRIGGER_ENTITY_OPTIONS}, diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index a3c3d0b3e9c..b02fb6dcd4a 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -46,9 +46,6 @@ async def async_setup_platform( } for object_id, switch_config in entities.items(): - if value_template := switch_config.get(CONF_VALUE_TEMPLATE): - value_template.hass = hass - trigger_entity_config = { CONF_NAME: Template(switch_config.get(CONF_NAME, object_id), hass), **{k: v for k, v in switch_config.items() if k in TRIGGER_ENTITY_OPTIONS}, @@ -61,7 +58,7 @@ async def async_setup_platform( switch_config[CONF_COMMAND_ON], switch_config[CONF_COMMAND_OFF], switch_config.get(CONF_COMMAND_STATE), - value_template, + switch_config.get(CONF_VALUE_TEMPLATE), switch_config[CONF_COMMAND_TIMEOUT], switch_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), ) From 32d2218ff0d6eaa7a7cd4eedbf0bc8723379da0f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 14:31:11 +0200 Subject: [PATCH 2167/2411] Remove unnecessary assignment of Template.hass from doods (#123666) --- homeassistant/components/doods/image_processing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 7ffb6655bb6..acd9d7fe71b 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -207,8 +207,6 @@ class Doods(ImageProcessingEntity): ] self._covers = area_config[CONF_COVERS] - template.attach(hass, self._file_out) - self._dconfig = dconfig self._matches = {} self._total_matches = 0 From 64ceb11f8c7f623bce388fb6754f4fec7f12c598 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 12 Aug 2024 14:44:52 +0200 Subject: [PATCH 2168/2411] Remove libcst constraint (#123661) --- homeassistant/package_constraints.txt | 5 ----- script/gen_requirements_all.py | 5 ----- 2 files changed, 10 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e3f80f1b82d..6c93e6b5f1c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -79,11 +79,6 @@ grpcio==1.59.0 grpcio-status==1.59.0 grpcio-reflection==1.59.0 -# libcst >=0.4.0 requires a newer Rust than we currently have available, -# thus our wheels builds fail. This pins it to the last working version, -# which at this point satisfies our needs. -libcst==0.3.23 - # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index aa92cacb237..f9bd379b7ce 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -101,11 +101,6 @@ grpcio==1.59.0 grpcio-status==1.59.0 grpcio-reflection==1.59.0 -# libcst >=0.4.0 requires a newer Rust than we currently have available, -# thus our wheels builds fail. This pins it to the last working version, -# which at this point satisfies our needs. -libcst==0.3.23 - # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 From 27d76f59531b8734378cf4d22ce2bc78217ed0a0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 14:52:34 +0200 Subject: [PATCH 2169/2411] Remove unnecessary assignment of Template.hass from history_stats (#123671) --- homeassistant/components/history_stats/sensor.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 99e953ff9dd..4558da8722c 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -103,10 +103,6 @@ async def async_setup_platform( name: str = config[CONF_NAME] unique_id: str | None = config.get(CONF_UNIQUE_ID) - for template in (start, end): - if template is not None: - template.hass = hass - history_stats = HistoryStats(hass, entity_id, entity_states, start, end, duration) coordinator = HistoryStatsUpdateCoordinator(hass, history_stats, name) await coordinator.async_refresh() From b9010e96a0ca41285002d7e22666dc00d1270b15 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 14:52:51 +0200 Subject: [PATCH 2170/2411] Remove unnecessary assignment of Template.hass from emoncms (#123668) --- homeassistant/components/emoncms/sensor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index c299c5a1b9f..3c448391974 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -87,9 +87,6 @@ async def async_setup_platform( sensor_names = config.get(CONF_SENSOR_NAMES) scan_interval = config.get(CONF_SCAN_INTERVAL, timedelta(seconds=30)) - if value_template is not None: - value_template.hass = hass - emoncms_client = EmoncmsClient(url, apikey, session=async_get_clientsession(hass)) coordinator = EmoncmsCoordinator(hass, emoncms_client, scan_interval) await coordinator.async_refresh() From 77e9acd864c1f9b9315dd0e5768205d258ca0dbf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 14:53:22 +0200 Subject: [PATCH 2171/2411] Remove unnecessary assignment of Template.hass from emulated_kasa (#123670) --- homeassistant/components/emulated_kasa/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/emulated_kasa/__init__.py b/homeassistant/components/emulated_kasa/__init__.py index d5fc8af1aa4..408d8c4eff8 100644 --- a/homeassistant/components/emulated_kasa/__init__.py +++ b/homeassistant/components/emulated_kasa/__init__.py @@ -95,8 +95,6 @@ async def validate_configs(hass, entity_configs): power_val = entity_config[CONF_POWER] if isinstance(power_val, str) and is_template_string(power_val): entity_config[CONF_POWER] = Template(power_val, hass) - elif isinstance(power_val, Template): - entity_config[CONF_POWER].hass = hass elif CONF_POWER_ENTITY in entity_config: power_val = entity_config[CONF_POWER_ENTITY] if hass.states.get(power_val) is None: From 33a22ae2080fbfa0a3363585bc665f8f8c57cdc2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 14:54:03 +0200 Subject: [PATCH 2172/2411] Remove unnecessary assignment of Template.hass from triggers (#123672) --- homeassistant/components/homeassistant/triggers/event.py | 3 --- .../components/homeassistant/triggers/numeric_state.py | 4 ---- homeassistant/components/homeassistant/triggers/state.py | 1 - homeassistant/components/hp_ilo/sensor.py | 3 --- 4 files changed, 11 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 98363de1f8d..985e4819b24 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -60,7 +60,6 @@ async def async_attach_trigger( trigger_data = trigger_info["trigger_data"] variables = trigger_info["variables"] - template.attach(hass, config[CONF_EVENT_TYPE]) event_types = template.render_complex( config[CONF_EVENT_TYPE], variables, limited=True ) @@ -72,7 +71,6 @@ async def async_attach_trigger( event_data_items: ItemsView | None = None if CONF_EVENT_DATA in config: # Render the schema input - template.attach(hass, config[CONF_EVENT_DATA]) event_data = {} event_data.update( template.render_complex(config[CONF_EVENT_DATA], variables, limited=True) @@ -94,7 +92,6 @@ async def async_attach_trigger( event_context_items: ItemsView | None = None if CONF_EVENT_CONTEXT in config: # Render the schema input - template.attach(hass, config[CONF_EVENT_CONTEXT]) event_context = {} event_context.update( template.render_complex(config[CONF_EVENT_CONTEXT], variables, limited=True) diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index bc2c95675ad..dac250792ea 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -108,7 +108,6 @@ async def async_attach_trigger( below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) time_delta = config.get(CONF_FOR) - template.attach(hass, time_delta) value_template = config.get(CONF_VALUE_TEMPLATE) unsub_track_same: dict[str, Callable[[], None]] = {} armed_entities: set[str] = set() @@ -119,9 +118,6 @@ async def async_attach_trigger( trigger_data = trigger_info["trigger_data"] _variables = trigger_info["variables"] or {} - if value_template is not None: - value_template.hass = hass - def variables(entity_id: str) -> dict[str, Any]: """Return a dict with trigger variables.""" trigger_info = { diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index e0cbbf09610..53372cb479e 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -117,7 +117,6 @@ async def async_attach_trigger( match_to_state = process_state_match(MATCH_ALL) time_delta = config.get(CONF_FOR) - template.attach(hass, time_delta) # If neither CONF_FROM or CONF_TO are specified, # fire on all changes to the state or an attribute match_all = all( diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index 85908a45af4..0eeb443cf2d 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -131,9 +131,6 @@ class HpIloSensor(SensorEntity): self._unit_of_measurement = unit_of_measurement self._ilo_function = SENSOR_TYPES[sensor_type][1] self.hp_ilo_data = hp_ilo_data - - if sensor_value_template is not None: - sensor_value_template.hass = hass self._sensor_value_template = sensor_value_template self._state = None From 82bedb1ab5ef85aed935313eb3c6bb09322376c0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 14:54:31 +0200 Subject: [PATCH 2173/2411] Remove unnecessary assignment of Template.hass from dweet (#123667) --- homeassistant/components/dweet/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py index 01e0567ac8d..10109189eb0 100644 --- a/homeassistant/components/dweet/sensor.py +++ b/homeassistant/components/dweet/sensor.py @@ -51,8 +51,6 @@ def setup_platform( device = config.get(CONF_DEVICE) value_template = config.get(CONF_VALUE_TEMPLATE) unit = config.get(CONF_UNIT_OF_MEASUREMENT) - if value_template is not None: - value_template.hass = hass try: content = json.dumps(dweepy.get_latest_dweet_for(device)[0]["content"]) @@ -60,7 +58,7 @@ def setup_platform( _LOGGER.error("Device/thing %s could not be found", device) return - if value_template.render_with_possible_json_value(content) == "": + if value_template and value_template.render_with_possible_json_value(content) == "": _LOGGER.error("%s was not found", value_template) return From f6e82ae0ba18c9cc561df45b988305f95d1a93ca Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 14:54:41 +0200 Subject: [PATCH 2174/2411] Remove unnecessary assignment of Template.hass from camera (#123663) --- homeassistant/components/camera/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index cbcf08cb7c2..ff11bc04da7 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -992,7 +992,6 @@ async def async_handle_snapshot_service( """Handle snapshot services calls.""" hass = camera.hass filename: Template = service_call.data[ATTR_FILENAME] - filename.hass = hass snapshot_file = filename.async_render(variables={ATTR_ENTITY_ID: camera}) @@ -1069,9 +1068,7 @@ async def async_handle_record_service( if not stream: raise HomeAssistantError(f"{camera.entity_id} does not support record service") - hass = camera.hass filename = service_call.data[CONF_FILENAME] - filename.hass = hass video_path = filename.async_render(variables={ATTR_ENTITY_ID: camera}) await stream.async_record( From b20623447e069979168af86f008c8a5cab0980bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Aug 2024 07:54:57 -0500 Subject: [PATCH 2175/2411] Ensure HomeKit connection is kept alive for devices that timeout too quickly (#123601) --- .../homekit_controller/connection.py | 36 ++++++++++++++----- .../homekit_controller/test_connection.py | 15 ++++---- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 0d21ff9ba1d..4da907daf3e 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -845,21 +845,41 @@ class HKDevice: async def async_update(self, now: datetime | None = None) -> None: """Poll state of all entities attached to this bridge/accessory.""" + to_poll = self.pollable_characteristics + accessories = self.entity_map.accessories + if ( - len(self.entity_map.accessories) == 1 + len(accessories) == 1 and self.available - and not (self.pollable_characteristics - self.watchable_characteristics) + and not (to_poll - self.watchable_characteristics) and self.pairing.is_available and await self.pairing.controller.async_reachable( self.unique_id, timeout=5.0 ) ): # If its a single accessory and all chars are watchable, - # we don't need to poll. - _LOGGER.debug("Accessory is reachable, skip polling: %s", self.unique_id) - return + # only poll the firmware version to keep the connection alive + # https://github.com/home-assistant/core/issues/123412 + # + # Firmware revision is used here since iOS does this to keep camera + # connections alive, and the goal is to not regress + # https://github.com/home-assistant/core/issues/116143 + # by polling characteristics that are not normally polled frequently + # and may not be tested by the device vendor. + # + _LOGGER.debug( + "Accessory is reachable, limiting poll to firmware version: %s", + self.unique_id, + ) + first_accessory = accessories[0] + accessory_info = first_accessory.services.first( + service_type=ServicesTypes.ACCESSORY_INFORMATION + ) + assert accessory_info is not None + firmware_iid = accessory_info[CharacteristicsTypes.FIRMWARE_REVISION].iid + to_poll = {(first_accessory.aid, firmware_iid)} - if not self.pollable_characteristics: + if not to_poll: self.async_update_available_state() _LOGGER.debug( "HomeKit connection not polling any characteristics: %s", self.unique_id @@ -892,9 +912,7 @@ class HKDevice: _LOGGER.debug("Starting HomeKit device update: %s", self.unique_id) try: - new_values_dict = await self.get_characteristics( - self.pollable_characteristics - ) + new_values_dict = await self.get_characteristics(to_poll) except AccessoryNotFoundError: # Not only did the connection fail, but also the accessory is not # visible on the network. diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index 60ef0b1c547..8d3cc02fab9 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -344,10 +344,10 @@ async def test_thread_provision_migration_failed(hass: HomeAssistant) -> None: assert config_entry.data["Connection"] == "BLE" -async def test_skip_polling_all_watchable_accessory_mode( +async def test_poll_firmware_version_only_all_watchable_accessory_mode( hass: HomeAssistant, get_next_aid: Callable[[], int] ) -> None: - """Test that we skip polling if available and all chars are watchable accessory mode.""" + """Test that we only poll firmware if available and all chars are watchable accessory mode.""" def _create_accessory(accessory): service = accessory.add_service(ServicesTypes.LIGHTBULB, name="TestDevice") @@ -370,7 +370,10 @@ async def test_skip_polling_all_watchable_accessory_mode( # Initial state is that the light is off state = await helper.poll_and_get_state() assert state.state == STATE_OFF - assert mock_get_characteristics.call_count == 0 + assert mock_get_characteristics.call_count == 2 + # Verify only firmware version is polled + assert mock_get_characteristics.call_args_list[0][0][0] == {(1, 7)} + assert mock_get_characteristics.call_args_list[1][0][0] == {(1, 7)} # Test device goes offline helper.pairing.available = False @@ -382,16 +385,16 @@ async def test_skip_polling_all_watchable_accessory_mode( state = await helper.poll_and_get_state() assert state.state == STATE_UNAVAILABLE # Tries twice before declaring unavailable - assert mock_get_characteristics.call_count == 2 + assert mock_get_characteristics.call_count == 4 # Test device comes back online helper.pairing.available = True state = await helper.poll_and_get_state() assert state.state == STATE_OFF - assert mock_get_characteristics.call_count == 3 + assert mock_get_characteristics.call_count == 6 # Next poll should not happen because its a single # accessory, available, and all chars are watchable state = await helper.poll_and_get_state() assert state.state == STATE_OFF - assert mock_get_characteristics.call_count == 3 + assert mock_get_characteristics.call_count == 8 From 5b5b9ac4ef49c3d118ca46c74e8cc2a4c761bc49 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 14:56:29 +0200 Subject: [PATCH 2176/2411] Remove unnecessary assignment of Template.hass from influxdb (#123673) --- homeassistant/components/influxdb/sensor.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index 03b6acb204c..a1a9e618cb8 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -205,28 +205,24 @@ class InfluxSensor(SensorEntity): self._attr_unique_id = query.get(CONF_UNIQUE_ID) if query[CONF_LANGUAGE] == LANGUAGE_FLUX: - query_clause = query.get(CONF_QUERY) - query_clause.hass = hass self.data = InfluxFluxSensorData( influx, query.get(CONF_BUCKET), query.get(CONF_RANGE_START), query.get(CONF_RANGE_STOP), - query_clause, + query.get(CONF_QUERY), query.get(CONF_IMPORTS), query.get(CONF_GROUP_FUNCTION), ) else: - where_clause = query.get(CONF_WHERE) - where_clause.hass = hass self.data = InfluxQLSensorData( influx, query.get(CONF_DB_NAME), query.get(CONF_GROUP_FUNCTION), query.get(CONF_FIELD), query.get(CONF_MEASUREMENT_NAME), - where_clause, + query.get(CONF_WHERE), ) @property From 11fd1086afd797286bfe9c26a28250a43f99374e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 15:39:18 +0200 Subject: [PATCH 2177/2411] Remove unnecessary assignment of Template.hass from logbook (#123677) --- homeassistant/components/logbook/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index d520cafb80e..239a52ff7a1 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -112,7 +112,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # away so we use the "logbook" domain domain = DOMAIN - message.hass = hass message = message.async_render(parse_result=False) async_log_entry(hass, name, message, domain, entity_id, service.context) From 78b6cdb2012e91dcb5580588140cbac3f9ac8319 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 15:39:34 +0200 Subject: [PATCH 2178/2411] Remove unnecessary assignment of Template.hass from logi_circle (#123678) --- homeassistant/components/logi_circle/camera.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index ad31713d734..04f12586679 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -166,7 +166,6 @@ class LogiCam(Camera): async def download_livestream(self, filename, duration): """Download a recording from the camera's livestream.""" # Render filename from template. - filename.hass = self.hass stream_file = filename.async_render(variables={ATTR_ENTITY_ID: self.entity_id}) # Respect configured allowed paths. @@ -183,7 +182,6 @@ class LogiCam(Camera): async def livestream_snapshot(self, filename): """Download a still frame from the camera's livestream.""" # Render filename from template. - filename.hass = self.hass snapshot_file = filename.async_render( variables={ATTR_ENTITY_ID: self.entity_id} ) From 4639e7d5c7693a3a48b8f784bb2ba4acac9ad716 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 15:49:37 +0200 Subject: [PATCH 2179/2411] Remove unnecessary assignment of Template.hass from tcp (#123691) --- homeassistant/components/tcp/common.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/tcp/common.py b/homeassistant/components/tcp/common.py index d6a7fb28f11..263fc416026 100644 --- a/homeassistant/components/tcp/common.py +++ b/homeassistant/components/tcp/common.py @@ -25,7 +25,6 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType from .const import ( @@ -63,10 +62,6 @@ class TcpEntity(Entity): def __init__(self, hass: HomeAssistant, config: ConfigType) -> None: """Set all the config values if they exist and get initial state.""" - value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - value_template.hass = hass - self._hass = hass self._config: TcpSensorConfig = { CONF_NAME: config[CONF_NAME], @@ -75,7 +70,7 @@ class TcpEntity(Entity): CONF_TIMEOUT: config[CONF_TIMEOUT], CONF_PAYLOAD: config[CONF_PAYLOAD], CONF_UNIT_OF_MEASUREMENT: config.get(CONF_UNIT_OF_MEASUREMENT), - CONF_VALUE_TEMPLATE: value_template, + CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE), CONF_VALUE_ON: config.get(CONF_VALUE_ON), CONF_BUFFER_SIZE: config[CONF_BUFFER_SIZE], CONF_SSL: config[CONF_SSL], From 533e383d5de6318c8a991be76020618cbe8b5c31 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 15:51:02 +0200 Subject: [PATCH 2180/2411] Remove unnecessary assignment of Template.hass from sql (#123690) --- homeassistant/components/sql/sensor.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index f09f7ae95cf..1d033728c0d 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -81,9 +81,6 @@ async def async_setup_platform( unique_id: str | None = conf.get(CONF_UNIQUE_ID) db_url: str = resolve_db_url(hass, conf.get(CONF_DB_URL)) - if value_template is not None: - value_template.hass = hass - trigger_entity_config = {CONF_NAME: name} for key in TRIGGER_ENTITY_OPTIONS: if key not in conf: @@ -117,12 +114,10 @@ async def async_setup_entry( value_template: Template | None = None if template is not None: try: - value_template = Template(template) + value_template = Template(template, hass) value_template.ensure_valid() except TemplateError: value_template = None - if value_template is not None: - value_template.hass = hass name_template = Template(name, hass) trigger_entity_config = {CONF_NAME: name_template, CONF_UNIQUE_ID: entry.entry_id} From efa3f228a55a146522ccdd01838e2d580ffb003c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 15:51:26 +0200 Subject: [PATCH 2181/2411] Remove unnecessary assignment of Template.hass from slack (#123688) --- homeassistant/components/slack/notify.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index a18b211962a..28f9dd203ff 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -291,7 +291,6 @@ class SlackNotificationService(BaseNotificationService): if ATTR_FILE not in data: if ATTR_BLOCKS_TEMPLATE in data: value = cv.template_complex(data[ATTR_BLOCKS_TEMPLATE]) - template.attach(self._hass, value) blocks = template.render_complex(value) elif ATTR_BLOCKS in data: blocks = data[ATTR_BLOCKS] From 1c9a1c71d3dd41729312a68b9beb4429fd77b3bf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 15:51:37 +0200 Subject: [PATCH 2182/2411] Remove unnecessary assignment of Template.hass from scrape (#123685) --- homeassistant/components/scrape/sensor.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index ceaf1e63a9d..dd84767ad41 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -67,10 +67,6 @@ async def async_setup_platform( entities: list[ScrapeSensor] = [] for sensor_config in sensors_config: - value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - value_template.hass = hass - trigger_entity_config = {CONF_NAME: sensor_config[CONF_NAME]} for key in TRIGGER_ENTITY_OPTIONS: if key not in sensor_config: @@ -85,7 +81,7 @@ async def async_setup_platform( sensor_config[CONF_SELECT], sensor_config.get(CONF_ATTRIBUTE), sensor_config[CONF_INDEX], - value_template, + sensor_config.get(CONF_VALUE_TEMPLATE), True, ) ) From ecd061d46fc27f521e5e8320e40d629764a0ebe8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 15:52:03 +0200 Subject: [PATCH 2183/2411] Remove unnecessary assignment of Template.hass from rest (#123682) --- homeassistant/components/rest/__init__.py | 5 ----- homeassistant/components/rest/binary_sensor.py | 2 -- homeassistant/components/rest/notify.py | 1 - homeassistant/components/rest/sensor.py | 2 -- homeassistant/components/rest/switch.py | 5 ----- homeassistant/components/rest_command/__init__.py | 4 ---- 6 files changed, 19 deletions(-) diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index b7cdee2e039..59239ad6744 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -202,19 +202,14 @@ def create_rest_data_from_config(hass: HomeAssistant, config: ConfigType) -> Res timeout: int = config[CONF_TIMEOUT] encoding: str = config[CONF_ENCODING] if resource_template is not None: - resource_template.hass = hass resource = resource_template.async_render(parse_result=False) if payload_template is not None: - payload_template.hass = hass payload = payload_template.async_render(parse_result=False) if not resource: raise HomeAssistantError("Resource not set for RestData") - template.attach(hass, headers) - template.attach(hass, params) - auth: httpx.DigestAuth | tuple[str, str] | None = None if username and password: if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index e8119a40f8c..c976506d1ba 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -133,8 +133,6 @@ class RestBinarySensor(ManualTriggerEntity, RestEntity, BinarySensorEntity): ) self._previous_data = None self._value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) - if (value_template := self._value_template) is not None: - value_template.hass = hass @property def available(self) -> bool: diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index c8314d18707..1ca3c55e2b2 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -172,7 +172,6 @@ class RestNotificationService(BaseNotificationService): } if not isinstance(value, Template): return value - value.hass = self._hass return value.async_render(kwargs, parse_result=False) if self._data: diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index d7bb0ea33fb..fc6ce8c6749 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -139,8 +139,6 @@ class RestSensor(ManualTriggerSensorEntity, RestEntity): config[CONF_FORCE_UPDATE], ) self._value_template = config.get(CONF_VALUE_TEMPLATE) - if (value_template := self._value_template) is not None: - value_template.hass = hass self._json_attrs = config.get(CONF_JSON_ATTRS) self._json_attrs_path = config.get(CONF_JSON_ATTRS_PATH) self._attr_extra_state_attributes = {} diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index d01aab2cf9f..219084ea683 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -153,11 +153,6 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity): self._body_on.hass = hass self._body_off.hass = hass - if (is_on_template := self._is_on_template) is not None: - is_on_template.hass = hass - - template.attach(hass, self._headers) - template.attach(hass, self._params) async def async_added_to_hass(self) -> None: """Handle adding to Home Assistant.""" diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index b6945c5ce98..ee93fde35fa 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -96,7 +96,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: method = command_config[CONF_METHOD] template_url = command_config[CONF_URL] - template_url.hass = hass auth = None if CONF_USERNAME in command_config: @@ -107,11 +106,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: template_payload = None if CONF_PAYLOAD in command_config: template_payload = command_config[CONF_PAYLOAD] - template_payload.hass = hass template_headers = command_config.get(CONF_HEADERS, {}) - for template_header in template_headers.values(): - template_header.hass = hass content_type = command_config.get(CONF_CONTENT_TYPE) From b04e3dc6fd6cb266dd58ca9746c762bb9c98f9cb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 15:52:41 +0200 Subject: [PATCH 2184/2411] Remove unnecessary assignment of Template.hass from serial (#123686) --- homeassistant/components/serial/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index e3fee36c09e..e7c39d97f6a 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -93,9 +93,7 @@ async def async_setup_platform( xonxoff = config.get(CONF_XONXOFF) rtscts = config.get(CONF_RTSCTS) dsrdtr = config.get(CONF_DSRDTR) - - if (value_template := config.get(CONF_VALUE_TEMPLATE)) is not None: - value_template.hass = hass + value_template = config.get(CONF_VALUE_TEMPLATE) sensor = SerialSensor( name, From 268044cd015207d370ec8bce31a9a266d5590a89 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 15:53:19 +0200 Subject: [PATCH 2185/2411] Remove unnecessary assignment of Template.hass from notify (#123680) --- homeassistant/components/notify/__init__.py | 2 -- homeassistant/components/notify/legacy.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 1fc7836ecd8..31c7b8e4d70 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -91,14 +91,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def persistent_notification(service: ServiceCall) -> None: """Send notification via the built-in persistent_notify integration.""" message: Template = service.data[ATTR_MESSAGE] - message.hass = hass check_templates_warn(hass, message) title = None title_tpl: Template | None if title_tpl := service.data.get(ATTR_TITLE): check_templates_warn(hass, title_tpl) - title_tpl.hass = hass title = title_tpl.async_render(parse_result=False) notification_id = None diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index 81444a36296..dcb148a99f5 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -259,7 +259,6 @@ class BaseNotificationService: title: Template | None if title := service.data.get(ATTR_TITLE): check_templates_warn(self.hass, title) - title.hass = self.hass kwargs[ATTR_TITLE] = title.async_render(parse_result=False) if self.registered_targets.get(service.service) is not None: @@ -268,7 +267,6 @@ class BaseNotificationService: kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET) check_templates_warn(self.hass, message) - message.hass = self.hass kwargs[ATTR_MESSAGE] = message.async_render(parse_result=False) kwargs[ATTR_DATA] = service.data.get(ATTR_DATA) From 81788790dfcd6b058b5b145cfc89366186f5210f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 15:53:32 +0200 Subject: [PATCH 2186/2411] Remove unnecessary assignment of Template.hass from rss_feed_template (#123683) --- .../components/rss_feed_template/__init__.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/homeassistant/components/rss_feed_template/__init__.py b/homeassistant/components/rss_feed_template/__init__.py index debff5a6e96..89624c922e6 100644 --- a/homeassistant/components/rss_feed_template/__init__.py +++ b/homeassistant/components/rss_feed_template/__init__.py @@ -49,18 +49,8 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: requires_auth: bool = feedconfig["requires_api_password"] - title: Template | None - if (title := feedconfig.get("title")) is not None: - title.hass = hass - items: list[dict[str, Template]] = feedconfig["items"] - for item in items: - if "title" in item: - item["title"].hass = hass - if "description" in item: - item["description"].hass = hass - - rss_view = RssView(url, requires_auth, title, items) + rss_view = RssView(url, requires_auth, feedconfig.get("title"), items) hass.http.register_view(rss_view) return True From 4dadf0ea1b2d4f0561f6743e87e9e6f9d18d5591 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 15:54:06 +0200 Subject: [PATCH 2187/2411] Remove unnecessary assignment of Template.hass from snmp (#123689) --- homeassistant/components/snmp/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index fb7b87403cb..4586d0600e9 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -174,8 +174,6 @@ async def async_setup_platform( trigger_entity_config[key] = config[key] value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - value_template.hass = hass data = SnmpData(request_args, baseoid, accept_errors, default_value) async_add_entities([SnmpSensor(hass, data, trigger_entity_config, value_template)]) From c47fdf70746a8effe205955aa34bb6a6617f123f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 15:54:20 +0200 Subject: [PATCH 2188/2411] Remove unnecessary assignment of Template.hass from intent_script (#123676) --- homeassistant/components/intent_script/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index d6fbb1edd80..371163ce00a 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -90,7 +90,6 @@ async def async_reload(hass: HomeAssistant, service_call: ServiceCall) -> None: def async_load_intents(hass: HomeAssistant, intents: dict[str, ConfigType]) -> None: """Load YAML intents into the intent system.""" - template.attach(hass, intents) hass.data[DOMAIN] = intents for intent_type, conf in intents.items(): From 2d41723cfecccec262edc06ac86bd77033fb3000 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 15:54:35 +0200 Subject: [PATCH 2189/2411] Remove unnecessary assignment of Template.hass from minio (#123679) --- homeassistant/components/minio/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py index e2cbcdf9ed1..e5470cc3313 100644 --- a/homeassistant/components/minio/__init__.py +++ b/homeassistant/components/minio/__init__.py @@ -127,7 +127,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: def _render_service_value(service, key): value = service.data[key] - value.hass = hass return value.async_render(parse_result=False) def put_file(service: ServiceCall) -> None: From 7985974a58d792a7402cf435e4b83f6f129269b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Aug 2024 09:02:23 -0500 Subject: [PATCH 2190/2411] Bump aiohomekit to 3.2.2 (#123669) --- 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 476d17d3515..007153aceaf 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.2.1"], + "requirements": ["aiohomekit==3.2.2"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 9898da0e19e..59fb5d23a1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -255,7 +255,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.2.1 +aiohomekit==3.2.2 # homeassistant.components.hue aiohue==4.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 572e70bc2b8..032a09fb699 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -240,7 +240,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.2.1 +aiohomekit==3.2.2 # homeassistant.components.hue aiohue==4.7.2 From 342ba1b5996b6330744d6bd62a58212cb039f7cc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 16:06:19 +0200 Subject: [PATCH 2191/2411] Remove unnecessary assignment of Template.hass from telegram_bot (#123693) --- homeassistant/components/telegram_bot/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index fed9021a46e..9d1a5398055 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -408,7 +408,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ): data[attribute] = attribute_templ else: - attribute_templ.hass = hass try: data[attribute] = attribute_templ.async_render( parse_result=False From 5cb990113494da4e21276c176592369c31b6e0c4 Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Mon, 12 Aug 2024 16:19:36 +0200 Subject: [PATCH 2192/2411] Cleaner unit tests for Swiss public transport (#123660) cleaner unit tests --- .../swiss_public_transport/test_service.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/components/swiss_public_transport/test_service.py b/tests/components/swiss_public_transport/test_service.py index 34640de9f21..4009327e77d 100644 --- a/tests/components/swiss_public_transport/test_service.py +++ b/tests/components/swiss_public_transport/test_service.py @@ -38,18 +38,18 @@ MOCK_DATA_STEP_BASE = { @pytest.mark.parametrize( - ("limit", "config_data"), + ("data", "config_data"), [ - (1, MOCK_DATA_STEP_BASE), - (2, MOCK_DATA_STEP_BASE), - (3, MOCK_DATA_STEP_BASE), - (CONNECTIONS_MAX, MOCK_DATA_STEP_BASE), - (None, MOCK_DATA_STEP_BASE), + ({ATTR_LIMIT: 1}, MOCK_DATA_STEP_BASE), + ({ATTR_LIMIT: 2}, MOCK_DATA_STEP_BASE), + ({ATTR_LIMIT: 3}, MOCK_DATA_STEP_BASE), + ({ATTR_LIMIT: CONNECTIONS_MAX}, MOCK_DATA_STEP_BASE), + ({}, MOCK_DATA_STEP_BASE), ], ) async def test_service_call_fetch_connections_success( hass: HomeAssistant, - limit: int, + data: dict, config_data, ) -> None: """Test the fetch_connections service.""" @@ -59,7 +59,7 @@ async def test_service_call_fetch_connections_success( config_entry = MockConfigEntry( domain=DOMAIN, data=config_data, - title=f"Service test call with limit={limit}", + title=f"Service test call with data={data}", unique_id=unique_id, entry_id=f"entry_{unique_id}", ) @@ -69,14 +69,12 @@ async def test_service_call_fetch_connections_success( return_value=AsyncMock(), ) as mock: mock().connections = json.loads(load_fixture("connections.json", DOMAIN))[ - 0 : (limit or CONNECTIONS_COUNT) + 2 + 0 : data.get(ATTR_LIMIT, CONNECTIONS_COUNT) + 2 ] await setup_integration(hass, config_entry) - data = {ATTR_CONFIG_ENTRY_ID: config_entry.entry_id} - if limit is not None: - data[ATTR_LIMIT] = limit + data[ATTR_CONFIG_ENTRY_ID] = config_entry.entry_id assert hass.services.has_service(DOMAIN, SERVICE_FETCH_CONNECTIONS) response = await hass.services.async_call( domain=DOMAIN, @@ -87,7 +85,7 @@ async def test_service_call_fetch_connections_success( ) await hass.async_block_till_done() assert response["connections"] is not None - assert len(response["connections"]) == (limit or CONNECTIONS_COUNT) + assert len(response["connections"]) == data.get(ATTR_LIMIT, CONNECTIONS_COUNT) @pytest.mark.parametrize( From 26a69458b0f1cfccdcd382b924c8c29bde92a8f2 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 12 Aug 2024 17:52:37 +0300 Subject: [PATCH 2193/2411] Bump aioswitcher to 4.0.1 (#123697) --- homeassistant/components/switcher_kis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 6aedd2f5670..1ec32a26e4e 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_push", "loggers": ["aioswitcher"], "quality_scale": "platinum", - "requirements": ["aioswitcher==4.0.0"], + "requirements": ["aioswitcher==4.0.1"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 59fb5d23a1e..3e868a419eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -374,7 +374,7 @@ aiosolaredge==0.2.0 aiosteamist==1.0.0 # homeassistant.components.switcher_kis -aioswitcher==4.0.0 +aioswitcher==4.0.1 # homeassistant.components.syncthing aiosyncthing==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 032a09fb699..af61773fa16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ aiosolaredge==0.2.0 aiosteamist==1.0.0 # homeassistant.components.switcher_kis -aioswitcher==4.0.0 +aioswitcher==4.0.1 # homeassistant.components.syncthing aiosyncthing==0.5.1 From 200f04bf21cda2c546ca6efb927e80228af8cba3 Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Mon, 12 Aug 2024 17:01:06 +0200 Subject: [PATCH 2194/2411] Fix startup block from Swiss public transport (#123704) --- homeassistant/components/swiss_public_transport/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/swiss_public_transport/__init__.py b/homeassistant/components/swiss_public_transport/__init__.py index 3e29fb9c746..dc1d0eb236c 100644 --- a/homeassistant/components/swiss_public_transport/__init__.py +++ b/homeassistant/components/swiss_public_transport/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry( translation_key="request_timeout", translation_placeholders={ "config_title": entry.title, - "error": e, + "error": str(e), }, ) from e except OpendataTransportError as e: @@ -68,7 +68,7 @@ async def async_setup_entry( translation_placeholders={ **PLACEHOLDERS, "config_title": entry.title, - "error": e, + "error": str(e), }, ) from e From 5fb6c65d23e4af9af3bd9080017772d5623162d6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 17:01:31 +0200 Subject: [PATCH 2195/2411] Remove unnecessary assignment of Template.hass from alexa (#123699) --- homeassistant/components/alexa/flash_briefings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index eed700602ce..0d75ee04b7a 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -52,7 +52,6 @@ class AlexaFlashBriefingView(http.HomeAssistantView): """Initialize Alexa view.""" super().__init__() self.flash_briefings = flash_briefings - template.attach(hass, self.flash_briefings) @callback def get( From b7a0bf152b4e27e01134df8e9ad50eef358d938e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 12 Aug 2024 18:56:54 +0200 Subject: [PATCH 2196/2411] Update aioqsw to v0.4.1 (#123721) --- 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 b8c62133193..d34848346b7 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", "iot_class": "local_polling", "loggers": ["aioqsw"], - "requirements": ["aioqsw==0.4.0"] + "requirements": ["aioqsw==0.4.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3e868a419eb..f8f30eba714 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -335,7 +335,7 @@ aiopvpc==4.2.2 aiopyarr==23.4.0 # homeassistant.components.qnap_qsw -aioqsw==0.4.0 +aioqsw==0.4.1 # homeassistant.components.rainforest_raven aioraven==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af61773fa16..270a4126979 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -317,7 +317,7 @@ aiopvpc==4.2.2 aiopyarr==23.4.0 # homeassistant.components.qnap_qsw -aioqsw==0.4.0 +aioqsw==0.4.1 # homeassistant.components.rainforest_raven aioraven==0.7.0 From 93eb74d970b2eab057ae274fc31cdaf3b06b3d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 12 Aug 2024 18:57:34 +0200 Subject: [PATCH 2197/2411] Update aioairzone-cloud to v0.6.2 (#123719) --- homeassistant/components/airzone_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/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index 362973ae833..b691770e934 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_push", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.6.1"] + "requirements": ["aioairzone-cloud==0.6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index f8f30eba714..a56599587d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -176,7 +176,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.6.1 +aioairzone-cloud==0.6.2 # homeassistant.components.airzone aioairzone==0.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 270a4126979..83e9b41145e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.6.1 +aioairzone-cloud==0.6.2 # homeassistant.components.airzone aioairzone==0.8.1 From 138d229fef307554ac0942b08a0d392ad698e900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 12 Aug 2024 18:59:31 +0200 Subject: [PATCH 2198/2411] Update AEMET-OpenData to v0.5.4 (#123716) --- homeassistant/components/aemet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index d2e5c5fdc5a..3696e16b437 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aemet", "iot_class": "cloud_polling", "loggers": ["aemet_opendata"], - "requirements": ["AEMET-OpenData==0.5.3"] + "requirements": ["AEMET-OpenData==0.5.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index a56599587d6..0dca62e06f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,7 +4,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.5.3 +AEMET-OpenData==0.5.4 # homeassistant.components.honeywell AIOSomecomfort==0.0.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83e9b41145e..ec32098b7bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.5.3 +AEMET-OpenData==0.5.4 # homeassistant.components.honeywell AIOSomecomfort==0.0.25 From 74a09073c2bcf5cf73bd43b6dfca45bd21f65d64 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Mon, 12 Aug 2024 13:01:07 -0400 Subject: [PATCH 2199/2411] Bump pyschlage to 2024.8.0 (#123714) --- homeassistant/components/schlage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index c6dfc443bb8..5619cf7b312 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2024.6.0"] + "requirements": ["pyschlage==2024.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0dca62e06f0..5414c205ac7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2160,7 +2160,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2024.6.0 +pyschlage==2024.8.0 # homeassistant.components.sensibo pysensibo==1.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec32098b7bb..8a1abbfa769 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1726,7 +1726,7 @@ pyrympro==0.0.8 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2024.6.0 +pyschlage==2024.8.0 # homeassistant.components.sensibo pysensibo==1.0.36 From 21987a67e7c4d3c526ddfbd4866c64f93c2569f0 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 12 Aug 2024 19:20:21 +0200 Subject: [PATCH 2200/2411] Cleanup unneeded assignment of hass property on MQTT Template objects (#123706) * Cleanup unneeded assignment of hass property on MQTT Template objects * Commented out code and unneeded checks * Consistent assign hass to Template in mqtt tests * Remove unused hass attribute * Missed line --- homeassistant/components/mqtt/__init__.py | 5 ++-- homeassistant/components/mqtt/models.py | 19 +------------- homeassistant/components/mqtt/tag.py | 6 ++--- homeassistant/components/mqtt/trigger.py | 5 ++-- tests/components/mqtt/test_client.py | 2 +- tests/components/mqtt/test_init.py | 30 +++++++++++------------ 6 files changed, 23 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 5f7f1b1d330..013bd26e49c 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -303,8 +303,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # has been deprecated with HA Core 2024.8.0 # and will be removed with HA Core 2025.2.0 rendered_topic: Any = MqttCommandTemplate( - template.Template(msg_topic_template), - hass=hass, + template.Template(msg_topic_template, hass), ).async_render() ir.async_create_issue( hass, @@ -353,7 +352,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: }, ) payload = MqttCommandTemplate( - template.Template(payload_template), hass=hass + template.Template(payload_template, hass) ).async_render() if TYPE_CHECKING: diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index c355510a5c2..f2b3165f66c 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -12,7 +12,7 @@ import logging from typing import TYPE_CHECKING, Any, TypedDict from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, Platform -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.exceptions import ServiceValidationError, TemplateError from homeassistant.helpers import template from homeassistant.helpers.entity import Entity @@ -159,22 +159,13 @@ class MqttCommandTemplate: self, command_template: template.Template | None, *, - hass: HomeAssistant | None = None, entity: Entity | None = None, ) -> None: """Instantiate a command template.""" self._template_state: template.TemplateStateFromEntityId | None = None self._command_template = command_template - if command_template is None: - return - self._entity = entity - command_template.hass = hass - - if entity: - command_template.hass = entity.hass - @callback def async_render( self, @@ -270,7 +261,6 @@ class MqttValueTemplate: self, value_template: template.Template | None, *, - hass: HomeAssistant | None = None, entity: Entity | None = None, config_attributes: TemplateVarsType = None, ) -> None: @@ -278,15 +268,8 @@ class MqttValueTemplate: self._template_state: template.TemplateStateFromEntityId | None = None self._value_template = value_template self._config_attributes = config_attributes - if value_template is None: - return - - value_template.hass = hass self._entity = entity - if entity: - value_template.hass = entity.hass - @callback def async_render_with_possible_json_value( self, diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index fbb0ea813c2..d5f5371c357 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -118,8 +118,7 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdateMixin): self.hass = hass self._sub_state: dict[str, EntitySubscription] | None = None self._value_template = MqttValueTemplate( - config.get(CONF_VALUE_TEMPLATE), - hass=self.hass, + config.get(CONF_VALUE_TEMPLATE) ).async_render_with_possible_json_value MqttDiscoveryDeviceUpdateMixin.__init__( @@ -136,8 +135,7 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdateMixin): return self._config = config self._value_template = MqttValueTemplate( - config.get(CONF_VALUE_TEMPLATE), - hass=self.hass, + config.get(CONF_VALUE_TEMPLATE) ).async_render_with_possible_json_value update_device(self.hass, self._config_entry, config) await self.subscribe_topics() diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index 91ac404a07a..3f7f03d7f19 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -60,10 +60,10 @@ async def async_attach_trigger( trigger_data: TriggerData = trigger_info["trigger_data"] command_template: Callable[ [PublishPayloadType, TemplateVarsType], PublishPayloadType - ] = MqttCommandTemplate(config.get(CONF_PAYLOAD), hass=hass).async_render + ] = MqttCommandTemplate(config.get(CONF_PAYLOAD)).async_render value_template: Callable[[ReceivePayloadType, str], ReceivePayloadType] value_template = MqttValueTemplate( - config.get(CONF_VALUE_TEMPLATE), hass=hass + config.get(CONF_VALUE_TEMPLATE) ).async_render_with_possible_json_value encoding: str | None = config[CONF_ENCODING] or None qos: int = config[CONF_QOS] @@ -75,7 +75,6 @@ async def async_attach_trigger( wanted_payload = command_template(None, variables) topic_template: Template = config[CONF_TOPIC] - topic_template.hass = hass topic = topic_template.async_render(variables, limited=True, parse_result=False) mqtt.util.valid_subscribe_topic(topic) diff --git a/tests/components/mqtt/test_client.py b/tests/components/mqtt/test_client.py index cd02d805e1c..c5887016f2e 100644 --- a/tests/components/mqtt/test_client.py +++ b/tests/components/mqtt/test_client.py @@ -225,7 +225,7 @@ async def test_publish( async def test_convert_outgoing_payload(hass: HomeAssistant) -> None: """Test the converting of outgoing MQTT payloads without template.""" - command_template = mqtt.MqttCommandTemplate(None, hass=hass) + command_template = mqtt.MqttCommandTemplate(None) assert command_template.async_render(b"\xde\xad\xbe\xef") == b"\xde\xad\xbe\xef" assert ( command_template.async_render("b'\\xde\\xad\\xbe\\xef'") diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 51379dc8508..f495c5ca585 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -89,12 +89,12 @@ async def test_command_template_value(hass: HomeAssistant) -> None: # test rendering value tpl = template.Template("{{ value + 1 }}", hass=hass) - cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass=hass) + cmd_tpl = mqtt.MqttCommandTemplate(tpl) assert cmd_tpl.async_render(4321) == "4322" # test variables at rendering tpl = template.Template("{{ some_var }}", hass=hass) - cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass=hass) + cmd_tpl = mqtt.MqttCommandTemplate(tpl) assert cmd_tpl.async_render(None, variables=variables) == "beer" @@ -161,8 +161,8 @@ async def test_command_template_variables( async def test_command_template_fails(hass: HomeAssistant) -> None: """Test the exception handling of an MQTT command template.""" - tpl = template.Template("{{ value * 2 }}") - cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass=hass) + tpl = template.Template("{{ value * 2 }}", hass=hass) + cmd_tpl = mqtt.MqttCommandTemplate(tpl) with pytest.raises(MqttCommandTemplateException) as exc: cmd_tpl.async_render(None) assert "unsupported operand type(s) for *: 'NoneType' and 'int'" in str(exc.value) @@ -174,13 +174,13 @@ async def test_value_template_value(hass: HomeAssistant) -> None: variables = {"id": 1234, "some_var": "beer"} # test rendering value - tpl = template.Template("{{ value_json.id }}") - val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass) + tpl = template.Template("{{ value_json.id }}", hass=hass) + val_tpl = mqtt.MqttValueTemplate(tpl) assert val_tpl.async_render_with_possible_json_value('{"id": 4321}') == "4321" # test variables at rendering - tpl = template.Template("{{ value_json.id }} {{ some_var }} {{ code }}") - val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass, config_attributes={"code": 1234}) + tpl = template.Template("{{ value_json.id }} {{ some_var }} {{ code }}", hass=hass) + val_tpl = mqtt.MqttValueTemplate(tpl, config_attributes={"code": 1234}) assert ( val_tpl.async_render_with_possible_json_value( '{"id": 4321}', variables=variables @@ -189,8 +189,8 @@ async def test_value_template_value(hass: HomeAssistant) -> None: ) # test with default value if an error occurs due to an invalid template - tpl = template.Template("{{ value_json.id | as_datetime }}") - val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass) + tpl = template.Template("{{ value_json.id | as_datetime }}", hass=hass) + val_tpl = mqtt.MqttValueTemplate(tpl) assert ( val_tpl.async_render_with_possible_json_value('{"otherid": 4321}', "my default") == "my default" @@ -200,19 +200,19 @@ async def test_value_template_value(hass: HomeAssistant) -> None: entity = Entity() entity.hass = hass entity.entity_id = "select.test" - tpl = template.Template("{{ value_json.id }}") + tpl = template.Template("{{ value_json.id }}", hass=hass) val_tpl = mqtt.MqttValueTemplate(tpl, entity=entity) assert val_tpl.async_render_with_possible_json_value('{"id": 4321}') == "4321" # test this object in a template - tpl2 = template.Template("{{ this.entity_id }}") + tpl2 = template.Template("{{ this.entity_id }}", hass=hass) val_tpl2 = mqtt.MqttValueTemplate(tpl2, entity=entity) assert val_tpl2.async_render_with_possible_json_value("bla") == "select.test" with patch( "homeassistant.helpers.template.TemplateStateFromEntityId", MagicMock() ) as template_state_calls: - tpl3 = template.Template("{{ this.entity_id }}") + tpl3 = template.Template("{{ this.entity_id }}", hass=hass) val_tpl3 = mqtt.MqttValueTemplate(tpl3, entity=entity) val_tpl3.async_render_with_possible_json_value("call1") val_tpl3.async_render_with_possible_json_value("call2") @@ -223,8 +223,8 @@ async def test_value_template_fails(hass: HomeAssistant) -> None: """Test the rendering of MQTT value template fails.""" entity = MockEntity(entity_id="sensor.test") entity.hass = hass - tpl = template.Template("{{ value_json.some_var * 2 }}") - val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass, entity=entity) + tpl = template.Template("{{ value_json.some_var * 2 }}", hass=hass) + val_tpl = mqtt.MqttValueTemplate(tpl, entity=entity) with pytest.raises(MqttValueTemplateException) as exc: val_tpl.async_render_with_possible_json_value('{"some_var": null }') assert str(exc.value) == ( From a4f0234841ceee6a526c3a7c804bd9b088309f1b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Aug 2024 20:42:39 +0200 Subject: [PATCH 2201/2411] Reduce logging in command_line (#123723) --- homeassistant/components/command_line/cover.py | 4 ++-- homeassistant/components/command_line/switch.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 344fddabc3b..2bcbb610296 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -110,7 +110,7 @@ class CommandCover(ManualTriggerEntity, CoverEntity): async def _async_move_cover(self, command: str) -> bool: """Execute the actual commands.""" - LOGGER.info("Running command: %s", command) + LOGGER.debug("Running command: %s", command) returncode = await async_call_shell_with_timeout(command, self._timeout) success = returncode == 0 @@ -140,7 +140,7 @@ class CommandCover(ManualTriggerEntity, CoverEntity): async def _async_query_state(self) -> str | None: """Query for the state.""" if self._command_state: - LOGGER.info("Running state value command: %s", self._command_state) + LOGGER.debug("Running state value command: %s", self._command_state) return await async_check_output_or_log(self._command_state, self._timeout) return None diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index b02fb6dcd4a..33b38ab9115 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -111,7 +111,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity): async def _switch(self, command: str) -> bool: """Execute the actual commands.""" - LOGGER.info("Running command: %s", command) + LOGGER.debug("Running command: %s", command) success = await async_call_shell_with_timeout(command, self._timeout) == 0 @@ -122,12 +122,12 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity): async def _async_query_state_value(self, command: str) -> str | None: """Execute state command for return value.""" - LOGGER.info("Running state value command: %s", command) + LOGGER.debug("Running state value command: %s", command) return await async_check_output_or_log(command, self._timeout) async def _async_query_state_code(self, command: str) -> bool: """Execute state command for return code.""" - LOGGER.info("Running state code command: %s", command) + LOGGER.debug("Running state code command: %s", command) return ( await async_call_shell_with_timeout( command, self._timeout, log_return_code=False From ff0a44cc123522736a8f1e0c41de1649c1900bf1 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 12 Aug 2024 22:28:39 +0300 Subject: [PATCH 2202/2411] Bump aioswitcher to 4.0.2 (#123734) --- homeassistant/components/switcher_kis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 1ec32a26e4e..75ace60e942 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_push", "loggers": ["aioswitcher"], "quality_scale": "platinum", - "requirements": ["aioswitcher==4.0.1"], + "requirements": ["aioswitcher==4.0.2"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 5414c205ac7..76dce213ebf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -374,7 +374,7 @@ aiosolaredge==0.2.0 aiosteamist==1.0.0 # homeassistant.components.switcher_kis -aioswitcher==4.0.1 +aioswitcher==4.0.2 # homeassistant.components.syncthing aiosyncthing==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a1abbfa769..d88d5607050 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ aiosolaredge==0.2.0 aiosteamist==1.0.0 # homeassistant.components.switcher_kis -aioswitcher==4.0.1 +aioswitcher==4.0.2 # homeassistant.components.syncthing aiosyncthing==0.5.1 From 05c4b1a6a9a505e883bba41651bd0f67d3f28690 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Aug 2024 21:31:10 +0200 Subject: [PATCH 2203/2411] Remove deprecated logi_circle integration (#123727) --- CODEOWNERS | 2 - .../components/logi_circle/__init__.py | 271 ------------------ .../components/logi_circle/camera.py | 200 ------------- .../components/logi_circle/config_flow.py | 206 ------------- homeassistant/components/logi_circle/const.py | 22 -- .../components/logi_circle/icons.json | 7 - .../components/logi_circle/manifest.json | 11 - .../components/logi_circle/sensor.py | 164 ----------- .../components/logi_circle/services.yaml | 53 ---- .../components/logi_circle/strings.json | 105 ------- homeassistant/generated/config_flows.py | 1 - homeassistant/generated/integrations.json | 6 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/logi_circle/__init__.py | 1 - .../logi_circle/test_config_flow.py | 227 --------------- tests/components/logi_circle/test_init.py | 68 ----- 17 files changed, 1350 deletions(-) delete mode 100644 homeassistant/components/logi_circle/__init__.py delete mode 100644 homeassistant/components/logi_circle/camera.py delete mode 100644 homeassistant/components/logi_circle/config_flow.py delete mode 100644 homeassistant/components/logi_circle/const.py delete mode 100644 homeassistant/components/logi_circle/icons.json delete mode 100644 homeassistant/components/logi_circle/manifest.json delete mode 100644 homeassistant/components/logi_circle/sensor.py delete mode 100644 homeassistant/components/logi_circle/services.yaml delete mode 100644 homeassistant/components/logi_circle/strings.json delete mode 100644 tests/components/logi_circle/__init__.py delete mode 100644 tests/components/logi_circle/test_config_flow.py delete mode 100644 tests/components/logi_circle/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 710846a7f42..a3b38498f68 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -826,8 +826,6 @@ build.json @home-assistant/supervisor /tests/components/logbook/ @home-assistant/core /homeassistant/components/logger/ @home-assistant/core /tests/components/logger/ @home-assistant/core -/homeassistant/components/logi_circle/ @evanjd -/tests/components/logi_circle/ @evanjd /homeassistant/components/london_underground/ @jpbede /tests/components/london_underground/ @jpbede /homeassistant/components/lookin/ @ANMalko @bdraco diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py deleted file mode 100644 index 0713bcc438e..00000000000 --- a/homeassistant/components/logi_circle/__init__.py +++ /dev/null @@ -1,271 +0,0 @@ -"""Support for Logi Circle devices.""" - -import asyncio - -from aiohttp.client_exceptions import ClientResponseError -from logi_circle import LogiCircle -from logi_circle.exception import AuthorizationFailed -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.components import persistent_notification -from homeassistant.components.camera import ATTR_FILENAME -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_MODE, - CONF_API_KEY, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_MONITORED_CONDITIONS, - CONF_SENSORS, - EVENT_HOMEASSISTANT_STOP, - Platform, -) -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import config_validation as cv, issue_registry as ir -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import ConfigType - -from . import config_flow -from .const import ( - CONF_REDIRECT_URI, - DATA_LOGI, - DEFAULT_CACHEDB, - DOMAIN, - LED_MODE_KEY, - RECORDING_MODE_KEY, - SIGNAL_LOGI_CIRCLE_RECONFIGURE, - SIGNAL_LOGI_CIRCLE_RECORD, - SIGNAL_LOGI_CIRCLE_SNAPSHOT, -) -from .sensor import SENSOR_TYPES - -NOTIFICATION_ID = "logi_circle_notification" -NOTIFICATION_TITLE = "Logi Circle Setup" - -_TIMEOUT = 15 # seconds - -SERVICE_SET_CONFIG = "set_config" -SERVICE_LIVESTREAM_SNAPSHOT = "livestream_snapshot" -SERVICE_LIVESTREAM_RECORD = "livestream_record" - -ATTR_VALUE = "value" -ATTR_DURATION = "duration" - -PLATFORMS = [Platform.CAMERA, Platform.SENSOR] - -SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] - -SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ) - } -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_REDIRECT_URI): cv.string, - vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - -LOGI_CIRCLE_SERVICE_SET_CONFIG = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_MODE): vol.In([LED_MODE_KEY, RECORDING_MODE_KEY]), - vol.Required(ATTR_VALUE): cv.boolean, - } -) - -LOGI_CIRCLE_SERVICE_SNAPSHOT = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_FILENAME): cv.template, - } -) - -LOGI_CIRCLE_SERVICE_RECORD = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_FILENAME): cv.template, - vol.Required(ATTR_DURATION): cv.positive_int, - } -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up configured Logi Circle component.""" - if DOMAIN not in config: - return True - - conf = config[DOMAIN] - - config_flow.register_flow_implementation( - hass, - DOMAIN, - client_id=conf[CONF_CLIENT_ID], - client_secret=conf[CONF_CLIENT_SECRET], - api_key=conf[CONF_API_KEY], - redirect_uri=conf[CONF_REDIRECT_URI], - sensors=conf[CONF_SENSORS], - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) - ) - - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Logi Circle from a config entry.""" - ir.async_create_issue( - hass, - DOMAIN, - DOMAIN, - breaks_in_ha_version="2024.9.0", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key="integration_removed", - translation_placeholders={ - "entries": "/config/integrations/integration/logi_circle", - }, - ) - - logi_circle = LogiCircle( - client_id=entry.data[CONF_CLIENT_ID], - client_secret=entry.data[CONF_CLIENT_SECRET], - api_key=entry.data[CONF_API_KEY], - redirect_uri=entry.data[CONF_REDIRECT_URI], - cache_file=hass.config.path(DEFAULT_CACHEDB), - ) - - if not logi_circle.authorized: - persistent_notification.create( - hass, - ( - "Error: The cached access tokens are missing from" - f" {DEFAULT_CACHEDB}.
Please unload then re-add the Logi Circle" - " integration to resolve." - ), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - return False - - try: - async with asyncio.timeout(_TIMEOUT): - # Ensure the cameras property returns the same Camera objects for - # all devices. Performs implicit login and session validation. - await logi_circle.synchronize_cameras() - except AuthorizationFailed: - persistent_notification.create( - hass, - ( - "Error: Failed to obtain an access token from the cached " - "refresh token.
" - "Token may have expired or been revoked.
" - "Please unload then re-add the Logi Circle integration to resolve" - ), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - return False - except TimeoutError: - # The TimeoutError exception object returns nothing when casted to a - # string, so we'll handle it separately. - err = f"{_TIMEOUT}s timeout exceeded when connecting to Logi Circle API" - persistent_notification.create( - hass, - f"Error: {err}
You will need to restart hass after fixing.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - return False - except ClientResponseError as ex: - persistent_notification.create( - hass, - f"Error: {ex}
You will need to restart hass after fixing.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - return False - - hass.data[DATA_LOGI] = logi_circle - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - async def service_handler(service: ServiceCall) -> None: - """Dispatch service calls to target entities.""" - params = dict(service.data) - - if service.service == SERVICE_SET_CONFIG: - async_dispatcher_send(hass, SIGNAL_LOGI_CIRCLE_RECONFIGURE, params) - if service.service == SERVICE_LIVESTREAM_SNAPSHOT: - async_dispatcher_send(hass, SIGNAL_LOGI_CIRCLE_SNAPSHOT, params) - if service.service == SERVICE_LIVESTREAM_RECORD: - async_dispatcher_send(hass, SIGNAL_LOGI_CIRCLE_RECORD, params) - - hass.services.async_register( - DOMAIN, - SERVICE_SET_CONFIG, - service_handler, - schema=LOGI_CIRCLE_SERVICE_SET_CONFIG, - ) - - hass.services.async_register( - DOMAIN, - SERVICE_LIVESTREAM_SNAPSHOT, - service_handler, - schema=LOGI_CIRCLE_SERVICE_SNAPSHOT, - ) - - hass.services.async_register( - DOMAIN, - SERVICE_LIVESTREAM_RECORD, - service_handler, - schema=LOGI_CIRCLE_SERVICE_RECORD, - ) - - async def shut_down(event=None): - """Close Logi Circle aiohttp session.""" - await logi_circle.auth_provider.close() - - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) - ) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - if all( - config_entry.state is config_entries.ConfigEntryState.NOT_LOADED - for config_entry in hass.config_entries.async_entries(DOMAIN) - if config_entry.entry_id != entry.entry_id - ): - ir.async_delete_issue(hass, DOMAIN, DOMAIN) - - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - logi_circle = hass.data.pop(DATA_LOGI) - - # Tell API wrapper to close all aiohttp sessions, invalidate WS connections - # and clear all locally cached tokens - await logi_circle.auth_provider.clear_authorization() - - return unload_ok diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py deleted file mode 100644 index 04f12586679..00000000000 --- a/homeassistant/components/logi_circle/camera.py +++ /dev/null @@ -1,200 +0,0 @@ -"""Support to the Logi Circle cameras.""" - -from __future__ import annotations - -from datetime import timedelta -import logging - -from homeassistant.components.camera import Camera, CameraEntityFeature -from homeassistant.components.ffmpeg import get_ffmpeg_manager -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_BATTERY_CHARGING, - ATTR_BATTERY_LEVEL, - ATTR_ENTITY_ID, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from .const import ( - ATTRIBUTION, - DEVICE_BRAND, - DOMAIN as LOGI_CIRCLE_DOMAIN, - LED_MODE_KEY, - RECORDING_MODE_KEY, - SIGNAL_LOGI_CIRCLE_RECONFIGURE, - SIGNAL_LOGI_CIRCLE_RECORD, - SIGNAL_LOGI_CIRCLE_SNAPSHOT, -) - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=60) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up a Logi Circle Camera. Obsolete.""" - _LOGGER.warning("Logi Circle no longer works with camera platform configuration") - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up a Logi Circle Camera based on a config entry.""" - devices = await hass.data[LOGI_CIRCLE_DOMAIN].cameras - ffmpeg = get_ffmpeg_manager(hass) - - cameras = [LogiCam(device, ffmpeg) for device in devices] - - async_add_entities(cameras, True) - - -class LogiCam(Camera): - """An implementation of a Logi Circle camera.""" - - _attr_attribution = ATTRIBUTION - _attr_should_poll = True # Cameras default to False - _attr_supported_features = CameraEntityFeature.ON_OFF - _attr_has_entity_name = True - _attr_name = None - - def __init__(self, camera, ffmpeg): - """Initialize Logi Circle camera.""" - super().__init__() - self._camera = camera - self._has_battery = camera.supports_feature("battery_level") - self._ffmpeg = ffmpeg - self._listeners = [] - self._attr_unique_id = camera.mac_address - self._attr_device_info = DeviceInfo( - identifiers={(LOGI_CIRCLE_DOMAIN, camera.id)}, - manufacturer=DEVICE_BRAND, - model=camera.model_name, - name=camera.name, - sw_version=camera.firmware, - ) - - async def async_added_to_hass(self) -> None: - """Connect camera methods to signals.""" - - def _dispatch_proxy(method): - """Expand parameters & filter entity IDs.""" - - async def _call(params): - entity_ids = params.get(ATTR_ENTITY_ID) - filtered_params = { - k: v for k, v in params.items() if k != ATTR_ENTITY_ID - } - if entity_ids is None or self.entity_id in entity_ids: - await method(**filtered_params) - - return _call - - self._listeners.extend( - [ - async_dispatcher_connect( - self.hass, - SIGNAL_LOGI_CIRCLE_RECONFIGURE, - _dispatch_proxy(self.set_config), - ), - async_dispatcher_connect( - self.hass, - SIGNAL_LOGI_CIRCLE_SNAPSHOT, - _dispatch_proxy(self.livestream_snapshot), - ), - async_dispatcher_connect( - self.hass, - SIGNAL_LOGI_CIRCLE_RECORD, - _dispatch_proxy(self.download_livestream), - ), - ] - ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect dispatcher listeners when removed.""" - for detach in self._listeners: - detach() - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - state = { - "battery_saving_mode": ( - STATE_ON if self._camera.battery_saving else STATE_OFF - ), - "microphone_gain": self._camera.microphone_gain, - } - - # Add battery attributes if camera is battery-powered - if self._has_battery: - state[ATTR_BATTERY_CHARGING] = self._camera.charging - state[ATTR_BATTERY_LEVEL] = self._camera.battery_level - - return state - - async def async_camera_image( - self, width: int | None = None, height: int | None = None - ) -> bytes | None: - """Return a still image from the camera.""" - return await self._camera.live_stream.download_jpeg() - - async def async_turn_off(self) -> None: - """Disable streaming mode for this camera.""" - await self._camera.set_config("streaming", False) - - async def async_turn_on(self) -> None: - """Enable streaming mode for this camera.""" - await self._camera.set_config("streaming", True) - - async def set_config(self, mode, value): - """Set an configuration property for the target camera.""" - if mode == LED_MODE_KEY: - await self._camera.set_config("led", value) - if mode == RECORDING_MODE_KEY: - await self._camera.set_config("recording_disabled", not value) - - async def download_livestream(self, filename, duration): - """Download a recording from the camera's livestream.""" - # Render filename from template. - stream_file = filename.async_render(variables={ATTR_ENTITY_ID: self.entity_id}) - - # Respect configured allowed paths. - if not self.hass.config.is_allowed_path(stream_file): - _LOGGER.error("Can't write %s, no access to path!", stream_file) - return - - await self._camera.live_stream.download_rtsp( - filename=stream_file, - duration=timedelta(seconds=duration), - ffmpeg_bin=self._ffmpeg.binary, - ) - - async def livestream_snapshot(self, filename): - """Download a still frame from the camera's livestream.""" - # Render filename from template. - snapshot_file = filename.async_render( - variables={ATTR_ENTITY_ID: self.entity_id} - ) - - # Respect configured allowed paths. - if not self.hass.config.is_allowed_path(snapshot_file): - _LOGGER.error("Can't write %s, no access to path!", snapshot_file) - return - - await self._camera.live_stream.download_jpeg( - filename=snapshot_file, refresh=True - ) - - async def async_update(self) -> None: - """Update camera entity and refresh attributes.""" - await self._camera.update() diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py deleted file mode 100644 index 6c1a549aa04..00000000000 --- a/homeassistant/components/logi_circle/config_flow.py +++ /dev/null @@ -1,206 +0,0 @@ -"""Config flow to configure Logi Circle component.""" - -import asyncio -from collections import OrderedDict -from http import HTTPStatus - -from logi_circle import LogiCircle -from logi_circle.exception import AuthorizationFailed -import voluptuous as vol - -from homeassistant.components.http import KEY_HASS, HomeAssistantView -from homeassistant.config_entries import ConfigFlow -from homeassistant.const import ( - CONF_API_KEY, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_SENSORS, -) -from homeassistant.core import callback - -from .const import CONF_REDIRECT_URI, DEFAULT_CACHEDB, DOMAIN - -_TIMEOUT = 15 # seconds - -DATA_FLOW_IMPL = "logi_circle_flow_implementation" -EXTERNAL_ERRORS = "logi_errors" -AUTH_CALLBACK_PATH = "/api/logi_circle" -AUTH_CALLBACK_NAME = "api:logi_circle" - - -@callback -def register_flow_implementation( - hass, domain, client_id, client_secret, api_key, redirect_uri, sensors -): - """Register a flow implementation. - - domain: Domain of the component responsible for the implementation. - client_id: Client ID. - client_secret: Client secret. - api_key: API key issued by Logitech. - redirect_uri: Auth callback redirect URI. - sensors: Sensor config. - """ - if DATA_FLOW_IMPL not in hass.data: - hass.data[DATA_FLOW_IMPL] = OrderedDict() - - hass.data[DATA_FLOW_IMPL][domain] = { - CONF_CLIENT_ID: client_id, - CONF_CLIENT_SECRET: client_secret, - CONF_API_KEY: api_key, - CONF_REDIRECT_URI: redirect_uri, - CONF_SENSORS: sensors, - EXTERNAL_ERRORS: None, - } - - -class LogiCircleFlowHandler(ConfigFlow, domain=DOMAIN): - """Config flow for Logi Circle component.""" - - VERSION = 1 - - def __init__(self) -> None: - """Initialize flow.""" - self.flow_impl = None - - async def async_step_import(self, user_input=None): - """Handle external yaml configuration.""" - self._async_abort_entries_match() - - self.flow_impl = DOMAIN - - return await self.async_step_auth() - - async def async_step_user(self, user_input=None): - """Handle a flow start.""" - flows = self.hass.data.get(DATA_FLOW_IMPL, {}) - - self._async_abort_entries_match() - - if not flows: - return self.async_abort(reason="missing_configuration") - - if len(flows) == 1: - self.flow_impl = list(flows)[0] - return await self.async_step_auth() - - if user_input is not None: - self.flow_impl = user_input["flow_impl"] - return await self.async_step_auth() - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema({vol.Required("flow_impl"): vol.In(list(flows))}), - ) - - async def async_step_auth(self, user_input=None): - """Create an entry for auth.""" - if self._async_current_entries(): - return self.async_abort(reason="external_setup") - - external_error = self.hass.data[DATA_FLOW_IMPL][DOMAIN][EXTERNAL_ERRORS] - errors = {} - if external_error: - # Handle error from another flow - errors["base"] = external_error - self.hass.data[DATA_FLOW_IMPL][DOMAIN][EXTERNAL_ERRORS] = None - elif user_input is not None: - errors["base"] = "follow_link" - - url = self._get_authorization_url() - - return self.async_show_form( - step_id="auth", - description_placeholders={"authorization_url": url}, - errors=errors, - ) - - def _get_authorization_url(self): - """Create temporary Circle session and generate authorization url.""" - flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] - client_id = flow[CONF_CLIENT_ID] - client_secret = flow[CONF_CLIENT_SECRET] - api_key = flow[CONF_API_KEY] - redirect_uri = flow[CONF_REDIRECT_URI] - - logi_session = LogiCircle( - client_id=client_id, - client_secret=client_secret, - api_key=api_key, - redirect_uri=redirect_uri, - ) - - self.hass.http.register_view(LogiCircleAuthCallbackView()) - - return logi_session.authorize_url - - async def async_step_code(self, code=None): - """Received code for authentication.""" - self._async_abort_entries_match() - - return await self._async_create_session(code) - - async def _async_create_session(self, code): - """Create Logi Circle session and entries.""" - flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] - client_id = flow[CONF_CLIENT_ID] - client_secret = flow[CONF_CLIENT_SECRET] - api_key = flow[CONF_API_KEY] - redirect_uri = flow[CONF_REDIRECT_URI] - sensors = flow[CONF_SENSORS] - - logi_session = LogiCircle( - client_id=client_id, - client_secret=client_secret, - api_key=api_key, - redirect_uri=redirect_uri, - cache_file=self.hass.config.path(DEFAULT_CACHEDB), - ) - - try: - async with asyncio.timeout(_TIMEOUT): - await logi_session.authorize(code) - except AuthorizationFailed: - (self.hass.data[DATA_FLOW_IMPL][DOMAIN][EXTERNAL_ERRORS]) = "invalid_auth" - return self.async_abort(reason="external_error") - except TimeoutError: - ( - self.hass.data[DATA_FLOW_IMPL][DOMAIN][EXTERNAL_ERRORS] - ) = "authorize_url_timeout" - return self.async_abort(reason="external_error") - - account_id = (await logi_session.account)["accountId"] - await logi_session.close() - return self.async_create_entry( - title=f"Logi Circle ({account_id})", - data={ - CONF_CLIENT_ID: client_id, - CONF_CLIENT_SECRET: client_secret, - CONF_API_KEY: api_key, - CONF_REDIRECT_URI: redirect_uri, - CONF_SENSORS: sensors, - }, - ) - - -class LogiCircleAuthCallbackView(HomeAssistantView): - """Logi Circle Authorization Callback View.""" - - requires_auth = False - url = AUTH_CALLBACK_PATH - name = AUTH_CALLBACK_NAME - - async def get(self, request): - """Receive authorization code.""" - hass = request.app[KEY_HASS] - if "code" in request.query: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": "code"}, data=request.query["code"] - ) - ) - return self.json_message("Authorisation code saved") - return self.json_message( - "Authorisation code missing from query string", - status_code=HTTPStatus.BAD_REQUEST, - ) diff --git a/homeassistant/components/logi_circle/const.py b/homeassistant/components/logi_circle/const.py deleted file mode 100644 index e144f47ce4e..00000000000 --- a/homeassistant/components/logi_circle/const.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Constants in Logi Circle component.""" - -from __future__ import annotations - -DOMAIN = "logi_circle" -DATA_LOGI = DOMAIN - -CONF_REDIRECT_URI = "redirect_uri" - -DEFAULT_CACHEDB = ".logi_cache.pickle" - - -LED_MODE_KEY = "LED" -RECORDING_MODE_KEY = "RECORDING_MODE" - -SIGNAL_LOGI_CIRCLE_RECONFIGURE = "logi_circle_reconfigure" -SIGNAL_LOGI_CIRCLE_SNAPSHOT = "logi_circle_snapshot" -SIGNAL_LOGI_CIRCLE_RECORD = "logi_circle_record" - -# Attribution -ATTRIBUTION = "Data provided by circle.logi.com" -DEVICE_BRAND = "Logitech" diff --git a/homeassistant/components/logi_circle/icons.json b/homeassistant/components/logi_circle/icons.json deleted file mode 100644 index 9289746d375..00000000000 --- a/homeassistant/components/logi_circle/icons.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "services": { - "set_config": "mdi:cog", - "livestream_snapshot": "mdi:camera", - "livestream_record": "mdi:record-rec" - } -} diff --git a/homeassistant/components/logi_circle/manifest.json b/homeassistant/components/logi_circle/manifest.json deleted file mode 100644 index f4f65b22505..00000000000 --- a/homeassistant/components/logi_circle/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "domain": "logi_circle", - "name": "Logi Circle", - "codeowners": ["@evanjd"], - "config_flow": true, - "dependencies": ["ffmpeg", "http"], - "documentation": "https://www.home-assistant.io/integrations/logi_circle", - "iot_class": "cloud_polling", - "loggers": ["logi_circle"], - "requirements": ["logi-circle==0.2.3"] -} diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py deleted file mode 100644 index 121cb8848ae..00000000000 --- a/homeassistant/components/logi_circle/sensor.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Support for Logi Circle sensors.""" - -from __future__ import annotations - -import logging -from typing import Any - -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_BATTERY_CHARGING, - CONF_MONITORED_CONDITIONS, - CONF_SENSORS, - PERCENTAGE, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util.dt import as_local - -from .const import ATTRIBUTION, DEVICE_BRAND, DOMAIN as LOGI_CIRCLE_DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="battery_level", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.BATTERY, - ), - SensorEntityDescription( - key="last_activity_time", - translation_key="last_activity", - icon="mdi:history", - ), - SensorEntityDescription( - key="recording", - translation_key="recording_mode", - icon="mdi:eye", - ), - SensorEntityDescription( - key="signal_strength_category", - translation_key="wifi_signal_category", - icon="mdi:wifi", - ), - SensorEntityDescription( - key="signal_strength_percentage", - translation_key="wifi_signal_strength", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:wifi", - ), - SensorEntityDescription( - key="streaming", - translation_key="streaming_mode", - icon="mdi:camera", - ), -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up a sensor for a Logi Circle device. Obsolete.""" - _LOGGER.warning("Logi Circle no longer works with sensor platform configuration") - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up a Logi Circle sensor based on a config entry.""" - devices = await hass.data[LOGI_CIRCLE_DOMAIN].cameras - time_zone = str(hass.config.time_zone) - - monitored_conditions = entry.data[CONF_SENSORS].get(CONF_MONITORED_CONDITIONS) - entities = [ - LogiSensor(device, time_zone, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - for device in devices - if device.supports_feature(description.key) - ] - - async_add_entities(entities, True) - - -class LogiSensor(SensorEntity): - """A sensor implementation for a Logi Circle camera.""" - - _attr_attribution = ATTRIBUTION - _attr_has_entity_name = True - - def __init__(self, camera, time_zone, description: SensorEntityDescription) -> None: - """Initialize a sensor for Logi Circle camera.""" - self.entity_description = description - self._camera = camera - self._attr_unique_id = f"{camera.mac_address}-{description.key}" - self._activity: dict[Any, Any] = {} - self._tz = time_zone - self._attr_device_info = DeviceInfo( - identifiers={(LOGI_CIRCLE_DOMAIN, camera.id)}, - manufacturer=DEVICE_BRAND, - model=camera.model_name, - name=camera.name, - sw_version=camera.firmware, - ) - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - state = { - "battery_saving_mode": ( - STATE_ON if self._camera.battery_saving else STATE_OFF - ), - "microphone_gain": self._camera.microphone_gain, - } - - if self.entity_description.key == "battery_level": - state[ATTR_BATTERY_CHARGING] = self._camera.charging - - return state - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - sensor_type = self.entity_description.key - if sensor_type == "recording_mode" and self._attr_native_value is not None: - return "mdi:eye" if self._attr_native_value == STATE_ON else "mdi:eye-off" - if sensor_type == "streaming_mode" and self._attr_native_value is not None: - return ( - "mdi:camera" - if self._attr_native_value == STATE_ON - else "mdi:camera-off" - ) - return self.entity_description.icon - - async def async_update(self) -> None: - """Get the latest data and updates the state.""" - _LOGGER.debug("Pulling data from %s sensor", self.name) - await self._camera.update() - - if self.entity_description.key == "last_activity_time": - last_activity = await self._camera.get_last_activity(force_refresh=True) - if last_activity is not None: - last_activity_time = as_local(last_activity.end_time_utc) - self._attr_native_value = ( - f"{last_activity_time.hour:0>2}:{last_activity_time.minute:0>2}" - ) - else: - state = getattr(self._camera, self.entity_description.key, None) - if isinstance(state, bool): - self._attr_native_value = STATE_ON if state is True else STATE_OFF - else: - self._attr_native_value = state diff --git a/homeassistant/components/logi_circle/services.yaml b/homeassistant/components/logi_circle/services.yaml deleted file mode 100644 index cb855a953a6..00000000000 --- a/homeassistant/components/logi_circle/services.yaml +++ /dev/null @@ -1,53 +0,0 @@ -# Describes the format for available Logi Circle services - -set_config: - fields: - entity_id: - selector: - entity: - integration: logi_circle - domain: camera - mode: - required: true - selector: - select: - options: - - "LED" - - "RECORDING_MODE" - value: - required: true - selector: - boolean: - -livestream_snapshot: - fields: - entity_id: - selector: - entity: - integration: logi_circle - domain: camera - filename: - required: true - example: "/tmp/snapshot_{{ entity_id }}.jpg" - selector: - text: - -livestream_record: - fields: - entity_id: - selector: - entity: - integration: logi_circle - domain: camera - filename: - required: true - example: "/tmp/snapshot_{{ entity_id }}.mp4" - selector: - text: - duration: - required: true - selector: - number: - min: 1 - max: 3600 - unit_of_measurement: seconds diff --git a/homeassistant/components/logi_circle/strings.json b/homeassistant/components/logi_circle/strings.json deleted file mode 100644 index be0f4632c25..00000000000 --- a/homeassistant/components/logi_circle/strings.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Authentication Provider", - "description": "Pick via which authentication provider you want to authenticate with Logi Circle.", - "data": { - "flow_impl": "Provider" - } - }, - "auth": { - "title": "Authenticate with Logi Circle", - "description": "Please follow the link below and **Accept** access to your Logi Circle account, then come back and press **Submit** below.\n\n[Link]({authorization_url})" - } - }, - "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", - "follow_link": "Please follow the link and authenticate before pressing Submit." - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "external_error": "Exception occurred from another flow.", - "external_setup": "Logi Circle successfully configured from another flow.", - "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]" - } - }, - "entity": { - "sensor": { - "last_activity": { - "name": "Last activity" - }, - "recording_mode": { - "name": "Recording mode" - }, - "wifi_signal_category": { - "name": "Wi-Fi signal category" - }, - "wifi_signal_strength": { - "name": "Wi-Fi signal strength" - }, - "streaming_mode": { - "name": "Streaming mode" - } - } - }, - "issues": { - "integration_removed": { - "title": "The Logi Circle integration has been deprecated and will be removed", - "description": "Logitech stopped accepting applications for access to the Logi Circle API in May 2022, and the Logi Circle integration will be removed from Home Assistant.\n\nTo resolve this issue, please remove the integration entries from your Home Assistant setup. [Click here to see your existing Logi Circle integration entries]({entries})." - } - }, - "services": { - "set_config": { - "name": "Set config", - "description": "Sets a configuration property.", - "fields": { - "entity_id": { - "name": "Entity", - "description": "Name(s) of entities to apply the operation mode to." - }, - "mode": { - "name": "[%key:common::config_flow::data::mode%]", - "description": "Operation mode. Allowed values: LED, RECORDING_MODE." - }, - "value": { - "name": "Value", - "description": "Operation value." - } - } - }, - "livestream_snapshot": { - "name": "Livestream snapshot", - "description": "Takes a snapshot from the camera's livestream. Will wake the camera from sleep if required.", - "fields": { - "entity_id": { - "name": "Entity", - "description": "Name(s) of entities to create snapshots from." - }, - "filename": { - "name": "File name", - "description": "Template of a Filename. Variable is entity_id." - } - } - }, - "livestream_record": { - "name": "Livestream record", - "description": "Takes a video recording from the camera's livestream.", - "fields": { - "entity_id": { - "name": "Entity", - "description": "Name(s) of entities to create recordings from." - }, - "filename": { - "name": "File name", - "description": "[%key:component::logi_circle::services::livestream_snapshot::fields::filename::description%]" - }, - "duration": { - "name": "Duration", - "description": "Recording duration." - } - } - } - } -} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 67cffd25f28..24e151d2902 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -325,7 +325,6 @@ FLOWS = { "local_ip", "local_todo", "locative", - "logi_circle", "lookin", "loqed", "luftdaten", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3d3344c1f60..1415ab51a75 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3339,12 +3339,6 @@ "config_flow": false, "iot_class": "cloud_push" }, - "logi_circle": { - "name": "Logi Circle", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_polling" - }, "logitech": { "name": "Logitech", "integrations": { diff --git a/requirements_all.txt b/requirements_all.txt index 76dce213ebf..1c4cd900cde 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1278,9 +1278,6 @@ lmcloud==1.1.13 # homeassistant.components.google_maps locationsharinglib==5.0.1 -# homeassistant.components.logi_circle -logi-circle==0.2.3 - # homeassistant.components.london_underground london-tube-status==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d88d5607050..e252e804be0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1056,9 +1056,6 @@ linear-garage-door==0.2.9 # homeassistant.components.lamarzocco lmcloud==1.1.13 -# homeassistant.components.logi_circle -logi-circle==0.2.3 - # homeassistant.components.london_underground london-tube-status==0.5 diff --git a/tests/components/logi_circle/__init__.py b/tests/components/logi_circle/__init__.py deleted file mode 100644 index d2e2fbb8fdb..00000000000 --- a/tests/components/logi_circle/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Logi Circle component.""" diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py deleted file mode 100644 index ab4bae02ad6..00000000000 --- a/tests/components/logi_circle/test_config_flow.py +++ /dev/null @@ -1,227 +0,0 @@ -"""Tests for Logi Circle config flow.""" - -import asyncio -from collections.abc import Generator -from http import HTTPStatus -from typing import Any -from unittest.mock import AsyncMock, MagicMock, Mock, patch - -import pytest - -from homeassistant import config_entries -from homeassistant.components.http import KEY_HASS -from homeassistant.components.logi_circle import config_flow -from homeassistant.components.logi_circle.config_flow import ( - DOMAIN, - AuthorizationFailed, - LogiCircleAuthCallbackView, -) -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import AbortFlow, FlowResultType -from homeassistant.setup import async_setup_component - -from tests.common import MockConfigEntry - - -class MockRequest: - """Mock request passed to HomeAssistantView.""" - - def __init__(self, hass: HomeAssistant, query: dict[str, Any]) -> None: - """Init request object.""" - self.app = {KEY_HASS: hass} - self.query = query - - -def init_config_flow(hass: HomeAssistant) -> config_flow.LogiCircleFlowHandler: - """Init a configuration flow.""" - config_flow.register_flow_implementation( - hass, - DOMAIN, - client_id="id", - client_secret="secret", - api_key="123", - redirect_uri="http://example.com", - sensors=None, - ) - flow = config_flow.LogiCircleFlowHandler() - flow._get_authorization_url = Mock(return_value="http://example.com") - flow.hass = hass - return flow - - -@pytest.fixture -def mock_logi_circle() -> Generator[MagicMock]: - """Mock logi_circle.""" - with patch( - "homeassistant.components.logi_circle.config_flow.LogiCircle" - ) as logi_circle: - future = asyncio.Future() - future.set_result({"accountId": "testId"}) - LogiCircle = logi_circle() - LogiCircle.authorize = AsyncMock(return_value=True) - LogiCircle.close = AsyncMock(return_value=True) - LogiCircle.account = future - LogiCircle.authorize_url = "http://authorize.url" - yield LogiCircle - - -@pytest.mark.usefixtures("mock_logi_circle") -async def test_step_import(hass: HomeAssistant) -> None: - """Test that we trigger import when configuring with client.""" - flow = init_config_flow(hass) - - result = await flow.async_step_import() - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - - -@pytest.mark.usefixtures("mock_logi_circle") -async def test_full_flow_implementation(hass: HomeAssistant) -> None: - """Test registering an implementation and finishing flow works.""" - config_flow.register_flow_implementation( - hass, - "test-other", - client_id=None, - client_secret=None, - api_key=None, - redirect_uri=None, - sensors=None, - ) - flow = init_config_flow(hass) - - result = await flow.async_step_user() - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - - result = await flow.async_step_user({"flow_impl": "test-other"}) - assert result["type"] is 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"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Logi Circle ({})".format("testId") - - -async def test_we_reprompt_user_to_follow_link(hass: HomeAssistant) -> None: - """Test we prompt user to follow link if previously prompted.""" - flow = init_config_flow(hass) - - result = await flow.async_step_auth("dummy") - assert result["errors"]["base"] == "follow_link" - - -async def test_abort_if_no_implementation_registered(hass: HomeAssistant) -> None: - """Test we abort if no implementation is registered.""" - flow = config_flow.LogiCircleFlowHandler() - flow.hass = hass - - result = await flow.async_step_user() - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "missing_configuration" - - -async def test_abort_if_already_setup(hass: HomeAssistant) -> None: - """Test we abort if Logi Circle is already setup.""" - flow = init_config_flow(hass) - MockConfigEntry(domain=config_flow.DOMAIN).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": config_entries.SOURCE_USER}, - ) - assert result["type"] is 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"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - with pytest.raises(AbortFlow): - result = await flow.async_step_code() - - result = await flow.async_step_auth() - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "external_setup" - - -@pytest.mark.parametrize( - ("side_effect", "error"), - [ - (TimeoutError, "authorize_url_timeout"), - (AuthorizationFailed, "invalid_auth"), - ], -) -async def test_abort_if_authorize_fails( - hass: HomeAssistant, - mock_logi_circle: MagicMock, - side_effect: type[Exception], - error: str, -) -> None: - """Test we abort if authorizing fails.""" - flow = init_config_flow(hass) - mock_logi_circle.authorize.side_effect = side_effect - - result = await flow.async_step_code("123ABC") - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "external_error" - - result = await flow.async_step_auth() - assert result["errors"]["base"] == error - - -async def test_not_pick_implementation_if_only_one(hass: HomeAssistant) -> None: - """Test we bypass picking implementation if we have one flow_imp.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user() - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - - -@pytest.mark.usefixtures("mock_logi_circle") -async def test_gen_auth_url(hass: HomeAssistant) -> None: - """Test generating authorize URL from Logi Circle API.""" - config_flow.register_flow_implementation( - hass, - "test-auth-url", - client_id="id", - client_secret="secret", - api_key="123", - redirect_uri="http://example.com", - sensors=None, - ) - flow = config_flow.LogiCircleFlowHandler() - flow.hass = hass - flow.flow_impl = "test-auth-url" - await async_setup_component(hass, "http", {}) - - result = flow._get_authorization_url() - assert result == "http://authorize.url" - - -async def test_callback_view_rejects_missing_code(hass: HomeAssistant) -> None: - """Test the auth callback view rejects requests with no code.""" - view = LogiCircleAuthCallbackView() - resp = await view.get(MockRequest(hass, {})) - - assert resp.status == HTTPStatus.BAD_REQUEST - - -async def test_callback_view_accepts_code( - hass: HomeAssistant, mock_logi_circle: MagicMock -) -> None: - """Test the auth callback view handles requests with auth code.""" - init_config_flow(hass) - view = LogiCircleAuthCallbackView() - - resp = await view.get(MockRequest(hass, {"code": "456"})) - assert resp.status == HTTPStatus.OK - - await hass.async_block_till_done() - mock_logi_circle.authorize.assert_called_with("456") diff --git a/tests/components/logi_circle/test_init.py b/tests/components/logi_circle/test_init.py deleted file mode 100644 index d953acdf744..00000000000 --- a/tests/components/logi_circle/test_init.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Tests for the Logi Circle integration.""" - -import asyncio -from collections.abc import Generator -from unittest.mock import AsyncMock, MagicMock, Mock, patch - -import pytest - -from homeassistant.components.logi_circle import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir - -from tests.common import MockConfigEntry - - -@pytest.fixture(name="disable_platforms") -def disable_platforms_fixture() -> Generator[None]: - """Disable logi_circle platforms.""" - with patch("homeassistant.components.logi_circle.PLATFORMS", []): - yield - - -@pytest.fixture -def mock_logi_circle() -> Generator[MagicMock]: - """Mock logi_circle.""" - - auth_provider_mock = Mock() - auth_provider_mock.close = AsyncMock() - auth_provider_mock.clear_authorization = AsyncMock() - - with patch("homeassistant.components.logi_circle.LogiCircle") as logi_circle: - future = asyncio.Future() - future.set_result({"accountId": "testId"}) - LogiCircle = logi_circle() - LogiCircle.auth_provider = auth_provider_mock - LogiCircle.synchronize_cameras = AsyncMock() - yield LogiCircle - - -@pytest.mark.usefixtures("disable_platforms", "mock_logi_circle") -async def test_repair_issue( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, -) -> None: - """Test the LogiCircle configuration entry loading/unloading handles the repair.""" - config_entry = MockConfigEntry( - title="Example 1", - domain=DOMAIN, - data={ - "api_key": "blah", - "client_id": "blah", - "client_secret": "blah", - "redirect_uri": "blah", - }, - ) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.LOADED - assert issue_registry.async_get_issue(DOMAIN, DOMAIN) - - # Remove the entry - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.NOT_LOADED - assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None From d8b13c8c02ae79e7fa763ec17754fe35fd4f1e76 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Aug 2024 21:31:42 +0200 Subject: [PATCH 2204/2411] Remove deprecated yaml import for gpsd (#123725) --- homeassistant/components/gpsd/config_flow.py | 4 -- homeassistant/components/gpsd/sensor.py | 60 +------------------- tests/components/gpsd/test_config_flow.py | 22 +------ 3 files changed, 4 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/gpsd/config_flow.py b/homeassistant/components/gpsd/config_flow.py index 59c95d0ddbf..ac41324f857 100644 --- a/homeassistant/components/gpsd/config_flow.py +++ b/homeassistant/components/gpsd/config_flow.py @@ -39,10 +39,6 @@ class GPSDConfigFlow(ConfigFlow, domain=DOMAIN): else: return True - async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: - """Import a config entry from configuration.yaml.""" - return await self.async_step_user(import_data) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index e67287ae134..63f8ac4f28c 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -7,35 +7,17 @@ from dataclasses import dataclass import logging from typing import Any -from gps3.agps3threaded import ( - GPSD_PORT as DEFAULT_PORT, - HOST as DEFAULT_HOST, - AGPS3mechanism, -) -import voluptuous as vol +from gps3.agps3threaded import AGPS3mechanism from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - ATTR_LATITUDE, - ATTR_LONGITUDE, - ATTR_MODE, - CONF_HOST, - CONF_NAME, - CONF_PORT, - EntityCategory, -) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -import homeassistant.helpers.config_validation as cv +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_MODE, EntityCategory +from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import GPSDConfigEntry from .const import DOMAIN @@ -71,14 +53,6 @@ SENSOR_TYPES: tuple[GpsdSensorDescription, ...] = ( ), ) -PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } -) - async def async_setup_entry( hass: HomeAssistant, @@ -98,34 +72,6 @@ async def async_setup_entry( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Initialize gpsd import from config.""" - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - is_fixable=False, - breaks_in_ha_version="2024.9.0", - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "GPSD", - }, - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - - class GpsdSensor(SensorEntity): """Representation of a GPS receiver available via GPSD.""" diff --git a/tests/components/gpsd/test_config_flow.py b/tests/components/gpsd/test_config_flow.py index 2d68a704119..4d832e120e4 100644 --- a/tests/components/gpsd/test_config_flow.py +++ b/tests/components/gpsd/test_config_flow.py @@ -6,7 +6,7 @@ from gps3.agps3threaded import GPSD_PORT as DEFAULT_PORT from homeassistant import config_entries from homeassistant.components.gpsd.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -52,23 +52,3 @@ async def test_connection_error(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" - - -async def test_import(hass: HomeAssistant) -> None: - """Test import step.""" - with patch("homeassistant.components.gpsd.config_flow.socket") as mock_socket: - mock_connect = mock_socket.return_value.connect - mock_connect.return_value = None - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: HOST, CONF_PORT: 1234, CONF_NAME: "MyGPS"}, - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "MyGPS" - assert result["data"] == { - CONF_HOST: HOST, - CONF_NAME: "MyGPS", - CONF_PORT: 1234, - } From f46fe7eeb2c6ed898c43297cd8269e48ef2f9825 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Aug 2024 21:32:28 +0200 Subject: [PATCH 2205/2411] Remove deprecated yaml import for velux (#123724) --- homeassistant/components/velux/__init__.py | 36 +----------- homeassistant/components/velux/config_flow.py | 57 ------------------- homeassistant/components/velux/strings.json | 10 ---- tests/components/velux/test_config_flow.py | 40 ++----------- 4 files changed, 5 insertions(+), 138 deletions(-) diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 1b7cbd1ff93..614ed810429 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -1,48 +1,14 @@ """Support for VELUX KLF 200 devices.""" from pyvlx import Node, PyVLX, PyVLXException -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, ServiceCall, callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, LOGGER, PLATFORMS -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the velux component.""" - if DOMAIN not in config: - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config[DOMAIN], - ) - ) - - return True - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the velux component.""" diff --git a/homeassistant/components/velux/config_flow.py b/homeassistant/components/velux/config_flow.py index c0d4ec8035b..f4bfa13b4d5 100644 --- a/homeassistant/components/velux/config_flow.py +++ b/homeassistant/components/velux/config_flow.py @@ -1,15 +1,11 @@ """Config flow for Velux integration.""" -from typing import Any - from pyvlx import PyVLX, PyVLXException import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import DOMAIN, LOGGER @@ -24,59 +20,6 @@ DATA_SCHEMA = vol.Schema( class VeluxConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for velux.""" - async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult: - """Import a config entry.""" - - def create_repair(error: str | None = None) -> None: - if error: - async_create_issue( - self.hass, - DOMAIN, - f"deprecated_yaml_import_issue_{error}", - breaks_in_ha_version="2024.9.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_{error}", - ) - else: - async_create_issue( - self.hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.9.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Velux", - }, - ) - - for entry in self._async_current_entries(): - if entry.data[CONF_HOST] == config[CONF_HOST]: - create_repair() - return self.async_abort(reason="already_configured") - - pyvlx = PyVLX(host=config[CONF_HOST], password=config[CONF_PASSWORD]) - try: - await pyvlx.connect() - await pyvlx.disconnect() - except (PyVLXException, ConnectionError): - create_repair("cannot_connect") - return self.async_abort(reason="cannot_connect") - except Exception: # noqa: BLE001 - create_repair("unknown") - return self.async_abort(reason="unknown") - - create_repair() - return self.async_create_entry( - title=config[CONF_HOST], - data=config, - ) - async def async_step_user( self, user_input: dict[str, str] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/velux/strings.json b/homeassistant/components/velux/strings.json index 3964c22efe2..5b7b459a3f7 100644 --- a/homeassistant/components/velux/strings.json +++ b/homeassistant/components/velux/strings.json @@ -17,16 +17,6 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, - "issues": { - "deprecated_yaml_import_issue_cannot_connect": { - "title": "The Velux YAML configuration import cannot connect to server", - "description": "Configuring Velux using YAML is being removed but there was an connection error importing your YAML configuration.\n\nMake sure your home assistant can reach the KLF 200." - }, - "deprecated_yaml_import_issue_unknown": { - "title": "The Velux YAML configuration import failed with unknown error raised by pyvlx", - "description": "Configuring Velux using YAML is being removed but there was an unknown error importing your YAML configuration.\n\nCheck your configuration or have a look at the documentation of the integration." - } - }, "services": { "reboot_gateway": { "name": "Reboot gateway", diff --git a/tests/components/velux/test_config_flow.py b/tests/components/velux/test_config_flow.py index 8021ad52810..5f7932d358a 100644 --- a/tests/components/velux/test_config_flow.py +++ b/tests/components/velux/test_config_flow.py @@ -10,7 +10,7 @@ import pytest from pyvlx import PyVLXException from homeassistant.components.velux import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -69,22 +69,8 @@ async def test_user_errors( assert result["errors"] == {"base": error_name} -async def test_import_valid_config(hass: HomeAssistant) -> None: - """Test import initialized flow with valid config.""" - with patch(PYVLX_CONFIG_FLOW_CLASS_PATH, autospec=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=DUMMY_DATA, - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == DUMMY_DATA[CONF_HOST] - assert result["data"] == DUMMY_DATA - - -@pytest.mark.parametrize("flow_source", [SOURCE_IMPORT, SOURCE_USER]) -async def test_flow_duplicate_entry(hass: HomeAssistant, flow_source: str) -> None: - """Test import initialized flow with a duplicate entry.""" +async def test_flow_duplicate_entry(hass: HomeAssistant) -> None: + """Test initialized flow with a duplicate entry.""" with patch(PYVLX_CONFIG_FLOW_CLASS_PATH, autospec=True): conf_entry: MockConfigEntry = MockConfigEntry( domain=DOMAIN, title=DUMMY_DATA[CONF_HOST], data=DUMMY_DATA @@ -94,26 +80,8 @@ async def test_flow_duplicate_entry(hass: HomeAssistant, flow_source: str) -> No result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": flow_source}, + context={"source": SOURCE_USER}, data=DUMMY_DATA, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - - -@pytest.mark.parametrize(("error", "error_name"), error_types_to_test) -async def test_import_errors( - hass: HomeAssistant, error: Exception, error_name: str -) -> None: - """Test import initialized flow with exceptions.""" - with patch( - PYVLX_CONFIG_FLOW_CONNECT_FUNCTION_PATH, - side_effect=error, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=DUMMY_DATA, - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == error_name From d1dff95ac897200ce3f280bd3a3e8f6f8a34a005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 12 Aug 2024 21:33:56 +0200 Subject: [PATCH 2206/2411] Update aioairzone to v0.8.2 (#123718) --- 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 0c32787d8ae..31ff7423ad6 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.8.1"] + "requirements": ["aioairzone==0.8.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1c4cd900cde..7f947776f09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -179,7 +179,7 @@ aioairq==0.3.2 aioairzone-cloud==0.6.2 # homeassistant.components.airzone -aioairzone==0.8.1 +aioairzone==0.8.2 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e252e804be0..b4f9fc0b103 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -167,7 +167,7 @@ aioairq==0.3.2 aioairzone-cloud==0.6.2 # homeassistant.components.airzone -aioairzone==0.8.1 +aioairzone==0.8.2 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 178cb0659a5f6cb969526c996b09c75250e753da Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Aug 2024 21:35:02 +0200 Subject: [PATCH 2207/2411] Guard for no discovery info in command_line (#123717) --- .../components/command_line/binary_sensor.py | 2 ++ homeassistant/components/command_line/cover.py | 2 ++ .../components/command_line/notify.py | 4 +++- .../components/command_line/sensor.py | 2 ++ .../components/command_line/switch.py | 2 +- .../command_line/test_binary_sensor.py | 18 ++++++++++++++++++ tests/components/command_line/test_cover.py | 18 ++++++++++++++++++ tests/components/command_line/test_notify.py | 18 ++++++++++++++++++ tests/components/command_line/test_sensor.py | 18 ++++++++++++++++++ tests/components/command_line/test_switch.py | 18 ++++++++++++++++++ 10 files changed, 100 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index 20deddcf14e..f5d9ad9d63d 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -40,6 +40,8 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Command line Binary Sensor.""" + if not discovery_info: + return discovery_info = cast(DiscoveryInfoType, discovery_info) binary_sensor_config = discovery_info diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 2bcbb610296..d848237467b 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -37,6 +37,8 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up cover controlled by shell commands.""" + if not discovery_info: + return covers = [] discovery_info = cast(DiscoveryInfoType, discovery_info) diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 14245b72288..4f5a4e4b499 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -21,8 +21,10 @@ def get_service( hass: HomeAssistant, config: ConfigType, discovery_info: DiscoveryInfoType | None = None, -) -> CommandLineNotificationService: +) -> CommandLineNotificationService | None: """Get the Command Line notification service.""" + if not discovery_info: + return None discovery_info = cast(DiscoveryInfoType, discovery_info) notify_config = discovery_info diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 786afc8f3a7..7c31af165f9 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -48,6 +48,8 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Command Sensor.""" + if not discovery_info: + return discovery_info = cast(DiscoveryInfoType, discovery_info) sensor_config = discovery_info diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 33b38ab9115..6d4670106ba 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -36,9 +36,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Find and return switches controlled by shell commands.""" - if not discovery_info: return + switches = [] discovery_info = cast(DiscoveryInfoType, discovery_info) entities: dict[str, dict[str, Any]] = { diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index fd726ab77a4..5d1cd845e27 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -56,6 +56,24 @@ async def test_setup_integration_yaml( assert entity_state.name == "Test" +async def test_setup_platform_yaml(hass: HomeAssistant) -> None: + """Test setting up the platform with platform yaml.""" + await setup.async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "command_line", + "command": "echo 1", + "payload_on": "1", + "payload_off": "0", + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + @pytest.mark.parametrize( "get_config", [ diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 7ed48909d79..b81d915c6d5 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -36,6 +36,24 @@ from . import mock_asyncio_subprocess_run from tests.common import async_fire_time_changed +async def test_setup_platform_yaml(hass: HomeAssistant) -> None: + """Test setting up the platform with platform yaml.""" + await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "command_line", + "command": "echo 1", + "payload_on": "1", + "payload_off": "0", + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + async def test_no_poll_when_cover_has_no_command_state(hass: HomeAssistant) -> None: """Test that the cover does not polls when there's no state command.""" diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index c775d87fedb..6898b44f062 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -16,6 +16,24 @@ from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.core import HomeAssistant +async def test_setup_platform_yaml(hass: HomeAssistant) -> None: + """Test setting up the platform with platform yaml.""" + await setup.async_setup_component( + hass, + "notify", + { + "notify": { + "platform": "command_line", + "command": "echo 1", + "payload_on": "1", + "payload_off": "0", + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + @pytest.mark.parametrize( "get_config", [ diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index eeccf2c358e..f7879b334cd 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -27,6 +27,24 @@ from . import mock_asyncio_subprocess_run from tests.common import async_fire_time_changed +async def test_setup_platform_yaml(hass: HomeAssistant) -> None: + """Test setting up the platform with platform yaml.""" + await setup.async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "command_line", + "command": "echo 1", + "payload_on": "1", + "payload_off": "0", + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + @pytest.mark.parametrize( "get_config", [ diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index c464ded34fb..549e729892c 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -37,6 +37,24 @@ from . import mock_asyncio_subprocess_run from tests.common import async_fire_time_changed +async def test_setup_platform_yaml(hass: HomeAssistant) -> None: + """Test setting up the platform with platform yaml.""" + await setup.async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "command_line", + "command": "echo 1", + "payload_on": "1", + "payload_off": "0", + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + async def test_state_integration_yaml(hass: HomeAssistant) -> None: """Test with none state.""" with tempfile.TemporaryDirectory() as tempdirname: From b09c6654ecda68711d7f3cefe984b3e7d6c7138f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Aug 2024 21:42:00 +0200 Subject: [PATCH 2208/2411] Replace not needed guard in command_line with type check (#123722) --- homeassistant/components/command_line/cover.py | 10 +++++----- homeassistant/components/command_line/switch.py | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index d848237467b..8ddfd399ba8 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from homeassistant.components.cover import CoverEntity from homeassistant.const import ( @@ -141,10 +141,10 @@ class CommandCover(ManualTriggerEntity, CoverEntity): async def _async_query_state(self) -> str | None: """Query for the state.""" - if self._command_state: - LOGGER.debug("Running state value command: %s", self._command_state) - return await async_check_output_or_log(self._command_state, self._timeout) - return None + if TYPE_CHECKING: + assert self._command_state + LOGGER.debug("Running state value command: %s", self._command_state) + return await async_check_output_or_log(self._command_state, self._timeout) async def _update_entity_state(self, now: datetime | None = None) -> None: """Update the state of the entity.""" diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 6d4670106ba..e42c2226cf2 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity from homeassistant.const import ( @@ -142,11 +142,11 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity): async def _async_query_state(self) -> str | int | None: """Query for state.""" - if self._command_state: - if self._value_template: - return await self._async_query_state_value(self._command_state) - return await self._async_query_state_code(self._command_state) - return None + if TYPE_CHECKING: + assert self._command_state + if self._value_template: + return await self._async_query_state_value(self._command_state) + return await self._async_query_state_code(self._command_state) async def _update_entity_state(self, now: datetime | None = None) -> None: """Update the state of the entity.""" From 7cf5d12ec09d075733ee5c5bd917d27049affc88 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Mon, 12 Aug 2024 15:45:05 -0400 Subject: [PATCH 2209/2411] Fix secondary russound controller discovery failure (#123590) --- .../components/russound_rio/manifest.json | 2 +- .../components/russound_rio/media_player.py | 17 ++++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index e7bb99010ee..67a01239615 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/russound_rio", "iot_class": "local_push", "loggers": ["aiorussound"], - "requirements": ["aiorussound==2.2.2"] + "requirements": ["aiorussound==2.2.3"] } diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 1489f12e59c..ff0d9e006c0 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -128,11 +128,18 @@ class RussoundZoneDevice(MediaPlayerEntity): self._zone = zone self._sources = sources self._attr_name = zone.name - self._attr_unique_id = f"{self._controller.mac_address}-{zone.device_str()}" + primary_mac_address = ( + self._controller.mac_address + or self._controller.parent_controller.mac_address + ) + self._attr_unique_id = f"{primary_mac_address}-{zone.device_str()}" + device_identifier = ( + self._controller.mac_address + or f"{primary_mac_address}-{self._controller.controller_id}" + ) self._attr_device_info = DeviceInfo( # Use MAC address of Russound device as identifier - identifiers={(DOMAIN, self._controller.mac_address)}, - connections={(CONNECTION_NETWORK_MAC, self._controller.mac_address)}, + identifiers={(DOMAIN, device_identifier)}, manufacturer="Russound", name=self._controller.controller_type, model=self._controller.controller_type, @@ -143,6 +150,10 @@ class RussoundZoneDevice(MediaPlayerEntity): DOMAIN, self._controller.parent_controller.mac_address, ) + else: + self._attr_device_info["connections"] = { + (CONNECTION_NETWORK_MAC, self._controller.mac_address) + } for flag, feature in MP_FEATURES_BY_FLAG.items(): if flag in zone.instance.supported_features: self._attr_supported_features |= feature diff --git a/requirements_all.txt b/requirements_all.txt index 7f947776f09..f6106738de4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.2.2 +aiorussound==2.2.3 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4f9fc0b103..82f62a21a52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -332,7 +332,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.2.2 +aiorussound==2.2.3 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 From 3c864322f7dfa68f4c4a07bf656611de9701edc1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Aug 2024 21:49:10 +0200 Subject: [PATCH 2210/2411] Combine requirements files in CI (#123687) --- .github/workflows/ci.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3df1bf5b6aa..0f0850ade1a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ on: type: boolean env: - CACHE_VERSION: 9 + CACHE_VERSION: 10 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 8 HA_SHORT_VERSION: "2024.9" @@ -514,8 +514,7 @@ jobs: uv pip install -U "pip>=21.3.1" setuptools wheel uv pip install -r requirements.txt python -m script.gen_requirements_all ci - uv pip install -r requirements_all_pytest.txt - uv pip install -r requirements_test.txt + uv pip install -r requirements_all_pytest.txt -r requirements_test.txt uv pip install -e . --config-settings editable_mode=compat hassfest: From b62f216c53ee832f4eeebe6ae74f3803c03b727f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 21:49:46 +0200 Subject: [PATCH 2211/2411] Remove unnecessary assignment of Template.hass from telnet (#123694) --- homeassistant/components/telnet/switch.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index 3b4b9e137d1..82d8905a775 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -67,11 +67,6 @@ def setup_platform( switches = [] for object_id, device_config in devices.items(): - value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE) - - if value_template is not None: - value_template.hass = hass - switches.append( TelnetSwitch( object_id, @@ -81,7 +76,7 @@ def setup_platform( device_config[CONF_COMMAND_ON], device_config[CONF_COMMAND_OFF], device_config.get(CONF_COMMAND_STATE), - value_template, + device_config.get(CONF_VALUE_TEMPLATE), device_config[CONF_TIMEOUT], ) ) From 9d67956fc83d9fccd03f84f5de25f3664880b049 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 21:49:58 +0200 Subject: [PATCH 2212/2411] Remove unnecessary assignment of Template.hass from tensorflow (#123695) --- homeassistant/components/tensorflow/image_processing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 85fe6439f1c..f13c0b24d0b 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -261,8 +261,6 @@ class TensorFlowImageProcessor(ImageProcessingEntity): area_config.get(CONF_RIGHT), ] - template.attach(hass, self._file_out) - self._matches = {} self._total_matches = 0 self._last_image = None From bf55cc605ac31c5e4607747988695fc32c25dd7c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 21:50:11 +0200 Subject: [PATCH 2213/2411] Remove unnecessary assignment of Template.hass from velbus (#123696) --- homeassistant/components/velbus/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index d47444e3994..685f8b49500 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -119,7 +119,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def set_memo_text(call: ServiceCall) -> None: """Handle Memo Text service call.""" memo_text = call.data[CONF_MEMO_TEXT] - memo_text.hass = hass await ( hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"] .get_module(call.data[CONF_ADDRESS]) From 6bde80ad65f6ef39d2ee7ec0a0a4b25bae1a5d11 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 21:50:23 +0200 Subject: [PATCH 2214/2411] Remove unnecessary assignment of Template.hass from esphome (#123701) --- homeassistant/components/esphome/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 4b4537d147f..7629d1fa9cd 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -197,9 +197,9 @@ class ESPHomeManager: if service.data_template: try: data_template = { - key: Template(value) for key, value in service.data_template.items() + key: Template(value, hass) + for key, value in service.data_template.items() } - template.attach(hass, data_template) service_data.update( template.render_complex(data_template, service.variables) ) From 5e75c5faff9fc4f029496571b6c92dfae0f6f8ac Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 21:50:34 +0200 Subject: [PATCH 2215/2411] Remove unnecessary assignment of Template.hass from mobile_app (#123702) --- homeassistant/components/mobile_app/device_action.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/mobile_app/device_action.py b/homeassistant/components/mobile_app/device_action.py index bebdef0e917..dccff926b34 100644 --- a/homeassistant/components/mobile_app/device_action.py +++ b/homeassistant/components/mobile_app/device_action.py @@ -64,7 +64,6 @@ async def async_call_action_from_config( continue value_template = config[key] - template.attach(hass, value_template) try: service_data[key] = template.render_complex(value_template, variables) From 6caec897931ad03bc2982599be3512b18c027708 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 21:50:50 +0200 Subject: [PATCH 2216/2411] Remove unnecessary assignment of Template.hass from trigger entity helper (#123709) --- homeassistant/helpers/trigger_template_entity.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/helpers/trigger_template_entity.py b/homeassistant/helpers/trigger_template_entity.py index 7b1c4ab8078..7f8ad41d7bb 100644 --- a/homeassistant/helpers/trigger_template_entity.py +++ b/homeassistant/helpers/trigger_template_entity.py @@ -30,7 +30,7 @@ from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads from . import config_validation as cv from .entity import Entity -from .template import attach as template_attach, render_complex +from .template import render_complex from .typing import ConfigType CONF_AVAILABILITY = "availability" @@ -157,11 +157,6 @@ class TriggerBaseEntity(Entity): """Return extra attributes.""" return self._rendered.get(CONF_ATTRIBUTES) - async def async_added_to_hass(self) -> None: - """Handle being added to Home Assistant.""" - await super().async_added_to_hass() - template_attach(self.hass, self._config) - def _set_unique_id(self, unique_id: str | None) -> None: """Set unique id.""" self._unique_id = unique_id From c5e87108895995542c8e71869b8bef048b177b29 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 21:51:02 +0200 Subject: [PATCH 2217/2411] Remove unnecessary assignment of Template.hass from service helper (#123710) --- homeassistant/helpers/service.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 58cd4657301..be4974906bb 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -365,7 +365,6 @@ def async_prepare_call_from_config( if isinstance(domain_service, template.Template): try: - domain_service.hass = hass domain_service = domain_service.async_render(variables) domain_service = cv.service(domain_service) except TemplateError as ex: @@ -384,10 +383,8 @@ def async_prepare_call_from_config( conf = config[CONF_TARGET] try: if isinstance(conf, template.Template): - conf.hass = hass target.update(conf.async_render(variables)) else: - template.attach(hass, conf) target.update(template.render_complex(conf, variables)) if CONF_ENTITY_ID in target: @@ -413,7 +410,6 @@ def async_prepare_call_from_config( if conf not in config: continue try: - template.attach(hass, config[conf]) render = template.render_complex(config[conf], variables) if not isinstance(render, dict): raise HomeAssistantError( From c49a31e0deb10409f1e28226b008a57ddfddea77 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Aug 2024 21:51:24 +0200 Subject: [PATCH 2218/2411] Remove unnecessary assignment of Template.hass from script variables helper (#123712) --- homeassistant/helpers/script_variables.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/helpers/script_variables.py b/homeassistant/helpers/script_variables.py index 043101b9b86..2b4507abd64 100644 --- a/homeassistant/helpers/script_variables.py +++ b/homeassistant/helpers/script_variables.py @@ -36,7 +36,6 @@ class ScriptVariables: """ if self._has_template is None: self._has_template = template.is_complex(self.variables) - template.attach(hass, self.variables) if not self._has_template: if render_as_defaults: From 31dcc6f685778fd1630d15344121193e192ad2ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Aug 2024 14:51:45 -0500 Subject: [PATCH 2219/2411] Bump protobuf to 4.25.4 (#123675) --- homeassistant/package_constraints.txt | 2 +- script/gen_requirements_all.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6c93e6b5f1c..8cad4d2037a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -142,7 +142,7 @@ pyOpenSSL>=24.0.0 # protobuf must be in package constraints for the wheel # builder to build binary wheels -protobuf==4.25.1 +protobuf==4.25.4 # faust-cchardet: Ensure we have a version we can build wheels # 2.1.18 is the first version that works with our wheel builder diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index f9bd379b7ce..522a626754d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -164,7 +164,7 @@ pyOpenSSL>=24.0.0 # protobuf must be in package constraints for the wheel # builder to build binary wheels -protobuf==4.25.1 +protobuf==4.25.4 # faust-cchardet: Ensure we have a version we can build wheels # 2.1.18 is the first version that works with our wheel builder From b0d1d7bdb23376f5e9f0a5f597c337130bc3a7b8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Aug 2024 21:53:32 +0200 Subject: [PATCH 2220/2411] Improve type hints in lcn tests (#123648) --- tests/components/lcn/conftest.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py index 2884bc833c2..24447abf77a 100644 --- a/tests/components/lcn/conftest.py +++ b/tests/components/lcn/conftest.py @@ -1,5 +1,6 @@ """Test configuration and mocks for LCN component.""" +from collections.abc import AsyncGenerator import json from unittest.mock import AsyncMock, patch @@ -10,8 +11,9 @@ from pypck.module import GroupConnection, ModuleConnection import pytest from homeassistant.components.lcn.const import DOMAIN -from homeassistant.components.lcn.helpers import generate_unique_id +from homeassistant.components.lcn.helpers import AddressType, generate_unique_id from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -42,13 +44,13 @@ class MockGroupConnection(GroupConnection): class MockPchkConnectionManager(PchkConnectionManager): """Fake connection handler.""" - async def async_connect(self, timeout=30): + async def async_connect(self, timeout: int = 30) -> None: """Mock establishing a connection to PCHK.""" self.authentication_completed_future.set_result(True) self.license_error_future.set_result(True) self.segment_scan_completed_event.set() - async def async_close(self): + async def async_close(self) -> None: """Mock closing a connection to PCHK.""" @patch.object(pypck.connection, "ModuleConnection", MockModuleConnection) @@ -60,7 +62,7 @@ class MockPchkConnectionManager(PchkConnectionManager): send_command = AsyncMock() -def create_config_entry(name): +def create_config_entry(name: str) -> MockConfigEntry: """Set up config entries with configuration data.""" fixture_filename = f"lcn/config_entry_{name}.json" entry_data = json.loads(load_fixture(fixture_filename)) @@ -78,19 +80,21 @@ def create_config_entry(name): @pytest.fixture(name="entry") -def create_config_entry_pchk(): +def create_config_entry_pchk() -> MockConfigEntry: """Return one specific config entry.""" return create_config_entry("pchk") @pytest.fixture(name="entry2") -def create_config_entry_myhome(): +def create_config_entry_myhome() -> MockConfigEntry: """Return one specific config entry.""" return create_config_entry("myhome") @pytest.fixture(name="lcn_connection") -async def init_integration(hass, entry): +async def init_integration( + hass: HomeAssistant, entry: MockConfigEntry +) -> AsyncGenerator[MockPchkConnectionManager]: """Set up the LCN integration in Home Assistant.""" lcn_connection = None @@ -109,7 +113,7 @@ async def init_integration(hass, entry): yield lcn_connection -async def setup_component(hass): +async def setup_component(hass: HomeAssistant) -> None: """Set up the LCN component.""" fixture_filename = "lcn/config.json" config_data = json.loads(load_fixture(fixture_filename)) @@ -118,7 +122,9 @@ async def setup_component(hass): await hass.async_block_till_done() -def get_device(hass, entry, address): +def get_device( + hass: HomeAssistant, entry: MockConfigEntry, address: AddressType +) -> dr.DeviceEntry: """Get LCN device for specified address.""" device_registry = dr.async_get(hass) identifiers = {(DOMAIN, generate_unique_id(entry.entry_id, address))} From 416d2fb82a2e09a337cafbc857ac81a7ab10222b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 Aug 2024 21:55:44 +0200 Subject: [PATCH 2221/2411] Improve type hints in locative tests (#123643) --- tests/components/locative/test_init.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 305497ebbd6..8fd239ee398 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -38,7 +38,7 @@ async def locative_client( @pytest.fixture -async def webhook_id(hass, locative_client): +async def webhook_id(hass: HomeAssistant, locative_client: TestClient) -> str: """Initialize the Geofency component and get the webhook_id.""" await async_process_ha_core_config( hass, @@ -56,7 +56,7 @@ async def webhook_id(hass, locative_client): return result["result"].data["webhook_id"] -async def test_missing_data(locative_client, webhook_id) -> None: +async def test_missing_data(locative_client: TestClient, webhook_id: str) -> None: """Test missing data.""" url = f"/api/webhook/{webhook_id}" @@ -116,7 +116,9 @@ async def test_missing_data(locative_client, webhook_id) -> None: assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY -async def test_enter_and_exit(hass: HomeAssistant, locative_client, webhook_id) -> None: +async def test_enter_and_exit( + hass: HomeAssistant, locative_client: TestClient, webhook_id: str +) -> None: """Test when there is a known zone.""" url = f"/api/webhook/{webhook_id}" @@ -186,7 +188,7 @@ async def test_enter_and_exit(hass: HomeAssistant, locative_client, webhook_id) async def test_exit_after_enter( - hass: HomeAssistant, locative_client, webhook_id + hass: HomeAssistant, locative_client: TestClient, webhook_id: str ) -> None: """Test when an exit message comes after an enter message.""" url = f"/api/webhook/{webhook_id}" @@ -229,7 +231,9 @@ async def test_exit_after_enter( assert state.state == "work" -async def test_exit_first(hass: HomeAssistant, locative_client, webhook_id) -> None: +async def test_exit_first( + hass: HomeAssistant, locative_client: TestClient, webhook_id: str +) -> None: """Test when an exit message is sent first on a new device.""" url = f"/api/webhook/{webhook_id}" @@ -250,7 +254,9 @@ async def test_exit_first(hass: HomeAssistant, locative_client, webhook_id) -> N assert state.state == "not_home" -async def test_two_devices(hass: HomeAssistant, locative_client, webhook_id) -> None: +async def test_two_devices( + hass: HomeAssistant, locative_client: TestClient, webhook_id: str +) -> None: """Test updating two different devices.""" url = f"/api/webhook/{webhook_id}" @@ -294,7 +300,7 @@ async def test_two_devices(hass: HomeAssistant, locative_client, webhook_id) -> reason="The device_tracker component does not support unloading yet." ) async def test_load_unload_entry( - hass: HomeAssistant, locative_client, webhook_id + hass: HomeAssistant, locative_client: TestClient, webhook_id: str ) -> None: """Test that the appropriate dispatch signals are added and removed.""" url = f"/api/webhook/{webhook_id}" From 52f52394d5f1888f1f03f7ed74a1a30e415f69ac Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Aug 2024 22:36:36 +0200 Subject: [PATCH 2222/2411] Remove demo mailbox (#123741) --- homeassistant/components/demo/__init__.py | 1 - homeassistant/components/demo/mailbox.py | 95 ----------------------- 2 files changed, 96 deletions(-) delete mode 100644 homeassistant/components/demo/mailbox.py diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 371b783b653..d088dfb140b 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -55,7 +55,6 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ COMPONENTS_WITH_DEMO_PLATFORM = [ Platform.TTS, - Platform.MAILBOX, Platform.IMAGE_PROCESSING, Platform.DEVICE_TRACKER, ] diff --git a/homeassistant/components/demo/mailbox.py b/homeassistant/components/demo/mailbox.py deleted file mode 100644 index e0cdd05782d..00000000000 --- a/homeassistant/components/demo/mailbox.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Support for a demo mailbox.""" - -from __future__ import annotations - -from hashlib import sha1 -import logging -import os -from typing import Any - -from homeassistant.components.mailbox import CONTENT_TYPE_MPEG, Mailbox, StreamError -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import dt as dt_util - -_LOGGER = logging.getLogger(__name__) - -MAILBOX_NAME = "DemoMailbox" - - -async def async_get_handler( - hass: HomeAssistant, - config: ConfigType, - discovery_info: DiscoveryInfoType | None = None, -) -> Mailbox: - """Set up the Demo mailbox.""" - return DemoMailbox(hass, MAILBOX_NAME) - - -class DemoMailbox(Mailbox): - """Demo Mailbox.""" - - def __init__(self, hass: HomeAssistant, name: str) -> None: - """Initialize Demo mailbox.""" - super().__init__(hass, name) - self._messages: dict[str, dict[str, Any]] = {} - txt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - for idx in range(10): - msgtime = int( - dt_util.as_timestamp(dt_util.utcnow()) - 3600 * 24 * (10 - idx) - ) - msgtxt = f"Message {idx + 1}. {txt * (1 + idx * (idx % 2))}" - msgsha = sha1(msgtxt.encode("utf-8")).hexdigest() - msg = { - "info": { - "origtime": msgtime, - "callerid": "John Doe <212-555-1212>", - "duration": "10", - }, - "text": msgtxt, - "sha": msgsha, - } - self._messages[msgsha] = msg - - @property - def media_type(self) -> str: - """Return the supported media type.""" - return CONTENT_TYPE_MPEG - - @property - def can_delete(self) -> bool: - """Return if messages can be deleted.""" - return True - - @property - def has_media(self) -> bool: - """Return if messages have attached media files.""" - return True - - def _get_media(self) -> bytes: - """Return the media blob for the msgid.""" - audio_path = os.path.join(os.path.dirname(__file__), "tts.mp3") - with open(audio_path, "rb") as file: - return file.read() - - async def async_get_media(self, msgid: str) -> bytes: - """Return the media blob for the msgid.""" - if msgid not in self._messages: - raise StreamError("Message not found") - return await self.hass.async_add_executor_job(self._get_media) - - async def async_get_messages(self) -> list[dict[str, Any]]: - """Return a list of the current messages.""" - return sorted( - self._messages.values(), - key=lambda item: item["info"]["origtime"], - reverse=True, - ) - - async def async_delete(self, msgid: str) -> bool: - """Delete the specified messages.""" - if msgid in self._messages: - _LOGGER.info("Deleting: %s", msgid) - del self._messages[msgid] - self.async_update() - return True From 732b4b95dba76ac547bd71c8d08c9bb0df1f9acd Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 12 Aug 2024 16:38:59 -0400 Subject: [PATCH 2223/2411] Bump ZHA lib to 0.0.31 (#123743) --- homeassistant/components/zha/entity.py | 2 +- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 6db0ffad964..348e545f1c4 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -62,7 +62,7 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity): @property def available(self) -> bool: """Return entity availability.""" - return self.entity_data.device_proxy.device.available + return self.entity_data.entity.available @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index bb1480b43e1..a5e57fcb1ec 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.30"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.31"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index f6106738de4..eb79831820c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2986,7 +2986,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.30 +zha==0.0.31 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82f62a21a52..43764d7518b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2363,7 +2363,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.30 +zha==0.0.31 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 From 831c28e890b1aa89e5c689ae9f22e7dbefee1af1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Aug 2024 15:40:35 -0500 Subject: [PATCH 2224/2411] Bump yalexs to 6.5.0 (#123739) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 293c94c9629..5a911eee5e5 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==6.4.3", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==6.5.0", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index eb79831820c..8c57eabb585 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2953,7 +2953,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==6.4.3 +yalexs==6.5.0 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 43764d7518b..8d2a53745a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2336,7 +2336,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==6.4.3 +yalexs==6.5.0 # homeassistant.components.yeelight yeelight==0.7.14 From e06e9bb39c5c95ff1b4cd331960e898c825474cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Aug 2024 15:41:19 -0500 Subject: [PATCH 2225/2411] Bump pyatv to 0.15.0 (#123674) --- 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 1f7ac45372e..9a053829516 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/apple_tv", "iot_class": "local_push", "loggers": ["pyatv", "srptools"], - "requirements": ["pyatv==0.14.3"], + "requirements": ["pyatv==0.15.0"], "zeroconf": [ "_mediaremotetv._tcp.local.", "_companion-link._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 8c57eabb585..02369d191cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1732,7 +1732,7 @@ pyatag==0.3.5.3 pyatmo==8.0.3 # homeassistant.components.apple_tv -pyatv==0.14.3 +pyatv==0.15.0 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d2a53745a6..997b44c252a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1403,7 +1403,7 @@ pyatag==0.3.5.3 pyatmo==8.0.3 # homeassistant.components.apple_tv -pyatv==0.14.3 +pyatv==0.15.0 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 From 7eccb38851b1e328172d8fa4cd9e92e23bf4c6a6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 12 Aug 2024 23:23:34 +0200 Subject: [PATCH 2226/2411] Update wled to 0.20.2 (#123746) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index efeb414438d..71939127356 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "platinum", - "requirements": ["wled==0.20.1"], + "requirements": ["wled==0.20.2"], "zeroconf": ["_wled._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 02369d191cb..70edbeec552 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2915,7 +2915,7 @@ wiffi==1.1.2 wirelesstagpy==0.8.1 # homeassistant.components.wled -wled==0.20.1 +wled==0.20.2 # homeassistant.components.wolflink wolf-comm==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 997b44c252a..fdaa9696d44 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2301,7 +2301,7 @@ whois==0.9.27 wiffi==1.1.2 # homeassistant.components.wled -wled==0.20.1 +wled==0.20.2 # homeassistant.components.wolflink wolf-comm==0.0.9 From dee06d57770b4724337d013946b651554fd458b9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 12 Aug 2024 23:47:47 -0700 Subject: [PATCH 2227/2411] Bump python-nest-sdm to 4.0.6 (#123762) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index d3ba571e65a..fbe5ddb6534 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -20,5 +20,5 @@ "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], "quality_scale": "platinum", - "requirements": ["google-nest-sdm==4.0.5"] + "requirements": ["google-nest-sdm==4.0.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 70edbeec552..0d7fa9d6710 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -986,7 +986,7 @@ google-cloud-texttospeech==2.16.3 google-generativeai==0.6.0 # homeassistant.components.nest -google-nest-sdm==4.0.5 +google-nest-sdm==4.0.6 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdaa9696d44..6ccc0145295 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -836,7 +836,7 @@ google-cloud-pubsub==2.13.11 google-generativeai==0.6.0 # homeassistant.components.nest -google-nest-sdm==4.0.5 +google-nest-sdm==4.0.6 # homeassistant.components.google_travel_time googlemaps==2.5.1 From a988cd050bdba74dd47d403cd0b2327ff8b64895 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 13 Aug 2024 08:50:02 +0200 Subject: [PATCH 2228/2411] Fix error message in html5 (#123749) --- homeassistant/components/html5/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 798589d2807..8082ca37aa3 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -533,7 +533,7 @@ class HTML5NotificationService(BaseNotificationService): elif response.status_code > 399: _LOGGER.error( "There was an issue sending the notification %s: %s", - response.status, + response.status_code, response.text, ) From 6406065e1f43cc92635619eb44d453eeafa03067 Mon Sep 17 00:00:00 2001 From: Ian Date: Tue, 13 Aug 2024 02:51:41 -0400 Subject: [PATCH 2229/2411] Bump py-nextbusnext to 2.0.4 (#123750) --- homeassistant/components/nextbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 27fec1bfba9..d22ba66d860 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nextbus", "iot_class": "cloud_polling", "loggers": ["py_nextbus"], - "requirements": ["py-nextbusnext==2.0.3"] + "requirements": ["py-nextbusnext==2.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0d7fa9d6710..8e574d22837 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1650,7 +1650,7 @@ py-madvr2==1.6.29 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==2.0.3 +py-nextbusnext==2.0.4 # homeassistant.components.nightscout py-nightscout==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ccc0145295..956e945d92d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1345,7 +1345,7 @@ py-madvr2==1.6.29 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==2.0.3 +py-nextbusnext==2.0.4 # homeassistant.components.nightscout py-nightscout==1.2.2 From 86322973d0de9f220215548c049ebabacd9720e7 Mon Sep 17 00:00:00 2001 From: Jan Rieger Date: Tue, 13 Aug 2024 10:37:43 +0200 Subject: [PATCH 2230/2411] Migrate GPSD extra state attributes to separate states (#122193) * Migrate GPSD extra state attributes to separate states * Use common translations * Address feedback --- homeassistant/components/gpsd/icons.json | 9 +++ homeassistant/components/gpsd/sensor.py | 85 ++++++++++++++++++++-- homeassistant/components/gpsd/strings.json | 26 +++++-- 3 files changed, 108 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/gpsd/icons.json b/homeassistant/components/gpsd/icons.json index b29640e0001..59d904f918c 100644 --- a/homeassistant/components/gpsd/icons.json +++ b/homeassistant/components/gpsd/icons.json @@ -7,6 +7,15 @@ "2d_fix": "mdi:crosshairs-gps", "3d_fix": "mdi:crosshairs-gps" } + }, + "latitude": { + "default": "mdi:latitude" + }, + "longitude": { + "default": "mdi:longitude" + }, + "elevation": { + "default": "mdi:arrow-up-down" } } } diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index 63f8ac4f28c..1bac41ecaae 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime import logging from typing import Any @@ -14,10 +15,20 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_MODE, EntityCategory +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_MODE, + ATTR_TIME, + EntityCategory, + UnitOfLength, + UnitOfSpeed, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.util import dt as dt_util from . import GPSDConfigEntry from .const import DOMAIN @@ -38,19 +49,73 @@ _MODE_VALUES = {2: "2d_fix", 3: "3d_fix"} class GpsdSensorDescription(SensorEntityDescription): """Class describing GPSD sensor entities.""" - value_fn: Callable[[AGPS3mechanism], str | None] + value_fn: Callable[[AGPS3mechanism], StateType | datetime] SENSOR_TYPES: tuple[GpsdSensorDescription, ...] = ( GpsdSensorDescription( - key="mode", - translation_key="mode", + key=ATTR_MODE, + translation_key=ATTR_MODE, name=None, entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, options=list(_MODE_VALUES.values()), value_fn=lambda agps_thread: _MODE_VALUES.get(agps_thread.data_stream.mode), ), + GpsdSensorDescription( + key=ATTR_LATITUDE, + translation_key=ATTR_LATITUDE, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda agps_thread: agps_thread.data_stream.lat, + entity_registry_enabled_default=False, + ), + GpsdSensorDescription( + key=ATTR_LONGITUDE, + translation_key=ATTR_LONGITUDE, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda agps_thread: agps_thread.data_stream.lon, + entity_registry_enabled_default=False, + ), + GpsdSensorDescription( + key=ATTR_ELEVATION, + translation_key=ATTR_ELEVATION, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.METERS, + value_fn=lambda agps_thread: agps_thread.data_stream.alt, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), + GpsdSensorDescription( + key=ATTR_TIME, + translation_key=ATTR_TIME, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda agps_thread: dt_util.parse_datetime( + agps_thread.data_stream.time + ), + entity_registry_enabled_default=False, + ), + GpsdSensorDescription( + key=ATTR_SPEED, + translation_key=ATTR_SPEED, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.SPEED, + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, + value_fn=lambda agps_thread: agps_thread.data_stream.speed, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), + GpsdSensorDescription( + key=ATTR_CLIMB, + translation_key=ATTR_CLIMB, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.SPEED, + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, + value_fn=lambda agps_thread: agps_thread.data_stream.climb, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), ) @@ -96,13 +161,19 @@ class GpsdSensor(SensorEntity): self.agps_thread = agps_thread @property - def native_value(self) -> str | None: + def native_value(self) -> StateType | datetime: """Return the state of GPSD.""" - return self.entity_description.value_fn(self.agps_thread) + value = self.entity_description.value_fn(self.agps_thread) + return None if value == "n/a" else value + # Deprecated since Home Assistant 2024.9.0 + # Can be removed completely in 2025.3.0 @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the GPS.""" + if self.entity_description.key != ATTR_MODE: + return None + return { ATTR_LATITUDE: self.agps_thread.data_stream.lat, ATTR_LONGITUDE: self.agps_thread.data_stream.lon, diff --git a/homeassistant/components/gpsd/strings.json b/homeassistant/components/gpsd/strings.json index 20dc283a8bb..867edf0b5a8 100644 --- a/homeassistant/components/gpsd/strings.json +++ b/homeassistant/components/gpsd/strings.json @@ -18,7 +18,15 @@ }, "entity": { "sensor": { + "latitude": { "name": "[%key:common::config_flow::data::latitude%]" }, + "longitude": { "name": "[%key:common::config_flow::data::longitude%]" }, + "elevation": { "name": "[%key:common::config_flow::data::elevation%]" }, + "time": { + "name": "[%key:component::time_date::selector::display_options::options::time%]" + }, + "climb": { "name": "Climb" }, "mode": { + "name": "[%key:common::config_flow::data::mode%]", "state": { "2d_fix": "2D Fix", "3d_fix": "3D Fix" @@ -28,11 +36,19 @@ "longitude": { "name": "[%key:common::config_flow::data::longitude%]" }, - "elevation": { "name": "Elevation" }, - "gps_time": { "name": "Time" }, - "speed": { "name": "Speed" }, - "climb": { "name": "Climb" }, - "mode": { "name": "Mode" } + "elevation": { + "name": "[%key:common::config_flow::data::elevation%]" + }, + "gps_time": { + "name": "[%key:component::time_date::selector::display_options::options::time%]" + }, + "speed": { + "name": "[%key:component::sensor::entity_component::speed::name%]" + }, + "climb": { + "name": "[%key:component::gpsd::entity::sensor::climb::name%]" + }, + "mode": { "name": "[%key:common::config_flow::data::mode%]" } } } } From 04570edb3fe2127a699c5e7e826d157df5737632 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Aug 2024 11:00:33 +0200 Subject: [PATCH 2231/2411] Remove unnecessary assignment of Template.hass from generic camera (#123767) --- homeassistant/components/generic/camera.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 80971760b85..3aac5145ca5 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -28,10 +28,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError -from homeassistant.helpers import config_validation as cv, template as template_helper from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.template import Template from . import DOMAIN from .const import ( @@ -91,18 +91,11 @@ class GenericCamera(Camera): self._password = device_info.get(CONF_PASSWORD) self._name = device_info.get(CONF_NAME, title) self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL) - if ( - not isinstance(self._still_image_url, template_helper.Template) - and self._still_image_url - ): - self._still_image_url = cv.template(self._still_image_url) if self._still_image_url: - self._still_image_url.hass = hass + self._still_image_url = Template(self._still_image_url, hass) self._stream_source = device_info.get(CONF_STREAM_SOURCE) if self._stream_source: - if not isinstance(self._stream_source, template_helper.Template): - self._stream_source = cv.template(self._stream_source) - self._stream_source.hass = hass + self._stream_source = Template(self._stream_source, hass) self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] self._attr_frame_interval = 1 / device_info[CONF_FRAMERATE] if self._stream_source: From 6317053cc61cc9b706da7b89965c2a75e339c18a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Aug 2024 11:52:27 +0200 Subject: [PATCH 2232/2411] Remove unnecessary assignment of Template.hass from condition helper (#123775) --- homeassistant/helpers/condition.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 3438336dbfa..629cdeef942 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -60,7 +60,7 @@ import homeassistant.util.dt as dt_util from . import config_validation as cv, entity_registry as er from .sun import get_astral_event_date -from .template import Template, attach as template_attach, render_complex +from .template import Template, render_complex from .trace import ( TraceElement, trace_append_element, @@ -510,9 +510,6 @@ def async_numeric_state_from_config(config: ConfigType) -> ConditionCheckerType: hass: HomeAssistant, variables: TemplateVarsType = None ) -> bool: """Test numeric state condition.""" - if value_template is not None: - value_template.hass = hass - errors = [] for index, entity_id in enumerate(entity_ids): try: @@ -630,7 +627,6 @@ def state_from_config(config: ConfigType) -> ConditionCheckerType: @trace_condition_function def if_state(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Test if condition.""" - template_attach(hass, for_period) errors = [] result: bool = match != ENTITY_MATCH_ANY for index, entity_id in enumerate(entity_ids): @@ -792,8 +788,6 @@ def async_template_from_config(config: ConfigType) -> ConditionCheckerType: @trace_condition_function def template_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Validate template based if-condition.""" - value_template.hass = hass - return async_template(hass, value_template, variables) return template_if From a6f3e587bcc9e48c69f9f984141309bd6c909ae6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Aug 2024 11:52:46 +0200 Subject: [PATCH 2233/2411] Remove unnecessary assignment of Template.hass from manual (#123770) --- homeassistant/components/manual/alarm_control_panel.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index 422a9726e81..055e79867ab 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -222,7 +222,6 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity): self._attr_unique_id = unique_id if code_template: self._code = code_template - self._code.hass = hass else: self._code = code or None self._attr_code_arm_required = code_arm_required From 314ee9c74cfc8ff2ef7ac7df0d97b8cacf9cffd4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Aug 2024 11:53:06 +0200 Subject: [PATCH 2234/2411] Remove unnecessary assignment of Template.hass from manual_mqtt (#123771) --- homeassistant/components/manual_mqtt/alarm_control_panel.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index 26946a2a45c..8d447bbc8ac 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -273,7 +273,6 @@ class ManualMQTTAlarm(AlarmControlPanelEntity): self._attr_name = name if code_template: self._code = code_template - self._code.hass = hass else: self._code = code or None self._disarm_after_trigger = disarm_after_trigger From f97fc8a90794c49edf5a30eaa8e4983bf52a6b0d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Aug 2024 11:53:24 +0200 Subject: [PATCH 2235/2411] Remove unnecessary assignment of Template.hass from rest (#123772) --- homeassistant/components/rest/switch.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 219084ea683..e4bb1f797d9 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -151,9 +151,6 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity): self._timeout: int = config[CONF_TIMEOUT] self._verify_ssl: bool = config[CONF_VERIFY_SSL] - self._body_on.hass = hass - self._body_off.hass = hass - async def async_added_to_hass(self) -> None: """Handle adding to Home Assistant.""" await super().async_added_to_hass() From 5837450a05f4f1be0aae39951a2467e2cbffe043 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Aug 2024 11:53:39 +0200 Subject: [PATCH 2236/2411] Remove unnecessary assignment of Template.hass from influxdb (#123768) --- homeassistant/components/influxdb/sensor.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index a1a9e618cb8..cc601888f56 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -194,12 +194,7 @@ class InfluxSensor(SensorEntity): """Initialize the sensor.""" self._name = query.get(CONF_NAME) self._unit_of_measurement = query.get(CONF_UNIT_OF_MEASUREMENT) - value_template = query.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - self._value_template = value_template - self._value_template.hass = hass - else: - self._value_template = None + self._value_template = query.get(CONF_VALUE_TEMPLATE) self._state = None self._hass = hass self._attr_unique_id = query.get(CONF_UNIQUE_ID) From dc462aa52950b7ad651f89adf3dcfae4cf3ad726 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Aug 2024 11:54:36 +0200 Subject: [PATCH 2237/2411] Remove unnecessary assignment of Template.hass from template (#123773) --- .../components/template/alarm_control_panel.py | 2 +- .../components/template/binary_sensor.py | 12 ++++++++---- homeassistant/components/template/config.py | 2 +- homeassistant/components/template/cover.py | 2 +- homeassistant/components/template/fan.py | 2 +- homeassistant/components/template/light.py | 2 +- homeassistant/components/template/lock.py | 2 +- homeassistant/components/template/sensor.py | 12 ++++++++---- homeassistant/components/template/switch.py | 2 +- .../components/template/template_entity.py | 17 +++++++++-------- homeassistant/components/template/trigger.py | 2 -- homeassistant/components/template/vacuum.py | 2 +- homeassistant/components/template/weather.py | 2 +- .../template/test_alarm_control_panel.py | 2 +- .../components/template/test_template_entity.py | 8 ++++---- 15 files changed, 39 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 2ac91d39858..7c23fdcebcc 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -108,7 +108,7 @@ async def _async_create_entities(hass, config): alarm_control_panels = [] for object_id, entity_config in config[CONF_ALARM_CONTROL_PANELS].items(): - entity_config = rewrite_common_legacy_to_modern_conf(entity_config) + entity_config = rewrite_common_legacy_to_modern_conf(hass, entity_config) unique_id = entity_config.get(CONF_UNIQUE_ID) alarm_control_panels.append( diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 68b3cd6d35a..187c7079f59 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -119,17 +119,21 @@ LEGACY_BINARY_SENSOR_SCHEMA = vol.All( ) -def rewrite_legacy_to_modern_conf(cfg: dict[str, dict]) -> list[dict]: +def rewrite_legacy_to_modern_conf( + hass: HomeAssistant, cfg: dict[str, dict] +) -> list[dict]: """Rewrite legacy binary sensor definitions to modern ones.""" sensors = [] for object_id, entity_cfg in cfg.items(): entity_cfg = {**entity_cfg, CONF_OBJECT_ID: object_id} - entity_cfg = rewrite_common_legacy_to_modern_conf(entity_cfg, LEGACY_FIELDS) + entity_cfg = rewrite_common_legacy_to_modern_conf( + hass, entity_cfg, LEGACY_FIELDS + ) if CONF_NAME not in entity_cfg: - entity_cfg[CONF_NAME] = template.Template(object_id) + entity_cfg[CONF_NAME] = template.Template(object_id, hass) sensors.append(entity_cfg) @@ -183,7 +187,7 @@ async def async_setup_platform( _async_create_template_tracking_entities( async_add_entities, hass, - rewrite_legacy_to_modern_conf(config[CONF_SENSORS]), + rewrite_legacy_to_modern_conf(hass, config[CONF_SENSORS]), None, ) return diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index 42a57cfc4aa..e2015743a0e 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -115,7 +115,7 @@ async def async_validate_config(hass: HomeAssistant, config: ConfigType) -> Conf ) definitions = list(cfg[new_key]) if new_key in cfg else [] - definitions.extend(transform(cfg[old_key])) + definitions.extend(transform(hass, cfg[old_key])) cfg = {**cfg, new_key: definitions} config_sections.append(cfg) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index d50067f6278..2c84387ed64 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -106,7 +106,7 @@ async def _async_create_entities(hass, config): covers = [] for object_id, entity_config in config[CONF_COVERS].items(): - entity_config = rewrite_common_legacy_to_modern_conf(entity_config) + entity_config = rewrite_common_legacy_to_modern_conf(hass, entity_config) unique_id = entity_config.get(CONF_UNIQUE_ID) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 20a2159e378..cedd7d0d725 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -94,7 +94,7 @@ async def _async_create_entities(hass, config): fans = [] for object_id, entity_config in config[CONF_FANS].items(): - entity_config = rewrite_common_legacy_to_modern_conf(entity_config) + entity_config = rewrite_common_legacy_to_modern_conf(hass, entity_config) unique_id = entity_config.get(CONF_UNIQUE_ID) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index ba6b8ce846b..cae6c0cebc1 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -126,7 +126,7 @@ async def _async_create_entities(hass, config): lights = [] for object_id, entity_config in config[CONF_LIGHTS].items(): - entity_config = rewrite_common_legacy_to_modern_conf(entity_config) + entity_config = rewrite_common_legacy_to_modern_conf(hass, entity_config) unique_id = entity_config.get(CONF_UNIQUE_ID) lights.append( diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 0fa219fcd9b..5c0b67a23dc 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -59,7 +59,7 @@ PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend( async def _async_create_entities(hass, config): """Create the Template lock.""" - config = rewrite_common_legacy_to_modern_conf(config) + config = rewrite_common_legacy_to_modern_conf(hass, config) return [TemplateLock(hass, config, config.get(CONF_UNIQUE_ID))] diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 70a2d5dd650..ee24407699d 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -142,17 +142,21 @@ def extra_validation_checks(val): return val -def rewrite_legacy_to_modern_conf(cfg: dict[str, dict]) -> list[dict]: +def rewrite_legacy_to_modern_conf( + hass: HomeAssistant, cfg: dict[str, dict] +) -> list[dict]: """Rewrite legacy sensor definitions to modern ones.""" sensors = [] for object_id, entity_cfg in cfg.items(): entity_cfg = {**entity_cfg, CONF_OBJECT_ID: object_id} - entity_cfg = rewrite_common_legacy_to_modern_conf(entity_cfg, LEGACY_FIELDS) + entity_cfg = rewrite_common_legacy_to_modern_conf( + hass, entity_cfg, LEGACY_FIELDS + ) if CONF_NAME not in entity_cfg: - entity_cfg[CONF_NAME] = template.Template(object_id) + entity_cfg[CONF_NAME] = template.Template(object_id, hass) sensors.append(entity_cfg) @@ -210,7 +214,7 @@ async def async_setup_platform( _async_create_template_tracking_entities( async_add_entities, hass, - rewrite_legacy_to_modern_conf(config[CONF_SENSORS]), + rewrite_legacy_to_modern_conf(hass, config[CONF_SENSORS]), None, ) return diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index fbb35399ef8..9145625f706 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -76,7 +76,7 @@ async def _async_create_entities(hass, config): switches = [] for object_id, entity_config in config[CONF_SWITCHES].items(): - entity_config = rewrite_common_legacy_to_modern_conf(entity_config) + entity_config = rewrite_common_legacy_to_modern_conf(hass, entity_config) unique_id = entity_config.get(CONF_UNIQUE_ID) switches.append( diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index b5d2ab6fff3..a074f828284 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -123,7 +123,9 @@ LEGACY_FIELDS = { def rewrite_common_legacy_to_modern_conf( - entity_cfg: dict[str, Any], extra_legacy_fields: dict[str, str] | None = None + hass: HomeAssistant, + entity_cfg: dict[str, Any], + extra_legacy_fields: dict[str, str] | None = None, ) -> dict[str, Any]: """Rewrite legacy config.""" entity_cfg = {**entity_cfg} @@ -138,11 +140,11 @@ def rewrite_common_legacy_to_modern_conf( val = entity_cfg.pop(from_key) if isinstance(val, str): - val = Template(val) + val = Template(val, hass) entity_cfg[to_key] = val if CONF_NAME in entity_cfg and isinstance(entity_cfg[CONF_NAME], str): - entity_cfg[CONF_NAME] = Template(entity_cfg[CONF_NAME]) + entity_cfg[CONF_NAME] = Template(entity_cfg[CONF_NAME], hass) return entity_cfg @@ -310,7 +312,6 @@ class TemplateEntity(Entity): # Try to render the name as it can influence the entity ID self._attr_name = fallback_name if self._friendly_name_template: - self._friendly_name_template.hass = hass with contextlib.suppress(TemplateError): self._attr_name = self._friendly_name_template.async_render( variables=variables, parse_result=False @@ -319,14 +320,12 @@ class TemplateEntity(Entity): # Templates will not render while the entity is unavailable, try to render the # icon and picture templates. if self._entity_picture_template: - self._entity_picture_template.hass = hass with contextlib.suppress(TemplateError): self._attr_entity_picture = self._entity_picture_template.async_render( variables=variables, parse_result=False ) if self._icon_template: - self._icon_template.hass = hass with contextlib.suppress(TemplateError): self._attr_icon = self._icon_template.async_render( variables=variables, parse_result=False @@ -388,8 +387,10 @@ class TemplateEntity(Entity): If True, the attribute will be set to None if the template errors. """ - assert self.hass is not None, "hass cannot be None" - template.hass = self.hass + if self.hass is None: + raise ValueError("hass cannot be None") + if template.hass is None: + raise ValueError("template.hass cannot be None") template_attribute = _TemplateAttribute( self, attribute, template, validator, on_update, none_on_template_error ) diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 09ad0754634..44ac2d93051 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -49,9 +49,7 @@ async def async_attach_trigger( """Listen for state changes based on configuration.""" trigger_data = trigger_info["trigger_data"] value_template: Template = config[CONF_VALUE_TEMPLATE] - value_template.hass = hass time_delta = config.get(CONF_FOR) - template.attach(hass, time_delta) delay_cancel = None job = HassJob(action) armed = False diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index e512ce2eb04..1d021bcb571 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -100,7 +100,7 @@ async def _async_create_entities(hass, config): vacuums = [] for object_id, entity_config in config[CONF_VACUUMS].items(): - entity_config = rewrite_common_legacy_to_modern_conf(entity_config) + entity_config = rewrite_common_legacy_to_modern_conf(hass, entity_config) unique_id = entity_config.get(CONF_UNIQUE_ID) vacuums.append( diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index 5c3e4107b2c..ec6d1f08dd3 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -153,7 +153,7 @@ async def async_setup_platform( ) return - config = rewrite_common_legacy_to_modern_conf(config) + config = rewrite_common_legacy_to_modern_conf(hass, config) unique_id = config.get(CONF_UNIQUE_ID) async_add_entities( diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 6a2a95a64eb..ea63d7b9926 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -244,7 +244,7 @@ async def test_template_syntax_error( "platform": "template", "panels": { "test_template_panel": { - "name": "Template Alarm Panel", + "name": '{{ "Template Alarm Panel" }}', "value_template": "disarmed", **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, } diff --git a/tests/components/template/test_template_entity.py b/tests/components/template/test_template_entity.py index dcceea95181..c09a09750fe 100644 --- a/tests/components/template/test_template_entity.py +++ b/tests/components/template/test_template_entity.py @@ -11,14 +11,14 @@ async def test_template_entity_requires_hass_set(hass: HomeAssistant) -> None: """Test template entity requires hass to be set before accepting templates.""" entity = template_entity.TemplateEntity(hass) - with pytest.raises(AssertionError): + with pytest.raises(ValueError, match="^hass cannot be None"): entity.add_template_attribute("_hello", template.Template("Hello")) entity.hass = object() - entity.add_template_attribute("_hello", template.Template("Hello", None)) + with pytest.raises(ValueError, match="^template.hass cannot be None"): + entity.add_template_attribute("_hello", template.Template("Hello", None)) tpl_with_hass = template.Template("Hello", entity.hass) entity.add_template_attribute("_hello", tpl_with_hass) - # Because hass is set in `add_template_attribute`, both templates match `tpl_with_hass` - assert len(entity._template_attrs.get(tpl_with_hass, [])) == 2 + assert len(entity._template_attrs.get(tpl_with_hass, [])) == 1 From e9682fe003b9ef1826166fb0bb13ce5baae2f812 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Aug 2024 11:54:56 +0200 Subject: [PATCH 2238/2411] Remove unnecessary assignment of Template.hass from xiaomi (#123774) --- homeassistant/components/xiaomi/camera.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index 323a0f8a157..8ab15f85147 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -80,7 +80,6 @@ class XiaomiCamera(Camera): self._manager = get_ffmpeg_manager(hass) self._name = config[CONF_NAME] self.host = config[CONF_HOST] - self.host.hass = hass self._model = config[CONF_MODEL] self.port = config[CONF_PORT] self.path = config[CONF_PATH] From 992de497f25a93c2ff907e0df909c405485aa241 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Aug 2024 11:55:37 +0200 Subject: [PATCH 2239/2411] Remove unnecessary assignment of Template.hass from script helper (#123780) --- homeassistant/helpers/script.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index a1b885d0c52..26a9b6e069e 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -669,7 +669,6 @@ class _ScriptRun: trace_set_result(wait=self._variables["wait"]) wait_template = self._action[CONF_WAIT_TEMPLATE] - wait_template.hass = self._hass # check if condition already okay if condition.async_template(self._hass, wait_template, self._variables, False): @@ -1429,7 +1428,6 @@ class Script: self._hass = hass self.sequence = sequence - template.attach(hass, self.sequence) self.name = name self.unique_id = f"{domain}.{name}-{id(self)}" self.domain = domain @@ -1459,8 +1457,6 @@ class Script: self._sequence_scripts: dict[int, Script] = {} self.variables = variables self._variables_dynamic = template.is_complex(variables) - if self._variables_dynamic: - template.attach(hass, variables) self._copy_variables_on_run = copy_variables @property From 5f694d9a8449e25aa3aff0ff252fd83c5afe54c3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 11:56:18 +0200 Subject: [PATCH 2240/2411] Improve type hints in mochad tests (#123794) --- tests/components/mochad/test_light.py | 2 +- tests/components/mochad/test_switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/mochad/test_light.py b/tests/components/mochad/test_light.py index 872bd3a9d61..49beebbaec6 100644 --- a/tests/components/mochad/test_light.py +++ b/tests/components/mochad/test_light.py @@ -18,7 +18,7 @@ def pymochad_mock(): @pytest.fixture -def light_mock(hass, brightness): +def light_mock(hass: HomeAssistant, brightness: int) -> mochad.MochadLight: """Mock light.""" controller_mock = mock.MagicMock() dev_dict = {"address": "a1", "name": "fake_light", "brightness_levels": brightness} diff --git a/tests/components/mochad/test_switch.py b/tests/components/mochad/test_switch.py index 750dd48296e..9fea3b5c14c 100644 --- a/tests/components/mochad/test_switch.py +++ b/tests/components/mochad/test_switch.py @@ -21,7 +21,7 @@ def pymochad_mock(): @pytest.fixture -def switch_mock(hass): +def switch_mock(hass: HomeAssistant) -> mochad.MochadSwitch: """Mock switch.""" controller_mock = mock.MagicMock() dev_dict = {"address": "a1", "name": "fake_switch"} From 2b968dfd9aab9d33fbb2ddc873b4a9498b8f233b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 11:56:55 +0200 Subject: [PATCH 2241/2411] Improve type hints in mfi tests (#123792) --- tests/components/mfi/test_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index 49efdd5dc71..37512ca78f8 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -116,13 +116,13 @@ async def test_setup_adds_proper_devices(hass: HomeAssistant) -> None: @pytest.fixture(name="port") -def port_fixture(): +def port_fixture() -> mock.MagicMock: """Port fixture.""" return mock.MagicMock() @pytest.fixture(name="sensor") -def sensor_fixture(hass, port): +def sensor_fixture(hass: HomeAssistant, port: mock.MagicMock) -> mfi.MfiSensor: """Sensor fixture.""" sensor = mfi.MfiSensor(port, hass) sensor.hass = hass From 3660c2dbb46476ac625e0dce8d512a6498fe9a41 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 11:59:22 +0200 Subject: [PATCH 2242/2411] Improve type hints in mailgun tests (#123789) --- tests/components/mailgun/test_init.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/mailgun/test_init.py b/tests/components/mailgun/test_init.py index 908e98ae31e..2e60c56faa4 100644 --- a/tests/components/mailgun/test_init.py +++ b/tests/components/mailgun/test_init.py @@ -10,7 +10,7 @@ from homeassistant import config_entries from homeassistant.components import mailgun, webhook from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_API_KEY, CONF_DOMAIN -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component @@ -29,7 +29,7 @@ async def http_client( @pytest.fixture -async def webhook_id_with_api_key(hass): +async def webhook_id_with_api_key(hass: HomeAssistant) -> str: """Initialize the Mailgun component and get the webhook_id.""" await async_setup_component( hass, @@ -53,7 +53,7 @@ async def webhook_id_with_api_key(hass): @pytest.fixture -async def webhook_id_without_api_key(hass): +async def webhook_id_without_api_key(hass: HomeAssistant) -> str: """Initialize the Mailgun component and get the webhook_id w/o API key.""" await async_setup_component(hass, mailgun.DOMAIN, {}) @@ -73,7 +73,7 @@ async def webhook_id_without_api_key(hass): @pytest.fixture -async def mailgun_events(hass): +async def mailgun_events(hass: HomeAssistant) -> list[Event]: """Return a list of mailgun_events triggered.""" events = [] From 4ceb9b9dbfc63bc7263fa626b7070a30d2a3767a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 11:59:48 +0200 Subject: [PATCH 2243/2411] Improve type hints in anthropic tests (#123784) --- tests/components/anthropic/conftest.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/components/anthropic/conftest.py b/tests/components/anthropic/conftest.py index fe3b20f15b8..ce6b98c480c 100644 --- a/tests/components/anthropic/conftest.py +++ b/tests/components/anthropic/conftest.py @@ -1,5 +1,6 @@ """Tests helpers.""" +from collections.abc import AsyncGenerator from unittest.mock import AsyncMock, patch import pytest @@ -13,7 +14,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_config_entry(hass): +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: """Mock a config entry.""" entry = MockConfigEntry( title="Claude", @@ -27,7 +28,9 @@ def mock_config_entry(hass): @pytest.fixture -def mock_config_entry_with_assist(hass, mock_config_entry): +def mock_config_entry_with_assist( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> MockConfigEntry: """Mock a config entry with assist.""" hass.config_entries.async_update_entry( mock_config_entry, options={CONF_LLM_HASS_API: llm.LLM_API_ASSIST} @@ -36,7 +39,9 @@ def mock_config_entry_with_assist(hass, mock_config_entry): @pytest.fixture -async def mock_init_component(hass, mock_config_entry): +async def mock_init_component( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> AsyncGenerator[None]: """Initialize integration.""" with patch( "anthropic.resources.messages.AsyncMessages.create", new_callable=AsyncMock From 78f7b3340dcd294d4176d8787ad6669f977a2843 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Aug 2024 12:09:30 +0200 Subject: [PATCH 2244/2411] Remove unnecessary assignment of Template.hass from event helper (#123777) --- homeassistant/helpers/event.py | 2 -- tests/helpers/test_event.py | 25 +++++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 207dd024b6a..38f461d8d7a 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -976,8 +976,6 @@ class TrackTemplateResultInfo: self.hass = hass self._job = HassJob(action, f"track template result {track_templates}") - for track_template_ in track_templates: - track_template_.template.hass = hass self._track_templates = track_templates self._has_super_template = has_super_template diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 4bb4c1a1967..6c71f1d8a7c 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1476,7 +1476,7 @@ async def test_track_template_result_super_template_2( wildercard_runs = [] wildercard_runs_availability = [] - template_availability = Template(availability_template) + template_availability = Template(availability_template, hass) template_condition = Template("{{states.sensor.test.state}}", hass) template_condition_var = Template( "{{(states.sensor.test.state|int) + test }}", hass @@ -1628,7 +1628,7 @@ async def test_track_template_result_super_template_2_initially_false( wildercard_runs = [] wildercard_runs_availability = [] - template_availability = Template(availability_template) + template_availability = Template(availability_template, hass) template_condition = Template("{{states.sensor.test.state}}", hass) template_condition_var = Template( "{{(states.sensor.test.state|int) + test }}", hass @@ -3124,11 +3124,11 @@ async def test_async_track_template_result_multiple_templates( ) -> None: """Test tracking multiple templates.""" - template_1 = Template("{{ states.switch.test.state == 'on' }}") - template_2 = Template("{{ states.switch.test.state == 'on' }}") - template_3 = Template("{{ states.switch.test.state == 'off' }}") + template_1 = Template("{{ states.switch.test.state == 'on' }}", hass) + template_2 = Template("{{ states.switch.test.state == 'on' }}", hass) + template_3 = Template("{{ states.switch.test.state == 'off' }}", hass) template_4 = Template( - "{{ states.binary_sensor | map(attribute='entity_id') | list }}" + "{{ states.binary_sensor | map(attribute='entity_id') | list }}", hass ) refresh_runs = [] @@ -3188,11 +3188,12 @@ async def test_async_track_template_result_multiple_templates_mixing_domain( ) -> None: """Test tracking multiple templates when tracking entities and an entire domain.""" - template_1 = Template("{{ states.switch.test.state == 'on' }}") - template_2 = Template("{{ states.switch.test.state == 'on' }}") - template_3 = Template("{{ states.switch.test.state == 'off' }}") + template_1 = Template("{{ states.switch.test.state == 'on' }}", hass) + template_2 = Template("{{ states.switch.test.state == 'on' }}", hass) + template_3 = Template("{{ states.switch.test.state == 'off' }}", hass) template_4 = Template( - "{{ states.switch | sort(attribute='entity_id') | map(attribute='entity_id') | list }}" + "{{ states.switch | sort(attribute='entity_id') | map(attribute='entity_id') | list }}", + hass, ) refresh_runs = [] @@ -3417,8 +3418,8 @@ async def test_async_track_template_result_multiple_templates_mixing_listeners( ) -> None: """Test tracking multiple templates with mixing listener types.""" - template_1 = Template("{{ states.switch.test.state == 'on' }}") - template_2 = Template("{{ now() and True }}") + template_1 = Template("{{ states.switch.test.state == 'on' }}", hass) + template_2 = Template("{{ now() and True }}", hass) refresh_runs = [] From e15ac2fbe0d7f67112b7d81e6153419dbc227505 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:10:15 +0200 Subject: [PATCH 2245/2411] Improve type hints in elevenlabs tests (#123786) --- tests/components/elevenlabs/test_tts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/elevenlabs/test_tts.py b/tests/components/elevenlabs/test_tts.py index 7fa289f24ed..5eee4084452 100644 --- a/tests/components/elevenlabs/test_tts.py +++ b/tests/components/elevenlabs/test_tts.py @@ -4,7 +4,7 @@ from __future__ import annotations from http import HTTPStatus from typing import Any -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from elevenlabs.core import ApiError from elevenlabs.types import GetVoicesResponse @@ -29,7 +29,7 @@ from tests.typing import ClientSessionGenerator @pytest.fixture(autouse=True) -def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock): +def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock: MagicMock) -> None: """Mock writing tags.""" From 193a7b7360c068eae55ee3596927bf97b77d99e0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:18:59 +0200 Subject: [PATCH 2246/2411] Improve type hints in dsmr tests (#123785) --- tests/components/dsmr/test_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index b93dd8d18d2..c2c6d48b007 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -1521,7 +1521,7 @@ async def test_gas_meter_providing_energy_reading( ) -def test_all_obis_references_exists(): +def test_all_obis_references_exists() -> None: """Verify that all attributes exist by name in database.""" for sensor in SENSORS: assert hasattr(obis_references, sensor.obis_reference) From 30994710e6a2001ff16d8355d8e3e67b0126fabd Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Tue, 13 Aug 2024 12:55:01 +0200 Subject: [PATCH 2247/2411] Fix status update loop in bluesound integration (#123790) * Fix retry loop for status update * Use 'available' instead of _is_online * Fix tests --- .../components/bluesound/media_player.py | 37 ++++++++++--------- tests/components/bluesound/conftest.py | 35 ++++++++++++++++-- .../components/bluesound/test_config_flow.py | 4 +- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index c1b662fcddc..92f47977ee5 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -244,7 +244,6 @@ class BluesoundPlayer(MediaPlayerEntity): self._status: Status | None = None self._inputs: list[Input] = [] self._presets: list[Preset] = [] - self._is_online = False self._muted = False self._master: BluesoundPlayer | None = None self._is_master = False @@ -312,20 +311,24 @@ class BluesoundPlayer(MediaPlayerEntity): async def _start_poll_command(self): """Loop which polls the status of the player.""" - try: - while True: + while True: + try: await self.async_update_status() - - except (TimeoutError, ClientError): - _LOGGER.error("Node %s:%s is offline, retrying later", self.host, self.port) - await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) - self.start_polling() - - except CancelledError: - _LOGGER.debug("Stopping the polling of node %s:%s", self.host, self.port) - except Exception: - _LOGGER.exception("Unexpected error in %s:%s", self.host, self.port) - raise + except (TimeoutError, ClientError): + _LOGGER.error( + "Node %s:%s is offline, retrying later", self.host, self.port + ) + await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) + except CancelledError: + _LOGGER.debug( + "Stopping the polling of node %s:%s", self.host, self.port + ) + return + except Exception: + _LOGGER.exception( + "Unexpected error in %s:%s, retrying later", self.host, self.port + ) + await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) async def async_added_to_hass(self) -> None: """Start the polling task.""" @@ -348,7 +351,7 @@ class BluesoundPlayer(MediaPlayerEntity): async def async_update(self) -> None: """Update internal status of the entity.""" - if not self._is_online: + if not self.available: return with suppress(TimeoutError): @@ -365,7 +368,7 @@ class BluesoundPlayer(MediaPlayerEntity): try: status = await self._player.status(etag=etag, poll_timeout=120, timeout=125) - self._is_online = True + self._attr_available = True self._last_status_update = dt_util.utcnow() self._status = status @@ -394,7 +397,7 @@ class BluesoundPlayer(MediaPlayerEntity): self.async_write_ha_state() except (TimeoutError, ClientError): - self._is_online = False + self._attr_available = False self._last_status_update = None self._status = None self.async_write_ha_state() diff --git a/tests/components/bluesound/conftest.py b/tests/components/bluesound/conftest.py index 5d81b6863c6..155d6b66e4e 100644 --- a/tests/components/bluesound/conftest.py +++ b/tests/components/bluesound/conftest.py @@ -3,7 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch -from pyblu import SyncStatus +from pyblu import Status, SyncStatus import pytest from homeassistant.components.bluesound.const import DOMAIN @@ -39,6 +39,35 @@ def sync_status() -> SyncStatus: ) +@pytest.fixture +def status() -> Status: + """Return a status object.""" + return Status( + etag="etag", + input_id=None, + service=None, + state="playing", + shuffle=False, + album=None, + artist=None, + name=None, + image=None, + volume=10, + volume_db=22.3, + mute=False, + mute_volume=None, + mute_volume_db=None, + seconds=2, + total_seconds=123.1, + can_seek=False, + sleep=0, + group_name=None, + group_volume=None, + indexing=False, + stream_url=None, + ) + + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" @@ -65,7 +94,7 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: @pytest.fixture -def mock_player() -> Generator[AsyncMock]: +def mock_player(status: Status) -> Generator[AsyncMock]: """Mock the player.""" with ( patch( @@ -78,7 +107,7 @@ def mock_player() -> Generator[AsyncMock]: ): player = mock_player.return_value player.__aenter__.return_value = player - player.status.return_value = None + player.status.return_value = status player.sync_status.return_value = SyncStatus( etag="etag", id="1.1.1.1:11000", diff --git a/tests/components/bluesound/test_config_flow.py b/tests/components/bluesound/test_config_flow.py index 32f36fcea58..8fecba7017d 100644 --- a/tests/components/bluesound/test_config_flow.py +++ b/tests/components/bluesound/test_config_flow.py @@ -41,7 +41,7 @@ async def test_user_flow_success( async def test_user_flow_cannot_connect( - hass: HomeAssistant, mock_player: AsyncMock + hass: HomeAssistant, mock_player: AsyncMock, mock_setup_entry: AsyncMock ) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( @@ -76,6 +76,8 @@ async def test_user_flow_cannot_connect( CONF_PORT: 11000, } + mock_setup_entry.assert_called_once() + async def test_user_flow_aleady_configured( hass: HomeAssistant, From 8e0dfbcd1339fa0c178d2058037d3bfebbee011d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:15:35 +0200 Subject: [PATCH 2248/2411] Improve type hints in modbus tests (#123795) --- tests/components/modbus/conftest.py | 4 +++- tests/components/modbus/test_sensor.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 6741504585a..28f8eae5a0b 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -192,7 +192,9 @@ async def mock_test_state_fixture( @pytest.fixture(name="mock_modbus_ha") -async def mock_modbus_ha_fixture(hass, mock_modbus): +async def mock_modbus_ha_fixture( + hass: HomeAssistant, mock_modbus: mock.AsyncMock +) -> mock.AsyncMock: """Load homeassistant to allow service calls.""" assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 20ff558fce6..87015fa634c 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -1335,7 +1335,7 @@ async def test_wrap_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None @pytest.fixture(name="mock_restore") -async def mock_restore(hass): +async def mock_restore(hass: HomeAssistant) -> None: """Mock restore cache.""" mock_restore_cache_with_extra_data( hass, From 71e23e78493fb516bf51773763b8b70d8307462f Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Tue, 13 Aug 2024 12:15:58 +0100 Subject: [PATCH 2249/2411] System Bridge package updates (#123657) --- homeassistant/components/system_bridge/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 80527de75cd..e886bcad150 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -10,6 +10,6 @@ "iot_class": "local_push", "loggers": ["systembridgeconnector"], "quality_scale": "silver", - "requirements": ["systembridgeconnector==4.1.0", "systembridgemodels==4.1.0"], + "requirements": ["systembridgeconnector==4.1.5", "systembridgemodels==4.2.4"], "zeroconf": ["_system-bridge._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 8e574d22837..c97d0fe527e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2700,10 +2700,10 @@ switchbot-api==2.2.1 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridgeconnector==4.1.0 +systembridgeconnector==4.1.5 # homeassistant.components.system_bridge -systembridgemodels==4.1.0 +systembridgemodels==4.2.4 # homeassistant.components.tailscale tailscale==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 956e945d92d..f04c0425af9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2137,10 +2137,10 @@ surepy==0.9.0 switchbot-api==2.2.1 # homeassistant.components.system_bridge -systembridgeconnector==4.1.0 +systembridgeconnector==4.1.5 # homeassistant.components.system_bridge -systembridgemodels==4.1.0 +systembridgemodels==4.2.4 # homeassistant.components.tailscale tailscale==0.6.1 From b3d1d79a49c610ef5e788e3d7715b4076d81faf2 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 13 Aug 2024 13:28:37 +0200 Subject: [PATCH 2250/2411] Update xknx to 3.1.0 and fix climate read only mode (#123776) --- homeassistant/components/knx/climate.py | 29 ++++++-- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/knx/test_climate.py | 84 ++++++++++++++++++++++ 5 files changed, 109 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 9abc9023617..abce143c760 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -5,7 +5,11 @@ from __future__ import annotations from typing import Any from xknx import XKNX -from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode +from xknx.devices import ( + Climate as XknxClimate, + ClimateMode as XknxClimateMode, + Device as XknxDevice, +) from xknx.dpt.dpt_20 import HVACControllerMode from homeassistant import config_entries @@ -241,12 +245,9 @@ class KNXClimate(KnxYamlEntity, ClimateEntity): if self._device.supports_on_off and not self._device.is_on: return HVACMode.OFF if self._device.mode is not None and self._device.mode.supports_controller_mode: - hvac_mode = CONTROLLER_MODES.get( + return CONTROLLER_MODES.get( self._device.mode.controller_mode, self.default_hvac_mode ) - if hvac_mode is not HVACMode.OFF: - self._last_hvac_mode = hvac_mode - return hvac_mode return self.default_hvac_mode @property @@ -261,11 +262,15 @@ class KNXClimate(KnxYamlEntity, ClimateEntity): if self._device.supports_on_off: if not ha_controller_modes: - ha_controller_modes.append(self.default_hvac_mode) + ha_controller_modes.append(self._last_hvac_mode) ha_controller_modes.append(HVACMode.OFF) hvac_modes = list(set(filter(None, ha_controller_modes))) - return hvac_modes if hvac_modes else [self.default_hvac_mode] + return ( + hvac_modes + if hvac_modes + else [self.hvac_mode] # mode read-only -> fall back to only current mode + ) @property def hvac_action(self) -> HVACAction | None: @@ -354,3 +359,13 @@ class KNXClimate(KnxYamlEntity, ClimateEntity): self._device.mode.unregister_device_updated_cb(self.after_update_callback) self._device.mode.xknx.devices.async_remove(self._device.mode) await super().async_will_remove_from_hass() + + def after_update_callback(self, _device: XknxDevice) -> None: + """Call after device was updated.""" + if self._device.mode is not None and self._device.mode.supports_controller_mode: + hvac_mode = CONTROLLER_MODES.get( + self._device.mode.controller_mode, self.default_hvac_mode + ) + if hvac_mode is not HVACMode.OFF: + self._last_hvac_mode = hvac_mode + super().after_update_callback(_device) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 6974ee300f5..9ecf687d6b9 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -11,7 +11,7 @@ "loggers": ["xknx", "xknxproject"], "quality_scale": "platinum", "requirements": [ - "xknx==3.0.0", + "xknx==3.1.0", "xknxproject==3.7.1", "knx-frontend==2024.8.9.225351" ], diff --git a/requirements_all.txt b/requirements_all.txt index c97d0fe527e..e19819dafde 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2930,7 +2930,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.30.2 # homeassistant.components.knx -xknx==3.0.0 +xknx==3.1.0 # homeassistant.components.knx xknxproject==3.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f04c0425af9..575fd7757be 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2316,7 +2316,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.30.2 # homeassistant.components.knx -xknx==3.0.0 +xknx==3.1.0 # homeassistant.components.knx xknxproject==3.7.1 diff --git a/tests/components/knx/test_climate.py b/tests/components/knx/test_climate.py index 77eeeef3559..9f198b48bd4 100644 --- a/tests/components/knx/test_climate.py +++ b/tests/components/knx/test_climate.py @@ -231,6 +231,90 @@ async def test_climate_hvac_mode( assert hass.states.get("climate.test").state == "cool" +async def test_climate_heat_cool_read_only( + hass: HomeAssistant, knx: KNXTestKit +) -> None: + """Test KNX climate hvac mode.""" + heat_cool_state_ga = "3/3/3" + await knx.setup_integration( + { + ClimateSchema.PLATFORM: { + CONF_NAME: "test", + ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3", + ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4", + ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5", + ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS: heat_cool_state_ga, + } + } + ) + # read states state updater + # StateUpdater semaphore allows 2 concurrent requests + await knx.assert_read("1/2/3") + await knx.assert_read("1/2/5") + # StateUpdater initialize state + await knx.receive_response("1/2/3", RAW_FLOAT_20_0) + await knx.receive_response("1/2/5", RAW_FLOAT_20_0) + await knx.assert_read(heat_cool_state_ga) + await knx.receive_response(heat_cool_state_ga, True) # heat + + state = hass.states.get("climate.test") + assert state.state == "heat" + assert state.attributes["hvac_modes"] == ["heat"] + assert state.attributes["hvac_action"] == "heating" + + await knx.receive_write(heat_cool_state_ga, False) # cool + state = hass.states.get("climate.test") + assert state.state == "cool" + assert state.attributes["hvac_modes"] == ["cool"] + assert state.attributes["hvac_action"] == "cooling" + + +async def test_climate_heat_cool_read_only_on_off( + hass: HomeAssistant, knx: KNXTestKit +) -> None: + """Test KNX climate hvac mode.""" + on_off_ga = "2/2/2" + heat_cool_state_ga = "3/3/3" + await knx.setup_integration( + { + ClimateSchema.PLATFORM: { + CONF_NAME: "test", + ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3", + ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4", + ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5", + ClimateSchema.CONF_ON_OFF_ADDRESS: on_off_ga, + ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS: heat_cool_state_ga, + } + } + ) + # read states state updater + # StateUpdater semaphore allows 2 concurrent requests + await knx.assert_read("1/2/3") + await knx.assert_read("1/2/5") + # StateUpdater initialize state + await knx.receive_response("1/2/3", RAW_FLOAT_20_0) + await knx.receive_response("1/2/5", RAW_FLOAT_20_0) + await knx.assert_read(heat_cool_state_ga) + await knx.receive_response(heat_cool_state_ga, True) # heat + + state = hass.states.get("climate.test") + assert state.state == "off" + assert set(state.attributes["hvac_modes"]) == {"off", "heat"} + assert state.attributes["hvac_action"] == "off" + + await knx.receive_write(heat_cool_state_ga, False) # cool + state = hass.states.get("climate.test") + assert state.state == "off" + assert set(state.attributes["hvac_modes"]) == {"off", "cool"} + assert state.attributes["hvac_action"] == "off" + + await knx.receive_write(on_off_ga, True) + state = hass.states.get("climate.test") + assert state.state == "cool" + assert set(state.attributes["hvac_modes"]) == {"off", "cool"} + assert state.attributes["hvac_action"] == "cooling" + + async def test_climate_preset_mode( hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry ) -> None: From 2c4b7c2577b628899f71bda15789f315bae96461 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:32:44 +0200 Subject: [PATCH 2251/2411] Improve type hints in knx tests (#123787) --- tests/components/knx/test_knx_selectors.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/components/knx/test_knx_selectors.py b/tests/components/knx/test_knx_selectors.py index 432a0fb9f80..7b2f09af84b 100644 --- a/tests/components/knx/test_knx_selectors.py +++ b/tests/components/knx/test_knx_selectors.py @@ -1,5 +1,7 @@ """Test KNX selectors.""" +from typing import Any + import pytest import voluptuous as vol @@ -111,7 +113,11 @@ INVALID = "invalid" ), ], ) -def test_ga_selector(selector_config, data, expected): +def test_ga_selector( + selector_config: dict[str, Any], + data: dict[str, Any], + expected: str | dict[str, Any], +) -> None: """Test GASelector.""" selector = GASelector(**selector_config) if expected == INVALID: From 2859dde69757e095ad191ed25cdc20bf9625caca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Aug 2024 06:56:10 -0500 Subject: [PATCH 2252/2411] Remove unifiprotect deprecate_package_sensor repair (#123807) --- .../components/unifiprotect/migrate.py | 20 ++++++------------- .../components/unifiprotect/strings.json | 4 ---- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/unifiprotect/migrate.py b/homeassistant/components/unifiprotect/migrate.py index e469b684518..2c631489217 100644 --- a/homeassistant/components/unifiprotect/migrate.py +++ b/homeassistant/components/unifiprotect/migrate.py @@ -107,20 +107,18 @@ async def async_migrate_data( ) -> None: """Run all valid UniFi Protect data migrations.""" - _LOGGER.debug("Start Migrate: async_deprecate_hdr_package") - async_deprecate_hdr_package(hass, entry) - _LOGGER.debug("Completed Migrate: async_deprecate_hdr_package") + _LOGGER.debug("Start Migrate: async_deprecate_hdr") + async_deprecate_hdr(hass, entry) + _LOGGER.debug("Completed Migrate: async_deprecate_hdr") @callback -def async_deprecate_hdr_package(hass: HomeAssistant, entry: UFPConfigEntry) -> None: - """Check for usages of hdr_mode switch and package sensor and raise repair if it is used. +def async_deprecate_hdr(hass: HomeAssistant, entry: UFPConfigEntry) -> None: + """Check for usages of hdr_mode switch and raise repair if it is used. UniFi Protect v3.0.22 changed how HDR works so it is no longer a simple on/off toggle. There is Always On, Always Off and Auto. So it has been migrated to a select. The old switch is now deprecated. - Additionally, the Package sensor is no longer functional due to how events work so a repair to notify users. - Added in 2024.4.0 """ @@ -128,11 +126,5 @@ def async_deprecate_hdr_package(hass: HomeAssistant, entry: UFPConfigEntry) -> N hass, entry, "2024.10.0", - { - "hdr_switch": {"id": "hdr_mode", "platform": Platform.SWITCH}, - "package_sensor": { - "id": "smart_obj_package", - "platform": Platform.BINARY_SENSOR, - }, - }, + {"hdr_switch": {"id": "hdr_mode", "platform": Platform.SWITCH}}, ) diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index f785498c005..aaef111a351 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -124,10 +124,6 @@ "deprecate_hdr_switch": { "title": "HDR Mode Switch Deprecated", "description": "UniFi Protect v3 added a new state for HDR (auto). As a result, the HDR Mode Switch has been replaced with an HDR Mode Select, and it is deprecated.\n\nBelow are the detected automations or scripts that use one or more of the deprecated entities:\n{items}\nThe above list may be incomplete and it does not include any template usages inside of dashboards. Please update any templates, automations or scripts accordingly." - }, - "deprecate_package_sensor": { - "title": "Package Event Sensor Deprecated", - "description": "The package event sensor never tripped because of the way events are reported in UniFi Protect. As a result, the sensor is deprecated and will be removed.\n\nBelow are the detected automations or scripts that use one or more of the deprecated entities:\n{items}\nThe above list may be incomplete and it does not include any template usages inside of dashboards. Please update any templates, automations or scripts accordingly." } }, "entity": { From f0247e942e1e6f3d2161518a090bafba2d6c6553 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Aug 2024 14:31:12 +0200 Subject: [PATCH 2253/2411] Remove unnecessary assignment of Template.hass from alert (#123766) --- homeassistant/components/alert/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 1ffeb7c73ac..f49a962fa87 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -162,16 +162,8 @@ class Alert(Entity): self._data = data self._message_template = message_template - if self._message_template is not None: - self._message_template.hass = hass - self._done_message_template = done_message_template - if self._done_message_template is not None: - self._done_message_template.hass = hass - self._title_template = title_template - if self._title_template is not None: - self._title_template.hass = hass self._notifiers = notifiers self._can_ack = can_ack From ae74fdf252df515965224804a250340e65975663 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:57:24 +0200 Subject: [PATCH 2254/2411] Improve type hints in nzbget tests (#123798) --- tests/components/nzbget/conftest.py | 3 ++- tests/components/nzbget/test_init.py | 4 +++- tests/components/nzbget/test_sensor.py | 7 ++++--- tests/components/nzbget/test_switch.py | 8 ++++++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/components/nzbget/conftest.py b/tests/components/nzbget/conftest.py index 8f48a4306c7..8a980d3ddb0 100644 --- a/tests/components/nzbget/conftest.py +++ b/tests/components/nzbget/conftest.py @@ -1,5 +1,6 @@ """Define fixtures available for all tests.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest @@ -8,7 +9,7 @@ from . import MOCK_HISTORY, MOCK_STATUS, MOCK_VERSION @pytest.fixture -def nzbget_api(hass): +def nzbget_api() -> Generator[MagicMock]: """Mock NZBGetApi for easier testing.""" with patch("homeassistant.components.nzbget.coordinator.NZBGetAPI") as mock_api: instance = mock_api.return_value diff --git a/tests/components/nzbget/test_init.py b/tests/components/nzbget/test_init.py index a119bb953ce..baf0a37546d 100644 --- a/tests/components/nzbget/test_init.py +++ b/tests/components/nzbget/test_init.py @@ -3,6 +3,7 @@ from unittest.mock import patch from pynzbgetapi import NZBGetAPIException +import pytest from homeassistant.components.nzbget.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -13,7 +14,8 @@ from . import ENTRY_CONFIG, _patch_version, init_integration from tests.common import MockConfigEntry -async def test_unload_entry(hass: HomeAssistant, nzbget_api) -> None: +@pytest.mark.usefixtures("nzbget_api") +async def test_unload_entry(hass: HomeAssistant) -> None: """Test successful unload of entry.""" entry = await init_integration(hass) diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py index 30a7f262b0b..38f7d8a68c3 100644 --- a/tests/components/nzbget/test_sensor.py +++ b/tests/components/nzbget/test_sensor.py @@ -3,6 +3,8 @@ from datetime import timedelta from unittest.mock import patch +import pytest + from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -16,9 +18,8 @@ from homeassistant.util import dt as dt_util from . import init_integration -async def test_sensors( - hass: HomeAssistant, entity_registry: er.EntityRegistry, nzbget_api -) -> None: +@pytest.mark.usefixtures("nzbget_api") +async def test_sensors(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: """Test the creation and values of the sensors.""" now = dt_util.utcnow().replace(microsecond=0) with patch("homeassistant.components.nzbget.sensor.utcnow", return_value=now): diff --git a/tests/components/nzbget/test_switch.py b/tests/components/nzbget/test_switch.py index 1c518486b9f..afb88a7be82 100644 --- a/tests/components/nzbget/test_switch.py +++ b/tests/components/nzbget/test_switch.py @@ -1,5 +1,7 @@ """Test the NZBGet switches.""" +from unittest.mock import MagicMock + from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, @@ -16,7 +18,7 @@ from . import init_integration async def test_download_switch( - hass: HomeAssistant, entity_registry: er.EntityRegistry, nzbget_api + hass: HomeAssistant, entity_registry: er.EntityRegistry, nzbget_api: MagicMock ) -> None: """Test the creation and values of the download switch.""" instance = nzbget_api.return_value @@ -44,7 +46,9 @@ async def test_download_switch( assert state.state == STATE_OFF -async def test_download_switch_services(hass: HomeAssistant, nzbget_api) -> None: +async def test_download_switch_services( + hass: HomeAssistant, nzbget_api: MagicMock +) -> None: """Test download switch services.""" instance = nzbget_api.return_value From 04b1d2414da4fb44ffdef8fd222db2e89c406a93 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:19:08 +0200 Subject: [PATCH 2255/2411] Improve type hints in mobile_app tests (#123793) --- tests/components/mobile_app/conftest.py | 7 +- .../mobile_app/test_binary_sensor.py | 22 ++-- .../mobile_app/test_device_tracker.py | 11 +- tests/components/mobile_app/test_sensor.py | 68 ++++++----- tests/components/mobile_app/test_webhook.py | 112 ++++++++++++------ 5 files changed, 147 insertions(+), 73 deletions(-) diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index 9f0681d41f7..53e90cb61ae 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -1,6 +1,7 @@ """Tests for mobile_app component.""" from http import HTTPStatus +from typing import Any from aiohttp.test_utils import TestClient import pytest @@ -15,7 +16,9 @@ from tests.typing import ClientSessionGenerator @pytest.fixture -async def create_registrations(hass, webhook_client): +async def create_registrations( + hass: HomeAssistant, webhook_client: TestClient +) -> tuple[dict[str, Any], dict[str, Any]]: """Return two new registrations.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -37,7 +40,7 @@ async def create_registrations(hass, webhook_client): @pytest.fixture -async def push_registration(hass, webhook_client): +async def push_registration(hass: HomeAssistant, webhook_client: TestClient): """Return registration with push notifications enabled.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) diff --git a/tests/components/mobile_app/test_binary_sensor.py b/tests/components/mobile_app/test_binary_sensor.py index acebd8796b7..9ffb61f92ab 100644 --- a/tests/components/mobile_app/test_binary_sensor.py +++ b/tests/components/mobile_app/test_binary_sensor.py @@ -1,7 +1,9 @@ """Entity tests for mobile_app.""" from http import HTTPStatus +from typing import Any +from aiohttp.test_utils import TestClient import pytest from homeassistant.const import STATE_UNKNOWN @@ -12,8 +14,8 @@ from homeassistant.helpers import device_registry as dr async def test_sensor( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - create_registrations, - webhook_client, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that sensors can be registered and updated.""" webhook_id = create_registrations[1]["webhook_id"] @@ -98,7 +100,9 @@ async def test_sensor( async def test_sensor_must_register( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that sensors must be registered before updating.""" webhook_id = create_registrations[1]["webhook_id"] @@ -122,8 +126,8 @@ async def test_sensor_must_register( async def test_sensor_id_no_dupes( hass: HomeAssistant, - create_registrations, - webhook_client, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test that a duplicate unique ID in registration updates the sensor.""" @@ -185,7 +189,9 @@ async def test_sensor_id_no_dupes( async def test_register_sensor_no_state( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that sensors can be registered, when there is no (unknown) state.""" webhook_id = create_registrations[1]["webhook_id"] @@ -244,7 +250,9 @@ async def test_register_sensor_no_state( async def test_update_sensor_no_state( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that sensors can be updated, when there is no (unknown) state.""" webhook_id = create_registrations[1]["webhook_id"] diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py index e3e2ce3227a..d1cbc21c36b 100644 --- a/tests/components/mobile_app/test_device_tracker.py +++ b/tests/components/mobile_app/test_device_tracker.py @@ -1,12 +1,17 @@ """Test mobile app device tracker.""" from http import HTTPStatus +from typing import Any + +from aiohttp.test_utils import TestClient from homeassistant.core import HomeAssistant async def test_sending_location( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test sending a location via a webhook.""" resp = await webhook_client.post( @@ -76,7 +81,9 @@ async def test_sending_location( async def test_restoring_location( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test sending a location via a webhook.""" resp = await webhook_client.post( diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index a7fb0ffc183..6411274fc4e 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -1,8 +1,10 @@ """Entity tests for mobile_app.""" from http import HTTPStatus +from typing import Any from unittest.mock import patch +from aiohttp.test_utils import TestClient import pytest from homeassistant.components.sensor import SensorDeviceClass @@ -14,7 +16,11 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM +from homeassistant.util.unit_system import ( + METRIC_SYSTEM, + US_CUSTOMARY_SYSTEM, + UnitSystem, +) @pytest.mark.parametrize( @@ -28,12 +34,12 @@ async def test_sensor( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - create_registrations, - webhook_client, - unit_system, - state_unit, - state1, - state2, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, + unit_system: UnitSystem, + state_unit: UnitOfTemperature, + state1: str, + state2: str, ) -> None: """Test that sensors can be registered and updated.""" hass.config.units = unit_system @@ -149,13 +155,13 @@ async def test_sensor( ) async def test_sensor_migration( hass: HomeAssistant, - create_registrations, - webhook_client, - unique_id, - unit_system, - state_unit, - state1, - state2, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, + unique_id: str, + unit_system: UnitSystem, + state_unit: UnitOfTemperature, + state1: str, + state2: str, ) -> None: """Test migration to RestoreSensor.""" hass.config.units = unit_system @@ -243,7 +249,9 @@ async def test_sensor_migration( async def test_sensor_must_register( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that sensors must be registered before updating.""" webhook_id = create_registrations[1]["webhook_id"] @@ -265,8 +273,8 @@ async def test_sensor_must_register( async def test_sensor_id_no_dupes( hass: HomeAssistant, - create_registrations, - webhook_client, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, caplog: pytest.LogCaptureFixture, ) -> None: """Test that a duplicate unique ID in registration updates the sensor.""" @@ -331,7 +339,9 @@ async def test_sensor_id_no_dupes( async def test_register_sensor_no_state( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that sensors can be registered, when there is no (unknown) state.""" webhook_id = create_registrations[1]["webhook_id"] @@ -390,7 +400,9 @@ async def test_register_sensor_no_state( async def test_update_sensor_no_state( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that sensors can be updated, when there is no (unknown) state.""" webhook_id = create_registrations[1]["webhook_id"] @@ -464,11 +476,11 @@ async def test_update_sensor_no_state( ) async def test_sensor_datetime( hass: HomeAssistant, - create_registrations, - webhook_client, - device_class, - native_value, - state_value, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, + device_class: SensorDeviceClass, + native_value: str, + state_value: str, ) -> None: """Test that sensors can be registered and updated.""" webhook_id = create_registrations[1]["webhook_id"] @@ -505,8 +517,8 @@ async def test_sensor_datetime( async def test_default_disabling_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, - create_registrations, - webhook_client, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that sensors can be disabled by default upon registration.""" webhook_id = create_registrations[1]["webhook_id"] @@ -543,8 +555,8 @@ async def test_default_disabling_entity( async def test_updating_disabled_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, - create_registrations, - webhook_client, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that sensors return error if disabled in instance.""" webhook_id = create_registrations[1]["webhook_id"] diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 77798c57f10..61e342a45ce 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,10 +1,13 @@ """Webhook tests for mobile_app.""" from binascii import unhexlify +from collections.abc import Callable from http import HTTPStatus import json +from typing import Any from unittest.mock import ANY, patch +from aiohttp.test_utils import TestClient from nacl.encoding import Base64Encoder from nacl.secret import SecretBox import pytest @@ -31,7 +34,7 @@ from tests.components.conversation import MockAgent @pytest.fixture -async def homeassistant(hass): +async def homeassistant(hass: HomeAssistant) -> None: """Load the homeassistant integration.""" await async_setup_component(hass, "homeassistant", {}) @@ -93,7 +96,8 @@ def decrypt_payload_legacy(secret_key, encrypted_data): async def test_webhook_handle_render_template( - create_registrations, webhook_client + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that we render templates properly.""" resp = await webhook_client.post( @@ -121,7 +125,9 @@ async def test_webhook_handle_render_template( async def test_webhook_handle_call_services( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that we call services properly.""" calls = async_mock_service(hass, "test", "mobile_app") @@ -137,7 +143,9 @@ async def test_webhook_handle_call_services( async def test_webhook_handle_fire_event( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that we can fire events.""" events = [] @@ -161,7 +169,7 @@ async def test_webhook_handle_fire_event( assert events[0].data["hello"] == "yo world" -async def test_webhook_update_registration(webhook_client) -> None: +async def test_webhook_update_registration(webhook_client: TestClient) -> None: """Test that a we can update an existing registration via webhook.""" register_resp = await webhook_client.post( "/api/mobile_app/registrations", json=REGISTER_CLEARTEXT @@ -186,7 +194,9 @@ async def test_webhook_update_registration(webhook_client) -> None: async def test_webhook_handle_get_zones( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that we can get zones properly.""" # Zone is already loaded as part of the fixture, @@ -238,7 +248,9 @@ async def test_webhook_handle_get_zones( async def test_webhook_handle_get_config( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that we can get config properly.""" webhook_id = create_registrations[1]["webhook_id"] @@ -299,7 +311,9 @@ async def test_webhook_handle_get_config( async def test_webhook_returns_error_incorrect_json( - webhook_client, create_registrations, caplog: pytest.LogCaptureFixture + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, + caplog: pytest.LogCaptureFixture, ) -> None: """Test that an error is returned when JSON is invalid.""" resp = await webhook_client.post( @@ -323,7 +337,11 @@ async def test_webhook_returns_error_incorrect_json( ], ) async def test_webhook_handle_decryption( - hass: HomeAssistant, webhook_client, create_registrations, msg, generate_response + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, + msg: dict[str, Any], + generate_response: Callable[[HomeAssistant], dict[str, Any]], ) -> None: """Test that we can encrypt/decrypt properly.""" key = create_registrations[0]["secret"] @@ -346,7 +364,8 @@ async def test_webhook_handle_decryption( async def test_webhook_handle_decryption_legacy( - webhook_client, create_registrations + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that we can encrypt/decrypt properly.""" key = create_registrations[0]["secret"] @@ -369,7 +388,9 @@ async def test_webhook_handle_decryption_legacy( async def test_webhook_handle_decryption_fail( - webhook_client, create_registrations, caplog: pytest.LogCaptureFixture + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, + caplog: pytest.LogCaptureFixture, ) -> None: """Test that we can encrypt/decrypt properly.""" key = create_registrations[0]["secret"] @@ -412,7 +433,9 @@ async def test_webhook_handle_decryption_fail( async def test_webhook_handle_decryption_legacy_fail( - webhook_client, create_registrations, caplog: pytest.LogCaptureFixture + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, + caplog: pytest.LogCaptureFixture, ) -> None: """Test that we can encrypt/decrypt properly.""" key = create_registrations[0]["secret"] @@ -455,7 +478,8 @@ async def test_webhook_handle_decryption_legacy_fail( async def test_webhook_handle_decryption_legacy_upgrade( - webhook_client, create_registrations + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that we can encrypt/decrypt properly.""" key = create_registrations[0]["secret"] @@ -510,7 +534,8 @@ async def test_webhook_handle_decryption_legacy_upgrade( async def test_webhook_requires_encryption( - webhook_client, create_registrations + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that encrypted registrations only accept encrypted data.""" resp = await webhook_client.post( @@ -527,7 +552,9 @@ async def test_webhook_requires_encryption( async def test_webhook_update_location_without_locations( - hass: HomeAssistant, webhook_client, create_registrations + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that location can be updated.""" @@ -564,7 +591,9 @@ async def test_webhook_update_location_without_locations( async def test_webhook_update_location_with_gps( - hass: HomeAssistant, webhook_client, create_registrations + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that location can be updated.""" resp = await webhook_client.post( @@ -586,7 +615,9 @@ async def test_webhook_update_location_with_gps( async def test_webhook_update_location_with_gps_without_accuracy( - hass: HomeAssistant, webhook_client, create_registrations + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that location can be updated.""" resp = await webhook_client.post( @@ -604,7 +635,9 @@ async def test_webhook_update_location_with_gps_without_accuracy( async def test_webhook_update_location_with_location_name( - hass: HomeAssistant, webhook_client, create_registrations + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that location can be updated.""" @@ -666,7 +699,9 @@ async def test_webhook_update_location_with_location_name( async def test_webhook_enable_encryption( - hass: HomeAssistant, webhook_client, create_registrations + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that encryption can be added to a reg initially created without.""" webhook_id = create_registrations[1]["webhook_id"] @@ -717,7 +752,9 @@ async def test_webhook_enable_encryption( async def test_webhook_camera_stream_non_existent( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test fetching camera stream URLs for a non-existent camera.""" webhook_id = create_registrations[1]["webhook_id"] @@ -736,7 +773,9 @@ async def test_webhook_camera_stream_non_existent( async def test_webhook_camera_stream_non_hls( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test fetching camera stream URLs for a non-HLS/stream-supporting camera.""" hass.states.async_set("camera.non_stream_camera", "idle", {"supported_features": 0}) @@ -761,7 +800,9 @@ async def test_webhook_camera_stream_non_hls( async def test_webhook_camera_stream_stream_available( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test fetching camera stream URLs for an HLS/stream-supporting camera.""" hass.states.async_set( @@ -791,7 +832,9 @@ async def test_webhook_camera_stream_stream_available( async def test_webhook_camera_stream_stream_available_but_errors( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test fetching camera stream URLs for an HLS/stream-supporting camera but that streaming errors.""" hass.states.async_set( @@ -823,8 +866,8 @@ async def test_webhook_camera_stream_stream_available_but_errors( async def test_webhook_handle_scan_tag( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - create_registrations, - webhook_client, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that we can scan tags.""" device = device_registry.async_get_device(identifiers={(DOMAIN, "mock-device-id")}) @@ -847,7 +890,9 @@ async def test_webhook_handle_scan_tag( async def test_register_sensor_limits_state_class( - hass: HomeAssistant, create_registrations, webhook_client + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that we limit state classes to sensors only.""" webhook_id = create_registrations[1]["webhook_id"] @@ -890,8 +935,8 @@ async def test_register_sensor_limits_state_class( async def test_reregister_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, - create_registrations, - webhook_client, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that we can add more info in re-registration.""" webhook_id = create_registrations[1]["webhook_id"] @@ -992,11 +1037,11 @@ async def test_reregister_sensor( assert entry.original_icon is None +@pytest.mark.usefixtures("homeassistant") async def test_webhook_handle_conversation_process( hass: HomeAssistant, - homeassistant, - create_registrations, - webhook_client, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, mock_conversation_agent: MockAgent, ) -> None: """Test that we can converse.""" @@ -1042,9 +1087,8 @@ async def test_webhook_handle_conversation_process( async def test_sending_sensor_state( hass: HomeAssistant, entity_registry: er.EntityRegistry, - create_registrations, - webhook_client, - caplog: pytest.LogCaptureFixture, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, ) -> None: """Test that we can register and send sensor state as number and None.""" webhook_id = create_registrations[1]["webhook_id"] From 135f15fdc3dfc17812c02260799a5c6b521ab126 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:01:24 +0200 Subject: [PATCH 2256/2411] Improve type hints in openai_conversation tests (#123811) --- tests/components/openai_conversation/conftest.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/components/openai_conversation/conftest.py b/tests/components/openai_conversation/conftest.py index 6d770b51ce9..4639d0dc8e0 100644 --- a/tests/components/openai_conversation/conftest.py +++ b/tests/components/openai_conversation/conftest.py @@ -13,7 +13,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_config_entry(hass): +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: """Mock a config entry.""" entry = MockConfigEntry( title="OpenAI", @@ -27,7 +27,9 @@ def mock_config_entry(hass): @pytest.fixture -def mock_config_entry_with_assist(hass, mock_config_entry): +def mock_config_entry_with_assist( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> MockConfigEntry: """Mock a config entry with assist.""" hass.config_entries.async_update_entry( mock_config_entry, options={CONF_LLM_HASS_API: llm.LLM_API_ASSIST} @@ -36,7 +38,9 @@ def mock_config_entry_with_assist(hass, mock_config_entry): @pytest.fixture -async def mock_init_component(hass, mock_config_entry): +async def mock_init_component( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Initialize integration.""" with patch( "openai.resources.models.AsyncModels.list", From 4cc3f7211bf249b2776b6c2e70bd7a5f311f0a83 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:07:17 +0200 Subject: [PATCH 2257/2411] Improve type hints in openuv tests (#123813) --- tests/components/openuv/conftest.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/components/openuv/conftest.py b/tests/components/openuv/conftest.py index cc344d25ccb..9bb1970bc2f 100644 --- a/tests/components/openuv/conftest.py +++ b/tests/components/openuv/conftest.py @@ -2,6 +2,7 @@ from collections.abc import Generator import json +from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest @@ -13,6 +14,7 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, ) +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -41,7 +43,9 @@ def client_fixture(data_protection_window, data_uv_index): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config): +def config_entry_fixture( + hass: HomeAssistant, config: dict[str, Any] +) -> MockConfigEntry: """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, @@ -54,7 +58,7 @@ def config_entry_fixture(hass, config): @pytest.fixture(name="config") -def config_fixture(): +def config_fixture() -> dict[str, Any]: """Define a config entry data fixture.""" return { CONF_API_KEY: TEST_API_KEY, @@ -89,7 +93,9 @@ async def mock_pyopenuv_fixture(client): @pytest.fixture(name="setup_config_entry") -async def setup_config_entry_fixture(hass, config_entry, mock_pyopenuv): +async def setup_config_entry_fixture( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_pyopenuv: None +) -> None: """Define a fixture to set up openuv.""" assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From e8157ed9a2d2b3d38492ccb9c885db699a145e37 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:08:15 +0200 Subject: [PATCH 2258/2411] Improve type hints in otbr tests (#123814) --- tests/components/otbr/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/otbr/conftest.py b/tests/components/otbr/conftest.py index ba0f43c4a71..56f29bdc79b 100644 --- a/tests/components/otbr/conftest.py +++ b/tests/components/otbr/conftest.py @@ -19,7 +19,7 @@ from tests.common import MockConfigEntry @pytest.fixture(name="otbr_config_entry_multipan") -async def otbr_config_entry_multipan_fixture(hass): +async def otbr_config_entry_multipan_fixture(hass: HomeAssistant) -> None: """Mock Open Thread Border Router config entry.""" config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA_MULTIPAN, @@ -46,7 +46,7 @@ async def otbr_config_entry_multipan_fixture(hass): @pytest.fixture(name="otbr_config_entry_thread") -async def otbr_config_entry_thread_fixture(hass): +async def otbr_config_entry_thread_fixture(hass: HomeAssistant) -> None: """Mock Open Thread Border Router config entry.""" config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA_THREAD, From 679baddd3d2050362d348d521c438691357ef2db Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:08:36 +0200 Subject: [PATCH 2259/2411] Improve type hints in openalpr_cloud tests (#123812) --- tests/components/openalpr_cloud/test_image_processing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/openalpr_cloud/test_image_processing.py b/tests/components/openalpr_cloud/test_image_processing.py index 7115c3e7bf0..143513f9852 100644 --- a/tests/components/openalpr_cloud/test_image_processing.py +++ b/tests/components/openalpr_cloud/test_image_processing.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components import camera, image_processing as ip from homeassistant.components.openalpr_cloud.image_processing import OPENALPR_API_URL -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.setup import async_setup_component from tests.common import assert_setup_component, async_capture_events, load_fixture @@ -15,13 +15,13 @@ from tests.test_util.aiohttp import AiohttpClientMocker @pytest.fixture(autouse=True) -async def setup_homeassistant(hass: HomeAssistant): +async def setup_homeassistant(hass: HomeAssistant) -> None: """Set up the homeassistant integration.""" await async_setup_component(hass, "homeassistant", {}) @pytest.fixture -async def setup_openalpr_cloud(hass): +async def setup_openalpr_cloud(hass: HomeAssistant) -> None: """Set up openalpr cloud.""" config = { ip.DOMAIN: { @@ -43,7 +43,7 @@ async def setup_openalpr_cloud(hass): @pytest.fixture -async def alpr_events(hass): +async def alpr_events(hass: HomeAssistant) -> list[Event]: """Listen for events.""" return async_capture_events(hass, "image_processing.found_plate") From 995ed778498a857587b844d3aeaa25ff6b37efb8 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Tue, 13 Aug 2024 10:23:13 -0400 Subject: [PATCH 2260/2411] Add error handling for Russound RIO async calls (#123756) Add better error handling to Russound RIO --- .../components/russound_rio/__init__.py | 8 +++-- .../components/russound_rio/media_player.py | 30 ++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/russound_rio/__init__.py b/homeassistant/components/russound_rio/__init__.py index 1560a4cd332..e36cedecfe3 100644 --- a/homeassistant/components/russound_rio/__init__.py +++ b/homeassistant/components/russound_rio/__init__.py @@ -8,7 +8,7 @@ from aiorussound import Russound from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError +from homeassistant.exceptions import ConfigEntryNotReady from .const import CONNECT_TIMEOUT, RUSSOUND_RIO_EXCEPTIONS @@ -22,13 +22,15 @@ type RussoundConfigEntry = ConfigEntry[Russound] async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) -> bool: """Set up a config entry.""" - russ = Russound(hass.loop, entry.data[CONF_HOST], entry.data[CONF_PORT]) + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + russ = Russound(hass.loop, host, port) try: async with asyncio.timeout(CONNECT_TIMEOUT): await russ.connect() except RUSSOUND_RIO_EXCEPTIONS as err: - raise ConfigEntryError(err) from err + raise ConfigEntryNotReady(f"Error while connecting to {host}:{port}") from err entry.runtime_data = russ diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index ff0d9e006c0..8cf07e23cc8 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -2,7 +2,10 @@ from __future__ import annotations +from collections.abc import Awaitable, Callable, Coroutine +from functools import wraps import logging +from typing import Any, Concatenate from aiorussound import Source, Zone @@ -17,12 +20,13 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import RussoundConfigEntry +from . import RUSSOUND_RIO_EXCEPTIONS, RussoundConfigEntry from .const import DOMAIN, MP_FEATURES_BY_FLAG _LOGGER = logging.getLogger(__name__) @@ -107,6 +111,24 @@ async def async_setup_entry( async_add_entities(entities) +def command[_T: RussoundZoneDevice, **_P]( + func: Callable[Concatenate[_T, _P], Awaitable[None]], +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: + """Wrap async calls to raise on request error.""" + + @wraps(func) + async def decorator(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + """Wrap all command methods.""" + try: + await func(self, *args, **kwargs) + except RUSSOUND_RIO_EXCEPTIONS as exc: + raise HomeAssistantError( + f"Error executing {func.__name__} on entity {self.entity_id}," + ) from exc + + return decorator + + class RussoundZoneDevice(MediaPlayerEntity): """Representation of a Russound Zone.""" @@ -221,19 +243,23 @@ class RussoundZoneDevice(MediaPlayerEntity): """ return float(self._zone.volume or "0") / 50.0 + @command async def async_turn_off(self) -> None: """Turn off the zone.""" await self._zone.zone_off() + @command async def async_turn_on(self) -> None: """Turn on the zone.""" await self._zone.zone_on() + @command async def async_set_volume_level(self, volume: float) -> None: """Set the volume level.""" rvol = int(volume * 50.0) await self._zone.set_volume(rvol) + @command async def async_select_source(self, source: str) -> None: """Select the source input for this zone.""" for source_id, src in self._sources.items(): @@ -242,10 +268,12 @@ class RussoundZoneDevice(MediaPlayerEntity): await self._zone.select_source(source_id) break + @command async def async_volume_up(self) -> None: """Step the volume up.""" await self._zone.volume_up() + @command async def async_volume_down(self) -> None: """Step the volume down.""" await self._zone.volume_down() From ba54a19d4b9819f2f35ad628089370c6c2a37c7f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 Aug 2024 18:01:06 +0200 Subject: [PATCH 2261/2411] Simplify mock_tts_cache_dir_autouse fixture (#123783) --- tests/components/assist_pipeline/conftest.py | 3 +-- tests/components/cloud/conftest.py | 3 +-- tests/components/elevenlabs/test_tts.py | 4 ++-- tests/components/google_translate/test_tts.py | 3 +-- tests/components/marytts/test_tts.py | 3 +-- tests/components/microsoft/test_tts.py | 3 +-- tests/components/voicerss/test_tts.py | 3 +-- tests/components/voip/test_voip.py | 3 +-- tests/components/wyoming/conftest.py | 3 +-- tests/components/yandextts/test_tts.py | 3 +-- 10 files changed, 11 insertions(+), 20 deletions(-) diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index b2eca1e7ce1..141baaa9870 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -43,9 +43,8 @@ BYTES_ONE_SECOND = SAMPLE_RATE * SAMPLE_WIDTH * SAMPLE_CHANNELS @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> None: """Mock the TTS cache dir with empty dir.""" - return mock_tts_cache_dir class BaseProvider: diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 3a5d333f9b8..2edd9571bdd 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -187,9 +187,8 @@ def set_cloud_prefs_fixture( @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> None: """Mock the TTS cache dir with empty dir.""" - return mock_tts_cache_dir @pytest.fixture(autouse=True) diff --git a/tests/components/elevenlabs/test_tts.py b/tests/components/elevenlabs/test_tts.py index 5eee4084452..8b14ab26487 100644 --- a/tests/components/elevenlabs/test_tts.py +++ b/tests/components/elevenlabs/test_tts.py @@ -3,6 +3,7 @@ from __future__ import annotations from http import HTTPStatus +from pathlib import Path from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -34,9 +35,8 @@ def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock: MagicMock) -> None: @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir): +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> None: """Mock the TTS cache dir with empty dir.""" - return mock_tts_cache_dir @pytest.fixture diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 41cecd8cd98..95313df6140 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -30,9 +30,8 @@ def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock: MagicMock) -> None: @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> None: """Mock the TTS cache dir with empty dir.""" - return mock_tts_cache_dir @pytest.fixture(autouse=True) diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 75784bb56c5..0ad27cde29b 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -34,9 +34,8 @@ def get_empty_wav() -> bytes: @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> None: """Mock the TTS cache dir with empty dir.""" - return mock_tts_cache_dir async def test_setup_component(hass: HomeAssistant) -> None: diff --git a/tests/components/microsoft/test_tts.py b/tests/components/microsoft/test_tts.py index dca760230ac..0f11501843e 100644 --- a/tests/components/microsoft/test_tts.py +++ b/tests/components/microsoft/test_tts.py @@ -20,9 +20,8 @@ from tests.typing import ClientSessionGenerator @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> None: """Mock the TTS cache dir with empty dir.""" - return mock_tts_cache_dir @pytest.fixture(autouse=True) diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index 1a2ad002586..776c0ac153a 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -36,9 +36,8 @@ def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock: MagicMock) -> None: @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> None: """Mock the TTS cache dir with empty dir.""" - return mock_tts_cache_dir async def test_setup_component(hass: HomeAssistant) -> None: diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index c2978afc17f..aab35bfd029 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -19,9 +19,8 @@ _MEDIA_ID = "12345" @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> None: """Mock the TTS cache dir with empty dir.""" - return mock_tts_cache_dir def _empty_wav() -> bytes: diff --git a/tests/components/wyoming/conftest.py b/tests/components/wyoming/conftest.py index f6093e34261..770186d92aa 100644 --- a/tests/components/wyoming/conftest.py +++ b/tests/components/wyoming/conftest.py @@ -19,9 +19,8 @@ from tests.common import MockConfigEntry @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> None: """Mock the TTS cache dir with empty dir.""" - return mock_tts_cache_dir @pytest.fixture(autouse=True) diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index 496c187469a..77878c2be51 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -29,9 +29,8 @@ def tts_mutagen_mock_fixture_autouse(tts_mutagen_mock: MagicMock) -> None: @pytest.fixture(autouse=True) -def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: +def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> None: """Mock the TTS cache dir with empty dir.""" - return mock_tts_cache_dir async def test_setup_component(hass: HomeAssistant) -> None: From 493859e5898b2fa8696205208f6a2e88f0cd9731 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 13 Aug 2024 18:44:12 +0200 Subject: [PATCH 2262/2411] Add update platform to AirGradient (#123534) --- .../components/airgradient/__init__.py | 1 + .../components/airgradient/update.py | 55 +++++++++++++++ tests/components/airgradient/conftest.py | 1 + .../airgradient/snapshots/test_update.ambr | 58 ++++++++++++++++ tests/components/airgradient/test_update.py | 69 +++++++++++++++++++ 5 files changed, 184 insertions(+) create mode 100644 homeassistant/components/airgradient/update.py create mode 100644 tests/components/airgradient/snapshots/test_update.ambr create mode 100644 tests/components/airgradient/test_update.py diff --git a/homeassistant/components/airgradient/__init__.py b/homeassistant/components/airgradient/__init__.py index 69f1e70c6af..7ee8ac6a3c7 100644 --- a/homeassistant/components/airgradient/__init__.py +++ b/homeassistant/components/airgradient/__init__.py @@ -21,6 +21,7 @@ PLATFORMS: list[Platform] = [ Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.UPDATE, ] diff --git a/homeassistant/components/airgradient/update.py b/homeassistant/components/airgradient/update.py new file mode 100644 index 00000000000..95e64930ea6 --- /dev/null +++ b/homeassistant/components/airgradient/update.py @@ -0,0 +1,55 @@ +"""Airgradient Update platform.""" + +from datetime import timedelta +from functools import cached_property + +from homeassistant.components.update import UpdateDeviceClass, UpdateEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AirGradientConfigEntry, AirGradientMeasurementCoordinator +from .entity import AirGradientEntity + +SCAN_INTERVAL = timedelta(hours=1) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: AirGradientConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Airgradient update platform.""" + + data = config_entry.runtime_data + + async_add_entities([AirGradientUpdate(data.measurement)], True) + + +class AirGradientUpdate(AirGradientEntity, UpdateEntity): + """Representation of Airgradient Update.""" + + _attr_device_class = UpdateDeviceClass.FIRMWARE + coordinator: AirGradientMeasurementCoordinator + + def __init__(self, coordinator: AirGradientMeasurementCoordinator) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.serial_number}-update" + + @cached_property + def should_poll(self) -> bool: + """Return True because we need to poll the latest version.""" + return True + + @property + def installed_version(self) -> str: + """Return the installed version of the entity.""" + return self.coordinator.data.firmware_version + + async def async_update(self) -> None: + """Update the entity.""" + self._attr_latest_version = ( + await self.coordinator.client.get_latest_firmware_version( + self.coordinator.serial_number + ) + ) diff --git a/tests/components/airgradient/conftest.py b/tests/components/airgradient/conftest.py index a6ee85ecbdd..1899e12c8ae 100644 --- a/tests/components/airgradient/conftest.py +++ b/tests/components/airgradient/conftest.py @@ -44,6 +44,7 @@ def mock_airgradient_client() -> Generator[AsyncMock]: client.get_config.return_value = Config.from_json( load_fixture("get_config_local.json", DOMAIN) ) + client.get_latest_firmware_version.return_value = "3.1.4" yield client diff --git a/tests/components/airgradient/snapshots/test_update.ambr b/tests/components/airgradient/snapshots/test_update.ambr new file mode 100644 index 00000000000..c639a97d5dd --- /dev/null +++ b/tests/components/airgradient/snapshots/test_update.ambr @@ -0,0 +1,58 @@ +# serializer version: 1 +# name: test_all_entities[update.airgradient_firmware-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'update', + 'entity_category': , + 'entity_id': 'update.airgradient_firmware', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Firmware', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '84fce612f5b8-update', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[update.airgradient_firmware-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'device_class': 'firmware', + 'entity_picture': 'https://brands.home-assistant.io/_/airgradient/icon.png', + 'friendly_name': 'Airgradient Firmware', + 'in_progress': False, + 'installed_version': '3.1.1', + 'latest_version': '3.1.4', + 'release_summary': None, + 'release_url': None, + 'skipped_version': None, + 'supported_features': , + 'title': None, + }), + 'context': , + 'entity_id': 'update.airgradient_firmware', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/airgradient/test_update.py b/tests/components/airgradient/test_update.py new file mode 100644 index 00000000000..020a9a82a71 --- /dev/null +++ b/tests/components/airgradient/test_update.py @@ -0,0 +1,69 @@ +"""Tests for the AirGradient update platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion + +from homeassistant.const import STATE_OFF, STATE_ON, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.UPDATE]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_update_mechanism( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test update entity.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("update.airgradient_firmware") + assert state.state == STATE_ON + assert state.attributes["installed_version"] == "3.1.1" + assert state.attributes["latest_version"] == "3.1.4" + mock_airgradient_client.get_latest_firmware_version.assert_called_once() + mock_airgradient_client.get_latest_firmware_version.reset_mock() + + mock_airgradient_client.get_current_measures.return_value.firmware_version = "3.1.4" + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("update.airgradient_firmware") + assert state.state == STATE_OFF + assert state.attributes["installed_version"] == "3.1.4" + assert state.attributes["latest_version"] == "3.1.4" + + mock_airgradient_client.get_latest_firmware_version.return_value = "3.1.5" + + freezer.tick(timedelta(minutes=59)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_airgradient_client.get_latest_firmware_version.assert_called_once() + state = hass.states.get("update.airgradient_firmware") + assert state.state == STATE_ON + assert state.attributes["installed_version"] == "3.1.4" + assert state.attributes["latest_version"] == "3.1.5" From f14d5ba5f28da23d86b6b8eedd6c8443f09d2272 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Aug 2024 14:06:38 -0500 Subject: [PATCH 2263/2411] Bump yalexs to 8.0.2 (#123817) --- homeassistant/components/august/config_flow.py | 2 +- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/mocks.py | 2 +- tests/components/august/test_config_flow.py | 2 +- tests/components/august/test_gateway.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index 18c15ad61a1..3523a4f7c39 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -8,7 +8,7 @@ from typing import Any import aiohttp import voluptuous as vol -from yalexs.authenticator import ValidationResult +from yalexs.authenticator_common import ValidationResult from yalexs.const import BRANDS, DEFAULT_BRAND from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 5a911eee5e5..13035d68dfe 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==6.5.0", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.0.2", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index e19819dafde..1c6163c4bb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2953,7 +2953,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==6.5.0 +yalexs==8.0.2 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 575fd7757be..496789466d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2336,7 +2336,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==6.5.0 +yalexs==8.0.2 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 30be50e75c9..a0f5b55a607 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -25,7 +25,7 @@ from yalexs.activity import ( DoorOperationActivity, LockOperationActivity, ) -from yalexs.authenticator import AuthenticationState +from yalexs.authenticator_common import AuthenticationState from yalexs.const import Brand from yalexs.doorbell import Doorbell, DoorbellDetail from yalexs.lock import Lock, LockDetail diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index aec08864c65..fdebb8d5c46 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from yalexs.authenticator import ValidationResult +from yalexs.authenticator_common import ValidationResult from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from homeassistant import config_entries diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index e605fd74f0a..74266397ed5 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -50,5 +50,5 @@ async def _patched_refresh_access_token( ) await august_gateway.async_refresh_access_token_if_needed() refresh_access_token_mock.assert_called() - assert august_gateway.access_token == new_token + assert await august_gateway.async_get_access_token() == new_token assert august_gateway.authentication.access_token_expires == new_token_expire_time From f8bc662620943c8f3441d7ebeb13374342e55978 Mon Sep 17 00:00:00 2001 From: Paarth Shah Date: Tue, 13 Aug 2024 12:31:59 -0700 Subject: [PATCH 2264/2411] Bump `matrix-nio` to 0.25.0 (#123832) Bump matrix-nio to 0.25.0 Co-authored-by: J. Nick Koston --- homeassistant/components/matrix/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index 7e854a85434..3c465c44f24 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/matrix", "iot_class": "cloud_push", "loggers": ["matrix_client"], - "requirements": ["matrix-nio==0.24.0", "Pillow==10.4.0"] + "requirements": ["matrix-nio==0.25.0", "Pillow==10.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1c6163c4bb9..a87cf317b7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1297,7 +1297,7 @@ lw12==0.9.2 lxml==5.1.0 # homeassistant.components.matrix -matrix-nio==0.24.0 +matrix-nio==0.25.0 # homeassistant.components.maxcube maxcube-api==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 496789466d2..1fb0f027ede 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1072,7 +1072,7 @@ lupupy==0.3.2 lxml==5.1.0 # homeassistant.components.matrix -matrix-nio==0.24.0 +matrix-nio==0.25.0 # homeassistant.components.maxcube maxcube-api==0.4.3 From 4a6e81296303cf9b3e72f9d4daffa8a2f22bd4a6 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 13 Aug 2024 22:21:48 +0200 Subject: [PATCH 2265/2411] Bump py-synologydsm-api to 2.4.5 (#123815) bump py-synologydsm-api to 2.4.5 --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index b1133fd61ad..9d977609d14 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/synology_dsm", "iot_class": "local_polling", "loggers": ["synology_dsm"], - "requirements": ["py-synologydsm-api==2.4.4"], + "requirements": ["py-synologydsm-api==2.4.5"], "ssdp": [ { "manufacturer": "Synology", diff --git a/requirements_all.txt b/requirements_all.txt index a87cf317b7b..535fc6a95ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1662,7 +1662,7 @@ py-schluter==0.1.7 py-sucks==0.9.10 # homeassistant.components.synology_dsm -py-synologydsm-api==2.4.4 +py-synologydsm-api==2.4.5 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1fb0f027ede..0fd598c84c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1354,7 +1354,7 @@ py-nightscout==1.2.2 py-sucks==0.9.10 # homeassistant.components.synology_dsm -py-synologydsm-api==2.4.4 +py-synologydsm-api==2.4.5 # homeassistant.components.hdmi_cec pyCEC==0.5.2 From 2b6949f3c7dd86ce5d1a729be1ef787975108f32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Aug 2024 16:29:26 -0500 Subject: [PATCH 2266/2411] Bump uiprotect to 6.0.2 (#123808) changelog: https://github.com/uilibs/uiprotect/compare/v6.0.1...v6.0.2 --- 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 93536d1ad1b..4483a5990eb 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==6.0.1", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==6.0.2", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 535fc6a95ba..cadd93c2b1f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2813,7 +2813,7 @@ twitchAPI==4.2.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==6.0.1 +uiprotect==6.0.2 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fd598c84c2..2b5581f3181 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2217,7 +2217,7 @@ twitchAPI==4.2.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==6.0.1 +uiprotect==6.0.2 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 29887c2a17d40c7e413919ef09557ea0f3dfb6ef Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:40:51 -0400 Subject: [PATCH 2267/2411] Add base entity to Russound RIO integration (#123842) * Add base entity to Russound RIO integration * Set entity back to primary mac addr * Switch to type shorthand --- .../components/russound_rio/entity.py | 70 +++++++++++++++++++ .../components/russound_rio/media_player.py | 59 ++-------------- 2 files changed, 75 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/russound_rio/entity.py diff --git a/homeassistant/components/russound_rio/entity.py b/homeassistant/components/russound_rio/entity.py new file mode 100644 index 00000000000..3430a77108b --- /dev/null +++ b/homeassistant/components/russound_rio/entity.py @@ -0,0 +1,70 @@ +"""Base entity for Russound RIO integration.""" + +from collections.abc import Awaitable, Callable, Coroutine +from functools import wraps +from typing import Any, Concatenate + +from aiorussound import Controller + +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN, RUSSOUND_RIO_EXCEPTIONS + + +def command[_EntityT: RussoundBaseEntity, **_P]( + func: Callable[Concatenate[_EntityT, _P], Awaitable[None]], +) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]: + """Wrap async calls to raise on request error.""" + + @wraps(func) + async def decorator(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None: + """Wrap all command methods.""" + try: + await func(self, *args, **kwargs) + except RUSSOUND_RIO_EXCEPTIONS as exc: + raise HomeAssistantError( + f"Error executing {func.__name__} on entity {self.entity_id}," + ) from exc + + return decorator + + +class RussoundBaseEntity(Entity): + """Russound Base Entity.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__( + self, + controller: Controller, + ) -> None: + """Initialize the entity.""" + self._instance = controller.instance + self._controller = controller + self._primary_mac_address = ( + controller.mac_address or controller.parent_controller.mac_address + ) + self._device_identifier = ( + self._controller.mac_address + or f"{self._primary_mac_address}-{self._controller.controller_id}" + ) + self._attr_device_info = DeviceInfo( + # Use MAC address of Russound device as identifier + identifiers={(DOMAIN, self._device_identifier)}, + manufacturer="Russound", + name=controller.controller_type, + model=controller.controller_type, + sw_version=controller.firmware_version, + ) + if controller.parent_controller: + self._attr_device_info["via_device"] = ( + DOMAIN, + controller.parent_controller.mac_address, + ) + else: + self._attr_device_info["connections"] = { + (CONNECTION_NETWORK_MAC, controller.mac_address) + } diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 8cf07e23cc8..5f11227ef53 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -2,10 +2,7 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable, Coroutine -from functools import wraps import logging -from typing import Any, Concatenate from aiorussound import Source, Zone @@ -20,14 +17,13 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import RUSSOUND_RIO_EXCEPTIONS, RussoundConfigEntry +from . import RussoundConfigEntry from .const import DOMAIN, MP_FEATURES_BY_FLAG +from .entity import RussoundBaseEntity, command _LOGGER = logging.getLogger(__name__) @@ -111,31 +107,11 @@ async def async_setup_entry( async_add_entities(entities) -def command[_T: RussoundZoneDevice, **_P]( - func: Callable[Concatenate[_T, _P], Awaitable[None]], -) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: - """Wrap async calls to raise on request error.""" - - @wraps(func) - async def decorator(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: - """Wrap all command methods.""" - try: - await func(self, *args, **kwargs) - except RUSSOUND_RIO_EXCEPTIONS as exc: - raise HomeAssistantError( - f"Error executing {func.__name__} on entity {self.entity_id}," - ) from exc - - return decorator - - -class RussoundZoneDevice(MediaPlayerEntity): +class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): """Representation of a Russound Zone.""" _attr_device_class = MediaPlayerDeviceClass.SPEAKER _attr_media_content_type = MediaType.MUSIC - _attr_should_poll = False - _attr_has_entity_name = True _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_STEP @@ -146,36 +122,11 @@ class RussoundZoneDevice(MediaPlayerEntity): def __init__(self, zone: Zone, sources: dict[int, Source]) -> None: """Initialize the zone device.""" - self._controller = zone.controller + super().__init__(zone.controller) self._zone = zone self._sources = sources self._attr_name = zone.name - primary_mac_address = ( - self._controller.mac_address - or self._controller.parent_controller.mac_address - ) - self._attr_unique_id = f"{primary_mac_address}-{zone.device_str()}" - device_identifier = ( - self._controller.mac_address - or f"{primary_mac_address}-{self._controller.controller_id}" - ) - self._attr_device_info = DeviceInfo( - # Use MAC address of Russound device as identifier - identifiers={(DOMAIN, device_identifier)}, - manufacturer="Russound", - name=self._controller.controller_type, - model=self._controller.controller_type, - sw_version=self._controller.firmware_version, - ) - if self._controller.parent_controller: - self._attr_device_info["via_device"] = ( - DOMAIN, - self._controller.parent_controller.mac_address, - ) - else: - self._attr_device_info["connections"] = { - (CONNECTION_NETWORK_MAC, self._controller.mac_address) - } + self._attr_unique_id = f"{self._primary_mac_address}-{zone.device_str()}" for flag, feature in MP_FEATURES_BY_FLAG.items(): if flag in zone.instance.supported_features: self._attr_supported_features |= feature From b7bbc938d348b2ae82766f434d508ca9f75b1522 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Aug 2024 09:31:37 +0200 Subject: [PATCH 2268/2411] Drop violating rows before adding foreign constraints in DB schema 44 migration (#123454) * Drop violating rows before adding foreign constraints * Don't delete rows with null-references * Only delete rows when integrityerror is caught * Move restore of dropped foreign key constraints to a separate migration step * Use aliases for tables * Update homeassistant/components/recorder/migration.py * Update test * Don't use alias for table we're deleting from, improve test * Fix MySQL * Update instead of deleting in case of self references * Improve log messages * Batch updates * Add workaround for unsupported LIMIT in PostgreSQL * Simplify --------- Co-authored-by: J. Nick Koston --- .../components/recorder/db_schema.py | 2 +- .../components/recorder/migration.py | 239 +++++++++++++++--- tests/components/recorder/test_migrate.py | 115 +++++++-- 3 files changed, 304 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 8d4cc29d9be..dd293ed6bc2 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -77,7 +77,7 @@ class LegacyBase(DeclarativeBase): """Base class for tables, used for schema migration.""" -SCHEMA_VERSION = 44 +SCHEMA_VERSION = 45 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index a41de07e243..55856dcf449 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -669,33 +669,177 @@ def _drop_foreign_key_constraints( def _restore_foreign_key_constraints( session_maker: Callable[[], Session], engine: Engine, - dropped_constraints: list[tuple[str, str, ReflectedForeignKeyConstraint]], + foreign_columns: list[tuple[str, str, str | None, str | None]], ) -> None: """Restore foreign key constraints.""" - for table, column, dropped_constraint in dropped_constraints: + for table, column, foreign_table, foreign_column in foreign_columns: constraints = Base.metadata.tables[table].foreign_key_constraints for constraint in constraints: if constraint.column_keys == [column]: break else: - _LOGGER.info( - "Did not find a matching constraint for %s", dropped_constraint - ) + _LOGGER.info("Did not find a matching constraint for %s.%s", table, column) continue + if TYPE_CHECKING: + assert foreign_table is not None + assert foreign_column is not None + # AddConstraint mutates the constraint passed to it, we need to # undo that to avoid changing the behavior of the table schema. # https://github.com/sqlalchemy/sqlalchemy/blob/96f1172812f858fead45cdc7874abac76f45b339/lib/sqlalchemy/sql/ddl.py#L746-L748 create_rule = constraint._create_rule # noqa: SLF001 add_constraint = AddConstraint(constraint) # type: ignore[no-untyped-call] constraint._create_rule = create_rule # noqa: SLF001 + try: + _add_constraint(session_maker, add_constraint, table, column) + except IntegrityError: + _LOGGER.exception( + ( + "Could not update foreign options in %s table, will delete " + "violations and try again" + ), + table, + ) + _delete_foreign_key_violations( + session_maker, engine, table, column, foreign_table, foreign_column + ) + _add_constraint(session_maker, add_constraint, table, column) - with session_scope(session=session_maker()) as session: - try: - connection = session.connection() - connection.execute(add_constraint) - except (InternalError, OperationalError): - _LOGGER.exception("Could not update foreign options in %s table", table) + +def _add_constraint( + session_maker: Callable[[], Session], + add_constraint: AddConstraint, + table: str, + column: str, +) -> None: + """Add a foreign key constraint.""" + _LOGGER.warning( + "Adding foreign key constraint to %s.%s. " + "Note: this can take several minutes on large databases and slow " + "machines. Please be patient!", + table, + column, + ) + with session_scope(session=session_maker()) as session: + try: + connection = session.connection() + connection.execute(add_constraint) + except (InternalError, OperationalError): + _LOGGER.exception("Could not update foreign options in %s table", table) + + +def _delete_foreign_key_violations( + session_maker: Callable[[], Session], + engine: Engine, + table: str, + column: str, + foreign_table: str, + foreign_column: str, +) -> None: + """Remove rows which violate the constraints.""" + if engine.dialect.name not in (SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL): + raise RuntimeError( + f"_delete_foreign_key_violations not supported for {engine.dialect.name}" + ) + + _LOGGER.warning( + "Rows in table %s where %s references non existing %s.%s will be %s. " + "Note: this can take several minutes on large databases and slow " + "machines. Please be patient!", + table, + column, + foreign_table, + foreign_column, + "set to NULL" if table == foreign_table else "deleted", + ) + + result: CursorResult | None = None + if table == foreign_table: + # In case of a foreign reference to the same table, we set invalid + # references to NULL instead of deleting as deleting rows may + # cause additional invalid references to be created. This is to handle + # old_state_id referencing a missing state. + if engine.dialect.name == SupportedDialect.MYSQL: + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + # The subquery (SELECT {foreign_column} from {foreign_table}) is + # to be compatible with old MySQL versions which do not allow + # referencing the table being updated in the WHERE clause. + result = session.connection().execute( + text( + f"UPDATE {table} as t1 " # noqa: S608 + f"SET {column} = NULL " + "WHERE (" + f"t1.{column} IS NOT NULL AND " + "NOT EXISTS " + "(SELECT 1 " + f"FROM (SELECT {foreign_column} from {foreign_table}) AS t2 " + f"WHERE t2.{foreign_column} = t1.{column})) " + "LIMIT 100000;" + ) + ) + elif engine.dialect.name == SupportedDialect.POSTGRESQL: + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + # PostgreSQL does not support LIMIT in UPDATE clauses, so we + # update matches from a limited subquery instead. + result = session.connection().execute( + text( + f"UPDATE {table} " # noqa: S608 + f"SET {column} = NULL " + f"WHERE {column} in " + f"(SELECT {column} from {table} as t1 " + "WHERE (" + f"t1.{column} IS NOT NULL AND " + "NOT EXISTS " + "(SELECT 1 " + f"FROM {foreign_table} AS t2 " + f"WHERE t2.{foreign_column} = t1.{column})) " + "LIMIT 100000);" + ) + ) + return + + if engine.dialect.name == SupportedDialect.MYSQL: + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + result = session.connection().execute( + # We don't use an alias for the table we're deleting from, + # support of the form `DELETE FROM table AS t1` was added in + # MariaDB 11.6 and is not supported by MySQL. Those engines + # instead support the from `DELETE t1 from table AS t1` which + # is not supported by PostgreSQL and undocumented for MariaDB. + text( + f"DELETE FROM {table} " # noqa: S608 + "WHERE (" + f"{table}.{column} IS NOT NULL AND " + "NOT EXISTS " + "(SELECT 1 " + f"FROM {foreign_table} AS t2 " + f"WHERE t2.{foreign_column} = {table}.{column})) " + "LIMIT 100000;" + ) + ) + elif engine.dialect.name == SupportedDialect.POSTGRESQL: + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + # PostgreSQL does not support LIMIT in DELETE clauses, so we + # delete matches from a limited subquery instead. + result = session.connection().execute( + text( + f"DELETE FROM {table} " # noqa: S608 + f"WHERE {column} in " + f"(SELECT {column} from {table} as t1 " + "WHERE (" + f"t1.{column} IS NOT NULL AND " + "NOT EXISTS " + "(SELECT 1 " + f"FROM {foreign_table} AS t2 " + f"WHERE t2.{foreign_column} = t1.{column})) " + "LIMIT 100000);" + ) + ) @database_job_retry_wrapper("Apply migration update", 10) @@ -1459,6 +1603,38 @@ class _SchemaVersion43Migrator(_SchemaVersionMigrator, target_version=43): ) +FOREIGN_COLUMNS = ( + ( + "events", + ("data_id", "event_type_id"), + ( + ("data_id", "event_data", "data_id"), + ("event_type_id", "event_types", "event_type_id"), + ), + ), + ( + "states", + ("event_id", "old_state_id", "attributes_id", "metadata_id"), + ( + ("event_id", None, None), + ("old_state_id", "states", "state_id"), + ("attributes_id", "state_attributes", "attributes_id"), + ("metadata_id", "states_meta", "metadata_id"), + ), + ), + ( + "statistics", + ("metadata_id",), + (("metadata_id", "statistics_meta", "id"),), + ), + ( + "statistics_short_term", + ("metadata_id",), + (("metadata_id", "statistics_meta", "id"),), + ), +) + + class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44): def _apply_update(self) -> None: """Version specific update method.""" @@ -1471,24 +1647,14 @@ class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44): else "" ) # First drop foreign key constraints - foreign_columns = ( - ("events", ("data_id", "event_type_id")), - ("states", ("event_id", "old_state_id", "attributes_id", "metadata_id")), - ("statistics", ("metadata_id",)), - ("statistics_short_term", ("metadata_id",)), - ) - dropped_constraints = [ - dropped_constraint - for table, columns in foreign_columns - for column in columns - for dropped_constraint in _drop_foreign_key_constraints( - self.session_maker, self.engine, table, column - )[1] - ] - _LOGGER.debug("Dropped foreign key constraints: %s", dropped_constraints) + for table, columns, _ in FOREIGN_COLUMNS: + for column in columns: + _drop_foreign_key_constraints( + self.session_maker, self.engine, table, column + ) # Then modify the constrained columns - for table, columns in foreign_columns: + for table, columns, _ in FOREIGN_COLUMNS: _modify_columns( self.session_maker, self.engine, @@ -1518,9 +1684,24 @@ class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44): table, [f"{column} {BIG_INTEGER_SQL} {identity_sql}"], ) - # Finally restore dropped constraints + + +class _SchemaVersion45Migrator(_SchemaVersionMigrator, target_version=45): + def _apply_update(self) -> None: + """Version specific update method.""" + # We skip this step for SQLITE, it doesn't have differently sized integers + if self.engine.dialect.name == SupportedDialect.SQLITE: + return + + # Restore constraints dropped in migration to schema version 44 _restore_foreign_key_constraints( - self.session_maker, self.engine, dropped_constraints + self.session_maker, + self.engine, + [ + (table, column, foreign_table, foreign_column) + for table, _, foreign_mappings in FOREIGN_COLUMNS + for column, foreign_table, foreign_column in foreign_mappings + ], ) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index e55793caad7..988eade29b6 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -831,9 +831,9 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: """ constraints_to_recreate = ( - ("events", "data_id"), - ("states", "event_id"), # This won't be found - ("states", "old_state_id"), + ("events", "data_id", "event_data", "data_id"), + ("states", "event_id", None, None), # This won't be found + ("states", "old_state_id", "states", "state_id"), ) db_engine = recorder_db_url.partition("://")[0] @@ -902,7 +902,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: session_maker = Mock(return_value=session) dropped_constraints_1 = [ dropped_constraint - for table, column in constraints_to_recreate + for table, column, _, _ in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column )[1] @@ -914,7 +914,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: session_maker = Mock(return_value=session) dropped_constraints_2 = [ dropped_constraint - for table, column in constraints_to_recreate + for table, column, _, _ in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column )[1] @@ -925,7 +925,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: with Session(engine) as session: session_maker = Mock(return_value=session) migration._restore_foreign_key_constraints( - session_maker, engine, dropped_constraints_1 + session_maker, engine, constraints_to_recreate ) # Check we do find the constrained columns again (they are restored) @@ -933,7 +933,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: session_maker = Mock(return_value=session) dropped_constraints_3 = [ dropped_constraint - for table, column in constraints_to_recreate + for table, column, _, _ in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column )[1] @@ -951,21 +951,7 @@ def test_restore_foreign_key_constraints_with_error( This is not supported on SQLite """ - constraints_to_restore = [ - ( - "events", - "data_id", - { - "comment": None, - "constrained_columns": ["data_id"], - "name": "events_data_id_fkey", - "options": {}, - "referred_columns": ["data_id"], - "referred_schema": None, - "referred_table": "event_data", - }, - ), - ] + constraints_to_restore = [("events", "data_id", "event_data", "data_id")] connection = Mock() connection.execute = Mock(side_effect=InternalError(None, None, None)) @@ -981,3 +967,88 @@ def test_restore_foreign_key_constraints_with_error( ) assert "Could not update foreign options in events table" in caplog.text + + +@pytest.mark.skip_on_db_engine(["sqlite"]) +@pytest.mark.usefixtures("skip_by_db_engine") +def test_restore_foreign_key_constraints_with_integrity_error( + recorder_db_url: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test we can drop and then restore foreign keys. + + This is not supported on SQLite + """ + + constraints = ( + ("events", "data_id", "event_data", "data_id", Events), + ("states", "old_state_id", "states", "state_id", States), + ) + + engine = create_engine(recorder_db_url) + db_schema.Base.metadata.create_all(engine) + + # Drop constraints + with Session(engine) as session: + session_maker = Mock(return_value=session) + for table, column, _, _, _ in constraints: + migration._drop_foreign_key_constraints( + session_maker, engine, table, column + ) + + # Add rows violating the constraints + with Session(engine) as session: + for _, column, _, _, table_class in constraints: + session.add(table_class(**{column: 123})) + session.add(table_class()) + # Insert a States row referencing the row with an invalid foreign reference + session.add(States(old_state_id=1)) + session.commit() + + # Check we could insert the rows + with Session(engine) as session: + assert session.query(Events).count() == 2 + assert session.query(States).count() == 3 + + # Restore constraints + to_restore = [ + (table, column, foreign_table, foreign_column) + for table, column, foreign_table, foreign_column, _ in constraints + ] + with Session(engine) as session: + session_maker = Mock(return_value=session) + migration._restore_foreign_key_constraints(session_maker, engine, to_restore) + + # Check the violating row has been deleted from the Events table + with Session(engine) as session: + assert session.query(Events).count() == 1 + assert session.query(States).count() == 3 + + engine.dispose() + + assert ( + "Could not update foreign options in events table, " + "will delete violations and try again" + ) in caplog.text + + +def test_delete_foreign_key_violations_unsupported_engine( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test calling _delete_foreign_key_violations with an unsupported engine.""" + + connection = Mock() + connection.execute = Mock(side_effect=InternalError(None, None, None)) + session = Mock() + session.connection = Mock(return_value=connection) + instance = Mock() + instance.get_session = Mock(return_value=session) + engine = Mock() + engine.dialect = Mock() + engine.dialect.name = "sqlite" + + session_maker = Mock(return_value=session) + with pytest.raises( + RuntimeError, match="_delete_foreign_key_violations not supported for sqlite" + ): + migration._delete_foreign_key_violations(session_maker, engine, "", "", "", "") From 5f967fdee25588d50b15bee5429548d5eb096b4a Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Wed, 14 Aug 2024 09:11:11 +0100 Subject: [PATCH 2269/2411] Correct case of config strings in Mastodon (#123859) Fix string casing --- homeassistant/components/mastodon/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mastodon/strings.json b/homeassistant/components/mastodon/strings.json index ed8162eb3df..906b67dd481 100644 --- a/homeassistant/components/mastodon/strings.json +++ b/homeassistant/components/mastodon/strings.json @@ -4,8 +4,8 @@ "user": { "data": { "base_url": "[%key:common::config_flow::data::url%]", - "client_id": "Client Key", - "client_secret": "Client Secret", + "client_id": "Client key", + "client_secret": "Client secret", "access_token": "[%key:common::config_flow::data::access_token%]" }, "data_description": { From 706354173318437d8b25d83d3a41c6d968f81163 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Aug 2024 10:46:29 +0200 Subject: [PATCH 2270/2411] Support None schema in EntityPlatform.async_register_entity_service (#123064) --- homeassistant/helpers/config_validation.py | 4 ++-- homeassistant/helpers/entity_platform.py | 4 ++-- tests/helpers/test_entity_platform.py | 28 ++++++++++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 01960b6c0c3..ed3eca6e316 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1275,7 +1275,7 @@ BASE_ENTITY_SCHEMA = _make_entity_service_schema({}, vol.PREVENT_EXTRA) def make_entity_service_schema( - schema: dict, *, extra: int = vol.PREVENT_EXTRA + schema: dict | None, *, extra: int = vol.PREVENT_EXTRA ) -> vol.Schema: """Create an entity service schema.""" if not schema and extra == vol.PREVENT_EXTRA: @@ -1283,7 +1283,7 @@ def make_entity_service_schema( # the base schema and avoid compiling a new schema which is the case # for ~50% of services. return BASE_ENTITY_SCHEMA - return _make_entity_service_schema(schema, extra) + return _make_entity_service_schema(schema or {}, extra) SCRIPT_CONVERSATION_RESPONSE_SCHEMA = vol.Any(template, None) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 6774780f00f..f3d5f5b076a 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -985,7 +985,7 @@ class EntityPlatform: def async_register_entity_service( self, name: str, - schema: VolDictType | VolSchemaType, + schema: VolDictType | VolSchemaType | None, func: str | Callable[..., Any], required_features: Iterable[int] | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, @@ -997,7 +997,7 @@ class EntityPlatform: if self.hass.services.has_service(self.platform_name, name): return - if isinstance(schema, dict): + if schema is None or isinstance(schema, dict): schema = cv.make_entity_service_schema(schema) service_func: str | HassJob[..., Any] diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 75a41945a91..be8ba998481 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1760,6 +1760,34 @@ async def test_register_entity_service_limited_to_matching_platforms( } +async def test_register_entity_service_none_schema( + hass: HomeAssistant, +) -> None: + """Test registering a service with schema set to None.""" + entity_platform = MockEntityPlatform( + hass, domain="mock_integration", platform_name="mock_platform", platform=None + ) + entity1 = SlowEntity(name="entity_1") + entity2 = SlowEntity(name="entity_1") + await entity_platform.async_add_entities([entity1, entity2]) + + entities = [] + + @callback + def handle_service(entity, *_): + entities.append(entity) + + entity_platform.async_register_entity_service("hello", None, handle_service) + + await hass.services.async_call( + "mock_platform", "hello", {"entity_id": "all"}, blocking=True + ) + + assert len(entities) == 2 + assert entity1 in entities + assert entity2 in entities + + @pytest.mark.parametrize("update_before_add", [True, False]) async def test_invalid_entity_id( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, update_before_add: bool From e1a0a855d52478de2d1b8eb65a8e22bf2fe54afa Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Aug 2024 11:44:38 +0200 Subject: [PATCH 2271/2411] Support None schema in EntityComponent.async_register_entity_service (#123867) --- homeassistant/helpers/entity_component.py | 4 +-- tests/helpers/test_entity_component.py | 30 +++++++++++++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 0034eb1c6fc..c8bcda0eef2 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -258,13 +258,13 @@ class EntityComponent(Generic[_EntityT]): def async_register_entity_service( self, name: str, - schema: VolDictType | VolSchemaType, + schema: VolDictType | VolSchemaType | None, func: str | Callable[..., Any], required_features: list[int] | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, ) -> None: """Register an entity service.""" - if isinstance(schema, dict): + if schema is None or isinstance(schema, dict): schema = cv.make_entity_service_schema(schema) service_func: str | HassJob[..., Any] diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 3f34305b39d..0c09c9d75f7 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -495,7 +495,19 @@ async def test_extract_all_use_match_all( ) not in caplog.text -async def test_register_entity_service(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("schema", "service_data"), + [ + ({"some": str}, {"some": "data"}), + ({}, {}), + (None, {}), + ], +) +async def test_register_entity_service( + hass: HomeAssistant, + schema: dict | None, + service_data: dict, +) -> None: """Test registering an enttiy service and calling it.""" entity = MockEntity(entity_id=f"{DOMAIN}.entity") calls = [] @@ -510,9 +522,7 @@ async def test_register_entity_service(hass: HomeAssistant) -> None: await component.async_setup({}) await component.async_add_entities([entity]) - component.async_register_entity_service( - "hello", {"some": str}, "async_called_by_service" - ) + component.async_register_entity_service("hello", schema, "async_called_by_service") with pytest.raises(vol.Invalid): await hass.services.async_call( @@ -524,24 +534,24 @@ async def test_register_entity_service(hass: HomeAssistant) -> None: assert len(calls) == 0 await hass.services.async_call( - DOMAIN, "hello", {"entity_id": entity.entity_id, "some": "data"}, blocking=True + DOMAIN, "hello", {"entity_id": entity.entity_id} | service_data, blocking=True ) assert len(calls) == 1 - assert calls[0] == {"some": "data"} + assert calls[0] == service_data await hass.services.async_call( - DOMAIN, "hello", {"entity_id": ENTITY_MATCH_ALL, "some": "data"}, blocking=True + DOMAIN, "hello", {"entity_id": ENTITY_MATCH_ALL} | service_data, blocking=True ) assert len(calls) == 2 - assert calls[1] == {"some": "data"} + assert calls[1] == service_data await hass.services.async_call( - DOMAIN, "hello", {"entity_id": ENTITY_MATCH_NONE, "some": "data"}, blocking=True + DOMAIN, "hello", {"entity_id": ENTITY_MATCH_NONE} | service_data, blocking=True ) assert len(calls) == 2 await hass.services.async_call( - DOMAIN, "hello", {"area_id": ENTITY_MATCH_NONE, "some": "data"}, blocking=True + DOMAIN, "hello", {"area_id": ENTITY_MATCH_NONE} | service_data, blocking=True ) assert len(calls) == 2 From 82c705e188d9c0a618817319f13ab95e7fb7df19 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:09:46 +0200 Subject: [PATCH 2272/2411] Fix translation for integration not found repair issue (#123868) * correct setp id in strings * add issue_ignored string --- homeassistant/components/homeassistant/strings.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index e3e1464077a..69a3e26ad79 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -60,8 +60,11 @@ "integration_not_found": { "title": "Integration {domain} not found", "fix_flow": { + "abort": { + "issue_ignored": "Not existing integration {domain} ignored." + }, "step": { - "remove_entries": { + "init": { "title": "[%key:component::homeassistant::issues::integration_not_found::title%]", "description": "The integration `{domain}` could not be found. This happens when a (custom) integration was removed from Home Assistant, but there are still configurations for this `integration`. Please use the buttons below to either remove the previous configurations for `{domain}` or ignore this.", "menu_options": { From bd509469ab5a2e69ba418f342170b390e7019df9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:16:38 +0200 Subject: [PATCH 2273/2411] Improve type hints in reolink tests (#123883) --- tests/components/reolink/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 5334e171e5e..1c93114217c 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -44,7 +44,7 @@ pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") CHIME_MODEL = "Reolink Chime" -async def test_wait(*args, **key_args): +async def test_wait(*args, **key_args) -> None: """Ensure a mocked function takes a bit of time to be able to timeout in test.""" await asyncio.sleep(0) From cd382bcddaa345d88d22b2aaa4eabe0e33aa63e8 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Wed, 14 Aug 2024 20:31:18 +1000 Subject: [PATCH 2274/2411] Bump pydaikin to 2.13.4 (#123623) * bump pydaikin to 2.13.3 * bump pydaikin to 2.13.4 --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index c5cb6064d88..0d93c0e25ad 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/daikin", "iot_class": "local_polling", "loggers": ["pydaikin"], - "requirements": ["pydaikin==2.13.2"], + "requirements": ["pydaikin==2.13.4"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index cadd93c2b1f..58058e40e10 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1786,7 +1786,7 @@ pycsspeechtts==1.0.8 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.13.2 +pydaikin==2.13.4 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b5581f3181..56a16273f3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1436,7 +1436,7 @@ pycoolmasternet-async==0.2.2 pycsspeechtts==1.0.8 # homeassistant.components.daikin -pydaikin==2.13.2 +pydaikin==2.13.4 # homeassistant.components.deconz pydeconz==116 From 36f9b69923f7af396e9005459631ff309360fe12 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:33:26 +0200 Subject: [PATCH 2275/2411] Improve type hints in rfxtrx tests (#123885) --- tests/components/rfxtrx/conftest.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/components/rfxtrx/conftest.py b/tests/components/rfxtrx/conftest.py index 88450638d6c..be5c72e6483 100644 --- a/tests/components/rfxtrx/conftest.py +++ b/tests/components/rfxtrx/conftest.py @@ -2,7 +2,9 @@ from __future__ import annotations -from unittest.mock import Mock, patch +from collections.abc import Callable, Coroutine, Generator +from typing import Any +from unittest.mock import MagicMock, Mock, patch from freezegun import freeze_time import pytest @@ -67,7 +69,7 @@ async def setup_rfx_test_cfg( @pytest.fixture(autouse=True) -async def transport_mock(hass): +def transport_mock() -> Generator[Mock]: """Fixture that make sure all transports are fake.""" transport = Mock(spec=RFXtrxTransport) with ( @@ -78,14 +80,14 @@ async def transport_mock(hass): @pytest.fixture(autouse=True) -async def connect_mock(hass): +def connect_mock() -> Generator[MagicMock]: """Fixture that make sure connect class is mocked.""" with patch("RFXtrx.Connect") as connect: yield connect @pytest.fixture(autouse=True, name="rfxtrx") -def rfxtrx_fixture(hass, connect_mock): +def rfxtrx_fixture(hass: HomeAssistant, connect_mock: MagicMock) -> Mock: """Fixture that cleans up threads from integration.""" rfx = Mock(spec=Connect) @@ -114,19 +116,21 @@ def rfxtrx_fixture(hass, connect_mock): @pytest.fixture(name="rfxtrx_automatic") -async def rfxtrx_automatic_fixture(hass, rfxtrx): +async def rfxtrx_automatic_fixture(hass: HomeAssistant, rfxtrx: Mock) -> Mock: """Fixture that starts up with automatic additions.""" await setup_rfx_test_cfg(hass, automatic_add=True, devices={}) return rfxtrx @pytest.fixture -async def timestep(hass): +def timestep( + hass: HomeAssistant, +) -> Generator[Callable[[int], Coroutine[Any, Any, None]]]: """Step system time forward.""" with freeze_time(utcnow()) as frozen_time: - async def delay(seconds): + async def delay(seconds: int) -> None: """Trigger delay in system.""" frozen_time.tick(delta=seconds) async_fire_time_changed(hass) From a712eca70af6352f55436465026ed706bd72e444 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:47:38 +0200 Subject: [PATCH 2276/2411] Improve type hints in stream tests (#123894) --- tests/components/stream/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 0142d71a805..6aab3c06d13 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -60,7 +60,7 @@ class WorkerSync: @pytest.fixture -def stream_worker_sync(hass): +def stream_worker_sync() -> Generator[WorkerSync]: """Patch StreamOutput to allow test to synchronize worker stream end.""" sync = WorkerSync() with patch( From 165ec62405de083fde4d5eb193347e1b8d585f0d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:48:10 +0200 Subject: [PATCH 2277/2411] Improve type hints in ssdp tests (#123892) --- tests/components/ssdp/conftest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/components/ssdp/conftest.py b/tests/components/ssdp/conftest.py index 8b06163cd95..ac0ac7298a8 100644 --- a/tests/components/ssdp/conftest.py +++ b/tests/components/ssdp/conftest.py @@ -1,11 +1,14 @@ """Configuration for SSDP tests.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch from async_upnp_client.server import UpnpServer from async_upnp_client.ssdp_listener import SsdpListener import pytest +from homeassistant.core import HomeAssistant + @pytest.fixture(autouse=True) async def silent_ssdp_listener(): @@ -32,7 +35,7 @@ async def disabled_upnp_server(): @pytest.fixture -def mock_flow_init(hass): +def mock_flow_init(hass: HomeAssistant) -> Generator[AsyncMock]: """Mock hass.config_entries.flow.async_init.""" with patch.object( hass.config_entries.flow, "async_init", return_value=AsyncMock() From 24a8060f43576e715d928646bd0c7534c1c440ab Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:48:36 +0200 Subject: [PATCH 2278/2411] Improve type hints in sonos tests (#123891) --- tests/components/sonos/conftest.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index bbec7a2308c..4f14a2aa132 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -1,9 +1,10 @@ """Configuration for Sonos tests.""" import asyncio -from collections.abc import Callable, Generator +from collections.abc import Callable, Coroutine, Generator from copy import copy from ipaddress import ip_address +from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest @@ -120,7 +121,9 @@ async def async_autosetup_sonos(async_setup_sonos): @pytest.fixture -def async_setup_sonos(hass, config_entry, fire_zgs_event): +def async_setup_sonos( + hass: HomeAssistant, config_entry: MockConfigEntry, fire_zgs_event +) -> Callable[[], Coroutine[Any, Any, None]]: """Return a coroutine to set up a Sonos integration instance on demand.""" async def _wrapper(): @@ -136,7 +139,7 @@ def async_setup_sonos(hass, config_entry, fire_zgs_event): @pytest.fixture(name="config_entry") -def config_entry_fixture(): +def config_entry_fixture() -> MockConfigEntry: """Create a mock Sonos config entry.""" return MockConfigEntry(domain=DOMAIN, title="Sonos") @@ -650,7 +653,9 @@ def zgs_discovery_fixture(): @pytest.fixture(name="fire_zgs_event") -def zgs_event_fixture(hass: HomeAssistant, soco: SoCo, zgs_discovery: str): +def zgs_event_fixture( + hass: HomeAssistant, soco: SoCo, zgs_discovery: str +) -> Callable[[], Coroutine[Any, Any, None]]: """Create alarm_event fixture.""" variables = {"ZoneGroupState": zgs_discovery} From 57902fed22722e1873885a3a2187c430b27a6572 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:49:27 +0200 Subject: [PATCH 2279/2411] Improve type hints in smart_meter_texas tests (#123890) --- tests/components/smart_meter_texas/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/smart_meter_texas/conftest.py b/tests/components/smart_meter_texas/conftest.py index d06571fe05e..9c0301037a9 100644 --- a/tests/components/smart_meter_texas/conftest.py +++ b/tests/components/smart_meter_texas/conftest.py @@ -19,6 +19,7 @@ from homeassistant.components.homeassistant import ( ) from homeassistant.components.smart_meter_texas.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture @@ -91,7 +92,7 @@ def mock_connection( @pytest.fixture(name="config_entry") -def mock_config_entry(hass): +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: """Return a mock config entry.""" config_entry = MockConfigEntry( domain=DOMAIN, From f8879a51fede3229f71ba9f4583c464c4396205b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:49:53 +0200 Subject: [PATCH 2280/2411] Improve type hints in sma tests (#123889) --- tests/components/sma/conftest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/components/sma/conftest.py b/tests/components/sma/conftest.py index a98eda673e4..a54f478a31d 100644 --- a/tests/components/sma/conftest.py +++ b/tests/components/sma/conftest.py @@ -9,6 +9,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.sma.const import DOMAIN +from homeassistant.core import HomeAssistant from . import MOCK_DEVICE, MOCK_USER_INPUT @@ -16,7 +17,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_config_entry(): +def mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" return MockConfigEntry( domain=DOMAIN, @@ -28,7 +29,9 @@ def mock_config_entry(): @pytest.fixture -async def init_integration(hass, mock_config_entry): +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> MockConfigEntry: """Create a fake SMA Config Entry.""" mock_config_entry.add_to_hass(hass) From 7ff368fe0d655a28cc14c55faeb4dc361637ca15 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:50:08 +0200 Subject: [PATCH 2281/2411] Improve type hints in sharkiq tests (#123888) --- tests/components/sharkiq/test_vacuum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index e5154008f56..3748cfd6dc4 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -141,7 +141,7 @@ class MockShark(SharkIqVacuum): @pytest.fixture(autouse=True) @patch("sharkiq.ayla_api.AylaApi", MockAyla) -async def setup_integration(hass): +async def setup_integration(hass: HomeAssistant) -> None: """Build the mock integration.""" entry = MockConfigEntry( domain=DOMAIN, unique_id=TEST_USERNAME, data=CONFIG, entry_id=ENTRY_ID From 13b071fd72cee862fd665da22bdc37289d5c324c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:50:29 +0200 Subject: [PATCH 2282/2411] Improve type hints in risco tests (#123887) --- tests/components/risco/conftest.py | 20 ++++++++++++++------ tests/components/risco/test_sensor.py | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/components/risco/conftest.py b/tests/components/risco/conftest.py index ab3b64b245d..3961d85d694 100644 --- a/tests/components/risco/conftest.py +++ b/tests/components/risco/conftest.py @@ -1,7 +1,10 @@ """Fixtures for Risco tests.""" +from collections.abc import AsyncGenerator +from typing import Any from unittest.mock import MagicMock, PropertyMock, patch +from pyrisco.cloud.event import Event import pytest from homeassistant.components.risco.const import DOMAIN, TYPE_LOCAL @@ -13,6 +16,7 @@ from homeassistant.const import ( CONF_TYPE, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant from .util import TEST_SITE_NAME, TEST_SITE_UUID, system_mock, zone_mock @@ -116,19 +120,19 @@ def two_zone_local(): @pytest.fixture -def options(): +def options() -> dict[str, Any]: """Fixture for default (empty) options.""" return {} @pytest.fixture -def events(): +def events() -> list[Event]: """Fixture for default (empty) events.""" return [] @pytest.fixture -def cloud_config_entry(hass, options): +def cloud_config_entry(hass: HomeAssistant, options: dict[str, Any]) -> MockConfigEntry: """Fixture for a cloud config entry.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -151,7 +155,9 @@ def login_with_error(exception): @pytest.fixture -async def setup_risco_cloud(hass, cloud_config_entry, events): +async def setup_risco_cloud( + hass: HomeAssistant, cloud_config_entry: MockConfigEntry, events: list[Event] +) -> AsyncGenerator[MockConfigEntry]: """Set up a Risco integration for testing.""" with ( patch( @@ -181,7 +187,7 @@ async def setup_risco_cloud(hass, cloud_config_entry, events): @pytest.fixture -def local_config_entry(hass, options): +def local_config_entry(hass: HomeAssistant, options: dict[str, Any]) -> MockConfigEntry: """Fixture for a local config entry.""" config_entry = MockConfigEntry( domain=DOMAIN, data=TEST_LOCAL_CONFIG, options=options @@ -201,7 +207,9 @@ def connect_with_error(exception): @pytest.fixture -async def setup_risco_local(hass, local_config_entry): +async def setup_risco_local( + hass: HomeAssistant, local_config_entry: MockConfigEntry +) -> AsyncGenerator[MockConfigEntry]: """Set up a local Risco integration for testing.""" with ( patch( diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py index 4c8f7bb4180..2b1094554ae 100644 --- a/tests/components/risco/test_sensor.py +++ b/tests/components/risco/test_sensor.py @@ -160,7 +160,7 @@ def _check_state(hass, category, entity_id): @pytest.fixture -async def _set_utc_time_zone(hass): +async def _set_utc_time_zone(hass: HomeAssistant) -> None: await hass.config.async_set_time_zone("UTC") From 7fe2f175aae3dba7e66bc1af0a322548313756bd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:50:51 +0200 Subject: [PATCH 2283/2411] Improve type hints in ridwell tests (#123886) --- tests/components/ridwell/conftest.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/components/ridwell/conftest.py b/tests/components/ridwell/conftest.py index 32907ac8037..6ea9d91f8e9 100644 --- a/tests/components/ridwell/conftest.py +++ b/tests/components/ridwell/conftest.py @@ -1,6 +1,8 @@ """Define test fixtures for Ridwell.""" +from collections.abc import Generator from datetime import date +from typing import Any from unittest.mock import AsyncMock, Mock, patch from aioridwell.model import EventState, RidwellPickup, RidwellPickupEvent @@ -8,6 +10,7 @@ import pytest from homeassistant.components.ridwell.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -19,7 +22,7 @@ TEST_USER_ID = "12345" @pytest.fixture(name="account") -def account_fixture(): +def account_fixture() -> Mock: """Define a Ridwell account.""" return Mock( account_id=TEST_ACCOUNT_ID, @@ -44,7 +47,7 @@ def account_fixture(): @pytest.fixture(name="client") -def client_fixture(account): +def client_fixture(account: Mock) -> Mock: """Define an aioridwell client.""" return Mock( async_authenticate=AsyncMock(), @@ -55,7 +58,9 @@ def client_fixture(account): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config): +def config_entry_fixture( + hass: HomeAssistant, config: dict[str, Any] +) -> MockConfigEntry: """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, @@ -68,7 +73,7 @@ def config_entry_fixture(hass, config): @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture() -> dict[str, Any]: """Define a config entry data fixture.""" return { CONF_USERNAME: TEST_USERNAME, @@ -77,7 +82,7 @@ def config_fixture(hass): @pytest.fixture(name="mock_aioridwell") -async def mock_aioridwell_fixture(hass, client, config): +def mock_aioridwell_fixture(client: Mock, config: dict[str, Any]) -> Generator[None]: """Define a fixture to patch aioridwell.""" with ( patch( @@ -93,7 +98,9 @@ async def mock_aioridwell_fixture(hass, client, config): @pytest.fixture(name="setup_config_entry") -async def setup_config_entry_fixture(hass, config_entry, mock_aioridwell): +async def setup_config_entry_fixture( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_aioridwell: None +) -> None: """Define a fixture to set up ridwell.""" assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From 6626c63bb516d2165c8cd7e8855c74add93cae1d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:51:52 +0200 Subject: [PATCH 2284/2411] Improve type hints in recollect_waste tests (#123882) --- tests/components/recollect_waste/conftest.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/components/recollect_waste/conftest.py b/tests/components/recollect_waste/conftest.py index 360dd8aac98..8384da3f388 100644 --- a/tests/components/recollect_waste/conftest.py +++ b/tests/components/recollect_waste/conftest.py @@ -1,6 +1,7 @@ """Define test fixtures for ReCollect Waste.""" from datetime import date +from typing import Any from unittest.mock import AsyncMock, Mock, patch from aiorecollect.client import PickupEvent, PickupType @@ -11,6 +12,7 @@ from homeassistant.components.recollect_waste.const import ( CONF_SERVICE_ID, DOMAIN, ) +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -25,7 +27,9 @@ def client_fixture(pickup_events): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config): +def config_entry_fixture( + hass: HomeAssistant, config: dict[str, Any] +) -> MockConfigEntry: """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, unique_id=f"{TEST_PLACE_ID}, {TEST_SERVICE_ID}", data=config @@ -35,7 +39,7 @@ def config_entry_fixture(hass, config): @pytest.fixture(name="config") -def config_fixture(): +def config_fixture() -> dict[str, Any]: """Define a config entry data fixture.""" return { CONF_PLACE_ID: TEST_PLACE_ID, @@ -54,7 +58,7 @@ def pickup_events_fixture(): @pytest.fixture(name="mock_aiorecollect") -async def mock_aiorecollect_fixture(client): +def mock_aiorecollect_fixture(client): """Define a fixture to patch aiorecollect.""" with ( patch( @@ -70,7 +74,9 @@ async def mock_aiorecollect_fixture(client): @pytest.fixture(name="setup_config_entry") -async def setup_config_entry_fixture(hass, config_entry, mock_aiorecollect): +async def setup_config_entry_fixture( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_aiorecollect: None +) -> None: """Define a fixture to set up recollect_waste.""" assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From d50bac3b3e0ec758f76a030d9256287bbfdb2443 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:52:09 +0200 Subject: [PATCH 2285/2411] Improve type hints in rainmachine tests (#123881) --- tests/components/rainmachine/conftest.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/components/rainmachine/conftest.py b/tests/components/rainmachine/conftest.py index 717d74b421b..22ee807d187 100644 --- a/tests/components/rainmachine/conftest.py +++ b/tests/components/rainmachine/conftest.py @@ -1,5 +1,6 @@ """Define test fixtures for RainMachine.""" +from collections.abc import AsyncGenerator import json from typing import Any from unittest.mock import AsyncMock, patch @@ -8,19 +9,20 @@ import pytest from homeassistant.components.rainmachine import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture @pytest.fixture(name="client") -def client_fixture(controller, controller_mac): +def client_fixture(controller: AsyncMock, controller_mac: str) -> AsyncMock: """Define a regenmaschine client.""" return AsyncMock(load_local=AsyncMock(), controllers={controller_mac: controller}) @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture() -> dict[str, Any]: """Define a config entry data fixture.""" return { CONF_IP_ADDRESS: "192.168.1.100", @@ -31,7 +33,9 @@ def config_fixture(hass): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, controller_mac): +def config_entry_fixture( + hass: HomeAssistant, config: dict[str, Any], controller_mac: str +) -> MockConfigEntry: """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, @@ -78,7 +82,7 @@ def controller_fixture( @pytest.fixture(name="controller_mac") -def controller_mac_fixture(): +def controller_mac_fixture() -> str: """Define a controller MAC address.""" return "aa:bb:cc:dd:ee:ff" @@ -145,7 +149,9 @@ def data_zones_fixture(): @pytest.fixture(name="setup_rainmachine") -async def setup_rainmachine_fixture(hass, client, config): +async def setup_rainmachine_fixture( + hass: HomeAssistant, client: AsyncMock, config: dict[str, Any] +) -> AsyncGenerator[None]: """Define a fixture to set up RainMachine.""" with ( patch("homeassistant.components.rainmachine.Client", return_value=client), From ac223e64f9cfc48069c24c50b56f01415841b29d Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Wed, 14 Aug 2024 11:55:59 +0100 Subject: [PATCH 2286/2411] Migrate Mastodon unique id (#123877) * Migrate unique id * Fix unique id check * Switch to minor version and other fixes --- homeassistant/components/mastodon/__init__.py | 38 ++++++++++++- .../components/mastodon/config_flow.py | 12 ++-- tests/components/mastodon/conftest.py | 4 +- .../mastodon/snapshots/test_init.ambr | 33 +++++++++++ .../mastodon/snapshots/test_sensor.ambr | 6 +- tests/components/mastodon/test_config_flow.py | 2 +- tests/components/mastodon/test_init.py | 57 +++++++++++++++++++ 7 files changed, 139 insertions(+), 13 deletions(-) create mode 100644 tests/components/mastodon/snapshots/test_init.ambr diff --git a/homeassistant/components/mastodon/__init__.py b/homeassistant/components/mastodon/__init__.py index 3c305ca655b..77e66b6e45c 100644 --- a/homeassistant/components/mastodon/__init__.py +++ b/homeassistant/components/mastodon/__init__.py @@ -17,10 +17,11 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery +from homeassistant.util import slugify -from .const import CONF_BASE_URL, DOMAIN +from .const import CONF_BASE_URL, DOMAIN, LOGGER from .coordinator import MastodonCoordinator -from .utils import create_mastodon_client +from .utils import construct_mastodon_username, create_mastodon_client PLATFORMS: list[Platform] = [Platform.NOTIFY, Platform.SENSOR] @@ -80,6 +81,39 @@ async def async_unload_entry(hass: HomeAssistant, entry: MastodonConfigEntry) -> ) +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old config.""" + + if entry.version == 1 and entry.minor_version == 1: + # Version 1.1 had the unique_id as client_id, this isn't necessarily unique + LOGGER.debug("Migrating config entry from version %s", entry.version) + + try: + _, instance, account = await hass.async_add_executor_job( + setup_mastodon, + entry, + ) + except MastodonError as ex: + LOGGER.error("Migration failed with error %s", ex) + return False + + entry.minor_version = 2 + + hass.config_entries.async_update_entry( + entry, + unique_id=slugify(construct_mastodon_username(instance, account)), + ) + + LOGGER.info( + "Entry %s successfully migrated to version %s.%s", + entry.entry_id, + entry.version, + entry.minor_version, + ) + + return True + + def setup_mastodon(entry: ConfigEntry) -> tuple[Mastodon, dict, dict]: """Get mastodon details.""" client = create_mastodon_client( diff --git a/homeassistant/components/mastodon/config_flow.py b/homeassistant/components/mastodon/config_flow.py index 7d1c9396cbb..4e856275736 100644 --- a/homeassistant/components/mastodon/config_flow.py +++ b/homeassistant/components/mastodon/config_flow.py @@ -20,6 +20,7 @@ from homeassistant.helpers.selector import ( TextSelectorType, ) from homeassistant.helpers.typing import ConfigType +from homeassistant.util import slugify from .const import CONF_BASE_URL, DEFAULT_URL, DOMAIN, LOGGER from .utils import construct_mastodon_username, create_mastodon_client @@ -47,6 +48,7 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 + MINOR_VERSION = 2 config_entry: ConfigEntry def check_connection( @@ -105,10 +107,6 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" errors: dict[str, str] | None = None if user_input: - self._async_abort_entries_match( - {CONF_CLIENT_ID: user_input[CONF_CLIENT_ID]} - ) - instance, account, errors = await self.hass.async_add_executor_job( self.check_connection, user_input[CONF_BASE_URL], @@ -119,7 +117,8 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN): if not errors: name = construct_mastodon_username(instance, account) - await self.async_set_unique_id(user_input[CONF_CLIENT_ID]) + await self.async_set_unique_id(slugify(name)) + self._abort_if_unique_id_configured() return self.async_create_entry( title=name, data=user_input, @@ -148,7 +147,8 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN): ) if not errors: - await self.async_set_unique_id(client_id) + name = construct_mastodon_username(instance, account) + await self.async_set_unique_id(slugify(name)) self._abort_if_unique_id_configured() if not name: diff --git a/tests/components/mastodon/conftest.py b/tests/components/mastodon/conftest.py index 03c3e754c11..c64de44d496 100644 --- a/tests/components/mastodon/conftest.py +++ b/tests/components/mastodon/conftest.py @@ -53,5 +53,7 @@ def mock_config_entry() -> MockConfigEntry: CONF_ACCESS_TOKEN: "access_token", }, entry_id="01J35M4AH9HYRC2V0G6RNVNWJH", - unique_id="client_id", + unique_id="trwnh_mastodon_social", + version=1, + minor_version=2, ) diff --git a/tests/components/mastodon/snapshots/test_init.ambr b/tests/components/mastodon/snapshots/test_init.ambr new file mode 100644 index 00000000000..37fa765acea --- /dev/null +++ b/tests/components/mastodon/snapshots/test_init.ambr @@ -0,0 +1,33 @@ +# serializer version: 1 +# name: test_device_info + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'mastodon', + 'trwnh_mastodon_social', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Mastodon gGmbH', + 'model': '@trwnh@mastodon.social', + 'model_id': None, + 'name': 'Mastodon @trwnh@mastodon.social', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '4.0.0rc1', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/mastodon/snapshots/test_sensor.ambr b/tests/components/mastodon/snapshots/test_sensor.ambr index f94e34c00ab..c8df8cdab19 100644 --- a/tests/components/mastodon/snapshots/test_sensor.ambr +++ b/tests/components/mastodon/snapshots/test_sensor.ambr @@ -30,7 +30,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'followers', - 'unique_id': 'client_id_followers', + 'unique_id': 'trwnh_mastodon_social_followers', 'unit_of_measurement': 'accounts', }) # --- @@ -80,7 +80,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'following', - 'unique_id': 'client_id_following', + 'unique_id': 'trwnh_mastodon_social_following', 'unit_of_measurement': 'accounts', }) # --- @@ -130,7 +130,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'posts', - 'unique_id': 'client_id_posts', + 'unique_id': 'trwnh_mastodon_social_posts', 'unit_of_measurement': 'posts', }) # --- diff --git a/tests/components/mastodon/test_config_flow.py b/tests/components/mastodon/test_config_flow.py index 01cdc061d3e..073a6534d7d 100644 --- a/tests/components/mastodon/test_config_flow.py +++ b/tests/components/mastodon/test_config_flow.py @@ -44,7 +44,7 @@ async def test_full_flow( CONF_CLIENT_SECRET: "client_secret", CONF_ACCESS_TOKEN: "access_token", } - assert result["result"].unique_id == "client_id" + assert result["result"].unique_id == "trwnh_mastodon_social" @pytest.mark.parametrize( diff --git a/tests/components/mastodon/test_init.py b/tests/components/mastodon/test_init.py index 53796e39782..c3d0728fe08 100644 --- a/tests/components/mastodon/test_init.py +++ b/tests/components/mastodon/test_init.py @@ -3,15 +3,36 @@ from unittest.mock import AsyncMock from mastodon.Mastodon import MastodonError +from syrupy.assertion import SnapshotAssertion +from homeassistant.components.mastodon.config_flow import MastodonConfigFlow +from homeassistant.components.mastodon.const import CONF_BASE_URL, DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from . import setup_integration from tests.common import MockConfigEntry +async def test_device_info( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device registry integration.""" + await setup_integration(hass, mock_config_entry) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry.unique_id)} + ) + assert device_entry is not None + assert device_entry == snapshot + + async def test_initialization_failure( hass: HomeAssistant, mock_mastodon_client: AsyncMock, @@ -23,3 +44,39 @@ async def test_initialization_failure( await setup_integration(hass, mock_config_entry) assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_migrate( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, +) -> None: + """Test migration.""" + # Setup the config entry + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_BASE_URL: "https://mastodon.social", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + CONF_ACCESS_TOKEN: "access_token", + }, + title="@trwnh@mastodon.social", + unique_id="client_id", + version=1, + minor_version=1, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check migration was successful + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.data == { + CONF_BASE_URL: "https://mastodon.social", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + CONF_ACCESS_TOKEN: "access_token", + } + assert config_entry.version == MastodonConfigFlow.VERSION + assert config_entry.minor_version == MastodonConfigFlow.MINOR_VERSION + assert config_entry.unique_id == "trwnh_mastodon_social" From 8117532cc7be91fea7801dfdb678f24bb851fec4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:58:49 +0200 Subject: [PATCH 2287/2411] Improve type hints in rainforest_eagle tests (#123880) --- tests/components/rainforest_eagle/conftest.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/components/rainforest_eagle/conftest.py b/tests/components/rainforest_eagle/conftest.py index 1aff693e61f..c3790a12e86 100644 --- a/tests/components/rainforest_eagle/conftest.py +++ b/tests/components/rainforest_eagle/conftest.py @@ -1,6 +1,7 @@ """Conftest for rainforest_eagle.""" -from unittest.mock import AsyncMock, Mock, patch +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest @@ -13,6 +14,7 @@ from homeassistant.components.rainforest_eagle.const import ( TYPE_EAGLE_200, ) from homeassistant.const import CONF_HOST, CONF_TYPE +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from . import MOCK_200_RESPONSE_WITHOUT_PRICE, MOCK_CLOUD_ID @@ -21,7 +23,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def config_entry_200(hass): +def config_entry_200(hass: HomeAssistant) -> MockConfigEntry: """Return a config entry.""" entry = MockConfigEntry( domain="rainforest_eagle", @@ -38,7 +40,9 @@ def config_entry_200(hass): @pytest.fixture -async def setup_rainforest_200(hass, config_entry_200): +async def setup_rainforest_200( + hass: HomeAssistant, config_entry_200: MockConfigEntry +) -> AsyncGenerator[Mock]: """Set up rainforest.""" with patch( "aioeagle.ElectricMeter.create_instance", @@ -53,7 +57,7 @@ async def setup_rainforest_200(hass, config_entry_200): @pytest.fixture -async def setup_rainforest_100(hass): +async def setup_rainforest_100(hass: HomeAssistant) -> AsyncGenerator[MagicMock]: """Set up rainforest.""" MockConfigEntry( domain="rainforest_eagle", From f414f5d77a876ef2fa8a0ae0ae83614a675ccd53 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:00:07 +0200 Subject: [PATCH 2288/2411] Improve type hints in person tests (#123871) --- tests/components/person/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/person/conftest.py b/tests/components/person/conftest.py index ecec42b003d..a6dc95ccc9e 100644 --- a/tests/components/person/conftest.py +++ b/tests/components/person/conftest.py @@ -18,7 +18,7 @@ DEVICE_TRACKER_2 = "device_tracker.test_tracker_2" @pytest.fixture -def storage_collection(hass): +def storage_collection(hass: HomeAssistant) -> person.PersonStorageCollection: """Return an empty storage collection.""" id_manager = collection.IDManager() return person.PersonStorageCollection( From 1af6528f4f30910a99eb79f4937f6c37de82506d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:05:43 +0200 Subject: [PATCH 2289/2411] Improve type hints in prusalink tests (#123873) --- tests/components/prusalink/conftest.py | 37 +++++++++++++++----------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/tests/components/prusalink/conftest.py b/tests/components/prusalink/conftest.py index 104e4d47afa..9bcf45056cd 100644 --- a/tests/components/prusalink/conftest.py +++ b/tests/components/prusalink/conftest.py @@ -1,16 +1,19 @@ """Fixtures for PrusaLink.""" +from collections.abc import Generator +from typing import Any from unittest.mock import patch import pytest from homeassistant.components.prusalink import DOMAIN +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @pytest.fixture -def mock_config_entry(hass): +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: """Mock a PrusaLink config entry.""" entry = MockConfigEntry( domain=DOMAIN, @@ -23,7 +26,7 @@ def mock_config_entry(hass): @pytest.fixture -def mock_version_api(hass): +def mock_version_api() -> Generator[dict[str, str]]: """Mock PrusaLink version API.""" resp = { "api": "2.0.0", @@ -36,7 +39,7 @@ def mock_version_api(hass): @pytest.fixture -def mock_info_api(hass): +def mock_info_api() -> Generator[dict[str, Any]]: """Mock PrusaLink info API.""" resp = { "nozzle_diameter": 0.40, @@ -50,7 +53,7 @@ def mock_info_api(hass): @pytest.fixture -def mock_get_legacy_printer(hass): +def mock_get_legacy_printer() -> Generator[dict[str, Any]]: """Mock PrusaLink printer API.""" resp = {"telemetry": {"material": "PLA"}} with patch("pyprusalink.PrusaLink.get_legacy_printer", return_value=resp): @@ -58,7 +61,7 @@ def mock_get_legacy_printer(hass): @pytest.fixture -def mock_get_status_idle(hass): +def mock_get_status_idle() -> Generator[dict[str, Any]]: """Mock PrusaLink printer API.""" resp = { "storage": { @@ -86,7 +89,7 @@ def mock_get_status_idle(hass): @pytest.fixture -def mock_get_status_printing(hass): +def mock_get_status_printing() -> Generator[dict[str, Any]]: """Mock PrusaLink printer API.""" resp = { "job": { @@ -114,7 +117,7 @@ def mock_get_status_printing(hass): @pytest.fixture -def mock_job_api_idle(hass): +def mock_job_api_idle() -> Generator[dict[str, Any]]: """Mock PrusaLink job API having no job.""" resp = {} with patch("pyprusalink.PrusaLink.get_job", return_value=resp): @@ -122,7 +125,7 @@ def mock_job_api_idle(hass): @pytest.fixture -def mock_job_api_idle_mk3(hass): +def mock_job_api_idle_mk3() -> Generator[dict[str, Any]]: """Mock PrusaLink job API having a job with idle state (MK3).""" resp = { "id": 129, @@ -148,7 +151,7 @@ def mock_job_api_idle_mk3(hass): @pytest.fixture -def mock_job_api_printing(hass): +def mock_job_api_printing() -> Generator[dict[str, Any]]: """Mock PrusaLink printing.""" resp = { "id": 129, @@ -174,7 +177,9 @@ def mock_job_api_printing(hass): @pytest.fixture -def mock_job_api_paused(hass, mock_get_status_printing, mock_job_api_printing): +def mock_job_api_paused( + mock_get_status_printing: dict[str, Any], mock_job_api_printing: dict[str, Any] +) -> None: """Mock PrusaLink paused printing.""" mock_job_api_printing["state"] = "PAUSED" mock_get_status_printing["printer"]["state"] = "PAUSED" @@ -182,10 +187,10 @@ def mock_job_api_paused(hass, mock_get_status_printing, mock_job_api_printing): @pytest.fixture def mock_api( - mock_version_api, - mock_info_api, - mock_get_legacy_printer, - mock_get_status_idle, - mock_job_api_idle, -): + mock_version_api: dict[str, str], + mock_info_api: dict[str, Any], + mock_get_legacy_printer: dict[str, Any], + mock_get_status_idle: dict[str, Any], + mock_job_api_idle: dict[str, Any], +) -> None: """Mock PrusaLink API.""" From 5f1d7e55662b732e9c97c2f7921f097009d089df Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:05:52 +0200 Subject: [PATCH 2290/2411] Improve type hints in purpleair tests (#123874) --- tests/components/purpleair/conftest.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/components/purpleair/conftest.py b/tests/components/purpleair/conftest.py index 1305c98308d..3d6776dd12e 100644 --- a/tests/components/purpleair/conftest.py +++ b/tests/components/purpleair/conftest.py @@ -1,5 +1,7 @@ """Define fixtures for PurpleAir tests.""" +from collections.abc import Generator +from typing import Any from unittest.mock import AsyncMock, Mock, patch from aiopurpleair.endpoints.sensors import NearbySensorResult @@ -7,6 +9,7 @@ from aiopurpleair.models.sensors import GetSensorsResponse import pytest from homeassistant.components.purpleair import DOMAIN +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -16,7 +19,7 @@ TEST_SENSOR_INDEX2 = 567890 @pytest.fixture(name="api") -def api_fixture(get_sensors_response): +def api_fixture(get_sensors_response: GetSensorsResponse) -> Mock: """Define a fixture to return a mocked aiopurple API object.""" return Mock( async_check_api_key=AsyncMock(), @@ -34,7 +37,11 @@ def api_fixture(get_sensors_response): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config_entry_data, config_entry_options): +def config_entry_fixture( + hass: HomeAssistant, + config_entry_data: dict[str, Any], + config_entry_options: dict[str, Any], +) -> MockConfigEntry: """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, @@ -48,7 +55,7 @@ def config_entry_fixture(hass, config_entry_data, config_entry_options): @pytest.fixture(name="config_entry_data") -def config_entry_data_fixture(): +def config_entry_data_fixture() -> dict[str, Any]: """Define a config entry data fixture.""" return { "api_key": TEST_API_KEY, @@ -56,7 +63,7 @@ def config_entry_data_fixture(): @pytest.fixture(name="config_entry_options") -def config_entry_options_fixture(): +def config_entry_options_fixture() -> dict[str, Any]: """Define a config entry options fixture.""" return { "sensor_indices": [TEST_SENSOR_INDEX1], @@ -64,7 +71,7 @@ def config_entry_options_fixture(): @pytest.fixture(name="get_sensors_response", scope="package") -def get_sensors_response_fixture(): +def get_sensors_response_fixture() -> GetSensorsResponse: """Define a fixture to mock an aiopurpleair GetSensorsResponse object.""" return GetSensorsResponse.parse_raw( load_fixture("get_sensors_response.json", "purpleair") @@ -72,7 +79,7 @@ def get_sensors_response_fixture(): @pytest.fixture(name="mock_aiopurpleair") -async def mock_aiopurpleair_fixture(api): +def mock_aiopurpleair_fixture(api: Mock) -> Generator[Mock]: """Define a fixture to patch aiopurpleair.""" with ( patch("homeassistant.components.purpleair.config_flow.API", return_value=api), @@ -82,7 +89,9 @@ async def mock_aiopurpleair_fixture(api): @pytest.fixture(name="setup_config_entry") -async def setup_config_entry_fixture(hass, config_entry, mock_aiopurpleair): +async def setup_config_entry_fixture( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_aiopurpleair: Mock +) -> None: """Define a fixture to set up purpleair.""" assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From 903342b394a96ce44a2b05b9fee7af0747334bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Wed, 14 Aug 2024 13:06:52 +0200 Subject: [PATCH 2291/2411] Handle timeouts on Airzone DHCP config flow (#123869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit airzone: config_flow: dhcp: catch timeout exception Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/airzone/config_flow.py b/homeassistant/components/airzone/config_flow.py index 24ee37bbcb4..406fd72a6db 100644 --- a/homeassistant/components/airzone/config_flow.py +++ b/homeassistant/components/airzone/config_flow.py @@ -114,7 +114,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN): ) try: await airzone.get_version() - except AirzoneError as err: + except (AirzoneError, TimeoutError) as err: raise AbortFlow("cannot_connect") from err return await self.async_step_discovered_connection() From d4082aee5ad2a41ea277c554a2c054f51a5c4c13 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:08:54 +0200 Subject: [PATCH 2292/2411] Improve type hints in owntracks tests (#123866) --- .../owntracks/test_device_tracker.py | 147 +++++++++++------- 1 file changed, 94 insertions(+), 53 deletions(-) diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 0648a94c70b..b4b5c00880c 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1,6 +1,7 @@ """The tests for the Owntracks device tracker.""" import base64 +from collections.abc import Callable import json import pickle from unittest.mock import patch @@ -18,6 +19,8 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, async_fire_mqtt_message from tests.typing import ClientSessionGenerator, MqttMockHAClient +type OwnTracksContextFactory = Callable[[], owntracks.OwnTracksContext] + USER = "greg" DEVICE = "phone" @@ -314,7 +317,7 @@ async def setup_owntracks(hass, config, ctx_cls=owntracks.OwnTracksContext): @pytest.fixture -def context(hass, setup_comp): +def context(hass: HomeAssistant, setup_comp: None) -> OwnTracksContextFactory: """Set up the mocked context.""" orig_context = owntracks.OwnTracksContext context = None @@ -407,14 +410,16 @@ def assert_mobile_tracker_accuracy(hass, accuracy, beacon=IBEACON_DEVICE): assert state.attributes.get("gps_accuracy") == accuracy -async def test_location_invalid_devid(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_location_invalid_devid(hass: HomeAssistant) -> None: """Test the update of a location.""" await send_message(hass, "owntracks/paulus/nexus-5x", LOCATION_MESSAGE) state = hass.states.get("device_tracker.paulus_nexus_5x") assert state.state == "outer" -async def test_location_update(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_location_update(hass: HomeAssistant) -> None: """Test the update of a location.""" await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE) @@ -424,7 +429,8 @@ async def test_location_update(hass: HomeAssistant, context) -> None: assert_location_state(hass, "outer") -async def test_location_update_no_t_key(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_location_update_no_t_key(hass: HomeAssistant) -> None: """Test the update of a location when message does not contain 't'.""" message = LOCATION_MESSAGE.copy() message.pop("t") @@ -436,7 +442,8 @@ async def test_location_update_no_t_key(hass: HomeAssistant, context) -> None: assert_location_state(hass, "outer") -async def test_location_inaccurate_gps(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_location_inaccurate_gps(hass: HomeAssistant) -> None: """Test the location for inaccurate GPS information.""" await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE) await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE_INACCURATE) @@ -446,7 +453,8 @@ async def test_location_inaccurate_gps(hass: HomeAssistant, context) -> None: assert_location_longitude(hass, LOCATION_MESSAGE["lon"]) -async def test_location_zero_accuracy_gps(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_location_zero_accuracy_gps(hass: HomeAssistant) -> None: """Ignore the location for zero accuracy GPS information.""" await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE) await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE_ZERO_ACCURACY) @@ -458,7 +466,9 @@ async def test_location_zero_accuracy_gps(hass: HomeAssistant, context) -> None: # ------------------------------------------------------------------------ # GPS based event entry / exit testing -async def test_event_gps_entry_exit(hass: HomeAssistant, context) -> None: +async def test_event_gps_entry_exit( + hass: HomeAssistant, context: OwnTracksContextFactory +) -> None: """Test the entry event.""" # Entering the owntracks circular region named "inner" await send_message(hass, EVENT_TOPIC, REGION_GPS_ENTER_MESSAGE) @@ -496,7 +506,9 @@ async def test_event_gps_entry_exit(hass: HomeAssistant, context) -> None: assert_location_accuracy(hass, LOCATION_MESSAGE["acc"]) -async def test_event_gps_with_spaces(hass: HomeAssistant, context) -> None: +async def test_event_gps_with_spaces( + hass: HomeAssistant, context: OwnTracksContextFactory +) -> None: """Test the entry event.""" message = build_message({"desc": "inner 2"}, REGION_GPS_ENTER_MESSAGE) await send_message(hass, EVENT_TOPIC, message) @@ -509,7 +521,8 @@ async def test_event_gps_with_spaces(hass: HomeAssistant, context) -> None: assert not context().regions_entered[USER] -async def test_event_gps_entry_inaccurate(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_event_gps_entry_inaccurate(hass: HomeAssistant) -> None: """Test the event for inaccurate entry.""" # Set location to the outer zone. await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE) @@ -522,7 +535,9 @@ async def test_event_gps_entry_inaccurate(hass: HomeAssistant, context) -> None: assert_location_state(hass, "inner") -async def test_event_gps_entry_exit_inaccurate(hass: HomeAssistant, context) -> None: +async def test_event_gps_entry_exit_inaccurate( + hass: HomeAssistant, context: OwnTracksContextFactory +) -> None: """Test the event for inaccurate exit.""" await send_message(hass, EVENT_TOPIC, REGION_GPS_ENTER_MESSAGE) @@ -542,7 +557,9 @@ async def test_event_gps_entry_exit_inaccurate(hass: HomeAssistant, context) -> assert not context().regions_entered[USER] -async def test_event_gps_entry_exit_zero_accuracy(hass: HomeAssistant, context) -> None: +async def test_event_gps_entry_exit_zero_accuracy( + hass: HomeAssistant, context: OwnTracksContextFactory +) -> None: """Test entry/exit events with accuracy zero.""" await send_message(hass, EVENT_TOPIC, REGION_GPS_ENTER_MESSAGE_ZERO) @@ -562,9 +579,8 @@ async def test_event_gps_entry_exit_zero_accuracy(hass: HomeAssistant, context) assert not context().regions_entered[USER] -async def test_event_gps_exit_outside_zone_sets_away( - hass: HomeAssistant, context -) -> None: +@pytest.mark.usefixtures("context") +async def test_event_gps_exit_outside_zone_sets_away(hass: HomeAssistant) -> None: """Test the event for exit zone.""" await send_message(hass, EVENT_TOPIC, REGION_GPS_ENTER_MESSAGE) assert_location_state(hass, "inner") @@ -577,7 +593,8 @@ async def test_event_gps_exit_outside_zone_sets_away( assert_location_state(hass, STATE_NOT_HOME) -async def test_event_gps_entry_exit_right_order(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_event_gps_entry_exit_right_order(hass: HomeAssistant) -> None: """Test the event for ordering.""" # Enter inner zone # Set location to the outer zone. @@ -602,7 +619,8 @@ async def test_event_gps_entry_exit_right_order(hass: HomeAssistant, context) -> assert_location_state(hass, "outer") -async def test_event_gps_entry_exit_wrong_order(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_event_gps_entry_exit_wrong_order(hass: HomeAssistant) -> None: """Test the event for wrong order.""" # Enter inner zone await send_message(hass, EVENT_TOPIC, REGION_GPS_ENTER_MESSAGE) @@ -625,7 +643,8 @@ async def test_event_gps_entry_exit_wrong_order(hass: HomeAssistant, context) -> assert_location_state(hass, "outer") -async def test_event_gps_entry_unknown_zone(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_event_gps_entry_unknown_zone(hass: HomeAssistant) -> None: """Test the event for unknown zone.""" # Just treat as location update message = build_message({"desc": "unknown"}, REGION_GPS_ENTER_MESSAGE) @@ -634,7 +653,8 @@ async def test_event_gps_entry_unknown_zone(hass: HomeAssistant, context) -> Non assert_location_state(hass, "inner") -async def test_event_gps_exit_unknown_zone(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_event_gps_exit_unknown_zone(hass: HomeAssistant) -> None: """Test the event for unknown zone.""" # Just treat as location update message = build_message({"desc": "unknown"}, REGION_GPS_LEAVE_MESSAGE) @@ -643,7 +663,8 @@ async def test_event_gps_exit_unknown_zone(hass: HomeAssistant, context) -> None assert_location_state(hass, "outer") -async def test_event_entry_zone_loading_dash(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_event_entry_zone_loading_dash(hass: HomeAssistant) -> None: """Test the event for zone landing.""" # Make sure the leading - is ignored # Owntracks uses this to switch on hold @@ -652,7 +673,9 @@ async def test_event_entry_zone_loading_dash(hass: HomeAssistant, context) -> No assert_location_state(hass, "inner") -async def test_events_only_on(hass: HomeAssistant, context) -> None: +async def test_events_only_on( + hass: HomeAssistant, context: OwnTracksContextFactory +) -> None: """Test events_only config suppresses location updates.""" # Sending a location message that is not home await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE_NOT_HOME) @@ -673,7 +696,9 @@ async def test_events_only_on(hass: HomeAssistant, context) -> None: assert_location_state(hass, STATE_NOT_HOME) -async def test_events_only_off(hass: HomeAssistant, context) -> None: +async def test_events_only_off( + hass: HomeAssistant, context: OwnTracksContextFactory +) -> None: """Test when events_only is False.""" # Sending a location message that is not home await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE_NOT_HOME) @@ -694,7 +719,8 @@ async def test_events_only_off(hass: HomeAssistant, context) -> None: assert_location_state(hass, "outer") -async def test_event_source_type_entry_exit(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_event_source_type_entry_exit(hass: HomeAssistant) -> None: """Test the entry and exit events of source type.""" # Entering the owntracks circular region named "inner" await send_message(hass, EVENT_TOPIC, REGION_GPS_ENTER_MESSAGE) @@ -724,7 +750,9 @@ async def test_event_source_type_entry_exit(hass: HomeAssistant, context) -> Non # Region Beacon based event entry / exit testing -async def test_event_region_entry_exit(hass: HomeAssistant, context) -> None: +async def test_event_region_entry_exit( + hass: HomeAssistant, context: OwnTracksContextFactory +) -> None: """Test the entry event.""" # Seeing a beacon named "inner" await send_message(hass, EVENT_TOPIC, REGION_BEACON_ENTER_MESSAGE) @@ -763,7 +791,9 @@ async def test_event_region_entry_exit(hass: HomeAssistant, context) -> None: assert_location_accuracy(hass, LOCATION_MESSAGE["acc"]) -async def test_event_region_with_spaces(hass: HomeAssistant, context) -> None: +async def test_event_region_with_spaces( + hass: HomeAssistant, context: OwnTracksContextFactory +) -> None: """Test the entry event.""" message = build_message({"desc": "inner 2"}, REGION_BEACON_ENTER_MESSAGE) await send_message(hass, EVENT_TOPIC, message) @@ -776,9 +806,8 @@ async def test_event_region_with_spaces(hass: HomeAssistant, context) -> None: assert not context().regions_entered[USER] -async def test_event_region_entry_exit_right_order( - hass: HomeAssistant, context -) -> None: +@pytest.mark.usefixtures("context") +async def test_event_region_entry_exit_right_order(hass: HomeAssistant) -> None: """Test the event for ordering.""" # Enter inner zone # Set location to the outer zone. @@ -809,9 +838,8 @@ async def test_event_region_entry_exit_right_order( assert_location_state(hass, "inner") -async def test_event_region_entry_exit_wrong_order( - hass: HomeAssistant, context -) -> None: +@pytest.mark.usefixtures("context") +async def test_event_region_entry_exit_wrong_order(hass: HomeAssistant) -> None: """Test the event for wrong order.""" # Enter inner zone await send_message(hass, EVENT_TOPIC, REGION_BEACON_ENTER_MESSAGE) @@ -838,9 +866,8 @@ async def test_event_region_entry_exit_wrong_order( assert_location_state(hass, "inner_2") -async def test_event_beacon_unknown_zone_no_location( - hass: HomeAssistant, context -) -> None: +@pytest.mark.usefixtures("context") +async def test_event_beacon_unknown_zone_no_location(hass: HomeAssistant) -> None: """Test the event for unknown zone.""" # A beacon which does not match a HA zone is the # definition of a mobile beacon. In this case, "unknown" @@ -865,7 +892,8 @@ async def test_event_beacon_unknown_zone_no_location( assert_mobile_tracker_state(hass, "unknown", "unknown") -async def test_event_beacon_unknown_zone(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_event_beacon_unknown_zone(hass: HomeAssistant) -> None: """Test the event for unknown zone.""" # A beacon which does not match a HA zone is the # definition of a mobile beacon. In this case, "unknown" @@ -885,9 +913,8 @@ async def test_event_beacon_unknown_zone(hass: HomeAssistant, context) -> None: assert_mobile_tracker_state(hass, "outer", "unknown") -async def test_event_beacon_entry_zone_loading_dash( - hass: HomeAssistant, context -) -> None: +@pytest.mark.usefixtures("context") +async def test_event_beacon_entry_zone_loading_dash(hass: HomeAssistant) -> None: """Test the event for beacon zone landing.""" # Make sure the leading - is ignored # Owntracks uses this to switch on hold @@ -899,7 +926,8 @@ async def test_event_beacon_entry_zone_loading_dash( # ------------------------------------------------------------------------ # Mobile Beacon based event entry / exit testing -async def test_mobile_enter_move_beacon(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_mobile_enter_move_beacon(hass: HomeAssistant) -> None: """Test the movement of a beacon.""" # I am in the outer zone. await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE) @@ -923,7 +951,8 @@ async def test_mobile_enter_move_beacon(hass: HomeAssistant, context) -> None: assert_mobile_tracker_latitude(hass, not_home_lat) -async def test_mobile_enter_exit_region_beacon(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_mobile_enter_exit_region_beacon(hass: HomeAssistant) -> None: """Test the enter and the exit of a mobile beacon.""" # I am in the outer zone. await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE) @@ -946,7 +975,8 @@ async def test_mobile_enter_exit_region_beacon(hass: HomeAssistant, context) -> assert_mobile_tracker_state(hass, "outer") -async def test_mobile_exit_move_beacon(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_mobile_exit_move_beacon(hass: HomeAssistant) -> None: """Test the exit move of a beacon.""" # I am in the outer zone. await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE) @@ -968,7 +998,9 @@ async def test_mobile_exit_move_beacon(hass: HomeAssistant, context) -> None: assert_mobile_tracker_state(hass, "outer") -async def test_mobile_multiple_async_enter_exit(hass: HomeAssistant, context) -> None: +async def test_mobile_multiple_async_enter_exit( + hass: HomeAssistant, context: OwnTracksContextFactory +) -> None: """Test the multiple entering.""" # Test race condition for _ in range(20): @@ -988,7 +1020,9 @@ async def test_mobile_multiple_async_enter_exit(hass: HomeAssistant, context) -> assert len(context().mobile_beacons_active["greg_phone"]) == 0 -async def test_mobile_multiple_enter_exit(hass: HomeAssistant, context) -> None: +async def test_mobile_multiple_enter_exit( + hass: HomeAssistant, context: OwnTracksContextFactory +) -> None: """Test the multiple entering.""" await send_message(hass, EVENT_TOPIC, MOBILE_BEACON_ENTER_EVENT_MESSAGE) await send_message(hass, EVENT_TOPIC, MOBILE_BEACON_ENTER_EVENT_MESSAGE) @@ -997,7 +1031,8 @@ async def test_mobile_multiple_enter_exit(hass: HomeAssistant, context) -> None: assert len(context().mobile_beacons_active["greg_phone"]) == 0 -async def test_complex_movement(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_complex_movement(hass: HomeAssistant) -> None: """Test a complex sequence representative of real-world use.""" # I am in the outer zone. await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE) @@ -1119,9 +1154,8 @@ async def test_complex_movement(hass: HomeAssistant, context) -> None: assert_mobile_tracker_state(hass, "outer") -async def test_complex_movement_sticky_keys_beacon( - hass: HomeAssistant, context -) -> None: +@pytest.mark.usefixtures("context") +async def test_complex_movement_sticky_keys_beacon(hass: HomeAssistant) -> None: """Test a complex sequence which was previously broken.""" # I am not_home await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE) @@ -1233,7 +1267,8 @@ async def test_complex_movement_sticky_keys_beacon( assert_mobile_tracker_latitude(hass, INNER_ZONE["latitude"]) -async def test_waypoint_import_simple(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_waypoint_import_simple(hass: HomeAssistant) -> None: """Test a simple import of list of waypoints.""" waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy() await send_message(hass, WAYPOINTS_TOPIC, waypoints_message) @@ -1244,7 +1279,8 @@ async def test_waypoint_import_simple(hass: HomeAssistant, context) -> None: assert wayp is not None -async def test_waypoint_import_block(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_waypoint_import_block(hass: HomeAssistant) -> None: """Test import of list of waypoints for blocked user.""" waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy() await send_message(hass, WAYPOINTS_TOPIC_BLOCKED, waypoints_message) @@ -1275,7 +1311,8 @@ async def test_waypoint_import_no_whitelist(hass: HomeAssistant, setup_comp) -> assert wayp is not None -async def test_waypoint_import_bad_json(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_waypoint_import_bad_json(hass: HomeAssistant) -> None: """Test importing a bad JSON payload.""" waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy() await send_message(hass, WAYPOINTS_TOPIC, waypoints_message, True) @@ -1286,7 +1323,8 @@ async def test_waypoint_import_bad_json(hass: HomeAssistant, context) -> None: assert wayp is None -async def test_waypoint_import_existing(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_waypoint_import_existing(hass: HomeAssistant) -> None: """Test importing a zone that exists.""" waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy() await send_message(hass, WAYPOINTS_TOPIC, waypoints_message) @@ -1299,7 +1337,8 @@ async def test_waypoint_import_existing(hass: HomeAssistant, context) -> None: assert wayp == new_wayp -async def test_single_waypoint_import(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_single_waypoint_import(hass: HomeAssistant) -> None: """Test single waypoint message.""" waypoint_message = WAYPOINT_MESSAGE.copy() await send_message(hass, WAYPOINT_TOPIC, waypoint_message) @@ -1307,7 +1346,8 @@ async def test_single_waypoint_import(hass: HomeAssistant, context) -> None: assert wayp is not None -async def test_not_implemented_message(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_not_implemented_message(hass: HomeAssistant) -> None: """Handle not implemented message type.""" patch_handler = patch( "homeassistant.components.owntracks.messages.async_handle_not_impl_msg", @@ -1318,7 +1358,8 @@ async def test_not_implemented_message(hass: HomeAssistant, context) -> None: patch_handler.stop() -async def test_unsupported_message(hass: HomeAssistant, context) -> None: +@pytest.mark.usefixtures("context") +async def test_unsupported_message(hass: HomeAssistant) -> None: """Handle not implemented message type.""" patch_handler = patch( "homeassistant.components.owntracks.messages.async_handle_unsupported_msg", From 1ddc7232745c8c66c95fc0483c679de78b056ef2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:12:24 +0200 Subject: [PATCH 2293/2411] Improve type hints in powerwall tests (#123872) --- tests/components/powerwall/test_switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/powerwall/test_switch.py b/tests/components/powerwall/test_switch.py index b01f60210a6..b4ff0ca724e 100644 --- a/tests/components/powerwall/test_switch.py +++ b/tests/components/powerwall/test_switch.py @@ -1,6 +1,6 @@ """Test for Powerwall off-grid switch.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from tesla_powerwall import GridStatus, PowerwallError @@ -24,7 +24,7 @@ ENTITY_ID = "switch.mysite_off_grid_operation" @pytest.fixture(name="mock_powerwall") -async def mock_powerwall_fixture(hass): +async def mock_powerwall_fixture(hass: HomeAssistant) -> MagicMock: """Set up base powerwall fixture.""" mock_powerwall = await _mock_powerwall_with_fixtures(hass) From dc2886d9b12078867cd3d2771869c3e29539892a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 14 Aug 2024 13:27:21 +0200 Subject: [PATCH 2294/2411] Use coordinator setup method in yale_smart_alarm (#123819) --- .../components/yale_smart_alarm/__init__.py | 4 --- .../yale_smart_alarm/coordinator.py | 25 ++++++++++--------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/__init__.py b/homeassistant/components/yale_smart_alarm/__init__.py index 1ef68d98a13..3c853afb6fd 100644 --- a/homeassistant/components/yale_smart_alarm/__init__.py +++ b/homeassistant/components/yale_smart_alarm/__init__.py @@ -6,7 +6,6 @@ from homeassistant.components.lock import CONF_DEFAULT_CODE, DOMAIN as LOCK_DOMA from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CODE from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import entity_registry as er from .const import LOGGER, PLATFORMS @@ -19,9 +18,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool """Set up Yale from a config entry.""" coordinator = YaleDataUpdateCoordinator(hass, entry) - if not await hass.async_add_executor_job(coordinator.get_updates): - raise ConfigEntryAuthFailed - await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/yale_smart_alarm/coordinator.py b/homeassistant/components/yale_smart_alarm/coordinator.py index 5307e166e17..328558d0aba 100644 --- a/homeassistant/components/yale_smart_alarm/coordinator.py +++ b/homeassistant/components/yale_smart_alarm/coordinator.py @@ -20,10 +20,11 @@ from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, YALE_BASE_ERRORS class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """A Yale Data Update Coordinator.""" + yale: YaleSmartAlarmClient + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the Yale hub.""" self.entry = entry - self.yale: YaleSmartAlarmClient | None = None super().__init__( hass, LOGGER, @@ -32,6 +33,17 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): always_update=False, ) + async def _async_setup(self) -> None: + """Set up connection to Yale.""" + try: + self.yale = YaleSmartAlarmClient( + self.entry.data[CONF_USERNAME], self.entry.data[CONF_PASSWORD] + ) + except AuthenticationError as error: + raise ConfigEntryAuthFailed from error + except YALE_BASE_ERRORS as error: + raise UpdateFailed from error + async def _async_update_data(self) -> dict[str, Any]: """Fetch data from Yale.""" @@ -132,17 +144,6 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): def get_updates(self) -> dict[str, Any]: """Fetch data from Yale.""" - - if self.yale is None: - try: - self.yale = YaleSmartAlarmClient( - self.entry.data[CONF_USERNAME], self.entry.data[CONF_PASSWORD] - ) - except AuthenticationError as error: - raise ConfigEntryAuthFailed from error - except YALE_BASE_ERRORS as error: - raise UpdateFailed from error - try: arm_status = self.yale.get_armed_status() data = self.yale.get_all() From 3b1b600606a379facc372ece209d4a279d7023de Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:47:49 +0200 Subject: [PATCH 2295/2411] Bump aioautomower to 2024.8.0 (#123826) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/husqvarna_automower/fixtures/mower.json | 1 + .../husqvarna_automower/snapshots/test_diagnostics.ambr | 3 +++ 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index bb03806e417..7326408e403 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.7.3"] + "requirements": ["aioautomower==2024.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 58058e40e10..782d41171f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -198,7 +198,7 @@ aioaseko==0.2.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.7.3 +aioautomower==2024.8.0 # homeassistant.components.azure_devops aioazuredevops==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56a16273f3e..bf39d8c33e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -186,7 +186,7 @@ aioaseko==0.2.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.7.3 +aioautomower==2024.8.0 # homeassistant.components.azure_devops aioazuredevops==2.1.1 diff --git a/tests/components/husqvarna_automower/fixtures/mower.json b/tests/components/husqvarna_automower/fixtures/mower.json index a5cae68f47c..aa8ea2cbef4 100644 --- a/tests/components/husqvarna_automower/fixtures/mower.json +++ b/tests/components/husqvarna_automower/fixtures/mower.json @@ -13,6 +13,7 @@ "batteryPercent": 100 }, "capabilities": { + "canConfirmError": true, "headlights": true, "workAreas": true, "position": true, diff --git a/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr b/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr index 212be85ce51..3838f2eb960 100644 --- a/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr @@ -9,6 +9,7 @@ dict({ 'end': '2024-03-02T00:00:00', 'rrule': 'FREQ=WEEKLY;BYDAY=MO,WE,FR', + 'schedule_no': 1, 'start': '2024-03-01T19:00:00', 'uid': '1140_300_MO,WE,FR', 'work_area_id': None, @@ -17,6 +18,7 @@ dict({ 'end': '2024-03-02T08:00:00', 'rrule': 'FREQ=WEEKLY;BYDAY=TU,TH,SA', + 'schedule_no': 2, 'start': '2024-03-02T00:00:00', 'uid': '0_480_TU,TH,SA', 'work_area_id': None, @@ -53,6 +55,7 @@ ]), }), 'capabilities': dict({ + 'can_confirm_error': True, 'headlights': True, 'position': True, 'stay_out_zones': True, From b698dd8f32ce63be2e3a12f331d9986e11705a60 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 14 Aug 2024 13:49:10 +0200 Subject: [PATCH 2296/2411] Bump pyflic to 2.0.4 (#123895) --- homeassistant/components/flic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/licenses.py | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flic/manifest.json b/homeassistant/components/flic/manifest.json index 8fc146ded6a..0442e4a7b7b 100644 --- a/homeassistant/components/flic/manifest.json +++ b/homeassistant/components/flic/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/flic", "iot_class": "local_push", "loggers": ["pyflic"], - "requirements": ["pyflic==2.0.3"] + "requirements": ["pyflic==2.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 782d41171f7..ba19291607d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1867,7 +1867,7 @@ pyfido==2.1.2 pyfireservicerota==0.0.43 # homeassistant.components.flic -pyflic==2.0.3 +pyflic==2.0.4 # homeassistant.components.futurenow pyfnip==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf39d8c33e5..c6785ede042 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1493,7 +1493,7 @@ pyfido==2.1.2 pyfireservicerota==0.0.43 # homeassistant.components.flic -pyflic==2.0.3 +pyflic==2.0.4 # homeassistant.components.forked_daapd pyforked-daapd==0.1.14 diff --git a/script/licenses.py b/script/licenses.py index 9bcc7b54540..0c2870aebd8 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -179,7 +179,6 @@ TODO = { "aiocache": AwesomeVersion( "0.12.2" ), # https://github.com/aio-libs/aiocache/blob/master/LICENSE all rights reserved? - "pyflic": AwesomeVersion("2.0.3"), # No OSI approved license CC0-1.0 Universal) } From 80f5683cd66182dbae87f956d70a3dc59b9960d2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Aug 2024 13:59:06 +0200 Subject: [PATCH 2297/2411] Raise on database error in recorder.migration._add_constraint (#123646) * Raise on database error in recorder.migration._add_constraint * Fix test --- homeassistant/components/recorder/migration.py | 1 + tests/components/recorder/test_migrate.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 55856dcf449..e96cef14cf4 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -727,6 +727,7 @@ def _add_constraint( connection.execute(add_constraint) except (InternalError, OperationalError): _LOGGER.exception("Could not update foreign options in %s table", table) + raise def _delete_foreign_key_violations( diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 988eade29b6..2a33f08050c 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -962,9 +962,10 @@ def test_restore_foreign_key_constraints_with_error( engine = Mock() session_maker = Mock(return_value=session) - migration._restore_foreign_key_constraints( - session_maker, engine, constraints_to_restore - ) + with pytest.raises(InternalError): + migration._restore_foreign_key_constraints( + session_maker, engine, constraints_to_restore + ) assert "Could not update foreign options in events table" in caplog.text From ea7e88d000b8550ac01315dab78cd71ef0091227 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Aug 2024 14:04:29 +0200 Subject: [PATCH 2298/2411] Pass None instead of empty dict when registering entity services (#123878) --- homeassistant/components/agent_dvr/camera.py | 2 +- homeassistant/components/alert/__init__.py | 6 +++--- .../components/androidtv/media_player.py | 2 +- homeassistant/components/automation/__init__.py | 4 ++-- homeassistant/components/blink/camera.py | 4 ++-- homeassistant/components/bond/light.py | 2 +- homeassistant/components/button/__init__.py | 2 +- homeassistant/components/camera/__init__.py | 8 ++++---- .../components/channels/media_player.py | 4 ++-- homeassistant/components/climate/__init__.py | 6 +++--- homeassistant/components/counter/__init__.py | 6 +++--- homeassistant/components/cover/__init__.py | 16 ++++++++-------- .../components/denonavr/media_player.py | 2 +- homeassistant/components/ecovacs/vacuum.py | 2 +- homeassistant/components/elgato/light.py | 2 +- homeassistant/components/elkm1/sensor.py | 4 ++-- homeassistant/components/ezviz/camera.py | 2 +- homeassistant/components/fan/__init__.py | 4 ++-- homeassistant/components/flo/switch.py | 6 +++--- homeassistant/components/harmony/remote.py | 2 +- homeassistant/components/hive/climate.py | 2 +- homeassistant/components/humidifier/__init__.py | 6 +++--- .../components/hydrawise/binary_sensor.py | 2 +- .../components/input_boolean/__init__.py | 6 +++--- .../components/input_button/__init__.py | 2 +- .../components/input_number/__init__.py | 4 ++-- .../components/input_select/__init__.py | 4 ++-- homeassistant/components/kef/media_player.py | 2 +- homeassistant/components/lawn_mower/__init__.py | 6 +++--- 29 files changed, 60 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index 4438bf72a1a..933d0c6b40b 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -59,7 +59,7 @@ async def async_setup_entry( platform = async_get_current_platform() for service, method in CAMERA_SERVICES.items(): - platform.async_register_entity_service(service, {}, method) + platform.async_register_entity_service(service, None, method) class AgentCamera(MjpegCamera): diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index f49a962fa87..c49e14f2c6f 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -124,9 +124,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not entities: return False - component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") - component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") - component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") + component.async_register_entity_service(SERVICE_TURN_OFF, None, "async_turn_off") + component.async_register_entity_service(SERVICE_TURN_ON, None, "async_turn_on") + component.async_register_entity_service(SERVICE_TOGGLE, None, "async_toggle") await component.async_add_entities(entities) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 884b5f60f57..75cf6ead6c3 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -87,7 +87,7 @@ async def async_setup_entry( "adb_command", ) platform.async_register_entity_service( - SERVICE_LEARN_SENDEVENT, {}, "learn_sendevent" + SERVICE_LEARN_SENDEVENT, None, "learn_sendevent" ) platform.async_register_entity_service( SERVICE_DOWNLOAD, diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index f2ef404ab34..8ab9c478bc4 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -322,8 +322,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: }, trigger_service_handler, ) - component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") - component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + component.async_register_entity_service(SERVICE_TOGGLE, None, "async_toggle") + component.async_register_entity_service(SERVICE_TURN_ON, None, "async_turn_on") component.async_register_entity_service( SERVICE_TURN_OFF, {vol.Optional(CONF_STOP_ACTIONS, default=DEFAULT_STOP_ACTIONS): cv.boolean}, diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index fcf19adf71e..cce9100a0bd 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -51,8 +51,8 @@ async def async_setup_entry( async_add_entities(entities) platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service(SERVICE_RECORD, {}, "record") - platform.async_register_entity_service(SERVICE_TRIGGER, {}, "trigger_camera") + platform.async_register_entity_service(SERVICE_RECORD, None, "record") + platform.async_register_entity_service(SERVICE_TRIGGER, None, "trigger_camera") platform.async_register_entity_service( SERVICE_SAVE_RECENT_CLIPS, {vol.Required(CONF_FILE_PATH): cv.string}, diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index 3bff7fe754e..c3cf23e4fad 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -52,7 +52,7 @@ async def async_setup_entry( for service in ENTITY_SERVICES: platform.async_register_entity_service( service, - {}, + None, f"async_{service}", ) diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py index 323f9eddd77..3955fabdf00 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -54,7 +54,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_PRESS, - {}, + None, "_async_press_action", ) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index ff11bc04da7..859ced1ba86 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -431,13 +431,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unsub_track_time_interval) component.async_register_entity_service( - SERVICE_ENABLE_MOTION, {}, "async_enable_motion_detection" + SERVICE_ENABLE_MOTION, None, "async_enable_motion_detection" ) component.async_register_entity_service( - SERVICE_DISABLE_MOTION, {}, "async_disable_motion_detection" + SERVICE_DISABLE_MOTION, None, "async_disable_motion_detection" ) - component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") - component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + component.async_register_entity_service(SERVICE_TURN_OFF, None, "async_turn_off") + component.async_register_entity_service(SERVICE_TURN_ON, None, "async_turn_on") component.async_register_entity_service( SERVICE_SNAPSHOT, CAMERA_SERVICE_SNAPSHOT, async_handle_snapshot_service ) diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 07ed8ce7d66..f6de35a4156 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -49,12 +49,12 @@ async def async_setup_platform( platform.async_register_entity_service( SERVICE_SEEK_FORWARD, - {}, + None, "seek_forward", ) platform.async_register_entity_service( SERVICE_SEEK_BACKWARD, - {}, + None, "seek_backward", ) platform.async_register_entity_service( diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index f546ae0e671..6097e4f1346 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -156,19 +156,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_TURN_ON, - {}, + None, "async_turn_on", [ClimateEntityFeature.TURN_ON], ) component.async_register_entity_service( SERVICE_TURN_OFF, - {}, + None, "async_turn_off", [ClimateEntityFeature.TURN_OFF], ) component.async_register_entity_service( SERVICE_TOGGLE, - {}, + None, "async_toggle", [ClimateEntityFeature.TURN_OFF, ClimateEntityFeature.TURN_ON], ) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 324668a63e2..f0a14aa7951 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -122,9 +122,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) - component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") - component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement") - component.async_register_entity_service(SERVICE_RESET, {}, "async_reset") + component.async_register_entity_service(SERVICE_INCREMENT, None, "async_increment") + component.async_register_entity_service(SERVICE_DECREMENT, None, "async_decrement") + component.async_register_entity_service(SERVICE_RESET, None, "async_reset") component.async_register_entity_service( SERVICE_SET_VALUE, {vol.Required(VALUE): cv.positive_int}, diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 645bd88de7a..90d2b644810 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -158,11 +158,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_setup(config) component.async_register_entity_service( - SERVICE_OPEN_COVER, {}, "async_open_cover", [CoverEntityFeature.OPEN] + SERVICE_OPEN_COVER, None, "async_open_cover", [CoverEntityFeature.OPEN] ) component.async_register_entity_service( - SERVICE_CLOSE_COVER, {}, "async_close_cover", [CoverEntityFeature.CLOSE] + SERVICE_CLOSE_COVER, None, "async_close_cover", [CoverEntityFeature.CLOSE] ) component.async_register_entity_service( @@ -177,33 +177,33 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) component.async_register_entity_service( - SERVICE_STOP_COVER, {}, "async_stop_cover", [CoverEntityFeature.STOP] + SERVICE_STOP_COVER, None, "async_stop_cover", [CoverEntityFeature.STOP] ) component.async_register_entity_service( SERVICE_TOGGLE, - {}, + None, "async_toggle", [CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE], ) component.async_register_entity_service( SERVICE_OPEN_COVER_TILT, - {}, + None, "async_open_cover_tilt", [CoverEntityFeature.OPEN_TILT], ) component.async_register_entity_service( SERVICE_CLOSE_COVER_TILT, - {}, + None, "async_close_cover_tilt", [CoverEntityFeature.CLOSE_TILT], ) component.async_register_entity_service( SERVICE_STOP_COVER_TILT, - {}, + None, "async_stop_cover_tilt", [CoverEntityFeature.STOP_TILT], ) @@ -221,7 +221,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_TOGGLE_COVER_TILT, - {}, + None, "async_toggle_tilt", [CoverEntityFeature.OPEN_TILT | CoverEntityFeature.CLOSE_TILT], ) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 8d6df72a67e..a7d8565d6a4 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -152,7 +152,7 @@ async def async_setup_entry( ) platform.async_register_entity_service( SERVICE_UPDATE_AUDYSSEY, - {}, + None, f"async_{SERVICE_UPDATE_AUDYSSEY}", ) diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index e690038ff71..0d14267e08d 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -65,7 +65,7 @@ async def async_setup_entry( platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_RAW_GET_POSITIONS, - {}, + None, "async_raw_get_positions", supports_response=SupportsResponse.ONLY, ) diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 339bed97f6f..a62a26f21d3 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -40,7 +40,7 @@ async def async_setup_entry( platform = async_get_current_platform() platform.async_register_entity_service( SERVICE_IDENTIFY, - {}, + None, ElgatoLight.async_identify.__name__, ) diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 7d3601f0bd0..16f877719a7 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -56,7 +56,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_SENSOR_COUNTER_REFRESH, - {}, + None, "async_counter_refresh", ) platform.async_register_entity_service( @@ -71,7 +71,7 @@ async def async_setup_entry( ) platform.async_register_entity_service( SERVICE_SENSOR_ZONE_TRIGGER, - {}, + None, "async_zone_trigger", ) diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 455c41b385f..3c4a5f70ff4 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -112,7 +112,7 @@ async def async_setup_entry( platform = async_get_current_platform() platform.async_register_entity_service( - SERVICE_WAKE_DEVICE, {}, "perform_wake_device" + SERVICE_WAKE_DEVICE, None, "perform_wake_device" ) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 6ecc675a45e..5a15ece665a 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -139,11 +139,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: [FanEntityFeature.TURN_ON], ) component.async_register_entity_service( - SERVICE_TURN_OFF, {}, "async_turn_off", [FanEntityFeature.TURN_OFF] + SERVICE_TURN_OFF, None, "async_turn_off", [FanEntityFeature.TURN_OFF] ) component.async_register_entity_service( SERVICE_TOGGLE, - {}, + None, "async_toggle", [FanEntityFeature.TURN_OFF, FanEntityFeature.TURN_ON], ) diff --git a/homeassistant/components/flo/switch.py b/homeassistant/components/flo/switch.py index ab201dfb906..f0460839837 100644 --- a/homeassistant/components/flo/switch.py +++ b/homeassistant/components/flo/switch.py @@ -42,13 +42,13 @@ async def async_setup_entry( platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( - SERVICE_SET_AWAY_MODE, {}, "async_set_mode_away" + SERVICE_SET_AWAY_MODE, None, "async_set_mode_away" ) platform.async_register_entity_service( - SERVICE_SET_HOME_MODE, {}, "async_set_mode_home" + SERVICE_SET_HOME_MODE, None, "async_set_mode_home" ) platform.async_register_entity_service( - SERVICE_RUN_HEALTH_TEST, {}, "async_run_health_test" + SERVICE_RUN_HEALTH_TEST, None, "async_run_health_test" ) platform.async_register_entity_service( SERVICE_SET_SLEEP_MODE, diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index d30aa475944..efbd4b2ac02 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -75,7 +75,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_SYNC, - {}, + None, "sync", ) platform.async_register_entity_service( diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index f4c8e678702..87d93eea95f 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -83,7 +83,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_BOOST_HEATING_OFF, - {}, + None, "async_heating_boost_off", ) diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index ce94eaaf5a0..37e2bd3e3ba 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -92,9 +92,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) await component.async_setup(config) - component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") - component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") - component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") + component.async_register_entity_service(SERVICE_TURN_ON, None, "async_turn_on") + component.async_register_entity_service(SERVICE_TURN_OFF, None, "async_turn_off") + component.async_register_entity_service(SERVICE_TOGGLE, None, "async_toggle") component.async_register_entity_service( SERVICE_SET_MODE, {vol.Required(ATTR_MODE): cv.string}, diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index 0e00d237fae..9b6dcadf95f 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -110,7 +110,7 @@ async def async_setup_entry( ) async_add_entities(entities) platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service(SERVICE_RESUME, {}, "resume") + platform.async_register_entity_service(SERVICE_RESUME, None, "resume") platform.async_register_entity_service( SERVICE_START_WATERING, SCHEMA_START_WATERING, "start_watering" ) diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 57165c5508a..54457ab2fb7 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -138,11 +138,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=RELOAD_SERVICE_SCHEMA, ) - component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + component.async_register_entity_service(SERVICE_TURN_ON, None, "async_turn_on") - component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") + component.async_register_entity_service(SERVICE_TURN_OFF, None, "async_turn_off") - component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") + component.async_register_entity_service(SERVICE_TOGGLE, None, "async_toggle") return True diff --git a/homeassistant/components/input_button/__init__.py b/homeassistant/components/input_button/__init__.py index 1488d80a1f5..6584b40fb55 100644 --- a/homeassistant/components/input_button/__init__.py +++ b/homeassistant/components/input_button/__init__.py @@ -123,7 +123,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=RELOAD_SERVICE_SCHEMA, ) - component.async_register_entity_service(SERVICE_PRESS, {}, "_async_press_action") + component.async_register_entity_service(SERVICE_PRESS, None, "_async_press_action") return True diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index f55ceabc6f0..d52bfedfe77 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -157,9 +157,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "async_set_value", ) - component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") + component.async_register_entity_service(SERVICE_INCREMENT, None, "async_increment") - component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement") + component.async_register_entity_service(SERVICE_DECREMENT, None, "async_decrement") return True diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 44d2df02a92..6efe16240cb 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -183,13 +183,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_SELECT_FIRST, - {}, + None, InputSelect.async_first.__name__, ) component.async_register_entity_service( SERVICE_SELECT_LAST, - {}, + None, InputSelect.async_last.__name__, ) diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index ad335499ba4..1c5188b1a6f 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -161,7 +161,7 @@ async def async_setup_platform( }, "set_mode", ) - platform.async_register_entity_service(SERVICE_UPDATE_DSP, {}, "update_dsp") + platform.async_register_entity_service(SERVICE_UPDATE_DSP, None, "update_dsp") def add_service(name, which, option): options = DSP_OPTION_MAPPING[which] diff --git a/homeassistant/components/lawn_mower/__init__.py b/homeassistant/components/lawn_mower/__init__.py index 27765d207d8..9eef6ad8343 100644 --- a/homeassistant/components/lawn_mower/__init__.py +++ b/homeassistant/components/lawn_mower/__init__.py @@ -39,15 +39,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_START_MOWING, - {}, + None, "async_start_mowing", [LawnMowerEntityFeature.START_MOWING], ) component.async_register_entity_service( - SERVICE_PAUSE, {}, "async_pause", [LawnMowerEntityFeature.PAUSE] + SERVICE_PAUSE, None, "async_pause", [LawnMowerEntityFeature.PAUSE] ) component.async_register_entity_service( - SERVICE_DOCK, {}, "async_dock", [LawnMowerEntityFeature.DOCK] + SERVICE_DOCK, None, "async_dock", [LawnMowerEntityFeature.DOCK] ) return True From e050d187c4c4b43fc1ff3c6133baae22541fdab5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Aug 2024 14:04:53 +0200 Subject: [PATCH 2299/2411] Clarify SQLite can't drop foreign key constraints (#123898) --- .../components/recorder/migration.py | 48 ++++++++++++++++--- tests/components/recorder/test_migrate.py | 48 +++++++++++++++++++ 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index e96cef14cf4..ccdaf3082e0 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -585,7 +585,18 @@ def _modify_columns( def _update_states_table_with_foreign_key_options( session_maker: Callable[[], Session], engine: Engine ) -> None: - """Add the options to foreign key constraints.""" + """Add the options to foreign key constraints. + + This is not supported for SQLite because it does not support + dropping constraints. + """ + + if engine.dialect.name not in (SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL): + raise RuntimeError( + "_update_states_table_with_foreign_key_options not supported for " + f"{engine.dialect.name}" + ) + inspector = sqlalchemy.inspect(engine) tmp_states_table = Table(TABLE_STATES, MetaData()) alters = [ @@ -633,7 +644,17 @@ def _update_states_table_with_foreign_key_options( def _drop_foreign_key_constraints( session_maker: Callable[[], Session], engine: Engine, table: str, column: str ) -> tuple[bool, list[tuple[str, str, ReflectedForeignKeyConstraint]]]: - """Drop foreign key constraints for a table on specific columns.""" + """Drop foreign key constraints for a table on specific columns. + + This is not supported for SQLite because it does not support + dropping constraints. + """ + + if engine.dialect.name not in (SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL): + raise RuntimeError( + f"_drop_foreign_key_constraints not supported for {engine.dialect.name}" + ) + inspector = sqlalchemy.inspect(engine) dropped_constraints = [ (table, column, foreign_key) @@ -1026,7 +1047,17 @@ class _SchemaVersion11Migrator(_SchemaVersionMigrator, target_version=11): def _apply_update(self) -> None: """Version specific update method.""" _create_index(self.session_maker, "states", "ix_states_old_state_id") - _update_states_table_with_foreign_key_options(self.session_maker, self.engine) + + # _update_states_table_with_foreign_key_options first drops foreign + # key constraints, and then re-adds them with the correct settings. + # This is not supported by SQLite + if self.engine.dialect.name in ( + SupportedDialect.MYSQL, + SupportedDialect.POSTGRESQL, + ): + _update_states_table_with_foreign_key_options( + self.session_maker, self.engine + ) class _SchemaVersion12Migrator(_SchemaVersionMigrator, target_version=12): @@ -1080,9 +1111,14 @@ class _SchemaVersion15Migrator(_SchemaVersionMigrator, target_version=15): class _SchemaVersion16Migrator(_SchemaVersionMigrator, target_version=16): def _apply_update(self) -> None: """Version specific update method.""" - _drop_foreign_key_constraints( - self.session_maker, self.engine, TABLE_STATES, "old_state_id" - ) + # Dropping foreign key constraints is not supported by SQLite + if self.engine.dialect.name in ( + SupportedDialect.MYSQL, + SupportedDialect.POSTGRESQL, + ): + _drop_foreign_key_constraints( + self.session_maker, self.engine, TABLE_STATES, "old_state_id" + ) class _SchemaVersion17Migrator(_SchemaVersionMigrator, target_version=17): diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 2a33f08050c..625a5023287 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -1053,3 +1053,51 @@ def test_delete_foreign_key_violations_unsupported_engine( RuntimeError, match="_delete_foreign_key_violations not supported for sqlite" ): migration._delete_foreign_key_violations(session_maker, engine, "", "", "", "") + + +def test_drop_foreign_key_constraints_unsupported_engine( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test calling _drop_foreign_key_constraints with an unsupported engine.""" + + connection = Mock() + connection.execute = Mock(side_effect=InternalError(None, None, None)) + session = Mock() + session.connection = Mock(return_value=connection) + instance = Mock() + instance.get_session = Mock(return_value=session) + engine = Mock() + engine.dialect = Mock() + engine.dialect.name = "sqlite" + + session_maker = Mock(return_value=session) + with pytest.raises( + RuntimeError, match="_drop_foreign_key_constraints not supported for sqlite" + ): + migration._drop_foreign_key_constraints(session_maker, engine, "", "") + + +def test_update_states_table_with_foreign_key_options_unsupported_engine( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test calling function with an unsupported engine. + + This tests _update_states_table_with_foreign_key_options. + """ + + connection = Mock() + connection.execute = Mock(side_effect=InternalError(None, None, None)) + session = Mock() + session.connection = Mock(return_value=connection) + instance = Mock() + instance.get_session = Mock(return_value=session) + engine = Mock() + engine.dialect = Mock() + engine.dialect.name = "sqlite" + + session_maker = Mock(return_value=session) + with pytest.raises( + RuntimeError, + match="_update_states_table_with_foreign_key_options not supported for sqlite", + ): + migration._update_states_table_with_foreign_key_options(session_maker, engine) From f6cb28eb5b3ca0a1c71d8a5d2281f9be9c24e501 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Aug 2024 07:42:20 -0500 Subject: [PATCH 2300/2411] Bump aioesphomeapi to 25.1.0 (#123851) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 97724a12203..b2647709e8e 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==25.0.0", + "aioesphomeapi==25.1.0", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index ba19291607d..beae1629b79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -237,7 +237,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==25.0.0 +aioesphomeapi==25.1.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6785ede042..86be5c9e14a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -225,7 +225,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==25.0.0 +aioesphomeapi==25.1.0 # homeassistant.components.flo aioflo==2021.11.0 From ccde51da855fd035f0e79dfcb2c2c5fc2d2c9392 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:56:03 +0200 Subject: [PATCH 2301/2411] Improve type hints in tasmota tests (#123913) --- tests/components/tasmota/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/tasmota/conftest.py b/tests/components/tasmota/conftest.py index 48cd4012f07..0de0788d7d9 100644 --- a/tests/components/tasmota/conftest.py +++ b/tests/components/tasmota/conftest.py @@ -10,6 +10,7 @@ from homeassistant.components.tasmota.const import ( DEFAULT_PREFIX, DOMAIN, ) +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa: F401 @@ -55,6 +56,6 @@ async def setup_tasmota_helper(hass): @pytest.fixture -async def setup_tasmota(hass): +async def setup_tasmota(hass: HomeAssistant) -> None: """Set up Tasmota.""" await setup_tasmota_helper(hass) From 324b6529e862143050657dafa93bb2e86aeb1501 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:56:58 +0200 Subject: [PATCH 2302/2411] Improve type hints in telegram_bot tests (#123914) --- tests/components/telegram_bot/conftest.py | 29 +++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/tests/components/telegram_bot/conftest.py b/tests/components/telegram_bot/conftest.py index 6ea5d1446dd..1afe70dcb8a 100644 --- a/tests/components/telegram_bot/conftest.py +++ b/tests/components/telegram_bot/conftest.py @@ -1,6 +1,8 @@ """Tests for the telegram_bot integration.""" +from collections.abc import AsyncGenerator, Generator from datetime import datetime +from typing import Any from unittest.mock import patch import pytest @@ -18,11 +20,12 @@ from homeassistant.const import ( CONF_URL, EVENT_HOMEASSISTANT_START, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @pytest.fixture -def config_webhooks(): +def config_webhooks() -> dict[str, Any]: """Fixture for a webhooks platform configuration.""" return { DOMAIN: [ @@ -43,7 +46,7 @@ def config_webhooks(): @pytest.fixture -def config_polling(): +def config_polling() -> dict[str, Any]: """Fixture for a polling platform configuration.""" return { DOMAIN: [ @@ -62,7 +65,7 @@ def config_polling(): @pytest.fixture -def mock_register_webhook(): +def mock_register_webhook() -> Generator[None]: """Mock calls made by telegram_bot when (de)registering webhook.""" with ( patch( @@ -78,7 +81,7 @@ def mock_register_webhook(): @pytest.fixture -def mock_external_calls(): +def mock_external_calls() -> Generator[None]: """Mock calls that make calls to the live Telegram API.""" test_user = User(123456, "Testbot", True) message = Message( @@ -109,7 +112,7 @@ def mock_external_calls(): @pytest.fixture -def mock_generate_secret_token(): +def mock_generate_secret_token() -> Generator[str]: """Mock secret token generated for webhook.""" mock_secret_token = "DEADBEEF12345678DEADBEEF87654321" with patch( @@ -217,12 +220,12 @@ def update_callback_query(): @pytest.fixture async def webhook_platform( - hass, - config_webhooks, - mock_register_webhook, - mock_external_calls, - mock_generate_secret_token, -): + hass: HomeAssistant, + config_webhooks: dict[str, Any], + mock_register_webhook: None, + mock_external_calls: None, + mock_generate_secret_token: str, +) -> AsyncGenerator[None]: """Fixture for setting up the webhooks platform using appropriate config and mocks.""" await async_setup_component( hass, @@ -235,7 +238,9 @@ async def webhook_platform( @pytest.fixture -async def polling_platform(hass, config_polling, mock_external_calls): +async def polling_platform( + hass: HomeAssistant, config_polling: dict[str, Any], mock_external_calls: None +) -> None: """Fixture for setting up the polling platform using appropriate config and mocks.""" await async_setup_component( hass, From 67f761c0e9538c28a63d7102f8150c7f597be64f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:58:16 +0200 Subject: [PATCH 2303/2411] Improve type hints in template tests (#123915) --- tests/components/template/test_light.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index ad97146d0fb..065a1488dc9 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -1,5 +1,7 @@ """The tests for the Template light platform.""" +from typing import Any + import pytest from homeassistant.components import light @@ -152,7 +154,9 @@ OPTIMISTIC_RGBWW_COLOR_LIGHT_CONFIG = { } -async def async_setup_light(hass, count, light_config): +async def async_setup_light( + hass: HomeAssistant, count: int, light_config: dict[str, Any] +) -> None: """Do setup of light integration.""" config = {"light": {"platform": "template", "lights": light_config}} @@ -169,7 +173,9 @@ async def async_setup_light(hass, count, light_config): @pytest.fixture -async def setup_light(hass, count, light_config): +async def setup_light( + hass: HomeAssistant, count: int, light_config: dict[str, Any] +) -> None: """Do setup of light integration.""" await async_setup_light(hass, count, light_config) From 7f6bf95aa6976da2c03ffdce9060f68c54874460 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:02:52 +0200 Subject: [PATCH 2304/2411] Improve type hints in universal tests (#123920) --- tests/components/universal/test_media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 187b62a93a1..69d4c8666cf 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -220,7 +220,7 @@ class MockMediaPlayer(media_player.MediaPlayerEntity): @pytest.fixture -async def mock_states(hass): +async def mock_states(hass: HomeAssistant) -> Mock: """Set mock states used in tests.""" result = Mock() From 1e5762fbf7a55590d903100fb41963db8a7326b9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:03:38 +0200 Subject: [PATCH 2305/2411] Improve type hints in tod tests (#123917) --- tests/components/tod/test_binary_sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index c4b28b527cb..b4b6b13d8e3 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -1,6 +1,6 @@ """Test Times of the Day Binary Sensor.""" -from datetime import datetime, timedelta +from datetime import datetime, timedelta, tzinfo from freezegun.api import FrozenDateTimeFactory import pytest @@ -16,13 +16,13 @@ from tests.common import assert_setup_component, async_fire_time_changed @pytest.fixture -def hass_time_zone(): +def hass_time_zone() -> str: """Return default hass timezone.""" return "US/Pacific" @pytest.fixture(autouse=True) -async def setup_fixture(hass, hass_time_zone): +async def setup_fixture(hass: HomeAssistant, hass_time_zone: str) -> None: """Set up things to be run when tests are started.""" hass.config.latitude = 50.27583 hass.config.longitude = 18.98583 @@ -30,7 +30,7 @@ async def setup_fixture(hass, hass_time_zone): @pytest.fixture -def hass_tz_info(hass): +def hass_tz_info(hass: HomeAssistant) -> tzinfo | None: """Return timezone info for the hass timezone.""" return dt_util.get_time_zone(hass.config.time_zone) From 78c868c0754303a433296a4e0773d23507fd4e0e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:03:51 +0200 Subject: [PATCH 2306/2411] Improve type hints in tile tests (#123916) --- tests/components/tile/conftest.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/components/tile/conftest.py b/tests/components/tile/conftest.py index e3b55c49ae7..01a711d9261 100644 --- a/tests/components/tile/conftest.py +++ b/tests/components/tile/conftest.py @@ -1,6 +1,8 @@ """Define test fixtures for Tile.""" +from collections.abc import Generator import json +from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest @@ -8,6 +10,7 @@ from pytile.tile import Tile from homeassistant.components.tile.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -16,7 +19,7 @@ TEST_USERNAME = "user@host.com" @pytest.fixture(name="api") -def api_fixture(hass, data_tile_details): +def api_fixture(data_tile_details: dict[str, Any]) -> Mock: """Define a pytile API object.""" tile = Tile(None, data_tile_details) tile.async_update = AsyncMock() @@ -29,7 +32,9 @@ def api_fixture(hass, data_tile_details): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config): +def config_entry_fixture( + hass: HomeAssistant, config: dict[str, Any] +) -> MockConfigEntry: """Define a config entry fixture.""" entry = MockConfigEntry(domain=DOMAIN, unique_id=config[CONF_USERNAME], data=config) entry.add_to_hass(hass) @@ -37,7 +42,7 @@ def config_entry_fixture(hass, config): @pytest.fixture(name="config") -def config_fixture(): +def config_fixture() -> dict[str, Any]: """Define a config entry data fixture.""" return { CONF_USERNAME: TEST_USERNAME, @@ -52,7 +57,7 @@ def data_tile_details_fixture(): @pytest.fixture(name="mock_pytile") -async def mock_pytile_fixture(api): +def mock_pytile_fixture(api: Mock) -> Generator[None]: """Define a fixture to patch pytile.""" with ( patch( @@ -64,7 +69,9 @@ async def mock_pytile_fixture(api): @pytest.fixture(name="setup_config_entry") -async def setup_config_entry_fixture(hass, config_entry, mock_pytile): +async def setup_config_entry_fixture( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_pytile: None +) -> None: """Define a fixture to set up tile.""" assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From 99b1fc75d33a8ee61e89d3ffa5454d17608f2352 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:04:51 +0200 Subject: [PATCH 2307/2411] Improve type hints in traccar tests (#123919) --- tests/components/traccar/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index b25ab6a0a34..49127aec347 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -63,7 +63,7 @@ async def setup_zones(hass: HomeAssistant) -> None: @pytest.fixture(name="webhook_id") -async def webhook_id_fixture(hass, client): +async def webhook_id_fixture(hass: HomeAssistant, client: TestClient) -> str: """Initialize the Traccar component and get the webhook_id.""" await async_process_ha_core_config( hass, From 2c99bd178ce6ce1ab710d99af1aef0964a21346c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:06:33 +0200 Subject: [PATCH 2308/2411] Improve type hints in subaru tests (#123911) --- tests/components/subaru/conftest.py | 8 +++++--- tests/components/subaru/test_config_flow.py | 17 ++++++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/components/subaru/conftest.py b/tests/components/subaru/conftest.py index f769eba252c..e18ea8fd398 100644 --- a/tests/components/subaru/conftest.py +++ b/tests/components/subaru/conftest.py @@ -100,7 +100,7 @@ TEST_DEVICE_NAME = "test_vehicle_2" TEST_ENTITY_ID = f"sensor.{TEST_DEVICE_NAME}_odometer" -def advance_time_to_next_fetch(hass): +def advance_time_to_next_fetch(hass: HomeAssistant) -> None: """Fast forward time to next fetch.""" future = dt_util.utcnow() + timedelta(seconds=FETCH_INTERVAL + 30) async_fire_time_changed(hass, future) @@ -181,7 +181,7 @@ async def setup_subaru_config_entry( @pytest.fixture -async def subaru_config_entry(hass): +async def subaru_config_entry(hass: HomeAssistant) -> MockConfigEntry: """Create a Subaru config entry prior to setup.""" await async_setup_component(hass, HA_DOMAIN, {}) config_entry = MockConfigEntry(**TEST_CONFIG_ENTRY) @@ -190,7 +190,9 @@ async def subaru_config_entry(hass): @pytest.fixture -async def ev_entry(hass, subaru_config_entry): +async def ev_entry( + hass: HomeAssistant, subaru_config_entry: MockConfigEntry +) -> MockConfigEntry: """Create a Subaru entry representing an EV vehicle with full STARLINK subscription.""" await setup_subaru_config_entry(hass, subaru_config_entry) assert DOMAIN in hass.config_entries.async_domains() diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index 9bddeeee051..6abc544c92a 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -10,6 +10,7 @@ from subarulink.exceptions import InvalidCredentials, InvalidPIN, SubaruExceptio from homeassistant import config_entries from homeassistant.components.subaru import config_flow from homeassistant.components.subaru.const import CONF_UPDATE_ENABLED, DOMAIN +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_DEVICE_ID, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -389,7 +390,7 @@ async def test_option_flow(hass: HomeAssistant, options_form) -> None: @pytest.fixture -async def user_form(hass): +async def user_form(hass: HomeAssistant) -> ConfigFlowResult: """Return initial form for Subaru config flow.""" return await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -397,7 +398,9 @@ async def user_form(hass): @pytest.fixture -async def two_factor_start_form(hass, user_form): +async def two_factor_start_form( + hass: HomeAssistant, user_form: ConfigFlowResult +) -> ConfigFlowResult: """Return two factor form for Subaru config flow.""" with ( patch(MOCK_API_CONNECT, return_value=True), @@ -410,7 +413,9 @@ async def two_factor_start_form(hass, user_form): @pytest.fixture -async def two_factor_verify_form(hass, two_factor_start_form): +async def two_factor_verify_form( + hass: HomeAssistant, two_factor_start_form: ConfigFlowResult +) -> ConfigFlowResult: """Return two factor form for Subaru config flow.""" with ( patch( @@ -427,7 +432,9 @@ async def two_factor_verify_form(hass, two_factor_start_form): @pytest.fixture -async def pin_form(hass, two_factor_verify_form): +async def pin_form( + hass: HomeAssistant, two_factor_verify_form: ConfigFlowResult +) -> ConfigFlowResult: """Return PIN input form for Subaru config flow.""" with ( patch( @@ -443,7 +450,7 @@ async def pin_form(hass, two_factor_verify_form): @pytest.fixture -async def options_form(hass): +async def options_form(hass: HomeAssistant) -> ConfigFlowResult: """Return options form for Subaru config flow.""" entry = MockConfigEntry(domain=DOMAIN, data={}, options=None) entry.add_to_hass(hass) From fa8f86b672144b335f381d781975e3f39558dcb0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:07:55 +0200 Subject: [PATCH 2309/2411] Improve type hints in smartthings tests (#123912) --- tests/components/smartthings/conftest.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 17e2c781989..70fd9db0744 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -91,7 +91,7 @@ async def setup_component( await async_setup_component(hass, "smartthings", {}) -def _create_location(): +def _create_location() -> Mock: loc = Mock(Location) loc.name = "Test Location" loc.location_id = str(uuid4()) @@ -99,19 +99,19 @@ def _create_location(): @pytest.fixture(name="location") -def location_fixture(): +def location_fixture() -> Mock: """Fixture for a single location.""" return _create_location() @pytest.fixture(name="locations") -def locations_fixture(location): +def locations_fixture(location: Mock) -> list[Mock]: """Fixture for 2 locations.""" return [location, _create_location()] @pytest.fixture(name="app") -async def app_fixture(hass, config_file): +async def app_fixture(hass: HomeAssistant, config_file: dict[str, str]) -> Mock: """Fixture for a single app.""" app = Mock(AppEntity) app.app_name = APP_NAME_PREFIX + str(uuid4()) @@ -133,7 +133,7 @@ async def app_fixture(hass, config_file): @pytest.fixture(name="app_oauth_client") -def app_oauth_client_fixture(): +def app_oauth_client_fixture() -> Mock: """Fixture for a single app's oauth.""" client = Mock(AppOAuthClient) client.client_id = str(uuid4()) @@ -150,7 +150,7 @@ def app_settings_fixture(app, config_file): return settings -def _create_installed_app(location_id, app_id): +def _create_installed_app(location_id: str, app_id: str) -> Mock: item = Mock(InstalledApp) item.installed_app_id = str(uuid4()) item.installed_app_status = InstalledAppStatus.AUTHORIZED @@ -161,7 +161,7 @@ def _create_installed_app(location_id, app_id): @pytest.fixture(name="installed_app") -def installed_app_fixture(location, app): +def installed_app_fixture(location: Mock, app: Mock) -> Mock: """Fixture for a single installed app.""" return _create_installed_app(location.location_id, app.app_id) @@ -222,7 +222,7 @@ def device_fixture(location): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, installed_app, location): +def config_entry_fixture(installed_app: Mock, location: Mock) -> MockConfigEntry: """Fixture representing a config entry.""" data = { CONF_ACCESS_TOKEN: str(uuid4()), From 3e5d0eb632313523ce4cfc637eb542294516d80d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:08:10 +0200 Subject: [PATCH 2310/2411] Improve type hints in owntracks tests (#123905) --- tests/components/owntracks/test_device_tracker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index b4b5c00880c..bc2ae7ce4d8 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1,7 +1,7 @@ """The tests for the Owntracks device tracker.""" import base64 -from collections.abc import Callable +from collections.abc import Callable, Generator import json import pickle from unittest.mock import patch @@ -294,7 +294,7 @@ def setup_comp( hass: HomeAssistant, mock_device_tracker_conf: list[Device], mqtt_mock: MqttMockHAClient, -): +) -> None: """Initialize components.""" hass.loop.run_until_complete(async_setup_component(hass, "device_tracker", {})) @@ -1426,7 +1426,7 @@ def mock_cipher(): @pytest.fixture -def config_context(hass, setup_comp): +def config_context(setup_comp: None) -> Generator[None]: """Set up the mocked context.""" patch_load = patch( "homeassistant.components.device_tracker.async_load_config", From f7e017aa73d912393cc95d7c9c488b6c858ca970 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:08:55 +0200 Subject: [PATCH 2311/2411] Improve type hints in sia tests (#123909) --- tests/components/sia/test_config_flow.py | 30 ++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/tests/components/sia/test_config_flow.py b/tests/components/sia/test_config_flow.py index 95de53d7fbe..b0d83855a25 100644 --- a/tests/components/sia/test_config_flow.py +++ b/tests/components/sia/test_config_flow.py @@ -1,5 +1,6 @@ """Test the sia config flow.""" +from collections.abc import Generator from unittest.mock import patch import pytest @@ -16,6 +17,7 @@ from homeassistant.components.sia.const import ( CONF_ZONES, DOMAIN, ) +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_PORT, CONF_PROTOCOL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -105,7 +107,7 @@ ADDITIONAL_OPTIONS = { @pytest.fixture -async def flow_at_user_step(hass): +async def flow_at_user_step(hass: HomeAssistant) -> ConfigFlowResult: """Return a initialized flow.""" return await hass.config_entries.flow.async_init( DOMAIN, @@ -114,7 +116,9 @@ async def flow_at_user_step(hass): @pytest.fixture -async def entry_with_basic_config(hass, flow_at_user_step): +async def entry_with_basic_config( + hass: HomeAssistant, flow_at_user_step: ConfigFlowResult +) -> ConfigFlowResult: """Return a entry with a basic config.""" with patch("homeassistant.components.sia.async_setup_entry", return_value=True): return await hass.config_entries.flow.async_configure( @@ -123,7 +127,9 @@ async def entry_with_basic_config(hass, flow_at_user_step): @pytest.fixture -async def flow_at_add_account_step(hass, flow_at_user_step): +async def flow_at_add_account_step( + hass: HomeAssistant, flow_at_user_step: ConfigFlowResult +) -> ConfigFlowResult: """Return a initialized flow at the additional account step.""" return await hass.config_entries.flow.async_configure( flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL @@ -131,7 +137,9 @@ async def flow_at_add_account_step(hass, flow_at_user_step): @pytest.fixture -async def entry_with_additional_account_config(hass, flow_at_add_account_step): +async def entry_with_additional_account_config( + hass: HomeAssistant, flow_at_add_account_step: ConfigFlowResult +) -> ConfigFlowResult: """Return a entry with a two account config.""" with patch("homeassistant.components.sia.async_setup_entry", return_value=True): return await hass.config_entries.flow.async_configure( @@ -139,7 +147,7 @@ async def entry_with_additional_account_config(hass, flow_at_add_account_step): ) -async def setup_sia(hass: HomeAssistant, config_entry: MockConfigEntry): +async def setup_sia(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: """Add mock config to HASS.""" assert await async_setup_component(hass, DOMAIN, {}) config_entry.add_to_hass(hass) @@ -147,23 +155,21 @@ async def setup_sia(hass: HomeAssistant, config_entry: MockConfigEntry): await hass.async_block_till_done() -async def test_form_start_user(hass: HomeAssistant, flow_at_user_step) -> None: +async def test_form_start_user(flow_at_user_step: ConfigFlowResult) -> None: """Start the form and check if you get the right id and schema for the user step.""" assert flow_at_user_step["step_id"] == "user" assert flow_at_user_step["errors"] is None assert flow_at_user_step["data_schema"] == HUB_SCHEMA -async def test_form_start_account( - hass: HomeAssistant, flow_at_add_account_step -) -> None: +async def test_form_start_account(flow_at_add_account_step: ConfigFlowResult) -> None: """Start the form and check if you get the right id and schema for the additional account step.""" assert flow_at_add_account_step["step_id"] == "add_account" assert flow_at_add_account_step["errors"] is None assert flow_at_add_account_step["data_schema"] == ACCOUNT_SCHEMA -async def test_create(hass: HomeAssistant, entry_with_basic_config) -> None: +async def test_create(entry_with_basic_config: ConfigFlowResult) -> None: """Test we create a entry through the form.""" assert entry_with_basic_config["type"] is FlowResultType.CREATE_ENTRY assert ( @@ -175,7 +181,7 @@ async def test_create(hass: HomeAssistant, entry_with_basic_config) -> None: async def test_create_additional_account( - hass: HomeAssistant, entry_with_additional_account_config + entry_with_additional_account_config: ConfigFlowResult, ) -> None: """Test we create a config with two accounts.""" assert entry_with_additional_account_config["type"] is FlowResultType.CREATE_ENTRY @@ -210,7 +216,7 @@ async def test_abort_form(hass: HomeAssistant) -> None: @pytest.fixture(autouse=True) -def mock_sia(): +def mock_sia() -> Generator[None]: """Mock SIAClient.""" with patch("homeassistant.components.sia.hub.SIAClient", autospec=True): yield From 17f0d9ce45c9eb7b65fec9f2da4991d02b1c352b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Aug 2024 08:36:49 -0500 Subject: [PATCH 2312/2411] Map pre-heating and defrosting hvac actions in homekit (#123907) closes #123864 --- .../components/homekit/type_thermostats.py | 8 +++-- .../homekit/test_type_thermostats.py | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 5dc520e8568..a97f26b7abb 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -150,6 +150,8 @@ HC_HASS_TO_HOMEKIT_ACTION = { HVACAction.COOLING: HC_HEAT_COOL_COOL, HVACAction.DRYING: HC_HEAT_COOL_COOL, HVACAction.FAN: HC_HEAT_COOL_COOL, + HVACAction.PREHEATING: HC_HEAT_COOL_HEAT, + HVACAction.DEFROSTING: HC_HEAT_COOL_HEAT, } FAN_STATE_INACTIVE = 0 @@ -623,8 +625,10 @@ class Thermostat(HomeAccessory): ) # Set current operation mode for supported thermostats - if hvac_action := attributes.get(ATTR_HVAC_ACTION): - homekit_hvac_action = HC_HASS_TO_HOMEKIT_ACTION[hvac_action] + if (hvac_action := attributes.get(ATTR_HVAC_ACTION)) and ( + homekit_hvac_action := HC_HASS_TO_HOMEKIT_ACTION.get(hvac_action), + HC_HEAT_COOL_OFF, + ): self.char_current_heat_cool.set_value(homekit_hvac_action) # Update current temperature diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 3a32e94e491..8454610566b 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -161,6 +161,40 @@ async def test_thermostat(hass: HomeAssistant, hk_driver, events: list[Event]) - assert acc.char_current_temp.value == 23.0 assert acc.char_display_units.value == 0 + hass.states.async_set( + entity_id, + HVACMode.HEAT, + { + **base_attrs, + ATTR_TEMPERATURE: 22.2, + ATTR_CURRENT_TEMPERATURE: 17.8, + ATTR_HVAC_ACTION: HVACAction.PREHEATING, + }, + ) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.2 + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 1 + assert acc.char_current_temp.value == 17.8 + assert acc.char_display_units.value == 0 + + hass.states.async_set( + entity_id, + HVACMode.HEAT, + { + **base_attrs, + ATTR_TEMPERATURE: 22.2, + ATTR_CURRENT_TEMPERATURE: 17.8, + ATTR_HVAC_ACTION: HVACAction.DEFROSTING, + }, + ) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.2 + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 1 + assert acc.char_current_temp.value == 17.8 + assert acc.char_display_units.value == 0 + hass.states.async_set( entity_id, HVACMode.FAN_ONLY, From e6fc34325de46f85a5bd98856228c5989d050da4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:39:52 +0200 Subject: [PATCH 2313/2411] Improve type hints in zha tests (#123926) --- tests/components/zha/test_config_flow.py | 62 +++++++++++++++++------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index f3104141269..af6f2d9af0c 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,14 +1,16 @@ """Tests for ZHA config flow.""" +from collections.abc import Callable, Coroutine, Generator import copy from datetime import timedelta from ipaddress import ip_address import json +from typing import Any from unittest.mock import AsyncMock, MagicMock, PropertyMock, create_autospec, patch import uuid import pytest -import serial.tools.list_ports +from serial.tools.list_ports_common import ListPortInfo from zha.application.const import RadioType from zigpy.backups import BackupManager import zigpy.config @@ -36,6 +38,7 @@ from homeassistant.config_entries import ( SOURCE_USER, SOURCE_ZEROCONF, ConfigEntryState, + ConfigFlowResult, ) from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant @@ -43,6 +46,9 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry +type RadioPicker = Callable[ + [RadioType], Coroutine[Any, Any, tuple[ConfigFlowResult, ListPortInfo]] +] PROBE_FUNCTION_PATH = "zigbee.application.ControllerApplication.probe" @@ -70,7 +76,7 @@ def mock_multipan_platform(): @pytest.fixture(autouse=True) -def mock_app(): +def mock_app() -> Generator[AsyncMock]: """Mock zigpy app interface.""" mock_app = AsyncMock() mock_app.backups = create_autospec(BackupManager, instance=True) @@ -130,9 +136,9 @@ def mock_detect_radio_type( return detect -def com_port(device="/dev/ttyUSB1234"): +def com_port(device="/dev/ttyUSB1234") -> ListPortInfo: """Mock of a serial port.""" - port = serial.tools.list_ports_common.ListPortInfo("/dev/ttyUSB1234") + port = ListPortInfo("/dev/ttyUSB1234") port.serial_number = "1234" port.manufacturer = "Virtual serial port" port.device = device @@ -1038,10 +1044,12 @@ def test_prevent_overwrite_ezsp_ieee() -> None: @pytest.fixture -def pick_radio(hass): +def pick_radio( + hass: HomeAssistant, +) -> Generator[RadioPicker]: """Fixture for the first step of the config flow (where a radio is picked).""" - async def wrapper(radio_type): + async def wrapper(radio_type: RadioType) -> tuple[ConfigFlowResult, ListPortInfo]: port = com_port() port_select = f"{port}, s/n: {port.serial_number} - {port.manufacturer}" @@ -1070,7 +1078,7 @@ def pick_radio(hass): async def test_strategy_no_network_settings( - pick_radio, mock_app, hass: HomeAssistant + pick_radio: RadioPicker, mock_app: AsyncMock, hass: HomeAssistant ) -> None: """Test formation strategy when no network settings are present.""" mock_app.load_network_info = MagicMock(side_effect=NetworkNotFormed()) @@ -1083,7 +1091,7 @@ async def test_strategy_no_network_settings( async def test_formation_strategy_form_new_network( - pick_radio, mock_app, hass: HomeAssistant + pick_radio: RadioPicker, mock_app: AsyncMock, hass: HomeAssistant ) -> None: """Test forming a new network.""" result, port = await pick_radio(RadioType.ezsp) @@ -1101,7 +1109,7 @@ async def test_formation_strategy_form_new_network( async def test_formation_strategy_form_initial_network( - pick_radio, mock_app, hass: HomeAssistant + pick_radio: RadioPicker, mock_app: AsyncMock, hass: HomeAssistant ) -> None: """Test forming a new network, with no previous settings on the radio.""" mock_app.load_network_info = AsyncMock(side_effect=NetworkNotFormed()) @@ -1122,7 +1130,7 @@ async def test_formation_strategy_form_initial_network( @patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) async def test_onboarding_auto_formation_new_hardware( - mock_app, hass: HomeAssistant + mock_app: AsyncMock, hass: HomeAssistant ) -> None: """Test auto network formation with new hardware during onboarding.""" mock_app.load_network_info = AsyncMock(side_effect=NetworkNotFormed()) @@ -1157,7 +1165,7 @@ async def test_onboarding_auto_formation_new_hardware( async def test_formation_strategy_reuse_settings( - pick_radio, mock_app, hass: HomeAssistant + pick_radio: RadioPicker, mock_app: AsyncMock, hass: HomeAssistant ) -> None: """Test reusing existing network settings.""" result, port = await pick_radio(RadioType.ezsp) @@ -1190,7 +1198,10 @@ def test_parse_uploaded_backup(process_mock) -> None: @patch("homeassistant.components.zha.radio_manager._allow_overwrite_ezsp_ieee") async def test_formation_strategy_restore_manual_backup_non_ezsp( - allow_overwrite_ieee_mock, pick_radio, mock_app, hass: HomeAssistant + allow_overwrite_ieee_mock, + pick_radio: RadioPicker, + mock_app: AsyncMock, + hass: HomeAssistant, ) -> None: """Test restoring a manual backup on non-EZSP coordinators.""" result, port = await pick_radio(RadioType.znp) @@ -1222,7 +1233,11 @@ async def test_formation_strategy_restore_manual_backup_non_ezsp( @patch("homeassistant.components.zha.radio_manager._allow_overwrite_ezsp_ieee") async def test_formation_strategy_restore_manual_backup_overwrite_ieee_ezsp( - allow_overwrite_ieee_mock, pick_radio, mock_app, backup, hass: HomeAssistant + allow_overwrite_ieee_mock, + pick_radio: RadioPicker, + mock_app: AsyncMock, + backup, + hass: HomeAssistant, ) -> None: """Test restoring a manual backup on EZSP coordinators (overwrite IEEE).""" result, port = await pick_radio(RadioType.ezsp) @@ -1262,7 +1277,10 @@ async def test_formation_strategy_restore_manual_backup_overwrite_ieee_ezsp( @patch("homeassistant.components.zha.radio_manager._allow_overwrite_ezsp_ieee") async def test_formation_strategy_restore_manual_backup_ezsp( - allow_overwrite_ieee_mock, pick_radio, mock_app, hass: HomeAssistant + allow_overwrite_ieee_mock, + pick_radio: RadioPicker, + mock_app: AsyncMock, + hass: HomeAssistant, ) -> None: """Test restoring a manual backup on EZSP coordinators (don't overwrite IEEE).""" result, port = await pick_radio(RadioType.ezsp) @@ -1303,7 +1321,7 @@ async def test_formation_strategy_restore_manual_backup_ezsp( async def test_formation_strategy_restore_manual_backup_invalid_upload( - pick_radio, mock_app, hass: HomeAssistant + pick_radio: RadioPicker, mock_app: AsyncMock, hass: HomeAssistant ) -> None: """Test restoring a manual backup but an invalid file is uploaded.""" result, port = await pick_radio(RadioType.ezsp) @@ -1355,7 +1373,7 @@ def test_format_backup_choice() -> None: ) @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) async def test_formation_strategy_restore_automatic_backup_ezsp( - pick_radio, mock_app, make_backup, hass: HomeAssistant + pick_radio: RadioPicker, mock_app: AsyncMock, make_backup, hass: HomeAssistant ) -> None: """Test restoring an automatic backup (EZSP radio).""" mock_app.backups.backups = [ @@ -1404,7 +1422,11 @@ async def test_formation_strategy_restore_automatic_backup_ezsp( @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) @pytest.mark.parametrize("is_advanced", [True, False]) async def test_formation_strategy_restore_automatic_backup_non_ezsp( - is_advanced, pick_radio, mock_app, make_backup, hass: HomeAssistant + is_advanced, + pick_radio: RadioPicker, + mock_app: AsyncMock, + make_backup, + hass: HomeAssistant, ) -> None: """Test restoring an automatic backup (non-EZSP radio).""" mock_app.backups.backups = [ @@ -1457,7 +1479,11 @@ async def test_formation_strategy_restore_automatic_backup_non_ezsp( @patch("homeassistant.components.zha.radio_manager._allow_overwrite_ezsp_ieee") async def test_ezsp_restore_without_settings_change_ieee( - allow_overwrite_ieee_mock, pick_radio, mock_app, backup, hass: HomeAssistant + allow_overwrite_ieee_mock, + pick_radio: RadioPicker, + mock_app: AsyncMock, + backup, + hass: HomeAssistant, ) -> None: """Test a manual backup on EZSP coordinators without settings (no IEEE write).""" # Fail to load settings From c65f84532994210e3257ab09b1b9a018603fdda1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:40:57 +0200 Subject: [PATCH 2314/2411] Improve type hints in wemo tests (#123923) * Improve type hints in wemo tests * typo --- tests/components/wemo/conftest.py | 34 ++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 1316c37b62b..64bd89f4793 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -1,13 +1,15 @@ """Fixtures for pywemo.""" +from collections.abc import Generator import contextlib -from unittest.mock import create_autospec, patch +from unittest.mock import MagicMock, create_autospec, patch import pytest import pywemo from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC from homeassistant.components.wemo.const import DOMAIN +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -22,13 +24,13 @@ MOCK_INSIGHT_STATE_THRESHOLD_POWER = 8.0 @pytest.fixture(name="pywemo_model") -def pywemo_model_fixture(): +def pywemo_model_fixture() -> str: """Fixture containing a pywemo class name used by pywemo_device_fixture.""" return "LightSwitch" @pytest.fixture(name="pywemo_registry", autouse=True) -async def async_pywemo_registry_fixture(): +def async_pywemo_registry_fixture() -> Generator[MagicMock]: """Fixture for SubscriptionRegistry instances.""" registry = create_autospec(pywemo.SubscriptionRegistry, instance=True) @@ -52,7 +54,9 @@ def pywemo_discovery_responder_fixture(): @contextlib.contextmanager -def create_pywemo_device(pywemo_registry, pywemo_model): +def create_pywemo_device( + pywemo_registry: MagicMock, pywemo_model: str +) -> pywemo.WeMoDevice: """Create a WeMoDevice instance.""" cls = getattr(pywemo, pywemo_model) device = create_autospec(cls, instance=True) @@ -90,14 +94,18 @@ def create_pywemo_device(pywemo_registry, pywemo_model): @pytest.fixture(name="pywemo_device") -def pywemo_device_fixture(pywemo_registry, pywemo_model): +def pywemo_device_fixture( + pywemo_registry: MagicMock, pywemo_model: str +) -> Generator[pywemo.WeMoDevice]: """Fixture for WeMoDevice instances.""" with create_pywemo_device(pywemo_registry, pywemo_model) as pywemo_device: yield pywemo_device @pytest.fixture(name="pywemo_dli_device") -def pywemo_dli_device_fixture(pywemo_registry, pywemo_model): +def pywemo_dli_device_fixture( + pywemo_registry: MagicMock, pywemo_model: str +) -> Generator[pywemo.WeMoDevice]: """Fixture for Digital Loggers emulated instances.""" with create_pywemo_device(pywemo_registry, pywemo_model) as pywemo_dli_device: pywemo_dli_device.model_name = "DLI emulated Belkin Socket" @@ -106,12 +114,14 @@ def pywemo_dli_device_fixture(pywemo_registry, pywemo_model): @pytest.fixture(name="wemo_entity_suffix") -def wemo_entity_suffix_fixture(): +def wemo_entity_suffix_fixture() -> str: """Fixture to select a specific entity for wemo_entity.""" return "" -async def async_create_wemo_entity(hass, pywemo_device, wemo_entity_suffix): +async def async_create_wemo_entity( + hass: HomeAssistant, pywemo_device: pywemo.WeMoDevice, wemo_entity_suffix: str +) -> er.RegistryEntry | None: """Create a hass entity for a wemo device.""" assert await async_setup_component( hass, @@ -134,12 +144,16 @@ async def async_create_wemo_entity(hass, pywemo_device, wemo_entity_suffix): @pytest.fixture(name="wemo_entity") -async def async_wemo_entity_fixture(hass, pywemo_device, wemo_entity_suffix): +async def async_wemo_entity_fixture( + hass: HomeAssistant, pywemo_device: pywemo.WeMoDevice, wemo_entity_suffix: str +) -> er.RegistryEntry | None: """Fixture for a Wemo entity in hass.""" return await async_create_wemo_entity(hass, pywemo_device, wemo_entity_suffix) @pytest.fixture(name="wemo_dli_entity") -async def async_wemo_dli_entity_fixture(hass, pywemo_dli_device, wemo_entity_suffix): +async def async_wemo_dli_entity_fixture( + hass: HomeAssistant, pywemo_dli_device: pywemo.WeMoDevice, wemo_entity_suffix: str +) -> er.RegistryEntry | None: """Fixture for a Wemo entity in hass.""" return await async_create_wemo_entity(hass, pywemo_dli_device, wemo_entity_suffix) From 1227cd8693da0a216942e878ebb642f6e9338602 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:41:15 +0200 Subject: [PATCH 2315/2411] Improve type hints in zerproc tests (#123925) --- tests/components/zerproc/test_light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/zerproc/test_light.py b/tests/components/zerproc/test_light.py index c47f960b182..6e00cfbde4c 100644 --- a/tests/components/zerproc/test_light.py +++ b/tests/components/zerproc/test_light.py @@ -35,13 +35,13 @@ from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture -async def mock_entry(hass): +async def mock_entry() -> MockConfigEntry: """Create a mock light entity.""" return MockConfigEntry(domain=DOMAIN) @pytest.fixture -async def mock_light(hass, mock_entry): +async def mock_light(hass: HomeAssistant, mock_entry: MockConfigEntry) -> MagicMock: """Create a mock light entity.""" mock_entry.add_to_hass(hass) From ff4dac8f3a336f57927b448bef0f1dad5a964b07 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:42:53 +0200 Subject: [PATCH 2316/2411] Improve type hints in watttime tests (#123921) --- tests/components/watttime/conftest.py | 38 ++++++++++++++++++--------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/tests/components/watttime/conftest.py b/tests/components/watttime/conftest.py index 0b7403d45fc..650d07b36a1 100644 --- a/tests/components/watttime/conftest.py +++ b/tests/components/watttime/conftest.py @@ -1,6 +1,7 @@ """Define test fixtures for WattTime.""" -import json +from collections.abc import AsyncGenerator +from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest @@ -20,13 +21,17 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util.json import JsonObjectType -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, load_json_object_fixture @pytest.fixture(name="client") -def client_fixture(get_grid_region, data_realtime_emissions): +def client_fixture( + get_grid_region: AsyncMock, data_realtime_emissions: JsonObjectType +) -> Mock: """Define an aiowatttime client.""" client = Mock() client.emissions.async_get_grid_region = get_grid_region @@ -37,7 +42,7 @@ def client_fixture(get_grid_region, data_realtime_emissions): @pytest.fixture(name="config_auth") -def config_auth_fixture(hass): +def config_auth_fixture() -> dict[str, Any]: """Define an auth config entry data fixture.""" return { CONF_USERNAME: "user", @@ -46,7 +51,7 @@ def config_auth_fixture(hass): @pytest.fixture(name="config_coordinates") -def config_coordinates_fixture(hass): +def config_coordinates_fixture() -> dict[str, Any]: """Define a coordinates config entry data fixture.""" return { CONF_LATITUDE: 32.87336, @@ -55,7 +60,7 @@ def config_coordinates_fixture(hass): @pytest.fixture(name="config_location_type") -def config_location_type_fixture(hass): +def config_location_type_fixture() -> dict[str, Any]: """Define a location type config entry data fixture.""" return { CONF_LOCATION_TYPE: LOCATION_TYPE_COORDINATES, @@ -63,7 +68,9 @@ def config_location_type_fixture(hass): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config_auth, config_coordinates): +def config_entry_fixture( + hass: HomeAssistant, config_auth: dict[str, Any], config_coordinates: dict[str, Any] +) -> MockConfigEntry: """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, @@ -82,25 +89,30 @@ def config_entry_fixture(hass, config_auth, config_coordinates): @pytest.fixture(name="data_grid_region", scope="package") -def data_grid_region_fixture(): +def data_grid_region_fixture() -> JsonObjectType: """Define grid region data.""" - return json.loads(load_fixture("grid_region_data.json", "watttime")) + return load_json_object_fixture("grid_region_data.json", "watttime") @pytest.fixture(name="data_realtime_emissions", scope="package") -def data_realtime_emissions_fixture(): +def data_realtime_emissions_fixture() -> JsonObjectType: """Define realtime emissions data.""" - return json.loads(load_fixture("realtime_emissions_data.json", "watttime")) + return load_json_object_fixture("realtime_emissions_data.json", "watttime") @pytest.fixture(name="get_grid_region") -def get_grid_region_fixture(data_grid_region): +def get_grid_region_fixture(data_grid_region: JsonObjectType) -> AsyncMock: """Define an aiowatttime method to get grid region data.""" return AsyncMock(return_value=data_grid_region) @pytest.fixture(name="setup_watttime") -async def setup_watttime_fixture(hass, client, config_auth, config_coordinates): +async def setup_watttime_fixture( + hass: HomeAssistant, + client: Mock, + config_auth: dict[str, Any], + config_coordinates: dict[str, Any], +) -> AsyncGenerator[None]: """Define a fixture to set up WattTime.""" with ( patch( From 04598c6fb12d0f20f1b3774924a80f3b41d7f1ef Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 14 Aug 2024 15:45:08 +0200 Subject: [PATCH 2317/2411] Use more snapshot in UniFi sensor tests (#122875) * Use more snapshot in UniFi sensor tests * Fix comment --- .../unifi/snapshots/test_sensor.ambr | 2121 ++++++++++++++++- tests/components/unifi/test_sensor.py | 452 ++-- 2 files changed, 2195 insertions(+), 378 deletions(-) diff --git a/tests/components/unifi/snapshots/test_sensor.ambr b/tests/components/unifi/snapshots/test_sensor.ambr index 496c386e2cc..3053f69d616 100644 --- a/tests/components/unifi/snapshots/test_sensor.ambr +++ b/tests/components/unifi/snapshots/test_sensor.ambr @@ -1,127 +1,2080 @@ # serializer version: 1 -# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0] - 'rx-00:00:00:00:00:01' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.device_clients-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.device_clients', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Clients', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'device_clients-20:00:00:00:01:01', + 'unit_of_measurement': None, + }) # --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0].1 - +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.device_clients-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Clients', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.device_clients', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) # --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0].2 - 'data_rate' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.device_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Disconnected', + 'Connected', + 'Pending', + 'Firmware Mismatch', + 'Upgrading', + 'Provisioning', + 'Heartbeat Missed', + 'Adopting', + 'Deleting', + 'Inform Error', + 'Adoption Failed', + 'Isolated', + 'Unknown', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.device_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'State', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'device_state-20:00:00:00:01:01', + 'unit_of_measurement': None, + }) # --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0].3 - 'Wired client RX' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.device_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Device State', + 'options': list([ + 'Disconnected', + 'Connected', + 'Pending', + 'Firmware Mismatch', + 'Upgrading', + 'Provisioning', + 'Heartbeat Missed', + 'Adopting', + 'Deleting', + 'Inform Error', + 'Adoption Failed', + 'Isolated', + 'Unknown', + ]), + }), + 'context': , + 'entity_id': 'sensor.device_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Connected', + }) # --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0].4 - +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.device_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.device_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'device_temperature-20:00:00:00:01:01', + 'unit_of_measurement': , + }) # --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0].5 - +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.device_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Device Temperature', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30', + }) # --- -# name: test_sensor_sources[client_payload0-sensor.wired_client_rx-rx--config_entry_options0].6 - '1234.0' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.device_uptime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.device_uptime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Uptime', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'device_uptime-20:00:00:00:01:01', + 'unit_of_measurement': None, + }) # --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0] - 'tx-00:00:00:00:00:01' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.device_uptime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Device Uptime', + }), + 'context': , + 'entity_id': 'sensor.device_uptime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-01T01:00:00+00:00', + }) # --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0].1 - +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_ac_power_budget-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_ac_power_budget', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC Power Budget', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ac_power_budget-01:02:03:04:05:ff', + 'unit_of_measurement': , + }) # --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0].2 - 'data_rate' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_ac_power_budget-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Dummy USP-PDU-Pro AC Power Budget', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_ac_power_budget', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1875.000', + }) # --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0].3 - 'Wired client TX' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_ac_power_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_ac_power_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC Power Consumption', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ac_power_conumption-01:02:03:04:05:ff', + 'unit_of_measurement': , + }) # --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0].4 - +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_ac_power_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Dummy USP-PDU-Pro AC Power Consumption', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_ac_power_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '201.683', + }) # --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0].5 - +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_clients-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_clients', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Clients', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'device_clients-01:02:03:04:05:ff', + 'unit_of_measurement': None, + }) # --- -# name: test_sensor_sources[client_payload1-sensor.wired_client_tx-tx--config_entry_options0].6 - '5678.0' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_clients-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dummy USP-PDU-Pro Clients', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_clients', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) # --- -# name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0] - 'uptime-00:00:00:00:00:01' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_cpu_utilization-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_cpu_utilization', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CPU utilization', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'cpu_utilization-01:02:03:04:05:ff', + 'unit_of_measurement': '%', + }) # --- -# name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0].1 - +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_cpu_utilization-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dummy USP-PDU-Pro CPU utilization', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_cpu_utilization', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.4', + }) # --- -# name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0].2 - 'timestamp' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_memory_utilization-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_memory_utilization', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Memory utilization', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'memory_utilization-01:02:03:04:05:ff', + 'unit_of_measurement': '%', + }) # --- -# name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0].3 - 'Wired client Uptime' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_memory_utilization-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dummy USP-PDU-Pro Memory utilization', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_memory_utilization', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.9', + }) # --- -# name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0].4 - None +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_outlet_2_outlet_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_outlet_2_outlet_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outlet 2 Outlet Power', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet_power-01:02:03:04:05:ff_2', + 'unit_of_measurement': , + }) # --- -# name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0].5 - None +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_outlet_2_outlet_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Dummy USP-PDU-Pro Outlet 2 Outlet Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_outlet_2_outlet_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '73.827', + }) # --- -# name: test_sensor_sources[client_payload2-sensor.wired_client_uptime-uptime--config_entry_options0].6 - '2020-09-14T14:41:45+00:00' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Disconnected', + 'Connected', + 'Pending', + 'Firmware Mismatch', + 'Upgrading', + 'Provisioning', + 'Heartbeat Missed', + 'Adopting', + 'Deleting', + 'Inform Error', + 'Adoption Failed', + 'Isolated', + 'Unknown', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'State', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'device_state-01:02:03:04:05:ff', + 'unit_of_measurement': None, + }) # --- -# name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0] - 'rx-00:00:00:00:00:01' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Dummy USP-PDU-Pro State', + 'options': list([ + 'Disconnected', + 'Connected', + 'Pending', + 'Firmware Mismatch', + 'Upgrading', + 'Provisioning', + 'Heartbeat Missed', + 'Adopting', + 'Deleting', + 'Inform Error', + 'Adoption Failed', + 'Isolated', + 'Unknown', + ]), + }), + 'context': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Connected', + }) # --- -# name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0].1 - +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_uptime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_uptime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Uptime', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'device_uptime-01:02:03:04:05:ff', + 'unit_of_measurement': None, + }) # --- -# name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0].2 - 'data_rate' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_uptime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Dummy USP-PDU-Pro Uptime', + }), + 'context': , + 'entity_id': 'sensor.dummy_usp_pdu_pro_uptime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2020-12-18T05:36:58+00:00', + }) # --- -# name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0].3 - 'Wireless client RX' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_clients-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_clients', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Clients', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'device_clients-10:00:00:00:01:01', + 'unit_of_measurement': None, + }) # --- -# name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0].4 - +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_clients-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'mock-name Clients', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_clients', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) # --- -# name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0].5 - +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_cloudflare_wan2_latency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_cloudflare_wan2_latency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cloudflare WAN2 latency', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'cloudflare_wan2_latency-10:00:00:00:01:01', + 'unit_of_measurement': , + }) # --- -# name: test_sensor_sources[client_payload3-sensor.wireless_client_rx-rx--config_entry_options0].6 - '2345.0' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_cloudflare_wan2_latency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'mock-name Cloudflare WAN2 latency', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_cloudflare_wan2_latency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) # --- -# name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0] - 'tx-00:00:00:00:00:01' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_cloudflare_wan_latency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_cloudflare_wan_latency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cloudflare WAN latency', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'cloudflare_wan_latency-10:00:00:00:01:01', + 'unit_of_measurement': , + }) # --- -# name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0].1 - +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_cloudflare_wan_latency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'mock-name Cloudflare WAN latency', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_cloudflare_wan_latency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30', + }) # --- -# name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0].2 - 'data_rate' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_google_wan2_latency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_google_wan2_latency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Google WAN2 latency', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'google_wan2_latency-10:00:00:00:01:01', + 'unit_of_measurement': , + }) # --- -# name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0].3 - 'Wireless client TX' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_google_wan2_latency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'mock-name Google WAN2 latency', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_google_wan2_latency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) # --- -# name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0].4 - +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_google_wan_latency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_google_wan_latency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Google WAN latency', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'google_wan_latency-10:00:00:00:01:01', + 'unit_of_measurement': , + }) # --- -# name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0].5 - +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_google_wan_latency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'mock-name Google WAN latency', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_google_wan_latency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '53', + }) # --- -# name: test_sensor_sources[client_payload4-sensor.wireless_client_tx-tx--config_entry_options0].6 - '6789.0' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_microsoft_wan2_latency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_microsoft_wan2_latency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Microsoft WAN2 latency', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'microsoft_wan2_latency-10:00:00:00:01:01', + 'unit_of_measurement': , + }) # --- -# name: test_sensor_sources[client_payload5-sensor.wireless_client_uptime-uptime--config_entry_options0] - 'uptime-00:00:00:00:00:01' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_microsoft_wan2_latency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'mock-name Microsoft WAN2 latency', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_microsoft_wan2_latency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) # --- -# name: test_sensor_sources[client_payload5-sensor.wireless_client_uptime-uptime--config_entry_options0].1 - +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_microsoft_wan_latency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_microsoft_wan_latency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Microsoft WAN latency', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'microsoft_wan_latency-10:00:00:00:01:01', + 'unit_of_measurement': , + }) # --- -# name: test_sensor_sources[client_payload5-sensor.wireless_client_uptime-uptime--config_entry_options0].2 - 'timestamp' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_microsoft_wan_latency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'mock-name Microsoft WAN latency', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_microsoft_wan_latency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '56', + }) # --- -# name: test_sensor_sources[client_payload5-sensor.wireless_client_uptime-uptime--config_entry_options0].3 - 'Wireless client Uptime' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_1_poe_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_port_1_poe_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Port 1 PoE Power', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe_power-10:00:00:00:01:01_1', + 'unit_of_measurement': , + }) # --- -# name: test_sensor_sources[client_payload5-sensor.wireless_client_uptime-uptime--config_entry_options0].4 - None +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_1_poe_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'mock-name Port 1 PoE Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_port_1_poe_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.56', + }) # --- -# name: test_sensor_sources[client_payload5-sensor.wireless_client_uptime-uptime--config_entry_options0].5 - None +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_1_rx-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_port_1_rx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:download', + 'original_name': 'Port 1 RX', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'port_rx-10:00:00:00:01:01_1', + 'unit_of_measurement': , + }) # --- -# name: test_sensor_sources[client_payload5-sensor.wireless_client_uptime-uptime--config_entry_options0].6 - '2021-01-01T01:00:00+00:00' +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_1_rx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'mock-name Port 1 RX', + 'icon': 'mdi:download', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_port_1_rx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.00000', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_1_tx-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_port_1_tx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:upload', + 'original_name': 'Port 1 TX', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'port_tx-10:00:00:00:01:01_1', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_1_tx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'mock-name Port 1 TX', + 'icon': 'mdi:upload', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_port_1_tx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.00000', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_2_poe_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_port_2_poe_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Port 2 PoE Power', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe_power-10:00:00:00:01:01_2', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_2_poe_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'mock-name Port 2 PoE Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_port_2_poe_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.56', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_2_rx-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_port_2_rx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:download', + 'original_name': 'Port 2 RX', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'port_rx-10:00:00:00:01:01_2', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_2_rx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'mock-name Port 2 RX', + 'icon': 'mdi:download', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_port_2_rx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.00000', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_2_tx-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_port_2_tx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:upload', + 'original_name': 'Port 2 TX', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'port_tx-10:00:00:00:01:01_2', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_2_tx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'mock-name Port 2 TX', + 'icon': 'mdi:upload', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_port_2_tx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.00000', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_3_rx-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_port_3_rx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:download', + 'original_name': 'Port 3 RX', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'port_rx-10:00:00:00:01:01_3', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_3_rx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'mock-name Port 3 RX', + 'icon': 'mdi:download', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_port_3_rx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.00000', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_3_tx-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_port_3_tx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:upload', + 'original_name': 'Port 3 TX', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'port_tx-10:00:00:00:01:01_3', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_3_tx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'mock-name Port 3 TX', + 'icon': 'mdi:upload', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_port_3_tx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.00000', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_4_poe_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_port_4_poe_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Port 4 PoE Power', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe_power-10:00:00:00:01:01_4', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_4_poe_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'mock-name Port 4 PoE Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_port_4_poe_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.00', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_4_rx-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_port_4_rx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:download', + 'original_name': 'Port 4 RX', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'port_rx-10:00:00:00:01:01_4', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_4_rx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'mock-name Port 4 RX', + 'icon': 'mdi:download', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_port_4_rx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.00000', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_4_tx-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_port_4_tx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:upload', + 'original_name': 'Port 4 TX', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'port_tx-10:00:00:00:01:01_4', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_4_tx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'mock-name Port 4 TX', + 'icon': 'mdi:upload', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_name_port_4_tx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.00000', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Disconnected', + 'Connected', + 'Pending', + 'Firmware Mismatch', + 'Upgrading', + 'Provisioning', + 'Heartbeat Missed', + 'Adopting', + 'Deleting', + 'Inform Error', + 'Adoption Failed', + 'Isolated', + 'Unknown', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'State', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'device_state-10:00:00:00:01:01', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'mock-name State', + 'options': list([ + 'Disconnected', + 'Connected', + 'Pending', + 'Firmware Mismatch', + 'Upgrading', + 'Provisioning', + 'Heartbeat Missed', + 'Adopting', + 'Deleting', + 'Inform Error', + 'Adoption Failed', + 'Isolated', + 'Unknown', + ]), + }), + 'context': , + 'entity_id': 'sensor.mock_name_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Connected', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_uptime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_name_uptime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Uptime', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'device_uptime-10:00:00:00:01:01', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_uptime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'mock-name Uptime', + }), + 'context': , + 'entity_id': 'sensor.mock_name_uptime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.ssid_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.ssid_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'wlan_clients-012345678910111213141516', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.ssid_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SSID 1', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.ssid_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wired_client_rx-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wired_client_rx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:upload', + 'original_name': 'RX', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'rx-00:00:00:00:00:01', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wired_client_rx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'Wired client RX', + 'icon': 'mdi:upload', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wired_client_rx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1234.0', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wired_client_tx-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wired_client_tx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:download', + 'original_name': 'TX', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tx-00:00:00:00:00:01', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wired_client_tx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'Wired client TX', + 'icon': 'mdi:download', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wired_client_tx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5678.0', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wired_client_uptime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wired_client_uptime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Uptime', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'uptime-00:00:00:00:00:01', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wired_client_uptime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Wired client Uptime', + }), + 'context': , + 'entity_id': 'sensor.wired_client_uptime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2020-09-14T14:41:45+00:00', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wireless_client_rx-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wireless_client_rx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:upload', + 'original_name': 'RX', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'rx-00:00:00:00:00:02', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wireless_client_rx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'Wireless client RX', + 'icon': 'mdi:upload', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wireless_client_rx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2345.0', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wireless_client_tx-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wireless_client_tx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:download', + 'original_name': 'TX', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tx-00:00:00:00:00:02', + 'unit_of_measurement': , + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wireless_client_tx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': 'Wireless client TX', + 'icon': 'mdi:download', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wireless_client_tx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6789.0', + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wireless_client_uptime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wireless_client_uptime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Uptime', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'uptime-00:00:00:00:00:02', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wireless_client_uptime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Wireless client Uptime', + }), + 'context': , + 'entity_id': 'sensor.wireless_client_uptime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-01T01:00:00+00:00', + }) # --- diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 5af4b297847..cc51c31fc8b 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -10,14 +10,12 @@ from aiounifi.models.device import DeviceState from aiounifi.models.message import MessageKey from freezegun.api import FrozenDateTimeFactory, freeze_time import pytest -from syrupy.assertion import SnapshotAssertion +from syrupy import SnapshotAssertion from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, SCAN_INTERVAL, SensorDeviceClass, - SensorStateClass, ) from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, @@ -29,13 +27,7 @@ from homeassistant.components.unifi.const import ( DEVICE_STATES, ) from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_FRIENDLY_NAME, - ATTR_UNIT_OF_MEASUREMENT, - STATE_UNAVAILABLE, - EntityCategory, -) +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryDisabler @@ -47,7 +39,26 @@ from .conftest import ( WebsocketStateManager, ) -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + +WIRED_CLIENT = { + "hostname": "Wired client", + "is_wired": True, + "mac": "00:00:00:00:00:01", + "oui": "Producer", + "wired-rx_bytes-r": 1234000000, + "wired-tx_bytes-r": 5678000000, + "uptime": 1600094505, +} +WIRELESS_CLIENT = { + "is_wired": False, + "mac": "00:00:00:00:00:02", + "name": "Wireless client", + "oui": "Producer", + "rx_bytes-r": 2345000000.0, + "tx_bytes-r": 6789000000.0, + "uptime": 60, +} DEVICE_1 = { "board_rev": 2, @@ -321,6 +332,114 @@ PDU_OUTLETS_UPDATE_DATA = [ ] +@pytest.mark.parametrize( + "config_entry_options", + [ + { + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, + } + ], +) +@pytest.mark.parametrize("client_payload", [[WIRED_CLIENT, WIRELESS_CLIENT]]) +@pytest.mark.parametrize( + "device_payload", + [ + [ + DEVICE_1, + PDU_DEVICE_1, + { # Temperature + "board_rev": 3, + "device_id": "mock-id", + "general_temperature": 30, + "has_fan": True, + "has_temperature": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "20:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "uptime": 60, + "version": "4.0.42.10433", + }, + { # Latency monitors + "board_rev": 2, + "device_id": "mock-id", + "ip": "10.0.1.1", + "mac": "10:00:00:00:01:01", + "last_seen": 1562600145, + "model": "US16P150", + "name": "mock-name", + "port_overrides": [], + "uptime_stats": { + "WAN": { + "availability": 100.0, + "latency_average": 39, + "monitors": [ + { + "availability": 100.0, + "latency_average": 56, + "target": "www.microsoft.com", + "type": "icmp", + }, + { + "availability": 100.0, + "latency_average": 53, + "target": "google.com", + "type": "icmp", + }, + { + "availability": 100.0, + "latency_average": 30, + "target": "1.1.1.1", + "type": "icmp", + }, + ], + }, + "WAN2": { + "monitors": [ + { + "availability": 0.0, + "target": "www.microsoft.com", + "type": "icmp", + }, + { + "availability": 0.0, + "target": "google.com", + "type": "icmp", + }, + {"availability": 0.0, "target": "1.1.1.1", "type": "icmp"}, + ], + }, + }, + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + }, + ] + ], +) +@pytest.mark.parametrize("wlan_payload", [[WLAN]]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.freeze_time("2021-01-01 01:01:00") +async def test_entity_and_device_data( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory, + snapshot: SnapshotAssertion, +) -> None: + """Validate entity and device data.""" + with patch("homeassistant.components.unifi.PLATFORMS", [Platform.SENSOR]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + @pytest.mark.parametrize( "config_entry_options", [{CONF_ALLOW_BANDWIDTH_SENSORS: True, CONF_ALLOW_UPTIME_SENSORS: True}], @@ -342,29 +461,7 @@ async def test_no_clients(hass: HomeAssistant) -> None: } ], ) -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "hostname": "Wired client", - "is_wired": True, - "mac": "00:00:00:00:00:01", - "oui": "Producer", - "wired-rx_bytes-r": 1234000000, - "wired-tx_bytes-r": 5678000000, - }, - { - "is_wired": False, - "mac": "00:00:00:00:00:02", - "name": "Wireless client", - "oui": "Producer", - "rx_bytes-r": 2345000000.0, - "tx_bytes-r": 6789000000.0, - }, - ] - ], -) +@pytest.mark.parametrize("client_payload", [[WIRED_CLIENT, WIRELESS_CLIENT]]) async def test_bandwidth_sensors( hass: HomeAssistant, mock_websocket_message: WebsocketMessageMock, @@ -373,33 +470,8 @@ async def test_bandwidth_sensors( client_payload: list[dict[str, Any]], ) -> None: """Verify that bandwidth sensors are working as expected.""" - assert len(hass.states.async_all()) == 5 - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 - - # Verify sensor attributes and state - - wrx_sensor = hass.states.get("sensor.wired_client_rx") - assert wrx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE - assert wrx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert wrx_sensor.state == "1234.0" - - wtx_sensor = hass.states.get("sensor.wired_client_tx") - assert wtx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE - assert wtx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert wtx_sensor.state == "5678.0" - - wlrx_sensor = hass.states.get("sensor.wireless_client_rx") - assert wlrx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE - assert wlrx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert wlrx_sensor.state == "2345.0" - - wltx_sensor = hass.states.get("sensor.wireless_client_tx") - assert wltx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE - assert wltx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert wltx_sensor.state == "6789.0" - # Verify state update - wireless_client = client_payload[1] + wireless_client = deepcopy(client_payload[1]) wireless_client["rx_bytes-r"] = 3456000000 wireless_client["tx_bytes-r"] = 7891000000 @@ -468,31 +540,7 @@ async def test_bandwidth_sensors( "config_entry_options", [{CONF_ALLOW_BANDWIDTH_SENSORS: True, CONF_ALLOW_UPTIME_SENSORS: True}], ) -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "hostname": "Wired client", - "is_wired": True, - "mac": "00:00:00:00:00:01", - "oui": "Producer", - "wired-rx_bytes": 1234000000, - "wired-tx_bytes": 5678000000, - "uptime": 1600094505, - }, - { - "is_wired": False, - "mac": "00:00:00:00:00:02", - "name": "Wireless client", - "oui": "Producer", - "rx_bytes": 2345000000, - "tx_bytes": 6789000000, - "uptime": 60, - }, - ] - ], -) +@pytest.mark.parametrize("client_payload", [[WIRED_CLIENT, WIRELESS_CLIENT]]) @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_remove_sensors( @@ -535,7 +583,6 @@ async def test_poe_port_switches( ent_reg_entry = entity_registry.async_get("sensor.mock_name_port_1_poe_power") assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION - assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC # Enable entity entity_registry.async_update_entity( @@ -600,7 +647,6 @@ async def test_poe_port_switches( @pytest.mark.parametrize("wlan_payload", [[WLAN]]) async def test_wlan_client_sensors( hass: HomeAssistant, - entity_registry: er.EntityRegistry, config_entry_factory: ConfigEntryFactoryType, mock_websocket_message: WebsocketMessageMock, mock_websocket_state: WebsocketStateManager, @@ -633,14 +679,8 @@ async def test_wlan_client_sensors( assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 - ent_reg_entry = entity_registry.async_get("sensor.ssid_1") - assert ent_reg_entry.unique_id == "wlan_clients-012345678910111213141516" - assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC - # Validate state object - ssid_1 = hass.states.get("sensor.ssid_1") - assert ssid_1 is not None - assert ssid_1.state == "1" + assert hass.states.get("sensor.ssid_1").state == "1" # Verify state update - increasing number wireless_client_1 = client_payload[0] @@ -709,7 +749,6 @@ async def test_wlan_client_sensors( @pytest.mark.parametrize( ( "entity_id", - "expected_unique_id", "expected_value", "changed_data", "expected_update_value", @@ -717,21 +756,18 @@ async def test_wlan_client_sensors( [ ( "dummy_usp_pdu_pro_outlet_2_outlet_power", - "outlet_power-01:02:03:04:05:ff_2", "73.827", {"outlet_table": PDU_OUTLETS_UPDATE_DATA}, "123.45", ), ( "dummy_usp_pdu_pro_ac_power_budget", - "ac_power_budget-01:02:03:04:05:ff", "1875.000", None, None, ), ( "dummy_usp_pdu_pro_ac_power_consumption", - "ac_power_conumption-01:02:03:04:05:ff", "201.683", {"outlet_ac_power_consumption": "456.78"}, "456.78", @@ -742,26 +778,18 @@ async def test_wlan_client_sensors( @pytest.mark.usefixtures("config_entry_setup") async def test_outlet_power_readings( hass: HomeAssistant, - entity_registry: er.EntityRegistry, mock_websocket_message: WebsocketMessageMock, device_payload: list[dict[str, Any]], entity_id: str, - expected_unique_id: str, - expected_value: Any, - changed_data: dict | None, - expected_update_value: Any, + expected_value: str, + changed_data: dict[str, Any] | None, + expected_update_value: str | None, ) -> None: """Test the outlet power reporting on PDU devices.""" assert len(hass.states.async_all()) == 13 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 7 - ent_reg_entry = entity_registry.async_get(f"sensor.{entity_id}") - assert ent_reg_entry.unique_id == expected_unique_id - assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC - - sensor_data = hass.states.get(f"sensor.{entity_id}") - assert sensor_data.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert sensor_data.state == expected_value + assert hass.states.get(f"sensor.{entity_id}").state == expected_value if changed_data is not None: updated_device_data = deepcopy(device_payload[0]) @@ -770,8 +798,7 @@ async def test_outlet_power_readings( mock_websocket_message(message=MessageKey.DEVICE, data=updated_device_data) await hass.async_block_till_done() - sensor_data = hass.states.get(f"sensor.{entity_id}") - assert sensor_data.state == expected_update_value + assert hass.states.get(f"sensor.{entity_id}").state == expected_update_value @pytest.mark.parametrize( @@ -804,17 +831,12 @@ async def test_outlet_power_readings( @pytest.mark.usefixtures("config_entry_setup") async def test_device_temperature( hass: HomeAssistant, - entity_registry: er.EntityRegistry, mock_websocket_message: WebsocketMessageMock, device_payload: list[dict[str, Any]], ) -> None: """Verify that temperature sensors are working as expected.""" assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 assert hass.states.get("sensor.device_temperature").state == "30" - assert ( - entity_registry.async_get("sensor.device_temperature").entity_category - is EntityCategory.DIAGNOSTIC - ) # Verify new event change temperature device = device_payload[0] @@ -859,10 +881,6 @@ async def test_device_state( ) -> None: """Verify that state sensors are working as expected.""" assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 - assert ( - entity_registry.async_get("sensor.device_state").entity_category - is EntityCategory.DIAGNOSTIC - ) device = device_payload[0] for i in list(map(int, DeviceState)): @@ -890,7 +908,6 @@ async def test_device_state( @pytest.mark.usefixtures("config_entry_setup") async def test_device_system_stats( hass: HomeAssistant, - entity_registry: er.EntityRegistry, mock_websocket_message: WebsocketMessageMock, device_payload: list[dict[str, Any]], ) -> None: @@ -901,16 +918,6 @@ async def test_device_system_stats( assert hass.states.get("sensor.device_cpu_utilization").state == "5.8" assert hass.states.get("sensor.device_memory_utilization").state == "31.1" - assert ( - entity_registry.async_get("sensor.device_cpu_utilization").entity_category - is EntityCategory.DIAGNOSTIC - ) - - assert ( - entity_registry.async_get("sensor.device_memory_utilization").entity_category - is EntityCategory.DIAGNOSTIC - ) - # Verify new event change system-stats device = device_payload[0] device["system-stats"] = {"cpu": 7.7, "mem": 33.3, "uptime": 7316} @@ -997,11 +1004,9 @@ async def test_bandwidth_port_sensors( p1rx_reg_entry = entity_registry.async_get("sensor.mock_name_port_1_rx") assert p1rx_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION - assert p1rx_reg_entry.entity_category is EntityCategory.DIAGNOSTIC p1tx_reg_entry = entity_registry.async_get("sensor.mock_name_port_1_tx") assert p1tx_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION - assert p1tx_reg_entry.entity_category is EntityCategory.DIAGNOSTIC # Enable entity entity_registry.async_update_entity( @@ -1028,26 +1033,11 @@ async def test_bandwidth_port_sensors( assert len(hass.states.async_all()) == 9 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 - # Verify sensor attributes and state - p1rx_sensor = hass.states.get("sensor.mock_name_port_1_rx") - assert p1rx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE - assert p1rx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert p1rx_sensor.state == "0.00921" - - p1tx_sensor = hass.states.get("sensor.mock_name_port_1_tx") - assert p1tx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE - assert p1tx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert p1tx_sensor.state == "0.04089" - - p2rx_sensor = hass.states.get("sensor.mock_name_port_2_rx") - assert p2rx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE - assert p2rx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert p2rx_sensor.state == "0.01229" - - p2tx_sensor = hass.states.get("sensor.mock_name_port_2_tx") - assert p2tx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE - assert p2tx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert p2tx_sensor.state == "0.02892" + # Verify sensor state + assert hass.states.get("sensor.mock_name_port_1_rx").state == "0.00921" + assert hass.states.get("sensor.mock_name_port_1_tx").state == "0.04089" + assert hass.states.get("sensor.mock_name_port_2_rx").state == "0.01229" + assert hass.states.get("sensor.mock_name_port_2_tx").state == "0.02892" # Verify state update device_1 = device_payload[0] @@ -1141,13 +1131,9 @@ async def test_device_client_sensors( ent_reg_entry = entity_registry.async_get("sensor.wired_device_clients") assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION - assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC - assert ent_reg_entry.unique_id == "device_clients-01:00:00:00:00:00" ent_reg_entry = entity_registry.async_get("sensor.wireless_device_clients") assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION - assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC - assert ent_reg_entry.unique_id == "device_clients-02:00:00:00:00:00" # Enable entity entity_registry.async_update_entity( @@ -1184,72 +1170,6 @@ async def test_device_client_sensors( assert hass.states.get("sensor.wireless_device_clients").state == "0" -WIRED_CLIENT = { - "hostname": "Wired client", - "is_wired": True, - "mac": "00:00:00:00:00:01", - "oui": "Producer", - "wired-rx_bytes-r": 1234000000, - "wired-tx_bytes-r": 5678000000, - "uptime": 1600094505, -} -WIRELESS_CLIENT = { - "is_wired": False, - "mac": "00:00:00:00:00:01", - "name": "Wireless client", - "oui": "Producer", - "rx_bytes-r": 2345000000.0, - "tx_bytes-r": 6789000000.0, - "uptime": 60, -} - - -@pytest.mark.parametrize( - "config_entry_options", - [ - { - CONF_ALLOW_BANDWIDTH_SENSORS: True, - CONF_ALLOW_UPTIME_SENSORS: True, - CONF_TRACK_CLIENTS: False, - CONF_TRACK_DEVICES: False, - } - ], -) -@pytest.mark.parametrize( - ("client_payload", "entity_id", "unique_id_prefix"), - [ - ([WIRED_CLIENT], "sensor.wired_client_rx", "rx-"), - ([WIRED_CLIENT], "sensor.wired_client_tx", "tx-"), - ([WIRED_CLIENT], "sensor.wired_client_uptime", "uptime-"), - ([WIRELESS_CLIENT], "sensor.wireless_client_rx", "rx-"), - ([WIRELESS_CLIENT], "sensor.wireless_client_tx", "tx-"), - ([WIRELESS_CLIENT], "sensor.wireless_client_uptime", "uptime-"), - ], -) -@pytest.mark.usefixtures("config_entry_setup") -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -@pytest.mark.freeze_time("2021-01-01 01:01:00") -async def test_sensor_sources( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, - entity_id: str, - unique_id_prefix: str, -) -> None: - """Test sensor sources and the entity description.""" - ent_reg_entry = entity_registry.async_get(entity_id) - assert ent_reg_entry.unique_id.startswith(unique_id_prefix) - assert ent_reg_entry.unique_id == snapshot - assert ent_reg_entry.entity_category == snapshot - - state = hass.states.get(entity_id) - assert state.attributes.get(ATTR_DEVICE_CLASS) == snapshot - assert state.attributes.get(ATTR_FRIENDLY_NAME) == snapshot - assert state.attributes.get(ATTR_STATE_CLASS) == snapshot - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == snapshot - assert state.state == snapshot - - async def _test_uptime_entity( hass: HomeAssistant, freezer: FrozenDateTimeFactory, @@ -1306,19 +1226,7 @@ async def _test_uptime_entity( @pytest.mark.parametrize("config_entry_options", [{CONF_ALLOW_UPTIME_SENSORS: True}]) -@pytest.mark.parametrize( - "client_payload", - [ - [ - { - "mac": "00:00:00:00:00:01", - "name": "client1", - "oui": "Producer", - "uptime": 0, - } - ] - ], -) +@pytest.mark.parametrize("client_payload", [[WIRED_CLIENT]]) @pytest.mark.parametrize( ("initial_uptime", "event_uptime", "small_variation_uptime", "new_uptime"), [ @@ -1331,7 +1239,6 @@ async def _test_uptime_entity( @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_client_uptime( hass: HomeAssistant, - entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, config_entry_options: MappingProxyType[str, Any], config_entry_factory: ConfigEntryFactoryType, @@ -1349,7 +1256,7 @@ async def test_client_uptime( mock_websocket_message, config_entry_factory, payload=client_payload[0], - entity_id="sensor.client1_uptime", + entity_id="sensor.wired_client_uptime", message_key=MessageKey.CLIENT, initial_uptime=initial_uptime, event_uptime=event_uptime, @@ -1357,18 +1264,13 @@ async def test_client_uptime( new_uptime=new_uptime, ) - assert ( - entity_registry.async_get("sensor.client1_uptime").entity_category - is EntityCategory.DIAGNOSTIC - ) - # Disable option options = deepcopy(config_entry_options) options[CONF_ALLOW_UPTIME_SENSORS] = False hass.config_entries.async_update_entry(config_entry, options=options) await hass.async_block_till_done() - assert hass.states.get("sensor.client1_uptime") is None + assert hass.states.get("sensor.wired_client_uptime") is None # Enable option options = deepcopy(config_entry_options) @@ -1376,34 +1278,10 @@ async def test_client_uptime( hass.config_entries.async_update_entry(config_entry, options=options) await hass.async_block_till_done() - assert hass.states.get("sensor.client1_uptime") + assert hass.states.get("sensor.wired_client_uptime") -@pytest.mark.parametrize( - "device_payload", - [ - [ - { - "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, - "uptime": 60, - "version": "4.0.42.10433", - } - ] - ], -) +@pytest.mark.parametrize("device_payload", [[DEVICE_1]]) async def test_device_uptime( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -1419,7 +1297,7 @@ async def test_device_uptime( mock_websocket_message, config_entry_factory, payload=device_payload[0], - entity_id="sensor.device_uptime", + entity_id="sensor.mock_name_uptime", message_key=MessageKey.DEVICE, initial_uptime=60, event_uptime=240, @@ -1427,11 +1305,6 @@ async def test_device_uptime( new_uptime=60, ) - assert ( - entity_registry.async_get("sensor.device_uptime").entity_category - is EntityCategory.DIAGNOSTIC - ) - @pytest.mark.parametrize( "device_payload", @@ -1495,7 +1368,7 @@ async def test_device_uptime( ], ) @pytest.mark.parametrize( - ("entity_id", "state", "updated_state", "index_to_update"), + ("monitor_id", "state", "updated_state", "index_to_update"), [ # Microsoft ("microsoft_wan", "56", "20", 0), @@ -1511,24 +1384,22 @@ async def test_wan_monitor_latency( entity_registry: er.EntityRegistry, mock_websocket_message: WebsocketMessageMock, device_payload: list[dict[str, Any]], - entity_id: str, + monitor_id: str, state: str, updated_state: str, index_to_update: int, ) -> None: """Verify that wan latency sensors are working as expected.""" + entity_id = f"sensor.mock_name_{monitor_id}_latency" assert len(hass.states.async_all()) == 6 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 - latency_entry = entity_registry.async_get(f"sensor.mock_name_{entity_id}_latency") + latency_entry = entity_registry.async_get(entity_id) assert latency_entry.disabled_by == RegistryEntryDisabler.INTEGRATION - assert latency_entry.entity_category is EntityCategory.DIAGNOSTIC # Enable entity - entity_registry.async_update_entity( - entity_id=f"sensor.mock_name_{entity_id}_latency", disabled_by=None - ) + entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) await hass.async_block_till_done() @@ -1541,13 +1412,8 @@ async def test_wan_monitor_latency( assert len(hass.states.async_all()) == 7 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 - # Verify sensor attributes and state - latency_entry = hass.states.get(f"sensor.mock_name_{entity_id}_latency") - assert latency_entry.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DURATION - assert ( - latency_entry.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - ) - assert latency_entry.state == state + # Verify sensor state + assert hass.states.get(entity_id).state == state # Verify state update device = device_payload[0] @@ -1557,9 +1423,7 @@ async def test_wan_monitor_latency( mock_websocket_message(message=MessageKey.DEVICE, data=device) - assert ( - hass.states.get(f"sensor.mock_name_{entity_id}_latency").state == updated_state - ) + assert hass.states.get(entity_id).state == updated_state @pytest.mark.parametrize( From e33a7ecefa0921a2d54bed25a5b1d343b7de86cb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:54:00 +0200 Subject: [PATCH 2318/2411] Improve type hints in websocket_api tests (#123922) --- tests/components/websocket_api/test_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index 62298098adc..20a728cf3cd 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -26,7 +26,7 @@ from tests.typing import ClientSessionGenerator @pytest.fixture -def track_connected(hass): +def track_connected(hass: HomeAssistant) -> dict[str, list[int]]: """Track connected and disconnected events.""" connected_evt = [] From c761d755501bbc6992697ccfe41cf7cc8c51d0ac Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 14 Aug 2024 15:55:59 +0200 Subject: [PATCH 2319/2411] Fix blocking I/O of SSLContext.load_default_certs in Ecovacs (#123856) --- .../components/ecovacs/config_flow.py | 14 +++++--- .../components/ecovacs/controller.py | 36 +++++++++++-------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ecovacs/config_flow.py b/homeassistant/components/ecovacs/config_flow.py index e19d9994f9e..2637dbbddf8 100644 --- a/homeassistant/components/ecovacs/config_flow.py +++ b/homeassistant/components/ecovacs/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial import logging import ssl from typing import Any @@ -100,11 +101,14 @@ async def _validate_input( if not user_input.get(CONF_VERIFY_MQTT_CERTIFICATE, True) and mqtt_url: ssl_context = get_default_no_verify_context() - mqtt_config = create_mqtt_config( - device_id=device_id, - country=country, - override_mqtt_url=mqtt_url, - ssl_context=ssl_context, + mqtt_config = await hass.async_add_executor_job( + partial( + create_mqtt_config, + device_id=device_id, + country=country, + override_mqtt_url=mqtt_url, + ssl_context=ssl_context, + ) ) client = MqttClient(mqtt_config, authenticator) diff --git a/homeassistant/components/ecovacs/controller.py b/homeassistant/components/ecovacs/controller.py index c22fb240536..ec67845cf9f 100644 --- a/homeassistant/components/ecovacs/controller.py +++ b/homeassistant/components/ecovacs/controller.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping +from functools import partial import logging import ssl from typing import Any @@ -64,32 +65,28 @@ class EcovacsController: if not config.get(CONF_VERIFY_MQTT_CERTIFICATE, True) and mqtt_url: ssl_context = get_default_no_verify_context() - self._mqtt = MqttClient( - create_mqtt_config( - device_id=self._device_id, - country=country, - override_mqtt_url=mqtt_url, - ssl_context=ssl_context, - ), - self._authenticator, + self._mqtt_config_fn = partial( + create_mqtt_config, + device_id=self._device_id, + country=country, + override_mqtt_url=mqtt_url, + ssl_context=ssl_context, ) + self._mqtt_client: MqttClient | None = None self._added_legacy_entities: set[str] = set() async def initialize(self) -> None: """Init controller.""" - mqtt_config_verfied = False try: devices = await self._api_client.get_devices() credentials = await self._authenticator.authenticate() for device_config in devices: if isinstance(device_config, DeviceInfo): # MQTT device - if not mqtt_config_verfied: - await self._mqtt.verify_config() - mqtt_config_verfied = True device = Device(device_config, self._authenticator) - await device.initialize(self._mqtt) + mqtt = await self._get_mqtt_client() + await device.initialize(mqtt) self._devices.append(device) else: # Legacy device @@ -116,7 +113,8 @@ class EcovacsController: await device.teardown() for legacy_device in self._legacy_devices: await self._hass.async_add_executor_job(legacy_device.disconnect) - await self._mqtt.disconnect() + if self._mqtt_client is not None: + await self._mqtt_client.disconnect() await self._authenticator.teardown() def add_legacy_entity(self, device: VacBot, component: str) -> None: @@ -127,6 +125,16 @@ class EcovacsController: """Check if legacy entity is added.""" return f"{device.vacuum['did']}_{component}" in self._added_legacy_entities + async def _get_mqtt_client(self) -> MqttClient: + """Return validated MQTT client.""" + if self._mqtt_client is None: + config = await self._hass.async_add_executor_job(self._mqtt_config_fn) + mqtt = MqttClient(config, self._authenticator) + await mqtt.verify_config() + self._mqtt_client = mqtt + + return self._mqtt_client + @property def devices(self) -> list[Device]: """Return devices.""" From ae6ac31d02ee25994cc7dd623de8859f560b2831 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:02:47 +0200 Subject: [PATCH 2320/2411] Improve type hints in smarttub tests (#123910) --- tests/components/smarttub/conftest.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index c05762a903d..06780f8fb1e 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -1,5 +1,6 @@ """Common fixtures for smarttub tests.""" +from typing import Any from unittest.mock import create_autospec, patch import pytest @@ -7,19 +8,20 @@ import smarttub from homeassistant.components.smarttub.const import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @pytest.fixture -def config_data(): +def config_data() -> dict[str, Any]: """Provide configuration data for tests.""" return {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"} @pytest.fixture -def config_entry(config_data): +def config_entry(config_data: dict[str, Any]) -> MockConfigEntry: """Create a mock config entry.""" return MockConfigEntry( domain=DOMAIN, @@ -29,7 +31,7 @@ def config_entry(config_data): @pytest.fixture -async def setup_component(hass): +async def setup_component(hass: HomeAssistant) -> None: """Set up the component.""" assert await async_setup_component(hass, DOMAIN, {}) is True @@ -162,7 +164,7 @@ def mock_api(account, spa): @pytest.fixture -async def setup_entry(hass, config_entry): +async def setup_entry(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: """Initialize the config entry.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) From bba298a44d6850f762028a88fe982ad8bfa05524 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 14 Aug 2024 16:08:34 +0200 Subject: [PATCH 2321/2411] Add favorite position buttons to Motion Blinds (#123489) --- .../components/motion_blinds/button.py | 71 +++++++++++++++++++ .../components/motion_blinds/const.py | 2 +- .../components/motion_blinds/cover.py | 56 +-------------- .../components/motion_blinds/entity.py | 53 ++++++++++++++ .../components/motion_blinds/icons.json | 10 +++ .../components/motion_blinds/strings.json | 8 +++ 6 files changed, 145 insertions(+), 55 deletions(-) create mode 100644 homeassistant/components/motion_blinds/button.py diff --git a/homeassistant/components/motion_blinds/button.py b/homeassistant/components/motion_blinds/button.py new file mode 100644 index 00000000000..30f1cd53e6f --- /dev/null +++ b/homeassistant/components/motion_blinds/button.py @@ -0,0 +1,71 @@ +"""Support for Motionblinds button entity using their WLAN API.""" + +from __future__ import annotations + +from motionblinds.motion_blinds import LimitStatus, MotionBlind + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY +from .coordinator import DataUpdateCoordinatorMotionBlinds +from .entity import MotionCoordinatorEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Perform the setup for Motionblinds.""" + entities: list[ButtonEntity] = [] + motion_gateway = hass.data[DOMAIN][config_entry.entry_id][KEY_GATEWAY] + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + + for blind in motion_gateway.device_list.values(): + if blind.limit_status == LimitStatus.Limit3Detected.name: + entities.append(MotionGoFavoriteButton(coordinator, blind)) + entities.append(MotionSetFavoriteButton(coordinator, blind)) + + async_add_entities(entities) + + +class MotionGoFavoriteButton(MotionCoordinatorEntity, ButtonEntity): + """Button entity to go to the favorite position of a blind.""" + + _attr_translation_key = "go_favorite" + + def __init__( + self, coordinator: DataUpdateCoordinatorMotionBlinds, blind: MotionBlind + ) -> None: + """Initialize the Motion Button.""" + super().__init__(coordinator, blind) + self._attr_unique_id = f"{blind.mac}-go-favorite" + + async def async_press(self) -> None: + """Execute the button action.""" + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Go_favorite_position) + await self.async_request_position_till_stop() + + +class MotionSetFavoriteButton(MotionCoordinatorEntity, ButtonEntity): + """Button entity to set the favorite position of a blind to the current position.""" + + _attr_entity_category = EntityCategory.CONFIG + _attr_translation_key = "set_favorite" + + def __init__( + self, coordinator: DataUpdateCoordinatorMotionBlinds, blind: MotionBlind + ) -> None: + """Initialize the Motion Button.""" + super().__init__(coordinator, blind) + self._attr_unique_id = f"{blind.mac}-set-favorite" + + async def async_press(self) -> None: + """Execute the button action.""" + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Set_favorite_position) diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py index e089fd17943..96067d7ceb0 100644 --- a/homeassistant/components/motion_blinds/const.py +++ b/homeassistant/components/motion_blinds/const.py @@ -6,7 +6,7 @@ DOMAIN = "motion_blinds" MANUFACTURER = "Motionblinds, Coulisse B.V." DEFAULT_GATEWAY_NAME = "Motionblinds Gateway" -PLATFORMS = [Platform.COVER, Platform.SENSOR] +PLATFORMS = [Platform.BUTTON, Platform.COVER, Platform.SENSOR] CONF_WAIT_FOR_PUSH = "wait_for_push" CONF_INTERFACE = "interface" diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 2cbee96adb7..72b78915bad 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging from typing import Any -from motionblinds import DEVICE_TYPES_WIFI, BlindType +from motionblinds import BlindType import voluptuous as vol from homeassistant.components.cover import ( @@ -16,10 +16,9 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import VolDictType from .const import ( @@ -31,8 +30,6 @@ from .const import ( KEY_GATEWAY, SERVICE_SET_ABSOLUTE_POSITION, UPDATE_DELAY_STOP, - UPDATE_INTERVAL_MOVING, - UPDATE_INTERVAL_MOVING_WIFI, ) from .entity import MotionCoordinatorEntity @@ -179,14 +176,6 @@ class MotionBaseDevice(MotionCoordinatorEntity, CoverEntity): """Initialize the blind.""" super().__init__(coordinator, blind) - self._requesting_position: CALLBACK_TYPE | None = None - self._previous_positions = [] - - if blind.device_type in DEVICE_TYPES_WIFI: - self._update_interval_moving = UPDATE_INTERVAL_MOVING_WIFI - else: - self._update_interval_moving = UPDATE_INTERVAL_MOVING - self._attr_device_class = device_class self._attr_unique_id = blind.mac @@ -218,47 +207,6 @@ class MotionBaseDevice(MotionCoordinatorEntity, CoverEntity): return None return self._blind.position == 100 - async def async_scheduled_update_request(self, *_): - """Request a state update from the blind at a scheduled point in time.""" - # add the last position to the list and keep the list at max 2 items - self._previous_positions.append(self.current_cover_position) - if len(self._previous_positions) > 2: - del self._previous_positions[: len(self._previous_positions) - 2] - - async with self._api_lock: - await self.hass.async_add_executor_job(self._blind.Update_trigger) - - self.async_write_ha_state() - - if len(self._previous_positions) < 2 or not all( - self.current_cover_position == prev_position - for prev_position in self._previous_positions - ): - # keep updating the position @self._update_interval_moving until the position does not change. - self._requesting_position = async_call_later( - self.hass, - self._update_interval_moving, - self.async_scheduled_update_request, - ) - else: - self._previous_positions = [] - self._requesting_position = None - - async def async_request_position_till_stop(self, delay=None): - """Request the position of the blind every self._update_interval_moving seconds until it stops moving.""" - if delay is None: - delay = self._update_interval_moving - - self._previous_positions = [] - if self.current_cover_position is None: - return - if self._requesting_position is not None: - self._requesting_position() - - self._requesting_position = async_call_later( - self.hass, delay, self.async_scheduled_update_request - ) - async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" async with self._api_lock: diff --git a/homeassistant/components/motion_blinds/entity.py b/homeassistant/components/motion_blinds/entity.py index 4734d4d9a65..483a638a0eb 100644 --- a/homeassistant/components/motion_blinds/entity.py +++ b/homeassistant/components/motion_blinds/entity.py @@ -5,8 +5,10 @@ from __future__ import annotations from motionblinds import DEVICE_TYPES_GATEWAY, DEVICE_TYPES_WIFI, MotionGateway from motionblinds.motion_blinds import MotionBlind +from homeassistant.core import CALLBACK_TYPE from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -15,6 +17,8 @@ from .const import ( DOMAIN, KEY_GATEWAY, MANUFACTURER, + UPDATE_INTERVAL_MOVING, + UPDATE_INTERVAL_MOVING_WIFI, ) from .coordinator import DataUpdateCoordinatorMotionBlinds from .gateway import device_name @@ -36,6 +40,14 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind self._blind = blind self._api_lock = coordinator.api_lock + self._requesting_position: CALLBACK_TYPE | None = None + self._previous_positions: list[int | dict | None] = [] + + if blind.device_type in DEVICE_TYPES_WIFI: + self._update_interval_moving = UPDATE_INTERVAL_MOVING_WIFI + else: + self._update_interval_moving = UPDATE_INTERVAL_MOVING + if blind.device_type in DEVICE_TYPES_GATEWAY: gateway = blind else: @@ -95,3 +107,44 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind """Unsubscribe when removed.""" self._blind.Remove_callback(self.unique_id) await super().async_will_remove_from_hass() + + async def async_scheduled_update_request(self, *_) -> None: + """Request a state update from the blind at a scheduled point in time.""" + # add the last position to the list and keep the list at max 2 items + self._previous_positions.append(self._blind.position) + if len(self._previous_positions) > 2: + del self._previous_positions[: len(self._previous_positions) - 2] + + async with self._api_lock: + await self.hass.async_add_executor_job(self._blind.Update_trigger) + + self.coordinator.async_update_listeners() + + if len(self._previous_positions) < 2 or not all( + self._blind.position == prev_position + for prev_position in self._previous_positions + ): + # keep updating the position @self._update_interval_moving until the position does not change. + self._requesting_position = async_call_later( + self.hass, + self._update_interval_moving, + self.async_scheduled_update_request, + ) + else: + self._previous_positions = [] + self._requesting_position = None + + async def async_request_position_till_stop(self, delay: int | None = None) -> None: + """Request the position of the blind every self._update_interval_moving seconds until it stops moving.""" + if delay is None: + delay = self._update_interval_moving + + self._previous_positions = [] + if self._blind.position is None: + return + if self._requesting_position is not None: + self._requesting_position() + + self._requesting_position = async_call_later( + self.hass, delay, self.async_scheduled_update_request + ) diff --git a/homeassistant/components/motion_blinds/icons.json b/homeassistant/components/motion_blinds/icons.json index a61c36e3f00..9e1cd613e5b 100644 --- a/homeassistant/components/motion_blinds/icons.json +++ b/homeassistant/components/motion_blinds/icons.json @@ -1,4 +1,14 @@ { + "entity": { + "button": { + "go_favorite": { + "default": "mdi:star" + }, + "set_favorite": { + "default": "mdi:star-cog" + } + } + }, "services": { "set_absolute_position": "mdi:set-square" } diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index cb9468c3a27..ddbf928462a 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -62,6 +62,14 @@ } }, "entity": { + "button": { + "go_favorite": { + "name": "Go to favorite position" + }, + "set_favorite": { + "name": "Set current position as favorite" + } + }, "cover": { "top": { "name": "Top" From bb889619680254270c7c17ff09b699f852d5d3a0 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:10:59 -0400 Subject: [PATCH 2322/2411] Bump aiorussound to 2.3.1 (#123929) --- homeassistant/components/russound_rio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 67a01239615..42e9dc2df8a 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/russound_rio", "iot_class": "local_push", "loggers": ["aiorussound"], - "requirements": ["aiorussound==2.2.3"] + "requirements": ["aiorussound==2.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index beae1629b79..21e4a9b7404 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.2.3 +aiorussound==2.3.1 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 86be5c9e14a..6cf59077ffd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -332,7 +332,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.2.3 +aiorussound==2.3.1 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 From faacfe3f90d3d3dceb244edc080fb78ccc30422c Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:38:40 -0400 Subject: [PATCH 2323/2411] Set available property in russound base entity (#123933) * Set available property in Russound base entity * Fix * Fix --- homeassistant/components/russound_rio/__init__.py | 15 ++++++++++++++- homeassistant/components/russound_rio/entity.py | 15 +++++++++++++++ .../components/russound_rio/media_player.py | 6 ++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/russound_rio/__init__.py b/homeassistant/components/russound_rio/__init__.py index e36cedecfe3..8627c636ef2 100644 --- a/homeassistant/components/russound_rio/__init__.py +++ b/homeassistant/components/russound_rio/__init__.py @@ -7,7 +7,7 @@ from aiorussound import Russound from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from .const import CONNECT_TIMEOUT, RUSSOUND_RIO_EXCEPTIONS @@ -26,6 +26,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) -> port = entry.data[CONF_PORT] russ = Russound(hass.loop, host, port) + @callback + def is_connected_updated(connected: bool) -> None: + if connected: + _LOGGER.warning("Reconnected to controller at %s:%s", host, port) + else: + _LOGGER.warning( + "Disconnected from controller at %s:%s", + host, + port, + ) + + russ.add_connection_callback(is_connected_updated) + try: async with asyncio.timeout(CONNECT_TIMEOUT): await russ.connect() diff --git a/homeassistant/components/russound_rio/entity.py b/homeassistant/components/russound_rio/entity.py index 3430a77108b..150c4e285d1 100644 --- a/homeassistant/components/russound_rio/entity.py +++ b/homeassistant/components/russound_rio/entity.py @@ -6,6 +6,7 @@ from typing import Any, Concatenate from aiorussound import Controller +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity import Entity @@ -68,3 +69,17 @@ class RussoundBaseEntity(Entity): self._attr_device_info["connections"] = { (CONNECTION_NETWORK_MAC, controller.mac_address) } + + @callback + def _is_connected_updated(self, connected: bool) -> None: + """Update the state when the device is ready to receive commands or is unavailable.""" + self._attr_available = connected + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self._instance.add_connection_callback(self._is_connected_updated) + + async def async_will_remove_from_hass(self) -> None: + """Remove callbacks.""" + self._instance.remove_connection_callback(self._is_connected_updated) diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 5f11227ef53..20aaf0f3c08 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -140,8 +140,14 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): async def async_added_to_hass(self) -> None: """Register callback handlers.""" + await super().async_added_to_hass() self._zone.add_callback(self._callback_handler) + async def async_will_remove_from_hass(self) -> None: + """Remove callbacks.""" + await super().async_will_remove_from_hass() + self._zone.remove_callback(self._callback_handler) + def _current_source(self) -> Source: return self._zone.fetch_current_source() From 56083011789c8eea9531e5a439f55057a3f6b89d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:48:52 +0200 Subject: [PATCH 2324/2411] Add missing return type in test __init__ methods (#123932) * Add missing return type in test __init__ methods * Adjust --- tests/components/androidtv/patchers.py | 3 ++- tests/components/apple_tv/common.py | 4 +++- tests/components/aws/test_init.py | 3 ++- tests/components/blackbird/test_media_player.py | 2 +- tests/components/bluetooth/__init__.py | 2 +- tests/components/bluetooth/test_init.py | 5 +++-- tests/components/bluetooth/test_scanner.py | 3 ++- .../components/bluetooth_le_tracker/test_device_tracker.py | 3 ++- tests/components/demo/test_media_player.py | 2 +- tests/components/device_tracker/common.py | 4 ++-- tests/components/esphome/conftest.py | 2 +- tests/components/fan/test_init.py | 2 +- tests/components/file_upload/test_init.py | 3 ++- tests/components/hyperion/test_config_flow.py | 2 +- tests/components/lcn/conftest.py | 3 ++- tests/components/lifx/__init__.py | 7 ++++--- tests/components/lifx/conftest.py | 3 ++- tests/components/lifx/test_config_flow.py | 3 ++- tests/components/lifx/test_init.py | 5 +++-- tests/components/lifx/test_migration.py | 3 ++- tests/components/monoprice/test_media_player.py | 2 +- tests/components/nest/common.py | 2 +- tests/components/nest/conftest.py | 2 +- tests/components/numato/numato_mock.py | 2 +- tests/components/number/test_init.py | 6 +++--- tests/components/oralb/conftest.py | 3 ++- tests/components/plex/test_config_flow.py | 2 +- tests/components/plex/test_init.py | 2 +- tests/components/profiler/test_init.py | 4 ++-- tests/components/stream/conftest.py | 7 ++++--- tests/components/stream/test_worker.py | 4 ++-- tests/components/websocket_api/test_commands.py | 2 +- tests/components/whois/conftest.py | 3 ++- tests/components/xiaomi_ble/conftest.py | 2 +- tests/test_util/aiohttp.py | 4 ++-- 35 files changed, 64 insertions(+), 47 deletions(-) diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 1c32e1770e0..500b9e75cb3 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,5 +1,6 @@ """Define patches used for androidtv tests.""" +from typing import Any from unittest.mock import patch from androidtv.adb_manager.adb_manager_async import DeviceAsync @@ -25,7 +26,7 @@ PROPS_DEV_MAC = "ether ab:cd:ef:gh:ij:kl brd" class AdbDeviceTcpAsyncFake: """A fake of the `adb_shell.adb_device_async.AdbDeviceTcpAsync` class.""" - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize a fake `adb_shell.adb_device_async.AdbDeviceTcpAsync` instance.""" self.available = False diff --git a/tests/components/apple_tv/common.py b/tests/components/apple_tv/common.py index ddb8c1348d9..8a81536c792 100644 --- a/tests/components/apple_tv/common.py +++ b/tests/components/apple_tv/common.py @@ -1,5 +1,7 @@ """Test code shared between test files.""" +from typing import Any + from pyatv import conf, const, interface from pyatv.const import Protocol @@ -7,7 +9,7 @@ from pyatv.const import Protocol class MockPairingHandler(interface.PairingHandler): """Mock for PairingHandler in pyatv.""" - def __init__(self, *args): + def __init__(self, *args: Any) -> None: """Initialize a new MockPairingHandler.""" super().__init__(*args) self.pin_code = None diff --git a/tests/components/aws/test_init.py b/tests/components/aws/test_init.py index 9589ad6c037..820b08e51b4 100644 --- a/tests/components/aws/test_init.py +++ b/tests/components/aws/test_init.py @@ -1,6 +1,7 @@ """Tests for the aws component config and setup.""" import json +from typing import Any from unittest.mock import AsyncMock, MagicMock, call, patch as async_patch from homeassistant.core import HomeAssistant @@ -10,7 +11,7 @@ from homeassistant.setup import async_setup_component class MockAioSession: """Mock AioSession.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Init a mock session.""" self.get_user = AsyncMock() self.invoke = AsyncMock() diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py index ec5a37f72ad..db92dddcc77 100644 --- a/tests/components/blackbird/test_media_player.py +++ b/tests/components/blackbird/test_media_player.py @@ -35,7 +35,7 @@ class AttrDict(dict): class MockBlackbird: """Mock for pyblackbird object.""" - def __init__(self): + def __init__(self) -> None: """Init mock object.""" self.zones = defaultdict(lambda: AttrDict(power=True, av=1)) diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index eae867b96d5..8794d808718 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -271,7 +271,7 @@ async def _async_setup_with_adapter( class MockBleakClient(BleakClient): """Mock bleak client.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Mock init.""" super().__init__(*args, **kwargs) self._device_path = "/dev/test" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index bd38c9cfbae..8e7d604f794 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -3,6 +3,7 @@ import asyncio from datetime import timedelta import time +from typing import Any from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from bleak import BleakError @@ -100,7 +101,7 @@ async def test_setup_and_stop_passive( init_kwargs = None class MockPassiveBleakScanner: - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Init the scanner.""" nonlocal init_kwargs init_kwargs = kwargs @@ -151,7 +152,7 @@ async def test_setup_and_stop_old_bluez( init_kwargs = None class MockBleakScanner: - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Init the scanner.""" nonlocal init_kwargs init_kwargs = kwargs diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index dc25f29111c..ecd1ee58692 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -3,6 +3,7 @@ import asyncio from datetime import timedelta import time +from typing import Any from unittest.mock import ANY, MagicMock, patch from bleak import BleakError @@ -631,7 +632,7 @@ async def test_setup_and_stop_macos( init_kwargs = None class MockBleakScanner: - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Init the scanner.""" nonlocal init_kwargs init_kwargs = kwargs diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index f183f987cde..452297e38c2 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -1,6 +1,7 @@ """Test Bluetooth LE device tracker.""" from datetime import timedelta +from typing import Any from unittest.mock import patch from bleak import BleakError @@ -31,7 +32,7 @@ from tests.components.bluetooth import generate_advertisement_data, generate_ble class MockBleakClient: """Mock BleakClient.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Mock BleakClient.""" async def __aenter__(self, *args, **kwargs): diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index a6669fa705c..7487a4c13e3 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -497,7 +497,7 @@ async def test_media_image_proxy( class MockResponse: """Test response.""" - def __init__(self): + def __init__(self) -> None: """Test response init.""" self.status = 200 self.headers = {"Content-Type": "sometype"} diff --git a/tests/components/device_tracker/common.py b/tests/components/device_tracker/common.py index d30db984a66..b6341443d36 100644 --- a/tests/components/device_tracker/common.py +++ b/tests/components/device_tracker/common.py @@ -61,7 +61,7 @@ def async_see( class MockScannerEntity(ScannerEntity): """Test implementation of a ScannerEntity.""" - def __init__(self): + def __init__(self) -> None: """Init.""" self.connected = False self._hostname = "test.hostname.org" @@ -110,7 +110,7 @@ class MockScannerEntity(ScannerEntity): class MockScanner(DeviceScanner): """Mock device scanner.""" - def __init__(self): + def __init__(self) -> None: """Initialize the MockScanner.""" self.devices_home = [] diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 75be231558f..ea4099560cd 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -421,7 +421,7 @@ async def _mock_generic_device_entry( class MockReconnectLogic(BaseMockReconnectLogic): """Mock ReconnectLogic.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Init the mock.""" super().__init__(*args, **kwargs) mock_device.set_on_disconnect(kwargs["on_disconnect"]) diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index a72ad5e48f6..a7dc544a97a 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -38,7 +38,7 @@ from tests.common import ( class BaseFan(FanEntity): """Implementation of the abstract FanEntity.""" - def __init__(self): + def __init__(self) -> None: """Initialize the fan.""" diff --git a/tests/components/file_upload/test_init.py b/tests/components/file_upload/test_init.py index 149bbb7ee2f..22ad9323f05 100644 --- a/tests/components/file_upload/test_init.py +++ b/tests/components/file_upload/test_init.py @@ -3,6 +3,7 @@ from contextlib import contextmanager from pathlib import Path from random import getrandbits +from typing import Any from unittest.mock import patch import pytest @@ -141,7 +142,7 @@ async def test_upload_large_file_fails( yield MockPathOpen() class MockPathOpen: - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: pass def write(self, data: bytes) -> None: diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 57749f5eedc..fb4fa1fe671 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -427,7 +427,7 @@ async def test_auth_create_token_approval_declined_task_canceled( class CanceledAwaitableMock(AsyncMock): """A canceled awaitable mock.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self.done = Mock(return_value=False) self.cancel = Mock() diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py index 24447abf77a..e29a7076430 100644 --- a/tests/components/lcn/conftest.py +++ b/tests/components/lcn/conftest.py @@ -2,6 +2,7 @@ from collections.abc import AsyncGenerator import json +from typing import Any from unittest.mock import AsyncMock, patch import pypck @@ -29,7 +30,7 @@ class MockModuleConnection(ModuleConnection): request_name = AsyncMock(return_value="TestModule") send_command = AsyncMock(return_value=True) - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Construct ModuleConnection instance.""" super().__init__(*args, **kwargs) self.serials_request_handler.serial_known.set() diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index 4834e486ec0..1d37496fbcb 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from contextlib import contextmanager +from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch from aiolifx.aiolifx import Light @@ -212,7 +213,7 @@ def _patch_device(device: Light | None = None, no_device: bool = False): class MockLifxConnecton: """Mock lifx discovery.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Init connection.""" if no_device: self.device = _mocked_failing_bulb() @@ -240,7 +241,7 @@ def _patch_discovery(device: Light | None = None, no_device: bool = False): class MockLifxDiscovery: """Mock lifx discovery.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Init discovery.""" if no_device: self.lights = {} @@ -276,7 +277,7 @@ def _patch_config_flow_try_connect( class MockLifxConnection: """Mock lifx discovery.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Init connection.""" if no_device: self.device = _mocked_failing_bulb() diff --git a/tests/components/lifx/conftest.py b/tests/components/lifx/conftest.py index 5cb7c702f43..e4a5f303f61 100644 --- a/tests/components/lifx/conftest.py +++ b/tests/components/lifx/conftest.py @@ -1,5 +1,6 @@ """Tests for the lifx integration.""" +from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -21,7 +22,7 @@ def mock_effect_conductor(): """Mock the effect conductor.""" class MockConductor: - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: """Mock the conductor.""" self.start = AsyncMock() self.stop = AsyncMock() diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index 59b7090788a..735fc4bf6f6 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -2,6 +2,7 @@ from ipaddress import ip_address import socket +from typing import Any from unittest.mock import patch import pytest @@ -288,7 +289,7 @@ async def test_manual_dns_error(hass: HomeAssistant) -> None: class MockLifxConnectonDnsError: """Mock lifx discovery.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Init connection.""" self.device = _mocked_failing_bulb() diff --git a/tests/components/lifx/test_init.py b/tests/components/lifx/test_init.py index 42ece68a2c5..66adc54704e 100644 --- a/tests/components/lifx/test_init.py +++ b/tests/components/lifx/test_init.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import socket +from typing import Any from unittest.mock import patch import pytest @@ -37,7 +38,7 @@ async def test_configuring_lifx_causes_discovery(hass: HomeAssistant) -> None: class MockLifxDiscovery: """Mock lifx discovery.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Init discovery.""" discovered = _mocked_bulb() self.lights = {discovered.mac_addr: discovered} @@ -137,7 +138,7 @@ async def test_dns_error_at_startup(hass: HomeAssistant) -> None: class MockLifxConnectonDnsError: """Mock lifx connection with a dns error.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Init connection.""" self.device = bulb diff --git a/tests/components/lifx/test_migration.py b/tests/components/lifx/test_migration.py index e5b2f9f8167..f984acce238 100644 --- a/tests/components/lifx/test_migration.py +++ b/tests/components/lifx/test_migration.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta +from typing import Any from unittest.mock import patch from homeassistant import setup @@ -114,7 +115,7 @@ async def test_discovery_is_more_frequent_during_migration( class MockLifxDiscovery: """Mock lifx discovery.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Init discovery.""" self.bulb = bulb self.lights = {} diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index f7d88692cf5..c4ba998261b 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -58,7 +58,7 @@ class AttrDict(dict): class MockMonoprice: """Mock for pymonoprice object.""" - def __init__(self): + def __init__(self) -> None: """Init mock object.""" self.zones = defaultdict( lambda: AttrDict(power=True, volume=0, mute=True, source=1) diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 0a553f9c114..9c8de0224f0 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -92,7 +92,7 @@ class FakeSubscriber(GoogleNestSubscriber): stop_calls = 0 - def __init__(self): # pylint: disable=super-init-not-called + def __init__(self) -> None: # pylint: disable=super-init-not-called """Initialize Fake Subscriber.""" self._device_manager = DeviceManager() diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index 4b64e80543b..85c64aff379 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -53,7 +53,7 @@ class FakeAuth(AbstractAuth): from the API. """ - def __init__(self): + def __init__(self) -> None: """Initialize FakeAuth.""" super().__init__(None, None) # Tests can set fake responses here. diff --git a/tests/components/numato/numato_mock.py b/tests/components/numato/numato_mock.py index 097a785beb1..3f2fecb4a58 100644 --- a/tests/components/numato/numato_mock.py +++ b/tests/components/numato/numato_mock.py @@ -8,7 +8,7 @@ class NumatoModuleMock: NumatoGpioError = NumatoGpioError - def __init__(self): + def __init__(self) -> None: """Initialize the numato_gpio module mockup class.""" self.devices = {} diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 55dad2506f1..721b531e8cd 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -121,7 +121,7 @@ class MockNumberEntityDescr(NumberEntity): Step is calculated based on the smaller max_value and min_value. """ - def __init__(self): + def __init__(self) -> None: """Initialize the clas instance.""" self.entity_description = NumberEntityDescription( "test", @@ -145,7 +145,7 @@ class MockNumberEntityAttrWithDescription(NumberEntity): members take precedence over the entity description. """ - def __init__(self): + def __init__(self) -> None: """Initialize the clas instance.""" self.entity_description = NumberEntityDescription( "test", @@ -223,7 +223,7 @@ class MockNumberEntityDescrDeprecated(NumberEntity): Step is calculated based on the smaller max_value and min_value. """ - def __init__(self): + def __init__(self) -> None: """Initialize the clas instance.""" self.entity_description = NumberEntityDescription( "test", diff --git a/tests/components/oralb/conftest.py b/tests/components/oralb/conftest.py index c757d79a78e..3e5f38ffb73 100644 --- a/tests/components/oralb/conftest.py +++ b/tests/components/oralb/conftest.py @@ -1,6 +1,7 @@ """OralB session fixtures.""" from collections.abc import Generator +from typing import Any from unittest import mock import pytest @@ -19,7 +20,7 @@ class MockBleakClient: services = MockServices() - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Mock BleakClient.""" async def __aenter__(self, *args, **kwargs): diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 08733a7dd17..202d62d70e0 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -537,7 +537,7 @@ async def test_manual_config(hass: HomeAssistant, mock_plex_calls) -> None: class WrongCertValidaitionException(requests.exceptions.SSLError): """Mock the exception showing an unmatched error.""" - def __init__(self): # pylint: disable=super-init-not-called + def __init__(self) -> None: # pylint: disable=super-init-not-called self.__context__ = ssl.SSLCertVerificationError( "some random message that doesn't match" ) diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 15af78faf65..490091998ff 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -209,7 +209,7 @@ async def test_setup_when_certificate_changed( class WrongCertHostnameException(requests.exceptions.SSLError): """Mock the exception showing a mismatched hostname.""" - def __init__(self): # pylint: disable=super-init-not-called + def __init__(self) -> None: # pylint: disable=super-init-not-called self.__context__ = ssl.SSLCertVerificationError( f"hostname '{old_domain}' doesn't match" ) diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index b172ae0e12d..1f8f8baf02b 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -284,14 +284,14 @@ async def test_lru_stats(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) return 1 class DomainData: - def __init__(self): + def __init__(self) -> None: self._data = LRU(1) domain_data = DomainData() assert hass.services.has_service(DOMAIN, SERVICE_LRU_STATS) class LRUCache: - def __init__(self): + def __init__(self) -> None: self._data = {"sqlalchemy_test": 1} sqlalchemy_lru_cache = LRUCache() diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 6aab3c06d13..39e4de13fed 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -16,6 +16,7 @@ import asyncio from collections.abc import Generator import logging import threading +from typing import Any from unittest.mock import Mock, patch from aiohttp import web @@ -32,7 +33,7 @@ TEST_TIMEOUT = 7.0 # Lower than 9s home assistant timeout class WorkerSync: """Test fixture that intercepts stream worker calls to StreamOutput.""" - def __init__(self): + def __init__(self) -> None: """Initialize WorkerSync.""" self._event = None self._original = StreamState.discontinuity @@ -74,7 +75,7 @@ def stream_worker_sync() -> Generator[WorkerSync]: class HLSSync: """Test fixture that intercepts stream worker calls to StreamOutput.""" - def __init__(self): + def __init__(self) -> None: """Initialize HLSSync.""" self._request_event = asyncio.Event() self._original_recv = StreamOutput.recv @@ -91,7 +92,7 @@ class HLSSync: self.check_requests_ready() class SyncResponse(web.Response): - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) on_resp() diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index a96866eac4b..d0f89364778 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -160,7 +160,7 @@ class PacketSequence: class FakePacket(bytearray): # Be a bytearray so that memoryview works - def __init__(self): + def __init__(self) -> None: super().__init__(3) time_base = VIDEO_TIME_BASE @@ -209,7 +209,7 @@ class FakePyAvContainer: class FakePyAvBuffer: """Holds outputs of the decoded stream for tests to assert on results.""" - def __init__(self): + def __init__(self) -> None: """Initialize the FakePyAvBuffer.""" self.segments = [] self.audio_packets = [] diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 10a9c4876b9..772a8ee793e 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -920,7 +920,7 @@ async def test_subscribe_entities_with_unserializable_state( class CannotSerializeMe: """Cannot serialize this.""" - def __init__(self): + def __init__(self) -> None: """Init cannot serialize this.""" hass.states.async_set("light.permitted", "off", {"color": "red"}) diff --git a/tests/components/whois/conftest.py b/tests/components/whois/conftest.py index 1c779cce671..4bb18581c1a 100644 --- a/tests/components/whois/conftest.py +++ b/tests/components/whois/conftest.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Generator from datetime import datetime +from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest @@ -74,7 +75,7 @@ def mock_whois_missing_some_attrs() -> Generator[Mock]: class LimitedWhoisMock: """A limited mock of whois_query.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: """Mock only attributes the library always sets being available.""" self.creation_date = datetime(2019, 1, 1, 0, 0, 0) self.dnssec = True diff --git a/tests/components/xiaomi_ble/conftest.py b/tests/components/xiaomi_ble/conftest.py index 8994aec813c..d4864cbe2f8 100644 --- a/tests/components/xiaomi_ble/conftest.py +++ b/tests/components/xiaomi_ble/conftest.py @@ -19,7 +19,7 @@ class MockBleakClient: services = MockServices() - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: """Mock BleakClient.""" async def __aenter__(self, *args, **kwargs): diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index d0bd7fbeb2f..b5eda374f01 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -37,7 +37,7 @@ def mock_stream(data): class AiohttpClientMocker: """Mock Aiohttp client requests.""" - def __init__(self): + def __init__(self) -> None: """Initialize the request mocker.""" self._mocks = [] self._cookies = {} @@ -327,7 +327,7 @@ class MockLongPollSideEffect: If queue is empty, will await until done. """ - def __init__(self): + def __init__(self) -> None: """Initialize the queue.""" self.semaphore = asyncio.Semaphore(0) self.response_list = [] From 5958ef363f7f0a2b5cfd36ac465d1f7146d46a8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Aug 2024 10:02:44 -0500 Subject: [PATCH 2325/2411] Bump pylutron_caseta to 0.21.1 (#123924) --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/licenses.py | 1 - tests/components/lutron_caseta/test_device_trigger.py | 5 +++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 48445f645aa..3c6348ed4da 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -9,7 +9,7 @@ }, "iot_class": "local_push", "loggers": ["pylutron_caseta"], - "requirements": ["pylutron-caseta==0.20.0"], + "requirements": ["pylutron-caseta==0.21.1"], "zeroconf": [ { "type": "_lutron._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 21e4a9b7404..c00200df958 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1990,7 +1990,7 @@ pylitejet==0.6.2 pylitterbot==2023.5.0 # homeassistant.components.lutron_caseta -pylutron-caseta==0.20.0 +pylutron-caseta==0.21.1 # homeassistant.components.lutron pylutron==0.2.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6cf59077ffd..6cd024839f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1592,7 +1592,7 @@ pylitejet==0.6.2 pylitterbot==2023.5.0 # homeassistant.components.lutron_caseta -pylutron-caseta==0.20.0 +pylutron-caseta==0.21.1 # homeassistant.components.lutron pylutron==0.2.15 diff --git a/script/licenses.py b/script/licenses.py index 0c2870aebd8..1e01fb6111b 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -159,7 +159,6 @@ EXCEPTIONS = { "pyTibber", # https://github.com/Danielhiversen/pyTibber/pull/294 "pybbox", # https://github.com/HydrelioxGitHub/pybbox/pull/5 "pyeconet", # https://github.com/w1ll1am23/pyeconet/pull/41 - "pylutron-caseta", # https://github.com/gurumitts/pylutron-caseta/pull/168 "pysabnzbd", # https://github.com/jeradM/pysabnzbd/pull/6 "pyvera", # https://github.com/maximvelichko/pyvera/pull/164 "pyxeoma", # https://github.com/jeradM/pyxeoma/pull/11 diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 405c504dee1..9353b897602 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -487,8 +487,9 @@ async def test_if_fires_on_button_event_late_setup( }, ) - await hass.config_entries.async_setup(config_entry_id) - await hass.async_block_till_done() + with patch("homeassistant.components.lutron_caseta.Smartbridge.create_tls"): + await hass.config_entries.async_setup(config_entry_id) + await hass.async_block_till_done() message = { ATTR_SERIAL: device.get("serial"), From 5e6f8373e15d6c805fe1e29f7e86f233105454b6 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:22:36 -0400 Subject: [PATCH 2326/2411] Set quality scale to silver for Russound RIO (#123937) --- homeassistant/components/russound_rio/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 42e9dc2df8a..7180c3be84f 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/russound_rio", "iot_class": "local_push", "loggers": ["aiorussound"], + "quality_scale": "silver", "requirements": ["aiorussound==2.3.1"] } From 178482068dfd376c27041c55bbf5c92b92f41dad Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 17:38:30 +0200 Subject: [PATCH 2327/2411] Add missing return type in test __init__ method (part 3) (#123940) --- tests/components/alexa/test_common.py | 4 ++-- tests/components/application_credentials/test_init.py | 7 ++++++- tests/components/media_player/test_async_helpers.py | 2 +- tests/components/nest/test_config_flow.py | 7 ++++++- tests/components/universal/test_media_player.py | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/components/alexa/test_common.py b/tests/components/alexa/test_common.py index 9fdcc1c89c1..f93d9483b43 100644 --- a/tests/components/alexa/test_common.py +++ b/tests/components/alexa/test_common.py @@ -7,7 +7,7 @@ import pytest from homeassistant.components.alexa import config, smart_home from homeassistant.components.alexa.const import CONF_ENDPOINT, CONF_FILTER, CONF_LOCALE -from homeassistant.core import Context, callback +from homeassistant.core import Context, HomeAssistant, callback from homeassistant.helpers import entityfilter from tests.common import async_mock_service @@ -28,7 +28,7 @@ class MockConfig(smart_home.AlexaConfig): "camera.test": {"display_categories": "CAMERA"}, } - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Mock Alexa config.""" super().__init__( hass, diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index e6fdf568bcc..134d996865d 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -124,7 +124,12 @@ def config_flow_handler( class OAuthFixture: """Fixture to facilitate testing an OAuth flow.""" - def __init__(self, hass, hass_client, aioclient_mock): + def __init__( + self, + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + ) -> None: """Initialize OAuthFixture.""" self.hass = hass self.hass_client = hass_client diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 783846d8857..750d2861f21 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant class SimpleMediaPlayer(mp.MediaPlayerEntity): """Media player test class.""" - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Initialize the test media player.""" self.hass = hass self._volume = 0 diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index 5c8f01c8e39..b6e84ce358f 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -56,7 +56,12 @@ def nest_test_config() -> NestTestConfig: class OAuthFixture: """Simulate the oauth flow used by the config flow.""" - def __init__(self, hass, hass_client_no_auth, aioclient_mock): + def __init__( + self, + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + ) -> None: """Initialize OAuthFixture.""" self.hass = hass self.hass_client = hass_client_no_auth diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 69d4c8666cf..7c992814cfe 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -55,7 +55,7 @@ def validate_config(config): class MockMediaPlayer(media_player.MediaPlayerEntity): """Mock media player for testing.""" - def __init__(self, hass, name): + def __init__(self, hass: HomeAssistant, name: str) -> None: """Initialize the media player.""" self.hass = hass self._name = name From 3322fa0294d3f4abcb8bf1b1803075b9674e1af1 Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:39:23 -0400 Subject: [PATCH 2328/2411] Bump LaCrosse View to 1.0.2, fixes blocking call (#123935) --- homeassistant/components/lacrosse_view/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lacrosse_view/manifest.json b/homeassistant/components/lacrosse_view/manifest.json index 1236f63ddad..1cf8794237d 100644 --- a/homeassistant/components/lacrosse_view/manifest.json +++ b/homeassistant/components/lacrosse_view/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/lacrosse_view", "iot_class": "cloud_polling", "loggers": ["lacrosse_view"], - "requirements": ["lacrosse-view==1.0.1"] + "requirements": ["lacrosse-view==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index c00200df958..c5fe1e2171a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1228,7 +1228,7 @@ konnected==1.2.0 krakenex==2.1.0 # homeassistant.components.lacrosse_view -lacrosse-view==1.0.1 +lacrosse-view==1.0.2 # homeassistant.components.eufy lakeside==0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6cd024839f8..b84660fb0b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1027,7 +1027,7 @@ konnected==1.2.0 krakenex==2.1.0 # homeassistant.components.lacrosse_view -lacrosse-view==1.0.1 +lacrosse-view==1.0.2 # homeassistant.components.laundrify laundrify-aio==1.2.2 From 3e967700fd4e51c8bf86a799e93b5594f1afffc3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 Aug 2024 17:59:15 +0200 Subject: [PATCH 2329/2411] Add missing return type in test __init__ method (part 2) (#123939) * Add missing return type in test __init__ method (part 2) * Adjust * One more * One more * More --- tests/components/alexa/test_common.py | 2 +- tests/components/androidtv/test_config_flow.py | 2 +- tests/components/application_credentials/test_init.py | 2 +- tests/components/assist_pipeline/conftest.py | 4 ++-- tests/components/baf/__init__.py | 2 +- tests/components/bluetooth/test_scanner.py | 2 +- tests/components/coinbase/common.py | 6 +++--- tests/components/fido/test_sensor.py | 2 +- tests/components/flic/test_binary_sensor.py | 2 +- tests/components/folder_watcher/test_init.py | 4 ++-- tests/components/fritz/conftest.py | 2 +- tests/components/google/test_calendar.py | 2 +- tests/components/hddtemp/test_sensor.py | 2 +- tests/components/hdmi_cec/__init__.py | 2 +- tests/components/hlk_sw16/test_config_flow.py | 2 +- tests/components/homematicip_cloud/helper.py | 2 +- tests/components/http/test_cors.py | 2 +- tests/components/insteon/mock_devices.py | 2 +- tests/components/keymitt_ble/__init__.py | 4 ++-- tests/components/knx/test_config_flow.py | 2 +- tests/components/kodi/util.py | 4 ++-- tests/components/lifx/__init__.py | 6 +++--- tests/components/lifx/test_config_flow.py | 2 +- tests/components/lifx/test_light.py | 7 ++++--- tests/components/lutron_caseta/__init__.py | 2 +- tests/components/modbus/conftest.py | 2 +- tests/components/nsw_fuel_station/test_sensor.py | 8 +++++--- tests/components/numato/numato_mock.py | 2 +- tests/components/pilight/test_init.py | 2 +- tests/components/plex/mock_classes.py | 2 +- tests/components/plex/test_playback.py | 2 +- tests/components/profiler/test_init.py | 2 +- tests/components/recorder/db_schema_16.py | 2 +- tests/components/recorder/db_schema_18.py | 2 +- tests/components/recorder/db_schema_22.py | 2 +- tests/components/recorder/db_schema_23.py | 2 +- .../recorder/db_schema_23_with_newer_columns.py | 2 +- tests/components/recorder/test_util.py | 2 +- tests/components/reddit/test_sensor.py | 2 +- tests/components/samsungtv/conftest.py | 2 +- tests/components/sonos/conftest.py | 2 +- tests/components/stream/test_hls.py | 2 +- tests/components/stream/test_worker.py | 10 +++++----- tests/components/tomato/test_device_tracker.py | 2 +- tests/components/venstar/__init__.py | 2 +- tests/components/vera/common.py | 2 +- tests/components/vizio/conftest.py | 2 +- tests/components/ws66i/test_media_player.py | 2 +- tests/components/yamaha/test_media_player.py | 2 +- tests/components/yeelight/__init__.py | 2 +- tests/test_util/aiohttp.py | 2 +- 51 files changed, 70 insertions(+), 67 deletions(-) diff --git a/tests/components/alexa/test_common.py b/tests/components/alexa/test_common.py index f93d9483b43..43e7d77ce71 100644 --- a/tests/components/alexa/test_common.py +++ b/tests/components/alexa/test_common.py @@ -213,7 +213,7 @@ async def reported_properties(hass, endpoint, return_full_response=False): class ReportedProperties: """Class to help assert reported properties.""" - def __init__(self, properties): + def __init__(self, properties) -> None: """Initialize class.""" self.properties = properties diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py index e2b5207c590..b73fee9fb10 100644 --- a/tests/components/androidtv/test_config_flow.py +++ b/tests/components/androidtv/test_config_flow.py @@ -73,7 +73,7 @@ CONNECT_METHOD = ( class MockConfigDevice: """Mock class to emulate Android device.""" - def __init__(self, eth_mac=ETH_MAC, wifi_mac=None): + def __init__(self, eth_mac=ETH_MAC, wifi_mac=None) -> None: """Initialize a fake device to test config flow.""" self.available = True self.device_properties = {PROP_ETHMAC: eth_mac, PROP_WIFIMAC: wifi_mac} diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index 134d996865d..d90084fa7c9 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -189,7 +189,7 @@ async def oauth_fixture( class Client: """Test client with helper methods for application credentials websocket.""" - def __init__(self, client): + def __init__(self, client) -> None: """Initialize Client.""" self.client = client self.id = 0 diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index 141baaa9870..b7bf83a7ed0 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -153,7 +153,7 @@ class MockTTSPlatform(MockPlatform): PLATFORM_SCHEMA = tts.PLATFORM_SCHEMA - def __init__(self, *, async_get_engine, **kwargs): + def __init__(self, *, async_get_engine, **kwargs: Any) -> None: """Initialize the tts platform.""" super().__init__(**kwargs) self.async_get_engine = async_get_engine @@ -180,7 +180,7 @@ def mock_stt_provider_entity() -> MockSttProviderEntity: class MockSttPlatform(MockPlatform): """Provide a fake STT platform.""" - def __init__(self, *, async_get_engine, **kwargs): + def __init__(self, *, async_get_engine, **kwargs: Any) -> None: """Initialize the stt platform.""" super().__init__(**kwargs) self.async_get_engine = async_get_engine diff --git a/tests/components/baf/__init__.py b/tests/components/baf/__init__.py index f1074a87cee..a047029f9a0 100644 --- a/tests/components/baf/__init__.py +++ b/tests/components/baf/__init__.py @@ -12,7 +12,7 @@ class MockBAFDevice(Device): """A simple mock for a BAF Device.""" # pylint: disable-next=super-init-not-called - def __init__(self, async_wait_available_side_effect=None): + def __init__(self, async_wait_available_side_effect=None) -> None: """Init simple mock.""" self._async_wait_available_side_effect = async_wait_available_side_effect diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index ecd1ee58692..6acb86476e7 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -212,7 +212,7 @@ async def test_recovery_from_dbus_restart(hass: HomeAssistant) -> None: mock_discovered = [] class MockBleakScanner: - def __init__(self, detection_callback, *args, **kwargs): + def __init__(self, detection_callback, *args: Any, **kwargs: Any) -> None: nonlocal _callback _callback = detection_callback diff --git a/tests/components/coinbase/common.py b/tests/components/coinbase/common.py index 2768b6a2cd4..1a141c88bc3 100644 --- a/tests/components/coinbase/common.py +++ b/tests/components/coinbase/common.py @@ -21,7 +21,7 @@ from tests.common import MockConfigEntry class MockPagination: """Mock pagination result.""" - def __init__(self, value=None): + def __init__(self, value=None) -> None: """Load simple pagination for tests.""" self.next_starting_after = value @@ -29,7 +29,7 @@ class MockPagination: class MockGetAccounts: """Mock accounts with pagination.""" - def __init__(self, starting_after=0): + def __init__(self, starting_after=0) -> None: """Init mocked object, forced to return two at a time.""" if (target_end := starting_after + 2) >= ( max_end := len(MOCK_ACCOUNTS_RESPONSE) @@ -58,7 +58,7 @@ def mocked_get_accounts(_, **kwargs): class MockGetAccountsV3: """Mock accounts with pagination.""" - def __init__(self, cursor=""): + def __init__(self, cursor="") -> None: """Init mocked object, forced to return two at a time.""" ids = [account["uuid"] for account in MOCK_ACCOUNTS_RESPONSE_V3] start = ids.index(cursor) if cursor else 0 diff --git a/tests/components/fido/test_sensor.py b/tests/components/fido/test_sensor.py index d47c7ce8e9f..654221cfacd 100644 --- a/tests/components/fido/test_sensor.py +++ b/tests/components/fido/test_sensor.py @@ -18,7 +18,7 @@ CONTRACT = "123456789" class FidoClientMock: """Fake Fido client.""" - def __init__(self, username, password, timeout=None, httpsession=None): + def __init__(self, username, password, timeout=None, httpsession=None) -> None: """Fake Fido client init.""" def get_phone_numbers(self): diff --git a/tests/components/flic/test_binary_sensor.py b/tests/components/flic/test_binary_sensor.py index 44db1d6ea1b..cdc1d64db41 100644 --- a/tests/components/flic/test_binary_sensor.py +++ b/tests/components/flic/test_binary_sensor.py @@ -8,7 +8,7 @@ from homeassistant.setup import async_setup_component class _MockFlicClient: - def __init__(self, button_addresses): + def __init__(self, button_addresses) -> None: self.addresses = button_addresses self.get_info_callback = None self.scan_wizard = None diff --git a/tests/components/folder_watcher/test_init.py b/tests/components/folder_watcher/test_init.py index 8309988931a..965ae33c4f8 100644 --- a/tests/components/folder_watcher/test_init.py +++ b/tests/components/folder_watcher/test_init.py @@ -36,7 +36,7 @@ def test_event() -> None: class MockPatternMatchingEventHandler: """Mock base class for the pattern matcher event handler.""" - def __init__(self, patterns): + def __init__(self, patterns) -> None: pass with patch( @@ -66,7 +66,7 @@ def test_move_event() -> None: class MockPatternMatchingEventHandler: """Mock base class for the pattern matcher event handler.""" - def __init__(self, patterns): + def __init__(self, patterns) -> None: pass with patch( diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index bb049f067b4..fa92fa37c04 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -30,7 +30,7 @@ class FritzServiceMock(Service): class FritzConnectionMock: """FritzConnection mocking.""" - def __init__(self, services): + def __init__(self, services) -> None: """Init Mocking class.""" self.modelname = MOCK_MODELNAME self.call_action = self._call_action diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 903b68a5cf2..11d4ec46bd1 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -74,7 +74,7 @@ def upcoming_event_url(entity: str = TEST_ENTITY) -> str: class Client: """Test client with helper methods for calendar websocket.""" - def __init__(self, client): + def __init__(self, client) -> None: """Initialize Client.""" self.client = client self.id = 0 diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index 2bd0519c12c..15740ffa0ea 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -60,7 +60,7 @@ REFERENCE = { class TelnetMock: """Mock class for the telnetlib.Telnet object.""" - def __init__(self, host, port, timeout=0): + def __init__(self, host, port, timeout=0) -> None: """Initialize Telnet object.""" self.host = host self.port = port diff --git a/tests/components/hdmi_cec/__init__.py b/tests/components/hdmi_cec/__init__.py index 5cf8ed18b6a..1d51fa0cc50 100644 --- a/tests/components/hdmi_cec/__init__.py +++ b/tests/components/hdmi_cec/__init__.py @@ -8,7 +8,7 @@ from homeassistant.components.hdmi_cec import KeyPressCommand, KeyReleaseCommand class MockHDMIDevice: """Mock of a HDMIDevice.""" - def __init__(self, *, logical_address, **values): + def __init__(self, *, logical_address, **values) -> None: """Mock of a HDMIDevice.""" self.set_update_callback = Mock(side_effect=self._set_update_callback) self.logical_address = logical_address diff --git a/tests/components/hlk_sw16/test_config_flow.py b/tests/components/hlk_sw16/test_config_flow.py index 6a758ec5066..2225ea1b79a 100644 --- a/tests/components/hlk_sw16/test_config_flow.py +++ b/tests/components/hlk_sw16/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant.data_entry_flow import FlowResultType class MockSW16Client: """Class to mock the SW16Client client.""" - def __init__(self, fail): + def __init__(self, fail) -> None: """Initialise client with failure modes.""" self.fail = fail self.disconnect_callback = None diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index e7d7350f98e..bf20d37f2a3 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -132,7 +132,7 @@ class HomeTemplate(Home): def __init__( self, connection=None, home_name="", test_devices=None, test_groups=None - ): + ) -> None: """Init template with connection.""" super().__init__(connection=connection) self.name = home_name diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 1188131cc0f..c0256abb25d 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -119,7 +119,7 @@ async def test_cors_middleware_with_cors_allowed_view(hass: HomeAssistant) -> No requires_auth = False cors_allowed = True - def __init__(self, url, name): + def __init__(self, url, name) -> None: """Initialize test view.""" self.url = url self.name = name diff --git a/tests/components/insteon/mock_devices.py b/tests/components/insteon/mock_devices.py index 6b5f5cf5e09..2c385c337fd 100644 --- a/tests/components/insteon/mock_devices.py +++ b/tests/components/insteon/mock_devices.py @@ -30,7 +30,7 @@ class MockSwitchLinc(SwitchedLightingControl_SwitchLinc02): class MockDevices: """Mock devices class.""" - def __init__(self, connected=True): + def __init__(self, connected=True) -> None: """Init the MockDevices class.""" self._devices = {} self.modem = None diff --git a/tests/components/keymitt_ble/__init__.py b/tests/components/keymitt_ble/__init__.py index 1e717b805c5..6fa608ad3b4 100644 --- a/tests/components/keymitt_ble/__init__.py +++ b/tests/components/keymitt_ble/__init__.py @@ -53,7 +53,7 @@ SERVICE_INFO = BluetoothServiceInfoBleak( class MockMicroBotApiClient: """Mock MicroBotApiClient.""" - def __init__(self, device, token): + def __init__(self, device, token) -> None: """Mock init.""" async def connect(self, init): @@ -70,7 +70,7 @@ class MockMicroBotApiClient: class MockMicroBotApiClientFail: """Mock MicroBotApiClient.""" - def __init__(self, device, token): + def __init__(self, device, token) -> None: """Mock init.""" async def connect(self, init): diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index d82e5ae33c7..78751c7e641 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -126,7 +126,7 @@ def _gateway_descriptor( class GatewayScannerMock: """Mock GatewayScanner.""" - def __init__(self, gateways=None): + def __init__(self, gateways=None) -> None: """Initialize GatewayScannerMock.""" # Key is a HPAI instance in xknx, but not used in HA anyway. self.found_gateways = ( diff --git a/tests/components/kodi/util.py b/tests/components/kodi/util.py index 6217a77903b..e56ba03b7e5 100644 --- a/tests/components/kodi/util.py +++ b/tests/components/kodi/util.py @@ -63,7 +63,7 @@ def get_kodi_connection( class MockConnection: """A mock kodi connection.""" - def __init__(self, connected=True): + def __init__(self, connected=True) -> None: """Mock the Kodi connection.""" self._connected = connected @@ -92,7 +92,7 @@ class MockConnection: class MockWSConnection: """A mock kodi websocket connection.""" - def __init__(self, connected=True): + def __init__(self, connected=True) -> None: """Mock the websocket connection.""" self._connected = connected diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index 1d37496fbcb..432e7673db6 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -26,7 +26,7 @@ DEFAULT_ENTRY_TITLE = LABEL class MockMessage: """Mock a lifx message.""" - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any) -> None: """Init message.""" self.target_addr = SERIAL self.count = 9 @@ -38,7 +38,7 @@ class MockMessage: class MockFailingLifxCommand: """Mock a lifx command that fails.""" - def __init__(self, bulb, **kwargs): + def __init__(self, bulb, **kwargs: Any) -> None: """Init command.""" self.bulb = bulb self.calls = [] @@ -61,7 +61,7 @@ class MockLifxCommand: """Return name.""" return "mock_lifx_command" - def __init__(self, bulb, **kwargs): + def __init__(self, bulb, **kwargs: Any) -> None: """Init command.""" self.bulb = bulb self.calls = [] diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index 735fc4bf6f6..29324d0d19a 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -575,7 +575,7 @@ async def test_suggested_area( class MockLifxCommandGetGroup: """Mock the get_group method that gets the group name from the bulb.""" - def __init__(self, bulb, **kwargs): + def __init__(self, bulb, **kwargs: Any) -> None: """Init command.""" self.bulb = bulb self.lifx_group = kwargs.get("lifx_group") diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 9972bc1021a..a642347b4e6 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -1,6 +1,7 @@ """Tests for the lifx integration light platform.""" from datetime import timedelta +from typing import Any from unittest.mock import patch import aiolifx_effects @@ -1299,7 +1300,7 @@ async def test_config_zoned_light_strip_fails( class MockFailingLifxCommand: """Mock a lifx command that fails on the 2nd try.""" - def __init__(self, bulb, **kwargs): + def __init__(self, bulb, **kwargs: Any) -> None: """Init command.""" self.bulb = bulb self.call_count = 0 @@ -1338,7 +1339,7 @@ async def test_legacy_zoned_light_strip( class MockPopulateLifxZonesCommand: """Mock populating the number of zones.""" - def __init__(self, bulb, **kwargs): + def __init__(self, bulb, **kwargs: Any) -> None: """Init command.""" self.bulb = bulb self.call_count = 0 @@ -1845,7 +1846,7 @@ async def test_color_bulb_is_actually_off(hass: HomeAssistant) -> None: class MockLifxCommandActuallyOff: """Mock a lifx command that will update our power level state.""" - def __init__(self, bulb, **kwargs): + def __init__(self, bulb, **kwargs: Any) -> None: """Init command.""" self.bulb = bulb self.calls = [] diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py index 9b25e2a0164..b27d30ac31f 100644 --- a/tests/components/lutron_caseta/__init__.py +++ b/tests/components/lutron_caseta/__init__.py @@ -101,7 +101,7 @@ async def async_setup_integration(hass: HomeAssistant, mock_bridge) -> MockConfi class MockBridge: """Mock Lutron bridge that emulates configured connected status.""" - def __init__(self, can_connect=True): + def __init__(self, can_connect=True) -> None: """Initialize MockBridge instance with configured mock connectivity.""" self.can_connect = can_connect self.is_currently_connected = False diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 28f8eae5a0b..5c612f9f8ad 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -37,7 +37,7 @@ TEST_PORT_SERIAL = "usb01" class ReadResult: """Storage class for register read results.""" - def __init__(self, register_words): + def __init__(self, register_words) -> None: """Init.""" self.registers = register_words self.bits = register_words diff --git a/tests/components/nsw_fuel_station/test_sensor.py b/tests/components/nsw_fuel_station/test_sensor.py index 898d5757870..dbf52d937f0 100644 --- a/tests/components/nsw_fuel_station/test_sensor.py +++ b/tests/components/nsw_fuel_station/test_sensor.py @@ -23,7 +23,9 @@ VALID_CONFIG_EXPECTED_ENTITY_IDS = ["my_fake_station_p95", "my_fake_station_e10" class MockPrice: """Mock Price implementation.""" - def __init__(self, price, fuel_type, last_updated, price_unit, station_code): + def __init__( + self, price, fuel_type, last_updated, price_unit, station_code + ) -> None: """Initialize a mock price instance.""" self.price = price self.fuel_type = fuel_type @@ -35,7 +37,7 @@ class MockPrice: class MockStation: """Mock Station implementation.""" - def __init__(self, name, code): + def __init__(self, name, code) -> None: """Initialize a mock Station instance.""" self.name = name self.code = code @@ -44,7 +46,7 @@ class MockStation: class MockGetFuelPricesResponse: """Mock GetFuelPricesResponse implementation.""" - def __init__(self, prices, stations): + def __init__(self, prices, stations) -> None: """Initialize a mock GetFuelPricesResponse instance.""" self.prices = prices self.stations = stations diff --git a/tests/components/numato/numato_mock.py b/tests/components/numato/numato_mock.py index 3f2fecb4a58..208beffe83f 100644 --- a/tests/components/numato/numato_mock.py +++ b/tests/components/numato/numato_mock.py @@ -15,7 +15,7 @@ class NumatoModuleMock: class NumatoDeviceMock: """Mockup for the numato_gpio.NumatoUsbGpio class.""" - def __init__(self, device): + def __init__(self, device) -> None: """Initialize numato device mockup.""" self.device = device self.callbacks = {} diff --git a/tests/components/pilight/test_init.py b/tests/components/pilight/test_init.py index c48135f59eb..dfc62d30619 100644 --- a/tests/components/pilight/test_init.py +++ b/tests/components/pilight/test_init.py @@ -40,7 +40,7 @@ class PilightDaemonSim: "message": {"id": 0, "unit": 0, "off": 1}, } - def __init__(self, host, port): + def __init__(self, host, port) -> None: """Init pilight client, ignore parameters.""" def send_code(self, call): diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index c6f1aeda9b7..92844f755d6 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -67,7 +67,7 @@ GDM_CLIENT_PAYLOAD = [ class MockGDM: """Mock a GDM instance.""" - def __init__(self, disabled=False): + def __init__(self, disabled=False) -> None: """Initialize the object.""" self.entries = [] self.disabled = disabled diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index 183a779c940..c4206bd5f3e 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -28,7 +28,7 @@ class MockPlexMedia: viewOffset = 333 _server = Mock(_baseurl=PLEX_DIRECT_URL) - def __init__(self, title, mediatype): + def __init__(self, title, mediatype) -> None: """Initialize the instance.""" self.listType = mediatype self.title = title diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index 1f8f8baf02b..3f0e0b92056 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -176,7 +176,7 @@ async def test_dump_log_object( await hass.async_block_till_done() class DumpLogDummy: - def __init__(self, fail): + def __init__(self, fail) -> None: self.fail = fail def __repr__(self): diff --git a/tests/components/recorder/db_schema_16.py b/tests/components/recorder/db_schema_16.py index 24786b1ad44..ffee438f2e9 100644 --- a/tests/components/recorder/db_schema_16.py +++ b/tests/components/recorder/db_schema_16.py @@ -356,7 +356,7 @@ class LazyState(State): "_context", ] - def __init__(self, row): # pylint: disable=super-init-not-called + def __init__(self, row) -> None: # pylint: disable=super-init-not-called """Init the lazy state.""" self._row = row self.entity_id = self._row.entity_id diff --git a/tests/components/recorder/db_schema_18.py b/tests/components/recorder/db_schema_18.py index db6fbb78f56..09cd41d9e33 100644 --- a/tests/components/recorder/db_schema_18.py +++ b/tests/components/recorder/db_schema_18.py @@ -369,7 +369,7 @@ class LazyState(State): "_context", ] - def __init__(self, row): # pylint: disable=super-init-not-called + def __init__(self, row) -> None: # pylint: disable=super-init-not-called """Init the lazy state.""" self._row = row self.entity_id = self._row.entity_id diff --git a/tests/components/recorder/db_schema_22.py b/tests/components/recorder/db_schema_22.py index cd0dc52a927..d05cb48ff6f 100644 --- a/tests/components/recorder/db_schema_22.py +++ b/tests/components/recorder/db_schema_22.py @@ -488,7 +488,7 @@ class LazyState(State): "_context", ] - def __init__(self, row): # pylint: disable=super-init-not-called + def __init__(self, row) -> None: # pylint: disable=super-init-not-called """Init the lazy state.""" self._row = row self.entity_id = self._row.entity_id diff --git a/tests/components/recorder/db_schema_23.py b/tests/components/recorder/db_schema_23.py index 9187d271216..9dffadaa0cc 100644 --- a/tests/components/recorder/db_schema_23.py +++ b/tests/components/recorder/db_schema_23.py @@ -478,7 +478,7 @@ class LazyState(State): "_context", ] - def __init__(self, row): # pylint: disable=super-init-not-called + def __init__(self, row) -> None: # pylint: disable=super-init-not-called """Init the lazy state.""" self._row = row self.entity_id = self._row.entity_id diff --git a/tests/components/recorder/db_schema_23_with_newer_columns.py b/tests/components/recorder/db_schema_23_with_newer_columns.py index 9f902523c64..4343f53d00d 100644 --- a/tests/components/recorder/db_schema_23_with_newer_columns.py +++ b/tests/components/recorder/db_schema_23_with_newer_columns.py @@ -602,7 +602,7 @@ class LazyState(State): "_context", ] - def __init__(self, row): # pylint: disable=super-init-not-called + def __init__(self, row) -> None: # pylint: disable=super-init-not-called """Init the lazy state.""" self._row = row self.entity_id = self._row.entity_id diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 04fe762c780..d850778d214 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -990,7 +990,7 @@ async def test_execute_stmt_lambda_element( all_calls = 0 class MockExecutor: - def __init__(self, stmt): + def __init__(self, stmt) -> None: assert isinstance(stmt, StatementLambdaElement) def all(self): diff --git a/tests/components/reddit/test_sensor.py b/tests/components/reddit/test_sensor.py index 52dac07d621..98cf2b79db3 100644 --- a/tests/components/reddit/test_sensor.py +++ b/tests/components/reddit/test_sensor.py @@ -66,7 +66,7 @@ INVALID_SORT_BY_CONFIG = { class ObjectView: """Use dict properties as attributes.""" - def __init__(self, d): + def __init__(self, d) -> None: """Set dict as internal dict.""" self.__dict__ = d diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 752bce3b960..ec12031ef96 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -179,7 +179,7 @@ def rest_api_fixture_non_ssl_only() -> Mock: class MockSamsungTVAsyncRest: """Mock for a MockSamsungTVAsyncRest.""" - def __init__(self, host, session, port, timeout): + def __init__(self, host, session, port, timeout) -> None: """Mock a MockSamsungTVAsyncRest.""" self.port = port self.host = host diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 4f14a2aa132..840fcb4dcdb 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -80,7 +80,7 @@ class SonosMockService: class SonosMockEvent: """Mock a sonos Event used in callbacks.""" - def __init__(self, soco, service, variables): + def __init__(self, soco, service, variables) -> None: """Initialize the instance.""" self.sid = f"{soco.uid}_sub0000000001" self.seq = "0" diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index ce66848a2b1..babd7c0b748 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -54,7 +54,7 @@ async def setup_component(hass: HomeAssistant) -> None: class HlsClient: """Test fixture for fetching the hls stream.""" - def __init__(self, http_client, parsed_url): + def __init__(self, http_client, parsed_url) -> None: """Initialize HlsClient.""" self.http_client = http_client self.parsed_url = parsed_url diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index d0f89364778..d61530f9076 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -100,7 +100,7 @@ def mock_stream_settings(hass: HomeAssistant) -> None: class FakeAvInputStream: """A fake pyav Stream.""" - def __init__(self, name, time_base): + def __init__(self, name, time_base) -> None: """Initialize the stream.""" self.name = name self.time_base = time_base @@ -142,7 +142,7 @@ class PacketSequence: exercise corner cases. """ - def __init__(self, num_packets): + def __init__(self, num_packets) -> None: """Initialize the sequence with the number of packets it provides.""" self.packet = 0 self.num_packets = num_packets @@ -181,7 +181,7 @@ class PacketSequence: class FakePyAvContainer: """A fake container returned by mock av.open for a stream.""" - def __init__(self, video_stream, audio_stream): + def __init__(self, video_stream, audio_stream) -> None: """Initialize the fake container.""" # Tests can override this to trigger different worker behavior self.packets = PacketSequence(0) @@ -220,7 +220,7 @@ class FakePyAvBuffer: """Create an output buffer that captures packets for test to examine.""" class FakeAvOutputStream: - def __init__(self, capture_packets): + def __init__(self, capture_packets) -> None: self.capture_packets = capture_packets self.type = "ignored-type" @@ -266,7 +266,7 @@ class FakePyAvBuffer: class MockPyAv: """Mocks out av.open.""" - def __init__(self, video=True, audio=False): + def __init__(self, video=True, audio=False) -> None: """Initialize the MockPyAv.""" video_stream = VIDEO_STREAM if video else None audio_stream = AUDIO_STREAM if audio else None diff --git a/tests/components/tomato/test_device_tracker.py b/tests/components/tomato/test_device_tracker.py index 099a2c2b40a..9484d3393d7 100644 --- a/tests/components/tomato/test_device_tracker.py +++ b/tests/components/tomato/test_device_tracker.py @@ -25,7 +25,7 @@ def mock_session_response(*args, **kwargs): """Mock data generation for session response.""" class MockSessionResponse: - def __init__(self, text, status_code): + def __init__(self, text, status_code) -> None: self.text = text self.status_code = status_code diff --git a/tests/components/venstar/__init__.py b/tests/components/venstar/__init__.py index 116a3be0925..6a40212b793 100644 --- a/tests/components/venstar/__init__.py +++ b/tests/components/venstar/__init__.py @@ -15,7 +15,7 @@ class VenstarColorTouchMock: pin=None, proto="http", SSLCert=False, - ): + ) -> None: """Initialize the Venstar library.""" self.status = {} self.model = "COLORTOUCH" diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index 5e0fac6c84a..c5e3a5d4931 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -83,7 +83,7 @@ def new_simple_controller_config( class ComponentFactory: """Factory class.""" - def __init__(self, vera_controller_class_mock): + def __init__(self, vera_controller_class_mock) -> None: """Initialize the factory.""" self.vera_controller_class_mock = vera_controller_class_mock diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index f33c7839c72..923509dea2c 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -30,7 +30,7 @@ from .const import ( class MockInput: """Mock Vizio device input.""" - def __init__(self, name): + def __init__(self, name) -> None: """Initialize mock Vizio device input.""" self.meta_name = name self.name = name diff --git a/tests/components/ws66i/test_media_player.py b/tests/components/ws66i/test_media_player.py index a66e79bf9e0..aa67ea24b63 100644 --- a/tests/components/ws66i/test_media_player.py +++ b/tests/components/ws66i/test_media_player.py @@ -73,7 +73,7 @@ class AttrDict(dict): class MockWs66i: """Mock for pyws66i object.""" - def __init__(self, fail_open=False, fail_zone_check=None): + def __init__(self, fail_open=False, fail_zone_check=None) -> None: """Init mock object.""" self.zones = defaultdict( lambda: AttrDict( diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 804b800aaef..d96da8f6854 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -25,7 +25,7 @@ def _create_zone_mock(name, url): class FakeYamahaDevice: """A fake Yamaha device.""" - def __init__(self, ctrl_url, name, zones=None): + def __init__(self, ctrl_url, name, zones=None) -> None: """Initialize the fake Yamaha device.""" self.ctrl_url = ctrl_url self.name = name diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 2de064cf567..bdd8cdda312 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -109,7 +109,7 @@ CONFIG_ENTRY_DATA = {CONF_ID: ID} class MockAsyncBulb: """A mock for yeelight.aio.AsyncBulb.""" - def __init__(self, model, bulb_type, cannot_connect): + def __init__(self, model, bulb_type, cannot_connect) -> None: """Init the mock.""" self.model = model self.bulb_type = bulb_type diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index b5eda374f01..de1db0e4847 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -174,7 +174,7 @@ class AiohttpClientMockResponse: headers=None, side_effect=None, closing=None, - ): + ) -> None: """Initialize a fake response.""" if json is not None: text = json_dumps(json) From 9b33d2f17ee688d3468462bc9e98f8e9634b4440 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Aug 2024 11:00:11 -0500 Subject: [PATCH 2330/2411] Fix paste error in homekit climate update (#123943) --- homeassistant/components/homekit/type_thermostats.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index a97f26b7abb..91bab2d470a 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -625,11 +625,10 @@ class Thermostat(HomeAccessory): ) # Set current operation mode for supported thermostats - if (hvac_action := attributes.get(ATTR_HVAC_ACTION)) and ( - homekit_hvac_action := HC_HASS_TO_HOMEKIT_ACTION.get(hvac_action), - HC_HEAT_COOL_OFF, - ): - self.char_current_heat_cool.set_value(homekit_hvac_action) + if hvac_action := attributes.get(ATTR_HVAC_ACTION): + self.char_current_heat_cool.set_value( + HC_HASS_TO_HOMEKIT_ACTION.get(hvac_action, HC_HEAT_COOL_OFF) + ) # Update current temperature current_temp = _get_current_temperature(new_state, self._unit) From 0790611b93b506c864810d0b7ba363eac40a9d13 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 14 Aug 2024 20:39:15 +0200 Subject: [PATCH 2331/2411] Fix PI-Hole update entity when no update available (#123930) show installed version when no update available --- homeassistant/components/pi_hole/update.py | 8 +++- tests/components/pi_hole/__init__.py | 23 +++++++++-- tests/components/pi_hole/test_config_flow.py | 2 +- tests/components/pi_hole/test_update.py | 43 +++++++++++++++++++- 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/pi_hole/update.py b/homeassistant/components/pi_hole/update.py index db78d3ab0a5..c1a435f628c 100644 --- a/homeassistant/components/pi_hole/update.py +++ b/homeassistant/components/pi_hole/update.py @@ -22,6 +22,7 @@ class PiHoleUpdateEntityDescription(UpdateEntityDescription): installed_version: Callable[[dict], str | None] = lambda api: None latest_version: Callable[[dict], str | None] = lambda api: None + has_update: Callable[[dict], bool | None] = lambda api: None release_base_url: str | None = None title: str | None = None @@ -34,6 +35,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, installed_version=lambda versions: versions.get("core_current"), latest_version=lambda versions: versions.get("core_latest"), + has_update=lambda versions: versions.get("core_update"), release_base_url="https://github.com/pi-hole/pi-hole/releases/tag", ), PiHoleUpdateEntityDescription( @@ -43,6 +45,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, installed_version=lambda versions: versions.get("web_current"), latest_version=lambda versions: versions.get("web_latest"), + has_update=lambda versions: versions.get("web_update"), release_base_url="https://github.com/pi-hole/AdminLTE/releases/tag", ), PiHoleUpdateEntityDescription( @@ -52,6 +55,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, installed_version=lambda versions: versions.get("FTL_current"), latest_version=lambda versions: versions.get("FTL_latest"), + has_update=lambda versions: versions.get("FTL_update"), release_base_url="https://github.com/pi-hole/FTL/releases/tag", ), ) @@ -110,7 +114,9 @@ class PiHoleUpdateEntity(PiHoleEntity, UpdateEntity): def latest_version(self) -> str | None: """Latest version available for install.""" if isinstance(self.api.versions, dict): - return self.entity_description.latest_version(self.api.versions) + if self.entity_description.has_update(self.api.versions): + return self.entity_description.latest_version(self.api.versions) + return self.installed_version return None @property diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index 38231778624..993f6a2571c 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -33,7 +33,7 @@ ZERO_DATA = { "unique_domains": 0, } -SAMPLE_VERSIONS = { +SAMPLE_VERSIONS_WITH_UPDATES = { "core_current": "v5.5", "core_latest": "v5.6", "core_update": True, @@ -45,6 +45,18 @@ SAMPLE_VERSIONS = { "FTL_update": True, } +SAMPLE_VERSIONS_NO_UPDATES = { + "core_current": "v5.5", + "core_latest": "v5.5", + "core_update": False, + "web_current": "v5.7", + "web_latest": "v5.7", + "web_update": False, + "FTL_current": "v5.10", + "FTL_latest": "v5.10", + "FTL_update": False, +} + HOST = "1.2.3.4" PORT = 80 LOCATION = "location" @@ -103,7 +115,9 @@ CONFIG_ENTRY_WITHOUT_API_KEY = { SWITCH_ENTITY_ID = "switch.pi_hole" -def _create_mocked_hole(raise_exception=False, has_versions=True, has_data=True): +def _create_mocked_hole( + raise_exception=False, has_versions=True, has_update=True, has_data=True +): mocked_hole = MagicMock() type(mocked_hole).get_data = AsyncMock( side_effect=HoleError("") if raise_exception else None @@ -118,7 +132,10 @@ def _create_mocked_hole(raise_exception=False, has_versions=True, has_data=True) else: mocked_hole.data = [] if has_versions: - mocked_hole.versions = SAMPLE_VERSIONS + if has_update: + mocked_hole.versions = SAMPLE_VERSIONS_WITH_UPDATES + else: + mocked_hole.versions = SAMPLE_VERSIONS_NO_UPDATES else: mocked_hole.versions = None return mocked_hole diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index 326b01b9a7a..d13712d6f76 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -96,7 +96,7 @@ async def test_flow_user_without_api_key(hass: HomeAssistant) -> None: async def test_flow_user_invalid(hass: HomeAssistant) -> None: """Test user initialized flow with invalid server.""" - mocked_hole = _create_mocked_hole(True) + mocked_hole = _create_mocked_hole(raise_exception=True) with _patch_config_flow_hole(mocked_hole): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_FLOW_USER diff --git a/tests/components/pi_hole/test_update.py b/tests/components/pi_hole/test_update.py index 091b553c475..705e9f9c08d 100644 --- a/tests/components/pi_hole/test_update.py +++ b/tests/components/pi_hole/test_update.py @@ -1,7 +1,7 @@ """Test pi_hole component.""" from homeassistant.components import pi_hole -from homeassistant.const import STATE_ON, STATE_UNKNOWN +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant from . import CONFIG_DATA_DEFAULTS, _create_mocked_hole, _patch_init_hole @@ -80,3 +80,44 @@ async def test_update_no_versions(hass: HomeAssistant) -> None: assert state.attributes["installed_version"] is None assert state.attributes["latest_version"] is None assert state.attributes["release_url"] is None + + +async def test_update_no_updates(hass: HomeAssistant) -> None: + """Tests update entity when no latest data available.""" + mocked_hole = _create_mocked_hole(has_versions=True, has_update=False) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONFIG_DATA_DEFAULTS) + entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole): + assert await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + state = hass.states.get("update.pi_hole_core_update_available") + assert state.name == "Pi-Hole Core update available" + assert state.state == STATE_OFF + assert state.attributes["installed_version"] == "v5.5" + assert state.attributes["latest_version"] == "v5.5" + assert ( + state.attributes["release_url"] + == "https://github.com/pi-hole/pi-hole/releases/tag/v5.5" + ) + + state = hass.states.get("update.pi_hole_ftl_update_available") + assert state.name == "Pi-Hole FTL update available" + assert state.state == STATE_OFF + assert state.attributes["installed_version"] == "v5.10" + assert state.attributes["latest_version"] == "v5.10" + assert ( + state.attributes["release_url"] + == "https://github.com/pi-hole/FTL/releases/tag/v5.10" + ) + + state = hass.states.get("update.pi_hole_web_update_available") + assert state.name == "Pi-Hole Web update available" + assert state.state == STATE_OFF + assert state.attributes["installed_version"] == "v5.7" + assert state.attributes["latest_version"] == "v5.7" + assert ( + state.attributes["release_url"] + == "https://github.com/pi-hole/AdminLTE/releases/tag/v5.7" + ) From 9c4677a3c650997660be99a898546794dd040c9c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Aug 2024 21:22:06 +0200 Subject: [PATCH 2332/2411] Add comment clarifying recorder migration to schema version 16 (#123902) --- homeassistant/components/recorder/migration.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index ccdaf3082e0..cbe0dcd7258 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -1116,6 +1116,10 @@ class _SchemaVersion16Migrator(_SchemaVersionMigrator, target_version=16): SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL, ): + # Version 16 changes settings for the foreign key constraint on + # states.old_state_id. Dropping the constraint is not really correct + # we should have recreated it instead. Recreating the constraint now + # happens in the migration to schema version 45. _drop_foreign_key_constraints( self.session_maker, self.engine, TABLE_STATES, "old_state_id" ) From 58851f0048cadbca502938aaa0f1bbba9997153a Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Wed, 14 Aug 2024 21:36:11 +0200 Subject: [PATCH 2333/2411] Bump pypck to 0.7.20 (#123948) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 6153ecf4540..44a4d683c81 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/lcn", "iot_class": "local_push", "loggers": ["pypck"], - "requirements": ["pypck==0.7.17"] + "requirements": ["pypck==0.7.20"] } diff --git a/requirements_all.txt b/requirements_all.txt index c5fe1e2171a..6908004c2d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2097,7 +2097,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.17 +pypck==0.7.20 # homeassistant.components.pjlink pypjlink2==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b84660fb0b0..189577764c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1678,7 +1678,7 @@ pyoverkiz==1.13.14 pyownet==0.10.0.post1 # homeassistant.components.lcn -pypck==0.7.17 +pypck==0.7.20 # homeassistant.components.pjlink pypjlink2==1.2.1 From aee1be1e643df46ea22bcb2ede8db25cde9e1a7e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 14 Aug 2024 21:47:47 +0200 Subject: [PATCH 2334/2411] Use `elif` in alexa handlers code to avoid additional checks (#123853) --- homeassistant/components/alexa/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 53bf6702138..3571f436ff6 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1206,7 +1206,7 @@ async def async_api_set_mode( raise AlexaInvalidValueError(msg) # Remote Activity - if instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}": + elif instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}": activity = mode.split(".")[1] activities: list[str] | None = entity.attributes.get(remote.ATTR_ACTIVITY_LIST) if activity != PRESET_MODE_NA and activities and activity in activities: From 392f64d33e23c5087ac5f26a3fa0677ea338c069 Mon Sep 17 00:00:00 2001 From: ilan <31193909+iloveicedgreentea@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:06:57 -0400 Subject: [PATCH 2335/2411] Fix Madvr sensor values on startup (#122479) * fix: add startup values * fix: update snap * fix: use native value to show None --- homeassistant/components/madvr/sensor.py | 13 ++++++++++++- tests/components/madvr/test_sensors.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/madvr/sensor.py b/homeassistant/components/madvr/sensor.py index 6f0933ac879..047b8bb83e6 100644 --- a/homeassistant/components/madvr/sensor.py +++ b/homeassistant/components/madvr/sensor.py @@ -277,4 +277,15 @@ class MadvrSensor(MadVREntity, SensorEntity): @property def native_value(self) -> float | str | None: """Return the state of the sensor.""" - return self.entity_description.value_fn(self.coordinator) + val = self.entity_description.value_fn(self.coordinator) + # check if sensor is enum + if self.entity_description.device_class == SensorDeviceClass.ENUM: + if ( + self.entity_description.options + and val in self.entity_description.options + ): + return val + # return None for values that are not in the options + return None + + return val diff --git a/tests/components/madvr/test_sensors.py b/tests/components/madvr/test_sensors.py index 25dcc1cdcca..ddc01fc737a 100644 --- a/tests/components/madvr/test_sensors.py +++ b/tests/components/madvr/test_sensors.py @@ -93,3 +93,16 @@ async def test_sensor_setup_and_states( # test get_temperature ValueError assert get_temperature(None, "temp_key") is None + + # test startup placeholder values + update_callback({"outgoing_bit_depth": "0bit"}) + await hass.async_block_till_done() + assert ( + hass.states.get("sensor.madvr_envy_outgoing_bit_depth").state == STATE_UNKNOWN + ) + + update_callback({"outgoing_color_space": "?"}) + await hass.async_block_till_done() + assert ( + hass.states.get("sensor.madvr_envy_outgoing_color_space").state == STATE_UNKNOWN + ) From e6ed3c8c5ccbecb31003845a596a968aa15898a7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Aug 2024 22:37:23 +0200 Subject: [PATCH 2336/2411] Raise on database error in recorder.migration function (#123644) * Raise on database error in recorder.migration._update_states_table_with_foreign_key_options * Improve test coverage * Fix test * Fix test --- .../components/recorder/migration.py | 1 + tests/components/recorder/test_migrate.py | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index cbe0dcd7258..ebec536dc48 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -639,6 +639,7 @@ def _update_states_table_with_foreign_key_options( _LOGGER.exception( "Could not update foreign options in %s table", TABLE_STATES ) + raise def _drop_foreign_key_constraints( diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 625a5023287..76387f56e9f 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -199,6 +199,64 @@ async def test_database_migration_failed( assert len(mock_dismiss.mock_calls) == expected_pn_dismiss +@pytest.mark.parametrize( + ( + "func_to_patch", + "expected_setup_result", + "expected_pn_create", + "expected_pn_dismiss", + ), + [ + ("DropConstraint", False, 1, 0), # This makes migration to step 11 fail + ], +) +@pytest.mark.skip_on_db_engine(["sqlite"]) +@pytest.mark.usefixtures("skip_by_db_engine") +async def test_database_migration_failed_step_11( + hass: HomeAssistant, + async_setup_recorder_instance: RecorderInstanceGenerator, + func_to_patch: str, + expected_setup_result: bool, + expected_pn_create: int, + expected_pn_dismiss: int, +) -> None: + """Test we notify if the migration fails.""" + assert recorder.util.async_migration_in_progress(hass) is False + + with ( + patch( + "homeassistant.components.recorder.core.create_engine", + new=create_engine_test, + ), + patch( + f"homeassistant.components.recorder.migration.{func_to_patch}", + side_effect=OperationalError( + None, None, OSError("No space left on device") + ), + ), + patch( + "homeassistant.components.persistent_notification.create", + side_effect=pn.create, + ) as mock_create, + patch( + "homeassistant.components.persistent_notification.dismiss", + side_effect=pn.dismiss, + ) as mock_dismiss, + ): + await async_setup_recorder_instance( + hass, wait_recorder=False, expected_setup_result=expected_setup_result + ) + 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(recorder.get_instance(hass).join) + await hass.async_block_till_done() + + assert recorder.util.async_migration_in_progress(hass) is False + assert len(mock_create.mock_calls) == expected_pn_create + assert len(mock_dismiss.mock_calls) == expected_pn_dismiss + + @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) @pytest.mark.usefixtures("skip_by_db_engine") async def test_live_database_migration_encounters_corruption( From e6b3d35cdf67d9d3410b336c0bce5728898b2754 Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 14 Aug 2024 22:53:29 +0200 Subject: [PATCH 2337/2411] Remove unnecessary check in fritz light (#123829) * Remove unnecessary check in fritz light * Revert remove SUPPORTED_COLOR_MODES --- homeassistant/components/fritzbox/light.py | 7 ++----- tests/components/fritzbox/__init__.py | 1 + 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py index 65446dc3e04..1009b0fb368 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FritzboxDataUpdateCoordinator, FritzBoxDeviceEntity -from .const import COLOR_MODE, COLOR_TEMP_MODE, LOGGER +from .const import COLOR_MODE, LOGGER from .coordinator import FritzboxConfigEntry SUPPORTED_COLOR_MODES = {ColorMode.COLOR_TEMP, ColorMode.HS} @@ -80,11 +80,8 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity): return (hue, float(saturation) * 100.0 / 255.0) @property - def color_temp_kelvin(self) -> int | None: + def color_temp_kelvin(self) -> int: """Return the CT color value.""" - if self.data.color_mode != COLOR_TEMP_MODE: - return None - return self.data.color_temp # type: ignore [no-any-return] @property diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 09e0aeaee51..bd68615212d 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -180,6 +180,7 @@ class FritzDeviceLightMock(FritzEntityBaseMock): level = 100 present = True state = True + color_temp = None class FritzDeviceCoverMock(FritzEntityBaseMock): From 667414a457360c9c21d14594a5e357eb2e8ed906 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Aug 2024 08:25:18 +0200 Subject: [PATCH 2338/2411] Raise on database error in recorder.migration._drop_foreign_key_constraints (#123645) * Raise on database error in recorder.migration._drop_foreign_key_constraints * Fix test * Fix test * Revert "Fix test" This reverts commit 940b8cb506e912826d43d09d7697c10888bdf685. * Update test * Improve test coverage * Disable test for SQLite --- .../components/recorder/migration.py | 18 ++-- tests/components/recorder/conftest.py | 90 ++++++++++++++++--- tests/components/recorder/test_migrate.py | 78 +++++++++++++++- .../components/recorder/test_v32_migration.py | 75 ++++++++++------ 4 files changed, 209 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index ebec536dc48..c34d05b3ade 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -644,7 +644,7 @@ def _update_states_table_with_foreign_key_options( def _drop_foreign_key_constraints( session_maker: Callable[[], Session], engine: Engine, table: str, column: str -) -> tuple[bool, list[tuple[str, str, ReflectedForeignKeyConstraint]]]: +) -> list[tuple[str, str, ReflectedForeignKeyConstraint]]: """Drop foreign key constraints for a table on specific columns. This is not supported for SQLite because it does not support @@ -671,7 +671,6 @@ def _drop_foreign_key_constraints( if foreign_key["name"] and foreign_key["constrained_columns"] == [column] ] - fk_remove_ok = True for drop in drops: with session_scope(session=session_maker()) as session: try: @@ -683,9 +682,9 @@ def _drop_foreign_key_constraints( TABLE_STATES, column, ) - fk_remove_ok = False + raise - return fk_remove_ok, dropped_constraints + return dropped_constraints def _restore_foreign_key_constraints( @@ -2183,9 +2182,14 @@ def cleanup_legacy_states_event_ids(instance: Recorder) -> bool: # so we have to rebuild the table fk_remove_ok = rebuild_sqlite_table(session_maker, instance.engine, States) else: - fk_remove_ok, _ = _drop_foreign_key_constraints( - session_maker, instance.engine, TABLE_STATES, "event_id" - ) + try: + _drop_foreign_key_constraints( + session_maker, instance.engine, TABLE_STATES, "event_id" + ) + except (InternalError, OperationalError): + fk_remove_ok = False + else: + fk_remove_ok = True if fk_remove_ok: _drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX) instance.use_legacy_events_index = False diff --git a/tests/components/recorder/conftest.py b/tests/components/recorder/conftest.py index f562ba163ba..d66cf4fe2ec 100644 --- a/tests/components/recorder/conftest.py +++ b/tests/components/recorder/conftest.py @@ -1,12 +1,15 @@ """Fixtures for the recorder component tests.""" -from collections.abc import AsyncGenerator, Generator +from collections.abc import Callable, Generator +from contextlib import contextmanager from dataclasses import dataclass from functools import partial import threading from unittest.mock import Mock, patch import pytest +from sqlalchemy.engine import Engine +from sqlalchemy.orm.session import Session from homeassistant.components import recorder from homeassistant.components.recorder import db_schema @@ -57,31 +60,69 @@ def recorder_dialect_name(hass: HomeAssistant, db_engine: str) -> Generator[None class InstrumentedMigration: """Container to aid controlling migration progress.""" - migration_done: threading.Event + live_migration_done: threading.Event + live_migration_done_stall: threading.Event migration_stall: threading.Event migration_started: threading.Event migration_version: int | None + non_live_migration_done: threading.Event + non_live_migration_done_stall: threading.Event apply_update_mock: Mock + stall_on_schema_version: int + apply_update_stalled: threading.Event -@pytest.fixture -async def instrument_migration( +@pytest.fixture(name="instrument_migration") +def instrument_migration_fixture( hass: HomeAssistant, -) -> AsyncGenerator[InstrumentedMigration]: +) -> Generator[InstrumentedMigration]: + """Instrument recorder migration.""" + with instrument_migration(hass) as instrumented_migration: + yield instrumented_migration + + +@contextmanager +def instrument_migration( + hass: HomeAssistant, +) -> Generator[InstrumentedMigration]: """Instrument recorder migration.""" real_migrate_schema_live = recorder.migration.migrate_schema_live real_migrate_schema_non_live = recorder.migration.migrate_schema_non_live real_apply_update = recorder.migration._apply_update - def _instrument_migrate_schema(real_func, *args): + def _instrument_migrate_schema_live(real_func, *args): + """Control migration progress and check results.""" + return _instrument_migrate_schema( + real_func, + args, + instrumented_migration.live_migration_done, + instrumented_migration.live_migration_done_stall, + ) + + def _instrument_migrate_schema_non_live(real_func, *args): + """Control migration progress and check results.""" + return _instrument_migrate_schema( + real_func, + args, + instrumented_migration.non_live_migration_done, + instrumented_migration.non_live_migration_done_stall, + ) + + def _instrument_migrate_schema( + real_func, + args, + migration_done: threading.Event, + migration_done_stall: threading.Event, + ): """Control migration progress and check results.""" instrumented_migration.migration_started.set() try: migration_result = real_func(*args) except Exception: - instrumented_migration.migration_done.set() + migration_done.set() + migration_done_stall.wait() raise # Check and report the outcome of the migration; if migration fails @@ -93,22 +134,36 @@ async def instrument_migration( .first() ) instrumented_migration.migration_version = res.schema_version - instrumented_migration.migration_done.set() + migration_done.set() + migration_done_stall.wait() return migration_result - def _instrument_apply_update(*args): + def _instrument_apply_update( + instance: recorder.Recorder, + hass: HomeAssistant, + engine: Engine, + session_maker: Callable[[], Session], + new_version: int, + old_version: int, + ): """Control migration progress.""" - instrumented_migration.migration_stall.wait() - real_apply_update(*args) + if new_version == instrumented_migration.stall_on_schema_version: + instrumented_migration.apply_update_stalled.set() + instrumented_migration.migration_stall.wait() + real_apply_update( + instance, hass, engine, session_maker, new_version, old_version + ) with ( patch( "homeassistant.components.recorder.migration.migrate_schema_live", - wraps=partial(_instrument_migrate_schema, real_migrate_schema_live), + wraps=partial(_instrument_migrate_schema_live, real_migrate_schema_live), ), patch( "homeassistant.components.recorder.migration.migrate_schema_non_live", - wraps=partial(_instrument_migrate_schema, real_migrate_schema_non_live), + wraps=partial( + _instrument_migrate_schema_non_live, real_migrate_schema_non_live + ), ), patch( "homeassistant.components.recorder.migration._apply_update", @@ -116,11 +171,18 @@ async def instrument_migration( ) as apply_update_mock, ): instrumented_migration = InstrumentedMigration( - migration_done=threading.Event(), + live_migration_done=threading.Event(), + live_migration_done_stall=threading.Event(), migration_stall=threading.Event(), migration_started=threading.Event(), migration_version=None, + non_live_migration_done=threading.Event(), + non_live_migration_done_stall=threading.Event(), apply_update_mock=apply_update_mock, + stall_on_schema_version=1, + apply_update_stalled=threading.Event(), ) + instrumented_migration.live_migration_done_stall.set() + instrumented_migration.non_live_migration_done_stall.set() yield instrumented_migration diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 76387f56e9f..eadcd2bd9ad 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -257,6 +257,76 @@ async def test_database_migration_failed_step_11( assert len(mock_dismiss.mock_calls) == expected_pn_dismiss +@pytest.mark.parametrize( + ( + "func_to_patch", + "expected_setup_result", + "expected_pn_create", + "expected_pn_dismiss", + ), + [ + ("DropConstraint", False, 2, 1), # This makes migration to step 44 fail + ], +) +@pytest.mark.skip_on_db_engine(["sqlite"]) +@pytest.mark.usefixtures("skip_by_db_engine") +async def test_database_migration_failed_step_44( + hass: HomeAssistant, + async_setup_recorder_instance: RecorderInstanceGenerator, + instrument_migration: InstrumentedMigration, + func_to_patch: str, + expected_setup_result: bool, + expected_pn_create: int, + expected_pn_dismiss: int, +) -> None: + """Test we notify if the migration fails.""" + assert recorder.util.async_migration_in_progress(hass) is False + instrument_migration.stall_on_schema_version = 44 + + with ( + patch( + "homeassistant.components.recorder.core.create_engine", + new=create_engine_test, + ), + patch( + "homeassistant.components.persistent_notification.create", + side_effect=pn.create, + ) as mock_create, + patch( + "homeassistant.components.persistent_notification.dismiss", + side_effect=pn.dismiss, + ) as mock_dismiss, + ): + await async_setup_recorder_instance( + hass, + wait_recorder=False, + wait_recorder_setup=False, + expected_setup_result=expected_setup_result, + ) + # Wait for migration to reach schema version 44 + await hass.async_add_executor_job( + instrument_migration.apply_update_stalled.wait + ) + + # Make it fail + with patch( + f"homeassistant.components.recorder.migration.{func_to_patch}", + side_effect=OperationalError( + None, None, OSError("No space left on device") + ), + ): + instrument_migration.migration_stall.set() + 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(recorder.get_instance(hass).join) + await hass.async_block_till_done() + + assert recorder.util.async_migration_in_progress(hass) is False + assert len(mock_create.mock_calls) == expected_pn_create + assert len(mock_dismiss.mock_calls) == expected_pn_dismiss + + @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) @pytest.mark.usefixtures("skip_by_db_engine") async def test_live_database_migration_encounters_corruption( @@ -611,7 +681,7 @@ async def test_schema_migrate( assert recorder.util.async_migration_is_live(hass) == live instrument_migration.migration_stall.set() await hass.async_block_till_done() - await hass.async_add_executor_job(instrument_migration.migration_done.wait) + await hass.async_add_executor_job(instrument_migration.live_migration_done.wait) await async_wait_recording_done(hass) assert instrument_migration.migration_version == db_schema.SCHEMA_VERSION assert setup_run.called @@ -963,7 +1033,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: for table, column, _, _ in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column - )[1] + ) ] assert dropped_constraints_1 == expected_dropped_constraints[db_engine] @@ -975,7 +1045,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: for table, column, _, _ in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column - )[1] + ) ] assert dropped_constraints_2 == [] @@ -994,7 +1064,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: for table, column, _, _ in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column - )[1] + ) ] assert dropped_constraints_3 == expected_dropped_constraints[db_engine] diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index 5266e55851c..c9ba330b758 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -19,6 +19,7 @@ from homeassistant.core import Event, EventOrigin, State import homeassistant.util.dt as dt_util from .common import async_wait_recording_done +from .conftest import instrument_migration from tests.common import async_test_home_assistant from tests.typing import RecorderInstanceGenerator @@ -637,6 +638,10 @@ async def test_out_of_disk_space_while_removing_foreign_key( This case tests the migration still happens if ix_states_event_id is removed from the states table. + + Note that the test is somewhat forced; the states.event_id foreign key constraint is + removed when migrating to schema version 44, inspecting the schema in + cleanup_legacy_states_event_ids is not likely to fail. """ importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] @@ -737,36 +742,52 @@ async def test_out_of_disk_space_while_removing_foreign_key( assert "ix_states_entity_id_last_updated_ts" in states_index_names - # Simulate out of disk space while removing the foreign key from the states table by - # - patching DropConstraint to raise InternalError for MySQL and PostgreSQL - with ( - patch( - "homeassistant.components.recorder.migration.DropConstraint", - side_effect=OperationalError( - None, None, OSError("No space left on device") - ), - ), - ): - async with ( - async_test_home_assistant() as hass, - async_test_recorder(hass) as instance, - ): - await hass.async_block_till_done() + async with async_test_home_assistant() as hass: + with instrument_migration(hass) as instrumented_migration: + # Allow migration to start, but stall when live migration is completed + instrumented_migration.migration_stall.set() + instrumented_migration.live_migration_done_stall.clear() - # We need to wait for all the migration tasks to complete - # before we can check the database. - for _ in range(number_of_migrations): - await instance.async_block_till_done() - await async_wait_recording_done(hass) + async with async_test_recorder(hass, wait_recorder=False) as instance: + await hass.async_block_till_done() - states_indexes = await instance.async_add_executor_job( - _get_states_index_names - ) - states_index_names = {index["name"] for index in states_indexes} - assert instance.use_legacy_events_index is True - assert await instance.async_add_executor_job(_get_event_id_foreign_keys) + # Wait for live migration to complete + await hass.async_add_executor_job( + instrumented_migration.live_migration_done.wait + ) - await hass.async_stop() + # Simulate out of disk space while removing the foreign key from the states table by + # - patching DropConstraint to raise InternalError for MySQL and PostgreSQL + with ( + patch( + "homeassistant.components.recorder.migration.sqlalchemy.inspect", + side_effect=OperationalError( + None, None, OSError("No space left on device") + ), + ), + ): + instrumented_migration.live_migration_done_stall.set() + # We need to wait for all the migration tasks to complete + # before we can check the database. + for _ in range(number_of_migrations): + await instance.async_block_till_done() + await async_wait_recording_done(hass) + + states_indexes = await instance.async_add_executor_job( + _get_states_index_names + ) + states_index_names = {index["name"] for index in states_indexes} + assert instance.use_legacy_events_index is True + # The states.event_id foreign key constraint was removed when + # migration to schema version 44 + assert ( + await instance.async_add_executor_job( + _get_event_id_foreign_keys + ) + is None + ) + + await hass.async_stop() # Now run it again to verify the table rebuild tries again caplog.clear() From 9911aa4ede2e81bf9df5ff924518555bc8c625a8 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 15 Aug 2024 08:29:06 +0200 Subject: [PATCH 2339/2411] Enable confirm error button in Husqvarna Automower by default (#123927) --- homeassistant/components/husqvarna_automower/button.py | 5 +++-- tests/components/husqvarna_automower/test_button.py | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/button.py b/homeassistant/components/husqvarna_automower/button.py index a9747108393..810dd4df92d 100644 --- a/homeassistant/components/husqvarna_automower/button.py +++ b/homeassistant/components/husqvarna_automower/button.py @@ -25,7 +25,9 @@ async def async_setup_entry( """Set up button platform.""" coordinator = entry.runtime_data async_add_entities( - AutomowerButtonEntity(mower_id, coordinator) for mower_id in coordinator.data + AutomowerButtonEntity(mower_id, coordinator) + for mower_id in coordinator.data + if coordinator.data[mower_id].capabilities.can_confirm_error ) @@ -33,7 +35,6 @@ class AutomowerButtonEntity(AutomowerAvailableEntity, ButtonEntity): """Defining the AutomowerButtonEntity.""" _attr_translation_key = "confirm_error" - _attr_entity_registry_enabled_default = False def __init__( self, diff --git a/tests/components/husqvarna_automower/test_button.py b/tests/components/husqvarna_automower/test_button.py index 6cc465df74b..5cbb9b893a8 100644 --- a/tests/components/husqvarna_automower/test_button.py +++ b/tests/components/husqvarna_automower/test_button.py @@ -34,7 +34,6 @@ from tests.common import ( @pytest.mark.freeze_time(datetime.datetime(2024, 2, 29, 11, tzinfo=datetime.UTC)) -@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_button_states_and_commands( hass: HomeAssistant, mock_automower_client: AsyncMock, From b042ebe4ff577a53ebc24611f34fd1ed45eebf7e Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 15 Aug 2024 08:37:10 +0200 Subject: [PATCH 2340/2411] Rename KNX Climate preset modes according to specification (#123964) * Rename KNX Climate preset modes according to specification * change icon for "standby" --- homeassistant/components/knx/climate.py | 45 ++++++++--------------- homeassistant/components/knx/const.py | 21 +---------- homeassistant/components/knx/icons.json | 14 +++++++ homeassistant/components/knx/strings.json | 16 ++++++++ tests/components/knx/test_climate.py | 17 ++++----- 5 files changed, 54 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index abce143c760..4932df55087 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -10,11 +10,10 @@ from xknx.devices import ( ClimateMode as XknxClimateMode, Device as XknxDevice, ) -from xknx.dpt.dpt_20 import HVACControllerMode +from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode from homeassistant import config_entries from homeassistant.components.climate import ( - PRESET_AWAY, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -32,19 +31,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType from . import KNXModule -from .const import ( - CONTROLLER_MODES, - CURRENT_HVAC_ACTIONS, - DATA_KNX_CONFIG, - DOMAIN, - PRESET_MODES, -) +from .const import CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxYamlEntity from .schema import ClimateSchema ATTR_COMMAND_VALUE = "command_value" CONTROLLER_MODES_INV = {value: key for key, value in CONTROLLER_MODES.items()} -PRESET_MODES_INV = {value: key for key, value in PRESET_MODES.items()} async def async_setup_entry( @@ -142,6 +134,7 @@ class KNXClimate(KnxYamlEntity, ClimateEntity): _device: XknxClimate _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_translation_key = "knx_climate" _enable_turn_on_off_backwards_compatibility = False def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: @@ -165,8 +158,14 @@ class KNXClimate(KnxYamlEntity, ClimateEntity): ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) - if self.preset_modes: + if ( + self._device.mode is not None + and self._device.mode.operation_modes # empty list when not writable + ): self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE + self._attr_preset_modes = [ + mode.name.lower() for mode in self._device.mode.operation_modes + ] self._attr_target_temperature_step = self._device.temperature_step self._attr_unique_id = ( f"{self._device.temperature.group_address_state}_" @@ -309,32 +308,18 @@ class KNXClimate(KnxYamlEntity, ClimateEntity): Requires ClimateEntityFeature.PRESET_MODE. """ if self._device.mode is not None and self._device.mode.supports_operation_mode: - return PRESET_MODES.get(self._device.mode.operation_mode, PRESET_AWAY) + return self._device.mode.operation_mode.name.lower() return None - @property - def preset_modes(self) -> list[str] | None: - """Return a list of available preset modes. - - Requires ClimateEntityFeature.PRESET_MODE. - """ - if self._device.mode is None: - return None - - presets = [ - PRESET_MODES.get(operation_mode) - for operation_mode in self._device.mode.operation_modes - ] - return list(filter(None, presets)) - async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if ( self._device.mode is not None - and self._device.mode.supports_operation_mode - and (knx_operation_mode := PRESET_MODES_INV.get(preset_mode)) is not None + and self._device.mode.operation_modes # empty list when not writable ): - await self._device.mode.set_operation_mode(knx_operation_mode) + await self._device.mode.set_operation_mode( + HVACOperationMode[preset_mode.upper()] + ) self.async_write_ha_state() @property diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 6400f0fe466..9ceb18385cb 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -6,18 +6,10 @@ from collections.abc import Awaitable, Callable from enum import Enum from typing import Final, TypedDict -from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode +from xknx.dpt.dpt_20 import HVACControllerMode from xknx.telegram import Telegram -from homeassistant.components.climate import ( - PRESET_AWAY, - PRESET_COMFORT, - PRESET_ECO, - PRESET_NONE, - PRESET_SLEEP, - HVACAction, - HVACMode, -) +from homeassistant.components.climate import HVACAction, HVACMode from homeassistant.const import Platform DOMAIN: Final = "knx" @@ -174,12 +166,3 @@ CURRENT_HVAC_ACTIONS: Final = { HVACMode.FAN_ONLY: HVACAction.FAN, HVACMode.DRY: HVACAction.DRYING, } - -PRESET_MODES: Final = { - # Map DPT 20.102 HVAC operating modes to HA presets - HVACOperationMode.AUTO: PRESET_NONE, - HVACOperationMode.BUILDING_PROTECTION: PRESET_ECO, - HVACOperationMode.ECONOMY: PRESET_SLEEP, - HVACOperationMode.STANDBY: PRESET_AWAY, - HVACOperationMode.COMFORT: PRESET_COMFORT, -} diff --git a/homeassistant/components/knx/icons.json b/homeassistant/components/knx/icons.json index 736923375ee..2aee34219f6 100644 --- a/homeassistant/components/knx/icons.json +++ b/homeassistant/components/knx/icons.json @@ -1,5 +1,19 @@ { "entity": { + "climate": { + "knx_climate": { + "state_attributes": { + "preset_mode": { + "state": { + "comfort": "mdi:sofa", + "standby": "mdi:home-export-outline", + "economy": "mdi:leaf", + "building_protection": "mdi:sun-snowflake-variant" + } + } + } + } + }, "sensor": { "individual_address": { "default": "mdi:router-network" diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index d6e1e2f49f0..8d8692f6b7a 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -267,6 +267,22 @@ } }, "entity": { + "climate": { + "knx_climate": { + "state_attributes": { + "preset_mode": { + "name": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::name%]", + "state": { + "auto": "Auto", + "comfort": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]", + "standby": "Standby", + "economy": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::eco%]", + "building_protection": "Building protection" + } + } + } + } + }, "sensor": { "individual_address": { "name": "[%key:component::knx::config::step::routing::data::individual_address%]" diff --git a/tests/components/knx/test_climate.py b/tests/components/knx/test_climate.py index 9f198b48bd4..ec0498dc447 100644 --- a/tests/components/knx/test_climate.py +++ b/tests/components/knx/test_climate.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.climate import PRESET_ECO, PRESET_SLEEP, HVACMode +from homeassistant.components.climate import HVACMode from homeassistant.components.knx.schema import ClimateSchema from homeassistant.const import CONF_NAME, STATE_IDLE from homeassistant.core import HomeAssistant @@ -331,7 +331,6 @@ async def test_climate_preset_mode( } } ) - events = async_capture_events(hass, "state_changed") # StateUpdater initialize state # StateUpdater semaphore allows 2 concurrent requests @@ -340,30 +339,28 @@ async def test_climate_preset_mode( await knx.receive_response("1/2/3", RAW_FLOAT_21_0) await knx.receive_response("1/2/5", RAW_FLOAT_22_0) await knx.assert_read("1/2/7") - await knx.receive_response("1/2/7", (0x01,)) - events.clear() + await knx.receive_response("1/2/7", (0x01,)) # comfort + knx.assert_state("climate.test", HVACMode.HEAT, preset_mode="comfort") # set preset mode await hass.services.async_call( "climate", "set_preset_mode", - {"entity_id": "climate.test", "preset_mode": PRESET_ECO}, + {"entity_id": "climate.test", "preset_mode": "building_protection"}, blocking=True, ) await knx.assert_write("1/2/6", (0x04,)) - assert len(events) == 1 - events.pop() + knx.assert_state("climate.test", HVACMode.HEAT, preset_mode="building_protection") # set preset mode await hass.services.async_call( "climate", "set_preset_mode", - {"entity_id": "climate.test", "preset_mode": PRESET_SLEEP}, + {"entity_id": "climate.test", "preset_mode": "economy"}, blocking=True, ) await knx.assert_write("1/2/6", (0x03,)) - assert len(events) == 1 - events.pop() + knx.assert_state("climate.test", HVACMode.HEAT, preset_mode="economy") assert len(knx.xknx.devices) == 2 assert len(knx.xknx.devices[0].device_updated_cbs) == 2 From 1f684330e06afeb9b213fa28978fb6f8a4aa0869 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Aug 2024 09:18:20 +0200 Subject: [PATCH 2341/2411] Bump github/codeql-action from 3.26.0 to 3.26.2 (#123966) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 68673455e1f..45c2b31d772 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.1.7 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.26.0 + uses: github/codeql-action/init@v3.26.2 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.26.0 + uses: github/codeql-action/analyze@v3.26.2 with: category: "/language:python" From ac30efb5ac5012ee7ba9f336239cbe4c73810b3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Aug 2024 09:31:42 +0200 Subject: [PATCH 2342/2411] Bump home-assistant/builder from 2024.03.5 to 2024.08.1 (#123967) 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 522095ad041..7f3c0b0e66e 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -197,7 +197,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2024.03.5 + uses: home-assistant/builder@2024.08.1 with: args: | $BUILD_ARGS \ @@ -263,7 +263,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2024.03.5 + uses: home-assistant/builder@2024.08.1 with: args: | $BUILD_ARGS \ From dde1ecbf5b03fb945851e4e265606e7bf4b7a637 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 15 Aug 2024 17:59:08 +1000 Subject: [PATCH 2343/2411] Improve code quality of Tesla Fleet tests (#123959) --- tests/components/tesla_fleet/conftest.py | 4 +-- .../tesla_fleet/test_binary_sensors.py | 8 +++-- .../tesla_fleet/test_config_flow.py | 8 ++--- .../tesla_fleet/test_device_tracker.py | 6 ++-- tests/components/tesla_fleet/test_init.py | 30 +++++++++---------- tests/components/tesla_fleet/test_sensor.py | 6 ++-- 6 files changed, 34 insertions(+), 28 deletions(-) diff --git a/tests/components/tesla_fleet/conftest.py b/tests/components/tesla_fleet/conftest.py index 7d60ae5e174..071fd7b02f1 100644 --- a/tests/components/tesla_fleet/conftest.py +++ b/tests/components/tesla_fleet/conftest.py @@ -106,7 +106,7 @@ def mock_wake_up() -> Generator[AsyncMock]: @pytest.fixture(autouse=True) def mock_live_status() -> Generator[AsyncMock]: - """Mock Teslemetry Energy Specific live_status method.""" + """Mock Tesla Fleet API Energy Specific live_status method.""" with patch( "homeassistant.components.tesla_fleet.EnergySpecific.live_status", side_effect=lambda: deepcopy(LIVE_STATUS), @@ -116,7 +116,7 @@ def mock_live_status() -> Generator[AsyncMock]: @pytest.fixture(autouse=True) def mock_site_info() -> Generator[AsyncMock]: - """Mock Teslemetry Energy Specific site_info method.""" + """Mock Tesla Fleet API Energy Specific site_info method.""" with patch( "homeassistant.components.tesla_fleet.EnergySpecific.site_info", side_effect=lambda: deepcopy(SITE_INFO), diff --git a/tests/components/tesla_fleet/test_binary_sensors.py b/tests/components/tesla_fleet/test_binary_sensors.py index ffbaac5e6d8..a759e5ced70 100644 --- a/tests/components/tesla_fleet/test_binary_sensors.py +++ b/tests/components/tesla_fleet/test_binary_sensors.py @@ -1,8 +1,10 @@ """Test the Tesla Fleet binary sensor platform.""" +from unittest.mock import AsyncMock + from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from tesla_fleet_api.exceptions import VehicleOffline from homeassistant.components.tesla_fleet.coordinator import VEHICLE_INTERVAL @@ -34,7 +36,7 @@ async def test_binary_sensor_refresh( hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, - mock_vehicle_data, + mock_vehicle_data: AsyncMock, freezer: FrozenDateTimeFactory, normal_config_entry: MockConfigEntry, ) -> None: @@ -53,7 +55,7 @@ async def test_binary_sensor_refresh( async def test_binary_sensor_offline( hass: HomeAssistant, - mock_vehicle_data, + mock_vehicle_data: AsyncMock, normal_config_entry: MockConfigEntry, ) -> None: """Tests that the binary sensor entities are correct when offline.""" diff --git a/tests/components/tesla_fleet/test_config_flow.py b/tests/components/tesla_fleet/test_config_flow.py index 45dbe6ca598..81ba92f1e9c 100644 --- a/tests/components/tesla_fleet/test_config_flow.py +++ b/tests/components/tesla_fleet/test_config_flow.py @@ -57,7 +57,7 @@ async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - access_token, + access_token: str, ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( @@ -121,7 +121,7 @@ async def test_full_flow_user_cred( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - access_token, + access_token: str, ) -> None: """Check full flow.""" @@ -200,7 +200,7 @@ async def test_reauthentication( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - access_token, + access_token: str, ) -> None: """Test Tesla Fleet reauthentication.""" old_entry = MockConfigEntry( @@ -261,7 +261,7 @@ async def test_reauth_account_mismatch( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - access_token, + access_token: str, ) -> None: """Test Tesla Fleet reauthentication with different account.""" old_entry = MockConfigEntry(domain=DOMAIN, unique_id="baduid", version=1, data={}) diff --git a/tests/components/tesla_fleet/test_device_tracker.py b/tests/components/tesla_fleet/test_device_tracker.py index 66a0c06de7f..e6f483d7953 100644 --- a/tests/components/tesla_fleet/test_device_tracker.py +++ b/tests/components/tesla_fleet/test_device_tracker.py @@ -1,6 +1,8 @@ """Test the Tesla Fleet device tracker platform.""" -from syrupy import SnapshotAssertion +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion from tesla_fleet_api.exceptions import VehicleOffline from homeassistant.const import STATE_UNKNOWN, Platform @@ -26,7 +28,7 @@ async def test_device_tracker( async def test_device_tracker_offline( hass: HomeAssistant, - mock_vehicle_data, + mock_vehicle_data: AsyncMock, normal_config_entry: MockConfigEntry, ) -> None: """Tests that the device tracker entities are correct when offline.""" diff --git a/tests/components/tesla_fleet/test_init.py b/tests/components/tesla_fleet/test_init.py index 20bb6c66906..a22d91ee531 100644 --- a/tests/components/tesla_fleet/test_init.py +++ b/tests/components/tesla_fleet/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from tesla_fleet_api.exceptions import ( InvalidToken, LoginRequired, @@ -59,9 +59,9 @@ async def test_load_unload( async def test_init_error( hass: HomeAssistant, normal_config_entry: MockConfigEntry, - mock_products, - side_effect, - state, + mock_products: AsyncMock, + side_effect: TeslaFleetError, + state: ConfigEntryState, ) -> None: """Test init with errors.""" @@ -91,8 +91,8 @@ async def test_devices( async def test_vehicle_refresh_offline( hass: HomeAssistant, normal_config_entry: MockConfigEntry, - mock_vehicle_state, - mock_vehicle_data, + mock_vehicle_state: AsyncMock, + mock_vehicle_data: AsyncMock, freezer: FrozenDateTimeFactory, ) -> None: """Test coordinator refresh with an error.""" @@ -148,7 +148,7 @@ async def test_vehicle_refresh_error( async def test_vehicle_refresh_ratelimited( hass: HomeAssistant, normal_config_entry: MockConfigEntry, - mock_vehicle_data, + mock_vehicle_data: AsyncMock, freezer: FrozenDateTimeFactory, ) -> None: """Test coordinator refresh handles 429.""" @@ -179,7 +179,7 @@ async def test_vehicle_refresh_ratelimited( async def test_vehicle_sleep( hass: HomeAssistant, normal_config_entry: MockConfigEntry, - mock_vehicle_data, + mock_vehicle_data: AsyncMock, freezer: FrozenDateTimeFactory, ) -> None: """Test coordinator refresh with an error.""" @@ -241,9 +241,9 @@ async def test_vehicle_sleep( async def test_energy_live_refresh_error( hass: HomeAssistant, normal_config_entry: MockConfigEntry, - mock_live_status, - side_effect, - state, + mock_live_status: AsyncMock, + side_effect: TeslaFleetError, + state: ConfigEntryState, ) -> None: """Test coordinator refresh with an error.""" mock_live_status.side_effect = side_effect @@ -256,9 +256,9 @@ async def test_energy_live_refresh_error( async def test_energy_site_refresh_error( hass: HomeAssistant, normal_config_entry: MockConfigEntry, - mock_site_info, - side_effect, - state, + mock_site_info: AsyncMock, + side_effect: TeslaFleetError, + state: ConfigEntryState, ) -> None: """Test coordinator refresh with an error.""" mock_site_info.side_effect = side_effect @@ -300,7 +300,7 @@ async def test_energy_live_refresh_ratelimited( async def test_energy_info_refresh_ratelimited( hass: HomeAssistant, normal_config_entry: MockConfigEntry, - mock_site_info, + mock_site_info: AsyncMock, freezer: FrozenDateTimeFactory, ) -> None: """Test coordinator refresh handles 429.""" diff --git a/tests/components/tesla_fleet/test_sensor.py b/tests/components/tesla_fleet/test_sensor.py index 2133194e2a0..377179ca26a 100644 --- a/tests/components/tesla_fleet/test_sensor.py +++ b/tests/components/tesla_fleet/test_sensor.py @@ -1,8 +1,10 @@ """Test the Tesla Fleet sensor platform.""" +from unittest.mock import AsyncMock + from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.tesla_fleet.coordinator import VEHICLE_INTERVAL from homeassistant.const import Platform @@ -22,7 +24,7 @@ async def test_sensors( normal_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, - mock_vehicle_data, + mock_vehicle_data: AsyncMock, ) -> None: """Tests that the sensor entities are correct.""" From 2c3d97d3733389c36a96f99843dd3aa33cf61cbe Mon Sep 17 00:00:00 2001 From: "Phill (pssc)" Date: Thu, 15 Aug 2024 09:03:03 +0100 Subject: [PATCH 2344/2411] Handle Yamaha ValueError (#123547) * fix yamaha remove info logging * ruff * fix yamnaha supress rxv.find UnicodeDecodeError * fix formatting * make more realistic * make more realistic and use parms * add value error after more feedback * ruff format * Update homeassistant/components/yamaha/media_player.py Co-authored-by: Martin Hjelmare * remove unused method * add more debugging * Increase discovery timeout add more debug allow config to overrite dicovery for name --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/yamaha/const.py | 1 + .../components/yamaha/media_player.py | 28 +++++++++++++++---- tests/components/yamaha/test_media_player.py | 20 +++++++++---- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/yamaha/const.py b/homeassistant/components/yamaha/const.py index 492babe9657..1cdb619b6ef 100644 --- a/homeassistant/components/yamaha/const.py +++ b/homeassistant/components/yamaha/const.py @@ -1,6 +1,7 @@ """Constants for the Yamaha component.""" DOMAIN = "yamaha" +DISCOVER_TIMEOUT = 3 KNOWN_ZONES = "known_zones" CURSOR_TYPE_DOWN = "down" CURSOR_TYPE_LEFT = "left" diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index f6434616cfa..58f501b99be 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -31,6 +31,7 @@ from .const import ( CURSOR_TYPE_RIGHT, CURSOR_TYPE_SELECT, CURSOR_TYPE_UP, + DISCOVER_TIMEOUT, DOMAIN, KNOWN_ZONES, SERVICE_ENABLE_OUTPUT, @@ -125,18 +126,33 @@ def _discovery(config_info): elif config_info.host is None: _LOGGER.debug("Config No Host Supplied Zones") zones = [] - for recv in rxv.find(): + for recv in rxv.find(DISCOVER_TIMEOUT): zones.extend(recv.zone_controllers()) else: _LOGGER.debug("Config Zones") zones = None # Fix for upstream issues in rxv.find() with some hardware. - with contextlib.suppress(AttributeError): - for recv in rxv.find(): + with contextlib.suppress(AttributeError, ValueError): + for recv in rxv.find(DISCOVER_TIMEOUT): + _LOGGER.debug( + "Found Serial %s %s %s", + recv.serial_number, + recv.ctrl_url, + recv.zone, + ) if recv.ctrl_url == config_info.ctrl_url: - _LOGGER.debug("Config Zones Matched %s", config_info.ctrl_url) - zones = recv.zone_controllers() + _LOGGER.debug( + "Config Zones Matched Serial %s: %s", + recv.ctrl_url, + recv.serial_number, + ) + zones = rxv.RXV( + config_info.ctrl_url, + friendly_name=config_info.name, + serial_number=recv.serial_number, + model_name=recv.model_name, + ).zone_controllers() break if not zones: @@ -170,7 +186,7 @@ async def async_setup_platform( entities = [] for zctrl in zone_ctrls: - _LOGGER.debug("Receiver zone: %s", zctrl.zone) + _LOGGER.debug("Receiver zone: %s serial %s", zctrl.zone, zctrl.serial_number) if config_info.zone_ignore and zctrl.zone in config_info.zone_ignore: _LOGGER.debug("Ignore receiver zone: %s %s", config_info.name, zctrl.zone) continue diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index d96da8f6854..2375e7d07f4 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -86,17 +86,25 @@ async def test_setup_host(hass: HomeAssistant, device, device2, main_zone) -> No assert state.state == "off" -async def test_setup_attribute_error(hass: HomeAssistant, device, main_zone) -> None: - """Test set up integration encountering an Attribute Error.""" +@pytest.mark.parametrize( + ("error"), + [ + AttributeError, + ValueError, + UnicodeDecodeError("", b"", 1, 0, ""), + ], +) +async def test_setup_find_errors(hass: HomeAssistant, device, main_zone, error) -> None: + """Test set up integration encountering an Error.""" - with patch("rxv.find", side_effect=AttributeError): + with patch("rxv.find", side_effect=error): assert await async_setup_component(hass, MP_DOMAIN, CONFIG) await hass.async_block_till_done() - state = hass.states.get("media_player.yamaha_receiver_main_zone") + state = hass.states.get("media_player.yamaha_receiver_main_zone") - assert state is not None - assert state.state == "off" + assert state is not None + assert state.state == "off" async def test_setup_no_host(hass: HomeAssistant, device, main_zone) -> None: From 5836f8edb55a40652fc65bdddbcf4b118053b232 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Aug 2024 10:11:43 +0200 Subject: [PATCH 2345/2411] Pass None instead of empty dict when registering entity services (#123879) * Pass None instead of empty dict when registering entity services * Update rainmachine --- .../components/media_player/__init__.py | 26 +++++++++---------- homeassistant/components/modern_forms/fan.py | 2 +- .../components/modern_forms/light.py | 2 +- homeassistant/components/motioneye/camera.py | 2 +- homeassistant/components/netatmo/climate.py | 2 +- homeassistant/components/prosegur/camera.py | 2 +- .../components/rainmachine/switch.py | 16 ++++++------ homeassistant/components/roborock/vacuum.py | 2 +- homeassistant/components/select/__init__.py | 4 +-- homeassistant/components/siren/__init__.py | 4 +-- .../components/snapcast/media_player.py | 6 ++--- .../components/sonos/media_player.py | 4 ++- .../components/squeezebox/media_player.py | 2 +- homeassistant/components/switch/__init__.py | 6 ++--- homeassistant/components/timer/__init__.py | 6 ++--- homeassistant/components/todo/__init__.py | 2 +- .../totalconnect/alarm_control_panel.py | 4 +-- homeassistant/components/upb/light.py | 2 +- homeassistant/components/upb/scene.py | 4 +-- homeassistant/components/update/__init__.py | 4 +-- homeassistant/components/vacuum/__init__.py | 12 ++++----- homeassistant/components/valve/__init__.py | 11 +++++--- homeassistant/components/verisure/camera.py | 2 +- homeassistant/components/verisure/lock.py | 4 +-- .../components/water_heater/__init__.py | 4 +-- homeassistant/components/wemo/fan.py | 2 +- .../components/xiaomi_miio/remote.py | 4 +-- .../components/xiaomi_miio/vacuum.py | 4 +-- 28 files changed, 75 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index d499ee8d6d3..beb672a1e58 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -274,59 +274,59 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_setup(config) component.async_register_entity_service( - SERVICE_TURN_ON, {}, "async_turn_on", [MediaPlayerEntityFeature.TURN_ON] + SERVICE_TURN_ON, None, "async_turn_on", [MediaPlayerEntityFeature.TURN_ON] ) component.async_register_entity_service( - SERVICE_TURN_OFF, {}, "async_turn_off", [MediaPlayerEntityFeature.TURN_OFF] + SERVICE_TURN_OFF, None, "async_turn_off", [MediaPlayerEntityFeature.TURN_OFF] ) component.async_register_entity_service( SERVICE_TOGGLE, - {}, + None, "async_toggle", [MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.TURN_ON], ) component.async_register_entity_service( SERVICE_VOLUME_UP, - {}, + None, "async_volume_up", [MediaPlayerEntityFeature.VOLUME_SET, MediaPlayerEntityFeature.VOLUME_STEP], ) component.async_register_entity_service( SERVICE_VOLUME_DOWN, - {}, + None, "async_volume_down", [MediaPlayerEntityFeature.VOLUME_SET, MediaPlayerEntityFeature.VOLUME_STEP], ) component.async_register_entity_service( SERVICE_MEDIA_PLAY_PAUSE, - {}, + None, "async_media_play_pause", [MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE], ) component.async_register_entity_service( - SERVICE_MEDIA_PLAY, {}, "async_media_play", [MediaPlayerEntityFeature.PLAY] + SERVICE_MEDIA_PLAY, None, "async_media_play", [MediaPlayerEntityFeature.PLAY] ) component.async_register_entity_service( - SERVICE_MEDIA_PAUSE, {}, "async_media_pause", [MediaPlayerEntityFeature.PAUSE] + SERVICE_MEDIA_PAUSE, None, "async_media_pause", [MediaPlayerEntityFeature.PAUSE] ) component.async_register_entity_service( - SERVICE_MEDIA_STOP, {}, "async_media_stop", [MediaPlayerEntityFeature.STOP] + SERVICE_MEDIA_STOP, None, "async_media_stop", [MediaPlayerEntityFeature.STOP] ) component.async_register_entity_service( SERVICE_MEDIA_NEXT_TRACK, - {}, + None, "async_media_next_track", [MediaPlayerEntityFeature.NEXT_TRACK], ) component.async_register_entity_service( SERVICE_MEDIA_PREVIOUS_TRACK, - {}, + None, "async_media_previous_track", [MediaPlayerEntityFeature.PREVIOUS_TRACK], ) component.async_register_entity_service( SERVICE_CLEAR_PLAYLIST, - {}, + None, "async_clear_playlist", [MediaPlayerEntityFeature.CLEAR_PLAYLIST], ) @@ -423,7 +423,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: [MediaPlayerEntityFeature.SHUFFLE_SET], ) component.async_register_entity_service( - SERVICE_UNJOIN, {}, "async_unjoin_player", [MediaPlayerEntityFeature.GROUPING] + SERVICE_UNJOIN, None, "async_unjoin_player", [MediaPlayerEntityFeature.GROUPING] ) component.async_register_entity_service( diff --git a/homeassistant/components/modern_forms/fan.py b/homeassistant/components/modern_forms/fan.py index c00549c327a..e34038c7be7 100644 --- a/homeassistant/components/modern_forms/fan.py +++ b/homeassistant/components/modern_forms/fan.py @@ -56,7 +56,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_CLEAR_FAN_SLEEP_TIMER, - {}, + None, "async_clear_fan_sleep_timer", ) diff --git a/homeassistant/components/modern_forms/light.py b/homeassistant/components/modern_forms/light.py index e758a50e77e..4c210038694 100644 --- a/homeassistant/components/modern_forms/light.py +++ b/homeassistant/components/modern_forms/light.py @@ -61,7 +61,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_CLEAR_LIGHT_SLEEP_TIMER, - {}, + None, "async_clear_light_sleep_timer", ) diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index da5eb36d494..d84f7b43c04 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -136,7 +136,7 @@ async def async_setup_entry( ) platform.async_register_entity_service( SERVICE_SNAPSHOT, - {}, + None, "async_request_snapshot", ) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index e257c7a89ea..c2953b9d49d 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -174,7 +174,7 @@ async def async_setup_entry( ) platform.async_register_entity_service( SERVICE_CLEAR_TEMPERATURE_SETTING, - {}, + None, "_async_service_clear_temperature_setting", ) diff --git a/homeassistant/components/prosegur/camera.py b/homeassistant/components/prosegur/camera.py index fd911fa5898..2df6ff62038 100644 --- a/homeassistant/components/prosegur/camera.py +++ b/homeassistant/components/prosegur/camera.py @@ -31,7 +31,7 @@ async def async_setup_entry( platform = async_get_current_platform() platform.async_register_entity_service( SERVICE_REQUEST_IMAGE, - {}, + None, "async_request_image", ) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index d4c0064219e..8368db47d61 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -6,7 +6,7 @@ import asyncio from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass from datetime import datetime -from typing import Any, Concatenate, cast +from typing import Any, Concatenate from regenmaschine.errors import RainMachineError import voluptuous as vol @@ -184,8 +184,8 @@ async def async_setup_entry( """Set up RainMachine switches based on a config entry.""" platform = entity_platform.async_get_current_platform() - for service_name, schema, method in ( - ("start_program", {}, "async_start_program"), + services: tuple[tuple[str, VolDictType | None, str], ...] = ( + ("start_program", None, "async_start_program"), ( "start_zone", { @@ -195,11 +195,11 @@ async def async_setup_entry( }, "async_start_zone", ), - ("stop_program", {}, "async_stop_program"), - ("stop_zone", {}, "async_stop_zone"), - ): - schema_dict = cast(VolDictType, schema) - platform.async_register_entity_service(service_name, schema_dict, method) + ("stop_program", None, "async_stop_program"), + ("stop_zone", None, "async_stop_zone"), + ) + for service_name, schema, method in services: + platform.async_register_entity_service(service_name, schema, method) data = entry.runtime_data entities: list[RainMachineBaseSwitch] = [] diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index f7fc58161a8..81a10e26415 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -69,7 +69,7 @@ async def async_setup_entry( platform.async_register_entity_service( GET_MAPS_SERVICE_NAME, - {}, + None, RoborockVacuum.get_maps.__name__, supports_response=SupportsResponse.ONLY, ) diff --git a/homeassistant/components/select/__init__.py b/homeassistant/components/select/__init__.py index 27d41dafcd1..24f7d8bffea 100644 --- a/homeassistant/components/select/__init__.py +++ b/homeassistant/components/select/__init__.py @@ -66,13 +66,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_SELECT_FIRST, - {}, + None, SelectEntity.async_first.__name__, ) component.async_register_entity_service( SERVICE_SELECT_LAST, - {}, + None, SelectEntity.async_last.__name__, ) diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index 216e111b7db..801ca4f2bee 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -129,11 +129,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: [SirenEntityFeature.TURN_ON], ) component.async_register_entity_service( - SERVICE_TURN_OFF, {}, "async_turn_off", [SirenEntityFeature.TURN_OFF] + SERVICE_TURN_OFF, None, "async_turn_off", [SirenEntityFeature.TURN_OFF] ) component.async_register_entity_service( SERVICE_TOGGLE, - {}, + None, "async_toggle", [SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF], ) diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 0918d6465ad..bda411acde3 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -42,12 +42,12 @@ def register_services(): """Register snapcast services.""" platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service(SERVICE_SNAPSHOT, {}, "snapshot") - platform.async_register_entity_service(SERVICE_RESTORE, {}, "async_restore") + platform.async_register_entity_service(SERVICE_SNAPSHOT, None, "snapshot") + platform.async_register_entity_service(SERVICE_RESTORE, None, "async_restore") platform.async_register_entity_service( SERVICE_JOIN, {vol.Required(ATTR_MASTER): cv.entity_id}, handle_async_join ) - platform.async_register_entity_service(SERVICE_UNJOIN, {}, handle_async_unjoin) + platform.async_register_entity_service(SERVICE_UNJOIN, None, handle_async_unjoin) platform.async_register_entity_service( SERVICE_SET_LATENCY, {vol.Required(ATTR_LATENCY): cv.positive_int}, diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 4125466bd99..590761752c5 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -162,7 +162,9 @@ async def async_setup_entry( "set_sleep_timer", ) - platform.async_register_entity_service(SERVICE_CLEAR_TIMER, {}, "clear_sleep_timer") + platform.async_register_entity_service( + SERVICE_CLEAR_TIMER, None, "clear_sleep_timer" + ) platform.async_register_entity_service( SERVICE_UPDATE_ALARM, diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 97d55c2f2ef..552b8ed800c 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -185,7 +185,7 @@ async def async_setup_entry( {vol.Required(ATTR_OTHER_PLAYER): cv.string}, "async_sync", ) - platform.async_register_entity_service(SERVICE_UNSYNC, {}, "async_unsync") + platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync") # Start server discovery task if not already running entry.async_on_unload(async_at_start(hass, start_server_discovery)) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 55e0a7a767e..43971741e51 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -79,9 +79,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) await component.async_setup(config) - component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") - component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") - component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") + component.async_register_entity_service(SERVICE_TURN_OFF, None, "async_turn_off") + component.async_register_entity_service(SERVICE_TURN_ON, None, "async_turn_on") + component.async_register_entity_service(SERVICE_TOGGLE, None, "async_toggle") return True diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 3f2b4bd7f43..c2057551239 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -159,9 +159,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: {vol.Optional(ATTR_DURATION, default=DEFAULT_DURATION): cv.time_period}, "async_start", ) - component.async_register_entity_service(SERVICE_PAUSE, {}, "async_pause") - component.async_register_entity_service(SERVICE_CANCEL, {}, "async_cancel") - component.async_register_entity_service(SERVICE_FINISH, {}, "async_finish") + component.async_register_entity_service(SERVICE_PAUSE, None, "async_pause") + component.async_register_entity_service(SERVICE_CANCEL, None, "async_cancel") + component.async_register_entity_service(SERVICE_FINISH, None, "async_finish") component.async_register_entity_service( SERVICE_CHANGE, {vol.Optional(ATTR_DURATION, default=DEFAULT_DURATION): cv.time_period}, diff --git a/homeassistant/components/todo/__init__.py b/homeassistant/components/todo/__init__.py index 5febc9561c4..d35d9d6bbea 100644 --- a/homeassistant/components/todo/__init__.py +++ b/homeassistant/components/todo/__init__.py @@ -183,7 +183,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) component.async_register_entity_service( TodoServices.REMOVE_COMPLETED_ITEMS, - {}, + None, _async_remove_completed_items, required_features=[TodoListEntityFeature.DELETE_TODO_ITEM], ) diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 17a16674dd5..49d97e45e00 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -55,13 +55,13 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_ALARM_ARM_AWAY_INSTANT, - {}, + None, "async_alarm_arm_away_instant", ) platform.async_register_entity_service( SERVICE_ALARM_ARM_HOME_INSTANT, - {}, + None, "async_alarm_arm_home_instant", ) diff --git a/homeassistant/components/upb/light.py b/homeassistant/components/upb/light.py index eb20fc949dc..881eda3525f 100644 --- a/homeassistant/components/upb/light.py +++ b/homeassistant/components/upb/light.py @@ -42,7 +42,7 @@ async def async_setup_entry( SERVICE_LIGHT_FADE_START, UPB_BRIGHTNESS_RATE_SCHEMA, "async_light_fade_start" ) platform.async_register_entity_service( - SERVICE_LIGHT_FADE_STOP, {}, "async_light_fade_stop" + SERVICE_LIGHT_FADE_STOP, None, "async_light_fade_stop" ) platform.async_register_entity_service( SERVICE_LIGHT_BLINK, UPB_BLINK_RATE_SCHEMA, "async_light_blink" diff --git a/homeassistant/components/upb/scene.py b/homeassistant/components/upb/scene.py index 9cf6788de4f..276b620d5b5 100644 --- a/homeassistant/components/upb/scene.py +++ b/homeassistant/components/upb/scene.py @@ -31,10 +31,10 @@ async def async_setup_entry( platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( - SERVICE_LINK_DEACTIVATE, {}, "async_link_deactivate" + SERVICE_LINK_DEACTIVATE, None, "async_link_deactivate" ) platform.async_register_entity_service( - SERVICE_LINK_FADE_STOP, {}, "async_link_fade_stop" + SERVICE_LINK_FADE_STOP, None, "async_link_fade_stop" ) platform.async_register_entity_service( SERVICE_LINK_GOTO, UPB_BRIGHTNESS_RATE_SCHEMA, "async_link_goto" diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index e7813b354c1..cd52de6550f 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -95,12 +95,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_SKIP, - {}, + None, async_skip, ) component.async_register_entity_service( "clear_skipped", - {}, + None, async_clear_skipped, ) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 90018e2d8cc..867e25d4b2a 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -116,37 +116,37 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_START, - {}, + None, "async_start", [VacuumEntityFeature.START], ) component.async_register_entity_service( SERVICE_PAUSE, - {}, + None, "async_pause", [VacuumEntityFeature.PAUSE], ) component.async_register_entity_service( SERVICE_RETURN_TO_BASE, - {}, + None, "async_return_to_base", [VacuumEntityFeature.RETURN_HOME], ) component.async_register_entity_service( SERVICE_CLEAN_SPOT, - {}, + None, "async_clean_spot", [VacuumEntityFeature.CLEAN_SPOT], ) component.async_register_entity_service( SERVICE_LOCATE, - {}, + None, "async_locate", [VacuumEntityFeature.LOCATE], ) component.async_register_entity_service( SERVICE_STOP, - {}, + None, "async_stop", [VacuumEntityFeature.STOP], ) diff --git a/homeassistant/components/valve/__init__.py b/homeassistant/components/valve/__init__.py index 3814275b703..04ce12e8a8f 100644 --- a/homeassistant/components/valve/__init__.py +++ b/homeassistant/components/valve/__init__.py @@ -71,11 +71,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_setup(config) component.async_register_entity_service( - SERVICE_OPEN_VALVE, {}, "async_handle_open_valve", [ValveEntityFeature.OPEN] + SERVICE_OPEN_VALVE, None, "async_handle_open_valve", [ValveEntityFeature.OPEN] ) component.async_register_entity_service( - SERVICE_CLOSE_VALVE, {}, "async_handle_close_valve", [ValveEntityFeature.CLOSE] + SERVICE_CLOSE_VALVE, + None, + "async_handle_close_valve", + [ValveEntityFeature.CLOSE], ) component.async_register_entity_service( @@ -90,12 +93,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) component.async_register_entity_service( - SERVICE_STOP_VALVE, {}, "async_stop_valve", [ValveEntityFeature.STOP] + SERVICE_STOP_VALVE, None, "async_stop_valve", [ValveEntityFeature.STOP] ) component.async_register_entity_service( SERVICE_TOGGLE, - {}, + None, "async_toggle", [ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE], ) diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index 72f5ab93c70..50606a49eab 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -33,7 +33,7 @@ async def async_setup_entry( platform = async_get_current_platform() platform.async_register_entity_service( SERVICE_CAPTURE_SMARTCAM, - {}, + None, VerisureSmartcam.capture_smartcam.__name__, ) diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index da2bc2ced2b..5c56fc0df2c 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -41,12 +41,12 @@ async def async_setup_entry( platform = async_get_current_platform() platform.async_register_entity_service( SERVICE_DISABLE_AUTOLOCK, - {}, + None, VerisureDoorlock.disable_autolock.__name__, ) platform.async_register_entity_service( SERVICE_ENABLE_AUTOLOCK, - {}, + None, VerisureDoorlock.enable_autolock.__name__, ) diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 731a513fb66..0b54a4c1aa4 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -129,10 +129,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_setup(config) component.async_register_entity_service( - SERVICE_TURN_ON, {}, "async_turn_on", [WaterHeaterEntityFeature.ON_OFF] + SERVICE_TURN_ON, None, "async_turn_on", [WaterHeaterEntityFeature.ON_OFF] ) component.async_register_entity_service( - SERVICE_TURN_OFF, {}, "async_turn_off", [WaterHeaterEntityFeature.ON_OFF] + SERVICE_TURN_OFF, None, "async_turn_off", [WaterHeaterEntityFeature.ON_OFF] ) component.async_register_entity_service( SERVICE_SET_AWAY_MODE, SET_AWAY_MODE_SCHEMA, async_service_away_mode diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index b7c9840bcdc..f9d3270aaa0 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -67,7 +67,7 @@ async def async_setup_entry( # This will call WemoHumidifier.reset_filter_life() platform.async_register_entity_service( - SERVICE_RESET_FILTER_LIFE, {}, WemoHumidifier.reset_filter_life.__name__ + SERVICE_RESET_FILTER_LIFE, None, WemoHumidifier.reset_filter_life.__name__ ) diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 959bf0a7bee..72707109ad6 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -170,12 +170,12 @@ async def async_setup_platform( ) platform.async_register_entity_service( SERVICE_SET_REMOTE_LED_ON, - {}, + None, async_service_led_on_handler, ) platform.async_register_entity_service( SERVICE_SET_REMOTE_LED_OFF, - {}, + None, async_service_led_off_handler, ) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index ef6f94c162f..ac833f7646c 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -104,13 +104,13 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_START_REMOTE_CONTROL, - {}, + None, MiroboVacuum.async_remote_control_start.__name__, ) platform.async_register_entity_service( SERVICE_STOP_REMOTE_CONTROL, - {}, + None, MiroboVacuum.async_remote_control_stop.__name__, ) From 81c4bb5f72f2c9b0770a2f7142408f8caf8f205f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Aug 2024 10:32:40 +0200 Subject: [PATCH 2346/2411] Fix flaky recorder migration tests (#123971) --- tests/components/recorder/conftest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/recorder/conftest.py b/tests/components/recorder/conftest.py index d66cf4fe2ec..b13b3b270a9 100644 --- a/tests/components/recorder/conftest.py +++ b/tests/components/recorder/conftest.py @@ -68,7 +68,7 @@ class InstrumentedMigration: non_live_migration_done: threading.Event non_live_migration_done_stall: threading.Event apply_update_mock: Mock - stall_on_schema_version: int + stall_on_schema_version: int | None apply_update_stalled: threading.Event @@ -147,7 +147,8 @@ def instrument_migration( old_version: int, ): """Control migration progress.""" - if new_version == instrumented_migration.stall_on_schema_version: + stall_version = instrumented_migration.stall_on_schema_version + if stall_version is None or stall_version == new_version: instrumented_migration.apply_update_stalled.set() instrumented_migration.migration_stall.wait() real_apply_update( @@ -179,7 +180,7 @@ def instrument_migration( non_live_migration_done=threading.Event(), non_live_migration_done_stall=threading.Event(), apply_update_mock=apply_update_mock, - stall_on_schema_version=1, + stall_on_schema_version=None, apply_update_stalled=threading.Event(), ) From d6d016e0290736f5ab0fbc27a0edfac9585d3805 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 15 Aug 2024 10:52:55 +0200 Subject: [PATCH 2347/2411] Fix KNX UI Light color temperature DPT (#123778) --- homeassistant/components/knx/light.py | 4 +-- tests/components/knx/test_light.py | 44 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index a2ce8f8d2cb..0caa3f0a799 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -226,7 +226,7 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight group_address_color_temp_state = None color_temperature_type = ColorTemperatureType.UINT_2_BYTE if ga_color_temp := knx_config.get(CONF_GA_COLOR_TEMP): - if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE: + if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE.value: group_address_tunable_white = ga_color_temp[CONF_GA_WRITE] group_address_tunable_white_state = [ ga_color_temp[CONF_GA_STATE], @@ -239,7 +239,7 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight ga_color_temp[CONF_GA_STATE], *ga_color_temp[CONF_GA_PASSIVE], ] - if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT: + if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT.value: color_temperature_type = ColorTemperatureType.FLOAT_2_BYTE _color_dpt = get_dpt(CONF_GA_COLOR) diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index 04f849bb555..e2e4a673a0d 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import timedelta from freezegun.api import FrozenDateTimeFactory +import pytest from xknx.core import XknxConnectionState from xknx.devices.light import Light as XknxLight @@ -1174,3 +1175,46 @@ async def test_light_ui_create( await knx.receive_response("2/2/2", True) state = hass.states.get("light.test") assert state.state is STATE_ON + + +@pytest.mark.parametrize( + ("color_temp_mode", "raw_ct"), + [ + ("7.600", (0x10, 0x68)), + ("9", (0x46, 0x69)), + ("5.001", (0x74,)), + ], +) +async def test_light_ui_color_temp( + hass: HomeAssistant, + knx: KNXTestKit, + create_ui_entity: KnxEntityGenerator, + color_temp_mode: str, + raw_ct: tuple[int, ...], +) -> None: + """Test creating a switch.""" + await knx.setup_integration({}) + await create_ui_entity( + platform=Platform.LIGHT, + entity_data={"name": "test"}, + knx_data={ + "ga_switch": {"write": "1/1/1", "state": "2/2/2"}, + "ga_color_temp": { + "write": "3/3/3", + "dpt": color_temp_mode, + }, + "_light_color_mode_schema": "default", + "sync_state": True, + }, + ) + await knx.assert_read("2/2/2", True) + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_TEMP_KELVIN: 4200}, + blocking=True, + ) + await knx.assert_write("3/3/3", raw_ct) + state = hass.states.get("light.test") + assert state.state is STATE_ON + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == pytest.approx(4200, abs=1) From 629b919707e31b7b0f41a23aec524c0939100422 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Aug 2024 10:56:04 +0200 Subject: [PATCH 2348/2411] Remove unnecessary assignment of Template.hass from knx (#123977) --- homeassistant/components/knx/expose.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 29d0be998b6..921af6ba4a9 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -84,8 +84,6 @@ class KNXExposeSensor: self.expose_default = config.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT) self.expose_type: int | str = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE] self.value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) - if self.value_template is not None: - self.value_template.hass = hass self._remove_listener: Callable[[], None] | None = None self.device: ExposeSensor = ExposeSensor( From 72e235ad9fa4028754ffc36aa71f41877192f950 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Aug 2024 10:56:56 +0200 Subject: [PATCH 2349/2411] Improve some comments in recorder migration code (#123969) --- homeassistant/components/recorder/migration.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index c34d05b3ade..44b4a980238 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -607,7 +607,7 @@ def _update_states_table_with_foreign_key_options( "columns": foreign_key["constrained_columns"], } for foreign_key in inspector.get_foreign_keys(TABLE_STATES) - if foreign_key["name"] + if foreign_key["name"] # It's not possible to drop an unnamed constraint and ( # MySQL/MariaDB will have empty options not foreign_key.get("options") @@ -663,7 +663,7 @@ def _drop_foreign_key_constraints( if foreign_key["name"] and foreign_key["constrained_columns"] == [column] ] - ## Bind the ForeignKeyConstraints to the table + ## Find matching named constraints and bind the ForeignKeyConstraints to the table tmp_table = Table(table, MetaData()) drops = [ ForeignKeyConstraint((), (), name=foreign_key["name"], table=tmp_table) @@ -829,9 +829,9 @@ def _delete_foreign_key_violations( result = session.connection().execute( # We don't use an alias for the table we're deleting from, # support of the form `DELETE FROM table AS t1` was added in - # MariaDB 11.6 and is not supported by MySQL. Those engines - # instead support the from `DELETE t1 from table AS t1` which - # is not supported by PostgreSQL and undocumented for MariaDB. + # MariaDB 11.6 and is not supported by MySQL. MySQL and older + # MariaDB instead support the from `DELETE t1 from table AS t1` + # which is undocumented for MariaDB. text( f"DELETE FROM {table} " # noqa: S608 "WHERE (" From 9b78ae5908196a1ea8e2af904dd0403b40148dde Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 15 Aug 2024 19:00:07 +1000 Subject: [PATCH 2350/2411] Handle InvalidRegion in Tesla Fleet (#123958) --- .../components/tesla_fleet/__init__.py | 13 +++++++- tests/components/tesla_fleet/conftest.py | 9 ++++++ tests/components/tesla_fleet/test_init.py | 31 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 45657b3d8fb..47a2a9173a5 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -7,7 +7,9 @@ import jwt from tesla_fleet_api import EnergySpecific, TeslaFleetApi, VehicleSpecific from tesla_fleet_api.const import Scope from tesla_fleet_api.exceptions import ( + InvalidRegion, InvalidToken, + LibraryError, LoginRequired, OAuthExpired, TeslaFleetError, @@ -75,7 +77,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - region=region, charging_scope=False, partner_scope=False, - user_scope=False, energy_scope=Scope.ENERGY_DEVICE_DATA in scopes, vehicle_scope=Scope.VEHICLE_DEVICE_DATA in scopes, refresh_hook=_refresh_token, @@ -84,6 +85,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - products = (await tesla.products())["response"] except (InvalidToken, OAuthExpired, LoginRequired) as e: raise ConfigEntryAuthFailed from e + except InvalidRegion: + try: + LOGGER.info("Region is invalid, trying to find the correct region") + await tesla.find_server() + try: + products = (await tesla.products())["response"] + except TeslaFleetError as e: + raise ConfigEntryNotReady from e + except LibraryError as e: + raise ConfigEntryAuthFailed from e except TeslaFleetError as e: raise ConfigEntryNotReady from e diff --git a/tests/components/tesla_fleet/conftest.py b/tests/components/tesla_fleet/conftest.py index 071fd7b02f1..49f0be9cca7 100644 --- a/tests/components/tesla_fleet/conftest.py +++ b/tests/components/tesla_fleet/conftest.py @@ -122,3 +122,12 @@ def mock_site_info() -> Generator[AsyncMock]: side_effect=lambda: deepcopy(SITE_INFO), ) as mock_live_status: yield mock_live_status + + +@pytest.fixture(autouse=True) +def mock_find_server() -> Generator[AsyncMock]: + """Mock Tesla Fleet find server method.""" + with patch( + "homeassistant.components.tesla_fleet.TeslaFleetApi.find_server", + ) as mock_find_server: + yield mock_find_server diff --git a/tests/components/tesla_fleet/test_init.py b/tests/components/tesla_fleet/test_init.py index a22d91ee531..b5eb21d1cdd 100644 --- a/tests/components/tesla_fleet/test_init.py +++ b/tests/components/tesla_fleet/test_init.py @@ -6,7 +6,9 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from tesla_fleet_api.exceptions import ( + InvalidRegion, InvalidToken, + LibraryError, LoginRequired, OAuthExpired, RateLimited, @@ -326,3 +328,32 @@ async def test_energy_info_refresh_ratelimited( await hass.async_block_till_done() assert mock_site_info.call_count == 3 + + +async def test_init_region_issue( + hass: HomeAssistant, + normal_config_entry: MockConfigEntry, + mock_products: AsyncMock, + mock_find_server: AsyncMock, +) -> None: + """Test init with region issue.""" + + mock_products.side_effect = InvalidRegion + await setup_platform(hass, normal_config_entry) + mock_find_server.assert_called_once() + assert normal_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_init_region_issue_failed( + hass: HomeAssistant, + normal_config_entry: MockConfigEntry, + mock_products: AsyncMock, + mock_find_server: AsyncMock, +) -> None: + """Test init with unresolvable region issue.""" + + mock_products.side_effect = InvalidRegion + mock_find_server.side_effect = LibraryError + await setup_platform(hass, normal_config_entry) + mock_find_server.assert_called_once() + assert normal_config_entry.state is ConfigEntryState.SETUP_ERROR From f2d39feec0300dcde25c47b0e64a09c55e1a3222 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 15 Aug 2024 04:08:40 -0500 Subject: [PATCH 2351/2411] Adjust VAD seconds better for microVAD (#123942) --- .../components/assist_pipeline/vad.py | 29 +++-- tests/components/assist_pipeline/test_vad.py | 109 +++++++++++++++++- 2 files changed, 123 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/assist_pipeline/vad.py b/homeassistant/components/assist_pipeline/vad.py index 49496e66159..8372dbc54c7 100644 --- a/homeassistant/components/assist_pipeline/vad.py +++ b/homeassistant/components/assist_pipeline/vad.py @@ -6,13 +6,11 @@ from collections.abc import Callable, Iterable from dataclasses import dataclass from enum import StrEnum import logging -from typing import Final + +from .const import SAMPLE_CHANNELS, SAMPLE_RATE, SAMPLE_WIDTH _LOGGER = logging.getLogger(__name__) -_SAMPLE_RATE: Final = 16000 # Hz -_SAMPLE_WIDTH: Final = 2 # bytes - class VadSensitivity(StrEnum): """How quickly the end of a voice command is detected.""" @@ -26,12 +24,12 @@ class VadSensitivity(StrEnum): """Return seconds of silence for sensitivity level.""" sensitivity = VadSensitivity(sensitivity) if sensitivity == VadSensitivity.RELAXED: - return 2.0 + return 1.25 if sensitivity == VadSensitivity.AGGRESSIVE: - return 0.5 + return 0.25 - return 1.0 + return 0.7 class AudioBuffer: @@ -80,7 +78,7 @@ class VoiceCommandSegmenter: speech_seconds: float = 0.3 """Seconds of speech before voice command has started.""" - silence_seconds: float = 1.0 + silence_seconds: float = 0.7 """Seconds of silence after voice command has ended.""" timeout_seconds: float = 15.0 @@ -92,6 +90,9 @@ class VoiceCommandSegmenter: in_command: bool = False """True if inside voice command.""" + timed_out: bool = False + """True a timeout occurred during voice command.""" + _speech_seconds_left: float = 0.0 """Seconds left before considering voice command as started.""" @@ -121,6 +122,9 @@ class VoiceCommandSegmenter: Returns False when command is done. """ + if self.timed_out: + self.timed_out = False + self._timeout_seconds_left -= chunk_seconds if self._timeout_seconds_left <= 0: _LOGGER.warning( @@ -128,6 +132,7 @@ class VoiceCommandSegmenter: self.timeout_seconds, ) self.reset() + self.timed_out = True return False if not self.in_command: @@ -179,7 +184,9 @@ class VoiceCommandSegmenter: """ if vad_samples_per_chunk is None: # No chunking - chunk_seconds = (len(chunk) // _SAMPLE_WIDTH) / _SAMPLE_RATE + chunk_seconds = ( + len(chunk) // (SAMPLE_WIDTH * SAMPLE_CHANNELS) + ) / SAMPLE_RATE is_speech = vad_is_speech(chunk) return self.process(chunk_seconds, is_speech) @@ -187,8 +194,8 @@ class VoiceCommandSegmenter: raise ValueError("leftover_chunk_buffer is required when vad uses chunking") # With chunking - seconds_per_chunk = vad_samples_per_chunk / _SAMPLE_RATE - bytes_per_chunk = vad_samples_per_chunk * _SAMPLE_WIDTH + seconds_per_chunk = vad_samples_per_chunk / SAMPLE_RATE + bytes_per_chunk = vad_samples_per_chunk * (SAMPLE_WIDTH * SAMPLE_CHANNELS) for vad_chunk in chunk_samples(chunk, bytes_per_chunk, leftover_chunk_buffer): is_speech = vad_is_speech(vad_chunk) if not self.process(seconds_per_chunk, is_speech): diff --git a/tests/components/assist_pipeline/test_vad.py b/tests/components/assist_pipeline/test_vad.py index 17cb73a9139..db039ab3140 100644 --- a/tests/components/assist_pipeline/test_vad.py +++ b/tests/components/assist_pipeline/test_vad.py @@ -17,15 +17,12 @@ def test_silence() -> None: # True return value indicates voice command has not finished assert segmenter.process(_ONE_SECOND * 3, False) + assert not segmenter.in_command def test_speech() -> None: """Test that silence + speech + silence triggers a voice command.""" - def is_speech(chunk): - """Anything non-zero is speech.""" - return sum(chunk) > 0 - segmenter = VoiceCommandSegmenter() # silence @@ -33,10 +30,12 @@ def test_speech() -> None: # "speech" assert segmenter.process(_ONE_SECOND, True) + assert segmenter.in_command # silence # False return value indicates voice command is finished assert not segmenter.process(_ONE_SECOND, False) + assert not segmenter.in_command def test_audio_buffer() -> None: @@ -105,3 +104,105 @@ def test_chunk_samples_leftover() -> None: assert len(chunks) == 1 assert leftover_chunk_buffer.bytes() == bytes([5, 6]) + + +def test_silence_seconds() -> None: + """Test end of voice command silence seconds.""" + + segmenter = VoiceCommandSegmenter(silence_seconds=1.0) + + # silence + assert segmenter.process(_ONE_SECOND, False) + assert not segmenter.in_command + + # "speech" + assert segmenter.process(_ONE_SECOND, True) + assert segmenter.in_command + + # not enough silence to end + assert segmenter.process(_ONE_SECOND * 0.5, False) + assert segmenter.in_command + + # exactly enough silence now + assert not segmenter.process(_ONE_SECOND * 0.5, False) + assert not segmenter.in_command + + +def test_silence_reset() -> None: + """Test that speech resets end of voice command detection.""" + + segmenter = VoiceCommandSegmenter(silence_seconds=1.0, reset_seconds=0.5) + + # silence + assert segmenter.process(_ONE_SECOND, False) + assert not segmenter.in_command + + # "speech" + assert segmenter.process(_ONE_SECOND, True) + assert segmenter.in_command + + # not enough silence to end + assert segmenter.process(_ONE_SECOND * 0.5, False) + assert segmenter.in_command + + # speech should reset silence detection + assert segmenter.process(_ONE_SECOND * 0.5, True) + assert segmenter.in_command + + # not enough silence to end + assert segmenter.process(_ONE_SECOND * 0.5, False) + assert segmenter.in_command + + # exactly enough silence now + assert not segmenter.process(_ONE_SECOND * 0.5, False) + assert not segmenter.in_command + + +def test_speech_reset() -> None: + """Test that silence resets start of voice command detection.""" + + segmenter = VoiceCommandSegmenter( + silence_seconds=1.0, reset_seconds=0.5, speech_seconds=1.0 + ) + + # silence + assert segmenter.process(_ONE_SECOND, False) + assert not segmenter.in_command + + # not enough speech to start voice command + assert segmenter.process(_ONE_SECOND * 0.5, True) + assert not segmenter.in_command + + # silence should reset speech detection + assert segmenter.process(_ONE_SECOND, False) + assert not segmenter.in_command + + # not enough speech to start voice command + assert segmenter.process(_ONE_SECOND * 0.5, True) + assert not segmenter.in_command + + # exactly enough speech now + assert segmenter.process(_ONE_SECOND * 0.5, True) + assert segmenter.in_command + + +def test_timeout() -> None: + """Test that voice command detection times out.""" + + segmenter = VoiceCommandSegmenter(timeout_seconds=1.0) + + # not enough to time out + assert not segmenter.timed_out + assert segmenter.process(_ONE_SECOND * 0.5, False) + assert not segmenter.timed_out + + # enough to time out + assert not segmenter.process(_ONE_SECOND * 0.5, True) + assert segmenter.timed_out + + # flag resets with more audio + assert segmenter.process(_ONE_SECOND * 0.5, True) + assert not segmenter.timed_out + + assert not segmenter.process(_ONE_SECOND * 0.5, False) + assert segmenter.timed_out From 26e80cec3d8b3dc7833e0521cff21ca33719696b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Aug 2024 11:09:24 +0200 Subject: [PATCH 2352/2411] Deduplicate some recorder migration tests (#123972) --- tests/components/recorder/test_migrate.py | 69 +++-------------------- 1 file changed, 7 insertions(+), 62 deletions(-) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index eadcd2bd9ad..7b91a97e5a0 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -201,79 +201,24 @@ async def test_database_migration_failed( @pytest.mark.parametrize( ( + "patch_version", "func_to_patch", "expected_setup_result", "expected_pn_create", "expected_pn_dismiss", ), [ - ("DropConstraint", False, 1, 0), # This makes migration to step 11 fail + (11, "DropConstraint", False, 1, 0), + (44, "DropConstraint", False, 2, 1), ], ) @pytest.mark.skip_on_db_engine(["sqlite"]) @pytest.mark.usefixtures("skip_by_db_engine") -async def test_database_migration_failed_step_11( - hass: HomeAssistant, - async_setup_recorder_instance: RecorderInstanceGenerator, - func_to_patch: str, - expected_setup_result: bool, - expected_pn_create: int, - expected_pn_dismiss: int, -) -> None: - """Test we notify if the migration fails.""" - assert recorder.util.async_migration_in_progress(hass) is False - - with ( - patch( - "homeassistant.components.recorder.core.create_engine", - new=create_engine_test, - ), - patch( - f"homeassistant.components.recorder.migration.{func_to_patch}", - side_effect=OperationalError( - None, None, OSError("No space left on device") - ), - ), - patch( - "homeassistant.components.persistent_notification.create", - side_effect=pn.create, - ) as mock_create, - patch( - "homeassistant.components.persistent_notification.dismiss", - side_effect=pn.dismiss, - ) as mock_dismiss, - ): - await async_setup_recorder_instance( - hass, wait_recorder=False, expected_setup_result=expected_setup_result - ) - 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(recorder.get_instance(hass).join) - await hass.async_block_till_done() - - assert recorder.util.async_migration_in_progress(hass) is False - assert len(mock_create.mock_calls) == expected_pn_create - assert len(mock_dismiss.mock_calls) == expected_pn_dismiss - - -@pytest.mark.parametrize( - ( - "func_to_patch", - "expected_setup_result", - "expected_pn_create", - "expected_pn_dismiss", - ), - [ - ("DropConstraint", False, 2, 1), # This makes migration to step 44 fail - ], -) -@pytest.mark.skip_on_db_engine(["sqlite"]) -@pytest.mark.usefixtures("skip_by_db_engine") -async def test_database_migration_failed_step_44( +async def test_database_migration_failed_non_sqlite( hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator, instrument_migration: InstrumentedMigration, + patch_version: int, func_to_patch: str, expected_setup_result: bool, expected_pn_create: int, @@ -281,7 +226,7 @@ async def test_database_migration_failed_step_44( ) -> None: """Test we notify if the migration fails.""" assert recorder.util.async_migration_in_progress(hass) is False - instrument_migration.stall_on_schema_version = 44 + instrument_migration.stall_on_schema_version = patch_version with ( patch( @@ -303,7 +248,7 @@ async def test_database_migration_failed_step_44( wait_recorder_setup=False, expected_setup_result=expected_setup_result, ) - # Wait for migration to reach schema version 44 + # Wait for migration to reach the schema version we want to break await hass.async_add_executor_job( instrument_migration.apply_update_stalled.wait ) From b3399082a88b9d6d9857795d37925e44bc7b027c Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 15 Aug 2024 19:33:31 +1000 Subject: [PATCH 2353/2411] Gold quality for Tesla Fleet (#122235) gold quality --- homeassistant/components/tesla_fleet/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tesla_fleet/manifest.json b/homeassistant/components/tesla_fleet/manifest.json index 2acacab5065..29966b3b49c 100644 --- a/homeassistant/components/tesla_fleet/manifest.json +++ b/homeassistant/components/tesla_fleet/manifest.json @@ -7,5 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/tesla_fleet", "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], + "quality_scale": "gold", "requirements": ["tesla-fleet-api==0.7.3"] } From ab163c356fc4aa5f421879f72b688771cf1042de Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Aug 2024 12:39:01 +0200 Subject: [PATCH 2354/2411] Bump pyhomeworks to 1.1.1 (#123981) --- homeassistant/components/homeworks/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homeworks/manifest.json b/homeassistant/components/homeworks/manifest.json index 1ba0672c9f1..a399e0a98e7 100644 --- a/homeassistant/components/homeworks/manifest.json +++ b/homeassistant/components/homeworks/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/homeworks", "iot_class": "local_push", "loggers": ["pyhomeworks"], - "requirements": ["pyhomeworks==1.1.0"] + "requirements": ["pyhomeworks==1.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6908004c2d1..649395442ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1906,7 +1906,7 @@ pyhiveapi==0.5.16 pyhomematic==0.1.77 # homeassistant.components.homeworks -pyhomeworks==1.1.0 +pyhomeworks==1.1.1 # homeassistant.components.ialarm pyialarm==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 189577764c1..0b77e322e95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1523,7 +1523,7 @@ pyhiveapi==0.5.16 pyhomematic==0.1.77 # homeassistant.components.homeworks -pyhomeworks==1.1.0 +pyhomeworks==1.1.1 # homeassistant.components.ialarm pyialarm==2.2.0 From f72d9a2c023a1e1c41065cc56914ab3caa3165a2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Aug 2024 14:46:23 +0200 Subject: [PATCH 2355/2411] Raise on database error in recorder.migration._modify_columns (#123642) * Raise on database error in recorder.migration._modify_columns * Improve test coverage --- homeassistant/components/recorder/migration.py | 1 + tests/components/recorder/conftest.py | 3 +++ tests/components/recorder/test_migrate.py | 11 ++++++++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 44b4a980238..9a19ff7084a 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -580,6 +580,7 @@ def _modify_columns( _LOGGER.exception( "Could not modify column %s in table %s", column_def, table_name ) + raise def _update_states_table_with_foreign_key_options( diff --git a/tests/components/recorder/conftest.py b/tests/components/recorder/conftest.py index b13b3b270a9..9cdf9dbb372 100644 --- a/tests/components/recorder/conftest.py +++ b/tests/components/recorder/conftest.py @@ -70,6 +70,7 @@ class InstrumentedMigration: apply_update_mock: Mock stall_on_schema_version: int | None apply_update_stalled: threading.Event + apply_update_version: int | None @pytest.fixture(name="instrument_migration") @@ -147,6 +148,7 @@ def instrument_migration( old_version: int, ): """Control migration progress.""" + instrumented_migration.apply_update_version = new_version stall_version = instrumented_migration.stall_on_schema_version if stall_version is None or stall_version == new_version: instrumented_migration.apply_update_stalled.set() @@ -182,6 +184,7 @@ def instrument_migration( apply_update_mock=apply_update_mock, stall_on_schema_version=None, apply_update_stalled=threading.Event(), + apply_update_version=None, ) instrumented_migration.live_migration_done_stall.set() diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 7b91a97e5a0..4bc317bdaa7 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -208,8 +208,12 @@ async def test_database_migration_failed( "expected_pn_dismiss", ), [ - (11, "DropConstraint", False, 1, 0), - (44, "DropConstraint", False, 2, 1), + # Test error handling in _update_states_table_with_foreign_key_options + (11, "homeassistant.components.recorder.migration.DropConstraint", False, 1, 0), + # Test error handling in _modify_columns + (12, "sqlalchemy.engine.base.Connection.execute", False, 1, 0), + # Test error handling in _drop_foreign_key_constraints + (44, "homeassistant.components.recorder.migration.DropConstraint", False, 2, 1), ], ) @pytest.mark.skip_on_db_engine(["sqlite"]) @@ -255,7 +259,7 @@ async def test_database_migration_failed_non_sqlite( # Make it fail with patch( - f"homeassistant.components.recorder.migration.{func_to_patch}", + func_to_patch, side_effect=OperationalError( None, None, OSError("No space left on device") ), @@ -267,6 +271,7 @@ async def test_database_migration_failed_non_sqlite( await hass.async_add_executor_job(recorder.get_instance(hass).join) await hass.async_block_till_done() + assert instrument_migration.apply_update_version == patch_version assert recorder.util.async_migration_in_progress(hass) is False assert len(mock_create.mock_calls) == expected_pn_create assert len(mock_dismiss.mock_calls) == expected_pn_dismiss From c674a25eba4a5975d37f5af813a6db8d4896a9e8 Mon Sep 17 00:00:00 2001 From: Lenn <78048721+LennP@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:39:47 +0200 Subject: [PATCH 2356/2411] Add Motionblinds Bluetooth full test coverage (#121878) * Add tests * Fix entity test * Format * Fix sensor tests * Fix sensor tests * Fix sensor tests * Add init tests * Change service info * Rename test_sensor parameters * Removce ConfigEntryState.LOADED assertion * Remove platforms parameter from setup_platform * Rename setup_platform to setup_integration * Fixture for blind_type and mock_config_entry * Use mock for MotionDevice * Use mock for MotionDevice * Add type hint * Use Mock instead of patch * Use mock_config_entry fixture * Move constants to init * Fix entity_id name * Use fixture * Use fixtures instead of constants * Use display_name fixture * Rename mac to mac_code * Remove one patch * Use fixtures for mock_config_entry * Apply suggestion * Replace patch with mock * Replace patch with mock * Replace patch with mock * Fix * Use pytest.mark.usefixtures if parameter not used * Base mac code on address * Remove if statement from entity test --------- Co-authored-by: Joostlek --- tests/components/motionblinds_ble/__init__.py | 15 ++ tests/components/motionblinds_ble/conftest.py | 139 +++++++++++++-- .../motionblinds_ble/test_button.py | 47 ++++++ .../motionblinds_ble/test_config_flow.py | 158 +++++++++--------- .../components/motionblinds_ble/test_cover.py | 124 ++++++++++++++ .../motionblinds_ble/test_entity.py | 54 ++++++ .../components/motionblinds_ble/test_init.py | 48 ++++++ .../motionblinds_ble/test_select.py | 76 +++++++++ .../motionblinds_ble/test_sensor.py | 107 ++++++++++++ 9 files changed, 677 insertions(+), 91 deletions(-) create mode 100644 tests/components/motionblinds_ble/test_button.py create mode 100644 tests/components/motionblinds_ble/test_cover.py create mode 100644 tests/components/motionblinds_ble/test_entity.py create mode 100644 tests/components/motionblinds_ble/test_init.py create mode 100644 tests/components/motionblinds_ble/test_select.py create mode 100644 tests/components/motionblinds_ble/test_sensor.py diff --git a/tests/components/motionblinds_ble/__init__.py b/tests/components/motionblinds_ble/__init__.py index c2385555dbf..e1caef9f51f 100644 --- a/tests/components/motionblinds_ble/__init__.py +++ b/tests/components/motionblinds_ble/__init__.py @@ -1 +1,16 @@ """Tests for the Motionblinds Bluetooth integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Mock a fully setup config entry.""" + + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/motionblinds_ble/conftest.py b/tests/components/motionblinds_ble/conftest.py index 00db23734dd..f89cf4f305d 100644 --- a/tests/components/motionblinds_ble/conftest.py +++ b/tests/components/motionblinds_ble/conftest.py @@ -3,21 +3,140 @@ from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch +from motionblindsble.const import MotionBlindType import pytest -TEST_MAC = "abcd" -TEST_NAME = f"MOTION_{TEST_MAC.upper()}" -TEST_ADDRESS = "test_adress" +from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak +from homeassistant.components.motionblinds_ble.const import ( + CONF_BLIND_TYPE, + CONF_LOCAL_NAME, + CONF_MAC_CODE, + DOMAIN, +) +from homeassistant.const import CONF_ADDRESS + +from tests.common import MockConfigEntry +from tests.components.bluetooth import generate_advertisement_data, generate_ble_device -@pytest.fixture(name="motionblinds_ble_connect", autouse=True) -def motion_blinds_connect_fixture( - enable_bluetooth: None, +@pytest.fixture +def address() -> str: + """Address fixture.""" + return "cc:cc:cc:cc:cc:cc" + + +@pytest.fixture +def mac_code(address: str) -> str: + """MAC code fixture.""" + return "".join(address.split(":")[-3:-1]).upper() + + +@pytest.fixture +def display_name(mac_code: str) -> str: + """Display name fixture.""" + return f"Motionblind {mac_code.upper()}" + + +@pytest.fixture +def name(display_name: str) -> str: + """Name fixture.""" + return display_name.lower().replace(" ", "_") + + +@pytest.fixture +def local_name(mac_code: str) -> str: + """Local name fixture.""" + return f"MOTION_{mac_code.upper()}" + + +@pytest.fixture +def blind_type() -> MotionBlindType: + """Blind type fixture.""" + return MotionBlindType.ROLLER + + +@pytest.fixture +def service_info(local_name: str, address: str) -> BluetoothServiceInfoBleak: + """Service info fixture.""" + return BluetoothServiceInfoBleak( + name=local_name, + address=address, + device=generate_ble_device( + address=address, + name=local_name, + ), + rssi=-61, + manufacturer_data={000: b"test"}, + service_data={ + "test": bytearray(b"0000"), + }, + service_uuids=[ + "test", + ], + source="local", + advertisement=generate_advertisement_data( + manufacturer_data={000: b"test"}, + service_uuids=["test"], + ), + connectable=True, + time=0, + tx_power=-127, + ) + + +@pytest.fixture +def mock_motion_device( + blind_type: MotionBlindType, display_name: str +) -> Generator[AsyncMock]: + """Mock a MotionDevice.""" + + with patch( + "homeassistant.components.motionblinds_ble.MotionDevice", + autospec=True, + ) as mock_device: + device = mock_device.return_value + device.ble_device = Mock() + device.display_name = display_name + device.blind_type = blind_type + yield device + + +@pytest.fixture +def mock_config_entry( + blind_type: MotionBlindType, address: str, display_name: str, mac_code: str +) -> MockConfigEntry: + """Config entry fixture.""" + return MockConfigEntry( + title="mock_title", + domain=DOMAIN, + unique_id=address, + data={ + CONF_ADDRESS: address, + CONF_LOCAL_NAME: display_name, + CONF_MAC_CODE: mac_code, + CONF_BLIND_TYPE: blind_type.name.lower(), + }, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.motionblinds_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def motionblinds_ble_connect( + enable_bluetooth: None, local_name: str, address: str ) -> Generator[tuple[AsyncMock, Mock]]: """Mock motion blinds ble connection and entry setup.""" device = Mock() - device.name = TEST_NAME - device.address = TEST_ADDRESS + device.name = local_name + device.address = address bleak_scanner = AsyncMock() bleak_scanner.discover.return_value = [device] @@ -31,9 +150,5 @@ def motion_blinds_connect_fixture( "homeassistant.components.motionblinds_ble.config_flow.bluetooth.async_get_scanner", return_value=bleak_scanner, ), - patch( - "homeassistant.components.motionblinds_ble.async_setup_entry", - return_value=True, - ), ): yield bleak_scanner, device diff --git a/tests/components/motionblinds_ble/test_button.py b/tests/components/motionblinds_ble/test_button.py new file mode 100644 index 00000000000..f0f80762759 --- /dev/null +++ b/tests/components/motionblinds_ble/test_button.py @@ -0,0 +1,47 @@ +"""Tests for Motionblinds BLE buttons.""" + +from unittest.mock import Mock + +import pytest + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.motionblinds_ble.const import ( + ATTR_CONNECT, + ATTR_DISCONNECT, + ATTR_FAVORITE, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("motionblinds_ble_connect") +@pytest.mark.parametrize( + ("button"), + [ + ATTR_CONNECT, + ATTR_DISCONNECT, + ATTR_FAVORITE, + ], +) +async def test_button( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + button: str, +) -> None: + """Test states of the button.""" + + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: f"button.{name}_{button}"}, + blocking=True, + ) + getattr(mock_motion_device, button).assert_called_once() diff --git a/tests/components/motionblinds_ble/test_config_flow.py b/tests/components/motionblinds_ble/test_config_flow.py index 4cab12269dd..05d3077ceb1 100644 --- a/tests/components/motionblinds_ble/test_config_flow.py +++ b/tests/components/motionblinds_ble/test_config_flow.py @@ -12,41 +12,19 @@ from homeassistant.const import CONF_ADDRESS from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .conftest import TEST_ADDRESS, TEST_MAC, TEST_NAME - from tests.common import MockConfigEntry -from tests.components.bluetooth import generate_advertisement_data, generate_ble_device - -TEST_BLIND_TYPE = MotionBlindType.ROLLER.name.lower() - -BLIND_SERVICE_INFO = BluetoothServiceInfoBleak( - name=TEST_NAME, - address=TEST_ADDRESS, - device=generate_ble_device( - address="cc:cc:cc:cc:cc:cc", - name=TEST_NAME, - ), - rssi=-61, - manufacturer_data={000: b"test"}, - service_data={ - "test": bytearray(b"0000"), - }, - service_uuids=[ - "test", - ], - source="local", - advertisement=generate_advertisement_data( - manufacturer_data={000: b"test"}, - service_uuids=["test"], - ), - connectable=True, - time=0, - tx_power=-127, -) @pytest.mark.usefixtures("motionblinds_ble_connect") -async def test_config_flow_manual_success(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("mock_setup_entry") +async def test_config_flow_manual_success( + hass: HomeAssistant, + blind_type: MotionBlindType, + mac_code: str, + address: str, + local_name: str, + display_name: str, +) -> None: """Successful flow manually initialized by the user.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -57,28 +35,36 @@ async def test_config_flow_manual_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_MAC_CODE: TEST_MAC}, + {const.CONF_MAC_CODE: mac_code}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, + {const.CONF_BLIND_TYPE: blind_type.name.lower()}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == f"Motionblind {TEST_MAC.upper()}" + assert result["title"] == display_name assert result["data"] == { - CONF_ADDRESS: TEST_ADDRESS, - const.CONF_LOCAL_NAME: TEST_NAME, - const.CONF_MAC_CODE: TEST_MAC.upper(), - const.CONF_BLIND_TYPE: TEST_BLIND_TYPE, + CONF_ADDRESS: address, + const.CONF_LOCAL_NAME: local_name, + const.CONF_MAC_CODE: mac_code, + const.CONF_BLIND_TYPE: blind_type.name.lower(), } assert result["options"] == {} @pytest.mark.usefixtures("motionblinds_ble_connect") -async def test_config_flow_manual_error_invalid_mac(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("mock_setup_entry") +async def test_config_flow_manual_error_invalid_mac( + hass: HomeAssistant, + mac_code: str, + address: str, + local_name: str, + display_name: str, + blind_type: MotionBlindType, +) -> None: """Invalid MAC code error flow manually initialized by the user.""" # Initialize @@ -101,7 +87,7 @@ async def test_config_flow_manual_error_invalid_mac(hass: HomeAssistant) -> None # Recover result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_MAC_CODE: TEST_MAC}, + {const.CONF_MAC_CODE: mac_code}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" @@ -109,15 +95,15 @@ async def test_config_flow_manual_error_invalid_mac(hass: HomeAssistant) -> None # Finish flow result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, + {const.CONF_BLIND_TYPE: blind_type.name.lower()}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == f"Motionblind {TEST_MAC.upper()}" + assert result["title"] == display_name assert result["data"] == { - CONF_ADDRESS: TEST_ADDRESS, - const.CONF_LOCAL_NAME: TEST_NAME, - const.CONF_MAC_CODE: TEST_MAC.upper(), - const.CONF_BLIND_TYPE: TEST_BLIND_TYPE, + CONF_ADDRESS: address, + const.CONF_LOCAL_NAME: local_name, + const.CONF_MAC_CODE: mac_code, + const.CONF_BLIND_TYPE: blind_type.name.lower(), } assert result["options"] == {} @@ -125,6 +111,7 @@ async def test_config_flow_manual_error_invalid_mac(hass: HomeAssistant) -> None @pytest.mark.usefixtures("motionblinds_ble_connect") async def test_config_flow_manual_error_no_bluetooth_adapter( hass: HomeAssistant, + mac_code: str, ) -> None: """No Bluetooth adapter error flow manually initialized by the user.""" @@ -153,14 +140,21 @@ async def test_config_flow_manual_error_no_bluetooth_adapter( ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_MAC_CODE: TEST_MAC}, + {const.CONF_MAC_CODE: mac_code}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == const.ERROR_NO_BLUETOOTH_ADAPTER +@pytest.mark.usefixtures("mock_setup_entry") async def test_config_flow_manual_error_could_not_find_motor( - hass: HomeAssistant, motionblinds_ble_connect: tuple[AsyncMock, Mock] + hass: HomeAssistant, + motionblinds_ble_connect: tuple[AsyncMock, Mock], + mac_code: str, + local_name: str, + display_name: str, + address: str, + blind_type: MotionBlindType, ) -> None: """Could not find motor error flow manually initialized by the user.""" @@ -176,17 +170,17 @@ async def test_config_flow_manual_error_could_not_find_motor( motionblinds_ble_connect[1].name = "WRONG_NAME" result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_MAC_CODE: TEST_MAC}, + {const.CONF_MAC_CODE: mac_code}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": const.ERROR_COULD_NOT_FIND_MOTOR} # Recover - motionblinds_ble_connect[1].name = TEST_NAME + motionblinds_ble_connect[1].name = local_name result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_MAC_CODE: TEST_MAC}, + {const.CONF_MAC_CODE: mac_code}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" @@ -194,21 +188,23 @@ async def test_config_flow_manual_error_could_not_find_motor( # Finish flow result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, + {const.CONF_BLIND_TYPE: blind_type.name.lower()}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == f"Motionblind {TEST_MAC.upper()}" + assert result["title"] == display_name assert result["data"] == { - CONF_ADDRESS: TEST_ADDRESS, - const.CONF_LOCAL_NAME: TEST_NAME, - const.CONF_MAC_CODE: TEST_MAC.upper(), - const.CONF_BLIND_TYPE: TEST_BLIND_TYPE, + CONF_ADDRESS: address, + const.CONF_LOCAL_NAME: local_name, + const.CONF_MAC_CODE: mac_code, + const.CONF_BLIND_TYPE: blind_type.name.lower(), } assert result["options"] == {} async def test_config_flow_manual_error_no_devices_found( - hass: HomeAssistant, motionblinds_ble_connect: tuple[AsyncMock, Mock] + hass: HomeAssistant, + motionblinds_ble_connect: tuple[AsyncMock, Mock], + mac_code: str, ) -> None: """No devices found error flow manually initialized by the user.""" @@ -224,19 +220,27 @@ async def test_config_flow_manual_error_no_devices_found( motionblinds_ble_connect[0].discover.return_value = [] result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_MAC_CODE: TEST_MAC}, + {const.CONF_MAC_CODE: mac_code}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == const.ERROR_NO_DEVICES_FOUND @pytest.mark.usefixtures("motionblinds_ble_connect") -async def test_config_flow_bluetooth_success(hass: HomeAssistant) -> None: +async def test_config_flow_bluetooth_success( + hass: HomeAssistant, + mac_code: str, + service_info: BluetoothServiceInfoBleak, + address: str, + local_name: str, + display_name: str, + blind_type: MotionBlindType, +) -> None: """Successful bluetooth discovery flow.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, - data=BLIND_SERVICE_INFO, + data=service_info, ) assert result["type"] is FlowResultType.FORM @@ -244,36 +248,32 @@ async def test_config_flow_bluetooth_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, + {const.CONF_BLIND_TYPE: blind_type.name.lower()}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == f"Motionblind {TEST_MAC.upper()}" + assert result["title"] == display_name assert result["data"] == { - CONF_ADDRESS: TEST_ADDRESS, - const.CONF_LOCAL_NAME: TEST_NAME, - const.CONF_MAC_CODE: TEST_MAC.upper(), - const.CONF_BLIND_TYPE: TEST_BLIND_TYPE, + CONF_ADDRESS: address, + const.CONF_LOCAL_NAME: local_name, + const.CONF_MAC_CODE: mac_code, + const.CONF_BLIND_TYPE: blind_type.name.lower(), } assert result["options"] == {} -async def test_options_flow(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("mock_setup_entry") +async def test_options_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: """Test the options flow.""" - entry = MockConfigEntry( - domain=const.DOMAIN, - unique_id="0123456789", - data={ - const.CONF_BLIND_TYPE: MotionBlindType.ROLLER, - }, - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(entry.entry_id) + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" diff --git a/tests/components/motionblinds_ble/test_cover.py b/tests/components/motionblinds_ble/test_cover.py new file mode 100644 index 00000000000..2e6f1ad587a --- /dev/null +++ b/tests/components/motionblinds_ble/test_cover.py @@ -0,0 +1,124 @@ +"""Tests for Motionblinds BLE covers.""" + +from typing import Any +from unittest.mock import Mock + +from motionblindsble.const import MotionBlindType, MotionRunningType +import pytest + +from homeassistant.components.cover import ( + ATTR_POSITION, + ATTR_TILT_POSITION, + DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, + SERVICE_STOP_COVER, + SERVICE_STOP_COVER_TILT, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("blind_type", [MotionBlindType.VENETIAN]) +@pytest.mark.parametrize( + ("service", "method", "kwargs"), + [ + (SERVICE_OPEN_COVER, "open", {}), + (SERVICE_CLOSE_COVER, "close", {}), + (SERVICE_OPEN_COVER_TILT, "open_tilt", {}), + (SERVICE_CLOSE_COVER_TILT, "close_tilt", {}), + (SERVICE_SET_COVER_POSITION, "position", {ATTR_POSITION: 5}), + (SERVICE_SET_COVER_TILT_POSITION, "tilt", {ATTR_TILT_POSITION: 10}), + (SERVICE_STOP_COVER, "stop", {}), + (SERVICE_STOP_COVER_TILT, "stop", {}), + ], +) +async def test_cover_service( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + service: str, + method: str, + kwargs: dict[str, Any], +) -> None: + """Test cover service.""" + + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + COVER_DOMAIN, + service, + {ATTR_ENTITY_ID: f"cover.{name}", **kwargs}, + blocking=True, + ) + getattr(mock_motion_device, method).assert_called_once() + + +@pytest.mark.parametrize( + ("running_type", "state"), + [ + (None, "unknown"), + (MotionRunningType.STILL, "unknown"), + (MotionRunningType.OPENING, STATE_OPENING), + (MotionRunningType.CLOSING, STATE_CLOSING), + ], +) +async def test_cover_update_running( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + running_type: str | None, + state: str, +) -> None: + """Test updating running status.""" + + await setup_integration(hass, mock_config_entry) + + async_update_running = mock_motion_device.register_running_callback.call_args[0][0] + + async_update_running(running_type) + assert hass.states.get(f"cover.{name}").state == state + + +@pytest.mark.parametrize( + ("position", "tilt", "state"), + [ + (None, None, "unknown"), + (0, 0, STATE_OPEN), + (50, 90, STATE_OPEN), + (100, 180, STATE_CLOSED), + ], +) +async def test_cover_update_position( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + position: int, + tilt: int, + state: str, +) -> None: + """Test updating cover position and tilt.""" + + await setup_integration(hass, mock_config_entry) + + async_update_position = mock_motion_device.register_position_callback.call_args[0][ + 0 + ] + + async_update_position(position, tilt) + assert hass.states.get(f"cover.{name}").state == state diff --git a/tests/components/motionblinds_ble/test_entity.py b/tests/components/motionblinds_ble/test_entity.py new file mode 100644 index 00000000000..d5927e438a5 --- /dev/null +++ b/tests/components/motionblinds_ble/test_entity.py @@ -0,0 +1,54 @@ +"""Tests for Motionblinds BLE entities.""" + +from unittest.mock import Mock + +import pytest + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.components.motionblinds_ble.const import ( + ATTR_CONNECT, + ATTR_DISCONNECT, + ATTR_FAVORITE, + ATTR_SPEED, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("platform", "entity"), + [ + (Platform.BUTTON, ATTR_CONNECT), + (Platform.BUTTON, ATTR_DISCONNECT), + (Platform.BUTTON, ATTR_FAVORITE), + (Platform.SELECT, ATTR_SPEED), + ], +) +async def test_entity_update( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + platform: Platform, + entity: str, +) -> None: + """Test updating entity using homeassistant.update_entity.""" + + await async_setup_component(hass, HA_DOMAIN, {}) + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: f"{platform.name.lower()}.{name}_{entity}"}, + blocking=True, + ) + getattr(mock_motion_device, "status_query").assert_called_once_with() diff --git a/tests/components/motionblinds_ble/test_init.py b/tests/components/motionblinds_ble/test_init.py new file mode 100644 index 00000000000..706dfdc2f01 --- /dev/null +++ b/tests/components/motionblinds_ble/test_init.py @@ -0,0 +1,48 @@ +"""Tests for Motionblinds BLE init.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak +from homeassistant.components.motionblinds_ble import options_update_listener +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_options_update_listener( + mock_config_entry: MockConfigEntry, hass: HomeAssistant +) -> None: + """Test options_update_listener.""" + + await setup_integration(hass, mock_config_entry) + + with ( + patch( + "homeassistant.components.motionblinds_ble.MotionDevice.set_custom_disconnect_time" + ) as mock_set_custom_disconnect_time, + patch( + "homeassistant.components.motionblinds_ble.MotionDevice.set_permanent_connection" + ) as set_permanent_connection, + ): + await options_update_listener(hass, mock_config_entry) + mock_set_custom_disconnect_time.assert_called_once() + set_permanent_connection.assert_called_once() + + +async def test_update_ble_device( + mock_config_entry: MockConfigEntry, + hass: HomeAssistant, + service_info: BluetoothServiceInfoBleak, +) -> None: + """Test async_update_ble_device.""" + + await setup_integration(hass, mock_config_entry) + + with patch( + "homeassistant.components.motionblinds_ble.MotionDevice.set_ble_device" + ) as mock_set_ble_device: + inject_bluetooth_service_info(hass, service_info) + mock_set_ble_device.assert_called_once() diff --git a/tests/components/motionblinds_ble/test_select.py b/tests/components/motionblinds_ble/test_select.py new file mode 100644 index 00000000000..813df89e387 --- /dev/null +++ b/tests/components/motionblinds_ble/test_select.py @@ -0,0 +1,76 @@ +"""Tests for Motionblinds BLE selects.""" + +from collections.abc import Callable +from enum import Enum +from typing import Any +from unittest.mock import Mock + +from motionblindsble.const import MotionSpeedLevel +from motionblindsble.device import MotionDevice +import pytest + +from homeassistant.components.motionblinds_ble.const import ATTR_SPEED +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize(("select", "args"), [(ATTR_SPEED, MotionSpeedLevel.HIGH)]) +async def test_select( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + select: str, + args: Any, +) -> None: + """Test select.""" + + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: f"select.{name}_{select}", + ATTR_OPTION: MotionSpeedLevel.HIGH.value, + }, + blocking=True, + ) + getattr(mock_motion_device, select).assert_called_once_with(args) + + +@pytest.mark.parametrize( + ("select", "register_callback", "value"), + [ + ( + ATTR_SPEED, + lambda device: device.register_speed_callback, + MotionSpeedLevel.HIGH, + ) + ], +) +async def test_select_update( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + select: str, + register_callback: Callable[[MotionDevice], Callable[..., None]], + value: type[Enum], +) -> None: + """Test select state update.""" + + await setup_integration(hass, mock_config_entry) + + update_func = register_callback(mock_motion_device).call_args[0][0] + + update_func(value) + assert hass.states.get(f"select.{name}_{select}").state == str(value.value) diff --git a/tests/components/motionblinds_ble/test_sensor.py b/tests/components/motionblinds_ble/test_sensor.py new file mode 100644 index 00000000000..4859fc643c9 --- /dev/null +++ b/tests/components/motionblinds_ble/test_sensor.py @@ -0,0 +1,107 @@ +"""Tests for Motionblinds BLE sensors.""" + +from collections.abc import Callable +from typing import Any +from unittest.mock import Mock + +from motionblindsble.const import ( + MotionBlindType, + MotionCalibrationType, + MotionConnectionType, +) +from motionblindsble.device import MotionDevice +import pytest + +from homeassistant.components.motionblinds_ble.const import ( + ATTR_BATTERY, + ATTR_SIGNAL_STRENGTH, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("blind_type", [MotionBlindType.CURTAIN]) +@pytest.mark.parametrize( + ("sensor", "register_callback", "initial_value", "args", "expected_value"), + [ + ( + "connection_status", + lambda device: device.register_connection_callback, + MotionConnectionType.DISCONNECTED.value, + [MotionConnectionType.CONNECTING], + MotionConnectionType.CONNECTING.value, + ), + ( + ATTR_BATTERY, + lambda device: device.register_battery_callback, + "unknown", + [25, True, False], + "25", + ), + ( # Battery unknown + ATTR_BATTERY, + lambda device: device.register_battery_callback, + "unknown", + [None, False, False], + "unknown", + ), + ( # Wired + ATTR_BATTERY, + lambda device: device.register_battery_callback, + "unknown", + [255, False, True], + "255", + ), + ( # Almost full + ATTR_BATTERY, + lambda device: device.register_battery_callback, + "unknown", + [99, False, False], + "99", + ), + ( # Almost empty + ATTR_BATTERY, + lambda device: device.register_battery_callback, + "unknown", + [1, False, False], + "1", + ), + ( + "calibration_status", + lambda device: device.register_calibration_callback, + "unknown", + [MotionCalibrationType.CALIBRATING], + MotionCalibrationType.CALIBRATING.value, + ), + ( + ATTR_SIGNAL_STRENGTH, + lambda device: device.register_signal_strength_callback, + "unknown", + [-50], + "-50", + ), + ], +) +async def test_sensor( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + sensor: str, + register_callback: Callable[[MotionDevice], Callable[..., None]], + initial_value: str, + args: list[Any], + expected_value: str, +) -> None: + """Test sensors.""" + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get(f"{SENSOR_DOMAIN}.{name}_{sensor}").state == initial_value + update_func = register_callback(mock_motion_device).call_args[0][0] + update_func(*args) + assert hass.states.get(f"{SENSOR_DOMAIN}.{name}_{sensor}").state == expected_value From 21c9cd1caa29a4371b943f051db75f56a6b5c212 Mon Sep 17 00:00:00 2001 From: cnico Date: Thu, 15 Aug 2024 15:44:49 +0200 Subject: [PATCH 2357/2411] Add switch platform to chacon_dio integration (#122514) * Adding switch platform for dio devices * Remove useless logger * Review corrections * review corrections --- .../components/chacon_dio/__init__.py | 2 +- homeassistant/components/chacon_dio/switch.py | 74 ++++++++++ tests/components/chacon_dio/conftest.py | 2 + .../chacon_dio/snapshots/test_switch.ambr | 48 +++++++ tests/components/chacon_dio/test_switch.py | 132 ++++++++++++++++++ 5 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/chacon_dio/switch.py create mode 100644 tests/components/chacon_dio/snapshots/test_switch.ambr create mode 100644 tests/components/chacon_dio/test_switch.py diff --git a/homeassistant/components/chacon_dio/__init__.py b/homeassistant/components/chacon_dio/__init__.py index 00558572fca..94617cb3929 100644 --- a/homeassistant/components/chacon_dio/__init__.py +++ b/homeassistant/components/chacon_dio/__init__.py @@ -17,7 +17,7 @@ from homeassistant.core import Event, HomeAssistant _LOGGER = logging.getLogger(__name__) -PLATFORMS: list[Platform] = [Platform.COVER] +PLATFORMS: list[Platform] = [Platform.COVER, Platform.SWITCH] @dataclass diff --git a/homeassistant/components/chacon_dio/switch.py b/homeassistant/components/chacon_dio/switch.py new file mode 100644 index 00000000000..be178c3c3b5 --- /dev/null +++ b/homeassistant/components/chacon_dio/switch.py @@ -0,0 +1,74 @@ +"""Switch Platform for Chacon Dio REV-LIGHT and switch plug devices.""" + +import logging +from typing import Any + +from dio_chacon_wifi_api.const import DeviceTypeEnum + +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ChaconDioConfigEntry +from .entity import ChaconDioEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ChaconDioConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Chacon Dio switch devices.""" + data = config_entry.runtime_data + client = data.client + + async_add_entities( + ChaconDioSwitch(client, device) + for device in data.list_devices + if device["type"] + in (DeviceTypeEnum.SWITCH_LIGHT.value, DeviceTypeEnum.SWITCH_PLUG.value) + ) + + +class ChaconDioSwitch(ChaconDioEntity, SwitchEntity): + """Object for controlling a Chacon Dio switch.""" + + _attr_device_class = SwitchDeviceClass.SWITCH + _attr_name = None + + def _update_attr(self, data: dict[str, Any]) -> None: + """Recomputes the attributes values either at init or when the device state changes.""" + self._attr_available = data["connected"] + self._attr_is_on = data["is_on"] + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the switch. + + Turned on status is effective after the server callback that triggers callback_device_state. + """ + + _LOGGER.debug( + "Turn on the switch %s , %s, %s", + self.target_id, + self.entity_id, + self._attr_is_on, + ) + + await self.client.switch_switch(self.target_id, True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the switch. + + Turned on status is effective after the server callback that triggers callback_device_state. + """ + + _LOGGER.debug( + "Turn off the switch %s , %s, %s", + self.target_id, + self.entity_id, + self._attr_is_on, + ) + + await self.client.switch_switch(self.target_id, False) diff --git a/tests/components/chacon_dio/conftest.py b/tests/components/chacon_dio/conftest.py index 3c3b970cec0..186bc468bee 100644 --- a/tests/components/chacon_dio/conftest.py +++ b/tests/components/chacon_dio/conftest.py @@ -65,6 +65,8 @@ def mock_dio_chacon_client() -> Generator[AsyncMock]: client.get_user_id.return_value = "dummy-user-id" client.search_all_devices.return_value = MOCK_COVER_DEVICE + client.switch_switch.return_value = {} + client.move_shutter_direction.return_value = {} client.disconnect.return_value = {} diff --git a/tests/components/chacon_dio/snapshots/test_switch.ambr b/tests/components/chacon_dio/snapshots/test_switch.ambr new file mode 100644 index 00000000000..7a65dad5445 --- /dev/null +++ b/tests/components/chacon_dio/snapshots/test_switch.ambr @@ -0,0 +1,48 @@ +# serializer version: 1 +# name: test_entities[switch.switch_mock_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.switch_mock_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'chacon_dio', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'L4HActuator_idmock1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.switch_mock_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Switch mock 1', + }), + 'context': , + 'entity_id': 'switch.switch_mock_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/chacon_dio/test_switch.py b/tests/components/chacon_dio/test_switch.py new file mode 100644 index 00000000000..a5ad0d0ea13 --- /dev/null +++ b/tests/components/chacon_dio/test_switch.py @@ -0,0 +1,132 @@ +"""Test the Chacon Dio switch.""" + +from collections.abc import Callable +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + +SWITCH_ENTITY_ID = "switch.switch_mock_1" + +MOCK_SWITCH_DEVICE = { + "L4HActuator_idmock1": { + "id": "L4HActuator_idmock1", + "name": "Switch mock 1", + "type": "SWITCH_LIGHT", + "model": "CERNwd-3B_1.0.6", + "connected": True, + "is_on": True, + } +} + + +async def test_entities( + hass: HomeAssistant, + mock_dio_chacon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the creation and values of the Chacon Dio switches.""" + + mock_dio_chacon_client.search_all_devices.return_value = MOCK_SWITCH_DEVICE + + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_switch_actions( + hass: HomeAssistant, + mock_dio_chacon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the actions on the Chacon Dio switch.""" + + mock_dio_chacon_client.search_all_devices.return_value = MOCK_SWITCH_DEVICE + + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: SWITCH_ENTITY_ID}, + blocking=True, + ) + state = hass.states.get(SWITCH_ENTITY_ID) + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: SWITCH_ENTITY_ID}, + blocking=True, + ) + state = hass.states.get(SWITCH_ENTITY_ID) + # turn off does not change directly the state, it is made by a server side callback. + assert state.state == STATE_ON + + +async def test_switch_callbacks( + hass: HomeAssistant, + mock_dio_chacon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test the callbacks on the Chacon Dio switches.""" + + mock_dio_chacon_client.search_all_devices.return_value = MOCK_SWITCH_DEVICE + + await setup_integration(hass, mock_config_entry) + + # Server side callback tests + # We find the callback method on the mock client + callback_device_state_function: Callable = ( + mock_dio_chacon_client.set_callback_device_state_by_device.call_args[0][1] + ) + + # Define a method to simply call it + async def _callback_device_state_function(is_on: bool) -> None: + callback_device_state_function( + { + "id": "L4HActuator_idmock1", + "connected": True, + "is_on": is_on, + } + ) + await hass.async_block_till_done() + + # And call it to effectively launch the callback as the server would do + await _callback_device_state_function(False) + state = hass.states.get(SWITCH_ENTITY_ID) + assert state + assert state.state == STATE_OFF + + +async def test_no_switch_found( + hass: HomeAssistant, + mock_dio_chacon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test the switch absence.""" + + mock_dio_chacon_client.search_all_devices.return_value = None + + await setup_integration(hass, mock_config_entry) + + assert not hass.states.async_entity_ids(SWITCH_DOMAIN) From 983806817ba524a14b94264a260adf309d2d94bd Mon Sep 17 00:00:00 2001 From: Philip Vanloo <26272906+dukeofphilberg@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:48:02 +0200 Subject: [PATCH 2358/2411] Add ArtSound as a virtual integration (#122636) * Add ArtSound as a virtual integration and brand * Remove ArtSound as brand * Add docstring for __init__ * Address hassfest --- homeassistant/components/artsound/__init__.py | 1 + homeassistant/components/artsound/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 homeassistant/components/artsound/__init__.py create mode 100644 homeassistant/components/artsound/manifest.json diff --git a/homeassistant/components/artsound/__init__.py b/homeassistant/components/artsound/__init__.py new file mode 100644 index 00000000000..149f06bc7c7 --- /dev/null +++ b/homeassistant/components/artsound/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: ArtSound.""" diff --git a/homeassistant/components/artsound/manifest.json b/homeassistant/components/artsound/manifest.json new file mode 100644 index 00000000000..589ba862102 --- /dev/null +++ b/homeassistant/components/artsound/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "artsound", + "name": "ArtSound", + "integration_type": "virtual", + "supported_by": "linkplay" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 1415ab51a75..6d2c5ec9a30 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -455,6 +455,11 @@ "config_flow": false, "iot_class": "local_polling" }, + "artsound": { + "name": "ArtSound", + "integration_type": "virtual", + "supported_by": "linkplay" + }, "aruba": { "name": "Aruba", "integrations": { From a50aeb0a660427303e13f6d07616f20988302b93 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 15 Aug 2024 10:14:01 -0400 Subject: [PATCH 2359/2411] Environment Canada weather format fix (#123960) * Add missing isoformat. * Move fixture loading to common conftest.py * Add deepcopy. --- .../components/environment_canada/weather.py | 8 ++++-- .../components/environment_canada/conftest.py | 27 +++++++++++++++++++ .../snapshots/test_weather.ambr | 22 +++++++-------- .../environment_canada/test_diagnostics.py | 2 ++ .../environment_canada/test_weather.py | 24 +++++------------ 5 files changed, 53 insertions(+), 30 deletions(-) create mode 100644 tests/components/environment_canada/conftest.py diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 2d54a313dde..1871062c2e9 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -190,10 +192,12 @@ def get_forecast(ec_data, hourly) -> list[Forecast] | None: if not (half_days := ec_data.daily_forecasts): return None - def get_day_forecast(fcst: list[dict[str, str]]) -> Forecast: + def get_day_forecast( + fcst: list[dict[str, Any]], + ) -> Forecast: high_temp = int(fcst[0]["temperature"]) if len(fcst) == 2 else None return { - ATTR_FORECAST_TIME: fcst[0]["timestamp"], + ATTR_FORECAST_TIME: fcst[0]["timestamp"].isoformat(), ATTR_FORECAST_NATIVE_TEMP: high_temp, ATTR_FORECAST_NATIVE_TEMP_LOW: int(fcst[-1]["temperature"]), ATTR_FORECAST_PRECIPITATION_PROBABILITY: int( diff --git a/tests/components/environment_canada/conftest.py b/tests/components/environment_canada/conftest.py new file mode 100644 index 00000000000..69cec187d11 --- /dev/null +++ b/tests/components/environment_canada/conftest.py @@ -0,0 +1,27 @@ +"""Common fixture for Environment Canada tests.""" + +import contextlib +from datetime import datetime +import json + +import pytest + +from tests.common import load_fixture + + +@pytest.fixture +def ec_data(): + """Load Environment Canada data.""" + + def date_hook(weather): + """Convert timestamp string to datetime.""" + + if t := weather.get("timestamp"): + with contextlib.suppress(ValueError): + weather["timestamp"] = datetime.fromisoformat(t) + return weather + + return json.loads( + load_fixture("environment_canada/current_conditions_data.json"), + object_hook=date_hook, + ) diff --git a/tests/components/environment_canada/snapshots/test_weather.ambr b/tests/components/environment_canada/snapshots/test_weather.ambr index 7ba37110c2a..cfa0ad912a4 100644 --- a/tests/components/environment_canada/snapshots/test_weather.ambr +++ b/tests/components/environment_canada/snapshots/test_weather.ambr @@ -5,35 +5,35 @@ 'forecast': list([ dict({ 'condition': 'sunny', - 'datetime': '2022-10-04 15:00:00+00:00', + 'datetime': '2022-10-04T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 18.0, 'templow': 3.0, }), dict({ 'condition': 'sunny', - 'datetime': '2022-10-05 15:00:00+00:00', + 'datetime': '2022-10-05T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 20.0, 'templow': 9.0, }), dict({ 'condition': 'partlycloudy', - 'datetime': '2022-10-06 15:00:00+00:00', + 'datetime': '2022-10-06T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 20.0, 'templow': 7.0, }), dict({ 'condition': 'rainy', - 'datetime': '2022-10-07 15:00:00+00:00', + 'datetime': '2022-10-07T15:00:00+00:00', 'precipitation_probability': 40, 'temperature': 13.0, 'templow': 1.0, }), dict({ 'condition': 'partlycloudy', - 'datetime': '2022-10-08 15:00:00+00:00', + 'datetime': '2022-10-08T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 10.0, 'templow': 3.0, @@ -48,42 +48,42 @@ 'forecast': list([ dict({ 'condition': 'clear-night', - 'datetime': '2022-10-03 15:00:00+00:00', + 'datetime': '2022-10-03T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': None, 'templow': -1.0, }), dict({ 'condition': 'sunny', - 'datetime': '2022-10-04 15:00:00+00:00', + 'datetime': '2022-10-04T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 18.0, 'templow': 3.0, }), dict({ 'condition': 'sunny', - 'datetime': '2022-10-05 15:00:00+00:00', + 'datetime': '2022-10-05T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 20.0, 'templow': 9.0, }), dict({ 'condition': 'partlycloudy', - 'datetime': '2022-10-06 15:00:00+00:00', + 'datetime': '2022-10-06T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 20.0, 'templow': 7.0, }), dict({ 'condition': 'rainy', - 'datetime': '2022-10-07 15:00:00+00:00', + 'datetime': '2022-10-07T15:00:00+00:00', 'precipitation_probability': 40, 'temperature': 13.0, 'templow': 1.0, }), dict({ 'condition': 'partlycloudy', - 'datetime': '2022-10-08 15:00:00+00:00', + 'datetime': '2022-10-08T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 10.0, 'templow': 3.0, diff --git a/tests/components/environment_canada/test_diagnostics.py b/tests/components/environment_canada/test_diagnostics.py index 7e9c8691f90..79b72961124 100644 --- a/tests/components/environment_canada/test_diagnostics.py +++ b/tests/components/environment_canada/test_diagnostics.py @@ -1,6 +1,7 @@ """Test Environment Canada diagnostics.""" import json +from typing import Any from syrupy import SnapshotAssertion @@ -26,6 +27,7 @@ async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, snapshot: SnapshotAssertion, + ec_data: dict[str, Any], ) -> None: """Test config entry diagnostics.""" diff --git a/tests/components/environment_canada/test_weather.py b/tests/components/environment_canada/test_weather.py index e8c21e2dc06..8e22f68462f 100644 --- a/tests/components/environment_canada/test_weather.py +++ b/tests/components/environment_canada/test_weather.py @@ -1,6 +1,7 @@ """Test weather.""" -import json +import copy +from typing import Any from syrupy.assertion import SnapshotAssertion @@ -12,23 +13,17 @@ from homeassistant.core import HomeAssistant from . import init_integration -from tests.common import load_fixture - async def test_forecast_daily( - hass: HomeAssistant, - snapshot: SnapshotAssertion, + hass: HomeAssistant, snapshot: SnapshotAssertion, ec_data: dict[str, Any] ) -> None: """Test basic forecast.""" - ec_data = json.loads( - load_fixture("environment_canada/current_conditions_data.json") - ) - # First entry in test data is a half day; we don't want that for this test - del ec_data["daily_forecasts"][0] + local_ec_data = copy.deepcopy(ec_data) + del local_ec_data["daily_forecasts"][0] - await init_integration(hass, ec_data) + await init_integration(hass, local_ec_data) response = await hass.services.async_call( WEATHER_DOMAIN, @@ -44,15 +39,10 @@ async def test_forecast_daily( async def test_forecast_daily_with_some_previous_days_data( - hass: HomeAssistant, - snapshot: SnapshotAssertion, + hass: HomeAssistant, snapshot: SnapshotAssertion, ec_data: dict[str, Any] ) -> None: """Test forecast with half day at start.""" - ec_data = json.loads( - load_fixture("environment_canada/current_conditions_data.json") - ) - await init_integration(hass, ec_data) response = await hass.services.async_call( From 874ae15d6a8915d2172077bc0ff830175f4abdb8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 15 Aug 2024 18:16:49 +0200 Subject: [PATCH 2360/2411] Fix motionblinds ble test (#123990) * Fix motionblinds ble test * Fix motionblinds ble test --- tests/components/motionblinds_ble/test_button.py | 2 +- tests/components/motionblinds_ble/test_cover.py | 9 ++++++--- tests/components/motionblinds_ble/test_entity.py | 2 +- tests/components/motionblinds_ble/test_init.py | 5 +++-- tests/components/motionblinds_ble/test_select.py | 4 ++-- tests/components/motionblinds_ble/test_sensor.py | 2 +- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/components/motionblinds_ble/test_button.py b/tests/components/motionblinds_ble/test_button.py index f0f80762759..9c27056c929 100644 --- a/tests/components/motionblinds_ble/test_button.py +++ b/tests/components/motionblinds_ble/test_button.py @@ -28,10 +28,10 @@ from tests.common import MockConfigEntry ], ) async def test_button( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_motion_device: Mock, name: str, - hass: HomeAssistant, button: str, ) -> None: """Test states of the button.""" diff --git a/tests/components/motionblinds_ble/test_cover.py b/tests/components/motionblinds_ble/test_cover.py index 2e6f1ad587a..2f6b33b3017 100644 --- a/tests/components/motionblinds_ble/test_cover.py +++ b/tests/components/motionblinds_ble/test_cover.py @@ -31,6 +31,7 @@ from . import setup_integration from tests.common import MockConfigEntry +@pytest.mark.usefixtures("motionblinds_ble_connect") @pytest.mark.parametrize("blind_type", [MotionBlindType.VENETIAN]) @pytest.mark.parametrize( ("service", "method", "kwargs"), @@ -46,10 +47,10 @@ from tests.common import MockConfigEntry ], ) async def test_cover_service( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_motion_device: Mock, name: str, - hass: HomeAssistant, service: str, method: str, kwargs: dict[str, Any], @@ -67,6 +68,7 @@ async def test_cover_service( getattr(mock_motion_device, method).assert_called_once() +@pytest.mark.usefixtures("motionblinds_ble_connect") @pytest.mark.parametrize( ("running_type", "state"), [ @@ -77,10 +79,10 @@ async def test_cover_service( ], ) async def test_cover_update_running( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_motion_device: Mock, name: str, - hass: HomeAssistant, running_type: str | None, state: str, ) -> None: @@ -94,6 +96,7 @@ async def test_cover_update_running( assert hass.states.get(f"cover.{name}").state == state +@pytest.mark.usefixtures("motionblinds_ble_connect") @pytest.mark.parametrize( ("position", "tilt", "state"), [ @@ -104,10 +107,10 @@ async def test_cover_update_running( ], ) async def test_cover_update_position( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_motion_device: Mock, name: str, - hass: HomeAssistant, position: int, tilt: int, state: str, diff --git a/tests/components/motionblinds_ble/test_entity.py b/tests/components/motionblinds_ble/test_entity.py index d5927e438a5..1bfd3b185e5 100644 --- a/tests/components/motionblinds_ble/test_entity.py +++ b/tests/components/motionblinds_ble/test_entity.py @@ -33,10 +33,10 @@ from tests.common import MockConfigEntry ], ) async def test_entity_update( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_motion_device: Mock, name: str, - hass: HomeAssistant, platform: Platform, entity: str, ) -> None: diff --git a/tests/components/motionblinds_ble/test_init.py b/tests/components/motionblinds_ble/test_init.py index 706dfdc2f01..09596bd8d5e 100644 --- a/tests/components/motionblinds_ble/test_init.py +++ b/tests/components/motionblinds_ble/test_init.py @@ -13,7 +13,8 @@ from tests.components.bluetooth import inject_bluetooth_service_info async def test_options_update_listener( - mock_config_entry: MockConfigEntry, hass: HomeAssistant + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, ) -> None: """Test options_update_listener.""" @@ -33,8 +34,8 @@ async def test_options_update_listener( async def test_update_ble_device( - mock_config_entry: MockConfigEntry, hass: HomeAssistant, + mock_config_entry: MockConfigEntry, service_info: BluetoothServiceInfoBleak, ) -> None: """Test async_update_ble_device.""" diff --git a/tests/components/motionblinds_ble/test_select.py b/tests/components/motionblinds_ble/test_select.py index 813df89e387..2bd1bb30ec2 100644 --- a/tests/components/motionblinds_ble/test_select.py +++ b/tests/components/motionblinds_ble/test_select.py @@ -24,10 +24,10 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize(("select", "args"), [(ATTR_SPEED, MotionSpeedLevel.HIGH)]) async def test_select( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_motion_device: Mock, name: str, - hass: HomeAssistant, select: str, args: Any, ) -> None: @@ -58,10 +58,10 @@ async def test_select( ], ) async def test_select_update( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_motion_device: Mock, name: str, - hass: HomeAssistant, select: str, register_callback: Callable[[MotionDevice], Callable[..., None]], value: type[Enum], diff --git a/tests/components/motionblinds_ble/test_sensor.py b/tests/components/motionblinds_ble/test_sensor.py index 4859fc643c9..54d2fbcb064 100644 --- a/tests/components/motionblinds_ble/test_sensor.py +++ b/tests/components/motionblinds_ble/test_sensor.py @@ -87,10 +87,10 @@ from tests.common import MockConfigEntry ], ) async def test_sensor( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_motion_device: Mock, name: str, - hass: HomeAssistant, sensor: str, register_callback: Callable[[MotionDevice], Callable[..., None]], initial_value: str, From e39bfeac08af624b316a538d6ce39e981dc9edc0 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 15 Aug 2024 18:18:05 +0200 Subject: [PATCH 2361/2411] Allow shared Synology DSM Photo albums shown in media browser (#123613) --- .../components/synology_dsm/manifest.json | 2 +- .../components/synology_dsm/media_source.py | 58 +++++++++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../synology_dsm/test_media_source.py | 35 ++++++----- 5 files changed, 65 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 9d977609d14..5d42188357b 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/synology_dsm", "iot_class": "local_polling", "loggers": ["synology_dsm"], - "requirements": ["py-synologydsm-api==2.4.5"], + "requirements": ["py-synologydsm-api==2.5.2"], "ssdp": [ { "manufacturer": "Synology", diff --git a/homeassistant/components/synology_dsm/media_source.py b/homeassistant/components/synology_dsm/media_source.py index ace5733c222..d35b262809c 100644 --- a/homeassistant/components/synology_dsm/media_source.py +++ b/homeassistant/components/synology_dsm/media_source.py @@ -46,18 +46,24 @@ class SynologyPhotosMediaSourceIdentifier: self.cache_key = None self.file_name = None self.is_shared = False + self.passphrase = "" - if parts: - self.unique_id = parts[0] - if len(parts) > 1: - self.album_id = parts[1] - if len(parts) > 2: - self.cache_key = parts[2] - if len(parts) > 3: - self.file_name = parts[3] - if self.file_name.endswith(SHARED_SUFFIX): - self.is_shared = True - self.file_name = self.file_name.removesuffix(SHARED_SUFFIX) + self.unique_id = parts[0] + + if len(parts) > 1: + album_parts = parts[1].split("_") + self.album_id = album_parts[0] + if len(album_parts) > 1: + self.passphrase = parts[1].replace(f"{self.album_id}_", "") + + if len(parts) > 2: + self.cache_key = parts[2] + + if len(parts) > 3: + self.file_name = parts[3] + if self.file_name.endswith(SHARED_SUFFIX): + self.is_shared = True + self.file_name = self.file_name.removesuffix(SHARED_SUFFIX) class SynologyPhotosMediaSource(MediaSource): @@ -135,7 +141,7 @@ class SynologyPhotosMediaSource(MediaSource): ret.extend( BrowseMediaSource( domain=DOMAIN, - identifier=f"{item.identifier}/{album.album_id}", + identifier=f"{item.identifier}/{album.album_id}_{album.passphrase}", media_class=MediaClass.DIRECTORY, media_content_type=MediaClass.IMAGE, title=album.name, @@ -149,7 +155,7 @@ class SynologyPhotosMediaSource(MediaSource): # Request items of album # Get Items - album = SynoPhotosAlbum(int(identifier.album_id), "", 0) + album = SynoPhotosAlbum(int(identifier.album_id), "", 0, identifier.passphrase) try: album_items = await diskstation.api.photos.get_items_from_album( album, 0, 1000 @@ -170,7 +176,12 @@ class SynologyPhotosMediaSource(MediaSource): ret.append( BrowseMediaSource( domain=DOMAIN, - identifier=f"{identifier.unique_id}/{identifier.album_id}/{album_item.thumbnail_cache_key}/{album_item.file_name}{suffix}", + identifier=( + f"{identifier.unique_id}/" + f"{identifier.album_id}_{identifier.passphrase}/" + f"{album_item.thumbnail_cache_key}/" + f"{album_item.file_name}{suffix}" + ), media_class=MediaClass.IMAGE, media_content_type=mime_type, title=album_item.file_name, @@ -197,7 +208,12 @@ class SynologyPhotosMediaSource(MediaSource): if identifier.is_shared: suffix = SHARED_SUFFIX return PlayMedia( - f"/synology_dsm/{identifier.unique_id}/{identifier.cache_key}/{identifier.file_name}{suffix}", + ( + f"/synology_dsm/{identifier.unique_id}/" + f"{identifier.cache_key}/" + f"{identifier.file_name}{suffix}/" + f"{identifier.passphrase}" + ), mime_type, ) @@ -231,18 +247,24 @@ class SynologyDsmMediaView(http.HomeAssistantView): if not self.hass.data.get(DOMAIN): raise web.HTTPNotFound # location: {cache_key}/{filename} - cache_key, file_name = location.split("/") + cache_key, file_name, passphrase = location.split("/") image_id = int(cache_key.split("_")[0]) + if shared := file_name.endswith(SHARED_SUFFIX): file_name = file_name.removesuffix(SHARED_SUFFIX) + mime_type, _ = mimetypes.guess_type(file_name) if not isinstance(mime_type, str): raise web.HTTPNotFound + diskstation: SynologyDSMData = self.hass.data[DOMAIN][source_dir_id] assert diskstation.api.photos is not None - item = SynoPhotosItem(image_id, "", "", "", cache_key, "", shared) + item = SynoPhotosItem(image_id, "", "", "", cache_key, "xl", shared, passphrase) try: - image = await diskstation.api.photos.download_item(item) + if passphrase: + image = await diskstation.api.photos.download_item_thumbnail(item) + else: + image = await diskstation.api.photos.download_item(item) except SynologyDSMException as exc: raise web.HTTPNotFound from exc return web.Response(body=image, content_type=mime_type) diff --git a/requirements_all.txt b/requirements_all.txt index 649395442ad..fc8351c9237 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1662,7 +1662,7 @@ py-schluter==0.1.7 py-sucks==0.9.10 # homeassistant.components.synology_dsm -py-synologydsm-api==2.4.5 +py-synologydsm-api==2.5.2 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b77e322e95..5a89f6c644d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1354,7 +1354,7 @@ py-nightscout==1.2.2 py-sucks==0.9.10 # homeassistant.components.synology_dsm -py-synologydsm-api==2.4.5 +py-synologydsm-api==2.5.2 # homeassistant.components.hdmi_cec pyCEC==0.5.2 diff --git a/tests/components/synology_dsm/test_media_source.py b/tests/components/synology_dsm/test_media_source.py index f7ab26997ba..0c7ab6bc1cc 100644 --- a/tests/components/synology_dsm/test_media_source.py +++ b/tests/components/synology_dsm/test_media_source.py @@ -48,11 +48,15 @@ def dsm_with_photos() -> MagicMock: dsm.surveillance_station.update = AsyncMock(return_value=True) dsm.upgrade.update = AsyncMock(return_value=True) - dsm.photos.get_albums = AsyncMock(return_value=[SynoPhotosAlbum(1, "Album 1", 10)]) + dsm.photos.get_albums = AsyncMock( + return_value=[SynoPhotosAlbum(1, "Album 1", 10, "")] + ) dsm.photos.get_items_from_album = AsyncMock( return_value=[ - SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm", False), - SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm", True), + SynoPhotosItem( + 10, "", "filename.jpg", 12345, "10_1298753", "sm", False, "" + ), + SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm", True, ""), ] ) dsm.photos.get_item_thumbnail_url = AsyncMock( @@ -96,17 +100,22 @@ async def test_resolve_media_bad_identifier( [ ( "ABC012345/10/27643_876876/filename.jpg", - "/synology_dsm/ABC012345/27643_876876/filename.jpg", + "/synology_dsm/ABC012345/27643_876876/filename.jpg/", "image/jpeg", ), ( "ABC012345/12/12631_47189/filename.png", - "/synology_dsm/ABC012345/12631_47189/filename.png", + "/synology_dsm/ABC012345/12631_47189/filename.png/", "image/png", ), ( "ABC012345/12/12631_47189/filename.png_shared", - "/synology_dsm/ABC012345/12631_47189/filename.png_shared", + "/synology_dsm/ABC012345/12631_47189/filename.png_shared/", + "image/png", + ), + ( + "ABC012345/12_dmypass/12631_47189/filename.png", + "/synology_dsm/ABC012345/12631_47189/filename.png/dmypass", "image/png", ), ], @@ -250,7 +259,7 @@ async def test_browse_media_get_albums( assert result.children[0].identifier == "mocked_syno_dsm_entry/0" assert result.children[0].title == "All images" assert isinstance(result.children[1], BrowseMedia) - assert result.children[1].identifier == "mocked_syno_dsm_entry/1" + assert result.children[1].identifier == "mocked_syno_dsm_entry/1_" assert result.children[1].title == "Album 1" @@ -382,7 +391,7 @@ async def test_browse_media_get_items( assert len(result.children) == 2 item = result.children[0] assert isinstance(item, BrowseMedia) - assert item.identifier == "mocked_syno_dsm_entry/1/10_1298753/filename.jpg" + assert item.identifier == "mocked_syno_dsm_entry/1_/10_1298753/filename.jpg" assert item.title == "filename.jpg" assert item.media_class == MediaClass.IMAGE assert item.media_content_type == "image/jpeg" @@ -391,7 +400,7 @@ async def test_browse_media_get_items( assert item.thumbnail == "http://my.thumbnail.url" item = result.children[1] assert isinstance(item, BrowseMedia) - assert item.identifier == "mocked_syno_dsm_entry/1/10_1298753/filename.jpg_shared" + assert item.identifier == "mocked_syno_dsm_entry/1_/10_1298753/filename.jpg_shared" assert item.title == "filename.jpg" assert item.media_class == MediaClass.IMAGE assert item.media_content_type == "image/jpeg" @@ -435,24 +444,24 @@ async def test_media_view( assert await hass.config_entries.async_setup(entry.entry_id) with pytest.raises(web.HTTPNotFound): - await view.get(request, "", "10_1298753/filename") + await view.get(request, "", "10_1298753/filename/") # exception in download_item() dsm_with_photos.photos.download_item = AsyncMock( side_effect=SynologyDSMException("", None) ) with pytest.raises(web.HTTPNotFound): - await view.get(request, "mocked_syno_dsm_entry", "10_1298753/filename.jpg") + await view.get(request, "mocked_syno_dsm_entry", "10_1298753/filename.jpg/") # success dsm_with_photos.photos.download_item = AsyncMock(return_value=b"xxxx") with patch.object(tempfile, "tempdir", tmp_path): result = await view.get( - request, "mocked_syno_dsm_entry", "10_1298753/filename.jpg" + request, "mocked_syno_dsm_entry", "10_1298753/filename.jpg/" ) assert isinstance(result, web.Response) with patch.object(tempfile, "tempdir", tmp_path): result = await view.get( - request, "mocked_syno_dsm_entry", "10_1298753/filename.jpg_shared" + request, "mocked_syno_dsm_entry", "10_1298753/filename.jpg_shared/" ) assert isinstance(result, web.Response) From 24a20c75eb4793d3fafd75b9f330d4c07f83c260 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 15 Aug 2024 18:21:07 +0200 Subject: [PATCH 2362/2411] Add options flow to File (#120269) * Add options flow to File * Review comments --- homeassistant/components/file/__init__.py | 32 ++++++- homeassistant/components/file/config_flow.py | 70 +++++++++++++-- homeassistant/components/file/notify.py | 5 +- homeassistant/components/file/sensor.py | 7 +- homeassistant/components/file/strings.json | 16 ++++ tests/components/file/test_config_flow.py | 89 +++++++++++++++++--- tests/components/file/test_init.py | 65 ++++++++++++++ tests/components/file/test_notify.py | 35 ++++++-- tests/components/file/test_sensor.py | 26 +++++- 9 files changed, 307 insertions(+), 38 deletions(-) create mode 100644 tests/components/file/test_init.py diff --git a/homeassistant/components/file/__init__.py b/homeassistant/components/file/__init__.py index aa3e241cc81..0c9cfee5f4d 100644 --- a/homeassistant/components/file/__init__.py +++ b/homeassistant/components/file/__init__.py @@ -1,5 +1,8 @@ """The file component.""" +from copy import deepcopy +from typing import Any + from homeassistant.components.notify import migrate_notify_issue from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -84,7 +87,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a file component entry.""" - config = dict(entry.data) + config = {**entry.data, **entry.options} filepath: str = config[CONF_FILE_PATH] if filepath and not await hass.async_add_executor_job( hass.config.is_allowed_path, filepath @@ -98,6 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups( entry, [Platform(entry.data[CONF_PLATFORM])] ) + entry.async_on_unload(entry.add_update_listener(update_listener)) if entry.data[CONF_PLATFORM] == Platform.NOTIFY and CONF_NAME in entry.data: # New notify entities are being setup through the config entry, # but during the deprecation period we want to keep the legacy notify platform, @@ -121,3 +125,29 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await hass.config_entries.async_unload_platforms( entry, [entry.data[CONF_PLATFORM]] ) + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate config entry.""" + if config_entry.version > 2: + # Downgraded from future + return False + + if config_entry.version < 2: + # Move optional fields from data to options in config entry + data: dict[str, Any] = deepcopy(dict(config_entry.data)) + options = {} + for key, value in config_entry.data.items(): + if key not in (CONF_FILE_PATH, CONF_PLATFORM, CONF_NAME): + data.pop(key) + options[key] = value + + hass.config_entries.async_update_entry( + config_entry, version=2, data=data, options=options + ) + return True diff --git a/homeassistant/components/file/config_flow.py b/homeassistant/components/file/config_flow.py index 2d729473929..8cb58ec1f47 100644 --- a/homeassistant/components/file/config_flow.py +++ b/homeassistant/components/file/config_flow.py @@ -1,11 +1,18 @@ """Config flow for file integration.""" +from copy import deepcopy import os from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, + OptionsFlowWithConfigEntry, +) from homeassistant.const import ( CONF_FILE_PATH, CONF_FILENAME, @@ -15,6 +22,7 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, Platform, ) +from homeassistant.core import callback from homeassistant.helpers.selector import ( BooleanSelector, BooleanSelectorConfig, @@ -31,27 +39,44 @@ BOOLEAN_SELECTOR = BooleanSelector(BooleanSelectorConfig()) TEMPLATE_SELECTOR = TemplateSelector(TemplateSelectorConfig()) TEXT_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT)) -FILE_FLOW_SCHEMAS = { +FILE_OPTIONS_SCHEMAS = { Platform.SENSOR.value: vol.Schema( { - vol.Required(CONF_FILE_PATH): TEXT_SELECTOR, vol.Optional(CONF_VALUE_TEMPLATE): TEMPLATE_SELECTOR, vol.Optional(CONF_UNIT_OF_MEASUREMENT): TEXT_SELECTOR, } ), Platform.NOTIFY.value: vol.Schema( { - vol.Required(CONF_FILE_PATH): TEXT_SELECTOR, vol.Optional(CONF_TIMESTAMP, default=False): BOOLEAN_SELECTOR, } ), } +FILE_FLOW_SCHEMAS = { + Platform.SENSOR.value: vol.Schema( + { + vol.Required(CONF_FILE_PATH): TEXT_SELECTOR, + } + ).extend(FILE_OPTIONS_SCHEMAS[Platform.SENSOR.value].schema), + Platform.NOTIFY.value: vol.Schema( + { + vol.Required(CONF_FILE_PATH): TEXT_SELECTOR, + } + ).extend(FILE_OPTIONS_SCHEMAS[Platform.NOTIFY.value].schema), +} + class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a file config flow.""" - VERSION = 1 + VERSION = 2 + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + """Get the options flow for this handler.""" + return FileOptionsFlowHandler(config_entry) async def validate_file_path(self, file_path: str) -> bool: """Ensure the file path is valid.""" @@ -80,7 +105,13 @@ class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors[CONF_FILE_PATH] = "not_allowed" else: title = f"{DEFAULT_NAME} [{user_input[CONF_FILE_PATH]}]" - return self.async_create_entry(data=user_input, title=title) + data = deepcopy(user_input) + options = {} + for key, value in user_input.items(): + if key not in (CONF_FILE_PATH, CONF_PLATFORM, CONF_NAME): + data.pop(key) + options[key] = value + return self.async_create_entry(data=data, title=title, options=options) return self.async_show_form( step_id=platform, data_schema=FILE_FLOW_SCHEMAS[platform], errors=errors @@ -114,4 +145,29 @@ class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN): else: file_path = import_data[CONF_FILE_PATH] title = f"{name} [{file_path}]" - return self.async_create_entry(title=title, data=import_data) + data = deepcopy(import_data) + options = {} + for key, value in import_data.items(): + if key not in (CONF_FILE_PATH, CONF_PLATFORM, CONF_NAME): + data.pop(key) + options[key] = value + return self.async_create_entry(title=title, data=data, options=options) + + +class FileOptionsFlowHandler(OptionsFlowWithConfigEntry): + """Handle File options.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage File options.""" + if user_input: + return self.async_create_entry(data=user_input) + + platform = self.config_entry.data[CONF_PLATFORM] + return self.async_show_form( + step_id="init", + data_schema=self.add_suggested_values_to_schema( + FILE_OPTIONS_SCHEMAS[platform], self.config_entry.options or {} + ), + ) diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index 1516efd6d96..9411b7cf1a8 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -5,7 +5,6 @@ from __future__ import annotations from functools import partial import logging import os -from types import MappingProxyType from typing import Any, TextIO import voluptuous as vol @@ -109,7 +108,7 @@ async def async_setup_entry( ) -> None: """Set up notify entity.""" unique_id = entry.entry_id - async_add_entities([FileNotifyEntity(unique_id, entry.data)]) + async_add_entities([FileNotifyEntity(unique_id, {**entry.data, **entry.options})]) class FileNotifyEntity(NotifyEntity): @@ -118,7 +117,7 @@ class FileNotifyEntity(NotifyEntity): _attr_icon = FILE_ICON _attr_supported_features = NotifyEntityFeature.TITLE - def __init__(self, unique_id: str, config: MappingProxyType[str, Any]) -> None: + def __init__(self, unique_id: str, config: dict[str, Any]) -> None: """Initialize the service.""" self._file_path: str = config[CONF_FILE_PATH] self._add_timestamp: bool = config.get(CONF_TIMESTAMP, False) diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index fda0d14a6aa..e37a3df86a6 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -60,14 +60,15 @@ async def async_setup_entry( ) -> None: """Set up the file sensor.""" config = dict(entry.data) + options = dict(entry.options) file_path: str = config[CONF_FILE_PATH] unique_id: str = entry.entry_id name: str = config.get(CONF_NAME, DEFAULT_NAME) - unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT) + unit: str | None = options.get(CONF_UNIT_OF_MEASUREMENT) value_template: Template | None = None - if CONF_VALUE_TEMPLATE in config: - value_template = Template(config[CONF_VALUE_TEMPLATE], hass) + if CONF_VALUE_TEMPLATE in options: + value_template = Template(options[CONF_VALUE_TEMPLATE], hass) async_add_entities( [FileSensor(unique_id, name, file_path, unit, value_template)], True diff --git a/homeassistant/components/file/strings.json b/homeassistant/components/file/strings.json index 9d49e6300e9..60ebf451f78 100644 --- a/homeassistant/components/file/strings.json +++ b/homeassistant/components/file/strings.json @@ -42,6 +42,22 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, + "options": { + "step": { + "init": { + "data": { + "value_template": "[%key:component::file::config::step::sensor::data::value_template%]", + "unit_of_measurement": "[%key:component::file::config::step::sensor::data::unit_of_measurement%]", + "timestamp": "[%key:component::file::config::step::notify::data::timestamp%]" + }, + "data_description": { + "value_template": "[%key:component::file::config::step::sensor::data_description::value_template%]", + "unit_of_measurement": "[%key:component::file::config::step::sensor::data_description::unit_of_measurement%]", + "timestamp": "[%key:component::file::config::step::notify::data_description::timestamp%]" + } + } + } + }, "exceptions": { "dir_not_allowed": { "message": "Access to {filename} is not allowed." diff --git a/tests/components/file/test_config_flow.py b/tests/components/file/test_config_flow.py index 86ada1fec61..30d00411c44 100644 --- a/tests/components/file/test_config_flow.py +++ b/tests/components/file/test_config_flow.py @@ -7,6 +7,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.file import DOMAIN +from homeassistant.const import CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -15,20 +16,22 @@ from tests.common import MockConfigEntry MOCK_CONFIG_NOTIFY = { "platform": "notify", "file_path": "some_file", - "timestamp": True, } +MOCK_OPTIONS_NOTIFY = {"timestamp": True} MOCK_CONFIG_SENSOR = { "platform": "sensor", "file_path": "some/path", - "value_template": "{{ value | round(1) }}", } - -pytestmark = pytest.mark.usefixtures("mock_setup_entry") +MOCK_OPTIONS_SENSOR = {"value_template": "{{ value | round(1) }}"} +@pytest.mark.usefixtures("mock_setup_entry") @pytest.mark.parametrize( - ("platform", "data"), - [("sensor", MOCK_CONFIG_SENSOR), ("notify", MOCK_CONFIG_NOTIFY)], + ("platform", "data", "options"), + [ + ("sensor", MOCK_CONFIG_SENSOR, MOCK_OPTIONS_SENSOR), + ("notify", MOCK_CONFIG_NOTIFY, MOCK_OPTIONS_NOTIFY), + ], ) async def test_form( hass: HomeAssistant, @@ -36,6 +39,7 @@ async def test_form( mock_is_allowed_path: bool, platform: str, data: dict[str, Any], + options: dict[str, Any], ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -50,7 +54,7 @@ async def test_form( ) await hass.async_block_till_done() - user_input = dict(data) + user_input = {**data, **options} user_input.pop("platform") result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=user_input @@ -59,12 +63,17 @@ async def test_form( assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == data + assert result2["options"] == options assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("mock_setup_entry") @pytest.mark.parametrize( - ("platform", "data"), - [("sensor", MOCK_CONFIG_SENSOR), ("notify", MOCK_CONFIG_NOTIFY)], + ("platform", "data", "options"), + [ + ("sensor", MOCK_CONFIG_SENSOR, MOCK_OPTIONS_SENSOR), + ("notify", MOCK_CONFIG_NOTIFY, MOCK_OPTIONS_NOTIFY), + ], ) async def test_already_configured( hass: HomeAssistant, @@ -72,9 +81,10 @@ async def test_already_configured( mock_is_allowed_path: bool, platform: str, data: dict[str, Any], + options: dict[str, Any], ) -> None: """Test aborting if the entry is already configured.""" - entry = MockConfigEntry(domain=DOMAIN, data=data) + entry = MockConfigEntry(domain=DOMAIN, data=data, options=options) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -91,7 +101,7 @@ async def test_already_configured( assert result["type"] is FlowResultType.FORM assert result["step_id"] == platform - user_input = dict(data) + user_input = {**data, **options} user_input.pop("platform") result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -103,10 +113,14 @@ async def test_already_configured( assert result2["reason"] == "already_configured" +@pytest.mark.usefixtures("mock_setup_entry") @pytest.mark.parametrize("is_allowed", [False], ids=["not_allowed"]) @pytest.mark.parametrize( - ("platform", "data"), - [("sensor", MOCK_CONFIG_SENSOR), ("notify", MOCK_CONFIG_NOTIFY)], + ("platform", "data", "options"), + [ + ("sensor", MOCK_CONFIG_SENSOR, MOCK_OPTIONS_SENSOR), + ("notify", MOCK_CONFIG_NOTIFY, MOCK_OPTIONS_NOTIFY), + ], ) async def test_not_allowed( hass: HomeAssistant, @@ -114,6 +128,7 @@ async def test_not_allowed( mock_is_allowed_path: bool, platform: str, data: dict[str, Any], + options: dict[str, Any], ) -> None: """Test aborting if the file path is not allowed.""" result = await hass.config_entries.flow.async_init( @@ -130,7 +145,7 @@ async def test_not_allowed( assert result["type"] is FlowResultType.FORM assert result["step_id"] == platform - user_input = dict(data) + user_input = {**data, **options} user_input.pop("platform") result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -140,3 +155,49 @@ async def test_not_allowed( assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"file_path": "not_allowed"} + + +@pytest.mark.parametrize( + ("platform", "data", "options", "new_options"), + [ + ( + "sensor", + MOCK_CONFIG_SENSOR, + MOCK_OPTIONS_SENSOR, + {CONF_UNIT_OF_MEASUREMENT: "mm"}, + ), + ("notify", MOCK_CONFIG_NOTIFY, MOCK_OPTIONS_NOTIFY, {"timestamp": False}), + ], +) +async def test_options_flow( + hass: HomeAssistant, + mock_is_allowed_path: bool, + platform: str, + data: dict[str, Any], + options: dict[str, Any], + new_options: dict[str, Any], +) -> None: + """Test options config flow.""" + entry = MockConfigEntry(domain=DOMAIN, data=data, options=options, version=2) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=new_options, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == new_options + + entry = hass.config_entries.async_get_entry(entry.entry_id) + assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.options == new_options diff --git a/tests/components/file/test_init.py b/tests/components/file/test_init.py new file mode 100644 index 00000000000..faf1488ed07 --- /dev/null +++ b/tests/components/file/test_init.py @@ -0,0 +1,65 @@ +"""The tests for local file init.""" + +from unittest.mock import MagicMock, Mock, patch + +from homeassistant.components.file import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, get_fixture_path + + +@patch("os.path.isfile", Mock(return_value=True)) +@patch("os.access", Mock(return_value=True)) +async def test_migration_to_version_2( + hass: HomeAssistant, mock_is_allowed_path: MagicMock +) -> None: + """Test the File sensor with JSON entries.""" + data = { + "platform": "sensor", + "name": "file2", + "file_path": get_fixture_path("file_value_template.txt", "file"), + "value_template": "{{ value_json.temperature }}", + } + + entry = MockConfigEntry( + domain=DOMAIN, + version=1, + data=data, + title=f"test [{data['file_path']}]", + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state is ConfigEntryState.LOADED + assert entry.version == 2 + assert entry.data == { + "platform": "sensor", + "name": "file2", + "file_path": get_fixture_path("file_value_template.txt", "file"), + } + assert entry.options == { + "value_template": "{{ value_json.temperature }}", + } + + +@patch("os.path.isfile", Mock(return_value=True)) +@patch("os.access", Mock(return_value=True)) +async def test_migration_from_future_version( + hass: HomeAssistant, mock_is_allowed_path: MagicMock +) -> None: + """Test the File sensor with JSON entries.""" + data = { + "platform": "sensor", + "name": "file2", + "file_path": get_fixture_path("file_value_template.txt", "file"), + "value_template": "{{ value_json.temperature }}", + } + + entry = MockConfigEntry( + domain=DOMAIN, version=3, data=data, title=f"test [{data['file_path']}]" + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state is ConfigEntryState.MIGRATION_ERROR diff --git a/tests/components/file/test_notify.py b/tests/components/file/test_notify.py index faa9027aa21..33e4739a488 100644 --- a/tests/components/file/test_notify.py +++ b/tests/components/file/test_notify.py @@ -174,7 +174,7 @@ async def test_legacy_notify_file_exception( @pytest.mark.parametrize( - ("timestamp", "data"), + ("timestamp", "data", "options"), [ ( False, @@ -182,6 +182,8 @@ async def test_legacy_notify_file_exception( "name": "test", "platform": "notify", "file_path": "mock_file", + }, + { "timestamp": False, }, ), @@ -191,6 +193,8 @@ async def test_legacy_notify_file_exception( "name": "test", "platform": "notify", "file_path": "mock_file", + }, + { "timestamp": True, }, ), @@ -203,6 +207,7 @@ async def test_legacy_notify_file_entry_only_setup( timestamp: bool, mock_is_allowed_path: MagicMock, data: dict[str, Any], + options: dict[str, Any], ) -> None: """Test the legacy notify file output in entry only setup.""" filename = "mock_file" @@ -213,7 +218,11 @@ async def test_legacy_notify_file_entry_only_setup( message = params["message"] entry = MockConfigEntry( - domain=DOMAIN, data=data, title=f"test [{data['file_path']}]" + domain=DOMAIN, + data=data, + version=2, + options=options, + title=f"test [{data['file_path']}]", ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -252,7 +261,7 @@ async def test_legacy_notify_file_entry_only_setup( @pytest.mark.parametrize( - ("is_allowed", "config"), + ("is_allowed", "config", "options"), [ ( False, @@ -260,6 +269,8 @@ async def test_legacy_notify_file_entry_only_setup( "name": "test", "platform": "notify", "file_path": "mock_file", + }, + { "timestamp": False, }, ), @@ -271,10 +282,15 @@ async def test_legacy_notify_file_not_allowed( caplog: pytest.LogCaptureFixture, mock_is_allowed_path: MagicMock, config: dict[str, Any], + options: dict[str, Any], ) -> None: """Test legacy notify file output not allowed.""" entry = MockConfigEntry( - domain=DOMAIN, data=config, title=f"test [{config['file_path']}]" + domain=DOMAIN, + data=config, + version=2, + options=options, + title=f"test [{config['file_path']}]", ) entry.add_to_hass(hass) assert not await hass.config_entries.async_setup(entry.entry_id) @@ -293,13 +309,15 @@ async def test_legacy_notify_file_not_allowed( ], ) @pytest.mark.parametrize( - ("data", "is_allowed"), + ("data", "options", "is_allowed"), [ ( { "name": "test", "platform": "notify", "file_path": "mock_file", + }, + { "timestamp": False, }, True, @@ -314,12 +332,17 @@ async def test_notify_file_write_access_failed( service: str, params: dict[str, Any], data: dict[str, Any], + options: dict[str, Any], ) -> None: """Test the notify file fails.""" domain = notify.DOMAIN entry = MockConfigEntry( - domain=DOMAIN, data=data, title=f"test [{data['file_path']}]" + domain=DOMAIN, + data=data, + version=2, + options=options, + title=f"test [{data['file_path']}]", ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/file/test_sensor.py b/tests/components/file/test_sensor.py index 60a81df2b1e..634ae9d626c 100644 --- a/tests/components/file/test_sensor.py +++ b/tests/components/file/test_sensor.py @@ -47,7 +47,11 @@ async def test_file_value_entry_setup( } entry = MockConfigEntry( - domain=DOMAIN, data=data, title=f"test [{data['file_path']}]" + domain=DOMAIN, + data=data, + version=2, + options={}, + title=f"test [{data['file_path']}]", ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -66,11 +70,17 @@ async def test_file_value_template( "platform": "sensor", "name": "file2", "file_path": get_fixture_path("file_value_template.txt", "file"), + } + options = { "value_template": "{{ value_json.temperature }}", } entry = MockConfigEntry( - domain=DOMAIN, data=data, title=f"test [{data['file_path']}]" + domain=DOMAIN, + data=data, + version=2, + options=options, + title=f"test [{data['file_path']}]", ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -90,7 +100,11 @@ async def test_file_empty(hass: HomeAssistant, mock_is_allowed_path: MagicMock) } entry = MockConfigEntry( - domain=DOMAIN, data=data, title=f"test [{data['file_path']}]" + domain=DOMAIN, + data=data, + version=2, + options={}, + title=f"test [{data['file_path']}]", ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -113,7 +127,11 @@ async def test_file_path_invalid( } entry = MockConfigEntry( - domain=DOMAIN, data=data, title=f"test [{data['file_path']}]" + domain=DOMAIN, + data=data, + version=2, + options={}, + title=f"test [{data['file_path']}]", ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) From 46357519e0dff3c43c400000c3d0469b76636533 Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Thu, 15 Aug 2024 12:46:06 -0400 Subject: [PATCH 2363/2411] Add Nice G.O. integration (#122748) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Convert Linear Garage Door to Nice G.O. * Remove useless fixtures * Update manifest (now cloud push! 🎉) * Fix entry unload * Extend config entry type * Fix circular import * Bump nice-go (hopefully fix dep conflict) * Bump nice-go (moves type stubs to dev deps) * Remove lingering mentions of Linear * Add nice-go as logger * Convert nice_go into a new integration and restore linear_garage_door * Add missing new lines to snapshots * Fixes suggested by @joostlek * More fixes * Fixes * Fixes * Fix coordinator tests * Move coordinator tests * Move test_no_connection_state from test_cover to test_init --------- Co-authored-by: Joostlek --- CODEOWNERS | 2 + homeassistant/components/nice_go/__init__.py | 43 +++ .../components/nice_go/config_flow.py | 68 +++++ homeassistant/components/nice_go/const.py | 13 + .../components/nice_go/coordinator.py | 220 +++++++++++++ homeassistant/components/nice_go/cover.py | 72 +++++ homeassistant/components/nice_go/entity.py | 41 +++ .../components/nice_go/manifest.json | 10 + homeassistant/components/nice_go/strings.json | 26 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/nice_go/__init__.py | 22 ++ tests/components/nice_go/conftest.py | 78 +++++ .../nice_go/fixtures/device_state_update.json | 21 ++ .../fixtures/device_state_update_1.json | 21 ++ .../nice_go/fixtures/get_all_barriers.json | 64 ++++ .../nice_go/snapshots/test_cover.ambr | 193 ++++++++++++ .../nice_go/snapshots/test_diagnostics.ambr | 43 +++ .../nice_go/snapshots/test_init.ambr | 16 + .../nice_go/snapshots/test_light.ambr | 223 ++++++++++++++ tests/components/nice_go/test_config_flow.py | 111 +++++++ tests/components/nice_go/test_cover.py | 115 +++++++ tests/components/nice_go/test_init.py | 288 ++++++++++++++++++ 25 files changed, 1703 insertions(+) create mode 100644 homeassistant/components/nice_go/__init__.py create mode 100644 homeassistant/components/nice_go/config_flow.py create mode 100644 homeassistant/components/nice_go/const.py create mode 100644 homeassistant/components/nice_go/coordinator.py create mode 100644 homeassistant/components/nice_go/cover.py create mode 100644 homeassistant/components/nice_go/entity.py create mode 100644 homeassistant/components/nice_go/manifest.json create mode 100644 homeassistant/components/nice_go/strings.json create mode 100644 tests/components/nice_go/__init__.py create mode 100644 tests/components/nice_go/conftest.py create mode 100644 tests/components/nice_go/fixtures/device_state_update.json create mode 100644 tests/components/nice_go/fixtures/device_state_update_1.json create mode 100644 tests/components/nice_go/fixtures/get_all_barriers.json create mode 100644 tests/components/nice_go/snapshots/test_cover.ambr create mode 100644 tests/components/nice_go/snapshots/test_diagnostics.ambr create mode 100644 tests/components/nice_go/snapshots/test_init.ambr create mode 100644 tests/components/nice_go/snapshots/test_light.ambr create mode 100644 tests/components/nice_go/test_config_flow.py create mode 100644 tests/components/nice_go/test_cover.py create mode 100644 tests/components/nice_go/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index a3b38498f68..6593c02c8a5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -968,6 +968,8 @@ build.json @home-assistant/supervisor /tests/components/nfandroidtv/ @tkdrob /homeassistant/components/nibe_heatpump/ @elupus /tests/components/nibe_heatpump/ @elupus +/homeassistant/components/nice_go/ @IceBotYT +/tests/components/nice_go/ @IceBotYT /homeassistant/components/nightscout/ @marciogranzotto /tests/components/nightscout/ @marciogranzotto /homeassistant/components/nilu/ @hfurubotten diff --git a/homeassistant/components/nice_go/__init__.py b/homeassistant/components/nice_go/__init__.py new file mode 100644 index 00000000000..33c81b70966 --- /dev/null +++ b/homeassistant/components/nice_go/__init__.py @@ -0,0 +1,43 @@ +"""The Nice G.O. integration.""" + +from __future__ import annotations + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .coordinator import NiceGOUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) +PLATFORMS: list[Platform] = [Platform.COVER] + +type NiceGOConfigEntry = ConfigEntry[NiceGOUpdateCoordinator] + + +async def async_setup_entry(hass: HomeAssistant, entry: NiceGOConfigEntry) -> bool: + """Set up Nice G.O. from a config entry.""" + + coordinator = NiceGOUpdateCoordinator(hass) + + await coordinator.async_config_entry_first_refresh() + entry.runtime_data = coordinator + + entry.async_create_background_task( + hass, + coordinator.client_listen(), + "nice_go_websocket_task", + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: NiceGOConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + await entry.runtime_data.api.close() + + return unload_ok diff --git a/homeassistant/components/nice_go/config_flow.py b/homeassistant/components/nice_go/config_flow.py new file mode 100644 index 00000000000..9d2c1c05518 --- /dev/null +++ b/homeassistant/components/nice_go/config_flow.py @@ -0,0 +1,68 @@ +"""Config flow for Nice G.O. integration.""" + +from __future__ import annotations + +from datetime import datetime +import logging +from typing import Any + +from nice_go import AuthFailedError, NiceGOApi +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_REFRESH_TOKEN, CONF_REFRESH_TOKEN_CREATION_TIME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_EMAIL): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class NiceGOConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Nice G.O.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_EMAIL]) + self._abort_if_unique_id_configured() + + hub = NiceGOApi() + + try: + refresh_token = await hub.authenticate( + user_input[CONF_EMAIL], + user_input[CONF_PASSWORD], + async_get_clientsession(self.hass), + ) + except AuthFailedError: + errors["base"] = "invalid_auth" + except Exception: # noqa: BLE001 + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=user_input[CONF_EMAIL], + data={ + CONF_EMAIL: user_input[CONF_EMAIL], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_REFRESH_TOKEN: refresh_token, + CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), + }, + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/nice_go/const.py b/homeassistant/components/nice_go/const.py new file mode 100644 index 00000000000..c3caa92c8be --- /dev/null +++ b/homeassistant/components/nice_go/const.py @@ -0,0 +1,13 @@ +"""Constants for the Nice G.O. integration.""" + +from datetime import timedelta + +DOMAIN = "nice_go" + +# Configuration +CONF_SITE_ID = "site_id" +CONF_DEVICE_ID = "device_id" +CONF_REFRESH_TOKEN = "refresh_token" +CONF_REFRESH_TOKEN_CREATION_TIME = "refresh_token_creation_time" + +REFRESH_TOKEN_EXPIRY_TIME = timedelta(days=30) diff --git a/homeassistant/components/nice_go/coordinator.py b/homeassistant/components/nice_go/coordinator.py new file mode 100644 index 00000000000..196ed0a211c --- /dev/null +++ b/homeassistant/components/nice_go/coordinator.py @@ -0,0 +1,220 @@ +"""DataUpdateCoordinator for Nice G.O.""" + +from __future__ import annotations + +import asyncio +from dataclasses import dataclass +from datetime import datetime +import json +import logging +from typing import Any + +from nice_go import ( + BARRIER_STATUS, + ApiError, + AuthFailedError, + BarrierState, + ConnectionState, + NiceGOApi, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import issue_registry as ir +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_REFRESH_TOKEN, + CONF_REFRESH_TOKEN_CREATION_TIME, + DOMAIN, + REFRESH_TOKEN_EXPIRY_TIME, +) + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class NiceGODevice: + """Nice G.O. device dataclass.""" + + id: str + name: str + barrier_status: str + light_status: bool + fw_version: str + connected: bool + + +class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]): + """DataUpdateCoordinator for Nice G.O.""" + + config_entry: ConfigEntry + organization_id: str + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize DataUpdateCoordinator for Nice G.O.""" + super().__init__( + hass, + _LOGGER, + name="Nice G.O.", + ) + + self.refresh_token = self.config_entry.data[CONF_REFRESH_TOKEN] + self.refresh_token_creation_time = self.config_entry.data[ + CONF_REFRESH_TOKEN_CREATION_TIME + ] + self.email = self.config_entry.data[CONF_EMAIL] + self.password = self.config_entry.data[CONF_PASSWORD] + self.api = NiceGOApi() + self.ws_connected = False + + async def _parse_barrier(self, barrier_state: BarrierState) -> NiceGODevice | None: + """Parse barrier data.""" + + device_id = barrier_state.deviceId + name = barrier_state.reported["displayName"] + if barrier_state.reported["migrationStatus"] == "NOT_STARTED": + ir.async_create_issue( + self.hass, + DOMAIN, + f"firmware_update_required_{device_id}", + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="firmware_update_required", + translation_placeholders={"device_name": name}, + ) + return None + ir.async_delete_issue( + self.hass, DOMAIN, f"firmware_update_required_{device_id}" + ) + barrier_status_raw = [ + int(x) for x in barrier_state.reported["barrierStatus"].split(",") + ] + + if BARRIER_STATUS[int(barrier_status_raw[2])] == "STATIONARY": + barrier_status = "open" if barrier_status_raw[0] == 1 else "closed" + else: + barrier_status = BARRIER_STATUS[int(barrier_status_raw[2])].lower() + + light_status = barrier_state.reported["lightStatus"].split(",")[0] == "1" + fw_version = barrier_state.reported["deviceFwVersion"] + if barrier_state.connectionState: + connected = barrier_state.connectionState.connected + else: + connected = False + + return NiceGODevice( + id=device_id, + name=name, + barrier_status=barrier_status, + light_status=light_status, + fw_version=fw_version, + connected=connected, + ) + + async def _async_update_data(self) -> dict[str, NiceGODevice]: + return self.data + + async def _async_setup(self) -> None: + """Set up the coordinator.""" + async with asyncio.timeout(10): + expiry_time = ( + self.refresh_token_creation_time + + REFRESH_TOKEN_EXPIRY_TIME.total_seconds() + ) + try: + if datetime.now().timestamp() >= expiry_time: + await self._update_refresh_token() + else: + await self.api.authenticate_refresh( + self.refresh_token, async_get_clientsession(self.hass) + ) + _LOGGER.debug("Authenticated with Nice G.O. API") + + barriers = await self.api.get_all_barriers() + parsed_barriers = [ + await self._parse_barrier(barrier.state) for barrier in barriers + ] + + # Parse the barriers and save them in a dictionary + devices = { + barrier.id: barrier for barrier in parsed_barriers if barrier + } + self.organization_id = await barriers[0].get_attr("organization") + except AuthFailedError as e: + raise ConfigEntryAuthFailed from e + except ApiError as e: + raise UpdateFailed from e + else: + self.async_set_updated_data(devices) + + async def _update_refresh_token(self) -> None: + """Update the refresh token with Nice G.O. API.""" + _LOGGER.debug("Updating the refresh token with Nice G.O. API") + try: + refresh_token = await self.api.authenticate( + self.email, self.password, async_get_clientsession(self.hass) + ) + except AuthFailedError as e: + _LOGGER.exception("Authentication failed") + raise ConfigEntryAuthFailed from e + except ApiError as e: + _LOGGER.exception("API error") + raise UpdateFailed from e + + self.refresh_token = refresh_token + data = { + **self.config_entry.data, + CONF_REFRESH_TOKEN: refresh_token, + CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), + } + self.hass.config_entries.async_update_entry(self.config_entry, data=data) + + async def client_listen(self) -> None: + """Listen to the websocket for updates.""" + self.api.event(self.on_connected) + self.api.event(self.on_data) + try: + await self.api.connect(reconnect=True) + except ApiError: + _LOGGER.exception("API error") + + if not self.hass.is_stopping: + await asyncio.sleep(5) + await self.client_listen() + + async def on_data(self, data: dict[str, Any]) -> None: + """Handle incoming data from the websocket.""" + _LOGGER.debug("Received data from the websocket") + _LOGGER.debug(data) + raw_data = data["data"]["devicesStatesUpdateFeed"]["item"] + parsed_data = await self._parse_barrier( + BarrierState( + deviceId=raw_data["deviceId"], + desired=json.loads(raw_data["desired"]), + reported=json.loads(raw_data["reported"]), + connectionState=ConnectionState( + connected=raw_data["connectionState"]["connected"], + updatedTimestamp=raw_data["connectionState"]["updatedTimestamp"], + ) + if raw_data["connectionState"] + else None, + version=raw_data["version"], + timestamp=raw_data["timestamp"], + ) + ) + if parsed_data is None: + return + + data_copy = self.data.copy() + data_copy[parsed_data.id] = parsed_data + + self.async_set_updated_data(data_copy) + + async def on_connected(self) -> None: + """Handle the websocket connection.""" + _LOGGER.debug("Connected to the websocket") + await self.api.subscribe(self.organization_id) diff --git a/homeassistant/components/nice_go/cover.py b/homeassistant/components/nice_go/cover.py new file mode 100644 index 00000000000..70bd4b136a5 --- /dev/null +++ b/homeassistant/components/nice_go/cover.py @@ -0,0 +1,72 @@ +"""Cover entity for Nice G.O.""" + +from typing import Any + +from homeassistant.components.cover import ( + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import NiceGOConfigEntry +from .entity import NiceGOEntity + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: NiceGOConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Nice G.O. cover.""" + coordinator = config_entry.runtime_data + + async_add_entities( + NiceGOCoverEntity(coordinator, device_id, device_data.name, "cover") + for device_id, device_data in coordinator.data.items() + ) + + +class NiceGOCoverEntity(NiceGOEntity, CoverEntity): + """Representation of a Nice G.O. cover.""" + + _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + _attr_name = None + _attr_device_class = CoverDeviceClass.GARAGE + + @property + def is_closed(self) -> bool: + """Return if cover is closed.""" + return self.data.barrier_status == "closed" + + @property + def is_opened(self) -> bool: + """Return if cover is open.""" + return self.data.barrier_status == "open" + + @property + def is_opening(self) -> bool: + """Return if cover is opening.""" + return self.data.barrier_status == "opening" + + @property + def is_closing(self) -> bool: + """Return if cover is closing.""" + return self.data.barrier_status == "closing" + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the garage door.""" + if self.is_closed: + return + + await self.coordinator.api.close_barrier(self._device_id) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the garage door.""" + if self.is_opened: + return + + await self.coordinator.api.open_barrier(self._device_id) diff --git a/homeassistant/components/nice_go/entity.py b/homeassistant/components/nice_go/entity.py new file mode 100644 index 00000000000..5af4b9c8731 --- /dev/null +++ b/homeassistant/components/nice_go/entity.py @@ -0,0 +1,41 @@ +"""Base entity for Nice G.O.""" + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import NiceGODevice, NiceGOUpdateCoordinator + + +class NiceGOEntity(CoordinatorEntity[NiceGOUpdateCoordinator]): + """Common base for Nice G.O. entities.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: NiceGOUpdateCoordinator, + device_id: str, + device_name: str, + sub_device_id: str, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + + self._attr_unique_id = f"{device_id}-{sub_device_id}" + self._device_id = device_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_id)}, + name=device_name, + sw_version=coordinator.data[device_id].fw_version, + ) + + @property + def data(self) -> NiceGODevice: + """Return the Nice G.O. device.""" + return self.coordinator.data[self._device_id] + + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self.data.connected diff --git a/homeassistant/components/nice_go/manifest.json b/homeassistant/components/nice_go/manifest.json new file mode 100644 index 00000000000..e86c68b491f --- /dev/null +++ b/homeassistant/components/nice_go/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "nice_go", + "name": "Nice G.O.", + "codeowners": ["@IceBotYT"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/nice_go", + "iot_class": "cloud_push", + "loggers": ["nice-go"], + "requirements": ["nice-go==0.1.6"] +} diff --git a/homeassistant/components/nice_go/strings.json b/homeassistant/components/nice_go/strings.json new file mode 100644 index 00000000000..8d21f6d9740 --- /dev/null +++ b/homeassistant/components/nice_go/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + }, + "issues": { + "firmware_update_required": { + "title": "Firmware update required", + "description": "Your device ({device_name}) requires a firmware update on the Nice G.O. app in order to work with this integration. Please update the firmware on the Nice G.O. app and reconfigure this integration." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 24e151d2902..c3fe4af4a76 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -385,6 +385,7 @@ FLOWS = { "nextdns", "nfandroidtv", "nibe_heatpump", + "nice_go", "nightscout", "nina", "nmap_tracker", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 6d2c5ec9a30..7df27aa5e68 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4029,6 +4029,12 @@ "config_flow": true, "iot_class": "local_polling" }, + "nice_go": { + "name": "Nice G.O.", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_push" + }, "nightscout": { "name": "Nightscout", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index fc8351c9237..a1ce1581826 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1419,6 +1419,9 @@ nextdns==3.1.0 # homeassistant.components.nibe_heatpump nibe==2.11.0 +# homeassistant.components.nice_go +nice-go==0.1.6 + # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a89f6c644d..4b979e53de3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1179,6 +1179,9 @@ nextdns==3.1.0 # homeassistant.components.nibe_heatpump nibe==2.11.0 +# homeassistant.components.nice_go +nice-go==0.1.6 + # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 diff --git a/tests/components/nice_go/__init__.py b/tests/components/nice_go/__init__.py new file mode 100644 index 00000000000..0208795a12c --- /dev/null +++ b/tests/components/nice_go/__init__.py @@ -0,0 +1,22 @@ +"""Tests for the Nice G.O. integration.""" + +from unittest.mock import patch + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration( + hass: HomeAssistant, config_entry: MockConfigEntry, platforms: list[Platform] +) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.nice_go.PLATFORMS", + platforms, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/nice_go/conftest.py b/tests/components/nice_go/conftest.py new file mode 100644 index 00000000000..31b21083c05 --- /dev/null +++ b/tests/components/nice_go/conftest.py @@ -0,0 +1,78 @@ +"""Common fixtures for the Nice G.O. tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from nice_go import Barrier, BarrierState, ConnectionState +import pytest + +from homeassistant.components.nice_go.const import ( + CONF_REFRESH_TOKEN, + CONF_REFRESH_TOKEN_CREATION_TIME, + DOMAIN, +) +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +from tests.common import MockConfigEntry, load_json_array_fixture + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.nice_go.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_nice_go() -> Generator[AsyncMock]: + """Mock a Nice G.O. client.""" + with ( + patch( + "homeassistant.components.nice_go.coordinator.NiceGOApi", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.nice_go.config_flow.NiceGOApi", + new=mock_client, + ), + ): + client = mock_client.return_value + client.authenticate.return_value = "test-refresh-token" + client.authenticate_refresh.return_value = None + client.id_token = None + client.get_all_barriers.return_value = [ + Barrier( + id=barrier["id"], + type=barrier["type"], + controlLevel=barrier["controlLevel"], + attr=barrier["attr"], + state=BarrierState( + **barrier["state"], + connectionState=ConnectionState(**barrier["connectionState"]), + ), + api=client, + ) + for barrier in load_json_array_fixture("get_all_barriers.json", DOMAIN) + ] + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + entry_id="acefdd4b3a4a0911067d1cf51414201e", + title="test-email", + data={ + CONF_EMAIL: "test-email", + CONF_PASSWORD: "test-password", + CONF_REFRESH_TOKEN: "test-refresh-token", + CONF_REFRESH_TOKEN_CREATION_TIME: 1722184160.738171, + }, + version=1, + unique_id="test-email", + ) diff --git a/tests/components/nice_go/fixtures/device_state_update.json b/tests/components/nice_go/fixtures/device_state_update.json new file mode 100644 index 00000000000..53d89c5411b --- /dev/null +++ b/tests/components/nice_go/fixtures/device_state_update.json @@ -0,0 +1,21 @@ +{ + "data": { + "devicesStatesUpdateFeed": { + "receiver": "ORG/0:2372", + "item": { + "deviceId": "1", + "desired": "{\"key\":\"value\"}", + "reported": "{\"displayName\":\"Test Garage 1\",\"autoDisabled\":false,\"migrationStatus\":\"DONE\",\"deviceId\":\"1\",\"lightStatus\":\"0,100\",\"vcnMode\":false,\"deviceFwVersion\":\"1.2.3.4.5.6\",\"barrierStatus\":\"0,0,1,0,-1,0,3,0\"}", + "timestamp": 123, + "version": 123, + "connectionState": { + "connected": true, + "updatedTimestamp": "123", + "__typename": "DeviceConnectionState" + }, + "__typename": "DeviceState" + }, + "__typename": "DeviceStateUpdateNotice" + } + } +} diff --git a/tests/components/nice_go/fixtures/device_state_update_1.json b/tests/components/nice_go/fixtures/device_state_update_1.json new file mode 100644 index 00000000000..cc718e8b093 --- /dev/null +++ b/tests/components/nice_go/fixtures/device_state_update_1.json @@ -0,0 +1,21 @@ +{ + "data": { + "devicesStatesUpdateFeed": { + "receiver": "ORG/0:2372", + "item": { + "deviceId": "2", + "desired": "{\"key\":\"value\"}", + "reported": "{\"displayName\":\"Test Garage 2\",\"autoDisabled\":false,\"migrationStatus\":\"DONE\",\"deviceId\":\"2\",\"lightStatus\":\"1,100\",\"vcnMode\":false,\"deviceFwVersion\":\"1.2.3.4.5.6\",\"barrierStatus\":\"1,100,2,0,-1,0,3,0\"}", + "timestamp": 123, + "version": 123, + "connectionState": { + "connected": true, + "updatedTimestamp": "123", + "__typename": "DeviceConnectionState" + }, + "__typename": "DeviceState" + }, + "__typename": "DeviceStateUpdateNotice" + } + } +} diff --git a/tests/components/nice_go/fixtures/get_all_barriers.json b/tests/components/nice_go/fixtures/get_all_barriers.json new file mode 100644 index 00000000000..481c73d91a8 --- /dev/null +++ b/tests/components/nice_go/fixtures/get_all_barriers.json @@ -0,0 +1,64 @@ +[ + { + "id": "1", + "type": "WallStation", + "controlLevel": "Owner", + "attr": [ + { + "key": "organization", + "value": "test_organization" + } + ], + "state": { + "deviceId": "1", + "desired": { "key": "value" }, + "reported": { + "displayName": "Test Garage 1", + "autoDisabled": false, + "migrationStatus": "DONE", + "deviceId": "1", + "lightStatus": "1,100", + "vcnMode": false, + "deviceFwVersion": "1.2.3.4.5.6", + "barrierStatus": "0,0,0,0,-1,0,3,0" + }, + "timestamp": null, + "version": null + }, + "connectionState": { + "connected": true, + "updatedTimestamp": "123" + } + }, + { + "id": "2", + "type": "WallStation", + "controlLevel": "Owner", + "attr": [ + { + "key": "organization", + "value": "test_organization" + } + ], + "state": { + "deviceId": "2", + "desired": { "key": "value" }, + "reported": { + "displayName": "Test Garage 2", + "autoDisabled": false, + "migrationStatus": "DONE", + "deviceId": "2", + "lightStatus": "0,100", + "vcnMode": false, + "deviceFwVersion": "1.2.3.4.5.6", + "barrierStatus": "1,100,0,0,-1,0,3,0" + }, + "timestamp": null, + "version": null + }, + "connectionState": { + "connected": true, + "updatedTimestamp": "123" + } + } +] diff --git a/tests/components/nice_go/snapshots/test_cover.ambr b/tests/components/nice_go/snapshots/test_cover.ambr new file mode 100644 index 00000000000..8f85fea2726 --- /dev/null +++ b/tests/components/nice_go/snapshots/test_cover.ambr @@ -0,0 +1,193 @@ +# serializer version: 1 +# name: test_covers[cover.test_garage_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.test_garage_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'nice_go', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '1-cover', + 'unit_of_measurement': None, + }) +# --- +# name: test_covers[cover.test_garage_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'garage', + 'friendly_name': 'Test Garage 1', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.test_garage_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'closed', + }) +# --- +# name: test_covers[cover.test_garage_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.test_garage_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'nice_go', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '2-cover', + 'unit_of_measurement': None, + }) +# --- +# name: test_covers[cover.test_garage_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'garage', + 'friendly_name': 'Test Garage 2', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.test_garage_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- +# name: test_covers[cover.test_garage_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.test_garage_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'linear_garage_door', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'test3-GDO', + 'unit_of_measurement': None, + }) +# --- +# name: test_covers[cover.test_garage_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'garage', + 'friendly_name': 'Test Garage 3', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.test_garage_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'opening', + }) +# --- +# name: test_covers[cover.test_garage_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.test_garage_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'linear_garage_door', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'test4-GDO', + 'unit_of_measurement': None, + }) +# --- +# name: test_covers[cover.test_garage_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'garage', + 'friendly_name': 'Test Garage 4', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.test_garage_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'closing', + }) +# --- diff --git a/tests/components/nice_go/snapshots/test_diagnostics.ambr b/tests/components/nice_go/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..b7d564b619b --- /dev/null +++ b/tests/components/nice_go/snapshots/test_diagnostics.ambr @@ -0,0 +1,43 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'coordinator_data': dict({ + '1': dict({ + 'barrier_status': 'closed', + 'connected': True, + 'fw_version': '1.2.3.4.5.6', + 'id': '1', + 'light_status': True, + 'name': 'Test Garage 1', + }), + '2': dict({ + 'barrier_status': 'open', + 'connected': True, + 'fw_version': '1.2.3.4.5.6', + 'id': '2', + 'light_status': False, + 'name': 'Test Garage 2', + }), + }), + 'entry': dict({ + 'data': dict({ + 'email': '**REDACTED**', + 'password': '**REDACTED**', + 'refresh_token': '**REDACTED**', + 'refresh_token_creation_time': 1722184160.738171, + }), + 'disabled_by': None, + 'domain': 'nice_go', + 'entry_id': 'acefdd4b3a4a0911067d1cf51414201e', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': None, + 'version': 1, + }), + }) +# --- diff --git a/tests/components/nice_go/snapshots/test_init.ambr b/tests/components/nice_go/snapshots/test_init.ambr new file mode 100644 index 00000000000..ff389568d1b --- /dev/null +++ b/tests/components/nice_go/snapshots/test_init.ambr @@ -0,0 +1,16 @@ +# serializer version: 1 +# name: test_on_data_none_parsed + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'garage', + 'friendly_name': 'Test Garage 1', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.test_garage_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'closed', + }) +# --- diff --git a/tests/components/nice_go/snapshots/test_light.ambr b/tests/components/nice_go/snapshots/test_light.ambr new file mode 100644 index 00000000000..294488e3d46 --- /dev/null +++ b/tests/components/nice_go/snapshots/test_light.ambr @@ -0,0 +1,223 @@ +# serializer version: 1 +# name: test_data[light.test_garage_1_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.test_garage_1_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light', + 'platform': 'nice_go', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light', + 'unique_id': '1-light', + 'unit_of_measurement': None, + }) +# --- +# name: test_data[light.test_garage_1_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': , + 'friendly_name': 'Test Garage 1 Light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.test_garage_1_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_data[light.test_garage_2_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.test_garage_2_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light', + 'platform': 'nice_go', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light', + 'unique_id': '2-light', + 'unit_of_measurement': None, + }) +# --- +# name: test_data[light.test_garage_2_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': None, + 'friendly_name': 'Test Garage 2 Light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.test_garage_2_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_data[light.test_garage_3_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.test_garage_3_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light', + 'platform': 'linear_garage_door', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light', + 'unique_id': 'test3-Light', + 'unit_of_measurement': None, + }) +# --- +# name: test_data[light.test_garage_3_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Test Garage 3 Light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.test_garage_3_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_data[light.test_garage_4_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.test_garage_4_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light', + 'platform': 'linear_garage_door', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light', + 'unique_id': 'test4-Light', + 'unit_of_measurement': None, + }) +# --- +# name: test_data[light.test_garage_4_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'friendly_name': 'Test Garage 4 Light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.test_garage_4_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/nice_go/test_config_flow.py b/tests/components/nice_go/test_config_flow.py new file mode 100644 index 00000000000..67930b9f752 --- /dev/null +++ b/tests/components/nice_go/test_config_flow.py @@ -0,0 +1,111 @@ +"""Test the Nice G.O. config flow.""" + +from unittest.mock import AsyncMock + +from freezegun.api import FrozenDateTimeFactory +from nice_go import AuthFailedError +import pytest + +from homeassistant.components.nice_go.const import ( + CONF_REFRESH_TOKEN, + CONF_REFRESH_TOKEN_CREATION_TIME, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_form( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_setup_entry: AsyncMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "test-email", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test-email" + assert result["data"][CONF_EMAIL] == "test-email" + assert result["data"][CONF_PASSWORD] == "test-password" + assert result["data"][CONF_REFRESH_TOKEN] == "test-refresh-token" + assert CONF_REFRESH_TOKEN_CREATION_TIME in result["data"] + assert result["result"].unique_id == "test-email" + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [(AuthFailedError, "invalid_auth"), (Exception, "unknown")], +) +async def test_form_exceptions( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_setup_entry: AsyncMock, + side_effect: Exception, + expected_error: str, +) -> None: + """Test we handle invalid auth.""" + mock_nice_go.authenticate.side_effect = side_effect + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "test-email", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": expected_error} + mock_nice_go.authenticate.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "test-email", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_duplicate_device( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_nice_go: AsyncMock, +) -> None: + """Test that duplicate devices are handled.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "test-email", + CONF_PASSWORD: "test-password", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/nice_go/test_cover.py b/tests/components/nice_go/test_cover.py new file mode 100644 index 00000000000..a6eb9bd27fb --- /dev/null +++ b/tests/components/nice_go/test_cover.py @@ -0,0 +1,115 @@ +"""Test Nice G.O. cover.""" + +from unittest.mock import AsyncMock + +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion + +from homeassistant.components.cover import ( + DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, +) +from homeassistant.components.nice_go.const import DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, load_json_object_fixture, snapshot_platform + + +async def test_covers( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that data gets parsed and returned appropriately.""" + + await setup_integration(hass, mock_config_entry, [Platform.COVER]) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_open_cover( + hass: HomeAssistant, mock_nice_go: AsyncMock, mock_config_entry: MockConfigEntry +) -> None: + """Test that opening the cover works as intended.""" + + await setup_integration(hass, mock_config_entry, [Platform.COVER]) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.test_garage_2"}, + blocking=True, + ) + + assert mock_nice_go.open_barrier.call_count == 0 + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.test_garage_1"}, + blocking=True, + ) + + assert mock_nice_go.open_barrier.call_count == 1 + + +async def test_close_cover( + hass: HomeAssistant, mock_nice_go: AsyncMock, mock_config_entry: MockConfigEntry +) -> None: + """Test that closing the cover works as intended.""" + + await setup_integration(hass, mock_config_entry, [Platform.COVER]) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.test_garage_1"}, + blocking=True, + ) + + assert mock_nice_go.close_barrier.call_count == 0 + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.test_garage_2"}, + blocking=True, + ) + + assert mock_nice_go.close_barrier.call_count == 1 + + +async def test_update_cover_state( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that closing the cover works as intended.""" + + await setup_integration(hass, mock_config_entry, [Platform.COVER]) + + assert hass.states.get("cover.test_garage_1").state == STATE_CLOSED + assert hass.states.get("cover.test_garage_2").state == STATE_OPEN + + device_update = load_json_object_fixture("device_state_update.json", DOMAIN) + await mock_config_entry.runtime_data.on_data(device_update) + device_update_1 = load_json_object_fixture("device_state_update_1.json", DOMAIN) + await mock_config_entry.runtime_data.on_data(device_update_1) + + assert hass.states.get("cover.test_garage_1").state == STATE_OPENING + assert hass.states.get("cover.test_garage_2").state == STATE_CLOSING diff --git a/tests/components/nice_go/test_init.py b/tests/components/nice_go/test_init.py new file mode 100644 index 00000000000..d6877d72724 --- /dev/null +++ b/tests/components/nice_go/test_init.py @@ -0,0 +1,288 @@ +"""Test Nice G.O. init.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, MagicMock + +from freezegun.api import FrozenDateTimeFactory +from nice_go import ApiError, AuthFailedError, Barrier, BarrierState +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.nice_go.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_unload_entry( + hass: HomeAssistant, mock_nice_go: AsyncMock, mock_config_entry: MockConfigEntry +) -> None: + """Test the unload entry.""" + + await setup_integration(hass, mock_config_entry, []) + 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 mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + ("side_effect", "entry_state"), + [ + ( + AuthFailedError(), + ConfigEntryState.SETUP_ERROR, + ), + (ApiError(), ConfigEntryState.SETUP_RETRY), + ], +) +async def test_setup_failure( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, + entry_state: ConfigEntryState, +) -> None: + """Test reauth trigger setup.""" + + mock_nice_go.authenticate_refresh.side_effect = side_effect + + await setup_integration(hass, mock_config_entry, []) + assert mock_config_entry.state is entry_state + + +async def test_firmware_update_required( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_config_entry: MockConfigEntry, + issue_registry: ir.IssueRegistry, +) -> None: + """Test firmware update required.""" + + mock_nice_go.get_all_barriers.return_value = [ + Barrier( + id="test-device-id", + type="test-type", + controlLevel="test-control-level", + attr=[{"key": "test-attr", "value": "test-value"}], + state=BarrierState( + deviceId="test-device-id", + reported={ + "displayName": "test-display-name", + "migrationStatus": "NOT_STARTED", + }, + desired=None, + connectionState=None, + version=None, + timestamp=None, + ), + api=mock_nice_go, + ) + ] + + await setup_integration(hass, mock_config_entry, []) + + issue = issue_registry.async_get_issue( + DOMAIN, + "firmware_update_required_test-device-id", + ) + assert issue + + +async def test_update_refresh_token( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test updating refresh token.""" + + await setup_integration(hass, mock_config_entry, [Platform.COVER]) + + assert mock_nice_go.authenticate_refresh.call_count == 1 + assert mock_nice_go.get_all_barriers.call_count == 1 + assert mock_nice_go.authenticate.call_count == 0 + + mock_nice_go.authenticate.return_value = "new-refresh-token" + freezer.tick(timedelta(days=30)) + async_fire_time_changed(hass) + assert await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_nice_go.authenticate_refresh.call_count == 1 + assert mock_nice_go.authenticate.call_count == 1 + assert mock_nice_go.get_all_barriers.call_count == 2 + assert mock_config_entry.data["refresh_token"] == "new-refresh-token" + + +async def test_update_refresh_token_api_error( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test updating refresh token with error.""" + + await setup_integration(hass, mock_config_entry, [Platform.COVER]) + + assert mock_nice_go.authenticate_refresh.call_count == 1 + assert mock_nice_go.get_all_barriers.call_count == 1 + assert mock_nice_go.authenticate.call_count == 0 + + mock_nice_go.authenticate.side_effect = ApiError + freezer.tick(timedelta(days=30)) + async_fire_time_changed(hass) + assert not await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_nice_go.authenticate_refresh.call_count == 1 + assert mock_nice_go.authenticate.call_count == 1 + assert mock_nice_go.get_all_barriers.call_count == 1 + assert mock_config_entry.data["refresh_token"] == "test-refresh-token" + assert "API error" in caplog.text + + +async def test_update_refresh_token_auth_failed( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test updating refresh token with error.""" + + await setup_integration(hass, mock_config_entry, [Platform.COVER]) + + assert mock_nice_go.authenticate_refresh.call_count == 1 + assert mock_nice_go.get_all_barriers.call_count == 1 + assert mock_nice_go.authenticate.call_count == 0 + + mock_nice_go.authenticate.side_effect = AuthFailedError + freezer.tick(timedelta(days=30)) + async_fire_time_changed(hass) + assert not await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_nice_go.authenticate_refresh.call_count == 1 + assert mock_nice_go.authenticate.call_count == 1 + assert mock_nice_go.get_all_barriers.call_count == 1 + assert mock_config_entry.data["refresh_token"] == "test-refresh-token" + assert "Authentication failed" in caplog.text + + +async def test_client_listen_api_error( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, +) -> None: + """Test client listen with error.""" + + mock_nice_go.connect.side_effect = ApiError + + await setup_integration(hass, mock_config_entry, [Platform.COVER]) + + assert "API error" in caplog.text + + mock_nice_go.connect.side_effect = None + + freezer.tick(timedelta(seconds=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert mock_nice_go.connect.call_count == 2 + + +async def test_on_data_none_parsed( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test on data with None parsed.""" + + mock_nice_go.event = MagicMock() + + await setup_integration(hass, mock_config_entry, [Platform.COVER]) + + await mock_nice_go.event.call_args[0][0]( + { + "data": { + "devicesStatesUpdateFeed": { + "item": { + "deviceId": "1", + "desired": '{"key": "value"}', + "reported": '{"displayName":"test-display-name", "migrationStatus":"NOT_STARTED"}', + "connectionState": { + "connected": None, + "updatedTimestamp": None, + }, + "version": None, + "timestamp": None, + } + } + } + } + ) + + assert hass.states.get("cover.test_garage_1") == snapshot + + +async def test_on_connected( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test on connected.""" + + mock_nice_go.event = MagicMock() + + await setup_integration(hass, mock_config_entry, [Platform.COVER]) + + assert mock_nice_go.event.call_count == 2 + + mock_nice_go.subscribe = AsyncMock() + await mock_nice_go.event.call_args_list[0][0][0]() + + assert mock_nice_go.subscribe.call_count == 1 + + +async def test_no_connection_state( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test parsing barrier with no connection state.""" + + mock_nice_go.event = MagicMock() + + await setup_integration(hass, mock_config_entry, [Platform.COVER]) + + assert mock_nice_go.event.call_count == 2 + + await mock_nice_go.event.call_args[0][0]( + { + "data": { + "devicesStatesUpdateFeed": { + "item": { + "deviceId": "1", + "desired": '{"key": "value"}', + "reported": '{"displayName":"Test Garage 1", "migrationStatus":"DONE", "barrierStatus": "1,100,0", "deviceFwVersion": "1.0.0", "lightStatus": "1,100"}', + "connectionState": None, + "version": None, + "timestamp": None, + } + } + } + } + ) + + assert hass.states.get("cover.test_garage_1").state == "unavailable" From 64a68b17f4c6425c2425f76ceaa7096fca028f26 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Aug 2024 18:58:52 +0200 Subject: [PATCH 2364/2411] Simplify recorder.migration._drop_foreign_key_constraints (#123968) --- .../components/recorder/migration.py | 10 +-- tests/components/recorder/test_migrate.py | 61 +++++++++++-------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 9a19ff7084a..185be02e9aa 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -15,7 +15,6 @@ from uuid import UUID import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text, update from sqlalchemy.engine import CursorResult, Engine -from sqlalchemy.engine.interfaces import ReflectedForeignKeyConstraint from sqlalchemy.exc import ( DatabaseError, IntegrityError, @@ -645,7 +644,7 @@ def _update_states_table_with_foreign_key_options( def _drop_foreign_key_constraints( session_maker: Callable[[], Session], engine: Engine, table: str, column: str -) -> list[tuple[str, str, ReflectedForeignKeyConstraint]]: +) -> None: """Drop foreign key constraints for a table on specific columns. This is not supported for SQLite because it does not support @@ -658,11 +657,6 @@ def _drop_foreign_key_constraints( ) inspector = sqlalchemy.inspect(engine) - dropped_constraints = [ - (table, column, foreign_key) - for foreign_key in inspector.get_foreign_keys(table) - if foreign_key["name"] and foreign_key["constrained_columns"] == [column] - ] ## Find matching named constraints and bind the ForeignKeyConstraints to the table tmp_table = Table(table, MetaData()) @@ -685,8 +679,6 @@ def _drop_foreign_key_constraints( ) raise - return dropped_constraints - def _restore_foreign_key_constraints( session_maker: Callable[[], Session], diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 4bc317bdaa7..b56dfe3e189 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -7,7 +7,9 @@ import sys from unittest.mock import ANY, Mock, PropertyMock, call, patch import pytest -from sqlalchemy import create_engine, text +from sqlalchemy import create_engine, inspect, text +from sqlalchemy.engine import Engine +from sqlalchemy.engine.interfaces import ReflectedForeignKeyConstraint from sqlalchemy.exc import ( DatabaseError, InternalError, @@ -973,31 +975,40 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: ], } + def find_constraints( + engine: Engine, table: str, column: str + ) -> list[tuple[str, str, ReflectedForeignKeyConstraint]]: + inspector = inspect(engine) + return [ + (table, column, foreign_key) + for foreign_key in inspector.get_foreign_keys(table) + if foreign_key["name"] and foreign_key["constrained_columns"] == [column] + ] + engine = create_engine(recorder_db_url) db_schema.Base.metadata.create_all(engine) + matching_constraints_1 = [ + dropped_constraint + for table, column, _, _ in constraints_to_recreate + for dropped_constraint in find_constraints(engine, table, column) + ] + assert matching_constraints_1 == expected_dropped_constraints[db_engine] + with Session(engine) as session: session_maker = Mock(return_value=session) - dropped_constraints_1 = [ - dropped_constraint - for table, column, _, _ in constraints_to_recreate - for dropped_constraint in migration._drop_foreign_key_constraints( + for table, column, _, _ in constraints_to_recreate: + migration._drop_foreign_key_constraints( session_maker, engine, table, column ) - ] - assert dropped_constraints_1 == expected_dropped_constraints[db_engine] # Check we don't find the constrained columns again (they are removed) - with Session(engine) as session: - session_maker = Mock(return_value=session) - dropped_constraints_2 = [ - dropped_constraint - for table, column, _, _ in constraints_to_recreate - for dropped_constraint in migration._drop_foreign_key_constraints( - session_maker, engine, table, column - ) - ] - assert dropped_constraints_2 == [] + matching_constraints_2 = [ + dropped_constraint + for table, column, _, _ in constraints_to_recreate + for dropped_constraint in find_constraints(engine, table, column) + ] + assert matching_constraints_2 == [] # Restore the constraints with Session(engine) as session: @@ -1007,16 +1018,12 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: ) # Check we do find the constrained columns again (they are restored) - with Session(engine) as session: - session_maker = Mock(return_value=session) - dropped_constraints_3 = [ - dropped_constraint - for table, column, _, _ in constraints_to_recreate - for dropped_constraint in migration._drop_foreign_key_constraints( - session_maker, engine, table, column - ) - ] - assert dropped_constraints_3 == expected_dropped_constraints[db_engine] + matching_constraints_3 = [ + dropped_constraint + for table, column, _, _ in constraints_to_recreate + for dropped_constraint in find_constraints(engine, table, column) + ] + assert matching_constraints_3 == expected_dropped_constraints[db_engine] engine.dispose() From bf9d621939ce33d843936484eafa860e7cfb1504 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Thu, 15 Aug 2024 19:09:21 +0200 Subject: [PATCH 2365/2411] Revert "Rename sensor to finished downloads in pyLoad integration" (#121483) Revert "Rename sensor to finished downloads in pyLoad integration (#120483)" This reverts commit 8e598ec3ff9b6c96dfe633d5510762c4053279c3. --- homeassistant/components/pyload/strings.json | 2 +- .../pyload/snapshots/test_sensor.ambr | 400 +++++++++--------- 2 files changed, 201 insertions(+), 201 deletions(-) diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 38e17e5016f..bbe6989f5e7 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -74,7 +74,7 @@ "name": "Downloads in queue" }, "total": { - "name": "Finished downloads" + "name": "Total downloads" }, "free_space": { "name": "Free space" diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index c1e5a9d6c3a..69d0387fc8f 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -99,56 +99,6 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_finished_downloads-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_finished_downloads', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Finished downloads', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': 'downloads', - }) -# --- -# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_finished_downloads-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Finished downloads', - 'state_class': , - 'unit_of_measurement': 'downloads', - }), - 'context': , - 'entity_id': 'sensor.pyload_finished_downloads', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- # name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_free_space-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -257,6 +207,56 @@ 'state': 'unavailable', }) # --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_total_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_total_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total downloads', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': 'downloads', + }) +# --- +# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_total_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Total downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', + }), + 'context': , + 'entity_id': 'sensor.pyload_total_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_active_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -357,56 +357,6 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_finished_downloads-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_finished_downloads', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Finished downloads', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': 'downloads', - }) -# --- -# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_finished_downloads-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Finished downloads', - 'state_class': , - 'unit_of_measurement': 'downloads', - }), - 'context': , - 'entity_id': 'sensor.pyload_finished_downloads', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- # name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_free_space-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -515,6 +465,56 @@ 'state': 'unavailable', }) # --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_total_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_total_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total downloads', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': 'downloads', + }) +# --- +# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_total_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Total downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', + }), + 'context': , + 'entity_id': 'sensor.pyload_total_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_sensor_update_exceptions[ParserError][sensor.pyload_active_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -615,56 +615,6 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_update_exceptions[ParserError][sensor.pyload_finished_downloads-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_finished_downloads', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Finished downloads', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': 'downloads', - }) -# --- -# name: test_sensor_update_exceptions[ParserError][sensor.pyload_finished_downloads-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Finished downloads', - 'state_class': , - 'unit_of_measurement': 'downloads', - }), - 'context': , - 'entity_id': 'sensor.pyload_finished_downloads', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- # name: test_sensor_update_exceptions[ParserError][sensor.pyload_free_space-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -773,6 +723,56 @@ 'state': 'unavailable', }) # --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_total_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_total_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total downloads', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': 'downloads', + }) +# --- +# name: test_sensor_update_exceptions[ParserError][sensor.pyload_total_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Total downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', + }), + 'context': , + 'entity_id': 'sensor.pyload_total_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_setup[sensor.pyload_active_downloads-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -873,56 +873,6 @@ 'state': '6', }) # --- -# name: test_setup[sensor.pyload_finished_downloads-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pyload_finished_downloads', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Finished downloads', - 'platform': 'pyload', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'XXXXXXXXXXXXXX_total', - 'unit_of_measurement': 'downloads', - }) -# --- -# name: test_setup[sensor.pyload_finished_downloads-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'pyLoad Finished downloads', - 'state_class': , - 'unit_of_measurement': 'downloads', - }), - 'context': , - 'entity_id': 'sensor.pyload_finished_downloads', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '37', - }) -# --- # name: test_setup[sensor.pyload_free_space-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1031,3 +981,53 @@ 'state': '43.247704', }) # --- +# name: test_setup[sensor.pyload_total_downloads-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pyload_total_downloads', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total downloads', + 'platform': 'pyload', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'XXXXXXXXXXXXXX_total', + 'unit_of_measurement': 'downloads', + }) +# --- +# name: test_setup[sensor.pyload_total_downloads-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'pyLoad Total downloads', + 'state_class': , + 'unit_of_measurement': 'downloads', + }), + 'context': , + 'entity_id': 'sensor.pyload_total_downloads', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '37', + }) +# --- From 29c0a7f32484cca4d55ec73ad4bb6247171be0e2 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Thu, 15 Aug 2024 13:23:35 -0400 Subject: [PATCH 2366/2411] Bump aiorussound to 2.3.2 (#123997) --- homeassistant/components/russound_rio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 7180c3be84f..6c473d94874 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["aiorussound"], "quality_scale": "silver", - "requirements": ["aiorussound==2.3.1"] + "requirements": ["aiorussound==2.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index a1ce1581826..920241247b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.3.1 +aiorussound==2.3.2 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b979e53de3..ef99d18d1fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -332,7 +332,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.3.1 +aiorussound==2.3.2 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 From f58106674729692c66c109f11da58870d4fb2f10 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:02:51 -0300 Subject: [PATCH 2367/2411] Add config flow for platform number in Template (#121849) * Add config flow to select platform in Template * Remove device id duplicate in schema * Add config flow for number platform in Template * Remove mode --- .../components/template/config_flow.py | 37 +++++++ homeassistant/components/template/number.py | 97 ++++++++++++++----- .../components/template/strings.json | 31 ++++++ .../template/snapshots/test_number.ambr | 18 ++++ tests/components/template/test_config_flow.py | 64 ++++++++++++ tests/components/template/test_init.py | 16 +++ tests/components/template/test_number.py | 78 ++++++++++++++- 7 files changed, 314 insertions(+), 27 deletions(-) create mode 100644 tests/components/template/snapshots/test_number.ambr diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index c52a890c1f7..2c12a0d03e9 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -41,6 +41,16 @@ from homeassistant.helpers.schema_config_entry_flow import ( from .binary_sensor import async_create_preview_binary_sensor from .const import CONF_PRESS, CONF_TURN_OFF, CONF_TURN_ON, DOMAIN +from .number import ( + CONF_MAX, + CONF_MIN, + CONF_SET_VALUE, + CONF_STEP, + DEFAULT_MAX_VALUE, + DEFAULT_MIN_VALUE, + DEFAULT_STEP, + async_create_preview_number, +) from .select import CONF_OPTIONS, CONF_SELECT_OPTION from .sensor import async_create_preview_sensor from .switch import async_create_preview_switch @@ -94,6 +104,21 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: vol.Optional(CONF_VERIFY_SSL, default=True): selector.BooleanSelector(), } + if domain == Platform.NUMBER: + schema |= { + vol.Required(CONF_STATE): selector.TemplateSelector(), + vol.Required( + CONF_MIN, default=f"{{{{{DEFAULT_MIN_VALUE}}}}}" + ): selector.TemplateSelector(), + vol.Required( + CONF_MAX, default=f"{{{{{DEFAULT_MAX_VALUE}}}}}" + ): selector.TemplateSelector(), + vol.Required( + CONF_STEP, default=f"{{{{{DEFAULT_STEP}}}}}" + ): selector.TemplateSelector(), + vol.Optional(CONF_SET_VALUE): selector.ActionSelector(), + } + if domain == Platform.SELECT: schema |= _SCHEMA_STATE | { vol.Required(CONF_OPTIONS): selector.TemplateSelector(), @@ -238,6 +263,7 @@ TEMPLATE_TYPES = [ "binary_sensor", "button", "image", + "number", "select", "sensor", "switch", @@ -258,6 +284,11 @@ CONFIG_FLOW = { config_schema(Platform.IMAGE), validate_user_input=validate_user_input(Platform.IMAGE), ), + Platform.NUMBER: SchemaFlowFormStep( + config_schema(Platform.NUMBER), + preview="template", + validate_user_input=validate_user_input(Platform.NUMBER), + ), Platform.SELECT: SchemaFlowFormStep( config_schema(Platform.SELECT), validate_user_input=validate_user_input(Platform.SELECT), @@ -290,6 +321,11 @@ OPTIONS_FLOW = { options_schema(Platform.IMAGE), validate_user_input=validate_user_input(Platform.IMAGE), ), + Platform.NUMBER: SchemaFlowFormStep( + options_schema(Platform.NUMBER), + preview="template", + validate_user_input=validate_user_input(Platform.NUMBER), + ), Platform.SELECT: SchemaFlowFormStep( options_schema(Platform.SELECT), validate_user_input=validate_user_input(Platform.SELECT), @@ -311,6 +347,7 @@ CREATE_PREVIEW_ENTITY: dict[ Callable[[HomeAssistant, str, dict[str, Any]], TemplateEntity], ] = { "binary_sensor": async_create_preview_binary_sensor, + "number": async_create_preview_number, "sensor": async_create_preview_sensor, "switch": async_create_preview_switch, } diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index d4004ee9535..955600a9b9e 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -8,9 +8,6 @@ from typing import Any import voluptuous as vol from homeassistant.components.number import ( - ATTR_MAX, - ATTR_MIN, - ATTR_STEP, ATTR_VALUE, DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE, @@ -18,9 +15,17 @@ from homeassistant.components.number import ( DOMAIN as NUMBER_DOMAIN, NumberEntity, ) -from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_NAME, + CONF_OPTIMISTIC, + CONF_STATE, + CONF_UNIQUE_ID, +) from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, selector +from homeassistant.helpers.device import async_device_info_to_link_from_device_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -37,6 +42,9 @@ from .trigger_entity import TriggerEntity _LOGGER = logging.getLogger(__name__) CONF_SET_VALUE = "set_value" +CONF_MIN = "min" +CONF_MAX = "max" +CONF_STEP = "step" DEFAULT_NAME = "Template Number" DEFAULT_OPTIMISTIC = False @@ -47,9 +55,9 @@ NUMBER_SCHEMA = ( vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template, vol.Required(CONF_STATE): cv.template, vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA, - vol.Required(ATTR_STEP): cv.template, - vol.Optional(ATTR_MIN, default=DEFAULT_MIN_VALUE): cv.template, - vol.Optional(ATTR_MAX, default=DEFAULT_MAX_VALUE): cv.template, + vol.Required(CONF_STEP): cv.template, + vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): cv.template, + vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, } @@ -57,6 +65,17 @@ NUMBER_SCHEMA = ( .extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema) .extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema) ) +NUMBER_CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.template, + vol.Required(CONF_STATE): cv.template, + vol.Required(CONF_STEP): cv.template, + vol.Optional(CONF_SET_VALUE): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_MIN): cv.template, + vol.Optional(CONF_MAX): cv.template, + vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(), + } +) async def _async_create_entities( @@ -99,6 +118,27 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize config entry.""" + _options = dict(config_entry.options) + _options.pop("template_type") + validated_config = NUMBER_CONFIG_SCHEMA(_options) + async_add_entities([TemplateNumber(hass, validated_config, config_entry.entry_id)]) + + +@callback +def async_create_preview_number( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> TemplateNumber: + """Create a preview number.""" + validated_config = NUMBER_CONFIG_SCHEMA(config | {CONF_NAME: name}) + return TemplateNumber(hass, validated_config, None) + + class TemplateNumber(TemplateEntity, NumberEntity): """Representation of a template number.""" @@ -114,16 +154,22 @@ class TemplateNumber(TemplateEntity, NumberEntity): super().__init__(hass, config=config, unique_id=unique_id) assert self._attr_name is not None self._value_template = config[CONF_STATE] - self._command_set_value = Script( - hass, config[CONF_SET_VALUE], self._attr_name, DOMAIN + self._command_set_value = ( + Script(hass, config[CONF_SET_VALUE], self._attr_name, DOMAIN) + if config.get(CONF_SET_VALUE, None) is not None + else None ) - self._step_template = config[ATTR_STEP] - self._min_value_template = config[ATTR_MIN] - self._max_value_template = config[ATTR_MAX] - self._attr_assumed_state = self._optimistic = config[CONF_OPTIMISTIC] + self._step_template = config[CONF_STEP] + self._min_value_template = config[CONF_MIN] + self._max_value_template = config[CONF_MAX] + self._attr_assumed_state = self._optimistic = config.get(CONF_OPTIMISTIC) self._attr_native_step = DEFAULT_STEP self._attr_native_min_value = DEFAULT_MIN_VALUE self._attr_native_max_value = DEFAULT_MAX_VALUE + self._attr_device_info = async_device_info_to_link_from_device_id( + hass, + config.get(CONF_DEVICE_ID), + ) @callback def _async_setup_templates(self) -> None: @@ -161,11 +207,12 @@ class TemplateNumber(TemplateEntity, NumberEntity): if self._optimistic: self._attr_native_value = value self.async_write_ha_state() - await self.async_run_script( - self._command_set_value, - run_variables={ATTR_VALUE: value}, - context=self._context, - ) + if self._command_set_value: + await self.async_run_script( + self._command_set_value, + run_variables={ATTR_VALUE: value}, + context=self._context, + ) class TriggerNumberEntity(TriggerEntity, NumberEntity): @@ -174,9 +221,9 @@ class TriggerNumberEntity(TriggerEntity, NumberEntity): domain = NUMBER_DOMAIN extra_template_keys = ( CONF_STATE, - ATTR_STEP, - ATTR_MIN, - ATTR_MAX, + CONF_STEP, + CONF_MIN, + CONF_MAX, ) def __init__( @@ -203,21 +250,21 @@ class TriggerNumberEntity(TriggerEntity, NumberEntity): def native_min_value(self) -> int: """Return the minimum value.""" return vol.Any(vol.Coerce(float), None)( - self._rendered.get(ATTR_MIN, super().native_min_value) + self._rendered.get(CONF_MIN, super().native_min_value) ) @property def native_max_value(self) -> int: """Return the maximum value.""" return vol.Any(vol.Coerce(float), None)( - self._rendered.get(ATTR_MAX, super().native_max_value) + self._rendered.get(CONF_MAX, super().native_max_value) ) @property def native_step(self) -> int: """Return the increment/decrement step.""" return vol.Any(vol.Coerce(float), None)( - self._rendered.get(ATTR_STEP, super().native_step) + self._rendered.get(CONF_STEP, super().native_step) ) async def async_set_native_value(self, value: float) -> None: diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index b1f14af2202..fa365bf3cfd 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -37,6 +37,21 @@ }, "title": "Template image" }, + "number": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::config::step::sensor::data::state%]", + "step": "Step value", + "set_value": "Actions on set value", + "max": "Maximum value", + "min": "Minimum value" + }, + "data_description": { + "device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" + }, + "title": "Template number" + }, "select": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -70,6 +85,7 @@ "binary_sensor": "Template a binary sensor", "button": "Template a button", "image": "Template a image", + "number": "Template a number", "select": "Template a select", "sensor": "Template a sensor", "switch": "Template a switch" @@ -125,6 +141,21 @@ }, "title": "[%key:component::template::config::step::image::title%]" }, + "number": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::config::step::sensor::data::state%]", + "step": "[%key:component::template::config::step::number::data::step%]", + "set_value": "[%key:component::template::config::step::number::data::set_value%]", + "max": "[%key:component::template::config::step::number::data::max%]", + "min": "[%key:component::template::config::step::number::data::min%]" + }, + "data_description": { + "device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" + }, + "title": "[%key:component::template::config::step::number::title%]" + }, "select": { "data": { "device_id": "[%key:common::config_flow::data::device%]", diff --git a/tests/components/template/snapshots/test_number.ambr b/tests/components/template/snapshots/test_number.ambr new file mode 100644 index 00000000000..d6f5b1e338d --- /dev/null +++ b/tests/components/template/snapshots/test_number.ambr @@ -0,0 +1,18 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'My template', + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 0.1, + }), + 'context': , + 'entity_id': 'number.my_template', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- \ No newline at end of file diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index ff5db52d667..a62370f4261 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -91,6 +91,24 @@ from tests.typing import WebSocketGenerator {"verify_ssl": True}, {}, ), + ( + "number", + {"state": "{{ states('number.one') }}"}, + "30.0", + {"one": "30.0", "two": "20.0"}, + {}, + { + "min": "{{ 0 }}", + "max": "{{ 100 }}", + "step": "{{ 0.1 }}", + }, + { + "min": "{{ 0 }}", + "max": "{{ 100 }}", + "step": "{{ 0.1 }}", + }, + {}, + ), ( "select", {"state": "{{ states('select.one') }}"}, @@ -226,6 +244,20 @@ async def test_config_flow( {"verify_ssl": True}, {"verify_ssl": True}, ), + ( + "number", + {"state": "{{ states('number.one') }}"}, + { + "min": "{{ 0 }}", + "max": "{{ 100 }}", + "step": "{{ 0.1 }}", + }, + { + "min": "{{ 0 }}", + "max": "{{ 100 }}", + "step": "{{ 0.1 }}", + }, + ), ( "select", {"state": "{{ states('select.one') }}"}, @@ -402,6 +434,24 @@ def get_suggested(schema, key): }, "url", ), + ( + "number", + {"state": "{{ states('number.one') }}"}, + {"state": "{{ states('number.two') }}"}, + ["30.0", "20.0"], + {"one": "30.0", "two": "20.0"}, + { + "min": "{{ 0 }}", + "max": "{{ 100 }}", + "step": "{{ 0.1 }}", + }, + { + "min": "{{ 0 }}", + "max": "{{ 100 }}", + "step": "{{ 0.1 }}", + }, + "state", + ), ( "select", {"state": "{{ states('select.one') }}"}, @@ -1156,6 +1206,20 @@ async def test_option_flow_sensor_preview_config_entry_removed( {}, {}, ), + ( + "number", + {"state": "{{ states('number.one') }}"}, + { + "min": "{{ 0 }}", + "max": "{{ 100 }}", + "step": "{{ 0.1 }}", + }, + { + "min": "{{ 0 }}", + "max": "{{ 100 }}", + "step": "{{ 0.1 }}", + }, + ), ( "select", {"state": "{{ states('select.one') }}"}, diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index fe08e1f4963..2e5870217a2 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -314,6 +314,22 @@ async def async_yaml_patch_helper(hass, filename): }, {}, ), + ( + { + "template_type": "number", + "name": "My template", + "state": "{{ 10 }}", + "min": "{{ 0 }}", + "max": "{{ 100 }}", + "step": "{{ 0.1 }}", + }, + { + "state": "{{ 11 }}", + "min": "{{ 0 }}", + "max": "{{ 100 }}", + "step": "{{ 0.1 }}", + }, + ), ( { "template_type": "select", diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index bf04151fd36..ca9fe2d7688 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -1,5 +1,7 @@ """The tests for the Template number platform.""" +from syrupy.assertion import SnapshotAssertion + from homeassistant import setup from homeassistant.components.input_number import ( ATTR_VALUE as INPUT_NUMBER_ATTR_VALUE, @@ -14,11 +16,12 @@ from homeassistant.components.number import ( DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE as NUMBER_SERVICE_SET_VALUE, ) +from homeassistant.components.template import DOMAIN from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import Context, HomeAssistant, ServiceCall -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er -from tests.common import assert_setup_component, async_capture_events +from tests.common import MockConfigEntry, assert_setup_component, async_capture_events _TEST_NUMBER = "number.template_number" # Represent for number's value @@ -42,6 +45,35 @@ _VALUE_INPUT_NUMBER_CONFIG = { } +async def test_setup_config_entry( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test the config flow.""" + + template_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My template", + "template_type": "number", + "state": "{{ 10 }}", + "min": "{{ 0 }}", + "max": "{{ 100 }}", + "step": "{{ 0.1 }}", + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("number.my_template") + assert state is not None + assert state == snapshot + + async def test_missing_optional_config(hass: HomeAssistant) -> None: """Test: missing optional template is ok.""" with assert_setup_component(1, "template"): @@ -460,3 +492,45 @@ async def test_icon_template_with_trigger(hass: HomeAssistant) -> None: state = hass.states.get(_TEST_NUMBER) assert float(state.state) == 51 assert state.attributes[ATTR_ICON] == "mdi:greater" + + +async def test_device_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test for device for number template.""" + + device_config_entry = MockConfigEntry() + device_config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=device_config_entry.entry_id, + identifiers={("test", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + await hass.async_block_till_done() + assert device_entry is not None + assert device_entry.id is not None + + template_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My template", + "template_type": "number", + "state": "{{ 10 }}", + "min": "{{ 0 }}", + "max": "{{ 100 }}", + "step": "{{ 0.1 }}", + "device_id": device_entry.id, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + template_entity = entity_registry.async_get("number.my_template") + assert template_entity is not None + assert template_entity.device_id == device_entry.id From 65fa4a34ed9eaef61b0edd62b4155b6dbea366fd Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:05:28 -0400 Subject: [PATCH 2368/2411] Add configuration url to russound device (#124001) feat: add configuration url --- homeassistant/components/russound_rio/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/russound_rio/entity.py b/homeassistant/components/russound_rio/entity.py index 150c4e285d1..0e4d5cf7dde 100644 --- a/homeassistant/components/russound_rio/entity.py +++ b/homeassistant/components/russound_rio/entity.py @@ -53,6 +53,7 @@ class RussoundBaseEntity(Entity): or f"{self._primary_mac_address}-{self._controller.controller_id}" ) self._attr_device_info = DeviceInfo( + configuration_url=f"http://{self._instance.host}", # Use MAC address of Russound device as identifier identifiers={(DOMAIN, self._device_identifier)}, manufacturer="Russound", From 7d552b64f7ebcf743c2c479fc3cc7c32d50650e4 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Thu, 15 Aug 2024 20:06:23 +0200 Subject: [PATCH 2369/2411] Use `clearCompletedTodos` API endpoint for deleting Habitica todos (#121877) Use clearCompletedTodos endpoint for deleting multiple completed todo items --- homeassistant/components/habitica/strings.json | 5 ++++- homeassistant/components/habitica/todo.py | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 5696e6f9911..21d2622245c 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -100,7 +100,10 @@ }, "exceptions": { "delete_todos_failed": { - "message": "Unable to delete {count} Habitica to-do(s), please try again" + "message": "Unable to delete item from Habitica to-do list, please try again" + }, + "delete_completed_todos_failed": { + "message": "Unable to delete completed to-do items from Habitica to-do list, please try again" }, "move_todos_item_failed": { "message": "Unable to move the Habitica to-do to position {pos}, please try again" diff --git a/homeassistant/components/habitica/todo.py b/homeassistant/components/habitica/todo.py index ab458f9f59f..451db1e6806 100644 --- a/homeassistant/components/habitica/todo.py +++ b/homeassistant/components/habitica/todo.py @@ -75,14 +75,23 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity): async def async_delete_todo_items(self, uids: list[str]) -> None: """Delete Habitica tasks.""" - for task_id in uids: + if len(uids) > 1 and self.entity_description.key is HabiticaTodoList.TODOS: try: - await self.coordinator.api.tasks[task_id].delete() + await self.coordinator.api.tasks.clearCompletedTodos.post() except ClientResponseError as e: raise ServiceValidationError( translation_domain=DOMAIN, - translation_key=f"delete_{self.entity_description.key}_failed", + translation_key="delete_completed_todos_failed", ) from e + else: + for task_id in uids: + try: + await self.coordinator.api.tasks[task_id].delete() + except ClientResponseError as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key=f"delete_{self.entity_description.key}_failed", + ) from e await self.coordinator.async_refresh() From 142469be9558500c5ef78e280b2742672b998570 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Thu, 15 Aug 2024 11:25:56 -0700 Subject: [PATCH 2370/2411] TotalConnect state attribute deprecation warning (#122320) * add warning comment * make comments smaller and put at top * Update homeassistant/components/totalconnect/alarm_control_panel.py --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/totalconnect/alarm_control_panel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 49d97e45e00..edbbbb06e70 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -103,6 +103,7 @@ class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity): @property def state(self) -> str | None: """Return the state of the device.""" + # State attributes can be removed in 2025.3 attr = { "location_id": self._location.location_id, "partition": self._partition_id, From 37328c78c142d430abd6a5dd2c07fe93a1142743 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 15 Aug 2024 21:29:32 +0200 Subject: [PATCH 2371/2411] Use snapshot in UniFi switch tests (#122871) * Use snapshot in UniFi switch tests * Fix review comment --- .../unifi/snapshots/test_switch.ambr | 2473 +++++++++++++++++ tests/components/unifi/test_switch.py | 132 +- 2 files changed, 2518 insertions(+), 87 deletions(-) create mode 100644 tests/components/unifi/snapshots/test_switch.ambr diff --git a/tests/components/unifi/snapshots/test_switch.ambr b/tests/components/unifi/snapshots/test_switch.ambr new file mode 100644 index 00000000000..04b15f329fd --- /dev/null +++ b/tests/components/unifi/snapshots/test_switch.ambr @@ -0,0 +1,2473 @@ +# serializer version: 1 +# name: test_entity_and_device_data[site_payload0-device_payload0][button.mock_name_port_1_power_cycle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.mock_name_port_1_power_cycle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Port 1 Power Cycle', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power_cycle-10:00:00:00:01:01_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][button.mock_name_port_1_power_cycle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'mock-name Port 1 Power Cycle', + }), + 'context': , + 'entity_id': 'button.mock_name_port_1_power_cycle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][button.mock_name_port_2_power_cycle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.mock_name_port_2_power_cycle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Port 2 Power Cycle', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power_cycle-10:00:00:00:01:01_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][button.mock_name_port_2_power_cycle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'mock-name Port 2 Power Cycle', + }), + 'context': , + 'entity_id': 'button.mock_name_port_2_power_cycle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][button.mock_name_port_4_power_cycle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.mock_name_port_4_power_cycle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Port 4 Power Cycle', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'power_cycle-10:00:00:00:01:01_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][button.mock_name_port_4_power_cycle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'mock-name Port 4 Power Cycle', + }), + 'context': , + 'entity_id': 'button.mock_name_port_4_power_cycle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][button.mock_name_restart-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.mock_name_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'device_restart-10:00:00:00:01:01', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][button.mock_name_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'mock-name Restart', + }), + 'context': , + 'entity_id': 'button.mock_name_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][switch.mock_name_port_1_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_1_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 1 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][switch.mock_name_port_1_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 1 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_1_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][switch.mock_name_port_2_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_2_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 2 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][switch.mock_name_port_2_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 2 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_2_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][switch.mock_name_port_4_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_4_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 4 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-device_payload0][switch.mock_name_port_4_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 4 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_4_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.block_media_streaming-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.block_media_streaming', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:network', + 'original_name': 'Block Media Streaming', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '5f976f4ae3c58f018ec7dff6', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.block_media_streaming-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Block Media Streaming', + 'icon': 'mdi:network', + }), + 'context': , + 'entity_id': 'switch.block_media_streaming', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.dummy_usp_pdu_pro_outlet_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.dummy_usp_pdu_pro_outlet_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outlet 2', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-01:02:03:04:05:ff_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.dummy_usp_pdu_pro_outlet_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Dummy USP-PDU-Pro Outlet 2', + }), + 'context': , + 'entity_id': 'switch.dummy_usp_pdu_pro_outlet_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.dummy_usp_pdu_pro_usb_outlet_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.dummy_usp_pdu_pro_usb_outlet_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'USB Outlet 1', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-01:02:03:04:05:ff_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.dummy_usp_pdu_pro_usb_outlet_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Dummy USP-PDU-Pro USB Outlet 1', + }), + 'context': , + 'entity_id': 'switch.dummy_usp_pdu_pro_usb_outlet_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_1_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_1_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 1 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_1_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 1 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_1_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_2_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_2_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 2 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_2_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 2 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_2_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_4_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_4_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 4 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_4_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 4 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_4_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.plug_outlet_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.plug_outlet_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outlet 1', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-fc:ec:da:76:4f:5f_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.plug_outlet_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Plug Outlet 1', + }), + 'context': , + 'entity_id': 'switch.plug_outlet_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.block_media_streaming-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.block_media_streaming', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:network', + 'original_name': 'Block Media Streaming', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '5f976f4ae3c58f018ec7dff6', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.block_media_streaming-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Block Media Streaming', + 'icon': 'mdi:network', + }), + 'context': , + 'entity_id': 'switch.block_media_streaming', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.dummy_usp_pdu_pro_outlet_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.dummy_usp_pdu_pro_outlet_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outlet 2', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-01:02:03:04:05:ff_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.dummy_usp_pdu_pro_outlet_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Dummy USP-PDU-Pro Outlet 2', + }), + 'context': , + 'entity_id': 'switch.dummy_usp_pdu_pro_outlet_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.dummy_usp_pdu_pro_usb_outlet_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.dummy_usp_pdu_pro_usb_outlet_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'USB Outlet 1', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-01:02:03:04:05:ff_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.dummy_usp_pdu_pro_usb_outlet_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Dummy USP-PDU-Pro USB Outlet 1', + }), + 'context': , + 'entity_id': 'switch.dummy_usp_pdu_pro_usb_outlet_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_1_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_1_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 1 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_1_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 1 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_1_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_2_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_2_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 2 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_2_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 2 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_2_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_4_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_4_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 4 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_4_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 4 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_4_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.plug_outlet_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.plug_outlet_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outlet 1', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-fc:ec:da:76:4f:5f_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.plug_outlet_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Plug Outlet 1', + }), + 'context': , + 'entity_id': 'switch.plug_outlet_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.ssid_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ssid_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:wifi-check', + 'original_name': None, + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'wlan-012345678910111213141516', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.ssid_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'SSID 1', + 'icon': 'mdi:wifi-check', + }), + 'context': , + 'entity_id': 'switch.ssid_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.block_client_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.block_client_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': None, + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'block-00:00:00:00:01:01', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.block_client_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Block Client 1', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.block_client_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.block_media_streaming-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.block_media_streaming', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:network', + 'original_name': 'Block Media Streaming', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '5f976f4ae3c58f018ec7dff6', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.block_media_streaming-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Block Media Streaming', + 'icon': 'mdi:network', + }), + 'context': , + 'entity_id': 'switch.block_media_streaming', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.dummy_usp_pdu_pro_outlet_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.dummy_usp_pdu_pro_outlet_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outlet 2', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-01:02:03:04:05:ff_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.dummy_usp_pdu_pro_outlet_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Dummy USP-PDU-Pro Outlet 2', + }), + 'context': , + 'entity_id': 'switch.dummy_usp_pdu_pro_outlet_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.dummy_usp_pdu_pro_usb_outlet_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.dummy_usp_pdu_pro_usb_outlet_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'USB Outlet 1', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-01:02:03:04:05:ff_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.dummy_usp_pdu_pro_usb_outlet_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Dummy USP-PDU-Pro USB Outlet 1', + }), + 'context': , + 'entity_id': 'switch.dummy_usp_pdu_pro_usb_outlet_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.mock_name_port_1_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_1_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 1 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.mock_name_port_1_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 1 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_1_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.mock_name_port_2_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_2_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 2 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.mock_name_port_2_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 2 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_2_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.mock_name_port_4_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_4_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 4 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.mock_name_port_4_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 4 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_4_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.plug_outlet_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.plug_outlet_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outlet 1', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-fc:ec:da:76:4f:5f_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.plug_outlet_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Plug Outlet 1', + }), + 'context': , + 'entity_id': 'switch.plug_outlet_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.ssid_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ssid_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:wifi-check', + 'original_name': None, + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'wlan-012345678910111213141516', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.ssid_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'SSID 1', + 'icon': 'mdi:wifi-check', + }), + 'context': , + 'entity_id': 'switch.ssid_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.unifi_network_plex-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.unifi_network_plex', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:upload-network', + 'original_name': 'plex', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'port_forward-5a32aa4ee4b0412345678911', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.unifi_network_plex-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'UniFi Network plex', + 'icon': 'mdi:upload-network', + }), + 'context': , + 'entity_id': 'switch.unifi_network_plex', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.block_media_streaming-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.block_media_streaming', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:network', + 'original_name': 'Block Media Streaming', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '5f976f4ae3c58f018ec7dff6', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.block_media_streaming-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Block Media Streaming', + 'icon': 'mdi:network', + }), + 'context': , + 'entity_id': 'switch.block_media_streaming', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.dummy_usp_pdu_pro_outlet_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.dummy_usp_pdu_pro_outlet_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outlet 2', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-01:02:03:04:05:ff_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.dummy_usp_pdu_pro_outlet_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Dummy USP-PDU-Pro Outlet 2', + }), + 'context': , + 'entity_id': 'switch.dummy_usp_pdu_pro_outlet_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.dummy_usp_pdu_pro_usb_outlet_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.dummy_usp_pdu_pro_usb_outlet_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'USB Outlet 1', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-01:02:03:04:05:ff_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.dummy_usp_pdu_pro_usb_outlet_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Dummy USP-PDU-Pro USB Outlet 1', + }), + 'context': , + 'entity_id': 'switch.dummy_usp_pdu_pro_usb_outlet_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_1_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_1_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 1 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_1_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 1 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_1_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_2_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_2_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 2 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_2_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 2 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_2_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_4_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_4_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 4 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.mock_name_port_4_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 4 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_4_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.plug_outlet_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.plug_outlet_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outlet 1', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-fc:ec:da:76:4f:5f_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.plug_outlet_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Plug Outlet 1', + }), + 'context': , + 'entity_id': 'switch.plug_outlet_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.ssid_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ssid_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:wifi-check', + 'original_name': None, + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'wlan-012345678910111213141516', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.ssid_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'SSID 1', + 'icon': 'mdi:wifi-check', + }), + 'context': , + 'entity_id': 'switch.ssid_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.unifi_network_plex-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.unifi_network_plex', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:upload-network', + 'original_name': 'plex', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'port_forward-5a32aa4ee4b0412345678911', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0][switch.unifi_network_plex-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'UniFi Network plex', + 'icon': 'mdi:upload-network', + }), + 'context': , + 'entity_id': 'switch.unifi_network_plex', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.block_client_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.block_client_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': None, + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'block-00:00:00:00:01:01', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.block_client_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Block Client 1', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.block_client_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.block_media_streaming-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.block_media_streaming', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:network', + 'original_name': 'Block Media Streaming', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '5f976f4ae3c58f018ec7dff6', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.block_media_streaming-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Block Media Streaming', + 'icon': 'mdi:network', + }), + 'context': , + 'entity_id': 'switch.block_media_streaming', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.dummy_usp_pdu_pro_outlet_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.dummy_usp_pdu_pro_outlet_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outlet 2', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-01:02:03:04:05:ff_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.dummy_usp_pdu_pro_outlet_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Dummy USP-PDU-Pro Outlet 2', + }), + 'context': , + 'entity_id': 'switch.dummy_usp_pdu_pro_outlet_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.dummy_usp_pdu_pro_usb_outlet_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.dummy_usp_pdu_pro_usb_outlet_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'USB Outlet 1', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-01:02:03:04:05:ff_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.dummy_usp_pdu_pro_usb_outlet_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Dummy USP-PDU-Pro USB Outlet 1', + }), + 'context': , + 'entity_id': 'switch.dummy_usp_pdu_pro_usb_outlet_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.mock_name_port_1_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_1_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 1 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.mock_name_port_1_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 1 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_1_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.mock_name_port_2_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_2_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 2 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.mock_name_port_2_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 2 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_2_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.mock_name_port_4_poe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mock_name_port_4_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 4 PoE', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'poe-10:00:00:00:01:01_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.mock_name_port_4_poe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'mock-name Port 4 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.mock_name_port_4_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.plug_outlet_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.plug_outlet_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outlet 1', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'outlet-fc:ec:da:76:4f:5f_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.plug_outlet_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Plug Outlet 1', + }), + 'context': , + 'entity_id': 'switch.plug_outlet_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.ssid_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ssid_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:wifi-check', + 'original_name': None, + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'wlan-012345678910111213141516', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.ssid_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'SSID 1', + 'icon': 'mdi:wifi-check', + }), + 'context': , + 'entity_id': 'switch.ssid_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.unifi_network_plex-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.unifi_network_plex', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:upload-network', + 'original_name': 'plex', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'port_forward-5a32aa4ee4b0412345678911', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.unifi_network_plex-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'UniFi Network plex', + 'icon': 'mdi:upload-network', + }), + 'context': , + 'entity_id': 'switch.unifi_network_plex', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.unifi_network_test_traffic_rule-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.unifi_network_test_traffic_rule', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:security-network', + 'original_name': 'Test Traffic Rule', + 'platform': 'unifi', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'traffic_rule-6452cd9b859d5b11aa002ea1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_and_device_data[site_payload0-wlan_payload0-traffic_rule_payload0-port_forward_payload0-dpi_group_payload0-dpi_app_payload0-device_payload0-client_payload0-config_entry_options0][switch.unifi_network_test_traffic_rule-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'UniFi Network Test Traffic Rule', + 'icon': 'mdi:security-network', + }), + 'context': , + 'entity_id': 'switch.unifi_network_test_traffic_rule', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 6d85437a244..ef93afa7e3e 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -3,15 +3,16 @@ from copy import deepcopy from datetime import timedelta from typing import Any +from unittest.mock import patch from aiounifi.models.message import MessageKey import pytest +from syrupy import SnapshotAssertion from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SwitchDeviceClass, ) from homeassistant.components.unifi.const import ( CONF_BLOCK_CLIENT, @@ -23,13 +24,12 @@ from homeassistant.components.unifi.const import ( ) from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( - ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, CONF_HOST, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, - EntityCategory, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -43,7 +43,7 @@ from .conftest import ( WebsocketStateManager, ) -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform from tests.test_util.aiohttp import AiohttpClientMocker CLIENT_1 = { @@ -810,6 +810,34 @@ TRAFFIC_RULE = { } +@pytest.mark.parametrize( + "config_entry_options", [{CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}] +) +@pytest.mark.parametrize("client_payload", [[BLOCKED]]) +@pytest.mark.parametrize("device_payload", [[DEVICE_1, OUTLET_UP1, PDU_DEVICE_1]]) +@pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) +@pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) +@pytest.mark.parametrize("port_forward_payload", [[PORT_FORWARD_PLEX]]) +@pytest.mark.parametrize(("traffic_rule_payload"), [([TRAFFIC_RULE])]) +@pytest.mark.parametrize("wlan_payload", [[WLAN]]) +@pytest.mark.parametrize( + "site_payload", + [[{"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}]], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_entity_and_device_data( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_factory: ConfigEntryFactoryType, + site_payload: dict[str, Any], + snapshot: SnapshotAssertion, +) -> None: + """Validate entity and device data with and without admin rights.""" + with patch("homeassistant.components.unifi.PLATFORMS", [Platform.SWITCH]): + config_entry = await config_entry_factory() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + @pytest.mark.parametrize("client_payload", [[CONTROLLER_HOST]]) @pytest.mark.parametrize("device_payload", [[DEVICE_1]]) @pytest.mark.usefixtures("config_entry_setup") @@ -819,18 +847,6 @@ async def test_hub_not_client(hass: HomeAssistant) -> None: assert hass.states.get("switch.cloud_key") is None -@pytest.mark.parametrize("client_payload", [[CLIENT_1]]) -@pytest.mark.parametrize("device_payload", [[DEVICE_1]]) -@pytest.mark.parametrize( - "site_payload", - [[{"desc": "Site name", "name": "site_id", "role": "not admin", "_id": "1"}]], -) -@pytest.mark.usefixtures("config_entry_setup") -async def test_not_admin(hass: HomeAssistant) -> None: - """Test that switch platform only work on an admin account.""" - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 - - @pytest.mark.parametrize( "config_entry_options", [ @@ -841,40 +857,17 @@ async def test_not_admin(hass: HomeAssistant) -> None: } ], ) -@pytest.mark.parametrize("client_payload", [[CLIENT_4]]) @pytest.mark.parametrize("clients_all_payload", [[BLOCKED, UNBLOCKED, CLIENT_1]]) @pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) @pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) async def test_switches( hass: HomeAssistant, - entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, config_entry_setup: MockConfigEntry, ) -> None: """Test the update_items function with some clients.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 3 - switch_4 = hass.states.get("switch.poe_client_4") - assert switch_4 is None - - blocked = hass.states.get("switch.block_client_1") - assert blocked is not None - assert blocked.state == "off" - - unblocked = hass.states.get("switch.block_client_2") - assert unblocked is not None - assert unblocked.state == "on" - - dpi_switch = hass.states.get("switch.block_media_streaming") - assert dpi_switch is not None - assert dpi_switch.state == "on" - assert dpi_switch.attributes["icon"] == "mdi:network" - - for entry_id in ("switch.block_client_1", "switch.block_media_streaming"): - assert ( - entity_registry.async_get(entry_id).entity_category is EntityCategory.CONFIG - ) - # Block and unblock client aioclient_mock.clear_requests() aioclient_mock.post( @@ -1038,10 +1031,7 @@ async def test_dpi_switches( """Test the update_items function with some clients.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - dpi_switch = hass.states.get("switch.block_media_streaming") - assert dpi_switch is not None - assert dpi_switch.state == STATE_ON - assert dpi_switch.attributes["icon"] == "mdi:network" + assert hass.states.get("switch.block_media_streaming").state == STATE_ON mock_websocket_message(data=DPI_APP_DISABLED_EVENT) await hass.async_block_till_done() @@ -1118,20 +1108,18 @@ async def test_traffic_rules( traffic_rule_payload: list[dict[str, Any]], ) -> None: """Test control of UniFi traffic rules.""" - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 # Validate state object - switch_1 = hass.states.get("switch.unifi_network_test_traffic_rule") - assert switch_1.state == STATE_ON - assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.SWITCH + assert hass.states.get("switch.unifi_network_test_traffic_rule").state == STATE_ON traffic_rule = deepcopy(traffic_rule_payload[0]) # Disable traffic rule aioclient_mock.put( f"https://{config_entry_setup.data[CONF_HOST]}:1234" - f"/v2/api/site/{config_entry_setup.data[CONF_SITE_ID]}/trafficrules/{traffic_rule['_id']}", + f"/v2/api/site/{config_entry_setup.data[CONF_SITE_ID]}" + f"/trafficrules/{traffic_rule['_id']}", ) call_count = aioclient_mock.call_count @@ -1188,10 +1176,7 @@ async def test_outlet_switches( assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == expected_switches # Validate state object - switch_1 = hass.states.get(f"switch.{entity_id}") - assert switch_1 is not None - assert switch_1.state == STATE_ON - assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.OUTLET + assert hass.states.get(f"switch.{entity_id}").state == STATE_ON # Update state object device_1 = deepcopy(device_payload[0]) @@ -1250,15 +1235,6 @@ async def test_outlet_switches( await hass.async_block_till_done() assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF - # Unload config entry - await hass.config_entries.async_unload(config_entry_setup.entry_id) - assert hass.states.get(f"switch.{entity_id}").state == STATE_UNAVAILABLE - - # Remove config entry - await hass.config_entries.async_remove(config_entry_setup.entry_id) - await hass.async_block_till_done() - assert hass.states.get(f"switch.{entity_id}") is None - @pytest.mark.parametrize( "config_entry_options", @@ -1359,8 +1335,8 @@ async def test_poe_port_switches( hass: HomeAssistant, entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - mock_websocket_message: WebsocketMessageMock, config_entry_setup: MockConfigEntry, + mock_websocket_message: WebsocketMessageMock, device_payload: list[dict[str, Any]], ) -> None: """Test PoE port entities work.""" @@ -1368,7 +1344,6 @@ async def test_poe_port_switches( ent_reg_entry = entity_registry.async_get("switch.mock_name_port_1_poe") assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION - assert ent_reg_entry.entity_category is EntityCategory.CONFIG # Enable entity entity_registry.async_update_entity( @@ -1385,10 +1360,7 @@ async def test_poe_port_switches( await hass.async_block_till_done() # Validate state object - switch_1 = hass.states.get("switch.mock_name_port_1_poe") - assert switch_1 is not None - assert switch_1.state == STATE_ON - assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.OUTLET + assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_ON # Update state object device_1 = deepcopy(device_payload[0]) @@ -1456,24 +1428,16 @@ async def test_poe_port_switches( @pytest.mark.parametrize("wlan_payload", [[WLAN]]) async def test_wlan_switches( hass: HomeAssistant, - entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - mock_websocket_message: WebsocketMessageMock, config_entry_setup: MockConfigEntry, + mock_websocket_message: WebsocketMessageMock, wlan_payload: list[dict[str, Any]], ) -> None: """Test control of UniFi WLAN availability.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - ent_reg_entry = entity_registry.async_get("switch.ssid_1") - assert ent_reg_entry.unique_id == "wlan-012345678910111213141516" - assert ent_reg_entry.entity_category is EntityCategory.CONFIG - # Validate state object - switch_1 = hass.states.get("switch.ssid_1") - assert switch_1 is not None - assert switch_1.state == STATE_ON - assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.SWITCH + assert hass.states.get("switch.ssid_1").state == STATE_ON # Update state object wlan = deepcopy(wlan_payload[0]) @@ -1512,24 +1476,16 @@ async def test_wlan_switches( @pytest.mark.parametrize("port_forward_payload", [[PORT_FORWARD_PLEX]]) async def test_port_forwarding_switches( hass: HomeAssistant, - entity_registry: er.EntityRegistry, aioclient_mock: AiohttpClientMocker, - mock_websocket_message: WebsocketMessageMock, config_entry_setup: MockConfigEntry, + mock_websocket_message: WebsocketMessageMock, port_forward_payload: list[dict[str, Any]], ) -> None: """Test control of UniFi port forwarding.""" assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - ent_reg_entry = entity_registry.async_get("switch.unifi_network_plex") - assert ent_reg_entry.unique_id == "port_forward-5a32aa4ee4b0412345678911" - assert ent_reg_entry.entity_category is EntityCategory.CONFIG - # Validate state object - switch_1 = hass.states.get("switch.unifi_network_plex") - assert switch_1 is not None - assert switch_1.state == STATE_ON - assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.SWITCH + assert hass.states.get("switch.unifi_network_plex").state == STATE_ON # Update state object data = port_forward_payload[0].copy() @@ -1648,6 +1604,7 @@ async def test_updating_unique_id( @pytest.mark.parametrize("dpi_app_payload", [DPI_APPS]) @pytest.mark.parametrize("dpi_group_payload", [DPI_GROUPS]) @pytest.mark.parametrize("port_forward_payload", [[PORT_FORWARD_PLEX]]) +@pytest.mark.parametrize(("traffic_rule_payload"), [([TRAFFIC_RULE])]) @pytest.mark.parametrize("wlan_payload", [[WLAN]]) @pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -1661,6 +1618,7 @@ async def test_hub_state_change( "switch.plug_outlet_1", "switch.block_media_streaming", "switch.unifi_network_plex", + "switch.unifi_network_test_traffic_rule", "switch.ssid_1", ) for entity_id in entity_ids: From 2f8766a9ece1c425a46789831a0cb0498b16f3e8 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 16 Aug 2024 07:52:18 +1000 Subject: [PATCH 2372/2411] Fix rear trunk logic in Tessie (#124011) Allow open to be anything not zero --- homeassistant/components/tessie/cover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tessie/cover.py b/homeassistant/components/tessie/cover.py index 93ce25993d9..e739f8c074d 100644 --- a/homeassistant/components/tessie/cover.py +++ b/homeassistant/components/tessie/cover.py @@ -168,13 +168,13 @@ class TessieRearTrunkEntity(TessieEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open rear trunk.""" - if self._value == TessieCoverStates.CLOSED: + if self.is_closed: await self.run(open_close_rear_trunk) self.set((self.key, TessieCoverStates.OPEN)) async def async_close_cover(self, **kwargs: Any) -> None: """Close rear trunk.""" - if self._value == TessieCoverStates.OPEN: + if not self.is_closed: await self.run(open_close_rear_trunk) self.set((self.key, TessieCoverStates.CLOSED)) From a944541c584188fc8eb6c19746be250afa226c3e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 16 Aug 2024 04:36:06 +0200 Subject: [PATCH 2373/2411] Bump aiounifi to v80 (#124004) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index aa9b553cb67..6f92dec5361 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==79"], + "requirements": ["aiounifi==80"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 920241247b9..87e1d39dcb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.6.0 # homeassistant.components.unifi -aiounifi==79 +aiounifi==80 # homeassistant.components.vlc_telnet aiovlc==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef99d18d1fe..12227469600 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -368,7 +368,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.6.0 # homeassistant.components.unifi -aiounifi==79 +aiounifi==80 # homeassistant.components.vlc_telnet aiovlc==0.3.2 From 6d9764185ba05d03db3d5a97549fd0bb6848dbd2 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Fri, 16 Aug 2024 09:22:00 +0200 Subject: [PATCH 2374/2411] Bump pypck to 0.7.21 (#124023) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 44a4d683c81..e9717774e17 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/lcn", "iot_class": "local_push", "loggers": ["pypck"], - "requirements": ["pypck==0.7.20"] + "requirements": ["pypck==0.7.21"] } diff --git a/requirements_all.txt b/requirements_all.txt index 87e1d39dcb9..2452ce37fac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2100,7 +2100,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.20 +pypck==0.7.21 # homeassistant.components.pjlink pypjlink2==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12227469600..7302bbe5bab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1681,7 +1681,7 @@ pyoverkiz==1.13.14 pyownet==0.10.0.post1 # homeassistant.components.lcn -pypck==0.7.20 +pypck==0.7.21 # homeassistant.components.pjlink pypjlink2==1.2.1 From f9ade788eb170f7912151b30d73ca5975b261ca1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Aug 2024 10:01:12 +0200 Subject: [PATCH 2375/2411] Do sanity check EntityPlatform.async_register_entity_service schema (#123058) * Do a sanity check of schema passed to EntityPlatform.async_register_entity_service * Only attempt to check schema of Schema * Handle All/Any wrapped in schema * Clarify comment * Apply suggestions from code review Co-authored-by: Robert Resch --------- Co-authored-by: Robert Resch --- homeassistant/helpers/entity_platform.py | 16 ++++++++++++++++ tests/helpers/test_entity_platform.py | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index f3d5f5b076a..ec177fbf316 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -10,6 +10,8 @@ from functools import partial from logging import Logger, getLogger from typing import TYPE_CHECKING, Any, Protocol +import voluptuous as vol + from homeassistant import config_entries from homeassistant.const import ( ATTR_RESTORED, @@ -999,6 +1001,20 @@ class EntityPlatform: if schema is None or isinstance(schema, dict): schema = cv.make_entity_service_schema(schema) + # Do a sanity check to check this is a valid entity service schema, + # the check could be extended to require All/Any to have sub schema(s) + # with all entity service fields + elif ( + # Don't check All/Any + not isinstance(schema, (vol.All, vol.Any)) + # Don't check All/Any wrapped in schema + and not isinstance(schema.schema, (vol.All, vol.Any)) + and any(key not in schema.schema for key in cv.ENTITY_SERVICE_FIELDS) + ): + raise HomeAssistantError( + "The schema does not include all required keys: " + f"{", ".join(str(key) for key in cv.ENTITY_SERVICE_FIELDS)}" + ) service_func: str | HassJob[..., Any] service_func = func if isinstance(func, str) else HassJob(func) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index be8ba998481..50180ecd844 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -8,6 +8,7 @@ from typing import Any from unittest.mock import ANY, AsyncMock, Mock, patch import pytest +import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, PERCENTAGE, EntityCategory from homeassistant.core import ( @@ -1788,6 +1789,28 @@ async def test_register_entity_service_none_schema( assert entity2 in entities +async def test_register_entity_service_non_entity_service_schema( + hass: HomeAssistant, +) -> None: + """Test attempting to register a service with an incomplete schema.""" + entity_platform = MockEntityPlatform( + hass, domain="mock_integration", platform_name="mock_platform", platform=None + ) + + with pytest.raises( + HomeAssistantError, + match=( + "The schema does not include all required keys: entity_id, device_id, area_id, " + "floor_id, label_id" + ), + ): + entity_platform.async_register_entity_service( + "hello", + vol.Schema({"some": str}), + Mock(), + ) + + @pytest.mark.parametrize("update_before_add", [True, False]) async def test_invalid_entity_id( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, update_before_add: bool From 66a873333326ee250084ced1e9400b639c549b8e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 Aug 2024 10:26:12 +0200 Subject: [PATCH 2376/2411] Add missing return type in test __init__ method (part 4) (#123947) --- tests/components/broadlink/__init__.py | 31 +++++++++++++++++--------- tests/components/ffmpeg/test_init.py | 7 +++++- tests/components/recorder/test_init.py | 2 +- tests/components/siren/test_init.py | 2 +- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py index 61ef27815fd..207014d0958 100644 --- a/tests/components/broadlink/__init__.py +++ b/tests/components/broadlink/__init__.py @@ -115,18 +115,27 @@ class BroadlinkDevice: """Representation of a Broadlink device.""" def __init__( - self, name, host, mac, model, manufacturer, type_, devtype, fwversion, timeout - ): + self, + name: str, + host: str, + mac: str, + model: str, + manufacturer: str, + type_: str, + devtype: int, + fwversion: int, + timeout: int, + ) -> None: """Initialize the device.""" - self.name: str = name - self.host: str = host - self.mac: str = mac - self.model: str = model - self.manufacturer: str = manufacturer - self.type: str = type_ - self.devtype: int = devtype - self.timeout: int = timeout - self.fwversion: int = fwversion + self.name = name + self.host = host + self.mac = mac + self.model = model + self.manufacturer = manufacturer + self.type = type_ + self.devtype = devtype + self.timeout = timeout + self.fwversion = fwversion async def setup_entry(self, hass, mock_api=None, mock_entry=None): """Set up the device.""" diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index 353b8fdfcc0..ceefed8d62b 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -54,7 +54,12 @@ def async_restart(hass, entity_id=None): class MockFFmpegDev(ffmpeg.FFmpegBase): """FFmpeg device mock.""" - def __init__(self, hass, initial_state=True, entity_id="test.ffmpeg_device"): + def __init__( + self, + hass: HomeAssistant, + initial_state: bool = True, + entity_id: str = "test.ffmpeg_device", + ) -> None: """Initialize mock.""" super().__init__(None, initial_state) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 3cd4c3ab4b6..c8e58d58105 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -2322,7 +2322,7 @@ async def test_connect_args_priority(hass: HomeAssistant, config_url) -> None: __bases__ = [] _has_events = False - def __init__(*args, **kwargs): ... + def __init__(self, *args: Any, **kwargs: Any) -> None: ... @property def is_async(self): diff --git a/tests/components/siren/test_init.py b/tests/components/siren/test_init.py index 168300d0abe..475b32540b4 100644 --- a/tests/components/siren/test_init.py +++ b/tests/components/siren/test_init.py @@ -27,7 +27,7 @@ class MockSirenEntity(SirenEntity): supported_features=0, available_tones_as_attr=None, available_tones_in_desc=None, - ): + ) -> None: """Initialize mock siren entity.""" self._attr_supported_features = supported_features if available_tones_as_attr is not None: From 4b62dcfd19b992abb39f33eccae04dde8c5484aa Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 16 Aug 2024 11:41:04 +0200 Subject: [PATCH 2377/2411] Improve rate limit handling in Habitica integration (#121763) * Adjustments to requests and update interval due to rate limiting * Use debounced refresh for to-do lists * Use debounced refresh in switch and buttons * Request refresh only if a to-do was changed * Update task order provisionally in the coordinator --- homeassistant/components/habitica/button.py | 2 +- .../components/habitica/coordinator.py | 14 ++++- homeassistant/components/habitica/todo.py | 54 +++++++++++++------ 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/habitica/button.py b/homeassistant/components/habitica/button.py index cdd166a4444..276aa4e7fc0 100644 --- a/homeassistant/components/habitica/button.py +++ b/homeassistant/components/habitica/button.py @@ -113,7 +113,7 @@ class HabiticaButton(HabiticaBase, ButtonEntity): translation_key="service_call_exception", ) from e else: - await self.coordinator.async_refresh() + await self.coordinator.async_request_refresh() @property def available(self) -> bool: diff --git a/homeassistant/components/habitica/coordinator.py b/homeassistant/components/habitica/coordinator.py index 9d0ebe651e3..4e949b703fb 100644 --- a/homeassistant/components/habitica/coordinator.py +++ b/homeassistant/components/habitica/coordinator.py @@ -15,6 +15,7 @@ from habitipy.aio import HabitipyAsync from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -41,7 +42,13 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): hass, _LOGGER, name=DOMAIN, - update_interval=timedelta(seconds=30), + update_interval=timedelta(seconds=60), + request_refresh_debouncer=Debouncer( + hass, + _LOGGER, + cooldown=5, + immediate=False, + ), ) self.api = habitipy @@ -51,6 +58,9 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): tasks_response = await self.api.tasks.user.get() tasks_response.extend(await self.api.tasks.user.get(type="completedTodos")) except ClientResponseError as error: + if error.status == HTTPStatus.TOO_MANY_REQUESTS: + _LOGGER.debug("Currently rate limited, skipping update") + return self.data raise UpdateFailed(f"Error communicating with API: {error}") from error return HabiticaData(user=user_response, tasks=tasks_response) @@ -73,4 +83,4 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): translation_key="service_call_exception", ) from e else: - await self.async_refresh() + await self.async_request_refresh() diff --git a/homeassistant/components/habitica/todo.py b/homeassistant/components/habitica/todo.py index 451db1e6806..ae739d47262 100644 --- a/homeassistant/components/habitica/todo.py +++ b/homeassistant/components/habitica/todo.py @@ -93,7 +93,7 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity): translation_key=f"delete_{self.entity_description.key}_failed", ) from e - await self.coordinator.async_refresh() + await self.coordinator.async_request_refresh() async def async_move_todo_item( self, uid: str, previous_uid: str | None = None @@ -121,9 +121,22 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity): translation_key=f"move_{self.entity_description.key}_item_failed", translation_placeholders={"pos": str(pos)}, ) from e + else: + # move tasks in the coordinator until we have fresh data + tasks = self.coordinator.data.tasks + new_pos = ( + tasks.index(next(task for task in tasks if task["id"] == previous_uid)) + + 1 + if previous_uid + else 0 + ) + old_pos = tasks.index(next(task for task in tasks if task["id"] == uid)) + tasks.insert(new_pos, tasks.pop(old_pos)) + await self.coordinator.async_request_refresh() async def async_update_todo_item(self, item: TodoItem) -> None: """Update a Habitica todo.""" + refresh_required = False current_item = next( (task for task in (self.todo_items or []) if task.uid == item.uid), None, @@ -132,7 +145,6 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity): if TYPE_CHECKING: assert item.uid assert current_item - assert item.due if ( self.entity_description.key is HabiticaTodoList.TODOS @@ -142,18 +154,24 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity): else: date = None - try: - await self.coordinator.api.tasks[item.uid].put( - text=item.summary, - notes=item.description or "", - date=date, - ) - except ClientResponseError as e: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key=f"update_{self.entity_description.key}_item_failed", - translation_placeholders={"name": item.summary or ""}, - ) from e + if ( + item.summary != current_item.summary + or item.description != current_item.description + or item.due != current_item.due + ): + try: + await self.coordinator.api.tasks[item.uid].put( + text=item.summary, + notes=item.description or "", + date=date, + ) + refresh_required = True + except ClientResponseError as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key=f"update_{self.entity_description.key}_item_failed", + translation_placeholders={"name": item.summary or ""}, + ) from e try: # Score up or down if item status changed @@ -164,6 +182,7 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity): score_result = ( await self.coordinator.api.tasks[item.uid].score["up"].post() ) + refresh_required = True elif ( current_item.status is TodoItemStatus.COMPLETED and item.status == TodoItemStatus.NEEDS_ACTION @@ -171,6 +190,7 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity): score_result = ( await self.coordinator.api.tasks[item.uid].score["down"].post() ) + refresh_required = True else: score_result = None @@ -189,8 +209,8 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity): persistent_notification.async_create( self.hass, message=msg, title="Habitica" ) - - await self.coordinator.async_refresh() + if refresh_required: + await self.coordinator.async_request_refresh() class HabiticaTodosListEntity(BaseHabiticaListEntity): @@ -254,7 +274,7 @@ class HabiticaTodosListEntity(BaseHabiticaListEntity): translation_placeholders={"name": item.summary or ""}, ) from e - await self.coordinator.async_refresh() + await self.coordinator.async_request_refresh() class HabiticaDailiesListEntity(BaseHabiticaListEntity): From 1f214bec9376c836c89763c89214c54c43e477d5 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Fri, 16 Aug 2024 05:49:00 -0400 Subject: [PATCH 2378/2411] Add Sonos tests for media_player shuffle and repeat (#122816) * initial commit * initial commit * update comments --- tests/components/sonos/test_media_player.py | 146 +++++++++++++++++++- 1 file changed, 145 insertions(+), 1 deletion(-) diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index c765ed82ac6..599a04b806a 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -12,6 +12,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, MediaPlayerEnqueue, + RepeatMode, ) from homeassistant.components.sonos.const import ( DOMAIN as SONOS_DOMAIN, @@ -25,6 +26,8 @@ from homeassistant.components.sonos.media_player import ( VOLUME_INCREMENT, ) from homeassistant.const import ( + SERVICE_REPEAT_SET, + SERVICE_SHUFFLE_SET, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, @@ -39,7 +42,7 @@ from homeassistant.helpers.device_registry import ( ) from homeassistant.setup import async_setup_component -from .conftest import MockMusicServiceItem, MockSoCo, SoCoMockFactory +from .conftest import MockMusicServiceItem, MockSoCo, SoCoMockFactory, SonosMockEvent async def test_device_registry( @@ -683,6 +686,147 @@ async def test_select_source_error( assert "Could not find a Sonos favorite" in str(sve.value) +async def test_shuffle_set( + hass: HomeAssistant, + soco: MockSoCo, + async_autosetup_sonos, +) -> None: + """Test the set shuffle method.""" + assert soco.play_mode == "NORMAL" + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SHUFFLE_SET, + { + "entity_id": "media_player.zone_a", + "shuffle": True, + }, + blocking=True, + ) + assert soco.play_mode == "SHUFFLE_NOREPEAT" + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SHUFFLE_SET, + { + "entity_id": "media_player.zone_a", + "shuffle": False, + }, + blocking=True, + ) + assert soco.play_mode == "NORMAL" + + +async def test_shuffle_get( + hass: HomeAssistant, + soco: MockSoCo, + async_autosetup_sonos, + no_media_event: SonosMockEvent, +) -> None: + """Test the get shuffle attribute by simulating a Sonos Event.""" + subscription = soco.avTransport.subscribe.return_value + sub_callback = subscription.callback + + state = hass.states.get("media_player.zone_a") + assert state.attributes["shuffle"] is False + + no_media_event.variables["current_play_mode"] = "SHUFFLE_NOREPEAT" + sub_callback(no_media_event) + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get("media_player.zone_a") + assert state.attributes["shuffle"] is True + + # The integration keeps a copy of the last event to check for + # changes, so we create a new event. + no_media_event = SonosMockEvent( + soco, soco.avTransport, no_media_event.variables.copy() + ) + no_media_event.variables["current_play_mode"] = "NORMAL" + sub_callback(no_media_event) + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get("media_player.zone_a") + assert state.attributes["shuffle"] is False + + +async def test_repeat_set( + hass: HomeAssistant, + soco: MockSoCo, + async_autosetup_sonos, +) -> None: + """Test the set repeat method.""" + assert soco.play_mode == "NORMAL" + await hass.services.async_call( + MP_DOMAIN, + SERVICE_REPEAT_SET, + { + "entity_id": "media_player.zone_a", + "repeat": RepeatMode.ALL, + }, + blocking=True, + ) + assert soco.play_mode == "REPEAT_ALL" + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_REPEAT_SET, + { + "entity_id": "media_player.zone_a", + "repeat": RepeatMode.ONE, + }, + blocking=True, + ) + assert soco.play_mode == "REPEAT_ONE" + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_REPEAT_SET, + { + "entity_id": "media_player.zone_a", + "repeat": RepeatMode.OFF, + }, + blocking=True, + ) + assert soco.play_mode == "NORMAL" + + +async def test_repeat_get( + hass: HomeAssistant, + soco: MockSoCo, + async_autosetup_sonos, + no_media_event: SonosMockEvent, +) -> None: + """Test the get repeat attribute by simulating a Sonos Event.""" + subscription = soco.avTransport.subscribe.return_value + sub_callback = subscription.callback + + state = hass.states.get("media_player.zone_a") + assert state.attributes["repeat"] == RepeatMode.OFF + + no_media_event.variables["current_play_mode"] = "REPEAT_ALL" + sub_callback(no_media_event) + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get("media_player.zone_a") + assert state.attributes["repeat"] == RepeatMode.ALL + + no_media_event = SonosMockEvent( + soco, soco.avTransport, no_media_event.variables.copy() + ) + no_media_event.variables["current_play_mode"] = "REPEAT_ONE" + sub_callback(no_media_event) + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get("media_player.zone_a") + assert state.attributes["repeat"] == RepeatMode.ONE + + no_media_event = SonosMockEvent( + soco, soco.avTransport, no_media_event.variables.copy() + ) + no_media_event.variables["current_play_mode"] = "NORMAL" + sub_callback(no_media_event) + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get("media_player.zone_a") + assert state.attributes["repeat"] == RepeatMode.OFF + + async def test_play_media_favorite_item_id( hass: HomeAssistant, soco_factory: SoCoMockFactory, From 723ea6173e7bff7ab9b277a31a5ff4d5dad620e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Aug 2024 05:04:57 -0500 Subject: [PATCH 2379/2411] Add Python-2.0.1 license to list of approved licenses (#124020) https://spdx.org/licenses/Python-2.0.1.html --- script/licenses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/script/licenses.py b/script/licenses.py index 1e01fb6111b..9d7da912398 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -116,6 +116,7 @@ OSI_APPROVED_LICENSES = { "Unlicense", "Apache-2", "GPLv2", + "Python-2.0.1", } EXCEPTIONS = { From 461ef335536ab3bf2f075e2832721a9a467d8bed Mon Sep 17 00:00:00 2001 From: WebSpider Date: Fri, 16 Aug 2024 12:27:20 +0200 Subject: [PATCH 2380/2411] Bump meteoalert to 0.3.1 (#123848) Bump meteoalertapi to 0.3.1 --- 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 9a41e8a3062..4de91f6a431 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/meteoalarm", "iot_class": "cloud_polling", "loggers": ["meteoalertapi"], - "requirements": ["meteoalertapi==0.3.0"] + "requirements": ["meteoalertapi==0.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2452ce37fac..f968b8addf5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1321,7 +1321,7 @@ melnor-bluetooth==0.0.25 messagebird==1.2.0 # homeassistant.components.meteoalarm -meteoalertapi==0.3.0 +meteoalertapi==0.3.1 # homeassistant.components.meteo_france meteofrance-api==1.3.0 From 0093276e93c25c58830098f4668563c553d80d5a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 16 Aug 2024 12:46:51 +0200 Subject: [PATCH 2381/2411] Reolink add 100% coverage of binary_sensor platfrom (#123862) * Implement 100% coverage of binary_sensor * fix styling * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker * use get().state instead of is_state * Remove unneeded "is True" * Remove unneeded "is True" * reset the mock and use assert_not_called * use freezer * fix styling * fix styling --------- Co-authored-by: Joost Lekkerkerker --- tests/components/reolink/conftest.py | 4 - .../components/reolink/test_binary_sensor.py | 52 ++++++++++++ tests/components/reolink/test_config_flow.py | 9 +- tests/components/reolink/test_host.py | 85 +++++++++++++++++++ tests/components/reolink/test_init.py | 23 +++-- tests/components/reolink/test_media_source.py | 6 +- tests/components/reolink/test_select.py | 31 ++++--- 7 files changed, 170 insertions(+), 40 deletions(-) create mode 100644 tests/components/reolink/test_binary_sensor.py create mode 100644 tests/components/reolink/test_host.py diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 981dcc30e60..ddea36cb292 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -53,10 +53,6 @@ def mock_setup_entry() -> Generator[AsyncMock]: def reolink_connect_class() -> Generator[MagicMock]: """Mock reolink connection and return both the host_mock and host_mock_class.""" with ( - patch( - "homeassistant.components.reolink.host.webhook.async_register", - return_value=True, - ), patch( "homeassistant.components.reolink.host.Host", autospec=True ) as host_mock_class, diff --git a/tests/components/reolink/test_binary_sensor.py b/tests/components/reolink/test_binary_sensor.py new file mode 100644 index 00000000000..e02742afe1d --- /dev/null +++ b/tests/components/reolink/test_binary_sensor.py @@ -0,0 +1,52 @@ +"""Test the Reolink binary sensor platform.""" + +from unittest.mock import MagicMock, patch + +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.components.reolink import DEVICE_UPDATE_INTERVAL, const +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_OFF, STATE_ON, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import TEST_NVR_NAME, TEST_UID + +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.typing import ClientSessionGenerator + + +async def test_motion_sensor( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test binary sensor entity with motion sensor.""" + reolink_connect.model = "Reolink Duo PoE" + reolink_connect.motion_detected.return_value = True + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BINARY_SENSOR]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + entity_id = f"{Platform.BINARY_SENSOR}.{TEST_NVR_NAME}_motion_lens_0" + assert hass.states.get(entity_id).state == STATE_ON + + reolink_connect.motion_detected.return_value = False + freezer.tick(DEVICE_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_OFF + + # test webhook callback + reolink_connect.motion_detected.return_value = True + reolink_connect.ONVIF_event_callback.return_value = [0] + webhook_id = f"{const.DOMAIN}_{TEST_UID.replace(':', '')}_ONVIF" + client = await hass_client_no_auth() + await client.post(f"/api/webhook/{webhook_id}", data="test_data") + + assert hass.states.get(entity_id).state == STATE_ON diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 6e57a7924e7..55dd0d4fea9 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Reolink config flow.""" -from datetime import timedelta import json from typing import Any from unittest.mock import AsyncMock, MagicMock, call +from freezegun.api import FrozenDateTimeFactory import pytest from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError @@ -25,7 +25,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.device_registry import format_mac -from homeassistant.util.dt import utcnow from .conftest import ( DHCP_FORMATTED_MAC, @@ -439,6 +438,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No ) async def test_dhcp_ip_update( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, reolink_connect_class: MagicMock, reolink_connect: MagicMock, last_update_success: bool, @@ -472,9 +472,8 @@ async def test_dhcp_ip_update( if not last_update_success: # ensure the last_update_succes is False for the device_coordinator. reolink_connect.get_states = AsyncMock(side_effect=ReolinkError("Test error")) - async_fire_time_changed( - hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(minutes=1) - ) + freezer.tick(DEVICE_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() dhcp_data = dhcp.DhcpServiceInfo( diff --git a/tests/components/reolink/test_host.py b/tests/components/reolink/test_host.py new file mode 100644 index 00000000000..690bfd035f8 --- /dev/null +++ b/tests/components/reolink/test_host.py @@ -0,0 +1,85 @@ +"""Test the Reolink host.""" + +from asyncio import CancelledError +from unittest.mock import AsyncMock, MagicMock + +from aiohttp import ClientResponseError +import pytest + +from homeassistant.components.reolink import const +from homeassistant.components.webhook import async_handle_webhook +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.aiohttp import MockRequest + +from .conftest import TEST_UID + +from tests.common import MockConfigEntry +from tests.typing import ClientSessionGenerator + + +async def test_webhook_callback( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test webhook callback with motion sensor.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + webhook_id = f"{const.DOMAIN}_{TEST_UID.replace(':', '')}_ONVIF" + + signal_all = MagicMock() + signal_ch = MagicMock() + async_dispatcher_connect(hass, f"{webhook_id}_all", signal_all) + async_dispatcher_connect(hass, f"{webhook_id}_0", signal_ch) + + client = await hass_client_no_auth() + + # test webhook callback success all channels + reolink_connect.ONVIF_event_callback.return_value = None + await client.post(f"/api/webhook/{webhook_id}") + signal_all.assert_called_once() + + # test webhook callback all channels with failure to read motion_state + signal_all.reset_mock() + reolink_connect.get_motion_state_all_ch.return_value = False + await client.post(f"/api/webhook/{webhook_id}") + signal_all.assert_not_called() + + # test webhook callback success single channel + reolink_connect.ONVIF_event_callback.return_value = [0] + await client.post(f"/api/webhook/{webhook_id}", data="test_data") + signal_ch.assert_called_once() + + # test webhook callback single channel with error in event callback + signal_ch.reset_mock() + reolink_connect.ONVIF_event_callback = AsyncMock( + side_effect=Exception("Test error") + ) + await client.post(f"/api/webhook/{webhook_id}", data="test_data") + signal_ch.assert_not_called() + + # test failure to read date from webhook post + request = MockRequest( + method="POST", + content=bytes("test", "utf-8"), + mock_source="test", + ) + request.read = AsyncMock(side_effect=ConnectionResetError("Test error")) + await async_handle_webhook(hass, webhook_id, request) + signal_all.assert_not_called() + + request.read = AsyncMock(side_effect=ClientResponseError("Test error", "Test")) + await async_handle_webhook(hass, webhook_id, request) + signal_all.assert_not_called() + + request.read = AsyncMock(side_effect=CancelledError("Test error")) + with pytest.raises(CancelledError): + await async_handle_webhook(hass, webhook_id, request) + signal_all.assert_not_called() diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 1c93114217c..f5cd56a05d2 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -1,10 +1,10 @@ """Test the Reolink init.""" import asyncio -from datetime import timedelta from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch +from freezegun.api import FrozenDateTimeFactory import pytest from reolink_aio.api import Chime from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError @@ -25,7 +25,6 @@ from homeassistant.helpers import ( issue_registry as ir, ) from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow from .conftest import ( TEST_CAM_MODEL, @@ -104,6 +103,7 @@ async def test_failures_parametrized( async def test_firmware_error_twice( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, reolink_connect: MagicMock, config_entry: MockConfigEntry, ) -> None: @@ -112,31 +112,31 @@ async def test_firmware_error_twice( side_effect=ReolinkError("Test error") ) with patch("homeassistant.components.reolink.PLATFORMS", [Platform.UPDATE]): - assert await hass.config_entries.async_setup(config_entry.entry_id) is True + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED entity_id = f"{Platform.UPDATE}.{TEST_NVR_NAME}_firmware" - assert hass.states.is_state(entity_id, STATE_OFF) + assert hass.states.get(entity_id).state == STATE_OFF - async_fire_time_changed( - hass, utcnow() + FIRMWARE_UPDATE_INTERVAL + timedelta(minutes=1) - ) + freezer.tick(FIRMWARE_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.is_state(entity_id, STATE_UNAVAILABLE) + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE async def test_credential_error_three( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, reolink_connect: MagicMock, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry, ) -> None: """Test when the update gives credential error 3 times.""" with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): - assert await hass.config_entries.async_setup(config_entry.entry_id) is True + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED @@ -147,9 +147,8 @@ async def test_credential_error_three( issue_id = f"config_entry_reauth_{const.DOMAIN}_{config_entry.entry_id}" for _ in range(NUM_CRED_ERRORS): assert (HOMEASSISTANT_DOMAIN, issue_id) not in issue_registry.issues - async_fire_time_changed( - hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30) - ) + freezer.tick(DEVICE_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert (HOMEASSISTANT_DOMAIN, issue_id) in issue_registry.issues diff --git a/tests/components/reolink/test_media_source.py b/tests/components/reolink/test_media_source.py index 66ed32ca823..b09c267fcfd 100644 --- a/tests/components/reolink/test_media_source.py +++ b/tests/components/reolink/test_media_source.py @@ -275,7 +275,7 @@ async def test_browsing_rec_playback_unsupported( reolink_connect.api_version.return_value = 0 with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]): - assert await hass.config_entries.async_setup(config_entry.entry_id) is True + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # browse root @@ -296,7 +296,7 @@ async def test_browsing_errors( reolink_connect.api_version.return_value = 1 with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]): - assert await hass.config_entries.async_setup(config_entry.entry_id) is True + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # browse root @@ -315,7 +315,7 @@ async def test_browsing_not_loaded( reolink_connect.api_version.return_value = 1 with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]): - assert await hass.config_entries.async_setup(config_entry.entry_id) is True + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() reolink_connect.get_host_data = AsyncMock(side_effect=ReolinkError("Test error")) diff --git a/tests/components/reolink/test_select.py b/tests/components/reolink/test_select.py index 5536797d7d3..5536e85afb9 100644 --- a/tests/components/reolink/test_select.py +++ b/tests/components/reolink/test_select.py @@ -1,8 +1,8 @@ """Test the Reolink select platform.""" -from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, patch +from freezegun.api import FrozenDateTimeFactory import pytest from reolink_aio.api import Chime from reolink_aio.exceptions import InvalidParameterError, ReolinkError @@ -19,7 +19,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er -from homeassistant.util.dt import utcnow from .conftest import TEST_NVR_NAME @@ -28,18 +27,19 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_floodlight_mode_select( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, config_entry: MockConfigEntry, reolink_connect: MagicMock, entity_registry: er.EntityRegistry, ) -> None: """Test select entity with floodlight_mode.""" with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]): - assert await hass.config_entries.async_setup(config_entry.entry_id) is True + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED entity_id = f"{Platform.SELECT}.{TEST_NVR_NAME}_floodlight_mode" - assert hass.states.is_state(entity_id, "auto") + assert hass.states.get(entity_id).state == "auto" reolink_connect.set_whiteled = AsyncMock() await hass.services.async_call( @@ -71,12 +71,11 @@ async def test_floodlight_mode_select( ) reolink_connect.whiteled_mode.return_value = -99 # invalid value - async_fire_time_changed( - hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30) - ) + freezer.tick(DEVICE_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.is_state(entity_id, STATE_UNKNOWN) + assert hass.states.get(entity_id).state == STATE_UNKNOWN async def test_play_quick_reply_message( @@ -88,12 +87,12 @@ async def test_play_quick_reply_message( """Test select play_quick_reply_message entity.""" reolink_connect.quick_reply_dict.return_value = {0: "off", 1: "test message"} with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]): - assert await hass.config_entries.async_setup(config_entry.entry_id) is True + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED entity_id = f"{Platform.SELECT}.{TEST_NVR_NAME}_play_quick_reply_message" - assert hass.states.is_state(entity_id, STATE_UNKNOWN) + assert hass.states.get(entity_id).state == STATE_UNKNOWN reolink_connect.play_quick_reply = AsyncMock() await hass.services.async_call( @@ -107,6 +106,7 @@ async def test_play_quick_reply_message( async def test_chime_select( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, config_entry: MockConfigEntry, reolink_connect: MagicMock, test_chime: Chime, @@ -114,13 +114,13 @@ async def test_chime_select( ) -> None: """Test chime select entity.""" with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]): - assert await hass.config_entries.async_setup(config_entry.entry_id) is True + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED entity_id = f"{Platform.SELECT}.test_chime_visitor_ringtone" - assert hass.states.is_state(entity_id, "pianokey") + assert hass.states.get(entity_id).state == "pianokey" test_chime.set_tone = AsyncMock() await hass.services.async_call( @@ -150,9 +150,8 @@ async def test_chime_select( ) test_chime.event_info = {} - async_fire_time_changed( - hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30) - ) + freezer.tick(DEVICE_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.is_state(entity_id, STATE_UNKNOWN) + assert hass.states.get(entity_id).state == STATE_UNKNOWN From 99ab2566c25a3ba40b205302065740907675bd84 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Aug 2024 12:58:05 +0200 Subject: [PATCH 2382/2411] Correct water heater service schemas (#124038) * Correct water heater service schemas * Update tests --- .../components/water_heater/__init__.py | 42 ++++++------------- tests/components/water_heater/test_init.py | 5 ++- 2 files changed, 15 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 0b54a4c1aa4..2e749735b0c 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -35,7 +35,7 @@ from homeassistant.helpers.deprecation import ( from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.temperature import display_temp as show_temp -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.util.unit_conversion import TemperatureConverter from .const import DOMAIN @@ -94,29 +94,17 @@ CONVERTIBLE_ATTRIBUTE = [ATTR_TEMPERATURE] _LOGGER = logging.getLogger(__name__) -ON_OFF_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids}) - -SET_AWAY_MODE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_AWAY_MODE): cv.boolean, - } -) -SET_TEMPERATURE_SCHEMA = vol.Schema( - vol.All( - { - vol.Required(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float), - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Optional(ATTR_OPERATION_MODE): cv.string, - } - ) -) -SET_OPERATION_MODE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_OPERATION_MODE): cv.string, - } -) +SET_AWAY_MODE_SCHEMA: VolDictType = { + vol.Required(ATTR_AWAY_MODE): cv.boolean, +} +SET_TEMPERATURE_SCHEMA: VolDictType = { + vol.Required(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float), + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, + vol.Optional(ATTR_OPERATION_MODE): cv.string, +} +SET_OPERATION_MODE_SCHEMA: VolDictType = { + vol.Required(ATTR_OPERATION_MODE): cv.string, +} # mypy: disallow-any-generics @@ -145,12 +133,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: SET_OPERATION_MODE_SCHEMA, "async_handle_set_operation_mode", ) - component.async_register_entity_service( - SERVICE_TURN_OFF, ON_OFF_SERVICE_SCHEMA, "async_turn_off" - ) - component.async_register_entity_service( - SERVICE_TURN_ON, ON_OFF_SERVICE_SCHEMA, "async_turn_on" - ) return True diff --git a/tests/components/water_heater/test_init.py b/tests/components/water_heater/test_init.py index f883cf47b19..4e0f860366c 100644 --- a/tests/components/water_heater/test_init.py +++ b/tests/components/water_heater/test_init.py @@ -22,6 +22,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from tests.common import ( @@ -42,7 +43,7 @@ async def test_set_temp_schema_no_req( """Test the set temperature schema with missing required data.""" domain = "climate" service = "test_set_temperature" - schema = SET_TEMPERATURE_SCHEMA + schema = cv.make_entity_service_schema(SET_TEMPERATURE_SCHEMA) calls = async_mock_service(hass, domain, service, schema) data = {"hvac_mode": "off", "entity_id": ["climate.test_id"]} @@ -59,7 +60,7 @@ async def test_set_temp_schema( """Test the set temperature schema with ok required data.""" domain = "water_heater" service = "test_set_temperature" - schema = SET_TEMPERATURE_SCHEMA + schema = cv.make_entity_service_schema(SET_TEMPERATURE_SCHEMA) calls = async_mock_service(hass, domain, service, schema) data = { From f3e2d0692214f91b0cdc19b5c32e284b993c77e0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:21:39 +0200 Subject: [PATCH 2383/2411] Add missing hass type in tests/scripts (#124042) --- tests/scripts/test_auth.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index 002807f08a5..e52a2cc6567 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -1,5 +1,6 @@ """Test the auth script to manage local users.""" +import argparse from asyncio import AbstractEventLoop from collections.abc import Generator import logging @@ -148,7 +149,9 @@ def test_parsing_args(event_loop: AbstractEventLoop) -> None: """Test we parse args correctly.""" called = False - async def mock_func(hass, provider, args2): + async def mock_func( + hass: HomeAssistant, provider: hass_auth.AuthProvider, args2: argparse.Namespace + ) -> None: """Mock function to be called.""" nonlocal called called = True From 183c191d63d945ddf7b7d0a442696bf81081b413 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 16 Aug 2024 13:34:14 +0200 Subject: [PATCH 2384/2411] Allow raw mqtt payload to be in mqtt publish action (#123900) * Publish raw rendered mqtt payload as raw for mqtt publish action * Move check out of try block * Only try to eval `bytes` is payload starts with supported string Co-authored-by: Erik Montnemery * Improve docst * Add `evaluate_bytes` option to publish action * Rename to `evaluate_payload` * Update homeassistant/components/mqtt/strings.json Co-authored-by: Erik Montnemery * Extend test to assert literal_eval is called or not --------- Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 7 +++ homeassistant/components/mqtt/models.py | 34 +++++------ homeassistant/components/mqtt/services.yaml | 5 ++ homeassistant/components/mqtt/strings.json | 4 ++ tests/components/mqtt/test_init.py | 68 +++++++++++++++++++++ 5 files changed, 101 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 013bd26e49c..b2adb7665fc 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -90,6 +90,7 @@ from .models import ( # noqa: F401 PublishPayloadType, ReceiveMessage, ReceivePayloadType, + convert_outgoing_mqtt_payload, ) from .subscription import ( # noqa: F401 EntitySubscription, @@ -115,6 +116,7 @@ SERVICE_DUMP = "dump" ATTR_TOPIC_TEMPLATE = "topic_template" ATTR_PAYLOAD_TEMPLATE = "payload_template" +ATTR_EVALUATE_PAYLOAD = "evaluate_payload" MAX_RECONNECT_WAIT = 300 # seconds @@ -166,6 +168,7 @@ MQTT_PUBLISH_SCHEMA = vol.All( vol.Exclusive(ATTR_TOPIC_TEMPLATE, CONF_TOPIC): cv.string, vol.Exclusive(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string, vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.string, + vol.Optional(ATTR_EVALUATE_PAYLOAD): cv.boolean, vol.Optional(ATTR_QOS, default=DEFAULT_QOS): valid_qos_schema, vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, }, @@ -295,6 +298,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: msg_topic: str | None = call.data.get(ATTR_TOPIC) msg_topic_template: str | None = call.data.get(ATTR_TOPIC_TEMPLATE) payload: PublishPayloadType = call.data.get(ATTR_PAYLOAD) + evaluate_payload: bool = call.data.get(ATTR_EVALUATE_PAYLOAD, False) payload_template: str | None = call.data.get(ATTR_PAYLOAD_TEMPLATE) qos: int = call.data[ATTR_QOS] retain: bool = call.data[ATTR_RETAIN] @@ -354,6 +358,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: payload = MqttCommandTemplate( template.Template(payload_template, hass) ).async_render() + elif evaluate_payload: + # Convert quoted binary literal to raw data + payload = convert_outgoing_mqtt_payload(payload) if TYPE_CHECKING: assert msg_topic is not None diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index f2b3165f66c..f7abbc29464 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -51,6 +51,22 @@ ATTR_THIS = "this" type PublishPayloadType = str | bytes | int | float | None +def convert_outgoing_mqtt_payload( + payload: PublishPayloadType, +) -> PublishPayloadType: + """Ensure correct raw MQTT payload is passed as bytes for publishing.""" + if isinstance(payload, str) and payload.startswith(("b'", 'b"')): + try: + native_object = literal_eval(payload) + except (ValueError, TypeError, SyntaxError, MemoryError): + pass + else: + if isinstance(native_object, bytes): + return native_object + + return payload + + @dataclass class PublishMessage: """MQTT Message for publishing.""" @@ -173,22 +189,6 @@ class MqttCommandTemplate: variables: TemplateVarsType = None, ) -> PublishPayloadType: """Render or convert the command template with given value or variables.""" - - def _convert_outgoing_payload( - payload: PublishPayloadType, - ) -> PublishPayloadType: - """Ensure correct raw MQTT payload is passed as bytes for publishing.""" - if isinstance(payload, str): - try: - native_object = literal_eval(payload) - if isinstance(native_object, bytes): - return native_object - - except (ValueError, TypeError, SyntaxError, MemoryError): - pass - - return payload - if self._command_template is None: return value @@ -210,7 +210,7 @@ class MqttCommandTemplate: self._command_template, ) try: - return _convert_outgoing_payload( + return convert_outgoing_mqtt_payload( self._command_template.async_render(values, parse_result=False) ) except TemplateError as exc: diff --git a/homeassistant/components/mqtt/services.yaml b/homeassistant/components/mqtt/services.yaml index ee5e4ff56e8..c5e4f372bd6 100644 --- a/homeassistant/components/mqtt/services.yaml +++ b/homeassistant/components/mqtt/services.yaml @@ -12,6 +12,11 @@ publish: example: "The temperature is {{ states('sensor.temperature') }}" selector: template: + evaluate_payload: + advanced: true + default: false + selector: + boolean: qos: advanced: true default: 0 diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 93131376154..c786d7e08a1 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -230,6 +230,10 @@ "name": "Publish", "description": "Publishes a message to an MQTT topic.", "fields": { + "evaluate_payload": { + "name": "Evaluate payload", + "description": "When `payload` is a Python bytes literal, evaluate the bytes literal and publish the raw data." + }, "topic": { "name": "Topic", "description": "Topic to publish to." diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index f495c5ca585..333960d8ad4 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -420,6 +420,74 @@ async def test_mqtt_publish_action_call_with_template_payload_renders_template( mqtt_mock.reset_mock() +@pytest.mark.parametrize( + ("attr_payload", "payload", "evaluate_payload", "literal_eval_calls"), + [ + ("b'\\xde\\xad\\xbe\\xef'", b"\xde\xad\xbe\xef", True, 1), + ("b'\\xde\\xad\\xbe\\xef'", "b'\\xde\\xad\\xbe\\xef'", False, 0), + ("DEADBEEF", "DEADBEEF", False, 0), + ( + "b'\\xde", + "b'\\xde", + True, + 1, + ), # Bytes literal is invalid, fall back to string + ], +) +async def test_mqtt_publish_action_call_with_raw_data( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + attr_payload: str, + payload: str | bytes, + evaluate_payload: bool, + literal_eval_calls: int, +) -> None: + """Test the mqtt publish action call raw data. + + When `payload` represents a `bytes` object, it should be published + as raw data if `evaluate_payload` is set. + """ + mqtt_mock = await mqtt_mock_entry() + await hass.services.async_call( + mqtt.DOMAIN, + mqtt.SERVICE_PUBLISH, + { + mqtt.ATTR_TOPIC: "test/topic", + mqtt.ATTR_PAYLOAD: attr_payload, + mqtt.ATTR_EVALUATE_PAYLOAD: evaluate_payload, + }, + blocking=True, + ) + assert mqtt_mock.async_publish.called + assert mqtt_mock.async_publish.call_args[0][1] == payload + + with patch( + "homeassistant.components.mqtt.models.literal_eval" + ) as literal_eval_mock: + await hass.services.async_call( + mqtt.DOMAIN, + mqtt.SERVICE_PUBLISH, + { + mqtt.ATTR_TOPIC: "test/topic", + mqtt.ATTR_PAYLOAD: attr_payload, + }, + blocking=True, + ) + literal_eval_mock.assert_not_called() + + await hass.services.async_call( + mqtt.DOMAIN, + mqtt.SERVICE_PUBLISH, + { + mqtt.ATTR_TOPIC: "test/topic", + mqtt.ATTR_PAYLOAD: attr_payload, + mqtt.ATTR_EVALUATE_PAYLOAD: evaluate_payload, + }, + blocking=True, + ) + assert len(literal_eval_mock.mock_calls) == literal_eval_calls + + # The use of a payload_template in an mqtt publish action call # has been deprecated with HA Core 2024.8.0 and will be removed with HA Core 2025.2.0 async def test_publish_action_call_with_bad_payload_template( From 799e95c1bdfa77ff80b23bfc6e3cdd39d14a7ec9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Aug 2024 13:39:25 +0200 Subject: [PATCH 2385/2411] Do sanity check in EntityComponent.async_register_entity_service schema (#124029) * Do sanity check in EntityComponent.async_register_entity_service schema * Improve test --- homeassistant/helpers/entity_component.py | 15 +++++++++++++ tests/helpers/test_entity_component.py | 27 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index c8bcda0eef2..826693de133 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -11,6 +11,7 @@ from types import ModuleType from typing import Any, Generic from typing_extensions import TypeVar +import voluptuous as vol from homeassistant import config as conf_util from homeassistant.config_entries import ConfigEntry @@ -266,6 +267,20 @@ class EntityComponent(Generic[_EntityT]): """Register an entity service.""" if schema is None or isinstance(schema, dict): schema = cv.make_entity_service_schema(schema) + # Do a sanity check to check this is a valid entity service schema, + # the check could be extended to require All/Any to have sub schema(s) + # with all entity service fields + elif ( + # Don't check All/Any + not isinstance(schema, (vol.All, vol.Any)) + # Don't check All/Any wrapped in schema + and not isinstance(schema.schema, (vol.All, vol.Any)) + and any(key not in schema.schema for key in cv.ENTITY_SERVICE_FIELDS) + ): + raise HomeAssistantError( + "The schema does not include all required keys: " + f"{", ".join(str(key) for key in cv.ENTITY_SERVICE_FIELDS)}" + ) service_func: str | HassJob[..., Any] service_func = func if isinstance(func, str) else HassJob(func) diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 0c09c9d75f7..230944e6a96 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -556,6 +556,33 @@ async def test_register_entity_service( assert len(calls) == 2 +async def test_register_entity_service_non_entity_service_schema( + hass: HomeAssistant, +) -> None: + """Test attempting to register a service with an incomplete schema.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + with pytest.raises( + HomeAssistantError, + match=( + "The schema does not include all required keys: entity_id, device_id, area_id, " + "floor_id, label_id" + ), + ): + component.async_register_entity_service( + "hello", vol.Schema({"some": str}), Mock() + ) + + # The check currently does not recurse into vol.All or vol.Any allowing these + # non-compliatn schemas to pass + component.async_register_entity_service( + "hello", vol.All(vol.Schema({"some": str})), Mock() + ) + component.async_register_entity_service( + "hello", vol.Any(vol.Schema({"some": str})), Mock() + ) + + async def test_register_entity_service_response_data(hass: HomeAssistant) -> None: """Test an entity service that does support response data.""" entity = MockEntity(entity_id=f"{DOMAIN}.entity") From ea52acd7bd3611e35a2dbe7c0d49fc1f6b4dc51e Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 16 Aug 2024 13:43:02 +0200 Subject: [PATCH 2386/2411] Fix loading KNX integration actions when not using YAML (#124027) * Fix loading KNX integration services when not using YAML * remove unnecessary comment * Remove unreachable test --- homeassistant/components/knx/__init__.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index fd46cad8489..a401ee2ccac 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -147,18 +147,10 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start the KNX integration.""" hass.data[DATA_HASS_CONFIG] = config - conf: ConfigType | None = config.get(DOMAIN) - - if conf is None: - # If we have a config entry, setup is done by that config entry. - # If there is no config entry, this should fail. - return bool(hass.config_entries.async_entries(DOMAIN)) - - conf = dict(conf) - hass.data[DATA_KNX_CONFIG] = conf + if (conf := config.get(DOMAIN)) is not None: + hass.data[DATA_KNX_CONFIG] = dict(conf) register_knx_services(hass) - return True From 0a846cfca88cca1a8607a499733674c58172b0bd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:43:43 +0200 Subject: [PATCH 2387/2411] Add missing hass type in tests/test_util (#124043) --- tests/test_util/aiohttp.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index de1db0e4847..04d6db509e0 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -5,6 +5,7 @@ from collections.abc import Iterator from contextlib import contextmanager from http import HTTPStatus import re +from typing import Any from unittest import mock from urllib.parse import parse_qs @@ -19,6 +20,7 @@ from multidict import CIMultiDict from yarl import URL from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE +from homeassistant.core import HomeAssistant from homeassistant.helpers.json import json_dumps from homeassistant.util.json import json_loads @@ -301,7 +303,7 @@ def mock_aiohttp_client() -> Iterator[AiohttpClientMocker]: """Context manager to mock aiohttp client.""" mocker = AiohttpClientMocker() - def create_session(hass, *args, **kwargs): + def create_session(hass: HomeAssistant, *args: Any, **kwargs: Any) -> ClientSession: session = mocker.create_session(hass.loop) async def close_session(event): From 4cc4ec44b03fb7729d1ebf1576634b73cb51ff8e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 Aug 2024 13:50:02 +0200 Subject: [PATCH 2388/2411] Exclude aiohappyeyeballs from license check (#124041) --- script/licenses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/script/licenses.py b/script/licenses.py index 9d7da912398..0663821ed2c 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -125,6 +125,7 @@ EXCEPTIONS = { "PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201 "aiocomelit", # https://github.com/chemelli74/aiocomelit/pull/138 "aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180 + "aiohappyeyeballs", # Python-2.0.1 "aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94 "aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8 "aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6 From 738cc5095d47c7559bb55879b6664a571c013037 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:53:11 +0200 Subject: [PATCH 2389/2411] Bump openwebifpy to 4.2.7 (#123995) * Bump openwebifpy to 4.2.6 * Bump openwebifpy to 4.2.7 --------- Co-authored-by: J. Nick Koston --- homeassistant/components/enigma2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index 538cfb56388..1a0875b04c0 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["openwebif"], - "requirements": ["openwebifpy==4.2.5"] + "requirements": ["openwebifpy==4.2.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index f968b8addf5..dbd9aae5fda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1505,7 +1505,7 @@ openhomedevice==2.2.0 opensensemap-api==0.2.0 # homeassistant.components.enigma2 -openwebifpy==4.2.5 +openwebifpy==4.2.7 # homeassistant.components.luci openwrt-luci-rpc==1.1.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7302bbe5bab..8cf132dd9a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1241,7 +1241,7 @@ openerz-api==0.3.0 openhomedevice==2.2.0 # homeassistant.components.enigma2 -openwebifpy==4.2.5 +openwebifpy==4.2.7 # homeassistant.components.opower opower==0.6.0 From 69943af68ac842ba3e90371c4787c8a076fae545 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Aug 2024 14:06:35 +0200 Subject: [PATCH 2390/2411] Deduplicate async_register_entity_service (#124045) --- homeassistant/helpers/entity_component.py | 39 +++------------ homeassistant/helpers/entity_platform.py | 43 ++++------------- homeassistant/helpers/service.py | 58 ++++++++++++++++++++++- 3 files changed, 72 insertions(+), 68 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 826693de133..76abb3020d1 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -5,13 +5,11 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Iterable from datetime import timedelta -from functools import partial import logging from types import ModuleType from typing import Any, Generic from typing_extensions import TypeVar -import voluptuous as vol from homeassistant import config as conf_util from homeassistant.config_entries import ConfigEntry @@ -265,39 +263,16 @@ class EntityComponent(Generic[_EntityT]): supports_response: SupportsResponse = SupportsResponse.NONE, ) -> None: """Register an entity service.""" - if schema is None or isinstance(schema, dict): - schema = cv.make_entity_service_schema(schema) - # Do a sanity check to check this is a valid entity service schema, - # the check could be extended to require All/Any to have sub schema(s) - # with all entity service fields - elif ( - # Don't check All/Any - not isinstance(schema, (vol.All, vol.Any)) - # Don't check All/Any wrapped in schema - and not isinstance(schema.schema, (vol.All, vol.Any)) - and any(key not in schema.schema for key in cv.ENTITY_SERVICE_FIELDS) - ): - raise HomeAssistantError( - "The schema does not include all required keys: " - f"{", ".join(str(key) for key in cv.ENTITY_SERVICE_FIELDS)}" - ) - - service_func: str | HassJob[..., Any] - service_func = func if isinstance(func, str) else HassJob(func) - - self.hass.services.async_register( + service.async_register_entity_service( + self.hass, self.domain, name, - partial( - service.entity_service_call, - self.hass, - self._entities, - service_func, - required_features=required_features, - ), - schema, - supports_response, + entities=self._entities, + func=func, job_type=HassJobType.Coroutinefunction, + required_features=required_features, + schema=schema, + supports_response=supports_response, ) async def async_setup_platform( diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ec177fbf316..ce107d63b73 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -6,12 +6,9 @@ import asyncio from collections.abc import Awaitable, Callable, Coroutine, Iterable from contextvars import ContextVar from datetime import timedelta -from functools import partial from logging import Logger, getLogger from typing import TYPE_CHECKING, Any, Protocol -import voluptuous as vol - from homeassistant import config_entries from homeassistant.const import ( ATTR_RESTORED, @@ -22,7 +19,6 @@ from homeassistant.core import ( CALLBACK_TYPE, DOMAIN as HOMEASSISTANT_DOMAIN, CoreState, - HassJob, HomeAssistant, ServiceCall, SupportsResponse, @@ -43,7 +39,6 @@ from homeassistant.util.async_ import create_eager_task from homeassistant.util.hass_dict import HassKey from . import ( - config_validation as cv, device_registry as dev_reg, entity_registry as ent_reg, service, @@ -999,38 +994,16 @@ class EntityPlatform: if self.hass.services.has_service(self.platform_name, name): return - if schema is None or isinstance(schema, dict): - schema = cv.make_entity_service_schema(schema) - # Do a sanity check to check this is a valid entity service schema, - # the check could be extended to require All/Any to have sub schema(s) - # with all entity service fields - elif ( - # Don't check All/Any - not isinstance(schema, (vol.All, vol.Any)) - # Don't check All/Any wrapped in schema - and not isinstance(schema.schema, (vol.All, vol.Any)) - and any(key not in schema.schema for key in cv.ENTITY_SERVICE_FIELDS) - ): - raise HomeAssistantError( - "The schema does not include all required keys: " - f"{", ".join(str(key) for key in cv.ENTITY_SERVICE_FIELDS)}" - ) - - service_func: str | HassJob[..., Any] - service_func = func if isinstance(func, str) else HassJob(func) - - self.hass.services.async_register( + service.async_register_entity_service( + self.hass, self.platform_name, name, - partial( - service.entity_service_call, - self.hass, - self.domain_platform_entities, - service_func, - required_features=required_features, - ), - schema, - supports_response, + entities=self.domain_platform_entities, + func=func, + job_type=None, + required_features=required_features, + schema=schema, + supports_response=supports_response, ) async def _async_update_entity_states(self) -> None: diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index be4974906bb..0551b5289c5 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -33,6 +33,7 @@ from homeassistant.core import ( Context, EntityServiceResponse, HassJob, + HassJobType, HomeAssistant, ServiceCall, ServiceResponse, @@ -63,7 +64,7 @@ from . import ( ) from .group import expand_entity_ids from .selector import TargetSelector -from .typing import ConfigType, TemplateVarsType, VolSchemaType +from .typing import ConfigType, TemplateVarsType, VolDictType, VolSchemaType if TYPE_CHECKING: from .entity import Entity @@ -1240,3 +1241,58 @@ class ReloadServiceHelper[_T]: self._service_running = False self._pending_reload_targets -= reload_targets self._service_condition.notify_all() + + +@callback +def async_register_entity_service( + hass: HomeAssistant, + domain: str, + name: str, + *, + entities: dict[str, Entity], + func: str | Callable[..., Any], + job_type: HassJobType | None, + required_features: Iterable[int] | None = None, + schema: VolDictType | VolSchemaType | None, + supports_response: SupportsResponse = SupportsResponse.NONE, +) -> None: + """Help registering an entity service. + + This is called by EntityComponent.async_register_entity_service and + EntityPlatform.async_register_entity_service and should not be called + directly by integrations. + """ + if schema is None or isinstance(schema, dict): + schema = cv.make_entity_service_schema(schema) + # Do a sanity check to check this is a valid entity service schema, + # the check could be extended to require All/Any to have sub schema(s) + # with all entity service fields + elif ( + # Don't check All/Any + not isinstance(schema, (vol.All, vol.Any)) + # Don't check All/Any wrapped in schema + and not isinstance(schema.schema, (vol.All, vol.Any)) + and any(key not in schema.schema for key in cv.ENTITY_SERVICE_FIELDS) + ): + raise HomeAssistantError( + "The schema does not include all required keys: " + f"{", ".join(str(key) for key in cv.ENTITY_SERVICE_FIELDS)}" + ) + + service_func: str | HassJob[..., Any] + service_func = func if isinstance(func, str) else HassJob(func) + + hass.services.async_register( + domain, + name, + partial( + entity_service_call, + hass, + entities, + service_func, + required_features=required_features, + ), + schema, + supports_response, + job_type=job_type, + ) From f1b7847d1c1a6c72df564f2379a6332b93a75b06 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Aug 2024 14:09:09 +0200 Subject: [PATCH 2391/2411] Simplify cv._make_entity_service_schema (#124046) --- homeassistant/helpers/config_validation.py | 28 ++++++++++------------ 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index ed3eca6e316..6e9a6d5a69d 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1253,21 +1253,19 @@ TARGET_SERVICE_FIELDS = { _HAS_ENTITY_SERVICE_FIELD = has_at_least_one_key(*ENTITY_SERVICE_FIELDS) -def _make_entity_service_schema(schema: dict, extra: int) -> vol.Schema: +def _make_entity_service_schema(schema: dict, extra: int) -> VolSchemaType: """Create an entity service schema.""" - return vol.Schema( - vol.All( - vol.Schema( - { - # The frontend stores data here. Don't use in core. - vol.Remove("metadata"): dict, - **schema, - **ENTITY_SERVICE_FIELDS, - }, - extra=extra, - ), - _HAS_ENTITY_SERVICE_FIELD, - ) + return vol.All( + vol.Schema( + { + # The frontend stores data here. Don't use in core. + vol.Remove("metadata"): dict, + **schema, + **ENTITY_SERVICE_FIELDS, + }, + extra=extra, + ), + _HAS_ENTITY_SERVICE_FIELD, ) @@ -1276,7 +1274,7 @@ BASE_ENTITY_SCHEMA = _make_entity_service_schema({}, vol.PREVENT_EXTRA) def make_entity_service_schema( schema: dict | None, *, extra: int = vol.PREVENT_EXTRA -) -> vol.Schema: +) -> VolSchemaType: """Create an entity service schema.""" if not schema and extra == vol.PREVENT_EXTRA: # If the schema is empty and we don't allow extra keys, we can return From c717e7a6f6ab11370eebe040a43ff2bdeb431964 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Aug 2024 07:12:17 -0500 Subject: [PATCH 2392/2411] Bump bluetooth-adapters to 0.19.4 (#124018) Fixes a call to enumerate USB devices that did blocking I/O --- 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 95d2b171c9f..657209cdba0 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,7 +16,7 @@ "requirements": [ "bleak==0.22.2", "bleak-retry-connector==3.5.0", - "bluetooth-adapters==0.19.3", + "bluetooth-adapters==0.19.4", "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.4", "dbus-fast==2.22.1", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8cad4d2037a..900d4510c17 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ awesomeversion==24.6.0 bcrypt==4.1.3 bleak-retry-connector==3.5.0 bleak==0.22.2 -bluetooth-adapters==0.19.3 +bluetooth-adapters==0.19.4 bluetooth-auto-recovery==1.4.2 bluetooth-data-tools==1.19.4 cached_ipaddress==0.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index dbd9aae5fda..3e12a7c3c52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -591,7 +591,7 @@ bluemaestro-ble==0.2.3 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.3 +bluetooth-adapters==0.19.4 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8cf132dd9a7..a2fd97338f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ bluecurrent-api==1.2.3 bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.3 +bluetooth-adapters==0.19.4 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 From 14a3217d7e3b871b492184512d4e9b4b01826203 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Aug 2024 15:08:37 +0200 Subject: [PATCH 2393/2411] Improve entity platform tests (#124051) --- tests/helpers/test_entity_component.py | 2 +- tests/helpers/test_entity_platform.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 230944e6a96..5ce0292c2ec 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -574,7 +574,7 @@ async def test_register_entity_service_non_entity_service_schema( ) # The check currently does not recurse into vol.All or vol.Any allowing these - # non-compliatn schemas to pass + # non-compliant schemas to pass component.async_register_entity_service( "hello", vol.All(vol.Schema({"some": str})), Mock() ) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 50180ecd844..fcbc825bbdc 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1809,6 +1809,14 @@ async def test_register_entity_service_non_entity_service_schema( vol.Schema({"some": str}), Mock(), ) + # The check currently does not recurse into vol.All or vol.Any allowing these + # non-compliant schemas to pass + entity_platform.async_register_entity_service( + "hello", vol.All(vol.Schema({"some": str})), Mock() + ) + entity_platform.async_register_entity_service( + "hello", vol.Any(vol.Schema({"some": str})), Mock() + ) @pytest.mark.parametrize("update_before_add", [True, False]) From cb8a6af12d189eb97cd5f23c9c6f383759e831de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Aug 2024 09:03:24 -0500 Subject: [PATCH 2394/2411] Add additional blocking operations to loop protection (#124017) --- homeassistant/block_async_io.py | 55 +++++++++++++++++++++ tests/test_block_async_io.py | 88 +++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index 6ea0925574e..7a68b2515e9 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -8,6 +8,7 @@ import glob from http.client import HTTPConnection import importlib import os +from pathlib import Path from ssl import SSLContext import sys import threading @@ -162,6 +163,60 @@ _BLOCKING_CALLS: tuple[BlockingCall, ...] = ( strict_core=False, skip_for_tests=True, ), + BlockingCall( + original_func=SSLContext.load_cert_chain, + object=SSLContext, + function="load_cert_chain", + check_allowed=None, + strict=False, + strict_core=False, + skip_for_tests=True, + ), + BlockingCall( + original_func=Path.open, + object=Path, + function="open", + check_allowed=_check_file_allowed, + strict=False, + strict_core=False, + skip_for_tests=True, + ), + BlockingCall( + original_func=Path.read_text, + object=Path, + function="read_text", + check_allowed=_check_file_allowed, + strict=False, + strict_core=False, + skip_for_tests=True, + ), + BlockingCall( + original_func=Path.read_bytes, + object=Path, + function="read_bytes", + check_allowed=_check_file_allowed, + strict=False, + strict_core=False, + skip_for_tests=True, + ), + BlockingCall( + original_func=Path.write_text, + object=Path, + function="write_text", + check_allowed=_check_file_allowed, + strict=False, + strict_core=False, + skip_for_tests=True, + ), + BlockingCall( + original_func=Path.write_bytes, + object=Path, + function="write_bytes", + check_allowed=_check_file_allowed, + strict=False, + strict_core=False, + skip_for_tests=True, + ), ) diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index 78b8711310b..dc2b096f595 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -218,6 +218,17 @@ async def test_protect_loop_open(caplog: pytest.LogCaptureFixture) -> None: assert "Detected blocking call to open with args" not in caplog.text +async def test_protect_loop_path_open(caplog: pytest.LogCaptureFixture) -> None: + """Test opening a file in /proc is not reported.""" + block_async_io.enable() + with ( + contextlib.suppress(FileNotFoundError), + Path("/proc/does_not_exist").open(encoding="utf8"), # noqa: ASYNC230 + ): + pass + assert "Detected blocking call to open with args" not in caplog.text + + async def test_protect_open(caplog: pytest.LogCaptureFixture) -> None: """Test opening a file in the event loop logs.""" with patch.object(block_async_io, "_IN_TESTS", False): @@ -231,6 +242,71 @@ async def test_protect_open(caplog: pytest.LogCaptureFixture) -> None: assert "Detected blocking call to open with args" in caplog.text +async def test_protect_path_open(caplog: pytest.LogCaptureFixture) -> None: + """Test opening a file in the event loop logs.""" + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() + with ( + contextlib.suppress(FileNotFoundError), + Path("/config/data_not_exist").open(encoding="utf8"), # noqa: ASYNC230 + ): + pass + + assert "Detected blocking call to open with args" in caplog.text + + +async def test_protect_path_read_bytes(caplog: pytest.LogCaptureFixture) -> None: + """Test reading file bytes in the event loop logs.""" + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() + with ( + contextlib.suppress(FileNotFoundError), + Path("/config/data_not_exist").read_bytes(), # noqa: ASYNC230 + ): + pass + + assert "Detected blocking call to read_bytes with args" in caplog.text + + +async def test_protect_path_read_text(caplog: pytest.LogCaptureFixture) -> None: + """Test reading a file text in the event loop logs.""" + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() + with ( + contextlib.suppress(FileNotFoundError), + Path("/config/data_not_exist").read_text(encoding="utf8"), # noqa: ASYNC230 + ): + pass + + assert "Detected blocking call to read_text with args" in caplog.text + + +async def test_protect_path_write_bytes(caplog: pytest.LogCaptureFixture) -> None: + """Test writing file bytes in the event loop logs.""" + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() + with ( + contextlib.suppress(FileNotFoundError), + Path("/config/data/not/exist").write_bytes(b"xxx"), # noqa: ASYNC230 + ): + pass + + assert "Detected blocking call to write_bytes with args" in caplog.text + + +async def test_protect_path_write_text(caplog: pytest.LogCaptureFixture) -> None: + """Test writing file text in the event loop logs.""" + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() + with ( + contextlib.suppress(FileNotFoundError), + Path("/config/data/not/exist").write_text("xxx", encoding="utf8"), # noqa: ASYNC230 + ): + pass + + assert "Detected blocking call to write_text with args" in caplog.text + + async def test_enable_multiple_times(caplog: pytest.LogCaptureFixture) -> None: """Test trying to enable multiple times.""" with patch.object(block_async_io, "_IN_TESTS", False): @@ -354,6 +430,18 @@ async def test_protect_loop_load_verify_locations( assert "Detected blocking call to load_verify_locations" in caplog.text +async def test_protect_loop_load_cert_chain( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test SSLContext.load_cert_chain calls in the loop are logged.""" + with patch.object(block_async_io, "_IN_TESTS", False): + block_async_io.enable() + context = ssl.create_default_context() + with pytest.raises(OSError): + context.load_cert_chain("/dev/null") + assert "Detected blocking call to load_cert_chain" in caplog.text + + async def test_open_calls_ignored_in_tests(caplog: pytest.LogCaptureFixture) -> None: """Test opening a file in tests is ignored.""" assert block_async_io._IN_TESTS From ea4443f79e0470651ea46207b473e0e63d521c99 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 16 Aug 2024 16:12:15 +0200 Subject: [PATCH 2395/2411] Add statistics import to Ista EcoTrend integration (#118788) * Add statistics import to Ista EcoTrend integration * Use decorator for fixtures * define recorder as after_dependency * Increase test coverage * remember initial statistic_id * fix type checking --- .../components/ista_ecotrend/manifest.json | 1 + .../components/ista_ecotrend/sensor.py | 98 ++- tests/components/ista_ecotrend/conftest.py | 49 ++ .../snapshots/test_statistics.ambr | 609 ++++++++++++++++++ .../ista_ecotrend/test_statistics.py | 82 +++ 5 files changed, 837 insertions(+), 2 deletions(-) create mode 100644 tests/components/ista_ecotrend/snapshots/test_statistics.ambr create mode 100644 tests/components/ista_ecotrend/test_statistics.py diff --git a/homeassistant/components/ista_ecotrend/manifest.json b/homeassistant/components/ista_ecotrend/manifest.json index 23d60a0a5bb..baa5fbde9c0 100644 --- a/homeassistant/components/ista_ecotrend/manifest.json +++ b/homeassistant/components/ista_ecotrend/manifest.json @@ -1,6 +1,7 @@ { "domain": "ista_ecotrend", "name": "ista EcoTrend", + "after_dependencies": ["recorder"], "codeowners": ["@tr4nt0r"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ista_ecotrend", diff --git a/homeassistant/components/ista_ecotrend/sensor.py b/homeassistant/components/ista_ecotrend/sensor.py index 3ae2128e142..7aa1adfe4c9 100644 --- a/homeassistant/components/ista_ecotrend/sensor.py +++ b/homeassistant/components/ista_ecotrend/sensor.py @@ -2,10 +2,21 @@ from __future__ import annotations +import asyncio from dataclasses import dataclass +import datetime from enum import StrEnum import logging +from homeassistant.components.recorder.models.statistics import ( + StatisticData, + StatisticMetaData, +) +from homeassistant.components.recorder.statistics import ( + async_add_external_statistics, + get_instance, + get_last_statistics, +) from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -14,7 +25,11 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import UnitOfEnergy, UnitOfVolume from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.device_registry import ( + DeviceEntry, + DeviceEntryType, + DeviceInfo, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -22,7 +37,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import IstaConfigEntry from .const import DOMAIN from .coordinator import IstaCoordinator -from .util import IstaConsumptionType, IstaValueType, get_native_value +from .util import IstaConsumptionType, IstaValueType, get_native_value, get_statistics _LOGGER = logging.getLogger(__name__) @@ -155,6 +170,7 @@ class IstaSensor(CoordinatorEntity[IstaCoordinator], SensorEntity): entity_description: IstaSensorEntityDescription _attr_has_entity_name = True + device_entry: DeviceEntry def __init__( self, @@ -186,3 +202,81 @@ class IstaSensor(CoordinatorEntity[IstaCoordinator], SensorEntity): consumption_type=self.entity_description.consumption_type, value_type=self.entity_description.value_type, ) + + async def async_added_to_hass(self) -> None: + """When added to hass.""" + # perform initial statistics import when sensor is added, otherwise it would take + # 1 day when _handle_coordinator_update is triggered for the first time. + await self.update_statistics() + await super().async_added_to_hass() + + def _handle_coordinator_update(self) -> None: + """Handle coordinator update.""" + asyncio.run_coroutine_threadsafe(self.update_statistics(), self.hass.loop) + + async def update_statistics(self) -> None: + """Import ista EcoTrend historical statistics.""" + + # Remember the statistic_id that was initially created + # in case the entity gets renamed, because we cannot + # change the statistic_id + name = self.coordinator.config_entry.options.get( + f"lts_{self.entity_description.key}_{self.consumption_unit}" + ) + if not name: + name = self.entity_id.removeprefix("sensor.") + self.hass.config_entries.async_update_entry( + entry=self.coordinator.config_entry, + options={ + **self.coordinator.config_entry.options, + f"lts_{self.entity_description.key}_{self.consumption_unit}": name, + }, + ) + + statistic_id = f"{DOMAIN}:{name}" + statistics_sum = 0.0 + statistics_since = None + + last_stats = await get_instance(self.hass).async_add_executor_job( + get_last_statistics, + self.hass, + 1, + statistic_id, + False, + {"sum"}, + ) + + _LOGGER.debug("Last statistics: %s", last_stats) + + if last_stats: + statistics_sum = last_stats[statistic_id][0].get("sum") or 0.0 + statistics_since = datetime.datetime.fromtimestamp( + last_stats[statistic_id][0].get("end") or 0, tz=datetime.UTC + ) + datetime.timedelta(days=1) + + if monthly_consumptions := get_statistics( + self.coordinator.data[self.consumption_unit], + self.entity_description.consumption_type, + self.entity_description.value_type, + ): + statistics: list[StatisticData] = [ + { + "start": consumptions["date"], + "state": consumptions["value"], + "sum": (statistics_sum := statistics_sum + consumptions["value"]), + } + for consumptions in monthly_consumptions + if statistics_since is None or consumptions["date"] > statistics_since + ] + + metadata: StatisticMetaData = { + "has_mean": False, + "has_sum": True, + "name": f"{self.device_entry.name} {self.name}", + "source": DOMAIN, + "statistic_id": statistic_id, + "unit_of_measurement": self.entity_description.native_unit_of_measurement, + } + if statistics: + _LOGGER.debug("Insert statistics: %s %s", metadata, statistics) + async_add_external_statistics(self.hass, metadata, statistics) diff --git a/tests/components/ista_ecotrend/conftest.py b/tests/components/ista_ecotrend/conftest.py index cbbc166031d..7edf2e4717b 100644 --- a/tests/components/ista_ecotrend/conftest.py +++ b/tests/components/ista_ecotrend/conftest.py @@ -166,3 +166,52 @@ def get_consumption_data(obj_uuid: str | None = None) -> dict[str, Any]: }, ], } + + +def extend_statistics(obj_uuid: str | None = None) -> dict[str, Any]: + """Extend statistics data with new values.""" + stats = get_consumption_data(obj_uuid) + + stats["costs"].insert( + 0, + { + "date": {"month": 6, "year": 2024}, + "costsByEnergyType": [ + { + "type": "heating", + "value": 9000, + }, + { + "type": "warmwater", + "value": 9000, + }, + { + "type": "water", + "value": 9000, + }, + ], + }, + ) + stats["consumptions"].insert( + 0, + { + "date": {"month": 6, "year": 2024}, + "readings": [ + { + "type": "heating", + "value": "9000", + "additionalValue": "9000,0", + }, + { + "type": "warmwater", + "value": "9999,0", + "additionalValue": "90000,0", + }, + { + "type": "water", + "value": "9000,0", + }, + ], + }, + ) + return stats diff --git a/tests/components/ista_ecotrend/snapshots/test_statistics.ambr b/tests/components/ista_ecotrend/snapshots/test_statistics.ambr new file mode 100644 index 00000000000..78ecd6a6b6b --- /dev/null +++ b/tests/components/ista_ecotrend/snapshots/test_statistics.ambr @@ -0,0 +1,609 @@ +# serializer version: 1 +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_heating_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 104.0, + 'sum': 104.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 35.0, + 'sum': 139.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_heating_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 104.0, + 'sum': 104.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 35.0, + 'sum': 139.0, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 9000.0, + 'sum': 9139.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_heating_cost_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 62.0, + 'sum': 62.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 21.0, + 'sum': 83.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_heating_cost_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 62.0, + 'sum': 62.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 21.0, + 'sum': 83.0, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 9000.0, + 'sum': 9083.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_heating_energy_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 113.0, + 'sum': 113.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 38.0, + 'sum': 151.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_heating_energy_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 113.0, + 'sum': 113.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 38.0, + 'sum': 151.0, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 9000.0, + 'sum': 9151.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_hot_water_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 1.1, + 'sum': 1.1, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 1.0, + 'sum': 2.1, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_hot_water_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 1.1, + 'sum': 1.1, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 1.0, + 'sum': 2.1, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 9999.0, + 'sum': 10001.1, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_hot_water_cost_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 7.0, + 'sum': 7.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 7.0, + 'sum': 14.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_hot_water_cost_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 7.0, + 'sum': 7.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 7.0, + 'sum': 14.0, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 9000.0, + 'sum': 9014.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_hot_water_energy_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 61.1, + 'sum': 61.1, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 57.0, + 'sum': 118.1, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_hot_water_energy_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 61.1, + 'sum': 61.1, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 57.0, + 'sum': 118.1, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 90000.0, + 'sum': 90118.1, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_water_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 6.8, + 'sum': 6.8, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 5.0, + 'sum': 11.8, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_water_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 6.8, + 'sum': 6.8, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 5.0, + 'sum': 11.8, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 9000.0, + 'sum': 9011.8, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_water_cost_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 2.0, + 'sum': 2.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 3.0, + 'sum': 5.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_water_cost_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 2.0, + 'sum': 2.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 3.0, + 'sum': 5.0, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 9000.0, + 'sum': 9005.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_heating_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 104.0, + 'sum': 104.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 35.0, + 'sum': 139.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_heating_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 104.0, + 'sum': 104.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 35.0, + 'sum': 139.0, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 9000.0, + 'sum': 9139.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_heating_cost_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 62.0, + 'sum': 62.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 21.0, + 'sum': 83.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_heating_cost_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 62.0, + 'sum': 62.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 21.0, + 'sum': 83.0, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 9000.0, + 'sum': 9083.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_heating_energy_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 113.0, + 'sum': 113.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 38.0, + 'sum': 151.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_heating_energy_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 113.0, + 'sum': 113.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 38.0, + 'sum': 151.0, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 9000.0, + 'sum': 9151.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_hot_water_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 1.1, + 'sum': 1.1, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 1.0, + 'sum': 2.1, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_hot_water_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 1.1, + 'sum': 1.1, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 1.0, + 'sum': 2.1, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 9999.0, + 'sum': 10001.1, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_hot_water_cost_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 7.0, + 'sum': 7.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 7.0, + 'sum': 14.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_hot_water_cost_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 7.0, + 'sum': 7.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 7.0, + 'sum': 14.0, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 9000.0, + 'sum': 9014.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_hot_water_energy_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 61.1, + 'sum': 61.1, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 57.0, + 'sum': 118.1, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_hot_water_energy_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 61.1, + 'sum': 61.1, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 57.0, + 'sum': 118.1, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 90000.0, + 'sum': 90118.1, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_water_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 6.8, + 'sum': 6.8, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 5.0, + 'sum': 11.8, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_water_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 6.8, + 'sum': 6.8, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 5.0, + 'sum': 11.8, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 9000.0, + 'sum': 9011.8, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_water_cost_2months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 2.0, + 'sum': 2.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 3.0, + 'sum': 5.0, + }), + ]) +# --- +# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_water_cost_3months] + list([ + dict({ + 'end': 1714546800.0, + 'start': 1711954800.0, + 'state': 2.0, + 'sum': 2.0, + }), + dict({ + 'end': 1717225200.0, + 'start': 1714546800.0, + 'state': 3.0, + 'sum': 5.0, + }), + dict({ + 'end': 1719817200.0, + 'start': 1717225200.0, + 'state': 9000.0, + 'sum': 9005.0, + }), + ]) +# --- diff --git a/tests/components/ista_ecotrend/test_statistics.py b/tests/components/ista_ecotrend/test_statistics.py new file mode 100644 index 00000000000..6b2f98affe9 --- /dev/null +++ b/tests/components/ista_ecotrend/test_statistics.py @@ -0,0 +1,82 @@ +"""Tests for the ista EcoTrend Statistics import.""" + +import datetime +from unittest.mock import MagicMock + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.recorder.statistics import statistics_during_period +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import extend_statistics + +from tests.common import MockConfigEntry +from tests.components.recorder.common import async_wait_recording_done + + +@pytest.mark.usefixtures("recorder_mock", "entity_registry_enabled_by_default") +async def test_statistics_import( + hass: HomeAssistant, + ista_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + mock_ista: MagicMock, + snapshot: SnapshotAssertion, + freezer: FrozenDateTimeFactory, +) -> None: + """Test setup of ista EcoTrend sensor platform.""" + + ista_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(ista_config_entry.entry_id) + await hass.async_block_till_done() + + assert ista_config_entry.state is ConfigEntryState.LOADED + entities = er.async_entries_for_config_entry( + entity_registry, ista_config_entry.entry_id + ) + await async_wait_recording_done(hass) + + # Test that consumption statistics for 2 months have been added + for entity in entities: + statistic_id = f"ista_ecotrend:{entity.entity_id.removeprefix("sensor.")}" + stats = await hass.async_add_executor_job( + statistics_during_period, + hass, + datetime.datetime.fromtimestamp(0, tz=datetime.UTC), + None, + {statistic_id}, + "month", + None, + {"state", "sum"}, + ) + assert stats[statistic_id] == snapshot(name=f"{statistic_id}_2months") + assert len(stats[statistic_id]) == 2 + + # Add another monthly consumption and forward + # 1 day and test if the new values have been + # appended to the statistics + mock_ista.get_consumption_data = extend_statistics + + freezer.tick(datetime.timedelta(days=1)) + await async_wait_recording_done(hass) + freezer.tick(datetime.timedelta(days=1)) + await async_wait_recording_done(hass) + + for entity in entities: + statistic_id = f"ista_ecotrend:{entity.entity_id.removeprefix("sensor.")}" + stats = await hass.async_add_executor_job( + statistics_during_period, + hass, + datetime.datetime.fromtimestamp(0, tz=datetime.UTC), + None, + {statistic_id}, + "month", + None, + {"state", "sum"}, + ) + assert stats[statistic_id] == snapshot(name=f"{statistic_id}_3months") + + assert len(stats[statistic_id]) == 3 From 0cb0af496e9ca5284c6a296f26203cba0a255f91 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 Aug 2024 16:46:58 +0200 Subject: [PATCH 2396/2411] Re-enable concord232 (#124000) --- homeassistant/components/concord232/alarm_control_panel.py | 3 +-- homeassistant/components/concord232/binary_sensor.py | 3 +-- homeassistant/components/concord232/manifest.json | 3 +-- homeassistant/components/concord232/ruff.toml | 5 ----- requirements_all.txt | 3 +++ 5 files changed, 6 insertions(+), 11 deletions(-) delete mode 100644 homeassistant/components/concord232/ruff.toml diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index d3bafdeba4a..661a2beacc0 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -1,12 +1,11 @@ """Support for Concord232 alarm control panels.""" -# mypy: ignore-errors from __future__ import annotations import datetime import logging -# from concord232 import client as concord232_client +from concord232 import client as concord232_client import requests import voluptuous as vol diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 588e7681746..a1dcbc222f7 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -1,12 +1,11 @@ """Support for exposing Concord232 elements as sensors.""" -# mypy: ignore-errors from __future__ import annotations import datetime import logging -# from concord232 import client as concord232_client +from concord232 import client as concord232_client import requests import voluptuous as vol diff --git a/homeassistant/components/concord232/manifest.json b/homeassistant/components/concord232/manifest.json index ef075ba5f96..e0aea5d64d9 100644 --- a/homeassistant/components/concord232/manifest.json +++ b/homeassistant/components/concord232/manifest.json @@ -2,9 +2,8 @@ "domain": "concord232", "name": "Concord232", "codeowners": [], - "disabled": "This integration is disabled because it uses non-open source code to operate.", "documentation": "https://www.home-assistant.io/integrations/concord232", "iot_class": "local_polling", "loggers": ["concord232", "stevedore"], - "requirements": ["concord232==0.15"] + "requirements": ["concord232==0.15.1"] } diff --git a/homeassistant/components/concord232/ruff.toml b/homeassistant/components/concord232/ruff.toml deleted file mode 100644 index 38f6f586aef..00000000000 --- a/homeassistant/components/concord232/ruff.toml +++ /dev/null @@ -1,5 +0,0 @@ -extend = "../../../pyproject.toml" - -lint.extend-ignore = [ - "F821" -] diff --git a/requirements_all.txt b/requirements_all.txt index 3e12a7c3c52..8a73d65eadb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -672,6 +672,9 @@ colorlog==6.8.2 # homeassistant.components.color_extractor colorthief==0.2.1 +# homeassistant.components.concord232 +concord232==0.15.1 + # homeassistant.components.upc_connect connect-box==0.3.1 From e8d57bf63668d3ef6a572cb68f290e3ee03f676c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 Aug 2024 16:48:33 +0200 Subject: [PATCH 2397/2411] Bump aiomealie to 0.8.1 (#124047) --- homeassistant/components/mealie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index acfe30aecaa..75093577b0f 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mealie", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["aiomealie==0.8.0"] + "requirements": ["aiomealie==0.8.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8a73d65eadb..522ee846847 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -288,7 +288,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.8.0 +aiomealie==0.8.1 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2fd97338f2..31e448dbbe4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -270,7 +270,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.8.0 +aiomealie==0.8.1 # homeassistant.components.modern_forms aiomodernforms==0.1.8 From c8b0c939e42ae4b10a9a739853b624f6649f6386 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Aug 2024 09:48:59 -0500 Subject: [PATCH 2398/2411] Ensure event entities are allowed for linked homekit config via YAML (#123994) --- homeassistant/components/homekit/util.py | 7 +++- tests/components/homekit/test_util.py | 50 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index a4566efaa35..4d4620477cb 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -22,6 +22,7 @@ from homeassistant.components import ( sensor, ) from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.event import DOMAIN as EVENT_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.media_player import ( DOMAIN as MEDIA_PLAYER_DOMAIN, @@ -167,9 +168,11 @@ CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend( vol.Optional( CONF_VIDEO_PACKET_SIZE, default=DEFAULT_VIDEO_PACKET_SIZE ): cv.positive_int, - vol.Optional(CONF_LINKED_MOTION_SENSOR): cv.entity_domain(binary_sensor.DOMAIN), + vol.Optional(CONF_LINKED_MOTION_SENSOR): cv.entity_domain( + [binary_sensor.DOMAIN, EVENT_DOMAIN] + ), vol.Optional(CONF_LINKED_DOORBELL_SENSOR): cv.entity_domain( - binary_sensor.DOMAIN + [binary_sensor.DOMAIN, EVENT_DOMAIN] ), } ) diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 4939511166f..7f7e3ee0ce0 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -7,13 +7,38 @@ import voluptuous as vol from homeassistant.components.homekit.const import ( BRIDGE_NAME, + CONF_AUDIO_CODEC, + CONF_AUDIO_MAP, + CONF_AUDIO_PACKET_SIZE, CONF_FEATURE, CONF_FEATURE_LIST, CONF_LINKED_BATTERY_SENSOR, + CONF_LINKED_DOORBELL_SENSOR, + CONF_LINKED_MOTION_SENSOR, CONF_LOW_BATTERY_THRESHOLD, + CONF_MAX_FPS, + CONF_MAX_HEIGHT, + CONF_MAX_WIDTH, + CONF_STREAM_COUNT, + CONF_SUPPORT_AUDIO, CONF_THRESHOLD_CO, CONF_THRESHOLD_CO2, + CONF_VIDEO_CODEC, + CONF_VIDEO_MAP, + CONF_VIDEO_PACKET_SIZE, + DEFAULT_AUDIO_CODEC, + DEFAULT_AUDIO_MAP, + DEFAULT_AUDIO_PACKET_SIZE, DEFAULT_CONFIG_FLOW_PORT, + DEFAULT_LOW_BATTERY_THRESHOLD, + DEFAULT_MAX_FPS, + DEFAULT_MAX_HEIGHT, + DEFAULT_MAX_WIDTH, + DEFAULT_STREAM_COUNT, + DEFAULT_SUPPORT_AUDIO, + DEFAULT_VIDEO_CODEC, + DEFAULT_VIDEO_MAP, + DEFAULT_VIDEO_PACKET_SIZE, DOMAIN, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, @@ -178,6 +203,31 @@ def test_validate_entity_config() -> None: assert vec({"sensor.co2": {CONF_THRESHOLD_CO2: 500}}) == { "sensor.co2": {CONF_THRESHOLD_CO2: 500, CONF_LOW_BATTERY_THRESHOLD: 20} } + assert vec( + { + "camera.demo": { + CONF_LINKED_DOORBELL_SENSOR: "event.doorbell", + CONF_LINKED_MOTION_SENSOR: "event.motion", + } + } + ) == { + "camera.demo": { + CONF_LINKED_DOORBELL_SENSOR: "event.doorbell", + CONF_LINKED_MOTION_SENSOR: "event.motion", + CONF_AUDIO_CODEC: DEFAULT_AUDIO_CODEC, + CONF_SUPPORT_AUDIO: DEFAULT_SUPPORT_AUDIO, + CONF_MAX_WIDTH: DEFAULT_MAX_WIDTH, + CONF_MAX_HEIGHT: DEFAULT_MAX_HEIGHT, + CONF_MAX_FPS: DEFAULT_MAX_FPS, + CONF_AUDIO_MAP: DEFAULT_AUDIO_MAP, + CONF_VIDEO_MAP: DEFAULT_VIDEO_MAP, + CONF_STREAM_COUNT: DEFAULT_STREAM_COUNT, + CONF_VIDEO_CODEC: DEFAULT_VIDEO_CODEC, + CONF_AUDIO_PACKET_SIZE: DEFAULT_AUDIO_PACKET_SIZE, + CONF_VIDEO_PACKET_SIZE: DEFAULT_VIDEO_PACKET_SIZE, + CONF_LOW_BATTERY_THRESHOLD: DEFAULT_LOW_BATTERY_THRESHOLD, + } + } def test_validate_media_player_features() -> None: From 06209dd94c262eb4b2e13800b5e69d7e53b256ff Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 Aug 2024 16:54:20 +0200 Subject: [PATCH 2399/2411] Bump ruff to 0.6.0 (#123985) --- .pre-commit-config.yaml | 2 +- homeassistant/components/environment_canada/config_flow.py | 4 ++-- homeassistant/components/environment_canada/coordinator.py | 4 ++-- requirements_test_pre_commit.txt | 2 +- tests/components/environment_canada/test_config_flow.py | 4 ++-- tests/components/lg_netcast/__init__.py | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f057931e2a8..4c3ce7fe104 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.7 + rev: v0.6.0 hooks: - id: ruff args: diff --git a/homeassistant/components/environment_canada/config_flow.py b/homeassistant/components/environment_canada/config_flow.py index a351bb0ef06..79b37c64c1b 100644 --- a/homeassistant/components/environment_canada/config_flow.py +++ b/homeassistant/components/environment_canada/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Environment Canada integration.""" import logging -import xml.etree.ElementTree as et +import xml.etree.ElementTree as ET import aiohttp from env_canada import ECWeather, ec_exc @@ -52,7 +52,7 @@ class EnvironmentCanadaConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: try: info = await validate_input(user_input) - except (et.ParseError, vol.MultipleInvalid, ec_exc.UnknownStationId): + except (ET.ParseError, vol.MultipleInvalid, ec_exc.UnknownStationId): errors["base"] = "bad_station_id" except aiohttp.ClientConnectionError: errors["base"] = "cannot_connect" diff --git a/homeassistant/components/environment_canada/coordinator.py b/homeassistant/components/environment_canada/coordinator.py index e17c360e3fb..8e77b309c78 100644 --- a/homeassistant/components/environment_canada/coordinator.py +++ b/homeassistant/components/environment_canada/coordinator.py @@ -1,7 +1,7 @@ """Coordinator for the Environment Canada (EC) component.""" import logging -import xml.etree.ElementTree as et +import xml.etree.ElementTree as ET from env_canada import ec_exc @@ -27,6 +27,6 @@ class ECDataUpdateCoordinator(DataUpdateCoordinator): """Fetch data from EC.""" try: await self.ec_data.update() - except (et.ParseError, ec_exc.UnknownStationId) as ex: + except (ET.ParseError, ec_exc.UnknownStationId) as ex: raise UpdateFailed(f"Error fetching {self.name} data: {ex}") from ex return self.ec_data diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index ba54a19da3e..091f872d511 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.3.0 -ruff==0.5.7 +ruff==0.6.0 yamllint==1.35.1 diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index f2c35ab4295..d61966e8da1 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Environment Canada (EC) config flow.""" from unittest.mock import AsyncMock, MagicMock, Mock, patch -import xml.etree.ElementTree as et +import xml.etree.ElementTree as ET import aiohttp import pytest @@ -94,7 +94,7 @@ async def test_create_same_entry_twice(hass: HomeAssistant) -> None: (aiohttp.ClientResponseError(Mock(), (), status=404), "bad_station_id"), (aiohttp.ClientResponseError(Mock(), (), status=400), "error_response"), (aiohttp.ClientConnectionError, "cannot_connect"), - (et.ParseError, "bad_station_id"), + (ET.ParseError, "bad_station_id"), (ValueError, "unknown"), ], ) diff --git a/tests/components/lg_netcast/__init__.py b/tests/components/lg_netcast/__init__.py index ce3e09aeb65..6e608ae207b 100644 --- a/tests/components/lg_netcast/__init__.py +++ b/tests/components/lg_netcast/__init__.py @@ -1,7 +1,7 @@ """Tests for LG Netcast TV.""" from unittest.mock import patch -from xml.etree import ElementTree +import xml.etree.ElementTree as ET from pylgnetcast import AccessTokenError, LgNetCastClient, SessionIdError import requests @@ -56,7 +56,7 @@ def _patched_lgnetcast_client( if always_404: return None if invalid_details: - raise ElementTree.ParseError("Mocked Parsed Error") + raise ET.ParseError("Mocked Parsed Error") return { "uuid": UNIQUE_ID if not no_unique_id else None, "model_name": MODEL_NAME, From 115c5d1704a04ce90cc3c7c95bdfbceb99809511 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Aug 2024 16:59:33 +0200 Subject: [PATCH 2400/2411] Fix threading in get_test_home_assistant test helper (#124056) --- tests/common.py | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/tests/common.py b/tests/common.py index d36df509142..684b9eb0433 100644 --- a/tests/common.py +++ b/tests/common.py @@ -13,7 +13,12 @@ from collections.abc import ( Mapping, Sequence, ) -from contextlib import asynccontextmanager, contextmanager, suppress +from contextlib import ( + AbstractAsyncContextManager, + asynccontextmanager, + contextmanager, + suppress, +) from datetime import UTC, datetime, timedelta from enum import Enum import functools as ft @@ -177,24 +182,36 @@ def get_test_config_dir(*add_path): @contextmanager def get_test_home_assistant() -> Generator[HomeAssistant]: """Return a Home Assistant object pointing at test config directory.""" - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - context_manager = async_test_home_assistant(loop) - hass = loop.run_until_complete(context_manager.__aenter__()) - + hass_created_event = threading.Event() loop_stop_event = threading.Event() + context_manager: AbstractAsyncContextManager = None + hass: HomeAssistant = None + loop: asyncio.AbstractEventLoop = None + orig_stop: Callable = None + def run_loop() -> None: - """Run event loop.""" + """Create and run event loop.""" + nonlocal context_manager, hass, loop, orig_stop + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + context_manager = async_test_home_assistant(loop) + hass = loop.run_until_complete(context_manager.__aenter__()) + + orig_stop = hass.stop + hass._stopped = Mock(set=loop.stop) + hass.start = start_hass + hass.stop = stop_hass loop._thread_ident = threading.get_ident() + + hass_created_event.set() + hass.loop_thread_id = loop._thread_ident loop.run_forever() loop_stop_event.set() - orig_stop = hass.stop - hass._stopped = Mock(set=loop.stop) - def start_hass(*mocks: Any) -> None: """Start hass.""" asyncio.run_coroutine_threadsafe(hass.async_start(), loop).result() @@ -204,11 +221,10 @@ def get_test_home_assistant() -> Generator[HomeAssistant]: orig_stop() loop_stop_event.wait() - hass.start = start_hass - hass.stop = stop_hass - threading.Thread(name="LoopThread", target=run_loop, daemon=False).start() + hass_created_event.wait() + try: yield hass finally: From 2cd44567627c5e7ba44baf28191c76512d856b08 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:07:57 +0200 Subject: [PATCH 2401/2411] Add missing hass type hint in component tests (a) (#124059) --- tests/components/advantage_air/__init__.py | 3 +- tests/components/airthings_ble/__init__.py | 3 +- .../components/alarm_control_panel/common.py | 29 ++++++++++++++----- tests/components/apache_kafka/test_init.py | 11 ++++--- tests/components/auth/test_init.py | 3 +- tests/components/auth/test_init_link_user.py | 5 +++- 6 files changed, 39 insertions(+), 15 deletions(-) diff --git a/tests/components/advantage_air/__init__.py b/tests/components/advantage_air/__init__.py index 05d98e957bb..5587c668c7e 100644 --- a/tests/components/advantage_air/__init__.py +++ b/tests/components/advantage_air/__init__.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch from homeassistant.components.advantage_air.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_json_object_fixture @@ -43,7 +44,7 @@ def patch_update(return_value=True, side_effect=None): ) -async def add_mock_config(hass): +async def add_mock_config(hass: HomeAssistant) -> MockConfigEntry: """Create a fake Advantage Air Config Entry.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py index 45521903a08..a736fa979e9 100644 --- a/tests/components/airthings_ble/__init__.py +++ b/tests/components/airthings_ble/__init__.py @@ -13,6 +13,7 @@ from airthings_ble import ( from homeassistant.components.airthings_ble.const import DOMAIN from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceRegistry from tests.common import MockConfigEntry, MockEntity @@ -225,7 +226,7 @@ VOC_V3 = MockEntity( ) -def create_entry(hass): +def create_entry(hass: HomeAssistant) -> MockConfigEntry: """Create a config entry.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/alarm_control_panel/common.py b/tests/components/alarm_control_panel/common.py index 9ec419d8cf0..36e9918f54c 100644 --- a/tests/components/alarm_control_panel/common.py +++ b/tests/components/alarm_control_panel/common.py @@ -27,11 +27,14 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) +from homeassistant.core import HomeAssistant from tests.common import MockEntity -async def async_alarm_disarm(hass, code=None, entity_id=ENTITY_MATCH_ALL): +async def async_alarm_disarm( + hass: HomeAssistant, code: str | None = None, entity_id: str = ENTITY_MATCH_ALL +) -> None: """Send the alarm the command for disarm.""" data = {} if code: @@ -42,7 +45,9 @@ async def async_alarm_disarm(hass, code=None, entity_id=ENTITY_MATCH_ALL): await hass.services.async_call(DOMAIN, SERVICE_ALARM_DISARM, data, blocking=True) -async def async_alarm_arm_home(hass, code=None, entity_id=ENTITY_MATCH_ALL): +async def async_alarm_arm_home( + hass: HomeAssistant, code: str | None = None, entity_id: str = ENTITY_MATCH_ALL +) -> None: """Send the alarm the command for disarm.""" data = {} if code: @@ -53,7 +58,9 @@ async def async_alarm_arm_home(hass, code=None, entity_id=ENTITY_MATCH_ALL): await hass.services.async_call(DOMAIN, SERVICE_ALARM_ARM_HOME, data, blocking=True) -async def async_alarm_arm_away(hass, code=None, entity_id=ENTITY_MATCH_ALL): +async def async_alarm_arm_away( + hass: HomeAssistant, code: str | None = None, entity_id: str = ENTITY_MATCH_ALL +) -> None: """Send the alarm the command for disarm.""" data = {} if code: @@ -64,7 +71,9 @@ async def async_alarm_arm_away(hass, code=None, entity_id=ENTITY_MATCH_ALL): await hass.services.async_call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data, blocking=True) -async def async_alarm_arm_night(hass, code=None, entity_id=ENTITY_MATCH_ALL): +async def async_alarm_arm_night( + hass: HomeAssistant, code: str | None = None, entity_id: str = ENTITY_MATCH_ALL +) -> None: """Send the alarm the command for disarm.""" data = {} if code: @@ -75,7 +84,9 @@ async def async_alarm_arm_night(hass, code=None, entity_id=ENTITY_MATCH_ALL): await hass.services.async_call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data, blocking=True) -async def async_alarm_arm_vacation(hass, code=None, entity_id=ENTITY_MATCH_ALL): +async def async_alarm_arm_vacation( + hass: HomeAssistant, code: str | None = None, entity_id: str = ENTITY_MATCH_ALL +) -> None: """Send the alarm the command for vacation mode.""" data = {} if code: @@ -88,7 +99,9 @@ async def async_alarm_arm_vacation(hass, code=None, entity_id=ENTITY_MATCH_ALL): ) -async def async_alarm_trigger(hass, code=None, entity_id=ENTITY_MATCH_ALL): +async def async_alarm_trigger( + hass: HomeAssistant, code: str | None = None, entity_id: str = ENTITY_MATCH_ALL +) -> None: """Send the alarm the command for disarm.""" data = {} if code: @@ -99,7 +112,9 @@ async def async_alarm_trigger(hass, code=None, entity_id=ENTITY_MATCH_ALL): await hass.services.async_call(DOMAIN, SERVICE_ALARM_TRIGGER, data, blocking=True) -async def async_alarm_arm_custom_bypass(hass, code=None, entity_id=ENTITY_MATCH_ALL): +async def async_alarm_arm_custom_bypass( + hass: HomeAssistant, code: str | None = None, entity_id: str = ENTITY_MATCH_ALL +) -> None: """Send the alarm the command for disarm.""" data = {} if code: diff --git a/tests/components/apache_kafka/test_init.py b/tests/components/apache_kafka/test_init.py index 2b702046054..cffe08ffd4a 100644 --- a/tests/components/apache_kafka/test_init.py +++ b/tests/components/apache_kafka/test_init.py @@ -3,8 +3,9 @@ from __future__ import annotations from asyncio import AbstractEventLoop -from collections.abc import Callable +from collections.abc import Callable, Generator from dataclasses import dataclass +from typing import Any from unittest.mock import patch import pytest @@ -41,7 +42,7 @@ class MockKafkaClient: @pytest.fixture(name="mock_client") -def mock_client_fixture(): +def mock_client_fixture() -> Generator[MockKafkaClient]: """Mock the apache kafka client.""" with ( patch(f"{PRODUCER_PATH}.start") as start, @@ -89,7 +90,7 @@ async def test_full_config(hass: HomeAssistant, mock_client: MockKafkaClient) -> mock_client.start.assert_called_once() -async def _setup(hass, filter_config): +async def _setup(hass: HomeAssistant, filter_config: dict[str, Any]) -> None: """Shared set up for filtering tests.""" config = {apache_kafka.DOMAIN: {"filter": filter_config}} config[apache_kafka.DOMAIN].update(MIN_CONFIG) @@ -98,7 +99,9 @@ async def _setup(hass, filter_config): await hass.async_block_till_done() -async def _run_filter_tests(hass, tests, mock_client): +async def _run_filter_tests( + hass: HomeAssistant, tests: list[FilterTest], mock_client: MockKafkaClient +) -> None: """Run a series of filter tests on apache kafka.""" for test in tests: hass.states.async_set(test.id, STATE_ON) diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 0f4908c2fc0..718bb369b53 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -13,6 +13,7 @@ from homeassistant.auth.models import ( TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, TOKEN_TYPE_NORMAL, Credentials, + RefreshToken, ) from homeassistant.components import auth from homeassistant.core import HomeAssistant @@ -37,7 +38,7 @@ def mock_credential(): ) -async def async_setup_user_refresh_token(hass): +async def async_setup_user_refresh_token(hass: HomeAssistant) -> RefreshToken: """Create a testing user with a connected credential.""" user = await hass.auth.async_create_user("Test User") diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py index d1a5fa51af2..a8f04c2720d 100644 --- a/tests/components/auth/test_init_link_user.py +++ b/tests/components/auth/test_init_link_user.py @@ -1,6 +1,7 @@ """Tests for the link user flow.""" from http import HTTPStatus +from typing import Any from unittest.mock import patch from homeassistant.core import HomeAssistant @@ -11,7 +12,9 @@ from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI from tests.typing import ClientSessionGenerator -async def async_get_code(hass, aiohttp_client): +async def async_get_code( + hass: HomeAssistant, aiohttp_client: ClientSessionGenerator +) -> dict[str, Any]: """Return authorization code for link user tests.""" config = [ { From 56b4a7f2917a7f089de3558730acdf5861381a79 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:09:12 +0200 Subject: [PATCH 2402/2411] Add missing hass type in tests/helpers (#124039) --- .../helpers/test_config_entry_oauth2_flow.py | 4 +- tests/helpers/test_discovery.py | 15 ++- tests/helpers/test_entity.py | 23 +++-- tests/helpers/test_entity_platform.py | 93 +++++++++++++------ tests/helpers/test_integration_platform.py | 25 +++-- tests/helpers/test_significant_change.py | 13 ++- tests/helpers/test_singleton.py | 10 +- tests/helpers/test_start.py | 24 ++--- 8 files changed, 146 insertions(+), 61 deletions(-) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 23919f3a6a3..52def52f3f0 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -873,7 +873,9 @@ async def test_implementation_provider(hass: HomeAssistant, local_impl) -> None: provider_source = [] - async def async_provide_implementation(hass, domain): + async def async_provide_implementation( + hass: HomeAssistant, domain: str + ) -> list[config_entry_oauth2_flow.AbstractOAuth2Implementation]: """Mock implementation provider.""" return provider_source diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index 100b50e2749..a66ac7474e3 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -9,6 +9,8 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import discovery from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from tests.common import MockModule, MockPlatform, mock_integration, mock_platform @@ -115,7 +117,7 @@ async def test_circular_import(hass: HomeAssistant) -> None: component_calls = [] platform_calls = [] - def component_setup(hass, config): + def component_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up mock component.""" discovery.load_platform( hass, Platform.SWITCH, "test_circular", {"key": "value"}, config @@ -123,7 +125,12 @@ async def test_circular_import(hass: HomeAssistant) -> None: component_calls.append(1) return True - def setup_platform(hass, config, add_entities_callback, discovery_info=None): + def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities_callback: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: """Set up mock platform.""" platform_calls.append("disc" if discovery_info else "component") @@ -162,14 +169,14 @@ async def test_1st_discovers_2nd_component(hass: HomeAssistant) -> None: """ component_calls = [] - async def component1_setup(hass, config): + async def component1_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up mock component.""" await discovery.async_discover( hass, "test_component2", {}, "test_component2", {} ) return True - def component2_setup(hass, config): + def component2_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up mock component.""" component_calls.append(1) return True diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 283a5b4fb37..58554059fb4 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -16,6 +16,7 @@ import pytest from syrupy.assertion import SnapshotAssertion import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, @@ -34,6 +35,7 @@ from homeassistant.core import ( from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import UNDEFINED, UndefinedType from tests.common import ( @@ -981,10 +983,13 @@ async def _test_friendly_name( ) -> None: """Test friendly name.""" - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities([ent]) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") @@ -1306,10 +1311,13 @@ async def test_entity_name_translation_placeholder_errors( """Return all backend translations.""" return translations[language] - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities([ent]) - return True ent = MockEntity( unique_id="qwer", @@ -1531,7 +1539,11 @@ async def test_friendly_name_updated( ) -> None: """Test friendly name is updated when device or entity registry updates.""" - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities( [ @@ -1547,7 +1559,6 @@ async def test_friendly_name_updated( ), ] ) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index fcbc825bbdc..2cc3348626c 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -10,6 +10,7 @@ from unittest.mock import ANY, AsyncMock, Mock, patch import pytest import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, PERCENTAGE, EntityCategory from homeassistant.core import ( CoreState, @@ -33,6 +34,7 @@ from homeassistant.helpers.entity_component import ( DEFAULT_SCAN_INTERVAL, EntityComponent, ) +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -855,10 +857,13 @@ async def test_setup_entry( ) -> None: """Test we can setup an entry.""" - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities([MockEntity(name="test1", unique_id="unique")]) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") @@ -1138,7 +1143,11 @@ async def test_device_info_called( model="via", ) - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities( [ @@ -1163,7 +1172,6 @@ async def test_device_info_called( ), ] ) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) entity_platform = MockEntityPlatform( @@ -1207,7 +1215,11 @@ async def test_device_info_not_overrides( assert device.manufacturer == "test-manufacturer" assert device.model == "test-model" - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities( [ @@ -1222,7 +1234,6 @@ async def test_device_info_not_overrides( ) ] ) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) entity_platform = MockEntityPlatform( @@ -1257,7 +1268,11 @@ async def test_device_info_homeassistant_url( model="via", ) - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities( [ @@ -1271,7 +1286,6 @@ async def test_device_info_homeassistant_url( ), ] ) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) entity_platform = MockEntityPlatform( @@ -1306,7 +1320,11 @@ async def test_device_info_change_to_no_url( configuration_url="homeassistant://config/mqtt", ) - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities( [ @@ -1320,7 +1338,6 @@ async def test_device_info_change_to_no_url( ), ] ) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) entity_platform = MockEntityPlatform( @@ -1375,10 +1392,13 @@ async def test_entity_disabled_by_device( unique_id="disabled", device_info=DeviceInfo(connections=connections) ) - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities([entity_disabled]) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id", domain=DOMAIN) @@ -1855,13 +1875,16 @@ async def test_setup_entry_with_entities_that_block_forever( ) -> None: """Test we cancel adding entities when we reach the timeout.""" - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities( [MockBlockingEntity(name="test1", unique_id="unique")], update_before_add=update_before_add, ) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") @@ -1900,13 +1923,16 @@ async def test_cancellation_is_not_blocked( ) -> None: """Test cancellation is not blocked while adding entities.""" - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities( [MockCancellingEntity(name="test1", unique_id="unique")], update_before_add=update_before_add, ) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") @@ -1994,7 +2020,11 @@ async def test_entity_name_influences_entity_id( ) -> None: """Test entity_id is influenced by entity name.""" - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities( [ @@ -2011,7 +2041,6 @@ async def test_entity_name_influences_entity_id( ], update_before_add=update_before_add, ) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") @@ -2079,12 +2108,15 @@ async def test_translated_entity_name_influences_entity_id( """Return all backend translations.""" return translations[language] - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities( [TranslatedEntity(has_entity_name)], update_before_add=update_before_add ) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") @@ -2164,10 +2196,13 @@ async def test_translated_device_class_name_influences_entity_id( """Return all backend translations.""" return translations[language] - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities([TranslatedDeviceClassEntity(device_class, has_entity_name)]) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") @@ -2223,10 +2258,13 @@ async def test_device_name_defaulting_config_entry( _attr_unique_id = "qwer" _attr_device_info = device_info - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities([DeviceNameEntity()]) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(title=config_entry_title, entry_id="super-mock-id") @@ -2276,10 +2314,13 @@ async def test_device_type_error_checking( _attr_unique_id = "qwer" _attr_device_info = device_info - async def async_setup_entry(hass, config_entry, async_add_entities): + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Mock setup entry method.""" async_add_entities([DeviceNameEntity()]) - return True platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry( diff --git a/tests/helpers/test_integration_platform.py b/tests/helpers/test_integration_platform.py index 497bae5fb88..93bfeb2da5b 100644 --- a/tests/helpers/test_integration_platform.py +++ b/tests/helpers/test_integration_platform.py @@ -2,6 +2,7 @@ from collections.abc import Callable from types import ModuleType +from typing import Any from unittest.mock import Mock, patch import pytest @@ -29,7 +30,9 @@ async def test_process_integration_platforms_with_wait(hass: HomeAssistant) -> N processed = [] - async def _process_platform(hass, domain, platform): + async def _process_platform( + hass: HomeAssistant, domain: str, platform: Any + ) -> None: """Process platform.""" processed.append((domain, platform)) @@ -67,7 +70,9 @@ async def test_process_integration_platforms(hass: HomeAssistant) -> None: processed = [] - async def _process_platform(hass, domain, platform): + async def _process_platform( + hass: HomeAssistant, domain: str, platform: Any + ) -> None: """Process platform.""" processed.append((domain, platform)) @@ -107,7 +112,9 @@ async def test_process_integration_platforms_import_fails( processed = [] - async def _process_platform(hass, domain, platform): + async def _process_platform( + hass: HomeAssistant, domain: str, platform: Any + ) -> None: """Process platform.""" processed.append((domain, platform)) @@ -150,7 +157,9 @@ async def test_process_integration_platforms_import_fails_after_registered( processed = [] - async def _process_platform(hass, domain, platform): + async def _process_platform( + hass: HomeAssistant, domain: str, platform: Any + ) -> None: """Process platform.""" processed.append((domain, platform)) @@ -242,7 +251,9 @@ async def test_broken_integration( processed = [] - async def _process_platform(hass, domain, platform): + async def _process_platform( + hass: HomeAssistant, domain: str, platform: Any + ) -> None: """Process platform.""" processed.append((domain, platform)) @@ -265,7 +276,9 @@ async def test_process_integration_platforms_no_integrations( processed = [] - async def _process_platform(hass, domain, platform): + async def _process_platform( + hass: HomeAssistant, domain: str, platform: Any + ) -> None: """Process platform.""" processed.append((domain, platform)) diff --git a/tests/helpers/test_significant_change.py b/tests/helpers/test_significant_change.py index f9dca5b6034..577ea5907e5 100644 --- a/tests/helpers/test_significant_change.py +++ b/tests/helpers/test_significant_change.py @@ -1,5 +1,8 @@ """Test significant change helper.""" +from types import MappingProxyType +from typing import Any + import pytest from homeassistant.components.sensor import SensorDeviceClass @@ -67,8 +70,14 @@ async def test_significant_change_extra( assert checker.async_is_significant_change(State(ent_id, "100", attrs), extra_arg=1) def extra_significant_check( - hass, old_state, old_attrs, old_extra_arg, new_state, new_attrs, new_extra_arg - ): + hass: HomeAssistant, + old_state: str, + old_attrs: dict | MappingProxyType, + old_extra_arg: Any, + new_state: str, + new_attrs: dict | MappingProxyType, + new_extra_arg: Any, + ) -> bool | None: return old_extra_arg != new_extra_arg checker.extra_significant_check = extra_significant_check diff --git a/tests/helpers/test_singleton.py b/tests/helpers/test_singleton.py index dcda1e2db3a..4722c58dc9f 100644 --- a/tests/helpers/test_singleton.py +++ b/tests/helpers/test_singleton.py @@ -1,9 +1,11 @@ """Test singleton helper.""" +from typing import Any from unittest.mock import Mock import pytest +from homeassistant.core import HomeAssistant from homeassistant.helpers import singleton @@ -14,11 +16,11 @@ def mock_hass(): @pytest.mark.parametrize("result", [object(), {}, []]) -async def test_singleton_async(mock_hass, result) -> None: +async def test_singleton_async(mock_hass: HomeAssistant, result: Any) -> None: """Test singleton with async function.""" @singleton.singleton("test_key") - async def something(hass): + async def something(hass: HomeAssistant) -> Any: return result result1 = await something(mock_hass) @@ -30,11 +32,11 @@ async def test_singleton_async(mock_hass, result) -> None: @pytest.mark.parametrize("result", [object(), {}, []]) -def test_singleton(mock_hass, result) -> None: +def test_singleton(mock_hass: HomeAssistant, result: Any) -> None: """Test singleton with function.""" @singleton.singleton("test_key") - def something(hass): + def something(hass: HomeAssistant) -> Any: return result result1 = something(mock_hass) diff --git a/tests/helpers/test_start.py b/tests/helpers/test_start.py index d9c6bbf441c..bd6b328a2c7 100644 --- a/tests/helpers/test_start.py +++ b/tests/helpers/test_start.py @@ -14,7 +14,7 @@ async def test_at_start_when_running_awaitable(hass: HomeAssistant) -> None: calls = [] - async def cb_at_start(hass): + async def cb_at_start(hass: HomeAssistant) -> None: """Home Assistant is started.""" calls.append(1) @@ -40,7 +40,7 @@ async def test_at_start_when_running_callback( calls = [] @callback - def cb_at_start(hass): + def cb_at_start(hass: HomeAssistant) -> None: """Home Assistant is started.""" calls.append(1) @@ -65,7 +65,7 @@ async def test_at_start_when_starting_awaitable(hass: HomeAssistant) -> None: calls = [] - async def cb_at_start(hass): + async def cb_at_start(hass: HomeAssistant) -> None: """Home Assistant is started.""" calls.append(1) @@ -88,7 +88,7 @@ async def test_at_start_when_starting_callback( calls = [] @callback - def cb_at_start(hass): + def cb_at_start(hass: HomeAssistant) -> None: """Home Assistant is started.""" calls.append(1) @@ -116,7 +116,7 @@ async def test_cancelling_at_start_when_running( calls = [] - async def cb_at_start(hass): + async def cb_at_start(hass: HomeAssistant) -> None: """Home Assistant is started.""" calls.append(1) @@ -137,7 +137,7 @@ async def test_cancelling_at_start_when_starting(hass: HomeAssistant) -> None: calls = [] @callback - def cb_at_start(hass): + def cb_at_start(hass: HomeAssistant) -> None: """Home Assistant is started.""" calls.append(1) @@ -156,7 +156,7 @@ async def test_at_started_when_running_awaitable(hass: HomeAssistant) -> None: calls = [] - async def cb_at_start(hass): + async def cb_at_start(hass: HomeAssistant) -> None: """Home Assistant is started.""" calls.append(1) @@ -181,7 +181,7 @@ async def test_at_started_when_running_callback( calls = [] @callback - def cb_at_start(hass): + def cb_at_start(hass: HomeAssistant) -> None: """Home Assistant is started.""" calls.append(1) @@ -205,7 +205,7 @@ async def test_at_started_when_starting_awaitable(hass: HomeAssistant) -> None: calls = [] - async def cb_at_start(hass): + async def cb_at_start(hass: HomeAssistant) -> None: """Home Assistant is started.""" calls.append(1) @@ -231,7 +231,7 @@ async def test_at_started_when_starting_callback( calls = [] @callback - def cb_at_start(hass): + def cb_at_start(hass: HomeAssistant) -> None: """Home Assistant is started.""" calls.append(1) @@ -263,7 +263,7 @@ async def test_cancelling_at_started_when_running( calls = [] - async def cb_at_start(hass): + async def cb_at_start(hass: HomeAssistant) -> None: """Home Assistant is started.""" calls.append(1) @@ -284,7 +284,7 @@ async def test_cancelling_at_started_when_starting(hass: HomeAssistant) -> None: calls = [] @callback - def cb_at_start(hass): + def cb_at_start(hass: HomeAssistant) -> None: """Home Assistant is started.""" calls.append(1) From e07768412a0343ba73bfc27190777927a27eb73a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Aug 2024 17:16:56 +0200 Subject: [PATCH 2403/2411] Update ffmpeg tests to async (#124058) --- tests/components/ffmpeg/test_init.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index ceefed8d62b..e9a781327e0 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -16,9 +16,9 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, get_test_home_assistant +from tests.common import assert_setup_component @callback @@ -82,26 +82,22 @@ class MockFFmpegDev(ffmpeg.FFmpegBase): self.called_entities = entity_ids -def test_setup_component() -> None: +async def test_setup_component(hass: HomeAssistant) -> None: """Set up ffmpeg component.""" - with get_test_home_assistant() as hass: - with assert_setup_component(1): - setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + with assert_setup_component(1): + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) - assert hass.data[ffmpeg.DATA_FFMPEG].binary == "ffmpeg" - hass.stop() + assert hass.data[ffmpeg.DATA_FFMPEG].binary == "ffmpeg" -def test_setup_component_test_service() -> None: +async def test_setup_component_test_service(hass: HomeAssistant) -> None: """Set up ffmpeg component test services.""" - with get_test_home_assistant() as hass: - with assert_setup_component(1): - setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + with assert_setup_component(1): + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) - assert hass.services.has_service(ffmpeg.DOMAIN, "start") - assert hass.services.has_service(ffmpeg.DOMAIN, "stop") - assert hass.services.has_service(ffmpeg.DOMAIN, "restart") - hass.stop() + assert hass.services.has_service(ffmpeg.DOMAIN, "start") + assert hass.services.has_service(ffmpeg.DOMAIN, "stop") + assert hass.services.has_service(ffmpeg.DOMAIN, "restart") async def test_setup_component_test_register(hass: HomeAssistant) -> None: From 8a110abc82e00e323ebac6d85e07548658fff167 Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:46:37 +0200 Subject: [PATCH 2404/2411] Bump fyta_cli to 0.6.0 (#123816) * Bump fyta_cli to 0.5.1 * Code adjustments to enable strit typing * Update homeassistant/components/fyta/__init__.py Co-authored-by: Joost Lekkerkerker * Update diagnostics * Update config_flow + init (ruff) * Update sensor * Update coordinator * Update homeassistant/components/fyta/diagnostics.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/fyta/diagnostics.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/fyta/sensor.py Co-authored-by: Joost Lekkerkerker * Set one ph sensor to null/none * Update sensor * Clean-up (ruff) --------- Co-authored-by: Joost Lekkerkerker --- .strict-typing | 1 + homeassistant/components/fyta/__init__.py | 7 +- homeassistant/components/fyta/config_flow.py | 24 +- homeassistant/components/fyta/coordinator.py | 12 +- homeassistant/components/fyta/diagnostics.py | 2 +- homeassistant/components/fyta/entity.py | 8 +- homeassistant/components/fyta/manifest.json | 2 +- homeassistant/components/fyta/sensor.py | 34 +- mypy.ini | 10 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/fyta/conftest.py | 25 +- .../components/fyta/fixtures/plant_list.json | 4 - .../fyta/fixtures/plant_status.json | 14 - .../fyta/fixtures/plant_status1.json | 23 + .../fyta/fixtures/plant_status2.json | 23 + .../fyta/snapshots/test_diagnostics.ambr | 38 +- .../fyta/snapshots/test_sensor.ambr | 1124 ++++++++++++++++- 18 files changed, 1279 insertions(+), 76 deletions(-) delete mode 100644 tests/components/fyta/fixtures/plant_list.json delete mode 100644 tests/components/fyta/fixtures/plant_status.json create mode 100644 tests/components/fyta/fixtures/plant_status1.json create mode 100644 tests/components/fyta/fixtures/plant_status2.json diff --git a/.strict-typing b/.strict-typing index 1eec42ad209..51ca93bb9fa 100644 --- a/.strict-typing +++ b/.strict-typing @@ -197,6 +197,7 @@ homeassistant.components.fritzbox_callmonitor.* homeassistant.components.fronius.* homeassistant.components.frontend.* homeassistant.components.fully_kiosk.* +homeassistant.components.fyta.* homeassistant.components.generic_hygrostat.* homeassistant.components.generic_thermostat.* homeassistant.components.geo_location.* diff --git a/homeassistant/components/fyta/__init__.py b/homeassistant/components/fyta/__init__.py index b666c5a1f52..efbb1453456 100644 --- a/homeassistant/components/fyta/__init__.py +++ b/homeassistant/components/fyta/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations from datetime import datetime import logging -from typing import Any from fyta_cli.fyta_connector import FytaConnector @@ -73,11 +72,11 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> fyta = FytaConnector( config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD] ) - credentials: dict[str, Any] = await fyta.login() + credentials = await fyta.login() await fyta.client.close() - new[CONF_ACCESS_TOKEN] = credentials[CONF_ACCESS_TOKEN] - new[CONF_EXPIRATION] = credentials[CONF_EXPIRATION].isoformat() + new[CONF_ACCESS_TOKEN] = credentials.access_token + new[CONF_EXPIRATION] = credentials.expiration.isoformat() hass.config_entries.async_update_entry( config_entry, data=new, minor_version=2, version=1 diff --git a/homeassistant/components/fyta/config_flow.py b/homeassistant/components/fyta/config_flow.py index 4cb8bddbf10..f2b5163c9db 100644 --- a/homeassistant/components/fyta/config_flow.py +++ b/homeassistant/components/fyta/config_flow.py @@ -12,10 +12,11 @@ from fyta_cli.fyta_exceptions import ( FytaConnectionError, FytaPasswordError, ) +from fyta_cli.fyta_models import Credentials import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.selector import ( TextSelector, TextSelectorConfig, @@ -49,14 +50,11 @@ DATA_SCHEMA = vol.Schema( class FytaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Fyta.""" + credentials: Credentials + _entry: FytaConfigEntry | None = None VERSION = 1 MINOR_VERSION = 2 - def __init__(self) -> None: - """Initialize FytaConfigFlow.""" - self.credentials: dict[str, Any] = {} - self._entry: FytaConfigEntry | None = None - async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]: """Reusable Auth Helper.""" fyta = FytaConnector(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) @@ -75,10 +73,6 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN): finally: await fyta.client.close() - self.credentials[CONF_EXPIRATION] = self.credentials[ - CONF_EXPIRATION - ].isoformat() - return {} async def async_step_user( @@ -90,7 +84,10 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]}) if not (errors := await self.async_auth(user_input)): - user_input |= self.credentials + user_input |= { + CONF_ACCESS_TOKEN: self.credentials.access_token, + CONF_EXPIRATION: self.credentials.expiration.isoformat(), + } return self.async_create_entry( title=user_input[CONF_USERNAME], data=user_input ) @@ -114,7 +111,10 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN): assert self._entry is not None if user_input and not (errors := await self.async_auth(user_input)): - user_input |= self.credentials + user_input |= { + CONF_ACCESS_TOKEN: self.credentials.access_token, + CONF_EXPIRATION: self.credentials.expiration.isoformat(), + } return self.async_update_reload_and_abort( self._entry, data={**self._entry.data, **user_input} ) diff --git a/homeassistant/components/fyta/coordinator.py b/homeassistant/components/fyta/coordinator.py index b6fbf73ec25..c92a96eed63 100644 --- a/homeassistant/components/fyta/coordinator.py +++ b/homeassistant/components/fyta/coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime, timedelta import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from fyta_cli.fyta_connector import FytaConnector from fyta_cli.fyta_exceptions import ( @@ -13,6 +13,7 @@ from fyta_cli.fyta_exceptions import ( FytaPasswordError, FytaPlantError, ) +from fyta_cli.fyta_models import Plant from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant @@ -27,7 +28,7 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]): +class FytaCoordinator(DataUpdateCoordinator[dict[int, Plant]]): """Fyta custom coordinator.""" config_entry: FytaConfigEntry @@ -44,7 +45,7 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]): async def _async_update_data( self, - ) -> dict[int, dict[str, Any]]: + ) -> dict[int, Plant]: """Fetch data from API endpoint.""" if ( @@ -60,7 +61,6 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]): async def renew_authentication(self) -> bool: """Renew access token for FYTA API.""" - credentials: dict[str, Any] = {} try: credentials = await self.fyta.login() @@ -70,8 +70,8 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]): raise ConfigEntryAuthFailed from ex new_config_entry = {**self.config_entry.data} - new_config_entry[CONF_ACCESS_TOKEN] = credentials[CONF_ACCESS_TOKEN] - new_config_entry[CONF_EXPIRATION] = credentials[CONF_EXPIRATION].isoformat() + new_config_entry[CONF_ACCESS_TOKEN] = credentials.access_token + new_config_entry[CONF_EXPIRATION] = credentials.expiration.isoformat() self.hass.config_entries.async_update_entry( self.config_entry, data=new_config_entry diff --git a/homeassistant/components/fyta/diagnostics.py b/homeassistant/components/fyta/diagnostics.py index 55720b75ee6..d02f8cacfa3 100644 --- a/homeassistant/components/fyta/diagnostics.py +++ b/homeassistant/components/fyta/diagnostics.py @@ -25,5 +25,5 @@ async def async_get_config_entry_diagnostics( return { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), - "plant_data": data, + "plant_data": {key: value.to_dict() for key, value in data.items()}, } diff --git a/homeassistant/components/fyta/entity.py b/homeassistant/components/fyta/entity.py index 681a50f4cf5..18c52d74e25 100644 --- a/homeassistant/components/fyta/entity.py +++ b/homeassistant/components/fyta/entity.py @@ -1,6 +1,6 @@ """Entities for FYTA integration.""" -from typing import Any +from fyta_cli.fyta_models import Plant from homeassistant.components.sensor import SensorEntityDescription from homeassistant.config_entries import ConfigEntry @@ -32,13 +32,13 @@ class FytaPlantEntity(CoordinatorEntity[FytaCoordinator]): manufacturer="Fyta", model="Plant", identifiers={(DOMAIN, f"{entry.entry_id}-{plant_id}")}, - name=self.plant.get("name"), - sw_version=self.plant.get("sw_version"), + name=self.plant.name, + sw_version=self.plant.sw_version, ) self.entity_description = description @property - def plant(self) -> dict[str, Any]: + def plant(self) -> Plant: """Get plant data.""" return self.coordinator.data[self.plant_id] diff --git a/homeassistant/components/fyta/manifest.json b/homeassistant/components/fyta/manifest.json index f0953dd2a33..07387f4ab05 100644 --- a/homeassistant/components/fyta/manifest.json +++ b/homeassistant/components/fyta/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["fyta_cli==0.4.1"] + "requirements": ["fyta_cli==0.6.0"] } diff --git a/homeassistant/components/fyta/sensor.py b/homeassistant/components/fyta/sensor.py index 27576ae5065..262d0b6d1f4 100644 --- a/homeassistant/components/fyta/sensor.py +++ b/homeassistant/components/fyta/sensor.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from datetime import datetime from typing import Final -from fyta_cli.fyta_connector import PLANT_MEASUREMENT_STATUS, PLANT_STATUS +from fyta_cli.fyta_models import Plant from homeassistant.components.sensor import ( SensorDeviceClass, @@ -23,19 +23,18 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from . import FytaConfigEntry from .coordinator import FytaCoordinator from .entity import FytaPlantEntity -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class FytaSensorEntityDescription(SensorEntityDescription): """Describes Fyta sensor entity.""" - value_fn: Callable[[str | int | float | datetime], str | int | float | datetime] = ( - lambda value: value - ) + value_fn: Callable[[Plant], StateType | datetime] PLANT_STATUS_LIST: list[str] = ["deleted", "doing_great", "need_attention", "no_sensor"] @@ -48,63 +47,68 @@ PLANT_MEASUREMENT_STATUS_LIST: list[str] = [ "too_high", ] + SENSORS: Final[list[FytaSensorEntityDescription]] = [ FytaSensorEntityDescription( key="scientific_name", translation_key="scientific_name", + value_fn=lambda plant: plant.scientific_name, ), FytaSensorEntityDescription( key="status", translation_key="plant_status", device_class=SensorDeviceClass.ENUM, options=PLANT_STATUS_LIST, - value_fn=PLANT_STATUS.get, + value_fn=lambda plant: plant.status.name.lower(), ), FytaSensorEntityDescription( key="temperature_status", translation_key="temperature_status", device_class=SensorDeviceClass.ENUM, options=PLANT_MEASUREMENT_STATUS_LIST, - value_fn=PLANT_MEASUREMENT_STATUS.get, + value_fn=lambda plant: plant.temperature_status.name.lower(), ), FytaSensorEntityDescription( key="light_status", translation_key="light_status", device_class=SensorDeviceClass.ENUM, options=PLANT_MEASUREMENT_STATUS_LIST, - value_fn=PLANT_MEASUREMENT_STATUS.get, + value_fn=lambda plant: plant.light_status.name.lower(), ), FytaSensorEntityDescription( key="moisture_status", translation_key="moisture_status", device_class=SensorDeviceClass.ENUM, options=PLANT_MEASUREMENT_STATUS_LIST, - value_fn=PLANT_MEASUREMENT_STATUS.get, + value_fn=lambda plant: plant.moisture_status.name.lower(), ), FytaSensorEntityDescription( key="salinity_status", translation_key="salinity_status", device_class=SensorDeviceClass.ENUM, options=PLANT_MEASUREMENT_STATUS_LIST, - value_fn=PLANT_MEASUREMENT_STATUS.get, + value_fn=lambda plant: plant.salinity_status.name.lower(), ), FytaSensorEntityDescription( key="temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda plant: plant.temperature, ), FytaSensorEntityDescription( key="light", translation_key="light", native_unit_of_measurement="μmol/s⋅m²", state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda plant: plant.light, ), FytaSensorEntityDescription( key="moisture", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.MOISTURE, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda plant: plant.moisture, ), FytaSensorEntityDescription( key="salinity", @@ -112,11 +116,13 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [ native_unit_of_measurement=UnitOfConductivity.MILLISIEMENS, device_class=SensorDeviceClass.CONDUCTIVITY, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda plant: plant.salinity, ), FytaSensorEntityDescription( key="ph", device_class=SensorDeviceClass.PH, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda plant: plant.ph, ), FytaSensorEntityDescription( key="battery_level", @@ -124,6 +130,7 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [ device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda plant: plant.battery_level, ), ] @@ -138,7 +145,7 @@ async def async_setup_entry( FytaPlantSensor(coordinator, entry, sensor, plant_id) for plant_id in coordinator.fyta.plant_list for sensor in SENSORS - if sensor.key in coordinator.data[plant_id] + if sensor.key in dir(coordinator.data[plant_id]) ] async_add_entities(plant_entities) @@ -150,8 +157,7 @@ class FytaPlantSensor(FytaPlantEntity, SensorEntity): entity_description: FytaSensorEntityDescription @property - def native_value(self) -> str | int | float | datetime: + def native_value(self) -> StateType | datetime: """Return the state for this sensor.""" - val = self.plant[self.entity_description.key] - return self.entity_description.value_fn(val) + return self.entity_description.value_fn(self.plant) diff --git a/mypy.ini b/mypy.ini index c5478689702..f0a941f20eb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1726,6 +1726,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.fyta.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.generic_hygrostat.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 522ee846847..59b8b35730e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -921,7 +921,7 @@ freesms==0.2.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.4.1 +fyta_cli==0.6.0 # homeassistant.components.google_translate gTTS==2.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31e448dbbe4..90ea1d2c806 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -774,7 +774,7 @@ freebox-api==1.1.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.4.1 +fyta_cli==0.6.0 # homeassistant.components.google_translate gTTS==2.2.4 diff --git a/tests/components/fyta/conftest.py b/tests/components/fyta/conftest.py index 6a67ae75ec2..2bcad9b3c80 100644 --- a/tests/components/fyta/conftest.py +++ b/tests/components/fyta/conftest.py @@ -4,6 +4,7 @@ from collections.abc import Generator from datetime import UTC, datetime from unittest.mock import AsyncMock, patch +from fyta_cli.fyta_models import Credentials, Plant import pytest from homeassistant.components.fyta.const import CONF_EXPIRATION, DOMAIN as FYTA_DOMAIN @@ -35,23 +36,27 @@ def mock_config_entry() -> MockConfigEntry: def mock_fyta_connector(): """Build a fixture for the Fyta API that connects successfully and returns one device.""" + plants: dict[int, Plant] = { + 0: Plant.from_dict(load_json_object_fixture("plant_status1.json", FYTA_DOMAIN)), + 1: Plant.from_dict(load_json_object_fixture("plant_status2.json", FYTA_DOMAIN)), + } + mock_fyta_connector = AsyncMock() mock_fyta_connector.expiration = datetime.fromisoformat(EXPIRATION).replace( tzinfo=UTC ) mock_fyta_connector.client = AsyncMock(autospec=True) - mock_fyta_connector.update_all_plants.return_value = load_json_object_fixture( - "plant_status.json", FYTA_DOMAIN - ) - mock_fyta_connector.plant_list = load_json_object_fixture( - "plant_list.json", FYTA_DOMAIN - ) + mock_fyta_connector.update_all_plants.return_value = plants + mock_fyta_connector.plant_list = { + 0: "Gummibaum", + 1: "Kakaobaum", + } mock_fyta_connector.login = AsyncMock( - return_value={ - CONF_ACCESS_TOKEN: ACCESS_TOKEN, - CONF_EXPIRATION: datetime.fromisoformat(EXPIRATION).replace(tzinfo=UTC), - } + return_value=Credentials( + access_token=ACCESS_TOKEN, + expiration=datetime.fromisoformat(EXPIRATION).replace(tzinfo=UTC), + ) ) with ( patch( diff --git a/tests/components/fyta/fixtures/plant_list.json b/tests/components/fyta/fixtures/plant_list.json deleted file mode 100644 index 9527c7d9d96..00000000000 --- a/tests/components/fyta/fixtures/plant_list.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "0": "Gummibaum", - "1": "Kakaobaum" -} diff --git a/tests/components/fyta/fixtures/plant_status.json b/tests/components/fyta/fixtures/plant_status.json deleted file mode 100644 index 5d9cb2d31d9..00000000000 --- a/tests/components/fyta/fixtures/plant_status.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "0": { - "name": "Gummibaum", - "scientific_name": "Ficus elastica", - "status": 1, - "sw_version": "1.0" - }, - "1": { - "name": "Kakaobaum", - "scientific_name": "Theobroma cacao", - "status": 2, - "sw_version": "1.0" - } -} diff --git a/tests/components/fyta/fixtures/plant_status1.json b/tests/components/fyta/fixtures/plant_status1.json new file mode 100644 index 00000000000..f2e8dc9c970 --- /dev/null +++ b/tests/components/fyta/fixtures/plant_status1.json @@ -0,0 +1,23 @@ +{ + "battery_level": 80, + "battery_status": true, + "last_updated": "2023-01-10 10:10:00", + "light": 2, + "light_status": 3, + "nickname": "Gummibaum", + "moisture": 61, + "moisture_status": 3, + "sensor_available": true, + "sw_version": "1.0", + "status": 3, + "online": true, + "ph": null, + "plant_id": 0, + "plant_origin_path": "", + "plant_thumb_path": "", + "salinity": 1, + "salinity_status": 4, + "scientific_name": "Ficus elastica", + "temperature": 25.2, + "temperature_status": 3 +} diff --git a/tests/components/fyta/fixtures/plant_status2.json b/tests/components/fyta/fixtures/plant_status2.json new file mode 100644 index 00000000000..a5c2735ca7c --- /dev/null +++ b/tests/components/fyta/fixtures/plant_status2.json @@ -0,0 +1,23 @@ +{ + "battery_level": 80, + "battery_status": true, + "last_updated": "2023-01-02 10:10:00", + "light": 2, + "light_status": 3, + "nickname": "Kakaobaum", + "moisture": 61, + "moisture_status": 3, + "sensor_available": true, + "sw_version": "1.0", + "status": 3, + "online": true, + "ph": 7, + "plant_id": 0, + "plant_origin_path": "", + "plant_thumb_path": "", + "salinity": 1, + "salinity_status": 4, + "scientific_name": "Theobroma cacao", + "temperature": 25.2, + "temperature_status": 3 +} diff --git a/tests/components/fyta/snapshots/test_diagnostics.ambr b/tests/components/fyta/snapshots/test_diagnostics.ambr index 7491310129b..cf6bcdb77ad 100644 --- a/tests/components/fyta/snapshots/test_diagnostics.ambr +++ b/tests/components/fyta/snapshots/test_diagnostics.ambr @@ -23,16 +23,50 @@ }), 'plant_data': dict({ '0': dict({ + 'battery_level': 80.0, + 'battery_status': True, + 'last_updated': '2023-01-10T10:10:00', + 'light': 2.0, + 'light_status': 3, + 'moisture': 61.0, + 'moisture_status': 3, 'name': 'Gummibaum', + 'online': True, + 'ph': None, + 'plant_id': 0, + 'plant_origin_path': '', + 'plant_thumb_path': '', + 'salinity': 1.0, + 'salinity_status': 4, 'scientific_name': 'Ficus elastica', - 'status': 1, + 'sensor_available': True, + 'status': 3, 'sw_version': '1.0', + 'temperature': 25.2, + 'temperature_status': 3, }), '1': dict({ + 'battery_level': 80.0, + 'battery_status': True, + 'last_updated': '2023-01-02T10:10:00', + 'light': 2.0, + 'light_status': 3, + 'moisture': 61.0, + 'moisture_status': 3, 'name': 'Kakaobaum', + 'online': True, + 'ph': 7.0, + 'plant_id': 0, + 'plant_origin_path': '', + 'plant_thumb_path': '', + 'salinity': 1.0, + 'salinity_status': 4, 'scientific_name': 'Theobroma cacao', - 'status': 2, + 'sensor_available': True, + 'status': 3, 'sw_version': '1.0', + 'temperature': 25.2, + 'temperature_status': 3, }), }), }) diff --git a/tests/components/fyta/snapshots/test_sensor.ambr b/tests/components/fyta/snapshots/test_sensor.ambr index 1041fff501e..2e96de0a283 100644 --- a/tests/components/fyta/snapshots/test_sensor.ambr +++ b/tests/components/fyta/snapshots/test_sensor.ambr @@ -1,4 +1,334 @@ # serializer version: 1 +# name: test_all_entities[sensor.gummibaum_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.gummibaum_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-battery_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.gummibaum_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Gummibaum Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.gummibaum_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80.0', + }) +# --- +# name: test_all_entities[sensor.gummibaum_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gummibaum_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-light', + 'unit_of_measurement': 'μmol/s⋅m²', + }) +# --- +# name: test_all_entities[sensor.gummibaum_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Gummibaum Light', + 'state_class': , + 'unit_of_measurement': 'μmol/s⋅m²', + }), + 'context': , + 'entity_id': 'sensor.gummibaum_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.0', + }) +# --- +# name: test_all_entities[sensor.gummibaum_light_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gummibaum_light_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Light state', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light_status', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-light_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.gummibaum_light_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Gummibaum Light state', + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'context': , + 'entity_id': 'sensor.gummibaum_light_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'perfect', + }) +# --- +# name: test_all_entities[sensor.gummibaum_moisture-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gummibaum_moisture', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Moisture', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-moisture', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.gummibaum_moisture-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'moisture', + 'friendly_name': 'Gummibaum Moisture', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.gummibaum_moisture', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '61.0', + }) +# --- +# name: test_all_entities[sensor.gummibaum_moisture_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gummibaum_moisture_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Moisture state', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'moisture_status', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-moisture_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.gummibaum_moisture_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Gummibaum Moisture state', + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'context': , + 'entity_id': 'sensor.gummibaum_moisture_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'perfect', + }) +# --- +# name: test_all_entities[sensor.gummibaum_ph-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gummibaum_ph', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'pH', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-ph', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.gummibaum_ph-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'ph', + 'friendly_name': 'Gummibaum pH', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.gummibaum_ph', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_all_entities[sensor.gummibaum_plant_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -56,7 +386,122 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'doing_great', + 'state': 'no_sensor', + }) +# --- +# name: test_all_entities[sensor.gummibaum_salinity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gummibaum_salinity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Salinity', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'salinity', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-salinity', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.gummibaum_salinity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'conductivity', + 'friendly_name': 'Gummibaum Salinity', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gummibaum_salinity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_all_entities[sensor.gummibaum_salinity_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gummibaum_salinity_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Salinity state', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'salinity_status', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-salinity_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.gummibaum_salinity_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Gummibaum Salinity state', + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'context': , + 'entity_id': 'sensor.gummibaum_salinity_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'high', }) # --- # name: test_all_entities[sensor.gummibaum_scientific_name-entry] @@ -105,6 +550,451 @@ 'state': 'Ficus elastica', }) # --- +# name: test_all_entities[sensor.gummibaum_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gummibaum_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.gummibaum_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Gummibaum Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gummibaum_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.2', + }) +# --- +# name: test_all_entities[sensor.gummibaum_temperature_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gummibaum_temperature_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature state', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_status', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-temperature_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.gummibaum_temperature_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Gummibaum Temperature state', + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'context': , + 'entity_id': 'sensor.gummibaum_temperature_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'perfect', + }) +# --- +# name: test_all_entities[sensor.kakaobaum_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.kakaobaum_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-battery_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.kakaobaum_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Kakaobaum Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.kakaobaum_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80.0', + }) +# --- +# name: test_all_entities[sensor.kakaobaum_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.kakaobaum_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-light', + 'unit_of_measurement': 'μmol/s⋅m²', + }) +# --- +# name: test_all_entities[sensor.kakaobaum_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Kakaobaum Light', + 'state_class': , + 'unit_of_measurement': 'μmol/s⋅m²', + }), + 'context': , + 'entity_id': 'sensor.kakaobaum_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.0', + }) +# --- +# name: test_all_entities[sensor.kakaobaum_light_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.kakaobaum_light_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Light state', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light_status', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-light_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.kakaobaum_light_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Kakaobaum Light state', + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'context': , + 'entity_id': 'sensor.kakaobaum_light_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'perfect', + }) +# --- +# name: test_all_entities[sensor.kakaobaum_moisture-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.kakaobaum_moisture', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Moisture', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-moisture', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.kakaobaum_moisture-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'moisture', + 'friendly_name': 'Kakaobaum Moisture', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.kakaobaum_moisture', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '61.0', + }) +# --- +# name: test_all_entities[sensor.kakaobaum_moisture_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.kakaobaum_moisture_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Moisture state', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'moisture_status', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-moisture_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.kakaobaum_moisture_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Kakaobaum Moisture state', + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'context': , + 'entity_id': 'sensor.kakaobaum_moisture_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'perfect', + }) +# --- +# name: test_all_entities[sensor.kakaobaum_ph-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.kakaobaum_ph', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'pH', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-ph', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.kakaobaum_ph-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'ph', + 'friendly_name': 'Kakaobaum pH', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.kakaobaum_ph', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.0', + }) +# --- # name: test_all_entities[sensor.kakaobaum_plant_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -162,7 +1052,122 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'need_attention', + 'state': 'no_sensor', + }) +# --- +# name: test_all_entities[sensor.kakaobaum_salinity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.kakaobaum_salinity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Salinity', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'salinity', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-salinity', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.kakaobaum_salinity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'conductivity', + 'friendly_name': 'Kakaobaum Salinity', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.kakaobaum_salinity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_all_entities[sensor.kakaobaum_salinity_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.kakaobaum_salinity_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Salinity state', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'salinity_status', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-salinity_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.kakaobaum_salinity_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Kakaobaum Salinity state', + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'context': , + 'entity_id': 'sensor.kakaobaum_salinity_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'high', }) # --- # name: test_all_entities[sensor.kakaobaum_scientific_name-entry] @@ -211,3 +1216,118 @@ 'state': 'Theobroma cacao', }) # --- +# name: test_all_entities[sensor.kakaobaum_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.kakaobaum_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.kakaobaum_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Kakaobaum Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.kakaobaum_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.2', + }) +# --- +# name: test_all_entities[sensor.kakaobaum_temperature_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.kakaobaum_temperature_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature state', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_status', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-temperature_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.kakaobaum_temperature_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Kakaobaum Temperature state', + 'options': list([ + 'no_data', + 'too_low', + 'low', + 'perfect', + 'high', + 'too_high', + ]), + }), + 'context': , + 'entity_id': 'sensor.kakaobaum_temperature_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'perfect', + }) +# --- From 9b11aaf1eb8731638976c31edd1fd541e144d9b8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 Aug 2024 19:00:44 +0200 Subject: [PATCH 2405/2411] Add missing hass type hint in alexa tests (#124064) * Add missing hass type hint in alexa tests * One more --- tests/components/alexa/test_auth.py | 16 +++--- tests/components/alexa/test_common.py | 53 +++++++++++++------ tests/components/alexa/test_smart_home.py | 26 +++++++-- .../components/alexa/test_smart_home_http.py | 5 +- 4 files changed, 69 insertions(+), 31 deletions(-) diff --git a/tests/components/alexa/test_auth.py b/tests/components/alexa/test_auth.py index 8d4308ba792..b3aa645bfcb 100644 --- a/tests/components/alexa/test_auth.py +++ b/tests/components/alexa/test_auth.py @@ -10,14 +10,14 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def run_auth_get_access_token( - hass, - aioclient_mock, - expires_in, - client_id, - client_secret, - accept_grant_code, - refresh_token, -): + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + expires_in: int, + client_id: str, + client_secret: str, + accept_grant_code: str, + refresh_token: str, +) -> None: """Do auth and request a new token for tests.""" aioclient_mock.post( TEST_TOKEN_URL, diff --git a/tests/components/alexa/test_common.py b/tests/components/alexa/test_common.py index 43e7d77ce71..e78f2cba40f 100644 --- a/tests/components/alexa/test_common.py +++ b/tests/components/alexa/test_common.py @@ -1,5 +1,8 @@ """Test helpers for the Alexa integration.""" +from __future__ import annotations + +from typing import Any from unittest.mock import Mock from uuid import uuid4 @@ -7,7 +10,7 @@ import pytest from homeassistant.components.alexa import config, smart_home from homeassistant.components.alexa.const import CONF_ENDPOINT, CONF_FILTER, CONF_LOCALE -from homeassistant.core import Context, HomeAssistant, callback +from homeassistant.core import Context, HomeAssistant, ServiceCall, callback from homeassistant.helpers import entityfilter from tests.common import async_mock_service @@ -62,7 +65,7 @@ class MockConfig(smart_home.AlexaConfig): """Accept a grant.""" -def get_default_config(hass): +def get_default_config(hass: HomeAssistant) -> MockConfig: """Return a MockConfig instance.""" return MockConfig(hass) @@ -93,15 +96,15 @@ def get_new_request(namespace, name, endpoint=None): async def assert_request_calls_service( - namespace, - name, - endpoint, - service, - hass, + namespace: str, + name: str, + endpoint: str, + service: str, + hass: HomeAssistant, response_type="Response", - payload=None, - instance=None, -): + payload: dict[str, Any] | None = None, + instance: str | None = None, +) -> tuple[ServiceCall, dict[str, Any]]: """Assert an API request calls a hass service.""" context = Context() request = get_new_request(namespace, name, endpoint) @@ -129,8 +132,14 @@ async def assert_request_calls_service( async def assert_request_fails( - namespace, name, endpoint, service_not_called, hass, payload=None, instance=None -): + namespace: str, + name: str, + endpoint: str, + service_not_called: str, + hass: HomeAssistant, + payload: dict[str, Any] | None = None, + instance: str | None = None, +) -> None: """Assert an API request returns an ErrorResponse.""" request = get_new_request(namespace, name, endpoint) if payload: @@ -152,8 +161,12 @@ async def assert_request_fails( async def assert_power_controller_works( - endpoint, on_service, off_service, hass, timestamp -): + endpoint: str, + on_service: str, + off_service: str, + hass: HomeAssistant, + timestamp: str, +) -> None: """Assert PowerController API requests work.""" _, response = await assert_request_calls_service( "Alexa.PowerController", "TurnOn", endpoint, on_service, hass @@ -169,8 +182,12 @@ async def assert_power_controller_works( async def assert_scene_controller_works( - endpoint, activate_service, deactivate_service, hass, timestamp -): + endpoint: str, + activate_service: str, + deactivate_service: str, + hass: HomeAssistant, + timestamp: str, +) -> None: """Assert SceneController API requests work.""" _, response = await assert_request_calls_service( "Alexa.SceneController", @@ -196,7 +213,9 @@ async def assert_scene_controller_works( assert response["event"]["payload"]["timestamp"] == timestamp -async def reported_properties(hass, endpoint, return_full_response=False): +async def reported_properties( + hass: HomeAssistant, endpoint: str, return_full_response: bool = False +) -> ReportedProperties: """Use ReportState to get properties and return them. The result is a ReportedProperties instance, which has methods to make diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index fb27c91eea7..6ccf265dcdc 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -120,7 +120,9 @@ async def test_wrong_version(hass: HomeAssistant) -> None: await smart_home.async_handle_message(hass, get_default_config(hass), msg) -async def discovery_test(device, hass, expected_endpoints=1): +async def discovery_test( + device, hass: HomeAssistant, expected_endpoints: int = 1 +) -> dict[str, Any] | list[dict[str, Any]] | None: """Test alexa discovery request.""" request = get_new_request("Alexa.Discovery", "Discover") @@ -2601,8 +2603,15 @@ async def test_stop_valve( async def assert_percentage_changes( - hass, adjustments, namespace, name, endpoint, parameter, service, changed_parameter -): + hass: HomeAssistant, + adjustments, + namespace, + name, + endpoint, + parameter, + service, + changed_parameter, +) -> None: """Assert an API request making percentage changes works. AdjustPercentage, AdjustBrightness, etc. are examples of such requests. @@ -2616,8 +2625,15 @@ async def assert_percentage_changes( async def assert_range_changes( - hass, adjustments, namespace, name, endpoint, service, changed_parameter, instance -): + hass: HomeAssistant, + adjustments: list[tuple[int | str, int, bool]], + namespace: str, + name: str, + endpoint: str, + service: str, + changed_parameter: str | None, + instance: str, +) -> None: """Assert an API request making range changes works. AdjustRangeValue are examples of such requests. diff --git a/tests/components/alexa/test_smart_home_http.py b/tests/components/alexa/test_smart_home_http.py index 1c30c72e72c..20d9b30dda5 100644 --- a/tests/components/alexa/test_smart_home_http.py +++ b/tests/components/alexa/test_smart_home_http.py @@ -5,6 +5,7 @@ import json import logging from typing import Any +from aiohttp import ClientResponse import pytest from homeassistant.components.alexa import DOMAIN, smart_home @@ -17,7 +18,9 @@ from .test_common import get_new_request from tests.typing import ClientSessionGenerator -async def do_http_discovery(config, hass, hass_client): +async def do_http_discovery( + config: dict[str, Any], hass: HomeAssistant, hass_client: ClientSessionGenerator +) -> ClientResponse: """Submit a request to the Smart Home HTTP API.""" await async_setup_component(hass, DOMAIN, config) http_client = await hass_client() From a8a7d01a845ef6fbf34c5a8bb99de7aa569a1a1b Mon Sep 17 00:00:00 2001 From: Kim de Vos Date: Fri, 16 Aug 2024 21:37:24 +0200 Subject: [PATCH 2406/2411] Add temperature sensors for unifi device (#122518) * Add temperature sensors for device * Move to single line * Use right reference * Always return a value * Update tests * Use slugify for id name * Return default value if not present * Make _device_temperature return value * Add default value if temperatures is None * Set value to go over all code paths * Add test for no matching temperatures * make first part deterministic --- homeassistant/components/unifi/sensor.py | 74 ++++++++++- tests/components/unifi/test_sensor.py | 162 +++++++++++++++++++++++ 2 files changed, 234 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 08bd0ddb869..39e487c0d57 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -21,7 +21,11 @@ from aiounifi.interfaces.ports import Ports from aiounifi.interfaces.wlans import Wlans from aiounifi.models.api import ApiItemT from aiounifi.models.client import Client -from aiounifi.models.device import Device, TypedDeviceUptimeStatsWanMonitor +from aiounifi.models.device import ( + Device, + TypedDeviceTemperature, + TypedDeviceUptimeStatsWanMonitor, +) from aiounifi.models.outlet import Outlet from aiounifi.models.port import Port from aiounifi.models.wlan import Wlan @@ -280,6 +284,72 @@ def make_wan_latency_sensors() -> tuple[UnifiSensorEntityDescription, ...]: ) +@callback +def async_device_temperatures_value_fn( + temperature_name: str, hub: UnifiHub, device: Device +) -> float: + """Retrieve the temperature of the device.""" + return_value: float = 0 + if device.temperatures: + temperature = _device_temperature(temperature_name, device.temperatures) + return_value = temperature if temperature is not None else 0 + return return_value + + +@callback +def async_device_temperatures_supported_fn( + temperature_name: str, hub: UnifiHub, obj_id: str +) -> bool: + """Determine if an device have a temperatures.""" + if (device := hub.api.devices[obj_id]) and device.temperatures: + return _device_temperature(temperature_name, device.temperatures) is not None + return False + + +@callback +def _device_temperature( + temperature_name: str, temperatures: list[TypedDeviceTemperature] +) -> float | None: + """Return the temperature of the device.""" + for temperature in temperatures: + if temperature_name in temperature["name"]: + return temperature["value"] + return None + + +def make_device_temperatur_sensors() -> tuple[UnifiSensorEntityDescription, ...]: + """Create device temperature sensors.""" + + def make_device_temperature_entity_description( + name: str, + ) -> UnifiSensorEntityDescription: + return UnifiSensorEntityDescription[Devices, Device]( + key=f"Device {name} temperature", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_registry_enabled_default=False, + api_handler_fn=lambda api: api.devices, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + name_fn=lambda device: f"{device.name} {name} Temperature", + object_fn=lambda api, obj_id: api.devices[obj_id], + supported_fn=partial(async_device_temperatures_supported_fn, name), + unique_id_fn=lambda hub, obj_id: f"temperature-{slugify(name)}-{obj_id}", + value_fn=partial(async_device_temperatures_value_fn, name), + ) + + return tuple( + make_device_temperature_entity_description(name) + for name in ( + "CPU", + "Local", + "PHY", + ) + ) + + @dataclass(frozen=True, kw_only=True) class UnifiSensorEntityDescription( SensorEntityDescription, UnifiEntityDescription[HandlerT, ApiItemT] @@ -544,7 +614,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( ), ) -ENTITY_DESCRIPTIONS += make_wan_latency_sensors() +ENTITY_DESCRIPTIONS += make_wan_latency_sensors() + make_device_temperatur_sensors() async def async_setup_entry( diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index cc51c31fc8b..afa256c087e 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1519,3 +1519,165 @@ async def test_wan_monitor_latency_with_no_uptime( latency_entry = entity_registry.async_get("sensor.mock_name_google_wan_latency") assert latency_entry is None + + +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "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, + "uptime": 60, + "version": "4.0.42.10433", + "temperatures": [ + {"name": "CPU", "type": "cpu", "value": 66.0}, + {"name": "Local", "type": "board", "value": 48.75}, + {"name": "PHY", "type": "board", "value": 50.25}, + ], + } + ] + ], +) +@pytest.mark.parametrize( + ("temperature_id", "state", "updated_state", "index_to_update"), + [ + ("device_cpu", "66.0", "20", 0), + ("device_local", "48.75", "90.64", 1), + ("device_phy", "50.25", "80", 2), + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_device_temperatures( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_websocket_message, + device_payload: list[dict[str, Any]], + temperature_id: str, + state: str, + updated_state: str, + index_to_update: int, +) -> None: + """Verify that device temperatures sensors are working as expected.""" + + entity_id = f"sensor.device_{temperature_id}_temperature" + + assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + + temperature_entity = entity_registry.async_get(entity_id) + assert temperature_entity.disabled_by == RegistryEntryDisabler.INTEGRATION + + # Enable entity + entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) + + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 7 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 + + # Verify sensor state + assert hass.states.get(entity_id).state == state + + # # Verify state update + device = device_payload[0] + device["temperatures"][index_to_update]["value"] = updated_state + + mock_websocket_message(message=MessageKey.DEVICE, data=device) + + assert hass.states.get(entity_id).state == updated_state + + +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 2, + "device_id": "mock-id", + "ip": "10.0.1.1", + "mac": "10:00:00:00:01:01", + "last_seen": 1562600145, + "model": "US16P150", + "name": "mock-name", + "port_overrides": [], + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + } + ] + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_device_with_no_temperature( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Verify that device temperature sensors is not created if there is no data.""" + + assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + + temperature_entity = entity_registry.async_get( + "sensor.device_device_cpu_temperature" + ) + + assert temperature_entity is None + + +@pytest.mark.parametrize( + "device_payload", + [ + [ + { + "board_rev": 2, + "device_id": "mock-id", + "ip": "10.0.1.1", + "mac": "10:00:00:00:01:01", + "last_seen": 1562600145, + "model": "US16P150", + "name": "mock-name", + "port_overrides": [], + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + "temperatures": [ + {"name": "MEM", "type": "mem", "value": 66.0}, + ], + } + ] + ], +) +@pytest.mark.usefixtures("config_entry_setup") +async def test_device_with_no_matching_temperatures( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Verify that device temperature sensors is not created if there is no matching data.""" + + assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + + temperature_entity = entity_registry.async_get( + "sensor.device_device_cpu_temperature" + ) + + assert temperature_entity is None From 24680b731faef233b3dc6e713d27795514ee139f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 Aug 2024 21:51:58 +0200 Subject: [PATCH 2407/2411] Add missing hass type hint in component tests (f) (#124076) --- tests/components/ffmpeg/test_init.py | 6 +++--- tests/components/flick_electric/test_config_flow.py | 3 ++- tests/components/fronius/__init__.py | 10 +++++++++- tests/components/fully_kiosk/test_number.py | 8 +++++--- tests/components/fully_kiosk/test_switch.py | 8 +++++--- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index e9a781327e0..aa407d5b695 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -22,7 +22,7 @@ from tests.common import assert_setup_component @callback -def async_start(hass, entity_id=None): +def async_start(hass: HomeAssistant, entity_id: str | None = None) -> None: """Start a FFmpeg process on entity. This is a legacy helper method. Do not use it for new tests. @@ -32,7 +32,7 @@ def async_start(hass, entity_id=None): @callback -def async_stop(hass, entity_id=None): +def async_stop(hass: HomeAssistant, entity_id: str | None = None) -> None: """Stop a FFmpeg process on entity. This is a legacy helper method. Do not use it for new tests. @@ -42,7 +42,7 @@ def async_stop(hass, entity_id=None): @callback -def async_restart(hass, entity_id=None): +def async_restart(hass: HomeAssistant, entity_id: str | None = None) -> None: """Restart a FFmpeg process on entity. This is a legacy helper method. Do not use it for new tests. diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py index 1b3ed1de34d..85a6495d3c5 100644 --- a/tests/components/flick_electric/test_config_flow.py +++ b/tests/components/flick_electric/test_config_flow.py @@ -6,6 +6,7 @@ from pyflick.authentication import AuthException from homeassistant import config_entries from homeassistant.components.flick_electric.const import DOMAIN +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -15,7 +16,7 @@ from tests.common import MockConfigEntry CONF = {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} -async def _flow_submit(hass): +async def _flow_submit(hass: HomeAssistant) -> ConfigFlowResult: return await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, diff --git a/tests/components/fronius/__init__.py b/tests/components/fronius/__init__.py index 2109d4a6692..57b22490ed0 100644 --- a/tests/components/fronius/__init__.py +++ b/tests/components/fronius/__init__.py @@ -3,9 +3,12 @@ from __future__ import annotations from collections.abc import Callable +from datetime import timedelta import json from typing import Any +from freezegun.api import FrozenDateTimeFactory + from homeassistant.components.fronius.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST @@ -114,7 +117,12 @@ def mock_responses( ) -async def enable_all_entities(hass, freezer, config_entry_id, time_till_next_update): +async def enable_all_entities( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + config_entry_id: str, + time_till_next_update: timedelta, +) -> None: """Enable all entities for a config entry and fast forward time to receive data.""" registry = er.async_get(hass) entities = er.async_entries_for_config_entry(registry, config_entry_id) diff --git a/tests/components/fully_kiosk/test_number.py b/tests/components/fully_kiosk/test_number.py index 2fbbf751725..5f74002f8cd 100644 --- a/tests/components/fully_kiosk/test_number.py +++ b/tests/components/fully_kiosk/test_number.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock from homeassistant.components import number from homeassistant.components.fully_kiosk.const import DOMAIN, UPDATE_INTERVAL from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceResponse from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt as dt_util @@ -81,9 +81,11 @@ async def test_numbers( assert device_entry.sw_version == "1.42.5" -def set_value(hass, entity_id, value): +async def set_value( + hass: HomeAssistant, entity_id: str, value: float +) -> ServiceResponse: """Set the value of a number entity.""" - return hass.services.async_call( + return await hass.services.async_call( number.DOMAIN, "set_value", {ATTR_ENTITY_ID: entity_id, number.ATTR_VALUE: value}, diff --git a/tests/components/fully_kiosk/test_switch.py b/tests/components/fully_kiosk/test_switch.py index 5b3b5e651b0..14a464e0dcd 100644 --- a/tests/components/fully_kiosk/test_switch.py +++ b/tests/components/fully_kiosk/test_switch.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock from homeassistant.components import switch from homeassistant.components.fully_kiosk.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceResponse from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry, async_fire_mqtt_message @@ -149,8 +149,10 @@ def has_subscribed(mqtt_mock: MqttMockHAClient, topic: str) -> bool: return False -def call_service(hass, service, entity_id): +async def call_service( + hass: HomeAssistant, service: str, entity_id: str +) -> ServiceResponse: """Call any service on entity.""" - return hass.services.async_call( + return await hass.services.async_call( switch.DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True ) From 91951ed7347022f1ccaf87ece9320aa43c8d2182 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Aug 2024 16:48:03 -0500 Subject: [PATCH 2408/2411] Speed up initializing config flows (#124015) --- homeassistant/config_entries.py | 43 +++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 75b0631339f..e48313cab33 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections import UserDict +from collections import UserDict, defaultdict from collections.abc import ( Callable, Coroutine, @@ -1224,8 +1224,12 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): super().__init__(hass) self.config_entries = config_entries self._hass_config = hass_config - self._pending_import_flows: dict[str, dict[str, asyncio.Future[None]]] = {} - self._initialize_futures: dict[str, list[asyncio.Future[None]]] = {} + self._pending_import_flows: defaultdict[ + str, dict[str, asyncio.Future[None]] + ] = defaultdict(dict) + self._initialize_futures: defaultdict[str, set[asyncio.Future[None]]] = ( + defaultdict(set) + ) self._discovery_debouncer = Debouncer[None]( hass, _LOGGER, @@ -1278,12 +1282,11 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): loop = self.hass.loop if context["source"] == SOURCE_IMPORT: - self._pending_import_flows.setdefault(handler, {})[flow_id] = ( - loop.create_future() - ) + self._pending_import_flows[handler][flow_id] = loop.create_future() cancel_init_future = loop.create_future() - self._initialize_futures.setdefault(handler, []).append(cancel_init_future) + handler_init_futures = self._initialize_futures[handler] + handler_init_futures.add(cancel_init_future) try: async with interrupt( cancel_init_future, @@ -1294,8 +1297,13 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): except FlowCancelledError as ex: raise asyncio.CancelledError from ex finally: - self._initialize_futures[handler].remove(cancel_init_future) - self._pending_import_flows.get(handler, {}).pop(flow_id, None) + handler_init_futures.remove(cancel_init_future) + if not handler_init_futures: + del self._initialize_futures[handler] + if handler in self._pending_import_flows: + self._pending_import_flows[handler].pop(flow_id, None) + if not self._pending_import_flows[handler]: + del self._pending_import_flows[handler] if result["type"] != data_entry_flow.FlowResultType.ABORT: await self.async_post_init(flow, result) @@ -1322,11 +1330,18 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): try: result = await self._async_handle_step(flow, flow.init_step, data) finally: - init_done = self._pending_import_flows.get(handler, {}).get(flow_id) - if init_done and not init_done.done(): - init_done.set_result(None) + self._set_pending_import_done(flow) return flow, result + def _set_pending_import_done(self, flow: ConfigFlow) -> None: + """Set pending import flow as done.""" + if ( + (handler_import_flows := self._pending_import_flows.get(flow.handler)) + and (init_done := handler_import_flows.get(flow.flow_id)) + and not init_done.done() + ): + init_done.set_result(None) + @callback def async_shutdown(self) -> None: """Cancel any initializing flows.""" @@ -1347,9 +1362,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): # We do this to avoid a circular dependency where async_finish_flow sets up a # new entry, which needs the integration to be set up, which is waiting for # init to be done. - init_done = self._pending_import_flows.get(flow.handler, {}).get(flow.flow_id) - if init_done and not init_done.done(): - init_done.set_result(None) + self._set_pending_import_done(flow) # Remove notification if no other discovery config entries in progress if not self._async_has_other_discovery_flows(flow.flow_id): From 69700f068f589ed028c8ec9791c67a2809da9589 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 16 Aug 2024 23:57:10 +0200 Subject: [PATCH 2409/2411] Migrate back from `pysnmp-lextudio` to `pysnmp` (#123579) --- homeassistant/components/brother/manifest.json | 2 +- homeassistant/components/snmp/manifest.json | 2 +- homeassistant/package_constraints.txt | 3 --- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- script/gen_requirements_all.py | 3 --- tests/components/snmp/test_integer_sensor.py | 2 +- tests/components/snmp/test_negative_sensor.py | 2 +- tests/components/snmp/test_switch.py | 2 +- 9 files changed, 9 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 5caaeb2f1a1..d9c8e36aa1d 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_polling", "loggers": ["brother", "pyasn1", "pysmi", "pysnmp"], "quality_scale": "platinum", - "requirements": ["brother==4.2.0"], + "requirements": ["brother==4.3.0"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index d79910c44cd..c3970e1e00a 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/snmp", "iot_class": "local_polling", "loggers": ["pyasn1", "pysmi", "pysnmp"], - "requirements": ["pysnmp-lextudio==6.0.11"] + "requirements": ["pysnmp==6.2.5"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 900d4510c17..1e90333a198 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -156,9 +156,6 @@ websockets>=11.0.1 # pysnmplib is no longer maintained and does not work with newer # python pysnmplib==1000000000.0.0 -# pysnmp is no longer maintained and does not work with newer -# python -pysnmp==1000000000.0.0 # The get-mac package has been replaced with getmac. Installing get-mac alongside getmac # breaks getmac due to them both sharing the same python package name inside 'getmac'. diff --git a/requirements_all.txt b/requirements_all.txt index 59b8b35730e..ad750c5ec4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -622,7 +622,7 @@ bring-api==0.8.1 broadlink==0.19.0 # homeassistant.components.brother -brother==4.2.0 +brother==4.3.0 # homeassistant.components.brottsplatskartan brottsplatskartan==1.0.5 @@ -2208,7 +2208,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.snmp -pysnmp-lextudio==6.0.11 +pysnmp==6.2.5 # homeassistant.components.snooz pysnooz==0.8.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 90ea1d2c806..808f93b6ad5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -542,7 +542,7 @@ bring-api==0.8.1 broadlink==0.19.0 # homeassistant.components.brother -brother==4.2.0 +brother==4.3.0 # homeassistant.components.brottsplatskartan brottsplatskartan==1.0.5 @@ -1762,7 +1762,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.snmp -pysnmp-lextudio==6.0.11 +pysnmp==6.2.5 # homeassistant.components.snooz pysnooz==0.8.6 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 522a626754d..6ce97468699 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -178,9 +178,6 @@ websockets>=11.0.1 # pysnmplib is no longer maintained and does not work with newer # python pysnmplib==1000000000.0.0 -# pysnmp is no longer maintained and does not work with newer -# python -pysnmp==1000000000.0.0 # The get-mac package has been replaced with getmac. Installing get-mac alongside getmac # breaks getmac due to them both sharing the same python package name inside 'getmac'. diff --git a/tests/components/snmp/test_integer_sensor.py b/tests/components/snmp/test_integer_sensor.py index dab2b080c97..8e7e0f166ef 100644 --- a/tests/components/snmp/test_integer_sensor.py +++ b/tests/components/snmp/test_integer_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from pysnmp.hlapi import Integer32 +from pysnmp.proto.rfc1902 import Integer32 import pytest from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN diff --git a/tests/components/snmp/test_negative_sensor.py b/tests/components/snmp/test_negative_sensor.py index dba09ea75bd..66a111b68d0 100644 --- a/tests/components/snmp/test_negative_sensor.py +++ b/tests/components/snmp/test_negative_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from pysnmp.hlapi import Integer32 +from pysnmp.proto.rfc1902 import Integer32 import pytest from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN diff --git a/tests/components/snmp/test_switch.py b/tests/components/snmp/test_switch.py index adb9d1c59d0..fe1c3922ff0 100644 --- a/tests/components/snmp/test_switch.py +++ b/tests/components/snmp/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from pysnmp.hlapi import Integer32 +from pysnmp.proto.rfc1902 import Integer32 import pytest from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN From 1614e2c825f796c2e195ac384dcf3ca3ed1acb68 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 Aug 2024 23:58:42 +0200 Subject: [PATCH 2410/2411] Use constants in Sonos media player tests (#124037) --- tests/components/sonos/test_media_player.py | 145 ++++++++++---------- 1 file changed, 76 insertions(+), 69 deletions(-) diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 599a04b806a..ddf84efd7da 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -7,7 +7,13 @@ from unittest.mock import patch import pytest from homeassistant.components.media_player import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_ENQUEUE, + ATTR_MEDIA_REPEAT, + ATTR_MEDIA_SHUFFLE, + ATTR_MEDIA_VOLUME_LEVEL, DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, @@ -26,6 +32,7 @@ from homeassistant.components.sonos.media_player import ( VOLUME_INCREMENT, ) from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_REPEAT_SET, SERVICE_SHUFFLE_SET, SERVICE_VOLUME_DOWN, @@ -176,9 +183,9 @@ async def test_play_media_library( MP_DOMAIN, SERVICE_PLAY_MEDIA, { - "entity_id": "media_player.zone_a", - "media_content_type": media_content_type, - "media_content_id": media_content_id, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: media_content_type, + ATTR_MEDIA_CONTENT_ID: media_content_id, ATTR_MEDIA_ENQUEUE: enqueue, }, blocking=True, @@ -225,9 +232,9 @@ async def test_play_media_lib_track_play( MP_DOMAIN, SERVICE_PLAY_MEDIA, { - "entity_id": "media_player.zone_a", - "media_content_type": "track", - "media_content_id": _track_url, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "track", + ATTR_MEDIA_CONTENT_ID: _track_url, ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.PLAY, }, blocking=True, @@ -254,9 +261,9 @@ async def test_play_media_lib_track_next( MP_DOMAIN, SERVICE_PLAY_MEDIA, { - "entity_id": "media_player.zone_a", - "media_content_type": "track", - "media_content_id": _track_url, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "track", + ATTR_MEDIA_CONTENT_ID: _track_url, ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.NEXT, }, blocking=True, @@ -282,9 +289,9 @@ async def test_play_media_lib_track_replace( MP_DOMAIN, SERVICE_PLAY_MEDIA, { - "entity_id": "media_player.zone_a", - "media_content_type": "track", - "media_content_id": _track_url, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "track", + ATTR_MEDIA_CONTENT_ID: _track_url, ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.REPLACE, }, blocking=True, @@ -305,9 +312,9 @@ async def test_play_media_lib_track_add( MP_DOMAIN, SERVICE_PLAY_MEDIA, { - "entity_id": "media_player.zone_a", - "media_content_type": "track", - "media_content_id": _track_url, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "track", + ATTR_MEDIA_CONTENT_ID: _track_url, ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.ADD, }, blocking=True, @@ -335,9 +342,9 @@ async def test_play_media_share_link_add( MP_DOMAIN, SERVICE_PLAY_MEDIA, { - "entity_id": "media_player.zone_a", - "media_content_type": "playlist", - "media_content_id": _share_link, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "playlist", + ATTR_MEDIA_CONTENT_ID: _share_link, ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.ADD, }, blocking=True, @@ -363,9 +370,9 @@ async def test_play_media_share_link_next( MP_DOMAIN, SERVICE_PLAY_MEDIA, { - "entity_id": "media_player.zone_a", - "media_content_type": "playlist", - "media_content_id": _share_link, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "playlist", + ATTR_MEDIA_CONTENT_ID: _share_link, ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.NEXT, }, blocking=True, @@ -395,9 +402,9 @@ async def test_play_media_share_link_play( MP_DOMAIN, SERVICE_PLAY_MEDIA, { - "entity_id": "media_player.zone_a", - "media_content_type": "playlist", - "media_content_id": _share_link, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "playlist", + ATTR_MEDIA_CONTENT_ID: _share_link, ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.PLAY, }, blocking=True, @@ -429,9 +436,9 @@ async def test_play_media_share_link_replace( MP_DOMAIN, SERVICE_PLAY_MEDIA, { - "entity_id": "media_player.zone_a", - "media_content_type": "playlist", - "media_content_id": _share_link, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "playlist", + ATTR_MEDIA_CONTENT_ID: _share_link, ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.REPLACE, }, blocking=True, @@ -494,9 +501,9 @@ async def test_play_media_music_library_playlist( MP_DOMAIN, SERVICE_PLAY_MEDIA, { - "entity_id": "media_player.zone_a", - "media_content_type": "playlist", - "media_content_id": media_content_id, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "playlist", + ATTR_MEDIA_CONTENT_ID: media_content_id, }, blocking=True, ) @@ -524,9 +531,9 @@ async def test_play_media_music_library_playlist_dne( MP_DOMAIN, SERVICE_PLAY_MEDIA, { - "entity_id": "media_player.zone_a", - "media_content_type": "playlist", - "media_content_id": media_content_id, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "playlist", + ATTR_MEDIA_CONTENT_ID: media_content_id, }, blocking=True, ) @@ -565,8 +572,8 @@ async def test_select_source_line_in_tv( MP_DOMAIN, SERVICE_SELECT_SOURCE, { - "entity_id": "media_player.zone_a", - "source": source, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_INPUT_SOURCE: source, }, blocking=True, ) @@ -608,8 +615,8 @@ async def test_select_source_play_uri( MP_DOMAIN, SERVICE_SELECT_SOURCE, { - "entity_id": "media_player.zone_a", - "source": source, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_INPUT_SOURCE: source, }, blocking=True, ) @@ -648,8 +655,8 @@ async def test_select_source_play_queue( MP_DOMAIN, SERVICE_SELECT_SOURCE, { - "entity_id": "media_player.zone_a", - "source": source, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_INPUT_SOURCE: source, }, blocking=True, ) @@ -677,8 +684,8 @@ async def test_select_source_error( MP_DOMAIN, SERVICE_SELECT_SOURCE, { - "entity_id": "media_player.zone_a", - "source": "invalid_source", + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_INPUT_SOURCE: "invalid_source", }, blocking=True, ) @@ -698,8 +705,8 @@ async def test_shuffle_set( MP_DOMAIN, SERVICE_SHUFFLE_SET, { - "entity_id": "media_player.zone_a", - "shuffle": True, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_SHUFFLE: True, }, blocking=True, ) @@ -709,8 +716,8 @@ async def test_shuffle_set( MP_DOMAIN, SERVICE_SHUFFLE_SET, { - "entity_id": "media_player.zone_a", - "shuffle": False, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_SHUFFLE: False, }, blocking=True, ) @@ -728,13 +735,13 @@ async def test_shuffle_get( sub_callback = subscription.callback state = hass.states.get("media_player.zone_a") - assert state.attributes["shuffle"] is False + assert state.attributes[ATTR_MEDIA_SHUFFLE] is False no_media_event.variables["current_play_mode"] = "SHUFFLE_NOREPEAT" sub_callback(no_media_event) await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("media_player.zone_a") - assert state.attributes["shuffle"] is True + assert state.attributes[ATTR_MEDIA_SHUFFLE] is True # The integration keeps a copy of the last event to check for # changes, so we create a new event. @@ -745,7 +752,7 @@ async def test_shuffle_get( sub_callback(no_media_event) await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("media_player.zone_a") - assert state.attributes["shuffle"] is False + assert state.attributes[ATTR_MEDIA_SHUFFLE] is False async def test_repeat_set( @@ -759,8 +766,8 @@ async def test_repeat_set( MP_DOMAIN, SERVICE_REPEAT_SET, { - "entity_id": "media_player.zone_a", - "repeat": RepeatMode.ALL, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_REPEAT: RepeatMode.ALL, }, blocking=True, ) @@ -770,8 +777,8 @@ async def test_repeat_set( MP_DOMAIN, SERVICE_REPEAT_SET, { - "entity_id": "media_player.zone_a", - "repeat": RepeatMode.ONE, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_REPEAT: RepeatMode.ONE, }, blocking=True, ) @@ -781,8 +788,8 @@ async def test_repeat_set( MP_DOMAIN, SERVICE_REPEAT_SET, { - "entity_id": "media_player.zone_a", - "repeat": RepeatMode.OFF, + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_REPEAT: RepeatMode.OFF, }, blocking=True, ) @@ -800,13 +807,13 @@ async def test_repeat_get( sub_callback = subscription.callback state = hass.states.get("media_player.zone_a") - assert state.attributes["repeat"] == RepeatMode.OFF + assert state.attributes[ATTR_MEDIA_REPEAT] == RepeatMode.OFF no_media_event.variables["current_play_mode"] = "REPEAT_ALL" sub_callback(no_media_event) await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("media_player.zone_a") - assert state.attributes["repeat"] == RepeatMode.ALL + assert state.attributes[ATTR_MEDIA_REPEAT] == RepeatMode.ALL no_media_event = SonosMockEvent( soco, soco.avTransport, no_media_event.variables.copy() @@ -815,7 +822,7 @@ async def test_repeat_get( sub_callback(no_media_event) await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("media_player.zone_a") - assert state.attributes["repeat"] == RepeatMode.ONE + assert state.attributes[ATTR_MEDIA_REPEAT] == RepeatMode.ONE no_media_event = SonosMockEvent( soco, soco.avTransport, no_media_event.variables.copy() @@ -824,7 +831,7 @@ async def test_repeat_get( sub_callback(no_media_event) await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("media_player.zone_a") - assert state.attributes["repeat"] == RepeatMode.OFF + assert state.attributes[ATTR_MEDIA_REPEAT] == RepeatMode.OFF async def test_play_media_favorite_item_id( @@ -838,9 +845,9 @@ async def test_play_media_favorite_item_id( MP_DOMAIN, SERVICE_PLAY_MEDIA, { - "entity_id": "media_player.zone_a", - "media_content_type": "favorite_item_id", - "media_content_id": "FV:2/4", + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "favorite_item_id", + ATTR_MEDIA_CONTENT_ID: "FV:2/4", }, blocking=True, ) @@ -860,9 +867,9 @@ async def test_play_media_favorite_item_id( MP_DOMAIN, SERVICE_PLAY_MEDIA, { - "entity_id": "media_player.zone_a", - "media_content_type": "favorite_item_id", - "media_content_id": "UNKNOWN_ID", + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "favorite_item_id", + ATTR_MEDIA_CONTENT_ID: "UNKNOWN_ID", }, blocking=True, ) @@ -900,7 +907,7 @@ async def test_service_snapshot_restore( SONOS_DOMAIN, SERVICE_SNAPSHOT, { - "entity_id": ["media_player.living_room", "media_player.bedroom"], + ATTR_ENTITY_ID: ["media_player.living_room", "media_player.bedroom"], }, blocking=True, ) @@ -913,7 +920,7 @@ async def test_service_snapshot_restore( SONOS_DOMAIN, SERVICE_RESTORE, { - "entity_id": ["media_player.living_room", "media_player.bedroom"], + ATTR_ENTITY_ID: ["media_player.living_room", "media_player.bedroom"], }, blocking=True, ) @@ -932,7 +939,7 @@ async def test_volume( MP_DOMAIN, SERVICE_VOLUME_UP, { - "entity_id": "media_player.zone_a", + ATTR_ENTITY_ID: "media_player.zone_a", }, blocking=True, ) @@ -942,7 +949,7 @@ async def test_volume( MP_DOMAIN, SERVICE_VOLUME_DOWN, { - "entity_id": "media_player.zone_a", + ATTR_ENTITY_ID: "media_player.zone_a", }, blocking=True, ) @@ -951,7 +958,7 @@ async def test_volume( await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_SET, - {"entity_id": "media_player.zone_a", "volume_level": 0.30}, + {ATTR_ENTITY_ID: "media_player.zone_a", ATTR_MEDIA_VOLUME_LEVEL: 0.30}, blocking=True, ) # SoCo uses 0..100 for its range. From 7deb9bf30f2d2ddb2b0d06baefa22ae4baa2178b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Aug 2024 23:59:21 +0200 Subject: [PATCH 2411/2411] Do not override hass.loop_thread_id in tests (#124053) --- tests/common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/common.py b/tests/common.py index 684b9eb0433..2f0c032616a 100644 --- a/tests/common.py +++ b/tests/common.py @@ -208,7 +208,6 @@ def get_test_home_assistant() -> Generator[HomeAssistant]: hass_created_event.set() - hass.loop_thread_id = loop._thread_ident loop.run_forever() loop_stop_event.set()